diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index aac499d008c28..57cda7d0940a2 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1,3510 +1,3510 @@ /* @internal */ namespace ts { - export const enum ModuleInstanceState { - NonInstantiated = 0, - Instantiated = 1, - ConstEnumOnly = 2 - } +export const enum ModuleInstanceState { + NonInstantiated = 0, + Instantiated = 1, + ConstEnumOnly = 2 +} - interface ActiveLabel { - next: ActiveLabel | undefined; - name: ts.__String; - breakTarget: ts.FlowLabel; - continueTarget: ts.FlowLabel | undefined; - referenced: boolean; - } +interface ActiveLabel { + next: ActiveLabel | undefined; + name: ts.__String; + breakTarget: ts.FlowLabel; + continueTarget: ts.FlowLabel | undefined; + referenced: boolean; +} - export function getModuleInstanceState(node: ts.ModuleDeclaration, visited?: ts.ESMap): 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 - ts.setParent(node.body, node); - ts.setParentRecursive(node.body, /*incremental*/ false); - } - return node.body ? getModuleInstanceStateCached(node.body, visited) : ModuleInstanceState.Instantiated; +export function getModuleInstanceState(node: ts.ModuleDeclaration, visited?: ts.ESMap): 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 + ts.setParent(node.body, node); + ts.setParentRecursive(node.body, /*incremental*/ false); } + return node.body ? getModuleInstanceStateCached(node.body, visited) : ModuleInstanceState.Instantiated; +} - function getModuleInstanceStateCached(node: ts.Node, visited = new ts.Map()) { - const nodeId = ts.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; +function getModuleInstanceStateCached(node: ts.Node, visited = new ts.Map()) { + const nodeId = ts.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; +} - function getModuleInstanceStateWorker(node: ts.Node, visited: ts.ESMap): ModuleInstanceState { - // A module is uninstantiated if it contains only - switch (node.kind) { - // 1. interface declarations, type alias declarations - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: +function getModuleInstanceStateWorker(node: ts.Node, visited: ts.ESMap): ModuleInstanceState { + // A module is uninstantiated if it contains only + switch (node.kind) { + // 1. interface declarations, type alias declarations + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + return ModuleInstanceState.NonInstantiated; + // 2. const enum declarations + case ts.SyntaxKind.EnumDeclaration: + if (ts.isEnumConst(node as ts.EnumDeclaration)) { + return ModuleInstanceState.ConstEnumOnly; + } + break; + // 3. non-exported import declarations + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + if (!(ts.hasSyntacticModifier(node, ts.ModifierFlags.Export))) { return ModuleInstanceState.NonInstantiated; - // 2. const enum declarations - case ts.SyntaxKind.EnumDeclaration: - if (ts.isEnumConst(node as ts.EnumDeclaration)) { - return ModuleInstanceState.ConstEnumOnly; - } - break; - // 3. non-exported import declarations - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - if (!(ts.hasSyntacticModifier(node, ts.ModifierFlags.Export))) { - return ModuleInstanceState.NonInstantiated; - } - break; - // 4. Export alias declarations pointing at only uninstantiated modules or things uninstantiated modules contain - case ts.SyntaxKind.ExportDeclaration: - const exportDeclaration = node as ts.ExportDeclaration; - if (!exportDeclaration.moduleSpecifier && exportDeclaration.exportClause && exportDeclaration.exportClause.kind === ts.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 ts.SyntaxKind.ModuleBlock: { + } + break; + // 4. Export alias declarations pointing at only uninstantiated modules or things uninstantiated modules contain + case ts.SyntaxKind.ExportDeclaration: + const exportDeclaration = node as ts.ExportDeclaration; + if (!exportDeclaration.moduleSpecifier && exportDeclaration.exportClause && exportDeclaration.exportClause.kind === ts.SyntaxKind.NamedExports) { let state = ModuleInstanceState.NonInstantiated; - ts.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: - ts.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 ts.SyntaxKind.ModuleDeclaration: - return getModuleInstanceState(node as ts.ModuleDeclaration, visited); - case ts.SyntaxKind.Identifier: - // Only jsdoc typedef definition can exist in jsdoc namespace, and it should - // be considered the same as type alias - if ((node as ts.Identifier).isInJSDocNamespace) { - return ModuleInstanceState.NonInstantiated; + break; + // 5. other uninstantiated module declarations. + case ts.SyntaxKind.ModuleBlock: { + let state = ModuleInstanceState.NonInstantiated; + ts.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: + ts.Debug.assertNever(childState); } + }); + return state; } - return ModuleInstanceState.Instantiated; + case ts.SyntaxKind.ModuleDeclaration: + return getModuleInstanceState(node as ts.ModuleDeclaration, visited); + case ts.SyntaxKind.Identifier: + // Only jsdoc typedef definition can exist in jsdoc namespace, and it should + // be considered the same as type alias + if ((node as ts.Identifier).isInJSDocNamespace) { + return ModuleInstanceState.NonInstantiated; + } } + return ModuleInstanceState.Instantiated; +} - function getModuleInstanceStateForAliasTarget(specifier: ts.ExportSpecifier, visited: ts.ESMap) { - const name = specifier.propertyName || specifier.name; - let p: ts.Node | undefined = specifier.parent; - while (p) { - if (ts.isBlock(p) || ts.isModuleBlock(p) || ts.isSourceFile(p)) { - const statements = p.statements; - let found: ModuleInstanceState | undefined; - for (const statement of statements) { - if (ts.nodeHasName(statement, name)) { - if (!statement.parent) { - ts.setParent(statement, p); - ts.setParentRecursive(statement, /*incremental*/ false); - } - const state = getModuleInstanceStateCached(statement, visited); - if (found === undefined || state > found) { - found = state; - } - if (found === ModuleInstanceState.Instantiated) { - return found; - } +function getModuleInstanceStateForAliasTarget(specifier: ts.ExportSpecifier, visited: ts.ESMap) { + const name = specifier.propertyName || specifier.name; + let p: ts.Node | undefined = specifier.parent; + while (p) { + if (ts.isBlock(p) || ts.isModuleBlock(p) || ts.isSourceFile(p)) { + const statements = p.statements; + let found: ModuleInstanceState | undefined; + for (const statement of statements) { + if (ts.nodeHasName(statement, name)) { + if (!statement.parent) { + ts.setParent(statement, p); + ts.setParentRecursive(statement, /*incremental*/ false); + } + 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; + if (found !== undefined) { + return found; + } } - return ModuleInstanceState.Instantiated; // Couldn't locate, assume could refer to a value + 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, +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, + IsObjectLiteralOrClassExpressionMethodOrAccessor = 1 << 7 +} - // 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, +function initFlowNode(node: T) { + ts.Debug.attachFlowNodeDebugInfo(node); + return node; +} - // 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, - IsObjectLiteralOrClassExpressionMethodOrAccessor = 1 << 7 - } - - function initFlowNode(node: T) { - ts.Debug.attachFlowNodeDebugInfo(node); - return node; - } - - const binder = createBinder(); - - export function bindSourceFile(file: ts.SourceFile, options: ts.CompilerOptions) { - ts.performance.mark("beforeBind"); - ts.perfLogger.logStartBindFile("" + file.fileName); - binder(file, options); - ts.perfLogger.logStopBindFile(); - ts.performance.mark("afterBind"); - ts.performance.measure("Bind", "beforeBind", "afterBind"); - } - function createBinder(): (file: ts.SourceFile, options: ts.CompilerOptions) => void { - let file: ts.SourceFile; - let options: ts.CompilerOptions; - let languageVersion: ts.ScriptTarget; - let parent: ts.Node; - let container: ts.Node; - let thisParentContainer: ts.Node; // Container one level up - let blockScopeContainer: ts.Node; - let lastContainer: ts.Node; - let delayedTypeAliases: (ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag)[]; - let seenThisKeyword: boolean; - - // state used by control flow analysis - let currentFlow: ts.FlowNode; - let currentBreakTarget: ts.FlowLabel | undefined; - let currentContinueTarget: ts.FlowLabel | undefined; - let currentReturnTarget: ts.FlowLabel | undefined; - let currentTrueTarget: ts.FlowLabel | undefined; - let currentFalseTarget: ts.FlowLabel | undefined; - let currentExceptionTarget: ts.FlowLabel | undefined; - let preSwitchCaseFlow: ts.FlowNode | undefined; - let activeLabelList: ActiveLabel | undefined; - let hasExplicitReturn: boolean; - - // state used for emit helpers - let emitFlags: ts.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; - - // If we are binding an assignment pattern, we will bind certain expressions differently. - let inAssignmentPattern = false; - - let symbolCount = 0; - - let Symbol: new (flags: ts.SymbolFlags, name: ts.__String) => ts.Symbol; - let classifiableNames: ts.Set; - const unreachableFlow: ts.FlowNode = { flags: ts.FlowFlags.Unreachable }; - const reportedUnreachableFlow: ts.FlowNode = { flags: ts.FlowFlags.Unreachable }; - const bindBinaryExpressionFlow = createBindBinaryExpressionFlow(); - - /** - * 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: ts.Node, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { - return ts.createDiagnosticForNodeInSourceFile(ts.getSourceFileOfNode(node) || file, node, message, arg0, arg1, arg2); - } - - function bindSourceFile(f: ts.SourceFile, opts: ts.CompilerOptions) { - file = f; - options = opts; - languageVersion = ts.getEmitScriptTarget(options); - inStrictMode = bindInStrictMode(file, opts); - classifiableNames = new ts.Set(); - symbolCount = 0; - - Symbol = ts.objectAllocator.getSymbolConstructor(); - - // Attach debugging information if necessary - ts.Debug.attachFlowNodeDebugInfo(unreachableFlow); - ts.Debug.attachFlowNodeDebugInfo(reportedUnreachableFlow); - - if (!file.locals) { - ts.tracing?.push(ts.tracing.Phase.Bind, "bindSourceFile", { path: file.path }, /*separateBeginAndEnd*/ true); - bind(file); - ts.tracing?.pop(); - 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; - inAssignmentPattern = false; - emitFlags = ts.NodeFlags.None; - } +const binder = createBinder(); - return bindSourceFile; +export function bindSourceFile(file: ts.SourceFile, options: ts.CompilerOptions) { + ts.performance.mark("beforeBind"); + ts.perfLogger.logStartBindFile("" + file.fileName); + binder(file, options); + ts.perfLogger.logStopBindFile(); + ts.performance.mark("afterBind"); + ts.performance.measure("Bind", "beforeBind", "afterBind"); +} +function createBinder(): (file: ts.SourceFile, options: ts.CompilerOptions) => void { + let file: ts.SourceFile; + let options: ts.CompilerOptions; + let languageVersion: ts.ScriptTarget; + let parent: ts.Node; + let container: ts.Node; + let thisParentContainer: ts.Node; // Container one level up + let blockScopeContainer: ts.Node; + let lastContainer: ts.Node; + let delayedTypeAliases: (ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag)[]; + let seenThisKeyword: boolean; + + // state used by control flow analysis + let currentFlow: ts.FlowNode; + let currentBreakTarget: ts.FlowLabel | undefined; + let currentContinueTarget: ts.FlowLabel | undefined; + let currentReturnTarget: ts.FlowLabel | undefined; + let currentTrueTarget: ts.FlowLabel | undefined; + let currentFalseTarget: ts.FlowLabel | undefined; + let currentExceptionTarget: ts.FlowLabel | undefined; + let preSwitchCaseFlow: ts.FlowNode | undefined; + let activeLabelList: ActiveLabel | undefined; + let hasExplicitReturn: boolean; + + // state used for emit helpers + let emitFlags: ts.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; + + // If we are binding an assignment pattern, we will bind certain expressions differently. + let inAssignmentPattern = false; + + let symbolCount = 0; + + let Symbol: new (flags: ts.SymbolFlags, name: ts.__String) => ts.Symbol; + let classifiableNames: ts.Set; + const unreachableFlow: ts.FlowNode = { flags: ts.FlowFlags.Unreachable }; + const reportedUnreachableFlow: ts.FlowNode = { flags: ts.FlowFlags.Unreachable }; + const bindBinaryExpressionFlow = createBindBinaryExpressionFlow(); + + /** + * 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: ts.Node, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { + return ts.createDiagnosticForNodeInSourceFile(ts.getSourceFileOfNode(node) || file, node, message, arg0, arg1, arg2); + } - function bindInStrictMode(file: ts.SourceFile, opts: ts.CompilerOptions): boolean { - if (ts.getStrictOptionValue(opts, "alwaysStrict") && !file.isDeclarationFile) { - // bind in strict mode source files with alwaysStrict option - return true; - } - else { - return !!file.externalModuleIndicator; - } - } + function bindSourceFile(f: ts.SourceFile, opts: ts.CompilerOptions) { + file = f; + options = opts; + languageVersion = ts.getEmitScriptTarget(options); + inStrictMode = bindInStrictMode(file, opts); + classifiableNames = new ts.Set(); + symbolCount = 0; + + Symbol = ts.objectAllocator.getSymbolConstructor(); + + // Attach debugging information if necessary + ts.Debug.attachFlowNodeDebugInfo(unreachableFlow); + ts.Debug.attachFlowNodeDebugInfo(reportedUnreachableFlow); + + if (!file.locals) { + ts.tracing?.push(ts.tracing.Phase.Bind, "bindSourceFile", { path: file.path }, /*separateBeginAndEnd*/ true); + bind(file); + ts.tracing?.pop(); + 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; + inAssignmentPattern = false; + emitFlags = ts.NodeFlags.None; + } - function createSymbol(flags: ts.SymbolFlags, name: ts.__String): ts.Symbol { - symbolCount++; - return new Symbol(flags, name); + return bindSourceFile; + + function bindInStrictMode(file: ts.SourceFile, opts: ts.CompilerOptions): boolean { + if (ts.getStrictOptionValue(opts, "alwaysStrict") && !file.isDeclarationFile) { + // bind in strict mode source files with alwaysStrict option + return true; } + else { + return !!file.externalModuleIndicator; + } + } - function addDeclarationToSymbol(symbol: ts.Symbol, node: ts.Declaration, symbolFlags: ts.SymbolFlags) { - symbol.flags |= symbolFlags; + function createSymbol(flags: ts.SymbolFlags, name: ts.__String): ts.Symbol { + symbolCount++; + return new Symbol(flags, name); + } - node.symbol = symbol; - symbol.declarations = ts.appendIfUnique(symbol.declarations, node); - if (symbolFlags & (ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.Module | ts.SymbolFlags.Variable) && !symbol.exports) { - symbol.exports = ts.createSymbolTable(); - } + function addDeclarationToSymbol(symbol: ts.Symbol, node: ts.Declaration, symbolFlags: ts.SymbolFlags) { + symbol.flags |= symbolFlags; - if (symbolFlags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.ObjectLiteral) && !symbol.members) { - symbol.members = ts.createSymbolTable(); - } + node.symbol = symbol; + symbol.declarations = ts.appendIfUnique(symbol.declarations, node); + if (symbolFlags & (ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.Module | ts.SymbolFlags.Variable) && !symbol.exports) { + symbol.exports = ts.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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.SymbolFlags.RegularEnum))) { - symbol.constEnumOnlyModule = false; - } + if (symbolFlags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.ObjectLiteral) && !symbol.members) { + symbol.members = ts.createSymbolTable(); + } - if (symbolFlags & ts.SymbolFlags.Value) { - ts.setValueDeclaration(symbol, node); - } + // On merge of const enum module with class or function, reset const enum only flag (namespaces will already recalculate) + if (symbol.constEnumOnlyModule && (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.SymbolFlags.RegularEnum))) { + symbol.constEnumOnlyModule = false; } - // Should not be called on a declaration with a computed property name, - // unless it is a well known Symbol. - function getDeclarationName(node: ts.Declaration): ts.__String | undefined { - if (node.kind === ts.SyntaxKind.ExportAssignment) { - return (node as ts.ExportAssignment).isExportEquals ? ts.InternalSymbolName.ExportEquals : ts.InternalSymbolName.Default; - } + if (symbolFlags & ts.SymbolFlags.Value) { + ts.setValueDeclaration(symbol, node); + } + } + + // Should not be called on a declaration with a computed property name, + // unless it is a well known Symbol. + function getDeclarationName(node: ts.Declaration): ts.__String | undefined { + if (node.kind === ts.SyntaxKind.ExportAssignment) { + return (node as ts.ExportAssignment).isExportEquals ? ts.InternalSymbolName.ExportEquals : ts.InternalSymbolName.Default; + } - const name = ts.getNameOfDeclaration(node); - if (name) { - if (ts.isAmbientModule(node)) { - const moduleName = ts.getTextOfIdentifierOrLiteral(name as ts.Identifier | ts.StringLiteral); - return (ts.isGlobalScopeAugmentation(node as ts.ModuleDeclaration) ? "__global" : `"${moduleName}"`) as ts.__String; + const name = ts.getNameOfDeclaration(node); + if (name) { + if (ts.isAmbientModule(node)) { + const moduleName = ts.getTextOfIdentifierOrLiteral(name as ts.Identifier | ts.StringLiteral); + return (ts.isGlobalScopeAugmentation(node as ts.ModuleDeclaration) ? "__global" : `"${moduleName}"`) as ts.__String; + } + if (name.kind === ts.SyntaxKind.ComputedPropertyName) { + const nameExpression = name.expression; + // treat computed property names where expression is string/numeric literal as just string/numeric literal + if (ts.isStringOrNumericLiteralLike(nameExpression)) { + return ts.escapeLeadingUnderscores(nameExpression.text); } - if (name.kind === ts.SyntaxKind.ComputedPropertyName) { - const nameExpression = name.expression; - // treat computed property names where expression is string/numeric literal as just string/numeric literal - if (ts.isStringOrNumericLiteralLike(nameExpression)) { - return ts.escapeLeadingUnderscores(nameExpression.text); - } - if (ts.isSignedNumericLiteral(nameExpression)) { - return ts.tokenToString(nameExpression.operator) + nameExpression.operand.text as ts.__String; - } - else { - ts.Debug.fail("Only computed properties with literal names have declaration names"); - } + if (ts.isSignedNumericLiteral(nameExpression)) { + return ts.tokenToString(nameExpression.operator) + nameExpression.operand.text as ts.__String; } - if (ts.isPrivateIdentifier(name)) { - // containingClass exists because private names only allowed inside classes - const containingClass = ts.getContainingClass(node); - if (!containingClass) { - // we can get here in cases where there is already a parse error. - return undefined; - } - const containingClassSymbol = containingClass.symbol; - return ts.getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText); - } - return ts.isPropertyNameLiteral(name) ? ts.getEscapedTextOfIdentifierOrLiteral(name) : undefined; - } - switch (node.kind) { - case ts.SyntaxKind.Constructor: - return ts.InternalSymbolName.Constructor; - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.JSDocSignature: - return ts.InternalSymbolName.Call; - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ConstructSignature: - return ts.InternalSymbolName.New; - case ts.SyntaxKind.IndexSignature: - return ts.InternalSymbolName.Index; - case ts.SyntaxKind.ExportDeclaration: - return ts.InternalSymbolName.ExportStar; - case ts.SyntaxKind.SourceFile: - // json file should behave as + else { + ts.Debug.fail("Only computed properties with literal names have declaration names"); + } + } + if (ts.isPrivateIdentifier(name)) { + // containingClass exists because private names only allowed inside classes + const containingClass = ts.getContainingClass(node); + if (!containingClass) { + // we can get here in cases where there is already a parse error. + return undefined; + } + const containingClassSymbol = containingClass.symbol; + return ts.getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText); + } + return ts.isPropertyNameLiteral(name) ? ts.getEscapedTextOfIdentifierOrLiteral(name) : undefined; + } + switch (node.kind) { + case ts.SyntaxKind.Constructor: + return ts.InternalSymbolName.Constructor; + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.JSDocSignature: + return ts.InternalSymbolName.Call; + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ConstructSignature: + return ts.InternalSymbolName.New; + case ts.SyntaxKind.IndexSignature: + return ts.InternalSymbolName.Index; + case ts.SyntaxKind.ExportDeclaration: + return ts.InternalSymbolName.ExportStar; + case ts.SyntaxKind.SourceFile: + // json file should behave as + // module.exports = ... + return ts.InternalSymbolName.ExportEquals; + case ts.SyntaxKind.BinaryExpression: + if (ts.getAssignmentDeclarationKind(node as ts.BinaryExpression) === ts.AssignmentDeclarationKind.ModuleExports) { // module.exports = ... return ts.InternalSymbolName.ExportEquals; - case ts.SyntaxKind.BinaryExpression: - if (ts.getAssignmentDeclarationKind(node as ts.BinaryExpression) === ts.AssignmentDeclarationKind.ModuleExports) { - // module.exports = ... - return ts.InternalSymbolName.ExportEquals; - } - ts.Debug.fail("Unknown binary declaration kind"); - break; - case ts.SyntaxKind.JSDocFunctionType: - return (ts.isJSDocConstructSignature(node) ? ts.InternalSymbolName.New : ts.InternalSymbolName.Call); - case ts.SyntaxKind.Parameter: - // Parameters with names are handled at the top of this function. Parameters - // without names can only come from JSDocFunctionTypes. - ts.Debug.assert(node.parent.kind === ts.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 as ts.JSDocFunctionType; - const index = functionType.parameters.indexOf(node as ts.ParameterDeclaration); - return "arg" + index as ts.__String; - } - } - - function getDisplayName(node: ts.Declaration): string { - return ts.isNamedDeclaration(node) ? ts.declarationNameToString(node.name) : ts.unescapeLeadingUnderscores(ts.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: ts.SymbolTable, parent: ts.Symbol | undefined, node: ts.Declaration, includes: ts.SymbolFlags, excludes: ts.SymbolFlags, isReplaceableByMethod?: boolean, isComputedName?: boolean): ts.Symbol { - ts.Debug.assert(isComputedName || !ts.hasDynamicName(node)); - const isDefaultExport = ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) || ts.isExportSpecifier(node) && node.name.escapedText === "default"; - - // The exported symbol for an export default function/class node is always named "default" - const name = isComputedName ? ts.InternalSymbolName.Computed - : isDefaultExport && parent ? ts.InternalSymbolName.Default - : getDeclarationName(node); - - let symbol: ts.Symbol | undefined; - if (name === undefined) { - symbol = createSymbol(ts.SymbolFlags.None, ts.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 & ts.SymbolFlags.Classifiable) { - classifiableNames.add(name); } + ts.Debug.fail("Unknown binary declaration kind"); + break; + case ts.SyntaxKind.JSDocFunctionType: + return (ts.isJSDocConstructSignature(node) ? ts.InternalSymbolName.New : ts.InternalSymbolName.Call); + case ts.SyntaxKind.Parameter: + // Parameters with names are handled at the top of this function. Parameters + // without names can only come from JSDocFunctionTypes. + ts.Debug.assert(node.parent.kind === ts.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 as ts.JSDocFunctionType; + const index = functionType.parameters.indexOf(node as ts.ParameterDeclaration); + return "arg" + index as ts.__String; + } + } + + function getDisplayName(node: ts.Declaration): string { + return ts.isNamedDeclaration(node) ? ts.declarationNameToString(node.name) : ts.unescapeLeadingUnderscores(ts.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: ts.SymbolTable, parent: ts.Symbol | undefined, node: ts.Declaration, includes: ts.SymbolFlags, excludes: ts.SymbolFlags, isReplaceableByMethod?: boolean, isComputedName?: boolean): ts.Symbol { + ts.Debug.assert(isComputedName || !ts.hasDynamicName(node)); + const isDefaultExport = ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) || ts.isExportSpecifier(node) && node.name.escapedText === "default"; + + // The exported symbol for an export default function/class node is always named "default" + const name = isComputedName ? ts.InternalSymbolName.Computed + : isDefaultExport && parent ? ts.InternalSymbolName.Default + : getDeclarationName(node); + + let symbol: ts.Symbol | undefined; + if (name === undefined) { + symbol = createSymbol(ts.SymbolFlags.None, ts.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 & ts.SymbolFlags.Classifiable) { + classifiableNames.add(name); + } - if (!symbol) { + if (!symbol) { + symbolTable.set(name, symbol = createSymbol(ts.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(ts.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(ts.SymbolFlags.None, name)); - } - else if (!(includes & ts.SymbolFlags.Variable && symbol.flags & ts.SymbolFlags.Assignment)) { - // Assignment declarations are allowed to merge with variables, no matter what other flags they have. - if (ts.isNamedDeclaration(node)) { - ts.setParent(node.name, node); - } - // Report errors every position with duplicate declaration - // Report errors on previous encountered declarations - let message = symbol.flags & ts.SymbolFlags.BlockScopedVariable - ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 - : ts.Diagnostics.Duplicate_identifier_0; - let messageNeedsName = true; - - if (symbol.flags & ts.SymbolFlags.Enum || includes & ts.SymbolFlags.Enum) { - message = ts.Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations; + else if (!(includes & ts.SymbolFlags.Variable && symbol.flags & ts.SymbolFlags.Assignment)) { + // Assignment declarations are allowed to merge with variables, no matter what other flags they have. + if (ts.isNamedDeclaration(node)) { + ts.setParent(node.name, node); + } + // Report errors every position with duplicate declaration + // Report errors on previous encountered declarations + let message = symbol.flags & ts.SymbolFlags.BlockScopedVariable + ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 + : ts.Diagnostics.Duplicate_identifier_0; + let messageNeedsName = true; + + if (symbol.flags & ts.SymbolFlags.Enum || includes & ts.SymbolFlags.Enum) { + message = ts.Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations; + messageNeedsName = false; + } + + let multipleDefaultExports = false; + if (ts.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 = ts.Diagnostics.A_module_cannot_have_multiple_default_exports; messageNeedsName = false; + multipleDefaultExports = true; } - - let multipleDefaultExports = false; - if (ts.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 === ts.SyntaxKind.ExportAssignment && !(node as ts.ExportAssignment).isExportEquals)) { message = ts.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 === ts.SyntaxKind.ExportAssignment && !(node as ts.ExportAssignment).isExportEquals)) { - message = ts.Diagnostics.A_module_cannot_have_multiple_default_exports; - messageNeedsName = false; - multipleDefaultExports = true; - } - } } + } - const relatedInformation: ts.DiagnosticRelatedInformation[] = []; - if (ts.isTypeAliasDeclaration(node) && ts.nodeIsMissing(node.type) && ts.hasSyntacticModifier(node, ts.ModifierFlags.Export) && symbol.flags & (ts.SymbolFlags.Alias | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) { - // export type T; - may have meant export type { T }? - relatedInformation.push(createDiagnosticForNode(node, ts.Diagnostics.Did_you_mean_0, `export type { ${ts.unescapeLeadingUnderscores(node.name.escapedText)} }`)); - } + const relatedInformation: ts.DiagnosticRelatedInformation[] = []; + if (ts.isTypeAliasDeclaration(node) && ts.nodeIsMissing(node.type) && ts.hasSyntacticModifier(node, ts.ModifierFlags.Export) && symbol.flags & (ts.SymbolFlags.Alias | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) { + // export type T; - may have meant export type { T }? + relatedInformation.push(createDiagnosticForNode(node, ts.Diagnostics.Did_you_mean_0, `export type { ${ts.unescapeLeadingUnderscores(node.name.escapedText)} }`)); + } - const declarationName = ts.getNameOfDeclaration(node) || node; - ts.forEach(symbol.declarations, (declaration, index) => { - const decl = ts.getNameOfDeclaration(declaration) || declaration; - const diag = createDiagnosticForNode(decl, message, messageNeedsName ? getDisplayName(declaration) : undefined); - file.bindDiagnostics.push(multipleDefaultExports ? ts.addRelatedInfo(diag, createDiagnosticForNode(declarationName, index === 0 ? ts.Diagnostics.Another_export_default_is_here : ts.Diagnostics.and_here)) : diag); - if (multipleDefaultExports) { - relatedInformation.push(createDiagnosticForNode(decl, ts.Diagnostics.The_first_export_default_is_here)); - } - }); + const declarationName = ts.getNameOfDeclaration(node) || node; + ts.forEach(symbol.declarations, (declaration, index) => { + const decl = ts.getNameOfDeclaration(declaration) || declaration; + const diag = createDiagnosticForNode(decl, message, messageNeedsName ? getDisplayName(declaration) : undefined); + file.bindDiagnostics.push(multipleDefaultExports ? ts.addRelatedInfo(diag, createDiagnosticForNode(declarationName, index === 0 ? ts.Diagnostics.Another_export_default_is_here : ts.Diagnostics.and_here)) : diag); + if (multipleDefaultExports) { + relatedInformation.push(createDiagnosticForNode(decl, ts.Diagnostics.The_first_export_default_is_here)); + } + }); - const diag = createDiagnosticForNode(declarationName, message, messageNeedsName ? getDisplayName(node) : undefined); - file.bindDiagnostics.push(ts.addRelatedInfo(diag, ...relatedInformation)); - symbol = createSymbol(ts.SymbolFlags.None, name); - } + const diag = createDiagnosticForNode(declarationName, message, messageNeedsName ? getDisplayName(node) : undefined); + file.bindDiagnostics.push(ts.addRelatedInfo(diag, ...relatedInformation)); + symbol = createSymbol(ts.SymbolFlags.None, name); } } + } + + addDeclarationToSymbol(symbol, node, includes); + if (symbol.parent) { + ts.Debug.assert(symbol.parent === parent, "Existing symbol parent should match new one"); + } + else { + symbol.parent = parent; + } + + return symbol; + } - addDeclarationToSymbol(symbol, node, includes); - if (symbol.parent) { - ts.Debug.assert(symbol.parent === parent, "Existing symbol parent should match new one"); + function declareModuleMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags): ts.Symbol { + const hasExportModifier = !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) || jsdocTreatAsExported(node); + if (symbolFlags & ts.SymbolFlags.Alias) { + if (node.kind === ts.SyntaxKind.ExportSpecifier || (node.kind === ts.SyntaxKind.ImportEqualsDeclaration && hasExportModifier)) { + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); } else { - symbol.parent = parent; + return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } - - return symbol; } - - function declareModuleMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags): ts.Symbol { - const hasExportModifier = !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) || jsdocTreatAsExported(node); - if (symbolFlags & ts.SymbolFlags.Alias) { - if (node.kind === ts.SyntaxKind.ExportSpecifier || (node.kind === ts.SyntaxKind.ImportEqualsDeclaration && hasExportModifier)) { - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); - } - else { - return declareSymbol(container.locals!, /*parent*/ undefined, 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 (ts.isJSDocTypeAlias(node)) + ts.Debug.assert(ts.isInJSFile(node)); // We shouldn't add symbols for JSDoc nodes if not in a JS file. + if (!ts.isAmbientModule(node) && (hasExportModifier || container.flags & ts.NodeFlags.ExportContext)) { + if (!container.locals || (ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) && !getDeclarationName(node))) { + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); // No local symbol for an unnamed default! + } + const exportKind = symbolFlags & ts.SymbolFlags.Value ? ts.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 { - // 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 (ts.isJSDocTypeAlias(node)) - ts.Debug.assert(ts.isInJSFile(node)); // We shouldn't add symbols for JSDoc nodes if not in a JS file. - if (!ts.isAmbientModule(node) && (hasExportModifier || container.flags & ts.NodeFlags.ExportContext)) { - if (!container.locals || (ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) && !getDeclarationName(node))) { - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); // No local symbol for an unnamed default! - } - const exportKind = symbolFlags & ts.SymbolFlags.Value ? ts.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); - } - } - } - - function jsdocTreatAsExported(node: ts.Node) { - if (node.parent && ts.isModuleDeclaration(node)) { - node = node.parent; - } - if (!ts.isJSDocTypeAlias(node)) - return false; - // jsdoc typedef handling is a bit of a doozy, but to summarize, treat the typedef as exported if: - // 1. It has an explicit name (since by default typedefs are always directly exported, either at the top level or in a container), or - if (!ts.isJSDocEnumTag(node) && !!node.fullName) - return true; - // 2. The thing a nameless typedef pulls its name from is implicitly a direct export (either by assignment or actual export flag). - const declName = ts.getNameOfDeclaration(node); - if (!declName) - return false; - if (ts.isPropertyAccessEntityNameExpression(declName.parent) && isTopLevelNamespaceAssignment(declName.parent)) - return true; - if (ts.isDeclaration(declName.parent) && ts.getCombinedModifierFlags(declName.parent) & ts.ModifierFlags.Export) - return true; - // This could potentially be simplified by having `delayedBindJSDocTypedefTag` pass in an override for `hasExportModifier`, since it should - // already have calculated and branched on most of this. - return false; + 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: ts.Mutable, 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 !== ts.SyntaxKind.ArrowFunction) { - thisParentContainer = container; - } - container = blockScopeContainer = node; - if (containerFlags & ContainerFlags.HasLocals) { - container.locals = ts.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 isImmediatelyInvoked = (containerFlags & ContainerFlags.IsFunctionExpression && - !ts.hasSyntacticModifier(node, ts.ModifierFlags.Async) && - !(node as ts.FunctionLikeDeclaration).asteriskToken && - !!ts.getImmediatelyInvokedFunctionExpression(node)) || - node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; - // 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 (!isImmediatelyInvoked) { - currentFlow = initFlowNode({ flags: ts.FlowFlags.Start }); - if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethodOrAccessor)) { - currentFlow.node = node as ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration; - } - } - // 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 = isImmediatelyInvoked || node.kind === ts.SyntaxKind.Constructor || (ts.isInJSFile(node) && (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.FunctionExpression)) ? 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 &= ~ts.NodeFlags.ReachabilityAndEmitFlags; - if (!(currentFlow.flags & ts.FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && ts.nodeIsPresent((node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).body)) { - node.flags |= ts.NodeFlags.HasImplicitReturn; - if (hasExplicitReturn) - node.flags |= ts.NodeFlags.HasExplicitReturn; - (node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).endFlowNode = currentFlow; - } - if (node.kind === ts.SyntaxKind.SourceFile) { - node.flags |= emitFlags; - (node as ts.SourceFile).endFlowNode = currentFlow; - } - - if (currentReturnTarget) { - addAntecedent(currentReturnTarget, currentFlow); - currentFlow = finishFlowLabel(currentReturnTarget); - if (node.kind === ts.SyntaxKind.Constructor || node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration || (ts.isInJSFile(node) && (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.FunctionExpression))) { - (node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).returnFlowNode = currentFlow; - } - } - if (!isImmediatelyInvoked) { - currentFlow = saveCurrentFlow; + function jsdocTreatAsExported(node: ts.Node) { + if (node.parent && ts.isModuleDeclaration(node)) { + node = node.parent; + } + if (!ts.isJSDocTypeAlias(node)) + return false; + // jsdoc typedef handling is a bit of a doozy, but to summarize, treat the typedef as exported if: + // 1. It has an explicit name (since by default typedefs are always directly exported, either at the top level or in a container), or + if (!ts.isJSDocEnumTag(node) && !!node.fullName) + return true; + // 2. The thing a nameless typedef pulls its name from is implicitly a direct export (either by assignment or actual export flag). + const declName = ts.getNameOfDeclaration(node); + if (!declName) + return false; + if (ts.isPropertyAccessEntityNameExpression(declName.parent) && isTopLevelNamespaceAssignment(declName.parent)) + return true; + if (ts.isDeclaration(declName.parent) && ts.getCombinedModifierFlags(declName.parent) & ts.ModifierFlags.Export) + return true; + // This could potentially be simplified by having `delayedBindJSDocTypedefTag` pass in an override for `hasExportModifier`, since it should + // already have calculated and branched on most of this. + return false; + } + + // 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: ts.Mutable, 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 !== ts.SyntaxKind.ArrowFunction) { + thisParentContainer = container; + } + container = blockScopeContainer = node; + if (containerFlags & ContainerFlags.HasLocals) { + container.locals = ts.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 isImmediatelyInvoked = (containerFlags & ContainerFlags.IsFunctionExpression && + !ts.hasSyntacticModifier(node, ts.ModifierFlags.Async) && + !(node as ts.FunctionLikeDeclaration).asteriskToken && + !!ts.getImmediatelyInvokedFunctionExpression(node)) || + node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; + // 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 (!isImmediatelyInvoked) { + currentFlow = initFlowNode({ flags: ts.FlowFlags.Start }); + if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethodOrAccessor)) { + currentFlow.node = node as ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration; } - 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 | ts.NodeFlags.ContainsThis : node.flags & ~ts.NodeFlags.ContainsThis; + // 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 = isImmediatelyInvoked || node.kind === ts.SyntaxKind.Constructor || (ts.isInJSFile(node) && (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.FunctionExpression)) ? 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 &= ~ts.NodeFlags.ReachabilityAndEmitFlags; + if (!(currentFlow.flags & ts.FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && ts.nodeIsPresent((node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).body)) { + node.flags |= ts.NodeFlags.HasImplicitReturn; + if (hasExplicitReturn) + node.flags |= ts.NodeFlags.HasExplicitReturn; + (node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).endFlowNode = currentFlow; } - else { - bindChildren(node); + if (node.kind === ts.SyntaxKind.SourceFile) { + node.flags |= emitFlags; + (node as ts.SourceFile).endFlowNode = currentFlow; } - container = saveContainer; - thisParentContainer = saveThisParentContainer; - blockScopeContainer = savedBlockScopeContainer; + if (currentReturnTarget) { + addAntecedent(currentReturnTarget, currentFlow); + currentFlow = finishFlowLabel(currentReturnTarget); + if (node.kind === ts.SyntaxKind.Constructor || node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration || (ts.isInJSFile(node) && (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.FunctionExpression))) { + (node as ts.FunctionLikeDeclaration | ts.ClassStaticBlockDeclaration).returnFlowNode = currentFlow; + } + } + if (!isImmediatelyInvoked) { + currentFlow = saveCurrentFlow; + } + currentBreakTarget = saveBreakTarget; + currentContinueTarget = saveContinueTarget; + currentReturnTarget = saveReturnTarget; + currentExceptionTarget = saveExceptionTarget; + activeLabelList = saveActiveLabelList; + hasExplicitReturn = saveHasExplicitReturn; } - - function bindEachFunctionsFirst(nodes: ts.NodeArray | undefined): void { - bindEach(nodes, n => n.kind === ts.SyntaxKind.FunctionDeclaration ? bind(n) : undefined); - bindEach(nodes, n => n.kind !== ts.SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + else if (containerFlags & ContainerFlags.IsInterface) { + seenThisKeyword = false; + bindChildren(node); + node.flags = seenThisKeyword ? node.flags | ts.NodeFlags.ContainsThis : node.flags & ~ts.NodeFlags.ContainsThis; + } + else { + bindChildren(node); } - function bindEach(nodes: ts.NodeArray | undefined, bindFunction: (node: ts.Node) => void = bind): void { - if (nodes === undefined) { - return; - } + container = saveContainer; + thisParentContainer = saveThisParentContainer; + blockScopeContainer = savedBlockScopeContainer; + } - ts.forEach(nodes, bindFunction); - } + function bindEachFunctionsFirst(nodes: ts.NodeArray | undefined): void { + bindEach(nodes, n => n.kind === ts.SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + bindEach(nodes, n => n.kind !== ts.SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + } - function bindEachChild(node: ts.Node) { - ts.forEachChild(node, bind, bindEach); + function bindEach(nodes: ts.NodeArray | undefined, bindFunction: (node: ts.Node) => void = bind): void { + if (nodes === undefined) { + return; } - function bindChildren(node: ts.Node): void { - const saveInAssignmentPattern = inAssignmentPattern; - // Most nodes aren't valid in an assignment pattern, so we clear the value here - // and set it before we descend into nodes that could actually be part of an assignment pattern. - inAssignmentPattern = false; - if (checkUnreachable(node)) { - bindEachChild(node); - bindJSDoc(node); - inAssignmentPattern = saveInAssignmentPattern; - return; - } - if (node.kind >= ts.SyntaxKind.FirstStatement && node.kind <= ts.SyntaxKind.LastStatement && !options.allowUnreachableCode) { - node.flowNode = currentFlow; - } - switch (node.kind) { - case ts.SyntaxKind.WhileStatement: - bindWhileStatement(node as ts.WhileStatement); - break; - case ts.SyntaxKind.DoStatement: - bindDoStatement(node as ts.DoStatement); - break; - case ts.SyntaxKind.ForStatement: - bindForStatement(node as ts.ForStatement); - break; - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - bindForInOrForOfStatement(node as ts.ForInOrOfStatement); - break; - case ts.SyntaxKind.IfStatement: - bindIfStatement(node as ts.IfStatement); - break; - case ts.SyntaxKind.ReturnStatement: - case ts.SyntaxKind.ThrowStatement: - bindReturnOrThrow(node as ts.ReturnStatement | ts.ThrowStatement); - break; - case ts.SyntaxKind.BreakStatement: - case ts.SyntaxKind.ContinueStatement: - bindBreakOrContinueStatement(node as ts.BreakOrContinueStatement); - break; - case ts.SyntaxKind.TryStatement: - bindTryStatement(node as ts.TryStatement); - break; - case ts.SyntaxKind.SwitchStatement: - bindSwitchStatement(node as ts.SwitchStatement); - break; - case ts.SyntaxKind.CaseBlock: - bindCaseBlock(node as ts.CaseBlock); - break; - case ts.SyntaxKind.CaseClause: - bindCaseClause(node as ts.CaseClause); - break; - case ts.SyntaxKind.ExpressionStatement: - bindExpressionStatement(node as ts.ExpressionStatement); - break; - case ts.SyntaxKind.LabeledStatement: - bindLabeledStatement(node as ts.LabeledStatement); - break; - case ts.SyntaxKind.PrefixUnaryExpression: - bindPrefixUnaryExpressionFlow(node as ts.PrefixUnaryExpression); - break; - case ts.SyntaxKind.PostfixUnaryExpression: - bindPostfixUnaryExpressionFlow(node as ts.PostfixUnaryExpression); - break; - case ts.SyntaxKind.BinaryExpression: - if (ts.isDestructuringAssignment(node)) { - // Carry over whether we are in an assignment pattern to - // binary expressions that could actually be an initializer - inAssignmentPattern = saveInAssignmentPattern; - bindDestructuringAssignmentFlow(node); - return; - } - bindBinaryExpressionFlow(node as ts.BinaryExpression); - break; - case ts.SyntaxKind.DeleteExpression: - bindDeleteExpressionFlow(node as ts.DeleteExpression); - break; - case ts.SyntaxKind.ConditionalExpression: - bindConditionalExpressionFlow(node as ts.ConditionalExpression); - break; - case ts.SyntaxKind.VariableDeclaration: - bindVariableDeclarationFlow(node as ts.VariableDeclaration); - break; - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - bindAccessExpressionFlow(node as ts.AccessExpression); - break; - case ts.SyntaxKind.CallExpression: - bindCallExpressionFlow(node as ts.CallExpression); - break; - case ts.SyntaxKind.NonNullExpression: - bindNonNullExpressionFlow(node as ts.NonNullExpression); - break; - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - bindJSDocTypeAlias(node as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag); - break; - // In source files and blocks, bind functions first to match hoisting that occurs at runtime - case ts.SyntaxKind.SourceFile: { - bindEachFunctionsFirst((node as ts.SourceFile).statements); - bind((node as ts.SourceFile).endOfFileToken); - break; - } - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - bindEachFunctionsFirst((node as ts.Block).statements); - break; - case ts.SyntaxKind.BindingElement: - bindBindingElementFlow(node as ts.BindingElement); - break; - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ArrayLiteralExpression: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.SpreadElement: - // Carry over whether we are in an assignment pattern of Object and Array literals - // as well as their children that are valid assignment targets. - inAssignmentPattern = saveInAssignmentPattern; - // falls through - default: - bindEachChild(node); - break; - } + ts.forEach(nodes, bindFunction); + } + + function bindEachChild(node: ts.Node) { + ts.forEachChild(node, bind, bindEach); + } + + function bindChildren(node: ts.Node): void { + const saveInAssignmentPattern = inAssignmentPattern; + // Most nodes aren't valid in an assignment pattern, so we clear the value here + // and set it before we descend into nodes that could actually be part of an assignment pattern. + inAssignmentPattern = false; + if (checkUnreachable(node)) { + bindEachChild(node); bindJSDoc(node); inAssignmentPattern = saveInAssignmentPattern; + return; } - - function isNarrowingExpression(expr: ts.Expression): boolean { - switch (expr.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PrivateIdentifier: - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return containsNarrowableReference(expr); - case ts.SyntaxKind.CallExpression: - return hasNarrowableArgument(expr as ts.CallExpression); - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.NonNullExpression: - return isNarrowingExpression((expr as ts.ParenthesizedExpression | ts.NonNullExpression).expression); - case ts.SyntaxKind.BinaryExpression: - return isNarrowingBinaryExpression(expr as ts.BinaryExpression); - case ts.SyntaxKind.PrefixUnaryExpression: - return (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken && isNarrowingExpression((expr as ts.PrefixUnaryExpression).operand); - case ts.SyntaxKind.TypeOfExpression: - return isNarrowingExpression((expr as ts.TypeOfExpression).expression); + if (node.kind >= ts.SyntaxKind.FirstStatement && node.kind <= ts.SyntaxKind.LastStatement && !options.allowUnreachableCode) { + node.flowNode = currentFlow; + } + switch (node.kind) { + case ts.SyntaxKind.WhileStatement: + bindWhileStatement(node as ts.WhileStatement); + break; + case ts.SyntaxKind.DoStatement: + bindDoStatement(node as ts.DoStatement); + break; + case ts.SyntaxKind.ForStatement: + bindForStatement(node as ts.ForStatement); + break; + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + bindForInOrForOfStatement(node as ts.ForInOrOfStatement); + break; + case ts.SyntaxKind.IfStatement: + bindIfStatement(node as ts.IfStatement); + break; + case ts.SyntaxKind.ReturnStatement: + case ts.SyntaxKind.ThrowStatement: + bindReturnOrThrow(node as ts.ReturnStatement | ts.ThrowStatement); + break; + case ts.SyntaxKind.BreakStatement: + case ts.SyntaxKind.ContinueStatement: + bindBreakOrContinueStatement(node as ts.BreakOrContinueStatement); + break; + case ts.SyntaxKind.TryStatement: + bindTryStatement(node as ts.TryStatement); + break; + case ts.SyntaxKind.SwitchStatement: + bindSwitchStatement(node as ts.SwitchStatement); + break; + case ts.SyntaxKind.CaseBlock: + bindCaseBlock(node as ts.CaseBlock); + break; + case ts.SyntaxKind.CaseClause: + bindCaseClause(node as ts.CaseClause); + break; + case ts.SyntaxKind.ExpressionStatement: + bindExpressionStatement(node as ts.ExpressionStatement); + break; + case ts.SyntaxKind.LabeledStatement: + bindLabeledStatement(node as ts.LabeledStatement); + break; + case ts.SyntaxKind.PrefixUnaryExpression: + bindPrefixUnaryExpressionFlow(node as ts.PrefixUnaryExpression); + break; + case ts.SyntaxKind.PostfixUnaryExpression: + bindPostfixUnaryExpressionFlow(node as ts.PostfixUnaryExpression); + break; + case ts.SyntaxKind.BinaryExpression: + if (ts.isDestructuringAssignment(node)) { + // Carry over whether we are in an assignment pattern to + // binary expressions that could actually be an initializer + inAssignmentPattern = saveInAssignmentPattern; + bindDestructuringAssignmentFlow(node); + return; + } + bindBinaryExpressionFlow(node as ts.BinaryExpression); + break; + case ts.SyntaxKind.DeleteExpression: + bindDeleteExpressionFlow(node as ts.DeleteExpression); + break; + case ts.SyntaxKind.ConditionalExpression: + bindConditionalExpressionFlow(node as ts.ConditionalExpression); + break; + case ts.SyntaxKind.VariableDeclaration: + bindVariableDeclarationFlow(node as ts.VariableDeclaration); + break; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + bindAccessExpressionFlow(node as ts.AccessExpression); + break; + case ts.SyntaxKind.CallExpression: + bindCallExpressionFlow(node as ts.CallExpression); + break; + case ts.SyntaxKind.NonNullExpression: + bindNonNullExpressionFlow(node as ts.NonNullExpression); + break; + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + bindJSDocTypeAlias(node as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag); + break; + // In source files and blocks, bind functions first to match hoisting that occurs at runtime + case ts.SyntaxKind.SourceFile: { + bindEachFunctionsFirst((node as ts.SourceFile).statements); + bind((node as ts.SourceFile).endOfFileToken); + break; } - return false; + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + bindEachFunctionsFirst((node as ts.Block).statements); + break; + case ts.SyntaxKind.BindingElement: + bindBindingElementFlow(node as ts.BindingElement); + break; + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ArrayLiteralExpression: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.SpreadElement: + // Carry over whether we are in an assignment pattern of Object and Array literals + // as well as their children that are valid assignment targets. + inAssignmentPattern = saveInAssignmentPattern; + // falls through + default: + bindEachChild(node); + break; } + bindJSDoc(node); + inAssignmentPattern = saveInAssignmentPattern; + } - function isNarrowableReference(expr: ts.Expression): boolean { - return ts.isDottedName(expr) - || (ts.isPropertyAccessExpression(expr) || ts.isNonNullExpression(expr) || ts.isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) - || ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken && isNarrowableReference(expr.right) - || ts.isElementAccessExpression(expr) && (ts.isStringOrNumericLiteralLike(expr.argumentExpression) || ts.isEntityNameExpression(expr.argumentExpression)) && isNarrowableReference(expr.expression) - || ts.isAssignmentExpression(expr) && isNarrowableReference(expr.left); + function isNarrowingExpression(expr: ts.Expression): boolean { + switch (expr.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PrivateIdentifier: + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return containsNarrowableReference(expr); + case ts.SyntaxKind.CallExpression: + return hasNarrowableArgument(expr as ts.CallExpression); + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.NonNullExpression: + return isNarrowingExpression((expr as ts.ParenthesizedExpression | ts.NonNullExpression).expression); + case ts.SyntaxKind.BinaryExpression: + return isNarrowingBinaryExpression(expr as ts.BinaryExpression); + case ts.SyntaxKind.PrefixUnaryExpression: + return (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken && isNarrowingExpression((expr as ts.PrefixUnaryExpression).operand); + case ts.SyntaxKind.TypeOfExpression: + return isNarrowingExpression((expr as ts.TypeOfExpression).expression); } + return false; + } - function containsNarrowableReference(expr: ts.Expression): boolean { - return isNarrowableReference(expr) || ts.isOptionalChain(expr) && containsNarrowableReference(expr.expression); - } + function isNarrowableReference(expr: ts.Expression): boolean { + return ts.isDottedName(expr) + || (ts.isPropertyAccessExpression(expr) || ts.isNonNullExpression(expr) || ts.isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) + || ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken && isNarrowableReference(expr.right) + || ts.isElementAccessExpression(expr) && (ts.isStringOrNumericLiteralLike(expr.argumentExpression) || ts.isEntityNameExpression(expr.argumentExpression)) && isNarrowableReference(expr.expression) + || ts.isAssignmentExpression(expr) && isNarrowableReference(expr.left); + } - function hasNarrowableArgument(expr: ts.CallExpression) { - if (expr.arguments) { - for (const argument of expr.arguments) { - if (containsNarrowableReference(argument)) { - return true; - } + function containsNarrowableReference(expr: ts.Expression): boolean { + return isNarrowableReference(expr) || ts.isOptionalChain(expr) && containsNarrowableReference(expr.expression); + } + + function hasNarrowableArgument(expr: ts.CallExpression) { + if (expr.arguments) { + for (const argument of expr.arguments) { + if (containsNarrowableReference(argument)) { + return true; } } - if (expr.expression.kind === ts.SyntaxKind.PropertyAccessExpression && - containsNarrowableReference((expr.expression as ts.PropertyAccessExpression).expression)) { - return true; - } - return false; } - - function isNarrowingTypeofOperands(expr1: ts.Expression, expr2: ts.Expression) { - return ts.isTypeOfExpression(expr1) && isNarrowableOperand(expr1.expression) && ts.isStringLiteralLike(expr2); - } - - function isNarrowingBinaryExpression(expr: ts.BinaryExpression) { - switch (expr.operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.BarBarEqualsToken: - case ts.SyntaxKind.AmpersandAmpersandEqualsToken: - case ts.SyntaxKind.QuestionQuestionEqualsToken: - return containsNarrowableReference(expr.left); - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return isNarrowableOperand(expr.left) || isNarrowableOperand(expr.right) || - isNarrowingTypeofOperands(expr.right, expr.left) || isNarrowingTypeofOperands(expr.left, expr.right); - case ts.SyntaxKind.InstanceOfKeyword: - return isNarrowableOperand(expr.left); - case ts.SyntaxKind.InKeyword: - return isNarrowingExpression(expr.right); - case ts.SyntaxKind.CommaToken: - return isNarrowingExpression(expr.right); - } - return false; + if (expr.expression.kind === ts.SyntaxKind.PropertyAccessExpression && + containsNarrowableReference((expr.expression as ts.PropertyAccessExpression).expression)) { + return true; } + return false; + } - function isNarrowableOperand(expr: ts.Expression): boolean { - switch (expr.kind) { - case ts.SyntaxKind.ParenthesizedExpression: - return isNarrowableOperand((expr as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.BinaryExpression: - switch ((expr as ts.BinaryExpression).operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - return isNarrowableOperand((expr as ts.BinaryExpression).left); - case ts.SyntaxKind.CommaToken: - return isNarrowableOperand((expr as ts.BinaryExpression).right); - } - } - return containsNarrowableReference(expr); - } + function isNarrowingTypeofOperands(expr1: ts.Expression, expr2: ts.Expression) { + return ts.isTypeOfExpression(expr1) && isNarrowableOperand(expr1.expression) && ts.isStringLiteralLike(expr2); + } - function createBranchLabel(): ts.FlowLabel { - return initFlowNode({ flags: ts.FlowFlags.BranchLabel, antecedents: undefined }); + function isNarrowingBinaryExpression(expr: ts.BinaryExpression) { + switch (expr.operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + return containsNarrowableReference(expr.left); + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return isNarrowableOperand(expr.left) || isNarrowableOperand(expr.right) || + isNarrowingTypeofOperands(expr.right, expr.left) || isNarrowingTypeofOperands(expr.left, expr.right); + case ts.SyntaxKind.InstanceOfKeyword: + return isNarrowableOperand(expr.left); + case ts.SyntaxKind.InKeyword: + return isNarrowingExpression(expr.right); + case ts.SyntaxKind.CommaToken: + return isNarrowingExpression(expr.right); } + return false; + } - function createLoopLabel(): ts.FlowLabel { - return initFlowNode({ flags: ts.FlowFlags.LoopLabel, antecedents: undefined }); + function isNarrowableOperand(expr: ts.Expression): boolean { + switch (expr.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + return isNarrowableOperand((expr as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.BinaryExpression: + switch ((expr as ts.BinaryExpression).operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + return isNarrowableOperand((expr as ts.BinaryExpression).left); + case ts.SyntaxKind.CommaToken: + return isNarrowableOperand((expr as ts.BinaryExpression).right); + } } + return containsNarrowableReference(expr); + } - function createReduceLabel(target: ts.FlowLabel, antecedents: ts.FlowNode[], antecedent: ts.FlowNode): ts.FlowReduceLabel { - return initFlowNode({ flags: ts.FlowFlags.ReduceLabel, target, antecedents, antecedent }); - } + function createBranchLabel(): ts.FlowLabel { + return initFlowNode({ flags: ts.FlowFlags.BranchLabel, antecedents: undefined }); + } - function setFlowNodeReferenced(flow: ts.FlowNode) { - // On first reference we set the Referenced flag, thereafter we set the Shared flag - flow.flags |= flow.flags & ts.FlowFlags.Referenced ? ts.FlowFlags.Shared : ts.FlowFlags.Referenced; - } + function createLoopLabel(): ts.FlowLabel { + return initFlowNode({ flags: ts.FlowFlags.LoopLabel, antecedents: undefined }); + } - function addAntecedent(label: ts.FlowLabel, antecedent: ts.FlowNode): void { - if (!(antecedent.flags & ts.FlowFlags.Unreachable) && !ts.contains(label.antecedents, antecedent)) { - (label.antecedents || (label.antecedents = [])).push(antecedent); - setFlowNodeReferenced(antecedent); - } - } + function createReduceLabel(target: ts.FlowLabel, antecedents: ts.FlowNode[], antecedent: ts.FlowNode): ts.FlowReduceLabel { + return initFlowNode({ flags: ts.FlowFlags.ReduceLabel, target, antecedents, antecedent }); + } - function createFlowCondition(flags: ts.FlowFlags, antecedent: ts.FlowNode, expression: ts.Expression | undefined): ts.FlowNode { - if (antecedent.flags & ts.FlowFlags.Unreachable) { - return antecedent; - } - if (!expression) { - return flags & ts.FlowFlags.TrueCondition ? antecedent : unreachableFlow; - } - if ((expression.kind === ts.SyntaxKind.TrueKeyword && flags & ts.FlowFlags.FalseCondition || - expression.kind === ts.SyntaxKind.FalseKeyword && flags & ts.FlowFlags.TrueCondition) && - !ts.isExpressionOfOptionalChainRoot(expression) && !ts.isNullishCoalesce(expression.parent)) { - return unreachableFlow; - } - if (!isNarrowingExpression(expression)) { - return antecedent; - } - setFlowNodeReferenced(antecedent); - return initFlowNode({ flags, antecedent, node: expression }); - } + function setFlowNodeReferenced(flow: ts.FlowNode) { + // On first reference we set the Referenced flag, thereafter we set the Shared flag + flow.flags |= flow.flags & ts.FlowFlags.Referenced ? ts.FlowFlags.Shared : ts.FlowFlags.Referenced; + } - function createFlowSwitchClause(antecedent: ts.FlowNode, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number): ts.FlowNode { + function addAntecedent(label: ts.FlowLabel, antecedent: ts.FlowNode): void { + if (!(antecedent.flags & ts.FlowFlags.Unreachable) && !ts.contains(label.antecedents, antecedent)) { + (label.antecedents || (label.antecedents = [])).push(antecedent); setFlowNodeReferenced(antecedent); - return initFlowNode({ flags: ts.FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd }); } + } - function createFlowMutation(flags: ts.FlowFlags, antecedent: ts.FlowNode, node: ts.Expression | ts.VariableDeclaration | ts.ArrayBindingElement): ts.FlowNode { - setFlowNodeReferenced(antecedent); - const result = initFlowNode({ flags, antecedent, node }); - if (currentExceptionTarget) { - addAntecedent(currentExceptionTarget, result); - } - return result; + function createFlowCondition(flags: ts.FlowFlags, antecedent: ts.FlowNode, expression: ts.Expression | undefined): ts.FlowNode { + if (antecedent.flags & ts.FlowFlags.Unreachable) { + return antecedent; } - - function createFlowCall(antecedent: ts.FlowNode, node: ts.CallExpression): ts.FlowNode { - setFlowNodeReferenced(antecedent); - return initFlowNode({ flags: ts.FlowFlags.Call, antecedent, node }); + if (!expression) { + return flags & ts.FlowFlags.TrueCondition ? antecedent : unreachableFlow; } - - function finishFlowLabel(flow: ts.FlowLabel): ts.FlowNode { - const antecedents = flow.antecedents; - if (!antecedents) { - return unreachableFlow; - } - if (antecedents.length === 1) { - return antecedents[0]; - } - return flow; + if ((expression.kind === ts.SyntaxKind.TrueKeyword && flags & ts.FlowFlags.FalseCondition || + expression.kind === ts.SyntaxKind.FalseKeyword && flags & ts.FlowFlags.TrueCondition) && + !ts.isExpressionOfOptionalChainRoot(expression) && !ts.isNullishCoalesce(expression.parent)) { + return unreachableFlow; } - - function isStatementCondition(node: ts.Node) { - const parent = node.parent; - switch (parent.kind) { - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.DoStatement: - return (parent as ts.IfStatement | ts.WhileStatement | ts.DoStatement).expression === node; - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ConditionalExpression: - return (parent as ts.ForStatement | ts.ConditionalExpression).condition === node; - } - return false; + if (!isNarrowingExpression(expression)) { + return antecedent; } + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags, antecedent, node: expression }); + } - function isLogicalExpression(node: ts.Node) { - while (true) { - if (node.kind === ts.SyntaxKind.ParenthesizedExpression) { - node = (node as ts.ParenthesizedExpression).expression; - } - else if (node.kind === ts.SyntaxKind.PrefixUnaryExpression && (node as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken) { - node = (node as ts.PrefixUnaryExpression).operand; - } - else { - return node.kind === ts.SyntaxKind.BinaryExpression && ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || - (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken || - (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken); - } - } - } + function createFlowSwitchClause(antecedent: ts.FlowNode, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number): ts.FlowNode { + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags: ts.FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd }); + } - function isLogicalAssignmentExpression(node: ts.Node) { - node = ts.skipParentheses(node); - return ts.isBinaryExpression(node) && ts.isLogicalOrCoalescingAssignmentOperator(node.operatorToken.kind); + function createFlowMutation(flags: ts.FlowFlags, antecedent: ts.FlowNode, node: ts.Expression | ts.VariableDeclaration | ts.ArrayBindingElement): ts.FlowNode { + setFlowNodeReferenced(antecedent); + const result = initFlowNode({ flags, antecedent, node }); + if (currentExceptionTarget) { + addAntecedent(currentExceptionTarget, result); } + return result; + } - function isTopLevelLogicalExpression(node: ts.Node): boolean { - while (ts.isParenthesizedExpression(node.parent) || - ts.isPrefixUnaryExpression(node.parent) && node.parent.operator === ts.SyntaxKind.ExclamationToken) { - node = node.parent; - } - return !isStatementCondition(node) && - !isLogicalExpression(node.parent) && - !(ts.isOptionalChain(node.parent) && node.parent.expression === node); + function createFlowCall(antecedent: ts.FlowNode, node: ts.CallExpression): ts.FlowNode { + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags: ts.FlowFlags.Call, antecedent, node }); + } + + function finishFlowLabel(flow: ts.FlowLabel): ts.FlowNode { + const antecedents = flow.antecedents; + if (!antecedents) { + return unreachableFlow; + } + if (antecedents.length === 1) { + return antecedents[0]; } + return flow; + } - function doWithConditionalBranches(action: (value: T) => void, value: T, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { - const savedTrueTarget = currentTrueTarget; - const savedFalseTarget = currentFalseTarget; - currentTrueTarget = trueTarget; - currentFalseTarget = falseTarget; - action(value); - currentTrueTarget = savedTrueTarget; - currentFalseTarget = savedFalseTarget; + function isStatementCondition(node: ts.Node) { + const parent = node.parent; + switch (parent.kind) { + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.DoStatement: + return (parent as ts.IfStatement | ts.WhileStatement | ts.DoStatement).expression === node; + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ConditionalExpression: + return (parent as ts.ForStatement | ts.ConditionalExpression).condition === node; } + return false; + } - function bindCondition(node: ts.Expression | undefined, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { - doWithConditionalBranches(bind, node, trueTarget, falseTarget); - if (!node || !isLogicalAssignmentExpression(node) && !isLogicalExpression(node) && !(ts.isOptionalChain(node) && ts.isOutermostOptionalChain(node))) { - addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); + function isLogicalExpression(node: ts.Node) { + while (true) { + if (node.kind === ts.SyntaxKind.ParenthesizedExpression) { + node = (node as ts.ParenthesizedExpression).expression; + } + else if (node.kind === ts.SyntaxKind.PrefixUnaryExpression && (node as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken) { + node = (node as ts.PrefixUnaryExpression).operand; + } + else { + return node.kind === ts.SyntaxKind.BinaryExpression && ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken || + (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken); } } + } - function bindIterativeStatement(node: ts.Statement, breakTarget: ts.FlowLabel, continueTarget: ts.FlowLabel): void { - const saveBreakTarget = currentBreakTarget; - const saveContinueTarget = currentContinueTarget; - currentBreakTarget = breakTarget; - currentContinueTarget = continueTarget; - bind(node); - currentBreakTarget = saveBreakTarget; - currentContinueTarget = saveContinueTarget; + function isLogicalAssignmentExpression(node: ts.Node) { + node = ts.skipParentheses(node); + return ts.isBinaryExpression(node) && ts.isLogicalOrCoalescingAssignmentOperator(node.operatorToken.kind); + } + + function isTopLevelLogicalExpression(node: ts.Node): boolean { + while (ts.isParenthesizedExpression(node.parent) || + ts.isPrefixUnaryExpression(node.parent) && node.parent.operator === ts.SyntaxKind.ExclamationToken) { + node = node.parent; } + return !isStatementCondition(node) && + !isLogicalExpression(node.parent) && + !(ts.isOptionalChain(node.parent) && node.parent.expression === node); + } - function setContinueTarget(node: ts.Node, target: ts.FlowLabel) { - let label = activeLabelList; - while (label && node.parent.kind === ts.SyntaxKind.LabeledStatement) { - label.continueTarget = target; - label = label.next; - node = node.parent; - } - return target; - } - - function bindWhileStatement(node: ts.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: ts.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: ts.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: ts.ForInOrOfStatement): void { - const preLoopLabel = setContinueTarget(node, createLoopLabel()); - const postLoopLabel = createBranchLabel(); - bind(node.expression); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = preLoopLabel; - if (node.kind === ts.SyntaxKind.ForOfStatement) { - bind(node.awaitModifier); - } - addAntecedent(postLoopLabel, currentFlow); - bind(node.initializer); - if (node.initializer.kind !== ts.SyntaxKind.VariableDeclarationList) { - bindAssignmentTargetFlow(node.initializer); - } - bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = finishFlowLabel(postLoopLabel); + function doWithConditionalBranches(action: (value: T) => void, value: T, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { + const savedTrueTarget = currentTrueTarget; + const savedFalseTarget = currentFalseTarget; + currentTrueTarget = trueTarget; + currentFalseTarget = falseTarget; + action(value); + currentTrueTarget = savedTrueTarget; + currentFalseTarget = savedFalseTarget; + } + + function bindCondition(node: ts.Expression | undefined, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { + doWithConditionalBranches(bind, node, trueTarget, falseTarget); + if (!node || !isLogicalAssignmentExpression(node) && !isLogicalExpression(node) && !(ts.isOptionalChain(node) && ts.isOutermostOptionalChain(node))) { + addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); } + } - function bindIfStatement(node: ts.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 bindIterativeStatement(node: ts.Statement, breakTarget: ts.FlowLabel, continueTarget: ts.FlowLabel): void { + const saveBreakTarget = currentBreakTarget; + const saveContinueTarget = currentContinueTarget; + currentBreakTarget = breakTarget; + currentContinueTarget = continueTarget; + bind(node); + currentBreakTarget = saveBreakTarget; + currentContinueTarget = saveContinueTarget; + } + + function setContinueTarget(node: ts.Node, target: ts.FlowLabel) { + let label = activeLabelList; + while (label && node.parent.kind === ts.SyntaxKind.LabeledStatement) { + label.continueTarget = target; + label = label.next; + node = node.parent; } + return target; + } - function bindReturnOrThrow(node: ts.ReturnStatement | ts.ThrowStatement): void { - bind(node.expression); - if (node.kind === ts.SyntaxKind.ReturnStatement) { - hasExplicitReturn = true; - if (currentReturnTarget) { - addAntecedent(currentReturnTarget, currentFlow); - } + function bindWhileStatement(node: ts.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: ts.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: ts.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: ts.ForInOrOfStatement): void { + const preLoopLabel = setContinueTarget(node, createLoopLabel()); + const postLoopLabel = createBranchLabel(); + bind(node.expression); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = preLoopLabel; + if (node.kind === ts.SyntaxKind.ForOfStatement) { + bind(node.awaitModifier); + } + addAntecedent(postLoopLabel, currentFlow); + bind(node.initializer); + if (node.initializer.kind !== ts.SyntaxKind.VariableDeclarationList) { + bindAssignmentTargetFlow(node.initializer); + } + bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = finishFlowLabel(postLoopLabel); + } + + function bindIfStatement(node: ts.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: ts.ReturnStatement | ts.ThrowStatement): void { + bind(node.expression); + if (node.kind === ts.SyntaxKind.ReturnStatement) { + hasExplicitReturn = true; + if (currentReturnTarget) { + addAntecedent(currentReturnTarget, currentFlow); } - currentFlow = unreachableFlow; } + currentFlow = unreachableFlow; + } - function findActiveLabel(name: ts.__String) { - for (let label = activeLabelList; label; label = label.next) { - if (label.name === name) { - return label; - } + function findActiveLabel(name: ts.__String) { + for (let label = activeLabelList; label; label = label.next) { + if (label.name === name) { + return label; } - return undefined; } + return undefined; + } - function bindBreakOrContinueFlow(node: ts.BreakOrContinueStatement, breakTarget: ts.FlowLabel | undefined, continueTarget: ts.FlowLabel | undefined) { - const flowLabel = node.kind === ts.SyntaxKind.BreakStatement ? breakTarget : continueTarget; - if (flowLabel) { - addAntecedent(flowLabel, currentFlow); - currentFlow = unreachableFlow; - } + function bindBreakOrContinueFlow(node: ts.BreakOrContinueStatement, breakTarget: ts.FlowLabel | undefined, continueTarget: ts.FlowLabel | undefined) { + const flowLabel = node.kind === ts.SyntaxKind.BreakStatement ? breakTarget : continueTarget; + if (flowLabel) { + addAntecedent(flowLabel, currentFlow); + currentFlow = unreachableFlow; } + } - function bindBreakOrContinueStatement(node: ts.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 bindBreakOrContinueStatement(node: ts.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: ts.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; - } + function bindTryStatement(node: ts.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 = ts.concatenate(ts.concatenate(normalExitLabel.antecedents, exceptionLabel.antecedents), returnLabel.antecedents); + currentFlow = finallyLabel; + bind(node.finallyBlock); + if (currentFlow.flags & ts.FlowFlags.Unreachable) { + // If the end of the finally block is unreachable, the end of the entire try statement is unreachable. + 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 = ts.concatenate(ts.concatenate(normalExitLabel.antecedents, exceptionLabel.antecedents), returnLabel.antecedents); - currentFlow = finallyLabel; - bind(node.finallyBlock); - if (currentFlow.flags & ts.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)); } - 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 we have an outer exception target (i.e. a containing try-finally or try-catch-finally), add a - // control flow that goes back through the finally blok and back through each possible exception source. - if (currentExceptionTarget && exceptionLabel.antecedents) { - addAntecedent(currentExceptionTarget, createReduceLabel(finallyLabel, exceptionLabel.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; + // If we have an outer exception target (i.e. a containing try-finally or try-catch-finally), add a + // control flow that goes back through the finally blok and back through each possible exception source. + if (currentExceptionTarget && exceptionLabel.antecedents) { + addAntecedent(currentExceptionTarget, createReduceLabel(finallyLabel, exceptionLabel.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; } - else { - currentFlow = finishFlowLabel(normalExitLabel); - } } + else { + currentFlow = finishFlowLabel(normalExitLabel); + } + } - function bindSwitchStatement(node: ts.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 = ts.forEach(node.caseBlock.clauses, c => c.kind === ts.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)); + function bindSwitchStatement(node: ts.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 = ts.forEach(node.caseBlock.clauses, c => c.kind === ts.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: ts.CaseBlock): void { + 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 & ts.FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) { + clause.fallthroughFlowNode = currentFlow; } - currentBreakTarget = saveBreakTarget; - preSwitchCaseFlow = savePreSwitchCaseFlow; - currentFlow = finishFlowLabel(postSwitchLabel); } + } + + function bindCaseClause(node: ts.CaseClause): void { + const saveCurrentFlow = currentFlow; + currentFlow = preSwitchCaseFlow!; + bind(node.expression); + currentFlow = saveCurrentFlow; + bindEach(node.statements); + } + + function bindExpressionStatement(node: ts.ExpressionStatement): void { + bind(node.expression); + maybeBindExpressionFlowIfCall(node.expression); + } - function bindCaseBlock(node: ts.CaseBlock): void { - 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 & ts.FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) { - clause.fallthroughFlowNode = currentFlow; - } + function maybeBindExpressionFlowIfCall(node: ts.Expression) { + // A top level or comma expression 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.kind === ts.SyntaxKind.CallExpression) { + const call = node as ts.CallExpression; + if (call.expression.kind !== ts.SyntaxKind.SuperKeyword && ts.isDottedName(call.expression)) { + currentFlow = createFlowCall(currentFlow, call); } } + } - function bindCaseClause(node: ts.CaseClause): void { - const saveCurrentFlow = currentFlow; - currentFlow = preSwitchCaseFlow!; - bind(node.expression); - currentFlow = saveCurrentFlow; - bindEach(node.statements); - } + function bindLabeledStatement(node: ts.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(ts.unusedLabelIsError(options), node.label, ts.Diagnostics.Unused_label); + } + activeLabelList = activeLabelList.next; + addAntecedent(postStatementLabel, currentFlow); + currentFlow = finishFlowLabel(postStatementLabel); + } - function bindExpressionStatement(node: ts.ExpressionStatement): void { - bind(node.expression); - maybeBindExpressionFlowIfCall(node.expression); + function bindDestructuringTargetFlow(node: ts.Expression) { + if (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { + bindAssignmentTargetFlow((node as ts.BinaryExpression).left); } - - function maybeBindExpressionFlowIfCall(node: ts.Expression) { - // A top level or comma expression 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.kind === ts.SyntaxKind.CallExpression) { - const call = node as ts.CallExpression; - if (call.expression.kind !== ts.SyntaxKind.SuperKeyword && ts.isDottedName(call.expression)) { - currentFlow = createFlowCall(currentFlow, call); - } - } + else { + bindAssignmentTargetFlow(node); } + } - function bindLabeledStatement(node: ts.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(ts.unusedLabelIsError(options), node.label, ts.Diagnostics.Unused_label); - } - activeLabelList = activeLabelList.next; - addAntecedent(postStatementLabel, currentFlow); - currentFlow = finishFlowLabel(postStatementLabel); + function bindAssignmentTargetFlow(node: ts.Expression) { + if (isNarrowableReference(node)) { + currentFlow = createFlowMutation(ts.FlowFlags.Assignment, currentFlow, node); } - - function bindDestructuringTargetFlow(node: ts.Expression) { - if (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { - bindAssignmentTargetFlow((node as ts.BinaryExpression).left); - } - else { - bindAssignmentTargetFlow(node); + else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) { + for (const e of (node as ts.ArrayLiteralExpression).elements) { + if (e.kind === ts.SyntaxKind.SpreadElement) { + bindAssignmentTargetFlow((e as ts.SpreadElement).expression); + } + else { + bindDestructuringTargetFlow(e); + } } } - - function bindAssignmentTargetFlow(node: ts.Expression) { - if (isNarrowableReference(node)) { - currentFlow = createFlowMutation(ts.FlowFlags.Assignment, currentFlow, node); - } - else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) { - for (const e of (node as ts.ArrayLiteralExpression).elements) { - if (e.kind === ts.SyntaxKind.SpreadElement) { - bindAssignmentTargetFlow((e as ts.SpreadElement).expression); - } - else { - bindDestructuringTargetFlow(e); - } + else if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) { + for (const p of (node as ts.ObjectLiteralExpression).properties) { + if (p.kind === ts.SyntaxKind.PropertyAssignment) { + bindDestructuringTargetFlow(p.initializer); } - } - else if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) { - for (const p of (node as ts.ObjectLiteralExpression).properties) { - if (p.kind === ts.SyntaxKind.PropertyAssignment) { - bindDestructuringTargetFlow(p.initializer); - } - else if (p.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - bindAssignmentTargetFlow(p.name); - } - else if (p.kind === ts.SyntaxKind.SpreadAssignment) { - bindAssignmentTargetFlow(p.expression); - } + else if (p.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + bindAssignmentTargetFlow(p.name); + } + else if (p.kind === ts.SyntaxKind.SpreadAssignment) { + bindAssignmentTargetFlow(p.expression); } } } + } - function bindLogicalLikeExpression(node: ts.BinaryExpression, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { - const preRightLabel = createBranchLabel(); - if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) { - bindCondition(node.left, preRightLabel, falseTarget); - } - else { - bindCondition(node.left, trueTarget, preRightLabel); - } - currentFlow = finishFlowLabel(preRightLabel); - bind(node.operatorToken); + function bindLogicalLikeExpression(node: ts.BinaryExpression, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { + const preRightLabel = createBranchLabel(); + if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) { + bindCondition(node.left, preRightLabel, falseTarget); + } + else { + bindCondition(node.left, trueTarget, preRightLabel); + } + currentFlow = finishFlowLabel(preRightLabel); + bind(node.operatorToken); - if (ts.isLogicalOrCoalescingAssignmentOperator(node.operatorToken.kind)) { - doWithConditionalBranches(bind, node.right, trueTarget, falseTarget); - bindAssignmentTargetFlow(node.left); + if (ts.isLogicalOrCoalescingAssignmentOperator(node.operatorToken.kind)) { + doWithConditionalBranches(bind, node.right, trueTarget, falseTarget); + bindAssignmentTargetFlow(node.left); - addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); - } - else { - bindCondition(node.right, trueTarget, falseTarget); - } + addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); } - - function bindPrefixUnaryExpressionFlow(node: ts.PrefixUnaryExpression) { - if (node.operator === ts.SyntaxKind.ExclamationToken) { - const saveTrueTarget = currentTrueTarget; - currentTrueTarget = currentFalseTarget; - currentFalseTarget = saveTrueTarget; - bindEachChild(node); - currentFalseTarget = currentTrueTarget; - currentTrueTarget = saveTrueTarget; - } - else { - bindEachChild(node); - if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { - bindAssignmentTargetFlow(node.operand); - } - } + else { + bindCondition(node.right, trueTarget, falseTarget); } + } - function bindPostfixUnaryExpressionFlow(node: ts.PostfixUnaryExpression) { + function bindPrefixUnaryExpressionFlow(node: ts.PrefixUnaryExpression) { + if (node.operator === ts.SyntaxKind.ExclamationToken) { + const saveTrueTarget = currentTrueTarget; + currentTrueTarget = currentFalseTarget; + currentFalseTarget = saveTrueTarget; + bindEachChild(node); + currentFalseTarget = currentTrueTarget; + currentTrueTarget = saveTrueTarget; + } + else { bindEachChild(node); if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { bindAssignmentTargetFlow(node.operand); } } + } - function bindDestructuringAssignmentFlow(node: ts.DestructuringAssignment) { - if (inAssignmentPattern) { - inAssignmentPattern = false; - bind(node.operatorToken); - bind(node.right); - inAssignmentPattern = true; - bind(node.left); - } - else { - inAssignmentPattern = true; - bind(node.left); - inAssignmentPattern = false; - bind(node.operatorToken); - bind(node.right); - } - bindAssignmentTargetFlow(node.left); + function bindPostfixUnaryExpressionFlow(node: ts.PostfixUnaryExpression) { + bindEachChild(node); + if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { + bindAssignmentTargetFlow(node.operand); + } + } + + function bindDestructuringAssignmentFlow(node: ts.DestructuringAssignment) { + if (inAssignmentPattern) { + inAssignmentPattern = false; + bind(node.operatorToken); + bind(node.right); + inAssignmentPattern = true; + bind(node.left); + } + else { + inAssignmentPattern = true; + bind(node.left); + inAssignmentPattern = false; + bind(node.operatorToken); + bind(node.right); } + bindAssignmentTargetFlow(node.left); + } - function createBindBinaryExpressionFlow() { - interface WorkArea { - stackIndex: number; - skip: boolean; - inStrictModeStack: (boolean | undefined)[]; - parentStack: (ts.Node | undefined)[]; - } - - return ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, /*foldState*/ undefined); - function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined) { - if (state) { - state.stackIndex++; - // Emulate the work that `bind` does before reaching `bindChildren`. A normal call to - // `bindBinaryExpressionFlow` will already have done this work. - ts.setParent(node, parent); - const saveInStrictMode = inStrictMode; - bindWorker(node); - const saveParent = parent; - parent = node; - state.skip = false; - state.inStrictModeStack[state.stackIndex] = saveInStrictMode; - state.parentStack[state.stackIndex] = saveParent; + function createBindBinaryExpressionFlow() { + interface WorkArea { + stackIndex: number; + skip: boolean; + inStrictModeStack: (boolean | undefined)[]; + parentStack: (ts.Node | undefined)[]; + } + + return ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, /*foldState*/ undefined); + function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined) { + if (state) { + state.stackIndex++; + // Emulate the work that `bind` does before reaching `bindChildren`. A normal call to + // `bindBinaryExpressionFlow` will already have done this work. + ts.setParent(node, parent); + const saveInStrictMode = inStrictMode; + bindWorker(node); + const saveParent = parent; + parent = node; + state.skip = false; + state.inStrictModeStack[state.stackIndex] = saveInStrictMode; + state.parentStack[state.stackIndex] = saveParent; + } + else { + state = { + stackIndex: 0, + skip: false, + inStrictModeStack: [undefined], + parentStack: [undefined] + }; + } + // 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 + const operator = node.operatorToken.kind; + if (operator === ts.SyntaxKind.AmpersandAmpersandToken || + operator === ts.SyntaxKind.BarBarToken || + operator === ts.SyntaxKind.QuestionQuestionToken || + ts.isLogicalOrCoalescingAssignmentOperator(operator)) { + if (isTopLevelLogicalExpression(node)) { + const postExpressionLabel = createBranchLabel(); + bindLogicalLikeExpression(node, postExpressionLabel, postExpressionLabel); + currentFlow = finishFlowLabel(postExpressionLabel); } else { - state = { - stackIndex: 0, - skip: false, - inStrictModeStack: [undefined], - parentStack: [undefined] - }; - } - // 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 - const operator = node.operatorToken.kind; - if (operator === ts.SyntaxKind.AmpersandAmpersandToken || - operator === ts.SyntaxKind.BarBarToken || - operator === ts.SyntaxKind.QuestionQuestionToken || - ts.isLogicalOrCoalescingAssignmentOperator(operator)) { - if (isTopLevelLogicalExpression(node)) { - const postExpressionLabel = createBranchLabel(); - bindLogicalLikeExpression(node, postExpressionLabel, postExpressionLabel); - currentFlow = finishFlowLabel(postExpressionLabel); - } - else { - bindLogicalLikeExpression(node, currentTrueTarget!, currentFalseTarget!); - } - state.skip = true; + bindLogicalLikeExpression(node, currentTrueTarget!, currentFalseTarget!); } - return state; + state.skip = true; } + return state; + } - function onLeft(left: ts.Expression, state: WorkArea, node: ts.BinaryExpression) { - if (!state.skip) { - const maybeBound = maybeBind(left); - if (node.operatorToken.kind === ts.SyntaxKind.CommaToken) { - maybeBindExpressionFlowIfCall(left); - } - return maybeBound; + function onLeft(left: ts.Expression, state: WorkArea, node: ts.BinaryExpression) { + if (!state.skip) { + const maybeBound = maybeBind(left); + if (node.operatorToken.kind === ts.SyntaxKind.CommaToken) { + maybeBindExpressionFlowIfCall(left); } + return maybeBound; } + } - function onOperator(operatorToken: ts.BinaryOperatorToken, state: WorkArea, _node: ts.BinaryExpression) { - if (!state.skip) { - bind(operatorToken); - } + function onOperator(operatorToken: ts.BinaryOperatorToken, state: WorkArea, _node: ts.BinaryExpression) { + if (!state.skip) { + bind(operatorToken); } + } - function onRight(right: ts.Expression, state: WorkArea, node: ts.BinaryExpression) { - if (!state.skip) { - const maybeBound = maybeBind(right); - if (node.operatorToken.kind === ts.SyntaxKind.CommaToken) { - maybeBindExpressionFlowIfCall(right); - } - return maybeBound; + function onRight(right: ts.Expression, state: WorkArea, node: ts.BinaryExpression) { + if (!state.skip) { + const maybeBound = maybeBind(right); + if (node.operatorToken.kind === ts.SyntaxKind.CommaToken) { + maybeBindExpressionFlowIfCall(right); } + return maybeBound; } + } - function onExit(node: ts.BinaryExpression, state: WorkArea) { - if (!state.skip) { - const operator = node.operatorToken.kind; - if (ts.isAssignmentOperator(operator) && !ts.isAssignmentTarget(node)) { - bindAssignmentTargetFlow(node.left); - if (operator === ts.SyntaxKind.EqualsToken && node.left.kind === ts.SyntaxKind.ElementAccessExpression) { - const elementAccess = node.left as ts.ElementAccessExpression; - if (isNarrowableOperand(elementAccess.expression)) { - currentFlow = createFlowMutation(ts.FlowFlags.ArrayMutation, currentFlow, node); - } + function onExit(node: ts.BinaryExpression, state: WorkArea) { + if (!state.skip) { + const operator = node.operatorToken.kind; + if (ts.isAssignmentOperator(operator) && !ts.isAssignmentTarget(node)) { + bindAssignmentTargetFlow(node.left); + if (operator === ts.SyntaxKind.EqualsToken && node.left.kind === ts.SyntaxKind.ElementAccessExpression) { + const elementAccess = node.left as ts.ElementAccessExpression; + if (isNarrowableOperand(elementAccess.expression)) { + currentFlow = createFlowMutation(ts.FlowFlags.ArrayMutation, currentFlow, node); } } } - const savedInStrictMode = state.inStrictModeStack[state.stackIndex]; - const savedParent = state.parentStack[state.stackIndex]; - if (savedInStrictMode !== undefined) { - inStrictMode = savedInStrictMode; - } - if (savedParent !== undefined) { - parent = savedParent; - } - state.skip = false; - state.stackIndex--; } - - function maybeBind(node: ts.Node) { - if (node && ts.isBinaryExpression(node) && !ts.isDestructuringAssignment(node)) { - return node; - } - bind(node); + const savedInStrictMode = state.inStrictModeStack[state.stackIndex]; + const savedParent = state.parentStack[state.stackIndex]; + if (savedInStrictMode !== undefined) { + inStrictMode = savedInStrictMode; } + if (savedParent !== undefined) { + parent = savedParent; + } + state.skip = false; + state.stackIndex--; } - function bindDeleteExpressionFlow(node: ts.DeleteExpression) { - bindEachChild(node); - if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - bindAssignmentTargetFlow(node.expression); + function maybeBind(node: ts.Node) { + if (node && ts.isBinaryExpression(node) && !ts.isDestructuringAssignment(node)) { + return node; } + bind(node); } + } - function bindConditionalExpressionFlow(node: ts.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 bindDeleteExpressionFlow(node: ts.DeleteExpression) { + bindEachChild(node); + if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + bindAssignmentTargetFlow(node.expression); } + } - function bindInitializedVariableFlow(node: ts.VariableDeclaration | ts.ArrayBindingElement) { - const name = !ts.isOmittedExpression(node) ? node.name : undefined; - if (ts.isBindingPattern(name)) { - for (const child of name.elements) { - bindInitializedVariableFlow(child); - } - } - else { - currentFlow = createFlowMutation(ts.FlowFlags.Assignment, currentFlow, node); + function bindConditionalExpressionFlow(node: ts.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: ts.VariableDeclaration | ts.ArrayBindingElement) { + const name = !ts.isOmittedExpression(node) ? node.name : undefined; + if (ts.isBindingPattern(name)) { + for (const child of name.elements) { + bindInitializedVariableFlow(child); } } + else { + currentFlow = createFlowMutation(ts.FlowFlags.Assignment, currentFlow, node); + } + } + + function bindVariableDeclarationFlow(node: ts.VariableDeclaration) { + bindEachChild(node); + if (node.initializer || ts.isForInOrOfStatement(node.parent.parent)) { + bindInitializedVariableFlow(node); + } + } - function bindVariableDeclarationFlow(node: ts.VariableDeclaration) { + function bindBindingElementFlow(node: ts.BindingElement) { + if (ts.isBindingPattern(node.name)) { + // When evaluating a binding pattern, the initializer is evaluated before the binding pattern, per: + // - https://tc39.es/ecma262/#sec-destructuring-binding-patterns-runtime-semantics-iteratorbindinginitialization + // - `BindingElement: BindingPattern Initializer?` + // - https://tc39.es/ecma262/#sec-runtime-semantics-keyedbindinginitialization + // - `BindingElement: BindingPattern Initializer?` + bindEach(node.decorators); + bindEach(node.modifiers); + bind(node.dotDotDotToken); + bind(node.propertyName); + bind(node.initializer); + bind(node.name); + } + else { bindEachChild(node); - if (node.initializer || ts.isForInOrOfStatement(node.parent.parent)) { - bindInitializedVariableFlow(node); - } - } - - function bindBindingElementFlow(node: ts.BindingElement) { - if (ts.isBindingPattern(node.name)) { - // When evaluating a binding pattern, the initializer is evaluated before the binding pattern, per: - // - https://tc39.es/ecma262/#sec-destructuring-binding-patterns-runtime-semantics-iteratorbindinginitialization - // - `BindingElement: BindingPattern Initializer?` - // - https://tc39.es/ecma262/#sec-runtime-semantics-keyedbindinginitialization - // - `BindingElement: BindingPattern Initializer?` - bindEach(node.decorators); - bindEach(node.modifiers); - bind(node.dotDotDotToken); - bind(node.propertyName); - bind(node.initializer); - bind(node.name); - } - else { - bindEachChild(node); - } } + } - function bindJSDocTypeAlias(node: ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag) { - bind(node.tagName); - if (node.kind !== ts.SyntaxKind.JSDocEnumTag && node.fullName) { - // don't bind the type name yet; that's delayed until delayedBindJSDocTypedefTag - ts.setParent(node.fullName, node); - ts.setParentRecursive(node.fullName, /*incremental*/ false); - } - if (typeof node.comment !== "string") { - bindEach(node.comment); - } + function bindJSDocTypeAlias(node: ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag) { + bind(node.tagName); + if (node.kind !== ts.SyntaxKind.JSDocEnumTag && node.fullName) { + // don't bind the type name yet; that's delayed until delayedBindJSDocTypedefTag + ts.setParent(node.fullName, node); + ts.setParentRecursive(node.fullName, /*incremental*/ false); + } + if (typeof node.comment !== "string") { + bindEach(node.comment); } + } - function bindJSDocClassTag(node: ts.JSDocClassTag) { - bindEachChild(node); - const host = ts.getHostSignatureFromJSDoc(node); - if (host && host.kind !== ts.SyntaxKind.MethodDeclaration) { - addDeclarationToSymbol(host.symbol, host, ts.SymbolFlags.Class); - } + function bindJSDocClassTag(node: ts.JSDocClassTag) { + bindEachChild(node); + const host = ts.getHostSignatureFromJSDoc(node); + if (host && host.kind !== ts.SyntaxKind.MethodDeclaration) { + addDeclarationToSymbol(host.symbol, host, ts.SymbolFlags.Class); } + } - function bindOptionalExpression(node: ts.Expression, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { - doWithConditionalBranches(bind, node, trueTarget, falseTarget); - if (!ts.isOptionalChain(node) || ts.isOutermostOptionalChain(node)) { - addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); - } + function bindOptionalExpression(node: ts.Expression, trueTarget: ts.FlowLabel, falseTarget: ts.FlowLabel) { + doWithConditionalBranches(bind, node, trueTarget, falseTarget); + if (!ts.isOptionalChain(node) || ts.isOutermostOptionalChain(node)) { + addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); } + } - function bindOptionalChainRest(node: ts.OptionalChain) { - switch (node.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - bind(node.questionDotToken); - bind(node.name); - break; - case ts.SyntaxKind.ElementAccessExpression: - bind(node.questionDotToken); - bind(node.argumentExpression); - break; - case ts.SyntaxKind.CallExpression: - bind(node.questionDotToken); - bindEach(node.typeArguments); - bindEach(node.arguments); - break; - } + function bindOptionalChainRest(node: ts.OptionalChain) { + switch (node.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + bind(node.questionDotToken); + bind(node.name); + break; + case ts.SyntaxKind.ElementAccessExpression: + bind(node.questionDotToken); + bind(node.argumentExpression); + break; + case ts.SyntaxKind.CallExpression: + bind(node.questionDotToken); + bindEach(node.typeArguments); + bindEach(node.arguments); + break; } + } - function bindOptionalChain(node: ts.OptionalChain, trueTarget: ts.FlowLabel, falseTarget: ts.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 = ts.isOptionalChainRoot(node) ? createBranchLabel() : undefined; - bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget); - if (preChainLabel) { - currentFlow = finishFlowLabel(preChainLabel); - } - doWithConditionalBranches(bindOptionalChainRest, node, trueTarget, falseTarget); - if (ts.isOutermostOptionalChain(node)) { - addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); - } + function bindOptionalChain(node: ts.OptionalChain, trueTarget: ts.FlowLabel, falseTarget: ts.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 = ts.isOptionalChainRoot(node) ? createBranchLabel() : undefined; + bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget); + if (preChainLabel) { + currentFlow = finishFlowLabel(preChainLabel); + } + doWithConditionalBranches(bindOptionalChainRest, node, trueTarget, falseTarget); + if (ts.isOutermostOptionalChain(node)) { + addAntecedent(trueTarget, createFlowCondition(ts.FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(ts.FlowFlags.FalseCondition, currentFlow, node)); } + } - function bindOptionalChainFlow(node: ts.OptionalChain) { - if (isTopLevelLogicalExpression(node)) { - const postExpressionLabel = createBranchLabel(); - bindOptionalChain(node, postExpressionLabel, postExpressionLabel); - currentFlow = finishFlowLabel(postExpressionLabel); - } - else { - bindOptionalChain(node, currentTrueTarget!, currentFalseTarget!); - } + function bindOptionalChainFlow(node: ts.OptionalChain) { + if (isTopLevelLogicalExpression(node)) { + const postExpressionLabel = createBranchLabel(); + bindOptionalChain(node, postExpressionLabel, postExpressionLabel); + currentFlow = finishFlowLabel(postExpressionLabel); + } + else { + bindOptionalChain(node, currentTrueTarget!, currentFalseTarget!); } + } - function bindNonNullExpressionFlow(node: ts.NonNullExpression | ts.NonNullChain) { - if (ts.isOptionalChain(node)) { - bindOptionalChainFlow(node); - } - else { - bindEachChild(node); - } + function bindNonNullExpressionFlow(node: ts.NonNullExpression | ts.NonNullChain) { + if (ts.isOptionalChain(node)) { + bindOptionalChainFlow(node); + } + else { + bindEachChild(node); } + } - function bindAccessExpressionFlow(node: ts.AccessExpression | ts.PropertyAccessChain | ts.ElementAccessChain) { - if (ts.isOptionalChain(node)) { - bindOptionalChainFlow(node); - } - else { - bindEachChild(node); - } + function bindAccessExpressionFlow(node: ts.AccessExpression | ts.PropertyAccessChain | ts.ElementAccessChain) { + if (ts.isOptionalChain(node)) { + bindOptionalChainFlow(node); + } + else { + bindEachChild(node); } + } - function bindCallExpressionFlow(node: ts.CallExpression | ts.CallChain) { - if (ts.isOptionalChain(node)) { - bindOptionalChainFlow(node); + function bindCallExpressionFlow(node: ts.CallExpression | ts.CallChain) { + if (ts.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 = ts.skipParentheses(node.expression); + if (expr.kind === ts.SyntaxKind.FunctionExpression || expr.kind === ts.SyntaxKind.ArrowFunction) { + bindEach(node.typeArguments); + bindEach(node.arguments); + bind(node.expression); } 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 = ts.skipParentheses(node.expression); - if (expr.kind === ts.SyntaxKind.FunctionExpression || expr.kind === ts.SyntaxKind.ArrowFunction) { - bindEach(node.typeArguments); - bindEach(node.arguments); - bind(node.expression); - } - else { - bindEachChild(node); - if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { - currentFlow = createFlowCall(currentFlow, node); - } + bindEachChild(node); + if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { + currentFlow = createFlowCall(currentFlow, node); } } - if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - const propertyAccess = node.expression as ts.PropertyAccessExpression; - if (ts.isIdentifier(propertyAccess.name) && isNarrowableOperand(propertyAccess.expression) && ts.isPushOrUnshiftIdentifier(propertyAccess.name)) { - currentFlow = createFlowMutation(ts.FlowFlags.ArrayMutation, currentFlow, node); - } + } + if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + const propertyAccess = node.expression as ts.PropertyAccessExpression; + if (ts.isIdentifier(propertyAccess.name) && isNarrowableOperand(propertyAccess.expression) && ts.isPushOrUnshiftIdentifier(propertyAccess.name)) { + currentFlow = createFlowMutation(ts.FlowFlags.ArrayMutation, currentFlow, node); } } + } - function getContainerFlags(node: ts.Node): ContainerFlags { - switch (node.kind) { - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.JSDocTypeLiteral: - case ts.SyntaxKind.JsxAttributes: - return ContainerFlags.IsContainer; + function getContainerFlags(node: ts.Node): ContainerFlags { + switch (node.kind) { + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.JSDocTypeLiteral: + case ts.SyntaxKind.JsxAttributes: + return ContainerFlags.IsContainer; - case ts.SyntaxKind.InterfaceDeclaration: - return ContainerFlags.IsContainer | ContainerFlags.IsInterface; + case ts.SyntaxKind.InterfaceDeclaration: + return ContainerFlags.IsContainer | ContainerFlags.IsInterface; - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.MappedType: - case ts.SyntaxKind.IndexSignature: - return ContainerFlags.IsContainer | ContainerFlags.HasLocals; + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.MappedType: + case ts.SyntaxKind.IndexSignature: + return ContainerFlags.IsContainer | ContainerFlags.HasLocals; + + case ts.SyntaxKind.SourceFile: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals; + + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: + if (ts.isObjectLiteralOrClassExpressionMethodOrAccessor(node)) { + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsObjectLiteralOrClassExpressionMethodOrAccessor; + } + // falls through + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.JSDocSignature: + case ts.SyntaxKind.JSDocFunctionType: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike; + + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsFunctionExpression; + + case ts.SyntaxKind.ModuleBlock: + return ContainerFlags.IsControlFlowContainer; + case ts.SyntaxKind.PropertyDeclaration: + return (node as ts.PropertyDeclaration).initializer ? ContainerFlags.IsControlFlowContainer : 0; + case ts.SyntaxKind.CatchClause: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.CaseBlock: + return ContainerFlags.IsBlockScopedContainer; + + case ts.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 ts.isFunctionLike(node.parent) || ts.isClassStaticBlockDeclaration(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer; + } - case ts.SyntaxKind.SourceFile: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals; + return ContainerFlags.None; + } - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: - if (ts.isObjectLiteralOrClassExpressionMethodOrAccessor(node)) { - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsObjectLiteralOrClassExpressionMethodOrAccessor; - } - // falls through - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.JSDocSignature: - case ts.SyntaxKind.JSDocFunctionType: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike; - - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsFunctionExpression; - - case ts.SyntaxKind.ModuleBlock: - return ContainerFlags.IsControlFlowContainer; - case ts.SyntaxKind.PropertyDeclaration: - return (node as ts.PropertyDeclaration).initializer ? ContainerFlags.IsControlFlowContainer : 0; - case ts.SyntaxKind.CatchClause: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.CaseBlock: - return ContainerFlags.IsBlockScopedContainer; - - case ts.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 ts.isFunctionLike(node.parent) || ts.isClassStaticBlockDeclaration(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer; - } - - return ContainerFlags.None; - } - - function addToContainerChain(next: ts.Node) { - if (lastContainer) { - lastContainer.nextContainer = next; - } - - lastContainer = next; - } - - function declareSymbolAndAddToSymbolTable(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.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 ts.SyntaxKind.ModuleDeclaration: - return declareModuleMember(node, symbolFlags, symbolExcludes); - - case ts.SyntaxKind.SourceFile: - return declareSourceFileMember(node, symbolFlags, symbolExcludes); - - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ClassDeclaration: - return declareClassMember(node, symbolFlags, symbolExcludes); - - case ts.SyntaxKind.EnumDeclaration: - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); - - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.JSDocTypeLiteral: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.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 ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.JSDocSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.JSDocFunctionType: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.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 declareClassMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { - return ts.isStatic(node) - ? declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes) - : declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); - } - - function declareSourceFileMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { - return ts.isExternalModule(file) - ? declareModuleMember(node, symbolFlags, symbolExcludes) - : declareSymbol(file.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } - - function hasExportDeclarations(node: ts.ModuleDeclaration | ts.SourceFile): boolean { - const body = ts.isSourceFile(node) ? node : ts.tryCast(node.body, ts.isModuleBlock); - return !!body && body.statements.some(s => ts.isExportDeclaration(s) || ts.isExportAssignment(s)); - } - - function setExportContextFlag(node: ts.Mutable) { - // 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 & ts.NodeFlags.Ambient && !hasExportDeclarations(node)) { - node.flags |= ts.NodeFlags.ExportContext; - } - else { - node.flags &= ~ts.NodeFlags.ExportContext; - } + function addToContainerChain(next: ts.Node) { + if (lastContainer) { + lastContainer.nextContainer = next; } - function bindModuleDeclaration(node: ts.ModuleDeclaration) { - setExportContextFlag(node); - if (ts.isAmbientModule(node)) { - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { - errorOnFirstToken(node, ts.Diagnostics.export_modifier_cannot_be_applied_to_ambient_modules_and_module_augmentations_since_they_are_always_visible); - } - if (ts.isModuleAugmentationExternal(node)) { - declareModuleSymbol(node); - } - else { - let pattern: string | ts.Pattern | undefined; - if (node.name.kind === ts.SyntaxKind.StringLiteral) { - const { text } = node.name; - pattern = ts.tryParsePattern(text); - if (pattern === undefined) { - errorOnFirstToken(node.name, ts.Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); - } - } + lastContainer = next; + } - const symbol = declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.ValueModule, ts.SymbolFlags.ValueModuleExcludes)!; - file.patternAmbientModules = ts.append(file.patternAmbientModules, pattern && !ts.isString(pattern) ? { pattern, symbol } : undefined); - } + function declareSymbolAndAddToSymbolTable(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.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 ts.SyntaxKind.ModuleDeclaration: + return declareModuleMember(node, symbolFlags, symbolExcludes); + + case ts.SyntaxKind.SourceFile: + return declareSourceFileMember(node, symbolFlags, symbolExcludes); + + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + return declareClassMember(node, symbolFlags, symbolExcludes); + + case ts.SyntaxKind.EnumDeclaration: + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); + + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.JSDocTypeLiteral: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.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 ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.JSDocSignature: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.JSDocFunctionType: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.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 declareClassMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { + return ts.isStatic(node) + ? declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes) + : declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); + } + + function declareSourceFileMember(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { + return ts.isExternalModule(file) + ? declareModuleMember(node, symbolFlags, symbolExcludes) + : declareSymbol(file.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); + } + + function hasExportDeclarations(node: ts.ModuleDeclaration | ts.SourceFile): boolean { + const body = ts.isSourceFile(node) ? node : ts.tryCast(node.body, ts.isModuleBlock); + return !!body && body.statements.some(s => ts.isExportDeclaration(s) || ts.isExportAssignment(s)); + } + + function setExportContextFlag(node: ts.Mutable) { + // 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 & ts.NodeFlags.Ambient && !hasExportDeclarations(node)) { + node.flags |= ts.NodeFlags.ExportContext; + } + else { + node.flags &= ~ts.NodeFlags.ExportContext; + } + } + + function bindModuleDeclaration(node: ts.ModuleDeclaration) { + setExportContextFlag(node); + if (ts.isAmbientModule(node)) { + if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { + errorOnFirstToken(node, ts.Diagnostics.export_modifier_cannot_be_applied_to_ambient_modules_and_module_augmentations_since_they_are_always_visible); + } + if (ts.isModuleAugmentationExternal(node)) { + declareModuleSymbol(node); } 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.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; + let pattern: string | ts.Pattern | undefined; + if (node.name.kind === ts.SyntaxKind.StringLiteral) { + const { text } = node.name; + pattern = ts.tryParsePattern(text); + if (pattern === undefined) { + errorOnFirstToken(node.name, ts.Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); + } } + + const symbol = declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.ValueModule, ts.SymbolFlags.ValueModuleExcludes)!; + file.patternAmbientModules = ts.append(file.patternAmbientModules, pattern && !ts.isString(pattern) ? { pattern, symbol } : undefined); } } - - function declareModuleSymbol(node: ts.ModuleDeclaration): ModuleInstanceState { - const state = getModuleInstanceState(node); - const instantiated = state !== ModuleInstanceState.NonInstantiated; - declareSymbolAndAddToSymbolTable(node, instantiated ? ts.SymbolFlags.ValueModule : ts.SymbolFlags.NamespaceModule, instantiated ? ts.SymbolFlags.ValueModuleExcludes : ts.SymbolFlags.NamespaceModuleExcludes); - return state; + 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.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 bindFunctionOrConstructorType(node: ts.SignatureDeclaration | ts.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(ts.SymbolFlags.Signature, getDeclarationName(node)!); // TODO: GH#18217 - addDeclarationToSymbol(symbol, node, ts.SymbolFlags.Signature); - const typeLiteralSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); - addDeclarationToSymbol(typeLiteralSymbol, node, ts.SymbolFlags.TypeLiteral); - typeLiteralSymbol.members = ts.createSymbolTable(); - typeLiteralSymbol.members.set(symbol.escapedName, symbol); + function declareModuleSymbol(node: ts.ModuleDeclaration): ModuleInstanceState { + const state = getModuleInstanceState(node); + const instantiated = state !== ModuleInstanceState.NonInstantiated; + declareSymbolAndAddToSymbolTable(node, instantiated ? ts.SymbolFlags.ValueModule : ts.SymbolFlags.NamespaceModule, instantiated ? ts.SymbolFlags.ValueModuleExcludes : ts.SymbolFlags.NamespaceModuleExcludes); + return state; + } + + function bindFunctionOrConstructorType(node: ts.SignatureDeclaration | ts.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(ts.SymbolFlags.Signature, getDeclarationName(node)!); // TODO: GH#18217 + addDeclarationToSymbol(symbol, node, ts.SymbolFlags.Signature); + const typeLiteralSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); + addDeclarationToSymbol(typeLiteralSymbol, node, ts.SymbolFlags.TypeLiteral); + typeLiteralSymbol.members = ts.createSymbolTable(); + typeLiteralSymbol.members.set(symbol.escapedName, symbol); + } + + function bindObjectLiteralExpression(node: ts.ObjectLiteralExpression) { + const enum ElementKind { + Property = 1, + Accessor = 2 } - function bindObjectLiteralExpression(node: ts.ObjectLiteralExpression) { - const enum ElementKind { - Property = 1, - Accessor = 2 - } + if (inStrictMode && !ts.isAssignmentTarget(node)) { + const seen = new ts.Map(); - if (inStrictMode && !ts.isAssignmentTarget(node)) { - const seen = new ts.Map(); + for (const prop of node.properties) { + if (prop.kind === ts.SyntaxKind.SpreadAssignment || prop.name.kind !== ts.SyntaxKind.Identifier) { + continue; + } - for (const prop of node.properties) { - if (prop.kind === ts.SyntaxKind.SpreadAssignment || prop.name.kind !== ts.SyntaxKind.Identifier) { - continue; - } + const identifier = prop.name; - 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 === ts.SyntaxKind.PropertyAssignment || prop.kind === ts.SyntaxKind.ShorthandPropertyAssignment || prop.kind === ts.SyntaxKind.MethodDeclaration - ? ElementKind.Property - : ElementKind.Accessor; - - const existingKind = seen.get(identifier.escapedText); - if (!existingKind) { - seen.set(identifier.escapedText, currentKind); - continue; - } + // 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 === ts.SyntaxKind.PropertyAssignment || prop.kind === ts.SyntaxKind.ShorthandPropertyAssignment || prop.kind === ts.SyntaxKind.MethodDeclaration + ? ElementKind.Property + : ElementKind.Accessor; + + const existingKind = seen.get(identifier.escapedText); + if (!existingKind) { + seen.set(identifier.escapedText, currentKind); + continue; } } - - return bindAnonymousDeclaration(node, ts.SymbolFlags.ObjectLiteral, ts.InternalSymbolName.Object); } - function bindJsxAttributes(node: ts.JsxAttributes) { - return bindAnonymousDeclaration(node, ts.SymbolFlags.ObjectLiteral, ts.InternalSymbolName.JSXAttributes); - } + return bindAnonymousDeclaration(node, ts.SymbolFlags.ObjectLiteral, ts.InternalSymbolName.Object); + } - function bindJsxAttribute(node: ts.JsxAttribute, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { - return declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); - } + function bindJsxAttributes(node: ts.JsxAttributes) { + return bindAnonymousDeclaration(node, ts.SymbolFlags.ObjectLiteral, ts.InternalSymbolName.JSXAttributes); + } - function bindAnonymousDeclaration(node: ts.Declaration, symbolFlags: ts.SymbolFlags, name: ts.__String) { - const symbol = createSymbol(symbolFlags, name); - if (symbolFlags & (ts.SymbolFlags.EnumMember | ts.SymbolFlags.ClassMember)) { - symbol.parent = container.symbol; - } - addDeclarationToSymbol(symbol, node, symbolFlags); - return symbol; - } + function bindJsxAttribute(node: ts.JsxAttribute, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { + return declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); + } - function bindBlockScopedDeclaration(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { - switch (blockScopeContainer.kind) { - case ts.SyntaxKind.ModuleDeclaration: - declareModuleMember(node, symbolFlags, symbolExcludes); - break; - case ts.SyntaxKind.SourceFile: - if (ts.isExternalOrCommonJsModule(container as ts.SourceFile)) { - declareModuleMember(node, symbolFlags, symbolExcludes); - break; - } - // falls through - default: - if (!blockScopeContainer.locals) { - blockScopeContainer.locals = ts.createSymbolTable(); - addToContainerChain(blockScopeContainer); - } - declareSymbol(blockScopeContainer.locals, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } + function bindAnonymousDeclaration(node: ts.Declaration, symbolFlags: ts.SymbolFlags, name: ts.__String) { + const symbol = createSymbol(symbolFlags, name); + if (symbolFlags & (ts.SymbolFlags.EnumMember | ts.SymbolFlags.ClassMember)) { + symbol.parent = container.symbol; } + addDeclarationToSymbol(symbol, node, symbolFlags); + return symbol; + } - 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 = typeAlias.parent.parent; - container = ts.findAncestor(host.parent, n => !!(getContainerFlags(n) & ContainerFlags.IsContainer)) || file; - blockScopeContainer = ts.getEnclosingBlockScopeContainer(host) || file; - currentFlow = initFlowNode({ flags: ts.FlowFlags.Start }); - parent = typeAlias; - bind(typeAlias.typeExpression); - const declName = ts.getNameOfDeclaration(typeAlias); - if ((ts.isJSDocEnumTag(typeAlias) || !typeAlias.fullName) && declName && ts.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, !!ts.findAncestor(declName, d => ts.isPropertyAccessExpression(d) && d.name.escapedText === "prototype"), /*containerIsClass*/ false); - const oldContainer = container; - switch (ts.getAssignmentDeclarationPropertyAccessKind(declName.parent)) { - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.ModuleExports: - if (!ts.isExternalOrCommonJsModule(file)) { - container = undefined!; - } - else { - container = file; - } - break; - case ts.AssignmentDeclarationKind.ThisProperty: - container = declName.parent.expression; - break; - case ts.AssignmentDeclarationKind.PrototypeProperty: - container = (declName.parent.expression as ts.PropertyAccessExpression).name; - break; - case ts.AssignmentDeclarationKind.Property: - container = isExportsOrModuleExportsOrAlias(file, declName.parent.expression) ? file - : ts.isPropertyAccessExpression(declName.parent.expression) ? declName.parent.expression.name - : declName.parent.expression; - break; - case ts.AssignmentDeclarationKind.None: - return ts.Debug.fail("Shouldn't have detected typedef or enum on non-assignment declaration"); - } - if (container) { - declareModuleMember(typeAlias, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); - } - container = oldContainer; - } - } - else if (ts.isJSDocEnumTag(typeAlias) || !typeAlias.fullName || typeAlias.fullName.kind === ts.SyntaxKind.Identifier) { - parent = typeAlias.parent; - bindBlockScopedDeclaration(typeAlias, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); + function bindBlockScopedDeclaration(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { + switch (blockScopeContainer.kind) { + case ts.SyntaxKind.ModuleDeclaration: + declareModuleMember(node, symbolFlags, symbolExcludes); + break; + case ts.SyntaxKind.SourceFile: + if (ts.isExternalOrCommonJsModule(container as ts.SourceFile)) { + declareModuleMember(node, symbolFlags, symbolExcludes); + break; } - else { - bind(typeAlias.fullName); + // falls through + default: + if (!blockScopeContainer.locals) { + blockScopeContainer.locals = ts.createSymbolTable(); + addToContainerChain(blockScopeContainer); } - } - container = saveContainer; - lastContainer = saveLastContainer; - blockScopeContainer = saveBlockScopeContainer; - parent = saveParent; - currentFlow = saveCurrentFlow; + declareSymbol(blockScopeContainer.locals, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } + } - // 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, as well as `yield` or `await` in - // [Yield] or [Await] contexts, respectively. - function checkContextualIdentifier(node: ts.Identifier) { - // Report error only if there are no parse errors in file - if (!file.parseDiagnostics.length && - !(node.flags & ts.NodeFlags.Ambient) && - !(node.flags & ts.NodeFlags.JSDoc) && - !ts.isIdentifierName(node)) { - - // strict mode identifiers - if (inStrictMode && - node.originalKeywordKind! >= ts.SyntaxKind.FirstFutureReservedWord && - node.originalKeywordKind! <= ts.SyntaxKind.LastFutureReservedWord) { - file.bindDiagnostics.push(createDiagnosticForNode(node, getStrictModeIdentifierMessage(node), ts.declarationNameToString(node))); - } - else if (node.originalKeywordKind === ts.SyntaxKind.AwaitKeyword) { - if (ts.isExternalModule(file) && ts.isInTopLevelContext(node)) { - file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_at_the_top_level_of_a_module, ts.declarationNameToString(node))); + 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 = typeAlias.parent.parent; + container = ts.findAncestor(host.parent, n => !!(getContainerFlags(n) & ContainerFlags.IsContainer)) || file; + blockScopeContainer = ts.getEnclosingBlockScopeContainer(host) || file; + currentFlow = initFlowNode({ flags: ts.FlowFlags.Start }); + parent = typeAlias; + bind(typeAlias.typeExpression); + const declName = ts.getNameOfDeclaration(typeAlias); + if ((ts.isJSDocEnumTag(typeAlias) || !typeAlias.fullName) && declName && ts.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, !!ts.findAncestor(declName, d => ts.isPropertyAccessExpression(d) && d.name.escapedText === "prototype"), /*containerIsClass*/ false); + const oldContainer = container; + switch (ts.getAssignmentDeclarationPropertyAccessKind(declName.parent)) { + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.ModuleExports: + if (!ts.isExternalOrCommonJsModule(file)) { + container = undefined!; + } + else { + container = file; + } + break; + case ts.AssignmentDeclarationKind.ThisProperty: + container = declName.parent.expression; + break; + case ts.AssignmentDeclarationKind.PrototypeProperty: + container = (declName.parent.expression as ts.PropertyAccessExpression).name; + break; + case ts.AssignmentDeclarationKind.Property: + container = isExportsOrModuleExportsOrAlias(file, declName.parent.expression) ? file + : ts.isPropertyAccessExpression(declName.parent.expression) ? declName.parent.expression.name + : declName.parent.expression; + break; + case ts.AssignmentDeclarationKind.None: + return ts.Debug.fail("Shouldn't have detected typedef or enum on non-assignment declaration"); } - else if (node.flags & ts.NodeFlags.AwaitContext) { - file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here, ts.declarationNameToString(node))); + if (container) { + declareModuleMember(typeAlias, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); } - } - else if (node.originalKeywordKind === ts.SyntaxKind.YieldKeyword && node.flags & ts.NodeFlags.YieldContext) { - file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here, ts.declarationNameToString(node))); + container = oldContainer; } } - } - - function getStrictModeIdentifierMessage(node: ts.Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (ts.getContainingClass(node)) { - return ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode; + else if (ts.isJSDocEnumTag(typeAlias) || !typeAlias.fullName || typeAlias.fullName.kind === ts.SyntaxKind.Identifier) { + parent = typeAlias.parent; + bindBlockScopedDeclaration(typeAlias, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); } - - if (file.externalModuleIndicator) { - return ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode; + else { + bind(typeAlias.fullName); } - - return ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode; } + container = saveContainer; + lastContainer = saveLastContainer; + blockScopeContainer = saveBlockScopeContainer; + parent = saveParent; + currentFlow = saveCurrentFlow; + } - // 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: ts.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, ts.Diagnostics.constructor_is_a_reserved_word, ts.declarationNameToString(node))); + // 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, as well as `yield` or `await` in + // [Yield] or [Await] contexts, respectively. + function checkContextualIdentifier(node: ts.Identifier) { + // Report error only if there are no parse errors in file + if (!file.parseDiagnostics.length && + !(node.flags & ts.NodeFlags.Ambient) && + !(node.flags & ts.NodeFlags.JSDoc) && + !ts.isIdentifierName(node)) { + + // strict mode identifiers + if (inStrictMode && + node.originalKeywordKind! >= ts.SyntaxKind.FirstFutureReservedWord && + node.originalKeywordKind! <= ts.SyntaxKind.LastFutureReservedWord) { + file.bindDiagnostics.push(createDiagnosticForNode(node, getStrictModeIdentifierMessage(node), ts.declarationNameToString(node))); + } + else if (node.originalKeywordKind === ts.SyntaxKind.AwaitKeyword) { + if (ts.isExternalModule(file) && ts.isInTopLevelContext(node)) { + file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_at_the_top_level_of_a_module, ts.declarationNameToString(node))); + } + else if (node.flags & ts.NodeFlags.AwaitContext) { + file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here, ts.declarationNameToString(node))); } } + else if (node.originalKeywordKind === ts.SyntaxKind.YieldKeyword && node.flags & ts.NodeFlags.YieldContext) { + file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here, ts.declarationNameToString(node))); + } + } + } + + function getStrictModeIdentifierMessage(node: ts.Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (ts.getContainingClass(node)) { + return ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode; } - function checkStrictModeBinaryExpression(node: ts.BinaryExpression) { - if (inStrictMode && ts.isLeftHandSideExpression(node.left) && ts.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 as ts.Identifier); - } + if (file.externalModuleIndicator) { + return ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode; } - function checkStrictModeCatchClause(node: ts.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); + return ts.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: ts.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, ts.Diagnostics.constructor_is_a_reserved_word, ts.declarationNameToString(node))); } } + } - function checkStrictModeDeleteExpression(node: ts.DeleteExpression) { - // Grammar checking - if (inStrictMode && node.expression.kind === ts.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 = ts.getErrorSpanForNode(file, node.expression); - file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, ts.Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode)); - } + function checkStrictModeBinaryExpression(node: ts.BinaryExpression) { + if (inStrictMode && ts.isLeftHandSideExpression(node.left) && ts.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 as ts.Identifier); } + } - function isEvalOrArgumentsIdentifier(node: ts.Node): boolean { - return ts.isIdentifier(node) && (node.escapedText === "eval" || node.escapedText === "arguments"); + function checkStrictModeCatchClause(node: ts.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 checkStrictModeEvalOrArguments(contextNode: ts.Node, name: ts.Node | undefined) { - if (name && name.kind === ts.SyntaxKind.Identifier) { - const identifier = name as ts.Identifier; - 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 = ts.getErrorSpanForNode(file, name); - file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, getStrictModeEvalOrArgumentsMessage(contextNode), ts.idText(identifier))); - } - } + function checkStrictModeDeleteExpression(node: ts.DeleteExpression) { + // Grammar checking + if (inStrictMode && node.expression.kind === ts.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 = ts.getErrorSpanForNode(file, node.expression); + file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, ts.Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode)); } + } - function getStrictModeEvalOrArgumentsMessage(node: ts.Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (ts.getContainingClass(node)) { - return ts.Diagnostics.Code_contained_in_a_class_is_evaluated_in_JavaScript_s_strict_mode_which_does_not_allow_this_use_of_0_For_more_information_see_https_Colon_Slash_Slashdeveloper_mozilla_org_Slashen_US_Slashdocs_SlashWeb_SlashJavaScript_SlashReference_SlashStrict_mode; - } + function isEvalOrArgumentsIdentifier(node: ts.Node): boolean { + return ts.isIdentifier(node) && (node.escapedText === "eval" || node.escapedText === "arguments"); + } - if (file.externalModuleIndicator) { - return ts.Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode; + function checkStrictModeEvalOrArguments(contextNode: ts.Node, name: ts.Node | undefined) { + if (name && name.kind === ts.SyntaxKind.Identifier) { + const identifier = name as ts.Identifier; + 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 = ts.getErrorSpanForNode(file, name); + file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, getStrictModeEvalOrArgumentsMessage(contextNode), ts.idText(identifier))); } - - return ts.Diagnostics.Invalid_use_of_0_in_strict_mode; } + } - function checkStrictModeFunctionName(node: ts.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 getStrictModeEvalOrArgumentsMessage(node: ts.Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (ts.getContainingClass(node)) { + return ts.Diagnostics.Code_contained_in_a_class_is_evaluated_in_JavaScript_s_strict_mode_which_does_not_allow_this_use_of_0_For_more_information_see_https_Colon_Slash_Slashdeveloper_mozilla_org_Slashen_US_Slashdocs_SlashWeb_SlashJavaScript_SlashReference_SlashStrict_mode; } - function getStrictModeBlockScopeFunctionDeclarationMessage(node: ts.Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (ts.getContainingClass(node)) { - return ts.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 ts.Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode; + } - if (file.externalModuleIndicator) { - return ts.Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode; - } + return ts.Diagnostics.Invalid_use_of_0_in_strict_mode; + } - return ts.Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5; + function checkStrictModeFunctionName(node: ts.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 checkStrictModeFunctionDeclaration(node: ts.FunctionDeclaration) { - if (languageVersion < ts.ScriptTarget.ES2015) { - // Report error if function is not top level function declaration - if (blockScopeContainer.kind !== ts.SyntaxKind.SourceFile && - blockScopeContainer.kind !== ts.SyntaxKind.ModuleDeclaration && - !ts.isFunctionLikeOrClassStaticBlockDeclaration(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 = ts.getErrorSpanForNode(file, node); - file.bindDiagnostics.push(ts.createFileDiagnostic(file, errorSpan.start, errorSpan.length, getStrictModeBlockScopeFunctionDeclarationMessage(node))); - } - } + function getStrictModeBlockScopeFunctionDeclarationMessage(node: ts.Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (ts.getContainingClass(node)) { + return ts.Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Class_definitions_are_automatically_in_strict_mode; } - function checkStrictModeNumericLiteral(node: ts.NumericLiteral) { - if (languageVersion < ts.ScriptTarget.ES5 && inStrictMode && node.numericLiteralFlags & ts.TokenFlags.Octal) { - file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Octal_literals_are_not_allowed_in_strict_mode)); - } + if (file.externalModuleIndicator) { + return ts.Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode; } - function checkStrictModePostfixUnaryExpression(node: ts.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 as ts.Identifier); + return ts.Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5; + } + + function checkStrictModeFunctionDeclaration(node: ts.FunctionDeclaration) { + if (languageVersion < ts.ScriptTarget.ES2015) { + // Report error if function is not top level function declaration + if (blockScopeContainer.kind !== ts.SyntaxKind.SourceFile && + blockScopeContainer.kind !== ts.SyntaxKind.ModuleDeclaration && + !ts.isFunctionLikeOrClassStaticBlockDeclaration(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 = ts.getErrorSpanForNode(file, node); + file.bindDiagnostics.push(ts.createFileDiagnostic(file, errorSpan.start, errorSpan.length, getStrictModeBlockScopeFunctionDeclarationMessage(node))); } } + } - function checkStrictModePrefixUnaryExpression(node: ts.PrefixUnaryExpression) { - // Grammar checking - if (inStrictMode) { - if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { - checkStrictModeEvalOrArguments(node, node.operand as ts.Identifier); - } - } + function checkStrictModeNumericLiteral(node: ts.NumericLiteral) { + if (languageVersion < ts.ScriptTarget.ES5 && inStrictMode && node.numericLiteralFlags & ts.TokenFlags.Octal) { + file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Octal_literals_are_not_allowed_in_strict_mode)); } + } - function checkStrictModeWithStatement(node: ts.WithStatement) { - // Grammar checking for withStatement - if (inStrictMode) { - errorOnFirstToken(node, ts.Diagnostics.with_statements_are_not_allowed_in_strict_mode); - } + function checkStrictModePostfixUnaryExpression(node: ts.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 as ts.Identifier); } + } - function checkStrictModeLabeledStatement(node: ts.LabeledStatement) { - // Grammar checking for labeledStatement - if (inStrictMode && ts.getEmitScriptTarget(options) >= ts.ScriptTarget.ES2015) { - if (ts.isDeclarationStatement(node.statement) || ts.isVariableStatement(node.statement)) { - errorOnFirstToken(node.label, ts.Diagnostics.A_label_is_not_allowed_here); - } + function checkStrictModePrefixUnaryExpression(node: ts.PrefixUnaryExpression) { + // Grammar checking + if (inStrictMode) { + if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { + checkStrictModeEvalOrArguments(node, node.operand as ts.Identifier); } } + } - function errorOnFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any) { - const span = ts.getSpanOfTokenAtPosition(file, node.pos); - file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, message, arg0, arg1, arg2)); + function checkStrictModeWithStatement(node: ts.WithStatement) { + // Grammar checking for withStatement + if (inStrictMode) { + errorOnFirstToken(node, ts.Diagnostics.with_statements_are_not_allowed_in_strict_mode); } + } - function errorOrSuggestionOnNode(isError: boolean, node: ts.Node, message: ts.DiagnosticMessage): void { - errorOrSuggestionOnRange(isError, node, node, message); + function checkStrictModeLabeledStatement(node: ts.LabeledStatement) { + // Grammar checking for labeledStatement + if (inStrictMode && ts.getEmitScriptTarget(options) >= ts.ScriptTarget.ES2015) { + if (ts.isDeclarationStatement(node.statement) || ts.isVariableStatement(node.statement)) { + errorOnFirstToken(node.label, ts.Diagnostics.A_label_is_not_allowed_here); + } } + } - function errorOrSuggestionOnRange(isError: boolean, startNode: ts.Node, endNode: ts.Node, message: ts.DiagnosticMessage): void { - addErrorOrSuggestionDiagnostic(isError, { pos: ts.getTokenPosOfNode(startNode, file), end: endNode.end }, message); - } + function errorOnFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any) { + const span = ts.getSpanOfTokenAtPosition(file, node.pos); + file.bindDiagnostics.push(ts.createFileDiagnostic(file, span.start, span.length, message, arg0, arg1, arg2)); + } - function addErrorOrSuggestionDiagnostic(isError: boolean, range: ts.TextRange, message: ts.DiagnosticMessage): void { - const diag = ts.createFileDiagnostic(file, range.pos, range.end - range.pos, message); - if (isError) { - file.bindDiagnostics.push(diag); - } - else { - file.bindSuggestionDiagnostics = ts.append(file.bindSuggestionDiagnostics, { ...diag, category: ts.DiagnosticCategory.Suggestion }); - } + function errorOrSuggestionOnNode(isError: boolean, node: ts.Node, message: ts.DiagnosticMessage): void { + errorOrSuggestionOnRange(isError, node, node, message); + } + + function errorOrSuggestionOnRange(isError: boolean, startNode: ts.Node, endNode: ts.Node, message: ts.DiagnosticMessage): void { + addErrorOrSuggestionDiagnostic(isError, { pos: ts.getTokenPosOfNode(startNode, file), end: endNode.end }, message); + } + + function addErrorOrSuggestionDiagnostic(isError: boolean, range: ts.TextRange, message: ts.DiagnosticMessage): void { + const diag = ts.createFileDiagnostic(file, range.pos, range.end - range.pos, message); + if (isError) { + file.bindDiagnostics.push(diag); + } + else { + file.bindSuggestionDiagnostics = ts.append(file.bindSuggestionDiagnostics, { ...diag, category: ts.DiagnosticCategory.Suggestion }); } + } - function bind(node: ts.Node | undefined): void { - if (!node) { - return; - } - ts.setParent(node, parent); - if (ts.tracing) - (node as ts.TracingNode).tracingPath = file.path; - const saveInStrictMode = inStrictMode; + function bind(node: ts.Node | undefined): void { + if (!node) { + return; + } + ts.setParent(node, parent); + if (ts.tracing) + (node as ts.TracingNode).tracingPath = file.path; + 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. + // 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 > ts.SyntaxKind.LastToken) { - const saveParent = parent; - parent = node; - const containerFlags = getContainerFlags(node); - if (containerFlags === ContainerFlags.None) { - bindChildren(node); - } - else { - bindContainer(node, containerFlags); - } - parent = saveParent; + // 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 > ts.SyntaxKind.LastToken) { + const saveParent = parent; + parent = node; + const containerFlags = getContainerFlags(node); + if (containerFlags === ContainerFlags.None) { + bindChildren(node); } else { - const saveParent = parent; - if (node.kind === ts.SyntaxKind.EndOfFileToken) - parent = node; - bindJSDoc(node); - parent = saveParent; + bindContainer(node, containerFlags); } - inStrictMode = saveInStrictMode; + parent = saveParent; + } + else { + const saveParent = parent; + if (node.kind === ts.SyntaxKind.EndOfFileToken) + parent = node; + bindJSDoc(node); + parent = saveParent; } + inStrictMode = saveInStrictMode; + } - function bindJSDoc(node: ts.Node) { - if (ts.hasJSDocNodes(node)) { - if (ts.isInJSFile(node)) { - for (const j of node.jsDoc!) { - bind(j); - } + function bindJSDoc(node: ts.Node) { + if (ts.hasJSDocNodes(node)) { + if (ts.isInJSFile(node)) { + for (const j of node.jsDoc!) { + bind(j); } - else { - for (const j of node.jsDoc!) { - ts.setParent(j, node); - ts.setParentRecursive(j, /*incremental*/ false); - } + } + else { + for (const j of node.jsDoc!) { + ts.setParent(j, node); + ts.setParentRecursive(j, /*incremental*/ false); } } } + } - function updateStrictModeStatementList(statements: ts.NodeArray) { - if (!inStrictMode) { - for (const statement of statements) { - if (!ts.isPrologueDirective(statement)) { - return; - } + function updateStrictModeStatementList(statements: ts.NodeArray) { + if (!inStrictMode) { + for (const statement of statements) { + if (!ts.isPrologueDirective(statement)) { + return; + } - if (isUseStrictPrologueDirective(statement as ts.ExpressionStatement)) { - inStrictMode = true; - return; - } + if (isUseStrictPrologueDirective(statement as ts.ExpressionStatement)) { + inStrictMode = true; + return; } } } + } - /// Should be called only on prologue directives (isPrologueDirective(node) should be true) - function isUseStrictPrologueDirective(node: ts.ExpressionStatement): boolean { - const nodeText = ts.getSourceTextOfNodeFromSourceFile(file, node.expression); + /// Should be called only on prologue directives (isPrologueDirective(node) should be true) + function isUseStrictPrologueDirective(node: ts.ExpressionStatement): boolean { + const nodeText = ts.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'"; - } + // 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: ts.Node) { - switch (node.kind) { - /* Strict mode checks */ - case ts.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 as ts.Identifier).isInJSDocNamespace) { - let parentNode = node.parent; - while (parentNode && !ts.isJSDocTypeAlias(parentNode)) { - parentNode = parentNode.parent; - } - bindBlockScopedDeclaration(parentNode as ts.Declaration, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); - break; - } - // falls through - case ts.SyntaxKind.ThisKeyword: - if (currentFlow && (ts.isExpression(node) || parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment)) { - node.flowNode = currentFlow; - } - return checkContextualIdentifier(node as ts.Identifier); - case ts.SyntaxKind.QualifiedName: - if (currentFlow && ts.isPartOfTypeQuery(node)) { - node.flowNode = currentFlow; + function bindWorker(node: ts.Node) { + switch (node.kind) { + /* Strict mode checks */ + case ts.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 as ts.Identifier).isInJSDocNamespace) { + let parentNode = node.parent; + while (parentNode && !ts.isJSDocTypeAlias(parentNode)) { + parentNode = parentNode.parent; } + bindBlockScopedDeclaration(parentNode as ts.Declaration, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); break; - case ts.SyntaxKind.MetaProperty: - case ts.SyntaxKind.SuperKeyword: + } + // falls through + case ts.SyntaxKind.ThisKeyword: + if (currentFlow && (ts.isExpression(node) || parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment)) { node.flowNode = currentFlow; - break; - case ts.SyntaxKind.PrivateIdentifier: - return checkPrivateIdentifier(node as ts.PrivateIdentifier); - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - const expr = node as ts.PropertyAccessExpression | ts.ElementAccessExpression; - if (currentFlow && isNarrowableReference(expr)) { - expr.flowNode = currentFlow; - } - if (ts.isSpecialPropertyDeclaration(expr)) { - bindSpecialPropertyDeclaration(expr); - } - if (ts.isInJSFile(expr) && - file.commonJsModuleIndicator && - ts.isModuleExportsAccessExpression(expr) && - !lookupSymbolForName(blockScopeContainer, "module" as ts.__String)) { - declareSymbol(file.locals!, /*parent*/ undefined, expr.expression, ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.ModuleExports, ts.SymbolFlags.FunctionScopedVariableExcludes); - } - break; - case ts.SyntaxKind.BinaryExpression: - const specialKind = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); - switch (specialKind) { - case ts.AssignmentDeclarationKind.ExportsProperty: - bindExportsPropertyAssignment(node as ts.BindableStaticPropertyAssignmentExpression); - break; - case ts.AssignmentDeclarationKind.ModuleExports: - bindModuleExportsAssignment(node as ts.BindablePropertyAssignmentExpression); - break; - case ts.AssignmentDeclarationKind.PrototypeProperty: - bindPrototypePropertyAssignment((node as ts.BindableStaticPropertyAssignmentExpression).left, node); - break; - case ts.AssignmentDeclarationKind.Prototype: - bindPrototypeAssignment(node as ts.BindableStaticPropertyAssignmentExpression); - break; - case ts.AssignmentDeclarationKind.ThisProperty: - bindThisPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); - break; - case ts.AssignmentDeclarationKind.Property: - const expression = ((node as ts.BinaryExpression).left as ts.AccessExpression).expression; - if (ts.isInJSFile(node) && ts.isIdentifier(expression)) { - const symbol = lookupSymbolForName(blockScopeContainer, expression.escapedText); - if (ts.isThisInitializedDeclaration(symbol?.valueDeclaration)) { - bindThisPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); - break; - } + } + return checkContextualIdentifier(node as ts.Identifier); + case ts.SyntaxKind.QualifiedName: + if (currentFlow && ts.isPartOfTypeQuery(node)) { + node.flowNode = currentFlow; + } + break; + case ts.SyntaxKind.MetaProperty: + case ts.SyntaxKind.SuperKeyword: + node.flowNode = currentFlow; + break; + case ts.SyntaxKind.PrivateIdentifier: + return checkPrivateIdentifier(node as ts.PrivateIdentifier); + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const expr = node as ts.PropertyAccessExpression | ts.ElementAccessExpression; + if (currentFlow && isNarrowableReference(expr)) { + expr.flowNode = currentFlow; + } + if (ts.isSpecialPropertyDeclaration(expr)) { + bindSpecialPropertyDeclaration(expr); + } + if (ts.isInJSFile(expr) && + file.commonJsModuleIndicator && + ts.isModuleExportsAccessExpression(expr) && + !lookupSymbolForName(blockScopeContainer, "module" as ts.__String)) { + declareSymbol(file.locals!, /*parent*/ undefined, expr.expression, ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.ModuleExports, ts.SymbolFlags.FunctionScopedVariableExcludes); + } + break; + case ts.SyntaxKind.BinaryExpression: + const specialKind = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); + switch (specialKind) { + case ts.AssignmentDeclarationKind.ExportsProperty: + bindExportsPropertyAssignment(node as ts.BindableStaticPropertyAssignmentExpression); + break; + case ts.AssignmentDeclarationKind.ModuleExports: + bindModuleExportsAssignment(node as ts.BindablePropertyAssignmentExpression); + break; + case ts.AssignmentDeclarationKind.PrototypeProperty: + bindPrototypePropertyAssignment((node as ts.BindableStaticPropertyAssignmentExpression).left, node); + break; + case ts.AssignmentDeclarationKind.Prototype: + bindPrototypeAssignment(node as ts.BindableStaticPropertyAssignmentExpression); + break; + case ts.AssignmentDeclarationKind.ThisProperty: + bindThisPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); + break; + case ts.AssignmentDeclarationKind.Property: + const expression = ((node as ts.BinaryExpression).left as ts.AccessExpression).expression; + if (ts.isInJSFile(node) && ts.isIdentifier(expression)) { + const symbol = lookupSymbolForName(blockScopeContainer, expression.escapedText); + if (ts.isThisInitializedDeclaration(symbol?.valueDeclaration)) { + bindThisPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); + break; } - bindSpecialPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); - break; - case ts.AssignmentDeclarationKind.None: - // Nothing to do - break; - default: - ts.Debug.fail("Unknown binary expression special property assignment kind"); - } - return checkStrictModeBinaryExpression(node as ts.BinaryExpression); - case ts.SyntaxKind.CatchClause: - return checkStrictModeCatchClause(node as ts.CatchClause); - case ts.SyntaxKind.DeleteExpression: - return checkStrictModeDeleteExpression(node as ts.DeleteExpression); - case ts.SyntaxKind.NumericLiteral: - return checkStrictModeNumericLiteral(node as ts.NumericLiteral); - case ts.SyntaxKind.PostfixUnaryExpression: - return checkStrictModePostfixUnaryExpression(node as ts.PostfixUnaryExpression); - case ts.SyntaxKind.PrefixUnaryExpression: - return checkStrictModePrefixUnaryExpression(node as ts.PrefixUnaryExpression); - case ts.SyntaxKind.WithStatement: - return checkStrictModeWithStatement(node as ts.WithStatement); - case ts.SyntaxKind.LabeledStatement: - return checkStrictModeLabeledStatement(node as ts.LabeledStatement); - case ts.SyntaxKind.ThisType: - seenThisKeyword = true; + } + bindSpecialPropertyAssignment(node as ts.BindablePropertyAssignmentExpression); + break; + case ts.AssignmentDeclarationKind.None: + // Nothing to do + break; + default: + ts.Debug.fail("Unknown binary expression special property assignment kind"); + } + return checkStrictModeBinaryExpression(node as ts.BinaryExpression); + case ts.SyntaxKind.CatchClause: + return checkStrictModeCatchClause(node as ts.CatchClause); + case ts.SyntaxKind.DeleteExpression: + return checkStrictModeDeleteExpression(node as ts.DeleteExpression); + case ts.SyntaxKind.NumericLiteral: + return checkStrictModeNumericLiteral(node as ts.NumericLiteral); + case ts.SyntaxKind.PostfixUnaryExpression: + return checkStrictModePostfixUnaryExpression(node as ts.PostfixUnaryExpression); + case ts.SyntaxKind.PrefixUnaryExpression: + return checkStrictModePrefixUnaryExpression(node as ts.PrefixUnaryExpression); + case ts.SyntaxKind.WithStatement: + return checkStrictModeWithStatement(node as ts.WithStatement); + case ts.SyntaxKind.LabeledStatement: + return checkStrictModeLabeledStatement(node as ts.LabeledStatement); + case ts.SyntaxKind.ThisType: + seenThisKeyword = true; + return; + case ts.SyntaxKind.TypePredicate: + break; // Binding the children will handle everything + case ts.SyntaxKind.TypeParameter: + return bindTypeParameter(node as ts.TypeParameterDeclaration); + case ts.SyntaxKind.Parameter: + return bindParameter(node as ts.ParameterDeclaration); + case ts.SyntaxKind.VariableDeclaration: + return bindVariableDeclarationOrBindingElement(node as ts.VariableDeclaration); + case ts.SyntaxKind.BindingElement: + node.flowNode = currentFlow; + return bindVariableDeclarationOrBindingElement(node as ts.BindingElement); + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + return bindPropertyWorker(node as ts.PropertyDeclaration | ts.PropertySignature); + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.Property, ts.SymbolFlags.PropertyExcludes); + case ts.SyntaxKind.EnumMember: + return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.EnumMember, ts.SymbolFlags.EnumMemberExcludes); + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.IndexSignature: + return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Signature, ts.SymbolFlags.None); + case ts.SyntaxKind.MethodDeclaration: + case ts.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 as ts.Declaration, ts.SymbolFlags.Method | ((node as ts.MethodDeclaration).questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.isObjectLiteralMethod(node) ? ts.SymbolFlags.PropertyExcludes : ts.SymbolFlags.MethodExcludes); + case ts.SyntaxKind.FunctionDeclaration: + return bindFunctionDeclaration(node as ts.FunctionDeclaration); + case ts.SyntaxKind.Constructor: + return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Constructor, /*symbolExcludes:*/ ts.SymbolFlags.None); + case ts.SyntaxKind.GetAccessor: + return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.GetAccessor, ts.SymbolFlags.GetAccessorExcludes); + case ts.SyntaxKind.SetAccessor: + return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.SetAccessor, ts.SymbolFlags.SetAccessorExcludes); + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.JSDocFunctionType: + case ts.SyntaxKind.JSDocSignature: + case ts.SyntaxKind.ConstructorType: + return bindFunctionOrConstructorType(node as ts.SignatureDeclaration | ts.JSDocSignature); + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.JSDocTypeLiteral: + case ts.SyntaxKind.MappedType: + return bindAnonymousTypeWorker(node as ts.TypeLiteralNode | ts.MappedTypeNode | ts.JSDocTypeLiteral); + case ts.SyntaxKind.JSDocClassTag: + return bindJSDocClassTag(node as ts.JSDocClassTag); + case ts.SyntaxKind.ObjectLiteralExpression: + return bindObjectLiteralExpression(node as ts.ObjectLiteralExpression); + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return bindFunctionExpression(node as ts.FunctionExpression); + case ts.SyntaxKind.CallExpression: + const assignmentKind = ts.getAssignmentDeclarationKind(node as ts.CallExpression); + switch (assignmentKind) { + case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: + return bindObjectDefinePropertyAssignment(node as ts.BindableObjectDefinePropertyCall); + case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: + return bindObjectDefinePropertyExport(node as ts.BindableObjectDefinePropertyCall); + case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: + return bindObjectDefinePrototypeProperty(node as ts.BindableObjectDefinePropertyCall); + case ts.AssignmentDeclarationKind.None: + break; // Nothing to do + default: + return ts.Debug.fail("Unknown call expression assignment declaration kind"); + } + if (ts.isInJSFile(node)) { + bindCallExpression(node as ts.CallExpression); + } + break; + + // Members of classes, interfaces, and modules + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + // All classes are automatically in strict mode in ES6. + inStrictMode = true; + return bindClassLikeDeclaration(node as ts.ClassLikeDeclaration); + case ts.SyntaxKind.InterfaceDeclaration: + return bindBlockScopedDeclaration(node as ts.Declaration, ts.SymbolFlags.Interface, ts.SymbolFlags.InterfaceExcludes); + case ts.SyntaxKind.TypeAliasDeclaration: + return bindBlockScopedDeclaration(node as ts.Declaration, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); + case ts.SyntaxKind.EnumDeclaration: + return bindEnumDeclaration(node as ts.EnumDeclaration); + case ts.SyntaxKind.ModuleDeclaration: + return bindModuleDeclaration(node as ts.ModuleDeclaration); + // Jsx-attributes + case ts.SyntaxKind.JsxAttributes: + return bindJsxAttributes(node as ts.JsxAttributes); + case ts.SyntaxKind.JsxAttribute: + return bindJsxAttribute(node as ts.JsxAttribute, ts.SymbolFlags.Property, ts.SymbolFlags.PropertyExcludes); + + // Imports and exports + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); + case ts.SyntaxKind.NamespaceExportDeclaration: + return bindNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration); + case ts.SyntaxKind.ImportClause: + return bindImportClause(node as ts.ImportClause); + case ts.SyntaxKind.ExportDeclaration: + return bindExportDeclaration(node as ts.ExportDeclaration); + case ts.SyntaxKind.ExportAssignment: + return bindExportAssignment(node as ts.ExportAssignment); + case ts.SyntaxKind.SourceFile: + updateStrictModeStatementList((node as ts.SourceFile).statements); + return bindSourceFileIfExternalModule(); + case ts.SyntaxKind.Block: + if (!ts.isFunctionLikeOrClassStaticBlockDeclaration(node.parent)) { return; - case ts.SyntaxKind.TypePredicate: - break; // Binding the children will handle everything - case ts.SyntaxKind.TypeParameter: - return bindTypeParameter(node as ts.TypeParameterDeclaration); - case ts.SyntaxKind.Parameter: - return bindParameter(node as ts.ParameterDeclaration); - case ts.SyntaxKind.VariableDeclaration: - return bindVariableDeclarationOrBindingElement(node as ts.VariableDeclaration); - case ts.SyntaxKind.BindingElement: - node.flowNode = currentFlow; - return bindVariableDeclarationOrBindingElement(node as ts.BindingElement); - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - return bindPropertyWorker(node as ts.PropertyDeclaration | ts.PropertySignature); - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.Property, ts.SymbolFlags.PropertyExcludes); - case ts.SyntaxKind.EnumMember: - return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.EnumMember, ts.SymbolFlags.EnumMemberExcludes); - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.IndexSignature: - return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Signature, ts.SymbolFlags.None); - case ts.SyntaxKind.MethodDeclaration: - case ts.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 as ts.Declaration, ts.SymbolFlags.Method | ((node as ts.MethodDeclaration).questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.isObjectLiteralMethod(node) ? ts.SymbolFlags.PropertyExcludes : ts.SymbolFlags.MethodExcludes); - case ts.SyntaxKind.FunctionDeclaration: - return bindFunctionDeclaration(node as ts.FunctionDeclaration); - case ts.SyntaxKind.Constructor: - return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Constructor, /*symbolExcludes:*/ ts.SymbolFlags.None); - case ts.SyntaxKind.GetAccessor: - return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.GetAccessor, ts.SymbolFlags.GetAccessorExcludes); - case ts.SyntaxKind.SetAccessor: - return bindPropertyOrMethodOrAccessor(node as ts.Declaration, ts.SymbolFlags.SetAccessor, ts.SymbolFlags.SetAccessorExcludes); - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.JSDocFunctionType: - case ts.SyntaxKind.JSDocSignature: - case ts.SyntaxKind.ConstructorType: - return bindFunctionOrConstructorType(node as ts.SignatureDeclaration | ts.JSDocSignature); - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.JSDocTypeLiteral: - case ts.SyntaxKind.MappedType: - return bindAnonymousTypeWorker(node as ts.TypeLiteralNode | ts.MappedTypeNode | ts.JSDocTypeLiteral); - case ts.SyntaxKind.JSDocClassTag: - return bindJSDocClassTag(node as ts.JSDocClassTag); - case ts.SyntaxKind.ObjectLiteralExpression: - return bindObjectLiteralExpression(node as ts.ObjectLiteralExpression); - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return bindFunctionExpression(node as ts.FunctionExpression); - case ts.SyntaxKind.CallExpression: - const assignmentKind = ts.getAssignmentDeclarationKind(node as ts.CallExpression); - switch (assignmentKind) { - case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: - return bindObjectDefinePropertyAssignment(node as ts.BindableObjectDefinePropertyCall); - case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: - return bindObjectDefinePropertyExport(node as ts.BindableObjectDefinePropertyCall); - case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: - return bindObjectDefinePrototypeProperty(node as ts.BindableObjectDefinePropertyCall); - case ts.AssignmentDeclarationKind.None: - break; // Nothing to do - default: - return ts.Debug.fail("Unknown call expression assignment declaration kind"); - } - if (ts.isInJSFile(node)) { - bindCallExpression(node as ts.CallExpression); - } + } + // falls through + case ts.SyntaxKind.ModuleBlock: + return updateStrictModeStatementList((node as ts.Block | ts.ModuleBlock).statements); + case ts.SyntaxKind.JSDocParameterTag: + if (node.parent.kind === ts.SyntaxKind.JSDocSignature) { + return bindParameter(node as ts.JSDocParameterTag); + } + if (node.parent.kind !== ts.SyntaxKind.JSDocTypeLiteral) { break; - - // Members of classes, interfaces, and modules - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ClassDeclaration: - // All classes are automatically in strict mode in ES6. - inStrictMode = true; - return bindClassLikeDeclaration(node as ts.ClassLikeDeclaration); - case ts.SyntaxKind.InterfaceDeclaration: - return bindBlockScopedDeclaration(node as ts.Declaration, ts.SymbolFlags.Interface, ts.SymbolFlags.InterfaceExcludes); - case ts.SyntaxKind.TypeAliasDeclaration: - return bindBlockScopedDeclaration(node as ts.Declaration, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.TypeAliasExcludes); - case ts.SyntaxKind.EnumDeclaration: - return bindEnumDeclaration(node as ts.EnumDeclaration); - case ts.SyntaxKind.ModuleDeclaration: - return bindModuleDeclaration(node as ts.ModuleDeclaration); - // Jsx-attributes - case ts.SyntaxKind.JsxAttributes: - return bindJsxAttributes(node as ts.JsxAttributes); - case ts.SyntaxKind.JsxAttribute: - return bindJsxAttribute(node as ts.JsxAttribute, ts.SymbolFlags.Property, ts.SymbolFlags.PropertyExcludes); - - // Imports and exports - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - return declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); - case ts.SyntaxKind.NamespaceExportDeclaration: - return bindNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration); - case ts.SyntaxKind.ImportClause: - return bindImportClause(node as ts.ImportClause); - case ts.SyntaxKind.ExportDeclaration: - return bindExportDeclaration(node as ts.ExportDeclaration); - case ts.SyntaxKind.ExportAssignment: - return bindExportAssignment(node as ts.ExportAssignment); - case ts.SyntaxKind.SourceFile: - updateStrictModeStatementList((node as ts.SourceFile).statements); - return bindSourceFileIfExternalModule(); - case ts.SyntaxKind.Block: - if (!ts.isFunctionLikeOrClassStaticBlockDeclaration(node.parent)) { - return; - } - // falls through - case ts.SyntaxKind.ModuleBlock: - return updateStrictModeStatementList((node as ts.Block | ts.ModuleBlock).statements); - case ts.SyntaxKind.JSDocParameterTag: - if (node.parent.kind === ts.SyntaxKind.JSDocSignature) { - return bindParameter(node as ts.JSDocParameterTag); - } - if (node.parent.kind !== ts.SyntaxKind.JSDocTypeLiteral) { - break; - } - // falls through - case ts.SyntaxKind.JSDocPropertyTag: - const propTag = node as ts.JSDocPropertyLikeTag; - const flags = propTag.isBracketed || propTag.typeExpression && propTag.typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType ? - ts.SymbolFlags.Property | ts.SymbolFlags.Optional : - ts.SymbolFlags.Property; - return declareSymbolAndAddToSymbolTable(propTag, flags, ts.SymbolFlags.PropertyExcludes); - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - return (delayedTypeAliases || (delayedTypeAliases = [])).push(node as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag); - } + } + // falls through + case ts.SyntaxKind.JSDocPropertyTag: + const propTag = node as ts.JSDocPropertyLikeTag; + const flags = propTag.isBracketed || propTag.typeExpression && propTag.typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType ? + ts.SymbolFlags.Property | ts.SymbolFlags.Optional : + ts.SymbolFlags.Property; + return declareSymbolAndAddToSymbolTable(propTag, flags, ts.SymbolFlags.PropertyExcludes); + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + return (delayedTypeAliases || (delayedTypeAliases = [])).push(node as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag); } - function bindPropertyWorker(node: ts.PropertyDeclaration | ts.PropertySignature) { - return bindPropertyOrMethodOrAccessor(node, ts.SymbolFlags.Property | (node.questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.SymbolFlags.PropertyExcludes); + } + function bindPropertyWorker(node: ts.PropertyDeclaration | ts.PropertySignature) { + return bindPropertyOrMethodOrAccessor(node, ts.SymbolFlags.Property | (node.questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.SymbolFlags.PropertyExcludes); + } + function bindAnonymousTypeWorker(node: ts.TypeLiteralNode | ts.MappedTypeNode | ts.JSDocTypeLiteral) { + return bindAnonymousDeclaration(node as ts.Declaration, ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); + } + + function bindSourceFileIfExternalModule() { + setExportContextFlag(file); + if (ts.isExternalModule(file)) { + bindSourceFileAsExternalModule(); } - function bindAnonymousTypeWorker(node: ts.TypeLiteralNode | ts.MappedTypeNode | ts.JSDocTypeLiteral) { - return bindAnonymousDeclaration(node as ts.Declaration, ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); + else if (ts.isJsonSourceFile(file)) { + bindSourceFileAsExternalModule(); + // Create symbol equivalent for the module.exports = {} + const originalSymbol = file.symbol; + declareSymbol(file.symbol.exports!, file.symbol, file, ts.SymbolFlags.Property, ts.SymbolFlags.All); + file.symbol = originalSymbol; } + } - function bindSourceFileIfExternalModule() { - setExportContextFlag(file); - if (ts.isExternalModule(file)) { - bindSourceFileAsExternalModule(); - } - else if (ts.isJsonSourceFile(file)) { - bindSourceFileAsExternalModule(); - // Create symbol equivalent for the module.exports = {} - const originalSymbol = file.symbol; - declareSymbol(file.symbol.exports!, file.symbol, file, ts.SymbolFlags.Property, ts.SymbolFlags.All); - file.symbol = originalSymbol; - } - } + function bindSourceFileAsExternalModule() { + bindAnonymousDeclaration(file, ts.SymbolFlags.ValueModule, `"${ts.removeFileExtension(file.fileName)}"` as ts.__String); + } - function bindSourceFileAsExternalModule() { - bindAnonymousDeclaration(file, ts.SymbolFlags.ValueModule, `"${ts.removeFileExtension(file.fileName)}"` as ts.__String); + function bindExportAssignment(node: ts.ExportAssignment) { + if (!container.symbol || !container.symbol.exports) { + // Incorrect export assignment in some sort of block construct + bindAnonymousDeclaration(node, ts.SymbolFlags.Value, getDeclarationName(node)!); } - - function bindExportAssignment(node: ts.ExportAssignment) { - if (!container.symbol || !container.symbol.exports) { - // Incorrect export assignment in some sort of block construct - bindAnonymousDeclaration(node, ts.SymbolFlags.Value, getDeclarationName(node)!); + else { + const flags = ts.exportAssignmentIsAlias(node) + // An export default clause with an EntityNameExpression or a class expression exports all meanings of that identifier or expression; + ? ts.SymbolFlags.Alias + // An export default clause with any other expression exports a value + : ts.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, ts.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. + ts.setValueDeclaration(symbol, node); } - else { - const flags = ts.exportAssignmentIsAlias(node) - // An export default clause with an EntityNameExpression or a class expression exports all meanings of that identifier or expression; - ? ts.SymbolFlags.Alias - // An export default clause with any other expression exports a value - : ts.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, ts.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. - ts.setValueDeclaration(symbol, node); - } - } + function bindNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration) { + if (node.modifiers && node.modifiers.length) { + file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Modifiers_cannot_appear_here)); + } + const diag = !ts.isSourceFile(node.parent) ? ts.Diagnostics.Global_module_exports_may_only_appear_at_top_level + : !ts.isExternalModule(node.parent) ? ts.Diagnostics.Global_module_exports_may_only_appear_in_module_files + : !node.parent.isDeclarationFile ? ts.Diagnostics.Global_module_exports_may_only_appear_in_declaration_files + : undefined; + if (diag) { + file.bindDiagnostics.push(createDiagnosticForNode(node, diag)); + } + else { + file.symbol.globalExports = file.symbol.globalExports || ts.createSymbolTable(); + declareSymbol(file.symbol.globalExports, file.symbol, node, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); } + } - function bindNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration) { - if (node.modifiers && node.modifiers.length) { - file.bindDiagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Modifiers_cannot_appear_here)); - } - const diag = !ts.isSourceFile(node.parent) ? ts.Diagnostics.Global_module_exports_may_only_appear_at_top_level - : !ts.isExternalModule(node.parent) ? ts.Diagnostics.Global_module_exports_may_only_appear_in_module_files - : !node.parent.isDeclarationFile ? ts.Diagnostics.Global_module_exports_may_only_appear_in_declaration_files - : undefined; - if (diag) { - file.bindDiagnostics.push(createDiagnosticForNode(node, diag)); - } - else { - file.symbol.globalExports = file.symbol.globalExports || ts.createSymbolTable(); - declareSymbol(file.symbol.globalExports, file.symbol, node, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); - } + function bindExportDeclaration(node: ts.ExportDeclaration) { + if (!container.symbol || !container.symbol.exports) { + // Export * in some sort of block construct + bindAnonymousDeclaration(node, ts.SymbolFlags.ExportStar, getDeclarationName(node)!); + } + else if (!node.exportClause) { + // All export * declarations are collected in an __export symbol + declareSymbol(container.symbol.exports, container.symbol, node, ts.SymbolFlags.ExportStar, ts.SymbolFlags.None); } + else if (ts.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. + ts.setParent(node.exportClause, node); + declareSymbol(container.symbol.exports, container.symbol, node.exportClause, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); + } + } - function bindExportDeclaration(node: ts.ExportDeclaration) { - if (!container.symbol || !container.symbol.exports) { - // Export * in some sort of block construct - bindAnonymousDeclaration(node, ts.SymbolFlags.ExportStar, getDeclarationName(node)!); - } - else if (!node.exportClause) { - // All export * declarations are collected in an __export symbol - declareSymbol(container.symbol.exports, container.symbol, node, ts.SymbolFlags.ExportStar, ts.SymbolFlags.None); - } - else if (ts.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. - ts.setParent(node.exportClause, node); - declareSymbol(container.symbol.exports, container.symbol, node.exportClause, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); - } + function bindImportClause(node: ts.ImportClause) { + if (node.name) { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); } + } - function bindImportClause(node: ts.ImportClause) { - if (node.name) { - declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); - } + function setCommonJsModuleIndicator(node: ts.Node) { + if (file.externalModuleIndicator) { + return false; + } + if (!file.commonJsModuleIndicator) { + file.commonJsModuleIndicator = node; + bindSourceFileAsExternalModule(); } + return true; + } - function setCommonJsModuleIndicator(node: ts.Node) { - if (file.externalModuleIndicator) { - return false; - } - if (!file.commonJsModuleIndicator) { - file.commonJsModuleIndicator = node; - bindSourceFileAsExternalModule(); - } - return true; + function bindObjectDefinePropertyExport(node: ts.BindableObjectDefinePropertyCall) { + if (!setCommonJsModuleIndicator(node)) { + return; } - - function bindObjectDefinePropertyExport(node: ts.BindableObjectDefinePropertyCall) { - if (!setCommonJsModuleIndicator(node)) { - return; - } - const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => { - if (symbol) { - addDeclarationToSymbol(symbol, id, ts.SymbolFlags.Module | ts.SymbolFlags.Assignment); - } - return symbol; - }); + const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => { if (symbol) { - const flags = ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue; - declareSymbol(symbol.exports!, symbol, node, flags, ts.SymbolFlags.None); + addDeclarationToSymbol(symbol, id, ts.SymbolFlags.Module | ts.SymbolFlags.Assignment); } + return symbol; + }); + if (symbol) { + const flags = ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue; + declareSymbol(symbol.exports!, symbol, node, flags, ts.SymbolFlags.None); } + } - function bindExportsPropertyAssignment(node: ts.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, ts.SymbolFlags.Module | ts.SymbolFlags.Assignment); - } - return symbol; - }); + function bindExportsPropertyAssignment(node: ts.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) { - const isAlias = ts.isAliasableExpression(node.right) && (ts.isExportsIdentifier(node.left.expression) || ts.isModuleExportsAccessExpression(node.left.expression)); - const flags = isAlias ? ts.SymbolFlags.Alias : ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue; - ts.setParent(node.left, node); - declareSymbol(symbol.exports!, symbol, node.left, flags, ts.SymbolFlags.None); + addDeclarationToSymbol(symbol, id, ts.SymbolFlags.Module | ts.SymbolFlags.Assignment); } + return symbol; + }); + if (symbol) { + const isAlias = ts.isAliasableExpression(node.right) && (ts.isExportsIdentifier(node.left.expression) || ts.isModuleExportsAccessExpression(node.left.expression)); + const flags = isAlias ? ts.SymbolFlags.Alias : ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue; + ts.setParent(node.left, node); + declareSymbol(symbol.exports!, symbol, node.left, flags, ts.SymbolFlags.None); } + } - function bindModuleExportsAssignment(node: ts.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 = ts.getRightMostAssignedExpression(node.right); - if (ts.isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) { - return; - } - - if (ts.isObjectLiteralExpression(assignedExpression) && ts.every(assignedExpression.properties, ts.isShorthandPropertyAssignment)) { - ts.forEach(assignedExpression.properties, bindExportAssignedObjectMemberAlias); - return; - } - - // 'module.exports = expr' assignment - const flags = ts.exportAssignmentIsAlias(node) - ? ts.SymbolFlags.Alias // An export= with an EntityNameExpression or a ClassExpression exports all meanings of that identifier or class - : ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue | ts.SymbolFlags.ValueModule; - const symbol = declareSymbol(file.symbol.exports!, file.symbol, node, flags | ts.SymbolFlags.Assignment, ts.SymbolFlags.None); - ts.setValueDeclaration(symbol, node); + function bindModuleExportsAssignment(node: ts.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; } - function bindExportAssignedObjectMemberAlias(node: ts.ShorthandPropertyAssignment) { - declareSymbol(file.symbol.exports!, file.symbol, node, ts.SymbolFlags.Alias | ts.SymbolFlags.Assignment, ts.SymbolFlags.None); + const assignedExpression = ts.getRightMostAssignedExpression(node.right); + if (ts.isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) { + return; } - function bindThisPropertyAssignment(node: ts.BindablePropertyAssignmentExpression | ts.PropertyAccessExpression | ts.LiteralLikeElementAccessExpression) { - ts.Debug.assert(ts.isInJSFile(node)); - // private identifiers *must* be declared (even in JS files) - const hasPrivateIdentifier = (ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && ts.isPrivateIdentifier(node.left.name)) - || (ts.isPropertyAccessExpression(node) && ts.isPrivateIdentifier(node.name)); - if (hasPrivateIdentifier) { - return; - } - const thisContainer = ts.getThisContainer(node, /*includeArrowFunctions*/ false); - switch (thisContainer.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.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 (ts.isBinaryExpression(thisContainer.parent) && thisContainer.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - const l = thisContainer.parent.left; - if (ts.isBindableStaticAccessExpression(l) && ts.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 || ts.createSymbolTable(); - // It's acceptable for multiple 'this' assignments of the same identifier to occur - if (ts.hasDynamicName(node)) { - bindDynamicallyNamedThisPropertyAssignment(node, constructorSymbol, constructorSymbol.members); - } - else { - declareSymbol(constructorSymbol.members, constructorSymbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.SymbolFlags.PropertyExcludes & ~ts.SymbolFlags.Property); - } - addDeclarationToSymbol(constructorSymbol, constructorSymbol.valueDeclaration, ts.SymbolFlags.Class); - } - break; + if (ts.isObjectLiteralExpression(assignedExpression) && ts.every(assignedExpression.properties, ts.isShorthandPropertyAssignment)) { + ts.forEach(assignedExpression.properties, bindExportAssignedObjectMemberAlias); + return; + } - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - // this.foo assignment in a JavaScript class - // Bind this property to the containing class - const containingClass = thisContainer.parent; - const symbolTable = ts.isStatic(thisContainer) ? containingClass.symbol.exports! : containingClass.symbol.members!; - if (ts.hasDynamicName(node)) { - bindDynamicallyNamedThisPropertyAssignment(node, containingClass.symbol, symbolTable); - } - else { - declareSymbol(symbolTable, containingClass.symbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.SymbolFlags.None, /*isReplaceableByMethod*/ true); - } - break; - case ts.SyntaxKind.SourceFile: - // this.property = assignment in a source file -- declare symbol in exports for a module, in locals for a script + // 'module.exports = expr' assignment + const flags = ts.exportAssignmentIsAlias(node) + ? ts.SymbolFlags.Alias // An export= with an EntityNameExpression or a ClassExpression exports all meanings of that identifier or class + : ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue | ts.SymbolFlags.ValueModule; + const symbol = declareSymbol(file.symbol.exports!, file.symbol, node, flags | ts.SymbolFlags.Assignment, ts.SymbolFlags.None); + ts.setValueDeclaration(symbol, node); + } + function bindExportAssignedObjectMemberAlias(node: ts.ShorthandPropertyAssignment) { + declareSymbol(file.symbol.exports!, file.symbol, node, ts.SymbolFlags.Alias | ts.SymbolFlags.Assignment, ts.SymbolFlags.None); + } + function bindThisPropertyAssignment(node: ts.BindablePropertyAssignmentExpression | ts.PropertyAccessExpression | ts.LiteralLikeElementAccessExpression) { + ts.Debug.assert(ts.isInJSFile(node)); + // private identifiers *must* be declared (even in JS files) + const hasPrivateIdentifier = (ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && ts.isPrivateIdentifier(node.left.name)) + || (ts.isPropertyAccessExpression(node) && ts.isPrivateIdentifier(node.name)); + if (hasPrivateIdentifier) { + return; + } + const thisContainer = ts.getThisContainer(node, /*includeArrowFunctions*/ false); + switch (thisContainer.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.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 (ts.isBinaryExpression(thisContainer.parent) && thisContainer.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const l = thisContainer.parent.left; + if (ts.isBindableStaticAccessExpression(l) && ts.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 || ts.createSymbolTable(); + // It's acceptable for multiple 'this' assignments of the same identifier to occur if (ts.hasDynamicName(node)) { - break; - } - else if ((thisContainer as ts.SourceFile).commonJsModuleIndicator) { - declareSymbol(thisContainer.symbol.exports!, thisContainer.symbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue, ts.SymbolFlags.None); + bindDynamicallyNamedThisPropertyAssignment(node, constructorSymbol, constructorSymbol.members); } else { - declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.FunctionScopedVariableExcludes); + declareSymbol(constructorSymbol.members, constructorSymbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.SymbolFlags.PropertyExcludes & ~ts.SymbolFlags.Property); } + addDeclarationToSymbol(constructorSymbol, constructorSymbol.valueDeclaration, ts.SymbolFlags.Class); + } + break; + + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + // this.foo assignment in a JavaScript class + // Bind this property to the containing class + const containingClass = thisContainer.parent; + const symbolTable = ts.isStatic(thisContainer) ? containingClass.symbol.exports! : containingClass.symbol.members!; + if (ts.hasDynamicName(node)) { + bindDynamicallyNamedThisPropertyAssignment(node, containingClass.symbol, symbolTable); + } + else { + declareSymbol(symbolTable, containingClass.symbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.SymbolFlags.None, /*isReplaceableByMethod*/ true); + } + break; + case ts.SyntaxKind.SourceFile: + // this.property = assignment in a source file -- declare symbol in exports for a module, in locals for a script + if (ts.hasDynamicName(node)) { break; + } + else if ((thisContainer as ts.SourceFile).commonJsModuleIndicator) { + declareSymbol(thisContainer.symbol.exports!, thisContainer.symbol, node, ts.SymbolFlags.Property | ts.SymbolFlags.ExportValue, ts.SymbolFlags.None); + } + else { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.FunctionScopedVariableExcludes); + } + break; - default: - ts.Debug.failBadSyntaxKind(thisContainer); - } + default: + ts.Debug.failBadSyntaxKind(thisContainer); } + } - function bindDynamicallyNamedThisPropertyAssignment(node: ts.BinaryExpression | ts.DynamicNamedDeclaration, symbol: ts.Symbol, symbolTable: ts.SymbolTable) { - declareSymbol(symbolTable, symbol, node, ts.SymbolFlags.Property, ts.SymbolFlags.None, /*isReplaceableByMethod*/ true, /*isComputedName*/ true); - addLateBoundAssignmentDeclarationToSymbol(node, symbol); - } + function bindDynamicallyNamedThisPropertyAssignment(node: ts.BinaryExpression | ts.DynamicNamedDeclaration, symbol: ts.Symbol, symbolTable: ts.SymbolTable) { + declareSymbol(symbolTable, symbol, node, ts.SymbolFlags.Property, ts.SymbolFlags.None, /*isReplaceableByMethod*/ true, /*isComputedName*/ true); + addLateBoundAssignmentDeclarationToSymbol(node, symbol); + } - function addLateBoundAssignmentDeclarationToSymbol(node: ts.BinaryExpression | ts.DynamicNamedDeclaration, symbol: ts.Symbol | undefined) { - if (symbol) { - (symbol.assignmentDeclarationMembers || (symbol.assignmentDeclarationMembers = new ts.Map())).set(ts.getNodeId(node), node); - } + function addLateBoundAssignmentDeclarationToSymbol(node: ts.BinaryExpression | ts.DynamicNamedDeclaration, symbol: ts.Symbol | undefined) { + if (symbol) { + (symbol.assignmentDeclarationMembers || (symbol.assignmentDeclarationMembers = new ts.Map())).set(ts.getNodeId(node), node); } + } - function bindSpecialPropertyDeclaration(node: ts.PropertyAccessExpression | ts.LiteralLikeElementAccessExpression) { - if (node.expression.kind === ts.SyntaxKind.ThisKeyword) { - bindThisPropertyAssignment(node); + function bindSpecialPropertyDeclaration(node: ts.PropertyAccessExpression | ts.LiteralLikeElementAccessExpression) { + if (node.expression.kind === ts.SyntaxKind.ThisKeyword) { + bindThisPropertyAssignment(node); + } + else if (ts.isBindableStaticAccessExpression(node) && node.parent.parent.kind === ts.SyntaxKind.SourceFile) { + if (ts.isPrototypeAccess(node.expression)) { + bindPrototypePropertyAssignment(node, node.parent); } - else if (ts.isBindableStaticAccessExpression(node) && node.parent.parent.kind === ts.SyntaxKind.SourceFile) { - if (ts.isPrototypeAccess(node.expression)) { - bindPrototypePropertyAssignment(node, node.parent); - } - else { - bindStaticPropertyAssignment(node); - } + else { + bindStaticPropertyAssignment(node); } } + } - /** For `x.prototype = { p, ... }`, declare members p,... if `x` is function/class/{}, or not declared. */ - function bindPrototypeAssignment(node: ts.BindableStaticPropertyAssignmentExpression) { - ts.setParent(node.left, node); - ts.setParent(node.right, node); - bindPropertyAssignment(node.left.expression, node.left, /*isPrototypeProperty*/ false, /*containerIsClass*/ true); - } + /** For `x.prototype = { p, ... }`, declare members p,... if `x` is function/class/{}, or not declared. */ + function bindPrototypeAssignment(node: ts.BindableStaticPropertyAssignmentExpression) { + ts.setParent(node.left, node); + ts.setParent(node.right, node); + bindPropertyAssignment(node.left.expression, node.left, /*isPrototypeProperty*/ false, /*containerIsClass*/ true); + } - function bindObjectDefinePrototypeProperty(node: ts.BindableObjectDefinePropertyCall) { - const namespaceSymbol = lookupSymbolForPropertyAccess((node.arguments[0] as ts.PropertyAccessExpression).expression as ts.EntityNameExpression); - if (namespaceSymbol && namespaceSymbol.valueDeclaration) { - // Ensure the namespace symbol becomes class-like - addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, ts.SymbolFlags.Class); - } - bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ true); + function bindObjectDefinePrototypeProperty(node: ts.BindableObjectDefinePropertyCall) { + const namespaceSymbol = lookupSymbolForPropertyAccess((node.arguments[0] as ts.PropertyAccessExpression).expression as ts.EntityNameExpression); + if (namespaceSymbol && namespaceSymbol.valueDeclaration) { + // Ensure the namespace symbol becomes class-like + addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, ts.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: ts.BindableStaticAccessExpression, parent: ts.Node) { - // Look up the function in the local scope, since prototype assignments should - // follow the function declaration - const classPrototype = lhs.expression as ts.BindableStaticAccessExpression; - const constructorFunction = classPrototype.expression; + /** + * 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: ts.BindableStaticAccessExpression, parent: ts.Node) { + // Look up the function in the local scope, since prototype assignments should + // follow the function declaration + const classPrototype = lhs.expression as ts.BindableStaticAccessExpression; + const constructorFunction = classPrototype.expression; + + // Fix up parent pointers since we're going to use these nodes before we bind into them + ts.setParent(constructorFunction, classPrototype); + ts.setParent(classPrototype, lhs); + ts.setParent(lhs, parent); + + bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true, /*containerIsClass*/ true); + } - // Fix up parent pointers since we're going to use these nodes before we bind into them - ts.setParent(constructorFunction, classPrototype); - ts.setParent(classPrototype, lhs); - ts.setParent(lhs, parent); + function bindObjectDefinePropertyAssignment(node: ts.BindableObjectDefinePropertyCall) { + let namespaceSymbol = lookupSymbolForPropertyAccess(node.arguments[0]); + const isToplevel = node.parent.parent.kind === ts.SyntaxKind.SourceFile; + namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, node.arguments[0], isToplevel, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); + bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ false); + } - bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true, /*containerIsClass*/ true); + function bindSpecialPropertyAssignment(node: ts.BindablePropertyAssignmentExpression) { + // Class declarations in Typescript do not allow property declarations + const parentSymbol = lookupSymbolForPropertyAccess(node.left.expression, container) || lookupSymbolForPropertyAccess(node.left.expression, blockScopeContainer) ; + if (!ts.isInJSFile(node) && !ts.isFunctionSymbol(parentSymbol)) { + return; + } + const rootExpr = ts.getLeftmostAccessExpression(node.left); + if (ts.isIdentifier(rootExpr) && lookupSymbolForName(container, rootExpr.escapedText)!?.flags & ts.SymbolFlags.Alias) { + return; + } + // Fix up parent pointers since we're going to use these nodes before we bind into them + ts.setParent(node.left, node); + ts.setParent(node.right, node); + if (ts.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 ts.BindableStaticPropertyAssignmentExpression); + } + else if (ts.hasDynamicName(node)) { + bindAnonymousDeclaration(node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.InternalSymbolName.Computed); + const sym = bindPotentiallyMissingNamespaces(parentSymbol, node.left.expression, isTopLevelNamespaceAssignment(node.left), /*isPrototype*/ false, /*containerIsClass*/ false); + addLateBoundAssignmentDeclarationToSymbol(node, sym); } - - function bindObjectDefinePropertyAssignment(node: ts.BindableObjectDefinePropertyCall) { - let namespaceSymbol = lookupSymbolForPropertyAccess(node.arguments[0]); - const isToplevel = node.parent.parent.kind === ts.SyntaxKind.SourceFile; - namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, node.arguments[0], isToplevel, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); - bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ false); + else { + bindStaticPropertyAssignment(ts.cast(node.left, ts.isBindableStaticNameExpression)); } + } - function bindSpecialPropertyAssignment(node: ts.BindablePropertyAssignmentExpression) { - // Class declarations in Typescript do not allow property declarations - const parentSymbol = lookupSymbolForPropertyAccess(node.left.expression, container) || lookupSymbolForPropertyAccess(node.left.expression, blockScopeContainer) ; - if (!ts.isInJSFile(node) && !ts.isFunctionSymbol(parentSymbol)) { - return; - } - const rootExpr = ts.getLeftmostAccessExpression(node.left); - if (ts.isIdentifier(rootExpr) && lookupSymbolForName(container, rootExpr.escapedText)!?.flags & ts.SymbolFlags.Alias) { - return; - } - // Fix up parent pointers since we're going to use these nodes before we bind into them - ts.setParent(node.left, node); - ts.setParent(node.right, node); - if (ts.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 ts.BindableStaticPropertyAssignmentExpression); - } - else if (ts.hasDynamicName(node)) { - bindAnonymousDeclaration(node, ts.SymbolFlags.Property | ts.SymbolFlags.Assignment, ts.InternalSymbolName.Computed); - const sym = bindPotentiallyMissingNamespaces(parentSymbol, node.left.expression, isTopLevelNamespaceAssignment(node.left), /*isPrototype*/ false, /*containerIsClass*/ false); - addLateBoundAssignmentDeclarationToSymbol(node, sym); - } - else { - bindStaticPropertyAssignment(ts.cast(node.left, ts.isBindableStaticNameExpression)); - } + /** + * 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: ts.BindableStaticNameExpression) { + ts.Debug.assert(!ts.isIdentifier(node)); + ts.setParent(node.expression, node); + bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); + } + + function bindPotentiallyMissingNamespaces(namespaceSymbol: ts.Symbol | undefined, entityName: ts.BindableStaticNameExpression, isToplevel: boolean, isPrototypeProperty: boolean, containerIsClass: boolean) { + if (namespaceSymbol?.flags! & ts.SymbolFlags.Alias) { + return namespaceSymbol; + } + if (isToplevel && !isPrototypeProperty) { + // make symbols or add declarations for intermediate containers + const flags = ts.SymbolFlags.Module | ts.SymbolFlags.Assignment; + const excludeFlags = ts.SymbolFlags.ValueModuleExcludes & ~ts.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 = ts.createSymbolTable()); + return declareSymbol(table, parent, id, flags, excludeFlags); + } + }); + } + if (containerIsClass && namespaceSymbol && namespaceSymbol.valueDeclaration) { + addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, ts.SymbolFlags.Class); } + return namespaceSymbol; + } - /** - * 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: ts.BindableStaticNameExpression) { - ts.Debug.assert(!ts.isIdentifier(node)); - ts.setParent(node.expression, node); - bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); + function bindPotentiallyNewExpandoMemberToNamespace(declaration: ts.BindableStaticAccessExpression | ts.CallExpression, namespaceSymbol: ts.Symbol | undefined, isPrototypeProperty: boolean) { + if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) { + return; } - function bindPotentiallyMissingNamespaces(namespaceSymbol: ts.Symbol | undefined, entityName: ts.BindableStaticNameExpression, isToplevel: boolean, isPrototypeProperty: boolean, containerIsClass: boolean) { - if (namespaceSymbol?.flags! & ts.SymbolFlags.Alias) { - return namespaceSymbol; - } - if (isToplevel && !isPrototypeProperty) { - // make symbols or add declarations for intermediate containers - const flags = ts.SymbolFlags.Module | ts.SymbolFlags.Assignment; - const excludeFlags = ts.SymbolFlags.ValueModuleExcludes & ~ts.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 = ts.createSymbolTable()); - return declareSymbol(table, parent, id, flags, excludeFlags); - } - }); + // Set up the members collection if it doesn't exist already + const symbolTable = isPrototypeProperty ? + (namespaceSymbol.members || (namespaceSymbol.members = ts.createSymbolTable())) : + (namespaceSymbol.exports || (namespaceSymbol.exports = ts.createSymbolTable())); + let includes = ts.SymbolFlags.None; + let excludes = ts.SymbolFlags.None; + // Method-like + if (ts.isFunctionLikeDeclaration(ts.getAssignedExpandoInitializer(declaration)!)) { + includes = ts.SymbolFlags.Method; + excludes = ts.SymbolFlags.MethodExcludes; + } + // Maybe accessor-like + else if (ts.isCallExpression(declaration) && ts.isBindableObjectDefinePropertyCall(declaration)) { + if (ts.some(declaration.arguments[2].properties, p => { + const id = ts.getNameOfDeclaration(p); + return !!id && ts.isIdentifier(id) && ts.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 |= ts.SymbolFlags.SetAccessor | ts.SymbolFlags.Property; + excludes |= ts.SymbolFlags.SetAccessorExcludes; } - if (containerIsClass && namespaceSymbol && namespaceSymbol.valueDeclaration) { - addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, ts.SymbolFlags.Class); + if (ts.some(declaration.arguments[2].properties, p => { + const id = ts.getNameOfDeclaration(p); + return !!id && ts.isIdentifier(id) && ts.idText(id) === "get"; + })) { + includes |= ts.SymbolFlags.GetAccessor | ts.SymbolFlags.Property; + excludes |= ts.SymbolFlags.GetAccessorExcludes; } - return namespaceSymbol; } - function bindPotentiallyNewExpandoMemberToNamespace(declaration: ts.BindableStaticAccessExpression | ts.CallExpression, namespaceSymbol: ts.Symbol | undefined, isPrototypeProperty: boolean) { - if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) { - return; - } + if (includes === ts.SymbolFlags.None) { + includes = ts.SymbolFlags.Property; + excludes = ts.SymbolFlags.PropertyExcludes; + } - // Set up the members collection if it doesn't exist already - const symbolTable = isPrototypeProperty ? - (namespaceSymbol.members || (namespaceSymbol.members = ts.createSymbolTable())) : - (namespaceSymbol.exports || (namespaceSymbol.exports = ts.createSymbolTable())); - let includes = ts.SymbolFlags.None; - let excludes = ts.SymbolFlags.None; - // Method-like - if (ts.isFunctionLikeDeclaration(ts.getAssignedExpandoInitializer(declaration)!)) { - includes = ts.SymbolFlags.Method; - excludes = ts.SymbolFlags.MethodExcludes; - } - // Maybe accessor-like - else if (ts.isCallExpression(declaration) && ts.isBindableObjectDefinePropertyCall(declaration)) { - if (ts.some(declaration.arguments[2].properties, p => { - const id = ts.getNameOfDeclaration(p); - return !!id && ts.isIdentifier(id) && ts.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 |= ts.SymbolFlags.SetAccessor | ts.SymbolFlags.Property; - excludes |= ts.SymbolFlags.SetAccessorExcludes; - } - if (ts.some(declaration.arguments[2].properties, p => { - const id = ts.getNameOfDeclaration(p); - return !!id && ts.isIdentifier(id) && ts.idText(id) === "get"; - })) { - includes |= ts.SymbolFlags.GetAccessor | ts.SymbolFlags.Property; - excludes |= ts.SymbolFlags.GetAccessorExcludes; - } - } - - if (includes === ts.SymbolFlags.None) { - includes = ts.SymbolFlags.Property; - excludes = ts.SymbolFlags.PropertyExcludes; - } - - declareSymbol(symbolTable, namespaceSymbol, declaration, includes | ts.SymbolFlags.Assignment, excludes & ~ts.SymbolFlags.Assignment); - } - - function isTopLevelNamespaceAssignment(propertyAccess: ts.BindableAccessExpression) { - return ts.isBinaryExpression(propertyAccess.parent) - ? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === ts.SyntaxKind.SourceFile - : propertyAccess.parent.parent.kind === ts.SyntaxKind.SourceFile; - } - - function bindPropertyAssignment(name: ts.BindableStaticNameExpression, propertyAccess: ts.BindableStaticAccessExpression, isPrototypeProperty: boolean, containerIsClass: boolean) { - let namespaceSymbol = lookupSymbolForPropertyAccess(name, container) || lookupSymbolForPropertyAccess(name, blockScopeContainer); - 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.SymbolFlags.NamespaceModule)) { - return true; - } - const node = symbol.valueDeclaration; - if (node && ts.isCallExpression(node)) { - return !!ts.getAssignedExpandoInitializer(node); - } - let init = !node ? undefined : - ts.isVariableDeclaration(node) ? node.initializer : - ts.isBinaryExpression(node) ? node.right : - ts.isPropertyAccessExpression(node) && ts.isBinaryExpression(node.parent) ? node.parent.right : - undefined; - init = init && ts.getRightMostAssignedExpression(init); - if (init) { - const isPrototypeAssignment = ts.isPrototypeAccess(ts.isVariableDeclaration(node!) ? node.name : ts.isBinaryExpression(node!) ? node.left : node!); - return !!ts.getExpandoInitializer(ts.isBinaryExpression(init) && (init.operatorToken.kind === ts.SyntaxKind.BarBarToken || init.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment); - } - return false; + declareSymbol(symbolTable, namespaceSymbol, declaration, includes | ts.SymbolFlags.Assignment, excludes & ~ts.SymbolFlags.Assignment); + } + + function isTopLevelNamespaceAssignment(propertyAccess: ts.BindableAccessExpression) { + return ts.isBinaryExpression(propertyAccess.parent) + ? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === ts.SyntaxKind.SourceFile + : propertyAccess.parent.parent.kind === ts.SyntaxKind.SourceFile; + } + + function bindPropertyAssignment(name: ts.BindableStaticNameExpression, propertyAccess: ts.BindableStaticAccessExpression, isPrototypeProperty: boolean, containerIsClass: boolean) { + let namespaceSymbol = lookupSymbolForPropertyAccess(name, container) || lookupSymbolForPropertyAccess(name, blockScopeContainer); + 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Class | ts.SymbolFlags.NamespaceModule)) { + return true; + } + const node = symbol.valueDeclaration; + if (node && ts.isCallExpression(node)) { + return !!ts.getAssignedExpandoInitializer(node); } + let init = !node ? undefined : + ts.isVariableDeclaration(node) ? node.initializer : + ts.isBinaryExpression(node) ? node.right : + ts.isPropertyAccessExpression(node) && ts.isBinaryExpression(node.parent) ? node.parent.right : + undefined; + init = init && ts.getRightMostAssignedExpression(init); + if (init) { + const isPrototypeAssignment = ts.isPrototypeAccess(ts.isVariableDeclaration(node!) ? node.name : ts.isBinaryExpression(node!) ? node.left : node!); + return !!ts.getExpandoInitializer(ts.isBinaryExpression(init) && (init.operatorToken.kind === ts.SyntaxKind.BarBarToken || init.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment); + } + return false; + } - function getParentOfBinaryExpression(expr: ts.Node) { - while (ts.isBinaryExpression(expr.parent)) { - expr = expr.parent; - } - return expr.parent; + function getParentOfBinaryExpression(expr: ts.Node) { + while (ts.isBinaryExpression(expr.parent)) { + expr = expr.parent; } + return expr.parent; + } - function lookupSymbolForPropertyAccess(node: ts.BindableStaticNameExpression, lookupContainer: ts.Node = container): ts.Symbol | undefined { - if (ts.isIdentifier(node)) { - return lookupSymbolForName(lookupContainer, node.escapedText); - } - else { - const symbol = lookupSymbolForPropertyAccess(node.expression); - return symbol && symbol.exports && symbol.exports.get(ts.getElementOrPropertyAccessName(node)); - } + function lookupSymbolForPropertyAccess(node: ts.BindableStaticNameExpression, lookupContainer: ts.Node = container): ts.Symbol | undefined { + if (ts.isIdentifier(node)) { + return lookupSymbolForName(lookupContainer, node.escapedText); } + else { + const symbol = lookupSymbolForPropertyAccess(node.expression); + return symbol && symbol.exports && symbol.exports.get(ts.getElementOrPropertyAccessName(node)); + } + } - function forEachIdentifierInEntityName(e: ts.BindableStaticNameExpression, parent: ts.Symbol | undefined, action: (e: ts.Declaration, symbol: ts.Symbol | undefined, parent: ts.Symbol | undefined) => ts.Symbol | undefined): ts.Symbol | undefined { - if (isExportsOrModuleExportsOrAlias(file, e)) { - return file.symbol; - } - else if (ts.isIdentifier(e)) { - return action(e, lookupSymbolForPropertyAccess(e), parent); - } - else { - const s = forEachIdentifierInEntityName(e.expression, parent, action); - const name = ts.getNameOrArgument(e); - // unreachable - if (ts.isPrivateIdentifier(name)) { - ts.Debug.fail("unexpected PrivateIdentifier"); - } - return action(name, s && s.exports && s.exports.get(ts.getElementOrPropertyAccessName(e)), s); + function forEachIdentifierInEntityName(e: ts.BindableStaticNameExpression, parent: ts.Symbol | undefined, action: (e: ts.Declaration, symbol: ts.Symbol | undefined, parent: ts.Symbol | undefined) => ts.Symbol | undefined): ts.Symbol | undefined { + if (isExportsOrModuleExportsOrAlias(file, e)) { + return file.symbol; + } + else if (ts.isIdentifier(e)) { + return action(e, lookupSymbolForPropertyAccess(e), parent); + } + else { + const s = forEachIdentifierInEntityName(e.expression, parent, action); + const name = ts.getNameOrArgument(e); + // unreachable + if (ts.isPrivateIdentifier(name)) { + ts.Debug.fail("unexpected PrivateIdentifier"); } + return action(name, s && s.exports && s.exports.get(ts.getElementOrPropertyAccessName(e)), s); } + } - function bindCallExpression(node: ts.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 && ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) { - setCommonJsModuleIndicator(node); - } + function bindCallExpression(node: ts.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 && ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) { + setCommonJsModuleIndicator(node); } + } - function bindClassLikeDeclaration(node: ts.ClassLikeDeclaration) { - if (node.kind === ts.SyntaxKind.ClassDeclaration) { - bindBlockScopedDeclaration(node, ts.SymbolFlags.Class, ts.SymbolFlags.ClassExcludes); - } - else { - const bindingName = node.name ? node.name.escapedText : ts.InternalSymbolName.Class; - bindAnonymousDeclaration(node, ts.SymbolFlags.Class, bindingName); - // Add name of class expression into the map for semantic classifier - if (node.name) { - classifiableNames.add(node.name.escapedText); - } + function bindClassLikeDeclaration(node: ts.ClassLikeDeclaration) { + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + bindBlockScopedDeclaration(node, ts.SymbolFlags.Class, ts.SymbolFlags.ClassExcludes); + } + else { + const bindingName = node.name ? node.name.escapedText : ts.InternalSymbolName.Class; + bindAnonymousDeclaration(node, ts.SymbolFlags.Class, bindingName); + // Add name of class expression into the map for semantic classifier + if (node.name) { + classifiableNames.add(node.name.escapedText); } + } - const { symbol } = node; + 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(ts.SymbolFlags.Property | ts.SymbolFlags.Prototype, "prototype" as ts.__String); - const symbolExport = symbol.exports!.get(prototypeSymbol.escapedName); - if (symbolExport) { - if (node.name) { - ts.setParent(node.name, node); - } - file.bindDiagnostics.push(createDiagnosticForNode(symbolExport.declarations![0], ts.Diagnostics.Duplicate_identifier_0, ts.symbolName(prototypeSymbol))); + // 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(ts.SymbolFlags.Property | ts.SymbolFlags.Prototype, "prototype" as ts.__String); + const symbolExport = symbol.exports!.get(prototypeSymbol.escapedName); + if (symbolExport) { + if (node.name) { + ts.setParent(node.name, node); } - symbol.exports!.set(prototypeSymbol.escapedName, prototypeSymbol); - prototypeSymbol.parent = symbol; - } - - function bindEnumDeclaration(node: ts.EnumDeclaration) { - return ts.isEnumConst(node) - ? bindBlockScopedDeclaration(node, ts.SymbolFlags.ConstEnum, ts.SymbolFlags.ConstEnumExcludes) - : bindBlockScopedDeclaration(node, ts.SymbolFlags.RegularEnum, ts.SymbolFlags.RegularEnumExcludes); + file.bindDiagnostics.push(createDiagnosticForNode(symbolExport.declarations![0], ts.Diagnostics.Duplicate_identifier_0, ts.symbolName(prototypeSymbol))); } + symbol.exports!.set(prototypeSymbol.escapedName, prototypeSymbol); + prototypeSymbol.parent = symbol; + } - function bindVariableDeclarationOrBindingElement(node: ts.VariableDeclaration | ts.BindingElement) { - if (inStrictMode) { - checkStrictModeEvalOrArguments(node, node.name); - } + function bindEnumDeclaration(node: ts.EnumDeclaration) { + return ts.isEnumConst(node) + ? bindBlockScopedDeclaration(node, ts.SymbolFlags.ConstEnum, ts.SymbolFlags.ConstEnumExcludes) + : bindBlockScopedDeclaration(node, ts.SymbolFlags.RegularEnum, ts.SymbolFlags.RegularEnumExcludes); + } - if (!ts.isBindingPattern(node.name)) { - if (ts.isInJSFile(node) && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node) && !ts.getJSDocTypeTag(node) && !(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export)) { - declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); - } - else if (ts.isBlockOrCatchScoped(node)) { - bindBlockScopedDeclaration(node, ts.SymbolFlags.BlockScopedVariable, ts.SymbolFlags.BlockScopedVariableExcludes); - } - else if (ts.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, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.ParameterExcludes); - } - else { - declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.FunctionScopedVariableExcludes); - } - } + function bindVariableDeclarationOrBindingElement(node: ts.VariableDeclaration | ts.BindingElement) { + if (inStrictMode) { + checkStrictModeEvalOrArguments(node, node.name); } - function bindParameter(node: ts.ParameterDeclaration | ts.JSDocParameterTag) { - if (node.kind === ts.SyntaxKind.JSDocParameterTag && container.kind !== ts.SyntaxKind.JSDocSignature) { - return; - } - if (inStrictMode && !(node.flags & ts.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 (!ts.isBindingPattern(node.name)) { + if (ts.isInJSFile(node) && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node) && !ts.getJSDocTypeTag(node) && !(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export)) { + declareSymbolAndAddToSymbolTable(node as ts.Declaration, ts.SymbolFlags.Alias, ts.SymbolFlags.AliasExcludes); } - - if (ts.isBindingPattern(node.name)) { - bindAnonymousDeclaration(node, ts.SymbolFlags.FunctionScopedVariable, "__" + (node as ts.ParameterDeclaration).parent.parameters.indexOf(node as ts.ParameterDeclaration) as ts.__String); + else if (ts.isBlockOrCatchScoped(node)) { + bindBlockScopedDeclaration(node, ts.SymbolFlags.BlockScopedVariable, ts.SymbolFlags.BlockScopedVariableExcludes); } - else { + else if (ts.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, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.ParameterExcludes); } - - // If this is a property-parameter, then also declare the property symbol into the - // containing class. - if (ts.isParameterPropertyDeclaration(node, node.parent)) { - const classDeclaration = node.parent.parent; - declareSymbol(classDeclaration.symbol.members!, classDeclaration.symbol, node, ts.SymbolFlags.Property | (node.questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.SymbolFlags.PropertyExcludes); + else { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.FunctionScopedVariableExcludes); } } + } - function bindFunctionDeclaration(node: ts.FunctionDeclaration) { - if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient)) { - if (ts.isAsyncFunction(node)) { - emitFlags |= ts.NodeFlags.HasAsyncFunctions; - } - } + function bindParameter(node: ts.ParameterDeclaration | ts.JSDocParameterTag) { + if (node.kind === ts.SyntaxKind.JSDocParameterTag && container.kind !== ts.SyntaxKind.JSDocSignature) { + return; + } + if (inStrictMode && !(node.flags & ts.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); + } - checkStrictModeFunctionName(node); - if (inStrictMode) { - checkStrictModeFunctionDeclaration(node); - bindBlockScopedDeclaration(node, ts.SymbolFlags.Function, ts.SymbolFlags.FunctionExcludes); - } - else { - declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.Function, ts.SymbolFlags.FunctionExcludes); - } + if (ts.isBindingPattern(node.name)) { + bindAnonymousDeclaration(node, ts.SymbolFlags.FunctionScopedVariable, "__" + (node as ts.ParameterDeclaration).parent.parameters.indexOf(node as ts.ParameterDeclaration) as ts.__String); + } + else { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.ParameterExcludes); } - function bindFunctionExpression(node: ts.FunctionExpression) { - if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient)) { - if (ts.isAsyncFunction(node)) { - emitFlags |= ts.NodeFlags.HasAsyncFunctions; - } - } - if (currentFlow) { - node.flowNode = currentFlow; - } - checkStrictModeFunctionName(node); - const bindingName = node.name ? node.name.escapedText : ts.InternalSymbolName.Function; - return bindAnonymousDeclaration(node, ts.SymbolFlags.Function, bindingName); + // If this is a property-parameter, then also declare the property symbol into the + // containing class. + if (ts.isParameterPropertyDeclaration(node, node.parent)) { + const classDeclaration = node.parent.parent; + declareSymbol(classDeclaration.symbol.members!, classDeclaration.symbol, node, ts.SymbolFlags.Property | (node.questionToken ? ts.SymbolFlags.Optional : ts.SymbolFlags.None), ts.SymbolFlags.PropertyExcludes); } + } - function bindPropertyOrMethodOrAccessor(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { - if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient) && ts.isAsyncFunction(node)) { + function bindFunctionDeclaration(node: ts.FunctionDeclaration) { + if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient)) { + if (ts.isAsyncFunction(node)) { emitFlags |= ts.NodeFlags.HasAsyncFunctions; } + } - if (currentFlow && ts.isObjectLiteralOrClassExpressionMethodOrAccessor(node)) { - node.flowNode = currentFlow; + checkStrictModeFunctionName(node); + if (inStrictMode) { + checkStrictModeFunctionDeclaration(node); + bindBlockScopedDeclaration(node, ts.SymbolFlags.Function, ts.SymbolFlags.FunctionExcludes); + } + else { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.Function, ts.SymbolFlags.FunctionExcludes); + } + } + + function bindFunctionExpression(node: ts.FunctionExpression) { + if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient)) { + if (ts.isAsyncFunction(node)) { + emitFlags |= ts.NodeFlags.HasAsyncFunctions; } + } + if (currentFlow) { + node.flowNode = currentFlow; + } + checkStrictModeFunctionName(node); + const bindingName = node.name ? node.name.escapedText : ts.InternalSymbolName.Function; + return bindAnonymousDeclaration(node, ts.SymbolFlags.Function, bindingName); + } - return ts.hasDynamicName(node) - ? bindAnonymousDeclaration(node, symbolFlags, ts.InternalSymbolName.Computed) - : declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); + function bindPropertyOrMethodOrAccessor(node: ts.Declaration, symbolFlags: ts.SymbolFlags, symbolExcludes: ts.SymbolFlags) { + if (!file.isDeclarationFile && !(node.flags & ts.NodeFlags.Ambient) && ts.isAsyncFunction(node)) { + emitFlags |= ts.NodeFlags.HasAsyncFunctions; } - function getInferTypeContainer(node: ts.Node): ts.ConditionalTypeNode | undefined { - const extendsType = ts.findAncestor(node, n => n.parent && ts.isConditionalTypeNode(n.parent) && n.parent.extendsType === n); - return extendsType && extendsType.parent as ts.ConditionalTypeNode; + if (currentFlow && ts.isObjectLiteralOrClassExpressionMethodOrAccessor(node)) { + node.flowNode = currentFlow; } - function bindTypeParameter(node: ts.TypeParameterDeclaration) { - if (ts.isJSDocTemplateTag(node.parent)) { - const container = ts.getEffectiveContainerForJSDocTemplateTag(node.parent); - if (container) { - if (!container.locals) { - container.locals = ts.createSymbolTable(); - } - declareSymbol(container.locals, /*parent*/ undefined, node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); - } - else { - declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); - } - } - else if (node.parent.kind === ts.SyntaxKind.InferType) { - const container = getInferTypeContainer(node.parent); - if (container) { - if (!container.locals) { - container.locals = ts.createSymbolTable(); - } - declareSymbol(container.locals, /*parent*/ undefined, node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); - } - else { - bindAnonymousDeclaration(node, ts.SymbolFlags.TypeParameter, getDeclarationName(node)!); // TODO: GH#18217 + return ts.hasDynamicName(node) + ? bindAnonymousDeclaration(node, symbolFlags, ts.InternalSymbolName.Computed) + : declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); + } + + function getInferTypeContainer(node: ts.Node): ts.ConditionalTypeNode | undefined { + const extendsType = ts.findAncestor(node, n => n.parent && ts.isConditionalTypeNode(n.parent) && n.parent.extendsType === n); + return extendsType && extendsType.parent as ts.ConditionalTypeNode; + } + + function bindTypeParameter(node: ts.TypeParameterDeclaration) { + if (ts.isJSDocTemplateTag(node.parent)) { + const container = ts.getEffectiveContainerForJSDocTemplateTag(node.parent); + if (container) { + if (!container.locals) { + container.locals = ts.createSymbolTable(); } + declareSymbol(container.locals, /*parent*/ undefined, node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); } else { declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); } } - - // reachability checks - - function shouldReportErrorOnModuleDeclaration(node: ts.ModuleDeclaration): boolean { - const instanceState = getModuleInstanceState(node); - return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && ts.shouldPreserveConstEnums(options)); - } - - function checkUnreachable(node: ts.Node): boolean { - if (!(currentFlow.flags & ts.FlowFlags.Unreachable)) { - return false; - } - if (currentFlow === unreachableFlow) { - const reportError = - // report error on all statements except empty ones - (ts.isStatementButNotDeclaration(node) && node.kind !== ts.SyntaxKind.EmptyStatement) || - // report error on class declarations - node.kind === ts.SyntaxKind.ClassDeclaration || - // report error on instantiated modules or const-enums only modules if preserveConstEnums is set - (node.kind === ts.SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration(node as ts.ModuleDeclaration)); - - 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 = ts.unreachableCodeIsError(options) && - !(node.flags & ts.NodeFlags.Ambient) && - (!ts.isVariableStatement(node) || - !!(ts.getCombinedNodeFlags(node.declarationList) & ts.NodeFlags.BlockScoped) || - node.declarationList.declarations.some(d => !!d.initializer)); - eachUnreachableRange(node, (start, end) => errorOrSuggestionOnRange(isError, start, end, ts.Diagnostics.Unreachable_code_detected)); - } + else if (node.parent.kind === ts.SyntaxKind.InferType) { + const container = getInferTypeContainer(node.parent); + if (container) { + if (!container.locals) { + container.locals = ts.createSymbolTable(); } + declareSymbol(container.locals, /*parent*/ undefined, node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); + } + else { + bindAnonymousDeclaration(node, ts.SymbolFlags.TypeParameter, getDeclarationName(node)!); // TODO: GH#18217 } - return true; + } + else { + declareSymbolAndAddToSymbolTable(node, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeParameterExcludes); } } - function eachUnreachableRange(node: ts.Node, cb: (start: ts.Node, last: ts.Node) => void): void { - if (ts.isStatement(node) && isExecutableStatement(node) && ts.isBlock(node.parent)) { - const { statements } = node.parent; - const slice = ts.sliceAfter(statements, node); - ts.getRangesWhere(slice, isExecutableStatement, (start, afterEnd) => cb(slice[start], slice[afterEnd - 1])); - } - else { - cb(node, node); + // reachability checks + + function shouldReportErrorOnModuleDeclaration(node: ts.ModuleDeclaration): boolean { + const instanceState = getModuleInstanceState(node); + return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && ts.shouldPreserveConstEnums(options)); + } + + function checkUnreachable(node: ts.Node): boolean { + if (!(currentFlow.flags & ts.FlowFlags.Unreachable)) { + return false; } + if (currentFlow === unreachableFlow) { + const reportError = + // report error on all statements except empty ones + (ts.isStatementButNotDeclaration(node) && node.kind !== ts.SyntaxKind.EmptyStatement) || + // report error on class declarations + node.kind === ts.SyntaxKind.ClassDeclaration || + // report error on instantiated modules or const-enums only modules if preserveConstEnums is set + (node.kind === ts.SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration(node as ts.ModuleDeclaration)); + + 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 = ts.unreachableCodeIsError(options) && + !(node.flags & ts.NodeFlags.Ambient) && + (!ts.isVariableStatement(node) || + !!(ts.getCombinedNodeFlags(node.declarationList) & ts.NodeFlags.BlockScoped) || + node.declarationList.declarations.some(d => !!d.initializer)); + eachUnreachableRange(node, (start, end) => errorOrSuggestionOnRange(isError, start, end, ts.Diagnostics.Unreachable_code_detected)); + } + } + } + return true; } - // As opposed to a pure declaration like an `interface` - function isExecutableStatement(s: ts.Statement): boolean { - // Don't remove statements that can validly be used before they appear. - return !ts.isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) && !ts.isEnumDeclaration(s) && - // `var x;` may declare a variable used above - !(ts.isVariableStatement(s) && !(ts.getCombinedNodeFlags(s) & (ts.NodeFlags.Let | ts.NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer)); +} + +function eachUnreachableRange(node: ts.Node, cb: (start: ts.Node, last: ts.Node) => void): void { + if (ts.isStatement(node) && isExecutableStatement(node) && ts.isBlock(node.parent)) { + const { statements } = node.parent; + const slice = ts.sliceAfter(statements, node); + ts.getRangesWhere(slice, isExecutableStatement, (start, afterEnd) => cb(slice[start], slice[afterEnd - 1])); } + else { + cb(node, node); + } +} +// As opposed to a pure declaration like an `interface` +function isExecutableStatement(s: ts.Statement): boolean { + // Don't remove statements that can validly be used before they appear. + return !ts.isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) && !ts.isEnumDeclaration(s) && + // `var x;` may declare a variable used above + !(ts.isVariableStatement(s) && !(ts.getCombinedNodeFlags(s) & (ts.NodeFlags.Let | ts.NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer)); +} - function isPurelyTypeDeclaration(s: ts.Statement): boolean { - switch (s.kind) { - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - return true; - case ts.SyntaxKind.ModuleDeclaration: - return getModuleInstanceState(s as ts.ModuleDeclaration) !== ModuleInstanceState.Instantiated; - case ts.SyntaxKind.EnumDeclaration: - return ts.hasSyntacticModifier(s, ts.ModifierFlags.Const); - default: - return false; - } - } - - export function isExportsOrModuleExportsOrAlias(sourceFile: ts.SourceFile, node: ts.Expression): boolean { - let i = 0; - const q = [node]; - while (q.length && i < 100) { - i++; - node = q.shift()!; - if (ts.isExportsIdentifier(node) || ts.isModuleExportsAccessExpression(node)) { - return true; - } - else if (ts.isIdentifier(node)) { - const symbol = lookupSymbolForName(sourceFile, node.escapedText); - if (!!symbol && !!symbol.valueDeclaration && ts.isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { - const init = symbol.valueDeclaration.initializer; - q.push(init); - if (ts.isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { - q.push(init.left); - q.push(init.right); - } +function isPurelyTypeDeclaration(s: ts.Statement): boolean { + switch (s.kind) { + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + return true; + case ts.SyntaxKind.ModuleDeclaration: + return getModuleInstanceState(s as ts.ModuleDeclaration) !== ModuleInstanceState.Instantiated; + case ts.SyntaxKind.EnumDeclaration: + return ts.hasSyntacticModifier(s, ts.ModifierFlags.Const); + default: + return false; + } +} + +export function isExportsOrModuleExportsOrAlias(sourceFile: ts.SourceFile, node: ts.Expression): boolean { + let i = 0; + const q = [node]; + while (q.length && i < 100) { + i++; + node = q.shift()!; + if (ts.isExportsIdentifier(node) || ts.isModuleExportsAccessExpression(node)) { + return true; + } + else if (ts.isIdentifier(node)) { + const symbol = lookupSymbolForName(sourceFile, node.escapedText); + if (!!symbol && !!symbol.valueDeclaration && ts.isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { + const init = symbol.valueDeclaration.initializer; + q.push(init); + if (ts.isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { + q.push(init.left); + q.push(init.right); } } } - return false; } + return false; +} - function lookupSymbolForName(container: ts.Node, name: ts.__String): ts.Symbol | undefined { - const local = container.locals && container.locals.get(name); - if (local) { - return local.exportSymbol || local; - } - if (ts.isSourceFile(container) && container.jsGlobalAugmentations && container.jsGlobalAugmentations.has(name)) { - return container.jsGlobalAugmentations.get(name); - } - return container.symbol && container.symbol.exports && container.symbol.exports.get(name); +function lookupSymbolForName(container: ts.Node, name: ts.__String): ts.Symbol | undefined { + const local = container.locals && container.locals.get(name); + if (local) { + return local.exportSymbol || local; } + if (ts.isSourceFile(container) && container.jsGlobalAugmentations && container.jsGlobalAugmentations.has(name)) { + return container.jsGlobalAugmentations.get(name); + } + return container.symbol && container.symbol.exports && container.symbol.exports.get(name); +} } diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 7626ad2638982..58c779d132d67 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,1358 +1,1358 @@ /*@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?: {}; - reportDeprecated?: {}; - source?: string; - relatedInformation?: ReusableDiagnosticRelatedInformation[]; - skippedOn?: keyof ts.CompilerOptions; - } +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?: {}; + reportDeprecated?: {}; + source?: string; + relatedInformation?: ReusableDiagnosticRelatedInformation[]; + skippedOn?: keyof ts.CompilerOptions; +} - export interface ReusableDiagnosticRelatedInformation { - category: ts.DiagnosticCategory; - code: number; - file: string | undefined; - start: number | undefined; - length: number | undefined; - messageText: string | ReusableDiagnosticMessageChain; - } +export interface ReusableDiagnosticRelatedInformation { + category: ts.DiagnosticCategory; + code: number; + file: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | ReusableDiagnosticMessageChain; +} - export type ReusableDiagnosticMessageChain = ts.DiagnosticMessageChain; - export interface ReusableBuilderProgramState extends ts.ReusableBuilderState { - /** - * Cache of bind and check diagnostics for files with their Path being the key - */ - semanticDiagnosticsPerFile?: ts.ReadonlyESMap | undefined; - /** - * The map has key by source file's path that has been changed - */ - changedFilesSet?: ts.ReadonlySet; - /** - * Set of affected files being iterated - */ - affectedFiles?: readonly ts.SourceFile[] | undefined; - /** - * Current changed file for iterating over affected files - */ - currentChangedFilePath?: ts.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.ReadonlyESMap | undefined; - /** - * Newly computed visible to outside referencedSet - */ - currentAffectedFilesExportedModulesMap?: ts.BuilderState.ReadonlyManyToManyPathMap | undefined; - /** - * True if the semantic diagnostics were copied from the old state - */ - semanticDiagnosticsFromOldState?: ts.Set; - /** - * program corresponding to this state - */ - program?: ts.Program | undefined; - /** - * compilerOptions for the program - */ - compilerOptions: ts.CompilerOptions; - /** - * Files pending to be emitted - */ - affectedFilesPendingEmit?: readonly ts.Path[] | undefined; - /** - * Files pending to be emitted kind. - */ - affectedFilesPendingEmitKind?: ts.ReadonlyESMap | undefined; - /** - * Current index to retrieve pending affected file - */ - affectedFilesPendingEmitIndex?: number | undefined; - /* - * true if semantic diagnostics are ReusableDiagnostic instead of Diagnostic - */ - hasReusableDiagnostic?: true; - } +export type ReusableDiagnosticMessageChain = ts.DiagnosticMessageChain; +export interface ReusableBuilderProgramState extends ts.ReusableBuilderState { + /** + * Cache of bind and check diagnostics for files with their Path being the key + */ + semanticDiagnosticsPerFile?: ts.ReadonlyESMap | undefined; + /** + * The map has key by source file's path that has been changed + */ + changedFilesSet?: ts.ReadonlySet; + /** + * Set of affected files being iterated + */ + affectedFiles?: readonly ts.SourceFile[] | undefined; + /** + * Current changed file for iterating over affected files + */ + currentChangedFilePath?: ts.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.ReadonlyESMap | undefined; + /** + * Newly computed visible to outside referencedSet + */ + currentAffectedFilesExportedModulesMap?: ts.BuilderState.ReadonlyManyToManyPathMap | undefined; + /** + * True if the semantic diagnostics were copied from the old state + */ + semanticDiagnosticsFromOldState?: ts.Set; + /** + * program corresponding to this state + */ + program?: ts.Program | undefined; + /** + * compilerOptions for the program + */ + compilerOptions: ts.CompilerOptions; + /** + * Files pending to be emitted + */ + affectedFilesPendingEmit?: readonly ts.Path[] | undefined; + /** + * Files pending to be emitted kind. + */ + affectedFilesPendingEmitKind?: ts.ReadonlyESMap | 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 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. +export interface BuilderProgramState extends ts.BuilderState { /** - * 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 ts.BuilderState { - /** - * Cache of bind and check diagnostics for files with their Path being the key - */ - semanticDiagnosticsPerFile: ts.ESMap | undefined; - /** - * The map has key by source file's path that has been changed - */ - changedFilesSet: ts.Set; - /** - * Set of affected files being iterated - */ - affectedFiles: readonly ts.SourceFile[] | undefined; - /** - * Current index to retrieve affected file from - */ - affectedFilesIndex: number | undefined; - /** - * Current changed file for iterating over affected files - */ - currentChangedFilePath: ts.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.ESMap | undefined; - /** - * Newly computed visible to outside referencedSet - * We need to store the updates separately in case the in-progress build is cancelled - * and we need to roll back. - */ - currentAffectedFilesExportedModulesMap: ts.BuilderState.ManyToManyPathMap | undefined; - /** - * Already seen affected files - */ - seenAffectedFiles: ts.Set | 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.Set; - /** - * program corresponding to this state - */ - program: ts.Program | undefined; - /** - * compilerOptions for the program - */ - compilerOptions: ts.CompilerOptions; - /** - * Files pending to be emitted - */ - affectedFilesPendingEmit: ts.Path[] | undefined; - /** - * Files pending to be emitted kind. - */ - affectedFilesPendingEmitKind: ts.ESMap | undefined; - /** - * Current index to retrieve pending affected file - */ - affectedFilesPendingEmitIndex: number | undefined; - /** - * true if build info is emitted - */ - buildInfoEmitPending: boolean; - /** - * Already seen emitted files - */ - seenEmittedFiles: ts.ESMap | undefined; - /** - * true if program has been emitted - */ - programEmitComplete?: true; - /** Stores list of files that change signature during emit - test only */ - filesChangingSignature?: ts.Set; - } + semanticDiagnosticsPerFile: ts.ESMap | undefined; + /** + * The map has key by source file's path that has been changed + */ + changedFilesSet: ts.Set; + /** + * Set of affected files being iterated + */ + affectedFiles: readonly ts.SourceFile[] | undefined; + /** + * Current index to retrieve affected file from + */ + affectedFilesIndex: number | undefined; + /** + * Current changed file for iterating over affected files + */ + currentChangedFilePath: ts.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.ESMap | undefined; + /** + * Newly computed visible to outside referencedSet + * We need to store the updates separately in case the in-progress build is cancelled + * and we need to roll back. + */ + currentAffectedFilesExportedModulesMap: ts.BuilderState.ManyToManyPathMap | undefined; + /** + * Already seen affected files + */ + seenAffectedFiles: ts.Set | 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.Set; + /** + * program corresponding to this state + */ + program: ts.Program | undefined; + /** + * compilerOptions for the program + */ + compilerOptions: ts.CompilerOptions; + /** + * Files pending to be emitted + */ + affectedFilesPendingEmit: ts.Path[] | undefined; + /** + * Files pending to be emitted kind. + */ + affectedFilesPendingEmitKind: ts.ESMap | undefined; + /** + * Current index to retrieve pending affected file + */ + affectedFilesPendingEmitIndex: number | undefined; + /** + * true if build info is emitted + */ + buildInfoEmitPending: boolean; + /** + * Already seen emitted files + */ + seenEmittedFiles: ts.ESMap | undefined; + /** + * true if program has been emitted + */ + programEmitComplete?: true; + /** Stores list of files that change signature during emit - test only */ + filesChangingSignature?: ts.Set; +} - function hasSameKeys(map1: ts.ReadonlyCollection | undefined, map2: ts.ReadonlyCollection | undefined): boolean { - // Has same size and every key is present in both maps - return map1 === map2 || map1 !== undefined && map2 !== undefined && map1.size === map2.size && !ts.forEachKey(map1, key => !map2.has(key)); +function hasSameKeys(map1: ts.ReadonlyCollection | undefined, map2: ts.ReadonlyCollection | undefined): boolean { + // Has same size and every key is present in both maps + return map1 === map2 || map1 !== undefined && map2 !== undefined && map1.size === map2.size && !ts.forEachKey(map1, key => !map2.has(key)); +} + +/** + * Create the state so that we can iterate on changedFiles/affected files + */ +function createBuilderProgramState(newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName, oldState: Readonly | undefined, disableUseFileVersionAsSignature: boolean | undefined): BuilderProgramState { + const state = ts.BuilderState.create(newProgram, getCanonicalFileName, oldState, disableUseFileVersionAsSignature) 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 (!ts.outFile(compilerOptions)) { + state.semanticDiagnosticsPerFile = new ts.Map(); } + state.changedFilesSet = new ts.Set(); + const useOldState = ts.BuilderState.canReuseOldState(state.referencedMap, oldState); + const oldCompilerOptions = useOldState ? oldState!.compilerOptions : undefined; + const canCopySemanticDiagnostics = useOldState && oldState!.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile && + !ts.compilerOptionsAffectSemanticDiagnostics(compilerOptions, oldCompilerOptions!); + if (useOldState) { + // Verify the sanity of old state + if (!oldState!.currentChangedFilePath) { + const affectedSignatures = oldState!.currentAffectedFilesSignatures; + ts.Debug.assert(!oldState!.affectedFiles && (!affectedSignatures || !affectedSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); + } + const changedFilesSet = oldState!.changedFilesSet; + if (canCopySemanticDiagnostics) { + ts.Debug.assert(!changedFilesSet || !ts.forEachKey(changedFilesSet, path => oldState!.semanticDiagnosticsPerFile!.has(path)), "Semantic diagnostics shouldnt be available for changed files"); + } - /** - * Create the state so that we can iterate on changedFiles/affected files - */ - function createBuilderProgramState(newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName, oldState: Readonly | undefined, disableUseFileVersionAsSignature: boolean | undefined): BuilderProgramState { - const state = ts.BuilderState.create(newProgram, getCanonicalFileName, oldState, disableUseFileVersionAsSignature) 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 (!ts.outFile(compilerOptions)) { - state.semanticDiagnosticsPerFile = new ts.Map(); + // Copy old state's changed files set + changedFilesSet?.forEach(value => state.changedFilesSet.add(value)); + if (!ts.outFile(compilerOptions) && oldState!.affectedFilesPendingEmit) { + state.affectedFilesPendingEmit = oldState!.affectedFilesPendingEmit.slice(); + state.affectedFilesPendingEmitKind = oldState!.affectedFilesPendingEmitKind && new ts.Map(oldState!.affectedFilesPendingEmitKind); + state.affectedFilesPendingEmitIndex = oldState!.affectedFilesPendingEmitIndex; + state.seenAffectedFiles = new ts.Set(); } - state.changedFilesSet = new ts.Set(); - const useOldState = ts.BuilderState.canReuseOldState(state.referencedMap, oldState); - const oldCompilerOptions = useOldState ? oldState!.compilerOptions : undefined; - const canCopySemanticDiagnostics = useOldState && oldState!.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile && - !ts.compilerOptionsAffectSemanticDiagnostics(compilerOptions, oldCompilerOptions!); - if (useOldState) { - // Verify the sanity of old state - if (!oldState!.currentChangedFilePath) { - const affectedSignatures = oldState!.currentAffectedFilesSignatures; - ts.Debug.assert(!oldState!.affectedFiles && (!affectedSignatures || !affectedSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); - } - const changedFilesSet = oldState!.changedFilesSet; - if (canCopySemanticDiagnostics) { - ts.Debug.assert(!changedFilesSet || !ts.forEachKey(changedFilesSet, path => oldState!.semanticDiagnosticsPerFile!.has(path)), "Semantic diagnostics shouldnt be available for changed files"); - } + } - // Copy old state's changed files set - changedFilesSet?.forEach(value => state.changedFilesSet.add(value)); - if (!ts.outFile(compilerOptions) && oldState!.affectedFilesPendingEmit) { - state.affectedFilesPendingEmit = oldState!.affectedFilesPendingEmit.slice(); - state.affectedFilesPendingEmitKind = oldState!.affectedFilesPendingEmitKind && new ts.Map(oldState!.affectedFilesPendingEmitKind); - state.affectedFilesPendingEmitIndex = oldState!.affectedFilesPendingEmitIndex; - state.seenAffectedFiles = new ts.Set(); + // 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: ts.ReadonlySet | undefined; + + // if not using old state, every file is changed + if (!useOldState || + // File wasn't present in old state + !(oldInfo = oldState!.fileInfos.get(sourceFilePath)) || + // versions dont match + oldInfo.version !== info.version || + // Referenced files changed + !hasSameKeys(newReferences = referencedMap && referencedMap.getValues(sourceFilePath), oldReferencedMap && oldReferencedMap.getValues(sourceFilePath)) || + // Referenced file was deleted in the new program + newReferences && ts.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.add(sourceFilePath); + } + else if (canCopySemanticDiagnostics) { + const sourceFile = newProgram.getSourceFileByPath(sourceFilePath)!; + + 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 ts.Diagnostic[]); + if (!state.semanticDiagnosticsFromOldState) { + state.semanticDiagnosticsFromOldState = new ts.Set(); + } + state.semanticDiagnosticsFromOldState.add(sourceFilePath); } } + }); - // 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: ts.ReadonlySet | undefined; - - // if not using old state, every file is changed - if (!useOldState || - // File wasn't present in old state - !(oldInfo = oldState!.fileInfos.get(sourceFilePath)) || - // versions dont match - oldInfo.version !== info.version || - // Referenced files changed - !hasSameKeys(newReferences = referencedMap && referencedMap.getValues(sourceFilePath), oldReferencedMap && oldReferencedMap.getValues(sourceFilePath)) || - // Referenced file was deleted in the new program - newReferences && ts.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 + // If the global file is removed, add all files as changed + if (useOldState && ts.forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => info.affectsGlobalScope && !state.fileInfos.has(sourceFilePath))) { + ts.BuilderState.getAllFilesExcludingDefaultLibraryFile(state, newProgram, /*firstSourceFile*/ undefined) + .forEach(file => state.changedFilesSet.add(file.resolvedPath)); + } + else if (oldCompilerOptions && !ts.outFile(compilerOptions) && ts.compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) { + // Add all files to affectedFilesPendingEmit since emit changed + newProgram.getSourceFiles().forEach(f => addToAffectedFilesPendingEmit(state, f.resolvedPath, BuilderFileEmit.Full)); + ts.Debug.assert(!state.seenAffectedFiles || !state.seenAffectedFiles.size); + state.seenAffectedFiles = state.seenAffectedFiles || new ts.Set(); + } + if (useOldState) { + // Any time the interpretation of a source file changes, mark it as changed + ts.forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => { + if (state.fileInfos.has(sourceFilePath) && state.fileInfos.get(sourceFilePath)!.impliedFormat !== info.impliedFormat) { state.changedFilesSet.add(sourceFilePath); } - else if (canCopySemanticDiagnostics) { - const sourceFile = newProgram.getSourceFileByPath(sourceFilePath)!; - - 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 ts.Diagnostic[]); - if (!state.semanticDiagnosticsFromOldState) { - state.semanticDiagnosticsFromOldState = new ts.Set(); - } - state.semanticDiagnosticsFromOldState.add(sourceFilePath); - } - } }); - - // If the global file is removed, add all files as changed - if (useOldState && ts.forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => info.affectsGlobalScope && !state.fileInfos.has(sourceFilePath))) { - ts.BuilderState.getAllFilesExcludingDefaultLibraryFile(state, newProgram, /*firstSourceFile*/ undefined) - .forEach(file => state.changedFilesSet.add(file.resolvedPath)); - } - else if (oldCompilerOptions && !ts.outFile(compilerOptions) && ts.compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) { - // Add all files to affectedFilesPendingEmit since emit changed - newProgram.getSourceFiles().forEach(f => addToAffectedFilesPendingEmit(state, f.resolvedPath, BuilderFileEmit.Full)); - ts.Debug.assert(!state.seenAffectedFiles || !state.seenAffectedFiles.size); - state.seenAffectedFiles = state.seenAffectedFiles || new ts.Set(); - } - if (useOldState) { - // Any time the interpretation of a source file changes, mark it as changed - ts.forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => { - if (state.fileInfos.has(sourceFilePath) && state.fileInfos.get(sourceFilePath)!.impliedFormat !== info.impliedFormat) { - state.changedFilesSet.add(sourceFilePath); - } - }); - } - - state.buildInfoEmitPending = !!state.changedFilesSet.size; - return state; } - function convertToDiagnostics(diagnostics: readonly ReusableDiagnostic[], newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName): readonly ts.Diagnostic[] { - if (!diagnostics.length) - return ts.emptyArray; - const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(ts.getTsBuildInfoEmitOutputFilePath(newProgram.getCompilerOptions())!, newProgram.getCurrentDirectory())); - return diagnostics.map(diagnostic => { - const result: ts.Diagnostic = convertToDiagnosticRelatedInformation(diagnostic, newProgram, toPath); - result.reportsUnnecessary = diagnostic.reportsUnnecessary; - result.reportsDeprecated = diagnostic.reportDeprecated; - result.source = diagnostic.source; - result.skippedOn = diagnostic.skippedOn; - const { relatedInformation } = diagnostic; - result.relatedInformation = relatedInformation ? - relatedInformation.length ? - relatedInformation.map(r => convertToDiagnosticRelatedInformation(r, newProgram, toPath)) : - [] : - undefined; - return result; - }); + state.buildInfoEmitPending = !!state.changedFilesSet.size; + return state; +} - function toPath(path: string) { - return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); - } - } +function convertToDiagnostics(diagnostics: readonly ReusableDiagnostic[], newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName): readonly ts.Diagnostic[] { + if (!diagnostics.length) + return ts.emptyArray; + const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(ts.getTsBuildInfoEmitOutputFilePath(newProgram.getCompilerOptions())!, newProgram.getCurrentDirectory())); + return diagnostics.map(diagnostic => { + const result: ts.Diagnostic = convertToDiagnosticRelatedInformation(diagnostic, newProgram, toPath); + result.reportsUnnecessary = diagnostic.reportsUnnecessary; + result.reportsDeprecated = diagnostic.reportDeprecated; + result.source = diagnostic.source; + result.skippedOn = diagnostic.skippedOn; + const { relatedInformation } = diagnostic; + result.relatedInformation = relatedInformation ? + relatedInformation.length ? + relatedInformation.map(r => convertToDiagnosticRelatedInformation(r, newProgram, toPath)) : + [] : + undefined; + return result; + }); - function convertToDiagnosticRelatedInformation(diagnostic: ReusableDiagnosticRelatedInformation, newProgram: ts.Program, toPath: (path: string) => ts.Path): ts.DiagnosticRelatedInformation { - const { file } = diagnostic; - return { - ...diagnostic, - file: file ? newProgram.getSourceFileByPath(toPath(file)) : undefined - }; + function toPath(path: string) { + return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); } +} - /** - * Releases program and other related not needed properties - */ - function releaseCache(state: BuilderProgramState) { - ts.BuilderState.releaseCache(state); - state.program = undefined; - } +function convertToDiagnosticRelatedInformation(diagnostic: ReusableDiagnosticRelatedInformation, newProgram: ts.Program, toPath: (path: string) => ts.Path): ts.DiagnosticRelatedInformation { + const { file } = diagnostic; + return { + ...diagnostic, + file: file ? newProgram.getSourceFileByPath(toPath(file)) : undefined + }; +} - /** - * Creates a clone of the state - */ - function cloneBuilderProgramState(state: Readonly): BuilderProgramState { - const newState = ts.BuilderState.clone(state) as BuilderProgramState; - newState.semanticDiagnosticsPerFile = state.semanticDiagnosticsPerFile && new ts.Map(state.semanticDiagnosticsPerFile); - newState.changedFilesSet = new ts.Set(state.changedFilesSet); - newState.affectedFiles = state.affectedFiles; - newState.affectedFilesIndex = state.affectedFilesIndex; - newState.currentChangedFilePath = state.currentChangedFilePath; - newState.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures && new ts.Map(state.currentAffectedFilesSignatures); - newState.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap?.clone(); - newState.seenAffectedFiles = state.seenAffectedFiles && new ts.Set(state.seenAffectedFiles); - newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles; - newState.semanticDiagnosticsFromOldState = state.semanticDiagnosticsFromOldState && new ts.Set(state.semanticDiagnosticsFromOldState); - newState.program = state.program; - newState.compilerOptions = state.compilerOptions; - newState.affectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); - newState.affectedFilesPendingEmitKind = state.affectedFilesPendingEmitKind && new ts.Map(state.affectedFilesPendingEmitKind); - newState.affectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; - newState.seenEmittedFiles = state.seenEmittedFiles && new ts.Map(state.seenEmittedFiles); - newState.programEmitComplete = state.programEmitComplete; - return newState; - } +/** + * Releases program and other related not needed properties + */ +function releaseCache(state: BuilderProgramState) { + ts.BuilderState.releaseCache(state); + state.program = undefined; +} - /** - * Verifies that source file is ok to be used in calls that arent handled by next - */ - function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: ts.SourceFile | undefined) { - ts.Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex! - 1] !== sourceFile || !state.semanticDiagnosticsPerFile!.has(sourceFile.resolvedPath)); - } +/** + * Creates a clone of the state + */ +function cloneBuilderProgramState(state: Readonly): BuilderProgramState { + const newState = ts.BuilderState.clone(state) as BuilderProgramState; + newState.semanticDiagnosticsPerFile = state.semanticDiagnosticsPerFile && new ts.Map(state.semanticDiagnosticsPerFile); + newState.changedFilesSet = new ts.Set(state.changedFilesSet); + newState.affectedFiles = state.affectedFiles; + newState.affectedFilesIndex = state.affectedFilesIndex; + newState.currentChangedFilePath = state.currentChangedFilePath; + newState.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures && new ts.Map(state.currentAffectedFilesSignatures); + newState.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap?.clone(); + newState.seenAffectedFiles = state.seenAffectedFiles && new ts.Set(state.seenAffectedFiles); + newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles; + newState.semanticDiagnosticsFromOldState = state.semanticDiagnosticsFromOldState && new ts.Set(state.semanticDiagnosticsFromOldState); + newState.program = state.program; + newState.compilerOptions = state.compilerOptions; + newState.affectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); + newState.affectedFilesPendingEmitKind = state.affectedFilesPendingEmitKind && new ts.Map(state.affectedFilesPendingEmitKind); + newState.affectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; + newState.seenEmittedFiles = state.seenEmittedFiles && new ts.Map(state.seenEmittedFiles); + newState.programEmitComplete = state.programEmitComplete; + return newState; +} - /** - * 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 - */ - function getNextAffectedFile(state: BuilderProgramState, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): ts.SourceFile | ts.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, host); - return affectedFile; - } - affectedFilesIndex++; - } +/** + * Verifies that source file is ok to be used in calls that arent handled by next + */ +function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: ts.SourceFile | undefined) { + ts.Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex! - 1] !== sourceFile || !state.semanticDiagnosticsPerFile!.has(sourceFile.resolvedPath)); +} - // Remove the changed file from the change set - state.changedFilesSet.delete(state.currentChangedFilePath!); - state.currentChangedFilePath = undefined; - // Commit the changes in file signature - ts.BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures!); - state.currentAffectedFilesSignatures!.clear(); - ts.BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); - state.currentAffectedFilesExportedModulesMap?.clear(); - state.affectedFiles = 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 + */ +function getNextAffectedFile(state: BuilderProgramState, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): ts.SourceFile | ts.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, host); + return affectedFile; + } + affectedFilesIndex++; } - // Get next changed file - const nextKey = state.changedFilesSet.keys().next(); - if (nextKey.done) { - // Done - return undefined; - } + // Remove the changed file from the change set + state.changedFilesSet.delete(state.currentChangedFilePath!); + state.currentChangedFilePath = undefined; + // Commit the changes in file signature + ts.BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures!); + state.currentAffectedFilesSignatures!.clear(); + ts.BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); + state.currentAffectedFilesExportedModulesMap?.clear(); + state.affectedFiles = undefined; + } - // With --out or --outFile all outputs go into single file - // so operations are performed directly on program, return program - const program = ts.Debug.checkDefined(state.program); - const compilerOptions = program.getCompilerOptions(); - if (ts.outFile(compilerOptions)) { - ts.Debug.assert(!state.semanticDiagnosticsPerFile); - return program; - } + // Get next changed file + const nextKey = state.changedFilesSet.keys().next(); + if (nextKey.done) { + // Done + return undefined; + } - // Get next batch of affected files - if (!state.currentAffectedFilesSignatures) - state.currentAffectedFilesSignatures = new ts.Map(); - if (state.exportedModulesMap) { - state.currentAffectedFilesExportedModulesMap ||= ts.BuilderState.createManyToManyPathMap(); - } - state.affectedFiles = ts.BuilderState.getFilesAffectedBy(state, program, nextKey.value, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); - state.currentChangedFilePath = nextKey.value; - state.affectedFilesIndex = 0; - if (!state.seenAffectedFiles) - state.seenAffectedFiles = new ts.Set(); + // With --out or --outFile all outputs go into single file + // so operations are performed directly on program, return program + const program = ts.Debug.checkDefined(state.program); + const compilerOptions = program.getCompilerOptions(); + if (ts.outFile(compilerOptions)) { + ts.Debug.assert(!state.semanticDiagnosticsPerFile); + return program; } - } - function clearAffectedFilesPendingEmit(state: BuilderProgramState) { - state.affectedFilesPendingEmit = undefined; - state.affectedFilesPendingEmitKind = undefined; - state.affectedFilesPendingEmitIndex = undefined; + // Get next batch of affected files + if (!state.currentAffectedFilesSignatures) + state.currentAffectedFilesSignatures = new ts.Map(); + if (state.exportedModulesMap) { + state.currentAffectedFilesExportedModulesMap ||= ts.BuilderState.createManyToManyPathMap(); + } + state.affectedFiles = ts.BuilderState.getFilesAffectedBy(state, program, nextKey.value, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); + state.currentChangedFilePath = nextKey.value; + state.affectedFilesIndex = 0; + if (!state.seenAffectedFiles) + state.seenAffectedFiles = new ts.Set(); } +} - /** - * Returns next file to be emitted from files that retrieved semantic diagnostics but did not emit yet - */ - function getNextAffectedFilePendingEmit(state: BuilderProgramState) { - const { affectedFilesPendingEmit } = state; - if (affectedFilesPendingEmit) { - const seenEmittedFiles = (state.seenEmittedFiles || (state.seenEmittedFiles = new ts.Map())); - for (let i = state.affectedFilesPendingEmitIndex!; i < affectedFilesPendingEmit.length; i++) { - const affectedFile = ts.Debug.checkDefined(state.program).getSourceFileByPath(affectedFilesPendingEmit[i]); - if (affectedFile) { - const seenKind = seenEmittedFiles.get(affectedFile.resolvedPath); - const emitKind = ts.Debug.checkDefined(ts.Debug.checkDefined(state.affectedFilesPendingEmitKind).get(affectedFile.resolvedPath)); - if (seenKind === undefined || seenKind < emitKind) { - // emit this file - state.affectedFilesPendingEmitIndex = i; - return { affectedFile, emitKind }; - } +function clearAffectedFilesPendingEmit(state: BuilderProgramState) { + state.affectedFilesPendingEmit = undefined; + state.affectedFilesPendingEmitKind = undefined; + state.affectedFilesPendingEmitIndex = undefined; +} + +/** + * Returns next file to be emitted from files that retrieved semantic diagnostics but did not emit yet + */ +function getNextAffectedFilePendingEmit(state: BuilderProgramState) { + const { affectedFilesPendingEmit } = state; + if (affectedFilesPendingEmit) { + const seenEmittedFiles = (state.seenEmittedFiles || (state.seenEmittedFiles = new ts.Map())); + for (let i = state.affectedFilesPendingEmitIndex!; i < affectedFilesPendingEmit.length; i++) { + const affectedFile = ts.Debug.checkDefined(state.program).getSourceFileByPath(affectedFilesPendingEmit[i]); + if (affectedFile) { + const seenKind = seenEmittedFiles.get(affectedFile.resolvedPath); + const emitKind = ts.Debug.checkDefined(ts.Debug.checkDefined(state.affectedFilesPendingEmitKind).get(affectedFile.resolvedPath)); + if (seenKind === undefined || seenKind < emitKind) { + // emit this file + state.affectedFilesPendingEmitIndex = i; + return { affectedFile, emitKind }; } } - clearAffectedFilesPendingEmit(state); } - return undefined; + clearAffectedFilesPendingEmit(state); } + return undefined; +} - function removeDiagnosticsOfLibraryFiles(state: BuilderProgramState) { - if (!state.cleanedDiagnosticsOfLibFiles) { - state.cleanedDiagnosticsOfLibFiles = true; - const program = ts.Debug.checkDefined(state.program); - const options = program.getCompilerOptions(); - ts.forEach(program.getSourceFiles(), f => program.isSourceFileDefaultLibrary(f) && - !ts.skipTypeChecking(f, options, program) && - removeSemanticDiagnosticsOf(state, f.resolvedPath)); - } +function removeDiagnosticsOfLibraryFiles(state: BuilderProgramState) { + if (!state.cleanedDiagnosticsOfLibFiles) { + state.cleanedDiagnosticsOfLibFiles = true; + const program = ts.Debug.checkDefined(state.program); + const options = program.getCompilerOptions(); + ts.forEach(program.getSourceFiles(), f => program.isSourceFileDefaultLibrary(f) && + !ts.skipTypeChecking(f, options, program) && + removeSemanticDiagnosticsOf(state, f.resolvedPath)); } +} - /** - * 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 - */ - function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost) { - removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath); - - // If affected files is everything except default library, then nothing more to do - if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) { - removeDiagnosticsOfLibraryFiles(state); - // When a change affects the global scope, all files are considered to be affected without updating their signature - // That means when affected file is handled, its signature can be out of date - // To avoid this, ensure that we update the signature for any affected file in this scenario. - ts.BuilderState.updateShapeSignature(state, ts.Debug.checkDefined(state.program), affectedFile, ts.Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap); - return; - } - ts.Debug.assert(state.hasCalledUpdateShapeSignature.has(affectedFile.resolvedPath) || state.currentAffectedFilesSignatures?.has(affectedFile.resolvedPath), `Signature not updated for affected file: ${affectedFile.fileName}`); - if (state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) - return; - handleDtsMayChangeOfReferencingExportOfAffectedFile(state, affectedFile, cancellationToken, computeHash, host); +/** + * 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 + */ +function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost) { + removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath); + + // If affected files is everything except default library, then nothing more to do + if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) { + removeDiagnosticsOfLibraryFiles(state); + // When a change affects the global scope, all files are considered to be affected without updating their signature + // That means when affected file is handled, its signature can be out of date + // To avoid this, ensure that we update the signature for any affected file in this scenario. + ts.BuilderState.updateShapeSignature(state, ts.Debug.checkDefined(state.program), affectedFile, ts.Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap); + return; } + ts.Debug.assert(state.hasCalledUpdateShapeSignature.has(affectedFile.resolvedPath) || state.currentAffectedFilesSignatures?.has(affectedFile.resolvedPath), `Signature not updated for affected file: ${affectedFile.fileName}`); + if (state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) + return; + handleDtsMayChangeOfReferencingExportOfAffectedFile(state, affectedFile, cancellationToken, computeHash, host); +} - /** - * 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 - */ - function handleDtsMayChangeOf(state: BuilderProgramState, path: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): void { - removeSemanticDiagnosticsOf(state, path); - - if (!state.changedFilesSet.has(path)) { - const program = ts.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 - // But we avoid expensive full shape computation, as using file version as shape is enough for correctness. - ts.BuilderState.updateShapeSignature(state, program, sourceFile, ts.Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap, !host.disableUseFileVersionAsSignature); - // If not dts emit, nothing more to do - if (ts.getEmitDeclarations(state.compilerOptions)) { - addToAffectedFilesPendingEmit(state, path, BuilderFileEmit.DtsOnly); - } +/** + * 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 + */ +function handleDtsMayChangeOf(state: BuilderProgramState, path: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): void { + removeSemanticDiagnosticsOf(state, path); + + if (!state.changedFilesSet.has(path)) { + const program = ts.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 + // But we avoid expensive full shape computation, as using file version as shape is enough for correctness. + ts.BuilderState.updateShapeSignature(state, program, sourceFile, ts.Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap, !host.disableUseFileVersionAsSignature); + // If not dts emit, nothing more to do + if (ts.getEmitDeclarations(state.compilerOptions)) { + addToAffectedFilesPendingEmit(state, path, BuilderFileEmit.DtsOnly); } } } +} - /** - * Removes semantic diagnostics for path and - * returns true if there are no more semantic diagnostics from the old state - */ - function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: ts.Path) { - if (!state.semanticDiagnosticsFromOldState) { - return true; - } - state.semanticDiagnosticsFromOldState.delete(path); - state.semanticDiagnosticsPerFile!.delete(path); - return !state.semanticDiagnosticsFromOldState.size; +/** + * Removes semantic diagnostics for path and + * returns true if there are no more semantic diagnostics from the old state + */ +function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: ts.Path) { + if (!state.semanticDiagnosticsFromOldState) { + return true; } + state.semanticDiagnosticsFromOldState.delete(path); + state.semanticDiagnosticsPerFile!.delete(path); + return !state.semanticDiagnosticsFromOldState.size; +} - function isChangedSignature(state: BuilderProgramState, path: ts.Path) { - const newSignature = ts.Debug.checkDefined(state.currentAffectedFilesSignatures).get(path); - const oldSignature = ts.Debug.checkDefined(state.fileInfos.get(path)).signature; - return newSignature !== oldSignature; - } +function isChangedSignature(state: BuilderProgramState, path: ts.Path) { + const newSignature = ts.Debug.checkDefined(state.currentAffectedFilesSignatures).get(path); + const oldSignature = ts.Debug.checkDefined(state.fileInfos.get(path)).signature; + return newSignature !== oldSignature; +} - function forEachKeyOfExportedModulesMap(state: BuilderProgramState, filePath: ts.Path, fn: (exportedFromPath: ts.Path) => T | undefined): T | undefined { - // Go through exported modules from cache first - let keys = state.currentAffectedFilesExportedModulesMap!.getKeys(filePath); - const result = keys && ts.forEachKey(keys, fn); - if (result) - return result; +function forEachKeyOfExportedModulesMap(state: BuilderProgramState, filePath: ts.Path, fn: (exportedFromPath: ts.Path) => T | undefined): T | undefined { + // Go through exported modules from cache first + let keys = state.currentAffectedFilesExportedModulesMap!.getKeys(filePath); + const result = keys && ts.forEachKey(keys, fn); + if (result) + return result; - // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected - keys = state.exportedModulesMap!.getKeys(filePath); - return keys && ts.forEachKey(keys, exportedFromPath => - // If the cache had an updated value, skip - !state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) && - !state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) ? - fn(exportedFromPath) : - undefined); - } - function handleDtsMayChangeOfGlobalScope(state: BuilderProgramState, filePath: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): boolean { - if (!state.fileInfos.get(filePath)?.affectsGlobalScope) - return false; - // Every file needs to be handled - ts.BuilderState.getAllFilesExcludingDefaultLibraryFile(state, state.program!, /*firstSourceFile*/ undefined) - .forEach(file => handleDtsMayChangeOf(state, file.resolvedPath, cancellationToken, computeHash, host)); - removeDiagnosticsOfLibraryFiles(state); - return true; - } + // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected + keys = state.exportedModulesMap!.getKeys(filePath); + return keys && ts.forEachKey(keys, exportedFromPath => + // If the cache had an updated value, skip + !state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) && + !state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) ? + fn(exportedFromPath) : + undefined); +} +function handleDtsMayChangeOfGlobalScope(state: BuilderProgramState, filePath: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): boolean { + if (!state.fileInfos.get(filePath)?.affectsGlobalScope) + return false; + // Every file needs to be handled + ts.BuilderState.getAllFilesExcludingDefaultLibraryFile(state, state.program!, /*firstSourceFile*/ undefined) + .forEach(file => handleDtsMayChangeOf(state, file.resolvedPath, cancellationToken, computeHash, host)); + removeDiagnosticsOfLibraryFiles(state); + return true; +} - /** - * Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit - */ - function handleDtsMayChangeOfReferencingExportOfAffectedFile(state: BuilderProgramState, affectedFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost) { - // 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 (!isChangedSignature(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 = new ts.Map(); - seenFileNamesMap.set(affectedFile.resolvedPath, true); - const queue = ts.BuilderState.getReferencedByPaths(state, affectedFile.resolvedPath); - while (queue.length > 0) { - const currentPath = queue.pop()!; - if (!seenFileNamesMap.has(currentPath)) { - seenFileNamesMap.set(currentPath, true); - if (handleDtsMayChangeOfGlobalScope(state, currentPath, cancellationToken, computeHash, host)) - return; - handleDtsMayChangeOf(state, currentPath, cancellationToken, computeHash, host); - if (isChangedSignature(state, currentPath)) { - const currentSourceFile = ts.Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!; - queue.push(...ts.BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath)); - } +/** + * Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit + */ +function handleDtsMayChangeOfReferencingExportOfAffectedFile(state: BuilderProgramState, affectedFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost) { + // 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 (!isChangedSignature(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 = new ts.Map(); + seenFileNamesMap.set(affectedFile.resolvedPath, true); + const queue = ts.BuilderState.getReferencedByPaths(state, affectedFile.resolvedPath); + while (queue.length > 0) { + const currentPath = queue.pop()!; + if (!seenFileNamesMap.has(currentPath)) { + seenFileNamesMap.set(currentPath, true); + if (handleDtsMayChangeOfGlobalScope(state, currentPath, cancellationToken, computeHash, host)) + return; + handleDtsMayChangeOf(state, currentPath, cancellationToken, computeHash, host); + if (isChangedSignature(state, currentPath)) { + const currentSourceFile = ts.Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!; + queue.push(...ts.BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath)); } } } - - ts.Debug.assert(!!state.currentAffectedFilesExportedModulesMap); - const seenFileAndExportsOfFile = new ts.Set(); - // Go through exported modules from cache first - // If exported modules has path, all files referencing file exported from are affected - forEachKeyOfExportedModulesMap(state, affectedFile.resolvedPath, exportedFromPath => { - if (handleDtsMayChangeOfGlobalScope(state, exportedFromPath, cancellationToken, computeHash, host)) - return true; - const references = state.referencedMap!.getKeys(exportedFromPath); - return references && ts.forEachKey(references, filePath => handleDtsMayChangeOfFileAndExportsOfFile(state, filePath, seenFileAndExportsOfFile, cancellationToken, computeHash, host)); - }); } - /** - * handle dts and semantic diagnostics on file and iterate on anything that exports this file - * return true when all work is done and we can exit handling dts emit and semantic diagnostics - */ - function handleDtsMayChangeOfFileAndExportsOfFile(state: BuilderProgramState, filePath: ts.Path, seenFileAndExportsOfFile: ts.Set, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): boolean | undefined { - if (!ts.tryAddToSet(seenFileAndExportsOfFile, filePath)) - return undefined; - if (handleDtsMayChangeOfGlobalScope(state, filePath, cancellationToken, computeHash, host)) + ts.Debug.assert(!!state.currentAffectedFilesExportedModulesMap); + const seenFileAndExportsOfFile = new ts.Set(); + // Go through exported modules from cache first + // If exported modules has path, all files referencing file exported from are affected + forEachKeyOfExportedModulesMap(state, affectedFile.resolvedPath, exportedFromPath => { + if (handleDtsMayChangeOfGlobalScope(state, exportedFromPath, cancellationToken, computeHash, host)) return true; - handleDtsMayChangeOf(state, filePath, cancellationToken, computeHash, host); - ts.Debug.assert(!!state.currentAffectedFilesExportedModulesMap); - - // If exported modules has path, all files referencing file exported from are affected - forEachKeyOfExportedModulesMap(state, filePath, exportedFromPath => handleDtsMayChangeOfFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, cancellationToken, computeHash, host)); + const references = state.referencedMap!.getKeys(exportedFromPath); + return references && ts.forEachKey(references, filePath => handleDtsMayChangeOfFileAndExportsOfFile(state, filePath, seenFileAndExportsOfFile, cancellationToken, computeHash, host)); + }); +} - // Remove diagnostics of files that import this file (without going to exports of referencing files) - state.referencedMap!.getKeys(filePath)?.forEach(referencingFilePath => !seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file - handleDtsMayChangeOf( // Dont add to seen since this is not yet done with the export removal - state, referencingFilePath, cancellationToken, computeHash, host)); +/** + * handle dts and semantic diagnostics on file and iterate on anything that exports this file + * return true when all work is done and we can exit handling dts emit and semantic diagnostics + */ +function handleDtsMayChangeOfFileAndExportsOfFile(state: BuilderProgramState, filePath: ts.Path, seenFileAndExportsOfFile: ts.Set, cancellationToken: ts.CancellationToken | undefined, computeHash: ts.BuilderState.ComputeHash, host: ts.BuilderProgramHost): boolean | undefined { + if (!ts.tryAddToSet(seenFileAndExportsOfFile, filePath)) return undefined; - } + if (handleDtsMayChangeOfGlobalScope(state, filePath, cancellationToken, computeHash, host)) + return true; + handleDtsMayChangeOf(state, filePath, cancellationToken, computeHash, host); + ts.Debug.assert(!!state.currentAffectedFilesExportedModulesMap); - /** - * 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: ts.SourceFile | ts.Program, emitKind?: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean) { - if (isBuildInfoEmit) { - state.buildInfoEmitPending = false; + // If exported modules has path, all files referencing file exported from are affected + forEachKeyOfExportedModulesMap(state, filePath, exportedFromPath => handleDtsMayChangeOfFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, cancellationToken, computeHash, host)); + + // Remove diagnostics of files that import this file (without going to exports of referencing files) + state.referencedMap!.getKeys(filePath)?.forEach(referencingFilePath => !seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file + handleDtsMayChangeOf( // Dont add to seen since this is not yet done with the export removal + state, referencingFilePath, cancellationToken, computeHash, host)); + return undefined; +} + +/** + * 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: ts.SourceFile | ts.Program, emitKind?: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean) { + if (isBuildInfoEmit) { + state.buildInfoEmitPending = false; + } + else if (affected === state.program) { + state.changedFilesSet.clear(); + state.programEmitComplete = true; + } + else { + state.seenAffectedFiles!.add((affected as ts.SourceFile).resolvedPath); + if (emitKind !== undefined) { + (state.seenEmittedFiles || (state.seenEmittedFiles = new ts.Map())).set((affected as ts.SourceFile).resolvedPath, emitKind); } - else if (affected === state.program) { - state.changedFilesSet.clear(); - state.programEmitComplete = true; + if (isPendingEmit) { + state.affectedFilesPendingEmitIndex!++; + state.buildInfoEmitPending = true; } else { - state.seenAffectedFiles!.add((affected as ts.SourceFile).resolvedPath); - if (emitKind !== undefined) { - (state.seenEmittedFiles || (state.seenEmittedFiles = new ts.Map())).set((affected as ts.SourceFile).resolvedPath, emitKind); - } - if (isPendingEmit) { - state.affectedFilesPendingEmitIndex!++; - state.buildInfoEmitPending = true; - } - else { - state.affectedFilesIndex!++; - } + state.affectedFilesIndex!++; } } +} - /** - * Returns the result with affected file - */ - function toAffectedFileResult(state: BuilderProgramState, result: T, affected: ts.SourceFile | ts.Program): ts.AffectedFileResult { - doneWithAffectedFile(state, affected); - return { result, affected }; - } - - /** - * Returns the result with affected file - */ - function toAffectedFileEmitResult(state: BuilderProgramState, result: ts.EmitResult, affected: ts.SourceFile | ts.Program, emitKind: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean): ts.AffectedFileResult { - doneWithAffectedFile(state, affected, emitKind, isPendingEmit, isBuildInfoEmit); - return { result, affected }; - } +/** + * Returns the result with affected file + */ +function toAffectedFileResult(state: BuilderProgramState, result: T, affected: ts.SourceFile | ts.Program): ts.AffectedFileResult { + doneWithAffectedFile(state, affected); + return { result, affected }; +} - /** - * Gets semantic diagnostics for the file which are - * bindAndCheckDiagnostics (from cache) and program diagnostics - */ - function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { - return ts.concatenate(getBinderAndCheckerDiagnosticsOfFile(state, sourceFile, cancellationToken), ts.Debug.checkDefined(state.program).getProgramDiagnostics(sourceFile)); - } +/** + * Returns the result with affected file + */ +function toAffectedFileEmitResult(state: BuilderProgramState, result: ts.EmitResult, affected: ts.SourceFile | ts.Program, emitKind: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean): ts.AffectedFileResult { + doneWithAffectedFile(state, affected, emitKind, isPendingEmit, isBuildInfoEmit); + return { result, affected }; +} - /** - * 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: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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 ts.filterSemanticDiagnostics(cachedDiagnostics, state.compilerOptions); - } - } +/** + * Gets semantic diagnostics for the file which are + * bindAndCheckDiagnostics (from cache) and program diagnostics + */ +function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { + return ts.concatenate(getBinderAndCheckerDiagnosticsOfFile(state, sourceFile, cancellationToken), ts.Debug.checkDefined(state.program).getProgramDiagnostics(sourceFile)); +} - // Diagnostics werent cached, get them from program, and cache the result - const diagnostics = ts.Debug.checkDefined(state.program).getBindAndCheckDiagnostics(sourceFile, cancellationToken); - if (state.semanticDiagnosticsPerFile) { - state.semanticDiagnosticsPerFile.set(path, diagnostics); +/** + * 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: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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 ts.filterSemanticDiagnostics(cachedDiagnostics, state.compilerOptions); } - return ts.filterSemanticDiagnostics(diagnostics, state.compilerOptions); } - export type ProgramBuildInfoFileId = number & { - __programBuildInfoFileIdBrand: any; - }; - export type ProgramBuildInfoFileIdListId = number & { - __programBuildInfoFileIdListIdBrand: any; - }; - export type ProgramBuildInfoDiagnostic = ProgramBuildInfoFileId | [ - fileId: ProgramBuildInfoFileId, - diagnostics: readonly ReusableDiagnostic[] - ]; - export type ProgramBuilderInfoFilePendingEmit = [ - fileId: ProgramBuildInfoFileId, - emitKind: BuilderFileEmit - ]; - export type ProgramBuildInfoReferencedMap = [ - fileId: ProgramBuildInfoFileId, - fileIdListId: ProgramBuildInfoFileIdListId - ][]; - export type ProgramBuildInfoBuilderStateFileInfo = Omit & { - /** - * Signature is - * - undefined if FileInfo.version === FileInfo.signature - * - false if FileInfo has signature as undefined (not calculated) - * - string actual signature - */ - signature: string | false | undefined; - }; - /** - * ProgramBuildInfoFileInfo is string if FileInfo.version === FileInfo.signature && !FileInfo.affectsGlobalScope otherwise encoded FileInfo - */ - export type ProgramBuildInfoFileInfo = string | ProgramBuildInfoBuilderStateFileInfo; - export interface ProgramBuildInfo { - fileNames: readonly string[]; - fileInfos: readonly ProgramBuildInfoFileInfo[]; - options: ts.CompilerOptions | undefined; - fileIdsList?: readonly (readonly ProgramBuildInfoFileId[])[]; - referencedMap?: ProgramBuildInfoReferencedMap; - exportedModulesMap?: ProgramBuildInfoReferencedMap; - semanticDiagnosticsPerFile?: ProgramBuildInfoDiagnostic[]; - affectedFilesPendingEmit?: ProgramBuilderInfoFilePendingEmit[]; + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = ts.Debug.checkDefined(state.program).getBindAndCheckDiagnostics(sourceFile, cancellationToken); + if (state.semanticDiagnosticsPerFile) { + state.semanticDiagnosticsPerFile.set(path, diagnostics); } + return ts.filterSemanticDiagnostics(diagnostics, state.compilerOptions); +} +export type ProgramBuildInfoFileId = number & { + __programBuildInfoFileIdBrand: any; +}; +export type ProgramBuildInfoFileIdListId = number & { + __programBuildInfoFileIdListIdBrand: any; +}; +export type ProgramBuildInfoDiagnostic = ProgramBuildInfoFileId | [ + fileId: ProgramBuildInfoFileId, + diagnostics: readonly ReusableDiagnostic[] +]; +export type ProgramBuilderInfoFilePendingEmit = [ + fileId: ProgramBuildInfoFileId, + emitKind: BuilderFileEmit +]; +export type ProgramBuildInfoReferencedMap = [ + fileId: ProgramBuildInfoFileId, + fileIdListId: ProgramBuildInfoFileIdListId +][]; +export type ProgramBuildInfoBuilderStateFileInfo = Omit & { /** - * Gets the program information to be emitted in buildInfo so that we can use it to create new program + * Signature is + * - undefined if FileInfo.version === FileInfo.signature + * - false if FileInfo has signature as undefined (not calculated) + * - string actual signature */ - function getProgramBuildInfo(state: Readonly, getCanonicalFileName: ts.GetCanonicalFileName): ProgramBuildInfo | undefined { - if (ts.outFile(state.compilerOptions)) - return undefined; - const currentDirectory = ts.Debug.checkDefined(state.program).getCurrentDirectory(); - const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(ts.getTsBuildInfoEmitOutputFilePath(state.compilerOptions)!, currentDirectory)); - const fileNames: string[] = []; - const fileNameToFileId = new ts.Map(); - let fileIdsList: (readonly ProgramBuildInfoFileId[])[] | undefined; - let fileNamesToFileIdListId: ts.ESMap | undefined; - const fileInfos = ts.arrayFrom(state.fileInfos.entries(), ([key, value]): ProgramBuildInfoFileInfo => { - // Ensure fileId - const fileId = toFileId(key); - ts.Debug.assert(fileNames[fileId - 1] === relativeToBuildInfo(key)); - const signature = state.currentAffectedFilesSignatures && state.currentAffectedFilesSignatures.get(key); - const actualSignature = signature ?? value.signature; - return value.version === actualSignature ? - value.affectsGlobalScope || value.impliedFormat ? - // If file version is same as signature, dont serialize signature - { version: value.version, signature: undefined, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : - // If file info only contains version and signature and both are same we can just write string - value.version : - actualSignature !== undefined ? // If signature is not same as version, encode signature in the fileInfo - signature === undefined ? - // If we havent computed signature, use fileInfo as is - value : - // Serialize fileInfo with new updated signature - { version: value.version, signature, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : - // Signature of the FileInfo is undefined, serialize it as false - { version: value.version, signature: false, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat }; - }); + signature: string | false | undefined; +}; +/** + * ProgramBuildInfoFileInfo is string if FileInfo.version === FileInfo.signature && !FileInfo.affectsGlobalScope otherwise encoded FileInfo + */ +export type ProgramBuildInfoFileInfo = string | ProgramBuildInfoBuilderStateFileInfo; +export interface ProgramBuildInfo { + fileNames: readonly string[]; + fileInfos: readonly ProgramBuildInfoFileInfo[]; + options: ts.CompilerOptions | undefined; + fileIdsList?: readonly (readonly ProgramBuildInfoFileId[])[]; + referencedMap?: ProgramBuildInfoReferencedMap; + exportedModulesMap?: ProgramBuildInfoReferencedMap; + semanticDiagnosticsPerFile?: ProgramBuildInfoDiagnostic[]; + affectedFilesPendingEmit?: ProgramBuilderInfoFilePendingEmit[]; +} - let referencedMap: ProgramBuildInfoReferencedMap | undefined; - if (state.referencedMap) { - referencedMap = ts.arrayFrom(state.referencedMap.keys()).sort(ts.compareStringsCaseSensitive).map(key => [ - toFileId(key), - toFileIdListId(state.referencedMap!.getValues(key)!) - ]); - } +/** + * Gets the program information to be emitted in buildInfo so that we can use it to create new program + */ +function getProgramBuildInfo(state: Readonly, getCanonicalFileName: ts.GetCanonicalFileName): ProgramBuildInfo | undefined { + if (ts.outFile(state.compilerOptions)) + return undefined; + const currentDirectory = ts.Debug.checkDefined(state.program).getCurrentDirectory(); + const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(ts.getTsBuildInfoEmitOutputFilePath(state.compilerOptions)!, currentDirectory)); + const fileNames: string[] = []; + const fileNameToFileId = new ts.Map(); + let fileIdsList: (readonly ProgramBuildInfoFileId[])[] | undefined; + let fileNamesToFileIdListId: ts.ESMap | undefined; + const fileInfos = ts.arrayFrom(state.fileInfos.entries(), ([key, value]): ProgramBuildInfoFileInfo => { + // Ensure fileId + const fileId = toFileId(key); + ts.Debug.assert(fileNames[fileId - 1] === relativeToBuildInfo(key)); + const signature = state.currentAffectedFilesSignatures && state.currentAffectedFilesSignatures.get(key); + const actualSignature = signature ?? value.signature; + return value.version === actualSignature ? + value.affectsGlobalScope || value.impliedFormat ? + // If file version is same as signature, dont serialize signature + { version: value.version, signature: undefined, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : + // If file info only contains version and signature and both are same we can just write string + value.version : + actualSignature !== undefined ? // If signature is not same as version, encode signature in the fileInfo + signature === undefined ? + // If we havent computed signature, use fileInfo as is + value : + // Serialize fileInfo with new updated signature + { version: value.version, signature, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : + // Signature of the FileInfo is undefined, serialize it as false + { version: value.version, signature: false, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat }; + }); + + let referencedMap: ProgramBuildInfoReferencedMap | undefined; + if (state.referencedMap) { + referencedMap = ts.arrayFrom(state.referencedMap.keys()).sort(ts.compareStringsCaseSensitive).map(key => [ + toFileId(key), + toFileIdListId(state.referencedMap!.getValues(key)!) + ]); + } - let exportedModulesMap: ProgramBuildInfoReferencedMap | undefined; - if (state.exportedModulesMap) { - exportedModulesMap = ts.mapDefined(ts.arrayFrom(state.exportedModulesMap.keys()).sort(ts.compareStringsCaseSensitive), key => { - if (state.currentAffectedFilesExportedModulesMap) { - if (state.currentAffectedFilesExportedModulesMap.deletedKeys()?.has(key)) { - return undefined; - } + let exportedModulesMap: ProgramBuildInfoReferencedMap | undefined; + if (state.exportedModulesMap) { + exportedModulesMap = ts.mapDefined(ts.arrayFrom(state.exportedModulesMap.keys()).sort(ts.compareStringsCaseSensitive), key => { + if (state.currentAffectedFilesExportedModulesMap) { + if (state.currentAffectedFilesExportedModulesMap.deletedKeys()?.has(key)) { + return undefined; + } - const newValue = state.currentAffectedFilesExportedModulesMap.getValues(key); - if (newValue) { - return [toFileId(key), toFileIdListId(newValue)]; - } + const newValue = state.currentAffectedFilesExportedModulesMap.getValues(key); + if (newValue) { + return [toFileId(key), toFileIdListId(newValue)]; } + } - // Not in temporary cache, use existing value - return [toFileId(key), toFileIdListId(state.exportedModulesMap!.getValues(key)!)]; - }); - } + // Not in temporary cache, use existing value + return [toFileId(key), toFileIdListId(state.exportedModulesMap!.getValues(key)!)]; + }); + } - let semanticDiagnosticsPerFile: ProgramBuildInfoDiagnostic[] | undefined; - if (state.semanticDiagnosticsPerFile) { - for (const key of ts.arrayFrom(state.semanticDiagnosticsPerFile.keys()).sort(ts.compareStringsCaseSensitive)) { - const value = state.semanticDiagnosticsPerFile.get(key)!; - (semanticDiagnosticsPerFile ||= []).push(value.length ? - [ - toFileId(key), - state.hasReusableDiagnostic ? - value as readonly ReusableDiagnostic[] : - convertToReusableDiagnostics(value as readonly ts.Diagnostic[], relativeToBuildInfo) - ] : - toFileId(key)); - } + let semanticDiagnosticsPerFile: ProgramBuildInfoDiagnostic[] | undefined; + if (state.semanticDiagnosticsPerFile) { + for (const key of ts.arrayFrom(state.semanticDiagnosticsPerFile.keys()).sort(ts.compareStringsCaseSensitive)) { + const value = state.semanticDiagnosticsPerFile.get(key)!; + (semanticDiagnosticsPerFile ||= []).push(value.length ? + [ + toFileId(key), + state.hasReusableDiagnostic ? + value as readonly ReusableDiagnostic[] : + convertToReusableDiagnostics(value as readonly ts.Diagnostic[], relativeToBuildInfo) + ] : + toFileId(key)); } + } - let affectedFilesPendingEmit: ProgramBuilderInfoFilePendingEmit[] | undefined; - if (state.affectedFilesPendingEmit) { - const seenFiles = new ts.Set(); - for (const path of state.affectedFilesPendingEmit.slice(state.affectedFilesPendingEmitIndex).sort(ts.compareStringsCaseSensitive)) { - if (ts.tryAddToSet(seenFiles, path)) { - (affectedFilesPendingEmit ||= []).push([toFileId(path), state.affectedFilesPendingEmitKind!.get(path)!]); - } + let affectedFilesPendingEmit: ProgramBuilderInfoFilePendingEmit[] | undefined; + if (state.affectedFilesPendingEmit) { + const seenFiles = new ts.Set(); + for (const path of state.affectedFilesPendingEmit.slice(state.affectedFilesPendingEmitIndex).sort(ts.compareStringsCaseSensitive)) { + if (ts.tryAddToSet(seenFiles, path)) { + (affectedFilesPendingEmit ||= []).push([toFileId(path), state.affectedFilesPendingEmitKind!.get(path)!]); } } + } - return { - fileNames, - fileInfos, - options: convertToProgramBuildInfoCompilerOptions(state.compilerOptions, relativeToBuildInfoEnsuringAbsolutePath), - fileIdsList, - referencedMap, - exportedModulesMap, - semanticDiagnosticsPerFile, - affectedFilesPendingEmit, - }; + return { + fileNames, + fileInfos, + options: convertToProgramBuildInfoCompilerOptions(state.compilerOptions, relativeToBuildInfoEnsuringAbsolutePath), + fileIdsList, + referencedMap, + exportedModulesMap, + semanticDiagnosticsPerFile, + affectedFilesPendingEmit, + }; - function relativeToBuildInfoEnsuringAbsolutePath(path: string) { - return relativeToBuildInfo(ts.getNormalizedAbsolutePath(path, currentDirectory)); - } + function relativeToBuildInfoEnsuringAbsolutePath(path: string) { + return relativeToBuildInfo(ts.getNormalizedAbsolutePath(path, currentDirectory)); + } - function relativeToBuildInfo(path: string) { - return ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(buildInfoDirectory, path, getCanonicalFileName)); - } + function relativeToBuildInfo(path: string) { + return ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(buildInfoDirectory, path, getCanonicalFileName)); + } - function toFileId(path: ts.Path): ProgramBuildInfoFileId { - let fileId = fileNameToFileId.get(path); - if (fileId === undefined) { - fileNames.push(relativeToBuildInfo(path)); - fileNameToFileId.set(path, fileId = fileNames.length as ProgramBuildInfoFileId); - } - return fileId; + function toFileId(path: ts.Path): ProgramBuildInfoFileId { + let fileId = fileNameToFileId.get(path); + if (fileId === undefined) { + fileNames.push(relativeToBuildInfo(path)); + fileNameToFileId.set(path, fileId = fileNames.length as ProgramBuildInfoFileId); } + return fileId; + } - function toFileIdListId(set: ts.ReadonlySet): ProgramBuildInfoFileIdListId { - const fileIds = ts.arrayFrom(set.keys(), toFileId).sort(ts.compareValues); - const key = fileIds.join(); - let fileIdListId = fileNamesToFileIdListId?.get(key); - if (fileIdListId === undefined) { - (fileIdsList ||= []).push(fileIds); - (fileNamesToFileIdListId ||= new ts.Map()).set(key, fileIdListId = fileIdsList.length as ProgramBuildInfoFileIdListId); - } - return fileIdListId; + function toFileIdListId(set: ts.ReadonlySet): ProgramBuildInfoFileIdListId { + const fileIds = ts.arrayFrom(set.keys(), toFileId).sort(ts.compareValues); + const key = fileIds.join(); + let fileIdListId = fileNamesToFileIdListId?.get(key); + if (fileIdListId === undefined) { + (fileIdsList ||= []).push(fileIds); + (fileNamesToFileIdListId ||= new ts.Map()).set(key, fileIdListId = fileIdsList.length as ProgramBuildInfoFileIdListId); } + return fileIdListId; } +} - function convertToProgramBuildInfoCompilerOptions(options: ts.CompilerOptions, relativeToBuildInfo: (path: string) => string) { - let result: ts.CompilerOptions | undefined; - const { optionsNameMap } = ts.getOptionsNameMap(); - for (const name of ts.getOwnKeys(options).sort(ts.compareStringsCaseSensitive)) { - const optionKey = name.toLowerCase(); - const optionInfo = optionsNameMap.get(optionKey); - if (optionInfo?.affectsEmit || optionInfo?.affectsSemanticDiagnostics || - // We need to store `strict`, even though it won't be examined directly, so that the - // flags it controls (e.g. `strictNullChecks`) will be retrieved correctly from the buildinfo - optionKey === "strict" || - // We need to store these to determine whether `lib` files need to be rechecked. - optionKey === "skiplibcheck" || optionKey === "skipdefaultlibcheck") { - (result ||= {})[name] = convertToReusableCompilerOptionValue(optionInfo, options[name] as ts.CompilerOptionsValue, relativeToBuildInfo); - } +function convertToProgramBuildInfoCompilerOptions(options: ts.CompilerOptions, relativeToBuildInfo: (path: string) => string) { + let result: ts.CompilerOptions | undefined; + const { optionsNameMap } = ts.getOptionsNameMap(); + for (const name of ts.getOwnKeys(options).sort(ts.compareStringsCaseSensitive)) { + const optionKey = name.toLowerCase(); + const optionInfo = optionsNameMap.get(optionKey); + if (optionInfo?.affectsEmit || optionInfo?.affectsSemanticDiagnostics || + // We need to store `strict`, even though it won't be examined directly, so that the + // flags it controls (e.g. `strictNullChecks`) will be retrieved correctly from the buildinfo + optionKey === "strict" || + // We need to store these to determine whether `lib` files need to be rechecked. + optionKey === "skiplibcheck" || optionKey === "skipdefaultlibcheck") { + (result ||= {})[name] = convertToReusableCompilerOptionValue(optionInfo, options[name] as ts.CompilerOptionsValue, relativeToBuildInfo); } - return result; } + return result; +} - function convertToReusableCompilerOptionValue(option: ts.CommandLineOption | undefined, value: ts.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 if (option.isFilePath) { - return relativeToBuildInfo(value as string); +function convertToReusableCompilerOptionValue(option: ts.CommandLineOption | undefined, value: ts.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 value; + else if (option.isFilePath) { + return relativeToBuildInfo(value as string); + } } + return value; +} - function convertToReusableDiagnostics(diagnostics: readonly ts.Diagnostic[], relativeToBuildInfo: (path: string) => string): readonly ReusableDiagnostic[] { - ts.Debug.assert(!!diagnostics.length); - return diagnostics.map(diagnostic => { - const result: ReusableDiagnostic = convertToReusableDiagnosticRelatedInformation(diagnostic, relativeToBuildInfo); - result.reportsUnnecessary = diagnostic.reportsUnnecessary; - result.reportDeprecated = diagnostic.reportsDeprecated; - result.source = diagnostic.source; - result.skippedOn = diagnostic.skippedOn; - const { relatedInformation } = diagnostic; - result.relatedInformation = relatedInformation ? - relatedInformation.length ? - relatedInformation.map(r => convertToReusableDiagnosticRelatedInformation(r, relativeToBuildInfo)) : - [] : - undefined; - return result; - }); - } +function convertToReusableDiagnostics(diagnostics: readonly ts.Diagnostic[], relativeToBuildInfo: (path: string) => string): readonly ReusableDiagnostic[] { + ts.Debug.assert(!!diagnostics.length); + return diagnostics.map(diagnostic => { + const result: ReusableDiagnostic = convertToReusableDiagnosticRelatedInformation(diagnostic, relativeToBuildInfo); + result.reportsUnnecessary = diagnostic.reportsUnnecessary; + result.reportDeprecated = diagnostic.reportsDeprecated; + result.source = diagnostic.source; + result.skippedOn = diagnostic.skippedOn; + const { relatedInformation } = diagnostic; + result.relatedInformation = relatedInformation ? + relatedInformation.length ? + relatedInformation.map(r => convertToReusableDiagnosticRelatedInformation(r, relativeToBuildInfo)) : + [] : + undefined; + return result; + }); +} - function convertToReusableDiagnosticRelatedInformation(diagnostic: ts.DiagnosticRelatedInformation, relativeToBuildInfo: (path: string) => string): ReusableDiagnosticRelatedInformation { - const { file } = diagnostic; - return { - ...diagnostic, - file: file ? relativeToBuildInfo(file.resolvedPath) : undefined - }; - } +function convertToReusableDiagnosticRelatedInformation(diagnostic: ts.DiagnosticRelatedInformation, relativeToBuildInfo: (path: string) => string): ReusableDiagnosticRelatedInformation { + const { file } = diagnostic; + return { + ...diagnostic, + file: file ? relativeToBuildInfo(file.resolvedPath) : undefined + }; +} - export enum BuilderProgramKind { - SemanticDiagnosticsBuilderProgram, - EmitAndSemanticDiagnosticsBuilderProgram - } +export enum BuilderProgramKind { + SemanticDiagnosticsBuilderProgram, + EmitAndSemanticDiagnosticsBuilderProgram +} - export interface BuilderCreationParameters { - newProgram: ts.Program; - host: ts.BuilderProgramHost; - oldProgram: ts.BuilderProgram | undefined; - configFileParsingDiagnostics: readonly ts.Diagnostic[]; +export interface BuilderCreationParameters { + newProgram: ts.Program; + host: ts.BuilderProgramHost; + oldProgram: ts.BuilderProgram | undefined; + configFileParsingDiagnostics: readonly ts.Diagnostic[]; +} +export function getBuilderCreationParameters(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: ts.BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.BuilderProgram | ts.CompilerHost, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | ts.BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderCreationParameters { + let host: ts.BuilderProgramHost; + let newProgram: ts.Program; + let oldProgram: ts.BuilderProgram; + if (newProgramOrRootNames === undefined) { + ts.Debug.assert(hostOrOptions === undefined); + host = oldProgramOrHost as ts.CompilerHost; + oldProgram = configFileParsingDiagnosticsOrOldProgram as ts.BuilderProgram; + ts.Debug.assert(!!oldProgram); + newProgram = oldProgram.getProgram(); } - export function getBuilderCreationParameters(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: ts.BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.BuilderProgram | ts.CompilerHost, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | ts.BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderCreationParameters { - let host: ts.BuilderProgramHost; - let newProgram: ts.Program; - let oldProgram: ts.BuilderProgram; - if (newProgramOrRootNames === undefined) { - ts.Debug.assert(hostOrOptions === undefined); - host = oldProgramOrHost as ts.CompilerHost; - oldProgram = configFileParsingDiagnosticsOrOldProgram as ts.BuilderProgram; - ts.Debug.assert(!!oldProgram); - newProgram = oldProgram.getProgram(); - } - else if (ts.isArray(newProgramOrRootNames)) { - oldProgram = configFileParsingDiagnosticsOrOldProgram as ts.BuilderProgram; - newProgram = ts.createProgram({ - rootNames: newProgramOrRootNames, - options: hostOrOptions as ts.CompilerOptions, - host: oldProgramOrHost as ts.CompilerHost, - oldProgram: oldProgram && oldProgram.getProgramOrUndefined(), - configFileParsingDiagnostics, - projectReferences - }); - host = oldProgramOrHost as ts.CompilerHost; - } - else { - newProgram = newProgramOrRootNames; - host = hostOrOptions as ts.BuilderProgramHost; - oldProgram = oldProgramOrHost as ts.BuilderProgram; - configFileParsingDiagnostics = configFileParsingDiagnosticsOrOldProgram as readonly ts.Diagnostic[]; - } - return { host, newProgram, oldProgram, configFileParsingDiagnostics: configFileParsingDiagnostics || ts.emptyArray }; + else if (ts.isArray(newProgramOrRootNames)) { + oldProgram = configFileParsingDiagnosticsOrOldProgram as ts.BuilderProgram; + newProgram = ts.createProgram({ + rootNames: newProgramOrRootNames, + options: hostOrOptions as ts.CompilerOptions, + host: oldProgramOrHost as ts.CompilerHost, + oldProgram: oldProgram && oldProgram.getProgramOrUndefined(), + configFileParsingDiagnostics, + projectReferences + }); + host = oldProgramOrHost as ts.CompilerHost; } + else { + newProgram = newProgramOrRootNames; + host = hostOrOptions as ts.BuilderProgramHost; + oldProgram = oldProgramOrHost as ts.BuilderProgram; + configFileParsingDiagnostics = configFileParsingDiagnosticsOrOldProgram as readonly ts.Diagnostic[]; + } + return { host, newProgram, oldProgram, configFileParsingDiagnostics: configFileParsingDiagnostics || ts.emptyArray }; +} - export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): ts.SemanticDiagnosticsBuilderProgram; - export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): ts.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; - } - - /** - * Create the canonical file name for identity - */ - const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - /** - * Computing hash to for signature verification - */ - const computeHash = ts.maybeBind(host, host.createHash); - let state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState, host.disableUseFileVersionAsSignature); - 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 +export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): ts.SemanticDiagnosticsBuilderProgram; +export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): ts.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 - oldProgram = undefined; oldState = undefined; + return oldProgram; + } - const getState = () => state; - const builderProgram = createRedirectedBuilderProgram(getState, configFileParsingDiagnostics); - builderProgram.getState = getState; - builderProgram.backupState = () => { - ts.Debug.assert(backupState === undefined); - backupState = cloneBuilderProgramState(state); - }; - builderProgram.restoreState = () => { - state = ts.Debug.checkDefined(backupState); - backupState = undefined; - }; - builderProgram.getAllDependencies = sourceFile => ts.BuilderState.getAllDependencies(state, ts.Debug.checkDefined(state.program), sourceFile); - builderProgram.getSemanticDiagnostics = getSemanticDiagnostics; - builderProgram.emit = emit; - builderProgram.releaseProgram = () => { - releaseCache(state); - backupState = undefined; - }; + /** + * Create the canonical file name for identity + */ + const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + /** + * Computing hash to for signature verification + */ + const computeHash = ts.maybeBind(host, host.createHash); + let state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState, host.disableUseFileVersionAsSignature); + 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 getState = () => state; + const builderProgram = createRedirectedBuilderProgram(getState, configFileParsingDiagnostics); + builderProgram.getState = getState; + builderProgram.backupState = () => { + ts.Debug.assert(backupState === undefined); + backupState = cloneBuilderProgramState(state); + }; + builderProgram.restoreState = () => { + state = ts.Debug.checkDefined(backupState); + backupState = undefined; + }; + builderProgram.getAllDependencies = sourceFile => ts.BuilderState.getAllDependencies(state, ts.Debug.checkDefined(state.program), sourceFile); + builderProgram.getSemanticDiagnostics = getSemanticDiagnostics; + builderProgram.emit = emit; + builderProgram.releaseProgram = () => { + releaseCache(state); + backupState = undefined; + }; - if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { - (builderProgram as ts.SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; - } - else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - (builderProgram as ts.EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; - (builderProgram as ts.EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; - builderProgram.emitBuildInfo = emitBuildInfo; - } - else { - ts.notImplemented(); - } + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + (builderProgram as ts.SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + } + else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + (builderProgram as ts.EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + (builderProgram as ts.EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; + builderProgram.emitBuildInfo = emitBuildInfo; + } + else { + ts.notImplemented(); + } - return builderProgram; + return builderProgram; - function emitBuildInfo(writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken): ts.EmitResult { - if (state.buildInfoEmitPending) { - const result = ts.Debug.checkDefined(state.program).emitBuildInfo(writeFile || ts.maybeBind(host, host.writeFile), cancellationToken); - state.buildInfoEmitPending = false; - return result; - } - return ts.emitSkippedWithNoDiagnostics; + function emitBuildInfo(writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken): ts.EmitResult { + if (state.buildInfoEmitPending) { + const result = ts.Debug.checkDefined(state.program).emitBuildInfo(writeFile || ts.maybeBind(host, host.writeFile), cancellationToken); + state.buildInfoEmitPending = false; + return result; } + return ts.emitSkippedWithNoDiagnostics; + } - /** - * 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?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.AffectedFileResult { - let affected = getNextAffectedFile(state, cancellationToken, computeHash, host); - let emitKind = BuilderFileEmit.Full; - let isPendingEmitFile = false; - if (!affected) { - if (!ts.outFile(state.compilerOptions)) { - const pendingAffectedFile = getNextAffectedFilePendingEmit(state); - if (!pendingAffectedFile) { - if (!state.buildInfoEmitPending) { - return undefined; - } - - const affected = ts.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 || ts.maybeBind(host, host.writeFile), cancellationToken), affected, - /*emitKind*/ BuilderFileEmit.Full, - /*isPendingEmitFile*/ false, - /*isBuildInfoEmit*/ true); - } - ({ affectedFile: affected, emitKind } = pendingAffectedFile); - isPendingEmitFile = true; - } - else { - const program = ts.Debug.checkDefined(state.program); - if (state.programEmitComplete) + /** + * 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?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.AffectedFileResult { + let affected = getNextAffectedFile(state, cancellationToken, computeHash, host); + let emitKind = BuilderFileEmit.Full; + let isPendingEmitFile = false; + if (!affected) { + if (!ts.outFile(state.compilerOptions)) { + const pendingAffectedFile = getNextAffectedFilePendingEmit(state); + if (!pendingAffectedFile) { + if (!state.buildInfoEmitPending) { return undefined; - affected = program; + } + + const affected = ts.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 || ts.maybeBind(host, host.writeFile), cancellationToken), affected, + /*emitKind*/ BuilderFileEmit.Full, + /*isPendingEmitFile*/ false, + /*isBuildInfoEmit*/ true); } + ({ affectedFile: affected, emitKind } = pendingAffectedFile); + isPendingEmitFile = true; + } + else { + const program = ts.Debug.checkDefined(state.program); + if (state.programEmitComplete) + return undefined; + affected = program; } - - return toAffectedFileEmitResult(state, - // When whole program is affected, do emit only once (eg when --out or --outFile is specified) - // Otherwise just affected file - ts.Debug.checkDefined(state.program).emit(affected === state.program ? undefined : affected as ts.SourceFile, affected !== state.program && ts.getEmitDeclarations(state.compilerOptions) && !customTransformers ? - getWriteFileUpdatingSignatureCallback(writeFile) : - writeFile || ts.maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles || emitKind === BuilderFileEmit.DtsOnly, customTransformers), affected, emitKind, isPendingEmitFile); } - function getWriteFileUpdatingSignatureCallback(writeFile: ts.WriteFileCallback | undefined): ts.WriteFileCallback { - return (fileName, text, writeByteOrderMark, onError, sourceFiles, data) => { - if (ts.isDeclarationFileName(fileName)) { - ts.Debug.assert(sourceFiles?.length === 1); - const file = sourceFiles[0]; - const info = state.fileInfos.get(file.resolvedPath)!; - const signature = state.currentAffectedFilesSignatures?.get(file.resolvedPath) || info.signature; - if (signature === file.version) { - const newSignature = (computeHash || ts.generateDjb2Hash)(data?.sourceMapUrlPos !== undefined ? text.substring(0, data.sourceMapUrlPos) : text); - if (newSignature !== file.version) { // Update it - if (host.storeFilesChangingSignatureDuringEmit) - (state.filesChangingSignature ||= new ts.Set()).add(file.resolvedPath); + + return toAffectedFileEmitResult(state, + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) + // Otherwise just affected file + ts.Debug.checkDefined(state.program).emit(affected === state.program ? undefined : affected as ts.SourceFile, affected !== state.program && ts.getEmitDeclarations(state.compilerOptions) && !customTransformers ? + getWriteFileUpdatingSignatureCallback(writeFile) : + writeFile || ts.maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles || emitKind === BuilderFileEmit.DtsOnly, customTransformers), affected, emitKind, isPendingEmitFile); + } + function getWriteFileUpdatingSignatureCallback(writeFile: ts.WriteFileCallback | undefined): ts.WriteFileCallback { + return (fileName, text, writeByteOrderMark, onError, sourceFiles, data) => { + if (ts.isDeclarationFileName(fileName)) { + ts.Debug.assert(sourceFiles?.length === 1); + const file = sourceFiles[0]; + const info = state.fileInfos.get(file.resolvedPath)!; + const signature = state.currentAffectedFilesSignatures?.get(file.resolvedPath) || info.signature; + if (signature === file.version) { + const newSignature = (computeHash || ts.generateDjb2Hash)(data?.sourceMapUrlPos !== undefined ? text.substring(0, data.sourceMapUrlPos) : text); + if (newSignature !== file.version) { // Update it + if (host.storeFilesChangingSignatureDuringEmit) + (state.filesChangingSignature ||= new ts.Set()).add(file.resolvedPath); + if (state.exportedModulesMap) + ts.BuilderState.updateExportedModules(file, file.exportedModulesFromDeclarationEmit, state.currentAffectedFilesExportedModulesMap ||= ts.BuilderState.createManyToManyPathMap()); + if (state.affectedFiles && state.affectedFilesIndex! < state.affectedFiles.length) { + state.currentAffectedFilesSignatures!.set(file.resolvedPath, newSignature); + } + else { + info.signature = newSignature; if (state.exportedModulesMap) - ts.BuilderState.updateExportedModules(file, file.exportedModulesFromDeclarationEmit, state.currentAffectedFilesExportedModulesMap ||= ts.BuilderState.createManyToManyPathMap()); - if (state.affectedFiles && state.affectedFilesIndex! < state.affectedFiles.length) { - state.currentAffectedFilesSignatures!.set(file.resolvedPath, newSignature); - } - else { - info.signature = newSignature; - if (state.exportedModulesMap) - ts.BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); - } + ts.BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); } } } - if (writeFile) - writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); - else if (host.writeFile) - host.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); - else - state.program!.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); - }; + } + if (writeFile) + writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); + else if (host.writeFile) + host.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); + else + state.program!.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); + }; + } + + /** + * 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?: ts.SourceFile, writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.EmitResult { + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); } + const result = ts.handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken); + if (result) + return result; - /** - * 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?: ts.SourceFile, writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.EmitResult { + // Emit only affected files if using builder for emit + if (!targetSourceFile) { if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); - } - const result = ts.handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken); - if (result) - return result; - - // Emit only affected files if using builder for emit - if (!targetSourceFile) { - if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - // Emit and report any errors we ran into. - let sourceMaps: ts.SourceMapEmitResult[] = []; - let emitSkipped = false; - let diagnostics: ts.Diagnostic[] | undefined; - let emittedFiles: string[] = []; - - let affectedEmitResult: ts.AffectedFileResult; - while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { - emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; - diagnostics = ts.addRange(diagnostics, affectedEmitResult.result.diagnostics); - emittedFiles = ts.addRange(emittedFiles, affectedEmitResult.result.emittedFiles); - sourceMaps = ts.addRange(sourceMaps, affectedEmitResult.result.sourceMaps); - } - return { - emitSkipped, - diagnostics: diagnostics || ts.emptyArray, - emittedFiles, - sourceMaps - }; - } - // In non Emit builder, clear affected files pending emit - else if (state.affectedFilesPendingEmitKind?.size) { - ts.Debug.assert(kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram); - // State can clear affected files pending emit if - if (!emitOnlyDtsFiles // If we are doing complete emit, affected files pending emit can be cleared - // If every file pending emit is pending on only dts emit - || ts.every(state.affectedFilesPendingEmit, (path, index) => index < state.affectedFilesPendingEmitIndex! || - state.affectedFilesPendingEmitKind!.get(path) === BuilderFileEmit.DtsOnly)) { - clearAffectedFilesPendingEmit(state); - } + // Emit and report any errors we ran into. + let sourceMaps: ts.SourceMapEmitResult[] = []; + let emitSkipped = false; + let diagnostics: ts.Diagnostic[] | undefined; + let emittedFiles: string[] = []; + + let affectedEmitResult: ts.AffectedFileResult; + while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { + emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; + diagnostics = ts.addRange(diagnostics, affectedEmitResult.result.diagnostics); + emittedFiles = ts.addRange(emittedFiles, affectedEmitResult.result.emittedFiles); + sourceMaps = ts.addRange(sourceMaps, affectedEmitResult.result.sourceMaps); } + return { + emitSkipped, + diagnostics: diagnostics || ts.emptyArray, + emittedFiles, + sourceMaps + }; } - return ts.Debug.checkDefined(state.program).emit(targetSourceFile, !ts.outFile(state.compilerOptions) && ts.getEmitDeclarations(state.compilerOptions) && !customTransformers ? - getWriteFileUpdatingSignatureCallback(writeFile) : - writeFile || ts.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?: ts.CancellationToken, ignoreSourceFile?: (sourceFile: ts.SourceFile) => boolean): ts.AffectedFileResult { - while (true) { - const affected = getNextAffectedFile(state, cancellationToken, computeHash, host); - if (!affected) { - // Done - return undefined; + // In non Emit builder, clear affected files pending emit + else if (state.affectedFilesPendingEmitKind?.size) { + ts.Debug.assert(kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram); + // State can clear affected files pending emit if + if (!emitOnlyDtsFiles // If we are doing complete emit, affected files pending emit can be cleared + // If every file pending emit is pending on only dts emit + || ts.every(state.affectedFilesPendingEmit, (path, index) => index < state.affectedFilesPendingEmitIndex! || + state.affectedFilesPendingEmitKind!.get(path) === BuilderFileEmit.DtsOnly)) { + clearAffectedFilesPendingEmit(state); } - 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 - // Apart for emit builder do this for tsbuildinfo, do this for non emit builder when noEmit is set as tsbuildinfo is written and reused between emitters - if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram || state.compilerOptions.noEmit || state.compilerOptions.noEmitOnError) { - addToAffectedFilesPendingEmit(state, (affected as ts.SourceFile).resolvedPath, BuilderFileEmit.Full); - } - - // Get diagnostics for the affected file if its not ignored - if (ignoreSourceFile && ignoreSourceFile(affected as ts.SourceFile)) { - // Get next affected file - doneWithAffectedFile(state, affected); - continue; - } - - return toAffectedFileResult(state, getSemanticDiagnosticsOfFile(state, affected as ts.SourceFile, cancellationToken), affected); } } + return ts.Debug.checkDefined(state.program).emit(targetSourceFile, !ts.outFile(state.compilerOptions) && ts.getEmitDeclarations(state.compilerOptions) && !customTransformers ? + getWriteFileUpdatingSignatureCallback(writeFile) : + writeFile || ts.maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles, customTransformers); + } - /** - * 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?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { - assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); - const compilerOptions = ts.Debug.checkDefined(state.program).getCompilerOptions(); - if (ts.outFile(compilerOptions)) { - ts.Debug.assert(!state.semanticDiagnosticsPerFile); - // We dont need to cache the diagnostics just return them from program - return ts.Debug.checkDefined(state.program).getSemanticDiagnostics(sourceFile, cancellationToken); + /** + * 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?: ts.CancellationToken, ignoreSourceFile?: (sourceFile: ts.SourceFile) => boolean): ts.AffectedFileResult { + while (true) { + const affected = getNextAffectedFile(state, cancellationToken, computeHash, host); + 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 + // Apart for emit builder do this for tsbuildinfo, do this for non emit builder when noEmit is set as tsbuildinfo is written and reused between emitters + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram || state.compilerOptions.noEmit || state.compilerOptions.noEmitOnError) { + addToAffectedFilesPendingEmit(state, (affected as ts.SourceFile).resolvedPath, BuilderFileEmit.Full); } - let diagnostics: ts.Diagnostic[] | undefined; - for (const sourceFile of ts.Debug.checkDefined(state.program).getSourceFiles()) { - diagnostics = ts.addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); + // Get diagnostics for the affected file if its not ignored + if (ignoreSourceFile && ignoreSourceFile(affected as ts.SourceFile)) { + // Get next affected file + doneWithAffectedFile(state, affected); + continue; } - return diagnostics || ts.emptyArray; + + return toAffectedFileResult(state, getSemanticDiagnosticsOfFile(state, affected as ts.SourceFile, cancellationToken), affected); } } - function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilePendingEmit: ts.Path, kind: BuilderFileEmit) { - if (!state.affectedFilesPendingEmit) - state.affectedFilesPendingEmit = []; - if (!state.affectedFilesPendingEmitKind) - state.affectedFilesPendingEmitKind = new ts.Map(); - - 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?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { + assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); + const compilerOptions = ts.Debug.checkDefined(state.program).getCompilerOptions(); + if (ts.outFile(compilerOptions)) { + ts.Debug.assert(!state.semanticDiagnosticsPerFile); + // We dont need to cache the diagnostics just return them from program + return ts.Debug.checkDefined(state.program).getSemanticDiagnostics(sourceFile, cancellationToken); + } + + if (sourceFile) { + return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); + } + + // 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)) { } + + let diagnostics: ts.Diagnostic[] | undefined; + for (const sourceFile of ts.Debug.checkDefined(state.program).getSourceFiles()) { + diagnostics = ts.addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); + } + return diagnostics || ts.emptyArray; } +} - export function toBuilderStateFileInfo(fileInfo: ProgramBuildInfoFileInfo): ts.BuilderState.FileInfo { - return ts.isString(fileInfo) ? - { version: fileInfo, signature: fileInfo, affectsGlobalScope: undefined, impliedFormat: undefined } : - ts.isString(fileInfo.signature) ? - fileInfo as ts.BuilderState.FileInfo : - { version: fileInfo.version, signature: fileInfo.signature === false ? undefined : fileInfo.version, affectsGlobalScope: fileInfo.affectsGlobalScope, impliedFormat: fileInfo.impliedFormat }; +function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilePendingEmit: ts.Path, kind: BuilderFileEmit) { + if (!state.affectedFilesPendingEmit) + state.affectedFilesPendingEmit = []; + if (!state.affectedFilesPendingEmitKind) + state.affectedFilesPendingEmitKind = new ts.Map(); + + 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; } +} - export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo, buildInfoPath: string, host: ts.ReadBuildProgramHost): ts.EmitAndSemanticDiagnosticsBuilderProgram { - const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); - const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - - const filePaths = program.fileNames.map(toPath); - const filePathsSetList = program.fileIdsList?.map(fileIds => new ts.Set(fileIds.map(toFilePath))); - const fileInfos = new ts.Map(); - program.fileInfos.forEach((fileInfo, index) => fileInfos.set(toFilePath(index + 1 as ProgramBuildInfoFileId), toBuilderStateFileInfo(fileInfo))); - const state: ReusableBuilderProgramState = { - fileInfos, - compilerOptions: program.options ? ts.convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath) : {}, - referencedMap: toManyToManyPathMap(program.referencedMap), - exportedModulesMap: toManyToManyPathMap(program.exportedModulesMap), - semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && ts.arrayToMap(program.semanticDiagnosticsPerFile, value => toFilePath(ts.isNumber(value) ? value : value[0]), value => ts.isNumber(value) ? ts.emptyArray : value[1]), - hasReusableDiagnostic: true, - affectedFilesPendingEmit: ts.map(program.affectedFilesPendingEmit, value => toFilePath(value[0])), - affectedFilesPendingEmitKind: program.affectedFilesPendingEmit && ts.arrayToMap(program.affectedFilesPendingEmit, value => toFilePath(value[0]), value => value[1]), - affectedFilesPendingEmitIndex: program.affectedFilesPendingEmit && 0, - }; - return { - getState: () => state, - backupState: ts.noop, - restoreState: ts.noop, - getProgram: ts.notImplemented, - getProgramOrUndefined: ts.returnUndefined, - releaseProgram: ts.noop, - getCompilerOptions: () => state.compilerOptions, - getSourceFile: ts.notImplemented, - getSourceFiles: ts.notImplemented, - getOptionsDiagnostics: ts.notImplemented, - getGlobalDiagnostics: ts.notImplemented, - getConfigFileParsingDiagnostics: ts.notImplemented, - getSyntacticDiagnostics: ts.notImplemented, - getDeclarationDiagnostics: ts.notImplemented, - getSemanticDiagnostics: ts.notImplemented, - emit: ts.notImplemented, - getAllDependencies: ts.notImplemented, - getCurrentDirectory: ts.notImplemented, - emitNextAffectedFile: ts.notImplemented, - getSemanticDiagnosticsOfNextAffectedFile: ts.notImplemented, - emitBuildInfo: ts.notImplemented, - close: ts.noop, - }; +export function toBuilderStateFileInfo(fileInfo: ProgramBuildInfoFileInfo): ts.BuilderState.FileInfo { + return ts.isString(fileInfo) ? + { version: fileInfo, signature: fileInfo, affectsGlobalScope: undefined, impliedFormat: undefined } : + ts.isString(fileInfo.signature) ? + fileInfo as ts.BuilderState.FileInfo : + { version: fileInfo.version, signature: fileInfo.signature === false ? undefined : fileInfo.version, affectsGlobalScope: fileInfo.affectsGlobalScope, impliedFormat: fileInfo.impliedFormat }; +} - function toPath(path: string) { - return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); - } +export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo, buildInfoPath: string, host: ts.ReadBuildProgramHost): ts.EmitAndSemanticDiagnosticsBuilderProgram { + const buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); + const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + + const filePaths = program.fileNames.map(toPath); + const filePathsSetList = program.fileIdsList?.map(fileIds => new ts.Set(fileIds.map(toFilePath))); + const fileInfos = new ts.Map(); + program.fileInfos.forEach((fileInfo, index) => fileInfos.set(toFilePath(index + 1 as ProgramBuildInfoFileId), toBuilderStateFileInfo(fileInfo))); + const state: ReusableBuilderProgramState = { + fileInfos, + compilerOptions: program.options ? ts.convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath) : {}, + referencedMap: toManyToManyPathMap(program.referencedMap), + exportedModulesMap: toManyToManyPathMap(program.exportedModulesMap), + semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && ts.arrayToMap(program.semanticDiagnosticsPerFile, value => toFilePath(ts.isNumber(value) ? value : value[0]), value => ts.isNumber(value) ? ts.emptyArray : value[1]), + hasReusableDiagnostic: true, + affectedFilesPendingEmit: ts.map(program.affectedFilesPendingEmit, value => toFilePath(value[0])), + affectedFilesPendingEmitKind: program.affectedFilesPendingEmit && ts.arrayToMap(program.affectedFilesPendingEmit, value => toFilePath(value[0]), value => value[1]), + affectedFilesPendingEmitIndex: program.affectedFilesPendingEmit && 0, + }; + return { + getState: () => state, + backupState: ts.noop, + restoreState: ts.noop, + getProgram: ts.notImplemented, + getProgramOrUndefined: ts.returnUndefined, + releaseProgram: ts.noop, + getCompilerOptions: () => state.compilerOptions, + getSourceFile: ts.notImplemented, + getSourceFiles: ts.notImplemented, + getOptionsDiagnostics: ts.notImplemented, + getGlobalDiagnostics: ts.notImplemented, + getConfigFileParsingDiagnostics: ts.notImplemented, + getSyntacticDiagnostics: ts.notImplemented, + getDeclarationDiagnostics: ts.notImplemented, + getSemanticDiagnostics: ts.notImplemented, + emit: ts.notImplemented, + getAllDependencies: ts.notImplemented, + getCurrentDirectory: ts.notImplemented, + emitNextAffectedFile: ts.notImplemented, + getSemanticDiagnosticsOfNextAffectedFile: ts.notImplemented, + emitBuildInfo: ts.notImplemented, + close: ts.noop, + }; - function toAbsolutePath(path: string) { - return ts.getNormalizedAbsolutePath(path, buildInfoDirectory); - } + function toPath(path: string) { + return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); + } - function toFilePath(fileId: ProgramBuildInfoFileId) { - return filePaths[fileId - 1]; - } + function toAbsolutePath(path: string) { + return ts.getNormalizedAbsolutePath(path, buildInfoDirectory); + } - function toFilePathsSet(fileIdsListId: ProgramBuildInfoFileIdListId) { - return filePathsSetList![fileIdsListId - 1]; - } + function toFilePath(fileId: ProgramBuildInfoFileId) { + return filePaths[fileId - 1]; + } - function toManyToManyPathMap(referenceMap: ProgramBuildInfoReferencedMap | undefined): ts.BuilderState.ManyToManyPathMap | undefined { - if (!referenceMap) { - return undefined; - } + function toFilePathsSet(fileIdsListId: ProgramBuildInfoFileIdListId) { + return filePathsSetList![fileIdsListId - 1]; + } - const map = ts.BuilderState.createManyToManyPathMap(); - referenceMap.forEach(([fileId, fileIdListId]) => map.set(toFilePath(fileId), toFilePathsSet(fileIdListId))); - return map; + function toManyToManyPathMap(referenceMap: ProgramBuildInfoReferencedMap | undefined): ts.BuilderState.ManyToManyPathMap | undefined { + if (!referenceMap) { + return undefined; } + + const map = ts.BuilderState.createManyToManyPathMap(); + referenceMap.forEach(([fileId, fileIdListId]) => map.set(toFilePath(fileId), toFilePathsSet(fileIdListId))); + return map; } +} - export function createRedirectedBuilderProgram(getState: () => { - program: ts.Program | undefined; - compilerOptions: ts.CompilerOptions; - }, configFileParsingDiagnostics: readonly ts.Diagnostic[]): ts.BuilderProgram { - return { - getState: ts.notImplemented, - backupState: ts.noop, - restoreState: ts.noop, - getProgram, - getProgramOrUndefined: () => getState().program, - releaseProgram: () => getState().program = undefined, - getCompilerOptions: () => getState().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), - emitBuildInfo: (writeFile, cancellationToken) => getProgram().emitBuildInfo(writeFile, cancellationToken), - getAllDependencies: ts.notImplemented, - getCurrentDirectory: () => getProgram().getCurrentDirectory(), - close: ts.noop, - }; +export function createRedirectedBuilderProgram(getState: () => { + program: ts.Program | undefined; + compilerOptions: ts.CompilerOptions; +}, configFileParsingDiagnostics: readonly ts.Diagnostic[]): ts.BuilderProgram { + return { + getState: ts.notImplemented, + backupState: ts.noop, + restoreState: ts.noop, + getProgram, + getProgramOrUndefined: () => getState().program, + releaseProgram: () => getState().program = undefined, + getCompilerOptions: () => getState().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), + emitBuildInfo: (writeFile, cancellationToken) => getProgram().emitBuildInfo(writeFile, cancellationToken), + getAllDependencies: ts.notImplemented, + getCurrentDirectory: () => getProgram().getCurrentDirectory(), + close: ts.noop, + }; - function getProgram() { - return ts.Debug.checkDefined(getState().program); - } + function getProgram() { + return ts.Debug.checkDefined(getState().program); } } +} diff --git a/src/compiler/builderPublic.ts b/src/compiler/builderPublic.ts index e80ae6e6f973b..1668eed9bd501 100644 --- a/src/compiler/builderPublic.ts +++ b/src/compiler/builderPublic.ts @@ -1,177 +1,177 @@ namespace ts { - export type AffectedFileResult = { - result: T; - affected: ts.SourceFile | ts.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?: ts.WriteFileCallback; - /** - * disable using source file version as signature for testing - */ - /*@internal*/ - disableUseFileVersionAsSignature?: boolean; - /** - * Store the list of files that update signature during the emit - */ - /*@internal*/ - storeFilesChangingSignatureDuringEmit?: boolean; - } +export type AffectedFileResult = { + result: T; + affected: ts.SourceFile | ts.Program; +} | undefined; +export interface BuilderProgramHost { /** - * Builder to manage the program state changes - */ - export interface BuilderProgram { - /*@internal*/ - getState(): ts.ReusableBuilderProgramState; - /*@internal*/ - backupState(): void; - /*@internal*/ - restoreState(): void; - /** - * Returns current program - */ - getProgram(): ts.Program; - /** - * Returns current program that could be undefined if the program was released - */ - /*@internal*/ - getProgramOrUndefined(): ts.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(): ts.CompilerOptions; - /** - * Get the source file in the program with file name - */ - getSourceFile(fileName: string): ts.SourceFile | undefined; - /** - * Get a list of files in the program - */ - getSourceFiles(): readonly ts.SourceFile[]; - /** - * Get the diagnostics for compiler options - */ - getOptionsDiagnostics(cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; - /** - * Get the diagnostics that dont belong to any file - */ - getGlobalDiagnostics(cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; - /** - * Get the diagnostics from config file parsing - */ - getConfigFileParsingDiagnostics(): readonly ts.Diagnostic[]; - /** - * Get the syntax diagnostics, for all source files if source file is not supplied - */ - getSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; - /** - * Get the declaration diagnostics, for all source files if source file is not supplied - */ - getDeclarationDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.DiagnosticWithLocation[]; - /** - * Get all the dependencies of the file - */ - getAllDependencies(sourceFile: ts.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?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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?: ts.SourceFile, writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.EmitResult; - /*@internal*/ - emitBuildInfo(writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken): ts.EmitResult; - /** - * Get the current directory of the program - */ - getCurrentDirectory(): string; - /*@internal*/ - close(): void; - } - + * 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; /** - * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write 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?: ts.CancellationToken, ignoreSourceFile?: (sourceFile: ts.SourceFile) => boolean): AffectedFileResult; - } + writeFile?: ts.WriteFileCallback; + /** + * disable using source file version as signature for testing + */ + /*@internal*/ + disableUseFileVersionAsSignature?: boolean; + /** + * Store the list of files that update signature during the emit + */ + /*@internal*/ + storeFilesChangingSignatureDuringEmit?: boolean; +} +/** + * Builder to manage the program state changes + */ +export interface BuilderProgram { + /*@internal*/ + getState(): ts.ReusableBuilderProgramState; + /*@internal*/ + backupState(): void; + /*@internal*/ + restoreState(): void; + /** + * Returns current program + */ + getProgram(): ts.Program; + /** + * Returns current program that could be undefined if the program was released + */ + /*@internal*/ + getProgramOrUndefined(): ts.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(): ts.CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): ts.SourceFile | undefined; /** - * 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?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): AffectedFileResult; - } + * Get a list of files in the program + */ + getSourceFiles(): readonly ts.SourceFile[]; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; + /** + * Get the diagnostics from config file parsing + */ + getConfigFileParsingDiagnostics(): readonly ts.Diagnostic[]; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; + /** + * Get the declaration diagnostics, for all source files if source file is not supplied + */ + getDeclarationDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.DiagnosticWithLocation[]; + /** + * Get all the dependencies of the file + */ + getAllDependencies(sourceFile: ts.SourceFile): readonly string[]; /** - * Create the builder to manage semantic diagnostics and cache them + * 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?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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 */ - export function createSemanticDiagnosticsBuilderProgram(newProgram: ts.Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): SemanticDiagnosticsBuilderProgram; - export function createSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): SemanticDiagnosticsBuilderProgram; - export function createSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]) { - return ts.createBuilderProgram(ts.BuilderProgramKind.SemanticDiagnosticsBuilderProgram, ts.getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); - } + emit(targetSourceFile?: ts.SourceFile, writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): ts.EmitResult; + /*@internal*/ + emitBuildInfo(writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken): ts.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 { /** - * 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 + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete */ - export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: ts.Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): EmitAndSemanticDiagnosticsBuilderProgram; - export function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): EmitAndSemanticDiagnosticsBuilderProgram; - export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]) { - return ts.createBuilderProgram(ts.BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, ts.getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); - } + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: ts.CancellationToken, ignoreSourceFile?: (sourceFile: ts.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 { /** - * Creates a builder thats just abstraction over program and can be used with watch + * 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 */ - export function createAbstractBuilder(newProgram: ts.Program, host: BuilderProgramHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): BuilderProgram; - export function createAbstractBuilder(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderProgram; - export function createAbstractBuilder(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | BuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderProgram { - const { newProgram, configFileParsingDiagnostics: newConfigFileParsingDiagnostics } = ts.getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences); - return ts.createRedirectedBuilderProgram(() => ({ program: newProgram, compilerOptions: newProgram.getCompilerOptions() }), newConfigFileParsingDiagnostics); - } + emitNextAffectedFile(writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers): AffectedFileResult; +} + +/** + * Create the builder to manage semantic diagnostics and cache them + */ +export function createSemanticDiagnosticsBuilderProgram(newProgram: ts.Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): SemanticDiagnosticsBuilderProgram; +export function createSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): SemanticDiagnosticsBuilderProgram; +export function createSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]) { + return ts.createBuilderProgram(ts.BuilderProgramKind.SemanticDiagnosticsBuilderProgram, ts.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: ts.Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): EmitAndSemanticDiagnosticsBuilderProgram; +export function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): EmitAndSemanticDiagnosticsBuilderProgram; +export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]) { + return ts.createBuilderProgram(ts.BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, ts.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: ts.Program, host: BuilderProgramHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): BuilderProgram; +export function createAbstractBuilder(rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderProgram; +export function createAbstractBuilder(newProgramOrRootNames: ts.Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | ts.CompilerOptions | undefined, oldProgramOrHost?: ts.CompilerHost | BuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly ts.Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]): BuilderProgram { + const { newProgram, configFileParsingDiagnostics: newConfigFileParsingDiagnostics } = ts.getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences); + return ts.createRedirectedBuilderProgram(() => ({ program: newProgram, compilerOptions: newProgram.getCompilerOptions() }), newConfigFileParsingDiagnostics); +} } diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index 1348aa6a809da..949b3b9f13350 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -1,649 +1,649 @@ /*@internal*/ namespace ts { - export function getFileEmitOutput(program: ts.Program, sourceFile: ts.SourceFile, emitOnlyDtsFiles: boolean, cancellationToken?: ts.CancellationToken, customTransformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitOutput { - const outputFiles: ts.OutputFile[] = []; - const { emitSkipped, diagnostics, exportedModulesFromDeclarationEmit } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit); - return { outputFiles, emitSkipped, diagnostics, exportedModulesFromDeclarationEmit }; +export function getFileEmitOutput(program: ts.Program, sourceFile: ts.SourceFile, emitOnlyDtsFiles: boolean, cancellationToken?: ts.CancellationToken, customTransformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitOutput { + const outputFiles: ts.OutputFile[] = []; + const { emitSkipped, diagnostics, exportedModulesFromDeclarationEmit } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit); + return { outputFiles, emitSkipped, diagnostics, exportedModulesFromDeclarationEmit }; - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - } + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); } +} - export interface ReusableBuilderState { - /** - * Information of the file eg. its version, signature etc - */ - fileInfos: ts.ReadonlyESMap; - /** - * 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?: BuilderState.ReadonlyManyToManyPathMap | undefined; - /** - * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled - * Otherwise undefined - */ - readonly exportedModulesMap?: BuilderState.ReadonlyManyToManyPathMap | undefined; +export interface ReusableBuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: ts.ReadonlyESMap; + /** + * 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?: BuilderState.ReadonlyManyToManyPathMap | undefined; + /** + * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled + * Otherwise undefined + */ + readonly exportedModulesMap?: BuilderState.ReadonlyManyToManyPathMap | undefined; +} + +export interface BuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: ts.ESMap; + /** + * 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: BuilderState.ReadonlyManyToManyPathMap | undefined; + /** + * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled + * Otherwise undefined + * + * This is equivalent to referencedMap, but for the emitted .d.ts file. + */ + readonly exportedModulesMap: BuilderState.ManyToManyPathMap | undefined; + + /** + * true if file version is used as signature + * This helps in delaying the calculation of the d.ts hash as version for the file till reasonable time + */ + useFileVersionAsSignature: boolean; + /** + * 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.Set; + /** + * Cache of all files excluding default library file for the current program + */ + allFilesExcludingDefaultLibraryFile?: readonly ts.SourceFile[]; + /** + * Cache of all the file names + */ + allFileNames?: readonly string[]; +} + +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; + affectsGlobalScope: true | undefined; + impliedFormat: number | undefined; } - export interface BuilderState { - /** - * Information of the file eg. its version, signature etc - */ - fileInfos: ts.ESMap; - /** - * 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: BuilderState.ReadonlyManyToManyPathMap | undefined; - /** - * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled - * Otherwise undefined - * - * This is equivalent to referencedMap, but for the emitted .d.ts file. - */ - readonly exportedModulesMap: BuilderState.ManyToManyPathMap | undefined; + export interface ReadonlyManyToManyPathMap { + clone(): ManyToManyPathMap; + forEach(action: (v: ts.ReadonlySet, k: ts.Path) => void): void; + getKeys(v: ts.Path): ts.ReadonlySet | undefined; + getValues(k: ts.Path): ts.ReadonlySet | undefined; + hasKey(k: ts.Path): boolean; + keys(): ts.Iterator; /** - * true if file version is used as signature - * This helps in delaying the calculation of the d.ts hash as version for the file till reasonable time - */ - useFileVersionAsSignature: boolean; - /** - * 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.Set; - /** - * Cache of all files excluding default library file for the current program + * The set of arguments to {@link deleteKeys} which have not subsequently + * been arguments to {@link set}. Note that a key does not have to have + * ever been in the map to appear in this set. */ - allFilesExcludingDefaultLibraryFile?: readonly ts.SourceFile[]; - /** - * Cache of all the file names - */ - allFileNames?: readonly string[]; + deletedKeys(): ts.ReadonlySet | undefined; } - 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; - affectsGlobalScope: true | undefined; - impliedFormat: number | undefined; - } - - export interface ReadonlyManyToManyPathMap { - clone(): ManyToManyPathMap; - forEach(action: (v: ts.ReadonlySet, k: ts.Path) => void): void; - getKeys(v: ts.Path): ts.ReadonlySet | undefined; - getValues(k: ts.Path): ts.ReadonlySet | undefined; - hasKey(k: ts.Path): boolean; - keys(): ts.Iterator; - - /** - * The set of arguments to {@link deleteKeys} which have not subsequently - * been arguments to {@link set}. Note that a key does not have to have - * ever been in the map to appear in this set. - */ - deletedKeys(): ts.ReadonlySet | undefined; - } - - export interface ManyToManyPathMap extends ReadonlyManyToManyPathMap { - deleteKey(k: ts.Path): boolean; - set(k: ts.Path, v: ts.ReadonlySet): void; - clear(): void; - } - - export function createManyToManyPathMap(): ManyToManyPathMap { - function create(forward: ts.ESMap>, reverse: ts.ESMap>, deleted: ts.Set | undefined): ManyToManyPathMap { - const map: ManyToManyPathMap = { - clone: () => create(new ts.Map(forward), new ts.Map(reverse), deleted && new ts.Set(deleted)), - forEach: fn => forward.forEach(fn), - getKeys: v => reverse.get(v), - getValues: k => forward.get(k), - hasKey: k => forward.has(k), - keys: () => forward.keys(), - - deletedKeys: () => deleted, - deleteKey: k => { - (deleted ||= new ts.Set()).add(k); - - const set = forward.get(k); - if (!set) { - return false; - } + export interface ManyToManyPathMap extends ReadonlyManyToManyPathMap { + deleteKey(k: ts.Path): boolean; + set(k: ts.Path, v: ts.ReadonlySet): void; + clear(): void; + } - set.forEach(v => deleteFromMultimap(reverse, v, k)); - forward.delete(k); - return true; - }, - set: (k, vSet) => { - deleted?.delete(k); - - const existingVSet = forward.get(k); - forward.set(k, vSet); - - existingVSet?.forEach(v => { - if (!vSet.has(v)) { - deleteFromMultimap(reverse, v, k); - } - }); - - vSet.forEach(v => { - if (!existingVSet?.has(v)) { - addToMultimap(reverse, v, k); - } - }); - - return map; - }, - clear: () => { - forward.clear(); - reverse.clear(); - deleted?.clear(); + export function createManyToManyPathMap(): ManyToManyPathMap { + function create(forward: ts.ESMap>, reverse: ts.ESMap>, deleted: ts.Set | undefined): ManyToManyPathMap { + const map: ManyToManyPathMap = { + clone: () => create(new ts.Map(forward), new ts.Map(reverse), deleted && new ts.Set(deleted)), + forEach: fn => forward.forEach(fn), + getKeys: v => reverse.get(v), + getValues: k => forward.get(k), + hasKey: k => forward.has(k), + keys: () => forward.keys(), + + deletedKeys: () => deleted, + deleteKey: k => { + (deleted ||= new ts.Set()).add(k); + + const set = forward.get(k); + if (!set) { + return false; } - }; - return map; - } + set.forEach(v => deleteFromMultimap(reverse, v, k)); + forward.delete(k); + return true; + }, + set: (k, vSet) => { + deleted?.delete(k); - return create(new ts.Map>(), new ts.Map>(), /*deleted*/ undefined); - } + const existingVSet = forward.get(k); + forward.set(k, vSet); - function addToMultimap(map: ts.ESMap>, k: K, v: V): void { - let set = map.get(k); - if (!set) { - set = new ts.Set(); - map.set(k, set); - } - set.add(v); - } - - function deleteFromMultimap(map: ts.ESMap>, k: K, v: V): boolean { - const set = map.get(k); + existingVSet?.forEach(v => { + if (!vSet.has(v)) { + deleteFromMultimap(reverse, v, k); + } + }); - if (set?.delete(v)) { - if (!set.size) { - map.delete(k); + vSet.forEach(v => { + if (!existingVSet?.has(v)) { + addToMultimap(reverse, v, k); + } + }); + + return map; + }, + clear: () => { + forward.clear(); + reverse.clear(); + deleted?.clear(); } - return true; - } + }; - return false; + return map; } - /** - * Compute the hash to store the shape of the file - */ - export type ComputeHash = ((data: string) => string) | undefined; + return create(new ts.Map>(), new ts.Map>(), /*deleted*/ undefined); + } - function getReferencedFilesFromImportedModuleSymbol(symbol: ts.Symbol): ts.Path[] { - return ts.mapDefined(symbol.declarations, declaration => ts.getSourceFileOfNode(declaration)?.resolvedPath); + function addToMultimap(map: ts.ESMap>, k: K, v: V): void { + let set = map.get(k); + if (!set) { + set = new ts.Set(); + map.set(k, set); } + set.add(v); + } - /** - * Get the module source file and all augmenting files from the import name node from file - */ - function getReferencedFilesFromImportLiteral(checker: ts.TypeChecker, importName: ts.StringLiteralLike): ts.Path[] | undefined { - const symbol = checker.getSymbolAtLocation(importName); - return symbol && getReferencedFilesFromImportedModuleSymbol(symbol); - } + function deleteFromMultimap(map: ts.ESMap>, k: K, v: V): boolean { + const set = map.get(k); - /** - * Gets the path to reference file from file name, it could be resolvedPath if present otherwise path - */ - function getReferencedFileFromFileName(program: ts.Program, fileName: string, sourceFileDirectory: ts.Path, getCanonicalFileName: ts.GetCanonicalFileName): ts.Path { - return ts.toPath(program.getProjectReferenceRedirect(fileName) || fileName, sourceFileDirectory, getCanonicalFileName); + if (set?.delete(v)) { + if (!set.size) { + map.delete(k); + } + return true; } - /** - * 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: ts.Program, sourceFile: ts.SourceFile, getCanonicalFileName: ts.GetCanonicalFileName): ts.Set | undefined { - let referencedFiles: ts.Set | 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: ts.TypeChecker = program.getTypeChecker(); - for (const importName of sourceFile.imports) { - const declarationSourceFilePaths = getReferencedFilesFromImportLiteral(checker, importName); - declarationSourceFilePaths?.forEach(addReferencedFile); - } - } + return false; + } - const sourceFileDirectory = ts.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); - } - } + /** + * Compute the hash to store the shape of the file + */ + export type ComputeHash = ((data: string) => string) | undefined; - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } + function getReferencedFilesFromImportedModuleSymbol(symbol: ts.Symbol): ts.Path[] { + return ts.mapDefined(symbol.declarations, declaration => ts.getSourceFileOfNode(declaration)?.resolvedPath); + } - const fileName = resolvedTypeReferenceDirective.resolvedFileName!; // TODO: GH#18217 - const typeFilePath = getReferencedFileFromFileName(program, fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(typeFilePath); - }); - } + /** + * Get the module source file and all augmenting files from the import name node from file + */ + function getReferencedFilesFromImportLiteral(checker: ts.TypeChecker, importName: ts.StringLiteralLike): ts.Path[] | undefined { + const symbol = checker.getSymbolAtLocation(importName); + return symbol && getReferencedFilesFromImportedModuleSymbol(symbol); + } - // Add module augmentation as references - if (sourceFile.moduleAugmentations.length) { - const checker = program.getTypeChecker(); - for (const moduleName of sourceFile.moduleAugmentations) { - if (!ts.isStringLiteral(moduleName)) - continue; - const symbol = checker.getSymbolAtLocation(moduleName); - if (!symbol) - continue; - - // Add any file other than our own as reference - addReferenceFromAmbientModule(symbol); - } - } + /** + * Gets the path to reference file from file name, it could be resolvedPath if present otherwise path + */ + function getReferencedFileFromFileName(program: ts.Program, fileName: string, sourceFileDirectory: ts.Path, getCanonicalFileName: ts.GetCanonicalFileName): ts.Path { + return ts.toPath(program.getProjectReferenceRedirect(fileName) || fileName, sourceFileDirectory, getCanonicalFileName); + } - // From ambient modules - for (const ambientModule of program.getTypeChecker().getAmbientModules()) { - if (ambientModule.declarations && ambientModule.declarations.length > 1) { - addReferenceFromAmbientModule(ambientModule); - } + /** + * 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: ts.Program, sourceFile: ts.SourceFile, getCanonicalFileName: ts.GetCanonicalFileName): ts.Set | undefined { + let referencedFiles: ts.Set | 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: ts.TypeChecker = program.getTypeChecker(); + for (const importName of sourceFile.imports) { + const declarationSourceFilePaths = getReferencedFilesFromImportLiteral(checker, importName); + declarationSourceFilePaths?.forEach(addReferencedFile); } + } - return referencedFiles; + const sourceFileDirectory = ts.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); + } + } - function addReferenceFromAmbientModule(symbol: ts.Symbol) { - if (!symbol.declarations) { + // 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 (!ts.isStringLiteral(moduleName)) + continue; + const symbol = checker.getSymbolAtLocation(moduleName); + if (!symbol) + continue; + // Add any file other than our own as reference - for (const declaration of symbol.declarations) { - const declarationSourceFile = ts.getSourceFileOfNode(declaration); - if (declarationSourceFile && - declarationSourceFile !== sourceFile) { - addReferencedFile(declarationSourceFile.resolvedPath); - } - } + addReferenceFromAmbientModule(symbol); } + } - function addReferencedFile(referencedPath: ts.Path) { - (referencedFiles || (referencedFiles = new ts.Set())).add(referencedPath); + // From ambient modules + for (const ambientModule of program.getTypeChecker().getAmbientModules()) { + if (ambientModule.declarations && ambientModule.declarations.length > 1) { + addReferenceFromAmbientModule(ambientModule); } } - /** - * Returns true if oldState is reusable, that is the emitKind = module/non module has not changed - */ - export function canReuseOldState(newReferencedMap: ReadonlyManyToManyPathMap | undefined, oldState: Readonly | undefined) { - return oldState && !oldState.referencedMap === !newReferencedMap; - } + return referencedFiles; - /** - * Creates the state of file references and signature for the new program from oldState if it is safe - */ - export function create(newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName, oldState?: Readonly, disableUseFileVersionAsSignature?: boolean): BuilderState { - const fileInfos = new ts.Map(); - const referencedMap = newProgram.getCompilerOptions().module !== ts.ModuleKind.None ? createManyToManyPathMap() : undefined; - const exportedModulesMap = referencedMap ? createManyToManyPathMap() : undefined; - const hasCalledUpdateShapeSignature = new ts.Set(); - const useOldState = canReuseOldState(referencedMap, oldState); - - // Ensure source files have parent pointers set - newProgram.getTypeChecker(); - - // Create the reference map, and set the file infos - for (const sourceFile of newProgram.getSourceFiles()) { - const version = ts.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!.getValues(sourceFile.resolvedPath); - if (exportedModules) { - exportedModulesMap!.set(sourceFile.resolvedPath, exportedModules); - } - } + function addReferenceFromAmbientModule(symbol: ts.Symbol) { + if (!symbol.declarations) { + return; + } + // Add any file other than our own as reference + for (const declaration of symbol.declarations) { + const declarationSourceFile = ts.getSourceFileOfNode(declaration); + if (declarationSourceFile && + declarationSourceFile !== sourceFile) { + addReferencedFile(declarationSourceFile.resolvedPath); } - fileInfos.set(sourceFile.resolvedPath, { version, signature: oldInfo && oldInfo.signature, affectsGlobalScope: isFileAffectingGlobalScope(sourceFile) || undefined, impliedFormat: sourceFile.impliedNodeFormat }); } - - return { - fileInfos, - referencedMap, - exportedModulesMap, - hasCalledUpdateShapeSignature, - useFileVersionAsSignature: !disableUseFileVersionAsSignature && !useOldState - }; } - /** - * Releases needed properties - */ - export function releaseCache(state: BuilderState) { - state.allFilesExcludingDefaultLibraryFile = undefined; - state.allFileNames = undefined; + function addReferencedFile(referencedPath: ts.Path) { + (referencedFiles || (referencedFiles = new ts.Set())).add(referencedPath); } + } - /** - * Creates a clone of the state - */ - export function clone(state: Readonly): BuilderState { - // Dont need to backup allFiles info since its cache anyway - return { - fileInfos: new ts.Map(state.fileInfos), - referencedMap: state.referencedMap?.clone(), - exportedModulesMap: state.exportedModulesMap?.clone(), - hasCalledUpdateShapeSignature: new ts.Set(state.hasCalledUpdateShapeSignature), - useFileVersionAsSignature: state.useFileVersionAsSignature, - }; - } + /** + * Returns true if oldState is reusable, that is the emitKind = module/non module has not changed + */ + export function canReuseOldState(newReferencedMap: ReadonlyManyToManyPathMap | undefined, oldState: Readonly | undefined) { + return oldState && !oldState.referencedMap === !newReferencedMap; + } - /** - * Gets the files affected by the path from the program - */ - export function getFilesAffectedBy(state: BuilderState, programOfThisState: ts.Program, path: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: ts.ESMap, exportedModulesMapCache?: ManyToManyPathMap): readonly ts.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 || new ts.Map(); - const sourceFile = programOfThisState.getSourceFileByPath(path); - if (!sourceFile) { - return ts.emptyArray; + /** + * Creates the state of file references and signature for the new program from oldState if it is safe + */ + export function create(newProgram: ts.Program, getCanonicalFileName: ts.GetCanonicalFileName, oldState?: Readonly, disableUseFileVersionAsSignature?: boolean): BuilderState { + const fileInfos = new ts.Map(); + const referencedMap = newProgram.getCompilerOptions().module !== ts.ModuleKind.None ? createManyToManyPathMap() : undefined; + const exportedModulesMap = referencedMap ? createManyToManyPathMap() : undefined; + const hasCalledUpdateShapeSignature = new ts.Set(); + const useOldState = canReuseOldState(referencedMap, oldState); + + // Ensure source files have parent pointers set + newProgram.getTypeChecker(); + + // Create the reference map, and set the file infos + for (const sourceFile of newProgram.getSourceFiles()) { + const version = ts.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!.getValues(sourceFile.resolvedPath); + if (exportedModules) { + exportedModulesMap!.set(sourceFile.resolvedPath, exportedModules); + } + } } + fileInfos.set(sourceFile.resolvedPath, { version, signature: oldInfo && oldInfo.signature, affectsGlobalScope: isFileAffectingGlobalScope(sourceFile) || undefined, impliedFormat: sourceFile.impliedNodeFormat }); + } - if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) { - return [sourceFile]; - } + return { + fileInfos, + referencedMap, + exportedModulesMap, + hasCalledUpdateShapeSignature, + useFileVersionAsSignature: !disableUseFileVersionAsSignature && !useOldState + }; + } - 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; + /** + * 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 { + // Dont need to backup allFiles info since its cache anyway + return { + fileInfos: new ts.Map(state.fileInfos), + referencedMap: state.referencedMap?.clone(), + exportedModulesMap: state.exportedModulesMap?.clone(), + hasCalledUpdateShapeSignature: new ts.Set(state.hasCalledUpdateShapeSignature), + useFileVersionAsSignature: state.useFileVersionAsSignature, + }; + } + + /** + * Gets the files affected by the path from the program + */ + export function getFilesAffectedBy(state: BuilderState, programOfThisState: ts.Program, path: ts.Path, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: ts.ESMap, exportedModulesMapCache?: ManyToManyPathMap): readonly ts.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 || new ts.Map(); + const sourceFile = programOfThisState.getSourceFileByPath(path); + if (!sourceFile) { + return ts.emptyArray; } - /** - * 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.ESMap) { - signatureCache.forEach((signature, path) => updateSignatureOfFile(state, signature, path)); + if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) { + return [sourceFile]; } - export function updateSignatureOfFile(state: BuilderState, signature: string | undefined, path: ts.Path) { - state.fileInfos.get(path)!.signature = signature; - state.hasCalledUpdateShapeSignature.add(path); + 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; + } - /** - * Returns if the shape of the signature has changed since last emit - */ - export function updateShapeSignature(state: Readonly, programOfThisState: ts.Program, sourceFile: ts.SourceFile, cacheToUpdateSignature: ts.ESMap, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ManyToManyPathMap, useFileVersionAsSignature: boolean = state.useFileVersionAsSignature) { - ts.Debug.assert(!!sourceFile); - ts.Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state"); + /** + * 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.ESMap) { + signatureCache.forEach((signature, path) => updateSignatureOfFile(state, signature, path)); + } - // 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; - } + export function updateSignatureOfFile(state: BuilderState, signature: string | undefined, path: ts.Path) { + state.fileInfos.get(path)!.signature = signature; + state.hasCalledUpdateShapeSignature.add(path); + } - const info = state.fileInfos.get(sourceFile.resolvedPath); - if (!info) - return ts.Debug.fail(); - - const prevSignature = info.signature; - let latestSignature: string | undefined; - if (!sourceFile.isDeclarationFile && !useFileVersionAsSignature) { - const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, - /*emitOnlyDtsFiles*/ true, cancellationToken, - /*customTransformers*/ undefined, - /*forceDtsEmit*/ true); - const firstDts = ts.firstOrUndefined(emitOutput.outputFiles); - if (firstDts) { - ts.Debug.assert(ts.isDeclarationFileName(firstDts.name), "File extension for signature expected to be dts", () => `Found: ${ts.getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`); - latestSignature = (computeHash || ts.generateDjb2Hash)(firstDts.text); - if (exportedModulesMapCache && latestSignature !== prevSignature) { - updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache); - } + /** + * Returns if the shape of the signature has changed since last emit + */ + export function updateShapeSignature(state: Readonly, programOfThisState: ts.Program, sourceFile: ts.SourceFile, cacheToUpdateSignature: ts.ESMap, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ManyToManyPathMap, useFileVersionAsSignature: boolean = state.useFileVersionAsSignature) { + ts.Debug.assert(!!sourceFile); + ts.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 ts.Debug.fail(); + + const prevSignature = info.signature; + let latestSignature: string | undefined; + if (!sourceFile.isDeclarationFile && !useFileVersionAsSignature) { + const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, + /*emitOnlyDtsFiles*/ true, cancellationToken, + /*customTransformers*/ undefined, + /*forceDtsEmit*/ true); + const firstDts = ts.firstOrUndefined(emitOutput.outputFiles); + if (firstDts) { + ts.Debug.assert(ts.isDeclarationFileName(firstDts.name), "File extension for signature expected to be dts", () => `Found: ${ts.getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`); + latestSignature = (computeHash || ts.generateDjb2Hash)(firstDts.text); + if (exportedModulesMapCache && latestSignature !== prevSignature) { + updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache); } } - // Default is to use file version as signature - if (latestSignature === undefined) { - latestSignature = sourceFile.version; - if (exportedModulesMapCache && latestSignature !== prevSignature) { - // All the references in this file are exported - const references = state.referencedMap ? state.referencedMap.getValues(sourceFile.resolvedPath) : undefined; - if (references) { - exportedModulesMapCache.set(sourceFile.resolvedPath, references); - } - else { - exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); - } + } + // Default is to use file version as signature + if (latestSignature === undefined) { + latestSignature = sourceFile.version; + if (exportedModulesMapCache && latestSignature !== prevSignature) { + // All the references in this file are exported + const references = state.referencedMap ? state.referencedMap.getValues(sourceFile.resolvedPath) : undefined; + if (references) { + exportedModulesMapCache.set(sourceFile.resolvedPath, references); + } + else { + exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); } } - cacheToUpdateSignature.set(sourceFile.resolvedPath, latestSignature); - return latestSignature !== prevSignature; } + cacheToUpdateSignature.set(sourceFile.resolvedPath, latestSignature); + return latestSignature !== prevSignature; + } - /** - * Coverts the declaration emit result into exported modules map - */ - export function updateExportedModules(sourceFile: ts.SourceFile, exportedModulesFromDeclarationEmit: ts.ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ManyToManyPathMap) { - if (!exportedModulesFromDeclarationEmit) { - exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); - return; - } + /** + * Coverts the declaration emit result into exported modules map + */ + export function updateExportedModules(sourceFile: ts.SourceFile, exportedModulesFromDeclarationEmit: ts.ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ManyToManyPathMap) { + if (!exportedModulesFromDeclarationEmit) { + exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); + return; + } - let exportedModules: ts.Set | undefined; - exportedModulesFromDeclarationEmit.forEach(symbol => addExportedModule(getReferencedFilesFromImportedModuleSymbol(symbol))); - if (exportedModules) { - exportedModulesMapCache.set(sourceFile.resolvedPath, exportedModules); - } - else { - exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); - } + let exportedModules: ts.Set | undefined; + exportedModulesFromDeclarationEmit.forEach(symbol => addExportedModule(getReferencedFilesFromImportedModuleSymbol(symbol))); + if (exportedModules) { + exportedModulesMapCache.set(sourceFile.resolvedPath, exportedModules); + } + else { + exportedModulesMapCache.deleteKey(sourceFile.resolvedPath); + } - function addExportedModule(exportedModulePaths: ts.Path[] | undefined) { - if (exportedModulePaths?.length) { - if (!exportedModules) { - exportedModules = new ts.Set(); - } - exportedModulePaths.forEach(path => exportedModules!.add(path)); + function addExportedModule(exportedModulePaths: ts.Path[] | undefined) { + if (exportedModulePaths?.length) { + if (!exportedModules) { + exportedModules = new ts.Set(); } + exportedModulePaths.forEach(path => exportedModules!.add(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: ManyToManyPathMap | undefined) { - if (exportedModulesMapCache) { - ts.Debug.assert(!!state.exportedModulesMap); - exportedModulesMapCache.deletedKeys()?.forEach(path => state.exportedModulesMap!.deleteKey(path)); - exportedModulesMapCache.forEach((exportedModules, path) => state.exportedModulesMap!.set(path, exportedModules)); - } + /** + * 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: ManyToManyPathMap | undefined) { + if (exportedModulesMapCache) { + ts.Debug.assert(!!state.exportedModulesMap); + exportedModulesMapCache.deletedKeys()?.forEach(path => state.exportedModulesMap!.deleteKey(path)); + exportedModulesMapCache.forEach((exportedModules, path) => state.exportedModulesMap!.set(path, exportedModules)); } + } - /** - * Get all the dependencies of the sourceFile - */ - export function getAllDependencies(state: BuilderState, programOfThisState: ts.Program, sourceFile: ts.SourceFile): readonly string[] { - const compilerOptions = programOfThisState.getCompilerOptions(); - // With --out or --outFile all outputs go into single file, all files depend on each other - if (ts.outFile(compilerOptions)) { - return getAllFileNames(state, programOfThisState); - } + /** + * Get all the dependencies of the sourceFile + */ + export function getAllDependencies(state: BuilderState, programOfThisState: ts.Program, sourceFile: ts.SourceFile): readonly string[] { + const compilerOptions = programOfThisState.getCompilerOptions(); + // With --out or --outFile all outputs go into single file, all files depend on each other + if (ts.outFile(compilerOptions)) { + 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); - } + // 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 = new ts.Set(); - const queue = [sourceFile.resolvedPath]; - while (queue.length) { - const path = queue.pop()!; - if (!seenMap.has(path)) { - seenMap.add(path); - const references = state.referencedMap.getValues(path); - if (references) { - const iterator = references.keys(); - for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { - queue.push(iterResult.value); - } + // Get the references, traversing deep from the referenceMap + const seenMap = new ts.Set(); + const queue = [sourceFile.resolvedPath]; + while (queue.length) { + const path = queue.pop()!; + if (!seenMap.has(path)) { + seenMap.add(path); + const references = state.referencedMap.getValues(path); + if (references) { + const iterator = references.keys(); + for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { + queue.push(iterResult.value); } } } - - return ts.arrayFrom(ts.mapDefinedIterator(seenMap.keys(), path => programOfThisState.getSourceFileByPath(path)?.fileName ?? path)); } - /** - * Gets the names of all files from the program - */ - function getAllFileNames(state: BuilderState, programOfThisState: ts.Program): readonly string[] { - if (!state.allFileNames) { - const sourceFiles = programOfThisState.getSourceFiles(); - state.allFileNames = sourceFiles === ts.emptyArray ? ts.emptyArray : sourceFiles.map(file => file.fileName); - } - return state.allFileNames; - } + return ts.arrayFrom(ts.mapDefinedIterator(seenMap.keys(), path => programOfThisState.getSourceFileByPath(path)?.fileName ?? path)); + } - /** - * Gets the files referenced by the the file path - */ - export function getReferencedByPaths(state: Readonly, referencedFilePath: ts.Path) { - const keys = state.referencedMap!.getKeys(referencedFilePath); - return keys ? ts.arrayFrom(keys.keys()) : []; + /** + * Gets the names of all files from the program + */ + function getAllFileNames(state: BuilderState, programOfThisState: ts.Program): readonly string[] { + if (!state.allFileNames) { + const sourceFiles = programOfThisState.getSourceFiles(); + state.allFileNames = sourceFiles === ts.emptyArray ? ts.emptyArray : sourceFiles.map(file => file.fileName); } + return state.allFileNames; + } - /** - * 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: ts.SourceFile) { - for (const statement of sourceFile.statements) { - if (!ts.isModuleWithStringLiteralName(statement)) { - return false; - } + /** + * Gets the files referenced by the the file path + */ + export function getReferencedByPaths(state: Readonly, referencedFilePath: ts.Path) { + const keys = state.referencedMap!.getKeys(referencedFilePath); + return keys ? ts.arrayFrom(keys.keys()) : []; + } + + /** + * 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: ts.SourceFile) { + for (const statement of sourceFile.statements) { + if (!ts.isModuleWithStringLiteralName(statement)) { + return false; } - return true; } + 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: ts.SourceFile) { - return ts.some(sourceFile.moduleAugmentations, augmentation => ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)); - } + /** + * 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: ts.SourceFile) { + return ts.some(sourceFile.moduleAugmentations, augmentation => ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)); + } - /** - * Return true if the file will invalidate all files because it affectes global scope - */ - function isFileAffectingGlobalScope(sourceFile: ts.SourceFile) { - return containsGlobalScopeAugmentation(sourceFile) || - !ts.isExternalOrCommonJsModule(sourceFile) && !ts.isJsonSourceFile(sourceFile) && !containsOnlyAmbientModules(sourceFile); + /** + * Return true if the file will invalidate all files because it affectes global scope + */ + function isFileAffectingGlobalScope(sourceFile: ts.SourceFile) { + return containsGlobalScopeAugmentation(sourceFile) || + !ts.isExternalOrCommonJsModule(sourceFile) && !ts.isJsonSourceFile(sourceFile) && !containsOnlyAmbientModules(sourceFile); + } + + /** + * Gets all files of the program excluding the default library file + */ + export function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: ts.Program, firstSourceFile: ts.SourceFile | undefined): readonly ts.SourceFile[] { + // Use cached result + if (state.allFilesExcludingDefaultLibraryFile) { + return state.allFilesExcludingDefaultLibraryFile; } - /** - * Gets all files of the program excluding the default library file - */ - export function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: ts.Program, firstSourceFile: ts.SourceFile | undefined): readonly ts.SourceFile[] { - // Use cached result - if (state.allFilesExcludingDefaultLibraryFile) { - return state.allFilesExcludingDefaultLibraryFile; + let result: ts.SourceFile[] | undefined; + if (firstSourceFile) + addSourceFile(firstSourceFile); + for (const sourceFile of programOfThisState.getSourceFiles()) { + if (sourceFile !== firstSourceFile) { + addSourceFile(sourceFile); } + } + state.allFilesExcludingDefaultLibraryFile = result || ts.emptyArray; + return state.allFilesExcludingDefaultLibraryFile; - let result: ts.SourceFile[] | undefined; - if (firstSourceFile) - addSourceFile(firstSourceFile); - for (const sourceFile of programOfThisState.getSourceFiles()) { - if (sourceFile !== firstSourceFile) { - addSourceFile(sourceFile); - } + function addSourceFile(sourceFile: ts.SourceFile) { + if (!programOfThisState.isSourceFileDefaultLibrary(sourceFile)) { + (result || (result = [])).push(sourceFile); } - state.allFilesExcludingDefaultLibraryFile = result || ts.emptyArray; - return state.allFilesExcludingDefaultLibraryFile; + } + } - function addSourceFile(sourceFile: ts.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: ts.Program, sourceFileWithUpdatedShape: ts.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 && ts.outFile(compilerOptions)) { + return [sourceFileWithUpdatedShape]; } + return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); + } - /** - * When program emits non modular code, gets the files affected by the sourceFile whose shape has changed - */ - function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(state: BuilderState, programOfThisState: ts.Program, sourceFileWithUpdatedShape: ts.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 && ts.outFile(compilerOptions)) { - return [sourceFileWithUpdatedShape]; - } + /** + * When program emits modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: ts.Program, sourceFileWithUpdatedShape: ts.SourceFile, cacheToUpdateSignature: ts.ESMap, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache: ManyToManyPathMap | 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: ts.Program, sourceFileWithUpdatedShape: ts.SourceFile, cacheToUpdateSignature: ts.ESMap, cancellationToken: ts.CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache: ManyToManyPathMap | undefined) { - if (isFileAffectingGlobalScope(sourceFileWithUpdatedShape)) { - return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); - } - - const compilerOptions = programOfThisState.getCompilerOptions(); - if (compilerOptions && (compilerOptions.isolatedModules || ts.outFile(compilerOptions))) { - return [sourceFileWithUpdatedShape]; - } + const compilerOptions = programOfThisState.getCompilerOptions(); + if (compilerOptions && (compilerOptions.isolatedModules || ts.outFile(compilerOptions))) { + 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 = new ts.Map(); - - // 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)) { - queue.push(...getReferencedByPaths(state, currentSourceFile.resolvedPath)); - } + // 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 = new ts.Map(); + + // 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)) { + queue.push(...getReferencedByPaths(state, currentSourceFile.resolvedPath)); } } - - // Return array of values that needs emit - return ts.arrayFrom(ts.mapDefinedIterator(seenFileNamesMap.values(), value => value)); } + + // Return array of values that needs emit + return ts.arrayFrom(ts.mapDefinedIterator(seenFileNamesMap.values(), value => value)); } } +} diff --git a/src/compiler/builderStatePublic.ts b/src/compiler/builderStatePublic.ts index 2b447d3264530..30fdcf41f04b3 100644 --- a/src/compiler/builderStatePublic.ts +++ b/src/compiler/builderStatePublic.ts @@ -1,14 +1,14 @@ namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - /* @internal */ diagnostics: readonly ts.Diagnostic[]; - /* @internal */ exportedModulesFromDeclarationEmit?: ts.ExportedModulesFromDeclarationEmit; - } +export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + /* @internal */ diagnostics: readonly ts.Diagnostic[]; + /* @internal */ exportedModulesFromDeclarationEmit?: ts.ExportedModulesFromDeclarationEmit; +} - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } +export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; +} } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d16a95b20c400..cf69f871fa52a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1,30092 +1,30115 @@ /* @internal */ namespace ts { - const ambientModuleSymbolRegex = /^".+"$/; - const anon = "(anonymous)" as ts.__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, - PossiblyOutOfBounds = 1 << 7, - - // 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 enum IterationTypeKind { - Yield, - Return, - Next - } - - interface IterationTypesResolver { - iterableCacheKey: "iterationTypesOfAsyncIterable" | "iterationTypesOfIterable"; - iteratorCacheKey: "iterationTypesOfAsyncIterator" | "iterationTypesOfIterator"; - iteratorSymbolName: "asyncIterator" | "iterator"; - getGlobalIteratorType: (reportErrors: boolean) => ts.GenericType; - getGlobalIterableType: (reportErrors: boolean) => ts.GenericType; - getGlobalIterableIteratorType: (reportErrors: boolean) => ts.GenericType; - getGlobalGeneratorType: (reportErrors: boolean) => ts.GenericType; - resolveIterationType: (type: ts.Type, errorNode: ts.Node | undefined) => ts.Type | undefined; - mustHaveANextMethodDiagnostic: ts.DiagnosticMessage; - mustBeAMethodDiagnostic: ts.DiagnosticMessage; - mustHaveAValueDiagnostic: ts.DiagnosticMessage; - } - - const enum WideningKind { - Normal, - FunctionReturn, - GeneratorNext, - GeneratorYield - } - - 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), - AllTypeofNE = TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | NEUndefined, - EmptyObjectFacts = All, - // Masks - OrFactsMask = TypeofEQFunction | TypeofNEObject, - AndFactsMask = All & ~OrFactsMask - } - - const typeofEQFacts: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ - 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 ambientModuleSymbolRegex = /^".+"$/; +const anon = "(anonymous)" as ts.__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, + PossiblyOutOfBounds = 1 << 7, + + // 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 typeofNEFacts: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ - string: TypeFacts.TypeofNEString, - number: TypeFacts.TypeofNENumber, - bigint: TypeFacts.TypeofNEBigInt, - boolean: TypeFacts.TypeofNEBoolean, - symbol: TypeFacts.TypeofNESymbol, - undefined: TypeFacts.NEUndefined, - object: TypeFacts.TypeofNEObject, - function: TypeFacts.TypeofNEFunction - })); +} - type TypeSystemEntity = ts.Node | ts.Symbol | ts.Type | ts.Signature; +const enum IterationTypeKind { + Yield, + Return, + Next +} - const enum TypeSystemPropertyName { - Type, - ResolvedBaseConstructorType, - DeclaredType, - ResolvedReturnType, - ImmediateBaseConstraint, - EnumTagType, - ResolvedTypeArguments, - ResolvedBaseTypes, - WriteType - } +interface IterationTypesResolver { + iterableCacheKey: "iterationTypesOfAsyncIterable" | "iterationTypesOfIterable"; + iteratorCacheKey: "iterationTypesOfAsyncIterator" | "iterationTypesOfIterator"; + iteratorSymbolName: "asyncIterator" | "iterator"; + getGlobalIteratorType: (reportErrors: boolean) => ts.GenericType; + getGlobalIterableType: (reportErrors: boolean) => ts.GenericType; + getGlobalIterableIteratorType: (reportErrors: boolean) => ts.GenericType; + getGlobalGeneratorType: (reportErrors: boolean) => ts.GenericType; + resolveIterationType: (type: ts.Type, errorNode: ts.Node | undefined) => ts.Type | undefined; + mustHaveANextMethodDiagnostic: ts.DiagnosticMessage; + mustBeAMethodDiagnostic: ts.DiagnosticMessage; + mustHaveAValueDiagnostic: ts.DiagnosticMessage; +} - const enum CheckMode { - Normal = 0, - Contextual = 1 << 0, - Inferential = 1 << 1, - SkipContextSensitive = 1 << 2, - SkipGenericFunctions = 1 << 3, - IsForSignatureHelp = 1 << 4, - IsForStringLiteralArgumentCompletions = 1 << 5, - RestBindingElement = 1 << 6 - } +const enum WideningKind { + Normal, + FunctionReturn, + GeneratorNext, + GeneratorYield +} - const enum SignatureCheckMode { - BivariantCallback = 1 << 0, - StrictCallback = 1 << 1, - IgnoreReturnTypes = 1 << 2, - StrictArity = 1 << 3, - Callback = BivariantCallback | StrictCallback - } +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), + AllTypeofNE = TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | NEUndefined, + EmptyObjectFacts = All, + // Masks + OrFactsMask = TypeofEQFunction | TypeofNEObject, + AndFactsMask = All & ~OrFactsMask +} - const enum IntersectionState { - None = 0, - Source = 1 << 0, - Target = 1 << 1, - PropertyCheck = 1 << 2, - InPropertyCheck = 1 << 3 - } +const typeofEQFacts: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ + 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: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ + string: TypeFacts.TypeofNEString, + number: TypeFacts.TypeofNENumber, + bigint: TypeFacts.TypeofNEBigInt, + boolean: TypeFacts.TypeofNEBoolean, + symbol: TypeFacts.TypeofNESymbol, + undefined: TypeFacts.NEUndefined, + object: TypeFacts.TypeofNEObject, + function: TypeFacts.TypeofNEFunction +})); + +type TypeSystemEntity = ts.Node | ts.Symbol | ts.Type | ts.Signature; + +const enum TypeSystemPropertyName { + Type, + ResolvedBaseConstructorType, + DeclaredType, + ResolvedReturnType, + ImmediateBaseConstraint, + EnumTagType, + ResolvedTypeArguments, + ResolvedBaseTypes, + WriteType +} - const enum RecursionFlags { - None = 0, - Source = 1 << 0, - Target = 1 << 1, - Both = Source | Target - } +const enum CheckMode { + Normal = 0, + Contextual = 1 << 0, + Inferential = 1 << 1, + SkipContextSensitive = 1 << 2, + SkipGenericFunctions = 1 << 3, + IsForSignatureHelp = 1 << 4, + IsForStringLiteralArgumentCompletions = 1 << 5, + RestBindingElement = 1 << 6 +} - const enum MappedTypeModifiers { - IncludeReadonly = 1 << 0, - ExcludeReadonly = 1 << 1, - IncludeOptional = 1 << 2, - ExcludeOptional = 1 << 3 - } +const enum SignatureCheckMode { + BivariantCallback = 1 << 0, + StrictCallback = 1 << 1, + IgnoreReturnTypes = 1 << 2, + StrictArity = 1 << 3, + Callback = BivariantCallback | StrictCallback +} - const enum ExpandingFlags { - None = 0, - Source = 1, - Target = 1 << 1, - Both = Source | Target - } +const enum IntersectionState { + None = 0, + Source = 1 << 0, + Target = 1 << 1, + PropertyCheck = 1 << 2, + InPropertyCheck = 1 << 3 +} - const enum MembersOrExportsResolutionKind { - resolvedExports = "resolvedExports", - resolvedMembers = "resolvedMembers" - } +const enum RecursionFlags { + None = 0, + Source = 1 << 0, + Target = 1 << 1, + Both = Source | Target +} - const enum UnusedKind { - Local, - Parameter - } +const enum MappedTypeModifiers { + IncludeReadonly = 1 << 0, + ExcludeReadonly = 1 << 1, + IncludeOptional = 1 << 2, + ExcludeOptional = 1 << 3 +} - /** @param containingNode Node to check for parse error */ - type AddUnusedDiagnostic = (containingNode: ts.Node, type: UnusedKind, diagnostic: ts.DiagnosticWithLocation) => void; - const isNotOverloadAndNotAccessor = ts.and(isNotOverload, isNotAccessor); +const enum ExpandingFlags { + None = 0, + Source = 1, + Target = 1 << 1, + Both = Source | Target +} - const enum DeclarationMeaning { - GetAccessor = 1, - SetAccessor = 2, - PropertyAssignment = 4, - Method = 8, - PrivateStatic = 16, - GetOrSetAccessor = GetAccessor | SetAccessor, - PropertyAssignmentOrMethod = PropertyAssignment | Method - } +const enum MembersOrExportsResolutionKind { + resolvedExports = "resolvedExports", + resolvedMembers = "resolvedMembers" +} - const enum DeclarationSpaces { - None = 0, - ExportValue = 1 << 0, - ExportType = 1 << 1, - ExportNamespace = 1 << 2 - } +const enum UnusedKind { + Local, + Parameter +} - const enum MinArgumentCountFlags { - None = 0, - StrongArityForUntypedJS = 1 << 0, - VoidIsNonOptional = 1 << 1 - } +/** @param containingNode Node to check for parse error */ +type AddUnusedDiagnostic = (containingNode: ts.Node, type: UnusedKind, diagnostic: ts.DiagnosticWithLocation) => void; +const isNotOverloadAndNotAccessor = ts.and(isNotOverload, isNotAccessor); + +const enum DeclarationMeaning { + GetAccessor = 1, + SetAccessor = 2, + PropertyAssignment = 4, + Method = 8, + PrivateStatic = 16, + GetOrSetAccessor = GetAccessor | SetAccessor, + PropertyAssignmentOrMethod = PropertyAssignment | Method +} - const enum IntrinsicTypeKind { - Uppercase, - Lowercase, - Capitalize, - Uncapitalize - } +const enum DeclarationSpaces { + None = 0, + ExportValue = 1 << 0, + ExportType = 1 << 1, + ExportNamespace = 1 << 2 +} - const intrinsicTypeKinds: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ - Uppercase: IntrinsicTypeKind.Uppercase, - Lowercase: IntrinsicTypeKind.Lowercase, - Capitalize: IntrinsicTypeKind.Capitalize, - Uncapitalize: IntrinsicTypeKind.Uncapitalize - })); +const enum MinArgumentCountFlags { + None = 0, + StrongArityForUntypedJS = 1 << 0, + VoidIsNonOptional = 1 << 1 +} - function SymbolLinks(this: ts.SymbolLinks) { - } +const enum IntrinsicTypeKind { + Uppercase, + Lowercase, + Capitalize, + Uncapitalize +} - function NodeLinks(this: ts.NodeLinks) { - this.flags = 0; - } +const intrinsicTypeKinds: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ + Uppercase: IntrinsicTypeKind.Uppercase, + Lowercase: IntrinsicTypeKind.Lowercase, + Capitalize: IntrinsicTypeKind.Capitalize, + Uncapitalize: IntrinsicTypeKind.Uncapitalize +})); - export function getNodeId(node: ts.Node): number { - if (!node.id) { - node.id = nextNodeId; - nextNodeId++; - } - return node.id; - } +function SymbolLinks(this: ts.SymbolLinks) { +} - export function getSymbolId(symbol: ts.Symbol): ts.SymbolId { - if (!symbol.id) { - symbol.id = nextSymbolId; - nextSymbolId++; - } +function NodeLinks(this: ts.NodeLinks) { + this.flags = 0; +} - return symbol.id; +export function getNodeId(node: ts.Node): number { + if (!node.id) { + node.id = nextNodeId; + nextNodeId++; } + return node.id; +} - export function isInstantiatedModule(node: ts.ModuleDeclaration, preserveConstEnums: boolean) { - const moduleState = ts.getModuleInstanceState(node); - return moduleState === ts.ModuleInstanceState.Instantiated || - (preserveConstEnums && moduleState === ts.ModuleInstanceState.ConstEnumOnly); +export function getSymbolId(symbol: ts.Symbol): ts.SymbolId { + if (!symbol.id) { + symbol.id = nextSymbolId; + nextSymbolId++; } - export function createTypeChecker(host: ts.TypeCheckerHost): ts.TypeChecker { - const getPackagesMap = ts.memoize(() => { - // A package name maps to true when we detect it has .d.ts files. - // This is useful as an approximation of whether a package bundles its own types. - // Note: we only look at files already found by module resolution, - // so there may be files we did not consider. - const map = new ts.Map(); - host.getSourceFiles().forEach(sf => { - if (!sf.resolvedModules) - return; - - sf.resolvedModules.forEach(r => { - if (r && r.packageId) - map.set(r.packageId.name, r.extension === ts.Extension.Dts || !!map.get(r.packageId.name)); - }); - }); - return map; - }); - - let deferredDiagnosticsCallbacks: (() => void)[] = []; + return symbol.id; +} - let addLazyDiagnostic = (arg: () => void) => { - deferredDiagnosticsCallbacks.push(arg); - }; +export function isInstantiatedModule(node: ts.ModuleDeclaration, preserveConstEnums: boolean) { + const moduleState = ts.getModuleInstanceState(node); + return moduleState === ts.ModuleInstanceState.Instantiated || + (preserveConstEnums && moduleState === ts.ModuleInstanceState.ConstEnumOnly); +} - // 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: ts.CancellationToken | undefined; - let requestedExternalEmitHelpers: ts.ExternalEmitHelpers; - let externalHelpersModule: ts.Symbol; - const Symbol = ts.objectAllocator.getSymbolConstructor(); - const Type = ts.objectAllocator.getTypeConstructor(); - const Signature = ts.objectAllocator.getSignatureConstructor(); - - let typeCount = 0; - let symbolCount = 0; - let enumCount = 0; - let totalInstantiationCount = 0; - let instantiationCount = 0; - let instantiationDepth = 0; - let inlineLevel = 0; - let currentNode: ts.Node | undefined; - let varianceTypeParameter: ts.TypeParameter | undefined; - const emptySymbols = ts.createSymbolTable(); - const arrayVariances = [ts.VarianceFlags.Covariant]; - - const compilerOptions = host.getCompilerOptions(); - const languageVersion = ts.getEmitScriptTarget(compilerOptions); - const moduleKind = ts.getEmitModuleKind(compilerOptions); - const useDefineForClassFields = ts.getUseDefineForClassFields(compilerOptions); - const allowSyntheticDefaultImports = ts.getAllowSyntheticDefaultImports(compilerOptions); - const strictNullChecks = ts.getStrictOptionValue(compilerOptions, "strictNullChecks"); - const strictFunctionTypes = ts.getStrictOptionValue(compilerOptions, "strictFunctionTypes"); - const strictBindCallApply = ts.getStrictOptionValue(compilerOptions, "strictBindCallApply"); - const strictPropertyInitialization = ts.getStrictOptionValue(compilerOptions, "strictPropertyInitialization"); - const noImplicitAny = ts.getStrictOptionValue(compilerOptions, "noImplicitAny"); - const noImplicitThis = ts.getStrictOptionValue(compilerOptions, "noImplicitThis"); - const useUnknownInCatchVariables = ts.getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables"); - const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; - const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ts.ObjectFlags.FreshLiteral; - const exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes; - - const checkBinaryExpression = createCheckBinaryExpression(); - const emitResolver = createResolver(); - const nodeBuilder = createNodeBuilder(); - - const globals = ts.createSymbolTable(); - const undefinedSymbol = createSymbol(ts.SymbolFlags.Property, "undefined" as ts.__String); - undefinedSymbol.declarations = []; - - const globalThisSymbol = createSymbol(ts.SymbolFlags.Module, "globalThis" as ts.__String, ts.CheckFlags.Readonly); - globalThisSymbol.exports = globals; - globalThisSymbol.declarations = []; - globals.set(globalThisSymbol.escapedName, globalThisSymbol); - - const argumentsSymbol = createSymbol(ts.SymbolFlags.Property, "arguments" as ts.__String); - const requireSymbol = createSymbol(ts.SymbolFlags.Property, "require" as ts.__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: ts.TypeChecker = { - getNodeCount: () => ts.sum(host.getSourceFiles(), "nodeCount"), - getIdentifierCount: () => ts.sum(host.getSourceFiles(), "identifierCount"), - getSymbolCount: () => ts.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, - getRecursionIdentity, - getUnmatchedProperties, - getTypeOfSymbolAtLocation: (symbol, locationIn) => { - const location = ts.getParseTreeNode(locationIn); - return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType; - }, - getTypeOfSymbol, - getSymbolsOfParameterPropertyDeclaration: (parameterIn, parameterName) => { - const parameter = ts.getParseTreeNode(parameterIn, ts.isParameter); - if (parameter === undefined) - return ts.Debug.fail("Cannot get symbols of a synthetic parameter that cannot be resolved to a parse-tree node."); - return getSymbolsOfParameterPropertyDeclaration(parameter, ts.escapeLeadingUnderscores(parameterName)); - }, - getDeclaredTypeOfSymbol, - getPropertiesOfType, - getPropertyOfType: (type, name) => getPropertyOfType(type, ts.escapeLeadingUnderscores(name)), - getPrivateIdentifierPropertyOfType: (leftType: ts.Type, name: string, location: ts.Node) => { - const node = ts.getParseTreeNode(location); - if (!node) { - return undefined; - } - const propName = ts.escapeLeadingUnderscores(name); - const lexicallyScopedIdentifier = lookupSymbolForPrivateIdentifierDeclaration(propName, node); - return lexicallyScopedIdentifier ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedIdentifier) : undefined; - }, - getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, ts.escapeLeadingUnderscores(name)), - getIndexInfoOfType: (type, kind) => getIndexInfoOfType(type, kind === ts.IndexKind.String ? stringType : numberType), - getIndexInfosOfType, - getSignaturesOfType, - getIndexTypeOfType: (type, kind) => getIndexTypeOfType(type, kind === ts.IndexKind.String ? stringType : numberType), - getIndexType: type => getIndexType(type), - getBaseTypes, - getBaseTypeOfLiteralType, - getWidenedType, - getTypeFromTypeNode: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isTypeNode); - return node ? getTypeFromTypeNode(node) : errorType; - }, - getParameterType: getTypeAtPosition, - getParameterIdentifierNameAtPosition, - getPromisedTypeOfPromise, - getAwaitedType: type => getAwaitedType(type), - 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: (locationIn, meaning) => { - const location = ts.getParseTreeNode(locationIn); - return location ? getSymbolsInScope(location, meaning) : []; - }, - getSymbolAtLocation: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - // set ignoreErrors: true because any lookups invoked by the API shouldn't cause any new errors - return node ? getSymbolAtLocation(node, /*ignoreErrors*/ true) : undefined; - }, - getIndexInfosAtLocation: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - return node ? getIndexInfosAtLocation(node) : undefined; - }, - getShorthandAssignmentValueSymbol: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - return node ? getShorthandAssignmentValueSymbol(node) : undefined; - }, - getExportSpecifierLocalTargetSymbol: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isExportSpecifier); - return node ? getExportSpecifierLocalTargetSymbol(node) : undefined; - }, - getExportSymbolOfSymbol(symbol) { - return getMergedSymbol(symbol.exportSymbol || symbol); - }, - getTypeAtLocation: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - return node ? getTypeOfNode(node) : errorType; - }, - getTypeOfAssignmentPattern: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isAssignmentPattern); - return node && getTypeOfAssignmentPattern(node) || errorType; - }, - getPropertySymbolOfDestructuringAssignment: locationIn => { - const location = ts.getParseTreeNode(locationIn, ts.isIdentifier); - return location ? getPropertySymbolOfDestructuringAssignment(location) : undefined; - }, - signatureToString: (signature, enclosingDeclaration, flags, kind) => { - return signatureToString(signature, ts.getParseTreeNode(enclosingDeclaration), flags, kind); - }, - typeToString: (type, enclosingDeclaration, flags) => { - return typeToString(type, ts.getParseTreeNode(enclosingDeclaration), flags); - }, - symbolToString: (symbol, enclosingDeclaration, meaning, flags) => { - return symbolToString(symbol, ts.getParseTreeNode(enclosingDeclaration), meaning, flags); - }, - typePredicateToString: (predicate, enclosingDeclaration, flags) => { - return typePredicateToString(predicate, ts.getParseTreeNode(enclosingDeclaration), flags); - }, - writeSignature: (signature, enclosingDeclaration, flags, kind, writer) => { - return signatureToString(signature, ts.getParseTreeNode(enclosingDeclaration), flags, kind, writer); - }, - writeType: (type, enclosingDeclaration, flags, writer) => { - return typeToString(type, ts.getParseTreeNode(enclosingDeclaration), flags, writer); - }, - writeSymbol: (symbol, enclosingDeclaration, meaning, flags, writer) => { - return symbolToString(symbol, ts.getParseTreeNode(enclosingDeclaration), meaning, flags, writer); - }, - writeTypePredicate: (predicate, enclosingDeclaration, flags, writer) => { - return typePredicateToString(predicate, ts.getParseTreeNode(enclosingDeclaration), flags, writer); - }, - getAugmentedPropertiesOfType, - getRootSymbols, - getSymbolOfExpando, - getContextualType: (nodeIn: ts.Expression, contextFlags?: ts.ContextFlags) => { - const node = ts.getParseTreeNode(nodeIn, ts.isExpression); - if (!node) { - return undefined; - } - if (contextFlags! & ts.ContextFlags.Completions) { - return runWithInferenceBlockedFromSourceNode(node, () => getContextualType(node, contextFlags)); - } - return getContextualType(node, contextFlags); - }, - getContextualTypeForObjectLiteralElement: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isObjectLiteralElementLike); - return node ? getContextualTypeForObjectLiteralElement(node) : undefined; - }, - getContextualTypeForArgumentAtIndex: (nodeIn, argIndex) => { - const node = ts.getParseTreeNode(nodeIn, ts.isCallLikeExpression); - return node && getContextualTypeForArgumentAtIndex(node, argIndex); - }, - getContextualTypeForJsxAttribute: (nodeIn) => { - const node = ts.getParseTreeNode(nodeIn, ts.isJsxAttributeLike); - return node && getContextualTypeForJsxAttribute(node); - }, - isContextSensitive, - getTypeOfPropertyOfContextualType, - getFullyQualifiedName, - getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal), - getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray) => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions, editingArgument), - getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp), - getExpandedParameters, - hasEffectiveRestParameter, - containsArgumentsReference, - getConstantValue: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, canHaveConstantValue); - return node ? getConstantValue(node) : undefined; - }, - isValidPropertyAccess: (nodeIn, propertyName) => { - const node = ts.getParseTreeNode(nodeIn, ts.isPropertyAccessOrQualifiedNameOrImportTypeNode); - return !!node && isValidPropertyAccess(node, ts.escapeLeadingUnderscores(propertyName)); - }, - isValidPropertyAccessForCompletions: (nodeIn, type, property) => { - const node = ts.getParseTreeNode(nodeIn, ts.isPropertyAccessExpression); - return !!node && isValidPropertyAccessForCompletions(node, type, property); - }, - getSignatureFromDeclaration: declarationIn => { - const declaration = ts.getParseTreeNode(declarationIn, ts.isFunctionLike); - return declaration ? getSignatureFromDeclaration(declaration) : undefined; - }, - isImplementationOfOverload: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isFunctionLike); - return node ? isImplementationOfOverload(node) : undefined; - }, - getImmediateAliasedSymbol, - getAliasedSymbol: resolveAlias, - getEmitResolver, - getExportsOfModule: getExportsOfModuleAsArray, - getExportsAndPropertiesOfModule, - forEachExportAndPropertyOfModule, - getSymbolWalker: ts.createGetSymbolWalker(getRestTypeOfSignature, getTypePredicateOfSignature, getReturnTypeOfSignature, getBaseTypes, resolveStructuredTypeMembers, getTypeOfSymbol, getResolvedSymbol, getConstraintOfTypeParameter, ts.getFirstIdentifier, getTypeArguments), - getAmbientModules, - getJsxIntrinsicTagNamesAt, - isOptionalParameter: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isParameter); - return node ? isOptionalParameter(node) : false; - }, - tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(ts.escapeLeadingUnderscores(name), symbol), - tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(ts.escapeLeadingUnderscores(name), symbol), - tryFindAmbientModule: moduleName => tryFindAmbientModule(moduleName, /*withAugmentations*/ true), - 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, - 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, - getPromiseType: () => getGlobalPromiseType(/*reportErrors*/ false), - getPromiseLikeType: () => getGlobalPromiseLikeType(/*reportErrors*/ false), - isSymbolAccessible, - isArrayType, - isTupleType, - isArrayLikeType, - isTypeInvalidDueToUnionDiscriminant, - getExactOptionalProperties, - getAllPossiblePropertiesOfTypes, - getSuggestedSymbolForNonexistentProperty, - getSuggestionForNonexistentProperty, - getSuggestedSymbolForNonexistentJSXAttribute, - getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, ts.escapeLeadingUnderscores(name), meaning), - getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, ts.escapeLeadingUnderscores(name), meaning), - getSuggestedSymbolForNonexistentModule, - getSuggestionForNonexistentExport, - getSuggestedSymbolForNonexistentClassMember, - getBaseConstraintOfType, - getDefaultFromTypeParameter: type => type && type.flags & ts.TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as ts.TypeParameter) : undefined, - resolveName(name, location, meaning, excludeGlobals) { - return resolveName(location, ts.escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals); - }, - getJsxNamespace: n => ts.unescapeLeadingUnderscores(getJsxNamespace(n)), - getJsxFragmentFactory: n => { - const jsxFragmentFactory = getJsxFragmentFactoryEntity(n); - return jsxFragmentFactory && ts.unescapeLeadingUnderscores(ts.getFirstIdentifier(jsxFragmentFactory).escapedText); - }, - getAccessibleSymbolChain, - getTypePredicateOfSignature, - resolveExternalModuleName: moduleSpecifierIn => { - const moduleSpecifier = ts.getParseTreeNode(moduleSpecifierIn, ts.isExpression); - return moduleSpecifier && resolveExternalModuleName(moduleSpecifier, moduleSpecifier, /*ignoreErrors*/ true); - }, - resolveExternalModuleSymbol, - tryGetThisTypeAt: (nodeIn, includeGlobalThis) => { - const node = ts.getParseTreeNode(nodeIn); - return node && tryGetThisTypeAt(node, includeGlobalThis); - }, - getTypeArgumentConstraint: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.isTypeNode); - return node && getTypeArgumentConstraint(node); - }, - getSuggestionDiagnostics: (fileIn, ct) => { - const file = ts.getParseTreeNode(fileIn, ts.isSourceFile) || ts.Debug.fail("Could not determine parsed source file."); - if (ts.skipTypeChecking(file, compilerOptions, host)) { - return ts.emptyArray; - } - - let diagnostics: ts.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, with _eager_ diagnostic production, so identifiers are registered as potentially unused - checkSourceFileWithEagerDiagnostics(file); - ts.Debug.assert(!!(getNodeLinks(file).flags & ts.NodeCheckFlags.TypeChecked)); - diagnostics = ts.addRange(diagnostics, suggestionDiagnostics.getDiagnostics(file.fileName)); - checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(file), (containingNode, kind, diag) => { - if (!ts.containsParseError(containingNode) && !unusedIsError(kind, !!(containingNode.flags & ts.NodeFlags.Ambient))) { - (diagnostics || (diagnostics = [])).push({ ...diag, category: ts.DiagnosticCategory.Suggestion }); - } - }); +export function createTypeChecker(host: ts.TypeCheckerHost): ts.TypeChecker { + const getPackagesMap = ts.memoize(() => { + // A package name maps to true when we detect it has .d.ts files. + // This is useful as an approximation of whether a package bundles its own types. + // Note: we only look at files already found by module resolution, + // so there may be files we did not consider. + const map = new ts.Map(); + host.getSourceFiles().forEach(sf => { + if (!sf.resolvedModules) + return; - return diagnostics || ts.emptyArray; - } - finally { - cancellationToken = undefined; - } - }, + sf.resolvedModules.forEach(r => { + if (r && r.packageId) + map.set(r.packageId.name, r.extension === ts.Extension.Dts || !!map.get(r.packageId.name)); + }); + }); + return map; + }); + + let deferredDiagnosticsCallbacks: (() => void)[] = []; + + let addLazyDiagnostic = (arg: () => void) => { + deferredDiagnosticsCallbacks.push(arg); + }; + + // 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: ts.CancellationToken | undefined; + let requestedExternalEmitHelpers: ts.ExternalEmitHelpers; + let externalHelpersModule: ts.Symbol; + const Symbol = ts.objectAllocator.getSymbolConstructor(); + const Type = ts.objectAllocator.getTypeConstructor(); + const Signature = ts.objectAllocator.getSignatureConstructor(); + + let typeCount = 0; + let symbolCount = 0; + let enumCount = 0; + let totalInstantiationCount = 0; + let instantiationCount = 0; + let instantiationDepth = 0; + let inlineLevel = 0; + let currentNode: ts.Node | undefined; + let varianceTypeParameter: ts.TypeParameter | undefined; + const emptySymbols = ts.createSymbolTable(); + const arrayVariances = [ts.VarianceFlags.Covariant]; + + const compilerOptions = host.getCompilerOptions(); + const languageVersion = ts.getEmitScriptTarget(compilerOptions); + const moduleKind = ts.getEmitModuleKind(compilerOptions); + const useDefineForClassFields = ts.getUseDefineForClassFields(compilerOptions); + const allowSyntheticDefaultImports = ts.getAllowSyntheticDefaultImports(compilerOptions); + const strictNullChecks = ts.getStrictOptionValue(compilerOptions, "strictNullChecks"); + const strictFunctionTypes = ts.getStrictOptionValue(compilerOptions, "strictFunctionTypes"); + const strictBindCallApply = ts.getStrictOptionValue(compilerOptions, "strictBindCallApply"); + const strictPropertyInitialization = ts.getStrictOptionValue(compilerOptions, "strictPropertyInitialization"); + const noImplicitAny = ts.getStrictOptionValue(compilerOptions, "noImplicitAny"); + const noImplicitThis = ts.getStrictOptionValue(compilerOptions, "noImplicitThis"); + const useUnknownInCatchVariables = ts.getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables"); + const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; + const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ts.ObjectFlags.FreshLiteral; + const exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes; + + const checkBinaryExpression = createCheckBinaryExpression(); + const emitResolver = createResolver(); + const nodeBuilder = createNodeBuilder(); + + const globals = ts.createSymbolTable(); + const undefinedSymbol = createSymbol(ts.SymbolFlags.Property, "undefined" as ts.__String); + undefinedSymbol.declarations = []; + + const globalThisSymbol = createSymbol(ts.SymbolFlags.Module, "globalThis" as ts.__String, ts.CheckFlags.Readonly); + globalThisSymbol.exports = globals; + globalThisSymbol.declarations = []; + globals.set(globalThisSymbol.escapedName, globalThisSymbol); + + const argumentsSymbol = createSymbol(ts.SymbolFlags.Property, "arguments" as ts.__String); + const requireSymbol = createSymbol(ts.SymbolFlags.Property, "require" as ts.__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: ts.TypeChecker = { + getNodeCount: () => ts.sum(host.getSourceFiles(), "nodeCount"), + getIdentifierCount: () => ts.sum(host.getSourceFiles(), "identifierCount"), + getSymbolCount: () => ts.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, + getRecursionIdentity, + getUnmatchedProperties, + getTypeOfSymbolAtLocation: (symbol, locationIn) => { + const location = ts.getParseTreeNode(locationIn); + return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType; + }, + getTypeOfSymbol, + getSymbolsOfParameterPropertyDeclaration: (parameterIn, parameterName) => { + const parameter = ts.getParseTreeNode(parameterIn, ts.isParameter); + if (parameter === undefined) + return ts.Debug.fail("Cannot get symbols of a synthetic parameter that cannot be resolved to a parse-tree node."); + return getSymbolsOfParameterPropertyDeclaration(parameter, ts.escapeLeadingUnderscores(parameterName)); + }, + getDeclaredTypeOfSymbol, + getPropertiesOfType, + getPropertyOfType: (type, name) => getPropertyOfType(type, ts.escapeLeadingUnderscores(name)), + getPrivateIdentifierPropertyOfType: (leftType: ts.Type, name: string, location: ts.Node) => { + const node = ts.getParseTreeNode(location); + if (!node) { + return undefined; + } + const propName = ts.escapeLeadingUnderscores(name); + const lexicallyScopedIdentifier = lookupSymbolForPrivateIdentifierDeclaration(propName, node); + return lexicallyScopedIdentifier ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedIdentifier) : undefined; + }, + getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, ts.escapeLeadingUnderscores(name)), + getIndexInfoOfType: (type, kind) => getIndexInfoOfType(type, kind === ts.IndexKind.String ? stringType : numberType), + getIndexInfosOfType, + getSignaturesOfType, + getIndexTypeOfType: (type, kind) => getIndexTypeOfType(type, kind === ts.IndexKind.String ? stringType : numberType), + getIndexType: type => getIndexType(type), + getBaseTypes, + getBaseTypeOfLiteralType, + getWidenedType, + getTypeFromTypeNode: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isTypeNode); + return node ? getTypeFromTypeNode(node) : errorType; + }, + getParameterType: getTypeAtPosition, + getParameterIdentifierNameAtPosition, + getPromisedTypeOfPromise, + getAwaitedType: type => getAwaitedType(type), + 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: (locationIn, meaning) => { + const location = ts.getParseTreeNode(locationIn); + return location ? getSymbolsInScope(location, meaning) : []; + }, + getSymbolAtLocation: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + // set ignoreErrors: true because any lookups invoked by the API shouldn't cause any new errors + return node ? getSymbolAtLocation(node, /*ignoreErrors*/ true) : undefined; + }, + getIndexInfosAtLocation: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + return node ? getIndexInfosAtLocation(node) : undefined; + }, + getShorthandAssignmentValueSymbol: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + return node ? getShorthandAssignmentValueSymbol(node) : undefined; + }, + getExportSpecifierLocalTargetSymbol: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isExportSpecifier); + return node ? getExportSpecifierLocalTargetSymbol(node) : undefined; + }, + getExportSymbolOfSymbol(symbol) { + return getMergedSymbol(symbol.exportSymbol || symbol); + }, + getTypeAtLocation: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + return node ? getTypeOfNode(node) : errorType; + }, + getTypeOfAssignmentPattern: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isAssignmentPattern); + return node && getTypeOfAssignmentPattern(node) || errorType; + }, + getPropertySymbolOfDestructuringAssignment: locationIn => { + const location = ts.getParseTreeNode(locationIn, ts.isIdentifier); + return location ? getPropertySymbolOfDestructuringAssignment(location) : undefined; + }, + signatureToString: (signature, enclosingDeclaration, flags, kind) => { + return signatureToString(signature, ts.getParseTreeNode(enclosingDeclaration), flags, kind); + }, + typeToString: (type, enclosingDeclaration, flags) => { + return typeToString(type, ts.getParseTreeNode(enclosingDeclaration), flags); + }, + symbolToString: (symbol, enclosingDeclaration, meaning, flags) => { + return symbolToString(symbol, ts.getParseTreeNode(enclosingDeclaration), meaning, flags); + }, + typePredicateToString: (predicate, enclosingDeclaration, flags) => { + return typePredicateToString(predicate, ts.getParseTreeNode(enclosingDeclaration), flags); + }, + writeSignature: (signature, enclosingDeclaration, flags, kind, writer) => { + return signatureToString(signature, ts.getParseTreeNode(enclosingDeclaration), flags, kind, writer); + }, + writeType: (type, enclosingDeclaration, flags, writer) => { + return typeToString(type, ts.getParseTreeNode(enclosingDeclaration), flags, writer); + }, + writeSymbol: (symbol, enclosingDeclaration, meaning, flags, writer) => { + return symbolToString(symbol, ts.getParseTreeNode(enclosingDeclaration), meaning, flags, writer); + }, + writeTypePredicate: (predicate, enclosingDeclaration, flags, writer) => { + return typePredicateToString(predicate, ts.getParseTreeNode(enclosingDeclaration), flags, writer); + }, + getAugmentedPropertiesOfType, + getRootSymbols, + getSymbolOfExpando, + getContextualType: (nodeIn: ts.Expression, contextFlags?: ts.ContextFlags) => { + const node = ts.getParseTreeNode(nodeIn, ts.isExpression); + if (!node) { + return undefined; + } + if (contextFlags! & ts.ContextFlags.Completions) { + return runWithInferenceBlockedFromSourceNode(node, () => getContextualType(node, contextFlags)); + } + return getContextualType(node, contextFlags); + }, + getContextualTypeForObjectLiteralElement: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isObjectLiteralElementLike); + return node ? getContextualTypeForObjectLiteralElement(node) : undefined; + }, + getContextualTypeForArgumentAtIndex: (nodeIn, argIndex) => { + const node = ts.getParseTreeNode(nodeIn, ts.isCallLikeExpression); + return node && getContextualTypeForArgumentAtIndex(node, argIndex); + }, + getContextualTypeForJsxAttribute: (nodeIn) => { + const node = ts.getParseTreeNode(nodeIn, ts.isJsxAttributeLike); + return node && getContextualTypeForJsxAttribute(node); + }, + isContextSensitive, + getTypeOfPropertyOfContextualType, + getFullyQualifiedName, + getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal), + getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray) => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions, editingArgument), + getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp), + getExpandedParameters, + hasEffectiveRestParameter, + containsArgumentsReference, + getConstantValue: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, canHaveConstantValue); + return node ? getConstantValue(node) : undefined; + }, + isValidPropertyAccess: (nodeIn, propertyName) => { + const node = ts.getParseTreeNode(nodeIn, ts.isPropertyAccessOrQualifiedNameOrImportTypeNode); + return !!node && isValidPropertyAccess(node, ts.escapeLeadingUnderscores(propertyName)); + }, + isValidPropertyAccessForCompletions: (nodeIn, type, property) => { + const node = ts.getParseTreeNode(nodeIn, ts.isPropertyAccessExpression); + return !!node && isValidPropertyAccessForCompletions(node, type, property); + }, + getSignatureFromDeclaration: declarationIn => { + const declaration = ts.getParseTreeNode(declarationIn, ts.isFunctionLike); + return declaration ? getSignatureFromDeclaration(declaration) : undefined; + }, + isImplementationOfOverload: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isFunctionLike); + return node ? isImplementationOfOverload(node) : undefined; + }, + getImmediateAliasedSymbol, + getAliasedSymbol: resolveAlias, + getEmitResolver, + getExportsOfModule: getExportsOfModuleAsArray, + getExportsAndPropertiesOfModule, + forEachExportAndPropertyOfModule, + getSymbolWalker: ts.createGetSymbolWalker(getRestTypeOfSignature, getTypePredicateOfSignature, getReturnTypeOfSignature, getBaseTypes, resolveStructuredTypeMembers, getTypeOfSymbol, getResolvedSymbol, getConstraintOfTypeParameter, ts.getFirstIdentifier, getTypeArguments), + getAmbientModules, + getJsxIntrinsicTagNamesAt, + isOptionalParameter: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isParameter); + return node ? isOptionalParameter(node) : false; + }, + tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(ts.escapeLeadingUnderscores(name), symbol), + tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(ts.escapeLeadingUnderscores(name), symbol), + tryFindAmbientModule: moduleName => tryFindAmbientModule(moduleName, /*withAugmentations*/ true), + 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, + 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, + getPromiseType: () => getGlobalPromiseType(/*reportErrors*/ false), + getPromiseLikeType: () => getGlobalPromiseLikeType(/*reportErrors*/ false), + isSymbolAccessible, + isArrayType, + isTupleType, + isArrayLikeType, + isTypeInvalidDueToUnionDiscriminant, + getExactOptionalProperties, + getAllPossiblePropertiesOfTypes, + getSuggestedSymbolForNonexistentProperty, + getSuggestionForNonexistentProperty, + getSuggestedSymbolForNonexistentJSXAttribute, + getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, ts.escapeLeadingUnderscores(name), meaning), + getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, ts.escapeLeadingUnderscores(name), meaning), + getSuggestedSymbolForNonexistentModule, + getSuggestionForNonexistentExport, + getSuggestedSymbolForNonexistentClassMember, + getBaseConstraintOfType, + getDefaultFromTypeParameter: type => type && type.flags & ts.TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as ts.TypeParameter) : undefined, + resolveName(name, location, meaning, excludeGlobals) { + return resolveName(location, ts.escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals); + }, + getJsxNamespace: n => ts.unescapeLeadingUnderscores(getJsxNamespace(n)), + getJsxFragmentFactory: n => { + const jsxFragmentFactory = getJsxFragmentFactoryEntity(n); + return jsxFragmentFactory && ts.unescapeLeadingUnderscores(ts.getFirstIdentifier(jsxFragmentFactory).escapedText); + }, + getAccessibleSymbolChain, + getTypePredicateOfSignature, + resolveExternalModuleName: moduleSpecifierIn => { + const moduleSpecifier = ts.getParseTreeNode(moduleSpecifierIn, ts.isExpression); + return moduleSpecifier && resolveExternalModuleName(moduleSpecifier, moduleSpecifier, /*ignoreErrors*/ true); + }, + resolveExternalModuleSymbol, + tryGetThisTypeAt: (nodeIn, includeGlobalThis) => { + const node = ts.getParseTreeNode(nodeIn); + return node && tryGetThisTypeAt(node, includeGlobalThis); + }, + getTypeArgumentConstraint: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.isTypeNode); + return node && getTypeArgumentConstraint(node); + }, + getSuggestionDiagnostics: (fileIn, ct) => { + const file = ts.getParseTreeNode(fileIn, ts.isSourceFile) || ts.Debug.fail("Could not determine parsed source file."); + if (ts.skipTypeChecking(file, compilerOptions, host)) { + return ts.emptyArray; + } - runWithCancellationToken: (token, callback) => { - try { - cancellationToken = token; - return callback(checker); - } - finally { - cancellationToken = undefined; - } - }, + let diagnostics: ts.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; - getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, - isDeclarationVisible, - isPropertyAccessible, - getTypeOnlyAliasDeclaration, - getMemberOverrideModifierStatus, - }; + // Ensure file is type checked, with _eager_ diagnostic production, so identifiers are registered as potentially unused + checkSourceFileWithEagerDiagnostics(file); + ts.Debug.assert(!!(getNodeLinks(file).flags & ts.NodeCheckFlags.TypeChecked)); + diagnostics = ts.addRange(diagnostics, suggestionDiagnostics.getDiagnostics(file.fileName)); + checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(file), (containingNode, kind, diag) => { + if (!ts.containsParseError(containingNode) && !unusedIsError(kind, !!(containingNode.flags & ts.NodeFlags.Ambient))) { + (diagnostics || (diagnostics = [])).push({ ...diag, category: ts.DiagnosticCategory.Suggestion }); + } + }); - function runWithInferenceBlockedFromSourceNode(node: ts.Node | undefined, fn: () => T): T { - const containingCall = ts.findAncestor(node, ts.isCallLikeExpression); - const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; - if (containingCall) { - let toMarkSkip = node!; - do { - getNodeLinks(toMarkSkip).skipDirectInference = true; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = undefined; - } - const result = fn(); - if (containingCall) { - let toMarkSkip = node!; - do { - getNodeLinks(toMarkSkip).skipDirectInference = undefined; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; + return diagnostics || ts.emptyArray; } - return result; - } - - function getResolvedSignatureWorker(nodeIn: ts.CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: ts.Node): ts.Signature | undefined { - const node = ts.getParseTreeNode(nodeIn, ts.isCallLikeExpression); - apparentArgumentCount = argumentCount; - const res = !node ? undefined : - editingArgument ? runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignature(node, candidatesOutArray, checkMode)) : - getResolvedSignature(node, candidatesOutArray, checkMode); - apparentArgumentCount = undefined; - return res; - } - - const tupleTypes = new ts.Map(); - const unionTypes = new ts.Map(); - const intersectionTypes = new ts.Map(); - const stringLiteralTypes = new ts.Map(); - const numberLiteralTypes = new ts.Map(); - const bigIntLiteralTypes = new ts.Map(); - const enumLiteralTypes = new ts.Map(); - const indexedAccessTypes = new ts.Map(); - const templateLiteralTypes = new ts.Map(); - const stringMappingTypes = new ts.Map(); - const substitutionTypes = new ts.Map(); - const subtypeReductionCache = new ts.Map(); - const evolvingArrayTypes: ts.EvolvingArrayType[] = []; - const undefinedProperties: ts.SymbolTable = new ts.Map(); - const markerTypes = new ts.Set(); - const unknownSymbol = createSymbol(ts.SymbolFlags.Property, "unknown" as ts.__String); - const resolvingSymbol = createSymbol(0, ts.InternalSymbolName.Resolving); - const unresolvedSymbols = new ts.Map(); - const errorTypes = new ts.Map(); - const anyType = createIntrinsicType(ts.TypeFlags.Any, "any"); - const autoType = createIntrinsicType(ts.TypeFlags.Any, "any"); - const wildcardType = createIntrinsicType(ts.TypeFlags.Any, "any"); - const errorType = createIntrinsicType(ts.TypeFlags.Any, "error"); - const unresolvedType = createIntrinsicType(ts.TypeFlags.Any, "unresolved"); - const nonInferrableAnyType = createIntrinsicType(ts.TypeFlags.Any, "any", ts.ObjectFlags.ContainsWideningType); - const intrinsicMarkerType = createIntrinsicType(ts.TypeFlags.Any, "intrinsic"); - const unknownType = createIntrinsicType(ts.TypeFlags.Unknown, "unknown"); - const nonNullUnknownType = createIntrinsicType(ts.TypeFlags.Unknown, "unknown"); - const undefinedType = createIntrinsicType(ts.TypeFlags.Undefined, "undefined"); - const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(ts.TypeFlags.Undefined, "undefined", ts.ObjectFlags.ContainsWideningType); - const optionalType = createIntrinsicType(ts.TypeFlags.Undefined, "undefined"); - const missingType = exactOptionalPropertyTypes ? createIntrinsicType(ts.TypeFlags.Undefined, "undefined") : undefinedType; - const nullType = createIntrinsicType(ts.TypeFlags.Null, "null"); - const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(ts.TypeFlags.Null, "null", ts.ObjectFlags.ContainsWideningType); - const stringType = createIntrinsicType(ts.TypeFlags.String, "string"); - const numberType = createIntrinsicType(ts.TypeFlags.Number, "number"); - const bigintType = createIntrinsicType(ts.TypeFlags.BigInt, "bigint"); - const falseType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "false") as ts.FreshableIntrinsicType; - const regularFalseType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "false") as ts.FreshableIntrinsicType; - const trueType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "true") as ts.FreshableIntrinsicType; - const regularTrueType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "true") as ts.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 = getUnionType([regularFalseType, regularTrueType]); - const esSymbolType = createIntrinsicType(ts.TypeFlags.ESSymbol, "symbol"); - const voidType = createIntrinsicType(ts.TypeFlags.Void, "void"); - const neverType = createIntrinsicType(ts.TypeFlags.Never, "never"); - const silentNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); - const nonInferrableType = createIntrinsicType(ts.TypeFlags.Never, "never", ts.ObjectFlags.NonInferrableType); - const implicitNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); - const unreachableNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); - const nonPrimitiveType = createIntrinsicType(ts.TypeFlags.NonPrimitive, "object"); - const stringOrNumberType = getUnionType([stringType, numberType]); - const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]); - const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; - const numberOrBigIntType = getUnionType([numberType, bigintType]); - const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as ts.UnionType; - const numericStringType = getTemplateLiteralType(["", ""], [numberType]); // The `${number}` type - - const restrictiveMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? getRestrictiveTypeParameter(t as ts.TypeParameter) : t); - const permissiveMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? wildcardType : t); - const uniqueLiteralType = createIntrinsicType(ts.TypeFlags.Never, "never"); // `uniqueLiteralType` is a special `never` flagged by union reduction to behave as a literal - const uniqueLiteralMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? uniqueLiteralType : t); // replace all type parameters with the unique literal type (disregarding constraints) - const emptyObjectType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - emptyJsxObjectType.objectFlags |= ts.ObjectFlags.JsxAttributes; - const emptyTypeLiteralSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); - emptyTypeLiteralSymbol.members = ts.createSymbolTable(); - const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const emptyGenericType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray) as ts.ObjectType as ts.GenericType; - emptyGenericType.instantiations = new ts.Map(); - const anyFunctionType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - // The anyFunctionType contains the anyFunctionType by definition. The flag is further propagated - // in getPropagatingFlagsOfTypes, and it is checked in inferFromTypes. - anyFunctionType.objectFlags |= ts.ObjectFlags.NonInferrableType; - const noConstraintType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const circularConstraintType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const resolvingDefaultType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - - const markerSuperType = createTypeParameter(); - const markerSubType = createTypeParameter(); - markerSubType.constraint = markerSuperType; - const markerOtherType = createTypeParameter(); - - const noTypePredicate = createTypePredicate(ts.TypePredicateKind.Identifier, "<>", 0, anyType); - const anySignature = createSignature(undefined, undefined, undefined, ts.emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); - const unknownSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); - const resolvingSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); - const silentNeverSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, silentNeverType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); - - const enumNumberIndexInfo = createIndexInfo(numberType, stringType, /*isReadonly*/ true); - - const iterationTypesCache = new ts.Map(); // cache for common IterationTypes instances - const noIterationTypes: ts.IterationTypes = { - get yieldType(): ts.Type { return ts.Debug.fail("Not supported"); }, - get returnType(): ts.Type { return ts.Debug.fail("Not supported"); }, - get nextType(): ts.Type { return ts.Debug.fail("Not supported"); }, - }; + finally { + cancellationToken = undefined; + } + }, - 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: ts.Diagnostics.An_async_iterator_must_have_a_next_method, - mustBeAMethodDiagnostic: ts.Diagnostics.The_0_property_of_an_async_iterator_must_be_a_method, - mustHaveAValueDiagnostic: ts.Diagnostics.The_type_returned_by_the_0_method_of_an_async_iterator_must_be_a_promise_for_a_type_with_a_value_property, - }; + runWithCancellationToken: (token, callback) => { + try { + cancellationToken = token; + return callback(checker); + } + finally { + cancellationToken = undefined; + } + }, + + getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, + isDeclarationVisible, + isPropertyAccessible, + getTypeOnlyAliasDeclaration, + getMemberOverrideModifierStatus, + }; + + function runWithInferenceBlockedFromSourceNode(node: ts.Node | undefined, fn: () => T): T { + const containingCall = ts.findAncestor(node, ts.isCallLikeExpression); + const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; + if (containingCall) { + let toMarkSkip = node!; + do { + getNodeLinks(toMarkSkip).skipDirectInference = true; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = undefined; + } + const result = fn(); + if (containingCall) { + let toMarkSkip = node!; + do { + getNodeLinks(toMarkSkip).skipDirectInference = undefined; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; + } + return result; + } - const syncIterationTypesResolver: IterationTypesResolver = { - iterableCacheKey: "iterationTypesOfIterable", - iteratorCacheKey: "iterationTypesOfIterator", - iteratorSymbolName: "iterator", - getGlobalIteratorType, - getGlobalIterableType, - getGlobalIterableIteratorType, - getGlobalGeneratorType, - resolveIterationType: (type, _errorNode) => type, - mustHaveANextMethodDiagnostic: ts.Diagnostics.An_iterator_must_have_a_next_method, - mustBeAMethodDiagnostic: ts.Diagnostics.The_0_property_of_an_iterator_must_be_a_method, - mustHaveAValueDiagnostic: ts.Diagnostics.The_type_returned_by_the_0_method_of_an_iterator_must_have_a_value_property, - }; + function getResolvedSignatureWorker(nodeIn: ts.CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: ts.Node): ts.Signature | undefined { + const node = ts.getParseTreeNode(nodeIn, ts.isCallLikeExpression); + apparentArgumentCount = argumentCount; + const res = !node ? undefined : + editingArgument ? runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignature(node, candidatesOutArray, checkMode)) : + getResolvedSignature(node, candidatesOutArray, checkMode); + apparentArgumentCount = undefined; + return res; + } - interface DuplicateInfoForSymbol { - readonly firstFileLocations: ts.Declaration[]; - readonly secondFileLocations: ts.Declaration[]; - readonly isBlockScoped: boolean; - } - interface DuplicateInfoForFiles { - readonly firstFile: ts.SourceFile; - readonly secondFile: ts.SourceFile; - /** Key is symbol name. */ - readonly conflictingSymbols: ts.ESMap; - } - /** Key is "/path/to/a.ts|/path/to/b.ts". */ - let amalgamatedDuplicates: ts.ESMap | undefined; - const reverseMappedCache = new ts.Map(); - let inInferTypeForHomomorphicMappedType = false; - 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: ts.PatternAmbientModule[]; - let patternAmbientModuleAugmentations: ts.ESMap | undefined; - let globalObjectType: ts.ObjectType; - let globalFunctionType: ts.ObjectType; - let globalCallableFunctionType: ts.ObjectType; - let globalNewableFunctionType: ts.ObjectType; - let globalArrayType: ts.GenericType; - let globalReadonlyArrayType: ts.GenericType; - let globalStringType: ts.ObjectType; - let globalNumberType: ts.ObjectType; - let globalBooleanType: ts.ObjectType; - let globalRegExpType: ts.ObjectType; - let globalThisType: ts.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 deferredGlobalESSymbolConstructorTypeSymbol: ts.Symbol | undefined; - let deferredGlobalESSymbolType: ts.ObjectType | undefined; - let deferredGlobalTypedPropertyDescriptorType: ts.GenericType; - let deferredGlobalPromiseType: ts.GenericType | undefined; - let deferredGlobalPromiseLikeType: ts.GenericType | undefined; - let deferredGlobalPromiseConstructorSymbol: ts.Symbol | undefined; - let deferredGlobalPromiseConstructorLikeType: ts.ObjectType | undefined; - let deferredGlobalIterableType: ts.GenericType | undefined; - let deferredGlobalIteratorType: ts.GenericType | undefined; - let deferredGlobalIterableIteratorType: ts.GenericType | undefined; - let deferredGlobalGeneratorType: ts.GenericType | undefined; - let deferredGlobalIteratorYieldResultType: ts.GenericType | undefined; - let deferredGlobalIteratorReturnResultType: ts.GenericType | undefined; - let deferredGlobalAsyncIterableType: ts.GenericType | undefined; - let deferredGlobalAsyncIteratorType: ts.GenericType | undefined; - let deferredGlobalAsyncIterableIteratorType: ts.GenericType | undefined; - let deferredGlobalAsyncGeneratorType: ts.GenericType | undefined; - let deferredGlobalTemplateStringsArrayType: ts.ObjectType | undefined; - let deferredGlobalImportMetaType: ts.ObjectType; - let deferredGlobalImportMetaExpressionType: ts.ObjectType; - let deferredGlobalImportCallOptionsType: ts.ObjectType | undefined; - let deferredGlobalExtractSymbol: ts.Symbol | undefined; - let deferredGlobalOmitSymbol: ts.Symbol | undefined; - let deferredGlobalAwaitedSymbol: ts.Symbol | undefined; - let deferredGlobalBigIntType: ts.ObjectType | undefined; - const allPotentiallyUnusedIdentifiers = new ts.Map(); // key is file name - - let flowLoopStart = 0; - let flowLoopCount = 0; - let sharedFlowCount = 0; - let flowAnalysisDisabled = false; - let flowInvocationCount = 0; - let lastFlowNode: ts.FlowNode | undefined; - let lastFlowNodeReachable: boolean; - let flowTypeCache: ts.Type[] | undefined; - - const emptyStringType = getStringLiteralType(""); - const zeroType = getNumberLiteralType(0); - const zeroBigIntType = getBigIntLiteralType({ 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: ts.SymbolLinks[] = []; - const nodeLinks: ts.NodeLinks[] = []; - const flowLoopCaches: ts.ESMap[] = []; - const flowLoopNodes: ts.FlowNode[] = []; - const flowLoopKeys: string[] = []; - const flowLoopTypes: ts.Type[][] = []; - const sharedFlowNodes: ts.FlowNode[] = []; - const sharedFlowTypes: ts.FlowType[] = []; - const flowNodeReachable: (boolean | undefined)[] = []; - const flowNodePostSuper: (boolean | undefined)[] = []; - const potentialThisCollisions: ts.Node[] = []; - const potentialNewTargetCollisions: ts.Node[] = []; - const potentialWeakMapSetCollisions: ts.Node[] = []; - const potentialReflectCollisions: ts.Node[] = []; - const awaitedTypeStack: number[] = []; - - const diagnostics = ts.createDiagnosticCollection(); - const suggestionDiagnostics = ts.createDiagnosticCollection(); - const typeofTypesByName: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ - string: stringType, - number: numberType, - bigint: bigintType, - boolean: booleanType, - symbol: esSymbolType, - undefined: undefinedType - })); - const typeofType = createTypeofType(); - - let _jsxNamespace: ts.__String; - let _jsxFactoryEntity: ts.EntityName | undefined; - let outofbandVarianceMarkerHandler: ((onlyUnreliable: boolean) => void) | undefined; - - const subtypeRelation = new ts.Map(); - const strictSubtypeRelation = new ts.Map(); - const assignableRelation = new ts.Map(); - const comparableRelation = new ts.Map(); - const identityRelation = new ts.Map(); - const enumRelation = new ts.Map(); - const builtinGlobals = ts.createSymbolTable(); - builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol); - - // Extensions suggested for path imports when module resolution is node16 or higher. - // The first element of each tuple is the extension a file has. - // The second element of each tuple is the extension that should be used in a path import. - // e.g. if we want to import file `foo.mts`, we should write `import {} from "./foo.mjs". - const suggestedExtensions: [ - string, - string - ][] = [ - [".mts", ".mjs"], - [".ts", ".js"], - [".cts", ".cjs"], - [".mjs", ".mjs"], - [".js", ".js"], - [".cjs", ".cjs"], - [".tsx", compilerOptions.jsx === ts.JsxEmit.Preserve ? ".jsx" : ".js"], - [".jsx", ".jsx"], - [".json", ".json"], - ]; - - initializeTypeChecker(); - - return checker; - - function getJsxNamespace(location: ts.Node | undefined): ts.__String { - if (location) { - const file = ts.getSourceFileOfNode(location); - if (file) { - if (ts.isJsxOpeningFragment(location)) { - if (file.localJsxFragmentNamespace) { - return file.localJsxFragmentNamespace; - } - const jsxFragmentPragma = file.pragmas.get("jsxfrag"); - if (jsxFragmentPragma) { - const chosenPragma = ts.isArray(jsxFragmentPragma) ? jsxFragmentPragma[0] : jsxFragmentPragma; - file.localJsxFragmentFactory = ts.parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion); - ts.visitNode(file.localJsxFragmentFactory, markAsSynthetic); - if (file.localJsxFragmentFactory) { - return file.localJsxFragmentNamespace = ts.getFirstIdentifier(file.localJsxFragmentFactory).escapedText; - } - } - const entity = getJsxFragmentFactoryEntity(location); - if (entity) { - file.localJsxFragmentFactory = entity; - return file.localJsxFragmentNamespace = ts.getFirstIdentifier(entity).escapedText; - } + const tupleTypes = new ts.Map(); + const unionTypes = new ts.Map(); + const intersectionTypes = new ts.Map(); + const stringLiteralTypes = new ts.Map(); + const numberLiteralTypes = new ts.Map(); + const bigIntLiteralTypes = new ts.Map(); + const enumLiteralTypes = new ts.Map(); + const indexedAccessTypes = new ts.Map(); + const templateLiteralTypes = new ts.Map(); + const stringMappingTypes = new ts.Map(); + const substitutionTypes = new ts.Map(); + const subtypeReductionCache = new ts.Map(); + const evolvingArrayTypes: ts.EvolvingArrayType[] = []; + const undefinedProperties: ts.SymbolTable = new ts.Map(); + const markerTypes = new ts.Set(); + const unknownSymbol = createSymbol(ts.SymbolFlags.Property, "unknown" as ts.__String); + const resolvingSymbol = createSymbol(0, ts.InternalSymbolName.Resolving); + const unresolvedSymbols = new ts.Map(); + const errorTypes = new ts.Map(); + const anyType = createIntrinsicType(ts.TypeFlags.Any, "any"); + const autoType = createIntrinsicType(ts.TypeFlags.Any, "any"); + const wildcardType = createIntrinsicType(ts.TypeFlags.Any, "any"); + const errorType = createIntrinsicType(ts.TypeFlags.Any, "error"); + const unresolvedType = createIntrinsicType(ts.TypeFlags.Any, "unresolved"); + const nonInferrableAnyType = createIntrinsicType(ts.TypeFlags.Any, "any", ts.ObjectFlags.ContainsWideningType); + const intrinsicMarkerType = createIntrinsicType(ts.TypeFlags.Any, "intrinsic"); + const unknownType = createIntrinsicType(ts.TypeFlags.Unknown, "unknown"); + const nonNullUnknownType = createIntrinsicType(ts.TypeFlags.Unknown, "unknown"); + const undefinedType = createIntrinsicType(ts.TypeFlags.Undefined, "undefined"); + const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(ts.TypeFlags.Undefined, "undefined", ts.ObjectFlags.ContainsWideningType); + const optionalType = createIntrinsicType(ts.TypeFlags.Undefined, "undefined"); + const missingType = exactOptionalPropertyTypes ? createIntrinsicType(ts.TypeFlags.Undefined, "undefined") : undefinedType; + const nullType = createIntrinsicType(ts.TypeFlags.Null, "null"); + const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(ts.TypeFlags.Null, "null", ts.ObjectFlags.ContainsWideningType); + const stringType = createIntrinsicType(ts.TypeFlags.String, "string"); + const numberType = createIntrinsicType(ts.TypeFlags.Number, "number"); + const bigintType = createIntrinsicType(ts.TypeFlags.BigInt, "bigint"); + const falseType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "false") as ts.FreshableIntrinsicType; + const regularFalseType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "false") as ts.FreshableIntrinsicType; + const trueType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "true") as ts.FreshableIntrinsicType; + const regularTrueType = createIntrinsicType(ts.TypeFlags.BooleanLiteral, "true") as ts.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 = getUnionType([regularFalseType, regularTrueType]); + const esSymbolType = createIntrinsicType(ts.TypeFlags.ESSymbol, "symbol"); + const voidType = createIntrinsicType(ts.TypeFlags.Void, "void"); + const neverType = createIntrinsicType(ts.TypeFlags.Never, "never"); + const silentNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); + const nonInferrableType = createIntrinsicType(ts.TypeFlags.Never, "never", ts.ObjectFlags.NonInferrableType); + const implicitNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); + const unreachableNeverType = createIntrinsicType(ts.TypeFlags.Never, "never"); + const nonPrimitiveType = createIntrinsicType(ts.TypeFlags.NonPrimitive, "object"); + const stringOrNumberType = getUnionType([stringType, numberType]); + const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]); + const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; + const numberOrBigIntType = getUnionType([numberType, bigintType]); + const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as ts.UnionType; + const numericStringType = getTemplateLiteralType(["", ""], [numberType]); // The `${number}` type + + const restrictiveMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? getRestrictiveTypeParameter(t as ts.TypeParameter) : t); + const permissiveMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? wildcardType : t); + const uniqueLiteralType = createIntrinsicType(ts.TypeFlags.Never, "never"); // `uniqueLiteralType` is a special `never` flagged by union reduction to behave as a literal + const uniqueLiteralMapper: ts.TypeMapper = makeFunctionTypeMapper(t => t.flags & ts.TypeFlags.TypeParameter ? uniqueLiteralType : t); // replace all type parameters with the unique literal type (disregarding constraints) + const emptyObjectType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + emptyJsxObjectType.objectFlags |= ts.ObjectFlags.JsxAttributes; + const emptyTypeLiteralSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); + emptyTypeLiteralSymbol.members = ts.createSymbolTable(); + const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const emptyGenericType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray) as ts.ObjectType as ts.GenericType; + emptyGenericType.instantiations = new ts.Map(); + const anyFunctionType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + // The anyFunctionType contains the anyFunctionType by definition. The flag is further propagated + // in getPropagatingFlagsOfTypes, and it is checked in inferFromTypes. + anyFunctionType.objectFlags |= ts.ObjectFlags.NonInferrableType; + const noConstraintType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const circularConstraintType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const resolvingDefaultType = createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + + const markerSuperType = createTypeParameter(); + const markerSubType = createTypeParameter(); + markerSubType.constraint = markerSuperType; + const markerOtherType = createTypeParameter(); + + const noTypePredicate = createTypePredicate(ts.TypePredicateKind.Identifier, "<>", 0, anyType); + const anySignature = createSignature(undefined, undefined, undefined, ts.emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); + const unknownSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); + const resolvingSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); + const silentNeverSignature = createSignature(undefined, undefined, undefined, ts.emptyArray, silentNeverType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); + + const enumNumberIndexInfo = createIndexInfo(numberType, stringType, /*isReadonly*/ true); + + const iterationTypesCache = new ts.Map(); // cache for common IterationTypes instances + const noIterationTypes: ts.IterationTypes = { + get yieldType(): ts.Type { return ts.Debug.fail("Not supported"); }, + get returnType(): ts.Type { return ts.Debug.fail("Not supported"); }, + get nextType(): ts.Type { return ts.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: ts.Diagnostics.An_async_iterator_must_have_a_next_method, + mustBeAMethodDiagnostic: ts.Diagnostics.The_0_property_of_an_async_iterator_must_be_a_method, + mustHaveAValueDiagnostic: ts.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: ts.Diagnostics.An_iterator_must_have_a_next_method, + mustBeAMethodDiagnostic: ts.Diagnostics.The_0_property_of_an_iterator_must_be_a_method, + mustHaveAValueDiagnostic: ts.Diagnostics.The_type_returned_by_the_0_method_of_an_iterator_must_have_a_value_property, + }; + + interface DuplicateInfoForSymbol { + readonly firstFileLocations: ts.Declaration[]; + readonly secondFileLocations: ts.Declaration[]; + readonly isBlockScoped: boolean; + } + interface DuplicateInfoForFiles { + readonly firstFile: ts.SourceFile; + readonly secondFile: ts.SourceFile; + /** Key is symbol name. */ + readonly conflictingSymbols: ts.ESMap; + } + /** Key is "/path/to/a.ts|/path/to/b.ts". */ + let amalgamatedDuplicates: ts.ESMap | undefined; + const reverseMappedCache = new ts.Map(); + let inInferTypeForHomomorphicMappedType = false; + 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: ts.PatternAmbientModule[]; + let patternAmbientModuleAugmentations: ts.ESMap | undefined; + let globalObjectType: ts.ObjectType; + let globalFunctionType: ts.ObjectType; + let globalCallableFunctionType: ts.ObjectType; + let globalNewableFunctionType: ts.ObjectType; + let globalArrayType: ts.GenericType; + let globalReadonlyArrayType: ts.GenericType; + let globalStringType: ts.ObjectType; + let globalNumberType: ts.ObjectType; + let globalBooleanType: ts.ObjectType; + let globalRegExpType: ts.ObjectType; + let globalThisType: ts.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 deferredGlobalESSymbolConstructorTypeSymbol: ts.Symbol | undefined; + let deferredGlobalESSymbolType: ts.ObjectType | undefined; + let deferredGlobalTypedPropertyDescriptorType: ts.GenericType; + let deferredGlobalPromiseType: ts.GenericType | undefined; + let deferredGlobalPromiseLikeType: ts.GenericType | undefined; + let deferredGlobalPromiseConstructorSymbol: ts.Symbol | undefined; + let deferredGlobalPromiseConstructorLikeType: ts.ObjectType | undefined; + let deferredGlobalIterableType: ts.GenericType | undefined; + let deferredGlobalIteratorType: ts.GenericType | undefined; + let deferredGlobalIterableIteratorType: ts.GenericType | undefined; + let deferredGlobalGeneratorType: ts.GenericType | undefined; + let deferredGlobalIteratorYieldResultType: ts.GenericType | undefined; + let deferredGlobalIteratorReturnResultType: ts.GenericType | undefined; + let deferredGlobalAsyncIterableType: ts.GenericType | undefined; + let deferredGlobalAsyncIteratorType: ts.GenericType | undefined; + let deferredGlobalAsyncIterableIteratorType: ts.GenericType | undefined; + let deferredGlobalAsyncGeneratorType: ts.GenericType | undefined; + let deferredGlobalTemplateStringsArrayType: ts.ObjectType | undefined; + let deferredGlobalImportMetaType: ts.ObjectType; + let deferredGlobalImportMetaExpressionType: ts.ObjectType; + let deferredGlobalImportCallOptionsType: ts.ObjectType | undefined; + let deferredGlobalExtractSymbol: ts.Symbol | undefined; + let deferredGlobalOmitSymbol: ts.Symbol | undefined; + let deferredGlobalAwaitedSymbol: ts.Symbol | undefined; + let deferredGlobalBigIntType: ts.ObjectType | undefined; + const allPotentiallyUnusedIdentifiers = new ts.Map(); // key is file name + + let flowLoopStart = 0; + let flowLoopCount = 0; + let sharedFlowCount = 0; + let flowAnalysisDisabled = false; + let flowInvocationCount = 0; + let lastFlowNode: ts.FlowNode | undefined; + let lastFlowNodeReachable: boolean; + let flowTypeCache: ts.Type[] | undefined; + + const emptyStringType = getStringLiteralType(""); + const zeroType = getNumberLiteralType(0); + const zeroBigIntType = getBigIntLiteralType({ 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: ts.SymbolLinks[] = []; + const nodeLinks: ts.NodeLinks[] = []; + const flowLoopCaches: ts.ESMap[] = []; + const flowLoopNodes: ts.FlowNode[] = []; + const flowLoopKeys: string[] = []; + const flowLoopTypes: ts.Type[][] = []; + const sharedFlowNodes: ts.FlowNode[] = []; + const sharedFlowTypes: ts.FlowType[] = []; + const flowNodeReachable: (boolean | undefined)[] = []; + const flowNodePostSuper: (boolean | undefined)[] = []; + const potentialThisCollisions: ts.Node[] = []; + const potentialNewTargetCollisions: ts.Node[] = []; + const potentialWeakMapSetCollisions: ts.Node[] = []; + const potentialReflectCollisions: ts.Node[] = []; + const awaitedTypeStack: number[] = []; + + const diagnostics = ts.createDiagnosticCollection(); + const suggestionDiagnostics = ts.createDiagnosticCollection(); + const typeofTypesByName: ts.ReadonlyESMap = new ts.Map(ts.getEntries({ + string: stringType, + number: numberType, + bigint: bigintType, + boolean: booleanType, + symbol: esSymbolType, + undefined: undefinedType + })); + const typeofType = createTypeofType(); + + let _jsxNamespace: ts.__String; + let _jsxFactoryEntity: ts.EntityName | undefined; + let outofbandVarianceMarkerHandler: ((onlyUnreliable: boolean) => void) | undefined; + + const subtypeRelation = new ts.Map(); + const strictSubtypeRelation = new ts.Map(); + const assignableRelation = new ts.Map(); + const comparableRelation = new ts.Map(); + const identityRelation = new ts.Map(); + const enumRelation = new ts.Map(); + const builtinGlobals = ts.createSymbolTable(); + builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol); + + // Extensions suggested for path imports when module resolution is node16 or higher. + // The first element of each tuple is the extension a file has. + // The second element of each tuple is the extension that should be used in a path import. + // e.g. if we want to import file `foo.mts`, we should write `import {} from "./foo.mjs". + const suggestedExtensions: [ + string, + string + ][] = [ + [".mts", ".mjs"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mjs", ".mjs"], + [".js", ".js"], + [".cjs", ".cjs"], + [".tsx", compilerOptions.jsx === ts.JsxEmit.Preserve ? ".jsx" : ".js"], + [".jsx", ".jsx"], + [".json", ".json"], + ]; + + initializeTypeChecker(); + + return checker; + + function getJsxNamespace(location: ts.Node | undefined): ts.__String { + if (location) { + const file = ts.getSourceFileOfNode(location); + if (file) { + if (ts.isJsxOpeningFragment(location)) { + if (file.localJsxFragmentNamespace) { + return file.localJsxFragmentNamespace; } - else { - const localJsxNamespace = getLocalJsxNamespace(file); - if (localJsxNamespace) { - return file.localJsxNamespace = localJsxNamespace; + const jsxFragmentPragma = file.pragmas.get("jsxfrag"); + if (jsxFragmentPragma) { + const chosenPragma = ts.isArray(jsxFragmentPragma) ? jsxFragmentPragma[0] : jsxFragmentPragma; + file.localJsxFragmentFactory = ts.parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion); + ts.visitNode(file.localJsxFragmentFactory, markAsSynthetic); + if (file.localJsxFragmentFactory) { + return file.localJsxFragmentNamespace = ts.getFirstIdentifier(file.localJsxFragmentFactory).escapedText; } } - } - } - if (!_jsxNamespace) { - _jsxNamespace = "React" as ts.__String; - if (compilerOptions.jsxFactory) { - _jsxFactoryEntity = ts.parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion); - ts.visitNode(_jsxFactoryEntity, markAsSynthetic); - if (_jsxFactoryEntity) { - _jsxNamespace = ts.getFirstIdentifier(_jsxFactoryEntity).escapedText; + const entity = getJsxFragmentFactoryEntity(location); + if (entity) { + file.localJsxFragmentFactory = entity; + return file.localJsxFragmentNamespace = ts.getFirstIdentifier(entity).escapedText; } } - else if (compilerOptions.reactNamespace) { - _jsxNamespace = ts.escapeLeadingUnderscores(compilerOptions.reactNamespace); + else { + const localJsxNamespace = getLocalJsxNamespace(file); + if (localJsxNamespace) { + return file.localJsxNamespace = localJsxNamespace; + } } } - if (!_jsxFactoryEntity) { - _jsxFactoryEntity = ts.factory.createQualifiedName(ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(_jsxNamespace)), "createElement"); - } - return _jsxNamespace; } - - function getLocalJsxNamespace(file: ts.SourceFile): ts.__String | undefined { - if (file.localJsxNamespace) { - return file.localJsxNamespace; - } - const jsxPragma = file.pragmas.get("jsx"); - if (jsxPragma) { - const chosenPragma = ts.isArray(jsxPragma) ? jsxPragma[0] : jsxPragma; - file.localJsxFactory = ts.parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion); - ts.visitNode(file.localJsxFactory, markAsSynthetic); - if (file.localJsxFactory) { - return file.localJsxNamespace = ts.getFirstIdentifier(file.localJsxFactory).escapedText; + if (!_jsxNamespace) { + _jsxNamespace = "React" as ts.__String; + if (compilerOptions.jsxFactory) { + _jsxFactoryEntity = ts.parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion); + ts.visitNode(_jsxFactoryEntity, markAsSynthetic); + if (_jsxFactoryEntity) { + _jsxNamespace = ts.getFirstIdentifier(_jsxFactoryEntity).escapedText; } } + else if (compilerOptions.reactNamespace) { + _jsxNamespace = ts.escapeLeadingUnderscores(compilerOptions.reactNamespace); + } } - - function markAsSynthetic(node: ts.Node): ts.VisitResult { - ts.setTextRangePosEnd(node, -1, -1); - return ts.visitEachChild(node, markAsSynthetic, ts.nullTransformationContext); + if (!_jsxFactoryEntity) { + _jsxFactoryEntity = ts.factory.createQualifiedName(ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(_jsxNamespace)), "createElement"); } + return _jsxNamespace; + } - function getEmitResolver(sourceFile: ts.SourceFile, cancellationToken: ts.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; + function getLocalJsxNamespace(file: ts.SourceFile): ts.__String | undefined { + if (file.localJsxNamespace) { + return file.localJsxNamespace; } - - function lookupOrIssueError(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { - const diagnostic = location - ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) - : ts.createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); - const existing = diagnostics.lookup(diagnostic); - if (existing) { - return existing; - } - else { - diagnostics.add(diagnostic); - return diagnostic; + const jsxPragma = file.pragmas.get("jsx"); + if (jsxPragma) { + const chosenPragma = ts.isArray(jsxPragma) ? jsxPragma[0] : jsxPragma; + file.localJsxFactory = ts.parseIsolatedEntityName(chosenPragma.arguments.factory, languageVersion); + ts.visitNode(file.localJsxFactory, markAsSynthetic); + if (file.localJsxFactory) { + return file.localJsxNamespace = ts.getFirstIdentifier(file.localJsxFactory).escapedText; } } + } - function errorSkippedOn(key: keyof ts.CompilerOptions, location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { - const diagnostic = error(location, message, arg0, arg1, arg2, arg3); - diagnostic.skippedOn = key; - return diagnostic; - } + function markAsSynthetic(node: ts.Node): ts.VisitResult { + ts.setTextRangePosEnd(node, -1, -1); + return ts.visitEachChild(node, markAsSynthetic, ts.nullTransformationContext); + } - function createError(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { - return location - ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) - : ts.createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); - } + function getEmitResolver(sourceFile: ts.SourceFile, cancellationToken: ts.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; + } - function error(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { - const diagnostic = createError(location, message, arg0, arg1, arg2, arg3); + function lookupOrIssueError(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { + const diagnostic = location + ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) + : ts.createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); + const existing = diagnostics.lookup(diagnostic); + if (existing) { + return existing; + } + else { diagnostics.add(diagnostic); return diagnostic; } + } - function addErrorOrSuggestion(isError: boolean, diagnostic: ts.Diagnostic) { - if (isError) { - diagnostics.add(diagnostic); - } - else { - suggestionDiagnostics.add({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }); - } + function errorSkippedOn(key: keyof ts.CompilerOptions, location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { + const diagnostic = error(location, message, arg0, arg1, arg2, arg3); + diagnostic.skippedOn = key; + return diagnostic; + } + + function createError(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { + return location + ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) + : ts.createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); + } + + function error(location: ts.Node | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.Diagnostic { + const diagnostic = createError(location, message, arg0, arg1, arg2, arg3); + diagnostics.add(diagnostic); + return diagnostic; + } + + function addErrorOrSuggestion(isError: boolean, diagnostic: ts.Diagnostic) { + if (isError) { + diagnostics.add(diagnostic); } - function errorOrSuggestion(isError: boolean, location: ts.Node, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { - // Pseudo-synthesized input node - if (location.pos < 0 || location.end < 0) { - if (!isError) { - return; // Drop suggestions (we have no span to suggest on) - } - // Issue errors globally - const file = ts.getSourceFileOfNode(location); - addErrorOrSuggestion(isError, "message" in message ? ts.createFileDiagnostic(file, 0, 0, message, arg0, arg1, arg2, arg3) : ts.createDiagnosticForFileFromMessageChain(file, message)); // eslint-disable-line no-in-operator - return; - } - addErrorOrSuggestion(isError, "message" in message ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : ts.createDiagnosticForNodeFromMessageChain(location, message)); // eslint-disable-line no-in-operator + else { + suggestionDiagnostics.add({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }); } - - function errorAndMaybeSuggestAwait(location: ts.Node, maybeMissingAwait: boolean, message: ts.DiagnosticMessage, arg0?: string | number | undefined, arg1?: string | number | undefined, arg2?: string | number | undefined, arg3?: string | number | undefined): ts.Diagnostic { - const diagnostic = error(location, message, arg0, arg1, arg2, arg3); - if (maybeMissingAwait) { - const related = ts.createDiagnosticForNode(location, ts.Diagnostics.Did_you_forget_to_use_await); - ts.addRelatedInfo(diagnostic, related); - } - return diagnostic; + } + function errorOrSuggestion(isError: boolean, location: ts.Node, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { + // Pseudo-synthesized input node + if (location.pos < 0 || location.end < 0) { + if (!isError) { + return; // Drop suggestions (we have no span to suggest on) + } + // Issue errors globally + const file = ts.getSourceFileOfNode(location); + addErrorOrSuggestion(isError, "message" in message ? ts.createFileDiagnostic(file, 0, 0, message, arg0, arg1, arg2, arg3) : ts.createDiagnosticForFileFromMessageChain(file, message)); // eslint-disable-line no-in-operator + return; } + addErrorOrSuggestion(isError, "message" in message ? ts.createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : ts.createDiagnosticForNodeFromMessageChain(location, message)); // eslint-disable-line no-in-operator + } - function addDeprecatedSuggestionWorker(declarations: ts.Node | ts.Node[], diagnostic: ts.DiagnosticWithLocation) { - const deprecatedTag = Array.isArray(declarations) ? ts.forEach(declarations, ts.getJSDocDeprecatedTag) : ts.getJSDocDeprecatedTag(declarations); - if (deprecatedTag) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(deprecatedTag, ts.Diagnostics.The_declaration_was_marked_as_deprecated_here)); - } - // We call `addRelatedInfo()` before adding the diagnostic to prevent duplicates. - suggestionDiagnostics.add(diagnostic); - return diagnostic; + function errorAndMaybeSuggestAwait(location: ts.Node, maybeMissingAwait: boolean, message: ts.DiagnosticMessage, arg0?: string | number | undefined, arg1?: string | number | undefined, arg2?: string | number | undefined, arg3?: string | number | undefined): ts.Diagnostic { + const diagnostic = error(location, message, arg0, arg1, arg2, arg3); + if (maybeMissingAwait) { + const related = ts.createDiagnosticForNode(location, ts.Diagnostics.Did_you_forget_to_use_await); + ts.addRelatedInfo(diagnostic, related); } + return diagnostic; + } - function isDeprecatedSymbol(symbol: ts.Symbol) { - return !!(getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Deprecated); + function addDeprecatedSuggestionWorker(declarations: ts.Node | ts.Node[], diagnostic: ts.DiagnosticWithLocation) { + const deprecatedTag = Array.isArray(declarations) ? ts.forEach(declarations, ts.getJSDocDeprecatedTag) : ts.getJSDocDeprecatedTag(declarations); + if (deprecatedTag) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(deprecatedTag, ts.Diagnostics.The_declaration_was_marked_as_deprecated_here)); } + // We call `addRelatedInfo()` before adding the diagnostic to prevent duplicates. + suggestionDiagnostics.add(diagnostic); + return diagnostic; + } - function addDeprecatedSuggestion(location: ts.Node, declarations: ts.Node[], deprecatedEntity: string) { - const diagnostic = ts.createDiagnosticForNode(location, ts.Diagnostics._0_is_deprecated, deprecatedEntity); - return addDeprecatedSuggestionWorker(declarations, diagnostic); - } + function isDeprecatedSymbol(symbol: ts.Symbol) { + return !!(getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Deprecated); + } - function addDeprecatedSuggestionWithSignature(location: ts.Node, declaration: ts.Node, deprecatedEntity: string | undefined, signatureString: string) { - const diagnostic = deprecatedEntity - ? ts.createDiagnosticForNode(location, ts.Diagnostics.The_signature_0_of_1_is_deprecated, signatureString, deprecatedEntity) - : ts.createDiagnosticForNode(location, ts.Diagnostics._0_is_deprecated, signatureString); - return addDeprecatedSuggestionWorker(declaration, diagnostic); - } + function addDeprecatedSuggestion(location: ts.Node, declarations: ts.Node[], deprecatedEntity: string) { + const diagnostic = ts.createDiagnosticForNode(location, ts.Diagnostics._0_is_deprecated, deprecatedEntity); + return addDeprecatedSuggestionWorker(declarations, diagnostic); + } - function createSymbol(flags: ts.SymbolFlags, name: ts.__String, checkFlags?: ts.CheckFlags) { - symbolCount++; - const symbol = (new Symbol(flags | ts.SymbolFlags.Transient, name) as ts.TransientSymbol); - symbol.checkFlags = checkFlags || 0; - return symbol; - } + function addDeprecatedSuggestionWithSignature(location: ts.Node, declaration: ts.Node, deprecatedEntity: string | undefined, signatureString: string) { + const diagnostic = deprecatedEntity + ? ts.createDiagnosticForNode(location, ts.Diagnostics.The_signature_0_of_1_is_deprecated, signatureString, deprecatedEntity) + : ts.createDiagnosticForNode(location, ts.Diagnostics._0_is_deprecated, signatureString); + return addDeprecatedSuggestionWorker(declaration, diagnostic); + } - function getExcludedSymbolFlags(flags: ts.SymbolFlags): ts.SymbolFlags { - let result: ts.SymbolFlags = 0; - if (flags & ts.SymbolFlags.BlockScopedVariable) - result |= ts.SymbolFlags.BlockScopedVariableExcludes; - if (flags & ts.SymbolFlags.FunctionScopedVariable) - result |= ts.SymbolFlags.FunctionScopedVariableExcludes; - if (flags & ts.SymbolFlags.Property) - result |= ts.SymbolFlags.PropertyExcludes; - if (flags & ts.SymbolFlags.EnumMember) - result |= ts.SymbolFlags.EnumMemberExcludes; - if (flags & ts.SymbolFlags.Function) - result |= ts.SymbolFlags.FunctionExcludes; - if (flags & ts.SymbolFlags.Class) - result |= ts.SymbolFlags.ClassExcludes; - if (flags & ts.SymbolFlags.Interface) - result |= ts.SymbolFlags.InterfaceExcludes; - if (flags & ts.SymbolFlags.RegularEnum) - result |= ts.SymbolFlags.RegularEnumExcludes; - if (flags & ts.SymbolFlags.ConstEnum) - result |= ts.SymbolFlags.ConstEnumExcludes; - if (flags & ts.SymbolFlags.ValueModule) - result |= ts.SymbolFlags.ValueModuleExcludes; - if (flags & ts.SymbolFlags.Method) - result |= ts.SymbolFlags.MethodExcludes; - if (flags & ts.SymbolFlags.GetAccessor) - result |= ts.SymbolFlags.GetAccessorExcludes; - if (flags & ts.SymbolFlags.SetAccessor) - result |= ts.SymbolFlags.SetAccessorExcludes; - if (flags & ts.SymbolFlags.TypeParameter) - result |= ts.SymbolFlags.TypeParameterExcludes; - if (flags & ts.SymbolFlags.TypeAlias) - result |= ts.SymbolFlags.TypeAliasExcludes; - if (flags & ts.SymbolFlags.Alias) - result |= ts.SymbolFlags.AliasExcludes; - return result; - } + function createSymbol(flags: ts.SymbolFlags, name: ts.__String, checkFlags?: ts.CheckFlags) { + symbolCount++; + const symbol = (new Symbol(flags | ts.SymbolFlags.Transient, name) as ts.TransientSymbol); + symbol.checkFlags = checkFlags || 0; + return symbol; + } - function recordMergedSymbol(target: ts.Symbol, source: ts.Symbol) { - if (!source.mergeId) { - source.mergeId = nextMergeId; - nextMergeId++; - } - mergedSymbols[source.mergeId] = target; - } + function getExcludedSymbolFlags(flags: ts.SymbolFlags): ts.SymbolFlags { + let result: ts.SymbolFlags = 0; + if (flags & ts.SymbolFlags.BlockScopedVariable) + result |= ts.SymbolFlags.BlockScopedVariableExcludes; + if (flags & ts.SymbolFlags.FunctionScopedVariable) + result |= ts.SymbolFlags.FunctionScopedVariableExcludes; + if (flags & ts.SymbolFlags.Property) + result |= ts.SymbolFlags.PropertyExcludes; + if (flags & ts.SymbolFlags.EnumMember) + result |= ts.SymbolFlags.EnumMemberExcludes; + if (flags & ts.SymbolFlags.Function) + result |= ts.SymbolFlags.FunctionExcludes; + if (flags & ts.SymbolFlags.Class) + result |= ts.SymbolFlags.ClassExcludes; + if (flags & ts.SymbolFlags.Interface) + result |= ts.SymbolFlags.InterfaceExcludes; + if (flags & ts.SymbolFlags.RegularEnum) + result |= ts.SymbolFlags.RegularEnumExcludes; + if (flags & ts.SymbolFlags.ConstEnum) + result |= ts.SymbolFlags.ConstEnumExcludes; + if (flags & ts.SymbolFlags.ValueModule) + result |= ts.SymbolFlags.ValueModuleExcludes; + if (flags & ts.SymbolFlags.Method) + result |= ts.SymbolFlags.MethodExcludes; + if (flags & ts.SymbolFlags.GetAccessor) + result |= ts.SymbolFlags.GetAccessorExcludes; + if (flags & ts.SymbolFlags.SetAccessor) + result |= ts.SymbolFlags.SetAccessorExcludes; + if (flags & ts.SymbolFlags.TypeParameter) + result |= ts.SymbolFlags.TypeParameterExcludes; + if (flags & ts.SymbolFlags.TypeAlias) + result |= ts.SymbolFlags.TypeAliasExcludes; + if (flags & ts.SymbolFlags.Alias) + result |= ts.SymbolFlags.AliasExcludes; + return result; + } - 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 = new ts.Map(symbol.members); - if (symbol.exports) - result.exports = new ts.Map(symbol.exports); - recordMergedSymbol(result, symbol); - return result; + function recordMergedSymbol(target: ts.Symbol, source: ts.Symbol) { + if (!source.mergeId) { + source.mergeId = nextMergeId; + nextMergeId++; } + mergedSymbols[source.mergeId] = target; + } - /** - * 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) & ts.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 & ts.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 & ts.SymbolFlags.ValueModule && target.flags & ts.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) { - ts.setValueDeclaration(target, source.valueDeclaration); - } - ts.addRange(target.declarations, source.declarations); - if (source.members) { - if (!target.members) - target.members = ts.createSymbolTable(); - mergeSymbolTable(target.members, source.members, unidirectional); - } - if (source.exports) { - if (!target.exports) - target.exports = ts.createSymbolTable(); - mergeSymbolTable(target.exports, source.exports, unidirectional); - } - if (!unidirectional) { - recordMergedSymbol(target, source); - } - } - else if (target.flags & ts.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(source.declarations && ts.getNameOfDeclaration(source.declarations[0]), ts.Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target)); - } - } - else { // error - const isEitherEnum = !!(target.flags & ts.SymbolFlags.Enum || source.flags & ts.SymbolFlags.Enum); - const isEitherBlockScoped = !!(target.flags & ts.SymbolFlags.BlockScopedVariable || source.flags & ts.SymbolFlags.BlockScopedVariable); - const message = isEitherEnum ? ts.Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations - : isEitherBlockScoped ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 - : ts.Diagnostics.Duplicate_identifier_0; - const sourceSymbolFile = source.declarations && ts.getSourceFileOfNode(source.declarations[0]); - const targetSymbolFile = target.declarations && ts.getSourceFileOfNode(target.declarations[0]); - const isSourcePlainJs = ts.isPlainJsFile(sourceSymbolFile, compilerOptions.checkJs); - const isTargetPlainJs = ts.isPlainJsFile(targetSymbolFile, compilerOptions.checkJs); - 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 = ts.comparePaths(sourceSymbolFile.path, targetSymbolFile.path) === ts.Comparison.LessThan ? sourceSymbolFile : targetSymbolFile; - const secondFile = firstFile === sourceSymbolFile ? targetSymbolFile : sourceSymbolFile; - const filesDuplicates = ts.getOrUpdate(amalgamatedDuplicates, `${firstFile.path}|${secondFile.path}`, () => ({ firstFile, secondFile, conflictingSymbols: new ts.Map() } as DuplicateInfoForFiles)); - const conflictingSymbolInfo = ts.getOrUpdate(filesDuplicates.conflictingSymbols, symbolName, () => ({ isBlockScoped: isEitherBlockScoped, firstFileLocations: [], secondFileLocations: [] } as DuplicateInfoForSymbol)); - if (!isSourcePlainJs) - addDuplicateLocations(conflictingSymbolInfo.firstFileLocations, source); - if (!isTargetPlainJs) - addDuplicateLocations(conflictingSymbolInfo.secondFileLocations, target); - } - else { - if (!isSourcePlainJs) - addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target); - if (!isTargetPlainJs) - addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source); + 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 = new ts.Map(symbol.members); + if (symbol.exports) + result.exports = new ts.Map(symbol.exports); + recordMergedSymbol(result, symbol); + return result; + } + + /** + * 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) & ts.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 & ts.SymbolFlags.Transient)) { + const resolvedTarget = resolveSymbol(target); + if (resolvedTarget === unknownSymbol) { + return source; } + target = cloneSymbol(resolvedTarget); } - return target; + // Javascript static-property-assignment declarations always merge, even though they are also values + if (source.flags & ts.SymbolFlags.ValueModule && target.flags & ts.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) { + ts.setValueDeclaration(target, source.valueDeclaration); + } + ts.addRange(target.declarations, source.declarations); + if (source.members) { + if (!target.members) + target.members = ts.createSymbolTable(); + mergeSymbolTable(target.members, source.members, unidirectional); + } + if (source.exports) { + if (!target.exports) + target.exports = ts.createSymbolTable(); + mergeSymbolTable(target.exports, source.exports, unidirectional); + } + if (!unidirectional) { + recordMergedSymbol(target, source); + } + } + else if (target.flags & ts.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(source.declarations && ts.getNameOfDeclaration(source.declarations[0]), ts.Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target)); + } + } + else { // error + const isEitherEnum = !!(target.flags & ts.SymbolFlags.Enum || source.flags & ts.SymbolFlags.Enum); + const isEitherBlockScoped = !!(target.flags & ts.SymbolFlags.BlockScopedVariable || source.flags & ts.SymbolFlags.BlockScopedVariable); + const message = isEitherEnum ? ts.Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations + : isEitherBlockScoped ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 + : ts.Diagnostics.Duplicate_identifier_0; + const sourceSymbolFile = source.declarations && ts.getSourceFileOfNode(source.declarations[0]); + const targetSymbolFile = target.declarations && ts.getSourceFileOfNode(target.declarations[0]); + const isSourcePlainJs = ts.isPlainJsFile(sourceSymbolFile, compilerOptions.checkJs); + const isTargetPlainJs = ts.isPlainJsFile(targetSymbolFile, compilerOptions.checkJs); + 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 = ts.comparePaths(sourceSymbolFile.path, targetSymbolFile.path) === ts.Comparison.LessThan ? sourceSymbolFile : targetSymbolFile; + const secondFile = firstFile === sourceSymbolFile ? targetSymbolFile : sourceSymbolFile; + const filesDuplicates = ts.getOrUpdate(amalgamatedDuplicates, `${firstFile.path}|${secondFile.path}`, () => ({ firstFile, secondFile, conflictingSymbols: new ts.Map() } as DuplicateInfoForFiles)); + const conflictingSymbolInfo = ts.getOrUpdate(filesDuplicates.conflictingSymbols, symbolName, () => ({ isBlockScoped: isEitherBlockScoped, firstFileLocations: [], secondFileLocations: [] } as DuplicateInfoForSymbol)); + if (!isSourcePlainJs) + addDuplicateLocations(conflictingSymbolInfo.firstFileLocations, source); + if (!isTargetPlainJs) + addDuplicateLocations(conflictingSymbolInfo.secondFileLocations, target); + } + else { + if (!isSourcePlainJs) + addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target); + if (!isTargetPlainJs) + addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source); + } + } + return target; - function addDuplicateLocations(locs: ts.Declaration[], symbol: ts.Symbol): void { - if (symbol.declarations) { - for (const decl of symbol.declarations) { - ts.pushIfUnique(locs, decl); - } + function addDuplicateLocations(locs: ts.Declaration[], symbol: ts.Symbol): void { + if (symbol.declarations) { + for (const decl of symbol.declarations) { + ts.pushIfUnique(locs, decl); } } } + } - function addDuplicateDeclarationErrorsForSymbols(target: ts.Symbol, message: ts.DiagnosticMessage, symbolName: string, source: ts.Symbol) { - ts.forEach(target.declarations, node => { - addDuplicateDeclarationError(node, message, symbolName, source.declarations); - }); - } + function addDuplicateDeclarationErrorsForSymbols(target: ts.Symbol, message: ts.DiagnosticMessage, symbolName: string, source: ts.Symbol) { + ts.forEach(target.declarations, node => { + addDuplicateDeclarationError(node, message, symbolName, source.declarations); + }); + } - function addDuplicateDeclarationError(node: ts.Declaration, message: ts.DiagnosticMessage, symbolName: string, relatedNodes: readonly ts.Declaration[] | undefined) { - const errorNode = (ts.getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? ts.getNameOfExpando(node) : ts.getNameOfDeclaration(node)) || node; - const err = lookupOrIssueError(errorNode, message, symbolName); - for (const relatedNode of relatedNodes || ts.emptyArray) { - const adjustedNode = (ts.getExpandoInitializer(relatedNode, /*isPrototypeAssignment*/ false) ? ts.getNameOfExpando(relatedNode) : ts.getNameOfDeclaration(relatedNode)) || relatedNode; - if (adjustedNode === errorNode) - continue; - err.relatedInformation = err.relatedInformation || []; - const leadingMessage = ts.createDiagnosticForNode(adjustedNode, ts.Diagnostics._0_was_also_declared_here, symbolName); - const followOnMessage = ts.createDiagnosticForNode(adjustedNode, ts.Diagnostics.and_here); - if (ts.length(err.relatedInformation) >= 5 || ts.some(err.relatedInformation, r => ts.compareDiagnostics(r, followOnMessage) === ts.Comparison.EqualTo || ts.compareDiagnostics(r, leadingMessage) === ts.Comparison.EqualTo)) - continue; - ts.addRelatedInfo(err, !ts.length(err.relatedInformation) ? leadingMessage : followOnMessage); - } + function addDuplicateDeclarationError(node: ts.Declaration, message: ts.DiagnosticMessage, symbolName: string, relatedNodes: readonly ts.Declaration[] | undefined) { + const errorNode = (ts.getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? ts.getNameOfExpando(node) : ts.getNameOfDeclaration(node)) || node; + const err = lookupOrIssueError(errorNode, message, symbolName); + for (const relatedNode of relatedNodes || ts.emptyArray) { + const adjustedNode = (ts.getExpandoInitializer(relatedNode, /*isPrototypeAssignment*/ false) ? ts.getNameOfExpando(relatedNode) : ts.getNameOfDeclaration(relatedNode)) || relatedNode; + if (adjustedNode === errorNode) + continue; + err.relatedInformation = err.relatedInformation || []; + const leadingMessage = ts.createDiagnosticForNode(adjustedNode, ts.Diagnostics._0_was_also_declared_here, symbolName); + const followOnMessage = ts.createDiagnosticForNode(adjustedNode, ts.Diagnostics.and_here); + if (ts.length(err.relatedInformation) >= 5 || ts.some(err.relatedInformation, r => ts.compareDiagnostics(r, followOnMessage) === ts.Comparison.EqualTo || ts.compareDiagnostics(r, leadingMessage) === ts.Comparison.EqualTo)) + continue; + ts.addRelatedInfo(err, !ts.length(err.relatedInformation) ? leadingMessage : followOnMessage); } + } - function combineSymbolTables(first: ts.SymbolTable | undefined, second: ts.SymbolTable | undefined): ts.SymbolTable | undefined { - if (!first?.size) - return second; - if (!second?.size) - return first; - const combined = ts.createSymbolTable(); - mergeSymbolTable(combined, first); - mergeSymbolTable(combined, second); - return combined; - } + function combineSymbolTables(first: ts.SymbolTable | undefined, second: ts.SymbolTable | undefined): ts.SymbolTable | undefined { + if (!first?.size) + return second; + if (!second?.size) + return first; + const combined = ts.createSymbolTable(); + mergeSymbolTable(combined, first); + mergeSymbolTable(combined, second); + return combined; + } - function mergeSymbolTable(target: ts.SymbolTable, source: ts.SymbolTable, unidirectional = false) { - source.forEach((sourceSymbol, id) => { - const targetSymbol = target.get(id); - target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); - }); + function mergeSymbolTable(target: ts.SymbolTable, source: ts.SymbolTable, unidirectional = false) { + source.forEach((sourceSymbol, id) => { + const targetSymbol = target.get(id); + target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); + }); + } + + function mergeModuleAugmentation(moduleName: ts.StringLiteral | ts.Identifier): void { + const moduleAugmentation = moduleName.parent as ts.ModuleDeclaration; + 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 + ts.Debug.assert(moduleAugmentation.symbol.declarations!.length > 1); + return; } - function mergeModuleAugmentation(moduleName: ts.StringLiteral | ts.Identifier): void { - const moduleAugmentation = moduleName.parent as ts.ModuleDeclaration; - 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 - ts.Debug.assert(moduleAugmentation.symbol.declarations!.length > 1); + if (ts.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 & ts.NodeFlags.Ambient) + ? ts.Diagnostics.Invalid_module_name_in_augmentation_module_0_cannot_be_found + : undefined; + let mainModule = resolveExternalModuleNameWorker(moduleName, moduleName, moduleNotFoundError, /*isForAugmentation*/ true); + if (!mainModule) { return; } - - if (ts.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 & ts.NodeFlags.Ambient) - ? ts.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 & ts.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 (ts.some(patternAmbientModules, module => mainModule === module.symbol)) { - const merged = mergeSymbol(moduleAugmentation.symbol, mainModule, /*unidirectional*/ true); - if (!patternAmbientModuleAugmentations) { - patternAmbientModuleAugmentations = new ts.Map(); - } - // moduleName will be a StringLiteral since this is not `declare global`. - patternAmbientModuleAugmentations.set((moduleName as ts.StringLiteral).text, merged); + // obtain item referenced by 'export=' + mainModule = resolveExternalModuleSymbol(mainModule); + if (mainModule.flags & ts.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 (ts.some(patternAmbientModules, module => mainModule === module.symbol)) { + const merged = mergeSymbol(moduleAugmentation.symbol, mainModule, /*unidirectional*/ true); + if (!patternAmbientModuleAugmentations) { + patternAmbientModuleAugmentations = new ts.Map(); } - else { - if (mainModule.exports?.get(ts.InternalSymbolName.ExportStar) && moduleAugmentation.symbol.exports?.size) { - // We may need to merge the module augmentation's exports into the target symbols of the resolved exports - const resolvedExports = getResolvedMembersOrExportsOfSymbol(mainModule, MembersOrExportsResolutionKind.resolvedExports); - for (const [key, value] of ts.arrayFrom(moduleAugmentation.symbol.exports.entries())) { - if (resolvedExports.has(key) && !mainModule.exports.has(key)) { - mergeSymbol(resolvedExports.get(key)!, value); - } + // moduleName will be a StringLiteral since this is not `declare global`. + patternAmbientModuleAugmentations.set((moduleName as ts.StringLiteral).text, merged); + } + else { + if (mainModule.exports?.get(ts.InternalSymbolName.ExportStar) && moduleAugmentation.symbol.exports?.size) { + // We may need to merge the module augmentation's exports into the target symbols of the resolved exports + const resolvedExports = getResolvedMembersOrExportsOfSymbol(mainModule, MembersOrExportsResolutionKind.resolvedExports); + for (const [key, value] of ts.arrayFrom(moduleAugmentation.symbol.exports.entries())) { + if (resolvedExports.has(key) && !mainModule.exports.has(key)) { + mergeSymbol(resolvedExports.get(key)!, value); } } - mergeSymbol(mainModule, moduleAugmentation.symbol); } + mergeSymbol(mainModule, moduleAugmentation.symbol); } - else { - // moduleName will be a StringLiteral since this is not `declare global`. - error(moduleName, ts.Diagnostics.Cannot_augment_module_0_because_it_resolves_to_a_non_module_entity, (moduleName as ts.StringLiteral).text); - } + } + else { + // moduleName will be a StringLiteral since this is not `declare global`. + error(moduleName, ts.Diagnostics.Cannot_augment_module_0_because_it_resolves_to_a_non_module_entity, (moduleName as ts.StringLiteral).text); } } + } - function addToSymbolTable(target: ts.SymbolTable, source: ts.SymbolTable, message: ts.DiagnosticMessage) { - source.forEach((sourceSymbol, id) => { - const targetSymbol = target.get(id); - if (targetSymbol) { - // Error on redeclarations - ts.forEach(targetSymbol.declarations, addDeclarationDiagnostic(ts.unescapeLeadingUnderscores(id), message)); - } - else { - target.set(id, sourceSymbol); - } - }); - - function addDeclarationDiagnostic(id: string, message: ts.DiagnosticMessage) { - return (declaration: ts.Declaration) => diagnostics.add(ts.createDiagnosticForNode(declaration, message, id)); + function addToSymbolTable(target: ts.SymbolTable, source: ts.SymbolTable, message: ts.DiagnosticMessage) { + source.forEach((sourceSymbol, id) => { + const targetSymbol = target.get(id); + if (targetSymbol) { + // Error on redeclarations + ts.forEach(targetSymbol.declarations, addDeclarationDiagnostic(ts.unescapeLeadingUnderscores(id), message)); } - } + else { + target.set(id, sourceSymbol); + } + }); - function getSymbolLinks(symbol: ts.Symbol): ts.SymbolLinks { - if (symbol.flags & ts.SymbolFlags.Transient) - return symbol as ts.TransientSymbol; - const id = getSymbolId(symbol); - return symbolLinks[id] || (symbolLinks[id] = new (SymbolLinks as any)()); + function addDeclarationDiagnostic(id: string, message: ts.DiagnosticMessage) { + return (declaration: ts.Declaration) => diagnostics.add(ts.createDiagnosticForNode(declaration, message, id)); } + } - function getNodeLinks(node: ts.Node): ts.NodeLinks { - const nodeId = getNodeId(node); - return nodeLinks[nodeId] || (nodeLinks[nodeId] = new (NodeLinks as any)()); - } + function getSymbolLinks(symbol: ts.Symbol): ts.SymbolLinks { + if (symbol.flags & ts.SymbolFlags.Transient) + return symbol as ts.TransientSymbol; + const id = getSymbolId(symbol); + return symbolLinks[id] || (symbolLinks[id] = new (SymbolLinks as any)()); + } - function isGlobalSourceFile(node: ts.Node) { - return node.kind === ts.SyntaxKind.SourceFile && !ts.isExternalOrCommonJsModule(node as ts.SourceFile); - } + function getNodeLinks(node: ts.Node): ts.NodeLinks { + const nodeId = getNodeId(node); + return nodeLinks[nodeId] || (nodeLinks[nodeId] = new (NodeLinks as any)()); + } - function getSymbol(symbols: ts.SymbolTable, name: ts.__String, meaning: ts.SymbolFlags): ts.Symbol | undefined { - if (meaning) { - const symbol = getMergedSymbol(symbols.get(name)); - if (symbol) { - ts.Debug.assert((ts.getCheckFlags(symbol) & ts.CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); - if (symbol.flags & meaning) { + function isGlobalSourceFile(node: ts.Node) { + return node.kind === ts.SyntaxKind.SourceFile && !ts.isExternalOrCommonJsModule(node as ts.SourceFile); + } + + function getSymbol(symbols: ts.SymbolTable, name: ts.__String, meaning: ts.SymbolFlags): ts.Symbol | undefined { + if (meaning) { + const symbol = getMergedSymbol(symbols.get(name)); + if (symbol) { + ts.Debug.assert((ts.getCheckFlags(symbol) & ts.CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); + if (symbol.flags & meaning) { + return symbol; + } + if (symbol.flags & ts.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; } - if (symbol.flags & ts.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; - } - } } } - // return undefined if we can't find a symbol. } + // 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: ts.ParameterDeclaration, parameterName: ts.__String): [ - ts.Symbol, - ts.Symbol - ] { - const constructorDeclaration = parameter.parent; - const classDeclaration = parameter.parent.parent; - - const parameterSymbol = getSymbol(constructorDeclaration.locals!, parameterName, ts.SymbolFlags.Value); - const propertySymbol = getSymbol(getMembersOfSymbol(classDeclaration.symbol), parameterName, ts.SymbolFlags.Value); + /** + * 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: ts.ParameterDeclaration, parameterName: ts.__String): [ + ts.Symbol, + ts.Symbol + ] { + const constructorDeclaration = parameter.parent; + const classDeclaration = parameter.parent.parent; - if (parameterSymbol && propertySymbol) { - return [parameterSymbol, propertySymbol]; - } + const parameterSymbol = getSymbol(constructorDeclaration.locals!, parameterName, ts.SymbolFlags.Value); + const propertySymbol = getSymbol(getMembersOfSymbol(classDeclaration.symbol), parameterName, ts.SymbolFlags.Value); - return ts.Debug.fail("There should exist two symbols, one as property declaration and one as parameter declaration"); + if (parameterSymbol && propertySymbol) { + return [parameterSymbol, propertySymbol]; } - function isBlockScopedNameDeclaredBeforeUse(declaration: ts.Declaration, usage: ts.Node): boolean { - const declarationFile = ts.getSourceFileOfNode(declaration); - const useFile = ts.getSourceFileOfNode(usage); - const declContainer = ts.getEnclosingBlockScopeContainer(declaration); - if (declarationFile !== useFile) { - if ((moduleKind && (declarationFile.externalModuleIndicator || useFile.externalModuleIndicator)) || - (!ts.outFile(compilerOptions)) || - isInTypeQuery(usage) || - declaration.flags & ts.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 && !(ts.isPropertyDeclaration(declaration) && ts.isThisProperty(usage.parent) && !declaration.initializer && !declaration.exclamationToken)) { - // declaration is before usage - if (declaration.kind === ts.SyntaxKind.BindingElement) { - // still might be illegal if declaration and usage are both binding elements (eg var [a = b, b = b] = [1, 2]) - const errorBindingElement = ts.getAncestor(usage, ts.SyntaxKind.BindingElement) as ts.BindingElement; - if (errorBindingElement) { - return ts.findAncestor(errorBindingElement, ts.isBindingElement) !== ts.findAncestor(declaration, ts.isBindingElement) || - declaration.pos < errorBindingElement.pos; - } - // or it might be illegal if usage happens before parent variable is declared (eg var [a] = a) - return isBlockScopedNameDeclaredBeforeUse(ts.getAncestor(declaration, ts.SyntaxKind.VariableDeclaration) as ts.Declaration, usage); - } - else if (declaration.kind === ts.SyntaxKind.VariableDeclaration) { - // still might be illegal if usage is in the initializer of the variable declaration (eg var a = a) - return !isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration as ts.VariableDeclaration, usage); - } - else if (ts.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 !ts.findAncestor(usage, n => ts.isComputedPropertyName(n) && n.parent.parent === declaration); - } - else if (ts.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 (ts.isParameterPropertyDeclaration(declaration, declaration.parent)) { - // foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property - return !(ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && useDefineForClassFields - && ts.getContainingClass(declaration) === ts.getContainingClass(usage) - && isUsedInFunctionOrInstanceProperty(usage, declaration)); - } - return true; - } - + return ts.Debug.fail("There should exist two symbols, one as property declaration and one as parameter declaration"); + } - // 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 === ts.SyntaxKind.ExportSpecifier || (usage.parent.kind === ts.SyntaxKind.ExportAssignment && (usage.parent as ts.ExportAssignment).isExportEquals)) { - // export specifiers do not use the variable, they only make it available for use + function isBlockScopedNameDeclaredBeforeUse(declaration: ts.Declaration, usage: ts.Node): boolean { + const declarationFile = ts.getSourceFileOfNode(declaration); + const useFile = ts.getSourceFileOfNode(usage); + const declContainer = ts.getEnclosingBlockScopeContainer(declaration); + if (declarationFile !== useFile) { + if ((moduleKind && (declarationFile.externalModuleIndicator || useFile.externalModuleIndicator)) || + (!ts.outFile(compilerOptions)) || + isInTypeQuery(usage) || + declaration.flags & ts.NodeFlags.Ambient) { + // nodes are in different files and order cannot be determined return true; } - // When resolving symbols for exports, the `usage` location passed in can be the export site directly - if (usage.kind === ts.SyntaxKind.ExportAssignment && (usage as ts.ExportAssignment).isExportEquals) { + // 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 (!!(usage.flags & ts.NodeFlags.JSDoc) || isInTypeQuery(usage) || usageInTypeDeclaration()) { - return true; - } - if (isUsedInFunctionOrInstanceProperty(usage, declaration)) { - if (ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && useDefineForClassFields - && ts.getContainingClass(declaration) - && (ts.isPropertyDeclaration(declaration) || ts.isParameterPropertyDeclaration(declaration, declaration.parent))) { - return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ true); - } - else { - return true; + if (declaration.pos <= usage.pos && !(ts.isPropertyDeclaration(declaration) && ts.isThisProperty(usage.parent) && !declaration.initializer && !declaration.exclamationToken)) { + // declaration is before usage + if (declaration.kind === ts.SyntaxKind.BindingElement) { + // still might be illegal if declaration and usage are both binding elements (eg var [a = b, b = b] = [1, 2]) + const errorBindingElement = ts.getAncestor(usage, ts.SyntaxKind.BindingElement) as ts.BindingElement; + if (errorBindingElement) { + return ts.findAncestor(errorBindingElement, ts.isBindingElement) !== ts.findAncestor(declaration, ts.isBindingElement) || + declaration.pos < errorBindingElement.pos; } + // or it might be illegal if usage happens before parent variable is declared (eg var [a] = a) + return isBlockScopedNameDeclaredBeforeUse(ts.getAncestor(declaration, ts.SyntaxKind.VariableDeclaration) as ts.Declaration, usage); } - return false; - - function usageInTypeDeclaration() { - return !!ts.findAncestor(usage, node => ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)); + else if (declaration.kind === ts.SyntaxKind.VariableDeclaration) { + // still might be illegal if usage is in the initializer of the variable declaration (eg var a = a) + return !isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration as ts.VariableDeclaration, usage); + } + else if (ts.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 !ts.findAncestor(usage, n => ts.isComputedPropertyName(n) && n.parent.parent === declaration); + } + else if (ts.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 (ts.isParameterPropertyDeclaration(declaration, declaration.parent)) { + // foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property + return !(ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && useDefineForClassFields + && ts.getContainingClass(declaration) === ts.getContainingClass(usage) + && isUsedInFunctionOrInstanceProperty(usage, declaration)); } + return true; + } - function isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration: ts.VariableDeclaration, usage: ts.Node): boolean { - switch (declaration.parent.parent.kind) { - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.ForStatement: - case ts.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, declContainer)) { - return true; - } - break; - } - // ForIn/ForOf case - use site should not be used in expression part - const grandparent = declaration.parent.parent; - return ts.isForInOrOfStatement(grandparent) && isSameScopeDescendentOf(usage, grandparent.expression, declContainer); + // 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 === ts.SyntaxKind.ExportSpecifier || (usage.parent.kind === ts.SyntaxKind.ExportAssignment && (usage.parent as ts.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 === ts.SyntaxKind.ExportAssignment && (usage as ts.ExportAssignment).isExportEquals) { + return true; + } + + if (!!(usage.flags & ts.NodeFlags.JSDoc) || isInTypeQuery(usage) || usageInTypeDeclaration()) { + return true; + } + if (isUsedInFunctionOrInstanceProperty(usage, declaration)) { + if (ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && useDefineForClassFields + && ts.getContainingClass(declaration) + && (ts.isPropertyDeclaration(declaration) || ts.isParameterPropertyDeclaration(declaration, declaration.parent))) { + return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ true); + } + else { + return true; } + } + return false; - function isUsedInFunctionOrInstanceProperty(usage: ts.Node, declaration: ts.Node): boolean { - return !!ts.findAncestor(usage, current => { - if (current === declContainer) { - return "quit"; - } - if (ts.isFunctionLike(current)) { + function usageInTypeDeclaration() { + return !!ts.findAncestor(usage, node => ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)); + } + + function isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration: ts.VariableDeclaration, usage: ts.Node): boolean { + switch (declaration.parent.parent.kind) { + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.ForStatement: + case ts.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, declContainer)) { return true; } - if (ts.isClassStaticBlockDeclaration(current)) { - return declaration.pos < usage.pos; - } + break; + } - const propertyDeclaration = ts.tryCast(current.parent, ts.isPropertyDeclaration); - if (propertyDeclaration) { - const initializerOfProperty = propertyDeclaration.initializer === current; - if (initializerOfProperty) { - if (ts.isStatic(current.parent)) { - if (declaration.kind === ts.SyntaxKind.MethodDeclaration) { - return true; - } - if (ts.isPropertyDeclaration(declaration) && ts.getContainingClass(usage) === ts.getContainingClass(declaration)) { - const propName = declaration.name; - if (ts.isIdentifier(propName) || ts.isPrivateIdentifier(propName)) { - const type = getTypeOfSymbol(getSymbolOfNode(declaration)); - const staticBlocks = ts.filter(declaration.parent.members, ts.isClassStaticBlockDeclaration); - if (isPropertyInitializedInStaticBlocks(propName, type, staticBlocks, declaration.parent.pos, current.pos)) { - return true; - } + // ForIn/ForOf case - use site should not be used in expression part + const grandparent = declaration.parent.parent; + return ts.isForInOrOfStatement(grandparent) && isSameScopeDescendentOf(usage, grandparent.expression, declContainer); + } + + function isUsedInFunctionOrInstanceProperty(usage: ts.Node, declaration: ts.Node): boolean { + return !!ts.findAncestor(usage, current => { + if (current === declContainer) { + return "quit"; + } + if (ts.isFunctionLike(current)) { + return true; + } + if (ts.isClassStaticBlockDeclaration(current)) { + return declaration.pos < usage.pos; + } + + const propertyDeclaration = ts.tryCast(current.parent, ts.isPropertyDeclaration); + if (propertyDeclaration) { + const initializerOfProperty = propertyDeclaration.initializer === current; + if (initializerOfProperty) { + if (ts.isStatic(current.parent)) { + if (declaration.kind === ts.SyntaxKind.MethodDeclaration) { + return true; + } + if (ts.isPropertyDeclaration(declaration) && ts.getContainingClass(usage) === ts.getContainingClass(declaration)) { + const propName = declaration.name; + if (ts.isIdentifier(propName) || ts.isPrivateIdentifier(propName)) { + const type = getTypeOfSymbol(getSymbolOfNode(declaration)); + const staticBlocks = ts.filter(declaration.parent.members, ts.isClassStaticBlockDeclaration); + if (isPropertyInitializedInStaticBlocks(propName, type, staticBlocks, declaration.parent.pos, current.pos)) { + return true; } } } - else { - const isDeclarationInstanceProperty = declaration.kind === ts.SyntaxKind.PropertyDeclaration && !ts.isStatic(declaration); - if (!isDeclarationInstanceProperty || ts.getContainingClass(usage) !== ts.getContainingClass(declaration)) { - return true; - } + } + else { + const isDeclarationInstanceProperty = declaration.kind === ts.SyntaxKind.PropertyDeclaration && !ts.isStatic(declaration); + if (!isDeclarationInstanceProperty || ts.getContainingClass(usage) !== ts.getContainingClass(declaration)) { + return true; } } } - return false; - }); - } - - /** stopAtAnyPropertyDeclaration is used for detecting ES-standard class field use-before-def errors */ - function isPropertyImmediatelyReferencedWithinDeclaration(declaration: ts.PropertyDeclaration | ts.ParameterPropertyDeclaration, usage: ts.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 = ts.findAncestor(usage, (node: ts.Node) => { - if (node === declaration) { - return "quit"; - } - - switch (node.kind) { - case ts.SyntaxKind.ArrowFunction: - return true; - case ts.SyntaxKind.PropertyDeclaration: - // even when stopping at any property declaration, they need to come from the same class - return stopAtAnyPropertyDeclaration && - (ts.isPropertyDeclaration(declaration) && node.parent === declaration.parent - || ts.isParameterPropertyDeclaration(declaration, declaration.parent) && node.parent === declaration.parent.parent) - ? "quit": true; - case ts.SyntaxKind.Block: - switch (node.parent.kind) { - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.SetAccessor: - return true; - default: - return false; - } - default: - return false; - } - }); - - return ancestorChangingReferenceScope === undefined; - } + return false; + }); } - function useOuterVariableScopeInParameter(result: ts.Symbol, location: ts.Node, lastLocation: ts.Node) { - const target = ts.getEmitScriptTarget(compilerOptions); - const functionLocation = location as ts.FunctionLikeDeclaration; - if (ts.isParameter(lastLocation) - && functionLocation.body - && result.valueDeclaration - && result.valueDeclaration.pos >= functionLocation.body.pos - && result.valueDeclaration.end <= functionLocation.body.end) { - // check for several cases where we introduce temporaries that require moving the name/initializer of the parameter to the body - // - static field in a class expression - // - optional chaining pre-es2020 - // - nullish coalesce pre-es2020 - // - spread assignment in binding pattern pre-es2017 - if (target >= ts.ScriptTarget.ES2015) { - const links = getNodeLinks(functionLocation); - if (links.declarationRequiresScopeChange === undefined) { - links.declarationRequiresScopeChange = ts.forEach(functionLocation.parameters, requiresScopeChange) || false; - } - return !links.declarationRequiresScopeChange; - } + /** stopAtAnyPropertyDeclaration is used for detecting ES-standard class field use-before-def errors */ + function isPropertyImmediatelyReferencedWithinDeclaration(declaration: ts.PropertyDeclaration | ts.ParameterPropertyDeclaration, usage: ts.Node, stopAtAnyPropertyDeclaration: boolean) { + // always legal if usage is after declaration + if (usage.end > declaration.end) { + return false; } - return false; - function requiresScopeChange(node: ts.ParameterDeclaration): boolean { - return requiresScopeChangeWorker(node.name) - || !!node.initializer && requiresScopeChangeWorker(node.initializer); - } + // 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 = ts.findAncestor(usage, (node: ts.Node) => { + if (node === declaration) { + return "quit"; + } - function requiresScopeChangeWorker(node: ts.Node): boolean { switch (node.kind) { case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.Constructor: - // do not descend into these - return false; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.PropertyAssignment: - return requiresScopeChangeWorker((node as ts.MethodDeclaration | ts.AccessorDeclaration | ts.PropertyAssignment).name); + return true; case ts.SyntaxKind.PropertyDeclaration: - // static properties in classes introduce temporary variables - if (ts.hasStaticModifier(node)) { - return target < ts.ScriptTarget.ESNext || !useDefineForClassFields; + // even when stopping at any property declaration, they need to come from the same class + return stopAtAnyPropertyDeclaration && + (ts.isPropertyDeclaration(declaration) && node.parent === declaration.parent + || ts.isParameterPropertyDeclaration(declaration, declaration.parent) && node.parent === declaration.parent.parent) + ? "quit": true; + case ts.SyntaxKind.Block: + switch (node.parent.kind) { + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.SetAccessor: + return true; + default: + return false; } - return requiresScopeChangeWorker((node as ts.PropertyDeclaration).name); default: - // null coalesce and optional chain pre-es2020 produce temporary variables - if (ts.isNullishCoalesce(node) || ts.isOptionalChain(node)) { - return target < ts.ScriptTarget.ES2020; - } - if (ts.isBindingElement(node) && node.dotDotDotToken && ts.isObjectBindingPattern(node.parent)) { - return target < ts.ScriptTarget.ES2017; - } - if (ts.isTypeNode(node)) - return false; - return ts.forEachChild(node, requiresScopeChangeWorker) || false; + return false; + } + }); + + return ancestorChangingReferenceScope === undefined; + } + } + + function useOuterVariableScopeInParameter(result: ts.Symbol, location: ts.Node, lastLocation: ts.Node) { + const target = ts.getEmitScriptTarget(compilerOptions); + const functionLocation = location as ts.FunctionLikeDeclaration; + if (ts.isParameter(lastLocation) + && functionLocation.body + && result.valueDeclaration + && result.valueDeclaration.pos >= functionLocation.body.pos + && result.valueDeclaration.end <= functionLocation.body.end) { + // check for several cases where we introduce temporaries that require moving the name/initializer of the parameter to the body + // - static field in a class expression + // - optional chaining pre-es2020 + // - nullish coalesce pre-es2020 + // - spread assignment in binding pattern pre-es2017 + if (target >= ts.ScriptTarget.ES2015) { + const links = getNodeLinks(functionLocation); + if (links.declarationRequiresScopeChange === undefined) { + links.declarationRequiresScopeChange = ts.forEach(functionLocation.parameters, requiresScopeChange) || false; } + return !links.declarationRequiresScopeChange; } } + return false; - function isConstAssertion(location: ts.Node) { - return (ts.isAssertionExpression(location) && ts.isConstTypeReference(location.type)) - || (ts.isJSDocTypeTag(location) && ts.isConstTypeReference(location.typeExpression)); + function requiresScopeChange(node: ts.ParameterDeclaration): boolean { + return requiresScopeChangeWorker(node.name) + || !!node.initializer && requiresScopeChangeWorker(node.initializer); } - /** - * 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: ts.Node | undefined, name: ts.__String, meaning: ts.SymbolFlags, nameNotFoundMessage: ts.DiagnosticMessage | undefined, nameArg: ts.__String | ts.Identifier | undefined, isUse: boolean, excludeGlobals = false, getSpellingSuggstions = true): ts.Symbol | undefined { - return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSpellingSuggstions, getSymbol); - } - - function resolveNameHelper(location: ts.Node | undefined, name: ts.__String, meaning: ts.SymbolFlags, nameNotFoundMessage: ts.DiagnosticMessage | undefined, nameArg: ts.__String | ts.Identifier | undefined, isUse: boolean, excludeGlobals: boolean, getSpellingSuggestions: boolean, lookup: typeof getSymbol): 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: ts.Node | undefined; - let lastSelfReferenceLocation: ts.Node | undefined; - let propertyWithInvalidInitializer: ts.Node | undefined; - let associatedDeclarationForContainingInitializerOrBindingName: ts.ParameterDeclaration | ts.BindingElement | undefined; - let withinDeferredContext = false; - const errorLocation = location; - let grandparent: ts.Node; - let isInExternalModule = false; - - loop: while (location) { - if (name === "const" && isConstAssertion(location)) { - // `const` in an `as const` has no symbol, but issues no error because there is no *actual* lookup of the type - // (it refers to the constant type of the expression instead) - return undefined; - } - // 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 (ts.isFunctionLike(location) && lastLocation && lastLocation !== (location as ts.FunctionLikeDeclaration).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 & ts.SymbolFlags.Type && lastLocation.kind !== ts.SyntaxKind.JSDoc) { - useResult = result.flags & ts.SymbolFlags.TypeParameter - // type parameters are visible in parameter list, return type and type parameter list - ? lastLocation === (location as ts.FunctionLikeDeclaration).type || - lastLocation.kind === ts.SyntaxKind.Parameter || - lastLocation.kind === ts.SyntaxKind.JSDocParameterTag || - lastLocation.kind === ts.SyntaxKind.JSDocReturnTag || - lastLocation.kind === ts.SyntaxKind.TypeParameter - // local types not visible outside the function body - : false; - } - if (meaning & result.flags & ts.SymbolFlags.Variable) { - // expression inside parameter will lookup as normal variable scope when targeting es2015+ - if (useOuterVariableScopeInParameter(result, location, lastLocation)) { - useResult = false; - } - else if (result.flags & ts.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 === ts.SyntaxKind.Parameter || - (lastLocation === (location as ts.FunctionLikeDeclaration).type && - !!ts.findAncestor(result.valueDeclaration, ts.isParameter)); - } - } - } - else if (location.kind === ts.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 as ts.ConditionalTypeNode).trueType; - } + function requiresScopeChangeWorker(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Constructor: + // do not descend into these + return false; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.PropertyAssignment: + return requiresScopeChangeWorker((node as ts.MethodDeclaration | ts.AccessorDeclaration | ts.PropertyAssignment).name); + case ts.SyntaxKind.PropertyDeclaration: + // static properties in classes introduce temporary variables + if (ts.hasStaticModifier(node)) { + return target < ts.ScriptTarget.ESNext || !useDefineForClassFields; + } + return requiresScopeChangeWorker((node as ts.PropertyDeclaration).name); + default: + // null coalesce and optional chain pre-es2020 produce temporary variables + if (ts.isNullishCoalesce(node) || ts.isOptionalChain(node)) { + return target < ts.ScriptTarget.ES2020; + } + if (ts.isBindingElement(node) && node.dotDotDotToken && ts.isObjectBindingPattern(node.parent)) { + return target < ts.ScriptTarget.ES2017; + } + if (ts.isTypeNode(node)) + return false; + return ts.forEachChild(node, requiresScopeChangeWorker) || false; + } + } + } - if (useResult) { - break loop; - } - else { - result = undefined; - } + function isConstAssertion(location: ts.Node) { + return (ts.isAssertionExpression(location) && ts.isConstTypeReference(location.type)) + || (ts.isJSDocTypeTag(location) && ts.isConstTypeReference(location.typeExpression)); + } + + /** + * 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: ts.Node | undefined, name: ts.__String, meaning: ts.SymbolFlags, nameNotFoundMessage: ts.DiagnosticMessage | undefined, nameArg: ts.__String | ts.Identifier | undefined, isUse: boolean, excludeGlobals = false, getSpellingSuggstions = true): ts.Symbol | undefined { + return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSpellingSuggstions, getSymbol); + } + + function resolveNameHelper(location: ts.Node | undefined, name: ts.__String, meaning: ts.SymbolFlags, nameNotFoundMessage: ts.DiagnosticMessage | undefined, nameArg: ts.__String | ts.Identifier | undefined, isUse: boolean, excludeGlobals: boolean, getSpellingSuggestions: boolean, lookup: typeof getSymbol): 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: ts.Node | undefined; + let lastSelfReferenceLocation: ts.Node | undefined; + let propertyWithInvalidInitializer: ts.Node | undefined; + let associatedDeclarationForContainingInitializerOrBindingName: ts.ParameterDeclaration | ts.BindingElement | undefined; + let withinDeferredContext = false; + const errorLocation = location; + let grandparent: ts.Node; + let isInExternalModule = false; + + loop: while (location) { + if (name === "const" && isConstAssertion(location)) { + // `const` in an `as const` has no symbol, but issues no error because there is no *actual* lookup of the type + // (it refers to the constant type of the expression instead) + return undefined; + } + // 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 (ts.isFunctionLike(location) && lastLocation && lastLocation !== (location as ts.FunctionLikeDeclaration).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 & ts.SymbolFlags.Type && lastLocation.kind !== ts.SyntaxKind.JSDoc) { + useResult = result.flags & ts.SymbolFlags.TypeParameter + // type parameters are visible in parameter list, return type and type parameter list + ? lastLocation === (location as ts.FunctionLikeDeclaration).type || + lastLocation.kind === ts.SyntaxKind.Parameter || + lastLocation.kind === ts.SyntaxKind.JSDocParameterTag || + lastLocation.kind === ts.SyntaxKind.JSDocReturnTag || + lastLocation.kind === ts.SyntaxKind.TypeParameter + // local types not visible outside the function body + : false; + } + if (meaning & result.flags & ts.SymbolFlags.Variable) { + // expression inside parameter will lookup as normal variable scope when targeting es2015+ + if (useOuterVariableScopeInParameter(result, location, lastLocation)) { + useResult = false; + } + else if (result.flags & ts.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 === ts.SyntaxKind.Parameter || + (lastLocation === (location as ts.FunctionLikeDeclaration).type && + !!ts.findAncestor(result.valueDeclaration, ts.isParameter)); + } + } + } + else if (location.kind === ts.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 as ts.ConditionalTypeNode).trueType; + } + + if (useResult) { + break loop; + } + else { + result = undefined; } } - withinDeferredContext = withinDeferredContext || getIsDeferredContext(location, lastLocation); - switch (location.kind) { - case ts.SyntaxKind.SourceFile: - if (!ts.isExternalOrCommonJsModule(location as ts.SourceFile)) - break; - isInExternalModule = true; - // falls through - case ts.SyntaxKind.ModuleDeclaration: - const moduleExports = getSymbolOfNode(location as ts.SourceFile | ts.ModuleDeclaration)?.exports || emptySymbols; - if (location.kind === ts.SyntaxKind.SourceFile || (ts.isModuleDeclaration(location) && location.flags & ts.NodeFlags.Ambient && !ts.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(ts.InternalSymbolName.Default)) { - const localSymbol = ts.getLocalSymbolForExportDefault(result); - if (localSymbol && (result.flags & meaning) && localSymbol.escapedName === name) { - break loop; - } - result = undefined; + } + withinDeferredContext = withinDeferredContext || getIsDeferredContext(location, lastLocation); + switch (location.kind) { + case ts.SyntaxKind.SourceFile: + if (!ts.isExternalOrCommonJsModule(location as ts.SourceFile)) + break; + isInExternalModule = true; + // falls through + case ts.SyntaxKind.ModuleDeclaration: + const moduleExports = getSymbolOfNode(location as ts.SourceFile | ts.ModuleDeclaration)?.exports || emptySymbols; + if (location.kind === ts.SyntaxKind.SourceFile || (ts.isModuleDeclaration(location) && location.flags & ts.NodeFlags.Ambient && !ts.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(ts.InternalSymbolName.Default)) { + const localSymbol = ts.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 === ts.SymbolFlags.Alias && - (ts.getDeclarationOfKind(moduleExport, ts.SyntaxKind.ExportSpecifier) || ts.getDeclarationOfKind(moduleExport, ts.SyntaxKind.NamespaceExport))) { - break; - } + // 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 === ts.SymbolFlags.Alias && + (ts.getDeclarationOfKind(moduleExport, ts.SyntaxKind.ExportSpecifier) || ts.getDeclarationOfKind(moduleExport, ts.SyntaxKind.NamespaceExport))) { + break; } + } - // ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs) - if (name !== ts.InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & ts.SymbolFlags.ModuleMember))) { - if (ts.isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations?.some(ts.isJSDocTypeAlias)) { - result = undefined; - } - else { - break loop; - } + // ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs) + if (name !== ts.InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & ts.SymbolFlags.ModuleMember))) { + if (ts.isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations?.some(ts.isJSDocTypeAlias)) { + result = undefined; } - break; - case ts.SyntaxKind.EnumDeclaration: - if (result = lookup(getSymbolOfNode(location)?.exports || emptySymbols, name, meaning & ts.SymbolFlags.EnumMember)) { + else { break loop; } - break; - case ts.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 (!ts.isStatic(location)) { - const ctor = findConstructorDeclaration(location.parent as ts.ClassLikeDeclaration); - if (ctor && ctor.locals) { - if (lookup(ctor.locals, name, meaning & ts.SymbolFlags.Value)) { - // Remember the property node, it will be used later to report appropriate error - propertyWithInvalidInitializer = location; - } + } + break; + case ts.SyntaxKind.EnumDeclaration: + if (result = lookup(getSymbolOfNode(location)?.exports || emptySymbols, name, meaning & ts.SymbolFlags.EnumMember)) { + break loop; + } + break; + case ts.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 (!ts.isStatic(location)) { + const ctor = findConstructorDeclaration(location.parent as ts.ClassLikeDeclaration); + if (ctor && ctor.locals) { + if (lookup(ctor.locals, name, meaning & ts.SymbolFlags.Value)) { + // Remember the property node, it will be used later to report appropriate error + propertyWithInvalidInitializer = location; } } - break; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.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 ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members || emptySymbols, name, meaning & ts.SymbolFlags.Type)) { - if (!isTypeParameterSymbolDeclaredInContainer(result, location)) { - // ignore type parameters not declared in this container - result = undefined; - break; - } - if (lastLocation && ts.isStatic(lastLocation)) { - // 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, ts.Diagnostics.Static_members_cannot_reference_class_type_parameters); - return undefined; - } - break loop; - } - if (location.kind === ts.SyntaxKind.ClassExpression && meaning & ts.SymbolFlags.Class) { - const className = (location as ts.ClassExpression).name; - if (className && name === className.escapedText) { - result = location.symbol; - break loop; - } - } - break; - case ts.SyntaxKind.ExpressionWithTypeArguments: - // The type parameters of a class are not in scope in the base class expression. - if (lastLocation === (location as ts.ExpressionWithTypeArguments).expression && (location.parent as ts.HeritageClause).token === ts.SyntaxKind.ExtendsKeyword) { - const container = location.parent.parent; - if (ts.isClassLike(container) && (result = lookup(getSymbolOfNode(container).members!, name, meaning & ts.SymbolFlags.Type))) { - if (nameNotFoundMessage) { - error(errorLocation, ts.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 ts.SyntaxKind.ComputedPropertyName: - grandparent = location.parent.parent; - if (ts.isClassLike(grandparent) || grandparent.kind === ts.SyntaxKind.InterfaceDeclaration) { - // A reference to this grandparent's type parameters would be an error - if (result = lookup(getSymbolOfNode(grandparent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members!, name, meaning & ts.SymbolFlags.Type)) { - error(errorLocation, ts.Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); - return undefined; - } - } - break; - case ts.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 (ts.getEmitScriptTarget(compilerOptions) >= ts.ScriptTarget.ES2015) { + } + break; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.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 ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members || emptySymbols, name, meaning & ts.SymbolFlags.Type)) { + if (!isTypeParameterSymbolDeclaredInContainer(result, location)) { + // ignore type parameters not declared in this container + result = undefined; break; } - // falls through - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionDeclaration: - if (meaning & ts.SymbolFlags.Variable && name === "arguments") { - result = argumentsSymbol; - break loop; + if (lastLocation && ts.isStatic(lastLocation)) { + // 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, ts.Diagnostics.Static_members_cannot_reference_class_type_parameters); + return undefined; } - break; - case ts.SyntaxKind.FunctionExpression: - if (meaning & ts.SymbolFlags.Variable && name === "arguments") { - result = argumentsSymbol; + break loop; + } + if (location.kind === ts.SyntaxKind.ClassExpression && meaning & ts.SymbolFlags.Class) { + const className = (location as ts.ClassExpression).name; + if (className && name === className.escapedText) { + result = location.symbol; break loop; } - - if (meaning & ts.SymbolFlags.Function) { - const functionName = (location as ts.FunctionExpression).name; - if (functionName && name === functionName.escapedText) { - result = location.symbol; - break loop; + } + break; + case ts.SyntaxKind.ExpressionWithTypeArguments: + // The type parameters of a class are not in scope in the base class expression. + if (lastLocation === (location as ts.ExpressionWithTypeArguments).expression && (location.parent as ts.HeritageClause).token === ts.SyntaxKind.ExtendsKeyword) { + const container = location.parent.parent; + if (ts.isClassLike(container) && (result = lookup(getSymbolOfNode(container).members!, name, meaning & ts.SymbolFlags.Type))) { + if (nameNotFoundMessage) { + error(errorLocation, ts.Diagnostics.Base_class_expressions_cannot_reference_class_type_parameters); } + return undefined; } - break; - case ts.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 === ts.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 && (ts.isClassElement(location.parent) || location.parent.kind === ts.SyntaxKind.ClassDeclaration)) { - location = location.parent; + } + 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 ts.SyntaxKind.ComputedPropertyName: + grandparent = location.parent.parent; + if (ts.isClassLike(grandparent) || grandparent.kind === ts.SyntaxKind.InterfaceDeclaration) { + // A reference to this grandparent's type parameters would be an error + if (result = lookup(getSymbolOfNode(grandparent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members!, name, meaning & ts.SymbolFlags.Type)) { + error(errorLocation, ts.Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); + return undefined; } + } + break; + case ts.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 (ts.getEmitScriptTarget(compilerOptions) >= ts.ScriptTarget.ES2015) { break; - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - // js type aliases do not resolve names from their host, so skip past it - const root = ts.getJSDocRoot(location); - if (root) { - location = root.parent; + } + // falls through + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionDeclaration: + if (meaning & ts.SymbolFlags.Variable && name === "arguments") { + result = argumentsSymbol; + break loop; + } + break; + case ts.SyntaxKind.FunctionExpression: + if (meaning & ts.SymbolFlags.Variable && name === "arguments") { + result = argumentsSymbol; + break loop; + } + + if (meaning & ts.SymbolFlags.Function) { + const functionName = (location as ts.FunctionExpression).name; + if (functionName && name === functionName.escapedText) { + result = location.symbol; + break loop; } - break; - case ts.SyntaxKind.Parameter: - if (lastLocation && (lastLocation === (location as ts.ParameterDeclaration).initializer || - lastLocation === (location as ts.ParameterDeclaration).name && ts.isBindingPattern(lastLocation))) { - if (!associatedDeclarationForContainingInitializerOrBindingName) { - associatedDeclarationForContainingInitializerOrBindingName = location as ts.ParameterDeclaration; - } + } + break; + case ts.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 === ts.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 && (ts.isClassElement(location.parent) || location.parent.kind === ts.SyntaxKind.ClassDeclaration)) { + location = location.parent; + } + break; + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + // js type aliases do not resolve names from their host, so skip past it + const root = ts.getJSDocRoot(location); + if (root) { + location = root.parent; + } + break; + case ts.SyntaxKind.Parameter: + if (lastLocation && (lastLocation === (location as ts.ParameterDeclaration).initializer || + lastLocation === (location as ts.ParameterDeclaration).name && ts.isBindingPattern(lastLocation))) { + if (!associatedDeclarationForContainingInitializerOrBindingName) { + associatedDeclarationForContainingInitializerOrBindingName = location as ts.ParameterDeclaration; } - break; - case ts.SyntaxKind.BindingElement: - if (lastLocation && (lastLocation === (location as ts.BindingElement).initializer || - lastLocation === (location as ts.BindingElement).name && ts.isBindingPattern(lastLocation))) { - if (ts.isParameterDeclaration(location as ts.BindingElement) && !associatedDeclarationForContainingInitializerOrBindingName) { - associatedDeclarationForContainingInitializerOrBindingName = location as ts.BindingElement; - } + } + break; + case ts.SyntaxKind.BindingElement: + if (lastLocation && (lastLocation === (location as ts.BindingElement).initializer || + lastLocation === (location as ts.BindingElement).name && ts.isBindingPattern(lastLocation))) { + if (ts.isParameterDeclaration(location as ts.BindingElement) && !associatedDeclarationForContainingInitializerOrBindingName) { + associatedDeclarationForContainingInitializerOrBindingName = location as ts.BindingElement; } - break; - case ts.SyntaxKind.InferType: - if (meaning & ts.SymbolFlags.TypeParameter) { - const parameterName = (location as ts.InferTypeNode).typeParameter.name; - if (parameterName && name === parameterName.escapedText) { - result = (location as ts.InferTypeNode).typeParameter.symbol; - break loop; - } + } + break; + case ts.SyntaxKind.InferType: + if (meaning & ts.SymbolFlags.TypeParameter) { + const parameterName = (location as ts.InferTypeNode).typeParameter.name; + if (parameterName && name === parameterName.escapedText) { + result = (location as ts.InferTypeNode).typeParameter.symbol; + break loop; } - break; - } - if (isSelfReferenceLocation(location)) { - lastSelfReferenceLocation = location; - } - lastLocation = location; - location = ts.isJSDocTemplateTag(location) ? ts.getEffectiveContainerForJSDocTemplateTag(location) || location.parent : - ts.isJSDocParameterTag(location) || ts.isJSDocReturnTag(location) ? ts.getHostSignatureFromJSDoc(location) || location.parent : - 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; } + lastLocation = location; + location = ts.isJSDocTemplateTag(location) ? ts.getEffectiveContainerForJSDocTemplateTag(location) || location.parent : + ts.isJSDocParameterTag(location) || ts.isJSDocReturnTag(location) ? ts.getHostSignatureFromJSDoc(location) || location.parent : + location.parent; + } - if (!result) { - if (lastLocation) { - ts.Debug.assert(lastLocation.kind === ts.SyntaxKind.SourceFile); - if ((lastLocation as ts.SourceFile).commonJsModuleIndicator && name === "exports" && meaning & lastLocation.symbol.flags) { - return lastLocation.symbol; - } - } + // 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 (!excludeGlobals) { - result = lookup(globals, name, meaning); + if (!result) { + if (lastLocation) { + ts.Debug.assert(lastLocation.kind === ts.SyntaxKind.SourceFile); + if ((lastLocation as ts.SourceFile).commonJsModuleIndicator && name === "exports" && meaning & lastLocation.symbol.flags) { + return lastLocation.symbol; } } - if (!result) { - if (originalLocation && ts.isInJSFile(originalLocation) && originalLocation.parent) { - if (ts.isRequireCall(originalLocation.parent, /*checkArgumentIsStringLiteralLike*/ false)) { - return requireSymbol; - } + + if (!excludeGlobals) { + result = lookup(globals, name, meaning); + } + } + if (!result) { + if (originalLocation && ts.isInJSFile(originalLocation) && originalLocation.parent) { + if (ts.isRequireCall(originalLocation.parent, /*checkArgumentIsStringLiteralLike*/ false)) { + return requireSymbol; } } - if (!result) { - if (nameNotFoundMessage) { - addLazyDiagnostic(() => { - 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: ts.Symbol | undefined; - if (getSpellingSuggestions && suggestionCount < maximumSuggestionCount) { - suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning); - const isGlobalScopeAugmentationDeclaration = suggestion?.valueDeclaration && ts.isAmbientModule(suggestion.valueDeclaration) && ts.isGlobalScopeAugmentation(suggestion.valueDeclaration); - if (isGlobalScopeAugmentationDeclaration) { - suggestion = undefined; - } - if (suggestion) { - const suggestionName = symbolToString(suggestion); - const isUncheckedJS = isUncheckedJSSuggestion(originalLocation, suggestion, /*excludeClasses*/ false); - const message = meaning === ts.SymbolFlags.Namespace || nameArg && typeof nameArg !== "string" && ts.nodeIsSynthesized(nameArg) ? ts.Diagnostics.Cannot_find_namespace_0_Did_you_mean_1 - : isUncheckedJS ? ts.Diagnostics.Could_not_find_name_0_Did_you_mean_1 - : ts.Diagnostics.Cannot_find_name_0_Did_you_mean_1; - const diagnostic = createError(errorLocation, message, diagnosticName(nameArg!), suggestionName); - addErrorOrSuggestion(!isUncheckedJS, diagnostic); - if (suggestion.valueDeclaration) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestionName)); - } + } + if (!result) { + if (nameNotFoundMessage) { + addLazyDiagnostic(() => { + 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: ts.Symbol | undefined; + if (getSpellingSuggestions && suggestionCount < maximumSuggestionCount) { + suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning); + const isGlobalScopeAugmentationDeclaration = suggestion?.valueDeclaration && ts.isAmbientModule(suggestion.valueDeclaration) && ts.isGlobalScopeAugmentation(suggestion.valueDeclaration); + if (isGlobalScopeAugmentationDeclaration) { + suggestion = undefined; + } + if (suggestion) { + const suggestionName = symbolToString(suggestion); + const isUncheckedJS = isUncheckedJSSuggestion(originalLocation, suggestion, /*excludeClasses*/ false); + const message = meaning === ts.SymbolFlags.Namespace || nameArg && typeof nameArg !== "string" && ts.nodeIsSynthesized(nameArg) ? ts.Diagnostics.Cannot_find_namespace_0_Did_you_mean_1 + : isUncheckedJS ? ts.Diagnostics.Could_not_find_name_0_Did_you_mean_1 + : ts.Diagnostics.Cannot_find_name_0_Did_you_mean_1; + const diagnostic = createError(errorLocation, message, diagnosticName(nameArg!), suggestionName); + addErrorOrSuggestion(!isUncheckedJS, diagnostic); + if (suggestion.valueDeclaration) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestionName)); } } - if (!suggestion) { - if (nameArg) { - const lib = getSuggestedLibForNonExistentName(nameArg); - if (lib) { - error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg), lib); - } - else { - error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg)); - } + } + if (!suggestion) { + if (nameArg) { + const lib = getSuggestedLibForNonExistentName(nameArg); + if (lib) { + error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg), lib); + } + else { + error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg)); } } - suggestionCount++; - } - }); - } - return undefined; - } - - if (propertyWithInvalidInitializer && !(ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && 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 as ts.PropertyDeclaration).name; - error(errorLocation, ts.Diagnostics.Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor, ts.declarationNameToString(propertyName), diagnosticName(nameArg!)); - return undefined; - } - - // Perform extra checks only if error reporting was requested - if (nameNotFoundMessage) { - addLazyDiagnostic(() => { - // 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 & ts.SymbolFlags.BlockScopedVariable || - ((meaning & ts.SymbolFlags.Class || meaning & ts.SymbolFlags.Enum) && (meaning & ts.SymbolFlags.Value) === ts.SymbolFlags.Value))) { - const exportOrLocalSymbol = getExportSymbolOfValueSymbolIfExported(result!); - if (exportOrLocalSymbol.flags & ts.SymbolFlags.BlockScopedVariable || exportOrLocalSymbol.flags & ts.SymbolFlags.Class || exportOrLocalSymbol.flags & ts.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 & ts.SymbolFlags.Value) === ts.SymbolFlags.Value && !(originalLocation!.flags & ts.NodeFlags.JSDoc)) { - const merged = getMergedSymbol(result); - if (ts.length(merged.declarations) && ts.every(merged.declarations, d => ts.isNamespaceExportDeclaration(d) || ts.isSourceFile(d) && !!d.symbol.globalExports)) { - errorOrSuggestion(!compilerOptions.allowUmdGlobalAccess, errorLocation!, ts.Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead, ts.unescapeLeadingUnderscores(name)); - } - } - - // If we're in a parameter initializer or binding name, we can't reference the values of the parameter whose initializer we're within or parameters to the right - if (result && associatedDeclarationForContainingInitializerOrBindingName && !withinDeferredContext && (meaning & ts.SymbolFlags.Value) === ts.SymbolFlags.Value) { - const candidate = getMergedSymbol(getLateBoundSymbol(result)); - const root = (ts.getRootDeclaration(associatedDeclarationForContainingInitializerOrBindingName) as ts.ParameterDeclaration); - // A parameter initializer or binding pattern initializer within a parameter cannot refer to itself - if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializerOrBindingName)) { - error(errorLocation, ts.Diagnostics.Parameter_0_cannot_reference_itself, ts.declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name)); - } - // And it cannot refer to any declarations which come after it - else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializerOrBindingName.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) { - error(errorLocation, ts.Diagnostics.Parameter_0_cannot_reference_identifier_1_declared_after_it, ts.declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name), ts.declarationNameToString(errorLocation as ts.Identifier)); - } - } - if (result && errorLocation && meaning & ts.SymbolFlags.Value && result.flags & ts.SymbolFlags.Alias && !(result.flags & ts.SymbolFlags.Value) && !ts.isValidTypeOnlyAliasUseSite(errorLocation)) { - const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(result); - if (typeOnlyDeclaration) { - const message = typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier - ? ts.Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type - : ts.Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type; - const unescapedName = ts.unescapeLeadingUnderscores(name); - addTypeOnlyDeclarationRelatedInfo(error(errorLocation, message, unescapedName), typeOnlyDeclaration, unescapedName); } + suggestionCount++; } }); } - return result; + return undefined; } - function addTypeOnlyDeclarationRelatedInfo(diagnostic: ts.Diagnostic, typeOnlyDeclaration: ts.TypeOnlyCompatibleAliasDeclaration | undefined, unescapedName: string) { - if (!typeOnlyDeclaration) - return diagnostic; - return ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(typeOnlyDeclaration, typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier ? ts.Diagnostics._0_was_exported_here : ts.Diagnostics._0_was_imported_here, unescapedName)); + if (propertyWithInvalidInitializer && !(ts.getEmitScriptTarget(compilerOptions) === ts.ScriptTarget.ESNext && 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 as ts.PropertyDeclaration).name; + error(errorLocation, ts.Diagnostics.Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor, ts.declarationNameToString(propertyName), diagnosticName(nameArg!)); + return undefined; } - function getIsDeferredContext(location: ts.Node, lastLocation: ts.Node | undefined): boolean { - if (location.kind !== ts.SyntaxKind.ArrowFunction && location.kind !== ts.SyntaxKind.FunctionExpression) { - // initializers in instance property declaration of class like entities are executed in constructor and thus deferred - return ts.isTypeQueryNode(location) || ((ts.isFunctionLikeDeclaration(location) || - (location.kind === ts.SyntaxKind.PropertyDeclaration && !ts.isStatic(location))) && (!lastLocation || lastLocation !== (location as ts.SignatureDeclaration | ts.PropertyDeclaration).name)); // A name is evaluated within the enclosing scope - so it shouldn't count as deferred - } - if (lastLocation && lastLocation === (location as ts.FunctionExpression | ts.ArrowFunction).name) { - return false; - } - // generator functions and async functions are not inlined in control flow when immediately invoked - if ((location as ts.FunctionExpression | ts.ArrowFunction).asteriskToken || ts.hasSyntacticModifier(location, ts.ModifierFlags.Async)) { - return true; - } - return !ts.getImmediatelyInvokedFunctionExpression(location); + // Perform extra checks only if error reporting was requested + if (nameNotFoundMessage) { + addLazyDiagnostic(() => { + // 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 & ts.SymbolFlags.BlockScopedVariable || + ((meaning & ts.SymbolFlags.Class || meaning & ts.SymbolFlags.Enum) && (meaning & ts.SymbolFlags.Value) === ts.SymbolFlags.Value))) { + const exportOrLocalSymbol = getExportSymbolOfValueSymbolIfExported(result!); + if (exportOrLocalSymbol.flags & ts.SymbolFlags.BlockScopedVariable || exportOrLocalSymbol.flags & ts.SymbolFlags.Class || exportOrLocalSymbol.flags & ts.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 & ts.SymbolFlags.Value) === ts.SymbolFlags.Value && !(originalLocation!.flags & ts.NodeFlags.JSDoc)) { + const merged = getMergedSymbol(result); + if (ts.length(merged.declarations) && ts.every(merged.declarations, d => ts.isNamespaceExportDeclaration(d) || ts.isSourceFile(d) && !!d.symbol.globalExports)) { + errorOrSuggestion(!compilerOptions.allowUmdGlobalAccess, errorLocation!, ts.Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead, ts.unescapeLeadingUnderscores(name)); + } + } + + // If we're in a parameter initializer or binding name, we can't reference the values of the parameter whose initializer we're within or parameters to the right + if (result && associatedDeclarationForContainingInitializerOrBindingName && !withinDeferredContext && (meaning & ts.SymbolFlags.Value) === ts.SymbolFlags.Value) { + const candidate = getMergedSymbol(getLateBoundSymbol(result)); + const root = (ts.getRootDeclaration(associatedDeclarationForContainingInitializerOrBindingName) as ts.ParameterDeclaration); + // A parameter initializer or binding pattern initializer within a parameter cannot refer to itself + if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializerOrBindingName)) { + error(errorLocation, ts.Diagnostics.Parameter_0_cannot_reference_itself, ts.declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name)); + } + // And it cannot refer to any declarations which come after it + else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializerOrBindingName.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) { + error(errorLocation, ts.Diagnostics.Parameter_0_cannot_reference_identifier_1_declared_after_it, ts.declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name), ts.declarationNameToString(errorLocation as ts.Identifier)); + } + } + if (result && errorLocation && meaning & ts.SymbolFlags.Value && result.flags & ts.SymbolFlags.Alias && !(result.flags & ts.SymbolFlags.Value) && !ts.isValidTypeOnlyAliasUseSite(errorLocation)) { + const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(result); + if (typeOnlyDeclaration) { + const message = typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier + ? ts.Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type + : ts.Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type; + const unescapedName = ts.unescapeLeadingUnderscores(name); + addTypeOnlyDeclarationRelatedInfo(error(errorLocation, message, unescapedName), typeOnlyDeclaration, unescapedName); + } + } + }); } + return result; + } - function isSelfReferenceLocation(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ModuleDeclaration: // For `namespace N { N; }` - return true; - default: - return false; - } + function addTypeOnlyDeclarationRelatedInfo(diagnostic: ts.Diagnostic, typeOnlyDeclaration: ts.TypeOnlyCompatibleAliasDeclaration | undefined, unescapedName: string) { + if (!typeOnlyDeclaration) + return diagnostic; + return ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(typeOnlyDeclaration, typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier ? ts.Diagnostics._0_was_exported_here : ts.Diagnostics._0_was_imported_here, unescapedName)); + } + + function getIsDeferredContext(location: ts.Node, lastLocation: ts.Node | undefined): boolean { + if (location.kind !== ts.SyntaxKind.ArrowFunction && location.kind !== ts.SyntaxKind.FunctionExpression) { + // initializers in instance property declaration of class like entities are executed in constructor and thus deferred + return ts.isTypeQueryNode(location) || ((ts.isFunctionLikeDeclaration(location) || + (location.kind === ts.SyntaxKind.PropertyDeclaration && !ts.isStatic(location))) && (!lastLocation || lastLocation !== (location as ts.SignatureDeclaration | ts.PropertyDeclaration).name)); // A name is evaluated within the enclosing scope - so it shouldn't count as deferred + } + if (lastLocation && lastLocation === (location as ts.FunctionExpression | ts.ArrowFunction).name) { + return false; } + // generator functions and async functions are not inlined in control flow when immediately invoked + if ((location as ts.FunctionExpression | ts.ArrowFunction).asteriskToken || ts.hasSyntacticModifier(location, ts.ModifierFlags.Async)) { + return true; + } + return !ts.getImmediatelyInvokedFunctionExpression(location); + } - function diagnosticName(nameArg: ts.__String | ts.Identifier | ts.PrivateIdentifier) { - return ts.isString(nameArg) ? ts.unescapeLeadingUnderscores(nameArg as ts.__String) : ts.declarationNameToString(nameArg as ts.Identifier); + function isSelfReferenceLocation(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.ModuleDeclaration: // For `namespace N { N; }` + return true; + default: + return false; } + } - function isTypeParameterSymbolDeclaredInContainer(symbol: ts.Symbol, container: ts.Node) { - if (symbol.declarations) { - for (const decl of symbol.declarations) { - if (decl.kind === ts.SyntaxKind.TypeParameter) { - const parent = ts.isJSDocTemplateTag(decl.parent) ? ts.getJSDocHost(decl.parent) : decl.parent; - if (parent === container) { - return !(ts.isJSDocTemplateTag(decl.parent) && ts.find((decl.parent.parent as ts.JSDoc).tags!, ts.isJSDocTypeAlias)); // TODO: GH#18217 - } + function diagnosticName(nameArg: ts.__String | ts.Identifier | ts.PrivateIdentifier) { + return ts.isString(nameArg) ? ts.unescapeLeadingUnderscores(nameArg as ts.__String) : ts.declarationNameToString(nameArg as ts.Identifier); + } + + function isTypeParameterSymbolDeclaredInContainer(symbol: ts.Symbol, container: ts.Node) { + if (symbol.declarations) { + for (const decl of symbol.declarations) { + if (decl.kind === ts.SyntaxKind.TypeParameter) { + const parent = ts.isJSDocTemplateTag(decl.parent) ? ts.getJSDocHost(decl.parent) : decl.parent; + if (parent === container) { + return !(ts.isJSDocTemplateTag(decl.parent) && ts.find((decl.parent.parent as ts.JSDoc).tags!, ts.isJSDocTypeAlias)); // TODO: GH#18217 } } } + } + + return false; + } + function checkAndReportErrorForMissingPrefix(errorLocation: ts.Node, name: ts.__String, nameArg: ts.__String | ts.Identifier): boolean { + if (!ts.isIdentifier(errorLocation) || errorLocation.escapedText !== name || isTypeReferenceIdentifier(errorLocation) || isInTypeQuery(errorLocation)) { return false; } - function checkAndReportErrorForMissingPrefix(errorLocation: ts.Node, name: ts.__String, nameArg: ts.__String | ts.Identifier): boolean { - if (!ts.isIdentifier(errorLocation) || errorLocation.escapedText !== name || isTypeReferenceIdentifier(errorLocation) || isInTypeQuery(errorLocation)) { - return false; - } + const container = ts.getThisContainer(errorLocation, /*includeArrowFunctions*/ false); + let location = container; + while (location) { + if (ts.isClassLike(location.parent)) { + const classSymbol = getSymbolOfNode(location.parent); + if (!classSymbol) { + break; + } - const container = ts.getThisContainer(errorLocation, /*includeArrowFunctions*/ false); - let location = container; - while (location) { - if (ts.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, ts.Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0, diagnosticName(nameArg), symbolToString(classSymbol)); + return true; + } - // Check to see if a static member exists. - const constructorType = getTypeOfSymbol(classSymbol); - if (getPropertyOfType(constructorType, name)) { - error(errorLocation, ts.Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0, diagnosticName(nameArg), symbolToString(classSymbol)); + // No static member is present. + // Check if we're in an instance method and look for a relevant instance member. + if (location === container && !ts.isStatic(location)) { + const instanceType = (getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType).thisType!; // TODO: GH#18217 + if (getPropertyOfType(instanceType, name)) { + error(errorLocation, ts.Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0, diagnosticName(nameArg)); 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 && !ts.isStatic(location)) { - const instanceType = (getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType).thisType!; // TODO: GH#18217 - if (getPropertyOfType(instanceType, name)) { - error(errorLocation, ts.Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0, diagnosticName(nameArg)); - return true; - } - } } - - location = location.parent; } - return false; + + location = location.parent; } + return false; + } - function checkAndReportErrorForExtendingInterface(errorLocation: ts.Node): boolean { - const expression = getEntityNameForExtendingInterface(errorLocation); - if (expression && resolveEntityName(expression, ts.SymbolFlags.Interface, /*ignoreErrors*/ true)) { - error(errorLocation, ts.Diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements, ts.getTextOfNode(expression)); - return true; - } - return false; + function checkAndReportErrorForExtendingInterface(errorLocation: ts.Node): boolean { + const expression = getEntityNameForExtendingInterface(errorLocation); + if (expression && resolveEntityName(expression, ts.SymbolFlags.Interface, /*ignoreErrors*/ true)) { + error(errorLocation, ts.Diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements, ts.getTextOfNode(expression)); + return true; } - /** - * Climbs up parents to an ExpressionWithTypeArguments, and returns its expression, - * but returns undefined if that expression is not an EntityNameExpression. - */ - function getEntityNameForExtendingInterface(node: ts.Node): ts.EntityNameExpression | undefined { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PropertyAccessExpression: - return node.parent ? getEntityNameForExtendingInterface(node.parent) : undefined; - case ts.SyntaxKind.ExpressionWithTypeArguments: - if (ts.isEntityNameExpression((node as ts.ExpressionWithTypeArguments).expression)) { - return (node as ts.ExpressionWithTypeArguments).expression as ts.EntityNameExpression; - } - // falls through - default: - return undefined; - } + 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: ts.Node): ts.EntityNameExpression | undefined { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: + return node.parent ? getEntityNameForExtendingInterface(node.parent) : undefined; + case ts.SyntaxKind.ExpressionWithTypeArguments: + if (ts.isEntityNameExpression((node as ts.ExpressionWithTypeArguments).expression)) { + return (node as ts.ExpressionWithTypeArguments).expression as ts.EntityNameExpression; + } + // falls through + default: + return undefined; } + } - function checkAndReportErrorForUsingTypeAsNamespace(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { - const namespaceMeaning = ts.SymbolFlags.Namespace | (ts.isInJSFile(errorLocation) ? ts.SymbolFlags.Value : 0); - if (meaning === namespaceMeaning) { - const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.Type & ~namespaceMeaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); - const parent = errorLocation.parent; - if (symbol) { - if (ts.isQualifiedName(parent)) { - ts.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, ts.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, ts.unescapeLeadingUnderscores(name), ts.unescapeLeadingUnderscores(propName)); - return true; - } + function checkAndReportErrorForUsingTypeAsNamespace(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { + const namespaceMeaning = ts.SymbolFlags.Namespace | (ts.isInJSFile(errorLocation) ? ts.SymbolFlags.Value : 0); + if (meaning === namespaceMeaning) { + const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.Type & ~namespaceMeaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + const parent = errorLocation.parent; + if (symbol) { + if (ts.isQualifiedName(parent)) { + ts.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, ts.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, ts.unescapeLeadingUnderscores(name), ts.unescapeLeadingUnderscores(propName)); + return true; } - error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_namespace_here, ts.unescapeLeadingUnderscores(name)); - return true; - } - } - - return false; - } - - function checkAndReportErrorForUsingValueAsType(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { - if (meaning & (ts.SymbolFlags.Type & ~ts.SymbolFlags.Namespace)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, ~ts.SymbolFlags.Type & ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol && !(symbol.flags & ts.SymbolFlags.Namespace)) { - error(errorLocation, ts.Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here_Did_you_mean_typeof_0, ts.unescapeLeadingUnderscores(name)); - return true; } + error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_namespace_here, ts.unescapeLeadingUnderscores(name)); + return true; } - return false; } - function isPrimitiveTypeName(name: ts.__String) { - return name === "any" || name === "string" || name === "number" || name === "boolean" || name === "never" || name === "unknown"; - } + return false; + } - function checkAndReportErrorForExportingPrimitiveType(errorLocation: ts.Node, name: ts.__String): boolean { - if (isPrimitiveTypeName(name) && errorLocation.parent.kind === ts.SyntaxKind.ExportSpecifier) { - error(errorLocation, ts.Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, name as string); + function checkAndReportErrorForUsingValueAsType(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { + if (meaning & (ts.SymbolFlags.Type & ~ts.SymbolFlags.Namespace)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, ~ts.SymbolFlags.Type & ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol && !(symbol.flags & ts.SymbolFlags.Namespace)) { + error(errorLocation, ts.Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here_Did_you_mean_typeof_0, ts.unescapeLeadingUnderscores(name)); return true; } - return false; } + return false; + } - function checkAndReportErrorForUsingTypeAsValue(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { - if (meaning & (ts.SymbolFlags.Value & ~ts.SymbolFlags.NamespaceModule)) { - if (isPrimitiveTypeName(name)) { - error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, ts.unescapeLeadingUnderscores(name)); - return true; + function isPrimitiveTypeName(name: ts.__String) { + return name === "any" || name === "string" || name === "number" || name === "boolean" || name === "never" || name === "unknown"; + } + + function checkAndReportErrorForExportingPrimitiveType(errorLocation: ts.Node, name: ts.__String): boolean { + if (isPrimitiveTypeName(name) && errorLocation.parent.kind === ts.SyntaxKind.ExportSpecifier) { + error(errorLocation, ts.Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, name as string); + return true; + } + return false; + } + + function checkAndReportErrorForUsingTypeAsValue(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { + if (meaning & (ts.SymbolFlags.Value & ~ts.SymbolFlags.NamespaceModule)) { + if (isPrimitiveTypeName(name)) { + error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, ts.unescapeLeadingUnderscores(name)); + return true; + } + const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.Type & ~ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol && !(symbol.flags & ts.SymbolFlags.NamespaceModule)) { + const rawName = ts.unescapeLeadingUnderscores(name); + if (isES2015OrLaterConstructorName(name)) { + error(errorLocation, ts.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, rawName); } - const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.Type & ~ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol && !(symbol.flags & ts.SymbolFlags.NamespaceModule)) { - const rawName = ts.unescapeLeadingUnderscores(name); - if (isES2015OrLaterConstructorName(name)) { - error(errorLocation, ts.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, rawName); - } - else if (maybeMappedType(errorLocation, symbol)) { - error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here_Did_you_mean_to_use_1_in_0, rawName, rawName === "K" ? "P" : "K"); - } - else { - error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, rawName); - } - return true; + else if (maybeMappedType(errorLocation, symbol)) { + error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here_Did_you_mean_to_use_1_in_0, rawName, rawName === "K" ? "P" : "K"); + } + else { + error(errorLocation, ts.Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, rawName); } + return true; } - return false; } + return false; + } - function maybeMappedType(node: ts.Node, symbol: ts.Symbol) { - const container = ts.findAncestor(node.parent, n => ts.isComputedPropertyName(n) || ts.isPropertySignature(n) ? false : ts.isTypeLiteralNode(n) || "quit") as ts.TypeLiteralNode | undefined; - if (container && container.members.length === 1) { - const type = getDeclaredTypeOfSymbol(symbol); - return !!(type.flags & ts.TypeFlags.Union) && allTypesAssignableToKind(type, ts.TypeFlags.StringOrNumberLiteral, /*strict*/ true); - } - return false; + function maybeMappedType(node: ts.Node, symbol: ts.Symbol) { + const container = ts.findAncestor(node.parent, n => ts.isComputedPropertyName(n) || ts.isPropertySignature(n) ? false : ts.isTypeLiteralNode(n) || "quit") as ts.TypeLiteralNode | undefined; + if (container && container.members.length === 1) { + const type = getDeclaredTypeOfSymbol(symbol); + return !!(type.flags & ts.TypeFlags.Union) && allTypesAssignableToKind(type, ts.TypeFlags.StringOrNumberLiteral, /*strict*/ true); } + return false; + } - function isES2015OrLaterConstructorName(n: ts.__String) { - switch (n) { - case "Promise": - case "Symbol": - case "Map": - case "WeakMap": - case "Set": - case "WeakSet": - return true; - } - return false; + function isES2015OrLaterConstructorName(n: ts.__String) { + switch (n) { + case "Promise": + case "Symbol": + case "Map": + case "WeakMap": + case "Set": + case "WeakSet": + return true; } + return false; + } - function checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { - if (meaning & (ts.SymbolFlags.Value & ~ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Type)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol) { - error(errorLocation, ts.Diagnostics.Cannot_use_namespace_0_as_a_value, ts.unescapeLeadingUnderscores(name)); - return true; - } + function checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation: ts.Node, name: ts.__String, meaning: ts.SymbolFlags): boolean { + if (meaning & (ts.SymbolFlags.Value & ~ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Type)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol) { + error(errorLocation, ts.Diagnostics.Cannot_use_namespace_0_as_a_value, ts.unescapeLeadingUnderscores(name)); + return true; } - else if (meaning & (ts.SymbolFlags.Type & ~ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Value)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) & ~ts.SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol) { - error(errorLocation, ts.Diagnostics.Cannot_use_namespace_0_as_a_type, ts.unescapeLeadingUnderscores(name)); - return true; - } + } + else if (meaning & (ts.SymbolFlags.Type & ~ts.SymbolFlags.NamespaceModule & ~ts.SymbolFlags.Value)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) & ~ts.SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol) { + error(errorLocation, ts.Diagnostics.Cannot_use_namespace_0_as_a_type, ts.unescapeLeadingUnderscores(name)); + return true; } - return false; } + return false; + } - function checkResolvedBlockScopedVariable(result: ts.Symbol, errorLocation: ts.Node): void { - ts.Debug.assert(!!(result.flags & ts.SymbolFlags.BlockScopedVariable || result.flags & ts.SymbolFlags.Class || result.flags & ts.SymbolFlags.Enum)); - if (result.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Assignment) && result.flags & ts.SymbolFlags.Class) { - // constructor functions aren't block scoped - return; + function checkResolvedBlockScopedVariable(result: ts.Symbol, errorLocation: ts.Node): void { + ts.Debug.assert(!!(result.flags & ts.SymbolFlags.BlockScopedVariable || result.flags & ts.SymbolFlags.Class || result.flags & ts.SymbolFlags.Enum)); + if (result.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Assignment) && result.flags & ts.SymbolFlags.Class) { + // constructor functions aren't block scoped + return; + } + // Block-scoped variables cannot be used before their definition + const declaration = result.declarations?.find(d => ts.isBlockOrCatchScoped(d) || ts.isClassLike(d) || (d.kind === ts.SyntaxKind.EnumDeclaration)); + if (declaration === undefined) + return ts.Debug.fail("checkResolvedBlockScopedVariable could not find block-scoped declaration"); + if (!(declaration.flags & ts.NodeFlags.Ambient) && !isBlockScopedNameDeclaredBeforeUse(declaration, errorLocation)) { + let diagnosticMessage; + const declarationName = ts.declarationNameToString(ts.getNameOfDeclaration(declaration)); + if (result.flags & ts.SymbolFlags.BlockScopedVariable) { + diagnosticMessage = error(errorLocation, ts.Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationName); } - // Block-scoped variables cannot be used before their definition - const declaration = result.declarations?.find(d => ts.isBlockOrCatchScoped(d) || ts.isClassLike(d) || (d.kind === ts.SyntaxKind.EnumDeclaration)); - if (declaration === undefined) - return ts.Debug.fail("checkResolvedBlockScopedVariable could not find block-scoped declaration"); - if (!(declaration.flags & ts.NodeFlags.Ambient) && !isBlockScopedNameDeclaredBeforeUse(declaration, errorLocation)) { - let diagnosticMessage; - const declarationName = ts.declarationNameToString(ts.getNameOfDeclaration(declaration)); - if (result.flags & ts.SymbolFlags.BlockScopedVariable) { - diagnosticMessage = error(errorLocation, ts.Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationName); - } - else if (result.flags & ts.SymbolFlags.Class) { - diagnosticMessage = error(errorLocation, ts.Diagnostics.Class_0_used_before_its_declaration, declarationName); - } - else if (result.flags & ts.SymbolFlags.RegularEnum) { + else if (result.flags & ts.SymbolFlags.Class) { + diagnosticMessage = error(errorLocation, ts.Diagnostics.Class_0_used_before_its_declaration, declarationName); + } + else if (result.flags & ts.SymbolFlags.RegularEnum) { + diagnosticMessage = error(errorLocation, ts.Diagnostics.Enum_0_used_before_its_declaration, declarationName); + } + else { + ts.Debug.assert(!!(result.flags & ts.SymbolFlags.ConstEnum)); + if (ts.shouldPreserveConstEnums(compilerOptions)) { diagnosticMessage = error(errorLocation, ts.Diagnostics.Enum_0_used_before_its_declaration, declarationName); } - else { - ts.Debug.assert(!!(result.flags & ts.SymbolFlags.ConstEnum)); - if (ts.shouldPreserveConstEnums(compilerOptions)) { - diagnosticMessage = error(errorLocation, ts.Diagnostics.Enum_0_used_before_its_declaration, declarationName); - } - } + } - if (diagnosticMessage) { - ts.addRelatedInfo(diagnosticMessage, ts.createDiagnosticForNode(declaration, ts.Diagnostics._0_is_declared_here, declarationName)); - } + if (diagnosticMessage) { + ts.addRelatedInfo(diagnosticMessage, ts.createDiagnosticForNode(declaration, ts.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. - * If current node is an IIFE, continue walking up. - * Return false if 'stopAt' node is reached or isFunctionLike(current) === true. - */ - function isSameScopeDescendentOf(initial: ts.Node, parent: ts.Node | undefined, stopAt: ts.Node): boolean { - return !!parent && !!ts.findAncestor(initial, n => n === parent - || (n === stopAt || ts.isFunctionLike(n) && !ts.getImmediatelyInvokedFunctionExpression(n) ? "quit" : false)); - } + /* 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. + * If current node is an IIFE, continue walking up. + * Return false if 'stopAt' node is reached or isFunctionLike(current) === true. + */ + function isSameScopeDescendentOf(initial: ts.Node, parent: ts.Node | undefined, stopAt: ts.Node): boolean { + return !!parent && !!ts.findAncestor(initial, n => n === parent + || (n === stopAt || ts.isFunctionLike(n) && !ts.getImmediatelyInvokedFunctionExpression(n) ? "quit" : false)); + } - function getAnyImportSyntax(node: ts.Node): ts.AnyImportSyntax | undefined { - switch (node.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - return node as ts.ImportEqualsDeclaration; - case ts.SyntaxKind.ImportClause: - return (node as ts.ImportClause).parent; - case ts.SyntaxKind.NamespaceImport: - return (node as ts.NamespaceImport).parent.parent; - case ts.SyntaxKind.ImportSpecifier: - return (node as ts.ImportSpecifier).parent.parent.parent; - default: - return undefined; - } + function getAnyImportSyntax(node: ts.Node): ts.AnyImportSyntax | undefined { + switch (node.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + return node as ts.ImportEqualsDeclaration; + case ts.SyntaxKind.ImportClause: + return (node as ts.ImportClause).parent; + case ts.SyntaxKind.NamespaceImport: + return (node as ts.NamespaceImport).parent.parent; + case ts.SyntaxKind.ImportSpecifier: + return (node as ts.ImportSpecifier).parent.parent.parent; + default: + return undefined; } + } - function getDeclarationOfAliasSymbol(symbol: ts.Symbol): ts.Declaration | undefined { - return symbol.declarations && ts.findLast(symbol.declarations, isAliasSymbolDeclaration); - } + function getDeclarationOfAliasSymbol(symbol: ts.Symbol): ts.Declaration | undefined { + return symbol.declarations && ts.findLast(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: } - * const { x } = require ... - */ - function isAliasSymbolDeclaration(node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.ImportEqualsDeclaration - || node.kind === ts.SyntaxKind.NamespaceExportDeclaration - || node.kind === ts.SyntaxKind.ImportClause && !!(node as ts.ImportClause).name - || node.kind === ts.SyntaxKind.NamespaceImport - || node.kind === ts.SyntaxKind.NamespaceExport - || node.kind === ts.SyntaxKind.ImportSpecifier - || node.kind === ts.SyntaxKind.ExportSpecifier - || node.kind === ts.SyntaxKind.ExportAssignment && ts.exportAssignmentIsAlias(node as ts.ExportAssignment) - || ts.isBinaryExpression(node) && ts.getAssignmentDeclarationKind(node) === ts.AssignmentDeclarationKind.ModuleExports && ts.exportAssignmentIsAlias(node) - || ts.isAccessExpression(node) - && ts.isBinaryExpression(node.parent) - && node.parent.left === node - && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken - && isAliasableOrJsExpression(node.parent.right) - || node.kind === ts.SyntaxKind.ShorthandPropertyAssignment - || node.kind === ts.SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as ts.PropertyAssignment).initializer) - || ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node); - } - - function isAliasableOrJsExpression(e: ts.Expression) { - return ts.isAliasableExpression(e) || ts.isFunctionExpression(e) && isJSConstructor(e); - } - - function getTargetOfImportEqualsDeclaration(node: ts.ImportEqualsDeclaration | ts.VariableDeclaration, dontResolveAlias: boolean): ts.Symbol | undefined { - const commonJSPropertyAccess = getCommonJSPropertyAccess(node); - if (commonJSPropertyAccess) { - const name = (ts.getLeftmostAccessExpression(commonJSPropertyAccess.expression) as ts.CallExpression).arguments[0] as ts.StringLiteral; - return ts.isIdentifier(commonJSPropertyAccess.name) - ? resolveSymbol(getPropertyOfType(resolveExternalModuleTypeByLiteral(name), commonJSPropertyAccess.name.escapedText)) - : undefined; - } - if (ts.isVariableDeclaration(node) || node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - const immediate = resolveExternalModuleName(node, ts.getExternalModuleRequireArgument(node) || ts.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; - } + /** + * 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: } + * const { x } = require ... + */ + function isAliasSymbolDeclaration(node: ts.Node): boolean { + return node.kind === ts.SyntaxKind.ImportEqualsDeclaration + || node.kind === ts.SyntaxKind.NamespaceExportDeclaration + || node.kind === ts.SyntaxKind.ImportClause && !!(node as ts.ImportClause).name + || node.kind === ts.SyntaxKind.NamespaceImport + || node.kind === ts.SyntaxKind.NamespaceExport + || node.kind === ts.SyntaxKind.ImportSpecifier + || node.kind === ts.SyntaxKind.ExportSpecifier + || node.kind === ts.SyntaxKind.ExportAssignment && ts.exportAssignmentIsAlias(node as ts.ExportAssignment) + || ts.isBinaryExpression(node) && ts.getAssignmentDeclarationKind(node) === ts.AssignmentDeclarationKind.ModuleExports && ts.exportAssignmentIsAlias(node) + || ts.isAccessExpression(node) + && ts.isBinaryExpression(node.parent) + && node.parent.left === node + && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + && isAliasableOrJsExpression(node.parent.right) + || node.kind === ts.SyntaxKind.ShorthandPropertyAssignment + || node.kind === ts.SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as ts.PropertyAssignment).initializer) + || ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node); + } - function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ts.ImportEqualsDeclaration, resolved: ts.Symbol | undefined) { - if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false) && !node.isTypeOnly) { - const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!; - const isExport = typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier; - const message = isExport - ? ts.Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type - : ts.Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type; - const relatedMessage = isExport - ? ts.Diagnostics._0_was_exported_here - : ts.Diagnostics._0_was_imported_here; - const name = ts.unescapeLeadingUnderscores(typeOnlyDeclaration.name.escapedText); - ts.addRelatedInfo(error(node.moduleReference, message), ts.createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name)); - } - } + function isAliasableOrJsExpression(e: ts.Expression) { + return ts.isAliasableExpression(e) || ts.isFunctionExpression(e) && isJSConstructor(e); + } - function resolveExportByName(moduleSymbol: ts.Symbol, name: ts.__String, sourceNode: ts.TypeOnlyCompatibleAliasDeclaration | undefined, dontResolveAlias: boolean) { - const exportValue = moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals); - const exportSymbol = exportValue ? getPropertyOfType(getTypeOfSymbol(exportValue), name) : moduleSymbol.exports!.get(name); - const resolved = resolveSymbol(exportSymbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(sourceNode, exportSymbol, resolved, /*overwriteEmpty*/ false); + function getTargetOfImportEqualsDeclaration(node: ts.ImportEqualsDeclaration | ts.VariableDeclaration, dontResolveAlias: boolean): ts.Symbol | undefined { + const commonJSPropertyAccess = getCommonJSPropertyAccess(node); + if (commonJSPropertyAccess) { + const name = (ts.getLeftmostAccessExpression(commonJSPropertyAccess.expression) as ts.CallExpression).arguments[0] as ts.StringLiteral; + return ts.isIdentifier(commonJSPropertyAccess.name) + ? resolveSymbol(getPropertyOfType(resolveExternalModuleTypeByLiteral(name), commonJSPropertyAccess.name.escapedText)) + : undefined; + } + if (ts.isVariableDeclaration(node) || node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { + const immediate = resolveExternalModuleName(node, ts.getExternalModuleRequireArgument(node) || ts.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 isSyntacticDefault(node: ts.Node) { - return ((ts.isExportAssignment(node) && !node.isExportEquals) || ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) || ts.isExportSpecifier(node)); + function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ts.ImportEqualsDeclaration, resolved: ts.Symbol | undefined) { + if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false) && !node.isTypeOnly) { + const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!; + const isExport = typeOnlyDeclaration.kind === ts.SyntaxKind.ExportSpecifier; + const message = isExport + ? ts.Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type + : ts.Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type; + const relatedMessage = isExport + ? ts.Diagnostics._0_was_exported_here + : ts.Diagnostics._0_was_imported_here; + const name = ts.unescapeLeadingUnderscores(typeOnlyDeclaration.name.escapedText); + ts.addRelatedInfo(error(node.moduleReference, message), ts.createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name)); } + } - function getUsageModeForExpression(usage: ts.Expression) { - return ts.isStringLiteralLike(usage) ? ts.getModeForUsageLocation(ts.getSourceFileOfNode(usage), usage) : undefined; - } + function resolveExportByName(moduleSymbol: ts.Symbol, name: ts.__String, sourceNode: ts.TypeOnlyCompatibleAliasDeclaration | undefined, dontResolveAlias: boolean) { + const exportValue = moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals); + const exportSymbol = exportValue ? getPropertyOfType(getTypeOfSymbol(exportValue), name) : moduleSymbol.exports!.get(name); + const resolved = resolveSymbol(exportSymbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(sourceNode, exportSymbol, resolved, /*overwriteEmpty*/ false); + return resolved; + } - function isESMFormatImportImportingCommonjsFormatFile(usageMode: ts.SourceFile["impliedNodeFormat"], targetMode: ts.SourceFile["impliedNodeFormat"]) { - return usageMode === ts.ModuleKind.ESNext && targetMode === ts.ModuleKind.CommonJS; - } + function isSyntacticDefault(node: ts.Node) { + return ((ts.isExportAssignment(node) && !node.isExportEquals) || ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) || ts.isExportSpecifier(node)); + } - function isOnlyImportedAsDefault(usage: ts.Expression) { - const usageMode = getUsageModeForExpression(usage); - return usageMode === ts.ModuleKind.ESNext && ts.endsWith((usage as ts.StringLiteralLike).text, ts.Extension.Json); - } + function getUsageModeForExpression(usage: ts.Expression) { + return ts.isStringLiteralLike(usage) ? ts.getModeForUsageLocation(ts.getSourceFileOfNode(usage), usage) : undefined; + } - function canHaveSyntheticDefault(file: ts.SourceFile | undefined, moduleSymbol: ts.Symbol, dontResolveAlias: boolean, usage: ts.Expression) { - const usageMode = file && getUsageModeForExpression(usage); - if (file && usageMode !== undefined) { - const result = isESMFormatImportImportingCommonjsFormatFile(usageMode, file.impliedNodeFormat); - if (usageMode === ts.ModuleKind.ESNext || result) { - return result; - } - // fallthrough on cjs usages so we imply defaults for interop'd imports, too + function isESMFormatImportImportingCommonjsFormatFile(usageMode: ts.SourceFile["impliedNodeFormat"], targetMode: ts.SourceFile["impliedNodeFormat"]) { + return usageMode === ts.ModuleKind.ESNext && targetMode === ts.ModuleKind.CommonJS; + } + + function isOnlyImportedAsDefault(usage: ts.Expression) { + const usageMode = getUsageModeForExpression(usage); + return usageMode === ts.ModuleKind.ESNext && ts.endsWith((usage as ts.StringLiteralLike).text, ts.Extension.Json); + } + + function canHaveSyntheticDefault(file: ts.SourceFile | undefined, moduleSymbol: ts.Symbol, dontResolveAlias: boolean, usage: ts.Expression) { + const usageMode = file && getUsageModeForExpression(usage); + if (file && usageMode !== undefined) { + const result = isESMFormatImportImportingCommonjsFormatFile(usageMode, file.impliedNodeFormat); + if (usageMode === ts.ModuleKind.ESNext || result) { + return result; } - if (!allowSyntheticDefaultImports) { + // fallthrough on cjs usages so we imply defaults for interop'd imports, too + } + if (!allowSyntheticDefaultImports) { + return false; + } + // 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, ts.InternalSymbolName.Default, /*sourceNode*/ undefined, /*dontResolveAlias*/ true); // Dont resolve alias because we want the immediately exported symbol's declaration + if (defaultExportSymbol && ts.some(defaultExportSymbol.declarations, isSyntacticDefault)) { return false; } - // 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, ts.InternalSymbolName.Default, /*sourceNode*/ undefined, /*dontResolveAlias*/ true); // Dont resolve alias because we want the immediately exported symbol's declaration - if (defaultExportSymbol && ts.some(defaultExportSymbol.declarations, isSyntacticDefault)) { - return false; - } - // 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, ts.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; - } - // 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 (!ts.isSourceFileJS(file)) { - return hasExportAssignmentSymbol(moduleSymbol); + // 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, ts.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; } - // 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, ts.escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias); + // 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 (!ts.isSourceFileJS(file)) { + return hasExportAssignmentSymbol(moduleSymbol); } + // 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, ts.escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias); + } - function getTargetOfImportClause(node: ts.ImportClause, dontResolveAlias: boolean): ts.Symbol | undefined { - const moduleSymbol = resolveExternalModuleName(node, node.parent.moduleSpecifier); - if (moduleSymbol) { - let exportDefaultSymbol: ts.Symbol | undefined; - if (ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { - exportDefaultSymbol = moduleSymbol; - } - else { - exportDefaultSymbol = resolveExportByName(moduleSymbol, ts.InternalSymbolName.Default, node, dontResolveAlias); - } + function getTargetOfImportClause(node: ts.ImportClause, dontResolveAlias: boolean): ts.Symbol | undefined { + const moduleSymbol = resolveExternalModuleName(node, node.parent.moduleSpecifier); + if (moduleSymbol) { + let exportDefaultSymbol: ts.Symbol | undefined; + if (ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { + exportDefaultSymbol = moduleSymbol; + } + else { + exportDefaultSymbol = resolveExportByName(moduleSymbol, ts.InternalSymbolName.Default, node, dontResolveAlias); + } - const file = moduleSymbol.declarations?.find(ts.isSourceFile); - const hasDefaultOnly = isOnlyImportedAsDefault(node.parent.moduleSpecifier); - const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, node.parent.moduleSpecifier); - if (!exportDefaultSymbol && !hasSyntheticDefault && !hasDefaultOnly) { - if (hasExportAssignmentSymbol(moduleSymbol)) { - const compilerOptionName = moduleKind >= ts.ModuleKind.ES2015 ? "allowSyntheticDefaultImports" : "esModuleInterop"; - const exportEqualsSymbol = moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals); - const exportAssignment = exportEqualsSymbol!.valueDeclaration; - const err = error(node.name, ts.Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), compilerOptionName); + const file = moduleSymbol.declarations?.find(ts.isSourceFile); + const hasDefaultOnly = isOnlyImportedAsDefault(node.parent.moduleSpecifier); + const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, node.parent.moduleSpecifier); + if (!exportDefaultSymbol && !hasSyntheticDefault && !hasDefaultOnly) { + if (hasExportAssignmentSymbol(moduleSymbol)) { + const compilerOptionName = moduleKind >= ts.ModuleKind.ES2015 ? "allowSyntheticDefaultImports" : "esModuleInterop"; + const exportEqualsSymbol = moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals); + const exportAssignment = exportEqualsSymbol!.valueDeclaration; + const err = error(node.name, ts.Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), compilerOptionName); - if (exportAssignment) { - ts.addRelatedInfo(err, ts.createDiagnosticForNode(exportAssignment, ts.Diagnostics.This_module_is_declared_with_using_export_and_can_only_be_used_with_a_default_import_when_using_the_0_flag, compilerOptionName)); - } - } - else { - reportNonDefaultExport(moduleSymbol, node); + if (exportAssignment) { + ts.addRelatedInfo(err, ts.createDiagnosticForNode(exportAssignment, ts.Diagnostics.This_module_is_declared_with_using_export_and_can_only_be_used_with_a_default_import_when_using_the_0_flag, compilerOptionName)); } } - else if (hasSyntheticDefault || hasDefaultOnly) { - // 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; + else { + reportNonDefaultExport(moduleSymbol, node); } - markSymbolOfAliasDeclarationIfTypeOnly(node, exportDefaultSymbol, /*finalTarget*/ undefined, /*overwriteTypeOnly*/ false); - return exportDefaultSymbol; } + else if (hasSyntheticDefault || hasDefaultOnly) { + // 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; } + } - function reportNonDefaultExport(moduleSymbol: ts.Symbol, node: ts.ImportClause) { - if (moduleSymbol.exports?.has(node.symbol.escapedName)) { - error(node.name, ts.Diagnostics.Module_0_has_no_default_export_Did_you_mean_to_use_import_1_from_0_instead, symbolToString(moduleSymbol), symbolToString(node.symbol)); - } - else { - const diagnostic = error(node.name, ts.Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol)); - const exportStar = moduleSymbol.exports?.get(ts.InternalSymbolName.ExportStar); - if (exportStar) { - const defaultExport = exportStar.declarations?.find(decl => !!(ts.isExportDeclaration(decl) && decl.moduleSpecifier && - resolveExternalModuleName(decl, decl.moduleSpecifier)?.exports?.has(ts.InternalSymbolName.Default))); - if (defaultExport) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(defaultExport, ts.Diagnostics.export_Asterisk_does_not_re_export_a_default)); - } + function reportNonDefaultExport(moduleSymbol: ts.Symbol, node: ts.ImportClause) { + if (moduleSymbol.exports?.has(node.symbol.escapedName)) { + error(node.name, ts.Diagnostics.Module_0_has_no_default_export_Did_you_mean_to_use_import_1_from_0_instead, symbolToString(moduleSymbol), symbolToString(node.symbol)); + } + else { + const diagnostic = error(node.name, ts.Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol)); + const exportStar = moduleSymbol.exports?.get(ts.InternalSymbolName.ExportStar); + if (exportStar) { + const defaultExport = exportStar.declarations?.find(decl => !!(ts.isExportDeclaration(decl) && decl.moduleSpecifier && + resolveExternalModuleName(decl, decl.moduleSpecifier)?.exports?.has(ts.InternalSymbolName.Default))); + if (defaultExport) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(defaultExport, ts.Diagnostics.export_Asterisk_does_not_re_export_a_default)); } } } + } - function getTargetOfNamespaceImport(node: ts.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 getTargetOfNamespaceImport(node: ts.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: ts.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 & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) { + return valueSymbol; + } + const result = createSymbol(valueSymbol.flags | typeSymbol.flags, valueSymbol.escapedName); + result.declarations = ts.deduplicate(ts.concatenate(valueSymbol.declarations, typeSymbol.declarations), ts.equateValues); + result.parent = valueSymbol.parent || typeSymbol.parent; + if (valueSymbol.valueDeclaration) + result.valueDeclaration = valueSymbol.valueDeclaration; + if (typeSymbol.members) + result.members = new ts.Map(typeSymbol.members); + if (valueSymbol.exports) + result.exports = new ts.Map(valueSymbol.exports); + return result; + } - function getTargetOfNamespaceExport(node: ts.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); + function getExportOfModule(symbol: ts.Symbol, name: ts.Identifier, specifier: ts.Declaration, dontResolveAlias: boolean): ts.Symbol | undefined { + if (symbol.flags & ts.SymbolFlags.Module) { + const exportSymbol = getExportsOfSymbol(symbol).get(name.escapedText); + const resolved = resolveSymbol(exportSymbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(specifier, exportSymbol, 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 & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) { - return valueSymbol; - } - const result = createSymbol(valueSymbol.flags | typeSymbol.flags, valueSymbol.escapedName); - result.declarations = ts.deduplicate(ts.concatenate(valueSymbol.declarations, typeSymbol.declarations), ts.equateValues); - result.parent = valueSymbol.parent || typeSymbol.parent; - if (valueSymbol.valueDeclaration) - result.valueDeclaration = valueSymbol.valueDeclaration; - if (typeSymbol.members) - result.members = new ts.Map(typeSymbol.members); - if (valueSymbol.exports) - result.exports = new ts.Map(valueSymbol.exports); - return result; + function getPropertyOfVariable(symbol: ts.Symbol, name: ts.__String): ts.Symbol | undefined { + if (symbol.flags & ts.SymbolFlags.Variable) { + const typeAnnotation = (symbol.valueDeclaration as ts.VariableDeclaration).type; + if (typeAnnotation) { + return resolveSymbol(getPropertyOfType(getTypeFromTypeNode(typeAnnotation), name)); + } } + } - function getExportOfModule(symbol: ts.Symbol, name: ts.Identifier, specifier: ts.Declaration, dontResolveAlias: boolean): ts.Symbol | undefined { - if (symbol.flags & ts.SymbolFlags.Module) { - const exportSymbol = getExportsOfSymbol(symbol).get(name.escapedText); - const resolved = resolveSymbol(exportSymbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(specifier, exportSymbol, resolved, /*overwriteEmpty*/ false); - return resolved; - } + function getExternalModuleMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, specifier: ts.ImportOrExportSpecifier | ts.BindingElement | ts.PropertyAccessExpression, dontResolveAlias = false): ts.Symbol | undefined { + const moduleSpecifier = ts.getExternalModuleRequireArgument(node) || (node as ts.ImportDeclaration | ts.ExportDeclaration).moduleSpecifier!; + const moduleSymbol = resolveExternalModuleName(node, moduleSpecifier)!; // TODO: GH#18217 + const name = !ts.isPropertyAccessExpression(specifier) && specifier.propertyName || specifier.name; + if (!ts.isIdentifier(name)) { + return undefined; } + const suppressInteropError = name.escapedText === ts.InternalSymbolName.Default && !!(compilerOptions.allowSyntheticDefaultImports || ts.getESModuleInterop(compilerOptions)); + const targetSymbol = resolveESModuleSymbol(moduleSymbol, moduleSpecifier, /*dontResolveAlias*/ false, suppressInteropError); + if (targetSymbol) { + if (name.escapedText) { + if (ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { + return moduleSymbol; + } - function getPropertyOfVariable(symbol: ts.Symbol, name: ts.__String): ts.Symbol | undefined { - if (symbol.flags & ts.SymbolFlags.Variable) { - const typeAnnotation = (symbol.valueDeclaration as ts.VariableDeclaration).type; - if (typeAnnotation) { - return resolveSymbol(getPropertyOfType(getTypeFromTypeNode(typeAnnotation), name)); + 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(ts.InternalSymbolName.ExportEquals)) { + symbolFromVariable = getPropertyOfType(getTypeOfSymbol(targetSymbol), name.escapedText, /*skipObjectFunctionPropertyAugment*/ true); } - } - } + else { + symbolFromVariable = getPropertyOfVariable(targetSymbol, name.escapedText); + } + // if symbolFromVariable is export - get its final target + symbolFromVariable = resolveSymbol(symbolFromVariable, dontResolveAlias); - function getExternalModuleMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, specifier: ts.ImportOrExportSpecifier | ts.BindingElement | ts.PropertyAccessExpression, dontResolveAlias = false): ts.Symbol | undefined { - const moduleSpecifier = ts.getExternalModuleRequireArgument(node) || (node as ts.ImportDeclaration | ts.ExportDeclaration).moduleSpecifier!; - const moduleSymbol = resolveExternalModuleName(node, moduleSpecifier)!; // TODO: GH#18217 - const name = !ts.isPropertyAccessExpression(specifier) && specifier.propertyName || specifier.name; - if (!ts.isIdentifier(name)) { - return undefined; - } - const suppressInteropError = name.escapedText === ts.InternalSymbolName.Default && !!(compilerOptions.allowSyntheticDefaultImports || ts.getESModuleInterop(compilerOptions)); - const targetSymbol = resolveESModuleSymbol(moduleSymbol, moduleSpecifier, /*dontResolveAlias*/ false, suppressInteropError); - if (targetSymbol) { - if (name.escapedText) { - if (ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { - return moduleSymbol; + let symbolFromModule = getExportOfModule(targetSymbol, name, specifier, dontResolveAlias); + if (symbolFromModule === undefined && name.escapedText === ts.InternalSymbolName.Default) { + const file = moduleSymbol.declarations?.find(ts.isSourceFile); + if (isOnlyImportedAsDefault(moduleSpecifier) || canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, moduleSpecifier)) { + symbolFromModule = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias); } + } - 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(ts.InternalSymbolName.ExportEquals)) { - symbolFromVariable = getPropertyOfType(getTypeOfSymbol(targetSymbol), name.escapedText, /*skipObjectFunctionPropertyAugment*/ true); + const symbol = symbolFromModule && symbolFromVariable && symbolFromModule !== symbolFromVariable ? + combineValueAndTypeSymbols(symbolFromVariable, symbolFromModule) : + symbolFromModule || symbolFromVariable; + if (!symbol) { + const moduleName = getFullyQualifiedName(moduleSymbol, node); + const declarationName = ts.declarationNameToString(name); + const suggestion = getSuggestedSymbolForNonexistentModule(name, targetSymbol); + if (suggestion !== undefined) { + const suggestionName = symbolToString(suggestion); + const diagnostic = error(name, ts.Diagnostics._0_has_no_exported_member_named_1_Did_you_mean_2, moduleName, declarationName, suggestionName); + if (suggestion.valueDeclaration) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestionName)); + } } else { - symbolFromVariable = getPropertyOfVariable(targetSymbol, name.escapedText); - } - // if symbolFromVariable is export - get its final target - symbolFromVariable = resolveSymbol(symbolFromVariable, dontResolveAlias); - - let symbolFromModule = getExportOfModule(targetSymbol, name, specifier, dontResolveAlias); - if (symbolFromModule === undefined && name.escapedText === ts.InternalSymbolName.Default) { - const file = moduleSymbol.declarations?.find(ts.isSourceFile); - if (isOnlyImportedAsDefault(moduleSpecifier) || canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, moduleSpecifier)) { - 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 = ts.declarationNameToString(name); - const suggestion = getSuggestedSymbolForNonexistentModule(name, targetSymbol); - if (suggestion !== undefined) { - const suggestionName = symbolToString(suggestion); - const diagnostic = error(name, ts.Diagnostics._0_has_no_exported_member_named_1_Did_you_mean_2, moduleName, declarationName, suggestionName); - if (suggestion.valueDeclaration) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestionName)); - } + if (moduleSymbol.exports?.has(ts.InternalSymbolName.Default)) { + error(name, ts.Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_to_use_import_1_from_0_instead, moduleName, declarationName); } else { - if (moduleSymbol.exports?.has(ts.InternalSymbolName.Default)) { - error(name, ts.Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_to_use_import_1_from_0_instead, moduleName, declarationName); - } - else { - reportNonExportedMember(node, name, declarationName, moduleSymbol, moduleName); - } + reportNonExportedMember(node, name, declarationName, moduleSymbol, moduleName); } } - return symbol; } + return symbol; } } + } - function reportNonExportedMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, name: ts.Identifier, declarationName: string, moduleSymbol: ts.Symbol, moduleName: string): void { - const localSymbol = moduleSymbol.valueDeclaration?.locals?.get(name.escapedText); - const exports = moduleSymbol.exports; - if (localSymbol) { - const exportedEqualsSymbol = exports?.get(ts.InternalSymbolName.ExportEquals); - if (exportedEqualsSymbol) { - getSymbolIfSameReference(exportedEqualsSymbol, localSymbol) ? reportInvalidImportEqualsExportMember(node, name, declarationName, moduleName) : - error(name, ts.Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); - } - else { - const exportedSymbol = exports ? ts.find(symbolsToArray(exports), symbol => !!getSymbolIfSameReference(symbol, localSymbol)) : undefined; - const diagnostic = exportedSymbol ? error(name, ts.Diagnostics.Module_0_declares_1_locally_but_it_is_exported_as_2, moduleName, declarationName, symbolToString(exportedSymbol)) : - error(name, ts.Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported, moduleName, declarationName); - if (localSymbol.declarations) { - ts.addRelatedInfo(diagnostic, ...ts.map(localSymbol.declarations, (decl, index) => ts.createDiagnosticForNode(decl, index === 0 ? ts.Diagnostics._0_is_declared_here : ts.Diagnostics.and_here, declarationName))); - } - } + function reportNonExportedMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, name: ts.Identifier, declarationName: string, moduleSymbol: ts.Symbol, moduleName: string): void { + const localSymbol = moduleSymbol.valueDeclaration?.locals?.get(name.escapedText); + const exports = moduleSymbol.exports; + if (localSymbol) { + const exportedEqualsSymbol = exports?.get(ts.InternalSymbolName.ExportEquals); + if (exportedEqualsSymbol) { + getSymbolIfSameReference(exportedEqualsSymbol, localSymbol) ? reportInvalidImportEqualsExportMember(node, name, declarationName, moduleName) : + error(name, ts.Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); } else { - error(name, ts.Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); + const exportedSymbol = exports ? ts.find(symbolsToArray(exports), symbol => !!getSymbolIfSameReference(symbol, localSymbol)) : undefined; + const diagnostic = exportedSymbol ? error(name, ts.Diagnostics.Module_0_declares_1_locally_but_it_is_exported_as_2, moduleName, declarationName, symbolToString(exportedSymbol)) : + error(name, ts.Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported, moduleName, declarationName); + if (localSymbol.declarations) { + ts.addRelatedInfo(diagnostic, ...ts.map(localSymbol.declarations, (decl, index) => ts.createDiagnosticForNode(decl, index === 0 ? ts.Diagnostics._0_is_declared_here : ts.Diagnostics.and_here, declarationName))); + } } } + else { + error(name, ts.Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); + } + } - function reportInvalidImportEqualsExportMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, name: ts.Identifier, declarationName: string, moduleName: string) { - if (moduleKind >= ts.ModuleKind.ES2015) { - const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_a_default_import : - ts.Diagnostics._0_can_only_be_imported_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; + function reportInvalidImportEqualsExportMember(node: ts.ImportDeclaration | ts.ExportDeclaration | ts.VariableDeclaration, name: ts.Identifier, declarationName: string, moduleName: string) { + if (moduleKind >= ts.ModuleKind.ES2015) { + const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_a_default_import : + ts.Diagnostics._0_can_only_be_imported_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; + error(name, message, declarationName); + } + else { + if (ts.isInJSFile(node)) { + const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_a_require_call_or_by_using_a_default_import : + ts.Diagnostics._0_can_only_be_imported_by_using_a_require_call_or_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; error(name, message, declarationName); } else { - if (ts.isInJSFile(node)) { - const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_a_require_call_or_by_using_a_default_import : - ts.Diagnostics._0_can_only_be_imported_by_using_a_require_call_or_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; - error(name, message, declarationName); - } - else { - const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_import_1_require_2_or_a_default_import : - ts.Diagnostics._0_can_only_be_imported_by_using_import_1_require_2_or_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; - error(name, message, declarationName, declarationName, moduleName); - } + const message = ts.getESModuleInterop(compilerOptions) ? ts.Diagnostics._0_can_only_be_imported_by_using_import_1_require_2_or_a_default_import : + ts.Diagnostics._0_can_only_be_imported_by_using_import_1_require_2_or_by_turning_on_the_esModuleInterop_flag_and_using_a_default_import; + error(name, message, declarationName, declarationName, moduleName); } } + } - function getTargetOfImportSpecifier(node: ts.ImportSpecifier | ts.BindingElement, dontResolveAlias: boolean): ts.Symbol | undefined { - const root = ts.isBindingElement(node) ? ts.getRootDeclaration(node) as ts.VariableDeclaration : node.parent.parent.parent; - const commonJSPropertyAccess = getCommonJSPropertyAccess(root); - const resolved = getExternalModuleMember(root, commonJSPropertyAccess || node, dontResolveAlias); - const name = node.propertyName || node.name; - if (commonJSPropertyAccess && resolved && ts.isIdentifier(name)) { - return resolveSymbol(getPropertyOfType(getTypeOfSymbol(resolved), name.escapedText), dontResolveAlias); - } - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + function getTargetOfImportSpecifier(node: ts.ImportSpecifier | ts.BindingElement, dontResolveAlias: boolean): ts.Symbol | undefined { + const root = ts.isBindingElement(node) ? ts.getRootDeclaration(node) as ts.VariableDeclaration : node.parent.parent.parent; + const commonJSPropertyAccess = getCommonJSPropertyAccess(root); + const resolved = getExternalModuleMember(root, commonJSPropertyAccess || node, dontResolveAlias); + const name = node.propertyName || node.name; + if (commonJSPropertyAccess && resolved && ts.isIdentifier(name)) { + return resolveSymbol(getPropertyOfType(getTypeOfSymbol(resolved), name.escapedText), dontResolveAlias); } + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } - function getCommonJSPropertyAccess(node: ts.Node) { - if (ts.isVariableDeclaration(node) && node.initializer && ts.isPropertyAccessExpression(node.initializer)) { - return node.initializer; - } + function getCommonJSPropertyAccess(node: ts.Node) { + if (ts.isVariableDeclaration(node) && node.initializer && ts.isPropertyAccessExpression(node.initializer)) { + return node.initializer; } + } - function getTargetOfNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration, dontResolveAlias: boolean): ts.Symbol { - const resolved = resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + function getTargetOfNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration, dontResolveAlias: boolean): ts.Symbol { + const resolved = resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + + function getTargetOfExportSpecifier(node: ts.ExportSpecifier, meaning: ts.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: ts.ExportAssignment | ts.BinaryExpression, dontResolveAlias: boolean): ts.Symbol | undefined { + const expression = ts.isExportAssignment(node) ? node.expression : node.right; + const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + + function getTargetOfAliasLikeExpression(expression: ts.Expression, dontResolveAlias: boolean) { + if (ts.isClassExpression(expression)) { + return checkExpressionCached(expression).symbol; + } + if (!ts.isEntityName(expression) && !ts.isEntityNameExpression(expression)) { + return undefined; + } + const aliasLike = resolveEntityName(expression, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ true, dontResolveAlias); + if (aliasLike) { + return aliasLike; } + checkExpressionCached(expression); + return getNodeLinks(expression).resolvedSymbol; + } - function getTargetOfExportSpecifier(node: ts.ExportSpecifier, meaning: ts.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 getTargetOfPropertyAssignment(node: ts.PropertyAssignment, dontRecursivelyResolve: boolean): ts.Symbol | undefined { + const expression = node.initializer; + return getTargetOfAliasLikeExpression(expression, dontRecursivelyResolve); + } + + function getTargetOfAccessExpression(node: ts.AccessExpression, dontRecursivelyResolve: boolean): ts.Symbol | undefined { + if (!(ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)) { + return undefined; } - function getTargetOfExportAssignment(node: ts.ExportAssignment | ts.BinaryExpression, dontResolveAlias: boolean): ts.Symbol | undefined { - const expression = ts.isExportAssignment(node) ? node.expression : node.right; - const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + return getTargetOfAliasLikeExpression(node.parent.right, dontRecursivelyResolve); + } + + function getTargetOfAliasDeclaration(node: ts.Declaration, dontRecursivelyResolve = false): ts.Symbol | undefined { + switch (node.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.VariableDeclaration: + return getTargetOfImportEqualsDeclaration(node as ts.ImportEqualsDeclaration | ts.VariableDeclaration, dontRecursivelyResolve); + case ts.SyntaxKind.ImportClause: + return getTargetOfImportClause(node as ts.ImportClause, dontRecursivelyResolve); + case ts.SyntaxKind.NamespaceImport: + return getTargetOfNamespaceImport(node as ts.NamespaceImport, dontRecursivelyResolve); + case ts.SyntaxKind.NamespaceExport: + return getTargetOfNamespaceExport(node as ts.NamespaceExport, dontRecursivelyResolve); + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.BindingElement: + return getTargetOfImportSpecifier(node as ts.ImportSpecifier | ts.BindingElement, dontRecursivelyResolve); + case ts.SyntaxKind.ExportSpecifier: + return getTargetOfExportSpecifier(node as ts.ExportSpecifier, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, dontRecursivelyResolve); + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.BinaryExpression: + return getTargetOfExportAssignment((node as ts.ExportAssignment | ts.BinaryExpression), dontRecursivelyResolve); + case ts.SyntaxKind.NamespaceExportDeclaration: + return getTargetOfNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration, dontRecursivelyResolve); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return resolveEntityName((node as ts.ShorthandPropertyAssignment).name, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ true, dontRecursivelyResolve); + case ts.SyntaxKind.PropertyAssignment: + return getTargetOfPropertyAssignment(node as ts.PropertyAssignment, dontRecursivelyResolve); + case ts.SyntaxKind.ElementAccessExpression: + case ts.SyntaxKind.PropertyAccessExpression: + return getTargetOfAccessExpression(node as ts.AccessExpression, dontRecursivelyResolve); + default: + return ts.Debug.fail(); } + } - function getTargetOfAliasLikeExpression(expression: ts.Expression, dontResolveAlias: boolean) { - if (ts.isClassExpression(expression)) { - return checkExpressionCached(expression).symbol; - } - if (!ts.isEntityName(expression) && !ts.isEntityNameExpression(expression)) { - return undefined; + /** + * 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 = ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace): symbol is ts.Symbol { + if (!symbol) + return false; + return (symbol.flags & (ts.SymbolFlags.Alias | excludes)) === ts.SymbolFlags.Alias || !!(symbol.flags & ts.SymbolFlags.Alias && symbol.flags & ts.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 { + ts.Debug.assert((symbol.flags & ts.SymbolFlags.Alias) !== 0, "Should only get Alias here."); + const links = getSymbolLinks(symbol); + if (!links.aliasTarget) { + links.aliasTarget = resolvingSymbol; + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return ts.Debug.fail(); + const target = getTargetOfAliasDeclaration(node); + if (links.aliasTarget === resolvingSymbol) { + links.aliasTarget = target || unknownSymbol; } - const aliasLike = resolveEntityName(expression, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ true, dontResolveAlias); - if (aliasLike) { - return aliasLike; + else { + error(node, ts.Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol)); } - checkExpressionCached(expression); - return getNodeLinks(expression).resolvedSymbol; } + else if (links.aliasTarget === resolvingSymbol) { + links.aliasTarget = unknownSymbol; + } + return links.aliasTarget; + } - function getTargetOfPropertyAssignment(node: ts.PropertyAssignment, dontRecursivelyResolve: boolean): ts.Symbol | undefined { - const expression = node.initializer; - return getTargetOfAliasLikeExpression(expression, dontRecursivelyResolve); + function tryResolveAlias(symbol: ts.Symbol): ts.Symbol | undefined { + const links = getSymbolLinks(symbol); + if (links.aliasTarget !== resolvingSymbol) { + return resolveAlias(symbol); } - function getTargetOfAccessExpression(node: ts.AccessExpression, dontRecursivelyResolve: boolean): ts.Symbol | undefined { - if (!(ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)) { - return undefined; - } + return undefined; + } - return getTargetOfAliasLikeExpression(node.parent.right, dontRecursivelyResolve); + /** + * 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 + * @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` + * 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. + */ + function markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration: ts.Declaration | undefined, immediateTarget: ts.Symbol | undefined, finalTarget: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { + if (!aliasDeclaration || ts.isPropertyAccessExpression(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 (ts.isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) { + const links = getSymbolLinks(sourceSymbol); + links.typeOnlyDeclaration = aliasDeclaration; + return true; } - function getTargetOfAliasDeclaration(node: ts.Declaration, dontRecursivelyResolve = false): ts.Symbol | undefined { - switch (node.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.VariableDeclaration: - return getTargetOfImportEqualsDeclaration(node as ts.ImportEqualsDeclaration | ts.VariableDeclaration, dontRecursivelyResolve); - case ts.SyntaxKind.ImportClause: - return getTargetOfImportClause(node as ts.ImportClause, dontRecursivelyResolve); - case ts.SyntaxKind.NamespaceImport: - return getTargetOfNamespaceImport(node as ts.NamespaceImport, dontRecursivelyResolve); - case ts.SyntaxKind.NamespaceExport: - return getTargetOfNamespaceExport(node as ts.NamespaceExport, dontRecursivelyResolve); - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.BindingElement: - return getTargetOfImportSpecifier(node as ts.ImportSpecifier | ts.BindingElement, dontRecursivelyResolve); - case ts.SyntaxKind.ExportSpecifier: - return getTargetOfExportSpecifier(node as ts.ExportSpecifier, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, dontRecursivelyResolve); - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.BinaryExpression: - return getTargetOfExportAssignment((node as ts.ExportAssignment | ts.BinaryExpression), dontRecursivelyResolve); - case ts.SyntaxKind.NamespaceExportDeclaration: - return getTargetOfNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration, dontRecursivelyResolve); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return resolveEntityName((node as ts.ShorthandPropertyAssignment).name, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ true, dontRecursivelyResolve); - case ts.SyntaxKind.PropertyAssignment: - return getTargetOfPropertyAssignment(node as ts.PropertyAssignment, dontRecursivelyResolve); - case ts.SyntaxKind.ElementAccessExpression: - case ts.SyntaxKind.PropertyAccessExpression: - return getTargetOfAccessExpression(node as ts.AccessExpression, dontRecursivelyResolve); - default: - return ts.Debug.fail(); - } + const links = getSymbolLinks(sourceSymbol); + return markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, immediateTarget, overwriteEmpty) + || markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, finalTarget, overwriteEmpty); + } + + function markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationLinks: ts.SymbolLinks, target: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { + if (target && (aliasDeclarationLinks.typeOnlyDeclaration === undefined || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration === false)) { + const exportSymbol = target.exports?.get(ts.InternalSymbolName.ExportEquals) ?? target; + const typeOnly = exportSymbol.declarations && ts.find(exportSymbol.declarations, ts.isTypeOnlyImportOrExportDeclaration); + aliasDeclarationLinks.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(exportSymbol).typeOnlyDeclaration ?? false; } + return !!aliasDeclarationLinks.typeOnlyDeclaration; + } - /** - * 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 = ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace): symbol is ts.Symbol { - if (!symbol) - return false; - return (symbol.flags & (ts.SymbolFlags.Alias | excludes)) === ts.SymbolFlags.Alias || !!(symbol.flags & ts.SymbolFlags.Alias && symbol.flags & ts.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 { - ts.Debug.assert((symbol.flags & ts.SymbolFlags.Alias) !== 0, "Should only get Alias here."); - const links = getSymbolLinks(symbol); - if (!links.aliasTarget) { - links.aliasTarget = resolvingSymbol; - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) - return ts.Debug.fail(); - const target = getTargetOfAliasDeclaration(node); - if (links.aliasTarget === resolvingSymbol) { - links.aliasTarget = target || unknownSymbol; - } - else { - error(node, ts.Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol)); - } - } - else if (links.aliasTarget === resolvingSymbol) { - links.aliasTarget = unknownSymbol; - } - return links.aliasTarget; - } - - function tryResolveAlias(symbol: ts.Symbol): ts.Symbol | undefined { - const links = getSymbolLinks(symbol); - if (links.aliasTarget !== resolvingSymbol) { - return resolveAlias(symbol); - } - + /** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */ + function getTypeOnlyAliasDeclaration(symbol: ts.Symbol): ts.TypeOnlyAliasDeclaration | undefined { + if (!(symbol.flags & ts.SymbolFlags.Alias)) { return undefined; } + const links = getSymbolLinks(symbol); + return links.typeOnlyDeclaration || undefined; + } - /** - * 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 - * @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` - * 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. - */ - function markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration: ts.Declaration | undefined, immediateTarget: ts.Symbol | undefined, finalTarget: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { - if (!aliasDeclaration || ts.isPropertyAccessExpression(aliasDeclaration)) - return false; + function markExportAsReferenced(node: ts.ImportEqualsDeclaration | ts.ExportSpecifier) { + const symbol = getSymbolOfNode(node); + const target = resolveAlias(symbol); + if (target) { + const markAlias = target === unknownSymbol || + ((target.flags & ts.SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target) && !getTypeOnlyAliasDeclaration(symbol)); - // If the declaration itself is type-only, mark it and return. - // No need to check what it resolves to. - const sourceSymbol = getSymbolOfNode(aliasDeclaration); - if (ts.isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) { - const links = getSymbolLinks(sourceSymbol); - links.typeOnlyDeclaration = aliasDeclaration; - return true; + if (markAlias) { + markAliasSymbolAsReferenced(symbol); } - - const links = getSymbolLinks(sourceSymbol); - return markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, immediateTarget, overwriteEmpty) - || markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, finalTarget, overwriteEmpty); } + } - function markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationLinks: ts.SymbolLinks, target: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { - if (target && (aliasDeclarationLinks.typeOnlyDeclaration === undefined || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration === false)) { - const exportSymbol = target.exports?.get(ts.InternalSymbolName.ExportEquals) ?? target; - const typeOnly = exportSymbol.declarations && ts.find(exportSymbol.declarations, ts.isTypeOnlyImportOrExportDeclaration); - aliasDeclarationLinks.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(exportSymbol).typeOnlyDeclaration ?? false; + // 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 ts.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 (ts.isInternalModuleImportEqualsDeclaration(node)) { + const target = resolveSymbol(symbol); + if (target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { + // import foo = + checkExpressionCached(node.moduleReference as ts.Expression); + } } - return !!aliasDeclarationLinks.typeOnlyDeclaration; } + } - /** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */ - function getTypeOnlyAliasDeclaration(symbol: ts.Symbol): ts.TypeOnlyAliasDeclaration | undefined { - if (!(symbol.flags & ts.SymbolFlags.Alias)) { - return undefined; - } - const links = getSymbolLinks(symbol); - return links.typeOnlyDeclaration || undefined; + // 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 markExportAsReferenced(node: ts.ImportEqualsDeclaration | ts.ExportSpecifier) { - const symbol = getSymbolOfNode(node); - const target = resolveAlias(symbol); - if (target) { - const markAlias = target === unknownSymbol || - ((target.flags & ts.SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target) && !getTypeOnlyAliasDeclaration(symbol)); - - if (markAlias) { - markAliasSymbolAsReferenced(symbol); - } - } + // This function is only for imports with entity names + function getSymbolOfPartOfRightHandSideOfImportEquals(entityName: ts.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 === ts.SyntaxKind.Identifier && ts.isRightSideOfQualifiedNameOrPropertyAccess(entityName)) { + entityName = entityName.parent as ts.QualifiedName; + } + // Check for case 1 and 3 in the above example + if (entityName.kind === ts.SyntaxKind.Identifier || entityName.parent.kind === ts.SyntaxKind.QualifiedName) { + return resolveEntityName(entityName, ts.SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); + } + else { + // Case 2 in above example + // entityName.kind could be a QualifiedName or a Missing identifier + ts.Debug.assert(entityName.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration); + return resolveEntityName(entityName, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); } + } - // 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 ts.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 (ts.isInternalModuleImportEqualsDeclaration(node)) { - const target = resolveSymbol(symbol); - if (target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { - // import foo = - checkExpressionCached(node.moduleReference as ts.Expression); - } - } - } - } + function getFullyQualifiedName(symbol: ts.Symbol, containingLocation?: ts.Node): string { + return symbol.parent ? getFullyQualifiedName(symbol.parent, containingLocation) + "." + symbolToString(symbol) : symbolToString(symbol, containingLocation, /*meaning*/ undefined, ts.SymbolFormatFlags.DoNotIncludeSymbolChain | ts.SymbolFormatFlags.AllowAnyNodeKind); + } - // 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 getContainingQualifiedNameNode(node: ts.QualifiedName) { + while (ts.isQualifiedName(node.parent)) { + node = node.parent; } + return node; + } - // This function is only for imports with entity names - function getSymbolOfPartOfRightHandSideOfImportEquals(entityName: ts.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 === ts.SyntaxKind.Identifier && ts.isRightSideOfQualifiedNameOrPropertyAccess(entityName)) { - entityName = entityName.parent as ts.QualifiedName; - } - // Check for case 1 and 3 in the above example - if (entityName.kind === ts.SyntaxKind.Identifier || entityName.parent.kind === ts.SyntaxKind.QualifiedName) { - return resolveEntityName(entityName, ts.SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); - } - else { - // Case 2 in above example - // entityName.kind could be a QualifiedName or a Missing identifier - ts.Debug.assert(entityName.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration); - return resolveEntityName(entityName, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); - } + function tryGetQualifiedNameAsValue(node: ts.QualifiedName) { + let left: ts.Identifier | ts.QualifiedName = ts.getFirstIdentifier(node); + let symbol = resolveName(left, left.escapedText, ts.SymbolFlags.Value, undefined, left, /*isUse*/ true); + if (!symbol) { + return undefined; } - - function getFullyQualifiedName(symbol: ts.Symbol, containingLocation?: ts.Node): string { - return symbol.parent ? getFullyQualifiedName(symbol.parent, containingLocation) + "." + symbolToString(symbol) : symbolToString(symbol, containingLocation, /*meaning*/ undefined, ts.SymbolFormatFlags.DoNotIncludeSymbolChain | ts.SymbolFormatFlags.AllowAnyNodeKind); + while (ts.isQualifiedName(left.parent)) { + const type = getTypeOfSymbol(symbol); + symbol = getPropertyOfType(type, left.parent.right.escapedText); + if (!symbol) { + return undefined; + } + left = left.parent; } + return symbol; + } - function getContainingQualifiedNameNode(node: ts.QualifiedName) { - while (ts.isQualifiedName(node.parent)) { - node = node.parent; - } - return node; + /** + * Resolves a qualified name and any involved aliases. + */ + function resolveEntityName(name: ts.EntityNameOrEntityNameExpression, meaning: ts.SymbolFlags, ignoreErrors?: boolean, dontResolveAlias?: boolean, location?: ts.Node): ts.Symbol | undefined { + if (ts.nodeIsMissing(name)) { + return undefined; } - function tryGetQualifiedNameAsValue(node: ts.QualifiedName) { - let left: ts.Identifier | ts.QualifiedName = ts.getFirstIdentifier(node); - let symbol = resolveName(left, left.escapedText, ts.SymbolFlags.Value, undefined, left, /*isUse*/ true); + const namespaceMeaning = ts.SymbolFlags.Namespace | (ts.isInJSFile(name) ? meaning & ts.SymbolFlags.Value : 0); + let symbol: ts.Symbol | undefined; + if (name.kind === ts.SyntaxKind.Identifier) { + const message = meaning === namespaceMeaning || ts.nodeIsSynthesized(name) ? ts.Diagnostics.Cannot_find_namespace_0 : getCannotFindNameDiagnosticForName(ts.getFirstIdentifier(name)); + const symbolFromJSPrototype = ts.isInJSFile(name) && !ts.nodeIsSynthesized(name) ? resolveEntityNameFromAssignmentDeclaration(name, meaning) : undefined; + symbol = getMergedSymbol(resolveName(location || name, name.escapedText, meaning, ignoreErrors || symbolFromJSPrototype ? undefined : message, name, /*isUse*/ true, false)); if (!symbol) { - return undefined; - } - while (ts.isQualifiedName(left.parent)) { - const type = getTypeOfSymbol(symbol); - symbol = getPropertyOfType(type, left.parent.right.escapedText); - if (!symbol) { - return undefined; - } - left = left.parent; + return getMergedSymbol(symbolFromJSPrototype); } - return symbol; } - - /** - * Resolves a qualified name and any involved aliases. - */ - function resolveEntityName(name: ts.EntityNameOrEntityNameExpression, meaning: ts.SymbolFlags, ignoreErrors?: boolean, dontResolveAlias?: boolean, location?: ts.Node): ts.Symbol | undefined { - if (ts.nodeIsMissing(name)) { + else if (name.kind === ts.SyntaxKind.QualifiedName || name.kind === ts.SyntaxKind.PropertyAccessExpression) { + const left = name.kind === ts.SyntaxKind.QualifiedName ? name.left : name.expression; + const right = name.kind === ts.SyntaxKind.QualifiedName ? name.right : name.name; + let namespace = resolveEntityName(left, namespaceMeaning, ignoreErrors, /*dontResolveAlias*/ false, location); + if (!namespace || ts.nodeIsMissing(right)) { return undefined; } - - const namespaceMeaning = ts.SymbolFlags.Namespace | (ts.isInJSFile(name) ? meaning & ts.SymbolFlags.Value : 0); - let symbol: ts.Symbol | undefined; - if (name.kind === ts.SyntaxKind.Identifier) { - const message = meaning === namespaceMeaning || ts.nodeIsSynthesized(name) ? ts.Diagnostics.Cannot_find_namespace_0 : getCannotFindNameDiagnosticForName(ts.getFirstIdentifier(name)); - const symbolFromJSPrototype = ts.isInJSFile(name) && !ts.nodeIsSynthesized(name) ? resolveEntityNameFromAssignmentDeclaration(name, meaning) : undefined; - symbol = getMergedSymbol(resolveName(location || name, name.escapedText, meaning, ignoreErrors || symbolFromJSPrototype ? undefined : message, name, /*isUse*/ true, false)); - if (!symbol) { - return getMergedSymbol(symbolFromJSPrototype); - } + else if (namespace === unknownSymbol) { + return namespace; } - else if (name.kind === ts.SyntaxKind.QualifiedName || name.kind === ts.SyntaxKind.PropertyAccessExpression) { - const left = name.kind === ts.SyntaxKind.QualifiedName ? name.left : name.expression; - const right = name.kind === ts.SyntaxKind.QualifiedName ? name.right : name.name; - let namespace = resolveEntityName(left, namespaceMeaning, ignoreErrors, /*dontResolveAlias*/ false, location); - if (!namespace || ts.nodeIsMissing(right)) { - return undefined; - } - else if (namespace === unknownSymbol) { - return namespace; - } - if (namespace.valueDeclaration && - ts.isInJSFile(namespace.valueDeclaration) && - ts.isVariableDeclaration(namespace.valueDeclaration) && - namespace.valueDeclaration.initializer && - isCommonJsRequire(namespace.valueDeclaration.initializer)) { - const moduleName = (namespace.valueDeclaration.initializer as ts.CallExpression).arguments[0] as ts.StringLiteral; - const moduleSym = resolveExternalModuleName(moduleName, moduleName); - if (moduleSym) { - const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); - if (resolvedModuleSymbol) { - namespace = resolvedModuleSymbol; - } + if (namespace.valueDeclaration && + ts.isInJSFile(namespace.valueDeclaration) && + ts.isVariableDeclaration(namespace.valueDeclaration) && + namespace.valueDeclaration.initializer && + isCommonJsRequire(namespace.valueDeclaration.initializer)) { + const moduleName = (namespace.valueDeclaration.initializer as ts.CallExpression).arguments[0] as ts.StringLiteral; + const moduleSym = resolveExternalModuleName(moduleName, moduleName); + if (moduleSym) { + const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); + if (resolvedModuleSymbol) { + namespace = resolvedModuleSymbol; } } - symbol = getMergedSymbol(getSymbol(getExportsOfSymbol(namespace), right.escapedText, meaning)); - if (!symbol) { - if (!ignoreErrors) { - const namespaceName = getFullyQualifiedName(namespace); - const declarationName = ts.declarationNameToString(right); - const suggestionForNonexistentModule = getSuggestedSymbolForNonexistentModule(right, namespace); - if (suggestionForNonexistentModule) { - error(right, ts.Diagnostics._0_has_no_exported_member_named_1_Did_you_mean_2, namespaceName, declarationName, symbolToString(suggestionForNonexistentModule)); - return undefined; - } + } + symbol = getMergedSymbol(getSymbol(getExportsOfSymbol(namespace), right.escapedText, meaning)); + if (!symbol) { + if (!ignoreErrors) { + const namespaceName = getFullyQualifiedName(namespace); + const declarationName = ts.declarationNameToString(right); + const suggestionForNonexistentModule = getSuggestedSymbolForNonexistentModule(right, namespace); + if (suggestionForNonexistentModule) { + error(right, ts.Diagnostics._0_has_no_exported_member_named_1_Did_you_mean_2, namespaceName, declarationName, symbolToString(suggestionForNonexistentModule)); + return undefined; + } - const containingQualifiedName = ts.isQualifiedName(name) && getContainingQualifiedNameNode(name); - const canSuggestTypeof = globalObjectType // <-- can't pull on types if global types aren't initialized yet - && (meaning & ts.SymbolFlags.Type) - && containingQualifiedName - && !ts.isTypeOfExpression(containingQualifiedName.parent) - && tryGetQualifiedNameAsValue(containingQualifiedName); - if (canSuggestTypeof) { - error(containingQualifiedName, ts.Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here_Did_you_mean_typeof_0, ts.entityNameToString(containingQualifiedName)); - return undefined; - } + const containingQualifiedName = ts.isQualifiedName(name) && getContainingQualifiedNameNode(name); + const canSuggestTypeof = globalObjectType // <-- can't pull on types if global types aren't initialized yet + && (meaning & ts.SymbolFlags.Type) + && containingQualifiedName + && !ts.isTypeOfExpression(containingQualifiedName.parent) + && tryGetQualifiedNameAsValue(containingQualifiedName); + if (canSuggestTypeof) { + error(containingQualifiedName, ts.Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here_Did_you_mean_typeof_0, ts.entityNameToString(containingQualifiedName)); + return undefined; + } - if (meaning & ts.SymbolFlags.Namespace && ts.isQualifiedName(name.parent)) { - const exportedTypeSymbol = getMergedSymbol(getSymbol(getExportsOfSymbol(namespace), right.escapedText, ts.SymbolFlags.Type)); - if (exportedTypeSymbol) { - error(name.parent.right, ts.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, symbolToString(exportedTypeSymbol), ts.unescapeLeadingUnderscores(name.parent.right.escapedText)); - return undefined; - } + if (meaning & ts.SymbolFlags.Namespace && ts.isQualifiedName(name.parent)) { + const exportedTypeSymbol = getMergedSymbol(getSymbol(getExportsOfSymbol(namespace), right.escapedText, ts.SymbolFlags.Type)); + if (exportedTypeSymbol) { + error(name.parent.right, ts.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, symbolToString(exportedTypeSymbol), ts.unescapeLeadingUnderscores(name.parent.right.escapedText)); + return undefined; } - - error(right, ts.Diagnostics.Namespace_0_has_no_exported_member_1, namespaceName, declarationName); } - return undefined; + + error(right, ts.Diagnostics.Namespace_0_has_no_exported_member_1, namespaceName, declarationName); } + return undefined; } - else { - throw ts.Debug.assertNever(name, "Unknown entity name kind."); - } - ts.Debug.assert((ts.getCheckFlags(symbol) & ts.CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); - if (!ts.nodeIsSynthesized(name) && ts.isEntityName(name) && (symbol.flags & ts.SymbolFlags.Alias || name.parent.kind === ts.SyntaxKind.ExportAssignment)) { - markSymbolOfAliasDeclarationIfTypeOnly(ts.getAliasDeclarationFromName(name), symbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ true); - } - return (symbol.flags & meaning) || dontResolveAlias ? symbol : resolveAlias(symbol); } + else { + throw ts.Debug.assertNever(name, "Unknown entity name kind."); + } + ts.Debug.assert((ts.getCheckFlags(symbol) & ts.CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); + if (!ts.nodeIsSynthesized(name) && ts.isEntityName(name) && (symbol.flags & ts.SymbolFlags.Alias || name.parent.kind === ts.SyntaxKind.ExportAssignment)) { + markSymbolOfAliasDeclarationIfTypeOnly(ts.getAliasDeclarationFromName(name), symbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ true); + } + 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: ts.Identifier, meaning: ts.SymbolFlags) { - if (isJSDocTypeReference(name.parent)) { - const secondaryLocation = getAssignmentDeclarationLocation(name.parent); - if (secondaryLocation) { - return resolveName(secondaryLocation, name.escapedText, meaning, /*nameNotFoundMessage*/ undefined, name, /*isUse*/ true); - } + /** + * 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: ts.Identifier, meaning: ts.SymbolFlags) { + if (isJSDocTypeReference(name.parent)) { + const secondaryLocation = getAssignmentDeclarationLocation(name.parent); + if (secondaryLocation) { + return resolveName(secondaryLocation, name.escapedText, meaning, /*nameNotFoundMessage*/ undefined, name, /*isUse*/ true); } } + } - function getAssignmentDeclarationLocation(node: ts.TypeReferenceNode): ts.Node | undefined { - const typeAlias = ts.findAncestor(node, node => !(ts.isJSDocNode(node) || node.flags & ts.NodeFlags.JSDoc) ? "quit" : ts.isJSDocTypeAlias(node)); - if (typeAlias) { - return; - } - const host = ts.getJSDocHost(node); - if (host && ts.isExpressionStatement(host) && ts.isPrototypePropertyAssignment(host.expression)) { - // /** @param {K} p */ X.prototype.m = function () { } <-- look for K on X's declaration - const symbol = getSymbolOfNode(host.expression.left); - if (symbol) { - return getDeclarationOfJSPrototypeContainer(symbol); - } - } - if (host && ts.isFunctionExpression(host) && ts.isPrototypePropertyAssignment(host.parent) && ts.isExpressionStatement(host.parent.parent)) { - // X.prototype.m = /** @param {K} p */ function () { } <-- look for K on X's declaration - const symbol = getSymbolOfNode(host.parent.left); - if (symbol) { - return getDeclarationOfJSPrototypeContainer(symbol); - } + function getAssignmentDeclarationLocation(node: ts.TypeReferenceNode): ts.Node | undefined { + const typeAlias = ts.findAncestor(node, node => !(ts.isJSDocNode(node) || node.flags & ts.NodeFlags.JSDoc) ? "quit" : ts.isJSDocTypeAlias(node)); + if (typeAlias) { + return; + } + const host = ts.getJSDocHost(node); + if (host && ts.isExpressionStatement(host) && ts.isPrototypePropertyAssignment(host.expression)) { + // /** @param {K} p */ X.prototype.m = function () { } <-- look for K on X's declaration + const symbol = getSymbolOfNode(host.expression.left); + if (symbol) { + return getDeclarationOfJSPrototypeContainer(symbol); } - if (host && (ts.isObjectLiteralMethod(host) || ts.isPropertyAssignment(host)) && - ts.isBinaryExpression(host.parent.parent) && - ts.getAssignmentDeclarationKind(host.parent.parent) === ts.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); - } + } + if (host && ts.isFunctionExpression(host) && ts.isPrototypePropertyAssignment(host.parent) && ts.isExpressionStatement(host.parent.parent)) { + // X.prototype.m = /** @param {K} p */ function () { } <-- look for K on X's declaration + const symbol = getSymbolOfNode(host.parent.left); + if (symbol) { + return getDeclarationOfJSPrototypeContainer(symbol); } - const sig = ts.getEffectiveJSDocHost(node); - if (sig && ts.isFunctionLike(sig)) { - const symbol = getSymbolOfNode(sig); - return symbol && symbol.valueDeclaration; + } + if (host && (ts.isObjectLiteralMethod(host) || ts.isPropertyAssignment(host)) && + ts.isBinaryExpression(host.parent.parent) && + ts.getAssignmentDeclarationKind(host.parent.parent) === ts.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); } } + const sig = ts.getEffectiveJSDocHost(node); + if (sig && ts.isFunctionLike(sig)) { + const symbol = getSymbolOfNode(sig); + return symbol && symbol.valueDeclaration; + } + } - function getDeclarationOfJSPrototypeContainer(symbol: ts.Symbol) { - const decl = symbol.parent!.valueDeclaration; - if (!decl) { - return undefined; - } - const initializer = ts.isAssignmentDeclaration(decl) ? ts.getAssignedExpandoInitializer(decl) : - ts.hasOnlyExpressionInitializer(decl) ? ts.getDeclaredExpandoInitializer(decl) : - undefined; - return initializer || decl; + function getDeclarationOfJSPrototypeContainer(symbol: ts.Symbol) { + const decl = symbol.parent!.valueDeclaration; + if (!decl) { + return undefined; } + const initializer = ts.isAssignmentDeclaration(decl) ? ts.getAssignedExpandoInitializer(decl) : + ts.hasOnlyExpressionInitializer(decl) ? ts.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 || !ts.isInJSFile(decl) || symbol.flags & ts.SymbolFlags.TypeAlias || ts.getExpandoInitializer(decl, /*isPrototypeAssignment*/ false)) { - return undefined; - } - const init = ts.isVariableDeclaration(decl) ? ts.getDeclaredExpandoInitializer(decl) : ts.getAssignedExpandoInitializer(decl); - if (init) { - const initSymbol = getSymbolOfNode(init); - if (initSymbol) { - return mergeJSSymbols(initSymbol, symbol); - } + /** + * 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 || !ts.isInJSFile(decl) || symbol.flags & ts.SymbolFlags.TypeAlias || ts.getExpandoInitializer(decl, /*isPrototypeAssignment*/ false)) { + return undefined; + } + const init = ts.isVariableDeclaration(decl) ? ts.getDeclaredExpandoInitializer(decl) : ts.getAssignedExpandoInitializer(decl); + if (init) { + const initSymbol = getSymbolOfNode(init); + if (initSymbol) { + return mergeJSSymbols(initSymbol, symbol); } } + } - function resolveExternalModuleName(location: ts.Node, moduleReferenceExpression: ts.Expression, ignoreErrors?: boolean): ts.Symbol | undefined { - const isClassic = ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Classic; - const errorMessage = isClassic? - ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option - : ts.Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations; - return resolveExternalModuleNameWorker(location, moduleReferenceExpression, ignoreErrors ? undefined : errorMessage); - } + function resolveExternalModuleName(location: ts.Node, moduleReferenceExpression: ts.Expression, ignoreErrors?: boolean): ts.Symbol | undefined { + const isClassic = ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Classic; + const errorMessage = isClassic? + ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option + : ts.Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations; + return resolveExternalModuleNameWorker(location, moduleReferenceExpression, ignoreErrors ? undefined : errorMessage); + } - function resolveExternalModuleNameWorker(location: ts.Node, moduleReferenceExpression: ts.Expression, moduleNotFoundError: ts.DiagnosticMessage | undefined, isForAugmentation = false): ts.Symbol | undefined { - return ts.isStringLiteralLike(moduleReferenceExpression) - ? resolveExternalModule(location, moduleReferenceExpression.text, moduleNotFoundError, moduleReferenceExpression, isForAugmentation) - : undefined; + function resolveExternalModuleNameWorker(location: ts.Node, moduleReferenceExpression: ts.Expression, moduleNotFoundError: ts.DiagnosticMessage | undefined, isForAugmentation = false): ts.Symbol | undefined { + return ts.isStringLiteralLike(moduleReferenceExpression) + ? resolveExternalModule(location, moduleReferenceExpression.text, moduleNotFoundError, moduleReferenceExpression, isForAugmentation) + : undefined; + } + + function resolveExternalModule(location: ts.Node, moduleReference: string, moduleNotFoundError: ts.DiagnosticMessage | undefined, errorNode: ts.Node, isForAugmentation = false): ts.Symbol | undefined { + if (ts.startsWith(moduleReference, "@types/")) { + const diag = ts.Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; + const withoutAtTypePrefix = ts.removePrefix(moduleReference, "@types/"); + error(errorNode, diag, withoutAtTypePrefix, moduleReference); + } + + const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true); + if (ambientModule) { + return ambientModule; + } + const currentSourceFile = ts.getSourceFileOfNode(location); + const contextSpecifier = ts.isStringLiteralLike(location) + ? location + : ts.findAncestor(location, ts.isImportCall)?.arguments[0] || + ts.findAncestor(location, ts.isImportDeclaration)?.moduleSpecifier || + ts.findAncestor(location, ts.isExternalModuleImportEqualsDeclaration)?.moduleReference.expression || + ts.findAncestor(location, ts.isExportDeclaration)?.moduleSpecifier || + (ts.isModuleDeclaration(location) ? location : location.parent && ts.isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name || + (ts.isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal; + const mode = contextSpecifier && ts.isStringLiteralLike(contextSpecifier) ? ts.getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat; + const resolvedModule = ts.getResolvedModule(currentSourceFile, moduleReference, mode); + const resolutionDiagnostic = resolvedModule && ts.getResolutionDiagnostic(compilerOptions, resolvedModule); + const sourceFile = resolvedModule + && (!resolutionDiagnostic || resolutionDiagnostic === ts.Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set) + && host.getSourceFile(resolvedModule.resolvedFileName); + if (sourceFile) { + // If there's a resolutionDiagnostic we need to report it even if a sourceFile is found. + if (resolutionDiagnostic) { + error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName); + } + if (sourceFile.symbol) { + if (resolvedModule.isExternalLibraryImport && !ts.resolutionExtensionIsTSOrJson(resolvedModule.extension)) { + errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference); + } + if (ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext) { + const isSyncImport = (currentSourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS && !ts.findAncestor(location, ts.isImportCall)) || !!ts.findAncestor(location, ts.isImportEqualsDeclaration); + const overrideClauseHost = ts.findAncestor(location, l => ts.isImportTypeNode(l) || ts.isExportDeclaration(l) || ts.isImportDeclaration(l)) as ts.ImportTypeNode | ts.ImportDeclaration | ts.ExportDeclaration | undefined; + const overrideClause = overrideClauseHost && ts.isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause; + // An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of + // normal mode restrictions + if (isSyncImport && sourceFile.impliedNodeFormat === ts.ModuleKind.ESNext && !ts.getResolutionModeOverrideForClause(overrideClause)) { + error(errorNode, ts.Diagnostics.Module_0_cannot_be_imported_using_this_construct_The_specifier_only_resolves_to_an_ES_module_which_cannot_be_imported_synchronously_Use_dynamic_import_instead, 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, ts.Diagnostics.File_0_is_not_a_module, sourceFile.fileName); + } + return undefined; } - function resolveExternalModule(location: ts.Node, moduleReference: string, moduleNotFoundError: ts.DiagnosticMessage | undefined, errorNode: ts.Node, isForAugmentation = false): ts.Symbol | undefined { - if (ts.startsWith(moduleReference, "@types/")) { - const diag = ts.Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; - const withoutAtTypePrefix = ts.removePrefix(moduleReference, "@types/"); - error(errorNode, diag, withoutAtTypePrefix, moduleReference); - } - - const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true); - if (ambientModule) { - return ambientModule; - } - const currentSourceFile = ts.getSourceFileOfNode(location); - const contextSpecifier = ts.isStringLiteralLike(location) - ? location - : ts.findAncestor(location, ts.isImportCall)?.arguments[0] || - ts.findAncestor(location, ts.isImportDeclaration)?.moduleSpecifier || - ts.findAncestor(location, ts.isExternalModuleImportEqualsDeclaration)?.moduleReference.expression || - ts.findAncestor(location, ts.isExportDeclaration)?.moduleSpecifier || - (ts.isModuleDeclaration(location) ? location : location.parent && ts.isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name || - (ts.isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal; - const mode = contextSpecifier && ts.isStringLiteralLike(contextSpecifier) ? ts.getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat; - const resolvedModule = ts.getResolvedModule(currentSourceFile, moduleReference, mode); - const resolutionDiagnostic = resolvedModule && ts.getResolutionDiagnostic(compilerOptions, resolvedModule); - const sourceFile = resolvedModule - && (!resolutionDiagnostic || resolutionDiagnostic === ts.Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set) - && host.getSourceFile(resolvedModule.resolvedFileName); - if (sourceFile) { - // If there's a resolutionDiagnostic we need to report it even if a sourceFile is found. - if (resolutionDiagnostic) { - error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName); - } - if (sourceFile.symbol) { - if (resolvedModule.isExternalLibraryImport && !ts.resolutionExtensionIsTSOrJson(resolvedModule.extension)) { - errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference); - } - if (ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext) { - const isSyncImport = (currentSourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS && !ts.findAncestor(location, ts.isImportCall)) || !!ts.findAncestor(location, ts.isImportEqualsDeclaration); - const overrideClauseHost = ts.findAncestor(location, l => ts.isImportTypeNode(l) || ts.isExportDeclaration(l) || ts.isImportDeclaration(l)) as ts.ImportTypeNode | ts.ImportDeclaration | ts.ExportDeclaration | undefined; - const overrideClause = overrideClauseHost && ts.isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause; - // An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of - // normal mode restrictions - if (isSyncImport && sourceFile.impliedNodeFormat === ts.ModuleKind.ESNext && !ts.getResolutionModeOverrideForClause(overrideClause)) { - error(errorNode, ts.Diagnostics.Module_0_cannot_be_imported_using_this_construct_The_specifier_only_resolves_to_an_ES_module_which_cannot_be_imported_synchronously_Use_dynamic_import_instead, 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, ts.Diagnostics.File_0_is_not_a_module, sourceFile.fileName); + if (patternAmbientModules) { + const pattern = ts.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 undefined; + return getMergedSymbol(pattern.symbol); } + } - if (patternAmbientModules) { - const pattern = ts.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); - } + // May be an untyped module. If so, ignore resolutionDiagnostic. + if (resolvedModule && !ts.resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) { + if (isForAugmentation) { + const diag = ts.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 { + errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule!, moduleReference); } + // Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first. + return undefined; + } - // May be an untyped module. If so, ignore resolutionDiagnostic. - if (resolvedModule && !ts.resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) { - if (isForAugmentation) { - const diag = ts.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 { - errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule!, moduleReference); + if (moduleNotFoundError) { + // See if this was possibly a projectReference redirect + if (resolvedModule) { + const redirect = host.getProjectReferenceRedirect(resolvedModule.resolvedFileName); + if (redirect) { + error(errorNode, ts.Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, resolvedModule.resolvedFileName); + return undefined; } - // 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, ts.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 = ts.tryExtractTSExtension(moduleReference); + const isExtensionlessRelativePathImport = ts.pathIsRelative(moduleReference) && !ts.hasExtension(moduleReference); + const moduleResolutionKind = ts.getEmitModuleResolutionKind(compilerOptions); + const resolutionIsNode16OrNext = moduleResolutionKind === ts.ModuleResolutionKind.Node16 || + moduleResolutionKind === ts.ModuleResolutionKind.NodeNext; + if (tsExtension) { + const diag = ts.Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead; + const importSourceWithoutExtension = ts.removeExtension(moduleReference, tsExtension); + let replacedImportSource = importSourceWithoutExtension; + /** + * Direct users to import source with .js extension if outputting an ES module. + * @see https://github.com/microsoft/TypeScript/issues/42151 + */ + if (moduleKind >= ts.ModuleKind.ES2015) { + replacedImportSource += tsExtension === ts.Extension.Mts ? ".mjs" : tsExtension === ts.Extension.Cts ? ".cjs" : ".js"; } + error(errorNode, diag, tsExtension, replacedImportSource); } - - if (resolutionDiagnostic) { - error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName); + else if (!compilerOptions.resolveJsonModule && + ts.fileExtensionIs(moduleReference, ts.Extension.Json) && + ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Classic && + ts.hasJsonModuleEmitEnabled(compilerOptions)) { + error(errorNode, ts.Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); } - else { - const tsExtension = ts.tryExtractTSExtension(moduleReference); - const isExtensionlessRelativePathImport = ts.pathIsRelative(moduleReference) && !ts.hasExtension(moduleReference); - const moduleResolutionKind = ts.getEmitModuleResolutionKind(compilerOptions); - const resolutionIsNode16OrNext = moduleResolutionKind === ts.ModuleResolutionKind.Node16 || - moduleResolutionKind === ts.ModuleResolutionKind.NodeNext; - if (tsExtension) { - const diag = ts.Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead; - const importSourceWithoutExtension = ts.removeExtension(moduleReference, tsExtension); - let replacedImportSource = importSourceWithoutExtension; - /** - * Direct users to import source with .js extension if outputting an ES module. - * @see https://github.com/microsoft/TypeScript/issues/42151 - */ - if (moduleKind >= ts.ModuleKind.ES2015) { - replacedImportSource += tsExtension === ts.Extension.Mts ? ".mjs" : tsExtension === ts.Extension.Cts ? ".cjs" : ".js"; - } - error(errorNode, diag, tsExtension, replacedImportSource); - } - else if (!compilerOptions.resolveJsonModule && - ts.fileExtensionIs(moduleReference, ts.Extension.Json) && - ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Classic && - ts.hasJsonModuleEmitEnabled(compilerOptions)) { - error(errorNode, ts.Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); - } - else if (mode === ts.ModuleKind.ESNext && resolutionIsNode16OrNext && isExtensionlessRelativePathImport) { - const absoluteRef = ts.getNormalizedAbsolutePath(moduleReference, ts.getDirectoryPath(currentSourceFile.path)); - const suggestedExt = suggestedExtensions.find(([actualExt, _importExt]) => host.fileExists(absoluteRef + actualExt))?.[1]; - if (suggestedExt) { - error(errorNode, ts.Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node16_or_nodenext_Did_you_mean_0, moduleReference + suggestedExt); - } - else { - error(errorNode, ts.Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node16_or_nodenext_Consider_adding_an_extension_to_the_import_path); - } + else if (mode === ts.ModuleKind.ESNext && resolutionIsNode16OrNext && isExtensionlessRelativePathImport) { + const absoluteRef = ts.getNormalizedAbsolutePath(moduleReference, ts.getDirectoryPath(currentSourceFile.path)); + const suggestedExt = suggestedExtensions.find(([actualExt, _importExt]) => host.fileExists(absoluteRef + actualExt))?.[1]; + if (suggestedExt) { + error(errorNode, ts.Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node16_or_nodenext_Did_you_mean_0, moduleReference + suggestedExt); } else { - error(errorNode, moduleNotFoundError, moduleReference); + error(errorNode, ts.Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node16_or_nodenext_Consider_adding_an_extension_to_the_import_path); } } + else { + error(errorNode, moduleNotFoundError, moduleReference); + } } - return undefined; } + return undefined; + } - function errorOnImplicitAnyModule(isError: boolean, errorNode: ts.Node, { packageId, resolvedFileName }: ts.ResolvedModuleFull, moduleReference: string): void { - const errorInfo = !ts.isExternalModuleNameRelative(moduleReference) && packageId - ? typesPackageExists(packageId.name) + function errorOnImplicitAnyModule(isError: boolean, errorNode: ts.Node, { packageId, resolvedFileName }: ts.ResolvedModuleFull, moduleReference: string): void { + const errorInfo = !ts.isExternalModuleNameRelative(moduleReference) && packageId + ? typesPackageExists(packageId.name) + ? ts.chainDiagnosticMessages( + /*details*/ undefined, ts.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, ts.mangleScopedPackageName(packageId.name)) + : packageBundlesTypes(packageId.name) ? ts.chainDiagnosticMessages( - /*details*/ undefined, ts.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, ts.mangleScopedPackageName(packageId.name)) - : packageBundlesTypes(packageId.name) - ? ts.chainDiagnosticMessages( - /*details*/ undefined, ts.Diagnostics.If_the_0_package_actually_exposes_this_module_try_adding_a_new_declaration_d_ts_file_containing_declare_module_1, packageId.name, moduleReference) - : ts.chainDiagnosticMessages( - /*details*/ undefined, ts.Diagnostics.Try_npm_i_save_dev_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0, moduleReference, ts.mangleScopedPackageName(packageId.name)) - : undefined; - errorOrSuggestion(isError, errorNode, ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type, moduleReference, resolvedFileName)); + /*details*/ undefined, ts.Diagnostics.If_the_0_package_actually_exposes_this_module_try_adding_a_new_declaration_d_ts_file_containing_declare_module_1, packageId.name, moduleReference) + : ts.chainDiagnosticMessages( + /*details*/ undefined, ts.Diagnostics.Try_npm_i_save_dev_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0, moduleReference, ts.mangleScopedPackageName(packageId.name)) + : undefined; + errorOrSuggestion(isError, errorNode, ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type, moduleReference, resolvedFileName)); + } + function typesPackageExists(packageName: string): boolean { + return getPackagesMap().has(ts.getTypesPackageName(packageName)); + } + function packageBundlesTypes(packageName: string): boolean { + return !!getPackagesMap().get(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 | undefined { + if (moduleSymbol?.exports) { + const exportEquals = resolveSymbol(moduleSymbol.exports.get(ts.InternalSymbolName.ExportEquals), dontResolveAlias); + const exported = getCommonJsExportEquals(getMergedSymbol(exportEquals), getMergedSymbol(moduleSymbol)); + return getMergedSymbol(exported) || moduleSymbol; } - function typesPackageExists(packageName: string): boolean { - return getPackagesMap().has(ts.getTypesPackageName(packageName)); + 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 & ts.SymbolFlags.Alias) { + return exported; } - function packageBundlesTypes(packageName: string): boolean { - return !!getPackagesMap().get(packageName); + const links = getSymbolLinks(exported); + if (links.cjsExportMerged) { + return links.cjsExportMerged; } - - 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 | undefined { - if (moduleSymbol?.exports) { - const exportEquals = resolveSymbol(moduleSymbol.exports.get(ts.InternalSymbolName.ExportEquals), dontResolveAlias); - const exported = getCommonJsExportEquals(getMergedSymbol(exportEquals), getMergedSymbol(moduleSymbol)); - return getMergedSymbol(exported) || moduleSymbol; - } - return undefined; + const merged = exported.flags & ts.SymbolFlags.Transient ? exported : cloneSymbol(exported); + merged.flags = merged.flags | ts.SymbolFlags.ValueModule; + if (merged.exports === undefined) { + merged.exports = ts.createSymbolTable(); } + moduleSymbol.exports!.forEach((s, name) => { + if (name === ts.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; + } - function getCommonJsExportEquals(exported: ts.Symbol | undefined, moduleSymbol: ts.Symbol): ts.Symbol | undefined { - if (!exported || exported === unknownSymbol || exported === moduleSymbol || moduleSymbol.exports!.size === 1 || exported.flags & ts.SymbolFlags.Alias) { - return exported; - } - const links = getSymbolLinks(exported); - if (links.cjsExportMerged) { - return links.cjsExportMerged; - } - const merged = exported.flags & ts.SymbolFlags.Transient ? exported : cloneSymbol(exported); - merged.flags = merged.flags | ts.SymbolFlags.ValueModule; - if (merged.exports === undefined) { - merged.exports = ts.createSymbolTable(); - } - moduleSymbol.exports!.forEach((s, name) => { - if (name === ts.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: ts.Node, dontResolveAlias: boolean, suppressInteropError: boolean): ts.Symbol | undefined { + const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias); - // 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: ts.Node, dontResolveAlias: boolean, suppressInteropError: boolean): ts.Symbol | undefined { - const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias); + if (!dontResolveAlias && symbol) { + if (!suppressInteropError && !(symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Variable)) && !ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SourceFile)) { + const compilerOptionName = moduleKind >= ts.ModuleKind.ES2015 + ? "allowSyntheticDefaultImports" + : "esModuleInterop"; - if (!dontResolveAlias && symbol) { - if (!suppressInteropError && !(symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Variable)) && !ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SourceFile)) { - const compilerOptionName = moduleKind >= ts.ModuleKind.ES2015 - ? "allowSyntheticDefaultImports" - : "esModuleInterop"; + error(referencingLocation, ts.Diagnostics.This_module_can_only_be_referenced_with_ECMAScript_imports_Slashexports_by_turning_on_the_0_flag_and_referencing_its_default_export, compilerOptionName); - error(referencingLocation, ts.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; + } - return symbol; + const referenceParent = referencingLocation.parent; + if ((ts.isImportDeclaration(referenceParent) && ts.getNamespaceDeclarationNode(referenceParent)) || + ts.isImportCall(referenceParent)) { + const reference = ts.isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier; + const type = getTypeOfSymbol(symbol); + const defaultOnlyType = getTypeWithSyntheticDefaultOnly(type, symbol, moduleSymbol!, reference); + if (defaultOnlyType) { + return cloneTypeAsModuleType(symbol, defaultOnlyType, referenceParent); } - const referenceParent = referencingLocation.parent; - if ((ts.isImportDeclaration(referenceParent) && ts.getNamespaceDeclarationNode(referenceParent)) || - ts.isImportCall(referenceParent)) { - const reference = ts.isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier; - const type = getTypeOfSymbol(symbol); - const defaultOnlyType = getTypeWithSyntheticDefaultOnly(type, symbol, moduleSymbol!, reference); - if (defaultOnlyType) { - return cloneTypeAsModuleType(symbol, defaultOnlyType, referenceParent); + const targetFile = moduleSymbol?.declarations?.find(ts.isSourceFile); + const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(getUsageModeForExpression(reference), targetFile.impliedNodeFormat); + if (ts.getESModuleInterop(compilerOptions) || isEsmCjsRef) { + let sigs = getSignaturesOfStructuredType(type, ts.SignatureKind.Call); + if (!sigs || !sigs.length) { + sigs = getSignaturesOfStructuredType(type, ts.SignatureKind.Construct); } - - const targetFile = moduleSymbol?.declarations?.find(ts.isSourceFile); - const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(getUsageModeForExpression(reference), targetFile.impliedNodeFormat); - if (ts.getESModuleInterop(compilerOptions) || isEsmCjsRef) { - let sigs = getSignaturesOfStructuredType(type, ts.SignatureKind.Call); - if (!sigs || !sigs.length) { - sigs = getSignaturesOfStructuredType(type, ts.SignatureKind.Construct); - } - if ((sigs && sigs.length) || - getPropertyOfType(type, ts.InternalSymbolName.Default, /*skipObjectFunctionPropertyAugment*/ true) || - isEsmCjsRef) { - const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!, reference); - return cloneTypeAsModuleType(symbol, moduleType, referenceParent); - } + if ((sigs && sigs.length) || + getPropertyOfType(type, ts.InternalSymbolName.Default, /*skipObjectFunctionPropertyAugment*/ true) || + isEsmCjsRef) { + const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!, reference); + return cloneTypeAsModuleType(symbol, moduleType, referenceParent); } } } - return symbol; } + return symbol; + } - /** - * Create a new symbol which has the module's type less the call and construct signatures - */ - function cloneTypeAsModuleType(symbol: ts.Symbol, moduleType: ts.Type, referenceParent: ts.ImportDeclaration | ts.ImportCall) { - 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 = new ts.Map(symbol.members); - if (symbol.exports) - result.exports = new ts.Map(symbol.exports); - const resolvedModuleType = resolveStructuredTypeMembers(moduleType as ts.StructuredType); // Should already be resolved from the signature checks above - result.type = createAnonymousType(result, resolvedModuleType.members, ts.emptyArray, ts.emptyArray, resolvedModuleType.indexInfos); - return result; - } + /** + * Create a new symbol which has the module's type less the call and construct signatures + */ + function cloneTypeAsModuleType(symbol: ts.Symbol, moduleType: ts.Type, referenceParent: ts.ImportDeclaration | ts.ImportCall) { + 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 = new ts.Map(symbol.members); + if (symbol.exports) + result.exports = new ts.Map(symbol.exports); + const resolvedModuleType = resolveStructuredTypeMembers(moduleType as ts.StructuredType); // Should already be resolved from the signature checks above + result.type = createAnonymousType(result, resolvedModuleType.members, ts.emptyArray, ts.emptyArray, resolvedModuleType.indexInfos); + return result; + } - function hasExportAssignmentSymbol(moduleSymbol: ts.Symbol): boolean { - return moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals) !== undefined; - } + function hasExportAssignmentSymbol(moduleSymbol: ts.Symbol): boolean { + return moduleSymbol.exports!.get(ts.InternalSymbolName.ExportEquals) !== undefined; + } - function getExportsOfModuleAsArray(moduleSymbol: ts.Symbol): ts.Symbol[] { - return symbolsToArray(getExportsOfModule(moduleSymbol)); - } + 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) { - const type = getTypeOfSymbol(exportEquals); - if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { - ts.addRange(exports, getPropertiesOfType(type)); - } + function getExportsAndPropertiesOfModule(moduleSymbol: ts.Symbol): ts.Symbol[] { + const exports = getExportsOfModuleAsArray(moduleSymbol); + const exportEquals = resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) { + const type = getTypeOfSymbol(exportEquals); + if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { + ts.addRange(exports, getPropertiesOfType(type)); } - return exports; } + return exports; + } - function forEachExportAndPropertyOfModule(moduleSymbol: ts.Symbol, cb: (symbol: ts.Symbol, key: ts.__String) => void): void { - const exports = getExportsOfModule(moduleSymbol); - exports.forEach((symbol, key) => { - if (!isReservedMemberName(key)) { - cb(symbol, key); - } - }); - const exportEquals = resolveExternalModuleSymbol(moduleSymbol); - if (exportEquals !== moduleSymbol) { - const type = getTypeOfSymbol(exportEquals); - if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { - forEachPropertyOfType(type, (symbol, escapedName) => { - cb(symbol, escapedName); - }); - } + function forEachExportAndPropertyOfModule(moduleSymbol: ts.Symbol, cb: (symbol: ts.Symbol, key: ts.__String) => void): void { + const exports = getExportsOfModule(moduleSymbol); + exports.forEach((symbol, key) => { + if (!isReservedMemberName(key)) { + cb(symbol, key); } - } - - function tryGetMemberInModuleExports(memberName: ts.__String, moduleSymbol: ts.Symbol): ts.Symbol | undefined { - const symbolTable = getExportsOfModule(moduleSymbol); - if (symbolTable) { - return symbolTable.get(memberName); + }); + const exportEquals = resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) { + const type = getTypeOfSymbol(exportEquals); + if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { + forEachPropertyOfType(type, (symbol, escapedName) => { + cb(symbol, escapedName); + }); } } + } - function tryGetMemberInModuleExportsAndProperties(memberName: ts.__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 shouldTreatPropertiesOfExternalModuleAsExports(type) ? getPropertyOfType(type, memberName) : undefined; + function tryGetMemberInModuleExports(memberName: ts.__String, moduleSymbol: ts.Symbol): ts.Symbol | undefined { + const symbolTable = getExportsOfModule(moduleSymbol); + if (symbolTable) { + return symbolTable.get(memberName); } + } - function shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType: ts.Type) { - return !(resolvedExternalModuleType.flags & ts.TypeFlags.Primitive || - ts.getObjectFlags(resolvedExternalModuleType) & ts.ObjectFlags.Class || - // `isArrayOrTupleLikeType` is too expensive to use in this auto-imports hot path - isArrayType(resolvedExternalModuleType) || - isTupleType(resolvedExternalModuleType)); + function tryGetMemberInModuleExportsAndProperties(memberName: ts.__String, moduleSymbol: ts.Symbol): ts.Symbol | undefined { + const symbol = tryGetMemberInModuleExports(memberName, moduleSymbol); + if (symbol) { + return symbol; } - function getExportsOfSymbol(symbol: ts.Symbol): ts.SymbolTable { - return symbol.flags & ts.SymbolFlags.LateBindingContainer ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedExports) : - symbol.flags & ts.SymbolFlags.Module ? getExportsOfModule(symbol) : - symbol.exports || emptySymbols; + const exportEquals = resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals === moduleSymbol) { + return undefined; } - function getExportsOfModule(moduleSymbol: ts.Symbol): ts.SymbolTable { - const links = getSymbolLinks(moduleSymbol); - return links.resolvedExports || (links.resolvedExports = getExportsOfModuleWorker(moduleSymbol)); - } + const type = getTypeOfSymbol(exportEquals); + return shouldTreatPropertiesOfExternalModuleAsExports(type) ? getPropertyOfType(type, memberName) : undefined; + } - interface ExportCollisionTracker { - specifierText: string; - exportsWithDuplicate: ts.ExportDeclaration[]; - } + function shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType: ts.Type) { + return !(resolvedExternalModuleType.flags & ts.TypeFlags.Primitive || + ts.getObjectFlags(resolvedExternalModuleType) & ts.ObjectFlags.Class || + // `isArrayOrTupleLikeType` is too expensive to use in this auto-imports hot path + isArrayType(resolvedExternalModuleType) || + isTupleType(resolvedExternalModuleType)); + } - type ExportCollisionTrackerTable = ts.UnderscoreEscapedMap; + function getExportsOfSymbol(symbol: ts.Symbol): ts.SymbolTable { + return symbol.flags & ts.SymbolFlags.LateBindingContainer ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedExports) : + symbol.flags & ts.SymbolFlags.Module ? getExportsOfModule(symbol) : + symbol.exports || emptySymbols; + } - /** - * 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: ts.SymbolTable, source: ts.SymbolTable | undefined, lookupTable?: ExportCollisionTrackerTable, exportNode?: ts.ExportDeclaration) { - if (!source) + function getExportsOfModule(moduleSymbol: ts.Symbol): ts.SymbolTable { + const links = getSymbolLinks(moduleSymbol); + return links.resolvedExports || (links.resolvedExports = getExportsOfModuleWorker(moduleSymbol)); + } + + interface ExportCollisionTracker { + specifierText: string; + exportsWithDuplicate: ts.ExportDeclaration[]; + } + + type ExportCollisionTrackerTable = ts.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: ts.SymbolTable, source: ts.SymbolTable | undefined, lookupTable?: ExportCollisionTrackerTable, exportNode?: ts.ExportDeclaration) { + if (!source) + return; + source.forEach((sourceSymbol, id) => { + if (id === ts.InternalSymbolName.Default) return; - source.forEach((sourceSymbol, id) => { - if (id === ts.InternalSymbolName.Default) - return; - const targetSymbol = target.get(id); - if (!targetSymbol) { - target.set(id, sourceSymbol); - if (lookupTable && exportNode) { - lookupTable.set(id, { - specifierText: ts.getTextOfNode(exportNode.moduleSpecifier!) - } as ExportCollisionTracker); - } + const targetSymbol = target.get(id); + if (!targetSymbol) { + target.set(id, sourceSymbol); + if (lookupTable && exportNode) { + lookupTable.set(id, { + specifierText: ts.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]; - } - else { - collisionTracker.exportsWithDuplicate.push(exportNode); - } + } + else if (lookupTable && exportNode && targetSymbol && resolveSymbol(targetSymbol) !== resolveSymbol(sourceSymbol)) { + const collisionTracker = lookupTable.get(id)!; + if (!collisionTracker.exportsWithDuplicate) { + collisionTracker.exportsWithDuplicate = [exportNode]; } - }); - } + else { + collisionTracker.exportsWithDuplicate.push(exportNode); + } + } + }); + } - function getExportsOfModuleWorker(moduleSymbol: ts.Symbol): ts.SymbolTable { - const visitedSymbols: ts.Symbol[] = []; + function getExportsOfModuleWorker(moduleSymbol: ts.Symbol): ts.SymbolTable { + const visitedSymbols: ts.Symbol[] = []; - // A module defined by an 'export=' consists of one export that needs to be resolved - moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); + // A module defined by an 'export=' consists of one export that needs to be resolved + moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); - return visit(moduleSymbol) || emptySymbols; + 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): ts.SymbolTable | undefined { - if (!(symbol && symbol.exports && ts.pushIfUnique(visitedSymbols, symbol))) { - return; - } - const symbols = new ts.Map(symbol.exports); - // All export * declarations are collected in an __export symbol by the binder - const exportStars = symbol.exports.get(ts.InternalSymbolName.ExportStar); - if (exportStars) { - const nestedSymbols = ts.createSymbolTable(); - const lookupTable: ExportCollisionTrackerTable = new ts.Map(); - if (exportStars.declarations) { - for (const node of exportStars.declarations) { - const resolvedModule = resolveExternalModuleName(node, (node as ts.ExportDeclaration).moduleSpecifier!); - const exportedSymbols = visit(resolvedModule); - extendExportSymbols(nestedSymbols, exportedSymbols, lookupTable, node as ts.ExportDeclaration); - } + // 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): ts.SymbolTable | undefined { + if (!(symbol && symbol.exports && ts.pushIfUnique(visitedSymbols, symbol))) { + return; + } + const symbols = new ts.Map(symbol.exports); + // All export * declarations are collected in an __export symbol by the binder + const exportStars = symbol.exports.get(ts.InternalSymbolName.ExportStar); + if (exportStars) { + const nestedSymbols = ts.createSymbolTable(); + const lookupTable: ExportCollisionTrackerTable = new ts.Map(); + if (exportStars.declarations) { + for (const node of exportStars.declarations) { + const resolvedModule = resolveExternalModuleName(node, (node as ts.ExportDeclaration).moduleSpecifier!); + const exportedSymbols = visit(resolvedModule); + extendExportSymbols(nestedSymbols, exportedSymbols, lookupTable, node as ts.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; } - 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(ts.createDiagnosticForNode(node, ts.Diagnostics.Module_0_has_already_exported_a_member_named_1_Consider_explicitly_re_exporting_to_resolve_the_ambiguity, lookupTable.get(id)!.specifierText, ts.unescapeLeadingUnderscores(id))); - } - }); - extendExportSymbols(symbols, nestedSymbols); - } - return symbols; + for (const node of exportsWithDuplicate) { + diagnostics.add(ts.createDiagnosticForNode(node, ts.Diagnostics.Module_0_has_already_exported_a_member_named_1_Consider_explicitly_re_exporting_to_resolve_the_ambiguity, lookupTable.get(id)!.specifierText, ts.unescapeLeadingUnderscores(id))); + } + }); + extendExportSymbols(symbols, nestedSymbols); } + return symbols; } + } - 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 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: ts.Declaration): ts.Symbol; - function getSymbolOfNode(node: ts.Node): ts.Symbol | undefined; - function getSymbolOfNode(node: ts.Node): ts.Symbol | undefined { - return getMergedSymbol(node.symbol && getLateBoundSymbol(node.symbol)); - } + function getSymbolOfNode(node: ts.Declaration): ts.Symbol; + function getSymbolOfNode(node: ts.Node): ts.Symbol | undefined; + function getSymbolOfNode(node: ts.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 getParentOfSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + return getMergedSymbol(symbol.parent && getLateBoundSymbol(symbol.parent)); + } - function getAlternativeContainingModules(symbol: ts.Symbol, enclosingDeclaration: ts.Node): ts.Symbol[] { - const containingFile = ts.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 (ts.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 = ts.append(results, resolvedModule); - } - if (ts.length(results)) { - (links.extendedContainersByFile || (links.extendedContainersByFile = new ts.Map())).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 (!ts.isExternalModule(file)) + function getAlternativeContainingModules(symbol: ts.Symbol, enclosingDeclaration: ts.Node): ts.Symbol[] { + const containingFile = ts.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 (ts.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 sym = getSymbolOfNode(file); - const ref = getAliasForSymbolInContainer(sym, symbol); + const ref = getAliasForSymbolInContainer(resolvedModule, symbol); if (!ref) continue; - results = ts.append(results, sym); + results = ts.append(results, resolvedModule); } - return links.extendedContainers = results || ts.emptyArray; + if (ts.length(results)) { + (links.extendedContainersByFile || (links.extendedContainersByFile = new ts.Map())).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 (!ts.isExternalModule(file)) + continue; + const sym = getSymbolOfNode(file); + const ref = getAliasForSymbolInContainer(sym, symbol); + if (!ref) + continue; + results = ts.append(results, sym); + } + return links.extendedContainers = results || ts.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: ts.Node | undefined, meaning: ts.SymbolFlags): ts.Symbol[] | undefined { - const container = getParentOfSymbol(symbol); - // Type parameters end up in the `members` lists but are not externally visible - if (container && !(symbol.flags & ts.SymbolFlags.TypeParameter)) { - const additionalContainers = ts.mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer); - const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration); - const objectLiteralContainer = getVariableDeclarationOfObjectLiteral(container, meaning); - if (enclosingDeclaration && - container.flags & getQualifiedLeftMeaning(meaning) && - getAccessibleSymbolChain(container, enclosingDeclaration, ts.SymbolFlags.Namespace, /*externalOnly*/ false)) { - return ts.append(ts.concatenate(ts.concatenate([container], additionalContainers), reexportContainers), objectLiteralContainer); // This order expresses a preference for the real container if it is in scope - } - // we potentially have a symbol which is a member of the instance side of something - look for a variable in scope with the container's type - // which may be acting like a namespace (eg, `Symbol` acts like a namespace when looking up `Symbol.toStringTag`) - const firstVariableMatch = !(container.flags & getQualifiedLeftMeaning(meaning)) - && container.flags & ts.SymbolFlags.Type - && getDeclaredTypeOfSymbol(container).flags & ts.TypeFlags.Object - && meaning === ts.SymbolFlags.Value - ? forEachSymbolTableInScope(enclosingDeclaration, t => { - return ts.forEachEntry(t, s => { - if (s.flags & getQualifiedLeftMeaning(meaning) && getTypeOfSymbol(s) === getDeclaredTypeOfSymbol(container)) { - return s; - } - }); - }) : undefined; - let res = firstVariableMatch ? [firstVariableMatch, ...additionalContainers, container] : [...additionalContainers, container]; - res = ts.append(res, objectLiteralContainer); - res = ts.addRange(res, reexportContainers); - return res; - } - const candidates = ts.mapDefined(symbol.declarations, d => { - if (!ts.isAmbientModule(d) && d.parent) { - // direct children of a module - if (hasNonGlobalAugmentationExternalModuleSymbol(d.parent)) { - return getSymbolOfNode(d.parent); - } - // export ='d member of an ambient module - if (ts.isModuleBlock(d.parent) && d.parent.parent && resolveExternalModuleSymbol(getSymbolOfNode(d.parent.parent)) === symbol) { - return getSymbolOfNode(d.parent.parent); + /** + * 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: ts.Node | undefined, meaning: ts.SymbolFlags): ts.Symbol[] | undefined { + const container = getParentOfSymbol(symbol); + // Type parameters end up in the `members` lists but are not externally visible + if (container && !(symbol.flags & ts.SymbolFlags.TypeParameter)) { + const additionalContainers = ts.mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer); + const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration); + const objectLiteralContainer = getVariableDeclarationOfObjectLiteral(container, meaning); + if (enclosingDeclaration && + container.flags & getQualifiedLeftMeaning(meaning) && + getAccessibleSymbolChain(container, enclosingDeclaration, ts.SymbolFlags.Namespace, /*externalOnly*/ false)) { + return ts.append(ts.concatenate(ts.concatenate([container], additionalContainers), reexportContainers), objectLiteralContainer); // This order expresses a preference for the real container if it is in scope + } + // we potentially have a symbol which is a member of the instance side of something - look for a variable in scope with the container's type + // which may be acting like a namespace (eg, `Symbol` acts like a namespace when looking up `Symbol.toStringTag`) + const firstVariableMatch = !(container.flags & getQualifiedLeftMeaning(meaning)) + && container.flags & ts.SymbolFlags.Type + && getDeclaredTypeOfSymbol(container).flags & ts.TypeFlags.Object + && meaning === ts.SymbolFlags.Value + ? forEachSymbolTableInScope(enclosingDeclaration, t => { + return ts.forEachEntry(t, s => { + if (s.flags & getQualifiedLeftMeaning(meaning) && getTypeOfSymbol(s) === getDeclaredTypeOfSymbol(container)) { + return s; } + }); + }) : undefined; + let res = firstVariableMatch ? [firstVariableMatch, ...additionalContainers, container] : [...additionalContainers, container]; + res = ts.append(res, objectLiteralContainer); + res = ts.addRange(res, reexportContainers); + return res; + } + const candidates = ts.mapDefined(symbol.declarations, d => { + if (!ts.isAmbientModule(d) && d.parent) { + // direct children of a module + if (hasNonGlobalAugmentationExternalModuleSymbol(d.parent)) { + return getSymbolOfNode(d.parent); } - if (ts.isClassExpression(d) && ts.isBinaryExpression(d.parent) && d.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isAccessExpression(d.parent.left) && ts.isEntityNameExpression(d.parent.left.expression)) { - if (ts.isModuleExportsAccessExpression(d.parent.left) || ts.isExportsIdentifier(d.parent.left.expression)) { - return getSymbolOfNode(ts.getSourceFileOfNode(d)); - } - checkExpressionCached(d.parent.left.expression); - return getNodeLinks(d.parent.left.expression).resolvedSymbol; + // export ='d member of an ambient module + if (ts.isModuleBlock(d.parent) && d.parent.parent && resolveExternalModuleSymbol(getSymbolOfNode(d.parent.parent)) === symbol) { + return getSymbolOfNode(d.parent.parent); } - }); - if (!ts.length(candidates)) { - return undefined; - } - return ts.mapDefined(candidates, candidate => getAliasForSymbolInContainer(candidate, symbol) ? candidate : undefined); - function fileSymbolIfFileSymbolExportEqualsContainer(d: ts.Declaration) { - return container && getFileSymbolIfFileSymbolExportEqualsContainer(d, container); } - } - - function getVariableDeclarationOfObjectLiteral(symbol: ts.Symbol, meaning: ts.SymbolFlags) { - // 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: ts.Node | false = !!ts.length(symbol.declarations) && ts.first(symbol.declarations!); - if (meaning & ts.SymbolFlags.Value && firstDecl && firstDecl.parent && ts.isVariableDeclaration(firstDecl.parent)) { - if (ts.isObjectLiteralExpression(firstDecl) && firstDecl === firstDecl.parent.initializer || ts.isTypeLiteralNode(firstDecl) && firstDecl === firstDecl.parent.type) { - return getSymbolOfNode(firstDecl.parent); + if (ts.isClassExpression(d) && ts.isBinaryExpression(d.parent) && d.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isAccessExpression(d.parent.left) && ts.isEntityNameExpression(d.parent.left.expression)) { + if (ts.isModuleExportsAccessExpression(d.parent.left) || ts.isExportsIdentifier(d.parent.left.expression)) { + return getSymbolOfNode(ts.getSourceFileOfNode(d)); } + checkExpressionCached(d.parent.left.expression); + return getNodeLinks(d.parent.left.expression).resolvedSymbol; } + }); + if (!ts.length(candidates)) { + return undefined; } - - function getFileSymbolIfFileSymbolExportEqualsContainer(d: ts.Declaration, container: ts.Symbol) { - const fileSymbol = getExternalModuleContainer(d); - const exported = fileSymbol && fileSymbol.exports && fileSymbol.exports.get(ts.InternalSymbolName.ExportEquals); - return exported && getSymbolIfSameReference(exported, container) ? fileSymbol : undefined; + return ts.mapDefined(candidates, candidate => getAliasForSymbolInContainer(candidate, symbol) ? candidate : undefined); + function fileSymbolIfFileSymbolExportEqualsContainer(d: ts.Declaration) { + return container && getFileSymbolIfFileSymbolExportEqualsContainer(d, container); } + } - 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; - } - // 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(ts.InternalSymbolName.ExportEquals); - if (exportEquals && getSymbolIfSameReference(exportEquals, symbol)) { - return container; + function getVariableDeclarationOfObjectLiteral(symbol: ts.Symbol, meaning: ts.SymbolFlags) { + // 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: ts.Node | false = !!ts.length(symbol.declarations) && ts.first(symbol.declarations!); + if (meaning & ts.SymbolFlags.Value && firstDecl && firstDecl.parent && ts.isVariableDeclaration(firstDecl.parent)) { + if (ts.isObjectLiteralExpression(firstDecl) && firstDecl === firstDecl.parent.initializer || ts.isTypeLiteralNode(firstDecl) && firstDecl === firstDecl.parent.type) { + return getSymbolOfNode(firstDecl.parent); } - const exports = getExportsOfSymbol(container); - const quick = exports.get(symbol.escapedName); - if (quick && getSymbolIfSameReference(quick, symbol)) { - return quick; - } - return ts.forEachEntry(exports, exported => { - if (getSymbolIfSameReference(exported, symbol)) { - return exported; - } - }); } + } - /** - * 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 getFileSymbolIfFileSymbolExportEqualsContainer(d: ts.Declaration, container: ts.Symbol) { + const fileSymbol = getExternalModuleContainer(d); + const exported = fileSymbol && fileSymbol.exports && fileSymbol.exports.get(ts.InternalSymbolName.ExportEquals); + return exported && getSymbolIfSameReference(exported, container) ? fileSymbol : undefined; + } - 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 & ts.SymbolFlags.ExportValue) !== 0 && symbol.exportSymbol || symbol); + 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; } + // 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(ts.InternalSymbolName.ExportEquals); + if (exportEquals && getSymbolIfSameReference(exportEquals, symbol)) { + return container; + } + const exports = getExportsOfSymbol(container); + const quick = exports.get(symbol.escapedName); + if (quick && getSymbolIfSameReference(quick, symbol)) { + return quick; + } + return ts.forEachEntry(exports, exported => { + if (getSymbolIfSameReference(exported, symbol)) { + return exported; + } + }); + } - function symbolIsValue(symbol: ts.Symbol, includeTypeOnlyMembers?: boolean): boolean { - return !!(symbol.flags & ts.SymbolFlags.Value || - symbol.flags & ts.SymbolFlags.Alias && resolveAlias(symbol).flags & ts.SymbolFlags.Value && (includeTypeOnlyMembers || !getTypeOnlyAliasDeclaration(symbol))); + /** + * 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 findConstructorDeclaration(node: ts.ClassLikeDeclaration): ts.ConstructorDeclaration | undefined { - const members = node.members; - for (const member of members) { - if (member.kind === ts.SyntaxKind.Constructor && ts.nodeIsPresent((member as ts.ConstructorDeclaration).body)) { - return member as ts.ConstructorDeclaration; - } + 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 & ts.SymbolFlags.ExportValue) !== 0 && symbol.exportSymbol || symbol); + } + + function symbolIsValue(symbol: ts.Symbol, includeTypeOnlyMembers?: boolean): boolean { + return !!(symbol.flags & ts.SymbolFlags.Value || + symbol.flags & ts.SymbolFlags.Alias && resolveAlias(symbol).flags & ts.SymbolFlags.Value && (includeTypeOnlyMembers || !getTypeOnlyAliasDeclaration(symbol))); + } + + function findConstructorDeclaration(node: ts.ClassLikeDeclaration): ts.ConstructorDeclaration | undefined { + const members = node.members; + for (const member of members) { + if (member.kind === ts.SyntaxKind.Constructor && ts.nodeIsPresent((member as ts.ConstructorDeclaration).body)) { + return member as ts.ConstructorDeclaration; } } + } - function createType(flags: ts.TypeFlags): ts.Type { - const result = new Type(checker, flags); - typeCount++; - result.id = typeCount; - ts.tracing?.recordType(result); - return result; - } + function createType(flags: ts.TypeFlags): ts.Type { + const result = new Type(checker, flags); + typeCount++; + result.id = typeCount; + ts.tracing?.recordType(result); + return result; + } - function createOriginType(flags: ts.TypeFlags): ts.Type { - return new Type(checker, flags); - } + function createOriginType(flags: ts.TypeFlags): ts.Type { + return new Type(checker, flags); + } - function createIntrinsicType(kind: ts.TypeFlags, intrinsicName: string, objectFlags: ts.ObjectFlags = 0): ts.IntrinsicType { - const type = createType(kind) as ts.IntrinsicType; - type.intrinsicName = intrinsicName; - type.objectFlags = objectFlags; - return type; - } + function createIntrinsicType(kind: ts.TypeFlags, intrinsicName: string, objectFlags: ts.ObjectFlags = 0): ts.IntrinsicType { + const type = createType(kind) as ts.IntrinsicType; + type.intrinsicName = intrinsicName; + type.objectFlags = objectFlags; + return type; + } - function createObjectType(objectFlags: ts.ObjectFlags, symbol?: ts.Symbol): ts.ObjectType { - const type = createType(ts.TypeFlags.Object) as ts.ObjectType; - type.objectFlags = objectFlags; - type.symbol = symbol!; - type.members = undefined; - type.properties = undefined; - type.callSignatures = undefined; - type.constructSignatures = undefined; - type.indexInfos = undefined; - return type; - } + function createObjectType(objectFlags: ts.ObjectFlags, symbol?: ts.Symbol): ts.ObjectType { + const type = createType(ts.TypeFlags.Object) as ts.ObjectType; + type.objectFlags = objectFlags; + type.symbol = symbol!; + type.members = undefined; + type.properties = undefined; + type.callSignatures = undefined; + type.constructSignatures = undefined; + type.indexInfos = undefined; + return type; + } - function createTypeofType() { - return getUnionType(ts.arrayFrom(typeofEQFacts.keys(), getStringLiteralType)); - } + function createTypeofType() { + return getUnionType(ts.arrayFrom(typeofEQFacts.keys(), getStringLiteralType)); + } - function createTypeParameter(symbol?: ts.Symbol) { - const type = createType(ts.TypeFlags.TypeParameter) as ts.TypeParameter; - if (symbol) - type.symbol = symbol; + function createTypeParameter(symbol?: ts.Symbol) { + const type = createType(ts.TypeFlags.TypeParameter) as ts.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: ts.__String) { + return (name as string).charCodeAt(0) === ts.CharacterCodes._ && + (name as string).charCodeAt(1) === ts.CharacterCodes._ && + (name as string).charCodeAt(2) !== ts.CharacterCodes._ && + (name as string).charCodeAt(2) !== ts.CharacterCodes.at && + (name as string).charCodeAt(2) !== ts.CharacterCodes.hash; + } + function getNamedMembers(members: ts.SymbolTable): ts.Symbol[] { + let result: ts.Symbol[] | undefined; + members.forEach((symbol, id) => { + if (isNamedMember(symbol, id)) { + (result || (result = [])).push(symbol); + } + }); + return result || ts.emptyArray; + } + + function isNamedMember(member: ts.Symbol, escapedName: ts.__String) { + return !isReservedMemberName(escapedName) && symbolIsValue(member); + } + + function getNamedOrIndexSignatureMembers(members: ts.SymbolTable): ts.Symbol[] { + const result = getNamedMembers(members); + const index = getIndexSymbolFromSymbolTable(members); + return index ? ts.concatenate(result, [index]) : result; + } + + function setStructuredTypeMembers(type: ts.StructuredType, members: ts.SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], indexInfos: readonly ts.IndexInfo[]): ts.ResolvedType { + const resolved = type as ts.ResolvedType; + resolved.members = members; + resolved.properties = ts.emptyArray; + resolved.callSignatures = callSignatures; + resolved.constructSignatures = constructSignatures; + resolved.indexInfos = indexInfos; + // This can loop back to getPropertyOfType() which would crash if `callSignatures` & `constructSignatures` are not initialized. + if (members !== emptySymbols) + resolved.properties = getNamedMembers(members); + return resolved; + } + + function createAnonymousType(symbol: ts.Symbol | undefined, members: ts.SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], indexInfos: readonly ts.IndexInfo[]): ts.ResolvedType { + return setStructuredTypeMembers(createObjectType(ts.ObjectFlags.Anonymous, symbol), members, callSignatures, constructSignatures, indexInfos); + } + + function getResolvedTypeWithoutAbstractConstructSignatures(type: ts.ResolvedType) { + if (type.constructSignatures.length === 0) return type; - } + if (type.objectTypeWithoutAbstractConstructSignatures) + return type.objectTypeWithoutAbstractConstructSignatures; + const constructSignatures = ts.filter(type.constructSignatures, signature => !(signature.flags & ts.SignatureFlags.Abstract)); + if (type.constructSignatures === constructSignatures) + return type; + const typeCopy = createAnonymousType(type.symbol, type.members, type.callSignatures, ts.some(constructSignatures) ? constructSignatures : ts.emptyArray, type.indexInfos); + type.objectTypeWithoutAbstractConstructSignatures = typeCopy; + typeCopy.objectTypeWithoutAbstractConstructSignatures = typeCopy; + return typeCopy; + } - // 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: ts.__String) { - return (name as string).charCodeAt(0) === ts.CharacterCodes._ && - (name as string).charCodeAt(1) === ts.CharacterCodes._ && - (name as string).charCodeAt(2) !== ts.CharacterCodes._ && - (name as string).charCodeAt(2) !== ts.CharacterCodes.at && - (name as string).charCodeAt(2) !== ts.CharacterCodes.hash; - } - function getNamedMembers(members: ts.SymbolTable): ts.Symbol[] { - let result: ts.Symbol[] | undefined; - members.forEach((symbol, id) => { - if (isNamedMember(symbol, id)) { - (result || (result = [])).push(symbol); + function forEachSymbolTableInScope(enclosingDeclaration: ts.Node | undefined, callback: (symbolTable: ts.SymbolTable, ignoreQualification?: boolean, isLocalNameLookup?: boolean, scopeNode?: ts.Node) => 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, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true, location)) { + return result; } - }); - return result || ts.emptyArray; + } + switch (location.kind) { + case ts.SyntaxKind.SourceFile: + if (!ts.isExternalOrCommonJsModule(location as ts.SourceFile)) { + break; + } + // falls through + case ts.SyntaxKind.ModuleDeclaration: + const sym = getSymbolOfNode(location as ts.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, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true, location)) { + return result; + } + break; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.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: ts.UnderscoreEscapedMap | undefined; + // TODO: Should this filtered table be cached in some way? + (getSymbolOfNode(location as ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members || emptySymbols).forEach((memberSymbol, key) => { + if (memberSymbol.flags & (ts.SymbolFlags.Type & ~ts.SymbolFlags.Assignment)) { + (table || (table = ts.createSymbolTable())).set(key, memberSymbol); + } + }); + if (table && (result = callback(table, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ false, location))) { + return result; + } + break; + } } - function isNamedMember(member: ts.Symbol, escapedName: ts.__String) { - return !isReservedMemberName(escapedName) && symbolIsValue(member); - } + return callback(globals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); + } - function getNamedOrIndexSignatureMembers(members: ts.SymbolTable): ts.Symbol[] { - const result = getNamedMembers(members); - const index = getIndexSymbolFromSymbolTable(members); - return index ? ts.concatenate(result, [index]) : result; - } + function getQualifiedLeftMeaning(rightMeaning: ts.SymbolFlags) { + // If we are looking in value space, the parent meaning is value, other wise it is namespace + return rightMeaning === ts.SymbolFlags.Value ? ts.SymbolFlags.Value : ts.SymbolFlags.Namespace; + } - function setStructuredTypeMembers(type: ts.StructuredType, members: ts.SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], indexInfos: readonly ts.IndexInfo[]): ts.ResolvedType { - const resolved = type as ts.ResolvedType; - resolved.members = members; - resolved.properties = ts.emptyArray; - resolved.callSignatures = callSignatures; - resolved.constructSignatures = constructSignatures; - resolved.indexInfos = indexInfos; - // This can loop back to getPropertyOfType() which would crash if `callSignatures` & `constructSignatures` are not initialized. - if (members !== emptySymbols) - resolved.properties = getNamedMembers(members); - return resolved; + function getAccessibleSymbolChain(symbol: ts.Symbol | undefined, enclosingDeclaration: ts.Node | undefined, meaning: ts.SymbolFlags, useOnlyExternalAliasing: boolean, visitedSymbolTablesMap: ts.ESMap = new ts.Map()): ts.Symbol[] | undefined { + if (!(symbol && !isPropertyOrMethodDeclarationSymbol(symbol))) { + return undefined; + } + const links = getSymbolLinks(symbol); + const cache = (links.accessibleChainCache ||= new ts.Map()); + // Go from enclosingDeclaration to the first scope we check, so the cache is keyed off the scope and thus shared more + const firstRelevantLocation = forEachSymbolTableInScope(enclosingDeclaration, (_, __, ___, node) => node); + const key = `${useOnlyExternalAliasing ? 0 : 1}|${firstRelevantLocation && getNodeId(firstRelevantLocation)}|${meaning}`; + if (cache.has(key)) { + return cache.get(key); } - function createAnonymousType(symbol: ts.Symbol | undefined, members: ts.SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], indexInfos: readonly ts.IndexInfo[]): ts.ResolvedType { - return setStructuredTypeMembers(createObjectType(ts.ObjectFlags.Anonymous, symbol), members, callSignatures, constructSignatures, indexInfos); + const id = getSymbolId(symbol); + let visitedSymbolTables = visitedSymbolTablesMap.get(id); + if (!visitedSymbolTables) { + visitedSymbolTablesMap.set(id, visitedSymbolTables = []); } + const result = forEachSymbolTableInScope(enclosingDeclaration, getAccessibleSymbolChainFromSymbolTable); + cache.set(key, result); + return result; - function getResolvedTypeWithoutAbstractConstructSignatures(type: ts.ResolvedType) { - if (type.constructSignatures.length === 0) - return type; - if (type.objectTypeWithoutAbstractConstructSignatures) - return type.objectTypeWithoutAbstractConstructSignatures; - const constructSignatures = ts.filter(type.constructSignatures, signature => !(signature.flags & ts.SignatureFlags.Abstract)); - if (type.constructSignatures === constructSignatures) - return type; - const typeCopy = createAnonymousType(type.symbol, type.members, type.callSignatures, ts.some(constructSignatures) ? constructSignatures : ts.emptyArray, type.indexInfos); - type.objectTypeWithoutAbstractConstructSignatures = typeCopy; - typeCopy.objectTypeWithoutAbstractConstructSignatures = typeCopy; - return typeCopy; + /** + * @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: ts.SymbolTable, ignoreQualification?: boolean, isLocalNameLookup?: boolean): ts.Symbol[] | undefined { + if (!ts.pushIfUnique(visitedSymbolTables!, symbols)) { + return undefined; + } + + const result = trySymbolTable(symbols, ignoreQualification, isLocalNameLookup); + visitedSymbolTables!.pop(); + return result; } - function forEachSymbolTableInScope(enclosingDeclaration: ts.Node | undefined, callback: (symbolTable: ts.SymbolTable, ignoreQualification?: boolean, isLocalNameLookup?: boolean, scopeNode?: ts.Node) => 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, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true, location)) { - return result; + function canQualifySymbol(symbolFromSymbolTable: ts.Symbol, meaning: ts.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 + !ts.some(symbolFromSymbolTable.declarations, hasNonGlobalAugmentationExternalModuleSymbol) && + (ignoreQualification || canQualifySymbol(getMergedSymbol(symbolFromSymbolTable), meaning)); + } + + function trySymbolTable(symbols: ts.SymbolTable, ignoreQualification: boolean | undefined, isLocalNameLookup: 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 = ts.forEachEntry(symbols, symbolFromSymbolTable => { + if (symbolFromSymbolTable.flags & ts.SymbolFlags.Alias + && symbolFromSymbolTable.escapedName !== ts.InternalSymbolName.ExportEquals + && symbolFromSymbolTable.escapedName !== ts.InternalSymbolName.Default + && !(ts.isUMDExportSymbol(symbolFromSymbolTable) && enclosingDeclaration && ts.isExternalModule(ts.getSourceFileOfNode(enclosingDeclaration))) + // If `!useOnlyExternalAliasing`, we can use any type of alias to get the name + && (!useOnlyExternalAliasing || ts.some(symbolFromSymbolTable.declarations, ts.isExternalModuleImportEqualsDeclaration)) + // If we're looking up a local name to reference directly, omit namespace reexports, otherwise when we're trawling through an export list to make a dotted name, we can keep it + && (isLocalNameLookup ? !ts.some(symbolFromSymbolTable.declarations, ts.isNamespaceReexportDeclaration) : true) + // While exports are generally considered to be in scope, export-specifier declared symbols are _not_ + // See similar comment in `resolveName` for details + && (ignoreQualification || !ts.getDeclarationOfKind(symbolFromSymbolTable, ts.SyntaxKind.ExportSpecifier))) { + + const resolvedImportedSymbol = resolveAlias(symbolFromSymbolTable); + const candidate = getCandidateListForSymbol(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification); + if (candidate) { + return candidate; } } - switch (location.kind) { - case ts.SyntaxKind.SourceFile: - if (!ts.isExternalOrCommonJsModule(location as ts.SourceFile)) { - break; - } - // falls through - case ts.SyntaxKind.ModuleDeclaration: - const sym = getSymbolOfNode(location as ts.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, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true, location)) { - return result; - } - break; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.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: ts.UnderscoreEscapedMap | undefined; - // TODO: Should this filtered table be cached in some way? - (getSymbolOfNode(location as ts.ClassLikeDeclaration | ts.InterfaceDeclaration).members || emptySymbols).forEach((memberSymbol, key) => { - if (memberSymbol.flags & (ts.SymbolFlags.Type & ~ts.SymbolFlags.Assignment)) { - (table || (table = ts.createSymbolTable())).set(key, memberSymbol); - } - }); - if (table && (result = callback(table, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ false, location))) { - return result; - } - break; + if (symbolFromSymbolTable.escapedName === symbol!.escapedName && symbolFromSymbolTable.exportSymbol) { + if (isAccessible(getMergedSymbol(symbolFromSymbolTable.exportSymbol), /*aliasSymbol*/ undefined, ignoreQualification)) { + return [symbol!]; + } } - } + }); - return callback(globals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); + // 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 getQualifiedLeftMeaning(rightMeaning: ts.SymbolFlags) { - // If we are looking in value space, the parent meaning is value, other wise it is namespace - return rightMeaning === ts.SymbolFlags.Value ? ts.SymbolFlags.Value : ts.SymbolFlags.Namespace; + function getCandidateListForSymbol(symbolFromSymbolTable: ts.Symbol, resolvedImportedSymbol: ts.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); + } } + } - function getAccessibleSymbolChain(symbol: ts.Symbol | undefined, enclosingDeclaration: ts.Node | undefined, meaning: ts.SymbolFlags, useOnlyExternalAliasing: boolean, visitedSymbolTablesMap: ts.ESMap = new ts.Map()): ts.Symbol[] | undefined { - if (!(symbol && !isPropertyOrMethodDeclarationSymbol(symbol))) { - return undefined; + function needsQualification(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, meaning: ts.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; } - const links = getSymbolLinks(symbol); - const cache = (links.accessibleChainCache ||= new ts.Map()); - // Go from enclosingDeclaration to the first scope we check, so the cache is keyed off the scope and thus shared more - const firstRelevantLocation = forEachSymbolTableInScope(enclosingDeclaration, (_, __, ___, node) => node); - const key = `${useOnlyExternalAliasing ? 0 : 1}|${firstRelevantLocation && getNodeId(firstRelevantLocation)}|${meaning}`; - if (cache.has(key)) { - return cache.get(key); + // If the symbol with this name is present it should refer to the symbol + if (symbolFromSymbolTable === symbol) { + // No need to qualify + return true; } - const id = getSymbolId(symbol); - let visitedSymbolTables = visitedSymbolTablesMap.get(id); - if (!visitedSymbolTables) { - visitedSymbolTablesMap.set(id, visitedSymbolTables = []); + // Qualify if the symbol from symbol table has same meaning as expected + symbolFromSymbolTable = (symbolFromSymbolTable.flags & ts.SymbolFlags.Alias && !ts.getDeclarationOfKind(symbolFromSymbolTable, ts.SyntaxKind.ExportSpecifier)) ? resolveAlias(symbolFromSymbolTable) : symbolFromSymbolTable; + if (symbolFromSymbolTable.flags & meaning) { + qualify = true; + return true; } - const result = forEachSymbolTableInScope(enclosingDeclaration, getAccessibleSymbolChainFromSymbolTable); - cache.set(key, result); - return result; - /** - * @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: ts.SymbolTable, ignoreQualification?: boolean, isLocalNameLookup?: boolean): ts.Symbol[] | undefined { - if (!ts.pushIfUnique(visitedSymbolTables!, symbols)) { - return undefined; - } + // Continue to the next symbol table + return false; + }); - const result = trySymbolTable(symbols, ignoreQualification, isLocalNameLookup); - visitedSymbolTables!.pop(); - return result; - } + return qualify; + } - function canQualifySymbol(symbolFromSymbolTable: ts.Symbol, meaning: ts.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 isPropertyOrMethodDeclarationSymbol(symbol: ts.Symbol) { + if (symbol.declarations && symbol.declarations.length) { + for (const declaration of symbol.declarations) { + switch (declaration.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + continue; + default: + return false; + } } + return true; + } + return false; + } - 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 - !ts.some(symbolFromSymbolTable.declarations, hasNonGlobalAugmentationExternalModuleSymbol) && - (ignoreQualification || canQualifySymbol(getMergedSymbol(symbolFromSymbolTable), meaning)); - } + function isTypeSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined): boolean { + const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, ts.SymbolFlags.Type, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ true); + return access.accessibility === ts.SymbolAccessibility.Accessible; + } - function trySymbolTable(symbols: ts.SymbolTable, ignoreQualification: boolean | undefined, isLocalNameLookup: 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!]; - } + function isValueSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined): boolean { + const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, ts.SymbolFlags.Value, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ true); + return access.accessibility === ts.SymbolAccessibility.Accessible; + } - // Check if symbol is any of the aliases in scope - const result = ts.forEachEntry(symbols, symbolFromSymbolTable => { - if (symbolFromSymbolTable.flags & ts.SymbolFlags.Alias - && symbolFromSymbolTable.escapedName !== ts.InternalSymbolName.ExportEquals - && symbolFromSymbolTable.escapedName !== ts.InternalSymbolName.Default - && !(ts.isUMDExportSymbol(symbolFromSymbolTable) && enclosingDeclaration && ts.isExternalModule(ts.getSourceFileOfNode(enclosingDeclaration))) - // If `!useOnlyExternalAliasing`, we can use any type of alias to get the name - && (!useOnlyExternalAliasing || ts.some(symbolFromSymbolTable.declarations, ts.isExternalModuleImportEqualsDeclaration)) - // If we're looking up a local name to reference directly, omit namespace reexports, otherwise when we're trawling through an export list to make a dotted name, we can keep it - && (isLocalNameLookup ? !ts.some(symbolFromSymbolTable.declarations, ts.isNamespaceReexportDeclaration) : true) - // While exports are generally considered to be in scope, export-specifier declared symbols are _not_ - // See similar comment in `resolveName` for details - && (ignoreQualification || !ts.getDeclarationOfKind(symbolFromSymbolTable, ts.SyntaxKind.ExportSpecifier))) { + function isSymbolAccessibleByFlags(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, flags: ts.SymbolFlags): boolean { + const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, flags, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ false); + return access.accessibility === ts.SymbolAccessibility.Accessible; + } - 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!]; - } + function isAnySymbolAccessible(symbols: ts.Symbol[] | undefined, enclosingDeclaration: ts.Node | undefined, initialSymbol: ts.Symbol, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean, allowModules: boolean): ts.SymbolAccessibilityResult | undefined { + if (!ts.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; + } + } + if (allowModules) { + if (ts.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; } - }); - - // 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); + // Any meaning of a module symbol is always accessible via an `import` type + return { + accessibility: ts.SymbolAccessibility.Accessible + }; + } } - function getCandidateListForSymbol(symbolFromSymbolTable: ts.Symbol, resolvedImportedSymbol: ts.Symbol, ignoreQualification: boolean | undefined) { - if (isAccessible(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification)) { - return [symbolFromSymbolTable]; - } + // 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 - // 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); - } + const containers = getContainersOfSymbol(symbol, enclosingDeclaration, meaning); + const parentResult = isAnySymbolAccessible(containers, enclosingDeclaration, initialSymbol, initialSymbol === symbol ? getQualifiedLeftMeaning(meaning) : meaning, shouldComputeAliasesToMakeVisible, allowModules); + if (parentResult) { + return parentResult; } } - function needsQualification(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, meaning: ts.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 & ts.SymbolFlags.Alias && !ts.getDeclarationOfKind(symbolFromSymbolTable, ts.SyntaxKind.ExportSpecifier)) ? resolveAlias(symbolFromSymbolTable) : symbolFromSymbolTable; - if (symbolFromSymbolTable.flags & meaning) { - qualify = true; - return true; - } - - // Continue to the next symbol table - return false; - }); + if (earlyModuleBail) { + return { + accessibility: ts.SymbolAccessibility.Accessible + }; + } - return qualify; + if (hadAccessibleChain) { + return { + accessibility: ts.SymbolAccessibility.NotAccessible, + errorSymbolName: symbolToString(initialSymbol, enclosingDeclaration, meaning), + errorModuleName: hadAccessibleChain !== initialSymbol ? symbolToString(hadAccessibleChain, enclosingDeclaration, ts.SymbolFlags.Namespace) : undefined, + }; } + } - function isPropertyOrMethodDeclarationSymbol(symbol: ts.Symbol) { - if (symbol.declarations && symbol.declarations.length) { - for (const declaration of symbol.declarations) { - switch (declaration.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - continue; - default: - return false; - } + /** + * 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: ts.Node | undefined, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): ts.SymbolAccessibilityResult { + return isSymbolAccessibleWorker(symbol, enclosingDeclaration, meaning, shouldComputeAliasesToMakeVisible, /*allowModules*/ true); + } + + function isSymbolAccessibleWorker(symbol: ts.Symbol | undefined, enclosingDeclaration: ts.Node | undefined, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean, allowModules: boolean): ts.SymbolAccessibilityResult { + if (symbol && enclosingDeclaration) { + const result = isAnySymbolAccessible([symbol], enclosingDeclaration, symbol, meaning, shouldComputeAliasesToMakeVisible, allowModules); + 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 = ts.forEach(symbol.declarations, getExternalModuleContainer); + if (symbolExternalModule) { + const enclosingExternalModule = getExternalModuleContainer(enclosingDeclaration); + if (symbolExternalModule !== enclosingExternalModule) { + // name from different external module that is not visible + return { + accessibility: ts.SymbolAccessibility.CannotBeNamed, + errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), + errorModuleName: symbolToString(symbolExternalModule), + errorNode: ts.isInJSFile(enclosingDeclaration) ? enclosingDeclaration : undefined, + }; } - return true; } - return false; - } - function isTypeSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined): boolean { - const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, ts.SymbolFlags.Type, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ true); - return access.accessibility === ts.SymbolAccessibility.Accessible; + // Just a local name that is not accessible + return { + accessibility: ts.SymbolAccessibility.NotAccessible, + errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), + }; } - function isValueSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined): boolean { - const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, ts.SymbolFlags.Value, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ true); - return access.accessibility === ts.SymbolAccessibility.Accessible; - } + return { accessibility: ts.SymbolAccessibility.Accessible }; + } - function isSymbolAccessibleByFlags(typeSymbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, flags: ts.SymbolFlags): boolean { - const access = isSymbolAccessibleWorker(typeSymbol, enclosingDeclaration, flags, /*shouldComputeAliasesToMakeVisible*/ false, /*allowModules*/ false); - return access.accessibility === ts.SymbolAccessibility.Accessible; - } + function getExternalModuleContainer(declaration: ts.Node) { + const node = ts.findAncestor(declaration, hasExternalModuleSymbol); + return node && getSymbolOfNode(node); + } - function isAnySymbolAccessible(symbols: ts.Symbol[] | undefined, enclosingDeclaration: ts.Node | undefined, initialSymbol: ts.Symbol, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean, allowModules: boolean): ts.SymbolAccessibilityResult | undefined { - if (!ts.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; - } - } - if (allowModules) { - if (ts.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: ts.SymbolAccessibility.Accessible - }; - } - } + function hasExternalModuleSymbol(declaration: ts.Node) { + return ts.isAmbientModule(declaration) || (declaration.kind === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(declaration as ts.SourceFile)); + } - // 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 + function hasNonGlobalAugmentationExternalModuleSymbol(declaration: ts.Node) { + return ts.isModuleWithStringLiteralName(declaration) || (declaration.kind === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(declaration as ts.SourceFile)); + } - const containers = getContainersOfSymbol(symbol, enclosingDeclaration, meaning); - const parentResult = isAnySymbolAccessible(containers, enclosingDeclaration, initialSymbol, initialSymbol === symbol ? getQualifiedLeftMeaning(meaning) : meaning, shouldComputeAliasesToMakeVisible, allowModules); - if (parentResult) { - return parentResult; - } + function hasVisibleDeclarations(symbol: ts.Symbol, shouldComputeAliasToMakeVisible: boolean): ts.SymbolVisibilityResult | undefined { + let aliasesToMakeVisible: ts.LateVisibilityPaintedStatement[] | undefined; + if (!ts.every(ts.filter(symbol.declarations, d => d.kind !== ts.SyntaxKind.Identifier), getIsDeclarationVisible)) { + return undefined; + } + return { accessibility: ts.SymbolAccessibility.Accessible, aliasesToMakeVisible }; + function getIsDeclarationVisible(declaration: ts.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 && + !ts.hasSyntacticModifier(anyImportSyntax, ts.ModifierFlags.Export) && // import clause without export + isDeclarationVisible(anyImportSyntax.parent)) { + return addVisibleAlias(declaration, anyImportSyntax); + } + else if (ts.isVariableDeclaration(declaration) && ts.isVariableStatement(declaration.parent.parent) && + !ts.hasSyntacticModifier(declaration.parent.parent, ts.ModifierFlags.Export) && // unexported variable statement + isDeclarationVisible(declaration.parent.parent.parent)) { + return addVisibleAlias(declaration, declaration.parent.parent); + } + else if (ts.isLateVisibilityPaintedStatement(declaration) // unexported top-level statement + && !ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Export) + && isDeclarationVisible(declaration.parent)) { + return addVisibleAlias(declaration, declaration); + } + else if (symbol.flags & ts.SymbolFlags.Alias && ts.isBindingElement(declaration) && ts.isInJSFile(declaration) && declaration.parent?.parent // exported import-like top-level JS require statement + && ts.isVariableDeclaration(declaration.parent.parent) + && declaration.parent.parent.parent?.parent && ts.isVariableStatement(declaration.parent.parent.parent.parent) + && !ts.hasSyntacticModifier(declaration.parent.parent.parent.parent, ts.ModifierFlags.Export) + && declaration.parent.parent.parent.parent.parent // check if the thing containing the variable statement is visible (ie, the file) + && isDeclarationVisible(declaration.parent.parent.parent.parent.parent)) { + return addVisibleAlias(declaration, declaration.parent.parent.parent.parent); + } + + // Declaration is not visible + return false; } - if (earlyModuleBail) { - return { - accessibility: ts.SymbolAccessibility.Accessible - }; - } + return true; + } - if (hadAccessibleChain) { - return { - accessibility: ts.SymbolAccessibility.NotAccessible, - errorSymbolName: symbolToString(initialSymbol, enclosingDeclaration, meaning), - errorModuleName: hadAccessibleChain !== initialSymbol ? symbolToString(hadAccessibleChain, enclosingDeclaration, ts.SymbolFlags.Namespace) : undefined, - }; + function addVisibleAlias(declaration: ts.Declaration, aliasingStatement: ts.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 = ts.appendIfUnique(aliasesToMakeVisible, aliasingStatement); } + return true; } + } - /** - * 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: ts.Node | undefined, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): ts.SymbolAccessibilityResult { - return isSymbolAccessibleWorker(symbol, enclosingDeclaration, meaning, shouldComputeAliasesToMakeVisible, /*allowModules*/ true); + function isEntityNameVisible(entityName: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node): ts.SymbolVisibilityResult { + // get symbol of the first identifier of the entityName + let meaning: ts.SymbolFlags; + if (entityName.parent.kind === ts.SyntaxKind.TypeQuery || + entityName.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && !ts.isPartOfTypeNode(entityName.parent) || + entityName.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + // Typeof value + meaning = ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue; + } + else if (entityName.kind === ts.SyntaxKind.QualifiedName || entityName.kind === ts.SyntaxKind.PropertyAccessExpression || + entityName.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration) { + // Left identifier from type reference or TypeAlias + // Entity name of the import declaration + meaning = ts.SymbolFlags.Namespace; + } + else { + // Type Reference or TypeAlias entity = Identifier + meaning = ts.SymbolFlags.Type; + } + + const firstIdentifier = ts.getFirstIdentifier(entityName); + const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + if (symbol && symbol.flags & ts.SymbolFlags.TypeParameter && meaning & ts.SymbolFlags.Type) { + return { accessibility: ts.SymbolAccessibility.Accessible }; } - function isSymbolAccessibleWorker(symbol: ts.Symbol | undefined, enclosingDeclaration: ts.Node | undefined, meaning: ts.SymbolFlags, shouldComputeAliasesToMakeVisible: boolean, allowModules: boolean): ts.SymbolAccessibilityResult { - if (symbol && enclosingDeclaration) { - const result = isAnySymbolAccessible([symbol], enclosingDeclaration, symbol, meaning, shouldComputeAliasesToMakeVisible, allowModules); - 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 = ts.forEach(symbol.declarations, getExternalModuleContainer); - if (symbolExternalModule) { - const enclosingExternalModule = getExternalModuleContainer(enclosingDeclaration); - if (symbolExternalModule !== enclosingExternalModule) { - // name from different external module that is not visible - return { - accessibility: ts.SymbolAccessibility.CannotBeNamed, - errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), - errorModuleName: symbolToString(symbolExternalModule), - errorNode: ts.isInJSFile(enclosingDeclaration) ? enclosingDeclaration : undefined, - }; - } - } - - // Just a local name that is not accessible - return { - accessibility: ts.SymbolAccessibility.NotAccessible, - errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), - }; - } + // Verify if the symbol is accessible + return (symbol && hasVisibleDeclarations(symbol, /*shouldComputeAliasToMakeVisible*/ true)) || { + accessibility: ts.SymbolAccessibility.NotAccessible, + errorSymbolName: ts.getTextOfNode(firstIdentifier), + errorNode: firstIdentifier + }; + } - return { accessibility: ts.SymbolAccessibility.Accessible }; + function symbolToString(symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags, flags: ts.SymbolFormatFlags = ts.SymbolFormatFlags.AllowAnyNodeKind, writer?: ts.EmitTextWriter): string { + let nodeFlags = ts.NodeBuilderFlags.IgnoreErrors; + if (flags & ts.SymbolFormatFlags.UseOnlyExternalAliasing) { + nodeFlags |= ts.NodeBuilderFlags.UseOnlyExternalAliasing; } - - function getExternalModuleContainer(declaration: ts.Node) { - const node = ts.findAncestor(declaration, hasExternalModuleSymbol); - return node && getSymbolOfNode(node); + if (flags & ts.SymbolFormatFlags.WriteTypeParametersOrArguments) { + nodeFlags |= ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName; } - - function hasExternalModuleSymbol(declaration: ts.Node) { - return ts.isAmbientModule(declaration) || (declaration.kind === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(declaration as ts.SourceFile)); + if (flags & ts.SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope) { + nodeFlags |= ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; } - - function hasNonGlobalAugmentationExternalModuleSymbol(declaration: ts.Node) { - return ts.isModuleWithStringLiteralName(declaration) || (declaration.kind === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(declaration as ts.SourceFile)); + if (flags & ts.SymbolFormatFlags.DoNotIncludeSymbolChain) { + nodeFlags |= ts.NodeBuilderFlags.DoNotIncludeSymbolChain; } - - function hasVisibleDeclarations(symbol: ts.Symbol, shouldComputeAliasToMakeVisible: boolean): ts.SymbolVisibilityResult | undefined { - let aliasesToMakeVisible: ts.LateVisibilityPaintedStatement[] | undefined; - if (!ts.every(ts.filter(symbol.declarations, d => d.kind !== ts.SyntaxKind.Identifier), getIsDeclarationVisible)) { - return undefined; - } - return { accessibility: ts.SymbolAccessibility.Accessible, aliasesToMakeVisible }; - function getIsDeclarationVisible(declaration: ts.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 && - !ts.hasSyntacticModifier(anyImportSyntax, ts.ModifierFlags.Export) && // import clause without export - isDeclarationVisible(anyImportSyntax.parent)) { - return addVisibleAlias(declaration, anyImportSyntax); - } - else if (ts.isVariableDeclaration(declaration) && ts.isVariableStatement(declaration.parent.parent) && - !ts.hasSyntacticModifier(declaration.parent.parent, ts.ModifierFlags.Export) && // unexported variable statement - isDeclarationVisible(declaration.parent.parent.parent)) { - return addVisibleAlias(declaration, declaration.parent.parent); - } - else if (ts.isLateVisibilityPaintedStatement(declaration) // unexported top-level statement - && !ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Export) - && isDeclarationVisible(declaration.parent)) { - return addVisibleAlias(declaration, declaration); - } - else if (symbol.flags & ts.SymbolFlags.Alias && ts.isBindingElement(declaration) && ts.isInJSFile(declaration) && declaration.parent?.parent // exported import-like top-level JS require statement - && ts.isVariableDeclaration(declaration.parent.parent) - && declaration.parent.parent.parent?.parent && ts.isVariableStatement(declaration.parent.parent.parent.parent) - && !ts.hasSyntacticModifier(declaration.parent.parent.parent.parent, ts.ModifierFlags.Export) - && declaration.parent.parent.parent.parent.parent // check if the thing containing the variable statement is visible (ie, the file) - && isDeclarationVisible(declaration.parent.parent.parent.parent.parent)) { - return addVisibleAlias(declaration, declaration.parent.parent.parent.parent); - } - - // Declaration is not visible - return false; - } - - return true; - } - - function addVisibleAlias(declaration: ts.Declaration, aliasingStatement: ts.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 = ts.appendIfUnique(aliasesToMakeVisible, aliasingStatement); - } - return true; - } + const builder = flags & ts.SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToExpression : nodeBuilder.symbolToEntityName; + return writer ? symbolToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(symbolToStringWorker); + function symbolToStringWorker(writer: ts.EmitTextWriter) { + const entity = builder(symbol, meaning!, enclosingDeclaration, nodeFlags)!; // TODO: GH#18217 + // add neverAsciiEscape for GH#39027 + const printer = enclosingDeclaration?.kind === ts.SyntaxKind.SourceFile ? ts.createPrinter({ removeComments: true, neverAsciiEscape: true }) : ts.createPrinter({ removeComments: true }); + const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(ts.EmitHint.Unspecified, entity, /*sourceFile*/ sourceFile, writer); + return writer; } + } - function isEntityNameVisible(entityName: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node): ts.SymbolVisibilityResult { - // get symbol of the first identifier of the entityName - let meaning: ts.SymbolFlags; - if (entityName.parent.kind === ts.SyntaxKind.TypeQuery || - entityName.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && !ts.isPartOfTypeNode(entityName.parent) || - entityName.parent.kind === ts.SyntaxKind.ComputedPropertyName) { - // Typeof value - meaning = ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue; - } - else if (entityName.kind === ts.SyntaxKind.QualifiedName || entityName.kind === ts.SyntaxKind.PropertyAccessExpression || - entityName.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration) { - // Left identifier from type reference or TypeAlias - // Entity name of the import declaration - meaning = ts.SymbolFlags.Namespace; + function signatureToString(signature: ts.Signature, enclosingDeclaration?: ts.Node, flags = ts.TypeFormatFlags.None, kind?: ts.SignatureKind, writer?: ts.EmitTextWriter): string { + return writer ? signatureToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(signatureToStringWorker); + function signatureToStringWorker(writer: ts.EmitTextWriter) { + let sigOutput: ts.SyntaxKind; + if (flags & ts.TypeFormatFlags.WriteArrowStyleSignature) { + sigOutput = kind === ts.SignatureKind.Construct ? ts.SyntaxKind.ConstructorType : ts.SyntaxKind.FunctionType; } else { - // Type Reference or TypeAlias entity = Identifier - meaning = ts.SymbolFlags.Type; - } - - const firstIdentifier = ts.getFirstIdentifier(entityName); - const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); - if (symbol && symbol.flags & ts.SymbolFlags.TypeParameter && meaning & ts.SymbolFlags.Type) { - return { accessibility: ts.SymbolAccessibility.Accessible }; - } - - // Verify if the symbol is accessible - return (symbol && hasVisibleDeclarations(symbol, /*shouldComputeAliasToMakeVisible*/ true)) || { - accessibility: ts.SymbolAccessibility.NotAccessible, - errorSymbolName: ts.getTextOfNode(firstIdentifier), - errorNode: firstIdentifier - }; - } - - function symbolToString(symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags, flags: ts.SymbolFormatFlags = ts.SymbolFormatFlags.AllowAnyNodeKind, writer?: ts.EmitTextWriter): string { - let nodeFlags = ts.NodeBuilderFlags.IgnoreErrors; - if (flags & ts.SymbolFormatFlags.UseOnlyExternalAliasing) { - nodeFlags |= ts.NodeBuilderFlags.UseOnlyExternalAliasing; - } - if (flags & ts.SymbolFormatFlags.WriteTypeParametersOrArguments) { - nodeFlags |= ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName; - } - if (flags & ts.SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope) { - nodeFlags |= ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; - } - if (flags & ts.SymbolFormatFlags.DoNotIncludeSymbolChain) { - nodeFlags |= ts.NodeBuilderFlags.DoNotIncludeSymbolChain; - } - const builder = flags & ts.SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToExpression : nodeBuilder.symbolToEntityName; - return writer ? symbolToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(symbolToStringWorker); - function symbolToStringWorker(writer: ts.EmitTextWriter) { - const entity = builder(symbol, meaning!, enclosingDeclaration, nodeFlags)!; // TODO: GH#18217 - // add neverAsciiEscape for GH#39027 - const printer = enclosingDeclaration?.kind === ts.SyntaxKind.SourceFile ? ts.createPrinter({ removeComments: true, neverAsciiEscape: true }) : ts.createPrinter({ removeComments: true }); - const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(ts.EmitHint.Unspecified, entity, /*sourceFile*/ sourceFile, writer); - return writer; + sigOutput = kind === ts.SignatureKind.Construct ? ts.SyntaxKind.ConstructSignature : ts.SyntaxKind.CallSignature; } + const sig = nodeBuilder.signatureToSignatureDeclaration(signature, sigOutput, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName); + const printer = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true }); + const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(ts.EmitHint.Unspecified, sig!, /*sourceFile*/ sourceFile, ts.getTrailingSemicolonDeferringWriter(writer)); // TODO: GH#18217 + return writer; } + } - function signatureToString(signature: ts.Signature, enclosingDeclaration?: ts.Node, flags = ts.TypeFormatFlags.None, kind?: ts.SignatureKind, writer?: ts.EmitTextWriter): string { - return writer ? signatureToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(signatureToStringWorker); - function signatureToStringWorker(writer: ts.EmitTextWriter) { - let sigOutput: ts.SyntaxKind; - if (flags & ts.TypeFormatFlags.WriteArrowStyleSignature) { - sigOutput = kind === ts.SignatureKind.Construct ? ts.SyntaxKind.ConstructorType : ts.SyntaxKind.FunctionType; - } - else { - sigOutput = kind === ts.SignatureKind.Construct ? ts.SyntaxKind.ConstructSignature : ts.SyntaxKind.CallSignature; - } - const sig = nodeBuilder.signatureToSignatureDeclaration(signature, sigOutput, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName); - const printer = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true }); - const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(ts.EmitHint.Unspecified, sig!, /*sourceFile*/ sourceFile, ts.getTrailingSemicolonDeferringWriter(writer)); // TODO: GH#18217 - return writer; - } - } + function typeToString(type: ts.Type, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer: ts.EmitTextWriter = ts.createTextWriter("")): string { + const noTruncation = compilerOptions.noErrorTruncation || flags & ts.TypeFormatFlags.NoTruncation; + const typeNode = nodeBuilder.typeToTypeNode(type, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | (noTruncation ? ts.NodeBuilderFlags.NoTruncation : 0), writer); + if (typeNode === undefined) + return ts.Debug.fail("should always get typenode"); + // The unresolved type gets a synthesized comment on `any` to hint to users that it's not a plain `any`. + // Otherwise, we always strip comments out. + const options = { removeComments: type !== unresolvedType }; + const printer = ts.createPrinter(options); + const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(ts.EmitHint.Unspecified, typeNode, /*sourceFile*/ sourceFile, writer); + const result = writer.getText(); + + const maxLength = noTruncation ? ts.noTruncationMaximumTruncationLength * 2 : ts.defaultMaximumTruncationLength * 2; + if (maxLength && result && result.length >= maxLength) { + return result.substr(0, maxLength - "...".length) + "..."; + } + return result; + } - function typeToString(type: ts.Type, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer: ts.EmitTextWriter = ts.createTextWriter("")): string { - const noTruncation = compilerOptions.noErrorTruncation || flags & ts.TypeFormatFlags.NoTruncation; - const typeNode = nodeBuilder.typeToTypeNode(type, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | (noTruncation ? ts.NodeBuilderFlags.NoTruncation : 0), writer); - if (typeNode === undefined) - return ts.Debug.fail("should always get typenode"); - // The unresolved type gets a synthesized comment on `any` to hint to users that it's not a plain `any`. - // Otherwise, we always strip comments out. - const options = { removeComments: type !== unresolvedType }; - const printer = ts.createPrinter(options); - const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(ts.EmitHint.Unspecified, typeNode, /*sourceFile*/ sourceFile, writer); - const result = writer.getText(); + 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 = getTypeNameForErrorDisplay(left); + rightStr = getTypeNameForErrorDisplay(right); + } + return [leftStr, rightStr]; + } - const maxLength = noTruncation ? ts.noTruncationMaximumTruncationLength * 2 : ts.defaultMaximumTruncationLength * 2; - if (maxLength && result && result.length >= maxLength) { - return result.substr(0, maxLength - "...".length) + "..."; - } - return result; - } + function getTypeNameForErrorDisplay(type: ts.Type) { + return typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.UseFullyQualifiedType); + } - 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 = getTypeNameForErrorDisplay(left); - rightStr = getTypeNameForErrorDisplay(right); - } - return [leftStr, rightStr]; - } + function symbolValueDeclarationIsContextSensitive(symbol: ts.Symbol): boolean { + return symbol && !!symbol.valueDeclaration && ts.isExpression(symbol.valueDeclaration) && !isContextSensitive(symbol.valueDeclaration); + } - function getTypeNameForErrorDisplay(type: ts.Type) { - return typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.UseFullyQualifiedType); - } + function toNodeBuilderFlags(flags = ts.TypeFormatFlags.None): ts.NodeBuilderFlags { + return flags & ts.TypeFormatFlags.NodeBuilderFlagsMask; + } - function symbolValueDeclarationIsContextSensitive(symbol: ts.Symbol): boolean { - return symbol && !!symbol.valueDeclaration && ts.isExpression(symbol.valueDeclaration) && !isContextSensitive(symbol.valueDeclaration); - } + function isClassInstanceSide(type: ts.Type) { + return !!type.symbol && !!(type.symbol.flags & ts.SymbolFlags.Class) && (type === getDeclaredTypeOfClassOrInterface(type.symbol) || (!!(type.flags & ts.TypeFlags.Object) && !!(ts.getObjectFlags(type) & ts.ObjectFlags.IsClassInstanceClone))); + } - function toNodeBuilderFlags(flags = ts.TypeFormatFlags.None): ts.NodeBuilderFlags { - return flags & ts.TypeFormatFlags.NodeBuilderFlagsMask; - } + function createNodeBuilder() { + return { + typeToTypeNode: (type: ts.Type, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeToTypeNodeHelper(type, context)), + indexInfoToIndexSignatureDeclaration: (indexInfo: ts.IndexInfo, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, /*typeNode*/ undefined)), + signatureToSignatureDeclaration: (signature: ts.Signature, kind: ts.SignatureDeclaration["kind"], enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => signatureToSignatureDeclarationHelper(signature, kind, context)), + symbolToEntityName: (symbol: ts.Symbol, meaning: ts.SymbolFlags, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToName(symbol, context, meaning, /*expectsIdentifier*/ false)), + symbolToExpression: (symbol: ts.Symbol, meaning: ts.SymbolFlags, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToExpression(symbol, context, meaning)), + symbolToTypeParameterDeclarations: (symbol: ts.Symbol, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParametersToTypeParameterDeclarations(symbol, context)), + symbolToParameterDeclaration: (symbol: ts.Symbol, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToParameterDeclaration(symbol, context)), + typeParameterToDeclaration: (parameter: ts.TypeParameter, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParameterToDeclaration(parameter, context)), + symbolTableToDeclarationStatements: (symbolTable: ts.SymbolTable, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker, bundled?: boolean) => withContext(enclosingDeclaration, flags, tracker, context => symbolTableToDeclarationStatements(symbolTable, context, bundled)), + }; - function isClassInstanceSide(type: ts.Type) { - return !!type.symbol && !!(type.symbol.flags & ts.SymbolFlags.Class) && (type === getDeclaredTypeOfClassOrInterface(type.symbol) || (!!(type.flags & ts.TypeFlags.Object) && !!(ts.getObjectFlags(type) & ts.ObjectFlags.IsClassInstanceClone))); + function withContext(enclosingDeclaration: ts.Node | undefined, flags: ts.NodeBuilderFlags | undefined, tracker: ts.SymbolTracker | undefined, cb: (context: NodeBuilderContext) => T): T | undefined { + ts.Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & ts.NodeFlags.Synthesized) === 0); + const context: NodeBuilderContext = { + enclosingDeclaration, + flags: flags || ts.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: () => false, moduleResolverHost: flags! & ts.NodeBuilderFlags.DoNotIncludeSymbolChain ? { + getCommonSourceDirectory: !!(host as ts.Program).getCommonSourceDirectory ? () => (host as ts.Program).getCommonSourceDirectory() : () => "", + getCurrentDirectory: () => host.getCurrentDirectory(), + getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache), + getPackageJsonInfoCache: () => host.getPackageJsonInfoCache?.(), + useCaseSensitiveFileNames: ts.maybeBind(host, host.useCaseSensitiveFileNames), + redirectTargetsMap: host.redirectTargetsMap, + getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName), + isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName), + fileExists: fileName => host.fileExists(fileName), + getFileIncludeReasons: () => host.getFileIncludeReasons(), + readFile: host.readFile ? (fileName => host.readFile!(fileName)) : undefined, + } : undefined }, + encounteredError: false, + reportedDiagnostic: false, + visitedTypes: undefined, + symbolDepth: undefined, + inferTypeParameters: undefined, + approximateLength: 0 + }; + context.tracker = wrapSymbolTrackerToReportForContext(context, context.tracker); + const resultingNode = cb(context); + if (context.truncating && context.flags & ts.NodeBuilderFlags.NoTruncation) { + context.tracker?.reportTruncationError?.(); + } + return context.encounteredError ? undefined : resultingNode; } - function createNodeBuilder() { + function wrapSymbolTrackerToReportForContext(context: NodeBuilderContext, tracker: ts.SymbolTracker): ts.SymbolTracker { + const oldTrackSymbol = tracker.trackSymbol; return { - typeToTypeNode: (type: ts.Type, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeToTypeNodeHelper(type, context)), - indexInfoToIndexSignatureDeclaration: (indexInfo: ts.IndexInfo, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, /*typeNode*/ undefined)), - signatureToSignatureDeclaration: (signature: ts.Signature, kind: ts.SignatureDeclaration["kind"], enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => signatureToSignatureDeclarationHelper(signature, kind, context)), - symbolToEntityName: (symbol: ts.Symbol, meaning: ts.SymbolFlags, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToName(symbol, context, meaning, /*expectsIdentifier*/ false)), - symbolToExpression: (symbol: ts.Symbol, meaning: ts.SymbolFlags, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToExpression(symbol, context, meaning)), - symbolToTypeParameterDeclarations: (symbol: ts.Symbol, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParametersToTypeParameterDeclarations(symbol, context)), - symbolToParameterDeclaration: (symbol: ts.Symbol, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToParameterDeclaration(symbol, context)), - typeParameterToDeclaration: (parameter: ts.TypeParameter, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParameterToDeclaration(parameter, context)), - symbolTableToDeclarationStatements: (symbolTable: ts.SymbolTable, enclosingDeclaration?: ts.Node, flags?: ts.NodeBuilderFlags, tracker?: ts.SymbolTracker, bundled?: boolean) => withContext(enclosingDeclaration, flags, tracker, context => symbolTableToDeclarationStatements(symbolTable, context, bundled)), + ...tracker, + reportCyclicStructureError: wrapReportedDiagnostic(tracker.reportCyclicStructureError), + reportInaccessibleThisError: wrapReportedDiagnostic(tracker.reportInaccessibleThisError), + reportInaccessibleUniqueSymbolError: wrapReportedDiagnostic(tracker.reportInaccessibleUniqueSymbolError), + reportLikelyUnsafeImportRequiredError: wrapReportedDiagnostic(tracker.reportLikelyUnsafeImportRequiredError), + reportNonlocalAugmentation: wrapReportedDiagnostic(tracker.reportNonlocalAugmentation), + reportPrivateInBaseOfClassExpression: wrapReportedDiagnostic(tracker.reportPrivateInBaseOfClassExpression), + reportNonSerializableProperty: wrapReportedDiagnostic(tracker.reportNonSerializableProperty), + trackSymbol: oldTrackSymbol && ((...args) => { + const result = oldTrackSymbol(...args); + if (result) { + context.reportedDiagnostic = true; + } + return result; + }), }; - function withContext(enclosingDeclaration: ts.Node | undefined, flags: ts.NodeBuilderFlags | undefined, tracker: ts.SymbolTracker | undefined, cb: (context: NodeBuilderContext) => T): T | undefined { - ts.Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & ts.NodeFlags.Synthesized) === 0); - const context: NodeBuilderContext = { - enclosingDeclaration, - flags: flags || ts.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: () => false, moduleResolverHost: flags! & ts.NodeBuilderFlags.DoNotIncludeSymbolChain ? { - getCommonSourceDirectory: !!(host as ts.Program).getCommonSourceDirectory ? () => (host as ts.Program).getCommonSourceDirectory() : () => "", - getCurrentDirectory: () => host.getCurrentDirectory(), - getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache), - getPackageJsonInfoCache: () => host.getPackageJsonInfoCache?.(), - useCaseSensitiveFileNames: ts.maybeBind(host, host.useCaseSensitiveFileNames), - redirectTargetsMap: host.redirectTargetsMap, - getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName), - isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName), - fileExists: fileName => host.fileExists(fileName), - getFileIncludeReasons: () => host.getFileIncludeReasons(), - readFile: host.readFile ? (fileName => host.readFile!(fileName)) : undefined, - } : undefined }, - encounteredError: false, - reportedDiagnostic: false, - visitedTypes: undefined, - symbolDepth: undefined, - inferTypeParameters: undefined, - approximateLength: 0 - }; - context.tracker = wrapSymbolTrackerToReportForContext(context, context.tracker); - const resultingNode = cb(context); - if (context.truncating && context.flags & ts.NodeBuilderFlags.NoTruncation) { - context.tracker?.reportTruncationError?.(); + function wrapReportedDiagnostic any>(method: T | undefined): T | undefined { + if (!method) { + return method; } - return context.encounteredError ? undefined : resultingNode; + return (((...args) => { + context.reportedDiagnostic = true; + return method(...args); + }) as T); } + } - function wrapSymbolTrackerToReportForContext(context: NodeBuilderContext, tracker: ts.SymbolTracker): ts.SymbolTracker { - const oldTrackSymbol = tracker.trackSymbol; - return { - ...tracker, - reportCyclicStructureError: wrapReportedDiagnostic(tracker.reportCyclicStructureError), - reportInaccessibleThisError: wrapReportedDiagnostic(tracker.reportInaccessibleThisError), - reportInaccessibleUniqueSymbolError: wrapReportedDiagnostic(tracker.reportInaccessibleUniqueSymbolError), - reportLikelyUnsafeImportRequiredError: wrapReportedDiagnostic(tracker.reportLikelyUnsafeImportRequiredError), - reportNonlocalAugmentation: wrapReportedDiagnostic(tracker.reportNonlocalAugmentation), - reportPrivateInBaseOfClassExpression: wrapReportedDiagnostic(tracker.reportPrivateInBaseOfClassExpression), - reportNonSerializableProperty: wrapReportedDiagnostic(tracker.reportNonSerializableProperty), - trackSymbol: oldTrackSymbol && ((...args) => { - const result = oldTrackSymbol(...args); - if (result) { - context.reportedDiagnostic = true; - } - return result; - }), - }; - - function wrapReportedDiagnostic any>(method: T | undefined): T | undefined { - if (!method) { - return method; - } - return (((...args) => { - context.reportedDiagnostic = true; - return method(...args); - }) as T); - } - } + function checkTruncationLength(context: NodeBuilderContext): boolean { + if (context.truncating) + return context.truncating; + return context.truncating = context.approximateLength > ((context.flags & ts.NodeBuilderFlags.NoTruncation) ? ts.noTruncationMaximumTruncationLength : ts.defaultMaximumTruncationLength); + } - function checkTruncationLength(context: NodeBuilderContext): boolean { - if (context.truncating) - return context.truncating; - return context.truncating = context.approximateLength > ((context.flags & ts.NodeBuilderFlags.NoTruncation) ? ts.noTruncationMaximumTruncationLength : ts.defaultMaximumTruncationLength); + function typeToTypeNodeHelper(type: ts.Type, context: NodeBuilderContext): ts.TypeNode { + if (cancellationToken && cancellationToken.throwIfCancellationRequested) { + cancellationToken.throwIfCancellationRequested(); } + const inTypeAlias = context.flags & ts.NodeBuilderFlags.InTypeAlias; + context.flags &= ~ts.NodeBuilderFlags.InTypeAlias; - function typeToTypeNodeHelper(type: ts.Type, context: NodeBuilderContext): ts.TypeNode { - if (cancellationToken && cancellationToken.throwIfCancellationRequested) { - cancellationToken.throwIfCancellationRequested(); + if (!type) { + if (!(context.flags & ts.NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { + context.encounteredError = true; + return undefined!; // TODO: GH#18217 } - const inTypeAlias = context.flags & ts.NodeBuilderFlags.InTypeAlias; - context.flags &= ~ts.NodeBuilderFlags.InTypeAlias; + context.approximateLength += 3; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } - if (!type) { - if (!(context.flags & ts.NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { - context.encounteredError = true; - return undefined!; // TODO: GH#18217 - } - context.approximateLength += 3; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } + if (!(context.flags & ts.NodeBuilderFlags.NoTypeReduction)) { + type = getReducedType(type); + } - if (!(context.flags & ts.NodeBuilderFlags.NoTypeReduction)) { - type = getReducedType(type); + if (type.flags & ts.TypeFlags.Any) { + if (type.aliasSymbol) { + return ts.factory.createTypeReferenceNode(symbolToEntityNameNode(type.aliasSymbol), mapToTypeNodes(type.aliasTypeArguments, context)); } - - if (type.flags & ts.TypeFlags.Any) { - if (type.aliasSymbol) { - return ts.factory.createTypeReferenceNode(symbolToEntityNameNode(type.aliasSymbol), mapToTypeNodes(type.aliasTypeArguments, context)); - } - if (type === unresolvedType) { - return ts.addSyntheticLeadingComment(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), ts.SyntaxKind.MultiLineCommentTrivia, "unresolved"); - } - context.approximateLength += 3; - return ts.factory.createKeywordTypeNode(type === intrinsicMarkerType ? ts.SyntaxKind.IntrinsicKeyword : ts.SyntaxKind.AnyKeyword); + if (type === unresolvedType) { + return ts.addSyntheticLeadingComment(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), ts.SyntaxKind.MultiLineCommentTrivia, "unresolved"); } - if (type.flags & ts.TypeFlags.Unknown) { - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); + context.approximateLength += 3; + return ts.factory.createKeywordTypeNode(type === intrinsicMarkerType ? ts.SyntaxKind.IntrinsicKeyword : ts.SyntaxKind.AnyKeyword); + } + if (type.flags & ts.TypeFlags.Unknown) { + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); + } + if (type.flags & ts.TypeFlags.String) { + context.approximateLength += 6; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); + } + if (type.flags & ts.TypeFlags.Number) { + context.approximateLength += 6; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); + } + if (type.flags & ts.TypeFlags.BigInt) { + context.approximateLength += 6; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword); + } + if (type.flags & ts.TypeFlags.Boolean && !type.aliasSymbol) { + context.approximateLength += 7; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); + } + if (type.flags & ts.TypeFlags.EnumLiteral && !(type.flags & ts.TypeFlags.Union)) { + const parentSymbol = getParentOfSymbol(type.symbol)!; + const parentName = symbolToTypeNode(parentSymbol, context, ts.SymbolFlags.Type); + if (getDeclaredTypeOfSymbol(parentSymbol) === type) { + return parentName; } - if (type.flags & ts.TypeFlags.String) { - context.approximateLength += 6; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); + const memberName = ts.symbolName(type.symbol); + if (ts.isIdentifierText(memberName, ts.ScriptTarget.ES3)) { + return appendReferenceToType(parentName as ts.TypeReferenceNode | ts.ImportTypeNode, ts.factory.createTypeReferenceNode(memberName, /*typeArguments*/ undefined)); } - if (type.flags & ts.TypeFlags.Number) { - context.approximateLength += 6; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); + if (ts.isImportTypeNode(parentName)) { + (parentName as any).isTypeOf = true; // mutably update, node is freshly manufactured anyhow + return ts.factory.createIndexedAccessTypeNode(parentName, ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(memberName))); } - if (type.flags & ts.TypeFlags.BigInt) { - context.approximateLength += 6; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword); + else if (ts.isTypeReferenceNode(parentName)) { + return ts.factory.createIndexedAccessTypeNode(ts.factory.createTypeQueryNode(parentName.typeName), ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(memberName))); } - if (type.flags & ts.TypeFlags.Boolean && !type.aliasSymbol) { - context.approximateLength += 7; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); + else { + return ts.Debug.fail("Unhandled type node kind returned from `symbolToTypeNode`."); } - if (type.flags & ts.TypeFlags.EnumLiteral && !(type.flags & ts.TypeFlags.Union)) { - const parentSymbol = getParentOfSymbol(type.symbol)!; - const parentName = symbolToTypeNode(parentSymbol, context, ts.SymbolFlags.Type); - if (getDeclaredTypeOfSymbol(parentSymbol) === type) { - return parentName; - } - const memberName = ts.symbolName(type.symbol); - if (ts.isIdentifierText(memberName, ts.ScriptTarget.ES3)) { - return appendReferenceToType(parentName as ts.TypeReferenceNode | ts.ImportTypeNode, ts.factory.createTypeReferenceNode(memberName, /*typeArguments*/ undefined)); + } + if (type.flags & ts.TypeFlags.EnumLike) { + return symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Type); + } + if (type.flags & ts.TypeFlags.StringLiteral) { + context.approximateLength += ((type as ts.StringLiteralType).value.length + 2); + return ts.factory.createLiteralTypeNode(ts.setEmitFlags(ts.factory.createStringLiteral((type as ts.StringLiteralType).value, !!(context.flags & ts.NodeBuilderFlags.UseSingleQuotesForStringLiteralType)), ts.EmitFlags.NoAsciiEscaping)); + } + if (type.flags & ts.TypeFlags.NumberLiteral) { + const value = (type as ts.NumberLiteralType).value; + context.approximateLength += ("" + value).length; + return ts.factory.createLiteralTypeNode(value < 0 ? ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(-value)) : ts.factory.createNumericLiteral(value)); + } + if (type.flags & ts.TypeFlags.BigIntLiteral) { + context.approximateLength += (ts.pseudoBigIntToString((type as ts.BigIntLiteralType).value).length) + 1; + return ts.factory.createLiteralTypeNode((ts.factory.createBigIntLiteral((type as ts.BigIntLiteralType).value))); + } + if (type.flags & ts.TypeFlags.BooleanLiteral) { + context.approximateLength += (type as ts.IntrinsicType).intrinsicName.length; + return ts.factory.createLiteralTypeNode((type as ts.IntrinsicType).intrinsicName === "true" ? ts.factory.createTrue() : ts.factory.createFalse()); + } + if (type.flags & ts.TypeFlags.UniqueESSymbol) { + if (!(context.flags & ts.NodeBuilderFlags.AllowUniqueESSymbolType)) { + if (isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + context.approximateLength += 6; + return symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Value); } - if (ts.isImportTypeNode(parentName)) { - (parentName as any).isTypeOf = true; // mutably update, node is freshly manufactured anyhow - return ts.factory.createIndexedAccessTypeNode(parentName, ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(memberName))); + if (context.tracker.reportInaccessibleUniqueSymbolError) { + context.tracker.reportInaccessibleUniqueSymbolError(); } - else if (ts.isTypeReferenceNode(parentName)) { - return ts.factory.createIndexedAccessTypeNode(ts.factory.createTypeQueryNode(parentName.typeName), ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(memberName))); + } + context.approximateLength += 13; + return ts.factory.createTypeOperatorNode(ts.SyntaxKind.UniqueKeyword, ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword)); + } + if (type.flags & ts.TypeFlags.Void) { + context.approximateLength += 4; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); + } + if (type.flags & ts.TypeFlags.Undefined) { + context.approximateLength += 9; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); + } + if (type.flags & ts.TypeFlags.Null) { + context.approximateLength += 4; + return ts.factory.createLiteralTypeNode(ts.factory.createNull()); + } + if (type.flags & ts.TypeFlags.Never) { + context.approximateLength += 5; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword); + } + if (type.flags & ts.TypeFlags.ESSymbol) { + context.approximateLength += 6; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword); + } + if (type.flags & ts.TypeFlags.NonPrimitive) { + context.approximateLength += 6; + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword); + } + if (ts.isThisTypeParameter(type)) { + if (context.flags & ts.NodeBuilderFlags.InObjectTypeLiteral) { + if (!context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowThisInObjectLiteral)) { + context.encounteredError = true; } - else { - return ts.Debug.fail("Unhandled type node kind returned from `symbolToTypeNode`."); + if (context.tracker.reportInaccessibleThisError) { + context.tracker.reportInaccessibleThisError(); } } - if (type.flags & ts.TypeFlags.EnumLike) { + context.approximateLength += 4; + return ts.factory.createThisTypeNode(); + } + + if (!inTypeAlias && type.aliasSymbol && (context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) { + const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context); + if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & ts.SymbolFlags.Class)) + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""), typeArgumentNodes); + return symbolToTypeNode(type.aliasSymbol, context, ts.SymbolFlags.Type, typeArgumentNodes); + } + const objectFlags = ts.getObjectFlags(type); + if (objectFlags & ts.ObjectFlags.Reference) { + ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); + return (type as ts.TypeReference).node ? visitAndTransformType(type, typeReferenceToTypeNode) : typeReferenceToTypeNode(type as ts.TypeReference); + } + if (type.flags & ts.TypeFlags.TypeParameter || objectFlags & ts.ObjectFlags.ClassOrInterface) { + if (type.flags & ts.TypeFlags.TypeParameter && ts.contains(context.inferTypeParameters, type)) { + context.approximateLength += (ts.symbolName(type.symbol).length + 6); + let constraintNode: ts.TypeNode | undefined; + const constraint = getConstraintOfTypeParameter(type as ts.TypeParameter); + if (constraint) { + // If the infer type has a constraint that is not the same as the constraint + // we would have normally inferred based on context, we emit the constraint + // using `infer T extends ?`. We omit inferred constraints from type references + // as they may be elided. + const inferredConstraint = getInferredTypeParameterConstraint(type as ts.TypeParameter, /*omitTypeReferences*/ true); + if (!(inferredConstraint && isTypeIdenticalTo(constraint, inferredConstraint))) { + context.approximateLength += 9; + constraintNode = constraint && typeToTypeNodeHelper(constraint, context); + } + } + return ts.factory.createInferTypeNode(typeParameterToDeclarationWithConstraint(type as ts.TypeParameter, context, constraintNode)); + } + if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && + type.flags & ts.TypeFlags.TypeParameter && + !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + const name = typeParameterToName(type, context); + context.approximateLength += ts.idText(name).length; + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(ts.idText(name)), /*typeArguments*/ undefined); + } + // Ignore constraint/default when creating a usage (as opposed to declaration) of a type parameter. + if (type.symbol) { return symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Type); } - if (type.flags & ts.TypeFlags.StringLiteral) { - context.approximateLength += ((type as ts.StringLiteralType).value.length + 2); - return ts.factory.createLiteralTypeNode(ts.setEmitFlags(ts.factory.createStringLiteral((type as ts.StringLiteralType).value, !!(context.flags & ts.NodeBuilderFlags.UseSingleQuotesForStringLiteralType)), ts.EmitFlags.NoAsciiEscaping)); - } - if (type.flags & ts.TypeFlags.NumberLiteral) { - const value = (type as ts.NumberLiteralType).value; - context.approximateLength += ("" + value).length; - return ts.factory.createLiteralTypeNode(value < 0 ? ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(-value)) : ts.factory.createNumericLiteral(value)); - } - if (type.flags & ts.TypeFlags.BigIntLiteral) { - context.approximateLength += (ts.pseudoBigIntToString((type as ts.BigIntLiteralType).value).length) + 1; - return ts.factory.createLiteralTypeNode((ts.factory.createBigIntLiteral((type as ts.BigIntLiteralType).value))); + const name = (type === markerSuperType || type === markerSubType) && varianceTypeParameter && varianceTypeParameter.symbol ? + (type === markerSubType ? "sub-" : "super-") + ts.symbolName(varianceTypeParameter.symbol) : "?"; + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(name), /*typeArguments*/ undefined); + } + if (type.flags & ts.TypeFlags.Union && (type as ts.UnionType).origin) { + type = (type as ts.UnionType).origin!; + } + if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) { + const types = type.flags & ts.TypeFlags.Union ? formatUnionTypes((type as ts.UnionType).types) : (type as ts.IntersectionType).types; + if (ts.length(types) === 1) { + return typeToTypeNodeHelper(types[0], context); } - if (type.flags & ts.TypeFlags.BooleanLiteral) { - context.approximateLength += (type as ts.IntrinsicType).intrinsicName.length; - return ts.factory.createLiteralTypeNode((type as ts.IntrinsicType).intrinsicName === "true" ? ts.factory.createTrue() : ts.factory.createFalse()); + const typeNodes = mapToTypeNodes(types, context, /*isBareList*/ true); + if (typeNodes && typeNodes.length > 0) { + return type.flags & ts.TypeFlags.Union ? ts.factory.createUnionTypeNode(typeNodes) : ts.factory.createIntersectionTypeNode(typeNodes); } - if (type.flags & ts.TypeFlags.UniqueESSymbol) { - if (!(context.flags & ts.NodeBuilderFlags.AllowUniqueESSymbolType)) { - if (isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { - context.approximateLength += 6; - return symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Value); - } - if (context.tracker.reportInaccessibleUniqueSymbolError) { - context.tracker.reportInaccessibleUniqueSymbolError(); - } + else { + if (!context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { + context.encounteredError = true; } - context.approximateLength += 13; - return ts.factory.createTypeOperatorNode(ts.SyntaxKind.UniqueKeyword, ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword)); - } - if (type.flags & ts.TypeFlags.Void) { - context.approximateLength += 4; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); - } - if (type.flags & ts.TypeFlags.Undefined) { - context.approximateLength += 9; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); + return undefined!; // TODO: GH#18217 } - if (type.flags & ts.TypeFlags.Null) { - context.approximateLength += 4; - return ts.factory.createLiteralTypeNode(ts.factory.createNull()); - } - if (type.flags & ts.TypeFlags.Never) { - context.approximateLength += 5; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword); - } - if (type.flags & ts.TypeFlags.ESSymbol) { - context.approximateLength += 6; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword); - } - if (type.flags & ts.TypeFlags.NonPrimitive) { - context.approximateLength += 6; - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword); - } - if (ts.isThisTypeParameter(type)) { - if (context.flags & ts.NodeBuilderFlags.InObjectTypeLiteral) { - if (!context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowThisInObjectLiteral)) { + } + if (objectFlags & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped)) { + ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); + // The type is an object literal type. + return createAnonymousTypeNode(type as ts.ObjectType); + } + if (type.flags & ts.TypeFlags.Index) { + const indexedType = (type as ts.IndexType).type; + context.approximateLength += 6; + const indexTypeNode = typeToTypeNodeHelper(indexedType, context); + return ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, indexTypeNode); + } + if (type.flags & ts.TypeFlags.TemplateLiteral) { + const texts = (type as ts.TemplateLiteralType).texts; + const types = (type as ts.TemplateLiteralType).types; + const templateHead = ts.factory.createTemplateHead(texts[0]); + const templateSpans = ts.factory.createNodeArray(ts.map(types, (t, i) => ts.factory.createTemplateLiteralTypeSpan(typeToTypeNodeHelper(t, context), (i < types.length - 1 ? ts.factory.createTemplateMiddle : ts.factory.createTemplateTail)(texts[i + 1])))); + context.approximateLength += 2; + return ts.factory.createTemplateLiteralType(templateHead, templateSpans); + } + if (type.flags & ts.TypeFlags.StringMapping) { + const typeNode = typeToTypeNodeHelper((type as ts.StringMappingType).type, context); + return symbolToTypeNode((type as ts.StringMappingType).symbol, context, ts.SymbolFlags.Type, [typeNode]); + } + if (type.flags & ts.TypeFlags.IndexedAccess) { + const objectTypeNode = typeToTypeNodeHelper((type as ts.IndexedAccessType).objectType, context); + const indexTypeNode = typeToTypeNodeHelper((type as ts.IndexedAccessType).indexType, context); + context.approximateLength += 2; + return ts.factory.createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); + } + if (type.flags & ts.TypeFlags.Conditional) { + return visitAndTransformType(type, type => conditionalTypeToTypeNode(type as ts.ConditionalType)); + } + if (type.flags & ts.TypeFlags.Substitution) { + return typeToTypeNodeHelper((type as ts.SubstitutionType).baseType, context); + } + + return ts.Debug.fail("Should be unreachable."); + function conditionalTypeToTypeNode(type: ts.ConditionalType) { + const checkTypeNode = typeToTypeNodeHelper(type.checkType, context); + context.approximateLength += 15; + if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && type.root.isDistributive && !(type.checkType.flags & ts.TypeFlags.TypeParameter)) { + const newParam = createTypeParameter(createSymbol(ts.SymbolFlags.TypeParameter, "T" as ts.__String)); + const name = typeParameterToName(newParam, context); + const newTypeVariable = ts.factory.createTypeReferenceNode(name); + context.approximateLength += 37; // 15 each for two added conditionals, 7 for an added infer type + const newMapper = prependTypeMapping(type.root.checkType, newParam, type.combinedMapper || type.mapper); + const saveInferTypeParameters = context.inferTypeParameters; + context.inferTypeParameters = type.root.inferTypeParameters; + const extendsTypeNode = typeToTypeNodeHelper(instantiateType(type.root.extendsType, newMapper), context); + context.inferTypeParameters = saveInferTypeParameters; + const trueTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.trueType), newMapper)); + const falseTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.falseType), newMapper)); + + + // outermost conditional makes `T` a type parameter, allowing the inner conditionals to be distributive + // second conditional makes `T` have `T & checkType` substitution, so it is correctly usable as the checkType + // inner conditional runs the check the user provided on the check type (distributively) and returns the result + // checkType extends infer T ? T extends checkType ? T extends extendsType ? trueType : falseType : never : never; + // this is potentially simplifiable to + // checkType extends infer T ? T extends checkType & extendsType ? trueType : falseType : never; + // but that may confuse users who read the output more. + // On the other hand, + // checkType extends infer T extends checkType ? T extends extendsType ? trueType : falseType : never; + // may also work with `infer ... extends ...` in, but would produce declarations only compatible with the latest TS. + return ts.factory.createConditionalTypeNode(checkTypeNode, ts.factory.createInferTypeNode(ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, ts.factory.cloneNode(newTypeVariable.typeName) as ts.Identifier)), ts.factory.createConditionalTypeNode(ts.factory.createTypeReferenceNode(ts.factory.cloneNode(name)), typeToTypeNodeHelper(type.checkType, context), ts.factory.createConditionalTypeNode(newTypeVariable, extendsTypeNode, trueTypeNode, falseTypeNode), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)); + } + const saveInferTypeParameters = context.inferTypeParameters; + context.inferTypeParameters = type.root.inferTypeParameters; + const extendsTypeNode = typeToTypeNodeHelper(type.extendsType, context); + context.inferTypeParameters = saveInferTypeParameters; + const trueTypeNode = typeToTypeNodeOrCircularityElision(getTrueTypeFromConditionalType(type)); + const falseTypeNode = typeToTypeNodeOrCircularityElision(getFalseTypeFromConditionalType(type)); + return ts.factory.createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); + } + + function typeToTypeNodeOrCircularityElision(type: ts.Type) { + if (type.flags & ts.TypeFlags.Union) { + if (context.visitedTypes?.has(getTypeId(type))) { + if (!(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier)) { context.encounteredError = true; + context.tracker?.reportCyclicStructureError?.(); } - if (context.tracker.reportInaccessibleThisError) { - context.tracker.reportInaccessibleThisError(); - } + return createElidedInformationPlaceholder(context); } - context.approximateLength += 4; - return ts.factory.createThisTypeNode(); + return visitAndTransformType(type, type => typeToTypeNodeHelper(type, context)); } + return typeToTypeNodeHelper(type, context); + } - if (!inTypeAlias && type.aliasSymbol && (context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) { - const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context); - if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & ts.SymbolFlags.Class)) - return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""), typeArgumentNodes); - return symbolToTypeNode(type.aliasSymbol, context, ts.SymbolFlags.Type, typeArgumentNodes); + function createMappedTypeNodeFromType(type: ts.MappedType) { + ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); + const readonlyToken = type.declaration.readonlyToken ? ts.factory.createToken(type.declaration.readonlyToken.kind) as ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken : undefined; + const questionToken = type.declaration.questionToken ? ts.factory.createToken(type.declaration.questionToken.kind) as ts.QuestionToken | ts.PlusToken | ts.MinusToken : undefined; + let appropriateConstraintTypeNode: ts.TypeNode; + let newTypeVariable: ts.TypeReferenceNode | undefined; + 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` + if (!(getModifiersTypeFromMappedType(type).flags & ts.TypeFlags.TypeParameter) && context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { + const newParam = createTypeParameter(createSymbol(ts.SymbolFlags.TypeParameter, "T" as ts.__String)); + const name = typeParameterToName(newParam, context); + newTypeVariable = ts.factory.createTypeReferenceNode(name); + } + appropriateConstraintTypeNode = ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)); } - const objectFlags = ts.getObjectFlags(type); - if (objectFlags & ts.ObjectFlags.Reference) { - ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); - return (type as ts.TypeReference).node ? visitAndTransformType(type, typeReferenceToTypeNode) : typeReferenceToTypeNode(type as ts.TypeReference); + else { + appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context); + } + const typeParameterNode = typeParameterToDeclarationWithConstraint(getTypeParameterFromMappedType(type), context, appropriateConstraintTypeNode); + const nameTypeNode = type.declaration.nameType ? typeToTypeNodeHelper(getNameTypeFromMappedType(type)!, context) : undefined; + const templateTypeNode = typeToTypeNodeHelper(removeMissingType(getTemplateTypeFromMappedType(type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), context); + const mappedTypeNode = ts.factory.createMappedTypeNode(readonlyToken, typeParameterNode, nameTypeNode, questionToken, templateTypeNode, /*members*/ undefined); + context.approximateLength += 10; + const result = ts.setEmitFlags(mappedTypeNode, ts.EmitFlags.SingleLine); + if (isMappedTypeWithKeyofConstraintDeclaration(type) && !(getModifiersTypeFromMappedType(type).flags & ts.TypeFlags.TypeParameter) && context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { + // homomorphic mapped type with a non-homomorphic naive inlining + // wrap it with a conditional like `SomeModifiersType extends infer U ? {..the mapped type...} : never` to ensure the resulting + // type stays homomorphic + return ts.factory.createConditionalTypeNode(typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context), ts.factory.createInferTypeNode(ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, ts.factory.cloneNode(newTypeVariable!.typeName) as ts.Identifier)), result, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)); } - if (type.flags & ts.TypeFlags.TypeParameter || objectFlags & ts.ObjectFlags.ClassOrInterface) { - if (type.flags & ts.TypeFlags.TypeParameter && ts.contains(context.inferTypeParameters, type)) { - context.approximateLength += (ts.symbolName(type.symbol).length + 6); - let constraintNode: ts.TypeNode | undefined; - const constraint = getConstraintOfTypeParameter(type as ts.TypeParameter); - if (constraint) { - // If the infer type has a constraint that is not the same as the constraint - // we would have normally inferred based on context, we emit the constraint - // using `infer T extends ?`. We omit inferred constraints from type references - // as they may be elided. - const inferredConstraint = getInferredTypeParameterConstraint(type as ts.TypeParameter, /*omitTypeReferences*/ true); - if (!(inferredConstraint && isTypeIdenticalTo(constraint, inferredConstraint))) { - context.approximateLength += 9; - constraintNode = constraint && typeToTypeNodeHelper(constraint, context); - } + return result; + } + + function createAnonymousTypeNode(type: ts.ObjectType): ts.TypeNode { + const typeId = type.id; + const symbol = type.symbol; + if (symbol) { + const isInstanceType = isClassInstanceSide(type) ? ts.SymbolFlags.Type : ts.SymbolFlags.Value; + if (isJSConstructor(symbol.valueDeclaration)) { + // Instance and static types share the same symbol; only add 'typeof' for the static side. + return symbolToTypeNode(symbol, context, isInstanceType); + } + // Always use 'typeof T' for type of class, enum, and module objects + else if (symbol.flags & ts.SymbolFlags.Class + && !getBaseTypeVariableOfClass(symbol) + && !(symbol.valueDeclaration && symbol.valueDeclaration.kind === ts.SyntaxKind.ClassExpression && context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) || + symbol.flags & (ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule) || + shouldWriteTypeOfFunctionSymbol()) { + return symbolToTypeNode(symbol, context, isInstanceType); + } + else if (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, ts.SymbolFlags.Type); + } + else { + return createElidedInformationPlaceholder(context); } - return ts.factory.createInferTypeNode(typeParameterToDeclarationWithConstraint(type as ts.TypeParameter, context, constraintNode)); - } - if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && - type.flags & ts.TypeFlags.TypeParameter && - !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration)) { - const name = typeParameterToName(type, context); - context.approximateLength += ts.idText(name).length; - return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(ts.idText(name)), /*typeArguments*/ undefined); } - // Ignore constraint/default when creating a usage (as opposed to declaration) of a type parameter. - if (type.symbol) { - return symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Type); + else { + return visitAndTransformType(type, createTypeNodeFromObjectType); } - const name = (type === markerSuperType || type === markerSubType) && varianceTypeParameter && varianceTypeParameter.symbol ? - (type === markerSubType ? "sub-" : "super-") + ts.symbolName(varianceTypeParameter.symbol) : "?"; - return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(name), /*typeArguments*/ undefined); } - if (type.flags & ts.TypeFlags.Union && (type as ts.UnionType).origin) { - type = (type as ts.UnionType).origin!; + else { + // Anonymous types without a symbol are never circular. + return createTypeNodeFromObjectType(type); } - if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) { - const types = type.flags & ts.TypeFlags.Union ? formatUnionTypes((type as ts.UnionType).types) : (type as ts.IntersectionType).types; - if (ts.length(types) === 1) { - return typeToTypeNodeHelper(types[0], context); - } - const typeNodes = mapToTypeNodes(types, context, /*isBareList*/ true); - if (typeNodes && typeNodes.length > 0) { - return type.flags & ts.TypeFlags.Union ? ts.factory.createUnionTypeNode(typeNodes) : ts.factory.createIntersectionTypeNode(typeNodes); - } - else { - if (!context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { - context.encounteredError = true; - } - return undefined!; // TODO: GH#18217 + function shouldWriteTypeOfFunctionSymbol() { + const isStaticMethodSymbol = !!(symbol.flags & ts.SymbolFlags.Method) && // typeof static method + ts.some(symbol.declarations, declaration => ts.isStatic(declaration)); + const isNonLocalFunctionSymbol = !!(symbol.flags & ts.SymbolFlags.Function) && + (symbol.parent || // is exported function symbol + ts.forEach(symbol.declarations, declaration => declaration.parent.kind === ts.SyntaxKind.SourceFile || declaration.parent.kind === ts.SyntaxKind.ModuleBlock)); + if (isStaticMethodSymbol || isNonLocalFunctionSymbol) { + // typeof is allowed only for static/non local functions + return (!!(context.flags & ts.NodeBuilderFlags.UseTypeOfFunction) || (context.visitedTypes?.has(typeId))) && // it is type of the symbol uses itself recursively + (!(context.flags & ts.NodeBuilderFlags.UseStructuralFallback) || isValueSymbolAccessible(symbol, context.enclosingDeclaration)); // And the build is going to succeed without visibility error or there is no structural fallback allowed } } - if (objectFlags & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped)) { - ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); - // The type is an object literal type. - return createAnonymousTypeNode(type as ts.ObjectType); - } - if (type.flags & ts.TypeFlags.Index) { - const indexedType = (type as ts.IndexType).type; - context.approximateLength += 6; - const indexTypeNode = typeToTypeNodeHelper(indexedType, context); - return ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, indexTypeNode); - } - if (type.flags & ts.TypeFlags.TemplateLiteral) { - const texts = (type as ts.TemplateLiteralType).texts; - const types = (type as ts.TemplateLiteralType).types; - const templateHead = ts.factory.createTemplateHead(texts[0]); - const templateSpans = ts.factory.createNodeArray(ts.map(types, (t, i) => ts.factory.createTemplateLiteralTypeSpan(typeToTypeNodeHelper(t, context), (i < types.length - 1 ? ts.factory.createTemplateMiddle : ts.factory.createTemplateTail)(texts[i + 1])))); - context.approximateLength += 2; - return ts.factory.createTemplateLiteralType(templateHead, templateSpans); - } - if (type.flags & ts.TypeFlags.StringMapping) { - const typeNode = typeToTypeNodeHelper((type as ts.StringMappingType).type, context); - return symbolToTypeNode((type as ts.StringMappingType).symbol, context, ts.SymbolFlags.Type, [typeNode]); + } + + function visitAndTransformType(type: ts.Type, transform: (type: ts.Type) => T) { + const typeId = type.id; + const isConstructorObject = ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && type.symbol && type.symbol.flags & ts.SymbolFlags.Class; + const id = ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).node ? "N" + getNodeId((type as ts.TypeReference).node!) : + type.flags & ts.TypeFlags.Conditional ? "N" + getNodeId((type as ts.ConditionalType).root.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 = new ts.Set(); } - if (type.flags & ts.TypeFlags.IndexedAccess) { - const objectTypeNode = typeToTypeNodeHelper((type as ts.IndexedAccessType).objectType, context); - const indexTypeNode = typeToTypeNodeHelper((type as ts.IndexedAccessType).indexType, context); - context.approximateLength += 2; - return ts.factory.createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); + if (id && !context.symbolDepth) { + context.symbolDepth = new ts.Map(); } - if (type.flags & ts.TypeFlags.Conditional) { - return visitAndTransformType(type, type => conditionalTypeToTypeNode(type as ts.ConditionalType)); + + const links = context.enclosingDeclaration && getNodeLinks(context.enclosingDeclaration); + const key = `${getTypeId(type)}|${context.flags}`; + if (links) { + links.serializedTypes ||= new ts.Map(); } - if (type.flags & ts.TypeFlags.Substitution) { - return typeToTypeNodeHelper((type as ts.SubstitutionType).baseType, context); + const cachedResult = links?.serializedTypes?.get(key); + if (cachedResult) { + if (cachedResult.truncating) { + context.truncating = true; + } + context.approximateLength += cachedResult.addedLength; + return deepCloneOrReuseNode(cachedResult) as ts.TypeNode as T; } - return ts.Debug.fail("Should be unreachable."); - function conditionalTypeToTypeNode(type: ts.ConditionalType) { - const checkTypeNode = typeToTypeNodeHelper(type.checkType, context); - context.approximateLength += 15; - if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && type.root.isDistributive && !(type.checkType.flags & ts.TypeFlags.TypeParameter)) { - const newParam = createTypeParameter(createSymbol(ts.SymbolFlags.TypeParameter, "T" as ts.__String)); - const name = typeParameterToName(newParam, context); - const newTypeVariable = ts.factory.createTypeReferenceNode(name); - context.approximateLength += 37; // 15 each for two added conditionals, 7 for an added infer type - const newMapper = prependTypeMapping(type.root.checkType, newParam, type.combinedMapper || type.mapper); - const saveInferTypeParameters = context.inferTypeParameters; - context.inferTypeParameters = type.root.inferTypeParameters; - const extendsTypeNode = typeToTypeNodeHelper(instantiateType(type.root.extendsType, newMapper), context); - context.inferTypeParameters = saveInferTypeParameters; - const trueTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.trueType), newMapper)); - const falseTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.falseType), newMapper)); - - - // outermost conditional makes `T` a type parameter, allowing the inner conditionals to be distributive - // second conditional makes `T` have `T & checkType` substitution, so it is correctly usable as the checkType - // inner conditional runs the check the user provided on the check type (distributively) and returns the result - // checkType extends infer T ? T extends checkType ? T extends extendsType ? trueType : falseType : never : never; - // this is potentially simplifiable to - // checkType extends infer T ? T extends checkType & extendsType ? trueType : falseType : never; - // but that may confuse users who read the output more. - // On the other hand, - // checkType extends infer T extends checkType ? T extends extendsType ? trueType : falseType : never; - // may also work with `infer ... extends ...` in, but would produce declarations only compatible with the latest TS. - return ts.factory.createConditionalTypeNode(checkTypeNode, ts.factory.createInferTypeNode(ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, ts.factory.cloneNode(newTypeVariable.typeName) as ts.Identifier)), ts.factory.createConditionalTypeNode(ts.factory.createTypeReferenceNode(ts.factory.cloneNode(name)), typeToTypeNodeHelper(type.checkType, context), ts.factory.createConditionalTypeNode(newTypeVariable, extendsTypeNode, trueTypeNode, falseTypeNode), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)); + let depth: number | undefined; + if (id) { + depth = context.symbolDepth!.get(id) || 0; + if (depth > 10) { + return createElidedInformationPlaceholder(context); } - const saveInferTypeParameters = context.inferTypeParameters; - context.inferTypeParameters = type.root.inferTypeParameters; - const extendsTypeNode = typeToTypeNodeHelper(type.extendsType, context); - context.inferTypeParameters = saveInferTypeParameters; - const trueTypeNode = typeToTypeNodeOrCircularityElision(getTrueTypeFromConditionalType(type)); - const falseTypeNode = typeToTypeNodeOrCircularityElision(getFalseTypeFromConditionalType(type)); - return ts.factory.createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); + context.symbolDepth!.set(id, depth + 1); } - - function typeToTypeNodeOrCircularityElision(type: ts.Type) { - if (type.flags & ts.TypeFlags.Union) { - if (context.visitedTypes?.has(getTypeId(type))) { - if (!(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier)) { - context.encounteredError = true; - context.tracker?.reportCyclicStructureError?.(); - } - return createElidedInformationPlaceholder(context); - } - return visitAndTransformType(type, type => typeToTypeNodeHelper(type, context)); + context.visitedTypes.add(typeId); + const startLength = context.approximateLength; + const result = transform(type); + const addedLength = context.approximateLength - startLength; + if (!context.reportedDiagnostic && !context.encounteredError) { + if (context.truncating) { + (result as any).truncating = true; } - return typeToTypeNodeHelper(type, context); + (result as any).addedLength = addedLength; + links?.serializedTypes?.set(key, result as ts.TypeNode as ts.TypeNode & { + truncating?: boolean; + addedLength: number; + }); + } + context.visitedTypes.delete(typeId); + if (id) { + context.symbolDepth!.set(id, depth!); } + return result; - function createMappedTypeNodeFromType(type: ts.MappedType) { - ts.Debug.assert(!!(type.flags & ts.TypeFlags.Object)); - const readonlyToken = type.declaration.readonlyToken ? ts.factory.createToken(type.declaration.readonlyToken.kind) as ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken : undefined; - const questionToken = type.declaration.questionToken ? ts.factory.createToken(type.declaration.questionToken.kind) as ts.QuestionToken | ts.PlusToken | ts.MinusToken : undefined; - let appropriateConstraintTypeNode: ts.TypeNode; - let newTypeVariable: ts.TypeReferenceNode | undefined; - 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` - if (!(getModifiersTypeFromMappedType(type).flags & ts.TypeFlags.TypeParameter) && context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { - const newParam = createTypeParameter(createSymbol(ts.SymbolFlags.TypeParameter, "T" as ts.__String)); - const name = typeParameterToName(newParam, context); - newTypeVariable = ts.factory.createTypeReferenceNode(name); - } - appropriateConstraintTypeNode = ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)); - } - else { - appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context); - } - const typeParameterNode = typeParameterToDeclarationWithConstraint(getTypeParameterFromMappedType(type), context, appropriateConstraintTypeNode); - const nameTypeNode = type.declaration.nameType ? typeToTypeNodeHelper(getNameTypeFromMappedType(type)!, context) : undefined; - const templateTypeNode = typeToTypeNodeHelper(removeMissingType(getTemplateTypeFromMappedType(type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), context); - const mappedTypeNode = ts.factory.createMappedTypeNode(readonlyToken, typeParameterNode, nameTypeNode, questionToken, templateTypeNode, /*members*/ undefined); - context.approximateLength += 10; - const result = ts.setEmitFlags(mappedTypeNode, ts.EmitFlags.SingleLine); - if (isMappedTypeWithKeyofConstraintDeclaration(type) && !(getModifiersTypeFromMappedType(type).flags & ts.TypeFlags.TypeParameter) && context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { - // homomorphic mapped type with a non-homomorphic naive inlining - // wrap it with a conditional like `SomeModifiersType extends infer U ? {..the mapped type...} : never` to ensure the resulting - // type stays homomorphic - return ts.factory.createConditionalTypeNode(typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context), ts.factory.createInferTypeNode(ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, ts.factory.cloneNode(newTypeVariable!.typeName) as ts.Identifier)), result, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)); + function deepCloneOrReuseNode(node: ts.Node): ts.Node { + if (!ts.nodeIsSynthesized(node) && ts.getParseTreeNode(node) === node) { + return node; } - return result; + return ts.setTextRange(ts.factory.cloneNode(ts.visitEachChild(node, deepCloneOrReuseNode, ts.nullTransformationContext, deepCloneOrReuseNodes)), node); } - function createAnonymousTypeNode(type: ts.ObjectType): ts.TypeNode { - const typeId = type.id; - const symbol = type.symbol; - if (symbol) { - const isInstanceType = isClassInstanceSide(type) ? ts.SymbolFlags.Type : ts.SymbolFlags.Value; - if (isJSConstructor(symbol.valueDeclaration)) { - // Instance and static types share the same symbol; only add 'typeof' for the static side. - return symbolToTypeNode(symbol, context, isInstanceType); - } - // Always use 'typeof T' for type of class, enum, and module objects - else if (symbol.flags & ts.SymbolFlags.Class - && !getBaseTypeVariableOfClass(symbol) - && !(symbol.valueDeclaration && symbol.valueDeclaration.kind === ts.SyntaxKind.ClassExpression && context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) || - symbol.flags & (ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule) || - shouldWriteTypeOfFunctionSymbol()) { - return symbolToTypeNode(symbol, context, isInstanceType); - } - else if (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, ts.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 & ts.SymbolFlags.Method) && // typeof static method - ts.some(symbol.declarations, declaration => ts.isStatic(declaration)); - const isNonLocalFunctionSymbol = !!(symbol.flags & ts.SymbolFlags.Function) && - (symbol.parent || // is exported function symbol - ts.forEach(symbol.declarations, declaration => declaration.parent.kind === ts.SyntaxKind.SourceFile || declaration.parent.kind === ts.SyntaxKind.ModuleBlock)); - if (isStaticMethodSymbol || isNonLocalFunctionSymbol) { - // typeof is allowed only for static/non local functions - return (!!(context.flags & ts.NodeBuilderFlags.UseTypeOfFunction) || (context.visitedTypes?.has(typeId))) && // it is type of the symbol uses itself recursively - (!(context.flags & ts.NodeBuilderFlags.UseStructuralFallback) || isValueSymbolAccessible(symbol, context.enclosingDeclaration)); // And the build is going to succeed without visibility error or there is no structural fallback allowed - } + function deepCloneOrReuseNodes(nodes: ts.NodeArray, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray; + function deepCloneOrReuseNodes(nodes: ts.NodeArray | undefined, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray | undefined; + function deepCloneOrReuseNodes(nodes: ts.NodeArray | undefined, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray | undefined { + if (nodes && nodes.length === 0) { + // Ensure we explicitly make a copy of an empty array; visitNodes will not do this unless the array has elements, + // which can lead to us reusing the same empty NodeArray more than once within the same AST during type noding. + return ts.setTextRange(ts.factory.createNodeArray(/*nodes*/ undefined, nodes.hasTrailingComma), nodes); } + return ts.visitNodes(nodes, visitor, test, start, count); } + } - function visitAndTransformType(type: ts.Type, transform: (type: ts.Type) => T) { - const typeId = type.id; - const isConstructorObject = ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && type.symbol && type.symbol.flags & ts.SymbolFlags.Class; - const id = ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).node ? "N" + getNodeId((type as ts.TypeReference).node!) : - type.flags & ts.TypeFlags.Conditional ? "N" + getNodeId((type as ts.ConditionalType).root.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 = new ts.Set(); - } - if (id && !context.symbolDepth) { - context.symbolDepth = new ts.Map(); - } + function createTypeNodeFromObjectType(type: ts.ObjectType): ts.TypeNode { + if (isGenericMappedType(type) || (type as ts.MappedType).containsError) { + return createMappedTypeNodeFromType(type as ts.MappedType); + } - const links = context.enclosingDeclaration && getNodeLinks(context.enclosingDeclaration); - const key = `${getTypeId(type)}|${context.flags}`; - if (links) { - links.serializedTypes ||= new ts.Map(); - } - const cachedResult = links?.serializedTypes?.get(key); - if (cachedResult) { - if (cachedResult.truncating) { - context.truncating = true; - } - context.approximateLength += cachedResult.addedLength; - return deepCloneOrReuseNode(cachedResult) as ts.TypeNode as T; + const resolved = resolveStructuredTypeMembers(type); + if (!resolved.properties.length && !resolved.indexInfos.length) { + if (!resolved.callSignatures.length && !resolved.constructSignatures.length) { + context.approximateLength += 2; + return ts.setEmitFlags(ts.factory.createTypeLiteralNode(/*members*/ undefined), ts.EmitFlags.SingleLine); } - 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.add(typeId); - const startLength = context.approximateLength; - const result = transform(type); - const addedLength = context.approximateLength - startLength; - if (!context.reportedDiagnostic && !context.encounteredError) { - if (context.truncating) { - (result as any).truncating = true; - } - (result as any).addedLength = addedLength; - links?.serializedTypes?.set(key, result as ts.TypeNode as ts.TypeNode & { - truncating?: boolean; - addedLength: number; - }); - } - context.visitedTypes.delete(typeId); - if (id) { - context.symbolDepth!.set(id, depth!); - } - return result; + if (resolved.callSignatures.length === 1 && !resolved.constructSignatures.length) { + const signature = resolved.callSignatures[0]; + const signatureNode = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.FunctionType, context) as ts.FunctionTypeNode; + return signatureNode; - function deepCloneOrReuseNode(node: ts.Node): ts.Node { - if (!ts.nodeIsSynthesized(node) && ts.getParseTreeNode(node) === node) { - return node; - } - return ts.setTextRange(ts.factory.cloneNode(ts.visitEachChild(node, deepCloneOrReuseNode, ts.nullTransformationContext, deepCloneOrReuseNodes)), node); } - function deepCloneOrReuseNodes(nodes: ts.NodeArray, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray; - function deepCloneOrReuseNodes(nodes: ts.NodeArray | undefined, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray | undefined; - function deepCloneOrReuseNodes(nodes: ts.NodeArray | undefined, visitor: ts.Visitor | undefined, test?: (node: ts.Node) => boolean, start?: number, count?: number): ts.NodeArray | undefined { - if (nodes && nodes.length === 0) { - // Ensure we explicitly make a copy of an empty array; visitNodes will not do this unless the array has elements, - // which can lead to us reusing the same empty NodeArray more than once within the same AST during type noding. - return ts.setTextRange(ts.factory.createNodeArray(/*nodes*/ undefined, nodes.hasTrailingComma), nodes); - } - return ts.visitNodes(nodes, visitor, test, start, count); + if (resolved.constructSignatures.length === 1 && !resolved.callSignatures.length) { + const signature = resolved.constructSignatures[0]; + const signatureNode = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.ConstructorType, context) as ts.ConstructorTypeNode; + return signatureNode; } } - function createTypeNodeFromObjectType(type: ts.ObjectType): ts.TypeNode { - if (isGenericMappedType(type) || (type as ts.MappedType).containsError) { - return createMappedTypeNodeFromType(type as ts.MappedType); + const abstractSignatures = ts.filter(resolved.constructSignatures, signature => !!(signature.flags & ts.SignatureFlags.Abstract)); + if (ts.some(abstractSignatures)) { + const types = ts.map(abstractSignatures, getOrCreateTypeFromSignature); + // count the number of type elements excluding abstract constructors + const typeElementCount = resolved.callSignatures.length + + (resolved.constructSignatures.length - abstractSignatures.length) + + resolved.indexInfos.length + + // exclude `prototype` when writing a class expression as a type literal, as per + // the logic in `createTypeNodesFromResolvedType`. + (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral ? + ts.countWhere(resolved.properties, p => !(p.flags & ts.SymbolFlags.Prototype)) : + ts.length(resolved.properties)); + // don't include an empty object literal if there were no other static-side + // properties to write, i.e. `abstract class C { }` becomes `abstract new () => {}` + // and not `(abstract new () => {}) & {}` + if (typeElementCount) { + // create a copy of the object type without any abstract construct signatures. + types.push(getResolvedTypeWithoutAbstractConstructSignatures(resolved)); } + return typeToTypeNodeHelper(getIntersectionType(types), context); + } - const resolved = resolveStructuredTypeMembers(type); - if (!resolved.properties.length && !resolved.indexInfos.length) { - if (!resolved.callSignatures.length && !resolved.constructSignatures.length) { - context.approximateLength += 2; - return ts.setEmitFlags(ts.factory.createTypeLiteralNode(/*members*/ undefined), ts.EmitFlags.SingleLine); - } - - if (resolved.callSignatures.length === 1 && !resolved.constructSignatures.length) { - const signature = resolved.callSignatures[0]; - const signatureNode = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.FunctionType, context) as ts.FunctionTypeNode; - return signatureNode; - - } - - if (resolved.constructSignatures.length === 1 && !resolved.callSignatures.length) { - const signature = resolved.constructSignatures[0]; - const signatureNode = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.ConstructorType, context) as ts.ConstructorTypeNode; - return signatureNode; - } - } + const savedFlags = context.flags; + context.flags |= ts.NodeBuilderFlags.InObjectTypeLiteral; + const members = createTypeNodesFromResolvedType(resolved); + context.flags = savedFlags; + const typeLiteralNode = ts.factory.createTypeLiteralNode(members); + context.approximateLength += 2; + ts.setEmitFlags(typeLiteralNode, (context.flags & ts.NodeBuilderFlags.MultilineObjectLiterals) ? 0 : ts.EmitFlags.SingleLine); + return typeLiteralNode; + } - const abstractSignatures = ts.filter(resolved.constructSignatures, signature => !!(signature.flags & ts.SignatureFlags.Abstract)); - if (ts.some(abstractSignatures)) { - const types = ts.map(abstractSignatures, getOrCreateTypeFromSignature); - // count the number of type elements excluding abstract constructors - const typeElementCount = resolved.callSignatures.length + - (resolved.constructSignatures.length - abstractSignatures.length) + - resolved.indexInfos.length + - // exclude `prototype` when writing a class expression as a type literal, as per - // the logic in `createTypeNodesFromResolvedType`. - (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral ? - ts.countWhere(resolved.properties, p => !(p.flags & ts.SymbolFlags.Prototype)) : - ts.length(resolved.properties)); - // don't include an empty object literal if there were no other static-side - // properties to write, i.e. `abstract class C { }` becomes `abstract new () => {}` - // and not `(abstract new () => {}) & {}` - if (typeElementCount) { - // create a copy of the object type without any abstract construct signatures. - types.push(getResolvedTypeWithoutAbstractConstructSignatures(resolved)); - } - return typeToTypeNodeHelper(getIntersectionType(types), context); + function typeReferenceToTypeNode(type: ts.TypeReference) { + let typeArguments: readonly ts.Type[] = getTypeArguments(type); + if (type.target === globalArrayType || type.target === globalReadonlyArrayType) { + if (context.flags & ts.NodeBuilderFlags.WriteArrayAsGenericType) { + const typeArgumentNode = typeToTypeNodeHelper(typeArguments[0], context); + return ts.factory.createTypeReferenceNode(type.target === globalArrayType ? "Array" : "ReadonlyArray", [typeArgumentNode]); } - - const savedFlags = context.flags; - context.flags |= ts.NodeBuilderFlags.InObjectTypeLiteral; - const members = createTypeNodesFromResolvedType(resolved); - context.flags = savedFlags; - const typeLiteralNode = ts.factory.createTypeLiteralNode(members); - context.approximateLength += 2; - ts.setEmitFlags(typeLiteralNode, (context.flags & ts.NodeBuilderFlags.MultilineObjectLiterals) ? 0 : ts.EmitFlags.SingleLine); - return typeLiteralNode; + const elementType = typeToTypeNodeHelper(typeArguments[0], context); + const arrayType = ts.factory.createArrayTypeNode(elementType); + return type.target === globalArrayType ? arrayType : ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, arrayType); } - - function typeReferenceToTypeNode(type: ts.TypeReference) { - let typeArguments: readonly ts.Type[] = getTypeArguments(type); - if (type.target === globalArrayType || type.target === globalReadonlyArrayType) { - if (context.flags & ts.NodeBuilderFlags.WriteArrayAsGenericType) { - const typeArgumentNode = typeToTypeNodeHelper(typeArguments[0], context); - return ts.factory.createTypeReferenceNode(type.target === globalArrayType ? "Array" : "ReadonlyArray", [typeArgumentNode]); - } - const elementType = typeToTypeNodeHelper(typeArguments[0], context); - const arrayType = ts.factory.createArrayTypeNode(elementType); - return type.target === globalArrayType ? arrayType : ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, arrayType); - } - else if (type.target.objectFlags & ts.ObjectFlags.Tuple) { - typeArguments = ts.sameMap(typeArguments, (t, i) => removeMissingType(t, !!((type.target as ts.TupleType).elementFlags[i] & ts.ElementFlags.Optional))); - if (typeArguments.length > 0) { - const arity = getTypeReferenceArity(type); - const tupleConstituentNodes = mapToTypeNodes(typeArguments.slice(0, arity), context); - if (tupleConstituentNodes) { - if ((type.target as ts.TupleType).labeledElementDeclarations) { - for (let i = 0; i < tupleConstituentNodes.length; i++) { - const flags = (type.target as ts.TupleType).elementFlags[i]; - tupleConstituentNodes[i] = ts.factory.createNamedTupleMember(flags & ts.ElementFlags.Variable ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined, ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(getTupleElementLabel((type.target as ts.TupleType).labeledElementDeclarations![i]))), flags & ts.ElementFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, flags & ts.ElementFlags.Rest ? ts.factory.createArrayTypeNode(tupleConstituentNodes[i]) : - tupleConstituentNodes[i]); - } + else if (type.target.objectFlags & ts.ObjectFlags.Tuple) { + typeArguments = ts.sameMap(typeArguments, (t, i) => removeMissingType(t, !!((type.target as ts.TupleType).elementFlags[i] & ts.ElementFlags.Optional))); + if (typeArguments.length > 0) { + const arity = getTypeReferenceArity(type); + const tupleConstituentNodes = mapToTypeNodes(typeArguments.slice(0, arity), context); + if (tupleConstituentNodes) { + if ((type.target as ts.TupleType).labeledElementDeclarations) { + for (let i = 0; i < tupleConstituentNodes.length; i++) { + const flags = (type.target as ts.TupleType).elementFlags[i]; + tupleConstituentNodes[i] = ts.factory.createNamedTupleMember(flags & ts.ElementFlags.Variable ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined, ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(getTupleElementLabel((type.target as ts.TupleType).labeledElementDeclarations![i]))), flags & ts.ElementFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, flags & ts.ElementFlags.Rest ? ts.factory.createArrayTypeNode(tupleConstituentNodes[i]) : + tupleConstituentNodes[i]); } - else { - for (let i = 0; i < Math.min(arity, tupleConstituentNodes.length); i++) { - const flags = (type.target as ts.TupleType).elementFlags[i]; - tupleConstituentNodes[i] = - flags & ts.ElementFlags.Variable ? ts.factory.createRestTypeNode(flags & ts.ElementFlags.Rest ? ts.factory.createArrayTypeNode(tupleConstituentNodes[i]) : tupleConstituentNodes[i]) : - flags & ts.ElementFlags.Optional ? ts.factory.createOptionalTypeNode(tupleConstituentNodes[i]) : - tupleConstituentNodes[i]; - } + } + else { + for (let i = 0; i < Math.min(arity, tupleConstituentNodes.length); i++) { + const flags = (type.target as ts.TupleType).elementFlags[i]; + tupleConstituentNodes[i] = + flags & ts.ElementFlags.Variable ? ts.factory.createRestTypeNode(flags & ts.ElementFlags.Rest ? ts.factory.createArrayTypeNode(tupleConstituentNodes[i]) : tupleConstituentNodes[i]) : + flags & ts.ElementFlags.Optional ? ts.factory.createOptionalTypeNode(tupleConstituentNodes[i]) : + tupleConstituentNodes[i]; } - const tupleTypeNode = ts.setEmitFlags(ts.factory.createTupleTypeNode(tupleConstituentNodes), ts.EmitFlags.SingleLine); - return (type.target as ts.TupleType).readonly ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; } - } - if (context.encounteredError || (context.flags & ts.NodeBuilderFlags.AllowEmptyTuple)) { - const tupleTypeNode = ts.setEmitFlags(ts.factory.createTupleTypeNode([]), ts.EmitFlags.SingleLine); + const tupleTypeNode = ts.setEmitFlags(ts.factory.createTupleTypeNode(tupleConstituentNodes), ts.EmitFlags.SingleLine); return (type.target as ts.TupleType).readonly ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; } - context.encounteredError = true; - return undefined!; // TODO: GH#18217 } - else if (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral && - type.symbol.valueDeclaration && - ts.isClassLike(type.symbol.valueDeclaration) && - !isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { - return createAnonymousTypeNode(type); + if (context.encounteredError || (context.flags & ts.NodeBuilderFlags.AllowEmptyTuple)) { + const tupleTypeNode = ts.setEmitFlags(ts.factory.createTupleTypeNode([]), ts.EmitFlags.SingleLine); + return (type.target as ts.TupleType).readonly ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; } - else { - const outerTypeParameters = type.target.outerTypeParameters; - let i = 0; - let resultType: ts.TypeReferenceNode | ts.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 (!ts.rangeEquals(outerTypeParameters, typeArguments, start, i)) { - const typeArgumentSlice = mapToTypeNodes(typeArguments.slice(start, i), context); - const flags = context.flags; - context.flags |= ts.NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; - const ref = symbolToTypeNode(parent, context, ts.SymbolFlags.Type, typeArgumentSlice) as ts.TypeReferenceNode | ts.ImportTypeNode; - context.flags = flags; - resultType = !resultType ? ref : appendReferenceToType(resultType, ref as ts.TypeReferenceNode); - } - } + context.encounteredError = true; + return undefined!; // TODO: GH#18217 + } + else if (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral && + type.symbol.valueDeclaration && + ts.isClassLike(type.symbol.valueDeclaration) && + !isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + return createAnonymousTypeNode(type); + } + else { + const outerTypeParameters = type.target.outerTypeParameters; + let i = 0; + let resultType: ts.TypeReferenceNode | ts.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 (!ts.rangeEquals(outerTypeParameters, typeArguments, start, i)) { + const typeArgumentSlice = mapToTypeNodes(typeArguments.slice(start, i), context); + const flags = context.flags; + context.flags |= ts.NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; + const ref = symbolToTypeNode(parent, context, ts.SymbolFlags.Type, typeArgumentSlice) as ts.TypeReferenceNode | ts.ImportTypeNode; + context.flags = flags; + resultType = !resultType ? ref : appendReferenceToType(resultType, ref as ts.TypeReferenceNode); + } + } + } + let typeArgumentNodes: readonly ts.TypeNode[] | undefined; + if (typeArguments.length > 0) { + const typeParameterCount = (type.target.typeParameters || ts.emptyArray).length; + typeArgumentNodes = mapToTypeNodes(typeArguments.slice(i, typeParameterCount), context); + } + const flags = context.flags; + context.flags |= ts.NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; + const finalRef = symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Type, typeArgumentNodes); + context.flags = flags; + return !resultType ? finalRef : appendReferenceToType(resultType, finalRef as ts.TypeReferenceNode); + } + } + + + function appendReferenceToType(root: ts.TypeReferenceNode | ts.ImportTypeNode, ref: ts.TypeReferenceNode): ts.TypeReferenceNode | ts.ImportTypeNode { + if (ts.isImportTypeNode(root)) { + // first shift type arguments + let typeArguments = root.typeArguments; + let qualifier = root.qualifier; + if (qualifier) { + if (ts.isIdentifier(qualifier)) { + qualifier = ts.factory.updateIdentifier(qualifier, typeArguments); } - let typeArgumentNodes: readonly ts.TypeNode[] | undefined; - if (typeArguments.length > 0) { - const typeParameterCount = (type.target.typeParameters || ts.emptyArray).length; - typeArgumentNodes = mapToTypeNodes(typeArguments.slice(i, typeParameterCount), context); + else { + qualifier = ts.factory.updateQualifiedName(qualifier, qualifier.left, ts.factory.updateIdentifier(qualifier.right, typeArguments)); } - const flags = context.flags; - context.flags |= ts.NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; - const finalRef = symbolToTypeNode(type.symbol, context, ts.SymbolFlags.Type, typeArgumentNodes); - context.flags = flags; - return !resultType ? finalRef : appendReferenceToType(resultType, finalRef as ts.TypeReferenceNode); } + typeArguments = ref.typeArguments; + // then move qualifiers + const ids = getAccessStack(ref); + for (const id of ids) { + qualifier = qualifier ? ts.factory.createQualifiedName(qualifier, id) : id; + } + return ts.factory.updateImportTypeNode(root, root.argument, qualifier, typeArguments, root.isTypeOf); } - - - function appendReferenceToType(root: ts.TypeReferenceNode | ts.ImportTypeNode, ref: ts.TypeReferenceNode): ts.TypeReferenceNode | ts.ImportTypeNode { - if (ts.isImportTypeNode(root)) { - // first shift type arguments - let typeArguments = root.typeArguments; - let qualifier = root.qualifier; - if (qualifier) { - if (ts.isIdentifier(qualifier)) { - qualifier = ts.factory.updateIdentifier(qualifier, typeArguments); - } - else { - qualifier = ts.factory.updateQualifiedName(qualifier, qualifier.left, ts.factory.updateIdentifier(qualifier.right, typeArguments)); - } - } - typeArguments = ref.typeArguments; - // then move qualifiers - const ids = getAccessStack(ref); - for (const id of ids) { - qualifier = qualifier ? ts.factory.createQualifiedName(qualifier, id) : id; - } - return ts.factory.updateImportTypeNode(root, root.argument, qualifier, typeArguments, root.isTypeOf); + else { + // first shift type arguments + let typeArguments = root.typeArguments; + let typeName = root.typeName; + if (ts.isIdentifier(typeName)) { + typeName = ts.factory.updateIdentifier(typeName, typeArguments); } else { - // first shift type arguments - let typeArguments = root.typeArguments; - let typeName = root.typeName; - if (ts.isIdentifier(typeName)) { - typeName = ts.factory.updateIdentifier(typeName, typeArguments); - } - else { - typeName = ts.factory.updateQualifiedName(typeName, typeName.left, ts.factory.updateIdentifier(typeName.right, typeArguments)); - } - typeArguments = ref.typeArguments; - // then move qualifiers - const ids = getAccessStack(ref); - for (const id of ids) { - typeName = ts.factory.createQualifiedName(typeName, id); - } - return ts.factory.updateTypeReferenceNode(root, typeName, typeArguments); + typeName = ts.factory.updateQualifiedName(typeName, typeName.left, ts.factory.updateIdentifier(typeName.right, typeArguments)); } + typeArguments = ref.typeArguments; + // then move qualifiers + const ids = getAccessStack(ref); + for (const id of ids) { + typeName = ts.factory.createQualifiedName(typeName, id); + } + return ts.factory.updateTypeReferenceNode(root, typeName, typeArguments); } + } - function getAccessStack(ref: ts.TypeReferenceNode): ts.Identifier[] { - let state = ref.typeName; - const ids = []; - while (!ts.isIdentifier(state)) { - ids.unshift(state.right); - state = state.left; - } - ids.unshift(state); - return ids; + function getAccessStack(ref: ts.TypeReferenceNode): ts.Identifier[] { + let state = ref.typeName; + const ids = []; + while (!ts.isIdentifier(state)) { + ids.unshift(state.right); + state = state.left; } + ids.unshift(state); + return ids; + } - function createTypeNodesFromResolvedType(resolvedType: ts.ResolvedType): ts.TypeElement[] | undefined { - if (checkTruncationLength(context)) { - return [ts.factory.createPropertySignature(/*modifiers*/ undefined, "...", /*questionToken*/ undefined, /*type*/ undefined)]; - } - const typeElements: ts.TypeElement[] = []; - for (const signature of resolvedType.callSignatures) { - typeElements.push(signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.CallSignature, context) as ts.CallSignatureDeclaration); - } - for (const signature of resolvedType.constructSignatures) { - if (signature.flags & ts.SignatureFlags.Abstract) - continue; - typeElements.push(signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.ConstructSignature, context) as ts.ConstructSignatureDeclaration); - } - for (const info of resolvedType.indexInfos) { - typeElements.push(indexInfoToIndexSignatureDeclarationHelper(info, context, resolvedType.objectFlags & ts.ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined)); - } + function createTypeNodesFromResolvedType(resolvedType: ts.ResolvedType): ts.TypeElement[] | undefined { + if (checkTruncationLength(context)) { + return [ts.factory.createPropertySignature(/*modifiers*/ undefined, "...", /*questionToken*/ undefined, /*type*/ undefined)]; + } + const typeElements: ts.TypeElement[] = []; + for (const signature of resolvedType.callSignatures) { + typeElements.push(signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.CallSignature, context) as ts.CallSignatureDeclaration); + } + for (const signature of resolvedType.constructSignatures) { + if (signature.flags & ts.SignatureFlags.Abstract) + continue; + typeElements.push(signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.ConstructSignature, context) as ts.ConstructSignatureDeclaration); + } + for (const info of resolvedType.indexInfos) { + typeElements.push(indexInfoToIndexSignatureDeclarationHelper(info, context, resolvedType.objectFlags & ts.ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined)); + } - const properties = resolvedType.properties; - if (!properties) { - return typeElements; - } + const properties = resolvedType.properties; + if (!properties) { + return typeElements; + } - let i = 0; - for (const propertySymbol of properties) { - i++; - if (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) { - if (propertySymbol.flags & ts.SymbolFlags.Prototype) { - continue; - } - if (ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected) && context.tracker.reportPrivateInBaseOfClassExpression) { - context.tracker.reportPrivateInBaseOfClassExpression(ts.unescapeLeadingUnderscores(propertySymbol.escapedName)); - } + let i = 0; + for (const propertySymbol of properties) { + i++; + if (context.flags & ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) { + if (propertySymbol.flags & ts.SymbolFlags.Prototype) { + continue; } - if (checkTruncationLength(context) && (i + 2 < properties.length - 1)) { - typeElements.push(ts.factory.createPropertySignature(/*modifiers*/ undefined, `... ${properties.length - i} more ...`, /*questionToken*/ undefined, /*type*/ undefined)); - addPropertyToElementList(properties[properties.length - 1], context, typeElements); - break; + if (ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected) && context.tracker.reportPrivateInBaseOfClassExpression) { + context.tracker.reportPrivateInBaseOfClassExpression(ts.unescapeLeadingUnderscores(propertySymbol.escapedName)); } - addPropertyToElementList(propertySymbol, context, typeElements); - } - return typeElements.length ? typeElements : undefined; + if (checkTruncationLength(context) && (i + 2 < properties.length - 1)) { + typeElements.push(ts.factory.createPropertySignature(/*modifiers*/ undefined, `... ${properties.length - i} more ...`, /*questionToken*/ undefined, /*type*/ 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 & ts.NodeBuilderFlags.NoTruncation)) { - return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("..."), /*typeArguments*/ undefined); - } - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + function createElidedInformationPlaceholder(context: NodeBuilderContext) { + context.approximateLength += 3; + if (!(context.flags & ts.NodeBuilderFlags.NoTruncation)) { + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("..."), /*typeArguments*/ undefined); } + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } - function shouldUsePlaceholderForProperty(propertySymbol: ts.Symbol, context: NodeBuilderContext) { - // Use placeholders for reverse mapped types we've either already descended into, or which - // are nested reverse mappings within a mapping over a non-anonymous type. The later is a restriction mostly just to - // reduce the blowup in printback size from doing, eg, a deep reverse mapping over `Window`. - // Since anonymous types usually come from expressions, this allows us to preserve the output - // for deep mappings which likely come from expressions, while truncating those parts which - // come from mappings over library functions. - return !!(ts.getCheckFlags(propertySymbol) & ts.CheckFlags.ReverseMapped) - && (ts.contains(context.reverseMappedStack, propertySymbol as ts.ReverseMappedSymbol) - || (context.reverseMappedStack?.[0] - && !(ts.getObjectFlags(ts.last(context.reverseMappedStack).propertyType) & ts.ObjectFlags.Anonymous))); - } - - function addPropertyToElementList(propertySymbol: ts.Symbol, context: NodeBuilderContext, typeElements: ts.TypeElement[]) { - const propertyIsReverseMapped = !!(ts.getCheckFlags(propertySymbol) & ts.CheckFlags.ReverseMapped); - const propertyType = shouldUsePlaceholderForProperty(propertySymbol, context) ? - anyType : getNonMissingTypeOfSymbol(propertySymbol); - const saveEnclosingDeclaration = context.enclosingDeclaration; - context.enclosingDeclaration = undefined; - if (context.tracker.trackSymbol && ts.getCheckFlags(propertySymbol) & ts.CheckFlags.Late && isLateBoundName(propertySymbol.escapedName)) { - if (propertySymbol.declarations) { - const decl = ts.first(propertySymbol.declarations); - if (hasLateBindableName(decl)) { - if (ts.isBinaryExpression(decl)) { - const name = ts.getNameOfDeclaration(decl); - if (name && ts.isElementAccessExpression(name) && ts.isPropertyAccessEntityNameExpression(name.argumentExpression)) { - trackComputedName(name.argumentExpression, saveEnclosingDeclaration, context); - } - } - else { - trackComputedName(decl.name.expression, saveEnclosingDeclaration, context); + function shouldUsePlaceholderForProperty(propertySymbol: ts.Symbol, context: NodeBuilderContext) { + // Use placeholders for reverse mapped types we've either already descended into, or which + // are nested reverse mappings within a mapping over a non-anonymous type. The later is a restriction mostly just to + // reduce the blowup in printback size from doing, eg, a deep reverse mapping over `Window`. + // Since anonymous types usually come from expressions, this allows us to preserve the output + // for deep mappings which likely come from expressions, while truncating those parts which + // come from mappings over library functions. + return !!(ts.getCheckFlags(propertySymbol) & ts.CheckFlags.ReverseMapped) + && (ts.contains(context.reverseMappedStack, propertySymbol as ts.ReverseMappedSymbol) + || (context.reverseMappedStack?.[0] + && !(ts.getObjectFlags(ts.last(context.reverseMappedStack).propertyType) & ts.ObjectFlags.Anonymous))); + } + + function addPropertyToElementList(propertySymbol: ts.Symbol, context: NodeBuilderContext, typeElements: ts.TypeElement[]) { + const propertyIsReverseMapped = !!(ts.getCheckFlags(propertySymbol) & ts.CheckFlags.ReverseMapped); + const propertyType = shouldUsePlaceholderForProperty(propertySymbol, context) ? + anyType : getNonMissingTypeOfSymbol(propertySymbol); + const saveEnclosingDeclaration = context.enclosingDeclaration; + context.enclosingDeclaration = undefined; + if (context.tracker.trackSymbol && ts.getCheckFlags(propertySymbol) & ts.CheckFlags.Late && isLateBoundName(propertySymbol.escapedName)) { + if (propertySymbol.declarations) { + const decl = ts.first(propertySymbol.declarations); + if (hasLateBindableName(decl)) { + if (ts.isBinaryExpression(decl)) { + const name = ts.getNameOfDeclaration(decl); + if (name && ts.isElementAccessExpression(name) && ts.isPropertyAccessEntityNameExpression(name.argumentExpression)) { + trackComputedName(name.argumentExpression, saveEnclosingDeclaration, context); } } - } - else if (context.tracker?.reportNonSerializableProperty) { - context.tracker.reportNonSerializableProperty(symbolToString(propertySymbol)); + else { + trackComputedName(decl.name.expression, saveEnclosingDeclaration, context); + } } } - context.enclosingDeclaration = propertySymbol.valueDeclaration || propertySymbol.declarations?.[0] || saveEnclosingDeclaration; - const propertyName = getPropertyNameNodeForSymbol(propertySymbol, context); - context.enclosingDeclaration = saveEnclosingDeclaration; - context.approximateLength += (ts.symbolName(propertySymbol).length + 1); - const optionalToken = propertySymbol.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined; - if (propertySymbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method) && !getPropertiesOfObjectType(propertyType).length && !isReadonlySymbol(propertySymbol)) { - const signatures = getSignaturesOfType(filterType(propertyType, t => !(t.flags & ts.TypeFlags.Undefined)), ts.SignatureKind.Call); - for (const signature of signatures) { - const methodDeclaration = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.MethodSignature, context, { name: propertyName, questionToken: optionalToken }) as ts.MethodSignature; - typeElements.push(preserveCommentsOn(methodDeclaration)); - } + else if (context.tracker?.reportNonSerializableProperty) { + context.tracker.reportNonSerializableProperty(symbolToString(propertySymbol)); + } + } + context.enclosingDeclaration = propertySymbol.valueDeclaration || propertySymbol.declarations?.[0] || saveEnclosingDeclaration; + const propertyName = getPropertyNameNodeForSymbol(propertySymbol, context); + context.enclosingDeclaration = saveEnclosingDeclaration; + context.approximateLength += (ts.symbolName(propertySymbol).length + 1); + const optionalToken = propertySymbol.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined; + if (propertySymbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method) && !getPropertiesOfObjectType(propertyType).length && !isReadonlySymbol(propertySymbol)) { + const signatures = getSignaturesOfType(filterType(propertyType, t => !(t.flags & ts.TypeFlags.Undefined)), ts.SignatureKind.Call); + for (const signature of signatures) { + const methodDeclaration = signatureToSignatureDeclarationHelper(signature, ts.SyntaxKind.MethodSignature, context, { name: propertyName, questionToken: optionalToken }) as ts.MethodSignature; + typeElements.push(preserveCommentsOn(methodDeclaration)); + } + } + else { + let propertyTypeNode: ts.TypeNode; + if (shouldUsePlaceholderForProperty(propertySymbol, context)) { + propertyTypeNode = createElidedInformationPlaceholder(context); } else { - let propertyTypeNode: ts.TypeNode; - if (shouldUsePlaceholderForProperty(propertySymbol, context)) { - propertyTypeNode = createElidedInformationPlaceholder(context); - } - else { - if (propertyIsReverseMapped) { - context.reverseMappedStack ||= []; - context.reverseMappedStack.push(propertySymbol as ts.ReverseMappedSymbol); - } - propertyTypeNode = propertyType ? serializeTypeForDeclaration(context, propertyType, propertySymbol, saveEnclosingDeclaration) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - if (propertyIsReverseMapped) { - context.reverseMappedStack!.pop(); - } + if (propertyIsReverseMapped) { + context.reverseMappedStack ||= []; + context.reverseMappedStack.push(propertySymbol as ts.ReverseMappedSymbol); } - - const modifiers = isReadonlySymbol(propertySymbol) ? [ts.factory.createToken(ts.SyntaxKind.ReadonlyKeyword)] : undefined; - if (modifiers) { - context.approximateLength += 9; + propertyTypeNode = propertyType ? serializeTypeForDeclaration(context, propertyType, propertySymbol, saveEnclosingDeclaration) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + if (propertyIsReverseMapped) { + context.reverseMappedStack!.pop(); } - const propertySignature = ts.factory.createPropertySignature(modifiers, propertyName, optionalToken, propertyTypeNode); + } - typeElements.push(preserveCommentsOn(propertySignature)); + const modifiers = isReadonlySymbol(propertySymbol) ? [ts.factory.createToken(ts.SyntaxKind.ReadonlyKeyword)] : undefined; + if (modifiers) { + context.approximateLength += 9; } + const propertySignature = ts.factory.createPropertySignature(modifiers, propertyName, optionalToken, propertyTypeNode); - function preserveCommentsOn(node: T) { - if (ts.some(propertySymbol.declarations, d => d.kind === ts.SyntaxKind.JSDocPropertyTag)) { - const d = propertySymbol.declarations?.find(d => d.kind === ts.SyntaxKind.JSDocPropertyTag)! as ts.JSDocPropertyTag; - const commentText = ts.getTextOfJSDocComment(d.comment); - if (commentText) { - ts.setSyntheticLeadingComments(node, [{ kind: ts.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 - ts.setCommentRange(node, propertySymbol.valueDeclaration); + typeElements.push(preserveCommentsOn(propertySignature)); + } + + function preserveCommentsOn(node: T) { + if (ts.some(propertySymbol.declarations, d => d.kind === ts.SyntaxKind.JSDocPropertyTag)) { + const d = propertySymbol.declarations?.find(d => d.kind === ts.SyntaxKind.JSDocPropertyTag)! as ts.JSDocPropertyTag; + const commentText = ts.getTextOfJSDocComment(d.comment); + if (commentText) { + ts.setSyntheticLeadingComments(node, [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]); } - return node; } + else if (propertySymbol.valueDeclaration) { + // Copy comments to node for declaration emit + ts.setCommentRange(node, propertySymbol.valueDeclaration); + } + return node; } + } - function mapToTypeNodes(types: readonly ts.Type[] | undefined, context: NodeBuilderContext, isBareList?: boolean): ts.TypeNode[] | undefined { - if (ts.some(types)) { - if (checkTruncationLength(context)) { - if (!isBareList) { - return [ts.factory.createTypeReferenceNode("...", /*typeArguments*/ undefined)]; - } - else if (types.length > 2) { - return [ - typeToTypeNodeHelper(types[0], context), - ts.factory.createTypeReferenceNode(`... ${types.length - 2} more ...`, /*typeArguments*/ undefined), - typeToTypeNodeHelper(types[types.length - 1], context) - ]; - } + function mapToTypeNodes(types: readonly ts.Type[] | undefined, context: NodeBuilderContext, isBareList?: boolean): ts.TypeNode[] | undefined { + if (ts.some(types)) { + if (checkTruncationLength(context)) { + if (!isBareList) { + return [ts.factory.createTypeReferenceNode("...", /*typeArguments*/ undefined)]; } - const mayHaveNameCollisions = !(context.flags & ts.NodeBuilderFlags.UseFullyQualifiedType); - /** Map from type reference identifier text to [type, index in `result` where the type node is] */ - const seenNames = mayHaveNameCollisions ? ts.createUnderscoreEscapedMultiMap<[ - ts.Type, - number - ]>() : undefined; - const result: ts.TypeNode[] = []; - let i = 0; - for (const type of types) { - i++; - if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { - result.push(ts.factory.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); + else if (types.length > 2) { + return [ + typeToTypeNodeHelper(types[0], context), + ts.factory.createTypeReferenceNode(`... ${types.length - 2} more ...`, /*typeArguments*/ undefined), + typeToTypeNodeHelper(types[types.length - 1], context) + ]; + } + } + const mayHaveNameCollisions = !(context.flags & ts.NodeBuilderFlags.UseFullyQualifiedType); + /** Map from type reference identifier text to [type, index in `result` where the type node is] */ + const seenNames = mayHaveNameCollisions ? ts.createUnderscoreEscapedMultiMap<[ + ts.Type, + number + ]>() : undefined; + const result: ts.TypeNode[] = []; + let i = 0; + for (const type of types) { + i++; + if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { + result.push(ts.factory.createTypeReferenceNode(`... ${types.length - i} more ...`, /*typeArguments*/ undefined)); + const typeNode = typeToTypeNodeHelper(types[types.length - 1], context); if (typeNode) { result.push(typeNode); - if (seenNames && ts.isIdentifierTypeReference(typeNode)) { - seenNames.add(typeNode.typeName.escapedText, [type, result.length - 1]); - } } + break; } - - if (seenNames) { - // To avoid printing types like `[Foo, Foo]` or `Bar & Bar` where - // occurrences of the same name actually come from different - // namespaces, go through the single-identifier type reference nodes - // we just generated, and see if any names were generated more than - // once while referring to different types. If so, regenerate the - // type node for each entry by that name with the - // `UseFullyQualifiedType` flag enabled. - const saveContextFlags = context.flags; - context.flags |= ts.NodeBuilderFlags.UseFullyQualifiedType; - seenNames.forEach(types => { - if (!ts.arrayIsHomogeneous(types, ([a], [b]) => typesAreSameReference(a, b))) { - for (const [type, resultIndex] of types) { - result[resultIndex] = typeToTypeNodeHelper(type, context); - } - } - }); - context.flags = saveContextFlags; + context.approximateLength += 2; // Account for whitespace + separator + const typeNode = typeToTypeNodeHelper(type, context); + if (typeNode) { + result.push(typeNode); + if (seenNames && ts.isIdentifierTypeReference(typeNode)) { + seenNames.add(typeNode.typeName.escapedText, [type, result.length - 1]); + } } + } - return result; + if (seenNames) { + // To avoid printing types like `[Foo, Foo]` or `Bar & Bar` where + // occurrences of the same name actually come from different + // namespaces, go through the single-identifier type reference nodes + // we just generated, and see if any names were generated more than + // once while referring to different types. If so, regenerate the + // type node for each entry by that name with the + // `UseFullyQualifiedType` flag enabled. + const saveContextFlags = context.flags; + context.flags |= ts.NodeBuilderFlags.UseFullyQualifiedType; + seenNames.forEach(types => { + if (!ts.arrayIsHomogeneous(types, ([a], [b]) => typesAreSameReference(a, b))) { + for (const [type, resultIndex] of types) { + result[resultIndex] = typeToTypeNodeHelper(type, context); + } + } + }); + context.flags = saveContextFlags; } - } - function typesAreSameReference(a: ts.Type, b: ts.Type): boolean { - return a === b - || !!a.symbol && a.symbol === b.symbol - || !!a.aliasSymbol && a.aliasSymbol === b.aliasSymbol; + return result; } + } - function indexInfoToIndexSignatureDeclarationHelper(indexInfo: ts.IndexInfo, context: NodeBuilderContext, typeNode: ts.TypeNode | undefined): ts.IndexSignatureDeclaration { - const name = ts.getNameFromIndexInfo(indexInfo) || "x"; - const indexerTypeNode = typeToTypeNodeHelper(indexInfo.keyType, context); + function typesAreSameReference(a: ts.Type, b: ts.Type): boolean { + return a === b + || !!a.symbol && a.symbol === b.symbol + || !!a.aliasSymbol && a.aliasSymbol === b.aliasSymbol; + } - const indexingParameter = ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, name, - /*questionToken*/ undefined, indexerTypeNode, - /*initializer*/ undefined); - if (!typeNode) { - typeNode = typeToTypeNodeHelper(indexInfo.type || anyType, context); - } - if (!indexInfo.type && !(context.flags & ts.NodeBuilderFlags.AllowEmptyIndexInfoType)) { - context.encounteredError = true; - } - context.approximateLength += (name.length + 4); - return ts.factory.createIndexSignature( - /*decorators*/ undefined, indexInfo.isReadonly ? [ts.factory.createToken(ts.SyntaxKind.ReadonlyKeyword)] : undefined, [indexingParameter], typeNode); - } + function indexInfoToIndexSignatureDeclarationHelper(indexInfo: ts.IndexInfo, context: NodeBuilderContext, typeNode: ts.TypeNode | undefined): ts.IndexSignatureDeclaration { + const name = ts.getNameFromIndexInfo(indexInfo) || "x"; + const indexerTypeNode = typeToTypeNodeHelper(indexInfo.keyType, context); - interface SignatureToSignatureDeclarationOptions { - modifiers?: readonly ts.Modifier[]; - name?: ts.PropertyName; - questionToken?: ts.QuestionToken; - privateSymbolVisitor?: (s: ts.Symbol) => void; - bundledImports?: boolean; + const indexingParameter = ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, name, + /*questionToken*/ undefined, indexerTypeNode, + /*initializer*/ undefined); + if (!typeNode) { + typeNode = typeToTypeNodeHelper(indexInfo.type || anyType, context); + } + if (!indexInfo.type && !(context.flags & ts.NodeBuilderFlags.AllowEmptyIndexInfoType)) { + context.encounteredError = true; } + context.approximateLength += (name.length + 4); + return ts.factory.createIndexSignature( + /*decorators*/ undefined, indexInfo.isReadonly ? [ts.factory.createToken(ts.SyntaxKind.ReadonlyKeyword)] : undefined, [indexingParameter], typeNode); + } - function signatureToSignatureDeclarationHelper(signature: ts.Signature, kind: ts.SignatureDeclaration["kind"], context: NodeBuilderContext, options?: SignatureToSignatureDeclarationOptions): ts.SignatureDeclaration { - const suppressAny = context.flags & ts.NodeBuilderFlags.SuppressAnyReturnType; - if (suppressAny) - context.flags &= ~ts.NodeBuilderFlags.SuppressAnyReturnType; // suppress only toplevel `any`s - context.approximateLength += 3; // Usually a signature contributes a few more characters than this, but 3 is the minimum - let typeParameters: ts.TypeParameterDeclaration[] | undefined; - let typeArguments: ts.TypeNode[] | undefined; - if (context.flags & ts.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)); - } + interface SignatureToSignatureDeclarationOptions { + modifiers?: readonly ts.Modifier[]; + name?: ts.PropertyName; + questionToken?: ts.QuestionToken; + privateSymbolVisitor?: (s: ts.Symbol) => void; + bundledImports?: boolean; + } - const expandedParams = getExpandedParameters(signature, /*skipUnionExpanding*/ true)[0]; - // If the expanded parameter list had a variadic in a non-trailing position, don't expand it - const parameters = (ts.some(expandedParams, p => p !== expandedParams[expandedParams.length - 1] && !!(ts.getCheckFlags(p) & ts.CheckFlags.RestParameter)) ? signature.parameters : expandedParams).map(parameter => symbolToParameterDeclaration(parameter, context, kind === ts.SyntaxKind.Constructor, options?.privateSymbolVisitor, options?.bundledImports)); - const thisParameter = tryGetThisParameterDeclaration(signature, context); - if (thisParameter) { - parameters.unshift(thisParameter); - } + function signatureToSignatureDeclarationHelper(signature: ts.Signature, kind: ts.SignatureDeclaration["kind"], context: NodeBuilderContext, options?: SignatureToSignatureDeclarationOptions): ts.SignatureDeclaration { + const suppressAny = context.flags & ts.NodeBuilderFlags.SuppressAnyReturnType; + if (suppressAny) + context.flags &= ~ts.NodeBuilderFlags.SuppressAnyReturnType; // suppress only toplevel `any`s + context.approximateLength += 3; // Usually a signature contributes a few more characters than this, but 3 is the minimum + let typeParameters: ts.TypeParameterDeclaration[] | undefined; + let typeArguments: ts.TypeNode[] | undefined; + if (context.flags & ts.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)); + } - let returnTypeNode: ts.TypeNode | undefined; - const typePredicate = getTypePredicateOfSignature(signature); - if (typePredicate) { - const assertsModifier = typePredicate.kind === ts.TypePredicateKind.AssertsThis || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? - ts.factory.createToken(ts.SyntaxKind.AssertsKeyword) : - undefined; - const parameterName = typePredicate.kind === ts.TypePredicateKind.Identifier || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? - ts.setEmitFlags(ts.factory.createIdentifier(typePredicate.parameterName), ts.EmitFlags.NoAsciiEscaping) : - ts.factory.createThisTypeNode(); - const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context); - returnTypeNode = ts.factory.createTypePredicateNode(assertsModifier, parameterName, typeNode); - } - else { - const returnType = getReturnTypeOfSignature(signature); - if (returnType && !(suppressAny && isTypeAny(returnType))) { - returnTypeNode = serializeReturnTypeForSignature(context, returnType, signature, options?.privateSymbolVisitor, options?.bundledImports); - } - else if (!suppressAny) { - returnTypeNode = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } - } - let modifiers = options?.modifiers; - if ((kind === ts.SyntaxKind.ConstructorType) && signature.flags & ts.SignatureFlags.Abstract) { - const flags = ts.modifiersToFlags(modifiers); - modifiers = ts.factory.createModifiersFromModifierFlags(flags | ts.ModifierFlags.Abstract); - } - const node = kind === ts.SyntaxKind.CallSignature ? ts.factory.createCallSignature(typeParameters, parameters, returnTypeNode) : - kind === ts.SyntaxKind.ConstructSignature ? ts.factory.createConstructSignature(typeParameters, parameters, returnTypeNode) : - kind === ts.SyntaxKind.MethodSignature ? ts.factory.createMethodSignature(modifiers, options?.name ?? ts.factory.createIdentifier(""), options?.questionToken, typeParameters, parameters, returnTypeNode) : - kind === ts.SyntaxKind.MethodDeclaration ? ts.factory.createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, options?.name ?? ts.factory.createIdentifier(""), /*questionToken*/ undefined, typeParameters, parameters, returnTypeNode, /*body*/ undefined) : - kind === ts.SyntaxKind.Constructor ? ts.factory.createConstructorDeclaration(/*decorators*/ undefined, modifiers, parameters, /*body*/ undefined) : - kind === ts.SyntaxKind.GetAccessor ? ts.factory.createGetAccessorDeclaration(/*decorators*/ undefined, modifiers, options?.name ?? ts.factory.createIdentifier(""), parameters, returnTypeNode, /*body*/ undefined) : - kind === ts.SyntaxKind.SetAccessor ? ts.factory.createSetAccessorDeclaration(/*decorators*/ undefined, modifiers, options?.name ?? ts.factory.createIdentifier(""), parameters, /*body*/ undefined) : - kind === ts.SyntaxKind.IndexSignature ? ts.factory.createIndexSignature(/*decorators*/ undefined, modifiers, parameters, returnTypeNode) : - kind === ts.SyntaxKind.JSDocFunctionType ? ts.factory.createJSDocFunctionType(parameters, returnTypeNode) : - kind === ts.SyntaxKind.FunctionType ? ts.factory.createFunctionTypeNode(typeParameters, parameters, returnTypeNode ?? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""))) : - kind === ts.SyntaxKind.ConstructorType ? ts.factory.createConstructorTypeNode(modifiers, typeParameters, parameters, returnTypeNode ?? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""))) : - kind === ts.SyntaxKind.FunctionDeclaration ? ts.factory.createFunctionDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, options?.name ? ts.cast(options.name, ts.isIdentifier) : ts.factory.createIdentifier(""), typeParameters, parameters, returnTypeNode, /*body*/ undefined) : - kind === ts.SyntaxKind.FunctionExpression ? ts.factory.createFunctionExpression(modifiers, /*asteriskToken*/ undefined, options?.name ? ts.cast(options.name, ts.isIdentifier) : ts.factory.createIdentifier(""), typeParameters, parameters, returnTypeNode, ts.factory.createBlock([])) : - kind === ts.SyntaxKind.ArrowFunction ? ts.factory.createArrowFunction(modifiers, typeParameters, parameters, returnTypeNode, /*equalsGreaterThanToken*/ undefined, ts.factory.createBlock([])) : - ts.Debug.assertNever(kind); - - if (typeArguments) { - node.typeArguments = ts.factory.createNodeArray(typeArguments); - } + const expandedParams = getExpandedParameters(signature, /*skipUnionExpanding*/ true)[0]; + // If the expanded parameter list had a variadic in a non-trailing position, don't expand it + const parameters = (ts.some(expandedParams, p => p !== expandedParams[expandedParams.length - 1] && !!(ts.getCheckFlags(p) & ts.CheckFlags.RestParameter)) ? signature.parameters : expandedParams).map(parameter => symbolToParameterDeclaration(parameter, context, kind === ts.SyntaxKind.Constructor, options?.privateSymbolVisitor, options?.bundledImports)); + const thisParameter = tryGetThisParameterDeclaration(signature, context); + if (thisParameter) { + parameters.unshift(thisParameter); + } - return node; + let returnTypeNode: ts.TypeNode | undefined; + const typePredicate = getTypePredicateOfSignature(signature); + if (typePredicate) { + const assertsModifier = typePredicate.kind === ts.TypePredicateKind.AssertsThis || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? + ts.factory.createToken(ts.SyntaxKind.AssertsKeyword) : + undefined; + const parameterName = typePredicate.kind === ts.TypePredicateKind.Identifier || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? + ts.setEmitFlags(ts.factory.createIdentifier(typePredicate.parameterName), ts.EmitFlags.NoAsciiEscaping) : + ts.factory.createThisTypeNode(); + const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context); + returnTypeNode = ts.factory.createTypePredicateNode(assertsModifier, parameterName, typeNode); + } + else { + const returnType = getReturnTypeOfSignature(signature); + if (returnType && !(suppressAny && isTypeAny(returnType))) { + returnTypeNode = serializeReturnTypeForSignature(context, returnType, signature, options?.privateSymbolVisitor, options?.bundledImports); + } + else if (!suppressAny) { + returnTypeNode = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } + } + let modifiers = options?.modifiers; + if ((kind === ts.SyntaxKind.ConstructorType) && signature.flags & ts.SignatureFlags.Abstract) { + const flags = ts.modifiersToFlags(modifiers); + modifiers = ts.factory.createModifiersFromModifierFlags(flags | ts.ModifierFlags.Abstract); + } + const node = kind === ts.SyntaxKind.CallSignature ? ts.factory.createCallSignature(typeParameters, parameters, returnTypeNode) : + kind === ts.SyntaxKind.ConstructSignature ? ts.factory.createConstructSignature(typeParameters, parameters, returnTypeNode) : + kind === ts.SyntaxKind.MethodSignature ? ts.factory.createMethodSignature(modifiers, options?.name ?? ts.factory.createIdentifier(""), options?.questionToken, typeParameters, parameters, returnTypeNode) : + kind === ts.SyntaxKind.MethodDeclaration ? ts.factory.createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, options?.name ?? ts.factory.createIdentifier(""), /*questionToken*/ undefined, typeParameters, parameters, returnTypeNode, /*body*/ undefined) : + kind === ts.SyntaxKind.Constructor ? ts.factory.createConstructorDeclaration(/*decorators*/ undefined, modifiers, parameters, /*body*/ undefined) : + kind === ts.SyntaxKind.GetAccessor ? ts.factory.createGetAccessorDeclaration(/*decorators*/ undefined, modifiers, options?.name ?? ts.factory.createIdentifier(""), parameters, returnTypeNode, /*body*/ undefined) : + kind === ts.SyntaxKind.SetAccessor ? ts.factory.createSetAccessorDeclaration(/*decorators*/ undefined, modifiers, options?.name ?? ts.factory.createIdentifier(""), parameters, /*body*/ undefined) : + kind === ts.SyntaxKind.IndexSignature ? ts.factory.createIndexSignature(/*decorators*/ undefined, modifiers, parameters, returnTypeNode) : + kind === ts.SyntaxKind.JSDocFunctionType ? ts.factory.createJSDocFunctionType(parameters, returnTypeNode) : + kind === ts.SyntaxKind.FunctionType ? ts.factory.createFunctionTypeNode(typeParameters, parameters, returnTypeNode ?? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""))) : + kind === ts.SyntaxKind.ConstructorType ? ts.factory.createConstructorTypeNode(modifiers, typeParameters, parameters, returnTypeNode ?? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(""))) : + kind === ts.SyntaxKind.FunctionDeclaration ? ts.factory.createFunctionDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, options?.name ? ts.cast(options.name, ts.isIdentifier) : ts.factory.createIdentifier(""), typeParameters, parameters, returnTypeNode, /*body*/ undefined) : + kind === ts.SyntaxKind.FunctionExpression ? ts.factory.createFunctionExpression(modifiers, /*asteriskToken*/ undefined, options?.name ? ts.cast(options.name, ts.isIdentifier) : ts.factory.createIdentifier(""), typeParameters, parameters, returnTypeNode, ts.factory.createBlock([])) : + kind === ts.SyntaxKind.ArrowFunction ? ts.factory.createArrowFunction(modifiers, typeParameters, parameters, returnTypeNode, /*equalsGreaterThanToken*/ undefined, ts.factory.createBlock([])) : + ts.Debug.assertNever(kind); + + if (typeArguments) { + node.typeArguments = ts.factory.createNodeArray(typeArguments); } - function tryGetThisParameterDeclaration(signature: ts.Signature, context: NodeBuilderContext) { - if (signature.thisParameter) { - return symbolToParameterDeclaration(signature.thisParameter, context); - } - if (signature.declaration) { - const thisTag = ts.getJSDocThisTag(signature.declaration); - if (thisTag && thisTag.typeExpression) { - return ts.factory.createParameterDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, "this", - /* questionToken */ undefined, typeToTypeNodeHelper(getTypeFromTypeNode(thisTag.typeExpression), context)); - } + return node; + } + + function tryGetThisParameterDeclaration(signature: ts.Signature, context: NodeBuilderContext) { + if (signature.thisParameter) { + return symbolToParameterDeclaration(signature.thisParameter, context); + } + if (signature.declaration) { + const thisTag = ts.getJSDocThisTag(signature.declaration); + if (thisTag && thisTag.typeExpression) { + return ts.factory.createParameterDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, "this", + /* questionToken */ undefined, typeToTypeNodeHelper(getTypeFromTypeNode(thisTag.typeExpression), context)); } } + } - function typeParameterToDeclarationWithConstraint(type: ts.TypeParameter, context: NodeBuilderContext, constraintNode: ts.TypeNode | undefined): ts.TypeParameterDeclaration { - const savedContextFlags = context.flags; - context.flags &= ~ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName; // Avoids potential infinite loop when building for a claimspace with a generic - const modifiers = ts.factory.createModifiersFromModifierFlags(getVarianceModifiers(type)); - const name = typeParameterToName(type, context); - const defaultParameter = getDefaultFromTypeParameter(type); - const defaultParameterNode = defaultParameter && typeToTypeNodeHelper(defaultParameter, context); - context.flags = savedContextFlags; - return ts.factory.createTypeParameterDeclaration(modifiers, name, constraintNode, defaultParameterNode); - } + function typeParameterToDeclarationWithConstraint(type: ts.TypeParameter, context: NodeBuilderContext, constraintNode: ts.TypeNode | undefined): ts.TypeParameterDeclaration { + const savedContextFlags = context.flags; + context.flags &= ~ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName; // Avoids potential infinite loop when building for a claimspace with a generic + const modifiers = ts.factory.createModifiersFromModifierFlags(getVarianceModifiers(type)); + const name = typeParameterToName(type, context); + const defaultParameter = getDefaultFromTypeParameter(type); + const defaultParameterNode = defaultParameter && typeToTypeNodeHelper(defaultParameter, context); + context.flags = savedContextFlags; + return ts.factory.createTypeParameterDeclaration(modifiers, name, constraintNode, defaultParameterNode); + } - function typeParameterToDeclaration(type: ts.TypeParameter, context: NodeBuilderContext, constraint = getConstraintOfTypeParameter(type)): ts.TypeParameterDeclaration { - const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); - return typeParameterToDeclarationWithConstraint(type, context, constraintNode); - } + function typeParameterToDeclaration(type: ts.TypeParameter, context: NodeBuilderContext, constraint = getConstraintOfTypeParameter(type)): ts.TypeParameterDeclaration { + const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); + return typeParameterToDeclarationWithConstraint(type, context, constraintNode); + } - function symbolToParameterDeclaration(parameterSymbol: ts.Symbol, context: NodeBuilderContext, preserveModifierFlags?: boolean, privateSymbolVisitor?: (s: ts.Symbol) => void, bundledImports?: boolean): ts.ParameterDeclaration { - let parameterDeclaration: ts.ParameterDeclaration | ts.JSDocParameterTag | undefined = ts.getDeclarationOfKind(parameterSymbol, ts.SyntaxKind.Parameter); - if (!parameterDeclaration && !ts.isTransientSymbol(parameterSymbol)) { - parameterDeclaration = ts.getDeclarationOfKind(parameterSymbol, ts.SyntaxKind.JSDocParameterTag); - } + function symbolToParameterDeclaration(parameterSymbol: ts.Symbol, context: NodeBuilderContext, preserveModifierFlags?: boolean, privateSymbolVisitor?: (s: ts.Symbol) => void, bundledImports?: boolean): ts.ParameterDeclaration { + let parameterDeclaration: ts.ParameterDeclaration | ts.JSDocParameterTag | undefined = ts.getDeclarationOfKind(parameterSymbol, ts.SyntaxKind.Parameter); + if (!parameterDeclaration && !ts.isTransientSymbol(parameterSymbol)) { + parameterDeclaration = ts.getDeclarationOfKind(parameterSymbol, ts.SyntaxKind.JSDocParameterTag); + } - let parameterType = getTypeOfSymbol(parameterSymbol); - if (parameterDeclaration && isRequiredInitializedParameter(parameterDeclaration)) { - parameterType = getOptionalType(parameterType); - } - const parameterTypeNode = serializeTypeForDeclaration(context, parameterType, parameterSymbol, context.enclosingDeclaration, privateSymbolVisitor, bundledImports); + let parameterType = getTypeOfSymbol(parameterSymbol); + if (parameterDeclaration && isRequiredInitializedParameter(parameterDeclaration)) { + parameterType = getOptionalType(parameterType); + } + const parameterTypeNode = serializeTypeForDeclaration(context, parameterType, parameterSymbol, context.enclosingDeclaration, privateSymbolVisitor, bundledImports); - const modifiers = !(context.flags & ts.NodeBuilderFlags.OmitParameterModifiers) && preserveModifierFlags && parameterDeclaration && parameterDeclaration.modifiers ? parameterDeclaration.modifiers.map(ts.factory.cloneNode) : undefined; - const isRest = parameterDeclaration && ts.isRestParameter(parameterDeclaration) || ts.getCheckFlags(parameterSymbol) & ts.CheckFlags.RestParameter; - const dotDotDotToken = isRest ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined; - const name = parameterDeclaration ? parameterDeclaration.name ? - parameterDeclaration.name.kind === ts.SyntaxKind.Identifier ? ts.setEmitFlags(ts.factory.cloneNode(parameterDeclaration.name), ts.EmitFlags.NoAsciiEscaping) : - parameterDeclaration.name.kind === ts.SyntaxKind.QualifiedName ? ts.setEmitFlags(ts.factory.cloneNode(parameterDeclaration.name.right), ts.EmitFlags.NoAsciiEscaping) : - cloneBindingName(parameterDeclaration.name) : - ts.symbolName(parameterSymbol) : - ts.symbolName(parameterSymbol); - const isOptional = parameterDeclaration && isOptionalParameter(parameterDeclaration) || ts.getCheckFlags(parameterSymbol) & ts.CheckFlags.OptionalParameter; - const questionToken = isOptional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined; - const parameterNode = ts.factory.createParameterDeclaration( - /*decorators*/ undefined, modifiers, dotDotDotToken, name, questionToken, parameterTypeNode, - /*initializer*/ undefined); - context.approximateLength += ts.symbolName(parameterSymbol).length + 3; - return parameterNode; + const modifiers = !(context.flags & ts.NodeBuilderFlags.OmitParameterModifiers) && preserveModifierFlags && parameterDeclaration && parameterDeclaration.modifiers ? parameterDeclaration.modifiers.map(ts.factory.cloneNode) : undefined; + const isRest = parameterDeclaration && ts.isRestParameter(parameterDeclaration) || ts.getCheckFlags(parameterSymbol) & ts.CheckFlags.RestParameter; + const dotDotDotToken = isRest ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined; + const name = parameterDeclaration ? parameterDeclaration.name ? + parameterDeclaration.name.kind === ts.SyntaxKind.Identifier ? ts.setEmitFlags(ts.factory.cloneNode(parameterDeclaration.name), ts.EmitFlags.NoAsciiEscaping) : + parameterDeclaration.name.kind === ts.SyntaxKind.QualifiedName ? ts.setEmitFlags(ts.factory.cloneNode(parameterDeclaration.name.right), ts.EmitFlags.NoAsciiEscaping) : + cloneBindingName(parameterDeclaration.name) : + ts.symbolName(parameterSymbol) : + ts.symbolName(parameterSymbol); + const isOptional = parameterDeclaration && isOptionalParameter(parameterDeclaration) || ts.getCheckFlags(parameterSymbol) & ts.CheckFlags.OptionalParameter; + const questionToken = isOptional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined; + const parameterNode = ts.factory.createParameterDeclaration( + /*decorators*/ undefined, modifiers, dotDotDotToken, name, questionToken, parameterTypeNode, + /*initializer*/ undefined); + context.approximateLength += ts.symbolName(parameterSymbol).length + 3; + return parameterNode; - function cloneBindingName(node: ts.BindingName): ts.BindingName { - return elideInitializerAndSetEmitFlags(node) as ts.BindingName; - function elideInitializerAndSetEmitFlags(node: ts.Node): ts.Node { - if (context.tracker.trackSymbol && ts.isComputedPropertyName(node) && isLateBindableName(node)) { - trackComputedName(node.expression, context.enclosingDeclaration, context); - } - let visited = ts.visitEachChild(node, elideInitializerAndSetEmitFlags, ts.nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags)!; - if (ts.isBindingElement(visited)) { - visited = ts.factory.updateBindingElement(visited, visited.dotDotDotToken, visited.propertyName, visited.name, - /*initializer*/ undefined); - } - if (!ts.nodeIsSynthesized(visited)) { - visited = ts.factory.cloneNode(visited); - } - return ts.setEmitFlags(visited, ts.EmitFlags.SingleLine | ts.EmitFlags.NoAsciiEscaping); + function cloneBindingName(node: ts.BindingName): ts.BindingName { + return elideInitializerAndSetEmitFlags(node) as ts.BindingName; + function elideInitializerAndSetEmitFlags(node: ts.Node): ts.Node { + if (context.tracker.trackSymbol && ts.isComputedPropertyName(node) && isLateBindableName(node)) { + trackComputedName(node.expression, context.enclosingDeclaration, context); } + let visited = ts.visitEachChild(node, elideInitializerAndSetEmitFlags, ts.nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags)!; + if (ts.isBindingElement(visited)) { + visited = ts.factory.updateBindingElement(visited, visited.dotDotDotToken, visited.propertyName, visited.name, + /*initializer*/ undefined); + } + if (!ts.nodeIsSynthesized(visited)) { + visited = ts.factory.cloneNode(visited); + } + return ts.setEmitFlags(visited, ts.EmitFlags.SingleLine | ts.EmitFlags.NoAsciiEscaping); } } + } - function trackComputedName(accessExpression: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node | undefined, context: NodeBuilderContext) { - if (!context.tracker.trackSymbol) - return; - // get symbol of the first identifier of the entityName - const firstIdentifier = ts.getFirstIdentifier(accessExpression); - const name = resolveName(firstIdentifier, firstIdentifier.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (name) { - context.tracker.trackSymbol(name, enclosingDeclaration, ts.SymbolFlags.Value); - } + function trackComputedName(accessExpression: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node | undefined, context: NodeBuilderContext) { + if (!context.tracker.trackSymbol) + return; + // get symbol of the first identifier of the entityName + const firstIdentifier = ts.getFirstIdentifier(accessExpression); + const name = resolveName(firstIdentifier, firstIdentifier.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (name) { + context.tracker.trackSymbol(name, enclosingDeclaration, ts.SymbolFlags.Value); } + } - function lookupSymbolChain(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, yieldModuleSymbol?: boolean) { - context.tracker.trackSymbol!(symbol, context.enclosingDeclaration, meaning); // TODO: GH#18217 - return lookupSymbolChainWorker(symbol, context, meaning, yieldModuleSymbol); - } + function lookupSymbolChain(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.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: ts.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 & ts.SymbolFlags.TypeParameter; - if (!isTypeParameter && (context.enclosingDeclaration || context.flags & ts.NodeBuilderFlags.UseFullyQualifiedType) && !(context.flags & ts.NodeBuilderFlags.DoNotIncludeSymbolChain)) { - chain = ts.Debug.checkDefined(getSymbolChain(symbol, meaning, /*endOfChain*/ true)); - ts.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: ts.SymbolFlags, endOfChain: boolean): ts.Symbol[] | undefined { - let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & ts.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, meaning); - if (ts.length(parents)) { - parentSpecifiers = parents!.map(symbol => ts.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(ts.InternalSymbolName.ExportEquals) && - getSymbolIfSameReference(parent.exports.get(ts.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]); + function lookupSymbolChainWorker(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.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 & ts.SymbolFlags.TypeParameter; + if (!isTypeParameter && (context.enclosingDeclaration || context.flags & ts.NodeBuilderFlags.UseFullyQualifiedType) && !(context.flags & ts.NodeBuilderFlags.DoNotIncludeSymbolChain)) { + chain = ts.Debug.checkDefined(getSymbolChain(symbol, meaning, /*endOfChain*/ true)); + ts.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: ts.SymbolFlags, endOfChain: boolean): ts.Symbol[] | undefined { + let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & ts.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, meaning); + if (ts.length(parents)) { + parentSpecifiers = parents!.map(symbol => ts.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(ts.InternalSymbolName.ExportEquals) && + getSymbolIfSameReference(parent.exports.get(ts.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 & (ts.SymbolFlags.TypeLiteral | ts.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 && !!ts.forEach(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - return; - } - return [symbol]; + 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 & (ts.SymbolFlags.TypeLiteral | ts.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 && !!ts.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 = ts.pathIsRelative(specifierB); - if (ts.pathIsRelative(specifierA) === isBRelative) { - // Both relative or both non-relative, sort by number of parts - return ts.moduleSpecifiers.countPathComponents(specifierA) - ts.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; + function sortByBestName(a: number, b: number) { + const specifierA = parentSpecifiers[a]; + const specifierB = parentSpecifiers[b]; + if (specifierA && specifierB) { + const isBRelative = ts.pathIsRelative(specifierB); + if (ts.pathIsRelative(specifierA) === isBRelative) { + // Both relative or both non-relative, sort by number of parts + return ts.moduleSpecifiers.countPathComponents(specifierA) - ts.moduleSpecifiers.countPathComponents(specifierB); + } + if (isBRelative) { + // A is non-relative, B is relative: prefer A + return -1; } - return 0; + // A is relative, B is non-relative: prefer B + return 1; } + return 0; } } + } - function typeParametersToTypeParameterDeclarations(symbol: ts.Symbol, context: NodeBuilderContext) { - let typeParameterNodes: ts.NodeArray | undefined; - const targetSymbol = getTargetSymbol(symbol); - if (targetSymbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias)) { - typeParameterNodes = ts.factory.createNodeArray(ts.map(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol), tp => typeParameterToDeclaration(tp, context))); - } - return typeParameterNodes; + function typeParametersToTypeParameterDeclarations(symbol: ts.Symbol, context: NodeBuilderContext) { + let typeParameterNodes: ts.NodeArray | undefined; + const targetSymbol = getTargetSymbol(symbol); + if (targetSymbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias)) { + typeParameterNodes = ts.factory.createNodeArray(ts.map(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol), tp => typeParameterToDeclaration(tp, context))); } + return typeParameterNodes; + } - function lookupTypeParameterNodes(chain: ts.Symbol[], index: number, context: NodeBuilderContext) { - ts.Debug.assert(chain && 0 <= index && index < chain.length); - const symbol = chain[index]; - const symbolId = getSymbolId(symbol); - if (context.typeParameterSymbolList?.has(symbolId)) { - return undefined; + function lookupTypeParameterNodes(chain: ts.Symbol[], index: number, context: NodeBuilderContext) { + ts.Debug.assert(chain && 0 <= index && index < chain.length); + const symbol = chain[index]; + const symbolId = getSymbolId(symbol); + if (context.typeParameterSymbolList?.has(symbolId)) { + return undefined; + } + (context.typeParameterSymbolList || (context.typeParameterSymbolList = new ts.Set())).add(symbolId); + let typeParameterNodes: readonly ts.TypeNode[] | readonly ts.TypeParameterDeclaration[] | undefined; + if (context.flags & ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName && index < (chain.length - 1)) { + const parentSymbol = symbol; + const nextSymbol = chain[index + 1]; + if (ts.getCheckFlags(nextSymbol) & ts.CheckFlags.Instantiated) { + const params = getTypeParametersOfClassOrInterface(parentSymbol.flags & ts.SymbolFlags.Alias ? resolveAlias(parentSymbol) : parentSymbol); + typeParameterNodes = mapToTypeNodes(ts.map(params, t => getMappedType(t, (nextSymbol as ts.TransientSymbol).mapper!)), context); } - (context.typeParameterSymbolList || (context.typeParameterSymbolList = new ts.Set())).add(symbolId); - let typeParameterNodes: readonly ts.TypeNode[] | readonly ts.TypeParameterDeclaration[] | undefined; - if (context.flags & ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName && index < (chain.length - 1)) { - const parentSymbol = symbol; - const nextSymbol = chain[index + 1]; - if (ts.getCheckFlags(nextSymbol) & ts.CheckFlags.Instantiated) { - const params = getTypeParametersOfClassOrInterface(parentSymbol.flags & ts.SymbolFlags.Alias ? resolveAlias(parentSymbol) : parentSymbol); - typeParameterNodes = mapToTypeNodes(ts.map(params, t => getMappedType(t, (nextSymbol as ts.TransientSymbol).mapper!)), context); - } - else { - typeParameterNodes = typeParametersToTypeParameterDeclarations(symbol, context); - } + else { + typeParameterNodes = typeParametersToTypeParameterDeclarations(symbol, context); } - return typeParameterNodes; } + return typeParameterNodes; + } - /** - * Given A[B][C][D], finds A[B] - */ - function getTopmostIndexedAccessType(top: ts.IndexedAccessTypeNode): ts.IndexedAccessTypeNode { - if (ts.isIndexedAccessTypeNode(top.objectType)) { - return getTopmostIndexedAccessType(top.objectType); - } - return top; + /** + * Given A[B][C][D], finds A[B] + */ + function getTopmostIndexedAccessType(top: ts.IndexedAccessTypeNode): ts.IndexedAccessTypeNode { + if (ts.isIndexedAccessTypeNode(top.objectType)) { + return getTopmostIndexedAccessType(top.objectType); } + return top; + } - function getSpecifierForModuleSymbol(symbol: ts.Symbol, context: NodeBuilderContext, overrideImportMode?: ts.SourceFile["impliedNodeFormat"]) { - let file = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SourceFile); - if (!file) { - const equivalentFileSymbol = ts.firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol)); - if (equivalentFileSymbol) { - file = ts.getDeclarationOfKind(equivalentFileSymbol, ts.SyntaxKind.SourceFile); - } - } - if (file && file.moduleName !== undefined) { - // Use the amd name if it is available - return file.moduleName; + function getSpecifierForModuleSymbol(symbol: ts.Symbol, context: NodeBuilderContext, overrideImportMode?: ts.SourceFile["impliedNodeFormat"]) { + let file = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SourceFile); + if (!file) { + const equivalentFileSymbol = ts.firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol)); + if (equivalentFileSymbol) { + file = ts.getDeclarationOfKind(equivalentFileSymbol, ts.SyntaxKind.SourceFile); } - if (!file) { - if (context.tracker.trackReferencedAmbientModule) { - const ambientDecls = ts.filter(symbol.declarations, ts.isAmbientModule); - if (ts.length(ambientDecls)) { - for (const decl of ambientDecls!) { - context.tracker.trackReferencedAmbientModule(decl, symbol); - } + } + if (file && file.moduleName !== undefined) { + // Use the amd name if it is available + return file.moduleName; + } + if (!file) { + if (context.tracker.trackReferencedAmbientModule) { + const ambientDecls = ts.filter(symbol.declarations, ts.isAmbientModule); + if (ts.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 ts.getSourceFileOfNode(ts.getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full + 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 ts.getSourceFileOfNode(ts.getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full + } + const contextFile = ts.getSourceFileOfNode(ts.getOriginalNode(context.enclosingDeclaration)); + const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat; + const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode); + const links = getSymbolLinks(symbol); + let specifier = links.specifierCache && links.specifierCache.get(cacheKey); + if (!specifier) { + const isBundle = !!ts.outFile(compilerOptions); + // 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 = ts.first(ts.moduleSpecifiers.getModuleSpecifiers(symbol, checker, specifierCompilerOptions, contextFile, moduleResolverHost, { + importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", + importModuleSpecifierEnding: isBundle ? "minimal" + : resolutionMode === ts.ModuleKind.ESNext ? "js" + : undefined, + }, { overrideImportMode })); + links.specifierCache ??= new ts.Map(); + links.specifierCache.set(cacheKey, specifier); + } + return specifier; + + function getSpecifierCacheKey(path: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined) { + return mode === undefined ? path : `${mode}|${path}`; + } + } + + function symbolToEntityNameNode(symbol: ts.Symbol): ts.EntityName { + const identifier = ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(symbol.escapedName)); + return symbol.parent ? ts.factory.createQualifiedName(symbolToEntityNameNode(symbol.parent), identifier) : identifier; + } + + function symbolToTypeNode(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, overrideTypeArguments?: readonly ts.TypeNode[]): ts.TypeNode { + const chain = lookupSymbolChain(symbol, context, meaning, !(context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope)); // If we're using aliases outside the current scope, dont bother with the module + const isTypeOf = meaning === ts.SymbolFlags.Value; + if (ts.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 contextFile = ts.getSourceFileOfNode(ts.getOriginalNode(context.enclosingDeclaration)); - const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat; - const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode); - const links = getSymbolLinks(symbol); - let specifier = links.specifierCache && links.specifierCache.get(cacheKey); + const targetFile = ts.getSourceFileOfModule(chain[0]); + let specifier: string | undefined; + let assertion: ts.ImportTypeAssertionContainer | undefined; + if (ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext) { + // An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion + if (targetFile?.impliedNodeFormat === ts.ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) { + specifier = getSpecifierForModuleSymbol(chain[0], context, ts.ModuleKind.ESNext); + assertion = ts.factory.createImportTypeAssertionContainer(ts.factory.createAssertClause(ts.factory.createNodeArray([ + ts.factory.createAssertEntry(ts.factory.createStringLiteral("resolution-mode"), ts.factory.createStringLiteral("import")) + ]))); + } + } if (!specifier) { - const isBundle = !!ts.outFile(compilerOptions); - // 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 = ts.first(ts.moduleSpecifiers.getModuleSpecifiers(symbol, checker, specifierCompilerOptions, contextFile, moduleResolverHost, { - importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", - importModuleSpecifierEnding: isBundle ? "minimal" - : resolutionMode === ts.ModuleKind.ESNext ? "js" - : undefined, - }, { overrideImportMode })); - links.specifierCache ??= new ts.Map(); - links.specifierCache.set(cacheKey, specifier); - } - return specifier; - - function getSpecifierCacheKey(path: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined) { - return mode === undefined ? path : `${mode}|${path}`; - } - } - - function symbolToEntityNameNode(symbol: ts.Symbol): ts.EntityName { - const identifier = ts.factory.createIdentifier(ts.unescapeLeadingUnderscores(symbol.escapedName)); - return symbol.parent ? ts.factory.createQualifiedName(symbolToEntityNameNode(symbol.parent), identifier) : identifier; - } - - function symbolToTypeNode(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, overrideTypeArguments?: readonly ts.TypeNode[]): ts.TypeNode { - const chain = lookupSymbolChain(symbol, context, meaning, !(context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope)); // If we're using aliases outside the current scope, dont bother with the module - const isTypeOf = meaning === ts.SymbolFlags.Value; - if (ts.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 contextFile = ts.getSourceFileOfNode(ts.getOriginalNode(context.enclosingDeclaration)); - const targetFile = ts.getSourceFileOfModule(chain[0]); - let specifier: string | undefined; - let assertion: ts.ImportTypeAssertionContainer | undefined; + specifier = getSpecifierForModuleSymbol(chain[0], context); + } + if (!(context.flags & ts.NodeBuilderFlags.AllowNodeModulesRelativePaths) && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) { + const oldSpecifier = specifier; if (ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext) { - // An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion - if (targetFile?.impliedNodeFormat === ts.ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) { - specifier = getSpecifierForModuleSymbol(chain[0], context, ts.ModuleKind.ESNext); + // We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set + const swappedMode = contextFile?.impliedNodeFormat === ts.ModuleKind.ESNext ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext; + specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode); + + if (specifier.indexOf("/node_modules/") >= 0) { + // Still unreachable :( + specifier = oldSpecifier; + } + else { assertion = ts.factory.createImportTypeAssertionContainer(ts.factory.createAssertClause(ts.factory.createNodeArray([ - ts.factory.createAssertEntry(ts.factory.createStringLiteral("resolution-mode"), ts.factory.createStringLiteral("import")) + ts.factory.createAssertEntry(ts.factory.createStringLiteral("resolution-mode"), ts.factory.createStringLiteral(swappedMode === ts.ModuleKind.ESNext ? "import" : "require")) ]))); } } - if (!specifier) { - specifier = getSpecifierForModuleSymbol(chain[0], context); - } - if (!(context.flags & ts.NodeBuilderFlags.AllowNodeModulesRelativePaths) && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) { - const oldSpecifier = specifier; - if (ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext) { - // We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set - const swappedMode = contextFile?.impliedNodeFormat === ts.ModuleKind.ESNext ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext; - specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode); - - if (specifier.indexOf("/node_modules/") >= 0) { - // Still unreachable :( - specifier = oldSpecifier; - } - else { - assertion = ts.factory.createImportTypeAssertionContainer(ts.factory.createAssertClause(ts.factory.createNodeArray([ - ts.factory.createAssertEntry(ts.factory.createStringLiteral("resolution-mode"), ts.factory.createStringLiteral(swappedMode === ts.ModuleKind.ESNext ? "import" : "require")) - ]))); - } - } - if (!assertion) { - // 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(oldSpecifier); - } - } - } - const lit = ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(specifier)); - if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) - context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]); - context.approximateLength += specifier.length + 10; // specifier + import("") - if (!nonRootParts || ts.isEntityName(nonRootParts)) { - if (nonRootParts) { - const lastId = ts.isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right; - lastId.typeArguments = undefined; + if (!assertion) { + // 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(oldSpecifier); } - return ts.factory.createImportTypeNode(lit, assertion, nonRootParts as ts.EntityName, typeParameterNodes as readonly ts.TypeNode[], isTypeOf); - } - else { - const splitNode = getTopmostIndexedAccessType(nonRootParts); - const qualifier = (splitNode.objectType as ts.TypeReferenceNode).typeName; - return ts.factory.createIndexedAccessTypeNode(ts.factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly ts.TypeNode[], isTypeOf), splitNode.indexType); } } - - const entityName = createAccessFromSymbolChain(chain, chain.length - 1, 0); - if (ts.isIndexedAccessTypeNode(entityName)) { - return entityName; // Indexed accesses can never be `typeof` - } - if (isTypeOf) { - return ts.factory.createTypeQueryNode(entityName); + const lit = ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(specifier)); + if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) + context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]); + context.approximateLength += specifier.length + 10; // specifier + import("") + if (!nonRootParts || ts.isEntityName(nonRootParts)) { + if (nonRootParts) { + const lastId = ts.isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right; + lastId.typeArguments = undefined; + } + return ts.factory.createImportTypeNode(lit, assertion, nonRootParts as ts.EntityName, typeParameterNodes as readonly ts.TypeNode[], isTypeOf); } else { - const lastId = ts.isIdentifier(entityName) ? entityName : entityName.right; - const lastTypeArgs = lastId.typeArguments; - lastId.typeArguments = undefined; - return ts.factory.createTypeReferenceNode(entityName, lastTypeArgs as ts.NodeArray); + const splitNode = getTopmostIndexedAccessType(nonRootParts); + const qualifier = (splitNode.objectType as ts.TypeReferenceNode).typeName; + return ts.factory.createIndexedAccessTypeNode(ts.factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly ts.TypeNode[], isTypeOf), splitNode.indexType); } + } - function createAccessFromSymbolChain(chain: ts.Symbol[], index: number, stopper: number): ts.EntityName | ts.IndexedAccessTypeNode { - const typeParameterNodes = index === (chain.length - 1) ? overrideTypeArguments : lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; - const parent = chain[index - 1]; + const entityName = createAccessFromSymbolChain(chain, chain.length - 1, 0); + if (ts.isIndexedAccessTypeNode(entityName)) { + return entityName; // Indexed accesses can never be `typeof` + } + if (isTypeOf) { + return ts.factory.createTypeQueryNode(entityName); + } + else { + const lastId = ts.isIdentifier(entityName) ? entityName : entityName.right; + const lastTypeArgs = lastId.typeArguments; + lastId.typeArguments = undefined; + return ts.factory.createTypeReferenceNode(entityName, lastTypeArgs as ts.NodeArray); + } - let symbolName: string | undefined; - if (index === 0) { - context.flags |= ts.NodeBuilderFlags.InInitialEntityName; - symbolName = getNameOfSymbolAsWritten(symbol, context); - context.approximateLength += (symbolName ? symbolName.length : 0) + 1; - context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; - } - else { - if (parent && getExportsOfSymbol(parent)) { - const exports = getExportsOfSymbol(parent); - ts.forEachEntry(exports, (ex, name) => { - if (getSymbolIfSameReference(ex, symbol) && !isLateBoundName(name) && name !== ts.InternalSymbolName.ExportEquals) { - symbolName = ts.unescapeLeadingUnderscores(name); - return true; - } - }); - } - } + function createAccessFromSymbolChain(chain: ts.Symbol[], index: number, stopper: number): ts.EntityName | ts.IndexedAccessTypeNode { + const typeParameterNodes = index === (chain.length - 1) ? overrideTypeArguments : lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + const parent = chain[index - 1]; - if (symbolName === undefined) { - const name = ts.firstDefined(symbol.declarations, ts.getNameOfDeclaration); - if (name && ts.isComputedPropertyName(name) && ts.isEntityName(name.expression)) { - const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); - if (ts.isEntityName(LHS)) { - return ts.factory.createIndexedAccessTypeNode(ts.factory.createParenthesizedType(ts.factory.createTypeQueryNode(LHS)), ts.factory.createTypeQueryNode(name.expression)); + let symbolName: string | undefined; + if (index === 0) { + context.flags |= ts.NodeBuilderFlags.InInitialEntityName; + symbolName = getNameOfSymbolAsWritten(symbol, context); + context.approximateLength += (symbolName ? symbolName.length : 0) + 1; + context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; + } + else { + if (parent && getExportsOfSymbol(parent)) { + const exports = getExportsOfSymbol(parent); + ts.forEachEntry(exports, (ex, name) => { + if (getSymbolIfSameReference(ex, symbol) && !isLateBoundName(name) && name !== ts.InternalSymbolName.ExportEquals) { + symbolName = ts.unescapeLeadingUnderscores(name); + return true; } - return LHS; - } - symbolName = getNameOfSymbolAsWritten(symbol, context); + }); } - context.approximateLength += symbolName.length + 1; + } - if (!(context.flags & ts.NodeBuilderFlags.ForbidIndexedAccessSymbolReferences) && parent && - getMembersOfSymbol(parent) && getMembersOfSymbol(parent).get(symbol.escapedName) && - getSymbolIfSameReference(getMembersOfSymbol(parent).get(symbol.escapedName)!, symbol)) { - // Should use an indexed access + if (symbolName === undefined) { + const name = ts.firstDefined(symbol.declarations, ts.getNameOfDeclaration); + if (name && ts.isComputedPropertyName(name) && ts.isEntityName(name.expression)) { const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); - if (ts.isIndexedAccessTypeNode(LHS)) { - return ts.factory.createIndexedAccessTypeNode(LHS, ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(symbolName))); - } - else { - return ts.factory.createIndexedAccessTypeNode(ts.factory.createTypeReferenceNode(LHS, typeParameterNodes as readonly ts.TypeNode[]), ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(symbolName))); + if (ts.isEntityName(LHS)) { + return ts.factory.createIndexedAccessTypeNode(ts.factory.createParenthesizedType(ts.factory.createTypeQueryNode(LHS)), ts.factory.createTypeQueryNode(name.expression)); } + return LHS; } + symbolName = getNameOfSymbolAsWritten(symbol, context); + } + context.approximateLength += symbolName.length + 1; - const identifier = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; + if (!(context.flags & ts.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 (ts.isIndexedAccessTypeNode(LHS)) { + return ts.factory.createIndexedAccessTypeNode(LHS, ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(symbolName))); + } + else { + return ts.factory.createIndexedAccessTypeNode(ts.factory.createTypeReferenceNode(LHS, typeParameterNodes as readonly ts.TypeNode[]), ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(symbolName))); + } + } - if (index > stopper) { - const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); - if (!ts.isEntityName(LHS)) { - return ts.Debug.fail("Impossible construct - an export of an indexed access cannot be reachable"); - } - return ts.factory.createQualifiedName(LHS, identifier); + const identifier = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; + + if (index > stopper) { + const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); + if (!ts.isEntityName(LHS)) { + return ts.Debug.fail("Impossible construct - an export of an indexed access cannot be reachable"); } - return identifier; + return ts.factory.createQualifiedName(LHS, identifier); } + return identifier; } + } - function typeParameterShadowsNameInScope(escapedName: ts.__String, context: NodeBuilderContext, type: ts.TypeParameter) { - const result = resolveName(context.enclosingDeclaration, escapedName, ts.SymbolFlags.Type, /*nameNotFoundArg*/ undefined, escapedName, /*isUse*/ false); - if (result) { - if (result.flags & ts.SymbolFlags.TypeParameter && result === type.symbol) { - return false; - } - return true; + function typeParameterShadowsNameInScope(escapedName: ts.__String, context: NodeBuilderContext, type: ts.TypeParameter) { + const result = resolveName(context.enclosingDeclaration, escapedName, ts.SymbolFlags.Type, /*nameNotFoundArg*/ undefined, escapedName, /*isUse*/ false); + if (result) { + if (result.flags & ts.SymbolFlags.TypeParameter && result === type.symbol) { + return false; } - return false; + return true; } + return false; + } - function typeParameterToName(type: ts.TypeParameter, context: NodeBuilderContext) { - if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && context.typeParameterNames) { - const cached = context.typeParameterNames.get(getTypeId(type)); - if (cached) { - return cached; - } + function typeParameterToName(type: ts.TypeParameter, context: NodeBuilderContext) { + if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams && context.typeParameterNames) { + const cached = context.typeParameterNames.get(getTypeId(type)); + if (cached) { + return cached; } - let result = symbolToName(type.symbol, context, ts.SymbolFlags.Type, /*expectsIdentifier*/ true); - if (!(result.kind & ts.SyntaxKind.Identifier)) { - return ts.factory.createIdentifier("(Missing type parameter)"); + } + let result = symbolToName(type.symbol, context, ts.SymbolFlags.Type, /*expectsIdentifier*/ true); + if (!(result.kind & ts.SyntaxKind.Identifier)) { + return ts.factory.createIdentifier("(Missing type parameter)"); + } + if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { + const rawtext = result.escapedText as string; + let i = context.typeParameterNamesByTextNextNameCount?.get(rawtext) || 0; + let text = rawtext; + while (context.typeParameterNamesByText?.has(text) || typeParameterShadowsNameInScope(text as ts.__String, context, type)) { + i++; + text = `${rawtext}_${i}`; } - if (context.flags & ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { - const rawtext = result.escapedText as string; - let i = context.typeParameterNamesByTextNextNameCount?.get(rawtext) || 0; - let text = rawtext; - while (context.typeParameterNamesByText?.has(text) || typeParameterShadowsNameInScope(text as ts.__String, context, type)) { - i++; - text = `${rawtext}_${i}`; - } - if (text !== rawtext) { - result = ts.factory.createIdentifier(text, result.typeArguments); - } - // avoiding iterations of the above loop turns out to be worth it when `i` starts to get large, so we cache the max - // `i` we've used thus far, to save work later - (context.typeParameterNamesByTextNextNameCount ||= new ts.Map()).set(rawtext, i); - (context.typeParameterNames ||= new ts.Map()).set(getTypeId(type), result); - (context.typeParameterNamesByText ||= new ts.Set()).add(rawtext); + if (text !== rawtext) { + result = ts.factory.createIdentifier(text, result.typeArguments); } - return result; + // avoiding iterations of the above loop turns out to be worth it when `i` starts to get large, so we cache the max + // `i` we've used thus far, to save work later + (context.typeParameterNamesByTextNextNameCount ||= new ts.Map()).set(rawtext, i); + (context.typeParameterNames ||= new ts.Map()).set(getTypeId(type), result); + (context.typeParameterNamesByText ||= new ts.Set()).add(rawtext); } + return result; + } - function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: true): ts.Identifier; - function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: false): ts.EntityName; - function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: boolean): ts.EntityName { - const chain = lookupSymbolChain(symbol, context, meaning); + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: true): ts.Identifier; + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: false): ts.EntityName; + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags, expectsIdentifier: boolean): ts.EntityName { + const chain = lookupSymbolChain(symbol, context, meaning); - if (expectsIdentifier && chain.length !== 1 - && !context.encounteredError - && !(context.flags & ts.NodeBuilderFlags.AllowQualifiedNameInPlaceOfIdentifier)) { - context.encounteredError = true; + if (expectsIdentifier && chain.length !== 1 + && !context.encounteredError + && !(context.flags & ts.NodeBuilderFlags.AllowQualifiedNameInPlaceOfIdentifier)) { + context.encounteredError = true; + } + return createEntityNameFromSymbolChain(chain, chain.length - 1); + + function createEntityNameFromSymbolChain(chain: ts.Symbol[], index: number): ts.EntityName { + const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + + if (index === 0) { + context.flags |= ts.NodeBuilderFlags.InInitialEntityName; + } + const symbolName = getNameOfSymbolAsWritten(symbol, context); + if (index === 0) { + context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; } - return createEntityNameFromSymbolChain(chain, chain.length - 1); - function createEntityNameFromSymbolChain(chain: ts.Symbol[], index: number): ts.EntityName { - const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; + const identifier = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; - if (index === 0) { - context.flags |= ts.NodeBuilderFlags.InInitialEntityName; - } - const symbolName = getNameOfSymbolAsWritten(symbol, context); - if (index === 0) { - context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; - } + return index > 0 ? ts.factory.createQualifiedName(createEntityNameFromSymbolChain(chain, index - 1), identifier) : identifier; + } + } + + function symbolToExpression(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags) { + const chain = lookupSymbolChain(symbol, context, meaning); + return createExpressionFromSymbolChain(chain, chain.length - 1); + + function createExpressionFromSymbolChain(chain: ts.Symbol[], index: number): ts.Expression { + const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + + if (index === 0) { + context.flags |= ts.NodeBuilderFlags.InInitialEntityName; + } + let symbolName = getNameOfSymbolAsWritten(symbol, context); + if (index === 0) { + context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; + } + let firstChar = symbolName.charCodeAt(0); + + if (ts.isSingleOrDoubleQuote(firstChar) && ts.some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + return ts.factory.createStringLiteral(getSpecifierForModuleSymbol(symbol, context)); + } + const canUsePropertyAccess = firstChar === ts.CharacterCodes.hash ? + symbolName.length > 1 && ts.isIdentifierStart(symbolName.charCodeAt(1), languageVersion) : + ts.isIdentifierStart(firstChar, languageVersion); + if (index === 0 || canUsePropertyAccess) { const identifier = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); identifier.symbol = symbol; - return index > 0 ? ts.factory.createQualifiedName(createEntityNameFromSymbolChain(chain, index - 1), identifier) : identifier; + return index > 0 ? ts.factory.createPropertyAccessExpression(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; + } + else { + if (firstChar === ts.CharacterCodes.openBracket) { + symbolName = symbolName.substring(1, symbolName.length - 1); + firstChar = symbolName.charCodeAt(0); + } + let expression: ts.Expression | undefined; + if (ts.isSingleOrDoubleQuote(firstChar) && !(symbol.flags & ts.SymbolFlags.EnumMember)) { + expression = ts.factory.createStringLiteral(ts.stripQuotes(symbolName).replace(/\\./g, s => s.substring(1)), firstChar === ts.CharacterCodes.singleQuote); + } + else if (("" + +symbolName) === symbolName) { + expression = ts.factory.createNumericLiteral(+symbolName); + } + if (!expression) { + expression = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); + expression.symbol = symbol; + } + return ts.factory.createElementAccessExpression(createExpressionFromSymbolChain(chain, index - 1), expression); } } + } - function symbolToExpression(symbol: ts.Symbol, context: NodeBuilderContext, meaning: ts.SymbolFlags) { - const chain = lookupSymbolChain(symbol, context, meaning); + function isStringNamed(d: ts.Declaration) { + const name = ts.getNameOfDeclaration(d); + return !!name && ts.isStringLiteral(name); + } - return createExpressionFromSymbolChain(chain, chain.length - 1); + function isSingleQuotedStringNamed(d: ts.Declaration) { + const name = ts.getNameOfDeclaration(d); + return !!(name && ts.isStringLiteral(name) && (name.singleQuote || !ts.nodeIsSynthesized(name) && ts.startsWith(ts.getTextOfNode(name, /*includeTrivia*/ false), "'"))); + } - function createExpressionFromSymbolChain(chain: ts.Symbol[], index: number): ts.Expression { - const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; + function getPropertyNameNodeForSymbol(symbol: ts.Symbol, context: NodeBuilderContext) { + const singleQuote = !!ts.length(symbol.declarations) && ts.every(symbol.declarations, isSingleQuotedStringNamed); + const fromNameType = getPropertyNameNodeForSymbolFromNameType(symbol, context, singleQuote); + if (fromNameType) { + return fromNameType; + } + const rawName = ts.unescapeLeadingUnderscores(symbol.escapedName); + const stringNamed = !!ts.length(symbol.declarations) && ts.every(symbol.declarations, isStringNamed); + return ts.createPropertyNameNodeForIdentifierOrLiteral(rawName, ts.getEmitScriptTarget(compilerOptions), singleQuote, stringNamed); + } - if (index === 0) { - context.flags |= ts.NodeBuilderFlags.InInitialEntityName; + // 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 & ts.TypeFlags.StringOrNumberLiteral) { + const name = "" + (nameType as ts.StringLiteralType | ts.NumberLiteralType).value; + if (!ts.isIdentifierText(name, ts.getEmitScriptTarget(compilerOptions)) && !ts.isNumericLiteralName(name)) { + return ts.factory.createStringLiteral(name, !!singleQuote); } - let symbolName = getNameOfSymbolAsWritten(symbol, context); - if (index === 0) { - context.flags ^= ts.NodeBuilderFlags.InInitialEntityName; + if (ts.isNumericLiteralName(name) && ts.startsWith(name, "-")) { + return ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(+name)); } - let firstChar = symbolName.charCodeAt(0); + return ts.createPropertyNameNodeForIdentifierOrLiteral(name, ts.getEmitScriptTarget(compilerOptions)); + } + if (nameType.flags & ts.TypeFlags.UniqueESSymbol) { + return ts.factory.createComputedPropertyName(symbolToExpression((nameType as ts.UniqueESSymbolType).symbol, context, ts.SymbolFlags.Value)); + } + } + } - if (ts.isSingleOrDoubleQuote(firstChar) && ts.some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - return ts.factory.createStringLiteral(getSpecifierForModuleSymbol(symbol, context)); - } - const canUsePropertyAccess = firstChar === ts.CharacterCodes.hash ? - symbolName.length > 1 && ts.isIdentifierStart(symbolName.charCodeAt(1), languageVersion) : - ts.isIdentifierStart(firstChar, languageVersion); - if (index === 0 || canUsePropertyAccess) { - const identifier = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; + 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 = new ts.Map(initial.typeParameterNames); + } + if (initial.typeParameterNamesByText) { + initial.typeParameterNamesByText = new ts.Set(initial.typeParameterNamesByText); + } + if (initial.typeParameterSymbolList) { + initial.typeParameterSymbolList = new ts.Set(initial.typeParameterSymbolList); + } + initial.tracker = wrapSymbolTrackerToReportForContext(initial, initial.tracker); + return initial; + } - return index > 0 ? ts.factory.createPropertyAccessExpression(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; - } - else { - if (firstChar === ts.CharacterCodes.openBracket) { - symbolName = symbolName.substring(1, symbolName.length - 1); - firstChar = symbolName.charCodeAt(0); - } - let expression: ts.Expression | undefined; - if (ts.isSingleOrDoubleQuote(firstChar) && !(symbol.flags & ts.SymbolFlags.EnumMember)) { - expression = ts.factory.createStringLiteral(ts.stripQuotes(symbolName).replace(/\\./g, s => s.substring(1)), firstChar === ts.CharacterCodes.singleQuote); - } - else if (("" + +symbolName) === symbolName) { - expression = ts.factory.createNumericLiteral(+symbolName); - } - if (!expression) { - expression = ts.setEmitFlags(ts.factory.createIdentifier(symbolName, typeParameterNodes), ts.EmitFlags.NoAsciiEscaping); - expression.symbol = symbol; + + function getDeclarationWithTypeAnnotation(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined) { + return symbol.declarations && ts.find(symbol.declarations, s => !!ts.getEffectiveTypeAnnotationNode(s) && (!enclosingDeclaration || !!ts.findAncestor(s, n => n === enclosingDeclaration))); + } + + function existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(existing: ts.TypeNode, type: ts.Type) { + return !(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) || !ts.isTypeReferenceNode(existing) || ts.length(existing.typeArguments) >= getMinTypeArgumentCount((type as ts.TypeReference).target.typeParameters); + } + + /** + * 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(context: NodeBuilderContext, type: ts.Type, symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { + if (!isErrorType(type) && enclosingDeclaration) { + const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol, enclosingDeclaration); + if (declWithExistingAnnotation && !ts.isFunctionLikeDeclaration(declWithExistingAnnotation) && !ts.isGetAccessorDeclaration(declWithExistingAnnotation)) { + // try to reuse the existing annotation + const existing = ts.getEffectiveTypeAnnotationNode(declWithExistingAnnotation)!; + if (typeNodeIsEquivalentToType(existing, declWithExistingAnnotation, type) && existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(existing, type)) { + const result = serializeExistingTypeNode(context, existing, includePrivateSymbol, bundled); + if (result) { + return result; } - return ts.factory.createElementAccessExpression(createExpressionFromSymbolChain(chain, index - 1), expression); } } } - - function isStringNamed(d: ts.Declaration) { - const name = ts.getNameOfDeclaration(d); - return !!name && ts.isStringLiteral(name); + const oldFlags = context.flags; + if (type.flags & ts.TypeFlags.UniqueESSymbol && + type.symbol === symbol && (!context.enclosingDeclaration || ts.some(symbol.declarations, d => ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(context.enclosingDeclaration!)))) { + context.flags |= ts.NodeBuilderFlags.AllowUniqueESSymbolType; } + const result = typeToTypeNodeHelper(type, context); + context.flags = oldFlags; + return result; + } - function isSingleQuotedStringNamed(d: ts.Declaration) { - const name = ts.getNameOfDeclaration(d); - return !!(name && ts.isStringLiteral(name) && (name.singleQuote || !ts.nodeIsSynthesized(name) && ts.startsWith(ts.getTextOfNode(name, /*includeTrivia*/ false), "'"))); + function typeNodeIsEquivalentToType(typeNode: ts.TypeNode, annotatedDeclaration: ts.Declaration, type: ts.Type) { + const typeFromTypeNode = getTypeFromTypeNode(typeNode); + if (typeFromTypeNode === type) { + return true; } - - function getPropertyNameNodeForSymbol(symbol: ts.Symbol, context: NodeBuilderContext) { - const singleQuote = !!ts.length(symbol.declarations) && ts.every(symbol.declarations, isSingleQuotedStringNamed); - const fromNameType = getPropertyNameNodeForSymbolFromNameType(symbol, context, singleQuote); - if (fromNameType) { - return fromNameType; - } - const rawName = ts.unescapeLeadingUnderscores(symbol.escapedName); - const stringNamed = !!ts.length(symbol.declarations) && ts.every(symbol.declarations, isStringNamed); - return ts.createPropertyNameNodeForIdentifierOrLiteral(rawName, ts.getEmitScriptTarget(compilerOptions), singleQuote, stringNamed); + if (ts.isParameter(annotatedDeclaration) && annotatedDeclaration.questionToken) { + return getTypeWithFacts(type, TypeFacts.NEUndefined) === typeFromTypeNode; } + return false; + } - // 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 & ts.TypeFlags.StringOrNumberLiteral) { - const name = "" + (nameType as ts.StringLiteralType | ts.NumberLiteralType).value; - if (!ts.isIdentifierText(name, ts.getEmitScriptTarget(compilerOptions)) && !ts.isNumericLiteralName(name)) { - return ts.factory.createStringLiteral(name, !!singleQuote); - } - if (ts.isNumericLiteralName(name) && ts.startsWith(name, "-")) { - return ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(+name)); + function serializeReturnTypeForSignature(context: NodeBuilderContext, type: ts.Type, signature: ts.Signature, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { + if (!isErrorType(type) && context.enclosingDeclaration) { + const annotation = signature.declaration && ts.getEffectiveReturnTypeNode(signature.declaration); + if (!!ts.findAncestor(annotation, n => n === context.enclosingDeclaration) && annotation) { + const annotated = getTypeFromTypeNode(annotation); + const thisInstantiated = annotated.flags & ts.TypeFlags.TypeParameter && (annotated as ts.TypeParameter).isThisType ? instantiateType(annotated, signature.mapper) : annotated; + if (thisInstantiated === type && existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(annotation, type)) { + const result = serializeExistingTypeNode(context, annotation, includePrivateSymbol, bundled); + if (result) { + return result; } - return ts.createPropertyNameNodeForIdentifierOrLiteral(name, ts.getEmitScriptTarget(compilerOptions)); - } - if (nameType.flags & ts.TypeFlags.UniqueESSymbol) { - return ts.factory.createComputedPropertyName(symbolToExpression((nameType as ts.UniqueESSymbolType).symbol, context, ts.SymbolFlags.Value)); } } } + return typeToTypeNodeHelper(type, context); + } - 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 = new ts.Map(initial.typeParameterNames); + function trackExistingEntityName(node: T, context: NodeBuilderContext, includePrivateSymbol?: (s: ts.Symbol) => void) { + let introducesError = false; + const leftmost = ts.getFirstIdentifier(node); + if (ts.isInJSFile(node) && (ts.isExportsIdentifier(leftmost) || ts.isModuleExportsAccessExpression(leftmost.parent) || (ts.isQualifiedName(leftmost.parent) && ts.isModuleIdentifier(leftmost.parent.left) && ts.isExportsIdentifier(leftmost.parent.right)))) { + introducesError = true; + return { introducesError, node }; + } + const sym = resolveEntityName(leftmost, ts.SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveALias*/ true); + if (sym) { + if (isSymbolAccessible(sym, context.enclosingDeclaration, ts.SymbolFlags.All, /*shouldComputeAliasesToMakeVisible*/ false).accessibility !== ts.SymbolAccessibility.Accessible) { + introducesError = true; } - if (initial.typeParameterNamesByText) { - initial.typeParameterNamesByText = new ts.Set(initial.typeParameterNamesByText); + else { + context.tracker?.trackSymbol?.(sym, context.enclosingDeclaration, ts.SymbolFlags.All); + includePrivateSymbol?.(sym); } - if (initial.typeParameterSymbolList) { - initial.typeParameterSymbolList = new ts.Set(initial.typeParameterSymbolList); + if (ts.isIdentifier(node)) { + const type = getDeclaredTypeOfSymbol(sym); + const name = sym.flags & ts.SymbolFlags.TypeParameter && !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration) ? typeParameterToName(type, context) : ts.factory.cloneNode(node); + name.symbol = sym; // for quickinfo, which uses identifier symbol information + return { introducesError, node: ts.setEmitFlags(ts.setOriginalNode(name, node), ts.EmitFlags.NoAsciiEscaping) }; } - initial.tracker = wrapSymbolTrackerToReportForContext(initial, initial.tracker); - return initial; } + return { introducesError, node }; + } - function getDeclarationWithTypeAnnotation(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined) { - return symbol.declarations && ts.find(symbol.declarations, s => !!ts.getEffectiveTypeAnnotationNode(s) && (!enclosingDeclaration || !!ts.findAncestor(s, n => n === enclosingDeclaration))); + function serializeExistingTypeNode(context: NodeBuilderContext, existing: ts.TypeNode, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { + if (cancellationToken && cancellationToken.throwIfCancellationRequested) { + cancellationToken.throwIfCancellationRequested(); } - - function existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(existing: ts.TypeNode, type: ts.Type) { - return !(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) || !ts.isTypeReferenceNode(existing) || ts.length(existing.typeArguments) >= getMinTypeArgumentCount((type as ts.TypeReference).target.typeParameters); + let hadError = false; + const file = ts.getSourceFileOfNode(existing); + const transformed = ts.visitNode(existing, visitExistingNodeTreeSymbols); + if (hadError) { + return undefined; } - - /** - * 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(context: NodeBuilderContext, type: ts.Type, symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { - if (!isErrorType(type) && enclosingDeclaration) { - const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol, enclosingDeclaration); - if (declWithExistingAnnotation && !ts.isFunctionLikeDeclaration(declWithExistingAnnotation) && !ts.isGetAccessorDeclaration(declWithExistingAnnotation)) { - // try to reuse the existing annotation - const existing = ts.getEffectiveTypeAnnotationNode(declWithExistingAnnotation)!; - if (typeNodeIsEquivalentToType(existing, declWithExistingAnnotation, type) && existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(existing, type)) { - const result = serializeExistingTypeNode(context, existing, includePrivateSymbol, bundled); - if (result) { - return result; - } - } - } + return transformed === existing ? ts.setTextRange(ts.factory.cloneNode(existing), existing) : transformed; + function visitExistingNodeTreeSymbols(node: T): ts.Node { + // We don't _actually_ support jsdoc namepath types, emit `any` instead + if (ts.isJSDocAllType(node) || node.kind === ts.SyntaxKind.JSDocNamepathType) { + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); } - const oldFlags = context.flags; - if (type.flags & ts.TypeFlags.UniqueESSymbol && - type.symbol === symbol && (!context.enclosingDeclaration || ts.some(symbol.declarations, d => ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(context.enclosingDeclaration!)))) { - context.flags |= ts.NodeBuilderFlags.AllowUniqueESSymbolType; + if (ts.isJSDocUnknownType(node)) { + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); } - const result = typeToTypeNodeHelper(type, context); - context.flags = oldFlags; - return result; - } - - function typeNodeIsEquivalentToType(typeNode: ts.TypeNode, annotatedDeclaration: ts.Declaration, type: ts.Type) { - const typeFromTypeNode = getTypeFromTypeNode(typeNode); - if (typeFromTypeNode === type) { - return true; + if (ts.isJSDocNullableType(node)) { + return ts.factory.createUnionTypeNode([ts.visitNode(node.type, visitExistingNodeTreeSymbols), ts.factory.createLiteralTypeNode(ts.factory.createNull())]); } - if (ts.isParameter(annotatedDeclaration) && annotatedDeclaration.questionToken) { - return getTypeWithFacts(type, TypeFacts.NEUndefined) === typeFromTypeNode; + if (ts.isJSDocOptionalType(node)) { + return ts.factory.createUnionTypeNode([ts.visitNode(node.type, visitExistingNodeTreeSymbols), ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)]); } - return false; - } - - function serializeReturnTypeForSignature(context: NodeBuilderContext, type: ts.Type, signature: ts.Signature, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { - if (!isErrorType(type) && context.enclosingDeclaration) { - const annotation = signature.declaration && ts.getEffectiveReturnTypeNode(signature.declaration); - if (!!ts.findAncestor(annotation, n => n === context.enclosingDeclaration) && annotation) { - const annotated = getTypeFromTypeNode(annotation); - const thisInstantiated = annotated.flags & ts.TypeFlags.TypeParameter && (annotated as ts.TypeParameter).isThisType ? instantiateType(annotated, signature.mapper) : annotated; - if (thisInstantiated === type && existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(annotation, type)) { - const result = serializeExistingTypeNode(context, annotation, includePrivateSymbol, bundled); - if (result) { - return result; - } - } - } + if (ts.isJSDocNonNullableType(node)) { + return ts.visitNode(node.type, visitExistingNodeTreeSymbols); } - return typeToTypeNodeHelper(type, context); - } + if (ts.isJSDocVariadicType(node)) { + return ts.factory.createArrayTypeNode(ts.visitNode((node as ts.JSDocVariadicType).type, visitExistingNodeTreeSymbols)); + } + if (ts.isJSDocTypeLiteral(node)) { + return ts.factory.createTypeLiteralNode(ts.map(node.jsDocPropertyTags, t => { + const name = ts.isIdentifier(t.name) ? t.name : t.name.right; + const typeViaParent = getTypeOfPropertyOfType(getTypeFromTypeNode(node), name.escapedText); + const overrideTypeNode = typeViaParent && t.typeExpression && getTypeFromTypeNode(t.typeExpression.type) !== typeViaParent ? typeToTypeNodeHelper(typeViaParent, context) : undefined; - function trackExistingEntityName(node: T, context: NodeBuilderContext, includePrivateSymbol?: (s: ts.Symbol) => void) { - let introducesError = false; - const leftmost = ts.getFirstIdentifier(node); - if (ts.isInJSFile(node) && (ts.isExportsIdentifier(leftmost) || ts.isModuleExportsAccessExpression(leftmost.parent) || (ts.isQualifiedName(leftmost.parent) && ts.isModuleIdentifier(leftmost.parent.left) && ts.isExportsIdentifier(leftmost.parent.right)))) { - introducesError = true; - return { introducesError, node }; + return ts.factory.createPropertySignature( + /*modifiers*/ undefined, name, t.isBracketed || t.typeExpression && ts.isJSDocOptionalType(t.typeExpression.type) ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, overrideTypeNode || (t.typeExpression && ts.visitNode(t.typeExpression.type, visitExistingNodeTreeSymbols)) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + })); + } + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "") { + return ts.setOriginalNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), node); + } + if ((ts.isExpressionWithTypeArguments(node) || ts.isTypeReferenceNode(node)) && ts.isJSDocIndexSignature(node)) { + return ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature( + /*decorators*/ undefined, + /*modifiers*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotdotdotToken*/ undefined, "x", + /*questionToken*/ undefined, ts.visitNode(node.typeArguments![0], visitExistingNodeTreeSymbols))], ts.visitNode(node.typeArguments![1], visitExistingNodeTreeSymbols))]); } - const sym = resolveEntityName(leftmost, ts.SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveALias*/ true); - if (sym) { - if (isSymbolAccessible(sym, context.enclosingDeclaration, ts.SymbolFlags.All, /*shouldComputeAliasesToMakeVisible*/ false).accessibility !== ts.SymbolAccessibility.Accessible) { - introducesError = true; + if (ts.isJSDocFunctionType(node)) { + if (ts.isJSDocConstructSignature(node)) { + let newTypeNode: ts.TypeNode | undefined; + return ts.factory.createConstructorTypeNode(node.modifiers, ts.visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), ts.mapDefined(node.parameters, (p, i) => p.name && ts.isIdentifier(p.name) && p.name.escapedText === "new" ? (newTypeNode = p.type, undefined) : ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, getEffectiveDotDotDotForParameter(p), getNameForJSDocFunctionParameter(p, i), p.questionToken, ts.visitNode(p.type, visitExistingNodeTreeSymbols), + /*initializer*/ undefined)), ts.visitNode(newTypeNode || node.type, visitExistingNodeTreeSymbols) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); } else { - context.tracker?.trackSymbol?.(sym, context.enclosingDeclaration, ts.SymbolFlags.All); - includePrivateSymbol?.(sym); + return ts.factory.createFunctionTypeNode(ts.visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), ts.map(node.parameters, (p, i) => ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, getEffectiveDotDotDotForParameter(p), getNameForJSDocFunctionParameter(p, i), p.questionToken, ts.visitNode(p.type, visitExistingNodeTreeSymbols), + /*initializer*/ undefined)), ts.visitNode(node.type, visitExistingNodeTreeSymbols) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + } + } + if (ts.isTypeReferenceNode(node) && ts.isInJSDoc(node) && (!existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(node, getTypeFromTypeNode(node)) || getIntendedTypeFromJSDocTypeReference(node) || unknownSymbol === resolveTypeReferenceName(node, ts.SymbolFlags.Type, /*ignoreErrors*/ true))) { + return ts.setOriginalNode(typeToTypeNodeHelper(getTypeFromTypeNode(node), context), node); + } + if (ts.isLiteralImportTypeNode(node)) { + const nodeSymbol = getNodeLinks(node).resolvedSymbol; + if (ts.isInJSDoc(node) && + nodeSymbol && + ( + // The import type resolved using jsdoc fallback logic + (!node.isTypeOf && !(nodeSymbol.flags & ts.SymbolFlags.Type)) || + // The import type had type arguments autofilled by js fallback logic + !(ts.length(node.typeArguments) >= getMinTypeArgumentCount(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(nodeSymbol))))) { + return ts.setOriginalNode(typeToTypeNodeHelper(getTypeFromTypeNode(node), context), node); } - if (ts.isIdentifier(node)) { - const type = getDeclaredTypeOfSymbol(sym); - const name = sym.flags & ts.SymbolFlags.TypeParameter && !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration) ? typeParameterToName(type, context) : ts.factory.cloneNode(node); - name.symbol = sym; // for quickinfo, which uses identifier symbol information - return { introducesError, node: ts.setEmitFlags(ts.setOriginalNode(name, node), ts.EmitFlags.NoAsciiEscaping) }; + return ts.factory.updateImportTypeNode(node, ts.factory.updateLiteralTypeNode(node.argument, rewriteModuleSpecifier(node, node.argument.literal)), node.qualifier, ts.visitNodes(node.typeArguments, visitExistingNodeTreeSymbols, ts.isTypeNode), node.isTypeOf); + } + + if (ts.isEntityName(node) || ts.isEntityNameExpression(node)) { + const { introducesError, node: result } = trackExistingEntityName(node, context, includePrivateSymbol); + hadError = hadError || introducesError; + if (result !== node) { + return result; } } - return { introducesError, node }; - } + if (file && ts.isTupleTypeNode(node) && (ts.getLineAndCharacterOfPosition(file, node.pos).line === ts.getLineAndCharacterOfPosition(file, node.end).line)) { + ts.setEmitFlags(node, ts.EmitFlags.SingleLine); + } - function serializeExistingTypeNode(context: NodeBuilderContext, existing: ts.TypeNode, includePrivateSymbol?: (s: ts.Symbol) => void, bundled?: boolean) { - if (cancellationToken && cancellationToken.throwIfCancellationRequested) { - cancellationToken.throwIfCancellationRequested(); + return ts.visitEachChild(node, visitExistingNodeTreeSymbols, ts.nullTransformationContext); + function getEffectiveDotDotDotForParameter(p: ts.ParameterDeclaration) { + return p.dotDotDotToken || (p.type && ts.isJSDocVariadicType(p.type) ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined); } - let hadError = false; - const file = ts.getSourceFileOfNode(existing); - const transformed = ts.visitNode(existing, visitExistingNodeTreeSymbols); - if (hadError) { - return undefined; + + /** Note that `new:T` parameters are not handled, but should be before calling this function. */ + function getNameForJSDocFunctionParameter(p: ts.ParameterDeclaration, index: number) { + return p.name && ts.isIdentifier(p.name) && p.name.escapedText === "this" ? "this" + : getEffectiveDotDotDotForParameter(p) ? `args` + : `arg${index}`; } - return transformed === existing ? ts.setTextRange(ts.factory.cloneNode(existing), existing) : transformed; - function visitExistingNodeTreeSymbols(node: T): ts.Node { - // We don't _actually_ support jsdoc namepath types, emit `any` instead - if (ts.isJSDocAllType(node) || node.kind === ts.SyntaxKind.JSDocNamepathType) { - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } - if (ts.isJSDocUnknownType(node)) { - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); - } - if (ts.isJSDocNullableType(node)) { - return ts.factory.createUnionTypeNode([ts.visitNode(node.type, visitExistingNodeTreeSymbols), ts.factory.createLiteralTypeNode(ts.factory.createNull())]); - } - if (ts.isJSDocOptionalType(node)) { - return ts.factory.createUnionTypeNode([ts.visitNode(node.type, visitExistingNodeTreeSymbols), ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)]); - } - if (ts.isJSDocNonNullableType(node)) { - return ts.visitNode(node.type, visitExistingNodeTreeSymbols); - } - if (ts.isJSDocVariadicType(node)) { - return ts.factory.createArrayTypeNode(ts.visitNode((node as ts.JSDocVariadicType).type, visitExistingNodeTreeSymbols)); - } - if (ts.isJSDocTypeLiteral(node)) { - return ts.factory.createTypeLiteralNode(ts.map(node.jsDocPropertyTags, t => { - const name = ts.isIdentifier(t.name) ? t.name : t.name.right; - const typeViaParent = getTypeOfPropertyOfType(getTypeFromTypeNode(node), name.escapedText); - const overrideTypeNode = typeViaParent && t.typeExpression && getTypeFromTypeNode(t.typeExpression.type) !== typeViaParent ? typeToTypeNodeHelper(typeViaParent, context) : undefined; - return ts.factory.createPropertySignature( - /*modifiers*/ undefined, name, t.isBracketed || t.typeExpression && ts.isJSDocOptionalType(t.typeExpression.type) ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, overrideTypeNode || (t.typeExpression && ts.visitNode(t.typeExpression.type, visitExistingNodeTreeSymbols)) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); - })); - } - if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "") { - return ts.setOriginalNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), node); + function rewriteModuleSpecifier(parent: ts.ImportTypeNode, lit: ts.StringLiteral) { + if (bundled) { + if (context.tracker && context.tracker.moduleResolverHost) { + const targetFile = getExternalModuleFileFromDeclaration(parent); + if (targetFile) { + const getCanonicalFileName = ts.createGetCanonicalFileName(!!host.useCaseSensitiveFileNames); + const resolverHost = { + getCanonicalFileName, + getCurrentDirectory: () => context.tracker.moduleResolverHost!.getCurrentDirectory(), + getCommonSourceDirectory: () => context.tracker.moduleResolverHost!.getCommonSourceDirectory() + }; + const newName = ts.getResolvedExternalModuleName(resolverHost, targetFile); + return ts.factory.createStringLiteral(newName); + } + } } - if ((ts.isExpressionWithTypeArguments(node) || ts.isTypeReferenceNode(node)) && ts.isJSDocIndexSignature(node)) { - return ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature( - /*decorators*/ undefined, - /*modifiers*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotdotdotToken*/ undefined, "x", - /*questionToken*/ undefined, ts.visitNode(node.typeArguments![0], visitExistingNodeTreeSymbols))], ts.visitNode(node.typeArguments![1], visitExistingNodeTreeSymbols))]); - } - if (ts.isJSDocFunctionType(node)) { - if (ts.isJSDocConstructSignature(node)) { - let newTypeNode: ts.TypeNode | undefined; - return ts.factory.createConstructorTypeNode(node.modifiers, ts.visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), ts.mapDefined(node.parameters, (p, i) => p.name && ts.isIdentifier(p.name) && p.name.escapedText === "new" ? (newTypeNode = p.type, undefined) : ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, getEffectiveDotDotDotForParameter(p), getNameForJSDocFunctionParameter(p, i), p.questionToken, ts.visitNode(p.type, visitExistingNodeTreeSymbols), - /*initializer*/ undefined)), ts.visitNode(newTypeNode || node.type, visitExistingNodeTreeSymbols) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); - } - else { - return ts.factory.createFunctionTypeNode(ts.visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), ts.map(node.parameters, (p, i) => ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, getEffectiveDotDotDotForParameter(p), getNameForJSDocFunctionParameter(p, i), p.questionToken, ts.visitNode(p.type, visitExistingNodeTreeSymbols), - /*initializer*/ undefined)), ts.visitNode(node.type, visitExistingNodeTreeSymbols) || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); - } - } - if (ts.isTypeReferenceNode(node) && ts.isInJSDoc(node) && (!existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(node, getTypeFromTypeNode(node)) || getIntendedTypeFromJSDocTypeReference(node) || unknownSymbol === resolveTypeReferenceName(node, ts.SymbolFlags.Type, /*ignoreErrors*/ true))) { - return ts.setOriginalNode(typeToTypeNodeHelper(getTypeFromTypeNode(node), context), node); - } - if (ts.isLiteralImportTypeNode(node)) { - const nodeSymbol = getNodeLinks(node).resolvedSymbol; - if (ts.isInJSDoc(node) && - nodeSymbol && - ( - // The import type resolved using jsdoc fallback logic - (!node.isTypeOf && !(nodeSymbol.flags & ts.SymbolFlags.Type)) || - // The import type had type arguments autofilled by js fallback logic - !(ts.length(node.typeArguments) >= getMinTypeArgumentCount(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(nodeSymbol))))) { - return ts.setOriginalNode(typeToTypeNodeHelper(getTypeFromTypeNode(node), context), node); - } - return ts.factory.updateImportTypeNode(node, ts.factory.updateLiteralTypeNode(node.argument, rewriteModuleSpecifier(node, node.argument.literal)), node.qualifier, ts.visitNodes(node.typeArguments, visitExistingNodeTreeSymbols, ts.isTypeNode), node.isTypeOf); - } - - if (ts.isEntityName(node) || ts.isEntityNameExpression(node)) { - const { introducesError, node: result } = trackExistingEntityName(node, context, includePrivateSymbol); - hadError = hadError || introducesError; - if (result !== node) { - return result; + else { + if (context.tracker && context.tracker.trackExternalModuleSymbolOfImportTypeNode) { + const moduleSym = resolveExternalModuleNameWorker(lit, lit, /*moduleNotFoundError*/ undefined); + if (moduleSym) { + context.tracker.trackExternalModuleSymbolOfImportTypeNode(moduleSym); + } } } + return lit; + } + } + } - if (file && ts.isTupleTypeNode(node) && (ts.getLineAndCharacterOfPosition(file, node.pos).line === ts.getLineAndCharacterOfPosition(file, node.end).line)) { - ts.setEmitFlags(node, ts.EmitFlags.SingleLine); - } - - return ts.visitEachChild(node, visitExistingNodeTreeSymbols, ts.nullTransformationContext); - function getEffectiveDotDotDotForParameter(p: ts.ParameterDeclaration) { - return p.dotDotDotToken || (p.type && ts.isJSDocVariadicType(p.type) ? ts.factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined); - } + function symbolTableToDeclarationStatements(symbolTable: ts.SymbolTable, context: NodeBuilderContext, bundled?: boolean): ts.Statement[] { + const serializePropertySymbolForClass = makeSerializePropertySymbol(ts.factory.createPropertyDeclaration, ts.SyntaxKind.MethodDeclaration, /*useAcessors*/ true); + const serializePropertySymbolForInterfaceWorker = makeSerializePropertySymbol((_decorators, mods, name, question, type) => ts.factory.createPropertySignature(mods, name, question, type), ts.SyntaxKind.MethodSignature, /*useAcessors*/ false); - /** Note that `new:T` parameters are not handled, but should be before calling this function. */ - function getNameForJSDocFunctionParameter(p: ts.ParameterDeclaration, index: number) { - return p.name && ts.isIdentifier(p.name) && p.name.escapedText === "this" ? "this" - : getEffectiveDotDotDotForParameter(p) ? `args` - : `arg${index}`; - } + // TODO: Use `setOriginalNode` on original declaration names where possible so these declarations see some kind of + // declaration mapping - function rewriteModuleSpecifier(parent: ts.ImportTypeNode, lit: ts.StringLiteral) { - if (bundled) { - if (context.tracker && context.tracker.moduleResolverHost) { - const targetFile = getExternalModuleFileFromDeclaration(parent); - if (targetFile) { - const getCanonicalFileName = ts.createGetCanonicalFileName(!!host.useCaseSensitiveFileNames); - const resolverHost = { - getCanonicalFileName, - getCurrentDirectory: () => context.tracker.moduleResolverHost!.getCurrentDirectory(), - getCommonSourceDirectory: () => context.tracker.moduleResolverHost!.getCommonSourceDirectory() - }; - const newName = ts.getResolvedExternalModuleName(resolverHost, targetFile); - return ts.factory.createStringLiteral(newName); - } + // 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: ts.Statement[] = []; + const visitedSymbols = new ts.Set(); + const deferredPrivatesStack: ts.ESMap[] = []; + const oldcontext = context; + context = { + ...oldcontext, + usedSymbolNames: new ts.Set(oldcontext.usedSymbolNames), + remappedSymbolNames: new ts.Map(), + tracker: { + ...oldcontext.tracker, + trackSymbol: (sym, decl, meaning) => { + const accessibleResult = isSymbolAccessible(sym, decl, meaning, /*computeAliases*/ false); + if (accessibleResult.accessibility === ts.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 & ts.SymbolFlags.Property)) { + includePrivateSymbol(chain[0]); } } - else { - if (context.tracker && context.tracker.trackExternalModuleSymbolOfImportTypeNode) { - const moduleSym = resolveExternalModuleNameWorker(lit, lit, /*moduleNotFoundError*/ undefined); - if (moduleSym) { - context.tracker.trackExternalModuleSymbolOfImportTypeNode(moduleSym); - } - } + else if (oldcontext.tracker && oldcontext.tracker.trackSymbol) { + return oldcontext.tracker.trackSymbol(sym, decl, meaning); } - return lit; - } - } - } - - function symbolTableToDeclarationStatements(symbolTable: ts.SymbolTable, context: NodeBuilderContext, bundled?: boolean): ts.Statement[] { - const serializePropertySymbolForClass = makeSerializePropertySymbol(ts.factory.createPropertyDeclaration, ts.SyntaxKind.MethodDeclaration, /*useAcessors*/ true); - const serializePropertySymbolForInterfaceWorker = makeSerializePropertySymbol((_decorators, mods, name, question, type) => ts.factory.createPropertySignature(mods, name, question, type), ts.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: ts.Statement[] = []; - const visitedSymbols = new ts.Set(); - const deferredPrivatesStack: ts.ESMap[] = []; - const oldcontext = context; - context = { - ...oldcontext, - usedSymbolNames: new ts.Set(oldcontext.usedSymbolNames), - remappedSymbolNames: new ts.Map(), - tracker: { - ...oldcontext.tracker, - trackSymbol: (sym, decl, meaning) => { - const accessibleResult = isSymbolAccessible(sym, decl, meaning, /*computeAliases*/ false); - if (accessibleResult.accessibility === ts.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 & ts.SymbolFlags.Property)) { - includePrivateSymbol(chain[0]); - } - } - else if (oldcontext.tracker && oldcontext.tracker.trackSymbol) { - return oldcontext.tracker.trackSymbol(sym, decl, meaning); - } - return false; - }, + return false; }, - }; - context.tracker = wrapSymbolTrackerToReportForContext(context, context.tracker); - ts.forEachEntry(symbolTable, (symbol, name) => { - const baseName = ts.unescapeLeadingUnderscores(name); - void getInternalSymbolName(symbol, baseName); // Called to cache values into `usedSymbolNames` and `remappedSymbolNames` - }); - let addingDeclare = !bundled; - const exportEquals = symbolTable.get(ts.InternalSymbolName.ExportEquals); - if (exportEquals && symbolTable.size > 1 && exportEquals.flags & ts.SymbolFlags.Alias) { - symbolTable = ts.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(ts.InternalSymbolName.ExportEquals, exportEquals); - } - - visitSymbolTable(symbolTable); - return mergeRedundantStatements(results); - - function isIdentifierAndNotUndefined(node: ts.Node | undefined): node is ts.Identifier { - return !!node && node.kind === ts.SyntaxKind.Identifier; - } - - function getNamesOfDeclaration(statement: ts.Statement): ts.Identifier[] { - if (ts.isVariableStatement(statement)) { - return ts.filter(ts.map(statement.declarationList.declarations, ts.getNameOfDeclaration), isIdentifierAndNotUndefined); - } - return ts.filter([ts.getNameOfDeclaration(statement as ts.DeclarationStatement)], isIdentifierAndNotUndefined); - } - - function flattenExportAssignedNamespace(statements: ts.Statement[]) { - const exportAssignment = ts.find(statements, ts.isExportAssignment); - const nsIndex = ts.findIndex(statements, ts.isModuleDeclaration); - let ns = nsIndex !== -1 ? statements[nsIndex] as ts.ModuleDeclaration : undefined; - if (ns && exportAssignment && exportAssignment.isExportEquals && - ts.isIdentifier(exportAssignment.expression) && ts.isIdentifier(ns.name) && ts.idText(ns.name) === ts.idText(exportAssignment.expression) && - ns.body && ts.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 = ts.filter(statements, s => !!(ts.getEffectiveModifierFlags(s) & ts.ModifierFlags.Export)); - const name = ns.name; - let body = ns.body; - if (ts.length(excessExports)) { - ns = ts.factory.updateModuleDeclaration(ns, ns.decorators, ns.modifiers, ns.name, body = ts.factory.updateModuleBlock(body, ts.factory.createNodeArray([...ns.body.statements, ts.factory.createExportDeclaration( + }, + }; + context.tracker = wrapSymbolTrackerToReportForContext(context, context.tracker); + ts.forEachEntry(symbolTable, (symbol, name) => { + const baseName = ts.unescapeLeadingUnderscores(name); + void getInternalSymbolName(symbol, baseName); // Called to cache values into `usedSymbolNames` and `remappedSymbolNames` + }); + let addingDeclare = !bundled; + const exportEquals = symbolTable.get(ts.InternalSymbolName.ExportEquals); + if (exportEquals && symbolTable.size > 1 && exportEquals.flags & ts.SymbolFlags.Alias) { + symbolTable = ts.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(ts.InternalSymbolName.ExportEquals, exportEquals); + } + + visitSymbolTable(symbolTable); + return mergeRedundantStatements(results); + + function isIdentifierAndNotUndefined(node: ts.Node | undefined): node is ts.Identifier { + return !!node && node.kind === ts.SyntaxKind.Identifier; + } + + function getNamesOfDeclaration(statement: ts.Statement): ts.Identifier[] { + if (ts.isVariableStatement(statement)) { + return ts.filter(ts.map(statement.declarationList.declarations, ts.getNameOfDeclaration), isIdentifierAndNotUndefined); + } + return ts.filter([ts.getNameOfDeclaration(statement as ts.DeclarationStatement)], isIdentifierAndNotUndefined); + } + + function flattenExportAssignedNamespace(statements: ts.Statement[]) { + const exportAssignment = ts.find(statements, ts.isExportAssignment); + const nsIndex = ts.findIndex(statements, ts.isModuleDeclaration); + let ns = nsIndex !== -1 ? statements[nsIndex] as ts.ModuleDeclaration : undefined; + if (ns && exportAssignment && exportAssignment.isExportEquals && + ts.isIdentifier(exportAssignment.expression) && ts.isIdentifier(ns.name) && ts.idText(ns.name) === ts.idText(exportAssignment.expression) && + ns.body && ts.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 = ts.filter(statements, s => !!(ts.getEffectiveModifierFlags(s) & ts.ModifierFlags.Export)); + const name = ns.name; + let body = ns.body; + if (ts.length(excessExports)) { + ns = ts.factory.updateModuleDeclaration(ns, ns.decorators, ns.modifiers, ns.name, body = ts.factory.updateModuleBlock(body, ts.factory.createNodeArray([...ns.body.statements, ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.map(ts.flatMap(excessExports, e => getNamesOfDeclaration(e)), id => ts.factory.createExportSpecifier(/*isTypeOnly*/ false, /*alias*/ undefined, id))), + /*moduleSpecifier*/ undefined)]))); + statements = [...statements.slice(0, nsIndex), ns, ...statements.slice(nsIndex + 1)]; + } + + // Pass 1: Flatten `export namespace _exports {} export = _exports;` so long as the `export=` only points at a single namespace declaration + if (!ts.find(statements, s => s !== ns && ts.nodeHasName(s, name))) { + results = []; + // If the namespace contains no export assignments or declarations, and no declarations flagged with `export`, then _everything_ is exported - + // to respect this as the top level, we need to add an `export` modifier to everything + const mixinExportFlag = !ts.some(body.statements, s => ts.hasSyntacticModifier(s, ts.ModifierFlags.Export) || ts.isExportAssignment(s) || ts.isExportDeclaration(s)); + ts.forEach(body.statements, s => { + addResult(s, mixinExportFlag ? ts.ModifierFlags.Export : ts.ModifierFlags.None); // Recalculates the ambient (and export, if applicable from above) flag + }); + statements = [...ts.filter(statements, s => s !== ns && s !== exportAssignment), ...results]; + } + } + return statements; + } + + function mergeExportDeclarations(statements: ts.Statement[]) { + // Pass 2: Combine all `export {}` declarations + const exports = ts.filter(statements, d => ts.isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause && ts.isNamedExports(d.exportClause)) as ts.ExportDeclaration[]; + if (ts.length(exports) > 1) { + const nonExports = ts.filter(statements, d => !ts.isExportDeclaration(d) || !!d.moduleSpecifier || !d.exportClause); + statements = [...nonExports, ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.flatMap(exports, e => ts.cast(e.exportClause, ts.isNamedExports).elements)), + /*moduleSpecifier*/ undefined)]; + } + // Pass 2b: Also combine all `export {} from "..."` declarations as needed + const reexports = ts.filter(statements, d => ts.isExportDeclaration(d) && !!d.moduleSpecifier && !!d.exportClause && ts.isNamedExports(d.exportClause)) as ts.ExportDeclaration[]; + if (ts.length(reexports) > 1) { + const groups = ts.group(reexports, decl => ts.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 = [ + ...ts.filter(statements, s => group.indexOf(s as ts.ExportDeclaration) === -1), + ts.factory.createExportDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.map(ts.flatMap(excessExports, e => getNamesOfDeclaration(e)), id => ts.factory.createExportSpecifier(/*isTypeOnly*/ false, /*alias*/ undefined, id))), - /*moduleSpecifier*/ undefined)]))); - statements = [...statements.slice(0, nsIndex), ns, ...statements.slice(nsIndex + 1)]; - } - - // Pass 1: Flatten `export namespace _exports {} export = _exports;` so long as the `export=` only points at a single namespace declaration - if (!ts.find(statements, s => s !== ns && ts.nodeHasName(s, name))) { - results = []; - // If the namespace contains no export assignments or declarations, and no declarations flagged with `export`, then _everything_ is exported - - // to respect this as the top level, we need to add an `export` modifier to everything - const mixinExportFlag = !ts.some(body.statements, s => ts.hasSyntacticModifier(s, ts.ModifierFlags.Export) || ts.isExportAssignment(s) || ts.isExportDeclaration(s)); - ts.forEach(body.statements, s => { - addResult(s, mixinExportFlag ? ts.ModifierFlags.Export : ts.ModifierFlags.None); // Recalculates the ambient (and export, if applicable from above) flag - }); - statements = [...ts.filter(statements, s => s !== ns && s !== exportAssignment), ...results]; + /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.flatMap(group, e => ts.cast(e.exportClause, ts.isNamedExports).elements)), group[0].moduleSpecifier) + ]; + } } } - return statements; } + return statements; + } - function mergeExportDeclarations(statements: ts.Statement[]) { - // Pass 2: Combine all `export {}` declarations - const exports = ts.filter(statements, d => ts.isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause && ts.isNamedExports(d.exportClause)) as ts.ExportDeclaration[]; - if (ts.length(exports) > 1) { - const nonExports = ts.filter(statements, d => !ts.isExportDeclaration(d) || !!d.moduleSpecifier || !d.exportClause); - statements = [...nonExports, ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.flatMap(exports, e => ts.cast(e.exportClause, ts.isNamedExports).elements)), - /*moduleSpecifier*/ undefined)]; - } - // Pass 2b: Also combine all `export {} from "..."` declarations as needed - const reexports = ts.filter(statements, d => ts.isExportDeclaration(d) && !!d.moduleSpecifier && !!d.exportClause && ts.isNamedExports(d.exportClause)) as ts.ExportDeclaration[]; - if (ts.length(reexports) > 1) { - const groups = ts.group(reexports, decl => ts.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 = [ - ...ts.filter(statements, s => group.indexOf(s as ts.ExportDeclaration) === -1), - ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.flatMap(group, e => ts.cast(e.exportClause, ts.isNamedExports).elements)), group[0].moduleSpecifier) - ]; + function inlineExportModifiers(statements: ts.Statement[]) { + // Pass 3: Move all `export {}`'s to `export` modifiers where possible + const index = ts.findIndex(statements, d => ts.isExportDeclaration(d) && !d.moduleSpecifier && !d.assertClause && !!d.exportClause && ts.isNamedExports(d.exportClause)); + if (index >= 0) { + const exportDecl = statements[index] as ts.ExportDeclaration & { + readonly exportClause: ts.NamedExports; + }; + const replacements = ts.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 indices = ts.indicesOf(statements); + const associatedIndices = ts.filter(indices, i => ts.nodeHasName(statements[i], e.name)); + if (ts.length(associatedIndices) && ts.every(associatedIndices, i => canHaveExportModifier(statements[i]))) { + for (const index of associatedIndices) { + statements[index] = addExportModifier(statements[index] as Extract); } + return undefined; } } + return e; + }); + if (!ts.length(replacements)) { + // all clauses removed, remove the export declaration + ts.orderedRemoveItemAt(statements, index); } - return statements; - } - - function inlineExportModifiers(statements: ts.Statement[]) { - // Pass 3: Move all `export {}`'s to `export` modifiers where possible - const index = ts.findIndex(statements, d => ts.isExportDeclaration(d) && !d.moduleSpecifier && !d.assertClause && !!d.exportClause && ts.isNamedExports(d.exportClause)); - if (index >= 0) { - const exportDecl = statements[index] as ts.ExportDeclaration & { - readonly exportClause: ts.NamedExports; - }; - const replacements = ts.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 indices = ts.indicesOf(statements); - const associatedIndices = ts.filter(indices, i => ts.nodeHasName(statements[i], e.name)); - if (ts.length(associatedIndices) && ts.every(associatedIndices, i => canHaveExportModifier(statements[i]))) { - for (const index of associatedIndices) { - statements[index] = addExportModifier(statements[index] as Extract); - } - return undefined; - } - } - return e; - }); - if (!ts.length(replacements)) { - // all clauses removed, remove the export declaration - ts.orderedRemoveItemAt(statements, index); - } - else { - // some items filtered, others not - update the export declaration - statements[index] = ts.factory.updateExportDeclaration(exportDecl, exportDecl.decorators, exportDecl.modifiers, exportDecl.isTypeOnly, ts.factory.updateNamedExports(exportDecl.exportClause, replacements), exportDecl.moduleSpecifier, exportDecl.assertClause); - } + else { + // some items filtered, others not - update the export declaration + statements[index] = ts.factory.updateExportDeclaration(exportDecl, exportDecl.decorators, exportDecl.modifiers, exportDecl.isTypeOnly, ts.factory.updateNamedExports(exportDecl.exportClause, replacements), exportDecl.moduleSpecifier, exportDecl.assertClause); } - return statements; } + return statements; + } - function mergeRedundantStatements(statements: ts.Statement[]) { - statements = flattenExportAssignedNamespace(statements); - statements = mergeExportDeclarations(statements); - statements = inlineExportModifiers(statements); + function mergeRedundantStatements(statements: ts.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 && - ((ts.isSourceFile(enclosingDeclaration) && ts.isExternalOrCommonJsModule(enclosingDeclaration)) || ts.isModuleDeclaration(enclosingDeclaration)) && - (!ts.some(statements, ts.isExternalModuleIndicator) || (!ts.hasScopeMarker(statements) && ts.some(statements, ts.needsScopeMarker)))) { - statements.push(ts.createEmptyExports(ts.factory)); - } - return 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 && + ((ts.isSourceFile(enclosingDeclaration) && ts.isExternalOrCommonJsModule(enclosingDeclaration)) || ts.isModuleDeclaration(enclosingDeclaration)) && + (!ts.some(statements, ts.isExternalModuleIndicator) || (!ts.hasScopeMarker(statements) && ts.some(statements, ts.needsScopeMarker)))) { + statements.push(ts.createEmptyExports(ts.factory)); } + return statements; + } - function canHaveExportModifier(node: ts.Statement): node is Extract { - return ts.isEnumDeclaration(node) || - ts.isVariableStatement(node) || - ts.isFunctionDeclaration(node) || - ts.isClassDeclaration(node) || - (ts.isModuleDeclaration(node) && !ts.isExternalModuleAugmentation(node) && !ts.isGlobalScopeAugmentation(node)) || - ts.isInterfaceDeclaration(node) || - isTypeDeclaration(node); - } + function canHaveExportModifier(node: ts.Statement): node is Extract { + return ts.isEnumDeclaration(node) || + ts.isVariableStatement(node) || + ts.isFunctionDeclaration(node) || + ts.isClassDeclaration(node) || + (ts.isModuleDeclaration(node) && !ts.isExternalModuleAugmentation(node) && !ts.isGlobalScopeAugmentation(node)) || + ts.isInterfaceDeclaration(node) || + isTypeDeclaration(node); + } - function addExportModifier(node: Extract) { - const flags = (ts.getEffectiveModifierFlags(node) | ts.ModifierFlags.Export) & ~ts.ModifierFlags.Ambient; - return ts.factory.updateModifiers(node, flags); - } + function addExportModifier(node: Extract) { + const flags = (ts.getEffectiveModifierFlags(node) | ts.ModifierFlags.Export) & ~ts.ModifierFlags.Ambient; + return ts.factory.updateModifiers(node, flags); + } - function removeExportModifier(node: Extract) { - const flags = ts.getEffectiveModifierFlags(node) & ~ts.ModifierFlags.Export; - return ts.factory.updateModifiers(node, flags); - } + function removeExportModifier(node: Extract) { + const flags = ts.getEffectiveModifierFlags(node) & ~ts.ModifierFlags.Export; + return ts.factory.updateModifiers(node, flags); + } - function visitSymbolTable(symbolTable: ts.SymbolTable, suppressNewPrivateContext?: boolean, propertyAsAlias?: boolean) { - if (!suppressNewPrivateContext) { - deferredPrivatesStack.push(new ts.Map()); - } - symbolTable.forEach((symbol: ts.Symbol) => { - serializeSymbol(symbol, /*isPrivate*/ false, !!propertyAsAlias); + function visitSymbolTable(symbolTable: ts.SymbolTable, suppressNewPrivateContext?: boolean, propertyAsAlias?: boolean) { + if (!suppressNewPrivateContext) { + deferredPrivatesStack.push(new ts.Map()); + } + 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) + deferredPrivatesStack[deferredPrivatesStack.length - 1].forEach((symbol: ts.Symbol) => { + serializeSymbol(symbol, /*isPrivate*/ true, !!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) - deferredPrivatesStack[deferredPrivatesStack.length - 1].forEach((symbol: ts.Symbol) => { - serializeSymbol(symbol, /*isPrivate*/ true, !!propertyAsAlias); - }); - deferredPrivatesStack.pop(); - } + deferredPrivatesStack.pop(); } + } - 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.add(getSymbolId(visitedSym)); - // 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 || (!!ts.length(symbol.declarations) && ts.some(symbol.declarations, d => !!ts.findAncestor(d, n => n === enclosingDeclaration)))) { - const oldContext = context; - context = cloneNodeBuilderContext(context); - const result = serializeSymbolWorker(symbol, isPrivate, propertyAsAlias); - if (context.reportedDiagnostic) { - oldcontext.reportedDiagnostic = context.reportedDiagnostic; // hoist diagnostic result into outer context - } - context = oldContext; - return result; + 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.add(getSymbolId(visitedSym)); + // 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 || (!!ts.length(symbol.declarations) && ts.some(symbol.declarations, d => !!ts.findAncestor(d, n => n === enclosingDeclaration)))) { + const oldContext = context; + context = cloneNodeBuilderContext(context); + const result = serializeSymbolWorker(symbol, isPrivate, propertyAsAlias); + if (context.reportedDiagnostic) { + oldcontext.reportedDiagnostic = context.reportedDiagnostic; // hoist diagnostic result into outer context } + 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: ts.Symbol, isPrivate: boolean, propertyAsAlias: boolean) { - const symbolName = ts.unescapeLeadingUnderscores(symbol.escapedName); - const isDefault = symbol.escapedName === ts.InternalSymbolName.Default; - if (isPrivate && !(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier) && ts.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 - } - let needsPostExportDefault = isDefault && !!(symbol.flags & ts.SymbolFlags.ExportDoesNotSupportDefaultModifier - || (symbol.flags & ts.SymbolFlags.Function && ts.length(getPropertiesOfType(getTypeOfSymbol(symbol))))) && !(symbol.flags & ts.SymbolFlags.Alias); // An alias symbol should preclude needing to make an alias ourselves - let needsExportDeclaration = !needsPostExportDefault && !isPrivate && ts.isStringANonContextualKeyword(symbolName) && !isDefault; - // `serializeVariableOrProperty` will handle adding the export declaration if it is run (since `getInternalSymbolName` will create the name mapping), so we need to ensuer we unset `needsExportDeclaration` if it is - if (needsPostExportDefault || needsExportDeclaration) { - isPrivate = true; - } - const modifierFlags = (!isPrivate ? ts.ModifierFlags.Export : 0) | (isDefault && !needsPostExportDefault ? ts.ModifierFlags.Default : 0); - const isConstMergedWithNS = symbol.flags & ts.SymbolFlags.Module && - symbol.flags & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Property) && - symbol.escapedName !== ts.InternalSymbolName.ExportEquals; - const isConstMergedWithNSPrintableAsSignatureMerge = isConstMergedWithNS && isTypeRepresentableAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol); - if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method) || isConstMergedWithNSPrintableAsSignatureMerge) { - serializeAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); - } - if (symbol.flags & ts.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 & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Property) - && symbol.escapedName !== ts.InternalSymbolName.ExportEquals - && !(symbol.flags & ts.SymbolFlags.Prototype) - && !(symbol.flags & ts.SymbolFlags.Class) - && !isConstMergedWithNSPrintableAsSignatureMerge) { - if (propertyAsAlias) { - const createdExport = serializeMaybeAliasAssignment(symbol); - if (createdExport) { - needsExportDeclaration = false; - needsPostExportDefault = false; - } + // 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 = ts.unescapeLeadingUnderscores(symbol.escapedName); + const isDefault = symbol.escapedName === ts.InternalSymbolName.Default; + if (isPrivate && !(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier) && ts.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 + } + let needsPostExportDefault = isDefault && !!(symbol.flags & ts.SymbolFlags.ExportDoesNotSupportDefaultModifier + || (symbol.flags & ts.SymbolFlags.Function && ts.length(getPropertiesOfType(getTypeOfSymbol(symbol))))) && !(symbol.flags & ts.SymbolFlags.Alias); // An alias symbol should preclude needing to make an alias ourselves + let needsExportDeclaration = !needsPostExportDefault && !isPrivate && ts.isStringANonContextualKeyword(symbolName) && !isDefault; + // `serializeVariableOrProperty` will handle adding the export declaration if it is run (since `getInternalSymbolName` will create the name mapping), so we need to ensuer we unset `needsExportDeclaration` if it is + if (needsPostExportDefault || needsExportDeclaration) { + isPrivate = true; + } + const modifierFlags = (!isPrivate ? ts.ModifierFlags.Export : 0) | (isDefault && !needsPostExportDefault ? ts.ModifierFlags.Default : 0); + const isConstMergedWithNS = symbol.flags & ts.SymbolFlags.Module && + symbol.flags & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Property) && + symbol.escapedName !== ts.InternalSymbolName.ExportEquals; + const isConstMergedWithNSPrintableAsSignatureMerge = isConstMergedWithNS && isTypeRepresentableAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol); + if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method) || isConstMergedWithNSPrintableAsSignatureMerge) { + serializeAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + if (symbol.flags & ts.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 & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.FunctionScopedVariable | ts.SymbolFlags.Property) + && symbol.escapedName !== ts.InternalSymbolName.ExportEquals + && !(symbol.flags & ts.SymbolFlags.Prototype) + && !(symbol.flags & ts.SymbolFlags.Class) + && !isConstMergedWithNSPrintableAsSignatureMerge) { + if (propertyAsAlias) { + const createdExport = serializeMaybeAliasAssignment(symbol); + if (createdExport) { + needsExportDeclaration = false; + needsPostExportDefault = false; + } + } + else { + const type = getTypeOfSymbol(symbol); + const localName = getInternalSymbolName(symbol, symbolName); + if (!(symbol.flags & ts.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 { - const type = getTypeOfSymbol(symbol); - const localName = getInternalSymbolName(symbol, symbolName); - if (!(symbol.flags & ts.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); + // 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 & ts.SymbolFlags.BlockScopedVariable) + ? symbol.parent?.valueDeclaration && ts.isSourceFile(symbol.parent?.valueDeclaration) + ? ts.NodeFlags.Const // exports are immutable in es6, which is what we emulate and check; so it's safe to mark all exports as `const` (there's no difference to consumers, but it allows unique symbol type declarations) + : undefined + : isConstVariable(symbol) + ? ts.NodeFlags.Const + : ts.NodeFlags.Let; + const name = (needsPostExportDefault || !(symbol.flags & ts.SymbolFlags.Property)) ? localName : getUnusedName(localName, symbol); + let textRange: ts.Node | undefined = symbol.declarations && ts.find(symbol.declarations, d => ts.isVariableDeclaration(d)); + if (textRange && ts.isVariableDeclarationList(textRange.parent) && textRange.parent.declarations.length === 1) { + textRange = textRange.parent.parent; + } + const propertyAccessRequire = symbol.declarations?.find(ts.isPropertyAccessExpression); + if (propertyAccessRequire && ts.isBinaryExpression(propertyAccessRequire.parent) && ts.isIdentifier(propertyAccessRequire.parent.right) + && type.symbol?.valueDeclaration && ts.isSourceFile(type.symbol.valueDeclaration)) { + const alias = localName === propertyAccessRequire.parent.right.escapedText ? undefined : propertyAccessRequire.parent.right; + addResult(ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, alias, localName)])), ts.ModifierFlags.None); + context.tracker.trackSymbol!(type.symbol, context.enclosingDeclaration, ts.SymbolFlags.Value); } 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 & ts.SymbolFlags.BlockScopedVariable) - ? symbol.parent?.valueDeclaration && ts.isSourceFile(symbol.parent?.valueDeclaration) - ? ts.NodeFlags.Const // exports are immutable in es6, which is what we emulate and check; so it's safe to mark all exports as `const` (there's no difference to consumers, but it allows unique symbol type declarations) - : undefined - : isConstVariable(symbol) - ? ts.NodeFlags.Const - : ts.NodeFlags.Let; - const name = (needsPostExportDefault || !(symbol.flags & ts.SymbolFlags.Property)) ? localName : getUnusedName(localName, symbol); - let textRange: ts.Node | undefined = symbol.declarations && ts.find(symbol.declarations, d => ts.isVariableDeclaration(d)); - if (textRange && ts.isVariableDeclarationList(textRange.parent) && textRange.parent.declarations.length === 1) { - textRange = textRange.parent.parent; - } - const propertyAccessRequire = symbol.declarations?.find(ts.isPropertyAccessExpression); - if (propertyAccessRequire && ts.isBinaryExpression(propertyAccessRequire.parent) && ts.isIdentifier(propertyAccessRequire.parent.right) - && type.symbol?.valueDeclaration && ts.isSourceFile(type.symbol.valueDeclaration)) { - const alias = localName === propertyAccessRequire.parent.right.escapedText ? undefined : propertyAccessRequire.parent.right; + const statement = ts.setTextRange(ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, serializeTypeForDeclaration(context, type, symbol, enclosingDeclaration, includePrivateSymbol, bundled)) + ], flags)), textRange); + addResult(statement, name !== localName ? modifierFlags & ~ts.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(ts.factory.createExportDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, alias, localName)])), ts.ModifierFlags.None); - context.tracker.trackSymbol!(type.symbol, context.enclosingDeclaration, ts.SymbolFlags.Value); - } - else { - const statement = ts.setTextRange(ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, serializeTypeForDeclaration(context, type, symbol, enclosingDeclaration, includePrivateSymbol, bundled)) - ], flags)), textRange); - addResult(statement, name !== localName ? modifierFlags & ~ts.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(ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, name, localName)])), ts.ModifierFlags.None); - needsExportDeclaration = false; - needsPostExportDefault = false; - } + /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, name, localName)])), ts.ModifierFlags.None); + needsExportDeclaration = false; + needsPostExportDefault = false; } } } } - if (symbol.flags & ts.SymbolFlags.Enum) { - serializeEnum(symbol, symbolName, modifierFlags); - } - if (symbol.flags & ts.SymbolFlags.Class) { - if (symbol.flags & ts.SymbolFlags.Property - && symbol.valueDeclaration - && ts.isBinaryExpression(symbol.valueDeclaration.parent) - && ts.isClassExpression(symbol.valueDeclaration.parent.right)) { - // 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 & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) && (!isConstMergedWithNS || isTypeOnlyNamespace(symbol))) || isConstMergedWithNSPrintableAsSignatureMerge) { - serializeModule(symbol, symbolName, modifierFlags); - } - // The class meaning serialization should handle serializing all interface members - if (symbol.flags & ts.SymbolFlags.Interface && !(symbol.flags & ts.SymbolFlags.Class)) { - serializeInterface(symbol, symbolName, modifierFlags); - } - if (symbol.flags & ts.SymbolFlags.Alias) { + } + if (symbol.flags & ts.SymbolFlags.Enum) { + serializeEnum(symbol, symbolName, modifierFlags); + } + if (symbol.flags & ts.SymbolFlags.Class) { + if (symbol.flags & ts.SymbolFlags.Property + && symbol.valueDeclaration + && ts.isBinaryExpression(symbol.valueDeclaration.parent) + && ts.isClassExpression(symbol.valueDeclaration.parent.right)) { + // 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); } - if (symbol.flags & ts.SymbolFlags.Property && symbol.escapedName === ts.InternalSymbolName.ExportEquals) { - serializeMaybeAliasAssignment(symbol); + else { + serializeAsClass(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); } - if (symbol.flags & ts.SymbolFlags.ExportStar) { - // synthesize export * from "moduleReference" - // Straightforward - only one thing to do - make an export declaration - if (symbol.declarations) { - for (const node of symbol.declarations) { - const resolvedModule = resolveExternalModuleName(node, (node as ts.ExportDeclaration).moduleSpecifier!); - if (!resolvedModule) - continue; - addResult(ts.factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, /*exportClause*/ undefined, ts.factory.createStringLiteral(getSpecifierForModuleSymbol(resolvedModule, context))), ts.ModifierFlags.None); - } + } + if ((symbol.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) && (!isConstMergedWithNS || isTypeOnlyNamespace(symbol))) || isConstMergedWithNSPrintableAsSignatureMerge) { + serializeModule(symbol, symbolName, modifierFlags); + } + // The class meaning serialization should handle serializing all interface members + if (symbol.flags & ts.SymbolFlags.Interface && !(symbol.flags & ts.SymbolFlags.Class)) { + serializeInterface(symbol, symbolName, modifierFlags); + } + if (symbol.flags & ts.SymbolFlags.Alias) { + serializeAsAlias(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + if (symbol.flags & ts.SymbolFlags.Property && symbol.escapedName === ts.InternalSymbolName.ExportEquals) { + serializeMaybeAliasAssignment(symbol); + } + if (symbol.flags & ts.SymbolFlags.ExportStar) { + // synthesize export * from "moduleReference" + // Straightforward - only one thing to do - make an export declaration + if (symbol.declarations) { + for (const node of symbol.declarations) { + const resolvedModule = resolveExternalModuleName(node, (node as ts.ExportDeclaration).moduleSpecifier!); + if (!resolvedModule) + continue; + addResult(ts.factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, /*exportClause*/ undefined, ts.factory.createStringLiteral(getSpecifierForModuleSymbol(resolvedModule, context))), ts.ModifierFlags.None); } } - if (needsPostExportDefault) { - addResult(ts.factory.createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportAssignment*/ false, ts.factory.createIdentifier(getInternalSymbolName(symbol, symbolName))), ts.ModifierFlags.None); - } - else if (needsExportDeclaration) { - addResult(ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, getInternalSymbolName(symbol, symbolName), symbolName)])), ts.ModifierFlags.None); - } } - - function includePrivateSymbol(symbol: ts.Symbol) { - if (ts.some(symbol.declarations, ts.isParameterDeclaration)) - return; - ts.Debug.assertIsDefined(deferredPrivatesStack[deferredPrivatesStack.length - 1]); - getUnusedName(ts.unescapeLeadingUnderscores(symbol.escapedName), symbol); // Call to cache unique name for symbol - // Blanket moving (import) aliases into the root private context should work, since imports are not valid within namespaces - // (so they must have been in the root to begin with if they were real imports) cjs `require` aliases (an upcoming feature) - // will throw a wrench in this, since those may have been nested, but we'll need to synthesize them in the outer scope - // anyway, as that's the only place the import they translate to is valid. In such a case, we might need to use a unique name - // for the moved import; which hopefully the above `getUnusedName` call should produce. - const isExternalImportAlias = !!(symbol.flags & ts.SymbolFlags.Alias) && !ts.some(symbol.declarations, d => !!ts.findAncestor(d, ts.isExportDeclaration) || - ts.isNamespaceExport(d) || - (ts.isImportEqualsDeclaration(d) && !ts.isExternalModuleReference(d.moduleReference))); - deferredPrivatesStack[isExternalImportAlias ? 0 : (deferredPrivatesStack.length - 1)].set(getSymbolId(symbol), symbol); - } - - function isExportingScope(enclosingDeclaration: ts.Node) { - return ((ts.isSourceFile(enclosingDeclaration) && (ts.isExternalOrCommonJsModule(enclosingDeclaration) || ts.isJsonSourceFile(enclosingDeclaration))) || - (ts.isAmbientModule(enclosingDeclaration) && !ts.isGlobalScopeAugmentation(enclosingDeclaration))); - } - - // Prepends a `declare` and/or `export` modifier if the context requires it, and then adds `node` to `result` and returns `node` - function addResult(node: ts.Statement, additionalModifierFlags: ts.ModifierFlags) { - if (ts.canHaveModifiers(node)) { - let newModifierFlags: ts.ModifierFlags = ts.ModifierFlags.None; - const enclosingDeclaration = context.enclosingDeclaration && - (ts.isJSDocTypeAlias(context.enclosingDeclaration) ? ts.getSourceFileOfNode(context.enclosingDeclaration) : context.enclosingDeclaration); - if (additionalModifierFlags & ts.ModifierFlags.Export && - enclosingDeclaration && (isExportingScope(enclosingDeclaration) || ts.isModuleDeclaration(enclosingDeclaration)) && - canHaveExportModifier(node)) { - // Classes, namespaces, variables, functions, interfaces, and types should all be `export`ed in a module context if not private - newModifierFlags |= ts.ModifierFlags.Export; - } - if (addingDeclare && !(newModifierFlags & ts.ModifierFlags.Export) && - (!enclosingDeclaration || !(enclosingDeclaration.flags & ts.NodeFlags.Ambient)) && - (ts.isEnumDeclaration(node) || ts.isVariableStatement(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node))) { - // Classes, namespaces, variables, enums, and functions all need `declare` modifiers to be valid in a declaration file top-level scope - newModifierFlags |= ts.ModifierFlags.Ambient; - } - if ((additionalModifierFlags & ts.ModifierFlags.Default) && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isFunctionDeclaration(node))) { - newModifierFlags |= ts.ModifierFlags.Default; - } - if (newModifierFlags) { - node = ts.factory.updateModifiers(node, newModifierFlags | ts.getEffectiveModifierFlags(node)); - } - } - results.push(node); - } - - function serializeTypeAlias(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { - const aliasType = getDeclaredTypeOfTypeAlias(symbol); - const typeParams = getSymbolLinks(symbol).typeParameters; - const typeParamDecls = ts.map(typeParams, p => typeParameterToDeclaration(p, context)); - const jsdocAliasDecl = symbol.declarations?.find(ts.isJSDocTypeAlias); - const commentText = ts.getTextOfJSDocComment(jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined); - const oldFlags = context.flags; - context.flags |= ts.NodeBuilderFlags.InTypeAlias; - const oldEnclosingDecl = context.enclosingDeclaration; - context.enclosingDeclaration = jsdocAliasDecl; - const typeNode = jsdocAliasDecl && jsdocAliasDecl.typeExpression - && ts.isJSDocTypeExpression(jsdocAliasDecl.typeExpression) - && serializeExistingTypeNode(context, jsdocAliasDecl.typeExpression.type, includePrivateSymbol, bundled) - || typeToTypeNodeHelper(aliasType, context); - addResult(ts.setSyntheticLeadingComments(ts.factory.createTypeAliasDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, typeNode), !commentText ? [] : [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]), modifierFlags); - context.flags = oldFlags; - context.enclosingDeclaration = oldEnclosingDecl; - } - - function serializeInterface(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { - const interfaceType = getDeclaredTypeOfClassOrInterface(symbol); - const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - const typeParamDecls = ts.map(localParams, p => typeParameterToDeclaration(p, context)); - const baseTypes = getBaseTypes(interfaceType); - const baseType = ts.length(baseTypes) ? getIntersectionType(baseTypes) : undefined; - const members = ts.flatMap(getPropertiesOfType(interfaceType), p => serializePropertySymbolForInterface(p, baseType)); - const callSignatures = serializeSignatures(ts.SignatureKind.Call, interfaceType, baseType, ts.SyntaxKind.CallSignature) as ts.CallSignatureDeclaration[]; - const constructSignatures = serializeSignatures(ts.SignatureKind.Construct, interfaceType, baseType, ts.SyntaxKind.ConstructSignature) as ts.ConstructSignatureDeclaration[]; - const indexSignatures = serializeIndexSignatures(interfaceType, baseType); - - const heritageClauses = !ts.length(baseTypes) ? undefined : [ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, ts.mapDefined(baseTypes, b => trySerializeAsTypeReference(b, ts.SymbolFlags.Value)))]; - addResult(ts.factory.createInterfaceDeclaration( + if (needsPostExportDefault) { + addResult(ts.factory.createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportAssignment*/ false, ts.factory.createIdentifier(getInternalSymbolName(symbol, symbolName))), ts.ModifierFlags.None); + } + else if (needsExportDeclaration) { + addResult(ts.factory.createExportDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, heritageClauses, [...indexSignatures, ...constructSignatures, ...callSignatures, ...members]), modifierFlags); + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, getInternalSymbolName(symbol, symbolName), symbolName)])), ts.ModifierFlags.None); } + } - function getNamespaceMembersForSerialization(symbol: ts.Symbol) { - return !symbol.exports ? [] : ts.filter(ts.arrayFrom(symbol.exports.values()), isNamespaceMember); - } + function includePrivateSymbol(symbol: ts.Symbol) { + if (ts.some(symbol.declarations, ts.isParameterDeclaration)) + return; + ts.Debug.assertIsDefined(deferredPrivatesStack[deferredPrivatesStack.length - 1]); + getUnusedName(ts.unescapeLeadingUnderscores(symbol.escapedName), symbol); // Call to cache unique name for symbol + // Blanket moving (import) aliases into the root private context should work, since imports are not valid within namespaces + // (so they must have been in the root to begin with if they were real imports) cjs `require` aliases (an upcoming feature) + // will throw a wrench in this, since those may have been nested, but we'll need to synthesize them in the outer scope + // anyway, as that's the only place the import they translate to is valid. In such a case, we might need to use a unique name + // for the moved import; which hopefully the above `getUnusedName` call should produce. + const isExternalImportAlias = !!(symbol.flags & ts.SymbolFlags.Alias) && !ts.some(symbol.declarations, d => !!ts.findAncestor(d, ts.isExportDeclaration) || + ts.isNamespaceExport(d) || + (ts.isImportEqualsDeclaration(d) && !ts.isExternalModuleReference(d.moduleReference))); + deferredPrivatesStack[isExternalImportAlias ? 0 : (deferredPrivatesStack.length - 1)].set(getSymbolId(symbol), symbol); + } + + function isExportingScope(enclosingDeclaration: ts.Node) { + return ((ts.isSourceFile(enclosingDeclaration) && (ts.isExternalOrCommonJsModule(enclosingDeclaration) || ts.isJsonSourceFile(enclosingDeclaration))) || + (ts.isAmbientModule(enclosingDeclaration) && !ts.isGlobalScopeAugmentation(enclosingDeclaration))); + } + + // Prepends a `declare` and/or `export` modifier if the context requires it, and then adds `node` to `result` and returns `node` + function addResult(node: ts.Statement, additionalModifierFlags: ts.ModifierFlags) { + if (ts.canHaveModifiers(node)) { + let newModifierFlags: ts.ModifierFlags = ts.ModifierFlags.None; + const enclosingDeclaration = context.enclosingDeclaration && + (ts.isJSDocTypeAlias(context.enclosingDeclaration) ? ts.getSourceFileOfNode(context.enclosingDeclaration) : context.enclosingDeclaration); + if (additionalModifierFlags & ts.ModifierFlags.Export && + enclosingDeclaration && (isExportingScope(enclosingDeclaration) || ts.isModuleDeclaration(enclosingDeclaration)) && + canHaveExportModifier(node)) { + // Classes, namespaces, variables, functions, interfaces, and types should all be `export`ed in a module context if not private + newModifierFlags |= ts.ModifierFlags.Export; + } + if (addingDeclare && !(newModifierFlags & ts.ModifierFlags.Export) && + (!enclosingDeclaration || !(enclosingDeclaration.flags & ts.NodeFlags.Ambient)) && + (ts.isEnumDeclaration(node) || ts.isVariableStatement(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node))) { + // Classes, namespaces, variables, enums, and functions all need `declare` modifiers to be valid in a declaration file top-level scope + newModifierFlags |= ts.ModifierFlags.Ambient; + } + if ((additionalModifierFlags & ts.ModifierFlags.Default) && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isFunctionDeclaration(node))) { + newModifierFlags |= ts.ModifierFlags.Default; + } + if (newModifierFlags) { + node = ts.factory.updateModifiers(node, newModifierFlags | ts.getEffectiveModifierFlags(node)); + } + } + results.push(node); + } + + function serializeTypeAlias(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { + const aliasType = getDeclaredTypeOfTypeAlias(symbol); + const typeParams = getSymbolLinks(symbol).typeParameters; + const typeParamDecls = ts.map(typeParams, p => typeParameterToDeclaration(p, context)); + const jsdocAliasDecl = symbol.declarations?.find(ts.isJSDocTypeAlias); + const commentText = ts.getTextOfJSDocComment(jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined); + const oldFlags = context.flags; + context.flags |= ts.NodeBuilderFlags.InTypeAlias; + const oldEnclosingDecl = context.enclosingDeclaration; + context.enclosingDeclaration = jsdocAliasDecl; + const typeNode = jsdocAliasDecl && jsdocAliasDecl.typeExpression + && ts.isJSDocTypeExpression(jsdocAliasDecl.typeExpression) + && serializeExistingTypeNode(context, jsdocAliasDecl.typeExpression.type, includePrivateSymbol, bundled) + || typeToTypeNodeHelper(aliasType, context); + addResult(ts.setSyntheticLeadingComments(ts.factory.createTypeAliasDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, typeNode), !commentText ? [] : [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]), modifierFlags); + context.flags = oldFlags; + context.enclosingDeclaration = oldEnclosingDecl; + } + + function serializeInterface(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { + const interfaceType = getDeclaredTypeOfClassOrInterface(symbol); + const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + const typeParamDecls = ts.map(localParams, p => typeParameterToDeclaration(p, context)); + const baseTypes = getBaseTypes(interfaceType); + const baseType = ts.length(baseTypes) ? getIntersectionType(baseTypes) : undefined; + const members = ts.flatMap(getPropertiesOfType(interfaceType), p => serializePropertySymbolForInterface(p, baseType)); + const callSignatures = serializeSignatures(ts.SignatureKind.Call, interfaceType, baseType, ts.SyntaxKind.CallSignature) as ts.CallSignatureDeclaration[]; + const constructSignatures = serializeSignatures(ts.SignatureKind.Construct, interfaceType, baseType, ts.SyntaxKind.ConstructSignature) as ts.ConstructSignatureDeclaration[]; + const indexSignatures = serializeIndexSignatures(interfaceType, baseType); + + const heritageClauses = !ts.length(baseTypes) ? undefined : [ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, ts.mapDefined(baseTypes, b => trySerializeAsTypeReference(b, ts.SymbolFlags.Value)))]; + addResult(ts.factory.createInterfaceDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, heritageClauses, [...indexSignatures, ...constructSignatures, ...callSignatures, ...members]), modifierFlags); + } - function isTypeOnlyNamespace(symbol: ts.Symbol) { - return ts.every(getNamespaceMembersForSerialization(symbol), m => !(resolveSymbol(m).flags & ts.SymbolFlags.Value)); - } + function getNamespaceMembersForSerialization(symbol: ts.Symbol) { + return !symbol.exports ? [] : ts.filter(ts.arrayFrom(symbol.exports.values()), isNamespaceMember); + } - function serializeModule(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.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 = ts.arrayToMultiMap(members, m => m.parent && m.parent === symbol ? "real" : "merged"); - const realMembers = locationMap.get("real") || ts.emptyArray; - const mergedMembers = locationMap.get("merged") || ts.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 (ts.length(realMembers)) { - const localName = getInternalSymbolName(symbol, symbolName); - serializeAsNamespaceDeclaration(realMembers, localName, modifierFlags, !!(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Assignment))); - } - if (ts.length(mergedMembers)) { - const containingFile = ts.getSourceFileOfNode(context.enclosingDeclaration); - const localName = getInternalSymbolName(symbol, symbolName); - const nsBody = ts.factory.createModuleBlock([ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.mapDefined(ts.filter(mergedMembers, n => n.escapedName !== ts.InternalSymbolName.ExportEquals), s => { - const name = ts.unescapeLeadingUnderscores(s.escapedName); - const localName = getInternalSymbolName(s, name); - const aliasDecl = s.declarations && getDeclarationOfAliasSymbol(s); - if (containingFile && (aliasDecl ? containingFile !== ts.getSourceFileOfNode(aliasDecl) : !ts.some(s.declarations, d => ts.getSourceFileOfNode(d) === containingFile))) { - context.tracker?.reportNonlocalAugmentation?.(containingFile, symbol, s); - return undefined; - } - const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); - includePrivateSymbol(target || s); - const targetName = target ? getInternalSymbolName(target, ts.unescapeLeadingUnderscores(target.escapedName)) : localName; - return ts.factory.createExportSpecifier(/*isTypeOnly*/ false, name === targetName ? undefined : targetName, name); - })))]); - addResult(ts.factory.createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createIdentifier(localName), nsBody, ts.NodeFlags.Namespace), ts.ModifierFlags.None); - } - } - - function serializeEnum(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { - addResult(ts.factory.createEnumDeclaration( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(isConstEnumSymbol(symbol) ? ts.ModifierFlags.Const : 0), getInternalSymbolName(symbol, symbolName), ts.map(ts.filter(getPropertiesOfType(getTypeOfSymbol(symbol)), p => !!(p.flags & ts.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] && ts.isEnumMember(p.declarations[0]) ? getConstantValue(p.declarations[0]) : undefined; - return ts.factory.createEnumMember(ts.unescapeLeadingUnderscores(p.escapedName), initializedValue === undefined ? undefined : - typeof initializedValue === "string" ? ts.factory.createStringLiteral(initializedValue) : - ts.factory.createNumericLiteral(initializedValue)); - })), modifierFlags); - } - function serializeAsFunctionNamespaceMerge(type: ts.Type, symbol: ts.Symbol, localName: string, modifierFlags: ts.ModifierFlags) { - const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); - for (const sig of signatures) { - // Each overload becomes a separate function declaration, in order - const decl = signatureToSignatureDeclarationHelper(sig, ts.SyntaxKind.FunctionDeclaration, context, { name: ts.factory.createIdentifier(localName), privateSymbolVisitor: includePrivateSymbol, bundledImports: bundled }) as ts.FunctionDeclaration; - addResult(ts.setTextRange(decl, getSignatureTextRangeLocation(sig)), modifierFlags); - } - // Module symbol emit will take care of module-y members, provided it has exports - if (!(symbol.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) && !!symbol.exports && !!symbol.exports.size)) { - const props = ts.filter(getPropertiesOfType(type), isNamespaceMember); - serializeAsNamespaceDeclaration(props, localName, modifierFlags, /*suppressNewPrivateContext*/ true); - } - } - - function getSignatureTextRangeLocation(signature: ts.Signature) { - if (signature.declaration && signature.declaration.parent) { - if (ts.isBinaryExpression(signature.declaration.parent) && ts.getAssignmentDeclarationKind(signature.declaration.parent) === ts.AssignmentDeclarationKind.Property) { - return signature.declaration.parent; - } - // for expressions assigned to `var`s, use the `var` as the text range - if (ts.isVariableDeclaration(signature.declaration.parent) && signature.declaration.parent.parent) { - return signature.declaration.parent.parent; - } - } - return signature.declaration; - } - - function serializeAsNamespaceDeclaration(props: readonly ts.Symbol[], localName: string, modifierFlags: ts.ModifierFlags, suppressNewPrivateContext: boolean) { - if (ts.length(props)) { - const localVsRemoteMap = ts.arrayToMultiMap(props, p => !ts.length(p.declarations) || ts.some(p.declarations, d => ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(context.enclosingDeclaration!)) ? "local" : "remote"); - const localProps = localVsRemoteMap.get("local") || ts.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 - // Create namespace as non-synthetic so it is usable as an enclosing declaration - let fakespace = ts.parseNodeFactory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createIdentifier(localName), ts.factory.createModuleBlock([]), ts.NodeFlags.Namespace); - ts.setParent(fakespace, enclosingDeclaration as ts.SourceFile | ts.NamespaceDeclaration); - fakespace.locals = ts.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(ts.createSymbolTable(localProps), suppressNewPrivateContext, /*propertyAsAlias*/ true); - context = oldContext; - addingDeclare = oldAddingDeclare; - const declarations = results; - results = oldResults; - // replace namespace with synthetic version - const defaultReplaced = ts.map(declarations, d => ts.isExportAssignment(d) && !d.isExportEquals && ts.isIdentifier(d.expression) ? ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, d.expression, ts.factory.createIdentifier(ts.InternalSymbolName.Default))])) : d); - const exportModifierStripped = ts.every(defaultReplaced, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.Export)) ? ts.map(defaultReplaced, removeExportModifier) : defaultReplaced; - fakespace = ts.factory.updateModuleDeclaration(fakespace, fakespace.decorators, fakespace.modifiers, fakespace.name, ts.factory.createModuleBlock(exportModifierStripped)); - addResult(fakespace, modifierFlags); // namespaces can never be default exported - } - } + function isTypeOnlyNamespace(symbol: ts.Symbol) { + return ts.every(getNamespaceMembersForSerialization(symbol), m => !(resolveSymbol(m).flags & ts.SymbolFlags.Value)); + } - function isNamespaceMember(p: ts.Symbol) { - return !!(p.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias)) || - !(p.flags & ts.SymbolFlags.Prototype || p.escapedName === "prototype" || p.valueDeclaration && ts.isStatic(p.valueDeclaration) && ts.isClassLike(p.valueDeclaration.parent)); + function serializeModule(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.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 = ts.arrayToMultiMap(members, m => m.parent && m.parent === symbol ? "real" : "merged"); + const realMembers = locationMap.get("real") || ts.emptyArray; + const mergedMembers = locationMap.get("merged") || ts.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 (ts.length(realMembers)) { + const localName = getInternalSymbolName(symbol, symbolName); + serializeAsNamespaceDeclaration(realMembers, localName, modifierFlags, !!(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Assignment))); } - - function sanitizeJSDocImplements(clauses: readonly ts.ExpressionWithTypeArguments[]): ts.ExpressionWithTypeArguments[] | undefined { - const result = ts.mapDefined(clauses, e => { - const oldEnclosing = context.enclosingDeclaration; - context.enclosingDeclaration = e; - let expr = e.expression; - if (ts.isEntityNameExpression(expr)) { - if (ts.isIdentifier(expr) && ts.idText(expr) === "") { - return cleanup(/*result*/ undefined); // Empty heritage clause, should be an error, but prefer emitting no heritage clauses to reemitting the empty one - } - let introducesError: boolean; - ({ introducesError, node: expr } = trackExistingEntityName(expr, context, includePrivateSymbol)); - if (introducesError) { - return cleanup(/*result*/ undefined); + if (ts.length(mergedMembers)) { + const containingFile = ts.getSourceFileOfNode(context.enclosingDeclaration); + const localName = getInternalSymbolName(symbol, symbolName); + const nsBody = ts.factory.createModuleBlock([ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports(ts.mapDefined(ts.filter(mergedMembers, n => n.escapedName !== ts.InternalSymbolName.ExportEquals), s => { + const name = ts.unescapeLeadingUnderscores(s.escapedName); + const localName = getInternalSymbolName(s, name); + const aliasDecl = s.declarations && getDeclarationOfAliasSymbol(s); + if (containingFile && (aliasDecl ? containingFile !== ts.getSourceFileOfNode(aliasDecl) : !ts.some(s.declarations, d => ts.getSourceFileOfNode(d) === containingFile))) { + context.tracker?.reportNonlocalAugmentation?.(containingFile, symbol, s); + return undefined; } - } - return cleanup(ts.factory.createExpressionWithTypeArguments(expr, ts.map(e.typeArguments, a => serializeExistingTypeNode(context, a, includePrivateSymbol, bundled) - || typeToTypeNodeHelper(getTypeFromTypeNode(a), context)))); + const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); + includePrivateSymbol(target || s); + const targetName = target ? getInternalSymbolName(target, ts.unescapeLeadingUnderscores(target.escapedName)) : localName; + return ts.factory.createExportSpecifier(/*isTypeOnly*/ false, name === targetName ? undefined : targetName, name); + })))]); + addResult(ts.factory.createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createIdentifier(localName), nsBody, ts.NodeFlags.Namespace), ts.ModifierFlags.None); + } + } + + function serializeEnum(symbol: ts.Symbol, symbolName: string, modifierFlags: ts.ModifierFlags) { + addResult(ts.factory.createEnumDeclaration( + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(isConstEnumSymbol(symbol) ? ts.ModifierFlags.Const : 0), getInternalSymbolName(symbol, symbolName), ts.map(ts.filter(getPropertiesOfType(getTypeOfSymbol(symbol)), p => !!(p.flags & ts.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] && ts.isEnumMember(p.declarations[0]) ? getConstantValue(p.declarations[0]) : undefined; + return ts.factory.createEnumMember(ts.unescapeLeadingUnderscores(p.escapedName), initializedValue === undefined ? undefined : + typeof initializedValue === "string" ? ts.factory.createStringLiteral(initializedValue) : + ts.factory.createNumericLiteral(initializedValue)); + })), modifierFlags); + } + function serializeAsFunctionNamespaceMerge(type: ts.Type, symbol: ts.Symbol, localName: string, modifierFlags: ts.ModifierFlags) { + const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); + for (const sig of signatures) { + // Each overload becomes a separate function declaration, in order + const decl = signatureToSignatureDeclarationHelper(sig, ts.SyntaxKind.FunctionDeclaration, context, { name: ts.factory.createIdentifier(localName), privateSymbolVisitor: includePrivateSymbol, bundledImports: bundled }) as ts.FunctionDeclaration; + addResult(ts.setTextRange(decl, getSignatureTextRangeLocation(sig)), modifierFlags); + } + // Module symbol emit will take care of module-y members, provided it has exports + if (!(symbol.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule) && !!symbol.exports && !!symbol.exports.size)) { + const props = ts.filter(getPropertiesOfType(type), isNamespaceMember); + serializeAsNamespaceDeclaration(props, localName, modifierFlags, /*suppressNewPrivateContext*/ true); + } + } + + function getSignatureTextRangeLocation(signature: ts.Signature) { + if (signature.declaration && signature.declaration.parent) { + if (ts.isBinaryExpression(signature.declaration.parent) && ts.getAssignmentDeclarationKind(signature.declaration.parent) === ts.AssignmentDeclarationKind.Property) { + return signature.declaration.parent; + } + // for expressions assigned to `var`s, use the `var` as the text range + if (ts.isVariableDeclaration(signature.declaration.parent) && signature.declaration.parent.parent) { + return signature.declaration.parent.parent; + } + } + return signature.declaration; + } + + function serializeAsNamespaceDeclaration(props: readonly ts.Symbol[], localName: string, modifierFlags: ts.ModifierFlags, suppressNewPrivateContext: boolean) { + if (ts.length(props)) { + const localVsRemoteMap = ts.arrayToMultiMap(props, p => !ts.length(p.declarations) || ts.some(p.declarations, d => ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(context.enclosingDeclaration!)) ? "local" : "remote"); + const localProps = localVsRemoteMap.get("local") || ts.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 + // Create namespace as non-synthetic so it is usable as an enclosing declaration + let fakespace = ts.parseNodeFactory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createIdentifier(localName), ts.factory.createModuleBlock([]), ts.NodeFlags.Namespace); + ts.setParent(fakespace, enclosingDeclaration as ts.SourceFile | ts.NamespaceDeclaration); + fakespace.locals = ts.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(ts.createSymbolTable(localProps), suppressNewPrivateContext, /*propertyAsAlias*/ true); + context = oldContext; + addingDeclare = oldAddingDeclare; + const declarations = results; + results = oldResults; + // replace namespace with synthetic version + const defaultReplaced = ts.map(declarations, d => ts.isExportAssignment(d) && !d.isExportEquals && ts.isIdentifier(d.expression) ? ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, d.expression, ts.factory.createIdentifier(ts.InternalSymbolName.Default))])) : d); + const exportModifierStripped = ts.every(defaultReplaced, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.Export)) ? ts.map(defaultReplaced, removeExportModifier) : defaultReplaced; + fakespace = ts.factory.updateModuleDeclaration(fakespace, fakespace.decorators, fakespace.modifiers, fakespace.name, ts.factory.createModuleBlock(exportModifierStripped)); + addResult(fakespace, modifierFlags); // namespaces can never be default exported + } + } - function cleanup(result: T): T { - context.enclosingDeclaration = oldEnclosing; - return result; + function isNamespaceMember(p: ts.Symbol) { + return !!(p.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias)) || + !(p.flags & ts.SymbolFlags.Prototype || p.escapedName === "prototype" || p.valueDeclaration && ts.isStatic(p.valueDeclaration) && ts.isClassLike(p.valueDeclaration.parent)); + } + + function sanitizeJSDocImplements(clauses: readonly ts.ExpressionWithTypeArguments[]): ts.ExpressionWithTypeArguments[] | undefined { + const result = ts.mapDefined(clauses, e => { + const oldEnclosing = context.enclosingDeclaration; + context.enclosingDeclaration = e; + let expr = e.expression; + if (ts.isEntityNameExpression(expr)) { + if (ts.isIdentifier(expr) && ts.idText(expr) === "") { + return cleanup(/*result*/ undefined); // Empty heritage clause, should be an error, but prefer emitting no heritage clauses to reemitting the empty one } - }); - if (result.length === clauses.length) { + let introducesError: boolean; + ({ introducesError, node: expr } = trackExistingEntityName(expr, context, includePrivateSymbol)); + if (introducesError) { + return cleanup(/*result*/ undefined); + } + } + return cleanup(ts.factory.createExpressionWithTypeArguments(expr, ts.map(e.typeArguments, a => serializeExistingTypeNode(context, a, includePrivateSymbol, bundled) + || typeToTypeNodeHelper(getTypeFromTypeNode(a), context)))); + + function cleanup(result: T): T { + context.enclosingDeclaration = oldEnclosing; return result; } - return undefined; + }); + if (result.length === clauses.length) { + return result; } + return undefined; + } - function serializeAsClass(symbol: ts.Symbol, localName: string, modifierFlags: ts.ModifierFlags) { - const originalDecl = symbol.declarations?.find(ts.isClassLike); - const oldEnclosing = context.enclosingDeclaration; - context.enclosingDeclaration = originalDecl || oldEnclosing; - const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - const typeParamDecls = ts.map(localParams, p => typeParameterToDeclaration(p, context)); - const classType = getDeclaredTypeOfClassOrInterface(symbol); - const baseTypes = getBaseTypes(classType); - const originalImplements = originalDecl && ts.getEffectiveImplementsTypeNodes(originalDecl); - const implementsExpressions = originalImplements && sanitizeJSDocImplements(originalImplements) - || ts.mapDefined(getImplementsTypes(classType), serializeImplementedType); - const staticType = getTypeOfSymbol(symbol); - const isClass = !!staticType.symbol?.valueDeclaration && ts.isClassLike(staticType.symbol.valueDeclaration); - const staticBaseType = isClass - ? getBaseConstructorTypeOfClass(staticType as ts.InterfaceType) - : anyType; - const heritageClauses = [ - ...!ts.length(baseTypes) ? [] : [ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, ts.map(baseTypes, b => serializeBaseType(b, staticBaseType, localName)))], - ...!ts.length(implementsExpressions) ? [] : [ts.factory.createHeritageClause(ts.SyntaxKind.ImplementsKeyword, implementsExpressions)] - ]; - const symbolProps = getNonInterhitedProperties(classType, baseTypes, getPropertiesOfType(classType)); - const publicSymbolProps = ts.filter(symbolProps, s => { - // `valueDeclaration` could be undefined if inherited from - // a union/intersection base type, but inherited properties - // don't matter here. - const valueDecl = s.valueDeclaration; - return !!valueDecl && !(ts.isNamedDeclaration(valueDecl) && ts.isPrivateIdentifier(valueDecl.name)); - }); - const hasPrivateIdentifier = ts.some(symbolProps, s => { - // `valueDeclaration` could be undefined if inherited from - // a union/intersection base type, but inherited properties - // don't matter here. - const valueDecl = s.valueDeclaration; - return !!valueDecl && ts.isNamedDeclaration(valueDecl) && ts.isPrivateIdentifier(valueDecl.name); - }); - // Boil down all private properties into a single one. - const privateProperties = hasPrivateIdentifier ? - [ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createPrivateIdentifier("#private"), - /*questionOrExclamationToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined)] : ts.emptyArray; - const publicProperties = ts.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 = ts.flatMap(ts.filter(getPropertiesOfType(staticType), p => !(p.flags & ts.SymbolFlags.Prototype) && p.escapedName !== "prototype" && !isNamespaceMember(p)), p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType)); - // When we encounter an `X.prototype.y` assignment in a JS file, we bind `X` as a class regardless as to whether - // the value is ever initialized with a class or function-like value. For cases where `X` could never be - // created via `new`, we will inject a `private constructor()` declaration to indicate it is not createable. - const isNonConstructableClassLikeInJsFile = !isClass && - !!symbol.valueDeclaration && - ts.isInJSFile(symbol.valueDeclaration) && - !ts.some(getSignaturesOfType(staticType, ts.SignatureKind.Construct)); - const constructors = isNonConstructableClassLikeInJsFile ? - [ts.factory.createConstructorDeclaration(/*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(ts.ModifierFlags.Private), [], /*body*/ undefined)] : - serializeSignatures(ts.SignatureKind.Construct, staticType, staticBaseType, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration[]; - const indexSignatures = serializeIndexSignatures(classType, baseTypes[0]); - context.enclosingDeclaration = oldEnclosing; - addResult(ts.setTextRange(ts.factory.createClassDeclaration( + function serializeAsClass(symbol: ts.Symbol, localName: string, modifierFlags: ts.ModifierFlags) { + const originalDecl = symbol.declarations?.find(ts.isClassLike); + const oldEnclosing = context.enclosingDeclaration; + context.enclosingDeclaration = originalDecl || oldEnclosing; + const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + const typeParamDecls = ts.map(localParams, p => typeParameterToDeclaration(p, context)); + const classType = getDeclaredTypeOfClassOrInterface(symbol); + const baseTypes = getBaseTypes(classType); + const originalImplements = originalDecl && ts.getEffectiveImplementsTypeNodes(originalDecl); + const implementsExpressions = originalImplements && sanitizeJSDocImplements(originalImplements) + || ts.mapDefined(getImplementsTypes(classType), serializeImplementedType); + const staticType = getTypeOfSymbol(symbol); + const isClass = !!staticType.symbol?.valueDeclaration && ts.isClassLike(staticType.symbol.valueDeclaration); + const staticBaseType = isClass + ? getBaseConstructorTypeOfClass(staticType as ts.InterfaceType) + : anyType; + const heritageClauses = [ + ...!ts.length(baseTypes) ? [] : [ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, ts.map(baseTypes, b => serializeBaseType(b, staticBaseType, localName)))], + ...!ts.length(implementsExpressions) ? [] : [ts.factory.createHeritageClause(ts.SyntaxKind.ImplementsKeyword, implementsExpressions)] + ]; + const symbolProps = getNonInterhitedProperties(classType, baseTypes, getPropertiesOfType(classType)); + const publicSymbolProps = ts.filter(symbolProps, s => { + // `valueDeclaration` could be undefined if inherited from + // a union/intersection base type, but inherited properties + // don't matter here. + const valueDecl = s.valueDeclaration; + return !!valueDecl && !(ts.isNamedDeclaration(valueDecl) && ts.isPrivateIdentifier(valueDecl.name)); + }); + const hasPrivateIdentifier = ts.some(symbolProps, s => { + // `valueDeclaration` could be undefined if inherited from + // a union/intersection base type, but inherited properties + // don't matter here. + const valueDecl = s.valueDeclaration; + return !!valueDecl && ts.isNamedDeclaration(valueDecl) && ts.isPrivateIdentifier(valueDecl.name); + }); + // Boil down all private properties into a single one. + const privateProperties = hasPrivateIdentifier ? + [ts.factory.createPropertyDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, localName, typeParamDecls, heritageClauses, [...indexSignatures, ...staticMembers, ...constructors, ...publicProperties, ...privateProperties]), symbol.declarations && ts.filter(symbol.declarations, d => ts.isClassDeclaration(d) || ts.isClassExpression(d))[0]), modifierFlags); - } + /*modifiers*/ undefined, ts.factory.createPrivateIdentifier("#private"), + /*questionOrExclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)] : ts.emptyArray; + const publicProperties = ts.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 = ts.flatMap(ts.filter(getPropertiesOfType(staticType), p => !(p.flags & ts.SymbolFlags.Prototype) && p.escapedName !== "prototype" && !isNamespaceMember(p)), p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType)); + // When we encounter an `X.prototype.y` assignment in a JS file, we bind `X` as a class regardless as to whether + // the value is ever initialized with a class or function-like value. For cases where `X` could never be + // created via `new`, we will inject a `private constructor()` declaration to indicate it is not createable. + const isNonConstructableClassLikeInJsFile = !isClass && + !!symbol.valueDeclaration && + ts.isInJSFile(symbol.valueDeclaration) && + !ts.some(getSignaturesOfType(staticType, ts.SignatureKind.Construct)); + const constructors = isNonConstructableClassLikeInJsFile ? + [ts.factory.createConstructorDeclaration(/*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(ts.ModifierFlags.Private), [], /*body*/ undefined)] : + serializeSignatures(ts.SignatureKind.Construct, staticType, staticBaseType, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration[]; + const indexSignatures = serializeIndexSignatures(classType, baseTypes[0]); + context.enclosingDeclaration = oldEnclosing; + addResult(ts.setTextRange(ts.factory.createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, localName, typeParamDecls, heritageClauses, [...indexSignatures, ...staticMembers, ...constructors, ...publicProperties, ...privateProperties]), symbol.declarations && ts.filter(symbol.declarations, d => ts.isClassDeclaration(d) || ts.isClassExpression(d))[0]), modifierFlags); + } - function getSomeTargetNameFromDeclarations(declarations: ts.Declaration[] | undefined) { - return ts.firstDefined(declarations, d => { - if (ts.isImportSpecifier(d) || ts.isExportSpecifier(d)) { - return ts.idText(d.propertyName || d.name); - } - if (ts.isBinaryExpression(d) || ts.isExportAssignment(d)) { - const expression = ts.isExportAssignment(d) ? d.expression : d.right; - if (ts.isPropertyAccessExpression(expression)) { - return ts.idText(expression.name); - } - } - if (isAliasSymbolDeclaration(d)) { - // This is... heuristic, at best. But it's probably better than always printing the name of the shorthand ambient module. - const name = ts.getNameOfDeclaration(d); - if (name && ts.isIdentifier(name)) { - return ts.idText(name); - } + function getSomeTargetNameFromDeclarations(declarations: ts.Declaration[] | undefined) { + return ts.firstDefined(declarations, d => { + if (ts.isImportSpecifier(d) || ts.isExportSpecifier(d)) { + return ts.idText(d.propertyName || d.name); + } + if (ts.isBinaryExpression(d) || ts.isExportAssignment(d)) { + const expression = ts.isExportAssignment(d) ? d.expression : d.right; + if (ts.isPropertyAccessExpression(expression)) { + return ts.idText(expression.name); } - return undefined; - }); - } - - function serializeAsAlias(symbol: ts.Symbol, localName: string, modifierFlags: ts.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 ts.Debug.fail(); - const target = getMergedSymbol(getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true)); - if (!target) { - return; } - // If `target` refers to a shorthand module symbol, the name we're trying to pull out isn;t recoverable from the target symbol - // In such a scenario, we must fall back to looking for an alias declaration on `symbol` and pulling the target name from that - let verbatimTargetName = ts.isShorthandAmbientModuleSymbol(target) && getSomeTargetNameFromDeclarations(symbol.declarations) || ts.unescapeLeadingUnderscores(target.escapedName); - if (verbatimTargetName === ts.InternalSymbolName.ExportEquals && (ts.getESModuleInterop(compilerOptions) || compilerOptions.allowSyntheticDefaultImports)) { - // target refers to an `export=` symbol that was hoisted into a synthetic default - rename here to match - verbatimTargetName = ts.InternalSymbolName.Default; + if (isAliasSymbolDeclaration(d)) { + // This is... heuristic, at best. But it's probably better than always printing the name of the shorthand ambient module. + const name = ts.getNameOfDeclaration(d); + if (name && ts.isIdentifier(name)) { + return ts.idText(name); + } } - const targetName = getInternalSymbolName(target, verbatimTargetName); - includePrivateSymbol(target); // the target may be within the same scope - attempt to serialize it first - switch (node.kind) { - case ts.SyntaxKind.BindingElement: - if (node.parent?.parent?.kind === ts.SyntaxKind.VariableDeclaration) { - // const { SomeClass } = require('./lib'); - const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // './lib' - const { propertyName } = node as ts.BindingElement; - addResult(ts.factory.createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, ts.factory.createNamedImports([ts.factory.createImportSpecifier( - /*isTypeOnly*/ false, propertyName && ts.isIdentifier(propertyName) ? ts.factory.createIdentifier(ts.idText(propertyName)) : undefined, ts.factory.createIdentifier(localName))])), ts.factory.createStringLiteral(specifier), - /*importClause*/ undefined), ts.ModifierFlags.None); - break; - } - // We don't know how to serialize this (nested?) binding element - ts.Debug.failBadSyntaxKind(node.parent?.parent || node, "Unhandled binding element grandparent kind in declaration serialization"); - break; - case ts.SyntaxKind.ShorthandPropertyAssignment: - if (node.parent?.parent?.kind === ts.SyntaxKind.BinaryExpression) { - // module.exports = { SomeClass } - serializeExportSpecifier(ts.unescapeLeadingUnderscores(symbol.escapedName), targetName); - } - break; - case ts.SyntaxKind.VariableDeclaration: - // commonjs require: const x = require('y') - if (ts.isPropertyAccessExpression((node as ts.VariableDeclaration).initializer!)) { - // const x = require('y').z - const initializer = (node as ts.VariableDeclaration).initializer! as ts.PropertyAccessExpression; // require('y').z - const uniqueName = ts.factory.createUniqueName(localName); // _x - const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // 'y' - // import _x = require('y'); - addResult(ts.factory.createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, uniqueName, ts.factory.createExternalModuleReference(ts.factory.createStringLiteral(specifier))), ts.ModifierFlags.None); - // import x = _x.z - addResult(ts.factory.createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createIdentifier(localName), ts.factory.createQualifiedName(uniqueName, initializer.name as ts.Identifier)), modifierFlags); - break; - } - // else fall through and treat commonjs require just like import= - case ts.SyntaxKind.ImportEqualsDeclaration: - // This _specifically_ only exists to handle json declarations - where we make aliases, but since - // we emit no declarations for the json document, must not refer to it in the declarations - if (target.escapedName === ts.InternalSymbolName.ExportEquals && ts.some(target.declarations, ts.isJsonSourceFile)) { - serializeMaybeAliasAssignment(symbol); - break; - } - // Could be a local `import localName = ns.member` or - // an external `import localName = require("whatever")` - const isLocalImport = !(target.flags & ts.SymbolFlags.ValueModule) && !ts.isVariableDeclaration(node); - addResult(ts.factory.createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createIdentifier(localName), isLocalImport - ? symbolToName(target, context, ts.SymbolFlags.All, /*expectsIdentifier*/ false) - : ts.factory.createExternalModuleReference(ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context)))), isLocalImport ? modifierFlags : ts.ModifierFlags.None); - break; - case ts.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(ts.factory.createNamespaceExportDeclaration(ts.idText((node as ts.NamespaceExportDeclaration).name)), ts.ModifierFlags.None); - break; - case ts.SyntaxKind.ImportClause: - addResult(ts.factory.createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, ts.factory.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 - ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target.parent || target, context)), - /*assertClause*/ undefined), ts.ModifierFlags.None); - break; - case ts.SyntaxKind.NamespaceImport: + return undefined; + }); + } + + function serializeAsAlias(symbol: ts.Symbol, localName: string, modifierFlags: ts.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 ts.Debug.fail(); + const target = getMergedSymbol(getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true)); + if (!target) { + return; + } + // If `target` refers to a shorthand module symbol, the name we're trying to pull out isn;t recoverable from the target symbol + // In such a scenario, we must fall back to looking for an alias declaration on `symbol` and pulling the target name from that + let verbatimTargetName = ts.isShorthandAmbientModuleSymbol(target) && getSomeTargetNameFromDeclarations(symbol.declarations) || ts.unescapeLeadingUnderscores(target.escapedName); + if (verbatimTargetName === ts.InternalSymbolName.ExportEquals && (ts.getESModuleInterop(compilerOptions) || compilerOptions.allowSyntheticDefaultImports)) { + // target refers to an `export=` symbol that was hoisted into a synthetic default - rename here to match + verbatimTargetName = ts.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 ts.SyntaxKind.BindingElement: + if (node.parent?.parent?.kind === ts.SyntaxKind.VariableDeclaration) { + // const { SomeClass } = require('./lib'); + const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // './lib' + const { propertyName } = node as ts.BindingElement; addResult(ts.factory.createImportDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*importClause*/ undefined, ts.factory.createNamespaceImport(ts.factory.createIdentifier(localName))), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context)), - /*assertClause*/ undefined), ts.ModifierFlags.None); + /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, ts.factory.createNamedImports([ts.factory.createImportSpecifier( + /*isTypeOnly*/ false, propertyName && ts.isIdentifier(propertyName) ? ts.factory.createIdentifier(ts.idText(propertyName)) : undefined, ts.factory.createIdentifier(localName))])), ts.factory.createStringLiteral(specifier), + /*importClause*/ undefined), ts.ModifierFlags.None); break; - case ts.SyntaxKind.NamespaceExport: - addResult(ts.factory.createExportDeclaration( + } + // We don't know how to serialize this (nested?) binding element + ts.Debug.failBadSyntaxKind(node.parent?.parent || node, "Unhandled binding element grandparent kind in declaration serialization"); + break; + case ts.SyntaxKind.ShorthandPropertyAssignment: + if (node.parent?.parent?.kind === ts.SyntaxKind.BinaryExpression) { + // module.exports = { SomeClass } + serializeExportSpecifier(ts.unescapeLeadingUnderscores(symbol.escapedName), targetName); + } + break; + case ts.SyntaxKind.VariableDeclaration: + // commonjs require: const x = require('y') + if (ts.isPropertyAccessExpression((node as ts.VariableDeclaration).initializer!)) { + // const x = require('y').z + const initializer = (node as ts.VariableDeclaration).initializer! as ts.PropertyAccessExpression; // require('y').z + const uniqueName = ts.factory.createUniqueName(localName); // _x + const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // 'y' + // import _x = require('y'); + addResult(ts.factory.createImportEqualsDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamespaceExport(ts.factory.createIdentifier(localName)), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context))), ts.ModifierFlags.None); - break; - case ts.SyntaxKind.ImportSpecifier: - addResult(ts.factory.createImportDeclaration( + /*isTypeOnly*/ false, uniqueName, ts.factory.createExternalModuleReference(ts.factory.createStringLiteral(specifier))), ts.ModifierFlags.None); + // import x = _x.z + addResult(ts.factory.createImportEqualsDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createImportClause( - /*isTypeOnly*/ false, - /*importClause*/ undefined, ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - /*isTypeOnly*/ false, localName !== verbatimTargetName ? ts.factory.createIdentifier(verbatimTargetName) : undefined, ts.factory.createIdentifier(localName)) - ])), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target.parent || target, context)), - /*assertClause*/ undefined), ts.ModifierFlags.None); - break; - case ts.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 ts.ExportDeclaration).moduleSpecifier; - // targetName is only used when the target is local, as otherwise the target is an alias that points at - // another file - serializeExportSpecifier(ts.unescapeLeadingUnderscores(symbol.escapedName), specifier ? verbatimTargetName : targetName, specifier && ts.isStringLiteralLike(specifier) ? ts.factory.createStringLiteral(specifier.text) : undefined); + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createIdentifier(localName), ts.factory.createQualifiedName(uniqueName, initializer.name as ts.Identifier)), modifierFlags); break; - case ts.SyntaxKind.ExportAssignment: + } + // else fall through and treat commonjs require just like import= + case ts.SyntaxKind.ImportEqualsDeclaration: + // This _specifically_ only exists to handle json declarations - where we make aliases, but since + // we emit no declarations for the json document, must not refer to it in the declarations + if (target.escapedName === ts.InternalSymbolName.ExportEquals && ts.some(target.declarations, ts.isJsonSourceFile)) { serializeMaybeAliasAssignment(symbol); break; - case ts.SyntaxKind.BinaryExpression: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - // 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 === ts.InternalSymbolName.Default || symbol.escapedName === ts.InternalSymbolName.ExportEquals) { - serializeMaybeAliasAssignment(symbol); - } - else { - serializeExportSpecifier(localName, targetName); - } - break; - default: - return ts.Debug.failBadSyntaxKind(node, "Unhandled alias declaration kind in symbol serializer!"); - } - } - - function serializeExportSpecifier(localName: string, targetName: string, specifier?: ts.Expression) { - addResult(ts.factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, localName !== targetName ? targetName : undefined, localName)]), specifier), ts.ModifierFlags.None); - } - - /** - * Returns `true` if an export assignment or declaration was produced for the symbol - */ - function serializeMaybeAliasAssignment(symbol: ts.Symbol): boolean { - if (symbol.flags & ts.SymbolFlags.Prototype) { - return false; - } - const name = ts.unescapeLeadingUnderscores(symbol.escapedName); - const isExportEquals = name === ts.InternalSymbolName.ExportEquals; - const isDefault = name === ts.InternalSymbolName.Default; - const isExportAssignmentCompatibleSymbolName = 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 && ts.length(target.declarations) && ts.some(target.declarations, d => ts.getSourceFileOfNode(d) === ts.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 = aliasDecl && ((ts.isExportAssignment(aliasDecl) || ts.isBinaryExpression(aliasDecl)) ? ts.getExportAssignmentExpression(aliasDecl) : ts.getPropertyAssignmentAliasLikeExpression(aliasDecl as ts.ShorthandPropertyAssignment | ts.PropertyAssignment | ts.PropertyAccessExpression)); - const first = expr && ts.isEntityNameExpression(expr) ? getFirstNonModuleExportsIdentifier(expr) : undefined; - const referenced = first && resolveEntityName(first, ts.SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, enclosingDeclaration); - if (referenced || target) { - includePrivateSymbol(referenced || target); - } - - // We disable the context's symbol tracker 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 = () => false; - if (isExportAssignmentCompatibleSymbolName) { - results.push(ts.factory.createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, isExportEquals, symbolToExpression(target, context, ts.SymbolFlags.All))); + } + // Could be a local `import localName = ns.member` or + // an external `import localName = require("whatever")` + const isLocalImport = !(target.flags & ts.SymbolFlags.ValueModule) && !ts.isVariableDeclaration(node); + addResult(ts.factory.createImportEqualsDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createIdentifier(localName), isLocalImport + ? symbolToName(target, context, ts.SymbolFlags.All, /*expectsIdentifier*/ false) + : ts.factory.createExternalModuleReference(ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context)))), isLocalImport ? modifierFlags : ts.ModifierFlags.None); + break; + case ts.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(ts.factory.createNamespaceExportDeclaration(ts.idText((node as ts.NamespaceExportDeclaration).name)), ts.ModifierFlags.None); + break; + case ts.SyntaxKind.ImportClause: + addResult(ts.factory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, ts.factory.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 + ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target.parent || target, context)), + /*assertClause*/ undefined), ts.ModifierFlags.None); + break; + case ts.SyntaxKind.NamespaceImport: + addResult(ts.factory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*importClause*/ undefined, ts.factory.createNamespaceImport(ts.factory.createIdentifier(localName))), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context)), + /*assertClause*/ undefined), ts.ModifierFlags.None); + break; + case ts.SyntaxKind.NamespaceExport: + addResult(ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamespaceExport(ts.factory.createIdentifier(localName)), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target, context))), ts.ModifierFlags.None); + break; + case ts.SyntaxKind.ImportSpecifier: + addResult(ts.factory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createImportClause( + /*isTypeOnly*/ false, + /*importClause*/ undefined, ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + /*isTypeOnly*/ false, localName !== verbatimTargetName ? ts.factory.createIdentifier(verbatimTargetName) : undefined, ts.factory.createIdentifier(localName)) + ])), ts.factory.createStringLiteral(getSpecifierForModuleSymbol(target.parent || target, context)), + /*assertClause*/ undefined), ts.ModifierFlags.None); + break; + case ts.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 ts.ExportDeclaration).moduleSpecifier; + // targetName is only used when the target is local, as otherwise the target is an alias that points at + // another file + serializeExportSpecifier(ts.unescapeLeadingUnderscores(symbol.escapedName), specifier ? verbatimTargetName : targetName, specifier && ts.isStringLiteralLike(specifier) ? ts.factory.createStringLiteral(specifier.text) : undefined); + break; + case ts.SyntaxKind.ExportAssignment: + serializeMaybeAliasAssignment(symbol); + break; + case ts.SyntaxKind.BinaryExpression: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + // 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 === ts.InternalSymbolName.Default || symbol.escapedName === ts.InternalSymbolName.ExportEquals) { + serializeMaybeAliasAssignment(symbol); } else { - if (first === expr && first) { - // serialize as `export {target as name}` - serializeExportSpecifier(name, ts.idText(first)); - } - else if (expr && ts.isClassExpression(expr)) { - serializeExportSpecifier(name, getInternalSymbolName(target, ts.symbolName(target))); - } - else { - // serialize as `import _Ref = t.arg.et; export { _Ref as name }` - const varName = getUnusedName(name, symbol); - addResult(ts.factory.createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, ts.factory.createIdentifier(varName), symbolToName(target, context, ts.SymbolFlags.All, /*expectsIdentifier*/ false)), ts.ModifierFlags.None); - serializeExportSpecifier(name, varName); - } + serializeExportSpecifier(localName, targetName); } - context.tracker.trackSymbol = oldTrack; - return true; + break; + default: + return ts.Debug.failBadSyntaxKind(node, "Unhandled alias declaration kind in symbol serializer!"); + } + } + + function serializeExportSpecifier(localName: string, targetName: string, specifier?: ts.Expression) { + addResult(ts.factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier(/*isTypeOnly*/ false, localName !== targetName ? targetName : undefined, localName)]), specifier), ts.ModifierFlags.None); + } + + /** + * Returns `true` if an export assignment or declaration was produced for the symbol + */ + function serializeMaybeAliasAssignment(symbol: ts.Symbol): boolean { + if (symbol.flags & ts.SymbolFlags.Prototype) { + return false; + } + const name = ts.unescapeLeadingUnderscores(symbol.escapedName); + const isExportEquals = name === ts.InternalSymbolName.ExportEquals; + const isDefault = name === ts.InternalSymbolName.Default; + const isExportAssignmentCompatibleSymbolName = 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 && ts.length(target.declarations) && ts.some(target.declarations, d => ts.getSourceFileOfNode(d) === ts.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 = aliasDecl && ((ts.isExportAssignment(aliasDecl) || ts.isBinaryExpression(aliasDecl)) ? ts.getExportAssignmentExpression(aliasDecl) : ts.getPropertyAssignmentAliasLikeExpression(aliasDecl as ts.ShorthandPropertyAssignment | ts.PropertyAssignment | ts.PropertyAccessExpression)); + const first = expr && ts.isEntityNameExpression(expr) ? getFirstNonModuleExportsIdentifier(expr) : undefined; + const referenced = first && resolveEntityName(first, ts.SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, enclosingDeclaration); + if (referenced || target) { + includePrivateSymbol(referenced || target); + } + + // We disable the context's symbol tracker 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 = () => false; + if (isExportAssignmentCompatibleSymbolName) { + results.push(ts.factory.createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, isExportEquals, symbolToExpression(target, context, ts.SymbolFlags.All))); } 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(getMergedSymbol(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, isExportAssignmentCompatibleSymbolName ? ts.ModifierFlags.None : ts.ModifierFlags.Export); + if (first === expr && first) { + // serialize as `export {target as name}` + serializeExportSpecifier(name, ts.idText(first)); + } + else if (expr && ts.isClassExpression(expr)) { + serializeExportSpecifier(name, getInternalSymbolName(target, ts.symbolName(target))); } else { - const statement = ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration(varName, /*exclamationToken*/ undefined, serializeTypeForDeclaration(context, typeToSerialize, symbol, enclosingDeclaration, includePrivateSymbol, bundled)) - ], ts.NodeFlags.Const)); - // Inlined JSON types exported with [module.]exports= will already emit an export=, so should use `declare`. - // Otherwise, the type itself should be exported. - addResult(statement, target && target.flags & ts.SymbolFlags.Property && target.escapedName === ts.InternalSymbolName.ExportEquals ? ts.ModifierFlags.Ambient - : name === varName ? ts.ModifierFlags.Export - : ts.ModifierFlags.None); - } - if (isExportAssignmentCompatibleSymbolName) { - results.push(ts.factory.createExportAssignment( + // serialize as `import _Ref = t.arg.et; export { _Ref as name }` + const varName = getUnusedName(name, symbol); + addResult(ts.factory.createImportEqualsDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, isExportEquals, ts.factory.createIdentifier(varName))); - return true; - } - else if (name !== varName) { + /*modifiers*/ undefined, + /*isTypeOnly*/ false, ts.factory.createIdentifier(varName), symbolToName(target, context, ts.SymbolFlags.All, /*expectsIdentifier*/ false)), ts.ModifierFlags.None); serializeExportSpecifier(name, varName); - return true; } - return false; } + context.tracker.trackSymbol = oldTrack; + return true; + } + 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(getMergedSymbol(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, isExportAssignmentCompatibleSymbolName ? ts.ModifierFlags.None : ts.ModifierFlags.Export); + } + else { + const statement = ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration(varName, /*exclamationToken*/ undefined, serializeTypeForDeclaration(context, typeToSerialize, symbol, enclosingDeclaration, includePrivateSymbol, bundled)) + ], ts.NodeFlags.Const)); + // Inlined JSON types exported with [module.]exports= will already emit an export=, so should use `declare`. + // Otherwise, the type itself should be exported. + addResult(statement, target && target.flags & ts.SymbolFlags.Property && target.escapedName === ts.InternalSymbolName.ExportEquals ? ts.ModifierFlags.Ambient + : name === varName ? ts.ModifierFlags.Export + : ts.ModifierFlags.None); + } + if (isExportAssignmentCompatibleSymbolName) { + results.push(ts.factory.createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, isExportEquals, ts.factory.createIdentifier(varName))); + return true; + } + else if (name !== varName) { + serializeExportSpecifier(name, varName); + return true; + } + return false; } + } - 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 = ts.getSourceFileOfNode(context.enclosingDeclaration); - return ts.getObjectFlags(typeToSerialize) & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped) && - !ts.length(getIndexInfosOfType(typeToSerialize)) && - !isClassInstanceSide(typeToSerialize) && // While a class instance is potentially representable as a NS, prefer printing a reference to the instance type and serializing the class - !!(ts.length(ts.filter(getPropertiesOfType(typeToSerialize), isNamespaceMember)) || ts.length(getSignaturesOfType(typeToSerialize, ts.SignatureKind.Call))) && - !ts.length(getSignaturesOfType(typeToSerialize, ts.SignatureKind.Construct)) && // TODO: could probably serialize as function + ns + class, now that that's OK - !getDeclarationWithTypeAnnotation(hostSymbol, enclosingDeclaration) && - !(typeToSerialize.symbol && ts.some(typeToSerialize.symbol.declarations, d => ts.getSourceFileOfNode(d) !== ctxSrc)) && - !ts.some(getPropertiesOfType(typeToSerialize), p => isLateBoundName(p.escapedName)) && - !ts.some(getPropertiesOfType(typeToSerialize), p => ts.some(p.declarations, d => ts.getSourceFileOfNode(d) !== ctxSrc)) && - ts.every(getPropertiesOfType(typeToSerialize), p => ts.isIdentifierText(ts.symbolName(p), languageVersion)); - } - function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: true): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]); - function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: false): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | T[]); - function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: boolean): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]) { - return function serializePropertySymbol(p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined): (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]) { - const modifierFlags = ts.getDeclarationModifierFlagsFromSymbol(p); - const isPrivate = !!(modifierFlags & ts.ModifierFlags.Private); - if (isStatic && (p.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.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 & ts.SymbolFlags.Prototype || - (baseType && getPropertyOfType(baseType, p.escapedName) - && isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p) - && (p.flags & ts.SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & ts.SymbolFlags.Optional) - && isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) { - return []; - } - const flag = (modifierFlags & ~ts.ModifierFlags.Async) | (isStatic ? ts.ModifierFlags.Static : 0); - const name = getPropertyNameNodeForSymbol(p, context); - const firstPropertyLikeDecl = p.declarations?.find(ts.or(ts.isPropertyDeclaration, ts.isAccessor, ts.isVariableDeclaration, ts.isPropertySignature, ts.isBinaryExpression, ts.isPropertyAccessExpression)); - if (p.flags & ts.SymbolFlags.Accessor && useAccessors) { - const result: ts.AccessorDeclaration[] = []; - if (p.flags & ts.SymbolFlags.SetAccessor) { - result.push(ts.setTextRange(ts.factory.createSetAccessorDeclaration( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(flag), name, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, "arg", - /*questionToken*/ undefined, isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled))], - /*body*/ undefined), p.declarations?.find(ts.isSetAccessor) || firstPropertyLikeDecl)); - } - if (p.flags & ts.SymbolFlags.GetAccessor) { - const isPrivate = modifierFlags & ts.ModifierFlags.Private; - result.push(ts.setTextRange(ts.factory.createGetAccessorDeclaration( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(flag), name, [], isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled), - /*body*/ undefined), p.declarations?.find(ts.isGetAccessor) || firstPropertyLikeDecl)); - } - return result; + 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 = ts.getSourceFileOfNode(context.enclosingDeclaration); + return ts.getObjectFlags(typeToSerialize) & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped) && + !ts.length(getIndexInfosOfType(typeToSerialize)) && + !isClassInstanceSide(typeToSerialize) && // While a class instance is potentially representable as a NS, prefer printing a reference to the instance type and serializing the class + !!(ts.length(ts.filter(getPropertiesOfType(typeToSerialize), isNamespaceMember)) || ts.length(getSignaturesOfType(typeToSerialize, ts.SignatureKind.Call))) && + !ts.length(getSignaturesOfType(typeToSerialize, ts.SignatureKind.Construct)) && // TODO: could probably serialize as function + ns + class, now that that's OK + !getDeclarationWithTypeAnnotation(hostSymbol, enclosingDeclaration) && + !(typeToSerialize.symbol && ts.some(typeToSerialize.symbol.declarations, d => ts.getSourceFileOfNode(d) !== ctxSrc)) && + !ts.some(getPropertiesOfType(typeToSerialize), p => isLateBoundName(p.escapedName)) && + !ts.some(getPropertiesOfType(typeToSerialize), p => ts.some(p.declarations, d => ts.getSourceFileOfNode(d) !== ctxSrc)) && + ts.every(getPropertiesOfType(typeToSerialize), p => ts.isIdentifierText(ts.symbolName(p), languageVersion)); + } + function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: true): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]); + function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: false): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | T[]); + function makeSerializePropertySymbol(createProperty: (decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) => T, methodKind: ts.SignatureDeclaration["kind"], useAccessors: boolean): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]) { + return function serializePropertySymbol(p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined): (T | ts.AccessorDeclaration | (T | ts.AccessorDeclaration)[]) { + const modifierFlags = ts.getDeclarationModifierFlagsFromSymbol(p); + const isPrivate = !!(modifierFlags & ts.ModifierFlags.Private); + if (isStatic && (p.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.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 & ts.SymbolFlags.Prototype || + (baseType && getPropertyOfType(baseType, p.escapedName) + && isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p) + && (p.flags & ts.SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & ts.SymbolFlags.Optional) + && isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) { + return []; + } + const flag = (modifierFlags & ~ts.ModifierFlags.Async) | (isStatic ? ts.ModifierFlags.Static : 0); + const name = getPropertyNameNodeForSymbol(p, context); + const firstPropertyLikeDecl = p.declarations?.find(ts.or(ts.isPropertyDeclaration, ts.isAccessor, ts.isVariableDeclaration, ts.isPropertySignature, ts.isBinaryExpression, ts.isPropertyAccessExpression)); + if (p.flags & ts.SymbolFlags.Accessor && useAccessors) { + const result: ts.AccessorDeclaration[] = []; + if (p.flags & ts.SymbolFlags.SetAccessor) { + result.push(ts.setTextRange(ts.factory.createSetAccessorDeclaration( + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(flag), name, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, "arg", + /*questionToken*/ undefined, isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled))], + /*body*/ undefined), p.declarations?.find(ts.isSetAccessor) || firstPropertyLikeDecl)); + } + if (p.flags & ts.SymbolFlags.GetAccessor) { + const isPrivate = modifierFlags & ts.ModifierFlags.Private; + result.push(ts.setTextRange(ts.factory.createGetAccessorDeclaration( + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(flag), name, [], isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled), + /*body*/ undefined), p.declarations?.find(ts.isGetAccessor) || firstPropertyLikeDecl)); } - // 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 & (ts.SymbolFlags.Property | ts.SymbolFlags.Variable | ts.SymbolFlags.Accessor)) { + 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 & (ts.SymbolFlags.Property | ts.SymbolFlags.Variable | ts.SymbolFlags.Accessor)) { + return ts.setTextRange(createProperty( + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags((isReadonlySymbol(p) ? ts.ModifierFlags.Readonly : 0) | flag), name, p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled), + // TODO: https://github.com/microsoft/TypeScript/pull/32372#discussion_r328386357 + // interface members can't have initializers, however class members _can_ + /*initializer*/ undefined), p.declarations?.find(ts.or(ts.isPropertyDeclaration, ts.isVariableDeclaration)) || firstPropertyLikeDecl); + } + if (p.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.Function)) { + const type = getTypeOfSymbol(p); + const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); + if (flag & ts.ModifierFlags.Private) { return ts.setTextRange(createProperty( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags((isReadonlySymbol(p) ? ts.ModifierFlags.Readonly : 0) | flag), name, p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, isPrivate ? undefined : serializeTypeForDeclaration(context, getTypeOfSymbol(p), p, enclosingDeclaration, includePrivateSymbol, bundled), - // TODO: https://github.com/microsoft/TypeScript/pull/32372#discussion_r328386357 - // interface members can't have initializers, however class members _can_ - /*initializer*/ undefined), p.declarations?.find(ts.or(ts.isPropertyDeclaration, ts.isVariableDeclaration)) || firstPropertyLikeDecl); - } - if (p.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.Function)) { - const type = getTypeOfSymbol(p); - const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); - if (flag & ts.ModifierFlags.Private) { - return ts.setTextRange(createProperty( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags((isReadonlySymbol(p) ? ts.ModifierFlags.Readonly : 0) | flag), name, p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, - /*type*/ undefined, - /*initializer*/ undefined), p.declarations?.find(ts.isFunctionLikeDeclaration) || signatures[0] && signatures[0].declaration || p.declarations && 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, { - name, - questionToken: p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, - modifiers: flag ? ts.factory.createModifiersFromModifierFlags(flag) : undefined - }); - const location = sig.declaration && ts.isPrototypePropertyAssignment(sig.declaration.parent) ? sig.declaration.parent : sig.declaration; - results.push(ts.setTextRange(decl, location)); - } - return results as unknown as T[]; + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags((isReadonlySymbol(p) ? ts.ModifierFlags.Readonly : 0) | flag), name, p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, + /*type*/ undefined, + /*initializer*/ undefined), p.declarations?.find(ts.isFunctionLikeDeclaration) || signatures[0] && signatures[0].declaration || p.declarations && 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, { + name, + questionToken: p.flags & ts.SymbolFlags.Optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, + modifiers: flag ? ts.factory.createModifiersFromModifierFlags(flag) : undefined + }); + const location = sig.declaration && ts.isPrototypePropertyAssignment(sig.declaration.parent) ? sig.declaration.parent : sig.declaration; + results.push(ts.setTextRange(decl, location)); } - // The `Constructor`'s symbol isn't in the class's properties lists, obviously, since it's a signature on the static - return ts.Debug.fail(`Unhandled class member kind! ${(p as any).__debugFlags || p.flags}`); - }; - } + 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 ts.Debug.fail(`Unhandled class member kind! ${(p as any).__debugFlags || p.flags}`); + }; + } - function serializePropertySymbolForInterface(p: ts.Symbol, baseType: ts.Type | undefined) { - return serializePropertySymbolForInterfaceWorker(p, /*isStatic*/ false, baseType); - } + function serializePropertySymbolForInterface(p: ts.Symbol, baseType: ts.Type | undefined) { + return serializePropertySymbolForInterfaceWorker(p, /*isStatic*/ false, baseType); + } - function serializeSignatures(kind: ts.SignatureKind, input: ts.Type, baseType: ts.Type | undefined, outputKind: ts.SignatureDeclaration["kind"]) { - const signatures = getSignaturesOfType(input, kind); - if (kind === ts.SignatureKind.Construct) { - if (!baseType && ts.every(signatures, s => ts.length(s.parameters) === 0)) { - return []; // No base type, every constructor is empty - elide the extraneous `constructor()` + function serializeSignatures(kind: ts.SignatureKind, input: ts.Type, baseType: ts.Type | undefined, outputKind: ts.SignatureDeclaration["kind"]) { + const signatures = getSignaturesOfType(input, kind); + if (kind === ts.SignatureKind.Construct) { + if (!baseType && ts.every(signatures, s => ts.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, ts.SignatureKind.Construct); + if (!ts.length(baseSigs) && ts.every(signatures, s => ts.length(s.parameters) === 0)) { + return []; // Base had no explicit signatures, if all our signatures are also implicit, return an empty list } - 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, ts.SignatureKind.Construct); - if (!ts.length(baseSigs) && ts.every(signatures, s => ts.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 + 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; } } - } - let privateProtected: ts.ModifierFlags = 0; - for (const s of signatures) { - if (s.declaration) { - privateProtected |= ts.getSelectedEffectiveModifierFlags(s.declaration, ts.ModifierFlags.Private | ts.ModifierFlags.Protected); + if (!failed) { + return []; // Every signature was identical - elide constructor list as it is inherited } } - if (privateProtected) { - return [ts.setTextRange(ts.factory.createConstructorDeclaration( - /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(privateProtected), - /*parameters*/ [], - /*body*/ undefined), signatures[0].declaration)]; + } + let privateProtected: ts.ModifierFlags = 0; + for (const s of signatures) { + if (s.declaration) { + privateProtected |= ts.getSelectedEffectiveModifierFlags(s.declaration, ts.ModifierFlags.Private | ts.ModifierFlags.Protected); } } - - const results = []; - for (const sig of signatures) { - // Each overload becomes a separate constructor declaration, in order - const decl = signatureToSignatureDeclarationHelper(sig, outputKind, context); - results.push(ts.setTextRange(decl, sig.declaration)); + if (privateProtected) { + return [ts.setTextRange(ts.factory.createConstructorDeclaration( + /*decorators*/ undefined, ts.factory.createModifiersFromModifierFlags(privateProtected), + /*parameters*/ [], + /*body*/ undefined), signatures[0].declaration)]; } - return results; } - function serializeIndexSignatures(input: ts.Type, baseType: ts.Type | undefined) { - const results: ts.IndexSignatureDeclaration[] = []; - for (const info of getIndexInfosOfType(input)) { - if (baseType) { - const baseInfo = getIndexInfoOfType(baseType, info.keyType); - if (baseInfo) { - if (isTypeIdenticalTo(info.type, baseInfo.type)) { - continue; // elide identical index signatures - } + const results = []; + for (const sig of signatures) { + // Each overload becomes a separate constructor declaration, in order + const decl = signatureToSignatureDeclarationHelper(sig, outputKind, context); + results.push(ts.setTextRange(decl, sig.declaration)); + } + return results; + } + + function serializeIndexSignatures(input: ts.Type, baseType: ts.Type | undefined) { + const results: ts.IndexSignatureDeclaration[] = []; + for (const info of getIndexInfosOfType(input)) { + if (baseType) { + const baseInfo = getIndexInfoOfType(baseType, info.keyType); + if (baseInfo) { + if (isTypeIdenticalTo(info.type, baseInfo.type)) { + continue; // elide identical index signatures } } - results.push(indexInfoToIndexSignatureDeclarationHelper(info, context, /*typeNode*/ undefined)); } - return results; + results.push(indexInfoToIndexSignatureDeclarationHelper(info, context, /*typeNode*/ undefined)); } + return results; + } - function serializeBaseType(t: ts.Type, staticType: ts.Type, rootName: string) { - const ref = trySerializeAsTypeReference(t, ts.SymbolFlags.Value); - if (ref) { - return ref; - } - const tempName = getUnusedName(`${rootName}_base`); - const statement = ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration(tempName, /*exclamationToken*/ undefined, typeToTypeNodeHelper(staticType, context)) - ], ts.NodeFlags.Const)); - addResult(statement, ts.ModifierFlags.None); - return ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier(tempName), /*typeArgs*/ undefined); + function serializeBaseType(t: ts.Type, staticType: ts.Type, rootName: string) { + const ref = trySerializeAsTypeReference(t, ts.SymbolFlags.Value); + if (ref) { + return ref; } - function trySerializeAsTypeReference(t: ts.Type, flags: ts.SymbolFlags) { - let typeArgs: ts.TypeNode[] | undefined; - let reference: ts.Expression | undefined; + const tempName = getUnusedName(`${rootName}_base`); + const statement = ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration(tempName, /*exclamationToken*/ undefined, typeToTypeNodeHelper(staticType, context)) + ], ts.NodeFlags.Const)); + addResult(statement, ts.ModifierFlags.None); + return ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier(tempName), /*typeArgs*/ undefined); + } + function trySerializeAsTypeReference(t: ts.Type, flags: ts.SymbolFlags) { + let typeArgs: ts.TypeNode[] | undefined; + let reference: ts.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 ts.TypeReference).target && isSymbolAccessibleByFlags((t as ts.TypeReference).target.symbol, enclosingDeclaration, flags)) { - typeArgs = ts.map(getTypeArguments(t as ts.TypeReference), t => typeToTypeNodeHelper(t, context)); - reference = symbolToExpression((t as ts.TypeReference).target.symbol, context, ts.SymbolFlags.Type); - } - else if (t.symbol && isSymbolAccessibleByFlags(t.symbol, enclosingDeclaration, flags)) { - reference = symbolToExpression(t.symbol, context, ts.SymbolFlags.Type); - } - if (reference) { - return ts.factory.createExpressionWithTypeArguments(reference, typeArgs); - } + // 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 ts.TypeReference).target && isSymbolAccessibleByFlags((t as ts.TypeReference).target.symbol, enclosingDeclaration, flags)) { + typeArgs = ts.map(getTypeArguments(t as ts.TypeReference), t => typeToTypeNodeHelper(t, context)); + reference = symbolToExpression((t as ts.TypeReference).target.symbol, context, ts.SymbolFlags.Type); } - - function serializeImplementedType(t: ts.Type) { - const ref = trySerializeAsTypeReference(t, ts.SymbolFlags.Type); - if (ref) { - return ref; - } - if (t.symbol) { - return ts.factory.createExpressionWithTypeArguments(symbolToExpression(t.symbol, context, ts.SymbolFlags.Type), /*typeArgs*/ undefined); - } + else if (t.symbol && isSymbolAccessibleByFlags(t.symbol, enclosingDeclaration, flags)) { + reference = symbolToExpression(t.symbol, context, ts.SymbolFlags.Type); } - - function getUnusedName(input: string, symbol?: ts.Symbol): string { - const id = symbol ? getSymbolId(symbol) : undefined; - if (id) { - if (context.remappedSymbolNames!.has(id)) { - return context.remappedSymbolNames!.get(id)!; - } - } - if (symbol) { - input = getNameCandidateWorker(symbol, input); - } - let i = 0; - const original = input; - while (context.usedSymbolNames?.has(input)) { - i++; - input = `${original}_${i}`; - } - context.usedSymbolNames?.add(input); - if (id) { - context.remappedSymbolNames!.set(id, input); - } - return input; + if (reference) { + return ts.factory.createExpressionWithTypeArguments(reference, typeArgs); } + } - function getNameCandidateWorker(symbol: ts.Symbol, localName: string) { - if (localName === ts.InternalSymbolName.Default || localName === ts.InternalSymbolName.Class || localName === ts.InternalSymbolName.Function) { - const flags = context.flags; - context.flags |= ts.NodeBuilderFlags.InInitialEntityName; - const nameCandidate = getNameOfSymbolAsWritten(symbol, context); - context.flags = flags; - localName = nameCandidate.length > 0 && ts.isSingleOrDoubleQuote(nameCandidate.charCodeAt(0)) ? ts.stripQuotes(nameCandidate) : nameCandidate; - } - if (localName === ts.InternalSymbolName.Default) { - localName = "_default"; - } - else if (localName === ts.InternalSymbolName.ExportEquals) { - localName = "_exports"; - } - localName = ts.isIdentifierText(localName, languageVersion) && !ts.isStringANonContextualKeyword(localName) ? localName : "_" + localName.replace(/[^a-zA-Z0-9]/g, "_"); - return localName; + function serializeImplementedType(t: ts.Type) { + const ref = trySerializeAsTypeReference(t, ts.SymbolFlags.Type); + if (ref) { + return ref; } + if (t.symbol) { + return ts.factory.createExpressionWithTypeArguments(symbolToExpression(t.symbol, context, ts.SymbolFlags.Type), /*typeArgs*/ undefined); + } + } - function getInternalSymbolName(symbol: ts.Symbol, localName: string) { - const id = getSymbolId(symbol); + function getUnusedName(input: string, symbol?: ts.Symbol): string { + const id = symbol ? getSymbolId(symbol) : undefined; + if (id) { if (context.remappedSymbolNames!.has(id)) { return context.remappedSymbolNames!.get(id)!; } - 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(id, localName); - return localName; } + if (symbol) { + input = getNameCandidateWorker(symbol, input); + } + let i = 0; + const original = input; + while (context.usedSymbolNames?.has(input)) { + i++; + input = `${original}_${i}`; + } + context.usedSymbolNames?.add(input); + if (id) { + context.remappedSymbolNames!.set(id, input); + } + return input; + } + + function getNameCandidateWorker(symbol: ts.Symbol, localName: string) { + if (localName === ts.InternalSymbolName.Default || localName === ts.InternalSymbolName.Class || localName === ts.InternalSymbolName.Function) { + const flags = context.flags; + context.flags |= ts.NodeBuilderFlags.InInitialEntityName; + const nameCandidate = getNameOfSymbolAsWritten(symbol, context); + context.flags = flags; + localName = nameCandidate.length > 0 && ts.isSingleOrDoubleQuote(nameCandidate.charCodeAt(0)) ? ts.stripQuotes(nameCandidate) : nameCandidate; + } + if (localName === ts.InternalSymbolName.Default) { + localName = "_default"; + } + else if (localName === ts.InternalSymbolName.ExportEquals) { + localName = "_exports"; + } + localName = ts.isIdentifierText(localName, languageVersion) && !ts.isStringANonContextualKeyword(localName) ? localName : "_" + localName.replace(/[^a-zA-Z0-9]/g, "_"); + return localName; } - } - function typePredicateToString(typePredicate: ts.TypePredicate, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer?: ts.EmitTextWriter): string { - return writer ? typePredicateToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(typePredicateToStringWorker); - function typePredicateToStringWorker(writer: ts.EmitTextWriter) { - const predicate = ts.factory.createTypePredicateNode(typePredicate.kind === ts.TypePredicateKind.AssertsThis || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? ts.factory.createToken(ts.SyntaxKind.AssertsKeyword) : undefined, typePredicate.kind === ts.TypePredicateKind.Identifier || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? ts.factory.createIdentifier(typePredicate.parameterName) : ts.factory.createThisTypeNode(), typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName)! // TODO: GH#18217 - ); - const printer = ts.createPrinter({ removeComments: true }); - const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(ts.EmitHint.Unspecified, predicate, /*sourceFile*/ sourceFile, writer); - return writer; + function getInternalSymbolName(symbol: ts.Symbol, localName: string) { + const id = getSymbolId(symbol); + if (context.remappedSymbolNames!.has(id)) { + return context.remappedSymbolNames!.get(id)!; + } + 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(id, localName); + return localName; } } + } - function formatUnionTypes(types: readonly ts.Type[]): ts.Type[] { - const result: ts.Type[] = []; - let flags: ts.TypeFlags = 0; - for (let i = 0; i < types.length; i++) { - const t = types[i]; - flags |= t.flags; - if (!(t.flags & ts.TypeFlags.Nullable)) { - if (t.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.EnumLiteral)) { - const baseType = t.flags & ts.TypeFlags.BooleanLiteral ? booleanType : getBaseTypeOfEnumLiteralType(t as ts.LiteralType); - if (baseType.flags & ts.TypeFlags.Union) { - const count = (baseType as ts.UnionType).types.length; - if (i + count <= types.length && getRegularTypeOfLiteralType(types[i + count - 1]) === getRegularTypeOfLiteralType((baseType as ts.UnionType).types[count - 1])) { - result.push(baseType); - i += count - 1; - continue; - } + function typePredicateToString(typePredicate: ts.TypePredicate, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer?: ts.EmitTextWriter): string { + return writer ? typePredicateToStringWorker(writer).getText() : ts.usingSingleLineStringWriter(typePredicateToStringWorker); + function typePredicateToStringWorker(writer: ts.EmitTextWriter) { + const predicate = ts.factory.createTypePredicateNode(typePredicate.kind === ts.TypePredicateKind.AssertsThis || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? ts.factory.createToken(ts.SyntaxKind.AssertsKeyword) : undefined, typePredicate.kind === ts.TypePredicateKind.Identifier || typePredicate.kind === ts.TypePredicateKind.AssertsIdentifier ? ts.factory.createIdentifier(typePredicate.parameterName) : ts.factory.createThisTypeNode(), typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.WriteTypeParametersInQualifiedName)! // TODO: GH#18217 + ); + const printer = ts.createPrinter({ removeComments: true }); + const sourceFile = enclosingDeclaration && ts.getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(ts.EmitHint.Unspecified, predicate, /*sourceFile*/ sourceFile, writer); + return writer; + } + } + + function formatUnionTypes(types: readonly ts.Type[]): ts.Type[] { + const result: ts.Type[] = []; + let flags: ts.TypeFlags = 0; + for (let i = 0; i < types.length; i++) { + const t = types[i]; + flags |= t.flags; + if (!(t.flags & ts.TypeFlags.Nullable)) { + if (t.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.EnumLiteral)) { + const baseType = t.flags & ts.TypeFlags.BooleanLiteral ? booleanType : getBaseTypeOfEnumLiteralType(t as ts.LiteralType); + if (baseType.flags & ts.TypeFlags.Union) { + const count = (baseType as ts.UnionType).types.length; + if (i + count <= types.length && getRegularTypeOfLiteralType(types[i + count - 1]) === getRegularTypeOfLiteralType((baseType as ts.UnionType).types[count - 1])) { + result.push(baseType); + i += count - 1; + continue; } } - result.push(t); } + result.push(t); } - if (flags & ts.TypeFlags.Null) - result.push(nullType); - if (flags & ts.TypeFlags.Undefined) - result.push(undefinedType); - return result || types; } + if (flags & ts.TypeFlags.Null) + result.push(nullType); + if (flags & ts.TypeFlags.Undefined) + result.push(undefinedType); + return result || types; + } - function visibilityToString(flags: ts.ModifierFlags): string | undefined { - if (flags === ts.ModifierFlags.Private) { - return "private"; - } - if (flags === ts.ModifierFlags.Protected) { - return "protected"; - } - return "public"; + function visibilityToString(flags: ts.ModifierFlags): string | undefined { + if (flags === ts.ModifierFlags.Private) { + return "private"; + } + if (flags === ts.ModifierFlags.Protected) { + return "protected"; } + return "public"; + } - function getTypeAliasForTypeLiteral(type: ts.Type): ts.Symbol | undefined { - if (type.symbol && type.symbol.flags & ts.SymbolFlags.TypeLiteral && type.symbol.declarations) { - const node = ts.walkUpParenthesizedTypes(type.symbol.declarations[0].parent); - if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) { - return getSymbolOfNode(node); - } + function getTypeAliasForTypeLiteral(type: ts.Type): ts.Symbol | undefined { + if (type.symbol && type.symbol.flags & ts.SymbolFlags.TypeLiteral && type.symbol.declarations) { + const node = ts.walkUpParenthesizedTypes(type.symbol.declarations[0].parent); + if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) { + return getSymbolOfNode(node); } - return undefined; } + return undefined; + } - function isTopLevelInExternalModuleAugmentation(node: ts.Node): boolean { - return node && node.parent && - node.parent.kind === ts.SyntaxKind.ModuleBlock && - ts.isExternalModuleAugmentation(node.parent.parent); - } - - interface NodeBuilderContext { - enclosingDeclaration: ts.Node | undefined; - flags: ts.NodeBuilderFlags; - tracker: ts.SymbolTracker; - - // State - encounteredError: boolean; - reportedDiagnostic: boolean; - visitedTypes: ts.Set | undefined; - symbolDepth: ts.ESMap | undefined; - inferTypeParameters: ts.TypeParameter[] | undefined; - approximateLength: number; - truncating?: boolean; - typeParameterSymbolList?: ts.Set; - typeParameterNames?: ts.ESMap; - typeParameterNamesByText?: ts.Set; - typeParameterNamesByTextNextNameCount?: ts.ESMap; - usedSymbolNames?: ts.Set; - remappedSymbolNames?: ts.ESMap; - reverseMappedStack?: ts.ReverseMappedSymbol[]; - } - function isDefaultBindingContext(location: ts.Node) { - return location.kind === ts.SyntaxKind.SourceFile || ts.isAmbientModule(location); - } - function getNameOfSymbolFromNameType(symbol: ts.Symbol, context?: NodeBuilderContext) { - const nameType = getSymbolLinks(symbol).nameType; - if (nameType) { - if (nameType.flags & ts.TypeFlags.StringOrNumberLiteral) { - const name = "" + (nameType as ts.StringLiteralType | ts.NumberLiteralType).value; - if (!ts.isIdentifierText(name, ts.getEmitScriptTarget(compilerOptions)) && !ts.isNumericLiteralName(name)) { - return `"${ts.escapeString(name, ts.CharacterCodes.doubleQuote)}"`; - } - if (ts.isNumericLiteralName(name) && ts.startsWith(name, "-")) { - return `[${name}]`; - } - return name; + function isTopLevelInExternalModuleAugmentation(node: ts.Node): boolean { + return node && node.parent && + node.parent.kind === ts.SyntaxKind.ModuleBlock && + ts.isExternalModuleAugmentation(node.parent.parent); + } + + interface NodeBuilderContext { + enclosingDeclaration: ts.Node | undefined; + flags: ts.NodeBuilderFlags; + tracker: ts.SymbolTracker; + + // State + encounteredError: boolean; + reportedDiagnostic: boolean; + visitedTypes: ts.Set | undefined; + symbolDepth: ts.ESMap | undefined; + inferTypeParameters: ts.TypeParameter[] | undefined; + approximateLength: number; + truncating?: boolean; + typeParameterSymbolList?: ts.Set; + typeParameterNames?: ts.ESMap; + typeParameterNamesByText?: ts.Set; + typeParameterNamesByTextNextNameCount?: ts.ESMap; + usedSymbolNames?: ts.Set; + remappedSymbolNames?: ts.ESMap; + reverseMappedStack?: ts.ReverseMappedSymbol[]; + } + function isDefaultBindingContext(location: ts.Node) { + return location.kind === ts.SyntaxKind.SourceFile || ts.isAmbientModule(location); + } + function getNameOfSymbolFromNameType(symbol: ts.Symbol, context?: NodeBuilderContext) { + const nameType = getSymbolLinks(symbol).nameType; + if (nameType) { + if (nameType.flags & ts.TypeFlags.StringOrNumberLiteral) { + const name = "" + (nameType as ts.StringLiteralType | ts.NumberLiteralType).value; + if (!ts.isIdentifierText(name, ts.getEmitScriptTarget(compilerOptions)) && !ts.isNumericLiteralName(name)) { + return `"${ts.escapeString(name, ts.CharacterCodes.doubleQuote)}"`; } - if (nameType.flags & ts.TypeFlags.UniqueESSymbol) { - return `[${getNameOfSymbolAsWritten((nameType as ts.UniqueESSymbolType).symbol, context)}]`; + if (ts.isNumericLiteralName(name) && ts.startsWith(name, "-")) { + return `[${name}]`; } + return name; + } + if (nameType.flags & ts.TypeFlags.UniqueESSymbol) { + return `[${getNameOfSymbolAsWritten((nameType as ts.UniqueESSymbolType).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 === ts.InternalSymbolName.Default && !(context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) && - // If it's not the first part of an entity name, it must print as `default` - (!(context.flags & ts.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 && ts.findAncestor(symbol.declarations[0], isDefaultBindingContext) !== ts.findAncestor(context.enclosingDeclaration, isDefaultBindingContext)))) { - return "default"; - } - if (symbol.declarations && symbol.declarations.length) { - let declaration = ts.firstDefined(symbol.declarations, d => ts.getNameOfDeclaration(d) ? d : undefined); // Try using a declaration with a name, first - const name = declaration && ts.getNameOfDeclaration(declaration); - if (declaration && name) { - if (ts.isCallExpression(declaration) && ts.isBindableObjectDefinePropertyCall(declaration)) { - return ts.symbolName(symbol); - } - if (ts.isComputedPropertyName(name) && !(ts.getCheckFlags(symbol) & ts.CheckFlags.Late)) { - const nameType = getSymbolLinks(symbol).nameType; - if (nameType && nameType.flags & ts.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; - } + /** + * 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 === ts.InternalSymbolName.Default && !(context.flags & ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) && + // If it's not the first part of an entity name, it must print as `default` + (!(context.flags & ts.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 && ts.findAncestor(symbol.declarations[0], isDefaultBindingContext) !== ts.findAncestor(context.enclosingDeclaration, isDefaultBindingContext)))) { + return "default"; + } + if (symbol.declarations && symbol.declarations.length) { + let declaration = ts.firstDefined(symbol.declarations, d => ts.getNameOfDeclaration(d) ? d : undefined); // Try using a declaration with a name, first + const name = declaration && ts.getNameOfDeclaration(declaration); + if (declaration && name) { + if (ts.isCallExpression(declaration) && ts.isBindableObjectDefinePropertyCall(declaration)) { + return ts.symbolName(symbol); + } + if (ts.isComputedPropertyName(name) && !(ts.getCheckFlags(symbol) & ts.CheckFlags.Late)) { + const nameType = getSymbolLinks(symbol).nameType; + if (nameType && nameType.flags & ts.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 ts.declarationNameToString(name); - } - if (!declaration) { - declaration = symbol.declarations[0]; // Declaration may be nameless, but we'll try anyway - } - if (declaration.parent && declaration.parent.kind === ts.SyntaxKind.VariableDeclaration) { - return ts.declarationNameToString((declaration.parent as ts.VariableDeclaration).name); - } - switch (declaration.kind) { - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - if (context && !context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier)) { - context.encounteredError = true; - } - return declaration.kind === ts.SyntaxKind.ClassExpression ? "(Anonymous class)" : "(Anonymous function)"; } + return ts.declarationNameToString(name); + } + if (!declaration) { + declaration = symbol.declarations[0]; // Declaration may be nameless, but we'll try anyway + } + if (declaration.parent && declaration.parent.kind === ts.SyntaxKind.VariableDeclaration) { + return ts.declarationNameToString((declaration.parent as ts.VariableDeclaration).name); + } + switch (declaration.kind) { + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + if (context && !context.encounteredError && !(context.flags & ts.NodeBuilderFlags.AllowAnonymousIdentifier)) { + context.encounteredError = true; + } + return declaration.kind === ts.SyntaxKind.ClassExpression ? "(Anonymous class)" : "(Anonymous function)"; } - const name = getNameOfSymbolFromNameType(symbol, context); - return name !== undefined ? name : ts.symbolName(symbol); } + const name = getNameOfSymbolFromNameType(symbol, context); + return name !== undefined ? name : ts.symbolName(symbol); + } - function isDeclarationVisible(node: ts.Node): boolean { - if (node) { - const links = getNodeLinks(node); - if (links.isVisible === undefined) { - links.isVisible = !!determineIfDeclarationIsVisible(); - } - return links.isVisible; + function isDeclarationVisible(node: ts.Node): boolean { + if (node) { + const links = getNodeLinks(node); + if (links.isVisible === undefined) { + links.isVisible = !!determineIfDeclarationIsVisible(); } + return links.isVisible; + } - return false; + return false; - function determineIfDeclarationIsVisible() { - switch (node.kind) { - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.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 && ts.isSourceFile(node.parent.parent.parent)); - case ts.SyntaxKind.BindingElement: - return isDeclarationVisible(node.parent.parent); - case ts.SyntaxKind.VariableDeclaration: - if (ts.isBindingPattern((node as ts.VariableDeclaration).name) && - !((node as ts.VariableDeclaration).name as ts.BindingPattern).elements.length) { - // If the binding pattern is empty, this variable declaration is not visible - return false; - } - // falls through - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - // external module augmentation is always visible - if (ts.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 (!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) && - !(node.kind !== ts.SyntaxKind.ImportEqualsDeclaration && parent.kind !== ts.SyntaxKind.SourceFile && parent.flags & ts.NodeFlags.Ambient)) { - return isGlobalSourceFile(parent); - } - // Exported members/ambient module elements (exception import declaration) are visible if parent is visible - return isDeclarationVisible(parent); + function determineIfDeclarationIsVisible() { + switch (node.kind) { + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.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 && ts.isSourceFile(node.parent.parent.parent)); + case ts.SyntaxKind.BindingElement: + return isDeclarationVisible(node.parent.parent); + case ts.SyntaxKind.VariableDeclaration: + if (ts.isBindingPattern((node as ts.VariableDeclaration).name) && + !((node as ts.VariableDeclaration).name as ts.BindingPattern).elements.length) { + // If the binding pattern is empty, this variable declaration is not visible + return false; + } + // falls through + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + // external module augmentation is always visible + if (ts.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 (!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) && + !(node.kind !== ts.SyntaxKind.ImportEqualsDeclaration && parent.kind !== ts.SyntaxKind.SourceFile && parent.flags & ts.NodeFlags.Ambient)) { + return isGlobalSourceFile(parent); + } + // Exported members/ambient module elements (exception import declaration) are visible if parent is visible + return isDeclarationVisible(parent); - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - if (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private | ts.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 ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.TypeReference: - case ts.SyntaxKind.ArrayType: - case ts.SyntaxKind.TupleType: - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.NamedTupleMember: - return isDeclarationVisible(node.parent); - - // Default binding, import specifier and namespace import is visible - // only on demand so by default it is not visible - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + if (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private | ts.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 ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.TypeReference: + case ts.SyntaxKind.ArrayType: + case ts.SyntaxKind.TupleType: + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.NamedTupleMember: + return isDeclarationVisible(node.parent); - // Type parameters are always visible - case ts.SyntaxKind.TypeParameter: + // Default binding, import specifier and namespace import is visible + // only on demand so by default it is not visible + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportSpecifier: + return false; - // Source file and namespace export are always visible - // falls through - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.NamespaceExportDeclaration: - return true; + // Type parameters are always visible + case ts.SyntaxKind.TypeParameter: - // Export assignments do not create name bindings outside the module - case ts.SyntaxKind.ExportAssignment: - return false; + // Source file and namespace export are always visible + // falls through + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.NamespaceExportDeclaration: + return true; - default: - return false; - } + // Export assignments do not create name bindings outside the module + case ts.SyntaxKind.ExportAssignment: + return false; + + default: + return false; } } + } + + function collectLinkedAliases(node: ts.Identifier, setVisibility?: boolean): ts.Node[] | undefined { + let exportSymbol: ts.Symbol | undefined; + if (node.parent && node.parent.kind === ts.SyntaxKind.ExportAssignment) { + exportSymbol = resolveName(node, node.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, /*nameNotFoundMessage*/ undefined, node, /*isUse*/ false); + } + else if (node.parent.kind === ts.SyntaxKind.ExportSpecifier) { + exportSymbol = getTargetOfExportSpecifier(node.parent as ts.ExportSpecifier, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); + } + let result: ts.Node[] | undefined; + let visited: ts.Set | undefined; + if (exportSymbol) { + visited = new ts.Set(); + visited.add(getSymbolId(exportSymbol)); + buildVisibleNodeList(exportSymbol.declarations); + } + return result; - function collectLinkedAliases(node: ts.Identifier, setVisibility?: boolean): ts.Node[] | undefined { - let exportSymbol: ts.Symbol | undefined; - if (node.parent && node.parent.kind === ts.SyntaxKind.ExportAssignment) { - exportSymbol = resolveName(node, node.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, /*nameNotFoundMessage*/ undefined, node, /*isUse*/ false); - } - else if (node.parent.kind === ts.SyntaxKind.ExportSpecifier) { - exportSymbol = getTargetOfExportSpecifier(node.parent as ts.ExportSpecifier, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); - } - let result: ts.Node[] | undefined; - let visited: ts.Set | undefined; - if (exportSymbol) { - visited = new ts.Set(); - visited.add(getSymbolId(exportSymbol)); - buildVisibleNodeList(exportSymbol.declarations); - } - return result; + function buildVisibleNodeList(declarations: ts.Declaration[] | undefined) { + ts.forEach(declarations, declaration => { + const resultNode = getAnyImportSyntax(declaration) || declaration; + if (setVisibility) { + getNodeLinks(declaration).isVisible = true; + } + else { + result = result || []; + ts.pushIfUnique(result, resultNode); + } - function buildVisibleNodeList(declarations: ts.Declaration[] | undefined) { - ts.forEach(declarations, declaration => { - const resultNode = getAnyImportSyntax(declaration) || declaration; - if (setVisibility) { - getNodeLinks(declaration).isVisible = true; - } - else { - result = result || []; - ts.pushIfUnique(result, resultNode); - } - - if (ts.isInternalModuleImportEqualsDeclaration(declaration)) { - // Add the referenced top container visible - const internalModuleReference = declaration.moduleReference as ts.Identifier | ts.QualifiedName; - const firstIdentifier = ts.getFirstIdentifier(internalModuleReference); - const importSymbol = resolveName(declaration, firstIdentifier.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, undefined, undefined, /*isUse*/ false); - if (importSymbol && visited) { - if (ts.tryAddToSet(visited, getSymbolId(importSymbol))) { - buildVisibleNodeList(importSymbol.declarations); - } + if (ts.isInternalModuleImportEqualsDeclaration(declaration)) { + // Add the referenced top container visible + const internalModuleReference = declaration.moduleReference as ts.Identifier | ts.QualifiedName; + const firstIdentifier = ts.getFirstIdentifier(internalModuleReference); + const importSymbol = resolveName(declaration, firstIdentifier.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, undefined, undefined, /*isUse*/ false); + if (importSymbol && visited) { + if (ts.tryAddToSet(visited, getSymbolId(importSymbol))) { + 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; + /** + * 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; } - resolutionTargets.push(target); - resolutionResults.push(/*items*/ true); - resolutionPropertyNames.push(propertyName); - return true; + 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; - } + 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; } + return -1; + } + + function hasType(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { + switch (propertyName) { + case TypeSystemPropertyName.Type: + return !!getSymbolLinks(target as ts.Symbol).type; + case TypeSystemPropertyName.EnumTagType: + return !!(getNodeLinks(target as ts.JSDocEnumTag).resolvedEnumType); + case TypeSystemPropertyName.DeclaredType: + return !!getSymbolLinks(target as ts.Symbol).declaredType; + case TypeSystemPropertyName.ResolvedBaseConstructorType: + return !!(target as ts.InterfaceType).resolvedBaseConstructorType; + case TypeSystemPropertyName.ResolvedReturnType: + return !!(target as ts.Signature).resolvedReturnType; + case TypeSystemPropertyName.ImmediateBaseConstraint: + return !!(target as ts.Type).immediateBaseConstraint; + case TypeSystemPropertyName.ResolvedTypeArguments: + return !!(target as ts.TypeReference).resolvedTypeArguments; + case TypeSystemPropertyName.ResolvedBaseTypes: + return !!(target as ts.InterfaceType).baseTypesResolved; + case TypeSystemPropertyName.WriteType: + return !!getSymbolLinks(target as ts.Symbol).writeType; + } + return ts.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 hasType(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { - switch (propertyName) { - case TypeSystemPropertyName.Type: - return !!getSymbolLinks(target as ts.Symbol).type; - case TypeSystemPropertyName.EnumTagType: - return !!(getNodeLinks(target as ts.JSDocEnumTag).resolvedEnumType); - case TypeSystemPropertyName.DeclaredType: - return !!getSymbolLinks(target as ts.Symbol).declaredType; - case TypeSystemPropertyName.ResolvedBaseConstructorType: - return !!(target as ts.InterfaceType).resolvedBaseConstructorType; - case TypeSystemPropertyName.ResolvedReturnType: - return !!(target as ts.Signature).resolvedReturnType; - case TypeSystemPropertyName.ImmediateBaseConstraint: - return !!(target as ts.Type).immediateBaseConstraint; - case TypeSystemPropertyName.ResolvedTypeArguments: - return !!(target as ts.TypeReference).resolvedTypeArguments; - case TypeSystemPropertyName.ResolvedBaseTypes: - return !!(target as ts.InterfaceType).baseTypesResolved; - case TypeSystemPropertyName.WriteType: - return !!getSymbolLinks(target as ts.Symbol).writeType; + function getDeclarationContainer(node: ts.Node): ts.Node { + return ts.findAncestor(ts.getRootDeclaration(node), node => { + switch (node.kind) { + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.VariableDeclarationList: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportClause: + return false; + default: + return true; } - return ts.Debug.assertNever(propertyName); - } + })!.parent; + } - /** - * 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 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)!) as ts.InterfaceType; + return classType.typeParameters ? createTypeReference(classType as ts.GenericType, ts.map(classType.typeParameters, _ => anyType)) : classType; + } - function getDeclarationContainer(node: ts.Node): ts.Node { - return ts.findAncestor(ts.getRootDeclaration(node), node => { - switch (node.kind) { - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.VariableDeclarationList: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportClause: - return false; - default: - return true; - } - })!.parent; - } + // Return the type of the given property in the given type, or undefined if no such property exists + function getTypeOfPropertyOfType(type: ts.Type, name: ts.__String): ts.Type | undefined { + const prop = getPropertyOfType(type, name); + return prop ? getTypeOfSymbol(prop) : undefined; + } - 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)!) as ts.InterfaceType; - return classType.typeParameters ? createTypeReference(classType as ts.GenericType, ts.map(classType.typeParameters, _ => anyType)) : classType; - } + function getTypeOfPropertyOrIndexSignature(type: ts.Type, name: ts.__String): ts.Type { + return getTypeOfPropertyOfType(type, name) || getApplicableIndexInfoForName(type, name)?.type || unknownType; + } - // Return the type of the given property in the given type, or undefined if no such property exists - function getTypeOfPropertyOfType(type: ts.Type, name: ts.__String): ts.Type | undefined { - const prop = getPropertyOfType(type, name); - return prop ? getTypeOfSymbol(prop) : undefined; - } + function isTypeAny(type: ts.Type | undefined) { + return type && (type.flags & ts.TypeFlags.Any) !== 0; + } - function getTypeOfPropertyOrIndexSignature(type: ts.Type, name: ts.__String): ts.Type { - return getTypeOfPropertyOfType(type, name) || getApplicableIndexInfoForName(type, name)?.type || unknownType; - } + function isErrorType(type: ts.Type) { + // The only 'any' types that have alias symbols are those manufactured by getTypeFromTypeAliasReference for + // a reference to an unresolved symbol. We want those to behave like the errorType. + return type === errorType || !!(type.flags & ts.TypeFlags.Any && type.aliasSymbol); + } - function isTypeAny(type: ts.Type | undefined) { - return type && (type.flags & ts.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: ts.BindingElementGrandparent, checkMode: CheckMode) { + if (checkMode !== CheckMode.Normal) { + return getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode); } + const symbol = getSymbolOfNode(node); + return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode); + } - function isErrorType(type: ts.Type) { - // The only 'any' types that have alias symbols are those manufactured by getTypeFromTypeAliasReference for - // a reference to an unresolved symbol. We want those to behave like the errorType. - return type === errorType || !!(type.flags & ts.TypeFlags.Any && type.aliasSymbol); + function getRestType(source: ts.Type, properties: ts.PropertyName[], symbol: ts.Symbol | undefined): ts.Type { + source = filterType(source, t => !(t.flags & ts.TypeFlags.Nullable)); + if (source.flags & ts.TypeFlags.Never) { + return emptyObjectType; + } + if (source.flags & ts.TypeFlags.Union) { + return mapType(source, t => getRestType(t, properties, symbol)); } - // 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: ts.BindingElementGrandparent, checkMode: CheckMode) { - if (checkMode !== CheckMode.Normal) { - return getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode); + let omitKeyType = getUnionType(ts.map(properties, getLiteralTypeFromPropertyName)); + const spreadableProperties: ts.Symbol[] = []; + const unspreadableToRestKeys: ts.Type[] = []; + + for (const prop of getPropertiesOfType(source)) { + const literalTypeFromProperty = getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique); + if (!isTypeAssignableTo(literalTypeFromProperty, omitKeyType) + && !(ts.getDeclarationModifierFlagsFromSymbol(prop) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) + && isSpreadableProperty(prop)) { + spreadableProperties.push(prop); + } + else { + unspreadableToRestKeys.push(literalTypeFromProperty); } - const symbol = getSymbolOfNode(node); - return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode); } - function getRestType(source: ts.Type, properties: ts.PropertyName[], symbol: ts.Symbol | undefined): ts.Type { - source = filterType(source, t => !(t.flags & ts.TypeFlags.Nullable)); - if (source.flags & ts.TypeFlags.Never) { - return emptyObjectType; - } - if (source.flags & ts.TypeFlags.Union) { - return mapType(source, t => getRestType(t, properties, symbol)); + if (isGenericObjectType(source) || isGenericIndexType(omitKeyType)) { + if (unspreadableToRestKeys.length) { + // If the type we're spreading from has properties that cannot + // be spread into the rest type (e.g. getters, methods), ensure + // they are explicitly omitted, as they would in the non-generic case. + omitKeyType = getUnionType([omitKeyType, ...unspreadableToRestKeys]); } - let omitKeyType = getUnionType(ts.map(properties, getLiteralTypeFromPropertyName)); - const spreadableProperties: ts.Symbol[] = []; - const unspreadableToRestKeys: ts.Type[] = []; + if (omitKeyType.flags & ts.TypeFlags.Never) { + return source; + } - for (const prop of getPropertiesOfType(source)) { - const literalTypeFromProperty = getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique); - if (!isTypeAssignableTo(literalTypeFromProperty, omitKeyType) - && !(ts.getDeclarationModifierFlagsFromSymbol(prop) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) - && isSpreadableProperty(prop)) { - spreadableProperties.push(prop); - } - else { - unspreadableToRestKeys.push(literalTypeFromProperty); - } + const omitTypeAlias = getGlobalOmitSymbol(); + if (!omitTypeAlias) { + return errorType; } + return getTypeAliasInstantiation(omitTypeAlias, [source, omitKeyType]); + } + const members = ts.createSymbolTable(); + for (const prop of spreadableProperties) { + members.set(prop.escapedName, getSpreadSymbol(prop, /*readonly*/ false)); + } + const result = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, getIndexInfosOfType(source)); + result.objectFlags |= ts.ObjectFlags.ObjectRestType; + return result; + } - if (isGenericObjectType(source) || isGenericIndexType(omitKeyType)) { - if (unspreadableToRestKeys.length) { - // If the type we're spreading from has properties that cannot - // be spread into the rest type (e.g. getters, methods), ensure - // they are explicitly omitted, as they would in the non-generic case. - omitKeyType = getUnionType([omitKeyType, ...unspreadableToRestKeys]); - } + function isGenericTypeWithUndefinedConstraint(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Instantiable) && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, ts.TypeFlags.Undefined); + } - if (omitKeyType.flags & ts.TypeFlags.Never) { - return source; - } + function getNonUndefinedType(type: ts.Type) { + const typeOrConstraint = someType(type, isGenericTypeWithUndefinedConstraint) ? mapType(type, t => t.flags & ts.TypeFlags.Instantiable ? getBaseConstraintOrType(t) : t) : type; + return getTypeWithFacts(typeOrConstraint, TypeFacts.NEUndefined); + } - const omitTypeAlias = getGlobalOmitSymbol(); - if (!omitTypeAlias) { - return errorType; - } - return getTypeAliasInstantiation(omitTypeAlias, [source, omitKeyType]); - } - const members = ts.createSymbolTable(); - for (const prop of spreadableProperties) { - members.set(prop.escapedName, getSpreadSymbol(prop, /*readonly*/ false)); + // 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: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression, declaredType: ts.Type) { + const reference = getSyntheticElementAccess(node); + return reference ? getFlowTypeOfReference(reference, declaredType) : declaredType; + } + + function getSyntheticElementAccess(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression): ts.ElementAccessExpression | undefined { + const parentAccess = getParentElementAccess(node); + if (parentAccess && parentAccess.flowNode) { + const propName = getDestructuringPropertyName(node); + if (propName) { + const literal = ts.setTextRange(ts.parseNodeFactory.createStringLiteral(propName), node); + const lhsExpr = ts.isLeftHandSideExpression(parentAccess) ? parentAccess : ts.parseNodeFactory.createParenthesizedExpression(parentAccess); + const result = ts.setTextRange(ts.parseNodeFactory.createElementAccessExpression(lhsExpr, literal), node); + ts.setParent(literal, result); + ts.setParent(result, node); + if (lhsExpr !== parentAccess) { + ts.setParent(lhsExpr, result); + } + result.flowNode = parentAccess.flowNode; + return result; } - const result = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, getIndexInfosOfType(source)); - result.objectFlags |= ts.ObjectFlags.ObjectRestType; - return result; } + } - function isGenericTypeWithUndefinedConstraint(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Instantiable) && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, ts.TypeFlags.Undefined); + function getParentElementAccess(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression) { + const ancestor = node.parent.parent; + switch (ancestor.kind) { + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.PropertyAssignment: + return getSyntheticElementAccess(ancestor as ts.BindingElement | ts.PropertyAssignment); + case ts.SyntaxKind.ArrayLiteralExpression: + return getSyntheticElementAccess(node.parent as ts.Expression); + case ts.SyntaxKind.VariableDeclaration: + return (ancestor as ts.VariableDeclaration).initializer; + case ts.SyntaxKind.BinaryExpression: + return (ancestor as ts.BinaryExpression).right; } - - function getNonUndefinedType(type: ts.Type) { - const typeOrConstraint = someType(type, isGenericTypeWithUndefinedConstraint) ? mapType(type, t => t.flags & ts.TypeFlags.Instantiable ? getBaseConstraintOrType(t) : t) : type; - return getTypeWithFacts(typeOrConstraint, TypeFacts.NEUndefined); + } + function getDestructuringPropertyName(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression) { + const parent = node.parent; + if (node.kind === ts.SyntaxKind.BindingElement && parent.kind === ts.SyntaxKind.ObjectBindingPattern) { + return getLiteralPropertyNameText((node as ts.BindingElement).propertyName || (node as ts.BindingElement).name as ts.Identifier); } + if (node.kind === ts.SyntaxKind.PropertyAssignment || node.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + return getLiteralPropertyNameText((node as ts.PropertyAssignment | ts.ShorthandPropertyAssignment).name); + } + return "" + ((parent as ts.BindingPattern | ts.ArrayLiteralExpression).elements as ts.NodeArray).indexOf(node); + } + + function getLiteralPropertyNameText(name: ts.PropertyName) { + const type = getLiteralTypeFromPropertyName(name); + return type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral) ? "" + (type as ts.StringLiteralType | ts.NumberLiteralType).value : undefined; + } - // 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: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression, declaredType: ts.Type) { - const reference = getSyntheticElementAccess(node); - return reference ? getFlowTypeOfReference(reference, declaredType) : declaredType; + /** Return the inferred type for a binding element */ + function getTypeForBindingElement(declaration: ts.BindingElement): ts.Type | undefined { + const checkMode = declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal; + const parentType = getTypeForBindingElementParent(declaration.parent.parent, checkMode); + return parentType && getBindingElementTypeFromParentType(declaration, parentType); + } + + function getBindingElementTypeFromParentType(declaration: ts.BindingElement, parentType: ts.Type): ts.Type { + // If an any type was inferred for parent, infer that for the binding element + if (isTypeAny(parentType)) { + return parentType; + } + const pattern = declaration.parent; + // Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation + if (strictNullChecks && declaration.flags & ts.NodeFlags.Ambient && ts.isParameterDeclaration(declaration)) { + parentType = getNonNullableType(parentType); + } + // Filter `undefined` from the type we check against if the parent has an initializer and that initializer is not possibly `undefined` + else if (strictNullChecks && pattern.parent.initializer && !(getTypeFacts(getTypeOfInitializer(pattern.parent.initializer)) & TypeFacts.EQUndefined)) { + parentType = getTypeWithFacts(parentType, TypeFacts.NEUndefined); } - function getSyntheticElementAccess(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression): ts.ElementAccessExpression | undefined { - const parentAccess = getParentElementAccess(node); - if (parentAccess && parentAccess.flowNode) { - const propName = getDestructuringPropertyName(node); - if (propName) { - const literal = ts.setTextRange(ts.parseNodeFactory.createStringLiteral(propName), node); - const lhsExpr = ts.isLeftHandSideExpression(parentAccess) ? parentAccess : ts.parseNodeFactory.createParenthesizedExpression(parentAccess); - const result = ts.setTextRange(ts.parseNodeFactory.createElementAccessExpression(lhsExpr, literal), node); - ts.setParent(literal, result); - ts.setParent(result, node); - if (lhsExpr !== parentAccess) { - ts.setParent(lhsExpr, result); + let type: ts.Type | undefined; + if (pattern.kind === ts.SyntaxKind.ObjectBindingPattern) { + if (declaration.dotDotDotToken) { + parentType = getReducedType(parentType); + if (parentType.flags & ts.TypeFlags.Unknown || !isValidSpreadType(parentType)) { + error(declaration, ts.Diagnostics.Rest_types_may_only_be_created_from_object_types); + return errorType; + } + const literalMembers: ts.PropertyName[] = []; + for (const element of pattern.elements) { + if (!element.dotDotDotToken) { + literalMembers.push(element.propertyName || element.name as ts.Identifier); } - result.flowNode = parentAccess.flowNode; - return result; } + type = getRestType(parentType, literalMembers, declaration.symbol); } - } - - function getParentElementAccess(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression) { - const ancestor = node.parent.parent; - switch (ancestor.kind) { - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.PropertyAssignment: - return getSyntheticElementAccess(ancestor as ts.BindingElement | ts.PropertyAssignment); - case ts.SyntaxKind.ArrayLiteralExpression: - return getSyntheticElementAccess(node.parent as ts.Expression); - case ts.SyntaxKind.VariableDeclaration: - return (ancestor as ts.VariableDeclaration).initializer; - case ts.SyntaxKind.BinaryExpression: - return (ancestor as ts.BinaryExpression).right; + else { + // Use explicitly specified property name ({ p: xxx } form), or otherwise the implied name ({ p } form) + const name = declaration.propertyName || declaration.name as ts.Identifier; + const indexType = getLiteralTypeFromPropertyName(name); + const declaredType = getIndexedAccessType(parentType, indexType, ts.AccessFlags.ExpressionPosition, name); + type = getFlowTypeOfDestructuring(declaration, declaredType); } } - function getDestructuringPropertyName(node: ts.BindingElement | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.Expression) { - const parent = node.parent; - if (node.kind === ts.SyntaxKind.BindingElement && parent.kind === ts.SyntaxKind.ObjectBindingPattern) { - return getLiteralPropertyNameText((node as ts.BindingElement).propertyName || (node as ts.BindingElement).name as ts.Identifier); + 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 | (declaration.dotDotDotToken ? 0 : IterationUse.PossiblyOutOfBounds), 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 as ts.TupleTypeReference, index)) : + createArrayType(elementType); + } + else if (isArrayLikeType(parentType)) { + const indexType = getNumberLiteralType(index); + const accessFlags = ts.AccessFlags.ExpressionPosition | (hasDefaultValue(declaration) ? ts.AccessFlags.NoTupleBoundsCheck : 0); + const declaredType = getIndexedAccessTypeOrUndefined(parentType, indexType, accessFlags, declaration.name) || errorType; + type = getFlowTypeOfDestructuring(declaration, declaredType); } - if (node.kind === ts.SyntaxKind.PropertyAssignment || node.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - return getLiteralPropertyNameText((node as ts.PropertyAssignment | ts.ShorthandPropertyAssignment).name); + else { + type = elementType; } - return "" + ((parent as ts.BindingPattern | ts.ArrayLiteralExpression).elements as ts.NodeArray).indexOf(node); + } + if (!declaration.initializer) { + return type; + } + if (ts.getEffectiveTypeAnnotationNode(ts.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, CheckMode.Normal)) & ts.TypeFlags.Undefined) ? getNonUndefinedType(type) : type; + } + return widenTypeInferredFromInitializer(declaration, getUnionType([getNonUndefinedType(type), checkDeclarationInitializer(declaration, CheckMode.Normal)], ts.UnionReduction.Subtype)); + } + + function getTypeForDeclarationFromJSDocComment(declaration: ts.Node) { + const jsdocType = ts.getJSDocType(declaration); + if (jsdocType) { + return getTypeFromTypeNode(jsdocType); + } + return undefined; + } + + function isNullOrUndefined(node: ts.Expression) { + const expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + return expr.kind === ts.SyntaxKind.NullKeyword || expr.kind === ts.SyntaxKind.Identifier && getResolvedSymbol(expr as ts.Identifier) === undefinedSymbol; + } + + function isEmptyArrayLiteral(node: ts.Expression) { + const expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + return expr.kind === ts.SyntaxKind.ArrayLiteralExpression && (expr as ts.ArrayLiteralExpression).elements.length === 0; + } + + function addOptionality(type: ts.Type, isProperty = false, isOptional = true): ts.Type { + return strictNullChecks && isOptional ? getOptionalType(type, isProperty) : type; + } + + // Return the inferred type for a variable, parameter, or property declaration + function getTypeForVariableLikeDeclaration(declaration: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement | ts.JSDocPropertyLikeTag, includeOptionality: boolean, checkMode: CheckMode): 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 (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.SyntaxKind.ForInStatement) { + const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression, /*checkMode*/ checkMode))); + return indexType.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index) ? getExtractStringType(indexType) : stringType; } - function getLiteralPropertyNameText(name: ts.PropertyName) { - const type = getLiteralTypeFromPropertyName(name); - return type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral) ? "" + (type as ts.StringLiteralType | ts.NumberLiteralType).value : undefined; + if (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.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) || anyType; } - /** Return the inferred type for a binding element */ - function getTypeForBindingElement(declaration: ts.BindingElement): ts.Type | undefined { - const checkMode = declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal; - const parentType = getTypeForBindingElementParent(declaration.parent.parent, checkMode); - return parentType && getBindingElementTypeFromParentType(declaration, parentType); + if (ts.isBindingPattern(declaration.parent)) { + return getTypeForBindingElement(declaration as ts.BindingElement); } - function getBindingElementTypeFromParentType(declaration: ts.BindingElement, parentType: ts.Type): ts.Type { - // If an any type was inferred for parent, infer that for the binding element - if (isTypeAny(parentType)) { - return parentType; - } - const pattern = declaration.parent; - // Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation - if (strictNullChecks && declaration.flags & ts.NodeFlags.Ambient && ts.isParameterDeclaration(declaration)) { - parentType = getNonNullableType(parentType); + const isProperty = ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration); + const isOptional = includeOptionality && (isProperty && !!declaration.questionToken || + ts.isParameter(declaration) && (!!declaration.questionToken || isJSDocOptionalParameter(declaration)) || + isOptionalJSDocPropertyLikeTag(declaration)); + + // Use type from type annotation if one is present + const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); + if (declaredType) { + return addOptionality(declaredType, isProperty, isOptional); + } + + if ((noImplicitAny || ts.isInJSFile(declaration)) && + ts.isVariableDeclaration(declaration) && !ts.isBindingPattern(declaration.name) && + !(ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) && !(declaration.flags & ts.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 (!(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) && (!declaration.initializer || isNullOrUndefined(declaration.initializer))) { + return autoType; } - // Filter `undefined` from the type we check against if the parent has an initializer and that initializer is not possibly `undefined` - else if (strictNullChecks && pattern.parent.initializer && !(getTypeFacts(getTypeOfInitializer(pattern.parent.initializer)) & TypeFacts.EQUndefined)) { - parentType = getTypeWithFacts(parentType, TypeFacts.NEUndefined); + // 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; } + } - let type: ts.Type | undefined; - if (pattern.kind === ts.SyntaxKind.ObjectBindingPattern) { - if (declaration.dotDotDotToken) { - parentType = getReducedType(parentType); - if (parentType.flags & ts.TypeFlags.Unknown || !isValidSpreadType(parentType)) { - error(declaration, ts.Diagnostics.Rest_types_may_only_be_created_from_object_types); - return errorType; - } - const literalMembers: ts.PropertyName[] = []; - for (const element of pattern.elements) { - if (!element.dotDotDotToken) { - literalMembers.push(element.propertyName || element.name as ts.Identifier); - } + if (ts.isParameter(declaration)) { + const func = declaration.parent as ts.FunctionLikeDeclaration; + // For a parameter of a set accessor, use the type of the get accessor if one is present + if (func.kind === ts.SyntaxKind.SetAccessor && hasBindableName(func)) { + const getter = ts.getDeclarationOfKind(getSymbolOfNode(declaration.parent), ts.SyntaxKind.GetAccessor); + if (getter) { + const getterSignature = getSignatureFromDeclaration(getter); + const thisParameter = getAccessorThisParameter(func as ts.AccessorDeclaration); + if (thisParameter && declaration === thisParameter) { + // Use the type from the *getter* + ts.Debug.assert(!thisParameter.type); + return getTypeOfSymbol(getterSignature.thisParameter!); } - 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 as ts.Identifier; - const indexType = getLiteralTypeFromPropertyName(name); - const declaredType = getIndexedAccessType(parentType, indexType, ts.AccessFlags.ExpressionPosition, 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 | (declaration.dotDotDotToken ? 0 : IterationUse.PossiblyOutOfBounds), 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 as ts.TupleTypeReference, index)) : - createArrayType(elementType); - } - else if (isArrayLikeType(parentType)) { - const indexType = getNumberLiteralType(index); - const accessFlags = ts.AccessFlags.ExpressionPosition | (hasDefaultValue(declaration) ? ts.AccessFlags.NoTupleBoundsCheck : 0); - const declaredType = getIndexedAccessTypeOrUndefined(parentType, indexType, accessFlags, declaration.name) || errorType; - type = getFlowTypeOfDestructuring(declaration, declaredType); - } - else { - type = elementType; + return getReturnTypeOfSignature(getterSignature); } } - if (!declaration.initializer) { - return type; + if (ts.isInJSFile(declaration)) { + const type = getParameterTypeOfTypeTag(func, declaration); + if (type) + return type; } - if (ts.getEffectiveTypeAnnotationNode(ts.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, CheckMode.Normal)) & ts.TypeFlags.Undefined) ? getNonUndefinedType(type) : type; + // Use contextual parameter type if one is available + const type = declaration.symbol.escapedName === ts.InternalSymbolName.This ? getContextualThisParameterType(func) : getContextuallyTypedParameterType(declaration); + if (type) { + return addOptionality(type, /*isProperty*/ false, isOptional); } - return widenTypeInferredFromInitializer(declaration, getUnionType([getNonUndefinedType(type), checkDeclarationInitializer(declaration, CheckMode.Normal)], ts.UnionReduction.Subtype)); } - function getTypeForDeclarationFromJSDocComment(declaration: ts.Node) { - const jsdocType = ts.getJSDocType(declaration); - if (jsdocType) { - return getTypeFromTypeNode(jsdocType); + // Use the type of the initializer expression if one is present and the declaration is + // not a parameter of a contextually typed function + if (ts.hasOnlyExpressionInitializer(declaration) && !!declaration.initializer) { + if (ts.isInJSFile(declaration) && !ts.isParameter(declaration)) { + const containerObjectType = getJSContainerObjectType(declaration, getSymbolOfNode(declaration), ts.getDeclaredExpandoInitializer(declaration)); + if (containerObjectType) { + return containerObjectType; + } } - return undefined; + const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration, checkMode)); + return addOptionality(type, isProperty, isOptional); } - function isNullOrUndefined(node: ts.Expression) { - const expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); - return expr.kind === ts.SyntaxKind.NullKeyword || expr.kind === ts.SyntaxKind.Identifier && getResolvedSymbol(expr as ts.Identifier) === undefinedSymbol; + if (ts.isPropertyDeclaration(declaration) && (noImplicitAny || ts.isInJSFile(declaration))) { + // We have a property declaration with no type annotation or initializer, in noImplicitAny mode or a .js file. + // Use control flow analysis of this.xxx assignments in the constructor or static block to determine the type of the property. + if (!ts.hasStaticModifier(declaration)) { + const constructor = findConstructorDeclaration(declaration.parent); + const type = constructor ? getFlowTypeInConstructor(declaration.symbol, constructor) : + ts.getEffectiveModifierFlags(declaration) & ts.ModifierFlags.Ambient ? getTypeOfPropertyInBaseClass(declaration.symbol) : + undefined; + return type && addOptionality(type, /*isProperty*/ true, isOptional); + } + else { + const staticBlocks = ts.filter(declaration.parent.members, ts.isClassStaticBlockDeclaration); + const type = staticBlocks.length ? getFlowTypeInStaticBlocks(declaration.symbol, staticBlocks) : + ts.getEffectiveModifierFlags(declaration) & ts.ModifierFlags.Ambient ? getTypeOfPropertyInBaseClass(declaration.symbol) : + undefined; + return type && addOptionality(type, /*isProperty*/ true, isOptional); + } } - function isEmptyArrayLiteral(node: ts.Expression) { - const expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); - return expr.kind === ts.SyntaxKind.ArrayLiteralExpression && (expr as ts.ArrayLiteralExpression).elements.length === 0; + if (ts.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; } - function addOptionality(type: ts.Type, isProperty = false, isOptional = true): ts.Type { - return strictNullChecks && isOptional ? getOptionalType(type, isProperty) : type; + // 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 (ts.isBindingPattern(declaration.name)) { + return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true); } - // Return the inferred type for a variable, parameter, or property declaration - function getTypeForVariableLikeDeclaration(declaration: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement | ts.JSDocPropertyLikeTag, includeOptionality: boolean, checkMode: CheckMode): 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 (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.SyntaxKind.ForInStatement) { - const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression, /*checkMode*/ checkMode))); - return indexType.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index) ? getExtractStringType(indexType) : stringType; - } + // No type specified and nothing can be inferred + return undefined; + } - if (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.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) || anyType; + function isConstructorDeclaredProperty(symbol: ts.Symbol) { + // A property is considered a constructor declared property when all declaration sites are this.xxx assignments, + // when no declaration sites have JSDoc type annotations, and when at least one declaration site is in the body of + // a class constructor. + if (symbol.valueDeclaration && ts.isBinaryExpression(symbol.valueDeclaration)) { + const links = getSymbolLinks(symbol); + if (links.isConstructorDeclaredProperty === undefined) { + links.isConstructorDeclaredProperty = false; + links.isConstructorDeclaredProperty = !!getDeclaringConstructor(symbol) && ts.every(symbol.declarations, declaration => ts.isBinaryExpression(declaration) && + isPossiblyAliasedThisProperty(declaration) && + (declaration.left.kind !== ts.SyntaxKind.ElementAccessExpression || ts.isStringOrNumericLiteralLike((declaration.left as ts.ElementAccessExpression).argumentExpression)) && + !getAnnotatedTypeForAssignmentDeclaration(/*declaredType*/ undefined, declaration, symbol, declaration)); } + return links.isConstructorDeclaredProperty; + } + return false; + } + + function isAutoTypedProperty(symbol: ts.Symbol) { + // A property is auto-typed when its declaration has no type annotation or initializer and we're in + // noImplicitAny mode or a .js file. + const declaration = symbol.valueDeclaration; + return declaration && ts.isPropertyDeclaration(declaration) && !ts.getEffectiveTypeAnnotationNode(declaration) && + !declaration.initializer && (noImplicitAny || ts.isInJSFile(declaration)); + } - if (ts.isBindingPattern(declaration.parent)) { - return getTypeForBindingElement(declaration as ts.BindingElement); + function getDeclaringConstructor(symbol: ts.Symbol) { + if (!symbol.declarations) { + return; + } + for (const declaration of symbol.declarations) { + const container = ts.getThisContainer(declaration, /*includeArrowFunctions*/ false); + if (container && (container.kind === ts.SyntaxKind.Constructor || isJSConstructor(container))) { + return container as ts.ConstructorDeclaration; } + } - const isProperty = ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration); - const isOptional = includeOptionality && (isProperty && !!declaration.questionToken || - ts.isParameter(declaration) && (!!declaration.questionToken || isJSDocOptionalParameter(declaration)) || - isOptionalJSDocPropertyLikeTag(declaration)); + ; + } + /** Create a synthetic property access flow node after the last statement of the file */ + function getFlowTypeFromCommonJSExport(symbol: ts.Symbol) { + const file = ts.getSourceFileOfNode(symbol.declarations![0]); + const accessName = ts.unescapeLeadingUnderscores(symbol.escapedName); + const areAllModuleExports = symbol.declarations!.every(d => ts.isInJSFile(d) && ts.isAccessExpression(d) && ts.isModuleExportsAccessExpression(d.expression)); + const reference = areAllModuleExports + ? ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("module"), ts.factory.createIdentifier("exports")), accessName) + : ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("exports"), accessName); + if (areAllModuleExports) { + ts.setParent((reference.expression as ts.PropertyAccessExpression).expression, reference.expression); + } + ts.setParent(reference.expression, reference); + ts.setParent(reference, file); + reference.flowNode = file.endFlowNode; + return getFlowTypeOfReference(reference, autoType, undefinedType); + } - // Use type from type annotation if one is present - const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); - if (declaredType) { - return addOptionality(declaredType, isProperty, isOptional); + function getFlowTypeInStaticBlocks(symbol: ts.Symbol, staticBlocks: readonly ts.ClassStaticBlockDeclaration[]) { + const accessName = ts.startsWith(symbol.escapedName as string, "__#") + ? ts.factory.createPrivateIdentifier((symbol.escapedName as string).split("@")[1]) + : ts.unescapeLeadingUnderscores(symbol.escapedName); + for (const staticBlock of staticBlocks) { + const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), accessName); + ts.setParent(reference.expression, reference); + ts.setParent(reference, staticBlock); + reference.flowNode = staticBlock.returnFlowNode; + const flowType = getFlowTypeOfProperty(reference, symbol); + if (noImplicitAny && (flowType === autoType || flowType === autoArrayType)) { + error(symbol.valueDeclaration, ts.Diagnostics.Member_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); } - - if ((noImplicitAny || ts.isInJSFile(declaration)) && - ts.isVariableDeclaration(declaration) && !ts.isBindingPattern(declaration.name) && - !(ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) && !(declaration.flags & ts.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 (!(ts.getCombinedNodeFlags(declaration) & ts.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; - } + // We don't infer a type if assignments are only null or undefined. + if (everyType(flowType, isNullableType)) { + continue; } - - if (ts.isParameter(declaration)) { - const func = declaration.parent as ts.FunctionLikeDeclaration; - // For a parameter of a set accessor, use the type of the get accessor if one is present - if (func.kind === ts.SyntaxKind.SetAccessor && hasBindableName(func)) { - const getter = ts.getDeclarationOfKind(getSymbolOfNode(declaration.parent), ts.SyntaxKind.GetAccessor); - if (getter) { - const getterSignature = getSignatureFromDeclaration(getter); - const thisParameter = getAccessorThisParameter(func as ts.AccessorDeclaration); - if (thisParameter && declaration === thisParameter) { - // Use the type from the *getter* - ts.Debug.assert(!thisParameter.type); - return getTypeOfSymbol(getterSignature.thisParameter!); - } - return getReturnTypeOfSignature(getterSignature); + return convertAutoToAny(flowType); + } + } + + function getFlowTypeInConstructor(symbol: ts.Symbol, constructor: ts.ConstructorDeclaration) { + const accessName = ts.startsWith(symbol.escapedName as string, "__#") + ? ts.factory.createPrivateIdentifier((symbol.escapedName as string).split("@")[1]) + : ts.unescapeLeadingUnderscores(symbol.escapedName); + const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), accessName); + ts.setParent(reference.expression, reference); + ts.setParent(reference, constructor); + reference.flowNode = constructor.returnFlowNode; + const flowType = getFlowTypeOfProperty(reference, symbol); + if (noImplicitAny && (flowType === autoType || flowType === autoArrayType)) { + error(symbol.valueDeclaration, ts.Diagnostics.Member_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); + } + // We don't infer a type if assignments are only null or undefined. + return everyType(flowType, isNullableType) ? undefined : convertAutoToAny(flowType); + } + + function getFlowTypeOfProperty(reference: ts.Node, prop: ts.Symbol | undefined) { + const initialType = prop?.valueDeclaration + && (!isAutoTypedProperty(prop) || ts.getEffectiveModifierFlags(prop.valueDeclaration) & ts.ModifierFlags.Ambient) + && getTypeOfPropertyInBaseClass(prop) + || undefinedType; + return getFlowTypeOfReference(reference, autoType, initialType); + } + + 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 = ts.getAssignedExpandoInitializer(symbol.valueDeclaration); + if (container) { + const tag = ts.getJSDocTypeTag(container); + if (tag && tag.typeExpression) { + return getTypeFromTypeNode(tag.typeExpression); + } + const containerObjectType = symbol.valueDeclaration && getJSContainerObjectType(symbol.valueDeclaration, symbol, container); + return containerObjectType || getWidenedLiteralType(checkExpressionCached(container)); + } + let type; + let definedInConstructor = false; + let definedInMethod = false; + // We use control flow analysis to determine the type of the property if the property qualifies as a constructor + // declared property and the resulting control flow type isn't just undefined or null. + if (isConstructorDeclaredProperty(symbol)) { + type = getFlowTypeInConstructor(symbol, getDeclaringConstructor(symbol)!); + } + if (!type) { + let types: ts.Type[] | undefined; + if (symbol.declarations) { + let jsdocType: ts.Type | undefined; + for (const declaration of symbol.declarations) { + const expression = (ts.isBinaryExpression(declaration) || ts.isCallExpression(declaration)) ? declaration : + ts.isAccessExpression(declaration) ? ts.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 (ts.isInJSFile(declaration)) { - const type = getParameterTypeOfTypeTag(func, declaration); - if (type) - return type; - } - // Use contextual parameter type if one is available - const type = declaration.symbol.escapedName === ts.InternalSymbolName.This ? getContextualThisParameterType(func) : getContextuallyTypedParameterType(declaration); - if (type) { - return addOptionality(type, /*isProperty*/ false, isOptional); - } - } - // Use the type of the initializer expression if one is present and the declaration is - // not a parameter of a contextually typed function - if (ts.hasOnlyExpressionInitializer(declaration) && !!declaration.initializer) { - if (ts.isInJSFile(declaration) && !ts.isParameter(declaration)) { - const containerObjectType = getJSContainerObjectType(declaration, getSymbolOfNode(declaration), ts.getDeclaredExpandoInitializer(declaration)); - if (containerObjectType) { - return containerObjectType; + const kind = ts.isAccessExpression(expression) + ? ts.getAssignmentDeclarationPropertyAccessKind(expression) + : ts.getAssignmentDeclarationKind(expression); + if (kind === ts.AssignmentDeclarationKind.ThisProperty || ts.isBinaryExpression(expression) && isPossiblyAliasedThisProperty(expression, kind)) { + if (isDeclarationInConstructor(expression)) { + definedInConstructor = true; + } + else { + definedInMethod = true; + } + } + if (!ts.isCallExpression(expression)) { + jsdocType = getAnnotatedTypeForAssignmentDeclaration(jsdocType, expression, symbol, declaration); + } + if (!jsdocType) { + (types || (types = [])).push((ts.isBinaryExpression(expression) || ts.isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType); } } - const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration, checkMode)); - return addOptionality(type, isProperty, isOptional); + type = jsdocType; } - - if (ts.isPropertyDeclaration(declaration) && (noImplicitAny || ts.isInJSFile(declaration))) { - // We have a property declaration with no type annotation or initializer, in noImplicitAny mode or a .js file. - // Use control flow analysis of this.xxx assignments in the constructor or static block to determine the type of the property. - if (!ts.hasStaticModifier(declaration)) { - const constructor = findConstructorDeclaration(declaration.parent); - const type = constructor ? getFlowTypeInConstructor(declaration.symbol, constructor) : - ts.getEffectiveModifierFlags(declaration) & ts.ModifierFlags.Ambient ? getTypeOfPropertyInBaseClass(declaration.symbol) : - undefined; - return type && addOptionality(type, /*isProperty*/ true, isOptional); + if (!type) { + if (!ts.length(types)) { + return errorType; // No types from any declarations :( } - else { - const staticBlocks = ts.filter(declaration.parent.members, ts.isClassStaticBlockDeclaration); - const type = staticBlocks.length ? getFlowTypeInStaticBlocks(declaration.symbol, staticBlocks) : - ts.getEffectiveModifierFlags(declaration) & ts.ModifierFlags.Ambient ? getTypeOfPropertyInBaseClass(declaration.symbol) : - undefined; - return type && addOptionality(type, /*isProperty*/ true, isOptional); + let constructorTypes = definedInConstructor && symbol.declarations ? getConstructorDefinedThisAssignmentTypes(types!, symbol.declarations) : undefined; + // use only the constructor types unless they were only assigned null | undefined (including widening variants) + if (definedInMethod) { + const propType = getTypeOfPropertyInBaseClass(symbol); + if (propType) { + (constructorTypes || (constructorTypes = [])).push(propType); + definedInConstructor = true; + } } + const sourceTypes = ts.some(constructorTypes, t => !!(t.flags & ~ts.TypeFlags.Nullable)) ? constructorTypes : types; // TODO: GH#18217 + type = getUnionType(sourceTypes!); } + } + const widened = getWidenedType(addOptionality(type, /*isProperty*/ false, definedInMethod && !definedInConstructor)); + if (symbol.valueDeclaration && filterType(widened, t => !!(t.flags & ~ts.TypeFlags.Nullable)) === neverType) { + reportImplicitAny(symbol.valueDeclaration, anyType); + return anyType; + } + return widened; + } - if (ts.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 (ts.isBindingPattern(declaration.name)) { - return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true); - } - - // No type specified and nothing can be inferred + function getJSContainerObjectType(decl: ts.Node, symbol: ts.Symbol, init: ts.Expression | undefined): ts.Type | undefined { + if (!ts.isInJSFile(decl) || !init || !ts.isObjectLiteralExpression(init) || init.properties.length) { return undefined; } - - function isConstructorDeclaredProperty(symbol: ts.Symbol) { - // A property is considered a constructor declared property when all declaration sites are this.xxx assignments, - // when no declaration sites have JSDoc type annotations, and when at least one declaration site is in the body of - // a class constructor. - if (symbol.valueDeclaration && ts.isBinaryExpression(symbol.valueDeclaration)) { - const links = getSymbolLinks(symbol); - if (links.isConstructorDeclaredProperty === undefined) { - links.isConstructorDeclaredProperty = false; - links.isConstructorDeclaredProperty = !!getDeclaringConstructor(symbol) && ts.every(symbol.declarations, declaration => ts.isBinaryExpression(declaration) && - isPossiblyAliasedThisProperty(declaration) && - (declaration.left.kind !== ts.SyntaxKind.ElementAccessExpression || ts.isStringOrNumericLiteralLike((declaration.left as ts.ElementAccessExpression).argumentExpression)) && - !getAnnotatedTypeForAssignmentDeclaration(/*declaredType*/ undefined, declaration, symbol, declaration)); - } - return links.isConstructorDeclaredProperty; + const exports = ts.createSymbolTable(); + while (ts.isBinaryExpression(decl) || ts.isPropertyAccessExpression(decl)) { + const s = getSymbolOfNode(decl); + if (s?.exports?.size) { + mergeSymbolTable(exports, s.exports); } - return false; + decl = ts.isBinaryExpression(decl) ? decl.parent : decl.parent.parent; } - - function isAutoTypedProperty(symbol: ts.Symbol) { - // A property is auto-typed when its declaration has no type annotation or initializer and we're in - // noImplicitAny mode or a .js file. - const declaration = symbol.valueDeclaration; - return declaration && ts.isPropertyDeclaration(declaration) && !ts.getEffectiveTypeAnnotationNode(declaration) && - !declaration.initializer && (noImplicitAny || ts.isInJSFile(declaration)); + const s = getSymbolOfNode(decl); + if (s?.exports?.size) { + mergeSymbolTable(exports, s.exports); } + const type = createAnonymousType(symbol, exports, ts.emptyArray, ts.emptyArray, ts.emptyArray); + type.objectFlags |= ts.ObjectFlags.JSLiteral; + return type; + } - function getDeclaringConstructor(symbol: ts.Symbol) { - if (!symbol.declarations) { - return; + function getAnnotatedTypeForAssignmentDeclaration(declaredType: ts.Type | undefined, expression: ts.Expression, symbol: ts.Symbol, declaration: ts.Declaration) { + const typeNode = ts.getEffectiveTypeAnnotationNode(expression.parent); + if (typeNode) { + const type = getWidenedType(getTypeFromTypeNode(typeNode)); + if (!declaredType) { + return type; } - for (const declaration of symbol.declarations) { - const container = ts.getThisContainer(declaration, /*includeArrowFunctions*/ false); - if (container && (container.kind === ts.SyntaxKind.Constructor || isJSConstructor(container))) { - return container as ts.ConstructorDeclaration; - } - } - - ; - } - /** Create a synthetic property access flow node after the last statement of the file */ - function getFlowTypeFromCommonJSExport(symbol: ts.Symbol) { - const file = ts.getSourceFileOfNode(symbol.declarations![0]); - const accessName = ts.unescapeLeadingUnderscores(symbol.escapedName); - const areAllModuleExports = symbol.declarations!.every(d => ts.isInJSFile(d) && ts.isAccessExpression(d) && ts.isModuleExportsAccessExpression(d.expression)); - const reference = areAllModuleExports - ? ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("module"), ts.factory.createIdentifier("exports")), accessName) - : ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("exports"), accessName); - if (areAllModuleExports) { - ts.setParent((reference.expression as ts.PropertyAccessExpression).expression, reference.expression); + else if (!isErrorType(declaredType) && !isErrorType(type) && !isTypeIdenticalTo(declaredType, type)) { + errorNextVariableOrPropertyDeclarationMustHaveSameType(/*firstDeclaration*/ undefined, declaredType, declaration, type); } - ts.setParent(reference.expression, reference); - ts.setParent(reference, file); - reference.flowNode = file.endFlowNode; - return getFlowTypeOfReference(reference, autoType, undefinedType); } - - function getFlowTypeInStaticBlocks(symbol: ts.Symbol, staticBlocks: readonly ts.ClassStaticBlockDeclaration[]) { - const accessName = ts.startsWith(symbol.escapedName as string, "__#") - ? ts.factory.createPrivateIdentifier((symbol.escapedName as string).split("@")[1]) - : ts.unescapeLeadingUnderscores(symbol.escapedName); - for (const staticBlock of staticBlocks) { - const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), accessName); - ts.setParent(reference.expression, reference); - ts.setParent(reference, staticBlock); - reference.flowNode = staticBlock.returnFlowNode; - const flowType = getFlowTypeOfProperty(reference, symbol); - if (noImplicitAny && (flowType === autoType || flowType === autoArrayType)) { - error(symbol.valueDeclaration, ts.Diagnostics.Member_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); - } - // We don't infer a type if assignments are only null or undefined. - if (everyType(flowType, isNullableType)) { - continue; + if (symbol.parent?.valueDeclaration) { + const typeNode = ts.getEffectiveTypeAnnotationNode(symbol.parent.valueDeclaration); + if (typeNode) { + const annotationSymbol = getPropertyOfType(getTypeFromTypeNode(typeNode), symbol.escapedName); + if (annotationSymbol) { + return getNonMissingTypeOfSymbol(annotationSymbol); } - return convertAutoToAny(flowType); - } - } - - function getFlowTypeInConstructor(symbol: ts.Symbol, constructor: ts.ConstructorDeclaration) { - const accessName = ts.startsWith(symbol.escapedName as string, "__#") - ? ts.factory.createPrivateIdentifier((symbol.escapedName as string).split("@")[1]) - : ts.unescapeLeadingUnderscores(symbol.escapedName); - const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), accessName); - ts.setParent(reference.expression, reference); - ts.setParent(reference, constructor); - reference.flowNode = constructor.returnFlowNode; - const flowType = getFlowTypeOfProperty(reference, symbol); - if (noImplicitAny && (flowType === autoType || flowType === autoArrayType)) { - error(symbol.valueDeclaration, ts.Diagnostics.Member_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); } - // We don't infer a type if assignments are only null or undefined. - return everyType(flowType, isNullableType) ? undefined : convertAutoToAny(flowType); } - function getFlowTypeOfProperty(reference: ts.Node, prop: ts.Symbol | undefined) { - const initialType = prop?.valueDeclaration - && (!isAutoTypedProperty(prop) || ts.getEffectiveModifierFlags(prop.valueDeclaration) & ts.ModifierFlags.Ambient) - && getTypeOfPropertyInBaseClass(prop) - || undefinedType; - return getFlowTypeOfReference(reference, autoType, initialType); - } + return declaredType; + } - 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 = ts.getAssignedExpandoInitializer(symbol.valueDeclaration); - if (container) { - const tag = ts.getJSDocTypeTag(container); - if (tag && tag.typeExpression) { - return getTypeFromTypeNode(tag.typeExpression); - } - const containerObjectType = symbol.valueDeclaration && getJSContainerObjectType(symbol.valueDeclaration, symbol, container); - return containerObjectType || getWidenedLiteralType(checkExpressionCached(container)); + /** 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: ts.BinaryExpression | ts.CallExpression, kind: ts.AssignmentDeclarationKind) { + if (ts.isCallExpression(expression)) { + if (resolvedSymbol) { + return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments } - let type; - let definedInConstructor = false; - let definedInMethod = false; - // We use control flow analysis to determine the type of the property if the property qualifies as a constructor - // declared property and the resulting control flow type isn't just undefined or null. - if (isConstructorDeclaredProperty(symbol)) { - type = getFlowTypeInConstructor(symbol, getDeclaringConstructor(symbol)!); + const objectLitType = checkExpressionCached((expression as ts.BindableObjectDefinePropertyCall).arguments[2]); + const valueType = getTypeOfPropertyOfType(objectLitType, "value" as ts.__String); + if (valueType) { + return valueType; } - if (!type) { - let types: ts.Type[] | undefined; - if (symbol.declarations) { - let jsdocType: ts.Type | undefined; - for (const declaration of symbol.declarations) { - const expression = (ts.isBinaryExpression(declaration) || ts.isCallExpression(declaration)) ? declaration : - ts.isAccessExpression(declaration) ? ts.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 = ts.isAccessExpression(expression) - ? ts.getAssignmentDeclarationPropertyAccessKind(expression) - : ts.getAssignmentDeclarationKind(expression); - if (kind === ts.AssignmentDeclarationKind.ThisProperty || ts.isBinaryExpression(expression) && isPossiblyAliasedThisProperty(expression, kind)) { - if (isDeclarationInConstructor(expression)) { - definedInConstructor = true; - } - else { - definedInMethod = true; - } - } - if (!ts.isCallExpression(expression)) { - jsdocType = getAnnotatedTypeForAssignmentDeclaration(jsdocType, expression, symbol, declaration); - } - if (!jsdocType) { - (types || (types = [])).push((ts.isBinaryExpression(expression) || ts.isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType); - } - } - type = jsdocType; - } - if (!type) { - if (!ts.length(types)) { - return errorType; // No types from any declarations :( - } - let constructorTypes = definedInConstructor && symbol.declarations ? getConstructorDefinedThisAssignmentTypes(types!, symbol.declarations) : undefined; - // use only the constructor types unless they were only assigned null | undefined (including widening variants) - if (definedInMethod) { - const propType = getTypeOfPropertyInBaseClass(symbol); - if (propType) { - (constructorTypes || (constructorTypes = [])).push(propType); - definedInConstructor = true; - } - } - const sourceTypes = ts.some(constructorTypes, t => !!(t.flags & ~ts.TypeFlags.Nullable)) ? constructorTypes : types; // TODO: GH#18217 - type = getUnionType(sourceTypes!); + const getFunc = getTypeOfPropertyOfType(objectLitType, "get" as ts.__String); + if (getFunc) { + const getSig = getSingleCallSignature(getFunc); + if (getSig) { + return getReturnTypeOfSignature(getSig); } } - const widened = getWidenedType(addOptionality(type, /*isProperty*/ false, definedInMethod && !definedInConstructor)); - if (symbol.valueDeclaration && filterType(widened, t => !!(t.flags & ~ts.TypeFlags.Nullable)) === neverType) { - reportImplicitAny(symbol.valueDeclaration, anyType); - return anyType; - } - return widened; - } - - function getJSContainerObjectType(decl: ts.Node, symbol: ts.Symbol, init: ts.Expression | undefined): ts.Type | undefined { - if (!ts.isInJSFile(decl) || !init || !ts.isObjectLiteralExpression(init) || init.properties.length) { - return undefined; - } - const exports = ts.createSymbolTable(); - while (ts.isBinaryExpression(decl) || ts.isPropertyAccessExpression(decl)) { - const s = getSymbolOfNode(decl); - if (s?.exports?.size) { - mergeSymbolTable(exports, s.exports); + const setFunc = getTypeOfPropertyOfType(objectLitType, "set" as ts.__String); + if (setFunc) { + const setSig = getSingleCallSignature(setFunc); + if (setSig) { + return getTypeOfFirstParameterOfSignature(setSig); } - decl = ts.isBinaryExpression(decl) ? decl.parent : decl.parent.parent; - } - const s = getSymbolOfNode(decl); - if (s?.exports?.size) { - mergeSymbolTable(exports, s.exports); } - const type = createAnonymousType(symbol, exports, ts.emptyArray, ts.emptyArray, ts.emptyArray); - type.objectFlags |= ts.ObjectFlags.JSLiteral; - return type; + return anyType; } - - function getAnnotatedTypeForAssignmentDeclaration(declaredType: ts.Type | undefined, expression: ts.Expression, symbol: ts.Symbol, declaration: ts.Declaration) { - const typeNode = ts.getEffectiveTypeAnnotationNode(expression.parent); - if (typeNode) { - const type = getWidenedType(getTypeFromTypeNode(typeNode)); - if (!declaredType) { - return type; - } - else if (!isErrorType(declaredType) && !isErrorType(type) && !isTypeIdenticalTo(declaredType, type)) { - errorNextVariableOrPropertyDeclarationMustHaveSameType(/*firstDeclaration*/ undefined, declaredType, declaration, type); - } - } - if (symbol.parent?.valueDeclaration) { - const typeNode = ts.getEffectiveTypeAnnotationNode(symbol.parent.valueDeclaration); - if (typeNode) { - const annotationSymbol = getPropertyOfType(getTypeFromTypeNode(typeNode), symbol.escapedName); - if (annotationSymbol) { - return getNonMissingTypeOfSymbol(annotationSymbol); - } - } - } - - return declaredType; + if (containsSameNamedThisProperty(expression.left, expression.right)) { + return anyType; } - - /** 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: ts.BinaryExpression | ts.CallExpression, kind: ts.AssignmentDeclarationKind) { - if (ts.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 ts.BindableObjectDefinePropertyCall).arguments[2]); - const valueType = getTypeOfPropertyOfType(objectLitType, "value" as ts.__String); - if (valueType) { - return valueType; - } - const getFunc = getTypeOfPropertyOfType(objectLitType, "get" as ts.__String); - if (getFunc) { - const getSig = getSingleCallSignature(getFunc); - if (getSig) { - return getReturnTypeOfSignature(getSig); - } - } - const setFunc = getTypeOfPropertyOfType(objectLitType, "set" as ts.__String); - if (setFunc) { - const setSig = getSingleCallSignature(setFunc); - if (setSig) { - return getTypeOfFirstParameterOfSignature(setSig); - } - } - return anyType; - } - if (containsSameNamedThisProperty(expression.left, expression.right)) { - return anyType; - } - const isDirectExport = kind === ts.AssignmentDeclarationKind.ExportsProperty && (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) && (ts.isModuleExportsAccessExpression(expression.left.expression) || (ts.isIdentifier(expression.left.expression) && ts.isExportsIdentifier(expression.left.expression))); - const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) - : isDirectExport ? getRegularTypeOfLiteralType(checkExpressionCached(expression.right)) - : getWidenedLiteralType(checkExpressionCached(expression.right)); - if (type.flags & ts.TypeFlags.Object && - kind === ts.AssignmentDeclarationKind.ModuleExports && - symbol.escapedName === ts.InternalSymbolName.ExportEquals) { - const exportedType = resolveStructuredTypeMembers(type as ts.ObjectType); - const members = ts.createSymbolTable(); - ts.copyEntries(exportedType.members, members); - const initialSize = members.size; - if (resolvedSymbol && !resolvedSymbol.exports) { - resolvedSymbol.exports = ts.createSymbolTable(); - } - (resolvedSymbol || symbol).exports!.forEach((s, name) => { - const exportedMember = members.get(name)!; - if (exportedMember && exportedMember !== s) { - if (s.flags & ts.SymbolFlags.Value && exportedMember.flags & ts.SymbolFlags.Value) { - // If the member has an additional value-like declaration, union the types from the two declarations, - // but issue an error if they occurred in two different files. The purpose is to support a JS file with - // a pattern like: - // - // module.exports = { a: true }; - // module.exports.a = 3; - // - // but we may have a JS file with `module.exports = { a: true }` along with a TypeScript module augmentation - // declaring an `export const a: number`. In that case, we issue a duplicate identifier error, because - // it's unclear what that's supposed to mean, so it's probably a mistake. - if (s.valueDeclaration && exportedMember.valueDeclaration && ts.getSourceFileOfNode(s.valueDeclaration) !== ts.getSourceFileOfNode(exportedMember.valueDeclaration)) { - const unescapedName = ts.unescapeLeadingUnderscores(s.escapedName); - const exportedMemberName = ts.tryCast(exportedMember.valueDeclaration, ts.isNamedDeclaration)?.name || exportedMember.valueDeclaration; - ts.addRelatedInfo(error(s.valueDeclaration, ts.Diagnostics.Duplicate_identifier_0, unescapedName), ts.createDiagnosticForNode(exportedMemberName, ts.Diagnostics._0_was_also_declared_here, unescapedName)); - ts.addRelatedInfo(error(exportedMemberName, ts.Diagnostics.Duplicate_identifier_0, unescapedName), ts.createDiagnosticForNode(s.valueDeclaration, ts.Diagnostics._0_was_also_declared_here, unescapedName)); - } - const union = createSymbol(s.flags | exportedMember.flags, name); - union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]); - union.valueDeclaration = exportedMember.valueDeclaration; - union.declarations = ts.concatenate(exportedMember.declarations, s.declarations); - members.set(name, union); - } - else { - members.set(name, mergeSymbol(s, exportedMember)); - } + const isDirectExport = kind === ts.AssignmentDeclarationKind.ExportsProperty && (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) && (ts.isModuleExportsAccessExpression(expression.left.expression) || (ts.isIdentifier(expression.left.expression) && ts.isExportsIdentifier(expression.left.expression))); + const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) + : isDirectExport ? getRegularTypeOfLiteralType(checkExpressionCached(expression.right)) + : getWidenedLiteralType(checkExpressionCached(expression.right)); + if (type.flags & ts.TypeFlags.Object && + kind === ts.AssignmentDeclarationKind.ModuleExports && + symbol.escapedName === ts.InternalSymbolName.ExportEquals) { + const exportedType = resolveStructuredTypeMembers(type as ts.ObjectType); + const members = ts.createSymbolTable(); + ts.copyEntries(exportedType.members, members); + const initialSize = members.size; + if (resolvedSymbol && !resolvedSymbol.exports) { + resolvedSymbol.exports = ts.createSymbolTable(); + } + (resolvedSymbol || symbol).exports!.forEach((s, name) => { + const exportedMember = members.get(name)!; + if (exportedMember && exportedMember !== s) { + if (s.flags & ts.SymbolFlags.Value && exportedMember.flags & ts.SymbolFlags.Value) { + // If the member has an additional value-like declaration, union the types from the two declarations, + // but issue an error if they occurred in two different files. The purpose is to support a JS file with + // a pattern like: + // + // module.exports = { a: true }; + // module.exports.a = 3; + // + // but we may have a JS file with `module.exports = { a: true }` along with a TypeScript module augmentation + // declaring an `export const a: number`. In that case, we issue a duplicate identifier error, because + // it's unclear what that's supposed to mean, so it's probably a mistake. + if (s.valueDeclaration && exportedMember.valueDeclaration && ts.getSourceFileOfNode(s.valueDeclaration) !== ts.getSourceFileOfNode(exportedMember.valueDeclaration)) { + const unescapedName = ts.unescapeLeadingUnderscores(s.escapedName); + const exportedMemberName = ts.tryCast(exportedMember.valueDeclaration, ts.isNamedDeclaration)?.name || exportedMember.valueDeclaration; + ts.addRelatedInfo(error(s.valueDeclaration, ts.Diagnostics.Duplicate_identifier_0, unescapedName), ts.createDiagnosticForNode(exportedMemberName, ts.Diagnostics._0_was_also_declared_here, unescapedName)); + ts.addRelatedInfo(error(exportedMemberName, ts.Diagnostics.Duplicate_identifier_0, unescapedName), ts.createDiagnosticForNode(s.valueDeclaration, ts.Diagnostics._0_was_also_declared_here, unescapedName)); + } + const union = createSymbol(s.flags | exportedMember.flags, name); + union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]); + union.valueDeclaration = exportedMember.valueDeclaration; + union.declarations = ts.concatenate(exportedMember.declarations, s.declarations); + members.set(name, union); } else { - members.set(name, s); + members.set(name, mergeSymbol(s, exportedMember)); } - }); - const result = createAnonymousType(initialSize !== members.size ? undefined : exportedType.symbol, // Only set the type's symbol if it looks to be the same as the original type - members, exportedType.callSignatures, exportedType.constructSignatures, exportedType.indexInfos); - result.objectFlags |= (ts.getObjectFlags(type) & ts.ObjectFlags.JSLiteral); // Propagate JSLiteral flag - if (result.symbol && result.symbol.flags & ts.SymbolFlags.Class && type === getDeclaredTypeOfClassOrInterface(result.symbol)) { - result.objectFlags |= ts.ObjectFlags.IsClassInstanceClone; // Propagate the knowledge that this type is equivalent to the symbol's class instance type } - return result; - } - if (isEmptyArrayLiteralType(type)) { - reportImplicitAny(expression, anyArrayType); - return anyArrayType; + else { + members.set(name, s); + } + }); + const result = createAnonymousType(initialSize !== members.size ? undefined : exportedType.symbol, // Only set the type's symbol if it looks to be the same as the original type + members, exportedType.callSignatures, exportedType.constructSignatures, exportedType.indexInfos); + result.objectFlags |= (ts.getObjectFlags(type) & ts.ObjectFlags.JSLiteral); // Propagate JSLiteral flag + if (result.symbol && result.symbol.flags & ts.SymbolFlags.Class && type === getDeclaredTypeOfClassOrInterface(result.symbol)) { + result.objectFlags |= ts.ObjectFlags.IsClassInstanceClone; // Propagate the knowledge that this type is equivalent to the symbol's class instance type } - return type; + return result; } - - function containsSameNamedThisProperty(thisProperty: ts.Expression, expression: ts.Expression) { - return ts.isPropertyAccessExpression(thisProperty) - && thisProperty.expression.kind === ts.SyntaxKind.ThisKeyword - && ts.forEachChildRecursively(expression, n => isMatchingReference(thisProperty, n)); + if (isEmptyArrayLiteralType(type)) { + reportImplicitAny(expression, anyArrayType); + return anyArrayType; } + return type; + } - function isDeclarationInConstructor(expression: ts.Expression) { - const thisContainer = ts.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 === ts.SyntaxKind.Constructor || - thisContainer.kind === ts.SyntaxKind.FunctionDeclaration || - (thisContainer.kind === ts.SyntaxKind.FunctionExpression && !ts.isPrototypePropertyAssignment(thisContainer.parent)); - } + function containsSameNamedThisProperty(thisProperty: ts.Expression, expression: ts.Expression) { + return ts.isPropertyAccessExpression(thisProperty) + && thisProperty.expression.kind === ts.SyntaxKind.ThisKeyword + && ts.forEachChildRecursively(expression, n => isMatchingReference(thisProperty, n)); + } - function getConstructorDefinedThisAssignmentTypes(types: ts.Type[], declarations: ts.Declaration[]): ts.Type[] | undefined { - ts.Debug.assert(types.length === declarations.length); - return types.filter((_, i) => { - const declaration = declarations[i]; - const expression = ts.isBinaryExpression(declaration) ? declaration : - ts.isBinaryExpression(declaration.parent) ? declaration.parent : undefined; - return expression && isDeclarationInConstructor(expression); - }); - } + function isDeclarationInConstructor(expression: ts.Expression) { + const thisContainer = ts.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 === ts.SyntaxKind.Constructor || + thisContainer.kind === ts.SyntaxKind.FunctionDeclaration || + (thisContainer.kind === ts.SyntaxKind.FunctionExpression && !ts.isPrototypePropertyAssignment(thisContainer.parent)); + } - // 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: ts.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 = ts.isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType; - return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType))); - } - if (ts.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; - } + function getConstructorDefinedThisAssignmentTypes(types: ts.Type[], declarations: ts.Declaration[]): ts.Type[] | undefined { + ts.Debug.assert(types.length === declarations.length); + return types.filter((_, i) => { + const declaration = declarations[i]; + const expression = ts.isBinaryExpression(declaration) ? declaration : + ts.isBinaryExpression(declaration.parent) ? declaration.parent : undefined; + return expression && isDeclarationInConstructor(expression); + }); + } - // Return the type implied by an object binding pattern - function getTypeFromObjectBindingPattern(pattern: ts.ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { - const members = ts.createSymbolTable(); - let stringIndexInfo: ts.IndexInfo | undefined; - let objectFlags = ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; - ts.forEach(pattern.elements, e => { - const name = e.propertyName || e.name as ts.Identifier; - if (e.dotDotDotToken) { - stringIndexInfo = createIndexInfo(stringType, anyType, /*isReadonly*/ false); - return; - } + // 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: ts.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 = ts.isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType; + return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType))); + } + if (ts.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; + } - const exprType = getLiteralTypeFromPropertyName(name); - if (!isTypeUsableAsPropertyName(exprType)) { - // do not include computed properties in the implied type - objectFlags |= ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties; - return; - } - const text = getPropertyNameFromType(exprType); - const flags = ts.SymbolFlags.Property | (e.initializer ? ts.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, ts.emptyArray, ts.emptyArray, stringIndexInfo ? [stringIndexInfo] : ts.emptyArray); - result.objectFlags |= objectFlags; - if (includePatternInType) { - result.pattern = pattern; - result.objectFlags |= ts.ObjectFlags.ContainsObjectOrArrayLiteral; + // Return the type implied by an object binding pattern + function getTypeFromObjectBindingPattern(pattern: ts.ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { + const members = ts.createSymbolTable(); + let stringIndexInfo: ts.IndexInfo | undefined; + let objectFlags = ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; + ts.forEach(pattern.elements, e => { + const name = e.propertyName || e.name as ts.Identifier; + if (e.dotDotDotToken) { + stringIndexInfo = createIndexInfo(stringType, anyType, /*isReadonly*/ false); + return; } - return result; - } - // Return the type implied by an array binding pattern - function getTypeFromArrayBindingPattern(pattern: ts.BindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { - const elements = pattern.elements; - const lastElement = ts.lastOrUndefined(elements); - const restElement = lastElement && lastElement.kind === ts.SyntaxKind.BindingElement && lastElement.dotDotDotToken ? lastElement : undefined; - if (elements.length === 0 || elements.length === 1 && restElement) { - return languageVersion >= ts.ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; - } - const elementTypes = ts.map(elements, e => ts.isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); - const minLength = ts.findLastIndex(elements, e => !(e === restElement || ts.isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1; - const elementFlags = ts.map(elements, (e, i) => e === restElement ? ts.ElementFlags.Rest : i >= minLength ? ts.ElementFlags.Optional : ts.ElementFlags.Required); - let result = createTupleType(elementTypes, elementFlags) as ts.TypeReference; - if (includePatternInType) { - result = cloneTypeReference(result); - result.pattern = pattern; - result.objectFlags |= ts.ObjectFlags.ContainsObjectOrArrayLiteral; + const exprType = getLiteralTypeFromPropertyName(name); + if (!isTypeUsableAsPropertyName(exprType)) { + // do not include computed properties in the implied type + objectFlags |= ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties; + return; } - return result; + const text = getPropertyNameFromType(exprType); + const flags = ts.SymbolFlags.Property | (e.initializer ? ts.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, ts.emptyArray, ts.emptyArray, stringIndexInfo ? [stringIndexInfo] : ts.emptyArray); + result.objectFlags |= objectFlags; + if (includePatternInType) { + result.pattern = pattern; + result.objectFlags |= ts.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: ts.BindingPattern, includePatternInType = false, reportErrors = false): ts.Type { - return pattern.kind === ts.SyntaxKind.ObjectBindingPattern - ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors) - : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors); - } + // Return the type implied by an array binding pattern + function getTypeFromArrayBindingPattern(pattern: ts.BindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { + const elements = pattern.elements; + const lastElement = ts.lastOrUndefined(elements); + const restElement = lastElement && lastElement.kind === ts.SyntaxKind.BindingElement && lastElement.dotDotDotToken ? lastElement : undefined; + if (elements.length === 0 || elements.length === 1 && restElement) { + return languageVersion >= ts.ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; + } + const elementTypes = ts.map(elements, e => ts.isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); + const minLength = ts.findLastIndex(elements, e => !(e === restElement || ts.isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1; + const elementFlags = ts.map(elements, (e, i) => e === restElement ? ts.ElementFlags.Rest : i >= minLength ? ts.ElementFlags.Optional : ts.ElementFlags.Required); + let result = createTupleType(elementTypes, elementFlags) as ts.TypeReference; + if (includePatternInType) { + result = cloneTypeReference(result); + result.pattern = pattern; + result.objectFlags |= ts.ObjectFlags.ContainsObjectOrArrayLiteral; + } + return result; + } - // 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: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement | ts.JSDocPropertyLikeTag, reportErrors?: boolean): ts.Type { - return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true, CheckMode.Normal), declaration, reportErrors); - } + // 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: ts.BindingPattern, includePatternInType = false, reportErrors = false): ts.Type { + return pattern.kind === ts.SyntaxKind.ObjectBindingPattern + ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors) + : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors); + } - function isGlobalSymbolConstructor(node: ts.Node) { - const symbol = getSymbolOfNode(node); - const globalSymbol = getGlobalESSymbolConstructorTypeSymbol(/*reportErrors*/ false); - return globalSymbol && symbol && symbol === globalSymbol; - } + // 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: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement | ts.JSDocPropertyLikeTag, reportErrors?: boolean): ts.Type { + return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true, CheckMode.Normal), declaration, reportErrors); + } - function widenTypeForVariableLikeDeclaration(type: ts.Type | undefined, declaration: any, reportErrors?: boolean) { - if (type) { - // TODO: If back compat with pre-3.0/4.0 libs isn't required, remove the following SymbolConstructor special case transforming `symbol` into `unique symbol` - if (type.flags & ts.TypeFlags.ESSymbol && isGlobalSymbolConstructor(declaration.parent)) { - type = getESSymbolLikeTypeForNode(declaration); - } - if (reportErrors) { - reportErrorsFromWidening(declaration, type); - } + function isGlobalSymbolConstructor(node: ts.Node) { + const symbol = getSymbolOfNode(node); + const globalSymbol = getGlobalESSymbolConstructorTypeSymbol(/*reportErrors*/ false); + return globalSymbol && symbol && symbol === globalSymbol; + } - // always widen a 'unique symbol' type if the type was created for a different declaration. - if (type.flags & ts.TypeFlags.UniqueESSymbol && (ts.isBindingElement(declaration) || !declaration.type) && type.symbol !== getSymbolOfNode(declaration)) { - type = esSymbolType; - } + function widenTypeForVariableLikeDeclaration(type: ts.Type | undefined, declaration: any, reportErrors?: boolean) { + if (type) { + // TODO: If back compat with pre-3.0/4.0 libs isn't required, remove the following SymbolConstructor special case transforming `symbol` into `unique symbol` + if (type.flags & ts.TypeFlags.ESSymbol && isGlobalSymbolConstructor(declaration.parent)) { + type = getESSymbolLikeTypeForNode(declaration); + } + if (reportErrors) { + reportErrorsFromWidening(declaration, type); + } - return getWidenedType(type); + // always widen a 'unique symbol' type if the type was created for a different declaration. + if (type.flags & ts.TypeFlags.UniqueESSymbol && (ts.isBindingElement(declaration) || !declaration.type) && type.symbol !== getSymbolOfNode(declaration)) { + type = esSymbolType; } - // Rest parameters default to type any[], other parameters default to type any - type = ts.isParameter(declaration) && declaration.dotDotDotToken ? anyArrayType : anyType; + return getWidenedType(type); + } + + // Rest parameters default to type any[], other parameters default to type any + type = ts.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); - } + // Report implicit any errors unless this is a private property within an ambient declaration + if (reportErrors) { + if (!declarationBelongsToPrivateAmbientMember(declaration)) { + reportImplicitAny(declaration, type); } - return type; } + return type; + } - function declarationBelongsToPrivateAmbientMember(declaration: ts.VariableLikeDeclaration) { - const root = ts.getRootDeclaration(declaration); - const memberDeclaration = root.kind === ts.SyntaxKind.Parameter ? root.parent : root; - return isPrivateWithinAmbient(memberDeclaration); - } + function declarationBelongsToPrivateAmbientMember(declaration: ts.VariableLikeDeclaration) { + const root = ts.getRootDeclaration(declaration); + const memberDeclaration = root.kind === ts.SyntaxKind.Parameter ? root.parent : root; + return isPrivateWithinAmbient(memberDeclaration); + } - function tryGetTypeFromEffectiveTypeNode(node: ts.Node) { - const typeNode = ts.getEffectiveTypeAnnotationNode(node); - if (typeNode) { - return getTypeFromTypeNode(typeNode); - } + function tryGetTypeFromEffectiveTypeNode(node: ts.Node) { + const typeNode = ts.getEffectiveTypeAnnotationNode(node); + if (typeNode) { + return getTypeFromTypeNode(typeNode); } + } - function getTypeOfVariableOrParameterOrProperty(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); + 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) { - 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; - } + links.type = type; } - return links.type; } + return links.type; + } - function getTypeOfVariableOrParameterOrPropertyWorker(symbol: ts.Symbol): ts.Type { - // Handle prototype property - if (symbol.flags & ts.SymbolFlags.Prototype) { - return getTypeOfPrototypeProperty(symbol); - } - // CommonsJS require and module both have type any. - if (symbol === requireSymbol) { - return anyType; - } - if (symbol.flags & ts.SymbolFlags.ModuleExports && symbol.valueDeclaration) { - const fileSymbol = getSymbolOfNode(ts.getSourceFileOfNode(symbol.valueDeclaration)); - const result = createSymbol(fileSymbol.flags, "exports" as ts.__String); - result.declarations = fileSymbol.declarations ? fileSymbol.declarations.slice() : []; - result.parent = symbol; - result.target = fileSymbol; - if (fileSymbol.valueDeclaration) - result.valueDeclaration = fileSymbol.valueDeclaration; - if (fileSymbol.members) - result.members = new ts.Map(fileSymbol.members); - if (fileSymbol.exports) - result.exports = new ts.Map(fileSymbol.exports); - const members = ts.createSymbolTable(); - members.set("exports" as ts.__String, result); - return createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); - } - // Handle catch clause variables - ts.Debug.assertIsDefined(symbol.valueDeclaration); - const declaration = symbol.valueDeclaration; - if (ts.isCatchClauseVariableDeclarationOrBindingElement(declaration)) { - const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); - if (typeNode === undefined) { - return useUnknownInCatchVariables ? unknownType : anyType; - } - const type = getTypeOfNode(typeNode); - // an errorType will make `checkTryStatement` issue an error - return isTypeAny(type) || type === unknownType ? type : errorType; - } - // Handle export default expressions - if (ts.isSourceFile(declaration) && ts.isJsonSourceFile(declaration)) { - if (!declaration.statements.length) { - return emptyObjectType; - } - return getWidenedType(getWidenedLiteralType(checkExpression(declaration.statements[0].expression))); + function getTypeOfVariableOrParameterOrPropertyWorker(symbol: ts.Symbol): ts.Type { + // Handle prototype property + if (symbol.flags & ts.SymbolFlags.Prototype) { + return getTypeOfPrototypeProperty(symbol); + } + // CommonsJS require and module both have type any. + if (symbol === requireSymbol) { + return anyType; + } + if (symbol.flags & ts.SymbolFlags.ModuleExports && symbol.valueDeclaration) { + const fileSymbol = getSymbolOfNode(ts.getSourceFileOfNode(symbol.valueDeclaration)); + const result = createSymbol(fileSymbol.flags, "exports" as ts.__String); + result.declarations = fileSymbol.declarations ? fileSymbol.declarations.slice() : []; + result.parent = symbol; + result.target = fileSymbol; + if (fileSymbol.valueDeclaration) + result.valueDeclaration = fileSymbol.valueDeclaration; + if (fileSymbol.members) + result.members = new ts.Map(fileSymbol.members); + if (fileSymbol.exports) + result.exports = new ts.Map(fileSymbol.exports); + const members = ts.createSymbolTable(); + members.set("exports" as ts.__String, result); + return createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); + } + // Handle catch clause variables + ts.Debug.assertIsDefined(symbol.valueDeclaration); + const declaration = symbol.valueDeclaration; + if (ts.isCatchClauseVariableDeclarationOrBindingElement(declaration)) { + const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); + if (typeNode === undefined) { + return useUnknownInCatchVariables ? unknownType : anyType; } - if (ts.isAccessor(declaration)) { - // Binding of certain patterns in JS code will occasionally mark symbols as both properties - // and accessors. Here we dispatch to accessor resolution if needed. - return getTypeOfAccessors(symbol); + const type = getTypeOfNode(typeNode); + // an errorType will make `checkTryStatement` issue an error + return isTypeAny(type) || type === unknownType ? type : errorType; + } + // Handle export default expressions + if (ts.isSourceFile(declaration) && ts.isJsonSourceFile(declaration)) { + if (!declaration.statements.length) { + return emptyObjectType; } + return getWidenedType(getWidenedLiteralType(checkExpression(declaration.statements[0].expression))); + } + if (ts.isAccessor(declaration)) { + // Binding of certain patterns in JS code will occasionally mark symbols as both properties + // and accessors. Here we dispatch to accessor resolution if needed. + return getTypeOfAccessors(symbol); + } - // 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 & ts.SymbolFlags.ValueModule && !(symbol.flags & ts.SymbolFlags.Assignment)) { - return getTypeOfFuncClassEnumModule(symbol); - } - return reportCircularityError(symbol); - } - let type: ts.Type; - if (declaration.kind === ts.SyntaxKind.ExportAssignment) { - type = widenTypeForVariableLikeDeclaration(tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionCached((declaration as ts.ExportAssignment).expression), declaration); - } - else if (ts.isBinaryExpression(declaration) || - (ts.isInJSFile(declaration) && - (ts.isCallExpression(declaration) || (ts.isPropertyAccessExpression(declaration) || ts.isBindableStaticElementAccessExpression(declaration)) && ts.isBinaryExpression(declaration.parent)))) { - type = getWidenedTypeForAssignmentDeclaration(symbol); - } - else if (ts.isPropertyAccessExpression(declaration) - || ts.isElementAccessExpression(declaration) - || ts.isIdentifier(declaration) - || ts.isStringLiteralLike(declaration) - || ts.isNumericLiteral(declaration) - || ts.isClassDeclaration(declaration) - || ts.isFunctionDeclaration(declaration) - || (ts.isMethodDeclaration(declaration) && !ts.isObjectLiteralMethod(declaration)) - || ts.isMethodSignature(declaration) - || ts.isSourceFile(declaration)) { - // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` - if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) { - return getTypeOfFuncClassEnumModule(symbol); - } - type = ts.isBinaryExpression(declaration.parent) ? - getWidenedTypeForAssignmentDeclaration(symbol) : - tryGetTypeFromEffectiveTypeNode(declaration) || anyType; - } - else if (ts.isPropertyAssignment(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkPropertyAssignment(declaration); - } - else if (ts.isJsxAttribute(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkJsxAttribute(declaration); - } - else if (ts.isShorthandPropertyAssignment(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionForMutableLocation(declaration.name, CheckMode.Normal); - } - else if (ts.isObjectLiteralMethod(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkObjectLiteralMethod(declaration, CheckMode.Normal); - } - else if (ts.isParameter(declaration) - || ts.isPropertyDeclaration(declaration) - || ts.isPropertySignature(declaration) - || ts.isVariableDeclaration(declaration) - || ts.isBindingElement(declaration) - || ts.isJSDocPropertyLikeTag(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 (ts.isEnumDeclaration(declaration)) { - type = getTypeOfFuncClassEnumModule(symbol); - } - else if (ts.isEnumMember(declaration)) { - type = getTypeOfEnumMember(symbol); - } - else { - return ts.Debug.fail("Unhandled declaration kind! " + ts.Debug.formatSyntaxKind(declaration.kind) + " for " + ts.Debug.formatSymbol(symbol)); + // 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 & ts.SymbolFlags.ValueModule && !(symbol.flags & ts.SymbolFlags.Assignment)) { + return getTypeOfFuncClassEnumModule(symbol); } - - if (!popTypeResolution()) { - // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` - if (symbol.flags & ts.SymbolFlags.ValueModule && !(symbol.flags & ts.SymbolFlags.Assignment)) { - return getTypeOfFuncClassEnumModule(symbol); - } - return reportCircularityError(symbol); + return reportCircularityError(symbol); + } + let type: ts.Type; + if (declaration.kind === ts.SyntaxKind.ExportAssignment) { + type = widenTypeForVariableLikeDeclaration(tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionCached((declaration as ts.ExportAssignment).expression), declaration); + } + else if (ts.isBinaryExpression(declaration) || + (ts.isInJSFile(declaration) && + (ts.isCallExpression(declaration) || (ts.isPropertyAccessExpression(declaration) || ts.isBindableStaticElementAccessExpression(declaration)) && ts.isBinaryExpression(declaration.parent)))) { + type = getWidenedTypeForAssignmentDeclaration(symbol); + } + else if (ts.isPropertyAccessExpression(declaration) + || ts.isElementAccessExpression(declaration) + || ts.isIdentifier(declaration) + || ts.isStringLiteralLike(declaration) + || ts.isNumericLiteral(declaration) + || ts.isClassDeclaration(declaration) + || ts.isFunctionDeclaration(declaration) + || (ts.isMethodDeclaration(declaration) && !ts.isObjectLiteralMethod(declaration)) + || ts.isMethodSignature(declaration) + || ts.isSourceFile(declaration)) { + // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` + if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) { + return getTypeOfFuncClassEnumModule(symbol); } - return type; + type = ts.isBinaryExpression(declaration.parent) ? + getWidenedTypeForAssignmentDeclaration(symbol) : + tryGetTypeFromEffectiveTypeNode(declaration) || anyType; } - - function getAnnotatedAccessorTypeNode(accessor: ts.AccessorDeclaration | undefined): ts.TypeNode | undefined { - if (accessor) { - if (accessor.kind === ts.SyntaxKind.GetAccessor) { - const getterTypeAnnotation = ts.getEffectiveReturnTypeNode(accessor); - return getterTypeAnnotation; - } - else { - const setterTypeAnnotation = ts.getEffectiveSetAccessorTypeAnnotationNode(accessor); - return setterTypeAnnotation; - } - } - return undefined; + else if (ts.isPropertyAssignment(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkPropertyAssignment(declaration); } - - function getAnnotatedAccessorType(accessor: ts.AccessorDeclaration | undefined): ts.Type | undefined { - const node = getAnnotatedAccessorTypeNode(accessor); - return node && getTypeFromTypeNode(node); + else if (ts.isJsxAttribute(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkJsxAttribute(declaration); } - - function getAnnotatedAccessorThisParameter(accessor: ts.AccessorDeclaration): ts.Symbol | undefined { - const parameter = getAccessorThisParameter(accessor); - return parameter && parameter.symbol; + else if (ts.isShorthandPropertyAssignment(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionForMutableLocation(declaration.name, CheckMode.Normal); + } + else if (ts.isObjectLiteralMethod(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkObjectLiteralMethod(declaration, CheckMode.Normal); + } + else if (ts.isParameter(declaration) + || ts.isPropertyDeclaration(declaration) + || ts.isPropertySignature(declaration) + || ts.isVariableDeclaration(declaration) + || ts.isBindingElement(declaration) + || ts.isJSDocPropertyLikeTag(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 (ts.isEnumDeclaration(declaration)) { + type = getTypeOfFuncClassEnumModule(symbol); + } + else if (ts.isEnumMember(declaration)) { + type = getTypeOfEnumMember(symbol); + } + else { + return ts.Debug.fail("Unhandled declaration kind! " + ts.Debug.formatSyntaxKind(declaration.kind) + " for " + ts.Debug.formatSymbol(symbol)); } - function getThisTypeOfDeclaration(declaration: ts.SignatureDeclaration): ts.Type | undefined { - return getThisTypeOfSignature(getSignatureFromDeclaration(declaration)); + if (!popTypeResolution()) { + // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` + if (symbol.flags & ts.SymbolFlags.ValueModule && !(symbol.flags & ts.SymbolFlags.Assignment)) { + return getTypeOfFuncClassEnumModule(symbol); + } + return reportCircularityError(symbol); } + return type; + } - function getTypeOfAccessors(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - if (!links.type) { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - return errorType; - } - const getter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.GetAccessor); - const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); - // We try to resolve a getter type annotation, a setter type annotation, or a getter function - // body return type inference, in that order. - let type = getter && ts.isInJSFile(getter) && getTypeForDeclarationFromJSDocComment(getter) || - getAnnotatedAccessorType(getter) || - getAnnotatedAccessorType(setter) || - getter && getter.body && getReturnTypeFromBody(getter); - if (!type) { - if (setter && !isPrivateWithinAmbient(setter)) { - errorOrSuggestion(noImplicitAny, setter, ts.Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation, symbolToString(symbol)); - } - else if (getter && !isPrivateWithinAmbient(getter)) { - errorOrSuggestion(noImplicitAny, getter, ts.Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation, symbolToString(symbol)); - } - type = anyType; - } - if (!popTypeResolution()) { - if (getAnnotatedAccessorTypeNode(getter)) { - error(getter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); - } - else if (getAnnotatedAccessorTypeNode(setter)) { - error(setter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); - } - else if (getter && noImplicitAny) { - error(getter, ts.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)); - } - type = anyType; - } - links.type = type; + function getAnnotatedAccessorTypeNode(accessor: ts.AccessorDeclaration | undefined): ts.TypeNode | undefined { + if (accessor) { + if (accessor.kind === ts.SyntaxKind.GetAccessor) { + const getterTypeAnnotation = ts.getEffectiveReturnTypeNode(accessor); + return getterTypeAnnotation; + } + else { + const setterTypeAnnotation = ts.getEffectiveSetAccessorTypeAnnotationNode(accessor); + return setterTypeAnnotation; } - return links.type; } + return undefined; + } - function getWriteTypeOfAccessors(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - if (!links.writeType) { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.WriteType)) { - return errorType; + function getAnnotatedAccessorType(accessor: ts.AccessorDeclaration | undefined): ts.Type | undefined { + const node = getAnnotatedAccessorTypeNode(accessor); + return node && getTypeFromTypeNode(node); + } + + function getAnnotatedAccessorThisParameter(accessor: ts.AccessorDeclaration): ts.Symbol | undefined { + const parameter = getAccessorThisParameter(accessor); + return parameter && parameter.symbol; + } + + function getThisTypeOfDeclaration(declaration: ts.SignatureDeclaration): ts.Type | undefined { + return getThisTypeOfSignature(getSignatureFromDeclaration(declaration)); + } + + function getTypeOfAccessors(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.type) { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + return errorType; + } + const getter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.GetAccessor); + const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); + // We try to resolve a getter type annotation, a setter type annotation, or a getter function + // body return type inference, in that order. + let type = getter && ts.isInJSFile(getter) && getTypeForDeclarationFromJSDocComment(getter) || + getAnnotatedAccessorType(getter) || + getAnnotatedAccessorType(setter) || + getter && getter.body && getReturnTypeFromBody(getter); + if (!type) { + if (setter && !isPrivateWithinAmbient(setter)) { + errorOrSuggestion(noImplicitAny, setter, ts.Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation, symbolToString(symbol)); } - const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); - let writeType = getAnnotatedAccessorType(setter); - if (!popTypeResolution()) { - if (getAnnotatedAccessorTypeNode(setter)) { - error(setter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); - } - writeType = anyType; + else if (getter && !isPrivateWithinAmbient(getter)) { + errorOrSuggestion(noImplicitAny, getter, ts.Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation, symbolToString(symbol)); } - // Absent an explicit setter type annotation we use the read type of the accessor. - links.writeType = writeType || getTypeOfAccessors(symbol); + type = anyType; } - return links.writeType; - } - - function getBaseTypeVariableOfClass(symbol: ts.Symbol) { - const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol)); - return baseConstructorType.flags & ts.TypeFlags.TypeVariable ? baseConstructorType : - baseConstructorType.flags & ts.TypeFlags.Intersection ? ts.find((baseConstructorType as ts.IntersectionType).types, t => !!(t.flags & ts.TypeFlags.TypeVariable)) : - undefined; - } - - function getTypeOfFuncClassEnumModule(symbol: ts.Symbol): ts.Type { - let links = getSymbolLinks(symbol); - const originalLinks = links; - if (!links.type) { - const expando = symbol.valueDeclaration && getSymbolOfExpando(symbol.valueDeclaration, /*allowDeclaration*/ false); - if (expando) { - const merged = mergeJSSymbols(symbol, expando); - if (merged) { - // note:we overwrite links because we just cloned the symbol - symbol = links = merged; - } + if (!popTypeResolution()) { + if (getAnnotatedAccessorTypeNode(getter)) { + error(getter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); + } + else if (getAnnotatedAccessorTypeNode(setter)) { + error(setter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); } - originalLinks.type = links.type = getTypeOfFuncClassEnumModuleWorker(symbol); + else if (getter && noImplicitAny) { + error(getter, ts.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)); + } + type = anyType; } - return links.type; + links.type = type; } + return links.type; + } - function getTypeOfFuncClassEnumModuleWorker(symbol: ts.Symbol): ts.Type { - const declaration = symbol.valueDeclaration; - if (symbol.flags & ts.SymbolFlags.Module && ts.isShorthandAmbientModuleSymbol(symbol)) { - return anyType; - } - else if (declaration && (declaration.kind === ts.SyntaxKind.BinaryExpression || - ts.isAccessExpression(declaration) && - declaration.parent.kind === ts.SyntaxKind.BinaryExpression)) { - return getWidenedTypeForAssignmentDeclaration(symbol); + function getWriteTypeOfAccessors(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.writeType) { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.WriteType)) { + return errorType; } - else if (symbol.flags & ts.SymbolFlags.ValueModule && declaration && ts.isSourceFile(declaration) && declaration.commonJsModuleIndicator) { - const resolvedModule = resolveExternalModuleSymbol(symbol); - if (resolvedModule !== symbol) { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - return errorType; - } - const exportEquals = getMergedSymbol(symbol.exports!.get(ts.InternalSymbolName.ExportEquals)!); - const type = getWidenedTypeForAssignmentDeclaration(exportEquals, exportEquals === resolvedModule ? undefined : resolvedModule); - if (!popTypeResolution()) { - return reportCircularityError(symbol); - } - return type; + const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); + let writeType = getAnnotatedAccessorType(setter); + if (!popTypeResolution()) { + if (getAnnotatedAccessorTypeNode(setter)) { + error(setter, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); } + writeType = anyType; } - const type = createObjectType(ts.ObjectFlags.Anonymous, symbol); - if (symbol.flags & ts.SymbolFlags.Class) { - const baseTypeVariable = getBaseTypeVariableOfClass(symbol); - return baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type; - } - else { - return strictNullChecks && symbol.flags & ts.SymbolFlags.Optional ? getOptionalType(type) : type; - } - } - - function getTypeOfEnumMember(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - return links.type || (links.type = getDeclaredTypeOfEnumMember(symbol)); + // Absent an explicit setter type annotation we use the read type of the accessor. + links.writeType = writeType || getTypeOfAccessors(symbol); } + return links.writeType; + } - function getTypeOfAlias(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - if (!links.type) { - const targetSymbol = resolveAlias(symbol); - const exportSymbol = symbol.declarations && getTargetOfAliasDeclaration(getDeclarationOfAliasSymbol(symbol)!, /*dontResolveAlias*/ true); - const declaredType = ts.firstDefined(exportSymbol?.declarations, d => ts.isExportAssignment(d) ? tryGetTypeFromEffectiveTypeNode(d) : undefined); - // 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 = exportSymbol?.declarations && isDuplicatedCommonJSExport(exportSymbol.declarations) && symbol.declarations!.length ? getFlowTypeFromCommonJSExport(exportSymbol) - : isDuplicatedCommonJSExport(symbol.declarations) ? autoType - : declaredType ? declaredType - : targetSymbol.flags & ts.SymbolFlags.Value ? getTypeOfSymbol(targetSymbol) - : errorType; - } - return links.type; - } - - function getTypeOfInstantiatedSymbol(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - return links.type || (links.type = instantiateType(getTypeOfSymbol(links.target!), links.mapper)); - } + function getBaseTypeVariableOfClass(symbol: ts.Symbol) { + const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol)); + return baseConstructorType.flags & ts.TypeFlags.TypeVariable ? baseConstructorType : + baseConstructorType.flags & ts.TypeFlags.Intersection ? ts.find((baseConstructorType as ts.IntersectionType).types, t => !!(t.flags & ts.TypeFlags.TypeVariable)) : + undefined; + } - function getWriteTypeOfInstantiatedSymbol(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - return links.writeType || (links.writeType = instantiateType(getWriteTypeOfSymbol(links.target!), links.mapper)); + function getTypeOfFuncClassEnumModule(symbol: ts.Symbol): ts.Type { + let links = getSymbolLinks(symbol); + const originalLinks = links; + if (!links.type) { + const expando = symbol.valueDeclaration && getSymbolOfExpando(symbol.valueDeclaration, /*allowDeclaration*/ false); + if (expando) { + const merged = mergeJSSymbols(symbol, expando); + 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 reportCircularityError(symbol: ts.Symbol) { - const declaration = symbol.valueDeclaration as ts.VariableLikeDeclaration; - // Check if variable has type annotation that circularly references the variable itself - if (ts.getEffectiveTypeAnnotationNode(declaration)) { - error(symbol.valueDeclaration, ts.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 !== ts.SyntaxKind.Parameter || (declaration as ts.HasInitializer).initializer)) { - error(symbol.valueDeclaration, ts.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. + function getTypeOfFuncClassEnumModuleWorker(symbol: ts.Symbol): ts.Type { + const declaration = symbol.valueDeclaration; + if (symbol.flags & ts.SymbolFlags.Module && ts.isShorthandAmbientModuleSymbol(symbol)) { return anyType; } - - function getTypeOfSymbolWithDeferredType(symbol: ts.Symbol) { - const links = getSymbolLinks(symbol); - if (!links.type) { - ts.Debug.assertIsDefined(links.deferralParent); - ts.Debug.assertIsDefined(links.deferralConstituents); - links.type = links.deferralParent.flags & ts.TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents); - } - return links.type; + else if (declaration && (declaration.kind === ts.SyntaxKind.BinaryExpression || + ts.isAccessExpression(declaration) && + declaration.parent.kind === ts.SyntaxKind.BinaryExpression)) { + return getWidenedTypeForAssignmentDeclaration(symbol); } - - function getWriteTypeOfSymbolWithDeferredType(symbol: ts.Symbol): ts.Type | undefined { - const links = getSymbolLinks(symbol); - if (!links.writeType && links.deferralWriteConstituents) { - ts.Debug.assertIsDefined(links.deferralParent); - ts.Debug.assertIsDefined(links.deferralConstituents); - links.writeType = links.deferralParent.flags & ts.TypeFlags.Union ? getUnionType(links.deferralWriteConstituents) : getIntersectionType(links.deferralWriteConstituents); + else if (symbol.flags & ts.SymbolFlags.ValueModule && declaration && ts.isSourceFile(declaration) && declaration.commonJsModuleIndicator) { + const resolvedModule = resolveExternalModuleSymbol(symbol); + if (resolvedModule !== symbol) { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + return errorType; + } + const exportEquals = getMergedSymbol(symbol.exports!.get(ts.InternalSymbolName.ExportEquals)!); + const type = getWidenedTypeForAssignmentDeclaration(exportEquals, exportEquals === resolvedModule ? undefined : resolvedModule); + if (!popTypeResolution()) { + return reportCircularityError(symbol); + } + return type; } - return links.writeType; } + const type = createObjectType(ts.ObjectFlags.Anonymous, symbol); + if (symbol.flags & ts.SymbolFlags.Class) { + const baseTypeVariable = getBaseTypeVariableOfClass(symbol); + return baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type; + } + else { + return strictNullChecks && symbol.flags & ts.SymbolFlags.Optional ? getOptionalType(type) : type; + } + } - /** - * Distinct write types come only from set accessors, but synthetic union and intersection - * properties deriving from set accessors will either pre-compute or defer the union or - * intersection of the writeTypes of their constituents. - */ - function getWriteTypeOfSymbol(symbol: ts.Symbol): ts.Type { - const checkFlags = ts.getCheckFlags(symbol); - if (symbol.flags & ts.SymbolFlags.Property) { - return checkFlags & ts.CheckFlags.SyntheticProperty ? - checkFlags & ts.CheckFlags.DeferredType ? - getWriteTypeOfSymbolWithDeferredType(symbol) || getTypeOfSymbolWithDeferredType(symbol) : - (symbol as ts.TransientSymbol).writeType || (symbol as ts.TransientSymbol).type! : - getTypeOfSymbol(symbol); - } - if (symbol.flags & ts.SymbolFlags.Accessor) { - return checkFlags & ts.CheckFlags.Instantiated ? - getWriteTypeOfInstantiatedSymbol(symbol) : - getWriteTypeOfAccessors(symbol); - } - return getTypeOfSymbol(symbol); + 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); + const exportSymbol = symbol.declarations && getTargetOfAliasDeclaration(getDeclarationOfAliasSymbol(symbol)!, /*dontResolveAlias*/ true); + const declaredType = ts.firstDefined(exportSymbol?.declarations, d => ts.isExportAssignment(d) ? tryGetTypeFromEffectiveTypeNode(d) : undefined); + // 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 = exportSymbol?.declarations && isDuplicatedCommonJSExport(exportSymbol.declarations) && symbol.declarations!.length ? getFlowTypeFromCommonJSExport(exportSymbol) + : isDuplicatedCommonJSExport(symbol.declarations) ? autoType + : declaredType ? declaredType + : targetSymbol.flags & ts.SymbolFlags.Value ? getTypeOfSymbol(targetSymbol) + : errorType; } + return links.type; + } - function getTypeOfSymbol(symbol: ts.Symbol): ts.Type { - const checkFlags = ts.getCheckFlags(symbol); - if (checkFlags & ts.CheckFlags.DeferredType) { - return getTypeOfSymbolWithDeferredType(symbol); - } - if (checkFlags & ts.CheckFlags.Instantiated) { - return getTypeOfInstantiatedSymbol(symbol); - } - if (checkFlags & ts.CheckFlags.Mapped) { - return getTypeOfMappedSymbol(symbol as ts.MappedSymbol); - } - if (checkFlags & ts.CheckFlags.ReverseMapped) { - return getTypeOfReverseMappedSymbol(symbol as ts.ReverseMappedSymbol); - } - if (symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) { - return getTypeOfVariableOrParameterOrProperty(symbol); - } - if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) { - return getTypeOfFuncClassEnumModule(symbol); - } - if (symbol.flags & ts.SymbolFlags.EnumMember) { - return getTypeOfEnumMember(symbol); - } - if (symbol.flags & ts.SymbolFlags.Accessor) { - return getTypeOfAccessors(symbol); - } - if (symbol.flags & ts.SymbolFlags.Alias) { - return getTypeOfAlias(symbol); - } + function getTypeOfInstantiatedSymbol(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + return links.type || (links.type = instantiateType(getTypeOfSymbol(links.target!), links.mapper)); + } + + function getWriteTypeOfInstantiatedSymbol(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + return links.writeType || (links.writeType = instantiateType(getWriteTypeOfSymbol(links.target!), links.mapper)); + } + + function reportCircularityError(symbol: ts.Symbol) { + const declaration = symbol.valueDeclaration as ts.VariableLikeDeclaration; + // Check if variable has type annotation that circularly references the variable itself + if (ts.getEffectiveTypeAnnotationNode(declaration)) { + error(symbol.valueDeclaration, ts.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 !== ts.SyntaxKind.Parameter || (declaration as ts.HasInitializer).initializer)) { + error(symbol.valueDeclaration, ts.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 getNonMissingTypeOfSymbol(symbol: ts.Symbol) { - return removeMissingType(getTypeOfSymbol(symbol), !!(symbol.flags & ts.SymbolFlags.Optional)); + function getTypeOfSymbolWithDeferredType(symbol: ts.Symbol) { + const links = getSymbolLinks(symbol); + if (!links.type) { + ts.Debug.assertIsDefined(links.deferralParent); + ts.Debug.assertIsDefined(links.deferralConstituents); + links.type = links.deferralParent.flags & ts.TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents); } + return links.type; + } - function isReferenceToType(type: ts.Type, target: ts.Type) { - return type !== undefined - && target !== undefined - && (ts.getObjectFlags(type) & ts.ObjectFlags.Reference) !== 0 - && (type as ts.TypeReference).target === target; + function getWriteTypeOfSymbolWithDeferredType(symbol: ts.Symbol): ts.Type | undefined { + const links = getSymbolLinks(symbol); + if (!links.writeType && links.deferralWriteConstituents) { + ts.Debug.assertIsDefined(links.deferralParent); + ts.Debug.assertIsDefined(links.deferralConstituents); + links.writeType = links.deferralParent.flags & ts.TypeFlags.Union ? getUnionType(links.deferralWriteConstituents) : getIntersectionType(links.deferralWriteConstituents); } + return links.writeType; + } + + /** + * Distinct write types come only from set accessors, but synthetic union and intersection + * properties deriving from set accessors will either pre-compute or defer the union or + * intersection of the writeTypes of their constituents. + */ + function getWriteTypeOfSymbol(symbol: ts.Symbol): ts.Type { + const checkFlags = ts.getCheckFlags(symbol); + if (symbol.flags & ts.SymbolFlags.Property) { + return checkFlags & ts.CheckFlags.SyntheticProperty ? + checkFlags & ts.CheckFlags.DeferredType ? + getWriteTypeOfSymbolWithDeferredType(symbol) || getTypeOfSymbolWithDeferredType(symbol) : + (symbol as ts.TransientSymbol).writeType || (symbol as ts.TransientSymbol).type! : + getTypeOfSymbol(symbol); + } + if (symbol.flags & ts.SymbolFlags.Accessor) { + return checkFlags & ts.CheckFlags.Instantiated ? + getWriteTypeOfInstantiatedSymbol(symbol) : + getWriteTypeOfAccessors(symbol); + } + return getTypeOfSymbol(symbol); + } - function getTargetType(type: ts.Type): ts.Type { - return ts.getObjectFlags(type) & ts.ObjectFlags.Reference ? (type as ts.TypeReference).target : type; + function getTypeOfSymbol(symbol: ts.Symbol): ts.Type { + const checkFlags = ts.getCheckFlags(symbol); + if (checkFlags & ts.CheckFlags.DeferredType) { + return getTypeOfSymbolWithDeferredType(symbol); + } + if (checkFlags & ts.CheckFlags.Instantiated) { + return getTypeOfInstantiatedSymbol(symbol); + } + if (checkFlags & ts.CheckFlags.Mapped) { + return getTypeOfMappedSymbol(symbol as ts.MappedSymbol); + } + if (checkFlags & ts.CheckFlags.ReverseMapped) { + return getTypeOfReverseMappedSymbol(symbol as ts.ReverseMappedSymbol); + } + if (symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) { + return getTypeOfVariableOrParameterOrProperty(symbol); + } + if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) { + return getTypeOfFuncClassEnumModule(symbol); } + if (symbol.flags & ts.SymbolFlags.EnumMember) { + return getTypeOfEnumMember(symbol); + } + if (symbol.flags & ts.SymbolFlags.Accessor) { + return getTypeOfAccessors(symbol); + } + if (symbol.flags & ts.SymbolFlags.Alias) { + return getTypeOfAlias(symbol); + } + return errorType; + } - // 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 (ts.getObjectFlags(type) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference)) { - const target = getTargetType(type) as ts.InterfaceType; - return target === checkBase || ts.some(getBaseTypes(target), check); - } - else if (type.flags & ts.TypeFlags.Intersection) { - return ts.some((type as ts.IntersectionType).types, check); - } - return false; + function getNonMissingTypeOfSymbol(symbol: ts.Symbol) { + return removeMissingType(getTypeOfSymbol(symbol), !!(symbol.flags & ts.SymbolFlags.Optional)); + } + + function isReferenceToType(type: ts.Type, target: ts.Type) { + return type !== undefined + && target !== undefined + && (ts.getObjectFlags(type) & ts.ObjectFlags.Reference) !== 0 + && (type as ts.TypeReference).target === target; + } + + function getTargetType(type: ts.Type): ts.Type { + return ts.getObjectFlags(type) & ts.ObjectFlags.Reference ? (type as ts.TypeReference).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 (ts.getObjectFlags(type) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference)) { + const target = getTargetType(type) as ts.InterfaceType; + return target === checkBase || ts.some(getBaseTypes(target), check); + } + else if (type.flags & ts.TypeFlags.Intersection) { + return ts.some((type as ts.IntersectionType).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: ts.TypeParameter[] | undefined, declarations: readonly ts.TypeParameterDeclaration[]): ts.TypeParameter[] | undefined { - for (const declaration of declarations) { - typeParameters = ts.appendIfUnique(typeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode(declaration))); - } - return typeParameters; + // 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: ts.TypeParameter[] | undefined, declarations: readonly ts.TypeParameterDeclaration[]): ts.TypeParameter[] | undefined { + for (const declaration of declarations) { + typeParameters = ts.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: ts.Node, includeThisTypes?: boolean): ts.TypeParameter[] | undefined { - while (true) { - node = node.parent; // TODO: GH#18217 Use SourceFile kind check instead - if (node && ts.isBinaryExpression(node)) { - // prototype assignments get the outer type parameters of their constructor function - const assignmentKind = ts.getAssignmentDeclarationKind(node); - if (assignmentKind === ts.AssignmentDeclarationKind.Prototype || assignmentKind === ts.AssignmentDeclarationKind.PrototypeProperty) { - const symbol = getSymbolOfNode(node.left); - if (symbol && symbol.parent && !ts.findAncestor(symbol.parent.valueDeclaration, d => node === d)) { - node = symbol.parent.valueDeclaration!; - } + // Return the outer type parameters of a node or undefined if the node has no outer type parameters. + function getOuterTypeParameters(node: ts.Node, includeThisTypes?: boolean): ts.TypeParameter[] | undefined { + while (true) { + node = node.parent; // TODO: GH#18217 Use SourceFile kind check instead + if (node && ts.isBinaryExpression(node)) { + // prototype assignments get the outer type parameters of their constructor function + const assignmentKind = ts.getAssignmentDeclarationKind(node); + if (assignmentKind === ts.AssignmentDeclarationKind.Prototype || assignmentKind === ts.AssignmentDeclarationKind.PrototypeProperty) { + const symbol = getSymbolOfNode(node.left); + if (symbol && symbol.parent && !ts.findAncestor(symbol.parent.valueDeclaration, d => node === d)) { + node = symbol.parent.valueDeclaration!; } } - if (!node) { - return undefined; + } + if (!node) { + return undefined; + } + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.JSDocFunctionType: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.JSDocTemplateTag: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocEnumTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.MappedType: + case ts.SyntaxKind.ConditionalType: { + const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); + if (node.kind === ts.SyntaxKind.MappedType) { + return ts.append(outerTypeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode((node as ts.MappedTypeNode).typeParameter))); + } + else if (node.kind === ts.SyntaxKind.ConditionalType) { + return ts.concatenate(outerTypeParameters, getInferTypeParameters(node as ts.ConditionalTypeNode)); + } + const outerAndOwnTypeParameters = appendTypeParameters(outerTypeParameters, ts.getEffectiveTypeParameterDeclarations(node as ts.DeclarationWithTypeParameters)); + const thisType = includeThisTypes && + (node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression || node.kind === ts.SyntaxKind.InterfaceDeclaration || isJSConstructor(node)) && + getDeclaredTypeOfClassOrInterface(getSymbolOfNode(node as ts.ClassLikeDeclaration | ts.InterfaceDeclaration)).thisType; + return thisType ? ts.append(outerAndOwnTypeParameters, thisType) : outerAndOwnTypeParameters; } - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.JSDocFunctionType: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.JSDocTemplateTag: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocEnumTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.MappedType: - case ts.SyntaxKind.ConditionalType: { - const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); - if (node.kind === ts.SyntaxKind.MappedType) { - return ts.append(outerTypeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode((node as ts.MappedTypeNode).typeParameter))); - } - else if (node.kind === ts.SyntaxKind.ConditionalType) { - return ts.concatenate(outerTypeParameters, getInferTypeParameters(node as ts.ConditionalTypeNode)); - } - const outerAndOwnTypeParameters = appendTypeParameters(outerTypeParameters, ts.getEffectiveTypeParameterDeclarations(node as ts.DeclarationWithTypeParameters)); - const thisType = includeThisTypes && - (node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression || node.kind === ts.SyntaxKind.InterfaceDeclaration || isJSConstructor(node)) && - getDeclaredTypeOfClassOrInterface(getSymbolOfNode(node as ts.ClassLikeDeclaration | ts.InterfaceDeclaration)).thisType; - return thisType ? ts.append(outerAndOwnTypeParameters, thisType) : outerAndOwnTypeParameters; - } - case ts.SyntaxKind.JSDocParameterTag: - const paramSymbol = ts.getParameterSymbolFromJSDoc(node as ts.JSDocParameterTag); - if (paramSymbol) { - node = paramSymbol.valueDeclaration!; - } - break; - case ts.SyntaxKind.JSDoc: { - const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); - return (node as ts.JSDoc).tags - ? appendTypeParameters(outerTypeParameters, ts.flatMap((node as ts.JSDoc).tags, t => ts.isJSDocTemplateTag(t) ? t.typeParameters : undefined)) - : outerTypeParameters; + case ts.SyntaxKind.JSDocParameterTag: + const paramSymbol = ts.getParameterSymbolFromJSDoc(node as ts.JSDocParameterTag); + if (paramSymbol) { + node = paramSymbol.valueDeclaration!; } + break; + case ts.SyntaxKind.JSDoc: { + const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); + return (node as ts.JSDoc).tags + ? appendTypeParameters(outerTypeParameters, ts.flatMap((node as ts.JSDoc).tags, t => ts.isJSDocTemplateTag(t) ? t.typeParameters : undefined)) + : outerTypeParameters; } } } + } - // The outer type parameters are those defined by enclosing generic classes, methods, or functions. - function getOuterTypeParametersOfClassOrInterface(symbol: ts.Symbol): ts.TypeParameter[] | undefined { - const declaration = symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : ts.getDeclarationOfKind(symbol, ts.SyntaxKind.InterfaceDeclaration)!; - ts.Debug.assert(!!declaration, "Class was missing valueDeclaration -OR- non-class had no interface declarations"); - return getOuterTypeParameters(declaration); - } + // The outer type parameters are those defined by enclosing generic classes, methods, or functions. + function getOuterTypeParametersOfClassOrInterface(symbol: ts.Symbol): ts.TypeParameter[] | undefined { + const declaration = symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : ts.getDeclarationOfKind(symbol, ts.SyntaxKind.InterfaceDeclaration)!; + ts.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): ts.TypeParameter[] | undefined { - if (!symbol.declarations) { - return; - } - let result: ts.TypeParameter[] | undefined; - for (const node of symbol.declarations) { - if (node.kind === ts.SyntaxKind.InterfaceDeclaration || - node.kind === ts.SyntaxKind.ClassDeclaration || - node.kind === ts.SyntaxKind.ClassExpression || - isJSConstructor(node) || - ts.isTypeAlias(node)) { - const declaration = node as ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTypedefTag | ts.JSDocCallbackTag; - result = appendTypeParameters(result, ts.getEffectiveTypeParameterDeclarations(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): ts.TypeParameter[] | undefined { + if (!symbol.declarations) { + return; + } + let result: ts.TypeParameter[] | undefined; + for (const node of symbol.declarations) { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration || + node.kind === ts.SyntaxKind.ClassDeclaration || + node.kind === ts.SyntaxKind.ClassExpression || + isJSConstructor(node) || + ts.isTypeAlias(node)) { + const declaration = node as ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTypedefTag | ts.JSDocCallbackTag; + result = appendTypeParameters(result, ts.getEffectiveTypeParameterDeclarations(declaration)); } - return result; } + 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): ts.TypeParameter[] | undefined { - return ts.concatenate(getOuterTypeParametersOfClassOrInterface(symbol), getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)); - } + // 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): ts.TypeParameter[] | undefined { + return ts.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, ts.SignatureKind.Construct); - if (signatures.length === 1) { - const s = signatures[0]; - if (!s.typeParameters && s.parameters.length === 1 && signatureHasRestParameter(s)) { - const paramType = getTypeOfParameter(s.parameters[0]); - return isTypeAny(paramType) || getElementTypeOfArrayType(paramType) === anyType; - } + // 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, ts.SignatureKind.Construct); + if (signatures.length === 1) { + const s = signatures[0]; + if (!s.typeParameters && s.parameters.length === 1 && signatureHasRestParameter(s)) { + const paramType = getTypeOfParameter(s.parameters[0]); + return isTypeAny(paramType) || getElementTypeOfArrayType(paramType) === anyType; } - return false; } + return false; + } - function isConstructorType(type: ts.Type): boolean { - if (getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0) { - return true; - } - if (type.flags & ts.TypeFlags.TypeVariable) { - const constraint = getBaseConstraintOfType(type); - return !!constraint && isMixinConstructorType(constraint); - } - return false; + function isConstructorType(type: ts.Type): boolean { + if (getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0) { + return true; } - - function getBaseTypeNodeOfClass(type: ts.InterfaceType): ts.ExpressionWithTypeArguments | undefined { - const decl = ts.getClassLikeDeclarationOfSymbol(type.symbol); - return decl && ts.getEffectiveBaseTypeNode(decl); + if (type.flags & ts.TypeFlags.TypeVariable) { + const constraint = getBaseConstraintOfType(type); + return !!constraint && isMixinConstructorType(constraint); } + return false; + } - function getConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly ts.TypeNode[] | undefined, location: ts.Node): readonly ts.Signature[] { - const typeArgCount = ts.length(typeArgumentNodes); - const isJavascript = ts.isInJSFile(location); - return ts.filter(getSignaturesOfType(type, ts.SignatureKind.Construct), sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= ts.length(sig.typeParameters)); - } + function getBaseTypeNodeOfClass(type: ts.InterfaceType): ts.ExpressionWithTypeArguments | undefined { + const decl = ts.getClassLikeDeclarationOfSymbol(type.symbol); + return decl && ts.getEffectiveBaseTypeNode(decl); + } - function getInstantiatedConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly ts.TypeNode[] | undefined, location: ts.Node): readonly ts.Signature[] { - const signatures = getConstructorsForTypeArguments(type, typeArgumentNodes, location); - const typeArguments = ts.map(typeArgumentNodes, getTypeFromTypeNode); - return ts.sameMap(signatures, sig => ts.some(sig.typeParameters) ? getSignatureInstantiation(sig, typeArguments, ts.isInJSFile(location)) : sig); - } + function getConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly ts.TypeNode[] | undefined, location: ts.Node): readonly ts.Signature[] { + const typeArgCount = ts.length(typeArgumentNodes); + const isJavascript = ts.isInJSFile(location); + return ts.filter(getSignaturesOfType(type, ts.SignatureKind.Construct), sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= ts.length(sig.typeParameters)); + } - /** - * 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: ts.InterfaceType): ts.Type { - if (!type.resolvedBaseConstructorType) { - const decl = ts.getClassLikeDeclarationOfSymbol(type.symbol); - const extended = decl && ts.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) { - ts.Debug.assert(!extended.typeArguments); // Because this is in a JS file, and baseTypeNode is in an @extends tag - checkExpression(extended.expression); - } - if (baseConstructorType.flags & (ts.TypeFlags.Object | ts.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 as ts.ObjectType); - } - if (!popTypeResolution()) { - error(type.symbol.valueDeclaration, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_base_expression, symbolToString(type.symbol)); - return type.resolvedBaseConstructorType = errorType; - } - if (!(baseConstructorType.flags & ts.TypeFlags.Any) && baseConstructorType !== nullWideningType && !isConstructorType(baseConstructorType)) { - const err = error(baseTypeNode.expression, ts.Diagnostics.Type_0_is_not_a_constructor_function_type, typeToString(baseConstructorType)); - if (baseConstructorType.flags & ts.TypeFlags.TypeParameter) { - const constraint = getConstraintFromTypeParameter(baseConstructorType); - let ctorReturn: ts.Type = unknownType; - if (constraint) { - const ctorSig = getSignaturesOfType(constraint, ts.SignatureKind.Construct); - if (ctorSig[0]) { - ctorReturn = getReturnTypeOfSignature(ctorSig[0]); - } - } - if (baseConstructorType.symbol.declarations) { - ts.addRelatedInfo(err, ts.createDiagnosticForNode(baseConstructorType.symbol.declarations[0], ts.Diagnostics.Did_you_mean_for_0_to_be_constrained_to_type_new_args_Colon_any_1, symbolToString(baseConstructorType.symbol), typeToString(ctorReturn))); + function getInstantiatedConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly ts.TypeNode[] | undefined, location: ts.Node): readonly ts.Signature[] { + const signatures = getConstructorsForTypeArguments(type, typeArgumentNodes, location); + const typeArguments = ts.map(typeArgumentNodes, getTypeFromTypeNode); + return ts.sameMap(signatures, sig => ts.some(sig.typeParameters) ? getSignatureInstantiation(sig, typeArguments, ts.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: ts.InterfaceType): ts.Type { + if (!type.resolvedBaseConstructorType) { + const decl = ts.getClassLikeDeclarationOfSymbol(type.symbol); + const extended = decl && ts.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) { + ts.Debug.assert(!extended.typeArguments); // Because this is in a JS file, and baseTypeNode is in an @extends tag + checkExpression(extended.expression); + } + if (baseConstructorType.flags & (ts.TypeFlags.Object | ts.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 as ts.ObjectType); + } + if (!popTypeResolution()) { + error(type.symbol.valueDeclaration, ts.Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_base_expression, symbolToString(type.symbol)); + return type.resolvedBaseConstructorType = errorType; + } + if (!(baseConstructorType.flags & ts.TypeFlags.Any) && baseConstructorType !== nullWideningType && !isConstructorType(baseConstructorType)) { + const err = error(baseTypeNode.expression, ts.Diagnostics.Type_0_is_not_a_constructor_function_type, typeToString(baseConstructorType)); + if (baseConstructorType.flags & ts.TypeFlags.TypeParameter) { + const constraint = getConstraintFromTypeParameter(baseConstructorType); + let ctorReturn: ts.Type = unknownType; + if (constraint) { + const ctorSig = getSignaturesOfType(constraint, ts.SignatureKind.Construct); + if (ctorSig[0]) { + ctorReturn = getReturnTypeOfSignature(ctorSig[0]); } } - return type.resolvedBaseConstructorType = errorType; + if (baseConstructorType.symbol.declarations) { + ts.addRelatedInfo(err, ts.createDiagnosticForNode(baseConstructorType.symbol.declarations[0], ts.Diagnostics.Did_you_mean_for_0_to_be_constrained_to_type_new_args_Colon_any_1, symbolToString(baseConstructorType.symbol), typeToString(ctorReturn))); + } } - type.resolvedBaseConstructorType = baseConstructorType; + return type.resolvedBaseConstructorType = errorType; } - return type.resolvedBaseConstructorType; + type.resolvedBaseConstructorType = baseConstructorType; } + return type.resolvedBaseConstructorType; + } - function getImplementsTypes(type: ts.InterfaceType): ts.BaseType[] { - let resolvedImplementsTypes: ts.BaseType[] = ts.emptyArray; - if (type.symbol.declarations) { - for (const declaration of type.symbol.declarations) { - const implementsTypeNodes = ts.getEffectiveImplementsTypeNodes(declaration as ts.ClassLikeDeclaration); - if (!implementsTypeNodes) - continue; - for (const node of implementsTypeNodes) { - const implementsType = getTypeFromTypeNode(node); - if (!isErrorType(implementsType)) { - if (resolvedImplementsTypes === ts.emptyArray) { - resolvedImplementsTypes = [implementsType as ts.ObjectType]; - } - else { - resolvedImplementsTypes.push(implementsType); - } + function getImplementsTypes(type: ts.InterfaceType): ts.BaseType[] { + let resolvedImplementsTypes: ts.BaseType[] = ts.emptyArray; + if (type.symbol.declarations) { + for (const declaration of type.symbol.declarations) { + const implementsTypeNodes = ts.getEffectiveImplementsTypeNodes(declaration as ts.ClassLikeDeclaration); + if (!implementsTypeNodes) + continue; + for (const node of implementsTypeNodes) { + const implementsType = getTypeFromTypeNode(node); + if (!isErrorType(implementsType)) { + if (resolvedImplementsTypes === ts.emptyArray) { + resolvedImplementsTypes = [implementsType as ts.ObjectType]; + } + else { + resolvedImplementsTypes.push(implementsType); } } } } - return resolvedImplementsTypes; } + return resolvedImplementsTypes; + } - function reportCircularBaseType(node: ts.Node, type: ts.Type) { - error(node, ts.Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrayAsGenericType)); - } + function reportCircularBaseType(node: ts.Node, type: ts.Type) { + error(node, ts.Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrayAsGenericType)); + } - function getBaseTypes(type: ts.InterfaceType): ts.BaseType[] { - if (!type.baseTypesResolved) { - if (pushTypeResolution(type, TypeSystemPropertyName.ResolvedBaseTypes)) { - if (type.objectFlags & ts.ObjectFlags.Tuple) { - type.resolvedBaseTypes = [getTupleBaseType(type as ts.TupleType)]; - } - else if (type.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - if (type.symbol.flags & ts.SymbolFlags.Class) { - resolveBaseTypesOfClass(type); - } - if (type.symbol.flags & ts.SymbolFlags.Interface) { - resolveBaseTypesOfInterface(type); - } + function getBaseTypes(type: ts.InterfaceType): ts.BaseType[] { + if (!type.baseTypesResolved) { + if (pushTypeResolution(type, TypeSystemPropertyName.ResolvedBaseTypes)) { + if (type.objectFlags & ts.ObjectFlags.Tuple) { + type.resolvedBaseTypes = [getTupleBaseType(type as ts.TupleType)]; + } + else if (type.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { + if (type.symbol.flags & ts.SymbolFlags.Class) { + resolveBaseTypesOfClass(type); } - else { - ts.Debug.fail("type must be class or interface"); + if (type.symbol.flags & ts.SymbolFlags.Interface) { + resolveBaseTypesOfInterface(type); } - if (!popTypeResolution() && type.symbol.declarations) { - for (const declaration of type.symbol.declarations) { - if (declaration.kind === ts.SyntaxKind.ClassDeclaration || declaration.kind === ts.SyntaxKind.InterfaceDeclaration) { - reportCircularBaseType(declaration, type); - } + } + else { + ts.Debug.fail("type must be class or interface"); + } + if (!popTypeResolution() && type.symbol.declarations) { + for (const declaration of type.symbol.declarations) { + if (declaration.kind === ts.SyntaxKind.ClassDeclaration || declaration.kind === ts.SyntaxKind.InterfaceDeclaration) { + reportCircularBaseType(declaration, type); } } } - type.baseTypesResolved = true; } - return type.resolvedBaseTypes; + type.baseTypesResolved = true; } + return type.resolvedBaseTypes; + } - function getTupleBaseType(type: ts.TupleType) { - const elementTypes = ts.sameMap(type.typeParameters, (t, i) => type.elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t); - return createArrayType(getUnionType(elementTypes || ts.emptyArray), type.readonly); - } + function getTupleBaseType(type: ts.TupleType) { + const elementTypes = ts.sameMap(type.typeParameters, (t, i) => type.elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t); + return createArrayType(getUnionType(elementTypes || ts.emptyArray), type.readonly); + } - function resolveBaseTypesOfClass(type: ts.InterfaceType) { - type.resolvedBaseTypes = ts.resolvingEmptyArray; - const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type)); - if (!(baseConstructorType.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.Any))) { + function resolveBaseTypesOfClass(type: ts.InterfaceType) { + type.resolvedBaseTypes = ts.resolvingEmptyArray; + const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type)); + if (!(baseConstructorType.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.Any))) { + return type.resolvedBaseTypes = ts.emptyArray; + } + const baseTypeNode = getBaseTypeNodeOfClass(type)!; + let baseType: ts.Type; + const originalBaseType = baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined; + if (baseConstructorType.symbol && baseConstructorType.symbol.flags & ts.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 & ts.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, ts.Diagnostics.No_base_constructor_has_the_specified_number_of_type_arguments); return type.resolvedBaseTypes = ts.emptyArray; } - const baseTypeNode = getBaseTypeNodeOfClass(type)!; - let baseType: ts.Type; - const originalBaseType = baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined; - if (baseConstructorType.symbol && baseConstructorType.symbol.flags & ts.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 & ts.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, ts.Diagnostics.No_base_constructor_has_the_specified_number_of_type_arguments); - return type.resolvedBaseTypes = ts.emptyArray; - } - baseType = getReturnTypeOfSignature(constructors[0]); - } + baseType = getReturnTypeOfSignature(constructors[0]); + } - if (isErrorType(baseType)) { - return type.resolvedBaseTypes = ts.emptyArray; - } - const reducedBaseType = getReducedType(baseType); - if (!isValidBaseType(reducedBaseType)) { - const elaboration = elaborateNeverIntersection(/*errorInfo*/ undefined, baseType); - const diagnostic = ts.chainDiagnosticMessages(elaboration, ts.Diagnostics.Base_constructor_return_type_0_is_not_an_object_type_or_intersection_of_object_types_with_statically_known_members, typeToString(reducedBaseType)); - diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(baseTypeNode.expression, diagnostic)); - return type.resolvedBaseTypes = ts.emptyArray; - } - if (type === reducedBaseType || hasBaseType(reducedBaseType, type)) { - error(type.symbol.valueDeclaration, ts.Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrayAsGenericType)); - return type.resolvedBaseTypes = ts.emptyArray; - } - if (type.resolvedBaseTypes === ts.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 = [reducedBaseType]; + if (isErrorType(baseType)) { + return type.resolvedBaseTypes = ts.emptyArray; + } + const reducedBaseType = getReducedType(baseType); + if (!isValidBaseType(reducedBaseType)) { + const elaboration = elaborateNeverIntersection(/*errorInfo*/ undefined, baseType); + const diagnostic = ts.chainDiagnosticMessages(elaboration, ts.Diagnostics.Base_constructor_return_type_0_is_not_an_object_type_or_intersection_of_object_types_with_statically_known_members, typeToString(reducedBaseType)); + diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(baseTypeNode.expression, diagnostic)); + return type.resolvedBaseTypes = ts.emptyArray; + } + if (type === reducedBaseType || hasBaseType(reducedBaseType, type)) { + error(type.symbol.valueDeclaration, ts.Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrayAsGenericType)); + return type.resolvedBaseTypes = ts.emptyArray; + } + if (type.resolvedBaseTypes === ts.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 = [reducedBaseType]; + } - 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 as ts.InterfaceType).outerTypeParameters; - if (outerTypeParameters) { - const last = outerTypeParameters.length - 1; - const typeArguments = getTypeArguments(type as ts.TypeReference); - return outerTypeParameters[last].symbol !== typeArguments[last].symbol; + 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 as ts.InterfaceType).outerTypeParameters; + if (outerTypeParameters) { + const last = outerTypeParameters.length - 1; + const typeArguments = getTypeArguments(type as ts.TypeReference); + 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 ts.BaseType { + if (type.flags & ts.TypeFlags.TypeParameter) { + const constraint = getBaseConstraintOfType(type); + if (constraint) { + return isValidBaseType(constraint); } - return true; } + // 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 & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Any) && !isGenericMappedType(type) || + type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, isValidBaseType)); + } - // A valid base type is `any`, an object type or intersection of object types. - function isValidBaseType(type: ts.Type): type is ts.BaseType { - if (type.flags & ts.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 & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Any) && !isGenericMappedType(type) || - type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, isValidBaseType)); - } - - function resolveBaseTypesOfInterface(type: ts.InterfaceType): void { - type.resolvedBaseTypes = type.resolvedBaseTypes || ts.emptyArray; - if (type.symbol.declarations) { - for (const declaration of type.symbol.declarations) { - if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration && ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration)) { - for (const node of ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration)!) { - const baseType = getReducedType(getTypeFromTypeNode(node)); - if (!isErrorType(baseType)) { - if (isValidBaseType(baseType)) { - if (type !== baseType && !hasBaseType(baseType, type)) { - if (type.resolvedBaseTypes === ts.emptyArray) { - type.resolvedBaseTypes = [baseType as ts.ObjectType]; - } - else { - type.resolvedBaseTypes.push(baseType); - } + function resolveBaseTypesOfInterface(type: ts.InterfaceType): void { + type.resolvedBaseTypes = type.resolvedBaseTypes || ts.emptyArray; + if (type.symbol.declarations) { + for (const declaration of type.symbol.declarations) { + if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration && ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration)) { + for (const node of ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration)!) { + const baseType = getReducedType(getTypeFromTypeNode(node)); + if (!isErrorType(baseType)) { + if (isValidBaseType(baseType)) { + if (type !== baseType && !hasBaseType(baseType, type)) { + if (type.resolvedBaseTypes === ts.emptyArray) { + type.resolvedBaseTypes = [baseType as ts.ObjectType]; } else { - reportCircularBaseType(declaration, type); + type.resolvedBaseTypes.push(baseType); } } else { - error(node, ts.Diagnostics.An_interface_can_only_extend_an_object_type_or_intersection_of_object_types_with_statically_known_members); + reportCircularBaseType(declaration, type); } } + else { + error(node, ts.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: ts.Symbol): boolean { - if (!symbol.declarations) { - return true; - } - for (const declaration of symbol.declarations) { - if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration) { - if (declaration.flags & ts.NodeFlags.ContainsThis) { - return false; - } - const baseTypeNodes = ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration); - if (baseTypeNodes) { - for (const node of baseTypeNodes) { - if (ts.isEntityNameExpression(node.expression)) { - const baseSymbol = resolveEntityName(node.expression, ts.SymbolFlags.Type, /*ignoreErrors*/ true); - if (!baseSymbol || !(baseSymbol.flags & ts.SymbolFlags.Interface) || getDeclaredTypeOfClassOrInterface(baseSymbol).thisType) { - return false; - } + /** + * 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 { + if (!symbol.declarations) { + return true; + } + for (const declaration of symbol.declarations) { + if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration) { + if (declaration.flags & ts.NodeFlags.ContainsThis) { + return false; + } + const baseTypeNodes = ts.getInterfaceBaseTypeNodes(declaration as ts.InterfaceDeclaration); + if (baseTypeNodes) { + for (const node of baseTypeNodes) { + if (ts.isEntityNameExpression(node.expression)) { + const baseSymbol = resolveEntityName(node.expression, ts.SymbolFlags.Type, /*ignoreErrors*/ true); + if (!baseSymbol || !(baseSymbol.flags & ts.SymbolFlags.Interface) || getDeclaredTypeOfClassOrInterface(baseSymbol).thisType) { + return false; } } } } } - return true; } + return true; + } - function getDeclaredTypeOfClassOrInterface(symbol: ts.Symbol): ts.InterfaceType { - let links = getSymbolLinks(symbol); - const originalLinks = links; - if (!links.declaredType) { - const kind = symbol.flags & ts.SymbolFlags.Class ? ts.ObjectFlags.Class : ts.ObjectFlags.Interface; - const merged = mergeJSSymbols(symbol, symbol.valueDeclaration && getAssignedClassSymbol(symbol.valueDeclaration)); - if (merged) { - // note:we overwrite links because we just cloned the symbol - symbol = links = merged; - } + function getDeclaredTypeOfClassOrInterface(symbol: ts.Symbol): ts.InterfaceType { + let links = getSymbolLinks(symbol); + const originalLinks = links; + if (!links.declaredType) { + const kind = symbol.flags & ts.SymbolFlags.Class ? ts.ObjectFlags.Class : ts.ObjectFlags.Interface; + const merged = mergeJSSymbols(symbol, symbol.valueDeclaration && 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) as ts.InterfaceType; + 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 === ts.ObjectFlags.Class || !isThislessInterface(symbol)) { + type.objectFlags |= ts.ObjectFlags.Reference; + type.typeParameters = ts.concatenate(outerTypeParameters, localTypeParameters); + type.outerTypeParameters = outerTypeParameters; + type.localTypeParameters = localTypeParameters; + (type as ts.GenericType).instantiations = new ts.Map(); + (type as ts.GenericType).instantiations.set(getTypeListId(type.typeParameters), type as ts.GenericType); + (type as ts.GenericType).target = type as ts.GenericType; + (type as ts.GenericType).resolvedTypeArguments = type.typeParameters; + type.thisType = createTypeParameter(symbol); + type.thisType.isThisType = true; + type.thisType.constraint = type; + } + } + return links.declaredType as ts.InterfaceType; + } - const type = originalLinks.declaredType = links.declaredType = createObjectType(kind, symbol) as ts.InterfaceType; - 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 === ts.ObjectFlags.Class || !isThislessInterface(symbol)) { - type.objectFlags |= ts.ObjectFlags.Reference; - type.typeParameters = ts.concatenate(outerTypeParameters, localTypeParameters); - type.outerTypeParameters = outerTypeParameters; - type.localTypeParameters = localTypeParameters; - (type as ts.GenericType).instantiations = new ts.Map(); - (type as ts.GenericType).instantiations.set(getTypeListId(type.typeParameters), type as ts.GenericType); - (type as ts.GenericType).target = type as ts.GenericType; - (type as ts.GenericType).resolvedTypeArguments = type.typeParameters; - type.thisType = createTypeParameter(symbol); - type.thisType.isThisType = true; - type.thisType.constraint = type; - } - } - return links.declaredType as ts.InterfaceType; - } - - 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; - } + 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; + } - const declaration = ts.Debug.checkDefined(symbol.declarations?.find(ts.isTypeAlias), "Type alias symbol with no valid declaration found"); - const typeNode = ts.isJSDocTypeAlias(declaration) ? declaration.typeExpression : declaration.type; - // If typeNode is missing, we will error in checkJSDocTypedefTag. - let type = typeNode ? getTypeFromTypeNode(typeNode) : errorType; + const declaration = ts.Debug.checkDefined(symbol.declarations?.find(ts.isTypeAlias), "Type alias symbol with no valid declaration found"); + const typeNode = ts.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 = new ts.Map(); - links.instantiations.set(getTypeListId(typeParameters), type); - } + 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 = new ts.Map(); + links.instantiations.set(getTypeListId(typeParameters), type); + } + } + else { + type = errorType; + if (declaration.kind === ts.SyntaxKind.JSDocEnumTag) { + error(declaration.typeExpression.type, ts.Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); } else { - type = errorType; - if (declaration.kind === ts.SyntaxKind.JSDocEnumTag) { - error(declaration.typeExpression.type, ts.Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); - } - else { - error(ts.isNamedDeclaration(declaration) ? declaration.name : declaration || declaration, ts.Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); - } + error(ts.isNamedDeclaration(declaration) ? declaration.name : declaration || declaration, ts.Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); } - links.declaredType = type; } - return links.declaredType; + links.declaredType = type; } + return links.declaredType; + } - function isStringConcatExpression(expr: ts.Node): boolean { - if (ts.isStringLiteralLike(expr)) { - return true; - } - else if (expr.kind === ts.SyntaxKind.BinaryExpression) { - return isStringConcatExpression((expr as ts.BinaryExpression).left) && isStringConcatExpression((expr as ts.BinaryExpression).right); - } - return false; + function isStringConcatExpression(expr: ts.Node): boolean { + if (ts.isStringLiteralLike(expr)) { + return true; + } + else if (expr.kind === ts.SyntaxKind.BinaryExpression) { + return isStringConcatExpression((expr as ts.BinaryExpression).left) && isStringConcatExpression((expr as ts.BinaryExpression).right); } + return false; + } - function isLiteralEnumMember(member: ts.EnumMember) { - const expr = member.initializer; - if (!expr) { - return !(member.flags & ts.NodeFlags.Ambient); - } - switch (expr.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return true; - case ts.SyntaxKind.PrefixUnaryExpression: - return (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && - (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.NumericLiteral; - case ts.SyntaxKind.Identifier: - return ts.nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports!.get((expr as ts.Identifier).escapedText); - case ts.SyntaxKind.BinaryExpression: - return isStringConcatExpression(expr); - default: - return false; - } + function isLiteralEnumMember(member: ts.EnumMember) { + const expr = member.initializer; + if (!expr) { + return !(member.flags & ts.NodeFlags.Ambient); + } + switch (expr.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return true; + case ts.SyntaxKind.PrefixUnaryExpression: + return (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && + (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.NumericLiteral; + case ts.SyntaxKind.Identifier: + return ts.nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports!.get((expr as ts.Identifier).escapedText); + case ts.SyntaxKind.BinaryExpression: + return isStringConcatExpression(expr); + default: + return false; } + } - function getEnumKind(symbol: ts.Symbol): ts.EnumKind { - const links = getSymbolLinks(symbol); - if (links.enumKind !== undefined) { - return links.enumKind; - } - let hasNonLiteralMember = false; - if (symbol.declarations) { - for (const declaration of symbol.declarations) { - if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { - for (const member of (declaration as ts.EnumDeclaration).members) { - if (member.initializer && ts.isStringLiteralLike(member.initializer)) { - return links.enumKind = ts.EnumKind.Literal; - } - if (!isLiteralEnumMember(member)) { - hasNonLiteralMember = true; - } + function getEnumKind(symbol: ts.Symbol): ts.EnumKind { + const links = getSymbolLinks(symbol); + if (links.enumKind !== undefined) { + return links.enumKind; + } + let hasNonLiteralMember = false; + if (symbol.declarations) { + for (const declaration of symbol.declarations) { + if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { + for (const member of (declaration as ts.EnumDeclaration).members) { + if (member.initializer && ts.isStringLiteralLike(member.initializer)) { + return links.enumKind = ts.EnumKind.Literal; + } + if (!isLiteralEnumMember(member)) { + hasNonLiteralMember = true; } } } } - return links.enumKind = hasNonLiteralMember ? ts.EnumKind.Numeric : ts.EnumKind.Literal; } + return links.enumKind = hasNonLiteralMember ? ts.EnumKind.Numeric : ts.EnumKind.Literal; + } - function getBaseTypeOfEnumLiteralType(type: ts.Type) { - return type.flags & ts.TypeFlags.EnumLiteral && !(type.flags & ts.TypeFlags.Union) ? getDeclaredTypeOfSymbol(getParentOfSymbol(type.symbol)!) : type; - } + function getBaseTypeOfEnumLiteralType(type: ts.Type) { + return type.flags & ts.TypeFlags.EnumLiteral && !(type.flags & ts.TypeFlags.Union) ? getDeclaredTypeOfSymbol(getParentOfSymbol(type.symbol)!) : type; + } - function getDeclaredTypeOfEnum(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); - if (links.declaredType) { - return links.declaredType; - } - if (getEnumKind(symbol) === ts.EnumKind.Literal) { - enumCount++; - const memberTypeList: ts.Type[] = []; - if (symbol.declarations) { - for (const declaration of symbol.declarations) { - if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { - for (const member of (declaration as ts.EnumDeclaration).members) { - const value = getEnumMemberValue(member); - const memberType = getFreshTypeOfLiteralType(getEnumLiteralType(value !== undefined ? value : 0, enumCount, getSymbolOfNode(member))); - getSymbolLinks(getSymbolOfNode(member)).declaredType = memberType; - memberTypeList.push(getRegularTypeOfLiteralType(memberType)); - } + function getDeclaredTypeOfEnum(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (links.declaredType) { + return links.declaredType; + } + if (getEnumKind(symbol) === ts.EnumKind.Literal) { + enumCount++; + const memberTypeList: ts.Type[] = []; + if (symbol.declarations) { + for (const declaration of symbol.declarations) { + if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { + for (const member of (declaration as ts.EnumDeclaration).members) { + const value = getEnumMemberValue(member); + const memberType = getFreshTypeOfLiteralType(getEnumLiteralType(value !== undefined ? value : 0, enumCount, getSymbolOfNode(member))); + getSymbolLinks(getSymbolOfNode(member)).declaredType = memberType; + memberTypeList.push(getRegularTypeOfLiteralType(memberType)); } } } - if (memberTypeList.length) { - const enumType = getUnionType(memberTypeList, ts.UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined); - if (enumType.flags & ts.TypeFlags.Union) { - enumType.flags |= ts.TypeFlags.EnumLiteral; - enumType.symbol = symbol; - } - return links.declaredType = enumType; + } + if (memberTypeList.length) { + const enumType = getUnionType(memberTypeList, ts.UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined); + if (enumType.flags & ts.TypeFlags.Union) { + enumType.flags |= ts.TypeFlags.EnumLiteral; + enumType.symbol = symbol; } + return links.declaredType = enumType; } - const enumType = createType(ts.TypeFlags.Enum); - enumType.symbol = symbol; - return links.declaredType = enumType; } + const enumType = createType(ts.TypeFlags.Enum); + enumType.symbol = symbol; + return links.declaredType = enumType; + } - function getDeclaredTypeOfEnumMember(symbol: ts.Symbol): ts.Type { - const links = getSymbolLinks(symbol); + function getDeclaredTypeOfEnumMember(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.declaredType) { + const enumType = getDeclaredTypeOfEnum(getParentOfSymbol(symbol)!); if (!links.declaredType) { - const enumType = getDeclaredTypeOfEnum(getParentOfSymbol(symbol)!); - if (!links.declaredType) { - links.declaredType = enumType; - } + links.declaredType = enumType; } - return links.declaredType; } + return links.declaredType; + } - function getDeclaredTypeOfTypeParameter(symbol: ts.Symbol): ts.TypeParameter { - const links = getSymbolLinks(symbol); - return links.declaredType || (links.declaredType = createTypeParameter(symbol)); - } + function getDeclaredTypeOfTypeParameter(symbol: ts.Symbol): ts.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 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 getDeclaredTypeOfSymbol(symbol: ts.Symbol): ts.Type { + return tryGetDeclaredTypeOfSymbol(symbol) || errorType; + } - function tryGetDeclaredTypeOfSymbol(symbol: ts.Symbol): ts.Type | undefined { - if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - return getDeclaredTypeOfClassOrInterface(symbol); - } - if (symbol.flags & ts.SymbolFlags.TypeAlias) { - return getDeclaredTypeOfTypeAlias(symbol); - } - if (symbol.flags & ts.SymbolFlags.TypeParameter) { - return getDeclaredTypeOfTypeParameter(symbol); - } - if (symbol.flags & ts.SymbolFlags.Enum) { - return getDeclaredTypeOfEnum(symbol); - } - if (symbol.flags & ts.SymbolFlags.EnumMember) { - return getDeclaredTypeOfEnumMember(symbol); - } - if (symbol.flags & ts.SymbolFlags.Alias) { - return getDeclaredTypeOfAlias(symbol); - } - return undefined; + function tryGetDeclaredTypeOfSymbol(symbol: ts.Symbol): ts.Type | undefined { + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { + return getDeclaredTypeOfClassOrInterface(symbol); } - - /** - * 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: ts.TypeNode): boolean { - switch (node.kind) { - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.UnknownKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.UndefinedKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.LiteralType: - return true; - case ts.SyntaxKind.ArrayType: - return isThislessType((node as ts.ArrayTypeNode).elementType); - case ts.SyntaxKind.TypeReference: - return !(node as ts.TypeReferenceNode).typeArguments || (node as ts.TypeReferenceNode).typeArguments!.every(isThislessType); - } - return false; + if (symbol.flags & ts.SymbolFlags.TypeAlias) { + return getDeclaredTypeOfTypeAlias(symbol); } - - /** A type parameter is thisless if its constraint is thisless, or if it has no constraint. */ - function isThislessTypeParameter(node: ts.TypeParameterDeclaration) { - const constraint = ts.getEffectiveConstraintOfTypeParameter(node); - return !constraint || isThislessType(constraint); + if (symbol.flags & ts.SymbolFlags.TypeParameter) { + return getDeclaredTypeOfTypeParameter(symbol); } - - /** - * 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: ts.VariableLikeDeclaration): boolean { - const typeNode = ts.getEffectiveTypeAnnotationNode(node); - return typeNode ? isThislessType(typeNode) : !ts.hasInitializer(node); + if (symbol.flags & ts.SymbolFlags.Enum) { + return getDeclaredTypeOfEnum(symbol); } - - /** - * 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: ts.FunctionLikeDeclaration): boolean { - const returnType = ts.getEffectiveReturnTypeNode(node); - const typeParameters = ts.getEffectiveTypeParameterDeclarations(node); - return (node.kind === ts.SyntaxKind.Constructor || (!!returnType && isThislessType(returnType))) && - node.parameters.every(isThislessVariableLikeDeclaration) && - typeParameters.every(isThislessTypeParameter); + if (symbol.flags & ts.SymbolFlags.EnumMember) { + return getDeclaredTypeOfEnumMember(symbol); } - - /** - * 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 ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - return isThislessVariableLikeDeclaration(declaration as ts.VariableLikeDeclaration); - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return isThislessFunctionLikeDeclaration(declaration as ts.FunctionLikeDeclaration | ts.AccessorDeclaration); - } - } - } - return false; + if (symbol.flags & ts.SymbolFlags.Alias) { + return getDeclaredTypeOfAlias(symbol); } + return undefined; + } - // 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: ts.TypeMapper, mappingThisOnly: boolean): ts.SymbolTable { - const result = ts.createSymbolTable(); - for (const symbol of symbols) { - result.set(symbol.escapedName, mappingThisOnly && isThisless(symbol) ? symbol : instantiateSymbol(symbol, mapper)); - } - return result; + /** + * 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: ts.TypeNode): boolean { + switch (node.kind) { + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.UndefinedKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.LiteralType: + return true; + case ts.SyntaxKind.ArrayType: + return isThislessType((node as ts.ArrayTypeNode).elementType); + case ts.SyntaxKind.TypeReference: + return !(node as ts.TypeReferenceNode).typeArguments || (node as ts.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: ts.TypeParameterDeclaration) { + const constraint = ts.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: ts.VariableLikeDeclaration): boolean { + const typeNode = ts.getEffectiveTypeAnnotationNode(node); + return typeNode ? isThislessType(typeNode) : !ts.hasInitializer(node); + } - function addInheritedMembers(symbols: ts.SymbolTable, baseSymbols: ts.Symbol[]) { - for (const s of baseSymbols) { - if (!symbols.has(s.escapedName) && !isStaticPrivateIdentifierProperty(s)) { - symbols.set(s.escapedName, s); + /** + * 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: ts.FunctionLikeDeclaration): boolean { + const returnType = ts.getEffectiveReturnTypeNode(node); + const typeParameters = ts.getEffectiveTypeParameterDeclarations(node); + return (node.kind === ts.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 ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + return isThislessVariableLikeDeclaration(declaration as ts.VariableLikeDeclaration); + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return isThislessFunctionLikeDeclaration(declaration as ts.FunctionLikeDeclaration | ts.AccessorDeclaration); } } } + return false; + } - function isStaticPrivateIdentifierProperty(s: ts.Symbol): boolean { - return !!s.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(s.valueDeclaration) && ts.isStatic(s.valueDeclaration); + // 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: ts.TypeMapper, mappingThisOnly: boolean): ts.SymbolTable { + const result = ts.createSymbolTable(); + for (const symbol of symbols) { + result.set(symbol.escapedName, mappingThisOnly && isThisless(symbol) ? symbol : instantiateSymbol(symbol, mapper)); } + return result; + } - function resolveDeclaredMembers(type: ts.InterfaceType): ts.InterfaceTypeWithDeclaredMembers { - if (!(type as ts.InterfaceTypeWithDeclaredMembers).declaredProperties) { - const symbol = type.symbol; - const members = getMembersOfSymbol(symbol); - (type as ts.InterfaceTypeWithDeclaredMembers).declaredProperties = getNamedMembers(members); - // Start with signatures at empty array in case of recursive types - (type as ts.InterfaceTypeWithDeclaredMembers).declaredCallSignatures = ts.emptyArray; - (type as ts.InterfaceTypeWithDeclaredMembers).declaredConstructSignatures = ts.emptyArray; - (type as ts.InterfaceTypeWithDeclaredMembers).declaredIndexInfos = ts.emptyArray; - (type as ts.InterfaceTypeWithDeclaredMembers).declaredCallSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.Call)); - (type as ts.InterfaceTypeWithDeclaredMembers).declaredConstructSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.New)); - (type as ts.InterfaceTypeWithDeclaredMembers).declaredIndexInfos = getIndexInfosOfSymbol(symbol); + function addInheritedMembers(symbols: ts.SymbolTable, baseSymbols: ts.Symbol[]) { + for (const s of baseSymbols) { + if (!symbols.has(s.escapedName) && !isStaticPrivateIdentifierProperty(s)) { + symbols.set(s.escapedName, s); } - return type as ts.InterfaceTypeWithDeclaredMembers; } + } - /** - * Indicates whether a type can be used as a property name. - */ - function isTypeUsableAsPropertyName(type: ts.Type): type is ts.StringLiteralType | ts.NumberLiteralType | ts.UniqueESSymbolType { - return !!(type.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique); - } + function isStaticPrivateIdentifierProperty(s: ts.Symbol): boolean { + return !!s.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(s.valueDeclaration) && ts.isStatic(s.valueDeclaration); + } - /** - * 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: ts.DeclarationName): node is ts.LateBoundName { - if (!ts.isComputedPropertyName(node) && !ts.isElementAccessExpression(node)) { - return false; - } - const expr = ts.isComputedPropertyName(node) ? node.expression : node.argumentExpression; - return ts.isEntityNameExpression(expr) - && isTypeUsableAsPropertyName(ts.isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr)); - } + function resolveDeclaredMembers(type: ts.InterfaceType): ts.InterfaceTypeWithDeclaredMembers { + if (!(type as ts.InterfaceTypeWithDeclaredMembers).declaredProperties) { + const symbol = type.symbol; + const members = getMembersOfSymbol(symbol); + (type as ts.InterfaceTypeWithDeclaredMembers).declaredProperties = getNamedMembers(members); + // Start with signatures at empty array in case of recursive types + (type as ts.InterfaceTypeWithDeclaredMembers).declaredCallSignatures = ts.emptyArray; + (type as ts.InterfaceTypeWithDeclaredMembers).declaredConstructSignatures = ts.emptyArray; + (type as ts.InterfaceTypeWithDeclaredMembers).declaredIndexInfos = ts.emptyArray; + (type as ts.InterfaceTypeWithDeclaredMembers).declaredCallSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.Call)); + (type as ts.InterfaceTypeWithDeclaredMembers).declaredConstructSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.New)); + (type as ts.InterfaceTypeWithDeclaredMembers).declaredIndexInfos = getIndexInfosOfSymbol(symbol); + } + return type as ts.InterfaceTypeWithDeclaredMembers; + } - function isLateBoundName(name: ts.__String): boolean { - return (name as string).charCodeAt(0) === ts.CharacterCodes._ && - (name as string).charCodeAt(1) === ts.CharacterCodes._ && - (name as string).charCodeAt(2) === ts.CharacterCodes.at; - } + /** + * Indicates whether a type can be used as a property name. + */ + function isTypeUsableAsPropertyName(type: ts.Type): type is ts.StringLiteralType | ts.NumberLiteralType | ts.UniqueESSymbolType { + return !!(type.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique); + } - /** - * Indicates whether a declaration has a late-bindable dynamic name. - */ - function hasLateBindableName(node: ts.Declaration): node is ts.LateBoundDeclaration | ts.LateBoundBinaryExpressionDeclaration { - const name = ts.getNameOfDeclaration(node); - return !!name && isLateBindableName(name); + /** + * 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: ts.DeclarationName): node is ts.LateBoundName { + if (!ts.isComputedPropertyName(node) && !ts.isElementAccessExpression(node)) { + return false; } + const expr = ts.isComputedPropertyName(node) ? node.expression : node.argumentExpression; + return ts.isEntityNameExpression(expr) + && isTypeUsableAsPropertyName(ts.isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr)); + } - /** - * Indicates whether a declaration has an early-bound name or a dynamic name that can be late-bound. - */ - function hasBindableName(node: ts.Declaration) { - return !ts.hasDynamicName(node) || hasLateBindableName(node); - } + function isLateBoundName(name: ts.__String): boolean { + return (name as string).charCodeAt(0) === ts.CharacterCodes._ && + (name as string).charCodeAt(1) === ts.CharacterCodes._ && + (name as string).charCodeAt(2) === ts.CharacterCodes.at; + } - /** - * Indicates whether a declaration name is a dynamic name that cannot be late-bound. - */ - function isNonBindableDynamicName(node: ts.DeclarationName) { - return ts.isDynamicName(node) && !isLateBindableName(node); + /** + * Indicates whether a declaration has a late-bindable dynamic name. + */ + function hasLateBindableName(node: ts.Declaration): node is ts.LateBoundDeclaration | ts.LateBoundBinaryExpressionDeclaration { + const name = ts.getNameOfDeclaration(node); + return !!name && isLateBindableName(name); + } + + /** + * Indicates whether a declaration has an early-bound name or a dynamic name that can be late-bound. + */ + function hasBindableName(node: ts.Declaration) { + return !ts.hasDynamicName(node) || hasLateBindableName(node); + } + + /** + * Indicates whether a declaration name is a dynamic name that cannot be late-bound. + */ + function isNonBindableDynamicName(node: ts.DeclarationName) { + return ts.isDynamicName(node) && !isLateBindableName(node); + } + + /** + * Gets the symbolic name for a member from its type. + */ + function getPropertyNameFromType(type: ts.StringLiteralType | ts.NumberLiteralType | ts.UniqueESSymbolType): ts.__String { + if (type.flags & ts.TypeFlags.UniqueESSymbol) { + return (type as ts.UniqueESSymbolType).escapedName; + } + if (type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { + return ts.escapeLeadingUnderscores("" + (type as ts.StringLiteralType | ts.NumberLiteralType).value); } + return ts.Debug.fail(); + } - /** - * Gets the symbolic name for a member from its type. - */ - function getPropertyNameFromType(type: ts.StringLiteralType | ts.NumberLiteralType | ts.UniqueESSymbolType): ts.__String { - if (type.flags & ts.TypeFlags.UniqueESSymbol) { - return (type as ts.UniqueESSymbolType).escapedName; - } - if (type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { - return ts.escapeLeadingUnderscores("" + (type as ts.StringLiteralType | ts.NumberLiteralType).value); + /** + * 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: ts.LateBoundDeclaration | ts.BinaryExpression, symbolFlags: ts.SymbolFlags) { + ts.Debug.assert(!!(ts.getCheckFlags(symbol) & ts.CheckFlags.Late), "Expected a late-bound symbol."); + symbol.flags |= symbolFlags; + getSymbolLinks(member.symbol).lateSymbol = symbol; + if (!symbol.declarations) { + symbol.declarations = [member]; + } + else if(!member.symbol.isReplaceableByMethod) { + symbol.declarations.push(member); + } + if (symbolFlags & ts.SymbolFlags.Value) { + if (!symbol.valueDeclaration || symbol.valueDeclaration.kind !== member.kind) { + symbol.valueDeclaration = member; } - return ts.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: ts.LateBoundDeclaration | ts.BinaryExpression, symbolFlags: ts.SymbolFlags) { - ts.Debug.assert(!!(ts.getCheckFlags(symbol) & ts.CheckFlags.Late), "Expected a late-bound symbol."); - symbol.flags |= symbolFlags; - getSymbolLinks(member.symbol).lateSymbol = symbol; - if (!symbol.declarations) { - symbol.declarations = [member]; - } - else if(!member.symbol.isReplaceableByMethod) { - symbol.declarations.push(member); - } - if (symbolFlags & ts.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: ts.Symbol, earlySymbols: ts.SymbolTable | undefined, lateSymbols: ts.UnderscoreEscapedMap, decl: ts.LateBoundDeclaration | ts.LateBoundBinaryExpressionDeclaration) { + ts.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 = ts.isBinaryExpression(decl) ? decl.left : decl.name; + const type = ts.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(ts.SymbolFlags.None, memberName, ts.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 ? ts.concatenate(earlySymbol.declarations, lateSymbol.declarations) : lateSymbol.declarations; + const name = !(type.flags & ts.TypeFlags.UniqueESSymbol) && ts.unescapeLeadingUnderscores(memberName) || ts.declarationNameToString(declName); + ts.forEach(declarations, declaration => error(ts.getNameOfDeclaration(declaration) || declaration, ts.Diagnostics.Property_0_was_also_declared_here, name)); + error(declName || decl, ts.Diagnostics.Duplicate_property_0, name); + lateSymbol = createSymbol(ts.SymbolFlags.None, memberName, ts.CheckFlags.Late); + } + lateSymbol.nameType = type; + addDeclarationToLateBoundSymbol(lateSymbol, decl, symbolFlags); + if (lateSymbol.parent) { + ts.Debug.assert(lateSymbol.parent === parent, "Existing symbol parent should match new one"); + } + else { + lateSymbol.parent = parent; } + return links.resolvedSymbol = lateSymbol; } } + return links.resolvedSymbol; + } - /** - * 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: ts.SymbolTable | undefined, lateSymbols: ts.UnderscoreEscapedMap, decl: ts.LateBoundDeclaration | ts.LateBoundBinaryExpressionDeclaration) { - ts.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 = ts.isBinaryExpression(decl) ? decl.left : decl.name; - const type = ts.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(ts.SymbolFlags.None, memberName, ts.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 ? ts.concatenate(earlySymbol.declarations, lateSymbol.declarations) : lateSymbol.declarations; - const name = !(type.flags & ts.TypeFlags.UniqueESSymbol) && ts.unescapeLeadingUnderscores(memberName) || ts.declarationNameToString(declName); - ts.forEach(declarations, declaration => error(ts.getNameOfDeclaration(declaration) || declaration, ts.Diagnostics.Property_0_was_also_declared_here, name)); - error(declName || decl, ts.Diagnostics.Duplicate_property_0, name); - lateSymbol = createSymbol(ts.SymbolFlags.None, memberName, ts.CheckFlags.Late); - } - lateSymbol.nameType = type; - addDeclarationToLateBoundSymbol(lateSymbol, decl, symbolFlags); - if (lateSymbol.parent) { - ts.Debug.assert(lateSymbol.parent === parent, "Existing symbol parent should match new one"); + function getResolvedMembersOrExportsOfSymbol(symbol: ts.Symbol, resolutionKind: MembersOrExportsResolutionKind): ts.UnderscoreEscapedMap { + const links = getSymbolLinks(symbol); + if (!links[resolutionKind]) { + const isStatic = resolutionKind === MembersOrExportsResolutionKind.resolvedExports; + const earlySymbols = !isStatic ? symbol.members : + symbol.flags & ts.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 = ts.createSymbolTable() as ts.UnderscoreEscapedMap; + for (const decl of symbol.declarations || ts.emptyArray) { + const members = ts.getMembersOfDeclaration(decl); + if (members) { + for (const member of members) { + if (isStatic === ts.hasStaticModifier(member) && hasLateBindableName(member)) { + lateBindMember(symbol, earlySymbols, lateSymbols, member); + } } - else { - lateSymbol.parent = parent; + } + } + const assignments = symbol.assignmentDeclarationMembers; + if (assignments) { + const decls = ts.arrayFrom(assignments.values()); + for (const member of decls) { + const assignmentKind = ts.getAssignmentDeclarationKind(member as ts.BinaryExpression | ts.CallExpression); + const isInstanceMember = assignmentKind === ts.AssignmentDeclarationKind.PrototypeProperty + || ts.isBinaryExpression(member) && isPossiblyAliasedThisProperty(member, assignmentKind) + || assignmentKind === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty + || assignmentKind === ts.AssignmentDeclarationKind.Prototype; // A straight `Prototype` assignment probably can never have a computed name + if (isStatic === !isInstanceMember && hasLateBindableName(member)) { + lateBindMember(symbol, earlySymbols, lateSymbols, member); } - return links.resolvedSymbol = lateSymbol; } } - return links.resolvedSymbol; + + links[resolutionKind] = combineSymbolTables(earlySymbols, lateSymbols) || emptySymbols; } - function getResolvedMembersOrExportsOfSymbol(symbol: ts.Symbol, resolutionKind: MembersOrExportsResolutionKind): ts.UnderscoreEscapedMap { - const links = getSymbolLinks(symbol); - if (!links[resolutionKind]) { - const isStatic = resolutionKind === MembersOrExportsResolutionKind.resolvedExports; - const earlySymbols = !isStatic ? symbol.members : - symbol.flags & ts.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 = ts.createSymbolTable() as ts.UnderscoreEscapedMap; - for (const decl of symbol.declarations || ts.emptyArray) { - const members = ts.getMembersOfDeclaration(decl); - if (members) { - for (const member of members) { - if (isStatic === ts.hasStaticModifier(member) && hasLateBindableName(member)) { - lateBindMember(symbol, earlySymbols, lateSymbols, member); - } - } - } - } - const assignments = symbol.assignmentDeclarationMembers; - if (assignments) { - const decls = ts.arrayFrom(assignments.values()); - for (const member of decls) { - const assignmentKind = ts.getAssignmentDeclarationKind(member as ts.BinaryExpression | ts.CallExpression); - const isInstanceMember = assignmentKind === ts.AssignmentDeclarationKind.PrototypeProperty - || ts.isBinaryExpression(member) && isPossiblyAliasedThisProperty(member, assignmentKind) - || assignmentKind === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty - || assignmentKind === ts.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]!; - } + 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 & ts.SymbolFlags.LateBindingContainer - ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedMembers) - : symbol.members || emptySymbols; - } + /** + * 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 & ts.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 & ts.SymbolFlags.ClassMember && symbol.escapedName === ts.InternalSymbolName.Computed) { - const links = getSymbolLinks(symbol); - if (!links.lateSymbol && ts.some(symbol.declarations, hasLateBindableName)) { - // force late binding of members/exports. This will set the late-bound symbol - const parent = getMergedSymbol(symbol.parent)!; - if (ts.some(symbol.declarations, ts.hasStaticModifier)) { - getExportsOfSymbol(parent); - } - else { - getMembersOfSymbol(parent); - } + /** + * 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 & ts.SymbolFlags.ClassMember && symbol.escapedName === ts.InternalSymbolName.Computed) { + const links = getSymbolLinks(symbol); + if (!links.lateSymbol && ts.some(symbol.declarations, hasLateBindableName)) { + // force late binding of members/exports. This will set the late-bound symbol + const parent = getMergedSymbol(symbol.parent)!; + if (ts.some(symbol.declarations, ts.hasStaticModifier)) { + getExportsOfSymbol(parent); + } + else { + getMembersOfSymbol(parent); } - return links.lateSymbol || (links.lateSymbol = symbol); } - return symbol; + return links.lateSymbol || (links.lateSymbol = symbol); } + return symbol; + } - function getTypeWithThisArgument(type: ts.Type, thisArgument?: ts.Type, needApparentType?: boolean): ts.Type { - if (ts.getObjectFlags(type) & ts.ObjectFlags.Reference) { - const target = (type as ts.TypeReference).target; - const typeArguments = getTypeArguments(type as ts.TypeReference); - if (ts.length(target.typeParameters) === ts.length(typeArguments)) { - const ref = createTypeReference(target, ts.concatenate(typeArguments, [thisArgument || target.thisType!])); - return needApparentType ? getApparentType(ref) : ref; - } - } - else if (type.flags & ts.TypeFlags.Intersection) { - const types = ts.sameMap((type as ts.IntersectionType).types, t => getTypeWithThisArgument(t, thisArgument, needApparentType)); - return types !== (type as ts.IntersectionType).types ? getIntersectionType(types) : type; + function getTypeWithThisArgument(type: ts.Type, thisArgument?: ts.Type, needApparentType?: boolean): ts.Type { + if (ts.getObjectFlags(type) & ts.ObjectFlags.Reference) { + const target = (type as ts.TypeReference).target; + const typeArguments = getTypeArguments(type as ts.TypeReference); + if (ts.length(target.typeParameters) === ts.length(typeArguments)) { + const ref = createTypeReference(target, ts.concatenate(typeArguments, [thisArgument || target.thisType!])); + return needApparentType ? getApparentType(ref) : ref; } - return needApparentType ? getApparentType(type) : type; } + else if (type.flags & ts.TypeFlags.Intersection) { + const types = ts.sameMap((type as ts.IntersectionType).types, t => getTypeWithThisArgument(t, thisArgument, needApparentType)); + return types !== (type as ts.IntersectionType).types ? getIntersectionType(types) : type; + } + return needApparentType ? getApparentType(type) : type; + } - function resolveObjectTypeMembers(type: ts.ObjectType, source: ts.InterfaceTypeWithDeclaredMembers, typeParameters: readonly ts.TypeParameter[], typeArguments: readonly ts.Type[]) { - let mapper: ts.TypeMapper | undefined; - let members: ts.SymbolTable; - let callSignatures: readonly ts.Signature[]; - let constructSignatures: readonly ts.Signature[]; - let indexInfos: readonly ts.IndexInfo[]; - if (ts.rangeEquals(typeParameters, typeArguments, 0, typeParameters.length)) { - members = source.symbol ? getMembersOfSymbol(source.symbol) : ts.createSymbolTable(source.declaredProperties); - callSignatures = source.declaredCallSignatures; - constructSignatures = source.declaredConstructSignatures; - indexInfos = source.declaredIndexInfos; - } - else { - mapper = createTypeMapper(typeParameters, typeArguments); - members = createInstantiatedSymbolTable(source.declaredProperties, mapper, /*mappingThisOnly*/ typeParameters.length === 1); - callSignatures = instantiateSignatures(source.declaredCallSignatures, mapper); - constructSignatures = instantiateSignatures(source.declaredConstructSignatures, mapper); - indexInfos = instantiateIndexInfos(source.declaredIndexInfos, mapper); - } - const baseTypes = getBaseTypes(source); - if (baseTypes.length) { - if (source.symbol && members === getMembersOfSymbol(source.symbol)) { - members = ts.createSymbolTable(source.declaredProperties); - } - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); - const thisArgument = ts.lastOrUndefined(typeArguments); - for (const baseType of baseTypes) { - const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(instantiateType(baseType, mapper), thisArgument) : baseType; - addInheritedMembers(members, getPropertiesOfType(instantiatedBaseType)); - callSignatures = ts.concatenate(callSignatures, getSignaturesOfType(instantiatedBaseType, ts.SignatureKind.Call)); - constructSignatures = ts.concatenate(constructSignatures, getSignaturesOfType(instantiatedBaseType, ts.SignatureKind.Construct)); - const inheritedIndexInfos = instantiatedBaseType !== anyType ? getIndexInfosOfType(instantiatedBaseType) : [createIndexInfo(stringType, anyType, /*isReadonly*/ false)]; - indexInfos = ts.concatenate(indexInfos, ts.filter(inheritedIndexInfos, info => !findIndexInfo(indexInfos, info.keyType))); - } + function resolveObjectTypeMembers(type: ts.ObjectType, source: ts.InterfaceTypeWithDeclaredMembers, typeParameters: readonly ts.TypeParameter[], typeArguments: readonly ts.Type[]) { + let mapper: ts.TypeMapper | undefined; + let members: ts.SymbolTable; + let callSignatures: readonly ts.Signature[]; + let constructSignatures: readonly ts.Signature[]; + let indexInfos: readonly ts.IndexInfo[]; + if (ts.rangeEquals(typeParameters, typeArguments, 0, typeParameters.length)) { + members = source.symbol ? getMembersOfSymbol(source.symbol) : ts.createSymbolTable(source.declaredProperties); + callSignatures = source.declaredCallSignatures; + constructSignatures = source.declaredConstructSignatures; + indexInfos = source.declaredIndexInfos; + } + else { + mapper = createTypeMapper(typeParameters, typeArguments); + members = createInstantiatedSymbolTable(source.declaredProperties, mapper, /*mappingThisOnly*/ typeParameters.length === 1); + callSignatures = instantiateSignatures(source.declaredCallSignatures, mapper); + constructSignatures = instantiateSignatures(source.declaredConstructSignatures, mapper); + indexInfos = instantiateIndexInfos(source.declaredIndexInfos, mapper); + } + const baseTypes = getBaseTypes(source); + if (baseTypes.length) { + if (source.symbol && members === getMembersOfSymbol(source.symbol)) { + members = ts.createSymbolTable(source.declaredProperties); } setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); + const thisArgument = ts.lastOrUndefined(typeArguments); + for (const baseType of baseTypes) { + const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(instantiateType(baseType, mapper), thisArgument) : baseType; + addInheritedMembers(members, getPropertiesOfType(instantiatedBaseType)); + callSignatures = ts.concatenate(callSignatures, getSignaturesOfType(instantiatedBaseType, ts.SignatureKind.Call)); + constructSignatures = ts.concatenate(constructSignatures, getSignaturesOfType(instantiatedBaseType, ts.SignatureKind.Construct)); + const inheritedIndexInfos = instantiatedBaseType !== anyType ? getIndexInfosOfType(instantiatedBaseType) : [createIndexInfo(stringType, anyType, /*isReadonly*/ false)]; + indexInfos = ts.concatenate(indexInfos, ts.filter(inheritedIndexInfos, info => !findIndexInfo(indexInfos, info.keyType))); + } } + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); + } - function resolveClassOrInterfaceMembers(type: ts.InterfaceType): void { - resolveObjectTypeMembers(type, resolveDeclaredMembers(type), ts.emptyArray, ts.emptyArray); - } + function resolveClassOrInterfaceMembers(type: ts.InterfaceType): void { + resolveObjectTypeMembers(type, resolveDeclaredMembers(type), ts.emptyArray, ts.emptyArray); + } - function resolveTypeReferenceMembers(type: ts.TypeReference): void { - const source = resolveDeclaredMembers(type.target); - const typeParameters = ts.concatenate(source.typeParameters!, [source.thisType!]); - const typeArguments = getTypeArguments(type); - const paddedTypeArguments = typeArguments.length === typeParameters.length ? typeArguments : ts.concatenate(typeArguments, [type]); - resolveObjectTypeMembers(type, source, typeParameters, paddedTypeArguments); - } - - function createSignature(declaration: ts.SignatureDeclaration | ts.JSDocSignature | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, thisParameter: ts.Symbol | undefined, parameters: readonly ts.Symbol[], resolvedReturnType: ts.Type | undefined, resolvedTypePredicate: ts.TypePredicate | undefined, minArgumentCount: number, flags: ts.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.resolvedMinArgumentCount = undefined; - sig.target = undefined; - sig.mapper = undefined; - sig.compositeSignatures = undefined; - sig.compositeKind = 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 & ts.SignatureFlags.PropagatingFlags); - result.target = sig.target; - result.mapper = sig.mapper; - result.compositeSignatures = sig.compositeSignatures; - result.compositeKind = sig.compositeKind; - return result; - } + function resolveTypeReferenceMembers(type: ts.TypeReference): void { + const source = resolveDeclaredMembers(type.target); + const typeParameters = ts.concatenate(source.typeParameters!, [source.thisType!]); + const typeArguments = getTypeArguments(type); + const paddedTypeArguments = typeArguments.length === typeParameters.length ? typeArguments : ts.concatenate(typeArguments, [type]); + resolveObjectTypeMembers(type, source, typeParameters, paddedTypeArguments); + } - function createUnionSignature(signature: ts.Signature, unionSignatures: ts.Signature[]) { - const result = cloneSignature(signature); - result.compositeSignatures = unionSignatures; - result.compositeKind = ts.TypeFlags.Union; - result.target = undefined; - result.mapper = undefined; - return result; - } + function createSignature(declaration: ts.SignatureDeclaration | ts.JSDocSignature | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, thisParameter: ts.Symbol | undefined, parameters: readonly ts.Symbol[], resolvedReturnType: ts.Type | undefined, resolvedTypePredicate: ts.TypePredicate | undefined, minArgumentCount: number, flags: ts.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.resolvedMinArgumentCount = undefined; + sig.target = undefined; + sig.mapper = undefined; + sig.compositeSignatures = undefined; + sig.compositeKind = undefined; + return sig; + } - function getOptionalCallSignature(signature: ts.Signature, callChainFlags: ts.SignatureFlags): ts.Signature { - if ((signature.flags & ts.SignatureFlags.CallChainFlags) === callChainFlags) { - return signature; - } - if (!signature.optionalCallSignatureCache) { - signature.optionalCallSignatureCache = {}; - } - const key = callChainFlags === ts.SignatureFlags.IsInnerCallChain ? "inner" : "outer"; - return signature.optionalCallSignatureCache[key] - || (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags)); - } + 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 & ts.SignatureFlags.PropagatingFlags); + result.target = sig.target; + result.mapper = sig.mapper; + result.compositeSignatures = sig.compositeSignatures; + result.compositeKind = sig.compositeKind; + return result; + } - function createOptionalCallSignature(signature: ts.Signature, callChainFlags: ts.SignatureFlags) { - ts.Debug.assert(callChainFlags === ts.SignatureFlags.IsInnerCallChain || callChainFlags === ts.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 createUnionSignature(signature: ts.Signature, unionSignatures: ts.Signature[]) { + const result = cloneSignature(signature); + result.compositeSignatures = unionSignatures; + result.compositeKind = ts.TypeFlags.Union; + result.target = undefined; + result.mapper = undefined; + return result; + } - function getExpandedParameters(sig: ts.Signature, skipUnionExpanding?: boolean): readonly (readonly ts.Symbol[])[] { - if (signatureHasRestParameter(sig)) { - const restIndex = sig.parameters.length - 1; - const restType = getTypeOfSymbol(sig.parameters[restIndex]); - if (isTupleType(restType)) { - return [expandSignatureParametersWithTupleMembers(restType, restIndex)]; - } - else if (!skipUnionExpanding && restType.flags & ts.TypeFlags.Union && ts.every((restType as ts.UnionType).types, isTupleType)) { - return ts.map((restType as ts.UnionType).types, t => expandSignatureParametersWithTupleMembers(t as ts.TupleTypeReference, restIndex)); - } - } - return [sig.parameters]; - - function expandSignatureParametersWithTupleMembers(restType: ts.TupleTypeReference, restIndex: number) { - const elementTypes = getTypeArguments(restType); - const associatedNames = restType.target.labeledElementDeclarations; - const restParams = ts.map(elementTypes, (t, i) => { - // Lookup the label from the individual tuple passed in before falling back to the signature `rest` parameter name - const tupleLabelName = !!associatedNames && getTupleElementLabel(associatedNames[i]); - const name = tupleLabelName || getParameterNameAtPosition(sig, restIndex + i, restType); - const flags = restType.target.elementFlags[i]; - const checkFlags = flags & ts.ElementFlags.Variable ? ts.CheckFlags.RestParameter : - flags & ts.ElementFlags.Optional ? ts.CheckFlags.OptionalParameter : 0; - const symbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, name, checkFlags); - symbol.type = flags & ts.ElementFlags.Rest ? createArrayType(t) : t; - return symbol; - }); - return ts.concatenate(sig.parameters.slice(0, restIndex), restParams); - } + function getOptionalCallSignature(signature: ts.Signature, callChainFlags: ts.SignatureFlags): ts.Signature { + if ((signature.flags & ts.SignatureFlags.CallChainFlags) === callChainFlags) { + return signature; } + if (!signature.optionalCallSignatureCache) { + signature.optionalCallSignatureCache = {}; + } + const key = callChainFlags === ts.SignatureFlags.IsInnerCallChain ? "inner" : "outer"; + return signature.optionalCallSignatureCache[key] + || (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags)); + } - function getDefaultConstructSignatures(classType: ts.InterfaceType): ts.Signature[] { - const baseConstructorType = getBaseConstructorTypeOfClass(classType); - const baseSignatures = getSignaturesOfType(baseConstructorType, ts.SignatureKind.Construct); - const declaration = ts.getClassLikeDeclarationOfSymbol(classType.symbol); - const isAbstract = !!declaration && ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Abstract); - if (baseSignatures.length === 0) { - return [createSignature(undefined, classType.localTypeParameters, undefined, ts.emptyArray, classType, /*resolvedTypePredicate*/ undefined, 0, isAbstract ? ts.SignatureFlags.Abstract : ts.SignatureFlags.None)]; - } - const baseTypeNode = getBaseTypeNodeOfClass(classType)!; - const isJavaScript = ts.isInJSFile(baseTypeNode); - const typeArguments = typeArgumentsFromTypeReferenceNode(baseTypeNode); - const typeArgCount = ts.length(typeArguments); - const result: ts.Signature[] = []; - for (const baseSig of baseSignatures) { - const minTypeArgumentCount = getMinTypeArgumentCount(baseSig.typeParameters); - const typeParamCount = ts.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; - sig.flags = isAbstract ? sig.flags | ts.SignatureFlags.Abstract : sig.flags & ~ts.SignatureFlags.Abstract; - result.push(sig); - } - } - return result; + function createOptionalCallSignature(signature: ts.Signature, callChainFlags: ts.SignatureFlags) { + ts.Debug.assert(callChainFlags === ts.SignatureFlags.IsInnerCallChain || callChainFlags === ts.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, skipUnionExpanding?: boolean): readonly (readonly ts.Symbol[])[] { + if (signatureHasRestParameter(sig)) { + const restIndex = sig.parameters.length - 1; + const restType = getTypeOfSymbol(sig.parameters[restIndex]); + if (isTupleType(restType)) { + return [expandSignatureParametersWithTupleMembers(restType, restIndex)]; + } + else if (!skipUnionExpanding && restType.flags & ts.TypeFlags.Union && ts.every((restType as ts.UnionType).types, isTupleType)) { + return ts.map((restType as ts.UnionType).types, t => expandSignatureParametersWithTupleMembers(t as ts.TupleTypeReference, restIndex)); + } + } + return [sig.parameters]; + + function expandSignatureParametersWithTupleMembers(restType: ts.TupleTypeReference, restIndex: number) { + const elementTypes = getTypeArguments(restType); + const associatedNames = restType.target.labeledElementDeclarations; + const restParams = ts.map(elementTypes, (t, i) => { + // Lookup the label from the individual tuple passed in before falling back to the signature `rest` parameter name + const tupleLabelName = !!associatedNames && getTupleElementLabel(associatedNames[i]); + const name = tupleLabelName || getParameterNameAtPosition(sig, restIndex + i, restType); + const flags = restType.target.elementFlags[i]; + const checkFlags = flags & ts.ElementFlags.Variable ? ts.CheckFlags.RestParameter : + flags & ts.ElementFlags.Optional ? ts.CheckFlags.OptionalParameter : 0; + const symbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, name, checkFlags); + symbol.type = flags & ts.ElementFlags.Rest ? createArrayType(t) : t; + return symbol; + }); + return ts.concatenate(sig.parameters.slice(0, restIndex), restParams); } + } - 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 getDefaultConstructSignatures(classType: ts.InterfaceType): ts.Signature[] { + const baseConstructorType = getBaseConstructorTypeOfClass(classType); + const baseSignatures = getSignaturesOfType(baseConstructorType, ts.SignatureKind.Construct); + const declaration = ts.getClassLikeDeclarationOfSymbol(classType.symbol); + const isAbstract = !!declaration && ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Abstract); + if (baseSignatures.length === 0) { + return [createSignature(undefined, classType.localTypeParameters, undefined, ts.emptyArray, classType, /*resolvedTypePredicate*/ undefined, 0, isAbstract ? ts.SignatureFlags.Abstract : ts.SignatureFlags.None)]; + } + const baseTypeNode = getBaseTypeNodeOfClass(classType)!; + const isJavaScript = ts.isInJSFile(baseTypeNode); + const typeArguments = typeArgumentsFromTypeReferenceNode(baseTypeNode); + const typeArgCount = ts.length(typeArguments); + const result: ts.Signature[] = []; + for (const baseSig of baseSignatures) { + const minTypeArgumentCount = getMinTypeArgumentCount(baseSig.typeParameters); + const typeParamCount = ts.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; + sig.flags = isAbstract ? sig.flags | ts.SignatureFlags.Abstract : sig.flags & ~ts.SignatureFlags.Abstract; + 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 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 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; } - 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) { + for (let i = 1; i < signatureLists.length; i++) { + if (!findMatchingSignature(signatureLists[i], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false)) { return undefined; } - result = ts.appendIfUnique(result, match); } - return result; + return [signature]; } - - // 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 ts.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 = ts.forEach(unionSignatures, sig => sig.thisParameter); - if (firstThisParameterOfUnionSignatures) { - const thisType = getIntersectionType(ts.mapDefined(unionSignatures, sig => sig.thisParameter && getTypeOfSymbol(sig.thisParameter))); - thisParameter = createSymbolWithType(firstThisParameterOfUnionSignatures, thisType); - } - s = createUnionSignature(signature, unionSignatures); - s.thisParameter = thisParameter; - } - (result || (result = [])).push(s); - } - } - } + 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; } - if (!ts.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]; - ts.Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass"); - results = !!signature.typeParameters && ts.some(results, s => !!s.typeParameters && !compareTypeParametersIdentical(signature.typeParameters, s.typeParameters)) ? undefined : ts.map(results, sig => combineSignaturesOfUnionMembers(sig, signature)); - if (!results) { - break; - } + result = ts.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 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 ts.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 = ts.forEach(unionSignatures, sig => sig.thisParameter); + if (firstThisParameterOfUnionSignatures) { + const thisType = getIntersectionType(ts.mapDefined(unionSignatures, sig => sig.thisParameter && getTypeOfSymbol(sig.thisParameter))); + thisParameter = createSymbolWithType(firstThisParameterOfUnionSignatures, thisType); + } + s = createUnionSignature(signature, unionSignatures); + s.thisParameter = thisParameter; + } + (result || (result = [])).push(s); + } + } + } + } + if (!ts.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]; + ts.Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass"); + results = !!signature.typeParameters && ts.some(results, s => !!s.typeParameters && !compareTypeParametersIdentical(signature.typeParameters, s.typeParameters)) ? undefined : ts.map(results, sig => combineSignaturesOfUnionMembers(sig, signature)); + if (!results) { + break; } } - result = results; } - return result || ts.emptyArray; + result = results; + } + return result || ts.emptyArray; + } + + function compareTypeParametersIdentical(sourceParams: readonly ts.TypeParameter[] | undefined, targetParams: readonly ts.TypeParameter[] | undefined): boolean { + if (ts.length(sourceParams) !== ts.length(targetParams)) { + return false; + } + if (!sourceParams || !targetParams) { + return true; } - function compareTypeParametersIdentical(sourceParams: readonly ts.TypeParameter[] | undefined, targetParams: readonly ts.TypeParameter[] | undefined): boolean { - if (ts.length(sourceParams) !== ts.length(targetParams)) { + const mapper = createTypeMapper(targetParams, sourceParams); + for (let i = 0; i < sourceParams.length; i++) { + const source = sourceParams[i]; + const target = targetParams[i]; + if (source === target) + continue; + // We instantiate the target type parameter constraints into the source types so we can recognize `` as the same as `` + if (!isTypeIdenticalTo(getConstraintFromTypeParameter(source) || unknownType, instantiateType(getConstraintFromTypeParameter(target) || unknownType, mapper))) return false; - } - if (!sourceParams || !targetParams) { - return true; - } + // We don't compare defaults - we just use the type parameter defaults from the first signature that seems to match. + // It might make sense to combine these defaults in the future, but doing so intelligently requires knowing + // if the parameter is used covariantly or contravariantly (so we intersect if it's used like a parameter or union if used like a return type) + // and, since it's just an inference _default_, just picking one arbitrarily works OK. + } - const mapper = createTypeMapper(targetParams, sourceParams); - for (let i = 0; i < sourceParams.length; i++) { - const source = sourceParams[i]; - const target = targetParams[i]; - if (source === target) - continue; - // We instantiate the target type parameter constraints into the source types so we can recognize `` as the same as `` - if (!isTypeIdenticalTo(getConstraintFromTypeParameter(source) || unknownType, instantiateType(getConstraintFromTypeParameter(target) || unknownType, mapper))) - return false; - // We don't compare defaults - we just use the type parameter defaults from the first signature that seems to match. - // It might make sense to combine these defaults in the future, but doing so intelligently requires knowing - // if the parameter is used covariantly or contravariantly (so we intersect if it's used like a parameter or union if used like a return type) - // and, since it's just an inference _default_, just picking one arbitrarily works OK. - } + return true; + } - return true; + function combineUnionThisParam(left: ts.Symbol | undefined, right: ts.Symbol | undefined, mapper: ts.TypeMapper | 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), instantiateType(getTypeOfSymbol(right), mapper)]); + return createSymbolWithType(left, thisType); + } - function combineUnionThisParam(left: ts.Symbol | undefined, right: ts.Symbol | undefined, mapper: ts.TypeMapper | 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), instantiateType(getTypeOfSymbol(right), mapper)]); - return createSymbolWithType(left, thisType); - } - - function combineUnionParameters(left: ts.Signature, right: ts.Signature, mapper: ts.TypeMapper | undefined) { - 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++) { - let longestParamType = tryGetTypeAtPosition(longest, i)!; - if (longest === right) { - longestParamType = instantiateType(longestParamType, mapper); - } - let shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; - if (shorter === right) { - shorterParamType = instantiateType(shorterParamType, mapper); - } - 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(ts.SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? ts.SymbolFlags.Optional : 0), paramName || `arg${i}` as ts.__String); - paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; - params[i] = paramSymbol; - } - if (needsExtraRestElement) { - const restParamSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "args" as ts.__String); - restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); - if (shorter === right) { - restParamSymbol.type = instantiateType(restParamSymbol.type, mapper); - } - params[longestCount] = restParamSymbol; - } - return params; - } - - function combineSignaturesOfUnionMembers(left: ts.Signature, right: ts.Signature): ts.Signature { - const typeParams = left.typeParameters || right.typeParameters; - let paramMapper: ts.TypeMapper | undefined; - if (left.typeParameters && right.typeParameters) { - paramMapper = createTypeMapper(right.typeParameters, left.typeParameters); - // We just use the type parameter defaults from the first signature - } - const declaration = left.declaration; - const params = combineUnionParameters(left, right, paramMapper); - const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter, paramMapper); - const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); - const result = createSignature(declaration, typeParams, thisParam, params, - /*resolvedReturnType*/ undefined, - /*resolvedTypePredicate*/ undefined, minArgCount, (left.flags | right.flags) & ts.SignatureFlags.PropagatingFlags); - result.compositeKind = ts.TypeFlags.Union; - result.compositeSignatures = ts.concatenate(left.compositeKind !== ts.TypeFlags.Intersection && left.compositeSignatures || [left], [right]); - if (paramMapper) { - result.mapper = left.compositeKind !== ts.TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; + function combineUnionParameters(left: ts.Signature, right: ts.Signature, mapper: ts.TypeMapper | undefined) { + 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++) { + let longestParamType = tryGetTypeAtPosition(longest, i)!; + if (longest === right) { + longestParamType = instantiateType(longestParamType, mapper); + } + let shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; + if (shorter === right) { + shorterParamType = instantiateType(shorterParamType, mapper); + } + 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(ts.SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? ts.SymbolFlags.Optional : 0), paramName || `arg${i}` as ts.__String); + paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; + params[i] = paramSymbol; + } + if (needsExtraRestElement) { + const restParamSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "args" as ts.__String); + restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); + if (shorter === right) { + restParamSymbol.type = instantiateType(restParamSymbol.type, mapper); } - return result; + params[longestCount] = restParamSymbol; } + return params; + } - function getUnionIndexInfos(types: readonly ts.Type[]): ts.IndexInfo[] { - const sourceInfos = getIndexInfosOfType(types[0]); - if (sourceInfos) { - const result = []; - for (const info of sourceInfos) { - const indexType = info.keyType; - if (ts.every(types, t => !!getIndexInfoOfType(t, indexType))) { - result.push(createIndexInfo(indexType, getUnionType(ts.map(types, t => getIndexTypeOfType(t, indexType)!)), ts.some(types, t => getIndexInfoOfType(t, indexType)!.isReadonly))); - } + function combineSignaturesOfUnionMembers(left: ts.Signature, right: ts.Signature): ts.Signature { + const typeParams = left.typeParameters || right.typeParameters; + let paramMapper: ts.TypeMapper | undefined; + if (left.typeParameters && right.typeParameters) { + paramMapper = createTypeMapper(right.typeParameters, left.typeParameters); + // We just use the type parameter defaults from the first signature + } + const declaration = left.declaration; + const params = combineUnionParameters(left, right, paramMapper); + const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter, paramMapper); + const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); + const result = createSignature(declaration, typeParams, thisParam, params, + /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, minArgCount, (left.flags | right.flags) & ts.SignatureFlags.PropagatingFlags); + result.compositeKind = ts.TypeFlags.Union; + result.compositeSignatures = ts.concatenate(left.compositeKind !== ts.TypeFlags.Intersection && left.compositeSignatures || [left], [right]); + if (paramMapper) { + result.mapper = left.compositeKind !== ts.TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; + } + return result; + } + + function getUnionIndexInfos(types: readonly ts.Type[]): ts.IndexInfo[] { + const sourceInfos = getIndexInfosOfType(types[0]); + if (sourceInfos) { + const result = []; + for (const info of sourceInfos) { + const indexType = info.keyType; + if (ts.every(types, t => !!getIndexInfoOfType(t, indexType))) { + result.push(createIndexInfo(indexType, getUnionType(ts.map(types, t => getIndexTypeOfType(t, indexType)!)), ts.some(types, t => getIndexInfoOfType(t, indexType)!.isReadonly))); } - return result; } - return ts.emptyArray; + return result; } + return ts.emptyArray; + } - function resolveUnionTypeMembers(type: ts.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(ts.map(type.types, t => t === globalFunctionType ? [unknownSignature] : getSignaturesOfType(t, ts.SignatureKind.Call))); - const constructSignatures = getUnionSignatures(ts.map(type.types, t => getSignaturesOfType(t, ts.SignatureKind.Construct))); - const indexInfos = getUnionIndexInfos(type.types); - setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, indexInfos); - } + function resolveUnionTypeMembers(type: ts.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(ts.map(type.types, t => t === globalFunctionType ? [unknownSignature] : getSignaturesOfType(t, ts.SignatureKind.Call))); + const constructSignatures = getUnionSignatures(ts.map(type.types, t => getSignaturesOfType(t, ts.SignatureKind.Construct))); + const indexInfos = getUnionIndexInfos(type.types); + setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, indexInfos); + } + + 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 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 findMixins(types: readonly ts.Type[]): readonly boolean[] { + const constructorTypeCount = ts.countWhere(types, (t) => getSignaturesOfType(t, ts.SignatureKind.Construct).length > 0); + const mixinFlags = ts.map(types, isMixinConstructorType); + if (constructorTypeCount > 0 && constructorTypeCount === ts.countWhere(mixinFlags, (b) => b)) { + const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true); + mixinFlags[firstMixinIndex] = false; } + return mixinFlags; + } - function findMixins(types: readonly ts.Type[]): readonly boolean[] { - const constructorTypeCount = ts.countWhere(types, (t) => getSignaturesOfType(t, ts.SignatureKind.Construct).length > 0); - const mixinFlags = ts.map(types, isMixinConstructorType); - if (constructorTypeCount > 0 && constructorTypeCount === ts.countWhere(mixinFlags, (b) => b)) { - const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true); - mixinFlags[firstMixinIndex] = false; + 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], ts.SignatureKind.Construct)[0])); } - return mixinFlags; } + return getIntersectionType(mixedTypes); + } - 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], ts.SignatureKind.Construct)[0])); + function resolveIntersectionTypeMembers(type: ts.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 indexInfos: ts.IndexInfo[] | undefined; + const types = type.types; + const mixinFlags = findMixins(types); + const mixinCount = ts.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, ts.SignatureKind.Construct); + if (signatures.length && mixinCount > 0) { + signatures = ts.map(signatures, s => { + const clone = cloneSignature(s); + clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i); + return clone; + }); } + constructSignatures = appendSignatures(constructSignatures, signatures); } - return getIntersectionType(mixedTypes); + callSignatures = appendSignatures(callSignatures, getSignaturesOfType(t, ts.SignatureKind.Call)); + indexInfos = ts.reduceLeft(getIndexInfosOfType(t), (infos, newInfo) => appendIndexInfo(infos, newInfo, /*union*/ false), indexInfos); } + setStructuredTypeMembers(type, emptySymbols, callSignatures || ts.emptyArray, constructSignatures || ts.emptyArray, indexInfos || ts.emptyArray); + } - function resolveIntersectionTypeMembers(type: ts.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 indexInfos: ts.IndexInfo[] | undefined; - const types = type.types; - const mixinFlags = findMixins(types); - const mixinCount = ts.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, ts.SignatureKind.Construct); - if (signatures.length && mixinCount > 0) { - signatures = ts.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, ts.SignatureKind.Call)); - indexInfos = ts.reduceLeft(getIndexInfosOfType(t), (infos, newInfo) => appendIndexInfo(infos, newInfo, /*union*/ false), indexInfos); + function appendSignatures(signatures: ts.Signature[] | undefined, newSignatures: readonly ts.Signature[]) { + for (const sig of newSignatures) { + if (!signatures || ts.every(signatures, s => !compareSignaturesIdentical(s, sig, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, compareTypesIdentical))) { + signatures = ts.append(signatures, sig); } - setStructuredTypeMembers(type, emptySymbols, callSignatures || ts.emptyArray, constructSignatures || ts.emptyArray, indexInfos || ts.emptyArray); } + return signatures; + } - function appendSignatures(signatures: ts.Signature[] | undefined, newSignatures: readonly ts.Signature[]) { - for (const sig of newSignatures) { - if (!signatures || ts.every(signatures, s => !compareSignaturesIdentical(s, sig, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, compareTypesIdentical))) { - signatures = ts.append(signatures, sig); + function appendIndexInfo(indexInfos: ts.IndexInfo[] | undefined, newInfo: ts.IndexInfo, union: boolean) { + if (indexInfos) { + for (let i = 0; i < indexInfos.length; i++) { + const info = indexInfos[i]; + if (info.keyType === newInfo.keyType) { + indexInfos[i] = createIndexInfo(info.keyType, union ? getUnionType([info.type, newInfo.type]) : getIntersectionType([info.type, newInfo.type]), union ? info.isReadonly || newInfo.isReadonly : info.isReadonly && newInfo.isReadonly); + return indexInfos; } } - return signatures; } + return ts.append(indexInfos, newInfo); + } - function appendIndexInfo(indexInfos: ts.IndexInfo[] | undefined, newInfo: ts.IndexInfo, union: boolean) { - if (indexInfos) { - for (let i = 0; i < indexInfos.length; i++) { - const info = indexInfos[i]; - if (info.keyType === newInfo.keyType) { - indexInfos[i] = createIndexInfo(info.keyType, union ? getUnionType([info.type, newInfo.type]) : getIntersectionType([info.type, newInfo.type]), union ? info.isReadonly || newInfo.isReadonly : info.isReadonly && newInfo.isReadonly); - return indexInfos; + /** + * Converts an AnonymousType to a ResolvedType. + */ + function resolveAnonymousTypeMembers(type: ts.AnonymousType) { + if (type.target) { + setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const members = createInstantiatedSymbolTable(getPropertiesOfObjectType(type.target), type.mapper!, /*mappingThisOnly*/ false); + const callSignatures = instantiateSignatures(getSignaturesOfType(type.target, ts.SignatureKind.Call), type.mapper!); + const constructSignatures = instantiateSignatures(getSignaturesOfType(type.target, ts.SignatureKind.Construct), type.mapper!); + const indexInfos = instantiateIndexInfos(getIndexInfosOfType(type.target), type.mapper!); + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); + return; + } + const symbol = getMergedSymbol(type.symbol); + if (symbol.flags & ts.SymbolFlags.TypeLiteral) { + setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const members = getMembersOfSymbol(symbol); + const callSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.Call)); + const constructSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.New)); + const indexInfos = getIndexInfosOfSymbol(symbol); + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); + return; + } + // Combinations of function, class, enum and module + let members = emptySymbols; + let indexInfos: ts.IndexInfo[] | undefined; + if (symbol.exports) { + members = getExportsOfSymbol(symbol); + if (symbol === globalThisSymbol) { + const varsOnly = new ts.Map() as ts.SymbolTable; + members.forEach(p => { + if (!(p.flags & ts.SymbolFlags.BlockScoped) && !(p.flags & ts.SymbolFlags.ValueModule && p.declarations?.length && ts.every(p.declarations, ts.isAmbientModule))) { + varsOnly.set(p.escapedName, p); } - } + }); + members = varsOnly; } - return ts.append(indexInfos, newInfo); } - - /** - * Converts an AnonymousType to a ResolvedType. - */ - function resolveAnonymousTypeMembers(type: ts.AnonymousType) { - if (type.target) { - setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const members = createInstantiatedSymbolTable(getPropertiesOfObjectType(type.target), type.mapper!, /*mappingThisOnly*/ false); - const callSignatures = instantiateSignatures(getSignaturesOfType(type.target, ts.SignatureKind.Call), type.mapper!); - const constructSignatures = instantiateSignatures(getSignaturesOfType(type.target, ts.SignatureKind.Construct), type.mapper!); - const indexInfos = instantiateIndexInfos(getIndexInfosOfType(type.target), type.mapper!); - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); - return; - } - const symbol = getMergedSymbol(type.symbol); - if (symbol.flags & ts.SymbolFlags.TypeLiteral) { - setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - const members = getMembersOfSymbol(symbol); - const callSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.Call)); - const constructSignatures = getSignaturesOfSymbol(members.get(ts.InternalSymbolName.New)); - const indexInfos = getIndexInfosOfSymbol(symbol); - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, indexInfos); - return; - } - // Combinations of function, class, enum and module - let members = emptySymbols; - let indexInfos: ts.IndexInfo[] | undefined; - if (symbol.exports) { - members = getExportsOfSymbol(symbol); - if (symbol === globalThisSymbol) { - const varsOnly = new ts.Map() as ts.SymbolTable; - members.forEach(p => { - if (!(p.flags & ts.SymbolFlags.BlockScoped) && !(p.flags & ts.SymbolFlags.ValueModule && p.declarations?.length && ts.every(p.declarations, ts.isAmbientModule))) { - varsOnly.set(p.escapedName, p); - } - }); - members = varsOnly; - } - } - let baseConstructorIndexInfo: ts.IndexInfo | undefined; - setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); - if (symbol.flags & ts.SymbolFlags.Class) { - const classType = getDeclaredTypeOfClassOrInterface(symbol); - const baseConstructorType = getBaseConstructorTypeOfClass(classType); - if (baseConstructorType.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.TypeVariable)) { - members = ts.createSymbolTable(getNamedOrIndexSignatureMembers(members)); - addInheritedMembers(members, getPropertiesOfType(baseConstructorType)); - } - else if (baseConstructorType === anyType) { - baseConstructorIndexInfo = createIndexInfo(stringType, anyType, /*isReadonly*/ false); - } - } - - const indexSymbol = getIndexSymbolFromSymbolTable(members); - if (indexSymbol) { - indexInfos = getIndexInfosOfIndexSymbol(indexSymbol); - } - else { - if (baseConstructorIndexInfo) { - indexInfos = ts.append(indexInfos, baseConstructorIndexInfo); - } - if (symbol.flags & ts.SymbolFlags.Enum && (getDeclaredTypeOfSymbol(symbol).flags & ts.TypeFlags.Enum || - ts.some(type.properties, prop => !!(getTypeOfSymbol(prop).flags & ts.TypeFlags.NumberLike)))) { - indexInfos = ts.append(indexInfos, enumNumberIndexInfo); - } - } - setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos || ts.emptyArray); - // 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) { - type.callSignatures = getSignaturesOfSymbol(symbol); + let baseConstructorIndexInfo: ts.IndexInfo | undefined; + setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); + if (symbol.flags & ts.SymbolFlags.Class) { + const classType = getDeclaredTypeOfClassOrInterface(symbol); + const baseConstructorType = getBaseConstructorTypeOfClass(classType); + if (baseConstructorType.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.TypeVariable)) { + members = ts.createSymbolTable(getNamedOrIndexSignatureMembers(members)); + addInheritedMembers(members, getPropertiesOfType(baseConstructorType)); } - // And likewise for construct signatures for classes - if (symbol.flags & ts.SymbolFlags.Class) { - const classType = getDeclaredTypeOfClassOrInterface(symbol); - let constructSignatures = symbol.members ? getSignaturesOfSymbol(symbol.members.get(ts.InternalSymbolName.Constructor)) : ts.emptyArray; - if (symbol.flags & ts.SymbolFlags.Function) { - constructSignatures = ts.addRange(constructSignatures.slice(), ts.mapDefined(type.callSignatures, sig => isJSConstructor(sig.declaration) ? - createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, classType, /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & ts.SignatureFlags.PropagatingFlags) : - undefined)); - } - if (!constructSignatures.length) { - constructSignatures = getDefaultConstructSignatures(classType); - } - type.constructSignatures = constructSignatures; + else if (baseConstructorType === anyType) { + baseConstructorIndexInfo = createIndexInfo(stringType, anyType, /*isReadonly*/ false); } } - type ReplaceableIndexedAccessType = ts.IndexedAccessType & { - objectType: ts.TypeParameter; - indexType: ts.TypeParameter; - }; - function replaceIndexedAccess(instantiable: ts.Type, type: ReplaceableIndexedAccessType, replacement: ts.Type) { - // map type.indexType to 0 - // map type.objectType to `[TReplacement]` - // thus making the indexed access `[TReplacement][0]` or `TReplacement` - return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])])); - } - - function resolveReverseMappedTypeMembers(type: ts.ReverseMappedType) { - const indexInfo = getIndexInfoOfType(type.source, stringType); - const modifiers = getMappedTypeModifiers(type.mappedType); - const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; - const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : ts.SymbolFlags.Optional; - const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : ts.emptyArray; - const members = ts.createSymbolTable(); - for (const prop of getPropertiesOfType(type.source)) { - const checkFlags = ts.CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? ts.CheckFlags.Readonly : 0); - const inferredProp = createSymbol(ts.SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ts.ReverseMappedSymbol; - inferredProp.declarations = prop.declarations; - inferredProp.nameType = getSymbolLinks(prop).nameType; - inferredProp.propertyType = getTypeOfSymbol(prop); - if (type.constraintType.type.flags & ts.TypeFlags.IndexedAccess - && (type.constraintType.type as ts.IndexedAccessType).objectType.flags & ts.TypeFlags.TypeParameter - && (type.constraintType.type as ts.IndexedAccessType).indexType.flags & ts.TypeFlags.TypeParameter) { - // A reverse mapping of `{[K in keyof T[K_1]]: T[K_1]}` is the same as that of `{[K in keyof T]: T}`, since all we care about is - // inferring to the "type parameter" (or indexed access) shared by the constraint and template. So, to reduce the number of - // type identities produced, we simplify such indexed access occurences - const newTypeParam = (type.constraintType.type as ts.IndexedAccessType).objectType; - const newMappedType = replaceIndexedAccess(type.mappedType, type.constraintType.type as ReplaceableIndexedAccessType, newTypeParam); - inferredProp.mappedType = newMappedType as ts.MappedType; - inferredProp.constraintType = getIndexType(newTypeParam) as ts.IndexType; - } - else { - inferredProp.mappedType = type.mappedType; - inferredProp.constraintType = type.constraintType; - } - members.set(prop.escapedName, inferredProp); - } - setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos); + const indexSymbol = getIndexSymbolFromSymbolTable(members); + if (indexSymbol) { + indexInfos = getIndexInfosOfIndexSymbol(indexSymbol); } - - // 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 & ts.TypeFlags.Index) { - const t = getApparentType((type as ts.IndexType).type); - return isGenericTupleType(t) ? getKnownKeysOfTupleType(t) : getIndexType(t); + else { + if (baseConstructorIndexInfo) { + indexInfos = ts.append(indexInfos, baseConstructorIndexInfo); } - if (type.flags & ts.TypeFlags.Conditional) { - if ((type as ts.ConditionalType).root.isDistributive) { - const checkType = (type as ts.ConditionalType).checkType; - const constraint = getLowerBoundOfKeyType(checkType); - if (constraint !== checkType) { - return getConditionalTypeInstantiation(type as ts.ConditionalType, prependTypeMapping((type as ts.ConditionalType).root.checkType, constraint, (type as ts.ConditionalType).mapper)); - } - } - return type; + if (symbol.flags & ts.SymbolFlags.Enum && (getDeclaredTypeOfSymbol(symbol).flags & ts.TypeFlags.Enum || + ts.some(type.properties, prop => !!(getTypeOfSymbol(prop).flags & ts.TypeFlags.NumberLike)))) { + indexInfos = ts.append(indexInfos, enumNumberIndexInfo); } - if (type.flags & ts.TypeFlags.Union) { - return mapType(type as ts.UnionType, getLowerBoundOfKeyType); + } + setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos || ts.emptyArray); + // 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 & (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) { + type.callSignatures = getSignaturesOfSymbol(symbol); + } + // And likewise for construct signatures for classes + if (symbol.flags & ts.SymbolFlags.Class) { + const classType = getDeclaredTypeOfClassOrInterface(symbol); + let constructSignatures = symbol.members ? getSignaturesOfSymbol(symbol.members.get(ts.InternalSymbolName.Constructor)) : ts.emptyArray; + if (symbol.flags & ts.SymbolFlags.Function) { + constructSignatures = ts.addRange(constructSignatures.slice(), ts.mapDefined(type.callSignatures, sig => isJSConstructor(sig.declaration) ? + createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, classType, /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & ts.SignatureFlags.PropagatingFlags) : + undefined)); } - if (type.flags & ts.TypeFlags.Intersection) { - return getIntersectionType(ts.sameMap((type as ts.UnionType).types, getLowerBoundOfKeyType)); + if (!constructSignatures.length) { + constructSignatures = getDefaultConstructSignatures(classType); } - return type; + type.constructSignatures = constructSignatures; } + } - function getIsLateCheckFlag(s: ts.Symbol): ts.CheckFlags { - return ts.getCheckFlags(s) & ts.CheckFlags.Late; - } + type ReplaceableIndexedAccessType = ts.IndexedAccessType & { + objectType: ts.TypeParameter; + indexType: ts.TypeParameter; + }; + function replaceIndexedAccess(instantiable: ts.Type, type: ReplaceableIndexedAccessType, replacement: ts.Type) { + // map type.indexType to 0 + // map type.objectType to `[TReplacement]` + // thus making the indexed access `[TReplacement][0]` or `TReplacement` + return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])])); + } - function forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(type: ts.Type, include: ts.TypeFlags, stringsOnly: boolean, cb: (keyType: ts.Type) => void) { - for (const prop of getPropertiesOfType(type)) { - cb(getLiteralTypeFromProperty(prop, include)); - } - if (type.flags & ts.TypeFlags.Any) { - cb(stringType); + function resolveReverseMappedTypeMembers(type: ts.ReverseMappedType) { + const indexInfo = getIndexInfoOfType(type.source, stringType); + const modifiers = getMappedTypeModifiers(type.mappedType); + const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; + const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : ts.SymbolFlags.Optional; + const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : ts.emptyArray; + const members = ts.createSymbolTable(); + for (const prop of getPropertiesOfType(type.source)) { + const checkFlags = ts.CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? ts.CheckFlags.Readonly : 0); + const inferredProp = createSymbol(ts.SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ts.ReverseMappedSymbol; + inferredProp.declarations = prop.declarations; + inferredProp.nameType = getSymbolLinks(prop).nameType; + inferredProp.propertyType = getTypeOfSymbol(prop); + if (type.constraintType.type.flags & ts.TypeFlags.IndexedAccess + && (type.constraintType.type as ts.IndexedAccessType).objectType.flags & ts.TypeFlags.TypeParameter + && (type.constraintType.type as ts.IndexedAccessType).indexType.flags & ts.TypeFlags.TypeParameter) { + // A reverse mapping of `{[K in keyof T[K_1]]: T[K_1]}` is the same as that of `{[K in keyof T]: T}`, since all we care about is + // inferring to the "type parameter" (or indexed access) shared by the constraint and template. So, to reduce the number of + // type identities produced, we simplify such indexed access occurences + const newTypeParam = (type.constraintType.type as ts.IndexedAccessType).objectType; + const newMappedType = replaceIndexedAccess(type.mappedType, type.constraintType.type as ReplaceableIndexedAccessType, newTypeParam); + inferredProp.mappedType = newMappedType as ts.MappedType; + inferredProp.constraintType = getIndexType(newTypeParam) as ts.IndexType; } else { - for (const info of getIndexInfosOfType(type)) { - if (!stringsOnly || info.keyType.flags & (ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral)) { - cb(info.keyType); - } - } + inferredProp.mappedType = type.mappedType; + inferredProp.constraintType = type.constraintType; } + members.set(prop.escapedName, inferredProp); } + setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos); + } - /** Resolve the members of a mapped type { [P in K]: T } */ - function resolveMappedTypeMembers(type: ts.MappedType) { - const members: ts.SymbolTable = ts.createSymbolTable(); - let indexInfos: ts.IndexInfo[] | undefined; - // Resolve upfront such that recursive references see an empty object type. - setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); - // 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 nameType = getNameTypeFromMappedType(type.target as ts.MappedType || type); - const templateType = getTemplateTypeFromMappedType(type.target as ts.MappedType || type); - const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' - const templateModifiers = getMappedTypeModifiers(type); - const include = keyofStringsOnly ? ts.TypeFlags.StringLiteral : ts.TypeFlags.StringOrNumberLiteralOrUnique; - if (isMappedTypeWithKeyofConstraintDeclaration(type)) { - // We have a { [P in keyof T]: X } - forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, include, keyofStringsOnly, addMemberForKeyType); - } - else { - forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); - } - setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos || ts.emptyArray); - function addMemberForKeyType(keyType: ts.Type) { - const propNameType = nameType ? instantiateType(nameType, appendTypeMapping(type.mapper, typeParameter, keyType)) : keyType; - forEachType(propNameType, t => addMemberForKeyTypeWorker(keyType, t)); - } - - function addMemberForKeyTypeWorker(keyType: ts.Type, propNameType: ts.Type) { - // 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(propNameType)) { - const propName = getPropertyNameFromType(propNameType); - // String enum members from separate enums with identical values - // are distinct types with the same property name. Make the resulting - // property symbol's name type be the union of those enum member types. - const existingProp = members.get(propName) as ts.MappedSymbol | undefined; - if (existingProp) { - existingProp.nameType = getUnionType([existingProp.nameType!, propNameType]); - existingProp.keyType = getUnionType([existingProp.keyType, keyType]); - } - else { - const modifiersProp = isTypeUsableAsPropertyName(keyType) ? getPropertyOfType(modifiersType, getPropertyNameFromType(keyType)) : undefined; - const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || - !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & ts.SymbolFlags.Optional); - const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || - !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); - const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & ts.SymbolFlags.Optional; - const lateFlag: ts.CheckFlags = modifiersProp ? getIsLateCheckFlag(modifiersProp) : 0; - const prop = createSymbol(ts.SymbolFlags.Property | (isOptional ? ts.SymbolFlags.Optional : 0), propName, lateFlag | ts.CheckFlags.Mapped | (isReadonly ? ts.CheckFlags.Readonly : 0) | (stripOptional ? ts.CheckFlags.StripOptional : 0)) as ts.MappedSymbol; - prop.mappedType = type; - prop.nameType = propNameType; - prop.keyType = keyType; - if (modifiersProp) { - prop.syntheticOrigin = modifiersProp; - // If the mapped type has an `as XXX` clause, the property name likely won't match the declaration name and - // multiple properties may map to the same name. Thus, we attach no declarations to the symbol. - prop.declarations = nameType ? undefined : modifiersProp.declarations; - } - members.set(propName, prop); - } - } - else if (isValidIndexKeyType(propNameType) || propNameType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Enum)) { - const indexKeyType = propNameType.flags & (ts.TypeFlags.Any | ts.TypeFlags.String) ? stringType : - propNameType.flags & (ts.TypeFlags.Number | ts.TypeFlags.Enum) ? numberType : - propNameType; - const propType = instantiateType(templateType, appendTypeMapping(type.mapper, typeParameter, keyType)); - const indexInfo = createIndexInfo(indexKeyType, propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); - indexInfos = appendIndexInfo(indexInfos, indexInfo, /*union*/ true); - } - } - } - - function getTypeOfMappedSymbol(symbol: ts.MappedSymbol) { - if (!symbol.type) { - const mappedType = symbol.mappedType; - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - mappedType.containsError = true; - return errorType; - } - const templateType = getTemplateTypeFromMappedType(mappedType.target as ts.MappedType || mappedType); - const mapper = appendTypeMapping(mappedType.mapper, getTypeParameterFromMappedType(mappedType), symbol.keyType); - const propType = instantiateType(templateType, mapper); - // 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. - let type = strictNullChecks && symbol.flags & ts.SymbolFlags.Optional && !maybeTypeOfKind(propType, ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? getOptionalType(propType, /*isProperty*/ true) : - symbol.checkFlags & ts.CheckFlags.StripOptional ? removeMissingOrUndefinedType(propType) : - propType; - if (!popTypeResolution()) { - error(currentNode, ts.Diagnostics.Type_of_property_0_circularly_references_itself_in_mapped_type_1, symbolToString(symbol), typeToString(mappedType)); - type = errorType; + // 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 & ts.TypeFlags.Index) { + const t = getApparentType((type as ts.IndexType).type); + return isGenericTupleType(t) ? getKnownKeysOfTupleType(t) : getIndexType(t); + } + if (type.flags & ts.TypeFlags.Conditional) { + if ((type as ts.ConditionalType).root.isDistributive) { + const checkType = (type as ts.ConditionalType).checkType; + const constraint = getLowerBoundOfKeyType(checkType); + if (constraint !== checkType) { + return getConditionalTypeInstantiation(type as ts.ConditionalType, prependTypeMapping((type as ts.ConditionalType).root.checkType, constraint, (type as ts.ConditionalType).mapper)); } - symbol.type = type; } - return symbol.type; + return type; } - - function getTypeParameterFromMappedType(type: ts.MappedType) { - return type.typeParameter || - (type.typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(type.declaration.typeParameter))); + if (type.flags & ts.TypeFlags.Union) { + return mapType(type as ts.UnionType, getLowerBoundOfKeyType); } - - function getConstraintTypeFromMappedType(type: ts.MappedType) { - return type.constraintType || - (type.constraintType = getConstraintOfTypeParameter(getTypeParameterFromMappedType(type)) || errorType); + if (type.flags & ts.TypeFlags.Intersection) { + return getIntersectionType(ts.sameMap((type as ts.UnionType).types, getLowerBoundOfKeyType)); } + return type; + } - function getNameTypeFromMappedType(type: ts.MappedType) { - return type.declaration.nameType ? - type.nameType || (type.nameType = instantiateType(getTypeFromTypeNode(type.declaration.nameType), type.mapper)) : - undefined; - } + function getIsLateCheckFlag(s: ts.Symbol): ts.CheckFlags { + return ts.getCheckFlags(s) & ts.CheckFlags.Late; + } - function getTemplateTypeFromMappedType(type: ts.MappedType) { - return type.templateType || - (type.templateType = type.declaration.type ? - instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), /*isProperty*/ true, !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper) : - errorType); + function forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(type: ts.Type, include: ts.TypeFlags, stringsOnly: boolean, cb: (keyType: ts.Type) => void) { + for (const prop of getPropertiesOfType(type)) { + cb(getLiteralTypeFromProperty(prop, include)); } - - function getConstraintDeclarationForMappedType(type: ts.MappedType) { - return ts.getEffectiveConstraintOfTypeParameter(type.declaration.typeParameter); + if (type.flags & ts.TypeFlags.Any) { + cb(stringType); } - - function isMappedTypeWithKeyofConstraintDeclaration(type: ts.MappedType) { - const constraintDeclaration = getConstraintDeclarationForMappedType(type)!; // TODO: GH#18217 - return constraintDeclaration.kind === ts.SyntaxKind.TypeOperator && - (constraintDeclaration as ts.TypeOperatorNode).operator === ts.SyntaxKind.KeyOfKeyword; + else { + for (const info of getIndexInfosOfType(type)) { + if (!stringsOnly || info.keyType.flags & (ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral)) { + cb(info.keyType); + } + } } + } - function getModifiersTypeFromMappedType(type: ts.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) as ts.TypeOperatorNode).type), type.mapper); + /** Resolve the members of a mapped type { [P in K]: T } */ + function resolveMappedTypeMembers(type: ts.MappedType) { + const members: ts.SymbolTable = ts.createSymbolTable(); + let indexInfos: ts.IndexInfo[] | undefined; + // Resolve upfront such that recursive references see an empty object type. + setStructuredTypeMembers(type, emptySymbols, ts.emptyArray, ts.emptyArray, ts.emptyArray); + // 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 nameType = getNameTypeFromMappedType(type.target as ts.MappedType || type); + const templateType = getTemplateTypeFromMappedType(type.target as ts.MappedType || type); + const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' + const templateModifiers = getMappedTypeModifiers(type); + const include = keyofStringsOnly ? ts.TypeFlags.StringLiteral : ts.TypeFlags.StringOrNumberLiteralOrUnique; + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { + // We have a { [P in keyof T]: X } + forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, include, keyofStringsOnly, addMemberForKeyType); + } + else { + forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); + } + setStructuredTypeMembers(type, members, ts.emptyArray, ts.emptyArray, indexInfos || ts.emptyArray); + function addMemberForKeyType(keyType: ts.Type) { + const propNameType = nameType ? instantiateType(nameType, appendTypeMapping(type.mapper, typeParameter, keyType)) : keyType; + forEachType(propNameType, t => addMemberForKeyTypeWorker(keyType, t)); + } + + function addMemberForKeyTypeWorker(keyType: ts.Type, propNameType: ts.Type) { + // 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(propNameType)) { + const propName = getPropertyNameFromType(propNameType); + // String enum members from separate enums with identical values + // are distinct types with the same property name. Make the resulting + // property symbol's name type be the union of those enum member types. + const existingProp = members.get(propName) as ts.MappedSymbol | undefined; + if (existingProp) { + existingProp.nameType = getUnionType([existingProp.nameType!, propNameType]); + existingProp.keyType = getUnionType([existingProp.keyType, keyType]); } 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) as ts.MappedType; - const constraint = getConstraintTypeFromMappedType(declaredType); - const extendedConstraint = constraint && constraint.flags & ts.TypeFlags.TypeParameter ? getConstraintOfTypeParameter(constraint as ts.TypeParameter) : constraint; - type.modifiersType = extendedConstraint && extendedConstraint.flags & ts.TypeFlags.Index ? instantiateType((extendedConstraint as ts.IndexType).type, type.mapper) : unknownType; - } + const modifiersProp = isTypeUsableAsPropertyName(keyType) ? getPropertyOfType(modifiersType, getPropertyNameFromType(keyType)) : undefined; + const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || + !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & ts.SymbolFlags.Optional); + const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || + !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); + const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & ts.SymbolFlags.Optional; + const lateFlag: ts.CheckFlags = modifiersProp ? getIsLateCheckFlag(modifiersProp) : 0; + const prop = createSymbol(ts.SymbolFlags.Property | (isOptional ? ts.SymbolFlags.Optional : 0), propName, lateFlag | ts.CheckFlags.Mapped | (isReadonly ? ts.CheckFlags.Readonly : 0) | (stripOptional ? ts.CheckFlags.StripOptional : 0)) as ts.MappedSymbol; + prop.mappedType = type; + prop.nameType = propNameType; + prop.keyType = keyType; + if (modifiersProp) { + prop.syntheticOrigin = modifiersProp; + // If the mapped type has an `as XXX` clause, the property name likely won't match the declaration name and + // multiple properties may map to the same name. Thus, we attach no declarations to the symbol. + prop.declarations = nameType ? undefined : modifiersProp.declarations; + } + members.set(propName, prop); + } + } + else if (isValidIndexKeyType(propNameType) || propNameType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Enum)) { + const indexKeyType = propNameType.flags & (ts.TypeFlags.Any | ts.TypeFlags.String) ? stringType : + propNameType.flags & (ts.TypeFlags.Number | ts.TypeFlags.Enum) ? numberType : + propNameType; + const propType = instantiateType(templateType, appendTypeMapping(type.mapper, typeParameter, keyType)); + const indexInfo = createIndexInfo(indexKeyType, propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); + indexInfos = appendIndexInfo(indexInfos, indexInfo, /*union*/ true); } - return type.modifiersType; } + } - function getMappedTypeModifiers(type: ts.MappedType): MappedTypeModifiers { - const declaration = type.declaration; - return (declaration.readonlyToken ? declaration.readonlyToken.kind === ts.SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) | - (declaration.questionToken ? declaration.questionToken.kind === ts.SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0); + function getTypeOfMappedSymbol(symbol: ts.MappedSymbol) { + if (!symbol.type) { + const mappedType = symbol.mappedType; + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + mappedType.containsError = true; + return errorType; + } + const templateType = getTemplateTypeFromMappedType(mappedType.target as ts.MappedType || mappedType); + const mapper = appendTypeMapping(mappedType.mapper, getTypeParameterFromMappedType(mappedType), symbol.keyType); + const propType = instantiateType(templateType, mapper); + // 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. + let type = strictNullChecks && symbol.flags & ts.SymbolFlags.Optional && !maybeTypeOfKind(propType, ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? getOptionalType(propType, /*isProperty*/ true) : + symbol.checkFlags & ts.CheckFlags.StripOptional ? removeMissingOrUndefinedType(propType) : + propType; + if (!popTypeResolution()) { + error(currentNode, ts.Diagnostics.Type_of_property_0_circularly_references_itself_in_mapped_type_1, symbolToString(symbol), typeToString(mappedType)); + type = errorType; + } + symbol.type = type; } + return symbol.type; + } - function getMappedTypeOptionality(type: ts.MappedType): number { - const modifiers = getMappedTypeModifiers(type); - return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; - } + function getTypeParameterFromMappedType(type: ts.MappedType) { + return type.typeParameter || + (type.typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(type.declaration.typeParameter))); + } - function getCombinedMappedTypeOptionality(type: ts.MappedType): number { - const optionality = getMappedTypeOptionality(type); - const modifiersType = getModifiersTypeFromMappedType(type); - return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(modifiersType) : 0); - } + function getConstraintTypeFromMappedType(type: ts.MappedType) { + return type.constraintType || + (type.constraintType = getConstraintOfTypeParameter(getTypeParameterFromMappedType(type)) || errorType); + } - function isPartialMappedType(type: ts.Type) { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Mapped && getMappedTypeModifiers(type as ts.MappedType) & MappedTypeModifiers.IncludeOptional); - } + function getNameTypeFromMappedType(type: ts.MappedType) { + return type.declaration.nameType ? + type.nameType || (type.nameType = instantiateType(getTypeFromTypeNode(type.declaration.nameType), type.mapper)) : + undefined; + } - function isGenericMappedType(type: ts.Type): type is ts.MappedType { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType(type as ts.MappedType)); - } + function getTemplateTypeFromMappedType(type: ts.MappedType) { + return type.templateType || + (type.templateType = type.declaration.type ? + instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), /*isProperty*/ true, !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper) : + errorType); + } - function resolveStructuredTypeMembers(type: ts.StructuredType): ts.ResolvedType { - if (!(type as ts.ResolvedType).members) { - if (type.flags & ts.TypeFlags.Object) { - if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) { - resolveTypeReferenceMembers(type as ts.TypeReference); - } - else if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.ClassOrInterface) { - resolveClassOrInterfaceMembers(type as ts.InterfaceType); - } - else if ((type as ts.ReverseMappedType).objectFlags & ts.ObjectFlags.ReverseMapped) { - resolveReverseMappedTypeMembers(type as ts.ReverseMappedType); - } - else if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) { - resolveAnonymousTypeMembers(type as ts.AnonymousType); - } - else if ((type as ts.MappedType).objectFlags & ts.ObjectFlags.Mapped) { - resolveMappedTypeMembers(type as ts.MappedType); - } - } - else if (type.flags & ts.TypeFlags.Union) { - resolveUnionTypeMembers(type as ts.UnionType); - } - else if (type.flags & ts.TypeFlags.Intersection) { - resolveIntersectionTypeMembers(type as ts.IntersectionType); - } - } - return type as ts.ResolvedType; - } + function getConstraintDeclarationForMappedType(type: ts.MappedType) { + return ts.getEffectiveConstraintOfTypeParameter(type.declaration.typeParameter); + } - /** Return properties of an object type or an empty array for other types */ - function getPropertiesOfObjectType(type: ts.Type): ts.Symbol[] { - if (type.flags & ts.TypeFlags.Object) { - return resolveStructuredTypeMembers(type as ts.ObjectType).properties; + function isMappedTypeWithKeyofConstraintDeclaration(type: ts.MappedType) { + const constraintDeclaration = getConstraintDeclarationForMappedType(type)!; // TODO: GH#18217 + return constraintDeclaration.kind === ts.SyntaxKind.TypeOperator && + (constraintDeclaration as ts.TypeOperatorNode).operator === ts.SyntaxKind.KeyOfKeyword; + } + + function getModifiersTypeFromMappedType(type: ts.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) as ts.TypeOperatorNode).type), type.mapper); + } + 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) as ts.MappedType; + const constraint = getConstraintTypeFromMappedType(declaredType); + const extendedConstraint = constraint && constraint.flags & ts.TypeFlags.TypeParameter ? getConstraintOfTypeParameter(constraint as ts.TypeParameter) : constraint; + type.modifiersType = extendedConstraint && extendedConstraint.flags & ts.TypeFlags.Index ? instantiateType((extendedConstraint as ts.IndexType).type, type.mapper) : unknownType; } - return ts.emptyArray; } + return type.modifiersType; + } - /** 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: ts.__String): ts.Symbol | undefined { + function getMappedTypeModifiers(type: ts.MappedType): MappedTypeModifiers { + const declaration = type.declaration; + return (declaration.readonlyToken ? declaration.readonlyToken.kind === ts.SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) | + (declaration.questionToken ? declaration.questionToken.kind === ts.SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0); + } + + function getMappedTypeOptionality(type: ts.MappedType): number { + const modifiers = getMappedTypeModifiers(type); + return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; + } + + function getCombinedMappedTypeOptionality(type: ts.MappedType): number { + const optionality = getMappedTypeOptionality(type); + const modifiersType = getModifiersTypeFromMappedType(type); + return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(modifiersType) : 0); + } + + function isPartialMappedType(type: ts.Type) { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Mapped && getMappedTypeModifiers(type as ts.MappedType) & MappedTypeModifiers.IncludeOptional); + } + + function isGenericMappedType(type: ts.Type): type is ts.MappedType { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType(type as ts.MappedType)); + } + + function resolveStructuredTypeMembers(type: ts.StructuredType): ts.ResolvedType { + if (!(type as ts.ResolvedType).members) { if (type.flags & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - const symbol = resolved.members.get(name); - if (symbol && symbolIsValue(symbol)) { - return symbol; + if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) { + resolveTypeReferenceMembers(type as ts.TypeReference); } - } - } - - function getPropertiesOfUnionOrIntersectionType(type: ts.UnionOrIntersectionType): ts.Symbol[] { - if (!type.resolvedProperties) { - const members = ts.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 without index signature - if (type.flags & ts.TypeFlags.Union && getIndexInfosOfType(current).length === 0) { - break; - } + else if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.ClassOrInterface) { + resolveClassOrInterfaceMembers(type as ts.InterfaceType); + } + else if ((type as ts.ReverseMappedType).objectFlags & ts.ObjectFlags.ReverseMapped) { + resolveReverseMappedTypeMembers(type as ts.ReverseMappedType); + } + else if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) { + resolveAnonymousTypeMembers(type as ts.AnonymousType); + } + else if ((type as ts.MappedType).objectFlags & ts.ObjectFlags.Mapped) { + resolveMappedTypeMembers(type as ts.MappedType); } - type.resolvedProperties = getNamedMembers(members); } - return type.resolvedProperties; - } - - function getPropertiesOfType(type: ts.Type): ts.Symbol[] { - type = getReducedApparentType(type); - return type.flags & ts.TypeFlags.UnionOrIntersection ? - getPropertiesOfUnionOrIntersectionType(type as ts.UnionType) : - getPropertiesOfObjectType(type); - } - - function forEachPropertyOfType(type: ts.Type, action: (symbol: ts.Symbol, escapedName: ts.__String) => void): void { - type = getReducedApparentType(type); - if (type.flags & ts.TypeFlags.StructuredType) { - resolveStructuredTypeMembers(type as ts.StructuredType).members.forEach((symbol, escapedName) => { - if (isNamedMember(symbol, escapedName)) { - action(symbol, escapedName); - } - }); + else if (type.flags & ts.TypeFlags.Union) { + resolveUnionTypeMembers(type as ts.UnionType); + } + else if (type.flags & ts.TypeFlags.Intersection) { + resolveIntersectionTypeMembers(type as ts.IntersectionType); } } + return type as ts.ResolvedType; + } - function isTypeInvalidDueToUnionDiscriminant(contextualType: ts.Type, obj: ts.ObjectLiteralExpression | ts.JsxAttributes): boolean { - const list = obj.properties as ts.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); - }); + /** Return properties of an object type or an empty array for other types */ + function getPropertiesOfObjectType(type: ts.Type): ts.Symbol[] { + if (type.flags & ts.TypeFlags.Object) { + return resolveStructuredTypeMembers(type as ts.ObjectType).properties; } + return ts.emptyArray; + } - function getAllPossiblePropertiesOfTypes(types: readonly ts.Type[]): ts.Symbol[] { - const unionType = getUnionType(types); - if (!(unionType.flags & ts.TypeFlags.Union)) { - return getAugmentedPropertiesOfType(unionType); + /** 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: ts.__String): ts.Symbol | undefined { + if (type.flags & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + const symbol = resolved.members.get(name); + if (symbol && symbolIsValue(symbol)) { + return symbol; } + } + } - const props = ts.createSymbolTable(); - for (const memberType of types) { - for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) { - if (!props.has(escapedName)) { - const prop = createUnionOrIntersectionProperty(unionType as ts.UnionType, escapedName); - // May be undefined if the property is private - if (prop) - props.set(escapedName, prop); + function getPropertiesOfUnionOrIntersectionType(type: ts.UnionOrIntersectionType): ts.Symbol[] { + if (!type.resolvedProperties) { + const members = ts.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 without index signature + if (type.flags & ts.TypeFlags.Union && getIndexInfosOfType(current).length === 0) { + break; + } } - return ts.arrayFrom(props.values()); + type.resolvedProperties = getNamedMembers(members); } + return type.resolvedProperties; + } - function getConstraintOfType(type: ts.InstantiableType | ts.UnionOrIntersectionType): ts.Type | undefined { - return type.flags & ts.TypeFlags.TypeParameter ? getConstraintOfTypeParameter(type as ts.TypeParameter) : - type.flags & ts.TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(type as ts.IndexedAccessType) : - type.flags & ts.TypeFlags.Conditional ? getConstraintOfConditionalType(type as ts.ConditionalType) : - getBaseConstraintOfType(type); - } + function getPropertiesOfType(type: ts.Type): ts.Symbol[] { + type = getReducedApparentType(type); + return type.flags & ts.TypeFlags.UnionOrIntersection ? + getPropertiesOfUnionOrIntersectionType(type as ts.UnionType) : + getPropertiesOfObjectType(type); + } - function getConstraintOfTypeParameter(typeParameter: ts.TypeParameter): ts.Type | undefined { - return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined; + function forEachPropertyOfType(type: ts.Type, action: (symbol: ts.Symbol, escapedName: ts.__String) => void): void { + type = getReducedApparentType(type); + if (type.flags & ts.TypeFlags.StructuredType) { + resolveStructuredTypeMembers(type as ts.StructuredType).members.forEach((symbol, escapedName) => { + if (isNamedMember(symbol, escapedName)) { + action(symbol, escapedName); + } + }); } + } - function getConstraintOfIndexedAccess(type: ts.IndexedAccessType) { - return hasNonCircularBaseConstraint(type) ? getConstraintFromIndexedAccess(type) : undefined; - } + function isTypeInvalidDueToUnionDiscriminant(contextualType: ts.Type, obj: ts.ObjectLiteralExpression | ts.JsxAttributes): boolean { + const list = obj.properties as ts.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 getSimplifiedTypeOrConstraint(type: ts.Type) { - const simplified = getSimplifiedType(type, /*writing*/ false); - return simplified !== type ? simplified : getConstraintOfType(type); + function getAllPossiblePropertiesOfTypes(types: readonly ts.Type[]): ts.Symbol[] { + const unionType = getUnionType(types); + if (!(unionType.flags & ts.TypeFlags.Union)) { + return getAugmentedPropertiesOfType(unionType); } - function getConstraintFromIndexedAccess(type: ts.IndexedAccessType) { - if (isMappedTypeGenericIndexedAccess(type)) { - // For indexed access types of the form { [P in K]: E }[X], where K is non-generic and X is generic, - // we substitute an instantiation of E where P is replaced with X. - return substituteIndexedMappedType(type.objectType as ts.MappedType, type.indexType); - } - const indexConstraint = getSimplifiedTypeOrConstraint(type.indexType); - if (indexConstraint && indexConstraint !== type.indexType) { - const indexedAccess = getIndexedAccessTypeOrUndefined(type.objectType, indexConstraint, type.accessFlags); - if (indexedAccess) { - return indexedAccess; + const props = ts.createSymbolTable(); + for (const memberType of types) { + for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) { + if (!props.has(escapedName)) { + const prop = createUnionOrIntersectionProperty(unionType as ts.UnionType, escapedName); + // May be undefined if the property is private + if (prop) + props.set(escapedName, prop); } } - const objectConstraint = getSimplifiedTypeOrConstraint(type.objectType); - if (objectConstraint && objectConstraint !== type.objectType) { - return getIndexedAccessTypeOrUndefined(objectConstraint, type.indexType, type.accessFlags); - } - return undefined; } + return ts.arrayFrom(props.values()); + } - function getDefaultConstraintOfConditionalType(type: ts.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: ts.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 instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper)); - if (!(instantiated.flags & ts.TypeFlags.Never)) { - return instantiated; - } - } + function getConstraintOfType(type: ts.InstantiableType | ts.UnionOrIntersectionType): ts.Type | undefined { + return type.flags & ts.TypeFlags.TypeParameter ? getConstraintOfTypeParameter(type as ts.TypeParameter) : + type.flags & ts.TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(type as ts.IndexedAccessType) : + type.flags & ts.TypeFlags.Conditional ? getConstraintOfConditionalType(type as ts.ConditionalType) : + getBaseConstraintOfType(type); + } + + function getConstraintOfTypeParameter(typeParameter: ts.TypeParameter): ts.Type | undefined { + return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined; + } + + function getConstraintOfIndexedAccess(type: ts.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: ts.IndexedAccessType) { + if (isMappedTypeGenericIndexedAccess(type)) { + // For indexed access types of the form { [P in K]: E }[X], where K is non-generic and X is generic, + // we substitute an instantiation of E where P is replaced with X. + return substituteIndexedMappedType(type.objectType as ts.MappedType, type.indexType); + } + const indexConstraint = getSimplifiedTypeOrConstraint(type.indexType); + if (indexConstraint && indexConstraint !== type.indexType) { + const indexedAccess = getIndexedAccessTypeOrUndefined(type.objectType, indexConstraint, type.accessFlags); + if (indexedAccess) { + return indexedAccess; } - return undefined; } - - function getConstraintFromConditionalType(type: ts.ConditionalType) { - return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type); + const objectConstraint = getSimplifiedTypeOrConstraint(type.objectType); + if (objectConstraint && objectConstraint !== type.objectType) { + return getIndexedAccessTypeOrUndefined(objectConstraint, type.indexType, type.accessFlags); } + return undefined; + } - function getConstraintOfConditionalType(type: ts.ConditionalType) { - return hasNonCircularBaseConstraint(type) ? getConstraintFromConditionalType(type) : undefined; - } + function getDefaultConstraintOfConditionalType(type: ts.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 getEffectiveConstraintOfIntersection(types: readonly ts.Type[], targetIsUnion: boolean) { - let constraints: ts.Type[] | undefined; - let hasDisjointDomainType = false; - for (const t of types) { - if (t.flags & ts.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 & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index | ts.TypeFlags.Conditional)) { - constraint = getConstraintOfType(constraint); - } - if (constraint) { - constraints = ts.append(constraints, constraint); - if (targetIsUnion) { - constraints = ts.append(constraints, t); - } - } + function getConstraintOfDistributiveConditionalType(type: ts.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 instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper)); + if (!(instantiated.flags & ts.TypeFlags.Never)) { + return instantiated; + } + } + } + return undefined; + } + + function getConstraintFromConditionalType(type: ts.ConditionalType) { + return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type); + } + + function getConstraintOfConditionalType(type: ts.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 & ts.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 & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index | ts.TypeFlags.Conditional)) { + constraint = getConstraintOfType(constraint); } - else if (t.flags & ts.TypeFlags.DisjointDomains) { - hasDisjointDomainType = true; + if (constraint) { + constraints = ts.append(constraints, constraint); + if (targetIsUnion) { + constraints = ts.append(constraints, t); + } } } - // 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 & ts.TypeFlags.DisjointDomains) { - constraints = ts.append(constraints, t); - } + else if (t.flags & ts.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 & ts.TypeFlags.DisjointDomains) { + constraints = ts.append(constraints, t); } } - return getIntersectionType(constraints); } - return undefined; + return getIntersectionType(constraints); } + return undefined; + } - function getBaseConstraintOfType(type: ts.Type): ts.Type | undefined { - if (type.flags & (ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping)) { - const constraint = getResolvedBaseConstraint(type as ts.InstantiableType | ts.UnionOrIntersectionType); - return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined; - } - return type.flags & ts.TypeFlags.Index ? keyofConstraintType : undefined; + function getBaseConstraintOfType(type: ts.Type): ts.Type | undefined { + if (type.flags & (ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping)) { + const constraint = getResolvedBaseConstraint(type as ts.InstantiableType | ts.UnionOrIntersectionType); + return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined; } + return type.flags & ts.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; - } + /** + * 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: ts.InstantiableType): boolean { - return getResolvedBaseConstraint(type) !== circularConstraintType; - } + function hasNonCircularBaseConstraint(type: ts.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: ts.InstantiableType | ts.UnionOrIntersectionType): ts.Type { - if (type.resolvedBaseConstraint) { - return type.resolvedBaseConstraint; - } - const stack: object[] = []; - return type.resolvedBaseConstraint = getTypeWithThisArgument(getImmediateBaseConstraint(type), type); - - function getImmediateBaseConstraint(t: ts.Type): ts.Type { - if (!t.immediateBaseConstraint) { - if (!pushTypeResolution(t, TypeSystemPropertyName.ImmediateBaseConstraint)) { - return circularConstraintType; - } - let result; - // We always explore at least 10 levels of nested constraints. Thereafter, we continue to explore - // up to 50 levels of nested constraints provided there are no "deeply nested" types on the stack - // (i.e. no types for which five instantiations have been recorded on the stack). If we reach 50 - // levels of nesting, we are presumably exploring a repeating pattern with a long cycle that hasn't - // yet triggered the deeply nested limiter. We have no test cases that actually get to 50 levels of - // nesting, so it is effectively just a safety stop. - const identity = getRecursionIdentity(t); - if (stack.length < 10 || stack.length < 50 && !ts.contains(stack, identity)) { - stack.push(identity); - result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false)); - stack.pop(); - } - if (!popTypeResolution()) { - if (t.flags & ts.TypeFlags.TypeParameter) { - const errorNode = getConstraintDeclaration(t as ts.TypeParameter); - if (errorNode) { - const diagnostic = error(errorNode, ts.Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(t)); - if (currentNode && !ts.isNodeDescendantOf(errorNode, currentNode) && !ts.isNodeDescendantOf(currentNode, errorNode)) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(currentNode, ts.Diagnostics.Circularity_originates_in_type_at_this_location)); - } + /** + * 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: ts.InstantiableType | ts.UnionOrIntersectionType): ts.Type { + if (type.resolvedBaseConstraint) { + return type.resolvedBaseConstraint; + } + const stack: object[] = []; + return type.resolvedBaseConstraint = getTypeWithThisArgument(getImmediateBaseConstraint(type), type); + + function getImmediateBaseConstraint(t: ts.Type): ts.Type { + if (!t.immediateBaseConstraint) { + if (!pushTypeResolution(t, TypeSystemPropertyName.ImmediateBaseConstraint)) { + return circularConstraintType; + } + let result; + // We always explore at least 10 levels of nested constraints. Thereafter, we continue to explore + // up to 50 levels of nested constraints provided there are no "deeply nested" types on the stack + // (i.e. no types for which five instantiations have been recorded on the stack). If we reach 50 + // levels of nesting, we are presumably exploring a repeating pattern with a long cycle that hasn't + // yet triggered the deeply nested limiter. We have no test cases that actually get to 50 levels of + // nesting, so it is effectively just a safety stop. + const identity = getRecursionIdentity(t); + if (stack.length < 10 || stack.length < 50 && !ts.contains(stack, identity)) { + stack.push(identity); + result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false)); + stack.pop(); + } + if (!popTypeResolution()) { + if (t.flags & ts.TypeFlags.TypeParameter) { + const errorNode = getConstraintDeclaration(t as ts.TypeParameter); + if (errorNode) { + const diagnostic = error(errorNode, ts.Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(t)); + if (currentNode && !ts.isNodeDescendantOf(errorNode, currentNode) && !ts.isNodeDescendantOf(currentNode, errorNode)) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(currentNode, ts.Diagnostics.Circularity_originates_in_type_at_this_location)); } } - result = circularConstraintType; } - t.immediateBaseConstraint = result || noConstraintType; + result = circularConstraintType; } - return t.immediateBaseConstraint; + t.immediateBaseConstraint = result || noConstraintType; } + return t.immediateBaseConstraint; + } - function getBaseConstraint(t: ts.Type): ts.Type | undefined { - const c = getImmediateBaseConstraint(t); - return c !== noConstraintType && c !== circularConstraintType ? c : undefined; - } + 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 & ts.TypeFlags.TypeParameter) { - const constraint = getConstraintFromTypeParameter(t as ts.TypeParameter); - return (t as ts.TypeParameter).isThisType || !constraint ? - constraint : - getBaseConstraint(constraint); - } - if (t.flags & ts.TypeFlags.UnionOrIntersection) { - const types = (t as ts.UnionOrIntersectionType).types; - const baseTypes: ts.Type[] = []; - let different = false; - for (const type of types) { - const baseType = getBaseConstraint(type); - if (baseType) { - if (baseType !== type) { - different = true; - } - baseTypes.push(baseType); - } - else { + function computeBaseConstraint(t: ts.Type): ts.Type | undefined { + if (t.flags & ts.TypeFlags.TypeParameter) { + const constraint = getConstraintFromTypeParameter(t as ts.TypeParameter); + return (t as ts.TypeParameter).isThisType || !constraint ? + constraint : + getBaseConstraint(constraint); + } + if (t.flags & ts.TypeFlags.UnionOrIntersection) { + const types = (t as ts.UnionOrIntersectionType).types; + const baseTypes: ts.Type[] = []; + let different = false; + for (const type of types) { + const baseType = getBaseConstraint(type); + if (baseType) { + if (baseType !== type) { different = true; } + baseTypes.push(baseType); } - if (!different) { - return t; - } - return t.flags & ts.TypeFlags.Union && baseTypes.length === types.length ? getUnionType(baseTypes) : - t.flags & ts.TypeFlags.Intersection && baseTypes.length ? getIntersectionType(baseTypes) : - undefined; - } - if (t.flags & ts.TypeFlags.Index) { - return keyofConstraintType; - } - if (t.flags & ts.TypeFlags.TemplateLiteral) { - const types = (t as ts.TemplateLiteralType).types; - const constraints = ts.mapDefined(types, getBaseConstraint); - return constraints.length === types.length ? getTemplateLiteralType((t as ts.TemplateLiteralType).texts, constraints) : stringType; - } - if (t.flags & ts.TypeFlags.StringMapping) { - const constraint = getBaseConstraint((t as ts.StringMappingType).type); - return constraint ? getStringMappingType((t as ts.StringMappingType).symbol, constraint) : stringType; - } - if (t.flags & ts.TypeFlags.IndexedAccess) { - if (isMappedTypeGenericIndexedAccess(t)) { - // For indexed access types of the form { [P in K]: E }[X], where K is non-generic and X is generic, - // we substitute an instantiation of E where P is replaced with X. - return getBaseConstraint(substituteIndexedMappedType((t as ts.IndexedAccessType).objectType as ts.MappedType, (t as ts.IndexedAccessType).indexType)); + else { + different = true; } - const baseObjectType = getBaseConstraint((t as ts.IndexedAccessType).objectType); - const baseIndexType = getBaseConstraint((t as ts.IndexedAccessType).indexType); - const baseIndexedAccess = baseObjectType && baseIndexType && getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, (t as ts.IndexedAccessType).accessFlags); - return baseIndexedAccess && getBaseConstraint(baseIndexedAccess); } - if (t.flags & ts.TypeFlags.Conditional) { - const constraint = getConstraintFromConditionalType(t as ts.ConditionalType); - return constraint && getBaseConstraint(constraint); + if (!different) { + return t; } - if (t.flags & ts.TypeFlags.Substitution) { - return getBaseConstraint((t as ts.SubstitutionType).substitute); + return t.flags & ts.TypeFlags.Union && baseTypes.length === types.length ? getUnionType(baseTypes) : + t.flags & ts.TypeFlags.Intersection && baseTypes.length ? getIntersectionType(baseTypes) : + undefined; + } + if (t.flags & ts.TypeFlags.Index) { + return keyofConstraintType; + } + if (t.flags & ts.TypeFlags.TemplateLiteral) { + const types = (t as ts.TemplateLiteralType).types; + const constraints = ts.mapDefined(types, getBaseConstraint); + return constraints.length === types.length ? getTemplateLiteralType((t as ts.TemplateLiteralType).texts, constraints) : stringType; + } + if (t.flags & ts.TypeFlags.StringMapping) { + const constraint = getBaseConstraint((t as ts.StringMappingType).type); + return constraint ? getStringMappingType((t as ts.StringMappingType).symbol, constraint) : stringType; + } + if (t.flags & ts.TypeFlags.IndexedAccess) { + if (isMappedTypeGenericIndexedAccess(t)) { + // For indexed access types of the form { [P in K]: E }[X], where K is non-generic and X is generic, + // we substitute an instantiation of E where P is replaced with X. + return getBaseConstraint(substituteIndexedMappedType((t as ts.IndexedAccessType).objectType as ts.MappedType, (t as ts.IndexedAccessType).indexType)); } - return t; + const baseObjectType = getBaseConstraint((t as ts.IndexedAccessType).objectType); + const baseIndexType = getBaseConstraint((t as ts.IndexedAccessType).indexType); + const baseIndexedAccess = baseObjectType && baseIndexType && getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, (t as ts.IndexedAccessType).accessFlags); + return baseIndexedAccess && getBaseConstraint(baseIndexedAccess); + } + if (t.flags & ts.TypeFlags.Conditional) { + const constraint = getConstraintFromConditionalType(t as ts.ConditionalType); + return constraint && getBaseConstraint(constraint); + } + if (t.flags & ts.TypeFlags.Substitution) { + return getBaseConstraint((t as ts.SubstitutionType).substitute); } + return t; } + } - function getApparentTypeOfIntersectionType(type: ts.IntersectionType) { - return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, type, /*apparentType*/ true)); - } + function getApparentTypeOfIntersectionType(type: ts.IntersectionType) { + return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, type, /*apparentType*/ true)); + } - function getResolvedTypeParameterDefault(typeParameter: ts.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 && ts.forEach(typeParameter.symbol.declarations, decl => ts.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; - } - } + function getResolvedTypeParameterDefault(typeParameter: ts.TypeParameter): ts.Type | undefined { + if (!typeParameter.default) { + if (typeParameter.target) { + const targetDefault = getResolvedTypeParameterDefault(typeParameter.target); + typeParameter.default = targetDefault ? instantiateType(targetDefault, typeParameter.mapper) : noConstraintType; } - else if (typeParameter.default === resolvingDefaultType) { - // If we are called recursively for this type parameter, mark the default as circular. - typeParameter.default = circularConstraintType; + else { + // To block recursion, set the initial value to the resolvingDefaultType. + typeParameter.default = resolvingDefaultType; + const defaultDeclaration = typeParameter.symbol && ts.forEach(typeParameter.symbol.declarations, decl => ts.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; + } } - 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: ts.TypeParameter): ts.Type | undefined { - const defaultType = getResolvedTypeParameterDefault(typeParameter); - return defaultType !== noConstraintType && defaultType !== circularConstraintType ? defaultType : undefined; + 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; + } - function hasNonCircularTypeParameterDefault(typeParameter: ts.TypeParameter) { - return getResolvedTypeParameterDefault(typeParameter) !== circularConstraintType; - } + /** + * 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: ts.TypeParameter): ts.Type | undefined { + const defaultType = getResolvedTypeParameterDefault(typeParameter); + return defaultType !== noConstraintType && defaultType !== circularConstraintType ? defaultType : undefined; + } - /** - * Indicates whether the declaration of a typeParameter has a default type. - */ - function hasTypeParameterDefault(typeParameter: ts.TypeParameter): boolean { - return !!(typeParameter.symbol && ts.forEach(typeParameter.symbol.declarations, decl => ts.isTypeParameterDeclaration(decl) && decl.default)); - } + function hasNonCircularTypeParameterDefault(typeParameter: ts.TypeParameter) { + return getResolvedTypeParameterDefault(typeParameter) !== circularConstraintType; + } - function getApparentTypeOfMappedType(type: ts.MappedType) { - return type.resolvedApparentType || (type.resolvedApparentType = getResolvedApparentTypeOfMappedType(type)); - } + /** + * Indicates whether the declaration of a typeParameter has a default type. + */ + function hasTypeParameterDefault(typeParameter: ts.TypeParameter): boolean { + return !!(typeParameter.symbol && ts.forEach(typeParameter.symbol.declarations, decl => ts.isTypeParameterDeclaration(decl) && decl.default)); + } - function getResolvedApparentTypeOfMappedType(type: ts.MappedType) { - const typeVariable = getHomomorphicTypeVariable(type); - if (typeVariable && !type.declaration.nameType) { - const constraint = getConstraintOfTypeParameter(typeVariable); - if (constraint && isArrayOrTupleType(constraint)) { - return instantiateType(type, prependTypeMapping(typeVariable, constraint, type.mapper)); - } + function getApparentTypeOfMappedType(type: ts.MappedType) { + return type.resolvedApparentType || (type.resolvedApparentType = getResolvedApparentTypeOfMappedType(type)); + } + + function getResolvedApparentTypeOfMappedType(type: ts.MappedType) { + const typeVariable = getHomomorphicTypeVariable(type); + if (typeVariable && !type.declaration.nameType) { + const constraint = getConstraintOfTypeParameter(typeVariable); + if (constraint && isArrayOrTupleType(constraint)) { + return instantiateType(type, prependTypeMapping(typeVariable, constraint, type.mapper)); } - return type; } + return type; + } - function isMappedTypeGenericIndexedAccess(type: ts.Type) { - let objectType; - return !!(type.flags & ts.TypeFlags.IndexedAccess && ts.getObjectFlags(objectType = (type as ts.IndexedAccessType).objectType) & ts.ObjectFlags.Mapped && - !isGenericMappedType(objectType) && isGenericIndexType((type as ts.IndexedAccessType).indexType) && - !(objectType as ts.MappedType).declaration.questionToken && !(objectType as ts.MappedType).declaration.nameType); - } + function isMappedTypeGenericIndexedAccess(type: ts.Type) { + let objectType; + return !!(type.flags & ts.TypeFlags.IndexedAccess && ts.getObjectFlags(objectType = (type as ts.IndexedAccessType).objectType) & ts.ObjectFlags.Mapped && + !isGenericMappedType(objectType) && isGenericIndexType((type as ts.IndexedAccessType).indexType) && + !(objectType as ts.MappedType).declaration.questionToken && !(objectType as ts.MappedType).declaration.nameType); + } - /** - * 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. - */ - function getApparentType(type: ts.Type): ts.Type { - const t = !(type.flags & ts.TypeFlags.Instantiable) ? type : getBaseConstraintOfType(type) || unknownType; - return ts.getObjectFlags(t) & ts.ObjectFlags.Mapped ? getApparentTypeOfMappedType(t as ts.MappedType) : - t.flags & ts.TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t as ts.IntersectionType) : - t.flags & ts.TypeFlags.StringLike ? globalStringType : - t.flags & ts.TypeFlags.NumberLike ? globalNumberType : - t.flags & ts.TypeFlags.BigIntLike ? getGlobalBigIntType(/*reportErrors*/ languageVersion >= ts.ScriptTarget.ES2020) : - t.flags & ts.TypeFlags.BooleanLike ? globalBooleanType : - t.flags & ts.TypeFlags.ESSymbolLike ? getGlobalESSymbolType(/*reportErrors*/ languageVersion >= ts.ScriptTarget.ES2015) : - t.flags & ts.TypeFlags.NonPrimitive ? emptyObjectType : - t.flags & ts.TypeFlags.Index ? keyofConstraintType : - t.flags & ts.TypeFlags.Unknown && !strictNullChecks ? emptyObjectType : - t; - } - - function getReducedApparentType(type: ts.Type): ts.Type { - // Since getApparentType may return a non-reduced union or intersection type, we need to perform - // type reduction both before and after obtaining the apparent type. For example, given a type parameter - // 'T extends A | B', the type 'T & X' becomes 'A & X | B & X' after obtaining the apparent type, and - // that type may need further reduction to remove empty intersections. - return getReducedType(getApparentType(getReducedType(type))); - } - - function createUnionOrIntersectionProperty(containingType: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { - let singleProp: ts.Symbol | undefined; - let propSet: ts.ESMap | undefined; - let indexTypes: ts.Type[] | undefined; - const isUnion = containingType.flags & ts.TypeFlags.Union; - // Flags we want to propagate to the result if they exist in all source symbols - let optionalFlag = isUnion ? ts.SymbolFlags.None : ts.SymbolFlags.Optional; - let syntheticFlag = ts.CheckFlags.SyntheticMethod; - let checkFlags = isUnion ? 0 : ts.CheckFlags.Readonly; - let mergedInstantiations = false; - for (const current of containingType.types) { - const type = getApparentType(current); - if (!(isErrorType(type) || type.flags & ts.TypeFlags.Never)) { - const prop = getPropertyOfType(type, name, skipObjectFunctionPropertyAugment); - const modifiers = prop ? ts.getDeclarationModifierFlagsFromSymbol(prop) : 0; - if (prop) { - if (isUnion) { - optionalFlag |= (prop.flags & ts.SymbolFlags.Optional); + /** + * 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. + */ + function getApparentType(type: ts.Type): ts.Type { + const t = !(type.flags & ts.TypeFlags.Instantiable) ? type : getBaseConstraintOfType(type) || unknownType; + return ts.getObjectFlags(t) & ts.ObjectFlags.Mapped ? getApparentTypeOfMappedType(t as ts.MappedType) : + t.flags & ts.TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t as ts.IntersectionType) : + t.flags & ts.TypeFlags.StringLike ? globalStringType : + t.flags & ts.TypeFlags.NumberLike ? globalNumberType : + t.flags & ts.TypeFlags.BigIntLike ? getGlobalBigIntType(/*reportErrors*/ languageVersion >= ts.ScriptTarget.ES2020) : + t.flags & ts.TypeFlags.BooleanLike ? globalBooleanType : + t.flags & ts.TypeFlags.ESSymbolLike ? getGlobalESSymbolType(/*reportErrors*/ languageVersion >= ts.ScriptTarget.ES2015) : + t.flags & ts.TypeFlags.NonPrimitive ? emptyObjectType : + t.flags & ts.TypeFlags.Index ? keyofConstraintType : + t.flags & ts.TypeFlags.Unknown && !strictNullChecks ? emptyObjectType : + t; + } + + function getReducedApparentType(type: ts.Type): ts.Type { + // Since getApparentType may return a non-reduced union or intersection type, we need to perform + // type reduction both before and after obtaining the apparent type. For example, given a type parameter + // 'T extends A | B', the type 'T & X' becomes 'A & X | B & X' after obtaining the apparent type, and + // that type may need further reduction to remove empty intersections. + return getReducedType(getApparentType(getReducedType(type))); + } + + function createUnionOrIntersectionProperty(containingType: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { + let singleProp: ts.Symbol | undefined; + let propSet: ts.ESMap | undefined; + let indexTypes: ts.Type[] | undefined; + const isUnion = containingType.flags & ts.TypeFlags.Union; + // Flags we want to propagate to the result if they exist in all source symbols + let optionalFlag = isUnion ? ts.SymbolFlags.None : ts.SymbolFlags.Optional; + let syntheticFlag = ts.CheckFlags.SyntheticMethod; + let checkFlags = isUnion ? 0 : ts.CheckFlags.Readonly; + let mergedInstantiations = false; + for (const current of containingType.types) { + const type = getApparentType(current); + if (!(isErrorType(type) || type.flags & ts.TypeFlags.Never)) { + const prop = getPropertyOfType(type, name, skipObjectFunctionPropertyAugment); + const modifiers = prop ? ts.getDeclarationModifierFlagsFromSymbol(prop) : 0; + if (prop) { + if (isUnion) { + optionalFlag |= (prop.flags & ts.SymbolFlags.Optional); + } + else { + optionalFlag &= prop.flags; + } + if (!singleProp) { + singleProp = prop; + } + else if (prop !== singleProp) { + const isInstantiation = (getTargetSymbol(prop) || prop) === (getTargetSymbol(singleProp) || singleProp); + // If the symbols are instances of one another with identical types - consider the symbols + // equivalent and just use the first one, which thus allows us to avoid eliding private + // members when intersecting a (this-)instantiations of a class with it's raw base or another instance + if (isInstantiation && compareProperties(singleProp, prop, (a, b) => a === b ? ts.Ternary.True : ts.Ternary.False) === ts.Ternary.True) { + // If we merged instantiations of a generic type, we replicate the symbol parent resetting behavior we used + // to do when we recorded multiple distinct symbols so that we still get, eg, `Array.length` printed + // back and not `Array.length` when we're looking at a `.length` access on a `string[] | number[]` + mergedInstantiations = !!singleProp.parent && !!ts.length(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(singleProp.parent)); } else { - optionalFlag &= prop.flags; - } - if (!singleProp) { - singleProp = prop; - } - else if (prop !== singleProp) { - const isInstantiation = (getTargetSymbol(prop) || prop) === (getTargetSymbol(singleProp) || singleProp); - // If the symbols are instances of one another with identical types - consider the symbols - // equivalent and just use the first one, which thus allows us to avoid eliding private - // members when intersecting a (this-)instantiations of a class with it's raw base or another instance - if (isInstantiation && compareProperties(singleProp, prop, (a, b) => a === b ? ts.Ternary.True : ts.Ternary.False) === ts.Ternary.True) { - // If we merged instantiations of a generic type, we replicate the symbol parent resetting behavior we used - // to do when we recorded multiple distinct symbols so that we still get, eg, `Array.length` printed - // back and not `Array.length` when we're looking at a `.length` access on a `string[] | number[]` - mergedInstantiations = !!singleProp.parent && !!ts.length(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(singleProp.parent)); + if (!propSet) { + propSet = new ts.Map(); + propSet.set(getSymbolId(singleProp), singleProp); } - else { - if (!propSet) { - propSet = new ts.Map(); - propSet.set(getSymbolId(singleProp), singleProp); - } - const id = getSymbolId(prop); - if (!propSet.has(id)) { - propSet.set(id, prop); - } + const id = getSymbolId(prop); + if (!propSet.has(id)) { + propSet.set(id, prop); } } - if (isUnion && isReadonlySymbol(prop)) { - checkFlags |= ts.CheckFlags.Readonly; - } - else if (!isUnion && !isReadonlySymbol(prop)) { - checkFlags &= ~ts.CheckFlags.Readonly; - } - checkFlags |= (!(modifiers & ts.ModifierFlags.NonPublicAccessibilityModifier) ? ts.CheckFlags.ContainsPublic : 0) | - (modifiers & ts.ModifierFlags.Protected ? ts.CheckFlags.ContainsProtected : 0) | - (modifiers & ts.ModifierFlags.Private ? ts.CheckFlags.ContainsPrivate : 0) | - (modifiers & ts.ModifierFlags.Static ? ts.CheckFlags.ContainsStatic : 0); - if (!isPrototypeProperty(prop)) { - syntheticFlag = ts.CheckFlags.SyntheticProperty; - } } - else if (isUnion) { - const indexInfo = !isLateBoundName(name) && getApplicableIndexInfoForName(type, name); - if (indexInfo) { - checkFlags |= ts.CheckFlags.WritePartial | (indexInfo.isReadonly ? ts.CheckFlags.Readonly : 0); - indexTypes = ts.append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type); - } - else if (isObjectLiteralType(type) && !(ts.getObjectFlags(type) & ts.ObjectFlags.ContainsSpread)) { - checkFlags |= ts.CheckFlags.WritePartial; - indexTypes = ts.append(indexTypes, undefinedType); - } - else { - checkFlags |= ts.CheckFlags.ReadPartial; - } + if (isUnion && isReadonlySymbol(prop)) { + checkFlags |= ts.CheckFlags.Readonly; + } + else if (!isUnion && !isReadonlySymbol(prop)) { + checkFlags &= ~ts.CheckFlags.Readonly; + } + checkFlags |= (!(modifiers & ts.ModifierFlags.NonPublicAccessibilityModifier) ? ts.CheckFlags.ContainsPublic : 0) | + (modifiers & ts.ModifierFlags.Protected ? ts.CheckFlags.ContainsProtected : 0) | + (modifiers & ts.ModifierFlags.Private ? ts.CheckFlags.ContainsPrivate : 0) | + (modifiers & ts.ModifierFlags.Static ? ts.CheckFlags.ContainsStatic : 0); + if (!isPrototypeProperty(prop)) { + syntheticFlag = ts.CheckFlags.SyntheticProperty; + } + } + else if (isUnion) { + const indexInfo = !isLateBoundName(name) && getApplicableIndexInfoForName(type, name); + if (indexInfo) { + checkFlags |= ts.CheckFlags.WritePartial | (indexInfo.isReadonly ? ts.CheckFlags.Readonly : 0); + indexTypes = ts.append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type); + } + else if (isObjectLiteralType(type) && !(ts.getObjectFlags(type) & ts.ObjectFlags.ContainsSpread)) { + checkFlags |= ts.CheckFlags.WritePartial; + indexTypes = ts.append(indexTypes, undefinedType); + } + else { + checkFlags |= ts.CheckFlags.ReadPartial; } } } - if (!singleProp || isUnion && (propSet || checkFlags & ts.CheckFlags.Partial) && checkFlags & (ts.CheckFlags.ContainsPrivate | ts.CheckFlags.ContainsProtected)) { - // No property was found, or, in a union, a property has a private or protected declaration in one - // constituent, but is missing or has a different declaration in another constituent. - return undefined; + } + if (!singleProp || isUnion && (propSet || checkFlags & ts.CheckFlags.Partial) && checkFlags & (ts.CheckFlags.ContainsPrivate | ts.CheckFlags.ContainsProtected)) { + // No property was found, or, in a union, a property has a private or protected declaration in one + // constituent, but is missing or has a different declaration in another constituent. + return undefined; + } + if (!propSet && !(checkFlags & ts.CheckFlags.ReadPartial) && !indexTypes) { + if (mergedInstantiations) { + // No symbol from a union/intersection should have a `.parent` set (since unions/intersections don't act as symbol parents) + // Unless that parent is "reconstituted" from the "first value declaration" on the symbol (which is likely different than its instantiated parent!) + // They also have a `.containingType` set, which affects some services endpoints behavior, like `getRootSymbol` + const clone = createSymbolWithType(singleProp, (singleProp as ts.TransientSymbol).type); + clone.parent = singleProp.valueDeclaration?.symbol?.parent; + clone.containingType = containingType; + clone.mapper = (singleProp as ts.TransientSymbol).mapper; + return clone; } - if (!propSet && !(checkFlags & ts.CheckFlags.ReadPartial) && !indexTypes) { - if (mergedInstantiations) { - // No symbol from a union/intersection should have a `.parent` set (since unions/intersections don't act as symbol parents) - // Unless that parent is "reconstituted" from the "first value declaration" on the symbol (which is likely different than its instantiated parent!) - // They also have a `.containingType` set, which affects some services endpoints behavior, like `getRootSymbol` - const clone = createSymbolWithType(singleProp, (singleProp as ts.TransientSymbol).type); - clone.parent = singleProp.valueDeclaration?.symbol?.parent; - clone.containingType = containingType; - clone.mapper = (singleProp as ts.TransientSymbol).mapper; - return clone; - } - else { - return singleProp; - } + else { + return singleProp; + } + } + const props = propSet ? ts.arrayFrom(propSet.values()) : [singleProp]; + let declarations: ts.Declaration[] | undefined; + let firstType: ts.Type | undefined; + let nameType: ts.Type | undefined; + const propTypes: ts.Type[] = []; + let writeTypes: ts.Type[] | undefined; + let firstValueDeclaration: ts.Declaration | undefined; + let hasNonUniformValueDeclaration = false; + for (const prop of props) { + if (!firstValueDeclaration) { + firstValueDeclaration = prop.valueDeclaration; + } + else if (prop.valueDeclaration && prop.valueDeclaration !== firstValueDeclaration) { + hasNonUniformValueDeclaration = true; + } + declarations = ts.addRange(declarations, prop.declarations); + const type = getTypeOfSymbol(prop); + if (!firstType) { + firstType = type; + nameType = getSymbolLinks(prop).nameType; } - const props = propSet ? ts.arrayFrom(propSet.values()) : [singleProp]; - let declarations: ts.Declaration[] | undefined; - let firstType: ts.Type | undefined; - let nameType: ts.Type | undefined; - const propTypes: ts.Type[] = []; - let writeTypes: ts.Type[] | undefined; - let firstValueDeclaration: ts.Declaration | undefined; - let hasNonUniformValueDeclaration = false; - for (const prop of props) { - if (!firstValueDeclaration) { - firstValueDeclaration = prop.valueDeclaration; - } - else if (prop.valueDeclaration && prop.valueDeclaration !== firstValueDeclaration) { - hasNonUniformValueDeclaration = true; - } - declarations = ts.addRange(declarations, prop.declarations); - const type = getTypeOfSymbol(prop); - if (!firstType) { - firstType = type; - nameType = getSymbolLinks(prop).nameType; - } - const writeType = getWriteTypeOfSymbol(prop); - if (writeTypes || writeType !== type) { - writeTypes = ts.append(!writeTypes ? propTypes.slice() : writeTypes, writeType); - } - else if (type !== firstType) { - checkFlags |= ts.CheckFlags.HasNonUniformType; - } - if (isLiteralType(type) || isPatternLiteralType(type) || type === uniqueLiteralType) { - checkFlags |= ts.CheckFlags.HasLiteralType; - } - if (type.flags & ts.TypeFlags.Never && type !== uniqueLiteralType) { - checkFlags |= ts.CheckFlags.HasNeverType; - } - propTypes.push(type); + const writeType = getWriteTypeOfSymbol(prop); + if (writeTypes || writeType !== type) { + writeTypes = ts.append(!writeTypes ? propTypes.slice() : writeTypes, writeType); } - ts.addRange(propTypes, indexTypes); - const result = createSymbol(ts.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; - } + else if (type !== firstType) { + checkFlags |= ts.CheckFlags.HasNonUniformType; } - - 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 |= ts.CheckFlags.DeferredType; - result.deferralParent = containingType; - result.deferralConstituents = propTypes; - result.deferralWriteConstituents = writeTypes; + if (isLiteralType(type) || isPatternLiteralType(type) || type === uniqueLiteralType) { + checkFlags |= ts.CheckFlags.HasLiteralType; } - else { - result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes); - if (writeTypes) { - result.writeType = isUnion ? getUnionType(writeTypes) : getIntersectionType(writeTypes); - } + if (type.flags & ts.TypeFlags.Never && type !== uniqueLiteralType) { + checkFlags |= ts.CheckFlags.HasNeverType; } - return result; + propTypes.push(type); } + ts.addRange(propTypes, indexTypes); + const result = createSymbol(ts.SymbolFlags.Property | optionalFlag, name, syntheticFlag | checkFlags); + result.containingType = containingType; + if (!hasNonUniformValueDeclaration && firstValueDeclaration) { + result.valueDeclaration = firstValueDeclaration; - // 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: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { - let property = type.propertyCacheWithoutObjectFunctionPropertyAugment?.get(name) || - !skipObjectFunctionPropertyAugment ? type.propertyCache?.get(name) : undefined; - if (!property) { - property = createUnionOrIntersectionProperty(type, name, skipObjectFunctionPropertyAugment); - if (property) { - const properties = skipObjectFunctionPropertyAugment ? - type.propertyCacheWithoutObjectFunctionPropertyAugment ||= ts.createSymbolTable() : - type.propertyCache ||= ts.createSymbolTable(); - properties.set(name, property); - } + // Inherit information about parent type. + if (firstValueDeclaration.symbol.parent) { + result.parent = firstValueDeclaration.symbol.parent; } - return property; } - function getPropertyOfUnionOrIntersectionType(type: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { - const property = getUnionOrIntersectionProperty(type, name, skipObjectFunctionPropertyAugment); - // We need to filter out partial properties in union types - return property && !(ts.getCheckFlags(property) & ts.CheckFlags.ReadPartial) ? property : undefined; + 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 |= ts.CheckFlags.DeferredType; + result.deferralParent = containingType; + result.deferralConstituents = propTypes; + result.deferralWriteConstituents = writeTypes; } - - /** - * Return the reduced form of the given type. For a union type, it is a union of the normalized constituent types. - * For an intersection of types containing one or more mututally exclusive discriminant properties, it is 'never'. - * For all other types, it is simply the type itself. Discriminant properties are considered mutually exclusive when - * no constituent property has type 'never', but the intersection of the constituent property types is 'never'. - */ - function getReducedType(type: ts.Type): ts.Type { - if (type.flags & ts.TypeFlags.Union && (type as ts.UnionType).objectFlags & ts.ObjectFlags.ContainsIntersections) { - return (type as ts.UnionType).resolvedReducedType || ((type as ts.UnionType).resolvedReducedType = getReducedUnionType(type as ts.UnionType)); + else { + result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes); + if (writeTypes) { + result.writeType = isUnion ? getUnionType(writeTypes) : getIntersectionType(writeTypes); } - else if (type.flags & ts.TypeFlags.Intersection) { - if (!((type as ts.IntersectionType).objectFlags & ts.ObjectFlags.IsNeverIntersectionComputed)) { - (type as ts.IntersectionType).objectFlags |= ts.ObjectFlags.IsNeverIntersectionComputed | - (ts.some(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isNeverReducedProperty) ? ts.ObjectFlags.IsNeverIntersection : 0); - } - return (type as ts.IntersectionType).objectFlags & ts.ObjectFlags.IsNeverIntersection ? neverType : type; - } - return type; } + return result; + } - function getReducedUnionType(unionType: ts.UnionType) { - const reducedTypes = ts.sameMap(unionType.types, getReducedType); - if (reducedTypes === unionType.types) { - return unionType; - } - const reduced = getUnionType(reducedTypes); - if (reduced.flags & ts.TypeFlags.Union) { - (reduced as ts.UnionType).resolvedReducedType = reduced; + // 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: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { + let property = type.propertyCacheWithoutObjectFunctionPropertyAugment?.get(name) || + !skipObjectFunctionPropertyAugment ? type.propertyCache?.get(name) : undefined; + if (!property) { + property = createUnionOrIntersectionProperty(type, name, skipObjectFunctionPropertyAugment); + if (property) { + const properties = skipObjectFunctionPropertyAugment ? + type.propertyCacheWithoutObjectFunctionPropertyAugment ||= ts.createSymbolTable() : + type.propertyCache ||= ts.createSymbolTable(); + properties.set(name, property); + } + } + return property; + } + + function getPropertyOfUnionOrIntersectionType(type: ts.UnionOrIntersectionType, name: ts.__String, skipObjectFunctionPropertyAugment?: boolean): ts.Symbol | undefined { + const property = getUnionOrIntersectionProperty(type, name, skipObjectFunctionPropertyAugment); + // We need to filter out partial properties in union types + return property && !(ts.getCheckFlags(property) & ts.CheckFlags.ReadPartial) ? property : undefined; + } + + /** + * Return the reduced form of the given type. For a union type, it is a union of the normalized constituent types. + * For an intersection of types containing one or more mututally exclusive discriminant properties, it is 'never'. + * For all other types, it is simply the type itself. Discriminant properties are considered mutually exclusive when + * no constituent property has type 'never', but the intersection of the constituent property types is 'never'. + */ + function getReducedType(type: ts.Type): ts.Type { + if (type.flags & ts.TypeFlags.Union && (type as ts.UnionType).objectFlags & ts.ObjectFlags.ContainsIntersections) { + return (type as ts.UnionType).resolvedReducedType || ((type as ts.UnionType).resolvedReducedType = getReducedUnionType(type as ts.UnionType)); + } + else if (type.flags & ts.TypeFlags.Intersection) { + if (!((type as ts.IntersectionType).objectFlags & ts.ObjectFlags.IsNeverIntersectionComputed)) { + (type as ts.IntersectionType).objectFlags |= ts.ObjectFlags.IsNeverIntersectionComputed | + (ts.some(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isNeverReducedProperty) ? ts.ObjectFlags.IsNeverIntersection : 0); } - return reduced; + return (type as ts.IntersectionType).objectFlags & ts.ObjectFlags.IsNeverIntersection ? neverType : type; } + return type; + } - function isNeverReducedProperty(prop: ts.Symbol) { - return isDiscriminantWithNeverType(prop) || isConflictingPrivateProperty(prop); + function getReducedUnionType(unionType: ts.UnionType) { + const reducedTypes = ts.sameMap(unionType.types, getReducedType); + if (reducedTypes === unionType.types) { + return unionType; } - - function isDiscriminantWithNeverType(prop: ts.Symbol) { - // Return true for a synthetic non-optional property with non-uniform types, where at least one is - // a literal type and none is never, that reduces to never. - return !(prop.flags & ts.SymbolFlags.Optional) && - (ts.getCheckFlags(prop) & (ts.CheckFlags.Discriminant | ts.CheckFlags.HasNeverType)) === ts.CheckFlags.Discriminant && - !!(getTypeOfSymbol(prop).flags & ts.TypeFlags.Never); + const reduced = getUnionType(reducedTypes); + if (reduced.flags & ts.TypeFlags.Union) { + (reduced as ts.UnionType).resolvedReducedType = reduced; } + return reduced; + } - function isConflictingPrivateProperty(prop: ts.Symbol) { - // Return true for a synthetic property with multiple declarations, at least one of which is private. - return !prop.valueDeclaration && !!(ts.getCheckFlags(prop) & ts.CheckFlags.ContainsPrivate); - } + function isNeverReducedProperty(prop: ts.Symbol) { + return isDiscriminantWithNeverType(prop) || isConflictingPrivateProperty(prop); + } - function elaborateNeverIntersection(errorInfo: ts.DiagnosticMessageChain | undefined, type: ts.Type) { - if (type.flags & ts.TypeFlags.Intersection && ts.getObjectFlags(type) & ts.ObjectFlags.IsNeverIntersection) { - const neverProp = ts.find(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isDiscriminantWithNeverType); - if (neverProp) { - return ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.The_intersection_0_was_reduced_to_never_because_property_1_has_conflicting_types_in_some_constituents, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTypeReduction), symbolToString(neverProp)); - } - const privateProp = ts.find(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isConflictingPrivateProperty); - if (privateProp) { - return ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.The_intersection_0_was_reduced_to_never_because_property_1_exists_in_multiple_constituents_and_is_private_in_some, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTypeReduction), symbolToString(privateProp)); - } + function isDiscriminantWithNeverType(prop: ts.Symbol) { + // Return true for a synthetic non-optional property with non-uniform types, where at least one is + // a literal type and none is never, that reduces to never. + return !(prop.flags & ts.SymbolFlags.Optional) && + (ts.getCheckFlags(prop) & (ts.CheckFlags.Discriminant | ts.CheckFlags.HasNeverType)) === ts.CheckFlags.Discriminant && + !!(getTypeOfSymbol(prop).flags & ts.TypeFlags.Never); + } + + function isConflictingPrivateProperty(prop: ts.Symbol) { + // Return true for a synthetic property with multiple declarations, at least one of which is private. + return !prop.valueDeclaration && !!(ts.getCheckFlags(prop) & ts.CheckFlags.ContainsPrivate); + } + + function elaborateNeverIntersection(errorInfo: ts.DiagnosticMessageChain | undefined, type: ts.Type) { + if (type.flags & ts.TypeFlags.Intersection && ts.getObjectFlags(type) & ts.ObjectFlags.IsNeverIntersection) { + const neverProp = ts.find(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isDiscriminantWithNeverType); + if (neverProp) { + return ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.The_intersection_0_was_reduced_to_never_because_property_1_has_conflicting_types_in_some_constituents, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTypeReduction), symbolToString(neverProp)); + } + const privateProp = ts.find(getPropertiesOfUnionOrIntersectionType(type as ts.IntersectionType), isConflictingPrivateProperty); + if (privateProp) { + return ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.The_intersection_0_was_reduced_to_never_because_property_1_exists_in_multiple_constituents_and_is_private_in_some, typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTypeReduction), symbolToString(privateProp)); } - return errorInfo; } + return errorInfo; + } - /** - * 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: ts.__String, skipObjectFunctionPropertyAugment?: boolean, includeTypeOnlyMembers?: boolean): ts.Symbol | undefined { - type = getReducedApparentType(type); - if (type.flags & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - const symbol = resolved.members.get(name); - if (symbol && symbolIsValue(symbol, includeTypeOnlyMembers)) { - return symbol; - } - if (skipObjectFunctionPropertyAugment) - return undefined; - 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); + /** + * 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: ts.__String, skipObjectFunctionPropertyAugment?: boolean, includeTypeOnlyMembers?: boolean): ts.Symbol | undefined { + type = getReducedApparentType(type); + if (type.flags & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + const symbol = resolved.members.get(name); + if (symbol && symbolIsValue(symbol, includeTypeOnlyMembers)) { + return symbol; } - if (type.flags & ts.TypeFlags.UnionOrIntersection) { - return getPropertyOfUnionOrIntersectionType(type as ts.UnionOrIntersectionType, name, skipObjectFunctionPropertyAugment); + if (skipObjectFunctionPropertyAugment) + return undefined; + 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 undefined; + return getPropertyOfObjectType(globalObjectType, name); } - - function getSignaturesOfStructuredType(type: ts.Type, kind: ts.SignatureKind): readonly ts.Signature[] { - if (type.flags & ts.TypeFlags.StructuredType) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - return kind === ts.SignatureKind.Call ? resolved.callSignatures : resolved.constructSignatures; - } - return ts.emptyArray; + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + return getPropertyOfUnionOrIntersectionType(type as ts.UnionOrIntersectionType, name, skipObjectFunctionPropertyAugment); } + return undefined; + } - /** - * 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: ts.SignatureKind): readonly ts.Signature[] { - return getSignaturesOfStructuredType(getReducedApparentType(type), kind); + function getSignaturesOfStructuredType(type: ts.Type, kind: ts.SignatureKind): readonly ts.Signature[] { + if (type.flags & ts.TypeFlags.StructuredType) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + return kind === ts.SignatureKind.Call ? resolved.callSignatures : resolved.constructSignatures; } + return ts.emptyArray; + } - function findIndexInfo(indexInfos: readonly ts.IndexInfo[], keyType: ts.Type) { - return ts.find(indexInfos, info => info.keyType === keyType); - } + /** + * 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: ts.SignatureKind): readonly ts.Signature[] { + return getSignaturesOfStructuredType(getReducedApparentType(type), kind); + } - function findApplicableIndexInfo(indexInfos: readonly ts.IndexInfo[], keyType: ts.Type) { - // Index signatures for type 'string' are considered only when no other index signatures apply. - let stringIndexInfo: ts.IndexInfo | undefined; - let applicableInfo: ts.IndexInfo | undefined; - let applicableInfos: ts.IndexInfo[] | undefined; - for (const info of indexInfos) { - if (info.keyType === stringType) { - stringIndexInfo = info; + function findIndexInfo(indexInfos: readonly ts.IndexInfo[], keyType: ts.Type) { + return ts.find(indexInfos, info => info.keyType === keyType); + } + + function findApplicableIndexInfo(indexInfos: readonly ts.IndexInfo[], keyType: ts.Type) { + // Index signatures for type 'string' are considered only when no other index signatures apply. + let stringIndexInfo: ts.IndexInfo | undefined; + let applicableInfo: ts.IndexInfo | undefined; + let applicableInfos: ts.IndexInfo[] | undefined; + for (const info of indexInfos) { + if (info.keyType === stringType) { + stringIndexInfo = info; + } + else if (isApplicableIndexType(keyType, info.keyType)) { + if (!applicableInfo) { + applicableInfo = info; } - else if (isApplicableIndexType(keyType, info.keyType)) { - if (!applicableInfo) { - applicableInfo = info; - } - else { - (applicableInfos || (applicableInfos = [applicableInfo])).push(info); - } + else { + (applicableInfos || (applicableInfos = [applicableInfo])).push(info); } } - // When more than one index signature is applicable we create a synthetic IndexInfo. Instead of computing - // the intersected key type, we just use unknownType for the key type as nothing actually depends on the - // keyType property of the returned IndexInfo. - return applicableInfos ? createIndexInfo(unknownType, getIntersectionType(ts.map(applicableInfos, info => info.type)), ts.reduceLeft(applicableInfos, (isReadonly, info) => isReadonly && info.isReadonly, /*initial*/ true)) : - applicableInfo ? applicableInfo : - stringIndexInfo && isApplicableIndexType(keyType, stringType) ? stringIndexInfo : - undefined; } + // When more than one index signature is applicable we create a synthetic IndexInfo. Instead of computing + // the intersected key type, we just use unknownType for the key type as nothing actually depends on the + // keyType property of the returned IndexInfo. + return applicableInfos ? createIndexInfo(unknownType, getIntersectionType(ts.map(applicableInfos, info => info.type)), ts.reduceLeft(applicableInfos, (isReadonly, info) => isReadonly && info.isReadonly, /*initial*/ true)) : + applicableInfo ? applicableInfo : + stringIndexInfo && isApplicableIndexType(keyType, stringType) ? stringIndexInfo : + undefined; + } - function isApplicableIndexType(source: ts.Type, target: ts.Type): boolean { - // A 'string' index signature applies to types assignable to 'string' or 'number', and a 'number' index - // signature applies to types assignable to 'number', `${number}` and numeric string literal types. - return isTypeAssignableTo(source, target) || - target === stringType && isTypeAssignableTo(source, numberType) || - target === numberType && (source === numericStringType || !!(source.flags & ts.TypeFlags.StringLiteral) && ts.isNumericLiteralName((source as ts.StringLiteralType).value)); - } + function isApplicableIndexType(source: ts.Type, target: ts.Type): boolean { + // A 'string' index signature applies to types assignable to 'string' or 'number', and a 'number' index + // signature applies to types assignable to 'number', `${number}` and numeric string literal types. + return isTypeAssignableTo(source, target) || + target === stringType && isTypeAssignableTo(source, numberType) || + target === numberType && (source === numericStringType || !!(source.flags & ts.TypeFlags.StringLiteral) && ts.isNumericLiteralName((source as ts.StringLiteralType).value)); + } - function getIndexInfosOfStructuredType(type: ts.Type): readonly ts.IndexInfo[] { - if (type.flags & ts.TypeFlags.StructuredType) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - return resolved.indexInfos; - } - return ts.emptyArray; + function getIndexInfosOfStructuredType(type: ts.Type): readonly ts.IndexInfo[] { + if (type.flags & ts.TypeFlags.StructuredType) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + return resolved.indexInfos; } + return ts.emptyArray; + } - function getIndexInfosOfType(type: ts.Type): readonly ts.IndexInfo[] { - return getIndexInfosOfStructuredType(getReducedApparentType(type)); - } + function getIndexInfosOfType(type: ts.Type): readonly ts.IndexInfo[] { + return getIndexInfosOfStructuredType(getReducedApparentType(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, keyType: ts.Type): ts.IndexInfo | undefined { - return findIndexInfo(getIndexInfosOfType(type), keyType); - } + // 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, keyType: ts.Type): ts.IndexInfo | undefined { + return findIndexInfo(getIndexInfosOfType(type), keyType); + } - // 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, keyType: ts.Type): ts.Type | undefined { - return getIndexInfoOfType(type, keyType)?.type; - } + // 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, keyType: ts.Type): ts.Type | undefined { + return getIndexInfoOfType(type, keyType)?.type; + } - function getApplicableIndexInfos(type: ts.Type, keyType: ts.Type): ts.IndexInfo[] { - return getIndexInfosOfType(type).filter(info => isApplicableIndexType(keyType, info.keyType)); - } + function getApplicableIndexInfos(type: ts.Type, keyType: ts.Type): ts.IndexInfo[] { + return getIndexInfosOfType(type).filter(info => isApplicableIndexType(keyType, info.keyType)); + } - function getApplicableIndexInfo(type: ts.Type, keyType: ts.Type): ts.IndexInfo | undefined { - return findApplicableIndexInfo(getIndexInfosOfType(type), keyType); - } + function getApplicableIndexInfo(type: ts.Type, keyType: ts.Type): ts.IndexInfo | undefined { + return findApplicableIndexInfo(getIndexInfosOfType(type), keyType); + } - function getApplicableIndexInfoForName(type: ts.Type, name: ts.__String): ts.IndexInfo | undefined { - return getApplicableIndexInfo(type, isLateBoundName(name) ? esSymbolType : getStringLiteralType(ts.unescapeLeadingUnderscores(name))); + function getApplicableIndexInfoForName(type: ts.Type, name: ts.__String): ts.IndexInfo | undefined { + return getApplicableIndexInfo(type, isLateBoundName(name) ? esSymbolType : getStringLiteralType(ts.unescapeLeadingUnderscores(name))); + } + + // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual + // type checking functions). + function getTypeParametersFromDeclaration(declaration: ts.DeclarationWithTypeParameters): ts.TypeParameter[] | undefined { + let result: ts.TypeParameter[] | undefined; + for (const node of ts.getEffectiveTypeParameterDeclarations(declaration)) { + result = ts.appendIfUnique(result, getDeclaredTypeOfTypeParameter(node.symbol)); } + return result; + } - // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual - // type checking functions). - function getTypeParametersFromDeclaration(declaration: ts.DeclarationWithTypeParameters): ts.TypeParameter[] | undefined { - let result: ts.TypeParameter[] | undefined; - for (const node of ts.getEffectiveTypeParameterDeclarations(declaration)) { - result = ts.appendIfUnique(result, getDeclaredTypeOfTypeParameter(node.symbol)); + function symbolsToArray(symbols: ts.SymbolTable): ts.Symbol[] { + const result: ts.Symbol[] = []; + symbols.forEach((symbol, id) => { + if (!isReservedMemberName(id)) { + result.push(symbol); } - return result; - } + }); + return result; + } - function symbolsToArray(symbols: ts.SymbolTable): ts.Symbol[] { - const result: ts.Symbol[] = []; - symbols.forEach((symbol, id) => { - if (!isReservedMemberName(id)) { - result.push(symbol); - } - }); - return result; + function isJSDocOptionalParameter(node: ts.ParameterDeclaration) { + return ts.isInJSFile(node) && ( + // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType + node.type && node.type.kind === ts.SyntaxKind.JSDocOptionalType + || ts.getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => isBracketed || !!typeExpression && typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType)); + } + + function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { + if (ts.isExternalModuleNameRelative(moduleName)) { + return undefined; } + const symbol = getSymbol(globals, '"' + moduleName + '"' as ts.__String, ts.SymbolFlags.ValueModule); + // merged symbol is module declaration symbol combined with all augmentations + return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; + } - function isJSDocOptionalParameter(node: ts.ParameterDeclaration) { - return ts.isInJSFile(node) && ( - // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType - node.type && node.type.kind === ts.SyntaxKind.JSDocOptionalType - || ts.getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => isBracketed || !!typeExpression && typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType)); + function isOptionalParameter(node: ts.ParameterDeclaration | ts.JSDocParameterTag | ts.JSDocPropertyTag) { + if (ts.hasQuestionToken(node) || isOptionalJSDocPropertyLikeTag(node) || isJSDocOptionalParameter(node)) { + return true; } - function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { - if (ts.isExternalModuleNameRelative(moduleName)) { - return undefined; - } - const symbol = getSymbol(globals, '"' + moduleName + '"' as ts.__String, ts.SymbolFlags.ValueModule); - // merged symbol is module declaration symbol combined with all augmentations - return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; + if (node.initializer) { + const signature = getSignatureFromDeclaration(node.parent); + const parameterIndex = node.parent.parameters.indexOf(node); + ts.Debug.assert(parameterIndex >= 0); + // Only consider syntactic or instantiated parameters as optional, not `void` parameters as this function is used + // in grammar checks and checking for `void` too early results in parameter types widening too early + // and causes some noImplicitAny errors to be lost. + return parameterIndex >= getMinArgumentCount(signature, MinArgumentCountFlags.StrongArityForUntypedJS | MinArgumentCountFlags.VoidIsNonOptional); + } + const iife = ts.getImmediatelyInvokedFunctionExpression(node.parent); + if (iife) { + return !node.type && + !node.dotDotDotToken && + node.parent.parameters.indexOf(node) >= iife.arguments.length; } - function isOptionalParameter(node: ts.ParameterDeclaration | ts.JSDocParameterTag | ts.JSDocPropertyTag) { - if (ts.hasQuestionToken(node) || isOptionalJSDocPropertyLikeTag(node) || isJSDocOptionalParameter(node)) { - return true; - } + return false; + } - if (node.initializer) { - const signature = getSignatureFromDeclaration(node.parent); - const parameterIndex = node.parent.parameters.indexOf(node); - ts.Debug.assert(parameterIndex >= 0); - // Only consider syntactic or instantiated parameters as optional, not `void` parameters as this function is used - // in grammar checks and checking for `void` too early results in parameter types widening too early - // and causes some noImplicitAny errors to be lost. - return parameterIndex >= getMinArgumentCount(signature, MinArgumentCountFlags.StrongArityForUntypedJS | MinArgumentCountFlags.VoidIsNonOptional); - } - const iife = ts.getImmediatelyInvokedFunctionExpression(node.parent); - if (iife) { - return !node.type && - !node.dotDotDotToken && - node.parent.parameters.indexOf(node) >= iife.arguments.length; - } + function isOptionalPropertyDeclaration(node: ts.Declaration) { + return ts.isPropertyDeclaration(node) && node.questionToken; + } + function isOptionalJSDocPropertyLikeTag(node: ts.Node): node is ts.JSDocPropertyLikeTag { + if (!ts.isJSDocPropertyLikeTag(node)) { return false; } + const { isBracketed, typeExpression } = node; + return isBracketed || !!typeExpression && typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType; + } - function isOptionalPropertyDeclaration(node: ts.Declaration) { - return ts.isPropertyDeclaration(node) && node.questionToken; - } + function createTypePredicate(kind: ts.TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: ts.Type | undefined): ts.TypePredicate { + return { kind, parameterName, parameterIndex, type } as ts.TypePredicate; + } - function isOptionalJSDocPropertyLikeTag(node: ts.Node): node is ts.JSDocPropertyLikeTag { - if (!ts.isJSDocPropertyLikeTag(node)) { - return false; + /** + * Gets the minimum number of type arguments needed to satisfy all non-optional type + * parameters. + */ + function getMinTypeArgumentCount(typeParameters: readonly ts.TypeParameter[] | undefined): number { + let minTypeArgumentCount = 0; + if (typeParameters) { + for (let i = 0; i < typeParameters.length; i++) { + if (!hasTypeParameterDefault(typeParameters[i])) { + minTypeArgumentCount = i + 1; + } } - const { isBracketed, typeExpression } = node; - return isBracketed || !!typeExpression && typeExpression.type.kind === ts.SyntaxKind.JSDocOptionalType; } + return minTypeArgumentCount; + } - function createTypePredicate(kind: ts.TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: ts.Type | undefined): ts.TypePredicate { - return { kind, parameterName, parameterIndex, type } as ts.TypePredicate; + /** + * 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 ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[]; + function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[] | undefined; + function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean) { + const numTypeParameters = ts.length(typeParameters); + if (!numTypeParameters) { + return []; + } + const numTypeArguments = ts.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(); + } - /** - * Gets the minimum number of type arguments needed to satisfy all non-optional type - * parameters. - */ - function getMinTypeArgumentCount(typeParameters: readonly ts.TypeParameter[] | undefined): number { - let minTypeArgumentCount = 0; - if (typeParameters) { - for (let i = 0; i < typeParameters.length; i++) { - if (!hasTypeParameterDefault(typeParameters[i])) { - minTypeArgumentCount = i + 1; - } + function getSignatureFromDeclaration(declaration: ts.SignatureDeclaration | ts.JSDocSignature): ts.Signature { + const links = getNodeLinks(declaration); + if (!links.resolvedSignature) { + const parameters: ts.Symbol[] = []; + let flags = ts.SignatureFlags.None; + let minArgumentCount = 0; + let thisParameter: ts.Symbol | undefined; + let hasThisParameter = false; + const iife = ts.getImmediatelyInvokedFunctionExpression(declaration); + const isJSConstructSignature = ts.isJSDocConstructSignature(declaration); + const isUntypedSignatureInJSFile = !iife && + ts.isInJSFile(declaration) && + ts.isValueSignatureDeclaration(declaration) && + !ts.hasJSDocParameterTags(declaration) && + !ts.getJSDocType(declaration); + if (isUntypedSignatureInJSFile) { + flags |= ts.SignatureFlags.IsUntypedSignatureInJSFile; + } + + // 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 = ts.isJSDocParameterTag(param) ? (param.typeExpression && param.typeExpression.type) : param.type; + // Include parameter symbol instead of property symbol in the signature + if (paramSymbol && !!(paramSymbol.flags & ts.SymbolFlags.Property) && !ts.isBindingPattern(param.name)) { + const resolvedSymbol = resolveName(param, paramSymbol.escapedName, ts.SymbolFlags.Value, undefined, undefined, /*isUse*/ false); + paramSymbol = resolvedSymbol!; + } + if (i === 0 && paramSymbol.escapedName === ts.InternalSymbolName.This) { + hasThisParameter = true; + thisParameter = param.symbol; } - } - 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 ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[]; - function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[] | undefined; - function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly ts.TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean) { - const numTypeParameters = ts.length(typeParameters); - if (!numTypeParameters) { - return []; - } - const numTypeArguments = ts.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: ts.SignatureDeclaration | ts.JSDocSignature): ts.Signature { - const links = getNodeLinks(declaration); - if (!links.resolvedSignature) { - const parameters: ts.Symbol[] = []; - let flags = ts.SignatureFlags.None; - let minArgumentCount = 0; - let thisParameter: ts.Symbol | undefined; - let hasThisParameter = false; - const iife = ts.getImmediatelyInvokedFunctionExpression(declaration); - const isJSConstructSignature = ts.isJSDocConstructSignature(declaration); - const isUntypedSignatureInJSFile = !iife && - ts.isInJSFile(declaration) && - ts.isValueSignatureDeclaration(declaration) && - !ts.hasJSDocParameterTags(declaration) && - !ts.getJSDocType(declaration); - if (isUntypedSignatureInJSFile) { - flags |= ts.SignatureFlags.IsUntypedSignatureInJSFile; - } - - // 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 = ts.isJSDocParameterTag(param) ? (param.typeExpression && param.typeExpression.type) : param.type; - // Include parameter symbol instead of property symbol in the signature - if (paramSymbol && !!(paramSymbol.flags & ts.SymbolFlags.Property) && !ts.isBindingPattern(param.name)) { - const resolvedSymbol = resolveName(param, paramSymbol.escapedName, ts.SymbolFlags.Value, undefined, undefined, /*isUse*/ false); - paramSymbol = resolvedSymbol!; - } - if (i === 0 && paramSymbol.escapedName === ts.InternalSymbolName.This) { - hasThisParameter = true; - thisParameter = param.symbol; - } - else { - parameters.push(paramSymbol); - } - - if (type && type.kind === ts.SyntaxKind.LiteralType) { - flags |= ts.SignatureFlags.HasLiteralTypes; - } - - // Record a new minimum argument count if this is not an optional parameter - const isOptionalParameter = isOptionalJSDocPropertyLikeTag(param) || - param.initializer || param.questionToken || ts.isRestParameter(param) || - iife && parameters.length > iife.arguments.length && !type || - isJSDocOptionalParameter(param); - if (!isOptionalParameter) { - minArgumentCount = parameters.length; - } + else { + parameters.push(paramSymbol); } - // If only one accessor includes a this-type annotation, the other behaves as if it had the same type annotation - if ((declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) && - hasBindableName(declaration) && - (!hasThisParameter || !thisParameter)) { - const otherKind = declaration.kind === ts.SyntaxKind.GetAccessor ? ts.SyntaxKind.SetAccessor : ts.SyntaxKind.GetAccessor; - const other = ts.getDeclarationOfKind(getSymbolOfNode(declaration), otherKind); - if (other) { - thisParameter = getAnnotatedAccessorThisParameter(other); - } + if (type && type.kind === ts.SyntaxKind.LiteralType) { + flags |= ts.SignatureFlags.HasLiteralTypes; } - const classType = declaration.kind === ts.SyntaxKind.Constructor ? - getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent as ts.ClassDeclaration).symbol)) - : undefined; - const typeParameters = classType ? classType.localTypeParameters : getTypeParametersFromDeclaration(declaration); - if (ts.hasRestParameter(declaration) || ts.isInJSFile(declaration) && maybeAddJsSyntheticRestParameter(declaration, parameters)) { - flags |= ts.SignatureFlags.HasRestParameter; + // Record a new minimum argument count if this is not an optional parameter + const isOptionalParameter = isOptionalJSDocPropertyLikeTag(param) || + param.initializer || param.questionToken || ts.isRestParameter(param) || + iife && parameters.length > iife.arguments.length && !type || + isJSDocOptionalParameter(param); + if (!isOptionalParameter) { + minArgumentCount = parameters.length; } - if (ts.isConstructorTypeNode(declaration) && ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Abstract) || - ts.isConstructorDeclaration(declaration) && ts.hasSyntacticModifier(declaration.parent, ts.ModifierFlags.Abstract)) { - flags |= ts.SignatureFlags.Abstract; + } + + // If only one accessor includes a this-type annotation, the other behaves as if it had the same type annotation + if ((declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) && + hasBindableName(declaration) && + (!hasThisParameter || !thisParameter)) { + const otherKind = declaration.kind === ts.SyntaxKind.GetAccessor ? ts.SyntaxKind.SetAccessor : ts.SyntaxKind.GetAccessor; + const other = ts.getDeclarationOfKind(getSymbolOfNode(declaration), otherKind); + if (other) { + thisParameter = getAnnotatedAccessorThisParameter(other); } - 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: ts.SignatureDeclaration | ts.JSDocSignature, parameters: ts.Symbol[]): boolean { - if (ts.isJSDocSignature(declaration) || !containsArgumentsReference(declaration)) { - return false; + const classType = declaration.kind === ts.SyntaxKind.Constructor ? + getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent as ts.ClassDeclaration).symbol)) + : undefined; + const typeParameters = classType ? classType.localTypeParameters : getTypeParametersFromDeclaration(declaration); + if (ts.hasRestParameter(declaration) || ts.isInJSFile(declaration) && maybeAddJsSyntheticRestParameter(declaration, parameters)) { + flags |= ts.SignatureFlags.HasRestParameter; } - const lastParam = ts.lastOrUndefined(declaration.parameters); - const lastParamTags = lastParam ? ts.getJSDocParameterTags(lastParam) : ts.getJSDocTags(declaration).filter(ts.isJSDocParameterTag); - const lastParamVariadicType = ts.firstDefined(lastParamTags, p => p.typeExpression && ts.isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); - const syntheticArgsSymbol = createSymbol(ts.SymbolFlags.Variable, "args" as ts.__String, ts.CheckFlags.RestParameter); - if (lastParamVariadicType) { - // Parameter has effective annotation, lock in type - syntheticArgsSymbol.type = createArrayType(getTypeFromTypeNode(lastParamVariadicType.type)); + if (ts.isConstructorTypeNode(declaration) && ts.hasSyntacticModifier(declaration, ts.ModifierFlags.Abstract) || + ts.isConstructorDeclaration(declaration) && ts.hasSyntacticModifier(declaration.parent, ts.ModifierFlags.Abstract)) { + flags |= ts.SignatureFlags.Abstract; } - else { - // Parameter has no annotation - // By using a `DeferredType` symbol, we allow the type of this rest arg to be overriden by contextual type assignment so long as its type hasn't been - // cached by `getTypeOfSymbol` yet. - syntheticArgsSymbol.checkFlags |= ts.CheckFlags.DeferredType; - syntheticArgsSymbol.deferralParent = neverType; - syntheticArgsSymbol.deferralConstituents = [anyArrayType]; - syntheticArgsSymbol.deferralWriteConstituents = [anyArrayType]; - } - if (lastParamVariadicType) { - // Replace the last parameter with a rest parameter. - parameters.pop(); - } - parameters.push(syntheticArgsSymbol); - return true; + links.resolvedSignature = createSignature(declaration, typeParameters, thisParameter, parameters, + /*resolvedReturnType*/ undefined, /*resolvedTypePredicate*/ undefined, minArgumentCount, flags); } + return links.resolvedSignature; + } - function getSignatureOfTypeTag(node: ts.SignatureDeclaration | ts.JSDocSignature) { - // should be attached to a function declaration or expression - if (!(ts.isInJSFile(node) && ts.isFunctionLikeDeclaration(node))) - return undefined; - const typeTag = ts.getJSDocTypeTag(node); - return typeTag?.typeExpression && getSingleCallSignature(getTypeFromTypeNode(typeTag.typeExpression)); + /** + * 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: ts.SignatureDeclaration | ts.JSDocSignature, parameters: ts.Symbol[]): boolean { + if (ts.isJSDocSignature(declaration) || !containsArgumentsReference(declaration)) { + return false; } + const lastParam = ts.lastOrUndefined(declaration.parameters); + const lastParamTags = lastParam ? ts.getJSDocParameterTags(lastParam) : ts.getJSDocTags(declaration).filter(ts.isJSDocParameterTag); + const lastParamVariadicType = ts.firstDefined(lastParamTags, p => p.typeExpression && ts.isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); + const syntheticArgsSymbol = createSymbol(ts.SymbolFlags.Variable, "args" as ts.__String, ts.CheckFlags.RestParameter); + if (lastParamVariadicType) { + // Parameter has effective annotation, lock in type + syntheticArgsSymbol.type = createArrayType(getTypeFromTypeNode(lastParamVariadicType.type)); + } + else { + // Parameter has no annotation + // By using a `DeferredType` symbol, we allow the type of this rest arg to be overriden by contextual type assignment so long as its type hasn't been + // cached by `getTypeOfSymbol` yet. + syntheticArgsSymbol.checkFlags |= ts.CheckFlags.DeferredType; + syntheticArgsSymbol.deferralParent = neverType; + syntheticArgsSymbol.deferralConstituents = [anyArrayType]; + syntheticArgsSymbol.deferralWriteConstituents = [anyArrayType]; + } + if (lastParamVariadicType) { + // Replace the last parameter with a rest parameter. + parameters.pop(); + } + parameters.push(syntheticArgsSymbol); + return true; + } - function getParameterTypeOfTypeTag(func: ts.FunctionLikeDeclaration, parameter: ts.ParameterDeclaration) { - const signature = getSignatureOfTypeTag(func); - if (!signature) - return undefined; - const pos = func.parameters.indexOf(parameter); - return parameter.dotDotDotToken ? getRestTypeAtPosition(signature, pos) : getTypeAtPosition(signature, pos); - } + function getSignatureOfTypeTag(node: ts.SignatureDeclaration | ts.JSDocSignature) { + // should be attached to a function declaration or expression + if (!(ts.isInJSFile(node) && ts.isFunctionLikeDeclaration(node))) + return undefined; + const typeTag = ts.getJSDocTypeTag(node); + return typeTag?.typeExpression && getSingleCallSignature(getTypeFromTypeNode(typeTag.typeExpression)); + } - function getReturnTypeOfTypeTag(node: ts.SignatureDeclaration | ts.JSDocSignature) { - const signature = getSignatureOfTypeTag(node); - return signature && getReturnTypeOfSignature(signature); - } + function getParameterTypeOfTypeTag(func: ts.FunctionLikeDeclaration, parameter: ts.ParameterDeclaration) { + const signature = getSignatureOfTypeTag(func); + if (!signature) + return undefined; + const pos = func.parameters.indexOf(parameter); + return parameter.dotDotDotToken ? getRestTypeAtPosition(signature, pos) : getTypeAtPosition(signature, pos); + } - function containsArgumentsReference(declaration: ts.SignatureDeclaration): boolean { - const links = getNodeLinks(declaration); - if (links.containsArgumentsReference === undefined) { - if (links.flags & ts.NodeCheckFlags.CaptureArguments) { - links.containsArgumentsReference = true; - } - else { - links.containsArgumentsReference = traverse((declaration as ts.FunctionLikeDeclaration).body!); - } + function getReturnTypeOfTypeTag(node: ts.SignatureDeclaration | ts.JSDocSignature) { + const signature = getSignatureOfTypeTag(node); + return signature && getReturnTypeOfSignature(signature); + } + + function containsArgumentsReference(declaration: ts.SignatureDeclaration): boolean { + const links = getNodeLinks(declaration); + if (links.containsArgumentsReference === undefined) { + if (links.flags & ts.NodeCheckFlags.CaptureArguments) { + links.containsArgumentsReference = true; + } + else { + links.containsArgumentsReference = traverse((declaration as ts.FunctionLikeDeclaration).body!); } - return links.containsArgumentsReference; + } + return links.containsArgumentsReference; - function traverse(node: ts.Node): boolean { - if (!node) - return false; - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return (node as ts.Identifier).escapedText === argumentsSymbol.escapedName && getReferencedValueSymbol(node as ts.Identifier) === argumentsSymbol; - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return (node as ts.NamedDeclaration).name!.kind === ts.SyntaxKind.ComputedPropertyName - && traverse((node as ts.NamedDeclaration).name!); - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return traverse((node as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression); - case ts.SyntaxKind.PropertyAssignment: - return traverse((node as ts.PropertyAssignment).initializer); + function traverse(node: ts.Node): boolean { + if (!node) + return false; + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return (node as ts.Identifier).escapedText === argumentsSymbol.escapedName && getReferencedValueSymbol(node as ts.Identifier) === argumentsSymbol; + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return (node as ts.NamedDeclaration).name!.kind === ts.SyntaxKind.ComputedPropertyName + && traverse((node as ts.NamedDeclaration).name!); + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return traverse((node as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression); + case ts.SyntaxKind.PropertyAssignment: + return traverse((node as ts.PropertyAssignment).initializer); - default: - return !ts.nodeStartsNewLexicalEnvironment(node) && !ts.isPartOfTypeNode(node) && !!ts.forEachChild(node, traverse); - } + default: + return !ts.nodeStartsNewLexicalEnvironment(node) && !ts.isPartOfTypeNode(node) && !!ts.forEachChild(node, traverse); } } + } - function getSignaturesOfSymbol(symbol: ts.Symbol | undefined): ts.Signature[] { - if (!symbol || !symbol.declarations) - return ts.emptyArray; - const result: ts.Signature[] = []; - for (let i = 0; i < symbol.declarations.length; i++) { - const decl = symbol.declarations[i]; - if (!ts.isFunctionLike(decl)) + function getSignaturesOfSymbol(symbol: ts.Symbol | undefined): ts.Signature[] { + if (!symbol || !symbol.declarations) + return ts.emptyArray; + const result: ts.Signature[] = []; + for (let i = 0; i < symbol.declarations.length; i++) { + const decl = symbol.declarations[i]; + if (!ts.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 ts.FunctionLikeDeclaration).body) { + const previous = symbol.declarations[i - 1]; + if (decl.parent === previous.parent && decl.kind === previous.kind && decl.pos === previous.end) { 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 ts.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; + result.push(getSignatureFromDeclaration(decl)); } + return result; + } - function resolveExternalModuleTypeByLiteral(name: ts.StringLiteral) { - const moduleSym = resolveExternalModuleName(name, name); - if (moduleSym) { - const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); - if (resolvedModuleSymbol) { - return getTypeOfSymbol(resolvedModuleSymbol); - } + function resolveExternalModuleTypeByLiteral(name: ts.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); - } + return anyType; + } + + function getThisTypeOfSignature(signature: ts.Signature): ts.Type | undefined { + if (signature.thisParameter) { + return getTypeOfSymbol(signature.thisParameter); } + } - function getTypePredicateOfSignature(signature: ts.Signature): ts.TypePredicate | undefined { - if (!signature.resolvedTypePredicate) { - if (signature.target) { - const targetTypePredicate = getTypePredicateOfSignature(signature.target); - signature.resolvedTypePredicate = targetTypePredicate ? instantiateTypePredicate(targetTypePredicate, signature.mapper!) : noTypePredicate; - } - else if (signature.compositeSignatures) { - signature.resolvedTypePredicate = getUnionOrIntersectionTypePredicate(signature.compositeSignatures, signature.compositeKind) || noTypePredicate; - } - else { - const type = signature.declaration && ts.getEffectiveReturnTypeNode(signature.declaration); - let jsdocPredicate: ts.TypePredicate | undefined; - if (!type && ts.isInJSFile(signature.declaration)) { - const jsdocSignature = getSignatureOfTypeTag(signature.declaration!); - if (jsdocSignature && signature !== jsdocSignature) { - jsdocPredicate = getTypePredicateOfSignature(jsdocSignature); - } + function getTypePredicateOfSignature(signature: ts.Signature): ts.TypePredicate | undefined { + if (!signature.resolvedTypePredicate) { + if (signature.target) { + const targetTypePredicate = getTypePredicateOfSignature(signature.target); + signature.resolvedTypePredicate = targetTypePredicate ? instantiateTypePredicate(targetTypePredicate, signature.mapper!) : noTypePredicate; + } + else if (signature.compositeSignatures) { + signature.resolvedTypePredicate = getUnionOrIntersectionTypePredicate(signature.compositeSignatures, signature.compositeKind) || noTypePredicate; + } + else { + const type = signature.declaration && ts.getEffectiveReturnTypeNode(signature.declaration); + let jsdocPredicate: ts.TypePredicate | undefined; + if (!type && ts.isInJSFile(signature.declaration)) { + const jsdocSignature = getSignatureOfTypeTag(signature.declaration!); + if (jsdocSignature && signature !== jsdocSignature) { + jsdocPredicate = getTypePredicateOfSignature(jsdocSignature); } - signature.resolvedTypePredicate = type && ts.isTypePredicateNode(type) ? - createTypePredicateFromTypePredicateNode(type, signature) : - jsdocPredicate || noTypePredicate; } - ts.Debug.assert(!!signature.resolvedTypePredicate); + signature.resolvedTypePredicate = type && ts.isTypePredicateNode(type) ? + createTypePredicateFromTypePredicateNode(type, signature) : + jsdocPredicate || noTypePredicate; } - return signature.resolvedTypePredicate === noTypePredicate ? undefined : signature.resolvedTypePredicate; + ts.Debug.assert(!!signature.resolvedTypePredicate); } + return signature.resolvedTypePredicate === noTypePredicate ? undefined : signature.resolvedTypePredicate; + } - function createTypePredicateFromTypePredicateNode(node: ts.TypePredicateNode, signature: ts.Signature): ts.TypePredicate { - const parameterName = node.parameterName; - const type = node.type && getTypeFromTypeNode(node.type); - return parameterName.kind === ts.SyntaxKind.ThisType ? - createTypePredicate(node.assertsModifier ? ts.TypePredicateKind.AssertsThis : ts.TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type) : - createTypePredicate(node.assertsModifier ? ts.TypePredicateKind.AssertsIdentifier : ts.TypePredicateKind.Identifier, parameterName.escapedText as string, ts.findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type); - } + function createTypePredicateFromTypePredicateNode(node: ts.TypePredicateNode, signature: ts.Signature): ts.TypePredicate { + const parameterName = node.parameterName; + const type = node.type && getTypeFromTypeNode(node.type); + return parameterName.kind === ts.SyntaxKind.ThisType ? + createTypePredicate(node.assertsModifier ? ts.TypePredicateKind.AssertsThis : ts.TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type) : + createTypePredicate(node.assertsModifier ? ts.TypePredicateKind.AssertsIdentifier : ts.TypePredicateKind.Identifier, parameterName.escapedText as string, ts.findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type); + } - function getUnionOrIntersectionType(types: ts.Type[], kind: ts.TypeFlags | undefined, unionReduction?: ts.UnionReduction) { - return kind !== ts.TypeFlags.Intersection ? getUnionType(types, unionReduction) : getIntersectionType(types); - } + function getUnionOrIntersectionType(types: ts.Type[], kind: ts.TypeFlags | undefined, unionReduction?: ts.UnionReduction) { + return kind !== ts.TypeFlags.Intersection ? getUnionType(types, unionReduction) : getIntersectionType(types); + } - function getReturnTypeOfSignature(signature: ts.Signature): ts.Type { - if (!signature.resolvedReturnType) { - if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { - return errorType; - } - let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) : - signature.compositeSignatures ? instantiateType(getUnionOrIntersectionType(ts.map(signature.compositeSignatures, getReturnTypeOfSignature), signature.compositeKind, ts.UnionReduction.Subtype), signature.mapper) : - getReturnTypeFromAnnotation(signature.declaration!) || - (ts.nodeIsMissing((signature.declaration as ts.FunctionLikeDeclaration).body) ? anyType : getReturnTypeFromBody(signature.declaration as ts.FunctionLikeDeclaration)); - if (signature.flags & ts.SignatureFlags.IsInnerCallChain) { - type = addOptionalTypeMarker(type); - } - else if (signature.flags & ts.SignatureFlags.IsOuterCallChain) { - type = getOptionalType(type); - } - if (!popTypeResolution()) { - if (signature.declaration) { - const typeNode = ts.getEffectiveReturnTypeNode(signature.declaration); - if (typeNode) { - error(typeNode, ts.Diagnostics.Return_type_annotation_circularly_references_itself); + function getReturnTypeOfSignature(signature: ts.Signature): ts.Type { + if (!signature.resolvedReturnType) { + if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { + return errorType; + } + let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) : + signature.compositeSignatures ? instantiateType(getUnionOrIntersectionType(ts.map(signature.compositeSignatures, getReturnTypeOfSignature), signature.compositeKind, ts.UnionReduction.Subtype), signature.mapper) : + getReturnTypeFromAnnotation(signature.declaration!) || + (ts.nodeIsMissing((signature.declaration as ts.FunctionLikeDeclaration).body) ? anyType : getReturnTypeFromBody(signature.declaration as ts.FunctionLikeDeclaration)); + if (signature.flags & ts.SignatureFlags.IsInnerCallChain) { + type = addOptionalTypeMarker(type); + } + else if (signature.flags & ts.SignatureFlags.IsOuterCallChain) { + type = getOptionalType(type); + } + if (!popTypeResolution()) { + if (signature.declaration) { + const typeNode = ts.getEffectiveReturnTypeNode(signature.declaration); + if (typeNode) { + error(typeNode, ts.Diagnostics.Return_type_annotation_circularly_references_itself); + } + else if (noImplicitAny) { + const declaration = signature.declaration as ts.Declaration; + const name = ts.getNameOfDeclaration(declaration); + if (name) { + error(name, ts.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, ts.declarationNameToString(name)); } - else if (noImplicitAny) { - const declaration = signature.declaration as ts.Declaration; - const name = ts.getNameOfDeclaration(declaration); - if (name) { - error(name, ts.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, ts.declarationNameToString(name)); - } - else { - error(declaration, ts.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); - } + else { + error(declaration, ts.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; + type = anyType; } - return signature.resolvedReturnType; + signature.resolvedReturnType = type; } + return signature.resolvedReturnType; + } - function getReturnTypeFromAnnotation(declaration: ts.SignatureDeclaration | ts.JSDocSignature) { - if (declaration.kind === ts.SyntaxKind.Constructor) { - return getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent as ts.ClassDeclaration).symbol)); - } - if (ts.isJSDocConstructSignature(declaration)) { - return getTypeFromTypeNode((declaration.parameters[0] as ts.ParameterDeclaration).type!); // TODO: GH#18217 - } - const typeNode = ts.getEffectiveReturnTypeNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); - } - if (declaration.kind === ts.SyntaxKind.GetAccessor && hasBindableName(declaration)) { - const jsDocType = ts.isInJSFile(declaration) && getTypeForDeclarationFromJSDocComment(declaration); - if (jsDocType) { - return jsDocType; - } - const setter = ts.getDeclarationOfKind(getSymbolOfNode(declaration), ts.SyntaxKind.SetAccessor); - const setterType = getAnnotatedAccessorType(setter); - if (setterType) { - return setterType; - } - } - return getReturnTypeOfTypeTag(declaration); + function getReturnTypeFromAnnotation(declaration: ts.SignatureDeclaration | ts.JSDocSignature) { + if (declaration.kind === ts.SyntaxKind.Constructor) { + return getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent as ts.ClassDeclaration).symbol)); } - - function isResolvingReturnTypeOfSignature(signature: ts.Signature) { - return !signature.resolvedReturnType && findResolutionCycleStartIndex(signature, TypeSystemPropertyName.ResolvedReturnType) >= 0; + if (ts.isJSDocConstructSignature(declaration)) { + return getTypeFromTypeNode((declaration.parameters[0] as ts.ParameterDeclaration).type!); // TODO: GH#18217 } - - function getRestTypeOfSignature(signature: ts.Signature): ts.Type { - return tryGetRestTypeOfSignature(signature) || anyType; + const typeNode = ts.getEffectiveReturnTypeNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); } - - 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, numberType); + if (declaration.kind === ts.SyntaxKind.GetAccessor && hasBindableName(declaration)) { + const jsDocType = ts.isInJSFile(declaration) && getTypeForDeclarationFromJSDocComment(declaration); + if (jsDocType) { + return jsDocType; } - return undefined; - } - - function getSignatureInstantiation(signature: ts.Signature, typeArguments: ts.Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly ts.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; - } + const setter = ts.getDeclarationOfKind(getSymbolOfNode(declaration), ts.SyntaxKind.SetAccessor); + const setterType = getAnnotatedAccessorType(setter); + if (setterType) { + return setterType; } - return instantiatedSignature; } + return getReturnTypeOfTypeTag(declaration); + } - function getSignatureInstantiationWithoutFillingInTypeArguments(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { - const instantiations = signature.instantiations || (signature.instantiations = new ts.Map()); - const id = getTypeListId(typeArguments); - let instantiation = instantiations.get(id); - if (!instantiation) { - instantiations.set(id, instantiation = createSignatureInstantiation(signature, typeArguments)); - } - return instantiation; - } + function isResolvingReturnTypeOfSignature(signature: ts.Signature) { + return !signature.resolvedReturnType && findResolutionCycleStartIndex(signature, TypeSystemPropertyName.ResolvedReturnType) >= 0; + } - function createSignatureInstantiation(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { - return instantiateSignature(signature, createSignatureTypeMapper(signature, typeArguments), /*eraseTypeParameters*/ true); - } + function getRestTypeOfSignature(signature: ts.Signature): ts.Type { + return tryGetRestTypeOfSignature(signature) || anyType; + } - function createSignatureTypeMapper(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.TypeMapper { - return createTypeMapper(signature.typeParameters!, typeArguments); + 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, numberType); } + return undefined; + } - function getErasedSignature(signature: ts.Signature): ts.Signature { - return signature.typeParameters ? - signature.erasedSignatureCache || (signature.erasedSignatureCache = createErasedSignature(signature)) : - signature; + function getSignatureInstantiation(signature: ts.Signature, typeArguments: ts.Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly ts.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 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 getSignatureInstantiationWithoutFillingInTypeArguments(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { + const instantiations = signature.instantiations || (signature.instantiations = new ts.Map()); + const id = getTypeListId(typeArguments); + let instantiation = instantiations.get(id); + if (!instantiation) { + instantiations.set(id, instantiation = createSignatureInstantiation(signature, typeArguments)); } + return instantiation; + } - function getCanonicalSignature(signature: ts.Signature): ts.Signature { - return signature.typeParameters ? - signature.canonicalSignatureCache || (signature.canonicalSignatureCache = createCanonicalSignature(signature)) : - signature; - } + function createSignatureInstantiation(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { + return instantiateSignature(signature, createSignatureTypeMapper(signature, typeArguments), /*eraseTypeParameters*/ true); + } - 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, ts.map(signature.typeParameters, tp => tp.target && !getConstraintOfTypeParameter(tp.target) ? tp.target : tp), ts.isInJSFile(signature.declaration)); - } + function createSignatureTypeMapper(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.TypeMapper { + return createTypeMapper(signature.typeParameters!, typeArguments); + } - function getBaseSignature(signature: ts.Signature) { - const typeParameters = signature.typeParameters; - if (typeParameters) { - if (signature.baseSignatureCache) { - return signature.baseSignatureCache; - } - const typeEraser = createTypeEraser(typeParameters); - const baseConstraintMapper = createTypeMapper(typeParameters, ts.map(typeParameters, tp => getConstraintOfTypeParameter(tp) || unknownType)); - let baseConstraints: readonly ts.Type[] = ts.map(typeParameters, tp => instantiateType(tp, baseConstraintMapper) || unknownType); - // Run N type params thru the immediate constraint mapper up to N times - // This way any noncircular interdependent type parameters are definitely resolved to their external dependencies - for (let i = 0; i < typeParameters.length - 1; i++) { - baseConstraints = instantiateTypes(baseConstraints, baseConstraintMapper); - } - // and then apply a type eraser to remove any remaining circularly dependent type parameters - baseConstraints = instantiateTypes(baseConstraints, typeEraser); - return signature.baseSignatureCache = instantiateSignature(signature, createTypeMapper(typeParameters, baseConstraints), /*eraseTypeParameters*/ true); - } - return signature; - } + function getErasedSignature(signature: ts.Signature): ts.Signature { + return signature.typeParameters ? + signature.erasedSignatureCache || (signature.erasedSignatureCache = createErasedSignature(signature)) : + signature; + } - function getOrCreateTypeFromSignature(signature: ts.Signature): ts.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?.kind; + 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); + } - // If declaration is undefined, it is likely to be the signature of the default constructor. - const isConstructor = kind === undefined || kind === ts.SyntaxKind.Constructor || kind === ts.SyntaxKind.ConstructSignature || kind === ts.SyntaxKind.ConstructorType; - const type = createObjectType(ts.ObjectFlags.Anonymous); - type.members = emptySymbols; - type.properties = ts.emptyArray; - type.callSignatures = !isConstructor ? [signature] : ts.emptyArray; - type.constructSignatures = isConstructor ? [signature] : ts.emptyArray; - type.indexInfos = ts.emptyArray; - signature.isolatedSignatureType = type; - } + function getCanonicalSignature(signature: ts.Signature): ts.Signature { + return signature.typeParameters ? + signature.canonicalSignatureCache || (signature.canonicalSignatureCache = createCanonicalSignature(signature)) : + signature; + } - return signature.isolatedSignatureType; - } + 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, ts.map(signature.typeParameters, tp => tp.target && !getConstraintOfTypeParameter(tp.target) ? tp.target : tp), ts.isInJSFile(signature.declaration)); + } - function getIndexSymbol(symbol: ts.Symbol): ts.Symbol | undefined { - return symbol.members ? getIndexSymbolFromSymbolTable(symbol.members) : undefined; - } + function getBaseSignature(signature: ts.Signature) { + const typeParameters = signature.typeParameters; + if (typeParameters) { + if (signature.baseSignatureCache) { + return signature.baseSignatureCache; + } + const typeEraser = createTypeEraser(typeParameters); + const baseConstraintMapper = createTypeMapper(typeParameters, ts.map(typeParameters, tp => getConstraintOfTypeParameter(tp) || unknownType)); + let baseConstraints: readonly ts.Type[] = ts.map(typeParameters, tp => instantiateType(tp, baseConstraintMapper) || unknownType); + // Run N type params thru the immediate constraint mapper up to N times + // This way any noncircular interdependent type parameters are definitely resolved to their external dependencies + for (let i = 0; i < typeParameters.length - 1; i++) { + baseConstraints = instantiateTypes(baseConstraints, baseConstraintMapper); + } + // and then apply a type eraser to remove any remaining circularly dependent type parameters + baseConstraints = instantiateTypes(baseConstraints, typeEraser); + return signature.baseSignatureCache = instantiateSignature(signature, createTypeMapper(typeParameters, baseConstraints), /*eraseTypeParameters*/ true); + } + return signature; + } - function getIndexSymbolFromSymbolTable(symbolTable: ts.SymbolTable): ts.Symbol | undefined { - return symbolTable.get(ts.InternalSymbolName.Index); - } + function getOrCreateTypeFromSignature(signature: ts.Signature): ts.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?.kind; + + // If declaration is undefined, it is likely to be the signature of the default constructor. + const isConstructor = kind === undefined || kind === ts.SyntaxKind.Constructor || kind === ts.SyntaxKind.ConstructSignature || kind === ts.SyntaxKind.ConstructorType; + const type = createObjectType(ts.ObjectFlags.Anonymous); + type.members = emptySymbols; + type.properties = ts.emptyArray; + type.callSignatures = !isConstructor ? [signature] : ts.emptyArray; + type.constructSignatures = isConstructor ? [signature] : ts.emptyArray; + type.indexInfos = ts.emptyArray; + signature.isolatedSignatureType = type; + } + + return signature.isolatedSignatureType; + } - function createIndexInfo(keyType: ts.Type, type: ts.Type, isReadonly: boolean, declaration?: ts.IndexSignatureDeclaration): ts.IndexInfo { - return { keyType, type, isReadonly, declaration }; - } + function getIndexSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + return symbol.members ? getIndexSymbolFromSymbolTable(symbol.members) : undefined; + } - function getIndexInfosOfSymbol(symbol: ts.Symbol): ts.IndexInfo[] { - const indexSymbol = getIndexSymbol(symbol); - return indexSymbol ? getIndexInfosOfIndexSymbol(indexSymbol) : ts.emptyArray; - } + function getIndexSymbolFromSymbolTable(symbolTable: ts.SymbolTable): ts.Symbol | undefined { + return symbolTable.get(ts.InternalSymbolName.Index); + } - function getIndexInfosOfIndexSymbol(indexSymbol: ts.Symbol): ts.IndexInfo[] { - if (indexSymbol.declarations) { - const indexInfos: ts.IndexInfo[] = []; - for (const declaration of (indexSymbol.declarations as ts.IndexSignatureDeclaration[])) { - if (declaration.parameters.length === 1) { - const parameter = declaration.parameters[0]; - if (parameter.type) { - forEachType(getTypeFromTypeNode(parameter.type), keyType => { - if (isValidIndexKeyType(keyType) && !findIndexInfo(indexInfos, keyType)) { - indexInfos.push(createIndexInfo(keyType, declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Readonly), declaration)); - } - }); - } + function createIndexInfo(keyType: ts.Type, type: ts.Type, isReadonly: boolean, declaration?: ts.IndexSignatureDeclaration): ts.IndexInfo { + return { keyType, type, isReadonly, declaration }; + } + + function getIndexInfosOfSymbol(symbol: ts.Symbol): ts.IndexInfo[] { + const indexSymbol = getIndexSymbol(symbol); + return indexSymbol ? getIndexInfosOfIndexSymbol(indexSymbol) : ts.emptyArray; + } + + function getIndexInfosOfIndexSymbol(indexSymbol: ts.Symbol): ts.IndexInfo[] { + if (indexSymbol.declarations) { + const indexInfos: ts.IndexInfo[] = []; + for (const declaration of (indexSymbol.declarations as ts.IndexSignatureDeclaration[])) { + if (declaration.parameters.length === 1) { + const parameter = declaration.parameters[0]; + if (parameter.type) { + forEachType(getTypeFromTypeNode(parameter.type), keyType => { + if (isValidIndexKeyType(keyType) && !findIndexInfo(indexInfos, keyType)) { + indexInfos.push(createIndexInfo(keyType, declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Readonly), declaration)); + } + }); } } - return indexInfos; } - return ts.emptyArray; + return indexInfos; } + return ts.emptyArray; + } + + function isValidIndexKeyType(type: ts.Type): boolean { + return !!(type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.ESSymbol)) || isPatternLiteralType(type) || + !!(type.flags & ts.TypeFlags.Intersection) && !isGenericType(type) && ts.some((type as ts.IntersectionType).types, isValidIndexKeyType); + } + + function getConstraintDeclaration(type: ts.TypeParameter): ts.TypeNode | undefined { + return ts.mapDefined(ts.filter(type.symbol && type.symbol.declarations, ts.isTypeParameterDeclaration), ts.getEffectiveConstraintOfTypeParameter)[0]; + } - function isValidIndexKeyType(type: ts.Type): boolean { - return !!(type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.ESSymbol)) || isPatternLiteralType(type) || - !!(type.flags & ts.TypeFlags.Intersection) && !isGenericType(type) && ts.some((type as ts.IntersectionType).types, isValidIndexKeyType); - } - - function getConstraintDeclaration(type: ts.TypeParameter): ts.TypeNode | undefined { - return ts.mapDefined(ts.filter(type.symbol && type.symbol.declarations, ts.isTypeParameterDeclaration), ts.getEffectiveConstraintOfTypeParameter)[0]; - } - - function getInferredTypeParameterConstraint(typeParameter: ts.TypeParameter, omitTypeReferences?: boolean) { - let inferences: ts.Type[] | undefined; - if (typeParameter.symbol?.declarations) { - for (const declaration of typeParameter.symbol.declarations) { - if (declaration.parent.kind === ts.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 [childTypeParameter = declaration.parent, grandParent] = ts.walkUpParenthesizedTypesAndGetParentAndChild(declaration.parent.parent); - if (grandParent.kind === ts.SyntaxKind.TypeReference && !omitTypeReferences) { - const typeReference = grandParent as ts.TypeReferenceNode; - const typeParameters = getTypeParametersForTypeReference(typeReference); - if (typeParameters) { - const index = typeReference.typeArguments!.indexOf(childTypeParameter as ts.TypeNode); - 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 = ts.append(inferences, constraint); - } + function getInferredTypeParameterConstraint(typeParameter: ts.TypeParameter, omitTypeReferences?: boolean) { + let inferences: ts.Type[] | undefined; + if (typeParameter.symbol?.declarations) { + for (const declaration of typeParameter.symbol.declarations) { + if (declaration.parent.kind === ts.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 [childTypeParameter = declaration.parent, grandParent] = ts.walkUpParenthesizedTypesAndGetParentAndChild(declaration.parent.parent); + if (grandParent.kind === ts.SyntaxKind.TypeReference && !omitTypeReferences) { + const typeReference = grandParent as ts.TypeReferenceNode; + const typeParameters = getTypeParametersForTypeReference(typeReference); + if (typeParameters) { + const index = typeReference.typeArguments!.indexOf(childTypeParameter as ts.TypeNode); + 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 = ts.append(inferences, constraint); } } } } - // When an 'infer T' declaration is immediately contained in a rest parameter declaration, a rest type - // or a named rest tuple element, we infer an 'unknown[]' constraint. - else if (grandParent.kind === ts.SyntaxKind.Parameter && (grandParent as ts.ParameterDeclaration).dotDotDotToken || - grandParent.kind === ts.SyntaxKind.RestType || - grandParent.kind === ts.SyntaxKind.NamedTupleMember && (grandParent as ts.NamedTupleMember).dotDotDotToken) { - inferences = ts.append(inferences, createArrayType(unknownType)); - } - // When an 'infer T' declaration is immediately contained in a string template type, we infer a 'string' - // constraint. - else if (grandParent.kind === ts.SyntaxKind.TemplateLiteralTypeSpan) { - inferences = ts.append(inferences, stringType); - } - // When an 'infer T' declaration is in the constraint position of a mapped type, we infer a 'keyof any' - // constraint. - else if (grandParent.kind === ts.SyntaxKind.TypeParameter && grandParent.parent.kind === ts.SyntaxKind.MappedType) { - inferences = ts.append(inferences, keyofConstraintType); - } - // When an 'infer T' declaration is the template of a mapped type, and that mapped type is the extends - // clause of a conditional whose check type is also a mapped type, give it a constraint equal to the template - // of the check type's mapped type - else if (grandParent.kind === ts.SyntaxKind.MappedType && (grandParent as ts.MappedTypeNode).type && - ts.skipParentheses((grandParent as ts.MappedTypeNode).type!) === declaration.parent && grandParent.parent.kind === ts.SyntaxKind.ConditionalType && - (grandParent.parent as ts.ConditionalTypeNode).extendsType === grandParent && (grandParent.parent as ts.ConditionalTypeNode).checkType.kind === ts.SyntaxKind.MappedType && - ((grandParent.parent as ts.ConditionalTypeNode).checkType as ts.MappedTypeNode).type) { - const checkMappedType = (grandParent.parent as ts.ConditionalTypeNode).checkType as ts.MappedTypeNode; - const nodeType = getTypeFromTypeNode(checkMappedType.type!); - inferences = ts.append(inferences, instantiateType(nodeType, makeUnaryTypeMapper(getDeclaredTypeOfTypeParameter(getSymbolOfNode(checkMappedType.typeParameter)), checkMappedType.typeParameter.constraint ? getTypeFromTypeNode(checkMappedType.typeParameter.constraint) : keyofConstraintType))); - } + } + // When an 'infer T' declaration is immediately contained in a rest parameter declaration, a rest type + // or a named rest tuple element, we infer an 'unknown[]' constraint. + else if (grandParent.kind === ts.SyntaxKind.Parameter && (grandParent as ts.ParameterDeclaration).dotDotDotToken || + grandParent.kind === ts.SyntaxKind.RestType || + grandParent.kind === ts.SyntaxKind.NamedTupleMember && (grandParent as ts.NamedTupleMember).dotDotDotToken) { + inferences = ts.append(inferences, createArrayType(unknownType)); + } + // When an 'infer T' declaration is immediately contained in a string template type, we infer a 'string' + // constraint. + else if (grandParent.kind === ts.SyntaxKind.TemplateLiteralTypeSpan) { + inferences = ts.append(inferences, stringType); + } + // When an 'infer T' declaration is in the constraint position of a mapped type, we infer a 'keyof any' + // constraint. + else if (grandParent.kind === ts.SyntaxKind.TypeParameter && grandParent.parent.kind === ts.SyntaxKind.MappedType) { + inferences = ts.append(inferences, keyofConstraintType); + } + // When an 'infer T' declaration is the template of a mapped type, and that mapped type is the extends + // clause of a conditional whose check type is also a mapped type, give it a constraint equal to the template + // of the check type's mapped type + else if (grandParent.kind === ts.SyntaxKind.MappedType && (grandParent as ts.MappedTypeNode).type && + ts.skipParentheses((grandParent as ts.MappedTypeNode).type!) === declaration.parent && grandParent.parent.kind === ts.SyntaxKind.ConditionalType && + (grandParent.parent as ts.ConditionalTypeNode).extendsType === grandParent && (grandParent.parent as ts.ConditionalTypeNode).checkType.kind === ts.SyntaxKind.MappedType && + ((grandParent.parent as ts.ConditionalTypeNode).checkType as ts.MappedTypeNode).type) { + const checkMappedType = (grandParent.parent as ts.ConditionalTypeNode).checkType as ts.MappedTypeNode; + const nodeType = getTypeFromTypeNode(checkMappedType.type!); + inferences = ts.append(inferences, instantiateType(nodeType, makeUnaryTypeMapper(getDeclaredTypeOfTypeParameter(getSymbolOfNode(checkMappedType.typeParameter)), checkMappedType.typeParameter.constraint ? getTypeFromTypeNode(checkMappedType.typeParameter.constraint) : keyofConstraintType))); } } } - return inferences && getIntersectionType(inferences); } + return inferences && getIntersectionType(inferences); + } - /** This is a worker function. Use getConstraintOfTypeParameter which guards against circular constraints. */ - function getConstraintFromTypeParameter(typeParameter: ts.TypeParameter): ts.Type | undefined { - if (!typeParameter.constraint) { - if (typeParameter.target) { - const targetConstraint = getConstraintOfTypeParameter(typeParameter.target); - typeParameter.constraint = targetConstraint ? instantiateType(targetConstraint, typeParameter.mapper) : noConstraintType; + /** This is a worker function. Use getConstraintOfTypeParameter which guards against circular constraints. */ + function getConstraintFromTypeParameter(typeParameter: ts.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); + if (!constraintDeclaration) { + typeParameter.constraint = getInferredTypeParameterConstraint(typeParameter) || noConstraintType; } else { - const constraintDeclaration = getConstraintDeclaration(typeParameter); - if (!constraintDeclaration) { - typeParameter.constraint = getInferredTypeParameterConstraint(typeParameter) || noConstraintType; - } - else { - let type = getTypeFromTypeNode(constraintDeclaration); - if (type.flags & ts.TypeFlags.Any && !isErrorType(type)) { // Allow errorType to propegate to keep downstream errors suppressed - // use keyofConstraintType as the base constraint for mapped type key constraints (unknown isn;t assignable to that, but `any` was), - // use unknown otherwise - type = constraintDeclaration.parent.parent.kind === ts.SyntaxKind.MappedType ? keyofConstraintType : unknownType; - } - typeParameter.constraint = type; + let type = getTypeFromTypeNode(constraintDeclaration); + if (type.flags & ts.TypeFlags.Any && !isErrorType(type)) { // Allow errorType to propegate to keep downstream errors suppressed + // use keyofConstraintType as the base constraint for mapped type key constraints (unknown isn;t assignable to that, but `any` was), + // use unknown otherwise + type = constraintDeclaration.parent.parent.kind === ts.SyntaxKind.MappedType ? keyofConstraintType : unknownType; } + typeParameter.constraint = type; } } - return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint; } + return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint; + } - function getParentSymbolOfTypeParameter(typeParameter: ts.TypeParameter): ts.Symbol | undefined { - const tp = ts.getDeclarationOfKind(typeParameter.symbol, ts.SyntaxKind.TypeParameter)!; - const host = ts.isJSDocTemplateTag(tp.parent) ? ts.getEffectiveContainerForJSDocTemplateTag(tp.parent) : tp.parent; - return host && getSymbolOfNode(host); - } + function getParentSymbolOfTypeParameter(typeParameter: ts.TypeParameter): ts.Symbol | undefined { + const tp = ts.getDeclarationOfKind(typeParameter.symbol, ts.SyntaxKind.TypeParameter)!; + const host = ts.isJSDocTemplateTag(tp.parent) ? ts.getEffectiveContainerForJSDocTemplateTag(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++; - } - if (result.length) { - result += ","; - } - result += startId; - if (count > 1) { - result += ":" + count; - } - i += count; + 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++; } - } - return result; - } - - function getAliasId(aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { - return aliasSymbol ? `@${getSymbolId(aliasSymbol)}` + (aliasTypeArguments ? `:${getTypeListId(aliasTypeArguments)}` : "") : ""; - } - - // 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: ts.TypeFlags): ts.ObjectFlags { - let result: ts.ObjectFlags = 0; - for (const type of types) { - if (!(type.flags & excludeKinds)) { - result |= ts.getObjectFlags(type); + if (result.length) { + result += ","; } + result += startId; + if (count > 1) { + result += ":" + count; + } + i += count; } - return result & ts.ObjectFlags.PropagatingFlags; - } - - function createTypeReference(target: ts.GenericType, typeArguments: readonly ts.Type[] | undefined): ts.TypeReference { - const id = getTypeListId(typeArguments); - let type = target.instantiations.get(id); - if (!type) { - type = createObjectType(ts.ObjectFlags.Reference, target.symbol) as ts.TypeReference; - target.instantiations.set(id, type); - type.objectFlags |= typeArguments ? getPropagatingFlagsOfTypes(typeArguments, /*excludeKinds*/ 0) : 0; - type.target = target; - type.resolvedTypeArguments = typeArguments; - } - return type; } + return result; + } - function cloneTypeReference(source: ts.TypeReference): ts.TypeReference { - const type = createType(source.flags) as ts.TypeReference; - type.symbol = source.symbol; - type.objectFlags = source.objectFlags; - type.target = source.target; - type.resolvedTypeArguments = source.resolvedTypeArguments; - return type; - } + function getAliasId(aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { + return aliasSymbol ? `@${getSymbolId(aliasSymbol)}` + (aliasTypeArguments ? `:${getTypeListId(aliasTypeArguments)}` : "") : ""; + } - function createDeferredTypeReference(target: ts.GenericType, node: ts.TypeReferenceNode | ts.ArrayTypeNode | ts.TupleTypeNode, mapper?: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.DeferredTypeReference { - if (!aliasSymbol) { - aliasSymbol = getAliasSymbolForTypeNode(node); - const localAliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); - aliasTypeArguments = mapper ? instantiateTypes(localAliasTypeArguments, mapper) : localAliasTypeArguments; + // 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: ts.TypeFlags): ts.ObjectFlags { + let result: ts.ObjectFlags = 0; + for (const type of types) { + if (!(type.flags & excludeKinds)) { + result |= ts.getObjectFlags(type); } - const type = createObjectType(ts.ObjectFlags.Reference, target.symbol) as ts.DeferredTypeReference; - type.target = target; - type.node = node; - type.mapper = mapper; - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = aliasTypeArguments; - return type; } + return result & ts.ObjectFlags.PropagatingFlags; + } - function getTypeArguments(type: ts.TypeReference): readonly ts.Type[] { - if (!type.resolvedTypeArguments) { - if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedTypeArguments)) { - return type.target.localTypeParameters?.map(() => errorType) || ts.emptyArray; - } - const node = type.node; - const typeArguments = !node ? ts.emptyArray : - node.kind === ts.SyntaxKind.TypeReference ? ts.concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) : - node.kind === ts.SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : - ts.map(node.elements, getTypeFromTypeNode); - if (popTypeResolution()) { - type.resolvedTypeArguments = type.mapper ? instantiateTypes(typeArguments, type.mapper) : typeArguments; - } - else { - type.resolvedTypeArguments = type.target.localTypeParameters?.map(() => errorType) || ts.emptyArray; - error(type.node || currentNode, type.target.symbol ? ts.Diagnostics.Type_arguments_for_0_circularly_reference_themselves : ts.Diagnostics.Tuple_type_arguments_circularly_reference_themselves, type.target.symbol && symbolToString(type.target.symbol)); - } - } - return type.resolvedTypeArguments; + function createTypeReference(target: ts.GenericType, typeArguments: readonly ts.Type[] | undefined): ts.TypeReference { + const id = getTypeListId(typeArguments); + let type = target.instantiations.get(id); + if (!type) { + type = createObjectType(ts.ObjectFlags.Reference, target.symbol) as ts.TypeReference; + target.instantiations.set(id, type); + type.objectFlags |= typeArguments ? getPropagatingFlagsOfTypes(typeArguments, /*excludeKinds*/ 0) : 0; + type.target = target; + type.resolvedTypeArguments = typeArguments; } + return type; + } - function getTypeReferenceArity(type: ts.TypeReference): number { - return ts.length(type.target.typeParameters); - } + function cloneTypeReference(source: ts.TypeReference): ts.TypeReference { + const type = createType(source.flags) as ts.TypeReference; + type.symbol = source.symbol; + type.objectFlags = source.objectFlags; + type.target = source.target; + type.resolvedTypeArguments = source.resolvedTypeArguments; + return type; + } + function createDeferredTypeReference(target: ts.GenericType, node: ts.TypeReferenceNode | ts.ArrayTypeNode | ts.TupleTypeNode, mapper?: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.DeferredTypeReference { + if (!aliasSymbol) { + aliasSymbol = getAliasSymbolForTypeNode(node); + const localAliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); + aliasTypeArguments = mapper ? instantiateTypes(localAliasTypeArguments, mapper) : localAliasTypeArguments; + } + const type = createObjectType(ts.ObjectFlags.Reference, target.symbol) as ts.DeferredTypeReference; + type.target = target; + type.node = node; + type.mapper = mapper; + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = aliasTypeArguments; + return type; + } - /** - * Get type from type-reference that reference to class or interface - */ - function getTypeFromClassOrInterfaceReference(node: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { - const type = getDeclaredTypeOfSymbol(getMergedSymbol(symbol)) as ts.InterfaceType; - const typeParameters = type.localTypeParameters; - if (typeParameters) { - const numTypeArguments = ts.length(node.typeArguments); - const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); - const isJs = ts.isInJSFile(node); - const isJsImplicitAny = !noImplicitAny && isJs; - if (!isJsImplicitAny && (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length)) { - const missingAugmentsTag = isJs && ts.isExpressionWithTypeArguments(node) && !ts.isJSDocAugmentsTag(node.parent); - const diag = minTypeArgumentCount === typeParameters.length ? - missingAugmentsTag ? - ts.Diagnostics.Expected_0_type_arguments_provide_these_with_an_extends_tag : - ts.Diagnostics.Generic_type_0_requires_1_type_argument_s : - missingAugmentsTag ? - ts.Diagnostics.Expected_0_1_type_arguments_provide_these_with_an_extends_tag : - ts.Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments; - const typeStr = typeToString(type, /*enclosingDeclaration*/ undefined, ts.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 === ts.SyntaxKind.TypeReference && isDeferredTypeReferenceNode(node as ts.TypeReferenceNode, ts.length(node.typeArguments) !== typeParameters.length)) { - return createDeferredTypeReference(type as ts.GenericType, node as ts.TypeReferenceNode, /*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 = ts.concatenate(type.outerTypeParameters, fillMissingTypeArguments(typeArgumentsFromTypeReferenceNode(node), typeParameters, minTypeArgumentCount, isJs)); - return createTypeReference(type as ts.GenericType, typeArguments); + function getTypeArguments(type: ts.TypeReference): readonly ts.Type[] { + if (!type.resolvedTypeArguments) { + if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedTypeArguments)) { + return type.target.localTypeParameters?.map(() => errorType) || ts.emptyArray; } - return checkNoTypeArguments(node, symbol) ? type : errorType; - } - - function getTypeAliasInstantiation(symbol: ts.Symbol, typeArguments: readonly ts.Type[] | undefined, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { - const type = getDeclaredTypeOfSymbol(symbol); - if (type === intrinsicMarkerType && intrinsicTypeKinds.has(symbol.escapedName as string) && typeArguments && typeArguments.length === 1) { - return getStringMappingType(symbol, typeArguments[0]); + const node = type.node; + const typeArguments = !node ? ts.emptyArray : + node.kind === ts.SyntaxKind.TypeReference ? ts.concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) : + node.kind === ts.SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : + ts.map(node.elements, getTypeFromTypeNode); + if (popTypeResolution()) { + type.resolvedTypeArguments = type.mapper ? instantiateTypes(typeArguments, type.mapper) : typeArguments; } - const links = getSymbolLinks(symbol); - const typeParameters = links.typeParameters!; - const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments); - let instantiation = links.instantiations!.get(id); - if (!instantiation) { - links.instantiations!.set(id, instantiation = instantiateTypeWithAlias(type, createTypeMapper(typeParameters, fillMissingTypeArguments(typeArguments, typeParameters, getMinTypeArgumentCount(typeParameters), ts.isInJSFile(symbol.valueDeclaration))), aliasSymbol, aliasTypeArguments)); + else { + type.resolvedTypeArguments = type.target.localTypeParameters?.map(() => errorType) || ts.emptyArray; + error(type.node || currentNode, type.target.symbol ? ts.Diagnostics.Type_arguments_for_0_circularly_reference_themselves : ts.Diagnostics.Tuple_type_arguments_circularly_reference_themselves, type.target.symbol && symbolToString(type.target.symbol)); } - return instantiation; } + return type.resolvedTypeArguments; + } - /** - * 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: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { - if (ts.getCheckFlags(symbol) & ts.CheckFlags.Unresolved) { - const typeArguments = typeArgumentsFromTypeReferenceNode(node); - const id = getAliasId(symbol, typeArguments); - let errorType = errorTypes.get(id); - if (!errorType) { - errorType = createIntrinsicType(ts.TypeFlags.Any, "error"); - errorType.aliasSymbol = symbol; - errorType.aliasTypeArguments = typeArguments; - errorTypes.set(id, errorType); - } - return errorType; - } - const type = getDeclaredTypeOfSymbol(symbol); - const typeParameters = getSymbolLinks(symbol).typeParameters; - if (typeParameters) { - const numTypeArguments = ts.length(node.typeArguments); - const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); - if (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length) { - error(node, minTypeArgumentCount === typeParameters.length ? + function getTypeReferenceArity(type: ts.TypeReference): number { + return ts.length(type.target.typeParameters); + } + + + /** + * Get type from type-reference that reference to class or interface + */ + function getTypeFromClassOrInterfaceReference(node: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + const type = getDeclaredTypeOfSymbol(getMergedSymbol(symbol)) as ts.InterfaceType; + const typeParameters = type.localTypeParameters; + if (typeParameters) { + const numTypeArguments = ts.length(node.typeArguments); + const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); + const isJs = ts.isInJSFile(node); + const isJsImplicitAny = !noImplicitAny && isJs; + if (!isJsImplicitAny && (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length)) { + const missingAugmentsTag = isJs && ts.isExpressionWithTypeArguments(node) && !ts.isJSDocAugmentsTag(node.parent); + const diag = minTypeArgumentCount === typeParameters.length ? + missingAugmentsTag ? + ts.Diagnostics.Expected_0_type_arguments_provide_these_with_an_extends_tag : ts.Diagnostics.Generic_type_0_requires_1_type_argument_s : - ts.Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments, symbolToString(symbol), minTypeArgumentCount, typeParameters.length); + missingAugmentsTag ? + ts.Diagnostics.Expected_0_1_type_arguments_provide_these_with_an_extends_tag : + ts.Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments; + const typeStr = typeToString(type, /*enclosingDeclaration*/ undefined, ts.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; } - // We refrain from associating a local type alias with an instantiation of a top-level type alias - // because the local alias may end up being referenced in an inferred return type where it is not - // accessible--which in turn may lead to a large structural expansion of the type when generating - // a .d.ts file. See #43622 for an example. - const aliasSymbol = getAliasSymbolForTypeNode(node); - const newAliasSymbol = aliasSymbol && (isLocalTypeAlias(symbol) || !isLocalTypeAlias(aliasSymbol)) ? aliasSymbol : undefined; - return getTypeAliasInstantiation(symbol, typeArgumentsFromTypeReferenceNode(node), newAliasSymbol, getTypeArgumentsForAliasSymbol(newAliasSymbol)); } - return checkNoTypeArguments(node, symbol) ? type : errorType; - } - - function isLocalTypeAlias(symbol: ts.Symbol) { - const declaration = symbol.declarations?.find(ts.isTypeAlias); - return !!(declaration && ts.getContainingFunction(declaration)); - } - - function getTypeReferenceName(node: ts.TypeReferenceType): ts.EntityNameOrEntityNameExpression | undefined { - switch (node.kind) { - case ts.SyntaxKind.TypeReference: - return node.typeName; - case ts.SyntaxKind.ExpressionWithTypeArguments: - // We only support expressions that are simple qualified names. For other - // expressions this produces undefined. - const expr = node.expression; - if (ts.isEntityNameExpression(expr)) { - return expr; - } - // fall through; + if (node.kind === ts.SyntaxKind.TypeReference && isDeferredTypeReferenceNode(node as ts.TypeReferenceNode, ts.length(node.typeArguments) !== typeParameters.length)) { + return createDeferredTypeReference(type as ts.GenericType, node as ts.TypeReferenceNode, /*mapper*/ undefined); } - - return 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 = ts.concatenate(type.outerTypeParameters, fillMissingTypeArguments(typeArgumentsFromTypeReferenceNode(node), typeParameters, minTypeArgumentCount, isJs)); + return createTypeReference(type as ts.GenericType, typeArguments); } + return checkNoTypeArguments(node, symbol) ? type : errorType; + } - function getSymbolPath(symbol: ts.Symbol): string { - return symbol.parent ? `${getSymbolPath(symbol.parent)}.${symbol.escapedName}` : symbol.escapedName as string; + function getTypeAliasInstantiation(symbol: ts.Symbol, typeArguments: readonly ts.Type[] | undefined, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + const type = getDeclaredTypeOfSymbol(symbol); + if (type === intrinsicMarkerType && intrinsicTypeKinds.has(symbol.escapedName as string) && typeArguments && typeArguments.length === 1) { + return getStringMappingType(symbol, typeArguments[0]); } - - function getUnresolvedSymbolForEntityName(name: ts.EntityNameOrEntityNameExpression) { - const identifier = name.kind === ts.SyntaxKind.QualifiedName ? name.right : - name.kind === ts.SyntaxKind.PropertyAccessExpression ? name.name : - name; - const text = identifier.escapedText; - if (text) { - const parentSymbol = name.kind === ts.SyntaxKind.QualifiedName ? getUnresolvedSymbolForEntityName(name.left) : - name.kind === ts.SyntaxKind.PropertyAccessExpression ? getUnresolvedSymbolForEntityName(name.expression) : - undefined; - const path = parentSymbol ? `${getSymbolPath(parentSymbol)}.${text}` : text as string; - let result = unresolvedSymbols.get(path); - if (!result) { - unresolvedSymbols.set(path, result = createSymbol(ts.SymbolFlags.TypeAlias, text, ts.CheckFlags.Unresolved)); - result.parent = parentSymbol; - result.declaredType = unresolvedType; - } - return result; - } - return unknownSymbol; + const links = getSymbolLinks(symbol); + const typeParameters = links.typeParameters!; + const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments); + let instantiation = links.instantiations!.get(id); + if (!instantiation) { + links.instantiations!.set(id, instantiation = instantiateTypeWithAlias(type, createTypeMapper(typeParameters, fillMissingTypeArguments(typeArguments, typeParameters, getMinTypeArgumentCount(typeParameters), ts.isInJSFile(symbol.valueDeclaration))), aliasSymbol, aliasTypeArguments)); } + return instantiation; + } - function resolveTypeReferenceName(typeReference: ts.TypeReferenceType, meaning: ts.SymbolFlags, ignoreErrors?: boolean) { - const name = getTypeReferenceName(typeReference); - if (!name) { - return unknownSymbol; + /** + * 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: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + if (ts.getCheckFlags(symbol) & ts.CheckFlags.Unresolved) { + const typeArguments = typeArgumentsFromTypeReferenceNode(node); + const id = getAliasId(symbol, typeArguments); + let errorType = errorTypes.get(id); + if (!errorType) { + errorType = createIntrinsicType(ts.TypeFlags.Any, "error"); + errorType.aliasSymbol = symbol; + errorType.aliasTypeArguments = typeArguments; + errorTypes.set(id, errorType); } - const symbol = resolveEntityName(name, meaning, ignoreErrors); - return symbol && symbol !== unknownSymbol ? symbol : - ignoreErrors ? unknownSymbol : getUnresolvedSymbolForEntityName(name); + return errorType; } - - function getTypeReferenceType(node: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { - if (symbol === unknownSymbol) { + const type = getDeclaredTypeOfSymbol(symbol); + const typeParameters = getSymbolLinks(symbol).typeParameters; + if (typeParameters) { + const numTypeArguments = ts.length(node.typeArguments); + const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); + if (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length) { + error(node, minTypeArgumentCount === typeParameters.length ? + ts.Diagnostics.Generic_type_0_requires_1_type_argument_s : + ts.Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments, symbolToString(symbol), minTypeArgumentCount, typeParameters.length); return errorType; } - symbol = getExpandoSymbol(symbol) || symbol; - if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - return getTypeFromClassOrInterfaceReference(node, symbol); - } - if (symbol.flags & ts.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) ? getRegularTypeOfLiteralType(res) : errorType; - } - if (symbol.flags & ts.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(node, ts.SymbolFlags.Type); - return getTypeOfSymbol(symbol); - } - } - return errorType; + // We refrain from associating a local type alias with an instantiation of a top-level type alias + // because the local alias may end up being referenced in an inferred return type where it is not + // accessible--which in turn may lead to a large structural expansion of the type when generating + // a .d.ts file. See #43622 for an example. + const aliasSymbol = getAliasSymbolForTypeNode(node); + const newAliasSymbol = aliasSymbol && (isLocalTypeAlias(symbol) || !isLocalTypeAlias(aliasSymbol)) ? aliasSymbol : undefined; + return getTypeAliasInstantiation(symbol, typeArgumentsFromTypeReferenceNode(node), newAliasSymbol, getTypeArgumentsForAliasSymbol(newAliasSymbol)); } + return checkNoTypeArguments(node, symbol) ? type : errorType; + } - /** - * A JSdoc TypeReference may be to a value, but resolve it as a type anyway. - * Example: import('./b').ConstructorFunction - */ - function getTypeFromJSDocValueReference(node: ts.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 isImportTypeWithQualifier = node.kind === ts.SyntaxKind.ImportType && (node as ts.ImportTypeNode).qualifier; - // valueType might not have a symbol, eg, {import('./b').STRING_LITERAL} - if (valueType.symbol && valueType.symbol !== symbol && isImportTypeWithQualifier) { - typeType = getTypeReferenceType(node, valueType.symbol); - } + function isLocalTypeAlias(symbol: ts.Symbol) { + const declaration = symbol.declarations?.find(ts.isTypeAlias); + return !!(declaration && ts.getContainingFunction(declaration)); + } + + function getTypeReferenceName(node: ts.TypeReferenceType): ts.EntityNameOrEntityNameExpression | undefined { + switch (node.kind) { + case ts.SyntaxKind.TypeReference: + return node.typeName; + case ts.SyntaxKind.ExpressionWithTypeArguments: + // We only support expressions that are simple qualified names. For other + // expressions this produces undefined. + const expr = node.expression; + if (ts.isEntityNameExpression(expr)) { + return expr; } - links.resolvedJSDocType = typeType; - } - return links.resolvedJSDocType; + // fall through; } - function getSubstitutionType(baseType: ts.Type, substitute: ts.Type) { - if (substitute.flags & ts.TypeFlags.AnyOrUnknown || substitute === baseType) { - return baseType; - } - const id = `${getTypeId(baseType)}>${getTypeId(substitute)}`; - const cached = substitutionTypes.get(id); - if (cached) { - return cached; + return undefined; + } + + function getSymbolPath(symbol: ts.Symbol): string { + return symbol.parent ? `${getSymbolPath(symbol.parent)}.${symbol.escapedName}` : symbol.escapedName as string; + } + + function getUnresolvedSymbolForEntityName(name: ts.EntityNameOrEntityNameExpression) { + const identifier = name.kind === ts.SyntaxKind.QualifiedName ? name.right : + name.kind === ts.SyntaxKind.PropertyAccessExpression ? name.name : + name; + const text = identifier.escapedText; + if (text) { + const parentSymbol = name.kind === ts.SyntaxKind.QualifiedName ? getUnresolvedSymbolForEntityName(name.left) : + name.kind === ts.SyntaxKind.PropertyAccessExpression ? getUnresolvedSymbolForEntityName(name.expression) : + undefined; + const path = parentSymbol ? `${getSymbolPath(parentSymbol)}.${text}` : text as string; + let result = unresolvedSymbols.get(path); + if (!result) { + unresolvedSymbols.set(path, result = createSymbol(ts.SymbolFlags.TypeAlias, text, ts.CheckFlags.Unresolved)); + result.parent = parentSymbol; + result.declaredType = unresolvedType; } - const result = createType(ts.TypeFlags.Substitution) as ts.SubstitutionType; - result.baseType = baseType; - result.substitute = substitute; - substitutionTypes.set(id, result); return result; } + return unknownSymbol; + } - function isUnaryTupleTypeNode(node: ts.TypeNode) { - return node.kind === ts.SyntaxKind.TupleType && (node as ts.TupleTypeNode).elements.length === 1; + function resolveTypeReferenceName(typeReference: ts.TypeReferenceType, meaning: ts.SymbolFlags, ignoreErrors?: boolean) { + const name = getTypeReferenceName(typeReference); + if (!name) { + return unknownSymbol; } + const symbol = resolveEntityName(name, meaning, ignoreErrors); + return symbol && symbol !== unknownSymbol ? symbol : + ignoreErrors ? unknownSymbol : getUnresolvedSymbolForEntityName(name); + } - function getImpliedConstraint(type: ts.Type, checkNode: ts.TypeNode, extendsNode: ts.TypeNode): ts.Type | undefined { - return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(type, (checkNode as ts.TupleTypeNode).elements[0], (extendsNode as ts.TupleTypeNode).elements[0]) : - getActualTypeVariable(getTypeFromTypeNode(checkNode)) === getActualTypeVariable(type) ? getTypeFromTypeNode(extendsNode) : - undefined; + function getTypeReferenceType(node: ts.NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + if (symbol === unknownSymbol) { + return errorType; } - - function getConditionalFlowTypeOfType(type: ts.Type, node: ts.Node) { - let constraints: ts.Type[] | undefined; - let covariant = true; - while (node && !ts.isStatement(node) && node.kind !== ts.SyntaxKind.JSDoc) { - const parent = node.parent; - // only consider variance flipped by parameter locations - `keyof` types would usually be considered variance inverting, but - // often get used in indexed accesses where they behave sortof invariantly, but our checking is lax - if (parent.kind === ts.SyntaxKind.Parameter) { - covariant = !covariant; - } - // Always substitute on type parameters, regardless of variance, since even - // in contravariant positions, they may rely on substituted constraints to be valid - if ((covariant || type.flags & ts.TypeFlags.TypeVariable) && parent.kind === ts.SyntaxKind.ConditionalType && node === (parent as ts.ConditionalTypeNode).trueType) { - const constraint = getImpliedConstraint(type, (parent as ts.ConditionalTypeNode).checkType, (parent as ts.ConditionalTypeNode).extendsType); - if (constraint) { - constraints = ts.append(constraints, constraint); - } - } - // Given a homomorphic mapped type { [K in keyof T]: XXX }, where T is constrained to an array or tuple type, in the - // template type XXX, K has an added constraint of number | `${number}`. - else if (type.flags & ts.TypeFlags.TypeParameter && parent.kind === ts.SyntaxKind.MappedType && node === (parent as ts.MappedTypeNode).type) { - const mappedType = getTypeFromTypeNode(parent as ts.TypeNode) as ts.MappedType; - if (getTypeParameterFromMappedType(mappedType) === getActualTypeVariable(type)) { - const typeParameter = getHomomorphicTypeVariable(mappedType); - if (typeParameter) { - const constraint = getConstraintOfTypeParameter(typeParameter); - if (constraint && everyType(constraint, isArrayOrTupleType)) { - constraints = ts.append(constraints, getUnionType([numberType, numericStringType])); - } - } - } - } - node = parent; - } - return constraints ? getSubstitutionType(type, getIntersectionType(ts.append(constraints, type))) : type; + symbol = getExpandoSymbol(symbol) || symbol; + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { + return getTypeFromClassOrInterfaceReference(node, symbol); } - - function isJSDocTypeReference(node: ts.Node): node is ts.TypeReferenceNode { - return !!(node.flags & ts.NodeFlags.JSDoc) && (node.kind === ts.SyntaxKind.TypeReference || node.kind === ts.SyntaxKind.ImportType); + if (symbol.flags & ts.SymbolFlags.TypeAlias) { + return getTypeFromTypeAliasReference(node, symbol); } - - function checkNoTypeArguments(node: ts.NodeWithTypeArguments, symbol?: ts.Symbol) { - if (node.typeArguments) { - error(node, ts.Diagnostics.Type_0_is_not_generic, symbol ? symbolToString(symbol) : (node as ts.TypeReferenceNode).typeName ? ts.declarationNameToString((node as ts.TypeReferenceNode).typeName) : anon); - return false; + // 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) ? getRegularTypeOfLiteralType(res) : errorType; + } + if (symbol.flags & ts.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(node, ts.SymbolFlags.Type); + return getTypeOfSymbol(symbol); } - return true; } + return errorType; + } - function getIntendedTypeFromJSDocTypeReference(node: ts.TypeReferenceNode): ts.Type | undefined { - if (ts.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 (ts.isJSDocIndexSignature(node)) { - const indexed = getTypeFromTypeNode(typeArgs[0]); - const target = getTypeFromTypeNode(typeArgs[1]); - const indexInfo = indexed === stringType || indexed === numberType ? [createIndexInfo(indexed, target, /*isReadonly*/ false)] : ts.emptyArray; - return createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, indexInfo); - } - return anyType; - } - checkNoTypeArguments(node); - return !noImplicitAny ? anyType : undefined; + /** + * A JSdoc TypeReference may be to a value, but resolve it as a type anyway. + * Example: import('./b').ConstructorFunction + */ + function getTypeFromJSDocValueReference(node: ts.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 isImportTypeWithQualifier = node.kind === ts.SyntaxKind.ImportType && (node as ts.ImportTypeNode).qualifier; + // valueType might not have a symbol, eg, {import('./b').STRING_LITERAL} + if (valueType.symbol && valueType.symbol !== symbol && isImportTypeWithQualifier) { + typeType = getTypeReferenceType(node, valueType.symbol); } } + links.resolvedJSDocType = typeType; } + return links.resolvedJSDocType; + } - function getTypeFromJSDocNullableTypeNode(node: ts.JSDocNullableType) { - const type = getTypeFromTypeNode(node.type); - return strictNullChecks ? getNullableType(type, ts.TypeFlags.Null) : type; + function getSubstitutionType(baseType: ts.Type, substitute: ts.Type) { + if (substitute.flags & ts.TypeFlags.AnyOrUnknown || substitute === baseType) { + return baseType; + } + const id = `${getTypeId(baseType)}>${getTypeId(substitute)}`; + const cached = substitutionTypes.get(id); + if (cached) { + return cached; } + const result = createType(ts.TypeFlags.Substitution) as ts.SubstitutionType; + result.baseType = baseType; + result.substitute = substitute; + substitutionTypes.set(id, result); + return result; + } - function getTypeFromTypeReference(node: ts.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 (ts.isConstTypeReference(node) && ts.isAssertionExpression(node.parent)) { - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = checkExpressionCached(node.parent.expression); + function isUnaryTupleTypeNode(node: ts.TypeNode) { + return node.kind === ts.SyntaxKind.TupleType && (node as ts.TupleTypeNode).elements.length === 1; + } + + function getImpliedConstraint(type: ts.Type, checkNode: ts.TypeNode, extendsNode: ts.TypeNode): ts.Type | undefined { + return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(type, (checkNode as ts.TupleTypeNode).elements[0], (extendsNode as ts.TupleTypeNode).elements[0]) : + getActualTypeVariable(getTypeFromTypeNode(checkNode)) === getActualTypeVariable(type) ? getTypeFromTypeNode(extendsNode) : + undefined; + } + + function getConditionalFlowTypeOfType(type: ts.Type, node: ts.Node) { + let constraints: ts.Type[] | undefined; + let covariant = true; + while (node && !ts.isStatement(node) && node.kind !== ts.SyntaxKind.JSDoc) { + const parent = node.parent; + // only consider variance flipped by parameter locations - `keyof` types would usually be considered variance inverting, but + // often get used in indexed accesses where they behave sortof invariantly, but our checking is lax + if (parent.kind === ts.SyntaxKind.Parameter) { + covariant = !covariant; + } + // Always substitute on type parameters, regardless of variance, since even + // in contravariant positions, they may rely on substituted constraints to be valid + if ((covariant || type.flags & ts.TypeFlags.TypeVariable) && parent.kind === ts.SyntaxKind.ConditionalType && node === (parent as ts.ConditionalTypeNode).trueType) { + const constraint = getImpliedConstraint(type, (parent as ts.ConditionalTypeNode).checkType, (parent as ts.ConditionalTypeNode).extendsType); + if (constraint) { + constraints = ts.append(constraints, constraint); } - let symbol: ts.Symbol | undefined; - let type: ts.Type | undefined; - const meaning = ts.SymbolFlags.Type; - if (isJSDocTypeReference(node)) { - type = getIntendedTypeFromJSDocTypeReference(node); - if (!type) { - symbol = resolveTypeReferenceName(node, meaning, /*ignoreErrors*/ true); - if (symbol === unknownSymbol) { - symbol = resolveTypeReferenceName(node, meaning | ts.SymbolFlags.Value); - } - else { - resolveTypeReferenceName(node, meaning); // Resolve again to mark errors, if any + } + // Given a homomorphic mapped type { [K in keyof T]: XXX }, where T is constrained to an array or tuple type, in the + // template type XXX, K has an added constraint of number | `${number}`. + else if (type.flags & ts.TypeFlags.TypeParameter && parent.kind === ts.SyntaxKind.MappedType && node === (parent as ts.MappedTypeNode).type) { + const mappedType = getTypeFromTypeNode(parent as ts.TypeNode) as ts.MappedType; + if (getTypeParameterFromMappedType(mappedType) === getActualTypeVariable(type)) { + const typeParameter = getHomomorphicTypeVariable(mappedType); + if (typeParameter) { + const constraint = getConstraintOfTypeParameter(typeParameter); + if (constraint && everyType(constraint, isArrayOrTupleType)) { + constraints = ts.append(constraints, getUnionType([numberType, numericStringType])); } - type = getTypeReferenceType(node, symbol); } } - if (!type) { - symbol = resolveTypeReferenceName(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; + node = parent; } + return constraints ? getSubstitutionType(type, getIntersectionType(ts.append(constraints, type))) : type; + } - function typeArgumentsFromTypeReferenceNode(node: ts.NodeWithTypeArguments): ts.Type[] | undefined { - return ts.map(node.typeArguments, getTypeFromTypeNode); - } + function isJSDocTypeReference(node: ts.Node): node is ts.TypeReferenceNode { + return !!(node.flags & ts.NodeFlags.JSDoc) && (node.kind === ts.SyntaxKind.TypeReference || node.kind === ts.SyntaxKind.ImportType); + } - function getTypeFromTypeQueryNode(node: ts.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. - const type = checkExpressionWithTypeArguments(node); - links.resolvedType = getRegularTypeOfLiteralType(getWidenedType(type)); - } - return links.resolvedType; + function checkNoTypeArguments(node: ts.NodeWithTypeArguments, symbol?: ts.Symbol) { + if (node.typeArguments) { + error(node, ts.Diagnostics.Type_0_is_not_generic, symbol ? symbolToString(symbol) : (node as ts.TypeReferenceNode).typeName ? ts.declarationNameToString((node as ts.TypeReferenceNode).typeName) : anon); + return false; } + return true; + } - function getTypeOfGlobalSymbol(symbol: ts.Symbol | undefined, arity: number): ts.ObjectType { - function getTypeDeclaration(symbol: ts.Symbol): ts.Declaration | undefined { - const declarations = symbol.declarations; - if (declarations) { - for (const declaration of declarations) { - switch (declaration.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - return declaration; + function getIntendedTypeFromJSDocTypeReference(node: ts.TypeReferenceNode): ts.Type | undefined { + if (ts.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 (ts.isJSDocIndexSignature(node)) { + const indexed = getTypeFromTypeNode(typeArgs[0]); + const target = getTypeFromTypeNode(typeArgs[1]); + const indexInfo = indexed === stringType || indexed === numberType ? [createIndexInfo(indexed, target, /*isReadonly*/ false)] : ts.emptyArray; + return createAnonymousType(undefined, emptySymbols, ts.emptyArray, ts.emptyArray, indexInfo); } + return anyType; } - } + checkNoTypeArguments(node); + return !noImplicitAny ? anyType : undefined; } + } + } - if (!symbol) { - return arity ? emptyGenericType : emptyObjectType; + function getTypeFromJSDocNullableTypeNode(node: ts.JSDocNullableType) { + const type = getTypeFromTypeNode(node.type); + return strictNullChecks ? getNullableType(type, ts.TypeFlags.Null) : type; + } + + function getTypeFromTypeReference(node: ts.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 (ts.isConstTypeReference(node) && ts.isAssertionExpression(node.parent)) { + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = checkExpressionCached(node.parent.expression); } - const type = getDeclaredTypeOfSymbol(symbol); - if (!(type.flags & ts.TypeFlags.Object)) { - error(getTypeDeclaration(symbol), ts.Diagnostics.Global_type_0_must_be_a_class_or_interface_type, ts.symbolName(symbol)); - return arity ? emptyGenericType : emptyObjectType; + let symbol: ts.Symbol | undefined; + let type: ts.Type | undefined; + const meaning = ts.SymbolFlags.Type; + if (isJSDocTypeReference(node)) { + type = getIntendedTypeFromJSDocTypeReference(node); + if (!type) { + symbol = resolveTypeReferenceName(node, meaning, /*ignoreErrors*/ true); + if (symbol === unknownSymbol) { + symbol = resolveTypeReferenceName(node, meaning | ts.SymbolFlags.Value); + } + else { + resolveTypeReferenceName(node, meaning); // Resolve again to mark errors, if any + } + type = getTypeReferenceType(node, symbol); + } } - if (ts.length((type as ts.InterfaceType).typeParameters) !== arity) { - error(getTypeDeclaration(symbol), ts.Diagnostics.Global_type_0_must_have_1_type_parameter_s, ts.symbolName(symbol), arity); - return arity ? emptyGenericType : emptyObjectType; + if (!type) { + symbol = resolveTypeReferenceName(node, meaning); + type = getTypeReferenceType(node, symbol); } - return type as ts.ObjectType; + // 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 getGlobalValueSymbol(name: ts.__String, reportErrors: boolean): ts.Symbol | undefined { - return getGlobalSymbol(name, ts.SymbolFlags.Value, reportErrors ? ts.Diagnostics.Cannot_find_global_value_0 : undefined); - } + function typeArgumentsFromTypeReferenceNode(node: ts.NodeWithTypeArguments): ts.Type[] | undefined { + return ts.map(node.typeArguments, getTypeFromTypeNode); + } - function getGlobalTypeSymbol(name: ts.__String, reportErrors: boolean): ts.Symbol | undefined { - return getGlobalSymbol(name, ts.SymbolFlags.Type, reportErrors ? ts.Diagnostics.Cannot_find_global_type_0 : undefined); + function getTypeFromTypeQueryNode(node: ts.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. + const type = checkExpressionWithTypeArguments(node); + links.resolvedType = getRegularTypeOfLiteralType(getWidenedType(type)); } + return links.resolvedType; + } - function getGlobalTypeAliasSymbol(name: ts.__String, arity: number, reportErrors: boolean): ts.Symbol | undefined { - const symbol = getGlobalSymbol(name, ts.SymbolFlags.Type, reportErrors ? ts.Diagnostics.Cannot_find_global_type_0 : undefined); - if (symbol) { - // Resolve the declared type of the symbol. This resolves type parameters for the type - // alias so that we can check arity. - getDeclaredTypeOfSymbol(symbol); - if (ts.length(getSymbolLinks(symbol).typeParameters) !== arity) { - const decl = symbol.declarations && ts.find(symbol.declarations, ts.isTypeAliasDeclaration); - error(decl, ts.Diagnostics.Global_type_0_must_have_1_type_parameter_s, ts.symbolName(symbol), arity); - return undefined; + function getTypeOfGlobalSymbol(symbol: ts.Symbol | undefined, arity: number): ts.ObjectType { + function getTypeDeclaration(symbol: ts.Symbol): ts.Declaration | undefined { + const declarations = symbol.declarations; + if (declarations) { + for (const declaration of declarations) { + switch (declaration.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + return declaration; + } } } - return symbol; } - function getGlobalSymbol(name: ts.__String, meaning: ts.SymbolFlags, diagnostic: ts.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, /*excludeGlobals*/ false, /*getSpellingSuggestions*/ false); + if (!symbol) { + return arity ? emptyGenericType : emptyObjectType; } - - function getGlobalType(name: ts.__String, arity: 0, reportErrors: true): ts.ObjectType; - function getGlobalType(name: ts.__String, arity: 0, reportErrors: boolean): ts.ObjectType | undefined; - function getGlobalType(name: ts.__String, arity: number, reportErrors: true): ts.GenericType; - function getGlobalType(name: ts.__String, arity: number, reportErrors: boolean): ts.GenericType | undefined; - function getGlobalType(name: ts.__String, arity: number, reportErrors: boolean): ts.ObjectType | undefined { - const symbol = getGlobalTypeSymbol(name, reportErrors); - return symbol || reportErrors ? getTypeOfGlobalSymbol(symbol, arity) : undefined; + const type = getDeclaredTypeOfSymbol(symbol); + if (!(type.flags & ts.TypeFlags.Object)) { + error(getTypeDeclaration(symbol), ts.Diagnostics.Global_type_0_must_be_a_class_or_interface_type, ts.symbolName(symbol)); + return arity ? emptyGenericType : emptyObjectType; } - - function getGlobalTypedPropertyDescriptorType() { - // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times - return deferredGlobalTypedPropertyDescriptorType ||= getGlobalType("TypedPropertyDescriptor" as ts.__String, /*arity*/ 1, /*reportErrors*/ true) || emptyGenericType; + if (ts.length((type as ts.InterfaceType).typeParameters) !== arity) { + error(getTypeDeclaration(symbol), ts.Diagnostics.Global_type_0_must_have_1_type_parameter_s, ts.symbolName(symbol), arity); + return arity ? emptyGenericType : emptyObjectType; } + return type as ts.ObjectType; + } - function getGlobalTemplateStringsArrayType() { - // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times - return deferredGlobalTemplateStringsArrayType ||= getGlobalType("TemplateStringsArray" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || emptyObjectType; - } + function getGlobalValueSymbol(name: ts.__String, reportErrors: boolean): ts.Symbol | undefined { + return getGlobalSymbol(name, ts.SymbolFlags.Value, reportErrors ? ts.Diagnostics.Cannot_find_global_value_0 : undefined); + } + + function getGlobalTypeSymbol(name: ts.__String, reportErrors: boolean): ts.Symbol | undefined { + return getGlobalSymbol(name, ts.SymbolFlags.Type, reportErrors ? ts.Diagnostics.Cannot_find_global_type_0 : undefined); + } - function getGlobalImportMetaType() { - // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times - return deferredGlobalImportMetaType ||= getGlobalType("ImportMeta" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || emptyObjectType; + function getGlobalTypeAliasSymbol(name: ts.__String, arity: number, reportErrors: boolean): ts.Symbol | undefined { + const symbol = getGlobalSymbol(name, ts.SymbolFlags.Type, reportErrors ? ts.Diagnostics.Cannot_find_global_type_0 : undefined); + if (symbol) { + // Resolve the declared type of the symbol. This resolves type parameters for the type + // alias so that we can check arity. + getDeclaredTypeOfSymbol(symbol); + if (ts.length(getSymbolLinks(symbol).typeParameters) !== arity) { + const decl = symbol.declarations && ts.find(symbol.declarations, ts.isTypeAliasDeclaration); + error(decl, ts.Diagnostics.Global_type_0_must_have_1_type_parameter_s, ts.symbolName(symbol), arity); + return undefined; + } } + return symbol; + } - function getGlobalImportMetaExpressionType() { - if (!deferredGlobalImportMetaExpressionType) { - // Create a synthetic type `ImportMetaExpression { meta: MetaProperty }` - const symbol = createSymbol(ts.SymbolFlags.None, "ImportMetaExpression" as ts.__String); - const importMetaType = getGlobalImportMetaType(); + function getGlobalSymbol(name: ts.__String, meaning: ts.SymbolFlags, diagnostic: ts.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, /*excludeGlobals*/ false, /*getSpellingSuggestions*/ false); + } - const metaPropertySymbol = createSymbol(ts.SymbolFlags.Property, "meta" as ts.__String, ts.CheckFlags.Readonly); - metaPropertySymbol.parent = symbol; - metaPropertySymbol.type = importMetaType; + function getGlobalType(name: ts.__String, arity: 0, reportErrors: true): ts.ObjectType; + function getGlobalType(name: ts.__String, arity: 0, reportErrors: boolean): ts.ObjectType | undefined; + function getGlobalType(name: ts.__String, arity: number, reportErrors: true): ts.GenericType; + function getGlobalType(name: ts.__String, arity: number, reportErrors: boolean): ts.GenericType | undefined; + function getGlobalType(name: ts.__String, arity: number, reportErrors: boolean): ts.ObjectType | undefined { + const symbol = getGlobalTypeSymbol(name, reportErrors); + return symbol || reportErrors ? getTypeOfGlobalSymbol(symbol, arity) : undefined; + } - const members = ts.createSymbolTable([metaPropertySymbol]); - symbol.members = members; + function getGlobalTypedPropertyDescriptorType() { + // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times + return deferredGlobalTypedPropertyDescriptorType ||= getGlobalType("TypedPropertyDescriptor" as ts.__String, /*arity*/ 1, /*reportErrors*/ true) || emptyGenericType; + } - deferredGlobalImportMetaExpressionType = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); - } - return deferredGlobalImportMetaExpressionType; - } + function getGlobalTemplateStringsArrayType() { + // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times + return deferredGlobalTemplateStringsArrayType ||= getGlobalType("TemplateStringsArray" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || emptyObjectType; + } - function getGlobalImportCallOptionsType(reportErrors: boolean) { - return (deferredGlobalImportCallOptionsType ||= getGlobalType("ImportCallOptions" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } + function getGlobalImportMetaType() { + // We always report an error, so store a result in the event we could not resolve the symbol to prevent reporting it multiple times + return deferredGlobalImportMetaType ||= getGlobalType("ImportMeta" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || emptyObjectType; + } - function getGlobalESSymbolConstructorSymbol(reportErrors: boolean): ts.Symbol | undefined { - return deferredGlobalESSymbolConstructorSymbol ||= getGlobalValueSymbol("Symbol" as ts.__String, reportErrors); - } + function getGlobalImportMetaExpressionType() { + if (!deferredGlobalImportMetaExpressionType) { + // Create a synthetic type `ImportMetaExpression { meta: MetaProperty }` + const symbol = createSymbol(ts.SymbolFlags.None, "ImportMetaExpression" as ts.__String); + const importMetaType = getGlobalImportMetaType(); - function getGlobalESSymbolConstructorTypeSymbol(reportErrors: boolean): ts.Symbol | undefined { - return deferredGlobalESSymbolConstructorTypeSymbol ||= getGlobalTypeSymbol("SymbolConstructor" as ts.__String, reportErrors); - } - - function getGlobalESSymbolType(reportErrors: boolean) { - return (deferredGlobalESSymbolType ||= getGlobalType("Symbol" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - - function getGlobalPromiseType(reportErrors: boolean) { - return (deferredGlobalPromiseType ||= getGlobalType("Promise" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalPromiseLikeType(reportErrors: boolean) { - return (deferredGlobalPromiseLikeType ||= getGlobalType("PromiseLike" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalPromiseConstructorSymbol(reportErrors: boolean): ts.Symbol | undefined { - return deferredGlobalPromiseConstructorSymbol ||= getGlobalValueSymbol("Promise" as ts.__String, reportErrors); - } - - function getGlobalPromiseConstructorLikeType(reportErrors: boolean) { - return (deferredGlobalPromiseConstructorLikeType ||= getGlobalType("PromiseConstructorLike" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - - function getGlobalAsyncIterableType(reportErrors: boolean) { - return (deferredGlobalAsyncIterableType ||= getGlobalType("AsyncIterable" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncIteratorType(reportErrors: boolean) { - return (deferredGlobalAsyncIteratorType ||= getGlobalType("AsyncIterator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncIterableIteratorType(reportErrors: boolean) { - return (deferredGlobalAsyncIterableIteratorType ||= getGlobalType("AsyncIterableIterator" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncGeneratorType(reportErrors: boolean) { - return (deferredGlobalAsyncGeneratorType ||= getGlobalType("AsyncGenerator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalIterableType(reportErrors: boolean) { - return (deferredGlobalIterableType ||= getGlobalType("Iterable" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } + const metaPropertySymbol = createSymbol(ts.SymbolFlags.Property, "meta" as ts.__String, ts.CheckFlags.Readonly); + metaPropertySymbol.parent = symbol; + metaPropertySymbol.type = importMetaType; - function getGlobalIteratorType(reportErrors: boolean) { - return (deferredGlobalIteratorType ||= getGlobalType("Iterator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } + const members = ts.createSymbolTable([metaPropertySymbol]); + symbol.members = members; - function getGlobalIterableIteratorType(reportErrors: boolean) { - return (deferredGlobalIterableIteratorType ||= getGlobalType("IterableIterator" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + deferredGlobalImportMetaExpressionType = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); } + return deferredGlobalImportMetaExpressionType; + } - function getGlobalGeneratorType(reportErrors: boolean) { - return (deferredGlobalGeneratorType ||= getGlobalType("Generator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } + function getGlobalImportCallOptionsType(reportErrors: boolean) { + return (deferredGlobalImportCallOptionsType ||= getGlobalType("ImportCallOptions" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; + } - function getGlobalIteratorYieldResultType(reportErrors: boolean) { - return (deferredGlobalIteratorYieldResultType ||= getGlobalType("IteratorYieldResult" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } + function getGlobalESSymbolConstructorSymbol(reportErrors: boolean): ts.Symbol | undefined { + return deferredGlobalESSymbolConstructorSymbol ||= getGlobalValueSymbol("Symbol" as ts.__String, reportErrors); + } - function getGlobalIteratorReturnResultType(reportErrors: boolean) { - return (deferredGlobalIteratorReturnResultType ||= getGlobalType("IteratorReturnResult" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } + function getGlobalESSymbolConstructorTypeSymbol(reportErrors: boolean): ts.Symbol | undefined { + return deferredGlobalESSymbolConstructorTypeSymbol ||= getGlobalTypeSymbol("SymbolConstructor" as ts.__String, reportErrors); + } - function getGlobalTypeOrUndefined(name: ts.__String, arity = 0): ts.ObjectType | undefined { - const symbol = getGlobalSymbol(name, ts.SymbolFlags.Type, /*diagnostic*/ undefined); - return symbol && getTypeOfGlobalSymbol(symbol, arity) as ts.GenericType; - } + function getGlobalESSymbolType(reportErrors: boolean) { + return (deferredGlobalESSymbolType ||= getGlobalType("Symbol" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; + } - function getGlobalExtractSymbol(): ts.Symbol | undefined { - // We always report an error, so cache a result in the event we could not resolve the symbol to prevent reporting it multiple times - deferredGlobalExtractSymbol ||= getGlobalTypeAliasSymbol("Extract" as ts.__String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol; - return deferredGlobalExtractSymbol === unknownSymbol ? undefined : deferredGlobalExtractSymbol; - } + function getGlobalPromiseType(reportErrors: boolean) { + return (deferredGlobalPromiseType ||= getGlobalType("Promise" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function getGlobalOmitSymbol(): ts.Symbol | undefined { - // We always report an error, so cache a result in the event we could not resolve the symbol to prevent reporting it multiple times - deferredGlobalOmitSymbol ||= getGlobalTypeAliasSymbol("Omit" as ts.__String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol; - return deferredGlobalOmitSymbol === unknownSymbol ? undefined : deferredGlobalOmitSymbol; - } + function getGlobalPromiseLikeType(reportErrors: boolean) { + return (deferredGlobalPromiseLikeType ||= getGlobalType("PromiseLike" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function getGlobalAwaitedSymbol(reportErrors: boolean): ts.Symbol | undefined { - // Only cache `unknownSymbol` if we are reporting errors so that we don't report the error more than once. - deferredGlobalAwaitedSymbol ||= getGlobalTypeAliasSymbol("Awaited" as ts.__String, /*arity*/ 1, reportErrors) || (reportErrors ? unknownSymbol : undefined); - return deferredGlobalAwaitedSymbol === unknownSymbol ? undefined : deferredGlobalAwaitedSymbol; - } + function getGlobalPromiseConstructorSymbol(reportErrors: boolean): ts.Symbol | undefined { + return deferredGlobalPromiseConstructorSymbol ||= getGlobalValueSymbol("Promise" as ts.__String, reportErrors); + } - function getGlobalBigIntType(reportErrors: boolean) { - return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } + function getGlobalPromiseConstructorLikeType(reportErrors: boolean) { + return (deferredGlobalPromiseConstructorLikeType ||= getGlobalType("PromiseConstructorLike" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; + } - /** - * Instantiates a global type that is generic with some element type, and returns that instantiation. - */ - function createTypeFromGenericGlobalType(genericGlobalType: ts.GenericType, typeArguments: readonly ts.Type[]): ts.ObjectType { - return genericGlobalType !== emptyGenericType ? createTypeReference(genericGlobalType, typeArguments) : emptyObjectType; - } + function getGlobalAsyncIterableType(reportErrors: boolean) { + return (deferredGlobalAsyncIterableType ||= getGlobalType("AsyncIterable" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function createTypedPropertyDescriptorType(propertyType: ts.Type): ts.Type { - return createTypeFromGenericGlobalType(getGlobalTypedPropertyDescriptorType(), [propertyType]); - } + function getGlobalAsyncIteratorType(reportErrors: boolean) { + return (deferredGlobalAsyncIteratorType ||= getGlobalType("AsyncIterator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; + } - function createIterableType(iteratedType: ts.Type): ts.Type { - return createTypeFromGenericGlobalType(getGlobalIterableType(/*reportErrors*/ true), [iteratedType]); - } + function getGlobalAsyncIterableIteratorType(reportErrors: boolean) { + return (deferredGlobalAsyncIterableIteratorType ||= getGlobalType("AsyncIterableIterator" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function createArrayType(elementType: ts.Type, readonly?: boolean): ts.ObjectType { - return createTypeFromGenericGlobalType(readonly ? globalReadonlyArrayType : globalArrayType, [elementType]); - } + function getGlobalAsyncGeneratorType(reportErrors: boolean) { + return (deferredGlobalAsyncGeneratorType ||= getGlobalType("AsyncGenerator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; + } - function getTupleElementFlags(node: ts.TypeNode) { - switch (node.kind) { - case ts.SyntaxKind.OptionalType: - return ts.ElementFlags.Optional; - case ts.SyntaxKind.RestType: - return getRestTypeElementFlags(node as ts.RestTypeNode); - case ts.SyntaxKind.NamedTupleMember: - return (node as ts.NamedTupleMember).questionToken ? ts.ElementFlags.Optional : - (node as ts.NamedTupleMember).dotDotDotToken ? getRestTypeElementFlags(node as ts.NamedTupleMember) : - ts.ElementFlags.Required; - default: - return ts.ElementFlags.Required; - } - } + function getGlobalIterableType(reportErrors: boolean) { + return (deferredGlobalIterableType ||= getGlobalType("Iterable" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function getRestTypeElementFlags(node: ts.RestTypeNode | ts.NamedTupleMember) { - return getArrayElementTypeNode(node.type) ? ts.ElementFlags.Rest : ts.ElementFlags.Variadic; - } + function getGlobalIteratorType(reportErrors: boolean) { + return (deferredGlobalIteratorType ||= getGlobalType("Iterator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; + } - function getArrayOrTupleTargetType(node: ts.ArrayTypeNode | ts.TupleTypeNode): ts.GenericType { - const readonly = isReadonlyTypeOperator(node.parent); - const elementType = getArrayElementTypeNode(node); - if (elementType) { - return readonly ? globalReadonlyArrayType : globalArrayType; - } - const elementFlags = ts.map((node as ts.TupleTypeNode).elements, getTupleElementFlags); - const missingName = ts.some((node as ts.TupleTypeNode).elements, e => e.kind !== ts.SyntaxKind.NamedTupleMember); - return getTupleTargetType(elementFlags, readonly, /*associatedNames*/ missingName ? undefined : (node as ts.TupleTypeNode).elements as readonly ts.NamedTupleMember[]); - } + function getGlobalIterableIteratorType(reportErrors: boolean) { + return (deferredGlobalIterableIteratorType ||= getGlobalType("IterableIterator" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - // 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: ts.TypeReferenceNode | ts.ArrayTypeNode | ts.TupleTypeNode, hasDefaultTypeArguments?: boolean) { - return !!getAliasSymbolForTypeNode(node) || isResolvedByTypeAlias(node) && (node.kind === ts.SyntaxKind.ArrayType ? mayResolveTypeAlias(node.elementType) : - node.kind === ts.SyntaxKind.TupleType ? ts.some(node.elements, mayResolveTypeAlias) : - hasDefaultTypeArguments || ts.some(node.typeArguments, mayResolveTypeAlias)); - } + function getGlobalGeneratorType(reportErrors: boolean) { + return (deferredGlobalGeneratorType ||= getGlobalType("Generator" as ts.__String, /*arity*/ 3, reportErrors)) || emptyGenericType; + } - // 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: ts.Node): boolean { - const parent = node.parent; - switch (parent.kind) { - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.NamedTupleMember: - case ts.SyntaxKind.TypeReference: - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - case ts.SyntaxKind.IndexedAccessType: - case ts.SyntaxKind.ConditionalType: - case ts.SyntaxKind.TypeOperator: - case ts.SyntaxKind.ArrayType: - case ts.SyntaxKind.TupleType: - return isResolvedByTypeAlias(parent); - case ts.SyntaxKind.TypeAliasDeclaration: - return true; - } - return false; - } + function getGlobalIteratorYieldResultType(reportErrors: boolean) { + return (deferredGlobalIteratorYieldResultType ||= getGlobalType("IteratorYieldResult" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - // Return true if resolving the given node (i.e. getTypeFromTypeNode) possibly causes resolution - // of a type alias. - function mayResolveTypeAlias(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.TypeReference: - return isJSDocTypeReference(node) || !!(resolveTypeReferenceName(node as ts.TypeReferenceNode, ts.SymbolFlags.Type).flags & ts.SymbolFlags.TypeAlias); - case ts.SyntaxKind.TypeQuery: - return true; - case ts.SyntaxKind.TypeOperator: - return (node as ts.TypeOperatorNode).operator !== ts.SyntaxKind.UniqueKeyword && mayResolveTypeAlias((node as ts.TypeOperatorNode).type); - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.OptionalType: - case ts.SyntaxKind.NamedTupleMember: - case ts.SyntaxKind.JSDocOptionalType: - case ts.SyntaxKind.JSDocNullableType: - case ts.SyntaxKind.JSDocNonNullableType: - case ts.SyntaxKind.JSDocTypeExpression: - return mayResolveTypeAlias((node as ts.ParenthesizedTypeNode | ts.OptionalTypeNode | ts.JSDocTypeReferencingNode | ts.NamedTupleMember).type); - case ts.SyntaxKind.RestType: - return (node as ts.RestTypeNode).type.kind !== ts.SyntaxKind.ArrayType || mayResolveTypeAlias(((node as ts.RestTypeNode).type as ts.ArrayTypeNode).elementType); - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - return ts.some((node as ts.UnionOrIntersectionTypeNode).types, mayResolveTypeAlias); - case ts.SyntaxKind.IndexedAccessType: - return mayResolveTypeAlias((node as ts.IndexedAccessTypeNode).objectType) || mayResolveTypeAlias((node as ts.IndexedAccessTypeNode).indexType); - case ts.SyntaxKind.ConditionalType: - return mayResolveTypeAlias((node as ts.ConditionalTypeNode).checkType) || mayResolveTypeAlias((node as ts.ConditionalTypeNode).extendsType) || - mayResolveTypeAlias((node as ts.ConditionalTypeNode).trueType) || mayResolveTypeAlias((node as ts.ConditionalTypeNode).falseType); - } - return false; - } - function getTypeFromArrayOrTupleTypeNode(node: ts.ArrayTypeNode | ts.TupleTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const target = getArrayOrTupleTargetType(node); - if (target === emptyGenericType) { - links.resolvedType = emptyObjectType; - } - else if (!(node.kind === ts.SyntaxKind.TupleType && ts.some(node.elements, e => !!(getTupleElementFlags(e) & ts.ElementFlags.Variadic))) && isDeferredTypeReferenceNode(node)) { - links.resolvedType = node.kind === ts.SyntaxKind.TupleType && node.elements.length === 0 ? target : - createDeferredTypeReference(target, node, /*mapper*/ undefined); - } - else { - const elementTypes = node.kind === ts.SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : ts.map(node.elements, getTypeFromTypeNode); - links.resolvedType = createNormalizedTypeReference(target, elementTypes); - } - } - return links.resolvedType; - } + function getGlobalIteratorReturnResultType(reportErrors: boolean) { + return (deferredGlobalIteratorReturnResultType ||= getGlobalType("IteratorReturnResult" as ts.__String, /*arity*/ 1, reportErrors)) || emptyGenericType; + } - function isReadonlyTypeOperator(node: ts.Node) { - return ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword; - } + function getGlobalTypeOrUndefined(name: ts.__String, arity = 0): ts.ObjectType | undefined { + const symbol = getGlobalSymbol(name, ts.SymbolFlags.Type, /*diagnostic*/ undefined); + return symbol && getTypeOfGlobalSymbol(symbol, arity) as ts.GenericType; + } - function createTupleType(elementTypes: readonly ts.Type[], elementFlags?: readonly ts.ElementFlags[], readonly = false, namedMemberDeclarations?: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[]) { - const tupleTarget = getTupleTargetType(elementFlags || ts.map(elementTypes, _ => ts.ElementFlags.Required), readonly, namedMemberDeclarations); - return tupleTarget === emptyGenericType ? emptyObjectType : - elementTypes.length ? createNormalizedTypeReference(tupleTarget, elementTypes) : - tupleTarget; - } + function getGlobalExtractSymbol(): ts.Symbol | undefined { + // We always report an error, so cache a result in the event we could not resolve the symbol to prevent reporting it multiple times + deferredGlobalExtractSymbol ||= getGlobalTypeAliasSymbol("Extract" as ts.__String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol; + return deferredGlobalExtractSymbol === unknownSymbol ? undefined : deferredGlobalExtractSymbol; + } - function getTupleTargetType(elementFlags: readonly ts.ElementFlags[], readonly: boolean, namedMemberDeclarations?: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[]): ts.GenericType { - if (elementFlags.length === 1 && elementFlags[0] & ts.ElementFlags.Rest) { - // [...X[]] is equivalent to just X[] - return readonly ? globalReadonlyArrayType : globalArrayType; - } - const key = ts.map(elementFlags, f => f & ts.ElementFlags.Required ? "#" : f & ts.ElementFlags.Optional ? "?" : f & ts.ElementFlags.Rest ? "." : "*").join() + - (readonly ? "R" : "") + - (namedMemberDeclarations && namedMemberDeclarations.length ? "," + ts.map(namedMemberDeclarations, getNodeId).join(",") : ""); - let type = tupleTypes.get(key); - if (!type) { - tupleTypes.set(key, type = createTupleTargetType(elementFlags, readonly, namedMemberDeclarations)); - } - return type; - } + function getGlobalOmitSymbol(): ts.Symbol | undefined { + // We always report an error, so cache a result in the event we could not resolve the symbol to prevent reporting it multiple times + deferredGlobalOmitSymbol ||= getGlobalTypeAliasSymbol("Omit" as ts.__String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol; + return deferredGlobalOmitSymbol === unknownSymbol ? undefined : deferredGlobalOmitSymbol; + } - // 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 createTupleTargetType(elementFlags: readonly ts.ElementFlags[], readonly: boolean, namedMemberDeclarations: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[] | undefined): ts.TupleType { - const arity = elementFlags.length; - const minLength = ts.countWhere(elementFlags, f => !!(f & (ts.ElementFlags.Required | ts.ElementFlags.Variadic))); - let typeParameters: ts.TypeParameter[] | undefined; - const properties: ts.Symbol[] = []; - let combinedFlags: ts.ElementFlags = 0; - if (arity) { - typeParameters = new Array(arity); - for (let i = 0; i < arity; i++) { - const typeParameter = typeParameters[i] = createTypeParameter(); - const flags = elementFlags[i]; - combinedFlags |= flags; - if (!(combinedFlags & ts.ElementFlags.Variable)) { - const property = createSymbol(ts.SymbolFlags.Property | (flags & ts.ElementFlags.Optional ? ts.SymbolFlags.Optional : 0), "" + i as ts.__String, readonly ? ts.CheckFlags.Readonly : 0); - property.tupleLabelDeclaration = namedMemberDeclarations?.[i]; - property.type = typeParameter; - properties.push(property); - } - } - } - const fixedLength = properties.length; - const lengthSymbol = createSymbol(ts.SymbolFlags.Property, "length" as ts.__String, readonly ? ts.CheckFlags.Readonly : 0); - if (combinedFlags & ts.ElementFlags.Variable) { - lengthSymbol.type = numberType; - } - else { - const literalTypes = []; - for (let i = minLength; i <= arity; i++) - literalTypes.push(getNumberLiteralType(i)); - lengthSymbol.type = getUnionType(literalTypes); - } - properties.push(lengthSymbol); - const type = createObjectType(ts.ObjectFlags.Tuple | ts.ObjectFlags.Reference) as ts.TupleType & ts.InterfaceTypeWithDeclaredMembers; - type.typeParameters = typeParameters; - type.outerTypeParameters = undefined; - type.localTypeParameters = typeParameters; - type.instantiations = new ts.Map(); - type.instantiations.set(getTypeListId(type.typeParameters), type as ts.GenericType); - type.target = type as ts.GenericType; - type.resolvedTypeArguments = type.typeParameters; - type.thisType = createTypeParameter(); - type.thisType.isThisType = true; - type.thisType.constraint = type; - type.declaredProperties = properties; - type.declaredCallSignatures = ts.emptyArray; - type.declaredConstructSignatures = ts.emptyArray; - type.declaredIndexInfos = ts.emptyArray; - type.elementFlags = elementFlags; - type.minLength = minLength; - type.fixedLength = fixedLength; - type.hasRestElement = !!(combinedFlags & ts.ElementFlags.Variable); - type.combinedFlags = combinedFlags; - type.readonly = readonly; - type.labeledElementDeclarations = namedMemberDeclarations; - return type; - } + function getGlobalAwaitedSymbol(reportErrors: boolean): ts.Symbol | undefined { + // Only cache `unknownSymbol` if we are reporting errors so that we don't report the error more than once. + deferredGlobalAwaitedSymbol ||= getGlobalTypeAliasSymbol("Awaited" as ts.__String, /*arity*/ 1, reportErrors) || (reportErrors ? unknownSymbol : undefined); + return deferredGlobalAwaitedSymbol === unknownSymbol ? undefined : deferredGlobalAwaitedSymbol; + } - function createNormalizedTypeReference(target: ts.GenericType, typeArguments: readonly ts.Type[] | undefined) { - return target.objectFlags & ts.ObjectFlags.Tuple ? createNormalizedTupleType(target as ts.TupleType, typeArguments!) : createTypeReference(target, typeArguments); - } - - function createNormalizedTupleType(target: ts.TupleType, elementTypes: readonly ts.Type[]): ts.Type { - if (!(target.combinedFlags & ts.ElementFlags.NonRequired)) { - // No need to normalize when we only have regular required elements - return createTypeReference(target, elementTypes); - } - if (target.combinedFlags & ts.ElementFlags.Variadic) { - // Transform [A, ...(X | Y | Z)] into [A, ...X] | [A, ...Y] | [A, ...Z] - const unionIndex = ts.findIndex(elementTypes, (t, i) => !!(target.elementFlags[i] & ts.ElementFlags.Variadic && t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Union))); - if (unionIndex >= 0) { - return checkCrossProductUnion(ts.map(elementTypes, (t, i) => target.elementFlags[i] & ts.ElementFlags.Variadic ? t : unknownType)) ? - mapType(elementTypes[unionIndex], t => createNormalizedTupleType(target, ts.replaceElement(elementTypes, unionIndex, t))) : - errorType; - } - } - // We have optional, rest, or variadic elements that may need normalizing. Normalization ensures that all variadic - // elements are generic and that the tuple type has one of the following layouts, disregarding variadic elements: - // (1) Zero or more required elements, followed by zero or more optional elements, followed by zero or one rest element. - // (2) Zero or more required elements, followed by a rest element, followed by zero or more required elements. - // In either layout, zero or more generic variadic elements may be present at any location. - const expandedTypes: ts.Type[] = []; - const expandedFlags: ts.ElementFlags[] = []; - let expandedDeclarations: (ts.NamedTupleMember | ts.ParameterDeclaration)[] | undefined = []; - let lastRequiredIndex = -1; - let firstRestIndex = -1; - let lastOptionalOrRestIndex = -1; - for (let i = 0; i < elementTypes.length; i++) { - const type = elementTypes[i]; - const flags = target.elementFlags[i]; - if (flags & ts.ElementFlags.Variadic) { - if (type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type)) { - // Generic variadic elements stay as they are. - addElement(type, ts.ElementFlags.Variadic, target.labeledElementDeclarations?.[i]); - } - else if (isTupleType(type)) { - const elements = getTypeArguments(type); - if (elements.length + expandedTypes.length >= 10000) { - error(currentNode, ts.isPartOfTypeNode(currentNode!) - ? ts.Diagnostics.Type_produces_a_tuple_type_that_is_too_large_to_represent - : ts.Diagnostics.Expression_produces_a_tuple_type_that_is_too_large_to_represent); - return errorType; - } - // Spread variadic elements with tuple types into the resulting tuple. - ts.forEach(elements, (t, n) => addElement(t, type.target.elementFlags[n], type.target.labeledElementDeclarations?.[n])); - } - else { - // Treat everything else as an array type and create a rest element. - addElement(isArrayLikeType(type) && getIndexTypeOfType(type, numberType) || errorType, ts.ElementFlags.Rest, target.labeledElementDeclarations?.[i]); - } - } - else { - // Copy other element kinds with no change. - addElement(type, flags, target.labeledElementDeclarations?.[i]); - } - } - // Turn optional elements preceding the last required element into required elements - for (let i = 0; i < lastRequiredIndex; i++) { - if (expandedFlags[i] & ts.ElementFlags.Optional) - expandedFlags[i] = ts.ElementFlags.Required; - } - if (firstRestIndex >= 0 && firstRestIndex < lastOptionalOrRestIndex) { - // Turn elements between first rest and last optional/rest into a single rest element - expandedTypes[firstRestIndex] = getUnionType(ts.sameMap(expandedTypes.slice(firstRestIndex, lastOptionalOrRestIndex + 1), (t, i) => expandedFlags[firstRestIndex + i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t)); - expandedTypes.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); - expandedFlags.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); - expandedDeclarations?.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); - } - const tupleTarget = getTupleTargetType(expandedFlags, target.readonly, expandedDeclarations); - return tupleTarget === emptyGenericType ? emptyObjectType : - expandedFlags.length ? createTypeReference(tupleTarget, expandedTypes) : - tupleTarget; + function getGlobalBigIntType(reportErrors: boolean) { + return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as ts.__String, /*arity*/ 0, reportErrors)) || emptyObjectType; + } - function addElement(type: ts.Type, flags: ts.ElementFlags, declaration: ts.NamedTupleMember | ts.ParameterDeclaration | undefined) { - if (flags & ts.ElementFlags.Required) { - lastRequiredIndex = expandedFlags.length; - } - if (flags & ts.ElementFlags.Rest && firstRestIndex < 0) { - firstRestIndex = expandedFlags.length; - } - if (flags & (ts.ElementFlags.Optional | ts.ElementFlags.Rest)) { - lastOptionalOrRestIndex = expandedFlags.length; - } - expandedTypes.push(type); - expandedFlags.push(flags); - if (expandedDeclarations && declaration) { - expandedDeclarations.push(declaration); - } - else { - expandedDeclarations = undefined; - } - } - } + /** + * Instantiates a global type that is generic with some element type, and returns that instantiation. + */ + function createTypeFromGenericGlobalType(genericGlobalType: ts.GenericType, typeArguments: readonly ts.Type[]): ts.ObjectType { + return genericGlobalType !== emptyGenericType ? createTypeReference(genericGlobalType, typeArguments) : emptyObjectType; + } - function sliceTupleType(type: ts.TupleTypeReference, index: number, endSkipCount = 0) { - const target = type.target; - const endIndex = getTypeReferenceArity(type) - endSkipCount; - return index > target.fixedLength ? getRestArrayTypeOfTupleType(type) || createTupleType(ts.emptyArray) : - createTupleType(getTypeArguments(type).slice(index, endIndex), target.elementFlags.slice(index, endIndex), - /*readonly*/ false, target.labeledElementDeclarations && target.labeledElementDeclarations.slice(index, endIndex)); - } + function createTypedPropertyDescriptorType(propertyType: ts.Type): ts.Type { + return createTypeFromGenericGlobalType(getGlobalTypedPropertyDescriptorType(), [propertyType]); + } - function getKnownKeysOfTupleType(type: ts.TupleTypeReference) { - return getUnionType(ts.append(ts.arrayOf(type.target.fixedLength, i => getStringLiteralType("" + i)), getIndexType(type.target.readonly ? globalReadonlyArrayType : globalArrayType))); - } + function createIterableType(iteratedType: ts.Type): ts.Type { + return createTypeFromGenericGlobalType(getGlobalIterableType(/*reportErrors*/ true), [iteratedType]); + } - // Return count of starting consecutive tuple elements of the given kind(s) - function getStartElementCount(type: ts.TupleType, flags: ts.ElementFlags) { - const index = ts.findIndex(type.elementFlags, f => !(f & flags)); - return index >= 0 ? index : type.elementFlags.length; - } + function createArrayType(elementType: ts.Type, readonly?: boolean): ts.ObjectType { + return createTypeFromGenericGlobalType(readonly ? globalReadonlyArrayType : globalArrayType, [elementType]); + } - // Return count of ending consecutive tuple elements of the given kind(s) - function getEndElementCount(type: ts.TupleType, flags: ts.ElementFlags) { - return type.elementFlags.length - ts.findLastIndex(type.elementFlags, f => !(f & flags)) - 1; + function getTupleElementFlags(node: ts.TypeNode) { + switch (node.kind) { + case ts.SyntaxKind.OptionalType: + return ts.ElementFlags.Optional; + case ts.SyntaxKind.RestType: + return getRestTypeElementFlags(node as ts.RestTypeNode); + case ts.SyntaxKind.NamedTupleMember: + return (node as ts.NamedTupleMember).questionToken ? ts.ElementFlags.Optional : + (node as ts.NamedTupleMember).dotDotDotToken ? getRestTypeElementFlags(node as ts.NamedTupleMember) : + ts.ElementFlags.Required; + default: + return ts.ElementFlags.Required; } + } - function getTypeFromOptionalTypeNode(node: ts.OptionalTypeNode): ts.Type { - return addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true); - } + function getRestTypeElementFlags(node: ts.RestTypeNode | ts.NamedTupleMember) { + return getArrayElementTypeNode(node.type) ? ts.ElementFlags.Rest : ts.ElementFlags.Variadic; + } - function getTypeId(type: ts.Type): ts.TypeId { - return type.id; + function getArrayOrTupleTargetType(node: ts.ArrayTypeNode | ts.TupleTypeNode): ts.GenericType { + const readonly = isReadonlyTypeOperator(node.parent); + const elementType = getArrayElementTypeNode(node); + if (elementType) { + return readonly ? globalReadonlyArrayType : globalArrayType; } + const elementFlags = ts.map((node as ts.TupleTypeNode).elements, getTupleElementFlags); + const missingName = ts.some((node as ts.TupleTypeNode).elements, e => e.kind !== ts.SyntaxKind.NamedTupleMember); + return getTupleTargetType(elementFlags, readonly, /*associatedNames*/ missingName ? undefined : (node as ts.TupleTypeNode).elements as readonly ts.NamedTupleMember[]); + } - function containsType(types: readonly ts.Type[], type: ts.Type): boolean { - return ts.binarySearch(types, type, getTypeId, ts.compareValues) >= 0; - } + // 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: ts.TypeReferenceNode | ts.ArrayTypeNode | ts.TupleTypeNode, hasDefaultTypeArguments?: boolean) { + return !!getAliasSymbolForTypeNode(node) || isResolvedByTypeAlias(node) && (node.kind === ts.SyntaxKind.ArrayType ? mayResolveTypeAlias(node.elementType) : + node.kind === ts.SyntaxKind.TupleType ? ts.some(node.elements, mayResolveTypeAlias) : + hasDefaultTypeArguments || ts.some(node.typeArguments, mayResolveTypeAlias)); + } - function insertType(types: ts.Type[], type: ts.Type): boolean { - const index = ts.binarySearch(types, type, getTypeId, ts.compareValues); - if (index < 0) { - types.splice(~index, 0, type); + // 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: ts.Node): boolean { + const parent = node.parent; + switch (parent.kind) { + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.NamedTupleMember: + case ts.SyntaxKind.TypeReference: + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + case ts.SyntaxKind.IndexedAccessType: + case ts.SyntaxKind.ConditionalType: + case ts.SyntaxKind.TypeOperator: + case ts.SyntaxKind.ArrayType: + case ts.SyntaxKind.TupleType: + return isResolvedByTypeAlias(parent); + case ts.SyntaxKind.TypeAliasDeclaration: return true; - } - return false; - } - - function addTypeToUnion(typeSet: ts.Type[], includes: ts.TypeFlags, type: ts.Type) { - const flags = type.flags; - if (flags & ts.TypeFlags.Union) { - return addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? ts.TypeFlags.Union : 0), (type as ts.UnionType).types); - } - // We ignore 'never' types in unions - if (!(flags & ts.TypeFlags.Never)) { - includes |= flags & ts.TypeFlags.IncludesMask; - if (flags & ts.TypeFlags.Instantiable) - includes |= ts.TypeFlags.IncludesInstantiable; - if (type === wildcardType) - includes |= ts.TypeFlags.IncludesWildcard; - if (!strictNullChecks && flags & ts.TypeFlags.Nullable) { - if (!(ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType)) - includes |= ts.TypeFlags.IncludesNonWideningType; - } - else { - const len = typeSet.length; - const index = len && type.id > typeSet[len - 1].id ? ~len : ts.binarySearch(typeSet, type, getTypeId, ts.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: ts.Type[], includes: ts.TypeFlags, types: readonly ts.Type[]): ts.TypeFlags { - for (const type of types) { - includes = addTypeToUnion(typeSet, includes, type); - } - return includes; - } - - function removeSubtypes(types: ts.Type[], hasObjectTypes: boolean): ts.Type[] | undefined { - // [] and [T] immediately reduce to [] and [T] respectively - if (types.length < 2) { - return types; - } - - const id = getTypeListId(types); - const match = subtypeReductionCache.get(id); - if (match) { - return match; - } - - // We assume that redundant primitive types have already been removed from the types array and that there - // are no any and unknown types in the array. Thus, the only possible supertypes for primitive types are empty - // object types, and if none of those are present we can exclude primitive types from the subtype check. - const hasEmptyObject = hasObjectTypes && ts.some(types, t => !!(t.flags & ts.TypeFlags.Object) && !isGenericMappedType(t) && isEmptyResolvedType(resolveStructuredTypeMembers(t as ts.ObjectType))); - const len = types.length; - let i = len; - let count = 0; - while (i > 0) { - i--; - const source = types[i]; - if (hasEmptyObject || source.flags & ts.TypeFlags.StructuredOrInstantiable) { - // Find the first property with a unit type, if any. When constituents have a property by the same name - // but of a different unit type, we can quickly disqualify them from subtype checks. This helps subtype - // reduction of large discriminated union types. - const keyProperty = source.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive) ? - ts.find(getPropertiesOfType(source), p => isUnitType(getTypeOfSymbol(p))) : - undefined; - const keyPropertyType = keyProperty && getRegularTypeOfLiteralType(getTypeOfSymbol(keyProperty)); - 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 1M we deem the union type too complex to represent. This for example - // caps union types at 1000 unique object types. - const estimatedCount = (count / (len - i)) * len; - if (estimatedCount > 1000000) { - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) }); - error(currentNode, ts.Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); - return undefined; - } - } - count++; - if (keyProperty && target.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive)) { - const t = getTypeOfPropertyOfType(target, keyProperty.escapedName); - if (t && isUnitType(t) && getRegularTypeOfLiteralType(t) !== keyPropertyType) { - continue; - } - } - if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (!(ts.getObjectFlags(getTargetType(source)) & ts.ObjectFlags.Class) || - !(ts.getObjectFlags(getTargetType(target)) & ts.ObjectFlags.Class) || - isTypeDerivedFrom(source, target))) { - ts.orderedRemoveItemAt(types, i); - break; - } - } - } - } - } - subtypeReductionCache.set(id, types); - return types; - } - - function removeRedundantLiteralTypes(types: ts.Type[], includes: ts.TypeFlags, reduceVoidUndefined: boolean) { - let i = types.length; - while (i > 0) { - i--; - const t = types[i]; - const flags = t.flags; - const remove = flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && includes & ts.TypeFlags.String || - flags & ts.TypeFlags.NumberLiteral && includes & ts.TypeFlags.Number || - flags & ts.TypeFlags.BigIntLiteral && includes & ts.TypeFlags.BigInt || - flags & ts.TypeFlags.UniqueESSymbol && includes & ts.TypeFlags.ESSymbol || - reduceVoidUndefined && flags & ts.TypeFlags.Undefined && includes & ts.TypeFlags.Void || - isFreshLiteralType(t) && containsType(types, (t as ts.LiteralType).regularType); - if (remove) { - ts.orderedRemoveItemAt(types, i); - } - } - } - - function removeStringLiteralsMatchedByTemplateLiterals(types: ts.Type[]) { - const templates = ts.filter(types, isPatternLiteralType) as ts.TemplateLiteralType[]; - if (templates.length) { - let i = types.length; - while (i > 0) { - i--; - const t = types[i]; - if (t.flags & ts.TypeFlags.StringLiteral && ts.some(templates, template => isTypeMatchedByTemplateLiteralType(t, template))) { - ts.orderedRemoveItemAt(types, i); - } - } - } - } - - function isNamedUnionType(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Union && (type.aliasSymbol || (type as ts.UnionType).origin)); - } - - function addNamedUnions(namedUnions: ts.Type[], types: readonly ts.Type[]) { - for (const t of types) { - if (t.flags & ts.TypeFlags.Union) { - const origin = (t as ts.UnionType).origin; - if (t.aliasSymbol || origin && !(origin.flags & ts.TypeFlags.Union)) { - ts.pushIfUnique(namedUnions, t); - } - else if (origin && origin.flags & ts.TypeFlags.Union) { - addNamedUnions(namedUnions, (origin as ts.UnionType).types); - } - } - } - } - - function createOriginUnionOrIntersectionType(flags: ts.TypeFlags, types: ts.Type[]) { - const result = createOriginType(flags) as ts.UnionOrIntersectionType; - result.types = types; - return result; - } - - // 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: ts.UnionReduction = ts.UnionReduction.Literal, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[], origin?: ts.Type): ts.Type { - if (types.length === 0) { - return neverType; - } - if (types.length === 1) { - return types[0]; - } - let typeSet: ts.Type[] | undefined = []; - const includes = addTypesToUnion(typeSet, 0, types); - if (unionReduction !== ts.UnionReduction.None) { - if (includes & ts.TypeFlags.AnyOrUnknown) { - return includes & ts.TypeFlags.Any ? - includes & ts.TypeFlags.IncludesWildcard ? wildcardType : anyType : - includes & ts.TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType; - } - if (exactOptionalPropertyTypes && includes & ts.TypeFlags.Undefined) { - const missingIndex = ts.binarySearch(typeSet, missingType, getTypeId, ts.compareValues); - if (missingIndex >= 0 && containsType(typeSet, undefinedType)) { - ts.orderedRemoveItemAt(typeSet, missingIndex); - } - } - if (includes & (ts.TypeFlags.Literal | ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || includes & ts.TypeFlags.Void && includes & ts.TypeFlags.Undefined) { - removeRedundantLiteralTypes(typeSet, includes, !!(unionReduction & ts.UnionReduction.Subtype)); - } - if (includes & ts.TypeFlags.StringLiteral && includes & ts.TypeFlags.TemplateLiteral) { - removeStringLiteralsMatchedByTemplateLiterals(typeSet); - } - if (unionReduction === ts.UnionReduction.Subtype) { - typeSet = removeSubtypes(typeSet, !!(includes & ts.TypeFlags.Object)); - if (!typeSet) { - return errorType; - } - } - if (typeSet.length === 0) { - return includes & ts.TypeFlags.Null ? includes & ts.TypeFlags.IncludesNonWideningType ? nullType : nullWideningType : - includes & ts.TypeFlags.Undefined ? includes & ts.TypeFlags.IncludesNonWideningType ? undefinedType : undefinedWideningType : - neverType; - } - } - if (!origin && includes & ts.TypeFlags.Union) { - const namedUnions: ts.Type[] = []; - addNamedUnions(namedUnions, types); - const reducedTypes: ts.Type[] = []; - for (const t of typeSet) { - if (!ts.some(namedUnions, union => containsType((union as ts.UnionType).types, t))) { - reducedTypes.push(t); - } - } - if (!aliasSymbol && namedUnions.length === 1 && reducedTypes.length === 0) { - return namedUnions[0]; - } - // We create a denormalized origin type only when the union was created from one or more named unions - // (unions with alias symbols or origins) and when there is no overlap between those named unions. - const namedTypesCount = ts.reduceLeft(namedUnions, (sum, union) => sum + (union as ts.UnionType).types.length, 0); - if (namedTypesCount + reducedTypes.length === typeSet.length) { - for (const t of namedUnions) { - insertType(reducedTypes, t); - } - origin = createOriginUnionOrIntersectionType(ts.TypeFlags.Union, reducedTypes); - } - } - const objectFlags = (includes & ts.TypeFlags.NotPrimitiveUnion ? 0 : ts.ObjectFlags.PrimitiveUnion) | - (includes & ts.TypeFlags.Intersection ? ts.ObjectFlags.ContainsIntersections : 0); - return getUnionTypeFromSortedList(typeSet, objectFlags, aliasSymbol, aliasTypeArguments, origin); - } - - function getUnionOrIntersectionTypePredicate(signatures: readonly ts.Signature[], kind: ts.TypeFlags | undefined): ts.TypePredicate | undefined { - let first: ts.TypePredicate | undefined; - const types: ts.Type[] = []; - for (const sig of signatures) { - const pred = getTypePredicateOfSignature(sig); - if (!pred || pred.kind === ts.TypePredicateKind.AssertsThis || pred.kind === ts.TypePredicateKind.AssertsIdentifier) { - if (kind !== ts.TypeFlags.Intersection) { - continue; - } - else { - return; // intersections demand all members be type predicates for the result to have a predicate - } - } - - if (first) { - if (!typePredicateKindsMatch(first, pred)) { - // No common type predicate. - return undefined; - } - } - else { - first = pred; - } - types.push(pred.type); - } - if (!first) { - // No signatures had a type predicate. - return undefined; - } - const compositeType = getUnionOrIntersectionType(types, kind); - return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType); - } - - function typePredicateKindsMatch(a: ts.TypePredicate, b: ts.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: ts.ObjectFlags, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[], origin?: ts.Type): ts.Type { - if (types.length === 0) { - return neverType; - } - if (types.length === 1) { - return types[0]; - } - const typeKey = !origin ? getTypeListId(types) : - origin.flags & ts.TypeFlags.Union ? `|${getTypeListId((origin as ts.UnionType).types)}` : - origin.flags & ts.TypeFlags.Intersection ? `&${getTypeListId((origin as ts.IntersectionType).types)}` : - `#${(origin as ts.IndexType).type.id}|${getTypeListId(types)}`; // origin type id alone is insufficient, as `keyof x` may resolve to multiple WIP values while `x` is still resolving - const id = typeKey + getAliasId(aliasSymbol, aliasTypeArguments); - let type = unionTypes.get(id); - if (!type) { - type = createType(ts.TypeFlags.Union) as ts.UnionType; - type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ ts.TypeFlags.Nullable); - type.types = types; - type.origin = origin; - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = aliasTypeArguments; - if (types.length === 2 && types[0].flags & ts.TypeFlags.BooleanLiteral && types[1].flags & ts.TypeFlags.BooleanLiteral) { - type.flags |= ts.TypeFlags.Boolean; - (type as ts.UnionType & ts.IntrinsicType).intrinsicName = "boolean"; - } - unionTypes.set(id, type); - } - return type; - } - - function getTypeFromUnionTypeNode(node: ts.UnionTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const aliasSymbol = getAliasSymbolForTypeNode(node); - links.resolvedType = getUnionType(ts.map(node.types, getTypeFromTypeNode), ts.UnionReduction.Literal, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); - } - return links.resolvedType; } + return false; + } - function addTypeToIntersection(typeSet: ts.ESMap, includes: ts.TypeFlags, type: ts.Type) { - const flags = type.flags; - if (flags & ts.TypeFlags.Intersection) { - return addTypesToIntersection(typeSet, includes, (type as ts.IntersectionType).types); + // Return true if resolving the given node (i.e. getTypeFromTypeNode) possibly causes resolution + // of a type alias. + function mayResolveTypeAlias(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.TypeReference: + return isJSDocTypeReference(node) || !!(resolveTypeReferenceName(node as ts.TypeReferenceNode, ts.SymbolFlags.Type).flags & ts.SymbolFlags.TypeAlias); + case ts.SyntaxKind.TypeQuery: + return true; + case ts.SyntaxKind.TypeOperator: + return (node as ts.TypeOperatorNode).operator !== ts.SyntaxKind.UniqueKeyword && mayResolveTypeAlias((node as ts.TypeOperatorNode).type); + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.OptionalType: + case ts.SyntaxKind.NamedTupleMember: + case ts.SyntaxKind.JSDocOptionalType: + case ts.SyntaxKind.JSDocNullableType: + case ts.SyntaxKind.JSDocNonNullableType: + case ts.SyntaxKind.JSDocTypeExpression: + return mayResolveTypeAlias((node as ts.ParenthesizedTypeNode | ts.OptionalTypeNode | ts.JSDocTypeReferencingNode | ts.NamedTupleMember).type); + case ts.SyntaxKind.RestType: + return (node as ts.RestTypeNode).type.kind !== ts.SyntaxKind.ArrayType || mayResolveTypeAlias(((node as ts.RestTypeNode).type as ts.ArrayTypeNode).elementType); + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + return ts.some((node as ts.UnionOrIntersectionTypeNode).types, mayResolveTypeAlias); + case ts.SyntaxKind.IndexedAccessType: + return mayResolveTypeAlias((node as ts.IndexedAccessTypeNode).objectType) || mayResolveTypeAlias((node as ts.IndexedAccessTypeNode).indexType); + case ts.SyntaxKind.ConditionalType: + return mayResolveTypeAlias((node as ts.ConditionalTypeNode).checkType) || mayResolveTypeAlias((node as ts.ConditionalTypeNode).extendsType) || + mayResolveTypeAlias((node as ts.ConditionalTypeNode).trueType) || mayResolveTypeAlias((node as ts.ConditionalTypeNode).falseType); + } + return false; + } + function getTypeFromArrayOrTupleTypeNode(node: ts.ArrayTypeNode | ts.TupleTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const target = getArrayOrTupleTargetType(node); + if (target === emptyGenericType) { + links.resolvedType = emptyObjectType; } - if (isEmptyAnonymousObjectType(type)) { - if (!(includes & ts.TypeFlags.IncludesEmptyObject)) { - includes |= ts.TypeFlags.IncludesEmptyObject; - typeSet.set(type.id.toString(), type); - } + else if (!(node.kind === ts.SyntaxKind.TupleType && ts.some(node.elements, e => !!(getTupleElementFlags(e) & ts.ElementFlags.Variadic))) && isDeferredTypeReferenceNode(node)) { + links.resolvedType = node.kind === ts.SyntaxKind.TupleType && node.elements.length === 0 ? target : + createDeferredTypeReference(target, node, /*mapper*/ undefined); } else { - if (flags & ts.TypeFlags.AnyOrUnknown) { - if (type === wildcardType) - includes |= ts.TypeFlags.IncludesWildcard; - } - else if (strictNullChecks || !(flags & ts.TypeFlags.Nullable)) { - if (exactOptionalPropertyTypes && type === missingType) { - includes |= ts.TypeFlags.IncludesMissingType; - type = undefinedType; - } - if (!typeSet.has(type.id.toString())) { - if (type.flags & ts.TypeFlags.Unit && includes & ts.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 |= ts.TypeFlags.NonPrimitive; - } - typeSet.set(type.id.toString(), type); - } - } - includes |= flags & ts.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: ts.ESMap, includes: ts.TypeFlags, types: readonly ts.Type[]) { - for (const type of types) { - includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type)); + const elementTypes = node.kind === ts.SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : ts.map(node.elements, getTypeFromTypeNode); + links.resolvedType = createNormalizedTypeReference(target, elementTypes); } - return includes; - } - - function removeRedundantPrimitiveTypes(types: ts.Type[], includes: ts.TypeFlags) { - let i = types.length; - while (i > 0) { - i--; - const t = types[i]; - const remove = t.flags & ts.TypeFlags.String && includes & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || - t.flags & ts.TypeFlags.Number && includes & ts.TypeFlags.NumberLiteral || - t.flags & ts.TypeFlags.BigInt && includes & ts.TypeFlags.BigIntLiteral || - t.flags & ts.TypeFlags.ESSymbol && includes & ts.TypeFlags.UniqueESSymbol; - if (remove) { - ts.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: ts.UnionType[], type: ts.Type) { - for (const u of unionTypes) { - if (!containsType(u.types, type)) { - const primitive = type.flags & ts.TypeFlags.StringLiteral ? stringType : - type.flags & ts.TypeFlags.NumberLiteral ? numberType : - type.flags & ts.TypeFlags.BigIntLiteral ? bigintType : - type.flags & ts.TypeFlags.UniqueESSymbol ? esSymbolType : - undefined; - if (!primitive || !containsType(u.types, primitive)) { - return false; - } - } - } - return true; - } - - /** - * Returns `true` if the intersection of the template literals and string literals is the empty set, eg `get${string}` & "setX", and should reduce to `never` - */ - function extractRedundantTemplateLiterals(types: ts.Type[]): boolean { - let i = types.length; - const literals = ts.filter(types, t => !!(t.flags & ts.TypeFlags.StringLiteral)); - while (i > 0) { - i--; - const t = types[i]; - if (!(t.flags & ts.TypeFlags.TemplateLiteral)) - continue; - for (const t2 of literals) { - if (isTypeSubtypeOf(t2, t)) { - // eg, ``get${T}` & "getX"` is just `"getX"` - ts.orderedRemoveItemAt(types, i); - break; - } - else if (isPatternLiteralType(t)) { - return true; - } - } - } - return false; - } - - function eachIsUnionContaining(types: ts.Type[], flag: ts.TypeFlags) { - return ts.every(types, t => !!(t.flags & ts.TypeFlags.Union) && ts.some((t as ts.UnionType).types, tt => !!(tt.flags & flag))); - } - - function removeFromEach(types: ts.Type[], flag: ts.TypeFlags) { - for (let i = 0; i < types.length; i++) { - types[i] = filterType(types[i], t => !(t.flags & flag)); - } - } - - // 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: ts.UnionType[] | undefined; - const index = ts.findIndex(types, t => !!(ts.getObjectFlags(t) & ts.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 (ts.getObjectFlags(t) & ts.ObjectFlags.PrimitiveUnion) { - (unionTypes || (unionTypes = [types[index] as ts.UnionType])).push(t as ts.UnionType); - ts.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: 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); - } - } - } - } - // Finally replace the first union with the result - types[index] = getUnionTypeFromSortedList(result, ts.ObjectFlags.PrimitiveUnion); - return true; - } - - function createIntersectionType(types: ts.Type[], aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]) { - const result = createType(ts.TypeFlags.Intersection) as ts.IntersectionType; - result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ ts.TypeFlags.Nullable); - result.types = types; - result.aliasSymbol = aliasSymbol; - 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.ESMap = new ts.Map(); - const includes = addTypesToIntersection(typeMembershipMap, 0, types); - const typeSet: ts.Type[] = ts.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 & ts.TypeFlags.Never) { - return ts.contains(typeSet, silentNeverType) ? silentNeverType : neverType; - } - if (strictNullChecks && includes & ts.TypeFlags.Nullable && includes & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive | ts.TypeFlags.IncludesEmptyObject) || - includes & ts.TypeFlags.NonPrimitive && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.NonPrimitive) || - includes & ts.TypeFlags.StringLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.StringLike) || - includes & ts.TypeFlags.NumberLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.NumberLike) || - includes & ts.TypeFlags.BigIntLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.BigIntLike) || - includes & ts.TypeFlags.ESSymbolLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.ESSymbolLike) || - includes & ts.TypeFlags.VoidLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.VoidLike)) { - return neverType; - } - if (includes & ts.TypeFlags.TemplateLiteral && includes & ts.TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) { - return neverType; - } - if (includes & ts.TypeFlags.Any) { - return includes & ts.TypeFlags.IncludesWildcard ? wildcardType : anyType; - } - if (!strictNullChecks && includes & ts.TypeFlags.Nullable) { - return includes & ts.TypeFlags.Undefined ? undefinedType : nullType; - } - if (includes & ts.TypeFlags.String && includes & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || - includes & ts.TypeFlags.Number && includes & ts.TypeFlags.NumberLiteral || - includes & ts.TypeFlags.BigInt && includes & ts.TypeFlags.BigIntLiteral || - includes & ts.TypeFlags.ESSymbol && includes & ts.TypeFlags.UniqueESSymbol) { - removeRedundantPrimitiveTypes(typeSet, includes); - } - if (includes & ts.TypeFlags.IncludesEmptyObject && includes & ts.TypeFlags.Object) { - ts.orderedRemoveItemAt(typeSet, ts.findIndex(typeSet, isEmptyAnonymousObjectType)); - } - if (includes & ts.TypeFlags.IncludesMissingType) { - typeSet[typeSet.indexOf(undefinedType)] = missingType; - } - if (typeSet.length === 0) { - return unknownType; - } - if (typeSet.length === 1) { - return typeSet[0]; - } - const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments); - let result = intersectionTypes.get(id); - if (!result) { - if (includes & ts.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 (eachIsUnionContaining(typeSet, ts.TypeFlags.Undefined)) { - const undefinedOrMissingType = exactOptionalPropertyTypes && ts.some(typeSet, t => containsType((t as ts.UnionType).types, missingType)) ? missingType : undefinedType; - removeFromEach(typeSet, ts.TypeFlags.Undefined); - result = getUnionType([getIntersectionType(typeSet), undefinedOrMissingType], ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); - } - else if (eachIsUnionContaining(typeSet, ts.TypeFlags.Null)) { - removeFromEach(typeSet, ts.TypeFlags.Null); - result = getUnionType([getIntersectionType(typeSet), nullType], ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); - } - else { - // We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of - // the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type - // exceeds 100000 constituents, report an error. - if (!checkCrossProductUnion(typeSet)) { - return errorType; - } - const constituents = getCrossProductIntersections(typeSet); - // We attach a denormalized origin type when at least one constituent of the cross-product union is an - // intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions). - const origin = ts.some(constituents, t => !!(t.flags & ts.TypeFlags.Intersection)) ? createOriginUnionOrIntersectionType(ts.TypeFlags.Intersection, typeSet) : undefined; - result = getUnionType(constituents, ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin); - } - } - else { - result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); - } - intersectionTypes.set(id, result); - } - return result; - } - - function getCrossProductUnionSize(types: readonly ts.Type[]) { - return ts.reduceLeft(types, (n, t) => t.flags & ts.TypeFlags.Union ? n * (t as ts.UnionType).types.length : t.flags & ts.TypeFlags.Never ? 0 : n, 1); } + return links.resolvedType; + } - function checkCrossProductUnion(types: readonly ts.Type[]) { - const size = getCrossProductUnionSize(types); - if (size >= 100000) { - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "checkCrossProductUnion_DepthLimit", { typeIds: types.map(t => t.id), size }); - error(currentNode, ts.Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); - return false; - } - return true; - } + function isReadonlyTypeOperator(node: ts.Node) { + return ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword; + } - function getCrossProductIntersections(types: readonly ts.Type[]) { - const count = getCrossProductUnionSize(types); - const intersections: ts.Type[] = []; - for (let i = 0; i < count; i++) { - const constituents = types.slice(); - let n = i; - for (let j = types.length - 1; j >= 0; j--) { - if (types[j].flags & ts.TypeFlags.Union) { - const sourceTypes = (types[j] as ts.UnionType).types; - const length = sourceTypes.length; - constituents[j] = sourceTypes[n % length]; - n = Math.floor(n / length); - } - } - const t = getIntersectionType(constituents); - if (!(t.flags & ts.TypeFlags.Never)) - intersections.push(t); - } - return intersections; - } + function createTupleType(elementTypes: readonly ts.Type[], elementFlags?: readonly ts.ElementFlags[], readonly = false, namedMemberDeclarations?: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[]) { + const tupleTarget = getTupleTargetType(elementFlags || ts.map(elementTypes, _ => ts.ElementFlags.Required), readonly, namedMemberDeclarations); + return tupleTarget === emptyGenericType ? emptyObjectType : + elementTypes.length ? createNormalizedTypeReference(tupleTarget, elementTypes) : + tupleTarget; + } - function getTypeFromIntersectionTypeNode(node: ts.IntersectionTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const aliasSymbol = getAliasSymbolForTypeNode(node); - links.resolvedType = getIntersectionType(ts.map(node.types, getTypeFromTypeNode), aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); - } - return links.resolvedType; + function getTupleTargetType(elementFlags: readonly ts.ElementFlags[], readonly: boolean, namedMemberDeclarations?: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[]): ts.GenericType { + if (elementFlags.length === 1 && elementFlags[0] & ts.ElementFlags.Rest) { + // [...X[]] is equivalent to just X[] + return readonly ? globalReadonlyArrayType : globalArrayType; } - - function createIndexType(type: ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly: boolean) { - const result = createType(ts.TypeFlags.Index) as ts.IndexType; - result.type = type; - result.stringsOnly = stringsOnly; - return result; + const key = ts.map(elementFlags, f => f & ts.ElementFlags.Required ? "#" : f & ts.ElementFlags.Optional ? "?" : f & ts.ElementFlags.Rest ? "." : "*").join() + + (readonly ? "R" : "") + + (namedMemberDeclarations && namedMemberDeclarations.length ? "," + ts.map(namedMemberDeclarations, getNodeId).join(",") : ""); + let type = tupleTypes.get(key); + if (!type) { + tupleTypes.set(key, type = createTupleTargetType(elementFlags, readonly, namedMemberDeclarations)); } + return type; + } - function createOriginIndexType(type: ts.InstantiableType | ts.UnionOrIntersectionType) { - const result = createOriginType(ts.TypeFlags.Index) as ts.IndexType; - result.type = type; - return result; - } + // 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 createTupleTargetType(elementFlags: readonly ts.ElementFlags[], readonly: boolean, namedMemberDeclarations: readonly (ts.NamedTupleMember | ts.ParameterDeclaration)[] | undefined): ts.TupleType { + const arity = elementFlags.length; + const minLength = ts.countWhere(elementFlags, f => !!(f & (ts.ElementFlags.Required | ts.ElementFlags.Variadic))); + let typeParameters: ts.TypeParameter[] | undefined; + const properties: ts.Symbol[] = []; + let combinedFlags: ts.ElementFlags = 0; + if (arity) { + typeParameters = new Array(arity); + for (let i = 0; i < arity; i++) { + const typeParameter = typeParameters[i] = createTypeParameter(); + const flags = elementFlags[i]; + combinedFlags |= flags; + if (!(combinedFlags & ts.ElementFlags.Variable)) { + const property = createSymbol(ts.SymbolFlags.Property | (flags & ts.ElementFlags.Optional ? ts.SymbolFlags.Optional : 0), "" + i as ts.__String, readonly ? ts.CheckFlags.Readonly : 0); + property.tupleLabelDeclaration = namedMemberDeclarations?.[i]; + property.type = typeParameter; + properties.push(property); + } + } + } + const fixedLength = properties.length; + const lengthSymbol = createSymbol(ts.SymbolFlags.Property, "length" as ts.__String, readonly ? ts.CheckFlags.Readonly : 0); + if (combinedFlags & ts.ElementFlags.Variable) { + lengthSymbol.type = numberType; + } + else { + const literalTypes = []; + for (let i = minLength; i <= arity; i++) + literalTypes.push(getNumberLiteralType(i)); + lengthSymbol.type = getUnionType(literalTypes); + } + properties.push(lengthSymbol); + const type = createObjectType(ts.ObjectFlags.Tuple | ts.ObjectFlags.Reference) as ts.TupleType & ts.InterfaceTypeWithDeclaredMembers; + type.typeParameters = typeParameters; + type.outerTypeParameters = undefined; + type.localTypeParameters = typeParameters; + type.instantiations = new ts.Map(); + type.instantiations.set(getTypeListId(type.typeParameters), type as ts.GenericType); + type.target = type as ts.GenericType; + type.resolvedTypeArguments = type.typeParameters; + type.thisType = createTypeParameter(); + type.thisType.isThisType = true; + type.thisType.constraint = type; + type.declaredProperties = properties; + type.declaredCallSignatures = ts.emptyArray; + type.declaredConstructSignatures = ts.emptyArray; + type.declaredIndexInfos = ts.emptyArray; + type.elementFlags = elementFlags; + type.minLength = minLength; + type.fixedLength = fixedLength; + type.hasRestElement = !!(combinedFlags & ts.ElementFlags.Variable); + type.combinedFlags = combinedFlags; + type.readonly = readonly; + type.labeledElementDeclarations = namedMemberDeclarations; + return type; + } - function getIndexTypeForGenericType(type: ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly: boolean) { - return stringsOnly ? - type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) : - type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false)); - } + function createNormalizedTypeReference(target: ts.GenericType, typeArguments: readonly ts.Type[] | undefined) { + return target.objectFlags & ts.ObjectFlags.Tuple ? createNormalizedTupleType(target as ts.TupleType, typeArguments!) : createTypeReference(target, typeArguments); + } - /** - * This roughly mirrors `resolveMappedTypeMembers` in the nongeneric case, except only reports a union of the keys calculated, - * rather than manufacturing the properties. We can't just fetch the `constraintType` since that would ignore mappings - * and mapping the `constraintType` directly ignores how mapped types map _properties_ and not keys (thus ignoring subtype - * reduction in the constraintType) when possible. - * @param noIndexSignatures Indicates if _string_ index signatures should be elided. (other index signatures are always reported) - */ - function getIndexTypeForMappedType(type: ts.MappedType, stringsOnly: boolean, noIndexSignatures: boolean | undefined) { - const typeParameter = getTypeParameterFromMappedType(type); - const constraintType = getConstraintTypeFromMappedType(type); - const nameType = getNameTypeFromMappedType(type.target as ts.MappedType || type); - if (!nameType && !noIndexSignatures) { - // no mapping and no filtering required, just quickly bail to returning the constraint in the common case - return constraintType; + function createNormalizedTupleType(target: ts.TupleType, elementTypes: readonly ts.Type[]): ts.Type { + if (!(target.combinedFlags & ts.ElementFlags.NonRequired)) { + // No need to normalize when we only have regular required elements + return createTypeReference(target, elementTypes); + } + if (target.combinedFlags & ts.ElementFlags.Variadic) { + // Transform [A, ...(X | Y | Z)] into [A, ...X] | [A, ...Y] | [A, ...Z] + const unionIndex = ts.findIndex(elementTypes, (t, i) => !!(target.elementFlags[i] & ts.ElementFlags.Variadic && t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Union))); + if (unionIndex >= 0) { + return checkCrossProductUnion(ts.map(elementTypes, (t, i) => target.elementFlags[i] & ts.ElementFlags.Variadic ? t : unknownType)) ? + mapType(elementTypes[unionIndex], t => createNormalizedTupleType(target, ts.replaceElement(elementTypes, unionIndex, t))) : + errorType; } - const keyTypes: ts.Type[] = []; - if (isMappedTypeWithKeyofConstraintDeclaration(type)) { - // We have a { [P in keyof T]: X } - - // `getApparentType` on the T in a generic mapped type can trigger a circularity - // (conditionals and `infer` types create a circular dependency in the constraint resolution) - // so we only eagerly manifest the keys if the constraint is nongeneric - if (!isGenericIndexType(constraintType)) { - const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' - forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, ts.TypeFlags.StringOrNumberLiteralOrUnique, stringsOnly, addMemberForKeyType); + } + // We have optional, rest, or variadic elements that may need normalizing. Normalization ensures that all variadic + // elements are generic and that the tuple type has one of the following layouts, disregarding variadic elements: + // (1) Zero or more required elements, followed by zero or more optional elements, followed by zero or one rest element. + // (2) Zero or more required elements, followed by a rest element, followed by zero or more required elements. + // In either layout, zero or more generic variadic elements may be present at any location. + const expandedTypes: ts.Type[] = []; + const expandedFlags: ts.ElementFlags[] = []; + let expandedDeclarations: (ts.NamedTupleMember | ts.ParameterDeclaration)[] | undefined = []; + let lastRequiredIndex = -1; + let firstRestIndex = -1; + let lastOptionalOrRestIndex = -1; + for (let i = 0; i < elementTypes.length; i++) { + const type = elementTypes[i]; + const flags = target.elementFlags[i]; + if (flags & ts.ElementFlags.Variadic) { + if (type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type)) { + // Generic variadic elements stay as they are. + addElement(type, ts.ElementFlags.Variadic, target.labeledElementDeclarations?.[i]); + } + else if (isTupleType(type)) { + const elements = getTypeArguments(type); + if (elements.length + expandedTypes.length >= 10000) { + error(currentNode, ts.isPartOfTypeNode(currentNode!) + ? ts.Diagnostics.Type_produces_a_tuple_type_that_is_too_large_to_represent + : ts.Diagnostics.Expression_produces_a_tuple_type_that_is_too_large_to_represent); + return errorType; + } + // Spread variadic elements with tuple types into the resulting tuple. + ts.forEach(elements, (t, n) => addElement(t, type.target.elementFlags[n], type.target.labeledElementDeclarations?.[n])); } else { - // we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later - // since it's not safe to resolve the shape of modifier type - return getIndexTypeForGenericType(type, stringsOnly); + // Treat everything else as an array type and create a rest element. + addElement(isArrayLikeType(type) && getIndexTypeOfType(type, numberType) || errorType, ts.ElementFlags.Rest, target.labeledElementDeclarations?.[i]); } } else { - forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); + // Copy other element kinds with no change. + addElement(type, flags, target.labeledElementDeclarations?.[i]); + } + } + // Turn optional elements preceding the last required element into required elements + for (let i = 0; i < lastRequiredIndex; i++) { + if (expandedFlags[i] & ts.ElementFlags.Optional) + expandedFlags[i] = ts.ElementFlags.Required; + } + if (firstRestIndex >= 0 && firstRestIndex < lastOptionalOrRestIndex) { + // Turn elements between first rest and last optional/rest into a single rest element + expandedTypes[firstRestIndex] = getUnionType(ts.sameMap(expandedTypes.slice(firstRestIndex, lastOptionalOrRestIndex + 1), (t, i) => expandedFlags[firstRestIndex + i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t)); + expandedTypes.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); + expandedFlags.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); + expandedDeclarations?.splice(firstRestIndex + 1, lastOptionalOrRestIndex - firstRestIndex); + } + const tupleTarget = getTupleTargetType(expandedFlags, target.readonly, expandedDeclarations); + return tupleTarget === emptyGenericType ? emptyObjectType : + expandedFlags.length ? createTypeReference(tupleTarget, expandedTypes) : + tupleTarget; + + function addElement(type: ts.Type, flags: ts.ElementFlags, declaration: ts.NamedTupleMember | ts.ParameterDeclaration | undefined) { + if (flags & ts.ElementFlags.Required) { + lastRequiredIndex = expandedFlags.length; } - if (isGenericIndexType(constraintType)) { // include the generic component in the resulting type - forEachType(constraintType, addMemberForKeyType); + if (flags & ts.ElementFlags.Rest && firstRestIndex < 0) { + firstRestIndex = expandedFlags.length; } - // we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType, - // so we can return the union that preserves aliases/origin data if possible - const result = noIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.String))) : getUnionType(keyTypes); - if (result.flags & ts.TypeFlags.Union && constraintType.flags & ts.TypeFlags.Union && getTypeListId((result as ts.UnionType).types) === getTypeListId((constraintType as ts.UnionType).types)) { - return constraintType; + if (flags & (ts.ElementFlags.Optional | ts.ElementFlags.Rest)) { + lastOptionalOrRestIndex = expandedFlags.length; } - return result; - - function addMemberForKeyType(keyType: ts.Type) { - const propNameType = nameType ? instantiateType(nameType, appendTypeMapping(type.mapper, typeParameter, keyType)) : keyType; - // `keyof` currently always returns `string | number` for concrete `string` index signatures - the below ternary keeps that behavior for mapped types - // See `getLiteralTypeFromProperties` where there's a similar ternary to cause the same behavior. - keyTypes.push(propNameType === stringType ? stringOrNumberType : propNameType); + expandedTypes.push(type); + expandedFlags.push(flags); + if (expandedDeclarations && declaration) { + expandedDeclarations.push(declaration); } - } - - // Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N

]: X }, to simply N. This however presumes - // that N distributes over union types, i.e. that N is equivalent to N | N | N. Specifically, we only - // want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable - // introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because - // they're the same type regardless of what's being distributed over. - function hasDistributiveNameType(mappedType: ts.MappedType) { - const typeVariable = getTypeParameterFromMappedType(mappedType); - return isDistributive(getNameTypeFromMappedType(mappedType) || typeVariable); - function isDistributive(type: ts.Type): boolean { - return type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Primitive | ts.TypeFlags.Never | ts.TypeFlags.TypeParameter | ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive) ? true : - type.flags & ts.TypeFlags.Conditional ? (type as ts.ConditionalType).root.isDistributive && (type as ts.ConditionalType).checkType === typeVariable : - type.flags & (ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.TemplateLiteral) ? ts.every((type as ts.UnionOrIntersectionType | ts.TemplateLiteralType).types, isDistributive) : - type.flags & ts.TypeFlags.IndexedAccess ? isDistributive((type as ts.IndexedAccessType).objectType) && isDistributive((type as ts.IndexedAccessType).indexType) : - type.flags & ts.TypeFlags.Substitution ? isDistributive((type as ts.SubstitutionType).substitute) : - type.flags & ts.TypeFlags.StringMapping ? isDistributive((type as ts.StringMappingType).type) : - false; + else { + expandedDeclarations = undefined; } } + } - function getLiteralTypeFromPropertyName(name: ts.PropertyName) { - if (ts.isPrivateIdentifier(name)) { - return neverType; - } - return ts.isIdentifier(name) ? getStringLiteralType(ts.unescapeLeadingUnderscores(name.escapedText)) : - getRegularTypeOfLiteralType(ts.isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name)); + function sliceTupleType(type: ts.TupleTypeReference, index: number, endSkipCount = 0) { + const target = type.target; + const endIndex = getTypeReferenceArity(type) - endSkipCount; + return index > target.fixedLength ? getRestArrayTypeOfTupleType(type) || createTupleType(ts.emptyArray) : + createTupleType(getTypeArguments(type).slice(index, endIndex), target.elementFlags.slice(index, endIndex), + /*readonly*/ false, target.labeledElementDeclarations && target.labeledElementDeclarations.slice(index, endIndex)); + } + + function getKnownKeysOfTupleType(type: ts.TupleTypeReference) { + return getUnionType(ts.append(ts.arrayOf(type.target.fixedLength, i => getStringLiteralType("" + i)), getIndexType(type.target.readonly ? globalReadonlyArrayType : globalArrayType))); + } + + // Return count of starting consecutive tuple elements of the given kind(s) + function getStartElementCount(type: ts.TupleType, flags: ts.ElementFlags) { + const index = ts.findIndex(type.elementFlags, f => !(f & flags)); + return index >= 0 ? index : type.elementFlags.length; + } + + // Return count of ending consecutive tuple elements of the given kind(s) + function getEndElementCount(type: ts.TupleType, flags: ts.ElementFlags) { + return type.elementFlags.length - ts.findLastIndex(type.elementFlags, f => !(f & flags)) - 1; + } + + function getTypeFromOptionalTypeNode(node: ts.OptionalTypeNode): ts.Type { + return addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true); + } + + function getTypeId(type: ts.Type): ts.TypeId { + return type.id; + } + + function containsType(types: readonly ts.Type[], type: ts.Type): boolean { + return ts.binarySearch(types, type, getTypeId, ts.compareValues) >= 0; + } + + function insertType(types: ts.Type[], type: ts.Type): boolean { + const index = ts.binarySearch(types, type, getTypeId, ts.compareValues); + if (index < 0) { + types.splice(~index, 0, type); + return true; } + return false; + } - function getLiteralTypeFromProperty(prop: ts.Symbol, include: ts.TypeFlags, includeNonPublic?: boolean) { - if (includeNonPublic || !(ts.getDeclarationModifierFlagsFromSymbol(prop) & ts.ModifierFlags.NonPublicAccessibilityModifier)) { - let type = getSymbolLinks(getLateBoundSymbol(prop)).nameType; - if (!type) { - const name = ts.getNameOfDeclaration(prop.valueDeclaration) as ts.PropertyName; - type = prop.escapedName === ts.InternalSymbolName.Default ? getStringLiteralType("default") : - name && getLiteralTypeFromPropertyName(name) || (!ts.isKnownSymbol(prop) ? getStringLiteralType(ts.symbolName(prop)) : undefined); - } - if (type && type.flags & include) { - return type; + function addTypeToUnion(typeSet: ts.Type[], includes: ts.TypeFlags, type: ts.Type) { + const flags = type.flags; + if (flags & ts.TypeFlags.Union) { + return addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? ts.TypeFlags.Union : 0), (type as ts.UnionType).types); + } + // We ignore 'never' types in unions + if (!(flags & ts.TypeFlags.Never)) { + includes |= flags & ts.TypeFlags.IncludesMask; + if (flags & ts.TypeFlags.Instantiable) + includes |= ts.TypeFlags.IncludesInstantiable; + if (type === wildcardType) + includes |= ts.TypeFlags.IncludesWildcard; + if (!strictNullChecks && flags & ts.TypeFlags.Nullable) { + if (!(ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType)) + includes |= ts.TypeFlags.IncludesNonWideningType; + } + else { + const len = typeSet.length; + const index = len && type.id > typeSet[len - 1].id ? ~len : ts.binarySearch(typeSet, type, getTypeId, ts.compareValues); + if (index < 0) { + typeSet.splice(~index, 0, type); } } - return neverType; } + return includes; + } - function isKeyTypeIncluded(keyType: ts.Type, include: ts.TypeFlags): boolean { - return !!(keyType.flags & include || keyType.flags & ts.TypeFlags.Intersection && ts.some((keyType as ts.IntersectionType).types, t => isKeyTypeIncluded(t, include))); + // 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: ts.TypeFlags, types: readonly ts.Type[]): ts.TypeFlags { + for (const type of types) { + includes = addTypeToUnion(typeSet, includes, type); } + return includes; + } - function getLiteralTypeFromProperties(type: ts.Type, include: ts.TypeFlags, includeOrigin: boolean) { - const origin = includeOrigin && (ts.getObjectFlags(type) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference) || type.aliasSymbol) ? createOriginIndexType(type) : undefined; - const propertyTypes = ts.map(getPropertiesOfType(type), prop => getLiteralTypeFromProperty(prop, include)); - const indexKeyTypes = ts.map(getIndexInfosOfType(type), info => info !== enumNumberIndexInfo && isKeyTypeIncluded(info.keyType, include) ? - info.keyType === stringType && include & ts.TypeFlags.Number ? stringOrNumberType : info.keyType : neverType); - return getUnionType(ts.concatenate(propertyTypes, indexKeyTypes), ts.UnionReduction.Literal, - /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin); + function removeSubtypes(types: ts.Type[], hasObjectTypes: boolean): ts.Type[] | undefined { + // [] and [T] immediately reduce to [] and [T] respectively + if (types.length < 2) { + return types; } - /** - * A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations) - * must be kept generic, as that instantiation information needs to flow through the type system. By replacing all - * type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic - * to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible). - */ - function isPossiblyReducibleByInstantiation(type: ts.UnionType): boolean { - return ts.some(type.types, t => { - const uniqueFilled = getUniqueLiteralFilledInstantiation(t); - return getReducedType(uniqueFilled) !== uniqueFilled; - }); + const id = getTypeListId(types); + const match = subtypeReductionCache.get(id); + if (match) { + return match; + } + + // We assume that redundant primitive types have already been removed from the types array and that there + // are no any and unknown types in the array. Thus, the only possible supertypes for primitive types are empty + // object types, and if none of those are present we can exclude primitive types from the subtype check. + const hasEmptyObject = hasObjectTypes && ts.some(types, t => !!(t.flags & ts.TypeFlags.Object) && !isGenericMappedType(t) && isEmptyResolvedType(resolveStructuredTypeMembers(t as ts.ObjectType))); + const len = types.length; + let i = len; + let count = 0; + while (i > 0) { + i--; + const source = types[i]; + if (hasEmptyObject || source.flags & ts.TypeFlags.StructuredOrInstantiable) { + // Find the first property with a unit type, if any. When constituents have a property by the same name + // but of a different unit type, we can quickly disqualify them from subtype checks. This helps subtype + // reduction of large discriminated union types. + const keyProperty = source.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive) ? + ts.find(getPropertiesOfType(source), p => isUnitType(getTypeOfSymbol(p))) : + undefined; + const keyPropertyType = keyProperty && getRegularTypeOfLiteralType(getTypeOfSymbol(keyProperty)); + 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 1M we deem the union type too complex to represent. This for example + // caps union types at 1000 unique object types. + const estimatedCount = (count / (len - i)) * len; + if (estimatedCount > 1000000) { + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) }); + error(currentNode, ts.Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); + return undefined; + } + } + count++; + if (keyProperty && target.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive)) { + const t = getTypeOfPropertyOfType(target, keyProperty.escapedName); + if (t && isUnitType(t) && getRegularTypeOfLiteralType(t) !== keyPropertyType) { + continue; + } + } + if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (!(ts.getObjectFlags(getTargetType(source)) & ts.ObjectFlags.Class) || + !(ts.getObjectFlags(getTargetType(target)) & ts.ObjectFlags.Class) || + isTypeDerivedFrom(source, target))) { + ts.orderedRemoveItemAt(types, i); + break; + } + } + } + } } + subtypeReductionCache.set(id, types); + return types; + } - function getIndexType(type: ts.Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): ts.Type { - type = getReducedType(type); - return type.flags & ts.TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as ts.UnionType) - ? getIndexTypeForGenericType(type as ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly) - : getIntersectionType(ts.map((type as ts.UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : - type.flags & ts.TypeFlags.Intersection ? getUnionType(ts.map((type as ts.IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : - type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly) : - ts.getObjectFlags(type) & ts.ObjectFlags.Mapped ? getIndexTypeForMappedType(type as ts.MappedType, stringsOnly, noIndexSignatures) : - type === wildcardType ? wildcardType : - type.flags & ts.TypeFlags.Unknown ? neverType : - type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never) ? keyofConstraintType : - getLiteralTypeFromProperties(type, (noIndexSignatures ? ts.TypeFlags.StringLiteral : ts.TypeFlags.StringLike) | (stringsOnly ? 0 : ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike), stringsOnly === keyofStringsOnly && !noIndexSignatures); + function removeRedundantLiteralTypes(types: ts.Type[], includes: ts.TypeFlags, reduceVoidUndefined: boolean) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + const flags = t.flags; + const remove = flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && includes & ts.TypeFlags.String || + flags & ts.TypeFlags.NumberLiteral && includes & ts.TypeFlags.Number || + flags & ts.TypeFlags.BigIntLiteral && includes & ts.TypeFlags.BigInt || + flags & ts.TypeFlags.UniqueESSymbol && includes & ts.TypeFlags.ESSymbol || + reduceVoidUndefined && flags & ts.TypeFlags.Undefined && includes & ts.TypeFlags.Void || + isFreshLiteralType(t) && containsType(types, (t as ts.LiteralType).regularType); + if (remove) { + ts.orderedRemoveItemAt(types, i); + } } + } - function getExtractStringType(type: ts.Type) { - if (keyofStringsOnly) { - return type; + function removeStringLiteralsMatchedByTemplateLiterals(types: ts.Type[]) { + const templates = ts.filter(types, isPatternLiteralType) as ts.TemplateLiteralType[]; + if (templates.length) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + if (t.flags & ts.TypeFlags.StringLiteral && ts.some(templates, template => isTypeMatchedByTemplateLiteralType(t, template))) { + ts.orderedRemoveItemAt(types, i); + } } - 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 & ts.TypeFlags.Never ? stringType : indexType; - } + function isNamedUnionType(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Union && (type.aliasSymbol || (type as ts.UnionType).origin)); + } - function getTypeFromTypeOperatorNode(node: ts.TypeOperatorNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - switch (node.operator) { - case ts.SyntaxKind.KeyOfKeyword: - links.resolvedType = getIndexType(getTypeFromTypeNode(node.type)); - break; - case ts.SyntaxKind.UniqueKeyword: - links.resolvedType = node.type.kind === ts.SyntaxKind.SymbolKeyword - ? getESSymbolLikeTypeForNode(ts.walkUpParenthesizedTypes(node.parent)) - : errorType; - break; - case ts.SyntaxKind.ReadonlyKeyword: - links.resolvedType = getTypeFromTypeNode(node.type); - break; - default: - throw ts.Debug.assertNever(node.operator); + function addNamedUnions(namedUnions: ts.Type[], types: readonly ts.Type[]) { + for (const t of types) { + if (t.flags & ts.TypeFlags.Union) { + const origin = (t as ts.UnionType).origin; + if (t.aliasSymbol || origin && !(origin.flags & ts.TypeFlags.Union)) { + ts.pushIfUnique(namedUnions, t); + } + else if (origin && origin.flags & ts.TypeFlags.Union) { + addNamedUnions(namedUnions, (origin as ts.UnionType).types); } } - return links.resolvedType; } + } - function getTypeFromTemplateTypeNode(node: ts.TemplateLiteralTypeNode) { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getTemplateLiteralType([node.head.text, ...ts.map(node.templateSpans, span => span.literal.text)], ts.map(node.templateSpans, span => getTypeFromTypeNode(span.type))); - } - return links.resolvedType; - } + function createOriginUnionOrIntersectionType(flags: ts.TypeFlags, types: ts.Type[]) { + const result = createOriginType(flags) as ts.UnionOrIntersectionType; + result.types = types; + return result; + } - function getTemplateLiteralType(texts: readonly string[], types: readonly ts.Type[]): ts.Type { - const unionIndex = ts.findIndex(types, t => !!(t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Union))); - if (unionIndex >= 0) { - return checkCrossProductUnion(types) ? - mapType(types[unionIndex], t => getTemplateLiteralType(texts, ts.replaceElement(types, unionIndex, t))) : - errorType; + // 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: ts.UnionReduction = ts.UnionReduction.Literal, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[], origin?: ts.Type): ts.Type { + if (types.length === 0) { + return neverType; + } + if (types.length === 1) { + return types[0]; + } + let typeSet: ts.Type[] | undefined = []; + const includes = addTypesToUnion(typeSet, 0, types); + if (unionReduction !== ts.UnionReduction.None) { + if (includes & ts.TypeFlags.AnyOrUnknown) { + return includes & ts.TypeFlags.Any ? + includes & ts.TypeFlags.IncludesWildcard ? wildcardType : anyType : + includes & ts.TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType; } - if (ts.contains(types, wildcardType)) { - return wildcardType; + if (exactOptionalPropertyTypes && includes & ts.TypeFlags.Undefined) { + const missingIndex = ts.binarySearch(typeSet, missingType, getTypeId, ts.compareValues); + if (missingIndex >= 0 && containsType(typeSet, undefinedType)) { + ts.orderedRemoveItemAt(typeSet, missingIndex); + } } - const newTypes: ts.Type[] = []; - const newTexts: string[] = []; - let text = texts[0]; - if (!addSpans(texts, types)) { - return stringType; + if (includes & (ts.TypeFlags.Literal | ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || includes & ts.TypeFlags.Void && includes & ts.TypeFlags.Undefined) { + removeRedundantLiteralTypes(typeSet, includes, !!(unionReduction & ts.UnionReduction.Subtype)); } - if (newTypes.length === 0) { - return getStringLiteralType(text); + if (includes & ts.TypeFlags.StringLiteral && includes & ts.TypeFlags.TemplateLiteral) { + removeStringLiteralsMatchedByTemplateLiterals(typeSet); } - newTexts.push(text); - if (ts.every(newTexts, t => t === "") && ts.every(newTypes, t => !!(t.flags & ts.TypeFlags.String))) { - return stringType; + if (unionReduction === ts.UnionReduction.Subtype) { + typeSet = removeSubtypes(typeSet, !!(includes & ts.TypeFlags.Object)); + if (!typeSet) { + return errorType; + } } - const id = `${getTypeListId(newTypes)}|${ts.map(newTexts, t => t.length).join(",")}|${newTexts.join("")}`; - let type = templateLiteralTypes.get(id); - if (!type) { - templateLiteralTypes.set(id, type = createTemplateLiteralType(newTexts, newTypes)); + if (typeSet.length === 0) { + return includes & ts.TypeFlags.Null ? includes & ts.TypeFlags.IncludesNonWideningType ? nullType : nullWideningType : + includes & ts.TypeFlags.Undefined ? includes & ts.TypeFlags.IncludesNonWideningType ? undefinedType : undefinedWideningType : + neverType; } - return type; - - function addSpans(texts: readonly string[] | string, types: readonly ts.Type[]): boolean { - const isTextsArray = ts.isArray(texts); - for (let i = 0; i < types.length; i++) { - const t = types[i]; - const addText = isTextsArray ? texts[i + 1] : texts; - if (t.flags & (ts.TypeFlags.Literal | ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { - text += getTemplateStringForType(t) || ""; - text += addText; - if (!isTextsArray) - return true; - } - else if (t.flags & ts.TypeFlags.TemplateLiteral) { - text += (t as ts.TemplateLiteralType).texts[0]; - if (!addSpans((t as ts.TemplateLiteralType).texts, (t as ts.TemplateLiteralType).types)) - return false; - text += addText; - if (!isTextsArray) - return true; - } - else if (isGenericIndexType(t) || isPatternLiteralPlaceholderType(t)) { - newTypes.push(t); - newTexts.push(text); - text = addText; - } - else if (t.flags & ts.TypeFlags.Intersection) { - const added = addSpans(texts[i + 1], (t as ts.IntersectionType).types); - if (!added) - return false; - } - else if (isTextsArray) { - return false; - } + } + if (!origin && includes & ts.TypeFlags.Union) { + const namedUnions: ts.Type[] = []; + addNamedUnions(namedUnions, types); + const reducedTypes: ts.Type[] = []; + for (const t of typeSet) { + if (!ts.some(namedUnions, union => containsType((union as ts.UnionType).types, t))) { + reducedTypes.push(t); } - return true; + } + if (!aliasSymbol && namedUnions.length === 1 && reducedTypes.length === 0) { + return namedUnions[0]; + } + // We create a denormalized origin type only when the union was created from one or more named unions + // (unions with alias symbols or origins) and when there is no overlap between those named unions. + const namedTypesCount = ts.reduceLeft(namedUnions, (sum, union) => sum + (union as ts.UnionType).types.length, 0); + if (namedTypesCount + reducedTypes.length === typeSet.length) { + for (const t of namedUnions) { + insertType(reducedTypes, t); + } + origin = createOriginUnionOrIntersectionType(ts.TypeFlags.Union, reducedTypes); } } + const objectFlags = (includes & ts.TypeFlags.NotPrimitiveUnion ? 0 : ts.ObjectFlags.PrimitiveUnion) | + (includes & ts.TypeFlags.Intersection ? ts.ObjectFlags.ContainsIntersections : 0); + return getUnionTypeFromSortedList(typeSet, objectFlags, aliasSymbol, aliasTypeArguments, origin); + } - function getTemplateStringForType(type: ts.Type) { - return type.flags & ts.TypeFlags.StringLiteral ? (type as ts.StringLiteralType).value : - type.flags & ts.TypeFlags.NumberLiteral ? "" + (type as ts.NumberLiteralType).value : - type.flags & ts.TypeFlags.BigIntLiteral ? ts.pseudoBigIntToString((type as ts.BigIntLiteralType).value) : - type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Nullable) ? (type as ts.IntrinsicType).intrinsicName : - undefined; - } + function getUnionOrIntersectionTypePredicate(signatures: readonly ts.Signature[], kind: ts.TypeFlags | undefined): ts.TypePredicate | undefined { + let first: ts.TypePredicate | undefined; + const types: ts.Type[] = []; + for (const sig of signatures) { + const pred = getTypePredicateOfSignature(sig); + if (!pred || pred.kind === ts.TypePredicateKind.AssertsThis || pred.kind === ts.TypePredicateKind.AssertsIdentifier) { + if (kind !== ts.TypeFlags.Intersection) { + continue; + } + else { + return; // intersections demand all members be type predicates for the result to have a predicate + } + } - function createTemplateLiteralType(texts: readonly string[], types: readonly ts.Type[]) { - const type = createType(ts.TypeFlags.TemplateLiteral) as ts.TemplateLiteralType; - type.texts = texts; - type.types = types; - return type; + if (first) { + if (!typePredicateKindsMatch(first, pred)) { + // No common type predicate. + return undefined; + } + } + else { + first = pred; + } + types.push(pred.type); } - - function getStringMappingType(symbol: ts.Symbol, type: ts.Type): ts.Type { - return type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never) ? mapType(type, t => getStringMappingType(symbol, t)) : - isGenericIndexType(type) ? getStringMappingTypeForGenericType(symbol, type) : - type.flags & ts.TypeFlags.StringLiteral ? getStringLiteralType(applyStringMapping(symbol, (type as ts.StringLiteralType).value)) : - type; + if (!first) { + // No signatures had a type predicate. + return undefined; } + const compositeType = getUnionOrIntersectionType(types, kind); + return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType); + } - function applyStringMapping(symbol: ts.Symbol, str: string) { - switch (intrinsicTypeKinds.get(symbol.escapedName as string)) { - case IntrinsicTypeKind.Uppercase: return str.toUpperCase(); - case IntrinsicTypeKind.Lowercase: return str.toLowerCase(); - case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1); - case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1); - } - return str; - } + function typePredicateKindsMatch(a: ts.TypePredicate, b: ts.TypePredicate): boolean { + return a.kind === b.kind && a.parameterIndex === b.parameterIndex; + } - function getStringMappingTypeForGenericType(symbol: ts.Symbol, type: ts.Type): ts.Type { - const id = `${getSymbolId(symbol)},${getTypeId(type)}`; - let result = stringMappingTypes.get(id); - if (!result) { - stringMappingTypes.set(id, result = createStringMappingType(symbol, type)); + // This function assumes the constituent type list is sorted and deduplicated. + function getUnionTypeFromSortedList(types: ts.Type[], objectFlags: ts.ObjectFlags, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[], origin?: ts.Type): ts.Type { + if (types.length === 0) { + return neverType; + } + if (types.length === 1) { + return types[0]; + } + const typeKey = !origin ? getTypeListId(types) : + origin.flags & ts.TypeFlags.Union ? `|${getTypeListId((origin as ts.UnionType).types)}` : + origin.flags & ts.TypeFlags.Intersection ? `&${getTypeListId((origin as ts.IntersectionType).types)}` : + `#${(origin as ts.IndexType).type.id}|${getTypeListId(types)}`; // origin type id alone is insufficient, as `keyof x` may resolve to multiple WIP values while `x` is still resolving + const id = typeKey + getAliasId(aliasSymbol, aliasTypeArguments); + let type = unionTypes.get(id); + if (!type) { + type = createType(ts.TypeFlags.Union) as ts.UnionType; + type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ ts.TypeFlags.Nullable); + type.types = types; + type.origin = origin; + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = aliasTypeArguments; + if (types.length === 2 && types[0].flags & ts.TypeFlags.BooleanLiteral && types[1].flags & ts.TypeFlags.BooleanLiteral) { + type.flags |= ts.TypeFlags.Boolean; + (type as ts.UnionType & ts.IntrinsicType).intrinsicName = "boolean"; } - return result; + unionTypes.set(id, type); } + return type; + } - function createStringMappingType(symbol: ts.Symbol, type: ts.Type) { - const result = createType(ts.TypeFlags.StringMapping) as ts.StringMappingType; - result.symbol = symbol; - result.type = type; - return result; + function getTypeFromUnionTypeNode(node: ts.UnionTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const aliasSymbol = getAliasSymbolForTypeNode(node); + links.resolvedType = getUnionType(ts.map(node.types, getTypeFromTypeNode), ts.UnionReduction.Literal, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); } + return links.resolvedType; + } - function createIndexedAccessType(objectType: ts.Type, indexType: ts.Type, accessFlags: ts.AccessFlags, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { - const type = createType(ts.TypeFlags.IndexedAccess) as ts.IndexedAccessType; - type.objectType = objectType; - type.indexType = indexType; - type.accessFlags = accessFlags; - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = aliasTypeArguments; - return type; + function addTypeToIntersection(typeSet: ts.ESMap, includes: ts.TypeFlags, type: ts.Type) { + const flags = type.flags; + if (flags & ts.TypeFlags.Intersection) { + return addTypesToIntersection(typeSet, includes, (type as ts.IntersectionType).types); } - - /** - * 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 (ts.getObjectFlags(type) & ts.ObjectFlags.JSLiteral) { - return true; + if (isEmptyAnonymousObjectType(type)) { + if (!(includes & ts.TypeFlags.IncludesEmptyObject)) { + includes |= ts.TypeFlags.IncludesEmptyObject; + typeSet.set(type.id.toString(), type); } - if (type.flags & ts.TypeFlags.Union) { - return ts.every((type as ts.UnionType).types, isJSLiteralType); - } - if (type.flags & ts.TypeFlags.Intersection) { - return ts.some((type as ts.IntersectionType).types, isJSLiteralType); + } + else { + if (flags & ts.TypeFlags.AnyOrUnknown) { + if (type === wildcardType) + includes |= ts.TypeFlags.IncludesWildcard; } - if (type.flags & ts.TypeFlags.Instantiable) { - const constraint = getResolvedBaseConstraint(type); - return constraint !== type && isJSLiteralType(constraint); + else if (strictNullChecks || !(flags & ts.TypeFlags.Nullable)) { + if (exactOptionalPropertyTypes && type === missingType) { + includes |= ts.TypeFlags.IncludesMissingType; + type = undefinedType; + } + if (!typeSet.has(type.id.toString())) { + if (type.flags & ts.TypeFlags.Unit && includes & ts.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 |= ts.TypeFlags.NonPrimitive; + } + typeSet.set(type.id.toString(), type); + } } - return false; + includes |= flags & ts.TypeFlags.IncludesMask; } + return includes; + } - function getPropertyNameFromIndex(indexType: ts.Type, accessNode: ts.StringLiteral | ts.Identifier | ts.PrivateIdentifier | ts.ObjectBindingPattern | ts.ArrayBindingPattern | ts.ComputedPropertyName | ts.NumericLiteral | ts.IndexedAccessTypeNode | ts.ElementAccessExpression | ts.SyntheticExpression | undefined) { - return isTypeUsableAsPropertyName(indexType) ? - getPropertyNameFromType(indexType) : - accessNode && ts.isPropertyName(accessNode) ? - // late bound names are handled in the first branch, so here we only need to handle normal names - ts.getPropertyNameForPropertyNameNode(accessNode) : - undefined; + // 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.ESMap, includes: ts.TypeFlags, types: readonly ts.Type[]) { + for (const type of types) { + includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type)); } + return includes; + } - function isUncalledFunctionReference(node: ts.Node, symbol: ts.Symbol) { - if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) { - const parent = ts.findAncestor(node.parent, n => !ts.isAccessExpression(n)) || node.parent; - if (ts.isCallLikeExpression(parent)) { - return ts.isCallOrNewExpression(parent) && ts.isIdentifier(node) && hasMatchingArgument(parent, node); - } - return ts.every(symbol.declarations, d => !ts.isFunctionLike(d) || !!(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Deprecated)); + function removeRedundantPrimitiveTypes(types: ts.Type[], includes: ts.TypeFlags) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + const remove = t.flags & ts.TypeFlags.String && includes & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || + t.flags & ts.TypeFlags.Number && includes & ts.TypeFlags.NumberLiteral || + t.flags & ts.TypeFlags.BigInt && includes & ts.TypeFlags.BigIntLiteral || + t.flags & ts.TypeFlags.ESSymbol && includes & ts.TypeFlags.UniqueESSymbol; + if (remove) { + ts.orderedRemoveItemAt(types, i); } - return true; } + } - function getPropertyTypeForIndexType(originalObjectType: ts.Type, objectType: ts.Type, indexType: ts.Type, fullIndexType: ts.Type, accessNode: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression | undefined, accessFlags: ts.AccessFlags) { - const accessExpression = accessNode && accessNode.kind === ts.SyntaxKind.ElementAccessExpression ? accessNode : undefined; - const propName = accessNode && ts.isPrivateIdentifier(accessNode) ? undefined : getPropertyNameFromIndex(indexType, accessNode); - - if (propName !== undefined) { - if (accessFlags & ts.AccessFlags.Contextual) { - return getTypeOfPropertyOfContextualType(objectType, propName) || anyType; - } - const prop = getPropertyOfType(objectType, propName); - if (prop) { - if (accessFlags & ts.AccessFlags.ReportDeprecated && accessNode && prop.declarations && isDeprecatedSymbol(prop) && isUncalledFunctionReference(accessNode, prop)) { - const deprecatedNode = accessExpression?.argumentExpression ?? (ts.isIndexedAccessTypeNode(accessNode) ? accessNode.indexType : accessNode); - addDeprecatedSuggestion(deprecatedNode, prop.declarations, propName as string); - } - if (accessExpression) { - markPropertyAsReferenced(prop, accessExpression, isSelfTypeAccess(accessExpression.expression, objectType.symbol)); - if (isAssignmentToReadonlyEntity(accessExpression, prop, ts.getAssignmentTargetKind(accessExpression))) { - error(accessExpression.argumentExpression, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop)); - return undefined; - } - if (accessFlags & ts.AccessFlags.CacheSymbol) { - getNodeLinks(accessNode!).resolvedSymbol = prop; - } - if (isThisPropertyAccessInConstructor(accessExpression, prop)) { - return autoType; - } - } - const propType = getTypeOfSymbol(prop); - return accessExpression && ts.getAssignmentTargetKind(accessExpression) !== ts.AssignmentKind.Definite ? - getFlowTypeOfReference(accessExpression, propType) : - propType; - } - if (everyType(objectType, isTupleType) && ts.isNumericLiteralName(propName) && +propName >= 0) { - if (accessNode && everyType(objectType, t => !(t as ts.TupleTypeReference).target.hasRestElement) && !(accessFlags & ts.AccessFlags.NoTupleBoundsCheck)) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - if (isTupleType(objectType)) { - error(indexNode, ts.Diagnostics.Tuple_type_0_of_length_1_has_no_element_at_index_2, typeToString(objectType), getTypeReferenceArity(objectType), ts.unescapeLeadingUnderscores(propName)); - } - else { - error(indexNode, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); - } - } - errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, numberType)); - return mapType(objectType, t => { - const restType = getRestTypeOfTupleType(t as ts.TupleTypeReference) || undefinedType; - return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([restType, undefinedType]) : restType; - }); + // 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: ts.UnionType[], type: ts.Type) { + for (const u of unionTypes) { + if (!containsType(u.types, type)) { + const primitive = type.flags & ts.TypeFlags.StringLiteral ? stringType : + type.flags & ts.TypeFlags.NumberLiteral ? numberType : + type.flags & ts.TypeFlags.BigIntLiteral ? bigintType : + type.flags & ts.TypeFlags.UniqueESSymbol ? esSymbolType : + undefined; + if (!primitive || !containsType(u.types, primitive)) { + return false; } } - if (!(indexType.flags & ts.TypeFlags.Nullable) && isTypeAssignableToKind(indexType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike)) { - if (objectType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never)) { - return objectType; - } - // If no index signature is applicable, we default to the string index signature. In effect, this means the string - // index signature applies even when accessing with a symbol-like type. - const indexInfo = getApplicableIndexInfo(objectType, indexType) || getIndexInfoOfType(objectType, stringType); - if (indexInfo) { - if (accessFlags & ts.AccessFlags.NoIndexSignatures && indexInfo.keyType !== numberType) { - if (accessExpression) { - error(accessExpression, ts.Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType)); - } - return undefined; - } - if (accessNode && indexInfo.keyType === stringType && !isTypeAssignableToKind(indexType, ts.TypeFlags.String | ts.TypeFlags.Number)) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - error(indexNode, ts.Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); - return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; - } - errorIfWritingToReadonlyIndex(indexInfo); - return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; - } - if (indexType.flags & ts.TypeFlags.Never) { - return neverType; + } + return true; + } + + /** + * Returns `true` if the intersection of the template literals and string literals is the empty set, eg `get${string}` & "setX", and should reduce to `never` + */ + function extractRedundantTemplateLiterals(types: ts.Type[]): boolean { + let i = types.length; + const literals = ts.filter(types, t => !!(t.flags & ts.TypeFlags.StringLiteral)); + while (i > 0) { + i--; + const t = types[i]; + if (!(t.flags & ts.TypeFlags.TemplateLiteral)) + continue; + for (const t2 of literals) { + if (isTypeSubtypeOf(t2, t)) { + // eg, ``get${T}` & "getX"` is just `"getX"` + ts.orderedRemoveItemAt(types, i); + break; } - if (isJSLiteralType(objectType)) { - return anyType; + else if (isPatternLiteralType(t)) { + return true; } - if (accessExpression && !isConstEnumObjectType(objectType)) { - if (isObjectLiteralType(objectType)) { - if (noImplicitAny && indexType.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { - diagnostics.add(ts.createDiagnosticForNode(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.StringLiteralType).value, typeToString(objectType))); - return undefinedType; - } - else if (indexType.flags & (ts.TypeFlags.Number | ts.TypeFlags.String)) { - const types = ts.map((objectType as ts.ResolvedType).properties, property => { - return getTypeOfSymbol(property); - }); - return getUnionType(ts.append(types, undefinedType)); - } - } + } + } + return false; + } - if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports!.has(propName) && (globalThisSymbol.exports!.get(propName)!.flags & ts.SymbolFlags.BlockScoped)) { - error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); - } - else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !(accessFlags & ts.AccessFlags.SuppressNoImplicitAnyError)) { - if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { - const typeName = typeToString(objectType); - error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_to_access_the_static_member_2_instead, propName as string, typeName, typeName + "[" + ts.getTextOfNode(accessExpression.argumentExpression) + "]"); - } - else if (getIndexTypeOfType(objectType, numberType)) { - error(accessExpression.argumentExpression, ts.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, ts.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, ts.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: ts.DiagnosticMessageChain | undefined; - if (indexType.flags & ts.TypeFlags.EnumLiteral) { - errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "[" + typeToString(indexType) + "]", typeToString(objectType)); - } - else if (indexType.flags & ts.TypeFlags.UniqueESSymbol) { - const symbolName = getFullyQualifiedName((indexType as ts.UniqueESSymbolType).symbol, accessExpression); - errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "[" + symbolName + "]", typeToString(objectType)); - } - else if (indexType.flags & ts.TypeFlags.StringLiteral) { - errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.StringLiteralType).value, typeToString(objectType)); - } - else if (indexType.flags & ts.TypeFlags.NumberLiteral) { - errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.NumberLiteralType).value, typeToString(objectType)); - } - else if (indexType.flags & (ts.TypeFlags.Number | ts.TypeFlags.String)) { - errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.No_index_signature_with_a_parameter_of_type_0_was_found_on_type_1, typeToString(indexType), typeToString(objectType)); - } + function eachIsUnionContaining(types: ts.Type[], flag: ts.TypeFlags) { + return ts.every(types, t => !!(t.flags & ts.TypeFlags.Union) && ts.some((t as ts.UnionType).types, tt => !!(tt.flags & flag))); + } - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.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(ts.createDiagnosticForNodeFromMessageChain(accessExpression, errorInfo)); - } - } - } + function removeFromEach(types: ts.Type[], flag: ts.TypeFlags) { + for (let i = 0; i < types.length; i++) { + types[i] = filterType(types[i], t => !(t.flags & flag)); + } + } + + // 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: ts.UnionType[] | undefined; + const index = ts.findIndex(types, t => !!(ts.getObjectFlags(t) & ts.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 (ts.getObjectFlags(t) & ts.ObjectFlags.PrimitiveUnion) { + (unionTypes || (unionTypes = [types[index] as ts.UnionType])).push(t as ts.UnionType); + ts.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: 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 undefined; } } - if (isJSLiteralType(objectType)) { - return anyType; - } - if (accessNode) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - if (indexType.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { - error(indexNode, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "" + (indexType as ts.StringLiteralType | ts.NumberLiteralType).value, typeToString(objectType)); + } + // Finally replace the first union with the result + types[index] = getUnionTypeFromSortedList(result, ts.ObjectFlags.PrimitiveUnion); + return true; + } + + function createIntersectionType(types: ts.Type[], aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]) { + const result = createType(ts.TypeFlags.Intersection) as ts.IntersectionType; + result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ ts.TypeFlags.Nullable); + result.types = types; + result.aliasSymbol = aliasSymbol; + 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.ESMap = new ts.Map(); + const includes = addTypesToIntersection(typeMembershipMap, 0, types); + const typeSet: ts.Type[] = ts.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 & ts.TypeFlags.Never) { + return ts.contains(typeSet, silentNeverType) ? silentNeverType : neverType; + } + if (strictNullChecks && includes & ts.TypeFlags.Nullable && includes & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive | ts.TypeFlags.IncludesEmptyObject) || + includes & ts.TypeFlags.NonPrimitive && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.NonPrimitive) || + includes & ts.TypeFlags.StringLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.StringLike) || + includes & ts.TypeFlags.NumberLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.NumberLike) || + includes & ts.TypeFlags.BigIntLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.BigIntLike) || + includes & ts.TypeFlags.ESSymbolLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.ESSymbolLike) || + includes & ts.TypeFlags.VoidLike && includes & (ts.TypeFlags.DisjointDomains & ~ts.TypeFlags.VoidLike)) { + return neverType; + } + if (includes & ts.TypeFlags.TemplateLiteral && includes & ts.TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) { + return neverType; + } + if (includes & ts.TypeFlags.Any) { + return includes & ts.TypeFlags.IncludesWildcard ? wildcardType : anyType; + } + if (!strictNullChecks && includes & ts.TypeFlags.Nullable) { + return includes & ts.TypeFlags.Undefined ? undefinedType : nullType; + } + if (includes & ts.TypeFlags.String && includes & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) || + includes & ts.TypeFlags.Number && includes & ts.TypeFlags.NumberLiteral || + includes & ts.TypeFlags.BigInt && includes & ts.TypeFlags.BigIntLiteral || + includes & ts.TypeFlags.ESSymbol && includes & ts.TypeFlags.UniqueESSymbol) { + removeRedundantPrimitiveTypes(typeSet, includes); + } + if (includes & ts.TypeFlags.IncludesEmptyObject && includes & ts.TypeFlags.Object) { + ts.orderedRemoveItemAt(typeSet, ts.findIndex(typeSet, isEmptyAnonymousObjectType)); + } + if (includes & ts.TypeFlags.IncludesMissingType) { + typeSet[typeSet.indexOf(undefinedType)] = missingType; + } + if (typeSet.length === 0) { + return unknownType; + } + if (typeSet.length === 1) { + return typeSet[0]; + } + const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments); + let result = intersectionTypes.get(id); + if (!result) { + if (includes & ts.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 (indexType.flags & (ts.TypeFlags.String | ts.TypeFlags.Number)) { - error(indexNode, ts.Diagnostics.Type_0_has_no_matching_index_signature_for_type_1, typeToString(objectType), typeToString(indexType)); + else if (eachIsUnionContaining(typeSet, ts.TypeFlags.Undefined)) { + const undefinedOrMissingType = exactOptionalPropertyTypes && ts.some(typeSet, t => containsType((t as ts.UnionType).types, missingType)) ? missingType : undefinedType; + removeFromEach(typeSet, ts.TypeFlags.Undefined); + result = getUnionType([getIntersectionType(typeSet), undefinedOrMissingType], ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + } + else if (eachIsUnionContaining(typeSet, ts.TypeFlags.Null)) { + removeFromEach(typeSet, ts.TypeFlags.Null); + result = getUnionType([getIntersectionType(typeSet), nullType], ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } else { - error(indexNode, ts.Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); + // We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of + // the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type + // exceeds 100000 constituents, report an error. + if (!checkCrossProductUnion(typeSet)) { + return errorType; + } + const constituents = getCrossProductIntersections(typeSet); + // We attach a denormalized origin type when at least one constituent of the cross-product union is an + // intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions). + const origin = ts.some(constituents, t => !!(t.flags & ts.TypeFlags.Intersection)) ? createOriginUnionOrIntersectionType(ts.TypeFlags.Intersection, typeSet) : undefined; + result = getUnionType(constituents, ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin); } } - if (isTypeAny(indexType)) { - return indexType; - } - return undefined; - - function errorIfWritingToReadonlyIndex(indexInfo: ts.IndexInfo | undefined): void { - if (indexInfo && indexInfo.isReadonly && accessExpression && (ts.isAssignmentTarget(accessExpression) || ts.isDeleteTarget(accessExpression))) { - error(accessExpression, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); - } + else { + result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); } + intersectionTypes.set(id, result); } + return result; + } - function getIndexNodeForAccessExpression(accessNode: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression) { - return accessNode.kind === ts.SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : - accessNode.kind === ts.SyntaxKind.IndexedAccessType ? accessNode.indexType : - accessNode.kind === ts.SyntaxKind.ComputedPropertyName ? accessNode.expression : - accessNode; + function getCrossProductUnionSize(types: readonly ts.Type[]) { + return ts.reduceLeft(types, (n, t) => t.flags & ts.TypeFlags.Union ? n * (t as ts.UnionType).types.length : t.flags & ts.TypeFlags.Never ? 0 : n, 1); + } + + function checkCrossProductUnion(types: readonly ts.Type[]) { + const size = getCrossProductUnionSize(types); + if (size >= 100000) { + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "checkCrossProductUnion_DepthLimit", { typeIds: types.map(t => t.id), size }); + error(currentNode, ts.Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); + return false; } + return true; + } - function isPatternLiteralPlaceholderType(type: ts.Type) { - return !!(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.BigInt)); + function getCrossProductIntersections(types: readonly ts.Type[]) { + const count = getCrossProductUnionSize(types); + const intersections: ts.Type[] = []; + for (let i = 0; i < count; i++) { + const constituents = types.slice(); + let n = i; + for (let j = types.length - 1; j >= 0; j--) { + if (types[j].flags & ts.TypeFlags.Union) { + const sourceTypes = (types[j] as ts.UnionType).types; + const length = sourceTypes.length; + constituents[j] = sourceTypes[n % length]; + n = Math.floor(n / length); + } + } + const t = getIntersectionType(constituents); + if (!(t.flags & ts.TypeFlags.Never)) + intersections.push(t); + } + return intersections; + } + + function getTypeFromIntersectionTypeNode(node: ts.IntersectionTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const aliasSymbol = getAliasSymbolForTypeNode(node); + links.resolvedType = getIntersectionType(ts.map(node.types, getTypeFromTypeNode), aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); } + return links.resolvedType; + } + + function createIndexType(type: ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly: boolean) { + const result = createType(ts.TypeFlags.Index) as ts.IndexType; + result.type = type; + result.stringsOnly = stringsOnly; + return result; + } + + function createOriginIndexType(type: ts.InstantiableType | ts.UnionOrIntersectionType) { + const result = createOriginType(ts.TypeFlags.Index) as ts.IndexType; + result.type = type; + return result; + } + + function getIndexTypeForGenericType(type: ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly: boolean) { + return stringsOnly ? + type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) : + type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false)); + } - function isPatternLiteralType(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.TemplateLiteral) && ts.every((type as ts.TemplateLiteralType).types, isPatternLiteralPlaceholderType); + /** + * This roughly mirrors `resolveMappedTypeMembers` in the nongeneric case, except only reports a union of the keys calculated, + * rather than manufacturing the properties. We can't just fetch the `constraintType` since that would ignore mappings + * and mapping the `constraintType` directly ignores how mapped types map _properties_ and not keys (thus ignoring subtype + * reduction in the constraintType) when possible. + * @param noIndexSignatures Indicates if _string_ index signatures should be elided. (other index signatures are always reported) + */ + function getIndexTypeForMappedType(type: ts.MappedType, stringsOnly: boolean, noIndexSignatures: boolean | undefined) { + const typeParameter = getTypeParameterFromMappedType(type); + const constraintType = getConstraintTypeFromMappedType(type); + const nameType = getNameTypeFromMappedType(type.target as ts.MappedType || type); + if (!nameType && !noIndexSignatures) { + // no mapping and no filtering required, just quickly bail to returning the constraint in the common case + return constraintType; + } + const keyTypes: ts.Type[] = []; + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { + // We have a { [P in keyof T]: X } + + // `getApparentType` on the T in a generic mapped type can trigger a circularity + // (conditionals and `infer` types create a circular dependency in the constraint resolution) + // so we only eagerly manifest the keys if the constraint is nongeneric + if (!isGenericIndexType(constraintType)) { + const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' + forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, ts.TypeFlags.StringOrNumberLiteralOrUnique, stringsOnly, addMemberForKeyType); + } + else { + // we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later + // since it's not safe to resolve the shape of modifier type + return getIndexTypeForGenericType(type, stringsOnly); + } } + else { + forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); + } + if (isGenericIndexType(constraintType)) { // include the generic component in the resulting type + forEachType(constraintType, addMemberForKeyType); + } + // we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType, + // so we can return the union that preserves aliases/origin data if possible + const result = noIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.String))) : getUnionType(keyTypes); + if (result.flags & ts.TypeFlags.Union && constraintType.flags & ts.TypeFlags.Union && getTypeListId((result as ts.UnionType).types) === getTypeListId((constraintType as ts.UnionType).types)) { + return constraintType; + } + return result; - function isGenericType(type: ts.Type): boolean { - return !!getGenericObjectFlags(type); + function addMemberForKeyType(keyType: ts.Type) { + const propNameType = nameType ? instantiateType(nameType, appendTypeMapping(type.mapper, typeParameter, keyType)) : keyType; + // `keyof` currently always returns `string | number` for concrete `string` index signatures - the below ternary keeps that behavior for mapped types + // See `getLiteralTypeFromProperties` where there's a similar ternary to cause the same behavior. + keyTypes.push(propNameType === stringType ? stringOrNumberType : propNameType); } + } - function isGenericObjectType(type: ts.Type): boolean { - return !!(getGenericObjectFlags(type) & ts.ObjectFlags.IsGenericObjectType); + // Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N

]: X }, to simply N. This however presumes + // that N distributes over union types, i.e. that N is equivalent to N | N | N. Specifically, we only + // want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable + // introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because + // they're the same type regardless of what's being distributed over. + function hasDistributiveNameType(mappedType: ts.MappedType) { + const typeVariable = getTypeParameterFromMappedType(mappedType); + return isDistributive(getNameTypeFromMappedType(mappedType) || typeVariable); + function isDistributive(type: ts.Type): boolean { + return type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Primitive | ts.TypeFlags.Never | ts.TypeFlags.TypeParameter | ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive) ? true : + type.flags & ts.TypeFlags.Conditional ? (type as ts.ConditionalType).root.isDistributive && (type as ts.ConditionalType).checkType === typeVariable : + type.flags & (ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.TemplateLiteral) ? ts.every((type as ts.UnionOrIntersectionType | ts.TemplateLiteralType).types, isDistributive) : + type.flags & ts.TypeFlags.IndexedAccess ? isDistributive((type as ts.IndexedAccessType).objectType) && isDistributive((type as ts.IndexedAccessType).indexType) : + type.flags & ts.TypeFlags.Substitution ? isDistributive((type as ts.SubstitutionType).substitute) : + type.flags & ts.TypeFlags.StringMapping ? isDistributive((type as ts.StringMappingType).type) : + false; } + } - function isGenericIndexType(type: ts.Type): boolean { - return !!(getGenericObjectFlags(type) & ts.ObjectFlags.IsGenericIndexType); + function getLiteralTypeFromPropertyName(name: ts.PropertyName) { + if (ts.isPrivateIdentifier(name)) { + return neverType; } + return ts.isIdentifier(name) ? getStringLiteralType(ts.unescapeLeadingUnderscores(name.escapedText)) : + getRegularTypeOfLiteralType(ts.isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name)); + } - function getGenericObjectFlags(type: ts.Type): ts.ObjectFlags { - if (type.flags & ts.TypeFlags.UnionOrIntersection) { - if (!((type as ts.UnionOrIntersectionType).objectFlags & ts.ObjectFlags.IsGenericTypeComputed)) { - (type as ts.UnionOrIntersectionType).objectFlags |= ts.ObjectFlags.IsGenericTypeComputed | - ts.reduceLeft((type as ts.UnionOrIntersectionType).types, (flags, t) => flags | getGenericObjectFlags(t), 0); - } - return (type as ts.UnionOrIntersectionType).objectFlags & ts.ObjectFlags.IsGenericType; + function getLiteralTypeFromProperty(prop: ts.Symbol, include: ts.TypeFlags, includeNonPublic?: boolean) { + if (includeNonPublic || !(ts.getDeclarationModifierFlagsFromSymbol(prop) & ts.ModifierFlags.NonPublicAccessibilityModifier)) { + let type = getSymbolLinks(getLateBoundSymbol(prop)).nameType; + if (!type) { + const name = ts.getNameOfDeclaration(prop.valueDeclaration) as ts.PropertyName; + type = prop.escapedName === ts.InternalSymbolName.Default ? getStringLiteralType("default") : + name && getLiteralTypeFromPropertyName(name) || (!ts.isKnownSymbol(prop) ? getStringLiteralType(ts.symbolName(prop)) : undefined); } - if (type.flags & ts.TypeFlags.Substitution) { - if (!((type as ts.SubstitutionType).objectFlags & ts.ObjectFlags.IsGenericTypeComputed)) { - (type as ts.SubstitutionType).objectFlags |= ts.ObjectFlags.IsGenericTypeComputed | - getGenericObjectFlags((type as ts.SubstitutionType).substitute) | getGenericObjectFlags((type as ts.SubstitutionType).baseType); - } - return (type as ts.SubstitutionType).objectFlags & ts.ObjectFlags.IsGenericType; + if (type && type.flags & include) { + return type; } - return (type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type) || isGenericTupleType(type) ? ts.ObjectFlags.IsGenericObjectType : 0) | - (type.flags & (ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && !isPatternLiteralType(type) ? ts.ObjectFlags.IsGenericIndexType : 0); } + return neverType; + } - function getSimplifiedType(type: ts.Type, writing: boolean): ts.Type { - return type.flags & ts.TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type as ts.IndexedAccessType, writing) : - type.flags & ts.TypeFlags.Conditional ? getSimplifiedConditionalType(type as ts.ConditionalType, writing) : - type; - } + function isKeyTypeIncluded(keyType: ts.Type, include: ts.TypeFlags): boolean { + return !!(keyType.flags & include || keyType.flags & ts.TypeFlags.Intersection && ts.some((keyType as ts.IntersectionType).types, t => isKeyTypeIncluded(t, include))); + } - 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 & ts.TypeFlags.UnionOrIntersection) { - const types = ts.map((objectType as ts.UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing)); - return objectType.flags & ts.TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types); - } + function getLiteralTypeFromProperties(type: ts.Type, include: ts.TypeFlags, includeOrigin: boolean) { + const origin = includeOrigin && (ts.getObjectFlags(type) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference) || type.aliasSymbol) ? createOriginIndexType(type) : undefined; + const propertyTypes = ts.map(getPropertiesOfType(type), prop => getLiteralTypeFromProperty(prop, include)); + const indexKeyTypes = ts.map(getIndexInfosOfType(type), info => info !== enumNumberIndexInfo && isKeyTypeIncluded(info.keyType, include) ? + info.keyType === stringType && include & ts.TypeFlags.Number ? stringOrNumberType : info.keyType : neverType); + return getUnionType(ts.concatenate(propertyTypes, indexKeyTypes), ts.UnionReduction.Literal, + /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin); + } + + /** + * A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations) + * must be kept generic, as that instantiation information needs to flow through the type system. By replacing all + * type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic + * to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible). + */ + function isPossiblyReducibleByInstantiation(type: ts.UnionType): boolean { + return ts.some(type.types, t => { + const uniqueFilled = getUniqueLiteralFilledInstantiation(t); + return getReducedType(uniqueFilled) !== uniqueFilled; + }); + } + + function getIndexType(type: ts.Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): ts.Type { + type = getReducedType(type); + return type.flags & ts.TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as ts.UnionType) + ? getIndexTypeForGenericType(type as ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly) + : getIntersectionType(ts.map((type as ts.UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : + type.flags & ts.TypeFlags.Intersection ? getUnionType(ts.map((type as ts.IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : + type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as ts.InstantiableType | ts.UnionOrIntersectionType, stringsOnly) : + ts.getObjectFlags(type) & ts.ObjectFlags.Mapped ? getIndexTypeForMappedType(type as ts.MappedType, stringsOnly, noIndexSignatures) : + type === wildcardType ? wildcardType : + type.flags & ts.TypeFlags.Unknown ? neverType : + type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never) ? keyofConstraintType : + getLiteralTypeFromProperties(type, (noIndexSignatures ? ts.TypeFlags.StringLiteral : ts.TypeFlags.StringLike) | (stringsOnly ? 0 : ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike), stringsOnly === keyofStringsOnly && !noIndexSignatures); + } + + 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 & ts.TypeFlags.Never ? stringType : indexType; + } - 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 & ts.TypeFlags.Union) { - const types = ts.map((indexType as ts.UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing)); - return writing ? getIntersectionType(types) : getUnionType(types); + function getTypeFromTypeOperatorNode(node: ts.TypeOperatorNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + switch (node.operator) { + case ts.SyntaxKind.KeyOfKeyword: + links.resolvedType = getIndexType(getTypeFromTypeNode(node.type)); + break; + case ts.SyntaxKind.UniqueKeyword: + links.resolvedType = node.type.kind === ts.SyntaxKind.SymbolKeyword + ? getESSymbolLikeTypeForNode(ts.walkUpParenthesizedTypes(node.parent)) + : errorType; + break; + case ts.SyntaxKind.ReadonlyKeyword: + links.resolvedType = getTypeFromTypeNode(node.type); + break; + default: + throw ts.Debug.assertNever(node.operator); } } + return links.resolvedType; + } - // 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: ts.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 = 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 & ts.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] + function getTypeFromTemplateTypeNode(node: ts.TemplateLiteralTypeNode) { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getTemplateLiteralType([node.head.text, ...ts.map(node.templateSpans, span => span.literal.text)], ts.map(node.templateSpans, span => getTypeFromTypeNode(span.type))); + } + return links.resolvedType; + } - // A generic tuple type indexed by a number exists only when the index type doesn't select a - // fixed element. We simplify to either the combined type of all elements (when the index type - // the actual number type) or to the combined type of all non-fixed elements. - if (isGenericTupleType(objectType) && indexType.flags & ts.TypeFlags.NumberLike) { - const elementType = getElementTypeOfSliceOfTupleType(objectType, indexType.flags & ts.TypeFlags.Number ? 0 : objectType.target.fixedLength, /*endSkipCount*/ 0, writing); - if (elementType) { - return type[cache] = elementType; - } - } - // If the object type is a mapped type { [P in K]: E }, where K is generic, or { [P in K as N]: E }, where - // K is generic and N is assignable to P, 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)) { - const nameType = getNameTypeFromMappedType(objectType); - if (!nameType || isTypeAssignableTo(nameType, getTypeParameterFromMappedType(objectType))) { - return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing)); - } - } - return type[cache] = type; + function getTemplateLiteralType(texts: readonly string[], types: readonly ts.Type[]): ts.Type { + const unionIndex = ts.findIndex(types, t => !!(t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Union))); + if (unionIndex >= 0) { + return checkCrossProductUnion(types) ? + mapType(types[unionIndex], t => getTemplateLiteralType(texts, ts.replaceElement(types, unionIndex, t))) : + errorType; + } + if (ts.contains(types, wildcardType)) { + return wildcardType; + } + const newTypes: ts.Type[] = []; + const newTexts: string[] = []; + let text = texts[0]; + if (!addSpans(texts, types)) { + return stringType; + } + if (newTypes.length === 0) { + return getStringLiteralType(text); } + newTexts.push(text); + if (ts.every(newTexts, t => t === "") && ts.every(newTypes, t => !!(t.flags & ts.TypeFlags.String))) { + return stringType; + } + const id = `${getTypeListId(newTypes)}|${ts.map(newTexts, t => t.length).join(",")}|${newTexts.join("")}`; + let type = templateLiteralTypes.get(id); + if (!type) { + templateLiteralTypes.set(id, type = createTemplateLiteralType(newTexts, newTypes)); + } + return type; - function getSimplifiedConditionalType(type: ts.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 & ts.TypeFlags.Never && getActualTypeVariable(trueType) === getActualTypeVariable(checkType)) { - if (checkType.flags & ts.TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true - return getSimplifiedType(trueType, writing); + function addSpans(texts: readonly string[] | string, types: readonly ts.Type[]): boolean { + const isTextsArray = ts.isArray(texts); + for (let i = 0; i < types.length; i++) { + const t = types[i]; + const addText = isTextsArray ? texts[i + 1] : texts; + if (t.flags & (ts.TypeFlags.Literal | ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { + text += getTemplateStringForType(t) || ""; + text += addText; + if (!isTextsArray) + return true; } - else if (isIntersectionEmpty(checkType, extendsType)) { // Always false - return neverType; + else if (t.flags & ts.TypeFlags.TemplateLiteral) { + text += (t as ts.TemplateLiteralType).texts[0]; + if (!addSpans((t as ts.TemplateLiteralType).texts, (t as ts.TemplateLiteralType).types)) + return false; + text += addText; + if (!isTextsArray) + return true; } - } - else if (trueType.flags & ts.TypeFlags.Never && getActualTypeVariable(falseType) === getActualTypeVariable(checkType)) { - if (!(checkType.flags & ts.TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true - return neverType; + else if (isGenericIndexType(t) || isPatternLiteralPlaceholderType(t)) { + newTypes.push(t); + newTexts.push(text); + text = addText; } - else if (checkType.flags & ts.TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false - return getSimplifiedType(falseType, writing); + else if (t.flags & ts.TypeFlags.Intersection) { + const added = addSpans(texts[i + 1], (t as ts.IntersectionType).types); + if (!added) + return false; + } + else if (isTextsArray) { + return false; } } - return type; + return true; } + } - /** - * 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 & ts.TypeFlags.Never); - } + function getTemplateStringForType(type: ts.Type) { + return type.flags & ts.TypeFlags.StringLiteral ? (type as ts.StringLiteralType).value : + type.flags & ts.TypeFlags.NumberLiteral ? "" + (type as ts.NumberLiteralType).value : + type.flags & ts.TypeFlags.BigIntLiteral ? ts.pseudoBigIntToString((type as ts.BigIntLiteralType).value) : + type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Nullable) ? (type as ts.IntrinsicType).intrinsicName : + undefined; + } + + function createTemplateLiteralType(texts: readonly string[], types: readonly ts.Type[]) { + const type = createType(ts.TypeFlags.TemplateLiteral) as ts.TemplateLiteralType; + type.texts = texts; + type.types = types; + return type; + } - function substituteIndexedMappedType(objectType: ts.MappedType, index: ts.Type) { - const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]); - const templateMapper = combineTypeMappers(objectType.mapper, mapper); - return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper); + function getStringMappingType(symbol: ts.Symbol, type: ts.Type): ts.Type { + return type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never) ? mapType(type, t => getStringMappingType(symbol, t)) : + isGenericIndexType(type) ? getStringMappingTypeForGenericType(symbol, type) : + type.flags & ts.TypeFlags.StringLiteral ? getStringLiteralType(applyStringMapping(symbol, (type as ts.StringLiteralType).value)) : + type; + } + + function applyStringMapping(symbol: ts.Symbol, str: string) { + switch (intrinsicTypeKinds.get(symbol.escapedName as string)) { + case IntrinsicTypeKind.Uppercase: return str.toUpperCase(); + case IntrinsicTypeKind.Lowercase: return str.toLowerCase(); + case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1); + case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1); } + return str; + } - function getIndexedAccessType(objectType: ts.Type, indexType: ts.Type, accessFlags = ts.AccessFlags.None, accessNode?: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { - return getIndexedAccessTypeOrUndefined(objectType, indexType, accessFlags, accessNode, aliasSymbol, aliasTypeArguments) || (accessNode ? errorType : unknownType); + function getStringMappingTypeForGenericType(symbol: ts.Symbol, type: ts.Type): ts.Type { + const id = `${getSymbolId(symbol)},${getTypeId(type)}`; + let result = stringMappingTypes.get(id); + if (!result) { + stringMappingTypes.set(id, result = createStringMappingType(symbol, type)); } + return result; + } - function indexTypeLessThan(indexType: ts.Type, limit: number) { - return everyType(indexType, t => { - if (t.flags & ts.TypeFlags.StringOrNumberLiteral) { - const propName = getPropertyNameFromType(t as ts.StringLiteralType | ts.NumberLiteralType); - if (ts.isNumericLiteralName(propName)) { - const index = +propName; - return index >= 0 && index < limit; - } - } - return false; - }); + function createStringMappingType(symbol: ts.Symbol, type: ts.Type) { + const result = createType(ts.TypeFlags.StringMapping) as ts.StringMappingType; + result.symbol = symbol; + result.type = type; + return result; + } + + function createIndexedAccessType(objectType: ts.Type, indexType: ts.Type, accessFlags: ts.AccessFlags, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { + const type = createType(ts.TypeFlags.IndexedAccess) as ts.IndexedAccessType; + type.objectType = objectType; + type.indexType = indexType; + type.accessFlags = accessFlags; + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = aliasTypeArguments; + 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 (ts.getObjectFlags(type) & ts.ObjectFlags.JSLiteral) { + return true; + } + if (type.flags & ts.TypeFlags.Union) { + return ts.every((type as ts.UnionType).types, isJSLiteralType); + } + if (type.flags & ts.TypeFlags.Intersection) { + return ts.some((type as ts.IntersectionType).types, isJSLiteralType); + } + if (type.flags & ts.TypeFlags.Instantiable) { + const constraint = getResolvedBaseConstraint(type); + return constraint !== type && isJSLiteralType(constraint); } + return false; + } - function getIndexedAccessTypeOrUndefined(objectType: ts.Type, indexType: ts.Type, accessFlags = ts.AccessFlags.None, accessNode?: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type | undefined { - if (objectType === wildcardType || indexType === wildcardType) { - return wildcardType; + function getPropertyNameFromIndex(indexType: ts.Type, accessNode: ts.StringLiteral | ts.Identifier | ts.PrivateIdentifier | ts.ObjectBindingPattern | ts.ArrayBindingPattern | ts.ComputedPropertyName | ts.NumericLiteral | ts.IndexedAccessTypeNode | ts.ElementAccessExpression | ts.SyntheticExpression | undefined) { + return isTypeUsableAsPropertyName(indexType) ? + getPropertyNameFromType(indexType) : + accessNode && ts.isPropertyName(accessNode) ? + // late bound names are handled in the first branch, so here we only need to handle normal names + ts.getPropertyNameForPropertyNameNode(accessNode) : + undefined; + } + + function isUncalledFunctionReference(node: ts.Node, symbol: ts.Symbol) { + if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) { + const parent = ts.findAncestor(node.parent, n => !ts.isAccessExpression(n)) || node.parent; + if (ts.isCallLikeExpression(parent)) { + return ts.isCallOrNewExpression(parent) && ts.isIdentifier(node) && hasMatchingArgument(parent, node); } - // 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 & ts.TypeFlags.Nullable) && isTypeAssignableToKind(indexType, ts.TypeFlags.String | ts.TypeFlags.Number)) { - indexType = stringType; - } - // In noUncheckedIndexedAccess mode, indexed access operations that occur in an expression in a read position and resolve to - // an index signature have 'undefined' included in their type. - if (compilerOptions.noUncheckedIndexedAccess && accessFlags & ts.AccessFlags.ExpressionPosition) - accessFlags |= ts.AccessFlags.IncludeUndefined; - // If the index type is generic, or if the object type is generic and doesn't originate in an expression and - // the operation isn't exclusively indexing the fixed (non-variadic) portion of a tuple type, 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 !== ts.SyntaxKind.IndexedAccessType ? - isGenericTupleType(objectType) && !indexTypeLessThan(indexType, objectType.target.fixedLength) : - isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)))) { - if (objectType.flags & ts.TypeFlags.AnyOrUnknown) { - return objectType; - } - // Defer the operation by creating an indexed access type. - const persistentAccessFlags = accessFlags & ts.AccessFlags.Persistent; - const id = objectType.id + "," + indexType.id + "," + persistentAccessFlags + getAliasId(aliasSymbol, aliasTypeArguments); - let type = indexedAccessTypes.get(id); - if (!type) { - indexedAccessTypes.set(id, type = createIndexedAccessType(objectType, indexType, persistentAccessFlags, aliasSymbol, aliasTypeArguments)); - } + return ts.every(symbol.declarations, d => !ts.isFunctionLike(d) || !!(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Deprecated)); + } + return true; + } - return type; + function getPropertyTypeForIndexType(originalObjectType: ts.Type, objectType: ts.Type, indexType: ts.Type, fullIndexType: ts.Type, accessNode: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression | undefined, accessFlags: ts.AccessFlags) { + const accessExpression = accessNode && accessNode.kind === ts.SyntaxKind.ElementAccessExpression ? accessNode : undefined; + const propName = accessNode && ts.isPrivateIdentifier(accessNode) ? undefined : getPropertyNameFromIndex(indexType, accessNode); + + if (propName !== undefined) { + if (accessFlags & ts.AccessFlags.Contextual) { + return getTypeOfPropertyOfContextualType(objectType, propName) || anyType; } - // 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 = getReducedApparentType(objectType); - if (indexType.flags & ts.TypeFlags.Union && !(indexType.flags & ts.TypeFlags.Boolean)) { - const propTypes: ts.Type[] = []; - let wasMissingProp = false; - for (const t of (indexType as ts.UnionType).types) { - const propType = getPropertyTypeForIndexType(objectType, apparentObjectType, t, indexType, accessNode, accessFlags | (wasMissingProp ? ts.AccessFlags.SuppressNoImplicitAnyError : 0)); - if (propType) { - propTypes.push(propType); - } - else if (!accessNode) { - // If there's no error node, we can immeditely stop, since error reporting is off + const prop = getPropertyOfType(objectType, propName); + if (prop) { + if (accessFlags & ts.AccessFlags.ReportDeprecated && accessNode && prop.declarations && isDeprecatedSymbol(prop) && isUncalledFunctionReference(accessNode, prop)) { + const deprecatedNode = accessExpression?.argumentExpression ?? (ts.isIndexedAccessTypeNode(accessNode) ? accessNode.indexType : accessNode); + addDeprecatedSuggestion(deprecatedNode, prop.declarations, propName as string); + } + if (accessExpression) { + markPropertyAsReferenced(prop, accessExpression, isSelfTypeAccess(accessExpression.expression, objectType.symbol)); + if (isAssignmentToReadonlyEntity(accessExpression, prop, ts.getAssignmentTargetKind(accessExpression))) { + error(accessExpression.argumentExpression, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop)); 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 (accessFlags & ts.AccessFlags.CacheSymbol) { + getNodeLinks(accessNode!).resolvedSymbol = prop; + } + if (isThisPropertyAccessInConstructor(accessExpression, prop)) { + return autoType; } } - if (wasMissingProp) { - return undefined; + const propType = getTypeOfSymbol(prop); + return accessExpression && ts.getAssignmentTargetKind(accessExpression) !== ts.AssignmentKind.Definite ? + getFlowTypeOfReference(accessExpression, propType) : + propType; + } + if (everyType(objectType, isTupleType) && ts.isNumericLiteralName(propName) && +propName >= 0) { + if (accessNode && everyType(objectType, t => !(t as ts.TupleTypeReference).target.hasRestElement) && !(accessFlags & ts.AccessFlags.NoTupleBoundsCheck)) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + if (isTupleType(objectType)) { + error(indexNode, ts.Diagnostics.Tuple_type_0_of_length_1_has_no_element_at_index_2, typeToString(objectType), getTypeReferenceArity(objectType), ts.unescapeLeadingUnderscores(propName)); + } + else { + error(indexNode, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); + } } - return accessFlags & ts.AccessFlags.Writing - ? getIntersectionType(propTypes, aliasSymbol, aliasTypeArguments) - : getUnionType(propTypes, ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, numberType)); + return mapType(objectType, t => { + const restType = getRestTypeOfTupleType(t as ts.TupleTypeReference) || undefinedType; + return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([restType, undefinedType]) : restType; + }); } - return getPropertyTypeForIndexType(objectType, apparentObjectType, indexType, indexType, accessNode, accessFlags | ts.AccessFlags.CacheSymbol | ts.AccessFlags.ReportDeprecated); } - - function getTypeFromIndexedAccessTypeNode(node: ts.IndexedAccessTypeNode) { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const objectType = getTypeFromTypeNode(node.objectType); - const indexType = getTypeFromTypeNode(node.indexType); - const potentialAlias = getAliasSymbolForTypeNode(node); - const resolved = getIndexedAccessType(objectType, indexType, ts.AccessFlags.None, node, potentialAlias, getTypeArgumentsForAliasSymbol(potentialAlias)); - links.resolvedType = resolved.flags & ts.TypeFlags.IndexedAccess && - (resolved as ts.IndexedAccessType).objectType === objectType && - (resolved as ts.IndexedAccessType).indexType === indexType ? - getConditionalFlowTypeOfType(resolved, node) : resolved; + if (!(indexType.flags & ts.TypeFlags.Nullable) && isTypeAssignableToKind(indexType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike)) { + if (objectType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never)) { + return objectType; } - return links.resolvedType; - } - - function getTypeFromMappedTypeNode(node: ts.MappedTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const type = createObjectType(ts.ObjectFlags.Mapped, node.symbol) as ts.MappedType; - 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); + // If no index signature is applicable, we default to the string index signature. In effect, this means the string + // index signature applies even when accessing with a symbol-like type. + const indexInfo = getApplicableIndexInfo(objectType, indexType) || getIndexInfoOfType(objectType, stringType); + if (indexInfo) { + if (accessFlags & ts.AccessFlags.NoIndexSignatures && indexInfo.keyType !== numberType) { + if (accessExpression) { + error(accessExpression, ts.Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType)); + } + return undefined; + } + if (accessNode && indexInfo.keyType === stringType && !isTypeAssignableToKind(indexType, ts.TypeFlags.String | ts.TypeFlags.Number)) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + error(indexNode, ts.Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); + return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; + } + errorIfWritingToReadonlyIndex(indexInfo); + return accessFlags & ts.AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; } - return links.resolvedType; - } - - function getActualTypeVariable(type: ts.Type): ts.Type { - if (type.flags & ts.TypeFlags.Substitution) { - return (type as ts.SubstitutionType).baseType; + if (indexType.flags & ts.TypeFlags.Never) { + return neverType; } - if (type.flags & ts.TypeFlags.IndexedAccess && ((type as ts.IndexedAccessType).objectType.flags & ts.TypeFlags.Substitution || - (type as ts.IndexedAccessType).indexType.flags & ts.TypeFlags.Substitution)) { - return getIndexedAccessType(getActualTypeVariable((type as ts.IndexedAccessType).objectType), getActualTypeVariable((type as ts.IndexedAccessType).indexType)); + if (isJSLiteralType(objectType)) { + return anyType; } - return type; - } - - function maybeCloneTypeParameter(p: ts.TypeParameter) { - const constraint = getConstraintOfTypeParameter(p); - return constraint && (isGenericObjectType(constraint) || isGenericIndexType(constraint)) ? cloneTypeParameter(p) : p; - } - - function isTypicalNondistributiveConditional(root: ts.ConditionalRoot) { - return !root.isDistributive && isSingletonTupleType(root.node.checkType) && isSingletonTupleType(root.node.extendsType); - } - - function isSingletonTupleType(node: ts.TypeNode) { - return ts.isTupleTypeNode(node) && - ts.length(node.elements) === 1 && - !ts.isOptionalTypeNode(node.elements[0]) && - !ts.isRestTypeNode(node.elements[0]) && - !(ts.isNamedTupleMember(node.elements[0]) && (node.elements[0].questionToken || node.elements[0].dotDotDotToken)); - } + if (accessExpression && !isConstEnumObjectType(objectType)) { + if (isObjectLiteralType(objectType)) { + if (noImplicitAny && indexType.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { + diagnostics.add(ts.createDiagnosticForNode(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.StringLiteralType).value, typeToString(objectType))); + return undefinedType; + } + else if (indexType.flags & (ts.TypeFlags.Number | ts.TypeFlags.String)) { + const types = ts.map((objectType as ts.ResolvedType).properties, property => { + return getTypeOfSymbol(property); + }); + return getUnionType(ts.append(types, undefinedType)); + } + } - /** - * We syntactually check for common nondistributive conditional shapes and unwrap them into - * the intended comparison - we do this so we can check if the unwrapped types are generic or - * not and appropriately defer condition calculation - */ - function unwrapNondistributiveConditionalTuple(root: ts.ConditionalRoot, type: ts.Type) { - return isTypicalNondistributiveConditional(root) && isTupleType(type) ? getTypeArguments(type)[0] : type; - } - - function getConditionalType(root: ts.ConditionalRoot, mapper: ts.TypeMapper | undefined, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { - let result; - let extraTypes: ts.Type[] | undefined; - let tailCount = 0; - // We loop here for an immediately nested conditional type in the false position, effectively treating - // types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for - // purposes of resolution. We also loop here when resolution of a conditional type ends in resolution of - // another (or, through recursion, possibly the same) conditional type. In the potentially tail-recursive - // cases we increment the tail recursion counter and stop after 1000 iterations. - while (true) { - if (tailCount === 1000) { - error(currentNode, ts.Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); - result = errorType; - break; + if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports!.has(propName) && (globalThisSymbol.exports!.get(propName)!.flags & ts.SymbolFlags.BlockScoped)) { + error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); } - const isUnwrapped = isTypicalNondistributiveConditional(root); - const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, getActualTypeVariable(root.checkType)), mapper); - const checkTypeInstantiable = isGenericType(checkType); - const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper); - if (checkType === wildcardType || extendsType === wildcardType) { - return wildcardType; - } - let combinedMapper: ts.TypeMapper | undefined; - if (root.inferTypeParameters) { - // When we're looking at making an inference for an infer type, when we get its constraint, it'll automagically be - // instantiated with the context, so it doesn't need the mapper for the inference contex - however the constraint - // may refer to another _root_, _uncloned_ `infer` type parameter [1], or to something mapped by `mapper` [2]. - // [1] Eg, if we have `Foo` and `Foo` - `B` is constrained to `T`, which, in turn, has been instantiated - // as `number` - // Conversely, if we have `Foo`, `B` is still constrained to `T` and `T` is instantiated as `A` - // [2] Eg, if we have `Foo` and `Foo` where `Q` is mapped by `mapper` into `number` - `B` is constrained to `T` - // which is in turn instantiated as `Q`, which is in turn instantiated as `number`. - // So we need to: - // * Clone the type parameters so their constraints can be instantiated in the context of `mapper` (otherwise theyd only get inference context information) - // * Set the clones to both map the conditional's enclosing `mapper` and the original params - // * instantiate the extends type with the clones - // * incorporate all of the component mappers into the combined mapper for the true and false members - // This means we have three mappers that need applying: - // * The original `mapper` used to create this conditional - // * The mapper that maps the old root type parameter to the clone (`freshMapper`) - // * The mapper that maps the clone to its inference result (`context.mapper`) - const freshParams = ts.sameMap(root.inferTypeParameters, maybeCloneTypeParameter); - const freshMapper = freshParams !== root.inferTypeParameters ? createTypeMapper(root.inferTypeParameters, freshParams) : undefined; - const context = createInferenceContext(freshParams, /*signature*/ undefined, ts.InferenceFlags.None); - if (freshMapper) { - const freshCombinedMapper = combineTypeMappers(mapper, freshMapper); - for (const p of freshParams) { - if (root.inferTypeParameters.indexOf(p) === -1) { - p.mapper = freshCombinedMapper; + else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !(accessFlags & ts.AccessFlags.SuppressNoImplicitAnyError)) { + if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { + const typeName = typeToString(objectType); + error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_to_access_the_static_member_2_instead, propName as string, typeName, typeName + "[" + ts.getTextOfNode(accessExpression.argumentExpression) + "]"); + } + else if (getIndexTypeOfType(objectType, numberType)) { + error(accessExpression.argumentExpression, ts.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, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName as string, typeToString(objectType), suggestion); } } - } - // 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 || !ts.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, instantiateType(extendsType, freshMapper), ts.InferencePriority.NoConstraints | ts.InferencePriority.AlwaysStrict); - } - const innerMapper = combineTypeMappers(freshMapper, context.mapper); - // It's possible for 'infer T' type paramteters to be given uninstantiated constraints when the - // those type parameters are used in type references (see getInferredTypeParameterConstraint). For - // that reason we need context.mapper to be first in the combined mapper. See #42636 for examples. - combinedMapper = mapper ? combineTypeMappers(innerMapper, mapper) : innerMapper; - } - // Instantiate the extends type including inferences for 'infer T' type parameters - const inferredExtendsType = combinedMapper ? instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), combinedMapper) : extendsType; - // We attempt to resolve the conditional type only when the check and extends types are non-generic - if (!checkTypeInstantiable && !isGenericType(inferredExtendsType)) { - // 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 (!(inferredExtendsType.flags & ts.TypeFlags.AnyOrUnknown) && ((checkType.flags & ts.TypeFlags.Any && !isUnwrapped) || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) { - // Return union of trueType and falseType for 'any' since it matches anything - if (checkType.flags & ts.TypeFlags.Any && !isUnwrapped) { - (extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper)); - } - // If falseType is an immediately nested conditional type that isn't distributive or has an - // identical checkType, switch to that type and loop. - const falseType = getTypeFromTypeNode(root.node.falseType); - if (falseType.flags & ts.TypeFlags.Conditional) { - const newRoot = (falseType as ts.ConditionalType).root; - if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) { - root = newRoot; - continue; + else { + const suggestion = getSuggestionForNonexistentIndexSignature(objectType, accessExpression, indexType); + if (suggestion !== undefined) { + error(accessExpression, ts.Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature_Did_you_mean_to_call_1, typeToString(objectType), suggestion); } - if (canTailRecurse(falseType, mapper)) { - continue; + else { + let errorInfo: ts.DiagnosticMessageChain | undefined; + if (indexType.flags & ts.TypeFlags.EnumLiteral) { + errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "[" + typeToString(indexType) + "]", typeToString(objectType)); + } + else if (indexType.flags & ts.TypeFlags.UniqueESSymbol) { + const symbolName = getFullyQualifiedName((indexType as ts.UniqueESSymbolType).symbol, accessExpression); + errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "[" + symbolName + "]", typeToString(objectType)); + } + else if (indexType.flags & ts.TypeFlags.StringLiteral) { + errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.StringLiteralType).value, typeToString(objectType)); + } + else if (indexType.flags & ts.TypeFlags.NumberLiteral) { + errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as ts.NumberLiteralType).value, typeToString(objectType)); + } + else if (indexType.flags & (ts.TypeFlags.Number | ts.TypeFlags.String)) { + errorInfo = ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics.No_index_signature_with_a_parameter_of_type_0_was_found_on_type_1, typeToString(indexType), typeToString(objectType)); + } + + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.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(ts.createDiagnosticForNodeFromMessageChain(accessExpression, errorInfo)); } } - result = instantiateType(falseType, mapper); - break; - } - // 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 (inferredExtendsType.flags & ts.TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) { - const trueType = getTypeFromTypeNode(root.node.trueType); - const trueMapper = combinedMapper || mapper; - if (canTailRecurse(trueType, trueMapper)) { - continue; - } - result = instantiateType(trueType, trueMapper); - break; } } - // Return a deferred type for a check that is neither definitely true nor definitely false - result = createType(ts.TypeFlags.Conditional) as ts.ConditionalType; - result.root = root; - result.checkType = instantiateType(root.checkType, mapper); - result.extendsType = instantiateType(root.extendsType, mapper); - result.mapper = mapper; - result.combinedMapper = combinedMapper; - result.aliasSymbol = aliasSymbol || root.aliasSymbol; - result.aliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217 - break; + return undefined; } - return extraTypes ? getUnionType(ts.append(extraTypes, result)) : result; - // We tail-recurse for generic conditional types that (a) have not already been evaluated and cached, and - // (b) are non distributive, have a check type that is unaffected by instantiation, or have a non-union check - // type. Note that recursion is possible only through aliased conditional types, so we only increment the tail - // recursion counter for those. - function canTailRecurse(newType: ts.Type, newMapper: ts.TypeMapper | undefined) { - if (newType.flags & ts.TypeFlags.Conditional && newMapper) { - const newRoot = (newType as ts.ConditionalType).root; - if (newRoot.outerTypeParameters) { - const typeParamMapper = combineTypeMappers((newType as ts.ConditionalType).mapper, newMapper); - const typeArguments = ts.map(newRoot.outerTypeParameters, t => getMappedType(t, typeParamMapper)); - const newRootMapper = createTypeMapper(newRoot.outerTypeParameters, typeArguments); - const newCheckType = newRoot.isDistributive ? getMappedType(newRoot.checkType, newRootMapper) : undefined; - if (!newCheckType || newCheckType === newRoot.checkType || !(newCheckType.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never))) { - root = newRoot; - mapper = newRootMapper; - aliasSymbol = undefined; - aliasTypeArguments = undefined; - if (newRoot.aliasSymbol) { - tailCount++; - } - return true; - } - } - } - return false; + } + if (isJSLiteralType(objectType)) { + return anyType; + } + if (accessNode) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + if (indexType.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { + error(indexNode, ts.Diagnostics.Property_0_does_not_exist_on_type_1, "" + (indexType as ts.StringLiteralType | ts.NumberLiteralType).value, typeToString(objectType)); + } + else if (indexType.flags & (ts.TypeFlags.String | ts.TypeFlags.Number)) { + error(indexNode, ts.Diagnostics.Type_0_has_no_matching_index_signature_for_type_1, typeToString(objectType), typeToString(indexType)); + } + else { + error(indexNode, ts.Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); + } + } + if (isTypeAny(indexType)) { + return indexType; + } + return undefined; + + function errorIfWritingToReadonlyIndex(indexInfo: ts.IndexInfo | undefined): void { + if (indexInfo && indexInfo.isReadonly && accessExpression && (ts.isAssignmentTarget(accessExpression) || ts.isDeleteTarget(accessExpression))) { + error(accessExpression, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); + } + } + } + + function getIndexNodeForAccessExpression(accessNode: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression) { + return accessNode.kind === ts.SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : + accessNode.kind === ts.SyntaxKind.IndexedAccessType ? accessNode.indexType : + accessNode.kind === ts.SyntaxKind.ComputedPropertyName ? accessNode.expression : + accessNode; + } + + function isPatternLiteralPlaceholderType(type: ts.Type) { + return !!(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.BigInt)); + } + + function isPatternLiteralType(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.TemplateLiteral) && ts.every((type as ts.TemplateLiteralType).types, isPatternLiteralPlaceholderType); + } + + function isGenericType(type: ts.Type): boolean { + return !!getGenericObjectFlags(type); + } + + function isGenericObjectType(type: ts.Type): boolean { + return !!(getGenericObjectFlags(type) & ts.ObjectFlags.IsGenericObjectType); + } + + function isGenericIndexType(type: ts.Type): boolean { + return !!(getGenericObjectFlags(type) & ts.ObjectFlags.IsGenericIndexType); + } + + function getGenericObjectFlags(type: ts.Type): ts.ObjectFlags { + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + if (!((type as ts.UnionOrIntersectionType).objectFlags & ts.ObjectFlags.IsGenericTypeComputed)) { + (type as ts.UnionOrIntersectionType).objectFlags |= ts.ObjectFlags.IsGenericTypeComputed | + ts.reduceLeft((type as ts.UnionOrIntersectionType).types, (flags, t) => flags | getGenericObjectFlags(t), 0); + } + return (type as ts.UnionOrIntersectionType).objectFlags & ts.ObjectFlags.IsGenericType; + } + if (type.flags & ts.TypeFlags.Substitution) { + if (!((type as ts.SubstitutionType).objectFlags & ts.ObjectFlags.IsGenericTypeComputed)) { + (type as ts.SubstitutionType).objectFlags |= ts.ObjectFlags.IsGenericTypeComputed | + getGenericObjectFlags((type as ts.SubstitutionType).substitute) | getGenericObjectFlags((type as ts.SubstitutionType).baseType); } + return (type as ts.SubstitutionType).objectFlags & ts.ObjectFlags.IsGenericType; } + return (type.flags & ts.TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type) || isGenericTupleType(type) ? ts.ObjectFlags.IsGenericObjectType : 0) | + (type.flags & (ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && !isPatternLiteralType(type) ? ts.ObjectFlags.IsGenericIndexType : 0); + } - function getTrueTypeFromConditionalType(type: ts.ConditionalType) { - return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.mapper)); - } + function getSimplifiedType(type: ts.Type, writing: boolean): ts.Type { + return type.flags & ts.TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type as ts.IndexedAccessType, writing) : + type.flags & ts.TypeFlags.Conditional ? getSimplifiedConditionalType(type as ts.ConditionalType, writing) : + type; + } - function getFalseTypeFromConditionalType(type: ts.ConditionalType) { - return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(getTypeFromTypeNode(type.root.node.falseType), type.mapper)); + 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 & ts.TypeFlags.UnionOrIntersection) { + const types = ts.map((objectType as ts.UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing)); + return objectType.flags & ts.TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types); } + } - function getInferredTrueTypeFromConditionalType(type: ts.ConditionalType) { - return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.combinedMapper) : getTrueTypeFromConditionalType(type)); + 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 & ts.TypeFlags.Union) { + const types = ts.map((indexType as ts.UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing)); + return writing ? getIntersectionType(types) : getUnionType(types); } + } - function getInferTypeParameters(node: ts.ConditionalTypeNode): ts.TypeParameter[] | undefined { - let result: ts.TypeParameter[] | undefined; - if (node.locals) { - node.locals.forEach(symbol => { - if (symbol.flags & ts.SymbolFlags.TypeParameter) { - result = ts.append(result, getDeclaredTypeOfSymbol(symbol)); - } - }); + // 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: ts.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 = 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 & ts.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; } - return result; } + // 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] - function isDistributionDependent(root: ts.ConditionalRoot) { - return root.isDistributive && (isTypeParameterPossiblyReferenced(root.checkType as ts.TypeParameter, root.node.trueType) || - isTypeParameterPossiblyReferenced(root.checkType as ts.TypeParameter, root.node.falseType)); + // A generic tuple type indexed by a number exists only when the index type doesn't select a + // fixed element. We simplify to either the combined type of all elements (when the index type + // the actual number type) or to the combined type of all non-fixed elements. + if (isGenericTupleType(objectType) && indexType.flags & ts.TypeFlags.NumberLike) { + const elementType = getElementTypeOfSliceOfTupleType(objectType, indexType.flags & ts.TypeFlags.Number ? 0 : objectType.target.fixedLength, /*endSkipCount*/ 0, writing); + if (elementType) { + return type[cache] = elementType; + } } - - function getTypeFromConditionalTypeNode(node: ts.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 : ts.filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node)); - const root: ts.ConditionalRoot = { - node, - checkType, - extendsType: getTypeFromTypeNode(node.extendsType), - isDistributive: !!(checkType.flags & ts.TypeFlags.TypeParameter), - inferTypeParameters: getInferTypeParameters(node), - outerTypeParameters, - instantiations: undefined, - aliasSymbol, - aliasTypeArguments - }; - links.resolvedType = getConditionalType(root, /*mapper*/ undefined); - if (outerTypeParameters) { - root.instantiations = new ts.Map(); - root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType); - } + // If the object type is a mapped type { [P in K]: E }, where K is generic, or { [P in K as N]: E }, where + // K is generic and N is assignable to P, 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)) { + const nameType = getNameTypeFromMappedType(objectType); + if (!nameType || isTypeAssignableTo(nameType, getTypeParameterFromMappedType(objectType))) { + return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing)); } - return links.resolvedType; } + return type[cache] = type; + } - function getTypeFromInferTypeNode(node: ts.InferTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node.typeParameter)); + function getSimplifiedConditionalType(type: ts.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 & ts.TypeFlags.Never && getActualTypeVariable(trueType) === getActualTypeVariable(checkType)) { + if (checkType.flags & ts.TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + return getSimplifiedType(trueType, writing); + } + else if (isIntersectionEmpty(checkType, extendsType)) { // Always false + return neverType; } - return links.resolvedType; } - - function getIdentifierChain(node: ts.EntityName): ts.Identifier[] { - if (ts.isIdentifier(node)) { - return [node]; + else if (trueType.flags & ts.TypeFlags.Never && getActualTypeVariable(falseType) === getActualTypeVariable(checkType)) { + if (!(checkType.flags & ts.TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + return neverType; } - else { - return ts.append(getIdentifierChain(node.left), node.right); + else if (checkType.flags & ts.TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false + return getSimplifiedType(falseType, writing); } } + return type; + } - function getTypeFromImportTypeNode(node: ts.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, ts.Diagnostics.Type_arguments_cannot_be_used_here); - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = errorType; - } - if (!ts.isLiteralImportTypeNode(node)) { - error(node.argument, ts.Diagnostics.String_literal_expected); - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = errorType; - } - const targetMeaning = node.isTypeOf ? ts.SymbolFlags.Value : node.flags & ts.NodeFlags.JSDoc ? ts.SymbolFlags.Value | ts.SymbolFlags.Type : ts.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 (!ts.nodeIsMissing(node.qualifier)) { - const nameStack: ts.Identifier[] = getIdentifierChain(node.qualifier!); - let currentNamespace = moduleSymbol; - let current: ts.Identifier | undefined; - while (current = nameStack.shift()) { - const meaning = nameStack.length ? ts.SymbolFlags.Namespace : targetMeaning; - // typeof a.b.c is normally resolved using `checkExpression` which in turn defers to `checkQualifiedName` - // That, in turn, ultimately uses `getPropertyOfType` on the type of the symbol, which differs slightly from - // the `exports` lookup process that only looks up namespace members which is used for most type references - const mergedResolvedSymbol = getMergedSymbol(resolveSymbol(currentNamespace)); - const next = node.isTypeOf - ? getPropertyOfType(getTypeOfSymbol(mergedResolvedSymbol), current.escapedText, /*skipObjectFunctionPropertyAugment*/ false, /*includeTypeOnlyMembers*/ true) - : getSymbol(getExportsOfSymbol(mergedResolvedSymbol), current.escapedText, meaning); - if (!next) { - error(current, ts.Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(currentNamespace), ts.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 === ts.SymbolFlags.Value - ? ts.Diagnostics.Module_0_does_not_refer_to_a_value_but_is_used_as_a_value_here - : ts.Diagnostics.Module_0_does_not_refer_to_a_type_but_is_used_as_a_type_here_Did_you_mean_typeof_import_0; + /** + * 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 & ts.TypeFlags.Never); + } + + function substituteIndexedMappedType(objectType: ts.MappedType, index: ts.Type) { + const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]); + const templateMapper = combineTypeMappers(objectType.mapper, mapper); + return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper); + } - error(node, errorMessage, node.argument.literal.text); + function getIndexedAccessType(objectType: ts.Type, indexType: ts.Type, accessFlags = ts.AccessFlags.None, accessNode?: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + return getIndexedAccessTypeOrUndefined(objectType, indexType, accessFlags, accessNode, aliasSymbol, aliasTypeArguments) || (accessNode ? errorType : unknownType); + } - links.resolvedSymbol = unknownSymbol; - links.resolvedType = errorType; - } + function indexTypeLessThan(indexType: ts.Type, limit: number) { + return everyType(indexType, t => { + if (t.flags & ts.TypeFlags.StringOrNumberLiteral) { + const propName = getPropertyNameFromType(t as ts.StringLiteralType | ts.NumberLiteralType); + if (ts.isNumericLiteralName(propName)) { + const index = +propName; + return index >= 0 && index < limit; } } - return links.resolvedType; - } + return false; + }); + } - function resolveImportSymbolType(node: ts.ImportTypeNode, links: ts.NodeLinks, symbol: ts.Symbol, meaning: ts.SymbolFlags) { - const resolvedSymbol = resolveSymbol(symbol); - links.resolvedSymbol = resolvedSymbol; - if (meaning === ts.SymbolFlags.Value) { - return getTypeOfSymbol(symbol); // intentionally doesn't use resolved symbol so type is cached as expected on the alias + function getIndexedAccessTypeOrUndefined(objectType: ts.Type, indexType: ts.Type, accessFlags = ts.AccessFlags.None, accessNode?: ts.ElementAccessExpression | ts.IndexedAccessTypeNode | ts.PropertyName | ts.BindingName | ts.SyntheticExpression, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): 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 & ts.TypeFlags.Nullable) && isTypeAssignableToKind(indexType, ts.TypeFlags.String | ts.TypeFlags.Number)) { + indexType = stringType; + } + // In noUncheckedIndexedAccess mode, indexed access operations that occur in an expression in a read position and resolve to + // an index signature have 'undefined' included in their type. + if (compilerOptions.noUncheckedIndexedAccess && accessFlags & ts.AccessFlags.ExpressionPosition) + accessFlags |= ts.AccessFlags.IncludeUndefined; + // If the index type is generic, or if the object type is generic and doesn't originate in an expression and + // the operation isn't exclusively indexing the fixed (non-variadic) portion of a tuple type, 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 !== ts.SyntaxKind.IndexedAccessType ? + isGenericTupleType(objectType) && !indexTypeLessThan(indexType, objectType.target.fixedLength) : + isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)))) { + if (objectType.flags & ts.TypeFlags.AnyOrUnknown) { + return objectType; } - else { - return getTypeReferenceType(node, resolvedSymbol); // getTypeReferenceType doesn't handle aliases - it must get the resolved symbol + // Defer the operation by creating an indexed access type. + const persistentAccessFlags = accessFlags & ts.AccessFlags.Persistent; + const id = objectType.id + "," + indexType.id + "," + persistentAccessFlags + getAliasId(aliasSymbol, aliasTypeArguments); + let type = indexedAccessTypes.get(id); + if (!type) { + indexedAccessTypes.set(id, type = createIndexedAccessType(objectType, indexType, persistentAccessFlags, aliasSymbol, aliasTypeArguments)); } - } - function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: ts.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; + 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 = getReducedApparentType(objectType); + if (indexType.flags & ts.TypeFlags.Union && !(indexType.flags & ts.TypeFlags.Boolean)) { + const propTypes: ts.Type[] = []; + let wasMissingProp = false; + for (const t of (indexType as ts.UnionType).types) { + const propType = getPropertyTypeForIndexType(objectType, apparentObjectType, t, indexType, accessNode, accessFlags | (wasMissingProp ? ts.AccessFlags.SuppressNoImplicitAnyError : 0)); + 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 { - let type = createObjectType(ts.ObjectFlags.Anonymous, node.symbol); - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); - if (ts.isJSDocTypeLiteral(node) && node.isArrayType) { - type = createArrayType(type); - } - links.resolvedType = type; + // Otherwise we set a flag and return at the end of the loop so we still mark all errors + wasMissingProp = true; } } - return links.resolvedType; - } - - function getAliasSymbolForTypeNode(node: ts.Node) { - let host = node.parent; - while (ts.isParenthesizedTypeNode(host) || ts.isJSDocTypeExpression(host) || ts.isTypeOperatorNode(host) && host.operator === ts.SyntaxKind.ReadonlyKeyword) { - host = host.parent; + if (wasMissingProp) { + return undefined; } - return ts.isTypeAlias(host) ? getSymbolOfNode(host) : undefined; + return accessFlags & ts.AccessFlags.Writing + ? getIntersectionType(propTypes, aliasSymbol, aliasTypeArguments) + : getUnionType(propTypes, ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } + return getPropertyTypeForIndexType(objectType, apparentObjectType, indexType, indexType, accessNode, accessFlags | ts.AccessFlags.CacheSymbol | ts.AccessFlags.ReportDeprecated); + } - function getTypeArgumentsForAliasSymbol(symbol: ts.Symbol | undefined) { - return symbol ? getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) : undefined; - } + function getTypeFromIndexedAccessTypeNode(node: ts.IndexedAccessTypeNode) { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const objectType = getTypeFromTypeNode(node.objectType); + const indexType = getTypeFromTypeNode(node.indexType); + const potentialAlias = getAliasSymbolForTypeNode(node); + const resolved = getIndexedAccessType(objectType, indexType, ts.AccessFlags.None, node, potentialAlias, getTypeArgumentsForAliasSymbol(potentialAlias)); + links.resolvedType = resolved.flags & ts.TypeFlags.IndexedAccess && + (resolved as ts.IndexedAccessType).objectType === objectType && + (resolved as ts.IndexedAccessType).indexType === indexType ? + getConditionalFlowTypeOfType(resolved, node) : resolved; + } + return links.resolvedType; + } - function isNonGenericObjectType(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Object) && !isGenericMappedType(type); - } + function getTypeFromMappedTypeNode(node: ts.MappedTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const type = createObjectType(ts.ObjectFlags.Mapped, node.symbol) as ts.MappedType; + 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 isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: ts.Type) { - return isEmptyObjectType(type) || !!(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.BooleanLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.EnumLike | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Index)); + function getActualTypeVariable(type: ts.Type): ts.Type { + if (type.flags & ts.TypeFlags.Substitution) { + return (type as ts.SubstitutionType).baseType; + } + if (type.flags & ts.TypeFlags.IndexedAccess && ((type as ts.IndexedAccessType).objectType.flags & ts.TypeFlags.Substitution || + (type as ts.IndexedAccessType).indexType.flags & ts.TypeFlags.Substitution)) { + return getIndexedAccessType(getActualTypeVariable((type as ts.IndexedAccessType).objectType), getActualTypeVariable((type as ts.IndexedAccessType).indexType)); } + return type; + } - function tryMergeUnionOfObjectTypeAndEmptyObject(type: ts.Type, readonly: boolean): ts.Type { - if (!(type.flags & ts.TypeFlags.Union)) { - return type; - } - if (ts.every((type as ts.UnionType).types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) { - return ts.find((type as ts.UnionType).types, isEmptyObjectType) || emptyObjectType; - } - const firstType = ts.find((type as ts.UnionType).types, t => !isEmptyObjectTypeOrSpreadsIntoEmptyObject(t)); - if (!firstType) { - return type; + function maybeCloneTypeParameter(p: ts.TypeParameter) { + const constraint = getConstraintOfTypeParameter(p); + return constraint && (isGenericObjectType(constraint) || isGenericIndexType(constraint)) ? cloneTypeParameter(p) : p; + } + + function isTypicalNondistributiveConditional(root: ts.ConditionalRoot) { + return !root.isDistributive && isSingletonTupleType(root.node.checkType) && isSingletonTupleType(root.node.extendsType); + } + + function isSingletonTupleType(node: ts.TypeNode) { + return ts.isTupleTypeNode(node) && + ts.length(node.elements) === 1 && + !ts.isOptionalTypeNode(node.elements[0]) && + !ts.isRestTypeNode(node.elements[0]) && + !(ts.isNamedTupleMember(node.elements[0]) && (node.elements[0].questionToken || node.elements[0].dotDotDotToken)); + } + + /** + * We syntactually check for common nondistributive conditional shapes and unwrap them into + * the intended comparison - we do this so we can check if the unwrapped types are generic or + * not and appropriately defer condition calculation + */ + function unwrapNondistributiveConditionalTuple(root: ts.ConditionalRoot, type: ts.Type) { + return isTypicalNondistributiveConditional(root) && isTupleType(type) ? getTypeArguments(type)[0] : type; + } + + function getConditionalType(root: ts.ConditionalRoot, mapper: ts.TypeMapper | undefined, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + let result; + let extraTypes: ts.Type[] | undefined; + let tailCount = 0; + // We loop here for an immediately nested conditional type in the false position, effectively treating + // types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for + // purposes of resolution. We also loop here when resolution of a conditional type ends in resolution of + // another (or, through recursion, possibly the same) conditional type. In the potentially tail-recursive + // cases we increment the tail recursion counter and stop after 1000 iterations. + while (true) { + if (tailCount === 1000) { + error(currentNode, ts.Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); + result = errorType; + break; } - const secondType = ts.find((type as ts.UnionType).types, t => t !== firstType && !isEmptyObjectTypeOrSpreadsIntoEmptyObject(t)); - if (secondType) { - return type; + const isUnwrapped = isTypicalNondistributiveConditional(root); + const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, getActualTypeVariable(root.checkType)), mapper); + const checkTypeInstantiable = isGenericType(checkType); + const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper); + if (checkType === wildcardType || extendsType === wildcardType) { + return wildcardType; } - return getAnonymousPartialType(firstType); - - 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 = ts.createSymbolTable(); - for (const prop of getPropertiesOfType(type)) { - if (ts.getDeclarationModifierFlagsFromSymbol(prop) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { - // do nothing, skip privates + let combinedMapper: ts.TypeMapper | undefined; + if (root.inferTypeParameters) { + // When we're looking at making an inference for an infer type, when we get its constraint, it'll automagically be + // instantiated with the context, so it doesn't need the mapper for the inference contex - however the constraint + // may refer to another _root_, _uncloned_ `infer` type parameter [1], or to something mapped by `mapper` [2]. + // [1] Eg, if we have `Foo` and `Foo` - `B` is constrained to `T`, which, in turn, has been instantiated + // as `number` + // Conversely, if we have `Foo`, `B` is still constrained to `T` and `T` is instantiated as `A` + // [2] Eg, if we have `Foo` and `Foo` where `Q` is mapped by `mapper` into `number` - `B` is constrained to `T` + // which is in turn instantiated as `Q`, which is in turn instantiated as `number`. + // So we need to: + // * Clone the type parameters so their constraints can be instantiated in the context of `mapper` (otherwise theyd only get inference context information) + // * Set the clones to both map the conditional's enclosing `mapper` and the original params + // * instantiate the extends type with the clones + // * incorporate all of the component mappers into the combined mapper for the true and false members + // This means we have three mappers that need applying: + // * The original `mapper` used to create this conditional + // * The mapper that maps the old root type parameter to the clone (`freshMapper`) + // * The mapper that maps the clone to its inference result (`context.mapper`) + const freshParams = ts.sameMap(root.inferTypeParameters, maybeCloneTypeParameter); + const freshMapper = freshParams !== root.inferTypeParameters ? createTypeMapper(root.inferTypeParameters, freshParams) : undefined; + const context = createInferenceContext(freshParams, /*signature*/ undefined, ts.InferenceFlags.None); + if (freshMapper) { + const freshCombinedMapper = combineTypeMappers(mapper, freshMapper); + for (const p of freshParams) { + if (root.inferTypeParameters.indexOf(p) === -1) { + p.mapper = freshCombinedMapper; + } + } + } + // 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 || !ts.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, instantiateType(extendsType, freshMapper), ts.InferencePriority.NoConstraints | ts.InferencePriority.AlwaysStrict); + } + const innerMapper = combineTypeMappers(freshMapper, context.mapper); + // It's possible for 'infer T' type paramteters to be given uninstantiated constraints when the + // those type parameters are used in type references (see getInferredTypeParameterConstraint). For + // that reason we need context.mapper to be first in the combined mapper. See #42636 for examples. + combinedMapper = mapper ? combineTypeMappers(innerMapper, mapper) : innerMapper; + } + // Instantiate the extends type including inferences for 'infer T' type parameters + const inferredExtendsType = combinedMapper ? instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), combinedMapper) : extendsType; + // We attempt to resolve the conditional type only when the check and extends types are non-generic + if (!checkTypeInstantiable && !isGenericType(inferredExtendsType)) { + // 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 (!(inferredExtendsType.flags & ts.TypeFlags.AnyOrUnknown) && ((checkType.flags & ts.TypeFlags.Any && !isUnwrapped) || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) { + // Return union of trueType and falseType for 'any' since it matches anything + if (checkType.flags & ts.TypeFlags.Any && !isUnwrapped) { + (extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper)); + } + // If falseType is an immediately nested conditional type that isn't distributive or has an + // identical checkType, switch to that type and loop. + const falseType = getTypeFromTypeNode(root.node.falseType); + if (falseType.flags & ts.TypeFlags.Conditional) { + const newRoot = (falseType as ts.ConditionalType).root; + if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) { + root = newRoot; + continue; + } + if (canTailRecurse(falseType, mapper)) { + continue; + } } - else if (isSpreadableProperty(prop)) { - const isSetonlyAccessor = prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); - const flags = ts.SymbolFlags.Property | ts.SymbolFlags.Optional; - const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? ts.CheckFlags.Readonly : 0)); - result.type = isSetonlyAccessor ? undefinedType : addOptionality(getTypeOfSymbol(prop), /*isProperty*/ true); - result.declarations = prop.declarations; - result.nameType = getSymbolLinks(prop).nameType; - result.syntheticOrigin = prop; - members.set(prop.escapedName, result); + result = instantiateType(falseType, mapper); + break; + } + // 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 (inferredExtendsType.flags & ts.TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) { + const trueType = getTypeFromTypeNode(root.node.trueType); + const trueMapper = combinedMapper || mapper; + if (canTailRecurse(trueType, trueMapper)) { + continue; } + result = instantiateType(trueType, trueMapper); + break; } - const spread = createAnonymousType(type.symbol, members, ts.emptyArray, ts.emptyArray, getIndexInfosOfType(type)); - spread.objectFlags |= ts.ObjectFlags.ObjectLiteral | ts.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: ts.ObjectFlags, readonly: boolean): ts.Type { - if (left.flags & ts.TypeFlags.Any || right.flags & ts.TypeFlags.Any) { - return anyType; } - if (left.flags & ts.TypeFlags.Unknown || right.flags & ts.TypeFlags.Unknown) { - return unknownType; - } - if (left.flags & ts.TypeFlags.Never) { - return right; - } - if (right.flags & ts.TypeFlags.Never) { - return left; - } - left = tryMergeUnionOfObjectTypeAndEmptyObject(left, readonly); - if (left.flags & ts.TypeFlags.Union) { - return checkCrossProductUnion([left, right]) - ? mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly)) - : errorType; - } - right = tryMergeUnionOfObjectTypeAndEmptyObject(right, readonly); - if (right.flags & ts.TypeFlags.Union) { - return checkCrossProductUnion([left, right]) - ? mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly)) - : errorType; - } - if (right.flags & (ts.TypeFlags.BooleanLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.EnumLike | ts.TypeFlags.NonPrimitive | ts.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 & ts.TypeFlags.Intersection) { - const types = (left as ts.IntersectionType).types; - const lastLeft = types[types.length - 1]; - if (isNonGenericObjectType(lastLeft) && isNonGenericObjectType(right)) { - return getIntersectionType(ts.concatenate(types.slice(0, types.length - 1), [getSpreadType(lastLeft, right, symbol, objectFlags, readonly)])); + // Return a deferred type for a check that is neither definitely true nor definitely false + result = createType(ts.TypeFlags.Conditional) as ts.ConditionalType; + result.root = root; + result.checkType = instantiateType(root.checkType, mapper); + result.extendsType = instantiateType(root.extendsType, mapper); + result.mapper = mapper; + result.combinedMapper = combinedMapper; + result.aliasSymbol = aliasSymbol || root.aliasSymbol; + result.aliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217 + break; + } + return extraTypes ? getUnionType(ts.append(extraTypes, result)) : result; + // We tail-recurse for generic conditional types that (a) have not already been evaluated and cached, and + // (b) are non distributive, have a check type that is unaffected by instantiation, or have a non-union check + // type. Note that recursion is possible only through aliased conditional types, so we only increment the tail + // recursion counter for those. + function canTailRecurse(newType: ts.Type, newMapper: ts.TypeMapper | undefined) { + if (newType.flags & ts.TypeFlags.Conditional && newMapper) { + const newRoot = (newType as ts.ConditionalType).root; + if (newRoot.outerTypeParameters) { + const typeParamMapper = combineTypeMappers((newType as ts.ConditionalType).mapper, newMapper); + const typeArguments = ts.map(newRoot.outerTypeParameters, t => getMappedType(t, typeParamMapper)); + const newRootMapper = createTypeMapper(newRoot.outerTypeParameters, typeArguments); + const newCheckType = newRoot.isDistributive ? getMappedType(newRoot.checkType, newRootMapper) : undefined; + if (!newCheckType || newCheckType === newRoot.checkType || !(newCheckType.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never))) { + root = newRoot; + mapper = newRootMapper; + aliasSymbol = undefined; + aliasTypeArguments = undefined; + if (newRoot.aliasSymbol) { + tailCount++; + } + return true; } } - return getIntersectionType([left, right]); } + return false; + } + } - const members = ts.createSymbolTable(); - const skippedPrivateMembers = new ts.Set(); - const indexInfos = left === emptyObjectType ? getIndexInfosOfType(right) : getUnionIndexInfos([left, right]); + function getTrueTypeFromConditionalType(type: ts.ConditionalType) { + return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.mapper)); + } - for (const rightProp of getPropertiesOfType(right)) { - if (ts.getDeclarationModifierFlagsFromSymbol(rightProp) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { - skippedPrivateMembers.add(rightProp.escapedName); - } - else if (isSpreadableProperty(rightProp)) { - members.set(rightProp.escapedName, getSpreadSymbol(rightProp, readonly)); + function getFalseTypeFromConditionalType(type: ts.ConditionalType) { + return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(getTypeFromTypeNode(type.root.node.falseType), type.mapper)); + } + + function getInferredTrueTypeFromConditionalType(type: ts.ConditionalType) { + return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.combinedMapper) : getTrueTypeFromConditionalType(type)); + } + + function getInferTypeParameters(node: ts.ConditionalTypeNode): ts.TypeParameter[] | undefined { + let result: ts.TypeParameter[] | undefined; + if (node.locals) { + node.locals.forEach(symbol => { + if (symbol.flags & ts.SymbolFlags.TypeParameter) { + result = ts.append(result, getDeclaredTypeOfSymbol(symbol)); } + }); + } + return result; + } + + function isDistributionDependent(root: ts.ConditionalRoot) { + return root.isDistributive && (isTypeParameterPossiblyReferenced(root.checkType as ts.TypeParameter, root.node.trueType) || + isTypeParameterPossiblyReferenced(root.checkType as ts.TypeParameter, root.node.falseType)); + } + + function getTypeFromConditionalTypeNode(node: ts.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 : ts.filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node)); + const root: ts.ConditionalRoot = { + node, + checkType, + extendsType: getTypeFromTypeNode(node.extendsType), + isDistributive: !!(checkType.flags & ts.TypeFlags.TypeParameter), + inferTypeParameters: getInferTypeParameters(node), + outerTypeParameters, + instantiations: undefined, + aliasSymbol, + aliasTypeArguments + }; + links.resolvedType = getConditionalType(root, /*mapper*/ undefined); + if (outerTypeParameters) { + root.instantiations = new ts.Map(); + root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType); } + } + return links.resolvedType; + } - 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 & ts.SymbolFlags.Optional) { - const declarations = ts.concatenate(leftProp.declarations, rightProp.declarations); - const flags = ts.SymbolFlags.Property | (leftProp.flags & ts.SymbolFlags.Optional); - const result = createSymbol(flags, leftProp.escapedName); - result.type = getUnionType([getTypeOfSymbol(leftProp), removeMissingOrUndefinedType(rightType)], ts.UnionReduction.Subtype); - result.leftSpread = leftProp; - result.rightSpread = rightProp; - result.declarations = declarations; - result.nameType = getSymbolLinks(leftProp).nameType; - members.set(leftProp.escapedName, result); - } + function getTypeFromInferTypeNode(node: ts.InferTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node.typeParameter)); + } + return links.resolvedType; + } + + function getIdentifierChain(node: ts.EntityName): ts.Identifier[] { + if (ts.isIdentifier(node)) { + return [node]; + } + else { + return ts.append(getIdentifierChain(node.left), node.right); + } + } + + function getTypeFromImportTypeNode(node: ts.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, ts.Diagnostics.Type_arguments_cannot_be_used_here); + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = errorType; + } + if (!ts.isLiteralImportTypeNode(node)) { + error(node.argument, ts.Diagnostics.String_literal_expected); + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = errorType; + } + const targetMeaning = node.isTypeOf ? ts.SymbolFlags.Value : node.flags & ts.NodeFlags.JSDoc ? ts.SymbolFlags.Value | ts.SymbolFlags.Type : ts.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 (!ts.nodeIsMissing(node.qualifier)) { + const nameStack: ts.Identifier[] = getIdentifierChain(node.qualifier!); + let currentNamespace = moduleSymbol; + let current: ts.Identifier | undefined; + while (current = nameStack.shift()) { + const meaning = nameStack.length ? ts.SymbolFlags.Namespace : targetMeaning; + // typeof a.b.c is normally resolved using `checkExpression` which in turn defers to `checkQualifiedName` + // That, in turn, ultimately uses `getPropertyOfType` on the type of the symbol, which differs slightly from + // the `exports` lookup process that only looks up namespace members which is used for most type references + const mergedResolvedSymbol = getMergedSymbol(resolveSymbol(currentNamespace)); + const next = node.isTypeOf + ? getPropertyOfType(getTypeOfSymbol(mergedResolvedSymbol), current.escapedText, /*skipObjectFunctionPropertyAugment*/ false, /*includeTypeOnlyMembers*/ true) + : getSymbol(getExportsOfSymbol(mergedResolvedSymbol), current.escapedText, meaning); + if (!next) { + error(current, ts.Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(currentNamespace), ts.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 { - members.set(leftProp.escapedName, getSpreadSymbol(leftProp, readonly)); + const errorMessage = targetMeaning === ts.SymbolFlags.Value + ? ts.Diagnostics.Module_0_does_not_refer_to_a_value_but_is_used_as_a_value_here + : ts.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; } } - - const spread = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.sameMap(indexInfos, info => getIndexInfoWithReadonly(info, readonly))); - spread.objectFlags |= ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral | ts.ObjectFlags.ContainsSpread | objectFlags; - return spread; } + return links.resolvedType; + } - /** We approximate own properties as non-methods plus methods that are inside the object literal */ - function isSpreadableProperty(prop: ts.Symbol): boolean { - return !ts.some(prop.declarations, ts.isPrivateIdentifierClassElementDeclaration) && - (!(prop.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) || - !prop.declarations?.some(decl => ts.isClassLike(decl.parent))); + function resolveImportSymbolType(node: ts.ImportTypeNode, links: ts.NodeLinks, symbol: ts.Symbol, meaning: ts.SymbolFlags) { + const resolvedSymbol = resolveSymbol(symbol); + links.resolvedSymbol = resolvedSymbol; + if (meaning === ts.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 getSpreadSymbol(prop: ts.Symbol, readonly: boolean) { - const isSetonlyAccessor = prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); - if (!isSetonlyAccessor && readonly === isReadonlySymbol(prop)) { - return prop; + function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: ts.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; + } + else { + let type = createObjectType(ts.ObjectFlags.Anonymous, node.symbol); + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); + if (ts.isJSDocTypeLiteral(node) && node.isArrayType) { + type = createArrayType(type); + } + links.resolvedType = type; } - const flags = ts.SymbolFlags.Property | (prop.flags & ts.SymbolFlags.Optional); - const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? ts.CheckFlags.Readonly : 0)); - result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); - result.declarations = prop.declarations; - result.nameType = getSymbolLinks(prop).nameType; - result.syntheticOrigin = prop; - return result; } + return links.resolvedType; + } - function getIndexInfoWithReadonly(info: ts.IndexInfo, readonly: boolean) { - return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration) : info; + function getAliasSymbolForTypeNode(node: ts.Node) { + let host = node.parent; + while (ts.isParenthesizedTypeNode(host) || ts.isJSDocTypeExpression(host) || ts.isTypeOperatorNode(host) && host.operator === ts.SyntaxKind.ReadonlyKeyword) { + host = host.parent; } + return ts.isTypeAlias(host) ? getSymbolOfNode(host) : undefined; + } + + function getTypeArgumentsForAliasSymbol(symbol: ts.Symbol | undefined) { + return symbol ? getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) : undefined; + } - function createLiteralType(flags: ts.TypeFlags, value: string | number | ts.PseudoBigInt, symbol?: ts.Symbol, regularType?: ts.LiteralType) { - const type = createType(flags) as ts.LiteralType; - type.symbol = symbol!; - type.value = value; - type.regularType = regularType || type; + function isNonGenericObjectType(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Object) && !isGenericMappedType(type); + } + + function isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: ts.Type) { + return isEmptyObjectType(type) || !!(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.BooleanLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.EnumLike | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Index)); + } + + function tryMergeUnionOfObjectTypeAndEmptyObject(type: ts.Type, readonly: boolean): ts.Type { + if (!(type.flags & ts.TypeFlags.Union)) { return type; } - - function getFreshTypeOfLiteralType(type: ts.Type): ts.Type { - if (type.flags & ts.TypeFlags.Literal) { - if (!(type as ts.LiteralType).freshType) { - const freshType = createLiteralType(type.flags, (type as ts.LiteralType).value, (type as ts.LiteralType).symbol, type as ts.LiteralType); - freshType.freshType = freshType; - (type as ts.LiteralType).freshType = freshType; - } - return (type as ts.LiteralType).freshType; - } + if (ts.every((type as ts.UnionType).types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) { + return ts.find((type as ts.UnionType).types, isEmptyObjectType) || emptyObjectType; + } + const firstType = ts.find((type as ts.UnionType).types, t => !isEmptyObjectTypeOrSpreadsIntoEmptyObject(t)); + if (!firstType) { return type; } - - function getRegularTypeOfLiteralType(type: ts.Type): ts.Type { - return type.flags & ts.TypeFlags.Literal ? (type as ts.LiteralType).regularType : - type.flags & ts.TypeFlags.Union ? ((type as ts.UnionType).regularType || ((type as ts.UnionType).regularType = mapType(type, getRegularTypeOfLiteralType) as ts.UnionType)) : - type; + const secondType = ts.find((type as ts.UnionType).types, t => t !== firstType && !isEmptyObjectTypeOrSpreadsIntoEmptyObject(t)); + if (secondType) { + return type; } + return getAnonymousPartialType(firstType); - function isFreshLiteralType(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Literal) && (type as ts.LiteralType).freshType === type; + 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 = ts.createSymbolTable(); + for (const prop of getPropertiesOfType(type)) { + if (ts.getDeclarationModifierFlagsFromSymbol(prop) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { + // do nothing, skip privates + } + else if (isSpreadableProperty(prop)) { + const isSetonlyAccessor = prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); + const flags = ts.SymbolFlags.Property | ts.SymbolFlags.Optional; + const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? ts.CheckFlags.Readonly : 0)); + result.type = isSetonlyAccessor ? undefinedType : addOptionality(getTypeOfSymbol(prop), /*isProperty*/ true); + result.declarations = prop.declarations; + result.nameType = getSymbolLinks(prop).nameType; + result.syntheticOrigin = prop; + members.set(prop.escapedName, result); + } + } + const spread = createAnonymousType(type.symbol, members, ts.emptyArray, ts.emptyArray, getIndexInfosOfType(type)); + spread.objectFlags |= ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; + return spread; } + } - function getStringLiteralType(value: string): ts.StringLiteralType { - let type; - return stringLiteralTypes.get(value) || - (stringLiteralTypes.set(value, type = createLiteralType(ts.TypeFlags.StringLiteral, value) as ts.StringLiteralType), type); + /** + * 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: ts.ObjectFlags, readonly: boolean): ts.Type { + if (left.flags & ts.TypeFlags.Any || right.flags & ts.TypeFlags.Any) { + return anyType; } - - function getNumberLiteralType(value: number): ts.NumberLiteralType { - let type; - return numberLiteralTypes.get(value) || - (numberLiteralTypes.set(value, type = createLiteralType(ts.TypeFlags.NumberLiteral, value) as ts.NumberLiteralType), type); + if (left.flags & ts.TypeFlags.Unknown || right.flags & ts.TypeFlags.Unknown) { + return unknownType; } - - function getBigIntLiteralType(value: ts.PseudoBigInt): ts.BigIntLiteralType { - let type; - const key = ts.pseudoBigIntToString(value); - return bigIntLiteralTypes.get(key) || - (bigIntLiteralTypes.set(key, type = createLiteralType(ts.TypeFlags.BigIntLiteral, value) as ts.BigIntLiteralType), type); + if (left.flags & ts.TypeFlags.Never) { + return right; } - - function getEnumLiteralType(value: string | number, enumId: number, symbol: ts.Symbol): ts.LiteralType { - let type; - const qualifier = typeof value === "string" ? "@" : "#"; - const key = enumId + qualifier + value; - const flags = ts.TypeFlags.EnumLiteral | (typeof value === "string" ? ts.TypeFlags.StringLiteral : ts.TypeFlags.NumberLiteral); - return enumLiteralTypes.get(key) || - (enumLiteralTypes.set(key, type = createLiteralType(flags, value, symbol)), type); + if (right.flags & ts.TypeFlags.Never) { + return left; } - - function getTypeFromLiteralTypeNode(node: ts.LiteralTypeNode): ts.Type { - if (node.literal.kind === ts.SyntaxKind.NullKeyword) { - return nullType; - } - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getRegularTypeOfLiteralType(checkExpression(node.literal)); - } - return links.resolvedType; + left = tryMergeUnionOfObjectTypeAndEmptyObject(left, readonly); + if (left.flags & ts.TypeFlags.Union) { + return checkCrossProductUnion([left, right]) + ? mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly)) + : errorType; } - - function createUniqueESSymbolType(symbol: ts.Symbol) { - const type = createType(ts.TypeFlags.UniqueESSymbol) as ts.UniqueESSymbolType; - type.symbol = symbol; - type.escapedName = `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as ts.__String; - return type; + right = tryMergeUnionOfObjectTypeAndEmptyObject(right, readonly); + if (right.flags & ts.TypeFlags.Union) { + return checkCrossProductUnion([left, right]) + ? mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly)) + : errorType; + } + if (right.flags & (ts.TypeFlags.BooleanLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.EnumLike | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Index)) { + return left; } - function getESSymbolLikeTypeForNode(node: ts.Node) { - if (ts.isValidESSymbolDeclaration(node)) { - const symbol = ts.isCommonJsExportPropertyAssignment(node) ? getSymbolOfNode((node as ts.BinaryExpression).left) : getSymbolOfNode(node); - if (symbol) { - const links = getSymbolLinks(symbol); - return links.uniqueESSymbolType || (links.uniqueESSymbolType = createUniqueESSymbolType(symbol)); + 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 & ts.TypeFlags.Intersection) { + const types = (left as ts.IntersectionType).types; + const lastLeft = types[types.length - 1]; + if (isNonGenericObjectType(lastLeft) && isNonGenericObjectType(right)) { + return getIntersectionType(ts.concatenate(types.slice(0, types.length - 1), [getSpreadType(lastLeft, right, symbol, objectFlags, readonly)])); } } - return esSymbolType; + return getIntersectionType([left, right]); } - function getThisType(node: ts.Node): ts.Type { - const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); - const parent = container && container.parent; - if (parent && (ts.isClassLike(parent) || parent.kind === ts.SyntaxKind.InterfaceDeclaration)) { - if (!ts.isStatic(container) && - (!ts.isConstructorDeclaration(container) || ts.isNodeDescendantOf(node, container.body))) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration)).thisType!; - } - } + const members = ts.createSymbolTable(); + const skippedPrivateMembers = new ts.Set(); + const indexInfos = left === emptyObjectType ? getIndexInfosOfType(right) : getUnionIndexInfos([left, right]); - // inside x.prototype = { ... } - if (parent && ts.isObjectLiteralExpression(parent) && ts.isBinaryExpression(parent.parent) && ts.getAssignmentDeclarationKind(parent.parent) === ts.AssignmentDeclarationKind.Prototype) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent.parent.left)!.parent!).thisType!; + for (const rightProp of getPropertiesOfType(right)) { + if (ts.getDeclarationModifierFlagsFromSymbol(rightProp) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { + skippedPrivateMembers.add(rightProp.escapedName); } - // /** @return {this} */ - // x.prototype.m = function() { ... } - const host = node.flags & ts.NodeFlags.JSDoc ? ts.getHostSignatureFromJSDoc(node) : undefined; - if (host && ts.isFunctionExpression(host) && ts.isBinaryExpression(host.parent) && ts.getAssignmentDeclarationKind(host.parent) === ts.AssignmentDeclarationKind.PrototypeProperty) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(host.parent.left)!.parent!).thisType!; + else if (isSpreadableProperty(rightProp)) { + members.set(rightProp.escapedName, getSpreadSymbol(rightProp, readonly)); } - // inside constructor function C() { ... } - if (isJSConstructor(container) && ts.isNodeDescendantOf(node, container.body)) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(container)).thisType!; - } - error(node, ts.Diagnostics.A_this_type_is_available_only_in_a_non_static_member_of_a_class_or_interface); - return errorType; } - function getTypeFromThisTypeNode(node: ts.ThisExpression | ts.ThisTypeNode): ts.Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getThisType(node); + for (const leftProp of getPropertiesOfType(left)) { + if (skippedPrivateMembers.has(leftProp.escapedName) || !isSpreadableProperty(leftProp)) { + continue; } - return links.resolvedType; - } - - function getTypeFromRestTypeNode(node: ts.RestTypeNode | ts.NamedTupleMember) { - return getTypeFromTypeNode(getArrayElementTypeNode(node.type) || node.type); - } - - function getArrayElementTypeNode(node: ts.TypeNode): ts.TypeNode | undefined { - switch (node.kind) { - case ts.SyntaxKind.ParenthesizedType: - return getArrayElementTypeNode((node as ts.ParenthesizedTypeNode).type); - case ts.SyntaxKind.TupleType: - if ((node as ts.TupleTypeNode).elements.length === 1) { - node = (node as ts.TupleTypeNode).elements[0]; - if (node.kind === ts.SyntaxKind.RestType || node.kind === ts.SyntaxKind.NamedTupleMember && (node as ts.NamedTupleMember).dotDotDotToken) { - return getArrayElementTypeNode((node as ts.RestTypeNode | ts.NamedTupleMember).type); - } - } - break; - case ts.SyntaxKind.ArrayType: - return (node as ts.ArrayTypeNode).elementType; + if (members.has(leftProp.escapedName)) { + const rightProp = members.get(leftProp.escapedName)!; + const rightType = getTypeOfSymbol(rightProp); + if (rightProp.flags & ts.SymbolFlags.Optional) { + const declarations = ts.concatenate(leftProp.declarations, rightProp.declarations); + const flags = ts.SymbolFlags.Property | (leftProp.flags & ts.SymbolFlags.Optional); + const result = createSymbol(flags, leftProp.escapedName); + result.type = getUnionType([getTypeOfSymbol(leftProp), removeMissingOrUndefinedType(rightType)], ts.UnionReduction.Subtype); + result.leftSpread = leftProp; + result.rightSpread = rightProp; + result.declarations = declarations; + result.nameType = getSymbolLinks(leftProp).nameType; + members.set(leftProp.escapedName, result); + } + } + else { + members.set(leftProp.escapedName, getSpreadSymbol(leftProp, readonly)); } - return undefined; } - function getTypeFromNamedTupleTypeNode(node: ts.NamedTupleMember): ts.Type { - const links = getNodeLinks(node); - return links.resolvedType || (links.resolvedType = - node.dotDotDotToken ? getTypeFromRestTypeNode(node) : - addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true, !!node.questionToken)); - } + const spread = createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.sameMap(indexInfos, info => getIndexInfoWithReadonly(info, readonly))); + spread.objectFlags |= ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral | ts.ObjectFlags.ContainsSpread | objectFlags; + return spread; + } - function getTypeFromTypeNode(node: ts.TypeNode): ts.Type { - return getConditionalFlowTypeOfType(getTypeFromTypeNodeWorker(node), node); - } + /** We approximate own properties as non-methods plus methods that are inside the object literal */ + function isSpreadableProperty(prop: ts.Symbol): boolean { + return !ts.some(prop.declarations, ts.isPrivateIdentifierClassElementDeclaration) && + (!(prop.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) || + !prop.declarations?.some(decl => ts.isClassLike(decl.parent))); + } - function getTypeFromTypeNodeWorker(node: ts.TypeNode): ts.Type { - switch (node.kind) { - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.JSDocAllType: - case ts.SyntaxKind.JSDocUnknownType: - return anyType; - case ts.SyntaxKind.UnknownKeyword: - return unknownType; - case ts.SyntaxKind.StringKeyword: - return stringType; - case ts.SyntaxKind.NumberKeyword: - return numberType; - case ts.SyntaxKind.BigIntKeyword: - return bigintType; - case ts.SyntaxKind.BooleanKeyword: - return booleanType; - case ts.SyntaxKind.SymbolKeyword: - return esSymbolType; - case ts.SyntaxKind.VoidKeyword: - return voidType; - case ts.SyntaxKind.UndefinedKeyword: - return undefinedType; - case ts.SyntaxKind.NullKeyword as ts.TypeNodeSyntaxKind: - // TODO(rbuckton): `NullKeyword` is no longer a `TypeNode`, but we defensively allow it here because of incorrect casts in the Language Service. - return nullType; - case ts.SyntaxKind.NeverKeyword: - return neverType; - case ts.SyntaxKind.ObjectKeyword: - return node.flags & ts.NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType; - case ts.SyntaxKind.IntrinsicKeyword: - return intrinsicMarkerType; - case ts.SyntaxKind.ThisType: - case ts.SyntaxKind.ThisKeyword as ts.TypeNodeSyntaxKind: - // TODO(rbuckton): `ThisKeyword` is no longer a `TypeNode`, but we defensively allow it here because of incorrect casts in the Language Service and because of `isPartOfTypeNode`. - return getTypeFromThisTypeNode(node as ts.ThisExpression | ts.ThisTypeNode); - case ts.SyntaxKind.LiteralType: - return getTypeFromLiteralTypeNode(node as ts.LiteralTypeNode); - case ts.SyntaxKind.TypeReference: - return getTypeFromTypeReference(node as ts.TypeReferenceNode); - case ts.SyntaxKind.TypePredicate: - return (node as ts.TypePredicateNode).assertsModifier ? voidType : booleanType; - case ts.SyntaxKind.ExpressionWithTypeArguments: - return getTypeFromTypeReference(node as ts.ExpressionWithTypeArguments); - case ts.SyntaxKind.TypeQuery: - return getTypeFromTypeQueryNode(node as ts.TypeQueryNode); - case ts.SyntaxKind.ArrayType: - case ts.SyntaxKind.TupleType: - return getTypeFromArrayOrTupleTypeNode(node as ts.ArrayTypeNode | ts.TupleTypeNode); - case ts.SyntaxKind.OptionalType: - return getTypeFromOptionalTypeNode(node as ts.OptionalTypeNode); - case ts.SyntaxKind.UnionType: - return getTypeFromUnionTypeNode(node as ts.UnionTypeNode); - case ts.SyntaxKind.IntersectionType: - return getTypeFromIntersectionTypeNode(node as ts.IntersectionTypeNode); - case ts.SyntaxKind.JSDocNullableType: - return getTypeFromJSDocNullableTypeNode(node as ts.JSDocNullableType); - case ts.SyntaxKind.JSDocOptionalType: - return addOptionality(getTypeFromTypeNode((node as ts.JSDocOptionalType).type)); - case ts.SyntaxKind.NamedTupleMember: - return getTypeFromNamedTupleTypeNode(node as ts.NamedTupleMember); - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.JSDocNonNullableType: - case ts.SyntaxKind.JSDocTypeExpression: - return getTypeFromTypeNode((node as ts.ParenthesizedTypeNode | ts.JSDocTypeReferencingNode | ts.JSDocTypeExpression | ts.NamedTupleMember).type); - case ts.SyntaxKind.RestType: - return getTypeFromRestTypeNode(node as ts.RestTypeNode); - case ts.SyntaxKind.JSDocVariadicType: - return getTypeFromJSDocVariadicType(node as ts.JSDocVariadicType); - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.JSDocTypeLiteral: - case ts.SyntaxKind.JSDocFunctionType: - case ts.SyntaxKind.JSDocSignature: - return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); - case ts.SyntaxKind.TypeOperator: - return getTypeFromTypeOperatorNode(node as ts.TypeOperatorNode); - case ts.SyntaxKind.IndexedAccessType: - return getTypeFromIndexedAccessTypeNode(node as ts.IndexedAccessTypeNode); - case ts.SyntaxKind.MappedType: - return getTypeFromMappedTypeNode(node as ts.MappedTypeNode); - case ts.SyntaxKind.ConditionalType: - return getTypeFromConditionalTypeNode(node as ts.ConditionalTypeNode); - case ts.SyntaxKind.InferType: - return getTypeFromInferTypeNode(node as ts.InferTypeNode); - case ts.SyntaxKind.TemplateLiteralType: - return getTypeFromTemplateTypeNode(node as ts.TemplateLiteralTypeNode); - case ts.SyntaxKind.ImportType: - return getTypeFromImportTypeNode(node as ts.ImportTypeNode); - // This function assumes that an identifier, qualified name, or property access expression is a type expression - // Callers should first ensure this by calling `isPartOfTypeNode` - // TODO(rbuckton): These aren't valid TypeNodes, but we treat them as such because of `isPartOfTypeNode`, which returns `true` for things that aren't `TypeNode`s. - case ts.SyntaxKind.Identifier as ts.TypeNodeSyntaxKind: - case ts.SyntaxKind.QualifiedName as ts.TypeNodeSyntaxKind: - case ts.SyntaxKind.PropertyAccessExpression as ts.TypeNodeSyntaxKind: - const symbol = getSymbolAtLocation(node); - return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; - default: - return errorType; - } - } + function getSpreadSymbol(prop: ts.Symbol, readonly: boolean) { + const isSetonlyAccessor = prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); + if (!isSetonlyAccessor && readonly === isReadonlySymbol(prop)) { + return prop; + } + const flags = ts.SymbolFlags.Property | (prop.flags & ts.SymbolFlags.Optional); + const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? ts.CheckFlags.Readonly : 0)); + result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); + result.declarations = prop.declarations; + result.nameType = getSymbolLinks(prop).nameType; + result.syntheticOrigin = prop; + return result; + } - function instantiateList(items: readonly T[], mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.TypeMapper) => T): readonly T[]; - function instantiateList(items: readonly T[] | undefined, mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.TypeMapper) => T): readonly T[] | undefined; - function instantiateList(items: readonly T[] | undefined, mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.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; - } - } + function getIndexInfoWithReadonly(info: ts.IndexInfo, readonly: boolean) { + return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration) : info; + } + + function createLiteralType(flags: ts.TypeFlags, value: string | number | ts.PseudoBigInt, symbol?: ts.Symbol, regularType?: ts.LiteralType) { + const type = createType(flags) as ts.LiteralType; + type.symbol = symbol!; + type.value = value; + type.regularType = regularType || type; + return type; + } + + function getFreshTypeOfLiteralType(type: ts.Type): ts.Type { + if (type.flags & ts.TypeFlags.Literal) { + if (!(type as ts.LiteralType).freshType) { + const freshType = createLiteralType(type.flags, (type as ts.LiteralType).value, (type as ts.LiteralType).symbol, type as ts.LiteralType); + freshType.freshType = freshType; + (type as ts.LiteralType).freshType = freshType; } - return items; + return (type as ts.LiteralType).freshType; } + return type; + } - function instantiateTypes(types: readonly ts.Type[], mapper: ts.TypeMapper): readonly ts.Type[]; - function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: ts.TypeMapper): readonly ts.Type[] | undefined; - function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: ts.TypeMapper): readonly ts.Type[] | undefined { - return instantiateList(types, mapper, instantiateType); - } + function getRegularTypeOfLiteralType(type: ts.Type): ts.Type { + return type.flags & ts.TypeFlags.Literal ? (type as ts.LiteralType).regularType : + type.flags & ts.TypeFlags.Union ? ((type as ts.UnionType).regularType || ((type as ts.UnionType).regularType = mapType(type, getRegularTypeOfLiteralType) as ts.UnionType)) : + type; + } - function instantiateSignatures(signatures: readonly ts.Signature[], mapper: ts.TypeMapper): readonly ts.Signature[] { - return instantiateList(signatures, mapper, instantiateSignature); - } + function isFreshLiteralType(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Literal) && (type as ts.LiteralType).freshType === type; + } - function instantiateIndexInfos(indexInfos: readonly ts.IndexInfo[], mapper: ts.TypeMapper): readonly ts.IndexInfo[] { - return instantiateList(indexInfos, mapper, instantiateIndexInfo); - } + function getStringLiteralType(value: string): ts.StringLiteralType { + let type; + return stringLiteralTypes.get(value) || + (stringLiteralTypes.set(value, type = createLiteralType(ts.TypeFlags.StringLiteral, value) as ts.StringLiteralType), type); + } - function createTypeMapper(sources: readonly ts.TypeParameter[], targets: readonly ts.Type[] | undefined): ts.TypeMapper { - return sources.length === 1 ? makeUnaryTypeMapper(sources[0], targets ? targets[0] : anyType) : makeArrayTypeMapper(sources, targets); + function getNumberLiteralType(value: number): ts.NumberLiteralType { + let type; + return numberLiteralTypes.get(value) || + (numberLiteralTypes.set(value, type = createLiteralType(ts.TypeFlags.NumberLiteral, value) as ts.NumberLiteralType), type); + } + + function getBigIntLiteralType(value: ts.PseudoBigInt): ts.BigIntLiteralType { + let type; + const key = ts.pseudoBigIntToString(value); + return bigIntLiteralTypes.get(key) || + (bigIntLiteralTypes.set(key, type = createLiteralType(ts.TypeFlags.BigIntLiteral, value) as ts.BigIntLiteralType), type); + } + + function getEnumLiteralType(value: string | number, enumId: number, symbol: ts.Symbol): ts.LiteralType { + let type; + const qualifier = typeof value === "string" ? "@" : "#"; + const key = enumId + qualifier + value; + const flags = ts.TypeFlags.EnumLiteral | (typeof value === "string" ? ts.TypeFlags.StringLiteral : ts.TypeFlags.NumberLiteral); + return enumLiteralTypes.get(key) || + (enumLiteralTypes.set(key, type = createLiteralType(flags, value, symbol)), type); + } + + function getTypeFromLiteralTypeNode(node: ts.LiteralTypeNode): ts.Type { + if (node.literal.kind === ts.SyntaxKind.NullKeyword) { + return nullType; } + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getRegularTypeOfLiteralType(checkExpression(node.literal)); + } + return links.resolvedType; + } - function getMappedType(type: ts.Type, mapper: ts.TypeMapper): ts.Type { - switch (mapper.kind) { - case ts.TypeMapKind.Simple: - return type === mapper.source ? mapper.target : type; - case ts.TypeMapKind.Array: - const sources = mapper.sources; - const targets = mapper.targets; - for (let i = 0; i < sources.length; i++) { - if (type === sources[i]) { - return targets ? targets[i] : anyType; - } - } - return type; - case ts.TypeMapKind.Function: - return mapper.func(type); - case ts.TypeMapKind.Composite: - case ts.TypeMapKind.Merged: - const t1 = getMappedType(type, mapper.mapper1); - return t1 !== type && mapper.kind === ts.TypeMapKind.Composite ? instantiateType(t1, mapper.mapper2) : getMappedType(t1, mapper.mapper2); + function createUniqueESSymbolType(symbol: ts.Symbol) { + const type = createType(ts.TypeFlags.UniqueESSymbol) as ts.UniqueESSymbolType; + type.symbol = symbol; + type.escapedName = `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as ts.__String; + return type; + } + + function getESSymbolLikeTypeForNode(node: ts.Node) { + if (ts.isValidESSymbolDeclaration(node)) { + const symbol = ts.isCommonJsExportPropertyAssignment(node) ? getSymbolOfNode((node as ts.BinaryExpression).left) : getSymbolOfNode(node); + if (symbol) { + const links = getSymbolLinks(symbol); + return links.uniqueESSymbolType || (links.uniqueESSymbolType = createUniqueESSymbolType(symbol)); } } + return esSymbolType; + } - function makeUnaryTypeMapper(source: ts.Type, target: ts.Type): ts.TypeMapper { - return { kind: ts.TypeMapKind.Simple, source, target }; + function getThisType(node: ts.Node): ts.Type { + const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); + const parent = container && container.parent; + if (parent && (ts.isClassLike(parent) || parent.kind === ts.SyntaxKind.InterfaceDeclaration)) { + if (!ts.isStatic(container) && + (!ts.isConstructorDeclaration(container) || ts.isNodeDescendantOf(node, container.body))) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration)).thisType!; + } } - function makeArrayTypeMapper(sources: readonly ts.TypeParameter[], targets: readonly ts.Type[] | undefined): ts.TypeMapper { - return { kind: ts.TypeMapKind.Array, sources, targets }; + // inside x.prototype = { ... } + if (parent && ts.isObjectLiteralExpression(parent) && ts.isBinaryExpression(parent.parent) && ts.getAssignmentDeclarationKind(parent.parent) === ts.AssignmentDeclarationKind.Prototype) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent.parent.left)!.parent!).thisType!; } - - function makeFunctionTypeMapper(func: (t: ts.Type) => ts.Type): ts.TypeMapper { - return { kind: ts.TypeMapKind.Function, func }; + // /** @return {this} */ + // x.prototype.m = function() { ... } + const host = node.flags & ts.NodeFlags.JSDoc ? ts.getHostSignatureFromJSDoc(node) : undefined; + if (host && ts.isFunctionExpression(host) && ts.isBinaryExpression(host.parent) && ts.getAssignmentDeclarationKind(host.parent) === ts.AssignmentDeclarationKind.PrototypeProperty) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(host.parent.left)!.parent!).thisType!; } - - function makeCompositeTypeMapper(kind: ts.TypeMapKind.Composite | ts.TypeMapKind.Merged, mapper1: ts.TypeMapper, mapper2: ts.TypeMapper): ts.TypeMapper { - return { kind, mapper1, mapper2 }; + // inside constructor function C() { ... } + if (isJSConstructor(container) && ts.isNodeDescendantOf(node, container.body)) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(container)).thisType!; } + error(node, ts.Diagnostics.A_this_type_is_available_only_in_a_non_static_member_of_a_class_or_interface); + return errorType; + } - function createTypeEraser(sources: readonly ts.TypeParameter[]): ts.TypeMapper { - return createTypeMapper(sources, /*targets*/ undefined); + function getTypeFromThisTypeNode(node: ts.ThisExpression | ts.ThisTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getThisType(node); } + return links.resolvedType; + } - /** - * 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: ts.InferenceContext, index: number): ts.TypeMapper { - return makeFunctionTypeMapper(t => ts.findIndex(context.inferences, info => info.typeParameter === t) >= index ? unknownType : t); - } + function getTypeFromRestTypeNode(node: ts.RestTypeNode | ts.NamedTupleMember) { + return getTypeFromTypeNode(getArrayElementTypeNode(node.type) || node.type); + } - function combineTypeMappers(mapper1: ts.TypeMapper | undefined, mapper2: ts.TypeMapper): ts.TypeMapper { - return mapper1 ? makeCompositeTypeMapper(ts.TypeMapKind.Composite, mapper1, mapper2) : mapper2; + function getArrayElementTypeNode(node: ts.TypeNode): ts.TypeNode | undefined { + switch (node.kind) { + case ts.SyntaxKind.ParenthesizedType: + return getArrayElementTypeNode((node as ts.ParenthesizedTypeNode).type); + case ts.SyntaxKind.TupleType: + if ((node as ts.TupleTypeNode).elements.length === 1) { + node = (node as ts.TupleTypeNode).elements[0]; + if (node.kind === ts.SyntaxKind.RestType || node.kind === ts.SyntaxKind.NamedTupleMember && (node as ts.NamedTupleMember).dotDotDotToken) { + return getArrayElementTypeNode((node as ts.RestTypeNode | ts.NamedTupleMember).type); + } + } + break; + case ts.SyntaxKind.ArrayType: + return (node as ts.ArrayTypeNode).elementType; } + return undefined; + } - function mergeTypeMappers(mapper1: ts.TypeMapper | undefined, mapper2: ts.TypeMapper): ts.TypeMapper { - return mapper1 ? makeCompositeTypeMapper(ts.TypeMapKind.Merged, mapper1, mapper2) : mapper2; - } + function getTypeFromNamedTupleTypeNode(node: ts.NamedTupleMember): ts.Type { + const links = getNodeLinks(node); + return links.resolvedType || (links.resolvedType = + node.dotDotDotToken ? getTypeFromRestTypeNode(node) : + addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true, !!node.questionToken)); + } - function prependTypeMapping(source: ts.Type, target: ts.Type, mapper: ts.TypeMapper | undefined) { - return !mapper ? makeUnaryTypeMapper(source, target) : makeCompositeTypeMapper(ts.TypeMapKind.Merged, makeUnaryTypeMapper(source, target), mapper); - } + function getTypeFromTypeNode(node: ts.TypeNode): ts.Type { + return getConditionalFlowTypeOfType(getTypeFromTypeNodeWorker(node), node); + } - function appendTypeMapping(mapper: ts.TypeMapper | undefined, source: ts.Type, target: ts.Type) { - return !mapper ? makeUnaryTypeMapper(source, target) : makeCompositeTypeMapper(ts.TypeMapKind.Merged, mapper, makeUnaryTypeMapper(source, target)); + function getTypeFromTypeNodeWorker(node: ts.TypeNode): ts.Type { + switch (node.kind) { + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.JSDocAllType: + case ts.SyntaxKind.JSDocUnknownType: + return anyType; + case ts.SyntaxKind.UnknownKeyword: + return unknownType; + case ts.SyntaxKind.StringKeyword: + return stringType; + case ts.SyntaxKind.NumberKeyword: + return numberType; + case ts.SyntaxKind.BigIntKeyword: + return bigintType; + case ts.SyntaxKind.BooleanKeyword: + return booleanType; + case ts.SyntaxKind.SymbolKeyword: + return esSymbolType; + case ts.SyntaxKind.VoidKeyword: + return voidType; + case ts.SyntaxKind.UndefinedKeyword: + return undefinedType; + case ts.SyntaxKind.NullKeyword as ts.TypeNodeSyntaxKind: + // TODO(rbuckton): `NullKeyword` is no longer a `TypeNode`, but we defensively allow it here because of incorrect casts in the Language Service. + return nullType; + case ts.SyntaxKind.NeverKeyword: + return neverType; + case ts.SyntaxKind.ObjectKeyword: + return node.flags & ts.NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType; + case ts.SyntaxKind.IntrinsicKeyword: + return intrinsicMarkerType; + case ts.SyntaxKind.ThisType: + case ts.SyntaxKind.ThisKeyword as ts.TypeNodeSyntaxKind: + // TODO(rbuckton): `ThisKeyword` is no longer a `TypeNode`, but we defensively allow it here because of incorrect casts in the Language Service and because of `isPartOfTypeNode`. + return getTypeFromThisTypeNode(node as ts.ThisExpression | ts.ThisTypeNode); + case ts.SyntaxKind.LiteralType: + return getTypeFromLiteralTypeNode(node as ts.LiteralTypeNode); + case ts.SyntaxKind.TypeReference: + return getTypeFromTypeReference(node as ts.TypeReferenceNode); + case ts.SyntaxKind.TypePredicate: + return (node as ts.TypePredicateNode).assertsModifier ? voidType : booleanType; + case ts.SyntaxKind.ExpressionWithTypeArguments: + return getTypeFromTypeReference(node as ts.ExpressionWithTypeArguments); + case ts.SyntaxKind.TypeQuery: + return getTypeFromTypeQueryNode(node as ts.TypeQueryNode); + case ts.SyntaxKind.ArrayType: + case ts.SyntaxKind.TupleType: + return getTypeFromArrayOrTupleTypeNode(node as ts.ArrayTypeNode | ts.TupleTypeNode); + case ts.SyntaxKind.OptionalType: + return getTypeFromOptionalTypeNode(node as ts.OptionalTypeNode); + case ts.SyntaxKind.UnionType: + return getTypeFromUnionTypeNode(node as ts.UnionTypeNode); + case ts.SyntaxKind.IntersectionType: + return getTypeFromIntersectionTypeNode(node as ts.IntersectionTypeNode); + case ts.SyntaxKind.JSDocNullableType: + return getTypeFromJSDocNullableTypeNode(node as ts.JSDocNullableType); + case ts.SyntaxKind.JSDocOptionalType: + return addOptionality(getTypeFromTypeNode((node as ts.JSDocOptionalType).type)); + case ts.SyntaxKind.NamedTupleMember: + return getTypeFromNamedTupleTypeNode(node as ts.NamedTupleMember); + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.JSDocNonNullableType: + case ts.SyntaxKind.JSDocTypeExpression: + return getTypeFromTypeNode((node as ts.ParenthesizedTypeNode | ts.JSDocTypeReferencingNode | ts.JSDocTypeExpression | ts.NamedTupleMember).type); + case ts.SyntaxKind.RestType: + return getTypeFromRestTypeNode(node as ts.RestTypeNode); + case ts.SyntaxKind.JSDocVariadicType: + return getTypeFromJSDocVariadicType(node as ts.JSDocVariadicType); + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.JSDocTypeLiteral: + case ts.SyntaxKind.JSDocFunctionType: + case ts.SyntaxKind.JSDocSignature: + return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); + case ts.SyntaxKind.TypeOperator: + return getTypeFromTypeOperatorNode(node as ts.TypeOperatorNode); + case ts.SyntaxKind.IndexedAccessType: + return getTypeFromIndexedAccessTypeNode(node as ts.IndexedAccessTypeNode); + case ts.SyntaxKind.MappedType: + return getTypeFromMappedTypeNode(node as ts.MappedTypeNode); + case ts.SyntaxKind.ConditionalType: + return getTypeFromConditionalTypeNode(node as ts.ConditionalTypeNode); + case ts.SyntaxKind.InferType: + return getTypeFromInferTypeNode(node as ts.InferTypeNode); + case ts.SyntaxKind.TemplateLiteralType: + return getTypeFromTemplateTypeNode(node as ts.TemplateLiteralTypeNode); + case ts.SyntaxKind.ImportType: + return getTypeFromImportTypeNode(node as ts.ImportTypeNode); + // This function assumes that an identifier, qualified name, or property access expression is a type expression + // Callers should first ensure this by calling `isPartOfTypeNode` + // TODO(rbuckton): These aren't valid TypeNodes, but we treat them as such because of `isPartOfTypeNode`, which returns `true` for things that aren't `TypeNode`s. + case ts.SyntaxKind.Identifier as ts.TypeNodeSyntaxKind: + case ts.SyntaxKind.QualifiedName as ts.TypeNodeSyntaxKind: + case ts.SyntaxKind.PropertyAccessExpression as ts.TypeNodeSyntaxKind: + const symbol = getSymbolAtLocation(node); + return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; + default: + return errorType; } + } - function getRestrictiveTypeParameter(tp: ts.TypeParameter) { - return tp.constraint === unknownType ? tp : tp.restrictiveInstantiation || (tp.restrictiveInstantiation = createTypeParameter(tp.symbol), - (tp.restrictiveInstantiation as ts.TypeParameter).constraint = unknownType, - tp.restrictiveInstantiation); + function instantiateList(items: readonly T[], mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.TypeMapper) => T): readonly T[]; + function instantiateList(items: readonly T[] | undefined, mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.TypeMapper) => T): readonly T[] | undefined; + function instantiateList(items: readonly T[] | undefined, mapper: ts.TypeMapper, instantiator: (item: T, mapper: ts.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 cloneTypeParameter(typeParameter: ts.TypeParameter): ts.TypeParameter { - const result = createTypeParameter(typeParameter.symbol); - result.target = typeParameter; - return result; - } + function instantiateTypes(types: readonly ts.Type[], mapper: ts.TypeMapper): readonly ts.Type[]; + function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: ts.TypeMapper): readonly ts.Type[] | undefined; + function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: ts.TypeMapper): readonly ts.Type[] | undefined { + return instantiateList(types, mapper, instantiateType); + } - function instantiateTypePredicate(predicate: ts.TypePredicate, mapper: ts.TypeMapper): ts.TypePredicate { - return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper)); - } + function instantiateSignatures(signatures: readonly ts.Signature[], mapper: ts.TypeMapper): readonly ts.Signature[] { + return instantiateList(signatures, mapper, instantiateSignature); + } + + function instantiateIndexInfos(indexInfos: readonly ts.IndexInfo[], mapper: ts.TypeMapper): readonly ts.IndexInfo[] { + return instantiateList(indexInfos, mapper, instantiateIndexInfo); + } + + function createTypeMapper(sources: readonly ts.TypeParameter[], targets: readonly ts.Type[] | undefined): ts.TypeMapper { + return sources.length === 1 ? makeUnaryTypeMapper(sources[0], targets ? targets[0] : anyType) : makeArrayTypeMapper(sources, targets); + } - function instantiateSignature(signature: ts.Signature, mapper: ts.TypeMapper, eraseTypeParameters?: boolean): ts.Signature { - let freshTypeParameters: ts.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 = ts.map(signature.typeParameters, cloneTypeParameter); - mapper = combineTypeMappers(createTypeMapper(signature.typeParameters, freshTypeParameters), mapper); - for (const tp of freshTypeParameters) { - tp.mapper = mapper; + function getMappedType(type: ts.Type, mapper: ts.TypeMapper): ts.Type { + switch (mapper.kind) { + case ts.TypeMapKind.Simple: + return type === mapper.source ? mapper.target : type; + case ts.TypeMapKind.Array: + const sources = mapper.sources; + const targets = mapper.targets; + for (let i = 0; i < sources.length; i++) { + if (type === sources[i]) { + return targets ? targets[i] : anyType; + } } - } - // 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 & ts.SignatureFlags.PropagatingFlags); - result.target = signature; - result.mapper = mapper; - return result; + return type; + case ts.TypeMapKind.Function: + return mapper.func(type); + case ts.TypeMapKind.Composite: + case ts.TypeMapKind.Merged: + const t1 = getMappedType(type, mapper.mapper1); + return t1 !== type && mapper.kind === ts.TypeMapKind.Composite ? instantiateType(t1, mapper.mapper2) : getMappedType(t1, mapper.mapper2); } + } - function instantiateSymbol(symbol: ts.Symbol, mapper: ts.TypeMapper): ts.Symbol { - const links = getSymbolLinks(symbol); - if (links.type && !couldContainTypeVariables(links.type)) { - // 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 (ts.getCheckFlags(symbol) & ts.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, ts.CheckFlags.Instantiated | ts.getCheckFlags(symbol) & (ts.CheckFlags.Readonly | ts.CheckFlags.Late | ts.CheckFlags.OptionalParameter | ts.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 makeUnaryTypeMapper(source: ts.Type, target: ts.Type): ts.TypeMapper { + return { kind: ts.TypeMapKind.Simple, source, target }; + } + + function makeArrayTypeMapper(sources: readonly ts.TypeParameter[], targets: readonly ts.Type[] | undefined): ts.TypeMapper { + return { kind: ts.TypeMapKind.Array, sources, targets }; + } + + function makeFunctionTypeMapper(func: (t: ts.Type) => ts.Type): ts.TypeMapper { + return { kind: ts.TypeMapKind.Function, func }; + } + + function makeCompositeTypeMapper(kind: ts.TypeMapKind.Composite | ts.TypeMapKind.Merged, mapper1: ts.TypeMapper, mapper2: ts.TypeMapper): ts.TypeMapper { + return { kind, mapper1, mapper2 }; + } + + function createTypeEraser(sources: readonly ts.TypeParameter[]): ts.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: ts.InferenceContext, index: number): ts.TypeMapper { + return makeFunctionTypeMapper(t => ts.findIndex(context.inferences, info => info.typeParameter === t) >= index ? unknownType : t); + } + + function combineTypeMappers(mapper1: ts.TypeMapper | undefined, mapper2: ts.TypeMapper): ts.TypeMapper { + return mapper1 ? makeCompositeTypeMapper(ts.TypeMapKind.Composite, mapper1, mapper2) : mapper2; + } + + function mergeTypeMappers(mapper1: ts.TypeMapper | undefined, mapper2: ts.TypeMapper): ts.TypeMapper { + return mapper1 ? makeCompositeTypeMapper(ts.TypeMapKind.Merged, mapper1, mapper2) : mapper2; + } + + function prependTypeMapping(source: ts.Type, target: ts.Type, mapper: ts.TypeMapper | undefined) { + return !mapper ? makeUnaryTypeMapper(source, target) : makeCompositeTypeMapper(ts.TypeMapKind.Merged, makeUnaryTypeMapper(source, target), mapper); + } + + function appendTypeMapping(mapper: ts.TypeMapper | undefined, source: ts.Type, target: ts.Type) { + return !mapper ? makeUnaryTypeMapper(source, target) : makeCompositeTypeMapper(ts.TypeMapKind.Merged, mapper, makeUnaryTypeMapper(source, target)); + } + + function getRestrictiveTypeParameter(tp: ts.TypeParameter) { + return tp.constraint === unknownType ? tp : tp.restrictiveInstantiation || (tp.restrictiveInstantiation = createTypeParameter(tp.symbol), + (tp.restrictiveInstantiation as ts.TypeParameter).constraint = unknownType, + tp.restrictiveInstantiation); + } + + function cloneTypeParameter(typeParameter: ts.TypeParameter): ts.TypeParameter { + const result = createTypeParameter(typeParameter.symbol); + result.target = typeParameter; + return result; + } + + function instantiateTypePredicate(predicate: ts.TypePredicate, mapper: ts.TypeMapper): ts.TypePredicate { + return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper)); + } + + function instantiateSignature(signature: ts.Signature, mapper: ts.TypeMapper, eraseTypeParameters?: boolean): ts.Signature { + let freshTypeParameters: ts.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 = ts.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 & ts.SignatureFlags.PropagatingFlags); + result.target = signature; + result.mapper = mapper; + return result; + } + + function instantiateSymbol(symbol: ts.Symbol, mapper: ts.TypeMapper): ts.Symbol { + const links = getSymbolLinks(symbol); + if (links.type && !couldContainTypeVariables(links.type)) { + // 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 (ts.getCheckFlags(symbol) & ts.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, ts.CheckFlags.Instantiated | ts.getCheckFlags(symbol) & (ts.CheckFlags.Readonly | ts.CheckFlags.Late | ts.CheckFlags.OptionalParameter | ts.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: ts.AnonymousType | ts.DeferredTypeReference, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]) { - const declaration = type.objectFlags & ts.ObjectFlags.Reference ? (type as ts.TypeReference).node! : - type.objectFlags & ts.ObjectFlags.InstantiationExpressionType ? (type as ts.InstantiationExpressionType).node : - type.symbol.declarations![0]; - const links = getNodeLinks(declaration); - const target = type.objectFlags & ts.ObjectFlags.Reference ? links.resolvedType! as ts.DeferredTypeReference : - type.objectFlags & ts.ObjectFlags.Instantiated ? type.target! : type; - 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 outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true); - if (isJSConstructor(declaration)) { - const templateTagParameters = getTypeParametersFromDeclaration(declaration as ts.DeclarationWithTypeParameters); - outerTypeParameters = ts.addRange(outerTypeParameters, templateTagParameters); - } - typeParameters = outerTypeParameters || ts.emptyArray; - const allDeclarations = type.objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.InstantiationExpressionType) ? [declaration] : type.symbol.declarations!; - typeParameters = (target.objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.InstantiationExpressionType) || target.symbol.flags & ts.SymbolFlags.Method || target.symbol.flags & ts.SymbolFlags.TypeLiteral) && !target.aliasTypeArguments ? - ts.filter(typeParameters, tp => ts.some(allDeclarations, d => isTypeParameterPossiblyReferenced(tp, d))) : - typeParameters; - links.outerTypeParameters = typeParameters; - } - 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 combinedMapper = combineTypeMappers(type.mapper, mapper); - const typeArguments = ts.map(typeParameters, t => getMappedType(t, combinedMapper)); - const newAliasSymbol = aliasSymbol || type.aliasSymbol; - const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); - const id = getTypeListId(typeArguments) + getAliasId(newAliasSymbol, newAliasTypeArguments); - if (!target.instantiations) { - target.instantiations = new ts.Map(); - target.instantiations.set(getTypeListId(typeParameters) + getAliasId(target.aliasSymbol, target.aliasTypeArguments), target); - } - let result = target.instantiations.get(id); - if (!result) { - const newMapper = createTypeMapper(typeParameters, typeArguments); - result = target.objectFlags & ts.ObjectFlags.Reference ? createDeferredTypeReference((type as ts.DeferredTypeReference).target, (type as ts.DeferredTypeReference).node, newMapper, newAliasSymbol, newAliasTypeArguments) : - target.objectFlags & ts.ObjectFlags.Mapped ? instantiateMappedType(target as ts.MappedType, newMapper, newAliasSymbol, newAliasTypeArguments) : - instantiateAnonymousType(target, newMapper, newAliasSymbol, newAliasTypeArguments); - target.instantiations.set(id, result); - } - return result; + function getObjectTypeInstantiation(type: ts.AnonymousType | ts.DeferredTypeReference, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]) { + const declaration = type.objectFlags & ts.ObjectFlags.Reference ? (type as ts.TypeReference).node! : + type.objectFlags & ts.ObjectFlags.InstantiationExpressionType ? (type as ts.InstantiationExpressionType).node : + type.symbol.declarations![0]; + const links = getNodeLinks(declaration); + const target = type.objectFlags & ts.ObjectFlags.Reference ? links.resolvedType! as ts.DeferredTypeReference : + type.objectFlags & ts.ObjectFlags.Instantiated ? type.target! : type; + 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 outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true); + if (isJSConstructor(declaration)) { + const templateTagParameters = getTypeParametersFromDeclaration(declaration as ts.DeclarationWithTypeParameters); + outerTypeParameters = ts.addRange(outerTypeParameters, templateTagParameters); + } + typeParameters = outerTypeParameters || ts.emptyArray; + const allDeclarations = type.objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.InstantiationExpressionType) ? [declaration] : type.symbol.declarations!; + typeParameters = (target.objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.InstantiationExpressionType) || target.symbol.flags & ts.SymbolFlags.Method || target.symbol.flags & ts.SymbolFlags.TypeLiteral) && !target.aliasTypeArguments ? + ts.filter(typeParameters, tp => ts.some(allDeclarations, d => isTypeParameterPossiblyReferenced(tp, d))) : + typeParameters; + links.outerTypeParameters = typeParameters; + } + 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 combinedMapper = combineTypeMappers(type.mapper, mapper); + const typeArguments = ts.map(typeParameters, t => getMappedType(t, combinedMapper)); + const newAliasSymbol = aliasSymbol || type.aliasSymbol; + const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); + const id = getTypeListId(typeArguments) + getAliasId(newAliasSymbol, newAliasTypeArguments); + if (!target.instantiations) { + target.instantiations = new ts.Map(); + target.instantiations.set(getTypeListId(typeParameters) + getAliasId(target.aliasSymbol, target.aliasTypeArguments), target); + } + let result = target.instantiations.get(id); + if (!result) { + const newMapper = createTypeMapper(typeParameters, typeArguments); + result = target.objectFlags & ts.ObjectFlags.Reference ? createDeferredTypeReference((type as ts.DeferredTypeReference).target, (type as ts.DeferredTypeReference).node, newMapper, newAliasSymbol, newAliasTypeArguments) : + target.objectFlags & ts.ObjectFlags.Mapped ? instantiateMappedType(target as ts.MappedType, newMapper, newAliasSymbol, newAliasTypeArguments) : + instantiateAnonymousType(target, newMapper, newAliasSymbol, newAliasTypeArguments); + target.instantiations.set(id, result); } - return type; + return result; } + return type; + } - function maybeTypeParameterReference(node: ts.Node) { - return !(node.parent.kind === ts.SyntaxKind.TypeReference && (node.parent as ts.TypeReferenceNode).typeArguments && node === (node.parent as ts.TypeReferenceNode).typeName || - node.parent.kind === ts.SyntaxKind.ImportType && (node.parent as ts.ImportTypeNode).typeArguments && node === (node.parent as ts.ImportTypeNode).qualifier); - } + function maybeTypeParameterReference(node: ts.Node) { + return !(node.parent.kind === ts.SyntaxKind.TypeReference && (node.parent as ts.TypeReferenceNode).typeArguments && node === (node.parent as ts.TypeReferenceNode).typeName || + node.parent.kind === ts.SyntaxKind.ImportType && (node.parent as ts.ImportTypeNode).typeArguments && node === (node.parent as ts.ImportTypeNode).qualifier); + } - function isTypeParameterPossiblyReferenced(tp: ts.TypeParameter, node: ts.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 === ts.SyntaxKind.Block || n.kind === ts.SyntaxKind.ConditionalType && ts.forEachChild((n as ts.ConditionalTypeNode).extendsType, containsReference)) { - return true; - } + function isTypeParameterPossiblyReferenced(tp: ts.TypeParameter, node: ts.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 === ts.SyntaxKind.Block || n.kind === ts.SyntaxKind.ConditionalType && ts.forEachChild((n as ts.ConditionalTypeNode).extendsType, containsReference)) { + return true; } - return containsReference(node); } - return true; - function containsReference(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.ThisType: - return !!tp.isThisType; - case ts.SyntaxKind.Identifier: - return !tp.isThisType && ts.isPartOfTypeNode(node) && maybeTypeParameterReference(node) && - getTypeFromTypeNodeWorker(node as ts.TypeNode) === tp; // use worker because we're looking for === equality - case ts.SyntaxKind.TypeQuery: - return true; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return !(node as ts.FunctionLikeDeclaration).type && !!(node as ts.FunctionLikeDeclaration).body || - ts.some((node as ts.FunctionLikeDeclaration).typeParameters, containsReference) || - ts.some((node as ts.FunctionLikeDeclaration).parameters, containsReference) || - !!(node as ts.FunctionLikeDeclaration).type && containsReference((node as ts.FunctionLikeDeclaration).type!); - } - return !!ts.forEachChild(node, containsReference); + return containsReference(node); + } + return true; + function containsReference(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ThisType: + return !!tp.isThisType; + case ts.SyntaxKind.Identifier: + return !tp.isThisType && ts.isPartOfTypeNode(node) && maybeTypeParameterReference(node) && + getTypeFromTypeNodeWorker(node as ts.TypeNode) === tp; // use worker because we're looking for === equality + case ts.SyntaxKind.TypeQuery: + return true; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return !(node as ts.FunctionLikeDeclaration).type && !!(node as ts.FunctionLikeDeclaration).body || + ts.some((node as ts.FunctionLikeDeclaration).typeParameters, containsReference) || + ts.some((node as ts.FunctionLikeDeclaration).parameters, containsReference) || + !!(node as ts.FunctionLikeDeclaration).type && containsReference((node as ts.FunctionLikeDeclaration).type!); } + return !!ts.forEachChild(node, containsReference); } + } - function getHomomorphicTypeVariable(type: ts.MappedType) { - const constraintType = getConstraintTypeFromMappedType(type); - if (constraintType.flags & ts.TypeFlags.Index) { - const typeVariable = getActualTypeVariable((constraintType as ts.IndexType).type); - if (typeVariable.flags & ts.TypeFlags.TypeParameter) { - return typeVariable as ts.TypeParameter; - } + function getHomomorphicTypeVariable(type: ts.MappedType) { + const constraintType = getConstraintTypeFromMappedType(type); + if (constraintType.flags & ts.TypeFlags.Index) { + const typeVariable = getActualTypeVariable((constraintType as ts.IndexType).type); + if (typeVariable.flags & ts.TypeFlags.TypeParameter) { + return typeVariable as ts.TypeParameter; } - return undefined; } + return undefined; + } - function instantiateMappedType(type: ts.MappedType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): 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 mapTypeWithAlias(getReducedType(mappedTypeVariable), t => { - if (t.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Object | ts.TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) { - if (!type.declaration.nameType) { - let constraint; - if (isArrayType(t) || t.flags & ts.TypeFlags.Any && findResolutionCycleStartIndex(typeVariable, TypeSystemPropertyName.ImmediateBaseConstraint) < 0 && - (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleType)) { - return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper)); - } - if (isGenericTupleType(t)) { - return instantiateMappedGenericTupleType(t, type, typeVariable, mapper); - } - if (isTupleType(t)) { - return instantiateMappedTupleType(t, type, prependTypeMapping(typeVariable, t, mapper)); - } - } - return instantiateAnonymousType(type, prependTypeMapping(typeVariable, t, mapper)); - } - return t; - }, aliasSymbol, aliasTypeArguments); - } + function instantiateMappedType(type: ts.MappedType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): 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 mapTypeWithAlias(getReducedType(mappedTypeVariable), t => { + if (t.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Object | ts.TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) { + if (!type.declaration.nameType) { + let constraint; + if (isArrayType(t) || t.flags & ts.TypeFlags.Any && findResolutionCycleStartIndex(typeVariable, TypeSystemPropertyName.ImmediateBaseConstraint) < 0 && + (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleType)) { + return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper)); + } + if (isGenericTupleType(t)) { + return instantiateMappedGenericTupleType(t, type, typeVariable, mapper); + } + if (isTupleType(t)) { + return instantiateMappedTupleType(t, type, prependTypeMapping(typeVariable, t, mapper)); + } + } + return instantiateAnonymousType(type, prependTypeMapping(typeVariable, t, mapper)); + } + return t; + }, aliasSymbol, aliasTypeArguments); } - // If the constraint type of the instantiation is the wildcard type, return the wildcard type. - return instantiateType(getConstraintTypeFromMappedType(type), mapper) === wildcardType ? wildcardType : instantiateAnonymousType(type, mapper, aliasSymbol, aliasTypeArguments); } + // If the constraint type of the instantiation is the wildcard type, return the wildcard type. + return instantiateType(getConstraintTypeFromMappedType(type), mapper) === wildcardType ? wildcardType : instantiateAnonymousType(type, mapper, aliasSymbol, aliasTypeArguments); + } - function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) { - return modifiers & MappedTypeModifiers.IncludeReadonly ? true : modifiers & MappedTypeModifiers.ExcludeReadonly ? false : state; - } + function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) { + return modifiers & MappedTypeModifiers.IncludeReadonly ? true : modifiers & MappedTypeModifiers.ExcludeReadonly ? false : state; + } - function instantiateMappedGenericTupleType(tupleType: ts.TupleTypeReference, mappedType: ts.MappedType, typeVariable: ts.TypeVariable, mapper: ts.TypeMapper) { - // When a tuple type is generic (i.e. when it contains variadic elements), we want to eagerly map the - // non-generic elements and defer mapping the generic elements. In order to facilitate this, we transform - // M<[A, B?, ...T, ...C[]] into [...M<[A]>, ...M<[B?]>, ...M, ...M] and then rely on tuple type - // normalization to resolve the non-generic parts of the resulting tuple. - const elementFlags = tupleType.target.elementFlags; - const elementTypes = ts.map(getTypeArguments(tupleType), (t, i) => { - const singleton = elementFlags[i] & ts.ElementFlags.Variadic ? t : - elementFlags[i] & ts.ElementFlags.Rest ? createArrayType(t) : - createTupleType([t], [elementFlags[i]]); - // The singleton is never a generic tuple type, so it is safe to recurse here. - return instantiateMappedType(mappedType, prependTypeMapping(typeVariable, singleton, mapper)); - }); - const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, getMappedTypeModifiers(mappedType)); - return createTupleType(elementTypes, ts.map(elementTypes, _ => ts.ElementFlags.Variadic), newReadonly); - } - - function instantiateMappedArrayType(arrayType: ts.Type, mappedType: ts.MappedType, mapper: ts.TypeMapper) { - const elementType = instantiateMappedTypeTemplate(mappedType, numberType, /*isOptional*/ true, mapper); - return isErrorType(elementType) ? errorType : - createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), getMappedTypeModifiers(mappedType))); - } - - function instantiateMappedTupleType(tupleType: ts.TupleTypeReference, mappedType: ts.MappedType, mapper: ts.TypeMapper) { - const elementFlags = tupleType.target.elementFlags; - const elementTypes = ts.map(getTypeArguments(tupleType), (_, i) => instantiateMappedTypeTemplate(mappedType, getStringLiteralType("" + i), !!(elementFlags[i] & ts.ElementFlags.Optional), mapper)); - const modifiers = getMappedTypeModifiers(mappedType); - const newTupleModifiers = modifiers & MappedTypeModifiers.IncludeOptional ? ts.map(elementFlags, f => f & ts.ElementFlags.Required ? ts.ElementFlags.Optional : f) : - modifiers & MappedTypeModifiers.ExcludeOptional ? ts.map(elementFlags, f => f & ts.ElementFlags.Optional ? ts.ElementFlags.Required : f) : - elementFlags; - const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, modifiers); - return ts.contains(elementTypes, errorType) ? errorType : - createTupleType(elementTypes, newTupleModifiers, newReadonly, tupleType.target.labeledElementDeclarations); - } - - function instantiateMappedTypeTemplate(type: ts.MappedType, key: ts.Type, isOptional: boolean, mapper: ts.TypeMapper) { - const templateMapper = appendTypeMapping(mapper, getTypeParameterFromMappedType(type), key); - const propType = instantiateType(getTemplateTypeFromMappedType(type.target as ts.MappedType || type), templateMapper); - const modifiers = getMappedTypeModifiers(type); - return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !maybeTypeOfKind(propType, ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? getOptionalType(propType, /*isProperty*/ true) : - strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : - propType; - } + function instantiateMappedGenericTupleType(tupleType: ts.TupleTypeReference, mappedType: ts.MappedType, typeVariable: ts.TypeVariable, mapper: ts.TypeMapper) { + // When a tuple type is generic (i.e. when it contains variadic elements), we want to eagerly map the + // non-generic elements and defer mapping the generic elements. In order to facilitate this, we transform + // M<[A, B?, ...T, ...C[]] into [...M<[A]>, ...M<[B?]>, ...M, ...M] and then rely on tuple type + // normalization to resolve the non-generic parts of the resulting tuple. + const elementFlags = tupleType.target.elementFlags; + const elementTypes = ts.map(getTypeArguments(tupleType), (t, i) => { + const singleton = elementFlags[i] & ts.ElementFlags.Variadic ? t : + elementFlags[i] & ts.ElementFlags.Rest ? createArrayType(t) : + createTupleType([t], [elementFlags[i]]); + // The singleton is never a generic tuple type, so it is safe to recurse here. + return instantiateMappedType(mappedType, prependTypeMapping(typeVariable, singleton, mapper)); + }); + const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, getMappedTypeModifiers(mappedType)); + return createTupleType(elementTypes, ts.map(elementTypes, _ => ts.ElementFlags.Variadic), newReadonly); + } - function instantiateAnonymousType(type: ts.AnonymousType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.AnonymousType { - const result = createObjectType(type.objectFlags | ts.ObjectFlags.Instantiated, type.symbol) as ts.AnonymousType; - if (type.objectFlags & ts.ObjectFlags.Mapped) { - (result as ts.MappedType).declaration = (type as ts.MappedType).declaration; - // C.f. instantiateSignature - const origTypeParameter = getTypeParameterFromMappedType(type as ts.MappedType); - const freshTypeParameter = cloneTypeParameter(origTypeParameter); - (result as ts.MappedType).typeParameter = freshTypeParameter; - mapper = combineTypeMappers(makeUnaryTypeMapper(origTypeParameter, freshTypeParameter), mapper); - freshTypeParameter.mapper = mapper; - } - if (type.objectFlags & ts.ObjectFlags.InstantiationExpressionType) { - (result as ts.InstantiationExpressionType).node = (type as ts.InstantiationExpressionType).node; + function instantiateMappedArrayType(arrayType: ts.Type, mappedType: ts.MappedType, mapper: ts.TypeMapper) { + const elementType = instantiateMappedTypeTemplate(mappedType, numberType, /*isOptional*/ true, mapper); + return isErrorType(elementType) ? errorType : + createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), getMappedTypeModifiers(mappedType))); + } + + function instantiateMappedTupleType(tupleType: ts.TupleTypeReference, mappedType: ts.MappedType, mapper: ts.TypeMapper) { + const elementFlags = tupleType.target.elementFlags; + const elementTypes = ts.map(getTypeArguments(tupleType), (_, i) => instantiateMappedTypeTemplate(mappedType, getStringLiteralType("" + i), !!(elementFlags[i] & ts.ElementFlags.Optional), mapper)); + const modifiers = getMappedTypeModifiers(mappedType); + const newTupleModifiers = modifiers & MappedTypeModifiers.IncludeOptional ? ts.map(elementFlags, f => f & ts.ElementFlags.Required ? ts.ElementFlags.Optional : f) : + modifiers & MappedTypeModifiers.ExcludeOptional ? ts.map(elementFlags, f => f & ts.ElementFlags.Optional ? ts.ElementFlags.Required : f) : + elementFlags; + const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, modifiers); + return ts.contains(elementTypes, errorType) ? errorType : + createTupleType(elementTypes, newTupleModifiers, newReadonly, tupleType.target.labeledElementDeclarations); + } + + function instantiateMappedTypeTemplate(type: ts.MappedType, key: ts.Type, isOptional: boolean, mapper: ts.TypeMapper) { + const templateMapper = appendTypeMapping(mapper, getTypeParameterFromMappedType(type), key); + const propType = instantiateType(getTemplateTypeFromMappedType(type.target as ts.MappedType || type), templateMapper); + const modifiers = getMappedTypeModifiers(type); + return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !maybeTypeOfKind(propType, ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? getOptionalType(propType, /*isProperty*/ true) : + strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : + propType; + } + + function instantiateAnonymousType(type: ts.AnonymousType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.AnonymousType { + const result = createObjectType(type.objectFlags | ts.ObjectFlags.Instantiated, type.symbol) as ts.AnonymousType; + if (type.objectFlags & ts.ObjectFlags.Mapped) { + (result as ts.MappedType).declaration = (type as ts.MappedType).declaration; + // C.f. instantiateSignature + const origTypeParameter = getTypeParameterFromMappedType(type as ts.MappedType); + const freshTypeParameter = cloneTypeParameter(origTypeParameter); + (result as ts.MappedType).typeParameter = freshTypeParameter; + mapper = combineTypeMappers(makeUnaryTypeMapper(origTypeParameter, freshTypeParameter), mapper); + freshTypeParameter.mapper = mapper; + } + if (type.objectFlags & ts.ObjectFlags.InstantiationExpressionType) { + (result as ts.InstantiationExpressionType).node = (type as ts.InstantiationExpressionType).node; + } + result.target = type; + result.mapper = mapper; + result.aliasSymbol = aliasSymbol || type.aliasSymbol; + result.aliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); + return result; + } + + function getConditionalTypeInstantiation(type: ts.ConditionalType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): 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 = ts.map(root.outerTypeParameters, t => getMappedType(t, mapper)); + const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments); + let result = root.instantiations!.get(id); + if (!result) { + const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments); + const checkType = root.checkType; + const distributionType = root.isDistributive ? getMappedType(checkType, newMapper) : undefined; + // Distributive conditional types are distributed over union types. For example, when the + // distributive conditional type T extends U ? X : Y is instantiated with A | B for T, the + // result is (A extends U ? X : Y) | (B extends U ? X : Y). + result = distributionType && checkType !== distributionType && distributionType.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never) ? + mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper)), aliasSymbol, aliasTypeArguments) : + getConditionalType(root, newMapper, aliasSymbol, aliasTypeArguments); + root.instantiations!.set(id, result); } - result.target = type; - result.mapper = mapper; - result.aliasSymbol = aliasSymbol || type.aliasSymbol; - result.aliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); return result; } + return type; + } - function getConditionalTypeInstantiation(type: ts.ConditionalType, mapper: ts.TypeMapper, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): 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 = ts.map(root.outerTypeParameters, t => getMappedType(t, mapper)); - const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments); - let result = root.instantiations!.get(id); - if (!result) { - const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments); - const checkType = root.checkType; - const distributionType = root.isDistributive ? getMappedType(checkType, newMapper) : undefined; - // Distributive conditional types are distributed over union types. For example, when the - // distributive conditional type T extends U ? X : Y is instantiated with A | B for T, the - // result is (A extends U ? X : Y) | (B extends U ? X : Y). - result = distributionType && checkType !== distributionType && distributionType.flags & (ts.TypeFlags.Union | ts.TypeFlags.Never) ? - mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper)), aliasSymbol, aliasTypeArguments) : - getConditionalType(root, newMapper, aliasSymbol, aliasTypeArguments); - root.instantiations!.set(id, result); - } - return result; - } + function instantiateType(type: ts.Type, mapper: ts.TypeMapper | undefined): ts.Type; + function instantiateType(type: ts.Type | undefined, mapper: ts.TypeMapper | undefined): ts.Type | undefined; + function instantiateType(type: ts.Type | undefined, mapper: ts.TypeMapper | undefined): ts.Type | undefined { + return type && mapper ? instantiateTypeWithAlias(type, mapper, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined) : type; + } + + function instantiateTypeWithAlias(type: ts.Type, mapper: ts.TypeMapper, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined): ts.Type { + if (!couldContainTypeVariables(type)) { return type; } - - function instantiateType(type: ts.Type, mapper: ts.TypeMapper | undefined): ts.Type; - function instantiateType(type: ts.Type | undefined, mapper: ts.TypeMapper | undefined): ts.Type | undefined; - function instantiateType(type: ts.Type | undefined, mapper: ts.TypeMapper | undefined): ts.Type | undefined { - return type && mapper ? instantiateTypeWithAlias(type, mapper, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined) : type; + if (instantiationDepth === 100 || instantiationCount >= 5000000) { + // We have reached 100 recursive type instantiations, or 5M type instantiations caused by the same statement + // or expression. There is a very high likelyhood we're dealing with a combination of infinite generic types + // that perpetually generate new type identities, so we stop the recursion here by yielding the error type. + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth, instantiationCount }); + error(currentNode, ts.Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); + return errorType; } + totalInstantiationCount++; + instantiationCount++; + instantiationDepth++; + const result = instantiateTypeWorker(type, mapper, aliasSymbol, aliasTypeArguments); + instantiationDepth--; + return result; + } - function instantiateTypeWithAlias(type: ts.Type, mapper: ts.TypeMapper, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined): ts.Type { - if (!couldContainTypeVariables(type)) { - return type; - } - if (instantiationDepth === 100 || instantiationCount >= 5000000) { - // We have reached 100 recursive type instantiations, or 5M type instantiations caused by the same statement - // or expression. There is a very high likelyhood we're dealing with a combination of infinite generic types - // that perpetually generate new type identities, so we stop the recursion here by yielding the error type. - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth, instantiationCount }); - error(currentNode, ts.Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); - return errorType; - } - totalInstantiationCount++; - instantiationCount++; - instantiationDepth++; - const result = instantiateTypeWorker(type, mapper, aliasSymbol, aliasTypeArguments); - instantiationDepth--; - return result; + function instantiateTypeWorker(type: ts.Type, mapper: ts.TypeMapper, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined): ts.Type { + const flags = type.flags; + if (flags & ts.TypeFlags.TypeParameter) { + return getMappedType(type, mapper); } - - function instantiateTypeWorker(type: ts.Type, mapper: ts.TypeMapper, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined): ts.Type { - const flags = type.flags; - if (flags & ts.TypeFlags.TypeParameter) { - return getMappedType(type, mapper); - } - if (flags & ts.TypeFlags.Object) { - const objectFlags = (type as ts.ObjectType).objectFlags; - if (objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped)) { - if (objectFlags & ts.ObjectFlags.Reference && !(type as ts.TypeReference).node) { - const resolvedTypeArguments = (type as ts.TypeReference).resolvedTypeArguments; - const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper); - return newTypeArguments !== resolvedTypeArguments ? createNormalizedTypeReference((type as ts.TypeReference).target, newTypeArguments) : type; - } - if (objectFlags & ts.ObjectFlags.ReverseMapped) { - return instantiateReverseMappedType(type as ts.ReverseMappedType, mapper); - } - return getObjectTypeInstantiation(type as ts.TypeReference | ts.AnonymousType | ts.MappedType, mapper, aliasSymbol, aliasTypeArguments); + if (flags & ts.TypeFlags.Object) { + const objectFlags = (type as ts.ObjectType).objectFlags; + if (objectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped)) { + if (objectFlags & ts.ObjectFlags.Reference && !(type as ts.TypeReference).node) { + const resolvedTypeArguments = (type as ts.TypeReference).resolvedTypeArguments; + const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper); + return newTypeArguments !== resolvedTypeArguments ? createNormalizedTypeReference((type as ts.TypeReference).target, newTypeArguments) : type; } - return type; - } - if (flags & ts.TypeFlags.UnionOrIntersection) { - const origin = type.flags & ts.TypeFlags.Union ? (type as ts.UnionType).origin : undefined; - const types = origin && origin.flags & ts.TypeFlags.UnionOrIntersection ? (origin as ts.UnionOrIntersectionType).types : (type as ts.UnionOrIntersectionType).types; - const newTypes = instantiateTypes(types, mapper); - if (newTypes === types && aliasSymbol === type.aliasSymbol) { - return type; - } - const newAliasSymbol = aliasSymbol || type.aliasSymbol; - const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); - return flags & ts.TypeFlags.Intersection || origin && origin.flags & ts.TypeFlags.Intersection ? - getIntersectionType(newTypes, newAliasSymbol, newAliasTypeArguments) : - getUnionType(newTypes, ts.UnionReduction.Literal, newAliasSymbol, newAliasTypeArguments); - } - if (flags & ts.TypeFlags.Index) { - return getIndexType(instantiateType((type as ts.IndexType).type, mapper)); - } - if (flags & ts.TypeFlags.TemplateLiteral) { - return getTemplateLiteralType((type as ts.TemplateLiteralType).texts, instantiateTypes((type as ts.TemplateLiteralType).types, mapper)); - } - if (flags & ts.TypeFlags.StringMapping) { - return getStringMappingType((type as ts.StringMappingType).symbol, instantiateType((type as ts.StringMappingType).type, mapper)); - } - if (flags & ts.TypeFlags.IndexedAccess) { - const newAliasSymbol = aliasSymbol || type.aliasSymbol; - const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); - return getIndexedAccessType(instantiateType((type as ts.IndexedAccessType).objectType, mapper), instantiateType((type as ts.IndexedAccessType).indexType, mapper), (type as ts.IndexedAccessType).accessFlags, /*accessNode*/ undefined, newAliasSymbol, newAliasTypeArguments); - } - if (flags & ts.TypeFlags.Conditional) { - return getConditionalTypeInstantiation(type as ts.ConditionalType, combineTypeMappers((type as ts.ConditionalType).mapper, mapper), aliasSymbol, aliasTypeArguments); - } - if (flags & ts.TypeFlags.Substitution) { - const maybeVariable = instantiateType((type as ts.SubstitutionType).baseType, mapper); - if (maybeVariable.flags & ts.TypeFlags.TypeVariable) { - return getSubstitutionType(maybeVariable as ts.TypeVariable, instantiateType((type as ts.SubstitutionType).substitute, mapper)); - } - else { - const sub = instantiateType((type as ts.SubstitutionType).substitute, mapper); - if (sub.flags & ts.TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) { - return maybeVariable; - } - return sub; + if (objectFlags & ts.ObjectFlags.ReverseMapped) { + return instantiateReverseMappedType(type as ts.ReverseMappedType, mapper); } + return getObjectTypeInstantiation(type as ts.TypeReference | ts.AnonymousType | ts.MappedType, mapper, aliasSymbol, aliasTypeArguments); } return type; } - - function instantiateReverseMappedType(type: ts.ReverseMappedType, mapper: ts.TypeMapper) { - const innerMappedType = instantiateType(type.mappedType, mapper); - if (!(ts.getObjectFlags(innerMappedType) & ts.ObjectFlags.Mapped)) { + if (flags & ts.TypeFlags.UnionOrIntersection) { + const origin = type.flags & ts.TypeFlags.Union ? (type as ts.UnionType).origin : undefined; + const types = origin && origin.flags & ts.TypeFlags.UnionOrIntersection ? (origin as ts.UnionOrIntersectionType).types : (type as ts.UnionOrIntersectionType).types; + const newTypes = instantiateTypes(types, mapper); + if (newTypes === types && aliasSymbol === type.aliasSymbol) { return type; } - const innerIndexType = instantiateType(type.constraintType, mapper); - if (!(innerIndexType.flags & ts.TypeFlags.Index)) { - return type; + const newAliasSymbol = aliasSymbol || type.aliasSymbol; + const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); + return flags & ts.TypeFlags.Intersection || origin && origin.flags & ts.TypeFlags.Intersection ? + getIntersectionType(newTypes, newAliasSymbol, newAliasTypeArguments) : + getUnionType(newTypes, ts.UnionReduction.Literal, newAliasSymbol, newAliasTypeArguments); + } + if (flags & ts.TypeFlags.Index) { + return getIndexType(instantiateType((type as ts.IndexType).type, mapper)); + } + if (flags & ts.TypeFlags.TemplateLiteral) { + return getTemplateLiteralType((type as ts.TemplateLiteralType).texts, instantiateTypes((type as ts.TemplateLiteralType).types, mapper)); + } + if (flags & ts.TypeFlags.StringMapping) { + return getStringMappingType((type as ts.StringMappingType).symbol, instantiateType((type as ts.StringMappingType).type, mapper)); + } + if (flags & ts.TypeFlags.IndexedAccess) { + const newAliasSymbol = aliasSymbol || type.aliasSymbol; + const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper); + return getIndexedAccessType(instantiateType((type as ts.IndexedAccessType).objectType, mapper), instantiateType((type as ts.IndexedAccessType).indexType, mapper), (type as ts.IndexedAccessType).accessFlags, /*accessNode*/ undefined, newAliasSymbol, newAliasTypeArguments); + } + if (flags & ts.TypeFlags.Conditional) { + return getConditionalTypeInstantiation(type as ts.ConditionalType, combineTypeMappers((type as ts.ConditionalType).mapper, mapper), aliasSymbol, aliasTypeArguments); + } + if (flags & ts.TypeFlags.Substitution) { + const maybeVariable = instantiateType((type as ts.SubstitutionType).baseType, mapper); + if (maybeVariable.flags & ts.TypeFlags.TypeVariable) { + return getSubstitutionType(maybeVariable as ts.TypeVariable, instantiateType((type as ts.SubstitutionType).substitute, mapper)); } - const instantiated = inferTypeForHomomorphicMappedType(instantiateType(type.source, mapper), innerMappedType as ts.MappedType, innerIndexType as ts.IndexType); - if (instantiated) { - return instantiated; + else { + const sub = instantiateType((type as ts.SubstitutionType).substitute, mapper); + if (sub.flags & ts.TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) { + return maybeVariable; + } + return sub; } - return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable } + return type; + } - function getUniqueLiteralFilledInstantiation(type: ts.Type) { - return type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never) ? type : - type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper)); + function instantiateReverseMappedType(type: ts.ReverseMappedType, mapper: ts.TypeMapper) { + const innerMappedType = instantiateType(type.mappedType, mapper); + if (!(ts.getObjectFlags(innerMappedType) & ts.ObjectFlags.Mapped)) { + return type; } - - function getPermissiveInstantiation(type: ts.Type) { - return type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never) ? type : - type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper)); + const innerIndexType = instantiateType(type.constraintType, mapper); + if (!(innerIndexType.flags & ts.TypeFlags.Index)) { + return type; + } + const instantiated = inferTypeForHomomorphicMappedType(instantiateType(type.source, mapper), innerMappedType as ts.MappedType, innerIndexType as ts.IndexType); + if (instantiated) { + return instantiated; } + return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable + } - function getRestrictiveInstantiation(type: ts.Type) { - if (type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.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; + function getUniqueLiteralFilledInstantiation(type: ts.Type) { + return type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never) ? type : + type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper)); + } + + function getPermissiveInstantiation(type: ts.Type) { + return type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never) ? type : + type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper)); + } + + function getRestrictiveInstantiation(type: ts.Type) { + if (type.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.AnyOrUnknown | ts.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: ts.IndexInfo, mapper: ts.TypeMapper) { - return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration); - } + function instantiateIndexInfo(info: ts.IndexInfo, mapper: ts.TypeMapper) { + return createIndexInfo(info.keyType, 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: ts.Expression | ts.MethodDeclaration | ts.ObjectLiteralElementLike | ts.JsxAttributeLike | ts.JsxChild): boolean { - ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); - switch (node.kind) { - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionDeclaration: // Function declarations can have context when annotated with a jsdoc @type - return isContextSensitiveFunctionLikeDeclaration(node as ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration); - case ts.SyntaxKind.ObjectLiteralExpression: - return ts.some((node as ts.ObjectLiteralExpression).properties, isContextSensitive); - case ts.SyntaxKind.ArrayLiteralExpression: - return ts.some((node as ts.ArrayLiteralExpression).elements, isContextSensitive); - case ts.SyntaxKind.ConditionalExpression: - return isContextSensitive((node as ts.ConditionalExpression).whenTrue) || - isContextSensitive((node as ts.ConditionalExpression).whenFalse); - case ts.SyntaxKind.BinaryExpression: - return ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken || (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) && - (isContextSensitive((node as ts.BinaryExpression).left) || isContextSensitive((node as ts.BinaryExpression).right)); - case ts.SyntaxKind.PropertyAssignment: - return isContextSensitive((node as ts.PropertyAssignment).initializer); - case ts.SyntaxKind.ParenthesizedExpression: - return isContextSensitive((node as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.JsxAttributes: - return ts.some((node as ts.JsxAttributes).properties, isContextSensitive) || ts.isJsxOpeningElement(node.parent) && ts.some(node.parent.parent.children, isContextSensitive); - case ts.SyntaxKind.JsxAttribute: { - // If there is no initializer, JSX attribute has a boolean value of true which is not context sensitive. - const { initializer } = node as ts.JsxAttribute; - return !!initializer && isContextSensitive(initializer); - } - case ts.SyntaxKind.JsxExpression: { - // It is possible to that node.expression is undefined (e.g

) - const { expression } = node as ts.JsxExpression; - return !!expression && isContextSensitive(expression); - } - } + // 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: ts.Expression | ts.MethodDeclaration | ts.ObjectLiteralElementLike | ts.JsxAttributeLike | ts.JsxChild): boolean { + ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); + switch (node.kind) { + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionDeclaration: // Function declarations can have context when annotated with a jsdoc @type + return isContextSensitiveFunctionLikeDeclaration(node as ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration); + case ts.SyntaxKind.ObjectLiteralExpression: + return ts.some((node as ts.ObjectLiteralExpression).properties, isContextSensitive); + case ts.SyntaxKind.ArrayLiteralExpression: + return ts.some((node as ts.ArrayLiteralExpression).elements, isContextSensitive); + case ts.SyntaxKind.ConditionalExpression: + return isContextSensitive((node as ts.ConditionalExpression).whenTrue) || + isContextSensitive((node as ts.ConditionalExpression).whenFalse); + case ts.SyntaxKind.BinaryExpression: + return ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken || (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) && + (isContextSensitive((node as ts.BinaryExpression).left) || isContextSensitive((node as ts.BinaryExpression).right)); + case ts.SyntaxKind.PropertyAssignment: + return isContextSensitive((node as ts.PropertyAssignment).initializer); + case ts.SyntaxKind.ParenthesizedExpression: + return isContextSensitive((node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.JsxAttributes: + return ts.some((node as ts.JsxAttributes).properties, isContextSensitive) || ts.isJsxOpeningElement(node.parent) && ts.some(node.parent.parent.children, isContextSensitive); + case ts.SyntaxKind.JsxAttribute: { + // If there is no initializer, JSX attribute has a boolean value of true which is not context sensitive. + const { initializer } = node as ts.JsxAttribute; + return !!initializer && isContextSensitive(initializer); + } + case ts.SyntaxKind.JsxExpression: { + // It is possible to that node.expression is undefined (e.g
) + const { expression } = node as ts.JsxExpression; + return !!expression && isContextSensitive(expression); + } + } + + return false; + } - return false; - } + function isContextSensitiveFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): boolean { + return (!ts.isFunctionDeclaration(node) || ts.isInJSFile(node) && !!getTypeForDeclarationFromJSDocComment(node)) && + (ts.hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node)); + } + + function hasContextSensitiveReturnExpression(node: ts.FunctionLikeDeclaration) { + // TODO(anhans): A block should be context-sensitive if it has a context-sensitive return value. + return !node.typeParameters && !ts.getEffectiveReturnTypeNode(node) && !!node.body && node.body.kind !== ts.SyntaxKind.Block && isContextSensitive(node.body); + } + + function isContextSensitiveFunctionOrObjectLiteralMethod(func: ts.Node): func is ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration { + return (ts.isInJSFile(func) && ts.isFunctionDeclaration(func) || ts.isFunctionExpressionOrArrowFunction(func) || ts.isObjectLiteralMethod(func)) && + isContextSensitiveFunctionLikeDeclaration(func); + } - function isContextSensitiveFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): boolean { - return (!ts.isFunctionDeclaration(node) || ts.isInJSFile(node) && !!getTypeForDeclarationFromJSDocComment(node)) && - (ts.hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node)); + function getTypeWithoutSignatures(type: ts.Type): ts.Type { + if (type.flags & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + if (resolved.constructSignatures.length || resolved.callSignatures.length) { + const result = createObjectType(ts.ObjectFlags.Anonymous, type.symbol); + result.members = resolved.members; + result.properties = resolved.properties; + result.callSignatures = ts.emptyArray; + result.constructSignatures = ts.emptyArray; + result.indexInfos = ts.emptyArray; + return result; + } + } + else if (type.flags & ts.TypeFlags.Intersection) { + return getIntersectionType(ts.map((type as ts.IntersectionType).types, getTypeWithoutSignatures)); } + return type; + } - function hasContextSensitiveReturnExpression(node: ts.FunctionLikeDeclaration) { - // TODO(anhans): A block should be context-sensitive if it has a context-sensitive return value. - return !node.typeParameters && !ts.getEffectiveReturnTypeNode(node) && !!node.body && node.body.kind !== ts.SyntaxKind.Block && isContextSensitive(node.body); - } + // TYPE CHECKING - function isContextSensitiveFunctionOrObjectLiteralMethod(func: ts.Node): func is ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration { - return (ts.isInJSFile(func) && ts.isFunctionDeclaration(func) || ts.isFunctionExpressionOrArrowFunction(func) || ts.isObjectLiteralMethod(func)) && - isContextSensitiveFunctionLikeDeclaration(func); - } + function isTypeIdenticalTo(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, identityRelation); + } - function getTypeWithoutSignatures(type: ts.Type): ts.Type { - if (type.flags & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - if (resolved.constructSignatures.length || resolved.callSignatures.length) { - const result = createObjectType(ts.ObjectFlags.Anonymous, type.symbol); - result.members = resolved.members; - result.properties = resolved.properties; - result.callSignatures = ts.emptyArray; - result.constructSignatures = ts.emptyArray; - result.indexInfos = ts.emptyArray; - return result; - } - } - else if (type.flags & ts.TypeFlags.Intersection) { - return getIntersectionType(ts.map((type as ts.IntersectionType).types, getTypeWithoutSignatures)); - } - return type; - } + function compareTypesIdentical(source: ts.Type, target: ts.Type): ts.Ternary { + return isTypeRelatedTo(source, target, identityRelation) ? ts.Ternary.True : ts.Ternary.False; + } - // TYPE CHECKING + function compareTypesAssignable(source: ts.Type, target: ts.Type): ts.Ternary { + return isTypeRelatedTo(source, target, assignableRelation) ? ts.Ternary.True : ts.Ternary.False; + } - function isTypeIdenticalTo(source: ts.Type, target: ts.Type): boolean { - return isTypeRelatedTo(source, target, identityRelation); - } + function compareTypesSubtypeOf(source: ts.Type, target: ts.Type): ts.Ternary { + return isTypeRelatedTo(source, target, subtypeRelation) ? ts.Ternary.True : ts.Ternary.False; + } - function compareTypesIdentical(source: ts.Type, target: ts.Type): ts.Ternary { - return isTypeRelatedTo(source, target, identityRelation) ? ts.Ternary.True : ts.Ternary.False; - } + function isTypeSubtypeOf(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, subtypeRelation); + } - function compareTypesAssignable(source: ts.Type, target: ts.Type): ts.Ternary { - return isTypeRelatedTo(source, target, assignableRelation) ? ts.Ternary.True : ts.Ternary.False; - } + function isTypeAssignableTo(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, assignableRelation); + } - function compareTypesSubtypeOf(source: ts.Type, target: ts.Type): ts.Ternary { - return isTypeRelatedTo(source, target, subtypeRelation) ? ts.Ternary.True : ts.Ternary.False; - } + // 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 & ts.TypeFlags.Union ? ts.every((source as ts.UnionType).types, t => isTypeDerivedFrom(t, target)) : + target.flags & ts.TypeFlags.Union ? ts.some((target as ts.UnionType).types, t => isTypeDerivedFrom(source, t)) : + source.flags & ts.TypeFlags.InstantiableNonPrimitive ? isTypeDerivedFrom(getBaseConstraintOfType(source) || unknownType, target) : + target === globalObjectType ? !!(source.flags & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive)) : + target === globalFunctionType ? !!(source.flags & ts.TypeFlags.Object) && isFunctionObjectType(source as ts.ObjectType) : + hasBaseType(source, getTargetType(target)) || (isArrayType(target) && !isReadonlyArrayType(target) && isTypeDerivedFrom(source, globalReadonlyArrayType)); + } - function isTypeSubtypeOf(source: ts.Type, target: ts.Type): boolean { - return isTypeRelatedTo(source, target, subtypeRelation); - } + /** + * 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 isTypeAssignableTo(source: ts.Type, target: ts.Type): boolean { - return isTypeRelatedTo(source, target, assignableRelation); - } + function areTypesComparable(type1: ts.Type, type2: ts.Type): boolean { + return isTypeComparableTo(type1, type2) || isTypeComparableTo(type2, type1); + } - // 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 & ts.TypeFlags.Union ? ts.every((source as ts.UnionType).types, t => isTypeDerivedFrom(t, target)) : - target.flags & ts.TypeFlags.Union ? ts.some((target as ts.UnionType).types, t => isTypeDerivedFrom(source, t)) : - source.flags & ts.TypeFlags.InstantiableNonPrimitive ? isTypeDerivedFrom(getBaseConstraintOfType(source) || unknownType, target) : - target === globalObjectType ? !!(source.flags & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive)) : - target === globalFunctionType ? !!(source.flags & ts.TypeFlags.Object) && isFunctionObjectType(source as ts.ObjectType) : - hasBaseType(source, getTargetType(target)) || (isArrayType(target) && !isReadonlyArrayType(target) && isTypeDerivedFrom(source, globalReadonlyArrayType)); - } + function checkTypeAssignableTo(source: ts.Type, target: ts.Type, errorNode: ts.Node | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined, errorOutputObject?: { + errors?: ts.Diagnostic[]; + }): boolean { + return checkTypeRelatedTo(source, target, assignableRelation, errorNode, headMessage, containingMessageChain, errorOutputObject); + } - /** - * 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); - } + /** + * 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: ts.Node | undefined, expr: ts.Expression | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined): boolean { + return checkTypeRelatedToAndOptionallyElaborate(source, target, assignableRelation, errorNode, expr, headMessage, containingMessageChain, /*errorOutputContainer*/ undefined); + } - function areTypesComparable(type1: ts.Type, type2: ts.Type): boolean { - return isTypeComparableTo(type1, type2) || isTypeComparableTo(type2, type1); + function checkTypeRelatedToAndOptionallyElaborate(source: ts.Type, target: ts.Type, relation: ts.ESMap, errorNode: ts.Node | undefined, expr: ts.Expression | undefined, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.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 checkTypeAssignableTo(source: ts.Type, target: ts.Type, errorNode: ts.Node | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined, errorOutputObject?: { - errors?: ts.Diagnostic[]; - }): boolean { - return checkTypeRelatedTo(source, target, assignableRelation, errorNode, headMessage, containingMessageChain, errorOutputObject); - } + function isOrHasGenericConditional(type: ts.Type): boolean { + return !!(type.flags & ts.TypeFlags.Conditional || (type.flags & ts.TypeFlags.Intersection && ts.some((type as ts.IntersectionType).types, isOrHasGenericConditional))); + } - /** - * 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: ts.Node | undefined, expr: ts.Expression | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined): boolean { - return checkTypeRelatedToAndOptionallyElaborate(source, target, assignableRelation, errorNode, expr, headMessage, containingMessageChain, /*errorOutputContainer*/ undefined); + function elaborateError(node: ts.Expression | undefined, source: ts.Type, target: ts.Type, relation: ts.ESMap, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.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; } - - function checkTypeRelatedToAndOptionallyElaborate(source: ts.Type, target: ts.Type, relation: ts.ESMap, errorNode: ts.Node | undefined, expr: ts.Expression | undefined, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined): boolean { - if (isTypeRelatedTo(source, target, relation)) + switch (node.kind) { + case ts.SyntaxKind.JsxExpression: + case ts.SyntaxKind.ParenthesizedExpression: + return elaborateError((node as ts.ParenthesizedExpression | ts.JsxExpression).expression, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); + case ts.SyntaxKind.BinaryExpression: + switch ((node as ts.BinaryExpression).operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.CommaToken: + return elaborateError((node as ts.BinaryExpression).right, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); + } + break; + case ts.SyntaxKind.ObjectLiteralExpression: + return elaborateObjectLiteral(node as ts.ObjectLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); + case ts.SyntaxKind.ArrayLiteralExpression: + return elaborateArrayLiteral(node as ts.ArrayLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); + case ts.SyntaxKind.JsxAttributes: + return elaborateJsxComponents(node as ts.JsxAttributes, source, target, relation, containingMessageChain, errorOutputContainer); + case ts.SyntaxKind.ArrowFunction: + return elaborateArrowFunction(node as ts.ArrowFunction, source, target, relation, containingMessageChain, errorOutputContainer); + } + return false; + } + function elaborateDidYouMeanToCallOrConstruct(node: ts.Expression, source: ts.Type, target: ts.Type, relation: ts.ESMap, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + const callSignatures = getSignaturesOfType(source, ts.SignatureKind.Call); + const constructSignatures = getSignaturesOfType(source, ts.SignatureKind.Construct); + for (const signatures of [constructSignatures, callSignatures]) { + if (ts.some(signatures, s => { + const returnType = getReturnTypeOfSignature(s); + return !(returnType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never)) && checkTypeRelatedTo(returnType, target, relation, /*errorNode*/ undefined); + })) { + const resultObj: { + errors?: ts.Diagnostic[]; + } = errorOutputContainer || {}; + checkTypeAssignableTo(source, target, node, headMessage, containingMessageChain, resultObj); + const diagnostic = resultObj.errors![resultObj.errors!.length - 1]; + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(node, signatures === constructSignatures ? ts.Diagnostics.Did_you_mean_to_use_new_with_this_expression : ts.Diagnostics.Did_you_mean_to_call_this_expression)); return true; - if (!errorNode || !elaborateError(expr, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) { - return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer); } - return false; } + return false; + } - function isOrHasGenericConditional(type: ts.Type): boolean { - return !!(type.flags & ts.TypeFlags.Conditional || (type.flags & ts.TypeFlags.Intersection && ts.some((type as ts.IntersectionType).types, isOrHasGenericConditional))); + function elaborateArrowFunction(node: ts.ArrowFunction, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + // Don't elaborate blocks + if (ts.isBlock(node.body)) { + return false; } - - function elaborateError(node: ts.Expression | undefined, source: ts.Type, target: ts.Type, relation: ts.ESMap, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.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 ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.ParenthesizedExpression: - return elaborateError((node as ts.ParenthesizedExpression | ts.JsxExpression).expression, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); - case ts.SyntaxKind.BinaryExpression: - switch ((node as ts.BinaryExpression).operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.CommaToken: - return elaborateError((node as ts.BinaryExpression).right, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); - } - break; - case ts.SyntaxKind.ObjectLiteralExpression: - return elaborateObjectLiteral(node as ts.ObjectLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); - case ts.SyntaxKind.ArrayLiteralExpression: - return elaborateArrayLiteral(node as ts.ArrayLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); - case ts.SyntaxKind.JsxAttributes: - return elaborateJsxComponents(node as ts.JsxAttributes, source, target, relation, containingMessageChain, errorOutputContainer); - case ts.SyntaxKind.ArrowFunction: - return elaborateArrowFunction(node as ts.ArrowFunction, source, target, relation, containingMessageChain, errorOutputContainer); - } + // Or functions with annotated parameter types + if (ts.some(node.parameters, ts.hasType)) { return false; } - function elaborateDidYouMeanToCallOrConstruct(node: ts.Expression, source: ts.Type, target: ts.Type, relation: ts.ESMap, headMessage: ts.DiagnosticMessage | undefined, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined): boolean { - const callSignatures = getSignaturesOfType(source, ts.SignatureKind.Call); - const constructSignatures = getSignaturesOfType(source, ts.SignatureKind.Construct); - for (const signatures of [constructSignatures, callSignatures]) { - if (ts.some(signatures, s => { - const returnType = getReturnTypeOfSignature(s); - return !(returnType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never)) && checkTypeRelatedTo(returnType, target, relation, /*errorNode*/ undefined); - })) { - const resultObj: { - errors?: ts.Diagnostic[]; - } = errorOutputContainer || {}; - checkTypeAssignableTo(source, target, node, headMessage, containingMessageChain, resultObj); - const diagnostic = resultObj.errors![resultObj.errors!.length - 1]; - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(node, signatures === constructSignatures ? ts.Diagnostics.Did_you_mean_to_use_new_with_this_expression : ts.Diagnostics.Did_you_mean_to_call_this_expression)); - return true; - } - } + const sourceSig = getSingleCallSignature(source); + if (!sourceSig) { return false; } - - function elaborateArrowFunction(node: ts.ArrowFunction, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined): boolean { - // Don't elaborate blocks - if (ts.isBlock(node.body)) { - return false; - } - // Or functions with annotated parameter types - if (ts.some(node.parameters, ts.hasType)) { - return false; - } - const sourceSig = getSingleCallSignature(source); - if (!sourceSig) { - return false; - } - const targetSignatures = getSignaturesOfType(target, ts.SignatureKind.Call); - if (!ts.length(targetSignatures)) { - return false; - } - const returnExpression = node.body; - const sourceReturn = getReturnTypeOfSignature(sourceSig); - const targetReturn = getUnionType(ts.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?: ts.Diagnostic[]; - } = errorOutputContainer || {}; - checkTypeRelatedTo(sourceReturn, targetReturn, relation, returnExpression, /*message*/ undefined, containingMessageChain, resultObj); - if (resultObj.errors) { - if (target.symbol && ts.length(target.symbol.declarations)) { - ts.addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], ts.createDiagnosticForNode(target.symbol.declarations![0], ts.Diagnostics.The_expected_type_comes_from_the_return_type_of_this_signature)); - } - if ((ts.getFunctionFlags(node) & ts.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 ts.__String) - && checkTypeRelatedTo(createPromiseType(sourceReturn), targetReturn, relation, /*errorNode*/ undefined)) { - ts.addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], ts.createDiagnosticForNode(node, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async)); - } - return true; - } - } + const targetSignatures = getSignaturesOfType(target, ts.SignatureKind.Call); + if (!ts.length(targetSignatures)) { return false; } - - function getBestMatchIndexedAccessTypeOrUndefined(source: ts.Type, target: ts.Type, nameType: ts.Type) { - const idx = getIndexedAccessTypeOrUndefined(target, nameType); - if (idx) { - return idx; + const returnExpression = node.body; + const sourceReturn = getReturnTypeOfSignature(sourceSig); + const targetReturn = getUnionType(ts.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; } - if (target.flags & ts.TypeFlags.Union) { - const best = getBestMatchingType(source, target as ts.UnionType); - if (best) { - return getIndexedAccessTypeOrUndefined(best, nameType); + const resultObj: { + errors?: ts.Diagnostic[]; + } = errorOutputContainer || {}; + checkTypeRelatedTo(sourceReturn, targetReturn, relation, returnExpression, /*message*/ undefined, containingMessageChain, resultObj); + if (resultObj.errors) { + if (target.symbol && ts.length(target.symbol.declarations)) { + ts.addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], ts.createDiagnosticForNode(target.symbol.declarations![0], ts.Diagnostics.The_expected_type_comes_from_the_return_type_of_this_signature)); + } + if ((ts.getFunctionFlags(node) & ts.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 ts.__String) + && checkTypeRelatedTo(createPromiseType(sourceReturn), targetReturn, relation, /*errorNode*/ undefined)) { + ts.addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], ts.createDiagnosticForNode(node, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async)); } + return true; } } + return false; + } - function checkExpressionForMutableLocationWithContextualType(next: ts.Expression, sourcePropType: ts.Type) { - next.contextualType = sourcePropType; - try { - return checkExpressionForMutableLocation(next, CheckMode.Contextual, sourcePropType); - } - finally { - next.contextualType = undefined; + function getBestMatchIndexedAccessTypeOrUndefined(source: ts.Type, target: ts.Type, nameType: ts.Type) { + const idx = getIndexedAccessTypeOrUndefined(target, nameType); + if (idx) { + return idx; + } + if (target.flags & ts.TypeFlags.Union) { + const best = getBestMatchingType(source, target as ts.UnionType); + if (best) { + return getIndexedAccessTypeOrUndefined(best, nameType); } } + } - type ElaborationIterator = IterableIterator<{ - errorNode: ts.Node; - innerExpression: ts.Expression | undefined; - nameType: ts.Type; - errorMessage?: ts.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.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.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; - let targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType); - if (!targetPropType || targetPropType.flags & ts.TypeFlags.IndexedAccess) - continue; // Don't elaborate on indexes on generic variables - let sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType); - if (!sourcePropType) - continue; - const propName = getPropertyNameFromIndex(nameType, /*accessNode*/ undefined); - if (!checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) { - const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); - reportedError = true; - if (!elaborated) { - // Issue error on the prop itself, since the prop couldn't elaborate the error - const resultObj: { - errors?: ts.Diagnostic[]; - } = errorOutputContainer || {}; - // Use the expression type, if available - const specificSource = next ? checkExpressionForMutableLocationWithContextualType(next, sourcePropType) : sourcePropType; - if (exactOptionalPropertyTypes && isExactOptionalPropertyMismatch(specificSource, targetPropType)) { - const diag = ts.createDiagnosticForNode(prop, ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, typeToString(specificSource), typeToString(targetPropType)); - diagnostics.add(diag); - resultObj.errors = [diag]; + function checkExpressionForMutableLocationWithContextualType(next: ts.Expression, sourcePropType: ts.Type) { + next.contextualType = sourcePropType; + try { + return checkExpressionForMutableLocation(next, CheckMode.Contextual, sourcePropType); + } + finally { + next.contextualType = undefined; + } + } + + type ElaborationIterator = IterableIterator<{ + errorNode: ts.Node; + innerExpression: ts.Expression | undefined; + nameType: ts.Type; + errorMessage?: ts.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.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.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; + let targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType); + if (!targetPropType || targetPropType.flags & ts.TypeFlags.IndexedAccess) + continue; // Don't elaborate on indexes on generic variables + let sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType); + if (!sourcePropType) + continue; + const propName = getPropertyNameFromIndex(nameType, /*accessNode*/ undefined); + if (!checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) { + const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); + reportedError = true; + if (!elaborated) { + // Issue error on the prop itself, since the prop couldn't elaborate the error + const resultObj: { + errors?: ts.Diagnostic[]; + } = errorOutputContainer || {}; + // Use the expression type, if available + const specificSource = next ? checkExpressionForMutableLocationWithContextualType(next, sourcePropType) : sourcePropType; + if (exactOptionalPropertyTypes && isExactOptionalPropertyMismatch(specificSource, targetPropType)) { + const diag = ts.createDiagnosticForNode(prop, ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, typeToString(specificSource), typeToString(targetPropType)); + diagnostics.add(diag); + resultObj.errors = [diag]; + } + else { + const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & ts.SymbolFlags.Optional); + const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & ts.SymbolFlags.Optional); + targetPropType = removeMissingType(targetPropType, targetIsOptional); + sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional); + 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); } - else { - const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & ts.SymbolFlags.Optional); - const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & ts.SymbolFlags.Optional); - targetPropType = removeMissingType(targetPropType, targetIsOptional); - sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional); - 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 = getApplicableIndexInfo(target, nameType); + if (indexInfo && indexInfo.declaration && !ts.getSourceFileOfNode(indexInfo.declaration).hasNoDefaultLib) { + issuedElaboration = true; + ts.addRelatedInfo(reportedDiag, ts.createDiagnosticForNode(indexInfo.declaration, ts.Diagnostics.The_expected_type_comes_from_this_index_signature)); } } - 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 = getApplicableIndexInfo(target, nameType); - if (indexInfo && indexInfo.declaration && !ts.getSourceFileOfNode(indexInfo.declaration).hasNoDefaultLib) { - issuedElaboration = true; - ts.addRelatedInfo(reportedDiag, ts.createDiagnosticForNode(indexInfo.declaration, ts.Diagnostics.The_expected_type_comes_from_this_index_signature)); - } - } - if (!issuedElaboration && (targetProp && ts.length(targetProp.declarations) || target.symbol && ts.length(target.symbol.declarations))) { - const targetNode = targetProp && ts.length(targetProp.declarations) ? targetProp.declarations![0] : target.symbol.declarations![0]; - if (!ts.getSourceFileOfNode(targetNode).hasNoDefaultLib) { - ts.addRelatedInfo(reportedDiag, ts.createDiagnosticForNode(targetNode, ts.Diagnostics.The_expected_type_comes_from_property_0_which_is_declared_here_on_type_1, propertyName && !(nameType.flags & ts.TypeFlags.UniqueESSymbol) ? ts.unescapeLeadingUnderscores(propertyName) : typeToString(nameType), typeToString(target))); - } + if (!issuedElaboration && (targetProp && ts.length(targetProp.declarations) || target.symbol && ts.length(target.symbol.declarations))) { + const targetNode = targetProp && ts.length(targetProp.declarations) ? targetProp.declarations![0] : target.symbol.declarations![0]; + if (!ts.getSourceFileOfNode(targetNode).hasNoDefaultLib) { + ts.addRelatedInfo(reportedDiag, ts.createDiagnosticForNode(targetNode, ts.Diagnostics.The_expected_type_comes_from_property_0_which_is_declared_here_on_type_1, propertyName && !(nameType.flags & ts.TypeFlags.UniqueESSymbol) ? ts.unescapeLeadingUnderscores(propertyName) : typeToString(nameType), typeToString(target))); } } } } } - return reportedError; } + return reportedError; + } - function* generateJsxAttributes(node: ts.JsxAttributes): ElaborationIterator { - if (!ts.length(node.properties)) - return; - for (const prop of node.properties) { - if (ts.isJsxSpreadAttribute(prop) || isHyphenatedJsxName(ts.idText(prop.name))) - continue; - yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getStringLiteralType(ts.idText(prop.name)) }; - } + function* generateJsxAttributes(node: ts.JsxAttributes): ElaborationIterator { + if (!ts.length(node.properties)) + return; + for (const prop of node.properties) { + if (ts.isJsxSpreadAttribute(prop) || isHyphenatedJsxName(ts.idText(prop.name))) + continue; + yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getStringLiteralType(ts.idText(prop.name)) }; } + } - function* generateJsxChildren(node: ts.JsxElement, getInvalidTextDiagnostic: () => ts.DiagnosticMessage): ElaborationIterator { - if (!ts.length(node.children)) - return; - let memberOffset = 0; - for (let i = 0; i < node.children.length; i++) { - const child = node.children[i]; - const nameType = getNumberLiteralType(i - memberOffset); - const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic); - if (elem) { - yield elem; - } - else { - memberOffset++; - } + function* generateJsxChildren(node: ts.JsxElement, getInvalidTextDiagnostic: () => ts.DiagnosticMessage): ElaborationIterator { + if (!ts.length(node.children)) + return; + let memberOffset = 0; + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const nameType = getNumberLiteralType(i - memberOffset); + const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic); + if (elem) { + yield elem; + } + else { + memberOffset++; } } + } - function getElaborationElementForJsxChild(child: ts.JsxChild, nameType: ts.LiteralType, getInvalidTextDiagnostic: () => ts.DiagnosticMessage) { - switch (child.kind) { - case ts.SyntaxKind.JsxExpression: - // child is of the type of the expression - return { errorNode: child, innerExpression: child.expression, nameType }; - case ts.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 ts.SyntaxKind.JsxElement: - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxFragment: - // child is of type JSX.Element - return { errorNode: child, innerExpression: child, nameType }; - default: - return ts.Debug.assertNever(child, "Found invalid jsx child"); - } + function getElaborationElementForJsxChild(child: ts.JsxChild, nameType: ts.LiteralType, getInvalidTextDiagnostic: () => ts.DiagnosticMessage) { + switch (child.kind) { + case ts.SyntaxKind.JsxExpression: + // child is of the type of the expression + return { errorNode: child, innerExpression: child.expression, nameType }; + case ts.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 ts.SyntaxKind.JsxElement: + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxFragment: + // child is of type JSX.Element + return { errorNode: child, innerExpression: child, nameType }; + default: + return ts.Debug.assertNever(child, "Found invalid jsx child"); } + } - function elaborateJsxComponents(node: ts.JsxAttributes, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined) { - let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation, containingMessageChain, errorOutputContainer); - let invalidTextDiagnostic: ts.DiagnosticMessage | undefined; - if (ts.isJsxOpeningElement(node.parent) && ts.isJsxElement(node.parent.parent)) { - const containingElement = node.parent.parent; - const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - const childrenPropName = childPropName === undefined ? "children" : ts.unescapeLeadingUnderscores(childPropName); - const childrenNameType = getStringLiteralType(childrenPropName); - const childrenTargetType = getIndexedAccessType(target, childrenNameType); - const validChildren = ts.getSemanticJsxChildren(containingElement.children); - if (!ts.length(validChildren)) { - return result; - } - const moreThanOneRealChildren = ts.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, ts.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); - } + function elaborateJsxComponents(node: ts.JsxAttributes, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation, containingMessageChain, errorOutputContainer); + let invalidTextDiagnostic: ts.DiagnosticMessage | undefined; + if (ts.isJsxOpeningElement(node.parent) && ts.isJsxElement(node.parent.parent)) { + const containingElement = node.parent.parent; + const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + const childrenPropName = childPropName === undefined ? "children" : ts.unescapeLeadingUnderscores(childPropName); + const childrenNameType = getStringLiteralType(childrenPropName); + const childrenTargetType = getIndexedAccessType(target, childrenNameType); + const validChildren = ts.getSemanticJsxChildren(containingElement.children); + if (!ts.length(validChildren)) { + return result; + } + const moreThanOneRealChildren = ts.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, ts.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, ts.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); - } + } + 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; } } - } - return result; - - function getInvalidTextualChildDiagnostic() { - if (!invalidTextDiagnostic) { - const tagNameText = ts.getTextOfNode(node.parent.tagName); - const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - const childrenPropName = childPropName === undefined ? "children" : ts.unescapeLeadingUnderscores(childPropName); - const childrenTargetType = getIndexedAccessType(target, getStringLiteralType(childrenPropName)); - const diagnostic = ts.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: ts.formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) }; + else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) { + // arity mismatch + result = true; + const diag = error(containingElement.openingElement.tagName, ts.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 invalidTextDiagnostic; } } + return result; - function* generateLimitedTupleElements(node: ts.ArrayLiteralExpression, target: ts.Type): ElaborationIterator { - const len = ts.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 ts.__String)) - continue; - const elem = node.elements[i]; - if (ts.isOmittedExpression(elem)) - continue; - const nameType = getNumberLiteralType(i); - yield { errorNode: elem, innerExpression: elem, nameType }; + function getInvalidTextualChildDiagnostic() { + if (!invalidTextDiagnostic) { + const tagNameText = ts.getTextOfNode(node.parent.tagName); + const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + const childrenPropName = childPropName === undefined ? "children" : ts.unescapeLeadingUnderscores(childPropName); + const childrenTargetType = getIndexedAccessType(target, getStringLiteralType(childrenPropName)); + const diagnostic = ts.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: ts.formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) }; } + return invalidTextDiagnostic; } + } - function elaborateArrayLiteral(node: ts.ArrayLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined) { - if (target.flags & ts.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* generateLimitedTupleElements(node: ts.ArrayLiteralExpression, target: ts.Type): ElaborationIterator { + const len = ts.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 ts.__String)) + continue; + const elem = node.elements[i]; + if (ts.isOmittedExpression(elem)) + continue; + const nameType = getNumberLiteralType(i); + yield { errorNode: elem, innerExpression: elem, nameType }; } + } - function* generateObjectLiteralElements(node: ts.ObjectLiteralExpression): ElaborationIterator { - if (!ts.length(node.properties)) - return; - for (const prop of node.properties) { - if (ts.isSpreadAssignment(prop)) - continue; - const type = getLiteralTypeFromProperty(getSymbolOfNode(prop), ts.TypeFlags.StringOrNumberLiteralOrUnique); - if (!type || (type.flags & ts.TypeFlags.Never)) { - continue; - } - switch (prop.kind) { - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.ShorthandPropertyAssignment: - yield { errorNode: prop.name, innerExpression: undefined, nameType: type }; - break; - case ts.SyntaxKind.PropertyAssignment: - yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: type, errorMessage: ts.isComputedNonLiteralName(prop.name) ? ts.Diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1 : undefined }; - break; - default: - ts.Debug.assertNever(prop); - } + function elaborateArrayLiteral(node: ts.ArrayLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + if (target.flags & ts.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; } - - function elaborateObjectLiteral(node: ts.ObjectLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } | undefined) { - if (target.flags & ts.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: ts.Node, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined): boolean { - return checkTypeRelatedTo(source, target, comparableRelation, errorNode, headMessage, containingMessageChain); + finally { + node.contextualType = oldContext; } + } - 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) !== ts.Ternary.False; + function* generateObjectLiteralElements(node: ts.ObjectLiteralExpression): ElaborationIterator { + if (!ts.length(node.properties)) + return; + for (const prop of node.properties) { + if (ts.isSpreadAssignment(prop)) + continue; + const type = getLiteralTypeFromProperty(getSymbolOfNode(prop), ts.TypeFlags.StringOrNumberLiteralOrUnique); + if (!type || (type.flags & ts.TypeFlags.Never)) { + continue; + } + switch (prop.kind) { + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.ShorthandPropertyAssignment: + yield { errorNode: prop.name, innerExpression: undefined, nameType: type }; + break; + case ts.SyntaxKind.PropertyAssignment: + yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: type, errorMessage: ts.isComputedNonLiteralName(prop.name) ? ts.Diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1 : undefined }; + break; + default: + ts.Debug.assertNever(prop); + } } + } - type ErrorReporter = (message: ts.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)); - } + function elaborateObjectLiteral(node: ts.ObjectLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.ESMap, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + if (target.flags & ts.TypeFlags.Primitive) + return false; + return elaborateElementwise(generateObjectLiteralElements(node), source, target, relation, containingMessageChain, errorOutputContainer); + } - /** - * 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: ts.TypeComparer, reportUnreliableMarkers: ts.TypeMapper | undefined): ts.Ternary { - // TODO (drosen): De-duplicate code between related functions. - if (source === target) { - return ts.Ternary.True; - } + /** + * 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: ts.Node, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined): boolean { + return checkTypeRelatedTo(source, target, comparableRelation, errorNode, headMessage, containingMessageChain); + } - if (isAnySignature(target)) { - return ts.Ternary.True; - } + 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) !== ts.Ternary.False; + } - const targetCount = getParameterCount(target); - const sourceHasMoreParameters = !hasEffectiveRestParameter(target) && - (checkMode & SignatureCheckMode.StrictArity ? hasEffectiveRestParameter(source) || getParameterCount(source) > targetCount : getMinArgumentCount(source) > targetCount); - if (sourceHasMoreParameters) { - return ts.Ternary.False; - } + type ErrorReporter = (message: ts.DiagnosticMessage, arg0?: string, arg1?: string) => void; - if (source.typeParameters && source.typeParameters !== target.typeParameters) { - target = getCanonicalSignature(target); - source = instantiateSignatureInContextOf(source, target, /*inferenceContext*/ undefined, compareTypes); - } + /** + * 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)); + } - const sourceCount = getParameterCount(source); - const sourceRestType = getNonArrayRestType(source); - const targetRestType = getNonArrayRestType(target); - if (sourceRestType || targetRestType) { - void instantiateType(sourceRestType || targetRestType, reportUnreliableMarkers); - } + /** + * 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: ts.TypeComparer, reportUnreliableMarkers: ts.TypeMapper | undefined): ts.Ternary { + // TODO (drosen): De-duplicate code between related functions. + if (source === target) { + return ts.Ternary.True; + } - const kind = target.declaration ? target.declaration.kind : ts.SyntaxKind.Unknown; - const strictVariance = !(checkMode & SignatureCheckMode.Callback) && strictFunctionTypes && kind !== ts.SyntaxKind.MethodDeclaration && - kind !== ts.SyntaxKind.MethodSignature && kind !== ts.SyntaxKind.Constructor; - let result = ts.Ternary.True; + if (isAnySignature(target)) { + return ts.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!(ts.Diagnostics.The_this_types_of_each_signature_are_incompatible); - } - return ts.Ternary.False; + const targetCount = getParameterCount(target); + const sourceHasMoreParameters = !hasEffectiveRestParameter(target) && + (checkMode & SignatureCheckMode.StrictArity ? hasEffectiveRestParameter(source) || getParameterCount(source) > targetCount : getMinArgumentCount(source) > targetCount); + if (sourceHasMoreParameters) { + return ts.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); + } + + const kind = target.declaration ? target.declaration.kind : ts.SyntaxKind.Unknown; + const strictVariance = !(checkMode & SignatureCheckMode.Callback) && strictFunctionTypes && kind !== ts.SyntaxKind.MethodDeclaration && + kind !== ts.SyntaxKind.MethodSignature && kind !== ts.SyntaxKind.Constructor; + let result = ts.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!(ts.Diagnostics.The_this_types_of_each_signature_are_incompatible); } - result &= related; + return ts.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) : tryGetTypeAtPosition(source, i); - const targetType = i === restIndex ? getRestTypeAtPosition(target, i) : tryGetTypeAtPosition(target, i); - if (sourceType && targetType) { - // 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) & ts.TypeFlags.Nullable) === (getFalsyFlags(targetType) & ts.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 = ts.Ternary.False; - } - if (!related) { - if (reportErrors) { - errorReporter!(ts.Diagnostics.Types_of_parameters_0_and_1_are_incompatible, ts.unescapeLeadingUnderscores(getParameterNameAtPosition(source, i)), ts.unescapeLeadingUnderscores(getParameterNameAtPosition(target, i))); - } - return ts.Ternary.False; + 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) : tryGetTypeAtPosition(source, i); + const targetType = i === restIndex ? getRestTypeAtPosition(target, i) : tryGetTypeAtPosition(target, i); + if (sourceType && targetType) { + // 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) & ts.TypeFlags.Nullable) === (getFalsyFlags(targetType) & ts.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 = ts.Ternary.False; + } + if (!related) { + if (reportErrors) { + errorReporter!(ts.Diagnostics.Types_of_parameters_0_and_1_are_incompatible, ts.unescapeLeadingUnderscores(getParameterNameAtPosition(source, i)), ts.unescapeLeadingUnderscores(getParameterNameAtPosition(target, i))); } - result &= related; + return ts.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 || targetReturnType === anyType) { - return result; - } - const sourceReturnType = isResolvingReturnTypeOfSignature(source) ? anyType - : source.declaration && isJSConstructor(source.declaration) ? getDeclaredTypeOfClassOrInterface(getMergedSymbol(source.declaration.symbol)) - : getReturnTypeOfSignature(source); + 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 || targetReturnType === anyType) { + 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 (ts.isIdentifierTypePredicate(targetTypePredicate)) { - if (reportErrors) { - errorReporter!(ts.Diagnostics.Signature_0_must_be_a_type_predicate, signatureToString(source)); - } - return ts.Ternary.False; - } + // 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 { - // 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); + else if (ts.isIdentifierTypePredicate(targetTypePredicate)) { + if (reportErrors) { + errorReporter!(ts.Diagnostics.Signature_0_must_be_a_type_predicate, signatureToString(source)); } + return ts.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: ts.TypePredicate, target: ts.TypePredicate, reportErrors: boolean, errorReporter: ErrorReporter | undefined, compareTypes: (s: ts.Type, t: ts.Type, reportErrors?: boolean) => ts.Ternary): ts.Ternary { - if (source.kind !== target.kind) { + return result; + } + + function compareTypePredicateRelatedTo(source: ts.TypePredicate, target: ts.TypePredicate, reportErrors: boolean, errorReporter: ErrorReporter | undefined, compareTypes: (s: ts.Type, t: ts.Type, reportErrors?: boolean) => ts.Ternary): ts.Ternary { + if (source.kind !== target.kind) { + if (reportErrors) { + errorReporter!(ts.Diagnostics.A_this_based_type_guard_is_not_compatible_with_a_parameter_based_type_guard); + errorReporter!(ts.Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); + } + return ts.Ternary.False; + } + + if (source.kind === ts.TypePredicateKind.Identifier || source.kind === ts.TypePredicateKind.AssertsIdentifier) { + if (source.parameterIndex !== (target as ts.IdentifierTypePredicate).parameterIndex) { if (reportErrors) { - errorReporter!(ts.Diagnostics.A_this_based_type_guard_is_not_compatible_with_a_parameter_based_type_guard); + errorReporter!(ts.Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, source.parameterName, (target as ts.IdentifierTypePredicate).parameterName); errorReporter!(ts.Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); } return ts.Ternary.False; } + } - if (source.kind === ts.TypePredicateKind.Identifier || source.kind === ts.TypePredicateKind.AssertsIdentifier) { - if (source.parameterIndex !== (target as ts.IdentifierTypePredicate).parameterIndex) { - if (reportErrors) { - errorReporter!(ts.Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, source.parameterName, (target as ts.IdentifierTypePredicate).parameterName); - errorReporter!(ts.Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); - } - return ts.Ternary.False; - } - } + const related = source.type === target.type ? ts.Ternary.True : + source.type && target.type ? compareTypes(source.type, target.type, reportErrors) : + ts.Ternary.False; + if (related === ts.Ternary.False && reportErrors) { + errorReporter!(ts.Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); + } + return related; + } - const related = source.type === target.type ? ts.Ternary.True : - source.type && target.type ? compareTypes(source.type, target.type, reportErrors) : - ts.Ternary.False; - if (related === ts.Ternary.False && reportErrors) { - errorReporter!(ts.Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); - } - 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); } - function isImplementationCompatibleWithOverload(implementation: ts.Signature, overload: ts.Signature): boolean { - const erasedSource = getErasedSignature(implementation); - const erasedTarget = getErasedSignature(overload); + return false; + } - // 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)) { + function isEmptyResolvedType(t: ts.ResolvedType) { + return t !== anyFunctionType && + t.properties.length === 0 && + t.callSignatures.length === 0 && + t.constructSignatures.length === 0 && + t.indexInfos.length === 0; + } - return isSignatureAssignableTo(erasedSource, erasedTarget, /*ignoreReturnTypes*/ true); - } + function isEmptyObjectType(type: ts.Type): boolean { + return type.flags & ts.TypeFlags.Object ? !isGenericMappedType(type) && isEmptyResolvedType(resolveStructuredTypeMembers(type as ts.ObjectType)) : + type.flags & ts.TypeFlags.NonPrimitive ? true : + type.flags & ts.TypeFlags.Union ? ts.some((type as ts.UnionType).types, isEmptyObjectType) : + type.flags & ts.TypeFlags.Intersection ? ts.every((type as ts.UnionType).types, isEmptyObjectType) : + false; + } - return false; - } + function isEmptyAnonymousObjectType(type: ts.Type) { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && ((type as ts.ResolvedType).members && isEmptyResolvedType(type as ts.ResolvedType) || + type.symbol && type.symbol.flags & ts.SymbolFlags.TypeLiteral && getMembersOfSymbol(type.symbol).size === 0)); + } - function isEmptyResolvedType(t: ts.ResolvedType) { - return t !== anyFunctionType && - t.properties.length === 0 && - t.callSignatures.length === 0 && - t.constructSignatures.length === 0 && - t.indexInfos.length === 0; - } + function isStringIndexSignatureOnlyType(type: ts.Type): boolean { + return type.flags & ts.TypeFlags.Object && !isGenericMappedType(type) && getPropertiesOfType(type).length === 0 && getIndexInfosOfType(type).length === 1 && !!getIndexInfoOfType(type, stringType) || + type.flags & ts.TypeFlags.UnionOrIntersection && ts.every((type as ts.UnionOrIntersectionType).types, isStringIndexSignatureOnlyType) || + false; + } - function isEmptyObjectType(type: ts.Type): boolean { - return type.flags & ts.TypeFlags.Object ? !isGenericMappedType(type) && isEmptyResolvedType(resolveStructuredTypeMembers(type as ts.ObjectType)) : - type.flags & ts.TypeFlags.NonPrimitive ? true : - type.flags & ts.TypeFlags.Union ? ts.some((type as ts.UnionType).types, isEmptyObjectType) : - type.flags & ts.TypeFlags.Intersection ? ts.every((type as ts.UnionType).types, isEmptyObjectType) : - false; + function isEnumTypeRelatedTo(sourceSymbol: ts.Symbol, targetSymbol: ts.Symbol, errorReporter?: ErrorReporter) { + if (sourceSymbol === targetSymbol) { + return true; } - - function isEmptyAnonymousObjectType(type: ts.Type) { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && ((type as ts.ResolvedType).members && isEmptyResolvedType(type as ts.ResolvedType) || - type.symbol && type.symbol.flags & ts.SymbolFlags.TypeLiteral && getMembersOfSymbol(type.symbol).size === 0)); + const id = getSymbolId(sourceSymbol) + "," + getSymbolId(targetSymbol); + const entry = enumRelation.get(id); + if (entry !== undefined && !(!(entry & ts.RelationComparisonResult.Reported) && entry & ts.RelationComparisonResult.Failed && errorReporter)) { + return !!(entry & ts.RelationComparisonResult.Succeeded); } - - function isStringIndexSignatureOnlyType(type: ts.Type): boolean { - return type.flags & ts.TypeFlags.Object && !isGenericMappedType(type) && getPropertiesOfType(type).length === 0 && getIndexInfosOfType(type).length === 1 && !!getIndexInfoOfType(type, stringType) || - type.flags & ts.TypeFlags.UnionOrIntersection && ts.every((type as ts.UnionOrIntersectionType).types, isStringIndexSignatureOnlyType) || - false; + if (sourceSymbol.escapedName !== targetSymbol.escapedName || !(sourceSymbol.flags & ts.SymbolFlags.RegularEnum) || !(targetSymbol.flags & ts.SymbolFlags.RegularEnum)) { + enumRelation.set(id, ts.RelationComparisonResult.Failed | ts.RelationComparisonResult.Reported); + return false; } - - function isEnumTypeRelatedTo(sourceSymbol: ts.Symbol, targetSymbol: ts.Symbol, errorReporter?: ErrorReporter) { - if (sourceSymbol === targetSymbol) { - return true; - } - const id = getSymbolId(sourceSymbol) + "," + getSymbolId(targetSymbol); - const entry = enumRelation.get(id); - if (entry !== undefined && !(!(entry & ts.RelationComparisonResult.Reported) && entry & ts.RelationComparisonResult.Failed && errorReporter)) { - return !!(entry & ts.RelationComparisonResult.Succeeded); - } - if (sourceSymbol.escapedName !== targetSymbol.escapedName || !(sourceSymbol.flags & ts.SymbolFlags.RegularEnum) || !(targetSymbol.flags & ts.SymbolFlags.RegularEnum)) { - enumRelation.set(id, ts.RelationComparisonResult.Failed | ts.RelationComparisonResult.Reported); - return false; - } - const targetEnumType = getTypeOfSymbol(targetSymbol); - for (const property of getPropertiesOfType(getTypeOfSymbol(sourceSymbol))) { - if (property.flags & ts.SymbolFlags.EnumMember) { - const targetProperty = getPropertyOfType(targetEnumType, property.escapedName); - if (!targetProperty || !(targetProperty.flags & ts.SymbolFlags.EnumMember)) { - if (errorReporter) { - errorReporter(ts.Diagnostics.Property_0_is_missing_in_type_1, ts.symbolName(property), typeToString(getDeclaredTypeOfSymbol(targetSymbol), /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.UseFullyQualifiedType)); - enumRelation.set(id, ts.RelationComparisonResult.Failed | ts.RelationComparisonResult.Reported); - } - else { - enumRelation.set(id, ts.RelationComparisonResult.Failed); - } - return false; + const targetEnumType = getTypeOfSymbol(targetSymbol); + for (const property of getPropertiesOfType(getTypeOfSymbol(sourceSymbol))) { + if (property.flags & ts.SymbolFlags.EnumMember) { + const targetProperty = getPropertyOfType(targetEnumType, property.escapedName); + if (!targetProperty || !(targetProperty.flags & ts.SymbolFlags.EnumMember)) { + if (errorReporter) { + errorReporter(ts.Diagnostics.Property_0_is_missing_in_type_1, ts.symbolName(property), typeToString(getDeclaredTypeOfSymbol(targetSymbol), /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.UseFullyQualifiedType)); + enumRelation.set(id, ts.RelationComparisonResult.Failed | ts.RelationComparisonResult.Reported); + } + else { + enumRelation.set(id, ts.RelationComparisonResult.Failed); } + return false; } } - enumRelation.set(id, ts.RelationComparisonResult.Succeeded); - return true; } + enumRelation.set(id, ts.RelationComparisonResult.Succeeded); + return true; + } - function isSimpleTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.ESMap, errorReporter?: ErrorReporter) { - const s = source.flags; - const t = target.flags; - if (t & ts.TypeFlags.AnyOrUnknown || s & ts.TypeFlags.Never || source === wildcardType) - return true; - if (t & ts.TypeFlags.Never) - return false; - if (s & ts.TypeFlags.StringLike && t & ts.TypeFlags.String) - return true; - if (s & ts.TypeFlags.StringLiteral && s & ts.TypeFlags.EnumLiteral && - t & ts.TypeFlags.StringLiteral && !(t & ts.TypeFlags.EnumLiteral) && - (source as ts.StringLiteralType).value === (target as ts.StringLiteralType).value) - return true; - if (s & ts.TypeFlags.NumberLike && t & ts.TypeFlags.Number) - return true; - if (s & ts.TypeFlags.NumberLiteral && s & ts.TypeFlags.EnumLiteral && - t & ts.TypeFlags.NumberLiteral && !(t & ts.TypeFlags.EnumLiteral) && - (source as ts.NumberLiteralType).value === (target as ts.NumberLiteralType).value) - return true; - if (s & ts.TypeFlags.BigIntLike && t & ts.TypeFlags.BigInt) - return true; - if (s & ts.TypeFlags.BooleanLike && t & ts.TypeFlags.Boolean) - return true; - if (s & ts.TypeFlags.ESSymbolLike && t & ts.TypeFlags.ESSymbol) - return true; - if (s & ts.TypeFlags.Enum && t & ts.TypeFlags.Enum && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) + function isSimpleTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.ESMap, errorReporter?: ErrorReporter) { + const s = source.flags; + const t = target.flags; + if (t & ts.TypeFlags.AnyOrUnknown || s & ts.TypeFlags.Never || source === wildcardType) + return true; + if (t & ts.TypeFlags.Never) + return false; + if (s & ts.TypeFlags.StringLike && t & ts.TypeFlags.String) + return true; + if (s & ts.TypeFlags.StringLiteral && s & ts.TypeFlags.EnumLiteral && + t & ts.TypeFlags.StringLiteral && !(t & ts.TypeFlags.EnumLiteral) && + (source as ts.StringLiteralType).value === (target as ts.StringLiteralType).value) + return true; + if (s & ts.TypeFlags.NumberLike && t & ts.TypeFlags.Number) + return true; + if (s & ts.TypeFlags.NumberLiteral && s & ts.TypeFlags.EnumLiteral && + t & ts.TypeFlags.NumberLiteral && !(t & ts.TypeFlags.EnumLiteral) && + (source as ts.NumberLiteralType).value === (target as ts.NumberLiteralType).value) + return true; + if (s & ts.TypeFlags.BigIntLike && t & ts.TypeFlags.BigInt) + return true; + if (s & ts.TypeFlags.BooleanLike && t & ts.TypeFlags.Boolean) + return true; + if (s & ts.TypeFlags.ESSymbolLike && t & ts.TypeFlags.ESSymbol) + return true; + if (s & ts.TypeFlags.Enum && t & ts.TypeFlags.Enum && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) + return true; + if (s & ts.TypeFlags.EnumLiteral && t & ts.TypeFlags.EnumLiteral) { + if (s & ts.TypeFlags.Union && t & ts.TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; - if (s & ts.TypeFlags.EnumLiteral && t & ts.TypeFlags.EnumLiteral) { - if (s & ts.TypeFlags.Union && t & ts.TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) - return true; - if (s & ts.TypeFlags.Literal && t & ts.TypeFlags.Literal && - (source as ts.LiteralType).value === (target as ts.LiteralType).value && - isEnumTypeRelatedTo(getParentOfSymbol(source.symbol)!, getParentOfSymbol(target.symbol)!, errorReporter)) - return true; - } - // In non-strictNullChecks mode, `undefined` and `null` are assignable to anything except `never`. - // Since unions and intersections may reduce to `never`, we exclude them here. - if (s & ts.TypeFlags.Undefined && (!strictNullChecks && !(t & ts.TypeFlags.UnionOrIntersection) || t & (ts.TypeFlags.Undefined | ts.TypeFlags.Void))) + if (s & ts.TypeFlags.Literal && t & ts.TypeFlags.Literal && + (source as ts.LiteralType).value === (target as ts.LiteralType).value && + isEnumTypeRelatedTo(getParentOfSymbol(source.symbol)!, getParentOfSymbol(target.symbol)!, errorReporter)) return true; - if (s & ts.TypeFlags.Null && (!strictNullChecks && !(t & ts.TypeFlags.UnionOrIntersection) || t & ts.TypeFlags.Null)) + } + // In non-strictNullChecks mode, `undefined` and `null` are assignable to anything except `never`. + // Since unions and intersections may reduce to `never`, we exclude them here. + if (s & ts.TypeFlags.Undefined && (!strictNullChecks && !(t & ts.TypeFlags.UnionOrIntersection) || t & (ts.TypeFlags.Undefined | ts.TypeFlags.Void))) + return true; + if (s & ts.TypeFlags.Null && (!strictNullChecks && !(t & ts.TypeFlags.UnionOrIntersection) || t & ts.TypeFlags.Null)) + return true; + if (s & ts.TypeFlags.Object && t & ts.TypeFlags.NonPrimitive) + return true; + if (relation === assignableRelation || relation === comparableRelation) { + if (s & ts.TypeFlags.Any) return true; - if (s & ts.TypeFlags.Object && t & ts.TypeFlags.NonPrimitive) + // 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 & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral) && !(s & ts.TypeFlags.EnumLiteral) && (t & ts.TypeFlags.Enum || relation === assignableRelation && t & ts.TypeFlags.NumberLiteral && t & ts.TypeFlags.EnumLiteral)) return true; - if (relation === assignableRelation || relation === comparableRelation) { - if (s & ts.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 & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral) && !(s & ts.TypeFlags.EnumLiteral) && (t & ts.TypeFlags.Enum || relation === assignableRelation && t & ts.TypeFlags.NumberLiteral && t & ts.TypeFlags.EnumLiteral)) - return true; - } - return false; } + return false; + } - function isTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.ESMap) { - if (isFreshLiteralType(source)) { - source = (source as ts.FreshableType).regularType; - } - if (isFreshLiteralType(target)) { - target = (target as ts.FreshableType).regularType; - } - if (source === target) { + function isTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.ESMap) { + if (isFreshLiteralType(source)) { + source = (source as ts.FreshableType).regularType; + } + if (isFreshLiteralType(target)) { + target = (target as ts.FreshableType).regularType; + } + if (source === target) { + return true; + } + if (relation !== identityRelation) { + if (relation === comparableRelation && !(target.flags & ts.TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation)) { return true; } - if (relation !== identityRelation) { - if (relation === comparableRelation && !(target.flags & ts.TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation)) { - return true; - } - } - else if (!((source.flags | target.flags) & (ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional | ts.TypeFlags.Substitution))) { - // We have excluded types that may simplify to other forms, so types must have identical flags - if (source.flags !== target.flags) - return false; - if (source.flags & ts.TypeFlags.Singleton) - return true; - } - if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object) { - const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false)); - if (related !== undefined) { - return !!(related & ts.RelationComparisonResult.Succeeded); - } - } - if (source.flags & ts.TypeFlags.StructuredOrInstantiable || target.flags & ts.TypeFlags.StructuredOrInstantiable) { - return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined); + } + else if (!((source.flags | target.flags) & (ts.TypeFlags.UnionOrIntersection | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional | ts.TypeFlags.Substitution))) { + // We have excluded types that may simplify to other forms, so types must have identical flags + if (source.flags !== target.flags) + return false; + if (source.flags & ts.TypeFlags.Singleton) + return true; + } + if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object) { + const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false)); + if (related !== undefined) { + return !!(related & ts.RelationComparisonResult.Succeeded); } - return false; } - - function isIgnoredJsxProperty(source: ts.Type, sourceProp: ts.Symbol) { - return ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes && isHyphenatedJsxName(sourceProp.escapedName); + if (source.flags & ts.TypeFlags.StructuredOrInstantiable || target.flags & ts.TypeFlags.StructuredOrInstantiable) { + return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined); } + return false; + } - function getNormalizedType(type: ts.Type, writing: boolean): ts.Type { - while (true) { - let t = isFreshLiteralType(type) ? (type as ts.FreshableType).regularType : - ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).node ? createTypeReference((type as ts.TypeReference).target, getTypeArguments(type as ts.TypeReference)) : - type.flags & ts.TypeFlags.UnionOrIntersection ? getReducedType(type) : - type.flags & ts.TypeFlags.Substitution ? writing ? (type as ts.SubstitutionType).baseType : (type as ts.SubstitutionType).substitute : - type.flags & ts.TypeFlags.Simplifiable ? getSimplifiedType(type, writing) : - type; - t = getSingleBaseForNonAugmentingSubtype(t) || t; - if (t === type) - break; - type = t; - } - return type; + function isIgnoredJsxProperty(source: ts.Type, sourceProp: ts.Symbol) { + return ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes && isHyphenatedJsxName(sourceProp.escapedName); + } + + function getNormalizedType(type: ts.Type, writing: boolean): ts.Type { + while (true) { + let t = isFreshLiteralType(type) ? (type as ts.FreshableType).regularType : + ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).node ? createTypeReference((type as ts.TypeReference).target, getTypeArguments(type as ts.TypeReference)) : + type.flags & ts.TypeFlags.UnionOrIntersection ? getReducedType(type) : + type.flags & ts.TypeFlags.Substitution ? writing ? (type as ts.SubstitutionType).baseType : (type as ts.SubstitutionType).substitute : + type.flags & ts.TypeFlags.Simplifiable ? getSimplifiedType(type, writing) : + type; + t = getSingleBaseForNonAugmentingSubtype(t) || t; + if (t === type) + break; + type = t; } + 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.ESMap, errorNode: ts.Node | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined, errorOutputContainer?: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - }): boolean { - let errorInfo: ts.DiagnosticMessageChain | undefined; - let relatedInfo: [ - ts.DiagnosticRelatedInformation, - ...ts.DiagnosticRelatedInformation[] - ] | undefined; - let maybeKeys: string[]; - let sourceStack: ts.Type[]; - let targetStack: ts.Type[]; - let maybeCount = 0; - let sourceDepth = 0; - let targetDepth = 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: [ - ts.DiagnosticMessage, - (string | number)?, - (string | number)?, - (string | number)?, - (string | number)? - ][] | undefined; - let inPropertyCheck = false; + /** + * 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.ESMap, errorNode: ts.Node | undefined, headMessage?: ts.DiagnosticMessage, containingMessageChain?: () => ts.DiagnosticMessageChain | undefined, errorOutputContainer?: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + }): boolean { + let errorInfo: ts.DiagnosticMessageChain | undefined; + let relatedInfo: [ + ts.DiagnosticRelatedInformation, + ...ts.DiagnosticRelatedInformation[] + ] | undefined; + let maybeKeys: string[]; + let sourceStack: ts.Type[]; + let targetStack: ts.Type[]; + let maybeCount = 0; + let sourceDepth = 0; + let targetDepth = 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: [ + ts.DiagnosticMessage, + (string | number)?, + (string | number)?, + (string | number)?, + (string | number)? + ][] | undefined; + let inPropertyCheck = false; - ts.Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking"); + ts.Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking"); - const result = isRelatedTo(source, target, RecursionFlags.Both, /*reportErrors*/ !!errorNode, headMessage); - if (incompatibleStack) { - reportIncompatibleStack(); + const result = isRelatedTo(source, target, RecursionFlags.Both, /*reportErrors*/ !!errorNode, headMessage); + if (incompatibleStack) { + reportIncompatibleStack(); + } + if (overflow) { + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "checkTypeRelatedTo_DepthLimit", { sourceId: source.id, targetId: target.id, depth: sourceDepth, targetDepth }); + const diag = error(errorNode || currentNode, ts.Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target)); + if (errorOutputContainer) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); } - if (overflow) { - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "checkTypeRelatedTo_DepthLimit", { sourceId: source.id, targetId: target.id, depth: sourceDepth, targetDepth }); - const diag = error(errorNode || currentNode, ts.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) { + ts.concatenateDiagnosticMessageChains(chain, errorInfo); + errorInfo = chain; } } - else if (errorInfo) { - if (containingMessageChain) { - const chain = containingMessageChain(); - if (chain) { - ts.concatenateDiagnosticMessageChains(chain, errorInfo); - errorInfo = chain; - } - } - let relatedInformation: ts.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 && !ts.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 = ts.createDiagnosticForNode(links.originatingImport, ts.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 = ts.append(relatedInformation, diag); // Cause the error to appear with the error that triggered it - } + let relatedInformation: ts.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 && !ts.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 = ts.createDiagnosticForNode(links.originatingImport, ts.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 = ts.append(relatedInformation, diag); // Cause the error to appear with the error that triggered it } } - const diag = ts.createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo, relatedInformation); - if (relatedInfo) { - ts.addRelatedInfo(diag, ...relatedInfo); - } - if (errorOutputContainer) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - if (!errorOutputContainer || !errorOutputContainer.skipLogging) { - diagnostics.add(diag); - } } - if (errorNode && errorOutputContainer && errorOutputContainer.skipLogging && result === ts.Ternary.False) { - ts.Debug.assert(!!errorOutputContainer.errors, "missed opportunity to interact with error."); + const diag = ts.createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo, relatedInformation); + if (relatedInfo) { + ts.addRelatedInfo(diag, ...relatedInfo); + } + if (errorOutputContainer) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); + } + if (!errorOutputContainer || !errorOutputContainer.skipLogging) { + diagnostics.add(diag); } + } + if (errorNode && errorOutputContainer && errorOutputContainer.skipLogging && result === ts.Ternary.False) { + ts.Debug.assert(!!errorOutputContainer.errors, "missed opportunity to interact with error."); + } - return result !== ts.Ternary.False; + return result !== ts.Ternary.False; - function resetErrorInfo(saved: ReturnType) { - errorInfo = saved.errorInfo; - lastSkippedInfo = saved.lastSkippedInfo; - incompatibleStack = saved.incompatibleStack; - overrideNextErrorInfo = saved.overrideNextErrorInfo; - relatedInfo = saved.relatedInfo; - } + 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?.slice() as [ - ts.DiagnosticRelatedInformation, - ...ts.DiagnosticRelatedInformation[] - ] | undefined, - }; - } + function captureErrorCalculationState() { + return { + errorInfo, + lastSkippedInfo, + incompatibleStack: incompatibleStack?.slice(), + overrideNextErrorInfo, + relatedInfo: relatedInfo?.slice() as [ + ts.DiagnosticRelatedInformation, + ...ts.DiagnosticRelatedInformation[] + ] | undefined, + }; + } - function reportIncompatibleError(message: ts.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 reportIncompatibleError(message: ts.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 = undefined; - 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; + function reportIncompatibleStack() { + const stack = incompatibleStack || []; + incompatibleStack = undefined; + const info = lastSkippedInfo; + lastSkippedInfo = undefined; + if (stack.length === 1) { + reportError(...stack[0]); + if (info) { + // Actually do the last relation error + reportRelationError(/*headMessage*/ undefined, ...info); } - // 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: [ - ts.DiagnosticMessage, - (string | number)?, - (string | number)?, - (string | number)?, - (string | number)? - ][] = []; - while (stack.length) { - const [msg, ...args] = stack.pop()!; - switch (msg.code) { - case ts.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 (ts.isIdentifierText(str, ts.getEmitScriptTarget(compilerOptions))) { - 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; + 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: [ + ts.DiagnosticMessage, + (string | number)?, + (string | number)?, + (string | number)?, + (string | number)? + ][] = []; + while (stack.length) { + const [msg, ...args] = stack.pop()!; + switch (msg.code) { + case ts.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 (ts.isIdentifierText(str, ts.getEmitScriptTarget(compilerOptions))) { + 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}]`; } - case ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible.code: - case ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code: - case ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: - case ts.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 === ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { - mappedMsg = ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible; - } - else if (msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { - mappedMsg = ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible; - } - secondaryRootErrors.unshift([mappedMsg, args[0], args[1]]); + break; + } + case ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible.code: + case ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code: + case ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: + case ts.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 === ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { + mappedMsg = ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible; } - else { - const prefix = (msg.code === ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code || - msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) - ? "new " - : ""; - const params = (msg.code === ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code || - msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) - ? "" - : "..."; - path = `${prefix}${path}(${params})`; + else if (msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { + mappedMsg = ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible; } - break; - } - case ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target.code: { - secondaryRootErrors.unshift([ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target, args[0], args[1]]); - break; + secondaryRootErrors.unshift([mappedMsg, args[0], args[1]]); } - case ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target.code: { - secondaryRootErrors.unshift([ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target, args[0], args[1], args[2]]); - break; + else { + const prefix = (msg.code === ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code || + msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) + ? "new " + : ""; + const params = (msg.code === ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code || + msg.code === ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) + ? "" + : "..."; + path = `${prefix}${path}(${params})`; } - default: - return ts.Debug.fail(`Unhandled Diagnostic: ${msg.code}`); + break; } + case ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target.code: { + secondaryRootErrors.unshift([ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target, args[0], args[1]]); + break; + } + case ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target.code: { + secondaryRootErrors.unshift([ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target, args[0], args[1], args[2]]); + break; + } + default: + return ts.Debug.fail(`Unhandled Diagnostic: ${msg.code}`); } - if (path) { - reportError(path[path.length - 1] === ")" - ? ts.Diagnostics.The_types_returned_by_0_are_incompatible_between_these_types - : ts.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; // Temporarily 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); - } } + if (path) { + reportError(path[path.length - 1] === ")" + ? ts.Diagnostics.The_types_returned_by_0_are_incompatible_between_these_types + : ts.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; // Temporarily 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: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { + ts.Debug.assert(!!errorNode); + if (incompatibleStack) + reportIncompatibleStack(); + if (message.elidedInCompatabilityPyramid) + return; + errorInfo = ts.chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3); + } + + function associateRelatedInfo(info: ts.DiagnosticRelatedInformation) { + ts.Debug.assert(!!errorInfo); + if (!relatedInfo) { + relatedInfo = [info]; + } + else { + relatedInfo.push(info); + } + } - function reportError(message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { - ts.Debug.assert(!!errorNode); - if (incompatibleStack) - reportIncompatibleStack(); - if (message.elidedInCompatabilityPyramid) - return; - errorInfo = ts.chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3); + function reportRelationError(message: ts.DiagnosticMessage | undefined, source: ts.Type, target: ts.Type) { + if (incompatibleStack) + reportIncompatibleStack(); + const [sourceType, targetType] = getTypeNamesForErrorDisplay(source, target); + let generalizedSource = source; + let generalizedSourceType = sourceType; + + if (isLiteralType(source) && !typeCouldHaveTopLevelSingletonTypes(target)) { + generalizedSource = getBaseTypeOfLiteralType(source); + ts.Debug.assert(!isTypeAssignableTo(generalizedSource, target), "generalized source shouldn't be assignable"); + generalizedSourceType = getTypeNameForErrorDisplay(generalizedSource); } - function associateRelatedInfo(info: ts.DiagnosticRelatedInformation) { - ts.Debug.assert(!!errorInfo); - if (!relatedInfo) { - relatedInfo = [info]; + if (target.flags & ts.TypeFlags.TypeParameter && target !== markerSuperType && target !== markerSubType) { + const constraint = getBaseConstraintOfType(target); + let needsOriginalSource; + if (constraint && (isTypeAssignableTo(generalizedSource, constraint) || (needsOriginalSource = isTypeAssignableTo(source, constraint)))) { + reportError(ts.Diagnostics._0_is_assignable_to_the_constraint_of_type_1_but_1_could_be_instantiated_with_a_different_subtype_of_constraint_2, needsOriginalSource ? sourceType : generalizedSourceType, targetType, typeToString(constraint)); } else { - relatedInfo.push(info); + errorInfo = undefined; + reportError(ts.Diagnostics._0_could_be_instantiated_with_an_arbitrary_type_which_could_be_unrelated_to_1, targetType, generalizedSourceType); } } - function reportRelationError(message: ts.DiagnosticMessage | undefined, source: ts.Type, target: ts.Type) { - if (incompatibleStack) - reportIncompatibleStack(); - const [sourceType, targetType] = getTypeNamesForErrorDisplay(source, target); - let generalizedSource = source; - let generalizedSourceType = sourceType; - - if (isLiteralType(source) && !typeCouldHaveTopLevelSingletonTypes(target)) { - generalizedSource = getBaseTypeOfLiteralType(source); - ts.Debug.assert(!isTypeAssignableTo(generalizedSource, target), "generalized source shouldn't be assignable"); - generalizedSourceType = getTypeNameForErrorDisplay(generalizedSource); + if (!message) { + if (relation === comparableRelation) { + message = ts.Diagnostics.Type_0_is_not_comparable_to_type_1; } - - if (target.flags & ts.TypeFlags.TypeParameter && target !== markerSuperType && target !== markerSubType) { - const constraint = getBaseConstraintOfType(target); - let needsOriginalSource; - if (constraint && (isTypeAssignableTo(generalizedSource, constraint) || (needsOriginalSource = isTypeAssignableTo(source, constraint)))) { - reportError(ts.Diagnostics._0_is_assignable_to_the_constraint_of_type_1_but_1_could_be_instantiated_with_a_different_subtype_of_constraint_2, needsOriginalSource ? sourceType : generalizedSourceType, targetType, typeToString(constraint)); - } - else { - errorInfo = undefined; - reportError(ts.Diagnostics._0_could_be_instantiated_with_an_arbitrary_type_which_could_be_unrelated_to_1, targetType, generalizedSourceType); - } + else if (sourceType === targetType) { + message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated; } - - if (!message) { - if (relation === comparableRelation) { - message = ts.Diagnostics.Type_0_is_not_comparable_to_type_1; - } - else if (sourceType === targetType) { - message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated; - } - else if (exactOptionalPropertyTypes && getExactOptionalUnassignableProperties(source, target).length) { - message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; - } - else { - if (source.flags & ts.TypeFlags.StringLiteral && target.flags & ts.TypeFlags.Union) { - const suggestedType = getSuggestedTypeForNonexistentStringLiteralType(source as ts.StringLiteralType, target as ts.UnionType); - if (suggestedType) { - reportError(ts.Diagnostics.Type_0_is_not_assignable_to_type_1_Did_you_mean_2, generalizedSourceType, targetType, typeToString(suggestedType)); - return; - } + else if (exactOptionalPropertyTypes && getExactOptionalUnassignableProperties(source, target).length) { + message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; + } + else { + if (source.flags & ts.TypeFlags.StringLiteral && target.flags & ts.TypeFlags.Union) { + const suggestedType = getSuggestedTypeForNonexistentStringLiteralType(source as ts.StringLiteralType, target as ts.UnionType); + if (suggestedType) { + reportError(ts.Diagnostics.Type_0_is_not_assignable_to_type_1_Did_you_mean_2, generalizedSourceType, targetType, typeToString(suggestedType)); + return; } - message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1; } + message = ts.Diagnostics.Type_0_is_not_assignable_to_type_1; } - else if (message === ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1 - && exactOptionalPropertyTypes - && getExactOptionalUnassignableProperties(source, target).length) { - message = ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; - } - - reportError(message, generalizedSourceType, targetType); } + else if (message === ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1 + && exactOptionalPropertyTypes + && getExactOptionalUnassignableProperties(source, target).length) { + message = ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties; + } + + reportError(message, generalizedSourceType, targetType); + } - 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); + 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(ts.Diagnostics._0_is_a_primitive_but_1_is_a_wrapper_object_Prefer_using_0_when_possible, targetType, sourceType); - } + if ((globalStringType === source && stringType === target) || + (globalNumberType === source && numberType === target) || + (globalBooleanType === source && booleanType === target) || + (getGlobalESSymbolType(/*reportErrors*/ false) === source && esSymbolType === target)) { + reportError(ts.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: ts.Type, target: ts.Type, reportErrors: boolean): boolean { /** - * 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. + * 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. */ - 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(ts.Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); - } - return false; - } - return isArrayOrTupleType(target); - } - if (isReadonlyArrayType(source) && isMutableArrayOrTuple(target)) { + if (isTupleType(source)) { + if (source.target.readonly && isMutableArrayOrTuple(target)) { if (reportErrors) { reportError(ts.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 isArrayOrTupleType(target); + } + if (isReadonlyArrayType(source) && isMutableArrayOrTuple(target)) { + if (reportErrors) { + reportError(ts.Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); } - return true; + return false; } - - function isRelatedToWorker(source: ts.Type, target: ts.Type, reportErrors: boolean) { - return isRelatedTo(source, target, RecursionFlags.Both, reportErrors); + 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: ts.Type, originalTarget: ts.Type, recursionFlags: RecursionFlags = RecursionFlags.Both, reportErrors = false, headMessage?: ts.DiagnosticMessage, intersectionState = IntersectionState.None): ts.Ternary { - // Before normalization: if `source` is type an object type, and `target` is primitive, - // skip all the checks we don't need and just return `isSimpleTypeRelatedTo` result - if (originalSource.flags & ts.TypeFlags.Object && originalTarget.flags & ts.TypeFlags.Primitive) { - if (isSimpleTypeRelatedTo(originalSource, originalTarget, relation, reportErrors ? reportError : undefined)) { - return ts.Ternary.True; - } - if (reportErrors) { - reportErrorResults(originalSource, originalTarget, originalSource, originalTarget, headMessage); - } - return ts.Ternary.False; + function isRelatedToWorker(source: ts.Type, target: ts.Type, reportErrors: boolean) { + return isRelatedTo(source, target, RecursionFlags.Both, reportErrors); + } + + /** + * 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: ts.Type, originalTarget: ts.Type, recursionFlags: RecursionFlags = RecursionFlags.Both, reportErrors = false, headMessage?: ts.DiagnosticMessage, intersectionState = IntersectionState.None): ts.Ternary { + // Before normalization: if `source` is type an object type, and `target` is primitive, + // skip all the checks we don't need and just return `isSimpleTypeRelatedTo` result + if (originalSource.flags & ts.TypeFlags.Object && originalTarget.flags & ts.TypeFlags.Primitive) { + if (isSimpleTypeRelatedTo(originalSource, originalTarget, relation, reportErrors ? reportError : undefined)) { + return ts.Ternary.True; } + if (reportErrors) { + reportErrorResults(originalSource, originalTarget, originalSource, originalTarget, headMessage); + } + return ts.Ternary.False; + } - // 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). - const source = getNormalizedType(originalSource, /*writing*/ false); - let target = getNormalizedType(originalTarget, /*writing*/ true); + // 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). + const source = getNormalizedType(originalSource, /*writing*/ false); + let target = getNormalizedType(originalTarget, /*writing*/ true); - if (source === target) + if (source === target) + return ts.Ternary.True; + + if (relation === identityRelation) { + if (source.flags !== target.flags) + return ts.Ternary.False; + if (source.flags & ts.TypeFlags.Singleton) return ts.Ternary.True; + traceUnionsOrIntersectionsTooLarge(source, target); + return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, IntersectionState.None, recursionFlags); + } - if (relation === identityRelation) { - if (source.flags !== target.flags) - return ts.Ternary.False; - if (source.flags & ts.TypeFlags.Singleton) - return ts.Ternary.True; - traceUnionsOrIntersectionsTooLarge(source, target); - return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, IntersectionState.None, recursionFlags); - } + // We fastpath comparing a type parameter to exactly its constraint, as this is _super_ common, + // and otherwise, for type parameters in large unions, causes us to need to compare the union to itself, + // as we break down the _target_ union first, _then_ get the source constraint - so for every + // member of the target, we attempt to find a match in the source. This avoids that in cases where + // the target is exactly the constraint. + if (source.flags & ts.TypeFlags.TypeParameter && getConstraintOfType(source) === target) { + return ts.Ternary.True; + } - // We fastpath comparing a type parameter to exactly its constraint, as this is _super_ common, - // and otherwise, for type parameters in large unions, causes us to need to compare the union to itself, - // as we break down the _target_ union first, _then_ get the source constraint - so for every - // member of the target, we attempt to find a match in the source. This avoids that in cases where - // the target is exactly the constraint. - if (source.flags & ts.TypeFlags.TypeParameter && getConstraintOfType(source) === target) { - return ts.Ternary.True; + // See if we're relating a definitely non-nullable type to a union that includes null and/or undefined + // plus a single non-nullable type. If so, remove null and/or undefined from the target type. + if (source.flags & ts.TypeFlags.DefinitelyNonNullable && target.flags & ts.TypeFlags.Union) { + const types = (target as ts.UnionType).types; + const candidate = types.length === 2 && types[0].flags & ts.TypeFlags.Nullable ? types[1] : + types.length === 3 && types[0].flags & ts.TypeFlags.Nullable && types[1].flags & ts.TypeFlags.Nullable ? types[2] : + undefined; + if (candidate && !(candidate.flags & ts.TypeFlags.Nullable)) { + target = getNormalizedType(candidate, /*writing*/ true); + if (source === target) + return ts.Ternary.True; } + } - // See if we're relating a definitely non-nullable type to a union that includes null and/or undefined - // plus a single non-nullable type. If so, remove null and/or undefined from the target type. - if (source.flags & ts.TypeFlags.DefinitelyNonNullable && target.flags & ts.TypeFlags.Union) { - const types = (target as ts.UnionType).types; - const candidate = types.length === 2 && types[0].flags & ts.TypeFlags.Nullable ? types[1] : - types.length === 3 && types[0].flags & ts.TypeFlags.Nullable && types[1].flags & ts.TypeFlags.Nullable ? types[2] : - undefined; - if (candidate && !(candidate.flags & ts.TypeFlags.Nullable)) { - target = getNormalizedType(candidate, /*writing*/ true); - if (source === target) - return ts.Ternary.True; + if (relation === comparableRelation && !(target.flags & ts.TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) + return ts.Ternary.True; + if (source.flags & ts.TypeFlags.StructuredOrInstantiable || target.flags & ts.TypeFlags.StructuredOrInstantiable) { + const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && ts.getObjectFlags(source) & ts.ObjectFlags.FreshLiteral); + if (isPerformingExcessPropertyChecks) { + if (hasExcessProperties(source as ts.FreshObjectLiteralType, target, reportErrors)) { + if (reportErrors) { + reportRelationError(headMessage, source, originalTarget.aliasSymbol ? originalTarget : target); + } + return ts.Ternary.False; } } - if (relation === comparableRelation && !(target.flags & ts.TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || - isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) - return ts.Ternary.True; - if (source.flags & ts.TypeFlags.StructuredOrInstantiable || target.flags & ts.TypeFlags.StructuredOrInstantiable) { - const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && ts.getObjectFlags(source) & ts.ObjectFlags.FreshLiteral); - if (isPerformingExcessPropertyChecks) { - if (hasExcessProperties(source as ts.FreshObjectLiteralType, target, reportErrors)) { - if (reportErrors) { - reportRelationError(headMessage, source, originalTarget.aliasSymbol ? originalTarget : target); - } - return ts.Ternary.False; + const isPerformingCommonPropertyChecks = relation !== comparableRelation && !(intersectionState & IntersectionState.Target) && + source.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.Object | ts.TypeFlags.Intersection) && source !== globalObjectType && + target.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && isWeakType(target) && + (getPropertiesOfType(source).length > 0 || typeHasCallOrConstructSignatures(source)); + const isComparingJsxAttributes = !!(ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes); + if (isPerformingCommonPropertyChecks && !hasCommonProperties(source, target, isComparingJsxAttributes)) { + if (reportErrors) { + const sourceString = typeToString(originalSource.aliasSymbol ? originalSource : source); + const targetString = typeToString(originalTarget.aliasSymbol ? originalTarget : target); + const calls = getSignaturesOfType(source, ts.SignatureKind.Call); + const constructs = getSignaturesOfType(source, ts.SignatureKind.Construct); + if (calls.length > 0 && isRelatedTo(getReturnTypeOfSignature(calls[0]), target, RecursionFlags.Source, /*reportErrors*/ false) || + constructs.length > 0 && isRelatedTo(getReturnTypeOfSignature(constructs[0]), target, RecursionFlags.Source, /*reportErrors*/ false)) { + reportError(ts.Diagnostics.Value_of_type_0_has_no_properties_in_common_with_type_1_Did_you_mean_to_call_it, sourceString, targetString); } - } - - const isPerformingCommonPropertyChecks = relation !== comparableRelation && !(intersectionState & IntersectionState.Target) && - source.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.Object | ts.TypeFlags.Intersection) && source !== globalObjectType && - target.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && isWeakType(target) && - (getPropertiesOfType(source).length > 0 || typeHasCallOrConstructSignatures(source)); - const isComparingJsxAttributes = !!(ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes); - if (isPerformingCommonPropertyChecks && !hasCommonProperties(source, target, isComparingJsxAttributes)) { - if (reportErrors) { - const sourceString = typeToString(originalSource.aliasSymbol ? originalSource : source); - const targetString = typeToString(originalTarget.aliasSymbol ? originalTarget : target); - const calls = getSignaturesOfType(source, ts.SignatureKind.Call); - const constructs = getSignaturesOfType(source, ts.SignatureKind.Construct); - if (calls.length > 0 && isRelatedTo(getReturnTypeOfSignature(calls[0]), target, RecursionFlags.Source, /*reportErrors*/ false) || - constructs.length > 0 && isRelatedTo(getReturnTypeOfSignature(constructs[0]), target, RecursionFlags.Source, /*reportErrors*/ false)) { - reportError(ts.Diagnostics.Value_of_type_0_has_no_properties_in_common_with_type_1_Did_you_mean_to_call_it, sourceString, targetString); - } - else { - reportError(ts.Diagnostics.Type_0_has_no_properties_in_common_with_type_1, sourceString, targetString); - } + else { + reportError(ts.Diagnostics.Type_0_has_no_properties_in_common_with_type_1, sourceString, targetString); } - return ts.Ternary.False; } + return ts.Ternary.False; + } - traceUnionsOrIntersectionsTooLarge(source, target); + traceUnionsOrIntersectionsTooLarge(source, target); - const skipCaching = source.flags & ts.TypeFlags.Union && (source as ts.UnionType).types.length < 4 && !(target.flags & ts.TypeFlags.Union) || - target.flags & ts.TypeFlags.Union && (target as ts.UnionType).types.length < 4 && !(source.flags & ts.TypeFlags.StructuredOrInstantiable); - let result = skipCaching ? - unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState) : - recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags); - // For certain combinations involving intersections and optional, excess, or mismatched properties we need - // an extra property check where the intersection is viewed as a single object. The following are motivating - // examples that all should be errors, but aren't without this extra property check: - // - // let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property - // - // declare let wrong: { a: { y: string } }; - // let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type - // - // function foo(x: { a?: string }, y: T & { a: boolean }) { - // x = y; // Mismatched property in source intersection - // } - // - // We suppress recursive intersection property checks because they can generate lots of work when relating - // recursive intersections that are structurally similar but not exactly identical. See #37854. - if (result && !inPropertyCheck && (target.flags & ts.TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) || - isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & ts.TypeFlags.Intersection && getApparentType(source).flags & ts.TypeFlags.StructuredType && !ts.some((source as ts.IntersectionType).types, t => !!(ts.getObjectFlags(t) & ts.ObjectFlags.NonInferrableType)))) { - inPropertyCheck = true; - result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck, recursionFlags); - inPropertyCheck = false; - } - if (result) { - return result; - } + const skipCaching = source.flags & ts.TypeFlags.Union && (source as ts.UnionType).types.length < 4 && !(target.flags & ts.TypeFlags.Union) || + target.flags & ts.TypeFlags.Union && (target as ts.UnionType).types.length < 4 && !(source.flags & ts.TypeFlags.StructuredOrInstantiable); + let result = skipCaching ? + unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState) : + recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags); + // For certain combinations involving intersections and optional, excess, or mismatched properties we need + // an extra property check where the intersection is viewed as a single object. The following are motivating + // examples that all should be errors, but aren't without this extra property check: + // + // let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property + // + // declare let wrong: { a: { y: string } }; + // let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type + // + // function foo(x: { a?: string }, y: T & { a: boolean }) { + // x = y; // Mismatched property in source intersection + // } + // + // We suppress recursive intersection property checks because they can generate lots of work when relating + // recursive intersections that are structurally similar but not exactly identical. See #37854. + if (result && !inPropertyCheck && (target.flags & ts.TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) || + isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & ts.TypeFlags.Intersection && getApparentType(source).flags & ts.TypeFlags.StructuredType && !ts.some((source as ts.IntersectionType).types, t => !!(ts.getObjectFlags(t) & ts.ObjectFlags.NonInferrableType)))) { + inPropertyCheck = true; + result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck, recursionFlags); + inPropertyCheck = false; } - - if (reportErrors) { - reportErrorResults(originalSource, originalTarget, source, target, headMessage); + if (result) { + return result; } - return ts.Ternary.False; } - function reportErrorResults(originalSource: ts.Type, originalTarget: ts.Type, source: ts.Type, target: ts.Type, headMessage: ts.DiagnosticMessage | undefined) { - const sourceHasBase = !!getSingleBaseForNonAugmentingSubtype(originalSource); - const targetHasBase = !!getSingleBaseForNonAugmentingSubtype(originalTarget); - source = (originalSource.aliasSymbol || sourceHasBase) ? originalSource : source; - target = (originalTarget.aliasSymbol || targetHasBase) ? originalTarget : target; - let maybeSuppress = overrideNextErrorInfo > 0; - if (maybeSuppress) { - overrideNextErrorInfo--; - } - if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object) { - const currentError = errorInfo; - tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ true); - if (errorInfo !== currentError) { - maybeSuppress = !!errorInfo; - } - } - if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Primitive) { - tryElaborateErrorsForPrimitivesAndObjects(source, target); - } - else if (source.symbol && source.flags & ts.TypeFlags.Object && globalObjectType === source) { - reportError(ts.Diagnostics.The_Object_type_is_assignable_to_very_few_other_types_Did_you_mean_to_use_the_any_type_instead); - } - else if (ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes && target.flags & ts.TypeFlags.Intersection) { - const targetTypes = (target as ts.IntersectionType).types; - const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes, errorNode); - const intrinsicClassAttributes = getJsxType(JsxNames.IntrinsicClassAttributes, errorNode); - if (!isErrorType(intrinsicAttributes) && !isErrorType(intrinsicClassAttributes) && - (ts.contains(targetTypes, intrinsicAttributes) || ts.contains(targetTypes, intrinsicClassAttributes))) { - // do not report top error - return; - } - } - else { - errorInfo = elaborateNeverIntersection(errorInfo, originalTarget); + if (reportErrors) { + reportErrorResults(originalSource, originalTarget, source, target, headMessage); + } + return ts.Ternary.False; + } + + function reportErrorResults(originalSource: ts.Type, originalTarget: ts.Type, source: ts.Type, target: ts.Type, headMessage: ts.DiagnosticMessage | undefined) { + const sourceHasBase = !!getSingleBaseForNonAugmentingSubtype(originalSource); + const targetHasBase = !!getSingleBaseForNonAugmentingSubtype(originalTarget); + source = (originalSource.aliasSymbol || sourceHasBase) ? originalSource : source; + target = (originalTarget.aliasSymbol || targetHasBase) ? originalTarget : target; + let maybeSuppress = overrideNextErrorInfo > 0; + if (maybeSuppress) { + overrideNextErrorInfo--; + } + if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object) { + const currentError = errorInfo; + tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ true); + if (errorInfo !== currentError) { + maybeSuppress = !!errorInfo; } - if (!headMessage && maybeSuppress) { - lastSkippedInfo = [source, target]; - // Used by, eg, missing property checking to replace the top-level message with a more informative one + } + if (source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Primitive) { + tryElaborateErrorsForPrimitivesAndObjects(source, target); + } + else if (source.symbol && source.flags & ts.TypeFlags.Object && globalObjectType === source) { + reportError(ts.Diagnostics.The_Object_type_is_assignable_to_very_few_other_types_Did_you_mean_to_use_the_any_type_instead); + } + else if (ts.getObjectFlags(source) & ts.ObjectFlags.JsxAttributes && target.flags & ts.TypeFlags.Intersection) { + const targetTypes = (target as ts.IntersectionType).types; + const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes, errorNode); + const intrinsicClassAttributes = getJsxType(JsxNames.IntrinsicClassAttributes, errorNode); + if (!isErrorType(intrinsicAttributes) && !isErrorType(intrinsicClassAttributes) && + (ts.contains(targetTypes, intrinsicAttributes) || ts.contains(targetTypes, intrinsicClassAttributes))) { + // do not report top error return; } - reportRelationError(headMessage, source, target); + } + else { + errorInfo = elaborateNeverIntersection(errorInfo, originalTarget); + } + if (!headMessage && maybeSuppress) { + lastSkippedInfo = [source, target]; + // Used by, eg, missing property checking to replace the top-level message with a more informative one + return; + } + reportRelationError(headMessage, source, target); + } + + function traceUnionsOrIntersectionsTooLarge(source: ts.Type, target: ts.Type): void { + if (!ts.tracing) { + return; } - function traceUnionsOrIntersectionsTooLarge(source: ts.Type, target: ts.Type): void { - if (!ts.tracing) { + if ((source.flags & ts.TypeFlags.UnionOrIntersection) && (target.flags & ts.TypeFlags.UnionOrIntersection)) { + const sourceUnionOrIntersection = source as ts.UnionOrIntersectionType; + const targetUnionOrIntersection = target as ts.UnionOrIntersectionType; + if (sourceUnionOrIntersection.objectFlags & targetUnionOrIntersection.objectFlags & ts.ObjectFlags.PrimitiveUnion) { + // There's a fast path for comparing primitive unions return; } - if ((source.flags & ts.TypeFlags.UnionOrIntersection) && (target.flags & ts.TypeFlags.UnionOrIntersection)) { - const sourceUnionOrIntersection = source as ts.UnionOrIntersectionType; - const targetUnionOrIntersection = target as ts.UnionOrIntersectionType; - if (sourceUnionOrIntersection.objectFlags & targetUnionOrIntersection.objectFlags & ts.ObjectFlags.PrimitiveUnion) { - // There's a fast path for comparing primitive unions - return; - } - - const sourceSize = sourceUnionOrIntersection.types.length; - const targetSize = targetUnionOrIntersection.types.length; - if (sourceSize * targetSize > 1E6) { - ts.tracing.instant(ts.tracing.Phase.CheckTypes, "traceUnionsOrIntersectionsTooLarge_DepthLimit", { - sourceId: source.id, - sourceSize, - targetId: target.id, - targetSize, - pos: errorNode?.pos, - end: errorNode?.end - }); - } + const sourceSize = sourceUnionOrIntersection.types.length; + const targetSize = targetUnionOrIntersection.types.length; + if (sourceSize * targetSize > 1E6) { + ts.tracing.instant(ts.tracing.Phase.CheckTypes, "traceUnionsOrIntersectionsTooLarge_DepthLimit", { + sourceId: source.id, + sourceSize, + targetId: target.id, + targetSize, + pos: errorNode?.pos, + end: errorNode?.end + }); } } + } - function getTypeOfPropertyInTypes(types: ts.Type[], name: ts.__String) { - const appendPropType = (propTypes: ts.Type[] | undefined, type: ts.Type) => { - type = getApparentType(type); - const prop = type.flags & ts.TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType(type as ts.UnionOrIntersectionType, name) : getPropertyOfObjectType(type, name); - const propType = prop && getTypeOfSymbol(prop) || getApplicableIndexInfoForName(type, name)?.type || undefinedType; - return ts.append(propTypes, propType); - }; - return getUnionType(ts.reduceLeft(types, appendPropType, /*initial*/ undefined) || ts.emptyArray); - } + function getTypeOfPropertyInTypes(types: ts.Type[], name: ts.__String) { + const appendPropType = (propTypes: ts.Type[] | undefined, type: ts.Type) => { + type = getApparentType(type); + const prop = type.flags & ts.TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType(type as ts.UnionOrIntersectionType, name) : getPropertyOfObjectType(type, name); + const propType = prop && getTypeOfSymbol(prop) || getApplicableIndexInfoForName(type, name)?.type || undefinedType; + return ts.append(propTypes, propType); + }; + return getUnionType(ts.reduceLeft(types, appendPropType, /*initial*/ undefined) || ts.emptyArray); + } - function hasExcessProperties(source: ts.FreshObjectLiteralType, target: ts.Type, reportErrors: boolean): boolean { - if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && ts.getObjectFlags(target) & ts.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 = !!(ts.getObjectFlags(source) & ts.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 & ts.TypeFlags.Union) { - reducedTarget = findMatchingDiscriminantType(source, target as ts.UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as ts.UnionType); - checkTypes = reducedTarget.flags & ts.TypeFlags.Union ? (reducedTarget as ts.UnionType).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 ts.Debug.fail(); - if (ts.isJsxAttributes(errorNode) || ts.isJsxOpeningLikeElement(errorNode) || ts.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. - if (prop.valueDeclaration && ts.isJsxAttribute(prop.valueDeclaration) && ts.getSourceFileOfNode(errorNode) === ts.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; - } - const propName = symbolToString(prop); - const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget); - const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined; - if (suggestion) { - reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion); - } - else { - reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget)); - } + function hasExcessProperties(source: ts.FreshObjectLiteralType, target: ts.Type, reportErrors: boolean): boolean { + if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && ts.getObjectFlags(target) & ts.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 = !!(ts.getObjectFlags(source) & ts.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 & ts.TypeFlags.Union) { + reducedTarget = findMatchingDiscriminantType(source, target as ts.UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as ts.UnionType); + checkTypes = reducedTarget.flags & ts.TypeFlags.Union ? (reducedTarget as ts.UnionType).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 ts.Debug.fail(); + if (ts.isJsxAttributes(errorNode) || ts.isJsxOpeningLikeElement(errorNode) || ts.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. + if (prop.valueDeclaration && ts.isJsxAttribute(prop.valueDeclaration) && ts.getSourceFileOfNode(errorNode) === ts.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; + } + const propName = symbolToString(prop); + const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget); + const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined; + if (suggestion) { + reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion); } else { - // use the property's value declaration if the property is assigned inside the literal itself - const objectLiteralDeclaration = source.symbol?.declarations && ts.firstOrUndefined(source.symbol.declarations); - let suggestion: string | undefined; - if (prop.valueDeclaration && ts.findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && ts.getSourceFileOfNode(objectLiteralDeclaration) === ts.getSourceFileOfNode(errorNode)) { - const propDeclaration = prop.valueDeclaration as ts.ObjectLiteralElementLike; - ts.Debug.assertNode(propDeclaration, ts.isObjectLiteralElementLike); - - errorNode = propDeclaration; - - const name = propDeclaration.name!; - if (ts.isIdentifier(name)) { - suggestion = getSuggestionForNonexistentProperty(name, errorTarget); - } - } - if (suggestion !== undefined) { - reportError(ts.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(ts.Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, symbolToString(prop), typeToString(errorTarget)); + reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget)); + } + } + else { + // use the property's value declaration if the property is assigned inside the literal itself + const objectLiteralDeclaration = source.symbol?.declarations && ts.firstOrUndefined(source.symbol.declarations); + let suggestion: string | undefined; + if (prop.valueDeclaration && ts.findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && ts.getSourceFileOfNode(objectLiteralDeclaration) === ts.getSourceFileOfNode(errorNode)) { + const propDeclaration = prop.valueDeclaration as ts.ObjectLiteralElementLike; + ts.Debug.assertNode(propDeclaration, ts.isObjectLiteralElementLike); + + errorNode = propDeclaration; + + const name = propDeclaration.name!; + if (ts.isIdentifier(name)) { + suggestion = getSuggestionForNonexistentProperty(name, errorTarget); } } + if (suggestion !== undefined) { + reportError(ts.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(ts.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), RecursionFlags.Both, reportErrors)) { - if (reportErrors) { - reportIncompatibleError(ts.Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop)); - } - return true; + return true; + } + if (checkTypes && !isRelatedTo(getTypeOfSymbol(prop), getTypeOfPropertyInTypes(checkTypes, prop.escapedName), RecursionFlags.Both, reportErrors)) { + if (reportErrors) { + reportIncompatibleError(ts.Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop)); } + return true; } } - return false; } + return false; + } - function shouldCheckAsExcessProperty(prop: ts.Symbol, container: ts.Symbol) { - return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration; - } + function shouldCheckAsExcessProperty(prop: ts.Symbol, container: ts.Symbol) { + return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration; + } - function unionOrIntersectionRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - // 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 & ts.TypeFlags.Union) { - return relation === comparableRelation ? - someTypeRelatedToType(source as ts.UnionType, target, reportErrors && !(source.flags & ts.TypeFlags.Primitive), intersectionState) : - eachTypeRelatedToType(source as ts.UnionType, target, reportErrors && !(source.flags & ts.TypeFlags.Primitive), intersectionState); - } - if (target.flags & ts.TypeFlags.Union) { - return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as ts.UnionType, reportErrors && !(source.flags & ts.TypeFlags.Primitive) && !(target.flags & ts.TypeFlags.Primitive)); - } - if (target.flags & ts.TypeFlags.Intersection) { - return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as ts.IntersectionType, reportErrors, IntersectionState.Target); - } - // Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the - // constraints of all non-primitive types in the source into a new intersection. We do this because the - // intersection may further constrain the constraints of the non-primitive types. For example, given a type - // parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't - // appear to be comparable to '2'. - if (relation === comparableRelation && target.flags & ts.TypeFlags.Primitive) { - const constraints = ts.sameMap((source as ts.IntersectionType).types, getBaseConstraintOrType); - if (constraints !== (source as ts.IntersectionType).types) { - source = getIntersectionType(constraints); - if (!(source.flags & ts.TypeFlags.Intersection)) { - return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false); - } + function unionOrIntersectionRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + // 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 & ts.TypeFlags.Union) { + return relation === comparableRelation ? + someTypeRelatedToType(source as ts.UnionType, target, reportErrors && !(source.flags & ts.TypeFlags.Primitive), intersectionState) : + eachTypeRelatedToType(source as ts.UnionType, target, reportErrors && !(source.flags & ts.TypeFlags.Primitive), intersectionState); + } + if (target.flags & ts.TypeFlags.Union) { + return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as ts.UnionType, reportErrors && !(source.flags & ts.TypeFlags.Primitive) && !(target.flags & ts.TypeFlags.Primitive)); + } + if (target.flags & ts.TypeFlags.Intersection) { + return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as ts.IntersectionType, reportErrors, IntersectionState.Target); + } + // Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the + // constraints of all non-primitive types in the source into a new intersection. We do this because the + // intersection may further constrain the constraints of the non-primitive types. For example, given a type + // parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't + // appear to be comparable to '2'. + if (relation === comparableRelation && target.flags & ts.TypeFlags.Primitive) { + const constraints = ts.sameMap((source as ts.IntersectionType).types, getBaseConstraintOrType); + if (constraints !== (source as ts.IntersectionType).types) { + source = getIntersectionType(constraints); + if (!(source.flags & ts.TypeFlags.Intersection)) { + return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false); } } - // Check to see if any constituents of the intersection are immediately related to the target. - // Don't report errors though. Elaborating on whether a source constituent is related to the target is - // not actually useful and leads to some confusing error messages. Instead, we rely on the caller - // checking whether the full intersection viewed as an object is related to the target. - return someTypeRelatedToType(source as ts.IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source); } + // Check to see if any constituents of the intersection are immediately related to the target. + // Don't report errors though. Elaborating on whether a source constituent is related to the target is + // not actually useful and leads to some confusing error messages. Instead, we rely on the caller + // checking whether the full intersection viewed as an object is related to the target. + return someTypeRelatedToType(source as ts.IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source); + } - function eachTypeRelatedToSomeType(source: ts.UnionOrIntersectionType, target: ts.UnionOrIntersectionType): ts.Ternary { - let result = ts.Ternary.True; - const sourceTypes = source.types; - for (const sourceType of sourceTypes) { - const related = typeRelatedToSomeType(sourceType, target, /*reportErrors*/ false); - if (!related) { - return ts.Ternary.False; - } - result &= related; + function eachTypeRelatedToSomeType(source: ts.UnionOrIntersectionType, target: ts.UnionOrIntersectionType): ts.Ternary { + let result = ts.Ternary.True; + const sourceTypes = source.types; + for (const sourceType of sourceTypes) { + const related = typeRelatedToSomeType(sourceType, target, /*reportErrors*/ false); + if (!related) { + return ts.Ternary.False; } - return result; + result &= related; } + return result; + } - function typeRelatedToSomeType(source: ts.Type, target: ts.UnionOrIntersectionType, reportErrors: boolean): ts.Ternary { - const targetTypes = target.types; - if (target.flags & ts.TypeFlags.Union) { - if (containsType(targetTypes, source)) { - return ts.Ternary.True; - } - const match = getMatchingUnionConstituentForType(target as ts.UnionType, source); - if (match) { - const related = isRelatedTo(source, match, RecursionFlags.Target, /*reportErrors*/ false); - if (related) { - return related; - } - } + function typeRelatedToSomeType(source: ts.Type, target: ts.UnionOrIntersectionType, reportErrors: boolean): ts.Ternary { + const targetTypes = target.types; + if (target.flags & ts.TypeFlags.Union) { + if (containsType(targetTypes, source)) { + return ts.Ternary.True; } - for (const type of targetTypes) { - const related = isRelatedTo(source, type, RecursionFlags.Target, /*reportErrors*/ false); + const match = getMatchingUnionConstituentForType(target as ts.UnionType, source); + if (match) { + const related = isRelatedTo(source, match, RecursionFlags.Target, /*reportErrors*/ false); if (related) { return related; } } - if (reportErrors) { - // Elaborate only if we can find a best matching type in the target union - const bestMatchingType = getBestMatchingType(source, target, isRelatedTo); - if (bestMatchingType) { - isRelatedTo(source, bestMatchingType, RecursionFlags.Target, /*reportErrors*/ true); - } + } + for (const type of targetTypes) { + const related = isRelatedTo(source, type, RecursionFlags.Target, /*reportErrors*/ false); + if (related) { + return related; + } + } + if (reportErrors) { + // Elaborate only if we can find a best matching type in the target union + const bestMatchingType = getBestMatchingType(source, target, isRelatedTo); + if (bestMatchingType) { + isRelatedTo(source, bestMatchingType, RecursionFlags.Target, /*reportErrors*/ true); } - return ts.Ternary.False; } + return ts.Ternary.False; + } - function typeRelatedToEachType(source: ts.Type, target: ts.IntersectionType, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - let result = ts.Ternary.True; - const targetTypes = target.types; - for (const targetType of targetTypes) { - const related = isRelatedTo(source, targetType, RecursionFlags.Target, reportErrors, /*headMessage*/ undefined, intersectionState); - if (!related) { - return ts.Ternary.False; - } - result &= related; + function typeRelatedToEachType(source: ts.Type, target: ts.IntersectionType, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + let result = ts.Ternary.True; + const targetTypes = target.types; + for (const targetType of targetTypes) { + const related = isRelatedTo(source, targetType, RecursionFlags.Target, reportErrors, /*headMessage*/ undefined, intersectionState); + if (!related) { + return ts.Ternary.False; + } + result &= related; + } + return result; + } + + function someTypeRelatedToType(source: ts.UnionOrIntersectionType, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + const sourceTypes = source.types; + if (source.flags & ts.TypeFlags.Union && containsType(sourceTypes, target)) { + return ts.Ternary.True; + } + const len = sourceTypes.length; + for (let i = 0; i < len; i++) { + const related = isRelatedTo(sourceTypes[i], target, RecursionFlags.Source, reportErrors && i === len - 1, /*headMessage*/ undefined, intersectionState); + if (related) { + return related; } - return result; } + return ts.Ternary.False; + } - function someTypeRelatedToType(source: ts.UnionOrIntersectionType, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - const sourceTypes = source.types; - if (source.flags & ts.TypeFlags.Union && containsType(sourceTypes, target)) { - return ts.Ternary.True; - } - const len = sourceTypes.length; - for (let i = 0; i < len; i++) { - const related = isRelatedTo(sourceTypes[i], target, RecursionFlags.Source, reportErrors && i === len - 1, /*headMessage*/ undefined, intersectionState); + function getUndefinedStrippedTargetIfNeeded(source: ts.Type, target: ts.Type) { + // As a builtin type, `undefined` is a very low type ID - making it almsot always first, making this a very fast check to see + // if we need to strip `undefined` from the target + if (source.flags & ts.TypeFlags.Union && target.flags & ts.TypeFlags.Union && + !((source as ts.UnionType).types[0].flags & ts.TypeFlags.Undefined) && (target as ts.UnionType).types[0].flags & ts.TypeFlags.Undefined) { + return extractTypesOfKind(target, ~ts.TypeFlags.Undefined); + } + return target; + } + + function eachTypeRelatedToType(source: ts.UnionOrIntersectionType, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + let result = ts.Ternary.True; + const sourceTypes = source.types; + // We strip `undefined` from the target if the `source` trivially doesn't contain it for our correspondence-checking fastpath + // since `undefined` is frequently added by optionality and would otherwise spoil a potentially useful correspondence + const undefinedStrippedTarget = getUndefinedStrippedTargetIfNeeded(source, target as ts.UnionType); + for (let i = 0; i < sourceTypes.length; i++) { + const sourceType = sourceTypes[i]; + if (undefinedStrippedTarget.flags & ts.TypeFlags.Union && sourceTypes.length >= (undefinedStrippedTarget as ts.UnionType).types.length && sourceTypes.length % (undefinedStrippedTarget as ts.UnionType).types.length === 0) { + // many unions are mappings of one another; in such cases, simply comparing members at the same index can shortcut the comparison + // such unions will have identical lengths, and their corresponding elements will match up. Another common scenario is where a large + // union has a union of objects intersected with it. In such cases, if the input was, eg `("a" | "b" | "c") & (string | boolean | {} | {whatever})`, + // the result will have the structure `"a" | "b" | "c" | "a" & {} | "b" & {} | "c" & {} | "a" & {whatever} | "b" & {whatever} | "c" & {whatever}` + // - the resulting union has a length which is a multiple of the original union, and the elements correspond modulo the length of the original union + const related = isRelatedTo(sourceType, (undefinedStrippedTarget as ts.UnionType).types[i % (undefinedStrippedTarget as ts.UnionType).types.length], RecursionFlags.Both, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState); if (related) { - return related; + result &= related; + continue; } } - return ts.Ternary.False; - } - - function getUndefinedStrippedTargetIfNeeded(source: ts.Type, target: ts.Type) { - // As a builtin type, `undefined` is a very low type ID - making it almsot always first, making this a very fast check to see - // if we need to strip `undefined` from the target - if (source.flags & ts.TypeFlags.Union && target.flags & ts.TypeFlags.Union && - !((source as ts.UnionType).types[0].flags & ts.TypeFlags.Undefined) && (target as ts.UnionType).types[0].flags & ts.TypeFlags.Undefined) { - return extractTypesOfKind(target, ~ts.TypeFlags.Undefined); + const related = isRelatedTo(sourceType, target, RecursionFlags.Source, reportErrors, /*headMessage*/ undefined, intersectionState); + if (!related) { + return ts.Ternary.False; } - return target; + result &= related; } + return result; + } - function eachTypeRelatedToType(source: ts.UnionOrIntersectionType, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - let result = ts.Ternary.True; - const sourceTypes = source.types; - // We strip `undefined` from the target if the `source` trivially doesn't contain it for our correspondence-checking fastpath - // since `undefined` is frequently added by optionality and would otherwise spoil a potentially useful correspondence - const undefinedStrippedTarget = getUndefinedStrippedTargetIfNeeded(source, target as ts.UnionType); - for (let i = 0; i < sourceTypes.length; i++) { - const sourceType = sourceTypes[i]; - if (undefinedStrippedTarget.flags & ts.TypeFlags.Union && sourceTypes.length >= (undefinedStrippedTarget as ts.UnionType).types.length && sourceTypes.length % (undefinedStrippedTarget as ts.UnionType).types.length === 0) { - // many unions are mappings of one another; in such cases, simply comparing members at the same index can shortcut the comparison - // such unions will have identical lengths, and their corresponding elements will match up. Another common scenario is where a large - // union has a union of objects intersected with it. In such cases, if the input was, eg `("a" | "b" | "c") & (string | boolean | {} | {whatever})`, - // the result will have the structure `"a" | "b" | "c" | "a" & {} | "b" & {} | "c" & {} | "a" & {whatever} | "b" & {whatever} | "c" & {whatever}` - // - the resulting union has a length which is a multiple of the original union, and the elements correspond modulo the length of the original union - const related = isRelatedTo(sourceType, (undefinedStrippedTarget as ts.UnionType).types[i % (undefinedStrippedTarget as ts.UnionType).types.length], RecursionFlags.Both, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState); + function typeArgumentsRelatedTo(sources: readonly ts.Type[] = ts.emptyArray, targets: readonly ts.Type[] = ts.emptyArray, variances: readonly ts.VarianceFlags[] = ts.emptyArray, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + if (sources.length !== targets.length && relation === identityRelation) { + return ts.Ternary.False; + } + const length = sources.length <= targets.length ? sources.length : targets.length; + let result = ts.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] : ts.VarianceFlags.Covariant; + const variance = varianceFlags & ts.VarianceFlags.VarianceMask; + // We ignore arguments for independent type parameters (because they're never witnessed). + if (variance !== ts.VarianceFlags.Independent) { + const s = sources[i]; + const t = targets[i]; + let related = ts.Ternary.True; + if (varianceFlags & ts.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, RecursionFlags.Both, /*reportErrors*/ false) : compareTypesIdentical(s, t); + } + else if (variance === ts.VarianceFlags.Covariant) { + related = isRelatedTo(s, t, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); + } + else if (variance === ts.VarianceFlags.Contravariant) { + related = isRelatedTo(t, s, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); + } + else if (variance === ts.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, RecursionFlags.Both, /*reportErrors*/ false); + if (!related) { + related = isRelatedTo(s, t, RecursionFlags.Both, 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, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); if (related) { - result &= related; - continue; + related &= isRelatedTo(t, s, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); } } - const related = isRelatedTo(sourceType, target, RecursionFlags.Source, reportErrors, /*headMessage*/ undefined, intersectionState); if (!related) { return ts.Ternary.False; } result &= related; } - return result; } + return result; + } - function typeArgumentsRelatedTo(sources: readonly ts.Type[] = ts.emptyArray, targets: readonly ts.Type[] = ts.emptyArray, variances: readonly ts.VarianceFlags[] = ts.emptyArray, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - if (sources.length !== targets.length && relation === identityRelation) { - return ts.Ternary.False; + // 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, recursionFlags: RecursionFlags): ts.Ternary { + if (overflow) { + return ts.Ternary.False; + } + const keyIntersectionState = intersectionState | (inPropertyCheck ? IntersectionState.InPropertyCheck : 0); + const id = getRelationKey(source, target, keyIntersectionState, relation, /*ingnoreConstraints*/ false); + const entry = relation.get(id); + if (entry !== undefined) { + if (reportErrors && entry & ts.RelationComparisonResult.Failed && !(entry & ts.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. } - const length = sources.length <= targets.length ? sources.length : targets.length; - let result = ts.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] : ts.VarianceFlags.Covariant; - const variance = varianceFlags & ts.VarianceFlags.VarianceMask; - // We ignore arguments for independent type parameters (because they're never witnessed). - if (variance !== ts.VarianceFlags.Independent) { - const s = sources[i]; - const t = targets[i]; - let related = ts.Ternary.True; - if (varianceFlags & ts.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, RecursionFlags.Both, /*reportErrors*/ false) : compareTypesIdentical(s, t); - } - else if (variance === ts.VarianceFlags.Covariant) { - related = isRelatedTo(s, t, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - } - else if (variance === ts.VarianceFlags.Contravariant) { - related = isRelatedTo(t, s, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - } - else if (variance === ts.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, RecursionFlags.Both, /*reportErrors*/ false); - if (!related) { - related = isRelatedTo(s, t, RecursionFlags.Both, 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, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - if (related) { - related &= isRelatedTo(t, s, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - } + else { + if (outofbandVarianceMarkerHandler) { + // We're in the middle of variance checking - integrate any unmeasurable/unreliable flags from this cached component + const saved = entry & ts.RelationComparisonResult.ReportsMask; + if (saved & ts.RelationComparisonResult.ReportsUnmeasurable) { + instantiateType(source, makeFunctionTypeMapper(reportUnmeasurableMarkers)); } - if (!related) { - return ts.Ternary.False; + if (saved & ts.RelationComparisonResult.ReportsUnreliable) { + instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)); } - result &= related; } + return entry & ts.RelationComparisonResult.Succeeded ? ts.Ternary.True : ts.Ternary.False; } - 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, recursionFlags: RecursionFlags): ts.Ternary { - if (overflow) { + if (!maybeKeys) { + maybeKeys = []; + sourceStack = []; + targetStack = []; + } + else { + // A key that starts with "*" is an indication that we have type references that reference constrained + // type parameters. For such keys we also check against the key we would have gotten if all type parameters + // were unconstrained. + const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, keyIntersectionState, relation, /*ignoreConstraints*/ true) : undefined; + for (let i = 0; i < maybeCount; i++) { + // If source and target are already being compared, consider them related with assumptions + if (id === maybeKeys[i] || broadestEquivalentId && broadestEquivalentId === maybeKeys[i]) { + return ts.Ternary.Maybe; + } + } + if (sourceDepth === 100 || targetDepth === 100) { + overflow = true; return ts.Ternary.False; } - const keyIntersectionState = intersectionState | (inPropertyCheck ? IntersectionState.InPropertyCheck : 0); - const id = getRelationKey(source, target, keyIntersectionState, relation, /*ingnoreConstraints*/ false); - const entry = relation.get(id); - if (entry !== undefined) { - if (reportErrors && entry & ts.RelationComparisonResult.Failed && !(entry & ts.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 & ts.RelationComparisonResult.ReportsMask; - if (saved & ts.RelationComparisonResult.ReportsUnmeasurable) { - instantiateType(source, makeFunctionTypeMapper(reportUnmeasurableMarkers)); - } - if (saved & ts.RelationComparisonResult.ReportsUnreliable) { - instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)); - } + } + const maybeStart = maybeCount; + maybeKeys[maybeCount] = id; + maybeCount++; + const saveExpandingFlags = expandingFlags; + if (recursionFlags & RecursionFlags.Source) { + sourceStack[sourceDepth] = source; + sourceDepth++; + if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, sourceDepth)) + expandingFlags |= ExpandingFlags.Source; + } + if (recursionFlags & RecursionFlags.Target) { + targetStack[targetDepth] = target; + targetDepth++; + if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, targetDepth)) + expandingFlags |= ExpandingFlags.Target; + } + let originalHandler: typeof outofbandVarianceMarkerHandler; + let propagatingVarianceFlags: ts.RelationComparisonResult = 0; + if (outofbandVarianceMarkerHandler) { + originalHandler = outofbandVarianceMarkerHandler; + outofbandVarianceMarkerHandler = onlyUnreliable => { + propagatingVarianceFlags |= onlyUnreliable ? ts.RelationComparisonResult.ReportsUnreliable : ts.RelationComparisonResult.ReportsUnmeasurable; + return originalHandler!(onlyUnreliable); + }; + } + + let result: ts.Ternary; + if (expandingFlags === ExpandingFlags.Both) { + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "recursiveTypeRelatedTo_DepthLimit", { + sourceId: source.id, + sourceIdStack: sourceStack.map(t => t.id), + targetId: target.id, + targetIdStack: targetStack.map(t => t.id), + depth: sourceDepth, + targetDepth + }); + result = ts.Ternary.Maybe; + } + else { + ts.tracing?.push(ts.tracing.Phase.CheckTypes, "structuredTypeRelatedTo", { sourceId: source.id, targetId: target.id }); + result = structuredTypeRelatedTo(source, target, reportErrors, intersectionState); + ts.tracing?.pop(); + } + + if (outofbandVarianceMarkerHandler) { + outofbandVarianceMarkerHandler = originalHandler; + } + if (recursionFlags & RecursionFlags.Source) { + sourceDepth--; + } + if (recursionFlags & RecursionFlags.Target) { + targetDepth--; + } + expandingFlags = saveExpandingFlags; + if (result) { + if (result === ts.Ternary.True || (sourceDepth === 0 && targetDepth === 0)) { + if (result === ts.Ternary.True || result === ts.Ternary.Maybe) { + // If result is definitely true, record all maybe keys as having succeeded. Also, record Ternary.Maybe + // results as having succeeded once we reach depth 0, but never record Ternary.Unknown results. + for (let i = maybeStart; i < maybeCount; i++) { + relation.set(maybeKeys[i], ts.RelationComparisonResult.Succeeded | propagatingVarianceFlags); } - return entry & ts.RelationComparisonResult.Succeeded ? ts.Ternary.True : ts.Ternary.False; } + maybeCount = maybeStart; } - if (!maybeKeys) { - maybeKeys = []; - sourceStack = []; - targetStack = []; - } - else { - // A key that starts with "*" is an indication that we have type references that reference constrained - // type parameters. For such keys we also check against the key we would have gotten if all type parameters - // were unconstrained. - const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, keyIntersectionState, relation, /*ignoreConstraints*/ true) : undefined; - for (let i = 0; i < maybeCount; i++) { - // If source and target are already being compared, consider them related with assumptions - if (id === maybeKeys[i] || broadestEquivalentId && broadestEquivalentId === maybeKeys[i]) { - return ts.Ternary.Maybe; - } - } - if (sourceDepth === 100 || targetDepth === 100) { - overflow = true; - return ts.Ternary.False; + } + 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 ? ts.RelationComparisonResult.Reported : 0) | ts.RelationComparisonResult.Failed | propagatingVarianceFlags); + maybeCount = maybeStart; + } + return result; + } + + function structuredTypeRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + if (intersectionState & IntersectionState.PropertyCheck) { + return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); + } + let result: ts.Ternary; + let originalErrorInfo: ts.DiagnosticMessageChain | undefined; + let varianceCheckFailed = false; + const saveErrorInfo = captureErrorCalculationState(); + let sourceFlags = source.flags; + const targetFlags = target.flags; + if (relation === identityRelation) { + // We've already checked that source.flags and target.flags are identical + if (sourceFlags & ts.TypeFlags.UnionOrIntersection) { + let result = eachTypeRelatedToSomeType(source as ts.UnionOrIntersectionType, target as ts.UnionOrIntersectionType); + if (result) { + result &= eachTypeRelatedToSomeType(target as ts.UnionOrIntersectionType, source as ts.UnionOrIntersectionType); } + return result; } - const maybeStart = maybeCount; - maybeKeys[maybeCount] = id; - maybeCount++; - const saveExpandingFlags = expandingFlags; - if (recursionFlags & RecursionFlags.Source) { - sourceStack[sourceDepth] = source; - sourceDepth++; - if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, sourceDepth)) - expandingFlags |= ExpandingFlags.Source; - } - if (recursionFlags & RecursionFlags.Target) { - targetStack[targetDepth] = target; - targetDepth++; - if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, targetDepth)) - expandingFlags |= ExpandingFlags.Target; - } - let originalHandler: typeof outofbandVarianceMarkerHandler; - let propagatingVarianceFlags: ts.RelationComparisonResult = 0; - if (outofbandVarianceMarkerHandler) { - originalHandler = outofbandVarianceMarkerHandler; - outofbandVarianceMarkerHandler = onlyUnreliable => { - propagatingVarianceFlags |= onlyUnreliable ? ts.RelationComparisonResult.ReportsUnreliable : ts.RelationComparisonResult.ReportsUnmeasurable; - return originalHandler!(onlyUnreliable); - }; + if (sourceFlags & ts.TypeFlags.Index) { + return isRelatedTo((source as ts.IndexType).type, (target as ts.IndexType).type, RecursionFlags.Both, /*reportErrors*/ false); } - - let result: ts.Ternary; - if (expandingFlags === ExpandingFlags.Both) { - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "recursiveTypeRelatedTo_DepthLimit", { - sourceId: source.id, - sourceIdStack: sourceStack.map(t => t.id), - targetId: target.id, - targetIdStack: targetStack.map(t => t.id), - depth: sourceDepth, - targetDepth - }); - result = ts.Ternary.Maybe; + if (sourceFlags & ts.TypeFlags.IndexedAccess) { + if (result = isRelatedTo((source as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType, RecursionFlags.Both, /*reportErrors*/ false)) { + if (result &= isRelatedTo((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType, RecursionFlags.Both, /*reportErrors*/ false)) { + return result; + } + } } - else { - ts.tracing?.push(ts.tracing.Phase.CheckTypes, "structuredTypeRelatedTo", { sourceId: source.id, targetId: target.id }); - result = structuredTypeRelatedTo(source, target, reportErrors, intersectionState); - ts.tracing?.pop(); + if (sourceFlags & ts.TypeFlags.Conditional) { + if ((source as ts.ConditionalType).root.isDistributive === (target as ts.ConditionalType).root.isDistributive) { + if (result = isRelatedTo((source as ts.ConditionalType).checkType, (target as ts.ConditionalType).checkType, RecursionFlags.Both, /*reportErrors*/ false)) { + if (result &= isRelatedTo((source as ts.ConditionalType).extendsType, (target as ts.ConditionalType).extendsType, RecursionFlags.Both, /*reportErrors*/ false)) { + if (result &= isRelatedTo(getTrueTypeFromConditionalType(source as ts.ConditionalType), getTrueTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, /*reportErrors*/ false)) { + if (result &= isRelatedTo(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, /*reportErrors*/ false)) { + return result; + } + } + } + } + } } - - if (outofbandVarianceMarkerHandler) { - outofbandVarianceMarkerHandler = originalHandler; + if (sourceFlags & ts.TypeFlags.Substitution) { + return isRelatedTo((source as ts.SubstitutionType).substitute, (target as ts.SubstitutionType).substitute, RecursionFlags.Both, /*reportErrors*/ false); } - if (recursionFlags & RecursionFlags.Source) { - sourceDepth--; + if (!(sourceFlags & ts.TypeFlags.Object)) { + return ts.Ternary.False; } - if (recursionFlags & RecursionFlags.Target) { - targetDepth--; + } + else if (sourceFlags & ts.TypeFlags.UnionOrIntersection || targetFlags & ts.TypeFlags.UnionOrIntersection) { + if (result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState)) { + return result; } - expandingFlags = saveExpandingFlags; - if (result) { - if (result === ts.Ternary.True || (sourceDepth === 0 && targetDepth === 0)) { - if (result === ts.Ternary.True || result === ts.Ternary.Maybe) { - // If result is definitely true, record all maybe keys as having succeeded. Also, record Ternary.Maybe - // results as having succeeded once we reach depth 0, but never record Ternary.Unknown results. - for (let i = maybeStart; i < maybeCount; i++) { - relation.set(maybeKeys[i], ts.RelationComparisonResult.Succeeded | propagatingVarianceFlags); - } + if (source.flags & ts.TypeFlags.Intersection || source.flags & ts.TypeFlags.TypeParameter && target.flags & ts.TypeFlags.Union) { + // 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 & ts.TypeFlags.Intersection ? (source as ts.IntersectionType).types : [source], !!(target.flags & ts.TypeFlags.Union)); + if (constraint && 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, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { + resetErrorInfo(saveErrorInfo); + return result; } - 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 ? ts.RelationComparisonResult.Reported : 0) | ts.RelationComparisonResult.Failed | propagatingVarianceFlags); - maybeCount = maybeStart; + // The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle: + // Source is instantiable (e.g. source has union or intersection constraint). + // Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }). + // Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }). + // Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }). + // Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }). + if (!(sourceFlags & ts.TypeFlags.Instantiable || + sourceFlags & ts.TypeFlags.Object && targetFlags & ts.TypeFlags.Union || + sourceFlags & ts.TypeFlags.Intersection && targetFlags & (ts.TypeFlags.Object | ts.TypeFlags.Union | ts.TypeFlags.Instantiable))) { + return ts.Ternary.False; } - return result; } - function structuredTypeRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - if (intersectionState & IntersectionState.PropertyCheck) { - return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); + // 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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Conditional) && source.aliasSymbol && source.aliasTypeArguments && + source.aliasSymbol === target.aliasSymbol && !(isMarkerType(source) || isMarkerType(target))) { + const variances = getAliasVariances(source.aliasSymbol); + if (variances === ts.emptyArray) { + return ts.Ternary.Unknown; } - let result: ts.Ternary; - let originalErrorInfo: ts.DiagnosticMessageChain | undefined; - let varianceCheckFailed = false; - const saveErrorInfo = captureErrorCalculationState(); - let sourceFlags = source.flags; - const targetFlags = target.flags; - if (relation === identityRelation) { - // We've already checked that source.flags and target.flags are identical - if (sourceFlags & ts.TypeFlags.UnionOrIntersection) { - let result = eachTypeRelatedToSomeType(source as ts.UnionOrIntersectionType, target as ts.UnionOrIntersectionType); - if (result) { - result &= eachTypeRelatedToSomeType(target as ts.UnionOrIntersectionType, source as ts.UnionOrIntersectionType); + const varianceResult = relateVariances(source.aliasTypeArguments, target.aliasTypeArguments, variances, intersectionState); + if (varianceResult !== undefined) { + return varianceResult; + } + } + + // For a generic type T and a type U that is assignable to T, [...U] is assignable to T, U is assignable to readonly [...T], + // and U is assignable to [...T] when U is constrained to a mutable array or tuple type. + if (isSingleElementGenericTupleType(source) && !source.target.readonly && (result = isRelatedTo(getTypeArguments(source)[0], target, RecursionFlags.Source)) || + isSingleElementGenericTupleType(target) && (target.target.readonly || isMutableArrayOrTuple(getBaseConstraintOfType(source) || source)) && (result = isRelatedTo(source, getTypeArguments(target)[0], RecursionFlags.Target))) { + return result; + } + + if (targetFlags & ts.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 (ts.getObjectFlags(source) & ts.ObjectFlags.Mapped && !(source as ts.MappedType).declaration.nameType && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType(source as ts.MappedType), RecursionFlags.Both)) { + if (!(getMappedTypeModifiers(source as ts.MappedType) & MappedTypeModifiers.IncludeOptional)) { + const templateType = getTemplateTypeFromMappedType(source as ts.MappedType); + const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source as ts.MappedType)); + if (result = isRelatedTo(templateType, indexedAccessType, RecursionFlags.Both, reportErrors)) { + return result; } - return result; } - if (sourceFlags & ts.TypeFlags.Index) { - return isRelatedTo((source as ts.IndexType).type, (target as ts.IndexType).type, RecursionFlags.Both, /*reportErrors*/ false); - } - if (sourceFlags & ts.TypeFlags.IndexedAccess) { - if (result = isRelatedTo((source as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType, RecursionFlags.Both, /*reportErrors*/ false)) { - if (result &= isRelatedTo((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType, RecursionFlags.Both, /*reportErrors*/ false)) { + } + if (relation === comparableRelation && sourceFlags & ts.TypeFlags.TypeParameter) { + // This is a carve-out in comparability to essentially forbid comparing a type parameter + // with another type parameter unless one extends the other. (Remember: comparability is mostly bidirectional!) + let constraint = getConstraintOfTypeParameter(source); + if (constraint && hasNonCircularBaseConstraint(source)) { + while (constraint && constraint.flags & ts.TypeFlags.TypeParameter) { + if (result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false)) { return result; } + constraint = getConstraintOfTypeParameter(constraint); } } - if (sourceFlags & ts.TypeFlags.Conditional) { - if ((source as ts.ConditionalType).root.isDistributive === (target as ts.ConditionalType).root.isDistributive) { - if (result = isRelatedTo((source as ts.ConditionalType).checkType, (target as ts.ConditionalType).checkType, RecursionFlags.Both, /*reportErrors*/ false)) { - if (result &= isRelatedTo((source as ts.ConditionalType).extendsType, (target as ts.ConditionalType).extendsType, RecursionFlags.Both, /*reportErrors*/ false)) { - if (result &= isRelatedTo(getTrueTypeFromConditionalType(source as ts.ConditionalType), getTrueTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, /*reportErrors*/ false)) { - if (result &= isRelatedTo(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, /*reportErrors*/ false)) { - return result; - } - } - } - } - } - } - if (sourceFlags & ts.TypeFlags.Substitution) { - return isRelatedTo((source as ts.SubstitutionType).substitute, (target as ts.SubstitutionType).substitute, RecursionFlags.Both, /*reportErrors*/ false); - } - if (!(sourceFlags & ts.TypeFlags.Object)) { - return ts.Ternary.False; + return ts.Ternary.False; + } + } + else if (targetFlags & ts.TypeFlags.Index) { + const targetType = (target as ts.IndexType).type; + // A keyof S is related to a keyof T if T is related to S. + if (sourceFlags & ts.TypeFlags.Index) { + if (result = isRelatedTo(targetType, (source as ts.IndexType).type, RecursionFlags.Both, /*reportErrors*/ false)) { + return result; } } - else if (sourceFlags & ts.TypeFlags.UnionOrIntersection || targetFlags & ts.TypeFlags.UnionOrIntersection) { - if (result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState)) { + if (isTupleType(targetType)) { + // An index type can have a tuple type target when the tuple type contains variadic elements. + // Check if the source is related to the known keys of the tuple type. + if (result = isRelatedTo(source, getKnownKeysOfTupleType(targetType), RecursionFlags.Target, reportErrors)) { return result; } - if (source.flags & ts.TypeFlags.Intersection || source.flags & ts.TypeFlags.TypeParameter && target.flags & ts.TypeFlags.Union) { - // 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 & ts.TypeFlags.Intersection ? (source as ts.IntersectionType).types : [source], !!(target.flags & ts.TypeFlags.Union)); - if (constraint && 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, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { - resetErrorInfo(saveErrorInfo); - return result; - } + } + else { + // 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(targetType); + 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 ts.IndexType).stringsOnly), RecursionFlags.Target, reportErrors) === ts.Ternary.True) { + return ts.Ternary.True; } } - // The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle: - // Source is instantiable (e.g. source has union or intersection constraint). - // Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }). - // Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }). - // Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }). - // Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }). - if (!(sourceFlags & ts.TypeFlags.Instantiable || - sourceFlags & ts.TypeFlags.Object && targetFlags & ts.TypeFlags.Union || - sourceFlags & ts.TypeFlags.Intersection && targetFlags & (ts.TypeFlags.Object | ts.TypeFlags.Union | ts.TypeFlags.Instantiable))) { - return ts.Ternary.False; + else if (isGenericMappedType(targetType)) { + // generic mapped types that don't simplify or have a constraint still have a very simple set of keys we can compare against + // - their nameType or constraintType. + // In many ways, this comparison is a deferred version of what `getIndexTypeForMappedType` does to actually resolve the keys for _non_-generic types + + const nameType = getNameTypeFromMappedType(targetType); + const constraintType = getConstraintTypeFromMappedType(targetType); + let targetKeys; + if (nameType && isMappedTypeWithKeyofConstraintDeclaration(targetType)) { + // we need to get the apparent mappings and union them with the generic mappings, since some properties may be + // missing from the `constraintType` which will otherwise be mapped in the object + const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType)); + const mappedKeys: ts.Type[] = []; + forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, ts.TypeFlags.StringOrNumberLiteralOrUnique, + /*stringsOnly*/ false, t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t)))); + // We still need to include the non-apparent (and thus still generic) keys in the target side of the comparison (in case they're in the source side) + targetKeys = getUnionType([...mappedKeys, nameType]); + } + else { + targetKeys = nameType || constraintType; + } + if (isRelatedTo(source, targetKeys, RecursionFlags.Target, reportErrors) === ts.Ternary.True) { + return ts.Ternary.True; + } } } - - // 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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Conditional) && source.aliasSymbol && source.aliasTypeArguments && - source.aliasSymbol === target.aliasSymbol && !(isMarkerType(source) || isMarkerType(target))) { - const variances = getAliasVariances(source.aliasSymbol); - if (variances === ts.emptyArray) { - return ts.Ternary.Unknown; + } + else if (targetFlags & ts.TypeFlags.IndexedAccess) { + if (sourceFlags & ts.TypeFlags.IndexedAccess) { + // Relate components directly before falling back to constraint relationships + // 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 as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType, RecursionFlags.Both, reportErrors)) { + result &= isRelatedTo((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType, RecursionFlags.Both, reportErrors); } - const varianceResult = relateVariances(source.aliasTypeArguments, target.aliasTypeArguments, variances, intersectionState); - if (varianceResult !== undefined) { - return varianceResult; + if (result) { + resetErrorInfo(saveErrorInfo); + return result; + } + if (reportErrors) { + originalErrorInfo = errorInfo; } } - - // For a generic type T and a type U that is assignable to T, [...U] is assignable to T, U is assignable to readonly [...T], - // and U is assignable to [...T] when U is constrained to a mutable array or tuple type. - if (isSingleElementGenericTupleType(source) && !source.target.readonly && (result = isRelatedTo(getTypeArguments(source)[0], target, RecursionFlags.Source)) || - isSingleElementGenericTupleType(target) && (target.target.readonly || isMutableArrayOrTuple(getBaseConstraintOfType(source) || source)) && (result = isRelatedTo(source, getTypeArguments(target)[0], RecursionFlags.Target))) { - return result; - } - - if (targetFlags & ts.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 (ts.getObjectFlags(source) & ts.ObjectFlags.Mapped && !(source as ts.MappedType).declaration.nameType && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType(source as ts.MappedType), RecursionFlags.Both)) { - if (!(getMappedTypeModifiers(source as ts.MappedType) & MappedTypeModifiers.IncludeOptional)) { - const templateType = getTemplateTypeFromMappedType(source as ts.MappedType); - const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source as ts.MappedType)); - if (result = isRelatedTo(templateType, indexedAccessType, RecursionFlags.Both, reportErrors)) { + // 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 === assignableRelation || relation === comparableRelation) { + const objectType = (target as ts.IndexedAccessType).objectType; + const indexType = (target as ts.IndexedAccessType).indexType; + const baseObjectType = getBaseConstraintOfType(objectType) || objectType; + const baseIndexType = getBaseConstraintOfType(indexType) || indexType; + if (!isGenericObjectType(baseObjectType) && !isGenericIndexType(baseIndexType)) { + const accessFlags = ts.AccessFlags.Writing | (baseObjectType !== objectType ? ts.AccessFlags.NoIndexSignatures : 0); + const constraint = getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, accessFlags); + if (constraint) { + if (reportErrors && originalErrorInfo) { + // create a new chain for the constraint error + resetErrorInfo(saveErrorInfo); + } + if (result = isRelatedTo(source, constraint, RecursionFlags.Target, reportErrors)) { return result; } - } - } - if (relation === comparableRelation && sourceFlags & ts.TypeFlags.TypeParameter) { - // This is a carve-out in comparability to essentially forbid comparing a type parameter - // with another type parameter unless one extends the other. (Remember: comparability is mostly bidirectional!) - let constraint = getConstraintOfTypeParameter(source); - if (constraint && hasNonCircularBaseConstraint(source)) { - while (constraint && constraint.flags & ts.TypeFlags.TypeParameter) { - if (result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false)) { - return result; - } - constraint = getConstraintOfTypeParameter(constraint); + // prefer the shorter chain of the constraint comparison chain, and the direct comparison chain + if (reportErrors && originalErrorInfo && errorInfo) { + errorInfo = countMessageChainBreadth([originalErrorInfo]) <= countMessageChainBreadth([errorInfo]) ? originalErrorInfo : errorInfo; } } - return ts.Ternary.False; } } - else if (targetFlags & ts.TypeFlags.Index) { - const targetType = (target as ts.IndexType).type; - // A keyof S is related to a keyof T if T is related to S. - if (sourceFlags & ts.TypeFlags.Index) { - if (result = isRelatedTo(targetType, (source as ts.IndexType).type, RecursionFlags.Both, /*reportErrors*/ false)) { - return result; - } - } - if (isTupleType(targetType)) { - // An index type can have a tuple type target when the tuple type contains variadic elements. - // Check if the source is related to the known keys of the tuple type. - if (result = isRelatedTo(source, getKnownKeysOfTupleType(targetType), RecursionFlags.Target, reportErrors)) { - return result; - } + if (reportErrors) { + originalErrorInfo = undefined; + } + } + else if (isGenericMappedType(target) && relation !== identityRelation) { + // Check if source type `S` is related to target type `{ [P in Q]: T }` or `{ [P in Q as R]: T}`. + const keysRemapped = !!target.declaration.nameType; + const templateType = getTemplateTypeFromMappedType(target); + const modifiers = getMappedTypeModifiers(target); + if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) { + // If the mapped type has shape `{ [P in Q]: T[P] }`, + // source `S` is related to target if `T` = `S`, i.e. `S` is related to `{ [P in Q]: S[P] }`. + if (!keysRemapped && templateType.flags & ts.TypeFlags.IndexedAccess && (templateType as ts.IndexedAccessType).objectType === source && + (templateType as ts.IndexedAccessType).indexType === getTypeParameterFromMappedType(target)) { + return ts.Ternary.True; } - else { - // 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(targetType); - 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 ts.IndexType).stringsOnly), RecursionFlags.Target, reportErrors) === ts.Ternary.True) { - return ts.Ternary.True; - } - } - else if (isGenericMappedType(targetType)) { - // generic mapped types that don't simplify or have a constraint still have a very simple set of keys we can compare against - // - their nameType or constraintType. - // In many ways, this comparison is a deferred version of what `getIndexTypeForMappedType` does to actually resolve the keys for _non_-generic types - - const nameType = getNameTypeFromMappedType(targetType); - const constraintType = getConstraintTypeFromMappedType(targetType); - let targetKeys; - if (nameType && isMappedTypeWithKeyofConstraintDeclaration(targetType)) { - // we need to get the apparent mappings and union them with the generic mappings, since some properties may be - // missing from the `constraintType` which will otherwise be mapped in the object - const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType)); - const mappedKeys: ts.Type[] = []; - forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, ts.TypeFlags.StringOrNumberLiteralOrUnique, - /*stringsOnly*/ false, t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t)))); - // We still need to include the non-apparent (and thus still generic) keys in the target side of the comparison (in case they're in the source side) - targetKeys = getUnionType([...mappedKeys, nameType]); + if (!isGenericMappedType(source)) { + // If target has shape `{ [P in Q as R]: T}`, then its keys have type `R`. + // If target has shape `{ [P in Q]: T }`, then its keys have type `Q`. + const targetKeys = keysRemapped ? getNameTypeFromMappedType(target)! : getConstraintTypeFromMappedType(target); + // Type of the keys of source type `S`, i.e. `keyof S`. + const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true); + const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional; + const filteredByApplicability = includeOptional ? intersectTypes(targetKeys, sourceKeys) : undefined; + // A source type `S` is related to a target type `{ [P in Q]: T }` if `Q` is related to `keyof S` and `S[Q]` is related to `T`. + // A source type `S` is related to a target type `{ [P in Q as R]: T }` if `R` is related to `keyof S` and `S[R]` is related to `T. + // A source type `S` is related to a target type `{ [P in Q]?: T }` if some constituent `Q'` of `Q` is related to `keyof S` and `S[Q']` is related to `T`. + // A source type `S` is related to a target type `{ [P in Q as R]?: T }` if some constituent `R'` of `R` is related to `keyof S` and `S[R']` is related to `T`. + if (includeOptional + ? !(filteredByApplicability!.flags & ts.TypeFlags.Never) + : isRelatedTo(targetKeys, sourceKeys, RecursionFlags.Both)) { + const templateType = getTemplateTypeFromMappedType(target); + const typeParameter = getTypeParameterFromMappedType(target); + + // Fastpath: When the template type has the form `Obj[P]` where `P` is the mapped type parameter, directly compare source `S` with `Obj` + // to avoid creating the (potentially very large) number of new intermediate types made by manufacturing `S[P]`. + const nonNullComponent = extractTypesOfKind(templateType, ~ts.TypeFlags.Nullable); + if (!keysRemapped && nonNullComponent.flags & ts.TypeFlags.IndexedAccess && (nonNullComponent as ts.IndexedAccessType).indexType === typeParameter) { + if (result = isRelatedTo(source, (nonNullComponent as ts.IndexedAccessType).objectType, RecursionFlags.Target, reportErrors)) { + return result; + } } else { - targetKeys = nameType || constraintType; - } - if (isRelatedTo(source, targetKeys, RecursionFlags.Target, reportErrors) === ts.Ternary.True) { - return ts.Ternary.True; + // We need to compare the type of a property on the source type `S` to the type of the same property on the target type, + // so we need to construct an indexing type representing a property, and then use indexing type to index the source type for comparison. + + // If the target type has shape `{ [P in Q]: T }`, then a property of the target has type `P`. + // If the target type has shape `{ [P in Q]?: T }`, then a property of the target has type `P`, + // but the property is optional, so we only want to compare properties `P` that are common between `keyof S` and `Q`. + // If the target type has shape `{ [P in Q as R]: T }`, then a property of the target has type `R`. + // If the target type has shape `{ [P in Q as R]?: T }`, then a property of the target has type `R`, + // but the property is optional, so we only want to compare properties `R` that are common between `keyof S` and `R`. + const indexingType = keysRemapped + ? (filteredByApplicability || targetKeys) + : filteredByApplicability + ? getIntersectionType([filteredByApplicability, typeParameter]) + : typeParameter; + const indexedAccessType = getIndexedAccessType(source, indexingType); + // Compare `S[indexingType]` to `T`, where `T` is the type of a property of the target type. + if (result = isRelatedTo(indexedAccessType, templateType, RecursionFlags.Both, reportErrors)) { + return result; + } } } + originalErrorInfo = errorInfo; + resetErrorInfo(saveErrorInfo); } } - else if (targetFlags & ts.TypeFlags.IndexedAccess) { - if (sourceFlags & ts.TypeFlags.IndexedAccess) { - // Relate components directly before falling back to constraint relationships - // 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 as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType, RecursionFlags.Both, reportErrors)) { - result &= isRelatedTo((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType, RecursionFlags.Both, reportErrors); - } + } + else if (targetFlags & ts.TypeFlags.Conditional) { + // If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive + // conditional type and bail out with a Ternary.Maybe result. + if (isDeeplyNestedType(target, targetStack, targetDepth, 10)) { + resetErrorInfo(saveErrorInfo); + return ts.Ternary.Maybe; + } + const c = target as ts.ConditionalType; + // We check for a relationship to a conditional type target only when the conditional type has no + // 'infer' positions and is not distributive or is distributive but doesn't reference the check type + // parameter in either of the result types. + if (!c.root.inferTypeParameters && !isDistributionDependent(c.root)) { + // Check if the conditional is always true or always false but still deferred for distribution purposes. + const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType)); + const skipFalse = !skipTrue && isTypeAssignableTo(getRestrictiveInstantiation(c.checkType), getRestrictiveInstantiation(c.extendsType)); + // TODO: Find a nice way to include potential conditional type breakdowns in error output, if they seem good (they usually don't) + if (result = skipTrue ? ts.Ternary.True : isRelatedTo(source, getTrueTypeFromConditionalType(c), RecursionFlags.Target, /*reportErrors*/ false)) { + result &= skipFalse ? ts.Ternary.True : isRelatedTo(source, getFalseTypeFromConditionalType(c), RecursionFlags.Target, /*reportErrors*/ false); if (result) { resetErrorInfo(saveErrorInfo); return result; } - if (reportErrors) { - originalErrorInfo = errorInfo; - } - } - // 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 === assignableRelation || relation === comparableRelation) { - const objectType = (target as ts.IndexedAccessType).objectType; - const indexType = (target as ts.IndexedAccessType).indexType; - const baseObjectType = getBaseConstraintOfType(objectType) || objectType; - const baseIndexType = getBaseConstraintOfType(indexType) || indexType; - if (!isGenericObjectType(baseObjectType) && !isGenericIndexType(baseIndexType)) { - const accessFlags = ts.AccessFlags.Writing | (baseObjectType !== objectType ? ts.AccessFlags.NoIndexSignatures : 0); - const constraint = getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, accessFlags); - if (constraint) { - if (reportErrors && originalErrorInfo) { - // create a new chain for the constraint error - resetErrorInfo(saveErrorInfo); - } - if (result = isRelatedTo(source, constraint, RecursionFlags.Target, reportErrors)) { - return result; - } - // prefer the shorter chain of the constraint comparison chain, and the direct comparison chain - if (reportErrors && originalErrorInfo && errorInfo) { - errorInfo = countMessageChainBreadth([originalErrorInfo]) <= countMessageChainBreadth([errorInfo]) ? originalErrorInfo : errorInfo; - } - } - } } - if (reportErrors) { - originalErrorInfo = undefined; - } - } - else if (isGenericMappedType(target) && relation !== identityRelation) { - // Check if source type `S` is related to target type `{ [P in Q]: T }` or `{ [P in Q as R]: T}`. - const keysRemapped = !!target.declaration.nameType; - const templateType = getTemplateTypeFromMappedType(target); - const modifiers = getMappedTypeModifiers(target); - if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) { - // If the mapped type has shape `{ [P in Q]: T[P] }`, - // source `S` is related to target if `T` = `S`, i.e. `S` is related to `{ [P in Q]: S[P] }`. - if (!keysRemapped && templateType.flags & ts.TypeFlags.IndexedAccess && (templateType as ts.IndexedAccessType).objectType === source && - (templateType as ts.IndexedAccessType).indexType === getTypeParameterFromMappedType(target)) { - return ts.Ternary.True; - } - if (!isGenericMappedType(source)) { - // If target has shape `{ [P in Q as R]: T}`, then its keys have type `R`. - // If target has shape `{ [P in Q]: T }`, then its keys have type `Q`. - const targetKeys = keysRemapped ? getNameTypeFromMappedType(target)! : getConstraintTypeFromMappedType(target); - // Type of the keys of source type `S`, i.e. `keyof S`. - const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true); - const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional; - const filteredByApplicability = includeOptional ? intersectTypes(targetKeys, sourceKeys) : undefined; - // A source type `S` is related to a target type `{ [P in Q]: T }` if `Q` is related to `keyof S` and `S[Q]` is related to `T`. - // A source type `S` is related to a target type `{ [P in Q as R]: T }` if `R` is related to `keyof S` and `S[R]` is related to `T. - // A source type `S` is related to a target type `{ [P in Q]?: T }` if some constituent `Q'` of `Q` is related to `keyof S` and `S[Q']` is related to `T`. - // A source type `S` is related to a target type `{ [P in Q as R]?: T }` if some constituent `R'` of `R` is related to `keyof S` and `S[R']` is related to `T`. - if (includeOptional - ? !(filteredByApplicability!.flags & ts.TypeFlags.Never) - : isRelatedTo(targetKeys, sourceKeys, RecursionFlags.Both)) { - const templateType = getTemplateTypeFromMappedType(target); - const typeParameter = getTypeParameterFromMappedType(target); - - // Fastpath: When the template type has the form `Obj[P]` where `P` is the mapped type parameter, directly compare source `S` with `Obj` - // to avoid creating the (potentially very large) number of new intermediate types made by manufacturing `S[P]`. - const nonNullComponent = extractTypesOfKind(templateType, ~ts.TypeFlags.Nullable); - if (!keysRemapped && nonNullComponent.flags & ts.TypeFlags.IndexedAccess && (nonNullComponent as ts.IndexedAccessType).indexType === typeParameter) { - if (result = isRelatedTo(source, (nonNullComponent as ts.IndexedAccessType).objectType, RecursionFlags.Target, reportErrors)) { - return result; - } - } - else { - // We need to compare the type of a property on the source type `S` to the type of the same property on the target type, - // so we need to construct an indexing type representing a property, and then use indexing type to index the source type for comparison. - - // If the target type has shape `{ [P in Q]: T }`, then a property of the target has type `P`. - // If the target type has shape `{ [P in Q]?: T }`, then a property of the target has type `P`, - // but the property is optional, so we only want to compare properties `P` that are common between `keyof S` and `Q`. - // If the target type has shape `{ [P in Q as R]: T }`, then a property of the target has type `R`. - // If the target type has shape `{ [P in Q as R]?: T }`, then a property of the target has type `R`, - // but the property is optional, so we only want to compare properties `R` that are common between `keyof S` and `R`. - const indexingType = keysRemapped - ? (filteredByApplicability || targetKeys) - : filteredByApplicability - ? getIntersectionType([filteredByApplicability, typeParameter]) - : typeParameter; - const indexedAccessType = getIndexedAccessType(source, indexingType); - // Compare `S[indexingType]` to `T`, where `T` is the type of a property of the target type. - if (result = isRelatedTo(indexedAccessType, templateType, RecursionFlags.Both, reportErrors)) { - return result; - } - } - } - originalErrorInfo = errorInfo; + } + } + else if (targetFlags & ts.TypeFlags.TemplateLiteral) { + if (sourceFlags & ts.TypeFlags.TemplateLiteral) { + if (relation === comparableRelation) { + return templateLiteralTypesDefinitelyUnrelated(source as ts.TemplateLiteralType, target as ts.TemplateLiteralType) ? ts.Ternary.False : ts.Ternary.True; + } + // Report unreliable variance for type variables referenced in template literal type placeholders. + // For example, `foo-${number}` is related to `foo-${string}` even though number isn't related to string. + instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)); + } + if (isTypeMatchedByTemplateLiteralType(source, target as ts.TemplateLiteralType)) { + return ts.Ternary.True; + } + } + + if (sourceFlags & ts.TypeFlags.TypeVariable) { + // IndexedAccess comparisons are handled above in the `targetFlags & TypeFlage.IndexedAccess` branch + if (!(sourceFlags & ts.TypeFlags.IndexedAccess && targetFlags & ts.TypeFlags.IndexedAccess)) { + const constraint = getConstraintOfType(source as ts.TypeVariable) || unknownType; + if (!getConstraintOfType(source as ts.TypeVariable) || (sourceFlags & ts.TypeFlags.TypeParameter && constraint.flags & ts.TypeFlags.Any)) { + // A type variable with no constraint is not related to the non-primitive object type. + if (result = isRelatedTo(emptyObjectType, extractTypesOfKind(target, ~ts.TypeFlags.NonPrimitive), RecursionFlags.Both)) { resetErrorInfo(saveErrorInfo); + return result; } } - } - else if (targetFlags & ts.TypeFlags.Conditional) { - // If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive - // conditional type and bail out with a Ternary.Maybe result. - if (isDeeplyNestedType(target, targetStack, targetDepth, 10)) { + // 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 + if (result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { resetErrorInfo(saveErrorInfo); - return ts.Ternary.Maybe; + return result; } - const c = target as ts.ConditionalType; - // We check for a relationship to a conditional type target only when the conditional type has no - // 'infer' positions and is not distributive or is distributive but doesn't reference the check type - // parameter in either of the result types. - if (!c.root.inferTypeParameters && !isDistributionDependent(c.root)) { - // Check if the conditional is always true or always false but still deferred for distribution purposes. - const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType)); - const skipFalse = !skipTrue && isTypeAssignableTo(getRestrictiveInstantiation(c.checkType), getRestrictiveInstantiation(c.extendsType)); - // TODO: Find a nice way to include potential conditional type breakdowns in error output, if they seem good (they usually don't) - if (result = skipTrue ? ts.Ternary.True : isRelatedTo(source, getTrueTypeFromConditionalType(c), RecursionFlags.Target, /*reportErrors*/ false)) { - result &= skipFalse ? ts.Ternary.True : isRelatedTo(source, getFalseTypeFromConditionalType(c), RecursionFlags.Target, /*reportErrors*/ false); - if (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, RecursionFlags.Source, reportErrors && constraint !== unknownType && !(targetFlags & sourceFlags & ts.TypeFlags.TypeParameter), /*headMessage*/ undefined, intersectionState)) { + resetErrorInfo(saveErrorInfo); + return result; + } + if (isMappedTypeGenericIndexedAccess(source)) { + // For an indexed access type { [P in K]: E}[X], above we have already explored an instantiation of E with X + // substituted for P. We also want to explore type { [P in K]: E }[C], where C is the constraint of X. + const indexConstraint = getConstraintOfType((source as ts.IndexedAccessType).indexType); + if (indexConstraint) { + if (result = isRelatedTo(getIndexedAccessType((source as ts.IndexedAccessType).objectType, indexConstraint), target, RecursionFlags.Source, reportErrors)) { resetErrorInfo(saveErrorInfo); return result; } } } } - else if (targetFlags & ts.TypeFlags.TemplateLiteral) { - if (sourceFlags & ts.TypeFlags.TemplateLiteral) { - if (relation === comparableRelation) { - return templateLiteralTypesDefinitelyUnrelated(source as ts.TemplateLiteralType, target as ts.TemplateLiteralType) ? ts.Ternary.False : ts.Ternary.True; - } - // Report unreliable variance for type variables referenced in template literal type placeholders. - // For example, `foo-${number}` is related to `foo-${string}` even though number isn't related to string. - instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)); - } - if (isTypeMatchedByTemplateLiteralType(source, target as ts.TemplateLiteralType)) { - return ts.Ternary.True; - } - } - - if (sourceFlags & ts.TypeFlags.TypeVariable) { - // IndexedAccess comparisons are handled above in the `targetFlags & TypeFlage.IndexedAccess` branch - if (!(sourceFlags & ts.TypeFlags.IndexedAccess && targetFlags & ts.TypeFlags.IndexedAccess)) { - const constraint = getConstraintOfType(source as ts.TypeVariable) || unknownType; - if (!getConstraintOfType(source as ts.TypeVariable) || (sourceFlags & ts.TypeFlags.TypeParameter && constraint.flags & ts.TypeFlags.Any)) { - // A type variable with no constraint is not related to the non-primitive object type. - if (result = isRelatedTo(emptyObjectType, extractTypesOfKind(target, ~ts.TypeFlags.NonPrimitive), RecursionFlags.Both)) { - 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 - if (result = isRelatedTo(constraint, target, RecursionFlags.Source, /*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, RecursionFlags.Source, reportErrors && constraint !== unknownType && !(targetFlags & sourceFlags & ts.TypeFlags.TypeParameter), /*headMessage*/ undefined, intersectionState)) { - resetErrorInfo(saveErrorInfo); - return result; - } - if (isMappedTypeGenericIndexedAccess(source)) { - // For an indexed access type { [P in K]: E}[X], above we have already explored an instantiation of E with X - // substituted for P. We also want to explore type { [P in K]: E }[C], where C is the constraint of X. - const indexConstraint = getConstraintOfType((source as ts.IndexedAccessType).indexType); - if (indexConstraint) { - if (result = isRelatedTo(getIndexedAccessType((source as ts.IndexedAccessType).objectType, indexConstraint), target, RecursionFlags.Source, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - } + } + else if (sourceFlags & ts.TypeFlags.Index) { + if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } + else if (sourceFlags & ts.TypeFlags.TemplateLiteral && !(targetFlags & ts.TypeFlags.Object)) { + if (!(targetFlags & ts.TypeFlags.TemplateLiteral)) { + const constraint = getBaseConstraintOfType(source); + if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, RecursionFlags.Source, reportErrors))) { + resetErrorInfo(saveErrorInfo); + return result; } } - else if (sourceFlags & ts.TypeFlags.Index) { - if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors)) { + } + else if (sourceFlags & ts.TypeFlags.StringMapping) { + if (targetFlags & ts.TypeFlags.StringMapping && (source as ts.StringMappingType).symbol === (target as ts.StringMappingType).symbol) { + if (result = isRelatedTo((source as ts.StringMappingType).type, (target as ts.StringMappingType).type, RecursionFlags.Both, reportErrors)) { resetErrorInfo(saveErrorInfo); return result; } } - else if (sourceFlags & ts.TypeFlags.TemplateLiteral && !(targetFlags & ts.TypeFlags.Object)) { - if (!(targetFlags & ts.TypeFlags.TemplateLiteral)) { - const constraint = getBaseConstraintOfType(source); - if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, RecursionFlags.Source, reportErrors))) { - resetErrorInfo(saveErrorInfo); - return result; - } + else { + const constraint = getBaseConstraintOfType(source); + if (constraint && (result = isRelatedTo(constraint, target, RecursionFlags.Source, reportErrors))) { + resetErrorInfo(saveErrorInfo); + return result; } } - else if (sourceFlags & ts.TypeFlags.StringMapping) { - if (targetFlags & ts.TypeFlags.StringMapping && (source as ts.StringMappingType).symbol === (target as ts.StringMappingType).symbol) { - if (result = isRelatedTo((source as ts.StringMappingType).type, (target as ts.StringMappingType).type, RecursionFlags.Both, reportErrors)) { + } + else if (sourceFlags & ts.TypeFlags.Conditional) { + // If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive + // conditional type and bail out with a Ternary.Maybe result. + if (isDeeplyNestedType(source, sourceStack, sourceDepth, 10)) { + resetErrorInfo(saveErrorInfo); + return ts.Ternary.Maybe; + } + if (targetFlags & ts.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 ts.ConditionalType).root.inferTypeParameters; + let sourceExtends = (source as ts.ConditionalType).extendsType; + let mapper: ts.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, ts.InferenceFlags.None, isRelatedToWorker); + inferTypes(ctx.inferences, (target as ts.ConditionalType).extendsType, sourceExtends, ts.InferencePriority.NoConstraints | ts.InferencePriority.AlwaysStrict); + sourceExtends = instantiateType(sourceExtends, ctx.mapper); + mapper = ctx.mapper; + } + if (isTypeIdenticalTo(sourceExtends, (target as ts.ConditionalType).extendsType) && + (isRelatedTo((source as ts.ConditionalType).checkType, (target as ts.ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ts.ConditionalType).checkType, (source as ts.ConditionalType).checkType, RecursionFlags.Both))) { + if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ts.ConditionalType), mapper), getTrueTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, reportErrors)) { + result &= isRelatedTo(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, reportErrors); + } + if (result) { resetErrorInfo(saveErrorInfo); return result; } } - else { - const constraint = getBaseConstraintOfType(source); - if (constraint && (result = isRelatedTo(constraint, target, RecursionFlags.Source, reportErrors))) { + } + else { + // conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way + // more assignments than are desirable (since it maps the source check type to its constraint, it loses information) + const distributiveConstraint = hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ts.ConditionalType) : undefined; + if (distributiveConstraint) { + if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) { resetErrorInfo(saveErrorInfo); return result; } } } - else if (sourceFlags & ts.TypeFlags.Conditional) { - // If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive - // conditional type and bail out with a Ternary.Maybe result. - if (isDeeplyNestedType(source, sourceStack, sourceDepth, 10)) { + + // conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O` + // when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!). + const defaultConstraint = getDefaultConstraintOfConditionalType(source as ts.ConditionalType); + if (defaultConstraint) { + if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) { resetErrorInfo(saveErrorInfo); - return ts.Ternary.Maybe; - } - if (targetFlags & ts.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 ts.ConditionalType).root.inferTypeParameters; - let sourceExtends = (source as ts.ConditionalType).extendsType; - let mapper: ts.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, ts.InferenceFlags.None, isRelatedToWorker); - inferTypes(ctx.inferences, (target as ts.ConditionalType).extendsType, sourceExtends, ts.InferencePriority.NoConstraints | ts.InferencePriority.AlwaysStrict); - sourceExtends = instantiateType(sourceExtends, ctx.mapper); - mapper = ctx.mapper; - } - if (isTypeIdenticalTo(sourceExtends, (target as ts.ConditionalType).extendsType) && - (isRelatedTo((source as ts.ConditionalType).checkType, (target as ts.ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ts.ConditionalType).checkType, (source as ts.ConditionalType).checkType, RecursionFlags.Both))) { - if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ts.ConditionalType), mapper), getTrueTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, reportErrors)) { - result &= isRelatedTo(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target as ts.ConditionalType), RecursionFlags.Both, reportErrors); - } - if (result) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - } - else { - // conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way - // more assignments than are desirable (since it maps the source check type to its constraint, it loses information) - const distributiveConstraint = hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ts.ConditionalType) : undefined; - if (distributiveConstraint) { - if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } + return result; } - - // conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O` - // when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!). - const defaultConstraint = getDefaultConstraintOfConditionalType(source as ts.ConditionalType); - if (defaultConstraint) { - if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) { + } + } + 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 ts.Ternary.True; + } + if (isGenericMappedType(target)) { + if (isGenericMappedType(source)) { + if (result = mappedTypeRelatedTo(source, target, reportErrors)) { resetErrorInfo(saveErrorInfo); return result; } } + return ts.Ternary.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)) { + const sourceIsPrimitive = !!(sourceFlags & ts.TypeFlags.Primitive); + if (relation !== identityRelation) { + source = getApparentType(source); + sourceFlags = source.flags; + } + else if (isGenericMappedType(source)) { + return ts.Ternary.False; + } + if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && (source as ts.TypeReference).target === (target as ts.TypeReference).target && + !isTupleType(source) && !(isMarkerType(source) || isMarkerType(target))) { + // When strictNullChecks is disabled, the element type of the empty array literal is undefinedWideningType, + // and an empty array literal wouldn't be assignable to a `never[]` without this check. + if (isEmptyArrayLiteralType(source)) { return ts.Ternary.True; } - if (isGenericMappedType(target)) { - if (isGenericMappedType(source)) { - if (result = mappedTypeRelatedTo(source, target, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - return ts.Ternary.False; - } - const sourceIsPrimitive = !!(sourceFlags & ts.TypeFlags.Primitive); - if (relation !== identityRelation) { - source = getApparentType(source); - sourceFlags = source.flags; - } - else if (isGenericMappedType(source)) { - return ts.Ternary.False; + // 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 as ts.TypeReference).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 === ts.emptyArray) { + return ts.Ternary.Unknown; } - if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && (source as ts.TypeReference).target === (target as ts.TypeReference).target && - !isTupleType(source) && !(isMarkerType(source) || isMarkerType(target))) { - // When strictNullChecks is disabled, the element type of the empty array literal is undefinedWideningType, - // and an empty array literal wouldn't be assignable to a `never[]` without this check. - if (isEmptyArrayLiteralType(source)) { - return ts.Ternary.True; - } - // 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 as ts.TypeReference).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 === ts.emptyArray) { - return ts.Ternary.Unknown; - } - const varianceResult = relateVariances(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), variances, intersectionState); - if (varianceResult !== undefined) { - return varianceResult; - } + const varianceResult = relateVariances(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), variances, intersectionState); + if (varianceResult !== undefined) { + return varianceResult; } - else if (isReadonlyArrayType(target) ? isArrayOrTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) { - if (relation !== identityRelation) { - return isRelatedTo(getIndexTypeOfType(source, numberType) || anyType, getIndexTypeOfType(target, numberType) || anyType, RecursionFlags.Both, 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 ts.Ternary.False; - } + } + else if (isReadonlyArrayType(target) ? isArrayOrTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) { + if (relation !== identityRelation) { + return isRelatedTo(getIndexTypeOfType(source, numberType) || anyType, getIndexTypeOfType(target, numberType) || anyType, RecursionFlags.Both, reportErrors); } - // 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) && ts.getObjectFlags(target) & ts.ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) { + 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 ts.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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && targetFlags & ts.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); + } + // 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) && ts.getObjectFlags(target) & ts.ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) { + return ts.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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && targetFlags & ts.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, ts.SignatureKind.Call, reportStructuralErrors); if (result) { - result &= signaturesRelatedTo(source, target, ts.SignatureKind.Call, reportStructuralErrors); + result &= signaturesRelatedTo(source, target, ts.SignatureKind.Construct, reportStructuralErrors); if (result) { - result &= signaturesRelatedTo(source, target, ts.SignatureKind.Construct, reportStructuralErrors); - if (result) { - result &= indexSignaturesRelatedTo(source, target, sourceIsPrimitive, reportStructuralErrors, intersectionState); - } + result &= indexSignaturesRelatedTo(source, target, 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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && targetFlags & ts.TypeFlags.Union) { - const objectOnlyTarget = extractTypesOfKind(target, ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.Substitution); - if (objectOnlyTarget.flags & ts.TypeFlags.Union) { - const result = typeRelatedToDiscriminatedType(source, objectOnlyTarget as ts.UnionType); - if (result) { - return result; - } - } + if (varianceCheckFailed && result) { + errorInfo = originalErrorInfo || errorInfo || saveErrorInfo.errorInfo; // Use variance error (there is no structural one) and return false } - } - return ts.Ternary.False; - function countMessageChainBreadth(info: ts.DiagnosticMessageChain[] | undefined): number { - if (!info) - return 0; - return ts.reduceLeft(info, (value, chain) => value + 1 + countMessageChainBreadth(chain.next), 0); - } - - function relateVariances(sourceTypeArguments: readonly ts.Type[] | undefined, targetTypeArguments: readonly ts.Type[] | undefined, variances: ts.VarianceFlags[], intersectionState: IntersectionState) { - if (result = typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState)) { + else if (result) { return result; } - if (ts.some(variances, v => !!(v & ts.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 !== ts.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 && ts.some(variances, v => (v & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Invariant))) { - return ts.Ternary.False; + } + // 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 (sourceFlags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) && targetFlags & ts.TypeFlags.Union) { + const objectOnlyTarget = extractTypesOfKind(target, ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.Substitution); + if (objectOnlyTarget.flags & ts.TypeFlags.Union) { + const result = typeRelatedToDiscriminatedType(source, objectOnlyTarget as ts.UnionType); + if (result) { + return result; } - // 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); } } } + return ts.Ternary.False; + function countMessageChainBreadth(info: ts.DiagnosticMessageChain[] | undefined): number { + if (!info) + return 0; + return ts.reduceLeft(info, (value, chain) => value + 1 + countMessageChainBreadth(chain.next), 0); + } - function reportUnmeasurableMarkers(p: ts.TypeParameter) { - if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { - outofbandVarianceMarkerHandler(/*onlyUnreliable*/ false); + function relateVariances(sourceTypeArguments: readonly ts.Type[] | undefined, targetTypeArguments: readonly ts.Type[] | undefined, variances: ts.VarianceFlags[], intersectionState: IntersectionState) { + if (result = typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState)) { + return result; + } + if (ts.some(variances, v => !!(v & ts.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 !== ts.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 && ts.some(variances, v => (v & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Invariant))) { + return ts.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); } - return p; } + } - function reportUnreliableMarkers(p: ts.TypeParameter) { - if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { - outofbandVarianceMarkerHandler(/*onlyUnreliable*/ true); - } - return p; + function reportUnmeasurableMarkers(p: ts.TypeParameter) { + if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { + outofbandVarianceMarkerHandler(/*onlyUnreliable*/ false); } + 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: ts.MappedType, target: ts.MappedType, reportErrors: boolean): ts.Ternary { - const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : - getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); - if (modifiersRelated) { - let result: ts.Ternary; - const targetConstraint = getConstraintTypeFromMappedType(target); - const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), makeFunctionTypeMapper(getCombinedMappedTypeOptionality(source) < 0 ? reportUnmeasurableMarkers : reportUnreliableMarkers)); - if (result = isRelatedTo(targetConstraint, sourceConstraint, RecursionFlags.Both, reportErrors)) { - const mapper = createTypeMapper([getTypeParameterFromMappedType(source)], [getTypeParameterFromMappedType(target)]); - if (instantiateType(getNameTypeFromMappedType(source), mapper) === instantiateType(getNameTypeFromMappedType(target), mapper)) { - return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(source), mapper), getTemplateTypeFromMappedType(target), RecursionFlags.Both, reportErrors); - } + function reportUnreliableMarkers(p: ts.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: ts.MappedType, target: ts.MappedType, reportErrors: boolean): ts.Ternary { + const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : + getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); + if (modifiersRelated) { + let result: ts.Ternary; + const targetConstraint = getConstraintTypeFromMappedType(target); + const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), makeFunctionTypeMapper(getCombinedMappedTypeOptionality(source) < 0 ? reportUnmeasurableMarkers : reportUnreliableMarkers)); + if (result = isRelatedTo(targetConstraint, sourceConstraint, RecursionFlags.Both, reportErrors)) { + const mapper = createTypeMapper([getTypeParameterFromMappedType(source)], [getTypeParameterFromMappedType(target)]); + if (instantiateType(getNameTypeFromMappedType(source), mapper) === instantiateType(getNameTypeFromMappedType(target), mapper)) { + return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(source), mapper), getTemplateTypeFromMappedType(target), RecursionFlags.Both, reportErrors); } } - return ts.Ternary.False; } + return ts.Ternary.False; + } - function typeRelatedToDiscriminatedType(source: ts.Type, target: ts.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. + function typeRelatedToDiscriminatedType(source: ts.Type, target: ts.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 ts.Ternary.False; + const sourceProperties = getPropertiesOfType(source); + const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); + if (!sourcePropertiesFiltered) + return ts.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(getNonMissingTypeOfSymbol(sourceProperty)); - if (numCombinations > 25) { - // We've reached the complexity limit. - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "typeRelatedToDiscriminatedType_DepthLimit", { sourceId: source.id, targetId: target.id, numCombinations }); - return ts.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(getNonMissingTypeOfSymbol(sourceProperty)); + if (numCombinations > 25) { + // We've reached the complexity limit. + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "typeRelatedToDiscriminatedType_DepthLimit", { sourceId: source.id, targetId: target.id, numCombinations }); + return ts.Ternary.False; } + } - // Compute the set of types for each discriminant property. - const sourceDiscriminantTypes: ts.Type[][] = new Array(sourcePropertiesFiltered.length); - const excludedProperties = new ts.Set(); - for (let i = 0; i < sourcePropertiesFiltered.length; i++) { - const sourceProperty = sourcePropertiesFiltered[i]; - const sourcePropertyType = getNonMissingTypeOfSymbol(sourceProperty); - sourceDiscriminantTypes[i] = sourcePropertyType.flags & ts.TypeFlags.Union - ? (sourcePropertyType as ts.UnionType).types - : [sourcePropertyType]; - excludedProperties.add(sourceProperty.escapedName); - } - - // 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 = ts.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, /*skipOptional*/ strictNullChecks || relation === comparableRelation); - // 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; - } + // Compute the set of types for each discriminant property. + const sourceDiscriminantTypes: ts.Type[][] = new Array(sourcePropertiesFiltered.length); + const excludedProperties = new ts.Set(); + for (let i = 0; i < sourcePropertiesFiltered.length; i++) { + const sourceProperty = sourcePropertiesFiltered[i]; + const sourcePropertyType = getNonMissingTypeOfSymbol(sourceProperty); + sourceDiscriminantTypes[i] = sourcePropertyType.flags & ts.TypeFlags.Union + ? (sourcePropertyType as ts.UnionType).types + : [sourcePropertyType]; + excludedProperties.add(sourceProperty.escapedName); + } + + // 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 = ts.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, /*skipOptional*/ strictNullChecks || relation === comparableRelation); + // 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; } - ts.pushIfUnique(matchingTypes, type, ts.equateValues); - hasMatch = true; - } - if (!hasMatch) { - // We failed to match any type for this combination. - return ts.Ternary.False; } + ts.pushIfUnique(matchingTypes, type, ts.equateValues); + hasMatch = true; + } + if (!hasMatch) { + // We failed to match any type for this combination. + return ts.Ternary.False; } + } - // Compare the remaining non-discriminant properties of each match. - let result = ts.Ternary.True; - for (const type of matchingTypes) { - result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, IntersectionState.None); + // Compare the remaining non-discriminant properties of each match. + let result = ts.Ternary.True; + for (const type of matchingTypes) { + result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, IntersectionState.None); + if (result) { + result &= signaturesRelatedTo(source, type, ts.SignatureKind.Call, /*reportStructuralErrors*/ false); if (result) { - result &= signaturesRelatedTo(source, type, ts.SignatureKind.Call, /*reportStructuralErrors*/ false); - if (result) { - result &= signaturesRelatedTo(source, type, ts.SignatureKind.Construct, /*reportStructuralErrors*/ false); - if (result && !(isTupleType(source) && isTupleType(type))) { - // Comparing numeric index types when both `source` and `type` are tuples is unnecessary as the - // element types should be sufficiently covered by `propertiesRelatedTo`. It also causes problems - // with index type assignability as the types for the excluded discriminants are still included - // in the index type. - result &= indexSignaturesRelatedTo(source, type, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); - } + result &= signaturesRelatedTo(source, type, ts.SignatureKind.Construct, /*reportStructuralErrors*/ false); + if (result && !(isTupleType(source) && isTupleType(type))) { + // Comparing numeric index types when both `source` and `type` are tuples is unnecessary as the + // element types should be sufficiently covered by `propertiesRelatedTo`. It also causes problems + // with index type assignability as the types for the excluded discriminants are still included + // in the index type. + result &= indexSignaturesRelatedTo(source, type, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); } } - if (!result) { - return result; - } } - return result; + if (!result) { + return result; + } } + return result; + } - function excludeProperties(properties: ts.Symbol[], excludedProperties: ts.Set | 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]); - } - } - else if (!result) { - result = properties.slice(0, i); + function excludeProperties(properties: ts.Symbol[], excludedProperties: ts.Set | 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]); } } - return result || properties; + else if (!result) { + result = properties.slice(0, i); + } } + return result || properties; + } - function isPropertySymbolTypeRelated(sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - const targetIsOptional = strictNullChecks && !!(ts.getCheckFlags(targetProp) & ts.CheckFlags.Partial); - const effectiveTarget = addOptionality(getNonMissingTypeOfSymbol(targetProp), /*isProperty*/ false, targetIsOptional); - const effectiveSource = getTypeOfSourceProperty(sourceProp); - return isRelatedTo(effectiveSource, effectiveTarget, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - } + function isPropertySymbolTypeRelated(sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + const targetIsOptional = strictNullChecks && !!(ts.getCheckFlags(targetProp) & ts.CheckFlags.Partial); + const effectiveTarget = addOptionality(getNonMissingTypeOfSymbol(targetProp), /*isProperty*/ false, targetIsOptional); + const effectiveSource = getTypeOfSourceProperty(sourceProp); + return isRelatedTo(effectiveSource, effectiveTarget, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); + } - function propertyRelatedTo(source: ts.Type, target: ts.Type, sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState, skipOptional: boolean): ts.Ternary { - const sourcePropFlags = ts.getDeclarationModifierFlagsFromSymbol(sourceProp); - const targetPropFlags = ts.getDeclarationModifierFlagsFromSymbol(targetProp); - if (sourcePropFlags & ts.ModifierFlags.Private || targetPropFlags & ts.ModifierFlags.Private) { - if (sourceProp.valueDeclaration !== targetProp.valueDeclaration) { - if (reportErrors) { - if (sourcePropFlags & ts.ModifierFlags.Private && targetPropFlags & ts.ModifierFlags.Private) { - reportError(ts.Diagnostics.Types_have_separate_declarations_of_a_private_property_0, symbolToString(targetProp)); - } - else { - reportError(ts.Diagnostics.Property_0_is_private_in_type_1_but_not_in_type_2, symbolToString(targetProp), typeToString(sourcePropFlags & ts.ModifierFlags.Private ? source : target), typeToString(sourcePropFlags & ts.ModifierFlags.Private ? target : source)); - } + function propertyRelatedTo(source: ts.Type, target: ts.Type, sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState, skipOptional: boolean): ts.Ternary { + const sourcePropFlags = ts.getDeclarationModifierFlagsFromSymbol(sourceProp); + const targetPropFlags = ts.getDeclarationModifierFlagsFromSymbol(targetProp); + if (sourcePropFlags & ts.ModifierFlags.Private || targetPropFlags & ts.ModifierFlags.Private) { + if (sourceProp.valueDeclaration !== targetProp.valueDeclaration) { + if (reportErrors) { + if (sourcePropFlags & ts.ModifierFlags.Private && targetPropFlags & ts.ModifierFlags.Private) { + reportError(ts.Diagnostics.Types_have_separate_declarations_of_a_private_property_0, symbolToString(targetProp)); } - return ts.Ternary.False; - } - } - else if (targetPropFlags & ts.ModifierFlags.Protected) { - if (!isValidOverrideOf(sourceProp, targetProp)) { - if (reportErrors) { - reportError(ts.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)); + else { + reportError(ts.Diagnostics.Property_0_is_private_in_type_1_but_not_in_type_2, symbolToString(targetProp), typeToString(sourcePropFlags & ts.ModifierFlags.Private ? source : target), typeToString(sourcePropFlags & ts.ModifierFlags.Private ? target : source)); } - return ts.Ternary.False; } + return ts.Ternary.False; } - else if (sourcePropFlags & ts.ModifierFlags.Protected) { + } + else if (targetPropFlags & ts.ModifierFlags.Protected) { + if (!isValidOverrideOf(sourceProp, targetProp)) { if (reportErrors) { - reportError(ts.Diagnostics.Property_0_is_protected_in_type_1_but_public_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); + reportError(ts.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 ts.Ternary.False; } + } + else if (sourcePropFlags & ts.ModifierFlags.Protected) { + if (reportErrors) { + reportError(ts.Diagnostics.Property_0_is_protected_in_type_1_but_public_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); + } + return ts.Ternary.False; + } - // Ensure {readonly a: whatever} is not a subtype of {a: whatever}, - // while {a: whatever} is a subtype of {readonly a: whatever}. - // This ensures the subtype relationship is ordered, and preventing declaration order - // from deciding which type "wins" in union subtype reduction. - // They're still assignable to one another, since `readonly` doesn't affect assignability. - // This is only applied during the strictSubtypeRelation -- currently used in subtype reduction - if (relation === strictSubtypeRelation && - isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp)) { - return ts.Ternary.False; + // Ensure {readonly a: whatever} is not a subtype of {a: whatever}, + // while {a: whatever} is a subtype of {readonly a: whatever}. + // This ensures the subtype relationship is ordered, and preventing declaration order + // from deciding which type "wins" in union subtype reduction. + // They're still assignable to one another, since `readonly` doesn't affect assignability. + // This is only applied during the strictSubtypeRelation -- currently used in subtype reduction + if (relation === strictSubtypeRelation && + isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp)) { + return ts.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(ts.Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); } - // 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(ts.Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); - } - return ts.Ternary.False; + return ts.Ternary.False; + } + // When checking for comparability, be more lenient with optional properties. + if (!skipOptional && sourceProp.flags & ts.SymbolFlags.Optional && !(targetProp.flags & ts.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(ts.Diagnostics.Property_0_is_optional_in_type_1_but_required_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); } - // When checking for comparability, be more lenient with optional properties. - if (!skipOptional && sourceProp.flags & ts.SymbolFlags.Optional && !(targetProp.flags & ts.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(ts.Diagnostics.Property_0_is_optional_in_type_1_but_required_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); - } - return ts.Ternary.False; + return ts.Ternary.False; + } + 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 + && ts.isNamedDeclaration(unmatchedProperty.valueDeclaration) + && ts.isPrivateIdentifier(unmatchedProperty.valueDeclaration.name) + && source.symbol + && source.symbol.flags & ts.SymbolFlags.Class) { + const privateIdentifierDescription = unmatchedProperty.valueDeclaration.name.escapedText; + const symbolTableKey = ts.getSymbolNameForPrivateIdentifier(source.symbol, privateIdentifierDescription); + if (symbolTableKey && getPropertyOfType(source, symbolTableKey)) { + const sourceName = ts.factory.getDeclarationName(source.symbol.valueDeclaration); + const targetName = ts.factory.getDeclarationName(target.symbol.valueDeclaration); + reportError(ts.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; } - 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 - && ts.isNamedDeclaration(unmatchedProperty.valueDeclaration) - && ts.isPrivateIdentifier(unmatchedProperty.valueDeclaration.name) - && source.symbol - && source.symbol.flags & ts.SymbolFlags.Class) { - const privateIdentifierDescription = unmatchedProperty.valueDeclaration.name.escapedText; - const symbolTableKey = ts.getSymbolNameForPrivateIdentifier(source.symbol, privateIdentifierDescription); - if (symbolTableKey && getPropertyOfType(source, symbolTableKey)) { - const sourceName = ts.factory.getDeclarationName(source.symbol.valueDeclaration); - const targetName = ts.factory.getDeclarationName(target.symbol.valueDeclaration); - reportError(ts.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 = ts.arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false)); + if (!headMessage || (headMessage.code !== ts.Diagnostics.Class_0_incorrectly_implements_interface_1.code && + headMessage.code !== ts.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(ts.Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, ...getTypeNamesForErrorDisplay(source, target)); + if (ts.length(unmatchedProperty.declarations)) { + associateRelatedInfo(ts.createDiagnosticForNode(unmatchedProperty.declarations![0], ts.Diagnostics._0_is_declared_here, propName)); } - const props = ts.arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false)); - if (!headMessage || (headMessage.code !== ts.Diagnostics.Class_0_incorrectly_implements_interface_1.code && - headMessage.code !== ts.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 (shouldSkipElaboration && errorInfo) { + overrideNextErrorInfo++; } - if (props.length === 1) { - const propName = symbolToString(unmatchedProperty); - reportError(ts.Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, ...getTypeNamesForErrorDisplay(source, target)); - if (ts.length(unmatchedProperty.declarations)) { - associateRelatedInfo(ts.createDiagnosticForNode(unmatchedProperty.declarations![0], ts.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(ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), ts.map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4); } - else if (tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ false)) { - if (props.length > 5) { // arbitrary cutoff for too-long list form - reportError(ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), ts.map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4); - } - else { - reportError(ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), ts.map(props, p => symbolToString(p)).join(", ")); - } - if (shouldSkipElaboration && errorInfo) { - overrideNextErrorInfo++; - } + else { + reportError(ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), ts.map(props, p => symbolToString(p)).join(", ")); + } + if (shouldSkipElaboration && errorInfo) { + overrideNextErrorInfo++; } - // No array like or unmatched property error - just issue top level error (errorInfo = undefined) } + // No array like or unmatched property error - just issue top level error (errorInfo = undefined) + } - function propertiesRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, excludedProperties: ts.Set | undefined, intersectionState: IntersectionState): ts.Ternary { - if (relation === identityRelation) { - return propertiesIdenticalTo(source, target, excludedProperties); - } - let result = ts.Ternary.True; - if (isTupleType(target)) { - if (isArrayOrTupleType(source)) { - if (!target.target.readonly && (isReadonlyArrayType(source) || isTupleType(source) && source.target.readonly)) { - return ts.Ternary.False; + function propertiesRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, excludedProperties: ts.Set | undefined, intersectionState: IntersectionState): ts.Ternary { + if (relation === identityRelation) { + return propertiesIdenticalTo(source, target, excludedProperties); + } + let result = ts.Ternary.True; + if (isTupleType(target)) { + if (isArrayOrTupleType(source)) { + if (!target.target.readonly && (isReadonlyArrayType(source) || isTupleType(source) && source.target.readonly)) { + return ts.Ternary.False; + } + const sourceArity = getTypeReferenceArity(source); + const targetArity = getTypeReferenceArity(target); + const sourceRestFlag = isTupleType(source) ? source.target.combinedFlags & ts.ElementFlags.Rest : ts.ElementFlags.Rest; + const targetRestFlag = target.target.combinedFlags & ts.ElementFlags.Rest; + const sourceMinLength = isTupleType(source) ? source.target.minLength : 0; + const targetMinLength = target.target.minLength; + if (!sourceRestFlag && sourceArity < targetMinLength) { + if (reportErrors) { + reportError(ts.Diagnostics.Source_has_0_element_s_but_target_requires_1, sourceArity, targetMinLength); } - const sourceArity = getTypeReferenceArity(source); - const targetArity = getTypeReferenceArity(target); - const sourceRestFlag = isTupleType(source) ? source.target.combinedFlags & ts.ElementFlags.Rest : ts.ElementFlags.Rest; - const targetRestFlag = target.target.combinedFlags & ts.ElementFlags.Rest; - const sourceMinLength = isTupleType(source) ? source.target.minLength : 0; - const targetMinLength = target.target.minLength; - if (!sourceRestFlag && sourceArity < targetMinLength) { + return ts.Ternary.False; + } + if (!targetRestFlag && targetArity < sourceMinLength) { + if (reportErrors) { + reportError(ts.Diagnostics.Source_has_0_element_s_but_target_allows_only_1, sourceMinLength, targetArity); + } + return ts.Ternary.False; + } + if (!targetRestFlag && (sourceRestFlag || targetArity < sourceArity)) { + if (reportErrors) { + if (sourceMinLength < targetMinLength) { + reportError(ts.Diagnostics.Target_requires_0_element_s_but_source_may_have_fewer, targetMinLength); + } + else { + reportError(ts.Diagnostics.Target_allows_only_0_element_s_but_source_may_have_more, targetArity); + } + } + return ts.Ternary.False; + } + const sourceTypeArguments = getTypeArguments(source); + const targetTypeArguments = getTypeArguments(target); + const startCount = Math.min(isTupleType(source) ? getStartElementCount(source.target, ts.ElementFlags.NonRest) : 0, getStartElementCount(target.target, ts.ElementFlags.NonRest)); + const endCount = Math.min(isTupleType(source) ? getEndElementCount(source.target, ts.ElementFlags.NonRest) : 0, targetRestFlag ? getEndElementCount(target.target, ts.ElementFlags.NonRest) : 0); + let canExcludeDiscriminants = !!excludedProperties; + for (let i = 0; i < targetArity; i++) { + const sourceIndex = i < targetArity - endCount ? i : i + sourceArity - targetArity; + const sourceFlags = isTupleType(source) && (i < startCount || i >= targetArity - endCount) ? source.target.elementFlags[sourceIndex] : ts.ElementFlags.Rest; + const targetFlags = target.target.elementFlags[i]; + if (targetFlags & ts.ElementFlags.Variadic && !(sourceFlags & ts.ElementFlags.Variadic)) { if (reportErrors) { - reportError(ts.Diagnostics.Source_has_0_element_s_but_target_requires_1, sourceArity, targetMinLength); + reportError(ts.Diagnostics.Source_provides_no_match_for_variadic_element_at_position_0_in_target, i); } return ts.Ternary.False; } - if (!targetRestFlag && targetArity < sourceMinLength) { + if (sourceFlags & ts.ElementFlags.Variadic && !(targetFlags & ts.ElementFlags.Variable)) { if (reportErrors) { - reportError(ts.Diagnostics.Source_has_0_element_s_but_target_allows_only_1, sourceMinLength, targetArity); + reportError(ts.Diagnostics.Variadic_element_at_position_0_in_source_does_not_match_element_at_position_1_in_target, sourceIndex, i); } return ts.Ternary.False; } - if (!targetRestFlag && (sourceRestFlag || targetArity < sourceArity)) { + if (targetFlags & ts.ElementFlags.Required && !(sourceFlags & ts.ElementFlags.Required)) { if (reportErrors) { - if (sourceMinLength < targetMinLength) { - reportError(ts.Diagnostics.Target_requires_0_element_s_but_source_may_have_fewer, targetMinLength); - } - else { - reportError(ts.Diagnostics.Target_allows_only_0_element_s_but_source_may_have_more, targetArity); - } + reportError(ts.Diagnostics.Source_provides_no_match_for_required_element_at_position_0_in_target, i); } return ts.Ternary.False; } - const sourceTypeArguments = getTypeArguments(source); - const targetTypeArguments = getTypeArguments(target); - const startCount = Math.min(isTupleType(source) ? getStartElementCount(source.target, ts.ElementFlags.NonRest) : 0, getStartElementCount(target.target, ts.ElementFlags.NonRest)); - const endCount = Math.min(isTupleType(source) ? getEndElementCount(source.target, ts.ElementFlags.NonRest) : 0, targetRestFlag ? getEndElementCount(target.target, ts.ElementFlags.NonRest) : 0); - let canExcludeDiscriminants = !!excludedProperties; - for (let i = 0; i < targetArity; i++) { - const sourceIndex = i < targetArity - endCount ? i : i + sourceArity - targetArity; - const sourceFlags = isTupleType(source) && (i < startCount || i >= targetArity - endCount) ? source.target.elementFlags[sourceIndex] : ts.ElementFlags.Rest; - const targetFlags = target.target.elementFlags[i]; - if (targetFlags & ts.ElementFlags.Variadic && !(sourceFlags & ts.ElementFlags.Variadic)) { - if (reportErrors) { - reportError(ts.Diagnostics.Source_provides_no_match_for_variadic_element_at_position_0_in_target, i); - } - return ts.Ternary.False; - } - if (sourceFlags & ts.ElementFlags.Variadic && !(targetFlags & ts.ElementFlags.Variable)) { - if (reportErrors) { - reportError(ts.Diagnostics.Variadic_element_at_position_0_in_source_does_not_match_element_at_position_1_in_target, sourceIndex, i); - } - return ts.Ternary.False; + // We can only exclude discriminant properties if we have not yet encountered a variable-length element. + if (canExcludeDiscriminants) { + if (sourceFlags & ts.ElementFlags.Variable || targetFlags & ts.ElementFlags.Variable) { + canExcludeDiscriminants = false; } - if (targetFlags & ts.ElementFlags.Required && !(sourceFlags & ts.ElementFlags.Required)) { - if (reportErrors) { - reportError(ts.Diagnostics.Source_provides_no_match_for_required_element_at_position_0_in_target, i); - } - return ts.Ternary.False; + if (canExcludeDiscriminants && excludedProperties?.has(("" + i) as ts.__String)) { + continue; } - // We can only exclude discriminant properties if we have not yet encountered a variable-length element. - if (canExcludeDiscriminants) { - if (sourceFlags & ts.ElementFlags.Variable || targetFlags & ts.ElementFlags.Variable) { - canExcludeDiscriminants = false; - } - if (canExcludeDiscriminants && excludedProperties?.has(("" + i) as ts.__String)) { - continue; + } + const sourceType = !isTupleType(source) ? sourceTypeArguments[0] : + i < startCount || i >= targetArity - endCount ? removeMissingType(sourceTypeArguments[sourceIndex], !!(sourceFlags & targetFlags & ts.ElementFlags.Optional)) : + getElementTypeOfSliceOfTupleType(source, startCount, endCount) || neverType; + const targetType = targetTypeArguments[i]; + const targetCheckType = sourceFlags & ts.ElementFlags.Variadic && targetFlags & ts.ElementFlags.Rest ? createArrayType(targetType) : + removeMissingType(targetType, !!(targetFlags & ts.ElementFlags.Optional)); + const related = isRelatedTo(sourceType, targetCheckType, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); + if (!related) { + if (reportErrors && (targetArity > 1 || sourceArity > 1)) { + if (i < startCount || i >= targetArity - endCount || sourceArity - startCount - endCount === 1) { + reportIncompatibleError(ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target, sourceIndex, i); } - } - const sourceType = !isTupleType(source) ? sourceTypeArguments[0] : - i < startCount || i >= targetArity - endCount ? removeMissingType(sourceTypeArguments[sourceIndex], !!(sourceFlags & targetFlags & ts.ElementFlags.Optional)) : - getElementTypeOfSliceOfTupleType(source, startCount, endCount) || neverType; - const targetType = targetTypeArguments[i]; - const targetCheckType = sourceFlags & ts.ElementFlags.Variadic && targetFlags & ts.ElementFlags.Rest ? createArrayType(targetType) : - removeMissingType(targetType, !!(targetFlags & ts.ElementFlags.Optional)); - const related = isRelatedTo(sourceType, targetCheckType, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); - if (!related) { - if (reportErrors && (targetArity > 1 || sourceArity > 1)) { - if (i < startCount || i >= targetArity - endCount || sourceArity - startCount - endCount === 1) { - reportIncompatibleError(ts.Diagnostics.Type_at_position_0_in_source_is_not_compatible_with_type_at_position_1_in_target, sourceIndex, i); - } - else { - reportIncompatibleError(ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target, startCount, sourceArity - endCount - 1, i); - } + else { + reportIncompatibleError(ts.Diagnostics.Type_at_positions_0_through_1_in_source_is_not_compatible_with_type_at_position_2_in_target, startCount, sourceArity - endCount - 1, i); } - return ts.Ternary.False; } - result &= related; + return ts.Ternary.False; } - return result; - } - if (target.target.combinedFlags & ts.ElementFlags.Variable) { - return ts.Ternary.False; + result &= related; } + return result; } - const requireOptionalProperties = (relation === subtypeRelation || relation === strictSubtypeRelation) && !isObjectLiteralType(source) && !isEmptyArrayLiteralType(source) && !isTupleType(source); - const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false); - if (unmatchedProperty) { - if (reportErrors && shouldReportUnmatchedPropertyError(source, target)) { - reportUnmatchedProperty(source, target, unmatchedProperty, requireOptionalProperties); - } + if (target.target.combinedFlags & ts.ElementFlags.Variable) { return ts.Ternary.False; } - if (isObjectLiteralType(target)) { - for (const sourceProp of excludeProperties(getPropertiesOfType(source), excludedProperties)) { - if (!getPropertyOfObjectType(target, sourceProp.escapedName)) { - const sourceType = getTypeOfSymbol(sourceProp); - if (!(sourceType.flags & ts.TypeFlags.Undefined)) { - if (reportErrors) { - reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target)); - } - return ts.Ternary.False; + } + const requireOptionalProperties = (relation === subtypeRelation || relation === strictSubtypeRelation) && !isObjectLiteralType(source) && !isEmptyArrayLiteralType(source) && !isTupleType(source); + const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false); + if (unmatchedProperty) { + if (reportErrors && shouldReportUnmatchedPropertyError(source, target)) { + reportUnmatchedProperty(source, target, unmatchedProperty, requireOptionalProperties); + } + return ts.Ternary.False; + } + if (isObjectLiteralType(target)) { + for (const sourceProp of excludeProperties(getPropertiesOfType(source), excludedProperties)) { + if (!getPropertyOfObjectType(target, sourceProp.escapedName)) { + const sourceType = getTypeOfSymbol(sourceProp); + if (!(sourceType.flags & ts.TypeFlags.Undefined)) { + if (reportErrors) { + reportError(ts.Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target)); } + return ts.Ternary.False; } } } - // 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 & ts.SymbolFlags.Prototype) && (!numericNamesOnly || ts.isNumericLiteralName(name) || name === "length")) { - const sourceProp = getPropertyOfType(source, name); - if (sourceProp && sourceProp !== targetProp) { - const related = propertyRelatedTo(source, target, sourceProp, targetProp, getNonMissingTypeOfSymbol, reportErrors, intersectionState, relation === comparableRelation); - if (!related) { - return ts.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 & ts.SymbolFlags.Prototype) && (!numericNamesOnly || ts.isNumericLiteralName(name) || name === "length")) { + const sourceProp = getPropertyOfType(source, name); + if (sourceProp && sourceProp !== targetProp) { + const related = propertyRelatedTo(source, target, sourceProp, targetProp, getNonMissingTypeOfSymbol, reportErrors, intersectionState, relation === comparableRelation); + if (!related) { + return ts.Ternary.False; } + result &= related; } } - return result; } + return result; + } - function propertiesIdenticalTo(source: ts.Type, target: ts.Type, excludedProperties: ts.Set | undefined): ts.Ternary { - if (!(source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object)) { + function propertiesIdenticalTo(source: ts.Type, target: ts.Type, excludedProperties: ts.Set | undefined): ts.Ternary { + if (!(source.flags & ts.TypeFlags.Object && target.flags & ts.TypeFlags.Object)) { + return ts.Ternary.False; + } + const sourceProperties = excludeProperties(getPropertiesOfObjectType(source), excludedProperties); + const targetProperties = excludeProperties(getPropertiesOfObjectType(target), excludedProperties); + if (sourceProperties.length !== targetProperties.length) { + return ts.Ternary.False; + } + let result = ts.Ternary.True; + for (const sourceProp of sourceProperties) { + const targetProp = getPropertyOfObjectType(target, sourceProp.escapedName); + if (!targetProp) { return ts.Ternary.False; } - const sourceProperties = excludeProperties(getPropertiesOfObjectType(source), excludedProperties); - const targetProperties = excludeProperties(getPropertiesOfObjectType(target), excludedProperties); - if (sourceProperties.length !== targetProperties.length) { + const related = compareProperties(sourceProp, targetProp, isRelatedTo); + if (!related) { return ts.Ternary.False; } - let result = ts.Ternary.True; - for (const sourceProp of sourceProperties) { - const targetProp = getPropertyOfObjectType(target, sourceProp.escapedName); - if (!targetProp) { - return ts.Ternary.False; - } - const related = compareProperties(sourceProp, targetProp, isRelatedTo); - if (!related) { - return ts.Ternary.False; - } - result &= related; - } - return result; + result &= related; } + return result; + } - function signaturesRelatedTo(source: ts.Type, target: ts.Type, kind: ts.SignatureKind, reportErrors: boolean): ts.Ternary { - if (relation === identityRelation) { - return signaturesIdenticalTo(source, target, kind); - } - if (target === anyFunctionType || source === anyFunctionType) { - return ts.Ternary.True; - } + function signaturesRelatedTo(source: ts.Type, target: ts.Type, kind: ts.SignatureKind, reportErrors: boolean): ts.Ternary { + if (relation === identityRelation) { + return signaturesIdenticalTo(source, target, kind); + } + if (target === anyFunctionType || source === anyFunctionType) { + return ts.Ternary.True; + } - const sourceIsJSConstructor = source.symbol && isJSConstructor(source.symbol.valueDeclaration); - const targetIsJSConstructor = target.symbol && isJSConstructor(target.symbol.valueDeclaration); - - const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === ts.SignatureKind.Construct) ? - ts.SignatureKind.Call : kind); - const targetSignatures = getSignaturesOfType(target, (targetIsJSConstructor && kind === ts.SignatureKind.Construct) ? - ts.SignatureKind.Call : kind); - if (kind === ts.SignatureKind.Construct && sourceSignatures.length && targetSignatures.length) { - const sourceIsAbstract = !!(sourceSignatures[0].flags & ts.SignatureFlags.Abstract); - const targetIsAbstract = !!(targetSignatures[0].flags & ts.SignatureFlags.Abstract); - if (sourceIsAbstract && !targetIsAbstract) { - // 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(ts.Diagnostics.Cannot_assign_an_abstract_constructor_type_to_a_non_abstract_constructor_type); - } - return ts.Ternary.False; - } - if (!constructorVisibilitiesAreCompatible(sourceSignatures[0], targetSignatures[0], reportErrors)) { - return ts.Ternary.False; + const sourceIsJSConstructor = source.symbol && isJSConstructor(source.symbol.valueDeclaration); + const targetIsJSConstructor = target.symbol && isJSConstructor(target.symbol.valueDeclaration); + + const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === ts.SignatureKind.Construct) ? + ts.SignatureKind.Call : kind); + const targetSignatures = getSignaturesOfType(target, (targetIsJSConstructor && kind === ts.SignatureKind.Construct) ? + ts.SignatureKind.Call : kind); + if (kind === ts.SignatureKind.Construct && sourceSignatures.length && targetSignatures.length) { + const sourceIsAbstract = !!(sourceSignatures[0].flags & ts.SignatureFlags.Abstract); + const targetIsAbstract = !!(targetSignatures[0].flags & ts.SignatureFlags.Abstract); + if (sourceIsAbstract && !targetIsAbstract) { + // 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(ts.Diagnostics.Cannot_assign_an_abstract_constructor_type_to_a_non_abstract_constructor_type); } + return ts.Ternary.False; + } + if (!constructorVisibilitiesAreCompatible(sourceSignatures[0], targetSignatures[0], reportErrors)) { + return ts.Ternary.False; } + } - let result = ts.Ternary.True; - const incompatibleReporter = kind === ts.SignatureKind.Construct ? reportIncompatibleConstructSignatureReturn : reportIncompatibleCallSignatureReturn; - const sourceObjectFlags = ts.getObjectFlags(source); - const targetObjectFlags = ts.getObjectFlags(target); - if (sourceObjectFlags & ts.ObjectFlags.Instantiated && targetObjectFlags & ts.ObjectFlags.Instantiated && source.symbol === target.symbol || - sourceObjectFlags & ts.ObjectFlags.Reference && targetObjectFlags & ts.ObjectFlags.Reference && (source as ts.TypeReference).target === (target as ts.TypeReference).target) { - // 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 ts.Ternary.False; - } - result &= related; + let result = ts.Ternary.True; + const incompatibleReporter = kind === ts.SignatureKind.Construct ? reportIncompatibleConstructSignatureReturn : reportIncompatibleCallSignatureReturn; + const sourceObjectFlags = ts.getObjectFlags(source); + const targetObjectFlags = ts.getObjectFlags(target); + if (sourceObjectFlags & ts.ObjectFlags.Instantiated && targetObjectFlags & ts.ObjectFlags.Instantiated && source.symbol === target.symbol || + sourceObjectFlags & ts.ObjectFlags.Reference && targetObjectFlags & ts.ObjectFlags.Reference && (source as ts.TypeReference).target === (target as ts.TypeReference).target) { + // 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 ts.Ternary.False; } + result &= related; } - 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; - const sourceSignature = ts.first(sourceSignatures); - const targetSignature = ts.first(targetSignatures); - result = signatureRelatedTo(sourceSignature, targetSignature, eraseGenerics, reportErrors, incompatibleReporter(sourceSignature, targetSignature)); - if (!result && reportErrors && kind === ts.SignatureKind.Construct && (sourceObjectFlags & targetObjectFlags) && - (targetSignature.declaration?.kind === ts.SyntaxKind.Constructor || sourceSignature.declaration?.kind === ts.SyntaxKind.Constructor)) { - const constructSignatureToString = (signature: ts.Signature) => signatureToString(signature, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrowStyleSignature, kind); - reportError(ts.Diagnostics.Type_0_is_not_assignable_to_type_1, constructSignatureToString(sourceSignature), constructSignatureToString(targetSignature)); - reportError(ts.Diagnostics.Types_of_construct_signatures_are_incompatible); - return result; - } + } + 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; + const sourceSignature = ts.first(sourceSignatures); + const targetSignature = ts.first(targetSignatures); + result = signatureRelatedTo(sourceSignature, targetSignature, eraseGenerics, reportErrors, incompatibleReporter(sourceSignature, targetSignature)); + if (!result && reportErrors && kind === ts.SignatureKind.Construct && (sourceObjectFlags & targetObjectFlags) && + (targetSignature.declaration?.kind === ts.SyntaxKind.Constructor || sourceSignature.declaration?.kind === ts.SyntaxKind.Constructor)) { + const constructSignatureToString = (signature: ts.Signature) => signatureToString(signature, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.WriteArrowStyleSignature, kind); + reportError(ts.Diagnostics.Type_0_is_not_assignable_to_type_1, constructSignatureToString(sourceSignature), constructSignatureToString(targetSignature)); + reportError(ts.Diagnostics.Types_of_construct_signatures_are_incompatible); + return result; } - else { - outer: for (const t of targetSignatures) { - const saveErrorInfo = captureErrorCalculationState(); - // 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(ts.Diagnostics.Type_0_provides_no_match_for_the_signature_1, typeToString(source), signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); + } + else { + outer: for (const t of targetSignatures) { + const saveErrorInfo = captureErrorCalculationState(); + // 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; } - return ts.Ternary.False; + shouldElaborateErrors = false; } - } - return result; - } - - function shouldReportUnmatchedPropertyError(source: ts.Type, target: ts.Type): boolean { - const typeCallSignatures = getSignaturesOfStructuredType(source, ts.SignatureKind.Call); - const typeConstructSignatures = getSignaturesOfStructuredType(source, ts.SignatureKind.Construct); - const typeProperties = getPropertiesOfObjectType(source); - if ((typeCallSignatures.length || typeConstructSignatures.length) && !typeProperties.length) { - if ((getSignaturesOfType(target, ts.SignatureKind.Call).length && typeCallSignatures.length) || - (getSignaturesOfType(target, ts.SignatureKind.Construct).length && typeConstructSignatures.length)) { - return true; // target has similar signature kinds to source, still focus on the unmatched property + if (shouldElaborateErrors) { + reportError(ts.Diagnostics.Type_0_provides_no_match_for_the_signature_1, typeToString(source), signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); } - return false; + return ts.Ternary.False; } - return true; } + return result; + } - 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(ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); + function shouldReportUnmatchedPropertyError(source: ts.Type, target: ts.Type): boolean { + const typeCallSignatures = getSignaturesOfStructuredType(source, ts.SignatureKind.Call); + const typeConstructSignatures = getSignaturesOfStructuredType(source, ts.SignatureKind.Construct); + const typeProperties = getPropertiesOfObjectType(source); + if ((typeCallSignatures.length || typeConstructSignatures.length) && !typeProperties.length) { + if ((getSignaturesOfType(target, ts.SignatureKind.Call).length && typeCallSignatures.length) || + (getSignaturesOfType(target, ts.SignatureKind.Construct).length && typeConstructSignatures.length)) { + return true; // target has similar signature kinds to source, still focus on the unmatched property } - return (source: ts.Type, target: ts.Type) => reportIncompatibleError(ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + return false; } + return true; + } - 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(ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); - } - return (source: ts.Type, target: ts.Type) => reportIncompatibleError(ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + 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(ts.Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); } + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(ts.Diagnostics.Call_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + } - /** - * See signatureAssignableTo, compareSignaturesIdentical - */ - function signatureRelatedTo(source: ts.Signature, target: ts.Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: ts.Type, target: ts.Type) => void): ts.Ternary { - return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target, relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, makeFunctionTypeMapper(reportUnreliableMarkers)); + 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(ts.Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); } + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(ts.Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + } + + /** + * See signatureAssignableTo, compareSignaturesIdentical + */ + function signatureRelatedTo(source: ts.Signature, target: ts.Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: ts.Type, target: ts.Type) => void): ts.Ternary { + return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target, relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, makeFunctionTypeMapper(reportUnreliableMarkers)); + } - function signaturesIdenticalTo(source: ts.Type, target: ts.Type, kind: ts.SignatureKind): ts.Ternary { - const sourceSignatures = getSignaturesOfType(source, kind); - const targetSignatures = getSignaturesOfType(target, kind); - if (sourceSignatures.length !== targetSignatures.length) { + function signaturesIdenticalTo(source: ts.Type, target: ts.Type, kind: ts.SignatureKind): ts.Ternary { + const sourceSignatures = getSignaturesOfType(source, kind); + const targetSignatures = getSignaturesOfType(target, kind); + if (sourceSignatures.length !== targetSignatures.length) { + return ts.Ternary.False; + } + let result = ts.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 ts.Ternary.False; } - let result = ts.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 ts.Ternary.False; - } - result &= related; - } - return result; + result &= related; } + return result; + } - function membersRelatedToIndexInfo(source: ts.Type, targetInfo: ts.IndexInfo, reportErrors: boolean): ts.Ternary { - let result = ts.Ternary.True; - const keyType = targetInfo.keyType; - const props = source.flags & ts.TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(source as ts.IntersectionType) : getPropertiesOfObjectType(source); - for (const prop of props) { - // Skip over ignored JSX and symbol-named members - if (isIgnoredJsxProperty(source, prop)) { - continue; - } - if (isApplicableIndexType(getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique), keyType)) { - const propType = getNonMissingTypeOfSymbol(prop); - const type = exactOptionalPropertyTypes || propType.flags & ts.TypeFlags.Undefined || keyType === numberType || !(prop.flags & ts.SymbolFlags.Optional) - ? propType - : getTypeWithFacts(propType, TypeFacts.NEUndefined); - const related = isRelatedTo(type, targetInfo.type, RecursionFlags.Both, reportErrors); - if (!related) { - if (reportErrors) { - reportError(ts.Diagnostics.Property_0_is_incompatible_with_index_signature, symbolToString(prop)); - } - return ts.Ternary.False; - } - result &= related; - } + function membersRelatedToIndexInfo(source: ts.Type, targetInfo: ts.IndexInfo, reportErrors: boolean): ts.Ternary { + let result = ts.Ternary.True; + const keyType = targetInfo.keyType; + const props = source.flags & ts.TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(source as ts.IntersectionType) : getPropertiesOfObjectType(source); + for (const prop of props) { + // Skip over ignored JSX and symbol-named members + if (isIgnoredJsxProperty(source, prop)) { + continue; } - for (const info of getIndexInfosOfType(source)) { - if (isApplicableIndexType(info.keyType, keyType)) { - const related = indexInfoRelatedTo(info, targetInfo, reportErrors); - if (!related) { - return ts.Ternary.False; + if (isApplicableIndexType(getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique), keyType)) { + const propType = getNonMissingTypeOfSymbol(prop); + const type = exactOptionalPropertyTypes || propType.flags & ts.TypeFlags.Undefined || keyType === numberType || !(prop.flags & ts.SymbolFlags.Optional) + ? propType + : getTypeWithFacts(propType, TypeFacts.NEUndefined); + const related = isRelatedTo(type, targetInfo.type, RecursionFlags.Both, reportErrors); + if (!related) { + if (reportErrors) { + reportError(ts.Diagnostics.Property_0_is_incompatible_with_index_signature, symbolToString(prop)); } - result &= related; - } - } - return result; - } - - function indexInfoRelatedTo(sourceInfo: ts.IndexInfo, targetInfo: ts.IndexInfo, reportErrors: boolean) { - const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors); - if (!related && reportErrors) { - if (sourceInfo.keyType === targetInfo.keyType) { - reportError(ts.Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType)); - } - else { - reportError(ts.Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType)); + return ts.Ternary.False; } + result &= related; } - return related; } - - function indexSignaturesRelatedTo(source: ts.Type, target: ts.Type, sourceIsPrimitive: boolean, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - if (relation === identityRelation) { - return indexSignaturesIdenticalTo(source, target); - } - const indexInfos = getIndexInfosOfType(target); - const targetHasStringIndex = ts.some(indexInfos, info => info.keyType === stringType); - let result = ts.Ternary.True; - for (const targetInfo of indexInfos) { - const related = !sourceIsPrimitive && targetHasStringIndex && targetInfo.type.flags & ts.TypeFlags.Any ? ts.Ternary.True : - isGenericMappedType(source) && targetHasStringIndex ? isRelatedTo(getTemplateTypeFromMappedType(source), targetInfo.type, RecursionFlags.Both, reportErrors) : - typeRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState); + for (const info of getIndexInfosOfType(source)) { + if (isApplicableIndexType(info.keyType, keyType)) { + const related = indexInfoRelatedTo(info, targetInfo, reportErrors); if (!related) { return ts.Ternary.False; } result &= related; } - return result; } + return result; + } - function typeRelatedToIndexInfo(source: ts.Type, targetInfo: ts.IndexInfo, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { - const sourceInfo = getApplicableIndexInfo(source, targetInfo.keyType); - if (sourceInfo) { - return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors); + function indexInfoRelatedTo(sourceInfo: ts.IndexInfo, targetInfo: ts.IndexInfo, reportErrors: boolean) { + const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors); + if (!related && reportErrors) { + if (sourceInfo.keyType === targetInfo.keyType) { + reportError(ts.Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType)); } - if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) { - // Intersection constituents are never considered to have an inferred index signature - return membersRelatedToIndexInfo(source, targetInfo, reportErrors); - } - if (reportErrors) { - reportError(ts.Diagnostics.Index_signature_for_type_0_is_missing_in_type_1, typeToString(targetInfo.keyType), typeToString(source)); + else { + reportError(ts.Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType)); } - return ts.Ternary.False; } + return related; + } - function indexSignaturesIdenticalTo(source: ts.Type, target: ts.Type): ts.Ternary { - const sourceInfos = getIndexInfosOfType(source); - const targetInfos = getIndexInfosOfType(target); - if (sourceInfos.length !== targetInfos.length) { + function indexSignaturesRelatedTo(source: ts.Type, target: ts.Type, sourceIsPrimitive: boolean, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + if (relation === identityRelation) { + return indexSignaturesIdenticalTo(source, target); + } + const indexInfos = getIndexInfosOfType(target); + const targetHasStringIndex = ts.some(indexInfos, info => info.keyType === stringType); + let result = ts.Ternary.True; + for (const targetInfo of indexInfos) { + const related = !sourceIsPrimitive && targetHasStringIndex && targetInfo.type.flags & ts.TypeFlags.Any ? ts.Ternary.True : + isGenericMappedType(source) && targetHasStringIndex ? isRelatedTo(getTemplateTypeFromMappedType(source), targetInfo.type, RecursionFlags.Both, reportErrors) : + typeRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState); + if (!related) { return ts.Ternary.False; } - for (const targetInfo of targetInfos) { - const sourceInfo = getIndexInfoOfType(source, targetInfo.keyType); - if (!(sourceInfo && isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both) && sourceInfo.isReadonly === targetInfo.isReadonly)) { - return ts.Ternary.False; - } - } - return ts.Ternary.True; + result &= related; } + return result; + } - function constructorVisibilitiesAreCompatible(sourceSignature: ts.Signature, targetSignature: ts.Signature, reportErrors: boolean) { - if (!sourceSignature.declaration || !targetSignature.declaration) { - return true; - } - - const sourceAccessibility = ts.getSelectedEffectiveModifierFlags(sourceSignature.declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); - const targetAccessibility = ts.getSelectedEffectiveModifierFlags(targetSignature.declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); - - // A public, protected and private signature is assignable to a private signature. - if (targetAccessibility === ts.ModifierFlags.Private) { - return true; - } + function typeRelatedToIndexInfo(source: ts.Type, targetInfo: ts.IndexInfo, reportErrors: boolean, intersectionState: IntersectionState): ts.Ternary { + const sourceInfo = getApplicableIndexInfo(source, targetInfo.keyType); + if (sourceInfo) { + return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors); + } + if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) { + // Intersection constituents are never considered to have an inferred index signature + return membersRelatedToIndexInfo(source, targetInfo, reportErrors); + } + if (reportErrors) { + reportError(ts.Diagnostics.Index_signature_for_type_0_is_missing_in_type_1, typeToString(targetInfo.keyType), typeToString(source)); + } + return ts.Ternary.False; + } - // A public and protected signature is assignable to a protected signature. - if (targetAccessibility === ts.ModifierFlags.Protected && sourceAccessibility !== ts.ModifierFlags.Private) { - return true; + function indexSignaturesIdenticalTo(source: ts.Type, target: ts.Type): ts.Ternary { + const sourceInfos = getIndexInfosOfType(source); + const targetInfos = getIndexInfosOfType(target); + if (sourceInfos.length !== targetInfos.length) { + return ts.Ternary.False; + } + for (const targetInfo of targetInfos) { + const sourceInfo = getIndexInfoOfType(source, targetInfo.keyType); + if (!(sourceInfo && isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both) && sourceInfo.isReadonly === targetInfo.isReadonly)) { + return ts.Ternary.False; } + } + return ts.Ternary.True; + } - // Only a public signature is assignable to public signature. - if (targetAccessibility !== ts.ModifierFlags.Protected && !sourceAccessibility) { - return true; - } + function constructorVisibilitiesAreCompatible(sourceSignature: ts.Signature, targetSignature: ts.Signature, reportErrors: boolean) { + if (!sourceSignature.declaration || !targetSignature.declaration) { + return true; + } - if (reportErrors) { - reportError(ts.Diagnostics.Cannot_assign_a_0_constructor_type_to_a_1_constructor_type, visibilityToString(sourceAccessibility), visibilityToString(targetAccessibility)); - } + const sourceAccessibility = ts.getSelectedEffectiveModifierFlags(sourceSignature.declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); + const targetAccessibility = ts.getSelectedEffectiveModifierFlags(targetSignature.declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); - return false; + // A public, protected and private signature is assignable to a private signature. + if (targetAccessibility === ts.ModifierFlags.Private) { + return true; } - } - function typeCouldHaveTopLevelSingletonTypes(type: ts.Type): boolean { - // Okay, yes, 'boolean' is a union of 'true | false', but that's not useful - // in error reporting scenarios. If you need to use this function but that detail matters, - // feel free to add a flag. - if (type.flags & ts.TypeFlags.Boolean) { - return false; + // A public and protected signature is assignable to a protected signature. + if (targetAccessibility === ts.ModifierFlags.Protected && sourceAccessibility !== ts.ModifierFlags.Private) { + return true; } - if (type.flags & ts.TypeFlags.UnionOrIntersection) { - return !!ts.forEach((type as ts.IntersectionType).types, typeCouldHaveTopLevelSingletonTypes); + // Only a public signature is assignable to public signature. + if (targetAccessibility !== ts.ModifierFlags.Protected && !sourceAccessibility) { + return true; } - if (type.flags & ts.TypeFlags.Instantiable) { - const constraint = getConstraintOfType(type); - if (constraint && constraint !== type) { - return typeCouldHaveTopLevelSingletonTypes(constraint); - } + if (reportErrors) { + reportError(ts.Diagnostics.Cannot_assign_a_0_constructor_type_to_a_1_constructor_type, visibilityToString(sourceAccessibility), visibilityToString(targetAccessibility)); } - return isUnitType(type) || !!(type.flags & ts.TypeFlags.TemplateLiteral); + return false; } + } - function getExactOptionalUnassignableProperties(source: ts.Type, target: ts.Type) { - if (isTupleType(source) && isTupleType(target)) - return ts.emptyArray; - return getPropertiesOfType(target) - .filter(targetProp => isExactOptionalPropertyMismatch(getTypeOfPropertyOfType(source, targetProp.escapedName), getTypeOfSymbol(targetProp))); + function typeCouldHaveTopLevelSingletonTypes(type: ts.Type): boolean { + // Okay, yes, 'boolean' is a union of 'true | false', but that's not useful + // in error reporting scenarios. If you need to use this function but that detail matters, + // feel free to add a flag. + if (type.flags & ts.TypeFlags.Boolean) { + return false; } - function isExactOptionalPropertyMismatch(source: ts.Type | undefined, target: ts.Type | undefined) { - return !!source && !!target && maybeTypeOfKind(source, ts.TypeFlags.Undefined) && !!containsMissingType(target); + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + return !!ts.forEach((type as ts.IntersectionType).types, typeCouldHaveTopLevelSingletonTypes); } - function getExactOptionalProperties(type: ts.Type) { - return getPropertiesOfType(type).filter(targetProp => containsMissingType(getTypeOfSymbol(targetProp))); + if (type.flags & ts.TypeFlags.Instantiable) { + const constraint = getConstraintOfType(type); + if (constraint && constraint !== type) { + return typeCouldHaveTopLevelSingletonTypes(constraint); + } } - function getBestMatchingType(source: ts.Type, target: ts.UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) { - return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) || - findMatchingTypeReferenceOrTypeAliasReference(source, target) || - findBestTypeForObjectLiteral(source, target) || - findBestTypeForInvokable(source, target) || - findMostOverlappyType(source, target); - } + return isUnitType(type) || !!(type.flags & ts.TypeFlags.TemplateLiteral); + } - function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ - () => ts.Type, - ts.__String - ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue?: undefined, skipPartial?: boolean): ts.Type | undefined; - function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ - () => ts.Type, - ts.__String - ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue: ts.Type, skipPartial?: boolean): ts.Type; - function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ - () => ts.Type, - ts.__String - ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue?: ts.Type, skipPartial?: boolean) { - // 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) { - const targetProp = getUnionOrIntersectionProperty(target, propertyName); - if (skipPartial && targetProp && ts.getCheckFlags(targetProp) & ts.CheckFlags.ReadPartial) { - continue; + function getExactOptionalUnassignableProperties(source: ts.Type, target: ts.Type) { + if (isTupleType(source) && isTupleType(target)) + return ts.emptyArray; + return getPropertiesOfType(target) + .filter(targetProp => isExactOptionalPropertyMismatch(getTypeOfPropertyOfType(source, targetProp.escapedName), getTypeOfSymbol(targetProp))); + } + + function isExactOptionalPropertyMismatch(source: ts.Type | undefined, target: ts.Type | undefined) { + return !!source && !!target && maybeTypeOfKind(source, ts.TypeFlags.Undefined) && !!containsMissingType(target); + } + + function getExactOptionalProperties(type: ts.Type) { + return getPropertiesOfType(type).filter(targetProp => containsMissingType(getTypeOfSymbol(targetProp))); + } + + function getBestMatchingType(source: ts.Type, target: ts.UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) { + return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) || + findMatchingTypeReferenceOrTypeAliasReference(source, target) || + findBestTypeForObjectLiteral(source, target) || + findBestTypeForInvokable(source, target) || + findMostOverlappyType(source, target); + } + + function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ + () => ts.Type, + ts.__String + ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue?: undefined, skipPartial?: boolean): ts.Type | undefined; + function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ + () => ts.Type, + ts.__String + ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue: ts.Type, skipPartial?: boolean): ts.Type; + function discriminateTypeByDiscriminableItems(target: ts.UnionType, discriminators: [ + () => ts.Type, + ts.__String + ][], related: (source: ts.Type, target: ts.Type) => boolean | ts.Ternary, defaultValue?: ts.Type, skipPartial?: boolean) { + // 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) { + const targetProp = getUnionOrIntersectionProperty(target, propertyName); + if (skipPartial && targetProp && ts.getCheckFlags(targetProp) & ts.CheckFlags.ReadPartial) { + continue; + } + 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]; } - 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 { - discriminable[i] = false; - } - i++; + else { + discriminable[i] = false; } + i++; } - const match = discriminable.indexOf(/*searchElement*/ true); - if (match === -1) { + } + const match = discriminable.indexOf(/*searchElement*/ true); + if (match === -1) { + return defaultValue; + } + // make sure exactly 1 matches before returning it + let nextMatch = discriminable.indexOf(/*searchElement*/ true, match + 1); + while (nextMatch !== -1) { + if (!isTypeIdenticalTo(target.types[match], target.types[nextMatch])) { return defaultValue; } - // make sure exactly 1 matches before returning it - let nextMatch = discriminable.indexOf(/*searchElement*/ true, match + 1); - while (nextMatch !== -1) { - if (!isTypeIdenticalTo(target.types[match], target.types[nextMatch])) { - return defaultValue; - } - nextMatch = discriminable.indexOf(/*searchElement*/ true, nextMatch + 1); - } - return target.types[match]; + nextMatch = discriminable.indexOf(/*searchElement*/ true, nextMatch + 1); } + return target.types[match]; + } - /** - * 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 & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && resolved.indexInfos.length === 0 && - resolved.properties.length > 0 && ts.every(resolved.properties, p => !!(p.flags & ts.SymbolFlags.Optional)); - } - if (type.flags & ts.TypeFlags.Intersection) { - return ts.every((type as ts.IntersectionType).types, isWeakType); - } - return false; + /** + * 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 & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && resolved.indexInfos.length === 0 && + resolved.properties.length > 0 && ts.every(resolved.properties, p => !!(p.flags & ts.SymbolFlags.Optional)); + } + if (type.flags & ts.TypeFlags.Intersection) { + return ts.every((type as ts.IntersectionType).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 hasCommonProperties(source: ts.Type, target: ts.Type, isComparingJsxAttributes: boolean) { + for (const prop of getPropertiesOfType(source)) { + if (isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) { + return true; } - return false; } + return false; + } - function getVariances(type: ts.GenericType): ts.VarianceFlags[] { - // Arrays and tuples are known to be covariant, no need to spend time computing this. - return type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ts.ObjectFlags.Tuple ? - arrayVariances : - getVariancesWorker(type.symbol, type.typeParameters); - } + function getVariances(type: ts.GenericType): ts.VarianceFlags[] { + // Arrays and tuples are known to be covariant, no need to spend time computing this. + return type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ts.ObjectFlags.Tuple ? + arrayVariances : + getVariancesWorker(type.symbol, type.typeParameters); + } - function getAliasVariances(symbol: ts.Symbol) { - return getVariancesWorker(symbol, getSymbolLinks(symbol).typeParameters); - } + function getAliasVariances(symbol: ts.Symbol) { + return getVariancesWorker(symbol, getSymbolLinks(symbol).typeParameters); + } - // 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(symbol: ts.Symbol, typeParameters: readonly ts.TypeParameter[] = ts.emptyArray): ts.VarianceFlags[] { - const links = getSymbolLinks(symbol); - if (!links.variances) { - ts.tracing?.push(ts.tracing.Phase.CheckTypes, "getVariancesWorker", { arity: typeParameters.length, id: getTypeId(getDeclaredTypeOfSymbol(symbol)) }); - links.variances = ts.emptyArray; - const variances = []; - for (const tp of typeParameters) { - const modifiers = getVarianceModifiers(tp); - let variance = modifiers & ts.ModifierFlags.Out ? - modifiers & ts.ModifierFlags.In ? ts.VarianceFlags.Invariant : ts.VarianceFlags.Covariant : - modifiers & ts.ModifierFlags.In ? ts.VarianceFlags.Contravariant : undefined; - if (variance === undefined) { - 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(symbol, tp, markerSuperType); - const typeWithSub = createMarkerType(symbol, tp, markerSubType); - variance = (isTypeAssignableTo(typeWithSub, typeWithSuper) ? ts.VarianceFlags.Covariant : 0) | - (isTypeAssignableTo(typeWithSuper, typeWithSub) ? ts.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 === ts.VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(symbol, tp, markerOtherType), typeWithSuper)) { - variance = ts.VarianceFlags.Independent; - } - outofbandVarianceMarkerHandler = oldHandler; - if (unmeasurable || unreliable) { - if (unmeasurable) { - variance |= ts.VarianceFlags.Unmeasurable; - } - if (unreliable) { - variance |= ts.VarianceFlags.Unreliable; - } - } - } - variances.push(variance); - } - links.variances = variances; - ts.tracing?.pop(); - } - return links.variances; + // 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(symbol: ts.Symbol, typeParameters: readonly ts.TypeParameter[] = ts.emptyArray): ts.VarianceFlags[] { + const links = getSymbolLinks(symbol); + if (!links.variances) { + ts.tracing?.push(ts.tracing.Phase.CheckTypes, "getVariancesWorker", { arity: typeParameters.length, id: getTypeId(getDeclaredTypeOfSymbol(symbol)) }); + links.variances = ts.emptyArray; + const variances = []; + for (const tp of typeParameters) { + const modifiers = getVarianceModifiers(tp); + let variance = modifiers & ts.ModifierFlags.Out ? + modifiers & ts.ModifierFlags.In ? ts.VarianceFlags.Invariant : ts.VarianceFlags.Covariant : + modifiers & ts.ModifierFlags.In ? ts.VarianceFlags.Contravariant : undefined; + if (variance === undefined) { + 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(symbol, tp, markerSuperType); + const typeWithSub = createMarkerType(symbol, tp, markerSubType); + variance = (isTypeAssignableTo(typeWithSub, typeWithSuper) ? ts.VarianceFlags.Covariant : 0) | + (isTypeAssignableTo(typeWithSuper, typeWithSub) ? ts.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 === ts.VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(symbol, tp, markerOtherType), typeWithSuper)) { + variance = ts.VarianceFlags.Independent; + } + outofbandVarianceMarkerHandler = oldHandler; + if (unmeasurable || unreliable) { + if (unmeasurable) { + variance |= ts.VarianceFlags.Unmeasurable; + } + if (unreliable) { + variance |= ts.VarianceFlags.Unreliable; + } + } + } + variances.push(variance); + } + links.variances = variances; + ts.tracing?.pop(); } + return links.variances; + } - function createMarkerType(symbol: ts.Symbol, source: ts.TypeParameter, target: ts.Type) { - const mapper = makeUnaryTypeMapper(source, target); - const type = getDeclaredTypeOfSymbol(symbol); - if (isErrorType(type)) { - return type; - } - const result = symbol.flags & ts.SymbolFlags.TypeAlias ? - getTypeAliasInstantiation(symbol, instantiateTypes(getSymbolLinks(symbol).typeParameters!, mapper)) : - createTypeReference(type as ts.GenericType, instantiateTypes((type as ts.GenericType).typeParameters, mapper)); - markerTypes.add(getTypeId(result)); - return result; + function createMarkerType(symbol: ts.Symbol, source: ts.TypeParameter, target: ts.Type) { + const mapper = makeUnaryTypeMapper(source, target); + const type = getDeclaredTypeOfSymbol(symbol); + if (isErrorType(type)) { + return type; } + const result = symbol.flags & ts.SymbolFlags.TypeAlias ? + getTypeAliasInstantiation(symbol, instantiateTypes(getSymbolLinks(symbol).typeParameters!, mapper)) : + createTypeReference(type as ts.GenericType, instantiateTypes((type as ts.GenericType).typeParameters, mapper)); + markerTypes.add(getTypeId(result)); + return result; + } - function isMarkerType(type: ts.Type) { - return markerTypes.has(getTypeId(type)); - } + function isMarkerType(type: ts.Type) { + return markerTypes.has(getTypeId(type)); + } - function getVarianceModifiers(tp: ts.TypeParameter): ts.ModifierFlags { - return (ts.some(tp.symbol?.declarations, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.In)) ? ts.ModifierFlags.In : 0) | - (ts.some(tp.symbol?.declarations, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.Out)) ? ts.ModifierFlags.Out : 0); - } + function getVarianceModifiers(tp: ts.TypeParameter): ts.ModifierFlags { + return (ts.some(tp.symbol?.declarations, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.In)) ? ts.ModifierFlags.In : 0) | + (ts.some(tp.symbol?.declarations, d => ts.hasSyntacticModifier(d, ts.ModifierFlags.Out)) ? ts.ModifierFlags.Out : 0); + } - // 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: ts.VarianceFlags[]): boolean { - for (let i = 0; i < variances.length; i++) { - if ((variances[i] & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Covariant && typeArguments[i].flags & ts.TypeFlags.Void) { - return true; - } + // 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: ts.VarianceFlags[]): boolean { + for (let i = 0; i < variances.length; i++) { + if ((variances[i] & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Covariant && typeArguments[i].flags & ts.TypeFlags.Void) { + return true; } - return false; } + return false; + } - function isUnconstrainedTypeParameter(type: ts.Type) { - return type.flags & ts.TypeFlags.TypeParameter && !getConstraintOfTypeParameter(type as ts.TypeParameter); - } + function isUnconstrainedTypeParameter(type: ts.Type) { + return type.flags & ts.TypeFlags.TypeParameter && !getConstraintOfTypeParameter(type as ts.TypeParameter); + } - function isNonDeferredTypeReference(type: ts.Type): type is ts.TypeReference { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && !(type as ts.TypeReference).node; - } + function isNonDeferredTypeReference(type: ts.Type): type is ts.TypeReference { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && !(type as ts.TypeReference).node; + } - function isTypeReferenceWithGenericArguments(type: ts.Type): boolean { - return isNonDeferredTypeReference(type) && ts.some(getTypeArguments(type), t => !!(t.flags & ts.TypeFlags.TypeParameter) || isTypeReferenceWithGenericArguments(t)); - } + function isTypeReferenceWithGenericArguments(type: ts.Type): boolean { + return isNonDeferredTypeReference(type) && ts.some(getTypeArguments(type), t => !!(t.flags & ts.TypeFlags.TypeParameter) || isTypeReferenceWithGenericArguments(t)); + } - function getGenericTypeReferenceRelationKey(source: ts.TypeReference, target: ts.TypeReference, postFix: string, ignoreConstraints: boolean) { - const typeParameters: ts.Type[] = []; - let constraintMarker = ""; - const sourceId = getTypeReferenceId(source, 0); - const targetId = getTypeReferenceId(target, 0); - return `${constraintMarker}${sourceId},${targetId}${postFix}`; - // getTypeReferenceId(A) returns "111=0-12=1" - // where A.id=111 and number.id=12 - function getTypeReferenceId(type: ts.TypeReference, depth = 0) { - let result = "" + type.target.id; - for (const t of getTypeArguments(type)) { - if (t.flags & ts.TypeFlags.TypeParameter) { - if (ignoreConstraints || isUnconstrainedTypeParameter(t)) { - let index = typeParameters.indexOf(t); - if (index < 0) { - index = typeParameters.length; - typeParameters.push(t); - } - result += "=" + index; - continue; + function getGenericTypeReferenceRelationKey(source: ts.TypeReference, target: ts.TypeReference, postFix: string, ignoreConstraints: boolean) { + const typeParameters: ts.Type[] = []; + let constraintMarker = ""; + const sourceId = getTypeReferenceId(source, 0); + const targetId = getTypeReferenceId(target, 0); + return `${constraintMarker}${sourceId},${targetId}${postFix}`; + // getTypeReferenceId(A) returns "111=0-12=1" + // where A.id=111 and number.id=12 + function getTypeReferenceId(type: ts.TypeReference, depth = 0) { + let result = "" + type.target.id; + for (const t of getTypeArguments(type)) { + if (t.flags & ts.TypeFlags.TypeParameter) { + if (ignoreConstraints || isUnconstrainedTypeParameter(t)) { + let index = typeParameters.indexOf(t); + if (index < 0) { + index = typeParameters.length; + typeParameters.push(t); } - // We mark type references that reference constrained type parameters such that we know to obtain - // and look for a "broadest equivalent key" in the cache. - constraintMarker = "*"; - } - else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) { - result += "<" + getTypeReferenceId(t as ts.TypeReference, depth + 1) + ">"; + result += "=" + index; continue; } - result += "-" + t.id; + // We mark type references that reference constrained type parameters such that we know to obtain + // and look for a "broadest equivalent key" in the cache. + constraintMarker = "*"; } - return result; + else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) { + result += "<" + getTypeReferenceId(t as ts.TypeReference, depth + 1) + ">"; + continue; + } + result += "-" + t.id; } + 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.ESMap, ignoreConstraints: boolean) { - if (relation === identityRelation && source.id > target.id) { - const temp = source; - source = target; - target = temp; - } - const postFix = intersectionState ? ":" + intersectionState : ""; - return isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target) ? - getGenericTypeReferenceRelationKey(source as ts.TypeReference, target as ts.TypeReference, postFix, ignoreConstraints) : - `${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 (ts.getCheckFlags(prop) & ts.CheckFlags.Synthetic) { - for (const t of (prop as ts.TransientSymbol).containingType!.types) { - const p = getPropertyOfType(t, prop.escapedName); - const result = p && forEachProperty(p, callback); - if (result) { - 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.ESMap, ignoreConstraints: boolean) { + if (relation === identityRelation && source.id > target.id) { + const temp = source; + source = target; + target = temp; + } + const postFix = intersectionState ? ":" + intersectionState : ""; + return isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target) ? + getGenericTypeReferenceRelationKey(source as ts.TypeReference, target as ts.TypeReference, postFix, ignoreConstraints) : + `${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 (ts.getCheckFlags(prop) & ts.CheckFlags.Synthetic) { + for (const t of (prop as ts.TransientSymbol).containingType!.types) { + const p = getPropertyOfType(t, prop.escapedName); + const result = p && forEachProperty(p, callback); + if (result) { + return result; } - return undefined; } - return callback(prop); + 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 & ts.SymbolFlags.Class ? getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) as ts.InterfaceType : undefined; - } + // 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 & ts.SymbolFlags.Class ? getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) as ts.InterfaceType : undefined; + } - // Return the inherited type of the given property or undefined if property doesn't exist in a base class. - function getTypeOfPropertyInBaseClass(property: ts.Symbol) { - const classType = getDeclaringClass(property); - const baseClassType = classType && getBaseTypes(classType)[0]; - return baseClassType && getTypeOfPropertyOfType(baseClassType, property.escapedName); - } + // Return the inherited type of the given property or undefined if property doesn't exist in a base class. + function getTypeOfPropertyInBaseClass(property: ts.Symbol) { + const classType = getDeclaringClass(property); + const baseClassType = classType && getBaseTypes(classType)[0]; + return baseClassType && getTypeOfPropertyOfType(baseClassType, property.escapedName); + } - // 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 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 => ts.getDeclarationModifierFlagsFromSymbol(tp) & ts.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: T, prop: ts.Symbol, writing: boolean) { - return forEachProperty(prop, p => ts.getDeclarationModifierFlagsFromSymbol(p, writing) & ts.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 maxDepth 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 maxDepth - // levels, but unequal at some level beyond that. - // In addition, this will also detect when an indexed access has been chained off of maxDepth 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]`). - // It also detects when a recursive type reference has expanded maxDepth or more times, e.g. if the true branch of - // `type A = null extends T ? [A>] : [T]` - // has expanded into `[A>>>>>]`. In such cases we need - // to terminate the expansion, and we do so here. - function isDeeplyNestedType(type: ts.Type, stack: ts.Type[], depth: number, maxDepth = 3): boolean { - if (depth >= maxDepth) { - const identity = getRecursionIdentity(type); - let count = 0; - let lastTypeId = 0; - for (let i = 0; i < depth; i++) { - const t = stack[i]; - if (getRecursionIdentity(t) === identity) { - // We only count occurrences with a higher type id than the previous occurrence, since higher - // type ids are an indicator of newer instantiations caused by recursion. - if (t.id >= lastTypeId) { - count++; - if (count >= maxDepth) { - return true; - } + // 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 => ts.getDeclarationModifierFlagsFromSymbol(tp) & ts.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: T, prop: ts.Symbol, writing: boolean) { + return forEachProperty(prop, p => ts.getDeclarationModifierFlagsFromSymbol(p, writing) & ts.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 maxDepth 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 maxDepth + // levels, but unequal at some level beyond that. + // In addition, this will also detect when an indexed access has been chained off of maxDepth 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]`). + // It also detects when a recursive type reference has expanded maxDepth or more times, e.g. if the true branch of + // `type A = null extends T ? [A>] : [T]` + // has expanded into `[A>>>>>]`. In such cases we need + // to terminate the expansion, and we do so here. + function isDeeplyNestedType(type: ts.Type, stack: ts.Type[], depth: number, maxDepth = 3): boolean { + if (depth >= maxDepth) { + const identity = getRecursionIdentity(type); + let count = 0; + let lastTypeId = 0; + for (let i = 0; i < depth; i++) { + const t = stack[i]; + if (getRecursionIdentity(t) === identity) { + // We only count occurrences with a higher type id than the previous occurrence, since higher + // type ids are an indicator of newer instantiations caused by recursion. + if (t.id >= lastTypeId) { + count++; + if (count >= maxDepth) { + return true; } - lastTypeId = t.id; } + lastTypeId = t.id; } } - return false; } + return false; + } - // The recursion identity of a type is an object identity that is shared among multiple instantiations of the type. - // We track recursion identities in order to identify deeply nested and possibly infinite type instantiations with - // the same origin. For example, when type parameters are in scope in an object type such as { x: T }, all - // instantiations of that type have the same recursion identity. The default recursion identity is the object - // identity of the type, meaning that every type is unique. Generally, types with constituents that could circularly - // reference the type have a recursion identity that differs from the object identity. - function getRecursionIdentity(type: ts.Type): object { - // Object and array literals are known not to contain recursive references and don't need a recursion identity. - if (type.flags & ts.TypeFlags.Object && !isObjectOrArrayLiteralType(type)) { - if (ts.getObjectFlags(type) && ts.ObjectFlags.Reference && (type as ts.TypeReference).node) { - // Deferred type references are tracked through their associated AST node. This gives us finer - // granularity than using their associated target because each manifest type reference has a - // unique AST node. - return (type as ts.TypeReference).node!; - } - if (type.symbol && !(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && type.symbol.flags & ts.SymbolFlags.Class)) { - // We track all object types that have an associated symbol (representing the origin of the type), but - // exclude the static side of classes from this check since it shares its symbol with the instance side. - return type.symbol; - } - if (isTupleType(type)) { - // Tuple types are tracked through their target type - return type.target; - } - } - if (type.flags & ts.TypeFlags.TypeParameter) { + // The recursion identity of a type is an object identity that is shared among multiple instantiations of the type. + // We track recursion identities in order to identify deeply nested and possibly infinite type instantiations with + // the same origin. For example, when type parameters are in scope in an object type such as { x: T }, all + // instantiations of that type have the same recursion identity. The default recursion identity is the object + // identity of the type, meaning that every type is unique. Generally, types with constituents that could circularly + // reference the type have a recursion identity that differs from the object identity. + function getRecursionIdentity(type: ts.Type): object { + // Object and array literals are known not to contain recursive references and don't need a recursion identity. + if (type.flags & ts.TypeFlags.Object && !isObjectOrArrayLiteralType(type)) { + if (ts.getObjectFlags(type) && ts.ObjectFlags.Reference && (type as ts.TypeReference).node) { + // Deferred type references are tracked through their associated AST node. This gives us finer + // granularity than using their associated target because each manifest type reference has a + // unique AST node. + return (type as ts.TypeReference).node!; + } + if (type.symbol && !(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && type.symbol.flags & ts.SymbolFlags.Class)) { + // We track all object types that have an associated symbol (representing the origin of the type), but + // exclude the static side of classes from this check since it shares its symbol with the instance side. return type.symbol; } - if (type.flags & ts.TypeFlags.IndexedAccess) { - // Identity is the leftmost object type in a chain of indexed accesses, eg, in A[P][Q] it is A - do { - type = (type as ts.IndexedAccessType).objectType; - } while (type.flags & ts.TypeFlags.IndexedAccess); - return type; - } - if (type.flags & ts.TypeFlags.Conditional) { - // The root object represents the origin of the conditional type - return (type as ts.ConditionalType).root; + if (isTupleType(type)) { + // Tuple types are tracked through their target type + return type.target; } + } + if (type.flags & ts.TypeFlags.TypeParameter) { + return type.symbol; + } + if (type.flags & ts.TypeFlags.IndexedAccess) { + // Identity is the leftmost object type in a chain of indexed accesses, eg, in A[P][Q] it is A + do { + type = (type as ts.IndexedAccessType).objectType; + } while (type.flags & ts.TypeFlags.IndexedAccess); return type; } - - function isPropertyIdenticalTo(sourceProp: ts.Symbol, targetProp: ts.Symbol): boolean { - return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== ts.Ternary.False; + if (type.flags & ts.TypeFlags.Conditional) { + // The root object represents the origin of the conditional type + return (type as ts.ConditionalType).root; } + return type; + } - function compareProperties(sourceProp: ts.Symbol, targetProp: ts.Symbol, compareTypes: (source: ts.Type, target: ts.Type) => ts.Ternary): ts.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 ts.Ternary.True; - } - const sourcePropAccessibility = ts.getDeclarationModifierFlagsFromSymbol(sourceProp) & ts.ModifierFlags.NonPublicAccessibilityModifier; - const targetPropAccessibility = ts.getDeclarationModifierFlagsFromSymbol(targetProp) & ts.ModifierFlags.NonPublicAccessibilityModifier; - if (sourcePropAccessibility !== targetPropAccessibility) { + function isPropertyIdenticalTo(sourceProp: ts.Symbol, targetProp: ts.Symbol): boolean { + return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== ts.Ternary.False; + } + + function compareProperties(sourceProp: ts.Symbol, targetProp: ts.Symbol, compareTypes: (source: ts.Type, target: ts.Type) => ts.Ternary): ts.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 ts.Ternary.True; + } + const sourcePropAccessibility = ts.getDeclarationModifierFlagsFromSymbol(sourceProp) & ts.ModifierFlags.NonPublicAccessibilityModifier; + const targetPropAccessibility = ts.getDeclarationModifierFlagsFromSymbol(targetProp) & ts.ModifierFlags.NonPublicAccessibilityModifier; + if (sourcePropAccessibility !== targetPropAccessibility) { + return ts.Ternary.False; + } + if (sourcePropAccessibility) { + if (getTargetSymbol(sourceProp) !== getTargetSymbol(targetProp)) { return ts.Ternary.False; } - if (sourcePropAccessibility) { - if (getTargetSymbol(sourceProp) !== getTargetSymbol(targetProp)) { - return ts.Ternary.False; - } - } - else { - if ((sourceProp.flags & ts.SymbolFlags.Optional) !== (targetProp.flags & ts.SymbolFlags.Optional)) { - return ts.Ternary.False; - } - } - if (isReadonlySymbol(sourceProp) !== isReadonlySymbol(targetProp)) { + } + else { + if ((sourceProp.flags & ts.SymbolFlags.Optional) !== (targetProp.flags & ts.SymbolFlags.Optional)) { return ts.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; } + if (isReadonlySymbol(sourceProp) !== isReadonlySymbol(targetProp)) { + return ts.Ternary.False; + } + return compareTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); + } - /** - * See signatureRelatedTo, compareSignaturesIdentical - */ - function compareSignaturesIdentical(source: ts.Signature, target: ts.Signature, partialMatch: boolean, ignoreThisTypes: boolean, ignoreReturnTypes: boolean, compareTypes: (s: ts.Type, t: ts.Type) => ts.Ternary): ts.Ternary { - // TODO (drosen): De-duplicate code between related functions. - if (source === target) { - return ts.Ternary.True; - } - if (!(isMatchingSignature(source, target, partialMatch))) { - return ts.Ternary.False; - } - // Check that the two signatures have the same number of type parameters. - if (ts.length(source.typeParameters) !== ts.length(target.typeParameters)) { - return ts.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 ts.Ternary.False; - } + 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) => ts.Ternary): ts.Ternary { + // TODO (drosen): De-duplicate code between related functions. + if (source === target) { + return ts.Ternary.True; + } + if (!(isMatchingSignature(source, target, partialMatch))) { + return ts.Ternary.False; + } + // Check that the two signatures have the same number of type parameters. + if (ts.length(source.typeParameters) !== ts.length(target.typeParameters)) { + return ts.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 ts.Ternary.False; } - source = instantiateSignature(source, mapper, /*eraseTypeParameters*/ true); } - let result = ts.Ternary.True; - if (!ignoreThisTypes) { - const sourceThisType = getThisTypeOfSignature(source); - if (sourceThisType) { - const targetThisType = getThisTypeOfSignature(target); - if (targetThisType) { - const related = compareTypes(sourceThisType, targetThisType); - if (!related) { - return ts.Ternary.False; - } - result &= related; + source = instantiateSignature(source, mapper, /*eraseTypeParameters*/ true); + } + let result = ts.Ternary.True; + if (!ignoreThisTypes) { + const sourceThisType = getThisTypeOfSignature(source); + if (sourceThisType) { + const targetThisType = getThisTypeOfSignature(target); + if (targetThisType) { + const related = compareTypes(sourceThisType, targetThisType); + if (!related) { + return ts.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 ts.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)); + } + 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 ts.Ternary.False; } - return result; + result &= related; } - - function compareTypePredicatesIdentical(source: ts.TypePredicate | undefined, target: ts.TypePredicate | undefined, compareTypes: (s: ts.Type, t: ts.Type) => ts.Ternary): ts.Ternary { - return !(source && target && typePredicateKindsMatch(source, target)) ? ts.Ternary.False : - source.type === target.type ? ts.Ternary.True : - source.type && target.type ? compareTypes(source.type, target.type) : - ts.Ternary.False; + 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 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; - } + function compareTypePredicatesIdentical(source: ts.TypePredicate | undefined, target: ts.TypePredicate | undefined, compareTypes: (s: ts.Type, t: ts.Type) => ts.Ternary): ts.Ternary { + return !(source && target && typePredicateKindsMatch(source, target)) ? ts.Ternary.False : + source.type === target.type ? ts.Ternary.True : + source.type && target.type ? compareTypes(source.type, target.type) : + ts.Ternary.False; + } - // 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 { - if (types.length === 1) { - return types[0]; + function literalTypesWithSameBaseType(types: ts.Type[]): boolean { + let commonBaseType: ts.Type | undefined; + for (const t of types) { + const baseType = getBaseTypeOfLiteralType(t); + if (!commonBaseType) { + commonBaseType = baseType; } - return literalTypesWithSameBaseType(types) ? - getUnionType(types) : - ts.reduceLeft(types, (s, t) => isTypeSubtypeOf(s, t) ? t : s)!; - } - - function getCommonSupertype(types: ts.Type[]): ts.Type { - if (!strictNullChecks) { - return getSupertypeOrUnion(types); + if (baseType === t || baseType !== commonBaseType) { + return false; } - const primaryTypes = ts.filter(types, t => !(t.flags & ts.TypeFlags.Nullable)); - return primaryTypes.length ? - getNullableType(getSupertypeOrUnion(primaryTypes), getFalsyFlagsOfTypes(types) & ts.TypeFlags.Nullable) : - getUnionType(types, ts.UnionReduction.Subtype); } + return true; + } - // Return the leftmost type for which no type to the right is a subtype. - function getCommonSubtype(types: ts.Type[]) { - return ts.reduceLeft(types, (s, t) => isTypeSubtypeOf(t, s) ? t : s)!; + // 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 { + if (types.length === 1) { + return types[0]; } + return literalTypesWithSameBaseType(types) ? + getUnionType(types) : + ts.reduceLeft(types, (s, t) => isTypeSubtypeOf(s, t) ? t : s)!; + } - function isArrayType(type: ts.Type): type is ts.TypeReference { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && ((type as ts.TypeReference).target === globalArrayType || (type as ts.TypeReference).target === globalReadonlyArrayType); + function getCommonSupertype(types: ts.Type[]): ts.Type { + if (!strictNullChecks) { + return getSupertypeOrUnion(types); } + const primaryTypes = ts.filter(types, t => !(t.flags & ts.TypeFlags.Nullable)); + return primaryTypes.length ? + getNullableType(getSupertypeOrUnion(primaryTypes), getFalsyFlagsOfTypes(types) & ts.TypeFlags.Nullable) : + getUnionType(types, ts.UnionReduction.Subtype); + } - function isReadonlyArrayType(type: ts.Type): boolean { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && (type as ts.TypeReference).target === globalReadonlyArrayType; - } + // Return the leftmost type for which no type to the right is a subtype. + function getCommonSubtype(types: ts.Type[]) { + return ts.reduceLeft(types, (s, t) => isTypeSubtypeOf(t, s) ? t : s)!; + } - function isArrayOrTupleType(type: ts.Type): type is ts.TypeReference { - return isArrayType(type) || isTupleType(type); - } + function isArrayType(type: ts.Type): type is ts.TypeReference { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && ((type as ts.TypeReference).target === globalArrayType || (type as ts.TypeReference).target === globalReadonlyArrayType); + } - function isMutableArrayOrTuple(type: ts.Type): boolean { - return isArrayType(type) && !isReadonlyArrayType(type) || isTupleType(type) && !type.target.readonly; - } + function isReadonlyArrayType(type: ts.Type): boolean { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) && (type as ts.TypeReference).target === globalReadonlyArrayType; + } - function getElementTypeOfArrayType(type: ts.Type): ts.Type | undefined { - return isArrayType(type) ? getTypeArguments(type)[0] : undefined; - } + function isArrayOrTupleType(type: ts.Type): type is ts.TypeReference { + return isArrayType(type) || isTupleType(type); + } - 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 & ts.TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType); - } + function isMutableArrayOrTuple(type: ts.Type): boolean { + return isArrayType(type) && !isReadonlyArrayType(type) || isTupleType(type) && !type.target.readonly; + } - function getSingleBaseForNonAugmentingSubtype(type: ts.Type) { - if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) || !(ts.getObjectFlags((type as ts.TypeReference).target) & ts.ObjectFlags.ClassOrInterface)) { - return undefined; - } - if (ts.getObjectFlags(type) & ts.ObjectFlags.IdenticalBaseTypeCalculated) { - return ts.getObjectFlags(type) & ts.ObjectFlags.IdenticalBaseTypeExists ? (type as ts.TypeReference).cachedEquivalentBaseType : undefined; - } - (type as ts.TypeReference).objectFlags |= ts.ObjectFlags.IdenticalBaseTypeCalculated; - const target = (type as ts.TypeReference).target as ts.InterfaceType; - if (ts.getObjectFlags(target) & ts.ObjectFlags.Class) { - const baseTypeNode = getBaseTypeNodeOfClass(target); - // A base type expression may circularly reference the class itself (e.g. as an argument to function call), so we only - // check for base types specified as simple qualified names. - if (baseTypeNode && baseTypeNode.expression.kind !== ts.SyntaxKind.Identifier && baseTypeNode.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { - return undefined; - } - } - const bases = getBaseTypes(target); - if (bases.length !== 1) { + function getElementTypeOfArrayType(type: ts.Type): ts.Type | undefined { + return isArrayType(type) ? getTypeArguments(type)[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 & ts.TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType); + } + + function getSingleBaseForNonAugmentingSubtype(type: ts.Type) { + if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference) || !(ts.getObjectFlags((type as ts.TypeReference).target) & ts.ObjectFlags.ClassOrInterface)) { + return undefined; + } + if (ts.getObjectFlags(type) & ts.ObjectFlags.IdenticalBaseTypeCalculated) { + return ts.getObjectFlags(type) & ts.ObjectFlags.IdenticalBaseTypeExists ? (type as ts.TypeReference).cachedEquivalentBaseType : undefined; + } + (type as ts.TypeReference).objectFlags |= ts.ObjectFlags.IdenticalBaseTypeCalculated; + const target = (type as ts.TypeReference).target as ts.InterfaceType; + if (ts.getObjectFlags(target) & ts.ObjectFlags.Class) { + const baseTypeNode = getBaseTypeNodeOfClass(target); + // A base type expression may circularly reference the class itself (e.g. as an argument to function call), so we only + // check for base types specified as simple qualified names. + if (baseTypeNode && baseTypeNode.expression.kind !== ts.SyntaxKind.Identifier && baseTypeNode.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { return undefined; } - if (getMembersOfSymbol(type.symbol).size) { - return undefined; // If the interface has any members, they may subtype members in the base, so we should do a full structural comparison - } - let instantiatedBase = !ts.length(target.typeParameters) ? bases[0] : instantiateType(bases[0], createTypeMapper(target.typeParameters!, getTypeArguments(type as ts.TypeReference).slice(0, target.typeParameters!.length))); - if (ts.length(getTypeArguments(type as ts.TypeReference)) > ts.length(target.typeParameters)) { - instantiatedBase = getTypeWithThisArgument(instantiatedBase, ts.last(getTypeArguments(type as ts.TypeReference))); - } - (type as ts.TypeReference).objectFlags |= ts.ObjectFlags.IdenticalBaseTypeExists; - return (type as ts.TypeReference).cachedEquivalentBaseType = instantiatedBase; } - - function isEmptyLiteralType(type: ts.Type): boolean { - return strictNullChecks ? type === implicitNeverType : type === undefinedWideningType; + const bases = getBaseTypes(target); + if (bases.length !== 1) { + return undefined; } - - function isEmptyArrayLiteralType(type: ts.Type): boolean { - const elementType = getElementTypeOfArrayType(type); - return !!elementType && isEmptyLiteralType(elementType); + if (getMembersOfSymbol(type.symbol).size) { + return undefined; // If the interface has any members, they may subtype members in the base, so we should do a full structural comparison } - - function isTupleLikeType(type: ts.Type): boolean { - return isTupleType(type) || !!getPropertyOfType(type, "0" as ts.__String); + let instantiatedBase = !ts.length(target.typeParameters) ? bases[0] : instantiateType(bases[0], createTypeMapper(target.typeParameters!, getTypeArguments(type as ts.TypeReference).slice(0, target.typeParameters!.length))); + if (ts.length(getTypeArguments(type as ts.TypeReference)) > ts.length(target.typeParameters)) { + instantiatedBase = getTypeWithThisArgument(instantiatedBase, ts.last(getTypeArguments(type as ts.TypeReference))); } + (type as ts.TypeReference).objectFlags |= ts.ObjectFlags.IdenticalBaseTypeExists; + return (type as ts.TypeReference).cachedEquivalentBaseType = instantiatedBase; + } - function isArrayOrTupleLikeType(type: ts.Type): boolean { - return isArrayLikeType(type) || isTupleLikeType(type); - } + function isEmptyLiteralType(type: ts.Type): boolean { + return strictNullChecks ? type === implicitNeverType : type === undefinedWideningType; + } - function getTupleElementType(type: ts.Type, index: number) { - const propType = getTypeOfPropertyOfType(type, "" + index as ts.__String); - if (propType) { - return propType; - } - if (everyType(type, isTupleType)) { - return mapType(type, t => getRestTypeOfTupleType(t as ts.TupleTypeReference) || undefinedType); - } - return undefined; - } + function isEmptyArrayLiteralType(type: ts.Type): boolean { + const elementType = getElementTypeOfArrayType(type); + return !!elementType && isEmptyLiteralType(elementType); + } - function isNeitherUnitTypeNorNever(type: ts.Type): boolean { - return !(type.flags & (ts.TypeFlags.Unit | ts.TypeFlags.Never)); - } + function isTupleLikeType(type: ts.Type): boolean { + return isTupleType(type) || !!getPropertyOfType(type, "0" as ts.__String); + } - function isUnitType(type: ts.Type): boolean { - return !!(type.flags & ts.TypeFlags.Unit); - } + function isArrayOrTupleLikeType(type: ts.Type): boolean { + return isArrayLikeType(type) || isTupleLikeType(type); + } - function isUnitLikeType(type: ts.Type): boolean { - return type.flags & ts.TypeFlags.Intersection ? ts.some((type as ts.IntersectionType).types, isUnitType) : - !!(type.flags & ts.TypeFlags.Unit); + function getTupleElementType(type: ts.Type, index: number) { + const propType = getTypeOfPropertyOfType(type, "" + index as ts.__String); + if (propType) { + return propType; } - - function extractUnitType(type: ts.Type) { - return type.flags & ts.TypeFlags.Intersection ? ts.find((type as ts.IntersectionType).types, isUnitType) || type : type; + if (everyType(type, isTupleType)) { + return mapType(type, t => getRestTypeOfTupleType(t as ts.TupleTypeReference) || undefinedType); } + return undefined; + } - function isLiteralType(type: ts.Type): boolean { - return type.flags & ts.TypeFlags.Boolean ? true : - type.flags & ts.TypeFlags.Union ? type.flags & ts.TypeFlags.EnumLiteral ? true : ts.every((type as ts.UnionType).types, isUnitType) : - isUnitType(type); - } + function isNeitherUnitTypeNorNever(type: ts.Type): boolean { + return !(type.flags & (ts.TypeFlags.Unit | ts.TypeFlags.Never)); + } - function getBaseTypeOfLiteralType(type: ts.Type): ts.Type { - return type.flags & ts.TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType(type as ts.LiteralType) : - type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) ? stringType : - type.flags & ts.TypeFlags.NumberLiteral ? numberType : - type.flags & ts.TypeFlags.BigIntLiteral ? bigintType : - type.flags & ts.TypeFlags.BooleanLiteral ? booleanType : - type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getBaseTypeOfLiteralType) : - type; - } + function isUnitType(type: ts.Type): boolean { + return !!(type.flags & ts.TypeFlags.Unit); + } - function getWidenedLiteralType(type: ts.Type): ts.Type { - return type.flags & ts.TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(type as ts.LiteralType) : - type.flags & ts.TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType : - type.flags & ts.TypeFlags.NumberLiteral && isFreshLiteralType(type) ? numberType : - type.flags & ts.TypeFlags.BigIntLiteral && isFreshLiteralType(type) ? bigintType : - type.flags & ts.TypeFlags.BooleanLiteral && isFreshLiteralType(type) ? booleanType : - type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getWidenedLiteralType) : - type; - } + function isUnitLikeType(type: ts.Type): boolean { + return type.flags & ts.TypeFlags.Intersection ? ts.some((type as ts.IntersectionType).types, isUnitType) : + !!(type.flags & ts.TypeFlags.Unit); + } - function getWidenedUniqueESSymbolType(type: ts.Type): ts.Type { - return type.flags & ts.TypeFlags.UniqueESSymbol ? esSymbolType : - type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getWidenedUniqueESSymbolType) : - type; - } + function extractUnitType(type: ts.Type) { + return type.flags & ts.TypeFlags.Intersection ? ts.find((type as ts.IntersectionType).types, isUnitType) || type : type; + } - function getWidenedLiteralLikeTypeForContextualType(type: ts.Type, contextualType: ts.Type | undefined) { - if (!isLiteralOfContextualType(type, contextualType)) { - type = getWidenedUniqueESSymbolType(getWidenedLiteralType(type)); - } - return type; - } + function isLiteralType(type: ts.Type): boolean { + return type.flags & ts.TypeFlags.Boolean ? true : + type.flags & ts.TypeFlags.Union ? type.flags & ts.TypeFlags.EnumLiteral ? true : ts.every((type as ts.UnionType).types, isUnitType) : + isUnitType(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); - } - return type; - } + function getBaseTypeOfLiteralType(type: ts.Type): ts.Type { + return type.flags & ts.TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType(type as ts.LiteralType) : + type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) ? stringType : + type.flags & ts.TypeFlags.NumberLiteral ? numberType : + type.flags & ts.TypeFlags.BigIntLiteral ? bigintType : + type.flags & ts.TypeFlags.BooleanLiteral ? booleanType : + type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getBaseTypeOfLiteralType) : + 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); - } - return type; - } + function getWidenedLiteralType(type: ts.Type): ts.Type { + return type.flags & ts.TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(type as ts.LiteralType) : + type.flags & ts.TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType : + type.flags & ts.TypeFlags.NumberLiteral && isFreshLiteralType(type) ? numberType : + type.flags & ts.TypeFlags.BigIntLiteral && isFreshLiteralType(type) ? bigintType : + type.flags & ts.TypeFlags.BooleanLiteral && isFreshLiteralType(type) ? booleanType : + type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getWidenedLiteralType) : + 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 ts.TupleTypeReference { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).target.objectFlags & ts.ObjectFlags.Tuple); - } + function getWidenedUniqueESSymbolType(type: ts.Type): ts.Type { + return type.flags & ts.TypeFlags.UniqueESSymbol ? esSymbolType : + type.flags & ts.TypeFlags.Union ? mapType(type as ts.UnionType, getWidenedUniqueESSymbolType) : + type; + } - function isGenericTupleType(type: ts.Type): type is ts.TupleTypeReference { - return isTupleType(type) && !!(type.target.combinedFlags & ts.ElementFlags.Variadic); + function getWidenedLiteralLikeTypeForContextualType(type: ts.Type, contextualType: ts.Type | undefined) { + if (!isLiteralOfContextualType(type, contextualType)) { + type = getWidenedUniqueESSymbolType(getWidenedLiteralType(type)); } + return type; + } - function isSingleElementGenericTupleType(type: ts.Type): type is ts.TupleTypeReference { - return isGenericTupleType(type) && type.target.elementFlags.length === 1; + 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); } + return type; + } - function getRestTypeOfTupleType(type: ts.TupleTypeReference) { - return getElementTypeOfSliceOfTupleType(type, type.target.fixedLength); + 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); } + return type; + } - function getRestArrayTypeOfTupleType(type: ts.TupleTypeReference) { - const restType = getRestTypeOfTupleType(type); - return restType && createArrayType(restType); - } + /** + * 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 ts.TupleTypeReference { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).target.objectFlags & ts.ObjectFlags.Tuple); + } - function getElementTypeOfSliceOfTupleType(type: ts.TupleTypeReference, index: number, endSkipCount = 0, writing = false) { - const length = getTypeReferenceArity(type) - endSkipCount; - if (index < length) { - const typeArguments = getTypeArguments(type); - const elementTypes: ts.Type[] = []; - for (let i = index; i < length; i++) { - const t = typeArguments[i]; - elementTypes.push(type.target.elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t); - } - return writing ? getIntersectionType(elementTypes) : getUnionType(elementTypes); - } - return undefined; - } + function isGenericTupleType(type: ts.Type): type is ts.TupleTypeReference { + return isTupleType(type) && !!(type.target.combinedFlags & ts.ElementFlags.Variadic); + } - function isTupleTypeStructureMatching(t1: ts.TupleTypeReference, t2: ts.TupleTypeReference) { - return getTypeReferenceArity(t1) === getTypeReferenceArity(t2) && - ts.every(t1.target.elementFlags, (f, i) => (f & ts.ElementFlags.Variable) === (t2.target.elementFlags[i] & ts.ElementFlags.Variable)); - } + function isSingleElementGenericTupleType(type: ts.Type): type is ts.TupleTypeReference { + return isGenericTupleType(type) && type.target.elementFlags.length === 1; + } - function isZeroBigInt({ value }: ts.BigIntLiteralType) { - return value.base10Value === "0"; - } + function getRestTypeOfTupleType(type: ts.TupleTypeReference) { + return getElementTypeOfSliceOfTupleType(type, type.target.fixedLength); + } - function getFalsyFlagsOfTypes(types: ts.Type[]): ts.TypeFlags { - let result: ts.TypeFlags = 0; - for (const t of types) { - result |= getFalsyFlags(t); + function getRestArrayTypeOfTupleType(type: ts.TupleTypeReference) { + const restType = getRestTypeOfTupleType(type); + return restType && createArrayType(restType); + } + + function getElementTypeOfSliceOfTupleType(type: ts.TupleTypeReference, index: number, endSkipCount = 0, writing = false) { + const length = getTypeReferenceArity(type) - endSkipCount; + if (index < length) { + const typeArguments = getTypeArguments(type); + const elementTypes: ts.Type[] = []; + for (let i = index; i < length; i++) { + const t = typeArguments[i]; + elementTypes.push(type.target.elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessType(t, numberType) : t); } - return result; + return writing ? getIntersectionType(elementTypes) : getUnionType(elementTypes); } + return undefined; + } - // 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): ts.TypeFlags { - return type.flags & ts.TypeFlags.Union ? getFalsyFlagsOfTypes((type as ts.UnionType).types) : - type.flags & ts.TypeFlags.StringLiteral ? (type as ts.StringLiteralType).value === "" ? ts.TypeFlags.StringLiteral : 0 : - type.flags & ts.TypeFlags.NumberLiteral ? (type as ts.NumberLiteralType).value === 0 ? ts.TypeFlags.NumberLiteral : 0 : - type.flags & ts.TypeFlags.BigIntLiteral ? isZeroBigInt(type as ts.BigIntLiteralType) ? ts.TypeFlags.BigIntLiteral : 0 : - type.flags & ts.TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? ts.TypeFlags.BooleanLiteral : 0 : - type.flags & ts.TypeFlags.PossiblyFalsy; - } - function removeDefinitelyFalsyTypes(type: ts.Type): ts.Type { - return getFalsyFlags(type) & ts.TypeFlags.DefinitelyFalsy ? - filterType(type, t => !(getFalsyFlags(t) & ts.TypeFlags.DefinitelyFalsy)) : - type; - } + function isTupleTypeStructureMatching(t1: ts.TupleTypeReference, t2: ts.TupleTypeReference) { + return getTypeReferenceArity(t1) === getTypeReferenceArity(t2) && + ts.every(t1.target.elementFlags, (f, i) => (f & ts.ElementFlags.Variable) === (t2.target.elementFlags[i] & ts.ElementFlags.Variable)); + } - function extractDefinitelyFalsyTypes(type: ts.Type): ts.Type { - return mapType(type, getDefinitelyFalsyPartOfType); - } + function isZeroBigInt({ value }: ts.BigIntLiteralType) { + return value.base10Value === "0"; + } - function getDefinitelyFalsyPartOfType(type: ts.Type): ts.Type { - return type.flags & ts.TypeFlags.String ? emptyStringType : - type.flags & ts.TypeFlags.Number ? zeroType : - type.flags & ts.TypeFlags.BigInt ? zeroBigIntType : - type === regularFalseType || - type === falseType || - type.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.AnyOrUnknown) || - type.flags & ts.TypeFlags.StringLiteral && (type as ts.StringLiteralType).value === "" || - type.flags & ts.TypeFlags.NumberLiteral && (type as ts.NumberLiteralType).value === 0 || - type.flags & ts.TypeFlags.BigIntLiteral && isZeroBigInt(type as ts.BigIntLiteralType) ? type : - neverType; + function getFalsyFlagsOfTypes(types: ts.Type[]): ts.TypeFlags { + let result: ts.TypeFlags = 0; + for (const t of types) { + result |= getFalsyFlags(t); } + return result; + } - /** - * 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: ts.TypeFlags): ts.Type { - const missing = (flags & ~type.flags) & (ts.TypeFlags.Undefined | ts.TypeFlags.Null); - return missing === 0 ? type : - missing === ts.TypeFlags.Undefined ? getUnionType([type, undefinedType]) : - missing === ts.TypeFlags.Null ? getUnionType([type, nullType]) : - getUnionType([type, undefinedType, nullType]); - } + // 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): ts.TypeFlags { + return type.flags & ts.TypeFlags.Union ? getFalsyFlagsOfTypes((type as ts.UnionType).types) : + type.flags & ts.TypeFlags.StringLiteral ? (type as ts.StringLiteralType).value === "" ? ts.TypeFlags.StringLiteral : 0 : + type.flags & ts.TypeFlags.NumberLiteral ? (type as ts.NumberLiteralType).value === 0 ? ts.TypeFlags.NumberLiteral : 0 : + type.flags & ts.TypeFlags.BigIntLiteral ? isZeroBigInt(type as ts.BigIntLiteralType) ? ts.TypeFlags.BigIntLiteral : 0 : + type.flags & ts.TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? ts.TypeFlags.BooleanLiteral : 0 : + type.flags & ts.TypeFlags.PossiblyFalsy; + } + function removeDefinitelyFalsyTypes(type: ts.Type): ts.Type { + return getFalsyFlags(type) & ts.TypeFlags.DefinitelyFalsy ? + filterType(type, t => !(getFalsyFlags(t) & ts.TypeFlags.DefinitelyFalsy)) : + type; + } + + function extractDefinitelyFalsyTypes(type: ts.Type): ts.Type { + return mapType(type, getDefinitelyFalsyPartOfType); + } + + function getDefinitelyFalsyPartOfType(type: ts.Type): ts.Type { + return type.flags & ts.TypeFlags.String ? emptyStringType : + type.flags & ts.TypeFlags.Number ? zeroType : + type.flags & ts.TypeFlags.BigInt ? zeroBigIntType : + type === regularFalseType || + type === falseType || + type.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.AnyOrUnknown) || + type.flags & ts.TypeFlags.StringLiteral && (type as ts.StringLiteralType).value === "" || + type.flags & ts.TypeFlags.NumberLiteral && (type as ts.NumberLiteralType).value === 0 || + type.flags & ts.TypeFlags.BigIntLiteral && isZeroBigInt(type as ts.BigIntLiteralType) ? 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: ts.TypeFlags): ts.Type { + const missing = (flags & ~type.flags) & (ts.TypeFlags.Undefined | ts.TypeFlags.Null); + return missing === 0 ? type : + missing === ts.TypeFlags.Undefined ? getUnionType([type, undefinedType]) : + missing === ts.TypeFlags.Null ? getUnionType([type, nullType]) : + getUnionType([type, undefinedType, nullType]); + } + + function getOptionalType(type: ts.Type, isProperty = false): ts.Type { + ts.Debug.assert(strictNullChecks); + return type.flags & ts.TypeFlags.Undefined ? type : getUnionType([type, isProperty ? missingType : undefinedType]); + } + + function getGlobalNonNullableTypeInstantiation(type: ts.Type) { + // First reduce away any constituents that are assignable to 'undefined' or 'null'. This not only eliminates + // 'undefined' and 'null', but also higher-order types such as a type parameter 'U extends undefined | null' + // that isn't eliminated by a NonNullable instantiation. + const reducedType = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + if (!deferredGlobalNonNullableTypeAlias) { + deferredGlobalNonNullableTypeAlias = getGlobalSymbol("NonNullable" as ts.__String, ts.SymbolFlags.TypeAlias, /*diagnostic*/ undefined) || unknownSymbol; + } + // If the NonNullable type is available, return an instantiation. Otherwise just return the reduced type. + return deferredGlobalNonNullableTypeAlias !== unknownSymbol ? + getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [reducedType]) : + reducedType; + } - function getOptionalType(type: ts.Type, isProperty = false): ts.Type { - ts.Debug.assert(strictNullChecks); - return type.flags & ts.TypeFlags.Undefined ? type : getUnionType([type, isProperty ? missingType : undefinedType]); - } + function getNonNullableType(type: ts.Type): ts.Type { + return strictNullChecks ? getGlobalNonNullableTypeInstantiation(type) : type; + } - function getGlobalNonNullableTypeInstantiation(type: ts.Type) { - // First reduce away any constituents that are assignable to 'undefined' or 'null'. This not only eliminates - // 'undefined' and 'null', but also higher-order types such as a type parameter 'U extends undefined | null' - // that isn't eliminated by a NonNullable instantiation. - const reducedType = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); - if (!deferredGlobalNonNullableTypeAlias) { - deferredGlobalNonNullableTypeAlias = getGlobalSymbol("NonNullable" as ts.__String, ts.SymbolFlags.TypeAlias, /*diagnostic*/ undefined) || unknownSymbol; - } - // If the NonNullable type is available, return an instantiation. Otherwise just return the reduced type. - return deferredGlobalNonNullableTypeAlias !== unknownSymbol ? - getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [reducedType]) : - reducedType; - } + function addOptionalTypeMarker(type: ts.Type) { + return strictNullChecks ? getUnionType([type, optionalType]) : type; + } - function getNonNullableType(type: ts.Type): ts.Type { - return strictNullChecks ? getGlobalNonNullableTypeInstantiation(type) : type; - } + function removeOptionalTypeMarker(type: ts.Type): ts.Type { + return strictNullChecks ? removeType(type, optionalType) : type; + } - function addOptionalTypeMarker(type: ts.Type) { - return strictNullChecks ? getUnionType([type, optionalType]) : type; - } + function propagateOptionalTypeMarker(type: ts.Type, node: ts.OptionalChain, wasOptional: boolean) { + return wasOptional ? ts.isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type; + } - function removeOptionalTypeMarker(type: ts.Type): ts.Type { - return strictNullChecks ? removeType(type, optionalType) : type; - } + function getOptionalExpressionType(exprType: ts.Type, expression: ts.Expression) { + return ts.isExpressionOfOptionalChainRoot(expression) ? getNonNullableType(exprType) : + ts.isOptionalChain(expression) ? removeOptionalTypeMarker(exprType) : + exprType; + } - function propagateOptionalTypeMarker(type: ts.Type, node: ts.OptionalChain, wasOptional: boolean) { - return wasOptional ? ts.isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type; - } + function removeMissingType(type: ts.Type, isOptional: boolean) { + return exactOptionalPropertyTypes && isOptional ? removeType(type, missingType) : type; + } - function getOptionalExpressionType(exprType: ts.Type, expression: ts.Expression) { - return ts.isExpressionOfOptionalChainRoot(expression) ? getNonNullableType(exprType) : - ts.isOptionalChain(expression) ? removeOptionalTypeMarker(exprType) : - exprType; - } + function containsMissingType(type: ts.Type) { + return exactOptionalPropertyTypes && (type === missingType || type.flags & ts.TypeFlags.Union && containsType((type as ts.UnionType).types, missingType)); + } - function removeMissingType(type: ts.Type, isOptional: boolean) { - return exactOptionalPropertyTypes && isOptional ? removeType(type, missingType) : type; - } + function removeMissingOrUndefinedType(type: ts.Type): ts.Type { + return exactOptionalPropertyTypes ? removeType(type, missingType) : getTypeWithFacts(type, TypeFacts.NEUndefined); + } - function containsMissingType(type: ts.Type) { - return exactOptionalPropertyTypes && (type === missingType || type.flags & ts.TypeFlags.Union && containsType((type as ts.UnionType).types, missingType)); - } + /** + * 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 & (ts.TypeFlags.Number | ts.TypeFlags.String | ts.TypeFlags.BooleanLiteral)) !== 0) + && ((target.flags & (ts.TypeFlags.Number | ts.TypeFlags.String | ts.TypeFlags.Boolean)) !== 0); + } - function removeMissingOrUndefinedType(type: ts.Type): ts.Type { - return exactOptionalPropertyTypes ? removeType(type, missingType) : getTypeWithFacts(type, TypeFacts.NEUndefined); - } + /** + * 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 & ts.TypeFlags.Intersection + ? ts.every((type as ts.IntersectionType).types, isObjectTypeWithInferableIndex) + : !!(type.symbol + && (type.symbol.flags & (ts.SymbolFlags.ObjectLiteral | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) !== 0 + && !(type.symbol.flags & ts.SymbolFlags.Class) + && !typeHasCallOrConstructSignatures(type)) || !!(ts.getObjectFlags(type) & ts.ObjectFlags.ReverseMapped && isObjectTypeWithInferableIndex((type as ts.ReverseMappedType).source)); + } + function createSymbolWithType(source: ts.Symbol, type: ts.Type | undefined) { + const symbol = createSymbol(source.flags, source.escapedName, ts.getCheckFlags(source) & ts.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; + } - /** - * 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 & (ts.TypeFlags.Number | ts.TypeFlags.String | ts.TypeFlags.BooleanLiteral)) !== 0) - && ((target.flags & (ts.TypeFlags.Number | ts.TypeFlags.String | ts.TypeFlags.Boolean)) !== 0); + function transformTypeOfMembers(type: ts.Type, f: (propertyType: ts.Type) => ts.Type) { + const members = ts.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; + } - /** - * 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 & ts.TypeFlags.Intersection - ? ts.every((type as ts.IntersectionType).types, isObjectTypeWithInferableIndex) - : !!(type.symbol - && (type.symbol.flags & (ts.SymbolFlags.ObjectLiteral | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) !== 0 - && !(type.symbol.flags & ts.SymbolFlags.Class) - && !typeHasCallOrConstructSignatures(type)) || !!(ts.getObjectFlags(type) & ts.ObjectFlags.ReverseMapped && isObjectTypeWithInferableIndex((type as ts.ReverseMappedType).source)); - } - function createSymbolWithType(source: ts.Symbol, type: ts.Type | undefined) { - const symbol = createSymbol(source.flags, source.escapedName, ts.getCheckFlags(source) & ts.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; + /** + * 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) && ts.getObjectFlags(type) & ts.ObjectFlags.FreshLiteral)) { + return type; } - - function transformTypeOfMembers(type: ts.Type, f: (propertyType: ts.Type) => ts.Type) { - const members = ts.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; + const regularType = (type as ts.FreshObjectLiteralType).regularType; + if (regularType) { + return regularType; } - /** - * 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) && ts.getObjectFlags(type) & ts.ObjectFlags.FreshLiteral)) { - return type; - } - const regularType = (type as ts.FreshObjectLiteralType).regularType; - if (regularType) { - return regularType; - } - - const resolved = type as ts.ResolvedType; - const members = transformTypeOfMembers(type, getRegularTypeOfObjectLiteral); - const regularNew = createAnonymousType(resolved.symbol, members, resolved.callSignatures, resolved.constructSignatures, resolved.indexInfos); - regularNew.flags = resolved.flags; - regularNew.objectFlags |= resolved.objectFlags & ~ts.ObjectFlags.FreshLiteral; - (type as ts.FreshObjectLiteralType).regularType = regularNew; - return regularNew; - } + const resolved = type as ts.ResolvedType; + const members = transformTypeOfMembers(type, getRegularTypeOfObjectLiteral); + const regularNew = createAnonymousType(resolved.symbol, members, resolved.callSignatures, resolved.constructSignatures, resolved.indexInfos); + regularNew.flags = resolved.flags; + regularNew.objectFlags |= resolved.objectFlags & ~ts.ObjectFlags.FreshLiteral; + (type as ts.FreshObjectLiteralType).regularType = regularNew; + return regularNew; + } - function createWideningContext(parent: ts.WideningContext | undefined, propertyName: ts.__String | undefined, siblings: ts.Type[] | undefined): ts.WideningContext { - return { parent, propertyName, siblings, resolvedProperties: undefined }; - } + function createWideningContext(parent: ts.WideningContext | undefined, propertyName: ts.__String | undefined, siblings: ts.Type[] | undefined): ts.WideningContext { + return { parent, propertyName, siblings, resolvedProperties: undefined }; + } - function getSiblingsOfContext(context: ts.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); - }); - } + function getSiblingsOfContext(context: ts.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); + }); } } - context.siblings = siblings; } - return context.siblings; + context.siblings = siblings; } + return context.siblings; + } - function getPropertiesOfContext(context: ts.WideningContext): ts.Symbol[] { - if (!context.resolvedProperties) { - const names = new ts.Map() as ts.UnderscoreEscapedMap; - for (const t of getSiblingsOfContext(context)) { - if (isObjectLiteralType(t) && !(ts.getObjectFlags(t) & ts.ObjectFlags.ContainsSpread)) { - for (const prop of getPropertiesOfType(t)) { - names.set(prop.escapedName, prop); - } + function getPropertiesOfContext(context: ts.WideningContext): ts.Symbol[] { + if (!context.resolvedProperties) { + const names = new ts.Map() as ts.UnderscoreEscapedMap; + for (const t of getSiblingsOfContext(context)) { + if (isObjectLiteralType(t) && !(ts.getObjectFlags(t) & ts.ObjectFlags.ContainsSpread)) { + for (const prop of getPropertiesOfType(t)) { + names.set(prop.escapedName, prop); } } - context.resolvedProperties = ts.arrayFrom(names.values()); } - return context.resolvedProperties; + context.resolvedProperties = ts.arrayFrom(names.values()); } + return context.resolvedProperties; + } - function getWidenedProperty(prop: ts.Symbol, context: ts.WideningContext | undefined): ts.Symbol { - if (!(prop.flags & ts.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 getWidenedProperty(prop: ts.Symbol, context: ts.WideningContext | undefined): ts.Symbol { + if (!(prop.flags & ts.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, missingType); - result.flags |= ts.SymbolFlags.Optional; - undefinedProperties.set(prop.escapedName, result); - return result; + function getUndefinedProperty(prop: ts.Symbol) { + const cached = undefinedProperties.get(prop.escapedName); + if (cached) { + return cached; } + const result = createSymbolWithType(prop, missingType); + result.flags |= ts.SymbolFlags.Optional; + undefinedProperties.set(prop.escapedName, result); + return result; + } - function getWidenedTypeOfObjectLiteral(type: ts.Type, context: ts.WideningContext | undefined): ts.Type { - const members = ts.createSymbolTable(); - for (const prop of getPropertiesOfObjectType(type)) { - members.set(prop.escapedName, getWidenedProperty(prop, context)); - } - if (context) { - for (const prop of getPropertiesOfContext(context)) { - if (!members.has(prop.escapedName)) { - members.set(prop.escapedName, getUndefinedProperty(prop)); - } + function getWidenedTypeOfObjectLiteral(type: ts.Type, context: ts.WideningContext | undefined): ts.Type { + const members = ts.createSymbolTable(); + for (const prop of getPropertiesOfObjectType(type)) { + members.set(prop.escapedName, getWidenedProperty(prop, context)); + } + if (context) { + for (const prop of getPropertiesOfContext(context)) { + if (!members.has(prop.escapedName)) { + members.set(prop.escapedName, getUndefinedProperty(prop)); } } - const result = createAnonymousType(type.symbol, members, ts.emptyArray, ts.emptyArray, ts.sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly))); - result.objectFlags |= (ts.getObjectFlags(type) & (ts.ObjectFlags.JSLiteral | ts.ObjectFlags.NonInferrableType)); // Retain js literal flag through widening - return result; } + const result = createAnonymousType(type.symbol, members, ts.emptyArray, ts.emptyArray, ts.sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly))); + result.objectFlags |= (ts.getObjectFlags(type) & (ts.ObjectFlags.JSLiteral | ts.ObjectFlags.NonInferrableType)); // Retain js literal flag through widening + return result; + } - function getWidenedType(type: ts.Type) { - return getWidenedTypeWithContext(type, /*context*/ undefined); - } + function getWidenedType(type: ts.Type) { + return getWidenedTypeWithContext(type, /*context*/ undefined); + } - function getWidenedTypeWithContext(type: ts.Type, context: ts.WideningContext | undefined): ts.Type { - if (ts.getObjectFlags(type) & ts.ObjectFlags.RequiresWidening) { - if (context === undefined && type.widened) { - return type.widened; - } - let result: ts.Type | undefined; - if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Nullable)) { - result = anyType; - } - else if (isObjectLiteralType(type)) { - result = getWidenedTypeOfObjectLiteral(type, context); - } - else if (type.flags & ts.TypeFlags.Union) { - const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (type as ts.UnionType).types); - const widenedTypes = ts.sameMap((type as ts.UnionType).types, t => t.flags & ts.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, ts.some(widenedTypes, isEmptyObjectType) ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal); - } - else if (type.flags & ts.TypeFlags.Intersection) { - result = getIntersectionType(ts.sameMap((type as ts.IntersectionType).types, getWidenedType)); - } - else if (isArrayOrTupleType(type)) { - result = createTypeReference(type.target, ts.sameMap(getTypeArguments(type), getWidenedType)); - } - if (result && context === undefined) { - type.widened = result; - } - return result || type; + function getWidenedTypeWithContext(type: ts.Type, context: ts.WideningContext | undefined): ts.Type { + if (ts.getObjectFlags(type) & ts.ObjectFlags.RequiresWidening) { + if (context === undefined && type.widened) { + return type.widened; } - return type; + let result: ts.Type | undefined; + if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Nullable)) { + result = anyType; + } + else if (isObjectLiteralType(type)) { + result = getWidenedTypeOfObjectLiteral(type, context); + } + else if (type.flags & ts.TypeFlags.Union) { + const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (type as ts.UnionType).types); + const widenedTypes = ts.sameMap((type as ts.UnionType).types, t => t.flags & ts.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, ts.some(widenedTypes, isEmptyObjectType) ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal); + } + else if (type.flags & ts.TypeFlags.Intersection) { + result = getIntersectionType(ts.sameMap((type as ts.IntersectionType).types, getWidenedType)); + } + else if (isArrayOrTupleType(type)) { + result = createTypeReference(type.target, ts.sameMap(getTypeArguments(type), getWidenedType)); + } + if (result && context === undefined) { + type.widened = result; + } + return result || type; } + 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 (ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType) { - if (type.flags & ts.TypeFlags.Union) { - if (ts.some((type as ts.UnionType).types, isEmptyObjectType)) { - errorReported = true; - } - else { - for (const t of (type as ts.UnionType).types) { - if (reportWideningErrorsInType(t)) { - errorReported = true; - } - } - } + /** + * 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 (ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType) { + if (type.flags & ts.TypeFlags.Union) { + if (ts.some((type as ts.UnionType).types, isEmptyObjectType)) { + errorReported = true; } - if (isArrayOrTupleType(type)) { - for (const t of getTypeArguments(type)) { + else { + for (const t of (type as ts.UnionType).types) { if (reportWideningErrorsInType(t)) { errorReported = true; } } } - if (isObjectLiteralType(type)) { - for (const p of getPropertiesOfObjectType(type)) { - const t = getTypeOfSymbol(p); - if (ts.getObjectFlags(t) & ts.ObjectFlags.ContainsWideningType) { - if (!reportWideningErrorsInType(t)) { - error(p.valueDeclaration, ts.Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolToString(p), typeToString(getWidenedType(t))); - } - errorReported = true; + } + if (isArrayOrTupleType(type)) { + for (const t of getTypeArguments(type)) { + if (reportWideningErrorsInType(t)) { + errorReported = true; + } + } + } + if (isObjectLiteralType(type)) { + for (const p of getPropertiesOfObjectType(type)) { + const t = getTypeOfSymbol(p); + if (ts.getObjectFlags(t) & ts.ObjectFlags.ContainsWideningType) { + if (!reportWideningErrorsInType(t)) { + error(p.valueDeclaration, ts.Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolToString(p), typeToString(getWidenedType(t))); } + errorReported = true; } } } - return errorReported; } + return errorReported; + } - function reportImplicitAny(declaration: ts.Declaration, type: ts.Type, wideningKind?: WideningKind) { - const typeAsString = typeToString(getWidenedType(type)); - if (ts.isInJSFile(declaration) && !ts.isCheckJsEnabledForFile(ts.getSourceFileOfNode(declaration), compilerOptions)) { - // Only report implicit any errors/suggestions in TS and ts-check JS files - return; - } - let diagnostic: ts.DiagnosticMessage; - switch (declaration.kind) { - case ts.SyntaxKind.BinaryExpression: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - diagnostic = noImplicitAny ? ts.Diagnostics.Member_0_implicitly_has_an_1_type : ts.Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; - break; - case ts.SyntaxKind.Parameter: - const param = declaration as ts.ParameterDeclaration; - if (ts.isIdentifier(param.name) && - (ts.isCallSignatureDeclaration(param.parent) || ts.isMethodSignature(param.parent) || ts.isFunctionTypeNode(param.parent)) && - param.parent.parameters.indexOf(param) > -1 && - (resolveName(param, param.name.escapedText, ts.SymbolFlags.Type, undefined, param.name.escapedText, /*isUse*/ true) || - param.name.originalKeywordKind && ts.isTypeNodeKind(param.name.originalKeywordKind))) { - const newName = "arg" + param.parent.parameters.indexOf(param); - const typeName = ts.declarationNameToString(param.name) + (param.dotDotDotToken ? "[]" : ""); - errorOrSuggestion(noImplicitAny, declaration, ts.Diagnostics.Parameter_has_a_name_but_no_type_Did_you_mean_0_Colon_1, newName, typeName); - return; - } - diagnostic = (declaration as ts.ParameterDeclaration).dotDotDotToken ? - noImplicitAny ? ts.Diagnostics.Rest_parameter_0_implicitly_has_an_any_type : ts.Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage : - noImplicitAny ? ts.Diagnostics.Parameter_0_implicitly_has_an_1_type : ts.Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; - break; - case ts.SyntaxKind.BindingElement: - diagnostic = ts.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; - } - break; - case ts.SyntaxKind.JSDocFunctionType: - error(declaration, ts.Diagnostics.Function_type_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); + function reportImplicitAny(declaration: ts.Declaration, type: ts.Type, wideningKind?: WideningKind) { + const typeAsString = typeToString(getWidenedType(type)); + if (ts.isInJSFile(declaration) && !ts.isCheckJsEnabledForFile(ts.getSourceFileOfNode(declaration), compilerOptions)) { + // Only report implicit any errors/suggestions in TS and ts-check JS files + return; + } + let diagnostic: ts.DiagnosticMessage; + switch (declaration.kind) { + case ts.SyntaxKind.BinaryExpression: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + diagnostic = noImplicitAny ? ts.Diagnostics.Member_0_implicitly_has_an_1_type : ts.Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; + break; + case ts.SyntaxKind.Parameter: + const param = declaration as ts.ParameterDeclaration; + if (ts.isIdentifier(param.name) && + (ts.isCallSignatureDeclaration(param.parent) || ts.isMethodSignature(param.parent) || ts.isFunctionTypeNode(param.parent)) && + param.parent.parameters.indexOf(param) > -1 && + (resolveName(param, param.name.escapedText, ts.SymbolFlags.Type, undefined, param.name.escapedText, /*isUse*/ true) || + param.name.originalKeywordKind && ts.isTypeNodeKind(param.name.originalKeywordKind))) { + const newName = "arg" + param.parent.parameters.indexOf(param); + const typeName = ts.declarationNameToString(param.name) + (param.dotDotDotToken ? "[]" : ""); + errorOrSuggestion(noImplicitAny, declaration, ts.Diagnostics.Parameter_has_a_name_but_no_type_Did_you_mean_0_Colon_1, newName, typeName); return; - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - if (noImplicitAny && !(declaration as ts.NamedDeclaration).name) { - if (wideningKind === WideningKind.GeneratorYield) { - error(declaration, ts.Diagnostics.Generator_implicitly_has_yield_type_0_because_it_does_not_yield_any_values_Consider_supplying_a_return_type_annotation, typeAsString); - } - else { - error(declaration, ts.Diagnostics.Function_expression_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); - } - return; + } + diagnostic = (declaration as ts.ParameterDeclaration).dotDotDotToken ? + noImplicitAny ? ts.Diagnostics.Rest_parameter_0_implicitly_has_an_any_type : ts.Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage : + noImplicitAny ? ts.Diagnostics.Parameter_0_implicitly_has_an_1_type : ts.Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; + break; + case ts.SyntaxKind.BindingElement: + diagnostic = ts.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; + } + break; + case ts.SyntaxKind.JSDocFunctionType: + error(declaration, ts.Diagnostics.Function_type_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); + return; + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + if (noImplicitAny && !(declaration as ts.NamedDeclaration).name) { + if (wideningKind === WideningKind.GeneratorYield) { + error(declaration, ts.Diagnostics.Generator_implicitly_has_yield_type_0_because_it_does_not_yield_any_values_Consider_supplying_a_return_type_annotation, typeAsString); } - diagnostic = !noImplicitAny ? ts.Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage : - wideningKind === WideningKind.GeneratorYield ? ts.Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_yield_type : - ts.Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type; - break; - case ts.SyntaxKind.MappedType: - if (noImplicitAny) { - error(declaration, ts.Diagnostics.Mapped_object_type_implicitly_has_an_any_template_type); + else { + error(declaration, ts.Diagnostics.Function_expression_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); } return; - default: - diagnostic = noImplicitAny ? ts.Diagnostics.Variable_0_implicitly_has_an_1_type : ts.Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; - } - errorOrSuggestion(noImplicitAny, declaration, diagnostic, ts.declarationNameToString(ts.getNameOfDeclaration(declaration)), typeAsString); - } - - function reportErrorsFromWidening(declaration: ts.Declaration, type: ts.Type, wideningKind?: WideningKind) { - addLazyDiagnostic(() => { - if (noImplicitAny && ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType && (!wideningKind || !getContextualSignatureForFunctionLikeDeclaration(declaration as ts.FunctionLikeDeclaration))) { - // Report implicit any error within type if possible, otherwise report error on declaration - if (!reportWideningErrorsInType(type)) { - reportImplicitAny(declaration, type, wideningKind); - } } - }); + diagnostic = !noImplicitAny ? ts.Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage : + wideningKind === WideningKind.GeneratorYield ? ts.Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_yield_type : + ts.Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type; + break; + case ts.SyntaxKind.MappedType: + if (noImplicitAny) { + error(declaration, ts.Diagnostics.Mapped_object_type_implicitly_has_an_any_template_type); + } + return; + default: + diagnostic = noImplicitAny ? ts.Diagnostics.Variable_0_implicitly_has_an_1_type : ts.Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; } + errorOrSuggestion(noImplicitAny, declaration, diagnostic, ts.declarationNameToString(ts.getNameOfDeclaration(declaration)), typeAsString); + } - 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); + function reportErrorsFromWidening(declaration: ts.Declaration, type: ts.Type, wideningKind?: WideningKind) { + addLazyDiagnostic(() => { + if (noImplicitAny && ts.getObjectFlags(type) & ts.ObjectFlags.ContainsWideningType && (!wideningKind || !getContextualSignatureForFunctionLikeDeclaration(declaration as ts.FunctionLikeDeclaration))) { + // Report implicit any error within type if possible, otherwise report error on declaration + if (!reportWideningErrorsInType(type)) { + reportImplicitAny(declaration, type, wideningKind); } } - for (let i = 0; i < paramCount; i++) { - callback(getTypeAtPosition(source, i), getTypeAtPosition(target, i)); - } - if (targetRestType) { - callback(getRestTypeAtPosition(source, paramCount), targetRestType); - } - } + }); + } - 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); - } - else { - callback(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); + 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); } } - - function createInferenceContext(typeParameters: readonly ts.TypeParameter[], signature: ts.Signature | undefined, flags: ts.InferenceFlags, compareTypes?: ts.TypeComparer): ts.InferenceContext { - return createInferenceContextWorker(typeParameters.map(createInferenceInfo), signature, flags, compareTypes || compareTypesAssignable); + for (let i = 0; i < paramCount; i++) { + callback(getTypeAtPosition(source, i), getTypeAtPosition(target, i)); } - - function cloneInferenceContext(context: T, extraFlags: ts.InferenceFlags = 0): ts.InferenceContext | (T & undefined) { - return context && createInferenceContextWorker(ts.map(context.inferences, cloneInferenceInfo), context.signature, context.flags | extraFlags, context.compareTypes); + if (targetRestType) { + callback(getRestTypeAtPosition(source, paramCount), targetRestType); } + } - function createInferenceContextWorker(inferences: ts.InferenceInfo[], signature: ts.Signature | undefined, flags: ts.InferenceFlags, compareTypes: ts.TypeComparer): ts.InferenceContext { - const context: ts.InferenceContext = { - inferences, - signature, - flags, - compareTypes, - mapper: makeFunctionTypeMapper(t => mapToInferredType(context, t, /*fix*/ true)), - nonFixingMapper: makeFunctionTypeMapper(t => mapToInferredType(context, t, /*fix*/ false)), - }; - return context; + 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); } + else { + callback(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); + } + } - function mapToInferredType(context: ts.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) { - // Before we commit to a particular inference (and thus lock out any further inferences), - // we infer from any intra-expression inference sites we have collected. - inferFromIntraExpressionSites(context); - clearCachedInferences(inferences); - inference.isFixed = true; - } - return getInferredType(context, i); + function createInferenceContext(typeParameters: readonly ts.TypeParameter[], signature: ts.Signature | undefined, flags: ts.InferenceFlags, compareTypes?: ts.TypeComparer): ts.InferenceContext { + return createInferenceContextWorker(typeParameters.map(createInferenceInfo), signature, flags, compareTypes || compareTypesAssignable); + } + + function cloneInferenceContext(context: T, extraFlags: ts.InferenceFlags = 0): ts.InferenceContext | (T & undefined) { + return context && createInferenceContextWorker(ts.map(context.inferences, cloneInferenceInfo), context.signature, context.flags | extraFlags, context.compareTypes); + } + + function createInferenceContextWorker(inferences: ts.InferenceInfo[], signature: ts.Signature | undefined, flags: ts.InferenceFlags, compareTypes: ts.TypeComparer): ts.InferenceContext { + const context: ts.InferenceContext = { + inferences, + signature, + flags, + compareTypes, + mapper: makeFunctionTypeMapper(t => mapToInferredType(context, t, /*fix*/ true)), + nonFixingMapper: makeFunctionTypeMapper(t => mapToInferredType(context, t, /*fix*/ false)), + }; + return context; + } + + function mapToInferredType(context: ts.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) { + // Before we commit to a particular inference (and thus lock out any further inferences), + // we infer from any intra-expression inference sites we have collected. + inferFromIntraExpressionSites(context); + clearCachedInferences(inferences); + inference.isFixed = true; } + return getInferredType(context, i); } - return t; } + return t; + } - function clearCachedInferences(inferences: ts.InferenceInfo[]) { - for (const inference of inferences) { - if (!inference.isFixed) { - inference.inferredType = undefined; - } + function clearCachedInferences(inferences: ts.InferenceInfo[]) { + for (const inference of inferences) { + if (!inference.isFixed) { + inference.inferredType = undefined; } } + } - function addIntraExpressionInferenceSite(context: ts.InferenceContext, node: ts.Expression | ts.MethodDeclaration, type: ts.Type) { - (context.intraExpressionInferenceSites ??= []).push({ node, type }); - } + function addIntraExpressionInferenceSite(context: ts.InferenceContext, node: ts.Expression | ts.MethodDeclaration, type: ts.Type) { + (context.intraExpressionInferenceSites ??= []).push({ node, type }); + } - // We collect intra-expression inference sites within object and array literals to handle cases where - // inferred types flow between context sensitive element expressions. For example: - // - // declare function foo(arg: [(n: number) => T, (x: T) => void]): void; - // foo([_a => 0, n => n.toFixed()]); - // - // Above, both arrow functions in the tuple argument are context sensitive, thus both are omitted from the - // pass that collects inferences from the non-context sensitive parts of the arguments. In the subsequent - // pass where nothing is omitted, we need to commit to an inference for T in order to contextually type the - // parameter in the second arrow function, but we want to first infer from the return type of the first - // arrow function. This happens automatically when the arrow functions are discrete arguments (because we - // infer from each argument before processing the next), but when the arrow functions are elements of an - // object or array literal, we need to perform intra-expression inferences early. - function inferFromIntraExpressionSites(context: ts.InferenceContext) { - if (context.intraExpressionInferenceSites) { - for (const { node, type } of context.intraExpressionInferenceSites) { - const contextualType = node.kind === ts.SyntaxKind.MethodDeclaration ? - getContextualTypeForObjectLiteralMethod(node as ts.MethodDeclaration, ts.ContextFlags.NoConstraints) : - getContextualType(node, ts.ContextFlags.NoConstraints); - if (contextualType) { - inferTypes(context.inferences, type, contextualType); - } + // We collect intra-expression inference sites within object and array literals to handle cases where + // inferred types flow between context sensitive element expressions. For example: + // + // declare function foo(arg: [(n: number) => T, (x: T) => void]): void; + // foo([_a => 0, n => n.toFixed()]); + // + // Above, both arrow functions in the tuple argument are context sensitive, thus both are omitted from the + // pass that collects inferences from the non-context sensitive parts of the arguments. In the subsequent + // pass where nothing is omitted, we need to commit to an inference for T in order to contextually type the + // parameter in the second arrow function, but we want to first infer from the return type of the first + // arrow function. This happens automatically when the arrow functions are discrete arguments (because we + // infer from each argument before processing the next), but when the arrow functions are elements of an + // object or array literal, we need to perform intra-expression inferences early. + function inferFromIntraExpressionSites(context: ts.InferenceContext) { + if (context.intraExpressionInferenceSites) { + for (const { node, type } of context.intraExpressionInferenceSites) { + const contextualType = node.kind === ts.SyntaxKind.MethodDeclaration ? + getContextualTypeForObjectLiteralMethod(node as ts.MethodDeclaration, ts.ContextFlags.NoConstraints) : + getContextualType(node, ts.ContextFlags.NoConstraints); + if (contextualType) { + inferTypes(context.inferences, type, contextualType); } - context.intraExpressionInferenceSites = undefined; } + context.intraExpressionInferenceSites = undefined; } + } - function createInferenceInfo(typeParameter: ts.TypeParameter): ts.InferenceInfo { - return { - typeParameter, - candidates: undefined, - contraCandidates: undefined, - inferredType: undefined, - priority: undefined, - topLevel: true, - isFixed: false, - impliedArity: undefined - }; - } - - function cloneInferenceInfo(inference: ts.InferenceInfo): ts.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, - impliedArity: inference.impliedArity - }; - } + function createInferenceInfo(typeParameter: ts.TypeParameter): ts.InferenceInfo { + return { + typeParameter, + candidates: undefined, + contraCandidates: undefined, + inferredType: undefined, + priority: undefined, + topLevel: true, + isFixed: false, + impliedArity: undefined + }; + } - function cloneInferredPartOfContext(context: ts.InferenceContext): ts.InferenceContext | undefined { - const inferences = ts.filter(context.inferences, hasInferenceCandidates); - return inferences.length ? - createInferenceContextWorker(ts.map(inferences, cloneInferenceInfo), context.signature, context.flags, context.compareTypes) : - undefined; - } + function cloneInferenceInfo(inference: ts.InferenceInfo): ts.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, + impliedArity: inference.impliedArity + }; + } - function getMapperFromContext(context: T): ts.TypeMapper | (T & undefined) { - return context && context.mapper; - } + function cloneInferredPartOfContext(context: ts.InferenceContext): ts.InferenceContext | undefined { + const inferences = ts.filter(context.inferences, hasInferenceCandidates); + return inferences.length ? + createInferenceContextWorker(ts.map(inferences, cloneInferenceInfo), context.signature, context.flags, context.compareTypes) : + undefined; + } - // 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 = ts.getObjectFlags(type); - if (objectFlags & ts.ObjectFlags.CouldContainTypeVariablesComputed) { - return !!(objectFlags & ts.ObjectFlags.CouldContainTypeVariables); - } - const result = !!(type.flags & ts.TypeFlags.Instantiable || - type.flags & ts.TypeFlags.Object && !isNonGenericTopLevelType(type) && (objectFlags & ts.ObjectFlags.Reference && ((type as ts.TypeReference).node || ts.forEach(getTypeArguments(type as ts.TypeReference), couldContainTypeVariables)) || - objectFlags & ts.ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.ObjectLiteral) && type.symbol.declarations || - objectFlags & (ts.ObjectFlags.Mapped | ts.ObjectFlags.ReverseMapped | ts.ObjectFlags.ObjectRestType | ts.ObjectFlags.InstantiationExpressionType)) || - type.flags & ts.TypeFlags.UnionOrIntersection && !(type.flags & ts.TypeFlags.EnumLiteral) && !isNonGenericTopLevelType(type) && ts.some((type as ts.UnionOrIntersectionType).types, couldContainTypeVariables)); - if (type.flags & ts.TypeFlags.ObjectFlagsType) { - (type as ts.ObjectFlagsType).objectFlags |= ts.ObjectFlags.CouldContainTypeVariablesComputed | (result ? ts.ObjectFlags.CouldContainTypeVariables : 0); - } - return result; - } + function getMapperFromContext(context: T): ts.TypeMapper | (T & undefined) { + return context && context.mapper; + } - function isNonGenericTopLevelType(type: ts.Type) { - if (type.aliasSymbol && !type.aliasTypeArguments) { - const declaration = ts.getDeclarationOfKind(type.aliasSymbol, ts.SyntaxKind.TypeAliasDeclaration); - return !!(declaration && ts.findAncestor(declaration.parent, n => n.kind === ts.SyntaxKind.SourceFile ? true : n.kind === ts.SyntaxKind.ModuleDeclaration ? false : "quit")); - } - return false; - } + // 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 = ts.getObjectFlags(type); + if (objectFlags & ts.ObjectFlags.CouldContainTypeVariablesComputed) { + return !!(objectFlags & ts.ObjectFlags.CouldContainTypeVariables); + } + const result = !!(type.flags & ts.TypeFlags.Instantiable || + type.flags & ts.TypeFlags.Object && !isNonGenericTopLevelType(type) && (objectFlags & ts.ObjectFlags.Reference && ((type as ts.TypeReference).node || ts.forEach(getTypeArguments(type as ts.TypeReference), couldContainTypeVariables)) || + objectFlags & ts.ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.ObjectLiteral) && type.symbol.declarations || + objectFlags & (ts.ObjectFlags.Mapped | ts.ObjectFlags.ReverseMapped | ts.ObjectFlags.ObjectRestType | ts.ObjectFlags.InstantiationExpressionType)) || + type.flags & ts.TypeFlags.UnionOrIntersection && !(type.flags & ts.TypeFlags.EnumLiteral) && !isNonGenericTopLevelType(type) && ts.some((type as ts.UnionOrIntersectionType).types, couldContainTypeVariables)); + if (type.flags & ts.TypeFlags.ObjectFlagsType) { + (type as ts.ObjectFlagsType).objectFlags |= ts.ObjectFlags.CouldContainTypeVariablesComputed | (result ? ts.ObjectFlags.CouldContainTypeVariables : 0); + } + return result; + } - function isTypeParameterAtTopLevel(type: ts.Type, typeParameter: ts.TypeParameter): boolean { - return !!(type === typeParameter || - type.flags & ts.TypeFlags.UnionOrIntersection && ts.some((type as ts.UnionOrIntersectionType).types, t => isTypeParameterAtTopLevel(t, typeParameter)) || - type.flags & ts.TypeFlags.Conditional && (getTrueTypeFromConditionalType(type as ts.ConditionalType) === typeParameter || getFalseTypeFromConditionalType(type as ts.ConditionalType) === typeParameter)); + function isNonGenericTopLevelType(type: ts.Type) { + if (type.aliasSymbol && !type.aliasTypeArguments) { + const declaration = ts.getDeclarationOfKind(type.aliasSymbol, ts.SyntaxKind.TypeAliasDeclaration); + return !!(declaration && ts.findAncestor(declaration.parent, n => n.kind === ts.SyntaxKind.SourceFile ? true : n.kind === ts.SyntaxKind.ModuleDeclaration ? false : "quit")); } + return false; + } - /** Create an object with properties named in the string literal type. Every property has type `any` */ - function createEmptyObjectTypeFromStringLiteral(type: ts.Type) { - const members = ts.createSymbolTable(); - forEachType(type, t => { - if (!(t.flags & ts.TypeFlags.StringLiteral)) { - return; - } - const name = ts.escapeLeadingUnderscores((t as ts.StringLiteralType).value); - const literalProp = createSymbol(ts.SymbolFlags.Property, name); - literalProp.type = anyType; - if (t.symbol) { - literalProp.declarations = t.symbol.declarations; - literalProp.valueDeclaration = t.symbol.valueDeclaration; - } - members.set(name, literalProp); - }); - const indexInfos = type.flags & ts.TypeFlags.String ? [createIndexInfo(stringType, emptyObjectType, /*isReadonly*/ false)] : ts.emptyArray; - return createAnonymousType(undefined, members, ts.emptyArray, ts.emptyArray, indexInfos); - } + function isTypeParameterAtTopLevel(type: ts.Type, typeParameter: ts.TypeParameter): boolean { + return !!(type === typeParameter || + type.flags & ts.TypeFlags.UnionOrIntersection && ts.some((type as ts.UnionOrIntersectionType).types, t => isTypeParameterAtTopLevel(t, typeParameter)) || + type.flags & ts.TypeFlags.Conditional && (getTrueTypeFromConditionalType(type as ts.ConditionalType) === typeParameter || getFalseTypeFromConditionalType(type as ts.ConditionalType) === typeParameter)); + } - /** - * 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: ts.MappedType, constraint: ts.IndexType): ts.Type | undefined { - if (inInferTypeForHomomorphicMappedType) { - return undefined; + /** Create an object with properties named in the string literal type. Every property has type `any` */ + function createEmptyObjectTypeFromStringLiteral(type: ts.Type) { + const members = ts.createSymbolTable(); + forEachType(type, t => { + if (!(t.flags & ts.TypeFlags.StringLiteral)) { + return; } - const key = source.id + "," + target.id + "," + constraint.id; - if (reverseMappedCache.has(key)) { - return reverseMappedCache.get(key); + const name = ts.escapeLeadingUnderscores((t as ts.StringLiteralType).value); + const literalProp = createSymbol(ts.SymbolFlags.Property, name); + literalProp.type = anyType; + if (t.symbol) { + literalProp.declarations = t.symbol.declarations; + literalProp.valueDeclaration = t.symbol.valueDeclaration; } - inInferTypeForHomomorphicMappedType = true; - const type = createReverseMappedType(source, target, constraint); - inInferTypeForHomomorphicMappedType = false; - reverseMappedCache.set(key, type); - return type; - } + members.set(name, literalProp); + }); + const indexInfos = type.flags & ts.TypeFlags.String ? [createIndexInfo(stringType, emptyObjectType, /*isReadonly*/ false)] : ts.emptyArray; + return createAnonymousType(undefined, members, ts.emptyArray, ts.emptyArray, indexInfos); + } - // 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 !(ts.getObjectFlags(type) & ts.ObjectFlags.NonInferrableType) || - isObjectLiteralType(type) && ts.some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))) || - isTupleType(type) && ts.some(getTypeArguments(type), isPartiallyInferableType); + /** + * 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: ts.MappedType, constraint: ts.IndexType): ts.Type | undefined { + if (inInferTypeForHomomorphicMappedType) { + return undefined; } + const key = source.id + "," + target.id + "," + constraint.id; + if (reverseMappedCache.has(key)) { + return reverseMappedCache.get(key); + } + inInferTypeForHomomorphicMappedType = true; + const type = createReverseMappedType(source, target, constraint); + inInferTypeForHomomorphicMappedType = false; + reverseMappedCache.set(key, type); + return type; + } - function createReverseMappedType(source: ts.Type, target: ts.MappedType, constraint: ts.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, stringType) || 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)); - } - if (isTupleType(source)) { - const elementTypes = ts.map(getTypeArguments(source), t => inferReverseMappedType(t, target, constraint)); - const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? - ts.sameMap(source.target.elementFlags, f => f & ts.ElementFlags.Optional ? ts.ElementFlags.Required : f) : - source.target.elementFlags; - return createTupleType(elementTypes, elementFlags, source.target.readonly, source.target.labeledElementDeclarations); - } - // 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(ts.ObjectFlags.ReverseMapped | ts.ObjectFlags.Anonymous, /*symbol*/ undefined) as ts.ReverseMappedType; - reversed.source = source; - reversed.mappedType = target; - reversed.constraintType = constraint; - return reversed; - } - - function getTypeOfReverseMappedSymbol(symbol: ts.ReverseMappedSymbol) { - const links = getSymbolLinks(symbol); - if (!links.type) { - links.type = inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType); - } - return links.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 !(ts.getObjectFlags(type) & ts.ObjectFlags.NonInferrableType) || + isObjectLiteralType(type) && ts.some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))) || + isTupleType(type) && ts.some(getTypeArguments(type), isPartiallyInferableType); + } + + function createReverseMappedType(source: ts.Type, target: ts.MappedType, constraint: ts.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, stringType) || 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)); + } + if (isTupleType(source)) { + const elementTypes = ts.map(getTypeArguments(source), t => inferReverseMappedType(t, target, constraint)); + const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? + ts.sameMap(source.target.elementFlags, f => f & ts.ElementFlags.Optional ? ts.ElementFlags.Required : f) : + source.target.elementFlags; + return createTupleType(elementTypes, elementFlags, source.target.readonly, source.target.labeledElementDeclarations); + } + // 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(ts.ObjectFlags.ReverseMapped | ts.ObjectFlags.Anonymous, /*symbol*/ undefined) as ts.ReverseMappedType; + reversed.source = source; + reversed.mappedType = target; + reversed.constraintType = constraint; + return reversed; + } - function inferReverseMappedType(sourceType: ts.Type, target: ts.MappedType, constraint: ts.IndexType): ts.Type { - const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as ts.TypeParameter; - const templateType = getTemplateTypeFromMappedType(target); - const inference = createInferenceInfo(typeParameter); - inferTypes([inference], sourceType, templateType); - return getTypeFromInference(inference) || unknownType; + function getTypeOfReverseMappedSymbol(symbol: ts.ReverseMappedSymbol) { + const links = getSymbolLinks(symbol); + if (!links.type) { + links.type = inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType); } + return links.type; + } - 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; + function inferReverseMappedType(sourceType: ts.Type, target: ts.MappedType, constraint: ts.IndexType): ts.Type { + const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as ts.TypeParameter; + 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; + } + if (requireOptionalProperties || !(targetProp.flags & ts.SymbolFlags.Optional || ts.getCheckFlags(targetProp) & ts.CheckFlags.Partial)) { + const sourceProp = getPropertyOfType(source, targetProp.escapedName); + if (!sourceProp) { + yield targetProp; } - if (requireOptionalProperties || !(targetProp.flags & ts.SymbolFlags.Optional || ts.getCheckFlags(targetProp) & ts.CheckFlags.Partial)) { - const sourceProp = getPropertyOfType(source, targetProp.escapedName); - if (!sourceProp) { - yield targetProp; - } - else if (matchDiscriminantProperties) { - const targetType = getTypeOfSymbol(targetProp); - if (targetType.flags & ts.TypeFlags.Unit) { - const sourceType = getTypeOfSymbol(sourceProp); - if (!(sourceType.flags & ts.TypeFlags.Any || getRegularTypeOfLiteralType(sourceType) === getRegularTypeOfLiteralType(targetType))) { - yield targetProp; - } + else if (matchDiscriminantProperties) { + const targetType = getTypeOfSymbol(targetProp); + if (targetType.flags & ts.TypeFlags.Unit) { + const sourceType = getTypeOfSymbol(sourceProp); + if (!(sourceType.flags & ts.TypeFlags.Any || getRegularTypeOfLiteralType(sourceType) === getRegularTypeOfLiteralType(targetType))) { + yield targetProp; } } } } } + } - 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 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: ts.TupleTypeReference, target: ts.TupleTypeReference) { - return !(target.target.combinedFlags & ts.ElementFlags.Variadic) && target.target.minLength > source.target.minLength || - !target.target.hasRestElement && (source.target.hasRestElement || target.target.fixedLength < source.target.fixedLength); - } + function tupleTypesDefinitelyUnrelated(source: ts.TupleTypeReference, target: ts.TupleTypeReference) { + return !(target.target.combinedFlags & ts.ElementFlags.Variadic) && target.target.minLength > source.target.minLength || + !target.target.hasRestElement && (source.target.hasRestElement || target.target.fixedLength < source.target.fixedLength); + } - 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*/ false); - } + 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*/ false); + } - function getTypeFromInference(inference: ts.InferenceInfo) { - return inference.candidates ? getUnionType(inference.candidates, ts.UnionReduction.Subtype) : - inference.contraCandidates ? getIntersectionType(inference.contraCandidates) : - undefined; - } + function getTypeFromInference(inference: ts.InferenceInfo) { + return inference.candidates ? getUnionType(inference.candidates, ts.UnionReduction.Subtype) : + inference.contraCandidates ? getIntersectionType(inference.contraCandidates) : + undefined; + } - function hasSkipDirectInferenceFlag(node: ts.Node) { - return !!getNodeLinks(node).skipDirectInference; - } + function hasSkipDirectInferenceFlag(node: ts.Node) { + return !!getNodeLinks(node).skipDirectInference; + } - function isFromInferenceBlockedSource(type: ts.Type) { - return !!(type.symbol && ts.some(type.symbol.declarations, hasSkipDirectInferenceFlag)); - } + function isFromInferenceBlockedSource(type: ts.Type) { + return !!(type.symbol && ts.some(type.symbol.declarations, hasSkipDirectInferenceFlag)); + } - function templateLiteralTypesDefinitelyUnrelated(source: ts.TemplateLiteralType, target: ts.TemplateLiteralType) { - // Two template literal types with diffences in their starting or ending text spans are definitely unrelated. - const sourceStart = source.texts[0]; - const targetStart = target.texts[0]; - const sourceEnd = source.texts[source.texts.length - 1]; - const targetEnd = target.texts[target.texts.length - 1]; - const startLen = Math.min(sourceStart.length, targetStart.length); - const endLen = Math.min(sourceEnd.length, targetEnd.length); - return sourceStart.slice(0, startLen) !== targetStart.slice(0, startLen) || - sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen); - } + function templateLiteralTypesDefinitelyUnrelated(source: ts.TemplateLiteralType, target: ts.TemplateLiteralType) { + // Two template literal types with diffences in their starting or ending text spans are definitely unrelated. + const sourceStart = source.texts[0]; + const targetStart = target.texts[0]; + const sourceEnd = source.texts[source.texts.length - 1]; + const targetEnd = target.texts[target.texts.length - 1]; + const startLen = Math.min(sourceStart.length, targetStart.length); + const endLen = Math.min(sourceEnd.length, targetEnd.length); + return sourceStart.slice(0, startLen) !== targetStart.slice(0, startLen) || + sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen); + } - function isValidBigIntString(s: string): boolean { - const scanner = ts.createScanner(ts.ScriptTarget.ESNext, /*skipTrivia*/ false); - let success = true; - scanner.setOnError(() => success = false); - scanner.setText(s + "n"); - let result = scanner.scan(); - if (result === ts.SyntaxKind.MinusToken) { - result = scanner.scan(); - } - const flags = scanner.getTokenFlags(); - // validate that - // * scanning proceeded without error - // * a bigint can be scanned, and that when it is scanned, it is - // * the full length of the input string (so the scanner is one character beyond the augmented input length) - // * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input) - return success && result === ts.SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & ts.TokenFlags.ContainsSeparator); - } + function isValidBigIntString(s: string): boolean { + const scanner = ts.createScanner(ts.ScriptTarget.ESNext, /*skipTrivia*/ false); + let success = true; + scanner.setOnError(() => success = false); + scanner.setText(s + "n"); + let result = scanner.scan(); + if (result === ts.SyntaxKind.MinusToken) { + result = scanner.scan(); + } + const flags = scanner.getTokenFlags(); + // validate that + // * scanning proceeded without error + // * a bigint can be scanned, and that when it is scanned, it is + // * the full length of the input string (so the scanner is one character beyond the augmented input length) + // * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input) + return success && result === ts.SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & ts.TokenFlags.ContainsSeparator); + } - function isValidTypeForTemplateLiteralPlaceholder(source: ts.Type, target: ts.Type): boolean { - if (source === target || target.flags & (ts.TypeFlags.Any | ts.TypeFlags.String)) { - return true; - } - if (source.flags & ts.TypeFlags.StringLiteral) { - const value = (source as ts.StringLiteralType).value; - return !!(target.flags & ts.TypeFlags.Number && value !== "" && isFinite(+value) || - target.flags & ts.TypeFlags.BigInt && value !== "" && isValidBigIntString(value) || - target.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Nullable) && value === (target as ts.IntrinsicType).intrinsicName); - } - if (source.flags & ts.TypeFlags.TemplateLiteral) { - const texts = (source as ts.TemplateLiteralType).texts; - return texts.length === 2 && texts[0] === "" && texts[1] === "" && isTypeAssignableTo((source as ts.TemplateLiteralType).types[0], target); - } - return isTypeAssignableTo(source, target); + function isValidTypeForTemplateLiteralPlaceholder(source: ts.Type, target: ts.Type): boolean { + if (source === target || target.flags & (ts.TypeFlags.Any | ts.TypeFlags.String)) { + return true; } - - function inferTypesFromTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType): ts.Type[] | undefined { - return source.flags & ts.TypeFlags.StringLiteral ? inferFromLiteralPartsToTemplateLiteral([(source as ts.StringLiteralType).value], ts.emptyArray, target) : - source.flags & ts.TypeFlags.TemplateLiteral ? - ts.arraysEqual((source as ts.TemplateLiteralType).texts, target.texts) ? ts.map((source as ts.TemplateLiteralType).types, getStringLikeTypeForType) : - inferFromLiteralPartsToTemplateLiteral((source as ts.TemplateLiteralType).texts, (source as ts.TemplateLiteralType).types, target) : - undefined; + if (source.flags & ts.TypeFlags.StringLiteral) { + const value = (source as ts.StringLiteralType).value; + return !!(target.flags & ts.TypeFlags.Number && value !== "" && isFinite(+value) || + target.flags & ts.TypeFlags.BigInt && value !== "" && isValidBigIntString(value) || + target.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Nullable) && value === (target as ts.IntrinsicType).intrinsicName); } - - function isTypeMatchedByTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType): boolean { - const inferences = inferTypesFromTemplateLiteralType(source, target); - return !!inferences && ts.every(inferences, (r, i) => isValidTypeForTemplateLiteralPlaceholder(r, target.types[i])); + if (source.flags & ts.TypeFlags.TemplateLiteral) { + const texts = (source as ts.TemplateLiteralType).texts; + return texts.length === 2 && texts[0] === "" && texts[1] === "" && isTypeAssignableTo((source as ts.TemplateLiteralType).types[0], target); } + return isTypeAssignableTo(source, target); + } - function getStringLikeTypeForType(type: ts.Type) { - return type.flags & (ts.TypeFlags.Any | ts.TypeFlags.StringLike) ? type : getTemplateLiteralType(["", ""], [type]); - } + function inferTypesFromTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType): ts.Type[] | undefined { + return source.flags & ts.TypeFlags.StringLiteral ? inferFromLiteralPartsToTemplateLiteral([(source as ts.StringLiteralType).value], ts.emptyArray, target) : + source.flags & ts.TypeFlags.TemplateLiteral ? + ts.arraysEqual((source as ts.TemplateLiteralType).texts, target.texts) ? ts.map((source as ts.TemplateLiteralType).types, getStringLikeTypeForType) : + inferFromLiteralPartsToTemplateLiteral((source as ts.TemplateLiteralType).texts, (source as ts.TemplateLiteralType).types, target) : + undefined; + } - // This function infers from the text parts and type parts of a source literal to a target template literal. The number - // of text parts is always one more than the number of type parts, and a source string literal is treated as a source - // with one text part and zero type parts. The function returns an array of inferred string or template literal types - // corresponding to the placeholders in the target template literal, or undefined if the source doesn't match the target. - // - // We first check that the starting source text part matches the starting target text part, and that the ending source - // text part ends matches the ending target text part. We then iterate through the remaining target text parts, finding - // a match for each in the source and inferring string or template literal types created from the segments of the source - // that occur between the matches. During this iteration, seg holds the index of the current text part in the sourceTexts - // array and pos holds the current character position in the current text part. - // - // Consider inference from type `<<${string}>.<${number}-${number}>>` to type `<${string}.${string}>`, i.e. - // sourceTexts = ['<<', '>.<', '-', '>>'] - // sourceTypes = [string, number, number] - // target.texts = ['<', '.', '>'] - // We first match '<' in the target to the start of '<<' in the source and '>' in the target to the end of '>>' in - // the source. The first match for the '.' in target occurs at character 1 in the source text part at index 1, and thus - // the first inference is the template literal type `<${string}>`. The remainder of the source makes up the second - // inference, the template literal type `<${number}-${number}>`. - function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly ts.Type[], target: ts.TemplateLiteralType): ts.Type[] | undefined { - const lastSourceIndex = sourceTexts.length - 1; - const sourceStartText = sourceTexts[0]; - const sourceEndText = sourceTexts[lastSourceIndex]; - const targetTexts = target.texts; - const lastTargetIndex = targetTexts.length - 1; - const targetStartText = targetTexts[0]; - const targetEndText = targetTexts[lastTargetIndex]; - if (lastSourceIndex === 0 && sourceStartText.length < targetStartText.length + targetEndText.length || - !sourceStartText.startsWith(targetStartText) || !sourceEndText.endsWith(targetEndText)) - return undefined; - const remainingEndText = sourceEndText.slice(0, sourceEndText.length - targetEndText.length); - const matches: ts.Type[] = []; - let seg = 0; - let pos = targetStartText.length; - for (let i = 1; i < lastTargetIndex; i++) { - const delim = targetTexts[i]; - if (delim.length > 0) { - let s = seg; - let p = pos; - while (true) { - p = getSourceText(s).indexOf(delim, p); - if (p >= 0) - break; - s++; - if (s === sourceTexts.length) - return undefined; - p = 0; - } - addMatch(s, p); - pos += delim.length; - } - else if (pos < getSourceText(seg).length) { - addMatch(seg, pos + 1); - } - else if (seg < lastSourceIndex) { - addMatch(seg + 1, 0); - } - else { - return undefined; + function isTypeMatchedByTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType): boolean { + const inferences = inferTypesFromTemplateLiteralType(source, target); + return !!inferences && ts.every(inferences, (r, i) => isValidTypeForTemplateLiteralPlaceholder(r, target.types[i])); + } + + function getStringLikeTypeForType(type: ts.Type) { + return type.flags & (ts.TypeFlags.Any | ts.TypeFlags.StringLike) ? type : getTemplateLiteralType(["", ""], [type]); + } + + // This function infers from the text parts and type parts of a source literal to a target template literal. The number + // of text parts is always one more than the number of type parts, and a source string literal is treated as a source + // with one text part and zero type parts. The function returns an array of inferred string or template literal types + // corresponding to the placeholders in the target template literal, or undefined if the source doesn't match the target. + // + // We first check that the starting source text part matches the starting target text part, and that the ending source + // text part ends matches the ending target text part. We then iterate through the remaining target text parts, finding + // a match for each in the source and inferring string or template literal types created from the segments of the source + // that occur between the matches. During this iteration, seg holds the index of the current text part in the sourceTexts + // array and pos holds the current character position in the current text part. + // + // Consider inference from type `<<${string}>.<${number}-${number}>>` to type `<${string}.${string}>`, i.e. + // sourceTexts = ['<<', '>.<', '-', '>>'] + // sourceTypes = [string, number, number] + // target.texts = ['<', '.', '>'] + // We first match '<' in the target to the start of '<<' in the source and '>' in the target to the end of '>>' in + // the source. The first match for the '.' in target occurs at character 1 in the source text part at index 1, and thus + // the first inference is the template literal type `<${string}>`. The remainder of the source makes up the second + // inference, the template literal type `<${number}-${number}>`. + function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly ts.Type[], target: ts.TemplateLiteralType): ts.Type[] | undefined { + const lastSourceIndex = sourceTexts.length - 1; + const sourceStartText = sourceTexts[0]; + const sourceEndText = sourceTexts[lastSourceIndex]; + const targetTexts = target.texts; + const lastTargetIndex = targetTexts.length - 1; + const targetStartText = targetTexts[0]; + const targetEndText = targetTexts[lastTargetIndex]; + if (lastSourceIndex === 0 && sourceStartText.length < targetStartText.length + targetEndText.length || + !sourceStartText.startsWith(targetStartText) || !sourceEndText.endsWith(targetEndText)) + return undefined; + const remainingEndText = sourceEndText.slice(0, sourceEndText.length - targetEndText.length); + const matches: ts.Type[] = []; + let seg = 0; + let pos = targetStartText.length; + for (let i = 1; i < lastTargetIndex; i++) { + const delim = targetTexts[i]; + if (delim.length > 0) { + let s = seg; + let p = pos; + while (true) { + p = getSourceText(s).indexOf(delim, p); + if (p >= 0) + break; + s++; + if (s === sourceTexts.length) + return undefined; + p = 0; } + addMatch(s, p); + pos += delim.length; + } + else if (pos < getSourceText(seg).length) { + addMatch(seg, pos + 1); } - addMatch(lastSourceIndex, getSourceText(lastSourceIndex).length); - return matches; - function getSourceText(index: number) { - return index < lastSourceIndex ? sourceTexts[index] : remainingEndText; + else if (seg < lastSourceIndex) { + addMatch(seg + 1, 0); } - function addMatch(s: number, p: number) { - const matchType = s === seg ? - getStringLiteralType(getSourceText(s).slice(pos, p)) : - getTemplateLiteralType([sourceTexts[seg].slice(pos), ...sourceTexts.slice(seg + 1, s), getSourceText(s).slice(0, p)], sourceTypes.slice(seg, s)); - matches.push(matchType); - seg = s; - pos = p; + else { + return undefined; } } + addMatch(lastSourceIndex, getSourceText(lastSourceIndex).length); + return matches; + function getSourceText(index: number) { + return index < lastSourceIndex ? sourceTexts[index] : remainingEndText; + } + function addMatch(s: number, p: number) { + const matchType = s === seg ? + getStringLiteralType(getSourceText(s).slice(pos, p)) : + getTemplateLiteralType([sourceTexts[seg].slice(pos), ...sourceTexts.slice(seg + 1, s), getSourceText(s).slice(0, p)], sourceTypes.slice(seg, s)); + matches.push(matchType); + seg = s; + pos = p; + } + } - function inferTypes(inferences: ts.InferenceInfo[], originalSource: ts.Type, originalTarget: ts.Type, priority: ts.InferencePriority = 0, contravariant = false) { - let bivariant = false; - let propagationType: ts.Type; - let inferencePriority = ts.InferencePriority.MaxValue; - let allowComplexConstraintInference = true; - let visited: ts.ESMap; - let sourceStack: object[]; - let targetStack: object[]; - let expandingFlags = ExpandingFlags.None; - inferFromTypes(originalSource, originalTarget); - - function inferFromTypes(source: ts.Type, target: ts.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; + function inferTypes(inferences: ts.InferenceInfo[], originalSource: ts.Type, originalTarget: ts.Type, priority: ts.InferencePriority = 0, contravariant = false) { + let bivariant = false; + let propagationType: ts.Type; + let inferencePriority = ts.InferencePriority.MaxValue; + let allowComplexConstraintInference = true; + let visited: ts.ESMap; + let sourceStack: object[]; + let targetStack: object[]; + let expandingFlags = ExpandingFlags.None; + inferFromTypes(originalSource, originalTarget); + + function inferFromTypes(source: ts.Type, target: ts.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 & ts.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 as ts.UnionOrIntersectionType).types) { + inferFromTypes(t, t); } - 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 (target.flags & ts.TypeFlags.Union) { + // First, infer between identically matching source and target constituents and remove the + // matching types. + const [tempSources, tempTargets] = inferFromMatchingTypes(source.flags & ts.TypeFlags.Union ? (source as ts.UnionType).types : [source], (target as ts.UnionType).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; } - if (source === target && source.flags & ts.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 as ts.UnionOrIntersectionType).types) { - inferFromTypes(t, t); - } + 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, ts.InferencePriority.NakedTypeVariable); return; } - if (target.flags & ts.TypeFlags.Union) { - // First, infer between identically matching source and target constituents and remove the - // matching types. - const [tempSources, tempTargets] = inferFromMatchingTypes(source.flags & ts.TypeFlags.Union ? (source as ts.UnionType).types : [source], (target as ts.UnionType).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, ts.InferencePriority.NakedTypeVariable); + source = getUnionType(sources); + } + else if (target.flags & ts.TypeFlags.Intersection && ts.some((target as ts.IntersectionType).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 & ts.TypeFlags.Union)) { + // Infer between identically matching source and target constituents and remove the matching types. + const [sources, targets] = inferFromMatchingTypes(source.flags & ts.TypeFlags.Intersection ? (source as ts.IntersectionType).types : [source], (target as ts.IntersectionType).types, isTypeIdenticalTo); + if (sources.length === 0 || targets.length === 0) { return; } - source = getUnionType(sources); - } - else if (target.flags & ts.TypeFlags.Intersection && ts.some((target as ts.IntersectionType).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 & ts.TypeFlags.Union)) { - // Infer between identically matching source and target constituents and remove the matching types. - const [sources, targets] = inferFromMatchingTypes(source.flags & ts.TypeFlags.Intersection ? (source as ts.IntersectionType).types : [source], (target as ts.IntersectionType).types, isTypeIdenticalTo); - if (sources.length === 0 || targets.length === 0) { - return; - } - source = getIntersectionType(sources); - target = getIntersectionType(targets); - } + source = getIntersectionType(sources); + target = getIntersectionType(targets); } - else if (target.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) { - target = getActualTypeVariable(target); + } + else if (target.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) { + target = getActualTypeVariable(target); + } + if (target.flags & ts.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 (source === nonInferrableAnyType || source === silentNeverType || (priority & ts.InferencePriority.ReturnType && (source === autoType || source === autoArrayType)) || isFromInferenceBlockedSource(source)) { + return; } - if (target.flags & ts.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 (source === nonInferrableAnyType || source === silentNeverType || (priority & ts.InferencePriority.ReturnType && (source === autoType || source === autoArrayType)) || isFromInferenceBlockedSource(source)) { + const inference = getInferenceInfoForType(target); + if (inference) { + if (ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType) { return; } - const inference = getInferenceInfoForType(target); - if (inference) { - if (ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType) { - return; - } - 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 (!ts.contains(inference.contraCandidates, candidate)) { - inference.contraCandidates = ts.append(inference.contraCandidates, candidate); - clearCachedInferences(inferences); - } - } - else if (!ts.contains(inference.candidates, candidate)) { - inference.candidates = ts.append(inference.candidates, candidate); + 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 (!ts.contains(inference.contraCandidates, candidate)) { + inference.contraCandidates = ts.append(inference.contraCandidates, candidate); clearCachedInferences(inferences); } } - if (!(priority & ts.InferencePriority.ReturnType) && target.flags & ts.TypeFlags.TypeParameter && inference.topLevel && !isTypeParameterAtTopLevel(originalTarget, target as ts.TypeParameter)) { - inference.topLevel = false; + else if (!ts.contains(inference.candidates, candidate)) { + inference.candidates = ts.append(inference.candidates, candidate); clearCachedInferences(inferences); } } - inferencePriority = Math.min(inferencePriority, priority); - return; - } - // 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) { - inferFromTypes(source, simplified); - } - else if (target.flags & ts.TypeFlags.IndexedAccess) { - const indexType = getSimplifiedType((target as ts.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 & ts.TypeFlags.Instantiable) { - const simplified = distributeIndexOverObjectType(getSimplifiedType((target as ts.IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false); - if (simplified && simplified !== target) { - inferFromTypes(source, simplified); - } + if (!(priority & ts.InferencePriority.ReturnType) && target.flags & ts.TypeFlags.TypeParameter && inference.topLevel && !isTypeParameterAtTopLevel(originalTarget, target as ts.TypeParameter)) { + inference.topLevel = false; + clearCachedInferences(inferences); } } + inferencePriority = Math.min(inferencePriority, priority); + return; } - if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && ((source as ts.TypeReference).target === (target as ts.TypeReference).target || isArrayType(source) && isArrayType(target)) && - !((source as ts.TypeReference).node && (target as ts.TypeReference).node)) { - // If source and target are references to the same generic type, infer from type arguments - inferFromTypeArguments(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), getVariances((source as ts.TypeReference).target)); - } - else if (source.flags & ts.TypeFlags.Index && target.flags & ts.TypeFlags.Index) { - contravariant = !contravariant; - inferFromTypes((source as ts.IndexType).type, (target as ts.IndexType).type); - contravariant = !contravariant; - } - else if ((isLiteralType(source) || source.flags & ts.TypeFlags.String) && target.flags & ts.TypeFlags.Index) { - const empty = createEmptyObjectTypeFromStringLiteral(source); - contravariant = !contravariant; - inferWithPriority(empty, (target as ts.IndexType).type, ts.InferencePriority.LiteralKeyof); - contravariant = !contravariant; - } - else if (source.flags & ts.TypeFlags.IndexedAccess && target.flags & ts.TypeFlags.IndexedAccess) { - inferFromTypes((source as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType); - inferFromTypes((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType); - } - else if (source.flags & ts.TypeFlags.StringMapping && target.flags & ts.TypeFlags.StringMapping) { - if ((source as ts.StringMappingType).symbol === (target as ts.StringMappingType).symbol) { - inferFromTypes((source as ts.StringMappingType).type, (target as ts.StringMappingType).type); - } - } - else if (source.flags & ts.TypeFlags.Substitution) { - inferFromTypes((source as ts.SubstitutionType).baseType, target); - const oldPriority = priority; - priority |= ts.InferencePriority.SubstituteSource; - inferFromTypes((source as ts.SubstitutionType).substitute, target); // Make substitute inference at a lower priority - priority = oldPriority; - } - else if (target.flags & ts.TypeFlags.Conditional) { - invokeOnce(source, target, inferToConditionalType); - } - else if (target.flags & ts.TypeFlags.UnionOrIntersection) { - inferToMultipleTypes(source, (target as ts.UnionOrIntersectionType).types, target.flags); - } - else if (source.flags & ts.TypeFlags.Union) { - // Source is a union or intersection type, infer from each constituent type - const sourceTypes = (source as ts.UnionOrIntersectionType).types; - for (const sourceType of sourceTypes) { - inferFromTypes(sourceType, target); - } - } - else if (target.flags & ts.TypeFlags.TemplateLiteral) { - inferToTemplateLiteralType(source, target as ts.TemplateLiteralType); + // 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) { + inferFromTypes(source, simplified); } - else { - source = getReducedType(source); - if (!(priority & ts.InferencePriority.NoConstraints && source.flags & (ts.TypeFlags.Intersection | ts.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 & (ts.TypeFlags.Object | ts.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); + else if (target.flags & ts.TypeFlags.IndexedAccess) { + const indexType = getSimplifiedType((target as ts.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 & ts.TypeFlags.Instantiable) { + const simplified = distributeIndexOverObjectType(getSimplifiedType((target as ts.IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false); + if (simplified && simplified !== target) { + inferFromTypes(source, simplified); } - source = apparentSource; - } - if (source.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) { - invokeOnce(source, target, inferFromObjectTypes); } } } - - function inferWithPriority(source: ts.Type, target: ts.Type, newPriority: ts.InferencePriority) { - const savePriority = priority; - priority |= newPriority; - inferFromTypes(source, target); - priority = savePriority; + if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && ((source as ts.TypeReference).target === (target as ts.TypeReference).target || isArrayType(source) && isArrayType(target)) && + !((source as ts.TypeReference).node && (target as ts.TypeReference).node)) { + // If source and target are references to the same generic type, infer from type arguments + inferFromTypeArguments(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), getVariances((source as ts.TypeReference).target)); } - - 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 = new ts.Map())).set(key, ts.InferencePriority.Circularity); - const saveInferencePriority = inferencePriority; - inferencePriority = ts.InferencePriority.MaxValue; - // We stop inferring and report a circularity if we encounter duplicate recursion identities on both - // the source side and the target side. - const saveExpandingFlags = expandingFlags; - const sourceIdentity = getRecursionIdentity(source); - const targetIdentity = getRecursionIdentity(target); - if (ts.contains(sourceStack, sourceIdentity)) - expandingFlags |= ExpandingFlags.Source; - if (ts.contains(targetStack, targetIdentity)) - expandingFlags |= ExpandingFlags.Target; - if (expandingFlags !== ExpandingFlags.Both) { - (sourceStack || (sourceStack = [])).push(sourceIdentity); - (targetStack || (targetStack = [])).push(targetIdentity); - action(source, target); - targetStack.pop(); - sourceStack.pop(); - } - else { - inferencePriority = ts.InferencePriority.Circularity; + else if (source.flags & ts.TypeFlags.Index && target.flags & ts.TypeFlags.Index) { + contravariant = !contravariant; + inferFromTypes((source as ts.IndexType).type, (target as ts.IndexType).type); + contravariant = !contravariant; + } + else if ((isLiteralType(source) || source.flags & ts.TypeFlags.String) && target.flags & ts.TypeFlags.Index) { + const empty = createEmptyObjectTypeFromStringLiteral(source); + contravariant = !contravariant; + inferWithPriority(empty, (target as ts.IndexType).type, ts.InferencePriority.LiteralKeyof); + contravariant = !contravariant; + } + else if (source.flags & ts.TypeFlags.IndexedAccess && target.flags & ts.TypeFlags.IndexedAccess) { + inferFromTypes((source as ts.IndexedAccessType).objectType, (target as ts.IndexedAccessType).objectType); + inferFromTypes((source as ts.IndexedAccessType).indexType, (target as ts.IndexedAccessType).indexType); + } + else if (source.flags & ts.TypeFlags.StringMapping && target.flags & ts.TypeFlags.StringMapping) { + if ((source as ts.StringMappingType).symbol === (target as ts.StringMappingType).symbol) { + inferFromTypes((source as ts.StringMappingType).type, (target as ts.StringMappingType).type); } - expandingFlags = saveExpandingFlags; - visited.set(key, inferencePriority); - inferencePriority = Math.min(inferencePriority, saveInferencePriority); } - - 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 = ts.appendIfUnique(matchedSources, s); - matchedTargets = ts.appendIfUnique(matchedTargets, t); - } - } + else if (source.flags & ts.TypeFlags.Substitution) { + inferFromTypes((source as ts.SubstitutionType).baseType, target); + const oldPriority = priority; + priority |= ts.InferencePriority.SubstituteSource; + inferFromTypes((source as ts.SubstitutionType).substitute, target); // Make substitute inference at a lower priority + priority = oldPriority; + } + else if (target.flags & ts.TypeFlags.Conditional) { + invokeOnce(source, target, inferToConditionalType); + } + else if (target.flags & ts.TypeFlags.UnionOrIntersection) { + inferToMultipleTypes(source, (target as ts.UnionOrIntersectionType).types, target.flags); + } + else if (source.flags & ts.TypeFlags.Union) { + // Source is a union or intersection type, infer from each constituent type + const sourceTypes = (source as ts.UnionOrIntersectionType).types; + for (const sourceType of sourceTypes) { + inferFromTypes(sourceType, target); } - return [ - matchedSources ? ts.filter(sources, t => !ts.contains(matchedSources, t)) : sources, - matchedTargets ? ts.filter(targets, t => !ts.contains(matchedTargets, t)) : targets, - ]; } + else if (target.flags & ts.TypeFlags.TemplateLiteral) { + inferToTemplateLiteralType(source, target as ts.TemplateLiteralType); + } + else { + source = getReducedType(source); + if (!(priority & ts.InferencePriority.NoConstraints && source.flags & (ts.TypeFlags.Intersection | ts.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 & (ts.TypeFlags.Object | ts.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 & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) { + invokeOnce(source, target, inferFromObjectTypes); + } + } + } + + function inferWithPriority(source: ts.Type, target: ts.Type, newPriority: ts.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 = new ts.Map())).set(key, ts.InferencePriority.Circularity); + const saveInferencePriority = inferencePriority; + inferencePriority = ts.InferencePriority.MaxValue; + // We stop inferring and report a circularity if we encounter duplicate recursion identities on both + // the source side and the target side. + const saveExpandingFlags = expandingFlags; + const sourceIdentity = getRecursionIdentity(source); + const targetIdentity = getRecursionIdentity(target); + if (ts.contains(sourceStack, sourceIdentity)) + expandingFlags |= ExpandingFlags.Source; + if (ts.contains(targetStack, targetIdentity)) + expandingFlags |= ExpandingFlags.Target; + if (expandingFlags !== ExpandingFlags.Both) { + (sourceStack || (sourceStack = [])).push(sourceIdentity); + (targetStack || (targetStack = [])).push(targetIdentity); + action(source, target); + targetStack.pop(); + sourceStack.pop(); + } + else { + inferencePriority = ts.InferencePriority.Circularity; + } + expandingFlags = saveExpandingFlags; + visited.set(key, inferencePriority); + inferencePriority = Math.min(inferencePriority, saveInferencePriority); + } - function inferFromTypeArguments(sourceTypes: readonly ts.Type[], targetTypes: readonly ts.Type[], variances: readonly ts.VarianceFlags[]) { - const count = sourceTypes.length < targetTypes.length ? sourceTypes.length : targetTypes.length; - for (let i = 0; i < count; i++) { - if (i < variances.length && (variances[i] & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Contravariant) { - inferFromContravariantTypes(sourceTypes[i], targetTypes[i]); - } - else { - inferFromTypes(sourceTypes[i], targetTypes[i]); + 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 = ts.appendIfUnique(matchedSources, s); + matchedTargets = ts.appendIfUnique(matchedTargets, t); } } } + return [ + matchedSources ? ts.filter(sources, t => !ts.contains(matchedSources, t)) : sources, + matchedTargets ? ts.filter(targets, t => !ts.contains(matchedTargets, t)) : targets, + ]; + } - function inferFromContravariantTypes(source: ts.Type, target: ts.Type) { - if (strictFunctionTypes || priority & ts.InferencePriority.AlwaysStrict) { - contravariant = !contravariant; - inferFromTypes(source, target); - contravariant = !contravariant; + function inferFromTypeArguments(sourceTypes: readonly ts.Type[], targetTypes: readonly ts.Type[], variances: readonly ts.VarianceFlags[]) { + const count = sourceTypes.length < targetTypes.length ? sourceTypes.length : targetTypes.length; + for (let i = 0; i < count; i++) { + if (i < variances.length && (variances[i] & ts.VarianceFlags.VarianceMask) === ts.VarianceFlags.Contravariant) { + inferFromContravariantTypes(sourceTypes[i], targetTypes[i]); } else { - inferFromTypes(source, target); + inferFromTypes(sourceTypes[i], targetTypes[i]); } } + } - function getInferenceInfoForType(type: ts.Type) { - if (type.flags & ts.TypeFlags.TypeVariable) { - for (const inference of inferences) { - if (type === inference.typeParameter) { - return inference; - } + function inferFromContravariantTypes(source: ts.Type, target: ts.Type) { + if (strictFunctionTypes || priority & ts.InferencePriority.AlwaysStrict) { + contravariant = !contravariant; + inferFromTypes(source, target); + contravariant = !contravariant; + } + else { + inferFromTypes(source, target); + } + } + + function getInferenceInfoForType(type: ts.Type) { + if (type.flags & ts.TypeFlags.TypeVariable) { + for (const inference of inferences) { + if (type === inference.typeParameter) { + return inference; } } - return undefined; } + return undefined; + } - function getSingleTypeVariableFromIntersectionTypes(types: ts.Type[]) { - let typeVariable: ts.Type | undefined; - for (const type of types) { - const t = type.flags & ts.TypeFlags.Intersection && ts.find((type as ts.IntersectionType).types, t => !!getInferenceInfoForType(t)); - if (!t || typeVariable && t !== typeVariable) { - return undefined; - } - typeVariable = t; + function getSingleTypeVariableFromIntersectionTypes(types: ts.Type[]) { + let typeVariable: ts.Type | undefined; + for (const type of types) { + const t = type.flags & ts.TypeFlags.Intersection && ts.find((type as ts.IntersectionType).types, t => !!getInferenceInfoForType(t)); + if (!t || typeVariable && t !== typeVariable) { + return undefined; } - return typeVariable; + typeVariable = t; } + return typeVariable; + } - function inferToMultipleTypes(source: ts.Type, targets: ts.Type[], targetFlags: ts.TypeFlags) { - let typeVariableCount = 0; - if (targetFlags & ts.TypeFlags.Union) { - let nakedTypeVariable: ts.Type | undefined; - const sources = source.flags & ts.TypeFlags.Union ? (source as ts.UnionType).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 = ts.InferencePriority.MaxValue; - inferFromTypes(sources[i], t); - if (inferencePriority === priority) - matched[i] = true; - inferenceCircularity = inferenceCircularity || inferencePriority === ts.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, ts.InferencePriority.NakedTypeVariable); - } - return; + function inferToMultipleTypes(source: ts.Type, targets: ts.Type[], targetFlags: ts.TypeFlags) { + let typeVariableCount = 0; + if (targetFlags & ts.TypeFlags.Union) { + let nakedTypeVariable: ts.Type | undefined; + const sources = source.flags & ts.TypeFlags.Union ? (source as ts.UnionType).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++; } - // 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 = ts.flatMap(sources, (s, i) => matched[i] ? undefined : s); - if (unmatched.length) { - inferFromTypes(getUnionType(unmatched), nakedTypeVariable!); - return; + else { + for (let i = 0; i < sources.length; i++) { + const saveInferencePriority = inferencePriority; + inferencePriority = ts.InferencePriority.MaxValue; + inferFromTypes(sources[i], t); + if (inferencePriority === priority) + matched[i] = true; + inferenceCircularity = inferenceCircularity || inferencePriority === ts.InferencePriority.Circularity; + inferencePriority = Math.min(inferencePriority, saveInferencePriority); } } } - 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 (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, ts.InferencePriority.NakedTypeVariable); } + return; } - // 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 & ts.TypeFlags.Intersection ? typeVariableCount === 1 : typeVariableCount > 0) { - for (const t of targets) { - if (getInferenceInfoForType(t)) { - inferWithPriority(source, t, ts.InferencePriority.NakedTypeVariable); - } + // 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 = ts.flatMap(sources, (s, i) => matched[i] ? undefined : s); + if (unmatched.length) { + inferFromTypes(getUnionType(unmatched), nakedTypeVariable!); + return; } } } - - function inferToMappedType(source: ts.Type, target: ts.MappedType, constraintType: ts.Type): boolean { - if (constraintType.flags & ts.TypeFlags.Union) { - let result = false; - for (const type of (constraintType as ts.UnionType).types) { - result = inferToMappedType(source, target, type) || result; + 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++; } - return result; - } - if (constraintType.flags & ts.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 as ts.IndexType).type); - if (inference && !inference.isFixed && !isFromInferenceBlockedSource(source)) { - const inferredType = inferTypeForHomomorphicMappedType(source, target, constraintType as ts.IndexType); - 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, ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType ? - ts.InferencePriority.PartialHomomorphicMappedType : - ts.InferencePriority.HomomorphicMappedType); - } + else { + inferFromTypes(source, t); } - return true; } - if (constraintType.flags & ts.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, ts.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; + } + // 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 & ts.TypeFlags.Intersection ? typeVariableCount === 1 : typeVariableCount > 0) { + for (const t of targets) { + if (getInferenceInfoForType(t)) { + inferWithPriority(source, t, ts.InferencePriority.NakedTypeVariable); } - // 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 = ts.map(getPropertiesOfType(source), getTypeOfSymbol); - const indexTypes = ts.map(getIndexInfosOfType(source), info => info !== enumNumberIndexInfo ? info.type : neverType); - inferFromTypes(getUnionType(ts.concatenate(propTypes, indexTypes)), getTemplateTypeFromMappedType(target)); - return true; } - return false; } + } - function inferToConditionalType(source: ts.Type, target: ts.ConditionalType) { - if (source.flags & ts.TypeFlags.Conditional) { - inferFromTypes((source as ts.ConditionalType).checkType, target.checkType); - inferFromTypes((source as ts.ConditionalType).extendsType, target.extendsType); - inferFromTypes(getTrueTypeFromConditionalType(source as ts.ConditionalType), getTrueTypeFromConditionalType(target)); - inferFromTypes(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target)); + function inferToMappedType(source: ts.Type, target: ts.MappedType, constraintType: ts.Type): boolean { + if (constraintType.flags & ts.TypeFlags.Union) { + let result = false; + for (const type of (constraintType as ts.UnionType).types) { + result = inferToMappedType(source, target, type) || result; } - else { - const savePriority = priority; - priority |= contravariant ? ts.InferencePriority.ContravariantConditional : 0; - const targetTypes = [getTrueTypeFromConditionalType(target), getFalseTypeFromConditionalType(target)]; - inferToMultipleTypes(source, targetTypes, target.flags); - priority = savePriority; + return result; + } + if (constraintType.flags & ts.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 as ts.IndexType).type); + if (inference && !inference.isFixed && !isFromInferenceBlockedSource(source)) { + const inferredType = inferTypeForHomomorphicMappedType(source, target, constraintType as ts.IndexType); + 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, ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType ? + ts.InferencePriority.PartialHomomorphicMappedType : + ts.InferencePriority.HomomorphicMappedType); + } + } + return true; + } + if (constraintType.flags & ts.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, ts.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 = ts.map(getPropertiesOfType(source), getTypeOfSymbol); + const indexTypes = ts.map(getIndexInfosOfType(source), info => info !== enumNumberIndexInfo ? info.type : neverType); + inferFromTypes(getUnionType(ts.concatenate(propTypes, indexTypes)), getTemplateTypeFromMappedType(target)); + return true; } + return false; + } - function inferToTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType) { - const matches = inferTypesFromTemplateLiteralType(source, target); - const types = target.types; - // When the target template literal contains only placeholders (meaning that inference is intended to extract - // single characters and remainder strings) and inference fails to produce matches, we want to infer 'never' for - // each placeholder such that instantiation with the inferred value(s) produces 'never', a type for which an - // assignment check will fail. If we make no inferences, we'll likely end up with the constraint 'string' which, - // upon instantiation, would collapse all the placeholders to just 'string', and an assignment check might - // succeed. That would be a pointless and confusing outcome. - if (matches || ts.every(target.texts, s => s.length === 0)) { - for (let i = 0; i < types.length; i++) { - inferFromTypes(matches ? matches[i] : neverType, types[i]); - } + function inferToConditionalType(source: ts.Type, target: ts.ConditionalType) { + if (source.flags & ts.TypeFlags.Conditional) { + inferFromTypes((source as ts.ConditionalType).checkType, target.checkType); + inferFromTypes((source as ts.ConditionalType).extendsType, target.extendsType); + inferFromTypes(getTrueTypeFromConditionalType(source as ts.ConditionalType), getTrueTypeFromConditionalType(target)); + inferFromTypes(getFalseTypeFromConditionalType(source as ts.ConditionalType), getFalseTypeFromConditionalType(target)); + } + else { + const savePriority = priority; + priority |= contravariant ? ts.InferencePriority.ContravariantConditional : 0; + const targetTypes = [getTrueTypeFromConditionalType(target), getFalseTypeFromConditionalType(target)]; + inferToMultipleTypes(source, targetTypes, target.flags); + priority = savePriority; + } + } + + function inferToTemplateLiteralType(source: ts.Type, target: ts.TemplateLiteralType) { + const matches = inferTypesFromTemplateLiteralType(source, target); + const types = target.types; + // When the target template literal contains only placeholders (meaning that inference is intended to extract + // single characters and remainder strings) and inference fails to produce matches, we want to infer 'never' for + // each placeholder such that instantiation with the inferred value(s) produces 'never', a type for which an + // assignment check will fail. If we make no inferences, we'll likely end up with the constraint 'string' which, + // upon instantiation, would collapse all the placeholders to just 'string', and an assignment check might + // succeed. That would be a pointless and confusing outcome. + if (matches || ts.every(target.texts, s => s.length === 0)) { + for (let i = 0; i < types.length; i++) { + inferFromTypes(matches ? matches[i] : neverType, types[i]); } } + } - function inferFromObjectTypes(source: ts.Type, target: ts.Type) { - if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && ((source as ts.TypeReference).target === (target as ts.TypeReference).target || isArrayType(source) && isArrayType(target))) { - // If source and target are references to the same generic type, infer from type arguments - inferFromTypeArguments(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), getVariances((source as ts.TypeReference).target)); + function inferFromObjectTypes(source: ts.Type, target: ts.Type) { + if (ts.getObjectFlags(source) & ts.ObjectFlags.Reference && ts.getObjectFlags(target) & ts.ObjectFlags.Reference && ((source as ts.TypeReference).target === (target as ts.TypeReference).target || isArrayType(source) && isArrayType(target))) { + // If source and target are references to the same generic type, infer from type arguments + inferFromTypeArguments(getTypeArguments(source as ts.TypeReference), getTypeArguments(target as ts.TypeReference), getVariances((source as ts.TypeReference).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)); + const sourceNameType = getNameTypeFromMappedType(source); + const targetNameType = getNameTypeFromMappedType(target); + if (sourceNameType && targetNameType) + inferFromTypes(sourceNameType, targetNameType); + } + if (ts.getObjectFlags(target) & ts.ObjectFlags.Mapped && !(target as ts.MappedType).declaration.nameType) { + const constraintType = getConstraintTypeFromMappedType(target as ts.MappedType); + if (inferToMappedType(source, target as ts.MappedType, constraintType)) { 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)); - const sourceNameType = getNameTypeFromMappedType(source); - const targetNameType = getNameTypeFromMappedType(target); - if (sourceNameType && targetNameType) - inferFromTypes(sourceNameType, targetNameType); - } - if (ts.getObjectFlags(target) & ts.ObjectFlags.Mapped && !(target as ts.MappedType).declaration.nameType) { - const constraintType = getConstraintTypeFromMappedType(target as ts.MappedType); - if (inferToMappedType(source, target as ts.MappedType, constraintType)) { - return; - } - } - // Infer from the members of source and target only if the two types are possibly related - if (!typesDefinitelyUnrelated(source, target)) { - if (isArrayOrTupleType(source)) { - if (isTupleType(target)) { - const sourceArity = getTypeReferenceArity(source); - const targetArity = getTypeReferenceArity(target); - const elementTypes = getTypeArguments(target); - const elementFlags = target.target.elementFlags; - // When source and target are tuple types with the same structure (fixed, variadic, and rest are matched - // to the same kind in each position), simply infer between the element types. - if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) { - for (let i = 0; i < targetArity; i++) { - inferFromTypes(getTypeArguments(source)[i], elementTypes[i]); - } - return; - } - const startLength = isTupleType(source) ? Math.min(source.target.fixedLength, target.target.fixedLength) : 0; - const endLength = Math.min(isTupleType(source) ? getEndElementCount(source.target, ts.ElementFlags.Fixed) : 0, target.target.hasRestElement ? getEndElementCount(target.target, ts.ElementFlags.Fixed) : 0); - // Infer between starting fixed elements. - for (let i = 0; i < startLength; i++) { + } + // Infer from the members of source and target only if the two types are possibly related + if (!typesDefinitelyUnrelated(source, target)) { + if (isArrayOrTupleType(source)) { + if (isTupleType(target)) { + const sourceArity = getTypeReferenceArity(source); + const targetArity = getTypeReferenceArity(target); + const elementTypes = getTypeArguments(target); + const elementFlags = target.target.elementFlags; + // When source and target are tuple types with the same structure (fixed, variadic, and rest are matched + // to the same kind in each position), simply infer between the element types. + if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) { + for (let i = 0; i < targetArity; i++) { inferFromTypes(getTypeArguments(source)[i], elementTypes[i]); } - if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ts.ElementFlags.Rest) { - // Single rest element remains in source, infer from that to every element in target - const restType = getTypeArguments(source)[startLength]; - for (let i = startLength; i < targetArity - endLength; i++) { - inferFromTypes(elementFlags[i] & ts.ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]); - } + return; + } + const startLength = isTupleType(source) ? Math.min(source.target.fixedLength, target.target.fixedLength) : 0; + const endLength = Math.min(isTupleType(source) ? getEndElementCount(source.target, ts.ElementFlags.Fixed) : 0, target.target.hasRestElement ? getEndElementCount(target.target, ts.ElementFlags.Fixed) : 0); + // Infer between starting fixed elements. + for (let i = 0; i < startLength; i++) { + inferFromTypes(getTypeArguments(source)[i], elementTypes[i]); + } + if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ts.ElementFlags.Rest) { + // Single rest element remains in source, infer from that to every element in target + const restType = getTypeArguments(source)[startLength]; + for (let i = startLength; i < targetArity - endLength; i++) { + inferFromTypes(elementFlags[i] & ts.ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]); } - else { - const middleLength = targetArity - startLength - endLength; - if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ts.ElementFlags.Variadic && isTupleType(source)) { - // Middle of target is [...T, ...U] and source is tuple type - const targetInfo = getInferenceInfoForType(elementTypes[startLength]); - if (targetInfo && targetInfo.impliedArity !== undefined) { - // Infer slices from source based on implied arity of T. - inferFromTypes(sliceTupleType(source, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]); - inferFromTypes(sliceTupleType(source, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]); - } - } - else if (middleLength === 1 && elementFlags[startLength] & ts.ElementFlags.Variadic) { - // Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source. - // If target ends in optional element(s), make a lower priority a speculative inference. - const endsInOptional = target.target.elementFlags[targetArity - 1] & ts.ElementFlags.Optional; - const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]); - inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? ts.InferencePriority.SpeculativeTuple : 0); - } - else if (middleLength === 1 && elementFlags[startLength] & ts.ElementFlags.Rest) { - // Middle of target is exactly one rest element. If middle of source is not empty, infer union of middle element types. - const restType = isTupleType(source) ? getElementTypeOfSliceOfTupleType(source, startLength, endLength) : getTypeArguments(source)[0]; - if (restType) { - inferFromTypes(restType, elementTypes[startLength]); - } + } + else { + const middleLength = targetArity - startLength - endLength; + if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ts.ElementFlags.Variadic && isTupleType(source)) { + // Middle of target is [...T, ...U] and source is tuple type + const targetInfo = getInferenceInfoForType(elementTypes[startLength]); + if (targetInfo && targetInfo.impliedArity !== undefined) { + // Infer slices from source based on implied arity of T. + inferFromTypes(sliceTupleType(source, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]); + inferFromTypes(sliceTupleType(source, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]); } } - // Infer between ending fixed elements - for (let i = 0; i < endLength; i++) { - inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]); + else if (middleLength === 1 && elementFlags[startLength] & ts.ElementFlags.Variadic) { + // Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source. + // If target ends in optional element(s), make a lower priority a speculative inference. + const endsInOptional = target.target.elementFlags[targetArity - 1] & ts.ElementFlags.Optional; + const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]); + inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? ts.InferencePriority.SpeculativeTuple : 0); + } + else if (middleLength === 1 && elementFlags[startLength] & ts.ElementFlags.Rest) { + // Middle of target is exactly one rest element. If middle of source is not empty, infer union of middle element types. + const restType = isTupleType(source) ? getElementTypeOfSliceOfTupleType(source, startLength, endLength) : getTypeArguments(source)[0]; + if (restType) { + inferFromTypes(restType, elementTypes[startLength]); + } } - return; } - if (isArrayType(target)) { - inferFromIndexTypes(source, target); - return; + // Infer between ending fixed elements + for (let i = 0; i < endLength; i++) { + inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]); } + return; + } + if (isArrayType(target)) { + inferFromIndexTypes(source, target); + return; } - inferFromProperties(source, target); - inferFromSignatures(source, target, ts.SignatureKind.Call); - inferFromSignatures(source, target, ts.SignatureKind.Construct); - inferFromIndexTypes(source, target); } + inferFromProperties(source, target); + inferFromSignatures(source, target, ts.SignatureKind.Call); + inferFromSignatures(source, target, ts.SignatureKind.Construct); + inferFromIndexTypes(source, target); } + } - function inferFromProperties(source: ts.Type, target: ts.Type) { - const properties = getPropertiesOfObjectType(target); - for (const targetProp of properties) { - const sourceProp = getPropertyOfType(source, targetProp.escapedName); - if (sourceProp && !ts.some(sourceProp.declarations, hasSkipDirectInferenceFlag)) { - inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); - } + function inferFromProperties(source: ts.Type, target: ts.Type) { + const properties = getPropertiesOfObjectType(target); + for (const targetProp of properties) { + const sourceProp = getPropertyOfType(source, targetProp.escapedName); + if (sourceProp && !ts.some(sourceProp.declarations, hasSkipDirectInferenceFlag)) { + inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); } } + } - function inferFromSignatures(source: ts.Type, target: ts.Type, kind: ts.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 = !!(ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType); - for (let i = 0; i < len; i++) { - inferFromSignature(getBaseSignature(sourceSignatures[sourceLen - len + i]), getErasedSignature(targetSignatures[targetLen - len + i]), skipParameters); - } + function inferFromSignatures(source: ts.Type, target: ts.Type, kind: ts.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 = !!(ts.getObjectFlags(source) & ts.ObjectFlags.NonInferrableType); + for (let i = 0; i < len; i++) { + inferFromSignature(getBaseSignature(sourceSignatures[sourceLen - len + i]), getErasedSignature(targetSignatures[targetLen - len + i]), skipParameters); } + } - function inferFromSignature(source: ts.Signature, target: ts.Signature, skipParameters: boolean) { - if (!skipParameters) { - const saveBivariant = bivariant; - const kind = target.declaration ? target.declaration.kind : ts.SyntaxKind.Unknown; - // Once we descend into a bivariant signature we remain bivariant for all nested inferences - bivariant = bivariant || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.MethodSignature || kind === ts.SyntaxKind.Constructor; - applyToParameterTypes(source, target, inferFromContravariantTypes); - bivariant = saveBivariant; - } - applyToReturnTypes(source, target, inferFromTypes); + function inferFromSignature(source: ts.Signature, target: ts.Signature, skipParameters: boolean) { + if (!skipParameters) { + const saveBivariant = bivariant; + const kind = target.declaration ? target.declaration.kind : ts.SyntaxKind.Unknown; + // Once we descend into a bivariant signature we remain bivariant for all nested inferences + bivariant = bivariant || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.MethodSignature || kind === ts.SyntaxKind.Constructor; + applyToParameterTypes(source, target, inferFromContravariantTypes); + bivariant = saveBivariant; } + applyToReturnTypes(source, target, inferFromTypes); + } - function inferFromIndexTypes(source: ts.Type, target: ts.Type) { - // Inferences across mapped type index signatures are pretty much the same a inferences to homomorphic variables - const priority = (ts.getObjectFlags(source) & ts.getObjectFlags(target) & ts.ObjectFlags.Mapped) ? ts.InferencePriority.HomomorphicMappedType : 0; - const indexInfos = getIndexInfosOfType(target); - if (isObjectTypeWithInferableIndex(source)) { - for (const targetInfo of indexInfos) { - const propTypes: ts.Type[] = []; - for (const prop of getPropertiesOfType(source)) { - if (isApplicableIndexType(getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique), targetInfo.keyType)) { - const propType = getTypeOfSymbol(prop); - propTypes.push(prop.flags & ts.SymbolFlags.Optional ? removeMissingOrUndefinedType(propType) : propType); - } - } - for (const info of getIndexInfosOfType(source)) { - if (isApplicableIndexType(info.keyType, targetInfo.keyType)) { - propTypes.push(info.type); - } + function inferFromIndexTypes(source: ts.Type, target: ts.Type) { + // Inferences across mapped type index signatures are pretty much the same a inferences to homomorphic variables + const priority = (ts.getObjectFlags(source) & ts.getObjectFlags(target) & ts.ObjectFlags.Mapped) ? ts.InferencePriority.HomomorphicMappedType : 0; + const indexInfos = getIndexInfosOfType(target); + if (isObjectTypeWithInferableIndex(source)) { + for (const targetInfo of indexInfos) { + const propTypes: ts.Type[] = []; + for (const prop of getPropertiesOfType(source)) { + if (isApplicableIndexType(getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique), targetInfo.keyType)) { + const propType = getTypeOfSymbol(prop); + propTypes.push(prop.flags & ts.SymbolFlags.Optional ? removeMissingOrUndefinedType(propType) : propType); } - if (propTypes.length) { - inferWithPriority(getUnionType(propTypes), targetInfo.type, priority); + } + for (const info of getIndexInfosOfType(source)) { + if (isApplicableIndexType(info.keyType, targetInfo.keyType)) { + propTypes.push(info.type); } } - } - for (const targetInfo of indexInfos) { - const sourceInfo = getApplicableIndexInfo(source, targetInfo.keyType); - if (sourceInfo) { - inferWithPriority(sourceInfo.type, targetInfo.type, priority); + if (propTypes.length) { + inferWithPriority(getUnionType(propTypes), targetInfo.type, priority); } } } + for (const targetInfo of indexInfos) { + const sourceInfo = getApplicableIndexInfo(source, targetInfo.keyType); + if (sourceInfo) { + inferWithPriority(sourceInfo.type, targetInfo.type, priority); + } + } } + } - function isTypeOrBaseIdenticalTo(s: ts.Type, t: ts.Type) { - return exactOptionalPropertyTypes && t === missingType ? s === t : - (isTypeIdenticalTo(s, t) || !!(t.flags & ts.TypeFlags.String && s.flags & ts.TypeFlags.StringLiteral || t.flags & ts.TypeFlags.Number && s.flags & ts.TypeFlags.NumberLiteral)); - } + function isTypeOrBaseIdenticalTo(s: ts.Type, t: ts.Type) { + return exactOptionalPropertyTypes && t === missingType ? s === t : + (isTypeIdenticalTo(s, t) || !!(t.flags & ts.TypeFlags.String && s.flags & ts.TypeFlags.StringLiteral || t.flags & ts.TypeFlags.Number && s.flags & ts.TypeFlags.NumberLiteral)); + } - function isTypeCloselyMatchedBy(s: ts.Type, t: ts.Type) { - return !!(s.flags & ts.TypeFlags.Object && t.flags & ts.TypeFlags.Object && s.symbol && s.symbol === t.symbol || - s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol); - } + function isTypeCloselyMatchedBy(s: ts.Type, t: ts.Type) { + return !!(s.flags & ts.TypeFlags.Object && t.flags & ts.TypeFlags.Object && s.symbol && s.symbol === t.symbol || + s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol); + } - function hasPrimitiveConstraint(type: ts.TypeParameter): boolean { - const constraint = getConstraintOfTypeParameter(type); - return !!constraint && maybeTypeOfKind(constraint.flags & ts.TypeFlags.Conditional ? getDefaultConstraintOfConditionalType(constraint as ts.ConditionalType) : constraint, ts.TypeFlags.Primitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping); - } + function hasPrimitiveConstraint(type: ts.TypeParameter): boolean { + const constraint = getConstraintOfTypeParameter(type); + return !!constraint && maybeTypeOfKind(constraint.flags & ts.TypeFlags.Conditional ? getDefaultConstraintOfConditionalType(constraint as ts.ConditionalType) : constraint, ts.TypeFlags.Primitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping); + } - function isObjectLiteralType(type: ts.Type) { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.ObjectLiteral); - } + function isObjectLiteralType(type: ts.Type) { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.ObjectLiteral); + } - function isObjectOrArrayLiteralType(type: ts.Type) { - return !!(ts.getObjectFlags(type) & (ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ArrayLiteral)); - } + function isObjectOrArrayLiteralType(type: ts.Type) { + return !!(ts.getObjectFlags(type) & (ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ArrayLiteral)); + } - function unionObjectAndArrayLiteralCandidates(candidates: ts.Type[]): ts.Type[] { - if (candidates.length > 1) { - const objectLiterals = ts.filter(candidates, isObjectOrArrayLiteralType); - if (objectLiterals.length) { - const literalsType = getUnionType(objectLiterals, ts.UnionReduction.Subtype); - return ts.concatenate(ts.filter(candidates, t => !isObjectOrArrayLiteralType(t)), [literalsType]); - } + function unionObjectAndArrayLiteralCandidates(candidates: ts.Type[]): ts.Type[] { + if (candidates.length > 1) { + const objectLiterals = ts.filter(candidates, isObjectOrArrayLiteralType); + if (objectLiterals.length) { + const literalsType = getUnionType(objectLiterals, ts.UnionReduction.Subtype); + return ts.concatenate(ts.filter(candidates, t => !isObjectOrArrayLiteralType(t)), [literalsType]); } - return candidates; } + return candidates; + } - function getContravariantInference(inference: ts.InferenceInfo) { - return inference.priority! & ts.InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates!) : getCommonSubtype(inference.contraCandidates!); - } + function getContravariantInference(inference: ts.InferenceInfo) { + return inference.priority! & ts.InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates!) : getCommonSubtype(inference.contraCandidates!); + } - function getCovariantInference(inference: ts.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 ? ts.sameMap(candidates, getRegularTypeOfLiteralType) : - widenLiteralTypes ? ts.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! & ts.InferencePriority.PriorityImpliesCombination ? - getUnionType(baseCandidates, ts.UnionReduction.Subtype) : - getCommonSupertype(baseCandidates); - return getWidenedType(unwidenedType); - } + function getCovariantInference(inference: ts.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 ? ts.sameMap(candidates, getRegularTypeOfLiteralType) : + widenLiteralTypes ? ts.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! & ts.InferencePriority.PriorityImpliesCombination ? + getUnionType(baseCandidates, ts.UnionReduction.Subtype) : + getCommonSupertype(baseCandidates); + return getWidenedType(unwidenedType); + } - function getInferredType(context: ts.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) { - // If we have both co- and contra-variant inferences, we prefer the contra-variant inference - // unless the co-variant inference is a subtype of some contra-variant inference and not 'never'. - inferredType = inferredCovariantType && !(inferredCovariantType.flags & ts.TypeFlags.Never) && - ts.some(inference.contraCandidates, t => isTypeSubtypeOf(inferredCovariantType, t)) ? - inferredCovariantType : getContravariantInference(inference); - } - else if (inferredCovariantType) { - inferredType = inferredCovariantType; - } - else if (context.flags & ts.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, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); - } - } + function getInferredType(context: ts.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) { + // If we have both co- and contra-variant inferences, we prefer the contra-variant inference + // unless the co-variant inference is a subtype of some contra-variant inference and not 'never'. + inferredType = inferredCovariantType && !(inferredCovariantType.flags & ts.TypeFlags.Never) && + ts.some(inference.contraCandidates, t => isTypeSubtypeOf(inferredCovariantType, t)) ? + inferredCovariantType : getContravariantInference(inference); + } + else if (inferredCovariantType) { + inferredType = inferredCovariantType; + } + else if (context.flags & ts.InferenceFlags.NoDefault) { + // We use silentNeverType as the wildcard that signals no inferences. + inferredType = silentNeverType; } else { - inferredType = getTypeFromInference(inference); + // 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, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); + } } + } + else { + inferredType = getTypeFromInference(inference); + } - inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & ts.InferenceFlags.AnyDefault)); + inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & ts.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; - } + 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 inference.inferredType; } - function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): ts.Type { - return isInJavaScriptFile ? anyType : unknownType; - } + return inference.inferredType; + } - function getInferredTypes(context: ts.InferenceContext): ts.Type[] { - const result: ts.Type[] = []; - for (let i = 0; i < context.inferences.length; i++) { - result.push(getInferredType(context, i)); - } - return result; - } + function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): ts.Type { + return isInJavaScriptFile ? anyType : unknownType; + } - // EXPRESSION TYPE CHECKING - - function getCannotFindNameDiagnosticForName(node: ts.Identifier): ts.DiagnosticMessage { - switch (node.escapedText) { - case "document": - case "console": - return ts.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 - ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig - : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery; - case "describe": - case "suite": - case "it": - case "test": - return compilerOptions.types - ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig - : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha; - case "process": - case "require": - case "Buffer": - case "module": - return compilerOptions.types - ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig - : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode; - case "Map": - case "Set": - case "Promise": - case "Symbol": - case "WeakMap": - case "WeakSet": - case "Iterator": - case "AsyncIterator": - case "SharedArrayBuffer": - case "Atomics": - case "AsyncIterable": - case "AsyncIterableIterator": - case "AsyncGenerator": - case "AsyncGeneratorFunction": - case "BigInt": - case "Reflect": - case "BigInt64Array": - case "BigUint64Array": - return ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_1_or_later; - case "await": - if (ts.isCallExpression(node.parent)) { - return ts.Diagnostics.Cannot_find_name_0_Did_you_mean_to_write_this_in_an_async_function; - } - // falls through - default: - if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - return ts.Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer; - } - else { - return ts.Diagnostics.Cannot_find_name_0; - } - } + function getInferredTypes(context: ts.InferenceContext): ts.Type[] { + const result: ts.Type[] = []; + for (let i = 0; i < context.inferences.length; i++) { + result.push(getInferredType(context, i)); } + return result; + } - function getResolvedSymbol(node: ts.Identifier): ts.Symbol { - const links = getNodeLinks(node); - if (!links.resolvedSymbol) { - links.resolvedSymbol = !ts.nodeIsMissing(node) && - resolveName(node, node.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue, getCannotFindNameDiagnosticForName(node), node, !ts.isWriteOnlyAccess(node), - /*excludeGlobals*/ false) || unknownSymbol; - } - return links.resolvedSymbol; + // EXPRESSION TYPE CHECKING + + function getCannotFindNameDiagnosticForName(node: ts.Identifier): ts.DiagnosticMessage { + switch (node.escapedText) { + case "document": + case "console": + return ts.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 + ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig + : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery; + case "describe": + case "suite": + case "it": + case "test": + return compilerOptions.types + ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig + : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha; + case "process": + case "require": + case "Buffer": + case "module": + return compilerOptions.types + ? ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig + : ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode; + case "Map": + case "Set": + case "Promise": + case "Symbol": + case "WeakMap": + case "WeakSet": + case "Iterator": + case "AsyncIterator": + case "SharedArrayBuffer": + case "Atomics": + case "AsyncIterable": + case "AsyncIterableIterator": + case "AsyncGenerator": + case "AsyncGeneratorFunction": + case "BigInt": + case "Reflect": + case "BigInt64Array": + case "BigUint64Array": + return ts.Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_1_or_later; + case "await": + if (ts.isCallExpression(node.parent)) { + return ts.Diagnostics.Cannot_find_name_0_Did_you_mean_to_write_this_in_an_async_function; + } + // falls through + default: + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + return ts.Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer; + } + else { + return ts.Diagnostics.Cannot_find_name_0; + } } + } - function isInTypeQuery(node: ts.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 !!ts.findAncestor(node, n => n.kind === ts.SyntaxKind.TypeQuery ? true : n.kind === ts.SyntaxKind.Identifier || n.kind === ts.SyntaxKind.QualifiedName ? false : "quit"); + function getResolvedSymbol(node: ts.Identifier): ts.Symbol { + const links = getNodeLinks(node); + if (!links.resolvedSymbol) { + links.resolvedSymbol = !ts.nodeIsMissing(node) && + resolveName(node, node.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue, getCannotFindNameDiagnosticForName(node), node, !ts.isWriteOnlyAccess(node), + /*excludeGlobals*/ false) || unknownSymbol; } + return links.resolvedSymbol; + } - // 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. - function getFlowCacheKey(node: ts.Node, declaredType: ts.Type, initialType: ts.Type, flowContainer: ts.Node | undefined): string | undefined { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - if (!ts.isThisInTypeQuery(node)) { - const symbol = getResolvedSymbol(node as ts.Identifier); - return symbol !== unknownSymbol ? `${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}|${getSymbolId(symbol)}` : undefined; - } - // falls through - case ts.SyntaxKind.ThisKeyword: - return `0|${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}`; - case ts.SyntaxKind.NonNullExpression: - case ts.SyntaxKind.ParenthesizedExpression: - return getFlowCacheKey((node as ts.NonNullExpression | ts.ParenthesizedExpression).expression, declaredType, initialType, flowContainer); - case ts.SyntaxKind.QualifiedName: - const left = getFlowCacheKey((node as ts.QualifiedName).left, declaredType, initialType, flowContainer); - return left && left + "." + (node as ts.QualifiedName).right.escapedText; - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - const propName = getAccessedPropertyName(node as ts.AccessExpression); - if (propName !== undefined) { - const key = getFlowCacheKey((node as ts.AccessExpression).expression, declaredType, initialType, flowContainer); - return key && key + "." + propName; - } - } - return undefined; - } + function isInTypeQuery(node: ts.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 !!ts.findAncestor(node, n => n.kind === ts.SyntaxKind.TypeQuery ? true : n.kind === ts.SyntaxKind.Identifier || n.kind === ts.SyntaxKind.QualifiedName ? false : "quit"); + } - function isMatchingReference(source: ts.Node, target: ts.Node): boolean { - switch (target.kind) { - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.NonNullExpression: - return isMatchingReference(source, (target as ts.NonNullExpression | ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.BinaryExpression: - return (ts.isAssignmentExpression(target) && isMatchingReference(source, target.left)) || - (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.CommaToken && isMatchingReference(source, target.right)); - } - switch (source.kind) { - case ts.SyntaxKind.MetaProperty: - return target.kind === ts.SyntaxKind.MetaProperty - && (source as ts.MetaProperty).keywordToken === (target as ts.MetaProperty).keywordToken - && (source as ts.MetaProperty).name.escapedText === (target as ts.MetaProperty).name.escapedText; - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PrivateIdentifier: - return ts.isThisInTypeQuery(source) ? - target.kind === ts.SyntaxKind.ThisKeyword : - target.kind === ts.SyntaxKind.Identifier && getResolvedSymbol(source as ts.Identifier) === getResolvedSymbol(target as ts.Identifier) || - (target.kind === ts.SyntaxKind.VariableDeclaration || target.kind === ts.SyntaxKind.BindingElement) && - getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(source as ts.Identifier)) === getSymbolOfNode(target); - case ts.SyntaxKind.ThisKeyword: - return target.kind === ts.SyntaxKind.ThisKeyword; - case ts.SyntaxKind.SuperKeyword: - return target.kind === ts.SyntaxKind.SuperKeyword; - case ts.SyntaxKind.NonNullExpression: - case ts.SyntaxKind.ParenthesizedExpression: - return isMatchingReference((source as ts.NonNullExpression | ts.ParenthesizedExpression).expression, target); - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - const sourcePropertyName = getAccessedPropertyName(source as ts.AccessExpression); - const targetPropertyName = ts.isAccessExpression(target) ? getAccessedPropertyName(target) : undefined; - return sourcePropertyName !== undefined && targetPropertyName !== undefined && targetPropertyName === sourcePropertyName && - isMatchingReference((source as ts.AccessExpression).expression, (target as ts.AccessExpression).expression); - case ts.SyntaxKind.QualifiedName: - return ts.isAccessExpression(target) && - (source as ts.QualifiedName).right.escapedText === getAccessedPropertyName(target) && - isMatchingReference((source as ts.QualifiedName).left, target.expression); - case ts.SyntaxKind.BinaryExpression: - return (ts.isBinaryExpression(source) && source.operatorToken.kind === ts.SyntaxKind.CommaToken && isMatchingReference(source.right, target)); - } - return false; - } + // 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. + function getFlowCacheKey(node: ts.Node, declaredType: ts.Type, initialType: ts.Type, flowContainer: ts.Node | undefined): string | undefined { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + if (!ts.isThisInTypeQuery(node)) { + const symbol = getResolvedSymbol(node as ts.Identifier); + return symbol !== unknownSymbol ? `${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}|${getSymbolId(symbol)}` : undefined; + } + // falls through + case ts.SyntaxKind.ThisKeyword: + return `0|${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}`; + case ts.SyntaxKind.NonNullExpression: + case ts.SyntaxKind.ParenthesizedExpression: + return getFlowCacheKey((node as ts.NonNullExpression | ts.ParenthesizedExpression).expression, declaredType, initialType, flowContainer); + case ts.SyntaxKind.QualifiedName: + const left = getFlowCacheKey((node as ts.QualifiedName).left, declaredType, initialType, flowContainer); + return left && left + "." + (node as ts.QualifiedName).right.escapedText; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const propName = getAccessedPropertyName(node as ts.AccessExpression); + if (propName !== undefined) { + const key = getFlowCacheKey((node as ts.AccessExpression).expression, declaredType, initialType, flowContainer); + return key && key + "." + propName; + } + } + return undefined; + } - function getAccessedPropertyName(access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration): ts.__String | undefined { - if (ts.isPropertyAccessExpression(access)) { - return access.name.escapedText; - } - if (ts.isElementAccessExpression(access)) { - return tryGetElementAccessExpressionName(access); - } - if (ts.isBindingElement(access)) { - const name = getDestructuringPropertyName(access); - return name ? ts.escapeLeadingUnderscores(name) : undefined; - } - if (ts.isParameter(access)) { - return ("" + access.parent.parameters.indexOf(access)) as ts.__String; - } - return undefined; - } + function isMatchingReference(source: ts.Node, target: ts.Node): boolean { + switch (target.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.NonNullExpression: + return isMatchingReference(source, (target as ts.NonNullExpression | ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.BinaryExpression: + return (ts.isAssignmentExpression(target) && isMatchingReference(source, target.left)) || + (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.CommaToken && isMatchingReference(source, target.right)); + } + switch (source.kind) { + case ts.SyntaxKind.MetaProperty: + return target.kind === ts.SyntaxKind.MetaProperty + && (source as ts.MetaProperty).keywordToken === (target as ts.MetaProperty).keywordToken + && (source as ts.MetaProperty).name.escapedText === (target as ts.MetaProperty).name.escapedText; + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PrivateIdentifier: + return ts.isThisInTypeQuery(source) ? + target.kind === ts.SyntaxKind.ThisKeyword : + target.kind === ts.SyntaxKind.Identifier && getResolvedSymbol(source as ts.Identifier) === getResolvedSymbol(target as ts.Identifier) || + (target.kind === ts.SyntaxKind.VariableDeclaration || target.kind === ts.SyntaxKind.BindingElement) && + getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(source as ts.Identifier)) === getSymbolOfNode(target); + case ts.SyntaxKind.ThisKeyword: + return target.kind === ts.SyntaxKind.ThisKeyword; + case ts.SyntaxKind.SuperKeyword: + return target.kind === ts.SyntaxKind.SuperKeyword; + case ts.SyntaxKind.NonNullExpression: + case ts.SyntaxKind.ParenthesizedExpression: + return isMatchingReference((source as ts.NonNullExpression | ts.ParenthesizedExpression).expression, target); + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const sourcePropertyName = getAccessedPropertyName(source as ts.AccessExpression); + const targetPropertyName = ts.isAccessExpression(target) ? getAccessedPropertyName(target) : undefined; + return sourcePropertyName !== undefined && targetPropertyName !== undefined && targetPropertyName === sourcePropertyName && + isMatchingReference((source as ts.AccessExpression).expression, (target as ts.AccessExpression).expression); + case ts.SyntaxKind.QualifiedName: + return ts.isAccessExpression(target) && + (source as ts.QualifiedName).right.escapedText === getAccessedPropertyName(target) && + isMatchingReference((source as ts.QualifiedName).left, target.expression); + case ts.SyntaxKind.BinaryExpression: + return (ts.isBinaryExpression(source) && source.operatorToken.kind === ts.SyntaxKind.CommaToken && isMatchingReference(source.right, target)); + } + return false; + } - function tryGetNameFromType(type: ts.Type) { - return type.flags & ts.TypeFlags.UniqueESSymbol ? (type as ts.UniqueESSymbolType).escapedName : - type.flags & ts.TypeFlags.StringOrNumberLiteral ? ts.escapeLeadingUnderscores("" + (type as ts.StringLiteralType | ts.NumberLiteralType).value) : undefined; + function getAccessedPropertyName(access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration): ts.__String | undefined { + if (ts.isPropertyAccessExpression(access)) { + return access.name.escapedText; + } + if (ts.isElementAccessExpression(access)) { + return tryGetElementAccessExpressionName(access); } + if (ts.isBindingElement(access)) { + const name = getDestructuringPropertyName(access); + return name ? ts.escapeLeadingUnderscores(name) : undefined; + } + if (ts.isParameter(access)) { + return ("" + access.parent.parameters.indexOf(access)) as ts.__String; + } + return undefined; + } - function tryGetElementAccessExpressionName(node: ts.ElementAccessExpression) { - if (ts.isStringOrNumericLiteralLike(node.argumentExpression)) { - return ts.escapeLeadingUnderscores(node.argumentExpression.text); - } - if (ts.isEntityNameExpression(node.argumentExpression)) { - const symbol = resolveEntityName(node.argumentExpression, ts.SymbolFlags.Value, /*ignoreErrors*/ true); - if (!symbol || !isConstVariable(symbol)) - return undefined; + function tryGetNameFromType(type: ts.Type) { + return type.flags & ts.TypeFlags.UniqueESSymbol ? (type as ts.UniqueESSymbolType).escapedName : + type.flags & ts.TypeFlags.StringOrNumberLiteral ? ts.escapeLeadingUnderscores("" + (type as ts.StringLiteralType | ts.NumberLiteralType).value) : undefined; + } - const declaration = symbol.valueDeclaration; - if (declaration === undefined) - return undefined; + function tryGetElementAccessExpressionName(node: ts.ElementAccessExpression) { + if (ts.isStringOrNumericLiteralLike(node.argumentExpression)) { + return ts.escapeLeadingUnderscores(node.argumentExpression.text); + } + if (ts.isEntityNameExpression(node.argumentExpression)) { + const symbol = resolveEntityName(node.argumentExpression, ts.SymbolFlags.Value, /*ignoreErrors*/ true); + if (!symbol || !isConstVariable(symbol)) + return undefined; - const type = tryGetTypeFromEffectiveTypeNode(declaration); - if (type) { - const name = tryGetNameFromType(type); - if (name !== undefined) { - return name; - } - } + const declaration = symbol.valueDeclaration; + if (declaration === undefined) + return undefined; - if (ts.hasOnlyExpressionInitializer(declaration)) { - const initializer = ts.getEffectiveInitializer(declaration); - return initializer && tryGetNameFromType(getTypeOfExpression(initializer)); + const type = tryGetTypeFromEffectiveTypeNode(declaration); + if (type) { + const name = tryGetNameFromType(type); + if (name !== undefined) { + return name; } } - return undefined; + + if (ts.hasOnlyExpressionInitializer(declaration)) { + const initializer = ts.getEffectiveInitializer(declaration); + return initializer && tryGetNameFromType(getTypeOfExpression(initializer)); + } } + return undefined; + } - function containsMatchingReference(source: ts.Node, target: ts.Node) { - while (ts.isAccessExpression(source)) { - source = source.expression; - if (isMatchingReference(source, target)) { - return true; - } + function containsMatchingReference(source: ts.Node, target: ts.Node) { + while (ts.isAccessExpression(source)) { + source = source.expression; + if (isMatchingReference(source, target)) { + return true; } - return false; } + return false; + } - function optionalChainContainsReference(source: ts.Node, target: ts.Node) { - while (ts.isOptionalChain(source)) { - source = source.expression; - if (isMatchingReference(source, target)) { - return true; - } + function optionalChainContainsReference(source: ts.Node, target: ts.Node) { + while (ts.isOptionalChain(source)) { + source = source.expression; + if (isMatchingReference(source, target)) { + return true; } - return false; } + return false; + } - function isDiscriminantProperty(type: ts.Type | undefined, name: ts.__String) { - if (type && type.flags & ts.TypeFlags.Union) { - const prop = getUnionOrIntersectionProperty(type as ts.UnionType, name); - if (prop && ts.getCheckFlags(prop) & ts.CheckFlags.SyntheticProperty) { - if ((prop as ts.TransientSymbol).isDiscriminantProperty === undefined) { - (prop as ts.TransientSymbol).isDiscriminantProperty = - ((prop as ts.TransientSymbol).checkFlags & ts.CheckFlags.Discriminant) === ts.CheckFlags.Discriminant && - !isGenericType(getTypeOfSymbol(prop)); - } - return !!(prop as ts.TransientSymbol).isDiscriminantProperty; + function isDiscriminantProperty(type: ts.Type | undefined, name: ts.__String) { + if (type && type.flags & ts.TypeFlags.Union) { + const prop = getUnionOrIntersectionProperty(type as ts.UnionType, name); + if (prop && ts.getCheckFlags(prop) & ts.CheckFlags.SyntheticProperty) { + if ((prop as ts.TransientSymbol).isDiscriminantProperty === undefined) { + (prop as ts.TransientSymbol).isDiscriminantProperty = + ((prop as ts.TransientSymbol).checkFlags & ts.CheckFlags.Discriminant) === ts.CheckFlags.Discriminant && + !isGenericType(getTypeOfSymbol(prop)); } + return !!(prop as ts.TransientSymbol).isDiscriminantProperty; } - return false; } + return false; + } - 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]; + 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]; } - return result; } + return result; + } - // Given a set of constituent types and a property name, create and return a map keyed by the literal - // types of the property by that name in each constituent type. No map is returned if some key property - // has a non-literal type or if less than 10 or less than 50% of the constituents have a unique key. - // Entries with duplicate keys have unknownType as the value. - function mapTypesByKeyProperty(types: ts.Type[], name: ts.__String) { - const map = new ts.Map(); - let count = 0; - for (const type of types) { - if (type.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive)) { - const discriminant = getTypeOfPropertyOfType(type, name); - if (discriminant) { - if (!isLiteralType(discriminant)) { - return undefined; - } - let duplicate = false; - forEachType(discriminant, t => { - const id = getTypeId(getRegularTypeOfLiteralType(t)); - const existing = map.get(id); - if (!existing) { - map.set(id, type); - } - else if (existing !== unknownType) { - map.set(id, unknownType); - duplicate = true; - } - }); - if (!duplicate) - count++; + // Given a set of constituent types and a property name, create and return a map keyed by the literal + // types of the property by that name in each constituent type. No map is returned if some key property + // has a non-literal type or if less than 10 or less than 50% of the constituents have a unique key. + // Entries with duplicate keys have unknownType as the value. + function mapTypesByKeyProperty(types: ts.Type[], name: ts.__String) { + const map = new ts.Map(); + let count = 0; + for (const type of types) { + if (type.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection | ts.TypeFlags.InstantiableNonPrimitive)) { + const discriminant = getTypeOfPropertyOfType(type, name); + if (discriminant) { + if (!isLiteralType(discriminant)) { + return undefined; } + let duplicate = false; + forEachType(discriminant, t => { + const id = getTypeId(getRegularTypeOfLiteralType(t)); + const existing = map.get(id); + if (!existing) { + map.set(id, type); + } + else if (existing !== unknownType) { + map.set(id, unknownType); + duplicate = true; + } + }); + if (!duplicate) + count++; } } - return count >= 10 && count * 2 >= types.length ? map : undefined; } + return count >= 10 && count * 2 >= types.length ? map : undefined; + } - // Return the name of a discriminant property for which it was possible and feasible to construct a map of - // constituent types keyed by the literal types of the property by that name in each constituent type. - function getKeyPropertyName(unionType: ts.UnionType): ts.__String | undefined { - const types = unionType.types; - // We only construct maps for unions with many non-primitive constituents. - if (types.length < 10 || ts.getObjectFlags(unionType) & ts.ObjectFlags.PrimitiveUnion || - ts.countWhere(types, t => !!(t.flags & (ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive))) < 10) { - return undefined; - } - if (unionType.keyPropertyName === undefined) { - // The candidate key property name is the name of the first property with a unit type in one of the - // constituent types. - const keyPropertyName = ts.forEach(types, t => t.flags & (ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive) ? - ts.forEach(getPropertiesOfType(t), p => isUnitType(getTypeOfSymbol(p)) ? p.escapedName : undefined) : - undefined); - const mapByKeyProperty = keyPropertyName && mapTypesByKeyProperty(types, keyPropertyName); - unionType.keyPropertyName = mapByKeyProperty ? keyPropertyName : "" as ts.__String; - unionType.constituentMap = mapByKeyProperty; - } - return (unionType.keyPropertyName as string).length ? unionType.keyPropertyName : undefined; + // Return the name of a discriminant property for which it was possible and feasible to construct a map of + // constituent types keyed by the literal types of the property by that name in each constituent type. + function getKeyPropertyName(unionType: ts.UnionType): ts.__String | undefined { + const types = unionType.types; + // We only construct maps for unions with many non-primitive constituents. + if (types.length < 10 || ts.getObjectFlags(unionType) & ts.ObjectFlags.PrimitiveUnion || + ts.countWhere(types, t => !!(t.flags & (ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive))) < 10) { + return undefined; } + if (unionType.keyPropertyName === undefined) { + // The candidate key property name is the name of the first property with a unit type in one of the + // constituent types. + const keyPropertyName = ts.forEach(types, t => t.flags & (ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive) ? + ts.forEach(getPropertiesOfType(t), p => isUnitType(getTypeOfSymbol(p)) ? p.escapedName : undefined) : + undefined); + const mapByKeyProperty = keyPropertyName && mapTypesByKeyProperty(types, keyPropertyName); + unionType.keyPropertyName = mapByKeyProperty ? keyPropertyName : "" as ts.__String; + unionType.constituentMap = mapByKeyProperty; + } + return (unionType.keyPropertyName as string).length ? unionType.keyPropertyName : undefined; + } - // Given a union type for which getKeyPropertyName returned a non-undefined result, return the constituent - // that corresponds to the given key type for that property name. - function getConstituentTypeForKeyType(unionType: ts.UnionType, keyType: ts.Type) { - const result = unionType.constituentMap?.get(getTypeId(getRegularTypeOfLiteralType(keyType))); - return result !== unknownType ? result : undefined; - } + // Given a union type for which getKeyPropertyName returned a non-undefined result, return the constituent + // that corresponds to the given key type for that property name. + function getConstituentTypeForKeyType(unionType: ts.UnionType, keyType: ts.Type) { + const result = unionType.constituentMap?.get(getTypeId(getRegularTypeOfLiteralType(keyType))); + return result !== unknownType ? result : undefined; + } - function getMatchingUnionConstituentForType(unionType: ts.UnionType, type: ts.Type) { - const keyPropertyName = getKeyPropertyName(unionType); - const propType = keyPropertyName && getTypeOfPropertyOfType(type, keyPropertyName); - return propType && getConstituentTypeForKeyType(unionType, propType); - } + function getMatchingUnionConstituentForType(unionType: ts.UnionType, type: ts.Type) { + const keyPropertyName = getKeyPropertyName(unionType); + const propType = keyPropertyName && getTypeOfPropertyOfType(type, keyPropertyName); + return propType && getConstituentTypeForKeyType(unionType, propType); + } - function getMatchingUnionConstituentForObjectLiteral(unionType: ts.UnionType, node: ts.ObjectLiteralExpression) { - const keyPropertyName = getKeyPropertyName(unionType); - const propNode = keyPropertyName && ts.find(node.properties, p => p.symbol && p.kind === ts.SyntaxKind.PropertyAssignment && - p.symbol.escapedName === keyPropertyName && isPossiblyDiscriminantValue(p.initializer)); - const propType = propNode && getContextFreeTypeOfExpression((propNode as ts.PropertyAssignment).initializer); - return propType && getConstituentTypeForKeyType(unionType, propType); - } + function getMatchingUnionConstituentForObjectLiteral(unionType: ts.UnionType, node: ts.ObjectLiteralExpression) { + const keyPropertyName = getKeyPropertyName(unionType); + const propNode = keyPropertyName && ts.find(node.properties, p => p.symbol && p.kind === ts.SyntaxKind.PropertyAssignment && + p.symbol.escapedName === keyPropertyName && isPossiblyDiscriminantValue(p.initializer)); + const propType = propNode && getContextFreeTypeOfExpression((propNode as ts.PropertyAssignment).initializer); + return propType && getConstituentTypeForKeyType(unionType, propType); + } - function isOrContainsMatchingReference(source: ts.Node, target: ts.Node) { - return isMatchingReference(source, target) || containsMatchingReference(source, target); - } + function isOrContainsMatchingReference(source: ts.Node, target: ts.Node) { + return isMatchingReference(source, target) || containsMatchingReference(source, target); + } - function hasMatchingArgument(expression: ts.CallExpression | ts.NewExpression, reference: ts.Node) { - if (expression.arguments) { - for (const argument of expression.arguments) { - if (isOrContainsMatchingReference(reference, argument)) { - return true; - } + function hasMatchingArgument(expression: ts.CallExpression | ts.NewExpression, reference: ts.Node) { + if (expression.arguments) { + for (const argument of expression.arguments) { + if (isOrContainsMatchingReference(reference, argument)) { + return true; } } - if (expression.expression.kind === ts.SyntaxKind.PropertyAccessExpression && - isOrContainsMatchingReference(reference, (expression.expression as ts.PropertyAccessExpression).expression)) { - return true; - } - return false; } - - function getFlowNodeId(flow: ts.FlowNode): number { - if (!flow.id || flow.id < 0) { - flow.id = nextFlowId; - nextFlowId++; - } - return flow.id; + if (expression.expression.kind === ts.SyntaxKind.PropertyAccessExpression && + isOrContainsMatchingReference(reference, (expression.expression as ts.PropertyAccessExpression).expression)) { + return true; } + return false; + } - function typeMaybeAssignableTo(source: ts.Type, target: ts.Type) { - if (!(source.flags & ts.TypeFlags.Union)) { - return isTypeAssignableTo(source, target); - } - for (const t of (source as ts.UnionType).types) { - if (isTypeAssignableTo(t, target)) { - return true; - } - } - return false; + function getFlowNodeId(flow: ts.FlowNode): number { + if (!flow.id || flow.id < 0) { + flow.id = nextFlowId; + nextFlowId++; } + return flow.id; + } - // 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: ts.UnionType, assignedType: ts.Type) { - if (declaredType !== assignedType) { - if (assignedType.flags & ts.TypeFlags.Never) { - return assignedType; - } - let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); - if (assignedType.flags & ts.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 typeMaybeAssignableTo(source: ts.Type, target: ts.Type) { + if (!(source.flags & ts.TypeFlags.Union)) { + return isTypeAssignableTo(source, target); } - - function isFunctionObjectType(type: ts.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 ts.__String) && isTypeSubtypeOf(type, globalFunctionType)); + for (const t of (source as ts.UnionType).types) { + if (isTypeAssignableTo(t, target)) { + return true; + } } + return false; + } - function getTypeFacts(type: ts.Type, ignoreObjects = false): TypeFacts { - const flags = type.flags; - if (flags & ts.TypeFlags.String) { - return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts; - } - if (flags & ts.TypeFlags.StringLiteral) { - const isEmpty = (type as ts.StringLiteralType).value === ""; - return strictNullChecks ? - isEmpty ? TypeFacts.EmptyStringStrictFacts : TypeFacts.NonEmptyStringStrictFacts : - isEmpty ? TypeFacts.EmptyStringFacts : TypeFacts.NonEmptyStringFacts; - } - if (flags & (ts.TypeFlags.Number | ts.TypeFlags.Enum)) { - return strictNullChecks ? TypeFacts.NumberStrictFacts : TypeFacts.NumberFacts; - } - if (flags & ts.TypeFlags.NumberLiteral) { - const isZero = (type as ts.NumberLiteralType).value === 0; - return strictNullChecks ? - isZero ? TypeFacts.ZeroNumberStrictFacts : TypeFacts.NonZeroNumberStrictFacts : - isZero ? TypeFacts.ZeroNumberFacts : TypeFacts.NonZeroNumberFacts; - } - if (flags & ts.TypeFlags.BigInt) { - return strictNullChecks ? TypeFacts.BigIntStrictFacts : TypeFacts.BigIntFacts; - } - if (flags & ts.TypeFlags.BigIntLiteral) { - const isZero = isZeroBigInt(type as ts.BigIntLiteralType); - return strictNullChecks ? - isZero ? TypeFacts.ZeroBigIntStrictFacts : TypeFacts.NonZeroBigIntStrictFacts : - isZero ? TypeFacts.ZeroBigIntFacts : TypeFacts.NonZeroBigIntFacts; - } - if (flags & ts.TypeFlags.Boolean) { - return strictNullChecks ? TypeFacts.BooleanStrictFacts : TypeFacts.BooleanFacts; - } - if (flags & ts.TypeFlags.BooleanLike) { - return strictNullChecks ? - (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : - (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; - } - if (flags & ts.TypeFlags.Object) { - if (ignoreObjects) { - return TypeFacts.AndFactsMask; // This is the identity element for computing type facts of intersection. - } - return ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && isEmptyObjectType(type as ts.ObjectType) ? - strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : - isFunctionObjectType(type as ts.ObjectType) ? - strictNullChecks ? TypeFacts.FunctionStrictFacts : TypeFacts.FunctionFacts : - strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; + // 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: ts.UnionType, assignedType: ts.Type) { + if (declaredType !== assignedType) { + if (assignedType.flags & ts.TypeFlags.Never) { + return assignedType; } - if (flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) { - return TypeFacts.UndefinedFacts; + let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); + if (assignedType.flags & ts.TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType)) { + reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types } - if (flags & ts.TypeFlags.Null) { - return TypeFacts.NullFacts; + // 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; } - if (flags & ts.TypeFlags.ESSymbolLike) { - return strictNullChecks ? TypeFacts.SymbolStrictFacts : TypeFacts.SymbolFacts; - } - if (flags & ts.TypeFlags.NonPrimitive) { - return strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; - } - if (flags & ts.TypeFlags.Never) { - return TypeFacts.None; - } - if (flags & ts.TypeFlags.Instantiable) { - return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType, ignoreObjects) : - strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts; - } - if (flags & ts.TypeFlags.Union) { - return ts.reduceLeft((type as ts.UnionType).types, (facts, t) => facts | getTypeFacts(t, ignoreObjects), TypeFacts.None); - } - if (flags & ts.TypeFlags.Intersection) { - // When an intersection contains a primitive type we ignore object type constituents as they are - // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type. - ignoreObjects ||= maybeTypeOfKind(type, ts.TypeFlags.Primitive); - return getIntersectionTypeFacts(type as ts.IntersectionType, ignoreObjects); - } - return TypeFacts.All; } + return declaredType; + } + + function isFunctionObjectType(type: ts.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 ts.__String) && isTypeSubtypeOf(type, globalFunctionType)); + } - function getIntersectionTypeFacts(type: ts.IntersectionType, ignoreObjects: boolean): TypeFacts { - // When computing the type facts of an intersection type, certain type facts are computed as `and` - // and others are computed as `or`. - let oredFacts = TypeFacts.None; - let andedFacts = TypeFacts.All; - for (const t of type.types) { - const f = getTypeFacts(t, ignoreObjects); - oredFacts |= f; - andedFacts &= f; + function getTypeFacts(type: ts.Type, ignoreObjects = false): TypeFacts { + const flags = type.flags; + if (flags & ts.TypeFlags.String) { + return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts; + } + if (flags & ts.TypeFlags.StringLiteral) { + const isEmpty = (type as ts.StringLiteralType).value === ""; + return strictNullChecks ? + isEmpty ? TypeFacts.EmptyStringStrictFacts : TypeFacts.NonEmptyStringStrictFacts : + isEmpty ? TypeFacts.EmptyStringFacts : TypeFacts.NonEmptyStringFacts; + } + if (flags & (ts.TypeFlags.Number | ts.TypeFlags.Enum)) { + return strictNullChecks ? TypeFacts.NumberStrictFacts : TypeFacts.NumberFacts; + } + if (flags & ts.TypeFlags.NumberLiteral) { + const isZero = (type as ts.NumberLiteralType).value === 0; + return strictNullChecks ? + isZero ? TypeFacts.ZeroNumberStrictFacts : TypeFacts.NonZeroNumberStrictFacts : + isZero ? TypeFacts.ZeroNumberFacts : TypeFacts.NonZeroNumberFacts; + } + if (flags & ts.TypeFlags.BigInt) { + return strictNullChecks ? TypeFacts.BigIntStrictFacts : TypeFacts.BigIntFacts; + } + if (flags & ts.TypeFlags.BigIntLiteral) { + const isZero = isZeroBigInt(type as ts.BigIntLiteralType); + return strictNullChecks ? + isZero ? TypeFacts.ZeroBigIntStrictFacts : TypeFacts.NonZeroBigIntStrictFacts : + isZero ? TypeFacts.ZeroBigIntFacts : TypeFacts.NonZeroBigIntFacts; + } + if (flags & ts.TypeFlags.Boolean) { + return strictNullChecks ? TypeFacts.BooleanStrictFacts : TypeFacts.BooleanFacts; + } + if (flags & ts.TypeFlags.BooleanLike) { + return strictNullChecks ? + (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : + (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; + } + if (flags & ts.TypeFlags.Object) { + if (ignoreObjects) { + return TypeFacts.AndFactsMask; // This is the identity element for computing type facts of intersection. } - return oredFacts & TypeFacts.OrFactsMask | andedFacts & TypeFacts.AndFactsMask; + return ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous && isEmptyObjectType(type as ts.ObjectType) ? + strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : + isFunctionObjectType(type as ts.ObjectType) ? + strictNullChecks ? TypeFacts.FunctionStrictFacts : TypeFacts.FunctionFacts : + strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; } - - function getTypeWithFacts(type: ts.Type, include: TypeFacts) { - return filterType(type, t => (getTypeFacts(t) & include) !== 0); + if (flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) { + return TypeFacts.UndefinedFacts; } - - function getTypeWithDefault(type: ts.Type, defaultExpression: ts.Expression) { - return defaultExpression ? - getUnionType([getNonUndefinedType(type), getTypeOfExpression(defaultExpression)]) : - type; + if (flags & ts.TypeFlags.Null) { + return TypeFacts.NullFacts; } - - function getTypeOfDestructuredProperty(type: ts.Type, name: ts.PropertyName) { - const nameType = getLiteralTypeFromPropertyName(name); - if (!isTypeUsableAsPropertyName(nameType)) - return errorType; - const text = getPropertyNameFromType(nameType); - return getTypeOfPropertyOfType(type, text) || includeUndefinedInIndexSignature(getApplicableIndexInfoForName(type, text)?.type) || errorType; + if (flags & ts.TypeFlags.ESSymbolLike) { + return strictNullChecks ? TypeFacts.SymbolStrictFacts : TypeFacts.SymbolFacts; } - - function getTypeOfDestructuredArrayElement(type: ts.Type, index: number) { - return everyType(type, isTupleLikeType) && getTupleElementType(type, index) || - includeUndefinedInIndexSignature(checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined)) || - errorType; + if (flags & ts.TypeFlags.NonPrimitive) { + return strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; } - - function includeUndefinedInIndexSignature(type: ts.Type | undefined): ts.Type | undefined { - if (!type) - return type; - return compilerOptions.noUncheckedIndexedAccess ? - getUnionType([type, undefinedType]) : - type; + if (flags & ts.TypeFlags.Never) { + return TypeFacts.None; } + if (flags & ts.TypeFlags.Instantiable) { + return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType, ignoreObjects) : + strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts; + } + if (flags & ts.TypeFlags.Union) { + return ts.reduceLeft((type as ts.UnionType).types, (facts, t) => facts | getTypeFacts(t, ignoreObjects), TypeFacts.None); + } + if (flags & ts.TypeFlags.Intersection) { + // When an intersection contains a primitive type we ignore object type constituents as they are + // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type. + ignoreObjects ||= maybeTypeOfKind(type, ts.TypeFlags.Primitive); + return getIntersectionTypeFacts(type as ts.IntersectionType, ignoreObjects); + } + return TypeFacts.All; + } + + function getIntersectionTypeFacts(type: ts.IntersectionType, ignoreObjects: boolean): TypeFacts { + // When computing the type facts of an intersection type, certain type facts are computed as `and` + // and others are computed as `or`. + let oredFacts = TypeFacts.None; + let andedFacts = TypeFacts.All; + for (const t of type.types) { + const f = getTypeFacts(t, ignoreObjects); + oredFacts |= f; + andedFacts &= f; + } + return oredFacts & TypeFacts.OrFactsMask | andedFacts & TypeFacts.AndFactsMask; + } + + function getTypeWithFacts(type: ts.Type, include: TypeFacts) { + return filterType(type, t => (getTypeFacts(t) & include) !== 0); + } + + function getTypeWithDefault(type: ts.Type, defaultExpression: ts.Expression) { + return defaultExpression ? + getUnionType([getNonUndefinedType(type), getTypeOfExpression(defaultExpression)]) : + type; + } + + function getTypeOfDestructuredProperty(type: ts.Type, name: ts.PropertyName) { + const nameType = getLiteralTypeFromPropertyName(name); + if (!isTypeUsableAsPropertyName(nameType)) + return errorType; + const text = getPropertyNameFromType(nameType); + return getTypeOfPropertyOfType(type, text) || includeUndefinedInIndexSignature(getApplicableIndexInfoForName(type, text)?.type) || errorType; + } + + function getTypeOfDestructuredArrayElement(type: ts.Type, index: number) { + return everyType(type, isTupleLikeType) && getTupleElementType(type, index) || + includeUndefinedInIndexSignature(checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined)) || + errorType; + } + + function includeUndefinedInIndexSignature(type: ts.Type | undefined): ts.Type | undefined { + if (!type) + return type; + return compilerOptions.noUncheckedIndexedAccess ? + getUnionType([type, undefinedType]) : + type; + } + + function getTypeOfDestructuredSpreadExpression(type: ts.Type) { + return createArrayType(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: ts.BinaryExpression): ts.Type { + const isDestructuringDefaultAssignment = node.parent.kind === ts.SyntaxKind.ArrayLiteralExpression && isDestructuringAssignmentTarget(node.parent) || + node.parent.kind === ts.SyntaxKind.PropertyAssignment && isDestructuringAssignmentTarget(node.parent.parent); + return isDestructuringDefaultAssignment ? + getTypeWithDefault(getAssignedType(node), node.right) : + getTypeOfExpression(node.right); + } - function getAssignedTypeOfBinaryExpression(node: ts.BinaryExpression): ts.Type { - const isDestructuringDefaultAssignment = node.parent.kind === ts.SyntaxKind.ArrayLiteralExpression && isDestructuringAssignmentTarget(node.parent) || - node.parent.kind === ts.SyntaxKind.PropertyAssignment && isDestructuringAssignmentTarget(node.parent.parent); - return isDestructuringDefaultAssignment ? - getTypeWithDefault(getAssignedType(node), node.right) : - getTypeOfExpression(node.right); - } + function isDestructuringAssignmentTarget(parent: ts.Node) { + return parent.parent.kind === ts.SyntaxKind.BinaryExpression && (parent.parent as ts.BinaryExpression).left === parent || + parent.parent.kind === ts.SyntaxKind.ForOfStatement && (parent.parent as ts.ForOfStatement).initializer === parent; + } - function isDestructuringAssignmentTarget(parent: ts.Node) { - return parent.parent.kind === ts.SyntaxKind.BinaryExpression && (parent.parent as ts.BinaryExpression).left === parent || - parent.parent.kind === ts.SyntaxKind.ForOfStatement && (parent.parent as ts.ForOfStatement).initializer === parent; - } + function getAssignedTypeOfArrayLiteralElement(node: ts.ArrayLiteralExpression, element: ts.Expression): ts.Type { + return getTypeOfDestructuredArrayElement(getAssignedType(node), node.elements.indexOf(element)); + } - function getAssignedTypeOfArrayLiteralElement(node: ts.ArrayLiteralExpression, element: ts.Expression): ts.Type { - return getTypeOfDestructuredArrayElement(getAssignedType(node), node.elements.indexOf(element)); - } + function getAssignedTypeOfSpreadExpression(node: ts.SpreadElement): ts.Type { + return getTypeOfDestructuredSpreadExpression(getAssignedType(node.parent as ts.ArrayLiteralExpression)); + } - function getAssignedTypeOfSpreadExpression(node: ts.SpreadElement): ts.Type { - return getTypeOfDestructuredSpreadExpression(getAssignedType(node.parent as ts.ArrayLiteralExpression)); - } + function getAssignedTypeOfPropertyAssignment(node: ts.PropertyAssignment | ts.ShorthandPropertyAssignment): ts.Type { + return getTypeOfDestructuredProperty(getAssignedType(node.parent), node.name); + } - function getAssignedTypeOfPropertyAssignment(node: ts.PropertyAssignment | ts.ShorthandPropertyAssignment): ts.Type { - return getTypeOfDestructuredProperty(getAssignedType(node.parent), node.name); - } + function getAssignedTypeOfShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment): ts.Type { + return getTypeWithDefault(getAssignedTypeOfPropertyAssignment(node), node.objectAssignmentInitializer!); + } - function getAssignedTypeOfShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment): ts.Type { - return getTypeWithDefault(getAssignedTypeOfPropertyAssignment(node), node.objectAssignmentInitializer!); - } + function getAssignedType(node: ts.Expression): ts.Type { + const { parent } = node; + switch (parent.kind) { + case ts.SyntaxKind.ForInStatement: + return stringType; + case ts.SyntaxKind.ForOfStatement: + return checkRightHandSideOfForOf(parent as ts.ForOfStatement) || errorType; + case ts.SyntaxKind.BinaryExpression: + return getAssignedTypeOfBinaryExpression(parent as ts.BinaryExpression); + case ts.SyntaxKind.DeleteExpression: + return undefinedType; + case ts.SyntaxKind.ArrayLiteralExpression: + return getAssignedTypeOfArrayLiteralElement(parent as ts.ArrayLiteralExpression, node); + case ts.SyntaxKind.SpreadElement: + return getAssignedTypeOfSpreadExpression(parent as ts.SpreadElement); + case ts.SyntaxKind.PropertyAssignment: + return getAssignedTypeOfPropertyAssignment(parent as ts.PropertyAssignment); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return getAssignedTypeOfShorthandPropertyAssignment(parent as ts.ShorthandPropertyAssignment); + } + return errorType; + } - function getAssignedType(node: ts.Expression): ts.Type { - const { parent } = node; - switch (parent.kind) { - case ts.SyntaxKind.ForInStatement: - return stringType; - case ts.SyntaxKind.ForOfStatement: - return checkRightHandSideOfForOf(parent as ts.ForOfStatement) || errorType; - case ts.SyntaxKind.BinaryExpression: - return getAssignedTypeOfBinaryExpression(parent as ts.BinaryExpression); - case ts.SyntaxKind.DeleteExpression: - return undefinedType; - case ts.SyntaxKind.ArrayLiteralExpression: - return getAssignedTypeOfArrayLiteralElement(parent as ts.ArrayLiteralExpression, node); - case ts.SyntaxKind.SpreadElement: - return getAssignedTypeOfSpreadExpression(parent as ts.SpreadElement); - case ts.SyntaxKind.PropertyAssignment: - return getAssignedTypeOfPropertyAssignment(parent as ts.PropertyAssignment); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return getAssignedTypeOfShorthandPropertyAssignment(parent as ts.ShorthandPropertyAssignment); - } - return errorType; - } + function getInitialTypeOfBindingElement(node: ts.BindingElement): ts.Type { + const pattern = node.parent; + const parentType = getInitialType(pattern.parent as ts.VariableDeclaration | ts.BindingElement); + const type = pattern.kind === ts.SyntaxKind.ObjectBindingPattern ? + getTypeOfDestructuredProperty(parentType, node.propertyName || node.name as ts.Identifier) : + !node.dotDotDotToken ? + getTypeOfDestructuredArrayElement(parentType, pattern.elements.indexOf(node)) : + getTypeOfDestructuredSpreadExpression(parentType); + return getTypeWithDefault(type, node.initializer!); + } - function getInitialTypeOfBindingElement(node: ts.BindingElement): ts.Type { - const pattern = node.parent; - const parentType = getInitialType(pattern.parent as ts.VariableDeclaration | ts.BindingElement); - const type = pattern.kind === ts.SyntaxKind.ObjectBindingPattern ? - getTypeOfDestructuredProperty(parentType, node.propertyName || node.name as ts.Identifier) : - !node.dotDotDotToken ? - getTypeOfDestructuredArrayElement(parentType, pattern.elements.indexOf(node)) : - getTypeOfDestructuredSpreadExpression(parentType); - return getTypeWithDefault(type, node.initializer!); - } + function getTypeOfInitializer(node: ts.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 getTypeOfInitializer(node: ts.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: ts.VariableDeclaration) { + if (node.initializer) { + return getTypeOfInitializer(node.initializer); } - - function getInitialTypeOfVariableDeclaration(node: ts.VariableDeclaration) { - if (node.initializer) { - return getTypeOfInitializer(node.initializer); - } - if (node.parent.parent.kind === ts.SyntaxKind.ForInStatement) { - return stringType; - } - if (node.parent.parent.kind === ts.SyntaxKind.ForOfStatement) { - return checkRightHandSideOfForOf(node.parent.parent) || errorType; - } - return errorType; + if (node.parent.parent.kind === ts.SyntaxKind.ForInStatement) { + return stringType; } - - function getInitialType(node: ts.VariableDeclaration | ts.BindingElement) { - return node.kind === ts.SyntaxKind.VariableDeclaration ? - getInitialTypeOfVariableDeclaration(node) : - getInitialTypeOfBindingElement(node); + if (node.parent.parent.kind === ts.SyntaxKind.ForOfStatement) { + return checkRightHandSideOfForOf(node.parent.parent) || errorType; } + return errorType; + } - function isEmptyArrayAssignment(node: ts.VariableDeclaration | ts.BindingElement | ts.Expression) { - return node.kind === ts.SyntaxKind.VariableDeclaration && (node as ts.VariableDeclaration).initializer && - isEmptyArrayLiteral((node as ts.VariableDeclaration).initializer!) || - node.kind !== ts.SyntaxKind.BindingElement && node.parent.kind === ts.SyntaxKind.BinaryExpression && - isEmptyArrayLiteral((node.parent as ts.BinaryExpression).right); - } + function getInitialType(node: ts.VariableDeclaration | ts.BindingElement) { + return node.kind === ts.SyntaxKind.VariableDeclaration ? + getInitialTypeOfVariableDeclaration(node) : + getInitialTypeOfBindingElement(node); + } - function getReferenceCandidate(node: ts.Expression): ts.Expression { - switch (node.kind) { - case ts.SyntaxKind.ParenthesizedExpression: - return getReferenceCandidate((node as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.BinaryExpression: - switch ((node as ts.BinaryExpression).operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.BarBarEqualsToken: - case ts.SyntaxKind.AmpersandAmpersandEqualsToken: - case ts.SyntaxKind.QuestionQuestionEqualsToken: - return getReferenceCandidate((node as ts.BinaryExpression).left); - case ts.SyntaxKind.CommaToken: - return getReferenceCandidate((node as ts.BinaryExpression).right); - } - } - return node; - } + function isEmptyArrayAssignment(node: ts.VariableDeclaration | ts.BindingElement | ts.Expression) { + return node.kind === ts.SyntaxKind.VariableDeclaration && (node as ts.VariableDeclaration).initializer && + isEmptyArrayLiteral((node as ts.VariableDeclaration).initializer!) || + node.kind !== ts.SyntaxKind.BindingElement && node.parent.kind === ts.SyntaxKind.BinaryExpression && + isEmptyArrayLiteral((node.parent as ts.BinaryExpression).right); + } - function getReferenceRoot(node: ts.Node): ts.Node { - const { parent } = node; - return parent.kind === ts.SyntaxKind.ParenthesizedExpression || - parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken && (parent as ts.BinaryExpression).left === node || - parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken && (parent as ts.BinaryExpression).right === node ? - getReferenceRoot(parent) : node; + function getReferenceCandidate(node: ts.Expression): ts.Expression { + switch (node.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + return getReferenceCandidate((node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.BinaryExpression: + switch ((node as ts.BinaryExpression).operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + return getReferenceCandidate((node as ts.BinaryExpression).left); + case ts.SyntaxKind.CommaToken: + return getReferenceCandidate((node as ts.BinaryExpression).right); + } } + return node; + } - function getTypeOfSwitchClause(clause: ts.CaseClause | ts.DefaultClause) { - if (clause.kind === ts.SyntaxKind.CaseClause) { - return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); - } - return neverType; + function getReferenceRoot(node: ts.Node): ts.Node { + const { parent } = node; + return parent.kind === ts.SyntaxKind.ParenthesizedExpression || + parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken && (parent as ts.BinaryExpression).left === node || + parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken && (parent as ts.BinaryExpression).right === node ? + getReferenceRoot(parent) : node; + } + + function getTypeOfSwitchClause(clause: ts.CaseClause | ts.DefaultClause) { + if (clause.kind === ts.SyntaxKind.CaseClause) { + return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); } + return neverType; + } - function getSwitchClauseTypes(switchStatement: ts.SwitchStatement): ts.Type[] { - const links = getNodeLinks(switchStatement); - if (!links.switchTypes) { - links.switchTypes = []; - for (const clause of switchStatement.caseBlock.clauses) { - links.switchTypes.push(getTypeOfSwitchClause(clause)); - } + function getSwitchClauseTypes(switchStatement: ts.SwitchStatement): ts.Type[] { + const links = getNodeLinks(switchStatement); + if (!links.switchTypes) { + links.switchTypes = []; + for (const clause of switchStatement.caseBlock.clauses) { + links.switchTypes.push(getTypeOfSwitchClause(clause)); } - return links.switchTypes; } + 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: ts.SwitchStatement, retainDefault: false): string[]; - function getSwitchClauseTypeOfWitnesses(switchStatement: ts.SwitchStatement, retainDefault: boolean): (string | undefined)[]; - function getSwitchClauseTypeOfWitnesses(switchStatement: ts.SwitchStatement, retainDefault: boolean): (string | undefined)[] { - const witnesses: (string | undefined)[] = []; - for (const clause of switchStatement.caseBlock.clauses) { - if (clause.kind === ts.SyntaxKind.CaseClause) { - if (ts.isStringLiteralLike(clause.expression)) { - witnesses.push(clause.expression.text); - continue; - } - return ts.emptyArray; + // Get the types from all cases in a switch on `typeof`. An + // `undefined` element denotes an explicit `default` clause. + function getSwitchClauseTypeOfWitnesses(switchStatement: ts.SwitchStatement, retainDefault: false): string[]; + function getSwitchClauseTypeOfWitnesses(switchStatement: ts.SwitchStatement, retainDefault: boolean): (string | undefined)[]; + function getSwitchClauseTypeOfWitnesses(switchStatement: ts.SwitchStatement, retainDefault: boolean): (string | undefined)[] { + const witnesses: (string | undefined)[] = []; + for (const clause of switchStatement.caseBlock.clauses) { + if (clause.kind === ts.SyntaxKind.CaseClause) { + if (ts.isStringLiteralLike(clause.expression)) { + witnesses.push(clause.expression.text); + continue; } - if (retainDefault) - witnesses.push(/*explicitDefaultStatement*/ undefined); + return ts.emptyArray; } - return witnesses; + if (retainDefault) + witnesses.push(/*explicitDefaultStatement*/ undefined); } + return witnesses; + } - function eachTypeContainedIn(source: ts.Type, types: ts.Type[]) { - return source.flags & ts.TypeFlags.Union ? !ts.forEach((source as ts.UnionType).types, t => !ts.contains(types, t)) : ts.contains(types, source); - } + function eachTypeContainedIn(source: ts.Type, types: ts.Type[]) { + return source.flags & ts.TypeFlags.Union ? !ts.forEach((source as ts.UnionType).types, t => !ts.contains(types, t)) : ts.contains(types, source); + } - function isTypeSubsetOf(source: ts.Type, target: ts.Type) { - return source === target || target.flags & ts.TypeFlags.Union && isTypeSubsetOfUnion(source, target as ts.UnionType); - } + function isTypeSubsetOf(source: ts.Type, target: ts.Type) { + return source === target || target.flags & ts.TypeFlags.Union && isTypeSubsetOfUnion(source, target as ts.UnionType); + } - function isTypeSubsetOfUnion(source: ts.Type, target: ts.UnionType) { - if (source.flags & ts.TypeFlags.Union) { - for (const t of (source as ts.UnionType).types) { - if (!containsType(target.types, t)) { - return false; - } + function isTypeSubsetOfUnion(source: ts.Type, target: ts.UnionType) { + if (source.flags & ts.TypeFlags.Union) { + for (const t of (source as ts.UnionType).types) { + if (!containsType(target.types, t)) { + return false; } - return true; - } - if (source.flags & ts.TypeFlags.EnumLiteral && getBaseTypeOfEnumLiteralType(source as ts.LiteralType) === target) { - return true; } - return containsType(target.types, source); - } - - function forEachType(type: ts.Type, f: (t: ts.Type) => T | undefined): T | undefined { - return type.flags & ts.TypeFlags.Union ? ts.forEach((type as ts.UnionType).types, f) : f(type); - } - - function someType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { - return type.flags & ts.TypeFlags.Union ? ts.some((type as ts.UnionType).types, f) : f(type); + return true; } - - function everyType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { - return type.flags & ts.TypeFlags.Union ? ts.every((type as ts.UnionType).types, f) : f(type); + if (source.flags & ts.TypeFlags.EnumLiteral && getBaseTypeOfEnumLiteralType(source as ts.LiteralType) === target) { + return true; } + return containsType(target.types, source); + } - function everyContainedType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { - return type.flags & ts.TypeFlags.UnionOrIntersection ? ts.every((type as ts.UnionOrIntersectionType).types, f) : f(type); - } + function forEachType(type: ts.Type, f: (t: ts.Type) => T | undefined): T | undefined { + return type.flags & ts.TypeFlags.Union ? ts.forEach((type as ts.UnionType).types, f) : f(type); + } - function filterType(type: ts.Type, f: (t: ts.Type) => boolean): ts.Type { - if (type.flags & ts.TypeFlags.Union) { - const types = (type as ts.UnionType).types; - const filtered = ts.filter(types, f); - if (filtered === types) { - return type; - } - const origin = (type as ts.UnionType).origin; - let newOrigin: ts.Type | undefined; - if (origin && origin.flags & ts.TypeFlags.Union) { - // If the origin type is a (denormalized) union type, filter its non-union constituents. If that ends - // up removing a smaller number of types than in the normalized constituent set (meaning some of the - // filtered types are within nested unions in the origin), then we can't construct a new origin type. - // Otherwise, if we have exactly one type left in the origin set, return that as the filtered type. - // Otherwise, construct a new filtered origin type. - const originTypes = (origin as ts.UnionType).types; - const originFiltered = ts.filter(originTypes, t => !!(t.flags & ts.TypeFlags.Union) || f(t)); - if (originTypes.length - originFiltered.length === types.length - filtered.length) { - if (originFiltered.length === 1) { - return originFiltered[0]; - } - newOrigin = createOriginUnionOrIntersectionType(ts.TypeFlags.Union, originFiltered); - } - } - return getUnionTypeFromSortedList(filtered, (type as ts.UnionType).objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin); - } - return type.flags & ts.TypeFlags.Never || f(type) ? type : neverType; - } + function someType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { + return type.flags & ts.TypeFlags.Union ? ts.some((type as ts.UnionType).types, f) : f(type); + } - function removeType(type: ts.Type, targetType: ts.Type) { - return filterType(type, t => t !== targetType); - } + function everyType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { + return type.flags & ts.TypeFlags.Union ? ts.every((type as ts.UnionType).types, f) : f(type); + } - function countTypes(type: ts.Type) { - return type.flags & ts.TypeFlags.Union ? (type as ts.UnionType).types.length : 1; - } + function everyContainedType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { + return type.flags & ts.TypeFlags.UnionOrIntersection ? ts.every((type as ts.UnionOrIntersectionType).types, f) : f(type); + } - // 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 & ts.TypeFlags.Never) { + function filterType(type: ts.Type, f: (t: ts.Type) => boolean): ts.Type { + if (type.flags & ts.TypeFlags.Union) { + const types = (type as ts.UnionType).types; + const filtered = ts.filter(types, f); + if (filtered === types) { return type; } - if (!(type.flags & ts.TypeFlags.Union)) { - return mapper(type); - } const origin = (type as ts.UnionType).origin; - const types = origin && origin.flags & ts.TypeFlags.Union ? (origin as ts.UnionType).types : (type as ts.UnionType).types; - let mappedTypes: ts.Type[] | undefined; - let changed = false; - for (const t of types) { - const mapped = t.flags & ts.TypeFlags.Union ? mapType(t, mapper, noReductions) : mapper(t); - changed ||= t !== mapped; - if (mapped) { - if (!mappedTypes) { - mappedTypes = [mapped]; - } - else { - mappedTypes.push(mapped); + let newOrigin: ts.Type | undefined; + if (origin && origin.flags & ts.TypeFlags.Union) { + // If the origin type is a (denormalized) union type, filter its non-union constituents. If that ends + // up removing a smaller number of types than in the normalized constituent set (meaning some of the + // filtered types are within nested unions in the origin), then we can't construct a new origin type. + // Otherwise, if we have exactly one type left in the origin set, return that as the filtered type. + // Otherwise, construct a new filtered origin type. + const originTypes = (origin as ts.UnionType).types; + const originFiltered = ts.filter(originTypes, t => !!(t.flags & ts.TypeFlags.Union) || f(t)); + if (originTypes.length - originFiltered.length === types.length - filtered.length) { + if (originFiltered.length === 1) { + return originFiltered[0]; } + newOrigin = createOriginUnionOrIntersectionType(ts.TypeFlags.Union, originFiltered); } } - return changed ? mappedTypes && getUnionType(mappedTypes, noReductions ? ts.UnionReduction.None : ts.UnionReduction.Literal) : type; + return getUnionTypeFromSortedList(filtered, (type as ts.UnionType).objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin); } + return type.flags & ts.TypeFlags.Never || f(type) ? type : neverType; + } - function mapTypeWithAlias(type: ts.Type, mapper: (t: ts.Type) => ts.Type, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { - return type.flags & ts.TypeFlags.Union && aliasSymbol ? - getUnionType(ts.map((type as ts.UnionType).types, mapper), ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments) : - mapType(type, mapper); - } + function removeType(type: ts.Type, targetType: ts.Type) { + return filterType(type, t => t !== targetType); + } - function extractTypesOfKind(type: ts.Type, kind: ts.TypeFlags) { - return filterType(type, t => (t.flags & kind) !== 0); - } + function countTypes(type: ts.Type) { + return type.flags & ts.TypeFlags.Union ? (type as ts.UnionType).types.length : 1; + } - // Return a new type in which occurrences of the string, number and bigint primitives and placeholder template - // literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types - // from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a - // true intersection because it is more costly and, when applied to union types, generates a large number of - // types we don't actually care about. - function replacePrimitivesWithLiterals(typeWithPrimitives: ts.Type, typeWithLiterals: ts.Type) { - if (maybeTypeOfKind(typeWithPrimitives, ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.Number | ts.TypeFlags.BigInt) && - maybeTypeOfKind(typeWithLiterals, ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BigIntLiteral)) { - return mapType(typeWithPrimitives, t => t.flags & ts.TypeFlags.String ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) : - isPatternLiteralType(t) && !maybeTypeOfKind(typeWithLiterals, ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.StringLiteral) : - t.flags & ts.TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral) : - t.flags & ts.TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral) : t); + // 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 & ts.TypeFlags.Never) { + return type; + } + if (!(type.flags & ts.TypeFlags.Union)) { + return mapper(type); + } + const origin = (type as ts.UnionType).origin; + const types = origin && origin.flags & ts.TypeFlags.Union ? (origin as ts.UnionType).types : (type as ts.UnionType).types; + let mappedTypes: ts.Type[] | undefined; + let changed = false; + for (const t of types) { + const mapped = t.flags & ts.TypeFlags.Union ? mapType(t, mapper, noReductions) : mapper(t); + changed ||= t !== mapped; + if (mapped) { + if (!mappedTypes) { + mappedTypes = [mapped]; + } + else { + mappedTypes.push(mapped); + } } - return typeWithPrimitives; } + return changed ? mappedTypes && getUnionType(mappedTypes, noReductions ? ts.UnionReduction.None : ts.UnionReduction.Literal) : type; + } - function isIncomplete(flowType: ts.FlowType) { - return flowType.flags === 0; - } + function mapTypeWithAlias(type: ts.Type, mapper: (t: ts.Type) => ts.Type, aliasSymbol: ts.Symbol | undefined, aliasTypeArguments: readonly ts.Type[] | undefined) { + return type.flags & ts.TypeFlags.Union && aliasSymbol ? + getUnionType(ts.map((type as ts.UnionType).types, mapper), ts.UnionReduction.Literal, aliasSymbol, aliasTypeArguments) : + mapType(type, mapper); + } - function getTypeFromFlowType(flowType: ts.FlowType) { - return flowType.flags === 0 ? (flowType as ts.IncompleteType).type : flowType as ts.Type; - } + function extractTypesOfKind(type: ts.Type, kind: ts.TypeFlags) { + return filterType(type, t => (t.flags & kind) !== 0); + } - function createFlowType(type: ts.Type, incomplete: boolean): ts.FlowType { - return incomplete ? { flags: 0, type: type.flags & ts.TypeFlags.Never ? silentNeverType : type } : type; - } + // Return a new type in which occurrences of the string, number and bigint primitives and placeholder template + // literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types + // from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a + // true intersection because it is more costly and, when applied to union types, generates a large number of + // types we don't actually care about. + function replacePrimitivesWithLiterals(typeWithPrimitives: ts.Type, typeWithLiterals: ts.Type) { + if (maybeTypeOfKind(typeWithPrimitives, ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.Number | ts.TypeFlags.BigInt) && + maybeTypeOfKind(typeWithLiterals, ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BigIntLiteral)) { + return mapType(typeWithPrimitives, t => t.flags & ts.TypeFlags.String ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) : + isPatternLiteralType(t) && !maybeTypeOfKind(typeWithLiterals, ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.StringLiteral) : + t.flags & ts.TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral) : + t.flags & ts.TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral) : t); + } + return typeWithPrimitives; + } - // 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): ts.EvolvingArrayType { - const result = createObjectType(ts.ObjectFlags.EvolvingArray) as ts.EvolvingArrayType; - result.elementType = elementType; - return result; - } + function isIncomplete(flowType: ts.FlowType) { + return flowType.flags === 0; + } - function getEvolvingArrayType(elementType: ts.Type): ts.EvolvingArrayType { - return evolvingArrayTypes[elementType.id] || (evolvingArrayTypes[elementType.id] = createEvolvingArrayType(elementType)); - } + function getTypeFromFlowType(flowType: ts.FlowType) { + return flowType.flags === 0 ? (flowType as ts.IncompleteType).type : flowType as ts.Type; + } - // 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: ts.EvolvingArrayType, node: ts.Expression): ts.EvolvingArrayType { - const elementType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(getContextFreeTypeOfExpression(node))); - return isTypeSubsetOf(elementType, evolvingArrayType.elementType) ? evolvingArrayType : getEvolvingArrayType(getUnionType([evolvingArrayType.elementType, elementType])); - } + function createFlowType(type: ts.Type, incomplete: boolean): ts.FlowType { + return incomplete ? { flags: 0, type: type.flags & ts.TypeFlags.Never ? silentNeverType : type } : type; + } - function createFinalArrayType(elementType: ts.Type) { - return elementType.flags & ts.TypeFlags.Never ? - autoArrayType : - createArrayType(elementType.flags & ts.TypeFlags.Union ? - getUnionType((elementType as ts.UnionType).types, ts.UnionReduction.Subtype) : - elementType); - } + // 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): ts.EvolvingArrayType { + const result = createObjectType(ts.ObjectFlags.EvolvingArray) as ts.EvolvingArrayType; + result.elementType = elementType; + return result; + } - // We perform subtype reduction upon obtaining the final array type from an evolving array type. - function getFinalArrayType(evolvingArrayType: ts.EvolvingArrayType): ts.Type { - return evolvingArrayType.finalArrayType || (evolvingArrayType.finalArrayType = createFinalArrayType(evolvingArrayType.elementType)); - } + function getEvolvingArrayType(elementType: ts.Type): ts.EvolvingArrayType { + return evolvingArrayTypes[elementType.id] || (evolvingArrayTypes[elementType.id] = createEvolvingArrayType(elementType)); + } - function finalizeEvolvingArrayType(type: ts.Type): ts.Type { - return ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray ? getFinalArrayType(type as ts.EvolvingArrayType) : type; - } + // 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: ts.EvolvingArrayType, node: ts.Expression): ts.EvolvingArrayType { + const elementType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(getContextFreeTypeOfExpression(node))); + return isTypeSubsetOf(elementType, evolvingArrayType.elementType) ? evolvingArrayType : getEvolvingArrayType(getUnionType([evolvingArrayType.elementType, elementType])); + } - function getElementTypeOfEvolvingArrayType(type: ts.Type) { - return ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray ? (type as ts.EvolvingArrayType).elementType : neverType; - } + function createFinalArrayType(elementType: ts.Type) { + return elementType.flags & ts.TypeFlags.Never ? + autoArrayType : + createArrayType(elementType.flags & ts.TypeFlags.Union ? + getUnionType((elementType as ts.UnionType).types, ts.UnionReduction.Subtype) : + elementType); + } - function isEvolvingArrayTypeList(types: ts.Type[]) { - let hasEvolvingArrayType = false; - for (const t of types) { - if (!(t.flags & ts.TypeFlags.Never)) { - if (!(ts.getObjectFlags(t) & ts.ObjectFlags.EvolvingArray)) { - return false; - } - hasEvolvingArrayType = true; + // We perform subtype reduction upon obtaining the final array type from an evolving array type. + function getFinalArrayType(evolvingArrayType: ts.EvolvingArrayType): ts.Type { + return evolvingArrayType.finalArrayType || (evolvingArrayType.finalArrayType = createFinalArrayType(evolvingArrayType.elementType)); + } + + function finalizeEvolvingArrayType(type: ts.Type): ts.Type { + return ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray ? getFinalArrayType(type as ts.EvolvingArrayType) : type; + } + + function getElementTypeOfEvolvingArrayType(type: ts.Type) { + return ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray ? (type as ts.EvolvingArrayType).elementType : neverType; + } + + function isEvolvingArrayTypeList(types: ts.Type[]) { + let hasEvolvingArrayType = false; + for (const t of types) { + if (!(t.flags & ts.TypeFlags.Never)) { + if (!(ts.getObjectFlags(t) & ts.ObjectFlags.EvolvingArray)) { + return false; } + hasEvolvingArrayType = true; } - return hasEvolvingArrayType; } + return hasEvolvingArrayType; + } - // 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: ts.Node) { - const root = getReferenceRoot(node); - const parent = root.parent; - const isLengthPushOrUnshift = ts.isPropertyAccessExpression(parent) && (parent.name.escapedText === "length" || - parent.parent.kind === ts.SyntaxKind.CallExpression - && ts.isIdentifier(parent.name) - && ts.isPushOrUnshiftIdentifier(parent.name)); - const isElementAssignment = parent.kind === ts.SyntaxKind.ElementAccessExpression && - (parent as ts.ElementAccessExpression).expression === root && - parent.parent.kind === ts.SyntaxKind.BinaryExpression && - (parent.parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken && - (parent.parent as ts.BinaryExpression).left === parent && - !ts.isAssignmentTarget(parent.parent) && - isTypeAssignableToKind(getTypeOfExpression((parent as ts.ElementAccessExpression).argumentExpression), ts.TypeFlags.NumberLike); - return isLengthPushOrUnshift || isElementAssignment; - } + // 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: ts.Node) { + const root = getReferenceRoot(node); + const parent = root.parent; + const isLengthPushOrUnshift = ts.isPropertyAccessExpression(parent) && (parent.name.escapedText === "length" || + parent.parent.kind === ts.SyntaxKind.CallExpression + && ts.isIdentifier(parent.name) + && ts.isPushOrUnshiftIdentifier(parent.name)); + const isElementAssignment = parent.kind === ts.SyntaxKind.ElementAccessExpression && + (parent as ts.ElementAccessExpression).expression === root && + parent.parent.kind === ts.SyntaxKind.BinaryExpression && + (parent.parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken && + (parent.parent as ts.BinaryExpression).left === parent && + !ts.isAssignmentTarget(parent.parent) && + isTypeAssignableToKind(getTypeOfExpression((parent as ts.ElementAccessExpression).argumentExpression), ts.TypeFlags.NumberLike); + return isLengthPushOrUnshift || isElementAssignment; + } - function isDeclarationWithExplicitTypeAnnotation(node: ts.Declaration) { - return (ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isParameter(node)) && - !!(ts.getEffectiveTypeAnnotationNode(node) || - ts.isInJSFile(node) && ts.hasInitializer(node) && node.initializer && ts.isFunctionExpressionOrArrowFunction(node.initializer) && ts.getEffectiveReturnTypeNode(node.initializer)); - } + function isDeclarationWithExplicitTypeAnnotation(node: ts.Declaration) { + return (ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isParameter(node)) && + !!(ts.getEffectiveTypeAnnotationNode(node) || + ts.isInJSFile(node) && ts.hasInitializer(node) && node.initializer && ts.isFunctionExpressionOrArrowFunction(node.initializer) && ts.getEffectiveReturnTypeNode(node.initializer)); + } - function getExplicitTypeOfSymbol(symbol: ts.Symbol, diagnostic?: ts.Diagnostic) { - if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.ValueModule)) { - return getTypeOfSymbol(symbol); + function getExplicitTypeOfSymbol(symbol: ts.Symbol, diagnostic?: ts.Diagnostic) { + if (symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Method | ts.SymbolFlags.Class | ts.SymbolFlags.ValueModule)) { + return getTypeOfSymbol(symbol); + } + if (symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) { + if (ts.getCheckFlags(symbol) & ts.CheckFlags.Mapped) { + const origin = (symbol as ts.MappedSymbol).syntheticOrigin; + if (origin && getExplicitTypeOfSymbol(origin)) { + return getTypeOfSymbol(symbol); + } } - if (symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) { - if (ts.getCheckFlags(symbol) & ts.CheckFlags.Mapped) { - const origin = (symbol as ts.MappedSymbol).syntheticOrigin; - if (origin && getExplicitTypeOfSymbol(origin)) { - return getTypeOfSymbol(symbol); - } + const declaration = symbol.valueDeclaration; + if (declaration) { + if (isDeclarationWithExplicitTypeAnnotation(declaration)) { + return getTypeOfSymbol(symbol); } - const declaration = symbol.valueDeclaration; - if (declaration) { - if (isDeclarationWithExplicitTypeAnnotation(declaration)) { - return getTypeOfSymbol(symbol); - } - if (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.SyntaxKind.ForOfStatement) { - const statement = declaration.parent.parent; - const expressionType = getTypeOfDottedName(statement.expression, /*diagnostic*/ undefined); - if (expressionType) { - const use = statement.awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; - return checkIteratedTypeOrElementType(use, expressionType, undefinedType, /*errorNode*/ undefined); - } - } - if (diagnostic) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(declaration, ts.Diagnostics._0_needs_an_explicit_type_annotation, symbolToString(symbol))); + if (ts.isVariableDeclaration(declaration) && declaration.parent.parent.kind === ts.SyntaxKind.ForOfStatement) { + const statement = declaration.parent.parent; + const expressionType = getTypeOfDottedName(statement.expression, /*diagnostic*/ undefined); + if (expressionType) { + const use = statement.awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; + return checkIteratedTypeOrElementType(use, expressionType, undefinedType, /*errorNode*/ undefined); } } + if (diagnostic) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(declaration, ts.Diagnostics._0_needs_an_explicit_type_annotation, symbolToString(symbol))); + } } } + } - // 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: ts.Expression, diagnostic: ts.Diagnostic | undefined): ts.Type | undefined { - if (!(node.flags & ts.NodeFlags.InWithStatement)) { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - const symbol = getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(node as ts.Identifier)); - return getExplicitTypeOfSymbol(symbol.flags & ts.SymbolFlags.Alias ? resolveAlias(symbol) : symbol, diagnostic); - case ts.SyntaxKind.ThisKeyword: - return getExplicitThisType(node); - case ts.SyntaxKind.SuperKeyword: - return checkSuperExpression(node); - case ts.SyntaxKind.PropertyAccessExpression: { - const type = getTypeOfDottedName((node as ts.PropertyAccessExpression).expression, diagnostic); - if (type) { - const name = (node as ts.PropertyAccessExpression).name; - let prop: ts.Symbol | undefined; - if (ts.isPrivateIdentifier(name)) { - if (!type.symbol) { - return undefined; - } - prop = getPropertyOfType(type, ts.getSymbolNameForPrivateIdentifier(type.symbol, name.escapedText)); - } - else { - prop = getPropertyOfType(type, name.escapedText); + // 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: ts.Expression, diagnostic: ts.Diagnostic | undefined): ts.Type | undefined { + if (!(node.flags & ts.NodeFlags.InWithStatement)) { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + const symbol = getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(node as ts.Identifier)); + return getExplicitTypeOfSymbol(symbol.flags & ts.SymbolFlags.Alias ? resolveAlias(symbol) : symbol, diagnostic); + case ts.SyntaxKind.ThisKeyword: + return getExplicitThisType(node); + case ts.SyntaxKind.SuperKeyword: + return checkSuperExpression(node); + case ts.SyntaxKind.PropertyAccessExpression: { + const type = getTypeOfDottedName((node as ts.PropertyAccessExpression).expression, diagnostic); + if (type) { + const name = (node as ts.PropertyAccessExpression).name; + let prop: ts.Symbol | undefined; + if (ts.isPrivateIdentifier(name)) { + if (!type.symbol) { + return undefined; } - return prop && getExplicitTypeOfSymbol(prop, diagnostic); + prop = getPropertyOfType(type, ts.getSymbolNameForPrivateIdentifier(type.symbol, name.escapedText)); } - return undefined; + else { + prop = getPropertyOfType(type, name.escapedText); + } + return prop && getExplicitTypeOfSymbol(prop, diagnostic); } - case ts.SyntaxKind.ParenthesizedExpression: - return getTypeOfDottedName((node as ts.ParenthesizedExpression).expression, diagnostic); + return undefined; } + case ts.SyntaxKind.ParenthesizedExpression: + return getTypeOfDottedName((node as ts.ParenthesizedExpression).expression, diagnostic); } } + } - function getEffectsSignature(node: ts.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 === ts.SyntaxKind.ExpressionStatement) { - funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined); - } - else if (node.expression.kind !== ts.SyntaxKind.SuperKeyword) { - if (ts.isOptionalChain(node)) { - funcType = checkNonNullType(getOptionalExpressionType(checkExpression(node.expression), node.expression), node.expression); + function getEffectsSignature(node: ts.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 === ts.SyntaxKind.ExpressionStatement) { + funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined); + } + else if (node.expression.kind !== ts.SyntaxKind.SuperKeyword) { + if (ts.isOptionalChain(node)) { + funcType = checkNonNullType(getOptionalExpressionType(checkExpression(node.expression), node.expression), node.expression); + } + else { + funcType = checkNonNullExpression(node.expression); + } + } + const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, ts.SignatureKind.Call); + const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] : + ts.some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) : + undefined; + signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature; + } + return signature === unknownSignature ? undefined : signature; + } + + function hasTypePredicateOrNeverReturnType(signature: ts.Signature) { + return !!(getTypePredicateOfSignature(signature) || + signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & ts.TypeFlags.Never); + } + + function getTypePredicateArgument(predicate: ts.TypePredicate, callExpression: ts.CallExpression) { + if (predicate.kind === ts.TypePredicateKind.Identifier || predicate.kind === ts.TypePredicateKind.AssertsIdentifier) { + return callExpression.arguments[predicate.parameterIndex]; + } + const invokedExpression = ts.skipParentheses(callExpression.expression); + return ts.isAccessExpression(invokedExpression) ? ts.skipParentheses(invokedExpression.expression) : undefined; + } + + function reportFlowControlError(node: ts.Node) { + const block = ts.findAncestor(node, ts.isFunctionOrModuleBlock) as ts.Block | ts.ModuleBlock | ts.SourceFile; + const sourceFile = ts.getSourceFileOfNode(node); + const span = ts.getSpanOfTokenAtPosition(sourceFile, block.statements.pos); + diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis)); + } + + function isReachableFlowNode(flow: ts.FlowNode) { + const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false); + lastFlowNode = flow; + lastFlowNodeReachable = result; + return result; + } + + function isFalseExpression(expr: ts.Expression): boolean { + const node = ts.skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); + return node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.BinaryExpression && ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node as ts.BinaryExpression).left) || isFalseExpression((node as ts.BinaryExpression).right)) || + (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken && isFalseExpression((node as ts.BinaryExpression).left) && isFalseExpression((node as ts.BinaryExpression).right)); + } + + function isReachableFlowNodeWorker(flow: ts.FlowNode, noCacheCheck: boolean): boolean { + while (true) { + if (flow === lastFlowNode) { + return lastFlowNodeReachable; + } + const flags = flow.flags; + if (flags & ts.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 & (ts.FlowFlags.Assignment | ts.FlowFlags.Condition | ts.FlowFlags.ArrayMutation)) { + flow = (flow as ts.FlowAssignment | ts.FlowCondition | ts.FlowArrayMutation).antecedent; + } + else if (flags & ts.FlowFlags.Call) { + const signature = getEffectsSignature((flow as ts.FlowCall).node); + if (signature) { + const predicate = getTypePredicateOfSignature(signature); + if (predicate && predicate.kind === ts.TypePredicateKind.AssertsIdentifier && !predicate.type) { + const predicateArgument = (flow as ts.FlowCall).node.arguments[predicate.parameterIndex]; + if (predicateArgument && isFalseExpression(predicateArgument)) { + return false; + } } - else { - funcType = checkNonNullExpression(node.expression); + if (getReturnTypeOfSignature(signature).flags & ts.TypeFlags.Never) { + return false; } } - const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, ts.SignatureKind.Call); - const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] : - ts.some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) : - undefined; - signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature; + flow = (flow as ts.FlowCall).antecedent; + } + else if (flags & ts.FlowFlags.BranchLabel) { + // A branching point is reachable if any branch is reachable. + return ts.some((flow as ts.FlowLabel).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); + } + else if (flags & ts.FlowFlags.LoopLabel) { + const antecedents = (flow as ts.FlowLabel).antecedents; + if (antecedents === undefined || antecedents.length === 0) { + return false; + } + // A loop is reachable if the control flow path that leads to the top is reachable. + flow = antecedents[0]; + } + else if (flags & ts.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 as ts.FlowSwitchClause).clauseStart === (flow as ts.FlowSwitchClause).clauseEnd && isExhaustiveSwitchStatement((flow as ts.FlowSwitchClause).switchStatement)) { + return false; + } + flow = (flow as ts.FlowSwitchClause).antecedent; + } + else if (flags & ts.FlowFlags.ReduceLabel) { + // Cache is unreliable once we start adjusting labels + lastFlowNode = undefined; + const target = (flow as ts.FlowReduceLabel).target; + const saveAntecedents = target.antecedents; + target.antecedents = (flow as ts.FlowReduceLabel).antecedents; + const result = isReachableFlowNodeWorker((flow as ts.FlowReduceLabel).antecedent, /*noCacheCheck*/ false); + target.antecedents = saveAntecedents; + return result; + } + else { + return !(flags & ts.FlowFlags.Unreachable); } - return signature === unknownSignature ? undefined : signature; } + } - function hasTypePredicateOrNeverReturnType(signature: ts.Signature) { - return !!(getTypePredicateOfSignature(signature) || - signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & ts.TypeFlags.Never); + // Return true if the given flow node is preceded by a 'super(...)' call in every possible code path + // leading to the node. + function isPostSuperFlowNode(flow: ts.FlowNode, noCacheCheck: boolean): boolean { + while (true) { + const flags = flow.flags; + if (flags & ts.FlowFlags.Shared) { + if (!noCacheCheck) { + const id = getFlowNodeId(flow); + const postSuper = flowNodePostSuper[id]; + return postSuper !== undefined ? postSuper : (flowNodePostSuper[id] = isPostSuperFlowNode(flow, /*noCacheCheck*/ true)); + } + noCacheCheck = false; + } + if (flags & (ts.FlowFlags.Assignment | ts.FlowFlags.Condition | ts.FlowFlags.ArrayMutation | ts.FlowFlags.SwitchClause)) { + flow = (flow as ts.FlowAssignment | ts.FlowCondition | ts.FlowArrayMutation | ts.FlowSwitchClause).antecedent; + } + else if (flags & ts.FlowFlags.Call) { + if ((flow as ts.FlowCall).node.expression.kind === ts.SyntaxKind.SuperKeyword) { + return true; + } + flow = (flow as ts.FlowCall).antecedent; + } + else if (flags & ts.FlowFlags.BranchLabel) { + // A branching point is post-super if every branch is post-super. + return ts.every((flow as ts.FlowLabel).antecedents, f => isPostSuperFlowNode(f, /*noCacheCheck*/ false)); + } + else if (flags & ts.FlowFlags.LoopLabel) { + // A loop is post-super if the control flow path that leads to the top is post-super. + flow = (flow as ts.FlowLabel).antecedents![0]; + } + else if (flags & ts.FlowFlags.ReduceLabel) { + const target = (flow as ts.FlowReduceLabel).target; + const saveAntecedents = target.antecedents; + target.antecedents = (flow as ts.FlowReduceLabel).antecedents; + const result = isPostSuperFlowNode((flow as ts.FlowReduceLabel).antecedent, /*noCacheCheck*/ false); + target.antecedents = saveAntecedents; + return result; + } + else { + // Unreachable nodes are considered post-super to silence errors + return !!(flags & ts.FlowFlags.Unreachable); + } } + } - function getTypePredicateArgument(predicate: ts.TypePredicate, callExpression: ts.CallExpression) { - if (predicate.kind === ts.TypePredicateKind.Identifier || predicate.kind === ts.TypePredicateKind.AssertsIdentifier) { - return callExpression.arguments[predicate.parameterIndex]; + function isConstantReference(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.Identifier: { + const symbol = getResolvedSymbol(node as ts.Identifier); + return isConstVariable(symbol) || ts.isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol); } - const invokedExpression = ts.skipParentheses(callExpression.expression); - return ts.isAccessExpression(invokedExpression) ? ts.skipParentheses(invokedExpression.expression) : undefined; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + // The resolvedSymbol property is initialized by checkPropertyAccess or checkElementAccess before we get here. + return isConstantReference((node as ts.AccessExpression).expression) && isReadonlySymbol(getNodeLinks(node).resolvedSymbol || unknownSymbol); } + return false; + } - function reportFlowControlError(node: ts.Node) { - const block = ts.findAncestor(node, ts.isFunctionOrModuleBlock) as ts.Block | ts.ModuleBlock | ts.SourceFile; - const sourceFile = ts.getSourceFileOfNode(node); - const span = ts.getSpanOfTokenAtPosition(sourceFile, block.statements.pos); - diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis)); + function getFlowTypeOfReference(reference: ts.Node, declaredType: ts.Type, initialType = declaredType, flowContainer?: ts.Node, flowNode = reference.flowNode) { + let key: string | undefined; + let isKeySet = false; + let flowDepth = 0; + if (flowAnalysisDisabled) { + return errorType; + } + if (!flowNode) { + return declaredType; } - - function isReachableFlowNode(flow: ts.FlowNode) { - const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false); - lastFlowNode = flow; - lastFlowNodeReachable = result; - return result; + flowInvocationCount++; + const sharedFlowStart = sharedFlowCount; + const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(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 = ts.getObjectFlags(evolvedType) & ts.ObjectFlags.EvolvingArray && isEvolvingArrayOperationTarget(reference) ? autoArrayType : finalizeEvolvingArrayType(evolvedType); + if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === ts.SyntaxKind.NonNullExpression && !(resultType.flags & ts.TypeFlags.Never) && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & ts.TypeFlags.Never) { + return declaredType; } + // The non-null unknown type should never escape control flow analysis. + return resultType === nonNullUnknownType ? unknownType : resultType; - function isFalseExpression(expr: ts.Expression): boolean { - const node = ts.skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - return node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.BinaryExpression && ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node as ts.BinaryExpression).left) || isFalseExpression((node as ts.BinaryExpression).right)) || - (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken && isFalseExpression((node as ts.BinaryExpression).left) && isFalseExpression((node as ts.BinaryExpression).right)); + function getOrSetCacheKey() { + if (isKeySet) { + return key; + } + isKeySet = true; + return key = getFlowCacheKey(reference, declaredType, initialType, flowContainer); } - function isReachableFlowNodeWorker(flow: ts.FlowNode, noCacheCheck: boolean): boolean { + function getTypeAtFlowNode(flow: ts.FlowNode): ts.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. + ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "getTypeAtFlowNode_DepthLimit", { flowId: flow.id }); + flowAnalysisDisabled = true; + reportFlowControlError(reference); + return errorType; + } + flowDepth++; + let sharedFlow: ts.FlowNode | undefined; while (true) { - if (flow === lastFlowNode) { - return lastFlowNodeReachable; - } const flags = flow.flags; if (flags & ts.FlowFlags.Shared) { - if (!noCacheCheck) { - const id = getFlowNodeId(flow); - const reachable = flowNodeReachable[id]; - return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true)); + // 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]; + } } - noCacheCheck = false; + sharedFlow = flow; } - if (flags & (ts.FlowFlags.Assignment | ts.FlowFlags.Condition | ts.FlowFlags.ArrayMutation)) { - flow = (flow as ts.FlowAssignment | ts.FlowCondition | ts.FlowArrayMutation).antecedent; + let type: ts.FlowType | undefined; + if (flags & ts.FlowFlags.Assignment) { + type = getTypeAtFlowAssignment(flow as ts.FlowAssignment); + if (!type) { + flow = (flow as ts.FlowAssignment).antecedent; + continue; + } } else if (flags & ts.FlowFlags.Call) { - const signature = getEffectsSignature((flow as ts.FlowCall).node); - if (signature) { - const predicate = getTypePredicateOfSignature(signature); - if (predicate && predicate.kind === ts.TypePredicateKind.AssertsIdentifier && !predicate.type) { - const predicateArgument = (flow as ts.FlowCall).node.arguments[predicate.parameterIndex]; - if (predicateArgument && isFalseExpression(predicateArgument)) { - return false; - } - } - if (getReturnTypeOfSignature(signature).flags & ts.TypeFlags.Never) { - return false; - } + type = getTypeAtFlowCall(flow as ts.FlowCall); + if (!type) { + flow = (flow as ts.FlowCall).antecedent; + continue; } - flow = (flow as ts.FlowCall).antecedent; } - else if (flags & ts.FlowFlags.BranchLabel) { - // A branching point is reachable if any branch is reachable. - return ts.some((flow as ts.FlowLabel).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); + else if (flags & ts.FlowFlags.Condition) { + type = getTypeAtFlowCondition(flow as ts.FlowCondition); } - else if (flags & ts.FlowFlags.LoopLabel) { - const antecedents = (flow as ts.FlowLabel).antecedents; - if (antecedents === undefined || antecedents.length === 0) { - return false; + else if (flags & ts.FlowFlags.SwitchClause) { + type = getTypeAtSwitchClause(flow as ts.FlowSwitchClause); + } + else if (flags & ts.FlowFlags.Label) { + if ((flow as ts.FlowLabel).antecedents!.length === 1) { + flow = (flow as ts.FlowLabel).antecedents![0]; + continue; } - // A loop is reachable if the control flow path that leads to the top is reachable. - flow = antecedents[0]; + type = flags & ts.FlowFlags.BranchLabel ? + getTypeAtFlowBranchLabel(flow as ts.FlowLabel) : + getTypeAtFlowLoopLabel(flow as ts.FlowLabel); } - else if (flags & ts.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 as ts.FlowSwitchClause).clauseStart === (flow as ts.FlowSwitchClause).clauseEnd && isExhaustiveSwitchStatement((flow as ts.FlowSwitchClause).switchStatement)) { - return false; + else if (flags & ts.FlowFlags.ArrayMutation) { + type = getTypeAtFlowArrayMutation(flow as ts.FlowArrayMutation); + if (!type) { + flow = (flow as ts.FlowArrayMutation).antecedent; + continue; } - flow = (flow as ts.FlowSwitchClause).antecedent; } else if (flags & ts.FlowFlags.ReduceLabel) { - // Cache is unreliable once we start adjusting labels - lastFlowNode = undefined; const target = (flow as ts.FlowReduceLabel).target; const saveAntecedents = target.antecedents; target.antecedents = (flow as ts.FlowReduceLabel).antecedents; - const result = isReachableFlowNodeWorker((flow as ts.FlowReduceLabel).antecedent, /*noCacheCheck*/ false); + type = getTypeAtFlowNode((flow as ts.FlowReduceLabel).antecedent); target.antecedents = saveAntecedents; - return result; + } + else if (flags & ts.FlowFlags.Start) { + // Check if we should continue with the control flow of the containing function. + const container = (flow as ts.FlowStart).node; + if (container && container !== flowContainer && + reference.kind !== ts.SyntaxKind.PropertyAccessExpression && + reference.kind !== ts.SyntaxKind.ElementAccessExpression && + reference.kind !== ts.SyntaxKind.ThisKeyword) { + flow = container.flowNode!; + continue; + } + // At the top of the flow we have the initial type. + type = initialType; } else { - return !(flags & ts.FlowFlags.Unreachable); + // 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 (sharedFlow) { + // Record visited node and the associated type in the cache. + sharedFlowNodes[sharedFlowCount] = sharedFlow; + sharedFlowTypes[sharedFlowCount] = type; + sharedFlowCount++; } + flowDepth--; + return type; } } - // Return true if the given flow node is preceded by a 'super(...)' call in every possible code path - // leading to the node. - function isPostSuperFlowNode(flow: ts.FlowNode, noCacheCheck: boolean): boolean { - while (true) { - const flags = flow.flags; - if (flags & ts.FlowFlags.Shared) { - if (!noCacheCheck) { - const id = getFlowNodeId(flow); - const postSuper = flowNodePostSuper[id]; - return postSuper !== undefined ? postSuper : (flowNodePostSuper[id] = isPostSuperFlowNode(flow, /*noCacheCheck*/ true)); - } - noCacheCheck = false; + function getInitialOrAssignedType(flow: ts.FlowAssignment) { + const node = flow.node; + return getNarrowableTypeForReference(node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement ? + getInitialType(node as ts.VariableDeclaration | ts.BindingElement) : + getAssignedType(node), reference); + } + + function getTypeAtFlowAssignment(flow: ts.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 (flags & (ts.FlowFlags.Assignment | ts.FlowFlags.Condition | ts.FlowFlags.ArrayMutation | ts.FlowFlags.SwitchClause)) { - flow = (flow as ts.FlowAssignment | ts.FlowCondition | ts.FlowArrayMutation | ts.FlowSwitchClause).antecedent; + if (ts.getAssignmentTargetKind(node) === ts.AssignmentKind.Compound) { + const flowType = getTypeAtFlowNode(flow.antecedent); + return createFlowType(getBaseTypeOfLiteralType(getTypeFromFlowType(flowType)), isIncomplete(flowType)); } - else if (flags & ts.FlowFlags.Call) { - if ((flow as ts.FlowCall).node.expression.kind === ts.SyntaxKind.SuperKeyword) { - return true; + if (declaredType === autoType || declaredType === autoArrayType) { + if (isEmptyArrayAssignment(node)) { + return getEvolvingArrayType(neverType); } - flow = (flow as ts.FlowCall).antecedent; - } - else if (flags & ts.FlowFlags.BranchLabel) { - // A branching point is post-super if every branch is post-super. - return ts.every((flow as ts.FlowLabel).antecedents, f => isPostSuperFlowNode(f, /*noCacheCheck*/ false)); + const assignedType = getWidenedLiteralType(getInitialOrAssignedType(flow)); + return isTypeAssignableTo(assignedType, declaredType) ? assignedType : anyArrayType; } - else if (flags & ts.FlowFlags.LoopLabel) { - // A loop is post-super if the control flow path that leads to the top is post-super. - flow = (flow as ts.FlowLabel).antecedents![0]; + if (declaredType.flags & ts.TypeFlags.Union) { + return getAssignmentReducedType(declaredType as ts.UnionType, getInitialOrAssignedType(flow)); } - else if (flags & ts.FlowFlags.ReduceLabel) { - const target = (flow as ts.FlowReduceLabel).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow as ts.FlowReduceLabel).antecedents; - const result = isPostSuperFlowNode((flow as ts.FlowReduceLabel).antecedent, /*noCacheCheck*/ false); - target.antecedents = saveAntecedents; - return result; + 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; } - else { - // Unreachable nodes are considered post-super to silence errors - return !!(flags & ts.FlowFlags.Unreachable); + // 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 (ts.isVariableDeclaration(node) && (ts.isInJSFile(node) || ts.isVarConst(node))) { + const init = ts.getDeclaredExpandoInitializer(node); + if (init && (init.kind === ts.SyntaxKind.FunctionExpression || init.kind === ts.SyntaxKind.ArrowFunction)) { + return getTypeAtFlowNode(flow.antecedent); + } } + return declaredType; } - } - - function isConstantReference(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.Identifier: { - const symbol = getResolvedSymbol(node as ts.Identifier); - return isConstVariable(symbol) || ts.isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol); - } - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - // The resolvedSymbol property is initialized by checkPropertyAccess or checkElementAccess before we get here. - return isConstantReference((node as ts.AccessExpression).expression) && isReadonlySymbol(getNodeLinks(node).resolvedSymbol || unknownSymbol); + // for (const _ in ref) acts as a nonnull on ref + if (ts.isVariableDeclaration(node) && node.parent.parent.kind === ts.SyntaxKind.ForInStatement && isMatchingReference(reference, node.parent.parent.expression)) { + return getNonNullableTypeIfNeeded(getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent))); } - return false; + // Assignment doesn't affect reference + return undefined; } - function getFlowTypeOfReference(reference: ts.Node, declaredType: ts.Type, initialType = declaredType, flowContainer?: ts.Node, flowNode = reference.flowNode) { - let key: string | undefined; - let isKeySet = false; - let flowDepth = 0; - if (flowAnalysisDisabled) { - return errorType; - } - if (!flowNode) { - return declaredType; + function narrowTypeByAssertion(type: ts.Type, expr: ts.Expression): ts.Type { + const node = ts.skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); + if (node.kind === ts.SyntaxKind.FalseKeyword) { + return unreachableNeverType; } - flowInvocationCount++; - const sharedFlowStart = sharedFlowCount; - const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(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 = ts.getObjectFlags(evolvedType) & ts.ObjectFlags.EvolvingArray && isEvolvingArrayOperationTarget(reference) ? autoArrayType : finalizeEvolvingArrayType(evolvedType); - if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === ts.SyntaxKind.NonNullExpression && !(resultType.flags & ts.TypeFlags.Never) && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & ts.TypeFlags.Never) { - return declaredType; + if (node.kind === ts.SyntaxKind.BinaryExpression) { + if ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + return narrowTypeByAssertion(narrowTypeByAssertion(type, (node as ts.BinaryExpression).left), (node as ts.BinaryExpression).right); + } + if ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken) { + return getUnionType([narrowTypeByAssertion(type, (node as ts.BinaryExpression).left), narrowTypeByAssertion(type, (node as ts.BinaryExpression).right)]); + } } - // The non-null unknown type should never escape control flow analysis. - return resultType === nonNullUnknownType ? unknownType : resultType; + return narrowType(type, node, /*assumeTrue*/ true); + } - function getOrSetCacheKey() { - if (isKeySet) { - return key; + function getTypeAtFlowCall(flow: ts.FlowCall): ts.FlowType | undefined { + const signature = getEffectsSignature(flow.node); + if (signature) { + const predicate = getTypePredicateOfSignature(signature); + if (predicate && (predicate.kind === ts.TypePredicateKind.AssertsThis || predicate.kind === ts.TypePredicateKind.AssertsIdentifier)) { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = finalizeEvolvingArrayType(getTypeFromFlowType(flowType)); + const narrowedType = predicate.type ? narrowTypeByTypePredicate(type, predicate, flow.node, /*assumeTrue*/ true) : + predicate.kind === ts.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 & ts.TypeFlags.Never) { + return unreachableNeverType; } - isKeySet = true; - return key = getFlowCacheKey(reference, declaredType, initialType, flowContainer); } + return undefined; + } - function getTypeAtFlowNode(flow: ts.FlowNode): ts.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. - ts.tracing?.instant(ts.tracing.Phase.CheckTypes, "getTypeAtFlowNode_DepthLimit", { flowId: flow.id }); - flowAnalysisDisabled = true; - reportFlowControlError(reference); - return errorType; - } - flowDepth++; - let sharedFlow: ts.FlowNode | undefined; - while (true) { - const flags = flow.flags; - if (flags & ts.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]; + function getTypeAtFlowArrayMutation(flow: ts.FlowArrayMutation): ts.FlowType | undefined { + if (declaredType === autoType || declaredType === autoArrayType) { + const node = flow.node; + const expr = node.kind === ts.SyntaxKind.CallExpression ? + (node.expression as ts.PropertyAccessExpression).expression : + (node.left as ts.ElementAccessExpression).expression; + if (isMatchingReference(reference, getReferenceCandidate(expr))) { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = getTypeFromFlowType(flowType); + if (ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray) { + let evolvedType = type as ts.EvolvingArrayType; + if (node.kind === ts.SyntaxKind.CallExpression) { + for (const arg of node.arguments) { + evolvedType = addEvolvingArrayElementType(evolvedType, arg); } } - sharedFlow = flow; - } - let type: ts.FlowType | undefined; - if (flags & ts.FlowFlags.Assignment) { - type = getTypeAtFlowAssignment(flow as ts.FlowAssignment); - if (!type) { - flow = (flow as ts.FlowAssignment).antecedent; - continue; - } - } - else if (flags & ts.FlowFlags.Call) { - type = getTypeAtFlowCall(flow as ts.FlowCall); - if (!type) { - flow = (flow as ts.FlowCall).antecedent; - continue; + 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 as ts.ElementAccessExpression).argumentExpression); + if (isTypeAssignableToKind(indexType, ts.TypeFlags.NumberLike)) { + evolvedType = addEvolvingArrayElementType(evolvedType, node.right); + } } + return evolvedType === type ? flowType : createFlowType(evolvedType, isIncomplete(flowType)); } - else if (flags & ts.FlowFlags.Condition) { - type = getTypeAtFlowCondition(flow as ts.FlowCondition); - } - else if (flags & ts.FlowFlags.SwitchClause) { - type = getTypeAtSwitchClause(flow as ts.FlowSwitchClause); + return flowType; + } + } + return undefined; + } + + function getTypeAtFlowCondition(flow: ts.FlowCondition): ts.FlowType { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = getTypeFromFlowType(flowType); + if (type.flags & ts.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 & ts.FlowFlags.TrueCondition) !== 0; + const nonEvolvingType = finalizeEvolvingArrayType(type); + const narrowedType = narrowType(nonEvolvingType, flow.node, assumeTrue); + if (narrowedType === nonEvolvingType) { + return flowType; + } + return createFlowType(narrowedType, isIncomplete(flowType)); + } + + function getTypeAtSwitchClause(flow: ts.FlowSwitchClause): ts.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 === ts.SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as ts.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 & (ts.TypeFlags.Undefined | ts.TypeFlags.Never))); } - else if (flags & ts.FlowFlags.Label) { - if ((flow as ts.FlowLabel).antecedents!.length === 1) { - flow = (flow as ts.FlowLabel).antecedents![0]; - continue; - } - type = flags & ts.FlowFlags.BranchLabel ? - getTypeAtFlowBranchLabel(flow as ts.FlowLabel) : - getTypeAtFlowLoopLabel(flow as ts.FlowLabel); + else if (expr.kind === ts.SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as ts.TypeOfExpression).expression, reference)) { + type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & ts.TypeFlags.Never || t.flags & ts.TypeFlags.StringLiteral && (t as ts.StringLiteralType).value === "undefined")); } - else if (flags & ts.FlowFlags.ArrayMutation) { - type = getTypeAtFlowArrayMutation(flow as ts.FlowArrayMutation); - if (!type) { - flow = (flow as ts.FlowArrayMutation).antecedent; - continue; - } + } + const access = getDiscriminantPropertyAccess(expr, type); + if (access) { + type = narrowTypeBySwitchOnDiscriminantProperty(type, access, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + } + } + return createFlowType(type, isIncomplete(flowType)); + } + + function getTypeAtFlowBranchLabel(flow: ts.FlowLabel): ts.FlowType { + const antecedentTypes: ts.Type[] = []; + let subtypeReduction = false; + let seenIncomplete = false; + let bypassFlow: ts.FlowSwitchClause | undefined; + for (const antecedent of flow.antecedents!) { + if (!bypassFlow && antecedent.flags & ts.FlowFlags.SwitchClause && (antecedent as ts.FlowSwitchClause).clauseStart === (antecedent as ts.FlowSwitchClause).clauseEnd) { + // The antecedent is the bypass branch of a potentially exhaustive switch statement. + bypassFlow = antecedent as ts.FlowSwitchClause; + 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; + } + ts.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 (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 (!ts.contains(antecedentTypes, type) && !isExhaustiveSwitchStatement(bypassFlow.switchStatement)) { + if (type === declaredType && declaredType === initialType) { + return type; } - else if (flags & ts.FlowFlags.ReduceLabel) { - const target = (flow as ts.FlowReduceLabel).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow as ts.FlowReduceLabel).antecedents; - type = getTypeAtFlowNode((flow as ts.FlowReduceLabel).antecedent); - target.antecedents = saveAntecedents; - } - else if (flags & ts.FlowFlags.Start) { - // Check if we should continue with the control flow of the containing function. - const container = (flow as ts.FlowStart).node; - if (container && container !== flowContainer && - reference.kind !== ts.SyntaxKind.PropertyAccessExpression && - reference.kind !== ts.SyntaxKind.ElementAccessExpression && - reference.kind !== ts.SyntaxKind.ThisKeyword) { - flow = container.flowNode!; - continue; - } - // At the top of the flow we have the initial type. - type = initialType; + antecedentTypes.push(type); + if (!isTypeSubsetOf(type, declaredType)) { + subtypeReduction = true; } - 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 (isIncomplete(flowType)) { + seenIncomplete = true; } - if (sharedFlow) { - // Record visited node and the associated type in the cache. - sharedFlowNodes[sharedFlowCount] = sharedFlow; - sharedFlowTypes[sharedFlowCount] = type; - sharedFlowCount++; + } + } + return createFlowType(getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal), seenIncomplete); + } + + function getTypeAtFlowLoopLabel(flow: ts.FlowLabel): ts.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] = new ts.Map()); + 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], ts.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: ts.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; } - flowDepth--; - return type; } + const type = getTypeFromFlowType(flowType); + ts.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; + } + } + // The result is incomplete if the first antecedent (the non-looping control flow path) + // is incomplete. + const result = getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal); + if (isIncomplete(firstAntecedentType!)) { + return createFlowType(result, /*incomplete*/ true); } + cache.set(key, result); + return result; + } - function getInitialOrAssignedType(flow: ts.FlowAssignment) { - const node = flow.node; - return getNarrowableTypeForReference(node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement ? - getInitialType(node as ts.VariableDeclaration | ts.BindingElement) : - getAssignedType(node), reference); + // 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: ts.UnionReduction) { + if (isEvolvingArrayTypeList(types)) { + return getEvolvingArrayType(getUnionType(ts.map(types, getElementTypeOfEvolvingArrayType))); + } + const result = getUnionType(ts.sameMap(types, finalizeEvolvingArrayType), subtypeReduction); + if (result !== declaredType && result.flags & declaredType.flags & ts.TypeFlags.Union && ts.arraysEqual((result as ts.UnionType).types, (declaredType as ts.UnionType).types)) { + return declaredType; } + return result; + } - function getTypeAtFlowAssignment(flow: ts.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 (ts.getAssignmentTargetKind(node) === ts.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); - } - const assignedType = getWidenedLiteralType(getInitialOrAssignedType(flow)); - return isTypeAssignableTo(assignedType, declaredType) ? assignedType : anyArrayType; - } - if (declaredType.flags & ts.TypeFlags.Union) { - return getAssignmentReducedType(declaredType as ts.UnionType, getInitialOrAssignedType(flow)); + function getCandidateDiscriminantPropertyAccess(expr: ts.Expression) { + if (ts.isBindingPattern(reference) || ts.isFunctionExpressionOrArrowFunction(reference) || ts.isObjectLiteralMethod(reference)) { + // When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in + // getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or + // parameter declared in the same parameter list is a candidate. + if (ts.isIdentifier(expr)) { + const symbol = getResolvedSymbol(expr); + const declaration = symbol.valueDeclaration; + if (declaration && (ts.isBindingElement(declaration) || ts.isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) { + return declaration; } - 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; + } + else if (ts.isAccessExpression(expr)) { + // An access expression is a candidate if the reference matches the left hand expression. + if (isMatchingReference(reference, expr.expression)) { + return expr; + } + } + else if (ts.isIdentifier(expr)) { + const symbol = getResolvedSymbol(expr); + if (isConstVariable(symbol)) { + const declaration = symbol.valueDeclaration!; + // Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind' + if (ts.isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && ts.isAccessExpression(declaration.initializer) && + isMatchingReference(reference, declaration.initializer.expression)) { + return declaration.initializer; } - // 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 (ts.isVariableDeclaration(node) && (ts.isInJSFile(node) || ts.isVarConst(node))) { - const init = ts.getDeclaredExpandoInitializer(node); - if (init && (init.kind === ts.SyntaxKind.FunctionExpression || init.kind === ts.SyntaxKind.ArrowFunction)) { - return getTypeAtFlowNode(flow.antecedent); + // Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind' + if (ts.isBindingElement(declaration) && !declaration.initializer) { + const parent = declaration.parent.parent; + if (ts.isVariableDeclaration(parent) && !parent.type && parent.initializer && (ts.isIdentifier(parent.initializer) || ts.isAccessExpression(parent.initializer)) && + isMatchingReference(reference, parent.initializer)) { + return declaration; } } - return declaredType; - } - // for (const _ in ref) acts as a nonnull on ref - if (ts.isVariableDeclaration(node) && node.parent.parent.kind === ts.SyntaxKind.ForInStatement && isMatchingReference(reference, node.parent.parent.expression)) { - return getNonNullableTypeIfNeeded(getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent))); } - // Assignment doesn't affect reference - return undefined; } + return undefined; + } - function narrowTypeByAssertion(type: ts.Type, expr: ts.Expression): ts.Type { - const node = ts.skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - if (node.kind === ts.SyntaxKind.FalseKeyword) { - return unreachableNeverType; - } - if (node.kind === ts.SyntaxKind.BinaryExpression) { - if ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { - return narrowTypeByAssertion(narrowTypeByAssertion(type, (node as ts.BinaryExpression).left), (node as ts.BinaryExpression).right); - } - if ((node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.BarBarToken) { - return getUnionType([narrowTypeByAssertion(type, (node as ts.BinaryExpression).left), narrowTypeByAssertion(type, (node as ts.BinaryExpression).right)]); + function getDiscriminantPropertyAccess(expr: ts.Expression, computedType: ts.Type) { + const type = declaredType.flags & ts.TypeFlags.Union ? declaredType : computedType; + if (type.flags & ts.TypeFlags.Union) { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name && isDiscriminantProperty(type, name)) { + return access; } } - return narrowType(type, node, /*assumeTrue*/ true); } + return undefined; + } + + function narrowTypeByDiscriminant(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, narrowType: (t: ts.Type) => ts.Type): ts.Type { + const propName = getAccessedPropertyName(access); + if (propName === undefined) { + return type; + } + const removeNullable = strictNullChecks && ts.isOptionalChain(access) && maybeTypeOfKind(type, ts.TypeFlags.Nullable); + let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); + if (!propType) { + return type; + } + propType = removeNullable ? getOptionalType(propType) : propType; + const narrowedPropType = narrowType(propType); + return filterType(type, t => { + const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); + return !(narrowedPropType.flags & ts.TypeFlags.Never) && isTypeComparableTo(narrowedPropType, discriminantType); + }); + } - function getTypeAtFlowCall(flow: ts.FlowCall): ts.FlowType | undefined { - const signature = getEffectsSignature(flow.node); - if (signature) { - const predicate = getTypePredicateOfSignature(signature); - if (predicate && (predicate.kind === ts.TypePredicateKind.AssertsThis || predicate.kind === ts.TypePredicateKind.AssertsIdentifier)) { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = finalizeEvolvingArrayType(getTypeFromFlowType(flowType)); - const narrowedType = predicate.type ? narrowTypeByTypePredicate(type, predicate, flow.node, /*assumeTrue*/ true) : - predicate.kind === ts.TypePredicateKind.AssertsIdentifier && predicate.parameterIndex >= 0 && predicate.parameterIndex < flow.node.arguments.length ? narrowTypeByAssertion(type, flow.node.arguments[predicate.parameterIndex]) : + function narrowTypeByDiscriminantProperty(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, operator: ts.SyntaxKind, value: ts.Expression, assumeTrue: boolean) { + if ((operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) && type.flags & ts.TypeFlags.Union) { + const keyPropertyName = getKeyPropertyName(type as ts.UnionType); + if (keyPropertyName && keyPropertyName === getAccessedPropertyName(access)) { + const candidate = getConstituentTypeForKeyType(type as ts.UnionType, getTypeOfExpression(value)); + if (candidate) { + return operator === (assumeTrue ? ts.SyntaxKind.EqualsEqualsEqualsToken : ts.SyntaxKind.ExclamationEqualsEqualsToken) ? candidate : + isUnitType(getTypeOfPropertyOfType(candidate, keyPropertyName) || unknownType) ? removeType(type, candidate) : type; - return narrowedType === type ? flowType : createFlowType(narrowedType, isIncomplete(flowType)); - } - if (getReturnTypeOfSignature(signature).flags & ts.TypeFlags.Never) { - return unreachableNeverType; } } - return undefined; } + return narrowTypeByDiscriminant(type, access, t => narrowTypeByEquality(t, operator, value, assumeTrue)); + } - function getTypeAtFlowArrayMutation(flow: ts.FlowArrayMutation): ts.FlowType | undefined { - if (declaredType === autoType || declaredType === autoArrayType) { - const node = flow.node; - const expr = node.kind === ts.SyntaxKind.CallExpression ? - (node.expression as ts.PropertyAccessExpression).expression : - (node.left as ts.ElementAccessExpression).expression; - if (isMatchingReference(reference, getReferenceCandidate(expr))) { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = getTypeFromFlowType(flowType); - if (ts.getObjectFlags(type) & ts.ObjectFlags.EvolvingArray) { - let evolvedType = type as ts.EvolvingArrayType; - if (node.kind === ts.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 as ts.ElementAccessExpression).argumentExpression); - if (isTypeAssignableToKind(indexType, ts.TypeFlags.NumberLike)) { - evolvedType = addEvolvingArrayElementType(evolvedType, node.right); - } - } - return evolvedType === type ? flowType : createFlowType(evolvedType, isIncomplete(flowType)); - } - return flowType; - } + function narrowTypeBySwitchOnDiscriminantProperty(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number) { + if (clauseStart < clauseEnd && type.flags & ts.TypeFlags.Union && getKeyPropertyName(type as ts.UnionType) === getAccessedPropertyName(access)) { + const clauseTypes = getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd); + const candidate = getUnionType(ts.map(clauseTypes, t => getConstituentTypeForKeyType(type as ts.UnionType, t) || unknownType)); + if (candidate !== unknownType) { + return candidate; } - return undefined; } + return narrowTypeByDiscriminant(type, access, t => narrowTypeBySwitchOnDiscriminant(t, switchStatement, clauseStart, clauseEnd)); + } - function getTypeAtFlowCondition(flow: ts.FlowCondition): ts.FlowType { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = getTypeFromFlowType(flowType); - if (type.flags & ts.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 & ts.FlowFlags.TrueCondition) !== 0; - const nonEvolvingType = finalizeEvolvingArrayType(type); - const narrowedType = narrowType(nonEvolvingType, flow.node, assumeTrue); - if (narrowedType === nonEvolvingType) { - return flowType; - } - return createFlowType(narrowedType, isIncomplete(flowType)); + function narrowTypeByTruthiness(type: ts.Type, expr: ts.Expression, assumeTrue: boolean): ts.Type { + if (isMatchingReference(reference, expr)) { + return type.flags & ts.TypeFlags.Unknown && assumeTrue ? nonNullUnknownType : + getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); + } + if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { + type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + } + const access = getDiscriminantPropertyAccess(expr, type); + if (access) { + return narrowTypeByDiscriminant(type, access, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); } + return type; + } - function getTypeAtSwitchClause(flow: ts.FlowSwitchClause): ts.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 === ts.SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as ts.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 & (ts.TypeFlags.Undefined | ts.TypeFlags.Never))); - } - else if (expr.kind === ts.SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as ts.TypeOfExpression).expression, reference)) { - type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & ts.TypeFlags.Never || t.flags & ts.TypeFlags.StringLiteral && (t as ts.StringLiteralType).value === "undefined")); - } - } - const access = getDiscriminantPropertyAccess(expr, type); - if (access) { - type = narrowTypeBySwitchOnDiscriminantProperty(type, access, flow.switchStatement, flow.clauseStart, flow.clauseEnd); - } - } - return createFlowType(type, isIncomplete(flowType)); + function isTypePresencePossible(type: ts.Type, propName: ts.__String, assumeTrue: boolean) { + const prop = getPropertyOfType(type, propName); + if (prop) { + return prop.flags & ts.SymbolFlags.Optional ? true : assumeTrue; } + return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue; + } - function getTypeAtFlowBranchLabel(flow: ts.FlowLabel): ts.FlowType { - const antecedentTypes: ts.Type[] = []; - let subtypeReduction = false; - let seenIncomplete = false; - let bypassFlow: ts.FlowSwitchClause | undefined; - for (const antecedent of flow.antecedents!) { - if (!bypassFlow && antecedent.flags & ts.FlowFlags.SwitchClause && (antecedent as ts.FlowSwitchClause).clauseStart === (antecedent as ts.FlowSwitchClause).clauseEnd) { - // The antecedent is the bypass branch of a potentially exhaustive switch statement. - bypassFlow = antecedent as ts.FlowSwitchClause; - continue; + function narrowByInKeyword(type: ts.Type, name: ts.__String, assumeTrue: boolean) { + if (type.flags & ts.TypeFlags.Union + || type.flags & ts.TypeFlags.Object && declaredType !== type + || ts.isThisTypeParameter(type) + || type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, t => t.symbol !== globalThisSymbol)) { + return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); + } + return type; + } + + function narrowTypeByBinaryExpression(type: ts.Type, expr: ts.BinaryExpression, assumeTrue: boolean): ts.Type { + switch (expr.operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + return narrowTypeByTruthiness(narrowType(type, expr.right, assumeTrue), expr.left, assumeTrue); + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + const operator = expr.operatorToken.kind; + const left = getReferenceCandidate(expr.left); + const right = getReferenceCandidate(expr.right); + if (left.kind === ts.SyntaxKind.TypeOfExpression && ts.isStringLiteralLike(right)) { + return narrowTypeByTypeof(type, left as ts.TypeOfExpression, operator, right, assumeTrue); } - 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; + if (right.kind === ts.SyntaxKind.TypeOfExpression && ts.isStringLiteralLike(left)) { + return narrowTypeByTypeof(type, right as ts.TypeOfExpression, operator, left, assumeTrue); } - ts.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 (isMatchingReference(reference, left)) { + return narrowTypeByEquality(type, operator, right, assumeTrue); } - if (isIncomplete(flowType)) { - seenIncomplete = true; + if (isMatchingReference(reference, right)) { + return narrowTypeByEquality(type, operator, left, assumeTrue); } - } - 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 (!ts.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; + if (strictNullChecks) { + if (optionalChainContainsReference(left, reference)) { + type = narrowTypeByOptionalChainContainment(type, operator, right, assumeTrue); } - } - } - return createFlowType(getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal), seenIncomplete); - } - - function getTypeAtFlowLoopLabel(flow: ts.FlowLabel): ts.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] = new ts.Map()); - 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], ts.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: ts.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; + else if (optionalChainContainsReference(right, reference)) { + type = narrowTypeByOptionalChainContainment(type, operator, left, assumeTrue); } } - const type = getTypeFromFlowType(flowType); - ts.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 leftAccess = getDiscriminantPropertyAccess(left, type); + if (leftAccess) { + return narrowTypeByDiscriminantProperty(type, leftAccess, operator, right, assumeTrue); } - // 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; + const rightAccess = getDiscriminantPropertyAccess(right, type); + if (rightAccess) { + return narrowTypeByDiscriminantProperty(type, rightAccess, operator, left, assumeTrue); } - } - // The result is incomplete if the first antecedent (the non-looping control flow path) - // is incomplete. - const result = getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? ts.UnionReduction.Subtype : ts.UnionReduction.Literal); - if (isIncomplete(firstAntecedentType!)) { - return createFlowType(result, /*incomplete*/ true); - } - cache.set(key, result); - return result; - } - - // 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: ts.UnionReduction) { - if (isEvolvingArrayTypeList(types)) { - return getEvolvingArrayType(getUnionType(ts.map(types, getElementTypeOfEvolvingArrayType))); - } - const result = getUnionType(ts.sameMap(types, finalizeEvolvingArrayType), subtypeReduction); - if (result !== declaredType && result.flags & declaredType.flags & ts.TypeFlags.Union && ts.arraysEqual((result as ts.UnionType).types, (declaredType as ts.UnionType).types)) { - return declaredType; - } - return result; - } - - function getCandidateDiscriminantPropertyAccess(expr: ts.Expression) { - if (ts.isBindingPattern(reference) || ts.isFunctionExpressionOrArrowFunction(reference) || ts.isObjectLiteralMethod(reference)) { - // When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in - // getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or - // parameter declared in the same parameter list is a candidate. - if (ts.isIdentifier(expr)) { - const symbol = getResolvedSymbol(expr); - const declaration = symbol.valueDeclaration; - if (declaration && (ts.isBindingElement(declaration) || ts.isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) { - return declaration; - } + if (isMatchingConstructorReference(left)) { + return narrowTypeByConstructor(type, operator, right, assumeTrue); } - } - else if (ts.isAccessExpression(expr)) { - // An access expression is a candidate if the reference matches the left hand expression. - if (isMatchingReference(reference, expr.expression)) { - return expr; + if (isMatchingConstructorReference(right)) { + return narrowTypeByConstructor(type, operator, left, assumeTrue); } - } - else if (ts.isIdentifier(expr)) { - const symbol = getResolvedSymbol(expr); - if (isConstVariable(symbol)) { - const declaration = symbol.valueDeclaration!; - // Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind' - if (ts.isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && ts.isAccessExpression(declaration.initializer) && - isMatchingReference(reference, declaration.initializer.expression)) { - return declaration.initializer; - } - // Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind' - if (ts.isBindingElement(declaration) && !declaration.initializer) { - const parent = declaration.parent.parent; - if (ts.isVariableDeclaration(parent) && !parent.type && parent.initializer && (ts.isIdentifier(parent.initializer) || ts.isAccessExpression(parent.initializer)) && - isMatchingReference(reference, parent.initializer)) { - return declaration; - } + break; + case ts.SyntaxKind.InstanceOfKeyword: + return narrowTypeByInstanceof(type, expr, assumeTrue); + case ts.SyntaxKind.InKeyword: + if (ts.isPrivateIdentifier(expr.left)) { + return narrowTypeByPrivateIdentifierInInExpression(type, expr, assumeTrue); + } + const target = getReferenceCandidate(expr.right); + const leftType = getTypeOfNode(expr.left); + if (leftType.flags & ts.TypeFlags.StringLiteral) { + const name = ts.escapeLeadingUnderscores((leftType as ts.StringLiteralType).value); + if (containsMissingType(type) && ts.isAccessExpression(reference) && isMatchingReference(reference.expression, target) && + getAccessedPropertyName(reference) === name) { + return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); } - } - } - return undefined; - } - - function getDiscriminantPropertyAccess(expr: ts.Expression, computedType: ts.Type) { - const type = declaredType.flags & ts.TypeFlags.Union ? declaredType : computedType; - if (type.flags & ts.TypeFlags.Union) { - const access = getCandidateDiscriminantPropertyAccess(expr); - if (access) { - const name = getAccessedPropertyName(access); - if (name && isDiscriminantProperty(type, name)) { - return access; + if (isMatchingReference(reference, target)) { + return narrowByInKeyword(type, name, assumeTrue); } } - } - return undefined; + break; + case ts.SyntaxKind.CommaToken: + return narrowType(type, expr.right, assumeTrue); + // Ordinarily we won't see && and || expressions in control flow analysis because the Binder breaks those + // expressions down to individual conditional control flows. However, we may encounter them when analyzing + // aliased conditional expressions. + case ts.SyntaxKind.AmpersandAmpersandToken: + return assumeTrue ? + narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ true) : + getUnionType([narrowType(type, expr.left, /*assumeTrue*/ false), narrowType(type, expr.right, /*assumeTrue*/ false)]); + case ts.SyntaxKind.BarBarToken: + return assumeTrue ? + getUnionType([narrowType(type, expr.left, /*assumeTrue*/ true), narrowType(type, expr.right, /*assumeTrue*/ true)]) : + narrowType(narrowType(type, expr.left, /*assumeTrue*/ false), expr.right, /*assumeTrue*/ false); } + return type; + } - function narrowTypeByDiscriminant(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, narrowType: (t: ts.Type) => ts.Type): ts.Type { - const propName = getAccessedPropertyName(access); - if (propName === undefined) { - return type; - } - const removeNullable = strictNullChecks && ts.isOptionalChain(access) && maybeTypeOfKind(type, ts.TypeFlags.Nullable); - let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); - if (!propType) { - return type; - } - propType = removeNullable ? getOptionalType(propType) : propType; - const narrowedPropType = narrowType(propType); - return filterType(type, t => { - const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); - return !(narrowedPropType.flags & ts.TypeFlags.Never) && isTypeComparableTo(narrowedPropType, discriminantType); - }); + function narrowTypeByPrivateIdentifierInInExpression(type: ts.Type, expr: ts.BinaryExpression, assumeTrue: boolean): ts.Type { + const target = getReferenceCandidate(expr.right); + if (!isMatchingReference(reference, target)) { + return type; } - function narrowTypeByDiscriminantProperty(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, operator: ts.SyntaxKind, value: ts.Expression, assumeTrue: boolean) { - if ((operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) && type.flags & ts.TypeFlags.Union) { - const keyPropertyName = getKeyPropertyName(type as ts.UnionType); - if (keyPropertyName && keyPropertyName === getAccessedPropertyName(access)) { - const candidate = getConstituentTypeForKeyType(type as ts.UnionType, getTypeOfExpression(value)); - if (candidate) { - return operator === (assumeTrue ? ts.SyntaxKind.EqualsEqualsEqualsToken : ts.SyntaxKind.ExclamationEqualsEqualsToken) ? candidate : - isUnitType(getTypeOfPropertyOfType(candidate, keyPropertyName) || unknownType) ? removeType(type, candidate) : - type; - } - } - } - return narrowTypeByDiscriminant(type, access, t => narrowTypeByEquality(t, operator, value, assumeTrue)); + ts.Debug.assertNode(expr.left, ts.isPrivateIdentifier); + const symbol = getSymbolForPrivateIdentifierExpression(expr.left); + if (symbol === undefined) { + return type; } - - function narrowTypeBySwitchOnDiscriminantProperty(type: ts.Type, access: ts.AccessExpression | ts.BindingElement | ts.ParameterDeclaration, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number) { - if (clauseStart < clauseEnd && type.flags & ts.TypeFlags.Union && getKeyPropertyName(type as ts.UnionType) === getAccessedPropertyName(access)) { - const clauseTypes = getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd); - const candidate = getUnionType(ts.map(clauseTypes, t => getConstituentTypeForKeyType(type as ts.UnionType, t) || unknownType)); - if (candidate !== unknownType) { - return candidate; - } - } - return narrowTypeByDiscriminant(type, access, t => narrowTypeBySwitchOnDiscriminant(t, switchStatement, clauseStart, clauseEnd)); + const classSymbol = symbol.parent!; + const targetType = ts.hasStaticModifier(ts.Debug.checkDefined(symbol.valueDeclaration, "should always have a declaration")) + ? getTypeOfSymbol(classSymbol) as ts.InterfaceType + : getDeclaredTypeOfSymbol(classSymbol); + return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); + } + + function narrowTypeByOptionalChainContainment(type: ts.Type, operator: ts.SyntaxKind, value: ts.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 === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsEqualsToken; + const nullableFlags = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken ? ts.TypeFlags.Nullable : ts.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 & (ts.TypeFlags.AnyOrUnknown | nullableFlags))); + return removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + } + + function narrowTypeByEquality(type: ts.Type, operator: ts.SyntaxKind, value: ts.Expression, assumeTrue: boolean): ts.Type { + if (type.flags & ts.TypeFlags.Any) { + return type; } - - function narrowTypeByTruthiness(type: ts.Type, expr: ts.Expression, assumeTrue: boolean): ts.Type { - if (isMatchingReference(reference, expr)) { - return type.flags & ts.TypeFlags.Unknown && assumeTrue ? nonNullUnknownType : - getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); - } - if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { - type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + if (operator === ts.SyntaxKind.ExclamationEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + const valueType = getTypeOfExpression(value); + if (assumeTrue && (type.flags & ts.TypeFlags.Unknown) && (operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken) && (valueType.flags & ts.TypeFlags.Null)) { + return getUnionType([nullType, undefinedType]); + } + if ((type.flags & ts.TypeFlags.Unknown) && assumeTrue && (operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken)) { + if (valueType.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.NonPrimitive)) { + return valueType; } - const access = getDiscriminantPropertyAccess(expr, type); - if (access) { - return narrowTypeByDiscriminant(type, access, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); + if (valueType.flags & ts.TypeFlags.Object) { + return nonPrimitiveType; } return type; } - - function isTypePresencePossible(type: ts.Type, propName: ts.__String, assumeTrue: boolean) { - const prop = getPropertyOfType(type, propName); - if (prop) { - return prop.flags & ts.SymbolFlags.Optional ? true : assumeTrue; + if (valueType.flags & ts.TypeFlags.Nullable) { + if (!strictNullChecks) { + return type; } - return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue; + const doubleEquals = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken; + const facts = doubleEquals ? + assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : + valueType.flags & ts.TypeFlags.Null ? + assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : + assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; + return type.flags & ts.TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts); } + if (assumeTrue) { + const filterFn: (t: ts.Type) => boolean = operator === ts.SyntaxKind.EqualsEqualsToken ? + t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType) : + t => areTypesComparable(t, valueType); + return replacePrimitivesWithLiterals(filterType(type, filterFn), valueType); + } + if (isUnitType(valueType)) { + return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); + } + return type; + } - function narrowByInKeyword(type: ts.Type, name: ts.__String, assumeTrue: boolean) { - if (type.flags & ts.TypeFlags.Union - || type.flags & ts.TypeFlags.Object && declaredType !== type - || ts.isThisTypeParameter(type) - || type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, t => t.symbol !== globalThisSymbol)) { - return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); + function narrowTypeByTypeof(type: ts.Type, typeOfExpr: ts.TypeOfExpression, operator: ts.SyntaxKind, literal: ts.LiteralExpression, assumeTrue: boolean): ts.Type { + // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands + if (operator === ts.SyntaxKind.ExclamationEqualsToken || operator === ts.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); } return type; } - - function narrowTypeByBinaryExpression(type: ts.Type, expr: ts.BinaryExpression, assumeTrue: boolean): ts.Type { - switch (expr.operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.BarBarEqualsToken: - case ts.SyntaxKind.AmpersandAmpersandEqualsToken: - case ts.SyntaxKind.QuestionQuestionEqualsToken: - return narrowTypeByTruthiness(narrowType(type, expr.right, assumeTrue), expr.left, assumeTrue); - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - const operator = expr.operatorToken.kind; - const left = getReferenceCandidate(expr.left); - const right = getReferenceCandidate(expr.right); - if (left.kind === ts.SyntaxKind.TypeOfExpression && ts.isStringLiteralLike(right)) { - return narrowTypeByTypeof(type, left as ts.TypeOfExpression, operator, right, assumeTrue); - } - if (right.kind === ts.SyntaxKind.TypeOfExpression && ts.isStringLiteralLike(left)) { - return narrowTypeByTypeof(type, right as ts.TypeOfExpression, 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); - } - } - const leftAccess = getDiscriminantPropertyAccess(left, type); - if (leftAccess) { - return narrowTypeByDiscriminantProperty(type, leftAccess, operator, right, assumeTrue); - } - const rightAccess = getDiscriminantPropertyAccess(right, type); - if (rightAccess) { - return narrowTypeByDiscriminantProperty(type, rightAccess, operator, left, assumeTrue); - } - if (isMatchingConstructorReference(left)) { - return narrowTypeByConstructor(type, operator, right, assumeTrue); - } - if (isMatchingConstructorReference(right)) { - return narrowTypeByConstructor(type, operator, left, assumeTrue); - } - break; - case ts.SyntaxKind.InstanceOfKeyword: - return narrowTypeByInstanceof(type, expr, assumeTrue); - case ts.SyntaxKind.InKeyword: - if (ts.isPrivateIdentifier(expr.left)) { - return narrowTypeByPrivateIdentifierInInExpression(type, expr, assumeTrue); - } - const target = getReferenceCandidate(expr.right); - const leftType = getTypeOfNode(expr.left); - if (leftType.flags & ts.TypeFlags.StringLiteral) { - const name = ts.escapeLeadingUnderscores((leftType as ts.StringLiteralType).value); - if (containsMissingType(type) && ts.isAccessExpression(reference) && isMatchingReference(reference.expression, target) && - getAccessedPropertyName(reference) === name) { - return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); - } - if (isMatchingReference(reference, target)) { - return narrowByInKeyword(type, name, assumeTrue); - } - } - break; - case ts.SyntaxKind.CommaToken: - return narrowType(type, expr.right, assumeTrue); - // Ordinarily we won't see && and || expressions in control flow analysis because the Binder breaks those - // expressions down to individual conditional control flows. However, we may encounter them when analyzing - // aliased conditional expressions. - case ts.SyntaxKind.AmpersandAmpersandToken: - return assumeTrue ? - narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ true) : - getUnionType([narrowType(type, expr.left, /*assumeTrue*/ false), narrowType(type, expr.right, /*assumeTrue*/ false)]); - case ts.SyntaxKind.BarBarToken: - return assumeTrue ? - getUnionType([narrowType(type, expr.left, /*assumeTrue*/ true), narrowType(type, expr.right, /*assumeTrue*/ true)]) : - narrowType(narrowType(type, expr.left, /*assumeTrue*/ false), expr.right, /*assumeTrue*/ false); - } + if (type.flags & ts.TypeFlags.Any && literal.text === "function") { return type; } + if (assumeTrue && type.flags & ts.TypeFlags.Unknown && literal.text === "object") { + // The non-null unknown type is used to track whether a previous narrowing operation has removed the null type + // from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null + // unknown type, and then narrows that to the non-primitive type. + return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]); + } + const facts = assumeTrue ? + typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : + typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; + const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text); + return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts); + } - function narrowTypeByPrivateIdentifierInInExpression(type: ts.Type, expr: ts.BinaryExpression, assumeTrue: boolean): ts.Type { - const target = getReferenceCandidate(expr.right); - if (!isMatchingReference(reference, target)) { - return type; - } + function narrowTypeBySwitchOptionalChainContainment(type: ts.Type, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: ts.Type) => boolean) { + const everyClauseChecks = clauseStart !== clauseEnd && ts.every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck); + return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + } - ts.Debug.assertNode(expr.left, ts.isPrivateIdentifier); - const symbol = getSymbolForPrivateIdentifierExpression(expr.left); - if (symbol === undefined) { - return type; - } - const classSymbol = symbol.parent!; - const targetType = ts.hasStaticModifier(ts.Debug.checkDefined(symbol.valueDeclaration, "should always have a declaration")) - ? getTypeOfSymbol(classSymbol) as ts.InterfaceType - : getDeclaredTypeOfSymbol(classSymbol); - return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); - } - - function narrowTypeByOptionalChainContainment(type: ts.Type, operator: ts.SyntaxKind, value: ts.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 === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsEqualsToken; - const nullableFlags = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken ? ts.TypeFlags.Nullable : ts.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 & (ts.TypeFlags.AnyOrUnknown | nullableFlags))); - return removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; - } - - function narrowTypeByEquality(type: ts.Type, operator: ts.SyntaxKind, value: ts.Expression, assumeTrue: boolean): ts.Type { - if (type.flags & ts.TypeFlags.Any) { - return type; - } - if (operator === ts.SyntaxKind.ExclamationEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - const valueType = getTypeOfExpression(value); - if (assumeTrue && (type.flags & ts.TypeFlags.Unknown) && (operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken) && (valueType.flags & ts.TypeFlags.Null)) { - return getUnionType([nullType, undefinedType]); - } - if ((type.flags & ts.TypeFlags.Unknown) && assumeTrue && (operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken)) { - if (valueType.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.NonPrimitive)) { - return valueType; + function narrowTypeBySwitchOnDiscriminant(type: ts.Type, switchStatement: ts.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 || ts.contains(clauseTypes, neverType); + if ((type.flags & ts.TypeFlags.Unknown) && !hasDefaultClause) { + let groundClauseTypes: ts.Type[] | undefined; + for (let i = 0; i < clauseTypes.length; i += 1) { + const t = clauseTypes[i]; + if (t.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.NonPrimitive)) { + if (groundClauseTypes !== undefined) { + groundClauseTypes.push(t); + } } - if (valueType.flags & ts.TypeFlags.Object) { - return nonPrimitiveType; + else if (t.flags & ts.TypeFlags.Object) { + if (groundClauseTypes === undefined) { + groundClauseTypes = clauseTypes.slice(0, i); + } + groundClauseTypes.push(nonPrimitiveType); } - return type; - } - if (valueType.flags & ts.TypeFlags.Nullable) { - if (!strictNullChecks) { + else { return type; } - const doubleEquals = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken; - const facts = doubleEquals ? - assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : - valueType.flags & ts.TypeFlags.Null ? - assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : - assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; - return type.flags & ts.TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts); - } - if (assumeTrue) { - const filterFn: (t: ts.Type) => boolean = operator === ts.SyntaxKind.EqualsEqualsToken ? - t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType) : - t => areTypesComparable(t, valueType); - return replacePrimitivesWithLiterals(filterType(type, filterFn), valueType); - } - if (isUnitType(valueType)) { - return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); } - return type; + return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); } - - function narrowTypeByTypeof(type: ts.Type, typeOfExpr: ts.TypeOfExpression, operator: ts.SyntaxKind, literal: ts.LiteralExpression, assumeTrue: boolean): ts.Type { - // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands - if (operator === ts.SyntaxKind.ExclamationEqualsToken || operator === ts.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); - } - return type; - } - if (type.flags & ts.TypeFlags.Any && literal.text === "function") { - return type; - } - if (assumeTrue && type.flags & ts.TypeFlags.Unknown && literal.text === "object") { - // The non-null unknown type is used to track whether a previous narrowing operation has removed the null type - // from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null - // unknown type, and then narrows that to the non-primitive type. - return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]); - } - const facts = assumeTrue ? - typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : - typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; - const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text); - return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts); + const discriminantType = getUnionType(clauseTypes); + const caseType = discriminantType.flags & ts.TypeFlags.Never ? neverType : + replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); + if (!hasDefaultClause) { + return caseType; } + const defaultType = filterType(type, t => !(isUnitLikeType(t) && ts.contains(switchTypes, getRegularTypeOfLiteralType(extractUnitType(t))))); + return caseType.flags & ts.TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); + } - function narrowTypeBySwitchOptionalChainContainment(type: ts.Type, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: ts.Type) => boolean) { - const everyClauseChecks = clauseStart !== clauseEnd && ts.every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck); - return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + function getImpliedTypeFromTypeofGuard(type: ts.Type, text: string) { + switch (text) { + case "function": + return type.flags & ts.TypeFlags.Any ? type : globalFunctionType; + case "object": + return type.flags & ts.TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type; + default: + return typeofTypesByName.get(text); } + } - function narrowTypeBySwitchOnDiscriminant(type: ts.Type, switchStatement: ts.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) { + // When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are + // super-types of the implied guard will be retained in the final type: this is because type-facts only + // filter. Instead, we would like to replace those union constituents with the more precise type implied by + // the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not + // the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to + // filtering by type-facts. + function narrowUnionMemberByTypeof(candidate: ts.Type) { + return (type: ts.Type) => { + if (isTypeSubtypeOf(type, candidate)) { return type; } - const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); - const hasDefaultClause = clauseStart === clauseEnd || ts.contains(clauseTypes, neverType); - if ((type.flags & ts.TypeFlags.Unknown) && !hasDefaultClause) { - let groundClauseTypes: ts.Type[] | undefined; - for (let i = 0; i < clauseTypes.length; i += 1) { - const t = clauseTypes[i]; - if (t.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.NonPrimitive)) { - if (groundClauseTypes !== undefined) { - groundClauseTypes.push(t); - } - } - else if (t.flags & ts.TypeFlags.Object) { - if (groundClauseTypes === undefined) { - groundClauseTypes = clauseTypes.slice(0, i); - } - groundClauseTypes.push(nonPrimitiveType); - } - else { - return type; - } - } - return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); + if (isTypeSubtypeOf(candidate, type)) { + return candidate; } - const discriminantType = getUnionType(clauseTypes); - const caseType = discriminantType.flags & ts.TypeFlags.Never ? neverType : - replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); - if (!hasDefaultClause) { - return caseType; + if (type.flags & ts.TypeFlags.Instantiable) { + const constraint = getBaseConstraintOfType(type) || anyType; + if (isTypeSubtypeOf(candidate, constraint)) { + return getIntersectionType([type, candidate]); + } } - const defaultType = filterType(type, t => !(isUnitLikeType(t) && ts.contains(switchTypes, getRegularTypeOfLiteralType(extractUnitType(t))))); - return caseType.flags & ts.TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); + return type; + }; + } + + function narrowBySwitchOnTypeOf(type: ts.Type, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number): ts.Type { + const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement, /*retainDefault*/ true); + if (!switchWitnesses.length) { + return type; + } + // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause + const defaultCaseLocation = ts.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) as string[]; + // 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) as string[]; + switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses as string[], 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. - function getImpliedTypeFromTypeofGuard(type: ts.Type, text: string) { - switch (text) { - case "function": - return type.flags & ts.TypeFlags.Any ? type : globalFunctionType; - case "object": - return type.flags & ts.TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type; - default: - return typeofTypesByName.get(text); - } + 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. + */ + const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts); + return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts); + } + + function isMatchingConstructorReference(expr: ts.Expression) { + return (ts.isPropertyAccessExpression(expr) && ts.idText(expr.name) === "constructor" || + ts.isElementAccessExpression(expr) && ts.isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor") && + isMatchingReference(reference, expr.expression); + } + + function narrowTypeByConstructor(type: ts.Type, operator: ts.SyntaxKind, identifier: ts.Expression, assumeTrue: boolean): ts.Type { + // Do not narrow when checking inequality. + if (assumeTrue ? (operator !== ts.SyntaxKind.EqualsEqualsToken && operator !== ts.SyntaxKind.EqualsEqualsEqualsToken) : (operator !== ts.SyntaxKind.ExclamationEqualsToken && operator !== ts.SyntaxKind.ExclamationEqualsEqualsToken)) { + return type; } - // When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are - // super-types of the implied guard will be retained in the final type: this is because type-facts only - // filter. Instead, we would like to replace those union constituents with the more precise type implied by - // the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not - // the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to - // filtering by type-facts. - function narrowUnionMemberByTypeof(candidate: ts.Type) { - return (type: ts.Type) => { - if (isTypeSubtypeOf(type, candidate)) { - return type; - } - if (isTypeSubtypeOf(candidate, type)) { - return candidate; - } - if (type.flags & ts.TypeFlags.Instantiable) { - const constraint = getBaseConstraintOfType(type) || anyType; - if (isTypeSubtypeOf(candidate, constraint)) { - return getIntersectionType([type, candidate]); - } - } - return type; - }; + // Get the type of the constructor identifier expression, if it is not a function then do not narrow. + const identifierType = getTypeOfExpression(identifier); + if (!isFunctionType(identifierType) && !isConstructorType(identifierType)) { + return type; } - function narrowBySwitchOnTypeOf(type: ts.Type, switchStatement: ts.SwitchStatement, clauseStart: number, clauseEnd: number): ts.Type { - const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement, /*retainDefault*/ true); - if (!switchWitnesses.length) { - return type; - } - // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause - const defaultCaseLocation = ts.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) as string[]; - // 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) as string[]; - switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses as string[], 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. - */ - const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts); - return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts); + // Get the prototype property of the type identifier so we can find out its type. + const prototypeProperty = getPropertyOfType(identifierType, "prototype" as ts.__String); + if (!prototypeProperty) { + return type; } - function isMatchingConstructorReference(expr: ts.Expression) { - return (ts.isPropertyAccessExpression(expr) && ts.idText(expr.name) === "constructor" || - ts.isElementAccessExpression(expr) && ts.isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor") && - isMatchingReference(reference, expr.expression); + // Get the type of the prototype, if it is undefined, or the global `Object` or `Function` types then do not narrow. + const prototypeType = getTypeOfSymbol(prototypeProperty); + const candidate = !isTypeAny(prototypeType) ? prototypeType : undefined; + if (!candidate || candidate === globalObjectType || candidate === globalFunctionType) { + return type; } - function narrowTypeByConstructor(type: ts.Type, operator: ts.SyntaxKind, identifier: ts.Expression, assumeTrue: boolean): ts.Type { - // Do not narrow when checking inequality. - if (assumeTrue ? (operator !== ts.SyntaxKind.EqualsEqualsToken && operator !== ts.SyntaxKind.EqualsEqualsEqualsToken) : (operator !== ts.SyntaxKind.ExclamationEqualsToken && operator !== ts.SyntaxKind.ExclamationEqualsEqualsToken)) { - return type; - } + // If the type that is being narrowed is `any` then just return the `candidate` type since every type is a subtype of `any`. + if (isTypeAny(type)) { + return candidate; + } - // Get the type of the constructor identifier expression, if it is not a function then do not narrow. - const identifierType = getTypeOfExpression(identifier); - if (!isFunctionType(identifierType) && !isConstructorType(identifierType)) { - return type; - } + // Filter out types that are not considered to be "constructed by" the `candidate` type. + return filterType(type, t => isConstructedBy(t, candidate)); - // Get the prototype property of the type identifier so we can find out its type. - const prototypeProperty = getPropertyOfType(identifierType, "prototype" as ts.__String); - if (!prototypeProperty) { - return type; + function isConstructedBy(source: ts.Type, target: ts.Type) { + // If either the source or target type are a class type then we need to check that they are the same exact type. + // This is because you may have a class `A` that defines some set of properties, and another class `B` + // that defines the same set of properties as class `A`, in that case they are structurally the same + // type, but when you do something like `instanceOfA.constructor === B` it will return false. + if (source.flags & ts.TypeFlags.Object && ts.getObjectFlags(source) & ts.ObjectFlags.Class || + target.flags & ts.TypeFlags.Object && ts.getObjectFlags(target) & ts.ObjectFlags.Class) { + return source.symbol === target.symbol; } - // Get the type of the prototype, if it is undefined, or the global `Object` or `Function` types then do not narrow. - const prototypeType = getTypeOfSymbol(prototypeProperty); - const candidate = !isTypeAny(prototypeType) ? prototypeType : undefined; - if (!candidate || candidate === globalObjectType || candidate === globalFunctionType) { - return type; - } + // For all other types just check that the `source` type is a subtype of the `target` type. + return isTypeSubtypeOf(source, target); + } + } - // If the type that is being narrowed is `any` then just return the `candidate` type since every type is a subtype of `any`. - if (isTypeAny(type)) { - return candidate; + function narrowTypeByInstanceof(type: ts.Type, expr: ts.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); } + return type; + } - // Filter out types that are not considered to be "constructed by" the `candidate` type. - return filterType(type, t => isConstructedBy(t, candidate)); - - function isConstructedBy(source: ts.Type, target: ts.Type) { - // If either the source or target type are a class type then we need to check that they are the same exact type. - // This is because you may have a class `A` that defines some set of properties, and another class `B` - // that defines the same set of properties as class `A`, in that case they are structurally the same - // type, but when you do something like `instanceOfA.constructor === B` it will return false. - if (source.flags & ts.TypeFlags.Object && ts.getObjectFlags(source) & ts.ObjectFlags.Class || - target.flags & ts.TypeFlags.Object && ts.getObjectFlags(target) & ts.ObjectFlags.Class) { - return source.symbol === target.symbol; - } - - // For all other types just check that the `source` type is a subtype of the `target` type. - return isTypeSubtypeOf(source, target); - } + // Check that right operand is a function type with a prototype property + const rightType = getTypeOfExpression(expr.right); + if (!isTypeDerivedFrom(rightType, globalFunctionType)) { + return type; } - function narrowTypeByInstanceof(type: ts.Type, expr: ts.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); - } - return type; + let targetType: ts.Type | undefined; + const prototypeProperty = getPropertyOfType(rightType, "prototype" as ts.__String); + if (prototypeProperty) { + // Target type is type of the prototype property + const prototypePropertyType = getTypeOfSymbol(prototypeProperty); + if (!isTypeAny(prototypePropertyType)) { + targetType = prototypePropertyType; } + } - // Check that right operand is a function type with a prototype property - const rightType = getTypeOfExpression(expr.right); - if (!isTypeDerivedFrom(rightType, globalFunctionType)) { - return type; - } + // Don't narrow from 'any' if the target type is exactly 'Object' or 'Function' + if (isTypeAny(type) && (targetType === globalObjectType || targetType === globalFunctionType)) { + return type; + } - let targetType: ts.Type | undefined; - const prototypeProperty = getPropertyOfType(rightType, "prototype" as ts.__String); - if (prototypeProperty) { - // Target type is type of the prototype property - const prototypePropertyType = getTypeOfSymbol(prototypeProperty); - if (!isTypeAny(prototypePropertyType)) { - targetType = prototypePropertyType; - } - } + if (!targetType) { + const constructSignatures = getSignaturesOfType(rightType, ts.SignatureKind.Construct); + targetType = constructSignatures.length ? + getUnionType(ts.map(constructSignatures, signature => getReturnTypeOfSignature(getErasedSignature(signature)))) : + emptyObjectType; + } - // Don't narrow from 'any' if the target type is exactly 'Object' or 'Function' - if (isTypeAny(type) && (targetType === globalObjectType || targetType === globalFunctionType)) { + // We can't narrow a union based off instanceof without negated types see #31576 for more info + if (!assumeTrue && rightType.flags & ts.TypeFlags.Union) { + const nonConstructorTypeInUnion = ts.find((rightType as ts.UnionType).types, (t) => !isConstructorType(t)); + if (!nonConstructorTypeInUnion) return type; - } + } - if (!targetType) { - const constructSignatures = getSignaturesOfType(rightType, ts.SignatureKind.Construct); - targetType = constructSignatures.length ? - getUnionType(ts.map(constructSignatures, signature => getReturnTypeOfSignature(getErasedSignature(signature)))) : - emptyObjectType; - } + return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); + } - // We can't narrow a union based off instanceof without negated types see #31576 for more info - if (!assumeTrue && rightType.flags & ts.TypeFlags.Union) { - const nonConstructorTypeInUnion = ts.find((rightType as ts.UnionType).types, (t) => !isConstructorType(t)); - if (!nonConstructorTypeInUnion) - return type; + 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)); + } + // 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 & ts.TypeFlags.Union) { + const assignableType = filterType(type, t => isRelated(t, candidate)); + if (!(assignableType.flags & ts.TypeFlags.Never)) { + return assignableType; } - - return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); } - 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)); + // 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: ts.CallExpression, assumeTrue: boolean): ts.Type { + if (hasMatchingArgument(callExpression, reference)) { + const signature = assumeTrue || !ts.isCallChain(callExpression) ? getEffectsSignature(callExpression) : undefined; + const predicate = signature && getTypePredicateOfSignature(signature); + if (predicate && (predicate.kind === ts.TypePredicateKind.This || predicate.kind === ts.TypePredicateKind.Identifier)) { + return narrowTypeByTypePredicate(type, predicate, callExpression, assumeTrue); } - // 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 & ts.TypeFlags.Union) { - const assignableType = filterType(type, t => isRelated(t, candidate)); - if (!(assignableType.flags & ts.TypeFlags.Never)) { - return assignableType; + } + if (containsMissingType(type) && ts.isAccessExpression(reference) && ts.isPropertyAccessExpression(callExpression.expression)) { + const callAccess = callExpression.expression; + if (isMatchingReference(reference.expression, getReferenceCandidate(callAccess.expression)) && + ts.isIdentifier(callAccess.name) && callAccess.name.escapedText === "hasOwnProperty" && callExpression.arguments.length === 1) { + const argument = callExpression.arguments[0]; + if (ts.isStringLiteralLike(argument) && getAccessedPropertyName(reference) === ts.escapeLeadingUnderscores(argument.text)) { + return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); } } - - // 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 type; + } - function narrowTypeByCallExpression(type: ts.Type, callExpression: ts.CallExpression, assumeTrue: boolean): ts.Type { - if (hasMatchingArgument(callExpression, reference)) { - const signature = assumeTrue || !ts.isCallChain(callExpression) ? getEffectsSignature(callExpression) : undefined; - const predicate = signature && getTypePredicateOfSignature(signature); - if (predicate && (predicate.kind === ts.TypePredicateKind.This || predicate.kind === ts.TypePredicateKind.Identifier)) { - return narrowTypeByTypePredicate(type, predicate, callExpression, assumeTrue); + function narrowTypeByTypePredicate(type: ts.Type, predicate: ts.TypePredicate, callExpression: ts.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); } - } - if (containsMissingType(type) && ts.isAccessExpression(reference) && ts.isPropertyAccessExpression(callExpression.expression)) { - const callAccess = callExpression.expression; - if (isMatchingReference(reference.expression, getReferenceCandidate(callAccess.expression)) && - ts.isIdentifier(callAccess.name) && callAccess.name.escapedText === "hasOwnProperty" && callExpression.arguments.length === 1) { - const argument = callExpression.arguments[0]; - if (ts.isStringLiteralLike(argument) && getAccessedPropertyName(reference) === ts.escapeLeadingUnderscores(argument.text)) { - return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); - } + if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && + !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { + type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); } - } - return type; - } - - function narrowTypeByTypePredicate(type: ts.Type, predicate: ts.TypePredicate, callExpression: ts.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); - } - if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && - !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { - type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); - } - const access = getDiscriminantPropertyAccess(predicateArgument, type); - if (access) { - return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf)); - } + const access = getDiscriminantPropertyAccess(predicateArgument, type); + if (access) { + return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf)); } } - return type; } + 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: ts.Type, expr: ts.Expression, assumeTrue: boolean): ts.Type { - // for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a` - if (ts.isExpressionOfOptionalChainRoot(expr) || - ts.isBinaryExpression(expr.parent) && expr.parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && expr.parent.left === expr) { - return narrowTypeByOptionality(type, expr, assumeTrue); - } - switch (expr.kind) { - case ts.SyntaxKind.Identifier: - // When narrowing a reference to a const variable, non-assigned parameter, or readonly property, we inline - // up to five levels of aliased conditional expressions that are themselves declared as const variables. - if (!isMatchingReference(reference, expr) && inlineLevel < 5) { - const symbol = getResolvedSymbol(expr as ts.Identifier); - if (isConstVariable(symbol)) { - const declaration = symbol.valueDeclaration; - if (declaration && ts.isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isConstantReference(reference)) { - inlineLevel++; - const result = narrowType(type, declaration.initializer, assumeTrue); - inlineLevel--; - return result; - } + // 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: ts.Expression, assumeTrue: boolean): ts.Type { + // for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a` + if (ts.isExpressionOfOptionalChainRoot(expr) || + ts.isBinaryExpression(expr.parent) && expr.parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && expr.parent.left === expr) { + return narrowTypeByOptionality(type, expr, assumeTrue); + } + switch (expr.kind) { + case ts.SyntaxKind.Identifier: + // When narrowing a reference to a const variable, non-assigned parameter, or readonly property, we inline + // up to five levels of aliased conditional expressions that are themselves declared as const variables. + if (!isMatchingReference(reference, expr) && inlineLevel < 5) { + const symbol = getResolvedSymbol(expr as ts.Identifier); + if (isConstVariable(symbol)) { + const declaration = symbol.valueDeclaration; + if (declaration && ts.isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isConstantReference(reference)) { + inlineLevel++; + const result = narrowType(type, declaration.initializer, assumeTrue); + inlineLevel--; + return result; } } - // falls through - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.SuperKeyword: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return narrowTypeByTruthiness(type, expr, assumeTrue); - case ts.SyntaxKind.CallExpression: - return narrowTypeByCallExpression(type, expr as ts.CallExpression, assumeTrue); - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.NonNullExpression: - return narrowType(type, (expr as ts.ParenthesizedExpression | ts.NonNullExpression).expression, assumeTrue); - case ts.SyntaxKind.BinaryExpression: - return narrowTypeByBinaryExpression(type, expr as ts.BinaryExpression, assumeTrue); - case ts.SyntaxKind.PrefixUnaryExpression: - if ((expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken) { - return narrowType(type, (expr as ts.PrefixUnaryExpression).operand, !assumeTrue); - } - break; - } - return type; - } - - function narrowTypeByOptionality(type: ts.Type, expr: ts.Expression, assumePresent: boolean): ts.Type { - if (isMatchingReference(reference, expr)) { - return getTypeWithFacts(type, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull); - } - const access = getDiscriminantPropertyAccess(expr, type); - if (access) { - return narrowTypeByDiscriminant(type, access, t => getTypeWithFacts(t, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull)); - } - return type; + } + // falls through + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return narrowTypeByTruthiness(type, expr, assumeTrue); + case ts.SyntaxKind.CallExpression: + return narrowTypeByCallExpression(type, expr as ts.CallExpression, assumeTrue); + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.NonNullExpression: + return narrowType(type, (expr as ts.ParenthesizedExpression | ts.NonNullExpression).expression, assumeTrue); + case ts.SyntaxKind.BinaryExpression: + return narrowTypeByBinaryExpression(type, expr as ts.BinaryExpression, assumeTrue); + case ts.SyntaxKind.PrefixUnaryExpression: + if ((expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.ExclamationToken) { + return narrowType(type, (expr as ts.PrefixUnaryExpression).operand, !assumeTrue); + } + break; } + return type; } - function getTypeOfSymbolAtLocation(symbol: ts.Symbol, location: ts.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 === ts.SyntaxKind.Identifier || location.kind === ts.SyntaxKind.PrivateIdentifier) { - if (ts.isRightSideOfQualifiedNameOrPropertyAccess(location)) { - location = location.parent; - } - if (ts.isExpressionNode(location) && (!ts.isAssignmentTarget(location) || ts.isWriteAccess(location))) { - const type = getTypeOfExpression(location as ts.Expression); - if (getExportSymbolOfValueSymbolIfExported(getNodeLinks(location).resolvedSymbol) === symbol) { - return type; - } - } + function narrowTypeByOptionality(type: ts.Type, expr: ts.Expression, assumePresent: boolean): ts.Type { + if (isMatchingReference(reference, expr)) { + return getTypeWithFacts(type, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull); } - if (ts.isDeclarationName(location) && ts.isSetAccessor(location.parent) && getAnnotatedAccessorTypeNode(location.parent)) { - return getWriteTypeOfAccessors(location.parent.symbol); + const access = getDiscriminantPropertyAccess(expr, type); + if (access) { + return narrowTypeByDiscriminant(type, access, t => getTypeWithFacts(t, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull)); } - // 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 getNonMissingTypeOfSymbol(symbol); + return type; } + } - function getControlFlowContainer(node: ts.Node): ts.Node { - return ts.findAncestor(node.parent, node => ts.isFunctionLike(node) && !ts.getImmediatelyInvokedFunctionExpression(node) || - node.kind === ts.SyntaxKind.ModuleBlock || - node.kind === ts.SyntaxKind.SourceFile || - node.kind === ts.SyntaxKind.PropertyDeclaration)!; - } + function getTypeOfSymbolAtLocation(symbol: ts.Symbol, location: ts.Node) { + symbol = symbol.exportSymbol || symbol; - // Check if a parameter or catch variable is assigned anywhere - function isSymbolAssigned(symbol: ts.Symbol) { - if (!symbol.valueDeclaration) { - return false; + // 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 === ts.SyntaxKind.Identifier || location.kind === ts.SyntaxKind.PrivateIdentifier) { + if (ts.isRightSideOfQualifiedNameOrPropertyAccess(location)) { + location = location.parent; } - const parent = ts.getRootDeclaration(symbol.valueDeclaration).parent; - const links = getNodeLinks(parent); - if (!(links.flags & ts.NodeCheckFlags.AssignmentsMarked)) { - links.flags |= ts.NodeCheckFlags.AssignmentsMarked; - if (!hasParentWithAssignmentsMarked(parent)) { - markNodeAssignments(parent); + if (ts.isExpressionNode(location) && (!ts.isAssignmentTarget(location) || ts.isWriteAccess(location))) { + const type = getTypeOfExpression(location as ts.Expression); + if (getExportSymbolOfValueSymbolIfExported(getNodeLinks(location).resolvedSymbol) === symbol) { + return type; } } - return symbol.isAssigned || false; } + if (ts.isDeclarationName(location) && ts.isSetAccessor(location.parent) && getAnnotatedAccessorTypeNode(location.parent)) { + return getWriteTypeOfAccessors(location.parent.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 getNonMissingTypeOfSymbol(symbol); + } + + function getControlFlowContainer(node: ts.Node): ts.Node { + return ts.findAncestor(node.parent, node => ts.isFunctionLike(node) && !ts.getImmediatelyInvokedFunctionExpression(node) || + node.kind === ts.SyntaxKind.ModuleBlock || + node.kind === ts.SyntaxKind.SourceFile || + node.kind === ts.SyntaxKind.PropertyDeclaration)!; + } - function hasParentWithAssignmentsMarked(node: ts.Node) { - return !!ts.findAncestor(node.parent, node => (ts.isFunctionLike(node) || ts.isCatchClause(node)) && !!(getNodeLinks(node).flags & ts.NodeCheckFlags.AssignmentsMarked)); + // Check if a parameter or catch variable is assigned anywhere + function isSymbolAssigned(symbol: ts.Symbol) { + if (!symbol.valueDeclaration) { + return false; + } + const parent = ts.getRootDeclaration(symbol.valueDeclaration).parent; + const links = getNodeLinks(parent); + if (!(links.flags & ts.NodeCheckFlags.AssignmentsMarked)) { + links.flags |= ts.NodeCheckFlags.AssignmentsMarked; + if (!hasParentWithAssignmentsMarked(parent)) { + markNodeAssignments(parent); + } } + return symbol.isAssigned || false; + } - function markNodeAssignments(node: ts.Node) { - if (node.kind === ts.SyntaxKind.Identifier) { - if (ts.isAssignmentTarget(node)) { - const symbol = getResolvedSymbol(node as ts.Identifier); - if (ts.isParameterOrCatchClauseVariable(symbol)) { - symbol.isAssigned = true; - } + function hasParentWithAssignmentsMarked(node: ts.Node) { + return !!ts.findAncestor(node.parent, node => (ts.isFunctionLike(node) || ts.isCatchClause(node)) && !!(getNodeLinks(node).flags & ts.NodeCheckFlags.AssignmentsMarked)); + } + + function markNodeAssignments(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Identifier) { + if (ts.isAssignmentTarget(node)) { + const symbol = getResolvedSymbol(node as ts.Identifier); + if (ts.isParameterOrCatchClauseVariable(symbol)) { + symbol.isAssigned = true; } } - else { - ts.forEachChild(node, markNodeAssignments); - } } + else { + ts.forEachChild(node, markNodeAssignments); + } + } + + function isConstVariable(symbol: ts.Symbol) { + return symbol.flags & ts.SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const) !== 0; + } + + /** 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: ts.VariableLikeDeclaration): ts.Type { + if (pushTypeResolution(declaration.symbol, TypeSystemPropertyName.DeclaredType)) { + const annotationIncludesUndefined = strictNullChecks && + declaration.kind === ts.SyntaxKind.Parameter && + declaration.initializer && + getFalsyFlags(declaredType) & ts.TypeFlags.Undefined && + !(getFalsyFlags(checkExpression(declaration.initializer)) & ts.TypeFlags.Undefined); + popTypeResolution(); - function isConstVariable(symbol: ts.Symbol) { - return symbol.flags & ts.SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const) !== 0; + return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType; } + else { + reportCircularityError(declaration.symbol); + return declaredType; + } + } + + function isConstraintPosition(type: ts.Type, node: ts.Node) { + const parent = node.parent; + // In an element access obj[x], we consider obj to be in a constraint position, except when obj is of + // a generic type without a nullable constraint and x is a generic type. This is because when both obj + // and x are of generic types T and K, we want the resulting type to be T[K]. + return parent.kind === ts.SyntaxKind.PropertyAccessExpression || + parent.kind === ts.SyntaxKind.QualifiedName || + parent.kind === ts.SyntaxKind.CallExpression && (parent as ts.CallExpression).expression === node || + parent.kind === ts.SyntaxKind.ElementAccessExpression && (parent as ts.ElementAccessExpression).expression === node && + !(someType(type, isGenericTypeWithoutNullableConstraint) && isGenericIndexType(getTypeOfExpression((parent as ts.ElementAccessExpression).argumentExpression))); + } + + function isGenericTypeWithUnionConstraint(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Instantiable && getBaseConstraintOrType(type).flags & (ts.TypeFlags.Nullable | ts.TypeFlags.Union)); + } + + function isGenericTypeWithoutNullableConstraint(type: ts.Type) { + return !!(type.flags & ts.TypeFlags.Instantiable && !maybeTypeOfKind(getBaseConstraintOrType(type), ts.TypeFlags.Nullable)); + } + + function hasContextualTypeWithNoGenericTypes(node: ts.Node, checkMode: CheckMode | undefined) { + // Computing the contextual type for a child of a JSX element involves resolving the type of the + // element's tag name, so we exclude that here to avoid circularities. + // If check mode has `CheckMode.RestBindingElement`, we skip binding pattern contextual types, + // as we want the type of a rest element to be generic when possible. + const contextualType = (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && + !((ts.isJsxOpeningElement(node.parent) || ts.isJsxSelfClosingElement(node.parent)) && node.parent.tagName === node) && + (checkMode && checkMode & CheckMode.RestBindingElement ? + getContextualType(node, ts.ContextFlags.SkipBindingPatterns) + : getContextualType(node)); + return contextualType && !isGenericType(contextualType); + } - /** 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: ts.VariableLikeDeclaration): ts.Type { - if (pushTypeResolution(declaration.symbol, TypeSystemPropertyName.DeclaredType)) { - const annotationIncludesUndefined = strictNullChecks && - declaration.kind === ts.SyntaxKind.Parameter && - declaration.initializer && - getFalsyFlags(declaredType) & ts.TypeFlags.Undefined && - !(getFalsyFlags(checkExpression(declaration.initializer)) & ts.TypeFlags.Undefined); - popTypeResolution(); + function getNarrowableTypeForReference(type: ts.Type, reference: ts.Node, checkMode?: CheckMode) { + // When the type of a reference is or contains an instantiable type with a union type constraint, and + // when the reference is in a constraint position (where it is known we'll obtain the apparent type) or + // has a contextual type containing no top-level instantiables (meaning constraints will determine + // assignability), we substitute constraints for all instantiables in the type of the reference to give + // control flow analysis an opportunity to narrow it further. For example, for a reference of a type + // parameter type 'T extends string | undefined' with a contextual type 'string', we substitute + // 'string | undefined' to give control flow analysis the opportunity to narrow to type 'string'. + const substituteConstraints = !(checkMode && checkMode & CheckMode.Inferential) && + someType(type, isGenericTypeWithUnionConstraint) && + (isConstraintPosition(type, reference) || hasContextualTypeWithNoGenericTypes(reference, checkMode)); + return substituteConstraints ? mapType(type, t => t.flags & ts.TypeFlags.Instantiable ? getBaseConstraintOrType(t) : t) : type; + } - return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType; + function isExportOrExportExpression(location: ts.Node) { + return !!ts.findAncestor(location, n => { + const parent = n.parent; + if (parent === undefined) { + return "quit"; } - else { - reportCircularityError(declaration.symbol); - return declaredType; + if (ts.isExportAssignment(parent)) { + return parent.expression === n && ts.isEntityNameExpression(n); } - } + if (ts.isExportSpecifier(parent)) { + return parent.name === n || parent.propertyName === n; + } + return false; + }); + } - function isConstraintPosition(type: ts.Type, node: ts.Node) { - const parent = node.parent; - // In an element access obj[x], we consider obj to be in a constraint position, except when obj is of - // a generic type without a nullable constraint and x is a generic type. This is because when both obj - // and x are of generic types T and K, we want the resulting type to be T[K]. - return parent.kind === ts.SyntaxKind.PropertyAccessExpression || - parent.kind === ts.SyntaxKind.QualifiedName || - parent.kind === ts.SyntaxKind.CallExpression && (parent as ts.CallExpression).expression === node || - parent.kind === ts.SyntaxKind.ElementAccessExpression && (parent as ts.ElementAccessExpression).expression === node && - !(someType(type, isGenericTypeWithoutNullableConstraint) && isGenericIndexType(getTypeOfExpression((parent as ts.ElementAccessExpression).argumentExpression))); - } - - function isGenericTypeWithUnionConstraint(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Instantiable && getBaseConstraintOrType(type).flags & (ts.TypeFlags.Nullable | ts.TypeFlags.Union)); - } - - function isGenericTypeWithoutNullableConstraint(type: ts.Type) { - return !!(type.flags & ts.TypeFlags.Instantiable && !maybeTypeOfKind(getBaseConstraintOrType(type), ts.TypeFlags.Nullable)); - } - - function hasContextualTypeWithNoGenericTypes(node: ts.Node, checkMode: CheckMode | undefined) { - // Computing the contextual type for a child of a JSX element involves resolving the type of the - // element's tag name, so we exclude that here to avoid circularities. - // If check mode has `CheckMode.RestBindingElement`, we skip binding pattern contextual types, - // as we want the type of a rest element to be generic when possible. - const contextualType = (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && - !((ts.isJsxOpeningElement(node.parent) || ts.isJsxSelfClosingElement(node.parent)) && node.parent.tagName === node) && - (checkMode && checkMode & CheckMode.RestBindingElement ? - getContextualType(node, ts.ContextFlags.SkipBindingPatterns) - : getContextualType(node)); - return contextualType && !isGenericType(contextualType); - } - - function getNarrowableTypeForReference(type: ts.Type, reference: ts.Node, checkMode?: CheckMode) { - // When the type of a reference is or contains an instantiable type with a union type constraint, and - // when the reference is in a constraint position (where it is known we'll obtain the apparent type) or - // has a contextual type containing no top-level instantiables (meaning constraints will determine - // assignability), we substitute constraints for all instantiables in the type of the reference to give - // control flow analysis an opportunity to narrow it further. For example, for a reference of a type - // parameter type 'T extends string | undefined' with a contextual type 'string', we substitute - // 'string | undefined' to give control flow analysis the opportunity to narrow to type 'string'. - const substituteConstraints = !(checkMode && checkMode & CheckMode.Inferential) && - someType(type, isGenericTypeWithUnionConstraint) && - (isConstraintPosition(type, reference) || hasContextualTypeWithNoGenericTypes(reference, checkMode)); - return substituteConstraints ? mapType(type, t => t.flags & ts.TypeFlags.Instantiable ? getBaseConstraintOrType(t) : t) : type; - } - - function isExportOrExportExpression(location: ts.Node) { - return !!ts.findAncestor(location, n => { - const parent = n.parent; - if (parent === undefined) { - return "quit"; - } - if (ts.isExportAssignment(parent)) { - return parent.expression === n && ts.isEntityNameExpression(n); - } - if (ts.isExportSpecifier(parent)) { - return parent.name === n || parent.propertyName === n; + function markAliasReferenced(symbol: ts.Symbol, location: ts.Node) { + if (isNonLocalAlias(symbol, /*excludes*/ ts.SymbolFlags.Value) && !isInTypeQuery(location) && !getTypeOnlyAliasDeclaration(symbol)) { + const target = resolveAlias(symbol); + if (target.flags & ts.SymbolFlags.Value) { + // An alias resolving to a const enum cannot be elided if (1) 'isolatedModules' is enabled + // (because the const enum value will not be inlined), or if (2) the alias is an export + // of a const enum declaration that will be preserved. + if (compilerOptions.isolatedModules || + ts.shouldPreserveConstEnums(compilerOptions) && isExportOrExportExpression(location) || + !isConstEnumOrConstEnumOnlyModule(target)) { + markAliasSymbolAsReferenced(symbol); } - return false; - }); - } - - function markAliasReferenced(symbol: ts.Symbol, location: ts.Node) { - if (isNonLocalAlias(symbol, /*excludes*/ ts.SymbolFlags.Value) && !isInTypeQuery(location) && !getTypeOnlyAliasDeclaration(symbol)) { - const target = resolveAlias(symbol); - if (target.flags & ts.SymbolFlags.Value) { - // An alias resolving to a const enum cannot be elided if (1) 'isolatedModules' is enabled - // (because the const enum value will not be inlined), or if (2) the alias is an export - // of a const enum declaration that will be preserved. - if (compilerOptions.isolatedModules || - ts.shouldPreserveConstEnums(compilerOptions) && isExportOrExportExpression(location) || - !isConstEnumOrConstEnumOnlyModule(target)) { - markAliasSymbolAsReferenced(symbol); - } - else { - markConstEnumAliasAsReferenced(symbol); - } + else { + markConstEnumAliasAsReferenced(symbol); } } } + } - function getNarrowedTypeOfSymbol(symbol: ts.Symbol, location: ts.Identifier) { - const declaration = symbol.valueDeclaration; - if (declaration) { - // If we have a non-rest binding element with no initializer declared as a const variable or a const-like - // parameter (a parameter for which there are no assignments in the function body), and if the parent type - // for the destructuring is a union type, one or more of the binding elements may represent discriminant - // properties, and we want the effects of conditional checks on such discriminants to affect the types of - // other binding elements from the same destructuring. Consider: - // - // type Action = - // | { kind: 'A', payload: number } - // | { kind: 'B', payload: string }; - // - // function f({ kind, payload }: Action) { - // if (kind === 'A') { - // payload.toFixed(); - // } - // if (kind === 'B') { - // payload.toUpperCase(); - // } - // } - // - // Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use - // the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference - // as if it occurred in the specified location. We then recompute the narrowed binding element type by - // destructuring from the narrowed parent type. - if (ts.isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) { - const parent = declaration.parent.parent; - if (parent.kind === ts.SyntaxKind.VariableDeclaration && ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const || parent.kind === ts.SyntaxKind.Parameter) { - const links = getNodeLinks(parent); - if (!(links.flags & ts.NodeCheckFlags.InCheckIdentifier)) { - links.flags |= ts.NodeCheckFlags.InCheckIdentifier; - const parentType = getTypeForBindingElementParent(parent, CheckMode.Normal); - links.flags &= ~ts.NodeCheckFlags.InCheckIdentifier; - if (parentType && parentType.flags & ts.TypeFlags.Union && !(parent.kind === ts.SyntaxKind.Parameter && isSymbolAssigned(symbol))) { - const pattern = declaration.parent; - const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode); - if (narrowedType.flags & ts.TypeFlags.Never) { - return neverType; - } - return getBindingElementTypeFromParentType(declaration, narrowedType); - } - } - } - } - // If we have a const-like parameter with no type annotation or initializer, and if the parameter is contextually - // typed by a signature with a single rest parameter of a union of tuple types, one or more of the parameters may - // represent discriminant tuple elements, and we want the effects of conditional checks on such discriminants to - // affect the types of other parameters in the same parameter list. Consider: - // - // type Action = [kind: 'A', payload: number] | [kind: 'B', payload: string]; - // - // const f: (...args: Action) => void = (kind, payload) => { - // if (kind === 'A') { - // payload.toFixed(); - // } - // if (kind === 'B') { - // payload.toUpperCase(); - // } - // } - // - // Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use - // the arrow function AST node for '(kind, payload) => ...' as a pseudo-reference and narrow this reference as - // if it occurred in the specified location. We then recompute the narrowed parameter type by indexing into the - // narrowed tuple type. - if (ts.isParameter(declaration) && !declaration.type && !declaration.initializer && !declaration.dotDotDotToken) { - const func = declaration.parent; - if (func.parameters.length >= 2 && isContextSensitiveFunctionOrObjectLiteralMethod(func)) { - const contextualSignature = getContextualSignature(func); - if (contextualSignature && contextualSignature.parameters.length === 1 && signatureHasRestParameter(contextualSignature)) { - const restType = getReducedApparentType(getTypeOfSymbol(contextualSignature.parameters[0])); - if (restType.flags & ts.TypeFlags.Union && everyType(restType, isTupleType) && !isSymbolAssigned(symbol)) { - const narrowedType = getFlowTypeOfReference(func, restType, restType, /*flowContainer*/ undefined, location.flowNode); - const index = func.parameters.indexOf(declaration) - (ts.getThisParameter(func) ? 1 : 0); - return getIndexedAccessType(narrowedType, getNumberLiteralType(index)); - } + function getNarrowedTypeOfSymbol(symbol: ts.Symbol, location: ts.Identifier) { + const declaration = symbol.valueDeclaration; + if (declaration) { + // If we have a non-rest binding element with no initializer declared as a const variable or a const-like + // parameter (a parameter for which there are no assignments in the function body), and if the parent type + // for the destructuring is a union type, one or more of the binding elements may represent discriminant + // properties, and we want the effects of conditional checks on such discriminants to affect the types of + // other binding elements from the same destructuring. Consider: + // + // type Action = + // | { kind: 'A', payload: number } + // | { kind: 'B', payload: string }; + // + // function f({ kind, payload }: Action) { + // if (kind === 'A') { + // payload.toFixed(); + // } + // if (kind === 'B') { + // payload.toUpperCase(); + // } + // } + // + // Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use + // the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference + // as if it occurred in the specified location. We then recompute the narrowed binding element type by + // destructuring from the narrowed parent type. + if (ts.isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) { + const parent = declaration.parent.parent; + if (parent.kind === ts.SyntaxKind.VariableDeclaration && ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const || parent.kind === ts.SyntaxKind.Parameter) { + const links = getNodeLinks(parent); + if (!(links.flags & ts.NodeCheckFlags.InCheckIdentifier)) { + links.flags |= ts.NodeCheckFlags.InCheckIdentifier; + const parentType = getTypeForBindingElementParent(parent, CheckMode.Normal); + links.flags &= ~ts.NodeCheckFlags.InCheckIdentifier; + if (parentType && parentType.flags & ts.TypeFlags.Union && !(parent.kind === ts.SyntaxKind.Parameter && isSymbolAssigned(symbol))) { + const pattern = declaration.parent; + const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode); + if (narrowedType.flags & ts.TypeFlags.Never) { + return neverType; + } + return getBindingElementTypeFromParentType(declaration, narrowedType); + } + } + } + } + // If we have a const-like parameter with no type annotation or initializer, and if the parameter is contextually + // typed by a signature with a single rest parameter of a union of tuple types, one or more of the parameters may + // represent discriminant tuple elements, and we want the effects of conditional checks on such discriminants to + // affect the types of other parameters in the same parameter list. Consider: + // + // type Action = [kind: 'A', payload: number] | [kind: 'B', payload: string]; + // + // const f: (...args: Action) => void = (kind, payload) => { + // if (kind === 'A') { + // payload.toFixed(); + // } + // if (kind === 'B') { + // payload.toUpperCase(); + // } + // } + // + // Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use + // the arrow function AST node for '(kind, payload) => ...' as a pseudo-reference and narrow this reference as + // if it occurred in the specified location. We then recompute the narrowed parameter type by indexing into the + // narrowed tuple type. + if (ts.isParameter(declaration) && !declaration.type && !declaration.initializer && !declaration.dotDotDotToken) { + const func = declaration.parent; + if (func.parameters.length >= 2 && isContextSensitiveFunctionOrObjectLiteralMethod(func)) { + const contextualSignature = getContextualSignature(func); + if (contextualSignature && contextualSignature.parameters.length === 1 && signatureHasRestParameter(contextualSignature)) { + const restType = getReducedApparentType(getTypeOfSymbol(contextualSignature.parameters[0])); + if (restType.flags & ts.TypeFlags.Union && everyType(restType, isTupleType) && !isSymbolAssigned(symbol)) { + const narrowedType = getFlowTypeOfReference(func, restType, restType, /*flowContainer*/ undefined, location.flowNode); + const index = func.parameters.indexOf(declaration) - (ts.getThisParameter(func) ? 1 : 0); + return getIndexedAccessType(narrowedType, getNumberLiteralType(index)); } } } } - return getTypeOfSymbol(symbol); } + return getTypeOfSymbol(symbol); + } - function checkIdentifier(node: ts.Identifier, checkMode: CheckMode | undefined): ts.Type { - if (ts.isThisInTypeQuery(node)) { - return checkThisExpression(node); - } + function checkIdentifier(node: ts.Identifier, checkMode: CheckMode | undefined): ts.Type { + if (ts.isThisInTypeQuery(node)) { + return checkThisExpression(node); + } + + const symbol = getResolvedSymbol(node); + if (symbol === unknownSymbol) { + return errorType; + } - const symbol = getResolvedSymbol(node); - if (symbol === unknownSymbol) { + // 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) { + if (isInPropertyInitializerOrClassStaticBlock(node)) { + error(node, ts.Diagnostics.arguments_cannot_be_referenced_in_property_initializers); 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) { - if (isInPropertyInitializerOrClassStaticBlock(node)) { - error(node, ts.Diagnostics.arguments_cannot_be_referenced_in_property_initializers); - return errorType; + const container = ts.getContainingFunction(node)!; + if (languageVersion < ts.ScriptTarget.ES2015) { + if (container.kind === ts.SyntaxKind.ArrowFunction) { + error(node, ts.Diagnostics.The_arguments_object_cannot_be_referenced_in_an_arrow_function_in_ES3_and_ES5_Consider_using_a_standard_function_expression); } - - const container = ts.getContainingFunction(node)!; - if (languageVersion < ts.ScriptTarget.ES2015) { - if (container.kind === ts.SyntaxKind.ArrowFunction) { - error(node, ts.Diagnostics.The_arguments_object_cannot_be_referenced_in_an_arrow_function_in_ES3_and_ES5_Consider_using_a_standard_function_expression); - } - else if (ts.hasSyntacticModifier(container, ts.ModifierFlags.Async)) { - error(node, ts.Diagnostics.The_arguments_object_cannot_be_referenced_in_an_async_function_or_method_in_ES3_and_ES5_Consider_using_a_standard_function_or_method); - } + else if (ts.hasSyntacticModifier(container, ts.ModifierFlags.Async)) { + error(node, ts.Diagnostics.The_arguments_object_cannot_be_referenced_in_an_async_function_or_method_in_ES3_and_ES5_Consider_using_a_standard_function_or_method); } - - getNodeLinks(container).flags |= ts.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 && ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) { - markAliasReferenced(symbol, node); - } + getNodeLinks(container).flags |= ts.NodeCheckFlags.CaptureArguments; + return getTypeOfSymbol(symbol); + } - const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol); - const targetSymbol = checkDeprecatedAliasedSymbol(localOrExportSymbol, node); - if (isDeprecatedSymbol(targetSymbol) && isUncalledFunctionReference(node, targetSymbol) && targetSymbol.declarations) { - addDeprecatedSuggestion(node, targetSymbol.declarations, node.escapedText as string); - } + // 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 && ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) { + markAliasReferenced(symbol, node); + } - let declaration = localOrExportSymbol.valueDeclaration; - if (declaration && localOrExportSymbol.flags & ts.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 === ts.SyntaxKind.ClassDeclaration - && ts.nodeIsDecorated(declaration as ts.ClassDeclaration)) { - let container = ts.getContainingClass(node); - while (container !== undefined) { - if (container === declaration && container.name !== node) { - getNodeLinks(declaration).flags |= ts.NodeCheckFlags.ClassWithConstructorReference; - getNodeLinks(node).flags |= ts.NodeCheckFlags.ConstructorReferenceInClass; - break; - } + const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol); + const targetSymbol = checkDeprecatedAliasedSymbol(localOrExportSymbol, node); + if (isDeprecatedSymbol(targetSymbol) && isUncalledFunctionReference(node, targetSymbol) && targetSymbol.declarations) { + addDeprecatedSuggestion(node, targetSymbol.declarations, node.escapedText as string); + } - container = ts.getContainingClass(container); + let declaration = localOrExportSymbol.valueDeclaration; + if (declaration && localOrExportSymbol.flags & ts.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 === ts.SyntaxKind.ClassDeclaration + && ts.nodeIsDecorated(declaration as ts.ClassDeclaration)) { + let container = ts.getContainingClass(node); + while (container !== undefined) { + if (container === declaration && container.name !== node) { + getNodeLinks(declaration).flags |= ts.NodeCheckFlags.ClassWithConstructorReference; + getNodeLinks(node).flags |= ts.NodeCheckFlags.ConstructorReferenceInClass; + break; } + + container = ts.getContainingClass(container); } - else if (declaration.kind === ts.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 = ts.getThisContainer(node, /*includeArrowFunctions*/ false); - while (container.kind !== ts.SyntaxKind.SourceFile) { - if (container.parent === declaration) { - if (ts.isPropertyDeclaration(container) && ts.isStatic(container) || ts.isClassStaticBlockDeclaration(container)) { - getNodeLinks(declaration).flags |= ts.NodeCheckFlags.ClassWithConstructorReference; - getNodeLinks(node).flags |= ts.NodeCheckFlags.ConstructorReferenceInClass; - } - break; + } + else if (declaration.kind === ts.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 = ts.getThisContainer(node, /*includeArrowFunctions*/ false); + while (container.kind !== ts.SyntaxKind.SourceFile) { + if (container.parent === declaration) { + if (ts.isPropertyDeclaration(container) && ts.isStatic(container) || ts.isClassStaticBlockDeclaration(container)) { + getNodeLinks(declaration).flags |= ts.NodeCheckFlags.ClassWithConstructorReference; + getNodeLinks(node).flags |= ts.NodeCheckFlags.ConstructorReferenceInClass; } - - container = ts.getThisContainer(container, /*includeArrowFunctions*/ false); + break; } + + container = ts.getThisContainer(container, /*includeArrowFunctions*/ false); } } + } - checkNestedBlockScopedBinding(node, symbol); + checkNestedBlockScopedBinding(node, symbol); - let type = getNarrowedTypeOfSymbol(localOrExportSymbol, node); - const assignmentKind = ts.getAssignmentTargetKind(node); + let type = getNarrowedTypeOfSymbol(localOrExportSymbol, node); + const assignmentKind = ts.getAssignmentTargetKind(node); - if (assignmentKind) { - if (!(localOrExportSymbol.flags & ts.SymbolFlags.Variable) && - !(ts.isInJSFile(node) && localOrExportSymbol.flags & ts.SymbolFlags.ValueModule)) { - const assignmentError = localOrExportSymbol.flags & ts.SymbolFlags.Enum ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_an_enum - : localOrExportSymbol.flags & ts.SymbolFlags.Class ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_class - : localOrExportSymbol.flags & ts.SymbolFlags.Module ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_namespace - : localOrExportSymbol.flags & ts.SymbolFlags.Function ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_function - : localOrExportSymbol.flags & ts.SymbolFlags.Alias ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_an_import - : ts.Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable; + if (assignmentKind) { + if (!(localOrExportSymbol.flags & ts.SymbolFlags.Variable) && + !(ts.isInJSFile(node) && localOrExportSymbol.flags & ts.SymbolFlags.ValueModule)) { + const assignmentError = localOrExportSymbol.flags & ts.SymbolFlags.Enum ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_an_enum + : localOrExportSymbol.flags & ts.SymbolFlags.Class ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_class + : localOrExportSymbol.flags & ts.SymbolFlags.Module ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_namespace + : localOrExportSymbol.flags & ts.SymbolFlags.Function ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_function + : localOrExportSymbol.flags & ts.SymbolFlags.Alias ? ts.Diagnostics.Cannot_assign_to_0_because_it_is_an_import + : ts.Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable; - error(node, assignmentError, symbolToString(symbol)); - return errorType; + error(node, assignmentError, symbolToString(symbol)); + return errorType; + } + if (isReadonlySymbol(localOrExportSymbol)) { + if (localOrExportSymbol.flags & ts.SymbolFlags.Variable) { + error(node, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_constant, symbolToString(symbol)); } - if (isReadonlySymbol(localOrExportSymbol)) { - if (localOrExportSymbol.flags & ts.SymbolFlags.Variable) { - error(node, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_constant, symbolToString(symbol)); - } - else { - error(node, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(symbol)); - } - return errorType; + else { + error(node, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(symbol)); } + return errorType; } + } - const isAlias = localOrExportSymbol.flags & ts.SymbolFlags.Alias; + const isAlias = localOrExportSymbol.flags & ts.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 & ts.SymbolFlags.Variable) { - if (assignmentKind === ts.AssignmentKind.Definite) { - return type; - } - } - else if (isAlias) { - declaration = getDeclarationOfAliasSymbol(symbol); - } - else { + // 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 & ts.SymbolFlags.Variable) { + if (assignmentKind === ts.AssignmentKind.Definite) { return type; } + } + else if (isAlias) { + declaration = getDeclarationOfAliasSymbol(symbol); + } + else { + return type; + } - if (!declaration) { - return type; - } + if (!declaration) { + return type; + } - type = getNarrowableTypeForReference(type, node, checkMode); - - // 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 = ts.getRootDeclaration(declaration).kind === ts.SyntaxKind.Parameter; - const declarationContainer = getControlFlowContainer(declaration); - let flowContainer = getControlFlowContainer(node); - const isOuterVariable = flowContainer !== declarationContainer; - const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && ts.isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent); - const isModuleExports = symbol.flags & ts.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 === ts.SyntaxKind.FunctionExpression || - flowContainer.kind === ts.SyntaxKind.ArrowFunction || ts.isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(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 || ts.isBindingElement(declaration) || - type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Void)) !== 0 || - isInTypeQuery(node) || node.parent.kind === ts.SyntaxKind.ExportSpecifier) || - node.parent.kind === ts.SyntaxKind.NonNullExpression || - declaration.kind === ts.SyntaxKind.VariableDeclaration && (declaration as ts.VariableDeclaration).exclamationToken || - declaration.flags & ts.NodeFlags.Ambient; - const initialType = assumeInitialized ? (isParameter ? removeOptionalityFromDeclaredType(type, declaration as ts.VariableLikeDeclaration) : type) : - type === autoType || type === autoArrayType ? undefinedType : - getOptionalType(type); - const flowType = getFlowTypeOfReference(node, type, initialType, flowContainer); - // 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(ts.getNameOfDeclaration(declaration), ts.Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined, symbolToString(symbol), typeToString(flowType)); - error(node, ts.Diagnostics.Variable_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); - } - return convertAutoToAny(flowType); - } - } - else if (!assumeInitialized && !(getFalsyFlags(type) & ts.TypeFlags.Undefined) && getFalsyFlags(flowType) & ts.TypeFlags.Undefined) { - error(node, ts.Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol)); - // Return the declared type to reduce follow-on errors - return type; + type = getNarrowableTypeForReference(type, node, checkMode); + + // 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 = ts.getRootDeclaration(declaration).kind === ts.SyntaxKind.Parameter; + const declarationContainer = getControlFlowContainer(declaration); + let flowContainer = getControlFlowContainer(node); + const isOuterVariable = flowContainer !== declarationContainer; + const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && ts.isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent); + const isModuleExports = symbol.flags & ts.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 === ts.SyntaxKind.FunctionExpression || + flowContainer.kind === ts.SyntaxKind.ArrowFunction || ts.isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && + (isConstVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(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 || ts.isBindingElement(declaration) || + type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Void)) !== 0 || + isInTypeQuery(node) || node.parent.kind === ts.SyntaxKind.ExportSpecifier) || + node.parent.kind === ts.SyntaxKind.NonNullExpression || + declaration.kind === ts.SyntaxKind.VariableDeclaration && (declaration as ts.VariableDeclaration).exclamationToken || + declaration.flags & ts.NodeFlags.Ambient; + const initialType = assumeInitialized ? (isParameter ? removeOptionalityFromDeclaredType(type, declaration as ts.VariableLikeDeclaration) : type) : + type === autoType || type === autoArrayType ? undefinedType : + getOptionalType(type); + const flowType = getFlowTypeOfReference(node, type, initialType, flowContainer); + // 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(ts.getNameOfDeclaration(declaration), ts.Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined, symbolToString(symbol), typeToString(flowType)); + error(node, ts.Diagnostics.Variable_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); + } + return convertAutoToAny(flowType); } - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; } - - function isInsideFunctionOrInstancePropertyInitializer(node: ts.Node, threshold: ts.Node): boolean { - return !!ts.findAncestor(node, n => n === threshold ? "quit" : ts.isFunctionLike(n) || (n.parent && ts.isPropertyDeclaration(n.parent) && !ts.hasStaticModifier(n.parent) && n.parent.initializer === n)); + else if (!assumeInitialized && !(getFalsyFlags(type) & ts.TypeFlags.Undefined) && getFalsyFlags(flowType) & ts.TypeFlags.Undefined) { + error(node, ts.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 getPartOfForStatementContainingNode(node: ts.Node, container: ts.ForStatement) { - return ts.findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement); - } + function isInsideFunctionOrInstancePropertyInitializer(node: ts.Node, threshold: ts.Node): boolean { + return !!ts.findAncestor(node, n => n === threshold ? "quit" : ts.isFunctionLike(n) || (n.parent && ts.isPropertyDeclaration(n.parent) && !ts.hasStaticModifier(n.parent) && n.parent.initializer === n)); + } + + function getPartOfForStatementContainingNode(node: ts.Node, container: ts.ForStatement) { + return ts.findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement); + } + + function getEnclosingIterationStatement(node: ts.Node): ts.Node | undefined { + return ts.findAncestor(node, n => (!n || ts.nodeStartsNewLexicalEnvironment(n)) ? "quit" : ts.isIterationStatement(n, /*lookInLabeledStatements*/ false)); + } - function getEnclosingIterationStatement(node: ts.Node): ts.Node | undefined { - return ts.findAncestor(node, n => (!n || ts.nodeStartsNewLexicalEnvironment(n)) ? "quit" : ts.isIterationStatement(n, /*lookInLabeledStatements*/ false)); + function checkNestedBlockScopedBinding(node: ts.Identifier, symbol: ts.Symbol): void { + if (languageVersion >= ts.ScriptTarget.ES2015 || + (symbol.flags & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.Class)) === 0 || + !symbol.valueDeclaration || + ts.isSourceFile(symbol.valueDeclaration) || + symbol.valueDeclaration.parent.kind === ts.SyntaxKind.CatchClause) { + return; } - function checkNestedBlockScopedBinding(node: ts.Identifier, symbol: ts.Symbol): void { - if (languageVersion >= ts.ScriptTarget.ES2015 || - (symbol.flags & (ts.SymbolFlags.BlockScopedVariable | ts.SymbolFlags.Class)) === 0 || - !symbol.valueDeclaration || - ts.isSourceFile(symbol.valueDeclaration) || - symbol.valueDeclaration.parent.kind === ts.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) - // 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 = ts.getEnclosingBlockScopeContainer(symbol.valueDeclaration); - const isCaptured = isInsideFunctionOrInstancePropertyInitializer(node, container); - - const enclosingIterationStatement = getEnclosingIterationStatement(container); - if (enclosingIterationStatement) { - if (isCaptured) { - // mark iteration statement as containing block-scoped binding captured in some function - let capturesBlockScopeBindingInLoopBody = true; - if (ts.isForStatement(container)) { - const varDeclList = ts.getAncestor(symbol.valueDeclaration, ts.SyntaxKind.VariableDeclarationList); - if (varDeclList && varDeclList.parent === container) { - const part = getPartOfForStatementContainingNode(node.parent, container); - if (part) { - const links = getNodeLinks(part); - links.flags |= ts.NodeCheckFlags.ContainsCapturedBlockScopeBinding; - - const capturedBindings = links.capturedBlockScopeBindings || (links.capturedBlockScopeBindings = []); - ts.pushIfUnique(capturedBindings, symbol); - - if (part === container.initializer) { - capturesBlockScopeBindingInLoopBody = false; // Initializer is outside of loop body - } - } - } - } - if (capturesBlockScopeBindingInLoopBody) { - getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; - } - } + const container = ts.getEnclosingBlockScopeContainer(symbol.valueDeclaration); + const isCaptured = isInsideFunctionOrInstancePropertyInitializer(node, container); - // 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. + const enclosingIterationStatement = getEnclosingIterationStatement(container); + if (enclosingIterationStatement) { + if (isCaptured) { + // mark iteration statement as containing block-scoped binding captured in some function + let capturesBlockScopeBindingInLoopBody = true; if (ts.isForStatement(container)) { const varDeclList = ts.getAncestor(symbol.valueDeclaration, ts.SyntaxKind.VariableDeclarationList); - if (varDeclList && varDeclList.parent === container && isAssignedInBodyOfForStatement(node, container)) { - getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.NeedsLoopOutParameter; + if (varDeclList && varDeclList.parent === container) { + const part = getPartOfForStatementContainingNode(node.parent, container); + if (part) { + const links = getNodeLinks(part); + links.flags |= ts.NodeCheckFlags.ContainsCapturedBlockScopeBinding; + + const capturedBindings = links.capturedBlockScopeBindings || (links.capturedBlockScopeBindings = []); + ts.pushIfUnique(capturedBindings, symbol); + + if (part === container.initializer) { + capturesBlockScopeBindingInLoopBody = false; // Initializer is outside of loop body + } + } } } - - // set 'declared inside loop' bit on the block-scoped binding - getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; + if (capturesBlockScopeBindingInLoopBody) { + getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; + } } - if (isCaptured) { - getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.CapturedBlockScopedBinding; + // 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 (ts.isForStatement(container)) { + const varDeclList = ts.getAncestor(symbol.valueDeclaration, ts.SyntaxKind.VariableDeclarationList); + if (varDeclList && varDeclList.parent === container && isAssignedInBodyOfForStatement(node, container)) { + getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.NeedsLoopOutParameter; + } } - } - function isBindingCapturedByNode(node: ts.Node, decl: ts.VariableDeclaration | ts.BindingElement) { - const links = getNodeLinks(node); - return !!links && ts.contains(links.capturedBlockScopeBindings, getSymbolOfNode(decl)); + // set 'declared inside loop' bit on the block-scoped binding + getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; } - function isAssignedInBodyOfForStatement(node: ts.Identifier, container: ts.ForStatement): boolean { - // skip parenthesized nodes - let current: ts.Node = node; - while (current.parent.kind === ts.SyntaxKind.ParenthesizedExpression) { - current = current.parent; - } - - // check if node is used as LHS in some assignment expression - let isAssigned = false; - if (ts.isAssignmentTarget(current)) { - isAssigned = true; - } - else if ((current.parent.kind === ts.SyntaxKind.PrefixUnaryExpression || current.parent.kind === ts.SyntaxKind.PostfixUnaryExpression)) { - const expr = current.parent as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression; - isAssigned = expr.operator === ts.SyntaxKind.PlusPlusToken || expr.operator === ts.SyntaxKind.MinusMinusToken; - } + if (isCaptured) { + getNodeLinks(symbol.valueDeclaration).flags |= ts.NodeCheckFlags.CapturedBlockScopedBinding; + } + } - if (!isAssigned) { - return false; - } + function isBindingCapturedByNode(node: ts.Node, decl: ts.VariableDeclaration | ts.BindingElement) { + const links = getNodeLinks(node); + return !!links && ts.contains(links.capturedBlockScopeBindings, getSymbolOfNode(decl)); + } - // 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 !!ts.findAncestor(current, n => n === container ? "quit" : n === container.statement); + function isAssignedInBodyOfForStatement(node: ts.Identifier, container: ts.ForStatement): boolean { + // skip parenthesized nodes + let current: ts.Node = node; + while (current.parent.kind === ts.SyntaxKind.ParenthesizedExpression) { + current = current.parent; } - function captureLexicalThis(node: ts.Node, container: ts.Node): void { - getNodeLinks(node).flags |= ts.NodeCheckFlags.LexicalThis; - if (container.kind === ts.SyntaxKind.PropertyDeclaration || container.kind === ts.SyntaxKind.Constructor) { - const classNode = container.parent; - getNodeLinks(classNode).flags |= ts.NodeCheckFlags.CaptureThis; - } - else { - getNodeLinks(container).flags |= ts.NodeCheckFlags.CaptureThis; - } + // check if node is used as LHS in some assignment expression + let isAssigned = false; + if (ts.isAssignmentTarget(current)) { + isAssigned = true; + } + else if ((current.parent.kind === ts.SyntaxKind.PrefixUnaryExpression || current.parent.kind === ts.SyntaxKind.PostfixUnaryExpression)) { + const expr = current.parent as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression; + isAssigned = expr.operator === ts.SyntaxKind.PlusPlusToken || expr.operator === ts.SyntaxKind.MinusMinusToken; } - function findFirstSuperCall(node: ts.Node): ts.SuperCall | undefined { - return ts.isSuperCall(node) ? node : - ts.isFunctionLike(node) ? undefined : - ts.forEachChild(node, findFirstSuperCall); + if (!isAssigned) { + return false; } - /** - * 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: ts.ClassDeclaration): boolean { - const classSymbol = getSymbolOfNode(classDecl); - const classInstanceType = getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType; - const baseConstructorType = getBaseConstructorTypeOfClass(classInstanceType); + // 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 !!ts.findAncestor(current, n => n === container ? "quit" : n === container.statement); + } - return baseConstructorType === nullWideningType; + function captureLexicalThis(node: ts.Node, container: ts.Node): void { + getNodeLinks(node).flags |= ts.NodeCheckFlags.LexicalThis; + if (container.kind === ts.SyntaxKind.PropertyDeclaration || container.kind === ts.SyntaxKind.Constructor) { + const classNode = container.parent; + getNodeLinks(classNode).flags |= ts.NodeCheckFlags.CaptureThis; } + else { + getNodeLinks(container).flags |= ts.NodeCheckFlags.CaptureThis; + } + } - function checkThisBeforeSuper(node: ts.Node, container: ts.Node, diagnosticMessage: ts.DiagnosticMessage) { - const containingClassDecl = container.parent as ts.ClassDeclaration; - const baseTypeNode = ts.getClassExtendsHeritageElement(containingClassDecl); + function findFirstSuperCall(node: ts.Node): ts.SuperCall | undefined { + return ts.isSuperCall(node) ? node : + ts.isFunctionLike(node) ? undefined : + ts.forEachChild(node, findFirstSuperCall); + } - // 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)) { - if (node.flowNode && !isPostSuperFlowNode(node.flowNode, /*noCacheCheck*/ false)) { - error(node, diagnosticMessage); - } + /** + * 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: ts.ClassDeclaration): boolean { + const classSymbol = getSymbolOfNode(classDecl); + const classInstanceType = getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType; + const baseConstructorType = getBaseConstructorTypeOfClass(classInstanceType); + + return baseConstructorType === nullWideningType; + } + + function checkThisBeforeSuper(node: ts.Node, container: ts.Node, diagnosticMessage: ts.DiagnosticMessage) { + const containingClassDecl = container.parent as ts.ClassDeclaration; + const baseTypeNode = ts.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)) { + if (node.flowNode && !isPostSuperFlowNode(node.flowNode, /*noCacheCheck*/ false)) { + error(node, diagnosticMessage); } } + } - function checkThisInStaticClassFieldInitializerInDecoratedClass(thisExpression: ts.Node, container: ts.Node) { - if (ts.isPropertyDeclaration(container) && ts.hasStaticModifier(container) && - container.initializer && ts.textRangeContainsPositionInclusive(container.initializer, thisExpression.pos) && ts.length(container.parent.decorators)) { - error(thisExpression, ts.Diagnostics.Cannot_use_this_in_a_static_property_initializer_of_a_decorated_class); - } + function checkThisInStaticClassFieldInitializerInDecoratedClass(thisExpression: ts.Node, container: ts.Node) { + if (ts.isPropertyDeclaration(container) && ts.hasStaticModifier(container) && + container.initializer && ts.textRangeContainsPositionInclusive(container.initializer, thisExpression.pos) && ts.length(container.parent.decorators)) { + error(thisExpression, ts.Diagnostics.Cannot_use_this_in_a_static_property_initializer_of_a_decorated_class); } + } - function checkThisExpression(node: ts.Node): ts.Type { - const isNodeInTypeQuery = isInTypeQuery(node); - // Stop at the first arrow function so that we can - // tell whether 'this' needs to be captured. - let container = ts.getThisContainer(node, /* includeArrowFunctions */ true); - let capturedByArrowFunction = false; + function checkThisExpression(node: ts.Node): ts.Type { + const isNodeInTypeQuery = isInTypeQuery(node); + // Stop at the first arrow function so that we can + // tell whether 'this' needs to be captured. + let container = ts.getThisContainer(node, /* includeArrowFunctions */ true); + let capturedByArrowFunction = false; - if (container.kind === ts.SyntaxKind.Constructor) { - checkThisBeforeSuper(node, container, ts.Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class); - } + if (container.kind === ts.SyntaxKind.Constructor) { + checkThisBeforeSuper(node, container, ts.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 === ts.SyntaxKind.ArrowFunction) { - container = ts.getThisContainer(container, /* includeArrowFunctions */ false); - capturedByArrowFunction = true; - } + // Now skip arrow functions to get the "real" owner of 'this'. + if (container.kind === ts.SyntaxKind.ArrowFunction) { + container = ts.getThisContainer(container, /* includeArrowFunctions */ false); + capturedByArrowFunction = true; + } - checkThisInStaticClassFieldInitializerInDecoratedClass(node, container); - switch (container.kind) { - case ts.SyntaxKind.ModuleDeclaration: - error(node, ts.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 ts.SyntaxKind.EnumDeclaration: - error(node, ts.Diagnostics.this_cannot_be_referenced_in_current_location); + checkThisInStaticClassFieldInitializerInDecoratedClass(node, container); + switch (container.kind) { + case ts.SyntaxKind.ModuleDeclaration: + error(node, ts.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 ts.SyntaxKind.EnumDeclaration: + error(node, ts.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 ts.SyntaxKind.Constructor: + if (isInConstructorArgumentInitializer(node, container)) { + error(node, ts.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 ts.SyntaxKind.Constructor: - if (isInConstructorArgumentInitializer(node, container)) { - error(node, ts.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 ts.SyntaxKind.ComputedPropertyName: - error(node, ts.Diagnostics.this_cannot_be_referenced_in_a_computed_property_name); - break; - } + } + break; + case ts.SyntaxKind.ComputedPropertyName: + error(node, ts.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 (!isNodeInTypeQuery && capturedByArrowFunction && languageVersion < ts.ScriptTarget.ES2015) { - captureLexicalThis(node, container); - } + // When targeting es6, mark that we'll need to capture `this` in its lexically bound scope. + if (!isNodeInTypeQuery && capturedByArrowFunction && languageVersion < ts.ScriptTarget.ES2015) { + captureLexicalThis(node, container); + } - const type = tryGetThisTypeAt(node, /*includeGlobalThis*/ true, container); - if (noImplicitThis) { - const globalThisType = getTypeOfSymbol(globalThisSymbol); - if (type === globalThisType && capturedByArrowFunction) { - error(node, ts.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, ts.Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation); - if (!ts.isSourceFile(container)) { - const outsideThis = tryGetThisTypeAt(container); - if (outsideThis && outsideThis !== globalThisType) { - ts.addRelatedInfo(diag, ts.createDiagnosticForNode(container, ts.Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container)); - } + const type = tryGetThisTypeAt(node, /*includeGlobalThis*/ true, container); + if (noImplicitThis) { + const globalThisType = getTypeOfSymbol(globalThisSymbol); + if (type === globalThisType && capturedByArrowFunction) { + error(node, ts.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, ts.Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation); + if (!ts.isSourceFile(container)) { + const outsideThis = tryGetThisTypeAt(container); + if (outsideThis && outsideThis !== globalThisType) { + ts.addRelatedInfo(diag, ts.createDiagnosticForNode(container, ts.Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container)); } } } - return type || anyType; } - - function tryGetThisTypeAt(node: ts.Node, includeGlobalThis = true, container = ts.getThisContainer(node, /*includeArrowFunctions*/ false)): ts.Type | undefined { - const isInJS = ts.isInJSFile(node); - if (ts.isFunctionLike(container) && - (!isInParameterInitializerBeforeContainingFunction(node) || ts.getThisParameter(container))) { - let thisType = getThisTypeOfDeclaration(container) || isInJS && getTypeForThisExpressionFromJSDoc(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. - if (!thisType) { - const className = getClassNameFromPrototypeMethod(container); - if (isInJS && className) { - const classSymbol = checkExpression(className).symbol; - if (classSymbol && classSymbol.members && (classSymbol.flags & ts.SymbolFlags.Function)) { - thisType = (getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType).thisType; - } - } - else if (isJSConstructor(container)) { - thisType = (getDeclaredTypeOfSymbol(getMergedSymbol(container.symbol)) as ts.InterfaceType).thisType; + return type || anyType; + } + + function tryGetThisTypeAt(node: ts.Node, includeGlobalThis = true, container = ts.getThisContainer(node, /*includeArrowFunctions*/ false)): ts.Type | undefined { + const isInJS = ts.isInJSFile(node); + if (ts.isFunctionLike(container) && + (!isInParameterInitializerBeforeContainingFunction(node) || ts.getThisParameter(container))) { + let thisType = getThisTypeOfDeclaration(container) || isInJS && getTypeForThisExpressionFromJSDoc(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. + if (!thisType) { + const className = getClassNameFromPrototypeMethod(container); + if (isInJS && className) { + const classSymbol = checkExpression(className).symbol; + if (classSymbol && classSymbol.members && (classSymbol.flags & ts.SymbolFlags.Function)) { + thisType = (getDeclaredTypeOfSymbol(classSymbol) as ts.InterfaceType).thisType; } - thisType ||= getContextualThisParameterType(container); } - - if (thisType) { - return getFlowTypeOfReference(node, thisType); + else if (isJSConstructor(container)) { + thisType = (getDeclaredTypeOfSymbol(getMergedSymbol(container.symbol)) as ts.InterfaceType).thisType; } + thisType ||= getContextualThisParameterType(container); } - if (ts.isClassLike(container.parent)) { - const symbol = getSymbolOfNode(container.parent); - const type = ts.isStatic(container) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType).thisType!; - return getFlowTypeOfReference(node, type); - } - - if (ts.isSourceFile(container)) { - // look up in the source file's locals or exports - if (container.commonJsModuleIndicator) { - const fileSymbol = getSymbolOfNode(container); - return fileSymbol && getTypeOfSymbol(fileSymbol); - } - else if (container.externalModuleIndicator) { - // TODO: Maybe issue a better error than 'object is possibly undefined' - return undefinedType; - } - else if (includeGlobalThis) { - return getTypeOfSymbol(globalThisSymbol); - } + if (thisType) { + return getFlowTypeOfReference(node, thisType); } } - function getExplicitThisType(node: ts.Expression) { - const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); - if (ts.isFunctionLike(container)) { - const signature = getSignatureFromDeclaration(container); - if (signature.thisParameter) { - return getExplicitTypeOfSymbol(signature.thisParameter); - } - } - if (ts.isClassLike(container.parent)) { - const symbol = getSymbolOfNode(container.parent); - return ts.isStatic(container) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType).thisType!; - } - } + if (ts.isClassLike(container.parent)) { + const symbol = getSymbolOfNode(container.parent); + const type = ts.isStatic(container) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType).thisType!; + return getFlowTypeOfReference(node, type); + } - function getClassNameFromPrototypeMethod(container: ts.Node) { - // Check if it's the RHS of a x.prototype.y = function [name]() { .... } - if (container.kind === ts.SyntaxKind.FunctionExpression && - ts.isBinaryExpression(container.parent) && - ts.getAssignmentDeclarationKind(container.parent) === ts.AssignmentDeclarationKind.PrototypeProperty) { - // Get the 'x' of 'x.prototype.y = container' - return ((container.parent // x.prototype.y = container - .left as ts.PropertyAccessExpression) // x.prototype.y - .expression as ts.PropertyAccessExpression) // x.prototype - .expression; // x - } - // x.prototype = { method() { } } - else if (container.kind === ts.SyntaxKind.MethodDeclaration && - container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression && - ts.isBinaryExpression(container.parent.parent) && - ts.getAssignmentDeclarationKind(container.parent.parent) === ts.AssignmentDeclarationKind.Prototype) { - return (container.parent.parent.left as ts.PropertyAccessExpression).expression; - } - // x.prototype = { method: function() { } } - else if (container.kind === ts.SyntaxKind.FunctionExpression && - container.parent.kind === ts.SyntaxKind.PropertyAssignment && - container.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression && - ts.isBinaryExpression(container.parent.parent.parent) && - ts.getAssignmentDeclarationKind(container.parent.parent.parent) === ts.AssignmentDeclarationKind.Prototype) { - return (container.parent.parent.parent.left as ts.PropertyAccessExpression).expression; + if (ts.isSourceFile(container)) { + // look up in the source file's locals or exports + if (container.commonJsModuleIndicator) { + const fileSymbol = getSymbolOfNode(container); + return fileSymbol && getTypeOfSymbol(fileSymbol); } - // Object.defineProperty(x, "method", { value: function() { } }); - // Object.defineProperty(x, "method", { set: (x: () => void) => void }); - // Object.defineProperty(x, "method", { get: () => function() { }) }); - else if (container.kind === ts.SyntaxKind.FunctionExpression && - ts.isPropertyAssignment(container.parent) && - ts.isIdentifier(container.parent.name) && - (container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") && - ts.isObjectLiteralExpression(container.parent.parent) && - ts.isCallExpression(container.parent.parent.parent) && - container.parent.parent.parent.arguments[2] === container.parent.parent && - ts.getAssignmentDeclarationKind(container.parent.parent.parent) === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { - return (container.parent.parent.parent.arguments[0] as ts.PropertyAccessExpression).expression; + else if (container.externalModuleIndicator) { + // TODO: Maybe issue a better error than 'object is possibly undefined' + return undefinedType; } - // Object.defineProperty(x, "method", { value() { } }); - // Object.defineProperty(x, "method", { set(x: () => void) {} }); - // Object.defineProperty(x, "method", { get() { return () => {} } }); - else if (ts.isMethodDeclaration(container) && - ts.isIdentifier(container.name) && - (container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") && - ts.isObjectLiteralExpression(container.parent) && - ts.isCallExpression(container.parent.parent) && - container.parent.parent.arguments[2] === container.parent && - ts.getAssignmentDeclarationKind(container.parent.parent) === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { - return (container.parent.parent.arguments[0] as ts.PropertyAccessExpression).expression; + else if (includeGlobalThis) { + return getTypeOfSymbol(globalThisSymbol); } } + } - function getTypeForThisExpressionFromJSDoc(node: ts.Node) { - const jsdocType = ts.getJSDocType(node); - if (jsdocType && jsdocType.kind === ts.SyntaxKind.JSDocFunctionType) { - const jsDocFunctionType = jsdocType as ts.JSDocFunctionType; - if (jsDocFunctionType.parameters.length > 0 && - jsDocFunctionType.parameters[0].name && - (jsDocFunctionType.parameters[0].name as ts.Identifier).escapedText === ts.InternalSymbolName.This) { - return getTypeFromTypeNode(jsDocFunctionType.parameters[0].type!); - } - } - const thisTag = ts.getJSDocThisTag(node); - if (thisTag && thisTag.typeExpression) { - return getTypeFromTypeNode(thisTag.typeExpression); + function getExplicitThisType(node: ts.Expression) { + const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); + if (ts.isFunctionLike(container)) { + const signature = getSignatureFromDeclaration(container); + if (signature.thisParameter) { + return getExplicitTypeOfSymbol(signature.thisParameter); } } - - function isInConstructorArgumentInitializer(node: ts.Node, constructorDecl: ts.Node): boolean { - return !!ts.findAncestor(node, n => ts.isFunctionLikeDeclaration(n) ? "quit" : n.kind === ts.SyntaxKind.Parameter && n.parent === constructorDecl); + if (ts.isClassLike(container.parent)) { + const symbol = getSymbolOfNode(container.parent); + return ts.isStatic(container) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType).thisType!; } + } - function checkSuperExpression(node: ts.Node): ts.Type { - const isCallExpression = node.parent.kind === ts.SyntaxKind.CallExpression && (node.parent as ts.CallExpression).expression === node; - const immediateContainer = ts.getSuperContainer(node, /*stopOnFunctions*/ true); - let container = immediateContainer; - let needToCaptureLexicalThis = false; + function getClassNameFromPrototypeMethod(container: ts.Node) { + // Check if it's the RHS of a x.prototype.y = function [name]() { .... } + if (container.kind === ts.SyntaxKind.FunctionExpression && + ts.isBinaryExpression(container.parent) && + ts.getAssignmentDeclarationKind(container.parent) === ts.AssignmentDeclarationKind.PrototypeProperty) { + // Get the 'x' of 'x.prototype.y = container' + return ((container.parent // x.prototype.y = container + .left as ts.PropertyAccessExpression) // x.prototype.y + .expression as ts.PropertyAccessExpression) // x.prototype + .expression; // x + } + // x.prototype = { method() { } } + else if (container.kind === ts.SyntaxKind.MethodDeclaration && + container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression && + ts.isBinaryExpression(container.parent.parent) && + ts.getAssignmentDeclarationKind(container.parent.parent) === ts.AssignmentDeclarationKind.Prototype) { + return (container.parent.parent.left as ts.PropertyAccessExpression).expression; + } + // x.prototype = { method: function() { } } + else if (container.kind === ts.SyntaxKind.FunctionExpression && + container.parent.kind === ts.SyntaxKind.PropertyAssignment && + container.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression && + ts.isBinaryExpression(container.parent.parent.parent) && + ts.getAssignmentDeclarationKind(container.parent.parent.parent) === ts.AssignmentDeclarationKind.Prototype) { + return (container.parent.parent.parent.left as ts.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 === ts.SyntaxKind.FunctionExpression && + ts.isPropertyAssignment(container.parent) && + ts.isIdentifier(container.parent.name) && + (container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") && + ts.isObjectLiteralExpression(container.parent.parent) && + ts.isCallExpression(container.parent.parent.parent) && + container.parent.parent.parent.arguments[2] === container.parent.parent && + ts.getAssignmentDeclarationKind(container.parent.parent.parent) === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { + return (container.parent.parent.parent.arguments[0] as ts.PropertyAccessExpression).expression; + } + // Object.defineProperty(x, "method", { value() { } }); + // Object.defineProperty(x, "method", { set(x: () => void) {} }); + // Object.defineProperty(x, "method", { get() { return () => {} } }); + else if (ts.isMethodDeclaration(container) && + ts.isIdentifier(container.name) && + (container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") && + ts.isObjectLiteralExpression(container.parent) && + ts.isCallExpression(container.parent.parent) && + container.parent.parent.arguments[2] === container.parent && + ts.getAssignmentDeclarationKind(container.parent.parent) === ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { + return (container.parent.parent.arguments[0] as ts.PropertyAccessExpression).expression; + } + } - // adjust the container reference in case if super is used inside arrow functions with arbitrarily deep nesting - if (!isCallExpression) { - while (container && container.kind === ts.SyntaxKind.ArrowFunction) { - container = ts.getSuperContainer(container, /*stopOnFunctions*/ true); - needToCaptureLexicalThis = languageVersion < ts.ScriptTarget.ES2015; - } - } - - const canUseSuperExpression = isLegalUsageOfSuperExpression(container); - let nodeCheckFlag: ts.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 = ts.findAncestor(node, n => n === container ? "quit" : n.kind === ts.SyntaxKind.ComputedPropertyName); - if (current && current.kind === ts.SyntaxKind.ComputedPropertyName) { - error(node, ts.Diagnostics.super_cannot_be_referenced_in_a_computed_property_name); - } - else if (isCallExpression) { - error(node, ts.Diagnostics.Super_calls_are_not_permitted_outside_constructors_or_in_nested_functions_inside_constructors); - } - else if (!container || !container.parent || !(ts.isClassLike(container.parent) || container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression)) { - error(node, ts.Diagnostics.super_can_only_be_referenced_in_members_of_derived_classes_or_object_literal_expressions); - } - else { - error(node, ts.Diagnostics.super_property_access_is_permitted_only_in_a_constructor_member_function_or_member_accessor_of_a_derived_class); - } - return errorType; + function getTypeForThisExpressionFromJSDoc(node: ts.Node) { + const jsdocType = ts.getJSDocType(node); + if (jsdocType && jsdocType.kind === ts.SyntaxKind.JSDocFunctionType) { + const jsDocFunctionType = jsdocType as ts.JSDocFunctionType; + if (jsDocFunctionType.parameters.length > 0 && + jsDocFunctionType.parameters[0].name && + (jsDocFunctionType.parameters[0].name as ts.Identifier).escapedText === ts.InternalSymbolName.This) { + return getTypeFromTypeNode(jsDocFunctionType.parameters[0].type!); } + } + const thisTag = ts.getJSDocThisTag(node); + if (thisTag && thisTag.typeExpression) { + return getTypeFromTypeNode(thisTag.typeExpression); + } + } - if (!isCallExpression && immediateContainer.kind === ts.SyntaxKind.Constructor) { - checkThisBeforeSuper(node, container, ts.Diagnostics.super_must_be_called_before_accessing_a_property_of_super_in_the_constructor_of_a_derived_class); + function isInConstructorArgumentInitializer(node: ts.Node, constructorDecl: ts.Node): boolean { + return !!ts.findAncestor(node, n => ts.isFunctionLikeDeclaration(n) ? "quit" : n.kind === ts.SyntaxKind.Parameter && n.parent === constructorDecl); + } + + function checkSuperExpression(node: ts.Node): ts.Type { + const isCallExpression = node.parent.kind === ts.SyntaxKind.CallExpression && (node.parent as ts.CallExpression).expression === node; + const immediateContainer = ts.getSuperContainer(node, /*stopOnFunctions*/ true); + let container = immediateContainer; + 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 === ts.SyntaxKind.ArrowFunction) { + container = ts.getSuperContainer(container, /*stopOnFunctions*/ true); + needToCaptureLexicalThis = languageVersion < ts.ScriptTarget.ES2015; } + } - if (ts.isStatic(container) || isCallExpression) { - nodeCheckFlag = ts.NodeCheckFlags.SuperStatic; - if (!isCallExpression && - languageVersion >= ts.ScriptTarget.ES2015 && languageVersion <= ts.ScriptTarget.ES2021 && - (ts.isPropertyDeclaration(container) || ts.isClassStaticBlockDeclaration(container))) { - // for `super.x` or `super[x]` in a static initializer, mark all enclosing - // block scope containers so that we can report potential collisions with - // `Reflect`. - ts.forEachEnclosingBlockScopeContainer(node.parent, current => { - if (!ts.isSourceFile(current) || ts.isExternalOrCommonJsModule(current)) { - getNodeLinks(current).flags |= ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer; - } - }); - } + const canUseSuperExpression = isLegalUsageOfSuperExpression(container); + let nodeCheckFlag: ts.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 = ts.findAncestor(node, n => n === container ? "quit" : n.kind === ts.SyntaxKind.ComputedPropertyName); + if (current && current.kind === ts.SyntaxKind.ComputedPropertyName) { + error(node, ts.Diagnostics.super_cannot_be_referenced_in_a_computed_property_name); + } + else if (isCallExpression) { + error(node, ts.Diagnostics.Super_calls_are_not_permitted_outside_constructors_or_in_nested_functions_inside_constructors); + } + else if (!container || !container.parent || !(ts.isClassLike(container.parent) || container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression)) { + error(node, ts.Diagnostics.super_can_only_be_referenced_in_members_of_derived_classes_or_object_literal_expressions); } else { - nodeCheckFlag = ts.NodeCheckFlags.SuperInstance; + error(node, ts.Diagnostics.super_property_access_is_permitted_only_in_a_constructor_member_function_or_member_accessor_of_a_derived_class); } + return errorType; + } - getNodeLinks(node).flags |= nodeCheckFlag; + if (!isCallExpression && immediateContainer.kind === ts.SyntaxKind.Constructor) { + checkThisBeforeSuper(node, container, ts.Diagnostics.super_must_be_called_before_accessing_a_property_of_super_in_the_constructor_of_a_derived_class); + } - // 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 === ts.SyntaxKind.MethodDeclaration && ts.hasSyntacticModifier(container, ts.ModifierFlags.Async)) { - if (ts.isSuperProperty(node.parent) && ts.isAssignmentTarget(node.parent)) { - getNodeLinks(container).flags |= ts.NodeCheckFlags.AsyncMethodWithSuperBinding; - } - else { - getNodeLinks(container).flags |= ts.NodeCheckFlags.AsyncMethodWithSuper; - } + if (ts.isStatic(container) || isCallExpression) { + nodeCheckFlag = ts.NodeCheckFlags.SuperStatic; + if (!isCallExpression && + languageVersion >= ts.ScriptTarget.ES2015 && languageVersion <= ts.ScriptTarget.ES2021 && + (ts.isPropertyDeclaration(container) || ts.isClassStaticBlockDeclaration(container))) { + // for `super.x` or `super[x]` in a static initializer, mark all enclosing + // block scope containers so that we can report potential collisions with + // `Reflect`. + ts.forEachEnclosingBlockScopeContainer(node.parent, current => { + if (!ts.isSourceFile(current) || ts.isExternalOrCommonJsModule(current)) { + getNodeLinks(current).flags |= ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer; + } + }); } + } + else { + nodeCheckFlag = ts.NodeCheckFlags.SuperInstance; + } - 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); - } + getNodeLinks(node).flags |= nodeCheckFlag; - if (container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) { - if (languageVersion < ts.ScriptTarget.ES2015) { - error(node, ts.Diagnostics.super_is_only_allowed_in_members_of_object_literal_expressions_when_option_target_is_ES2015_or_higher); - return errorType; - } - else { - // for object literal assume that type of 'super' is 'any' - return anyType; - } + // 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 === ts.SyntaxKind.MethodDeclaration && ts.hasSyntacticModifier(container, ts.ModifierFlags.Async)) { + if (ts.isSuperProperty(node.parent) && ts.isAssignmentTarget(node.parent)) { + getNodeLinks(container).flags |= ts.NodeCheckFlags.AsyncMethodWithSuperBinding; } - - // at this point the only legal case for parent is ClassLikeDeclaration - const classLikeDeclaration = container.parent as ts.ClassLikeDeclaration; - if (!ts.getClassExtendsHeritageElement(classLikeDeclaration)) { - error(node, ts.Diagnostics.super_can_only_be_referenced_in_a_derived_class); - return errorType; + else { + getNodeLinks(container).flags |= ts.NodeCheckFlags.AsyncMethodWithSuper; } + } - const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(classLikeDeclaration)) as ts.InterfaceType; - const baseClassType = classType && getBaseTypes(classType)[0]; - if (!baseClassType) { - return errorType; - } + 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.kind === ts.SyntaxKind.Constructor && isInConstructorArgumentInitializer(node, container)) { - // issue custom error message for super property access in constructor arguments (to be aligned with old compiler) - error(node, ts.Diagnostics.super_cannot_be_referenced_in_constructor_arguments); + if (container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) { + if (languageVersion < ts.ScriptTarget.ES2015) { + error(node, ts.Diagnostics.super_is_only_allowed_in_members_of_object_literal_expressions_when_option_target_is_ES2015_or_higher); return errorType; } - - return nodeCheckFlag === ts.NodeCheckFlags.SuperStatic - ? getBaseConstructorTypeOfClass(classType) - : getTypeWithThisArgument(baseClassType, classType.thisType); - - function isLegalUsageOfSuperExpression(container: ts.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 === ts.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 (ts.isClassLike(container.parent) || container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) { - if (ts.isStatic(container)) { - return container.kind === ts.SyntaxKind.MethodDeclaration || - container.kind === ts.SyntaxKind.MethodSignature || - container.kind === ts.SyntaxKind.GetAccessor || - container.kind === ts.SyntaxKind.SetAccessor || - container.kind === ts.SyntaxKind.PropertyDeclaration || - container.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; - } - else { - return container.kind === ts.SyntaxKind.MethodDeclaration || - container.kind === ts.SyntaxKind.MethodSignature || - container.kind === ts.SyntaxKind.GetAccessor || - container.kind === ts.SyntaxKind.SetAccessor || - container.kind === ts.SyntaxKind.PropertyDeclaration || - container.kind === ts.SyntaxKind.PropertySignature || - container.kind === ts.SyntaxKind.Constructor; - } - } - } - - return false; + else { + // for object literal assume that type of 'super' is 'any' + return anyType; } } - function getContainingObjectLiteral(func: ts.SignatureDeclaration): ts.ObjectLiteralExpression | undefined { - return (func.kind === ts.SyntaxKind.MethodDeclaration || - func.kind === ts.SyntaxKind.GetAccessor || - func.kind === ts.SyntaxKind.SetAccessor) && func.parent.kind === ts.SyntaxKind.ObjectLiteralExpression ? func.parent : - func.kind === ts.SyntaxKind.FunctionExpression && func.parent.kind === ts.SyntaxKind.PropertyAssignment ? func.parent.parent as ts.ObjectLiteralExpression : - undefined; + // at this point the only legal case for parent is ClassLikeDeclaration + const classLikeDeclaration = container.parent as ts.ClassLikeDeclaration; + if (!ts.getClassExtendsHeritageElement(classLikeDeclaration)) { + error(node, ts.Diagnostics.super_can_only_be_referenced_in_a_derived_class); + return errorType; } - function getThisTypeArgument(type: ts.Type): ts.Type | undefined { - return ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).target === globalThisType ? getTypeArguments(type as ts.TypeReference)[0] : undefined; + const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(classLikeDeclaration)) as ts.InterfaceType; + const baseClassType = classType && getBaseTypes(classType)[0]; + if (!baseClassType) { + return errorType; } - function getThisTypeFromContextualType(type: ts.Type): ts.Type | undefined { - return mapType(type, t => { - return t.flags & ts.TypeFlags.Intersection ? ts.forEach((t as ts.IntersectionType).types, getThisTypeArgument) : getThisTypeArgument(t); - }); + if (container.kind === ts.SyntaxKind.Constructor && isInConstructorArgumentInitializer(node, container)) { + // issue custom error message for super property access in constructor arguments (to be aligned with old compiler) + error(node, ts.Diagnostics.super_cannot_be_referenced_in_constructor_arguments); + return errorType; } - function getContextualThisParameterType(func: ts.SignatureDeclaration): ts.Type | undefined { - if (func.kind === ts.SyntaxKind.ArrowFunction) { - return undefined; - } - if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) { - const contextualSignature = getContextualSignature(func); - if (contextualSignature) { - const thisParameter = contextualSignature.thisParameter; - if (thisParameter) { - return getTypeOfSymbol(thisParameter); - } - } - } - const inJs = ts.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 !== ts.SyntaxKind.PropertyAssignment) { - break; - } - literal = literal.parent.parent as ts.ObjectLiteralExpression; - 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 = ts.walkUpParenthesizedExpressions(func.parent); - if (parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { - const target = (parent as ts.BinaryExpression).left; - if (ts.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 && ts.isIdentifier(expression)) { - const sourceFile = ts.getSourceFileOfNode(parent); - if (sourceFile.commonJsModuleIndicator && getResolvedSymbol(expression) === sourceFile.symbol) { - return undefined; - } - } + return nodeCheckFlag === ts.NodeCheckFlags.SuperStatic + ? getBaseConstructorTypeOfClass(classType) + : getTypeWithThisArgument(baseClassType, classType.thisType); - return getWidenedType(checkExpressionCached(expression)); - } - } + function isLegalUsageOfSuperExpression(container: ts.Node): boolean { + if (!container) { + return false; } - return undefined; - } - // Return contextual type of parameter or undefined if no contextual type is available - function getContextuallyTypedParameterType(parameter: ts.ParameterDeclaration): ts.Type | undefined { - const func = parameter.parent; - if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) { - return undefined; - } - const iife = ts.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, CheckMode.Normal); - } - 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) - (ts.getThisParameter(func) ? 1 : 0); - return parameter.dotDotDotToken && ts.lastOrUndefined(func.parameters) === parameter ? - getRestTypeAtPosition(contextualSignature, index) : - tryGetTypeAtPosition(contextualSignature, index); + if (isCallExpression) { + // TS 1.0 SPEC (April 2014): 4.8.1 + // Super calls are only permitted in constructors of derived classes + return container.kind === ts.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 - function getContextualTypeForVariableLikeDeclaration(declaration: ts.VariableLikeDeclaration): ts.Type | undefined { - const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); - } - switch (declaration.kind) { - case ts.SyntaxKind.Parameter: - return getContextuallyTypedParameterType(declaration); - case ts.SyntaxKind.BindingElement: - return getContextualTypeForBindingElement(declaration); - case ts.SyntaxKind.PropertyDeclaration: - if (ts.isStatic(declaration)) { - return getContextualTypeForStaticPropertyDeclaration(declaration); + // topmost container must be something that is directly nested in the class declaration\object literal expression + if (ts.isClassLike(container.parent) || container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) { + if (ts.isStatic(container)) { + return container.kind === ts.SyntaxKind.MethodDeclaration || + container.kind === ts.SyntaxKind.MethodSignature || + container.kind === ts.SyntaxKind.GetAccessor || + container.kind === ts.SyntaxKind.SetAccessor || + container.kind === ts.SyntaxKind.PropertyDeclaration || + container.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; } - // By default, do nothing and return undefined - only the above cases have context implied by a parent + else { + return container.kind === ts.SyntaxKind.MethodDeclaration || + container.kind === ts.SyntaxKind.MethodSignature || + container.kind === ts.SyntaxKind.GetAccessor || + container.kind === ts.SyntaxKind.SetAccessor || + container.kind === ts.SyntaxKind.PropertyDeclaration || + container.kind === ts.SyntaxKind.PropertySignature || + container.kind === ts.SyntaxKind.Constructor; + } + } } - } - function getContextualTypeForBindingElement(declaration: ts.BindingElement): ts.Type | undefined { - const parent = declaration.parent.parent; - const name = declaration.propertyName || declaration.name; - const parentType = getContextualTypeForVariableLikeDeclaration(parent) || - parent.kind !== ts.SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal); - if (!parentType || ts.isBindingPattern(name) || ts.isComputedNonLiteralName(name)) - return undefined; - if (parent.name.kind === ts.SyntaxKind.ArrayBindingPattern) { - const index = ts.indexOfNode(declaration.parent.elements, declaration); - if (index < 0) - return undefined; - return getContextualTypeForElementExpression(parentType, index); - } - const nameType = getLiteralTypeFromPropertyName(name); - if (isTypeUsableAsPropertyName(nameType)) { - const text = getPropertyNameFromType(nameType); - return getTypeOfPropertyOfType(parentType, text); - } + return false; } + } - function getContextualTypeForStaticPropertyDeclaration(declaration: ts.PropertyDeclaration): ts.Type | undefined { - const parentType = ts.isExpression(declaration.parent) && getContextualType(declaration.parent); - if (!parentType) - return undefined; - return getTypeOfPropertyOfContextualType(parentType, getSymbolOfNode(declaration).escapedName); - } - - // 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: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { - const declaration = node.parent as ts.VariableLikeDeclaration; - if (ts.hasInitializer(declaration) && node === declaration.initializer) { - const result = getContextualTypeForVariableLikeDeclaration(declaration); - if (result) { - return result; - } - if (!(contextFlags! & ts.ContextFlags.SkipBindingPatterns) && ts.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 getContainingObjectLiteral(func: ts.SignatureDeclaration): ts.ObjectLiteralExpression | undefined { + return (func.kind === ts.SyntaxKind.MethodDeclaration || + func.kind === ts.SyntaxKind.GetAccessor || + func.kind === ts.SyntaxKind.SetAccessor) && func.parent.kind === ts.SyntaxKind.ObjectLiteralExpression ? func.parent : + func.kind === ts.SyntaxKind.FunctionExpression && func.parent.kind === ts.SyntaxKind.PropertyAssignment ? func.parent.parent as ts.ObjectLiteralExpression : + undefined; + } + + function getThisTypeArgument(type: ts.Type): ts.Type | undefined { + return ts.getObjectFlags(type) & ts.ObjectFlags.Reference && (type as ts.TypeReference).target === globalThisType ? getTypeArguments(type as ts.TypeReference)[0] : undefined; + } + + function getThisTypeFromContextualType(type: ts.Type): ts.Type | undefined { + return mapType(type, t => { + return t.flags & ts.TypeFlags.Intersection ? ts.forEach((t as ts.IntersectionType).types, getThisTypeArgument) : getThisTypeArgument(t); + }); + } + + function getContextualThisParameterType(func: ts.SignatureDeclaration): ts.Type | undefined { + if (func.kind === ts.SyntaxKind.ArrowFunction) { return undefined; } - - function getContextualTypeForReturnExpression(node: ts.Expression): ts.Type | undefined { - const func = ts.getContainingFunction(node); - if (func) { - let contextualReturnType = getContextualReturnType(func); - if (contextualReturnType) { - const functionFlags = ts.getFunctionFlags(func); - if (functionFlags & ts.FunctionFlags.Generator) { // Generator or AsyncGenerator function - const use = functionFlags & ts.FunctionFlags.Async ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; - const iterationTypes = getIterationTypesOfIterable(contextualReturnType, use, /*errorNode*/ undefined); - if (!iterationTypes) { + if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) { + const contextualSignature = getContextualSignature(func); + if (contextualSignature) { + const thisParameter = contextualSignature.thisParameter; + if (thisParameter) { + return getTypeOfSymbol(thisParameter); + } + } + } + const inJs = ts.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 !== ts.SyntaxKind.PropertyAssignment) { + break; + } + literal = literal.parent.parent as ts.ObjectLiteralExpression; + 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 = ts.walkUpParenthesizedExpressions(func.parent); + if (parent.kind === ts.SyntaxKind.BinaryExpression && (parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const target = (parent as ts.BinaryExpression).left; + if (ts.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 && ts.isIdentifier(expression)) { + const sourceFile = ts.getSourceFileOfNode(parent); + if (sourceFile.commonJsModuleIndicator && getResolvedSymbol(expression) === sourceFile.symbol) { return undefined; } - contextualReturnType = iterationTypes.returnType; - // falls through to unwrap Promise for AsyncGenerators - } - - if (functionFlags & ts.FunctionFlags.Async) { // Async function or AsyncGenerator function - // Get the awaited type without the `Awaited` alias - const contextualAwaitedType = mapType(contextualReturnType, getAwaitedTypeNoAlias); - return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); } - return contextualReturnType; // Regular function or Generator function + return getWidenedType(checkExpressionCached(expression)); } } - return undefined; } + return undefined; + } - function getContextualTypeForAwaitOperand(node: ts.AwaitExpression, contextFlags?: ts.ContextFlags): ts.Type | undefined { - const contextualType = getContextualType(node, contextFlags); - if (contextualType) { - const contextualAwaitedType = getAwaitedTypeNoAlias(contextualType); - return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); - } + // Return contextual type of parameter or undefined if no contextual type is available + function getContextuallyTypedParameterType(parameter: ts.ParameterDeclaration): ts.Type | undefined { + const func = parameter.parent; + if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) { return undefined; } + const iife = ts.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, CheckMode.Normal); + } + 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) - (ts.getThisParameter(func) ? 1 : 0); + return parameter.dotDotDotToken && ts.lastOrUndefined(func.parameters) === parameter ? + getRestTypeAtPosition(contextualSignature, index) : + tryGetTypeAtPosition(contextualSignature, index); + } + } - function getContextualTypeForYieldOperand(node: ts.YieldExpression): ts.Type | undefined { - const func = ts.getContainingFunction(node); - if (func) { - const functionFlags = ts.getFunctionFlags(func); - const contextualReturnType = getContextualReturnType(func); - if (contextualReturnType) { - return node.asteriskToken - ? contextualReturnType - : getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, contextualReturnType, (functionFlags & ts.FunctionFlags.Async) !== 0); + function getContextualTypeForVariableLikeDeclaration(declaration: ts.VariableLikeDeclaration): ts.Type | undefined { + const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); + } + switch (declaration.kind) { + case ts.SyntaxKind.Parameter: + return getContextuallyTypedParameterType(declaration); + case ts.SyntaxKind.BindingElement: + return getContextualTypeForBindingElement(declaration); + case ts.SyntaxKind.PropertyDeclaration: + if (ts.isStatic(declaration)) { + return getContextualTypeForStaticPropertyDeclaration(declaration); } - } + // By default, do nothing and return undefined - only the above cases have context implied by a parent + } + } + function getContextualTypeForBindingElement(declaration: ts.BindingElement): ts.Type | undefined { + const parent = declaration.parent.parent; + const name = declaration.propertyName || declaration.name; + const parentType = getContextualTypeForVariableLikeDeclaration(parent) || + parent.kind !== ts.SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal); + if (!parentType || ts.isBindingPattern(name) || ts.isComputedNonLiteralName(name)) return undefined; + if (parent.name.kind === ts.SyntaxKind.ArrayBindingPattern) { + const index = ts.indexOfNode(declaration.parent.elements, declaration); + if (index < 0) + return undefined; + return getContextualTypeForElementExpression(parentType, index); + } + const nameType = getLiteralTypeFromPropertyName(name); + if (isTypeUsableAsPropertyName(nameType)) { + const text = getPropertyNameFromType(nameType); + return getTypeOfPropertyOfType(parentType, text); } + } - function isInParameterInitializerBeforeContainingFunction(node: ts.Node) { - let inBindingInitializer = false; - while (node.parent && !ts.isFunctionLike(node.parent)) { - if (ts.isParameter(node.parent) && (inBindingInitializer || node.parent.initializer === node)) { - return true; + function getContextualTypeForStaticPropertyDeclaration(declaration: ts.PropertyDeclaration): ts.Type | undefined { + const parentType = ts.isExpression(declaration.parent) && getContextualType(declaration.parent); + if (!parentType) + return undefined; + return getTypeOfPropertyOfContextualType(parentType, getSymbolOfNode(declaration).escapedName); + } + + // 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: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { + const declaration = node.parent as ts.VariableLikeDeclaration; + if (ts.hasInitializer(declaration) && node === declaration.initializer) { + const result = getContextualTypeForVariableLikeDeclaration(declaration); + if (result) { + return result; + } + if (!(contextFlags! & ts.ContextFlags.SkipBindingPatterns) && ts.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: ts.Expression): ts.Type | undefined { + const func = ts.getContainingFunction(node); + if (func) { + let contextualReturnType = getContextualReturnType(func); + if (contextualReturnType) { + const functionFlags = ts.getFunctionFlags(func); + if (functionFlags & ts.FunctionFlags.Generator) { // Generator or AsyncGenerator function + const use = functionFlags & ts.FunctionFlags.Async ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; + const iterationTypes = getIterationTypesOfIterable(contextualReturnType, use, /*errorNode*/ undefined); + if (!iterationTypes) { + return undefined; + } + contextualReturnType = iterationTypes.returnType; + // falls through to unwrap Promise for AsyncGenerators } - if (ts.isBindingElement(node.parent) && node.parent.initializer === node) { - inBindingInitializer = true; + + if (functionFlags & ts.FunctionFlags.Async) { // Async function or AsyncGenerator function + // Get the awaited type without the `Awaited` alias + const contextualAwaitedType = mapType(contextualReturnType, getAwaitedTypeNoAlias); + return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); } - node = node.parent; + return contextualReturnType; // Regular function or Generator function } + } + return undefined; + } - return false; + function getContextualTypeForAwaitOperand(node: ts.AwaitExpression, contextFlags?: ts.ContextFlags): ts.Type | undefined { + const contextualType = getContextualType(node, contextFlags); + if (contextualType) { + const contextualAwaitedType = getAwaitedTypeNoAlias(contextualType); + return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); } + return undefined; + } - function getContextualIterationType(kind: IterationTypeKind, functionDecl: ts.SignatureDeclaration): ts.Type | undefined { - const isAsync = !!(ts.getFunctionFlags(functionDecl) & ts.FunctionFlags.Async); - const contextualReturnType = getContextualReturnType(functionDecl); + function getContextualTypeForYieldOperand(node: ts.YieldExpression): ts.Type | undefined { + const func = ts.getContainingFunction(node); + if (func) { + const functionFlags = ts.getFunctionFlags(func); + const contextualReturnType = getContextualReturnType(func); if (contextualReturnType) { - return getIterationTypeOfGeneratorFunctionReturnType(kind, contextualReturnType, isAsync) - || undefined; + return node.asteriskToken + ? contextualReturnType + : getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, contextualReturnType, (functionFlags & ts.FunctionFlags.Async) !== 0); } - - return undefined; } - function getContextualReturnType(functionDecl: ts.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 as ts.FunctionExpression); - if (signature && !isResolvingReturnTypeOfSignature(signature)) { - return getReturnTypeOfSignature(signature); + return undefined; + } + + function isInParameterInitializerBeforeContainingFunction(node: ts.Node) { + let inBindingInitializer = false; + while (node.parent && !ts.isFunctionLike(node.parent)) { + if (ts.isParameter(node.parent) && (inBindingInitializer || node.parent.initializer === node)) { + return true; } - const iife = ts.getImmediatelyInvokedFunctionExpression(functionDecl); - if (iife) { - return getContextualType(iife); + if (ts.isBindingElement(node.parent) && node.parent.initializer === node) { + inBindingInitializer = true; } - return undefined; + + node = node.parent; } - // In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter. - function getContextualTypeForArgument(callTarget: ts.CallLikeExpression, arg: ts.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); + return false; + } + + function getContextualIterationType(kind: IterationTypeKind, functionDecl: ts.SignatureDeclaration): ts.Type | undefined { + const isAsync = !!(ts.getFunctionFlags(functionDecl) & ts.FunctionFlags.Async); + const contextualReturnType = getContextualReturnType(functionDecl); + if (contextualReturnType) { + return getIterationTypeOfGeneratorFunctionReturnType(kind, contextualReturnType, isAsync) + || undefined; } - function getContextualTypeForArgumentAtIndex(callTarget: ts.CallLikeExpression, argIndex: number): ts.Type { - if (ts.isImportCall(callTarget)) { - return argIndex === 0 ? stringType : - argIndex === 1 ? getGlobalImportCallOptionsType(/*reportErrors*/ false) : - anyType; - } + return undefined; + } - // 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); + function getContextualReturnType(functionDecl: ts.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 as ts.FunctionExpression); + if (signature && !isResolvingReturnTypeOfSignature(signature)) { + return getReturnTypeOfSignature(signature); + } + const iife = ts.getImmediatelyInvokedFunctionExpression(functionDecl); + if (iife) { + return getContextualType(iife); + } + return undefined; + } - if (ts.isJsxOpeningLikeElement(callTarget) && argIndex === 0) { - return getEffectiveFirstArgumentForJsxSignature(signature, callTarget); - } - const restIndex = signature.parameters.length - 1; - return signatureHasRestParameter(signature) && argIndex >= restIndex ? - getIndexedAccessType(getTypeOfSymbol(signature.parameters[restIndex]), getNumberLiteralType(argIndex - restIndex), ts.AccessFlags.Contextual) : - getTypeAtPosition(signature, argIndex); + // In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter. + function getContextualTypeForArgument(callTarget: ts.CallLikeExpression, arg: ts.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: ts.CallLikeExpression, argIndex: number): ts.Type { + if (ts.isImportCall(callTarget)) { + return argIndex === 0 ? stringType : + argIndex === 1 ? getGlobalImportCallOptionsType(/*reportErrors*/ false) : + anyType; } - function getContextualTypeForSubstitutionExpression(template: ts.TemplateExpression, substitutionExpression: ts.Expression) { - if (template.parent.kind === ts.SyntaxKind.TaggedTemplateExpression) { - return getContextualTypeForArgument(template.parent as ts.TaggedTemplateExpression, substitutionExpression); - } + // 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); - return undefined; + if (ts.isJsxOpeningLikeElement(callTarget) && argIndex === 0) { + return getEffectiveFirstArgumentForJsxSignature(signature, callTarget); } + const restIndex = signature.parameters.length - 1; + return signatureHasRestParameter(signature) && argIndex >= restIndex ? + getIndexedAccessType(getTypeOfSymbol(signature.parameters[restIndex]), getNumberLiteralType(argIndex - restIndex), ts.AccessFlags.Contextual) : + getTypeAtPosition(signature, argIndex); + } - function getContextualTypeForBinaryOperand(node: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { - const binaryExpression = node.parent as ts.BinaryExpression; - const { left, operatorToken, right } = binaryExpression; - switch (operatorToken.kind) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.AmpersandAmpersandEqualsToken: - case ts.SyntaxKind.BarBarEqualsToken: - case ts.SyntaxKind.QuestionQuestionEqualsToken: - return node === right ? getContextualTypeForAssignmentDeclaration(binaryExpression) : undefined; - case ts.SyntaxKind.BarBarToken: - case ts.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 && !ts.isDefaultedExpandoInitializer(binaryExpression)) ? - getTypeOfExpression(left) : type; - case ts.SyntaxKind.AmpersandAmpersandToken: - case ts.SyntaxKind.CommaToken: - return node === right ? getContextualType(binaryExpression, contextFlags) : undefined; - default: - return undefined; - } + function getContextualTypeForSubstitutionExpression(template: ts.TemplateExpression, substitutionExpression: ts.Expression) { + if (template.parent.kind === ts.SyntaxKind.TaggedTemplateExpression) { + return getContextualTypeForArgument(template.parent as ts.TaggedTemplateExpression, substitutionExpression); } - /** - * Try to find a resolved symbol for an expression without also resolving its type, as - * getSymbolAtLocation would (as that could be reentrant into contextual typing) - */ - function getSymbolForExpression(e: ts.Expression) { - if (e.symbol) { - return e.symbol; - } - if (ts.isIdentifier(e)) { - return getResolvedSymbol(e); - } - if (ts.isPropertyAccessExpression(e)) { - const lhsType = getTypeOfExpression(e.expression); - return ts.isPrivateIdentifier(e.name) ? tryGetPrivateIdentifierPropertyOfType(lhsType, e.name) : getPropertyOfType(lhsType, e.name.escapedText); - } - return undefined; + return undefined; + } + + function getContextualTypeForBinaryOperand(node: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { + const binaryExpression = node.parent as ts.BinaryExpression; + const { left, operatorToken, right } = binaryExpression; + switch (operatorToken.kind) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + return node === right ? getContextualTypeForAssignmentDeclaration(binaryExpression) : undefined; + case ts.SyntaxKind.BarBarToken: + case ts.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 && !ts.isDefaultedExpandoInitializer(binaryExpression)) ? + getTypeOfExpression(left) : type; + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.CommaToken: + return node === right ? getContextualType(binaryExpression, contextFlags) : undefined; + default: + return undefined; + } + } + + /** + * Try to find a resolved symbol for an expression without also resolving its type, as + * getSymbolAtLocation would (as that could be reentrant into contextual typing) + */ + function getSymbolForExpression(e: ts.Expression) { + if (e.symbol) { + return e.symbol; + } + if (ts.isIdentifier(e)) { + return getResolvedSymbol(e); + } + if (ts.isPropertyAccessExpression(e)) { + const lhsType = getTypeOfExpression(e.expression); + return ts.isPrivateIdentifier(e.name) ? tryGetPrivateIdentifierPropertyOfType(lhsType, e.name) : getPropertyOfType(lhsType, e.name.escapedText); + } + return undefined; - function tryGetPrivateIdentifierPropertyOfType(type: ts.Type, id: ts.PrivateIdentifier) { - const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(id.escapedText, id); - return lexicallyScopedSymbol && getPrivateIdentifierPropertyOfType(type, lexicallyScopedSymbol); - } + function tryGetPrivateIdentifierPropertyOfType(type: ts.Type, id: ts.PrivateIdentifier) { + const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(id.escapedText, id); + return lexicallyScopedSymbol && getPrivateIdentifierPropertyOfType(type, lexicallyScopedSymbol); } + } - // 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 getContextualTypeForAssignmentDeclaration(binaryExpression: ts.BinaryExpression): ts.Type | undefined { - const kind = ts.getAssignmentDeclarationKind(binaryExpression); - switch (kind) { - case ts.AssignmentDeclarationKind.None: - case ts.AssignmentDeclarationKind.ThisProperty: - const lhsSymbol = getSymbolForExpression(binaryExpression.left); - const decl = lhsSymbol && lhsSymbol.valueDeclaration; - // Unannotated, uninitialized property declarations have a type implied by their usage in the constructor. - // We avoid calling back into `getTypeOfExpression` and reentering contextual typing to avoid a bogus circularity error in that case. - if (decl && (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl))) { - const overallAnnotation = ts.getEffectiveTypeAnnotationNode(decl); - return (overallAnnotation && instantiateType(getTypeFromTypeNode(overallAnnotation), getSymbolLinks(lhsSymbol).mapper)) || - (decl.initializer && getTypeOfExpression(binaryExpression.left)); - } - if (kind === ts.AssignmentDeclarationKind.None) { - return getTypeOfExpression(binaryExpression.left); - } + // 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 getContextualTypeForAssignmentDeclaration(binaryExpression: ts.BinaryExpression): ts.Type | undefined { + const kind = ts.getAssignmentDeclarationKind(binaryExpression); + switch (kind) { + case ts.AssignmentDeclarationKind.None: + case ts.AssignmentDeclarationKind.ThisProperty: + const lhsSymbol = getSymbolForExpression(binaryExpression.left); + const decl = lhsSymbol && lhsSymbol.valueDeclaration; + // Unannotated, uninitialized property declarations have a type implied by their usage in the constructor. + // We avoid calling back into `getTypeOfExpression` and reentering contextual typing to avoid a bogus circularity error in that case. + if (decl && (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl))) { + const overallAnnotation = ts.getEffectiveTypeAnnotationNode(decl); + return (overallAnnotation && instantiateType(getTypeFromTypeNode(overallAnnotation), getSymbolLinks(lhsSymbol).mapper)) || + (decl.initializer && getTypeOfExpression(binaryExpression.left)); + } + if (kind === ts.AssignmentDeclarationKind.None) { + return getTypeOfExpression(binaryExpression.left); + } + return getContextualTypeForThisPropertyAssignment(binaryExpression); + case ts.AssignmentDeclarationKind.Property: + if (isPossiblyAliasedThisProperty(binaryExpression, kind)) { return getContextualTypeForThisPropertyAssignment(binaryExpression); - case ts.AssignmentDeclarationKind.Property: - if (isPossiblyAliasedThisProperty(binaryExpression, kind)) { - return getContextualTypeForThisPropertyAssignment(binaryExpression); - } - // 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`. - else if (!binaryExpression.left.symbol) { - return getTypeOfExpression(binaryExpression.left); + } + // 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`. + else if (!binaryExpression.left.symbol) { + return getTypeOfExpression(binaryExpression.left); + } + else { + const decl = binaryExpression.left.symbol.valueDeclaration; + if (!decl) { + return undefined; } - else { - const decl = binaryExpression.left.symbol.valueDeclaration; - if (!decl) { - return undefined; - } - const lhs = ts.cast(binaryExpression.left, ts.isAccessExpression); - const overallAnnotation = ts.getEffectiveTypeAnnotationNode(decl); - if (overallAnnotation) { - return getTypeFromTypeNode(overallAnnotation); - } - else if (ts.isIdentifier(lhs.expression)) { - const id = lhs.expression; - const parentSymbol = resolveName(id, id.escapedText, ts.SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true); - if (parentSymbol) { - const annotated = parentSymbol.valueDeclaration && ts.getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration); - if (annotated) { - const nameStr = ts.getElementOrPropertyAccessName(lhs); - if (nameStr !== undefined) { - return getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), nameStr); - } + const lhs = ts.cast(binaryExpression.left, ts.isAccessExpression); + const overallAnnotation = ts.getEffectiveTypeAnnotationNode(decl); + if (overallAnnotation) { + return getTypeFromTypeNode(overallAnnotation); + } + else if (ts.isIdentifier(lhs.expression)) { + const id = lhs.expression; + const parentSymbol = resolveName(id, id.escapedText, ts.SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true); + if (parentSymbol) { + const annotated = parentSymbol.valueDeclaration && ts.getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration); + if (annotated) { + const nameStr = ts.getElementOrPropertyAccessName(lhs); + if (nameStr !== undefined) { + return getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), nameStr); } - return undefined; } + return undefined; } - return ts.isInJSFile(decl) ? undefined : getTypeOfExpression(binaryExpression.left); } - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.Prototype: - case ts.AssignmentDeclarationKind.PrototypeProperty: - let valueDeclaration = binaryExpression.left.symbol?.valueDeclaration; - // falls through - case ts.AssignmentDeclarationKind.ModuleExports: - valueDeclaration ||= binaryExpression.symbol?.valueDeclaration; - const annotated = valueDeclaration && ts.getEffectiveTypeAnnotationNode(valueDeclaration); - return annotated ? getTypeFromTypeNode(annotated) : undefined; - case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: - case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: - case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: - return ts.Debug.fail("Does not apply"); - default: - return ts.Debug.assertNever(kind); - } + return ts.isInJSFile(decl) ? undefined : getTypeOfExpression(binaryExpression.left); + } + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.Prototype: + case ts.AssignmentDeclarationKind.PrototypeProperty: + let valueDeclaration = binaryExpression.left.symbol?.valueDeclaration; + // falls through + case ts.AssignmentDeclarationKind.ModuleExports: + valueDeclaration ||= binaryExpression.symbol?.valueDeclaration; + const annotated = valueDeclaration && ts.getEffectiveTypeAnnotationNode(valueDeclaration); + return annotated ? getTypeFromTypeNode(annotated) : undefined; + case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: + case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: + case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: + return ts.Debug.fail("Does not apply"); + default: + return ts.Debug.assertNever(kind); } + } - function isPossiblyAliasedThisProperty(declaration: ts.BinaryExpression, kind = ts.getAssignmentDeclarationKind(declaration)) { - if (kind === ts.AssignmentDeclarationKind.ThisProperty) { - return true; - } - if (!ts.isInJSFile(declaration) || kind !== ts.AssignmentDeclarationKind.Property || !ts.isIdentifier((declaration.left as ts.AccessExpression).expression)) { - return false; - } - const name = ((declaration.left as ts.AccessExpression).expression as ts.Identifier).escapedText; - const symbol = resolveName(declaration.left, name, ts.SymbolFlags.Value, undefined, undefined, /*isUse*/ true, /*excludeGlobals*/ true); - return ts.isThisInitializedDeclaration(symbol?.valueDeclaration); + function isPossiblyAliasedThisProperty(declaration: ts.BinaryExpression, kind = ts.getAssignmentDeclarationKind(declaration)) { + if (kind === ts.AssignmentDeclarationKind.ThisProperty) { + return true; + } + if (!ts.isInJSFile(declaration) || kind !== ts.AssignmentDeclarationKind.Property || !ts.isIdentifier((declaration.left as ts.AccessExpression).expression)) { + return false; } + const name = ((declaration.left as ts.AccessExpression).expression as ts.Identifier).escapedText; + const symbol = resolveName(declaration.left, name, ts.SymbolFlags.Value, undefined, undefined, /*isUse*/ true, /*excludeGlobals*/ true); + return ts.isThisInitializedDeclaration(symbol?.valueDeclaration); + } - function getContextualTypeForThisPropertyAssignment(binaryExpression: ts.BinaryExpression): ts.Type | undefined { - if (!binaryExpression.symbol) - return getTypeOfExpression(binaryExpression.left); - if (binaryExpression.symbol.valueDeclaration) { - const annotated = ts.getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration); - if (annotated) { - const type = getTypeFromTypeNode(annotated); - if (type) { - return type; - } + function getContextualTypeForThisPropertyAssignment(binaryExpression: ts.BinaryExpression): ts.Type | undefined { + if (!binaryExpression.symbol) + return getTypeOfExpression(binaryExpression.left); + if (binaryExpression.symbol.valueDeclaration) { + const annotated = ts.getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration); + if (annotated) { + const type = getTypeFromTypeNode(annotated); + if (type) { + return type; } } - const thisAccess = ts.cast(binaryExpression.left, ts.isAccessExpression); - if (!ts.isObjectLiteralMethod(ts.getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) { - return undefined; - } - const thisType = checkThisExpression(thisAccess.expression); - const nameStr = ts.getElementOrPropertyAccessName(thisAccess); - return nameStr !== undefined && getTypeOfPropertyOfContextualType(thisType, nameStr) || undefined; - } - - function isCircularMappedProperty(symbol: ts.Symbol) { - return !!(ts.getCheckFlags(symbol) & ts.CheckFlags.Mapped && !(symbol as ts.MappedSymbol).type && findResolutionCycleStartIndex(symbol, TypeSystemPropertyName.Type) >= 0); + const thisAccess = ts.cast(binaryExpression.left, ts.isAccessExpression); + if (!ts.isObjectLiteralMethod(ts.getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) { + return undefined; } + const thisType = checkThisExpression(thisAccess.expression); + const nameStr = ts.getElementOrPropertyAccessName(thisAccess); + return nameStr !== undefined && getTypeOfPropertyOfContextualType(thisType, nameStr) || undefined; - function getTypeOfPropertyOfContextualType(type: ts.Type, name: ts.__String, nameType?: ts.Type) { - return mapType(type, (t): ts.Type | undefined => { - if (t.flags & ts.TypeFlags.Intersection) { - const intersection = t as ts.IntersectionType; - let newTypes = ts.mapDefined(intersection.types, getTypeOfConcretePropertyOfContextualType); - if (newTypes.length > 0) { - return getIntersectionType(newTypes); - } - newTypes = ts.mapDefined(intersection.types, getTypeOfApplicableIndexInfoOfContextualType); - if (newTypes.length > 0) { - return getIntersectionType(newTypes); - } - return undefined; - } - const concretePropertyType = getTypeOfConcretePropertyOfContextualType(t); - if (concretePropertyType) { - return concretePropertyType; - } - return getTypeOfApplicableIndexInfoOfContextualType(t); - }, /*noReductions*/ true); + } - function getTypeOfConcretePropertyOfContextualType(t: ts.Type) { - if (isGenericMappedType(t) && !t.declaration.nameType) { - const constraint = getConstraintTypeFromMappedType(t); - const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint; - const propertyNameType = nameType || getStringLiteralType(ts.unescapeLeadingUnderscores(name)); - if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) { - return substituteIndexedMappedType(t, propertyNameType); - } - return undefined; + function isCircularMappedProperty(symbol: ts.Symbol) { + return !!(ts.getCheckFlags(symbol) & ts.CheckFlags.Mapped && !(symbol as ts.MappedSymbol).type && findResolutionCycleStartIndex(symbol, TypeSystemPropertyName.Type) >= 0); + } + + function getTypeOfPropertyOfContextualType(type: ts.Type, name: ts.__String, nameType?: ts.Type) { + return mapType(type, (t): ts.Type | undefined => { + if (t.flags & ts.TypeFlags.Intersection) { + const intersection = t as ts.IntersectionType; + let newTypes = ts.mapDefined(intersection.types, getTypeOfConcretePropertyOfContextualType); + if (newTypes.length > 0) { + return getIntersectionType(newTypes); } - if (t.flags & ts.TypeFlags.StructuredType) { - const prop = getPropertyOfType(t, name); - if (prop) { - return isCircularMappedProperty(prop) ? undefined : getTypeOfSymbol(prop); - } - if (isTupleType(t)) { - const restType = getRestTypeOfTupleType(t); - if (restType && ts.isNumericLiteralName(name) && +name >= 0) { - return restType; - } - } + newTypes = ts.mapDefined(intersection.types, getTypeOfApplicableIndexInfoOfContextualType); + if (newTypes.length > 0) { + return getIntersectionType(newTypes); } return undefined; } - function getTypeOfApplicableIndexInfoOfContextualType(t: ts.Type) { - if (!(t.flags & ts.TypeFlags.StructuredType)) { - return undefined; - } - return findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType || getStringLiteralType(ts.unescapeLeadingUnderscores(name)))?.type; + const concretePropertyType = getTypeOfConcretePropertyOfContextualType(t); + if (concretePropertyType) { + return concretePropertyType; } - } + return getTypeOfApplicableIndexInfoOfContextualType(t); + }, /*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: ts.MethodDeclaration, contextFlags?: ts.ContextFlags): ts.Type | undefined { - ts.Debug.assert(ts.isObjectLiteralMethod(node)); - if (node.flags & ts.NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further + function getTypeOfConcretePropertyOfContextualType(t: ts.Type) { + if (isGenericMappedType(t) && !t.declaration.nameType) { + const constraint = getConstraintTypeFromMappedType(t); + const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint; + const propertyNameType = nameType || getStringLiteralType(ts.unescapeLeadingUnderscores(name)); + if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) { + return substituteIndexedMappedType(t, propertyNameType); + } return undefined; } - return getContextualTypeForObjectLiteralElement(node, contextFlags); - } - - function getContextualTypeForObjectLiteralElement(element: ts.ObjectLiteralElementLike, contextFlags?: ts.ContextFlags) { - const objectLiteral = element.parent as ts.ObjectLiteralExpression; - const propertyAssignmentType = ts.isPropertyAssignment(element) && getContextualTypeForVariableLikeDeclaration(element); - if (propertyAssignmentType) { - return propertyAssignmentType; - } - const type = getApparentTypeOfContextualType(objectLiteral, contextFlags); - if (type) { - if (hasBindableName(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 symbol = getSymbolOfNode(element); - return getTypeOfPropertyOfContextualType(type, symbol.escapedName, getSymbolLinks(symbol).nameType); + if (t.flags & ts.TypeFlags.StructuredType) { + const prop = getPropertyOfType(t, name); + if (prop) { + return isCircularMappedProperty(prop) ? undefined : getTypeOfSymbol(prop); } - if (element.name) { - const nameType = getLiteralTypeFromPropertyName(element.name); - // We avoid calling getApplicableIndexInfo here because it performs potentially expensive intersection reduction. - return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType)?.type, /*noReductions*/ true); + if (isTupleType(t)) { + const restType = getRestTypeOfTupleType(t); + if (restType && ts.isNumericLiteralName(name) && +name >= 0) { + return restType; + } } } 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 ts.__String) - || mapType(arrayContextualType, t => getIteratedTypeOrElementType(IterationUse.Element, t, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false), - /*noReductions*/ true)); + function getTypeOfApplicableIndexInfoOfContextualType(t: ts.Type) { + if (!(t.flags & ts.TypeFlags.StructuredType)) { + return undefined; + } + return findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType || getStringLiteralType(ts.unescapeLeadingUnderscores(name)))?.type; } + } - // In a contextually typed conditional expression, the true/false expressions are contextually typed by the same type. - function getContextualTypeForConditionalOperand(node: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { - const conditional = node.parent as ts.ConditionalExpression; - return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional, contextFlags) : undefined; + // 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: ts.MethodDeclaration, contextFlags?: ts.ContextFlags): ts.Type | undefined { + ts.Debug.assert(ts.isObjectLiteralMethod(node)); + if (node.flags & ts.NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return undefined; } + return getContextualTypeForObjectLiteralElement(node, contextFlags); + } - function getContextualTypeForChildJsxExpression(node: ts.JsxElement, child: ts.JsxChild) { - const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); - // JSX expression is in children of JSX Element, we will look for an "children" attribute (we get the name from JSX.ElementAttributesProperty) - const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) { - return undefined; + function getContextualTypeForObjectLiteralElement(element: ts.ObjectLiteralElementLike, contextFlags?: ts.ContextFlags) { + const objectLiteral = element.parent as ts.ObjectLiteralExpression; + const propertyAssignmentType = ts.isPropertyAssignment(element) && getContextualTypeForVariableLikeDeclaration(element); + if (propertyAssignmentType) { + return propertyAssignmentType; + } + const type = getApparentTypeOfContextualType(objectLiteral, contextFlags); + if (type) { + if (hasBindableName(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 symbol = getSymbolOfNode(element); + return getTypeOfPropertyOfContextualType(type, symbol.escapedName, getSymbolLinks(symbol).nameType); + } + if (element.name) { + const nameType = getLiteralTypeFromPropertyName(element.name); + // We avoid calling getApplicableIndexInfo here because it performs potentially expensive intersection reduction. + return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType)?.type, /*noReductions*/ true); } - const realChildren = ts.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, getNumberLiteralType(childIndex)); - } - else { - return t; - } - }, /*noReductions*/ true)); } + return undefined; + } - function getContextualTypeForJsxExpression(node: ts.JsxExpression): ts.Type | undefined { - const exprParent = node.parent; - return ts.isJsxAttributeLike(exprParent) - ? getContextualType(node) - : ts.isJsxElement(exprParent) - ? getContextualTypeForChildJsxExpression(exprParent, node) - : 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 ts.__String) + || mapType(arrayContextualType, t => getIteratedTypeOrElementType(IterationUse.Element, t, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false), + /*noReductions*/ true)); + } - function getContextualTypeForJsxAttribute(attribute: ts.JsxAttribute | ts.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 (ts.isJsxAttribute(attribute)) { - const attributesType = getApparentTypeOfContextualType(attribute.parent); - if (!attributesType || isTypeAny(attributesType)) { - return undefined; - } - return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText); + // In a contextually typed conditional expression, the true/false expressions are contextually typed by the same type. + function getContextualTypeForConditionalOperand(node: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { + const conditional = node.parent as ts.ConditionalExpression; + return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional, contextFlags) : undefined; + } + + function getContextualTypeForChildJsxExpression(node: ts.JsxElement, child: ts.JsxChild) { + const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); + // JSX expression is in children of JSX Element, we will look for an "children" attribute (we get the name from JSX.ElementAttributesProperty) + const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) { + return undefined; + } + const realChildren = ts.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, getNumberLiteralType(childIndex)); } else { - return getContextualType(attribute.parent); + return t; } - } + }, /*noReductions*/ true)); + } - // 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: ts.Expression): boolean { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.UndefinedKeyword: - return true; - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ParenthesizedExpression: - return isPossiblyDiscriminantValue((node as ts.PropertyAccessExpression | ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.JsxExpression: - return !(node as ts.JsxExpression).expression || isPossiblyDiscriminantValue((node as ts.JsxExpression).expression!); + function getContextualTypeForJsxExpression(node: ts.JsxExpression): ts.Type | undefined { + const exprParent = node.parent; + return ts.isJsxAttributeLike(exprParent) + ? getContextualType(node) + : ts.isJsxElement(exprParent) + ? getContextualTypeForChildJsxExpression(exprParent, node) + : undefined; + } + + function getContextualTypeForJsxAttribute(attribute: ts.JsxAttribute | ts.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 (ts.isJsxAttribute(attribute)) { + const attributesType = getApparentTypeOfContextualType(attribute.parent); + if (!attributesType || isTypeAny(attributesType)) { + return undefined; } - return false; + return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText); } - function discriminateContextualTypeByObjectMembers(node: ts.ObjectLiteralExpression, contextualType: ts.UnionType) { - return getMatchingUnionConstituentForObjectLiteral(contextualType, node) || discriminateTypeByDiscriminableItems(contextualType, ts.concatenate(ts.map(ts.filter(node.properties, p => !!p.symbol && p.kind === ts.SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)), prop => ([() => getContextFreeTypeOfExpression((prop as ts.PropertyAssignment).initializer), prop.symbol.escapedName] as [ - () => ts.Type, - ts.__String - ])), ts.map(ts.filter(getPropertiesOfType(contextualType), s => !!(s.flags & ts.SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)), s => [() => undefinedType, s.escapedName] as [ - () => ts.Type, - ts.__String - ])), isTypeAssignableTo, contextualType); - } - function discriminateContextualTypeByJSXAttributes(node: ts.JsxAttributes, contextualType: ts.UnionType) { - return discriminateTypeByDiscriminableItems(contextualType, ts.concatenate(ts.map(ts.filter(node.properties, p => !!p.symbol && p.kind === ts.SyntaxKind.JsxAttribute && isDiscriminantProperty(contextualType, p.symbol.escapedName) && (!p.initializer || isPossiblyDiscriminantValue(p.initializer))), prop => ([!(prop as ts.JsxAttribute).initializer ? (() => trueType) : (() => getContextFreeTypeOfExpression((prop as ts.JsxAttribute).initializer!)), prop.symbol.escapedName] as [ - () => ts.Type, - ts.__String - ])), ts.map(ts.filter(getPropertiesOfType(contextualType), s => !!(s.flags & ts.SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)), s => [() => undefinedType, s.escapedName] as [ - () => ts.Type, - ts.__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: ts.Expression | ts.MethodDeclaration, contextFlags?: ts.ContextFlags): ts.Type | undefined { - const contextualType = ts.isObjectLiteralMethod(node) ? - getContextualTypeForObjectLiteralMethod(node, contextFlags) : - getContextualType(node, contextFlags); - const instantiatedType = instantiateContextualType(contextualType, node, contextFlags); - if (instantiatedType && !(contextFlags && contextFlags & ts.ContextFlags.NoConstraints && instantiatedType.flags & ts.TypeFlags.TypeVariable)) { - const apparentType = mapType(instantiatedType, getApparentType, /*noReductions*/ true); - return apparentType.flags & ts.TypeFlags.Union && ts.isObjectLiteralExpression(node) ? discriminateContextualTypeByObjectMembers(node, apparentType as ts.UnionType) : - apparentType.flags & ts.TypeFlags.Union && ts.isJsxAttributes(node) ? discriminateContextualTypeByJSXAttributes(node, apparentType as ts.UnionType) : - 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: ts.Type | undefined, node: ts.Node, contextFlags?: ts.ContextFlags): ts.Type | undefined { - if (contextualType && maybeTypeOfKind(contextualType, ts.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 && ts.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 & ts.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. We remove - // the 'boolean' type from the contextual type such that contextually typed boolean - // literals actually end up widening to 'boolean' (see #48363). - if (inferenceContext.returnMapper) { - const type = instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper); - return type.flags & ts.TypeFlags.Union && containsType((type as ts.UnionType).types, regularFalseType) && containsType((type as ts.UnionType).types, regularTrueType) ? - filterType(type, t => t !== regularFalseType && t !== regularTrueType) : - type; - } - } - } - return contextualType; + else { + return getContextualType(attribute.parent); } + } - // 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: ts.TypeMapper): ts.Type { - if (type.flags & ts.TypeFlags.Instantiable) { - return instantiateType(type, mapper); - } - if (type.flags & ts.TypeFlags.Union) { - return getUnionType(ts.map((type as ts.UnionType).types, t => instantiateInstantiableTypes(t, mapper)), ts.UnionReduction.None); - } - if (type.flags & ts.TypeFlags.Intersection) { - return getIntersectionType(ts.map((type as ts.IntersectionType).types, t => instantiateInstantiableTypes(t, mapper))); - } - return type; + // 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: ts.Expression): boolean { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.UndefinedKeyword: + return true; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ParenthesizedExpression: + return isPossiblyDiscriminantValue((node as ts.PropertyAccessExpression | ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.JsxExpression: + return !(node as ts.JsxExpression).expression || isPossiblyDiscriminantValue((node as ts.JsxExpression).expression!); + } + return false; + } + function discriminateContextualTypeByObjectMembers(node: ts.ObjectLiteralExpression, contextualType: ts.UnionType) { + return getMatchingUnionConstituentForObjectLiteral(contextualType, node) || discriminateTypeByDiscriminableItems(contextualType, ts.concatenate(ts.map(ts.filter(node.properties, p => !!p.symbol && p.kind === ts.SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)), prop => ([() => getContextFreeTypeOfExpression((prop as ts.PropertyAssignment).initializer), prop.symbol.escapedName] as [ + () => ts.Type, + ts.__String + ])), ts.map(ts.filter(getPropertiesOfType(contextualType), s => !!(s.flags & ts.SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)), s => [() => undefinedType, s.escapedName] as [ + () => ts.Type, + ts.__String + ])), isTypeAssignableTo, contextualType); + } + function discriminateContextualTypeByJSXAttributes(node: ts.JsxAttributes, contextualType: ts.UnionType) { + return discriminateTypeByDiscriminableItems(contextualType, ts.concatenate(ts.map(ts.filter(node.properties, p => !!p.symbol && p.kind === ts.SyntaxKind.JsxAttribute && isDiscriminantProperty(contextualType, p.symbol.escapedName) && (!p.initializer || isPossiblyDiscriminantValue(p.initializer))), prop => ([!(prop as ts.JsxAttribute).initializer ? (() => trueType) : (() => getContextFreeTypeOfExpression((prop as ts.JsxAttribute).initializer!)), prop.symbol.escapedName] as [ + () => ts.Type, + ts.__String + ])), ts.map(ts.filter(getPropertiesOfType(contextualType), s => !!(s.flags & ts.SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)), s => [() => undefinedType, s.escapedName] as [ + () => ts.Type, + ts.__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: ts.Expression | ts.MethodDeclaration, contextFlags?: ts.ContextFlags): ts.Type | undefined { + const contextualType = ts.isObjectLiteralMethod(node) ? + getContextualTypeForObjectLiteralMethod(node, contextFlags) : + getContextualType(node, contextFlags); + const instantiatedType = instantiateContextualType(contextualType, node, contextFlags); + if (instantiatedType && !(contextFlags && contextFlags & ts.ContextFlags.NoConstraints && instantiatedType.flags & ts.TypeFlags.TypeVariable)) { + const apparentType = mapType(instantiatedType, getApparentType, /*noReductions*/ true); + return apparentType.flags & ts.TypeFlags.Union && ts.isObjectLiteralExpression(node) ? discriminateContextualTypeByObjectMembers(node, apparentType as ts.UnionType) : + apparentType.flags & ts.TypeFlags.Union && ts.isJsxAttributes(node) ? discriminateContextualTypeByJSXAttributes(node, apparentType as ts.UnionType) : + apparentType; } + } - /** - * 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: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { - if (node.flags & ts.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 ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.BindingElement: - return getContextualTypeForInitializerExpression(node, contextFlags); - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.ReturnStatement: - return getContextualTypeForReturnExpression(node); - case ts.SyntaxKind.YieldExpression: - return getContextualTypeForYieldOperand(parent as ts.YieldExpression); - case ts.SyntaxKind.AwaitExpression: - return getContextualTypeForAwaitOperand(parent as ts.AwaitExpression, contextFlags); - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - return getContextualTypeForArgument(parent as ts.CallExpression | ts.NewExpression, node); - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.AsExpression: - return ts.isConstTypeReference((parent as ts.AssertionExpression).type) ? tryFindWhenConstTypeReference(parent as ts.AssertionExpression) : getTypeFromTypeNode((parent as ts.AssertionExpression).type); - case ts.SyntaxKind.BinaryExpression: - return getContextualTypeForBinaryOperand(node, contextFlags); - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - return getContextualTypeForObjectLiteralElement(parent as ts.PropertyAssignment | ts.ShorthandPropertyAssignment, contextFlags); - case ts.SyntaxKind.SpreadAssignment: - return getContextualType(parent.parent as ts.ObjectLiteralExpression, contextFlags); - case ts.SyntaxKind.ArrayLiteralExpression: { - const arrayLiteral = parent as ts.ArrayLiteralExpression; - const type = getApparentTypeOfContextualType(arrayLiteral, contextFlags); - return getContextualTypeForElementExpression(type, ts.indexOfNode(arrayLiteral.elements, node)); - } - case ts.SyntaxKind.ConditionalExpression: - return getContextualTypeForConditionalOperand(node, contextFlags); - case ts.SyntaxKind.TemplateSpan: - ts.Debug.assert(parent.parent.kind === ts.SyntaxKind.TemplateExpression); - return getContextualTypeForSubstitutionExpression(parent.parent as ts.TemplateExpression, node); - case ts.SyntaxKind.ParenthesizedExpression: { - // Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast. - const tag = ts.isInJSFile(parent) ? ts.getJSDocTypeTag(parent) : undefined; - return !tag ? getContextualType(parent as ts.ParenthesizedExpression, contextFlags) : - ts.isJSDocTypeTag(tag) && ts.isConstTypeReference(tag.typeExpression.type) ? tryFindWhenConstTypeReference(parent as ts.ParenthesizedExpression) : - getTypeFromTypeNode(tag.typeExpression.type); - } - case ts.SyntaxKind.NonNullExpression: - return getContextualType(parent as ts.NonNullExpression, contextFlags); - case ts.SyntaxKind.ExportAssignment: - return tryGetTypeFromEffectiveTypeNode(parent as ts.ExportAssignment); - case ts.SyntaxKind.JsxExpression: - return getContextualTypeForJsxExpression(parent as ts.JsxExpression); - case ts.SyntaxKind.JsxAttribute: - case ts.SyntaxKind.JsxSpreadAttribute: - return getContextualTypeForJsxAttribute(parent as ts.JsxAttribute | ts.JsxSpreadAttribute); - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxSelfClosingElement: - return getContextualJsxElementAttributesType(parent as ts.JsxOpeningLikeElement, contextFlags); - } - return undefined; + // 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: ts.Node, contextFlags?: ts.ContextFlags): ts.Type | undefined { + if (contextualType && maybeTypeOfKind(contextualType, ts.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 && ts.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 & ts.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. We remove + // the 'boolean' type from the contextual type such that contextually typed boolean + // literals actually end up widening to 'boolean' (see #48363). + if (inferenceContext.returnMapper) { + const type = instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper); + return type.flags & ts.TypeFlags.Union && containsType((type as ts.UnionType).types, regularFalseType) && containsType((type as ts.UnionType).types, regularTrueType) ? + filterType(type, t => t !== regularFalseType && t !== regularTrueType) : + type; + } + } + } + return contextualType; + } - function tryFindWhenConstTypeReference(node: ts.Expression) { - return getContextualType(node); - } + // 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: ts.TypeMapper): ts.Type { + if (type.flags & ts.TypeFlags.Instantiable) { + return instantiateType(type, mapper); + } + if (type.flags & ts.TypeFlags.Union) { + return getUnionType(ts.map((type as ts.UnionType).types, t => instantiateInstantiableTypes(t, mapper)), ts.UnionReduction.None); } + if (type.flags & ts.TypeFlags.Intersection) { + return getIntersectionType(ts.map((type as ts.IntersectionType).types, t => instantiateInstantiableTypes(t, mapper))); + } + return type; + } - function getInferenceContext(node: ts.Node) { - const ancestor = ts.findAncestor(node, n => !!n.inferenceContext); - return ancestor && ancestor.inferenceContext!; + /** + * 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: ts.Expression, contextFlags?: ts.ContextFlags): ts.Type | undefined { + if (node.flags & ts.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 ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.BindingElement: + return getContextualTypeForInitializerExpression(node, contextFlags); + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.ReturnStatement: + return getContextualTypeForReturnExpression(node); + case ts.SyntaxKind.YieldExpression: + return getContextualTypeForYieldOperand(parent as ts.YieldExpression); + case ts.SyntaxKind.AwaitExpression: + return getContextualTypeForAwaitOperand(parent as ts.AwaitExpression, contextFlags); + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + return getContextualTypeForArgument(parent as ts.CallExpression | ts.NewExpression, node); + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.AsExpression: + return ts.isConstTypeReference((parent as ts.AssertionExpression).type) ? tryFindWhenConstTypeReference(parent as ts.AssertionExpression) : getTypeFromTypeNode((parent as ts.AssertionExpression).type); + case ts.SyntaxKind.BinaryExpression: + return getContextualTypeForBinaryOperand(node, contextFlags); + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + return getContextualTypeForObjectLiteralElement(parent as ts.PropertyAssignment | ts.ShorthandPropertyAssignment, contextFlags); + case ts.SyntaxKind.SpreadAssignment: + return getContextualType(parent.parent as ts.ObjectLiteralExpression, contextFlags); + case ts.SyntaxKind.ArrayLiteralExpression: { + const arrayLiteral = parent as ts.ArrayLiteralExpression; + const type = getApparentTypeOfContextualType(arrayLiteral, contextFlags); + return getContextualTypeForElementExpression(type, ts.indexOfNode(arrayLiteral.elements, node)); + } + case ts.SyntaxKind.ConditionalExpression: + return getContextualTypeForConditionalOperand(node, contextFlags); + case ts.SyntaxKind.TemplateSpan: + ts.Debug.assert(parent.parent.kind === ts.SyntaxKind.TemplateExpression); + return getContextualTypeForSubstitutionExpression(parent.parent as ts.TemplateExpression, node); + case ts.SyntaxKind.ParenthesizedExpression: { + // Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast. + const tag = ts.isInJSFile(parent) ? ts.getJSDocTypeTag(parent) : undefined; + return !tag ? getContextualType(parent as ts.ParenthesizedExpression, contextFlags) : + ts.isJSDocTypeTag(tag) && ts.isConstTypeReference(tag.typeExpression.type) ? tryFindWhenConstTypeReference(parent as ts.ParenthesizedExpression) : + getTypeFromTypeNode(tag.typeExpression.type); + } + case ts.SyntaxKind.NonNullExpression: + return getContextualType(parent as ts.NonNullExpression, contextFlags); + case ts.SyntaxKind.ExportAssignment: + return tryGetTypeFromEffectiveTypeNode(parent as ts.ExportAssignment); + case ts.SyntaxKind.JsxExpression: + return getContextualTypeForJsxExpression(parent as ts.JsxExpression); + case ts.SyntaxKind.JsxAttribute: + case ts.SyntaxKind.JsxSpreadAttribute: + return getContextualTypeForJsxAttribute(parent as ts.JsxAttribute | ts.JsxSpreadAttribute); + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxSelfClosingElement: + return getContextualJsxElementAttributesType(parent as ts.JsxOpeningLikeElement, contextFlags); + } + return undefined; + + function tryFindWhenConstTypeReference(node: ts.Expression) { + return getContextualType(node); } + } - function getContextualJsxElementAttributesType(node: ts.JsxOpeningLikeElement, contextFlags?: ts.ContextFlags) { - if (ts.isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ts.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 getInferenceContext(node: ts.Node) { + const ancestor = ts.findAncestor(node, n => !!n.inferenceContext); + return ancestor && ancestor.inferenceContext!; + } + + function getContextualJsxElementAttributesType(node: ts.JsxOpeningLikeElement, contextFlags?: ts.ContextFlags) { + if (ts.isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ts.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: ts.JsxOpeningLikeElement) { + return getJsxReferenceKind(node) !== ts.JsxReferenceKind.Component + ? getJsxPropsTypeFromCallSignature(signature, node) + : getJsxPropsTypeFromClassType(signature, node); + } - function getEffectiveFirstArgumentForJsxSignature(signature: ts.Signature, node: ts.JsxOpeningLikeElement) { - return getJsxReferenceKind(node) !== ts.JsxReferenceKind.Component - ? getJsxPropsTypeFromCallSignature(signature, node) - : getJsxPropsTypeFromClassType(signature, node); + function getJsxPropsTypeFromCallSignature(sig: ts.Signature, context: ts.JsxOpeningLikeElement) { + let propsType = getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType); + propsType = getJsxManagedAttributesFromLocatedAttributes(context, getJsxNamespaceAt(context), propsType); + const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); + if (!isErrorType(intrinsicAttribs)) { + propsType = intersectTypes(intrinsicAttribs, propsType); } + return propsType; + } - function getJsxPropsTypeFromCallSignature(sig: ts.Signature, context: ts.JsxOpeningLikeElement) { - let propsType = getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType); - propsType = getJsxManagedAttributesFromLocatedAttributes(context, getJsxNamespaceAt(context), propsType); - const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); - if (!isErrorType(intrinsicAttribs)) { - propsType = intersectTypes(intrinsicAttribs, propsType); - } - return propsType; - } - - function getJsxPropsTypeForSignatureFromMember(sig: ts.Signature, forcedLookupLocation: ts.__String) { - if (sig.compositeSignatures) { - // 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.compositeSignatures) { - const instance = getReturnTypeOfSignature(signature); - if (isTypeAny(instance)) { - return instance; - } - const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation); - if (!propType) { - return; - } - results.push(propType); + function getJsxPropsTypeForSignatureFromMember(sig: ts.Signature, forcedLookupLocation: ts.__String) { + if (sig.compositeSignatures) { + // 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.compositeSignatures) { + const instance = getReturnTypeOfSignature(signature); + if (isTypeAny(instance)) { + return instance; + } + const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation); + if (!propType) { + return; } - return getIntersectionType(results); // Same result for both union and intersection signatures + results.push(propType); } - const instanceType = getReturnTypeOfSignature(sig); - return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); + return getIntersectionType(results); // Same result for both union and intersection signatures } + const instanceType = getReturnTypeOfSignature(sig); + return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); + } - function getStaticTypeOfReferencedJsxConstructor(context: ts.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 & ts.TypeFlags.StringLiteral) { - const result = getIntrinsicAttributesTypeFromStringLiteralType(tagType as ts.StringLiteralType, context); - if (!result) { - return errorType; - } - const fakeSignature = createSignatureForJSXIntrinsic(context, result); - return getOrCreateTypeFromSignature(fakeSignature); + function getStaticTypeOfReferencedJsxConstructor(context: ts.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 & ts.TypeFlags.StringLiteral) { + const result = getIntrinsicAttributesTypeFromStringLiteralType(tagType as ts.StringLiteralType, context); + if (!result) { + return errorType; } - return tagType; + const fakeSignature = createSignatureForJSXIntrinsic(context, result); + return getOrCreateTypeFromSignature(fakeSignature); } + return tagType; + } - function getJsxManagedAttributesFromLocatedAttributes(context: ts.JsxOpeningLikeElement, ns: ts.Symbol, attributesType: ts.Type) { - const managedSym = getJsxLibraryManagedAttributes(ns); - if (managedSym) { - const declaredManagedType = getDeclaredTypeOfSymbol(managedSym); // fetches interface type, or initializes symbol links type parmaeters - const ctorType = getStaticTypeOfReferencedJsxConstructor(context); - if (managedSym.flags & ts.SymbolFlags.TypeAlias) { - const params = getSymbolLinks(managedSym).typeParameters; - if (ts.length(params) >= 2) { - const args = fillMissingTypeArguments([ctorType, attributesType], params, 2, ts.isInJSFile(context)); - return getTypeAliasInstantiation(managedSym, args); - } - } - if (ts.length((declaredManagedType as ts.GenericType).typeParameters) >= 2) { - const args = fillMissingTypeArguments([ctorType, attributesType], (declaredManagedType as ts.GenericType).typeParameters, 2, ts.isInJSFile(context)); - return createTypeReference((declaredManagedType as ts.GenericType), args); + function getJsxManagedAttributesFromLocatedAttributes(context: ts.JsxOpeningLikeElement, ns: ts.Symbol, attributesType: ts.Type) { + const managedSym = getJsxLibraryManagedAttributes(ns); + if (managedSym) { + const declaredManagedType = getDeclaredTypeOfSymbol(managedSym); // fetches interface type, or initializes symbol links type parmaeters + const ctorType = getStaticTypeOfReferencedJsxConstructor(context); + if (managedSym.flags & ts.SymbolFlags.TypeAlias) { + const params = getSymbolLinks(managedSym).typeParameters; + if (ts.length(params) >= 2) { + const args = fillMissingTypeArguments([ctorType, attributesType], params, 2, ts.isInJSFile(context)); + return getTypeAliasInstantiation(managedSym, args); } } - return attributesType; + if (ts.length((declaredManagedType as ts.GenericType).typeParameters) >= 2) { + const args = fillMissingTypeArguments([ctorType, attributesType], (declaredManagedType as ts.GenericType).typeParameters, 2, ts.isInJSFile(context)); + return createTypeReference((declaredManagedType as ts.GenericType), args); + } } + return attributesType; + } - function getJsxPropsTypeFromClassType(sig: ts.Signature, context: ts.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 && !!ts.length(context.attributes.properties)) { - error(context, ts.Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, ts.unescapeLeadingUnderscores(forcedLookupLocation)); - } - return unknownType; + function getJsxPropsTypeFromClassType(sig: ts.Signature, context: ts.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 && !!ts.length(context.attributes.properties)) { + error(context, ts.Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, ts.unescapeLeadingUnderscores(forcedLookupLocation)); } + return unknownType; + } - attributesType = getJsxManagedAttributesFromLocatedAttributes(context, ns, attributesType); + attributesType = getJsxManagedAttributesFromLocatedAttributes(context, ns, attributesType); - if (isTypeAny(attributesType)) { - // Props is of type 'any' or unknown - return 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 (!isErrorType(intrinsicClassAttribs)) { + const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); + const hostClassType = getReturnTypeOfSignature(sig); + apparentAttributesType = intersectTypes(typeParams + ? createTypeReference(intrinsicClassAttribs as ts.GenericType, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), ts.isInJSFile(context))) + : intrinsicClassAttribs, apparentAttributesType); } - else { - // Normal case -- add in IntrinsicClassElements and IntrinsicElements - let apparentAttributesType = attributesType; - const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes, context); - if (!isErrorType(intrinsicClassAttribs)) { - const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); - const hostClassType = getReturnTypeOfSignature(sig); - apparentAttributesType = intersectTypes(typeParams - ? createTypeReference(intrinsicClassAttribs as ts.GenericType, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), ts.isInJSFile(context))) - : intrinsicClassAttribs, apparentAttributesType); - } - - const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); - if (!isErrorType(intrinsicAttribs)) { - apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); - } - return apparentAttributesType; + const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); + if (!isErrorType(intrinsicAttribs)) { + apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); } + + return apparentAttributesType; } + } - function getIntersectedSignatures(signatures: readonly ts.Signature[]) { - return ts.getStrictOptionValue(compilerOptions, "noImplicitAny") - ? ts.reduceLeft(signatures, (left, right) => left === right || !left ? left - : compareTypeParametersIdentical(left.typeParameters, right.typeParameters) ? combineSignaturesOfIntersectionMembers(left, right) - : undefined) - : undefined; + function getIntersectedSignatures(signatures: readonly ts.Signature[]) { + return ts.getStrictOptionValue(compilerOptions, "noImplicitAny") + ? ts.reduceLeft(signatures, (left, right) => left === right || !left ? left + : compareTypeParametersIdentical(left.typeParameters, right.typeParameters) ? combineSignaturesOfIntersectionMembers(left, right) + : undefined) + : undefined; + } + + function combineIntersectionThisParam(left: ts.Symbol | undefined, right: ts.Symbol | undefined, mapper: ts.TypeMapper | 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 + // pessimistic when contextual typing, for now, we'll union the `this` types. + const thisType = getUnionType([getTypeOfSymbol(left), instantiateType(getTypeOfSymbol(right), mapper)]); + return createSymbolWithType(left, thisType); + } - function combineIntersectionThisParam(left: ts.Symbol | undefined, right: ts.Symbol | undefined, mapper: ts.TypeMapper | 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 - // pessimistic when contextual typing, for now, we'll union the `this` types. - const thisType = getUnionType([getTypeOfSymbol(left), instantiateType(getTypeOfSymbol(right), mapper)]); - return createSymbolWithType(left, thisType); - } - - function combineIntersectionParameters(left: ts.Signature, right: ts.Signature, mapper: ts.TypeMapper | undefined) { - 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++) { - let longestParamType = tryGetTypeAtPosition(longest, i)!; - if (longest === right) { - longestParamType = instantiateType(longestParamType, mapper); - } - let shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; - if (shorter === right) { - shorterParamType = instantiateType(shorterParamType, mapper); - } - const unionParamType = getUnionType([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(ts.SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? ts.SymbolFlags.Optional : 0), paramName || `arg${i}` as ts.__String); - paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; - params[i] = paramSymbol; - } - if (needsExtraRestElement) { - const restParamSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "args" as ts.__String); - restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); - if (shorter === right) { - restParamSymbol.type = instantiateType(restParamSymbol.type, mapper); - } - params[longestCount] = restParamSymbol; - } - return params; - } - - function combineSignaturesOfIntersectionMembers(left: ts.Signature, right: ts.Signature): ts.Signature { - const typeParams = left.typeParameters || right.typeParameters; - let paramMapper: ts.TypeMapper | undefined; - if (left.typeParameters && right.typeParameters) { - paramMapper = createTypeMapper(right.typeParameters, left.typeParameters); - // We just use the type parameter defaults from the first signature - } - const declaration = left.declaration; - const params = combineIntersectionParameters(left, right, paramMapper); - const thisParam = combineIntersectionThisParam(left.thisParameter, right.thisParameter, paramMapper); - const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); - const result = createSignature(declaration, typeParams, thisParam, params, - /*resolvedReturnType*/ undefined, - /*resolvedTypePredicate*/ undefined, minArgCount, (left.flags | right.flags) & ts.SignatureFlags.PropagatingFlags); - result.compositeKind = ts.TypeFlags.Intersection; - result.compositeSignatures = ts.concatenate(left.compositeKind === ts.TypeFlags.Intersection && left.compositeSignatures || [left], [right]); - if (paramMapper) { - result.mapper = left.compositeKind === ts.TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; + function combineIntersectionParameters(left: ts.Signature, right: ts.Signature, mapper: ts.TypeMapper | undefined) { + 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++) { + let longestParamType = tryGetTypeAtPosition(longest, i)!; + if (longest === right) { + longestParamType = instantiateType(longestParamType, mapper); + } + let shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; + if (shorter === right) { + shorterParamType = instantiateType(shorterParamType, mapper); + } + const unionParamType = getUnionType([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(ts.SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? ts.SymbolFlags.Optional : 0), paramName || `arg${i}` as ts.__String); + paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; + params[i] = paramSymbol; + } + if (needsExtraRestElement) { + const restParamSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "args" as ts.__String); + restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); + if (shorter === right) { + restParamSymbol.type = instantiateType(restParamSymbol.type, mapper); } - return result; + params[longestCount] = restParamSymbol; } + return params; + } - // 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: ts.SignatureDeclaration): ts.Signature | undefined { - const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); - const applicableByArity = ts.filter(signatures, s => !isAritySmaller(s, node)); - return applicableByArity.length === 1 ? applicableByArity[0] : getIntersectedSignatures(applicableByArity); - } + function combineSignaturesOfIntersectionMembers(left: ts.Signature, right: ts.Signature): ts.Signature { + const typeParams = left.typeParameters || right.typeParameters; + let paramMapper: ts.TypeMapper | undefined; + if (left.typeParameters && right.typeParameters) { + paramMapper = createTypeMapper(right.typeParameters, left.typeParameters); + // We just use the type parameter defaults from the first signature + } + const declaration = left.declaration; + const params = combineIntersectionParameters(left, right, paramMapper); + const thisParam = combineIntersectionThisParam(left.thisParameter, right.thisParameter, paramMapper); + const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); + const result = createSignature(declaration, typeParams, thisParam, params, + /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, minArgCount, (left.flags | right.flags) & ts.SignatureFlags.PropagatingFlags); + result.compositeKind = ts.TypeFlags.Intersection; + result.compositeSignatures = ts.concatenate(left.compositeKind === ts.TypeFlags.Intersection && left.compositeSignatures || [left], [right]); + if (paramMapper) { + result.mapper = left.compositeKind === ts.TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; + } + return result; + } - /** If the contextual signature has fewer parameters than the function expression, do not use it */ - function isAritySmaller(signature: ts.Signature, target: ts.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 (target.parameters.length && ts.parameterIsThisKeyword(target.parameters[0])) { - targetParameterCount--; + // 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: ts.SignatureDeclaration): ts.Signature | undefined { + const signatures = getSignaturesOfType(type, ts.SignatureKind.Call); + const applicableByArity = ts.filter(signatures, s => !isAritySmaller(s, node)); + return applicableByArity.length === 1 ? applicableByArity[0] : getIntersectedSignatures(applicableByArity); + } + + /** If the contextual signature has fewer parameters than the function expression, do not use it */ + function isAritySmaller(signature: ts.Signature, target: ts.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; } - return !hasEffectiveRestParameter(signature) && getParameterCount(signature) < targetParameterCount; } - - function getContextualSignatureForFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): ts.Signature | undefined { - // Only function expressions, arrow functions, and object literal methods are contextually typed. - return ts.isFunctionExpressionOrArrowFunction(node) || ts.isObjectLiteralMethod(node) - ? getContextualSignature(node as ts.FunctionExpression) - : undefined; + if (target.parameters.length && ts.parameterIsThisKeyword(target.parameters[0])) { + targetParameterCount--; } + return !hasEffectiveRestParameter(signature) && getParameterCount(signature) < targetParameterCount; + } - // 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: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration): ts.Signature | undefined { - ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); - const typeTagSignature = getSignatureOfTypeTag(node); - if (typeTagSignature) { - return typeTagSignature; - } - const type = getApparentTypeOfContextualType(node, ts.ContextFlags.Signature); - if (!type) { - return undefined; - } - if (!(type.flags & ts.TypeFlags.Union)) { - return getContextualCallSignature(type, node); - } - let signatureList: ts.Signature[] | undefined; - const types = (type as ts.UnionType).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); - } + function getContextualSignatureForFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): ts.Signature | undefined { + // Only function expressions, arrow functions, and object literal methods are contextually typed. + return ts.isFunctionExpressionOrArrowFunction(node) || ts.isObjectLiteralMethod(node) + ? getContextualSignature(node as ts.FunctionExpression) + : 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: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration): ts.Signature | undefined { + ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); + const typeTagSignature = getSignatureOfTypeTag(node); + if (typeTagSignature) { + return typeTagSignature; + } + const type = getApparentTypeOfContextualType(node, ts.ContextFlags.Signature); + if (!type) { + return undefined; + } + if (!(type.flags & ts.TypeFlags.Union)) { + return getContextualCallSignature(type, node); + } + let signatureList: ts.Signature[] | undefined; + const types = (type as ts.UnionType).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); } } + // 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); + } + } - function checkSpreadExpression(node: ts.SpreadElement, checkMode?: CheckMode): ts.Type { - if (languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, compilerOptions.downlevelIteration ? ts.ExternalEmitHelpers.SpreadIncludes : ts.ExternalEmitHelpers.SpreadArray); - } - - const arrayOrIterableType = checkExpression(node.expression, checkMode); - return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression); + function checkSpreadExpression(node: ts.SpreadElement, checkMode?: CheckMode): ts.Type { + if (languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, compilerOptions.downlevelIteration ? ts.ExternalEmitHelpers.SpreadIncludes : ts.ExternalEmitHelpers.SpreadArray); } - function checkSyntheticExpression(node: ts.SyntheticExpression): ts.Type { - return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type; - } + const arrayOrIterableType = checkExpression(node.expression, checkMode); + return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression); + } - function hasDefaultValue(node: ts.BindingElement | ts.Expression): boolean { - return (node.kind === ts.SyntaxKind.BindingElement && !!(node as ts.BindingElement).initializer) || - (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken); - } + function checkSyntheticExpression(node: ts.SyntheticExpression): ts.Type { + return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type; + } - function checkArrayLiteral(node: ts.ArrayLiteralExpression, checkMode: CheckMode | undefined, forceTuple: boolean | undefined): ts.Type { - const elements = node.elements; - const elementCount = elements.length; - const elementTypes: ts.Type[] = []; - const elementFlags: ts.ElementFlags[] = []; - const contextualType = getApparentTypeOfContextualType(node); - const inDestructuringPattern = ts.isAssignmentTarget(node); - const inConstContext = isConstContext(node); - let hasOmittedExpression = false; - for (let i = 0; i < elementCount; i++) { - const e = elements[i]; - if (e.kind === ts.SyntaxKind.SpreadElement) { - if (languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(e, compilerOptions.downlevelIteration ? ts.ExternalEmitHelpers.SpreadIncludes : ts.ExternalEmitHelpers.SpreadArray); - } - const spreadType = checkExpression((e as ts.SpreadElement).expression, checkMode, forceTuple); - if (isArrayLikeType(spreadType)) { - elementTypes.push(spreadType); - elementFlags.push(ts.ElementFlags.Variadic); - } - else if (inDestructuringPattern) { - // 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, numberType) || - getIteratedTypeOrElementType(IterationUse.Destructuring, spreadType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false) || - unknownType; - elementTypes.push(restElementType); - elementFlags.push(ts.ElementFlags.Rest); - } - else { - elementTypes.push(checkIteratedTypeOrElementType(IterationUse.Spread, spreadType, undefinedType, (e as ts.SpreadElement).expression)); - elementFlags.push(ts.ElementFlags.Rest); - } + function hasDefaultValue(node: ts.BindingElement | ts.Expression): boolean { + return (node.kind === ts.SyntaxKind.BindingElement && !!(node as ts.BindingElement).initializer) || + (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken); + } + + function checkArrayLiteral(node: ts.ArrayLiteralExpression, checkMode: CheckMode | undefined, forceTuple: boolean | undefined): ts.Type { + const elements = node.elements; + const elementCount = elements.length; + const elementTypes: ts.Type[] = []; + const elementFlags: ts.ElementFlags[] = []; + const contextualType = getApparentTypeOfContextualType(node); + const inDestructuringPattern = ts.isAssignmentTarget(node); + const inConstContext = isConstContext(node); + let hasOmittedExpression = false; + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + if (e.kind === ts.SyntaxKind.SpreadElement) { + if (languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(e, compilerOptions.downlevelIteration ? ts.ExternalEmitHelpers.SpreadIncludes : ts.ExternalEmitHelpers.SpreadArray); } - else if (exactOptionalPropertyTypes && e.kind === ts.SyntaxKind.OmittedExpression) { - hasOmittedExpression = true; - elementTypes.push(missingType); - elementFlags.push(ts.ElementFlags.Optional); + const spreadType = checkExpression((e as ts.SpreadElement).expression, checkMode, forceTuple); + if (isArrayLikeType(spreadType)) { + elementTypes.push(spreadType); + elementFlags.push(ts.ElementFlags.Variadic); + } + else if (inDestructuringPattern) { + // 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, numberType) || + getIteratedTypeOrElementType(IterationUse.Destructuring, spreadType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false) || + unknownType; + elementTypes.push(restElementType); + elementFlags.push(ts.ElementFlags.Rest); } else { - const elementContextualType = getContextualTypeForElementExpression(contextualType, elementTypes.length); - const type = checkExpressionForMutableLocation(e, checkMode, elementContextualType, forceTuple); - elementTypes.push(addOptionality(type, /*isProperty*/ true, hasOmittedExpression)); - elementFlags.push(hasOmittedExpression ? ts.ElementFlags.Optional : ts.ElementFlags.Required); - if (contextualType && someType(contextualType, isTupleLikeType) && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && isContextSensitive(e)) { - const inferenceContext = getInferenceContext(node); - ts.Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context - addIntraExpressionInferenceSite(inferenceContext, e, type); - } + elementTypes.push(checkIteratedTypeOrElementType(IterationUse.Spread, spreadType, undefinedType, (e as ts.SpreadElement).expression)); + elementFlags.push(ts.ElementFlags.Rest); } } - if (inDestructuringPattern) { - return createTupleType(elementTypes, elementFlags); + else if (exactOptionalPropertyTypes && e.kind === ts.SyntaxKind.OmittedExpression) { + hasOmittedExpression = true; + elementTypes.push(missingType); + elementFlags.push(ts.ElementFlags.Optional); } - if (forceTuple || inConstContext || contextualType && someType(contextualType, isTupleLikeType)) { - return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext)); + else { + const elementContextualType = getContextualTypeForElementExpression(contextualType, elementTypes.length); + const type = checkExpressionForMutableLocation(e, checkMode, elementContextualType, forceTuple); + elementTypes.push(addOptionality(type, /*isProperty*/ true, hasOmittedExpression)); + elementFlags.push(hasOmittedExpression ? ts.ElementFlags.Optional : ts.ElementFlags.Required); + if (contextualType && someType(contextualType, isTupleLikeType) && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && isContextSensitive(e)) { + const inferenceContext = getInferenceContext(node); + ts.Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context + addIntraExpressionInferenceSite(inferenceContext, e, type); + } } - return createArrayLiteralType(createArrayType(elementTypes.length ? - getUnionType(ts.sameMap(elementTypes, (t, i) => elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessTypeOrUndefined(t, numberType) || anyType : t), ts.UnionReduction.Subtype) : - strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext)); } - - function createArrayLiteralType(type: ts.Type) { - if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference)) { - return type; - } - let literalType = (type as ts.TypeReference).literalType; - if (!literalType) { - literalType = (type as ts.TypeReference).literalType = cloneTypeReference(type as ts.TypeReference); - literalType.objectFlags |= ts.ObjectFlags.ArrayLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; - } - return literalType; + if (inDestructuringPattern) { + return createTupleType(elementTypes, elementFlags); + } + if (forceTuple || inConstContext || contextualType && someType(contextualType, isTupleLikeType)) { + return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext)); } + return createArrayLiteralType(createArrayType(elementTypes.length ? + getUnionType(ts.sameMap(elementTypes, (t, i) => elementFlags[i] & ts.ElementFlags.Variadic ? getIndexedAccessTypeOrUndefined(t, numberType) || anyType : t), ts.UnionReduction.Subtype) : + strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext)); + } - function isNumericName(name: ts.DeclarationName): boolean { - switch (name.kind) { - case ts.SyntaxKind.ComputedPropertyName: - return isNumericComputedName(name); - case ts.SyntaxKind.Identifier: - return ts.isNumericLiteralName(name.escapedText); - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.StringLiteral: - return ts.isNumericLiteralName(name.text); - default: - return false; - } + function createArrayLiteralType(type: ts.Type) { + if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Reference)) { + return type; + } + let literalType = (type as ts.TypeReference).literalType; + if (!literalType) { + literalType = (type as ts.TypeReference).literalType = cloneTypeReference(type as ts.TypeReference); + literalType.objectFlags |= ts.ObjectFlags.ArrayLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; } + return literalType; + } - function isNumericComputedName(name: ts.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), ts.TypeFlags.NumberLike); + function isNumericName(name: ts.DeclarationName): boolean { + switch (name.kind) { + case ts.SyntaxKind.ComputedPropertyName: + return isNumericComputedName(name); + case ts.SyntaxKind.Identifier: + return ts.isNumericLiteralName(name.escapedText); + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.StringLiteral: + return ts.isNumericLiteralName(name.text); + default: + return false; } + } - function checkComputedPropertyName(node: ts.ComputedPropertyName): ts.Type { - const links = getNodeLinks(node.expression); - if (!links.resolvedType) { - if ((ts.isTypeLiteralNode(node.parent.parent) || ts.isClassLike(node.parent.parent) || ts.isInterfaceDeclaration(node.parent.parent)) - && ts.isBinaryExpression(node.expression) && node.expression.operatorToken.kind === ts.SyntaxKind.InKeyword - && node.parent.kind !== ts.SyntaxKind.GetAccessor && node.parent.kind !== ts.SyntaxKind.SetAccessor) { - return links.resolvedType = errorType; - } - links.resolvedType = checkExpression(node.expression); - // The computed property name of a non-static class field within a loop must be stored in a block-scoped binding. - // (It needs to be bound at class evaluation time.) - if (ts.isPropertyDeclaration(node.parent) && !ts.hasStaticModifier(node.parent) && ts.isClassExpression(node.parent.parent)) { - const container = ts.getEnclosingBlockScopeContainer(node.parent.parent); - const enclosingIterationStatement = getEnclosingIterationStatement(container); - if (enclosingIterationStatement) { - // The computed field name will use a block scoped binding which can be unique for each iteration of the loop. - getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; - // The generated variable which stores the computed field name must be block-scoped. - getNodeLinks(node).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; - // The generated variable which stores the class must be block-scoped. - getNodeLinks(node.parent.parent).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; - } - } - // 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 & ts.TypeFlags.Nullable || - !isTypeAssignableToKind(links.resolvedType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike) && - !isTypeAssignableTo(links.resolvedType, stringNumberSymbolType)) { - error(node, ts.Diagnostics.A_computed_property_name_must_be_of_type_string_number_symbol_or_any); - } - } + function isNumericComputedName(name: ts.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), ts.TypeFlags.NumberLike); + } - return links.resolvedType; - } + function checkComputedPropertyName(node: ts.ComputedPropertyName): ts.Type { + const links = getNodeLinks(node.expression); + if (!links.resolvedType) { + if ((ts.isTypeLiteralNode(node.parent.parent) || ts.isClassLike(node.parent.parent) || ts.isInterfaceDeclaration(node.parent.parent)) + && ts.isBinaryExpression(node.expression) && node.expression.operatorToken.kind === ts.SyntaxKind.InKeyword + && node.parent.kind !== ts.SyntaxKind.GetAccessor && node.parent.kind !== ts.SyntaxKind.SetAccessor) { + return links.resolvedType = errorType; + } + links.resolvedType = checkExpression(node.expression); + // The computed property name of a non-static class field within a loop must be stored in a block-scoped binding. + // (It needs to be bound at class evaluation time.) + if (ts.isPropertyDeclaration(node.parent) && !ts.hasStaticModifier(node.parent) && ts.isClassExpression(node.parent.parent)) { + const container = ts.getEnclosingBlockScopeContainer(node.parent.parent); + const enclosingIterationStatement = getEnclosingIterationStatement(container); + if (enclosingIterationStatement) { + // The computed field name will use a block scoped binding which can be unique for each iteration of the loop. + getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; + // The generated variable which stores the computed field name must be block-scoped. + getNodeLinks(node).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; + // The generated variable which stores the class must be block-scoped. + getNodeLinks(node.parent.parent).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; + } + } + // 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 & ts.TypeFlags.Nullable || + !isTypeAssignableToKind(links.resolvedType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike) && + !isTypeAssignableTo(links.resolvedType, stringNumberSymbolType)) { + error(node, ts.Diagnostics.A_computed_property_name_must_be_of_type_string_number_symbol_or_any); + } + } + + return links.resolvedType; + } - function isSymbolWithNumericName(symbol: ts.Symbol) { - const firstDecl = symbol.declarations?.[0]; - return ts.isNumericLiteralName(symbol.escapedName) || (firstDecl && ts.isNamedDeclaration(firstDecl) && isNumericName(firstDecl.name)); - } + function isSymbolWithNumericName(symbol: ts.Symbol) { + const firstDecl = symbol.declarations?.[0]; + return ts.isNumericLiteralName(symbol.escapedName) || (firstDecl && ts.isNamedDeclaration(firstDecl) && isNumericName(firstDecl.name)); + } - function isSymbolWithSymbolName(symbol: ts.Symbol) { - const firstDecl = symbol.declarations?.[0]; - return ts.isKnownSymbol(symbol) || (firstDecl && ts.isNamedDeclaration(firstDecl) && ts.isComputedPropertyName(firstDecl.name) && - isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), ts.TypeFlags.ESSymbol)); - } + function isSymbolWithSymbolName(symbol: ts.Symbol) { + const firstDecl = symbol.declarations?.[0]; + return ts.isKnownSymbol(symbol) || (firstDecl && ts.isNamedDeclaration(firstDecl) && ts.isComputedPropertyName(firstDecl.name) && + isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), ts.TypeFlags.ESSymbol)); + } - function getObjectLiteralIndexInfo(node: ts.ObjectLiteralExpression, offset: number, properties: ts.Symbol[], keyType: ts.Type): ts.IndexInfo { - const propTypes: ts.Type[] = []; - for (let i = offset; i < properties.length; i++) { - const prop = properties[i]; - if (keyType === stringType && !isSymbolWithSymbolName(prop) || - keyType === numberType && isSymbolWithNumericName(prop) || - keyType === esSymbolType && isSymbolWithSymbolName(prop)) { - propTypes.push(getTypeOfSymbol(properties[i])); - } + function getObjectLiteralIndexInfo(node: ts.ObjectLiteralExpression, offset: number, properties: ts.Symbol[], keyType: ts.Type): ts.IndexInfo { + const propTypes: ts.Type[] = []; + for (let i = offset; i < properties.length; i++) { + const prop = properties[i]; + if (keyType === stringType && !isSymbolWithSymbolName(prop) || + keyType === numberType && isSymbolWithNumericName(prop) || + keyType === esSymbolType && isSymbolWithSymbolName(prop)) { + propTypes.push(getTypeOfSymbol(properties[i])); } - const unionType = propTypes.length ? getUnionType(propTypes, ts.UnionReduction.Subtype) : undefinedType; - return createIndexInfo(keyType, unionType, isConstContext(node)); } + const unionType = propTypes.length ? getUnionType(propTypes, ts.UnionReduction.Subtype) : undefinedType; + return createIndexInfo(keyType, unionType, isConstContext(node)); + } - function getImmediateAliasedSymbol(symbol: ts.Symbol): ts.Symbol | undefined { - ts.Debug.assert((symbol.flags & ts.SymbolFlags.Alias) !== 0, "Should only get Alias here."); - const links = getSymbolLinks(symbol); - if (!links.immediateTarget) { - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) - return ts.Debug.fail(); - links.immediateTarget = getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true); - } - - return links.immediateTarget; + function getImmediateAliasedSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + ts.Debug.assert((symbol.flags & ts.SymbolFlags.Alias) !== 0, "Should only get Alias here."); + const links = getSymbolLinks(symbol); + if (!links.immediateTarget) { + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return ts.Debug.fail(); + links.immediateTarget = getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true); } - function checkObjectLiteral(node: ts.ObjectLiteralExpression, checkMode?: CheckMode): ts.Type { - const inDestructuringPattern = ts.isAssignmentTarget(node); - // Grammar checking - checkGrammarObjectLiteralExpression(node, inDestructuringPattern); - - const allPropertiesTable = strictNullChecks ? ts.createSymbolTable() : undefined; - let propertiesTable = ts.createSymbolTable(); - let propertiesArray: ts.Symbol[] = []; - let spread: ts.Type = emptyObjectType; - - const contextualType = getApparentTypeOfContextualType(node); - const contextualTypeHasPattern = contextualType && contextualType.pattern && - (contextualType.pattern.kind === ts.SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === ts.SyntaxKind.ObjectLiteralExpression); - const inConstContext = isConstContext(node); - const checkFlags = inConstContext ? ts.CheckFlags.Readonly : 0; - const isInJavascript = ts.isInJSFile(node) && !ts.isInJsonFile(node); - const enumTag = ts.getJSDocEnumTag(node); - const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag; - let objectFlags: ts.ObjectFlags = freshObjectLiteralFlag; - let patternWithComputedProperties = false; - let hasComputedStringProperty = false; - let hasComputedNumberProperty = false; - let hasComputedSymbolProperty = false; - - // Spreads may cause an early bail; ensure computed names are always checked (this is cached) - // As otherwise they may not be checked until exports for the type at this position are retrieved, - // which may never occur. - for (const elem of node.properties) { - if (elem.name && ts.isComputedPropertyName(elem.name)) { - checkComputedPropertyName(elem.name); - } - } - - let offset = 0; - for (const memberDecl of node.properties) { - let member = getSymbolOfNode(memberDecl); - const computedNameType = memberDecl.name && memberDecl.name.kind === ts.SyntaxKind.ComputedPropertyName ? - checkComputedPropertyName(memberDecl.name) : undefined; - if (memberDecl.kind === ts.SyntaxKind.PropertyAssignment || - memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment || - ts.isObjectLiteralMethod(memberDecl)) { - let type = memberDecl.kind === ts.SyntaxKind.PropertyAssignment ? checkPropertyAssignment(memberDecl, checkMode) : - // avoid resolving the left side of the ShorthandPropertyAssignment outside of the destructuring - // for error recovery purposes. For example, if a user wrote `{ a = 100 }` instead of `{ a: 100 }`. - // we don't want to say "could not find 'a'". - memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment ? checkExpressionForMutableLocation(!inDestructuringPattern && memberDecl.objectAssignmentInitializer ? memberDecl.objectAssignmentInitializer : 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 |= ts.getObjectFlags(type) & ts.ObjectFlags.PropagatingFlags; - const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined; - const prop = nameType ? - createSymbol(ts.SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | ts.CheckFlags.Late) : - createSymbol(ts.SymbolFlags.Property | member.flags, member.escapedName, checkFlags); - if (nameType) { - prop.nameType = nameType; - } + return links.immediateTarget; + } - 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 === ts.SyntaxKind.PropertyAssignment && hasDefaultValue(memberDecl.initializer)) || - (memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment && memberDecl.objectAssignmentInitializer); - if (isOptional) { - prop.flags |= ts.SymbolFlags.Optional; - } - } - else if (contextualTypeHasPattern && !(ts.getObjectFlags(contextualType) & ts.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 & ts.SymbolFlags.Optional; - } + function checkObjectLiteral(node: ts.ObjectLiteralExpression, checkMode?: CheckMode): ts.Type { + const inDestructuringPattern = ts.isAssignmentTarget(node); + // Grammar checking + checkGrammarObjectLiteralExpression(node, inDestructuringPattern); + + const allPropertiesTable = strictNullChecks ? ts.createSymbolTable() : undefined; + let propertiesTable = ts.createSymbolTable(); + let propertiesArray: ts.Symbol[] = []; + let spread: ts.Type = emptyObjectType; + + const contextualType = getApparentTypeOfContextualType(node); + const contextualTypeHasPattern = contextualType && contextualType.pattern && + (contextualType.pattern.kind === ts.SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === ts.SyntaxKind.ObjectLiteralExpression); + const inConstContext = isConstContext(node); + const checkFlags = inConstContext ? ts.CheckFlags.Readonly : 0; + const isInJavascript = ts.isInJSFile(node) && !ts.isInJsonFile(node); + const enumTag = ts.getJSDocEnumTag(node); + const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag; + let objectFlags: ts.ObjectFlags = freshObjectLiteralFlag; + let patternWithComputedProperties = false; + let hasComputedStringProperty = false; + let hasComputedNumberProperty = false; + let hasComputedSymbolProperty = false; + + // Spreads may cause an early bail; ensure computed names are always checked (this is cached) + // As otherwise they may not be checked until exports for the type at this position are retrieved, + // which may never occur. + for (const elem of node.properties) { + if (elem.name && ts.isComputedPropertyName(elem.name)) { + checkComputedPropertyName(elem.name); + } + } + + let offset = 0; + for (const memberDecl of node.properties) { + let member = getSymbolOfNode(memberDecl); + const computedNameType = memberDecl.name && memberDecl.name.kind === ts.SyntaxKind.ComputedPropertyName ? + checkComputedPropertyName(memberDecl.name) : undefined; + if (memberDecl.kind === ts.SyntaxKind.PropertyAssignment || + memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment || + ts.isObjectLiteralMethod(memberDecl)) { + let type = memberDecl.kind === ts.SyntaxKind.PropertyAssignment ? checkPropertyAssignment(memberDecl, checkMode) : + // avoid resolving the left side of the ShorthandPropertyAssignment outside of the destructuring + // for error recovery purposes. For example, if a user wrote `{ a = 100 }` instead of `{ a: 100 }`. + // we don't want to say "could not find 'a'". + memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment ? checkExpressionForMutableLocation(!inDestructuringPattern && memberDecl.objectAssignmentInitializer ? memberDecl.objectAssignmentInitializer : 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 |= ts.getObjectFlags(type) & ts.ObjectFlags.PropagatingFlags; + const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined; + const prop = nameType ? + createSymbol(ts.SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | ts.CheckFlags.Late) : + createSymbol(ts.SymbolFlags.Property | member.flags, member.escapedName, checkFlags); + if (nameType) { + prop.nameType = nameType; + } - else if (!compilerOptions.suppressExcessPropertyErrors && !getIndexInfoOfType(contextualType, stringType)) { - error(memberDecl.name, ts.Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, symbolToString(member), typeToString(contextualType)); - } + 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 === ts.SyntaxKind.PropertyAssignment && hasDefaultValue(memberDecl.initializer)) || + (memberDecl.kind === ts.SyntaxKind.ShorthandPropertyAssignment && memberDecl.objectAssignmentInitializer); + if (isOptional) { + prop.flags |= ts.SymbolFlags.Optional; } - - prop.declarations = member.declarations; - prop.parent = member.parent; - if (member.valueDeclaration) { - prop.valueDeclaration = member.valueDeclaration; + } + else if (contextualTypeHasPattern && !(ts.getObjectFlags(contextualType) & ts.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 & ts.SymbolFlags.Optional; } - prop.type = type; - prop.target = member; - member = prop; - allPropertiesTable?.set(prop.escapedName, prop); - - if (contextualType && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && - (memberDecl.kind === ts.SyntaxKind.PropertyAssignment || memberDecl.kind === ts.SyntaxKind.MethodDeclaration) && isContextSensitive(memberDecl)) { - const inferenceContext = getInferenceContext(node); - ts.Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context - const inferenceNode = memberDecl.kind === ts.SyntaxKind.PropertyAssignment ? memberDecl.initializer : memberDecl; - addIntraExpressionInferenceSite(inferenceContext, inferenceNode, type); - } - } - else if (memberDecl.kind === ts.SyntaxKind.SpreadAssignment) { - if (languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(memberDecl, ts.ExternalEmitHelpers.Assign); - } - if (propertiesArray.length > 0) { - spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); - propertiesArray = []; - propertiesTable = ts.createSymbolTable(); - hasComputedStringProperty = false; - hasComputedNumberProperty = false; - hasComputedSymbolProperty = false; - } - const type = getReducedType(checkExpression(memberDecl.expression)); - if (isValidSpreadType(type)) { - const mergedType = tryMergeUnionOfObjectTypeAndEmptyObject(type, inConstContext); - if (allPropertiesTable) { - checkSpreadPropOverrides(mergedType, allPropertiesTable, memberDecl); - } - offset = propertiesArray.length; - if (isErrorType(spread)) { - continue; - } - spread = getSpreadType(spread, mergedType, node.symbol, objectFlags, inConstContext); - } - else { - error(memberDecl, ts.Diagnostics.Spread_types_may_only_be_created_from_object_types); - spread = errorType; + else if (!compilerOptions.suppressExcessPropertyErrors && !getIndexInfoOfType(contextualType, stringType)) { + error(memberDecl.name, ts.Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, symbolToString(member), typeToString(contextualType)); } - 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. - ts.Debug.assert(memberDecl.kind === ts.SyntaxKind.GetAccessor || memberDecl.kind === ts.SyntaxKind.SetAccessor); - checkNodeDeferred(memberDecl); } - if (computedNameType && !(computedNameType.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique)) { - if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) { - if (isTypeAssignableTo(computedNameType, numberType)) { - hasComputedNumberProperty = true; - } - else if (isTypeAssignableTo(computedNameType, esSymbolType)) { - hasComputedSymbolProperty = true; - } - else { - hasComputedStringProperty = true; - } - if (inDestructuringPattern) { - patternWithComputedProperties = true; - } - } - } - else { - propertiesTable.set(member.escapedName, member); + prop.declarations = member.declarations; + prop.parent = member.parent; + if (member.valueDeclaration) { + prop.valueDeclaration = member.valueDeclaration; } - 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 !== ts.SyntaxKind.SpreadAssignment) { - for (const prop of getPropertiesOfType(contextualType)) { - if (!propertiesTable.get(prop.escapedName) && !getPropertyOfType(spread, prop.escapedName)) { - if (!(prop.flags & ts.SymbolFlags.Optional)) { - error(prop.valueDeclaration || (prop as ts.TransientSymbol).bindingElement, ts.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); - } - } - } + prop.type = type; + prop.target = member; + member = prop; + allPropertiesTable?.set(prop.escapedName, prop); - if (isErrorType(spread)) { - return errorType; + if (contextualType && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && + (memberDecl.kind === ts.SyntaxKind.PropertyAssignment || memberDecl.kind === ts.SyntaxKind.MethodDeclaration) && isContextSensitive(memberDecl)) { + const inferenceContext = getInferenceContext(node); + ts.Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context + const inferenceNode = memberDecl.kind === ts.SyntaxKind.PropertyAssignment ? memberDecl.initializer : memberDecl; + addIntraExpressionInferenceSite(inferenceContext, inferenceNode, type); + } } - - if (spread !== emptyObjectType) { + else if (memberDecl.kind === ts.SyntaxKind.SpreadAssignment) { + if (languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(memberDecl, ts.ExternalEmitHelpers.Assign); + } if (propertiesArray.length > 0) { spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); propertiesArray = []; propertiesTable = ts.createSymbolTable(); hasComputedStringProperty = false; hasComputedNumberProperty = false; + hasComputedSymbolProperty = 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); - } - - return createObjectLiteralType(); - - function createObjectLiteralType() { - const indexInfos = []; - if (hasComputedStringProperty) - indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, stringType)); - if (hasComputedNumberProperty) - indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, numberType)); - if (hasComputedSymbolProperty) - indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, esSymbolType)); - const result = createAnonymousType(node.symbol, propertiesTable, ts.emptyArray, ts.emptyArray, indexInfos); - result.objectFlags |= objectFlags | ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; - if (isJSObjectLiteral) { - result.objectFlags |= ts.ObjectFlags.JSLiteral; + const type = getReducedType(checkExpression(memberDecl.expression)); + if (isValidSpreadType(type)) { + const mergedType = tryMergeUnionOfObjectTypeAndEmptyObject(type, inConstContext); + if (allPropertiesTable) { + checkSpreadPropOverrides(mergedType, allPropertiesTable, memberDecl); + } + offset = propertiesArray.length; + if (isErrorType(spread)) { + continue; + } + spread = getSpreadType(spread, mergedType, node.symbol, objectFlags, inConstContext); } - if (patternWithComputedProperties) { - result.objectFlags |= ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties; + else { + error(memberDecl, ts.Diagnostics.Spread_types_may_only_be_created_from_object_types); + spread = errorType; } - if (inDestructuringPattern) { - result.pattern = node; + 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. + ts.Debug.assert(memberDecl.kind === ts.SyntaxKind.GetAccessor || memberDecl.kind === ts.SyntaxKind.SetAccessor); + checkNodeDeferred(memberDecl); + } + + if (computedNameType && !(computedNameType.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique)) { + if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) { + if (isTypeAssignableTo(computedNameType, numberType)) { + hasComputedNumberProperty = true; + } + else if (isTypeAssignableTo(computedNameType, esSymbolType)) { + hasComputedSymbolProperty = true; + } + else { + hasComputedStringProperty = true; + } + if (inDestructuringPattern) { + patternWithComputedProperties = true; + } } - return result; } + else { + propertiesTable.set(member.escapedName, member); + } + propertiesArray.push(member); } - function isValidSpreadType(type: ts.Type): boolean { - const t = removeDefinitelyFalsyTypes(mapType(type, getBaseConstraintOrType)); - return !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive) || - t.flags & ts.TypeFlags.UnionOrIntersection && ts.every((t as ts.UnionOrIntersectionType).types, isValidSpreadType)); + // 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 !== ts.SyntaxKind.SpreadAssignment) { + for (const prop of getPropertiesOfType(contextualType)) { + if (!propertiesTable.get(prop.escapedName) && !getPropertyOfType(spread, prop.escapedName)) { + if (!(prop.flags & ts.SymbolFlags.Optional)) { + error(prop.valueDeclaration || (prop as ts.TransientSymbol).bindingElement, ts.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 checkJsxSelfClosingElementDeferred(node: ts.JsxSelfClosingElement) { - checkJsxOpeningLikeElementOrOpeningFragment(node); + if (isErrorType(spread)) { + return errorType; } - function checkJsxSelfClosingElement(node: ts.JsxSelfClosingElement, _checkMode: CheckMode | undefined): ts.Type { - checkNodeDeferred(node); - return getJsxElementTypeAt(node) || anyType; + if (spread !== emptyObjectType) { + if (propertiesArray.length > 0) { + spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); + propertiesArray = []; + propertiesTable = ts.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); } - function checkJsxElementDeferred(node: ts.JsxElement) { - // Check attributes - checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement); + return createObjectLiteralType(); - // Perform resolution on the closing tag so that rename/go to definition/etc work - if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) { - getIntrinsicTagSymbol(node.closingElement); + function createObjectLiteralType() { + const indexInfos = []; + if (hasComputedStringProperty) + indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, stringType)); + if (hasComputedNumberProperty) + indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, numberType)); + if (hasComputedSymbolProperty) + indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, esSymbolType)); + const result = createAnonymousType(node.symbol, propertiesTable, ts.emptyArray, ts.emptyArray, indexInfos); + result.objectFlags |= objectFlags | ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; + if (isJSObjectLiteral) { + result.objectFlags |= ts.ObjectFlags.JSLiteral; } - else { - checkExpression(node.closingElement.tagName); + if (patternWithComputedProperties) { + result.objectFlags |= ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties; } - - checkJsxChildren(node); + if (inDestructuringPattern) { + result.pattern = node; + } + return result; } + } - function checkJsxElement(node: ts.JsxElement, _checkMode: CheckMode | undefined): ts.Type { - checkNodeDeferred(node); - - return getJsxElementTypeAt(node) || anyType; - } + function isValidSpreadType(type: ts.Type): boolean { + const t = removeDefinitelyFalsyTypes(mapType(type, getBaseConstraintOrType)); + return !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Object | ts.TypeFlags.InstantiableNonPrimitive) || + t.flags & ts.TypeFlags.UnionOrIntersection && ts.every((t as ts.UnionOrIntersectionType).types, isValidSpreadType)); + } - function checkJsxFragment(node: ts.JsxFragment): ts.Type { - checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment); + function checkJsxSelfClosingElementDeferred(node: ts.JsxSelfClosingElement) { + checkJsxOpeningLikeElementOrOpeningFragment(node); + } - // by default, jsx:'react' will use jsxFactory = React.createElement and jsxFragmentFactory = React.Fragment - // if jsxFactory compiler option is provided, ensure jsxFragmentFactory compiler option or @jsxFrag pragma is provided too - const nodeSourceFile = ts.getSourceFileOfNode(node); - if (ts.getJSXTransformEnabled(compilerOptions) && (compilerOptions.jsxFactory || nodeSourceFile.pragmas.has("jsx")) - && !compilerOptions.jsxFragmentFactory && !nodeSourceFile.pragmas.has("jsxfrag")) { - error(node, compilerOptions.jsxFactory - ? ts.Diagnostics.The_jsxFragmentFactory_compiler_option_must_be_provided_to_use_JSX_fragments_with_the_jsxFactory_compiler_option - : ts.Diagnostics.An_jsxFrag_pragma_is_required_when_using_an_jsx_pragma_with_JSX_fragments); - } + function checkJsxSelfClosingElement(node: ts.JsxSelfClosingElement, _checkMode: CheckMode | undefined): ts.Type { + checkNodeDeferred(node); + return getJsxElementTypeAt(node) || anyType; + } - checkJsxChildren(node); - return getJsxElementTypeAt(node) || anyType; - } + function checkJsxElementDeferred(node: ts.JsxElement) { + // Check attributes + checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement); - function isHyphenatedJsxName(name: string | ts.__String) { - return ts.stringContains(name as string, "-"); + // Perform resolution on the closing tag so that rename/go to definition/etc work + if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) { + getIntrinsicTagSymbol(node.closingElement); } - - /** - * Returns true iff React would emit this tag name as a string rather than an identifier or qualified name - */ - function isJsxIntrinsicIdentifier(tagName: ts.JsxTagNameExpression): boolean { - return tagName.kind === ts.SyntaxKind.Identifier && ts.isIntrinsicJsxName(tagName.escapedText); + else { + checkExpression(node.closingElement.tagName); } - function checkJsxAttribute(node: ts.JsxAttribute, checkMode?: CheckMode) { - return node.initializer - ? checkExpressionForMutableLocation(node.initializer, checkMode) - : trueType; // is sugar for - } + checkJsxChildren(node); + } - /** - * 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: ts.JsxOpeningLikeElement, checkMode: CheckMode | undefined) { - const attributes = openingLikeElement.attributes; - const allAttributesTable = strictNullChecks ? ts.createSymbolTable() : undefined; - let attributesTable = ts.createSymbolTable(); - let spread: ts.Type = emptyJsxObjectType; - let hasSpreadAnyType = false; - let typeToIntersect: ts.Type | undefined; - let explicitlySpecifyChildrenAttribute = false; - let objectFlags: ts.ObjectFlags = ts.ObjectFlags.JsxAttributes; - const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(openingLikeElement)); - - for (const attributeDecl of attributes.properties) { - const member = attributeDecl.symbol; - if (ts.isJsxAttribute(attributeDecl)) { - const exprType = checkJsxAttribute(attributeDecl, checkMode); - objectFlags |= ts.getObjectFlags(exprType) & ts.ObjectFlags.PropagatingFlags; - const attributeSymbol = createSymbol(ts.SymbolFlags.Property | 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); - allAttributesTable?.set(attributeSymbol.escapedName, attributeSymbol); - if (attributeDecl.name.escapedText === jsxChildrenPropertyName) { - explicitlySpecifyChildrenAttribute = true; - } - } - else { - ts.Debug.assert(attributeDecl.kind === ts.SyntaxKind.JsxSpreadAttribute); - if (attributesTable.size > 0) { - spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); - attributesTable = ts.createSymbolTable(); - } - const exprType = getReducedType(checkExpressionCached(attributeDecl.expression, checkMode)); - if (isTypeAny(exprType)) { - hasSpreadAnyType = true; - } - if (isValidSpreadType(exprType)) { - spread = getSpreadType(spread, exprType, attributes.symbol, objectFlags, /*readonly*/ false); - if (allAttributesTable) { - checkSpreadPropOverrides(exprType, allAttributesTable, attributeDecl); - } - } - else { - error(attributeDecl.expression, ts.Diagnostics.Spread_types_may_only_be_created_from_object_types); - typeToIntersect = typeToIntersect ? getIntersectionType([typeToIntersect, exprType]) : exprType; - } - } - } + function checkJsxElement(node: ts.JsxElement, _checkMode: CheckMode | undefined): ts.Type { + checkNodeDeferred(node); - if (!hasSpreadAnyType) { - if (attributesTable.size > 0) { - spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); - } - } + return getJsxElementTypeAt(node) || anyType; + } - // Handle children attribute - const parent = openingLikeElement.parent.kind === ts.SyntaxKind.JsxElement ? openingLikeElement.parent as ts.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); + function checkJsxFragment(node: ts.JsxFragment): ts.Type { + checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment); - 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, ts.Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, ts.unescapeLeadingUnderscores(jsxChildrenPropertyName)); - } + // by default, jsx:'react' will use jsxFactory = React.createElement and jsxFragmentFactory = React.Fragment + // if jsxFactory compiler option is provided, ensure jsxFragmentFactory compiler option or @jsxFrag pragma is provided too + const nodeSourceFile = ts.getSourceFileOfNode(node); + if (ts.getJSXTransformEnabled(compilerOptions) && (compilerOptions.jsxFactory || nodeSourceFile.pragmas.has("jsx")) + && !compilerOptions.jsxFragmentFactory && !nodeSourceFile.pragmas.has("jsxfrag")) { + error(node, compilerOptions.jsxFactory + ? ts.Diagnostics.The_jsxFragmentFactory_compiler_option_must_be_provided_to_use_JSX_fragments_with_the_jsxFactory_compiler_option + : ts.Diagnostics.An_jsxFrag_pragma_is_required_when_using_an_jsx_pragma_with_JSX_fragments); + } - 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(ts.SymbolFlags.Property, jsxChildrenPropertyName); - childrenPropSymbol.type = childrenTypes.length === 1 ? childrenTypes[0] : - childrenContextualType && someType(childrenContextualType, isTupleLikeType) ? createTupleType(childrenTypes) : - createArrayType(getUnionType(childrenTypes)); - // Fake up a property declaration for the children - childrenPropSymbol.valueDeclaration = ts.factory.createPropertySignature(/*modifiers*/ undefined, ts.unescapeLeadingUnderscores(jsxChildrenPropertyName), /*questionToken*/ undefined, /*type*/ undefined); - ts.setParent(childrenPropSymbol.valueDeclaration, attributes); - childrenPropSymbol.valueDeclaration.symbol = childrenPropSymbol; - const childPropMap = ts.createSymbolTable(); - childPropMap.set(jsxChildrenPropertyName, childrenPropSymbol); - spread = getSpreadType(spread, createAnonymousType(attributes.symbol, childPropMap, ts.emptyArray, ts.emptyArray, ts.emptyArray), attributes.symbol, objectFlags, /*readonly*/ false); + checkJsxChildren(node); + return getJsxElementTypeAt(node) || anyType; + } - } - } + function isHyphenatedJsxName(name: string | ts.__String) { + return ts.stringContains(name as string, "-"); + } - if (hasSpreadAnyType) { - return anyType; - } - if (typeToIntersect && spread !== emptyJsxObjectType) { - return getIntersectionType([typeToIntersect, spread]); - } - return typeToIntersect || (spread === emptyJsxObjectType ? createJsxAttributesType() : spread); + /** + * Returns true iff React would emit this tag name as a string rather than an identifier or qualified name + */ + function isJsxIntrinsicIdentifier(tagName: ts.JsxTagNameExpression): boolean { + return tagName.kind === ts.SyntaxKind.Identifier && ts.isIntrinsicJsxName(tagName.escapedText); + } - /** - * 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, ts.emptyArray, ts.emptyArray, ts.emptyArray); - result.objectFlags |= objectFlags | ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; - return result; - } - } + function checkJsxAttribute(node: ts.JsxAttribute, checkMode?: CheckMode) { + return node.initializer + ? checkExpressionForMutableLocation(node.initializer, checkMode) + : trueType; // is sugar for + } - function checkJsxChildren(node: ts.JsxElement | ts.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 === ts.SyntaxKind.JsxText) { - if (!child.containsOnlyTriviaWhiteSpaces) { - childrenTypes.push(stringType); - } - } - else if (child.kind === ts.SyntaxKind.JsxExpression && !child.expression) { - continue; // empty jsx expressions don't *really* count as present children - } - else { - childrenTypes.push(checkExpressionForMutableLocation(child, checkMode)); + /** + * 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: ts.JsxOpeningLikeElement, checkMode: CheckMode | undefined) { + const attributes = openingLikeElement.attributes; + const allAttributesTable = strictNullChecks ? ts.createSymbolTable() : undefined; + let attributesTable = ts.createSymbolTable(); + let spread: ts.Type = emptyJsxObjectType; + let hasSpreadAnyType = false; + let typeToIntersect: ts.Type | undefined; + let explicitlySpecifyChildrenAttribute = false; + let objectFlags: ts.ObjectFlags = ts.ObjectFlags.JsxAttributes; + const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(openingLikeElement)); + + for (const attributeDecl of attributes.properties) { + const member = attributeDecl.symbol; + if (ts.isJsxAttribute(attributeDecl)) { + const exprType = checkJsxAttribute(attributeDecl, checkMode); + objectFlags |= ts.getObjectFlags(exprType) & ts.ObjectFlags.PropagatingFlags; + const attributeSymbol = createSymbol(ts.SymbolFlags.Property | 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); + allAttributesTable?.set(attributeSymbol.escapedName, attributeSymbol); + if (attributeDecl.name.escapedText === jsxChildrenPropertyName) { + explicitlySpecifyChildrenAttribute = true; } } - return childrenTypes; - } - - function checkSpreadPropOverrides(type: ts.Type, props: ts.SymbolTable, spread: ts.SpreadAssignment | ts.JsxSpreadAttribute) { - for (const right of getPropertiesOfType(type)) { - if (!(right.flags & ts.SymbolFlags.Optional)) { - const left = props.get(right.escapedName); - if (left) { - const diagnostic = error(left.valueDeclaration, ts.Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, ts.unescapeLeadingUnderscores(left.escapedName)); - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(spread, ts.Diagnostics.This_spread_always_overwrites_this_property)); - } + else { + ts.Debug.assert(attributeDecl.kind === ts.SyntaxKind.JsxSpreadAttribute); + if (attributesTable.size > 0) { + spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); + attributesTable = ts.createSymbolTable(); } - } - } - - /** - * 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: ts.JsxAttributes, checkMode: CheckMode | undefined) { - return createJsxAttributesTypeFromAttributesProperty(node.parent, checkMode); - } - - function getJsxType(name: ts.__String, location: ts.Node | undefined) { - const namespace = getJsxNamespaceAt(location); - const exports = namespace && getExportsOfSymbol(namespace); - const typeSymbol = exports && getSymbol(exports, name, ts.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: ts.JsxOpeningLikeElement | ts.JsxClosingElement): ts.Symbol { - const links = getNodeLinks(node); - if (!links.resolvedSymbol) { - const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node); - if (!isErrorType(intrinsicElementsType)) { - // Property case - if (!ts.isIdentifier(node.tagName)) - return ts.Debug.fail(); - const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText); - if (intrinsicProp) { - links.jsxFlags |= ts.JsxFlags.IntrinsicNamedElement; - return links.resolvedSymbol = intrinsicProp; - } - - // Intrinsic string indexer case - const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, stringType); - if (indexSignatureType) { - links.jsxFlags |= ts.JsxFlags.IntrinsicIndexedElement; - return links.resolvedSymbol = intrinsicElementsType.symbol; + const exprType = getReducedType(checkExpressionCached(attributeDecl.expression, checkMode)); + if (isTypeAny(exprType)) { + hasSpreadAnyType = true; + } + if (isValidSpreadType(exprType)) { + spread = getSpreadType(spread, exprType, attributes.symbol, objectFlags, /*readonly*/ false); + if (allAttributesTable) { + checkSpreadPropOverrides(exprType, allAttributesTable, attributeDecl); } - - // Wasn't found - error(node, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.idText(node.tagName), "JSX." + JsxNames.IntrinsicElements); - return links.resolvedSymbol = unknownSymbol; } else { - if (noImplicitAny) { - error(node, ts.Diagnostics.JSX_element_implicitly_has_type_any_because_no_interface_JSX_0_exists, ts.unescapeLeadingUnderscores(JsxNames.IntrinsicElements)); - } - return links.resolvedSymbol = unknownSymbol; + error(attributeDecl.expression, ts.Diagnostics.Spread_types_may_only_be_created_from_object_types); + typeToIntersect = typeToIntersect ? getIntersectionType([typeToIntersect, exprType]) : exprType; } } - return links.resolvedSymbol; } - function getJsxNamespaceContainerForImplicitImport(location: ts.Node | undefined): ts.Symbol | undefined { - const file = location && ts.getSourceFileOfNode(location); - const links = file && getNodeLinks(file); - if (links && links.jsxImplicitImportContainer === false) { - return undefined; - } - if (links && links.jsxImplicitImportContainer) { - return links.jsxImplicitImportContainer; + if (!hasSpreadAnyType) { + if (attributesTable.size > 0) { + spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); } - const runtimeImportSpecifier = ts.getJSXRuntimeImport(ts.getJSXImplicitImportBase(compilerOptions, file), compilerOptions); - if (!runtimeImportSpecifier) { - return undefined; - } - const isClassic = ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Classic; - const errorMessage = isClassic - ? ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option - : ts.Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations; - const mod = resolveExternalModule(location!, runtimeImportSpecifier, errorMessage, location!); - const result = mod && mod !== unknownSymbol ? getMergedSymbol(resolveSymbol(mod)) : undefined; - if (links) { - links.jsxImplicitImportContainer = result || false; - } - return result; } - function getJsxNamespaceAt(location: ts.Node | undefined): ts.Symbol { - const links = location && getNodeLinks(location); - if (links && links.jsxNamespace) { - return links.jsxNamespace; - } - if (!links || links.jsxNamespace !== false) { - let resolvedNamespace = getJsxNamespaceContainerForImplicitImport(location); + // Handle children attribute + const parent = openingLikeElement.parent.kind === ts.SyntaxKind.JsxElement ? openingLikeElement.parent as ts.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 (!resolvedNamespace || resolvedNamespace === unknownSymbol) { - const namespaceName = getJsxNamespace(location); - resolvedNamespace = resolveName(location, namespaceName, ts.SymbolFlags.Namespace, /*diagnosticMessage*/ undefined, namespaceName, /*isUse*/ false); + 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, ts.Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, ts.unescapeLeadingUnderscores(jsxChildrenPropertyName)); } - if (resolvedNamespace) { - const candidate = resolveSymbol(getSymbol(getExportsOfSymbol(resolveSymbol(resolvedNamespace)), JsxNames.JSX, ts.SymbolFlags.Namespace)); - if (candidate && candidate !== unknownSymbol) { - if (links) { - links.jsxNamespace = candidate; - } - return candidate; - } - } - if (links) { - links.jsxNamespace = false; - } - } - // JSX global fallback - const s = resolveSymbol(getGlobalSymbol(JsxNames.JSX, ts.SymbolFlags.Namespace, /*diagnosticMessage*/ undefined)); - if (s === unknownSymbol) { - return undefined!; // TODO: GH#18217 - } - return s!; // TODO: GH#18217 - } + 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(ts.SymbolFlags.Property, jsxChildrenPropertyName); + childrenPropSymbol.type = childrenTypes.length === 1 ? childrenTypes[0] : + childrenContextualType && someType(childrenContextualType, isTupleLikeType) ? createTupleType(childrenTypes) : + createArrayType(getUnionType(childrenTypes)); + // Fake up a property declaration for the children + childrenPropSymbol.valueDeclaration = ts.factory.createPropertySignature(/*modifiers*/ undefined, ts.unescapeLeadingUnderscores(jsxChildrenPropertyName), /*questionToken*/ undefined, /*type*/ undefined); + ts.setParent(childrenPropSymbol.valueDeclaration, attributes); + childrenPropSymbol.valueDeclaration.symbol = childrenPropSymbol; + const childPropMap = ts.createSymbolTable(); + childPropMap.set(jsxChildrenPropertyName, childrenPropSymbol); + spread = getSpreadType(spread, createAnonymousType(attributes.symbol, childPropMap, ts.emptyArray, ts.emptyArray, ts.emptyArray), attributes.symbol, objectFlags, /*readonly*/ 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: ts.__String, jsxNamespace: ts.Symbol): ts.__String | undefined { - // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [symbol] - const jsxElementAttribPropInterfaceSym = jsxNamespace && getSymbol(jsxNamespace.exports!, nameOfAttribPropContainer, ts.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 ts.__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 && jsxElementAttribPropInterfaceSym.declarations) { - // More than one property on ElementAttributesProperty is an error - error(jsxElementAttribPropInterfaceSym.declarations[0], ts.Diagnostics.The_global_type_JSX_0_may_not_have_more_than_one_property, ts.unescapeLeadingUnderscores(nameOfAttribPropContainer)); - } } - return undefined; } - function getJsxLibraryManagedAttributes(jsxNamespace: ts.Symbol) { - // JSX.LibraryManagedAttributes [symbol] - return jsxNamespace && getSymbol(jsxNamespace.exports!, JsxNames.LibraryManagedAttributes, ts.SymbolFlags.Type); + if (hasSpreadAnyType) { + return anyType; } - - /// 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); + if (typeToIntersect && spread !== emptyJsxObjectType) { + return getIntersectionType([typeToIntersect, spread]); } + return typeToIntersect || (spread === emptyJsxObjectType ? createJsxAttributesType() : spread); - function getJsxElementChildrenPropertyName(jsxNamespace: ts.Symbol): ts.__String | undefined { - return getNameFromJsxElementAttributesContainer(JsxNames.ElementChildrenAttributeNameContainer, jsxNamespace); + /** + * 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, ts.emptyArray, ts.emptyArray, ts.emptyArray); + result.objectFlags |= objectFlags | ts.ObjectFlags.ObjectLiteral | ts.ObjectFlags.ContainsObjectOrArrayLiteral; + return result; } + } - function getUninstantiatedJsxSignaturesOfType(elementType: ts.Type, caller: ts.JsxOpeningLikeElement): readonly ts.Signature[] { - if (elementType.flags & ts.TypeFlags.String) { - return [anySignature]; - } - else if (elementType.flags & ts.TypeFlags.StringLiteral) { - const intrinsicType = getIntrinsicAttributesTypeFromStringLiteralType(elementType as ts.StringLiteralType, caller); - if (!intrinsicType) { - error(caller, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (elementType as ts.StringLiteralType).value, "JSX." + JsxNames.IntrinsicElements); - return ts.emptyArray; - } - else { - const fakeSignature = createSignatureForJSXIntrinsic(caller, intrinsicType); - return [fakeSignature]; + function checkJsxChildren(node: ts.JsxElement | ts.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 === ts.SyntaxKind.JsxText) { + if (!child.containsOnlyTriviaWhiteSpaces) { + childrenTypes.push(stringType); } } - const apparentElemType = getApparentType(elementType); - // Resolve the signatures, preferring constructor - let signatures = getSignaturesOfType(apparentElemType, ts.SignatureKind.Construct); - if (signatures.length === 0) { - // No construct signatures, try call signatures - signatures = getSignaturesOfType(apparentElemType, ts.SignatureKind.Call); + else if (child.kind === ts.SyntaxKind.JsxExpression && !child.expression) { + continue; // empty jsx expressions don't *really* count as present children } - if (signatures.length === 0 && apparentElemType.flags & ts.TypeFlags.Union) { - // If each member has some combination of new/call signatures; make a union signature list for those - signatures = getUnionSignatures(ts.map((apparentElemType as ts.UnionType).types, t => getUninstantiatedJsxSignaturesOfType(t, caller))); - } - return signatures; - } - - function getIntrinsicAttributesTypeFromStringLiteralType(type: ts.StringLiteralType, location: ts.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 (!isErrorType(intrinsicElementsType)) { - const stringLiteralTypeName = type.value; - const intrinsicProp = getPropertyOfType(intrinsicElementsType, ts.escapeLeadingUnderscores(stringLiteralTypeName)); - if (intrinsicProp) { - return getTypeOfSymbol(intrinsicProp); - } - const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, stringType); - if (indexSignatureType) { - return indexSignatureType; - } - return undefined; + else { + childrenTypes.push(checkExpressionForMutableLocation(child, checkMode)); } - // 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 childrenTypes; + } - function checkJsxReturnAssignableToAppropriateBound(refKind: ts.JsxReferenceKind, elemInstanceType: ts.Type, openingLikeElement: ts.JsxOpeningLikeElement) { - if (refKind === ts.JsxReferenceKind.Function) { - const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); - if (sfcReturnConstraint) { - checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_return_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); - } - } - else if (refKind === ts.JsxReferenceKind.Component) { - const classConstraint = getJsxElementClassTypeAt(openingLikeElement); - if (classConstraint) { - // Issue an error if this return type isn't assignable to JSX.ElementClass, failing that - checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_instance_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); - } - } - else { // Mixed - const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); - const classConstraint = getJsxElementClassTypeAt(openingLikeElement); - if (!sfcReturnConstraint || !classConstraint) { - return; + function checkSpreadPropOverrides(type: ts.Type, props: ts.SymbolTable, spread: ts.SpreadAssignment | ts.JsxSpreadAttribute) { + for (const right of getPropertiesOfType(type)) { + if (!(right.flags & ts.SymbolFlags.Optional)) { + const left = props.get(right.escapedName); + if (left) { + const diagnostic = error(left.valueDeclaration, ts.Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, ts.unescapeLeadingUnderscores(left.escapedName)); + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(spread, ts.Diagnostics.This_spread_always_overwrites_this_property)); } - const combined = getUnionType([sfcReturnConstraint, classConstraint]); - checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_element_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); - } - - function generateInitialErrorChain(): ts.DiagnosticMessageChain { - const componentName = ts.getTextOfNode(openingLikeElement.tagName); - return ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics._0_cannot_be_used_as_a_JSX_component, componentName); } } + } - /** - * 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: ts.JsxOpeningLikeElement): ts.Type { - ts.Debug.assert(isJsxIntrinsicIdentifier(node.tagName)); - const links = getNodeLinks(node); - if (!links.resolvedJsxElementAttributesType) { - const symbol = getIntrinsicTagSymbol(node); - if (links.jsxFlags & ts.JsxFlags.IntrinsicNamedElement) { - return links.resolvedJsxElementAttributesType = getTypeOfSymbol(symbol) || errorType; + /** + * 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: ts.JsxAttributes, checkMode: CheckMode | undefined) { + return createJsxAttributesTypeFromAttributesProperty(node.parent, checkMode); + } + + function getJsxType(name: ts.__String, location: ts.Node | undefined) { + const namespace = getJsxNamespaceAt(location); + const exports = namespace && getExportsOfSymbol(namespace); + const typeSymbol = exports && getSymbol(exports, name, ts.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: ts.JsxOpeningLikeElement | ts.JsxClosingElement): ts.Symbol { + const links = getNodeLinks(node); + if (!links.resolvedSymbol) { + const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node); + if (!isErrorType(intrinsicElementsType)) { + // Property case + if (!ts.isIdentifier(node.tagName)) + return ts.Debug.fail(); + const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText); + if (intrinsicProp) { + links.jsxFlags |= ts.JsxFlags.IntrinsicNamedElement; + return links.resolvedSymbol = intrinsicProp; } - else if (links.jsxFlags & ts.JsxFlags.IntrinsicIndexedElement) { - return links.resolvedJsxElementAttributesType = - getIndexTypeOfType(getJsxType(JsxNames.IntrinsicElements, node), stringType) || errorType; + + // Intrinsic string indexer case + const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, stringType); + if (indexSignatureType) { + links.jsxFlags |= ts.JsxFlags.IntrinsicIndexedElement; + return links.resolvedSymbol = intrinsicElementsType.symbol; } - else { - return links.resolvedJsxElementAttributesType = errorType; + + // Wasn't found + error(node, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.idText(node.tagName), "JSX." + JsxNames.IntrinsicElements); + return links.resolvedSymbol = unknownSymbol; + } + else { + if (noImplicitAny) { + error(node, ts.Diagnostics.JSX_element_implicitly_has_type_any_because_no_interface_JSX_0_exists, ts.unescapeLeadingUnderscores(JsxNames.IntrinsicElements)); } + return links.resolvedSymbol = unknownSymbol; } - return links.resolvedJsxElementAttributesType; } + return links.resolvedSymbol; + } - function getJsxElementClassTypeAt(location: ts.Node): ts.Type | undefined { - const type = getJsxType(JsxNames.ElementClass, location); - if (isErrorType(type)) - return undefined; - return type; + function getJsxNamespaceContainerForImplicitImport(location: ts.Node | undefined): ts.Symbol | undefined { + const file = location && ts.getSourceFileOfNode(location); + const links = file && getNodeLinks(file); + if (links && links.jsxImplicitImportContainer === false) { + return undefined; } - - function getJsxElementTypeAt(location: ts.Node): ts.Type { - return getJsxType(JsxNames.Element, location); + if (links && links.jsxImplicitImportContainer) { + return links.jsxImplicitImportContainer; } - - function getJsxStatelessElementTypeAt(location: ts.Node): ts.Type | undefined { - const jsxElementType = getJsxElementTypeAt(location); - if (jsxElementType) { - return getUnionType([jsxElementType, nullType]); - } + const runtimeImportSpecifier = ts.getJSXRuntimeImport(ts.getJSXImplicitImportBase(compilerOptions, file), compilerOptions); + if (!runtimeImportSpecifier) { + return undefined; + } + const isClassic = ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Classic; + const errorMessage = isClassic + ? ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option + : ts.Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations; + const mod = resolveExternalModule(location!, runtimeImportSpecifier, errorMessage, location!); + const result = mod && mod !== unknownSymbol ? getMergedSymbol(resolveSymbol(mod)) : undefined; + if (links) { + links.jsxImplicitImportContainer = result || false; } + return result; + } - /** - * Returns all the properties of the Jsx.IntrinsicElements interface - */ - function getJsxIntrinsicTagNamesAt(location: ts.Node): ts.Symbol[] { - const intrinsics = getJsxType(JsxNames.IntrinsicElements, location); - return intrinsics ? getPropertiesOfType(intrinsics) : ts.emptyArray; + function getJsxNamespaceAt(location: ts.Node | undefined): ts.Symbol { + const links = location && getNodeLinks(location); + if (links && links.jsxNamespace) { + return links.jsxNamespace; } + if (!links || links.jsxNamespace !== false) { + let resolvedNamespace = getJsxNamespaceContainerForImplicitImport(location); - function checkJsxPreconditions(errorNode: ts.Node) { - // Preconditions for using JSX - if ((compilerOptions.jsx || ts.JsxEmit.None) === ts.JsxEmit.None) { - error(errorNode, ts.Diagnostics.Cannot_use_JSX_unless_the_jsx_flag_is_provided); + if (!resolvedNamespace || resolvedNamespace === unknownSymbol) { + const namespaceName = getJsxNamespace(location); + resolvedNamespace = resolveName(location, namespaceName, ts.SymbolFlags.Namespace, /*diagnosticMessage*/ undefined, namespaceName, /*isUse*/ false); } - if (getJsxElementTypeAt(errorNode) === undefined) { - if (noImplicitAny) { - error(errorNode, ts.Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist); + if (resolvedNamespace) { + const candidate = resolveSymbol(getSymbol(getExportsOfSymbol(resolveSymbol(resolvedNamespace)), JsxNames.JSX, ts.SymbolFlags.Namespace)); + if (candidate && candidate !== unknownSymbol) { + if (links) { + links.jsxNamespace = candidate; + } + return candidate; } } - } - - function checkJsxOpeningLikeElementOrOpeningFragment(node: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment) { - const isNodeOpeningLikeElement = ts.isJsxOpeningLikeElement(node); - - if (isNodeOpeningLikeElement) { - checkGrammarJsxElement(node); + if (links) { + links.jsxNamespace = false; } + } + // JSX global fallback + const s = resolveSymbol(getGlobalSymbol(JsxNames.JSX, ts.SymbolFlags.Namespace, /*diagnosticMessage*/ undefined)); + if (s === unknownSymbol) { + return undefined!; // TODO: GH#18217 + } + return s!; // TODO: GH#18217 + } - checkJsxPreconditions(node); - - if (!getJsxNamespaceContainerForImplicitImport(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 jsxFactoryRefErr = diagnostics && compilerOptions.jsx === ts.JsxEmit.React ? ts.Diagnostics.Cannot_find_name_0 : undefined; - const jsxFactoryNamespace = getJsxNamespace(node); - const jsxFactoryLocation = isNodeOpeningLikeElement ? node.tagName : node; + /** + * 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: ts.__String, jsxNamespace: ts.Symbol): ts.__String | undefined { + // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [symbol] + const jsxElementAttribPropInterfaceSym = jsxNamespace && getSymbol(jsxNamespace.exports!, nameOfAttribPropContainer, ts.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 ts.__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 && jsxElementAttribPropInterfaceSym.declarations) { + // More than one property on ElementAttributesProperty is an error + error(jsxElementAttribPropInterfaceSym.declarations[0], ts.Diagnostics.The_global_type_JSX_0_may_not_have_more_than_one_property, ts.unescapeLeadingUnderscores(nameOfAttribPropContainer)); + } + } + return undefined; + } - // allow null as jsxFragmentFactory - let jsxFactorySym: ts.Symbol | undefined; - if (!(ts.isJsxOpeningFragment(node) && jsxFactoryNamespace === "null")) { - jsxFactorySym = resolveName(jsxFactoryLocation, jsxFactoryNamespace, ts.SymbolFlags.Value, jsxFactoryRefErr, jsxFactoryNamespace, /*isUse*/ true); - } + function getJsxLibraryManagedAttributes(jsxNamespace: ts.Symbol) { + // JSX.LibraryManagedAttributes [symbol] + return jsxNamespace && getSymbol(jsxNamespace.exports!, JsxNames.LibraryManagedAttributes, ts.SymbolFlags.Type); + } - if (jsxFactorySym) { - // Mark local symbol as referenced here because it might not have been marked - // if jsx emit was not jsxFactory as there wont be error being emitted - jsxFactorySym.isReferenced = ts.SymbolFlags.All; + /// 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); + } - // If react/jsxFactory symbol is alias, mark it as refereced - if (jsxFactorySym.flags & ts.SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(jsxFactorySym)) { - markAliasSymbolAsReferenced(jsxFactorySym); - } - } + function getJsxElementChildrenPropertyName(jsxNamespace: ts.Symbol): ts.__String | undefined { + return getNameFromJsxElementAttributesContainer(JsxNames.ElementChildrenAttributeNameContainer, jsxNamespace); + } - // For JsxFragment, mark jsx pragma as referenced via resolveName - if (ts.isJsxOpeningFragment(node)) { - const file = ts.getSourceFileOfNode(node); - const localJsxNamespace = getLocalJsxNamespace(file); - if (localJsxNamespace) { - resolveName(jsxFactoryLocation, localJsxNamespace, ts.SymbolFlags.Value, jsxFactoryRefErr, localJsxNamespace, /*isUse*/ true); - } - } + function getUninstantiatedJsxSignaturesOfType(elementType: ts.Type, caller: ts.JsxOpeningLikeElement): readonly ts.Signature[] { + if (elementType.flags & ts.TypeFlags.String) { + return [anySignature]; + } + else if (elementType.flags & ts.TypeFlags.StringLiteral) { + const intrinsicType = getIntrinsicAttributesTypeFromStringLiteralType(elementType as ts.StringLiteralType, caller); + if (!intrinsicType) { + error(caller, ts.Diagnostics.Property_0_does_not_exist_on_type_1, (elementType as ts.StringLiteralType).value, "JSX." + JsxNames.IntrinsicElements); + return ts.emptyArray; } + else { + const fakeSignature = createSignatureForJSXIntrinsic(caller, intrinsicType); + return [fakeSignature]; + } + } + const apparentElemType = getApparentType(elementType); + // Resolve the signatures, preferring constructor + let signatures = getSignaturesOfType(apparentElemType, ts.SignatureKind.Construct); + if (signatures.length === 0) { + // No construct signatures, try call signatures + signatures = getSignaturesOfType(apparentElemType, ts.SignatureKind.Call); + } + if (signatures.length === 0 && apparentElemType.flags & ts.TypeFlags.Union) { + // If each member has some combination of new/call signatures; make a union signature list for those + signatures = getUnionSignatures(ts.map((apparentElemType as ts.UnionType).types, t => getUninstantiatedJsxSignaturesOfType(t, caller))); + } + return signatures; + } - if (isNodeOpeningLikeElement) { - const jsxOpeningLikeNode = node ; - const sig = getResolvedSignature(jsxOpeningLikeNode); - checkDeprecatedSignature(sig, node); - checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(jsxOpeningLikeNode), getReturnTypeOfSignature(sig), jsxOpeningLikeNode); + function getIntrinsicAttributesTypeFromStringLiteralType(type: ts.StringLiteralType, location: ts.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 (!isErrorType(intrinsicElementsType)) { + const stringLiteralTypeName = type.value; + const intrinsicProp = getPropertyOfType(intrinsicElementsType, ts.escapeLeadingUnderscores(stringLiteralTypeName)); + if (intrinsicProp) { + return getTypeOfSymbol(intrinsicProp); + } + const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, stringType); + if (indexSignatureType) { + return indexSignatureType; } + return undefined; } + // If we need to report an error, we already done so here. So just return any to prevent any more error downstream + return anyType; + } - /** - * 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: ts.__String, isComparingJsxAttributes: boolean): boolean { - if (targetType.flags & ts.TypeFlags.Object) { - // For backwards compatibility a symbol-named property is satisfied by a string index signature. This - // is incorrect and inconsistent with element access expressions, where it is an error, so eventually - // we should remove this exception. - if (getPropertyOfObjectType(targetType, name) || - getApplicableIndexInfoForName(targetType, name) || - isLateBoundName(name) && getIndexInfoOfType(targetType, stringType) || - isComparingJsxAttributes && isHyphenatedJsxName(name)) { - // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. - return true; - } + function checkJsxReturnAssignableToAppropriateBound(refKind: ts.JsxReferenceKind, elemInstanceType: ts.Type, openingLikeElement: ts.JsxOpeningLikeElement) { + if (refKind === ts.JsxReferenceKind.Function) { + const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); + if (sfcReturnConstraint) { + checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_return_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); } - else if (targetType.flags & ts.TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) { - for (const t of (targetType as ts.UnionOrIntersectionType).types) { - if (isKnownProperty(t, name, isComparingJsxAttributes)) { - return true; - } - } + } + else if (refKind === ts.JsxReferenceKind.Component) { + const classConstraint = getJsxElementClassTypeAt(openingLikeElement); + if (classConstraint) { + // Issue an error if this return type isn't assignable to JSX.ElementClass, failing that + checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_instance_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); } - return false; + } + else { // Mixed + const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); + const classConstraint = getJsxElementClassTypeAt(openingLikeElement); + if (!sfcReturnConstraint || !classConstraint) { + return; + } + const combined = getUnionType([sfcReturnConstraint, classConstraint]); + checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement.tagName, ts.Diagnostics.Its_element_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain); } - function isExcessPropertyCheckTarget(type: ts.Type): boolean { - return !!(type.flags & ts.TypeFlags.Object && !(ts.getObjectFlags(type) & ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties) || - type.flags & ts.TypeFlags.NonPrimitive || - type.flags & ts.TypeFlags.Union && ts.some((type as ts.UnionType).types, isExcessPropertyCheckTarget) || - type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, isExcessPropertyCheckTarget)); + function generateInitialErrorChain(): ts.DiagnosticMessageChain { + const componentName = ts.getTextOfNode(openingLikeElement.tagName); + return ts.chainDiagnosticMessages(/* details */ undefined, ts.Diagnostics._0_cannot_be_used_as_a_JSX_component, componentName); } + } - function checkJsxExpression(node: ts.JsxExpression, checkMode?: CheckMode) { - checkGrammarJsxExpression(node); - if (node.expression) { - const type = checkExpression(node.expression, checkMode); - if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { - error(node, ts.Diagnostics.JSX_spread_child_must_be_an_array_type); - } - return type; + /** + * 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: ts.JsxOpeningLikeElement): ts.Type { + ts.Debug.assert(isJsxIntrinsicIdentifier(node.tagName)); + const links = getNodeLinks(node); + if (!links.resolvedJsxElementAttributesType) { + const symbol = getIntrinsicTagSymbol(node); + if (links.jsxFlags & ts.JsxFlags.IntrinsicNamedElement) { + return links.resolvedJsxElementAttributesType = getTypeOfSymbol(symbol) || errorType; + } + else if (links.jsxFlags & ts.JsxFlags.IntrinsicIndexedElement) { + return links.resolvedJsxElementAttributesType = + getIndexTypeOfType(getJsxType(JsxNames.IntrinsicElements, node), stringType) || errorType; } else { - return errorType; + return links.resolvedJsxElementAttributesType = errorType; } } + return links.resolvedJsxElementAttributesType; + } + + function getJsxElementClassTypeAt(location: ts.Node): ts.Type | undefined { + const type = getJsxType(JsxNames.ElementClass, location); + if (isErrorType(type)) + return undefined; + return type; + } - function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): ts.NodeFlags { - return s.valueDeclaration ? ts.getCombinedNodeFlags(s.valueDeclaration) : 0; + function getJsxElementTypeAt(location: ts.Node): ts.Type { + return getJsxType(JsxNames.Element, location); + } + + function getJsxStatelessElementTypeAt(location: ts.Node): ts.Type | undefined { + const jsxElementType = getJsxElementTypeAt(location); + if (jsxElementType) { + return getUnionType([jsxElementType, nullType]); } + } - /** - * 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 & ts.SymbolFlags.Method || ts.getCheckFlags(symbol) & ts.CheckFlags.SyntheticMethod) { - return true; - } - if (ts.isInJSFile(symbol.valueDeclaration)) { - const parent = symbol.valueDeclaration!.parent; - return parent && ts.isBinaryExpression(parent) && - ts.getAssignmentDeclarationKind(parent) === ts.AssignmentDeclarationKind.PrototypeProperty; - } + /** + * Returns all the properties of the Jsx.IntrinsicElements interface + */ + function getJsxIntrinsicTagNamesAt(location: ts.Node): ts.Symbol[] { + const intrinsics = getJsxType(JsxNames.IntrinsicElements, location); + return intrinsics ? getPropertiesOfType(intrinsics) : ts.emptyArray; + } + + function checkJsxPreconditions(errorNode: ts.Node) { + // Preconditions for using JSX + if ((compilerOptions.jsx || ts.JsxEmit.None) === ts.JsxEmit.None) { + error(errorNode, ts.Diagnostics.Cannot_use_JSX_unless_the_jsx_flag_is_provided); } - /** - * 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: ts.PropertyAccessExpression | ts.QualifiedName | ts.PropertyAccessExpression | ts.VariableDeclaration | ts.ParameterDeclaration | ts.ImportTypeNode | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.BindingElement, isSuper: boolean, writing: boolean, type: ts.Type, prop: ts.Symbol, reportError = true): boolean { + if (getJsxElementTypeAt(errorNode) === undefined) { + if (noImplicitAny) { + error(errorNode, ts.Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist); + } + } + } - const errorNode = !reportError ? undefined : - node.kind === ts.SyntaxKind.QualifiedName ? node.right : - node.kind === ts.SyntaxKind.ImportType ? node : - node.kind === ts.SyntaxKind.BindingElement && node.propertyName ? node.propertyName : node.name; + function checkJsxOpeningLikeElementOrOpeningFragment(node: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment) { + const isNodeOpeningLikeElement = ts.isJsxOpeningLikeElement(node); - return checkPropertyAccessibilityAtLocation(node, isSuper, writing, type, prop, errorNode); + if (isNodeOpeningLikeElement) { + checkGrammarJsxElement(node); } - /** - * Check whether the requested property can be accessed at the requested location. - * Returns true if node is a valid property access, and false otherwise. - * @param location The location node where we want to check if the property is accessible. - * @param isSuper True if the access is from `super.`. - * @param writing True if this is a write property access, false if it is a read property access. - * @param containingType 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. - * @param errorNode The node where we should report an invalid property access error, or undefined if we should not report errors. - */ - function checkPropertyAccessibilityAtLocation(location: ts.Node, isSuper: boolean, writing: boolean, containingType: ts.Type, prop: ts.Symbol, errorNode?: ts.Node): boolean { - const flags = ts.getDeclarationModifierFlagsFromSymbol(prop, writing); - - 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 < ts.ScriptTarget.ES2015) { - if (symbolHasNonMethodDeclaration(prop)) { - if (errorNode) { - error(errorNode, ts.Diagnostics.Only_public_and_protected_methods_of_the_base_class_are_accessible_via_the_super_keyword); - } - return false; - } - } - if (flags & ts.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. - if (errorNode) { - error(errorNode, ts.Diagnostics.Abstract_method_0_in_class_1_cannot_be_accessed_via_super_expression, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); - } - return false; - } + checkJsxPreconditions(node); + + if (!getJsxNamespaceContainerForImplicitImport(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 jsxFactoryRefErr = diagnostics && compilerOptions.jsx === ts.JsxEmit.React ? ts.Diagnostics.Cannot_find_name_0 : undefined; + const jsxFactoryNamespace = getJsxNamespace(node); + const jsxFactoryLocation = isNodeOpeningLikeElement ? node.tagName : node; + + // allow null as jsxFragmentFactory + let jsxFactorySym: ts.Symbol | undefined; + if (!(ts.isJsxOpeningFragment(node) && jsxFactoryNamespace === "null")) { + jsxFactorySym = resolveName(jsxFactoryLocation, jsxFactoryNamespace, ts.SymbolFlags.Value, jsxFactoryRefErr, jsxFactoryNamespace, /*isUse*/ true); } - // Referencing abstract properties within their own constructors is not allowed - if ((flags & ts.ModifierFlags.Abstract) && symbolHasNonMethodDeclaration(prop) && - (ts.isThisProperty(location) || ts.isThisInitializedObjectBindingExpression(location) || ts.isObjectBindingPattern(location.parent) && ts.isThisInitializedDeclaration(location.parent.parent))) { - const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!); - if (declaringClassDeclaration && isNodeUsedDuringClassInitialization(location)) { - if (errorNode) { - error(errorNode, ts.Diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, symbolToString(prop), ts.getTextOfIdentifierOrLiteral(declaringClassDeclaration.name!)); - } - return false; + if (jsxFactorySym) { + // Mark local symbol as referenced here because it might not have been marked + // if jsx emit was not jsxFactory as there wont be error being emitted + jsxFactorySym.isReferenced = ts.SymbolFlags.All; + + // If react/jsxFactory symbol is alias, mark it as refereced + if (jsxFactorySym.flags & ts.SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(jsxFactorySym)) { + markAliasSymbolAsReferenced(jsxFactorySym); } } - // Public properties are otherwise accessible. - if (!(flags & ts.ModifierFlags.NonPublicAccessibilityModifier)) { - return true; + // For JsxFragment, mark jsx pragma as referenced via resolveName + if (ts.isJsxOpeningFragment(node)) { + const file = ts.getSourceFileOfNode(node); + const localJsxNamespace = getLocalJsxNamespace(file); + if (localJsxNamespace) { + resolveName(jsxFactoryLocation, localJsxNamespace, ts.SymbolFlags.Value, jsxFactoryRefErr, localJsxNamespace, /*isUse*/ true); + } } + } - // Property is known to be private or protected at this point + if (isNodeOpeningLikeElement) { + const jsxOpeningLikeNode = node ; + const sig = getResolvedSignature(jsxOpeningLikeNode); + checkDeprecatedSignature(sig, node); + checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(jsxOpeningLikeNode), getReturnTypeOfSignature(sig), jsxOpeningLikeNode); + } + } - // Private property is accessible if the property is within the declaring class - if (flags & ts.ModifierFlags.Private) { - const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!)!; - if (!isNodeWithinClass(location, declaringClassDeclaration)) { - if (errorNode) { - error(errorNode, ts.Diagnostics.Property_0_is_private_and_only_accessible_within_class_1, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); - } - return false; - } + /** + * 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: ts.__String, isComparingJsxAttributes: boolean): boolean { + if (targetType.flags & ts.TypeFlags.Object) { + // For backwards compatibility a symbol-named property is satisfied by a string index signature. This + // is incorrect and inconsistent with element access expressions, where it is an error, so eventually + // we should remove this exception. + if (getPropertyOfObjectType(targetType, name) || + getApplicableIndexInfoForName(targetType, name) || + isLateBoundName(name) && getIndexInfoOfType(targetType, stringType) || + isComparingJsxAttributes && isHyphenatedJsxName(name)) { + // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. return true; } + } + else if (targetType.flags & ts.TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) { + for (const t of (targetType as ts.UnionOrIntersectionType).types) { + if (isKnownProperty(t, name, isComparingJsxAttributes)) { + return true; + } + } + } + return false; + } - // Property is known to be protected at this point + function isExcessPropertyCheckTarget(type: ts.Type): boolean { + return !!(type.flags & ts.TypeFlags.Object && !(ts.getObjectFlags(type) & ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties) || + type.flags & ts.TypeFlags.NonPrimitive || + type.flags & ts.TypeFlags.Union && ts.some((type as ts.UnionType).types, isExcessPropertyCheckTarget) || + type.flags & ts.TypeFlags.Intersection && ts.every((type as ts.IntersectionType).types, isExcessPropertyCheckTarget)); + } - // All protected properties of a supertype are accessible in a super access - if (isSuper) { - return true; + function checkJsxExpression(node: ts.JsxExpression, checkMode?: CheckMode) { + checkGrammarJsxExpression(node); + if (node.expression) { + const type = checkExpression(node.expression, checkMode); + if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { + error(node, ts.Diagnostics.JSX_spread_child_must_be_an_array_type); } + return type; + } + else { + return errorType; + } + } - // Find the first enclosing class that has the declaring classes of the protected constituents - // of the property as base classes - let enclosingClass = forEachEnclosingClass(location, enclosingDeclaration => { - const enclosingClass = getDeclaredTypeOfSymbol(getSymbolOfNode(enclosingDeclaration)!) as ts.InterfaceType; - return isClassDerivedFromDeclaringClasses(enclosingClass, prop, writing); - }); - // 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 disallowed - enclosingClass = getEnclosingClassFromThisParameter(location); - enclosingClass = enclosingClass && isClassDerivedFromDeclaringClasses(enclosingClass, prop, writing); - if (flags & ts.ModifierFlags.Static || !enclosingClass) { + function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): ts.NodeFlags { + return s.valueDeclaration ? ts.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 & ts.SymbolFlags.Method || ts.getCheckFlags(symbol) & ts.CheckFlags.SyntheticMethod) { + return true; + } + if (ts.isInJSFile(symbol.valueDeclaration)) { + const parent = symbol.valueDeclaration!.parent; + return parent && ts.isBinaryExpression(parent) && + ts.getAssignmentDeclarationKind(parent) === ts.AssignmentDeclarationKind.PrototypeProperty; + } + } + + /** + * 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: ts.PropertyAccessExpression | ts.QualifiedName | ts.PropertyAccessExpression | ts.VariableDeclaration | ts.ParameterDeclaration | ts.ImportTypeNode | ts.PropertyAssignment | ts.ShorthandPropertyAssignment | ts.BindingElement, isSuper: boolean, writing: boolean, type: ts.Type, prop: ts.Symbol, reportError = true): boolean { + + const errorNode = !reportError ? undefined : + node.kind === ts.SyntaxKind.QualifiedName ? node.right : + node.kind === ts.SyntaxKind.ImportType ? node : + node.kind === ts.SyntaxKind.BindingElement && node.propertyName ? node.propertyName : node.name; + + return checkPropertyAccessibilityAtLocation(node, isSuper, writing, type, prop, errorNode); + } + + /** + * Check whether the requested property can be accessed at the requested location. + * Returns true if node is a valid property access, and false otherwise. + * @param location The location node where we want to check if the property is accessible. + * @param isSuper True if the access is from `super.`. + * @param writing True if this is a write property access, false if it is a read property access. + * @param containingType 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. + * @param errorNode The node where we should report an invalid property access error, or undefined if we should not report errors. + */ + function checkPropertyAccessibilityAtLocation(location: ts.Node, isSuper: boolean, writing: boolean, containingType: ts.Type, prop: ts.Symbol, errorNode?: ts.Node): boolean { + const flags = ts.getDeclarationModifierFlagsFromSymbol(prop, writing); + + 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 < ts.ScriptTarget.ES2015) { + if (symbolHasNonMethodDeclaration(prop)) { if (errorNode) { - error(errorNode, ts.Diagnostics.Property_0_is_protected_and_only_accessible_within_class_1_and_its_subclasses, symbolToString(prop), typeToString(getDeclaringClass(prop) || containingType)); + error(errorNode, ts.Diagnostics.Only_public_and_protected_methods_of_the_base_class_are_accessible_via_the_super_keyword); } return false; } } - // No further restrictions for static properties - if (flags & ts.ModifierFlags.Static) { - return true; - } - if (containingType.flags & ts.TypeFlags.TypeParameter) { - // get the original type -- represented as the type constraint of the 'this' type - containingType = (containingType as ts.TypeParameter).isThisType ? getConstraintOfTypeParameter(containingType as ts.TypeParameter)! : getBaseConstraintOfType(containingType as ts.TypeParameter)!; // TODO: GH#18217 Use a different variable that's allowed to be undefined - } - if (!containingType || !hasBaseType(containingType, enclosingClass)) { + if (flags & ts.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. if (errorNode) { - error(errorNode, ts.Diagnostics.Property_0_is_protected_and_only_accessible_through_an_instance_of_class_1_This_is_an_instance_of_class_2, symbolToString(prop), typeToString(enclosingClass), typeToString(containingType)); + error(errorNode, ts.Diagnostics.Abstract_method_0_in_class_1_cannot_be_accessed_via_super_expression, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); } return false; } - return true; } - function getEnclosingClassFromThisParameter(node: ts.Node): ts.InterfaceType | undefined { - const thisParameter = getThisParameterFromNodeContext(node); - let thisType = thisParameter?.type && getTypeFromTypeNode(thisParameter.type); - if (thisType && thisType.flags & ts.TypeFlags.TypeParameter) { - thisType = getConstraintOfTypeParameter(thisType as ts.TypeParameter); - } - if (thisType && ts.getObjectFlags(thisType) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference)) { - return getTargetType(thisType) as ts.InterfaceType; + // Referencing abstract properties within their own constructors is not allowed + if ((flags & ts.ModifierFlags.Abstract) && symbolHasNonMethodDeclaration(prop) && + (ts.isThisProperty(location) || ts.isThisInitializedObjectBindingExpression(location) || ts.isObjectBindingPattern(location.parent) && ts.isThisInitializedDeclaration(location.parent.parent))) { + const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!); + if (declaringClassDeclaration && isNodeUsedDuringClassInitialization(location)) { + if (errorNode) { + error(errorNode, ts.Diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, symbolToString(prop), ts.getTextOfIdentifierOrLiteral(declaringClassDeclaration.name!)); + } + return false; } - return undefined; } - function getThisParameterFromNodeContext(node: ts.Node) { - const thisContainer = ts.getThisContainer(node, /* includeArrowFunctions */ false); - return thisContainer && ts.isFunctionLike(thisContainer) ? ts.getThisParameter(thisContainer) : undefined; + // Public properties are otherwise accessible. + if (!(flags & ts.ModifierFlags.NonPublicAccessibilityModifier)) { + return true; } - function symbolHasNonMethodDeclaration(symbol: ts.Symbol) { - return !!forEachProperty(symbol, prop => !(prop.flags & ts.SymbolFlags.Method)); - } + // Property is known to be private or protected at this point - function checkNonNullExpression(node: ts.Expression | ts.QualifiedName) { - return checkNonNullType(checkExpression(node), node); + // Private property is accessible if the property is within the declaring class + if (flags & ts.ModifierFlags.Private) { + const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!)!; + if (!isNodeWithinClass(location, declaringClassDeclaration)) { + if (errorNode) { + error(errorNode, ts.Diagnostics.Property_0_is_private_and_only_accessible_within_class_1, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); + } + return false; + } + return true; } - function isNullableType(type: ts.Type) { - return !!((strictNullChecks ? getFalsyFlags(type) : type.flags) & ts.TypeFlags.Nullable); - } + // Property is known to be protected at this point - function getNonNullableTypeIfNeeded(type: ts.Type) { - return isNullableType(type) ? getNonNullableType(type) : type; + // All protected properties of a supertype are accessible in a super access + if (isSuper) { + return true; } - function reportObjectPossiblyNullOrUndefinedError(node: ts.Node, flags: ts.TypeFlags) { - error(node, flags & ts.TypeFlags.Undefined ? flags & ts.TypeFlags.Null ? - ts.Diagnostics.Object_is_possibly_null_or_undefined : - ts.Diagnostics.Object_is_possibly_undefined : - ts.Diagnostics.Object_is_possibly_null); + // Find the first enclosing class that has the declaring classes of the protected constituents + // of the property as base classes + let enclosingClass = forEachEnclosingClass(location, enclosingDeclaration => { + const enclosingClass = getDeclaredTypeOfSymbol(getSymbolOfNode(enclosingDeclaration)!) as ts.InterfaceType; + return isClassDerivedFromDeclaringClasses(enclosingClass, prop, writing); + }); + // 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 disallowed + enclosingClass = getEnclosingClassFromThisParameter(location); + enclosingClass = enclosingClass && isClassDerivedFromDeclaringClasses(enclosingClass, prop, writing); + if (flags & ts.ModifierFlags.Static || !enclosingClass) { + if (errorNode) { + error(errorNode, ts.Diagnostics.Property_0_is_protected_and_only_accessible_within_class_1_and_its_subclasses, symbolToString(prop), typeToString(getDeclaringClass(prop) || containingType)); + } + return false; + } } - function reportCannotInvokePossiblyNullOrUndefinedError(node: ts.Node, flags: ts.TypeFlags) { - error(node, flags & ts.TypeFlags.Undefined ? flags & ts.TypeFlags.Null ? - ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_null_or_undefined : - ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_undefined : - ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_null); + // No further restrictions for static properties + if (flags & ts.ModifierFlags.Static) { + return true; } - function checkNonNullTypeWithReporter(type: ts.Type, node: ts.Node, reportError: (node: ts.Node, kind: ts.TypeFlags) => void): ts.Type { - if (strictNullChecks && type.flags & ts.TypeFlags.Unknown) { - error(node, ts.Diagnostics.Object_is_of_type_unknown); - return errorType; - } - const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & ts.TypeFlags.Nullable; - if (kind) { - reportError(node, kind); - const t = getNonNullableType(type); - return t.flags & (ts.TypeFlags.Nullable | ts.TypeFlags.Never) ? errorType : t; + if (containingType.flags & ts.TypeFlags.TypeParameter) { + // get the original type -- represented as the type constraint of the 'this' type + containingType = (containingType as ts.TypeParameter).isThisType ? getConstraintOfTypeParameter(containingType as ts.TypeParameter)! : getBaseConstraintOfType(containingType as ts.TypeParameter)!; // TODO: GH#18217 Use a different variable that's allowed to be undefined + } + if (!containingType || !hasBaseType(containingType, enclosingClass)) { + if (errorNode) { + error(errorNode, ts.Diagnostics.Property_0_is_protected_and_only_accessible_through_an_instance_of_class_1_This_is_an_instance_of_class_2, symbolToString(prop), typeToString(enclosingClass), typeToString(containingType)); } - return type; + return false; } + return true; + } - function checkNonNullType(type: ts.Type, node: ts.Node) { - return checkNonNullTypeWithReporter(type, node, reportObjectPossiblyNullOrUndefinedError); + function getEnclosingClassFromThisParameter(node: ts.Node): ts.InterfaceType | undefined { + const thisParameter = getThisParameterFromNodeContext(node); + let thisType = thisParameter?.type && getTypeFromTypeNode(thisParameter.type); + if (thisType && thisType.flags & ts.TypeFlags.TypeParameter) { + thisType = getConstraintOfTypeParameter(thisType as ts.TypeParameter); } - - function checkNonNullNonVoidType(type: ts.Type, node: ts.Node): ts.Type { - const nonNullType = checkNonNullType(type, node); - if (nonNullType.flags & ts.TypeFlags.Void) { - error(node, ts.Diagnostics.Object_is_possibly_undefined); - } - return nonNullType; + if (thisType && ts.getObjectFlags(thisType) & (ts.ObjectFlags.ClassOrInterface | ts.ObjectFlags.Reference)) { + return getTargetType(thisType) as ts.InterfaceType; } + return undefined; + } - function checkPropertyAccessExpression(node: ts.PropertyAccessExpression, checkMode: CheckMode | undefined) { - return node.flags & ts.NodeFlags.OptionalChain ? checkPropertyAccessChain(node as ts.PropertyAccessChain, checkMode) : - checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullExpression(node.expression), node.name, checkMode); - } + function getThisParameterFromNodeContext(node: ts.Node) { + const thisContainer = ts.getThisContainer(node, /* includeArrowFunctions */ false); + return thisContainer && ts.isFunctionLike(thisContainer) ? ts.getThisParameter(thisContainer) : undefined; + } - function checkPropertyAccessChain(node: ts.PropertyAccessChain, checkMode: CheckMode | undefined) { - const leftType = checkExpression(node.expression); - const nonOptionalType = getOptionalExpressionType(leftType, node.expression); - return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name, checkMode), node, nonOptionalType !== leftType); - } + function symbolHasNonMethodDeclaration(symbol: ts.Symbol) { + return !!forEachProperty(symbol, prop => !(prop.flags & ts.SymbolFlags.Method)); + } - function checkQualifiedName(node: ts.QualifiedName, checkMode: CheckMode | undefined) { - const leftType = ts.isPartOfTypeQuery(node) && ts.isThisIdentifier(node.left) ? checkNonNullType(checkThisExpression(node.left), node.left) : checkNonNullExpression(node.left); - return checkPropertyAccessExpressionOrQualifiedName(node, node.left, leftType, node.right, checkMode); - } + function checkNonNullExpression(node: ts.Expression | ts.QualifiedName) { + return checkNonNullType(checkExpression(node), node); + } - function isMethodAccessForCall(node: ts.Node) { - while (node.parent.kind === ts.SyntaxKind.ParenthesizedExpression) { - node = node.parent; - } - return ts.isCallOrNewExpression(node.parent) && node.parent.expression === node; + function isNullableType(type: ts.Type) { + return !!((strictNullChecks ? getFalsyFlags(type) : type.flags) & ts.TypeFlags.Nullable); + } + + function getNonNullableTypeIfNeeded(type: ts.Type) { + return isNullableType(type) ? getNonNullableType(type) : type; + } + + function reportObjectPossiblyNullOrUndefinedError(node: ts.Node, flags: ts.TypeFlags) { + error(node, flags & ts.TypeFlags.Undefined ? flags & ts.TypeFlags.Null ? + ts.Diagnostics.Object_is_possibly_null_or_undefined : + ts.Diagnostics.Object_is_possibly_undefined : + ts.Diagnostics.Object_is_possibly_null); + } + function reportCannotInvokePossiblyNullOrUndefinedError(node: ts.Node, flags: ts.TypeFlags) { + error(node, flags & ts.TypeFlags.Undefined ? flags & ts.TypeFlags.Null ? + ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_null_or_undefined : + ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_undefined : + ts.Diagnostics.Cannot_invoke_an_object_which_is_possibly_null); + } + function checkNonNullTypeWithReporter(type: ts.Type, node: ts.Node, reportError: (node: ts.Node, kind: ts.TypeFlags) => void): ts.Type { + if (strictNullChecks && type.flags & ts.TypeFlags.Unknown) { + error(node, ts.Diagnostics.Object_is_of_type_unknown); + return errorType; } + const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & ts.TypeFlags.Nullable; + if (kind) { + reportError(node, kind); + const t = getNonNullableType(type); + return t.flags & (ts.TypeFlags.Nullable | ts.TypeFlags.Never) ? errorType : t; + } + return type; + } - // Lookup the private identifier lexically. - function lookupSymbolForPrivateIdentifierDeclaration(propName: ts.__String, location: ts.Node): ts.Symbol | undefined { - for (let containingClass = ts.getContainingClass(location); !!containingClass; containingClass = ts.getContainingClass(containingClass)) { - const { symbol } = containingClass; - const name = ts.getSymbolNameForPrivateIdentifier(symbol, propName); - const prop = (symbol.members && symbol.members.get(name)) || (symbol.exports && symbol.exports.get(name)); - if (prop) { - return prop; - } - } + function checkNonNullType(type: ts.Type, node: ts.Node) { + return checkNonNullTypeWithReporter(type, node, reportObjectPossiblyNullOrUndefinedError); + } + + function checkNonNullNonVoidType(type: ts.Type, node: ts.Node): ts.Type { + const nonNullType = checkNonNullType(type, node); + if (nonNullType.flags & ts.TypeFlags.Void) { + error(node, ts.Diagnostics.Object_is_possibly_undefined); } + return nonNullType; + } - function checkGrammarPrivateIdentifierExpression(privId: ts.PrivateIdentifier): boolean { - if (!ts.getContainingClass(privId)) { - return grammarErrorOnNode(privId, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - } + function checkPropertyAccessExpression(node: ts.PropertyAccessExpression, checkMode: CheckMode | undefined) { + return node.flags & ts.NodeFlags.OptionalChain ? checkPropertyAccessChain(node as ts.PropertyAccessChain, checkMode) : + checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullExpression(node.expression), node.name, checkMode); + } - if (!ts.isForInStatement(privId.parent)) { - if (!ts.isExpressionNode(privId)) { - return grammarErrorOnNode(privId, ts.Diagnostics.Private_identifiers_are_only_allowed_in_class_bodies_and_may_only_be_used_as_part_of_a_class_member_declaration_property_access_or_on_the_left_hand_side_of_an_in_expression); - } + function checkPropertyAccessChain(node: ts.PropertyAccessChain, checkMode: CheckMode | undefined) { + const leftType = checkExpression(node.expression); + const nonOptionalType = getOptionalExpressionType(leftType, node.expression); + return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name, checkMode), node, nonOptionalType !== leftType); + } - const isInOperation = ts.isBinaryExpression(privId.parent) && privId.parent.operatorToken.kind === ts.SyntaxKind.InKeyword; - if (!getSymbolForPrivateIdentifierExpression(privId) && !isInOperation) { - return grammarErrorOnNode(privId, ts.Diagnostics.Cannot_find_name_0, ts.idText(privId)); - } - } + function checkQualifiedName(node: ts.QualifiedName, checkMode: CheckMode | undefined) { + const leftType = ts.isPartOfTypeQuery(node) && ts.isThisIdentifier(node.left) ? checkNonNullType(checkThisExpression(node.left), node.left) : checkNonNullExpression(node.left); + return checkPropertyAccessExpressionOrQualifiedName(node, node.left, leftType, node.right, checkMode); + } - return false; + function isMethodAccessForCall(node: ts.Node) { + while (node.parent.kind === ts.SyntaxKind.ParenthesizedExpression) { + node = node.parent; } + return ts.isCallOrNewExpression(node.parent) && node.parent.expression === node; + } - function checkPrivateIdentifierExpression(privId: ts.PrivateIdentifier): ts.Type { - checkGrammarPrivateIdentifierExpression(privId); - const symbol = getSymbolForPrivateIdentifierExpression(privId); - if (symbol) { - markPropertyAsReferenced(symbol, /* nodeForCheckWriteOnly: */ undefined, /* isThisAccess: */ false); + // Lookup the private identifier lexically. + function lookupSymbolForPrivateIdentifierDeclaration(propName: ts.__String, location: ts.Node): ts.Symbol | undefined { + for (let containingClass = ts.getContainingClass(location); !!containingClass; containingClass = ts.getContainingClass(containingClass)) { + const { symbol } = containingClass; + const name = ts.getSymbolNameForPrivateIdentifier(symbol, propName); + const prop = (symbol.members && symbol.members.get(name)) || (symbol.exports && symbol.exports.get(name)); + if (prop) { + return prop; } - return anyType; + } + } + + function checkGrammarPrivateIdentifierExpression(privId: ts.PrivateIdentifier): boolean { + if (!ts.getContainingClass(privId)) { + return grammarErrorOnNode(privId, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); } - function getSymbolForPrivateIdentifierExpression(privId: ts.PrivateIdentifier): ts.Symbol | undefined { + if (!ts.isForInStatement(privId.parent)) { if (!ts.isExpressionNode(privId)) { - return undefined; + return grammarErrorOnNode(privId, ts.Diagnostics.Private_identifiers_are_only_allowed_in_class_bodies_and_may_only_be_used_as_part_of_a_class_member_declaration_property_access_or_on_the_left_hand_side_of_an_in_expression); } - const links = getNodeLinks(privId); - if (links.resolvedSymbol === undefined) { - links.resolvedSymbol = lookupSymbolForPrivateIdentifierDeclaration(privId.escapedText, privId); + const isInOperation = ts.isBinaryExpression(privId.parent) && privId.parent.operatorToken.kind === ts.SyntaxKind.InKeyword; + if (!getSymbolForPrivateIdentifierExpression(privId) && !isInOperation) { + return grammarErrorOnNode(privId, ts.Diagnostics.Cannot_find_name_0, ts.idText(privId)); } - return links.resolvedSymbol; } - function getPrivateIdentifierPropertyOfType(leftType: ts.Type, lexicallyScopedIdentifier: ts.Symbol): ts.Symbol | undefined { - return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName); + return false; + } + + function checkPrivateIdentifierExpression(privId: ts.PrivateIdentifier): ts.Type { + checkGrammarPrivateIdentifierExpression(privId); + const symbol = getSymbolForPrivateIdentifierExpression(privId); + if (symbol) { + markPropertyAsReferenced(symbol, /* nodeForCheckWriteOnly: */ undefined, /* isThisAccess: */ false); } + return anyType; + } - function checkPrivateIdentifierPropertyAccess(leftType: ts.Type, right: ts.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) { - ts.forEach(properties, (symbol: ts.Symbol) => { - const decl = symbol.valueDeclaration; - if (decl && ts.isNamedDeclaration(decl) && ts.isPrivateIdentifier(decl.name) && decl.name.escapedText === right.escapedText) { - propertyOnType = symbol; - return true; - } - }); - } - const diagName = diagnosticName(right); - if (propertyOnType) { - const typeValueDecl = ts.Debug.checkDefined(propertyOnType.valueDeclaration); - const typeClass = ts.Debug.checkDefined(ts.getContainingClass(typeValueDecl)); - // 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?.valueDeclaration) { - const lexicalValueDecl = lexicallyScopedIdentifier.valueDeclaration; - const lexicalClass = ts.getContainingClass(lexicalValueDecl); - ts.Debug.assert(!!lexicalClass); - if (ts.findAncestor(lexicalClass, n => typeClass === n)) { - const diagnostic = error(right, ts.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)); - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(lexicalValueDecl, ts.Diagnostics.The_shadowing_declaration_of_0_is_defined_here, diagName), ts.createDiagnosticForNode(typeValueDecl, ts.Diagnostics.The_declaration_of_0_that_you_probably_intended_to_use_is_defined_here, diagName)); - return true; - } - } - error(right, ts.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 getSymbolForPrivateIdentifierExpression(privId: ts.PrivateIdentifier): ts.Symbol | undefined { + if (!ts.isExpressionNode(privId)) { + return undefined; } - function isThisPropertyAccessInConstructor(node: ts.ElementAccessExpression | ts.PropertyAccessExpression | ts.QualifiedName, prop: ts.Symbol) { - return (isConstructorDeclaredProperty(prop) || ts.isThisProperty(node) && isAutoTypedProperty(prop)) - && ts.getThisContainer(node, /*includeArrowFunctions*/ true) === getDeclaringConstructor(prop); + const links = getNodeLinks(privId); + if (links.resolvedSymbol === undefined) { + links.resolvedSymbol = lookupSymbolForPrivateIdentifierDeclaration(privId.escapedText, privId); } + return links.resolvedSymbol; + } - function checkPropertyAccessExpressionOrQualifiedName(node: ts.PropertyAccessExpression | ts.QualifiedName, left: ts.Expression | ts.QualifiedName, leftType: ts.Type, right: ts.Identifier | ts.PrivateIdentifier, checkMode: CheckMode | undefined) { - const parentSymbol = getNodeLinks(left).resolvedSymbol; - const assignmentKind = ts.getAssignmentTargetKind(node); - const apparentType = getApparentType(assignmentKind !== ts.AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(leftType) : leftType); - const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType; - let prop: ts.Symbol | undefined; - if (ts.isPrivateIdentifier(right)) { - if (languageVersion < ts.ScriptTarget.ESNext) { - if (assignmentKind !== ts.AssignmentKind.None) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ClassPrivateFieldSet); - } - if (assignmentKind !== ts.AssignmentKind.Definite) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ClassPrivateFieldGet); - } - } + function getPrivateIdentifierPropertyOfType(leftType: ts.Type, lexicallyScopedIdentifier: ts.Symbol): ts.Symbol | undefined { + return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName); + } - const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(right.escapedText, right); - if (assignmentKind && lexicallyScopedSymbol && lexicallyScopedSymbol.valueDeclaration && ts.isMethodDeclaration(lexicallyScopedSymbol.valueDeclaration)) { - grammarErrorOnNode(right, ts.Diagnostics.Cannot_assign_to_private_method_0_Private_methods_are_not_writable, ts.idText(right)); + function checkPrivateIdentifierPropertyAccess(leftType: ts.Type, right: ts.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) { + ts.forEach(properties, (symbol: ts.Symbol) => { + const decl = symbol.valueDeclaration; + if (decl && ts.isNamedDeclaration(decl) && ts.isPrivateIdentifier(decl.name) && decl.name.escapedText === right.escapedText) { + propertyOnType = symbol; + return true; + } + }); + } + const diagName = diagnosticName(right); + if (propertyOnType) { + const typeValueDecl = ts.Debug.checkDefined(propertyOnType.valueDeclaration); + const typeClass = ts.Debug.checkDefined(ts.getContainingClass(typeValueDecl)); + // 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?.valueDeclaration) { + const lexicalValueDecl = lexicallyScopedIdentifier.valueDeclaration; + const lexicalClass = ts.getContainingClass(lexicalValueDecl); + ts.Debug.assert(!!lexicalClass); + if (ts.findAncestor(lexicalClass, n => typeClass === n)) { + const diagnostic = error(right, ts.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)); + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(lexicalValueDecl, ts.Diagnostics.The_shadowing_declaration_of_0_is_defined_here, diagName), ts.createDiagnosticForNode(typeValueDecl, ts.Diagnostics.The_declaration_of_0_that_you_probably_intended_to_use_is_defined_here, diagName)); + return true; } + } + error(right, ts.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 isThisPropertyAccessInConstructor(node: ts.ElementAccessExpression | ts.PropertyAccessExpression | ts.QualifiedName, prop: ts.Symbol) { + return (isConstructorDeclaredProperty(prop) || ts.isThisProperty(node) && isAutoTypedProperty(prop)) + && ts.getThisContainer(node, /*includeArrowFunctions*/ true) === getDeclaringConstructor(prop); + } - if (isAnyLike) { - if (lexicallyScopedSymbol) { - return isErrorType(apparentType) ? errorType : apparentType; - } - if (!ts.getContainingClass(right)) { - grammarErrorOnNode(right, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - return anyType; - } + function checkPropertyAccessExpressionOrQualifiedName(node: ts.PropertyAccessExpression | ts.QualifiedName, left: ts.Expression | ts.QualifiedName, leftType: ts.Type, right: ts.Identifier | ts.PrivateIdentifier, checkMode: CheckMode | undefined) { + const parentSymbol = getNodeLinks(left).resolvedSymbol; + const assignmentKind = ts.getAssignmentTargetKind(node); + const apparentType = getApparentType(assignmentKind !== ts.AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(leftType) : leftType); + const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType; + let prop: ts.Symbol | undefined; + if (ts.isPrivateIdentifier(right)) { + if (languageVersion < ts.ScriptTarget.ESNext) { + if (assignmentKind !== ts.AssignmentKind.None) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ClassPrivateFieldSet); } - 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 { - const isSetonlyAccessor = prop && prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); - if (isSetonlyAccessor && assignmentKind !== ts.AssignmentKind.Definite) { - error(node, ts.Diagnostics.Private_accessor_was_defined_without_a_getter); - } + if (assignmentKind !== ts.AssignmentKind.Definite) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ClassPrivateFieldGet); } } - else { - if (isAnyLike) { - if (ts.isIdentifier(left) && parentSymbol) { - markAliasReferenced(parentSymbol, node); - } - return isErrorType(apparentType) ? errorType : apparentType; - } - prop = getPropertyOfType(apparentType, right.escapedText, /*skipObjectFunctionPropertyAugment*/ false, /*includeTypeOnlyMembers*/ node.kind === ts.SyntaxKind.QualifiedName); + + const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(right.escapedText, right); + if (assignmentKind && lexicallyScopedSymbol && lexicallyScopedSymbol.valueDeclaration && ts.isMethodDeclaration(lexicallyScopedSymbol.valueDeclaration)) { + grammarErrorOnNode(right, ts.Diagnostics.Cannot_assign_to_private_method_0_Private_methods_are_not_writable, ts.idText(right)); } - // In `Foo.Bar.Baz`, 'Foo' is not referenced if 'Bar' is a const enum or a module containing only const enums. - // `Foo` is also not referenced in `enum FooCopy { Bar = Foo.Bar }`, because the enum member value gets inlined - // here even if `Foo` is not a const enum. - // - // The exceptions are: - // 1. if 'isolatedModules' is enabled, because the const enum value will not be inlined, and - // 2. if 'preserveConstEnums' is enabled and the expression is itself an export, e.g. `export = Foo.Bar.Baz`. - if (ts.isIdentifier(left) && parentSymbol && (compilerOptions.isolatedModules || - !(prop && (isConstEnumOrConstEnumOnlyModule(prop) || prop.flags & ts.SymbolFlags.EnumMember && node.parent.kind === ts.SyntaxKind.EnumMember)) || - ts.shouldPreserveConstEnums(compilerOptions) && isExportOrExportExpression(node))) { - markAliasReferenced(parentSymbol, node); - } - - let propType: ts.Type; - if (!prop) { - const indexInfo = !ts.isPrivateIdentifier(right) && (assignmentKind === ts.AssignmentKind.None || !isGenericObjectType(leftType) || ts.isThisTypeParameter(leftType)) ? - getApplicableIndexInfoForName(apparentType, right.escapedText) : undefined; - if (!(indexInfo && indexInfo.type)) { - const isUncheckedJS = isUncheckedJSSuggestion(node, leftType.symbol, /*excludeClasses*/ true); - if (!isUncheckedJS && isJSLiteralType(leftType)) { - return anyType; - } - if (leftType.symbol === globalThisSymbol) { - if (globalThisSymbol.exports!.has(right.escapedText) && (globalThisSymbol.exports!.get(right.escapedText)!.flags & ts.SymbolFlags.BlockScoped)) { - error(right, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(right.escapedText), typeToString(leftType)); - } - else if (noImplicitAny) { - error(right, ts.Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(leftType)); - } - return anyType; - } - if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { - reportNonexistentProperty(right, ts.isThisTypeParameter(leftType) ? apparentType : leftType, isUncheckedJS); - } - return errorType; - } - if (indexInfo.isReadonly && (ts.isAssignmentTarget(node) || ts.isDeleteTarget(node))) { - error(node, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType)); - } - propType = (compilerOptions.noUncheckedIndexedAccess && !ts.isAssignmentTarget(node)) ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; - if (compilerOptions.noPropertyAccessFromIndexSignature && ts.isPropertyAccessExpression(node)) { - error(right, ts.Diagnostics.Property_0_comes_from_an_index_signature_so_it_must_be_accessed_with_0, ts.unescapeLeadingUnderscores(right.escapedText)); + if (isAnyLike) { + if (lexicallyScopedSymbol) { + return isErrorType(apparentType) ? errorType : apparentType; } - if (indexInfo.declaration && ts.getCombinedNodeFlags(indexInfo.declaration) & ts.NodeFlags.Deprecated) { - addDeprecatedSuggestion(right, [indexInfo.declaration], right.escapedText as string); + if (!ts.getContainingClass(right)) { + grammarErrorOnNode(right, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + return anyType; } } + 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 (isDeprecatedSymbol(prop) && isUncalledFunctionReference(node, prop) && prop.declarations) { - addDeprecatedSuggestion(right, prop.declarations, right.escapedText as string); - } - checkPropertyNotUsedBeforeDeclaration(prop, node, right); - markPropertyAsReferenced(prop, node, isSelfTypeAccess(left, parentSymbol)); - getNodeLinks(node).resolvedSymbol = prop; - const writing = ts.isWriteAccess(node); - checkPropertyAccessibility(node, left.kind === ts.SyntaxKind.SuperKeyword, writing, apparentType, prop); - if (isAssignmentToReadonlyEntity(node as ts.Expression, prop, assignmentKind)) { - error(right, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, ts.idText(right)); - return errorType; + const isSetonlyAccessor = prop && prop.flags & ts.SymbolFlags.SetAccessor && !(prop.flags & ts.SymbolFlags.GetAccessor); + if (isSetonlyAccessor && assignmentKind !== ts.AssignmentKind.Definite) { + error(node, ts.Diagnostics.Private_accessor_was_defined_without_a_getter); } - - propType = isThisPropertyAccessInConstructor(node, prop) ? autoType : writing ? getWriteTypeOfSymbol(prop) : getTypeOfSymbol(prop); } - - return getFlowTypeOfAccessExpression(node, prop, propType, right, checkMode); } - - /** - * Determines whether a did-you-mean error should be a suggestion in an unchecked JS file. - * Only applies to unchecked JS files without checkJS, // @ts-check or // @ts-nocheck - * It does not suggest when the suggestion: - * - Is from a global file that is different from the reference file, or - * - (optionally) Is a class, or is a this.x property access expression - */ - function isUncheckedJSSuggestion(node: ts.Node | undefined, suggestion: ts.Symbol | undefined, excludeClasses: boolean): boolean { - const file = ts.getSourceFileOfNode(node); - if (file) { - if (compilerOptions.checkJs === undefined && file.checkJsDirective === undefined && (file.scriptKind === ts.ScriptKind.JS || file.scriptKind === ts.ScriptKind.JSX)) { - const declarationFile = ts.forEach(suggestion?.declarations, ts.getSourceFileOfNode); - return !(file !== declarationFile && !!declarationFile && isGlobalSourceFile(declarationFile)) - && !(excludeClasses && suggestion && suggestion.flags & ts.SymbolFlags.Class) - && !(!!node && excludeClasses && ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword); + else { + if (isAnyLike) { + if (ts.isIdentifier(left) && parentSymbol) { + markAliasReferenced(parentSymbol, node); } + return isErrorType(apparentType) ? errorType : apparentType; } - return false; + prop = getPropertyOfType(apparentType, right.escapedText, /*skipObjectFunctionPropertyAugment*/ false, /*includeTypeOnlyMembers*/ node.kind === ts.SyntaxKind.QualifiedName); } - - function getFlowTypeOfAccessExpression(node: ts.ElementAccessExpression | ts.PropertyAccessExpression | ts.QualifiedName, prop: ts.Symbol | undefined, propType: ts.Type, errorNode: ts.Node, checkMode: CheckMode | undefined) { - // 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 = ts.getAssignmentTargetKind(node); - if (assignmentKind === ts.AssignmentKind.Definite) { - return removeMissingType(propType, !!(prop && prop.flags & ts.SymbolFlags.Optional)); + // In `Foo.Bar.Baz`, 'Foo' is not referenced if 'Bar' is a const enum or a module containing only const enums. + // `Foo` is also not referenced in `enum FooCopy { Bar = Foo.Bar }`, because the enum member value gets inlined + // here even if `Foo` is not a const enum. + // + // The exceptions are: + // 1. if 'isolatedModules' is enabled, because the const enum value will not be inlined, and + // 2. if 'preserveConstEnums' is enabled and the expression is itself an export, e.g. `export = Foo.Bar.Baz`. + if (ts.isIdentifier(left) && parentSymbol && (compilerOptions.isolatedModules || + !(prop && (isConstEnumOrConstEnumOnlyModule(prop) || prop.flags & ts.SymbolFlags.EnumMember && node.parent.kind === ts.SyntaxKind.EnumMember)) || + ts.shouldPreserveConstEnums(compilerOptions) && isExportOrExportExpression(node))) { + markAliasReferenced(parentSymbol, node); + } + + let propType: ts.Type; + if (!prop) { + const indexInfo = !ts.isPrivateIdentifier(right) && (assignmentKind === ts.AssignmentKind.None || !isGenericObjectType(leftType) || ts.isThisTypeParameter(leftType)) ? + getApplicableIndexInfoForName(apparentType, right.escapedText) : undefined; + if (!(indexInfo && indexInfo.type)) { + const isUncheckedJS = isUncheckedJSSuggestion(node, leftType.symbol, /*excludeClasses*/ true); + if (!isUncheckedJS && isJSLiteralType(leftType)) { + return anyType; + } + if (leftType.symbol === globalThisSymbol) { + if (globalThisSymbol.exports!.has(right.escapedText) && (globalThisSymbol.exports!.get(right.escapedText)!.flags & ts.SymbolFlags.BlockScoped)) { + error(right, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(right.escapedText), typeToString(leftType)); + } + else if (noImplicitAny) { + error(right, ts.Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(leftType)); + } + return anyType; + } + if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { + reportNonexistentProperty(right, ts.isThisTypeParameter(leftType) ? apparentType : leftType, isUncheckedJS); + } + return errorType; } - if (prop && - !(prop.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property | ts.SymbolFlags.Accessor)) - && !(prop.flags & ts.SymbolFlags.Method && propType.flags & ts.TypeFlags.Union) - && !isDuplicatedCommonJSExport(prop.declarations)) { - return propType; + if (indexInfo.isReadonly && (ts.isAssignmentTarget(node) || ts.isDeleteTarget(node))) { + error(node, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType)); } - if (propType === autoType) { - return getFlowTypeOfProperty(node, prop); + + propType = (compilerOptions.noUncheckedIndexedAccess && !ts.isAssignmentTarget(node)) ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type; + if (compilerOptions.noPropertyAccessFromIndexSignature && ts.isPropertyAccessExpression(node)) { + error(right, ts.Diagnostics.Property_0_comes_from_an_index_signature_so_it_must_be_accessed_with_0, ts.unescapeLeadingUnderscores(right.escapedText)); } - propType = getNarrowableTypeForReference(propType, node, checkMode); - // 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 && ts.isAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) { - const declaration = prop && prop.valueDeclaration; - if (declaration && isPropertyWithoutInitializer(declaration)) { - if (!ts.isStatic(declaration)) { - const flowContainer = getControlFlowContainer(node); - if (flowContainer.kind === ts.SyntaxKind.Constructor && flowContainer.parent === declaration.parent && !(declaration.flags & ts.NodeFlags.Ambient)) { - assumeUninitialized = true; - } - } - } + if (indexInfo.declaration && ts.getCombinedNodeFlags(indexInfo.declaration) & ts.NodeFlags.Deprecated) { + addDeprecatedSuggestion(right, [indexInfo.declaration], right.escapedText as string); } - else if (strictNullChecks && prop && prop.valueDeclaration && - ts.isPropertyAccessExpression(prop.valueDeclaration) && - ts.getAssignmentDeclarationPropertyAccessKind(prop.valueDeclaration) && - getControlFlowContainer(node) === getControlFlowContainer(prop.valueDeclaration)) { - assumeUninitialized = true; + } + else { + if (isDeprecatedSymbol(prop) && isUncalledFunctionReference(node, prop) && prop.declarations) { + addDeprecatedSuggestion(right, prop.declarations, right.escapedText as string); } - const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType); - if (assumeUninitialized && !(getFalsyFlags(propType) & ts.TypeFlags.Undefined) && getFalsyFlags(flowType) & ts.TypeFlags.Undefined) { - error(errorNode, ts.Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217 - // Return the declared type to reduce follow-on errors - return propType; + checkPropertyNotUsedBeforeDeclaration(prop, node, right); + markPropertyAsReferenced(prop, node, isSelfTypeAccess(left, parentSymbol)); + getNodeLinks(node).resolvedSymbol = prop; + const writing = ts.isWriteAccess(node); + checkPropertyAccessibility(node, left.kind === ts.SyntaxKind.SuperKeyword, writing, apparentType, prop); + if (isAssignmentToReadonlyEntity(node as ts.Expression, prop, assignmentKind)) { + error(right, ts.Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, ts.idText(right)); + return errorType; } - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + + propType = isThisPropertyAccessInConstructor(node, prop) ? autoType : writing ? getWriteTypeOfSymbol(prop) : getTypeOfSymbol(prop); } - function checkPropertyNotUsedBeforeDeclaration(prop: ts.Symbol, node: ts.PropertyAccessExpression | ts.QualifiedName, right: ts.Identifier | ts.PrivateIdentifier): void { - const { valueDeclaration } = prop; - if (!valueDeclaration || ts.getSourceFileOfNode(node).isDeclarationFile) { - return; - } + return getFlowTypeOfAccessExpression(node, prop, propType, right, checkMode); + } - let diagnosticMessage; - const declarationName = ts.idText(right); - if (isInPropertyInitializerOrClassStaticBlock(node) - && !isOptionalPropertyDeclaration(valueDeclaration) - && !(ts.isAccessExpression(node) && ts.isAccessExpression(node.expression)) - && !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right) - && !(ts.isMethodDeclaration(valueDeclaration) && ts.getCombinedModifierFlags(valueDeclaration) & ts.ModifierFlags.Static) - && (compilerOptions.useDefineForClassFields || !isPropertyDeclaredInAncestorClass(prop))) { - diagnosticMessage = error(right, ts.Diagnostics.Property_0_is_used_before_its_initialization, declarationName); - } - else if (valueDeclaration.kind === ts.SyntaxKind.ClassDeclaration && - node.parent.kind !== ts.SyntaxKind.TypeReference && - !(valueDeclaration.flags & ts.NodeFlags.Ambient) && - !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right)) { - diagnosticMessage = error(right, ts.Diagnostics.Class_0_used_before_its_declaration, declarationName); - } + /** + * Determines whether a did-you-mean error should be a suggestion in an unchecked JS file. + * Only applies to unchecked JS files without checkJS, // @ts-check or // @ts-nocheck + * It does not suggest when the suggestion: + * - Is from a global file that is different from the reference file, or + * - (optionally) Is a class, or is a this.x property access expression + */ + function isUncheckedJSSuggestion(node: ts.Node | undefined, suggestion: ts.Symbol | undefined, excludeClasses: boolean): boolean { + const file = ts.getSourceFileOfNode(node); + if (file) { + if (compilerOptions.checkJs === undefined && file.checkJsDirective === undefined && (file.scriptKind === ts.ScriptKind.JS || file.scriptKind === ts.ScriptKind.JSX)) { + const declarationFile = ts.forEach(suggestion?.declarations, ts.getSourceFileOfNode); + return !(file !== declarationFile && !!declarationFile && isGlobalSourceFile(declarationFile)) + && !(excludeClasses && suggestion && suggestion.flags & ts.SymbolFlags.Class) + && !(!!node && excludeClasses && ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword); + } + } + return false; + } - if (diagnosticMessage) { - ts.addRelatedInfo(diagnosticMessage, ts.createDiagnosticForNode(valueDeclaration, ts.Diagnostics._0_is_declared_here, declarationName)); - } + function getFlowTypeOfAccessExpression(node: ts.ElementAccessExpression | ts.PropertyAccessExpression | ts.QualifiedName, prop: ts.Symbol | undefined, propType: ts.Type, errorNode: ts.Node, checkMode: CheckMode | undefined) { + // 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 = ts.getAssignmentTargetKind(node); + if (assignmentKind === ts.AssignmentKind.Definite) { + return removeMissingType(propType, !!(prop && prop.flags & ts.SymbolFlags.Optional)); + } + if (prop && + !(prop.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property | ts.SymbolFlags.Accessor)) + && !(prop.flags & ts.SymbolFlags.Method && propType.flags & ts.TypeFlags.Union) + && !isDuplicatedCommonJSExport(prop.declarations)) { + return propType; + } + if (propType === autoType) { + return getFlowTypeOfProperty(node, prop); + } + propType = getNarrowableTypeForReference(propType, node, checkMode); + // 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 && ts.isAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) { + const declaration = prop && prop.valueDeclaration; + if (declaration && isPropertyWithoutInitializer(declaration)) { + if (!ts.isStatic(declaration)) { + const flowContainer = getControlFlowContainer(node); + if (flowContainer.kind === ts.SyntaxKind.Constructor && flowContainer.parent === declaration.parent && !(declaration.flags & ts.NodeFlags.Ambient)) { + assumeUninitialized = true; + } + } + } + } + else if (strictNullChecks && prop && prop.valueDeclaration && + ts.isPropertyAccessExpression(prop.valueDeclaration) && + ts.getAssignmentDeclarationPropertyAccessKind(prop.valueDeclaration) && + getControlFlowContainer(node) === getControlFlowContainer(prop.valueDeclaration)) { + assumeUninitialized = true; + } + const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType); + if (assumeUninitialized && !(getFalsyFlags(propType) & ts.TypeFlags.Undefined) && getFalsyFlags(flowType) & ts.TypeFlags.Undefined) { + error(errorNode, ts.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: ts.PropertyAccessExpression | ts.QualifiedName, right: ts.Identifier | ts.PrivateIdentifier): void { + const { valueDeclaration } = prop; + if (!valueDeclaration || ts.getSourceFileOfNode(node).isDeclarationFile) { + return; } - function isInPropertyInitializerOrClassStaticBlock(node: ts.Node): boolean { - return !!ts.findAncestor(node, node => { - switch (node.kind) { - case ts.SyntaxKind.PropertyDeclaration: - return true; - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.SpreadAssignment: - case ts.SyntaxKind.ComputedPropertyName: - case ts.SyntaxKind.TemplateSpan: - case ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.JsxAttribute: - case ts.SyntaxKind.JsxAttributes: - case ts.SyntaxKind.JsxSpreadAttribute: - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.ExpressionWithTypeArguments: - case ts.SyntaxKind.HeritageClause: - return false; - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.ExpressionStatement: - return ts.isBlock(node.parent) && ts.isClassStaticBlockDeclaration(node.parent.parent) ? true : "quit"; - default: - return ts.isExpressionNode(node) ? false : "quit"; - } - }); + let diagnosticMessage; + const declarationName = ts.idText(right); + if (isInPropertyInitializerOrClassStaticBlock(node) + && !isOptionalPropertyDeclaration(valueDeclaration) + && !(ts.isAccessExpression(node) && ts.isAccessExpression(node.expression)) + && !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right) + && !(ts.isMethodDeclaration(valueDeclaration) && ts.getCombinedModifierFlags(valueDeclaration) & ts.ModifierFlags.Static) + && (compilerOptions.useDefineForClassFields || !isPropertyDeclaredInAncestorClass(prop))) { + diagnosticMessage = error(right, ts.Diagnostics.Property_0_is_used_before_its_initialization, declarationName); + } + else if (valueDeclaration.kind === ts.SyntaxKind.ClassDeclaration && + node.parent.kind !== ts.SyntaxKind.TypeReference && + !(valueDeclaration.flags & ts.NodeFlags.Ambient) && + !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right)) { + diagnosticMessage = error(right, ts.Diagnostics.Class_0_used_before_its_declaration, declarationName); } - /** - * 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 & ts.SymbolFlags.Class)) { + if (diagnosticMessage) { + ts.addRelatedInfo(diagnosticMessage, ts.createDiagnosticForNode(valueDeclaration, ts.Diagnostics._0_is_declared_here, declarationName)); + } + } + + function isInPropertyInitializerOrClassStaticBlock(node: ts.Node): boolean { + return !!ts.findAncestor(node, node => { + switch (node.kind) { + case ts.SyntaxKind.PropertyDeclaration: + return true; + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.SpreadAssignment: + case ts.SyntaxKind.ComputedPropertyName: + case ts.SyntaxKind.TemplateSpan: + case ts.SyntaxKind.JsxExpression: + case ts.SyntaxKind.JsxAttribute: + case ts.SyntaxKind.JsxAttributes: + case ts.SyntaxKind.JsxSpreadAttribute: + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.ExpressionWithTypeArguments: + case ts.SyntaxKind.HeritageClause: + return false; + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.ExpressionStatement: + return ts.isBlock(node.parent) && ts.isClassStaticBlockDeclaration(node.parent.parent) ? true : "quit"; + default: + return ts.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 & ts.SymbolFlags.Class)) { + return false; + } + let classType: ts.InterfaceType | undefined = getTypeOfSymbol(prop.parent!) as ts.InterfaceType; + while (true) { + classType = classType.symbol && getSuperClass(classType) as ts.InterfaceType | undefined; + if (!classType) { return false; } - let classType: ts.InterfaceType | undefined = getTypeOfSymbol(prop.parent!) as ts.InterfaceType; - while (true) { - classType = classType.symbol && getSuperClass(classType) as ts.InterfaceType | undefined; - if (!classType) { - return false; - } - const superProperty = getPropertyOfType(classType, prop.escapedName); - if (superProperty && superProperty.valueDeclaration) { - return true; - } + const superProperty = getPropertyOfType(classType, prop.escapedName); + if (superProperty && superProperty.valueDeclaration) { + return true; } } + } - function getSuperClass(classType: ts.InterfaceType): ts.Type | undefined { - const x = getBaseTypes(classType); - if (x.length === 0) { - return undefined; - } - return getIntersectionType(x); + function getSuperClass(classType: ts.InterfaceType): ts.Type | undefined { + const x = getBaseTypes(classType); + if (x.length === 0) { + return undefined; } + return getIntersectionType(x); + } - function reportNonexistentProperty(propNode: ts.Identifier | ts.PrivateIdentifier, containingType: ts.Type, isUncheckedJS: boolean) { - let errorInfo: ts.DiagnosticMessageChain | undefined; - let relatedInfo: ts.Diagnostic | undefined; - if (!ts.isPrivateIdentifier(propNode) && containingType.flags & ts.TypeFlags.Union && !(containingType.flags & ts.TypeFlags.Primitive)) { - for (const subtype of (containingType as ts.UnionType).types) { - if (!getPropertyOfType(subtype, propNode.escapedText) && !getApplicableIndexInfoForName(subtype, propNode.escapedText)) { - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.declarationNameToString(propNode), typeToString(subtype)); - break; - } + function reportNonexistentProperty(propNode: ts.Identifier | ts.PrivateIdentifier, containingType: ts.Type, isUncheckedJS: boolean) { + let errorInfo: ts.DiagnosticMessageChain | undefined; + let relatedInfo: ts.Diagnostic | undefined; + if (!ts.isPrivateIdentifier(propNode) && containingType.flags & ts.TypeFlags.Union && !(containingType.flags & ts.TypeFlags.Primitive)) { + for (const subtype of (containingType as ts.UnionType).types) { + if (!getPropertyOfType(subtype, propNode.escapedText) && !getApplicableIndexInfoForName(subtype, propNode.escapedText)) { + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.declarationNameToString(propNode), typeToString(subtype)); + break; } } - if (typeHasStaticProperty(propNode.escapedText, containingType)) { - const propName = ts.declarationNameToString(propNode); - const typeName = typeToString(containingType); - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_to_access_the_static_member_2_instead, propName, typeName, typeName + "." + propName); + } + if (typeHasStaticProperty(propNode.escapedText, containingType)) { + const propName = ts.declarationNameToString(propNode); + const typeName = typeToString(containingType); + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_to_access_the_static_member_2_instead, propName, typeName, typeName + "." + propName); + } + else { + const promisedType = getPromisedTypeOfPromise(containingType); + if (promisedType && getPropertyOfType(promisedType, propNode.escapedText)) { + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.declarationNameToString(propNode), typeToString(containingType)); + relatedInfo = ts.createDiagnosticForNode(propNode, ts.Diagnostics.Did_you_forget_to_use_await); } else { - const promisedType = getPromisedTypeOfPromise(containingType); - if (promisedType && getPropertyOfType(promisedType, propNode.escapedText)) { - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.declarationNameToString(propNode), typeToString(containingType)); - relatedInfo = ts.createDiagnosticForNode(propNode, ts.Diagnostics.Did_you_forget_to_use_await); + const missingProperty = ts.declarationNameToString(propNode); + const container = typeToString(containingType); + const libSuggestion = getSuggestedLibForNonExistentProperty(missingProperty, containingType); + if (libSuggestion !== undefined) { + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_2_or_later, missingProperty, container, libSuggestion); } else { - const missingProperty = ts.declarationNameToString(propNode); - const container = typeToString(containingType); - const libSuggestion = getSuggestedLibForNonExistentProperty(missingProperty, containingType); - if (libSuggestion !== undefined) { - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Property_0_does_not_exist_on_type_1_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_2_or_later, missingProperty, container, libSuggestion); + const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType); + if (suggestion !== undefined) { + const suggestedName = ts.symbolName(suggestion); + const message = isUncheckedJS ? ts.Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2 : ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2; + errorInfo = ts.chainDiagnosticMessages(errorInfo, message, missingProperty, container, suggestedName); + relatedInfo = suggestion.valueDeclaration && ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestedName); } else { - const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType); - if (suggestion !== undefined) { - const suggestedName = ts.symbolName(suggestion); - const message = isUncheckedJS ? ts.Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2 : ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2; - errorInfo = ts.chainDiagnosticMessages(errorInfo, message, missingProperty, container, suggestedName); - relatedInfo = suggestion.valueDeclaration && ts.createDiagnosticForNode(suggestion.valueDeclaration, ts.Diagnostics._0_is_declared_here, suggestedName); - } - else { - const diagnostic = containerSeemsToBeEmptyDomElement(containingType) - ? ts.Diagnostics.Property_0_does_not_exist_on_type_1_Try_changing_the_lib_compiler_option_to_include_dom - : ts.Diagnostics.Property_0_does_not_exist_on_type_1; - errorInfo = ts.chainDiagnosticMessages(elaborateNeverIntersection(errorInfo, containingType), diagnostic, missingProperty, container); - } + const diagnostic = containerSeemsToBeEmptyDomElement(containingType) + ? ts.Diagnostics.Property_0_does_not_exist_on_type_1_Try_changing_the_lib_compiler_option_to_include_dom + : ts.Diagnostics.Property_0_does_not_exist_on_type_1; + errorInfo = ts.chainDiagnosticMessages(elaborateNeverIntersection(errorInfo, containingType), diagnostic, missingProperty, container); } } } - const resultDiagnostic = ts.createDiagnosticForNodeFromMessageChain(propNode, errorInfo); - if (relatedInfo) { - ts.addRelatedInfo(resultDiagnostic, relatedInfo); - } - addErrorOrSuggestion(!isUncheckedJS || errorInfo.code !== ts.Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2.code, resultDiagnostic); } - - function containerSeemsToBeEmptyDomElement(containingType: ts.Type) { - return (compilerOptions.lib && !compilerOptions.lib.includes("dom")) && - everyContainedType(containingType, type => type.symbol && /^(EventTarget|Node|((HTML[a-zA-Z]*)?Element))$/.test(ts.unescapeLeadingUnderscores(type.symbol.escapedName))) && - isEmptyObjectType(containingType); + const resultDiagnostic = ts.createDiagnosticForNodeFromMessageChain(propNode, errorInfo); + if (relatedInfo) { + ts.addRelatedInfo(resultDiagnostic, relatedInfo); } + addErrorOrSuggestion(!isUncheckedJS || errorInfo.code !== ts.Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2.code, resultDiagnostic); + } - function typeHasStaticProperty(propName: ts.__String, containingType: ts.Type): boolean { - const prop = containingType.symbol && getPropertyOfType(getTypeOfSymbol(containingType.symbol), propName); - return prop !== undefined && !!prop.valueDeclaration && ts.isStatic(prop.valueDeclaration); - } + function containerSeemsToBeEmptyDomElement(containingType: ts.Type) { + return (compilerOptions.lib && !compilerOptions.lib.includes("dom")) && + everyContainedType(containingType, type => type.symbol && /^(EventTarget|Node|((HTML[a-zA-Z]*)?Element))$/.test(ts.unescapeLeadingUnderscores(type.symbol.escapedName))) && + isEmptyObjectType(containingType); + } - function getSuggestedLibForNonExistentName(name: ts.__String | ts.Identifier) { - const missingName = diagnosticName(name); - const allFeatures = ts.getScriptTargetFeatures(); - const libTargets = ts.getOwnKeys(allFeatures); - for (const libTarget of libTargets) { - const containingTypes = ts.getOwnKeys(allFeatures[libTarget]); - if (containingTypes !== undefined && ts.contains(containingTypes, missingName)) { - return libTarget; - } - } - } + function typeHasStaticProperty(propName: ts.__String, containingType: ts.Type): boolean { + const prop = containingType.symbol && getPropertyOfType(getTypeOfSymbol(containingType.symbol), propName); + return prop !== undefined && !!prop.valueDeclaration && ts.isStatic(prop.valueDeclaration); + } - function getSuggestedLibForNonExistentProperty(missingProperty: string, containingType: ts.Type) { - const container = getApparentType(containingType).symbol; - if (!container) { - return undefined; - } - const allFeatures = ts.getScriptTargetFeatures(); - const libTargets = ts.getOwnKeys(allFeatures); - for (const libTarget of libTargets) { - const featuresOfLib = allFeatures[libTarget]; - const featuresOfContainingType = featuresOfLib[ts.symbolName(container)]; - if (featuresOfContainingType !== undefined && ts.contains(featuresOfContainingType, missingProperty)) { - return libTarget; - } + function getSuggestedLibForNonExistentName(name: ts.__String | ts.Identifier) { + const missingName = diagnosticName(name); + const allFeatures = ts.getScriptTargetFeatures(); + const libTargets = ts.getOwnKeys(allFeatures); + for (const libTarget of libTargets) { + const containingTypes = ts.getOwnKeys(allFeatures[libTarget]); + if (containingTypes !== undefined && ts.contains(containingTypes, missingName)) { + return libTarget; } } + } - function getSuggestedSymbolForNonexistentClassMember(name: string, baseType: ts.Type): ts.Symbol | undefined { - return getSpellingSuggestionForName(name, getPropertiesOfType(baseType), ts.SymbolFlags.ClassMember); + function getSuggestedLibForNonExistentProperty(missingProperty: string, containingType: ts.Type) { + const container = getApparentType(containingType).symbol; + if (!container) { + return undefined; } - - function getSuggestedSymbolForNonexistentProperty(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): ts.Symbol | undefined { - let props = getPropertiesOfType(containingType); - if (typeof name !== "string") { - const parent = name.parent; - if (ts.isPropertyAccessExpression(parent)) { - props = ts.filter(props, prop => isValidPropertyAccessForCompletions(parent, containingType, prop)); - } - name = ts.idText(name); + const allFeatures = ts.getScriptTargetFeatures(); + const libTargets = ts.getOwnKeys(allFeatures); + for (const libTarget of libTargets) { + const featuresOfLib = allFeatures[libTarget]; + const featuresOfContainingType = featuresOfLib[ts.symbolName(container)]; + if (featuresOfContainingType !== undefined && ts.contains(featuresOfContainingType, missingProperty)) { + return libTarget; } - return getSpellingSuggestionForName(name, props, ts.SymbolFlags.Value); - } - - function getSuggestedSymbolForNonexistentJSXAttribute(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): ts.Symbol | undefined { - const strName = ts.isString(name) ? name : ts.idText(name); - const properties = getPropertiesOfType(containingType); - const jsxSpecific = strName === "for" ? ts.find(properties, x => ts.symbolName(x) === "htmlFor") - : strName === "class" ? ts.find(properties, x => ts.symbolName(x) === "className") - : undefined; - return jsxSpecific ?? getSpellingSuggestionForName(strName, properties, ts.SymbolFlags.Value); - } - - function getSuggestionForNonexistentProperty(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): string | undefined { - const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType); - return suggestion && ts.symbolName(suggestion); - } - - function getSuggestedSymbolForNonexistentSymbol(location: ts.Node | undefined, outerName: ts.__String, meaning: ts.SymbolFlags): ts.Symbol | undefined { - ts.Debug.assert(outerName !== undefined, "outername should always be defined"); - const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, /*getSpellingSuggestions*/ true, (symbols, name, meaning) => { - ts.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. - if (symbol) - return symbol; - let candidates: ts.Symbol[]; - if (symbols === globals) { - const primitives = ts.mapDefined(["string", "number", "boolean", "object", "bigint", "symbol"], s => symbols.has((s.charAt(0).toUpperCase() + s.slice(1)) as ts.__String) - ? createSymbol(ts.SymbolFlags.TypeAlias, s as ts.__String) as ts.Symbol - : undefined); - candidates = primitives.concat(ts.arrayFrom(symbols.values())); - } - else { - candidates = ts.arrayFrom(symbols.values()); - } - return getSpellingSuggestionForName(ts.unescapeLeadingUnderscores(name), candidates, meaning); - }); - return result; } + } - function getSuggestionForNonexistentSymbol(location: ts.Node | undefined, outerName: ts.__String, meaning: ts.SymbolFlags): string | undefined { - const symbolResult = getSuggestedSymbolForNonexistentSymbol(location, outerName, meaning); - return symbolResult && ts.symbolName(symbolResult); - } + function getSuggestedSymbolForNonexistentClassMember(name: string, baseType: ts.Type): ts.Symbol | undefined { + return getSpellingSuggestionForName(name, getPropertiesOfType(baseType), ts.SymbolFlags.ClassMember); + } - function getSuggestedSymbolForNonexistentModule(name: ts.Identifier, targetModule: ts.Symbol): ts.Symbol | undefined { - return targetModule.exports && getSpellingSuggestionForName(ts.idText(name), getExportsOfModuleAsArray(targetModule), ts.SymbolFlags.ModuleMember); + function getSuggestedSymbolForNonexistentProperty(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): ts.Symbol | undefined { + let props = getPropertiesOfType(containingType); + if (typeof name !== "string") { + const parent = name.parent; + if (ts.isPropertyAccessExpression(parent)) { + props = ts.filter(props, prop => isValidPropertyAccessForCompletions(parent, containingType, prop)); + } + name = ts.idText(name); } + return getSpellingSuggestionForName(name, props, ts.SymbolFlags.Value); + } - function getSuggestionForNonexistentExport(name: ts.Identifier, targetModule: ts.Symbol): string | undefined { - const suggestion = getSuggestedSymbolForNonexistentModule(name, targetModule); - return suggestion && ts.symbolName(suggestion); - } + function getSuggestedSymbolForNonexistentJSXAttribute(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): ts.Symbol | undefined { + const strName = ts.isString(name) ? name : ts.idText(name); + const properties = getPropertiesOfType(containingType); + const jsxSpecific = strName === "for" ? ts.find(properties, x => ts.symbolName(x) === "htmlFor") + : strName === "class" ? ts.find(properties, x => ts.symbolName(x) === "className") + : undefined; + return jsxSpecific ?? getSpellingSuggestionForName(strName, properties, ts.SymbolFlags.Value); + } - function getSuggestionForNonexistentIndexSignature(objectType: ts.Type, expr: ts.ElementAccessExpression, keyedType: ts.Type): string | undefined { - // check if object type has setter or getter - function hasProp(name: "set" | "get") { - const prop = getPropertyOfObjectType(objectType, name as ts.__String); - if (prop) { - const s = getSingleCallSignature(getTypeOfSymbol(prop)); - return !!s && getMinArgumentCount(s) >= 1 && isTypeAssignableTo(keyedType, getTypeAtPosition(s, 0)); - } - return false; - } - ; - const suggestedMethod = ts.isAssignmentTarget(expr) ? "set" : "get"; - if (!hasProp(suggestedMethod)) { - return undefined; - } + function getSuggestionForNonexistentProperty(name: ts.Identifier | ts.PrivateIdentifier | string, containingType: ts.Type): string | undefined { + const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType); + return suggestion && ts.symbolName(suggestion); + } - let suggestion = ts.tryGetPropertyAccessOrIdentifierToString(expr.expression); - if (suggestion === undefined) { - suggestion = suggestedMethod; + function getSuggestedSymbolForNonexistentSymbol(location: ts.Node | undefined, outerName: ts.__String, meaning: ts.SymbolFlags): ts.Symbol | undefined { + ts.Debug.assert(outerName !== undefined, "outername should always be defined"); + const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, /*getSpellingSuggestions*/ true, (symbols, name, meaning) => { + ts.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. + if (symbol) + return symbol; + let candidates: ts.Symbol[]; + if (symbols === globals) { + const primitives = ts.mapDefined(["string", "number", "boolean", "object", "bigint", "symbol"], s => symbols.has((s.charAt(0).toUpperCase() + s.slice(1)) as ts.__String) + ? createSymbol(ts.SymbolFlags.TypeAlias, s as ts.__String) as ts.Symbol + : undefined); + candidates = primitives.concat(ts.arrayFrom(symbols.values())); } else { - suggestion += "." + suggestedMethod; + candidates = ts.arrayFrom(symbols.values()); } + return getSpellingSuggestionForName(ts.unescapeLeadingUnderscores(name), candidates, meaning); + }); + return result; + } - return suggestion; - } + function getSuggestionForNonexistentSymbol(location: ts.Node | undefined, outerName: ts.__String, meaning: ts.SymbolFlags): string | undefined { + const symbolResult = getSuggestedSymbolForNonexistentSymbol(location, outerName, meaning); + return symbolResult && ts.symbolName(symbolResult); + } + + function getSuggestedSymbolForNonexistentModule(name: ts.Identifier, targetModule: ts.Symbol): ts.Symbol | undefined { + return targetModule.exports && getSpellingSuggestionForName(ts.idText(name), getExportsOfModuleAsArray(targetModule), ts.SymbolFlags.ModuleMember); + } + + function getSuggestionForNonexistentExport(name: ts.Identifier, targetModule: ts.Symbol): string | undefined { + const suggestion = getSuggestedSymbolForNonexistentModule(name, targetModule); + return suggestion && ts.symbolName(suggestion); + } - function getSuggestedTypeForNonexistentStringLiteralType(source: ts.StringLiteralType, target: ts.UnionType): ts.StringLiteralType | undefined { - const candidates = target.types.filter((type): type is ts.StringLiteralType => !!(type.flags & ts.TypeFlags.StringLiteral)); - return ts.getSpellingSuggestion(source.value, candidates, type => type.value); + function getSuggestionForNonexistentIndexSignature(objectType: ts.Type, expr: ts.ElementAccessExpression, keyedType: ts.Type): string | undefined { + // check if object type has setter or getter + function hasProp(name: "set" | "get") { + const prop = getPropertyOfObjectType(objectType, name as ts.__String); + if (prop) { + const s = getSingleCallSignature(getTypeOfSymbol(prop)); + return !!s && getMinArgumentCount(s) >= 1 && isTypeAssignableTo(keyedType, getTypeAtPosition(s, 0)); + } + return false; + } + ; + const suggestedMethod = ts.isAssignmentTarget(expr) ? "set" : "get"; + if (!hasProp(suggestedMethod)) { + return undefined; } - /** - * 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: ts.SymbolFlags): ts.Symbol | undefined { - return ts.getSpellingSuggestion(name, symbols, getCandidateName); - function getCandidateName(candidate: ts.Symbol) { - const candidateName = ts.symbolName(candidate); - if (ts.startsWith(candidateName, "\"")) { - return undefined; - } + let suggestion = ts.tryGetPropertyAccessOrIdentifierToString(expr.expression); + if (suggestion === undefined) { + suggestion = suggestedMethod; + } + else { + suggestion += "." + suggestedMethod; + } - if (candidate.flags & meaning) { - return candidateName; - } + return suggestion; + } - if (candidate.flags & ts.SymbolFlags.Alias) { - const alias = tryResolveAlias(candidate); - if (alias && alias.flags & meaning) { - return candidateName; - } - } + function getSuggestedTypeForNonexistentStringLiteralType(source: ts.StringLiteralType, target: ts.UnionType): ts.StringLiteralType | undefined { + const candidates = target.types.filter((type): type is ts.StringLiteralType => !!(type.flags & ts.TypeFlags.StringLiteral)); + return ts.getSpellingSuggestion(source.value, candidates, type => type.value); + } + /** + * 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: ts.SymbolFlags): ts.Symbol | undefined { + return ts.getSpellingSuggestion(name, symbols, getCandidateName); + function getCandidateName(candidate: ts.Symbol) { + const candidateName = ts.symbolName(candidate); + if (ts.startsWith(candidateName, "\"")) { return undefined; } - } - function markPropertyAsReferenced(prop: ts.Symbol, nodeForCheckWriteOnly: ts.Node | undefined, isSelfTypeAccess: boolean) { - const valueDeclaration = prop && (prop.flags & ts.SymbolFlags.ClassMember) && prop.valueDeclaration; - if (!valueDeclaration) { - return; - } - const hasPrivateModifier = ts.hasEffectiveModifier(valueDeclaration, ts.ModifierFlags.Private); - const hasPrivateIdentifier = prop.valueDeclaration && ts.isNamedDeclaration(prop.valueDeclaration) && ts.isPrivateIdentifier(prop.valueDeclaration.name); - if (!hasPrivateModifier && !hasPrivateIdentifier) { - return; - } - if (nodeForCheckWriteOnly && ts.isWriteOnlyAccess(nodeForCheckWriteOnly) && !(prop.flags & ts.SymbolFlags.SetAccessor)) { - return; + if (candidate.flags & meaning) { + return candidateName; } - if (isSelfTypeAccess) { - // Find any FunctionLikeDeclaration because those create a new 'this' binding. But this should only matter for methods (or getters/setters). - const containingMethod = ts.findAncestor(nodeForCheckWriteOnly, ts.isFunctionLikeDeclaration); - if (containingMethod && containingMethod.symbol === prop) { - return; + + if (candidate.flags & ts.SymbolFlags.Alias) { + const alias = tryResolveAlias(candidate); + if (alias && alias.flags & meaning) { + return candidateName; } } - (ts.getCheckFlags(prop) & ts.CheckFlags.Instantiated ? getSymbolLinks(prop).target : prop)!.isReferenced = ts.SymbolFlags.All; + return undefined; } + } - function isSelfTypeAccess(name: ts.Expression | ts.QualifiedName, parent: ts.Symbol | undefined) { - return name.kind === ts.SyntaxKind.ThisKeyword - || !!parent && ts.isEntityNameExpression(name) && parent === getResolvedSymbol(ts.getFirstIdentifier(name)); + function markPropertyAsReferenced(prop: ts.Symbol, nodeForCheckWriteOnly: ts.Node | undefined, isSelfTypeAccess: boolean) { + const valueDeclaration = prop && (prop.flags & ts.SymbolFlags.ClassMember) && prop.valueDeclaration; + if (!valueDeclaration) { + return; } - - function isValidPropertyAccess(node: ts.PropertyAccessExpression | ts.QualifiedName | ts.ImportTypeNode, propertyName: ts.__String): boolean { - switch (node.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - return isValidPropertyAccessWithType(node, node.expression.kind === ts.SyntaxKind.SuperKeyword, propertyName, getWidenedType(checkExpression(node.expression))); - case ts.SyntaxKind.QualifiedName: - return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getWidenedType(checkExpression(node.left))); - case ts.SyntaxKind.ImportType: - return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getTypeFromTypeNode(node)); - } + const hasPrivateModifier = ts.hasEffectiveModifier(valueDeclaration, ts.ModifierFlags.Private); + const hasPrivateIdentifier = prop.valueDeclaration && ts.isNamedDeclaration(prop.valueDeclaration) && ts.isPrivateIdentifier(prop.valueDeclaration.name); + if (!hasPrivateModifier && !hasPrivateIdentifier) { + return; } - - /** - * Checks if an existing property access is valid for completions purposes. - * @param node a property access-like node where we want to check if we can access a property. - * This node does not need to be an access of the property we are checking. - * e.g. in completions, this node will often be an incomplete property access node, as in `foo.`. - * Besides providing a location (i.e. scope) used to check property accessibility, we use this node for - * computing whether this is a `super` property access. - * @param type the type whose property we are checking. - * @param property the accessed property's symbol. - */ - function isValidPropertyAccessForCompletions(node: ts.PropertyAccessExpression | ts.ImportTypeNode | ts.QualifiedName, type: ts.Type, property: ts.Symbol): boolean { - return isPropertyAccessible(node, node.kind === ts.SyntaxKind.PropertyAccessExpression && node.expression.kind === ts.SyntaxKind.SuperKeyword, - /* isWrite */ false, type, property); - // Previously we validated the 'this' type of methods but this adversely affected performance. See #31377 for more context. + if (nodeForCheckWriteOnly && ts.isWriteOnlyAccess(nodeForCheckWriteOnly) && !(prop.flags & ts.SymbolFlags.SetAccessor)) { + return; } - - function isValidPropertyAccessWithType(node: ts.PropertyAccessExpression | ts.QualifiedName | ts.ImportTypeNode, isSuper: boolean, propertyName: ts.__String, type: ts.Type): boolean { - - // Short-circuiting for improved performance. - if (isTypeAny(type)) { - return true; + if (isSelfTypeAccess) { + // Find any FunctionLikeDeclaration because those create a new 'this' binding. But this should only matter for methods (or getters/setters). + const containingMethod = ts.findAncestor(nodeForCheckWriteOnly, ts.isFunctionLikeDeclaration); + if (containingMethod && containingMethod.symbol === prop) { + return; } - - const prop = getPropertyOfType(type, propertyName); - return !!prop && isPropertyAccessible(node, isSuper, /* isWrite */ false, type, prop); } - /** - * Checks if a property can be accessed in a location. - * The location is given by the `node` parameter. - * The node does not need to be a property access. - * @param node location where to check property accessibility - * @param isSuper whether to consider this a `super` property access, e.g. `super.foo`. - * @param isWrite whether this is a write access, e.g. `++foo.x`. - * @param containingType type where the property comes from. - * @param property property symbol. - */ - function isPropertyAccessible(node: ts.Node, isSuper: boolean, isWrite: boolean, containingType: ts.Type, property: ts.Symbol): boolean { - - // Short-circuiting for improved performance. - if (isTypeAny(containingType)) { - return true; - } + (ts.getCheckFlags(prop) & ts.CheckFlags.Instantiated ? getSymbolLinks(prop).target : prop)!.isReferenced = ts.SymbolFlags.All; + } - // A #private property access in an optional chain is an error dealt with by the parser. - // The checker does not check for it, so we need to do our own check here. - if (property.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(property.valueDeclaration)) { - const declClass = ts.getContainingClass(property.valueDeclaration); - return !ts.isOptionalChain(node) && !!ts.findAncestor(node, parent => parent === declClass); - } + function isSelfTypeAccess(name: ts.Expression | ts.QualifiedName, parent: ts.Symbol | undefined) { + return name.kind === ts.SyntaxKind.ThisKeyword + || !!parent && ts.isEntityNameExpression(name) && parent === getResolvedSymbol(ts.getFirstIdentifier(name)); + } - return checkPropertyAccessibilityAtLocation(node, isSuper, isWrite, containingType, property); + function isValidPropertyAccess(node: ts.PropertyAccessExpression | ts.QualifiedName | ts.ImportTypeNode, propertyName: ts.__String): boolean { + switch (node.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + return isValidPropertyAccessWithType(node, node.expression.kind === ts.SyntaxKind.SuperKeyword, propertyName, getWidenedType(checkExpression(node.expression))); + case ts.SyntaxKind.QualifiedName: + return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getWidenedType(checkExpression(node.left))); + case ts.SyntaxKind.ImportType: + return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getTypeFromTypeNode(node)); } + } - /** - * Return the symbol of the for-in variable declared or referenced by the given for-in statement. - */ - function getForInVariableSymbol(node: ts.ForInStatement): ts.Symbol | undefined { - const initializer = node.initializer; - if (initializer.kind === ts.SyntaxKind.VariableDeclarationList) { - const variable = (initializer as ts.VariableDeclarationList).declarations[0]; - if (variable && !ts.isBindingPattern(variable.name)) { - return getSymbolOfNode(variable); - } - } - else if (initializer.kind === ts.SyntaxKind.Identifier) { - return getResolvedSymbol(initializer as ts.Identifier); - } - return undefined; - } + /** + * Checks if an existing property access is valid for completions purposes. + * @param node a property access-like node where we want to check if we can access a property. + * This node does not need to be an access of the property we are checking. + * e.g. in completions, this node will often be an incomplete property access node, as in `foo.`. + * Besides providing a location (i.e. scope) used to check property accessibility, we use this node for + * computing whether this is a `super` property access. + * @param type the type whose property we are checking. + * @param property the accessed property's symbol. + */ + function isValidPropertyAccessForCompletions(node: ts.PropertyAccessExpression | ts.ImportTypeNode | ts.QualifiedName, type: ts.Type, property: ts.Symbol): boolean { + return isPropertyAccessible(node, node.kind === ts.SyntaxKind.PropertyAccessExpression && node.expression.kind === ts.SyntaxKind.SuperKeyword, + /* isWrite */ false, type, property); + // Previously we validated the 'this' type of methods but this adversely affected performance. See #31377 for more context. + } - /** - * Return true if the given type is considered to have numeric property names. - */ - function hasNumericPropertyNames(type: ts.Type) { - return getIndexInfosOfType(type).length === 1 && !!getIndexInfoOfType(type, numberType); - } + function isValidPropertyAccessWithType(node: ts.PropertyAccessExpression | ts.QualifiedName | ts.ImportTypeNode, isSuper: boolean, propertyName: ts.__String, type: ts.Type): boolean { - /** - * 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: ts.Expression) { - const e = ts.skipParentheses(expr); - if (e.kind === ts.SyntaxKind.Identifier) { - const symbol = getResolvedSymbol(e as ts.Identifier); - if (symbol.flags & ts.SymbolFlags.Variable) { - let child: ts.Node = expr; - let node = expr.parent; - while (node) { - if (node.kind === ts.SyntaxKind.ForInStatement && - child === (node as ts.ForInStatement).statement && - getForInVariableSymbol(node as ts.ForInStatement) === symbol && - hasNumericPropertyNames(getTypeOfExpression((node as ts.ForInStatement).expression))) { - return true; - } - child = node; - node = node.parent; - } - } - } - return false; + // Short-circuiting for improved performance. + if (isTypeAny(type)) { + return true; } - function checkIndexedAccess(node: ts.ElementAccessExpression, checkMode: CheckMode | undefined): ts.Type { - return node.flags & ts.NodeFlags.OptionalChain ? checkElementAccessChain(node as ts.ElementAccessChain, checkMode) : - checkElementAccessExpression(node, checkNonNullExpression(node.expression), checkMode); + const prop = getPropertyOfType(type, propertyName); + return !!prop && isPropertyAccessible(node, isSuper, /* isWrite */ false, type, prop); + } + + /** + * Checks if a property can be accessed in a location. + * The location is given by the `node` parameter. + * The node does not need to be a property access. + * @param node location where to check property accessibility + * @param isSuper whether to consider this a `super` property access, e.g. `super.foo`. + * @param isWrite whether this is a write access, e.g. `++foo.x`. + * @param containingType type where the property comes from. + * @param property property symbol. + */ + function isPropertyAccessible(node: ts.Node, isSuper: boolean, isWrite: boolean, containingType: ts.Type, property: ts.Symbol): boolean { + + // Short-circuiting for improved performance. + if (isTypeAny(containingType)) { + return true; } - function checkElementAccessChain(node: ts.ElementAccessChain, checkMode: CheckMode | undefined) { - const exprType = checkExpression(node.expression); - const nonOptionalType = getOptionalExpressionType(exprType, node.expression); - return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression), checkMode), node, nonOptionalType !== exprType); + // A #private property access in an optional chain is an error dealt with by the parser. + // The checker does not check for it, so we need to do our own check here. + if (property.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(property.valueDeclaration)) { + const declClass = ts.getContainingClass(property.valueDeclaration); + return !ts.isOptionalChain(node) && !!ts.findAncestor(node, parent => parent === declClass); } - function checkElementAccessExpression(node: ts.ElementAccessExpression, exprType: ts.Type, checkMode: CheckMode | undefined): ts.Type { - const objectType = ts.getAssignmentTargetKind(node) !== ts.AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(exprType) : exprType; - const indexExpression = node.argumentExpression; - const indexType = checkExpression(indexExpression); - - if (isErrorType(objectType) || objectType === silentNeverType) { - return objectType; - } + return checkPropertyAccessibilityAtLocation(node, isSuper, isWrite, containingType, property); + } - if (isConstEnumObjectType(objectType) && !ts.isStringLiteralLike(indexExpression)) { - error(indexExpression, ts.Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal); - return errorType; + /** + * Return the symbol of the for-in variable declared or referenced by the given for-in statement. + */ + function getForInVariableSymbol(node: ts.ForInStatement): ts.Symbol | undefined { + const initializer = node.initializer; + if (initializer.kind === ts.SyntaxKind.VariableDeclarationList) { + const variable = (initializer as ts.VariableDeclarationList).declarations[0]; + if (variable && !ts.isBindingPattern(variable.name)) { + return getSymbolOfNode(variable); } - - const effectiveIndexType = isForInVariableForNumericPropertyNames(indexExpression) ? numberType : indexType; - const accessFlags = ts.isAssignmentTarget(node) ? - ts.AccessFlags.Writing | (isGenericObjectType(objectType) && !ts.isThisTypeParameter(objectType) ? ts.AccessFlags.NoIndexSignatures : 0) : - ts.AccessFlags.ExpressionPosition; - const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, accessFlags, node) || errorType; - return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode), node); } - - function callLikeExpressionMayHaveTypeArguments(node: ts.CallLikeExpression): node is ts.CallExpression | ts.NewExpression | ts.TaggedTemplateExpression | ts.JsxOpeningElement { - return ts.isCallOrNewExpression(node) || ts.isTaggedTemplateExpression(node) || ts.isJsxOpeningLikeElement(node); + else if (initializer.kind === ts.SyntaxKind.Identifier) { + return getResolvedSymbol(initializer as ts.Identifier); } + return undefined; + } - function resolveUntypedCall(node: ts.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. - ts.forEach(node.typeArguments, checkSourceElement); - } + /** + * Return true if the given type is considered to have numeric property names. + */ + function hasNumericPropertyNames(type: ts.Type) { + return getIndexInfosOfType(type).length === 1 && !!getIndexInfoOfType(type, numberType); + } - if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { - checkExpression(node.template); - } - else if (ts.isJsxOpeningLikeElement(node)) { - checkExpression(node.attributes); - } - else if (node.kind !== ts.SyntaxKind.Decorator) { - ts.forEach((node as ts.CallExpression).arguments, argument => { - checkExpression(argument); - }); - } - return anySignature; - } - - function resolveErrorCall(node: ts.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: ts.SignatureFlags): void { - let lastParent: ts.Node | undefined; - let lastSymbol: ts.Symbol | undefined; - let cutoffIndex = 0; - let index: number | undefined; - let specializedIndex = -1; - let spliceIndex: number; - ts.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; + /** + * 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: ts.Expression) { + const e = ts.skipParentheses(expr); + if (e.kind === ts.SyntaxKind.Identifier) { + const symbol = getResolvedSymbol(e as ts.Identifier); + if (symbol.flags & ts.SymbolFlags.Variable) { + let child: ts.Node = expr; + let node = expr.parent; + while (node) { + if (node.kind === ts.SyntaxKind.ForInStatement && + child === (node as ts.ForInStatement).statement && + getForInVariableSymbol(node as ts.ForInStatement) === symbol && + hasNumericPropertyNames(getTypeOfExpression((node as ts.ForInStatement).expression))) { + return true; } + child = node; + node = node.parent; } - 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++; - } - else { - spliceIndex = index; - } - - result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature); } } + return false; + } + + function checkIndexedAccess(node: ts.ElementAccessExpression, checkMode: CheckMode | undefined): ts.Type { + return node.flags & ts.NodeFlags.OptionalChain ? checkElementAccessChain(node as ts.ElementAccessChain, checkMode) : + checkElementAccessExpression(node, checkNonNullExpression(node.expression), checkMode); + } - function isSpreadArgument(arg: ts.Expression | undefined): arg is ts.Expression { - return !!arg && (arg.kind === ts.SyntaxKind.SpreadElement || arg.kind === ts.SyntaxKind.SyntheticExpression && (arg as ts.SyntheticExpression).isSpread); + function checkElementAccessChain(node: ts.ElementAccessChain, checkMode: CheckMode | undefined) { + const exprType = checkExpression(node.expression); + const nonOptionalType = getOptionalExpressionType(exprType, node.expression); + return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression), checkMode), node, nonOptionalType !== exprType); + } + + function checkElementAccessExpression(node: ts.ElementAccessExpression, exprType: ts.Type, checkMode: CheckMode | undefined): ts.Type { + const objectType = ts.getAssignmentTargetKind(node) !== ts.AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(exprType) : exprType; + const indexExpression = node.argumentExpression; + const indexType = checkExpression(indexExpression); + + if (isErrorType(objectType) || objectType === silentNeverType) { + return objectType; } - function getSpreadArgumentIndex(args: readonly ts.Expression[]): number { - return ts.findIndex(args, isSpreadArgument); + if (isConstEnumObjectType(objectType) && !ts.isStringLiteralLike(indexExpression)) { + error(indexExpression, ts.Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal); + return errorType; } - function acceptsVoid(t: ts.Type): boolean { - return !!(t.flags & ts.TypeFlags.Void); + const effectiveIndexType = isForInVariableForNumericPropertyNames(indexExpression) ? numberType : indexType; + const accessFlags = ts.isAssignmentTarget(node) ? + ts.AccessFlags.Writing | (isGenericObjectType(objectType) && !ts.isThisTypeParameter(objectType) ? ts.AccessFlags.NoIndexSignatures : 0) : + ts.AccessFlags.ExpressionPosition; + const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, accessFlags, node) || errorType; + return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode), node); + } + + function callLikeExpressionMayHaveTypeArguments(node: ts.CallLikeExpression): node is ts.CallExpression | ts.NewExpression | ts.TaggedTemplateExpression | ts.JsxOpeningElement { + return ts.isCallOrNewExpression(node) || ts.isTaggedTemplateExpression(node) || ts.isJsxOpeningLikeElement(node); + } + + function resolveUntypedCall(node: ts.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. + ts.forEach(node.typeArguments, checkSourceElement); } - function acceptsVoidUndefinedUnknownOrAny(t: ts.Type): boolean { - return !!(t.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Unknown | ts.TypeFlags.Any)); + if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { + checkExpression(node.template); + } + else if (ts.isJsxOpeningLikeElement(node)) { + checkExpression(node.attributes); + } + else if (node.kind !== ts.SyntaxKind.Decorator) { + ts.forEach((node as ts.CallExpression).arguments, argument => { + checkExpression(argument); + }); } + return anySignature; + } - function hasCorrectArity(node: ts.CallLikeExpression, args: readonly ts.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); + function resolveErrorCall(node: ts.CallLikeExpression): ts.Signature { + resolveUntypedCall(node); + return unknownSignature; + } - if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { - argCount = args.length; - if (node.template.kind === ts.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 = ts.last(node.template.templateSpans); // we should always have at least one span. - callIsIncomplete = ts.nodeIsMissing(lastSpan.literal) || !!lastSpan.literal.isUnterminated; + // 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: ts.SignatureFlags): void { + let lastParent: ts.Node | undefined; + let lastSymbol: ts.Symbol | undefined; + let cutoffIndex = 0; + let index: number | undefined; + let specializedIndex = -1; + let spliceIndex: number; + ts.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 { - // 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 as ts.LiteralExpression; - ts.Debug.assert(templateLiteral.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral); - callIsIncomplete = !!templateLiteral.isUnterminated; - } - } - else if (node.kind === ts.SyntaxKind.Decorator) { - argCount = getDecoratorArgumentCount(node, signature); - } - else if (ts.isJsxOpeningLikeElement(node)) { - callIsIncomplete = node.attributes.end === node.end; - if (callIsIncomplete) { - return true; + lastParent = parent; + index = cutoffIndex; } - 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' - ts.Debug.assert(node.kind === ts.SyntaxKind.NewExpression); - return getMinArgumentCount(signature) === 0; + 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++; } else { - argCount = signatureHelpTrailingComma ? args.length + 1 : args.length; + spliceIndex = index; + } - // If we are missing the close parenthesis, the call is incomplete. - callIsIncomplete = node.arguments.end === node.end; + result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature); + } + } - // 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)); - } - } + function isSpreadArgument(arg: ts.Expression | undefined): arg is ts.Expression { + return !!arg && (arg.kind === ts.SyntaxKind.SpreadElement || arg.kind === ts.SyntaxKind.SyntheticExpression && (arg as ts.SyntheticExpression).isSpread); + } - // Too many arguments implies incorrect arity. - if (!hasEffectiveRestParameter(signature) && argCount > effectiveParameterCount) { - return false; - } + function getSpreadArgumentIndex(args: readonly ts.Expression[]): number { + return ts.findIndex(args, isSpreadArgument); + } - // 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; + function acceptsVoid(t: ts.Type): boolean { + return !!(t.flags & ts.TypeFlags.Void); + } + + function acceptsVoidUndefinedUnknownOrAny(t: ts.Type): boolean { + return !!(t.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Unknown | ts.TypeFlags.Any)); + } + + function hasCorrectArity(node: ts.CallLikeExpression, args: readonly ts.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 === ts.SyntaxKind.TaggedTemplateExpression) { + argCount = args.length; + if (node.template.kind === ts.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 = ts.last(node.template.templateSpans); // we should always have at least one span. + callIsIncomplete = ts.nodeIsMissing(lastSpan.literal) || !!lastSpan.literal.isUnterminated; } - for (let i = argCount; i < effectiveMinimumArguments; i++) { - const type = getTypeAtPosition(signature, i); - if (filterType(type, ts.isInJSFile(node) && !strictNullChecks ? acceptsVoidUndefinedUnknownOrAny : acceptsVoid).flags & ts.TypeFlags.Never) { - return false; - } + 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 as ts.LiteralExpression; + ts.Debug.assert(templateLiteral.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral); + callIsIncomplete = !!templateLiteral.isUnterminated; } - return true; } - - function hasCorrectTypeArgumentArity(signature: ts.Signature, typeArguments: ts.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 = ts.length(signature.typeParameters); - const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters); - return !ts.some(typeArguments) || - (typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters); + else if (node.kind === ts.SyntaxKind.Decorator) { + argCount = getDecoratorArgumentCount(node, signature); } - - // 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, ts.SignatureKind.Call, /*allowMembers*/ false); + else if (ts.isJsxOpeningLikeElement(node)) { + callIsIncomplete = node.attributes.end === node.end; + if (callIsIncomplete) { + return true; + } + 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 } - - function getSingleCallOrConstructSignature(type: ts.Type): ts.Signature | undefined { - return getSingleSignature(type, ts.SignatureKind.Call, /*allowMembers*/ false) || - getSingleSignature(type, ts.SignatureKind.Construct, /*allowMembers*/ false); + else if (!node.arguments) { + // This only happens when we have something of the form: 'new C' + ts.Debug.assert(node.kind === ts.SyntaxKind.NewExpression); + return getMinArgumentCount(signature) === 0; } + else { + argCount = signatureHelpTrailingComma ? args.length + 1 : args.length; - function getSingleSignature(type: ts.Type, kind: ts.SignatureKind, allowMembers: boolean): ts.Signature | undefined { - if (type.flags & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - if (allowMembers || resolved.properties.length === 0 && resolved.indexInfos.length === 0) { - if (kind === ts.SignatureKind.Call && resolved.callSignatures.length === 1 && resolved.constructSignatures.length === 0) { - return resolved.callSignatures[0]; - } - if (kind === ts.SignatureKind.Construct && resolved.constructSignatures.length === 1 && resolved.callSignatures.length === 0) { - return resolved.constructSignatures[0]; - } - } - } - return undefined; - } + // If we are missing the close parenthesis, the call is incomplete. + callIsIncomplete = node.arguments.end === node.end; - // 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?: ts.InferenceContext, compareTypes?: ts.TypeComparer): ts.Signature { - const context = createInferenceContext(signature.typeParameters!, signature, ts.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 & ts.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, ts.InferencePriority.ReturnType); - }); + // 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 getSignatureInstantiation(signature, getInferredTypes(context), ts.isInJSFile(contextualSignature.declaration)); } - function inferJsxTypeArguments(node: ts.JsxOpeningLikeElement, signature: ts.Signature, checkMode: CheckMode, context: ts.InferenceContext): ts.Type[] { - const paramType = getEffectiveFirstArgumentForJsxSignature(signature, node); - const checkAttrType = checkExpressionWithContextualType(node.attributes, paramType, context, checkMode); - inferTypes(context.inferences, checkAttrType, paramType); - return getInferredTypes(context); + // Too many arguments implies incorrect arity. + if (!hasEffectiveRestParameter(signature) && argCount > effectiveParameterCount) { + return false; } - function getThisArgumentType(thisArgumentNode: ts.LeftHandSideExpression | undefined) { - if (!thisArgumentNode) { - return voidType; + // 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, ts.isInJSFile(node) && !strictNullChecks ? acceptsVoidUndefinedUnknownOrAny : acceptsVoid).flags & ts.TypeFlags.Never) { + return false; } - const thisArgumentType = checkExpression(thisArgumentNode); - return ts.isOptionalChainRoot(thisArgumentNode.parent) ? getNonNullableType(thisArgumentType) : - ts.isOptionalChain(thisArgumentNode.parent) ? removeOptionalTypeMarker(thisArgumentType) : - thisArgumentType; } + return true; + } - function inferTypeArguments(node: ts.CallLikeExpression, signature: ts.Signature, args: readonly ts.Expression[], checkMode: CheckMode, context: ts.InferenceContext): ts.Type[] { - if (ts.isJsxOpeningLikeElement(node)) { - return inferJsxTypeArguments(node, signature, checkMode, context); - } + function hasCorrectTypeArgumentArity(signature: ts.Signature, typeArguments: ts.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 = ts.length(signature.typeParameters); + const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters); + return !ts.some(typeArguments) || + (typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters); + } - // 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 !== ts.SyntaxKind.Decorator) { - const contextualType = getContextualType(node, ts.every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p)) ? ts.ContextFlags.SkipBindingPatterns : ts.ContextFlags.None); - if (contextualType) { - const inferenceTargetType = getReturnTypeOfSignature(signature); - if (couldContainTypeVariables(inferenceTargetType)) { - // 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, ts.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; - // Inferences made from return types have lower priority than all other inferences. - inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, ts.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 = ts.some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined; - } - } - } - - const restType = getNonArrayRestType(signature); - const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; - if (restType && restType.flags & ts.TypeFlags.TypeParameter) { - const info = ts.find(context.inferences, info => info.typeParameter === restType); - if (info) { - info.impliedArity = ts.findIndex(args, isSpreadArgument, argCount) < 0 ? args.length - argCount : undefined; - } - } + // 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, ts.SignatureKind.Call, /*allowMembers*/ false); + } - const thisType = getThisTypeOfSignature(signature); - if (thisType && couldContainTypeVariables(thisType)) { - const thisArgumentNode = getThisArgumentOfCall(node); - inferTypes(context.inferences, getThisArgumentType(thisArgumentNode), thisType); - } + function getSingleCallOrConstructSignature(type: ts.Type): ts.Signature | undefined { + return getSingleSignature(type, ts.SignatureKind.Call, /*allowMembers*/ false) || + getSingleSignature(type, ts.SignatureKind.Construct, /*allowMembers*/ false); + } - for (let i = 0; i < argCount; i++) { - const arg = args[i]; - if (arg.kind !== ts.SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) { - const paramType = getTypeAtPosition(signature, i); - if (couldContainTypeVariables(paramType)) { - const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode); - inferTypes(context.inferences, argType, paramType); - } + function getSingleSignature(type: ts.Type, kind: ts.SignatureKind, allowMembers: boolean): ts.Signature | undefined { + if (type.flags & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + if (allowMembers || resolved.properties.length === 0 && resolved.indexInfos.length === 0) { + if (kind === ts.SignatureKind.Call && resolved.callSignatures.length === 1 && resolved.constructSignatures.length === 0) { + return resolved.callSignatures[0]; + } + if (kind === ts.SignatureKind.Construct && resolved.constructSignatures.length === 1 && resolved.callSignatures.length === 0) { + return resolved.constructSignatures[0]; } } + } + return undefined; + } - if (restType && couldContainTypeVariables(restType)) { - const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, context, checkMode); - inferTypes(context.inferences, spreadType, restType); - } + // 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?: ts.InferenceContext, compareTypes?: ts.TypeComparer): ts.Signature { + const context = createInferenceContext(signature.typeParameters!, signature, ts.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 & ts.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, ts.InferencePriority.ReturnType); + }); + } + return getSignatureInstantiation(signature, getInferredTypes(context), ts.isInJSFile(contextualSignature.declaration)); + } - return getInferredTypes(context); + function inferJsxTypeArguments(node: ts.JsxOpeningLikeElement, signature: ts.Signature, checkMode: CheckMode, context: ts.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 getThisArgumentType(thisArgumentNode: ts.LeftHandSideExpression | undefined) { + if (!thisArgumentNode) { + return voidType; } + const thisArgumentType = checkExpression(thisArgumentNode); + return ts.isOptionalChainRoot(thisArgumentNode.parent) ? getNonNullableType(thisArgumentType) : + ts.isOptionalChain(thisArgumentNode.parent) ? removeOptionalTypeMarker(thisArgumentType) : + thisArgumentType; + } - function getMutableArrayOrTupleType(type: ts.Type) { - return type.flags & ts.TypeFlags.Union ? mapType(type, getMutableArrayOrTupleType) : - type.flags & ts.TypeFlags.Any || isMutableArrayOrTuple(getBaseConstraintOfType(type) || type) ? type : - isTupleType(type) ? createTupleType(getTypeArguments(type), type.target.elementFlags, /*readonly*/ false, type.target.labeledElementDeclarations) : - createTupleType([type], [ts.ElementFlags.Variadic]); + function inferTypeArguments(node: ts.CallLikeExpression, signature: ts.Signature, args: readonly ts.Expression[], checkMode: CheckMode, context: ts.InferenceContext): ts.Type[] { + if (ts.isJsxOpeningLikeElement(node)) { + return inferJsxTypeArguments(node, signature, checkMode, context); } - function getSpreadArgumentType(args: readonly ts.Expression[], index: number, argCount: number, restType: ts.Type, context: ts.InferenceContext | undefined, checkMode: CheckMode) { - 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 getMutableArrayOrTupleType(arg.kind === ts.SyntaxKind.SyntheticExpression ? (arg as ts.SyntheticExpression).type : - checkExpressionWithContextualType((arg as ts.SpreadElement).expression, restType, context, checkMode)); - } - } - const types = []; - const flags = []; - const names = []; - for (let i = index; i < argCount; i++) { - const arg = args[i]; - if (isSpreadArgument(arg)) { - const spreadType = arg.kind === ts.SyntaxKind.SyntheticExpression ? (arg as ts.SyntheticExpression).type : checkExpression((arg as ts.SpreadElement).expression); - if (isArrayLikeType(spreadType)) { - types.push(spreadType); - flags.push(ts.ElementFlags.Variadic); - } - else { - types.push(checkIteratedTypeOrElementType(IterationUse.Spread, spreadType, undefinedType, arg.kind === ts.SyntaxKind.SpreadElement ? (arg as ts.SpreadElement).expression : arg)); - flags.push(ts.ElementFlags.Rest); - } + // 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 !== ts.SyntaxKind.Decorator) { + const contextualType = getContextualType(node, ts.every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p)) ? ts.ContextFlags.SkipBindingPatterns : ts.ContextFlags.None); + if (contextualType) { + const inferenceTargetType = getReturnTypeOfSignature(signature); + if (couldContainTypeVariables(inferenceTargetType)) { + // 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, ts.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; + // Inferences made from return types have lower priority than all other inferences. + inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, ts.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 = ts.some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined; + } + } + } + + const restType = getNonArrayRestType(signature); + const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; + if (restType && restType.flags & ts.TypeFlags.TypeParameter) { + const info = ts.find(context.inferences, info => info.typeParameter === restType); + if (info) { + info.impliedArity = ts.findIndex(args, isSpreadArgument, argCount) < 0 ? args.length - argCount : undefined; + } + } + + const thisType = getThisTypeOfSignature(signature); + if (thisType && couldContainTypeVariables(thisType)) { + const thisArgumentNode = getThisArgumentOfCall(node); + inferTypes(context.inferences, getThisArgumentType(thisArgumentNode), thisType); + } + + for (let i = 0; i < argCount; i++) { + const arg = args[i]; + if (arg.kind !== ts.SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) { + const paramType = getTypeAtPosition(signature, i); + if (couldContainTypeVariables(paramType)) { + const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode); + inferTypes(context.inferences, argType, paramType); + } + } + } + + if (restType && couldContainTypeVariables(restType)) { + const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, context, checkMode); + inferTypes(context.inferences, spreadType, restType); + } + + return getInferredTypes(context); + } + + function getMutableArrayOrTupleType(type: ts.Type) { + return type.flags & ts.TypeFlags.Union ? mapType(type, getMutableArrayOrTupleType) : + type.flags & ts.TypeFlags.Any || isMutableArrayOrTuple(getBaseConstraintOfType(type) || type) ? type : + isTupleType(type) ? createTupleType(getTypeArguments(type), type.target.elementFlags, /*readonly*/ false, type.target.labeledElementDeclarations) : + createTupleType([type], [ts.ElementFlags.Variadic]); + } + + function getSpreadArgumentType(args: readonly ts.Expression[], index: number, argCount: number, restType: ts.Type, context: ts.InferenceContext | undefined, checkMode: CheckMode) { + 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 getMutableArrayOrTupleType(arg.kind === ts.SyntaxKind.SyntheticExpression ? (arg as ts.SyntheticExpression).type : + checkExpressionWithContextualType((arg as ts.SpreadElement).expression, restType, context, checkMode)); + } + } + const types = []; + const flags = []; + const names = []; + for (let i = index; i < argCount; i++) { + const arg = args[i]; + if (isSpreadArgument(arg)) { + const spreadType = arg.kind === ts.SyntaxKind.SyntheticExpression ? (arg as ts.SyntheticExpression).type : checkExpression((arg as ts.SpreadElement).expression); + if (isArrayLikeType(spreadType)) { + types.push(spreadType); + flags.push(ts.ElementFlags.Variadic); } else { - const contextualType = getIndexedAccessType(restType, getNumberLiteralType(i - index), ts.AccessFlags.Contextual); - const argType = checkExpressionWithContextualType(arg, contextualType, context, checkMode); - const hasPrimitiveContextualType = maybeTypeOfKind(contextualType, ts.TypeFlags.Primitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping); - types.push(hasPrimitiveContextualType ? getRegularTypeOfLiteralType(argType) : getWidenedLiteralType(argType)); - flags.push(ts.ElementFlags.Required); - } - if (arg.kind === ts.SyntaxKind.SyntheticExpression && (arg as ts.SyntheticExpression).tupleNameSource) { - names.push((arg as ts.SyntheticExpression).tupleNameSource!); + types.push(checkIteratedTypeOrElementType(IterationUse.Spread, spreadType, undefinedType, arg.kind === ts.SyntaxKind.SpreadElement ? (arg as ts.SpreadElement).expression : arg)); + flags.push(ts.ElementFlags.Rest); } } - return createTupleType(types, flags, /*readonly*/ false, ts.length(names) === ts.length(types) ? names : undefined); + else { + const contextualType = getIndexedAccessType(restType, getNumberLiteralType(i - index), ts.AccessFlags.Contextual); + const argType = checkExpressionWithContextualType(arg, contextualType, context, checkMode); + const hasPrimitiveContextualType = maybeTypeOfKind(contextualType, ts.TypeFlags.Primitive | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping); + types.push(hasPrimitiveContextualType ? getRegularTypeOfLiteralType(argType) : getWidenedLiteralType(argType)); + flags.push(ts.ElementFlags.Required); + } + if (arg.kind === ts.SyntaxKind.SyntheticExpression && (arg as ts.SyntheticExpression).tupleNameSource) { + names.push((arg as ts.SyntheticExpression).tupleNameSource!); + } } + return createTupleType(types, flags, /*readonly*/ false, ts.length(names) === ts.length(types) ? names : undefined); + } - function checkTypeArguments(signature: ts.Signature, typeArgumentNodes: readonly ts.TypeNode[], reportErrors: boolean, headMessage?: ts.DiagnosticMessage): ts.Type[] | undefined { - const isJavascript = ts.isInJSFile(signature.declaration); - const typeParameters = signature.typeParameters!; - const typeArgumentTypes = fillMissingTypeArguments(ts.map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript); - let mapper: ts.TypeMapper | undefined; - for (let i = 0; i < typeArgumentNodes.length; i++) { - ts.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 ? (() => ts.chainDiagnosticMessages(/*details*/ undefined, ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1)) : undefined; - const typeArgumentHeadMessage = headMessage || ts.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; - } + function checkTypeArguments(signature: ts.Signature, typeArgumentNodes: readonly ts.TypeNode[], reportErrors: boolean, headMessage?: ts.DiagnosticMessage): ts.Type[] | undefined { + const isJavascript = ts.isInJSFile(signature.declaration); + const typeParameters = signature.typeParameters!; + const typeArgumentTypes = fillMissingTypeArguments(ts.map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript); + let mapper: ts.TypeMapper | undefined; + for (let i = 0; i < typeArgumentNodes.length; i++) { + ts.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 ? (() => ts.chainDiagnosticMessages(/*details*/ undefined, ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1)) : undefined; + const typeArgumentHeadMessage = headMessage || ts.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 typeArgumentTypes; } + return typeArgumentTypes; + } - function getJsxReferenceKind(node: ts.JsxOpeningLikeElement): ts.JsxReferenceKind { - if (isJsxIntrinsicIdentifier(node.tagName)) { - return ts.JsxReferenceKind.Mixed; - } - const tagType = getApparentType(checkExpression(node.tagName)); - if (ts.length(getSignaturesOfType(tagType, ts.SignatureKind.Construct))) { - return ts.JsxReferenceKind.Component; - } - if (ts.length(getSignaturesOfType(tagType, ts.SignatureKind.Call))) { - return ts.JsxReferenceKind.Function; - } + function getJsxReferenceKind(node: ts.JsxOpeningLikeElement): ts.JsxReferenceKind { + if (isJsxIntrinsicIdentifier(node.tagName)) { return ts.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: ts.JsxOpeningLikeElement, signature: ts.Signature, relation: ts.ESMap, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { - errors?: ts.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 { - if (getJsxNamespaceContainerForImplicitImport(node)) { - return true; // factory is implicitly jsx/jsxdev - assume it fits the bill, since we don't strongly look for the jsx/jsxs/jsxDEV factory APIs anywhere else (at least not yet) - } - const tagType = ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined; - if (!tagType) { - return true; - } - const tagCallSignatures = getSignaturesOfType(tagType, ts.SignatureKind.Call); - if (!ts.length(tagCallSignatures)) { - return true; - } - const factory = getJsxFactoryEntity(node); - if (!factory) { - return true; - } - const factorySymbol = resolveEntityName(factory, ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, node); - if (!factorySymbol) { - return true; - } - - const factoryType = getTypeOfSymbol(factorySymbol); - const callSignatures = getSignaturesOfType(factoryType, ts.SignatureKind.Call); - if (!ts.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, ts.SignatureKind.Call); - if (!ts.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; - } - } - } - 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 (absoluteMinArgCount <= maxParamCount) { - return true; // some signature accepts the number of arguments the function component provides - } - - if (reportErrors) { - const diag = ts.createDiagnosticForNode(node.tagName, ts.Diagnostics.Tag_0_expects_at_least_1_arguments_but_the_JSX_factory_2_provides_at_most_3, ts.entityNameToString(node.tagName), absoluteMinArgCount, ts.entityNameToString(factory), maxParamCount); - const tagNameDeclaration = getSymbolAtLocation(node.tagName)?.valueDeclaration; - if (tagNameDeclaration) { - ts.addRelatedInfo(diag, ts.createDiagnosticForNode(tagNameDeclaration, ts.Diagnostics._0_is_declared_here, ts.entityNameToString(node.tagName))); - } - if (errorOutputContainer && errorOutputContainer.skipLogging) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - if (!errorOutputContainer.skipLogging) { - diagnostics.add(diag); - } - } - return false; - } + const tagType = getApparentType(checkExpression(node.tagName)); + if (ts.length(getSignaturesOfType(tagType, ts.SignatureKind.Construct))) { + return ts.JsxReferenceKind.Component; + } + if (ts.length(getSignaturesOfType(tagType, ts.SignatureKind.Call))) { + return ts.JsxReferenceKind.Function; } + return ts.JsxReferenceKind.Mixed; + } - function getSignatureApplicabilityError(node: ts.CallLikeExpression, args: readonly ts.Expression[], signature: ts.Signature, relation: ts.ESMap, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined): readonly ts.Diagnostic[] | undefined { - const errorOutputContainer: { - errors?: ts.Diagnostic[]; - skipLogging?: boolean; - } = { errors: undefined, skipLogging: true }; - if (ts.isJsxOpeningLikeElement(node)) { - if (!checkApplicableSignatureForJsxOpeningLikeElement(node, signature, relation, checkMode, reportErrors, containingMessageChain, errorOutputContainer)) { - ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "jsx should have errors when reporting errors"); - return errorOutputContainer.errors || ts.emptyArray; - } - return undefined; + /** + * 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: ts.JsxOpeningLikeElement, signature: ts.Signature, relation: ts.ESMap, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: ts.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 { + if (getJsxNamespaceContainerForImplicitImport(node)) { + return true; // factory is implicitly jsx/jsxdev - assume it fits the bill, since we don't strongly look for the jsx/jsxs/jsxDEV factory APIs anywhere else (at least not yet) + } + const tagType = ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined; + if (!tagType) { + return true; } - const thisType = getThisTypeOfSignature(signature); - if (thisType && thisType !== voidType && node.kind !== ts.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); - const thisArgumentType = getThisArgumentType(thisArgumentNode); - const errorNode = reportErrors ? (thisArgumentNode || node) : undefined; - const headMessage = ts.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)) { - ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "this parameter should have errors when reporting errors"); - return errorOutputContainer.errors || ts.emptyArray; - } + const tagCallSignatures = getSignaturesOfType(tagType, ts.SignatureKind.Call); + if (!ts.length(tagCallSignatures)) { + return true; } - const headMessage = ts.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 !== ts.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)) { - ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); - maybeAddMissingAwaitInfo(arg, checkArgType, paramType); - return errorOutputContainer.errors || ts.emptyArray; - } - } - } - if (restType) { - const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, /*context*/ undefined, checkMode); - const restArgCount = args.length - argCount; - const errorNode = !reportErrors ? undefined : - restArgCount === 0 ? node : - restArgCount === 1 ? args[argCount] : - ts.setTextRangePosEnd(createSyntheticExpression(node, spreadType), args[argCount].pos, args[args.length - 1].end); - if (!checkTypeRelatedTo(spreadType, restType, relation, errorNode, headMessage, /*containingMessageChain*/ undefined, errorOutputContainer)) { - ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "rest parameter should have errors when reporting errors"); - maybeAddMissingAwaitInfo(errorNode, spreadType, restType); - return errorOutputContainer.errors || ts.emptyArray; - } + const factory = getJsxFactoryEntity(node); + if (!factory) { + return true; } - return undefined; - - function maybeAddMissingAwaitInfo(errorNode: ts.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)) { - ts.addRelatedInfo(errorOutputContainer.errors[0], ts.createDiagnosticForNode(errorNode, ts.Diagnostics.Did_you_forget_to_use_await)); - } - } + const factorySymbol = resolveEntityName(factory, ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, node); + if (!factorySymbol) { + return true; } - } - /** - * Returns the this argument in calls like x.f(...) and x[f](...). Undefined otherwise. - */ - function getThisArgumentOfCall(node: ts.CallLikeExpression): ts.LeftHandSideExpression | undefined { - const expression = node.kind === ts.SyntaxKind.CallExpression ? node.expression : - node.kind === ts.SyntaxKind.TaggedTemplateExpression ? node.tag : undefined; - if (expression) { - const callee = ts.skipOuterExpressions(expression); - if (ts.isAccessExpression(callee)) { - return callee.expression; - } + const factoryType = getTypeOfSymbol(factorySymbol); + const callSignatures = getSignaturesOfType(factoryType, ts.SignatureKind.Call); + if (!ts.length(callSignatures)) { + return true; } - } - - function createSyntheticExpression(parent: ts.Node, type: ts.Type, isSpread?: boolean, tupleNameSource?: ts.ParameterDeclaration | ts.NamedTupleMember) { - const result = ts.parseNodeFactory.createSyntheticExpression(type, isSpread, tupleNameSource); - ts.setTextRange(result, parent); - ts.setParent(result, parent); - return result; - } - /** - * Returns the effective arguments for an expression that works like a function invocation. - */ - function getEffectiveCallArguments(node: ts.CallLikeExpression): readonly ts.Expression[] { - if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { - const template = node.template; - const args: ts.Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())]; - if (template.kind === ts.SyntaxKind.TemplateExpression) { - ts.forEach(template.templateSpans, span => { - args.push(span.expression); - }); + 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, ts.SignatureKind.Call); + if (!ts.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; + } } - return args; - } - if (node.kind === ts.SyntaxKind.Decorator) { - return getEffectiveDecoratorArguments(node); } - if (ts.isJsxOpeningLikeElement(node)) { - return node.attributes.properties.length > 0 || (ts.isJsxOpeningElement(node) && node.parent.children.length > 0) ? [node.attributes] : ts.emptyArray; + 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; } - const args = node.arguments || ts.emptyArray; - const spreadIndex = getSpreadArgumentIndex(args); - if (spreadIndex >= 0) { - // Create synthetic arguments from spreads of tuple types. - const effectiveArgs = args.slice(0, spreadIndex); - for (let i = spreadIndex; i < args.length; i++) { - const arg = args[i]; - // We can call checkExpressionCached because spread expressions never have a contextual type. - const spreadType = arg.kind === ts.SyntaxKind.SpreadElement && (flowLoopCount ? checkExpression((arg as ts.SpreadElement).expression) : checkExpressionCached((arg as ts.SpreadElement).expression)); - if (spreadType && isTupleType(spreadType)) { - ts.forEach(getTypeArguments(spreadType), (t, i) => { - const flags = spreadType.target.elementFlags[i]; - const syntheticArg = createSyntheticExpression(arg, flags & ts.ElementFlags.Rest ? createArrayType(t) : t, !!(flags & ts.ElementFlags.Variable), spreadType.target.labeledElementDeclarations?.[i]); - effectiveArgs.push(syntheticArg); - }); - } - else { - effectiveArgs.push(arg); - } + let absoluteMinArgCount = Infinity; + for (const tagSig of tagCallSignatures) { + const tagRequiredArgCount = getMinArgumentCount(tagSig); + if (tagRequiredArgCount < absoluteMinArgCount) { + absoluteMinArgCount = tagRequiredArgCount; } - return effectiveArgs; } - return args; - } - - /** - * Returns the synthetic argument list for a decorator invocation. - */ - function getEffectiveDecoratorArguments(node: ts.Decorator): readonly ts.Expression[] { - const parent = node.parent; - const expr = node.expression; - switch (parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.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 ts.SyntaxKind.Parameter: - // A parameter declaration decorator will have three arguments (see - // `ParameterDecorator` in core.d.ts). - const func = parent.parent as ts.FunctionLikeDeclaration; - return [ - createSyntheticExpression(expr, parent.parent.kind === ts.SyntaxKind.Constructor ? getTypeOfSymbol(getSymbolOfNode(func)) : errorType), - createSyntheticExpression(expr, anyType), - createSyntheticExpression(expr, numberType) - ]; - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.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 !== ts.SyntaxKind.PropertyDeclaration && languageVersion !== ts.ScriptTarget.ES3; - return [ - createSyntheticExpression(expr, getParentTypeOfClassElement(parent as ts.ClassElement)), - createSyntheticExpression(expr, getClassElementPropertyKeyType(parent as ts.ClassElement)), - createSyntheticExpression(expr, hasPropDesc ? createTypedPropertyDescriptorType(getTypeOfNode(parent)) : anyType) - ]; + if (absoluteMinArgCount <= maxParamCount) { + return true; // some signature accepts the number of arguments the function component provides } - return ts.Debug.fail(); - } - /** - * Returns the argument count for a decorator node that works like a function invocation. - */ - function getDecoratorArgumentCount(node: ts.Decorator, signature: ts.Signature) { - switch (node.parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return 1; - case ts.SyntaxKind.PropertyDeclaration: - return 2; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - // For ES3 or decorators with only two parameters we supply only two arguments - return languageVersion === ts.ScriptTarget.ES3 || signature.parameters.length <= 2 ? 2 : 3; - case ts.SyntaxKind.Parameter: - return 3; - default: - return ts.Debug.fail(); + if (reportErrors) { + const diag = ts.createDiagnosticForNode(node.tagName, ts.Diagnostics.Tag_0_expects_at_least_1_arguments_but_the_JSX_factory_2_provides_at_most_3, ts.entityNameToString(node.tagName), absoluteMinArgCount, ts.entityNameToString(factory), maxParamCount); + const tagNameDeclaration = getSymbolAtLocation(node.tagName)?.valueDeclaration; + if (tagNameDeclaration) { + ts.addRelatedInfo(diag, ts.createDiagnosticForNode(tagNameDeclaration, ts.Diagnostics._0_is_declared_here, ts.entityNameToString(node.tagName))); + } + if (errorOutputContainer && errorOutputContainer.skipLogging) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); + } + if (!errorOutputContainer.skipLogging) { + diagnostics.add(diag); + } } + return false; } - function getDiagnosticSpanForCallNode(node: ts.CallExpression, doNotIncludeArguments?: boolean) { - let start: number; - let length: number; - const sourceFile = ts.getSourceFileOfNode(node); - if (ts.isPropertyAccessExpression(node.expression)) { - const nameSpan = ts.getErrorSpanForNode(sourceFile, node.expression.name); - start = nameSpan.start; - length = doNotIncludeArguments ? nameSpan.length : node.end - start; + } + + function getSignatureApplicabilityError(node: ts.CallLikeExpression, args: readonly ts.Expression[], signature: ts.Signature, relation: ts.ESMap, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => ts.DiagnosticMessageChain | undefined) | undefined): readonly ts.Diagnostic[] | undefined { + const errorOutputContainer: { + errors?: ts.Diagnostic[]; + skipLogging?: boolean; + } = { errors: undefined, skipLogging: true }; + if (ts.isJsxOpeningLikeElement(node)) { + if (!checkApplicableSignatureForJsxOpeningLikeElement(node, signature, relation, checkMode, reportErrors, containingMessageChain, errorOutputContainer)) { + ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "jsx should have errors when reporting errors"); + return errorOutputContainer.errors || ts.emptyArray; } - else { - const expressionSpan = ts.getErrorSpanForNode(sourceFile, node.expression); - start = expressionSpan.start; - length = doNotIncludeArguments ? expressionSpan.length : node.end - start; + return undefined; + } + const thisType = getThisTypeOfSignature(signature); + if (thisType && thisType !== voidType && node.kind !== ts.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); + const thisArgumentType = getThisArgumentType(thisArgumentNode); + const errorNode = reportErrors ? (thisArgumentNode || node) : undefined; + const headMessage = ts.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)) { + ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "this parameter should have errors when reporting errors"); + return errorOutputContainer.errors || ts.emptyArray; + } + } + const headMessage = ts.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 !== ts.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)) { + ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); + maybeAddMissingAwaitInfo(arg, checkArgType, paramType); + return errorOutputContainer.errors || ts.emptyArray; + } } - return { start, length, sourceFile }; } - function getDiagnosticForCallNode(node: ts.CallLikeExpression, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.DiagnosticWithLocation { - if (ts.isCallExpression(node)) { - const { sourceFile, start, length } = getDiagnosticSpanForCallNode(node); - return ts.createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2, arg3); + if (restType) { + const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, /*context*/ undefined, checkMode); + const restArgCount = args.length - argCount; + const errorNode = !reportErrors ? undefined : + restArgCount === 0 ? node : + restArgCount === 1 ? args[argCount] : + ts.setTextRangePosEnd(createSyntheticExpression(node, spreadType), args[argCount].pos, args[args.length - 1].end); + if (!checkTypeRelatedTo(spreadType, restType, relation, errorNode, headMessage, /*containingMessageChain*/ undefined, errorOutputContainer)) { + ts.Debug.assert(!reportErrors || !!errorOutputContainer.errors, "rest parameter should have errors when reporting errors"); + maybeAddMissingAwaitInfo(errorNode, spreadType, restType); + return errorOutputContainer.errors || ts.emptyArray; } - else { - return ts.createDiagnosticForNode(node, message, arg0, arg1, arg2, arg3); + } + return undefined; + + function maybeAddMissingAwaitInfo(errorNode: ts.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)) { + ts.addRelatedInfo(errorOutputContainer.errors[0], ts.createDiagnosticForNode(errorNode, ts.Diagnostics.Did_you_forget_to_use_await)); + } } } + } - function isPromiseResolveArityError(node: ts.CallLikeExpression) { - if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) - return false; - const symbol = resolveName(node.expression, node.expression.escapedText, ts.SymbolFlags.Value, undefined, undefined, false); - const decl = symbol?.valueDeclaration; - if (!decl || !ts.isParameter(decl) || !ts.isFunctionExpressionOrArrowFunction(decl.parent) || !ts.isNewExpression(decl.parent.parent) || !ts.isIdentifier(decl.parent.parent.expression)) { - return false; + /** + * Returns the this argument in calls like x.f(...) and x[f](...). Undefined otherwise. + */ + function getThisArgumentOfCall(node: ts.CallLikeExpression): ts.LeftHandSideExpression | undefined { + const expression = node.kind === ts.SyntaxKind.CallExpression ? node.expression : + node.kind === ts.SyntaxKind.TaggedTemplateExpression ? node.tag : undefined; + if (expression) { + const callee = ts.skipOuterExpressions(expression); + if (ts.isAccessExpression(callee)) { + return callee.expression; } + } + } - const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); - if (!globalPromiseSymbol) - return false; + function createSyntheticExpression(parent: ts.Node, type: ts.Type, isSpread?: boolean, tupleNameSource?: ts.ParameterDeclaration | ts.NamedTupleMember) { + const result = ts.parseNodeFactory.createSyntheticExpression(type, isSpread, tupleNameSource); + ts.setTextRange(result, parent); + ts.setParent(result, parent); + return result; + } - const constructorSymbol = getSymbolAtLocation(decl.parent.parent.expression, /*ignoreErrors*/ true); - return constructorSymbol === globalPromiseSymbol; - } - - function getArgumentArityError(node: ts.CallLikeExpression, signatures: readonly ts.Signature[], args: readonly ts.Expression[]) { - const spreadIndex = getSpreadArgumentIndex(args); - if (spreadIndex > -1) { - return ts.createDiagnosticForNode(args[spreadIndex], ts.Diagnostics.A_spread_argument_must_either_have_a_tuple_type_or_be_passed_to_a_rest_parameter); - } - let min = Number.POSITIVE_INFINITY; // smallest parameter count - let max = Number.NEGATIVE_INFINITY; // largest parameter count - let maxBelow = Number.NEGATIVE_INFINITY; // largest parameter count that is smaller than the number of arguments - let minAbove = Number.POSITIVE_INFINITY; // smallest parameter count that is larger than the number of arguments - - let closestSignature: ts.Signature | undefined; - for (const sig of signatures) { - const minParameter = getMinArgumentCount(sig); - const maxParameter = getParameterCount(sig); - // smallest/largest parameter counts - if (minParameter < min) { - min = minParameter; - closestSignature = sig; - } - max = Math.max(max, maxParameter); - // shortest parameter count *longer than the call*/longest parameter count *shorter than the call* - if (minParameter < args.length && minParameter > maxBelow) - maxBelow = minParameter; - if (args.length < maxParameter && maxParameter < minAbove) - minAbove = maxParameter; - } - const hasRestParameter = ts.some(signatures, hasEffectiveRestParameter); - const parameterRange = hasRestParameter ? min - : min < max ? min + "-" + max - : min; - const isVoidPromiseError = !hasRestParameter && parameterRange === 1 && args.length === 0 && isPromiseResolveArityError(node); - if (isVoidPromiseError && ts.isInJSFile(node)) { - return getDiagnosticForCallNode(node, ts.Diagnostics.Expected_1_argument_but_got_0_new_Promise_needs_a_JSDoc_hint_to_produce_a_resolve_that_can_be_called_without_arguments); - } - const error = hasRestParameter - ? ts.Diagnostics.Expected_at_least_0_arguments_but_got_1 - : isVoidPromiseError - ? ts.Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise - : ts.Diagnostics.Expected_0_arguments_but_got_1; - if (min < args.length && args.length < max) { - // between min and max, but with no matching overload - return getDiagnosticForCallNode(node, ts.Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, args.length, maxBelow, minAbove); - } - else if (args.length < min) { - // too short: put the error span on the call expression, not any of the args - const diagnostic = getDiagnosticForCallNode(node, error, parameterRange, args.length); - const parameter = closestSignature?.declaration?.parameters[closestSignature.thisParameter ? args.length + 1 : args.length]; - if (parameter) { - const parameterError = ts.createDiagnosticForNode(parameter, ts.isBindingPattern(parameter.name) ? ts.Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided - : ts.isRestParameter(parameter) ? ts.Diagnostics.Arguments_for_the_rest_parameter_0_were_not_provided - : ts.Diagnostics.An_argument_for_0_was_not_provided, !parameter.name ? args.length : !ts.isBindingPattern(parameter.name) ? ts.idText(ts.getFirstIdentifier(parameter.name)) : undefined); - return ts.addRelatedInfo(diagnostic, parameterError); - } - return diagnostic; + /** + * Returns the effective arguments for an expression that works like a function invocation. + */ + function getEffectiveCallArguments(node: ts.CallLikeExpression): readonly ts.Expression[] { + if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { + const template = node.template; + const args: ts.Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())]; + if (template.kind === ts.SyntaxKind.TemplateExpression) { + ts.forEach(template.templateSpans, span => { + args.push(span.expression); + }); } - else { - // too long; error goes on the excess parameters - const errorSpan = ts.factory.createNodeArray(args.slice(max)); - const pos = ts.first(errorSpan).pos; - let end = ts.last(errorSpan).end; - if (end === pos) { - end++; + return args; + } + if (node.kind === ts.SyntaxKind.Decorator) { + return getEffectiveDecoratorArguments(node); + } + if (ts.isJsxOpeningLikeElement(node)) { + return node.attributes.properties.length > 0 || (ts.isJsxOpeningElement(node) && node.parent.children.length > 0) ? [node.attributes] : ts.emptyArray; + } + const args = node.arguments || ts.emptyArray; + const spreadIndex = getSpreadArgumentIndex(args); + if (spreadIndex >= 0) { + // Create synthetic arguments from spreads of tuple types. + const effectiveArgs = args.slice(0, spreadIndex); + for (let i = spreadIndex; i < args.length; i++) { + const arg = args[i]; + // We can call checkExpressionCached because spread expressions never have a contextual type. + const spreadType = arg.kind === ts.SyntaxKind.SpreadElement && (flowLoopCount ? checkExpression((arg as ts.SpreadElement).expression) : checkExpressionCached((arg as ts.SpreadElement).expression)); + if (spreadType && isTupleType(spreadType)) { + ts.forEach(getTypeArguments(spreadType), (t, i) => { + const flags = spreadType.target.elementFlags[i]; + const syntheticArg = createSyntheticExpression(arg, flags & ts.ElementFlags.Rest ? createArrayType(t) : t, !!(flags & ts.ElementFlags.Variable), spreadType.target.labeledElementDeclarations?.[i]); + effectiveArgs.push(syntheticArg); + }); + } + else { + effectiveArgs.push(arg); } - ts.setTextRangePosEnd(errorSpan, pos, end); - return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), errorSpan, error, parameterRange, args.length); } + return effectiveArgs; + } + return args; + } + + /** + * Returns the synthetic argument list for a decorator invocation. + */ + function getEffectiveDecoratorArguments(node: ts.Decorator): readonly ts.Expression[] { + const parent = node.parent; + const expr = node.expression; + switch (parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.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 ts.SyntaxKind.Parameter: + // A parameter declaration decorator will have three arguments (see + // `ParameterDecorator` in core.d.ts). + const func = parent.parent as ts.FunctionLikeDeclaration; + return [ + createSyntheticExpression(expr, parent.parent.kind === ts.SyntaxKind.Constructor ? getTypeOfSymbol(getSymbolOfNode(func)) : errorType), + createSyntheticExpression(expr, anyType), + createSyntheticExpression(expr, numberType) + ]; + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.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 !== ts.SyntaxKind.PropertyDeclaration && languageVersion !== ts.ScriptTarget.ES3; + return [ + createSyntheticExpression(expr, getParentTypeOfClassElement(parent as ts.ClassElement)), + createSyntheticExpression(expr, getClassElementPropertyKeyType(parent as ts.ClassElement)), + createSyntheticExpression(expr, hasPropDesc ? createTypedPropertyDescriptorType(getTypeOfNode(parent)) : anyType) + ]; + } + return ts.Debug.fail(); + } + + /** + * Returns the argument count for a decorator node that works like a function invocation. + */ + function getDecoratorArgumentCount(node: ts.Decorator, signature: ts.Signature) { + switch (node.parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return 1; + case ts.SyntaxKind.PropertyDeclaration: + return 2; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + // For ES3 or decorators with only two parameters we supply only two arguments + return languageVersion === ts.ScriptTarget.ES3 || signature.parameters.length <= 2 ? 2 : 3; + case ts.SyntaxKind.Parameter: + return 3; + default: + return ts.Debug.fail(); + } + } + function getDiagnosticSpanForCallNode(node: ts.CallExpression, doNotIncludeArguments?: boolean) { + let start: number; + let length: number; + const sourceFile = ts.getSourceFileOfNode(node); + if (ts.isPropertyAccessExpression(node.expression)) { + const nameSpan = ts.getErrorSpanForNode(sourceFile, node.expression.name); + start = nameSpan.start; + length = doNotIncludeArguments ? nameSpan.length : node.end - start; + } + else { + const expressionSpan = ts.getErrorSpanForNode(sourceFile, node.expression); + start = expressionSpan.start; + length = doNotIncludeArguments ? expressionSpan.length : node.end - start; + } + return { start, length, sourceFile }; + } + function getDiagnosticForCallNode(node: ts.CallLikeExpression, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): ts.DiagnosticWithLocation { + if (ts.isCallExpression(node)) { + const { sourceFile, start, length } = getDiagnosticSpanForCallNode(node); + return ts.createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2, arg3); + } + else { + return ts.createDiagnosticForNode(node, message, arg0, arg1, arg2, arg3); + } + } + + function isPromiseResolveArityError(node: ts.CallLikeExpression) { + if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) + return false; + const symbol = resolveName(node.expression, node.expression.escapedText, ts.SymbolFlags.Value, undefined, undefined, false); + const decl = symbol?.valueDeclaration; + if (!decl || !ts.isParameter(decl) || !ts.isFunctionExpressionOrArrowFunction(decl.parent) || !ts.isNewExpression(decl.parent.parent) || !ts.isIdentifier(decl.parent.parent.expression)) { + return false; } - function getTypeArgumentArityError(node: ts.Node, signatures: readonly ts.Signature[], typeArguments: ts.NodeArray) { - const argCount = typeArguments.length; - // No overloads exist - if (signatures.length === 1) { - const sig = signatures[0]; - const min = getMinTypeArgumentCount(sig.typeParameters); - const max = ts.length(sig.typeParameters); - return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.Expected_0_type_arguments_but_got_1, min < max ? min + "-" + max : min, argCount); + const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); + if (!globalPromiseSymbol) + return false; + + const constructorSymbol = getSymbolAtLocation(decl.parent.parent.expression, /*ignoreErrors*/ true); + return constructorSymbol === globalPromiseSymbol; + } + + function getArgumentArityError(node: ts.CallLikeExpression, signatures: readonly ts.Signature[], args: readonly ts.Expression[]) { + const spreadIndex = getSpreadArgumentIndex(args); + if (spreadIndex > -1) { + return ts.createDiagnosticForNode(args[spreadIndex], ts.Diagnostics.A_spread_argument_must_either_have_a_tuple_type_or_be_passed_to_a_rest_parameter); + } + let min = Number.POSITIVE_INFINITY; // smallest parameter count + let max = Number.NEGATIVE_INFINITY; // largest parameter count + let maxBelow = Number.NEGATIVE_INFINITY; // largest parameter count that is smaller than the number of arguments + let minAbove = Number.POSITIVE_INFINITY; // smallest parameter count that is larger than the number of arguments + + let closestSignature: ts.Signature | undefined; + for (const sig of signatures) { + const minParameter = getMinArgumentCount(sig); + const maxParameter = getParameterCount(sig); + // smallest/largest parameter counts + if (minParameter < min) { + min = minParameter; + closestSignature = sig; + } + max = Math.max(max, maxParameter); + // shortest parameter count *longer than the call*/longest parameter count *shorter than the call* + if (minParameter < args.length && minParameter > maxBelow) + maxBelow = minParameter; + if (args.length < maxParameter && maxParameter < minAbove) + minAbove = maxParameter; + } + const hasRestParameter = ts.some(signatures, hasEffectiveRestParameter); + const parameterRange = hasRestParameter ? min + : min < max ? min + "-" + max + : min; + const isVoidPromiseError = !hasRestParameter && parameterRange === 1 && args.length === 0 && isPromiseResolveArityError(node); + if (isVoidPromiseError && ts.isInJSFile(node)) { + return getDiagnosticForCallNode(node, ts.Diagnostics.Expected_1_argument_but_got_0_new_Promise_needs_a_JSDoc_hint_to_produce_a_resolve_that_can_be_called_without_arguments); + } + const error = hasRestParameter + ? ts.Diagnostics.Expected_at_least_0_arguments_but_got_1 + : isVoidPromiseError + ? ts.Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise + : ts.Diagnostics.Expected_0_arguments_but_got_1; + if (min < args.length && args.length < max) { + // between min and max, but with no matching overload + return getDiagnosticForCallNode(node, ts.Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, args.length, maxBelow, minAbove); + } + else if (args.length < min) { + // too short: put the error span on the call expression, not any of the args + const diagnostic = getDiagnosticForCallNode(node, error, parameterRange, args.length); + const parameter = closestSignature?.declaration?.parameters[closestSignature.thisParameter ? args.length + 1 : args.length]; + if (parameter) { + const parameterError = ts.createDiagnosticForNode(parameter, ts.isBindingPattern(parameter.name) ? ts.Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided + : ts.isRestParameter(parameter) ? ts.Diagnostics.Arguments_for_the_rest_parameter_0_were_not_provided + : ts.Diagnostics.An_argument_for_0_was_not_provided, !parameter.name ? args.length : !ts.isBindingPattern(parameter.name) ? ts.idText(ts.getFirstIdentifier(parameter.name)) : undefined); + return ts.addRelatedInfo(diagnostic, parameterError); } - // Overloads exist - let belowArgCount = -Infinity; - let aboveArgCount = Infinity; - for (const sig of signatures) { - const min = getMinTypeArgumentCount(sig.typeParameters); - const max = ts.length(sig.typeParameters); - if (min > argCount) { - aboveArgCount = Math.min(aboveArgCount, min); - } - else if (max < argCount) { - belowArgCount = Math.max(belowArgCount, max); - } + return diagnostic; + } + else { + // too long; error goes on the excess parameters + const errorSpan = ts.factory.createNodeArray(args.slice(max)); + const pos = ts.first(errorSpan).pos; + let end = ts.last(errorSpan).end; + if (end === pos) { + end++; + } + ts.setTextRangePosEnd(errorSpan, pos, end); + return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), errorSpan, error, parameterRange, args.length); + } + } + + function getTypeArgumentArityError(node: ts.Node, signatures: readonly ts.Signature[], typeArguments: ts.NodeArray) { + const argCount = typeArguments.length; + // No overloads exist + if (signatures.length === 1) { + const sig = signatures[0]; + const min = getMinTypeArgumentCount(sig.typeParameters); + const max = ts.length(sig.typeParameters); + return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.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 = ts.length(sig.typeParameters); + if (min > argCount) { + aboveArgCount = Math.min(aboveArgCount, min); } - if (belowArgCount !== -Infinity && aboveArgCount !== Infinity) { - return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.No_overload_expects_0_type_arguments_but_overloads_do_exist_that_expect_either_1_or_2_type_arguments, argCount, belowArgCount, aboveArgCount); + else if (max < argCount) { + belowArgCount = Math.max(belowArgCount, max); } - return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount); } + if (belowArgCount !== -Infinity && aboveArgCount !== Infinity) { + return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.No_overload_expects_0_type_arguments_but_overloads_do_exist_that_expect_either_1_or_2_type_arguments, argCount, belowArgCount, aboveArgCount); + } + return ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount); + } - function resolveCall(node: ts.CallLikeExpression, signatures: readonly ts.Signature[], candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode, callChainFlags: ts.SignatureFlags, fallbackError?: ts.DiagnosticMessage): ts.Signature { - const isTaggedTemplate = node.kind === ts.SyntaxKind.TaggedTemplateExpression; - const isDecorator = node.kind === ts.SyntaxKind.Decorator; - const isJsxOpeningOrSelfClosingElement = ts.isJsxOpeningLikeElement(node); - const reportErrors = !candidatesOutArray; + function resolveCall(node: ts.CallLikeExpression, signatures: readonly ts.Signature[], candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode, callChainFlags: ts.SignatureFlags, fallbackError?: ts.DiagnosticMessage): ts.Signature { + const isTaggedTemplate = node.kind === ts.SyntaxKind.TaggedTemplateExpression; + const isDecorator = node.kind === ts.SyntaxKind.Decorator; + const isJsxOpeningOrSelfClosingElement = ts.isJsxOpeningLikeElement(node); + const reportErrors = !candidatesOutArray; - let typeArguments: ts.NodeArray | undefined; + let typeArguments: ts.NodeArray | undefined; - if (!isDecorator) { - typeArguments = (node as ts.CallExpression).typeArguments; + if (!isDecorator) { + typeArguments = (node as ts.CallExpression).typeArguments; - // We already perform checking on the type arguments on the class declaration itself. - if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as ts.CallExpression).expression.kind !== ts.SyntaxKind.SuperKeyword) { - ts.forEach(typeArguments, checkSourceElement); - } + // We already perform checking on the type arguments on the class declaration itself. + if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as ts.CallExpression).expression.kind !== ts.SyntaxKind.SuperKeyword) { + ts.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, ts.Diagnostics.Call_target_does_not_contain_any_signatures)); - } - return resolveErrorCall(node); + const candidates = candidatesOutArray || []; + // reorderCandidates fills up the candidates array directly + reorderCandidates(signatures, candidates, callChainFlags); + if (!candidates.length) { + if (reportErrors) { + diagnostics.add(getDiagnosticForCallNode(node, ts.Diagnostics.Call_target_does_not_contain_any_signatures)); } + return resolveErrorCall(node); + } - const args = getEffectiveCallArguments(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 && ts.some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal; - argCheckMode |= checkMode & CheckMode.IsForStringLiteralArgumentCompletions; - - // 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 === ts.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, isSingleNonGenericCandidate, signatureHelpTrailingComma); - } - if (!result) { - result = chooseOverload(candidates, assignableRelation, isSingleNonGenericCandidate, signatureHelpTrailingComma); - } - if (result) { - return result; - } + // 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 && ts.some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal; + argCheckMode |= checkMode & CheckMode.IsForStringLiteralArgumentCompletions; + + // 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 === ts.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, isSingleNonGenericCandidate, signatureHelpTrailingComma); + } + if (!result) { + result = chooseOverload(candidates, assignableRelation, isSingleNonGenericCandidate, 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: ts.DiagnosticMessageChain | undefined; - if (candidatesForArgumentError.length > 3) { - chain = ts.chainDiagnosticMessages(chain, ts.Diagnostics.The_last_overload_gave_the_following_error); - chain = ts.chainDiagnosticMessages(chain, ts.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) { - ts.addRelatedInfo(d, ts.createDiagnosticForNode(last.declaration, ts.Diagnostics.The_last_overload_is_declared_here)); - } - addImplementationSuccessElaboration(last, d); - diagnostics.add(d); + // 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: ts.DiagnosticMessageChain | undefined; + if (candidatesForArgumentError.length > 3) { + chain = ts.chainDiagnosticMessages(chain, ts.Diagnostics.The_last_overload_gave_the_following_error); + chain = ts.chainDiagnosticMessages(chain, ts.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) { + ts.addRelatedInfo(d, ts.createDiagnosticForNode(last.declaration, ts.Diagnostics.The_last_overload_is_declared_here)); } - } - else { - ts.Debug.fail("No error for last overload signature"); + addImplementationSuccessElaboration(last, d); + diagnostics.add(d); } } else { - const allDiagnostics: (readonly ts.DiagnosticRelatedInformation[])[] = []; - let max = 0; - let min = Number.MAX_VALUE; - let minIndex = 0; - let i = 0; - for (const c of candidatesForArgumentError) { - const chain = () => ts.chainDiagnosticMessages(/*details*/ undefined, ts.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 { - ts.Debug.fail("No error for 3 or fewer overload signatures"); + ts.Debug.fail("No error for last overload signature"); + } + } + else { + const allDiagnostics: (readonly ts.DiagnosticRelatedInformation[])[] = []; + let max = 0; + let min = Number.MAX_VALUE; + let minIndex = 0; + let i = 0; + for (const c of candidatesForArgumentError) { + const chain = () => ts.chainDiagnosticMessages(/*details*/ undefined, ts.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; } - i++; - } - - const diags = max > 1 ? allDiagnostics[minIndex] : ts.flatten(allDiagnostics); - ts.Debug.assert(diags.length > 0, "No errors reported for 3 or fewer overload signatures"); - const chain = ts.chainDiagnosticMessages(ts.map(diags, ts.createDiagnosticMessageChainFromDiagnostic), ts.Diagnostics.No_overload_matches_this_call); - // The below is a spread to guarantee we get a new (mutable) array - our `flatMap` helper tries to do "smart" optimizations where it reuses input - // arrays and the emptyArray singleton where possible, which is decidedly not what we want while we're still constructing this diagnostic - const related = [...ts.flatMap(diags, d => (d as ts.Diagnostic).relatedInformation) as ts.DiagnosticRelatedInformation[]]; - let diag: ts.Diagnostic; - if (ts.every(diags, d => d.start === diags[0].start && d.length === diags[0].length && d.file === diags[0].file)) { - const { file, start, length } = diags[0]; - diag = { file, start, length, code: chain.code, category: chain.category, messageText: chain, relatedInformation: related }; + max = Math.max(max, diags.length); + allDiagnostics.push(diags); } else { - diag = ts.createDiagnosticForNodeFromMessageChain(node, chain, related); + ts.Debug.fail("No error for 3 or fewer overload signatures"); } - addImplementationSuccessElaboration(candidatesForArgumentError[0], diag); - diagnostics.add(diag); + i++; } + + const diags = max > 1 ? allDiagnostics[minIndex] : ts.flatten(allDiagnostics); + ts.Debug.assert(diags.length > 0, "No errors reported for 3 or fewer overload signatures"); + const chain = ts.chainDiagnosticMessages(ts.map(diags, ts.createDiagnosticMessageChainFromDiagnostic), ts.Diagnostics.No_overload_matches_this_call); + // The below is a spread to guarantee we get a new (mutable) array - our `flatMap` helper tries to do "smart" optimizations where it reuses input + // arrays and the emptyArray singleton where possible, which is decidedly not what we want while we're still constructing this diagnostic + const related = [...ts.flatMap(diags, d => (d as ts.Diagnostic).relatedInformation) as ts.DiagnosticRelatedInformation[]]; + let diag: ts.Diagnostic; + if (ts.every(diags, d => d.start === diags[0].start && d.length === diags[0].length && d.file === diags[0].file)) { + const { file, start, length } = diags[0]; + diag = { file, start, length, code: chain.code, category: chain.category, messageText: chain, relatedInformation: related }; + } + else { + diag = ts.createDiagnosticForNodeFromMessageChain(node, chain, related); + } + addImplementationSuccessElaboration(candidatesForArgumentError[0], diag); + diagnostics.add(diag); } - else if (candidateForArgumentArityError) { - diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args)); + } + else if (candidateForArgumentArityError) { + diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args)); + } + else if (candidateForTypeArgumentError) { + checkTypeArguments(candidateForTypeArgumentError, (node as ts.CallExpression | ts.TaggedTemplateExpression | ts.JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, fallbackError); + } + else { + const signaturesWithCorrectTypeArgumentArity = ts.filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments)); + if (signaturesWithCorrectTypeArgumentArity.length === 0) { + diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!)); } - else if (candidateForTypeArgumentError) { - checkTypeArguments(candidateForTypeArgumentError, (node as ts.CallExpression | ts.TaggedTemplateExpression | ts.JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, fallbackError); + else if (!isDecorator) { + diagnostics.add(getArgumentArityError(node, signaturesWithCorrectTypeArgumentArity, args)); } - else { - const signaturesWithCorrectTypeArgumentArity = ts.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)); - } + else if (fallbackError) { + diagnostics.add(getDiagnosticForCallNode(node, fallbackError)); } } + } - return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray, checkMode); + return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray, checkMode); - function addImplementationSuccessElaboration(failed: ts.Signature, diagnostic: ts.Diagnostic) { - const oldCandidatesForArgumentError = candidatesForArgumentError; - const oldCandidateForArgumentArityError = candidateForArgumentArityError; - const oldCandidateForTypeArgumentError = candidateForTypeArgumentError; + function addImplementationSuccessElaboration(failed: ts.Signature, diagnostic: ts.Diagnostic) { + const oldCandidatesForArgumentError = candidatesForArgumentError; + const oldCandidateForArgumentArityError = candidateForArgumentArityError; + const oldCandidateForTypeArgumentError = candidateForTypeArgumentError; - const failedSignatureDeclarations = failed.declaration?.symbol?.declarations || ts.emptyArray; - const isOverload = failedSignatureDeclarations.length > 1; - const implDecl = isOverload ? ts.find(failedSignatureDeclarations, d => ts.isFunctionLikeDeclaration(d) && ts.nodeIsPresent(d.body)) : undefined; - if (implDecl) { - const candidate = getSignatureFromDeclaration(implDecl as ts.FunctionLikeDeclaration); - const isSingleNonGenericCandidate = !candidate.typeParameters; - if (chooseOverload([candidate], assignableRelation, isSingleNonGenericCandidate)) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(implDecl, ts.Diagnostics.The_call_would_have_succeeded_against_this_implementation_but_implementation_signatures_of_overloads_are_not_externally_visible)); - } + const failedSignatureDeclarations = failed.declaration?.symbol?.declarations || ts.emptyArray; + const isOverload = failedSignatureDeclarations.length > 1; + const implDecl = isOverload ? ts.find(failedSignatureDeclarations, d => ts.isFunctionLikeDeclaration(d) && ts.nodeIsPresent(d.body)) : undefined; + if (implDecl) { + const candidate = getSignatureFromDeclaration(implDecl as ts.FunctionLikeDeclaration); + const isSingleNonGenericCandidate = !candidate.typeParameters; + if (chooseOverload([candidate], assignableRelation, isSingleNonGenericCandidate)) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(implDecl, ts.Diagnostics.The_call_would_have_succeeded_against_this_implementation_but_implementation_signatures_of_overloads_are_not_externally_visible)); } - - candidatesForArgumentError = oldCandidatesForArgumentError; - candidateForArgumentArityError = oldCandidateForArgumentArityError; - candidateForTypeArgumentError = oldCandidateForTypeArgumentError; } - function chooseOverload(candidates: ts.Signature[], relation: ts.ESMap, isSingleNonGenericCandidate: boolean, signatureHelpTrailingComma = false) { - candidatesForArgumentError = undefined; - candidateForArgumentArityError = undefined; - candidateForTypeArgumentError = undefined; + candidatesForArgumentError = oldCandidatesForArgumentError; + candidateForArgumentArityError = oldCandidateForArgumentArityError; + candidateForTypeArgumentError = oldCandidateForTypeArgumentError; + } - if (isSingleNonGenericCandidate) { - const candidate = candidates[0]; - if (ts.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; + function chooseOverload(candidates: ts.Signature[], relation: ts.ESMap, isSingleNonGenericCandidate: boolean, signatureHelpTrailingComma = false) { + candidatesForArgumentError = undefined; + candidateForArgumentArityError = undefined; + candidateForTypeArgumentError = undefined; + + if (isSingleNonGenericCandidate) { + const candidate = candidates[0]; + if (ts.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; + } - for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { - const candidate = candidates[candidateIndex]; - if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { - continue; - } + for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { + const candidate = candidates[candidateIndex]; + if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { + continue; + } - let checkCandidate: ts.Signature; - let inferenceContext: ts.InferenceContext | undefined; + let checkCandidate: ts.Signature; + let inferenceContext: ts.InferenceContext | undefined; - if (candidate.typeParameters) { - let typeArgumentTypes: ts.Type[] | undefined; - if (ts.some(typeArguments)) { - typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); - if (!typeArgumentTypes) { - candidateForTypeArgumentError = candidate; - continue; - } - } - else { - inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ ts.isInJSFile(node) ? ts.InferenceFlags.AnyDefault : ts.InferenceFlags.None); - typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext); - argCheckMode |= inferenceContext.flags & ts.InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal; + if (candidate.typeParameters) { + let typeArgumentTypes: ts.Type[] | undefined; + if (ts.some(typeArguments)) { + typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); + if (!typeArgumentTypes) { + candidateForTypeArgumentError = candidate; + continue; } + } + else { + inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ ts.isInJSFile(node) ? ts.InferenceFlags.AnyDefault : ts.InferenceFlags.None); + typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext); + argCheckMode |= inferenceContext.flags & ts.InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal; + } + checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, ts.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; + } + } + 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 & CheckMode.IsForStringLiteralArgumentCompletions; + if (inferenceContext) { + const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext); checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, ts.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. @@ -30095,14222 +30118,14199 @@ namespace ts { continue; } } - 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 & CheckMode.IsForStringLiteralArgumentCompletions; - if (inferenceContext) { - const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext); - checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, ts.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 (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; + candidates[candidateIndex] = checkCandidate; + return checkCandidate; } - } - // No signature was applicable. We have already reported the errors for the invalid signature. - function getCandidateForOverloadFailure(node: ts.CallLikeExpression, candidates: ts.Signature[], args: readonly ts.Expression[], hasCandidatesOutArray: boolean, checkMode: CheckMode): ts.Signature { - ts.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, checkMode) - : createUnionOfSignaturesForOverloadFailure(candidates); + return undefined; } + } - function createUnionOfSignaturesForOverloadFailure(candidates: readonly ts.Signature[]): ts.Signature { - const thisParameters = ts.mapDefined(candidates, c => c.thisParameter); - let thisParameter: ts.Symbol | undefined; - if (thisParameters.length) { - thisParameter = createCombinedSymbolFromTypes(thisParameters, thisParameters.map(getTypeOfParameter)); - } - const { min: minArgumentCount, max: maxNonRestParam } = ts.minAndMax(candidates, getNumNonRestParameters); - const parameters: ts.Symbol[] = []; - for (let i = 0; i < maxNonRestParam; i++) { - const symbols = ts.mapDefined(candidates, s => signatureHasRestParameter(s) ? - i < s.parameters.length - 1 ? s.parameters[i] : ts.last(s.parameters) : - i < s.parameters.length ? s.parameters[i] : undefined); - ts.Debug.assert(symbols.length !== 0); - parameters.push(createCombinedSymbolFromTypes(symbols, ts.mapDefined(candidates, candidate => tryGetTypeAtPosition(candidate, i)))); - } - const restParameterSymbols = ts.mapDefined(candidates, c => signatureHasRestParameter(c) ? ts.last(c.parameters) : undefined); - let flags = ts.SignatureFlags.None; - if (restParameterSymbols.length !== 0) { - const type = createArrayType(getUnionType(ts.mapDefined(candidates, tryGetRestTypeOfSignature), ts.UnionReduction.Subtype)); - parameters.push(createCombinedSymbolForOverloadFailure(restParameterSymbols, type)); - flags |= ts.SignatureFlags.HasRestParameter; - } - if (candidates.some(signatureHasLiteralTypes)) { - flags |= ts.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); - } + // No signature was applicable. We have already reported the errors for the invalid signature. + function getCandidateForOverloadFailure(node: ts.CallLikeExpression, candidates: ts.Signature[], args: readonly ts.Expression[], hasCandidatesOutArray: boolean, checkMode: CheckMode): ts.Signature { + ts.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, checkMode) + : createUnionOfSignaturesForOverloadFailure(candidates); + } - function getNumNonRestParameters(signature: ts.Signature): number { - const numParams = signature.parameters.length; - return signatureHasRestParameter(signature) ? numParams - 1 : numParams; - } + function createUnionOfSignaturesForOverloadFailure(candidates: readonly ts.Signature[]): ts.Signature { + const thisParameters = ts.mapDefined(candidates, c => c.thisParameter); + let thisParameter: ts.Symbol | undefined; + if (thisParameters.length) { + thisParameter = createCombinedSymbolFromTypes(thisParameters, thisParameters.map(getTypeOfParameter)); + } + const { min: minArgumentCount, max: maxNonRestParam } = ts.minAndMax(candidates, getNumNonRestParameters); + const parameters: ts.Symbol[] = []; + for (let i = 0; i < maxNonRestParam; i++) { + const symbols = ts.mapDefined(candidates, s => signatureHasRestParameter(s) ? + i < s.parameters.length - 1 ? s.parameters[i] : ts.last(s.parameters) : + i < s.parameters.length ? s.parameters[i] : undefined); + ts.Debug.assert(symbols.length !== 0); + parameters.push(createCombinedSymbolFromTypes(symbols, ts.mapDefined(candidates, candidate => tryGetTypeAtPosition(candidate, i)))); + } + const restParameterSymbols = ts.mapDefined(candidates, c => signatureHasRestParameter(c) ? ts.last(c.parameters) : undefined); + let flags = ts.SignatureFlags.None; + if (restParameterSymbols.length !== 0) { + const type = createArrayType(getUnionType(ts.mapDefined(candidates, tryGetRestTypeOfSignature), ts.UnionReduction.Subtype)); + parameters.push(createCombinedSymbolForOverloadFailure(restParameterSymbols, type)); + flags |= ts.SignatureFlags.HasRestParameter; + } + if (candidates.some(signatureHasLiteralTypes)) { + flags |= ts.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 createCombinedSymbolFromTypes(sources: readonly ts.Symbol[], types: ts.Type[]): ts.Symbol { - return createCombinedSymbolForOverloadFailure(sources, getUnionType(types, ts.UnionReduction.Subtype)); - } + function getNumNonRestParameters(signature: ts.Signature): number { + const numParams = signature.parameters.length; + return signatureHasRestParameter(signature) ? numParams - 1 : numParams; + } - 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(ts.first(sources), type); - } + function createCombinedSymbolFromTypes(sources: readonly ts.Symbol[], types: ts.Type[]): ts.Symbol { + return createCombinedSymbolForOverloadFailure(sources, getUnionType(types, ts.UnionReduction.Subtype)); + } - function pickLongestCandidateSignature(node: ts.CallLikeExpression, candidates: ts.Signature[], args: readonly ts.Expression[], checkMode: CheckMode): 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; - } + 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(ts.first(sources), type); + } - const typeArgumentNodes: readonly ts.TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined; - const instantiated = typeArgumentNodes - ? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, ts.isInJSFile(node))) - : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode); - candidates[bestIndex] = instantiated; - return instantiated; - } + function pickLongestCandidateSignature(node: ts.CallLikeExpression, candidates: ts.Signature[], args: readonly ts.Expression[], checkMode: CheckMode): 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 ts.TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined; + const instantiated = typeArgumentNodes + ? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, ts.isInJSFile(node))) + : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode); + candidates[bestIndex] = instantiated; + return instantiated; + } - function getTypeArgumentsFromNodes(typeArgumentNodes: readonly ts.TypeNode[], typeParameters: readonly ts.TypeParameter[], isJs: boolean): readonly ts.Type[] { - const typeArguments = typeArgumentNodes.map(getTypeOfNode); - while (typeArguments.length > typeParameters.length) { - typeArguments.pop(); - } - while (typeArguments.length < typeParameters.length) { - typeArguments.push(getDefaultFromTypeParameter(typeParameters[typeArguments.length]) || getConstraintOfTypeParameter(typeParameters[typeArguments.length]) || getDefaultTypeArgumentType(isJs)); - } - return typeArguments; + function getTypeArgumentsFromNodes(typeArgumentNodes: readonly ts.TypeNode[], typeParameters: readonly ts.TypeParameter[], isJs: boolean): readonly ts.Type[] { + const typeArguments = typeArgumentNodes.map(getTypeOfNode); + while (typeArguments.length > typeParameters.length) { + typeArguments.pop(); } - - function inferSignatureInstantiationForOverloadFailure(node: ts.CallLikeExpression, typeParameters: readonly ts.TypeParameter[], candidate: ts.Signature, args: readonly ts.Expression[], checkMode: CheckMode): ts.Signature { - const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ ts.isInJSFile(node) ? ts.InferenceFlags.AnyDefault : ts.InferenceFlags.None); - const typeArgumentTypes = inferTypeArguments(node, candidate, args, checkMode | CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); - return createSignatureInstantiation(candidate, typeArgumentTypes); + while (typeArguments.length < typeParameters.length) { + typeArguments.push(getDefaultFromTypeParameter(typeParameters[typeArguments.length]) || getConstraintOfTypeParameter(typeParameters[typeArguments.length]) || getDefaultTypeArgumentType(isJs)); } + return typeArguments; + } - function getLongestCandidateIndex(candidates: ts.Signature[], argsCount: number): number { - let maxParamsIndex = -1; - let maxParams = -1; + function inferSignatureInstantiationForOverloadFailure(node: ts.CallLikeExpression, typeParameters: readonly ts.TypeParameter[], candidate: ts.Signature, args: readonly ts.Expression[], checkMode: CheckMode): ts.Signature { + const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ ts.isInJSFile(node) ? ts.InferenceFlags.AnyDefault : ts.InferenceFlags.None); + const typeArgumentTypes = inferTypeArguments(node, candidate, args, checkMode | CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); + return createSignatureInstantiation(candidate, typeArgumentTypes); + } - 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; - } - } + function getLongestCandidateIndex(candidates: ts.Signature[], argsCount: number): number { + let maxParamsIndex = -1; + let maxParams = -1; - return maxParamsIndex; + 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; + } } - function resolveCallExpression(node: ts.CallExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { - if (node.expression.kind === ts.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 (!isErrorType(superType)) { - // 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 = ts.getEffectiveBaseTypeNode(ts.getContainingClass(node)!); - if (baseTypeNode) { - const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode); - return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, ts.SignatureFlags.None); - } - } - return resolveUntypedCall(node); - } + return maxParamsIndex; + } - let callChainFlags: ts.SignatureFlags; - let funcType = checkExpression(node.expression); - if (ts.isCallChain(node)) { - const nonOptionalType = getOptionalExpressionType(funcType, node.expression); - callChainFlags = nonOptionalType === funcType ? ts.SignatureFlags.None : - ts.isOutermostOptionalChain(node) ? ts.SignatureFlags.IsOuterCallChain : - ts.SignatureFlags.IsInnerCallChain; - funcType = nonOptionalType; + function resolveCallExpression(node: ts.CallExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + if (node.expression.kind === ts.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; } - else { - callChainFlags = ts.SignatureFlags.None; + if (!isErrorType(superType)) { + // 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 = ts.getEffectiveBaseTypeNode(ts.getContainingClass(node)!); + if (baseTypeNode) { + const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode); + return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, ts.SignatureFlags.None); + } } + return resolveUntypedCall(node); + } - funcType = checkNonNullTypeWithReporter(funcType, node.expression, reportCannotInvokePossiblyNullOrUndefinedError); + let callChainFlags: ts.SignatureFlags; + let funcType = checkExpression(node.expression); + if (ts.isCallChain(node)) { + const nonOptionalType = getOptionalExpressionType(funcType, node.expression); + callChainFlags = nonOptionalType === funcType ? ts.SignatureFlags.None : + ts.isOutermostOptionalChain(node) ? ts.SignatureFlags.IsOuterCallChain : + ts.SignatureFlags.IsInnerCallChain; + funcType = nonOptionalType; + } + else { + callChainFlags = ts.SignatureFlags.None; + } - if (funcType === silentNeverType) { - return silentNeverSignature; - } + funcType = checkNonNullTypeWithReporter(funcType, node.expression, reportCannotInvokePossiblyNullOrUndefinedError); - const apparentType = getApparentType(funcType); - if (isErrorType(apparentType)) { - // Another error has already been reported - return resolveErrorCall(node); - } + if (funcType === silentNeverType) { + return silentNeverSignature; + } - // 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, ts.SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Construct).length; + const apparentType = getApparentType(funcType); + if (isErrorType(apparentType)) { + // Another error has already been reported + return resolveErrorCall(node); + } - // 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 (!isErrorType(funcType) && node.typeArguments) { - error(node, ts.Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); - } - return resolveUntypedCall(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, ts.SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, ts.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 (!isErrorType(funcType) && node.typeArguments) { + error(node, ts.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, ts.Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); } - // 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, ts.Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); - } - else { - let relatedInformation: ts.DiagnosticRelatedInformation | undefined; - if (node.arguments.length === 1) { - const text = ts.getSourceFileOfNode(node).text; - if (ts.isLineBreak(text.charCodeAt(ts.skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) { - relatedInformation = ts.createDiagnosticForNode(node.expression, ts.Diagnostics.Are_you_missing_a_semicolon); - } + else { + let relatedInformation: ts.DiagnosticRelatedInformation | undefined; + if (node.arguments.length === 1) { + const text = ts.getSourceFileOfNode(node).text; + if (ts.isLineBreak(text.charCodeAt(ts.skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) { + relatedInformation = ts.createDiagnosticForNode(node.expression, ts.Diagnostics.Are_you_missing_a_semicolon); } - invocationError(node.expression, apparentType, ts.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 => ts.isInJSFile(sig.declaration) && !!ts.getJSDocClassTag(sig.declaration!))) { - error(node, ts.Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); - return resolveErrorCall(node); + invocationError(node.expression, apparentType, ts.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 => ts.isInJSFile(sig.declaration) && !!ts.getJSDocClassTag(sig.declaration!))) { + error(node, ts.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: 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 & ts.TypeFlags.TypeParameter) || + !numCallSignatures && !numConstructSignatures && !(apparentFuncType.flags & ts.TypeFlags.Union) && !(getReducedType(apparentFuncType).flags & ts.TypeFlags.Never) && isTypeAssignableTo(funcType, globalFunctionType); + } - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags); + function resolveNewExpression(node: ts.NewExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + if (node.arguments && languageVersion < ts.ScriptTarget.ES5) { + const spreadIndex = getSpreadArgumentIndex(node.arguments); + if (spreadIndex >= 0) { + error(node.arguments[spreadIndex], ts.Diagnostics.Spread_operator_in_new_expressions_is_only_available_when_targeting_ECMAScript_5_and_higher); + } } - function isGenericFunctionReturningFunction(signature: ts.Signature) { - return !!(signature.typeParameters && isFunctionType(getReturnTypeOfSignature(signature))); + let expressionType = checkNonNullExpression(node.expression); + if (expressionType === silentNeverType) { + return silentNeverSignature; } - /** - * 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 & ts.TypeFlags.TypeParameter) || - !numCallSignatures && !numConstructSignatures && !(apparentFuncType.flags & ts.TypeFlags.Union) && !(getReducedType(apparentFuncType).flags & ts.TypeFlags.Never) && isTypeAssignableTo(funcType, globalFunctionType); + // 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 (isErrorType(expressionType)) { + // Another error has already been reported + return resolveErrorCall(node); } - function resolveNewExpression(node: ts.NewExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { - if (node.arguments && languageVersion < ts.ScriptTarget.ES5) { - const spreadIndex = getSpreadArgumentIndex(node.arguments); - if (spreadIndex >= 0) { - error(node.arguments[spreadIndex], ts.Diagnostics.Spread_operator_in_new_expressions_is_only_available_when_targeting_ECMAScript_5_and_higher); - } + // 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, ts.Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); } + return resolveUntypedCall(node); + } - let expressionType = checkNonNullExpression(node.expression); - if (expressionType === silentNeverType) { - return silentNeverSignature; + // 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, ts.SignatureKind.Construct); + if (constructSignatures.length) { + if (!isConstructorAccessible(node, constructSignatures[0])) { + return resolveErrorCall(node); } - - // 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 (isErrorType(expressionType)) { - // Another error has already been reported + // If the expression is a class of abstract type, or an abstract construct signature, + // then it cannot be instantiated. + // In the case of a merged class-module or class-interface declaration, + // only the class declaration node will have the Abstract flag set. + if (someSignature(constructSignatures, signature => !!(signature.flags & ts.SignatureFlags.Abstract))) { + error(node, ts.Diagnostics.Cannot_create_an_instance_of_an_abstract_class); 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, ts.Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); - } - return resolveUntypedCall(node); + const valueDecl = expressionType.symbol && ts.getClassLikeDeclarationOfSymbol(expressionType.symbol); + if (valueDecl && ts.hasSyntacticModifier(valueDecl, ts.ModifierFlags.Abstract)) { + error(node, ts.Diagnostics.Cannot_create_an_instance_of_an_abstract_class); + return resolveErrorCall(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, ts.SignatureKind.Construct); - if (constructSignatures.length) { - if (!isConstructorAccessible(node, constructSignatures[0])) { - return resolveErrorCall(node); - } - // If the expression is a class of abstract type, or an abstract construct signature, - // then it cannot be instantiated. - // In the case of a merged class-module or class-interface declaration, - // only the class declaration node will have the Abstract flag set. - if (someSignature(constructSignatures, signature => !!(signature.flags & ts.SignatureFlags.Abstract))) { - error(node, ts.Diagnostics.Cannot_create_an_instance_of_an_abstract_class); - return resolveErrorCall(node); + return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, ts.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, ts.SignatureKind.Call); + if (callSignatures.length) { + const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); + if (!noImplicitAny) { + if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) { + error(node, ts.Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword); } - const valueDecl = expressionType.symbol && ts.getClassLikeDeclarationOfSymbol(expressionType.symbol); - if (valueDecl && ts.hasSyntacticModifier(valueDecl, ts.ModifierFlags.Abstract)) { - error(node, ts.Diagnostics.Cannot_create_an_instance_of_an_abstract_class); - return resolveErrorCall(node); + if (getThisTypeOfSignature(signature) === voidType) { + error(node, ts.Diagnostics.A_function_that_is_called_with_the_new_keyword_cannot_have_a_this_type_that_is_void); } - - return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); } + return signature; + } - // 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, ts.SignatureKind.Call); - if (callSignatures.length) { - const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); - if (!noImplicitAny) { - if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) { - error(node, ts.Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword); - } - if (getThisTypeOfSignature(signature) === voidType) { - error(node, ts.Diagnostics.A_function_that_is_called_with_the_new_keyword_cannot_have_a_this_type_that_is_void); - } - } - return signature; - } + invocationError(node.expression, expressionType, ts.SignatureKind.Construct); + return resolveErrorCall(node); + } - invocationError(node.expression, expressionType, ts.SignatureKind.Construct); - return resolveErrorCall(node); + function someSignature(signatures: ts.Signature | readonly ts.Signature[], f: (s: ts.Signature) => boolean): boolean { + if (ts.isArray(signatures)) { + return ts.some(signatures, signature => someSignature(signature, f)); } + return signatures.compositeKind === ts.TypeFlags.Union ? ts.some(signatures.compositeSignatures, f) : f(signatures); + } - function someSignature(signatures: ts.Signature | readonly ts.Signature[], f: (s: ts.Signature) => boolean): boolean { - if (ts.isArray(signatures)) { - return ts.some(signatures, signature => someSignature(signature, f)); - } - return signatures.compositeKind === ts.TypeFlags.Union ? ts.some(signatures.compositeSignatures, f) : f(signatures); + function typeHasProtectedAccessibleBase(target: ts.Symbol, type: ts.InterfaceType): boolean { + const baseTypes = getBaseTypes(type); + if (!ts.length(baseTypes)) { + return false; } - - function typeHasProtectedAccessibleBase(target: ts.Symbol, type: ts.InterfaceType): boolean { - const baseTypes = getBaseTypes(type); - if (!ts.length(baseTypes)) { - return false; - } - const firstBase = baseTypes[0]; - if (firstBase.flags & ts.TypeFlags.Intersection) { - const types = (firstBase as ts.IntersectionType).types; - const mixinFlags = findMixins(types); - let i = 0; - for (const intersectionMember of (firstBase as ts.IntersectionType).types) { - // We want to ignore mixin ctors - if (!mixinFlags[i]) { - if (ts.getObjectFlags(intersectionMember) & (ts.ObjectFlags.Class | ts.ObjectFlags.Interface)) { - if (intersectionMember.symbol === target) { - return true; - } - if (typeHasProtectedAccessibleBase(target, intersectionMember as ts.InterfaceType)) { - return true; - } + const firstBase = baseTypes[0]; + if (firstBase.flags & ts.TypeFlags.Intersection) { + const types = (firstBase as ts.IntersectionType).types; + const mixinFlags = findMixins(types); + let i = 0; + for (const intersectionMember of (firstBase as ts.IntersectionType).types) { + // We want to ignore mixin ctors + if (!mixinFlags[i]) { + if (ts.getObjectFlags(intersectionMember) & (ts.ObjectFlags.Class | ts.ObjectFlags.Interface)) { + if (intersectionMember.symbol === target) { + return true; + } + if (typeHasProtectedAccessibleBase(target, intersectionMember as ts.InterfaceType)) { + return true; } } - i++; } - return false; - } - if (firstBase.symbol === target) { - return true; + i++; } - return typeHasProtectedAccessibleBase(target, firstBase as ts.InterfaceType); + return false; } + if (firstBase.symbol === target) { + return true; + } + return typeHasProtectedAccessibleBase(target, firstBase as ts.InterfaceType); + } - function isConstructorAccessible(node: ts.NewExpression, signature: ts.Signature) { - if (!signature || !signature.declaration) { - return true; - } + function isConstructorAccessible(node: ts.NewExpression, signature: ts.Signature) { + if (!signature || !signature.declaration) { + return true; + } - const declaration = signature.declaration; - const modifiers = ts.getSelectedEffectiveModifierFlags(declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); + const declaration = signature.declaration; + const modifiers = ts.getSelectedEffectiveModifierFlags(declaration, ts.ModifierFlags.NonPublicAccessibilityModifier); - // (1) Public constructors and (2) constructor functions are always accessible. - if (!modifiers || declaration.kind !== ts.SyntaxKind.Constructor) { - return true; - } + // (1) Public constructors and (2) constructor functions are always accessible. + if (!modifiers || declaration.kind !== ts.SyntaxKind.Constructor) { + return true; + } - const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(declaration.parent.symbol)!; - const declaringClass = getDeclaredTypeOfSymbol(declaration.parent.symbol) as ts.InterfaceType; + const declaringClassDeclaration = ts.getClassLikeDeclarationOfSymbol(declaration.parent.symbol)!; + const declaringClass = getDeclaredTypeOfSymbol(declaration.parent.symbol) as ts.InterfaceType; - // A private or protected constructor can only be instantiated within its own class (or a subclass, for protected) - if (!isNodeWithinClass(node, declaringClassDeclaration)) { - const containingClass = ts.getContainingClass(node); - if (containingClass && modifiers & ts.ModifierFlags.Protected) { - const containingType = getTypeOfNode(containingClass); - if (typeHasProtectedAccessibleBase(declaration.parent.symbol, containingType as ts.InterfaceType)) { - return true; - } - } - if (modifiers & ts.ModifierFlags.Private) { - error(node, ts.Diagnostics.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); - } - if (modifiers & ts.ModifierFlags.Protected) { - error(node, ts.Diagnostics.Constructor_of_class_0_is_protected_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + // A private or protected constructor can only be instantiated within its own class (or a subclass, for protected) + if (!isNodeWithinClass(node, declaringClassDeclaration)) { + const containingClass = ts.getContainingClass(node); + if (containingClass && modifiers & ts.ModifierFlags.Protected) { + const containingType = getTypeOfNode(containingClass); + if (typeHasProtectedAccessibleBase(declaration.parent.symbol, containingType as ts.InterfaceType)) { + return true; } - return false; } - - return true; + if (modifiers & ts.ModifierFlags.Private) { + error(node, ts.Diagnostics.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + } + if (modifiers & ts.ModifierFlags.Protected) { + error(node, ts.Diagnostics.Constructor_of_class_0_is_protected_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + } + return false; } - function invocationErrorDetails(errorTarget: ts.Node, apparentType: ts.Type, kind: ts.SignatureKind): { - messageChain: ts.DiagnosticMessageChain; - relatedMessage: ts.DiagnosticMessage | undefined; - } { - let errorInfo: ts.DiagnosticMessageChain | undefined; - const isCall = kind === ts.SignatureKind.Call; - const awaitedType = getAwaitedType(apparentType); - const maybeMissingAwait = awaitedType && getSignaturesOfType(awaitedType, kind).length > 0; - if (apparentType.flags & ts.TypeFlags.Union) { - const types = (apparentType as ts.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; - } - } - else { - // Error on the first non callable constituent only - if (!errorInfo) { - errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? - ts.Diagnostics.Type_0_has_no_call_signatures : - ts.Diagnostics.Type_0_has_no_construct_signatures, typeToString(constituent)); - errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? - ts.Diagnostics.Not_all_constituents_of_type_0_are_callable : - ts.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; - } + return true; + } + + function invocationErrorDetails(errorTarget: ts.Node, apparentType: ts.Type, kind: ts.SignatureKind): { + messageChain: ts.DiagnosticMessageChain; + relatedMessage: ts.DiagnosticMessage | undefined; + } { + let errorInfo: ts.DiagnosticMessageChain | undefined; + const isCall = kind === ts.SignatureKind.Call; + const awaitedType = getAwaitedType(apparentType); + const maybeMissingAwait = awaitedType && getSignaturesOfType(awaitedType, kind).length > 0; + if (apparentType.flags & ts.TypeFlags.Union) { + const types = (apparentType as ts.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; } } - if (!hasSignatures) { - errorInfo = ts.chainDiagnosticMessages( - /* detials */ undefined, isCall ? - ts.Diagnostics.No_constituent_of_type_0_is_callable : - ts.Diagnostics.No_constituent_of_type_0_is_constructable, typeToString(apparentType)); - } - if (!errorInfo) { - errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? - ts.Diagnostics.Each_member_of_the_union_type_0_has_signatures_but_none_of_those_signatures_are_compatible_with_each_other : - ts.Diagnostics.Each_member_of_the_union_type_0_has_construct_signatures_but_none_of_those_signatures_are_compatible_with_each_other, typeToString(apparentType)); + else { + // Error on the first non callable constituent only + if (!errorInfo) { + errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? + ts.Diagnostics.Type_0_has_no_call_signatures : + ts.Diagnostics.Type_0_has_no_construct_signatures, typeToString(constituent)); + errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? + ts.Diagnostics.Not_all_constituents_of_type_0_are_callable : + ts.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; + } } } - else { + if (!hasSignatures) { + errorInfo = ts.chainDiagnosticMessages( + /* detials */ undefined, isCall ? + ts.Diagnostics.No_constituent_of_type_0_is_callable : + ts.Diagnostics.No_constituent_of_type_0_is_constructable, typeToString(apparentType)); + } + if (!errorInfo) { errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? - ts.Diagnostics.Type_0_has_no_call_signatures : - ts.Diagnostics.Type_0_has_no_construct_signatures, typeToString(apparentType)); + ts.Diagnostics.Each_member_of_the_union_type_0_has_signatures_but_none_of_those_signatures_are_compatible_with_each_other : + ts.Diagnostics.Each_member_of_the_union_type_0_has_construct_signatures_but_none_of_those_signatures_are_compatible_with_each_other, typeToString(apparentType)); } + } + else { + errorInfo = ts.chainDiagnosticMessages(errorInfo, isCall ? + ts.Diagnostics.Type_0_has_no_call_signatures : + ts.Diagnostics.Type_0_has_no_construct_signatures, typeToString(apparentType)); + } - let headMessage = isCall ? ts.Diagnostics.This_expression_is_not_callable : ts.Diagnostics.This_expression_is_not_constructable; + let headMessage = isCall ? ts.Diagnostics.This_expression_is_not_callable : ts.Diagnostics.This_expression_is_not_constructable; - // Diagnose get accessors incorrectly called as functions - if (ts.isCallExpression(errorTarget.parent) && errorTarget.parent.arguments.length === 0) { - const { resolvedSymbol } = getNodeLinks(errorTarget); - if (resolvedSymbol && resolvedSymbol.flags & ts.SymbolFlags.GetAccessor) { - headMessage = ts.Diagnostics.This_expression_is_not_callable_because_it_is_a_get_accessor_Did_you_mean_to_use_it_without; - } + // Diagnose get accessors incorrectly called as functions + if (ts.isCallExpression(errorTarget.parent) && errorTarget.parent.arguments.length === 0) { + const { resolvedSymbol } = getNodeLinks(errorTarget); + if (resolvedSymbol && resolvedSymbol.flags & ts.SymbolFlags.GetAccessor) { + headMessage = ts.Diagnostics.This_expression_is_not_callable_because_it_is_a_get_accessor_Did_you_mean_to_use_it_without; } + } - return { - messageChain: ts.chainDiagnosticMessages(errorInfo, headMessage), - relatedMessage: maybeMissingAwait ? ts.Diagnostics.Did_you_forget_to_use_await : undefined, - }; + return { + messageChain: ts.chainDiagnosticMessages(errorInfo, headMessage), + relatedMessage: maybeMissingAwait ? ts.Diagnostics.Did_you_forget_to_use_await : undefined, + }; + } + function invocationError(errorTarget: ts.Node, apparentType: ts.Type, kind: ts.SignatureKind, relatedInformation?: ts.DiagnosticRelatedInformation) { + const { messageChain, relatedMessage: relatedInfo } = invocationErrorDetails(errorTarget, apparentType, kind); + const diagnostic = ts.createDiagnosticForNodeFromMessageChain(errorTarget, messageChain); + if (relatedInfo) { + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(errorTarget, relatedInfo)); + } + if (ts.isCallExpression(errorTarget.parent)) { + const { start, length } = getDiagnosticSpanForCallNode(errorTarget.parent, /* doNotIncludeArguments */ true); + diagnostic.start = start; + diagnostic.length = length; + } + diagnostics.add(diagnostic); + invocationErrorRecovery(apparentType, kind, relatedInformation ? ts.addRelatedInfo(diagnostic, relatedInformation) : diagnostic); + } + + function invocationErrorRecovery(apparentType: ts.Type, kind: ts.SignatureKind, diagnostic: ts.Diagnostic) { + if (!apparentType.symbol) { + return; } - function invocationError(errorTarget: ts.Node, apparentType: ts.Type, kind: ts.SignatureKind, relatedInformation?: ts.DiagnosticRelatedInformation) { - const { messageChain, relatedMessage: relatedInfo } = invocationErrorDetails(errorTarget, apparentType, kind); - const diagnostic = ts.createDiagnosticForNodeFromMessageChain(errorTarget, messageChain); - if (relatedInfo) { - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(errorTarget, relatedInfo)); - } - if (ts.isCallExpression(errorTarget.parent)) { - const { start, length } = getDiagnosticSpanForCallNode(errorTarget.parent, /* doNotIncludeArguments */ true); - diagnostic.start = start; - diagnostic.length = length; - } - diagnostics.add(diagnostic); - invocationErrorRecovery(apparentType, kind, relatedInformation ? ts.addRelatedInfo(diagnostic, relatedInformation) : diagnostic); + 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 && !ts.isImportCall(importNode)) { + const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target!), kind); + if (!sigs || !sigs.length) + return; + ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(importNode, ts.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 invocationErrorRecovery(apparentType: ts.Type, kind: ts.SignatureKind, diagnostic: ts.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 && !ts.isImportCall(importNode)) { - const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target!), kind); - if (!sigs || !sigs.length) - return; - ts.addRelatedInfo(diagnostic, ts.createDiagnosticForNode(importNode, ts.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: ts.TaggedTemplateExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + const tagType = checkExpression(node.tag); + const apparentType = getApparentType(tagType); + + if (isErrorType(apparentType)) { + // Another error has already been reported + return resolveErrorCall(node); } - function resolveTaggedTemplateExpression(node: ts.TaggedTemplateExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { - const tagType = checkExpression(node.tag); - const apparentType = getApparentType(tagType); + const callSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Construct).length; + + if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, numConstructSignatures)) { + return resolveUntypedCall(node); + } - if (isErrorType(apparentType)) { - // Another error has already been reported + if (!callSignatures.length) { + if (ts.isArrayLiteralExpression(node.parent)) { + const diagnostic = ts.createDiagnosticForNode(node.tag, ts.Diagnostics.It_is_likely_that_you_are_missing_a_comma_to_separate_these_two_template_expressions_They_form_a_tagged_template_expression_which_cannot_be_invoked); + diagnostics.add(diagnostic); return resolveErrorCall(node); } - const callSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Construct).length; + invocationError(node.tag, apparentType, ts.SignatureKind.Call); + return resolveErrorCall(node); + } - if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, numConstructSignatures)) { - return resolveUntypedCall(node); - } + return resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); + } - if (!callSignatures.length) { - if (ts.isArrayLiteralExpression(node.parent)) { - const diagnostic = ts.createDiagnosticForNode(node.tag, ts.Diagnostics.It_is_likely_that_you_are_missing_a_comma_to_separate_these_two_template_expressions_They_form_a_tagged_template_expression_which_cannot_be_invoked); - diagnostics.add(diagnostic); - return resolveErrorCall(node); - } + /** + * Gets the localized diagnostic head message to use for errors when resolving a decorator as a call expression. + */ + function getDiagnosticHeadMessageForDecoratorResolution(node: ts.Decorator) { + switch (node.parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return ts.Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression; + case ts.SyntaxKind.Parameter: + return ts.Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression; + case ts.SyntaxKind.PropertyDeclaration: + return ts.Diagnostics.Unable_to_resolve_signature_of_property_decorator_when_called_as_an_expression; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return ts.Diagnostics.Unable_to_resolve_signature_of_method_decorator_when_called_as_an_expression; - invocationError(node.tag, apparentType, ts.SignatureKind.Call); - return resolveErrorCall(node); - } + default: + return ts.Debug.fail(); + } + } - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); + /** + * Resolves a decorator as if it were a call expression. + */ + function resolveDecorator(node: ts.Decorator, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + const funcType = checkExpression(node.expression); + const apparentType = getApparentType(funcType); + if (isErrorType(apparentType)) { + return resolveErrorCall(node); } - /** - * Gets the localized diagnostic head message to use for errors when resolving a decorator as a call expression. - */ - function getDiagnosticHeadMessageForDecoratorResolution(node: ts.Decorator) { - switch (node.parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return ts.Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression; - case ts.SyntaxKind.Parameter: - return ts.Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression; - case ts.SyntaxKind.PropertyDeclaration: - return ts.Diagnostics.Unable_to_resolve_signature_of_property_decorator_when_called_as_an_expression; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return ts.Diagnostics.Unable_to_resolve_signature_of_method_decorator_when_called_as_an_expression; + const callSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Construct).length; + if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { + return resolveUntypedCall(node); + } - default: - return ts.Debug.fail(); - } + if (isPotentiallyUncalledDecorator(node, callSignatures)) { + const nodeStr = ts.getTextOfNode(node.expression, /*includeTrivia*/ false); + error(node, ts.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); } - /** - * Resolves a decorator as if it were a call expression. - */ - function resolveDecorator(node: ts.Decorator, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { - const funcType = checkExpression(node.expression); - const apparentType = getApparentType(funcType); - if (isErrorType(apparentType)) { - return resolveErrorCall(node); + const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); + if (!callSignatures.length) { + const errorDetails = invocationErrorDetails(node.expression, apparentType, ts.SignatureKind.Call); + const messageChain = ts.chainDiagnosticMessages(errorDetails.messageChain, headMessage); + const diag = ts.createDiagnosticForNodeFromMessageChain(node.expression, messageChain); + if (errorDetails.relatedMessage) { + ts.addRelatedInfo(diag, ts.createDiagnosticForNode(node.expression, errorDetails.relatedMessage)); } + diagnostics.add(diag); + invocationErrorRecovery(apparentType, ts.SignatureKind.Call, diag); + return resolveErrorCall(node); + } - const callSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, ts.SignatureKind.Construct).length; - if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { - return resolveUntypedCall(node); - } + return resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None, headMessage); + } - if (isPotentiallyUncalledDecorator(node, callSignatures)) { - const nodeStr = ts.getTextOfNode(node.expression, /*includeTrivia*/ false); - error(node, ts.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); - } + function createSignatureForJSXIntrinsic(node: ts.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, ts.SymbolFlags.Type); + const returnNode = typeSymbol && nodeBuilder.symbolToEntityName(typeSymbol, ts.SymbolFlags.Type, node); + const declaration = ts.factory.createFunctionTypeNode(/*typeParameters*/ undefined, [ts.factory.createParameterDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*dotdotdot*/ undefined, "props", /*questionMark*/ undefined, nodeBuilder.typeToTypeNode(result, node))], returnNode ? ts.factory.createTypeReferenceNode(returnNode, /*typeArguments*/ undefined) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + const parameterSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "props" as ts.__String); + parameterSymbol.type = result; + return createSignature(declaration, + /*typeParameters*/ undefined, + /*thisParameter*/ undefined, [parameterSymbol], typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType, + /*returnTypePredicate*/ undefined, 1, ts.SignatureFlags.None); + } - const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); - if (!callSignatures.length) { - const errorDetails = invocationErrorDetails(node.expression, apparentType, ts.SignatureKind.Call); - const messageChain = ts.chainDiagnosticMessages(errorDetails.messageChain, headMessage); - const diag = ts.createDiagnosticForNodeFromMessageChain(node.expression, messageChain); - if (errorDetails.relatedMessage) { - ts.addRelatedInfo(diag, ts.createDiagnosticForNode(node.expression, errorDetails.relatedMessage)); - } - diagnostics.add(diag); - invocationErrorRecovery(apparentType, ts.SignatureKind.Call, diag); - return resolveErrorCall(node); + function resolveJsxOpeningLikeElement(node: ts.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); + if (ts.length(node.typeArguments)) { + ts.forEach(node.typeArguments, checkSourceElement); + diagnostics.add(ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), node.typeArguments!, ts.Diagnostics.Expected_0_type_arguments_but_got_1, 0, ts.length(node.typeArguments))); } + return fakeSignature; + } + const exprTypes = checkExpression(node.tagName); + const apparentType = getApparentType(exprTypes); + if (isErrorType(apparentType)) { + return resolveErrorCall(node); + } - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, ts.SignatureFlags.None, headMessage); + const signatures = getUninstantiatedJsxSignaturesOfType(exprTypes, node); + if (isUntypedFunctionCall(exprTypes, apparentType, signatures.length, /*constructSignatures*/ 0)) { + return resolveUntypedCall(node); } - function createSignatureForJSXIntrinsic(node: ts.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, ts.SymbolFlags.Type); - const returnNode = typeSymbol && nodeBuilder.symbolToEntityName(typeSymbol, ts.SymbolFlags.Type, node); - const declaration = ts.factory.createFunctionTypeNode(/*typeParameters*/ undefined, [ts.factory.createParameterDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*dotdotdot*/ undefined, "props", /*questionMark*/ undefined, nodeBuilder.typeToTypeNode(result, node))], returnNode ? ts.factory.createTypeReferenceNode(returnNode, /*typeArguments*/ undefined) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); - const parameterSymbol = createSymbol(ts.SymbolFlags.FunctionScopedVariable, "props" as ts.__String); - parameterSymbol.type = result; - return createSignature(declaration, - /*typeParameters*/ undefined, - /*thisParameter*/ undefined, [parameterSymbol], typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType, - /*returnTypePredicate*/ undefined, 1, ts.SignatureFlags.None); + if (signatures.length === 0) { + // We found no signatures at all, which is an error + error(node.tagName, ts.Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, ts.getTextOfNode(node.tagName)); + return resolveErrorCall(node); } - function resolveJsxOpeningLikeElement(node: ts.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); - if (ts.length(node.typeArguments)) { - ts.forEach(node.typeArguments, checkSourceElement); - diagnostics.add(ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), node.typeArguments!, ts.Diagnostics.Expected_0_type_arguments_but_got_1, 0, ts.length(node.typeArguments))); - } - return fakeSignature; - } - const exprTypes = checkExpression(node.tagName); - const apparentType = getApparentType(exprTypes); - if (isErrorType(apparentType)) { - return resolveErrorCall(node); - } + return resolveCall(node, signatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); + } - const signatures = getUninstantiatedJsxSignaturesOfType(exprTypes, node); - if (isUntypedFunctionCall(exprTypes, apparentType, signatures.length, /*constructSignatures*/ 0)) { - return resolveUntypedCall(node); - } + /** + * 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: ts.Decorator, signatures: readonly ts.Signature[]) { + return signatures.length && ts.every(signatures, signature => signature.minArgumentCount === 0 && + !signatureHasRestParameter(signature) && + signature.parameters.length < getDecoratorArgumentCount(decorator, signature)); + } - if (signatures.length === 0) { - // We found no signatures at all, which is an error - error(node.tagName, ts.Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, ts.getTextOfNode(node.tagName)); - return resolveErrorCall(node); - } + function resolveSignature(node: ts.CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + switch (node.kind) { + case ts.SyntaxKind.CallExpression: + return resolveCallExpression(node, candidatesOutArray, checkMode); + case ts.SyntaxKind.NewExpression: + return resolveNewExpression(node, candidatesOutArray, checkMode); + case ts.SyntaxKind.TaggedTemplateExpression: + return resolveTaggedTemplateExpression(node, candidatesOutArray, checkMode); + case ts.SyntaxKind.Decorator: + return resolveDecorator(node, candidatesOutArray, checkMode); + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxSelfClosingElement: + return resolveJsxOpeningLikeElement(node, candidatesOutArray, checkMode); + } + throw ts.Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable."); + } - return resolveCall(node, signatures, candidatesOutArray, checkMode, ts.SignatureFlags.None); - } + /** + * 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: ts.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; + } - /** - * 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: ts.Decorator, signatures: readonly ts.Signature[]) { - return signatures.length && ts.every(signatures, signature => signature.minArgumentCount === 0 && - !signatureHasRestParameter(signature) && - signature.parameters.length < getDecoratorArgumentCount(decorator, signature)); + /** + * Indicates whether a declaration can be treated as a constructor in a JavaScript + * file. + */ + function isJSConstructor(node: ts.Node | undefined): node is ts.FunctionDeclaration | ts.FunctionExpression { + if (!node || !ts.isInJSFile(node)) { + return false; } + const func = ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ? node : + ts.isVariableDeclaration(node) && node.initializer && ts.isFunctionExpression(node.initializer) ? node.initializer : + undefined; + if (func) { + // If the node has a @class tag, treat it like a constructor. + if (ts.getJSDocClassTag(node)) + return true; - function resolveSignature(node: ts.CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { - switch (node.kind) { - case ts.SyntaxKind.CallExpression: - return resolveCallExpression(node, candidatesOutArray, checkMode); - case ts.SyntaxKind.NewExpression: - return resolveNewExpression(node, candidatesOutArray, checkMode); - case ts.SyntaxKind.TaggedTemplateExpression: - return resolveTaggedTemplateExpression(node, candidatesOutArray, checkMode); - case ts.SyntaxKind.Decorator: - return resolveDecorator(node, candidatesOutArray, checkMode); - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxSelfClosingElement: - return resolveJsxOpeningLikeElement(node, candidatesOutArray, checkMode); - } - throw ts.Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable."); + // If the symbol of the node has members, treat it like a constructor. + const symbol = getSymbolOfNode(func); + return !!symbol?.members?.size; } + return false; + } - /** - * 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: ts.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; + 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 = ts.isTransientSymbol(target) ? target : cloneSymbol(target) as ts.TransientSymbol; + inferred.exports = inferred.exports || ts.createSymbolTable(); + inferred.members = inferred.members || ts.createSymbolTable(); + inferred.flags |= source.flags & ts.SymbolFlags.Class; + if (source.exports?.size) { + mergeSymbolTable(inferred.exports, source.exports); + } + if (source.members?.size) { + mergeSymbolTable(inferred.members, source.members); + } + (links.inferredClassSymbol || (links.inferredClassSymbol = new ts.Map())).set(getSymbolId(inferred), inferred); + return inferred; } - return result; + return links.inferredClassSymbol.get(getSymbolId(target)); } + } - /** - * Indicates whether a declaration can be treated as a constructor in a JavaScript - * file. - */ - function isJSConstructor(node: ts.Node | undefined): node is ts.FunctionDeclaration | ts.FunctionExpression { - if (!node || !ts.isInJSFile(node)) { - return false; - } - const func = ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ? node : - ts.isVariableDeclaration(node) && node.initializer && ts.isFunctionExpression(node.initializer) ? node.initializer : - undefined; - if (func) { - // If the node has a @class tag, treat it like a constructor. - if (ts.getJSDocClassTag(node)) - return true; + function getAssignedClassSymbol(decl: ts.Declaration): ts.Symbol | undefined { + const assignmentSymbol = decl && getSymbolOfExpando(decl, /*allowDeclaration*/ true); + const prototype = assignmentSymbol?.exports?.get("prototype" as ts.__String); + const init = prototype?.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); + return init ? getSymbolOfNode(init) : undefined; + } - // If the symbol of the node has members, treat it like a constructor. - const symbol = getSymbolOfNode(func); - return !!symbol?.members?.size; - } - return false; + function getSymbolOfExpando(node: ts.Node, allowDeclaration: boolean): ts.Symbol | undefined { + if (!node.parent) { + return undefined; } - - 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 = ts.isTransientSymbol(target) ? target : cloneSymbol(target) as ts.TransientSymbol; - inferred.exports = inferred.exports || ts.createSymbolTable(); - inferred.members = inferred.members || ts.createSymbolTable(); - inferred.flags |= source.flags & ts.SymbolFlags.Class; - if (source.exports?.size) { - mergeSymbolTable(inferred.exports, source.exports); - } - if (source.members?.size) { - mergeSymbolTable(inferred.members, source.members); - } - (links.inferredClassSymbol || (links.inferredClassSymbol = new ts.Map())).set(getSymbolId(inferred), inferred); - return inferred; - } - return links.inferredClassSymbol.get(getSymbolId(target)); + let name: ts.Expression | ts.BindingName | undefined; + let decl: ts.Node | undefined; + if (ts.isVariableDeclaration(node.parent) && node.parent.initializer === node) { + if (!ts.isInJSFile(node) && !(ts.isVarConst(node.parent) && ts.isFunctionLikeDeclaration(node))) { + return undefined; } + name = node.parent.name; + decl = node.parent; } - - function getAssignedClassSymbol(decl: ts.Declaration): ts.Symbol | undefined { - const assignmentSymbol = decl && getSymbolOfExpando(decl, /*allowDeclaration*/ true); - const prototype = assignmentSymbol?.exports?.get("prototype" as ts.__String); - const init = prototype?.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); - return init ? getSymbolOfNode(init) : undefined; - } - - function getSymbolOfExpando(node: ts.Node, allowDeclaration: boolean): ts.Symbol | undefined { - if (!node.parent) { - return undefined; + else if (ts.isBinaryExpression(node.parent)) { + const parentNode = node.parent; + const parentNodeOperator = node.parent.operatorToken.kind; + if (parentNodeOperator === ts.SyntaxKind.EqualsToken && (allowDeclaration || parentNode.right === node)) { + name = parentNode.left; + decl = name; } - let name: ts.Expression | ts.BindingName | undefined; - let decl: ts.Node | undefined; - if (ts.isVariableDeclaration(node.parent) && node.parent.initializer === node) { - if (!ts.isInJSFile(node) && !(ts.isVarConst(node.parent) && ts.isFunctionLikeDeclaration(node))) { - return undefined; + else if (parentNodeOperator === ts.SyntaxKind.BarBarToken || parentNodeOperator === ts.SyntaxKind.QuestionQuestionToken) { + if (ts.isVariableDeclaration(parentNode.parent) && parentNode.parent.initializer === parentNode) { + name = parentNode.parent.name; + decl = parentNode.parent; } - name = node.parent.name; - decl = node.parent; - } - else if (ts.isBinaryExpression(node.parent)) { - const parentNode = node.parent; - const parentNodeOperator = node.parent.operatorToken.kind; - if (parentNodeOperator === ts.SyntaxKind.EqualsToken && (allowDeclaration || parentNode.right === node)) { - name = parentNode.left; + else if (ts.isBinaryExpression(parentNode.parent) && parentNode.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && (allowDeclaration || parentNode.parent.right === parentNode)) { + name = parentNode.parent.left; decl = name; } - else if (parentNodeOperator === ts.SyntaxKind.BarBarToken || parentNodeOperator === ts.SyntaxKind.QuestionQuestionToken) { - if (ts.isVariableDeclaration(parentNode.parent) && parentNode.parent.initializer === parentNode) { - name = parentNode.parent.name; - decl = parentNode.parent; - } - else if (ts.isBinaryExpression(parentNode.parent) && parentNode.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && (allowDeclaration || parentNode.parent.right === parentNode)) { - name = parentNode.parent.left; - decl = name; - } - if (!name || !ts.isBindableStaticNameExpression(name) || !ts.isSameEntityName(name, parentNode.left)) { - return undefined; - } + if (!name || !ts.isBindableStaticNameExpression(name) || !ts.isSameEntityName(name, parentNode.left)) { + return undefined; } } - else if (allowDeclaration && ts.isFunctionDeclaration(node)) { - name = node.name; - decl = node; - } - - if (!decl || !name || (!allowDeclaration && !ts.getExpandoInitializer(node, ts.isPrototypeAccess(name)))) { - return undefined; - } - return getSymbolOfNode(decl); } - - - function getAssignedJSPrototype(node: ts.Node) { - if (!node.parent) { - return false; - } - let parent: ts.Node = node.parent; - while (parent && parent.kind === ts.SyntaxKind.PropertyAccessExpression) { - parent = parent.parent; - } - if (parent && ts.isBinaryExpression(parent) && ts.isPrototypeAccess(parent.left) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - const right = ts.getInitializerOfBinaryExpression(parent); - return ts.isObjectLiteralExpression(right) && right; - } + else if (allowDeclaration && ts.isFunctionDeclaration(node)) { + name = node.name; + decl = node; } - /** - * 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: ts.CallExpression | ts.NewExpression, checkMode?: CheckMode): ts.Type { - checkGrammarTypeArguments(node, node.typeArguments); - - 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; - } - - checkDeprecatedSignature(signature, node); + if (!decl || !name || (!allowDeclaration && !ts.getExpandoInitializer(node, ts.isPrototypeAccess(name)))) { + return undefined; + } + return getSymbolOfNode(decl); + } - if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { - return voidType; - } - if (node.kind === ts.SyntaxKind.NewExpression) { - const declaration = signature.declaration; + function getAssignedJSPrototype(node: ts.Node) { + if (!node.parent) { + return false; + } + let parent: ts.Node = node.parent; + while (parent && parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + parent = parent.parent; + } + if (parent && ts.isBinaryExpression(parent) && ts.isPrototypeAccess(parent.left) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const right = ts.getInitializerOfBinaryExpression(parent); + return ts.isObjectLiteralExpression(right) && right; + } + } - if (declaration && - declaration.kind !== ts.SyntaxKind.Constructor && - declaration.kind !== ts.SyntaxKind.ConstructSignature && - declaration.kind !== ts.SyntaxKind.ConstructorType && - !ts.isJSDocConstructSignature(declaration) && - !isJSConstructor(declaration)) { + /** + * 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: ts.CallExpression | ts.NewExpression, checkMode?: CheckMode): ts.Type { + checkGrammarTypeArguments(node, node.typeArguments); - // When resolved signature is a call signature (and not a construct signature) the result type is any - if (noImplicitAny) { - error(node, ts.Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); - } - return anyType; - } - } + 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; + } - // In JavaScript files, calls to any identifier 'require' are treated as external module imports - if (ts.isInJSFile(node) && isCommonJsRequire(node)) { - return resolveExternalModuleTypeByLiteral(node.arguments![0] as ts.StringLiteral); - } + checkDeprecatedSignature(signature, node); - 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 & ts.TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) { - return getESSymbolLikeTypeForNode(ts.walkUpParenthesizedExpressions(node.parent)); - } - if (node.kind === ts.SyntaxKind.CallExpression && !node.questionDotToken && node.parent.kind === ts.SyntaxKind.ExpressionStatement && - returnType.flags & ts.TypeFlags.Void && getTypePredicateOfSignature(signature)) { - if (!ts.isDottedName(node.expression)) { - error(node.expression, ts.Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name); - } - else if (!getEffectsSignature(node)) { - const diagnostic = error(node.expression, ts.Diagnostics.Assertions_require_every_name_in_the_call_target_to_be_declared_with_an_explicit_type_annotation); - getTypeOfDottedName(node.expression, diagnostic); - } - } + if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { + return voidType; + } - if (ts.isInJSFile(node)) { - const jsSymbol = getSymbolOfExpando(node, /*allowDeclaration*/ false); - if (jsSymbol?.exports?.size) { - const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, ts.emptyArray, ts.emptyArray, ts.emptyArray); - jsAssignmentType.objectFlags |= ts.ObjectFlags.JSLiteral; - return getIntersectionType([returnType, jsAssignmentType]); - } - } + if (node.kind === ts.SyntaxKind.NewExpression) { + const declaration = signature.declaration; - return returnType; - } + if (declaration && + declaration.kind !== ts.SyntaxKind.Constructor && + declaration.kind !== ts.SyntaxKind.ConstructSignature && + declaration.kind !== ts.SyntaxKind.ConstructorType && + !ts.isJSDocConstructSignature(declaration) && + !isJSConstructor(declaration)) { - function checkDeprecatedSignature(signature: ts.Signature, node: ts.CallLikeExpression) { - if (signature.declaration && signature.declaration.flags & ts.NodeFlags.Deprecated) { - const suggestionNode = getDeprecatedSuggestionNode(node); - const name = ts.tryGetPropertyAccessOrIdentifierToString(ts.getInvokedExpression(node)); - addDeprecatedSuggestionWithSignature(suggestionNode, signature.declaration, name, signatureToString(signature)); + // When resolved signature is a call signature (and not a construct signature) the result type is any + if (noImplicitAny) { + error(node, ts.Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); + } + return anyType; } } - function getDeprecatedSuggestionNode(node: ts.Node): ts.Node { - node = ts.skipParentheses(node); - switch (node.kind) { - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.Decorator: - case ts.SyntaxKind.NewExpression: - return getDeprecatedSuggestionNode((node as ts.Decorator | ts.CallExpression | ts.NewExpression).expression); - case ts.SyntaxKind.TaggedTemplateExpression: - return getDeprecatedSuggestionNode((node as ts.TaggedTemplateExpression).tag); - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxSelfClosingElement: - return getDeprecatedSuggestionNode((node as ts.JsxOpeningLikeElement).tagName); - case ts.SyntaxKind.ElementAccessExpression: - return (node as ts.ElementAccessExpression).argumentExpression; - case ts.SyntaxKind.PropertyAccessExpression: - return (node as ts.PropertyAccessExpression).name; - case ts.SyntaxKind.TypeReference: - const typeReference = node as ts.TypeReferenceNode; - return ts.isQualifiedName(typeReference.typeName) ? typeReference.typeName.right : typeReference; - default: - return node; - } + // In JavaScript files, calls to any identifier 'require' are treated as external module imports + if (ts.isInJSFile(node) && isCommonJsRequire(node)) { + return resolveExternalModuleTypeByLiteral(node.arguments![0] as ts.StringLiteral); } - function isSymbolOrSymbolForCall(node: ts.Node) { - if (!ts.isCallExpression(node)) - return false; - let left = node.expression; - if (ts.isPropertyAccessExpression(left) && left.name.escapedText === "for") { - left = left.expression; + 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 & ts.TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) { + return getESSymbolLikeTypeForNode(ts.walkUpParenthesizedExpressions(node.parent)); + } + if (node.kind === ts.SyntaxKind.CallExpression && !node.questionDotToken && node.parent.kind === ts.SyntaxKind.ExpressionStatement && + returnType.flags & ts.TypeFlags.Void && getTypePredicateOfSignature(signature)) { + if (!ts.isDottedName(node.expression)) { + error(node.expression, ts.Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name); } - if (!ts.isIdentifier(left) || left.escapedText !== "Symbol") { - return false; + else if (!getEffectsSignature(node)) { + const diagnostic = error(node.expression, ts.Diagnostics.Assertions_require_every_name_in_the_call_target_to_be_declared_with_an_explicit_type_annotation); + getTypeOfDottedName(node.expression, diagnostic); } + } - // make sure `Symbol` is the global symbol - const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); - if (!globalESSymbol) { - return false; + if (ts.isInJSFile(node)) { + const jsSymbol = getSymbolOfExpando(node, /*allowDeclaration*/ false); + if (jsSymbol?.exports?.size) { + const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, ts.emptyArray, ts.emptyArray, ts.emptyArray); + jsAssignmentType.objectFlags |= ts.ObjectFlags.JSLiteral; + return getIntersectionType([returnType, jsAssignmentType]); } + } - return globalESSymbol === resolveName(left, "Symbol" as ts.__String, ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + return returnType; + } + + function checkDeprecatedSignature(signature: ts.Signature, node: ts.CallLikeExpression) { + if (signature.declaration && signature.declaration.flags & ts.NodeFlags.Deprecated) { + const suggestionNode = getDeprecatedSuggestionNode(node); + const name = ts.tryGetPropertyAccessOrIdentifierToString(ts.getInvokedExpression(node)); + addDeprecatedSuggestionWithSignature(suggestionNode, signature.declaration, name, signatureToString(signature)); } + } - function checkImportCallExpression(node: ts.ImportCall): ts.Type { - // Check grammar of dynamic import - checkGrammarImportCallExpression(node); + function getDeprecatedSuggestionNode(node: ts.Node): ts.Node { + node = ts.skipParentheses(node); + switch (node.kind) { + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.Decorator: + case ts.SyntaxKind.NewExpression: + return getDeprecatedSuggestionNode((node as ts.Decorator | ts.CallExpression | ts.NewExpression).expression); + case ts.SyntaxKind.TaggedTemplateExpression: + return getDeprecatedSuggestionNode((node as ts.TaggedTemplateExpression).tag); + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxSelfClosingElement: + return getDeprecatedSuggestionNode((node as ts.JsxOpeningLikeElement).tagName); + case ts.SyntaxKind.ElementAccessExpression: + return (node as ts.ElementAccessExpression).argumentExpression; + case ts.SyntaxKind.PropertyAccessExpression: + return (node as ts.PropertyAccessExpression).name; + case ts.SyntaxKind.TypeReference: + const typeReference = node as ts.TypeReferenceNode; + return ts.isQualifiedName(typeReference.typeName) ? typeReference.typeName.right : typeReference; + default: + return node; + } + } - if (node.arguments.length === 0) { - return createPromiseReturnType(node, anyType); - } + function isSymbolOrSymbolForCall(node: ts.Node) { + if (!ts.isCallExpression(node)) + return false; + let left = node.expression; + if (ts.isPropertyAccessExpression(left) && left.name.escapedText === "for") { + left = left.expression; + } + if (!ts.isIdentifier(left) || left.escapedText !== "Symbol") { + return false; + } - const specifier = node.arguments[0]; - const specifierType = checkExpressionCached(specifier); - const optionsType = node.arguments.length > 1 ? checkExpressionCached(node.arguments[1]) : undefined; - // Even though multiple arguments is grammatically incorrect, type-check extra arguments for completion - for (let i = 2; i < node.arguments.length; ++i) { - checkExpressionCached(node.arguments[i]); - } + // make sure `Symbol` is the global symbol + const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); + if (!globalESSymbol) { + return false; + } - if (specifierType.flags & ts.TypeFlags.Undefined || specifierType.flags & ts.TypeFlags.Null || !isTypeAssignableTo(specifierType, stringType)) { - error(specifier, ts.Diagnostics.Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0, typeToString(specifierType)); - } + return globalESSymbol === resolveName(left, "Symbol" as ts.__String, ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + } - if (optionsType) { - const importCallOptionsType = getGlobalImportCallOptionsType(/*reportErrors*/ true); - if (importCallOptionsType !== emptyObjectType) { - checkTypeAssignableTo(optionsType, getNullableType(importCallOptionsType, ts.TypeFlags.Undefined), node.arguments[1]); - } - } + function checkImportCallExpression(node: ts.ImportCall): ts.Type { + // Check grammar of dynamic import + checkGrammarImportCallExpression(node); - // 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, getTypeWithSyntheticDefaultOnly(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier) || - getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier)); - } - } + if (node.arguments.length === 0) { return createPromiseReturnType(node, anyType); } - function createDefaultPropertyWrapperForModule(symbol: ts.Symbol, originalSymbol: ts.Symbol, anonymousSymbol?: ts.Symbol | undefined) { - const memberTable = ts.createSymbolTable(); - const newSymbol = createSymbol(ts.SymbolFlags.Alias, ts.InternalSymbolName.Default); - newSymbol.parent = originalSymbol; - newSymbol.nameType = getStringLiteralType("default"); - newSymbol.aliasTarget = resolveSymbol(symbol); - memberTable.set(ts.InternalSymbolName.Default, newSymbol); - return createAnonymousType(anonymousSymbol, memberTable, ts.emptyArray, ts.emptyArray, ts.emptyArray); + const specifier = node.arguments[0]; + const specifierType = checkExpressionCached(specifier); + const optionsType = node.arguments.length > 1 ? checkExpressionCached(node.arguments[1]) : undefined; + // Even though multiple arguments is grammatically incorrect, type-check extra arguments for completion + for (let i = 2; i < node.arguments.length; ++i) { + checkExpressionCached(node.arguments[i]); } - function getTypeWithSyntheticDefaultOnly(type: ts.Type, symbol: ts.Symbol, originalSymbol: ts.Symbol, moduleSpecifier: ts.Expression) { - const hasDefaultOnly = isOnlyImportedAsDefault(moduleSpecifier); - if (hasDefaultOnly && type && !isErrorType(type)) { - const synthType = type as ts.SyntheticDefaultModuleType; - if (!synthType.defaultOnlyType) { - const type = createDefaultPropertyWrapperForModule(symbol, originalSymbol); - synthType.defaultOnlyType = type; - } - return synthType.defaultOnlyType; - } - return undefined; + if (specifierType.flags & ts.TypeFlags.Undefined || specifierType.flags & ts.TypeFlags.Null || !isTypeAssignableTo(specifierType, stringType)) { + error(specifier, ts.Diagnostics.Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0, typeToString(specifierType)); } - function getTypeWithSyntheticDefaultImportType(type: ts.Type, symbol: ts.Symbol, originalSymbol: ts.Symbol, moduleSpecifier: ts.Expression): ts.Type { - if (allowSyntheticDefaultImports && type && !isErrorType(type)) { - const synthType = type as ts.SyntheticDefaultModuleType; - if (!synthType.syntheticType) { - const file = originalSymbol.declarations?.find(ts.isSourceFile); - const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false, moduleSpecifier); - if (hasSyntheticDefault) { - const anonymousSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); - const defaultContainingObject = createDefaultPropertyWrapperForModule(symbol, originalSymbol, anonymousSymbol); - anonymousSymbol.type = defaultContainingObject; - synthType.syntheticType = isValidSpreadType(type) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*objectFlags*/ 0, /*readonly*/ false) : defaultContainingObject; - } - else { - synthType.syntheticType = type; - } - } - return synthType.syntheticType; + if (optionsType) { + const importCallOptionsType = getGlobalImportCallOptionsType(/*reportErrors*/ true); + if (importCallOptionsType !== emptyObjectType) { + checkTypeAssignableTo(optionsType, getNullableType(importCallOptionsType, ts.TypeFlags.Undefined), node.arguments[1]); } - return type; } - function isCommonJsRequire(node: ts.Node): boolean { - if (!ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { - return false; + // 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, getTypeWithSyntheticDefaultOnly(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier) || + getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier)); } + } + return createPromiseReturnType(node, anyType); + } - // Make sure require is not a local function - if (!ts.isIdentifier(node.expression)) - return ts.Debug.fail(); - const resolvedRequire = resolveName(node.expression, node.expression.escapedText, ts.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 & ts.SymbolFlags.Alias) { - return false; - } + function createDefaultPropertyWrapperForModule(symbol: ts.Symbol, originalSymbol: ts.Symbol, anonymousSymbol?: ts.Symbol | undefined) { + const memberTable = ts.createSymbolTable(); + const newSymbol = createSymbol(ts.SymbolFlags.Alias, ts.InternalSymbolName.Default); + newSymbol.parent = originalSymbol; + newSymbol.nameType = getStringLiteralType("default"); + newSymbol.aliasTarget = resolveSymbol(symbol); + memberTable.set(ts.InternalSymbolName.Default, newSymbol); + return createAnonymousType(anonymousSymbol, memberTable, ts.emptyArray, ts.emptyArray, ts.emptyArray); + } - const targetDeclarationKind = resolvedRequire.flags & ts.SymbolFlags.Function - ? ts.SyntaxKind.FunctionDeclaration - : resolvedRequire.flags & ts.SymbolFlags.Variable - ? ts.SyntaxKind.VariableDeclaration - : ts.SyntaxKind.Unknown; - if (targetDeclarationKind !== ts.SyntaxKind.Unknown) { - const decl = ts.getDeclarationOfKind(resolvedRequire, targetDeclarationKind)!; - // function/variable declaration should be ambient - return !!decl && !!(decl.flags & ts.NodeFlags.Ambient); + function getTypeWithSyntheticDefaultOnly(type: ts.Type, symbol: ts.Symbol, originalSymbol: ts.Symbol, moduleSpecifier: ts.Expression) { + const hasDefaultOnly = isOnlyImportedAsDefault(moduleSpecifier); + if (hasDefaultOnly && type && !isErrorType(type)) { + const synthType = type as ts.SyntheticDefaultModuleType; + if (!synthType.defaultOnlyType) { + const type = createDefaultPropertyWrapperForModule(symbol, originalSymbol); + synthType.defaultOnlyType = type; } - return false; + return synthType.defaultOnlyType; } + return undefined; + } - function checkTaggedTemplateExpression(node: ts.TaggedTemplateExpression): ts.Type { - if (!checkGrammarTaggedTemplateChain(node)) - checkGrammarTypeArguments(node, node.typeArguments); - if (languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.MakeTemplateObject); + function getTypeWithSyntheticDefaultImportType(type: ts.Type, symbol: ts.Symbol, originalSymbol: ts.Symbol, moduleSpecifier: ts.Expression): ts.Type { + if (allowSyntheticDefaultImports && type && !isErrorType(type)) { + const synthType = type as ts.SyntheticDefaultModuleType; + if (!synthType.syntheticType) { + const file = originalSymbol.declarations?.find(ts.isSourceFile); + const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false, moduleSpecifier); + if (hasSyntheticDefault) { + const anonymousSymbol = createSymbol(ts.SymbolFlags.TypeLiteral, ts.InternalSymbolName.Type); + const defaultContainingObject = createDefaultPropertyWrapperForModule(symbol, originalSymbol, anonymousSymbol); + anonymousSymbol.type = defaultContainingObject; + synthType.syntheticType = isValidSpreadType(type) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*objectFlags*/ 0, /*readonly*/ false) : defaultContainingObject; + } + else { + synthType.syntheticType = type; + } } - const signature = getResolvedSignature(node); - checkDeprecatedSignature(signature, node); - return getReturnTypeOfSignature(signature); + return synthType.syntheticType; } + return type; + } - function checkAssertion(node: ts.AssertionExpression) { - if (node.kind === ts.SyntaxKind.TypeAssertionExpression) { - const file = ts.getSourceFileOfNode(node); - if (file && ts.fileExtensionIsOneOf(file.fileName, [ts.Extension.Cts, ts.Extension.Mts])) { - grammarErrorOnNode(node, ts.Diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Use_an_as_expression_instead); - } - } - return checkAssertionWorker(node, node.type, node.expression); + function isCommonJsRequire(node: ts.Node): boolean { + if (!ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { + return false; } - function isValidConstAssertionArgument(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.ArrayLiteralExpression: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.TemplateExpression: - return true; - case ts.SyntaxKind.ParenthesizedExpression: - return isValidConstAssertionArgument((node as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.PrefixUnaryExpression: - const op = (node as ts.PrefixUnaryExpression).operator; - const arg = (node as ts.PrefixUnaryExpression).operand; - return op === ts.SyntaxKind.MinusToken && (arg.kind === ts.SyntaxKind.NumericLiteral || arg.kind === ts.SyntaxKind.BigIntLiteral) || - op === ts.SyntaxKind.PlusToken && arg.kind === ts.SyntaxKind.NumericLiteral; - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - const expr = (node as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression; - let symbol = getTypeOfNode(expr).symbol; - if (symbol && symbol.flags & ts.SymbolFlags.Alias) { - symbol = resolveAlias(symbol); - } - return !!(symbol && (symbol.flags & ts.SymbolFlags.Enum) && getEnumKind(symbol) === ts.EnumKind.Literal); - } + // Make sure require is not a local function + if (!ts.isIdentifier(node.expression)) + return ts.Debug.fail(); + const resolvedRequire = resolveName(node.expression, node.expression.escapedText, ts.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 & ts.SymbolFlags.Alias) { return false; } - function checkAssertionWorker(errNode: ts.Node, type: ts.TypeNode, expression: ts.UnaryExpression | ts.Expression, checkMode?: CheckMode) { - let exprType = checkExpression(expression, checkMode); - if (ts.isConstTypeReference(type)) { - if (!isValidConstAssertionArgument(expression)) { - error(expression, ts.Diagnostics.A_const_assertions_can_only_be_applied_to_references_to_enum_members_or_string_number_boolean_array_or_object_literals); - } - return getRegularTypeOfLiteralType(exprType); - } - checkSourceElement(type); - exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType)); - const targetType = getTypeFromTypeNode(type); - if (!isErrorType(targetType)) { - addLazyDiagnostic(() => { - const widenedType = getWidenedType(exprType); - if (!isTypeComparableTo(targetType, widenedType)) { - checkTypeComparableTo(exprType, targetType, errNode, ts.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 targetType; + const targetDeclarationKind = resolvedRequire.flags & ts.SymbolFlags.Function + ? ts.SyntaxKind.FunctionDeclaration + : resolvedRequire.flags & ts.SymbolFlags.Variable + ? ts.SyntaxKind.VariableDeclaration + : ts.SyntaxKind.Unknown; + if (targetDeclarationKind !== ts.SyntaxKind.Unknown) { + const decl = ts.getDeclarationOfKind(resolvedRequire, targetDeclarationKind)!; + // function/variable declaration should be ambient + return !!decl && !!(decl.flags & ts.NodeFlags.Ambient); } + return false; + } - function checkNonNullChain(node: ts.NonNullChain) { - const leftType = checkExpression(node.expression); - const nonOptionalType = getOptionalExpressionType(leftType, node.expression); - return propagateOptionalTypeMarker(getNonNullableType(nonOptionalType), node, nonOptionalType !== leftType); + function checkTaggedTemplateExpression(node: ts.TaggedTemplateExpression): ts.Type { + if (!checkGrammarTaggedTemplateChain(node)) + checkGrammarTypeArguments(node, node.typeArguments); + if (languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.MakeTemplateObject); } + const signature = getResolvedSignature(node); + checkDeprecatedSignature(signature, node); + return getReturnTypeOfSignature(signature); + } - function checkNonNullAssertion(node: ts.NonNullExpression) { - return node.flags & ts.NodeFlags.OptionalChain ? checkNonNullChain(node as ts.NonNullChain) : - getNonNullableType(checkExpression(node.expression)); + function checkAssertion(node: ts.AssertionExpression) { + if (node.kind === ts.SyntaxKind.TypeAssertionExpression) { + const file = ts.getSourceFileOfNode(node); + if (file && ts.fileExtensionIsOneOf(file.fileName, [ts.Extension.Cts, ts.Extension.Mts])) { + grammarErrorOnNode(node, ts.Diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Use_an_as_expression_instead); + } } + return checkAssertionWorker(node, node.type, node.expression); + } - function checkExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments | ts.TypeQueryNode) { - checkGrammarExpressionWithTypeArguments(node); - const exprType = node.kind === ts.SyntaxKind.ExpressionWithTypeArguments ? checkExpression(node.expression) : - ts.isThisIdentifier(node.exprName) ? checkThisExpression(node.exprName) : - checkExpression(node.exprName); - const typeArguments = node.typeArguments; - if (exprType === silentNeverType || isErrorType(exprType) || !ts.some(typeArguments)) { - return exprType; + function isValidConstAssertionArgument(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.ArrayLiteralExpression: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.TemplateExpression: + return true; + case ts.SyntaxKind.ParenthesizedExpression: + return isValidConstAssertionArgument((node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.PrefixUnaryExpression: + const op = (node as ts.PrefixUnaryExpression).operator; + const arg = (node as ts.PrefixUnaryExpression).operand; + return op === ts.SyntaxKind.MinusToken && (arg.kind === ts.SyntaxKind.NumericLiteral || arg.kind === ts.SyntaxKind.BigIntLiteral) || + op === ts.SyntaxKind.PlusToken && arg.kind === ts.SyntaxKind.NumericLiteral; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const expr = (node as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression; + let symbol = getTypeOfNode(expr).symbol; + if (symbol && symbol.flags & ts.SymbolFlags.Alias) { + symbol = resolveAlias(symbol); + } + return !!(symbol && (symbol.flags & ts.SymbolFlags.Enum) && getEnumKind(symbol) === ts.EnumKind.Literal); + } + return false; + } + + function checkAssertionWorker(errNode: ts.Node, type: ts.TypeNode, expression: ts.UnaryExpression | ts.Expression, checkMode?: CheckMode) { + let exprType = checkExpression(expression, checkMode); + if (ts.isConstTypeReference(type)) { + if (!isValidConstAssertionArgument(expression)) { + error(expression, ts.Diagnostics.A_const_assertions_can_only_be_applied_to_references_to_enum_members_or_string_number_boolean_array_or_object_literals); } - let hasSomeApplicableSignature = false; - let nonApplicableType: ts.Type | undefined; - const result = getInstantiatedType(exprType); - const errorType = hasSomeApplicableSignature ? nonApplicableType : exprType; - if (errorType) { - diagnostics.add(ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.Type_0_has_no_signatures_for_which_the_type_argument_list_is_applicable, typeToString(errorType))); + return getRegularTypeOfLiteralType(exprType); + } + checkSourceElement(type); + exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType)); + const targetType = getTypeFromTypeNode(type); + if (!isErrorType(targetType)) { + addLazyDiagnostic(() => { + const widenedType = getWidenedType(exprType); + if (!isTypeComparableTo(targetType, widenedType)) { + checkTypeComparableTo(exprType, targetType, errNode, ts.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 targetType; + } + + function checkNonNullChain(node: ts.NonNullChain) { + const leftType = checkExpression(node.expression); + const nonOptionalType = getOptionalExpressionType(leftType, node.expression); + return propagateOptionalTypeMarker(getNonNullableType(nonOptionalType), node, nonOptionalType !== leftType); + } + + function checkNonNullAssertion(node: ts.NonNullExpression) { + return node.flags & ts.NodeFlags.OptionalChain ? checkNonNullChain(node as ts.NonNullChain) : + getNonNullableType(checkExpression(node.expression)); + } + + function checkExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments | ts.TypeQueryNode) { + checkGrammarExpressionWithTypeArguments(node); + const exprType = node.kind === ts.SyntaxKind.ExpressionWithTypeArguments ? checkExpression(node.expression) : + ts.isThisIdentifier(node.exprName) ? checkThisExpression(node.exprName) : + checkExpression(node.exprName); + const typeArguments = node.typeArguments; + if (exprType === silentNeverType || isErrorType(exprType) || !ts.some(typeArguments)) { + return exprType; + } + let hasSomeApplicableSignature = false; + let nonApplicableType: ts.Type | undefined; + const result = getInstantiatedType(exprType); + const errorType = hasSomeApplicableSignature ? nonApplicableType : exprType; + if (errorType) { + diagnostics.add(ts.createDiagnosticForNodeArray(ts.getSourceFileOfNode(node), typeArguments, ts.Diagnostics.Type_0_has_no_signatures_for_which_the_type_argument_list_is_applicable, typeToString(errorType))); + } + return result; + + function getInstantiatedType(type: ts.Type): ts.Type { + let hasSignatures = false; + let hasApplicableSignature = false; + const result = getInstantiatedTypePart(type); + hasSomeApplicableSignature ||= hasApplicableSignature; + if (hasSignatures && !hasApplicableSignature) { + nonApplicableType ??= type; } return result; - function getInstantiatedType(type: ts.Type): ts.Type { - let hasSignatures = false; - let hasApplicableSignature = false; - const result = getInstantiatedTypePart(type); - hasSomeApplicableSignature ||= hasApplicableSignature; - if (hasSignatures && !hasApplicableSignature) { - nonApplicableType ??= type; - } - return result; - - function getInstantiatedTypePart(type: ts.Type): ts.Type { - if (type.flags & ts.TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); - const callSignatures = getInstantiatedSignatures(resolved.callSignatures); - const constructSignatures = getInstantiatedSignatures(resolved.constructSignatures); - hasSignatures ||= resolved.callSignatures.length !== 0 || resolved.constructSignatures.length !== 0; - hasApplicableSignature ||= callSignatures.length !== 0 || constructSignatures.length !== 0; - if (callSignatures !== resolved.callSignatures || constructSignatures !== resolved.constructSignatures) { - const result = createAnonymousType(undefined, resolved.members, callSignatures, constructSignatures, resolved.indexInfos) as ts.ResolvedType & ts.InstantiationExpressionType; - result.objectFlags |= ts.ObjectFlags.InstantiationExpressionType; - result.node = node; - return result; - } + function getInstantiatedTypePart(type: ts.Type): ts.Type { + if (type.flags & ts.TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type as ts.ObjectType); + const callSignatures = getInstantiatedSignatures(resolved.callSignatures); + const constructSignatures = getInstantiatedSignatures(resolved.constructSignatures); + hasSignatures ||= resolved.callSignatures.length !== 0 || resolved.constructSignatures.length !== 0; + hasApplicableSignature ||= callSignatures.length !== 0 || constructSignatures.length !== 0; + if (callSignatures !== resolved.callSignatures || constructSignatures !== resolved.constructSignatures) { + const result = createAnonymousType(undefined, resolved.members, callSignatures, constructSignatures, resolved.indexInfos) as ts.ResolvedType & ts.InstantiationExpressionType; + result.objectFlags |= ts.ObjectFlags.InstantiationExpressionType; + result.node = node; + return result; } - else if (type.flags & ts.TypeFlags.InstantiableNonPrimitive) { - const constraint = getBaseConstraintOfType(type); - if (constraint) { - const instantiated = getInstantiatedTypePart(constraint); - if (instantiated !== constraint) { - return instantiated; - } + } + else if (type.flags & ts.TypeFlags.InstantiableNonPrimitive) { + const constraint = getBaseConstraintOfType(type); + if (constraint) { + const instantiated = getInstantiatedTypePart(constraint); + if (instantiated !== constraint) { + return instantiated; } } - else if (type.flags & ts.TypeFlags.Union) { - return mapType(type, getInstantiatedType); - } - else if (type.flags & ts.TypeFlags.Intersection) { - return getIntersectionType(ts.sameMap((type as ts.IntersectionType).types, getInstantiatedTypePart)); - } - return type; } - } - - function getInstantiatedSignatures(signatures: readonly ts.Signature[]) { - const applicableSignatures = ts.filter(signatures, sig => !!sig.typeParameters && hasCorrectTypeArgumentArity(sig, typeArguments)); - return ts.sameMap(applicableSignatures, sig => { - const typeArgumentTypes = checkTypeArguments(sig, typeArguments!, /*reportErrors*/ true); - return typeArgumentTypes ? getSignatureInstantiation(sig, typeArgumentTypes, ts.isInJSFile(sig.declaration)) : sig; - }); + else if (type.flags & ts.TypeFlags.Union) { + return mapType(type, getInstantiatedType); + } + else if (type.flags & ts.TypeFlags.Intersection) { + return getIntersectionType(ts.sameMap((type as ts.IntersectionType).types, getInstantiatedTypePart)); + } + return type; } } - function checkMetaProperty(node: ts.MetaProperty): ts.Type { - checkGrammarMetaProperty(node); - - if (node.keywordToken === ts.SyntaxKind.NewKeyword) { - return checkNewTargetMetaProperty(node); - } + function getInstantiatedSignatures(signatures: readonly ts.Signature[]) { + const applicableSignatures = ts.filter(signatures, sig => !!sig.typeParameters && hasCorrectTypeArgumentArity(sig, typeArguments)); + return ts.sameMap(applicableSignatures, sig => { + const typeArgumentTypes = checkTypeArguments(sig, typeArguments!, /*reportErrors*/ true); + return typeArgumentTypes ? getSignatureInstantiation(sig, typeArgumentTypes, ts.isInJSFile(sig.declaration)) : sig; + }); + } + } - if (node.keywordToken === ts.SyntaxKind.ImportKeyword) { - return checkImportMetaProperty(node); - } + function checkMetaProperty(node: ts.MetaProperty): ts.Type { + checkGrammarMetaProperty(node); - return ts.Debug.assertNever(node.keywordToken); + if (node.keywordToken === ts.SyntaxKind.NewKeyword) { + return checkNewTargetMetaProperty(node); } - function checkMetaPropertyKeyword(node: ts.MetaProperty): ts.Type { - switch (node.keywordToken) { - case ts.SyntaxKind.ImportKeyword: - return getGlobalImportMetaExpressionType(); - case ts.SyntaxKind.NewKeyword: - const type = checkNewTargetMetaProperty(node); - return isErrorType(type) ? errorType : createNewTargetExpressionType(type); - default: - ts.Debug.assertNever(node.keywordToken); - } + if (node.keywordToken === ts.SyntaxKind.ImportKeyword) { + return checkImportMetaProperty(node); } - function checkNewTargetMetaProperty(node: ts.MetaProperty) { - const container = ts.getNewTargetContainer(node); - if (!container) { - error(node, ts.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 === ts.SyntaxKind.Constructor) { - const symbol = getSymbolOfNode(container.parent as ts.ClassLikeDeclaration); - return getTypeOfSymbol(symbol); - } - else { - const symbol = getSymbolOfNode(container)!; - return getTypeOfSymbol(symbol); - } - } + return ts.Debug.assertNever(node.keywordToken); + } - function checkImportMetaProperty(node: ts.MetaProperty) { - if (moduleKind === ts.ModuleKind.Node16 || moduleKind === ts.ModuleKind.NodeNext) { - if (ts.getSourceFileOfNode(node).impliedNodeFormat !== ts.ModuleKind.ESNext) { - error(node, ts.Diagnostics.The_import_meta_meta_property_is_not_allowed_in_files_which_will_build_into_CommonJS_output); - } - } - else if (moduleKind < ts.ModuleKind.ES2020 && moduleKind !== ts.ModuleKind.System) { - error(node, ts.Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_es2020_es2022_esnext_system_node16_or_nodenext); - } - const file = ts.getSourceFileOfNode(node); - ts.Debug.assert(!!(file.flags & ts.NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); - return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType; + function checkMetaPropertyKeyword(node: ts.MetaProperty): ts.Type { + switch (node.keywordToken) { + case ts.SyntaxKind.ImportKeyword: + return getGlobalImportMetaExpressionType(); + case ts.SyntaxKind.NewKeyword: + const type = checkNewTargetMetaProperty(node); + return isErrorType(type) ? errorType : createNewTargetExpressionType(type); + default: + ts.Debug.assertNever(node.keywordToken); } + } - function getTypeOfParameter(symbol: ts.Symbol) { - const type = getTypeOfSymbol(symbol); - if (strictNullChecks) { - const declaration = symbol.valueDeclaration; - if (declaration && ts.hasInitializer(declaration)) { - return getOptionalType(type); - } - } - return type; + function checkNewTargetMetaProperty(node: ts.MetaProperty) { + const container = ts.getNewTargetContainer(node); + if (!container) { + error(node, ts.Diagnostics.Meta_property_0_is_only_allowed_in_the_body_of_a_function_declaration_function_expression_or_constructor, "new.target"); + return errorType; } - - function getTupleElementLabel(d: ts.ParameterDeclaration | ts.NamedTupleMember) { - ts.Debug.assert(ts.isIdentifier(d.name)); // Parameter declarations could be binding patterns, but we only allow identifier names - return d.name.escapedText; + else if (container.kind === ts.SyntaxKind.Constructor) { + const symbol = getSymbolOfNode(container.parent as ts.ClassLikeDeclaration); + return getTypeOfSymbol(symbol); } + else { + const symbol = getSymbolOfNode(container)!; + return getTypeOfSymbol(symbol); + } + } - function getParameterNameAtPosition(signature: ts.Signature, pos: number, overrideRestType?: ts.Type) { - 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 = overrideRestType || getTypeOfSymbol(restParameter); - if (isTupleType(restType)) { - const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; - const index = pos - paramCount; - return associatedNames && getTupleElementLabel(associatedNames[index]) || restParameter.escapedName + "_" + index as ts.__String; + function checkImportMetaProperty(node: ts.MetaProperty) { + if (moduleKind === ts.ModuleKind.Node16 || moduleKind === ts.ModuleKind.NodeNext) { + if (ts.getSourceFileOfNode(node).impliedNodeFormat !== ts.ModuleKind.ESNext) { + error(node, ts.Diagnostics.The_import_meta_meta_property_is_not_allowed_in_files_which_will_build_into_CommonJS_output); } - return restParameter.escapedName; } + else if (moduleKind < ts.ModuleKind.ES2020 && moduleKind !== ts.ModuleKind.System) { + error(node, ts.Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_es2020_es2022_esnext_system_node16_or_nodenext); + } + const file = ts.getSourceFileOfNode(node); + ts.Debug.assert(!!(file.flags & ts.NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); + return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType; + } - function getParameterIdentifierNameAtPosition(signature: ts.Signature, pos: number): [ - parameterName: ts.__String, - isRestParameter: boolean - ] | undefined { - if (signature.declaration?.kind === ts.SyntaxKind.JSDocFunctionType) { - return undefined; - } - const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - if (pos < paramCount) { - const param = signature.parameters[pos]; - return isParameterDeclarationWithIdentifierName(param) ? [param.escapedName, false] : undefined; + function getTypeOfParameter(symbol: ts.Symbol) { + const type = getTypeOfSymbol(symbol); + if (strictNullChecks) { + const declaration = symbol.valueDeclaration; + if (declaration && ts.hasInitializer(declaration)) { + return getOptionalType(type); } + } + return type; + } - const restParameter = signature.parameters[paramCount] || unknownSymbol; - if (!isParameterDeclarationWithIdentifierName(restParameter)) { - return undefined; - } + function getTupleElementLabel(d: ts.ParameterDeclaration | ts.NamedTupleMember) { + ts.Debug.assert(ts.isIdentifier(d.name)); // Parameter declarations could be binding patterns, but we only allow identifier names + return d.name.escapedText; + } - const restType = getTypeOfSymbol(restParameter); - if (isTupleType(restType)) { - const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; - const index = pos - paramCount; - const associatedName = associatedNames?.[index]; - const isRestTupleElement = !!associatedName?.dotDotDotToken; - return associatedName ? [ - getTupleElementLabel(associatedName), - isRestTupleElement - ] : undefined; - } + function getParameterNameAtPosition(signature: ts.Signature, pos: number, overrideRestType?: ts.Type) { + 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 = overrideRestType || getTypeOfSymbol(restParameter); + if (isTupleType(restType)) { + const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; + const index = pos - paramCount; + return associatedNames && getTupleElementLabel(associatedNames[index]) || restParameter.escapedName + "_" + index as ts.__String; + } + return restParameter.escapedName; + } - if (pos === paramCount) { - return [restParameter.escapedName, true]; - } + function getParameterIdentifierNameAtPosition(signature: ts.Signature, pos: number): [ + parameterName: ts.__String, + isRestParameter: boolean + ] | undefined { + if (signature.declaration?.kind === ts.SyntaxKind.JSDocFunctionType) { return undefined; } + const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + if (pos < paramCount) { + const param = signature.parameters[pos]; + return isParameterDeclarationWithIdentifierName(param) ? [param.escapedName, false] : undefined; + } - function isParameterDeclarationWithIdentifierName(symbol: ts.Symbol) { - return symbol.valueDeclaration && ts.isParameter(symbol.valueDeclaration) && ts.isIdentifier(symbol.valueDeclaration.name); + const restParameter = signature.parameters[paramCount] || unknownSymbol; + if (!isParameterDeclarationWithIdentifierName(restParameter)) { + return undefined; } - function isValidDeclarationForTupleLabel(d: ts.Declaration): d is ts.NamedTupleMember | (ts.ParameterDeclaration & { - name: ts.Identifier; - }) { - return d.kind === ts.SyntaxKind.NamedTupleMember || (ts.isParameter(d) && d.name && ts.isIdentifier(d.name)); + + const restType = getTypeOfSymbol(restParameter); + if (isTupleType(restType)) { + const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; + const index = pos - paramCount; + const associatedName = associatedNames?.[index]; + const isRestTupleElement = !!associatedName?.dotDotDotToken; + return associatedName ? [ + getTupleElementLabel(associatedName), + isRestTupleElement + ] : undefined; } - function getNameableDeclarationAtPosition(signature: ts.Signature, pos: number) { - const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - if (pos < paramCount) { - const decl = signature.parameters[pos].valueDeclaration; - return decl && isValidDeclarationForTupleLabel(decl) ? decl : undefined; - } - const restParameter = signature.parameters[paramCount] || unknownSymbol; - const restType = getTypeOfSymbol(restParameter); - if (isTupleType(restType)) { - const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; - const index = pos - paramCount; - return associatedNames && associatedNames[index]; - } - return restParameter.valueDeclaration && isValidDeclarationForTupleLabel(restParameter.valueDeclaration) ? restParameter.valueDeclaration : undefined; + if (pos === paramCount) { + return [restParameter.escapedName, true]; } + return undefined; + } - function getTypeAtPosition(signature: ts.Signature, pos: number): ts.Type { - return tryGetTypeAtPosition(signature, pos) || anyType; + function isParameterDeclarationWithIdentifierName(symbol: ts.Symbol) { + return symbol.valueDeclaration && ts.isParameter(symbol.valueDeclaration) && ts.isIdentifier(symbol.valueDeclaration.name); + } + function isValidDeclarationForTupleLabel(d: ts.Declaration): d is ts.NamedTupleMember | (ts.ParameterDeclaration & { + name: ts.Identifier; + }) { + return d.kind === ts.SyntaxKind.NamedTupleMember || (ts.isParameter(d) && d.name && ts.isIdentifier(d.name)); + } + + function getNameableDeclarationAtPosition(signature: ts.Signature, pos: number) { + const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + if (pos < paramCount) { + const decl = signature.parameters[pos].valueDeclaration; + return decl && isValidDeclarationForTupleLabel(decl) ? decl : undefined; + } + const restParameter = signature.parameters[paramCount] || unknownSymbol; + const restType = getTypeOfSymbol(restParameter); + if (isTupleType(restType)) { + const associatedNames = ((restType as ts.TypeReference).target as ts.TupleType).labeledElementDeclarations; + const index = pos - paramCount; + return associatedNames && associatedNames[index]; } + return restParameter.valueDeclaration && isValidDeclarationForTupleLabel(restParameter.valueDeclaration) ? restParameter.valueDeclaration : undefined; + } - 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 < restType.target.fixedLength) { - return getIndexedAccessType(restType, getNumberLiteralType(index)); - } + 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 < restType.target.fixedLength) { + return getIndexedAccessType(restType, getNumberLiteralType(index)); } - return undefined; } + return undefined; + } - function getRestTypeAtPosition(source: ts.Signature, pos: number): ts.Type { - const parameterCount = getParameterCount(source); - const minArgumentCount = getMinArgumentCount(source); - const restType = getEffectiveRestType(source); - if (restType && pos >= parameterCount - 1) { - return pos === parameterCount - 1 ? restType : createArrayType(getIndexedAccessType(restType, numberType)); + function getRestTypeAtPosition(source: ts.Signature, pos: number): ts.Type { + const parameterCount = getParameterCount(source); + const minArgumentCount = getMinArgumentCount(source); + const restType = getEffectiveRestType(source); + if (restType && pos >= parameterCount - 1) { + return pos === parameterCount - 1 ? restType : createArrayType(getIndexedAccessType(restType, numberType)); + } + const types = []; + const flags = []; + const names = []; + for (let i = pos; i < parameterCount; i++) { + if (!restType || i < parameterCount - 1) { + types.push(getTypeAtPosition(source, i)); + flags.push(i < minArgumentCount ? ts.ElementFlags.Required : ts.ElementFlags.Optional); } - const types = []; - const flags = []; - const names = []; - for (let i = pos; i < parameterCount; i++) { - if (!restType || i < parameterCount - 1) { - types.push(getTypeAtPosition(source, i)); - flags.push(i < minArgumentCount ? ts.ElementFlags.Required : ts.ElementFlags.Optional); - } - else { - types.push(restType); - flags.push(ts.ElementFlags.Variadic); - } - const name = getNameableDeclarationAtPosition(source, i); - if (name) { - names.push(name); - } + else { + types.push(restType); + flags.push(ts.ElementFlags.Variadic); + } + const name = getNameableDeclarationAtPosition(source, i); + if (name) { + names.push(name); } - return createTupleType(types, flags, /*readonly*/ false, ts.length(names) === ts.length(types) ? names : undefined); } + return createTupleType(types, flags, /*readonly*/ false, ts.length(names) === ts.length(types) ? names : undefined); + } - // Return the number of parameters in a signature. The rest parameter, if present, counts as one - // parameter. For example, the parameter count of (x: number, y: number, ...z: string[]) is 3 and - // the parameter count of (x: number, ...args: [number, ...string[], boolean])) is also 3. In the - // latter example, the effective rest type is [...string[], boolean]. - 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 + restType.target.fixedLength - (restType.target.hasRestElement ? 0 : 1); - } + // Return the number of parameters in a signature. The rest parameter, if present, counts as one + // parameter. For example, the parameter count of (x: number, y: number, ...z: string[]) is 3 and + // the parameter count of (x: number, ...args: [number, ...string[], boolean])) is also 3. In the + // latter example, the effective rest type is [...string[], boolean]. + 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 + restType.target.fixedLength - (restType.target.hasRestElement ? 0 : 1); } - return length; } + return length; + } - function getMinArgumentCount(signature: ts.Signature, flags?: MinArgumentCountFlags) { - const strongArityForUntypedJS = flags! & MinArgumentCountFlags.StrongArityForUntypedJS; - const voidIsNonOptional = flags! & MinArgumentCountFlags.VoidIsNonOptional; - if (voidIsNonOptional || signature.resolvedMinArgumentCount === undefined) { - let minArgumentCount: number | undefined; - if (signatureHasRestParameter(signature)) { - const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); - if (isTupleType(restType)) { - const firstOptionalIndex = ts.findIndex(restType.target.elementFlags, f => !(f & ts.ElementFlags.Required)); - const requiredCount = firstOptionalIndex < 0 ? restType.target.fixedLength : firstOptionalIndex; - if (requiredCount > 0) { - minArgumentCount = signature.parameters.length - 1 + requiredCount; - } - } - } - if (minArgumentCount === undefined) { - if (!strongArityForUntypedJS && signature.flags & ts.SignatureFlags.IsUntypedSignatureInJSFile) { - return 0; + function getMinArgumentCount(signature: ts.Signature, flags?: MinArgumentCountFlags) { + const strongArityForUntypedJS = flags! & MinArgumentCountFlags.StrongArityForUntypedJS; + const voidIsNonOptional = flags! & MinArgumentCountFlags.VoidIsNonOptional; + if (voidIsNonOptional || signature.resolvedMinArgumentCount === undefined) { + let minArgumentCount: number | undefined; + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + if (isTupleType(restType)) { + const firstOptionalIndex = ts.findIndex(restType.target.elementFlags, f => !(f & ts.ElementFlags.Required)); + const requiredCount = firstOptionalIndex < 0 ? restType.target.fixedLength : firstOptionalIndex; + if (requiredCount > 0) { + minArgumentCount = signature.parameters.length - 1 + requiredCount; } - minArgumentCount = signature.minArgumentCount; } - if (voidIsNonOptional) { - return minArgumentCount; + } + if (minArgumentCount === undefined) { + if (!strongArityForUntypedJS && signature.flags & ts.SignatureFlags.IsUntypedSignatureInJSFile) { + return 0; } - for (let i = minArgumentCount - 1; i >= 0; i--) { - const type = getTypeAtPosition(signature, i); - if (filterType(type, acceptsVoid).flags & ts.TypeFlags.Never) { - break; - } - minArgumentCount = i; + minArgumentCount = signature.minArgumentCount; + } + if (voidIsNonOptional) { + return minArgumentCount; + } + for (let i = minArgumentCount - 1; i >= 0; i--) { + const type = getTypeAtPosition(signature, i); + if (filterType(type, acceptsVoid).flags & ts.TypeFlags.Never) { + break; } - signature.resolvedMinArgumentCount = minArgumentCount; + minArgumentCount = i; } - return signature.resolvedMinArgumentCount; + signature.resolvedMinArgumentCount = minArgumentCount; } + return signature.resolvedMinArgumentCount; + } - 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 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]); - if (!isTupleType(restType)) { - return restType; - } - if (restType.target.hasRestElement) { - return sliceTupleType(restType, restType.target.fixedLength); - } + function getEffectiveRestType(signature: ts.Signature) { + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + if (!isTupleType(restType)) { + return restType; + } + if (restType.target.hasRestElement) { + return sliceTupleType(restType, restType.target.fixedLength); } - return undefined; } + return undefined; + } - function getNonArrayRestType(signature: ts.Signature) { - const restType = getEffectiveRestType(signature); - return restType && !isArrayType(restType) && !isTypeAny(restType) && (getReducedType(restType).flags & ts.TypeFlags.Never) === 0 ? restType : undefined; - } + function getNonArrayRestType(signature: ts.Signature) { + const restType = getEffectiveRestType(signature); + return restType && !isArrayType(restType) && !isTypeAny(restType) && (getReducedType(restType).flags & ts.TypeFlags.Never) === 0 ? restType : undefined; + } - function getTypeOfFirstParameterOfSignature(signature: ts.Signature) { - return getTypeOfFirstParameterOfSignatureWithFallback(signature, neverType); - } + 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 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: ts.InferenceContext) { - const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - for (let i = 0; i < len; i++) { - const declaration = signature.parameters[i].valueDeclaration as ts.ParameterDeclaration; - if (declaration.type) { - const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - inferTypes(inferenceContext.inferences, getTypeFromTypeNode(typeNode), getTypeAtPosition(context, i)); - } + function inferFromAnnotatedParameters(signature: ts.Signature, context: ts.Signature, inferenceContext: ts.InferenceContext) { + const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + for (let i = 0; i < len; i++) { + const declaration = signature.parameters[i].valueDeclaration as ts.ParameterDeclaration; + if (declaration.type) { + const typeNode = ts.getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + inferTypes(inferenceContext.inferences, getTypeFromTypeNode(typeNode), getTypeAtPosition(context, i)); } } - const restType = getEffectiveRestType(context); - if (restType && restType.flags & ts.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 restType = getEffectiveRestType(context); + if (restType && restType.flags & ts.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) { - if (context.typeParameters) { - if (!signature.typeParameters) { - signature.typeParameters = context.typeParameters; - } - else { - return; // This signature has already has a contextual inference performed and cached on it! - } - } - if (context.thisParameter) { - const parameter = signature.thisParameter; - if (!parameter || parameter.valueDeclaration && !(parameter.valueDeclaration as ts.ParameterDeclaration).type) { - if (!parameter) { - signature.thisParameter = createSymbolWithType(context.thisParameter, /*type*/ undefined); - } - assignParameterType(signature.thisParameter!, getTypeOfSymbol(context.thisParameter)); - } + function assignContextualParameterTypes(signature: ts.Signature, context: ts.Signature) { + if (context.typeParameters) { + if (!signature.typeParameters) { + signature.typeParameters = context.typeParameters; } - const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - for (let i = 0; i < len; i++) { - const parameter = signature.parameters[i]; - if (!ts.getEffectiveTypeAnnotationNode(parameter.valueDeclaration as ts.ParameterDeclaration)) { - const contextualParameterType = tryGetTypeAtPosition(context, i); - assignParameterType(parameter, contextualParameterType); - } + else { + return; // This signature has already has a contextual inference performed and cached on it! } - if (signatureHasRestParameter(signature)) { - // parameter might be a transient symbol generated by use of `arguments` in the function body. - const parameter = ts.last(signature.parameters); - if (parameter.valueDeclaration - ? !ts.getEffectiveTypeAnnotationNode(parameter.valueDeclaration as ts.ParameterDeclaration) - // a declarationless parameter may still have a `.type` already set by its construction logic - // (which may pull a type from a jsdoc) - only allow fixing on `DeferredType` parameters with a fallback type - : !!(ts.getCheckFlags(parameter) & ts.CheckFlags.DeferredType)) { - const contextualParameterType = getRestTypeAtPosition(context, len); - assignParameterType(parameter, contextualParameterType); + } + if (context.thisParameter) { + const parameter = signature.thisParameter; + if (!parameter || parameter.valueDeclaration && !(parameter.valueDeclaration as ts.ParameterDeclaration).type) { + if (!parameter) { + signature.thisParameter = createSymbolWithType(context.thisParameter, /*type*/ undefined); } + assignParameterType(signature.thisParameter!, getTypeOfSymbol(context.thisParameter)); } } - - function assignNonContextualParameterTypes(signature: ts.Signature) { - if (signature.thisParameter) { - assignParameterType(signature.thisParameter); + const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + for (let i = 0; i < len; i++) { + const parameter = signature.parameters[i]; + if (!ts.getEffectiveTypeAnnotationNode(parameter.valueDeclaration as ts.ParameterDeclaration)) { + const contextualParameterType = tryGetTypeAtPosition(context, i); + assignParameterType(parameter, contextualParameterType); } - for (const parameter of signature.parameters) { - assignParameterType(parameter); + } + if (signatureHasRestParameter(signature)) { + // parameter might be a transient symbol generated by use of `arguments` in the function body. + const parameter = ts.last(signature.parameters); + if (parameter.valueDeclaration + ? !ts.getEffectiveTypeAnnotationNode(parameter.valueDeclaration as ts.ParameterDeclaration) + // a declarationless parameter may still have a `.type` already set by its construction logic + // (which may pull a type from a jsdoc) - only allow fixing on `DeferredType` parameters with a fallback type + : !!(ts.getCheckFlags(parameter) & ts.CheckFlags.DeferredType)) { + const contextualParameterType = getRestTypeAtPosition(context, len); + assignParameterType(parameter, contextualParameterType); } } + } - function assignParameterType(parameter: ts.Symbol, type?: ts.Type) { - const links = getSymbolLinks(parameter); - if (!links.type) { - const declaration = parameter.valueDeclaration as ts.ParameterDeclaration | undefined; - links.type = type || (declaration ? getWidenedTypeForVariableLikeDeclaration(declaration, /*reportErrors*/ true) : getTypeOfSymbol(parameter)); - if (declaration && declaration.name.kind !== ts.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, links.type); - } - } - else if (type) { - ts.Debug.assertEqual(links.type, type, "Parameter symbol already has a cached type which differs from newly assigned type"); - } + function assignNonContextualParameterTypes(signature: ts.Signature) { + if (signature.thisParameter) { + assignParameterType(signature.thisParameter); } + for (const parameter of signature.parameters) { + assignParameterType(parameter); + } + } - // 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: ts.BindingPattern, parentType: ts.Type) { - for (const element of pattern.elements) { - if (!ts.isOmittedExpression(element)) { - const type = getBindingElementTypeFromParentType(element, parentType); - if (element.name.kind === ts.SyntaxKind.Identifier) { - getSymbolLinks(getSymbolOfNode(element)).type = type; - } - else { - assignBindingElementTypes(element.name, type); - } + function assignParameterType(parameter: ts.Symbol, type?: ts.Type) { + const links = getSymbolLinks(parameter); + if (!links.type) { + const declaration = parameter.valueDeclaration as ts.ParameterDeclaration | undefined; + links.type = type || (declaration ? getWidenedTypeForVariableLikeDeclaration(declaration, /*reportErrors*/ true) : getTypeOfSymbol(parameter)); + if (declaration && declaration.name.kind !== ts.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, links.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 - // Unwrap an `Awaited` to `T` to improve inference. - promisedType = getAwaitedTypeNoAlias(unwrapAwaitedType(promisedType)) || unknownType; - return createTypeReference(globalPromiseType, [promisedType]); - } - - return unknownType; + else if (type) { + ts.Debug.assertEqual(links.type, type, "Parameter symbol already has a cached type which differs from newly assigned type"); } + } - 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 - // Unwrap an `Awaited` to `T` to improve inference. - promisedType = getAwaitedTypeNoAlias(unwrapAwaitedType(promisedType)) || unknownType; - return createTypeReference(globalPromiseLikeType, [promisedType]); + // 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: ts.BindingPattern, parentType: ts.Type) { + for (const element of pattern.elements) { + if (!ts.isOmittedExpression(element)) { + const type = getBindingElementTypeFromParentType(element, parentType); + if (element.name.kind === ts.SyntaxKind.Identifier) { + getSymbolLinks(getSymbolOfNode(element)).type = type; + } + else { + assignBindingElementTypes(element.name, type); + } } + } + } - return unknownType; + 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 + // Unwrap an `Awaited` to `T` to improve inference. + promisedType = getAwaitedTypeNoAlias(unwrapAwaitedType(promisedType)) || unknownType; + return createTypeReference(globalPromiseType, [promisedType]); } - function createPromiseReturnType(func: ts.FunctionLikeDeclaration | ts.ImportCall, promisedType: ts.Type) { - const promiseType = createPromiseType(promisedType); - if (promiseType === unknownType) { - error(func, ts.isImportCall(func) ? - ts.Diagnostics.A_dynamic_import_call_returns_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option : - ts.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; - } - else if (!getGlobalPromiseConstructorSymbol(/*reportErrors*/ true)) { - error(func, ts.isImportCall(func) ? - ts.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 : - ts.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 unknownType; + } - return promiseType; + 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 + // Unwrap an `Awaited` to `T` to improve inference. + promisedType = getAwaitedTypeNoAlias(unwrapAwaitedType(promisedType)) || unknownType; + return createTypeReference(globalPromiseLikeType, [promisedType]); } - function createNewTargetExpressionType(targetType: ts.Type): ts.Type { - // Create a synthetic type `NewTargetExpression { target: TargetType; }` - const symbol = createSymbol(ts.SymbolFlags.None, "NewTargetExpression" as ts.__String); - const targetPropertySymbol = createSymbol(ts.SymbolFlags.Property, "target" as ts.__String, ts.CheckFlags.Readonly); - targetPropertySymbol.parent = symbol; - targetPropertySymbol.type = targetType; + return unknownType; + } - const members = ts.createSymbolTable([targetPropertySymbol]); - symbol.members = members; - return createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); + function createPromiseReturnType(func: ts.FunctionLikeDeclaration | ts.ImportCall, promisedType: ts.Type) { + const promiseType = createPromiseType(promisedType); + if (promiseType === unknownType) { + error(func, ts.isImportCall(func) ? + ts.Diagnostics.A_dynamic_import_call_returns_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option : + ts.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; + } + else if (!getGlobalPromiseConstructorSymbol(/*reportErrors*/ true)) { + error(func, ts.isImportCall(func) ? + ts.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 : + ts.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); } - function getReturnTypeFromBody(func: ts.FunctionLikeDeclaration, checkMode?: CheckMode): ts.Type { - if (!func.body) { - return errorType; - } + return promiseType; + } - const functionFlags = ts.getFunctionFlags(func); - const isAsync = (functionFlags & ts.FunctionFlags.Async) !== 0; - const isGenerator = (functionFlags & ts.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 !== ts.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 = unwrapAwaitedType(checkAwaitedType(returnType, /*withAlias*/ false, /*errorNode*/ func, ts.Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member)); - } - } - else if (isGenerator) { // Generator or AsyncGenerator function - const returnTypes = checkAndAggregateReturnExpressionTypes(func, checkMode); - if (!returnTypes) { - fallbackReturnType = neverType; - } - else if (returnTypes.length > 0) { - returnType = getUnionType(returnTypes, ts.UnionReduction.Subtype); - } - const { yieldTypes, nextTypes } = checkAndAggregateYieldOperandTypes(func, checkMode); - yieldType = ts.some(yieldTypes) ? getUnionType(yieldTypes, ts.UnionReduction.Subtype) : undefined; - nextType = ts.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 & ts.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 & ts.FunctionFlags.Async - ? createPromiseReturnType(func, voidType) // Async function - : voidType; // Normal function - } - - // Return a union of the return expression types. - returnType = getUnionType(types, ts.UnionReduction.Subtype); - } - - if (returnType || yieldType || nextType) { - if (yieldType) - reportErrorsFromWidening(func, yieldType, WideningKind.GeneratorYield); - if (returnType) - reportErrorsFromWidening(func, returnType, WideningKind.FunctionReturn); - if (nextType) - reportErrorsFromWidening(func, nextType, WideningKind.GeneratorNext); - if (returnType && isUnitType(returnType) || - yieldType && isUnitType(yieldType) || - nextType && isUnitType(nextType)) { - const contextualSignature = getContextualSignatureForFunctionLikeDeclaration(func); - 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); - } - else { - returnType = getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(returnType, contextualType, isAsync); - } - } + function createNewTargetExpressionType(targetType: ts.Type): ts.Type { + // Create a synthetic type `NewTargetExpression { target: TargetType; }` + const symbol = createSymbol(ts.SymbolFlags.None, "NewTargetExpression" as ts.__String); + const targetPropertySymbol = createSymbol(ts.SymbolFlags.Property, "target" as ts.__String, ts.CheckFlags.Readonly); + targetPropertySymbol.parent = symbol; + targetPropertySymbol.type = targetType; - if (yieldType) - yieldType = getWidenedType(yieldType); - if (returnType) - returnType = getWidenedType(returnType); - if (nextType) - nextType = getWidenedType(nextType); - } + const members = ts.createSymbolTable([targetPropertySymbol]); + symbol.members = members; + return createAnonymousType(symbol, members, ts.emptyArray, ts.emptyArray, ts.emptyArray); + } - if (isGenerator) { - return createGeneratorReturnType(yieldType || neverType, returnType || fallbackReturnType, nextType || getContextualIterationType(IterationTypeKind.Next, func) || unknownType, isAsync); - } - else { + function getReturnTypeFromBody(func: ts.FunctionLikeDeclaration, checkMode?: CheckMode): ts.Type { + if (!func.body) { + return errorType; + } + + const functionFlags = ts.getFunctionFlags(func); + const isAsync = (functionFlags & ts.FunctionFlags.Async) !== 0; + const isGenerator = (functionFlags & ts.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 !== ts.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 is awaited type of the body, wrapped in a native Promise type. - return isAsync - ? createPromiseType(returnType || fallbackReturnType) - : returnType || fallbackReturnType; + // 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 = unwrapAwaitedType(checkAwaitedType(returnType, /*withAlias*/ false, /*errorNode*/ func, ts.Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member)); } } - - 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; - } - - // The global Generator type doesn't exist, so report an error - resolver.getGlobalGeneratorType(/*reportErrors*/ true); - return emptyObjectType; + else if (isGenerator) { // Generator or AsyncGenerator function + const returnTypes = checkAndAggregateReturnExpressionTypes(func, checkMode); + if (!returnTypes) { + fallbackReturnType = neverType; } - - return createTypeFromGenericGlobalType(globalGeneratorType, [yieldType, returnType, nextType]); + else if (returnTypes.length > 0) { + returnType = getUnionType(returnTypes, ts.UnionReduction.Subtype); + } + const { yieldTypes, nextTypes } = checkAndAggregateYieldOperandTypes(func, checkMode); + yieldType = ts.some(yieldTypes) ? getUnionType(yieldTypes, ts.UnionReduction.Subtype) : undefined; + nextType = ts.some(nextTypes) ? getIntersectionType(nextTypes) : undefined; } - - function checkAndAggregateYieldOperandTypes(func: ts.FunctionLikeDeclaration, checkMode: CheckMode | undefined) { - const yieldTypes: ts.Type[] = []; - const nextTypes: ts.Type[] = []; - const isAsync = (ts.getFunctionFlags(func) & ts.FunctionFlags.Async) !== 0; - ts.forEachYieldExpression(func.body as ts.Block, yieldExpression => { - const yieldExpressionType = yieldExpression.expression ? checkExpression(yieldExpression.expression, checkMode) : undefinedWideningType; - ts.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; + 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 & ts.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 & ts.FunctionFlags.Async + ? createPromiseReturnType(func, voidType) // Async function + : voidType; // Normal function + } + + // Return a union of the return expression types. + returnType = getUnionType(types, ts.UnionReduction.Subtype); + } + + if (returnType || yieldType || nextType) { + if (yieldType) + reportErrorsFromWidening(func, yieldType, WideningKind.GeneratorYield); + if (returnType) + reportErrorsFromWidening(func, returnType, WideningKind.FunctionReturn); + if (nextType) + reportErrorsFromWidening(func, nextType, WideningKind.GeneratorNext); + if (returnType && isUnitType(returnType) || + yieldType && isUnitType(yieldType) || + nextType && isUnitType(nextType)) { + const contextualSignature = getContextualSignatureForFunctionLikeDeclaration(func); + 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); } else { - nextType = getContextualType(yieldExpression); + returnType = getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(returnType, contextualType, isAsync); } - if (nextType) - ts.pushIfUnique(nextTypes, nextType); - }); - return { yieldTypes, nextTypes }; + } + + if (yieldType) + yieldType = getWidenedType(yieldType); + if (returnType) + returnType = getWidenedType(returnType); + if (nextType) + nextType = getWidenedType(nextType); } - function getYieldedTypeOfYieldExpression(node: ts.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 - ? ts.Diagnostics.Type_of_iterated_elements_of_a_yield_Asterisk_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member - : ts.Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + 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; + } + } - /** - * 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); + 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; } - return facts; - } - function isExhaustiveSwitchStatement(node: ts.SwitchStatement): boolean { - const links = getNodeLinks(node); - return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node)); + // The global Generator type doesn't exist, so report an error + resolver.getGlobalGeneratorType(/*reportErrors*/ true); + return emptyObjectType; } - function computeExhaustiveSwitchStatement(node: ts.SwitchStatement): boolean { - if (node.expression.kind === ts.SyntaxKind.TypeOfExpression) { - const operandType = getTypeOfExpression((node.expression as ts.TypeOfExpression).expression); - const witnesses = getSwitchClauseTypeOfWitnesses(node, /*retainDefault*/ false); - // 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; - // Take any/unknown as a special condition. Or maybe we could change `type` to a union containing all primitive types. - if (type.flags & ts.TypeFlags.AnyOrUnknown) { - return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; - } - return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & ts.TypeFlags.Never); - } - const type = getTypeOfExpression(node.expression); - if (!isLiteralType(type)) { - return false; + return createTypeFromGenericGlobalType(globalGeneratorType, [yieldType, returnType, nextType]); + } + + function checkAndAggregateYieldOperandTypes(func: ts.FunctionLikeDeclaration, checkMode: CheckMode | undefined) { + const yieldTypes: ts.Type[] = []; + const nextTypes: ts.Type[] = []; + const isAsync = (ts.getFunctionFlags(func) & ts.FunctionFlags.Async) !== 0; + ts.forEachYieldExpression(func.body as ts.Block, yieldExpression => { + const yieldExpressionType = yieldExpression.expression ? checkExpression(yieldExpression.expression, checkMode) : undefinedWideningType; + ts.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; } - const switchTypes = getSwitchClauseTypes(node); - if (!switchTypes.length || ts.some(switchTypes, isNeitherUnitTypeNorNever)) { - return false; + else { + nextType = getContextualType(yieldExpression); } - return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); - } + if (nextType) + ts.pushIfUnique(nextTypes, nextType); + }); + return { yieldTypes, nextTypes }; + } + + function getYieldedTypeOfYieldExpression(node: ts.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 + ? ts.Diagnostics.Type_of_iterated_elements_of_a_yield_Asterisk_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member + : ts.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 functionHasImplicitReturn(func: ts.FunctionLikeDeclaration) { - return func.endFlowNode && isReachableFlowNode(func.endFlowNode); + function isExhaustiveSwitchStatement(node: ts.SwitchStatement): boolean { + const links = getNodeLinks(node); + return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node)); + } + + function computeExhaustiveSwitchStatement(node: ts.SwitchStatement): boolean { + if (node.expression.kind === ts.SyntaxKind.TypeOfExpression) { + const operandType = getTypeOfExpression((node.expression as ts.TypeOfExpression).expression); + const witnesses = getSwitchClauseTypeOfWitnesses(node, /*retainDefault*/ false); + // 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; + // Take any/unknown as a special condition. Or maybe we could change `type` to a union containing all primitive types. + if (type.flags & ts.TypeFlags.AnyOrUnknown) { + return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; + } + return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & ts.TypeFlags.Never); + } + const type = getTypeOfExpression(node.expression); + if (!isLiteralType(type)) { + return false; + } + const switchTypes = getSwitchClauseTypes(node); + if (!switchTypes.length || ts.some(switchTypes, isNeitherUnitTypeNorNever)) { + return false; } + return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); + } - /** NOTE: Return value of `[]` means a different thing than `undefined`. `[]` means func returns `void`, `undefined` means it returns `never`. */ - function checkAndAggregateReturnExpressionTypes(func: ts.FunctionLikeDeclaration, checkMode: CheckMode | undefined): ts.Type[] | undefined { - const functionFlags = ts.getFunctionFlags(func); - const aggregatedTypes: ts.Type[] = []; - let hasReturnWithNoExpression = functionHasImplicitReturn(func); - let hasReturnOfTypeNever = false; - ts.forEachReturnStatement(func.body as ts.Block, returnStatement => { - const expr = returnStatement.expression; - if (expr) { - let type = checkExpressionCached(expr, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); - if (functionFlags & ts.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 = unwrapAwaitedType(checkAwaitedType(type, /*withAlias*/ false, func, ts.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 & ts.TypeFlags.Never) { - hasReturnOfTypeNever = true; - } - ts.pushIfUnique(aggregatedTypes, type); + function functionHasImplicitReturn(func: ts.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: ts.FunctionLikeDeclaration, checkMode: CheckMode | undefined): ts.Type[] | undefined { + const functionFlags = ts.getFunctionFlags(func); + const aggregatedTypes: ts.Type[] = []; + let hasReturnWithNoExpression = functionHasImplicitReturn(func); + let hasReturnOfTypeNever = false; + ts.forEachReturnStatement(func.body as ts.Block, returnStatement => { + const expr = returnStatement.expression; + if (expr) { + let type = checkExpressionCached(expr, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); + if (functionFlags & ts.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 = unwrapAwaitedType(checkAwaitedType(type, /*withAlias*/ false, func, ts.Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member)); } - else { - hasReturnWithNoExpression = true; + if (type.flags & ts.TypeFlags.Never) { + hasReturnOfTypeNever = true; } - }); - if (aggregatedTypes.length === 0 && !hasReturnWithNoExpression && (hasReturnOfTypeNever || mayReturnNever(func))) { - return undefined; + ts.pushIfUnique(aggregatedTypes, type); } - 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 - ts.pushIfUnique(aggregatedTypes, undefinedType); + else { + hasReturnWithNoExpression = true; } - return aggregatedTypes; + }); + if (aggregatedTypes.length === 0 && !hasReturnWithNoExpression && (hasReturnOfTypeNever || mayReturnNever(func))) { + return undefined; } - function mayReturnNever(func: ts.FunctionLikeDeclaration): boolean { - switch (func.kind) { - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return true; - case ts.SyntaxKind.MethodDeclaration: - return func.parent.kind === ts.SyntaxKind.ObjectLiteralExpression; - default: - return false; - } + 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 + ts.pushIfUnique(aggregatedTypes, undefinedType); } + return aggregatedTypes; + } + function mayReturnNever(func: ts.FunctionLikeDeclaration): boolean { + switch (func.kind) { + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return true; + case ts.SyntaxKind.MethodDeclaration: + return func.parent.kind === ts.SyntaxKind.ObjectLiteralExpression; + default: + return false; + } + } - /** - * 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: ts.FunctionLikeDeclaration | ts.MethodSignature, returnType: ts.Type | undefined) { - addLazyDiagnostic(checkAllCodePathsInNonVoidFunctionReturnOrThrowDiagnostics); - return; - - function checkAllCodePathsInNonVoidFunctionReturnOrThrowDiagnostics(): void { - const functionFlags = ts.getFunctionFlags(func); - const type = returnType && unwrapReturnType(returnType, functionFlags); + /** + * 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: ts.FunctionLikeDeclaration | ts.MethodSignature, returnType: ts.Type | undefined) { + addLazyDiagnostic(checkAllCodePathsInNonVoidFunctionReturnOrThrowDiagnostics); + return; + + function checkAllCodePathsInNonVoidFunctionReturnOrThrowDiagnostics(): void { + const functionFlags = ts.getFunctionFlags(func); + const type = returnType && unwrapReturnType(returnType, functionFlags); - // Functions with with an explicitly specified 'void' or 'any' return type don't need any return expressions. - if (type && maybeTypeOfKind(type, ts.TypeFlags.Any | ts.TypeFlags.Void)) { - return; - } + // Functions with with an explicitly specified 'void' or 'any' return type don't need any return expressions. + if (type && maybeTypeOfKind(type, ts.TypeFlags.Any | ts.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 === ts.SyntaxKind.MethodSignature || ts.nodeIsMissing(func.body) || func.body!.kind !== ts.SyntaxKind.Block || !functionHasImplicitReturn(func)) { - 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 === ts.SyntaxKind.MethodSignature || ts.nodeIsMissing(func.body) || func.body!.kind !== ts.SyntaxKind.Block || !functionHasImplicitReturn(func)) { + return; + } - const hasExplicitReturn = func.flags & ts.NodeFlags.HasExplicitReturn; - const errorNode = ts.getEffectiveReturnTypeNode(func) || func; - if (type && type.flags & ts.TypeFlags.Never) { - error(errorNode, ts.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. - error(errorNode, ts.Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value); - } - else if (type && strictNullChecks && !isTypeAssignableTo(undefinedType, type)) { - error(errorNode, ts.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; - } + const hasExplicitReturn = func.flags & ts.NodeFlags.HasExplicitReturn; + const errorNode = ts.getEffectiveReturnTypeNode(func) || func; + if (type && type.flags & ts.TypeFlags.Never) { + error(errorNode, ts.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. + error(errorNode, ts.Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value); + } + else if (type && strictNullChecks && !isTypeAssignableTo(undefinedType, type)) { + error(errorNode, ts.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(errorNode, ts.Diagnostics.Not_all_code_paths_return_a_value); } + error(errorNode, ts.Diagnostics.Not_all_code_paths_return_a_value); } } + } - function checkFunctionExpressionOrObjectLiteralMethod(node: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration, checkMode?: CheckMode): ts.Type { - ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); - checkNodeDeferred(node); + function checkFunctionExpressionOrObjectLiteralMethod(node: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration, checkMode?: CheckMode): ts.Type { + ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); + checkNodeDeferred(node); - if (ts.isFunctionExpression(node)) { - checkCollisionsForDeclarationName(node, node.name); - } + if (ts.isFunctionExpression(node)) { + checkCollisionsForDeclarationName(node, node.name); + } - // 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 (!ts.getEffectiveReturnTypeNode(node) && !ts.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, ts.emptyArray, returnType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); - const returnOnlyType = createAnonymousType(node.symbol, emptySymbols, [returnOnlySignature], ts.emptyArray, ts.emptyArray); - returnOnlyType.objectFlags |= ts.ObjectFlags.NonInferrableType; - return links.contextFreeType = returnOnlyType; + // 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 (!ts.getEffectiveReturnTypeNode(node) && !ts.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, ts.emptyArray, returnType, /*resolvedTypePredicate*/ undefined, 0, ts.SignatureFlags.None); + const returnOnlyType = createAnonymousType(node.symbol, emptySymbols, [returnOnlySignature], ts.emptyArray, ts.emptyArray); + returnOnlyType.objectFlags |= ts.ObjectFlags.NonInferrableType; + return links.contextFreeType = returnOnlyType; } - return anyFunctionType; } + return anyFunctionType; + } - // Grammar checking - const hasGrammarError = checkGrammarFunctionLikeDeclaration(node); - if (!hasGrammarError && node.kind === ts.SyntaxKind.FunctionExpression) { - checkGrammarForGenerator(node); - } + // Grammar checking + const hasGrammarError = checkGrammarFunctionLikeDeclaration(node); + if (!hasGrammarError && node.kind === ts.SyntaxKind.FunctionExpression) { + checkGrammarForGenerator(node); + } - contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node, checkMode); + contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node, checkMode); - return getTypeOfSymbol(getSymbolOfNode(node)); - } + return getTypeOfSymbol(getSymbolOfNode(node)); + } - function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration, checkMode?: CheckMode) { - const links = getNodeLinks(node); - // Check if function expression is contextually typed and assign parameter types if so. + function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration, checkMode?: CheckMode) { + const links = getNodeLinks(node); + // Check if function expression is contextually typed and assign parameter types if so. + if (!(links.flags & ts.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 & ts.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 & ts.NodeCheckFlags.ContextChecked)) { - links.flags |= ts.NodeCheckFlags.ContextChecked; - const signature = ts.firstOrUndefined(getSignaturesOfType(getTypeOfSymbol(getSymbolOfNode(node)), ts.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); + links.flags |= ts.NodeCheckFlags.ContextChecked; + const signature = ts.firstOrUndefined(getSignaturesOfType(getTypeOfSymbol(getSymbolOfNode(node)), ts.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); } - if (contextualSignature && !getReturnTypeFromAnnotation(node) && !signature.resolvedReturnType) { - const returnType = getReturnTypeFromBody(node, checkMode); - if (!signature.resolvedReturnType) { - signature.resolvedReturnType = returnType; - } + 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); } + checkSignatureDeclaration(node); } } + } - function checkFunctionExpressionOrObjectLiteralMethodDeferred(node: ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration) { - ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); - const functionFlags = ts.getFunctionFlags(node); - const returnType = getReturnTypeFromAnnotation(node); - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); + function checkFunctionExpressionOrObjectLiteralMethodDeferred(node: ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration) { + ts.Debug.assert(node.kind !== ts.SyntaxKind.MethodDeclaration || ts.isObjectLiteralMethod(node)); + const functionFlags = ts.getFunctionFlags(node); + const returnType = getReturnTypeFromAnnotation(node); + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); - if (node.body) { - if (!ts.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) { + if (!ts.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 === ts.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 = returnType && unwrapReturnType(returnType, functionFlags); - if (returnOrPromisedType) { - if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async) { // Async function - const awaitedType = checkAwaitedType(exprType, /*withAlias*/ false, node.body, ts.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); - } + if (node.body.kind === ts.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 = returnType && unwrapReturnType(returnType, functionFlags); + if (returnOrPromisedType) { + if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async) { // Async function + const awaitedType = checkAwaitedType(exprType, /*withAlias*/ false, node.body, ts.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); } } } } + } - function checkArithmeticOperandType(operand: ts.Node, type: ts.Type, diagnostic: ts.DiagnosticMessage, isAwaitValid = false): boolean { - if (!isTypeAssignableTo(type, numberOrBigIntType)) { - const awaitedType = isAwaitValid && getAwaitedTypeOfPromise(type); - errorAndMaybeSuggestAwait(operand, !!awaitedType && isTypeAssignableTo(awaitedType, numberOrBigIntType), diagnostic); - return false; - } - return true; + function checkArithmeticOperandType(operand: ts.Node, type: ts.Type, diagnostic: ts.DiagnosticMessage, isAwaitValid = false): boolean { + if (!isTypeAssignableTo(type, numberOrBigIntType)) { + const awaitedType = isAwaitValid && getAwaitedTypeOfPromise(type); + errorAndMaybeSuggestAwait(operand, !!awaitedType && isTypeAssignableTo(awaitedType, numberOrBigIntType), diagnostic); + return false; } + return true; + } - function isReadonlyAssignmentDeclaration(d: ts.Declaration) { - if (!ts.isCallExpression(d)) { - return false; - } - if (!ts.isBindableObjectDefinePropertyCall(d)) { - return false; - } - const objectLitType = checkExpressionCached(d.arguments[2]); - const valueType = getTypeOfPropertyOfType(objectLitType, "value" as ts.__String); - if (valueType) { - const writableProp = getPropertyOfType(objectLitType, "writable" as ts.__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 && ts.isPropertyAssignment(writableProp.valueDeclaration)) { - const initializer = writableProp.valueDeclaration.initializer; - const rawOriginalType = checkExpression(initializer); - if (rawOriginalType === falseType || rawOriginalType === regularFalseType) { - return true; - } - } - return false; - } - const setProp = getPropertyOfType(objectLitType, "set" as ts.__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 !!(ts.getCheckFlags(symbol) & ts.CheckFlags.Readonly || - symbol.flags & ts.SymbolFlags.Property && ts.getDeclarationModifierFlagsFromSymbol(symbol) & ts.ModifierFlags.Readonly || - symbol.flags & ts.SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const || - symbol.flags & ts.SymbolFlags.Accessor && !(symbol.flags & ts.SymbolFlags.SetAccessor) || - symbol.flags & ts.SymbolFlags.EnumMember || - ts.some(symbol.declarations, isReadonlyAssignmentDeclaration)); - } - function isAssignmentToReadonlyEntity(expr: ts.Expression, symbol: ts.Symbol, assignmentKind: ts.AssignmentKind) { - if (assignmentKind === ts.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 & ts.SymbolFlags.Property && - ts.isAccessExpression(expr) && - expr.expression.kind === ts.SyntaxKind.ThisKeyword) { - // Look for if this is the constructor for the class that `symbol` is a property of. - const ctor = ts.getContainingFunction(expr); - if (!(ctor && (ctor.kind === ts.SyntaxKind.Constructor || isJSConstructor(ctor)))) { - return true; - } - if (symbol.valueDeclaration) { - const isAssignmentDeclaration = ts.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; - } - } + function isReadonlyAssignmentDeclaration(d: ts.Declaration) { + if (!ts.isCallExpression(d)) { + return false; + } + if (!ts.isBindableObjectDefinePropertyCall(d)) { + return false; + } + const objectLitType = checkExpressionCached(d.arguments[2]); + const valueType = getTypeOfPropertyOfType(objectLitType, "value" as ts.__String); + if (valueType) { + const writableProp = getPropertyOfType(objectLitType, "writable" as ts.__String); + const writableType = writableProp && getTypeOfSymbol(writableProp); + if (!writableType || writableType === falseType || writableType === regularFalseType) { return true; } - if (ts.isAccessExpression(expr)) { - // references through namespace import should be readonly - const node = ts.skipParentheses(expr.expression); - if (node.kind === ts.SyntaxKind.Identifier) { - const symbol = getNodeLinks(node).resolvedSymbol!; - if (symbol.flags & ts.SymbolFlags.Alias) { - const declaration = getDeclarationOfAliasSymbol(symbol); - return !!declaration && declaration.kind === ts.SyntaxKind.NamespaceImport; - } + // 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 && ts.isPropertyAssignment(writableProp.valueDeclaration)) { + const initializer = writableProp.valueDeclaration.initializer; + const rawOriginalType = checkExpression(initializer); + if (rawOriginalType === falseType || rawOriginalType === regularFalseType) { + return true; } } return false; } + const setProp = getPropertyOfType(objectLitType, "set" as ts.__String); + return !setProp; + } - function checkReferenceExpression(expr: ts.Expression, invalidReferenceMessage: ts.DiagnosticMessage, invalidOptionalChainMessage: ts.DiagnosticMessage): boolean { - // References are combinations of identifiers, parentheses, and property accesses. - const node = ts.skipOuterExpressions(expr, ts.OuterExpressionKinds.Assertions | ts.OuterExpressionKinds.Parentheses); - if (node.kind !== ts.SyntaxKind.Identifier && !ts.isAccessExpression(node)) { - error(expr, invalidReferenceMessage); - return false; - } - if (node.flags & ts.NodeFlags.OptionalChain) { - error(expr, invalidOptionalChainMessage); - return false; + 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 !!(ts.getCheckFlags(symbol) & ts.CheckFlags.Readonly || + symbol.flags & ts.SymbolFlags.Property && ts.getDeclarationModifierFlagsFromSymbol(symbol) & ts.ModifierFlags.Readonly || + symbol.flags & ts.SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const || + symbol.flags & ts.SymbolFlags.Accessor && !(symbol.flags & ts.SymbolFlags.SetAccessor) || + symbol.flags & ts.SymbolFlags.EnumMember || + ts.some(symbol.declarations, isReadonlyAssignmentDeclaration)); + } + function isAssignmentToReadonlyEntity(expr: ts.Expression, symbol: ts.Symbol, assignmentKind: ts.AssignmentKind) { + if (assignmentKind === ts.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 & ts.SymbolFlags.Property && + ts.isAccessExpression(expr) && + expr.expression.kind === ts.SyntaxKind.ThisKeyword) { + // Look for if this is the constructor for the class that `symbol` is a property of. + const ctor = ts.getContainingFunction(expr); + if (!(ctor && (ctor.kind === ts.SyntaxKind.Constructor || isJSConstructor(ctor)))) { + return true; + } + if (symbol.valueDeclaration) { + const isAssignmentDeclaration = ts.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; + } } return true; } - - function checkDeleteExpression(node: ts.DeleteExpression): ts.Type { - checkExpression(node.expression); - const expr = ts.skipParentheses(node.expression); - if (!ts.isAccessExpression(expr)) { - error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference); - return booleanType; - } - if (ts.isPropertyAccessExpression(expr) && ts.isPrivateIdentifier(expr.name)) { - error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier); - } - const links = getNodeLinks(expr); - const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol); - if (symbol) { - if (isReadonlySymbol(symbol)) { - error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_read_only_property); + if (ts.isAccessExpression(expr)) { + // references through namespace import should be readonly + const node = ts.skipParentheses(expr.expression); + if (node.kind === ts.SyntaxKind.Identifier) { + const symbol = getNodeLinks(node).resolvedSymbol!; + if (symbol.flags & ts.SymbolFlags.Alias) { + const declaration = getDeclarationOfAliasSymbol(symbol); + return !!declaration && declaration.kind === ts.SyntaxKind.NamespaceImport; } - checkDeleteExpressionMustBeOptional(expr, symbol); } - return booleanType; } + return false; + } - function checkDeleteExpressionMustBeOptional(expr: ts.AccessExpression, symbol: ts.Symbol) { - const type = getTypeOfSymbol(symbol); - if (strictNullChecks && - !(type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never)) && - !(exactOptionalPropertyTypes ? symbol.flags & ts.SymbolFlags.Optional : getFalsyFlags(type) & ts.TypeFlags.Undefined)) { - error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_must_be_optional); - } + function checkReferenceExpression(expr: ts.Expression, invalidReferenceMessage: ts.DiagnosticMessage, invalidOptionalChainMessage: ts.DiagnosticMessage): boolean { + // References are combinations of identifiers, parentheses, and property accesses. + const node = ts.skipOuterExpressions(expr, ts.OuterExpressionKinds.Assertions | ts.OuterExpressionKinds.Parentheses); + if (node.kind !== ts.SyntaxKind.Identifier && !ts.isAccessExpression(node)) { + error(expr, invalidReferenceMessage); + return false; + } + if (node.flags & ts.NodeFlags.OptionalChain) { + error(expr, invalidOptionalChainMessage); + return false; } + return true; + } - function checkTypeOfExpression(node: ts.TypeOfExpression): ts.Type { - checkExpression(node.expression); - return typeofType; + function checkDeleteExpression(node: ts.DeleteExpression): ts.Type { + checkExpression(node.expression); + const expr = ts.skipParentheses(node.expression); + if (!ts.isAccessExpression(expr)) { + error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference); + return booleanType; + } + if (ts.isPropertyAccessExpression(expr) && ts.isPrivateIdentifier(expr.name)) { + error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier); + } + const links = getNodeLinks(expr); + const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol); + if (symbol) { + if (isReadonlySymbol(symbol)) { + error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_read_only_property); + } + checkDeleteExpressionMustBeOptional(expr, symbol); } + return booleanType; + } - function checkVoidExpression(node: ts.VoidExpression): ts.Type { - checkExpression(node.expression); - return undefinedWideningType; + function checkDeleteExpressionMustBeOptional(expr: ts.AccessExpression, symbol: ts.Symbol) { + const type = getTypeOfSymbol(symbol); + if (strictNullChecks && + !(type.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Never)) && + !(exactOptionalPropertyTypes ? symbol.flags & ts.SymbolFlags.Optional : getFalsyFlags(type) & ts.TypeFlags.Undefined)) { + error(expr, ts.Diagnostics.The_operand_of_a_delete_operator_must_be_optional); } + } - function checkAwaitExpressionGrammar(node: ts.AwaitExpression): void { - // Grammar checking - const container = ts.getContainingFunctionOrClassStaticBlock(node); - if (container && ts.isClassStaticBlockDeclaration(container)) { - error(node, ts.Diagnostics.Await_expression_cannot_be_used_inside_a_class_static_block); - } - else if (!(node.flags & ts.NodeFlags.AwaitContext)) { - if (ts.isInTopLevelContext(node)) { - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - let span: ts.TextSpan | undefined; - if (!ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { - span ??= ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - const diagnostic = ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.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); - } - switch (moduleKind) { - case ts.ModuleKind.Node16: - case ts.ModuleKind.NodeNext: - if (sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS) { - span ??= ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level)); - break; - } - // fallthrough - case ts.ModuleKind.ES2022: - case ts.ModuleKind.ESNext: - case ts.ModuleKind.System: - if (languageVersion >= ts.ScriptTarget.ES2017) { - break; - } - // fallthrough - default: + function checkTypeOfExpression(node: ts.TypeOfExpression): ts.Type { + checkExpression(node.expression); + return typeofType; + } + + function checkVoidExpression(node: ts.VoidExpression): ts.Type { + checkExpression(node.expression); + return undefinedWideningType; + } + + function checkAwaitExpressionGrammar(node: ts.AwaitExpression): void { + // Grammar checking + const container = ts.getContainingFunctionOrClassStaticBlock(node); + if (container && ts.isClassStaticBlockDeclaration(container)) { + error(node, ts.Diagnostics.Await_expression_cannot_be_used_inside_a_class_static_block); + } + else if (!(node.flags & ts.NodeFlags.AwaitContext)) { + if (ts.isInTopLevelContext(node)) { + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + let span: ts.TextSpan | undefined; + if (!ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { + span ??= ts.getSpanOfTokenAtPosition(sourceFile, node.pos); + const diagnostic = ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.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); + } + switch (moduleKind) { + case ts.ModuleKind.Node16: + case ts.ModuleKind.NodeNext: + if (sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS) { span ??= ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_or_nodenext_and_the_target_option_is_set_to_es2017_or_higher)); + diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level)); break; - } + } + // fallthrough + case ts.ModuleKind.ES2022: + case ts.ModuleKind.ESNext: + case ts.ModuleKind.System: + if (languageVersion >= ts.ScriptTarget.ES2017) { + break; + } + // fallthrough + default: + span ??= ts.getSpanOfTokenAtPosition(sourceFile, node.pos); + diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_or_nodenext_and_the_target_option_is_set_to_es2017_or_higher)); + break; } } - else { - // use of 'await' in non-async function - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - const diagnostic = ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); - if (container && container.kind !== ts.SyntaxKind.Constructor && (ts.getFunctionFlags(container) & ts.FunctionFlags.Async) === 0) { - const relatedInfo = ts.createDiagnosticForNode(container, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async); - ts.addRelatedInfo(diagnostic, relatedInfo); - } - diagnostics.add(diagnostic); + } + else { + // use of 'await' in non-async function + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); + const diagnostic = ts.createFileDiagnostic(sourceFile, span.start, span.length, ts.Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); + if (container && container.kind !== ts.SyntaxKind.Constructor && (ts.getFunctionFlags(container) & ts.FunctionFlags.Async) === 0) { + const relatedInfo = ts.createDiagnosticForNode(container, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async); + ts.addRelatedInfo(diagnostic, relatedInfo); } + diagnostics.add(diagnostic); } } + } - if (isInParameterInitializerBeforeContainingFunction(node)) { - error(node, ts.Diagnostics.await_expressions_cannot_be_used_in_a_parameter_initializer); - } + if (isInParameterInitializerBeforeContainingFunction(node)) { + error(node, ts.Diagnostics.await_expressions_cannot_be_used_in_a_parameter_initializer); } + } - function checkAwaitExpression(node: ts.AwaitExpression): ts.Type { - addLazyDiagnostic(() => checkAwaitExpressionGrammar(node)); + function checkAwaitExpression(node: ts.AwaitExpression): ts.Type { + addLazyDiagnostic(() => checkAwaitExpressionGrammar(node)); - const operandType = checkExpression(node.expression); - const awaitedType = checkAwaitedType(operandType, /*withAlias*/ true, node, ts.Diagnostics.Type_of_await_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); - if (awaitedType === operandType && !isErrorType(awaitedType) && !(operandType.flags & ts.TypeFlags.AnyOrUnknown)) { - addErrorOrSuggestion(/*isError*/ false, ts.createDiagnosticForNode(node, ts.Diagnostics.await_has_no_effect_on_the_type_of_this_expression)); - } - return awaitedType; + const operandType = checkExpression(node.expression); + const awaitedType = checkAwaitedType(operandType, /*withAlias*/ true, node, ts.Diagnostics.Type_of_await_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + if (awaitedType === operandType && !isErrorType(awaitedType) && !(operandType.flags & ts.TypeFlags.AnyOrUnknown)) { + addErrorOrSuggestion(/*isError*/ false, ts.createDiagnosticForNode(node, ts.Diagnostics.await_has_no_effect_on_the_type_of_this_expression)); } + return awaitedType; + } - function checkPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ts.Type { - const operandType = checkExpression(node.operand); - if (operandType === silentNeverType) { - return silentNeverType; - } - switch (node.operand.kind) { - case ts.SyntaxKind.NumericLiteral: - switch (node.operator) { - case ts.SyntaxKind.MinusToken: - return getFreshTypeOfLiteralType(getNumberLiteralType(-(node.operand as ts.NumericLiteral).text)); - case ts.SyntaxKind.PlusToken: - return getFreshTypeOfLiteralType(getNumberLiteralType(+(node.operand as ts.NumericLiteral).text)); - } - break; - case ts.SyntaxKind.BigIntLiteral: - if (node.operator === ts.SyntaxKind.MinusToken) { - return getFreshTypeOfLiteralType(getBigIntLiteralType({ - negative: true, - base10Value: ts.parsePseudoBigInt((node.operand as ts.BigIntLiteral).text) - })); + function checkPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ts.Type { + const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; + } + switch (node.operand.kind) { + case ts.SyntaxKind.NumericLiteral: + switch (node.operator) { + case ts.SyntaxKind.MinusToken: + return getFreshTypeOfLiteralType(getNumberLiteralType(-(node.operand as ts.NumericLiteral).text)); + case ts.SyntaxKind.PlusToken: + return getFreshTypeOfLiteralType(getNumberLiteralType(+(node.operand as ts.NumericLiteral).text)); + } + break; + case ts.SyntaxKind.BigIntLiteral: + if (node.operator === ts.SyntaxKind.MinusToken) { + return getFreshTypeOfLiteralType(getBigIntLiteralType({ + negative: true, + base10Value: ts.parsePseudoBigInt((node.operand as ts.BigIntLiteral).text) + })); + } + } + switch (node.operator) { + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.TildeToken: + checkNonNullType(operandType, node.operand); + if (maybeTypeOfKindConsideringBaseConstraint(operandType, ts.TypeFlags.ESSymbolLike)) { + error(node.operand, ts.Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, ts.tokenToString(node.operator)); + } + if (node.operator === ts.SyntaxKind.PlusToken) { + if (maybeTypeOfKind(operandType, ts.TypeFlags.BigIntLike)) { + error(node.operand, ts.Diagnostics.Operator_0_cannot_be_applied_to_type_1, ts.tokenToString(node.operator), typeToString(getBaseTypeOfLiteralType(operandType))); } - } - switch (node.operator) { - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.TildeToken: - checkNonNullType(operandType, node.operand); - if (maybeTypeOfKindConsideringBaseConstraint(operandType, ts.TypeFlags.ESSymbolLike)) { - error(node.operand, ts.Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, ts.tokenToString(node.operator)); - } - if (node.operator === ts.SyntaxKind.PlusToken) { - if (maybeTypeOfKind(operandType, ts.TypeFlags.BigIntLike)) { - error(node.operand, ts.Diagnostics.Operator_0_cannot_be_applied_to_type_1, ts.tokenToString(node.operator), typeToString(getBaseTypeOfLiteralType(operandType))); - } - return numberType; - } - return getUnaryResultType(operandType); - case ts.SyntaxKind.ExclamationToken: - checkTruthinessExpression(node.operand); - const facts = getTypeFacts(operandType) & (TypeFacts.Truthy | TypeFacts.Falsy); - return facts === TypeFacts.Truthy ? falseType : - facts === TypeFacts.Falsy ? trueType : - booleanType; - case ts.SyntaxKind.PlusPlusToken: - case ts.SyntaxKind.MinusMinusToken: - const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), ts.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, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); - } - return getUnaryResultType(operandType); - } - return errorType; + return numberType; + } + return getUnaryResultType(operandType); + case ts.SyntaxKind.ExclamationToken: + checkTruthinessExpression(node.operand); + const facts = getTypeFacts(operandType) & (TypeFacts.Truthy | TypeFacts.Falsy); + return facts === TypeFacts.Truthy ? falseType : + facts === TypeFacts.Falsy ? trueType : + booleanType; + case ts.SyntaxKind.PlusPlusToken: + case ts.SyntaxKind.MinusMinusToken: + const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), ts.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, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); + } + return getUnaryResultType(operandType); + } + return errorType; + } + + function checkPostfixUnaryExpression(node: ts.PostfixUnaryExpression): ts.Type { + const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; } + const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), ts.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, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); + } + return getUnaryResultType(operandType); + } - function checkPostfixUnaryExpression(node: ts.PostfixUnaryExpression): ts.Type { - const operandType = checkExpression(node.operand); - if (operandType === silentNeverType) { - return silentNeverType; - } - const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), ts.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, ts.Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, ts.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, ts.TypeFlags.BigIntLike)) { + return isTypeAssignableToKind(operandType, ts.TypeFlags.AnyOrUnknown) || maybeTypeOfKind(operandType, ts.TypeFlags.NumberLike) + ? numberOrBigIntType + : bigintType; } + // If it's not a bigint type, implicit coercion will result in a number + return numberType; + } - function getUnaryResultType(operandType: ts.Type): ts.Type { - if (maybeTypeOfKind(operandType, ts.TypeFlags.BigIntLike)) { - return isTypeAssignableToKind(operandType, ts.TypeFlags.AnyOrUnknown) || maybeTypeOfKind(operandType, ts.TypeFlags.NumberLike) - ? numberOrBigIntType - : bigintType; - } - // If it's not a bigint type, implicit coercion will result in a number - return numberType; + function maybeTypeOfKindConsideringBaseConstraint(type: ts.Type, kind: ts.TypeFlags): boolean { + if (maybeTypeOfKind(type, kind)) { + return true; } - function maybeTypeOfKindConsideringBaseConstraint(type: ts.Type, kind: ts.TypeFlags): boolean { - if (maybeTypeOfKind(type, kind)) { - return true; - } + const baseConstraint = getBaseConstraintOrType(type); + return !!baseConstraint && maybeTypeOfKind(baseConstraint, kind); + } - const baseConstraint = getBaseConstraintOrType(type); - return !!baseConstraint && maybeTypeOfKind(baseConstraint, kind); + // 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: ts.TypeFlags): boolean { + if (type.flags & kind) { + return true; } - - // 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: ts.TypeFlags): boolean { - if (type.flags & kind) { - return true; - } - if (type.flags & ts.TypeFlags.UnionOrIntersection) { - const types = (type as ts.UnionOrIntersectionType).types; - for (const t of types) { - if (maybeTypeOfKind(t, kind)) { - return true; - } + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + const types = (type as ts.UnionOrIntersectionType).types; + for (const t of types) { + if (maybeTypeOfKind(t, kind)) { + return true; } } - return false; } + return false; + } - function isTypeAssignableToKind(source: ts.Type, kind: ts.TypeFlags, strict?: boolean): boolean { - if (source.flags & kind) { - return true; - } - if (strict && source.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null)) { - return false; - } - return !!(kind & ts.TypeFlags.NumberLike) && isTypeAssignableTo(source, numberType) || - !!(kind & ts.TypeFlags.BigIntLike) && isTypeAssignableTo(source, bigintType) || - !!(kind & ts.TypeFlags.StringLike) && isTypeAssignableTo(source, stringType) || - !!(kind & ts.TypeFlags.BooleanLike) && isTypeAssignableTo(source, booleanType) || - !!(kind & ts.TypeFlags.Void) && isTypeAssignableTo(source, voidType) || - !!(kind & ts.TypeFlags.Never) && isTypeAssignableTo(source, neverType) || - !!(kind & ts.TypeFlags.Null) && isTypeAssignableTo(source, nullType) || - !!(kind & ts.TypeFlags.Undefined) && isTypeAssignableTo(source, undefinedType) || - !!(kind & ts.TypeFlags.ESSymbol) && isTypeAssignableTo(source, esSymbolType) || - !!(kind & ts.TypeFlags.NonPrimitive) && isTypeAssignableTo(source, nonPrimitiveType); + function isTypeAssignableToKind(source: ts.Type, kind: ts.TypeFlags, strict?: boolean): boolean { + if (source.flags & kind) { + return true; } - - function allTypesAssignableToKind(source: ts.Type, kind: ts.TypeFlags, strict?: boolean): boolean { - return source.flags & ts.TypeFlags.Union ? - ts.every((source as ts.UnionType).types, subType => allTypesAssignableToKind(subType, kind, strict)) : - isTypeAssignableToKind(source, kind, strict); + if (strict && source.flags & (ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null)) { + return false; } + return !!(kind & ts.TypeFlags.NumberLike) && isTypeAssignableTo(source, numberType) || + !!(kind & ts.TypeFlags.BigIntLike) && isTypeAssignableTo(source, bigintType) || + !!(kind & ts.TypeFlags.StringLike) && isTypeAssignableTo(source, stringType) || + !!(kind & ts.TypeFlags.BooleanLike) && isTypeAssignableTo(source, booleanType) || + !!(kind & ts.TypeFlags.Void) && isTypeAssignableTo(source, voidType) || + !!(kind & ts.TypeFlags.Never) && isTypeAssignableTo(source, neverType) || + !!(kind & ts.TypeFlags.Null) && isTypeAssignableTo(source, nullType) || + !!(kind & ts.TypeFlags.Undefined) && isTypeAssignableTo(source, undefinedType) || + !!(kind & ts.TypeFlags.ESSymbol) && isTypeAssignableTo(source, esSymbolType) || + !!(kind & ts.TypeFlags.NonPrimitive) && isTypeAssignableTo(source, nonPrimitiveType); + } - function isConstEnumObjectType(type: ts.Type): boolean { - return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous) && !!type.symbol && isConstEnumSymbol(type.symbol); - } + function allTypesAssignableToKind(source: ts.Type, kind: ts.TypeFlags, strict?: boolean): boolean { + return source.flags & ts.TypeFlags.Union ? + ts.every((source as ts.UnionType).types, subType => allTypesAssignableToKind(subType, kind, strict)) : + isTypeAssignableToKind(source, kind, strict); + } - function isConstEnumSymbol(symbol: ts.Symbol): boolean { - return (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0; - } + function isConstEnumObjectType(type: ts.Type): boolean { + return !!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous) && !!type.symbol && isConstEnumSymbol(type.symbol); + } - function checkInstanceOfExpression(left: ts.Expression, right: ts.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, ts.TypeFlags.Primitive)) { - error(left, ts.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, ts.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 isConstEnumSymbol(symbol: ts.Symbol): boolean { + return (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0; + } + + function checkInstanceOfExpression(left: ts.Expression, right: ts.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, ts.TypeFlags.Primitive)) { + error(left, ts.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, ts.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: ts.Expression, right: ts.Expression, leftType: ts.Type, rightType: ts.Type): ts.Type { - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - if (ts.isPrivateIdentifier(left)) { - if (languageVersion < ts.ScriptTarget.ESNext) { - checkExternalEmitHelpers(left, ts.ExternalEmitHelpers.ClassPrivateFieldIn); - } - // Unlike in 'checkPrivateIdentifierExpression' we now have access to the RHS type - // which provides us with the opportunity to emit more detailed errors - if (!getNodeLinks(left).resolvedSymbol && ts.getContainingClass(left)) { - const isUncheckedJS = isUncheckedJSSuggestion(left, rightType.symbol, /*excludeClasses*/ true); - reportNonexistentProperty(left, rightType, isUncheckedJS); - } + function checkInExpression(left: ts.Expression, right: ts.Expression, leftType: ts.Type, rightType: ts.Type): ts.Type { + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } + if (ts.isPrivateIdentifier(left)) { + if (languageVersion < ts.ScriptTarget.ESNext) { + checkExternalEmitHelpers(left, ts.ExternalEmitHelpers.ClassPrivateFieldIn); } - else { - leftType = checkNonNullType(leftType, left); - // TypeScript 1.0 spec (April 2014): 4.15.5 - // Require the left operand to be of type Any, the String primitive type, or the Number primitive type. - if (!(allTypesAssignableToKind(leftType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike) || - isTypeAssignableToKind(leftType, ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping | ts.TypeFlags.TypeParameter))) { - error(left, ts.Diagnostics.The_left_hand_side_of_an_in_expression_must_be_a_private_identifier_or_of_type_any_string_number_or_symbol); - } + // Unlike in 'checkPrivateIdentifierExpression' we now have access to the RHS type + // which provides us with the opportunity to emit more detailed errors + if (!getNodeLinks(left).resolvedSymbol && ts.getContainingClass(left)) { + const isUncheckedJS = isUncheckedJSSuggestion(left, rightType.symbol, /*excludeClasses*/ true); + reportNonexistentProperty(left, rightType, isUncheckedJS); } - rightType = checkNonNullType(rightType, right); + } + else { + leftType = checkNonNullType(leftType, left); // TypeScript 1.0 spec (April 2014): 4.15.5 - // The in operator requires the right operand to be - // - // 1. assignable to the non-primitive type, - // 2. an unconstrained type parameter, - // 3. a union or intersection including one or more type parameters, whose constituents are all assignable to the - // the non-primitive type, or are unconstrainted type parameters, or have constraints assignable to the - // non-primitive type, or - // 4. a type parameter whose constraint is - // i. an object type, - // ii. the non-primitive type, or - // iii. a union or intersection with at least one constituent assignable to an object or non-primitive type. - // - // The divergent behavior for type parameters and unions containing type parameters is a workaround for type - // parameters not being narrowable. If the right operand is a concrete type, we can error if there is any chance - // it is a primitive. But if the operand is a type parameter, it cannot be narrowed, so we don't issue an error - // unless *all* instantiations would result in an error. - // - // The result is always of the Boolean primitive type. - const rightTypeConstraint = getConstraintOfType(rightType); - if (!allTypesAssignableToKind(rightType, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive) || - rightTypeConstraint && (isTypeAssignableToKind(rightType, ts.TypeFlags.UnionOrIntersection) && !allTypesAssignableToKind(rightTypeConstraint, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive) || - !maybeTypeOfKind(rightTypeConstraint, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Object))) { - error(right, ts.Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive); + // Require the left operand to be of type Any, the String primitive type, or the Number primitive type. + if (!(allTypesAssignableToKind(leftType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike) || + isTypeAssignableToKind(leftType, ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping | ts.TypeFlags.TypeParameter))) { + error(left, ts.Diagnostics.The_left_hand_side_of_an_in_expression_must_be_a_private_identifier_or_of_type_any_string_number_or_symbol); } - return booleanType; } + rightType = checkNonNullType(rightType, right); + // TypeScript 1.0 spec (April 2014): 4.15.5 + // The in operator requires the right operand to be + // + // 1. assignable to the non-primitive type, + // 2. an unconstrained type parameter, + // 3. a union or intersection including one or more type parameters, whose constituents are all assignable to the + // the non-primitive type, or are unconstrainted type parameters, or have constraints assignable to the + // non-primitive type, or + // 4. a type parameter whose constraint is + // i. an object type, + // ii. the non-primitive type, or + // iii. a union or intersection with at least one constituent assignable to an object or non-primitive type. + // + // The divergent behavior for type parameters and unions containing type parameters is a workaround for type + // parameters not being narrowable. If the right operand is a concrete type, we can error if there is any chance + // it is a primitive. But if the operand is a type parameter, it cannot be narrowed, so we don't issue an error + // unless *all* instantiations would result in an error. + // + // The result is always of the Boolean primitive type. + const rightTypeConstraint = getConstraintOfType(rightType); + if (!allTypesAssignableToKind(rightType, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive) || + rightTypeConstraint && (isTypeAssignableToKind(rightType, ts.TypeFlags.UnionOrIntersection) && !allTypesAssignableToKind(rightTypeConstraint, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive) || + !maybeTypeOfKind(rightTypeConstraint, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Object))) { + error(right, ts.Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive); + } + return booleanType; + } - function checkObjectLiteralAssignment(node: ts.ObjectLiteralExpression, sourceType: ts.Type, rightIsThis?: boolean): ts.Type { - const properties = node.properties; - if (strictNullChecks && properties.length === 0) { - return checkNonNullType(sourceType, node); - } - for (let i = 0; i < properties.length; i++) { - checkObjectLiteralDestructuringPropertyAssignment(node, sourceType, i, properties, rightIsThis); - } - return sourceType; + function checkObjectLiteralAssignment(node: ts.ObjectLiteralExpression, sourceType: ts.Type, rightIsThis?: boolean): ts.Type { + const properties = node.properties; + if (strictNullChecks && properties.length === 0) { + return checkNonNullType(sourceType, node); } + 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: ts.ObjectLiteralExpression, objectLiteralType: ts.Type, propertyIndex: number, allProperties?: ts.NodeArray, rightIsThis = false) { - const properties = node.properties; - const property = properties[propertyIndex]; - if (property.kind === ts.SyntaxKind.PropertyAssignment || property.kind === ts.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, /*writing*/ true, objectLiteralType, prop); - } + /** Note: If property cannot be a SpreadAssignment, then allProperties does not need to be provided */ + function checkObjectLiteralDestructuringPropertyAssignment(node: ts.ObjectLiteralExpression, objectLiteralType: ts.Type, propertyIndex: number, allProperties?: ts.NodeArray, rightIsThis = false) { + const properties = node.properties; + const property = properties[propertyIndex]; + if (property.kind === ts.SyntaxKind.PropertyAssignment || property.kind === ts.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, /*writing*/ true, objectLiteralType, prop); } - const elementType = getIndexedAccessType(objectLiteralType, exprType, ts.AccessFlags.ExpressionPosition, name); - const type = getFlowTypeOfDestructuring(property, elementType); - return checkDestructuringAssignment(property.kind === ts.SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); } - else if (property.kind === ts.SyntaxKind.SpreadAssignment) { - if (propertyIndex < properties.length - 1) { - error(property, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); + const elementType = getIndexedAccessType(objectLiteralType, exprType, ts.AccessFlags.ExpressionPosition, name); + const type = getFlowTypeOfDestructuring(property, elementType); + return checkDestructuringAssignment(property.kind === ts.SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); + } + else if (property.kind === ts.SyntaxKind.SpreadAssignment) { + if (propertyIndex < properties.length - 1) { + error(property, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); + } + else { + if (languageVersion < ts.ScriptTarget.ESNext) { + checkExternalEmitHelpers(property, ts.ExternalEmitHelpers.Rest); } - else { - if (languageVersion < ts.ScriptTarget.ESNext) { - checkExternalEmitHelpers(property, ts.ExternalEmitHelpers.Rest); - } - const nonRestNames: ts.PropertyName[] = []; - if (allProperties) { - for (const otherProperty of allProperties) { - if (!ts.isSpreadAssignment(otherProperty)) { - nonRestNames.push(otherProperty.name); - } + const nonRestNames: ts.PropertyName[] = []; + if (allProperties) { + for (const otherProperty of allProperties) { + if (!ts.isSpreadAssignment(otherProperty)) { + nonRestNames.push(otherProperty.name); } } - const type = getRestType(objectLiteralType, nonRestNames, objectLiteralType.symbol); - checkGrammarForDisallowedTrailingComma(allProperties, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - return checkDestructuringAssignment(property.expression, type); } + const type = getRestType(objectLiteralType, nonRestNames, objectLiteralType.symbol); + checkGrammarForDisallowedTrailingComma(allProperties, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + return checkDestructuringAssignment(property.expression, type); } - else { - error(property, ts.Diagnostics.Property_assignment_expected); + } + else { + error(property, ts.Diagnostics.Property_assignment_expected); + } + } + + function checkArrayLiteralAssignment(node: ts.ArrayLiteralExpression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { + const elements = node.elements; + if (languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ts.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 possiblyOutOfBoundsType = checkIteratedTypeOrElementType(IterationUse.Destructuring | IterationUse.PossiblyOutOfBounds, sourceType, undefinedType, node) || errorType; + let inBoundsType: ts.Type | undefined = compilerOptions.noUncheckedIndexedAccess ? undefined : possiblyOutOfBoundsType; + for (let i = 0; i < elements.length; i++) { + let type = possiblyOutOfBoundsType; + if (node.elements[i].kind === ts.SyntaxKind.SpreadElement) { + type = inBoundsType = inBoundsType ?? (checkIteratedTypeOrElementType(IterationUse.Destructuring, sourceType, undefinedType, node) || errorType); } + checkArrayLiteralDestructuringElementAssignment(node, sourceType, i, type, checkMode); } + return sourceType; + } - function checkArrayLiteralAssignment(node: ts.ArrayLiteralExpression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { - const elements = node.elements; - if (languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Read); + function checkArrayLiteralDestructuringElementAssignment(node: ts.ArrayLiteralExpression, sourceType: ts.Type, elementIndex: number, elementType: ts.Type, checkMode?: CheckMode) { + const elements = node.elements; + const element = elements[elementIndex]; + if (element.kind !== ts.SyntaxKind.OmittedExpression) { + if (element.kind !== ts.SyntaxKind.SpreadElement) { + const indexType = getNumberLiteralType(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 = ts.AccessFlags.ExpressionPosition | (hasDefaultValue(element) ? ts.AccessFlags.NoTupleBoundsCheck : 0); + const elementType = getIndexedAccessTypeOrUndefined(sourceType, indexType, accessFlags, createSyntheticExpression(element, indexType)) || 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, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); } - // 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 possiblyOutOfBoundsType = checkIteratedTypeOrElementType(IterationUse.Destructuring | IterationUse.PossiblyOutOfBounds, sourceType, undefinedType, node) || errorType; - let inBoundsType: ts.Type | undefined = compilerOptions.noUncheckedIndexedAccess ? undefined : possiblyOutOfBoundsType; - for (let i = 0; i < elements.length; i++) { - let type = possiblyOutOfBoundsType; - if (node.elements[i].kind === ts.SyntaxKind.SpreadElement) { - type = inBoundsType = inBoundsType ?? (checkIteratedTypeOrElementType(IterationUse.Destructuring, sourceType, undefinedType, node) || errorType); - } - checkArrayLiteralDestructuringElementAssignment(node, sourceType, i, type, checkMode); - } - return sourceType; - } - - function checkArrayLiteralDestructuringElementAssignment(node: ts.ArrayLiteralExpression, sourceType: ts.Type, elementIndex: number, elementType: ts.Type, checkMode?: CheckMode) { - const elements = node.elements; - const element = elements[elementIndex]; - if (element.kind !== ts.SyntaxKind.OmittedExpression) { - if (element.kind !== ts.SyntaxKind.SpreadElement) { - const indexType = getNumberLiteralType(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 = ts.AccessFlags.ExpressionPosition | (hasDefaultValue(element) ? ts.AccessFlags.NoTupleBoundsCheck : 0); - const elementType = getIndexedAccessTypeOrUndefined(sourceType, indexType, accessFlags, createSyntheticExpression(element, indexType)) || 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, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); + else { + const restExpression = (element as ts.SpreadElement).expression; + if (restExpression.kind === ts.SyntaxKind.BinaryExpression && (restExpression as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { + error((restExpression as ts.BinaryExpression).operatorToken, ts.Diagnostics.A_rest_element_cannot_have_an_initializer); } else { - const restExpression = (element as ts.SpreadElement).expression; - if (restExpression.kind === ts.SyntaxKind.BinaryExpression && (restExpression as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { - error((restExpression as ts.BinaryExpression).operatorToken, ts.Diagnostics.A_rest_element_cannot_have_an_initializer); - } - else { - checkGrammarForDisallowedTrailingComma(node.elements, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - const type = everyType(sourceType, isTupleType) ? - mapType(sourceType, t => sliceTupleType(t as ts.TupleTypeReference, elementIndex)) : - createArrayType(elementType); - return checkDestructuringAssignment(restExpression, type, checkMode); - } + checkGrammarForDisallowedTrailingComma(node.elements, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + const type = everyType(sourceType, isTupleType) ? + mapType(sourceType, t => sliceTupleType(t as ts.TupleTypeReference, elementIndex)) : + createArrayType(elementType); + return checkDestructuringAssignment(restExpression, type, checkMode); } } - return undefined; } + return undefined; + } - function checkDestructuringAssignment(exprOrAssignment: ts.Expression | ts.ShorthandPropertyAssignment, sourceType: ts.Type, checkMode?: CheckMode, rightIsThis?: boolean): ts.Type { - let target: ts.Expression; - if (exprOrAssignment.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - const prop = exprOrAssignment as ts.ShorthandPropertyAssignment; - 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)) & ts.TypeFlags.Undefined)) { - sourceType = getTypeWithFacts(sourceType, TypeFacts.NEUndefined); - } - checkBinaryLikeExpression(prop.name, prop.equalsToken!, prop.objectAssignmentInitializer, checkMode); + function checkDestructuringAssignment(exprOrAssignment: ts.Expression | ts.ShorthandPropertyAssignment, sourceType: ts.Type, checkMode?: CheckMode, rightIsThis?: boolean): ts.Type { + let target: ts.Expression; + if (exprOrAssignment.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + const prop = exprOrAssignment as ts.ShorthandPropertyAssignment; + 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)) & ts.TypeFlags.Undefined)) { + sourceType = getTypeWithFacts(sourceType, TypeFacts.NEUndefined); } - target = (exprOrAssignment as ts.ShorthandPropertyAssignment).name; - } - else { - target = exprOrAssignment; - } - - if (target.kind === ts.SyntaxKind.BinaryExpression && (target as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { - checkBinaryExpression(target as ts.BinaryExpression, checkMode); - target = (target as ts.BinaryExpression).left; - } - if (target.kind === ts.SyntaxKind.ObjectLiteralExpression) { - return checkObjectLiteralAssignment(target as ts.ObjectLiteralExpression, sourceType, rightIsThis); + checkBinaryLikeExpression(prop.name, prop.equalsToken!, prop.objectAssignmentInitializer, checkMode); } - if (target.kind === ts.SyntaxKind.ArrayLiteralExpression) { - return checkArrayLiteralAssignment(target as ts.ArrayLiteralExpression, sourceType, checkMode); - } - return checkReferenceAssignment(target, sourceType, checkMode); + target = (exprOrAssignment as ts.ShorthandPropertyAssignment).name; + } + else { + target = exprOrAssignment; } - function checkReferenceAssignment(target: ts.Expression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { - const targetType = checkExpression(target, checkMode); - const error = target.parent.kind === ts.SyntaxKind.SpreadAssignment ? - ts.Diagnostics.The_target_of_an_object_rest_assignment_must_be_a_variable_or_a_property_access : - ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access; - const optionalError = target.parent.kind === ts.SyntaxKind.SpreadAssignment ? - ts.Diagnostics.The_target_of_an_object_rest_assignment_may_not_be_an_optional_property_access : - ts.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 (ts.isPrivateIdentifierPropertyAccessExpression(target)) { - checkExternalEmitHelpers(target.parent, ts.ExternalEmitHelpers.ClassPrivateFieldSet); - } - return sourceType; + if (target.kind === ts.SyntaxKind.BinaryExpression && (target as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { + checkBinaryExpression(target as ts.BinaryExpression, checkMode); + target = (target as ts.BinaryExpression).left; } + if (target.kind === ts.SyntaxKind.ObjectLiteralExpression) { + return checkObjectLiteralAssignment(target as ts.ObjectLiteralExpression, sourceType, rightIsThis); + } + if (target.kind === ts.SyntaxKind.ArrayLiteralExpression) { + return checkArrayLiteralAssignment(target as ts.ArrayLiteralExpression, sourceType, checkMode); + } + return checkReferenceAssignment(target, sourceType, checkMode); + } - /** - * 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: ts.Node): boolean { - node = ts.skipParentheses(node); - switch (node.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.RegularExpressionLiteral: - case ts.SyntaxKind.TaggedTemplateExpression: - case ts.SyntaxKind.TemplateExpression: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.UndefinedKeyword: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.ArrayLiteralExpression: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.TypeOfExpression: - case ts.SyntaxKind.NonNullExpression: - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxElement: - return true; + function checkReferenceAssignment(target: ts.Expression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { + const targetType = checkExpression(target, checkMode); + const error = target.parent.kind === ts.SyntaxKind.SpreadAssignment ? + ts.Diagnostics.The_target_of_an_object_rest_assignment_must_be_a_variable_or_a_property_access : + ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access; + const optionalError = target.parent.kind === ts.SyntaxKind.SpreadAssignment ? + ts.Diagnostics.The_target_of_an_object_rest_assignment_may_not_be_an_optional_property_access : + ts.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 (ts.isPrivateIdentifierPropertyAccessExpression(target)) { + checkExternalEmitHelpers(target.parent, ts.ExternalEmitHelpers.ClassPrivateFieldSet); + } + return sourceType; + } - case ts.SyntaxKind.ConditionalExpression: - return isSideEffectFree((node as ts.ConditionalExpression).whenTrue) && - isSideEffectFree((node as ts.ConditionalExpression).whenFalse); - case ts.SyntaxKind.BinaryExpression: - if (ts.isAssignmentOperator((node as ts.BinaryExpression).operatorToken.kind)) { - return false; - } - return isSideEffectFree((node as ts.BinaryExpression).left) && - isSideEffectFree((node as ts.BinaryExpression).right); - case ts.SyntaxKind.PrefixUnaryExpression: - case ts.SyntaxKind.PostfixUnaryExpression: - // Unary operators ~, !, +, and - have no side effects. - // The rest do. - switch ((node as ts.PrefixUnaryExpression).operator) { - case ts.SyntaxKind.ExclamationToken: - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.TildeToken: - return true; - } - return false; + /** + * 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: ts.Node): boolean { + node = ts.skipParentheses(node); + switch (node.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.RegularExpressionLiteral: + case ts.SyntaxKind.TaggedTemplateExpression: + case ts.SyntaxKind.TemplateExpression: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.UndefinedKeyword: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.ArrayLiteralExpression: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.TypeOfExpression: + case ts.SyntaxKind.NonNullExpression: + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxElement: + return true; - // Some forms listed here for clarity - case ts.SyntaxKind.VoidExpression: // Explicit opt-out - case ts.SyntaxKind.TypeAssertionExpression: // Not SEF, but can produce useful type warnings - case ts.SyntaxKind.AsExpression: // Not SEF, but can produce useful type warnings - default: + case ts.SyntaxKind.ConditionalExpression: + return isSideEffectFree((node as ts.ConditionalExpression).whenTrue) && + isSideEffectFree((node as ts.ConditionalExpression).whenFalse); + case ts.SyntaxKind.BinaryExpression: + if (ts.isAssignmentOperator((node as ts.BinaryExpression).operatorToken.kind)) { return false; - } - } + } + return isSideEffectFree((node as ts.BinaryExpression).left) && + isSideEffectFree((node as ts.BinaryExpression).right); + case ts.SyntaxKind.PrefixUnaryExpression: + case ts.SyntaxKind.PostfixUnaryExpression: + // Unary operators ~, !, +, and - have no side effects. + // The rest do. + switch ((node as ts.PrefixUnaryExpression).operator) { + case ts.SyntaxKind.ExclamationToken: + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.TildeToken: + return true; + } + return false; - function isTypeEqualityComparableTo(source: ts.Type, target: ts.Type) { - return (target.flags & ts.TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target); + // Some forms listed here for clarity + case ts.SyntaxKind.VoidExpression: // Explicit opt-out + case ts.SyntaxKind.TypeAssertionExpression: // Not SEF, but can produce useful type warnings + case ts.SyntaxKind.AsExpression: // Not SEF, but can produce useful type warnings + default: + return false; } + } - function createCheckBinaryExpression() { - interface WorkArea { - readonly checkMode: CheckMode | undefined; - skip: boolean; - stackIndex: number; - /** - * Holds the types from the left-side of an expression from [0..stackIndex]. - * Holds the type of the result at stackIndex+1. This allows us to reuse existing stack entries - * and avoid storing an extra property on the object (i.e., `lastResult`). - */ - typeStack: (ts.Type | undefined)[]; - } + function isTypeEqualityComparableTo(source: ts.Type, target: ts.Type) { + return (target.flags & ts.TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target); + } - const trampoline = ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, foldState); - return (node: ts.BinaryExpression, checkMode: CheckMode | undefined) => { - const result = trampoline(node, checkMode); - ts.Debug.assertIsDefined(result); - return result; - }; + function createCheckBinaryExpression() { + interface WorkArea { + readonly checkMode: CheckMode | undefined; + skip: boolean; + stackIndex: number; + /** + * Holds the types from the left-side of an expression from [0..stackIndex]. + * Holds the type of the result at stackIndex+1. This allows us to reuse existing stack entries + * and avoid storing an extra property on the object (i.e., `lastResult`). + */ + typeStack: (ts.Type | undefined)[]; + } - function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined, checkMode: CheckMode | undefined) { - if (state) { - state.stackIndex++; - state.skip = false; - setLeftType(state, /*type*/ undefined); - setLastResult(state, /*type*/ undefined); - } - else { - state = { - checkMode, - skip: false, - stackIndex: 0, - typeStack: [undefined, undefined], - }; - } + const trampoline = ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, foldState); + return (node: ts.BinaryExpression, checkMode: CheckMode | undefined) => { + const result = trampoline(node, checkMode); + ts.Debug.assertIsDefined(result); + return result; + }; - if (ts.isInJSFile(node) && ts.getAssignedExpandoInitializer(node)) { - state.skip = true; - setLastResult(state, checkExpression(node.right, checkMode)); - return state; - } + function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined, checkMode: CheckMode | undefined) { + if (state) { + state.stackIndex++; + state.skip = false; + setLeftType(state, /*type*/ undefined); + setLastResult(state, /*type*/ undefined); + } + else { + state = { + checkMode, + skip: false, + stackIndex: 0, + typeStack: [undefined, undefined], + }; + } - checkGrammarNullishCoalesceWithLogicalExpression(node); + if (ts.isInJSFile(node) && ts.getAssignedExpandoInitializer(node)) { + state.skip = true; + setLastResult(state, checkExpression(node.right, checkMode)); + return state; + } - const operator = node.operatorToken.kind; - if (operator === ts.SyntaxKind.EqualsToken && (node.left.kind === ts.SyntaxKind.ObjectLiteralExpression || node.left.kind === ts.SyntaxKind.ArrayLiteralExpression)) { - state.skip = true; - setLastResult(state, checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === ts.SyntaxKind.ThisKeyword)); - return state; - } + checkGrammarNullishCoalesceWithLogicalExpression(node); + const operator = node.operatorToken.kind; + if (operator === ts.SyntaxKind.EqualsToken && (node.left.kind === ts.SyntaxKind.ObjectLiteralExpression || node.left.kind === ts.SyntaxKind.ArrayLiteralExpression)) { + state.skip = true; + setLastResult(state, checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === ts.SyntaxKind.ThisKeyword)); return state; } - function onLeft(left: ts.Expression, state: WorkArea, _node: ts.BinaryExpression) { - if (!state.skip) { - return maybeCheckExpression(state, left); - } + return state; + } + + function onLeft(left: ts.Expression, state: WorkArea, _node: ts.BinaryExpression) { + if (!state.skip) { + return maybeCheckExpression(state, left); } + } - function onOperator(operatorToken: ts.BinaryOperatorToken, state: WorkArea, node: ts.BinaryExpression) { - if (!state.skip) { - const leftType = getLastResult(state); - ts.Debug.assertIsDefined(leftType); - setLeftType(state, leftType); - setLastResult(state, /*type*/ undefined); - const operator = operatorToken.kind; - if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken || operator === ts.SyntaxKind.QuestionQuestionToken) { - if (operator === ts.SyntaxKind.AmpersandAmpersandToken) { - const parent = ts.walkUpParenthesizedExpressions(node.parent); - checkTestingKnownTruthyCallableOrAwaitableType(node.left, ts.isIfStatement(parent) ? parent.thenStatement : undefined); - } - checkTruthinessOfType(leftType, node.left); + function onOperator(operatorToken: ts.BinaryOperatorToken, state: WorkArea, node: ts.BinaryExpression) { + if (!state.skip) { + const leftType = getLastResult(state); + ts.Debug.assertIsDefined(leftType); + setLeftType(state, leftType); + setLastResult(state, /*type*/ undefined); + const operator = operatorToken.kind; + if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken || operator === ts.SyntaxKind.QuestionQuestionToken) { + if (operator === ts.SyntaxKind.AmpersandAmpersandToken) { + const parent = ts.walkUpParenthesizedExpressions(node.parent); + checkTestingKnownTruthyCallableOrAwaitableType(node.left, ts.isIfStatement(parent) ? parent.thenStatement : undefined); } + checkTruthinessOfType(leftType, node.left); } } + } - function onRight(right: ts.Expression, state: WorkArea, _node: ts.BinaryExpression) { - if (!state.skip) { - return maybeCheckExpression(state, right); - } + function onRight(right: ts.Expression, state: WorkArea, _node: ts.BinaryExpression) { + if (!state.skip) { + return maybeCheckExpression(state, right); } + } - function onExit(node: ts.BinaryExpression, state: WorkArea): ts.Type | undefined { - let result: ts.Type | undefined; - if (state.skip) { - result = getLastResult(state); - } - else { - const leftType = getLeftType(state); - ts.Debug.assertIsDefined(leftType); - - const rightType = getLastResult(state); - ts.Debug.assertIsDefined(rightType); + function onExit(node: ts.BinaryExpression, state: WorkArea): ts.Type | undefined { + let result: ts.Type | undefined; + if (state.skip) { + result = getLastResult(state); + } + else { + const leftType = getLeftType(state); + ts.Debug.assertIsDefined(leftType); - result = checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node); - } + const rightType = getLastResult(state); + ts.Debug.assertIsDefined(rightType); - state.skip = false; - setLeftType(state, /*type*/ undefined); - setLastResult(state, /*type*/ undefined); - state.stackIndex--; - return result; + result = checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node); } - function foldState(state: WorkArea, result: ts.Type | undefined, _side: "left" | "right") { - setLastResult(state, result); - return state; - } + state.skip = false; + setLeftType(state, /*type*/ undefined); + setLastResult(state, /*type*/ undefined); + state.stackIndex--; + return result; + } - function maybeCheckExpression(state: WorkArea, node: ts.Expression): ts.BinaryExpression | undefined { - if (ts.isBinaryExpression(node)) { - return node; - } - setLastResult(state, checkExpression(node, state.checkMode)); - } + function foldState(state: WorkArea, result: ts.Type | undefined, _side: "left" | "right") { + setLastResult(state, result); + return state; + } - function getLeftType(state: WorkArea) { - return state.typeStack[state.stackIndex]; + function maybeCheckExpression(state: WorkArea, node: ts.Expression): ts.BinaryExpression | undefined { + if (ts.isBinaryExpression(node)) { + return node; } + setLastResult(state, checkExpression(node, state.checkMode)); + } - function setLeftType(state: WorkArea, type: ts.Type | undefined) { - state.typeStack[state.stackIndex] = type; - } + function getLeftType(state: WorkArea) { + return state.typeStack[state.stackIndex]; + } - function getLastResult(state: WorkArea) { - return state.typeStack[state.stackIndex + 1]; - } + function setLeftType(state: WorkArea, type: ts.Type | undefined) { + state.typeStack[state.stackIndex] = type; + } - function setLastResult(state: WorkArea, type: ts.Type | undefined) { - // To reduce overhead, reuse the next stack entry to store the - // last result. This avoids the overhead of an additional property - // on `WorkArea` and reuses empty stack entries as we walk back up - // the stack. - state.typeStack[state.stackIndex + 1] = type; - } + function getLastResult(state: WorkArea) { + return state.typeStack[state.stackIndex + 1]; } - function checkGrammarNullishCoalesceWithLogicalExpression(node: ts.BinaryExpression) { - const { left, operatorToken, right } = node; - if (operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) { - if (ts.isBinaryExpression(left) && (left.operatorToken.kind === ts.SyntaxKind.BarBarToken || left.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(left, ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, ts.tokenToString(left.operatorToken.kind), ts.tokenToString(operatorToken.kind)); - } - if (ts.isBinaryExpression(right) && (right.operatorToken.kind === ts.SyntaxKind.BarBarToken || right.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(right, ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, ts.tokenToString(right.operatorToken.kind), ts.tokenToString(operatorToken.kind)); - } - } + function setLastResult(state: WorkArea, type: ts.Type | undefined) { + // To reduce overhead, reuse the next stack entry to store the + // last result. This avoids the overhead of an additional property + // on `WorkArea` and reuses empty stack entries as we walk back up + // the stack. + state.typeStack[state.stackIndex + 1] = type; } + } - // 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: ts.Expression, operatorToken: ts.Node, right: ts.Expression, checkMode?: CheckMode, errorNode?: ts.Node): ts.Type { - const operator = operatorToken.kind; - if (operator === ts.SyntaxKind.EqualsToken && (left.kind === ts.SyntaxKind.ObjectLiteralExpression || left.kind === ts.SyntaxKind.ArrayLiteralExpression)) { - return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === ts.SyntaxKind.ThisKeyword); - } - let leftType: ts.Type; - if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken || operator === ts.SyntaxKind.QuestionQuestionToken) { - leftType = checkTruthinessExpression(left, checkMode); + function checkGrammarNullishCoalesceWithLogicalExpression(node: ts.BinaryExpression) { + const { left, operatorToken, right } = node; + if (operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) { + if (ts.isBinaryExpression(left) && (left.operatorToken.kind === ts.SyntaxKind.BarBarToken || left.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(left, ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, ts.tokenToString(left.operatorToken.kind), ts.tokenToString(operatorToken.kind)); } - else { - leftType = checkExpression(left, checkMode); + if (ts.isBinaryExpression(right) && (right.operatorToken.kind === ts.SyntaxKind.BarBarToken || right.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(right, ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, ts.tokenToString(right.operatorToken.kind), ts.tokenToString(operatorToken.kind)); } - - const rightType = checkExpression(right, checkMode); - return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode); } + } - function checkBinaryLikeExpressionWorker(left: ts.Expression, operatorToken: ts.Node, right: ts.Expression, leftType: ts.Type, rightType: ts.Type, errorNode?: ts.Node): ts.Type { - const operator = operatorToken.kind; - switch (operator) { - case ts.SyntaxKind.AsteriskToken: - case ts.SyntaxKind.AsteriskAsteriskToken: - case ts.SyntaxKind.AsteriskEqualsToken: - case ts.SyntaxKind.AsteriskAsteriskEqualsToken: - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.SlashEqualsToken: - case ts.SyntaxKind.PercentToken: - case ts.SyntaxKind.PercentEqualsToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.MinusEqualsToken: - case ts.SyntaxKind.LessThanLessThanToken: - case ts.SyntaxKind.LessThanLessThanEqualsToken: - case ts.SyntaxKind.GreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.BarEqualsToken: - case ts.SyntaxKind.CaretToken: - case ts.SyntaxKind.CaretEqualsToken: - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.AmpersandEqualsToken: - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - - leftType = checkNonNullType(leftType, left); - rightType = checkNonNullType(rightType, right); + // 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: ts.Expression, operatorToken: ts.Node, right: ts.Expression, checkMode?: CheckMode, errorNode?: ts.Node): ts.Type { + const operator = operatorToken.kind; + if (operator === ts.SyntaxKind.EqualsToken && (left.kind === ts.SyntaxKind.ObjectLiteralExpression || left.kind === ts.SyntaxKind.ArrayLiteralExpression)) { + return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === ts.SyntaxKind.ThisKeyword); + } + let leftType: ts.Type; + if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken || operator === ts.SyntaxKind.QuestionQuestionToken) { + leftType = checkTruthinessExpression(left, checkMode); + } + else { + leftType = checkExpression(left, checkMode); + } - let suggestedOperator: ts.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 & ts.TypeFlags.BooleanLike) && - (rightType.flags & ts.TypeFlags.BooleanLike) && - (suggestedOperator = getSuggestedBooleanOperator(operatorToken.kind)) !== undefined) { - error(errorNode || operatorToken, ts.Diagnostics.The_0_operator_is_not_allowed_for_boolean_types_Consider_using_1_instead, ts.tokenToString(operatorToken.kind), ts.tokenToString(suggestedOperator)); - return numberType; - } - else { - // otherwise just check each operand separately and report errors as normal - const leftOk = checkArithmeticOperandType(left, leftType, ts.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, ts.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, ts.TypeFlags.AnyOrUnknown) && isTypeAssignableToKind(rightType, ts.TypeFlags.AnyOrUnknown)) || - // Or, if neither could be bigint, implicit coercion results in a number result - !(maybeTypeOfKind(leftType, ts.TypeFlags.BigIntLike) || maybeTypeOfKind(rightType, ts.TypeFlags.BigIntLike))) { - resultType = numberType; - } - // At least one is assignable to bigint, so check that both are - else if (bothAreBigIntLike(leftType, rightType)) { - switch (operator) { - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - reportOperatorError(); - break; - case ts.SyntaxKind.AsteriskAsteriskToken: - case ts.SyntaxKind.AsteriskAsteriskEqualsToken: - if (languageVersion < ts.ScriptTarget.ES2016) { - error(errorNode, ts.Diagnostics.Exponentiation_cannot_be_performed_on_bigint_values_unless_the_target_option_is_set_to_es2016_or_later); - } - } - resultType = bigintType; - } - // Exactly one of leftType/rightType is assignable to bigint - else { - reportOperatorError(bothAreBigIntLike); - resultType = errorType; - } - if (leftOk && rightOk) { - checkAssignmentOperator(resultType); - } - return resultType; - } - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.PlusEqualsToken: - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } + const rightType = checkExpression(right, checkMode); + return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode); + } - if (!isTypeAssignableToKind(leftType, ts.TypeFlags.StringLike) && !isTypeAssignableToKind(rightType, ts.TypeFlags.StringLike)) { - leftType = checkNonNullType(leftType, left); - rightType = checkNonNullType(rightType, right); - } + function checkBinaryLikeExpressionWorker(left: ts.Expression, operatorToken: ts.Node, right: ts.Expression, leftType: ts.Type, rightType: ts.Type, errorNode?: ts.Node): ts.Type { + const operator = operatorToken.kind; + switch (operator) { + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.AsteriskAsteriskToken: + case ts.SyntaxKind.AsteriskEqualsToken: + case ts.SyntaxKind.AsteriskAsteriskEqualsToken: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.SlashEqualsToken: + case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.PercentEqualsToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.MinusEqualsToken: + case ts.SyntaxKind.LessThanLessThanToken: + case ts.SyntaxKind.LessThanLessThanEqualsToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.BarEqualsToken: + case ts.SyntaxKind.CaretToken: + case ts.SyntaxKind.CaretEqualsToken: + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.AmpersandEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } - let resultType: ts.Type | undefined; - if (isTypeAssignableToKind(leftType, ts.TypeFlags.NumberLike, /*strict*/ true) && isTypeAssignableToKind(rightType, ts.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. + leftType = checkNonNullType(leftType, left); + rightType = checkNonNullType(rightType, right); + + let suggestedOperator: ts.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 & ts.TypeFlags.BooleanLike) && + (rightType.flags & ts.TypeFlags.BooleanLike) && + (suggestedOperator = getSuggestedBooleanOperator(operatorToken.kind)) !== undefined) { + error(errorNode || operatorToken, ts.Diagnostics.The_0_operator_is_not_allowed_for_boolean_types_Consider_using_1_instead, ts.tokenToString(operatorToken.kind), ts.tokenToString(suggestedOperator)); + return numberType; + } + else { + // otherwise just check each operand separately and report errors as normal + const leftOk = checkArithmeticOperandType(left, leftType, ts.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, ts.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, ts.TypeFlags.AnyOrUnknown) && isTypeAssignableToKind(rightType, ts.TypeFlags.AnyOrUnknown)) || + // Or, if neither could be bigint, implicit coercion results in a number result + !(maybeTypeOfKind(leftType, ts.TypeFlags.BigIntLike) || maybeTypeOfKind(rightType, ts.TypeFlags.BigIntLike))) { resultType = numberType; } - else if (isTypeAssignableToKind(leftType, ts.TypeFlags.BigIntLike, /*strict*/ true) && isTypeAssignableToKind(rightType, ts.TypeFlags.BigIntLike, /*strict*/ true)) { - // If both operands are of the BigInt primitive type, the result is of the BigInt primitive type. + // At least one is assignable to bigint, so check that both are + else if (bothAreBigIntLike(leftType, rightType)) { + switch (operator) { + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + reportOperatorError(); + break; + case ts.SyntaxKind.AsteriskAsteriskToken: + case ts.SyntaxKind.AsteriskAsteriskEqualsToken: + if (languageVersion < ts.ScriptTarget.ES2016) { + error(errorNode, ts.Diagnostics.Exponentiation_cannot_be_performed_on_bigint_values_unless_the_target_option_is_set_to_es2016_or_later); + } + } resultType = bigintType; } - else if (isTypeAssignableToKind(leftType, ts.TypeFlags.StringLike, /*strict*/ true) || isTypeAssignableToKind(rightType, ts.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; + // Exactly one of leftType/rightType is assignable to bigint + else { + reportOperatorError(bothAreBigIntLike); + resultType = errorType; } - 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 = isErrorType(leftType) || isErrorType(rightType) ? errorType : anyType; + if (leftOk && rightOk) { + checkAssignmentOperator(resultType); } + return resultType; + } + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.PlusEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } - // Symbols are not allowed at all in arithmetic expressions - if (resultType && !checkForDisallowedESSymbolOperand(operator)) { - return resultType; - } + if (!isTypeAssignableToKind(leftType, ts.TypeFlags.StringLike) && !isTypeAssignableToKind(rightType, ts.TypeFlags.StringLike)) { + leftType = checkNonNullType(leftType, left); + rightType = checkNonNullType(rightType, right); + } - 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 = ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.AnyOrUnknown; - reportOperatorError((left, right) => isTypeAssignableToKind(left, closeEnoughKind) && - isTypeAssignableToKind(right, closeEnoughKind)); - return anyType; - } + let resultType: ts.Type | undefined; + if (isTypeAssignableToKind(leftType, ts.TypeFlags.NumberLike, /*strict*/ true) && isTypeAssignableToKind(rightType, ts.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, ts.TypeFlags.BigIntLike, /*strict*/ true) && isTypeAssignableToKind(rightType, ts.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, ts.TypeFlags.StringLike, /*strict*/ true) || isTypeAssignableToKind(rightType, ts.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 = isErrorType(leftType) || isErrorType(rightType) ? errorType : anyType; + } - if (operator === ts.SyntaxKind.PlusEqualsToken) { - checkAssignmentOperator(resultType); - } + // Symbols are not allowed at all in arithmetic expressions + if (resultType && !checkForDisallowedESSymbolOperand(operator)) { return resultType; - case ts.SyntaxKind.LessThanToken: - case ts.SyntaxKind.GreaterThanToken: - case ts.SyntaxKind.LessThanEqualsToken: - case ts.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 ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - if (ts.isLiteralExpressionOfObject(left) || ts.isLiteralExpressionOfObject(right)) { - const eqType = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsEqualsToken; - error(errorNode, ts.Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true"); - } - reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); - return booleanType; + } - case ts.SyntaxKind.InstanceOfKeyword: - return checkInstanceOfExpression(left, right, leftType, rightType); - case ts.SyntaxKind.InKeyword: - return checkInExpression(left, right, leftType, rightType); - case ts.SyntaxKind.AmpersandAmpersandToken: - case ts.SyntaxKind.AmpersandAmpersandEqualsToken: { - const resultType = getTypeFacts(leftType) & TypeFacts.Truthy ? - getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) : - leftType; - if (operator === ts.SyntaxKind.AmpersandAmpersandEqualsToken) { - checkAssignmentOperator(rightType); - } - 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 = ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.StringLike | ts.TypeFlags.AnyOrUnknown; + reportOperatorError((left, right) => isTypeAssignableToKind(left, closeEnoughKind) && + isTypeAssignableToKind(right, closeEnoughKind)); + return anyType; } - case ts.SyntaxKind.BarBarToken: - case ts.SyntaxKind.BarBarEqualsToken: { - const resultType = getTypeFacts(leftType) & TypeFacts.Falsy ? - getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], ts.UnionReduction.Subtype) : - leftType; - if (operator === ts.SyntaxKind.BarBarEqualsToken) { - checkAssignmentOperator(rightType); - } - return resultType; + + if (operator === ts.SyntaxKind.PlusEqualsToken) { + checkAssignmentOperator(resultType); } - case ts.SyntaxKind.QuestionQuestionToken: - case ts.SyntaxKind.QuestionQuestionEqualsToken: { - const resultType = getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ? - getUnionType([getNonNullableType(leftType), rightType], ts.UnionReduction.Subtype) : - leftType; - if (operator === ts.SyntaxKind.QuestionQuestionEqualsToken) { - checkAssignmentOperator(rightType); - } - return resultType; + return resultType; + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.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))); } - case ts.SyntaxKind.EqualsToken: - const declKind = ts.isBinaryExpression(left.parent) ? ts.getAssignmentDeclarationKind(left.parent) : ts.AssignmentDeclarationKind.None; - checkAssignmentDeclaration(declKind, rightType); - if (isAssignmentDeclaration(declKind)) { - if (!(rightType.flags & ts.TypeFlags.Object) || - declKind !== ts.AssignmentDeclarationKind.ModuleExports && - declKind !== ts.AssignmentDeclarationKind.Prototype && - !isEmptyObjectType(rightType) && - !isFunctionObjectType(rightType as ts.ObjectType) && - !(ts.getObjectFlags(rightType) & ts.ObjectFlags.Class)) { - // don't check assignability of module.exports=, C.prototype=, or expando types because they will necessarily be incomplete - checkAssignmentOperator(rightType); - } - return leftType; - } - else { + return booleanType; + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + if (ts.isLiteralExpressionOfObject(left) || ts.isLiteralExpressionOfObject(right)) { + const eqType = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsEqualsToken; + error(errorNode, ts.Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true"); + } + reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); + return booleanType; + + case ts.SyntaxKind.InstanceOfKeyword: + return checkInstanceOfExpression(left, right, leftType, rightType); + case ts.SyntaxKind.InKeyword: + return checkInExpression(left, right, leftType, rightType); + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: { + const resultType = getTypeFacts(leftType) & TypeFacts.Truthy ? + getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) : + leftType; + if (operator === ts.SyntaxKind.AmpersandAmpersandEqualsToken) { + checkAssignmentOperator(rightType); + } + return resultType; + } + case ts.SyntaxKind.BarBarToken: + case ts.SyntaxKind.BarBarEqualsToken: { + const resultType = getTypeFacts(leftType) & TypeFacts.Falsy ? + getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], ts.UnionReduction.Subtype) : + leftType; + if (operator === ts.SyntaxKind.BarBarEqualsToken) { + checkAssignmentOperator(rightType); + } + return resultType; + } + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: { + const resultType = getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ? + getUnionType([getNonNullableType(leftType), rightType], ts.UnionReduction.Subtype) : + leftType; + if (operator === ts.SyntaxKind.QuestionQuestionEqualsToken) { + checkAssignmentOperator(rightType); + } + return resultType; + } + case ts.SyntaxKind.EqualsToken: + const declKind = ts.isBinaryExpression(left.parent) ? ts.getAssignmentDeclarationKind(left.parent) : ts.AssignmentDeclarationKind.None; + checkAssignmentDeclaration(declKind, rightType); + if (isAssignmentDeclaration(declKind)) { + if (!(rightType.flags & ts.TypeFlags.Object) || + declKind !== ts.AssignmentDeclarationKind.ModuleExports && + declKind !== ts.AssignmentDeclarationKind.Prototype && + !isEmptyObjectType(rightType) && + !isFunctionObjectType(rightType as ts.ObjectType) && + !(ts.getObjectFlags(rightType) & ts.ObjectFlags.Class)) { + // don't check assignability of module.exports=, C.prototype=, or expando types because they will necessarily be incomplete checkAssignmentOperator(rightType); - return getRegularTypeOfObjectLiteral(rightType); - } - case ts.SyntaxKind.CommaToken: - if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isEvalNode(right)) { - const sf = ts.getSourceFileOfNode(left); - const sourceText = sf.text; - const start = ts.skipTrivia(sourceText, left.pos); - const isInDiag2657 = sf.parseDiagnostics.some(diag => { - if (diag.code !== ts.Diagnostics.JSX_expressions_must_have_one_parent_element.code) - return false; - return ts.textSpanContainsPosition(diag, start); - }); - if (!isInDiag2657) - error(left, ts.Diagnostics.Left_side_of_comma_operator_is_unused_and_has_no_side_effects); } - return rightType; + return leftType; + } + else { + checkAssignmentOperator(rightType); + return getRegularTypeOfObjectLiteral(rightType); + } + case ts.SyntaxKind.CommaToken: + if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isEvalNode(right)) { + const sf = ts.getSourceFileOfNode(left); + const sourceText = sf.text; + const start = ts.skipTrivia(sourceText, left.pos); + const isInDiag2657 = sf.parseDiagnostics.some(diag => { + if (diag.code !== ts.Diagnostics.JSX_expressions_must_have_one_parent_element.code) + return false; + return ts.textSpanContainsPosition(diag, start); + }); + if (!isInDiag2657) + error(left, ts.Diagnostics.Left_side_of_comma_operator_is_unused_and_has_no_side_effects); + } + return rightType; - default: - return ts.Debug.fail(); - } + default: + return ts.Debug.fail(); + } - function bothAreBigIntLike(left: ts.Type, right: ts.Type): boolean { - return isTypeAssignableToKind(left, ts.TypeFlags.BigIntLike) && isTypeAssignableToKind(right, ts.TypeFlags.BigIntLike); - } + function bothAreBigIntLike(left: ts.Type, right: ts.Type): boolean { + return isTypeAssignableToKind(left, ts.TypeFlags.BigIntLike) && isTypeAssignableToKind(right, ts.TypeFlags.BigIntLike); + } - function checkAssignmentDeclaration(kind: ts.AssignmentDeclarationKind, rightType: ts.Type) { - if (kind === ts.AssignmentDeclarationKind.ModuleExports) { - for (const prop of getPropertiesOfObjectType(rightType)) { - const propType = getTypeOfSymbol(prop); - if (propType.symbol && propType.symbol.flags & ts.SymbolFlags.Class) { - const name = prop.escapedName; - const symbol = resolveName(prop.valueDeclaration, name, ts.SymbolFlags.Type, undefined, name, /*isUse*/ false); - if (symbol?.declarations && symbol.declarations.some(ts.isJSDocTypedefTag)) { - addDuplicateDeclarationErrorsForSymbols(symbol, ts.Diagnostics.Duplicate_identifier_0, ts.unescapeLeadingUnderscores(name), prop); - addDuplicateDeclarationErrorsForSymbols(prop, ts.Diagnostics.Duplicate_identifier_0, ts.unescapeLeadingUnderscores(name), symbol); - } + function checkAssignmentDeclaration(kind: ts.AssignmentDeclarationKind, rightType: ts.Type) { + if (kind === ts.AssignmentDeclarationKind.ModuleExports) { + for (const prop of getPropertiesOfObjectType(rightType)) { + const propType = getTypeOfSymbol(prop); + if (propType.symbol && propType.symbol.flags & ts.SymbolFlags.Class) { + const name = prop.escapedName; + const symbol = resolveName(prop.valueDeclaration, name, ts.SymbolFlags.Type, undefined, name, /*isUse*/ false); + if (symbol?.declarations && symbol.declarations.some(ts.isJSDocTypedefTag)) { + addDuplicateDeclarationErrorsForSymbols(symbol, ts.Diagnostics.Duplicate_identifier_0, ts.unescapeLeadingUnderscores(name), prop); + addDuplicateDeclarationErrorsForSymbols(prop, ts.Diagnostics.Duplicate_identifier_0, ts.unescapeLeadingUnderscores(name), symbol); } } } } + } - function isEvalNode(node: ts.Expression) { - return node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).escapedText === "eval"; - } - - // Return true if there was no error, false if there was an error. - function checkForDisallowedESSymbolOperand(operator: ts.SyntaxKind): boolean { - const offendingSymbolOperand = maybeTypeOfKindConsideringBaseConstraint(leftType, ts.TypeFlags.ESSymbolLike) ? left : - maybeTypeOfKindConsideringBaseConstraint(rightType, ts.TypeFlags.ESSymbolLike) ? right : - undefined; + function isEvalNode(node: ts.Expression) { + return node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).escapedText === "eval"; + } - if (offendingSymbolOperand) { - error(offendingSymbolOperand, ts.Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, ts.tokenToString(operator)); - return false; - } + // Return true if there was no error, false if there was an error. + function checkForDisallowedESSymbolOperand(operator: ts.SyntaxKind): boolean { + const offendingSymbolOperand = maybeTypeOfKindConsideringBaseConstraint(leftType, ts.TypeFlags.ESSymbolLike) ? left : + maybeTypeOfKindConsideringBaseConstraint(rightType, ts.TypeFlags.ESSymbolLike) ? right : + undefined; - return true; + if (offendingSymbolOperand) { + error(offendingSymbolOperand, ts.Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, ts.tokenToString(operator)); + return false; } - function getSuggestedBooleanOperator(operator: ts.SyntaxKind): ts.SyntaxKind | undefined { - switch (operator) { - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.BarEqualsToken: - return ts.SyntaxKind.BarBarToken; - case ts.SyntaxKind.CaretToken: - case ts.SyntaxKind.CaretEqualsToken: - return ts.SyntaxKind.ExclamationEqualsEqualsToken; - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.AmpersandEqualsToken: - return ts.SyntaxKind.AmpersandAmpersandToken; - default: - return undefined; - } + return true; + } + + function getSuggestedBooleanOperator(operator: ts.SyntaxKind): ts.SyntaxKind | undefined { + switch (operator) { + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.BarEqualsToken: + return ts.SyntaxKind.BarBarToken; + case ts.SyntaxKind.CaretToken: + case ts.SyntaxKind.CaretEqualsToken: + return ts.SyntaxKind.ExclamationEqualsEqualsToken; + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.AmpersandEqualsToken: + return ts.SyntaxKind.AmpersandAmpersandToken; + default: + return undefined; } + } - function checkAssignmentOperator(valueType: ts.Type): void { - if (ts.isAssignmentOperator(operator)) { - addLazyDiagnostic(checkAssignmentOperatorWorker); - } + function checkAssignmentOperator(valueType: ts.Type): void { + if (ts.isAssignmentOperator(operator)) { + addLazyDiagnostic(checkAssignmentOperatorWorker); + } - function checkAssignmentOperatorWorker() { - // 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. + function checkAssignmentOperatorWorker() { + // 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, ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) - && (!ts.isIdentifier(left) || ts.unescapeLeadingUnderscores(left.escapedText) !== "exports")) { - let headMessage: ts.DiagnosticMessage | undefined; - if (exactOptionalPropertyTypes && ts.isPropertyAccessExpression(left) && maybeTypeOfKind(valueType, ts.TypeFlags.Undefined)) { - const target = getTypeOfPropertyOfType(getTypeOfExpression(left.expression), left.name.escapedText); - if (isExactOptionalPropertyMismatch(valueType, target)) { - headMessage = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target; - } + if (checkReferenceExpression(left, ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) + && (!ts.isIdentifier(left) || ts.unescapeLeadingUnderscores(left.escapedText) !== "exports")) { + let headMessage: ts.DiagnosticMessage | undefined; + if (exactOptionalPropertyTypes && ts.isPropertyAccessExpression(left) && maybeTypeOfKind(valueType, ts.TypeFlags.Undefined)) { + const target = getTypeOfPropertyOfType(getTypeOfExpression(left.expression), left.name.escapedText); + if (isExactOptionalPropertyMismatch(valueType, target)) { + headMessage = ts.Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target; } - // to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported - checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right, headMessage); } + // to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported + checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right, headMessage); } } + } - function isAssignmentDeclaration(kind: ts.AssignmentDeclarationKind) { - switch (kind) { - case ts.AssignmentDeclarationKind.ModuleExports: - return true; - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.Property: - case ts.AssignmentDeclarationKind.Prototype: - case ts.AssignmentDeclarationKind.PrototypeProperty: - case ts.AssignmentDeclarationKind.ThisProperty: - const symbol = getSymbolOfNode(left); - const init = ts.getAssignedExpandoInitializer(right); - return !!init && ts.isObjectLiteralExpression(init) && - !!symbol?.exports?.size; - default: - return false; - } - } - - /** - * Returns true if an error is reported - */ - function reportOperatorErrorUnless(typesAreCompatible: (left: ts.Type, right: ts.Type) => boolean): boolean { - if (!typesAreCompatible(leftType, rightType)) { - reportOperatorError(typesAreCompatible); + function isAssignmentDeclaration(kind: ts.AssignmentDeclarationKind) { + switch (kind) { + case ts.AssignmentDeclarationKind.ModuleExports: return true; - } - return false; + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.Property: + case ts.AssignmentDeclarationKind.Prototype: + case ts.AssignmentDeclarationKind.PrototypeProperty: + case ts.AssignmentDeclarationKind.ThisProperty: + const symbol = getSymbolOfNode(left); + const init = ts.getAssignedExpandoInitializer(right); + return !!init && ts.isObjectLiteralExpression(init) && + !!symbol?.exports?.size; + default: + return false; } + } - function reportOperatorError(isRelated?: (left: ts.Type, right: ts.Type) => boolean) { - let wouldWorkWithAwait = false; - const errNode = errorNode || operatorToken; - if (isRelated) { - const awaitedLeftType = getAwaitedTypeNoAlias(leftType); - const awaitedRightType = getAwaitedTypeNoAlias(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, ts.Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, ts.tokenToString(operatorToken.kind), leftStr, rightStr); - } + /** + * Returns true if an error is reported + */ + function reportOperatorErrorUnless(typesAreCompatible: (left: ts.Type, right: ts.Type) => boolean): boolean { + if (!typesAreCompatible(leftType, rightType)) { + reportOperatorError(typesAreCompatible); + return true; } + return false; + } - function tryGiveBetterPrimaryError(errNode: ts.Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { - let typeName: string | undefined; - switch (operatorToken.kind) { - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.EqualsEqualsToken: - typeName = "false"; - break; - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - typeName = "true"; - } - - if (typeName) { - return errorAndMaybeSuggestAwait(errNode, maybeMissingAwait, ts.Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, typeName, leftStr, rightStr); - } - - return undefined; + function reportOperatorError(isRelated?: (left: ts.Type, right: ts.Type) => boolean) { + let wouldWorkWithAwait = false; + const errNode = errorNode || operatorToken; + if (isRelated) { + const awaitedLeftType = getAwaitedTypeNoAlias(leftType); + const awaitedRightType = getAwaitedTypeNoAlias(rightType); + wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType) + && !!(awaitedLeftType && awaitedRightType) + && isRelated(awaitedLeftType, awaitedRightType); } - } - 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; + if (!wouldWorkWithAwait && isRelated) { + [effectiveLeft, effectiveRight] = getBaseTypesIfUnrelated(leftType, rightType, isRelated); + } + const [leftStr, rightStr] = getTypeNamesForErrorDisplay(effectiveLeft, effectiveRight); + if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { + errorAndMaybeSuggestAwait(errNode, wouldWorkWithAwait, ts.Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, ts.tokenToString(operatorToken.kind), leftStr, rightStr); } - return [ effectiveLeft, effectiveRight ]; } - function checkYieldExpression(node: ts.YieldExpression): ts.Type { - addLazyDiagnostic(checkYieldExpressionGrammar); + function tryGiveBetterPrimaryError(errNode: ts.Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { + let typeName: string | undefined; + switch (operatorToken.kind) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + typeName = "false"; + break; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + typeName = "true"; + } - const func = ts.getContainingFunction(node); - if (!func) - return anyType; - const functionFlags = ts.getFunctionFlags(func); - if (!(functionFlags & ts.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; + if (typeName) { + return errorAndMaybeSuggestAwait(errNode, maybeMissingAwait, ts.Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, typeName, leftStr, rightStr); } - const isAsync = (functionFlags & ts.FunctionFlags.Async) !== 0; - if (node.asteriskToken) { - // Async generator functions prior to ESNext require the __await, __asyncDelegator, - // and __asyncValues helpers - if (isAsync && languageVersion < ts.ScriptTarget.ESNext) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.AsyncDelegatorIncludes); - } + return undefined; + } + } - // Generator functions prior to ES2015 require the __values helper - if (!isAsync && languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Values); - } - } + 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 ]; + } - // 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); - } + function checkYieldExpression(node: ts.YieldExpression): ts.Type { + addLazyDiagnostic(checkYieldExpressionGrammar); - 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; + const func = ts.getContainingFunction(node); + if (!func) + return anyType; + const functionFlags = ts.getFunctionFlags(func); + if (!(functionFlags & ts.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 & ts.FunctionFlags.Async) !== 0; + if (node.asteriskToken) { + // Async generator functions prior to ESNext require the __await, __asyncDelegator, + // and __asyncValues helpers + if (isAsync && languageVersion < ts.ScriptTarget.ESNext) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.AsyncDelegatorIncludes); } - let type = getContextualIterationType(IterationTypeKind.Next, func); - if (!type) { - type = anyType; - addLazyDiagnostic(() => { - if (noImplicitAny && !ts.expressionResultIsUnused(node)) { - const contextualType = getContextualType(node); - if (!contextualType || isTypeAny(contextualType)) { - error(node, ts.Diagnostics.yield_expression_implicitly_results_in_an_any_type_because_its_containing_generator_lacks_a_return_type_annotation); - } - } - }); + + // Generator functions prior to ES2015 require the __values helper + if (!isAsync && languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Values); } - return type; + } - function checkYieldExpressionGrammar() { - if (!(node.flags & ts.NodeFlags.YieldContext)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.A_yield_expression_is_only_allowed_in_a_generator_body); - } + // 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 (isInParameterInitializerBeforeContainingFunction(node)) { - error(node, ts.Diagnostics.yield_expressions_cannot_be_used_in_a_parameter_initializer); + 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; + } + let type = getContextualIterationType(IterationTypeKind.Next, func); + if (!type) { + type = anyType; + addLazyDiagnostic(() => { + if (noImplicitAny && !ts.expressionResultIsUnused(node)) { + const contextualType = getContextualType(node); + if (!contextualType || isTypeAny(contextualType)) { + error(node, ts.Diagnostics.yield_expression_implicitly_results_in_an_any_type_because_its_containing_generator_lacks_a_return_type_annotation); + } } + }); + } + return type; + + function checkYieldExpressionGrammar() { + if (!(node.flags & ts.NodeFlags.YieldContext)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.A_yield_expression_is_only_allowed_in_a_generator_body); + } + + if (isInParameterInitializerBeforeContainingFunction(node)) { + error(node, ts.Diagnostics.yield_expressions_cannot_be_used_in_a_parameter_initializer); } } + } - function checkConditionalExpression(node: ts.ConditionalExpression, checkMode?: CheckMode): ts.Type { - checkTruthinessExpression(node.condition); - checkTestingKnownTruthyCallableOrAwaitableType(node.condition, node.whenTrue); - const type1 = checkExpression(node.whenTrue, checkMode); - const type2 = checkExpression(node.whenFalse, checkMode); - return getUnionType([type1, type2], ts.UnionReduction.Subtype); - } + function checkConditionalExpression(node: ts.ConditionalExpression, checkMode?: CheckMode): ts.Type { + checkTruthinessExpression(node.condition); + checkTestingKnownTruthyCallableOrAwaitableType(node.condition, node.whenTrue); + const type1 = checkExpression(node.whenTrue, checkMode); + const type2 = checkExpression(node.whenFalse, checkMode); + return getUnionType([type1, type2], ts.UnionReduction.Subtype); + } - function isTemplateLiteralContext(node: ts.Node): boolean { - const parent = node.parent; - return ts.isParenthesizedExpression(parent) && isTemplateLiteralContext(parent) || - ts.isElementAccessExpression(parent) && parent.argumentExpression === node; - } + function isTemplateLiteralContext(node: ts.Node): boolean { + const parent = node.parent; + return ts.isParenthesizedExpression(parent) && isTemplateLiteralContext(parent) || + ts.isElementAccessExpression(parent) && parent.argumentExpression === node; + } - function checkTemplateExpression(node: ts.TemplateExpression): ts.Type { - const texts = [node.head.text]; - const types = []; - for (const span of node.templateSpans) { - const type = checkExpression(span.expression); - if (maybeTypeOfKindConsideringBaseConstraint(type, ts.TypeFlags.ESSymbolLike)) { - error(span.expression, ts.Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String); - } - texts.push(span.literal.text); - types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType); + function checkTemplateExpression(node: ts.TemplateExpression): ts.Type { + const texts = [node.head.text]; + const types = []; + for (const span of node.templateSpans) { + const type = checkExpression(span.expression); + if (maybeTypeOfKindConsideringBaseConstraint(type, ts.TypeFlags.ESSymbolLike)) { + error(span.expression, ts.Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String); } - return isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node) || unknownType, isTemplateLiteralContextualType) ? getTemplateLiteralType(texts, types) : stringType; + texts.push(span.literal.text); + types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType); } + return isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node) || unknownType, isTemplateLiteralContextualType) ? getTemplateLiteralType(texts, types) : stringType; + } - function isTemplateLiteralContextualType(type: ts.Type): boolean { - return !!(type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral) || - type.flags & ts.TypeFlags.InstantiableNonPrimitive && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, ts.TypeFlags.StringLike)); - } + function isTemplateLiteralContextualType(type: ts.Type): boolean { + return !!(type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral) || + type.flags & ts.TypeFlags.InstantiableNonPrimitive && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, ts.TypeFlags.StringLike)); + } - function getContextNode(node: ts.Expression): ts.Node { - if (node.kind === ts.SyntaxKind.JsxAttributes && !ts.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 node; + function getContextNode(node: ts.Expression): ts.Node { + if (node.kind === ts.SyntaxKind.JsxAttributes && !ts.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 node; + } - function checkExpressionWithContextualType(node: ts.Expression, contextualType: ts.Type, inferenceContext: ts.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)); - // In CheckMode.Inferential we collect intra-expression inference sites to process before fixing any type - // parameters. This information is no longer needed after the call to checkExpression. - if (inferenceContext && inferenceContext.intraExpressionInferenceSites) { - inferenceContext.intraExpressionInferenceSites = undefined; - } - // 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, ts.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; - } + function checkExpressionWithContextualType(node: ts.Expression, contextualType: ts.Type, inferenceContext: ts.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)); + // In CheckMode.Inferential we collect intra-expression inference sites to process before fixing any type + // parameters. This information is no longer needed after the call to checkExpression. + if (inferenceContext && inferenceContext.intraExpressionInferenceSites) { + inferenceContext.intraExpressionInferenceSites = undefined; + } + // 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, ts.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; + } + } - function checkExpressionCached(node: ts.Expression | ts.QualifiedName, checkMode?: CheckMode): ts.Type { - if (checkMode && checkMode !== CheckMode.Normal) { - return checkExpression(node, checkMode); - } - const links = getNodeLinks(node); - if (!links.resolvedType) { - // 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: ts.Expression) { - node = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); - return node.kind === ts.SyntaxKind.TypeAssertionExpression || - node.kind === ts.SyntaxKind.AsExpression || - ts.isJSDocTypeAssertion(node); - } - - function checkDeclarationInitializer(declaration: ts.HasExpressionInitializer, checkMode: CheckMode, contextualType?: ts.Type | undefined) { - const initializer = ts.getEffectiveInitializer(declaration)!; - const type = getQuickTypeOfExpression(initializer) || - (contextualType ? - checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, checkMode || CheckMode.Normal) - : checkExpressionCached(initializer, checkMode)); - return ts.isParameter(declaration) && declaration.name.kind === ts.SyntaxKind.ArrayBindingPattern && - isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ? - padTupleType(type, declaration.name) : type; - } - - function padTupleType(type: ts.TupleTypeReference, pattern: ts.ArrayBindingPattern) { - const patternElements = pattern.elements; - const elementTypes = getTypeArguments(type).slice(); - const elementFlags = type.target.elementFlags.slice(); - for (let i = getTypeReferenceArity(type); i < patternElements.length; i++) { - const e = patternElements[i]; - if (i < patternElements.length - 1 || !(e.kind === ts.SyntaxKind.BindingElement && e.dotDotDotToken)) { - elementTypes.push(!ts.isOmittedExpression(e) && hasDefaultValue(e) ? getTypeFromBindingElement(e, /*includePatternInType*/ false, /*reportErrors*/ false) : anyType); - elementFlags.push(ts.ElementFlags.Optional); - if (!ts.isOmittedExpression(e) && !hasDefaultValue(e)) { - reportImplicitAny(e, anyType); - } - } - } - return createTupleType(elementTypes, elementFlags, type.target.readonly); - } - - function widenTypeInferredFromInitializer(declaration: ts.HasExpressionInitializer, type: ts.Type) { - const widened = ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const || ts.isDeclarationReadonly(declaration) ? type : getWidenedLiteralType(type); - if (ts.isInJSFile(declaration)) { - if (isEmptyLiteralType(widened)) { - reportImplicitAny(declaration, anyType); - return anyType; - } - else if (isEmptyArrayLiteralType(widened)) { - reportImplicitAny(declaration, anyArrayType); - return anyArrayType; + function checkExpressionCached(node: ts.Expression | ts.QualifiedName, checkMode?: CheckMode): ts.Type { + if (checkMode && checkMode !== CheckMode.Normal) { + return checkExpression(node, checkMode); + } + const links = getNodeLinks(node); + if (!links.resolvedType) { + // 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: ts.Expression) { + node = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + return node.kind === ts.SyntaxKind.TypeAssertionExpression || + node.kind === ts.SyntaxKind.AsExpression || + ts.isJSDocTypeAssertion(node); + } + + function checkDeclarationInitializer(declaration: ts.HasExpressionInitializer, checkMode: CheckMode, contextualType?: ts.Type | undefined) { + const initializer = ts.getEffectiveInitializer(declaration)!; + const type = getQuickTypeOfExpression(initializer) || + (contextualType ? + checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, checkMode || CheckMode.Normal) + : checkExpressionCached(initializer, checkMode)); + return ts.isParameter(declaration) && declaration.name.kind === ts.SyntaxKind.ArrayBindingPattern && + isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ? + padTupleType(type, declaration.name) : type; + } + + function padTupleType(type: ts.TupleTypeReference, pattern: ts.ArrayBindingPattern) { + const patternElements = pattern.elements; + const elementTypes = getTypeArguments(type).slice(); + const elementFlags = type.target.elementFlags.slice(); + for (let i = getTypeReferenceArity(type); i < patternElements.length; i++) { + const e = patternElements[i]; + if (i < patternElements.length - 1 || !(e.kind === ts.SyntaxKind.BindingElement && e.dotDotDotToken)) { + elementTypes.push(!ts.isOmittedExpression(e) && hasDefaultValue(e) ? getTypeFromBindingElement(e, /*includePatternInType*/ false, /*reportErrors*/ false) : anyType); + elementFlags.push(ts.ElementFlags.Optional); + if (!ts.isOmittedExpression(e) && !hasDefaultValue(e)) { + reportImplicitAny(e, anyType); } } - return widened; } + return createTupleType(elementTypes, elementFlags, type.target.readonly); + } - function isLiteralOfContextualType(candidateType: ts.Type, contextualType: ts.Type | undefined): boolean { - if (contextualType) { - if (contextualType.flags & ts.TypeFlags.UnionOrIntersection) { - const types = (contextualType as ts.UnionType).types; - return ts.some(types, t => isLiteralOfContextualType(candidateType, t)); - } - if (contextualType.flags & ts.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, ts.TypeFlags.String) && maybeTypeOfKind(candidateType, ts.TypeFlags.StringLiteral) || - maybeTypeOfKind(constraint, ts.TypeFlags.Number) && maybeTypeOfKind(candidateType, ts.TypeFlags.NumberLiteral) || - maybeTypeOfKind(constraint, ts.TypeFlags.BigInt) && maybeTypeOfKind(candidateType, ts.TypeFlags.BigIntLiteral) || - maybeTypeOfKind(constraint, ts.TypeFlags.ESSymbol) && maybeTypeOfKind(candidateType, ts.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 & (ts.TypeFlags.StringLiteral | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && maybeTypeOfKind(candidateType, ts.TypeFlags.StringLiteral) || - contextualType.flags & ts.TypeFlags.NumberLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.NumberLiteral) || - contextualType.flags & ts.TypeFlags.BigIntLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.BigIntLiteral) || - contextualType.flags & ts.TypeFlags.BooleanLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.BooleanLiteral) || - contextualType.flags & ts.TypeFlags.UniqueESSymbol && maybeTypeOfKind(candidateType, ts.TypeFlags.UniqueESSymbol)); + function widenTypeInferredFromInitializer(declaration: ts.HasExpressionInitializer, type: ts.Type) { + const widened = ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const || ts.isDeclarationReadonly(declaration) ? type : getWidenedLiteralType(type); + if (ts.isInJSFile(declaration)) { + if (isEmptyLiteralType(widened)) { + reportImplicitAny(declaration, anyType); + return anyType; + } + else if (isEmptyArrayLiteralType(widened)) { + reportImplicitAny(declaration, anyArrayType); + return anyArrayType; } - return false; } + return widened; + } - function isConstContext(node: ts.Expression): boolean { - const parent = node.parent; - return ts.isAssertionExpression(parent) && ts.isConstTypeReference(parent.type) || - ts.isJSDocTypeAssertion(parent) && ts.isConstTypeReference(ts.getJSDocTypeAssertionType(parent)) || - (ts.isParenthesizedExpression(parent) || ts.isArrayLiteralExpression(parent) || ts.isSpreadElement(parent)) && isConstContext(parent) || - (ts.isPropertyAssignment(parent) || ts.isShorthandPropertyAssignment(parent) || ts.isTemplateSpan(parent)) && isConstContext(parent.parent); - } + function isLiteralOfContextualType(candidateType: ts.Type, contextualType: ts.Type | undefined): boolean { + if (contextualType) { + if (contextualType.flags & ts.TypeFlags.UnionOrIntersection) { + const types = (contextualType as ts.UnionType).types; + return ts.some(types, t => isLiteralOfContextualType(candidateType, t)); + } + if (contextualType.flags & ts.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, ts.TypeFlags.String) && maybeTypeOfKind(candidateType, ts.TypeFlags.StringLiteral) || + maybeTypeOfKind(constraint, ts.TypeFlags.Number) && maybeTypeOfKind(candidateType, ts.TypeFlags.NumberLiteral) || + maybeTypeOfKind(constraint, ts.TypeFlags.BigInt) && maybeTypeOfKind(candidateType, ts.TypeFlags.BigIntLiteral) || + maybeTypeOfKind(constraint, ts.TypeFlags.ESSymbol) && maybeTypeOfKind(candidateType, ts.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 & (ts.TypeFlags.StringLiteral | ts.TypeFlags.Index | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping) && maybeTypeOfKind(candidateType, ts.TypeFlags.StringLiteral) || + contextualType.flags & ts.TypeFlags.NumberLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.NumberLiteral) || + contextualType.flags & ts.TypeFlags.BigIntLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.BigIntLiteral) || + contextualType.flags & ts.TypeFlags.BooleanLiteral && maybeTypeOfKind(candidateType, ts.TypeFlags.BooleanLiteral) || + contextualType.flags & ts.TypeFlags.UniqueESSymbol && maybeTypeOfKind(candidateType, ts.TypeFlags.UniqueESSymbol)); + } + return false; + } - function checkExpressionForMutableLocation(node: ts.Expression, checkMode: CheckMode | undefined, contextualType?: ts.Type, forceTuple?: boolean): ts.Type { - const type = checkExpression(node, checkMode, forceTuple); - return isConstContext(node) || ts.isCommonJsExportedExpression(node) ? getRegularTypeOfLiteralType(type) : - isTypeAssertion(node) ? type : - getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node)); - } + function isConstContext(node: ts.Expression): boolean { + const parent = node.parent; + return ts.isAssertionExpression(parent) && ts.isConstTypeReference(parent.type) || + ts.isJSDocTypeAssertion(parent) && ts.isConstTypeReference(ts.getJSDocTypeAssertionType(parent)) || + (ts.isParenthesizedExpression(parent) || ts.isArrayLiteralExpression(parent) || ts.isSpreadElement(parent)) && isConstContext(parent) || + (ts.isPropertyAssignment(parent) || ts.isShorthandPropertyAssignment(parent) || ts.isTemplateSpan(parent)) && isConstContext(parent.parent); + } - function checkPropertyAssignment(node: ts.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 === ts.SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - } + function checkExpressionForMutableLocation(node: ts.Expression, checkMode: CheckMode | undefined, contextualType?: ts.Type, forceTuple?: boolean): ts.Type { + const type = checkExpression(node, checkMode, forceTuple); + return isConstContext(node) || ts.isCommonJsExportedExpression(node) ? getRegularTypeOfLiteralType(type) : + isTypeAssertion(node) ? type : + getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node)); + } - return checkExpressionForMutableLocation(node.initializer, checkMode); + function checkPropertyAssignment(node: ts.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 === ts.SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); } - function checkObjectLiteralMethod(node: ts.MethodDeclaration, checkMode?: CheckMode): ts.Type { - // Grammar checking - checkGrammarMethod(node); + return checkExpressionForMutableLocation(node.initializer, checkMode); + } - // 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 === ts.SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - } + function checkObjectLiteralMethod(node: ts.MethodDeclaration, checkMode?: CheckMode): ts.Type { + // Grammar checking + checkGrammarMethod(node); - const uninstantiatedType = checkFunctionExpressionOrObjectLiteralMethod(node, checkMode); - return instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); + // 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 === ts.SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); } - function instantiateTypeWithSingleGenericCallSignature(node: ts.Expression | ts.MethodDeclaration | ts.QualifiedName, type: ts.Type, checkMode?: CheckMode) { - if (checkMode && checkMode & (CheckMode.Inferential | CheckMode.SkipGenericFunctions)) { - const callSignature = getSingleSignature(type, ts.SignatureKind.Call, /*allowMembers*/ true); - const constructSignature = getSingleSignature(type, ts.SignatureKind.Construct, /*allowMembers*/ true); - const signature = callSignature || constructSignature; - if (signature && signature.typeParameters) { - const contextualType = getApparentTypeOfContextualType(node as ts.Expression, ts.ContextFlags.NoConstraints); - if (contextualType) { - const contextualSignature = getSingleSignature(getNonNullableType(contextualType), callSignature ? ts.SignatureKind.Call : ts.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 && !ts.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 = ts.map(context.inferences, info => createInferenceInfo(info.typeParameter)); - applyToParameterTypes(instantiatedSignature, contextualSignature, (source, target) => { - inferTypes(inferences, source, target, /*priority*/ 0, /*contravariant*/ true); + const uninstantiatedType = checkFunctionExpressionOrObjectLiteralMethod(node, checkMode); + return instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); + } + + function instantiateTypeWithSingleGenericCallSignature(node: ts.Expression | ts.MethodDeclaration | ts.QualifiedName, type: ts.Type, checkMode?: CheckMode) { + if (checkMode && checkMode & (CheckMode.Inferential | CheckMode.SkipGenericFunctions)) { + const callSignature = getSingleSignature(type, ts.SignatureKind.Call, /*allowMembers*/ true); + const constructSignature = getSingleSignature(type, ts.SignatureKind.Construct, /*allowMembers*/ true); + const signature = callSignature || constructSignature; + if (signature && signature.typeParameters) { + const contextualType = getApparentTypeOfContextualType(node as ts.Expression, ts.ContextFlags.NoConstraints); + if (contextualType) { + const contextualSignature = getSingleSignature(getNonNullableType(contextualType), callSignature ? ts.SignatureKind.Call : ts.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 && !ts.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 = ts.map(context.inferences, info => createInferenceInfo(info.typeParameter)); + applyToParameterTypes(instantiatedSignature, contextualSignature, (source, target) => { + inferTypes(inferences, source, target, /*priority*/ 0, /*contravariant*/ true); + }); + if (ts.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 (ts.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 = ts.concatenate(context.inferredTypeParameters, uniqueTypeParameters); - return getOrCreateTypeFromSignature(instantiatedSignature); - } + // 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 = ts.concatenate(context.inferredTypeParameters, uniqueTypeParameters); + return getOrCreateTypeFromSignature(instantiatedSignature); } } - return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context)); } + return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context)); } } } - return type; } + return type; + } - function skippedGenericFunction(node: ts.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 |= ts.InferenceFlags.SkippedGenericFunction; - } + function skippedGenericFunction(node: ts.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 |= ts.InferenceFlags.SkippedGenericFunction; } + } - function hasInferenceCandidates(info: ts.InferenceInfo) { - return !!(info.candidates || info.contraCandidates); - } + function hasInferenceCandidates(info: ts.InferenceInfo) { + return !!(info.candidates || info.contraCandidates); + } - function hasOverlappingInferences(a: ts.InferenceInfo[], b: ts.InferenceInfo[]) { - for (let i = 0; i < a.length; i++) { - if (hasInferenceCandidates(a[i]) && hasInferenceCandidates(b[i])) { - return true; - } + function hasOverlappingInferences(a: ts.InferenceInfo[], b: ts.InferenceInfo[]) { + for (let i = 0; i < a.length; i++) { + if (hasInferenceCandidates(a[i]) && hasInferenceCandidates(b[i])) { + return true; } - return false; } + return false; + } - function mergeInferences(target: ts.InferenceInfo[], source: ts.InferenceInfo[]) { - for (let i = 0; i < target.length; i++) { - if (!hasInferenceCandidates(target[i]) && hasInferenceCandidates(source[i])) { - target[i] = source[i]; - } + function mergeInferences(target: ts.InferenceInfo[], source: ts.InferenceInfo[]) { + for (let i = 0; i < target.length; i++) { + if (!hasInferenceCandidates(target[i]) && hasInferenceCandidates(source[i])) { + target[i] = source[i]; } } + } - function getUniqueTypeParameters(context: ts.InferenceContext, typeParameters: readonly ts.TypeParameter[]): readonly ts.TypeParameter[] { - const result: ts.TypeParameter[] = []; - let oldTypeParameters: ts.TypeParameter[] | undefined; - let newTypeParameters: ts.TypeParameter[] | undefined; - for (const tp of typeParameters) { - const name = tp.symbol.escapedName; - if (hasTypeParameterByName(context.inferredTypeParameters, name) || hasTypeParameterByName(result, name)) { - const newName = getUniqueTypeParameterName(ts.concatenate(context.inferredTypeParameters, result), name); - const symbol = createSymbol(ts.SymbolFlags.TypeParameter, newName); - const newTypeParameter = createTypeParameter(symbol); - newTypeParameter.target = tp; - oldTypeParameters = ts.append(oldTypeParameters, tp); - newTypeParameters = ts.append(newTypeParameters, newTypeParameter); - result.push(newTypeParameter); - } - else { - result.push(tp); - } + function getUniqueTypeParameters(context: ts.InferenceContext, typeParameters: readonly ts.TypeParameter[]): readonly ts.TypeParameter[] { + const result: ts.TypeParameter[] = []; + let oldTypeParameters: ts.TypeParameter[] | undefined; + let newTypeParameters: ts.TypeParameter[] | undefined; + for (const tp of typeParameters) { + const name = tp.symbol.escapedName; + if (hasTypeParameterByName(context.inferredTypeParameters, name) || hasTypeParameterByName(result, name)) { + const newName = getUniqueTypeParameterName(ts.concatenate(context.inferredTypeParameters, result), name); + const symbol = createSymbol(ts.SymbolFlags.TypeParameter, newName); + const newTypeParameter = createTypeParameter(symbol); + newTypeParameter.target = tp; + oldTypeParameters = ts.append(oldTypeParameters, tp); + newTypeParameters = ts.append(newTypeParameters, newTypeParameter); + result.push(newTypeParameter); } - if (newTypeParameters) { - const mapper = createTypeMapper(oldTypeParameters!, newTypeParameters); - for (const tp of newTypeParameters) { - tp.mapper = mapper; - } + else { + result.push(tp); } - return result; - } - - function hasTypeParameterByName(typeParameters: readonly ts.TypeParameter[] | undefined, name: ts.__String) { - return ts.some(typeParameters, tp => tp.symbol.escapedName === name); } - - function getUniqueTypeParameterName(typeParameters: readonly ts.TypeParameter[], baseName: ts.__String) { - let len = (baseName as string).length; - while (len > 1 && (baseName as string).charCodeAt(len - 1) >= ts.CharacterCodes._0 && (baseName as string).charCodeAt(len - 1) <= ts.CharacterCodes._9) - len--; - const s = (baseName as string).slice(0, len); - for (let index = 1; true; index++) { - const augmentedName = (s + index as ts.__String); - if (!hasTypeParameterByName(typeParameters, augmentedName)) { - return augmentedName; - } + if (newTypeParameters) { + const mapper = createTypeMapper(oldTypeParameters!, newTypeParameters); + for (const tp of newTypeParameters) { + tp.mapper = mapper; } } + return result; + } + + function hasTypeParameterByName(typeParameters: readonly ts.TypeParameter[] | undefined, name: ts.__String) { + return ts.some(typeParameters, tp => tp.symbol.escapedName === name); + } - function getReturnTypeOfSingleNonGenericCallSignature(funcType: ts.Type) { - const signature = getSingleCallSignature(funcType); - if (signature && !signature.typeParameters) { - return getReturnTypeOfSignature(signature); + function getUniqueTypeParameterName(typeParameters: readonly ts.TypeParameter[], baseName: ts.__String) { + let len = (baseName as string).length; + while (len > 1 && (baseName as string).charCodeAt(len - 1) >= ts.CharacterCodes._0 && (baseName as string).charCodeAt(len - 1) <= ts.CharacterCodes._9) + len--; + const s = (baseName as string).slice(0, len); + for (let index = 1; true; index++) { + const augmentedName = (s + index as ts.__String); + if (!hasTypeParameterByName(typeParameters, augmentedName)) { + return augmentedName; } } + } - function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: ts.CallChain) { - const funcType = checkExpression(expr.expression); - const nonOptionalType = getOptionalExpressionType(funcType, expr.expression); - const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType); - return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType); + function getReturnTypeOfSingleNonGenericCallSignature(funcType: ts.Type) { + const signature = getSingleCallSignature(funcType); + if (signature && !signature.typeParameters) { + return getReturnTypeOfSignature(signature); } + } - /** - * 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: ts.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 & ts.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; - ts.setNodeFlags(node, node.flags | ts.NodeFlags.TypeCached); - } - return type; - } + function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: ts.CallChain) { + const funcType = checkExpression(expr.expression); + const nonOptionalType = getOptionalExpressionType(funcType, expr.expression); + const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType); + return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType); + } - function getQuickTypeOfExpression(node: ts.Expression) { - let expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); - if (ts.isJSDocTypeAssertion(expr)) { - const type = ts.getJSDocTypeAssertionType(expr); - if (!ts.isConstTypeReference(type)) { - return getTypeFromTypeNode(type); - } - } - expr = ts.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 (ts.isCallExpression(expr) && expr.expression.kind !== ts.SyntaxKind.SuperKeyword && !ts.isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) { - const type = ts.isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) : - getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression)); - if (type) { - return type; - } - } - else if (ts.isAssertionExpression(expr) && !ts.isConstTypeReference(expr.type)) { - return getTypeFromTypeNode((expr as ts.TypeAssertion).type); - } - else if (node.kind === ts.SyntaxKind.NumericLiteral || node.kind === ts.SyntaxKind.StringLiteral || - node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.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. + */ + function getTypeOfExpression(node: ts.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 & ts.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; + ts.setNodeFlags(node, node.flags | ts.NodeFlags.TypeCached); + } + return type; + } - /** - * 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: ts.Expression) { - const links = getNodeLinks(node); - if (links.contextFreeType) { - return links.contextFreeType; + function getQuickTypeOfExpression(node: ts.Expression) { + let expr = ts.skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + if (ts.isJSDocTypeAssertion(expr)) { + const type = ts.getJSDocTypeAssertionType(expr); + if (!ts.isConstTypeReference(type)) { + return getTypeFromTypeNode(type); } - const saveContextualType = node.contextualType; - node.contextualType = anyType; - try { - const type = links.contextFreeType = checkExpression(node, CheckMode.SkipContextSensitive); + } + expr = ts.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 (ts.isCallExpression(expr) && expr.expression.kind !== ts.SyntaxKind.SuperKeyword && !ts.isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) { + const type = ts.isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) : + getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression)); + if (type) { 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 (ts.isAssertionExpression(expr) && !ts.isConstTypeReference(expr.type)) { + return getTypeFromTypeNode((expr as ts.TypeAssertion).type); + } + else if (node.kind === ts.SyntaxKind.NumericLiteral || node.kind === ts.SyntaxKind.StringLiteral || + node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) { + return checkExpression(node); + } + return undefined; + } - function checkExpression(node: ts.Expression | ts.QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): ts.Type { - ts.tracing?.push(ts.tracing.Phase.Check, "checkExpression", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); - 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; - ts.tracing?.pop(); + /** + * 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: ts.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; } + 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 checkConstEnumAccess(node: ts.Expression | ts.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 === ts.SyntaxKind.PropertyAccessExpression && (node.parent as ts.PropertyAccessExpression).expression === node) || - (node.parent.kind === ts.SyntaxKind.ElementAccessExpression && (node.parent as ts.ElementAccessExpression).expression === node) || - ((node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.QualifiedName) && isInRightSideOfImportOrExportAssignment(node as ts.Identifier) || - (node.parent.kind === ts.SyntaxKind.TypeQuery && (node.parent as ts.TypeQueryNode).exprName === node)) || - (node.parent.kind === ts.SyntaxKind.ExportSpecifier); // We allow reexporting const enums + function checkExpression(node: ts.Expression | ts.QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): ts.Type { + ts.tracing?.push(ts.tracing.Phase.Check, "checkExpression", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); + 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; + ts.tracing?.pop(); + return type; + } - if (!ok) { - error(node, ts.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); - } + function checkConstEnumAccess(node: ts.Expression | ts.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 === ts.SyntaxKind.PropertyAccessExpression && (node.parent as ts.PropertyAccessExpression).expression === node) || + (node.parent.kind === ts.SyntaxKind.ElementAccessExpression && (node.parent as ts.ElementAccessExpression).expression === node) || + ((node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.QualifiedName) && isInRightSideOfImportOrExportAssignment(node as ts.Identifier) || + (node.parent.kind === ts.SyntaxKind.TypeQuery && (node.parent as ts.TypeQueryNode).exprName === node)) || + (node.parent.kind === ts.SyntaxKind.ExportSpecifier); // We allow reexporting const enums - if (compilerOptions.isolatedModules) { - ts.Debug.assert(!!(type.symbol.flags & ts.SymbolFlags.ConstEnum)); - const constEnumDeclaration = type.symbol.valueDeclaration as ts.EnumDeclaration; - if (constEnumDeclaration.flags & ts.NodeFlags.Ambient) { - error(node, ts.Diagnostics.Cannot_access_ambient_const_enums_when_the_isolatedModules_flag_is_provided); - } - } + if (!ok) { + error(node, ts.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); } - function checkParenthesizedExpression(node: ts.ParenthesizedExpression, checkMode?: CheckMode): ts.Type { - if (ts.hasJSDocNodes(node) && ts.isJSDocTypeAssertion(node)) { - const type = ts.getJSDocTypeAssertionType(node); - return checkAssertionWorker(type, type, node.expression, checkMode); + if (compilerOptions.isolatedModules) { + ts.Debug.assert(!!(type.symbol.flags & ts.SymbolFlags.ConstEnum)); + const constEnumDeclaration = type.symbol.valueDeclaration as ts.EnumDeclaration; + if (constEnumDeclaration.flags & ts.NodeFlags.Ambient) { + error(node, ts.Diagnostics.Cannot_access_ambient_const_enums_when_the_isolatedModules_flag_is_provided); } - return checkExpression(node.expression, checkMode); } + } - function checkExpressionWorker(node: ts.Expression | ts.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 ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - cancellationToken.throwIfCancellationRequested(); - } - } + function checkParenthesizedExpression(node: ts.ParenthesizedExpression, checkMode?: CheckMode): ts.Type { + if (ts.hasJSDocNodes(node) && ts.isJSDocTypeAssertion(node)) { + const type = ts.getJSDocTypeAssertionType(node); + return checkAssertionWorker(type, type, node.expression, checkMode); + } + return checkExpression(node.expression, checkMode); + } + + function checkExpressionWorker(node: ts.Expression | ts.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 ts.SyntaxKind.Identifier: - return checkIdentifier(node as ts.Identifier, checkMode); - case ts.SyntaxKind.PrivateIdentifier: - return checkPrivateIdentifierExpression(node as ts.PrivateIdentifier); - case ts.SyntaxKind.ThisKeyword: - return checkThisExpression(node); - case ts.SyntaxKind.SuperKeyword: - return checkSuperExpression(node); - case ts.SyntaxKind.NullKeyword: - return nullWideningType; - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.StringLiteral: - return getFreshTypeOfLiteralType(getStringLiteralType((node as ts.StringLiteralLike).text)); - case ts.SyntaxKind.NumericLiteral: - checkGrammarNumericLiteral(node as ts.NumericLiteral); - return getFreshTypeOfLiteralType(getNumberLiteralType(+(node as ts.NumericLiteral).text)); - case ts.SyntaxKind.BigIntLiteral: - checkGrammarBigIntLiteral(node as ts.BigIntLiteral); - return getFreshTypeOfLiteralType(getBigIntLiteralType({ - negative: false, - base10Value: ts.parsePseudoBigInt((node as ts.BigIntLiteral).text) - })); - case ts.SyntaxKind.TrueKeyword: - return trueType; - case ts.SyntaxKind.FalseKeyword: - return falseType; - case ts.SyntaxKind.TemplateExpression: - return checkTemplateExpression(node as ts.TemplateExpression); - case ts.SyntaxKind.RegularExpressionLiteral: - return globalRegExpType; - case ts.SyntaxKind.ArrayLiteralExpression: - return checkArrayLiteral(node as ts.ArrayLiteralExpression, checkMode, forceTuple); - case ts.SyntaxKind.ObjectLiteralExpression: - return checkObjectLiteral(node as ts.ObjectLiteralExpression, checkMode); - case ts.SyntaxKind.PropertyAccessExpression: - return checkPropertyAccessExpression(node as ts.PropertyAccessExpression, checkMode); - case ts.SyntaxKind.QualifiedName: - return checkQualifiedName(node as ts.QualifiedName, checkMode); - case ts.SyntaxKind.ElementAccessExpression: - return checkIndexedAccess(node as ts.ElementAccessExpression, checkMode); - case ts.SyntaxKind.CallExpression: - if ((node as ts.CallExpression).expression.kind === ts.SyntaxKind.ImportKeyword) { - return checkImportCallExpression(node as ts.ImportCall); - } - // falls through - case ts.SyntaxKind.NewExpression: - return checkCallExpression(node as ts.CallExpression, checkMode); - case ts.SyntaxKind.TaggedTemplateExpression: - return checkTaggedTemplateExpression(node as ts.TaggedTemplateExpression); - case ts.SyntaxKind.ParenthesizedExpression: - return checkParenthesizedExpression(node as ts.ParenthesizedExpression, checkMode); case ts.SyntaxKind.ClassExpression: - return checkClassExpression(node as ts.ClassExpression); case ts.SyntaxKind.FunctionExpression: case ts.SyntaxKind.ArrowFunction: - return checkFunctionExpressionOrObjectLiteralMethod(node as ts.FunctionExpression | ts.ArrowFunction, checkMode); - case ts.SyntaxKind.TypeOfExpression: - return checkTypeOfExpression(node as ts.TypeOfExpression); - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.AsExpression: - return checkAssertion(node as ts.AssertionExpression); - case ts.SyntaxKind.NonNullExpression: - return checkNonNullAssertion(node as ts.NonNullExpression); - case ts.SyntaxKind.ExpressionWithTypeArguments: - return checkExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); - case ts.SyntaxKind.MetaProperty: - return checkMetaProperty(node as ts.MetaProperty); - case ts.SyntaxKind.DeleteExpression: - return checkDeleteExpression(node as ts.DeleteExpression); - case ts.SyntaxKind.VoidExpression: - return checkVoidExpression(node as ts.VoidExpression); - case ts.SyntaxKind.AwaitExpression: - return checkAwaitExpression(node as ts.AwaitExpression); - case ts.SyntaxKind.PrefixUnaryExpression: - return checkPrefixUnaryExpression(node as ts.PrefixUnaryExpression); - case ts.SyntaxKind.PostfixUnaryExpression: - return checkPostfixUnaryExpression(node as ts.PostfixUnaryExpression); - case ts.SyntaxKind.BinaryExpression: - return checkBinaryExpression(node as ts.BinaryExpression, checkMode); - case ts.SyntaxKind.ConditionalExpression: - return checkConditionalExpression(node as ts.ConditionalExpression, checkMode); - case ts.SyntaxKind.SpreadElement: - return checkSpreadExpression(node as ts.SpreadElement, checkMode); - case ts.SyntaxKind.OmittedExpression: - return undefinedWideningType; - case ts.SyntaxKind.YieldExpression: - return checkYieldExpression(node as ts.YieldExpression); - case ts.SyntaxKind.SyntheticExpression: - return checkSyntheticExpression(node as ts.SyntheticExpression); - case ts.SyntaxKind.JsxExpression: - return checkJsxExpression(node as ts.JsxExpression, checkMode); - case ts.SyntaxKind.JsxElement: - return checkJsxElement(node as ts.JsxElement, checkMode); - case ts.SyntaxKind.JsxSelfClosingElement: - return checkJsxSelfClosingElement(node as ts.JsxSelfClosingElement, checkMode); - case ts.SyntaxKind.JsxFragment: - return checkJsxFragment(node as ts.JsxFragment); - case ts.SyntaxKind.JsxAttributes: - return checkJsxAttributes(node as ts.JsxAttributes, checkMode); - case ts.SyntaxKind.JsxOpeningElement: - ts.Debug.fail("Shouldn't ever directly check a JsxOpeningElement"); + cancellationToken.throwIfCancellationRequested(); } - return errorType; } + switch (kind) { + case ts.SyntaxKind.Identifier: + return checkIdentifier(node as ts.Identifier, checkMode); + case ts.SyntaxKind.PrivateIdentifier: + return checkPrivateIdentifierExpression(node as ts.PrivateIdentifier); + case ts.SyntaxKind.ThisKeyword: + return checkThisExpression(node); + case ts.SyntaxKind.SuperKeyword: + return checkSuperExpression(node); + case ts.SyntaxKind.NullKeyword: + return nullWideningType; + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.StringLiteral: + return getFreshTypeOfLiteralType(getStringLiteralType((node as ts.StringLiteralLike).text)); + case ts.SyntaxKind.NumericLiteral: + checkGrammarNumericLiteral(node as ts.NumericLiteral); + return getFreshTypeOfLiteralType(getNumberLiteralType(+(node as ts.NumericLiteral).text)); + case ts.SyntaxKind.BigIntLiteral: + checkGrammarBigIntLiteral(node as ts.BigIntLiteral); + return getFreshTypeOfLiteralType(getBigIntLiteralType({ + negative: false, + base10Value: ts.parsePseudoBigInt((node as ts.BigIntLiteral).text) + })); + case ts.SyntaxKind.TrueKeyword: + return trueType; + case ts.SyntaxKind.FalseKeyword: + return falseType; + case ts.SyntaxKind.TemplateExpression: + return checkTemplateExpression(node as ts.TemplateExpression); + case ts.SyntaxKind.RegularExpressionLiteral: + return globalRegExpType; + case ts.SyntaxKind.ArrayLiteralExpression: + return checkArrayLiteral(node as ts.ArrayLiteralExpression, checkMode, forceTuple); + case ts.SyntaxKind.ObjectLiteralExpression: + return checkObjectLiteral(node as ts.ObjectLiteralExpression, checkMode); + case ts.SyntaxKind.PropertyAccessExpression: + return checkPropertyAccessExpression(node as ts.PropertyAccessExpression, checkMode); + case ts.SyntaxKind.QualifiedName: + return checkQualifiedName(node as ts.QualifiedName, checkMode); + case ts.SyntaxKind.ElementAccessExpression: + return checkIndexedAccess(node as ts.ElementAccessExpression, checkMode); + case ts.SyntaxKind.CallExpression: + if ((node as ts.CallExpression).expression.kind === ts.SyntaxKind.ImportKeyword) { + return checkImportCallExpression(node as ts.ImportCall); + } + // falls through + case ts.SyntaxKind.NewExpression: + return checkCallExpression(node as ts.CallExpression, checkMode); + case ts.SyntaxKind.TaggedTemplateExpression: + return checkTaggedTemplateExpression(node as ts.TaggedTemplateExpression); + case ts.SyntaxKind.ParenthesizedExpression: + return checkParenthesizedExpression(node as ts.ParenthesizedExpression, checkMode); + case ts.SyntaxKind.ClassExpression: + return checkClassExpression(node as ts.ClassExpression); + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return checkFunctionExpressionOrObjectLiteralMethod(node as ts.FunctionExpression | ts.ArrowFunction, checkMode); + case ts.SyntaxKind.TypeOfExpression: + return checkTypeOfExpression(node as ts.TypeOfExpression); + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.AsExpression: + return checkAssertion(node as ts.AssertionExpression); + case ts.SyntaxKind.NonNullExpression: + return checkNonNullAssertion(node as ts.NonNullExpression); + case ts.SyntaxKind.ExpressionWithTypeArguments: + return checkExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); + case ts.SyntaxKind.MetaProperty: + return checkMetaProperty(node as ts.MetaProperty); + case ts.SyntaxKind.DeleteExpression: + return checkDeleteExpression(node as ts.DeleteExpression); + case ts.SyntaxKind.VoidExpression: + return checkVoidExpression(node as ts.VoidExpression); + case ts.SyntaxKind.AwaitExpression: + return checkAwaitExpression(node as ts.AwaitExpression); + case ts.SyntaxKind.PrefixUnaryExpression: + return checkPrefixUnaryExpression(node as ts.PrefixUnaryExpression); + case ts.SyntaxKind.PostfixUnaryExpression: + return checkPostfixUnaryExpression(node as ts.PostfixUnaryExpression); + case ts.SyntaxKind.BinaryExpression: + return checkBinaryExpression(node as ts.BinaryExpression, checkMode); + case ts.SyntaxKind.ConditionalExpression: + return checkConditionalExpression(node as ts.ConditionalExpression, checkMode); + case ts.SyntaxKind.SpreadElement: + return checkSpreadExpression(node as ts.SpreadElement, checkMode); + case ts.SyntaxKind.OmittedExpression: + return undefinedWideningType; + case ts.SyntaxKind.YieldExpression: + return checkYieldExpression(node as ts.YieldExpression); + case ts.SyntaxKind.SyntheticExpression: + return checkSyntheticExpression(node as ts.SyntheticExpression); + case ts.SyntaxKind.JsxExpression: + return checkJsxExpression(node as ts.JsxExpression, checkMode); + case ts.SyntaxKind.JsxElement: + return checkJsxElement(node as ts.JsxElement, checkMode); + case ts.SyntaxKind.JsxSelfClosingElement: + return checkJsxSelfClosingElement(node as ts.JsxSelfClosingElement, checkMode); + case ts.SyntaxKind.JsxFragment: + return checkJsxFragment(node as ts.JsxFragment); + case ts.SyntaxKind.JsxAttributes: + return checkJsxAttributes(node as ts.JsxAttributes, checkMode); + case ts.SyntaxKind.JsxOpeningElement: + ts.Debug.fail("Shouldn't ever directly check a JsxOpeningElement"); + } + return errorType; + } - // DECLARATION AND STATEMENT TYPE CHECKING + // DECLARATION AND STATEMENT TYPE CHECKING - function checkTypeParameter(node: ts.TypeParameterDeclaration) { - // Grammar Checking - checkGrammarModifiers(node); - if (node.expression) { - grammarErrorOnFirstToken(node.expression, ts.Diagnostics.Type_expected); - } + function checkTypeParameter(node: ts.TypeParameterDeclaration) { + // Grammar Checking + checkGrammarModifiers(node); + if (node.expression) { + grammarErrorOnFirstToken(node.expression, ts.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, ts.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, ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1); - } - checkNodeDeferred(node); - addLazyDiagnostic(() => checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_parameter_name_cannot_be_0)); + 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, ts.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, ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1); } + checkNodeDeferred(node); + addLazyDiagnostic(() => checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_parameter_name_cannot_be_0)); + } - function checkTypeParameterDeferred(node: ts.TypeParameterDeclaration) { - if (ts.isInterfaceDeclaration(node.parent) || ts.isClassLike(node.parent) || ts.isTypeAliasDeclaration(node.parent)) { - const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node)); - const modifiers = getVarianceModifiers(typeParameter); - if (modifiers) { - const symbol = getSymbolOfNode(node.parent); - if (ts.isTypeAliasDeclaration(node.parent) && !(ts.getObjectFlags(getDeclaredTypeOfSymbol(symbol)) & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped))) { - error(node, ts.Diagnostics.Variance_annotations_are_only_supported_in_type_aliases_for_object_function_constructor_and_mapped_types); - } - else if (modifiers === ts.ModifierFlags.In || modifiers === ts.ModifierFlags.Out) { - const source = createMarkerType(symbol, typeParameter, modifiers === ts.ModifierFlags.Out ? markerSubType : markerSuperType); - const target = createMarkerType(symbol, typeParameter, modifiers === ts.ModifierFlags.Out ? markerSuperType : markerSubType); - const saveVarianceTypeParameter = typeParameter; - varianceTypeParameter = typeParameter; - checkTypeAssignableTo(source, target, node, ts.Diagnostics.Type_0_is_not_assignable_to_type_1_as_implied_by_variance_annotation); - varianceTypeParameter = saveVarianceTypeParameter; - } + function checkTypeParameterDeferred(node: ts.TypeParameterDeclaration) { + if (ts.isInterfaceDeclaration(node.parent) || ts.isClassLike(node.parent) || ts.isTypeAliasDeclaration(node.parent)) { + const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node)); + const modifiers = getVarianceModifiers(typeParameter); + if (modifiers) { + const symbol = getSymbolOfNode(node.parent); + if (ts.isTypeAliasDeclaration(node.parent) && !(ts.getObjectFlags(getDeclaredTypeOfSymbol(symbol)) & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Mapped))) { + error(node, ts.Diagnostics.Variance_annotations_are_only_supported_in_type_aliases_for_object_function_constructor_and_mapped_types); + } + else if (modifiers === ts.ModifierFlags.In || modifiers === ts.ModifierFlags.Out) { + const source = createMarkerType(symbol, typeParameter, modifiers === ts.ModifierFlags.Out ? markerSubType : markerSuperType); + const target = createMarkerType(symbol, typeParameter, modifiers === ts.ModifierFlags.Out ? markerSuperType : markerSubType); + const saveVarianceTypeParameter = typeParameter; + varianceTypeParameter = typeParameter; + checkTypeAssignableTo(source, target, node, ts.Diagnostics.Type_0_is_not_assignable_to_type_1_as_implied_by_variance_annotation); + varianceTypeParameter = saveVarianceTypeParameter; } } } + } - function checkParameter(node: ts.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); + function checkParameter(node: ts.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 = ts.getContainingFunction(node)!; - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier)) { - if (!(func.kind === ts.SyntaxKind.Constructor && ts.nodeIsPresent(func.body))) { - error(node, ts.Diagnostics.A_parameter_property_is_only_allowed_in_a_constructor_implementation); - } - if (func.kind === ts.SyntaxKind.Constructor && ts.isIdentifier(node.name) && node.name.escapedText === "constructor") { - error(node.name, ts.Diagnostics.constructor_cannot_be_used_as_a_parameter_property_name); - } + checkVariableLikeDeclaration(node); + const func = ts.getContainingFunction(node)!; + if (ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier)) { + if (!(func.kind === ts.SyntaxKind.Constructor && ts.nodeIsPresent(func.body))) { + error(node, ts.Diagnostics.A_parameter_property_is_only_allowed_in_a_constructor_implementation); } - if (node.questionToken && ts.isBindingPattern(node.name) && (func as ts.FunctionLikeDeclaration).body) { - error(node, ts.Diagnostics.A_binding_pattern_parameter_cannot_be_optional_in_an_implementation_signature); + if (func.kind === ts.SyntaxKind.Constructor && ts.isIdentifier(node.name) && node.name.escapedText === "constructor") { + error(node.name, ts.Diagnostics.constructor_cannot_be_used_as_a_parameter_property_name); } - if (node.name && ts.isIdentifier(node.name) && (node.name.escapedText === "this" || node.name.escapedText === "new")) { - if (func.parameters.indexOf(node) !== 0) { - error(node, ts.Diagnostics.A_0_parameter_must_be_the_first_parameter, node.name.escapedText as string); - } - if (func.kind === ts.SyntaxKind.Constructor || func.kind === ts.SyntaxKind.ConstructSignature || func.kind === ts.SyntaxKind.ConstructorType) { - error(node, ts.Diagnostics.A_constructor_cannot_have_a_this_parameter); - } - if (func.kind === ts.SyntaxKind.ArrowFunction) { - error(node, ts.Diagnostics.An_arrow_function_cannot_have_a_this_parameter); - } - if (func.kind === ts.SyntaxKind.GetAccessor || func.kind === ts.SyntaxKind.SetAccessor) { - error(node, ts.Diagnostics.get_and_set_accessors_cannot_declare_this_parameters); - } + } + if (node.questionToken && ts.isBindingPattern(node.name) && (func as ts.FunctionLikeDeclaration).body) { + error(node, ts.Diagnostics.A_binding_pattern_parameter_cannot_be_optional_in_an_implementation_signature); + } + if (node.name && ts.isIdentifier(node.name) && (node.name.escapedText === "this" || node.name.escapedText === "new")) { + if (func.parameters.indexOf(node) !== 0) { + error(node, ts.Diagnostics.A_0_parameter_must_be_the_first_parameter, node.name.escapedText as string); } - - // 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 && !ts.isBindingPattern(node.name) && !isTypeAssignableTo(getReducedType(getTypeOfSymbol(node.symbol)), anyReadonlyArrayType)) { - error(node, ts.Diagnostics.A_rest_parameter_must_be_of_an_array_type); + if (func.kind === ts.SyntaxKind.Constructor || func.kind === ts.SyntaxKind.ConstructSignature || func.kind === ts.SyntaxKind.ConstructorType) { + error(node, ts.Diagnostics.A_constructor_cannot_have_a_this_parameter); + } + if (func.kind === ts.SyntaxKind.ArrowFunction) { + error(node, ts.Diagnostics.An_arrow_function_cannot_have_a_this_parameter); + } + if (func.kind === ts.SyntaxKind.GetAccessor || func.kind === ts.SyntaxKind.SetAccessor) { + error(node, ts.Diagnostics.get_and_set_accessors_cannot_declare_this_parameters); } } - function checkTypePredicate(node: ts.TypePredicateNode): void { - const parent = getTypePredicateParent(node); - if (!parent) { - // The parent must not be valid. - error(node, ts.Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); - return; - } + // 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 && !ts.isBindingPattern(node.name) && !isTypeAssignableTo(getReducedType(getTypeOfSymbol(node.symbol)), anyReadonlyArrayType)) { + error(node, ts.Diagnostics.A_rest_parameter_must_be_of_an_array_type); + } + } - const signature = getSignatureFromDeclaration(parent); - const typePredicate = getTypePredicateOfSignature(signature); - if (!typePredicate) { - return; - } + function checkTypePredicate(node: ts.TypePredicateNode): void { + const parent = getTypePredicateParent(node); + if (!parent) { + // The parent must not be valid. + error(node, ts.Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); + return; + } - checkSourceElement(node.type); + const signature = getSignatureFromDeclaration(parent); + const typePredicate = getTypePredicateOfSignature(signature); + if (!typePredicate) { + return; + } - const { parameterName } = node; - if (typePredicate.kind === ts.TypePredicateKind.This || typePredicate.kind === ts.TypePredicateKind.AssertsThis) { - getTypeFromThisTypeNode(parameterName as ts.ThisTypeNode); - } - else { - if (typePredicate.parameterIndex >= 0) { - if (signatureHasRestParameter(signature) && typePredicate.parameterIndex === signature.parameters.length - 1) { - error(parameterName, ts.Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); - } - else { - if (typePredicate.type) { - const leadingError = () => ts.chainDiagnosticMessages(/*details*/ undefined, ts.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); - } - } + checkSourceElement(node.type); + + const { parameterName } = node; + if (typePredicate.kind === ts.TypePredicateKind.This || typePredicate.kind === ts.TypePredicateKind.AssertsThis) { + getTypeFromThisTypeNode(parameterName as ts.ThisTypeNode); + } + else { + if (typePredicate.parameterIndex >= 0) { + if (signatureHasRestParameter(signature) && typePredicate.parameterIndex === signature.parameters.length - 1) { + error(parameterName, ts.Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); } - else if (parameterName) { - let hasReportedError = false; - for (const { name } of parent.parameters) { - if (ts.isBindingPattern(name) && - checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, parameterName, typePredicate.parameterName)) { - hasReportedError = true; - break; - } - } - if (!hasReportedError) { - error(node.parameterName, ts.Diagnostics.Cannot_find_parameter_0, typePredicate.parameterName); + else { + if (typePredicate.type) { + const leadingError = () => ts.chainDiagnosticMessages(/*details*/ undefined, ts.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); } } } - } - - function getTypePredicateParent(node: ts.Node): ts.SignatureDeclaration | undefined { - switch (node.parent.kind) { - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - const parent = node.parent as ts.SignatureDeclaration; - if (node === parent.type) { - return parent; + else if (parameterName) { + let hasReportedError = false; + for (const { name } of parent.parameters) { + if (ts.isBindingPattern(name) && + checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, parameterName, typePredicate.parameterName)) { + hasReportedError = true; + break; } + } + if (!hasReportedError) { + error(node.parameterName, ts.Diagnostics.Cannot_find_parameter_0, typePredicate.parameterName); + } } } + } - function checkIfTypePredicateVariableIsDeclaredInBindingPattern(pattern: ts.BindingPattern, predicateVariableNode: ts.Node, predicateVariableName: string) { - for (const element of pattern.elements) { - if (ts.isOmittedExpression(element)) { - continue; + function getTypePredicateParent(node: ts.Node): ts.SignatureDeclaration | undefined { + switch (node.parent.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + const parent = node.parent as ts.SignatureDeclaration; + if (node === parent.type) { + return parent; } + } + } + + function checkIfTypePredicateVariableIsDeclaredInBindingPattern(pattern: ts.BindingPattern, predicateVariableNode: ts.Node, predicateVariableName: string) { + for (const element of pattern.elements) { + if (ts.isOmittedExpression(element)) { + continue; + } - const name = element.name; - if (name.kind === ts.SyntaxKind.Identifier && name.escapedText === predicateVariableName) { - error(predicateVariableNode, ts.Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, predicateVariableName); + const name = element.name; + if (name.kind === ts.SyntaxKind.Identifier && name.escapedText === predicateVariableName) { + error(predicateVariableNode, ts.Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, predicateVariableName); + return true; + } + else if (name.kind === ts.SyntaxKind.ArrayBindingPattern || name.kind === ts.SyntaxKind.ObjectBindingPattern) { + if (checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, predicateVariableNode, predicateVariableName)) { return true; } - else if (name.kind === ts.SyntaxKind.ArrayBindingPattern || name.kind === ts.SyntaxKind.ObjectBindingPattern) { - if (checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, predicateVariableNode, predicateVariableName)) { - return true; - } - } } } + } - function checkSignatureDeclaration(node: ts.SignatureDeclaration) { - // Grammar checking - if (node.kind === ts.SyntaxKind.IndexSignature) { - checkGrammarIndexSignature(node as ts.SignatureDeclaration); - } - // TODO (yuisu): Remove this check in else-if when SyntaxKind.Construct is moved and ambient context is handled - else if (node.kind === ts.SyntaxKind.FunctionType || node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.ConstructorType || - node.kind === ts.SyntaxKind.CallSignature || node.kind === ts.SyntaxKind.Constructor || - node.kind === ts.SyntaxKind.ConstructSignature) { - checkGrammarFunctionLikeDeclaration(node as ts.FunctionLikeDeclaration); - } + function checkSignatureDeclaration(node: ts.SignatureDeclaration) { + // Grammar checking + if (node.kind === ts.SyntaxKind.IndexSignature) { + checkGrammarIndexSignature(node as ts.SignatureDeclaration); + } + // TODO (yuisu): Remove this check in else-if when SyntaxKind.Construct is moved and ambient context is handled + else if (node.kind === ts.SyntaxKind.FunctionType || node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.ConstructorType || + node.kind === ts.SyntaxKind.CallSignature || node.kind === ts.SyntaxKind.Constructor || + node.kind === ts.SyntaxKind.ConstructSignature) { + checkGrammarFunctionLikeDeclaration(node as ts.FunctionLikeDeclaration); + } - const functionFlags = ts.getFunctionFlags(node as ts.FunctionLikeDeclaration); - if (!(functionFlags & ts.FunctionFlags.Invalid)) { - // Async generators prior to ESNext require the __await and __asyncGenerator helpers - if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.AsyncGenerator && languageVersion < ts.ScriptTarget.ESNext) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.AsyncGeneratorIncludes); - } + const functionFlags = ts.getFunctionFlags(node as ts.FunctionLikeDeclaration); + if (!(functionFlags & ts.FunctionFlags.Invalid)) { + // Async generators prior to ESNext require the __await and __asyncGenerator helpers + if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.AsyncGenerator && languageVersion < ts.ScriptTarget.ESNext) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.AsyncGeneratorIncludes); + } - // Async functions prior to ES2017 require the __awaiter helper - if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async && languageVersion < ts.ScriptTarget.ES2017) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Awaiter); - } + // Async functions prior to ES2017 require the __awaiter helper + if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async && languageVersion < ts.ScriptTarget.ES2017) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Awaiter); + } - // Generator functions, Async functions, and Async Generator functions prior to - // ES2015 require the __generator helper - if ((functionFlags & ts.FunctionFlags.AsyncGenerator) !== ts.FunctionFlags.Normal && languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Generator); - } + // Generator functions, Async functions, and Async Generator functions prior to + // ES2015 require the __generator helper + if ((functionFlags & ts.FunctionFlags.AsyncGenerator) !== ts.FunctionFlags.Normal && languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Generator); } + } - checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); - checkUnmatchedJSDocParameters(node); + checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); + checkUnmatchedJSDocParameters(node); - ts.forEach(node.parameters, checkParameter); + ts.forEach(node.parameters, checkParameter); - // TODO(rbuckton): Should we start checking JSDoc types? - if (node.type) { - checkSourceElement(node.type); - } + // TODO(rbuckton): Should we start checking JSDoc types? + if (node.type) { + checkSourceElement(node.type); + } - addLazyDiagnostic(checkSignatureDeclarationDiagnostics); + addLazyDiagnostic(checkSignatureDeclarationDiagnostics); - function checkSignatureDeclarationDiagnostics() { - checkCollisionWithArgumentsInGeneratedCode(node); - const returnTypeNode = ts.getEffectiveReturnTypeNode(node); - if (noImplicitAny && !returnTypeNode) { - switch (node.kind) { - case ts.SyntaxKind.ConstructSignature: - error(node, ts.Diagnostics.Construct_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); - break; - case ts.SyntaxKind.CallSignature: - error(node, ts.Diagnostics.Call_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); - break; - } + function checkSignatureDeclarationDiagnostics() { + checkCollisionWithArgumentsInGeneratedCode(node); + const returnTypeNode = ts.getEffectiveReturnTypeNode(node); + if (noImplicitAny && !returnTypeNode) { + switch (node.kind) { + case ts.SyntaxKind.ConstructSignature: + error(node, ts.Diagnostics.Construct_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); + break; + case ts.SyntaxKind.CallSignature: + error(node, ts.Diagnostics.Call_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); + break; } + } - if (returnTypeNode) { - const functionFlags = ts.getFunctionFlags(node as ts.FunctionDeclaration); - if ((functionFlags & (ts.FunctionFlags.Invalid | ts.FunctionFlags.Generator)) === ts.FunctionFlags.Generator) { - const returnType = getTypeFromTypeNode(returnTypeNode); - if (returnType === voidType) { - error(returnTypeNode, ts.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 & ts.FunctionFlags.Async) !== 0) || anyType; - const generatorReturnType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, (functionFlags & ts.FunctionFlags.Async) !== 0) || generatorYieldType; - const generatorNextType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, (functionFlags & ts.FunctionFlags.Async) !== 0) || unknownType; - const generatorInstantiation = createGeneratorReturnType(generatorYieldType, generatorReturnType, generatorNextType, !!(functionFlags & ts.FunctionFlags.Async)); - checkTypeAssignableTo(generatorInstantiation, returnType, returnTypeNode); - } + if (returnTypeNode) { + const functionFlags = ts.getFunctionFlags(node as ts.FunctionDeclaration); + if ((functionFlags & (ts.FunctionFlags.Invalid | ts.FunctionFlags.Generator)) === ts.FunctionFlags.Generator) { + const returnType = getTypeFromTypeNode(returnTypeNode); + if (returnType === voidType) { + error(returnTypeNode, ts.Diagnostics.A_generator_cannot_have_a_void_type_annotation); } - else if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async) { - checkAsyncFunctionReturnType(node as ts.FunctionLikeDeclaration, returnTypeNode); + 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 & ts.FunctionFlags.Async) !== 0) || anyType; + const generatorReturnType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, (functionFlags & ts.FunctionFlags.Async) !== 0) || generatorYieldType; + const generatorNextType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, (functionFlags & ts.FunctionFlags.Async) !== 0) || unknownType; + const generatorInstantiation = createGeneratorReturnType(generatorYieldType, generatorReturnType, generatorNextType, !!(functionFlags & ts.FunctionFlags.Async)); + checkTypeAssignableTo(generatorInstantiation, returnType, returnTypeNode); } } - if (node.kind !== ts.SyntaxKind.IndexSignature && node.kind !== ts.SyntaxKind.JSDocFunctionType) { - registerForUnusedIdentifiersCheck(node); + else if ((functionFlags & ts.FunctionFlags.AsyncGenerator) === ts.FunctionFlags.Async) { + checkAsyncFunctionReturnType(node as ts.FunctionLikeDeclaration, returnTypeNode); } } + if (node.kind !== ts.SyntaxKind.IndexSignature && node.kind !== ts.SyntaxKind.JSDocFunctionType) { + registerForUnusedIdentifiersCheck(node); + } } + } - function checkClassForDuplicateDeclarations(node: ts.ClassLikeDeclaration) { - const instanceNames = new ts.Map(); - const staticNames = new ts.Map(); - // instance and static private identifiers share the same scope - const privateIdentifiers = new ts.Map(); - for (const member of node.members) { - if (member.kind === ts.SyntaxKind.Constructor) { - for (const param of (member as ts.ConstructorDeclaration).parameters) { - if (ts.isParameterPropertyDeclaration(param, member) && !ts.isBindingPattern(param.name)) { - addName(instanceNames, param.name, param.name.escapedText, DeclarationMeaning.GetOrSetAccessor); - } + function checkClassForDuplicateDeclarations(node: ts.ClassLikeDeclaration) { + const instanceNames = new ts.Map(); + const staticNames = new ts.Map(); + // instance and static private identifiers share the same scope + const privateIdentifiers = new ts.Map(); + for (const member of node.members) { + if (member.kind === ts.SyntaxKind.Constructor) { + for (const param of (member as ts.ConstructorDeclaration).parameters) { + if (ts.isParameterPropertyDeclaration(param, member) && !ts.isBindingPattern(param.name)) { + addName(instanceNames, param.name, param.name.escapedText, DeclarationMeaning.GetOrSetAccessor); } } - else { - const isStaticMember = ts.isStatic(member); - const name = member.name; - if (!name) { - continue; - } - const isPrivate = ts.isPrivateIdentifier(name); - const privateStaticFlags = isPrivate && isStaticMember ? DeclarationMeaning.PrivateStatic : 0; - const names = isPrivate ? privateIdentifiers : - isStaticMember ? staticNames : - instanceNames; + } + else { + const isStaticMember = ts.isStatic(member); + const name = member.name; + if (!name) { + continue; + } + const isPrivate = ts.isPrivateIdentifier(name); + const privateStaticFlags = isPrivate && isStaticMember ? DeclarationMeaning.PrivateStatic : 0; + const names = isPrivate ? privateIdentifiers : + isStaticMember ? staticNames : + instanceNames; - const memberName = name && ts.getPropertyNameForPropertyNameNode(name); - if (memberName) { - switch (member.kind) { - case ts.SyntaxKind.GetAccessor: - addName(names, name, memberName, DeclarationMeaning.GetAccessor | privateStaticFlags); - break; + const memberName = name && ts.getPropertyNameForPropertyNameNode(name); + if (memberName) { + switch (member.kind) { + case ts.SyntaxKind.GetAccessor: + addName(names, name, memberName, DeclarationMeaning.GetAccessor | privateStaticFlags); + break; - case ts.SyntaxKind.SetAccessor: - addName(names, name, memberName, DeclarationMeaning.SetAccessor | privateStaticFlags); - break; + case ts.SyntaxKind.SetAccessor: + addName(names, name, memberName, DeclarationMeaning.SetAccessor | privateStaticFlags); + break; - case ts.SyntaxKind.PropertyDeclaration: - addName(names, name, memberName, DeclarationMeaning.GetOrSetAccessor | privateStaticFlags); - break; + case ts.SyntaxKind.PropertyDeclaration: + addName(names, name, memberName, DeclarationMeaning.GetOrSetAccessor | privateStaticFlags); + break; - case ts.SyntaxKind.MethodDeclaration: - addName(names, name, memberName, DeclarationMeaning.Method | privateStaticFlags); - break; - } + case ts.SyntaxKind.MethodDeclaration: + addName(names, name, memberName, DeclarationMeaning.Method | privateStaticFlags); + break; } } } + } - function addName(names: ts.UnderscoreEscapedMap, location: ts.Node, name: ts.__String, meaning: DeclarationMeaning) { - const prev = names.get(name); - if (prev) { - // For private identifiers, do not allow mixing of static and instance members with the same name - if ((prev & DeclarationMeaning.PrivateStatic) !== (meaning & DeclarationMeaning.PrivateStatic)) { - error(location, ts.Diagnostics.Duplicate_identifier_0_Static_and_instance_elements_cannot_share_the_same_private_name, ts.getTextOfNode(location)); - } - else { - const prevIsMethod = !!(prev & DeclarationMeaning.Method); - const isMethod = !!(meaning & DeclarationMeaning.Method); - if (prevIsMethod || isMethod) { - if (prevIsMethod !== isMethod) { - error(location, ts.Diagnostics.Duplicate_identifier_0, ts.getTextOfNode(location)); - } - // If this is a method/method duplication is might be an overload, so this will be handled when overloads are considered - } - else if (prev & meaning & ~DeclarationMeaning.PrivateStatic) { + function addName(names: ts.UnderscoreEscapedMap, location: ts.Node, name: ts.__String, meaning: DeclarationMeaning) { + const prev = names.get(name); + if (prev) { + // For private identifiers, do not allow mixing of static and instance members with the same name + if ((prev & DeclarationMeaning.PrivateStatic) !== (meaning & DeclarationMeaning.PrivateStatic)) { + error(location, ts.Diagnostics.Duplicate_identifier_0_Static_and_instance_elements_cannot_share_the_same_private_name, ts.getTextOfNode(location)); + } + else { + const prevIsMethod = !!(prev & DeclarationMeaning.Method); + const isMethod = !!(meaning & DeclarationMeaning.Method); + if (prevIsMethod || isMethod) { + if (prevIsMethod !== isMethod) { error(location, ts.Diagnostics.Duplicate_identifier_0, ts.getTextOfNode(location)); } - else { - names.set(name, prev | meaning); - } + // If this is a method/method duplication is might be an overload, so this will be handled when overloads are considered + } + else if (prev & meaning & ~DeclarationMeaning.PrivateStatic) { + error(location, ts.Diagnostics.Duplicate_identifier_0, ts.getTextOfNode(location)); + } + else { + names.set(name, prev | meaning); } } - else { - names.set(name, meaning); - } + } + else { + names.set(name, meaning); } } + } - /** - * 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: ts.ClassLikeDeclaration) { - for (const member of node.members) { - const memberNameNode = member.name; - const isStaticMember = ts.isStatic(member); - if (isStaticMember && memberNameNode) { - const memberName = ts.getPropertyNameForPropertyNameNode(memberNameNode); - switch (memberName) { - case "name": - case "length": - case "caller": - case "arguments": - case "prototype": - const message = ts.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; - } + /** + * 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: ts.ClassLikeDeclaration) { + for (const member of node.members) { + const memberNameNode = member.name; + const isStaticMember = ts.isStatic(member); + if (isStaticMember && memberNameNode) { + const memberName = ts.getPropertyNameForPropertyNameNode(memberNameNode); + switch (memberName) { + case "name": + case "length": + case "caller": + case "arguments": + case "prototype": + const message = ts.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 checkObjectTypeForDuplicateDeclarations(node: ts.TypeLiteralNode | ts.InterfaceDeclaration) { - const names = new ts.Map(); - for (const member of node.members) { - if (member.kind === ts.SyntaxKind.PropertySignature) { - let memberName: string; - const name = member.name!; - switch (name.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - memberName = name.text; - break; - case ts.SyntaxKind.Identifier: - memberName = ts.idText(name); - break; - default: - continue; - } + function checkObjectTypeForDuplicateDeclarations(node: ts.TypeLiteralNode | ts.InterfaceDeclaration) { + const names = new ts.Map(); + for (const member of node.members) { + if (member.kind === ts.SyntaxKind.PropertySignature) { + let memberName: string; + const name = member.name!; + switch (name.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + memberName = name.text; + break; + case ts.SyntaxKind.Identifier: + memberName = ts.idText(name); + break; + default: + continue; + } - if (names.get(memberName)) { - error(ts.getNameOfDeclaration(member.symbol.valueDeclaration), ts.Diagnostics.Duplicate_identifier_0, memberName); - error(member.name, ts.Diagnostics.Duplicate_identifier_0, memberName); - } - else { - names.set(memberName, true); - } + if (names.get(memberName)) { + error(ts.getNameOfDeclaration(member.symbol.valueDeclaration), ts.Diagnostics.Duplicate_identifier_0, memberName); + error(member.name, ts.Diagnostics.Duplicate_identifier_0, memberName); + } + else { + names.set(memberName, true); } } } + } - function checkTypeForDuplicateIndexSignatures(node: ts.Node) { - if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { - const nodeSymbol = getSymbolOfNode(node as ts.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 && nodeSymbol.declarations.length > 0 && nodeSymbol.declarations[0] !== node) { - return; - } + function checkTypeForDuplicateIndexSignatures(node: ts.Node) { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { + const nodeSymbol = getSymbolOfNode(node as ts.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 && nodeSymbol.declarations.length > 0 && nodeSymbol.declarations[0] !== node) { + return; } + } - // 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?.declarations) { - const indexSignatureMap = new ts.Map(); - for (const declaration of (indexSymbol.declarations as ts.IndexSignatureDeclaration[])) { - if (declaration.parameters.length === 1 && declaration.parameters[0].type) { - forEachType(getTypeFromTypeNode(declaration.parameters[0].type), type => { - const entry = indexSignatureMap.get(getTypeId(type)); - if (entry) { - entry.declarations.push(declaration); - } - else { - indexSignatureMap.set(getTypeId(type), { type, declarations: [declaration] }); - } - }); - } - } - indexSignatureMap.forEach(entry => { - if (entry.declarations.length > 1) { - for (const declaration of entry.declarations) { - error(declaration, ts.Diagnostics.Duplicate_index_signature_for_type_0, typeToString(entry.type)); + // 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?.declarations) { + const indexSignatureMap = new ts.Map(); + for (const declaration of (indexSymbol.declarations as ts.IndexSignatureDeclaration[])) { + if (declaration.parameters.length === 1 && declaration.parameters[0].type) { + forEachType(getTypeFromTypeNode(declaration.parameters[0].type), type => { + const entry = indexSignatureMap.get(getTypeId(type)); + if (entry) { + entry.declarations.push(declaration); } - } - }); + else { + indexSignatureMap.set(getTypeId(type), { type, declarations: [declaration] }); + } + }); + } } + indexSignatureMap.forEach(entry => { + if (entry.declarations.length > 1) { + for (const declaration of entry.declarations) { + error(declaration, ts.Diagnostics.Duplicate_index_signature_for_type_0, typeToString(entry.type)); + } + } + }); } + } - function checkPropertyDeclaration(node: ts.PropertyDeclaration | ts.PropertySignature) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarProperty(node)) - checkGrammarComputedPropertyName(node.name); - checkVariableLikeDeclaration(node); + function checkPropertyDeclaration(node: ts.PropertyDeclaration | ts.PropertySignature) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarProperty(node)) + checkGrammarComputedPropertyName(node.name); + checkVariableLikeDeclaration(node); - setNodeLinksForPrivateIdentifierScope(node); + setNodeLinksForPrivateIdentifierScope(node); - // property signatures already report "initializer not allowed in ambient context" elsewhere - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract) && node.kind === ts.SyntaxKind.PropertyDeclaration && node.initializer) { - error(node, ts.Diagnostics.Property_0_cannot_have_an_initializer_because_it_is_marked_abstract, ts.declarationNameToString(node.name)); - } + // property signatures already report "initializer not allowed in ambient context" elsewhere + if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract) && node.kind === ts.SyntaxKind.PropertyDeclaration && node.initializer) { + error(node, ts.Diagnostics.Property_0_cannot_have_an_initializer_because_it_is_marked_abstract, ts.declarationNameToString(node.name)); } + } - function checkPropertySignature(node: ts.PropertySignature) { - if (ts.isPrivateIdentifier(node.name)) { - error(node, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - } - return checkPropertyDeclaration(node); + function checkPropertySignature(node: ts.PropertySignature) { + if (ts.isPrivateIdentifier(node.name)) { + error(node, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); } + return checkPropertyDeclaration(node); + } - function checkMethodDeclaration(node: ts.MethodDeclaration | ts.MethodSignature) { - // Grammar checking - if (!checkGrammarMethod(node)) - checkGrammarComputedPropertyName(node.name); - - // Grammar checking for modifiers is done inside the function checkGrammarFunctionLikeDeclaration - checkFunctionOrMethodDeclaration(node); + function checkMethodDeclaration(node: ts.MethodDeclaration | ts.MethodSignature) { + // Grammar checking + if (!checkGrammarMethod(node)) + checkGrammarComputedPropertyName(node.name); - // method signatures already report "implementation not allowed in ambient context" elsewhere - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract) && node.kind === ts.SyntaxKind.MethodDeclaration && node.body) { - error(node, ts.Diagnostics.Method_0_cannot_have_an_implementation_because_it_is_marked_abstract, ts.declarationNameToString(node.name)); - } + // Grammar checking for modifiers is done inside the function checkGrammarFunctionLikeDeclaration + checkFunctionOrMethodDeclaration(node); - // Private named methods are only allowed in class declarations - if (ts.isPrivateIdentifier(node.name) && !ts.getContainingClass(node)) { - error(node, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - } + // method signatures already report "implementation not allowed in ambient context" elsewhere + if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract) && node.kind === ts.SyntaxKind.MethodDeclaration && node.body) { + error(node, ts.Diagnostics.Method_0_cannot_have_an_implementation_because_it_is_marked_abstract, ts.declarationNameToString(node.name)); + } - setNodeLinksForPrivateIdentifierScope(node); + // Private named methods are only allowed in class declarations + if (ts.isPrivateIdentifier(node.name) && !ts.getContainingClass(node)) { + error(node, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); } - function setNodeLinksForPrivateIdentifierScope(node: ts.PropertyDeclaration | ts.PropertySignature | ts.MethodDeclaration | ts.MethodSignature | ts.AccessorDeclaration) { - if (ts.isPrivateIdentifier(node.name) && languageVersion < ts.ScriptTarget.ESNext) { - for (let lexicalScope = ts.getEnclosingBlockScopeContainer(node); !!lexicalScope; lexicalScope = ts.getEnclosingBlockScopeContainer(lexicalScope)) { - getNodeLinks(lexicalScope).flags |= ts.NodeCheckFlags.ContainsClassWithPrivateIdentifiers; - } + setNodeLinksForPrivateIdentifierScope(node); + } - // If this is a private element in a class expression inside the body of a loop, - // then we must use a block-scoped binding to store the additional variables required - // to transform private elements. - if (ts.isClassExpression(node.parent)) { - const enclosingIterationStatement = getEnclosingIterationStatement(node.parent); - if (enclosingIterationStatement) { - getNodeLinks(node.name).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; - getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; - } - } + function setNodeLinksForPrivateIdentifierScope(node: ts.PropertyDeclaration | ts.PropertySignature | ts.MethodDeclaration | ts.MethodSignature | ts.AccessorDeclaration) { + if (ts.isPrivateIdentifier(node.name) && languageVersion < ts.ScriptTarget.ESNext) { + for (let lexicalScope = ts.getEnclosingBlockScopeContainer(node); !!lexicalScope; lexicalScope = ts.getEnclosingBlockScopeContainer(lexicalScope)) { + getNodeLinks(lexicalScope).flags |= ts.NodeCheckFlags.ContainsClassWithPrivateIdentifiers; } - } - function checkClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { - checkGrammarDecoratorsAndModifiers(node); - - ts.forEachChild(node, checkSourceElement); + // If this is a private element in a class expression inside the body of a loop, + // then we must use a block-scoped binding to store the additional variables required + // to transform private elements. + if (ts.isClassExpression(node.parent)) { + const enclosingIterationStatement = getEnclosingIterationStatement(node.parent); + if (enclosingIterationStatement) { + getNodeLinks(node.name).flags |= ts.NodeCheckFlags.BlockScopedBindingInLoop; + getNodeLinks(enclosingIterationStatement).flags |= ts.NodeCheckFlags.LoopWithCapturedBlockScopedBinding; + } + } } + } - function checkConstructorDeclaration(node: ts.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); + function checkClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { + checkGrammarDecoratorsAndModifiers(node); - checkSourceElement(node.body); + ts.forEachChild(node, checkSourceElement); + } - const symbol = getSymbolOfNode(node); - const firstDeclaration = ts.getDeclarationOfKind(symbol, node.kind); + function checkConstructorDeclaration(node: ts.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); - // Only type check the symbol once - if (node === firstDeclaration) { - checkFunctionOrConstructorSymbol(symbol); - } + checkSourceElement(node.body); - // exit early in the case of signature - super checks are not relevant to them - if (ts.nodeIsMissing(node.body)) { - return; - } + const symbol = getSymbolOfNode(node); + const firstDeclaration = ts.getDeclarationOfKind(symbol, node.kind); - addLazyDiagnostic(checkConstructorDeclarationDiagnostics); + // 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 (ts.nodeIsMissing(node.body)) { return; + } - function isInstancePropertyWithInitializerOrPrivateIdentifierProperty(n: ts.Node): boolean { - if (ts.isPrivateIdentifierClassElementDeclaration(n)) { - return true; - } - return n.kind === ts.SyntaxKind.PropertyDeclaration && - !ts.isStatic(n) && - !!(n as ts.PropertyDeclaration).initializer; - } + addLazyDiagnostic(checkConstructorDeclarationDiagnostics); - function checkConstructorDeclarationDiagnostics() { - // 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 as ts.ClassDeclaration; - if (ts.getClassExtendsHeritageElement(containingClassDecl)) { - captureLexicalThis(node.parent, containingClassDecl); - const classExtendsNull = classDeclarationExtendsNull(containingClassDecl); - const superCall = findFirstSuperCall(node.body!); - if (superCall) { - if (classExtendsNull) { - error(superCall, ts.Diagnostics.A_constructor_cannot_contain_a_super_call_when_its_class_extends_null); - } + return; - // A super call must be root-level in a constructor 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. + function isInstancePropertyWithInitializerOrPrivateIdentifierProperty(n: ts.Node): boolean { + if (ts.isPrivateIdentifierClassElementDeclaration(n)) { + return true; + } + return n.kind === ts.SyntaxKind.PropertyDeclaration && + !ts.isStatic(n) && + !!(n as ts.PropertyDeclaration).initializer; + } + + function checkConstructorDeclarationDiagnostics() { + // 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 as ts.ClassDeclaration; + if (ts.getClassExtendsHeritageElement(containingClassDecl)) { + captureLexicalThis(node.parent, containingClassDecl); + const classExtendsNull = classDeclarationExtendsNull(containingClassDecl); + const superCall = findFirstSuperCall(node.body!); + if (superCall) { + if (classExtendsNull) { + error(superCall, ts.Diagnostics.A_constructor_cannot_contain_a_super_call_when_its_class_extends_null); + } + + // A super call must be root-level in a constructor 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 superCallShouldBeRootLevel = (ts.getEmitScriptTarget(compilerOptions) !== ts.ScriptTarget.ESNext || !useDefineForClassFields) && + (ts.some((node.parent as ts.ClassDeclaration).members, isInstancePropertyWithInitializerOrPrivateIdentifierProperty) || + ts.some(node.parameters, p => ts.hasSyntacticModifier(p, ts.ModifierFlags.ParameterPropertyModifier))); + + if (superCallShouldBeRootLevel) { + // Until we have better flow analysis, it is an error to place the super call within any kind of block or conditional + // See GH #8277 + if (!superCallIsRootLevelInConstructor(superCall, node.body!)) { + error(superCall, ts.Diagnostics.A_super_call_must_be_a_root_level_statement_within_a_constructor_of_a_derived_class_that_contains_initialized_properties_parameter_properties_or_private_identifiers); + } + // Skip past any prologue directives to check statements for referring to 'super' or 'this' before a super call + else { + let superCallStatement: ts.ExpressionStatement | undefined; - const superCallShouldBeRootLevel = (ts.getEmitScriptTarget(compilerOptions) !== ts.ScriptTarget.ESNext || !useDefineForClassFields) && - (ts.some((node.parent as ts.ClassDeclaration).members, isInstancePropertyWithInitializerOrPrivateIdentifierProperty) || - ts.some(node.parameters, p => ts.hasSyntacticModifier(p, ts.ModifierFlags.ParameterPropertyModifier))); + for (const statement of node.body!.statements) { + if (ts.isExpressionStatement(statement) && ts.isSuperCall(ts.skipOuterExpressions(statement.expression))) { + superCallStatement = statement; + break; + } + if (nodeImmediatelyReferencesSuperOrThis(statement)) { + break; + } + } - if (superCallShouldBeRootLevel) { // Until we have better flow analysis, it is an error to place the super call within any kind of block or conditional // See GH #8277 - if (!superCallIsRootLevelInConstructor(superCall, node.body!)) { - error(superCall, ts.Diagnostics.A_super_call_must_be_a_root_level_statement_within_a_constructor_of_a_derived_class_that_contains_initialized_properties_parameter_properties_or_private_identifiers); - } - // Skip past any prologue directives to check statements for referring to 'super' or 'this' before a super call - else { - let superCallStatement: ts.ExpressionStatement | undefined; - - for (const statement of node.body!.statements) { - if (ts.isExpressionStatement(statement) && ts.isSuperCall(ts.skipOuterExpressions(statement.expression))) { - superCallStatement = statement; - break; - } - if (nodeImmediatelyReferencesSuperOrThis(statement)) { - break; - } - } - - // Until we have better flow analysis, it is an error to place the super call within any kind of block or conditional - // See GH #8277 - if (superCallStatement === undefined) { - error(node, ts.Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_to_refer_to_super_or_this_when_a_derived_class_contains_initialized_properties_parameter_properties_or_private_identifiers); - } + if (superCallStatement === undefined) { + error(node, ts.Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_to_refer_to_super_or_this_when_a_derived_class_contains_initialized_properties_parameter_properties_or_private_identifiers); } } } - else if (!classExtendsNull) { - error(node, ts.Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call); - } + } + else if (!classExtendsNull) { + error(node, ts.Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call); } } } + } - function superCallIsRootLevelInConstructor(superCall: ts.Node, body: ts.Block) { - const superCallParent = ts.walkUpParenthesizedExpressions(superCall.parent); - return ts.isExpressionStatement(superCallParent) && superCallParent.parent === body; - } - - function nodeImmediatelyReferencesSuperOrThis(node: ts.Node): boolean { - if (node.kind === ts.SyntaxKind.SuperKeyword || node.kind === ts.SyntaxKind.ThisKeyword) { - return true; - } + function superCallIsRootLevelInConstructor(superCall: ts.Node, body: ts.Block) { + const superCallParent = ts.walkUpParenthesizedExpressions(superCall.parent); + return ts.isExpressionStatement(superCallParent) && superCallParent.parent === body; + } - if (ts.isThisContainerOrFunctionBlock(node)) { - return false; - } + function nodeImmediatelyReferencesSuperOrThis(node: ts.Node): boolean { + if (node.kind === ts.SyntaxKind.SuperKeyword || node.kind === ts.SyntaxKind.ThisKeyword) { + return true; + } - return !!ts.forEachChild(node, nodeImmediatelyReferencesSuperOrThis); + if (ts.isThisContainerOrFunctionBlock(node)) { + return false; } - function checkAccessorDeclaration(node: ts.AccessorDeclaration) { - addLazyDiagnostic(checkAccessorDeclarationDiagnostics); - checkSourceElement(node.body); - setNodeLinksForPrivateIdentifierScope(node); + return !!ts.forEachChild(node, nodeImmediatelyReferencesSuperOrThis); + } - function checkAccessorDeclarationDiagnostics() { - // Grammar checking accessors - if (!checkGrammarFunctionLikeDeclaration(node) && !checkGrammarAccessor(node)) - checkGrammarComputedPropertyName(node.name); + function checkAccessorDeclaration(node: ts.AccessorDeclaration) { + addLazyDiagnostic(checkAccessorDeclarationDiagnostics); + checkSourceElement(node.body); + setNodeLinksForPrivateIdentifierScope(node); - checkDecorators(node); - checkSignatureDeclaration(node); - if (node.kind === ts.SyntaxKind.GetAccessor) { - if (!(node.flags & ts.NodeFlags.Ambient) && ts.nodeIsPresent(node.body) && (node.flags & ts.NodeFlags.HasImplicitReturn)) { - if (!(node.flags & ts.NodeFlags.HasExplicitReturn)) { - error(node.name, ts.Diagnostics.A_get_accessor_must_return_a_value); - } + function checkAccessorDeclarationDiagnostics() { + // Grammar checking accessors + if (!checkGrammarFunctionLikeDeclaration(node) && !checkGrammarAccessor(node)) + checkGrammarComputedPropertyName(node.name); + + checkDecorators(node); + checkSignatureDeclaration(node); + if (node.kind === ts.SyntaxKind.GetAccessor) { + if (!(node.flags & ts.NodeFlags.Ambient) && ts.nodeIsPresent(node.body) && (node.flags & ts.NodeFlags.HasImplicitReturn)) { + if (!(node.flags & ts.NodeFlags.HasExplicitReturn)) { + error(node.name, ts.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 === ts.SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - } + } + // 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 === ts.SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); + } - if (hasBindableName(node)) { - // TypeScript 1.0 spec (April 2014): 8.4.3 - // Accessors for the same member name must specify the same accessibility. - const symbol = getSymbolOfNode(node); - const getter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.GetAccessor); - const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); - if (getter && setter && !(getNodeCheckFlags(getter) & ts.NodeCheckFlags.TypeChecked)) { - getNodeLinks(getter).flags |= ts.NodeCheckFlags.TypeChecked; - const getterFlags = ts.getEffectiveModifierFlags(getter); - const setterFlags = ts.getEffectiveModifierFlags(setter); - if ((getterFlags & ts.ModifierFlags.Abstract) !== (setterFlags & ts.ModifierFlags.Abstract)) { - error(getter.name, ts.Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); - error(setter.name, ts.Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); - } - if (((getterFlags & ts.ModifierFlags.Protected) && !(setterFlags & (ts.ModifierFlags.Protected | ts.ModifierFlags.Private))) || - ((getterFlags & ts.ModifierFlags.Private) && !(setterFlags & ts.ModifierFlags.Private))) { - error(getter.name, ts.Diagnostics.A_get_accessor_must_be_at_least_as_accessible_as_the_setter); - error(setter.name, ts.Diagnostics.A_get_accessor_must_be_at_least_as_accessible_as_the_setter); - } + if (hasBindableName(node)) { + // TypeScript 1.0 spec (April 2014): 8.4.3 + // Accessors for the same member name must specify the same accessibility. + const symbol = getSymbolOfNode(node); + const getter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.GetAccessor); + const setter = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.SetAccessor); + if (getter && setter && !(getNodeCheckFlags(getter) & ts.NodeCheckFlags.TypeChecked)) { + getNodeLinks(getter).flags |= ts.NodeCheckFlags.TypeChecked; + const getterFlags = ts.getEffectiveModifierFlags(getter); + const setterFlags = ts.getEffectiveModifierFlags(setter); + if ((getterFlags & ts.ModifierFlags.Abstract) !== (setterFlags & ts.ModifierFlags.Abstract)) { + error(getter.name, ts.Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); + error(setter.name, ts.Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); + } + if (((getterFlags & ts.ModifierFlags.Protected) && !(setterFlags & (ts.ModifierFlags.Protected | ts.ModifierFlags.Private))) || + ((getterFlags & ts.ModifierFlags.Private) && !(setterFlags & ts.ModifierFlags.Private))) { + error(getter.name, ts.Diagnostics.A_get_accessor_must_be_at_least_as_accessible_as_the_setter); + error(setter.name, ts.Diagnostics.A_get_accessor_must_be_at_least_as_accessible_as_the_setter); + } - const getterType = getAnnotatedAccessorType(getter); - const setterType = getAnnotatedAccessorType(setter); - if (getterType && setterType) { - checkTypeAssignableTo(getterType, setterType, getter, ts.Diagnostics.The_return_type_of_a_get_accessor_must_be_assignable_to_its_set_accessor_type); - } + const getterType = getAnnotatedAccessorType(getter); + const setterType = getAnnotatedAccessorType(setter); + if (getterType && setterType) { + checkTypeAssignableTo(getterType, setterType, getter, ts.Diagnostics.The_return_type_of_a_get_accessor_must_be_assignable_to_its_set_accessor_type); } } - const returnType = getTypeOfAccessors(getSymbolOfNode(node)); - if (node.kind === ts.SyntaxKind.GetAccessor) { - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); - } + } + const returnType = getTypeOfAccessors(getSymbolOfNode(node)); + if (node.kind === ts.SyntaxKind.GetAccessor) { + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); } } + } - function checkMissingDeclaration(node: ts.Node) { - checkDecorators(node); - } + function checkMissingDeclaration(node: ts.Node) { + checkDecorators(node); + } - function getEffectiveTypeArguments(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, typeParameters: readonly ts.TypeParameter[]): ts.Type[] { - return fillMissingTypeArguments(ts.map(node.typeArguments!, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), ts.isInJSFile(node)); - } + function getEffectiveTypeArguments(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, typeParameters: readonly ts.TypeParameter[]): ts.Type[] { + return fillMissingTypeArguments(ts.map(node.typeArguments!, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), ts.isInJSFile(node)); + } - function checkTypeArgumentConstraints(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, typeParameters: readonly ts.TypeParameter[]): boolean { - let typeArguments: ts.Type[] | undefined; - let mapper: ts.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], ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1); + function checkTypeArgumentConstraints(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, typeParameters: readonly ts.TypeParameter[]): boolean { + let typeArguments: ts.Type[] | undefined; + let mapper: ts.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], ts.Diagnostics.Type_0_does_not_satisfy_the_constraint_1); } - return result; } + return result; + } - function getTypeParametersForTypeReference(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments) { - const type = getTypeFromTypeReference(node); - if (!isErrorType(type)) { - const symbol = getNodeLinks(node).resolvedSymbol; - if (symbol) { - return symbol.flags & ts.SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters || - (ts.getObjectFlags(type) & ts.ObjectFlags.Reference ? (type as ts.TypeReference).target.localTypeParameters : undefined); - } + function getTypeParametersForTypeReference(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments) { + const type = getTypeFromTypeReference(node); + if (!isErrorType(type)) { + const symbol = getNodeLinks(node).resolvedSymbol; + if (symbol) { + return symbol.flags & ts.SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters || + (ts.getObjectFlags(type) & ts.ObjectFlags.Reference ? (type as ts.TypeReference).target.localTypeParameters : undefined); } - return undefined; } + return undefined; + } - function checkTypeReferenceNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments) { - checkGrammarTypeArguments(node, node.typeArguments); - if (node.kind === ts.SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !ts.isInJSFile(node) && !ts.isInJSDoc(node)) { - grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, ts.Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + function checkTypeReferenceNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments) { + checkGrammarTypeArguments(node, node.typeArguments); + if (node.kind === ts.SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !ts.isInJSFile(node) && !ts.isInJSDoc(node)) { + grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, ts.Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + } + ts.forEach(node.typeArguments, checkSourceElement); + const type = getTypeFromTypeReference(node); + if (!isErrorType(type)) { + if (node.typeArguments) { + addLazyDiagnostic(() => { + const typeParameters = getTypeParametersForTypeReference(node); + if (typeParameters) { + checkTypeArgumentConstraints(node, typeParameters); + } + }); } - ts.forEach(node.typeArguments, checkSourceElement); - const type = getTypeFromTypeReference(node); - if (!isErrorType(type)) { - if (node.typeArguments) { - addLazyDiagnostic(() => { - const typeParameters = getTypeParametersForTypeReference(node); - if (typeParameters) { - checkTypeArgumentConstraints(node, typeParameters); - } - }); + const symbol = getNodeLinks(node).resolvedSymbol; + if (symbol) { + if (ts.some(symbol.declarations, d => isTypeDeclaration(d) && !!(d.flags & ts.NodeFlags.Deprecated))) { + addDeprecatedSuggestion(getDeprecatedSuggestionNode(node), symbol.declarations!, symbol.escapedName as string); } - const symbol = getNodeLinks(node).resolvedSymbol; - if (symbol) { - if (ts.some(symbol.declarations, d => isTypeDeclaration(d) && !!(d.flags & ts.NodeFlags.Deprecated))) { - addDeprecatedSuggestion(getDeprecatedSuggestionNode(node), symbol.declarations!, symbol.escapedName as string); - } - if (type.flags & ts.TypeFlags.Enum && symbol.flags & ts.SymbolFlags.EnumMember) { - error(node, ts.Diagnostics.Enum_type_0_has_members_with_initializers_that_are_not_literals, typeToString(type)); - } + if (type.flags & ts.TypeFlags.Enum && symbol.flags & ts.SymbolFlags.EnumMember) { + error(node, ts.Diagnostics.Enum_type_0_has_members_with_initializers_that_are_not_literals, typeToString(type)); } } } + } - function getTypeArgumentConstraint(node: ts.TypeNode): ts.Type | undefined { - const typeReferenceNode = ts.tryCast(node.parent, ts.isTypeReferenceType); - if (!typeReferenceNode) - return undefined; - const typeParameters = getTypeParametersForTypeReference(typeReferenceNode); - if (!typeParameters) - return undefined; - const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]); - return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters))); - } + function getTypeArgumentConstraint(node: ts.TypeNode): ts.Type | undefined { + const typeReferenceNode = ts.tryCast(node.parent, ts.isTypeReferenceType); + if (!typeReferenceNode) + return undefined; + const typeParameters = getTypeParametersForTypeReference(typeReferenceNode); + if (!typeParameters) + return undefined; + const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]); + return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters))); + } - function checkTypeQuery(node: ts.TypeQueryNode) { - getTypeFromTypeQueryNode(node); - } + function checkTypeQuery(node: ts.TypeQueryNode) { + getTypeFromTypeQueryNode(node); + } - function checkTypeLiteral(node: ts.TypeLiteralNode) { - ts.forEach(node.members, checkSourceElement); - addLazyDiagnostic(checkTypeLiteralDiagnostics); + function checkTypeLiteral(node: ts.TypeLiteralNode) { + ts.forEach(node.members, checkSourceElement); + addLazyDiagnostic(checkTypeLiteralDiagnostics); - function checkTypeLiteralDiagnostics() { - const type = getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); - checkIndexConstraints(type, type.symbol); - checkTypeForDuplicateIndexSignatures(node); - checkObjectTypeForDuplicateDeclarations(node); - } + function checkTypeLiteralDiagnostics() { + const type = getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); + checkIndexConstraints(type, type.symbol); + checkTypeForDuplicateIndexSignatures(node); + checkObjectTypeForDuplicateDeclarations(node); } + } - function checkArrayType(node: ts.ArrayTypeNode) { - checkSourceElement(node.elementType); - } + function checkArrayType(node: ts.ArrayTypeNode) { + checkSourceElement(node.elementType); + } - function checkTupleType(node: ts.TupleTypeNode) { - const elementTypes = node.elements; - let seenOptionalElement = false; - let seenRestElement = false; - const hasNamedElement = ts.some(elementTypes, ts.isNamedTupleMember); - for (const e of elementTypes) { - if (e.kind !== ts.SyntaxKind.NamedTupleMember && hasNamedElement) { - grammarErrorOnNode(e, ts.Diagnostics.Tuple_members_must_all_have_names_or_all_not_have_names); + function checkTupleType(node: ts.TupleTypeNode) { + const elementTypes = node.elements; + let seenOptionalElement = false; + let seenRestElement = false; + const hasNamedElement = ts.some(elementTypes, ts.isNamedTupleMember); + for (const e of elementTypes) { + if (e.kind !== ts.SyntaxKind.NamedTupleMember && hasNamedElement) { + grammarErrorOnNode(e, ts.Diagnostics.Tuple_members_must_all_have_names_or_all_not_have_names); + break; + } + const flags = getTupleElementFlags(e); + if (flags & ts.ElementFlags.Variadic) { + const type = getTypeFromTypeNode((e as ts.RestTypeNode | ts.NamedTupleMember).type); + if (!isArrayLikeType(type)) { + error(e, ts.Diagnostics.A_rest_element_type_must_be_an_array_type); break; } - const flags = getTupleElementFlags(e); - if (flags & ts.ElementFlags.Variadic) { - const type = getTypeFromTypeNode((e as ts.RestTypeNode | ts.NamedTupleMember).type); - if (!isArrayLikeType(type)) { - error(e, ts.Diagnostics.A_rest_element_type_must_be_an_array_type); - break; - } - if (isArrayType(type) || isTupleType(type) && type.target.combinedFlags & ts.ElementFlags.Rest) { - seenRestElement = true; - } - } - else if (flags & ts.ElementFlags.Rest) { - if (seenRestElement) { - grammarErrorOnNode(e, ts.Diagnostics.A_rest_element_cannot_follow_another_rest_element); - break; - } + if (isArrayType(type) || isTupleType(type) && type.target.combinedFlags & ts.ElementFlags.Rest) { seenRestElement = true; } - else if (flags & ts.ElementFlags.Optional) { - if (seenRestElement) { - grammarErrorOnNode(e, ts.Diagnostics.An_optional_element_cannot_follow_a_rest_element); - break; - } - seenOptionalElement = true; + } + else if (flags & ts.ElementFlags.Rest) { + if (seenRestElement) { + grammarErrorOnNode(e, ts.Diagnostics.A_rest_element_cannot_follow_another_rest_element); + break; } - else if (seenOptionalElement) { - grammarErrorOnNode(e, ts.Diagnostics.A_required_element_cannot_follow_an_optional_element); + seenRestElement = true; + } + else if (flags & ts.ElementFlags.Optional) { + if (seenRestElement) { + grammarErrorOnNode(e, ts.Diagnostics.An_optional_element_cannot_follow_a_rest_element); break; } + seenOptionalElement = true; + } + else if (seenOptionalElement) { + grammarErrorOnNode(e, ts.Diagnostics.A_required_element_cannot_follow_an_optional_element); + break; } - ts.forEach(node.elements, checkSourceElement); - getTypeFromTypeNode(node); } + ts.forEach(node.elements, checkSourceElement); + getTypeFromTypeNode(node); + } - function checkUnionOrIntersectionType(node: ts.UnionOrIntersectionTypeNode) { - ts.forEach(node.types, checkSourceElement); - getTypeFromTypeNode(node); - } + function checkUnionOrIntersectionType(node: ts.UnionOrIntersectionTypeNode) { + ts.forEach(node.types, checkSourceElement); + getTypeFromTypeNode(node); + } - function checkIndexedAccessIndexType(type: ts.Type, accessNode: ts.IndexedAccessTypeNode | ts.ElementAccessExpression) { - if (!(type.flags & ts.TypeFlags.IndexedAccess)) { - return type; - } - // Check if the index type is assignable to 'keyof T' for the object type. - const objectType = (type as ts.IndexedAccessType).objectType; - const indexType = (type as ts.IndexedAccessType).indexType; - if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) { - if (accessNode.kind === ts.SyntaxKind.ElementAccessExpression && ts.isAssignmentTarget(accessNode) && - ts.getObjectFlags(objectType) & ts.ObjectFlags.Mapped && getMappedTypeModifiers(objectType as ts.MappedType) & MappedTypeModifiers.IncludeReadonly) { - error(accessNode, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); - } - return type; - } - // 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, numberType) && isTypeAssignableToKind(indexType, ts.TypeFlags.NumberLike)) { - return type; + function checkIndexedAccessIndexType(type: ts.Type, accessNode: ts.IndexedAccessTypeNode | ts.ElementAccessExpression) { + if (!(type.flags & ts.TypeFlags.IndexedAccess)) { + return type; + } + // Check if the index type is assignable to 'keyof T' for the object type. + const objectType = (type as ts.IndexedAccessType).objectType; + const indexType = (type as ts.IndexedAccessType).indexType; + if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) { + if (accessNode.kind === ts.SyntaxKind.ElementAccessExpression && ts.isAssignmentTarget(accessNode) && + ts.getObjectFlags(objectType) & ts.ObjectFlags.Mapped && getMappedTypeModifiers(objectType as ts.MappedType) & MappedTypeModifiers.IncludeReadonly) { + error(accessNode, ts.Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); } - if (isGenericObjectType(objectType)) { - const propertyName = getPropertyNameFromIndex(indexType, accessNode); - if (propertyName) { - const propertySymbol = forEachType(apparentObjectType, t => getPropertyOfType(t, propertyName)); - if (propertySymbol && ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & ts.ModifierFlags.NonPublicAccessibilityModifier) { - error(accessNode, ts.Diagnostics.Private_or_protected_member_0_cannot_be_accessed_on_a_type_parameter, ts.unescapeLeadingUnderscores(propertyName)); - return errorType; - } + return type; + } + // 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, numberType) && isTypeAssignableToKind(indexType, ts.TypeFlags.NumberLike)) { + return type; + } + if (isGenericObjectType(objectType)) { + const propertyName = getPropertyNameFromIndex(indexType, accessNode); + if (propertyName) { + const propertySymbol = forEachType(apparentObjectType, t => getPropertyOfType(t, propertyName)); + if (propertySymbol && ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & ts.ModifierFlags.NonPublicAccessibilityModifier) { + error(accessNode, ts.Diagnostics.Private_or_protected_member_0_cannot_be_accessed_on_a_type_parameter, ts.unescapeLeadingUnderscores(propertyName)); + return errorType; } } - error(accessNode, ts.Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); - return errorType; - } - - function checkIndexedAccessType(node: ts.IndexedAccessTypeNode) { - checkSourceElement(node.objectType); - checkSourceElement(node.indexType); - checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node); } + error(accessNode, ts.Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); + return errorType; + } - function checkMappedType(node: ts.MappedTypeNode) { - checkGrammarMappedType(node); - checkSourceElement(node.typeParameter); - checkSourceElement(node.nameType); - checkSourceElement(node.type); + function checkIndexedAccessType(node: ts.IndexedAccessTypeNode) { + checkSourceElement(node.objectType); + checkSourceElement(node.indexType); + checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node); + } - if (!node.type) { - reportImplicitAny(node, anyType); - } + function checkMappedType(node: ts.MappedTypeNode) { + checkGrammarMappedType(node); + checkSourceElement(node.typeParameter); + checkSourceElement(node.nameType); + checkSourceElement(node.type); - const type = getTypeFromMappedTypeNode(node) as ts.MappedType; - const nameType = getNameTypeFromMappedType(type); - if (nameType) { - checkTypeAssignableTo(nameType, keyofConstraintType, node.nameType); - } - else { - const constraintType = getConstraintTypeFromMappedType(type); - checkTypeAssignableTo(constraintType, keyofConstraintType, ts.getEffectiveConstraintOfTypeParameter(node.typeParameter)); - } + if (!node.type) { + reportImplicitAny(node, anyType); } - function checkGrammarMappedType(node: ts.MappedTypeNode) { - if (node.members?.length) { - return grammarErrorOnNode(node.members[0], ts.Diagnostics.A_mapped_type_may_not_declare_properties_or_methods); - } + const type = getTypeFromMappedTypeNode(node) as ts.MappedType; + const nameType = getNameTypeFromMappedType(type); + if (nameType) { + checkTypeAssignableTo(nameType, keyofConstraintType, node.nameType); } - - function checkThisType(node: ts.ThisTypeNode) { - getTypeFromThisTypeNode(node); + else { + const constraintType = getConstraintTypeFromMappedType(type); + checkTypeAssignableTo(constraintType, keyofConstraintType, ts.getEffectiveConstraintOfTypeParameter(node.typeParameter)); } + } - function checkTypeOperator(node: ts.TypeOperatorNode) { - checkGrammarTypeOperatorNode(node); - checkSourceElement(node.type); + function checkGrammarMappedType(node: ts.MappedTypeNode) { + if (node.members?.length) { + return grammarErrorOnNode(node.members[0], ts.Diagnostics.A_mapped_type_may_not_declare_properties_or_methods); } + } - function checkConditionalType(node: ts.ConditionalTypeNode) { - ts.forEachChild(node, checkSourceElement); - } + function checkThisType(node: ts.ThisTypeNode) { + getTypeFromThisTypeNode(node); + } - function checkInferType(node: ts.InferTypeNode) { - if (!ts.findAncestor(node, n => n.parent && n.parent.kind === ts.SyntaxKind.ConditionalType && (n.parent as ts.ConditionalTypeNode).extendsType === n)) { - grammarErrorOnNode(node, ts.Diagnostics.infer_declarations_are_only_permitted_in_the_extends_clause_of_a_conditional_type); - } - checkSourceElement(node.typeParameter); - const symbol = getSymbolOfNode(node.typeParameter); - if (symbol.declarations && symbol.declarations.length > 1) { - const links = getSymbolLinks(symbol); - if (!links.typeParametersChecked) { - links.typeParametersChecked = true; - const typeParameter = getDeclaredTypeOfTypeParameter(symbol); - const declarations: ts.TypeParameterDeclaration[] = ts.getDeclarationsOfKind(symbol, ts.SyntaxKind.TypeParameter); - if (!areTypeParametersIdentical(declarations, [typeParameter], decl => [decl])) { - // Report an error on every conflicting declaration. - const name = symbolToString(symbol); - for (const declaration of declarations) { - error(declaration.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_constraints, name); - } + function checkTypeOperator(node: ts.TypeOperatorNode) { + checkGrammarTypeOperatorNode(node); + checkSourceElement(node.type); + } + + function checkConditionalType(node: ts.ConditionalTypeNode) { + ts.forEachChild(node, checkSourceElement); + } + + function checkInferType(node: ts.InferTypeNode) { + if (!ts.findAncestor(node, n => n.parent && n.parent.kind === ts.SyntaxKind.ConditionalType && (n.parent as ts.ConditionalTypeNode).extendsType === n)) { + grammarErrorOnNode(node, ts.Diagnostics.infer_declarations_are_only_permitted_in_the_extends_clause_of_a_conditional_type); + } + checkSourceElement(node.typeParameter); + const symbol = getSymbolOfNode(node.typeParameter); + if (symbol.declarations && symbol.declarations.length > 1) { + const links = getSymbolLinks(symbol); + if (!links.typeParametersChecked) { + links.typeParametersChecked = true; + const typeParameter = getDeclaredTypeOfTypeParameter(symbol); + const declarations: ts.TypeParameterDeclaration[] = ts.getDeclarationsOfKind(symbol, ts.SyntaxKind.TypeParameter); + if (!areTypeParametersIdentical(declarations, [typeParameter], decl => [decl])) { + // Report an error on every conflicting declaration. + const name = symbolToString(symbol); + for (const declaration of declarations) { + error(declaration.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_constraints, name); } } } - registerForUnusedIdentifiersCheck(node); } + registerForUnusedIdentifiersCheck(node); + } - function checkTemplateLiteralType(node: ts.TemplateLiteralTypeNode) { - for (const span of node.templateSpans) { - checkSourceElement(span.type); - const type = getTypeFromTypeNode(span.type); - checkTypeAssignableTo(type, templateConstraintType, span.type); - } - getTypeFromTypeNode(node); + function checkTemplateLiteralType(node: ts.TemplateLiteralTypeNode) { + for (const span of node.templateSpans) { + checkSourceElement(span.type); + const type = getTypeFromTypeNode(span.type); + checkTypeAssignableTo(type, templateConstraintType, span.type); } + getTypeFromTypeNode(node); + } - function checkImportType(node: ts.ImportTypeNode) { - checkSourceElement(node.argument); + function checkImportType(node: ts.ImportTypeNode) { + checkSourceElement(node.argument); - if (node.assertions) { - const override = ts.getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode); - if (override) { - if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { - grammarErrorOnNode(node.assertions.assertClause, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext); - } + if (node.assertions) { + const override = ts.getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode); + if (override) { + if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { + grammarErrorOnNode(node.assertions.assertClause, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext); } } + } + + getTypeFromTypeNode(node); + } - getTypeFromTypeNode(node); + function checkNamedTupleMember(node: ts.NamedTupleMember) { + if (node.dotDotDotToken && node.questionToken) { + grammarErrorOnNode(node, ts.Diagnostics.A_tuple_member_cannot_be_both_optional_and_rest); + } + if (node.type.kind === ts.SyntaxKind.OptionalType) { + grammarErrorOnNode(node.type, ts.Diagnostics.A_labeled_tuple_element_is_declared_as_optional_with_a_question_mark_after_the_name_and_before_the_colon_rather_than_after_the_type); } + if (node.type.kind === ts.SyntaxKind.RestType) { + grammarErrorOnNode(node.type, ts.Diagnostics.A_labeled_tuple_element_is_declared_as_rest_with_a_before_the_name_rather_than_before_the_type); + } + checkSourceElement(node.type); + getTypeFromTypeNode(node); + } - function checkNamedTupleMember(node: ts.NamedTupleMember) { - if (node.dotDotDotToken && node.questionToken) { - grammarErrorOnNode(node, ts.Diagnostics.A_tuple_member_cannot_be_both_optional_and_rest); - } - if (node.type.kind === ts.SyntaxKind.OptionalType) { - grammarErrorOnNode(node.type, ts.Diagnostics.A_labeled_tuple_element_is_declared_as_optional_with_a_question_mark_after_the_name_and_before_the_colon_rather_than_after_the_type); - } - if (node.type.kind === ts.SyntaxKind.RestType) { - grammarErrorOnNode(node.type, ts.Diagnostics.A_labeled_tuple_element_is_declared_as_rest_with_a_before_the_name_rather_than_before_the_type); + function isPrivateWithinAmbient(node: ts.Node): boolean { + return (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private) || ts.isPrivateIdentifierClassElementDeclaration(node)) && !!(node.flags & ts.NodeFlags.Ambient); + } + + function getEffectiveDeclarationFlags(n: ts.Declaration, flagsToCheck: ts.ModifierFlags): ts.ModifierFlags { + let flags = ts.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 !== ts.SyntaxKind.InterfaceDeclaration && + n.parent.kind !== ts.SyntaxKind.ClassDeclaration && + n.parent.kind !== ts.SyntaxKind.ClassExpression && + n.flags & ts.NodeFlags.Ambient) { + if (!(flags & ts.ModifierFlags.Ambient) && !(ts.isModuleBlock(n.parent) && ts.isModuleDeclaration(n.parent.parent) && ts.isGlobalScopeAugmentation(n.parent.parent))) { + // It is nested in an ambient context, which means it is automatically exported + flags |= ts.ModifierFlags.Export; } - checkSourceElement(node.type); - getTypeFromTypeNode(node); + flags |= ts.ModifierFlags.Ambient; } - function isPrivateWithinAmbient(node: ts.Node): boolean { - return (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private) || ts.isPrivateIdentifierClassElementDeclaration(node)) && !!(node.flags & ts.NodeFlags.Ambient); - } + return flags & flagsToCheck; + } - function getEffectiveDeclarationFlags(n: ts.Declaration, flagsToCheck: ts.ModifierFlags): ts.ModifierFlags { - let flags = ts.getCombinedModifierFlags(n); + function checkFunctionOrConstructorSymbol(symbol: ts.Symbol): void { + addLazyDiagnostic(() => checkFunctionOrConstructorSymbolWorker(symbol)); + } - // 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 !== ts.SyntaxKind.InterfaceDeclaration && - n.parent.kind !== ts.SyntaxKind.ClassDeclaration && - n.parent.kind !== ts.SyntaxKind.ClassExpression && - n.flags & ts.NodeFlags.Ambient) { - if (!(flags & ts.ModifierFlags.Ambient) && !(ts.isModuleBlock(n.parent) && ts.isModuleDeclaration(n.parent.parent) && ts.isGlobalScopeAugmentation(n.parent.parent))) { - // It is nested in an ambient context, which means it is automatically exported - flags |= ts.ModifierFlags.Export; - } - flags |= ts.ModifierFlags.Ambient; + function checkFunctionOrConstructorSymbolWorker(symbol: ts.Symbol): void { + function getCanonicalOverload(overloads: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined): ts.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: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined, flagsToCheck: ts.ModifierFlags, someOverloadFlags: ts.ModifierFlags, allOverloadFlags: ts.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); + ts.forEach(overloads, o => { + const deviation = getEffectiveDeclarationFlags(o, flagsToCheck) ^ canonicalFlags; + if (deviation & ts.ModifierFlags.Export) { + error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_exported_or_non_exported); + } + else if (deviation & ts.ModifierFlags.Ambient) { + error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_ambient_or_non_ambient); + } + else if (deviation & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { + error(ts.getNameOfDeclaration(o) || o, ts.Diagnostics.Overload_signatures_must_all_be_public_private_or_protected); + } + else if (deviation & ts.ModifierFlags.Abstract) { + error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_abstract_or_non_abstract); + } + }); } - - return flags & flagsToCheck; } - function checkFunctionOrConstructorSymbol(symbol: ts.Symbol): void { - addLazyDiagnostic(() => checkFunctionOrConstructorSymbolWorker(symbol)); + function checkQuestionTokenAgreementBetweenOverloads(overloads: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined, someHaveQuestionToken: boolean, allHaveQuestionToken: boolean): void { + if (someHaveQuestionToken !== allHaveQuestionToken) { + const canonicalHasQuestionToken = ts.hasQuestionToken(getCanonicalOverload(overloads, implementation)); + ts.forEach(overloads, o => { + const deviation = ts.hasQuestionToken(o) !== canonicalHasQuestionToken; + if (deviation) { + error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_optional_or_required); + } + }); + } } - function checkFunctionOrConstructorSymbolWorker(symbol: ts.Symbol): void { - function getCanonicalOverload(overloads: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined): ts.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]; - } + const flagsToCheck: ts.ModifierFlags = ts.ModifierFlags.Export | ts.ModifierFlags.Ambient | ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Abstract; + let someNodeFlags: ts.ModifierFlags = ts.ModifierFlags.None; + let allNodeFlags = flagsToCheck; + let someHaveQuestionToken = false; + let allHaveQuestionToken = true; + let hasOverloads = false; + let bodyDeclaration: ts.FunctionLikeDeclaration | undefined; + let lastSeenNonAmbientDeclaration: ts.FunctionLikeDeclaration | undefined; + let previousDeclaration: ts.SignatureDeclaration | undefined; - function checkFlagAgreementBetweenOverloads(overloads: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined, flagsToCheck: ts.ModifierFlags, someOverloadFlags: ts.ModifierFlags, allOverloadFlags: ts.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); - ts.forEach(overloads, o => { - const deviation = getEffectiveDeclarationFlags(o, flagsToCheck) ^ canonicalFlags; - if (deviation & ts.ModifierFlags.Export) { - error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_exported_or_non_exported); - } - else if (deviation & ts.ModifierFlags.Ambient) { - error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_ambient_or_non_ambient); - } - else if (deviation & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) { - error(ts.getNameOfDeclaration(o) || o, ts.Diagnostics.Overload_signatures_must_all_be_public_private_or_protected); - } - else if (deviation & ts.ModifierFlags.Abstract) { - error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_abstract_or_non_abstract); - } - }); - } + const declarations = symbol.declarations; + const isConstructor = (symbol.flags & ts.SymbolFlags.Constructor) !== 0; + function reportImplementationExpectedError(node: ts.SignatureDeclaration): void { + if (node.name && ts.nodeIsMissing(node.name)) { + return; } - function checkQuestionTokenAgreementBetweenOverloads(overloads: ts.Declaration[], implementation: ts.FunctionLikeDeclaration | undefined, someHaveQuestionToken: boolean, allHaveQuestionToken: boolean): void { - if (someHaveQuestionToken !== allHaveQuestionToken) { - const canonicalHasQuestionToken = ts.hasQuestionToken(getCanonicalOverload(overloads, implementation)); - ts.forEach(overloads, o => { - const deviation = ts.hasQuestionToken(o) !== canonicalHasQuestionToken; - if (deviation) { - error(ts.getNameOfDeclaration(o), ts.Diagnostics.Overload_signatures_must_all_be_optional_or_required); - } - }); + let seen = false; + const subsequentNode = ts.forEachChild(node.parent, c => { + if (seen) { + return c; } - } - - const flagsToCheck: ts.ModifierFlags = ts.ModifierFlags.Export | ts.ModifierFlags.Ambient | ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Abstract; - let someNodeFlags: ts.ModifierFlags = ts.ModifierFlags.None; - let allNodeFlags = flagsToCheck; - let someHaveQuestionToken = false; - let allHaveQuestionToken = true; - let hasOverloads = false; - let bodyDeclaration: ts.FunctionLikeDeclaration | undefined; - let lastSeenNonAmbientDeclaration: ts.FunctionLikeDeclaration | undefined; - let previousDeclaration: ts.SignatureDeclaration | undefined; - - const declarations = symbol.declarations; - const isConstructor = (symbol.flags & ts.SymbolFlags.Constructor) !== 0; - function reportImplementationExpectedError(node: ts.SignatureDeclaration): void { - if (node.name && ts.nodeIsMissing(node.name)) { - return; + else { + seen = c === node; } - - let seen = false; - const subsequentNode = ts.forEachChild(node.parent, c => { - if (seen) { - return c; - } - else { - 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: ts.Node = (subsequentNode as ts.FunctionLikeDeclaration).name || subsequentNode; - const subsequentName = (subsequentNode as ts.FunctionLikeDeclaration).name; - if (node.name && subsequentName && ( - // both are private identifiers - ts.isPrivateIdentifier(node.name) && ts.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!) - ts.isComputedPropertyName(node.name) && ts.isComputedPropertyName(subsequentName) || - // Both are literal property names that are the same. - ts.isPropertyNameLiteral(node.name) && ts.isPropertyNameLiteral(subsequentName) && - ts.getEscapedTextOfIdentifierOrLiteral(node.name) === ts.getEscapedTextOfIdentifierOrLiteral(subsequentName))) { - const reportError = (node.kind === ts.SyntaxKind.MethodDeclaration || node.kind === ts.SyntaxKind.MethodSignature) && - ts.isStatic(node) !== ts.isStatic(subsequentNode); - // 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 = ts.isStatic(node) ? ts.Diagnostics.Function_overload_must_be_static : ts.Diagnostics.Function_overload_must_not_be_static; - error(errorNode, diagnostic); - } - return; - } - if (ts.nodeIsPresent((subsequentNode as ts.FunctionLikeDeclaration).body)) { - error(errorNode, ts.Diagnostics.Function_implementation_name_must_be_0, ts.declarationNameToString(node.name)); - return; + }); + // 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: ts.Node = (subsequentNode as ts.FunctionLikeDeclaration).name || subsequentNode; + const subsequentName = (subsequentNode as ts.FunctionLikeDeclaration).name; + if (node.name && subsequentName && ( + // both are private identifiers + ts.isPrivateIdentifier(node.name) && ts.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!) + ts.isComputedPropertyName(node.name) && ts.isComputedPropertyName(subsequentName) || + // Both are literal property names that are the same. + ts.isPropertyNameLiteral(node.name) && ts.isPropertyNameLiteral(subsequentName) && + ts.getEscapedTextOfIdentifierOrLiteral(node.name) === ts.getEscapedTextOfIdentifierOrLiteral(subsequentName))) { + const reportError = (node.kind === ts.SyntaxKind.MethodDeclaration || node.kind === ts.SyntaxKind.MethodSignature) && + ts.isStatic(node) !== ts.isStatic(subsequentNode); + // 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 = ts.isStatic(node) ? ts.Diagnostics.Function_overload_must_be_static : ts.Diagnostics.Function_overload_must_not_be_static; + error(errorNode, diagnostic); } + return; + } + if (ts.nodeIsPresent((subsequentNode as ts.FunctionLikeDeclaration).body)) { + error(errorNode, ts.Diagnostics.Function_implementation_name_must_be_0, ts.declarationNameToString(node.name)); + return; } } - const errorNode: ts.Node = node.name || node; - if (isConstructor) { - error(errorNode, ts.Diagnostics.Constructor_implementation_is_missing); + } + const errorNode: ts.Node = node.name || node; + if (isConstructor) { + error(errorNode, ts.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 (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract)) { + error(errorNode, ts.Diagnostics.All_declarations_of_an_abstract_method_must_be_consecutive); } else { - // Report different errors regarding non-consecutive blocks of declarations depending on whether - // the node in question is abstract. - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract)) { - error(errorNode, ts.Diagnostics.All_declarations_of_an_abstract_method_must_be_consecutive); - } - else { - error(errorNode, ts.Diagnostics.Function_implementation_is_missing_or_not_immediately_following_the_declaration); - } + error(errorNode, ts.Diagnostics.Function_implementation_is_missing_or_not_immediately_following_the_declaration); } } + } - let duplicateFunctionDeclaration = false; - let multipleConstructorImplementation = false; - let hasNonAmbientClass = false; - const functionDeclarations = [] as ts.Declaration[]; - if (declarations) { - for (const current of declarations) { - const node = current as ts.SignatureDeclaration | ts.ClassDeclaration | ts.ClassExpression; - const inAmbientContext = node.flags & ts.NodeFlags.Ambient; - const inAmbientContextOrInterface = node.parent && (node.parent.kind === ts.SyntaxKind.InterfaceDeclaration || node.parent.kind === ts.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 === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression) && !inAmbientContext) { - hasNonAmbientClass = true; - } - - if (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.MethodDeclaration || node.kind === ts.SyntaxKind.MethodSignature || node.kind === ts.SyntaxKind.Constructor) { - functionDeclarations.push(node); - const currentNodeFlags = getEffectiveDeclarationFlags(node, flagsToCheck); - someNodeFlags |= currentNodeFlags; - allNodeFlags &= currentNodeFlags; - someHaveQuestionToken = someHaveQuestionToken || ts.hasQuestionToken(node); - allHaveQuestionToken = allHaveQuestionToken && ts.hasQuestionToken(node); - const bodyIsPresent = ts.nodeIsPresent((node as ts.FunctionLikeDeclaration).body); - - if (bodyIsPresent && bodyDeclaration) { - if (isConstructor) { - multipleConstructorImplementation = true; - } - else { - duplicateFunctionDeclaration = true; - } - } - else if (previousDeclaration?.parent === node.parent && previousDeclaration.end !== node.pos) { - reportImplementationExpectedError(previousDeclaration); - } - - if (bodyIsPresent) { - if (!bodyDeclaration) { - bodyDeclaration = node as ts.FunctionLikeDeclaration; - } + let duplicateFunctionDeclaration = false; + let multipleConstructorImplementation = false; + let hasNonAmbientClass = false; + const functionDeclarations = [] as ts.Declaration[]; + if (declarations) { + for (const current of declarations) { + const node = current as ts.SignatureDeclaration | ts.ClassDeclaration | ts.ClassExpression; + const inAmbientContext = node.flags & ts.NodeFlags.Ambient; + const inAmbientContextOrInterface = node.parent && (node.parent.kind === ts.SyntaxKind.InterfaceDeclaration || node.parent.kind === ts.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 === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression) && !inAmbientContext) { + hasNonAmbientClass = true; + } + + if (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.MethodDeclaration || node.kind === ts.SyntaxKind.MethodSignature || node.kind === ts.SyntaxKind.Constructor) { + functionDeclarations.push(node); + const currentNodeFlags = getEffectiveDeclarationFlags(node, flagsToCheck); + someNodeFlags |= currentNodeFlags; + allNodeFlags &= currentNodeFlags; + someHaveQuestionToken = someHaveQuestionToken || ts.hasQuestionToken(node); + allHaveQuestionToken = allHaveQuestionToken && ts.hasQuestionToken(node); + const bodyIsPresent = ts.nodeIsPresent((node as ts.FunctionLikeDeclaration).body); + + if (bodyIsPresent && bodyDeclaration) { + if (isConstructor) { + multipleConstructorImplementation = true; } else { - hasOverloads = true; + duplicateFunctionDeclaration = true; } + } + else if (previousDeclaration?.parent === node.parent && previousDeclaration.end !== node.pos) { + reportImplementationExpectedError(previousDeclaration); + } - previousDeclaration = node; - - if (!inAmbientContextOrInterface) { - lastSeenNonAmbientDeclaration = node as ts.FunctionLikeDeclaration; + if (bodyIsPresent) { + if (!bodyDeclaration) { + bodyDeclaration = node as ts.FunctionLikeDeclaration; } } - } - } - - if (multipleConstructorImplementation) { - ts.forEach(functionDeclarations, declaration => { - error(declaration, ts.Diagnostics.Multiple_constructor_implementations_are_not_allowed); - }); - } + else { + hasOverloads = true; + } - if (duplicateFunctionDeclaration) { - ts.forEach(functionDeclarations, declaration => { - error(ts.getNameOfDeclaration(declaration) || declaration, ts.Diagnostics.Duplicate_function_implementation); - }); - } + previousDeclaration = node; - if (hasNonAmbientClass && !isConstructor && symbol.flags & ts.SymbolFlags.Function && declarations) { - const relatedDiagnostics = ts.filter(declarations, d => d.kind === ts.SyntaxKind.ClassDeclaration) - .map(d => ts.createDiagnosticForNode(d, ts.Diagnostics.Consider_adding_a_declare_modifier_to_this_class)); - ts.forEach(declarations, declaration => { - const diagnostic = declaration.kind === ts.SyntaxKind.ClassDeclaration - ? ts.Diagnostics.Class_declaration_cannot_implement_overload_list_for_0 - : declaration.kind === ts.SyntaxKind.FunctionDeclaration - ? ts.Diagnostics.Function_with_bodies_can_only_merge_with_classes_that_are_ambient - : undefined; - if (diagnostic) { - ts.addRelatedInfo(error(ts.getNameOfDeclaration(declaration) || declaration, diagnostic, ts.symbolName(symbol)), ...relatedDiagnostics); + if (!inAmbientContextOrInterface) { + lastSeenNonAmbientDeclaration = node as ts.FunctionLikeDeclaration; } - }); + } } + } - // Abstract methods can't have an implementation -- in particular, they don't need one. - if (lastSeenNonAmbientDeclaration && !lastSeenNonAmbientDeclaration.body && - !ts.hasSyntacticModifier(lastSeenNonAmbientDeclaration, ts.ModifierFlags.Abstract) && !lastSeenNonAmbientDeclaration.questionToken) { - reportImplementationExpectedError(lastSeenNonAmbientDeclaration); - } + if (multipleConstructorImplementation) { + ts.forEach(functionDeclarations, declaration => { + error(declaration, ts.Diagnostics.Multiple_constructor_implementations_are_not_allowed); + }); + } - if (hasOverloads) { - if (declarations) { - checkFlagAgreementBetweenOverloads(declarations, bodyDeclaration, flagsToCheck, someNodeFlags, allNodeFlags); - checkQuestionTokenAgreementBetweenOverloads(declarations, bodyDeclaration, someHaveQuestionToken, allHaveQuestionToken); - } + if (duplicateFunctionDeclaration) { + ts.forEach(functionDeclarations, declaration => { + error(ts.getNameOfDeclaration(declaration) || declaration, ts.Diagnostics.Duplicate_function_implementation); + }); + } - if (bodyDeclaration) { - const signatures = getSignaturesOfSymbol(symbol); - const bodySignature = getSignatureFromDeclaration(bodyDeclaration); - for (const signature of signatures) { - if (!isImplementationCompatibleWithOverload(bodySignature, signature)) { - ts.addRelatedInfo(error(signature.declaration, ts.Diagnostics.This_overload_signature_is_not_compatible_with_its_implementation_signature), ts.createDiagnosticForNode(bodyDeclaration, ts.Diagnostics.The_implementation_signature_is_declared_here)); - break; - } - } + if (hasNonAmbientClass && !isConstructor && symbol.flags & ts.SymbolFlags.Function && declarations) { + const relatedDiagnostics = ts.filter(declarations, d => d.kind === ts.SyntaxKind.ClassDeclaration) + .map(d => ts.createDiagnosticForNode(d, ts.Diagnostics.Consider_adding_a_declare_modifier_to_this_class)); + ts.forEach(declarations, declaration => { + const diagnostic = declaration.kind === ts.SyntaxKind.ClassDeclaration + ? ts.Diagnostics.Class_declaration_cannot_implement_overload_list_for_0 + : declaration.kind === ts.SyntaxKind.FunctionDeclaration + ? ts.Diagnostics.Function_with_bodies_can_only_merge_with_classes_that_are_ambient + : undefined; + if (diagnostic) { + ts.addRelatedInfo(error(ts.getNameOfDeclaration(declaration) || declaration, diagnostic, ts.symbolName(symbol)), ...relatedDiagnostics); } - } + }); } - function checkExportsOnMergedDeclarations(node: ts.Declaration): void { - addLazyDiagnostic(() => checkExportsOnMergedDeclarationsWorker(node)); + // Abstract methods can't have an implementation -- in particular, they don't need one. + if (lastSeenNonAmbientDeclaration && !lastSeenNonAmbientDeclaration.body && + !ts.hasSyntacticModifier(lastSeenNonAmbientDeclaration, ts.ModifierFlags.Abstract) && !lastSeenNonAmbientDeclaration.questionToken) { + reportImplementationExpectedError(lastSeenNonAmbientDeclaration); } - function checkExportsOnMergedDeclarationsWorker(node: ts.Declaration): void { - // 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; + if (hasOverloads) { + if (declarations) { + 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)) { + ts.addRelatedInfo(error(signature.declaration, ts.Diagnostics.This_overload_signature_is_not_compatible_with_its_implementation_signature), ts.createDiagnosticForNode(bodyDeclaration, ts.Diagnostics.The_implementation_signature_is_declared_here)); + break; + } } } + } + } + + function checkExportsOnMergedDeclarations(node: ts.Declaration): void { + addLazyDiagnostic(() => checkExportsOnMergedDeclarationsWorker(node)); + } - // run the check only for the first declaration in the list - if (ts.getDeclarationOfKind(symbol, node.kind) !== node) { + function checkExportsOnMergedDeclarationsWorker(node: ts.Declaration): void { + // 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; } + } - 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, ts.ModifierFlags.Export | ts.ModifierFlags.Default); - if (effectiveDeclarationFlags & ts.ModifierFlags.Export) { - if (effectiveDeclarationFlags & ts.ModifierFlags.Default) { - defaultExportedDeclarationSpaces |= declarationSpaces; - } - else { - exportedDeclarationSpaces |= declarationSpaces; - } + // run the check only for the first declaration in the list + if (ts.getDeclarationOfKind(symbol, node.kind) !== node) { + return; + } + + 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, ts.ModifierFlags.Export | ts.ModifierFlags.Default); + if (effectiveDeclarationFlags & ts.ModifierFlags.Export) { + if (effectiveDeclarationFlags & ts.ModifierFlags.Default) { + defaultExportedDeclarationSpaces |= declarationSpaces; } else { - nonExportedDeclarationSpaces |= declarationSpaces; + exportedDeclarationSpaces |= declarationSpaces; } } + else { + nonExportedDeclarationSpaces |= declarationSpaces; + } + } - // Spaces for anything not declared a 'default export'. - const nonDefaultExportedDeclarationSpaces = exportedDeclarationSpaces | nonExportedDeclarationSpaces; + // Spaces for anything not declared a 'default export'. + const nonDefaultExportedDeclarationSpaces = exportedDeclarationSpaces | nonExportedDeclarationSpaces; - const commonDeclarationSpacesForExportsAndLocals = exportedDeclarationSpaces & nonExportedDeclarationSpaces; - const commonDeclarationSpacesForDefaultAndNonDefault = defaultExportedDeclarationSpaces & nonDefaultExportedDeclarationSpaces; + 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); + if (commonDeclarationSpacesForExportsAndLocals || commonDeclarationSpacesForDefaultAndNonDefault) { + // declaration spaces for exported and non-exported declarations intersect + for (const d of symbol.declarations!) { + const declarationSpaces = getDeclarationSpaces(d); - const name = ts.getNameOfDeclaration(d); - // Only error on the declarations that contributed to the intersecting spaces. - if (declarationSpaces & commonDeclarationSpacesForDefaultAndNonDefault) { - error(name, ts.Diagnostics.Merged_declaration_0_cannot_include_a_default_export_declaration_Consider_adding_a_separate_export_default_0_declaration_instead, ts.declarationNameToString(name)); - } - else if (declarationSpaces & commonDeclarationSpacesForExportsAndLocals) { - error(name, ts.Diagnostics.Individual_declarations_in_merged_declaration_0_must_be_all_exported_or_all_local, ts.declarationNameToString(name)); - } + const name = ts.getNameOfDeclaration(d); + // Only error on the declarations that contributed to the intersecting spaces. + if (declarationSpaces & commonDeclarationSpacesForDefaultAndNonDefault) { + error(name, ts.Diagnostics.Merged_declaration_0_cannot_include_a_default_export_declaration_Consider_adding_a_separate_export_default_0_declaration_instead, ts.declarationNameToString(name)); + } + else if (declarationSpaces & commonDeclarationSpacesForExportsAndLocals) { + error(name, ts.Diagnostics.Individual_declarations_in_merged_declaration_0_must_be_all_exported_or_all_local, ts.declarationNameToString(name)); } } + } - function getDeclarationSpaces(decl: ts.Declaration): DeclarationSpaces { - let d = decl as ts.Node; - switch (d.kind) { - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - - // A jsdoc typedef and callback are, by definition, type aliases. - // falls through - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - return DeclarationSpaces.ExportType; - case ts.SyntaxKind.ModuleDeclaration: - return ts.isAmbientModule(d as ts.ModuleDeclaration) || ts.getModuleInstanceState(d as ts.ModuleDeclaration) !== ts.ModuleInstanceState.NonInstantiated - ? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue - : DeclarationSpaces.ExportNamespace; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.EnumMember: - return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue; - case ts.SyntaxKind.SourceFile: - return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue | DeclarationSpaces.ExportNamespace; - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.BinaryExpression: - const node = d as ts.ExportAssignment | ts.BinaryExpression; - const expression = ts.isExportAssignment(node) ? node.expression : node.right; - // Export assigned entity name expressions act as aliases and should fall through, otherwise they export values - if (!ts.isEntityNameExpression(expression)) { - return DeclarationSpaces.ExportValue; - } - d = expression; + function getDeclarationSpaces(decl: ts.Declaration): DeclarationSpaces { + let d = decl as ts.Node; + switch (d.kind) { + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: - // The below options all declare an Alias, which is allowed to merge with other values within the importing module. - // falls through - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportClause: - let result = DeclarationSpaces.None; - const target = resolveAlias(getSymbolOfNode(d)!); - ts.forEach(target.declarations, d => { - result |= getDeclarationSpaces(d); - }); - return result; - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ImportSpecifier: // https://github.com/Microsoft/TypeScript/pull/7591 - case ts.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. + // A jsdoc typedef and callback are, by definition, type aliases. + // falls through + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + return DeclarationSpaces.ExportType; + case ts.SyntaxKind.ModuleDeclaration: + return ts.isAmbientModule(d as ts.ModuleDeclaration) || ts.getModuleInstanceState(d as ts.ModuleDeclaration) !== ts.ModuleInstanceState.NonInstantiated + ? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue + : DeclarationSpaces.ExportNamespace; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.EnumMember: + return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue; + case ts.SyntaxKind.SourceFile: + return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue | DeclarationSpaces.ExportNamespace; + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.BinaryExpression: + const node = d as ts.ExportAssignment | ts.BinaryExpression; + const expression = ts.isExportAssignment(node) ? node.expression : node.right; + // Export assigned entity name expressions act as aliases and should fall through, otherwise they export values + if (!ts.isEntityNameExpression(expression)) { return DeclarationSpaces.ExportValue; - default: - return ts.Debug.failBadSyntaxKind(d); - } + } + d = expression; + + // The below options all declare an Alias, which is allowed to merge with other values within the importing module. + // falls through + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportClause: + let result = DeclarationSpaces.None; + const target = resolveAlias(getSymbolOfNode(d)!); + ts.forEach(target.declarations, d => { + result |= getDeclarationSpaces(d); + }); + return result; + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ImportSpecifier: // https://github.com/Microsoft/TypeScript/pull/7591 + case ts.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 ts.Debug.failBadSyntaxKind(d); } } + } - function getAwaitedTypeOfPromise(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.DiagnosticMessage, arg0?: string | number): ts.Type | undefined { - const promisedType = getPromisedTypeOfPromise(type, errorNode); - return promisedType && getAwaitedType(promisedType, errorNode, diagnosticMessage, arg0); - } + function getAwaitedTypeOfPromise(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.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(type: ts.Type, errorNode?: ts.Node, thisTypeForErrorOut?: { - value?: ts.Type; - }): ts.Type | undefined { - // - // { // type - // then( // thenFunction - // onfulfilled: ( // onfulfilledParameterType - // value: T // valueParameterType - // ) => any - // ): any; - // } - // + /** + * 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(type: ts.Type, errorNode?: ts.Node, thisTypeForErrorOut?: { + value?: ts.Type; + }): ts.Type | undefined { + // + // { // type + // then( // thenFunction + // onfulfilled: ( // onfulfilledParameterType + // value: T // valueParameterType + // ) => any + // ): any; + // } + // - if (isTypeAny(type)) { - return undefined; - } + if (isTypeAny(type)) { + return undefined; + } - const typeAsPromise = type as ts.PromiseOrAwaitableType; - if (typeAsPromise.promisedTypeOfPromise) { - return typeAsPromise.promisedTypeOfPromise; - } + const typeAsPromise = type as ts.PromiseOrAwaitableType; + if (typeAsPromise.promisedTypeOfPromise) { + return typeAsPromise.promisedTypeOfPromise; + } - if (isReferenceToType(type, getGlobalPromiseType(/*reportErrors*/ false))) { - return typeAsPromise.promisedTypeOfPromise = getTypeArguments(type as ts.GenericType)[0]; - } + if (isReferenceToType(type, getGlobalPromiseType(/*reportErrors*/ false))) { + return typeAsPromise.promisedTypeOfPromise = getTypeArguments(type as ts.GenericType)[0]; + } - // primitives with a `{ then() }` won't be unwrapped/adopted. - if (allTypesAssignableToKind(type, ts.TypeFlags.Primitive | ts.TypeFlags.Never)) { - return undefined; - } + // primitives with a `{ then() }` won't be unwrapped/adopted. + if (allTypesAssignableToKind(type, ts.TypeFlags.Primitive | ts.TypeFlags.Never)) { + return undefined; + } - const thenFunction = getTypeOfPropertyOfType(type, "then" as ts.__String)!; // TODO: GH#18217 - if (isTypeAny(thenFunction)) { - return undefined; - } + const thenFunction = getTypeOfPropertyOfType(type, "then" as ts.__String)!; // TODO: GH#18217 + if (isTypeAny(thenFunction)) { + return undefined; + } - const thenSignatures = thenFunction ? getSignaturesOfType(thenFunction, ts.SignatureKind.Call) : ts.emptyArray; - if (thenSignatures.length === 0) { - if (errorNode) { - error(errorNode, ts.Diagnostics.A_promise_must_have_a_then_method); - } - return undefined; + const thenSignatures = thenFunction ? getSignaturesOfType(thenFunction, ts.SignatureKind.Call) : ts.emptyArray; + if (thenSignatures.length === 0) { + if (errorNode) { + error(errorNode, ts.Diagnostics.A_promise_must_have_a_then_method); } + return undefined; + } - let thisTypeForError: ts.Type | undefined; - let candidates: ts.Signature[] | undefined; - for (const thenSignature of thenSignatures) { - const thisType = getThisTypeOfSignature(thenSignature); - if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) { - thisTypeForError = thisType; - } - else { - candidates = ts.append(candidates, thenSignature); - } + let thisTypeForError: ts.Type | undefined; + let candidates: ts.Signature[] | undefined; + for (const thenSignature of thenSignatures) { + const thisType = getThisTypeOfSignature(thenSignature); + if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) { + thisTypeForError = thisType; } - - if (!candidates) { - ts.Debug.assertIsDefined(thisTypeForError); - if (thisTypeForErrorOut) { - thisTypeForErrorOut.value = thisTypeForError; - } - if (errorNode) { - error(errorNode, ts.Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError)); - } - return undefined; + else { + candidates = ts.append(candidates, thenSignature); } + } - const onfulfilledParameterType = getTypeWithFacts(getUnionType(ts.map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); - if (isTypeAny(onfulfilledParameterType)) { - return undefined; + if (!candidates) { + ts.Debug.assertIsDefined(thisTypeForError); + if (thisTypeForErrorOut) { + thisTypeForErrorOut.value = thisTypeForError; } - - const onfulfilledParameterSignatures = getSignaturesOfType(onfulfilledParameterType, ts.SignatureKind.Call); - if (onfulfilledParameterSignatures.length === 0) { - if (errorNode) { - error(errorNode, ts.Diagnostics.The_first_parameter_of_the_then_method_of_a_promise_must_be_a_callback); - } - return undefined; + if (errorNode) { + error(errorNode, ts.Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError)); } - - return typeAsPromise.promisedTypeOfPromise = getUnionType(ts.map(onfulfilledParameterSignatures, getTypeOfFirstParameterOfSignature), ts.UnionReduction.Subtype); + return undefined; } - /** - * Gets the "awaited type" of a type. - * @param type The type to await. - * @param withAlias When `true`, wraps the "awaited type" in `Awaited` if needed. - * @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, withAlias: boolean, errorNode: ts.Node, diagnosticMessage: ts.DiagnosticMessage, arg0?: string | number): ts.Type { - const awaitedType = withAlias ? - getAwaitedType(type, errorNode, diagnosticMessage, arg0) : - getAwaitedTypeNoAlias(type, errorNode, diagnosticMessage, arg0); - return awaitedType || errorType; + const onfulfilledParameterType = getTypeWithFacts(getUnionType(ts.map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); + if (isTypeAny(onfulfilledParameterType)) { + return undefined; } - /** - * Determines whether a type is an object with a callable `then` member. - */ - function isThenableType(type: ts.Type): boolean { - if (allTypesAssignableToKind(type, ts.TypeFlags.Primitive | ts.TypeFlags.Never)) { - // primitive types cannot be considered "thenable" since they are not objects. - return false; + const onfulfilledParameterSignatures = getSignaturesOfType(onfulfilledParameterType, ts.SignatureKind.Call); + if (onfulfilledParameterSignatures.length === 0) { + if (errorNode) { + error(errorNode, ts.Diagnostics.The_first_parameter_of_the_then_method_of_a_promise_must_be_a_callback); } - - const thenFunction = getTypeOfPropertyOfType(type, "then" as ts.__String); - return !!thenFunction && getSignaturesOfType(getTypeWithFacts(thenFunction, TypeFacts.NEUndefinedOrNull), ts.SignatureKind.Call).length > 0; + return undefined; } - interface AwaitedTypeInstantiation extends ts.Type { - _awaitedTypeBrand: never; - aliasSymbol: ts.Symbol; - aliasTypeArguments: readonly ts.Type[]; - } + return typeAsPromise.promisedTypeOfPromise = getUnionType(ts.map(onfulfilledParameterSignatures, getTypeOfFirstParameterOfSignature), ts.UnionReduction.Subtype); + } - function isAwaitedTypeInstantiation(type: ts.Type): type is AwaitedTypeInstantiation { - if (type.flags & ts.TypeFlags.Conditional) { - const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ false); - return !!awaitedSymbol && type.aliasSymbol === awaitedSymbol && type.aliasTypeArguments?.length === 1; - } + /** + * Gets the "awaited type" of a type. + * @param type The type to await. + * @param withAlias When `true`, wraps the "awaited type" in `Awaited` if needed. + * @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, withAlias: boolean, errorNode: ts.Node, diagnosticMessage: ts.DiagnosticMessage, arg0?: string | number): ts.Type { + const awaitedType = withAlias ? + getAwaitedType(type, errorNode, diagnosticMessage, arg0) : + getAwaitedTypeNoAlias(type, errorNode, diagnosticMessage, arg0); + return awaitedType || errorType; + } + + /** + * Determines whether a type is an object with a callable `then` member. + */ + function isThenableType(type: ts.Type): boolean { + if (allTypesAssignableToKind(type, ts.TypeFlags.Primitive | ts.TypeFlags.Never)) { + // primitive types cannot be considered "thenable" since they are not objects. return false; } - /** - * For a generic `Awaited`, gets `T`. - */ - function unwrapAwaitedType(type: ts.Type) { - return type.flags & ts.TypeFlags.Union ? mapType(type, unwrapAwaitedType) : - isAwaitedTypeInstantiation(type) ? type.aliasTypeArguments[0] : - type; - } + const thenFunction = getTypeOfPropertyOfType(type, "then" as ts.__String); + return !!thenFunction && getSignaturesOfType(getTypeWithFacts(thenFunction, TypeFacts.NEUndefinedOrNull), ts.SignatureKind.Call).length > 0; + } - function createAwaitedTypeIfNeeded(type: ts.Type): ts.Type { - // We wrap type `T` in `Awaited` based on the following conditions: - // - `T` is not already an `Awaited`, and - // - `T` is generic, and - // - One of the following applies: - // - `T` has no base constraint, or - // - The base constraint of `T` is `any`, `unknown`, `object`, or `{}`, or - // - The base constraint of `T` is an object type with a callable `then` method. + interface AwaitedTypeInstantiation extends ts.Type { + _awaitedTypeBrand: never; + aliasSymbol: ts.Symbol; + aliasTypeArguments: readonly ts.Type[]; + } - if (isTypeAny(type)) { - return type; - } + function isAwaitedTypeInstantiation(type: ts.Type): type is AwaitedTypeInstantiation { + if (type.flags & ts.TypeFlags.Conditional) { + const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ false); + return !!awaitedSymbol && type.aliasSymbol === awaitedSymbol && type.aliasTypeArguments?.length === 1; + } + return false; + } - // If this is already an `Awaited`, just return it. This helps to avoid `Awaited>` in higher-order. - if (isAwaitedTypeInstantiation(type)) { - return type; - } + /** + * For a generic `Awaited`, gets `T`. + */ + function unwrapAwaitedType(type: ts.Type) { + return type.flags & ts.TypeFlags.Union ? mapType(type, unwrapAwaitedType) : + isAwaitedTypeInstantiation(type) ? type.aliasTypeArguments[0] : + type; + } - // Only instantiate `Awaited` if `T` contains possibly non-primitive types. - if (isGenericObjectType(type)) { - const baseConstraint = getBaseConstraintOfType(type); - // Only instantiate `Awaited` if `T` has no base constraint, or the base constraint of `T` is `any`, `unknown`, `{}`, `object`, - // or is promise-like. - if (!baseConstraint || (baseConstraint.flags & ts.TypeFlags.AnyOrUnknown) || isEmptyObjectType(baseConstraint) || isThenableType(baseConstraint)) { - // Nothing to do if `Awaited` doesn't exist - const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ true); - if (awaitedSymbol) { - // Unwrap unions that may contain `Awaited`, otherwise its possible to manufacture an `Awaited | U>` where - // an `Awaited` would suffice. - return getTypeAliasInstantiation(awaitedSymbol, [unwrapAwaitedType(type)]); - } - } - } + function createAwaitedTypeIfNeeded(type: ts.Type): ts.Type { + // We wrap type `T` in `Awaited` based on the following conditions: + // - `T` is not already an `Awaited`, and + // - `T` is generic, and + // - One of the following applies: + // - `T` has no base constraint, or + // - The base constraint of `T` is `any`, `unknown`, `object`, or `{}`, or + // - The base constraint of `T` is an object type with a callable `then` method. - ts.Debug.assert(getPromisedTypeOfPromise(type) === undefined, "type provided should not be a non-generic 'promise'-like."); + if (isTypeAny(type)) { return type; } - /** - * Gets the "awaited type" of a type. - * - * 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. If the "promised - * type" is itself a Promise-like, the "promised type" is recursively unwrapped until a - * non-promise type is found. - * - * This is used to reflect the runtime behavior of the `await` keyword. - */ - function getAwaitedType(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.DiagnosticMessage, arg0?: string | number): ts.Type | undefined { - const awaitedType = getAwaitedTypeNoAlias(type, errorNode, diagnosticMessage, arg0); - return awaitedType && createAwaitedTypeIfNeeded(awaitedType); + // If this is already an `Awaited`, just return it. This helps to avoid `Awaited>` in higher-order. + if (isAwaitedTypeInstantiation(type)) { + return type; } - /** - * Gets the "awaited type" of a type without introducing an `Awaited` wrapper. - * - * @see {@link getAwaitedType} - */ - function getAwaitedTypeNoAlias(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.DiagnosticMessage, arg0?: string | number): ts.Type | undefined { - if (isTypeAny(type)) { - return type; + // Only instantiate `Awaited` if `T` contains possibly non-primitive types. + if (isGenericObjectType(type)) { + const baseConstraint = getBaseConstraintOfType(type); + // Only instantiate `Awaited` if `T` has no base constraint, or the base constraint of `T` is `any`, `unknown`, `{}`, `object`, + // or is promise-like. + if (!baseConstraint || (baseConstraint.flags & ts.TypeFlags.AnyOrUnknown) || isEmptyObjectType(baseConstraint) || isThenableType(baseConstraint)) { + // Nothing to do if `Awaited` doesn't exist + const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ true); + if (awaitedSymbol) { + // Unwrap unions that may contain `Awaited`, otherwise its possible to manufacture an `Awaited | U>` where + // an `Awaited` would suffice. + return getTypeAliasInstantiation(awaitedSymbol, [unwrapAwaitedType(type)]); + } } + } - // If this is already an `Awaited`, just return it. This avoids `Awaited>` in higher-order - if (isAwaitedTypeInstantiation(type)) { - return type; - } + ts.Debug.assert(getPromisedTypeOfPromise(type) === undefined, "type provided should not be a non-generic 'promise'-like."); + return type; + } - // If we've already cached an awaited type, return a possible `Awaited` for it. - const typeAsAwaitable = type as ts.PromiseOrAwaitableType; - if (typeAsAwaitable.awaitedTypeOfType) { - return typeAsAwaitable.awaitedTypeOfType; - } + /** + * Gets the "awaited type" of a type. + * + * 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. If the "promised + * type" is itself a Promise-like, the "promised type" is recursively unwrapped until a + * non-promise type is found. + * + * This is used to reflect the runtime behavior of the `await` keyword. + */ + function getAwaitedType(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.DiagnosticMessage, arg0?: string | number): ts.Type | undefined { + const awaitedType = getAwaitedTypeNoAlias(type, errorNode, diagnosticMessage, arg0); + return awaitedType && createAwaitedTypeIfNeeded(awaitedType); + } - // For a union, get a union of the awaited types of each constituent. - if (type.flags & ts.TypeFlags.Union) { - const mapper = errorNode ? (constituentType: ts.Type) => getAwaitedTypeNoAlias(constituentType, errorNode, diagnosticMessage, arg0) : getAwaitedTypeNoAlias; - return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper); - } - - const thisTypeForErrorOut: { - value: ts.Type | undefined; - } = { value: undefined }; - const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut); - if (promisedType) { - if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(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, ts.Diagnostics.Type_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method); - } - return undefined; - } + /** + * Gets the "awaited type" of a type without introducing an `Awaited` wrapper. + * + * @see {@link getAwaitedType} + */ + function getAwaitedTypeNoAlias(type: ts.Type, errorNode?: ts.Node, diagnosticMessage?: ts.DiagnosticMessage, arg0?: string | number): ts.Type | undefined { + if (isTypeAny(type)) { + return type; + } - // 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 = getAwaitedTypeNoAlias(promisedType, errorNode, diagnosticMessage, arg0); - awaitedTypeStack.pop(); + // If this is already an `Awaited`, just return it. This avoids `Awaited>` in higher-order + if (isAwaitedTypeInstantiation(type)) { + return type; + } - if (!awaitedType) { - return undefined; - } + // If we've already cached an awaited type, return a possible `Awaited` for it. + const typeAsAwaitable = type as ts.PromiseOrAwaitableType; + if (typeAsAwaitable.awaitedTypeOfType) { + return typeAsAwaitable.awaitedTypeOfType; + } - return typeAsAwaitable.awaitedTypeOfType = awaitedType; - } + // For a union, get a union of the awaited types of each constituent. + if (type.flags & ts.TypeFlags.Union) { + const mapper = errorNode ? (constituentType: ts.Type) => getAwaitedTypeNoAlias(constituentType, errorNode, diagnosticMessage, arg0) : getAwaitedTypeNoAlias; + return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper); + } - // 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 is reported and we 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 . - if (isThenableType(type)) { + const thisTypeForErrorOut: { + value: ts.Type | undefined; + } = { value: undefined }; + const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut); + if (promisedType) { + if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(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) { - ts.Debug.assertIsDefined(diagnosticMessage); - let chain: ts.DiagnosticMessageChain | undefined; - if (thisTypeForErrorOut.value) { - chain = ts.chainDiagnosticMessages(chain, ts.Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForErrorOut.value)); - } - chain = ts.chainDiagnosticMessages(chain, diagnosticMessage, arg0); - diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(errorNode, chain)); + error(errorNode, ts.Diagnostics.Type_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method); } 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: ts.FunctionLikeDeclaration | ts.MethodSignature, returnTypeNode: ts.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); + // 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 = getAwaitedTypeNoAlias(promisedType, errorNode, diagnosticMessage, arg0); + awaitedTypeStack.pop(); - if (languageVersion >= ts.ScriptTarget.ES2015) { - if (isErrorType(returnType)) { - 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, ts.Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type_Did_you_mean_to_write_Promise_0, typeToString(getAwaitedTypeNoAlias(returnType) || voidType)); - return; - } + if (!awaitedType) { + return undefined; } - else { - // Always mark the type node as referenced if it points to a value - markTypeNodeAsReferenced(returnTypeNode); - - if (isErrorType(returnType)) { - return; - } - const promiseConstructorName = ts.getEntityNameFromTypeNode(returnTypeNode); - if (promiseConstructorName === undefined) { - error(returnTypeNode, ts.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; - } + return typeAsAwaitable.awaitedTypeOfType = awaitedType; + } - const promiseConstructorSymbol = resolveEntityName(promiseConstructorName, ts.SymbolFlags.Value, /*ignoreErrors*/ true); - const promiseConstructorType = promiseConstructorSymbol ? getTypeOfSymbol(promiseConstructorSymbol) : errorType; - if (isErrorType(promiseConstructorType)) { - if (promiseConstructorName.kind === ts.SyntaxKind.Identifier && promiseConstructorName.escapedText === "Promise" && getTargetType(returnType) === getGlobalPromiseType(/*reportErrors*/ false)) { - error(returnTypeNode, ts.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, ts.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, ts.entityNameToString(promiseConstructorName)); - } - return; + // 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 is reported and we 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 . + if (isThenableType(type)) { + if (errorNode) { + ts.Debug.assertIsDefined(diagnosticMessage); + let chain: ts.DiagnosticMessageChain | undefined; + if (thisTypeForErrorOut.value) { + chain = ts.chainDiagnosticMessages(chain, ts.Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForErrorOut.value)); } + chain = ts.chainDiagnosticMessages(chain, diagnosticMessage, arg0); + diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(errorNode, chain)); + } + return undefined; + } - 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, ts.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, ts.entityNameToString(promiseConstructorName)); - return; - } + return typeAsAwaitable.awaitedTypeOfType = type; + } - if (!checkTypeAssignableTo(promiseConstructorType, globalPromiseConstructorLikeType, returnTypeNode, ts.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; - } + /** + * 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: ts.FunctionLikeDeclaration | ts.MethodSignature, returnTypeNode: ts.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); - // Verify there is no local declaration that could collide with the promise constructor. - const rootName = promiseConstructorName && ts.getFirstIdentifier(promiseConstructorName); - const collidingSymbol = getSymbol(node.locals!, rootName.escapedText, ts.SymbolFlags.Value); - if (collidingSymbol) { - error(collidingSymbol.valueDeclaration, ts.Diagnostics.Duplicate_identifier_0_Compiler_uses_declaration_1_to_support_async_functions, ts.idText(rootName), ts.entityNameToString(promiseConstructorName)); - return; - } + if (languageVersion >= ts.ScriptTarget.ES2015) { + if (isErrorType(returnType)) { + 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, ts.Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type_Did_you_mean_to_write_Promise_0, typeToString(getAwaitedTypeNoAlias(returnType) || voidType)); + return; } - checkAwaitedType(returnType, /*withAlias*/ false, node, ts.Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); } + else { + // Always mark the type node as referenced if it points to a value + markTypeNodeAsReferenced(returnTypeNode); - /** Check a decorator */ - function checkDecorator(node: ts.Decorator): void { - const signature = getResolvedSignature(node); - checkDeprecatedSignature(signature, node); - const returnType = getReturnTypeOfSignature(signature); - if (returnType.flags & ts.TypeFlags.Any) { + if (isErrorType(returnType)) { return; } - let headMessage: ts.DiagnosticMessage; - let expectedReturnType: ts.Type; - switch (node.parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - headMessage = ts.Diagnostics.Decorator_function_return_type_0_is_not_assignable_to_type_1; - const classSymbol = getSymbolOfNode(node.parent); - const classConstructorType = getTypeOfSymbol(classSymbol); - expectedReturnType = getUnionType([classConstructorType, voidType]); - break; - - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.Parameter: - headMessage = ts.Diagnostics.Decorator_function_return_type_is_0_but_is_expected_to_be_void_or_any; - expectedReturnType = voidType; - break; - - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - headMessage = ts.Diagnostics.Decorator_function_return_type_0_is_not_assignable_to_type_1; - const methodType = getTypeOfNode(node.parent); - const descriptorType = createTypedPropertyDescriptorType(methodType); - expectedReturnType = getUnionType([descriptorType, voidType]); - break; + const promiseConstructorName = ts.getEntityNameFromTypeNode(returnTypeNode); + if (promiseConstructorName === undefined) { + error(returnTypeNode, ts.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; + } - default: - return ts.Debug.fail(); + const promiseConstructorSymbol = resolveEntityName(promiseConstructorName, ts.SymbolFlags.Value, /*ignoreErrors*/ true); + const promiseConstructorType = promiseConstructorSymbol ? getTypeOfSymbol(promiseConstructorSymbol) : errorType; + if (isErrorType(promiseConstructorType)) { + if (promiseConstructorName.kind === ts.SyntaxKind.Identifier && promiseConstructorName.escapedText === "Promise" && getTargetType(returnType) === getGlobalPromiseType(/*reportErrors*/ false)) { + error(returnTypeNode, ts.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, ts.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, ts.entityNameToString(promiseConstructorName)); + } + return; } - checkTypeAssignableTo(returnType, expectedReturnType, node, headMessage); - } + 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, ts.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, ts.entityNameToString(promiseConstructorName)); + return; + } - /** - * 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: ts.TypeNode) { - markEntityNameOrEntityExpressionAsReference(node && ts.getEntityNameFromTypeNode(node), /*forDecoratorMetadata*/ false); - } + if (!checkTypeAssignableTo(promiseConstructorType, globalPromiseConstructorLikeType, returnTypeNode, ts.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; + } - function markEntityNameOrEntityExpressionAsReference(typeName: ts.EntityNameOrEntityNameExpression | undefined, forDecoratorMetadata: boolean) { - if (!typeName) + // Verify there is no local declaration that could collide with the promise constructor. + const rootName = promiseConstructorName && ts.getFirstIdentifier(promiseConstructorName); + const collidingSymbol = getSymbol(node.locals!, rootName.escapedText, ts.SymbolFlags.Value); + if (collidingSymbol) { + error(collidingSymbol.valueDeclaration, ts.Diagnostics.Duplicate_identifier_0_Compiler_uses_declaration_1_to_support_async_functions, ts.idText(rootName), ts.entityNameToString(promiseConstructorName)); return; - const rootName = ts.getFirstIdentifier(typeName); - const meaning = (typeName.kind === ts.SyntaxKind.Identifier ? ts.SymbolFlags.Type : ts.SymbolFlags.Namespace) | ts.SymbolFlags.Alias; - const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isReference*/ true); - if (rootSymbol && rootSymbol.flags & ts.SymbolFlags.Alias) { - if (symbolIsValue(rootSymbol) - && !isConstEnumOrConstEnumOnlyModule(resolveAlias(rootSymbol)) - && !getTypeOnlyAliasDeclaration(rootSymbol)) { - markAliasSymbolAsReferenced(rootSymbol); - } - else if (forDecoratorMetadata - && compilerOptions.isolatedModules - && ts.getEmitModuleKind(compilerOptions) >= ts.ModuleKind.ES2015 - && !symbolIsValue(rootSymbol) - && !ts.some(rootSymbol.declarations, ts.isTypeOnlyImportOrExportDeclaration)) { - const diag = error(typeName, ts.Diagnostics.A_type_referenced_in_a_decorated_signature_must_be_imported_with_import_type_or_a_namespace_import_when_isolatedModules_and_emitDecoratorMetadata_are_enabled); - const aliasDeclaration = ts.find(rootSymbol.declarations || ts.emptyArray, isAliasSymbolDeclaration); - if (aliasDeclaration) { - ts.addRelatedInfo(diag, ts.createDiagnosticForNode(aliasDeclaration, ts.Diagnostics._0_was_imported_here, ts.idText(rootName))); - } - } } } + checkAwaitedType(returnType, /*withAlias*/ false, node, ts.Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + } - /** - * 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: ts.TypeNode | undefined): void { - const entityName = getEntityNameForDecoratorMetadata(node); - if (entityName && ts.isEntityName(entityName)) { - markEntityNameOrEntityExpressionAsReference(entityName, /*forDecoratorMetadata*/ true); - } + /** Check a decorator */ + function checkDecorator(node: ts.Decorator): void { + const signature = getResolvedSignature(node); + checkDeprecatedSignature(signature, node); + const returnType = getReturnTypeOfSignature(signature); + if (returnType.flags & ts.TypeFlags.Any) { + return; } - function getEntityNameForDecoratorMetadata(node: ts.TypeNode | undefined): ts.EntityName | undefined { - if (node) { - switch (node.kind) { - case ts.SyntaxKind.IntersectionType: - case ts.SyntaxKind.UnionType: - return getEntityNameForDecoratorMetadataFromTypeList((node as ts.UnionOrIntersectionTypeNode).types); - case ts.SyntaxKind.ConditionalType: - return getEntityNameForDecoratorMetadataFromTypeList([(node as ts.ConditionalTypeNode).trueType, (node as ts.ConditionalTypeNode).falseType]); - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.NamedTupleMember: - return getEntityNameForDecoratorMetadata((node as ts.ParenthesizedTypeNode).type); - case ts.SyntaxKind.TypeReference: - return (node as ts.TypeReferenceNode).typeName; - } - } + let headMessage: ts.DiagnosticMessage; + let expectedReturnType: ts.Type; + switch (node.parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + headMessage = ts.Diagnostics.Decorator_function_return_type_0_is_not_assignable_to_type_1; + const classSymbol = getSymbolOfNode(node.parent); + const classConstructorType = getTypeOfSymbol(classSymbol); + expectedReturnType = getUnionType([classConstructorType, voidType]); + break; + + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.Parameter: + headMessage = ts.Diagnostics.Decorator_function_return_type_is_0_but_is_expected_to_be_void_or_any; + expectedReturnType = voidType; + break; + + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + headMessage = ts.Diagnostics.Decorator_function_return_type_0_is_not_assignable_to_type_1; + const methodType = getTypeOfNode(node.parent); + const descriptorType = createTypedPropertyDescriptorType(methodType); + expectedReturnType = getUnionType([descriptorType, voidType]); + break; + + default: + return ts.Debug.fail(); } - function getEntityNameForDecoratorMetadataFromTypeList(types: readonly ts.TypeNode[]): ts.EntityName | undefined { - let commonEntityName: ts.EntityName | undefined; - for (let typeNode of types) { - while (typeNode.kind === ts.SyntaxKind.ParenthesizedType || typeNode.kind === ts.SyntaxKind.NamedTupleMember) { - typeNode = (typeNode as ts.ParenthesizedTypeNode | ts.NamedTupleMember).type; // Skip parens if need be - } - if (typeNode.kind === ts.SyntaxKind.NeverKeyword) { - continue; // Always elide `never` from the union/intersection if possible - } - if (!strictNullChecks && (typeNode.kind === ts.SyntaxKind.LiteralType && (typeNode as ts.LiteralTypeNode).literal.kind === ts.SyntaxKind.NullKeyword || typeNode.kind === ts.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; - } + checkTypeAssignableTo(returnType, expectedReturnType, node, headMessage); + } - 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 (!ts.isIdentifier(commonEntityName) || - !ts.isIdentifier(individualEntityName) || - commonEntityName.escapedText !== individualEntityName.escapedText) { - return undefined; - } - } - else { - commonEntityName = individualEntityName; + /** + * 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: ts.TypeNode) { + markEntityNameOrEntityExpressionAsReference(node && ts.getEntityNameFromTypeNode(node), /*forDecoratorMetadata*/ false); + } + + function markEntityNameOrEntityExpressionAsReference(typeName: ts.EntityNameOrEntityNameExpression | undefined, forDecoratorMetadata: boolean) { + if (!typeName) + return; + const rootName = ts.getFirstIdentifier(typeName); + const meaning = (typeName.kind === ts.SyntaxKind.Identifier ? ts.SymbolFlags.Type : ts.SymbolFlags.Namespace) | ts.SymbolFlags.Alias; + const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isReference*/ true); + if (rootSymbol && rootSymbol.flags & ts.SymbolFlags.Alias) { + if (symbolIsValue(rootSymbol) + && !isConstEnumOrConstEnumOnlyModule(resolveAlias(rootSymbol)) + && !getTypeOnlyAliasDeclaration(rootSymbol)) { + markAliasSymbolAsReferenced(rootSymbol); + } + else if (forDecoratorMetadata + && compilerOptions.isolatedModules + && ts.getEmitModuleKind(compilerOptions) >= ts.ModuleKind.ES2015 + && !symbolIsValue(rootSymbol) + && !ts.some(rootSymbol.declarations, ts.isTypeOnlyImportOrExportDeclaration)) { + const diag = error(typeName, ts.Diagnostics.A_type_referenced_in_a_decorated_signature_must_be_imported_with_import_type_or_a_namespace_import_when_isolatedModules_and_emitDecoratorMetadata_are_enabled); + const aliasDeclaration = ts.find(rootSymbol.declarations || ts.emptyArray, isAliasSymbolDeclaration); + if (aliasDeclaration) { + ts.addRelatedInfo(diag, ts.createDiagnosticForNode(aliasDeclaration, ts.Diagnostics._0_was_imported_here, ts.idText(rootName))); } } - return commonEntityName; } + } - function getParameterTypeNodeForDecoratorCheck(node: ts.ParameterDeclaration): ts.TypeNode | undefined { - const typeNode = ts.getEffectiveTypeAnnotationNode(node); - return ts.isRestParameter(node) ? ts.getRestParameterElementType(typeNode) : typeNode; + /** + * 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: ts.TypeNode | undefined): void { + const entityName = getEntityNameForDecoratorMetadata(node); + if (entityName && ts.isEntityName(entityName)) { + markEntityNameOrEntityExpressionAsReference(entityName, /*forDecoratorMetadata*/ true); } + } - /** Check the decorators of a node */ - function checkDecorators(node: ts.Node): void { - if (!node.decorators) { - return; + function getEntityNameForDecoratorMetadata(node: ts.TypeNode | undefined): ts.EntityName | undefined { + if (node) { + switch (node.kind) { + case ts.SyntaxKind.IntersectionType: + case ts.SyntaxKind.UnionType: + return getEntityNameForDecoratorMetadataFromTypeList((node as ts.UnionOrIntersectionTypeNode).types); + case ts.SyntaxKind.ConditionalType: + return getEntityNameForDecoratorMetadataFromTypeList([(node as ts.ConditionalTypeNode).trueType, (node as ts.ConditionalTypeNode).falseType]); + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.NamedTupleMember: + return getEntityNameForDecoratorMetadata((node as ts.ParenthesizedTypeNode).type); + case ts.SyntaxKind.TypeReference: + return (node as ts.TypeReferenceNode).typeName; } + } + } - // skip this check for nodes that cannot have decorators. These should have already had an error reported by - // checkGrammarDecorators. - if (!ts.nodeCanBeDecorated(node, node.parent, node.parent.parent)) { - return; + function getEntityNameForDecoratorMetadataFromTypeList(types: readonly ts.TypeNode[]): ts.EntityName | undefined { + let commonEntityName: ts.EntityName | undefined; + for (let typeNode of types) { + while (typeNode.kind === ts.SyntaxKind.ParenthesizedType || typeNode.kind === ts.SyntaxKind.NamedTupleMember) { + typeNode = (typeNode as ts.ParenthesizedTypeNode | ts.NamedTupleMember).type; // Skip parens if need be } - - if (!compilerOptions.experimentalDecorators) { - error(node, ts.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); + if (typeNode.kind === ts.SyntaxKind.NeverKeyword) { + continue; // Always elide `never` from the union/intersection if possible + } + if (!strictNullChecks && (typeNode.kind === ts.SyntaxKind.LiteralType && (typeNode as ts.LiteralTypeNode).literal.kind === ts.SyntaxKind.NullKeyword || typeNode.kind === ts.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; } - const firstDecorator = node.decorators[0]; - checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Decorate); - if (node.kind === ts.SyntaxKind.Parameter) { - checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Param); + 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 (!ts.isIdentifier(commonEntityName) || + !ts.isIdentifier(individualEntityName) || + commonEntityName.escapedText !== individualEntityName.escapedText) { + return undefined; + } + } + else { + commonEntityName = individualEntityName; } + } + return commonEntityName; + } - if (compilerOptions.emitDecoratorMetadata) { - checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Metadata); + function getParameterTypeNodeForDecoratorCheck(node: ts.ParameterDeclaration): ts.TypeNode | undefined { + const typeNode = ts.getEffectiveTypeAnnotationNode(node); + return ts.isRestParameter(node) ? ts.getRestParameterElementType(typeNode) : typeNode; + } - // we only need to perform these checks if we are emitting serialized type metadata for the target of a decorator. - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - const constructor = ts.getFirstConstructorWithBody(node as ts.ClassDeclaration); - if (constructor) { - for (const parameter of constructor.parameters) { - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); - } - } - break; + /** Check the decorators of a node */ + function checkDecorators(node: ts.Node): void { + if (!node.decorators) { + return; + } - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - const otherKind = node.kind === ts.SyntaxKind.GetAccessor ? ts.SyntaxKind.SetAccessor : ts.SyntaxKind.GetAccessor; - const otherAccessor = ts.getDeclarationOfKind(getSymbolOfNode(node as ts.AccessorDeclaration), otherKind); - markDecoratorMedataDataTypeNodeAsReferenced(getAnnotatedAccessorTypeNode(node as ts.AccessorDeclaration) || otherAccessor && getAnnotatedAccessorTypeNode(otherAccessor)); - break; - case ts.SyntaxKind.MethodDeclaration: - for (const parameter of (node as ts.FunctionLikeDeclaration).parameters) { - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); - } + // skip this check for nodes that cannot have decorators. These should have already had an error reported by + // checkGrammarDecorators. + if (!ts.nodeCanBeDecorated(node, node.parent, node.parent.parent)) { + return; + } - markDecoratorMedataDataTypeNodeAsReferenced(ts.getEffectiveReturnTypeNode(node as ts.FunctionLikeDeclaration)); - break; + if (!compilerOptions.experimentalDecorators) { + error(node, ts.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); + } - case ts.SyntaxKind.PropertyDeclaration: - markDecoratorMedataDataTypeNodeAsReferenced(ts.getEffectiveTypeAnnotationNode(node as ts.ParameterDeclaration)); - break; + const firstDecorator = node.decorators[0]; + checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Decorate); + if (node.kind === ts.SyntaxKind.Parameter) { + checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Param); + } + + if (compilerOptions.emitDecoratorMetadata) { + checkExternalEmitHelpers(firstDecorator, ts.ExternalEmitHelpers.Metadata); - case ts.SyntaxKind.Parameter: - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(node as ts.ParameterDeclaration)); - const containingSignature = (node as ts.ParameterDeclaration).parent; - for (const parameter of containingSignature.parameters) { + // we only need to perform these checks if we are emitting serialized type metadata for the target of a decorator. + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + const constructor = ts.getFirstConstructorWithBody(node as ts.ClassDeclaration); + if (constructor) { + for (const parameter of constructor.parameters) { markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); } - break; - } - } + } + break; - ts.forEach(node.decorators, checkDecorator); - } + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + const otherKind = node.kind === ts.SyntaxKind.GetAccessor ? ts.SyntaxKind.SetAccessor : ts.SyntaxKind.GetAccessor; + const otherAccessor = ts.getDeclarationOfKind(getSymbolOfNode(node as ts.AccessorDeclaration), otherKind); + markDecoratorMedataDataTypeNodeAsReferenced(getAnnotatedAccessorTypeNode(node as ts.AccessorDeclaration) || otherAccessor && getAnnotatedAccessorTypeNode(otherAccessor)); + break; + case ts.SyntaxKind.MethodDeclaration: + for (const parameter of (node as ts.FunctionLikeDeclaration).parameters) { + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); + } - function checkFunctionDeclaration(node: ts.FunctionDeclaration): void { - addLazyDiagnostic(checkFunctionDeclarationDiagnostics); + markDecoratorMedataDataTypeNodeAsReferenced(ts.getEffectiveReturnTypeNode(node as ts.FunctionLikeDeclaration)); + break; - function checkFunctionDeclarationDiagnostics() { - checkFunctionOrMethodDeclaration(node); - checkGrammarForGenerator(node); - checkCollisionsForDeclarationName(node, node.name); + case ts.SyntaxKind.PropertyDeclaration: + markDecoratorMedataDataTypeNodeAsReferenced(ts.getEffectiveTypeAnnotationNode(node as ts.ParameterDeclaration)); + break; + + case ts.SyntaxKind.Parameter: + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(node as ts.ParameterDeclaration)); + const containingSignature = (node as ts.ParameterDeclaration).parent; + for (const parameter of containingSignature.parameters) { + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); + } + break; } } - function checkJSDocTypeAliasTag(node: ts.JSDocTypedefTag | ts.JSDocCallbackTag) { - if (!node.typeExpression) { - // If the node had `@property` tags, `typeExpression` would have been set to the first property tag. - error(node.name, ts.Diagnostics.JSDoc_typedef_tag_should_either_have_a_type_annotation_or_be_followed_by_property_or_member_tags); - } + ts.forEach(node.decorators, checkDecorator); + } - if (node.name) { - checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_alias_name_cannot_be_0); - } - checkSourceElement(node.typeExpression); - checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); - } + function checkFunctionDeclaration(node: ts.FunctionDeclaration): void { + addLazyDiagnostic(checkFunctionDeclarationDiagnostics); - function checkJSDocTemplateTag(node: ts.JSDocTemplateTag): void { - checkSourceElement(node.constraint); - for (const tp of node.typeParameters) { - checkSourceElement(tp); - } + function checkFunctionDeclarationDiagnostics() { + checkFunctionOrMethodDeclaration(node); + checkGrammarForGenerator(node); + checkCollisionsForDeclarationName(node, node.name); } + } - function checkJSDocTypeTag(node: ts.JSDocTypeTag) { - checkSourceElement(node.typeExpression); + function checkJSDocTypeAliasTag(node: ts.JSDocTypedefTag | ts.JSDocCallbackTag) { + if (!node.typeExpression) { + // If the node had `@property` tags, `typeExpression` would have been set to the first property tag. + error(node.name, ts.Diagnostics.JSDoc_typedef_tag_should_either_have_a_type_annotation_or_be_followed_by_property_or_member_tags); } - function checkJSDocParameterTag(node: ts.JSDocParameterTag) { - checkSourceElement(node.typeExpression); + if (node.name) { + checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_alias_name_cannot_be_0); } - function checkJSDocPropertyTag(node: ts.JSDocPropertyTag) { - checkSourceElement(node.typeExpression); + checkSourceElement(node.typeExpression); + checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); + } + + function checkJSDocTemplateTag(node: ts.JSDocTemplateTag): void { + checkSourceElement(node.constraint); + for (const tp of node.typeParameters) { + checkSourceElement(tp); } + } - function checkJSDocFunctionType(node: ts.JSDocFunctionType): void { - addLazyDiagnostic(checkJSDocFunctionTypeImplicitAny); - checkSignatureDeclaration(node); + function checkJSDocTypeTag(node: ts.JSDocTypeTag) { + checkSourceElement(node.typeExpression); + } - function checkJSDocFunctionTypeImplicitAny() { - if (!node.type && !ts.isJSDocConstructSignature(node)) { - reportImplicitAny(node, anyType); - } + function checkJSDocParameterTag(node: ts.JSDocParameterTag) { + checkSourceElement(node.typeExpression); + } + function checkJSDocPropertyTag(node: ts.JSDocPropertyTag) { + checkSourceElement(node.typeExpression); + } + + function checkJSDocFunctionType(node: ts.JSDocFunctionType): void { + addLazyDiagnostic(checkJSDocFunctionTypeImplicitAny); + checkSignatureDeclaration(node); + + function checkJSDocFunctionTypeImplicitAny() { + if (!node.type && !ts.isJSDocConstructSignature(node)) { + reportImplicitAny(node, anyType); } } + } - function checkJSDocImplementsTag(node: ts.JSDocImplementsTag): void { - const classLike = ts.getEffectiveJSDocHost(node); - if (!classLike || !ts.isClassDeclaration(classLike) && !ts.isClassExpression(classLike)) { - error(classLike, ts.Diagnostics.JSDoc_0_is_not_attached_to_a_class, ts.idText(node.tagName)); - } + function checkJSDocImplementsTag(node: ts.JSDocImplementsTag): void { + const classLike = ts.getEffectiveJSDocHost(node); + if (!classLike || !ts.isClassDeclaration(classLike) && !ts.isClassExpression(classLike)) { + error(classLike, ts.Diagnostics.JSDoc_0_is_not_attached_to_a_class, ts.idText(node.tagName)); } + } - function checkJSDocAugmentsTag(node: ts.JSDocAugmentsTag): void { - const classLike = ts.getEffectiveJSDocHost(node); - if (!classLike || !ts.isClassDeclaration(classLike) && !ts.isClassExpression(classLike)) { - error(classLike, ts.Diagnostics.JSDoc_0_is_not_attached_to_a_class, ts.idText(node.tagName)); - return; - } + function checkJSDocAugmentsTag(node: ts.JSDocAugmentsTag): void { + const classLike = ts.getEffectiveJSDocHost(node); + if (!classLike || !ts.isClassDeclaration(classLike) && !ts.isClassExpression(classLike)) { + error(classLike, ts.Diagnostics.JSDoc_0_is_not_attached_to_a_class, ts.idText(node.tagName)); + return; + } - const augmentsTags = ts.getJSDocTags(classLike).filter(ts.isJSDocAugmentsTag); - ts.Debug.assert(augmentsTags.length > 0); - if (augmentsTags.length > 1) { - error(augmentsTags[1], ts.Diagnostics.Class_declarations_cannot_have_more_than_one_augments_or_extends_tag); - } + const augmentsTags = ts.getJSDocTags(classLike).filter(ts.isJSDocAugmentsTag); + ts.Debug.assert(augmentsTags.length > 0); + if (augmentsTags.length > 1) { + error(augmentsTags[1], ts.Diagnostics.Class_declarations_cannot_have_more_than_one_augments_or_extends_tag); + } - const name = getIdentifierFromEntityNameExpression(node.class.expression); - const extend = ts.getClassExtendsHeritageElement(classLike); - if (extend) { - const className = getIdentifierFromEntityNameExpression(extend.expression); - if (className && name.escapedText !== className.escapedText) { - error(name, ts.Diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, ts.idText(node.tagName), ts.idText(name), ts.idText(className)); - } + const name = getIdentifierFromEntityNameExpression(node.class.expression); + const extend = ts.getClassExtendsHeritageElement(classLike); + if (extend) { + const className = getIdentifierFromEntityNameExpression(extend.expression); + if (className && name.escapedText !== className.escapedText) { + error(name, ts.Diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, ts.idText(node.tagName), ts.idText(name), ts.idText(className)); } } + } - function checkJSDocAccessibilityModifiers(node: ts.JSDocPublicTag | ts.JSDocProtectedTag | ts.JSDocPrivateTag): void { - const host = ts.getJSDocHost(node); - if (host && ts.isPrivateIdentifierClassElementDeclaration(host)) { - error(node, ts.Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); - } + function checkJSDocAccessibilityModifiers(node: ts.JSDocPublicTag | ts.JSDocProtectedTag | ts.JSDocPrivateTag): void { + const host = ts.getJSDocHost(node); + if (host && ts.isPrivateIdentifierClassElementDeclaration(host)) { + error(node, ts.Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); } + } - function getIdentifierFromEntityNameExpression(node: ts.Identifier | ts.PropertyAccessExpression): ts.Identifier | ts.PrivateIdentifier; - function getIdentifierFromEntityNameExpression(node: ts.Expression): ts.Identifier | ts.PrivateIdentifier | undefined; - function getIdentifierFromEntityNameExpression(node: ts.Expression): ts.Identifier | ts.PrivateIdentifier | undefined { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return node as ts.Identifier; - case ts.SyntaxKind.PropertyAccessExpression: - return (node as ts.PropertyAccessExpression).name; - default: - return undefined; - } + function getIdentifierFromEntityNameExpression(node: ts.Identifier | ts.PropertyAccessExpression): ts.Identifier | ts.PrivateIdentifier; + function getIdentifierFromEntityNameExpression(node: ts.Expression): ts.Identifier | ts.PrivateIdentifier | undefined; + function getIdentifierFromEntityNameExpression(node: ts.Expression): ts.Identifier | ts.PrivateIdentifier | undefined { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return node as ts.Identifier; + case ts.SyntaxKind.PropertyAccessExpression: + return (node as ts.PropertyAccessExpression).name; + default: + return undefined; } + } - function checkFunctionOrMethodDeclaration(node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.MethodSignature): void { - checkDecorators(node); - checkSignatureDeclaration(node); - const functionFlags = ts.getFunctionFlags(node); + function checkFunctionOrMethodDeclaration(node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.MethodSignature): void { + checkDecorators(node); + checkSignatureDeclaration(node); + const functionFlags = ts.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 === ts.SyntaxKind.ComputedPropertyName) { - // This check will account for methods in class/interface declarations, - // as well as accessors in classes/object literals - checkComputedPropertyName(node.name); - } + // 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 === ts.SyntaxKind.ComputedPropertyName) { + // This check will account for methods in class/interface declarations, + // as well as accessors in classes/object literals + checkComputedPropertyName(node.name); + } - if (hasBindableName(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; + if (hasBindableName(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 = localSymbol.declarations?.find( - // Get first non javascript function declaration - declaration => declaration.kind === node.kind && !(declaration.flags & ts.NodeFlags.JavaScriptFile)); + // 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 = localSymbol.declarations?.find( + // Get first non javascript function declaration + declaration => declaration.kind === node.kind && !(declaration.flags & ts.NodeFlags.JavaScriptFile)); - // Only type check the symbol once - if (node === firstDeclaration) { - checkFunctionOrConstructorSymbol(localSymbol); - } + // Only type check the symbol once + if (node === firstDeclaration) { + checkFunctionOrConstructorSymbol(localSymbol); + } - if (symbol.parent) { - // run check on export symbol to check that modifiers agree across all exported declarations - checkFunctionOrConstructorSymbol(symbol); - } + if (symbol.parent) { + // run check on export symbol to check that modifiers agree across all exported declarations + checkFunctionOrConstructorSymbol(symbol); } + } - const body = node.kind === ts.SyntaxKind.MethodSignature ? undefined : node.body; - checkSourceElement(body); - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, getReturnTypeFromAnnotation(node)); + const body = node.kind === ts.SyntaxKind.MethodSignature ? undefined : node.body; + checkSourceElement(body); + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, getReturnTypeFromAnnotation(node)); - addLazyDiagnostic(checkFunctionOrMethodDeclarationDiagnostics); + addLazyDiagnostic(checkFunctionOrMethodDeclarationDiagnostics); - // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature - if (ts.isInJSFile(node)) { - const typeTag = ts.getJSDocTypeTag(node); - if (typeTag && typeTag.typeExpression && !getContextualCallSignature(getTypeFromTypeNode(typeTag.typeExpression), node)) { - error(typeTag.typeExpression.type, ts.Diagnostics.The_type_of_a_function_declaration_must_match_the_function_s_signature); - } + // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature + if (ts.isInJSFile(node)) { + const typeTag = ts.getJSDocTypeTag(node); + if (typeTag && typeTag.typeExpression && !getContextualCallSignature(getTypeFromTypeNode(typeTag.typeExpression), node)) { + error(typeTag.typeExpression.type, ts.Diagnostics.The_type_of_a_function_declaration_must_match_the_function_s_signature); } + } - function checkFunctionOrMethodDeclarationDiagnostics() { - if (!ts.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 (ts.nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { - reportImplicitAny(node, anyType); - } + function checkFunctionOrMethodDeclarationDiagnostics() { + if (!ts.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 (ts.nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { + reportImplicitAny(node, anyType); + } - if (functionFlags & ts.FunctionFlags.Generator && ts.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)); - } + if (functionFlags & ts.FunctionFlags.Generator && ts.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)); } } } + } - function registerForUnusedIdentifiersCheck(node: PotentiallyUnusedIdentifier): void { - addLazyDiagnostic(registerForUnusedIdentifiersCheckDiagnostics); + function registerForUnusedIdentifiersCheck(node: PotentiallyUnusedIdentifier): void { + addLazyDiagnostic(registerForUnusedIdentifiersCheckDiagnostics); - function registerForUnusedIdentifiersCheckDiagnostics() { - // May be in a call such as getTypeOfNode that happened to call this. But potentiallyUnusedIdentifiers is only defined in the scope of `checkSourceFile`. - const sourceFile = ts.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); + function registerForUnusedIdentifiersCheckDiagnostics() { + // May be in a call such as getTypeOfNode that happened to call this. But potentiallyUnusedIdentifiers is only defined in the scope of `checkSourceFile`. + const sourceFile = ts.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 = ts.SourceFile | ts.ModuleDeclaration | ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.Block | ts.CaseBlock | ts.ForStatement | ts.ForInStatement | ts.ForOfStatement | Exclude | ts.TypeAliasDeclaration | ts.InferTypeNode; + type PotentiallyUnusedIdentifier = ts.SourceFile | ts.ModuleDeclaration | ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.Block | ts.CaseBlock | ts.ForStatement | ts.ForInStatement | ts.ForOfStatement | Exclude | ts.TypeAliasDeclaration | ts.InferTypeNode; - function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: readonly PotentiallyUnusedIdentifier[], addDiagnostic: AddUnusedDiagnostic) { - for (const node of potentiallyUnusedIdentifiers) { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - checkUnusedClassMembers(node, addDiagnostic); - checkUnusedTypeParameters(node, addDiagnostic); - break; - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.CaseBlock: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: + function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: readonly PotentiallyUnusedIdentifier[], addDiagnostic: AddUnusedDiagnostic) { + for (const node of potentiallyUnusedIdentifiers) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + checkUnusedClassMembers(node, addDiagnostic); + checkUnusedTypeParameters(node, addDiagnostic); + break; + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.Block: + case ts.SyntaxKind.CaseBlock: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + checkUnusedLocalsAndParameters(node, addDiagnostic); + break; + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + if (node.body) { // Don't report unused parameters in overloads checkUnusedLocalsAndParameters(node, addDiagnostic); - break; - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - if (node.body) { // Don't report unused parameters in overloads - checkUnusedLocalsAndParameters(node, addDiagnostic); - } - checkUnusedTypeParameters(node, addDiagnostic); - break; - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - checkUnusedTypeParameters(node, addDiagnostic); - break; - case ts.SyntaxKind.InferType: - checkUnusedInferTypeParameter(node, addDiagnostic); - break; - default: - ts.Debug.assertNever(node, "Node should not have been registered for unused identifiers check"); - } + } + checkUnusedTypeParameters(node, addDiagnostic); + break; + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + checkUnusedTypeParameters(node, addDiagnostic); + break; + case ts.SyntaxKind.InferType: + checkUnusedInferTypeParameter(node, addDiagnostic); + break; + default: + ts.Debug.assertNever(node, "Node should not have been registered for unused identifiers check"); } } + } - function errorUnusedLocal(declaration: ts.Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) { - const node = ts.getNameOfDeclaration(declaration) || declaration; - const message = isTypeDeclaration(declaration) ? ts.Diagnostics._0_is_declared_but_never_used : ts.Diagnostics._0_is_declared_but_its_value_is_never_read; - addDiagnostic(declaration, UnusedKind.Local, ts.createDiagnosticForNode(node, message, name)); - } + function errorUnusedLocal(declaration: ts.Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) { + const node = ts.getNameOfDeclaration(declaration) || declaration; + const message = isTypeDeclaration(declaration) ? ts.Diagnostics._0_is_declared_but_never_used : ts.Diagnostics._0_is_declared_but_its_value_is_never_read; + addDiagnostic(declaration, UnusedKind.Local, ts.createDiagnosticForNode(node, message, name)); + } - function isIdentifierThatStartsWithUnderscore(node: ts.Node) { - return ts.isIdentifier(node) && ts.idText(node).charCodeAt(0) === ts.CharacterCodes._; - } + function isIdentifierThatStartsWithUnderscore(node: ts.Node) { + return ts.isIdentifier(node) && ts.idText(node).charCodeAt(0) === ts.CharacterCodes._; + } - function checkUnusedClassMembers(node: ts.ClassDeclaration | ts.ClassExpression, addDiagnostic: AddUnusedDiagnostic): void { - for (const member of node.members) { - switch (member.kind) { - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - if (member.kind === ts.SyntaxKind.SetAccessor && member.symbol.flags & ts.SymbolFlags.GetAccessor) { - // Already would have reported an error on the getter. - break; - } - const symbol = getSymbolOfNode(member); - if (!symbol.isReferenced - && (ts.hasEffectiveModifier(member, ts.ModifierFlags.Private) || ts.isNamedDeclaration(member) && ts.isPrivateIdentifier(member.name)) - && !(member.flags & ts.NodeFlags.Ambient)) { - addDiagnostic(member, UnusedKind.Local, ts.createDiagnosticForNode(member.name!, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, symbolToString(symbol))); - } + function checkUnusedClassMembers(node: ts.ClassDeclaration | ts.ClassExpression, addDiagnostic: AddUnusedDiagnostic): void { + for (const member of node.members) { + switch (member.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + if (member.kind === ts.SyntaxKind.SetAccessor && member.symbol.flags & ts.SymbolFlags.GetAccessor) { + // Already would have reported an error on the getter. break; - case ts.SyntaxKind.Constructor: - for (const parameter of (member as ts.ConstructorDeclaration).parameters) { - if (!parameter.symbol.isReferenced && ts.hasSyntacticModifier(parameter, ts.ModifierFlags.Private)) { - addDiagnostic(parameter, UnusedKind.Local, ts.createDiagnosticForNode(parameter.name, ts.Diagnostics.Property_0_is_declared_but_its_value_is_never_read, ts.symbolName(parameter.symbol))); - } + } + const symbol = getSymbolOfNode(member); + if (!symbol.isReferenced + && (ts.hasEffectiveModifier(member, ts.ModifierFlags.Private) || ts.isNamedDeclaration(member) && ts.isPrivateIdentifier(member.name)) + && !(member.flags & ts.NodeFlags.Ambient)) { + addDiagnostic(member, UnusedKind.Local, ts.createDiagnosticForNode(member.name!, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, symbolToString(symbol))); + } + break; + case ts.SyntaxKind.Constructor: + for (const parameter of (member as ts.ConstructorDeclaration).parameters) { + if (!parameter.symbol.isReferenced && ts.hasSyntacticModifier(parameter, ts.ModifierFlags.Private)) { + addDiagnostic(parameter, UnusedKind.Local, ts.createDiagnosticForNode(parameter.name, ts.Diagnostics.Property_0_is_declared_but_its_value_is_never_read, ts.symbolName(parameter.symbol))); } - break; - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.SemicolonClassElement: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - // Can't be private - break; - default: - ts.Debug.fail("Unexpected class member"); - } + } + break; + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.SemicolonClassElement: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + // Can't be private + break; + default: + ts.Debug.fail("Unexpected class member"); } } + } - function checkUnusedInferTypeParameter(node: ts.InferTypeNode, addDiagnostic: AddUnusedDiagnostic): void { - const { typeParameter } = node; - if (isTypeParameterUnused(typeParameter)) { - addDiagnostic(node, UnusedKind.Parameter, ts.createDiagnosticForNode(node, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.idText(typeParameter.name))); - } + function checkUnusedInferTypeParameter(node: ts.InferTypeNode, addDiagnostic: AddUnusedDiagnostic): void { + const { typeParameter } = node; + if (isTypeParameterUnused(typeParameter)) { + addDiagnostic(node, UnusedKind.Parameter, ts.createDiagnosticForNode(node, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.idText(typeParameter.name))); } + } - function checkUnusedTypeParameters(node: ts.ClassLikeDeclaration | ts.SignatureDeclaration | ts.InterfaceDeclaration | ts.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. - const declarations = getSymbolOfNode(node).declarations; - if (!declarations || ts.last(declarations) !== node) - return; - const typeParameters = ts.getEffectiveTypeParameterDeclarations(node); - const seenParentsWithEveryUnused = new ts.Set(); - - for (const typeParameter of typeParameters) { - if (!isTypeParameterUnused(typeParameter)) - continue; - const name = ts.idText(typeParameter.name); - const { parent } = typeParameter; - if (parent.kind !== ts.SyntaxKind.InferType && parent.typeParameters!.every(isTypeParameterUnused)) { - if (ts.tryAddToSet(seenParentsWithEveryUnused, parent)) { - const sourceFile = ts.getSourceFileOfNode(parent); - const range = ts.isJSDocTemplateTag(parent) - // Whole @template tag - ? ts.rangeOfNode(parent) - // Include the `<>` in the error message - : ts.rangeOfTypeParameters(sourceFile, parent.typeParameters!); - const only = parent.typeParameters!.length === 1; - //TODO: following line is possible reason for bug #41974, unusedTypeParameters_TemplateTag - const message = only ? ts.Diagnostics._0_is_declared_but_its_value_is_never_read : ts.Diagnostics.All_type_parameters_are_unused; - const arg0 = only ? name : undefined; - addDiagnostic(typeParameter, UnusedKind.Parameter, ts.createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, message, arg0)); - } - } - else { + function checkUnusedTypeParameters(node: ts.ClassLikeDeclaration | ts.SignatureDeclaration | ts.InterfaceDeclaration | ts.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. + const declarations = getSymbolOfNode(node).declarations; + if (!declarations || ts.last(declarations) !== node) + return; + const typeParameters = ts.getEffectiveTypeParameterDeclarations(node); + const seenParentsWithEveryUnused = new ts.Set(); + + for (const typeParameter of typeParameters) { + if (!isTypeParameterUnused(typeParameter)) + continue; + const name = ts.idText(typeParameter.name); + const { parent } = typeParameter; + if (parent.kind !== ts.SyntaxKind.InferType && parent.typeParameters!.every(isTypeParameterUnused)) { + if (ts.tryAddToSet(seenParentsWithEveryUnused, parent)) { + const sourceFile = ts.getSourceFileOfNode(parent); + const range = ts.isJSDocTemplateTag(parent) + // Whole @template tag + ? ts.rangeOfNode(parent) + // Include the `<>` in the error message + : ts.rangeOfTypeParameters(sourceFile, parent.typeParameters!); + const only = parent.typeParameters!.length === 1; //TODO: following line is possible reason for bug #41974, unusedTypeParameters_TemplateTag - addDiagnostic(typeParameter, UnusedKind.Parameter, ts.createDiagnosticForNode(typeParameter, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, name)); + const message = only ? ts.Diagnostics._0_is_declared_but_its_value_is_never_read : ts.Diagnostics.All_type_parameters_are_unused; + const arg0 = only ? name : undefined; + addDiagnostic(typeParameter, UnusedKind.Parameter, ts.createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, message, arg0)); } } - } - function isTypeParameterUnused(typeParameter: ts.TypeParameterDeclaration): boolean { - return !(getMergedSymbol(typeParameter.symbol).isReferenced! & ts.SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name); - } - - function addToGroup(map: ts.ESMap, 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]]); + //TODO: following line is possible reason for bug #41974, unusedTypeParameters_TemplateTag + addDiagnostic(typeParameter, UnusedKind.Parameter, ts.createDiagnosticForNode(typeParameter, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, name)); } } + } + function isTypeParameterUnused(typeParameter: ts.TypeParameterDeclaration): boolean { + return !(getMergedSymbol(typeParameter.symbol).isReferenced! & ts.SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name); + } - function tryGetRootParameterDeclaration(node: ts.Node): ts.ParameterDeclaration | undefined { - return ts.tryCast(ts.getRootDeclaration(node), ts.isParameter); + function addToGroup(map: ts.ESMap, 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]]); + } + } - function isValidUnusedLocalDeclaration(declaration: ts.Declaration): boolean { - if (ts.isBindingElement(declaration)) { - if (ts.isObjectBindingPattern(declaration.parent)) { - /** - * ignore starts with underscore names _ - * const { a: _a } = { a: 1 } - */ - return !!(declaration.propertyName && isIdentifierThatStartsWithUnderscore(declaration.name)); - } - return isIdentifierThatStartsWithUnderscore(declaration.name); + function tryGetRootParameterDeclaration(node: ts.Node): ts.ParameterDeclaration | undefined { + return ts.tryCast(ts.getRootDeclaration(node), ts.isParameter); + } + + function isValidUnusedLocalDeclaration(declaration: ts.Declaration): boolean { + if (ts.isBindingElement(declaration)) { + if (ts.isObjectBindingPattern(declaration.parent)) { + /** + * ignore starts with underscore names _ + * const { a: _a } = { a: 1 } + */ + return !!(declaration.propertyName && isIdentifierThatStartsWithUnderscore(declaration.name)); } - return ts.isAmbientModule(declaration) || - (ts.isVariableDeclaration(declaration) && ts.isForInOrOfStatement(declaration.parent.parent) || isImportedDeclaration(declaration)) && isIdentifierThatStartsWithUnderscore(declaration.name!); + return isIdentifierThatStartsWithUnderscore(declaration.name); } + return ts.isAmbientModule(declaration) || + (ts.isVariableDeclaration(declaration) && ts.isForInOrOfStatement(declaration.parent.parent) || isImportedDeclaration(declaration)) && isIdentifierThatStartsWithUnderscore(declaration.name!); + } - function checkUnusedLocalsAndParameters(nodeWithLocals: ts.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 = new ts.Map(); - const unusedDestructures = new ts.Map(); - const unusedVariables = new ts.Map(); - 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 & ts.SymbolFlags.TypeParameter ? !(local.flags & ts.SymbolFlags.Variable && !(local.isReferenced! & ts.SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) { - return; - } + function checkUnusedLocalsAndParameters(nodeWithLocals: ts.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 = new ts.Map(); + const unusedDestructures = new ts.Map(); + const unusedVariables = new ts.Map(); + 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 & ts.SymbolFlags.TypeParameter ? !(local.flags & ts.SymbolFlags.Variable && !(local.isReferenced! & ts.SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) { + return; + } - if (local.declarations) { - for (const declaration of local.declarations) { - if (isValidUnusedLocalDeclaration(declaration)) { - continue; - } + if (local.declarations) { + for (const declaration of local.declarations) { + if (isValidUnusedLocalDeclaration(declaration)) { + continue; + } - if (isImportedDeclaration(declaration)) { - addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId); - } - else if (ts.isBindingElement(declaration) && ts.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 = ts.last(declaration.parent.elements); - if (declaration === lastElement || !ts.last(declaration.parent.elements).dotDotDotToken) { - addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); - } - } - else if (ts.isVariableDeclaration(declaration)) { - addToGroup(unusedVariables, declaration.parent, declaration, getNodeId); + if (isImportedDeclaration(declaration)) { + addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId); + } + else if (ts.isBindingElement(declaration) && ts.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 = ts.last(declaration.parent.elements); + if (declaration === lastElement || !ts.last(declaration.parent.elements).dotDotDotToken) { + addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); } - else { - const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration); - const name = local.valueDeclaration && ts.getNameOfDeclaration(local.valueDeclaration); - if (parameter && name) { - if (!ts.isParameterPropertyDeclaration(parameter, parameter.parent) && !ts.parameterIsThisKeyword(parameter) && !isIdentifierThatStartsWithUnderscore(name)) { - if (ts.isBindingElement(declaration) && ts.isArrayBindingPattern(declaration.parent)) { - addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); - } - else { - addDiagnostic(parameter, UnusedKind.Parameter, ts.createDiagnosticForNode(name, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.symbolName(local))); - } + } + else if (ts.isVariableDeclaration(declaration)) { + addToGroup(unusedVariables, declaration.parent, declaration, getNodeId); + } + else { + const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration); + const name = local.valueDeclaration && ts.getNameOfDeclaration(local.valueDeclaration); + if (parameter && name) { + if (!ts.isParameterPropertyDeclaration(parameter, parameter.parent) && !ts.parameterIsThisKeyword(parameter) && !isIdentifierThatStartsWithUnderscore(name)) { + if (ts.isBindingElement(declaration) && ts.isArrayBindingPattern(declaration.parent)) { + addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); + } + else { + addDiagnostic(parameter, UnusedKind.Parameter, ts.createDiagnosticForNode(name, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.symbolName(local))); } - } - else { - errorUnusedLocal(declaration, ts.symbolName(local), addDiagnostic); } } + else { + errorUnusedLocal(declaration, ts.symbolName(local), addDiagnostic); + } } } - }); - unusedImports.forEach(([importClause, unuseds]) => { - const importDecl = importClause.parent; - const nDeclarations = (importClause.name ? 1 : 0) + - (importClause.namedBindings ? - (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport ? 1 : importClause.namedBindings.elements.length) - : 0); - if (nDeclarations === unuseds.length) { - addDiagnostic(importDecl, UnusedKind.Local, unuseds.length === 1 - ? ts.createDiagnosticForNode(importDecl, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.idText(ts.first(unuseds).name!)) - : ts.createDiagnosticForNode(importDecl, ts.Diagnostics.All_imports_in_import_declaration_are_unused)); - } - else { - for (const unused of unuseds) - errorUnusedLocal(unused, ts.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 === ts.SyntaxKind.VariableDeclaration && bindingPattern.parent.parent.kind === ts.SyntaxKind.VariableDeclarationList) { - addToGroup(unusedVariables, bindingPattern.parent.parent, bindingPattern.parent, getNodeId); - } - else { - addDiagnostic(bindingPattern, kind, bindingElements.length === 1 - ? ts.createDiagnosticForNode(bindingPattern, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(ts.first(bindingElements).name)) - : ts.createDiagnosticForNode(bindingPattern, ts.Diagnostics.All_destructured_elements_are_unused)); - } + } + }); + unusedImports.forEach(([importClause, unuseds]) => { + const importDecl = importClause.parent; + const nDeclarations = (importClause.name ? 1 : 0) + + (importClause.namedBindings ? + (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport ? 1 : importClause.namedBindings.elements.length) + : 0); + if (nDeclarations === unuseds.length) { + addDiagnostic(importDecl, UnusedKind.Local, unuseds.length === 1 + ? ts.createDiagnosticForNode(importDecl, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, ts.idText(ts.first(unuseds).name!)) + : ts.createDiagnosticForNode(importDecl, ts.Diagnostics.All_imports_in_import_declaration_are_unused)); + } + else { + for (const unused of unuseds) + errorUnusedLocal(unused, ts.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 === ts.SyntaxKind.VariableDeclaration && bindingPattern.parent.parent.kind === ts.SyntaxKind.VariableDeclarationList) { + addToGroup(unusedVariables, bindingPattern.parent.parent, bindingPattern.parent, getNodeId); } else { - for (const e of bindingElements) { - addDiagnostic(e, kind, ts.createDiagnosticForNode(e, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(e.name))); - } + addDiagnostic(bindingPattern, kind, bindingElements.length === 1 + ? ts.createDiagnosticForNode(bindingPattern, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(ts.first(bindingElements).name)) + : ts.createDiagnosticForNode(bindingPattern, ts.Diagnostics.All_destructured_elements_are_unused)); } - }); - unusedVariables.forEach(([declarationList, declarations]) => { - if (declarationList.declarations.length === declarations.length) { - addDiagnostic(declarationList, UnusedKind.Local, declarations.length === 1 - ? ts.createDiagnosticForNode(ts.first(declarations).name, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(ts.first(declarations).name)) - : ts.createDiagnosticForNode(declarationList.parent.kind === ts.SyntaxKind.VariableStatement ? declarationList.parent : declarationList, ts.Diagnostics.All_variables_are_unused)); + } + else { + for (const e of bindingElements) { + addDiagnostic(e, kind, ts.createDiagnosticForNode(e, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(e.name))); } - else { - for (const decl of declarations) { - addDiagnostic(decl, UnusedKind.Local, ts.createDiagnosticForNode(decl, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(decl.name))); - } + } + }); + unusedVariables.forEach(([declarationList, declarations]) => { + if (declarationList.declarations.length === declarations.length) { + addDiagnostic(declarationList, UnusedKind.Local, declarations.length === 1 + ? ts.createDiagnosticForNode(ts.first(declarations).name, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(ts.first(declarations).name)) + : ts.createDiagnosticForNode(declarationList.parent.kind === ts.SyntaxKind.VariableStatement ? declarationList.parent : declarationList, ts.Diagnostics.All_variables_are_unused)); + } + else { + for (const decl of declarations) { + addDiagnostic(decl, UnusedKind.Local, ts.createDiagnosticForNode(decl, ts.Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(decl.name))); } - }); - } - - function bindingNameText(name: ts.BindingName): string { - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return ts.idText(name); - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ObjectBindingPattern: - return bindingNameText(ts.cast(ts.first(name.elements), ts.isBindingElement).name); - default: - return ts.Debug.assertNever(name); } + }); + } + + function bindingNameText(name: ts.BindingName): string { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return ts.idText(name); + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ObjectBindingPattern: + return bindingNameText(ts.cast(ts.first(name.elements), ts.isBindingElement).name); + default: + return ts.Debug.assertNever(name); } + } - type ImportedDeclaration = ts.ImportClause | ts.ImportSpecifier | ts.NamespaceImport; - function isImportedDeclaration(node: ts.Node): node is ImportedDeclaration { - return node.kind === ts.SyntaxKind.ImportClause || node.kind === ts.SyntaxKind.ImportSpecifier || node.kind === ts.SyntaxKind.NamespaceImport; + type ImportedDeclaration = ts.ImportClause | ts.ImportSpecifier | ts.NamespaceImport; + function isImportedDeclaration(node: ts.Node): node is ImportedDeclaration { + return node.kind === ts.SyntaxKind.ImportClause || node.kind === ts.SyntaxKind.ImportSpecifier || node.kind === ts.SyntaxKind.NamespaceImport; + } + function importClauseFromImported(decl: ImportedDeclaration): ts.ImportClause { + return decl.kind === ts.SyntaxKind.ImportClause ? decl : decl.kind === ts.SyntaxKind.NamespaceImport ? decl.parent : decl.parent.parent; + } + + function checkBlock(node: ts.Block) { + // Grammar checking for SyntaxKind.Block + if (node.kind === ts.SyntaxKind.Block) { + checkGrammarStatementInAmbientContext(node); } - function importClauseFromImported(decl: ImportedDeclaration): ts.ImportClause { - return decl.kind === ts.SyntaxKind.ImportClause ? decl : decl.kind === ts.SyntaxKind.NamespaceImport ? decl.parent : decl.parent.parent; + if (ts.isFunctionOrModuleBlock(node)) { + const saveFlowAnalysisDisabled = flowAnalysisDisabled; + ts.forEach(node.statements, checkSourceElement); + flowAnalysisDisabled = saveFlowAnalysisDisabled; } + else { + ts.forEach(node.statements, checkSourceElement); + } + if (node.locals) { + registerForUnusedIdentifiersCheck(node); + } + } - function checkBlock(node: ts.Block) { - // Grammar checking for SyntaxKind.Block - if (node.kind === ts.SyntaxKind.Block) { - checkGrammarStatementInAmbientContext(node); - } - if (ts.isFunctionOrModuleBlock(node)) { - const saveFlowAnalysisDisabled = flowAnalysisDisabled; - ts.forEach(node.statements, checkSourceElement); - flowAnalysisDisabled = saveFlowAnalysisDisabled; - } - else { - ts.forEach(node.statements, checkSourceElement); - } - if (node.locals) { - registerForUnusedIdentifiersCheck(node); - } + function checkCollisionWithArgumentsInGeneratedCode(node: ts.SignatureDeclaration) { + // no rest parameters \ declaration context \ overload - no codegen impact + if (languageVersion >= ts.ScriptTarget.ES2015 || !ts.hasRestParameter(node) || node.flags & ts.NodeFlags.Ambient || ts.nodeIsMissing((node as ts.FunctionLikeDeclaration).body)) { + return; } - function checkCollisionWithArgumentsInGeneratedCode(node: ts.SignatureDeclaration) { - // no rest parameters \ declaration context \ overload - no codegen impact - if (languageVersion >= ts.ScriptTarget.ES2015 || !ts.hasRestParameter(node) || node.flags & ts.NodeFlags.Ambient || ts.nodeIsMissing((node as ts.FunctionLikeDeclaration).body)) { - return; + ts.forEach(node.parameters, p => { + if (p.name && !ts.isBindingPattern(p.name) && p.name.escapedText === argumentsSymbol.escapedName) { + errorSkippedOn("noEmit", p, ts.Diagnostics.Duplicate_identifier_arguments_Compiler_uses_arguments_to_initialize_rest_parameters); } + }); + } - ts.forEach(node.parameters, p => { - if (p.name && !ts.isBindingPattern(p.name) && p.name.escapedText === argumentsSymbol.escapedName) { - errorSkippedOn("noEmit", p, ts.Diagnostics.Duplicate_identifier_arguments_Compiler_uses_arguments_to_initialize_rest_parameters); - } - }); + /** + * Checks whether an {@link Identifier}, in the context of another {@link Node}, would collide with a runtime value + * of {@link name} in an outer scope. This is used to check for collisions for downlevel transformations that + * require names like `Object`, `Promise`, `Reflect`, `require`, `exports`, etc. + */ + function needCollisionCheckForIdentifier(node: ts.Node, identifier: ts.Identifier | undefined, name: string): boolean { + if (identifier?.escapedText !== name) { + return false; } - /** - * Checks whether an {@link Identifier}, in the context of another {@link Node}, would collide with a runtime value - * of {@link name} in an outer scope. This is used to check for collisions for downlevel transformations that - * require names like `Object`, `Promise`, `Reflect`, `require`, `exports`, etc. - */ - function needCollisionCheckForIdentifier(node: ts.Node, identifier: ts.Identifier | undefined, name: string): boolean { - if (identifier?.escapedText !== name) { - return false; - } + if (node.kind === ts.SyntaxKind.PropertyDeclaration || + node.kind === ts.SyntaxKind.PropertySignature || + node.kind === ts.SyntaxKind.MethodDeclaration || + node.kind === ts.SyntaxKind.MethodSignature || + node.kind === ts.SyntaxKind.GetAccessor || + node.kind === ts.SyntaxKind.SetAccessor || + node.kind === ts.SyntaxKind.PropertyAssignment) { + // it is ok to have member named '_super', '_this', `Promise`, etc. - member access is always qualified + return false; + } - if (node.kind === ts.SyntaxKind.PropertyDeclaration || - node.kind === ts.SyntaxKind.PropertySignature || - node.kind === ts.SyntaxKind.MethodDeclaration || - node.kind === ts.SyntaxKind.MethodSignature || - node.kind === ts.SyntaxKind.GetAccessor || - node.kind === ts.SyntaxKind.SetAccessor || - node.kind === ts.SyntaxKind.PropertyAssignment) { - // it is ok to have member named '_super', '_this', `Promise`, etc. - member access is always qualified - return false; - } + if (node.flags & ts.NodeFlags.Ambient) { + // ambient context - no codegen impact + return false; + } - if (node.flags & ts.NodeFlags.Ambient) { - // ambient context - no codegen impact + if (ts.isImportClause(node) || ts.isImportEqualsDeclaration(node) || ts.isImportSpecifier(node)) { + // type-only imports do not require collision checks against runtime values. + if (ts.isTypeOnlyImportOrExportDeclaration(node)) { return false; } + } - if (ts.isImportClause(node) || ts.isImportEqualsDeclaration(node) || ts.isImportSpecifier(node)) { - // type-only imports do not require collision checks against runtime values. - if (ts.isTypeOnlyImportOrExportDeclaration(node)) { - return false; + const root = ts.getRootDeclaration(node); + if (ts.isParameter(root) && ts.nodeIsMissing((root.parent as ts.FunctionLikeDeclaration).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: ts.Node): void { + ts.findAncestor(node, current => { + if (getNodeCheckFlags(current) & ts.NodeCheckFlags.CaptureThis) { + const isDeclaration = node.kind !== ts.SyntaxKind.Identifier; + if (isDeclaration) { + error(ts.getNameOfDeclaration(node as ts.Declaration), ts.Diagnostics.Duplicate_identifier_this_Compiler_uses_variable_declaration_this_to_capture_this_reference); } + else { + error(node, ts.Diagnostics.Expression_resolves_to_variable_declaration_this_that_compiler_uses_to_capture_this_reference); + } + return true; } + return false; + }); + } - const root = ts.getRootDeclaration(node); - if (ts.isParameter(root) && ts.nodeIsMissing((root.parent as ts.FunctionLikeDeclaration).body)) { - // just an overload - no codegen impact - return false; + function checkIfNewTargetIsCapturedInEnclosingScope(node: ts.Node): void { + ts.findAncestor(node, current => { + if (getNodeCheckFlags(current) & ts.NodeCheckFlags.CaptureNewTarget) { + const isDeclaration = node.kind !== ts.SyntaxKind.Identifier; + if (isDeclaration) { + error(ts.getNameOfDeclaration(node as ts.Declaration), ts.Diagnostics.Duplicate_identifier_newTarget_Compiler_uses_variable_declaration_newTarget_to_capture_new_target_meta_property_reference); + } + else { + error(node, ts.Diagnostics.Expression_resolves_to_variable_declaration_newTarget_that_compiler_uses_to_capture_new_target_meta_property_reference); + } + return true; } + return false; + }); + } - return true; + function checkCollisionWithRequireExportsInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined) { + // No need to check for require or exports for ES6 modules and later + if (moduleKind >= ts.ModuleKind.ES2015 && !(moduleKind >= ts.ModuleKind.Node16 && ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { + return; } - // this function will run after checking the source file so 'CaptureThis' is correct for all nodes - function checkIfThisIsCapturedInEnclosingScope(node: ts.Node): void { - ts.findAncestor(node, current => { - if (getNodeCheckFlags(current) & ts.NodeCheckFlags.CaptureThis) { - const isDeclaration = node.kind !== ts.SyntaxKind.Identifier; - if (isDeclaration) { - error(ts.getNameOfDeclaration(node as ts.Declaration), ts.Diagnostics.Duplicate_identifier_this_Compiler_uses_variable_declaration_this_to_capture_this_reference); - } - else { - error(node, ts.Diagnostics.Expression_resolves_to_variable_declaration_this_that_compiler_uses_to_capture_this_reference); - } - return true; - } - return false; - }); + if (!name || !needCollisionCheckForIdentifier(node, name, "require") && !needCollisionCheckForIdentifier(node, name, "exports")) { + return; } - function checkIfNewTargetIsCapturedInEnclosingScope(node: ts.Node): void { - ts.findAncestor(node, current => { - if (getNodeCheckFlags(current) & ts.NodeCheckFlags.CaptureNewTarget) { - const isDeclaration = node.kind !== ts.SyntaxKind.Identifier; - if (isDeclaration) { - error(ts.getNameOfDeclaration(node as ts.Declaration), ts.Diagnostics.Duplicate_identifier_newTarget_Compiler_uses_variable_declaration_newTarget_to_capture_new_target_meta_property_reference); - } - else { - error(node, ts.Diagnostics.Expression_resolves_to_variable_declaration_newTarget_that_compiler_uses_to_capture_new_target_meta_property_reference); - } - return true; - } - return false; - }); + // Uninstantiated modules shouldnt do this check + if (ts.isModuleDeclaration(node) && ts.getModuleInstanceState(node) !== ts.ModuleInstanceState.Instantiated) { + return; } - function checkCollisionWithRequireExportsInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined) { - // No need to check for require or exports for ES6 modules and later - if (moduleKind >= ts.ModuleKind.ES2015 && !(moduleKind >= ts.ModuleKind.Node16 && ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { - return; - } - - if (!name || !needCollisionCheckForIdentifier(node, name, "require") && !needCollisionCheckForIdentifier(node, name, "exports")) { - return; - } - - // Uninstantiated modules shouldnt do this check - if (ts.isModuleDeclaration(node) && ts.getModuleInstanceState(node) !== ts.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 === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(parent as ts.SourceFile)) { - // If the declaration happens to be in external module, report error that require and exports are reserved keywords - errorSkippedOn("noEmit", name, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module, ts.declarationNameToString(name), ts.declarationNameToString(name)); - } + // 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 === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(parent as ts.SourceFile)) { + // If the declaration happens to be in external module, report error that require and exports are reserved keywords + errorSkippedOn("noEmit", name, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module, ts.declarationNameToString(name), ts.declarationNameToString(name)); } + } - function checkCollisionWithGlobalPromiseInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined): void { - if (!name || languageVersion >= ts.ScriptTarget.ES2017 || !needCollisionCheckForIdentifier(node, name, "Promise")) { - return; - } + function checkCollisionWithGlobalPromiseInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined): void { + if (!name || languageVersion >= ts.ScriptTarget.ES2017 || !needCollisionCheckForIdentifier(node, name, "Promise")) { + return; + } - // Uninstantiated modules shouldnt do this check - if (ts.isModuleDeclaration(node) && ts.getModuleInstanceState(node) !== ts.ModuleInstanceState.Instantiated) { - return; - } + // Uninstantiated modules shouldnt do this check + if (ts.isModuleDeclaration(node) && ts.getModuleInstanceState(node) !== ts.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 === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(parent as ts.SourceFile) && parent.flags & ts.NodeFlags.HasAsyncFunctions) { - // If the declaration happens to be in external module, report error that Promise is a reserved identifier. - errorSkippedOn("noEmit", name, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module_containing_async_functions, ts.declarationNameToString(name), ts.declarationNameToString(name)); - } + // 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 === ts.SyntaxKind.SourceFile && ts.isExternalOrCommonJsModule(parent as ts.SourceFile) && parent.flags & ts.NodeFlags.HasAsyncFunctions) { + // If the declaration happens to be in external module, report error that Promise is a reserved identifier. + errorSkippedOn("noEmit", name, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module_containing_async_functions, ts.declarationNameToString(name), ts.declarationNameToString(name)); } + } - function recordPotentialCollisionWithWeakMapSetInGeneratedCode(node: ts.Node, name: ts.Identifier): void { - if (languageVersion <= ts.ScriptTarget.ES2021 - && (needCollisionCheckForIdentifier(node, name, "WeakMap") || needCollisionCheckForIdentifier(node, name, "WeakSet"))) { - potentialWeakMapSetCollisions.push(node); - } + function recordPotentialCollisionWithWeakMapSetInGeneratedCode(node: ts.Node, name: ts.Identifier): void { + if (languageVersion <= ts.ScriptTarget.ES2021 + && (needCollisionCheckForIdentifier(node, name, "WeakMap") || needCollisionCheckForIdentifier(node, name, "WeakSet"))) { + potentialWeakMapSetCollisions.push(node); } + } - function checkWeakMapSetCollision(node: ts.Node) { - const enclosingBlockScope = ts.getEnclosingBlockScopeContainer(node); - if (getNodeCheckFlags(enclosingBlockScope) & ts.NodeCheckFlags.ContainsClassWithPrivateIdentifiers) { - ts.Debug.assert(ts.isNamedDeclaration(node) && ts.isIdentifier(node.name) && typeof node.name.escapedText === "string", "The target of a WeakMap/WeakSet collision check should be an identifier"); - errorSkippedOn("noEmit", node, ts.Diagnostics.Compiler_reserves_name_0_when_emitting_private_identifier_downlevel, node.name.escapedText); - } + function checkWeakMapSetCollision(node: ts.Node) { + const enclosingBlockScope = ts.getEnclosingBlockScopeContainer(node); + if (getNodeCheckFlags(enclosingBlockScope) & ts.NodeCheckFlags.ContainsClassWithPrivateIdentifiers) { + ts.Debug.assert(ts.isNamedDeclaration(node) && ts.isIdentifier(node.name) && typeof node.name.escapedText === "string", "The target of a WeakMap/WeakSet collision check should be an identifier"); + errorSkippedOn("noEmit", node, ts.Diagnostics.Compiler_reserves_name_0_when_emitting_private_identifier_downlevel, node.name.escapedText); } + } - function recordPotentialCollisionWithReflectInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined): void { - if (name && languageVersion >= ts.ScriptTarget.ES2015 && languageVersion <= ts.ScriptTarget.ES2021 - && needCollisionCheckForIdentifier(node, name, "Reflect")) { - potentialReflectCollisions.push(node); - } + function recordPotentialCollisionWithReflectInGeneratedCode(node: ts.Node, name: ts.Identifier | undefined): void { + if (name && languageVersion >= ts.ScriptTarget.ES2015 && languageVersion <= ts.ScriptTarget.ES2021 + && needCollisionCheckForIdentifier(node, name, "Reflect")) { + potentialReflectCollisions.push(node); } + } - function checkReflectCollision(node: ts.Node) { - let hasCollision = false; - if (ts.isClassExpression(node)) { - // ClassExpression names don't contribute to their containers, but do matter for any of their block-scoped members. - for (const member of node.members) { - if (getNodeCheckFlags(member) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { - hasCollision = true; - break; - } - } - } - else if (ts.isFunctionExpression(node)) { - // FunctionExpression names don't contribute to their containers, but do matter for their contents - if (getNodeCheckFlags(node) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { + function checkReflectCollision(node: ts.Node) { + let hasCollision = false; + if (ts.isClassExpression(node)) { + // ClassExpression names don't contribute to their containers, but do matter for any of their block-scoped members. + for (const member of node.members) { + if (getNodeCheckFlags(member) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { hasCollision = true; + break; } } - else { - const container = ts.getEnclosingBlockScopeContainer(node); - if (container && getNodeCheckFlags(container) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { - hasCollision = true; - } + } + else if (ts.isFunctionExpression(node)) { + // FunctionExpression names don't contribute to their containers, but do matter for their contents + if (getNodeCheckFlags(node) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { + hasCollision = true; } - if (hasCollision) { - ts.Debug.assert(ts.isNamedDeclaration(node) && ts.isIdentifier(node.name), "The target of a Reflect collision check should be an identifier"); - errorSkippedOn("noEmit", node, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_when_emitting_super_references_in_static_initializers, ts.declarationNameToString(node.name), "Reflect"); + } + else { + const container = ts.getEnclosingBlockScopeContainer(node); + if (container && getNodeCheckFlags(container) & ts.NodeCheckFlags.ContainsSuperPropertyInStaticInitializer) { + hasCollision = true; } } + if (hasCollision) { + ts.Debug.assert(ts.isNamedDeclaration(node) && ts.isIdentifier(node.name), "The target of a Reflect collision check should be an identifier"); + errorSkippedOn("noEmit", node, ts.Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_when_emitting_super_references_in_static_initializers, ts.declarationNameToString(node.name), "Reflect"); + } + } - function checkCollisionsForDeclarationName(node: ts.Node, name: ts.Identifier | undefined) { - if (!name) - return; - checkCollisionWithRequireExportsInGeneratedCode(node, name); - checkCollisionWithGlobalPromiseInGeneratedCode(node, name); - recordPotentialCollisionWithWeakMapSetInGeneratedCode(node, name); - recordPotentialCollisionWithReflectInGeneratedCode(node, name); - if (ts.isClassLike(node)) { - checkTypeNameIsReserved(name, ts.Diagnostics.Class_name_cannot_be_0); - if (!(node.flags & ts.NodeFlags.Ambient)) { - checkClassNameCollisionWithObject(name); - } - } - else if (ts.isEnumDeclaration(node)) { - checkTypeNameIsReserved(name, ts.Diagnostics.Enum_name_cannot_be_0); - } - } - - function checkVarDeclaredNamesNotShadowed(node: ts.VariableDeclaration | ts.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 ((ts.getCombinedNodeFlags(node) & ts.NodeFlags.BlockScoped) !== 0 || ts.isParameterDeclaration(node)) { - return; + function checkCollisionsForDeclarationName(node: ts.Node, name: ts.Identifier | undefined) { + if (!name) + return; + checkCollisionWithRequireExportsInGeneratedCode(node, name); + checkCollisionWithGlobalPromiseInGeneratedCode(node, name); + recordPotentialCollisionWithWeakMapSetInGeneratedCode(node, name); + recordPotentialCollisionWithReflectInGeneratedCode(node, name); + if (ts.isClassLike(node)) { + checkTypeNameIsReserved(name, ts.Diagnostics.Class_name_cannot_be_0); + if (!(node.flags & ts.NodeFlags.Ambient)) { + checkClassNameCollisionWithObject(name); } + } + else if (ts.isEnumDeclaration(node)) { + checkTypeNameIsReserved(name, ts.Diagnostics.Enum_name_cannot_be_0); + } + } - // 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 === ts.SyntaxKind.VariableDeclaration && !node.initializer) { - return; - } + function checkVarDeclaredNamesNotShadowed(node: ts.VariableDeclaration | ts.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 ((ts.getCombinedNodeFlags(node) & ts.NodeFlags.BlockScoped) !== 0 || ts.isParameterDeclaration(node)) { + return; + } - const symbol = getSymbolOfNode(node); - if (symbol.flags & ts.SymbolFlags.FunctionScopedVariable) { - if (!ts.isIdentifier(node.name)) - return ts.Debug.fail(); - const localDeclarationSymbol = resolveName(node, node.name.escapedText, ts.SymbolFlags.Variable, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); - if (localDeclarationSymbol && - localDeclarationSymbol !== symbol && - localDeclarationSymbol.flags & ts.SymbolFlags.BlockScopedVariable) { - if (getDeclarationNodeFlagsFromSymbol(localDeclarationSymbol) & ts.NodeFlags.BlockScoped) { - const varDeclList = ts.getAncestor(localDeclarationSymbol.valueDeclaration, ts.SyntaxKind.VariableDeclarationList)!; - const container = varDeclList.parent.kind === ts.SyntaxKind.VariableStatement && varDeclList.parent.parent - ? varDeclList.parent.parent - : undefined; + // 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 === ts.SyntaxKind.VariableDeclaration && !node.initializer) { + return; + } + + const symbol = getSymbolOfNode(node); + if (symbol.flags & ts.SymbolFlags.FunctionScopedVariable) { + if (!ts.isIdentifier(node.name)) + return ts.Debug.fail(); + const localDeclarationSymbol = resolveName(node, node.name.escapedText, ts.SymbolFlags.Variable, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + if (localDeclarationSymbol && + localDeclarationSymbol !== symbol && + localDeclarationSymbol.flags & ts.SymbolFlags.BlockScopedVariable) { + if (getDeclarationNodeFlagsFromSymbol(localDeclarationSymbol) & ts.NodeFlags.BlockScoped) { + const varDeclList = ts.getAncestor(localDeclarationSymbol.valueDeclaration, ts.SyntaxKind.VariableDeclarationList)!; + const container = varDeclList.parent.kind === ts.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 === ts.SyntaxKind.Block && ts.isFunctionLike(container.parent) || - container.kind === ts.SyntaxKind.ModuleBlock || - container.kind === ts.SyntaxKind.ModuleDeclaration || - container.kind === ts.SyntaxKind.SourceFile); + // 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 === ts.SyntaxKind.Block && ts.isFunctionLike(container.parent) || + container.kind === ts.SyntaxKind.ModuleBlock || + container.kind === ts.SyntaxKind.ModuleDeclaration || + container.kind === ts.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, ts.Diagnostics.Cannot_initialize_outer_scoped_variable_0_in_the_same_scope_as_block_scoped_declaration_1, name, name); - } + // 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, ts.Diagnostics.Cannot_initialize_outer_scoped_variable_0_in_the_same_scope_as_block_scoped_declaration_1, name, name); } } } } + } + + function convertAutoToAny(type: ts.Type) { + return type === autoType ? anyType : type === autoArrayType ? anyArrayType : type; + } - function convertAutoToAny(type: ts.Type) { - return type === autoType ? anyType : type === autoArrayType ? anyArrayType : type; + // Check variable, parameter, or property declaration + function checkVariableLikeDeclaration(node: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement) { + checkDecorators(node); + if (!ts.isBindingElement(node)) { + checkSourceElement(node.type); } - // Check variable, parameter, or property declaration - function checkVariableLikeDeclaration(node: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.VariableDeclaration | ts.BindingElement) { - checkDecorators(node); - if (!ts.isBindingElement(node)) { - checkSourceElement(node.type); - } + // JSDoc `function(string, string): string` syntax results in parameters with no name + if (!node.name) { + return; + } - // 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 === ts.SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); + if (node.initializer) { + checkExpressionCached(node.initializer); } + } - // 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 === ts.SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - if (node.initializer) { - checkExpressionCached(node.initializer); - } + if (ts.isBindingElement(node)) { + if (ts.isObjectBindingPattern(node.parent) && node.dotDotDotToken && languageVersion < ts.ScriptTarget.ES2018) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Rest); } - - if (ts.isBindingElement(node)) { - if (ts.isObjectBindingPattern(node.parent) && node.dotDotDotToken && languageVersion < ts.ScriptTarget.ES2018) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Rest); - } - // check computed properties inside property names of binding elements - if (node.propertyName && node.propertyName.kind === ts.SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.propertyName); - } - - // check private/protected variable access - const parent = node.parent.parent; - const parentCheckMode = node.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal; - const parentType = getTypeForBindingElementParent(parent, parentCheckMode); - const name = node.propertyName || node.name; - if (parentType && !ts.isBindingPattern(name)) { - const exprType = getLiteralTypeFromPropertyName(name); - if (isTypeUsableAsPropertyName(exprType)) { - const nameText = getPropertyNameFromType(exprType); - const property = getPropertyOfType(parentType, nameText); - if (property) { - markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined, /*isSelfTypeAccess*/ false); // A destructuring is never a write-only reference. - checkPropertyAccessibility(node, !!parent.initializer && parent.initializer.kind === ts.SyntaxKind.SuperKeyword, /*writing*/ false, parentType, property); - } - } - } + // check computed properties inside property names of binding elements + if (node.propertyName && node.propertyName.kind === ts.SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.propertyName); } - // For a binding pattern, check contained binding elements - if (ts.isBindingPattern(node.name)) { - if (node.name.kind === ts.SyntaxKind.ArrayBindingPattern && languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Read); - } - - ts.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 && ts.isParameterDeclaration(node) && ts.nodeIsMissing((ts.getContainingFunction(node) as ts.FunctionLikeDeclaration).body)) { - error(node, ts.Diagnostics.A_parameter_initializer_is_only_allowed_in_a_function_or_constructor_implementation); - return; - } - // For a binding pattern, validate the initializer and exit - if (ts.isBindingPattern(node.name)) { - const needCheckInitializer = node.initializer && node.parent.parent.kind !== ts.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 (ts.isArrayBindingPattern(node.name)) { - checkIteratedTypeOrElementType(IterationUse.Destructuring, widenedType, undefinedType, node); - } - else if (strictNullChecks) { - checkNonNullNonVoidType(widenedType, node); - } + // check private/protected variable access + const parent = node.parent.parent; + const parentCheckMode = node.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal; + const parentType = getTypeForBindingElementParent(parent, parentCheckMode); + const name = node.propertyName || node.name; + if (parentType && !ts.isBindingPattern(name)) { + const exprType = getLiteralTypeFromPropertyName(name); + if (isTypeUsableAsPropertyName(exprType)) { + const nameText = getPropertyNameFromType(exprType); + const property = getPropertyOfType(parentType, nameText); + if (property) { + markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined, /*isSelfTypeAccess*/ false); // A destructuring is never a write-only reference. + checkPropertyAccessibility(node, !!parent.initializer && parent.initializer.kind === ts.SyntaxKind.SuperKeyword, /*writing*/ false, parentType, property); } } - return; } - // For a commonjs `const x = require`, validate the alias and exit - const symbol = getSymbolOfNode(node); - if (symbol.flags & ts.SymbolFlags.Alias && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node)) { - checkAliasSymbol(node); - return; + } + + // For a binding pattern, check contained binding elements + if (ts.isBindingPattern(node.name)) { + if (node.name.kind === ts.SyntaxKind.ArrayBindingPattern && languageVersion < ts.ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.Read); } - const type = convertAutoToAny(getTypeOfSymbol(symbol)); - if (node === symbol.valueDeclaration) { - // Node is the primary declaration of the symbol, just validate the initializer + ts.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 && ts.isParameterDeclaration(node) && ts.nodeIsMissing((ts.getContainingFunction(node) as ts.FunctionLikeDeclaration).body)) { + error(node, ts.Diagnostics.A_parameter_initializer_is_only_allowed_in_a_function_or_constructor_implementation); + return; + } + // For a binding pattern, validate the initializer and exit + if (ts.isBindingPattern(node.name)) { + const needCheckInitializer = node.initializer && node.parent.parent.kind !== ts.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 initializer = ts.getEffectiveInitializer(node); - if (initializer) { - const isJSObjectLiteralInitializer = ts.isInJSFile(node) && - ts.isObjectLiteralExpression(initializer) && - (initializer.properties.length === 0 || ts.isPrototypeAccess(node.name)) && - !!symbol.exports?.size; - if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== ts.SyntaxKind.ForInStatement) { - checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(initializer), type, node, initializer, /*headMessage*/ undefined); + 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); } } - if (symbol.declarations && symbol.declarations.length > 1) { - if (ts.some(symbol.declarations, d => d !== node && ts.isVariableLike(d) && !areDeclarationFlagsIdentical(d, node))) { - error(node.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_modifiers, ts.declarationNameToString(node.name)); + // check the binding pattern with empty elements + if (needCheckWidenedType) { + if (ts.isArrayBindingPattern(node.name)) { + checkIteratedTypeOrElementType(IterationUse.Destructuring, widenedType, undefinedType, node); + } + else if (strictNullChecks) { + checkNonNullNonVoidType(widenedType, node); } } } - 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)); + return; + } + // For a commonjs `const x = require`, validate the alias and exit + const symbol = getSymbolOfNode(node); + if (symbol.flags & ts.SymbolFlags.Alias && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(node)) { + checkAliasSymbol(node); + return; + } - if (!isErrorType(type) && !isErrorType(declarationType) && - !isTypeIdenticalTo(type, declarationType) && - !(symbol.flags & ts.SymbolFlags.Assignment)) { - errorNextVariableOrPropertyDeclarationMustHaveSameType(symbol.valueDeclaration, type, node, declarationType); - } - if (node.initializer) { - checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(node.initializer), declarationType, node, node.initializer, /*headMessage*/ undefined); - } - if (symbol.valueDeclaration && !areDeclarationFlagsIdentical(node, symbol.valueDeclaration)) { - error(node.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_modifiers, ts.declarationNameToString(node.name)); + 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 = ts.getEffectiveInitializer(node); + if (initializer) { + const isJSObjectLiteralInitializer = ts.isInJSFile(node) && + ts.isObjectLiteralExpression(initializer) && + (initializer.properties.length === 0 || ts.isPrototypeAccess(node.name)) && + !!symbol.exports?.size; + if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== ts.SyntaxKind.ForInStatement) { + checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(initializer), type, node, initializer, /*headMessage*/ undefined); } } - if (node.kind !== ts.SyntaxKind.PropertyDeclaration && node.kind !== ts.SyntaxKind.PropertySignature) { - // We know we don't have a binding pattern or computed name here - checkExportsOnMergedDeclarations(node); - if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { - checkVarDeclaredNamesNotShadowed(node); + if (symbol.declarations && symbol.declarations.length > 1) { + if (ts.some(symbol.declarations, d => d !== node && ts.isVariableLike(d) && !areDeclarationFlagsIdentical(d, node))) { + error(node.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_modifiers, ts.declarationNameToString(node.name)); } - checkCollisionsForDeclarationName(node, node.name); } } + 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)); - function errorNextVariableOrPropertyDeclarationMustHaveSameType(firstDeclaration: ts.Declaration | undefined, firstType: ts.Type, nextDeclaration: ts.Declaration, nextType: ts.Type): void { - const nextDeclarationName = ts.getNameOfDeclaration(nextDeclaration); - const message = nextDeclaration.kind === ts.SyntaxKind.PropertyDeclaration || nextDeclaration.kind === ts.SyntaxKind.PropertySignature - ? ts.Diagnostics.Subsequent_property_declarations_must_have_the_same_type_Property_0_must_be_of_type_1_but_here_has_type_2 - : ts.Diagnostics.Subsequent_variable_declarations_must_have_the_same_type_Variable_0_must_be_of_type_1_but_here_has_type_2; - const declName = ts.declarationNameToString(nextDeclarationName); - const err = error(nextDeclarationName, message, declName, typeToString(firstType), typeToString(nextType)); - if (firstDeclaration) { - ts.addRelatedInfo(err, ts.createDiagnosticForNode(firstDeclaration, ts.Diagnostics._0_was_also_declared_here, declName)); + if (!isErrorType(type) && !isErrorType(declarationType) && + !isTypeIdenticalTo(type, declarationType) && + !(symbol.flags & ts.SymbolFlags.Assignment)) { + errorNextVariableOrPropertyDeclarationMustHaveSameType(symbol.valueDeclaration, type, node, declarationType); } - } - - function areDeclarationFlagsIdentical(left: ts.Declaration, right: ts.Declaration) { - if ((left.kind === ts.SyntaxKind.Parameter && right.kind === ts.SyntaxKind.VariableDeclaration) || - (left.kind === ts.SyntaxKind.VariableDeclaration && right.kind === ts.SyntaxKind.Parameter)) { - // Differences in optionality between parameters and variables are allowed. - return true; + if (node.initializer) { + checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(node.initializer), declarationType, node, node.initializer, /*headMessage*/ undefined); } - - if (ts.hasQuestionToken(left) !== ts.hasQuestionToken(right)) { - return false; + if (symbol.valueDeclaration && !areDeclarationFlagsIdentical(node, symbol.valueDeclaration)) { + error(node.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_modifiers, ts.declarationNameToString(node.name)); } - - const interestingFlags = ts.ModifierFlags.Private | - ts.ModifierFlags.Protected | - ts.ModifierFlags.Async | - ts.ModifierFlags.Abstract | - ts.ModifierFlags.Readonly | - ts.ModifierFlags.Static; - return ts.getSelectedEffectiveModifierFlags(left, interestingFlags) === ts.getSelectedEffectiveModifierFlags(right, interestingFlags); } + if (node.kind !== ts.SyntaxKind.PropertyDeclaration && node.kind !== ts.SyntaxKind.PropertySignature) { + // We know we don't have a binding pattern or computed name here + checkExportsOnMergedDeclarations(node); + if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { + checkVarDeclaredNamesNotShadowed(node); + } + checkCollisionsForDeclarationName(node, node.name); + } + } - function checkVariableDeclaration(node: ts.VariableDeclaration) { - ts.tracing?.push(ts.tracing.Phase.Check, "checkVariableDeclaration", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); - checkGrammarVariableDeclaration(node); - checkVariableLikeDeclaration(node); - ts.tracing?.pop(); + function errorNextVariableOrPropertyDeclarationMustHaveSameType(firstDeclaration: ts.Declaration | undefined, firstType: ts.Type, nextDeclaration: ts.Declaration, nextType: ts.Type): void { + const nextDeclarationName = ts.getNameOfDeclaration(nextDeclaration); + const message = nextDeclaration.kind === ts.SyntaxKind.PropertyDeclaration || nextDeclaration.kind === ts.SyntaxKind.PropertySignature + ? ts.Diagnostics.Subsequent_property_declarations_must_have_the_same_type_Property_0_must_be_of_type_1_but_here_has_type_2 + : ts.Diagnostics.Subsequent_variable_declarations_must_have_the_same_type_Variable_0_must_be_of_type_1_but_here_has_type_2; + const declName = ts.declarationNameToString(nextDeclarationName); + const err = error(nextDeclarationName, message, declName, typeToString(firstType), typeToString(nextType)); + if (firstDeclaration) { + ts.addRelatedInfo(err, ts.createDiagnosticForNode(firstDeclaration, ts.Diagnostics._0_was_also_declared_here, declName)); } + } - function checkBindingElement(node: ts.BindingElement) { - checkGrammarBindingElement(node); - return checkVariableLikeDeclaration(node); + function areDeclarationFlagsIdentical(left: ts.Declaration, right: ts.Declaration) { + if ((left.kind === ts.SyntaxKind.Parameter && right.kind === ts.SyntaxKind.VariableDeclaration) || + (left.kind === ts.SyntaxKind.VariableDeclaration && right.kind === ts.SyntaxKind.Parameter)) { + // Differences in optionality between parameters and variables are allowed. + return true; } - function checkVariableStatement(node: ts.VariableStatement) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarVariableDeclarationList(node.declarationList)) - checkGrammarForDisallowedLetOrConstStatement(node); - ts.forEach(node.declarationList.declarations, checkSourceElement); + if (ts.hasQuestionToken(left) !== ts.hasQuestionToken(right)) { + return false; } - function checkExpressionStatement(node: ts.ExpressionStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); + const interestingFlags = ts.ModifierFlags.Private | + ts.ModifierFlags.Protected | + ts.ModifierFlags.Async | + ts.ModifierFlags.Abstract | + ts.ModifierFlags.Readonly | + ts.ModifierFlags.Static; + return ts.getSelectedEffectiveModifierFlags(left, interestingFlags) === ts.getSelectedEffectiveModifierFlags(right, interestingFlags); + } + + function checkVariableDeclaration(node: ts.VariableDeclaration) { + ts.tracing?.push(ts.tracing.Phase.Check, "checkVariableDeclaration", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); + checkGrammarVariableDeclaration(node); + checkVariableLikeDeclaration(node); + ts.tracing?.pop(); + } + + function checkBindingElement(node: ts.BindingElement) { + checkGrammarBindingElement(node); + return checkVariableLikeDeclaration(node); + } + + function checkVariableStatement(node: ts.VariableStatement) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarVariableDeclarationList(node.declarationList)) + checkGrammarForDisallowedLetOrConstStatement(node); + ts.forEach(node.declarationList.declarations, checkSourceElement); + } + + function checkExpressionStatement(node: ts.ExpressionStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + + checkExpression(node.expression); + } + + function checkIfStatement(node: ts.IfStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + checkTruthinessExpression(node.expression); + checkTestingKnownTruthyCallableOrAwaitableType(node.expression, node.thenStatement); + checkSourceElement(node.thenStatement); - checkExpression(node.expression); + if (node.thenStatement.kind === ts.SyntaxKind.EmptyStatement) { + error(node.thenStatement, ts.Diagnostics.The_body_of_an_if_statement_cannot_be_the_empty_statement); } - function checkIfStatement(node: ts.IfStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - checkTruthinessExpression(node.expression); - checkTestingKnownTruthyCallableOrAwaitableType(node.expression, node.thenStatement); - checkSourceElement(node.thenStatement); + checkSourceElement(node.elseStatement); + } - if (node.thenStatement.kind === ts.SyntaxKind.EmptyStatement) { - error(node.thenStatement, ts.Diagnostics.The_body_of_an_if_statement_cannot_be_the_empty_statement); - } + function checkTestingKnownTruthyCallableOrAwaitableType(condExpr: ts.Expression, body?: ts.Statement | ts.Expression) { + if (!strictNullChecks) + return; - checkSourceElement(node.elseStatement); + helper(condExpr, body); + while (ts.isBinaryExpression(condExpr) && condExpr.operatorToken.kind === ts.SyntaxKind.BarBarToken) { + condExpr = condExpr.left; + helper(condExpr, body); } - function checkTestingKnownTruthyCallableOrAwaitableType(condExpr: ts.Expression, body?: ts.Statement | ts.Expression) { - if (!strictNullChecks) + function helper(condExpr: ts.Expression, body: ts.Expression | ts.Statement | undefined) { + const location = ts.isBinaryExpression(condExpr) && + (condExpr.operatorToken.kind === ts.SyntaxKind.BarBarToken || condExpr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + ? condExpr.right + : condExpr; + if (ts.isModuleExportsAccessExpression(location)) + return; + const type = checkTruthinessExpression(location); + const isPropertyExpressionCast = ts.isPropertyAccessExpression(location) && isTypeAssertion(location.expression); + if (getFalsyFlags(type) || isPropertyExpressionCast) return; - helper(condExpr, body); - while (ts.isBinaryExpression(condExpr) && condExpr.operatorToken.kind === ts.SyntaxKind.BarBarToken) { - condExpr = condExpr.left; - helper(condExpr, body); + // While it technically should be invalid for any known-truthy value + // to be tested, we de-scope to functions and Promises unreferenced 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, ts.SignatureKind.Call); + const isPromise = !!getAwaitedTypeOfPromise(type); + if (callSignatures.length === 0 && !isPromise) { + return; } - function helper(condExpr: ts.Expression, body: ts.Expression | ts.Statement | undefined) { - const location = ts.isBinaryExpression(condExpr) && - (condExpr.operatorToken.kind === ts.SyntaxKind.BarBarToken || condExpr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) - ? condExpr.right - : condExpr; - if (ts.isModuleExportsAccessExpression(location)) - return; - const type = checkTruthinessExpression(location); - const isPropertyExpressionCast = ts.isPropertyAccessExpression(location) && isTypeAssertion(location.expression); - if (getFalsyFlags(type) || isPropertyExpressionCast) - return; - - // While it technically should be invalid for any known-truthy value - // to be tested, we de-scope to functions and Promises unreferenced 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, ts.SignatureKind.Call); - const isPromise = !!getAwaitedTypeOfPromise(type); - if (callSignatures.length === 0 && !isPromise) { - return; - } + const testedNode = ts.isIdentifier(location) ? location + : ts.isPropertyAccessExpression(location) ? location.name + : ts.isBinaryExpression(location) && ts.isIdentifier(location.right) ? location.right + : undefined; + const testedSymbol = testedNode && getSymbolAtLocation(testedNode); + if (!testedSymbol && !isPromise) { + return; + } - const testedNode = ts.isIdentifier(location) ? location - : ts.isPropertyAccessExpression(location) ? location.name - : ts.isBinaryExpression(location) && ts.isIdentifier(location.right) ? location.right - : undefined; - const testedSymbol = testedNode && getSymbolAtLocation(testedNode); - if (!testedSymbol && !isPromise) { - return; + const isUsed = testedSymbol && ts.isBinaryExpression(condExpr.parent) && isSymbolUsedInBinaryExpressionChain(condExpr.parent, testedSymbol) + || testedSymbol && body && isSymbolUsedInConditionBody(condExpr, body, testedNode, testedSymbol); + if (!isUsed) { + if (isPromise) { + errorAndMaybeSuggestAwait(location, + /*maybeMissingAwait*/ true, ts.Diagnostics.This_condition_will_always_return_true_since_this_0_is_always_defined, getTypeNameForErrorDisplay(type)); } - - const isUsed = testedSymbol && ts.isBinaryExpression(condExpr.parent) && isSymbolUsedInBinaryExpressionChain(condExpr.parent, testedSymbol) - || testedSymbol && body && isSymbolUsedInConditionBody(condExpr, body, testedNode, testedSymbol); - if (!isUsed) { - if (isPromise) { - errorAndMaybeSuggestAwait(location, - /*maybeMissingAwait*/ true, ts.Diagnostics.This_condition_will_always_return_true_since_this_0_is_always_defined, getTypeNameForErrorDisplay(type)); - } - else { - error(location, ts.Diagnostics.This_condition_will_always_return_true_since_this_function_is_always_defined_Did_you_mean_to_call_it_instead); - } + else { + error(location, ts.Diagnostics.This_condition_will_always_return_true_since_this_function_is_always_defined_Did_you_mean_to_call_it_instead); } } } + } - function isSymbolUsedInConditionBody(expr: ts.Expression, body: ts.Statement | ts.Expression, testedNode: ts.Node, testedSymbol: ts.Symbol): boolean { - return !!ts.forEachChild(body, function check(childNode): boolean | undefined { - if (ts.isIdentifier(childNode)) { - const childSymbol = getSymbolAtLocation(childNode); - if (childSymbol && childSymbol === testedSymbol) { - // If the test was a simple identifier, the above check is sufficient - if (ts.isIdentifier(expr)) { - return true; + function isSymbolUsedInConditionBody(expr: ts.Expression, body: ts.Statement | ts.Expression, testedNode: ts.Node, testedSymbol: ts.Symbol): boolean { + return !!ts.forEachChild(body, function check(childNode): boolean | undefined { + if (ts.isIdentifier(childNode)) { + const childSymbol = getSymbolAtLocation(childNode); + if (childSymbol && childSymbol === testedSymbol) { + // If the test was a simple identifier, the above check is sufficient + if (ts.isIdentifier(expr)) { + return true; + } + // Otherwise we need to ensure the symbol is called on the same target + let testedExpression = testedNode.parent; + let childExpression = childNode.parent; + while (testedExpression && childExpression) { + if (ts.isIdentifier(testedExpression) && ts.isIdentifier(childExpression) || + testedExpression.kind === ts.SyntaxKind.ThisKeyword && childExpression.kind === ts.SyntaxKind.ThisKeyword) { + return getSymbolAtLocation(testedExpression) === getSymbolAtLocation(childExpression); } - // Otherwise we need to ensure the symbol is called on the same target - let testedExpression = testedNode.parent; - let childExpression = childNode.parent; - while (testedExpression && childExpression) { - if (ts.isIdentifier(testedExpression) && ts.isIdentifier(childExpression) || - testedExpression.kind === ts.SyntaxKind.ThisKeyword && childExpression.kind === ts.SyntaxKind.ThisKeyword) { - return getSymbolAtLocation(testedExpression) === getSymbolAtLocation(childExpression); - } - else if (ts.isPropertyAccessExpression(testedExpression) && ts.isPropertyAccessExpression(childExpression)) { - if (getSymbolAtLocation(testedExpression.name) !== getSymbolAtLocation(childExpression.name)) { - return false; - } - childExpression = childExpression.expression; - testedExpression = testedExpression.expression; - } - else if (ts.isCallExpression(testedExpression) && ts.isCallExpression(childExpression)) { - childExpression = childExpression.expression; - testedExpression = testedExpression.expression; - } - else { + else if (ts.isPropertyAccessExpression(testedExpression) && ts.isPropertyAccessExpression(childExpression)) { + if (getSymbolAtLocation(testedExpression.name) !== getSymbolAtLocation(childExpression.name)) { return false; } + childExpression = childExpression.expression; + testedExpression = testedExpression.expression; + } + else if (ts.isCallExpression(testedExpression) && ts.isCallExpression(childExpression)) { + childExpression = childExpression.expression; + testedExpression = testedExpression.expression; + } + else { + return false; } } } - return ts.forEachChild(childNode, check); - }); - } + } + return ts.forEachChild(childNode, check); + }); + } - function isSymbolUsedInBinaryExpressionChain(node: ts.Node, testedSymbol: ts.Symbol): boolean { - while (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { - const isUsed = ts.forEachChild(node.right, function visit(child): boolean | undefined { - if (ts.isIdentifier(child)) { - const symbol = getSymbolAtLocation(child); - if (symbol && symbol === testedSymbol) { - return true; - } + function isSymbolUsedInBinaryExpressionChain(node: ts.Node, testedSymbol: ts.Symbol): boolean { + while (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + const isUsed = ts.forEachChild(node.right, function visit(child): boolean | undefined { + if (ts.isIdentifier(child)) { + const symbol = getSymbolAtLocation(child); + if (symbol && symbol === testedSymbol) { + return true; } - return ts.forEachChild(child, visit); - }); - if (isUsed) { - return true; } - node = node.parent; + return ts.forEachChild(child, visit); + }); + if (isUsed) { + return true; } - return false; + node = node.parent; } + return false; + } - function checkDoStatement(node: ts.DoStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); + function checkDoStatement(node: ts.DoStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); - checkSourceElement(node.statement); - checkTruthinessExpression(node.expression); - } + checkSourceElement(node.statement); + checkTruthinessExpression(node.expression); + } - function checkWhileStatement(node: ts.WhileStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); + function checkWhileStatement(node: ts.WhileStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); - checkTruthinessExpression(node.expression); - checkSourceElement(node.statement); - } + checkTruthinessExpression(node.expression); + checkSourceElement(node.statement); + } - function checkTruthinessOfType(type: ts.Type, node: ts.Node) { - if (type.flags & ts.TypeFlags.Void) { - error(node, ts.Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness); - } - return type; + function checkTruthinessOfType(type: ts.Type, node: ts.Node) { + if (type.flags & ts.TypeFlags.Void) { + error(node, ts.Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness); } + return type; + } - function checkTruthinessExpression(node: ts.Expression, checkMode?: CheckMode) { - return checkTruthinessOfType(checkExpression(node, checkMode), node); - } + function checkTruthinessExpression(node: ts.Expression, checkMode?: CheckMode) { + return checkTruthinessOfType(checkExpression(node, checkMode), node); + } - function checkForStatement(node: ts.ForStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - if (node.initializer && node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { - checkGrammarVariableDeclarationList(node.initializer as ts.VariableDeclarationList); - } + function checkForStatement(node: ts.ForStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + if (node.initializer && node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { + checkGrammarVariableDeclarationList(node.initializer as ts.VariableDeclarationList); } + } - if (node.initializer) { - if (node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { - ts.forEach((node.initializer as ts.VariableDeclarationList).declarations, checkVariableDeclaration); - } - else { - checkExpression(node.initializer); - } + if (node.initializer) { + if (node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { + ts.forEach((node.initializer as ts.VariableDeclarationList).declarations, checkVariableDeclaration); } - - if (node.condition) - checkTruthinessExpression(node.condition); - if (node.incrementor) - checkExpression(node.incrementor); - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); + else { + checkExpression(node.initializer); } } - function checkForOfStatement(node: ts.ForOfStatement): void { - checkGrammarForInOrForOfStatement(node); + if (node.condition) + checkTruthinessExpression(node.condition); + if (node.incrementor) + checkExpression(node.incrementor); + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); + } + } - const container = ts.getContainingFunctionOrClassStaticBlock(node); - if (node.awaitModifier) { - if (container && ts.isClassStaticBlockDeclaration(container)) { - grammarErrorOnNode(node.awaitModifier, ts.Diagnostics.For_await_loops_cannot_be_used_inside_a_class_static_block); - } - else { - const functionFlags = ts.getFunctionFlags(container); - if ((functionFlags & (ts.FunctionFlags.Invalid | ts.FunctionFlags.Async)) === ts.FunctionFlags.Async && languageVersion < ts.ScriptTarget.ESNext) { - // for..await..of in an async function or async generator function prior to ESNext requires the __asyncValues helper - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ForAwaitOfIncludes); - } - } + function checkForOfStatement(node: ts.ForOfStatement): void { + checkGrammarForInOrForOfStatement(node); + + const container = ts.getContainingFunctionOrClassStaticBlock(node); + if (node.awaitModifier) { + if (container && ts.isClassStaticBlockDeclaration(container)) { + grammarErrorOnNode(node.awaitModifier, ts.Diagnostics.For_await_loops_cannot_be_used_inside_a_class_static_block); } - else if (compilerOptions.downlevelIteration && languageVersion < ts.ScriptTarget.ES2015) { - // for..of prior to ES2015 requires the __values helper when downlevelIteration is enabled - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ForOfIncludes); + else { + const functionFlags = ts.getFunctionFlags(container); + if ((functionFlags & (ts.FunctionFlags.Invalid | ts.FunctionFlags.Async)) === ts.FunctionFlags.Async && languageVersion < ts.ScriptTarget.ESNext) { + // for..await..of in an async function or async generator function prior to ESNext requires the __asyncValues helper + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ForAwaitOfIncludes); + } } + } + else if (compilerOptions.downlevelIteration && languageVersion < ts.ScriptTarget.ES2015) { + // for..of prior to ES2015 requires the __values helper when downlevelIteration is enabled + checkExternalEmitHelpers(node, ts.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 === ts.SyntaxKind.VariableDeclarationList) { - checkForInOrForOfVariableDeclaration(node); + // 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 === ts.SyntaxKind.VariableDeclarationList) { + checkForInOrForOfVariableDeclaration(node); + } + else { + const varExpr = node.initializer; + const iteratedType = checkRightHandSideOfForOf(node); + + // There may be a destructuring assignment on the left side + if (varExpr.kind === ts.SyntaxKind.ArrayLiteralExpression || varExpr.kind === ts.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 varExpr = node.initializer; - const iteratedType = checkRightHandSideOfForOf(node); - - // There may be a destructuring assignment on the left side - if (varExpr.kind === ts.SyntaxKind.ArrayLiteralExpression || varExpr.kind === ts.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, ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_an_optional_property_access); + const leftType = checkExpression(varExpr); + checkReferenceExpression(varExpr, ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_must_be_a_variable_or_a_property_access, ts.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); - } + // 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); } } + } - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); - } + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); } + } - function checkForInStatement(node: ts.ForInStatement) { - // Grammar checking - checkGrammarForInOrForOfStatement(node); + function checkForInStatement(node: ts.ForInStatement) { + // Grammar checking + checkGrammarForInOrForOfStatement(node); - const rightType = getNonNullableTypeIfNeeded(checkExpression(node.expression)); - // TypeScript 1.0 spec (April 2014): 5.4 + 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 === ts.SyntaxKind.VariableDeclarationList) { + const variable = (node.initializer as ts.VariableDeclarationList).declarations[0]; + if (variable && ts.isBindingPattern(variable.name)) { + error(variable.name, ts.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 (let VarDecl in Expr) Statement - // VarDecl must be a variable declaration without a type annotation that declares a variable of type Any, + // 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. - if (node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { - const variable = (node.initializer as ts.VariableDeclarationList).declarations[0]; - if (variable && ts.isBindingPattern(variable.name)) { - error(variable.name, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); - } - checkForInOrForOfVariableDeclaration(node); + const varExpr = node.initializer; + const leftType = checkExpression(varExpr); + if (varExpr.kind === ts.SyntaxKind.ArrayLiteralExpression || varExpr.kind === ts.SyntaxKind.ObjectLiteralExpression) { + error(varExpr, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); } - 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 === ts.SyntaxKind.ArrayLiteralExpression || varExpr.kind === ts.SyntaxKind.ObjectLiteralExpression) { - error(varExpr, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); - } - else if (!isTypeAssignableTo(getIndexTypeOrString(rightType), leftType)) { - error(varExpr, ts.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, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_may_not_be_an_optional_property_access); - } + else if (!isTypeAssignableTo(getIndexTypeOrString(rightType), leftType)) { + error(varExpr, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_of_type_string_or_any); } - - // 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, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive)) { - error(node.expression, ts.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)); + else { + // run check only former check succeeded to avoid cascading errors + checkReferenceExpression(varExpr, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_a_variable_or_a_property_access, ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_may_not_be_an_optional_property_access); } + } - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); - } + // 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, ts.TypeFlags.NonPrimitive | ts.TypeFlags.InstantiableNonPrimitive)) { + error(node.expression, ts.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)); } - function checkForInOrForOfVariableDeclaration(iterationStatement: ts.ForInOrOfStatement): void { - const variableDeclarationList = iterationStatement.initializer as ts.VariableDeclarationList; - // checkGrammarForInOrForOfStatement will check that there is exactly one declaration. - if (variableDeclarationList.declarations.length >= 1) { - const decl = variableDeclarationList.declarations[0]; - checkVariableDeclaration(decl); - } + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); } + } - function checkRightHandSideOfForOf(statement: ts.ForOfStatement): ts.Type { - const use = statement.awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; - return checkIteratedTypeOrElementType(use, checkNonNullExpression(statement.expression), undefinedType, statement.expression); + function checkForInOrForOfVariableDeclaration(iterationStatement: ts.ForInOrOfStatement): void { + const variableDeclarationList = iterationStatement.initializer as ts.VariableDeclarationList; + // checkGrammarForInOrForOfStatement will check that there is exactly one declaration. + if (variableDeclarationList.declarations.length >= 1) { + const decl = variableDeclarationList.declarations[0]; + checkVariableDeclaration(decl); } + } - function checkIteratedTypeOrElementType(use: IterationUse, inputType: ts.Type, sentType: ts.Type, errorNode: ts.Node | undefined): ts.Type { - if (isTypeAny(inputType)) { - return inputType; - } - return getIteratedTypeOrElementType(use, inputType, sentType, errorNode, /*checkAssignability*/ true) || anyType; + function checkRightHandSideOfForOf(statement: ts.ForOfStatement): ts.Type { + const use = statement.awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; + return checkIteratedTypeOrElementType(use, checkNonNullExpression(statement.expression), undefinedType, statement.expression); + } + + function checkIteratedTypeOrElementType(use: IterationUse, inputType: ts.Type, sentType: ts.Type, errorNode: ts.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: ts.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; - } + /** + * 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: ts.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 >= ts.ScriptTarget.ES2015; - const downlevelIteration = !uplevelIteration && compilerOptions.downlevelIteration; - const possibleOutOfBounds = compilerOptions.noUncheckedIndexedAccess && !!(use & IterationUse.PossiblyOutOfBounds); + const uplevelIteration = languageVersion >= ts.ScriptTarget.ES2015; + const downlevelIteration = !uplevelIteration && compilerOptions.downlevelIteration; + const possibleOutOfBounds = compilerOptions.noUncheckedIndexedAccess && !!(use & IterationUse.PossiblyOutOfBounds); - // 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 ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_for_of_will_always_send_0 : - use & IterationUse.SpreadFlag ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_spread_will_always_send_0 : - use & IterationUse.DestructuringFlag ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_destructuring_will_always_send_0 : - use & IterationUse.YieldStarFlag ? ts.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); - } + // 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 ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_for_of_will_always_send_0 : + use & IterationUse.SpreadFlag ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_spread_will_always_send_0 : + use & IterationUse.DestructuringFlag ? ts.Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_destructuring_will_always_send_0 : + use & IterationUse.YieldStarFlag ? ts.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 possibleOutOfBounds ? includeUndefinedInIndexSignature(iterationTypes && iterationTypes.yieldType) : (iterationTypes && iterationTypes.yieldType); - } } + if (iterationTypes || uplevelIteration) { + return possibleOutOfBounds ? includeUndefinedInIndexSignature(iterationTypes && iterationTypes.yieldType) : (iterationTypes && iterationTypes.yieldType); + } + } - let arrayType = inputType; - let reportedError = false; - let hasStringConstituent = false; + 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 & ts.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 as ts.UnionType).types; - const filteredTypes = ts.filter(arrayTypes, t => !(t.flags & ts.TypeFlags.StringLike)); - if (filteredTypes !== arrayTypes) { - arrayType = getUnionType(filteredTypes, ts.UnionReduction.Subtype); - } - } - else if (arrayType.flags & ts.TypeFlags.StringLike) { - arrayType = neverType; + // 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 & ts.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 as ts.UnionType).types; + const filteredTypes = ts.filter(arrayTypes, t => !(t.flags & ts.TypeFlags.StringLike)); + if (filteredTypes !== arrayTypes) { + arrayType = getUnionType(filteredTypes, ts.UnionReduction.Subtype); } + } + else if (arrayType.flags & ts.TypeFlags.StringLike) { + arrayType = neverType; + } - hasStringConstituent = arrayType !== inputType; - if (hasStringConstituent) { - if (languageVersion < ts.ScriptTarget.ES5) { - if (errorNode) { - error(errorNode, ts.Diagnostics.Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher); - reportedError = true; - } - } - - // Now that we've removed all the StringLike types, if no constituents remain, then the entire - // arrayOrStringType was a string. - if (arrayType.flags & ts.TypeFlags.Never) { - return possibleOutOfBounds ? includeUndefinedInIndexSignature(stringType) : stringType; + hasStringConstituent = arrayType !== inputType; + if (hasStringConstituent) { + if (languageVersion < ts.ScriptTarget.ES5) { + if (errorNode) { + error(errorNode, ts.Diagnostics.Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher); + reportedError = true; } } - } - 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 allowsStrings = !!(use & IterationUse.AllowsStringInputFlag) && !hasStringConstituent; - const [defaultDiagnostic, maybeMissingAwait] = getIterationDiagnosticDetails(allowsStrings, downlevelIteration); - errorAndMaybeSuggestAwait(errorNode, maybeMissingAwait && !!getAwaitedTypeOfPromise(arrayType), defaultDiagnostic, typeToString(arrayType)); + // Now that we've removed all the StringLike types, if no constituents remain, then the entire + // arrayOrStringType was a string. + if (arrayType.flags & ts.TypeFlags.Never) { + return possibleOutOfBounds ? includeUndefinedInIndexSignature(stringType) : stringType; } - return hasStringConstituent ? possibleOutOfBounds ? includeUndefinedInIndexSignature(stringType) : stringType : undefined; } + } - const arrayElementType = getIndexTypeOfType(arrayType, numberType); - if (hasStringConstituent && arrayElementType) { - // This is just an optimization for the case where arrayOrStringType is string | string[] - if (arrayElementType.flags & ts.TypeFlags.StringLike && !compilerOptions.noUncheckedIndexedAccess) { - return stringType; - } - - return getUnionType(possibleOutOfBounds ? [arrayElementType, stringType, undefinedType] : [arrayElementType, stringType], ts.UnionReduction.Subtype); + 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 allowsStrings = !!(use & IterationUse.AllowsStringInputFlag) && !hasStringConstituent; + const [defaultDiagnostic, maybeMissingAwait] = getIterationDiagnosticDetails(allowsStrings, downlevelIteration); + errorAndMaybeSuggestAwait(errorNode, maybeMissingAwait && !!getAwaitedTypeOfPromise(arrayType), defaultDiagnostic, typeToString(arrayType)); } + return hasStringConstituent ? possibleOutOfBounds ? includeUndefinedInIndexSignature(stringType) : stringType : undefined; + } - return (use & IterationUse.PossiblyOutOfBounds) ? includeUndefinedInIndexSignature(arrayElementType) : arrayElementType; + const arrayElementType = getIndexTypeOfType(arrayType, numberType); + if (hasStringConstituent && arrayElementType) { + // This is just an optimization for the case where arrayOrStringType is string | string[] + if (arrayElementType.flags & ts.TypeFlags.StringLike && !compilerOptions.noUncheckedIndexedAccess) { + return stringType; + } - function getIterationDiagnosticDetails(allowsStrings: boolean, downlevelIteration: boolean | undefined): [ - error: ts.DiagnosticMessage, - maybeMissingAwait: boolean - ] { - if (downlevelIteration) { - return allowsStrings - ? [ts.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] - : [ts.Diagnostics.Type_0_is_not_an_array_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true]; - } + return getUnionType(possibleOutOfBounds ? [arrayElementType, stringType, undefinedType] : [arrayElementType, stringType], ts.UnionReduction.Subtype); + } - const yieldType = getIterationTypeOfIterable(use, IterationTypeKind.Yield, inputType, /*errorNode*/ undefined); + return (use & IterationUse.PossiblyOutOfBounds) ? includeUndefinedInIndexSignature(arrayElementType) : arrayElementType; - if (yieldType) { - return [ts.Diagnostics.Type_0_can_only_be_iterated_through_when_using_the_downlevelIteration_flag_or_with_a_target_of_es2015_or_higher, false]; - } + function getIterationDiagnosticDetails(allowsStrings: boolean, downlevelIteration: boolean | undefined): [ + error: ts.DiagnosticMessage, + maybeMissingAwait: boolean + ] { + if (downlevelIteration) { + return allowsStrings + ? [ts.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] + : [ts.Diagnostics.Type_0_is_not_an_array_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true]; + } - if (isES2015OrLaterIterable(inputType.symbol?.escapedName)) { - return [ts.Diagnostics.Type_0_can_only_be_iterated_through_when_using_the_downlevelIteration_flag_or_with_a_target_of_es2015_or_higher, true]; - } + const yieldType = getIterationTypeOfIterable(use, IterationTypeKind.Yield, inputType, /*errorNode*/ undefined); - return allowsStrings - ? [ts.Diagnostics.Type_0_is_not_an_array_type_or_a_string_type, true] - : [ts.Diagnostics.Type_0_is_not_an_array_type, true]; - } - } - - function isES2015OrLaterIterable(n: ts.__String) { - switch (n) { - case "Float32Array": - case "Float64Array": - case "Int16Array": - case "Int32Array": - case "Int8Array": - case "NodeList": - case "Uint16Array": - case "Uint32Array": - case "Uint8Array": - case "Uint8ClampedArray": - return true; + if (yieldType) { + return [ts.Diagnostics.Type_0_can_only_be_iterated_through_when_using_the_downlevelIteration_flag_or_with_a_target_of_es2015_or_higher, false]; } - return false; - } - /** - * Gets the requested "iteration type" from an `Iterable`-like or `AsyncIterable`-like type. - */ - function getIterationTypeOfIterable(use: IterationUse, typeKind: IterationTypeKind, inputType: ts.Type, errorNode: ts.Node | undefined): ts.Type | undefined { - if (isTypeAny(inputType)) { - return undefined; + if (isES2015OrLaterIterable(inputType.symbol?.escapedName)) { + return [ts.Diagnostics.Type_0_can_only_be_iterated_through_when_using_the_downlevelIteration_flag_or_with_a_target_of_es2015_or_higher, true]; } - const iterationTypes = getIterationTypesOfIterable(inputType, use, errorNode); - return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(typeKind)]; + return allowsStrings + ? [ts.Diagnostics.Type_0_is_not_an_array_type_or_a_string_type, true] + : [ts.Diagnostics.Type_0_is_not_an_array_type, true]; } + } - function createIterationTypes(yieldType: ts.Type = neverType, returnType: ts.Type = neverType, nextType: ts.Type = unknownType): ts.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. + function isES2015OrLaterIterable(n: ts.__String) { + switch (n) { + case "Float32Array": + case "Float64Array": + case "Int16Array": + case "Int32Array": + case "Int8Array": + case "NodeList": + case "Uint16Array": + case "Uint32Array": + case "Uint8Array": + case "Uint8ClampedArray": + return true; + } + return false; + } - // 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 & ts.TypeFlags.Intrinsic && - returnType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never | ts.TypeFlags.Unknown | ts.TypeFlags.Void | ts.TypeFlags.Undefined) && - nextType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never | ts.TypeFlags.Unknown | ts.TypeFlags.Void | ts.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 }; + /** + * Gets the requested "iteration type" from an `Iterable`-like or `AsyncIterable`-like type. + */ + function getIterationTypeOfIterable(use: IterationUse, typeKind: IterationTypeKind, inputType: ts.Type, errorNode: ts.Node | undefined): ts.Type | undefined { + if (isTypeAny(inputType)) { + return undefined; } - /** - * 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: (ts.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 = ts.append(yieldTypes, iterationTypes.yieldType); - returnTypes = ts.append(returnTypes, iterationTypes.returnType); - nextTypes = ts.append(nextTypes, iterationTypes.nextType); - } - if (yieldTypes || returnTypes || nextTypes) { - return createIterationTypes(yieldTypes && getUnionType(yieldTypes), returnTypes && getUnionType(returnTypes), nextTypes && getIntersectionType(nextTypes)); + 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): ts.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 & ts.TypeFlags.Intrinsic && + returnType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never | ts.TypeFlags.Unknown | ts.TypeFlags.Void | ts.TypeFlags.Undefined) && + nextType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never | ts.TypeFlags.Unknown | ts.TypeFlags.Void | ts.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: (ts.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; } - return noIterationTypes; + yieldTypes = ts.append(yieldTypes, iterationTypes.yieldType); + returnTypes = ts.append(returnTypes, iterationTypes.returnType); + nextTypes = ts.append(nextTypes, iterationTypes.nextType); } - - function getCachedIterationTypes(type: ts.Type, cacheKey: ts.MatchingKeys) { - return (type as ts.IterableOrIteratorType)[cacheKey]; + if (yieldTypes || returnTypes || nextTypes) { + return createIterationTypes(yieldTypes && getUnionType(yieldTypes), returnTypes && getUnionType(returnTypes), nextTypes && getIntersectionType(nextTypes)); } + return noIterationTypes; + } - function setCachedIterationTypes(type: ts.Type, cacheKey: ts.MatchingKeys, cachedTypes: ts.IterationTypes) { - return (type as ts.IterableOrIteratorType)[cacheKey] = cachedTypes; - } + function getCachedIterationTypes(type: ts.Type, cacheKey: ts.MatchingKeys) { + return (type as ts.IterableOrIteratorType)[cacheKey]; + } - /** - * 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: ts.Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; + function setCachedIterationTypes(type: ts.Type, cacheKey: ts.MatchingKeys, cachedTypes: ts.IterationTypes) { + return (type as ts.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: ts.Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + + if (!(type.flags & ts.TypeFlags.Union)) { + const iterationTypes = getIterationTypesOfIterableWorker(type, use, errorNode); + if (iterationTypes === noIterationTypes) { + if (errorNode) { + reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); + } + return undefined; } + return iterationTypes; + } - if (!(type.flags & ts.TypeFlags.Union)) { - const iterationTypes = getIterationTypesOfIterableWorker(type, use, errorNode); - if (iterationTypes === noIterationTypes) { - if (errorNode) { - reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); - } - return undefined; + const cacheKey = use & IterationUse.AllowsAsyncIterablesFlag ? "iterationTypesOfAsyncIterable" : "iterationTypesOfIterable"; + const cachedTypes = getCachedIterationTypes(type, cacheKey); + if (cachedTypes) + return cachedTypes === noIterationTypes ? undefined : cachedTypes; + let allIterationTypes: ts.IterationTypes[] | undefined; + for (const constituent of (type as ts.UnionType).types) { + const iterationTypes = getIterationTypesOfIterableWorker(constituent, use, errorNode); + if (iterationTypes === noIterationTypes) { + if (errorNode) { + reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); } - return iterationTypes; + setCachedIterationTypes(type, cacheKey, noIterationTypes); + return undefined; + } + else { + allIterationTypes = ts.append(allIterationTypes, iterationTypes); } + } - const cacheKey = use & IterationUse.AllowsAsyncIterablesFlag ? "iterationTypesOfAsyncIterable" : "iterationTypesOfIterable"; - const cachedTypes = getCachedIterationTypes(type, cacheKey); - if (cachedTypes) - return cachedTypes === noIterationTypes ? undefined : cachedTypes; - let allIterationTypes: ts.IterationTypes[] | undefined; - for (const constituent of (type as ts.UnionType).types) { - const iterationTypes = getIterationTypesOfIterableWorker(constituent, use, errorNode); - if (iterationTypes === noIterationTypes) { - if (errorNode) { - reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); + const iterationTypes = allIterationTypes ? combineIterationTypes(allIterationTypes) : noIterationTypes; + setCachedIterationTypes(type, cacheKey, iterationTypes); + return iterationTypes === noIterationTypes ? undefined : iterationTypes; + } + + function getAsyncFromSyncIterationTypes(iterationTypes: ts.IterationTypes, errorNode: ts.Node | undefined) { + if (iterationTypes === noIterationTypes) + return noIterationTypes; + if (iterationTypes === anyIterationTypes) + return anyIterationTypes; + const { yieldType, returnType, nextType } = iterationTypes; + // if we're requesting diagnostics, report errors for a missing `Awaited`. + if (errorNode) { + getGlobalAwaitedSymbol(/*reportErrors*/ true); + } + 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: ts.Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + + if (use & IterationUse.AllowsAsyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableCached(type, asyncIterationTypesResolver) || + getIterationTypesOfIterableFast(type, asyncIterationTypesResolver); + if (iterationTypes) { + return use & IterationUse.ForOfFlag ? + getAsyncFromSyncIterationTypes(iterationTypes, errorNode) : + 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)); } - setCachedIterationTypes(type, cacheKey, noIterationTypes); - return undefined; } else { - allIterationTypes = ts.append(allIterationTypes, iterationTypes); + return iterationTypes; } } - - const iterationTypes = allIterationTypes ? combineIterationTypes(allIterationTypes) : noIterationTypes; - setCachedIterationTypes(type, cacheKey, iterationTypes); - return iterationTypes === noIterationTypes ? undefined : iterationTypes; } - function getAsyncFromSyncIterationTypes(iterationTypes: ts.IterationTypes, errorNode: ts.Node | undefined) { - if (iterationTypes === noIterationTypes) - return noIterationTypes; - if (iterationTypes === anyIterationTypes) - return anyIterationTypes; - const { yieldType, returnType, nextType } = iterationTypes; - // if we're requesting diagnostics, report errors for a missing `Awaited`. - if (errorNode) { - getGlobalAwaitedSymbol(/*reportErrors*/ true); + if (use & IterationUse.AllowsAsyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableSlow(type, asyncIterationTypesResolver, errorNode); + if (iterationTypes !== noIterationTypes) { + return 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: ts.Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; - } - - if (use & IterationUse.AllowsAsyncIterablesFlag) { - const iterationTypes = getIterationTypesOfIterableCached(type, asyncIterationTypesResolver) || - getIterationTypesOfIterableFast(type, asyncIterationTypesResolver); - if (iterationTypes) { - return use & IterationUse.ForOfFlag ? - getAsyncFromSyncIterationTypes(iterationTypes, errorNode) : - 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)); - } - } - else { - return iterationTypes; - } + 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 (use & IterationUse.AllowsAsyncIterablesFlag) { - const iterationTypes = getIterationTypesOfIterableSlow(type, asyncIterationTypesResolver, errorNode); - if (iterationTypes !== noIterationTypes) { + else { return iterationTypes; } } + } + + 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); + } - 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; - } - } - } + function getIterationTypesOfGlobalIterableType(globalType: ts.Type, resolver: IterationTypesResolver) { + const globalIterationTypes = getIterationTypesOfIterableCached(globalType, resolver) || + getIterationTypesOfIterableSlow(globalType, resolver, /*errorNode*/ undefined); + return globalIterationTypes === noIterationTypes ? defaultIterationTypes : globalIterationTypes; + } - return noIterationTypes; + /** + * 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 ts.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(resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || yieldType, resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || 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 ts.GenericType); + return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || yieldType, resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || returnType, nextType)); } + } - /** - * 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 getPropertyNameForKnownSymbolName(symbolName: string): ts.__String { + const ctorType = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); + const uniqueType = ctorType && getTypeOfPropertyOfType(getTypeOfSymbol(ctorType), ts.escapeLeadingUnderscores(symbolName)); + return uniqueType && isTypeUsableAsPropertyName(uniqueType) ? getPropertyNameFromType(uniqueType) : `__@${symbolName}` as ts.__String; + } - 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 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: ts.Node | undefined) { + const method = getPropertyOfType(type, getPropertyNameForKnownSymbolName(resolver.iteratorSymbolName)); + const methodType = method && !(method.flags & ts.SymbolFlags.Optional) ? getTypeOfSymbol(method) : undefined; + if (isTypeAny(methodType)) { + return setCachedIterationTypes(type, resolver.iterableCacheKey, anyIterationTypes); + } + + const signatures = methodType ? getSignaturesOfType(methodType, ts.SignatureKind.Call) : undefined; + if (!ts.some(signatures)) { + return setCachedIterationTypes(type, resolver.iterableCacheKey, noIterationTypes); + } + + const iteratorType = getIntersectionType(ts.map(signatures, getReturnTypeOfSignature)); + const iterationTypes = getIterationTypesOfIterator(iteratorType, resolver, errorNode) ?? noIterationTypes; + return setCachedIterationTypes(type, resolver.iterableCacheKey, iterationTypes); + } - /** - * 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 ts.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(resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || yieldType, resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || 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 ts.GenericType); - return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || yieldType, resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || returnType, nextType)); - } - } - - function getPropertyNameForKnownSymbolName(symbolName: string): ts.__String { - const ctorType = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); - const uniqueType = ctorType && getTypeOfPropertyOfType(getTypeOfSymbol(ctorType), ts.escapeLeadingUnderscores(symbolName)); - return uniqueType && isTypeUsableAsPropertyName(uniqueType) ? getPropertyNameFromType(uniqueType) : `__@${symbolName}` as ts.__String; - } + function reportTypeNotIterableError(errorNode: ts.Node, type: ts.Type, allowAsyncIterables: boolean): void { + const message = allowAsyncIterables + ? ts.Diagnostics.Type_0_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator + : ts.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 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: ts.Node | undefined) { - const method = getPropertyOfType(type, getPropertyNameForKnownSymbolName(resolver.iteratorSymbolName)); - const methodType = method && !(method.flags & ts.SymbolFlags.Optional) ? getTypeOfSymbol(method) : undefined; - if (isTypeAny(methodType)) { - return setCachedIterationTypes(type, resolver.iterableCacheKey, anyIterationTypes); - } + /** + * 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: ts.Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + + const iterationTypes = getIterationTypesOfIteratorCached(type, resolver) || + getIterationTypesOfIteratorFast(type, resolver) || + getIterationTypesOfIteratorSlow(type, resolver, errorNode); + return iterationTypes === noIterationTypes ? undefined : iterationTypes; + } - const signatures = methodType ? getSignaturesOfType(methodType, ts.SignatureKind.Call) : undefined; - if (!ts.some(signatures)) { - return setCachedIterationTypes(type, resolver.iterableCacheKey, noIterationTypes); - } + /** + * 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); + } - const iteratorType = getIntersectionType(ts.map(signatures, getReturnTypeOfSignature)); - const iterationTypes = getIterationTypesOfIterator(iteratorType, resolver, errorNode) ?? noIterationTypes; - return setCachedIterationTypes(type, resolver.iterableCacheKey, iterationTypes); + /** + * 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 ts.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 ts.GenericType); + return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); } + } - function reportTypeNotIterableError(errorNode: ts.Node, type: ts.Type, allowAsyncIterables: boolean): void { - const message = allowAsyncIterables - ? ts.Diagnostics.Type_0_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator - : ts.Diagnostics.Type_0_must_have_a_Symbol_iterator_method_that_returns_an_iterator; - errorAndMaybeSuggestAwait(errorNode, !!getAwaitedTypeOfPromise(type), message, typeToString(type)); - } + 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 ts.__String) || falseType; + return isTypeAssignableTo(kind === IterationTypeKind.Yield ? falseType : trueType, doneType); + } - /** - * 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: ts.Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; - } + function isYieldIteratorResult(type: ts.Type) { + return isIteratorResult(type, IterationTypeKind.Yield); + } + + function isReturnIteratorResult(type: ts.Type) { + return isIteratorResult(type, IterationTypeKind.Return); + } - const iterationTypes = getIterationTypesOfIteratorCached(type, resolver) || - getIterationTypesOfIteratorFast(type, resolver) || - getIterationTypesOfIteratorSlow(type, resolver, errorNode); - return iterationTypes === noIterationTypes ? undefined : iterationTypes; + /** + * 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; } - /** - * 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); + const cachedTypes = getCachedIterationTypes(type, "iterationTypesOfIteratorResult"); + if (cachedTypes) { + return cachedTypes; } - /** - * 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 ts.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 ts.GenericType); - return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); - } + // 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 ts.GenericType)[0]; + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, /*returnType*/ undefined, /*nextType*/ undefined)); } + if (isReferenceToType(type, getGlobalIteratorReturnResultType(/*reportErrors*/ false))) { + const returnType = getTypeArguments(type as ts.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 ts.__String) : undefined; - 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 ts.__String) || falseType; - return isTypeAssignableTo(kind === IterationTypeKind.Yield ? falseType : trueType, doneType); + const returnIteratorResult = filterType(type, isReturnIteratorResult); + const returnType = returnIteratorResult !== neverType ? getTypeOfPropertyOfType(returnIteratorResult, "value" as ts.__String) : undefined; + + if (!yieldType && !returnType) { + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", noIterationTypes); } - function isYieldIteratorResult(type: ts.Type) { - return isIteratorResult(type, IterationTypeKind.Yield); + // 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: ts.Node | undefined): ts.IterationTypes | undefined { + const method = getPropertyOfType(type, methodName as ts.__String); + + // Ignore 'return' or 'throw' if they are missing. + if (!method && methodName !== "next") { + return undefined; } - function isReturnIteratorResult(type: ts.Type) { - return isIteratorResult(type, IterationTypeKind.Return); + const methodType = method && !(methodName === "next" && (method.flags & ts.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; } - /** - * 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; + // Both async and non-async iterators *must* have a `next` method. + const methodSignatures = methodType ? getSignaturesOfType(methodType, ts.SignatureKind.Call) : ts.emptyArray; + if (methodSignatures.length === 0) { + if (errorNode) { + const diagnostic = methodName === "next" + ? resolver.mustHaveANextMethodDiagnostic + : resolver.mustBeAMethodDiagnostic; + error(errorNode, diagnostic, methodName); + } + return methodName === "next" ? anyIterationTypes : undefined; + } + + // If the method signature comes exclusively from the global iterator or generator type, + // create iteration types from its type arguments like `getIterationTypesOfIteratorFast` + // does (so as to remove `undefined` from the next and return types). We arrive here when + // a contextual type for a generator was not a direct reference to one of those global types, + // but looking up `methodType` referred to one of them (and nothing else). E.g., in + // `interface SpecialIterator extends Iterator {}`, `SpecialIterator` is not a + // reference to `Iterator`, but its `next` member derives exclusively from `Iterator`. + if (methodType?.symbol && methodSignatures.length === 1) { + const globalGeneratorType = resolver.getGlobalGeneratorType(/*reportErrors*/ false); + const globalIteratorType = resolver.getGlobalIteratorType(/*reportErrors*/ false); + const isGeneratorMethod = globalGeneratorType.symbol?.members?.get(methodName as ts.__String) === methodType.symbol; + const isIteratorMethod = !isGeneratorMethod && globalIteratorType.symbol?.members?.get(methodName as ts.__String) === methodType.symbol; + if (isGeneratorMethod || isIteratorMethod) { + const globalType = isGeneratorMethod ? globalGeneratorType : globalIteratorType; + const { mapper } = methodType as ts.AnonymousType; + return createIterationTypes(getMappedType(globalType.typeParameters![0], mapper!), getMappedType(globalType.typeParameters![1], mapper!), methodName === "next" ? getMappedType(globalType.typeParameters![2], mapper!) : undefined); } + } - const cachedTypes = getCachedIterationTypes(type, "iterationTypesOfIteratorResult"); - if (cachedTypes) { - return cachedTypes; + // 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" && ts.some(signature.parameters)) { + methodParameterTypes = ts.append(methodParameterTypes, getTypeAtPosition(signature, 0)); } + methodReturnTypes = ts.append(methodReturnTypes, getReturnTypeOfSignature(signature)); + } - // 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 ts.GenericType)[0]; - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, /*returnType*/ undefined, /*nextType*/ undefined)); + // 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; } - if (isReferenceToType(type, getGlobalIteratorReturnResultType(/*reportErrors*/ false))) { - const returnType = getTypeArguments(type as ts.GenericType)[0]; - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(/*yieldType*/ undefined, returnType, /*nextType*/ undefined)); + else if (methodName === "return") { + // The value of `return(value)` *is* awaited by async generators + const resolvedMethodParameterType = resolver.resolveIterationType(methodParameterType, errorNode) || anyType; + returnTypes = ts.append(returnTypes, resolvedMethodParameterType); } + } - // Choose any constituents that can produce the requested iteration type. - const yieldIteratorResult = filterType(type, isYieldIteratorResult); - const yieldType = yieldIteratorResult !== neverType ? getTypeOfPropertyOfType(yieldIteratorResult, "value" as ts.__String) : undefined; + // Resolve the *yield* and *return* types from the return type of the method (i.e. `IteratorResult`) + let yieldType: ts.Type; + const methodReturnType = methodReturnTypes ? getIntersectionType(methodReturnTypes) : 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 = ts.append(returnTypes, anyType); + } + else { + yieldType = iterationTypes.yieldType; + returnTypes = ts.append(returnTypes, iterationTypes.returnType); + } - const returnIteratorResult = filterType(type, isReturnIteratorResult); - const returnType = returnIteratorResult !== neverType ? getTypeOfPropertyOfType(returnIteratorResult, "value" as ts.__String) : undefined; + return createIterationTypes(yieldType, getUnionType(returnTypes), nextType); + } - if (!yieldType && !returnType) { - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", noIterationTypes); - } + /** + * 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: ts.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); + } - // 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 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; } - /** - * 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: ts.Node | undefined): ts.IterationTypes | undefined { - const method = getPropertyOfType(type, methodName as ts.__String); + const iterationTypes = getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsyncGenerator); + return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(kind)]; + } - // Ignore 'return' or 'throw' if they are missing. - if (!method && methodName !== "next") { - return undefined; - } + function getIterationTypesOfGeneratorFunctionReturnType(type: ts.Type, isAsyncGenerator: boolean) { + if (isTypeAny(type)) { + return anyIterationTypes; + } - const methodType = method && !(methodName === "next" && (method.flags & ts.SymbolFlags.Optional)) - ? methodName === "next" ? getTypeOfSymbol(method) : getTypeWithFacts(getTypeOfSymbol(method), TypeFacts.NEUndefinedOrNull) - : undefined; + const use = isAsyncGenerator ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; + const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; + return getIterationTypesOfIterable(type, use, /*errorNode*/ undefined) || + getIterationTypesOfIterator(type, resolver, /*errorNode*/ undefined); + } - if (isTypeAny(methodType)) { - // `return()` and `throw()` don't provide a *next* type. - return methodName === "next" ? anyIterationTypes : anyIterationTypesExceptNext; - } + function checkBreakOrContinueStatement(node: ts.BreakOrContinueStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) + checkGrammarBreakOrContinueStatement(node); - // Both async and non-async iterators *must* have a `next` method. - const methodSignatures = methodType ? getSignaturesOfType(methodType, ts.SignatureKind.Call) : ts.emptyArray; - if (methodSignatures.length === 0) { - if (errorNode) { - const diagnostic = methodName === "next" - ? resolver.mustHaveANextMethodDiagnostic - : resolver.mustBeAMethodDiagnostic; - error(errorNode, diagnostic, methodName); - } - return methodName === "next" ? anyIterationTypes : undefined; - } - - // If the method signature comes exclusively from the global iterator or generator type, - // create iteration types from its type arguments like `getIterationTypesOfIteratorFast` - // does (so as to remove `undefined` from the next and return types). We arrive here when - // a contextual type for a generator was not a direct reference to one of those global types, - // but looking up `methodType` referred to one of them (and nothing else). E.g., in - // `interface SpecialIterator extends Iterator {}`, `SpecialIterator` is not a - // reference to `Iterator`, but its `next` member derives exclusively from `Iterator`. - if (methodType?.symbol && methodSignatures.length === 1) { - const globalGeneratorType = resolver.getGlobalGeneratorType(/*reportErrors*/ false); - const globalIteratorType = resolver.getGlobalIteratorType(/*reportErrors*/ false); - const isGeneratorMethod = globalGeneratorType.symbol?.members?.get(methodName as ts.__String) === methodType.symbol; - const isIteratorMethod = !isGeneratorMethod && globalIteratorType.symbol?.members?.get(methodName as ts.__String) === methodType.symbol; - if (isGeneratorMethod || isIteratorMethod) { - const globalType = isGeneratorMethod ? globalGeneratorType : globalIteratorType; - const { mapper } = methodType as ts.AnonymousType; - return createIterationTypes(getMappedType(globalType.typeParameters![0], mapper!), getMappedType(globalType.typeParameters![1], mapper!), methodName === "next" ? getMappedType(globalType.typeParameters![2], mapper!) : 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" && ts.some(signature.parameters)) { - methodParameterTypes = ts.append(methodParameterTypes, getTypeAtPosition(signature, 0)); - } - methodReturnTypes = ts.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 = ts.append(returnTypes, resolvedMethodParameterType); - } - } + // TODO: Check that target label is valid + } - // Resolve the *yield* and *return* types from the return type of the method (i.e. `IteratorResult`) - let yieldType: ts.Type; - const methodReturnType = methodReturnTypes ? getIntersectionType(methodReturnTypes) : 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 = ts.append(returnTypes, anyType); - } - else { - yieldType = iterationTypes.yieldType; - returnTypes = ts.append(returnTypes, iterationTypes.returnType); - } + function unwrapReturnType(returnType: ts.Type, functionFlags: ts.FunctionFlags) { + const isGenerator = !!(functionFlags & ts.FunctionFlags.Generator); + const isAsync = !!(functionFlags & ts.FunctionFlags.Async); + return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) || errorType : + isAsync ? getAwaitedTypeNoAlias(returnType) || errorType : + returnType; + } - return createIterationTypes(yieldType, getUnionType(returnTypes), nextType); - } + function isUnwrappedReturnTypeVoidOrAny(func: ts.SignatureDeclaration, returnType: ts.Type): boolean { + const unwrappedReturnType = unwrapReturnType(returnType, ts.getFunctionFlags(func)); + return !!unwrappedReturnType && maybeTypeOfKind(unwrappedReturnType, ts.TypeFlags.Void | ts.TypeFlags.AnyOrUnknown); + } - /** - * 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: ts.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); + function checkReturnStatement(node: ts.ReturnStatement) { + // Grammar checking + if (checkGrammarStatementInAmbientContext(node)) { + return; } - /** - * 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 container = ts.getContainingFunctionOrClassStaticBlock(node); + if (container && ts.isClassStaticBlockDeclaration(container)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.A_return_statement_cannot_be_used_inside_a_class_static_block); + return; + } - const iterationTypes = getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsyncGenerator); - return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(kind)]; + if (!container) { + grammarErrorOnFirstToken(node, ts.Diagnostics.A_return_statement_can_only_be_used_within_a_function_body); + return; } - function getIterationTypesOfGeneratorFunctionReturnType(type: ts.Type, isAsyncGenerator: boolean) { - if (isTypeAny(type)) { - return anyIterationTypes; + const signature = getSignatureFromDeclaration(container); + const returnType = getReturnTypeOfSignature(signature); + const functionFlags = ts.getFunctionFlags(container); + if (strictNullChecks || node.expression || returnType.flags & ts.TypeFlags.Never) { + const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType; + if (container.kind === ts.SyntaxKind.SetAccessor) { + if (node.expression) { + error(node, ts.Diagnostics.Setters_cannot_return_a_value); + } + } + else if (container.kind === ts.SyntaxKind.Constructor) { + if (node.expression && !checkTypeAssignableToAndOptionallyElaborate(exprType, returnType, node, node.expression)) { + error(node, ts.Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class); + } + } + else if (getReturnTypeFromAnnotation(container)) { + const unwrappedReturnType = unwrapReturnType(returnType, functionFlags) ?? returnType; + const unwrappedExprType = functionFlags & ts.FunctionFlags.Async + ? checkAwaitedType(exprType, /*withAlias*/ false, node, ts.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); + } } + } + else if (container.kind !== ts.SyntaxKind.Constructor && compilerOptions.noImplicitReturns && !isUnwrappedReturnTypeVoidOrAny(container, returnType)) { + // The function has a return type, but the return statement doesn't have an expression. + error(node, ts.Diagnostics.Not_all_code_paths_return_a_value); + } + } - const use = isAsyncGenerator ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; - const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; - return getIterationTypesOfIterable(type, use, /*errorNode*/ undefined) || - getIterationTypesOfIterator(type, resolver, /*errorNode*/ undefined); + function checkWithStatement(node: ts.WithStatement) { + // Grammar checking for withStatement + if (!checkGrammarStatementInAmbientContext(node)) { + if (node.flags & ts.NodeFlags.AwaitContext) { + grammarErrorOnFirstToken(node, ts.Diagnostics.with_statements_are_not_allowed_in_an_async_function_block); + } } - function checkBreakOrContinueStatement(node: ts.BreakOrContinueStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) - checkGrammarBreakOrContinueStatement(node); + checkExpression(node.expression); - // TODO: Check that target label is valid + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const start = ts.getSpanOfTokenAtPosition(sourceFile, node.pos).start; + const end = node.statement.pos; + grammarErrorAtPos(sourceFile, start, end - start, ts.Diagnostics.The_with_statement_is_not_supported_All_symbols_in_a_with_block_will_have_type_any); } + } - function unwrapReturnType(returnType: ts.Type, functionFlags: ts.FunctionFlags) { - const isGenerator = !!(functionFlags & ts.FunctionFlags.Generator); - const isAsync = !!(functionFlags & ts.FunctionFlags.Async); - return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) || errorType : - isAsync ? getAwaitedTypeNoAlias(returnType) || errorType : - returnType; - } + function checkSwitchStatement(node: ts.SwitchStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); - function isUnwrappedReturnTypeVoidOrAny(func: ts.SignatureDeclaration, returnType: ts.Type): boolean { - const unwrappedReturnType = unwrapReturnType(returnType, ts.getFunctionFlags(func)); - return !!unwrappedReturnType && maybeTypeOfKind(unwrappedReturnType, ts.TypeFlags.Void | ts.TypeFlags.AnyOrUnknown); - } + let firstDefaultClause: ts.CaseOrDefaultClause; + let hasDuplicateDefaultClause = false; - function checkReturnStatement(node: ts.ReturnStatement) { - // Grammar checking - if (checkGrammarStatementInAmbientContext(node)) { - return; + const expressionType = checkExpression(node.expression); + const expressionIsLiteral = isLiteralType(expressionType); + ts.forEach(node.caseBlock.clauses, clause => { + // Grammar check for duplicate default clauses, skip if we already report duplicate default clause + if (clause.kind === ts.SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) { + if (firstDefaultClause === undefined) { + firstDefaultClause = clause; + } + else { + grammarErrorOnNode(clause, ts.Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement); + hasDuplicateDefaultClause = true; + } } - const container = ts.getContainingFunctionOrClassStaticBlock(node); - if (container && ts.isClassStaticBlockDeclaration(container)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.A_return_statement_cannot_be_used_inside_a_class_static_block); - return; + if (clause.kind === ts.SyntaxKind.CaseClause) { + addLazyDiagnostic(createLazyCaseClauseDiagnostics(clause)); } - - if (!container) { - grammarErrorOnFirstToken(node, ts.Diagnostics.A_return_statement_can_only_be_used_within_a_function_body); - return; + ts.forEach(clause.statements, checkSourceElement); + if (compilerOptions.noFallthroughCasesInSwitch && clause.fallthroughFlowNode && isReachableFlowNode(clause.fallthroughFlowNode)) { + error(clause, ts.Diagnostics.Fallthrough_case_in_switch); } - const signature = getSignatureFromDeclaration(container); - const returnType = getReturnTypeOfSignature(signature); - const functionFlags = ts.getFunctionFlags(container); - if (strictNullChecks || node.expression || returnType.flags & ts.TypeFlags.Never) { - const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType; - if (container.kind === ts.SyntaxKind.SetAccessor) { - if (node.expression) { - error(node, ts.Diagnostics.Setters_cannot_return_a_value); - } - } - else if (container.kind === ts.SyntaxKind.Constructor) { - if (node.expression && !checkTypeAssignableToAndOptionallyElaborate(exprType, returnType, node, node.expression)) { - error(node, ts.Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class); - } - } - else if (getReturnTypeFromAnnotation(container)) { - const unwrappedReturnType = unwrapReturnType(returnType, functionFlags) ?? returnType; - const unwrappedExprType = functionFlags & ts.FunctionFlags.Async - ? checkAwaitedType(exprType, /*withAlias*/ false, node, ts.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 createLazyCaseClauseDiagnostics(clause: ts.CaseClause) { + return () => { + // 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); } - } - } - else if (container.kind !== ts.SyntaxKind.Constructor && compilerOptions.noImplicitReturns && !isUnwrappedReturnTypeVoidOrAny(container, returnType)) { - // The function has a return type, but the return statement doesn't have an expression. - error(node, ts.Diagnostics.Not_all_code_paths_return_a_value); + }; } + }); + if (node.caseBlock.locals) { + registerForUnusedIdentifiersCheck(node.caseBlock); } + } - function checkWithStatement(node: ts.WithStatement) { - // Grammar checking for withStatement - if (!checkGrammarStatementInAmbientContext(node)) { - if (node.flags & ts.NodeFlags.AwaitContext) { - grammarErrorOnFirstToken(node, ts.Diagnostics.with_statements_are_not_allowed_in_an_async_function_block); + function checkLabeledStatement(node: ts.LabeledStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + ts.findAncestor(node.parent, current => { + if (ts.isFunctionLike(current)) { + return "quit"; } - } + if (current.kind === ts.SyntaxKind.LabeledStatement && (current as ts.LabeledStatement).label.escapedText === node.label.escapedText) { + grammarErrorOnNode(node.label, ts.Diagnostics.Duplicate_label_0, ts.getTextOfNode(node.label)); + return true; + } + return false; + }); + } - checkExpression(node.expression); + // ensure that label is unique + checkSourceElement(node.statement); + } - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const start = ts.getSpanOfTokenAtPosition(sourceFile, node.pos).start; - const end = node.statement.pos; - grammarErrorAtPos(sourceFile, start, end - start, ts.Diagnostics.The_with_statement_is_not_supported_All_symbols_in_a_with_block_will_have_type_any); + function checkThrowStatement(node: ts.ThrowStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + if (ts.isIdentifier(node.expression) && !node.expression.escapedText) { + grammarErrorAfterFirstToken(node, ts.Diagnostics.Line_break_not_permitted_here); } } - function checkSwitchStatement(node: ts.SwitchStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); + if (node.expression) { + checkExpression(node.expression); + } + } - let firstDefaultClause: ts.CaseOrDefaultClause; - let hasDuplicateDefaultClause = false; + function checkTryStatement(node: ts.TryStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); - const expressionType = checkExpression(node.expression); - const expressionIsLiteral = isLiteralType(expressionType); - ts.forEach(node.caseBlock.clauses, clause => { - // Grammar check for duplicate default clauses, skip if we already report duplicate default clause - if (clause.kind === ts.SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) { - if (firstDefaultClause === undefined) { - firstDefaultClause = clause; - } - else { - grammarErrorOnNode(clause, ts.Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement); - hasDuplicateDefaultClause = true; + checkBlock(node.tryBlock); + const catchClause = node.catchClause; + if (catchClause) { + // Grammar checking + if (catchClause.variableDeclaration) { + const declaration = catchClause.variableDeclaration; + const typeNode = ts.getEffectiveTypeAnnotationNode(ts.getRootDeclaration(declaration)); + if (typeNode) { + const type = getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ false, CheckMode.Normal); + if (type && !(type.flags & ts.TypeFlags.AnyOrUnknown)) { + grammarErrorOnFirstToken(typeNode, ts.Diagnostics.Catch_clause_variable_type_annotation_must_be_any_or_unknown_if_specified); } } - - if (clause.kind === ts.SyntaxKind.CaseClause) { - addLazyDiagnostic(createLazyCaseClauseDiagnostics(clause)); - } - ts.forEach(clause.statements, checkSourceElement); - if (compilerOptions.noFallthroughCasesInSwitch && clause.fallthroughFlowNode && isReachableFlowNode(clause.fallthroughFlowNode)) { - error(clause, ts.Diagnostics.Fallthrough_case_in_switch); + else if (declaration.initializer) { + grammarErrorOnFirstToken(declaration.initializer, ts.Diagnostics.Catch_clause_variable_cannot_have_an_initializer); } - - function createLazyCaseClauseDiagnostics(clause: ts.CaseClause) { - return () => { - // 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); - } - }; + else { + const blockLocals = catchClause.block.locals; + if (blockLocals) { + ts.forEachKey(catchClause.locals!, caughtName => { + const blockLocal = blockLocals.get(caughtName); + if (blockLocal?.valueDeclaration && (blockLocal.flags & ts.SymbolFlags.BlockScopedVariable) !== 0) { + grammarErrorOnNode(blockLocal.valueDeclaration, ts.Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause, caughtName); + } + }); + } } - }); - if (node.caseBlock.locals) { - registerForUnusedIdentifiersCheck(node.caseBlock); } - } - function checkLabeledStatement(node: ts.LabeledStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - ts.findAncestor(node.parent, current => { - if (ts.isFunctionLike(current)) { - return "quit"; - } - if (current.kind === ts.SyntaxKind.LabeledStatement && (current as ts.LabeledStatement).label.escapedText === node.label.escapedText) { - grammarErrorOnNode(node.label, ts.Diagnostics.Duplicate_label_0, ts.getTextOfNode(node.label)); - return true; - } - return false; - }); - } + checkBlock(catchClause.block); + } - // ensure that label is unique - checkSourceElement(node.statement); + if (node.finallyBlock) { + checkBlock(node.finallyBlock); } + } - function checkThrowStatement(node: ts.ThrowStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - if (ts.isIdentifier(node.expression) && !node.expression.escapedText) { - grammarErrorAfterFirstToken(node, ts.Diagnostics.Line_break_not_permitted_here); + function checkIndexConstraints(type: ts.Type, symbol: ts.Symbol, isStaticIndex?: boolean) { + const indexInfos = getIndexInfosOfType(type); + if (indexInfos.length === 0) { + return; + } + for (const prop of getPropertiesOfObjectType(type)) { + if (!(isStaticIndex && prop.flags & ts.SymbolFlags.Prototype)) { + checkIndexConstraintForProperty(type, prop, getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique, /*includeNonPublic*/ true), getNonMissingTypeOfSymbol(prop)); + } + } + const typeDeclaration = symbol.valueDeclaration; + if (typeDeclaration && ts.isClassLike(typeDeclaration)) { + for (const member of typeDeclaration.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 (!ts.isStatic(member) && !hasBindableName(member)) { + const symbol = getSymbolOfNode(member); + checkIndexConstraintForProperty(type, symbol, getTypeOfExpression((member as ts.DynamicNamedDeclaration).name.expression), getNonMissingTypeOfSymbol(symbol)); } } - - if (node.expression) { - checkExpression(node.expression); + } + if (indexInfos.length > 1) { + for (const info of indexInfos) { + checkIndexConstraintForIndexSignature(type, info); } } + } - function checkTryStatement(node: ts.TryStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - checkBlock(node.tryBlock); - const catchClause = node.catchClause; - if (catchClause) { - // Grammar checking - if (catchClause.variableDeclaration) { - const declaration = catchClause.variableDeclaration; - const typeNode = ts.getEffectiveTypeAnnotationNode(ts.getRootDeclaration(declaration)); - if (typeNode) { - const type = getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ false, CheckMode.Normal); - if (type && !(type.flags & ts.TypeFlags.AnyOrUnknown)) { - grammarErrorOnFirstToken(typeNode, ts.Diagnostics.Catch_clause_variable_type_annotation_must_be_any_or_unknown_if_specified); - } - } - else if (declaration.initializer) { - grammarErrorOnFirstToken(declaration.initializer, ts.Diagnostics.Catch_clause_variable_cannot_have_an_initializer); - } - else { - const blockLocals = catchClause.block.locals; - if (blockLocals) { - ts.forEachKey(catchClause.locals!, caughtName => { - const blockLocal = blockLocals.get(caughtName); - if (blockLocal?.valueDeclaration && (blockLocal.flags & ts.SymbolFlags.BlockScopedVariable) !== 0) { - grammarErrorOnNode(blockLocal.valueDeclaration, ts.Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause, caughtName); - } - }); - } - } - } - - checkBlock(catchClause.block); + function checkIndexConstraintForProperty(type: ts.Type, prop: ts.Symbol, propNameType: ts.Type, propType: ts.Type) { + const declaration = prop.valueDeclaration; + const name = ts.getNameOfDeclaration(declaration); + if (name && ts.isPrivateIdentifier(name)) { + return; + } + const indexInfos = getApplicableIndexInfos(type, propNameType); + const interfaceDeclaration = ts.getObjectFlags(type) & ts.ObjectFlags.Interface ? ts.getDeclarationOfKind(type.symbol, ts.SyntaxKind.InterfaceDeclaration) : undefined; + const localPropDeclaration = declaration && declaration.kind === ts.SyntaxKind.BinaryExpression || + name && name.kind === ts.SyntaxKind.ComputedPropertyName || getParentOfSymbol(prop) === type.symbol ? declaration : undefined; + for (const info of indexInfos) { + const localIndexDeclaration = info.declaration && getParentOfSymbol(getSymbolOfNode(info.declaration)) === type.symbol ? info.declaration : undefined; + // We check only when (a) the property is declared in the containing type, or (b) the applicable index signature is declared + // in the containing type, or (c) the containing type is an interface and no base interface contains both the property and + // the index signature (i.e. property and index signature are declared in separate inherited interfaces). + const errorNode = localPropDeclaration || localIndexDeclaration || + (interfaceDeclaration && !ts.some(getBaseTypes(type as ts.InterfaceType), base => !!getPropertyOfObjectType(base, prop.escapedName) && !!getIndexTypeOfType(base, info.keyType)) ? interfaceDeclaration : undefined); + if (errorNode && !isTypeAssignableTo(propType, info.type)) { + error(errorNode, ts.Diagnostics.Property_0_of_type_1_is_not_assignable_to_2_index_type_3, symbolToString(prop), typeToString(propType), typeToString(info.keyType), typeToString(info.type)); } + } + } - if (node.finallyBlock) { - checkBlock(node.finallyBlock); + function checkIndexConstraintForIndexSignature(type: ts.Type, checkInfo: ts.IndexInfo) { + const declaration = checkInfo.declaration; + const indexInfos = getApplicableIndexInfos(type, checkInfo.keyType); + const interfaceDeclaration = ts.getObjectFlags(type) & ts.ObjectFlags.Interface ? ts.getDeclarationOfKind(type.symbol, ts.SyntaxKind.InterfaceDeclaration) : undefined; + const localCheckDeclaration = declaration && getParentOfSymbol(getSymbolOfNode(declaration)) === type.symbol ? declaration : undefined; + for (const info of indexInfos) { + if (info === checkInfo) + continue; + const localIndexDeclaration = info.declaration && getParentOfSymbol(getSymbolOfNode(info.declaration)) === type.symbol ? info.declaration : undefined; + // We check only when (a) the check index signature is declared in the containing type, or (b) the applicable index + // signature is declared in the containing type, or (c) the containing type is an interface and no base interface contains + // both index signatures (i.e. the index signatures are declared in separate inherited interfaces). + const errorNode = localCheckDeclaration || localIndexDeclaration || + (interfaceDeclaration && !ts.some(getBaseTypes(type as ts.InterfaceType), base => !!getIndexInfoOfType(base, checkInfo.keyType) && !!getIndexTypeOfType(base, info.keyType)) ? interfaceDeclaration : undefined); + if (errorNode && !isTypeAssignableTo(checkInfo.type, info.type)) { + error(errorNode, ts.Diagnostics._0_index_type_1_is_not_assignable_to_2_index_type_3, typeToString(checkInfo.keyType), typeToString(checkInfo.type), typeToString(info.keyType), typeToString(info.type)); } } + } - function checkIndexConstraints(type: ts.Type, symbol: ts.Symbol, isStaticIndex?: boolean) { - const indexInfos = getIndexInfosOfType(type); - if (indexInfos.length === 0) { - return; - } - for (const prop of getPropertiesOfObjectType(type)) { - if (!(isStaticIndex && prop.flags & ts.SymbolFlags.Prototype)) { - checkIndexConstraintForProperty(type, prop, getLiteralTypeFromProperty(prop, ts.TypeFlags.StringOrNumberLiteralOrUnique, /*includeNonPublic*/ true), getNonMissingTypeOfSymbol(prop)); - } - } - const typeDeclaration = symbol.valueDeclaration; - if (typeDeclaration && ts.isClassLike(typeDeclaration)) { - for (const member of typeDeclaration.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 (!ts.isStatic(member) && !hasBindableName(member)) { - const symbol = getSymbolOfNode(member); - checkIndexConstraintForProperty(type, symbol, getTypeOfExpression((member as ts.DynamicNamedDeclaration).name.expression), getNonMissingTypeOfSymbol(symbol)); - } - } - } - if (indexInfos.length > 1) { - for (const info of indexInfos) { - checkIndexConstraintForIndexSignature(type, info); - } - } + function checkTypeNameIsReserved(name: ts.Identifier, message: ts.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 "never": + case "number": + case "bigint": + case "boolean": + case "string": + case "symbol": + case "void": + case "object": + error(name, message, name.escapedText as string); } + } - function checkIndexConstraintForProperty(type: ts.Type, prop: ts.Symbol, propNameType: ts.Type, propType: ts.Type) { - const declaration = prop.valueDeclaration; - const name = ts.getNameOfDeclaration(declaration); - if (name && ts.isPrivateIdentifier(name)) { - return; - } - const indexInfos = getApplicableIndexInfos(type, propNameType); - const interfaceDeclaration = ts.getObjectFlags(type) & ts.ObjectFlags.Interface ? ts.getDeclarationOfKind(type.symbol, ts.SyntaxKind.InterfaceDeclaration) : undefined; - const localPropDeclaration = declaration && declaration.kind === ts.SyntaxKind.BinaryExpression || - name && name.kind === ts.SyntaxKind.ComputedPropertyName || getParentOfSymbol(prop) === type.symbol ? declaration : undefined; - for (const info of indexInfos) { - const localIndexDeclaration = info.declaration && getParentOfSymbol(getSymbolOfNode(info.declaration)) === type.symbol ? info.declaration : undefined; - // We check only when (a) the property is declared in the containing type, or (b) the applicable index signature is declared - // in the containing type, or (c) the containing type is an interface and no base interface contains both the property and - // the index signature (i.e. property and index signature are declared in separate inherited interfaces). - const errorNode = localPropDeclaration || localIndexDeclaration || - (interfaceDeclaration && !ts.some(getBaseTypes(type as ts.InterfaceType), base => !!getPropertyOfObjectType(base, prop.escapedName) && !!getIndexTypeOfType(base, info.keyType)) ? interfaceDeclaration : undefined); - if (errorNode && !isTypeAssignableTo(propType, info.type)) { - error(errorNode, ts.Diagnostics.Property_0_of_type_1_is_not_assignable_to_2_index_type_3, symbolToString(prop), typeToString(propType), typeToString(info.keyType), typeToString(info.type)); - } - } + /** + * The name cannot be used as 'Object' of user defined types with special target. + */ + function checkClassNameCollisionWithObject(name: ts.Identifier): void { + if (languageVersion >= ts.ScriptTarget.ES5 && name.escapedText === "Object" + && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(name).impliedNodeFormat === ts.ModuleKind.CommonJS)) { + error(name, ts.Diagnostics.Class_name_cannot_be_Object_when_targeting_ES5_with_module_0, ts.ModuleKind[moduleKind]); // https://github.com/Microsoft/TypeScript/issues/17494 } + } - function checkIndexConstraintForIndexSignature(type: ts.Type, checkInfo: ts.IndexInfo) { - const declaration = checkInfo.declaration; - const indexInfos = getApplicableIndexInfos(type, checkInfo.keyType); - const interfaceDeclaration = ts.getObjectFlags(type) & ts.ObjectFlags.Interface ? ts.getDeclarationOfKind(type.symbol, ts.SyntaxKind.InterfaceDeclaration) : undefined; - const localCheckDeclaration = declaration && getParentOfSymbol(getSymbolOfNode(declaration)) === type.symbol ? declaration : undefined; - for (const info of indexInfos) { - if (info === checkInfo) - continue; - const localIndexDeclaration = info.declaration && getParentOfSymbol(getSymbolOfNode(info.declaration)) === type.symbol ? info.declaration : undefined; - // We check only when (a) the check index signature is declared in the containing type, or (b) the applicable index - // signature is declared in the containing type, or (c) the containing type is an interface and no base interface contains - // both index signatures (i.e. the index signatures are declared in separate inherited interfaces). - const errorNode = localCheckDeclaration || localIndexDeclaration || - (interfaceDeclaration && !ts.some(getBaseTypes(type as ts.InterfaceType), base => !!getIndexInfoOfType(base, checkInfo.keyType) && !!getIndexTypeOfType(base, info.keyType)) ? interfaceDeclaration : undefined); - if (errorNode && !isTypeAssignableTo(checkInfo.type, info.type)) { - error(errorNode, ts.Diagnostics._0_index_type_1_is_not_assignable_to_2_index_type_3, typeToString(checkInfo.keyType), typeToString(checkInfo.type), typeToString(info.keyType), typeToString(info.type)); - } - } - } - - function checkTypeNameIsReserved(name: ts.Identifier, message: ts.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 "never": - case "number": - case "bigint": - case "boolean": - case "string": - case "symbol": - case "void": - case "object": - error(name, message, name.escapedText as string); + function checkUnmatchedJSDocParameters(node: ts.SignatureDeclaration) { + const jsdocParameters = ts.filter(ts.getJSDocTags(node), ts.isJSDocParameterTag); + if (!ts.length(jsdocParameters)) + return; + const isJs = ts.isInJSFile(node); + const parameters = new ts.Set(); + const excludedParameters = new ts.Set(); + ts.forEach(node.parameters, ({ name }, index) => { + if (ts.isIdentifier(name)) { + parameters.add(name.escapedText); } - } + if (ts.isBindingPattern(name)) { + excludedParameters.add(index); + } + }); - /** - * The name cannot be used as 'Object' of user defined types with special target. - */ - function checkClassNameCollisionWithObject(name: ts.Identifier): void { - if (languageVersion >= ts.ScriptTarget.ES5 && name.escapedText === "Object" - && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(name).impliedNodeFormat === ts.ModuleKind.CommonJS)) { - error(name, ts.Diagnostics.Class_name_cannot_be_Object_when_targeting_ES5_with_module_0, ts.ModuleKind[moduleKind]); // https://github.com/Microsoft/TypeScript/issues/17494 + const containsArguments = containsArgumentsReference(node); + if (containsArguments) { + const lastJSDocParam = ts.lastOrUndefined(jsdocParameters); + if (isJs && lastJSDocParam && ts.isIdentifier(lastJSDocParam.name) && lastJSDocParam.typeExpression && + lastJSDocParam.typeExpression.type && !parameters.has(lastJSDocParam.name.escapedText) && !isArrayType(getTypeFromTypeNode(lastJSDocParam.typeExpression.type))) { + error(lastJSDocParam.name, ts.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, ts.idText(lastJSDocParam.name)); } } - - function checkUnmatchedJSDocParameters(node: ts.SignatureDeclaration) { - const jsdocParameters = ts.filter(ts.getJSDocTags(node), ts.isJSDocParameterTag); - if (!ts.length(jsdocParameters)) - return; - const isJs = ts.isInJSFile(node); - const parameters = new ts.Set(); - const excludedParameters = new ts.Set(); - ts.forEach(node.parameters, ({ name }, index) => { - if (ts.isIdentifier(name)) { - parameters.add(name.escapedText); + else { + ts.forEach(jsdocParameters, ({ name }, index) => { + if (excludedParameters.has(index) || ts.isIdentifier(name) && parameters.has(name.escapedText)) { + return; } - if (ts.isBindingPattern(name)) { - excludedParameters.add(index); + if (ts.isQualifiedName(name)) { + if (isJs) { + error(name, ts.Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1, ts.entityNameToString(name), ts.entityNameToString(name.left)); + } } - }); - - const containsArguments = containsArgumentsReference(node); - if (containsArguments) { - const lastJSDocParam = ts.lastOrUndefined(jsdocParameters); - if (isJs && lastJSDocParam && ts.isIdentifier(lastJSDocParam.name) && lastJSDocParam.typeExpression && - lastJSDocParam.typeExpression.type && !parameters.has(lastJSDocParam.name.escapedText) && !isArrayType(getTypeFromTypeNode(lastJSDocParam.typeExpression.type))) { - error(lastJSDocParam.name, ts.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, ts.idText(lastJSDocParam.name)); + else { + errorOrSuggestion(isJs, name, ts.Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, ts.idText(name)); } - } - else { - ts.forEach(jsdocParameters, ({ name }, index) => { - if (excludedParameters.has(index) || ts.isIdentifier(name) && parameters.has(name.escapedText)) { - return; - } - if (ts.isQualifiedName(name)) { - if (isJs) { - error(name, ts.Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1, ts.entityNameToString(name), ts.entityNameToString(name.left)); - } - } - else { - errorOrSuggestion(isJs, name, ts.Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, ts.idText(name)); - } - }); - } + }); } + } - /** - * Check each type parameter and check that type parameters have no duplicate type parameter declarations - */ - function checkTypeParameters(typeParameterDeclarations: readonly ts.TypeParameterDeclaration[] | undefined) { - let seenDefault = false; - if (typeParameterDeclarations) { - for (let i = 0; i < typeParameterDeclarations.length; i++) { - const node = typeParameterDeclarations[i]; - checkTypeParameter(node); + /** + * Check each type parameter and check that type parameters have no duplicate type parameter declarations + */ + function checkTypeParameters(typeParameterDeclarations: readonly ts.TypeParameterDeclaration[] | undefined) { + let seenDefault = false; + if (typeParameterDeclarations) { + for (let i = 0; i < typeParameterDeclarations.length; i++) { + const node = typeParameterDeclarations[i]; + checkTypeParameter(node); - addLazyDiagnostic(createCheckTypeParameterDiagnostic(node, i)); - } + addLazyDiagnostic(createCheckTypeParameterDiagnostic(node, i)); } + } - function createCheckTypeParameterDiagnostic(node: ts.TypeParameterDeclaration, i: number) { - return () => { - if (node.default) { - seenDefault = true; - checkTypeParametersNotReferenced(node.default, typeParameterDeclarations!, i); - } - else if (seenDefault) { - error(node, ts.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, ts.Diagnostics.Duplicate_identifier_0, ts.declarationNameToString(node.name)); - } + function createCheckTypeParameterDiagnostic(node: ts.TypeParameterDeclaration, i: number) { + return () => { + if (node.default) { + seenDefault = true; + checkTypeParametersNotReferenced(node.default, typeParameterDeclarations!, i); + } + else if (seenDefault) { + error(node, ts.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, ts.Diagnostics.Duplicate_identifier_0, ts.declarationNameToString(node.name)); } - }; - } + } + }; } + } - /** Check that type parameter defaults only reference previously declared type parameters */ - function checkTypeParametersNotReferenced(root: ts.TypeNode, typeParameters: readonly ts.TypeParameterDeclaration[], index: number) { - visit(root); - function visit(node: ts.Node) { - if (node.kind === ts.SyntaxKind.TypeReference) { - const type = getTypeFromTypeReference(node as ts.TypeReferenceNode); - if (type.flags & ts.TypeFlags.TypeParameter) { - for (let i = index; i < typeParameters.length; i++) { - if (type.symbol === getSymbolOfNode(typeParameters[i])) { - error(node, ts.Diagnostics.Type_parameter_defaults_can_only_reference_previously_declared_type_parameters); - } + /** Check that type parameter defaults only reference previously declared type parameters */ + function checkTypeParametersNotReferenced(root: ts.TypeNode, typeParameters: readonly ts.TypeParameterDeclaration[], index: number) { + visit(root); + function visit(node: ts.Node) { + if (node.kind === ts.SyntaxKind.TypeReference) { + const type = getTypeFromTypeReference(node as ts.TypeReferenceNode); + if (type.flags & ts.TypeFlags.TypeParameter) { + for (let i = index; i < typeParameters.length; i++) { + if (type.symbol === getSymbolOfNode(typeParameters[i])) { + error(node, ts.Diagnostics.Type_parameter_defaults_can_only_reference_previously_declared_type_parameters); } } } - ts.forEachChild(node, visit); } + ts.forEachChild(node, visit); + } + } + + /** Check that type parameter lists are identical across multiple declarations */ + function checkTypeParameterListsIdentical(symbol: ts.Symbol) { + if (symbol.declarations && symbol.declarations.length === 1) { + return; } - /** Check that type parameter lists are identical across multiple declarations */ - function checkTypeParameterListsIdentical(symbol: ts.Symbol) { - if (symbol.declarations && symbol.declarations.length === 1) { + const links = getSymbolLinks(symbol); + if (!links.typeParametersChecked) { + links.typeParametersChecked = true; + const declarations = getClassOrInterfaceDeclarationsOfSymbol(symbol); + if (!declarations || declarations.length <= 1) { return; } - const links = getSymbolLinks(symbol); - if (!links.typeParametersChecked) { - links.typeParametersChecked = true; - const declarations = getClassOrInterfaceDeclarationsOfSymbol(symbol); - if (!declarations || declarations.length <= 1) { - return; - } - - const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; - if (!areTypeParametersIdentical(declarations, type.localTypeParameters!, ts.getEffectiveTypeParameterDeclarations)) { - // Report an error on every conflicting declaration. - const name = symbolToString(symbol); - for (const declaration of declarations) { - error(declaration.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_type_parameters, name); - } + const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; + if (!areTypeParametersIdentical(declarations, type.localTypeParameters!, ts.getEffectiveTypeParameterDeclarations)) { + // Report an error on every conflicting declaration. + const name = symbolToString(symbol); + for (const declaration of declarations) { + error(declaration.name, ts.Diagnostics.All_declarations_of_0_must_have_identical_type_parameters, name); } } } + } - function areTypeParametersIdentical(declarations: readonly T[], targetParameters: ts.TypeParameter[], getTypeParameterDeclarations: (node: T) => readonly ts.TypeParameterDeclaration[]) { - const maxTypeArgumentCount = ts.length(targetParameters); - const minTypeArgumentCount = getMinTypeArgumentCount(targetParameters); + function areTypeParametersIdentical(declarations: readonly T[], targetParameters: ts.TypeParameter[], getTypeParameterDeclarations: (node: T) => readonly ts.TypeParameterDeclaration[]) { + const maxTypeArgumentCount = ts.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 = getTypeParameterDeclarations(declaration); - const numTypeParameters = sourceParameters.length; - if (numTypeParameters < minTypeArgumentCount || numTypeParameters > maxTypeArgumentCount) { - return false; - } + for (const declaration of declarations) { + // If this declaration has too few or too many type parameters, we report an error + const sourceParameters = getTypeParameterDeclarations(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]; + 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 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 = ts.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 does not have an identical constraint as the resolved + // type parameter at this position, we report an error. + const constraint = ts.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; - } + // 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; } } - - return true; } - function checkClassExpression(node: ts.ClassExpression): ts.Type { - checkClassLikeDeclaration(node); - checkNodeDeferred(node); - return getTypeOfSymbol(getSymbolOfNode(node)); - } + return true; + } - function checkClassExpressionDeferred(node: ts.ClassExpression) { - ts.forEach(node.members, checkSourceElement); - registerForUnusedIdentifiersCheck(node); - } + function checkClassExpression(node: ts.ClassExpression): ts.Type { + checkClassLikeDeclaration(node); + checkNodeDeferred(node); + return getTypeOfSymbol(getSymbolOfNode(node)); + } - function checkClassDeclaration(node: ts.ClassDeclaration) { - if (ts.some(node.decorators) && ts.some(node.members, p => ts.hasStaticModifier(p) && ts.isPrivateIdentifierClassElementDeclaration(p))) { - grammarErrorOnNode(node.decorators[0], ts.Diagnostics.Class_decorators_can_t_be_used_with_static_private_identifier_Consider_removing_the_experimental_decorator); - } - if (!node.name && !ts.hasSyntacticModifier(node, ts.ModifierFlags.Default)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name); - } - checkClassLikeDeclaration(node); - ts.forEach(node.members, checkSourceElement); + function checkClassExpressionDeferred(node: ts.ClassExpression) { + ts.forEach(node.members, checkSourceElement); + registerForUnusedIdentifiersCheck(node); + } - registerForUnusedIdentifiersCheck(node); + function checkClassDeclaration(node: ts.ClassDeclaration) { + if (ts.some(node.decorators) && ts.some(node.members, p => ts.hasStaticModifier(p) && ts.isPrivateIdentifierClassElementDeclaration(p))) { + grammarErrorOnNode(node.decorators[0], ts.Diagnostics.Class_decorators_can_t_be_used_with_static_private_identifier_Consider_removing_the_experimental_decorator); + } + if (!node.name && !ts.hasSyntacticModifier(node, ts.ModifierFlags.Default)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name); } + checkClassLikeDeclaration(node); + ts.forEach(node.members, checkSourceElement); - function checkClassLikeDeclaration(node: ts.ClassLikeDeclaration) { - checkGrammarClassLikeDeclaration(node); - checkDecorators(node); - checkCollisionsForDeclarationName(node, node.name); - checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; - const typeWithThis = getTypeWithThisArgument(type); - const staticType = getTypeOfSymbol(symbol) as ts.ObjectType; - checkTypeParameterListsIdentical(symbol); - checkFunctionOrConstructorSymbol(symbol); - checkClassForDuplicateDeclarations(node); + registerForUnusedIdentifiersCheck(node); + } - // Only check for reserved static identifiers on non-ambient context. - const nodeInAmbientContext = !!(node.flags & ts.NodeFlags.Ambient); - if (!nodeInAmbientContext) { - checkClassForStaticPropertyNameConflicts(node); + function checkClassLikeDeclaration(node: ts.ClassLikeDeclaration) { + checkGrammarClassLikeDeclaration(node); + checkDecorators(node); + checkCollisionsForDeclarationName(node, node.name); + checkTypeParameters(ts.getEffectiveTypeParameterDeclarations(node)); + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); + const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; + const typeWithThis = getTypeWithThisArgument(type); + const staticType = getTypeOfSymbol(symbol) as ts.ObjectType; + checkTypeParameterListsIdentical(symbol); + checkFunctionOrConstructorSymbol(symbol); + checkClassForDuplicateDeclarations(node); + + // Only check for reserved static identifiers on non-ambient context. + const nodeInAmbientContext = !!(node.flags & ts.NodeFlags.Ambient); + if (!nodeInAmbientContext) { + checkClassForStaticPropertyNameConflicts(node); + } + + const baseTypeNode = ts.getEffectiveBaseTypeNode(node); + if (baseTypeNode) { + ts.forEach(baseTypeNode.typeArguments, checkSourceElement); + if (languageVersion < ts.ScriptTarget.ES2015) { + checkExternalEmitHelpers(baseTypeNode.parent, ts.ExternalEmitHelpers.Extends); + } + // check both @extends and extends if both are specified. + const extendsNode = ts.getClassExtendsHeritageElement(node); + if (extendsNode && extendsNode !== baseTypeNode) { + checkExpression(extendsNode.expression); } - const baseTypeNode = ts.getEffectiveBaseTypeNode(node); - if (baseTypeNode) { - ts.forEach(baseTypeNode.typeArguments, checkSourceElement); - if (languageVersion < ts.ScriptTarget.ES2015) { - checkExternalEmitHelpers(baseTypeNode.parent, ts.ExternalEmitHelpers.Extends); - } - // check both @extends and extends if both are specified. - const extendsNode = ts.getClassExtendsHeritageElement(node); - if (extendsNode && extendsNode !== baseTypeNode) { - checkExpression(extendsNode.expression); - } - - const baseTypes = getBaseTypes(type); - if (baseTypes.length) { - addLazyDiagnostic(() => { - const baseType = baseTypes[0]; - const baseConstructorType = getBaseConstructorTypeOfClass(type); - const staticBaseType = getApparentType(baseConstructorType); - checkBaseTypeAccessibility(staticBaseType, baseTypeNode); - checkSourceElement(baseTypeNode.expression); - if (ts.some(baseTypeNode.typeArguments)) { - ts.forEach(baseTypeNode.typeArguments, checkSourceElement); - for (const constructor of getConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode)) { - if (!checkTypeArgumentConstraints(baseTypeNode, constructor.typeParameters!)) { - break; - } + const baseTypes = getBaseTypes(type); + if (baseTypes.length) { + addLazyDiagnostic(() => { + const baseType = baseTypes[0]; + const baseConstructorType = getBaseConstructorTypeOfClass(type); + const staticBaseType = getApparentType(baseConstructorType); + checkBaseTypeAccessibility(staticBaseType, baseTypeNode); + checkSourceElement(baseTypeNode.expression); + if (ts.some(baseTypeNode.typeArguments)) { + ts.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, ts.Diagnostics.Class_0_incorrectly_extends_base_class_1); + } + const baseWithThis = getTypeWithThisArgument(baseType, type.thisType); + if (!checkTypeAssignableTo(typeWithThis, baseWithThis, /*errorNode*/ undefined)) { + issueMemberSpecificError(node, typeWithThis, baseWithThis, ts.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, ts.Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1); + } + if (baseConstructorType.flags & ts.TypeFlags.TypeVariable) { + if (!isMixinConstructorType(staticType)) { + error(node.name || node, ts.Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any); } else { - // Report static side error only when instance type is assignable - checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node, ts.Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1); - } - if (baseConstructorType.flags & ts.TypeFlags.TypeVariable) { - if (!isMixinConstructorType(staticType)) { - error(node.name || node, ts.Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any); - } - else { - const constructSignatures = getSignaturesOfType(baseConstructorType, ts.SignatureKind.Construct); - if (constructSignatures.some(signature => signature.flags & ts.SignatureFlags.Abstract) && !ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract)) { - error(node.name || node, ts.Diagnostics.A_mixin_class_that_extends_from_a_type_variable_containing_an_abstract_construct_signature_must_also_be_declared_abstract); - } + const constructSignatures = getSignaturesOfType(baseConstructorType, ts.SignatureKind.Construct); + if (constructSignatures.some(signature => signature.flags & ts.SignatureFlags.Abstract) && !ts.hasSyntacticModifier(node, ts.ModifierFlags.Abstract)) { + error(node.name || node, ts.Diagnostics.A_mixin_class_that_extends_from_a_type_variable_containing_an_abstract_construct_signature_must_also_be_declared_abstract); } } + } - if (!(staticBaseType.symbol && staticBaseType.symbol.flags & ts.SymbolFlags.Class) && !(baseConstructorType.flags & ts.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 (ts.forEach(constructors, sig => !isJSConstructor(sig.declaration) && !isTypeIdenticalTo(getReturnTypeOfSignature(sig), baseType))) { - error(baseTypeNode.expression, ts.Diagnostics.Base_constructors_must_all_have_the_same_return_type); - } + if (!(staticBaseType.symbol && staticBaseType.symbol.flags & ts.SymbolFlags.Class) && !(baseConstructorType.flags & ts.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 (ts.forEach(constructors, sig => !isJSConstructor(sig.declaration) && !isTypeIdenticalTo(getReturnTypeOfSignature(sig), baseType))) { + error(baseTypeNode.expression, ts.Diagnostics.Base_constructors_must_all_have_the_same_return_type); } - checkKindsOfPropertyMemberOverrides(type, baseType); - }); - } + } + checkKindsOfPropertyMemberOverrides(type, baseType); + }); } + } - checkMembersForOverrideModifier(node, type, typeWithThis, staticType); + checkMembersForOverrideModifier(node, type, typeWithThis, staticType); - const implementedTypeNodes = ts.getEffectiveImplementsTypeNodes(node); - if (implementedTypeNodes) { - for (const typeRefNode of implementedTypeNodes) { - if (!ts.isEntityNameExpression(typeRefNode.expression) || ts.isOptionalChain(typeRefNode.expression)) { - error(typeRefNode.expression, ts.Diagnostics.A_class_can_only_implement_an_identifier_Slashqualified_name_with_optional_type_arguments); - } - checkTypeReferenceNode(typeRefNode); - addLazyDiagnostic(createImplementsDiagnostics(typeRefNode)); + const implementedTypeNodes = ts.getEffectiveImplementsTypeNodes(node); + if (implementedTypeNodes) { + for (const typeRefNode of implementedTypeNodes) { + if (!ts.isEntityNameExpression(typeRefNode.expression) || ts.isOptionalChain(typeRefNode.expression)) { + error(typeRefNode.expression, ts.Diagnostics.A_class_can_only_implement_an_identifier_Slashqualified_name_with_optional_type_arguments); } + checkTypeReferenceNode(typeRefNode); + addLazyDiagnostic(createImplementsDiagnostics(typeRefNode)); } + } - addLazyDiagnostic(() => { - checkIndexConstraints(type, symbol); - checkIndexConstraints(staticType, symbol, /*isStaticIndex*/ true); - checkTypeForDuplicateIndexSignatures(node); - checkPropertyInitialization(node); - }); + addLazyDiagnostic(() => { + checkIndexConstraints(type, symbol); + checkIndexConstraints(staticType, symbol, /*isStaticIndex*/ true); + checkTypeForDuplicateIndexSignatures(node); + checkPropertyInitialization(node); + }); - function createImplementsDiagnostics(typeRefNode: ts.ExpressionWithTypeArguments) { - return () => { - const t = getReducedType(getTypeFromTypeNode(typeRefNode)); - if (!isErrorType(t)) { - if (isValidBaseType(t)) { - const genericDiag = t.symbol && t.symbol.flags & ts.SymbolFlags.Class ? - ts.Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass : - ts.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, ts.Diagnostics.A_class_can_only_implement_an_object_type_or_intersection_of_object_types_with_statically_known_members); + function createImplementsDiagnostics(typeRefNode: ts.ExpressionWithTypeArguments) { + return () => { + const t = getReducedType(getTypeFromTypeNode(typeRefNode)); + if (!isErrorType(t)) { + if (isValidBaseType(t)) { + const genericDiag = t.symbol && t.symbol.flags & ts.SymbolFlags.Class ? + ts.Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass : + ts.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, ts.Diagnostics.A_class_can_only_implement_an_object_type_or_intersection_of_object_types_with_statically_known_members); + } + } + }; } + } - function checkMembersForOverrideModifier(node: ts.ClassLikeDeclaration, type: ts.InterfaceType, typeWithThis: ts.Type, staticType: ts.ObjectType) { - const baseTypeNode = ts.getEffectiveBaseTypeNode(node); - const baseTypes = baseTypeNode && getBaseTypes(type); - const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(ts.first(baseTypes), type.thisType) : undefined; - const baseStaticType = getBaseConstructorTypeOfClass(type); - - for (const member of node.members) { - if (ts.hasAmbientModifier(member)) { - continue; - } + function checkMembersForOverrideModifier(node: ts.ClassLikeDeclaration, type: ts.InterfaceType, typeWithThis: ts.Type, staticType: ts.ObjectType) { + const baseTypeNode = ts.getEffectiveBaseTypeNode(node); + const baseTypes = baseTypeNode && getBaseTypes(type); + const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(ts.first(baseTypes), type.thisType) : undefined; + const baseStaticType = getBaseConstructorTypeOfClass(type); - if (ts.isConstructorDeclaration(member)) { - ts.forEach(member.parameters, param => { - if (ts.isParameterPropertyDeclaration(param, member)) { - checkExistingMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, param, - /* memberIsParameterProperty */ true); - } - }); - } - checkExistingMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, member, - /* memberIsParameterProperty */ false); + for (const member of node.members) { + if (ts.hasAmbientModifier(member)) { + continue; } - } - /** - * @param member Existing member node to be checked. - * Note: `member` cannot be a synthetic node. - */ - function checkExistingMemberForOverrideModifier(node: ts.ClassLikeDeclaration, staticType: ts.ObjectType, baseStaticType: ts.Type, baseWithThis: ts.Type | undefined, type: ts.InterfaceType, typeWithThis: ts.Type, member: ts.ClassElement | ts.ParameterPropertyDeclaration, memberIsParameterProperty: boolean, reportErrors = true): ts.MemberOverrideStatus { - const declaredProp = member.name - && getSymbolAtLocation(member.name) - || getSymbolAtLocation(member); - if (!declaredProp) { - return ts.MemberOverrideStatus.Ok; + if (ts.isConstructorDeclaration(member)) { + ts.forEach(member.parameters, param => { + if (ts.isParameterPropertyDeclaration(param, member)) { + checkExistingMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, param, + /* memberIsParameterProperty */ true); + } + }); } + checkExistingMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, member, + /* memberIsParameterProperty */ false); + } + } - return checkMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, ts.hasOverrideModifier(member), ts.hasAbstractModifier(member), ts.isStatic(member), memberIsParameterProperty, ts.symbolName(declaredProp), reportErrors ? member : undefined); + /** + * @param member Existing member node to be checked. + * Note: `member` cannot be a synthetic node. + */ + function checkExistingMemberForOverrideModifier(node: ts.ClassLikeDeclaration, staticType: ts.ObjectType, baseStaticType: ts.Type, baseWithThis: ts.Type | undefined, type: ts.InterfaceType, typeWithThis: ts.Type, member: ts.ClassElement | ts.ParameterPropertyDeclaration, memberIsParameterProperty: boolean, reportErrors = true): ts.MemberOverrideStatus { + const declaredProp = member.name + && getSymbolAtLocation(member.name) + || getSymbolAtLocation(member); + if (!declaredProp) { + return ts.MemberOverrideStatus.Ok; } - /** - * Checks a class member declaration for either a missing or an invalid `override` modifier. - * Note: this function can be used for speculative checking, - * i.e. checking a member that does not yet exist in the program. - * An example of that would be to call this function in a completions scenario, - * when offering a method declaration as completion. - * @param errorNode The node where we should report an error, or undefined if we should not report errors. - */ - function checkMemberForOverrideModifier(node: ts.ClassLikeDeclaration, staticType: ts.ObjectType, baseStaticType: ts.Type, baseWithThis: ts.Type | undefined, type: ts.InterfaceType, typeWithThis: ts.Type, memberHasOverrideModifier: boolean, memberHasAbstractModifier: boolean, memberIsStatic: boolean, memberIsParameterProperty: boolean, memberName: string, errorNode?: ts.Node): ts.MemberOverrideStatus { - const isJs = ts.isInJSFile(node); - const nodeInAmbientContext = !!(node.flags & ts.NodeFlags.Ambient); - if (baseWithThis && (memberHasOverrideModifier || compilerOptions.noImplicitOverride)) { - const memberEscapedName = ts.escapeLeadingUnderscores(memberName); - const thisType = memberIsStatic ? staticType : typeWithThis; - const baseType = memberIsStatic ? baseStaticType : baseWithThis; - const prop = getPropertyOfType(thisType, memberEscapedName); - const baseProp = getPropertyOfType(baseType, memberEscapedName); - - const baseClassName = typeToString(baseWithThis); - if (prop && !baseProp && memberHasOverrideModifier) { - if (errorNode) { - const suggestion = getSuggestedSymbolForNonexistentClassMember(memberName, baseType); // Again, using symbol name: note that's different from `symbol.escapedName` - suggestion ? - error(errorNode, isJs ? - ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1 : - ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1, baseClassName, symbolToString(suggestion)) : - error(errorNode, isJs ? - ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0 : - ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0, baseClassName); - } - return ts.MemberOverrideStatus.HasInvalidOverride; - } - else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) { - const baseHasAbstract = ts.some(baseProp.declarations, ts.hasAbstractModifier); - if (memberHasOverrideModifier) { - return ts.MemberOverrideStatus.Ok; - } + return checkMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, ts.hasOverrideModifier(member), ts.hasAbstractModifier(member), ts.isStatic(member), memberIsParameterProperty, ts.symbolName(declaredProp), reportErrors ? member : undefined); + } - if (!baseHasAbstract) { - if (errorNode) { - const diag = memberIsParameterProperty ? - isJs ? - ts.Diagnostics.This_parameter_property_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : - ts.Diagnostics.This_parameter_property_must_have_an_override_modifier_because_it_overrides_a_member_in_base_class_0 : - isJs ? - ts.Diagnostics.This_member_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : - ts.Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_a_member_in_the_base_class_0; - error(errorNode, diag, baseClassName); - } - return ts.MemberOverrideStatus.NeedsOverride; - } - else if (memberHasAbstractModifier && baseHasAbstract) { - if (errorNode) { - error(errorNode, ts.Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName); - } - return ts.MemberOverrideStatus.NeedsOverride; - } - } - } - else if (memberHasOverrideModifier) { + /** + * Checks a class member declaration for either a missing or an invalid `override` modifier. + * Note: this function can be used for speculative checking, + * i.e. checking a member that does not yet exist in the program. + * An example of that would be to call this function in a completions scenario, + * when offering a method declaration as completion. + * @param errorNode The node where we should report an error, or undefined if we should not report errors. + */ + function checkMemberForOverrideModifier(node: ts.ClassLikeDeclaration, staticType: ts.ObjectType, baseStaticType: ts.Type, baseWithThis: ts.Type | undefined, type: ts.InterfaceType, typeWithThis: ts.Type, memberHasOverrideModifier: boolean, memberHasAbstractModifier: boolean, memberIsStatic: boolean, memberIsParameterProperty: boolean, memberName: string, errorNode?: ts.Node): ts.MemberOverrideStatus { + const isJs = ts.isInJSFile(node); + const nodeInAmbientContext = !!(node.flags & ts.NodeFlags.Ambient); + if (baseWithThis && (memberHasOverrideModifier || compilerOptions.noImplicitOverride)) { + const memberEscapedName = ts.escapeLeadingUnderscores(memberName); + const thisType = memberIsStatic ? staticType : typeWithThis; + const baseType = memberIsStatic ? baseStaticType : baseWithThis; + const prop = getPropertyOfType(thisType, memberEscapedName); + const baseProp = getPropertyOfType(baseType, memberEscapedName); + + const baseClassName = typeToString(baseWithThis); + if (prop && !baseProp && memberHasOverrideModifier) { if (errorNode) { - const className = typeToString(type); - error(errorNode, isJs ? - ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_its_containing_class_0_does_not_extend_another_class : - ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_its_containing_class_0_does_not_extend_another_class, className); + const suggestion = getSuggestedSymbolForNonexistentClassMember(memberName, baseType); // Again, using symbol name: note that's different from `symbol.escapedName` + suggestion ? + error(errorNode, isJs ? + ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1 : + ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1, baseClassName, symbolToString(suggestion)) : + error(errorNode, isJs ? + ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0 : + ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0, baseClassName); } return ts.MemberOverrideStatus.HasInvalidOverride; } - - return ts.MemberOverrideStatus.Ok; - } - - function issueMemberSpecificError(node: ts.ClassLikeDeclaration, typeWithThis: ts.Type, baseWithThis: ts.Type, broadDiag: ts.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 (ts.isStatic(member)) { - continue; + else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) { + const baseHasAbstract = ts.some(baseProp.declarations, ts.hasAbstractModifier); + if (memberHasOverrideModifier) { + return ts.MemberOverrideStatus.Ok; } - 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 = () => ts.chainDiagnosticMessages( - /*details*/ undefined, ts.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; - } + + if (!baseHasAbstract) { + if (errorNode) { + const diag = memberIsParameterProperty ? + isJs ? + ts.Diagnostics.This_parameter_property_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : + ts.Diagnostics.This_parameter_property_must_have_an_override_modifier_because_it_overrides_a_member_in_base_class_0 : + isJs ? + ts.Diagnostics.This_member_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : + ts.Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_a_member_in_the_base_class_0; + error(errorNode, diag, baseClassName); + } + return ts.MemberOverrideStatus.NeedsOverride; + } + else if (memberHasAbstractModifier && baseHasAbstract) { + if (errorNode) { + error(errorNode, ts.Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName); } + return ts.MemberOverrideStatus.NeedsOverride; } } - if (!issuedMemberError) { - // check again with diagnostics to generate a less-specific error - checkTypeAssignableTo(typeWithThis, baseWithThis, node.name || node, broadDiag); + } + else if (memberHasOverrideModifier) { + if (errorNode) { + const className = typeToString(type); + error(errorNode, isJs ? + ts.Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_its_containing_class_0_does_not_extend_another_class : + ts.Diagnostics.This_member_cannot_have_an_override_modifier_because_its_containing_class_0_does_not_extend_another_class, className); } + return ts.MemberOverrideStatus.HasInvalidOverride; } - function checkBaseTypeAccessibility(type: ts.Type, node: ts.ExpressionWithTypeArguments) { - const signatures = getSignaturesOfType(type, ts.SignatureKind.Construct); - if (signatures.length) { - const declaration = signatures[0].declaration; - if (declaration && ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Private)) { - const typeClassDeclaration = ts.getClassLikeDeclarationOfSymbol(type.symbol)!; - if (!isNodeWithinClass(node, typeClassDeclaration)) { - error(node, ts.Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol)); + return ts.MemberOverrideStatus.Ok; + } + + function issueMemberSpecificError(node: ts.ClassLikeDeclaration, typeWithThis: ts.Type, baseWithThis: ts.Type, broadDiag: ts.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 (ts.isStatic(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 = () => ts.chainDiagnosticMessages( + /*details*/ undefined, ts.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; } } } } + if (!issuedMemberError) { + // check again with diagnostics to generate a less-specific error + checkTypeAssignableTo(typeWithThis, baseWithThis, node.name || node, broadDiag); + } + } - /** - * Checks a member declaration node to see if has a missing or invalid `override` modifier. - * @param node Class-like node where the member is declared. - * @param member Member declaration node. - * Note: `member` can be a synthetic node without a parent. - */ - function getMemberOverrideModifierStatus(node: ts.ClassLikeDeclaration, member: ts.ClassElement): ts.MemberOverrideStatus { - if (!member.name) { - return ts.MemberOverrideStatus.Ok; + function checkBaseTypeAccessibility(type: ts.Type, node: ts.ExpressionWithTypeArguments) { + const signatures = getSignaturesOfType(type, ts.SignatureKind.Construct); + if (signatures.length) { + const declaration = signatures[0].declaration; + if (declaration && ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Private)) { + const typeClassDeclaration = ts.getClassLikeDeclarationOfSymbol(type.symbol)!; + if (!isNodeWithinClass(node, typeClassDeclaration)) { + error(node, ts.Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol)); + } } + } + } - const symbol = getSymbolOfNode(node); - const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; - const typeWithThis = getTypeWithThisArgument(type); - const staticType = getTypeOfSymbol(symbol) as ts.ObjectType; - const baseTypeNode = ts.getEffectiveBaseTypeNode(node); - const baseTypes = baseTypeNode && getBaseTypes(type); - const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(ts.first(baseTypes), type.thisType) : undefined; - const baseStaticType = getBaseConstructorTypeOfClass(type); - - const memberHasOverrideModifier = member.parent - ? ts.hasOverrideModifier(member) - : ts.hasSyntacticModifier(member, ts.ModifierFlags.Override); - const memberName = ts.unescapeLeadingUnderscores(ts.getTextOfPropertyName(member.name)); - return checkMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, memberHasOverrideModifier, ts.hasAbstractModifier(member), ts.isStatic(member), - /* memberIsParameterProperty */ false, memberName); - } - - 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 ts.getCheckFlags(s) & ts.CheckFlags.Instantiated ? (s as ts.TransientSymbol).target! : s; - } - - function getClassOrInterfaceDeclarationsOfSymbol(symbol: ts.Symbol) { - return ts.filter(symbol.declarations, (d: ts.Declaration): d is ts.ClassDeclaration | ts.InterfaceDeclaration => d.kind === ts.SyntaxKind.ClassDeclaration || d.kind === ts.SyntaxKind.InterfaceDeclaration); - } - - function checkKindsOfPropertyMemberOverrides(type: ts.InterfaceType, baseType: ts.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 & ts.SymbolFlags.Prototype) { - continue; - } - const baseSymbol = getPropertyOfObjectType(type, base.escapedName); - if (!baseSymbol) { - continue; - } - const derived = getTargetSymbol(baseSymbol); - const baseDeclarationFlags = ts.getDeclarationModifierFlagsFromSymbol(base); - ts.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 = ts.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 & ts.ModifierFlags.Abstract && (!derivedClassDecl || !ts.hasSyntacticModifier(derivedClassDecl, ts.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; - } - } + /** + * Checks a member declaration node to see if has a missing or invalid `override` modifier. + * @param node Class-like node where the member is declared. + * @param member Member declaration node. + * Note: `member` can be a synthetic node without a parent. + */ + function getMemberOverrideModifierStatus(node: ts.ClassLikeDeclaration, member: ts.ClassElement): ts.MemberOverrideStatus { + if (!member.name) { + return ts.MemberOverrideStatus.Ok; + } - if (derivedClassDecl.kind === ts.SyntaxKind.ClassExpression) { - error(derivedClassDecl, ts.Diagnostics.Non_abstract_class_expression_does_not_implement_inherited_abstract_member_0_from_class_1, symbolToString(baseProperty), typeToString(baseType)); - } - else { - error(derivedClassDecl, ts.Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2, typeToString(type), symbolToString(baseProperty), typeToString(baseType)); + const symbol = getSymbolOfNode(node); + const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; + const typeWithThis = getTypeWithThisArgument(type); + const staticType = getTypeOfSymbol(symbol) as ts.ObjectType; + const baseTypeNode = ts.getEffectiveBaseTypeNode(node); + const baseTypes = baseTypeNode && getBaseTypes(type); + const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(ts.first(baseTypes), type.thisType) : undefined; + const baseStaticType = getBaseConstructorTypeOfClass(type); + + const memberHasOverrideModifier = member.parent + ? ts.hasOverrideModifier(member) + : ts.hasSyntacticModifier(member, ts.ModifierFlags.Override); + const memberName = ts.unescapeLeadingUnderscores(ts.getTextOfPropertyName(member.name)); + return checkMemberForOverrideModifier(node, staticType, baseStaticType, baseWithThis, type, typeWithThis, memberHasOverrideModifier, ts.hasAbstractModifier(member), ts.isStatic(member), + /* memberIsParameterProperty */ false, memberName); + } + + 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 ts.getCheckFlags(s) & ts.CheckFlags.Instantiated ? (s as ts.TransientSymbol).target! : s; + } + + function getClassOrInterfaceDeclarationsOfSymbol(symbol: ts.Symbol) { + return ts.filter(symbol.declarations, (d: ts.Declaration): d is ts.ClassDeclaration | ts.InterfaceDeclaration => d.kind === ts.SyntaxKind.ClassDeclaration || d.kind === ts.SyntaxKind.InterfaceDeclaration); + } + + function checkKindsOfPropertyMemberOverrides(type: ts.InterfaceType, baseType: ts.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 & ts.SymbolFlags.Prototype) { + continue; + } + const baseSymbol = getPropertyOfObjectType(type, base.escapedName); + if (!baseSymbol) { + continue; + } + const derived = getTargetSymbol(baseSymbol); + const baseDeclarationFlags = ts.getDeclarationModifierFlagsFromSymbol(base); + ts.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 = ts.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 & ts.ModifierFlags.Abstract && (!derivedClassDecl || !ts.hasSyntacticModifier(derivedClassDecl, ts.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 === ts.SyntaxKind.ClassExpression) { + error(derivedClassDecl, ts.Diagnostics.Non_abstract_class_expression_does_not_implement_inherited_abstract_member_0_from_class_1, symbolToString(baseProperty), typeToString(baseType)); + } + else { + error(derivedClassDecl, ts.Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2, typeToString(type), symbolToString(baseProperty), typeToString(baseType)); + } } - else { - // derived overrides base. - const derivedDeclarationFlags = ts.getDeclarationModifierFlagsFromSymbol(derived); - if (baseDeclarationFlags & ts.ModifierFlags.Private || derivedDeclarationFlags & ts.ModifierFlags.Private) { - // either base or derived property is private - not override, skip it + } + else { + // derived overrides base. + const derivedDeclarationFlags = ts.getDeclarationModifierFlagsFromSymbol(derived); + if (baseDeclarationFlags & ts.ModifierFlags.Private || derivedDeclarationFlags & ts.ModifierFlags.Private) { + // either base or derived property is private - not override, skip it + continue; + } + + let errorMessage: ts.DiagnosticMessage; + const basePropertyFlags = base.flags & ts.SymbolFlags.PropertyOrAccessor; + const derivedPropertyFlags = derived.flags & ts.SymbolFlags.PropertyOrAccessor; + if (basePropertyFlags && derivedPropertyFlags) { + // property/accessor is overridden with property/accessor + if (baseDeclarationFlags & ts.ModifierFlags.Abstract && !(base.valueDeclaration && ts.isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer) + || base.valueDeclaration && base.valueDeclaration.parent.kind === ts.SyntaxKind.InterfaceDeclaration + || derived.valueDeclaration && ts.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; } - let errorMessage: ts.DiagnosticMessage; - const basePropertyFlags = base.flags & ts.SymbolFlags.PropertyOrAccessor; - const derivedPropertyFlags = derived.flags & ts.SymbolFlags.PropertyOrAccessor; - if (basePropertyFlags && derivedPropertyFlags) { - // property/accessor is overridden with property/accessor - if (baseDeclarationFlags & ts.ModifierFlags.Abstract && !(base.valueDeclaration && ts.isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer) - || base.valueDeclaration && base.valueDeclaration.parent.kind === ts.SyntaxKind.InterfaceDeclaration - || derived.valueDeclaration && ts.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 !== ts.SymbolFlags.Property && derivedPropertyFlags === ts.SymbolFlags.Property; - const overriddenInstanceAccessor = basePropertyFlags === ts.SymbolFlags.Property && derivedPropertyFlags !== ts.SymbolFlags.Property; - if (overriddenInstanceProperty || overriddenInstanceAccessor) { - const errorMessage = overriddenInstanceProperty ? - ts.Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property : - ts.Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor; - error(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType), typeToString(type)); - } - else if (useDefineForClassFields) { - const uninitialized = derived.declarations?.find(d => d.kind === ts.SyntaxKind.PropertyDeclaration && !(d as ts.PropertyDeclaration).initializer); - if (uninitialized - && !(derived.flags & ts.SymbolFlags.Transient) - && !(baseDeclarationFlags & ts.ModifierFlags.Abstract) - && !(derivedDeclarationFlags & ts.ModifierFlags.Abstract) - && !derived.declarations?.some(d => !!(d.flags & ts.NodeFlags.Ambient))) { - const constructor = findConstructorDeclaration(ts.getClassLikeDeclarationOfSymbol(type.symbol)!); - const propName = (uninitialized as ts.PropertyDeclaration).name; - if ((uninitialized as ts.PropertyDeclaration).exclamationToken - || !constructor - || !ts.isIdentifier(propName) - || !strictNullChecks - || !isPropertyInitializedInConstructor(propName, type, constructor)) { - const errorMessage = ts.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(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType)); - } + const overriddenInstanceProperty = basePropertyFlags !== ts.SymbolFlags.Property && derivedPropertyFlags === ts.SymbolFlags.Property; + const overriddenInstanceAccessor = basePropertyFlags === ts.SymbolFlags.Property && derivedPropertyFlags !== ts.SymbolFlags.Property; + if (overriddenInstanceProperty || overriddenInstanceAccessor) { + const errorMessage = overriddenInstanceProperty ? + ts.Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property : + ts.Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor; + error(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType), typeToString(type)); + } + else if (useDefineForClassFields) { + const uninitialized = derived.declarations?.find(d => d.kind === ts.SyntaxKind.PropertyDeclaration && !(d as ts.PropertyDeclaration).initializer); + if (uninitialized + && !(derived.flags & ts.SymbolFlags.Transient) + && !(baseDeclarationFlags & ts.ModifierFlags.Abstract) + && !(derivedDeclarationFlags & ts.ModifierFlags.Abstract) + && !derived.declarations?.some(d => !!(d.flags & ts.NodeFlags.Ambient))) { + const constructor = findConstructorDeclaration(ts.getClassLikeDeclarationOfSymbol(type.symbol)!); + const propName = (uninitialized as ts.PropertyDeclaration).name; + if ((uninitialized as ts.PropertyDeclaration).exclamationToken + || !constructor + || !ts.isIdentifier(propName) + || !strictNullChecks + || !isPropertyInitializedInConstructor(propName, type, constructor)) { + const errorMessage = ts.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(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType)); } } + } - // correct case + // correct case + continue; + } + else if (isPrototypeProperty(base)) { + if (isPrototypeProperty(derived) || derived.flags & ts.SymbolFlags.Property) { + // method is overridden with method or property -- correct case continue; } - else if (isPrototypeProperty(base)) { - if (isPrototypeProperty(derived) || derived.flags & ts.SymbolFlags.Property) { - // method is overridden with method or property -- correct case - continue; - } - else { - ts.Debug.assert(!!(derived.flags & ts.SymbolFlags.Accessor)); - errorMessage = ts.Diagnostics.Class_0_defines_instance_member_function_1_but_extended_class_2_defines_it_as_instance_member_accessor; - } - } - else if (base.flags & ts.SymbolFlags.Accessor) { - errorMessage = ts.Diagnostics.Class_0_defines_instance_member_accessor_1_but_extended_class_2_defines_it_as_instance_member_function; - } else { - errorMessage = ts.Diagnostics.Class_0_defines_instance_member_property_1_but_extended_class_2_defines_it_as_instance_member_function; + ts.Debug.assert(!!(derived.flags & ts.SymbolFlags.Accessor)); + errorMessage = ts.Diagnostics.Class_0_defines_instance_member_function_1_but_extended_class_2_defines_it_as_instance_member_accessor; } - - error(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, typeToString(baseType), symbolToString(base), typeToString(type)); } + else if (base.flags & ts.SymbolFlags.Accessor) { + errorMessage = ts.Diagnostics.Class_0_defines_instance_member_accessor_1_but_extended_class_2_defines_it_as_instance_member_function; + } + else { + errorMessage = ts.Diagnostics.Class_0_defines_instance_member_property_1_but_extended_class_2_defines_it_as_instance_member_function; + } + + error(ts.getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, typeToString(baseType), symbolToString(base), typeToString(type)); } } + } - function getNonInterhitedProperties(type: ts.InterfaceType, baseTypes: ts.BaseType[], properties: ts.Symbol[]) { - if (!ts.length(baseTypes)) { - return properties; - } - const seen = new ts.Map(); - ts.forEach(properties, p => { - seen.set(p.escapedName, p); - }); + function getNonInterhitedProperties(type: ts.InterfaceType, baseTypes: ts.BaseType[], properties: ts.Symbol[]) { + if (!ts.length(baseTypes)) { + return properties; + } + const seen = new ts.Map(); + ts.forEach(properties, p => { + seen.set(p.escapedName, p); + }); - 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 && prop.parent === existing.parent) { - seen.delete(prop.escapedName); - } + 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 && prop.parent === existing.parent) { + seen.delete(prop.escapedName); } } - - return ts.arrayFrom(seen.values()); } - function checkInheritedPropertiesAreIdentical(type: ts.InterfaceType, typeNode: ts.Node): boolean { - const baseTypes = getBaseTypes(type); - if (baseTypes.length < 2) { - return true; - } + return ts.arrayFrom(seen.values()); + } - interface InheritanceInfoMap { - prop: ts.Symbol; - containingType: ts.Type; - } - const seen = new ts.Map(); - ts.forEach(resolveDeclaredMembers(type).declaredProperties, p => { - seen.set(p.escapedName, { prop: p, containingType: type }); - }); - let ok = true; + function checkInheritedPropertiesAreIdentical(type: ts.InterfaceType, typeNode: ts.Node): boolean { + const baseTypes = getBaseTypes(type); + if (baseTypes.length < 2) { + return 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; + interface InheritanceInfoMap { + prop: ts.Symbol; + containingType: ts.Type; + } + const seen = new ts.Map(); + ts.forEach(resolveDeclaredMembers(type).declaredProperties, p => { + seen.set(p.escapedName, { prop: p, containingType: type }); + }); + let ok = true; - const typeName1 = typeToString(existing.containingType); - const typeName2 = typeToString(base); + 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; - let errorInfo = ts.chainDiagnosticMessages(/*details*/ undefined, ts.Diagnostics.Named_property_0_of_types_1_and_2_are_not_identical, symbolToString(prop), typeName1, typeName2); - errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Interface_0_cannot_simultaneously_extend_types_1_and_2, typeToString(type), typeName1, typeName2); - diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(typeNode, errorInfo)); - } + const typeName1 = typeToString(existing.containingType); + const typeName2 = typeToString(base); + + let errorInfo = ts.chainDiagnosticMessages(/*details*/ undefined, ts.Diagnostics.Named_property_0_of_types_1_and_2_are_not_identical, symbolToString(prop), typeName1, typeName2); + errorInfo = ts.chainDiagnosticMessages(errorInfo, ts.Diagnostics.Interface_0_cannot_simultaneously_extend_types_1_and_2, typeToString(type), typeName1, typeName2); + diagnostics.add(ts.createDiagnosticForNodeFromMessageChain(typeNode, errorInfo)); } } } - - return ok; } - function checkPropertyInitialization(node: ts.ClassLikeDeclaration) { - if (!strictNullChecks || !strictPropertyInitialization || node.flags & ts.NodeFlags.Ambient) { - return; + return ok; + } + + function checkPropertyInitialization(node: ts.ClassLikeDeclaration) { + if (!strictNullChecks || !strictPropertyInitialization || node.flags & ts.NodeFlags.Ambient) { + return; + } + const constructor = findConstructorDeclaration(node); + for (const member of node.members) { + if (ts.getEffectiveModifierFlags(member) & ts.ModifierFlags.Ambient) { + continue; } - const constructor = findConstructorDeclaration(node); - for (const member of node.members) { - if (ts.getEffectiveModifierFlags(member) & ts.ModifierFlags.Ambient) { - continue; - } - if (!ts.isStatic(member) && isPropertyWithoutInitializer(member)) { - const propName = (member as ts.PropertyDeclaration).name; - if (ts.isIdentifier(propName) || ts.isPrivateIdentifier(propName) || ts.isComputedPropertyName(propName)) { - const type = getTypeOfSymbol(getSymbolOfNode(member)); - if (!(type.flags & ts.TypeFlags.AnyOrUnknown || getFalsyFlags(type) & ts.TypeFlags.Undefined)) { - if (!constructor || !isPropertyInitializedInConstructor(propName, type, constructor)) { - error(member.name, ts.Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor, ts.declarationNameToString(propName)); - } + if (!ts.isStatic(member) && isPropertyWithoutInitializer(member)) { + const propName = (member as ts.PropertyDeclaration).name; + if (ts.isIdentifier(propName) || ts.isPrivateIdentifier(propName) || ts.isComputedPropertyName(propName)) { + const type = getTypeOfSymbol(getSymbolOfNode(member)); + if (!(type.flags & ts.TypeFlags.AnyOrUnknown || getFalsyFlags(type) & ts.TypeFlags.Undefined)) { + if (!constructor || !isPropertyInitializedInConstructor(propName, type, constructor)) { + error(member.name, ts.Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor, ts.declarationNameToString(propName)); } } } } } + } - function isPropertyWithoutInitializer(node: ts.Node) { - return node.kind === ts.SyntaxKind.PropertyDeclaration && - !ts.hasAbstractModifier(node) && - !(node as ts.PropertyDeclaration).exclamationToken && - !(node as ts.PropertyDeclaration).initializer; - } + function isPropertyWithoutInitializer(node: ts.Node) { + return node.kind === ts.SyntaxKind.PropertyDeclaration && + !ts.hasAbstractModifier(node) && + !(node as ts.PropertyDeclaration).exclamationToken && + !(node as ts.PropertyDeclaration).initializer; + } - function isPropertyInitializedInStaticBlocks(propName: ts.Identifier | ts.PrivateIdentifier, propType: ts.Type, staticBlocks: readonly ts.ClassStaticBlockDeclaration[], startPos: number, endPos: number) { - for (const staticBlock of staticBlocks) { - // static block must be within the provided range as they are evaluated in document order (unlike constructors) - if (staticBlock.pos >= startPos && staticBlock.pos <= endPos) { - const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propName); - ts.setParent(reference.expression, reference); - ts.setParent(reference, staticBlock); - reference.flowNode = staticBlock.returnFlowNode; - const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); - if (!(getFalsyFlags(flowType) & ts.TypeFlags.Undefined)) { - return true; - } + function isPropertyInitializedInStaticBlocks(propName: ts.Identifier | ts.PrivateIdentifier, propType: ts.Type, staticBlocks: readonly ts.ClassStaticBlockDeclaration[], startPos: number, endPos: number) { + for (const staticBlock of staticBlocks) { + // static block must be within the provided range as they are evaluated in document order (unlike constructors) + if (staticBlock.pos >= startPos && staticBlock.pos <= endPos) { + const reference = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propName); + ts.setParent(reference.expression, reference); + ts.setParent(reference, staticBlock); + reference.flowNode = staticBlock.returnFlowNode; + const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); + if (!(getFalsyFlags(flowType) & ts.TypeFlags.Undefined)) { + return true; } } - return false; - } - - function isPropertyInitializedInConstructor(propName: ts.Identifier | ts.PrivateIdentifier | ts.ComputedPropertyName, propType: ts.Type, constructor: ts.ConstructorDeclaration) { - const reference = ts.isComputedPropertyName(propName) - ? ts.factory.createElementAccessExpression(ts.factory.createThis(), propName.expression) - : ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propName); - ts.setParent(reference.expression, reference); - ts.setParent(reference, constructor); - reference.flowNode = constructor.returnFlowNode; - const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); - return !(getFalsyFlags(flowType) & ts.TypeFlags.Undefined); } + return false; + } + function isPropertyInitializedInConstructor(propName: ts.Identifier | ts.PrivateIdentifier | ts.ComputedPropertyName, propType: ts.Type, constructor: ts.ConstructorDeclaration) { + const reference = ts.isComputedPropertyName(propName) + ? ts.factory.createElementAccessExpression(ts.factory.createThis(), propName.expression) + : ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propName); + ts.setParent(reference.expression, reference); + ts.setParent(reference, constructor); + reference.flowNode = constructor.returnFlowNode; + const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); + return !(getFalsyFlags(flowType) & ts.TypeFlags.Undefined); + } - function checkInterfaceDeclaration(node: ts.InterfaceDeclaration) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node)) - checkGrammarInterfaceDeclaration(node); - checkTypeParameters(node.typeParameters); - addLazyDiagnostic(() => { - checkTypeNameIsReserved(node.name, ts.Diagnostics.Interface_name_cannot_be_0); + function checkInterfaceDeclaration(node: ts.InterfaceDeclaration) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node)) + checkGrammarInterfaceDeclaration(node); - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - checkTypeParameterListsIdentical(symbol); + checkTypeParameters(node.typeParameters); + addLazyDiagnostic(() => { + checkTypeNameIsReserved(node.name, ts.Diagnostics.Interface_name_cannot_be_0); - // Only check this symbol once - const firstInterfaceDecl = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.InterfaceDeclaration); - if (node === firstInterfaceDecl) { - const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; - 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, ts.Diagnostics.Interface_0_incorrectly_extends_interface_1); - } - checkIndexConstraints(type, symbol); + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); + checkTypeParameterListsIdentical(symbol); + + // Only check this symbol once + const firstInterfaceDecl = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.InterfaceDeclaration); + if (node === firstInterfaceDecl) { + const type = getDeclaredTypeOfSymbol(symbol) as ts.InterfaceType; + 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, ts.Diagnostics.Interface_0_incorrectly_extends_interface_1); } + checkIndexConstraints(type, symbol); } - checkObjectTypeForDuplicateDeclarations(node); - }); - ts.forEach(ts.getInterfaceBaseTypeNodes(node), heritageElement => { - if (!ts.isEntityNameExpression(heritageElement.expression) || ts.isOptionalChain(heritageElement.expression)) { - error(heritageElement.expression, ts.Diagnostics.An_interface_can_only_extend_an_identifier_Slashqualified_name_with_optional_type_arguments); - } - checkTypeReferenceNode(heritageElement); - }); + } + checkObjectTypeForDuplicateDeclarations(node); + }); + ts.forEach(ts.getInterfaceBaseTypeNodes(node), heritageElement => { + if (!ts.isEntityNameExpression(heritageElement.expression) || ts.isOptionalChain(heritageElement.expression)) { + error(heritageElement.expression, ts.Diagnostics.An_interface_can_only_extend_an_identifier_Slashqualified_name_with_optional_type_arguments); + } + checkTypeReferenceNode(heritageElement); + }); - ts.forEach(node.members, checkSourceElement); + ts.forEach(node.members, checkSourceElement); - addLazyDiagnostic(() => { - checkTypeForDuplicateIndexSignatures(node); - registerForUnusedIdentifiersCheck(node); - }); - } + addLazyDiagnostic(() => { + checkTypeForDuplicateIndexSignatures(node); + registerForUnusedIdentifiersCheck(node); + }); + } - function checkTypeAliasDeclaration(node: ts.TypeAliasDeclaration) { - // Grammar checking - checkGrammarDecoratorsAndModifiers(node); - checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_alias_name_cannot_be_0); - checkExportsOnMergedDeclarations(node); - checkTypeParameters(node.typeParameters); - if (node.type.kind === ts.SyntaxKind.IntrinsicKeyword) { - if (!intrinsicTypeKinds.has(node.name.escapedText as string) || ts.length(node.typeParameters) !== 1) { - error(node.type, ts.Diagnostics.The_intrinsic_keyword_can_only_be_used_to_declare_compiler_provided_intrinsic_types); - } - } - else { - checkSourceElement(node.type); - registerForUnusedIdentifiersCheck(node); + function checkTypeAliasDeclaration(node: ts.TypeAliasDeclaration) { + // Grammar checking + checkGrammarDecoratorsAndModifiers(node); + checkTypeNameIsReserved(node.name, ts.Diagnostics.Type_alias_name_cannot_be_0); + checkExportsOnMergedDeclarations(node); + checkTypeParameters(node.typeParameters); + if (node.type.kind === ts.SyntaxKind.IntrinsicKeyword) { + if (!intrinsicTypeKinds.has(node.name.escapedText as string) || ts.length(node.typeParameters) !== 1) { + error(node.type, ts.Diagnostics.The_intrinsic_keyword_can_only_be_used_to_declare_compiler_provided_intrinsic_types); } } + else { + checkSourceElement(node.type); + registerForUnusedIdentifiersCheck(node); + } + } - function computeEnumMemberValues(node: ts.EnumDeclaration) { - const nodeLinks = getNodeLinks(node); - if (!(nodeLinks.flags & ts.NodeCheckFlags.EnumValuesComputed)) { - nodeLinks.flags |= ts.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 computeEnumMemberValues(node: ts.EnumDeclaration) { + const nodeLinks = getNodeLinks(node); + if (!(nodeLinks.flags & ts.NodeCheckFlags.EnumValuesComputed)) { + nodeLinks.flags |= ts.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 computeMemberValue(member: ts.EnumMember, autoValue: number | undefined) { - if (ts.isComputedNonLiteralName(member.name)) { - error(member.name, ts.Diagnostics.Computed_property_names_are_not_allowed_in_enums); - } - else { - const text = ts.getTextOfPropertyName(member.name); - if (ts.isNumericLiteralName(text) && !ts.isInfinityOrNaNString(text)) { - error(member.name, ts.Diagnostics.An_enum_member_cannot_have_a_numeric_name); - } - } - if (member.initializer) { - return computeConstantValue(member); - } - // 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 & ts.NodeFlags.Ambient && !ts.isEnumConst(member.parent) && getEnumKind(getSymbolOfNode(member.parent)) === ts.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; + function computeMemberValue(member: ts.EnumMember, autoValue: number | undefined) { + if (ts.isComputedNonLiteralName(member.name)) { + error(member.name, ts.Diagnostics.Computed_property_names_are_not_allowed_in_enums); + } + else { + const text = ts.getTextOfPropertyName(member.name); + if (ts.isNumericLiteralName(text) && !ts.isInfinityOrNaNString(text)) { + error(member.name, ts.Diagnostics.An_enum_member_cannot_have_a_numeric_name); } - error(member.name, ts.Diagnostics.Enum_member_must_have_initializer); + } + if (member.initializer) { + return computeConstantValue(member); + } + // 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 & ts.NodeFlags.Ambient && !ts.isEnumConst(member.parent) && getEnumKind(getSymbolOfNode(member.parent)) === ts.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, ts.Diagnostics.Enum_member_must_have_initializer); + return undefined; + } - function computeConstantValue(member: ts.EnumMember): string | number | undefined { - const enumKind = getEnumKind(getSymbolOfNode(member.parent)); - const isConstEnum = ts.isEnumConst(member.parent); - const initializer = member.initializer!; - const value = enumKind === ts.EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer); - if (value !== undefined) { - if (isConstEnum && typeof value === "number" && !isFinite(value)) { - error(initializer, isNaN(value) ? - ts.Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN : - ts.Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value); - } - } - else if (enumKind === ts.EnumKind.Literal) { - error(initializer, ts.Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members); - return 0; + function computeConstantValue(member: ts.EnumMember): string | number | undefined { + const enumKind = getEnumKind(getSymbolOfNode(member.parent)); + const isConstEnum = ts.isEnumConst(member.parent); + const initializer = member.initializer!; + const value = enumKind === ts.EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer); + if (value !== undefined) { + if (isConstEnum && typeof value === "number" && !isFinite(value)) { + error(initializer, isNaN(value) ? + ts.Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN : + ts.Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value); } - else if (isConstEnum) { - error(initializer, ts.Diagnostics.const_enum_member_initializers_can_only_contain_literal_values_and_other_computed_enum_values); - } - else if (member.parent.flags & ts.NodeFlags.Ambient) { - error(initializer, ts.Diagnostics.In_ambient_enum_declarations_member_initializer_must_be_constant_expression); + } + else if (enumKind === ts.EnumKind.Literal) { + error(initializer, ts.Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members); + return 0; + } + else if (isConstEnum) { + error(initializer, ts.Diagnostics.const_enum_member_initializers_can_only_contain_literal_values_and_other_computed_enum_values); + } + else if (member.parent.flags & ts.NodeFlags.Ambient) { + error(initializer, ts.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. + const source = checkExpression(initializer); + if (!isTypeAssignableToKind(source, ts.TypeFlags.NumberLike)) { + error(initializer, ts.Diagnostics.Only_numeric_enums_can_have_computed_members_but_this_expression_has_type_0_If_you_do_not_need_exhaustiveness_checks_consider_using_an_object_literal_instead, typeToString(source)); } else { - // Only here do we need to check that the initializer is assignable to the enum type. - const source = checkExpression(initializer); - if (!isTypeAssignableToKind(source, ts.TypeFlags.NumberLike)) { - error(initializer, ts.Diagnostics.Only_numeric_enums_can_have_computed_members_but_this_expression_has_type_0_If_you_do_not_need_exhaustiveness_checks_consider_using_an_object_literal_instead, typeToString(source)); - } - else { - checkTypeAssignableTo(source, getDeclaredTypeOfSymbol(getSymbolOfNode(member.parent)), initializer, /*headMessage*/ undefined); - } + checkTypeAssignableTo(source, getDeclaredTypeOfSymbol(getSymbolOfNode(member.parent)), initializer, /*headMessage*/ undefined); } - return value; + } + return value; - function evaluate(expr: ts.Expression): string | number | undefined { - switch (expr.kind) { - case ts.SyntaxKind.PrefixUnaryExpression: - const value = evaluate((expr as ts.PrefixUnaryExpression).operand); - if (typeof value === "number") { - switch ((expr as ts.PrefixUnaryExpression).operator) { - case ts.SyntaxKind.PlusToken: return value; - case ts.SyntaxKind.MinusToken: return -value; - case ts.SyntaxKind.TildeToken: return ~value; - } + function evaluate(expr: ts.Expression): string | number | undefined { + switch (expr.kind) { + case ts.SyntaxKind.PrefixUnaryExpression: + const value = evaluate((expr as ts.PrefixUnaryExpression).operand); + if (typeof value === "number") { + switch ((expr as ts.PrefixUnaryExpression).operator) { + case ts.SyntaxKind.PlusToken: return value; + case ts.SyntaxKind.MinusToken: return -value; + case ts.SyntaxKind.TildeToken: return ~value; } - break; - case ts.SyntaxKind.BinaryExpression: - const left = evaluate((expr as ts.BinaryExpression).left); - const right = evaluate((expr as ts.BinaryExpression).right); - if (typeof left === "number" && typeof right === "number") { - switch ((expr as ts.BinaryExpression).operatorToken.kind) { - case ts.SyntaxKind.BarToken: return left | right; - case ts.SyntaxKind.AmpersandToken: return left & right; - case ts.SyntaxKind.GreaterThanGreaterThanToken: return left >> right; - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return left >>> right; - case ts.SyntaxKind.LessThanLessThanToken: return left << right; - case ts.SyntaxKind.CaretToken: return left ^ right; - case ts.SyntaxKind.AsteriskToken: return left * right; - case ts.SyntaxKind.SlashToken: return left / right; - case ts.SyntaxKind.PlusToken: return left + right; - case ts.SyntaxKind.MinusToken: return left - right; - case ts.SyntaxKind.PercentToken: return left % right; - case ts.SyntaxKind.AsteriskAsteriskToken: return left ** right; + } + break; + case ts.SyntaxKind.BinaryExpression: + const left = evaluate((expr as ts.BinaryExpression).left); + const right = evaluate((expr as ts.BinaryExpression).right); + if (typeof left === "number" && typeof right === "number") { + switch ((expr as ts.BinaryExpression).operatorToken.kind) { + case ts.SyntaxKind.BarToken: return left | right; + case ts.SyntaxKind.AmpersandToken: return left & right; + case ts.SyntaxKind.GreaterThanGreaterThanToken: return left >> right; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return left >>> right; + case ts.SyntaxKind.LessThanLessThanToken: return left << right; + case ts.SyntaxKind.CaretToken: return left ^ right; + case ts.SyntaxKind.AsteriskToken: return left * right; + case ts.SyntaxKind.SlashToken: return left / right; + case ts.SyntaxKind.PlusToken: return left + right; + case ts.SyntaxKind.MinusToken: return left - right; + case ts.SyntaxKind.PercentToken: return left % right; + case ts.SyntaxKind.AsteriskAsteriskToken: return left ** right; + } + } + else if (typeof left === "string" && typeof right === "string" && (expr as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.PlusToken) { + return left + right; + } + break; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return (expr as ts.StringLiteralLike).text; + case ts.SyntaxKind.NumericLiteral: + checkGrammarNumericLiteral(expr as ts.NumericLiteral); + return +(expr as ts.NumericLiteral).text; + case ts.SyntaxKind.ParenthesizedExpression: + return evaluate((expr as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.Identifier: + const identifier = expr as ts.Identifier; + if (ts.isInfinityOrNaNString(identifier.escapedText)) { + return +(identifier.escapedText); + } + return ts.nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), identifier.escapedText); + case ts.SyntaxKind.ElementAccessExpression: + case ts.SyntaxKind.PropertyAccessExpression: + if (isConstantMemberAccess(expr)) { + const type = getTypeOfExpression(expr.expression); + if (type.symbol && type.symbol.flags & ts.SymbolFlags.Enum) { + let name: ts.__String; + if (expr.kind === ts.SyntaxKind.PropertyAccessExpression) { + name = expr.name.escapedText; } - } - else if (typeof left === "string" && typeof right === "string" && (expr as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.PlusToken) { - return left + right; - } - break; - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return (expr as ts.StringLiteralLike).text; - case ts.SyntaxKind.NumericLiteral: - checkGrammarNumericLiteral(expr as ts.NumericLiteral); - return +(expr as ts.NumericLiteral).text; - case ts.SyntaxKind.ParenthesizedExpression: - return evaluate((expr as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.Identifier: - const identifier = expr as ts.Identifier; - if (ts.isInfinityOrNaNString(identifier.escapedText)) { - return +(identifier.escapedText); - } - return ts.nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), identifier.escapedText); - case ts.SyntaxKind.ElementAccessExpression: - case ts.SyntaxKind.PropertyAccessExpression: - if (isConstantMemberAccess(expr)) { - const type = getTypeOfExpression(expr.expression); - if (type.symbol && type.symbol.flags & ts.SymbolFlags.Enum) { - let name: ts.__String; - if (expr.kind === ts.SyntaxKind.PropertyAccessExpression) { - name = expr.name.escapedText; - } - else { - name = ts.escapeLeadingUnderscores(ts.cast(expr.argumentExpression, ts.isLiteralExpression).text); - } - return evaluateEnumMember(expr, type.symbol, name); + else { + name = ts.escapeLeadingUnderscores(ts.cast(expr.argumentExpression, ts.isLiteralExpression).text); } + return evaluateEnumMember(expr, type.symbol, name); } - break; - } - return undefined; + } + break; } + return undefined; + } - function evaluateEnumMember(expr: ts.Expression, enumSymbol: ts.Symbol, name: ts.__String) { - const memberSymbol = enumSymbol.exports!.get(name); - if (memberSymbol) { - const declaration = memberSymbol.valueDeclaration; - if (declaration !== member) { - if (declaration && isBlockScopedNameDeclaredBeforeUse(declaration, member) && ts.isEnumDeclaration(declaration.parent)) { - return getEnumMemberValue(declaration as ts.EnumMember); - } - error(expr, ts.Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums); - return 0; - } - else { - error(expr, ts.Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(memberSymbol)); + function evaluateEnumMember(expr: ts.Expression, enumSymbol: ts.Symbol, name: ts.__String) { + const memberSymbol = enumSymbol.exports!.get(name); + if (memberSymbol) { + const declaration = memberSymbol.valueDeclaration; + if (declaration !== member) { + if (declaration && isBlockScopedNameDeclaredBeforeUse(declaration, member) && ts.isEnumDeclaration(declaration.parent)) { + return getEnumMemberValue(declaration as ts.EnumMember); } + error(expr, ts.Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums); + return 0; + } + else { + error(expr, ts.Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(memberSymbol)); } - return undefined; } + return undefined; } + } - function isConstantMemberAccess(node: ts.Expression): node is ts.AccessExpression { - const type = getTypeOfExpression(node); - if (type === errorType) { - return false; - } - - return node.kind === ts.SyntaxKind.Identifier || - node.kind === ts.SyntaxKind.PropertyAccessExpression && isConstantMemberAccess((node as ts.PropertyAccessExpression).expression) || - node.kind === ts.SyntaxKind.ElementAccessExpression && isConstantMemberAccess((node as ts.ElementAccessExpression).expression) && - ts.isStringLiteralLike((node as ts.ElementAccessExpression).argumentExpression); + function isConstantMemberAccess(node: ts.Expression): node is ts.AccessExpression { + const type = getTypeOfExpression(node); + if (type === errorType) { + return false; } - function checkEnumDeclaration(node: ts.EnumDeclaration) { - addLazyDiagnostic(() => checkEnumDeclarationWorker(node)); - } + return node.kind === ts.SyntaxKind.Identifier || + node.kind === ts.SyntaxKind.PropertyAccessExpression && isConstantMemberAccess((node as ts.PropertyAccessExpression).expression) || + node.kind === ts.SyntaxKind.ElementAccessExpression && isConstantMemberAccess((node as ts.ElementAccessExpression).expression) && + ts.isStringLiteralLike((node as ts.ElementAccessExpression).argumentExpression); + } - function checkEnumDeclarationWorker(node: ts.EnumDeclaration) { - // Grammar checking - checkGrammarDecoratorsAndModifiers(node); + function checkEnumDeclaration(node: ts.EnumDeclaration) { + addLazyDiagnostic(() => checkEnumDeclarationWorker(node)); + } - checkCollisionsForDeclarationName(node, node.name); - checkExportsOnMergedDeclarations(node); - node.members.forEach(checkEnumMember); + function checkEnumDeclarationWorker(node: ts.EnumDeclaration) { + // Grammar checking + checkGrammarDecoratorsAndModifiers(node); - computeEnumMemberValues(node); + checkCollisionsForDeclarationName(node, node.name); + checkExportsOnMergedDeclarations(node); + node.members.forEach(checkEnumMember); - // 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 = ts.getDeclarationOfKind(enumSymbol, node.kind); - if (node === firstDeclaration) { - if (enumSymbol.declarations && enumSymbol.declarations.length > 1) { - const enumIsConst = ts.isEnumConst(node); - // check that const is placed\omitted on all enum declarations - ts.forEach(enumSymbol.declarations, decl => { - if (ts.isEnumDeclaration(decl) && ts.isEnumConst(decl) !== enumIsConst) { - error(ts.getNameOfDeclaration(decl), ts.Diagnostics.Enum_declarations_must_all_be_const_or_non_const); - } - }); - } + computeEnumMemberValues(node); - let seenEnumMissingInitialInitializer = false; - ts.forEach(enumSymbol.declarations, declaration => { - // return true if we hit a violation of the rule, false otherwise - if (declaration.kind !== ts.SyntaxKind.EnumDeclaration) { - return false; + // 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 = ts.getDeclarationOfKind(enumSymbol, node.kind); + if (node === firstDeclaration) { + if (enumSymbol.declarations && enumSymbol.declarations.length > 1) { + const enumIsConst = ts.isEnumConst(node); + // check that const is placed\omitted on all enum declarations + ts.forEach(enumSymbol.declarations, decl => { + if (ts.isEnumDeclaration(decl) && ts.isEnumConst(decl) !== enumIsConst) { + error(ts.getNameOfDeclaration(decl), ts.Diagnostics.Enum_declarations_must_all_be_const_or_non_const); } + }); + } - const enumDeclaration = declaration as ts.EnumDeclaration; - if (!enumDeclaration.members.length) { - return false; - } + let seenEnumMissingInitialInitializer = false; + ts.forEach(enumSymbol.declarations, declaration => { + // return true if we hit a violation of the rule, false otherwise + if (declaration.kind !== ts.SyntaxKind.EnumDeclaration) { + return false; + } - const firstEnumMember = enumDeclaration.members[0]; - if (!firstEnumMember.initializer) { - if (seenEnumMissingInitialInitializer) { - error(firstEnumMember.name, ts.Diagnostics.In_an_enum_with_multiple_declarations_only_one_declaration_can_omit_an_initializer_for_its_first_enum_element); - } - else { - seenEnumMissingInitialInitializer = true; - } + const enumDeclaration = declaration as ts.EnumDeclaration; + if (!enumDeclaration.members.length) { + return false; + } + + const firstEnumMember = enumDeclaration.members[0]; + if (!firstEnumMember.initializer) { + if (seenEnumMissingInitialInitializer) { + error(firstEnumMember.name, ts.Diagnostics.In_an_enum_with_multiple_declarations_only_one_declaration_can_omit_an_initializer_for_its_first_enum_element); } - }); - } + else { + seenEnumMissingInitialInitializer = true; + } + } + }); } + } - function checkEnumMember(node: ts.EnumMember) { - if (ts.isPrivateIdentifier(node.name)) { - error(node, ts.Diagnostics.An_enum_member_cannot_be_named_with_a_private_identifier); - } + function checkEnumMember(node: ts.EnumMember) { + if (ts.isPrivateIdentifier(node.name)) { + error(node, ts.Diagnostics.An_enum_member_cannot_be_named_with_a_private_identifier); } + } - function getFirstNonAmbientClassOrFunctionDeclaration(symbol: ts.Symbol): ts.Declaration | undefined { - const declarations = symbol.declarations; - if (declarations) { - for (const declaration of declarations) { - if ((declaration.kind === ts.SyntaxKind.ClassDeclaration || - (declaration.kind === ts.SyntaxKind.FunctionDeclaration && ts.nodeIsPresent((declaration as ts.FunctionLikeDeclaration).body))) && - !(declaration.flags & ts.NodeFlags.Ambient)) { - return declaration; - } + function getFirstNonAmbientClassOrFunctionDeclaration(symbol: ts.Symbol): ts.Declaration | undefined { + const declarations = symbol.declarations; + if (declarations) { + for (const declaration of declarations) { + if ((declaration.kind === ts.SyntaxKind.ClassDeclaration || + (declaration.kind === ts.SyntaxKind.FunctionDeclaration && ts.nodeIsPresent((declaration as ts.FunctionLikeDeclaration).body))) && + !(declaration.flags & ts.NodeFlags.Ambient)) { + return declaration; } } - return undefined; } + return undefined; + } - function inSameLexicalScope(node1: ts.Node, node2: ts.Node) { - const container1 = ts.getEnclosingBlockScopeContainer(node1); - const container2 = ts.getEnclosingBlockScopeContainer(node2); - if (isGlobalSourceFile(container1)) { - return isGlobalSourceFile(container2); - } - else if (isGlobalSourceFile(container2)) { - return false; - } - else { - return container1 === container2; - } + function inSameLexicalScope(node1: ts.Node, node2: ts.Node) { + const container1 = ts.getEnclosingBlockScopeContainer(node1); + const container2 = ts.getEnclosingBlockScopeContainer(node2); + if (isGlobalSourceFile(container1)) { + return isGlobalSourceFile(container2); + } + else if (isGlobalSourceFile(container2)) { + return false; } + else { + return container1 === container2; + } + } - function checkModuleDeclaration(node: ts.ModuleDeclaration) { - if (node.body) { - checkSourceElement(node.body); - if (!ts.isGlobalScopeAugmentation(node)) { - registerForUnusedIdentifiersCheck(node); - } + function checkModuleDeclaration(node: ts.ModuleDeclaration) { + if (node.body) { + checkSourceElement(node.body); + if (!ts.isGlobalScopeAugmentation(node)) { + registerForUnusedIdentifiersCheck(node); } + } - addLazyDiagnostic(checkModuleDeclarationDiagnostics); + addLazyDiagnostic(checkModuleDeclarationDiagnostics); - function checkModuleDeclarationDiagnostics() { - // Grammar checking - const isGlobalAugmentation = ts.isGlobalScopeAugmentation(node); - const inAmbientContext = node.flags & ts.NodeFlags.Ambient; - if (isGlobalAugmentation && !inAmbientContext) { - error(node.name, ts.Diagnostics.Augmentations_for_the_global_scope_should_have_declare_modifier_unless_they_appear_in_already_ambient_context); - } + function checkModuleDeclarationDiagnostics() { + // Grammar checking + const isGlobalAugmentation = ts.isGlobalScopeAugmentation(node); + const inAmbientContext = node.flags & ts.NodeFlags.Ambient; + if (isGlobalAugmentation && !inAmbientContext) { + error(node.name, ts.Diagnostics.Augmentations_for_the_global_scope_should_have_declare_modifier_unless_they_appear_in_already_ambient_context); + } + + const isAmbientExternalModule: boolean = ts.isAmbientModule(node); + const contextErrorMessage = isAmbientExternalModule + ? ts.Diagnostics.An_ambient_module_declaration_is_only_allowed_at_the_top_level_in_a_file + : ts.Diagnostics.A_namespace_declaration_is_only_allowed_at_the_top_level_of_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 isAmbientExternalModule: boolean = ts.isAmbientModule(node); - const contextErrorMessage = isAmbientExternalModule - ? ts.Diagnostics.An_ambient_module_declaration_is_only_allowed_at_the_top_level_in_a_file - : ts.Diagnostics.A_namespace_declaration_is_only_allowed_at_the_top_level_of_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 === ts.SyntaxKind.StringLiteral) { + grammarErrorOnNode(node.name, ts.Diagnostics.Only_ambient_modules_can_use_quoted_names); } + } + + if (ts.isIdentifier(node.name)) { + checkCollisionsForDeclarationName(node, node.name); + } + + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); - if (!checkGrammarDecoratorsAndModifiers(node)) { - if (!inAmbientContext && node.name.kind === ts.SyntaxKind.StringLiteral) { - grammarErrorOnNode(node.name, ts.Diagnostics.Only_ambient_modules_can_use_quoted_names); + // The following checks only apply on a non-ambient instantiated module declaration. + if (symbol.flags & ts.SymbolFlags.ValueModule + && !inAmbientContext + && symbol.declarations + && symbol.declarations.length > 1 + && isInstantiatedModule(node, ts.shouldPreserveConstEnums(compilerOptions))) { + const firstNonAmbientClassOrFunc = getFirstNonAmbientClassOrFunctionDeclaration(symbol); + if (firstNonAmbientClassOrFunc) { + if (ts.getSourceFileOfNode(node) !== ts.getSourceFileOfNode(firstNonAmbientClassOrFunc)) { + error(node.name, ts.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, ts.Diagnostics.A_namespace_declaration_cannot_be_located_prior_to_a_class_or_function_with_which_it_is_merged); } } - if (ts.isIdentifier(node.name)) { - checkCollisionsForDeclarationName(node, node.name); + // 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 = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ClassDeclaration); + if (mergedClass && + inSameLexicalScope(node, mergedClass)) { + getNodeLinks(node).flags |= ts.NodeCheckFlags.LexicalModuleMergesWithClass; } + } - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - - // The following checks only apply on a non-ambient instantiated module declaration. - if (symbol.flags & ts.SymbolFlags.ValueModule - && !inAmbientContext - && symbol.declarations - && symbol.declarations.length > 1 - && isInstantiatedModule(node, ts.shouldPreserveConstEnums(compilerOptions))) { - const firstNonAmbientClassOrFunc = getFirstNonAmbientClassOrFunctionDeclaration(symbol); - if (firstNonAmbientClassOrFunc) { - if (ts.getSourceFileOfNode(node) !== ts.getSourceFileOfNode(firstNonAmbientClassOrFunc)) { - error(node.name, ts.Diagnostics.A_namespace_declaration_cannot_be_in_a_different_file_from_a_class_or_function_with_which_it_is_merged); + if (isAmbientExternalModule) { + if (ts.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 & ts.SymbolFlags.Transient); + if (checkBody && node.body) { + for (const statement of node.body.statements) { + checkModuleAugmentationElement(statement, isGlobalAugmentation); } - else if (node.pos < firstNonAmbientClassOrFunc.pos) { - error(node.name, ts.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 = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ClassDeclaration); - if (mergedClass && - inSameLexicalScope(node, mergedClass)) { - getNodeLinks(node).flags |= ts.NodeCheckFlags.LexicalModuleMergesWithClass; } } - - if (isAmbientExternalModule) { - if (ts.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 & ts.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, ts.Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); } - else if (isGlobalSourceFile(node.parent)) { - if (isGlobalAugmentation) { - error(node.name, ts.Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); - } - else if (ts.isExternalModuleNameRelative(ts.getTextOfIdentifierOrLiteral(node.name))) { - error(node.name, ts.Diagnostics.Ambient_module_declaration_cannot_specify_relative_module_name); - } + else if (ts.isExternalModuleNameRelative(ts.getTextOfIdentifierOrLiteral(node.name))) { + error(node.name, ts.Diagnostics.Ambient_module_declaration_cannot_specify_relative_module_name); + } + } + else { + if (isGlobalAugmentation) { + error(node.name, ts.Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); } else { - if (isGlobalAugmentation) { - error(node.name, ts.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, ts.Diagnostics.Ambient_modules_cannot_be_nested_in_other_modules_or_namespaces); - } + // 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, ts.Diagnostics.Ambient_modules_cannot_be_nested_in_other_modules_or_namespaces); } } } } + } - function checkModuleAugmentationElement(node: ts.Node, isGlobalAugmentation: boolean): void { - switch (node.kind) { - case ts.SyntaxKind.VariableStatement: - // error each individual name in variable statement instead of marking the entire variable statement - for (const decl of (node as ts.VariableStatement).declarationList.declarations) { - checkModuleAugmentationElement(decl, isGlobalAugmentation); + function checkModuleAugmentationElement(node: ts.Node, isGlobalAugmentation: boolean): void { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + // error each individual name in variable statement instead of marking the entire variable statement + for (const decl of (node as ts.VariableStatement).declarationList.declarations) { + checkModuleAugmentationElement(decl, isGlobalAugmentation); + } + break; + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.ExportDeclaration: + grammarErrorOnFirstToken(node, ts.Diagnostics.Exports_and_export_assignments_are_not_permitted_in_module_augmentations); + break; + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportDeclaration: + grammarErrorOnFirstToken(node, ts.Diagnostics.Imports_are_not_permitted_in_module_augmentations_Consider_moving_them_to_the_enclosing_external_module); + break; + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.VariableDeclaration: + const name = (node as ts.VariableDeclaration | ts.BindingElement).name; + if (ts.isBindingPattern(name)) { + for (const el of name.elements) { + // mark individual names in binding pattern + checkModuleAugmentationElement(el, isGlobalAugmentation); } break; - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.ExportDeclaration: - grammarErrorOnFirstToken(node, ts.Diagnostics.Exports_and_export_assignments_are_not_permitted_in_module_augmentations); - break; - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportDeclaration: - grammarErrorOnFirstToken(node, ts.Diagnostics.Imports_are_not_permitted_in_module_augmentations_Consider_moving_them_to_the_enclosing_external_module); - break; - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.VariableDeclaration: - const name = (node as ts.VariableDeclaration | ts.BindingElement).name; - if (ts.isBindingPattern(name)) { - for (const el of name.elements) { - // mark individual names in binding pattern - checkModuleAugmentationElement(el, isGlobalAugmentation); - } - break; - } - // falls through - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - if (isGlobalAugmentation) { - return; + } + // falls through + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + if (isGlobalAugmentation) { + return; + } + 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 & ts.SymbolFlags.Transient); + if (!reportError) { + // symbol should not originate in augmentation + reportError = !!symbol.parent?.declarations && ts.isExternalModuleAugmentation(symbol.parent.declarations[0]); } - 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 & ts.SymbolFlags.Transient); - if (!reportError) { - // symbol should not originate in augmentation - reportError = !!symbol.parent?.declarations && ts.isExternalModuleAugmentation(symbol.parent.declarations[0]); - } + } + break; + } + } + + function getFirstNonModuleExportsIdentifier(node: ts.EntityNameOrEntityNameExpression): ts.Identifier { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return node; + case ts.SyntaxKind.QualifiedName: + do { + node = node.left; + } while (node.kind !== ts.SyntaxKind.Identifier); + return node; + case ts.SyntaxKind.PropertyAccessExpression: + do { + if (ts.isModuleExportsAccessExpression(node.expression) && !ts.isPrivateIdentifier(node.name)) { + return node.name; } - break; - } + node = node.expression; + } while (node.kind !== ts.SyntaxKind.Identifier); + return node; } + } - function getFirstNonModuleExportsIdentifier(node: ts.EntityNameOrEntityNameExpression): ts.Identifier { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return node; - case ts.SyntaxKind.QualifiedName: - do { - node = node.left; - } while (node.kind !== ts.SyntaxKind.Identifier); - return node; - case ts.SyntaxKind.PropertyAccessExpression: - do { - if (ts.isModuleExportsAccessExpression(node.expression) && !ts.isPrivateIdentifier(node.name)) { - return node.name; - } - node = node.expression; - } while (node.kind !== ts.SyntaxKind.Identifier); - return node; - } + function checkExternalImportOrExportDeclaration(node: ts.ImportDeclaration | ts.ImportEqualsDeclaration | ts.ExportDeclaration): boolean { + const moduleName = ts.getExternalModuleName(node); + if (!moduleName || ts.nodeIsMissing(moduleName)) { + // Should be a parse error. + return false; + } + if (!ts.isStringLiteral(moduleName)) { + error(moduleName, ts.Diagnostics.String_literal_expected); + return false; + } + const inAmbientExternalModule = node.parent.kind === ts.SyntaxKind.ModuleBlock && ts.isAmbientModule(node.parent.parent); + if (node.parent.kind !== ts.SyntaxKind.SourceFile && !inAmbientExternalModule) { + error(moduleName, node.kind === ts.SyntaxKind.ExportDeclaration ? + ts.Diagnostics.Export_declarations_are_not_permitted_in_a_namespace : + ts.Diagnostics.Import_declarations_in_a_namespace_cannot_reference_a_module); + return false; } - - function checkExternalImportOrExportDeclaration(node: ts.ImportDeclaration | ts.ImportEqualsDeclaration | ts.ExportDeclaration): boolean { - const moduleName = ts.getExternalModuleName(node); - if (!moduleName || ts.nodeIsMissing(moduleName)) { - // Should be a parse error. - return false; - } - if (!ts.isStringLiteral(moduleName)) { - error(moduleName, ts.Diagnostics.String_literal_expected); - return false; - } - const inAmbientExternalModule = node.parent.kind === ts.SyntaxKind.ModuleBlock && ts.isAmbientModule(node.parent.parent); - if (node.parent.kind !== ts.SyntaxKind.SourceFile && !inAmbientExternalModule) { - error(moduleName, node.kind === ts.SyntaxKind.ExportDeclaration ? - ts.Diagnostics.Export_declarations_are_not_permitted_in_a_namespace : - ts.Diagnostics.Import_declarations_in_a_namespace_cannot_reference_a_module); + if (inAmbientExternalModule && ts.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, ts.Diagnostics.Import_or_export_declaration_in_an_ambient_module_declaration_cannot_reference_module_through_relative_module_name); return false; } - if (inAmbientExternalModule && ts.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, ts.Diagnostics.Import_or_export_declaration_in_an_ambient_module_declaration_cannot_reference_module_through_relative_module_name); - return false; - } - } - if (!ts.isImportEqualsDeclaration(node) && node.assertClause) { - let hasError = false; - for (const clause of node.assertClause.elements) { - if (!ts.isStringLiteral(clause.value)) { - hasError = true; - error(clause.value, ts.Diagnostics.Import_assertion_values_must_be_string_literal_expressions); - } + } + if (!ts.isImportEqualsDeclaration(node) && node.assertClause) { + let hasError = false; + for (const clause of node.assertClause.elements) { + if (!ts.isStringLiteral(clause.value)) { + hasError = true; + error(clause.value, ts.Diagnostics.Import_assertion_values_must_be_string_literal_expressions); } - return !hasError; } - return true; + return !hasError; } + return true; + } - function checkAliasSymbol(node: ts.ImportEqualsDeclaration | ts.VariableDeclaration | ts.ImportClause | ts.NamespaceImport | ts.ImportSpecifier | ts.ExportSpecifier | ts.NamespaceExport) { - let symbol = getSymbolOfNode(node); - const target = resolveAlias(symbol); - - if (target !== unknownSymbol) { - // For external modules, `symbol` represents the 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 & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue) ? ts.SymbolFlags.Value : 0) | - (symbol.flags & ts.SymbolFlags.Type ? ts.SymbolFlags.Type : 0) | - (symbol.flags & ts.SymbolFlags.Namespace ? ts.SymbolFlags.Namespace : 0); - if (target.flags & excludedMeanings) { - const message = node.kind === ts.SyntaxKind.ExportSpecifier ? - ts.Diagnostics.Export_declaration_conflicts_with_exported_declaration_of_0 : - ts.Diagnostics.Import_declaration_conflicts_with_local_declaration_of_0; - error(node, message, symbolToString(symbol)); - } - - if (compilerOptions.isolatedModules - && !ts.isTypeOnlyImportOrExportDeclaration(node) - && !(node.flags & ts.NodeFlags.Ambient)) { - const typeOnlyAlias = getTypeOnlyAliasDeclaration(symbol); - const isType = !(target.flags & ts.SymbolFlags.Value); - if (isType || typeOnlyAlias) { - switch (node.kind) { - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ImportEqualsDeclaration: { - if (compilerOptions.preserveValueImports) { - ts.Debug.assertIsDefined(node.name, "An ImportClause with a symbol should have a name"); - const message = isType - ? ts.Diagnostics._0_is_a_type_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled - : ts.Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled; - const name = ts.idText(node.kind === ts.SyntaxKind.ImportSpecifier ? node.propertyName || node.name : node.name); - addTypeOnlyDeclarationRelatedInfo(error(node, message, name), isType ? undefined : typeOnlyAlias, name); - } - if (isType && node.kind === ts.SyntaxKind.ImportEqualsDeclaration && ts.hasEffectiveModifier(node, ts.ModifierFlags.Export)) { - error(node, ts.Diagnostics.Cannot_use_export_import_on_a_type_or_type_only_namespace_when_the_isolatedModules_flag_is_provided); - } - break; + function checkAliasSymbol(node: ts.ImportEqualsDeclaration | ts.VariableDeclaration | ts.ImportClause | ts.NamespaceImport | ts.ImportSpecifier | ts.ExportSpecifier | ts.NamespaceExport) { + let symbol = getSymbolOfNode(node); + const target = resolveAlias(symbol); + + if (target !== unknownSymbol) { + // For external modules, `symbol` represents the 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 & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue) ? ts.SymbolFlags.Value : 0) | + (symbol.flags & ts.SymbolFlags.Type ? ts.SymbolFlags.Type : 0) | + (symbol.flags & ts.SymbolFlags.Namespace ? ts.SymbolFlags.Namespace : 0); + if (target.flags & excludedMeanings) { + const message = node.kind === ts.SyntaxKind.ExportSpecifier ? + ts.Diagnostics.Export_declaration_conflicts_with_exported_declaration_of_0 : + ts.Diagnostics.Import_declaration_conflicts_with_local_declaration_of_0; + error(node, message, symbolToString(symbol)); + } + + if (compilerOptions.isolatedModules + && !ts.isTypeOnlyImportOrExportDeclaration(node) + && !(node.flags & ts.NodeFlags.Ambient)) { + const typeOnlyAlias = getTypeOnlyAliasDeclaration(symbol); + const isType = !(target.flags & ts.SymbolFlags.Value); + if (isType || typeOnlyAlias) { + switch (node.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ImportEqualsDeclaration: { + if (compilerOptions.preserveValueImports) { + ts.Debug.assertIsDefined(node.name, "An ImportClause with a symbol should have a name"); + const message = isType + ? ts.Diagnostics._0_is_a_type_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled + : ts.Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled; + const name = ts.idText(node.kind === ts.SyntaxKind.ImportSpecifier ? node.propertyName || node.name : node.name); + addTypeOnlyDeclarationRelatedInfo(error(node, message, name), isType ? undefined : typeOnlyAlias, name); + } + if (isType && node.kind === ts.SyntaxKind.ImportEqualsDeclaration && ts.hasEffectiveModifier(node, ts.ModifierFlags.Export)) { + error(node, ts.Diagnostics.Cannot_use_export_import_on_a_type_or_type_only_namespace_when_the_isolatedModules_flag_is_provided); } - case ts.SyntaxKind.ExportSpecifier: { - // Don't allow re-exporting an export that will be elided when `--isolatedModules` is set. - // The exception is that `import type { A } from './a'; export { A }` is allowed - // because single-file analysis can determine that the export should be dropped. - if (ts.getSourceFileOfNode(typeOnlyAlias) !== ts.getSourceFileOfNode(node)) { - const message = isType - ? ts.Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type - : ts.Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_using_a_type_only_re_export_when_isolatedModules_is_enabled; - const name = ts.idText(node.propertyName || node.name); - addTypeOnlyDeclarationRelatedInfo(error(node, message, name), isType ? undefined : typeOnlyAlias, name); - return; - } + break; + } + case ts.SyntaxKind.ExportSpecifier: { + // Don't allow re-exporting an export that will be elided when `--isolatedModules` is set. + // The exception is that `import type { A } from './a'; export { A }` is allowed + // because single-file analysis can determine that the export should be dropped. + if (ts.getSourceFileOfNode(typeOnlyAlias) !== ts.getSourceFileOfNode(node)) { + const message = isType + ? ts.Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type + : ts.Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_using_a_type_only_re_export_when_isolatedModules_is_enabled; + const name = ts.idText(node.propertyName || node.name); + addTypeOnlyDeclarationRelatedInfo(error(node, message, name), isType ? undefined : typeOnlyAlias, name); + return; } } } } + } - if (ts.isImportSpecifier(node)) { - const targetSymbol = checkDeprecatedAliasedSymbol(symbol, node); - if (isDeprecatedAliasedSymbol(targetSymbol) && targetSymbol.declarations) { - addDeprecatedSuggestion(node, targetSymbol.declarations, targetSymbol.escapedName as string); - } + if (ts.isImportSpecifier(node)) { + const targetSymbol = checkDeprecatedAliasedSymbol(symbol, node); + if (isDeprecatedAliasedSymbol(targetSymbol) && targetSymbol.declarations) { + addDeprecatedSuggestion(node, targetSymbol.declarations, targetSymbol.escapedName as string); } } } + } - function isDeprecatedAliasedSymbol(symbol: ts.Symbol) { - return !!symbol.declarations && ts.every(symbol.declarations, d => !!(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Deprecated)); - } + function isDeprecatedAliasedSymbol(symbol: ts.Symbol) { + return !!symbol.declarations && ts.every(symbol.declarations, d => !!(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Deprecated)); + } - function checkDeprecatedAliasedSymbol(symbol: ts.Symbol, location: ts.Node) { - if (!(symbol.flags & ts.SymbolFlags.Alias)) - return symbol; + function checkDeprecatedAliasedSymbol(symbol: ts.Symbol, location: ts.Node) { + if (!(symbol.flags & ts.SymbolFlags.Alias)) + return symbol; - const targetSymbol = resolveAlias(symbol); - if (targetSymbol === unknownSymbol) - return targetSymbol; - while (symbol.flags & ts.SymbolFlags.Alias) { - const target = getImmediateAliasedSymbol(symbol); - if (target) { - if (target === targetSymbol) + const targetSymbol = resolveAlias(symbol); + if (targetSymbol === unknownSymbol) + return targetSymbol; + while (symbol.flags & ts.SymbolFlags.Alias) { + const target = getImmediateAliasedSymbol(symbol); + if (target) { + if (target === targetSymbol) + break; + if (target.declarations && ts.length(target.declarations)) { + if (isDeprecatedAliasedSymbol(target)) { + addDeprecatedSuggestion(location, target.declarations, target.escapedName as string); break; - if (target.declarations && ts.length(target.declarations)) { - if (isDeprecatedAliasedSymbol(target)) { - addDeprecatedSuggestion(location, target.declarations, target.escapedName as string); + } + else { + if (symbol === targetSymbol) break; - } - else { - if (symbol === targetSymbol) - break; - symbol = target; - } + symbol = target; } } - else { - break; - } } - return targetSymbol; - } - - function checkImportBinding(node: ts.ImportEqualsDeclaration | ts.ImportClause | ts.NamespaceImport | ts.ImportSpecifier) { - checkCollisionsForDeclarationName(node, node.name); - checkAliasSymbol(node); - if (node.kind === ts.SyntaxKind.ImportSpecifier && - ts.idText(node.propertyName || node.name) === "default" && - ts.getESModuleInterop(compilerOptions) && - moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportDefault); + else { + break; } } + return targetSymbol; + } - function checkAssertClause(declaration: ts.ImportDeclaration | ts.ExportDeclaration) { - if (declaration.assertClause) { - const validForTypeAssertions = ts.isExclusivelyTypeOnlyImportOrExport(declaration); - const override = ts.getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined); - if (validForTypeAssertions && override) { - if (!ts.isNightly()) { - grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Resolution_mode_assertions_are_unstable_Use_nightly_TypeScript_to_silence_this_error_Try_updating_with_npm_install_D_typescript_next); - } + function checkImportBinding(node: ts.ImportEqualsDeclaration | ts.ImportClause | ts.NamespaceImport | ts.ImportSpecifier) { + checkCollisionsForDeclarationName(node, node.name); + checkAliasSymbol(node); + if (node.kind === ts.SyntaxKind.ImportSpecifier && + ts.idText(node.propertyName || node.name) === "default" && + ts.getESModuleInterop(compilerOptions) && + moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportDefault); + } + } - if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { - return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext); - } - return; // Other grammar checks do not apply to type-only imports with resolution mode assertions + function checkAssertClause(declaration: ts.ImportDeclaration | ts.ExportDeclaration) { + if (declaration.assertClause) { + const validForTypeAssertions = ts.isExclusivelyTypeOnlyImportOrExport(declaration); + const override = ts.getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined); + if (validForTypeAssertions && override) { + if (!ts.isNightly()) { + grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Resolution_mode_assertions_are_unstable_Use_nightly_TypeScript_to_silence_this_error_Try_updating_with_npm_install_D_typescript_next); } - const mode = (moduleKind === ts.ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier); - if (mode !== ts.ModuleKind.ESNext && moduleKind !== ts.ModuleKind.ESNext) { - return grammarErrorOnNode(declaration.assertClause, moduleKind === ts.ModuleKind.NodeNext - ? ts.Diagnostics.Import_assertions_are_not_allowed_on_statements_that_transpile_to_commonjs_require_calls - : ts.Diagnostics.Import_assertions_are_only_supported_when_the_module_option_is_set_to_esnext_or_nodenext); + if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { + return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext); } + return; // Other grammar checks do not apply to type-only imports with resolution mode assertions + } - if (ts.isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) { - return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports); - } + const mode = (moduleKind === ts.ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier); + if (mode !== ts.ModuleKind.ESNext && moduleKind !== ts.ModuleKind.ESNext) { + return grammarErrorOnNode(declaration.assertClause, moduleKind === ts.ModuleKind.NodeNext + ? ts.Diagnostics.Import_assertions_are_not_allowed_on_statements_that_transpile_to_commonjs_require_calls + : ts.Diagnostics.Import_assertions_are_only_supported_when_the_module_option_is_set_to_esnext_or_nodenext); + } - if (override) { - return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports); - } + if (ts.isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) { + return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports); } - } - function checkImportDeclaration(node: ts.ImportDeclaration) { - if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) { - // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. - return; + if (override) { + return grammarErrorOnNode(declaration.assertClause, ts.Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports); } - if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasEffectiveModifiers(node)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.An_import_declaration_cannot_have_modifiers); - } - if (checkExternalImportOrExportDeclaration(node)) { - const importClause = node.importClause; - if (importClause && !checkGrammarImportClause(importClause)) { - if (importClause.name) { - checkImportBinding(importClause); - } - if (importClause.namedBindings) { - if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { - checkImportBinding(importClause.namedBindings); - if (moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && ts.getESModuleInterop(compilerOptions)) { - // import * as ns from "foo"; - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportStar); - } + } + } + + function checkImportDeclaration(node: ts.ImportDeclaration) { + if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) { + // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. + return; + } + if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasEffectiveModifiers(node)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.An_import_declaration_cannot_have_modifiers); + } + if (checkExternalImportOrExportDeclaration(node)) { + const importClause = node.importClause; + if (importClause && !checkGrammarImportClause(importClause)) { + if (importClause.name) { + checkImportBinding(importClause); + } + if (importClause.namedBindings) { + if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { + checkImportBinding(importClause.namedBindings); + if (moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && ts.getESModuleInterop(compilerOptions)) { + // import * as ns from "foo"; + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportStar); } - else { - const moduleExisted = resolveExternalModuleName(node, node.moduleSpecifier); - if (moduleExisted) { - ts.forEach(importClause.namedBindings.elements, checkImportBinding); - } + } + else { + const moduleExisted = resolveExternalModuleName(node, node.moduleSpecifier); + if (moduleExisted) { + ts.forEach(importClause.namedBindings.elements, checkImportBinding); } } } } - checkAssertClause(node); } + checkAssertClause(node); + } - function checkImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { - if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) { - // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. - return; - } + function checkImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { + if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_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 (ts.isInternalModuleImportEqualsDeclaration(node) || checkExternalImportOrExportDeclaration(node)) { - checkImportBinding(node); - if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { - markExportAsReferenced(node); - } - if (node.moduleReference.kind !== ts.SyntaxKind.ExternalModuleReference) { - const target = resolveAlias(getSymbolOfNode(node)); - if (target !== unknownSymbol) { - if (target.flags & ts.SymbolFlags.Value) { - // Target is a value symbol, check that it is not hidden by a local declaration with the same name - const moduleName = ts.getFirstIdentifier(node.moduleReference); - if (!(resolveEntityName(moduleName, ts.SymbolFlags.Value | ts.SymbolFlags.Namespace)!.flags & ts.SymbolFlags.Namespace)) { - error(moduleName, ts.Diagnostics.Module_0_is_hidden_by_a_local_declaration_with_the_same_name, ts.declarationNameToString(moduleName)); - } - } - if (target.flags & ts.SymbolFlags.Type) { - checkTypeNameIsReserved(node.name, ts.Diagnostics.Import_name_cannot_be_0); + checkGrammarDecoratorsAndModifiers(node); + if (ts.isInternalModuleImportEqualsDeclaration(node) || checkExternalImportOrExportDeclaration(node)) { + checkImportBinding(node); + if (ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { + markExportAsReferenced(node); + } + if (node.moduleReference.kind !== ts.SyntaxKind.ExternalModuleReference) { + const target = resolveAlias(getSymbolOfNode(node)); + if (target !== unknownSymbol) { + if (target.flags & ts.SymbolFlags.Value) { + // Target is a value symbol, check that it is not hidden by a local declaration with the same name + const moduleName = ts.getFirstIdentifier(node.moduleReference); + if (!(resolveEntityName(moduleName, ts.SymbolFlags.Value | ts.SymbolFlags.Namespace)!.flags & ts.SymbolFlags.Namespace)) { + error(moduleName, ts.Diagnostics.Module_0_is_hidden_by_a_local_declaration_with_the_same_name, ts.declarationNameToString(moduleName)); } } - if (node.isTypeOnly) { - grammarErrorOnNode(node, ts.Diagnostics.An_import_alias_cannot_use_import_type); + if (target.flags & ts.SymbolFlags.Type) { + checkTypeNameIsReserved(node.name, ts.Diagnostics.Import_name_cannot_be_0); } } - else { - if (moduleKind >= ts.ModuleKind.ES2015 && ts.getSourceFileOfNode(node).impliedNodeFormat === undefined && !node.isTypeOnly && !(node.flags & ts.NodeFlags.Ambient)) { - // Import equals declaration is deprecated in es6 or above - grammarErrorOnNode(node, ts.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); - } + if (node.isTypeOnly) { + grammarErrorOnNode(node, ts.Diagnostics.An_import_alias_cannot_use_import_type); + } + } + else { + if (moduleKind >= ts.ModuleKind.ES2015 && ts.getSourceFileOfNode(node).impliedNodeFormat === undefined && !node.isTypeOnly && !(node.flags & ts.NodeFlags.Ambient)) { + // Import equals declaration is deprecated in es6 or above + grammarErrorOnNode(node, ts.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); } } } + } - function checkExportDeclaration(node: ts.ExportDeclaration) { - if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) { - // If we hit an export in an illegal context, just bail out to avoid cascading errors. - return; - } + function checkExportDeclaration(node: ts.ExportDeclaration) { + if (checkGrammarModuleElementContext(node, ts.isInJSFile(node) ? ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_module : ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) { + // If we hit an export in an illegal context, just bail out to avoid cascading errors. + return; + } - if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasSyntacticModifiers(node)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.An_export_declaration_cannot_have_modifiers); - } + if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasSyntacticModifiers(node)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.An_export_declaration_cannot_have_modifiers); + } - if (node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause) && ts.length(node.exportClause.elements) && languageVersion === ts.ScriptTarget.ES3) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.CreateBinding); - } + if (node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause) && ts.length(node.exportClause.elements) && languageVersion === ts.ScriptTarget.ES3) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.CreateBinding); + } - checkGrammarExportDeclaration(node); - if (!node.moduleSpecifier || checkExternalImportOrExportDeclaration(node)) { - if (node.exportClause && !ts.isNamespaceExport(node.exportClause)) { - // export { x, y } - // export { x, y } from "foo" - ts.forEach(node.exportClause.elements, checkExportSpecifier); - const inAmbientExternalModule = node.parent.kind === ts.SyntaxKind.ModuleBlock && ts.isAmbientModule(node.parent.parent); - const inAmbientNamespaceDeclaration = !inAmbientExternalModule && node.parent.kind === ts.SyntaxKind.ModuleBlock && - !node.moduleSpecifier && node.flags & ts.NodeFlags.Ambient; - if (node.parent.kind !== ts.SyntaxKind.SourceFile && !inAmbientExternalModule && !inAmbientNamespaceDeclaration) { - error(node, ts.Diagnostics.Export_declarations_are_not_permitted_in_a_namespace); - } + checkGrammarExportDeclaration(node); + if (!node.moduleSpecifier || checkExternalImportOrExportDeclaration(node)) { + if (node.exportClause && !ts.isNamespaceExport(node.exportClause)) { + // export { x, y } + // export { x, y } from "foo" + ts.forEach(node.exportClause.elements, checkExportSpecifier); + const inAmbientExternalModule = node.parent.kind === ts.SyntaxKind.ModuleBlock && ts.isAmbientModule(node.parent.parent); + const inAmbientNamespaceDeclaration = !inAmbientExternalModule && node.parent.kind === ts.SyntaxKind.ModuleBlock && + !node.moduleSpecifier && node.flags & ts.NodeFlags.Ambient; + if (node.parent.kind !== ts.SyntaxKind.SourceFile && !inAmbientExternalModule && !inAmbientNamespaceDeclaration) { + error(node, ts.Diagnostics.Export_declarations_are_not_permitted_in_a_namespace); } - else { - // export * from "foo" - // export * as ns from "foo"; - const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!); - if (moduleSymbol && hasExportAssignmentSymbol(moduleSymbol)) { - error(node.moduleSpecifier, ts.Diagnostics.Module_0_uses_export_and_cannot_be_used_with_export_Asterisk, symbolToString(moduleSymbol)); - } - else if (node.exportClause) { - checkAliasSymbol(node.exportClause); - } - if (moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { - if (node.exportClause) { - // export * as ns from "foo"; - // For ES2015 modules, we emit it as a pair of `import * as a_1 ...; export { a_1 as ns }` and don't need the helper. - // We only use the helper here when in esModuleInterop - if (ts.getESModuleInterop(compilerOptions)) { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportStar); - } - } - else { - // export * from "foo" - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ExportStar); + } + else { + // export * from "foo" + // export * as ns from "foo"; + const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!); + if (moduleSymbol && hasExportAssignmentSymbol(moduleSymbol)) { + error(node.moduleSpecifier, ts.Diagnostics.Module_0_uses_export_and_cannot_be_used_with_export_Asterisk, symbolToString(moduleSymbol)); + } + else if (node.exportClause) { + checkAliasSymbol(node.exportClause); + } + if (moduleKind !== ts.ModuleKind.System && (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS)) { + if (node.exportClause) { + // export * as ns from "foo"; + // For ES2015 modules, we emit it as a pair of `import * as a_1 ...; export { a_1 as ns }` and don't need the helper. + // We only use the helper here when in esModuleInterop + if (ts.getESModuleInterop(compilerOptions)) { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportStar); } } + else { + // export * from "foo" + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ExportStar); + } } } - checkAssertClause(node); } + checkAssertClause(node); + } - function checkGrammarExportDeclaration(node: ts.ExportDeclaration): boolean { - if (node.isTypeOnly) { - if (node.exportClause?.kind === ts.SyntaxKind.NamedExports) { - return checkGrammarNamedImportsOrExports(node.exportClause); - } - else { - return grammarErrorOnNode(node, ts.Diagnostics.Only_named_exports_may_use_export_type); - } + function checkGrammarExportDeclaration(node: ts.ExportDeclaration): boolean { + if (node.isTypeOnly) { + if (node.exportClause?.kind === ts.SyntaxKind.NamedExports) { + return checkGrammarNamedImportsOrExports(node.exportClause); } - return false; - } - - function checkGrammarModuleElementContext(node: ts.Statement, errorMessage: ts.DiagnosticMessage): boolean { - const isInAppropriateContext = node.parent.kind === ts.SyntaxKind.SourceFile || node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.ModuleDeclaration; - if (!isInAppropriateContext) { - grammarErrorOnFirstToken(node, errorMessage); + else { + return grammarErrorOnNode(node, ts.Diagnostics.Only_named_exports_may_use_export_type); } - return !isInAppropriateContext; } + return false; + } - function importClauseContainsReferencedImport(importClause: ts.ImportClause) { - return ts.forEachImportClauseDeclaration(importClause, declaration => { - return !!getSymbolOfNode(declaration).isReferenced; - }); + function checkGrammarModuleElementContext(node: ts.Statement, errorMessage: ts.DiagnosticMessage): boolean { + const isInAppropriateContext = node.parent.kind === ts.SyntaxKind.SourceFile || node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.ModuleDeclaration; + if (!isInAppropriateContext) { + grammarErrorOnFirstToken(node, errorMessage); } + return !isInAppropriateContext; + } - function importClauseContainsConstEnumUsedAsValue(importClause: ts.ImportClause) { - return ts.forEachImportClauseDeclaration(importClause, declaration => { - return !!getSymbolLinks(getSymbolOfNode(declaration)).constEnumReferenced; - }); - } + function importClauseContainsReferencedImport(importClause: ts.ImportClause) { + return ts.forEachImportClauseDeclaration(importClause, declaration => { + return !!getSymbolOfNode(declaration).isReferenced; + }); + } - function canConvertImportDeclarationToTypeOnly(statement: ts.Statement) { - return ts.isImportDeclaration(statement) && - statement.importClause && - !statement.importClause.isTypeOnly && - importClauseContainsReferencedImport(statement.importClause) && - !isReferencedAliasDeclaration(statement.importClause, /*checkChildren*/ true) && - !importClauseContainsConstEnumUsedAsValue(statement.importClause); - } + function importClauseContainsConstEnumUsedAsValue(importClause: ts.ImportClause) { + return ts.forEachImportClauseDeclaration(importClause, declaration => { + return !!getSymbolLinks(getSymbolOfNode(declaration)).constEnumReferenced; + }); + } - function canConvertImportEqualsDeclarationToTypeOnly(statement: ts.Statement) { - return ts.isImportEqualsDeclaration(statement) && - ts.isExternalModuleReference(statement.moduleReference) && - !statement.isTypeOnly && - getSymbolOfNode(statement).isReferenced && - !isReferencedAliasDeclaration(statement, /*checkChildren*/ false) && - !getSymbolLinks(getSymbolOfNode(statement)).constEnumReferenced; - } + function canConvertImportDeclarationToTypeOnly(statement: ts.Statement) { + return ts.isImportDeclaration(statement) && + statement.importClause && + !statement.importClause.isTypeOnly && + importClauseContainsReferencedImport(statement.importClause) && + !isReferencedAliasDeclaration(statement.importClause, /*checkChildren*/ true) && + !importClauseContainsConstEnumUsedAsValue(statement.importClause); + } - function checkImportsForTypeOnlyConversion(sourceFile: ts.SourceFile) { - for (const statement of sourceFile.statements) { - if (canConvertImportDeclarationToTypeOnly(statement) || canConvertImportEqualsDeclarationToTypeOnly(statement)) { - error(statement, ts.Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_importsNotUsedAsValues_is_set_to_error); - } + function canConvertImportEqualsDeclarationToTypeOnly(statement: ts.Statement) { + return ts.isImportEqualsDeclaration(statement) && + ts.isExternalModuleReference(statement.moduleReference) && + !statement.isTypeOnly && + getSymbolOfNode(statement).isReferenced && + !isReferencedAliasDeclaration(statement, /*checkChildren*/ false) && + !getSymbolLinks(getSymbolOfNode(statement)).constEnumReferenced; + } + + function checkImportsForTypeOnlyConversion(sourceFile: ts.SourceFile) { + for (const statement of sourceFile.statements) { + if (canConvertImportDeclarationToTypeOnly(statement) || canConvertImportEqualsDeclarationToTypeOnly(statement)) { + error(statement, ts.Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_importsNotUsedAsValues_is_set_to_error); } } + } - function checkExportSpecifier(node: ts.ExportSpecifier) { - checkAliasSymbol(node); - if (ts.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, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, - /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { - error(exportedName, ts.Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, ts.idText(exportedName)); - } - else { - markExportAsReferenced(node); - const target = symbol && (symbol.flags & ts.SymbolFlags.Alias ? resolveAlias(symbol) : symbol); - if (!target || target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { - checkExpressionCached(node.propertyName || node.name); - } - } + function checkExportSpecifier(node: ts.ExportSpecifier) { + checkAliasSymbol(node); + if (ts.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, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, + /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { + error(exportedName, ts.Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, ts.idText(exportedName)); } else { - if (ts.getESModuleInterop(compilerOptions) && - moduleKind !== ts.ModuleKind.System && - (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && - ts.idText(node.propertyName || node.name) === "default") { - checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportDefault); + markExportAsReferenced(node); + const target = symbol && (symbol.flags & ts.SymbolFlags.Alias ? resolveAlias(symbol) : symbol); + if (!target || target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { + checkExpressionCached(node.propertyName || node.name); } } } - - function checkExportAssignment(node: ts.ExportAssignment) { - const illegalContextMessage = node.isExportEquals - ? ts.Diagnostics.An_export_assignment_must_be_at_the_top_level_of_a_file_or_module_declaration - : ts.Diagnostics.A_default_export_must_be_at_the_top_level_of_a_file_or_module_declaration; - if (checkGrammarModuleElementContext(node, illegalContextMessage)) { - // If we hit an export assignment in an illegal context, just bail out to avoid cascading errors. - return; + else { + if (ts.getESModuleInterop(compilerOptions) && + moduleKind !== ts.ModuleKind.System && + (moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && + ts.idText(node.propertyName || node.name) === "default") { + checkExternalEmitHelpers(node, ts.ExternalEmitHelpers.ImportDefault); } + } + } - const container = node.parent.kind === ts.SyntaxKind.SourceFile ? node.parent : node.parent.parent as ts.ModuleDeclaration; - if (container.kind === ts.SyntaxKind.ModuleDeclaration && !ts.isAmbientModule(container)) { - if (node.isExportEquals) { - error(node, ts.Diagnostics.An_export_assignment_cannot_be_used_in_a_namespace); - } - else { - error(node, ts.Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); - } + function checkExportAssignment(node: ts.ExportAssignment) { + const illegalContextMessage = node.isExportEquals + ? ts.Diagnostics.An_export_assignment_must_be_at_the_top_level_of_a_file_or_module_declaration + : ts.Diagnostics.A_default_export_must_be_at_the_top_level_of_a_file_or_module_declaration; + if (checkGrammarModuleElementContext(node, illegalContextMessage)) { + // If we hit an export assignment in an illegal context, just bail out to avoid cascading errors. + return; + } - return; + const container = node.parent.kind === ts.SyntaxKind.SourceFile ? node.parent : node.parent.parent as ts.ModuleDeclaration; + if (container.kind === ts.SyntaxKind.ModuleDeclaration && !ts.isAmbientModule(container)) { + if (node.isExportEquals) { + error(node, ts.Diagnostics.An_export_assignment_cannot_be_used_in_a_namespace); } - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasEffectiveModifiers(node)) { - grammarErrorOnFirstToken(node, ts.Diagnostics.An_export_assignment_cannot_have_modifiers); + else { + error(node, ts.Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); } - const typeAnnotationNode = ts.getEffectiveTypeAnnotationNode(node); - if (typeAnnotationNode) { - checkTypeAssignableTo(checkExpressionCached(node.expression), getTypeFromTypeNode(typeAnnotationNode), node.expression); - } + return; + } + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && ts.hasEffectiveModifiers(node)) { + grammarErrorOnFirstToken(node, ts.Diagnostics.An_export_assignment_cannot_have_modifiers); + } - if (node.expression.kind === ts.SyntaxKind.Identifier) { - const id = node.expression as ts.Identifier; - const sym = resolveEntityName(id, ts.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 & ts.SymbolFlags.Alias ? resolveAlias(sym) : sym; - if (target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { - // However if it is a value, we need to check it's being used correctly - checkExpressionCached(node.expression); - } - } - else { - checkExpressionCached(node.expression); // doesn't resolve, check as expression to mark as error - } + const typeAnnotationNode = ts.getEffectiveTypeAnnotationNode(node); + if (typeAnnotationNode) { + checkTypeAssignableTo(checkExpressionCached(node.expression), getTypeFromTypeNode(typeAnnotationNode), node.expression); + } - if (ts.getEmitDeclarations(compilerOptions)) { - collectLinkedAliases(node.expression as ts.Identifier, /*setVisibility*/ true); + if (node.expression.kind === ts.SyntaxKind.Identifier) { + const id = node.expression as ts.Identifier; + const sym = resolveEntityName(id, ts.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 & ts.SymbolFlags.Alias ? resolveAlias(sym) : sym; + if (target === unknownSymbol || target.flags & ts.SymbolFlags.Value) { + // However if it is a value, we need to check it's being used correctly + checkExpressionCached(node.expression); } } else { - checkExpressionCached(node.expression); - } - - checkExternalModuleExports(container); - - if ((node.flags & ts.NodeFlags.Ambient) && !ts.isEntityNameExpression(node.expression)) { - grammarErrorOnNode(node.expression, ts.Diagnostics.The_expression_of_an_export_assignment_must_be_an_identifier_or_qualified_name_in_an_ambient_context); + checkExpressionCached(node.expression); // doesn't resolve, check as expression to mark as error } - if (node.isExportEquals && !(node.flags & ts.NodeFlags.Ambient)) { - if (moduleKind >= ts.ModuleKind.ES2015 && ts.getSourceFileOfNode(node).impliedNodeFormat !== ts.ModuleKind.CommonJS) { - // export assignment is not supported in es6 modules - grammarErrorOnNode(node, ts.Diagnostics.Export_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_export_default_or_another_module_format_instead); - } - else if (moduleKind === ts.ModuleKind.System) { - // system modules does not support export assignment - grammarErrorOnNode(node, ts.Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system); - } + if (ts.getEmitDeclarations(compilerOptions)) { + collectLinkedAliases(node.expression as ts.Identifier, /*setVisibility*/ true); } } - - function hasExportedMembers(moduleSymbol: ts.Symbol) { - return ts.forEachEntry(moduleSymbol.exports!, (_, id) => id !== "export="); + else { + checkExpressionCached(node.expression); } - function checkExternalModuleExports(node: ts.SourceFile | ts.ModuleDeclaration) { - const moduleSymbol = getSymbolOfNode(node); - const links = getSymbolLinks(moduleSymbol); - if (!links.exportsChecked) { - const exportEqualsSymbol = moduleSymbol.exports!.get("export=" as ts.__String); - if (exportEqualsSymbol && hasExportedMembers(moduleSymbol)) { - const declaration = getDeclarationOfAliasSymbol(exportEqualsSymbol) || exportEqualsSymbol.valueDeclaration; - if (declaration && !isTopLevelInExternalModuleAugmentation(declaration) && !ts.isInJSFile(declaration)) { - error(declaration, ts.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 & (ts.SymbolFlags.Namespace | ts.SymbolFlags.Enum)) { - return; - } - const exportedDeclarationsCount = ts.countWhere(declarations, ts.and(isNotOverloadAndNotAccessor, ts.not(ts.isInterfaceDeclaration))); - if (flags & ts.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) { - if (!isDuplicatedCommonJSExport(declarations)) { - for (const declaration of declarations!) { - if (isNotOverload(declaration)) { - diagnostics.add(ts.createDiagnosticForNode(declaration, ts.Diagnostics.Cannot_redeclare_exported_variable_0, ts.unescapeLeadingUnderscores(id))); - } - } - } - } - }); - } - links.exportsChecked = true; - } - } + checkExternalModuleExports(container); - function isDuplicatedCommonJSExport(declarations: ts.Declaration[] | undefined) { - return declarations - && declarations.length > 1 - && declarations.every(d => ts.isInJSFile(d) && ts.isAccessExpression(d) && (ts.isExportsIdentifier(d.expression) || ts.isModuleExportsAccessExpression(d.expression))); + if ((node.flags & ts.NodeFlags.Ambient) && !ts.isEntityNameExpression(node.expression)) { + grammarErrorOnNode(node.expression, ts.Diagnostics.The_expression_of_an_export_assignment_must_be_an_identifier_or_qualified_name_in_an_ambient_context); } - function checkSourceElement(node: ts.Node | undefined): void { - if (node) { - const saveCurrentNode = currentNode; - currentNode = node; - instantiationCount = 0; - checkSourceElementWorker(node); - currentNode = saveCurrentNode; + if (node.isExportEquals && !(node.flags & ts.NodeFlags.Ambient)) { + if (moduleKind >= ts.ModuleKind.ES2015 && ts.getSourceFileOfNode(node).impliedNodeFormat !== ts.ModuleKind.CommonJS) { + // export assignment is not supported in es6 modules + grammarErrorOnNode(node, ts.Diagnostics.Export_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_export_default_or_another_module_format_instead); + } + else if (moduleKind === ts.ModuleKind.System) { + // system modules does not support export assignment + grammarErrorOnNode(node, ts.Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system); } } + } - function checkSourceElementWorker(node: ts.Node): void { - if (ts.isInJSFile(node)) { - ts.forEach((node as ts.JSDocContainer).jsDoc, ({ tags }) => ts.forEach(tags, checkSourceElement)); - } + function hasExportedMembers(moduleSymbol: ts.Symbol) { + return ts.forEachEntry(moduleSymbol.exports!, (_, id) => id !== "export="); + } - 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 ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - cancellationToken.throwIfCancellationRequested(); + function checkExternalModuleExports(node: ts.SourceFile | ts.ModuleDeclaration) { + const moduleSymbol = getSymbolOfNode(node); + const links = getSymbolLinks(moduleSymbol); + if (!links.exportsChecked) { + const exportEqualsSymbol = moduleSymbol.exports!.get("export=" as ts.__String); + if (exportEqualsSymbol && hasExportedMembers(moduleSymbol)) { + const declaration = getDeclarationOfAliasSymbol(exportEqualsSymbol) || exportEqualsSymbol.valueDeclaration; + if (declaration && !isTopLevelInExternalModuleAugmentation(declaration) && !ts.isInJSFile(declaration)) { + error(declaration, ts.Diagnostics.An_export_assignment_cannot_be_used_in_a_module_with_other_exported_elements); } } - if (kind >= ts.SyntaxKind.FirstStatement && kind <= ts.SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) { - errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, ts.Diagnostics.Unreachable_code_detected); - } - - switch (kind) { - case ts.SyntaxKind.TypeParameter: - return checkTypeParameter(node as ts.TypeParameterDeclaration); - case ts.SyntaxKind.Parameter: - return checkParameter(node as ts.ParameterDeclaration); - case ts.SyntaxKind.PropertyDeclaration: - return checkPropertyDeclaration(node as ts.PropertyDeclaration); - case ts.SyntaxKind.PropertySignature: - return checkPropertySignature(node as ts.PropertySignature); - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.IndexSignature: - return checkSignatureDeclaration(node as ts.SignatureDeclaration); - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return checkMethodDeclaration(node as ts.MethodDeclaration | ts.MethodSignature); - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return checkClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); - case ts.SyntaxKind.Constructor: - return checkConstructorDeclaration(node as ts.ConstructorDeclaration); - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return checkAccessorDeclaration(node as ts.AccessorDeclaration); - case ts.SyntaxKind.TypeReference: - return checkTypeReferenceNode(node as ts.TypeReferenceNode); - case ts.SyntaxKind.TypePredicate: - return checkTypePredicate(node as ts.TypePredicateNode); - case ts.SyntaxKind.TypeQuery: - return checkTypeQuery(node as ts.TypeQueryNode); - case ts.SyntaxKind.TypeLiteral: - return checkTypeLiteral(node as ts.TypeLiteralNode); - case ts.SyntaxKind.ArrayType: - return checkArrayType(node as ts.ArrayTypeNode); - case ts.SyntaxKind.TupleType: - return checkTupleType(node as ts.TupleTypeNode); - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - return checkUnionOrIntersectionType(node as ts.UnionOrIntersectionTypeNode); - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.OptionalType: - case ts.SyntaxKind.RestType: - return checkSourceElement((node as ts.ParenthesizedTypeNode | ts.OptionalTypeNode | ts.RestTypeNode).type); - case ts.SyntaxKind.ThisType: - return checkThisType(node as ts.ThisTypeNode); - case ts.SyntaxKind.TypeOperator: - return checkTypeOperator(node as ts.TypeOperatorNode); - case ts.SyntaxKind.ConditionalType: - return checkConditionalType(node as ts.ConditionalTypeNode); - case ts.SyntaxKind.InferType: - return checkInferType(node as ts.InferTypeNode); - case ts.SyntaxKind.TemplateLiteralType: - return checkTemplateLiteralType(node as ts.TemplateLiteralTypeNode); - case ts.SyntaxKind.ImportType: - return checkImportType(node as ts.ImportTypeNode); - case ts.SyntaxKind.NamedTupleMember: - return checkNamedTupleMember(node as ts.NamedTupleMember); - case ts.SyntaxKind.JSDocAugmentsTag: - return checkJSDocAugmentsTag(node as ts.JSDocAugmentsTag); - case ts.SyntaxKind.JSDocImplementsTag: - return checkJSDocImplementsTag(node as ts.JSDocImplementsTag); - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - return checkJSDocTypeAliasTag(node as ts.JSDocTypedefTag); - case ts.SyntaxKind.JSDocTemplateTag: - return checkJSDocTemplateTag(node as ts.JSDocTemplateTag); - case ts.SyntaxKind.JSDocTypeTag: - return checkJSDocTypeTag(node as ts.JSDocTypeTag); - case ts.SyntaxKind.JSDocParameterTag: - return checkJSDocParameterTag(node as ts.JSDocParameterTag); - case ts.SyntaxKind.JSDocPropertyTag: - return checkJSDocPropertyTag(node as ts.JSDocPropertyTag); - case ts.SyntaxKind.JSDocFunctionType: - checkJSDocFunctionType(node as ts.JSDocFunctionType); - // falls through - case ts.SyntaxKind.JSDocNonNullableType: - case ts.SyntaxKind.JSDocNullableType: - case ts.SyntaxKind.JSDocAllType: - case ts.SyntaxKind.JSDocUnknownType: - case ts.SyntaxKind.JSDocTypeLiteral: - checkJSDocTypeIsInJsFile(node); - ts.forEachChild(node, checkSourceElement); - return; - case ts.SyntaxKind.JSDocVariadicType: - checkJSDocVariadicType(node as ts.JSDocVariadicType); - return; - case ts.SyntaxKind.JSDocTypeExpression: - return checkSourceElement((node as ts.JSDocTypeExpression).type); - case ts.SyntaxKind.JSDocPublicTag: - case ts.SyntaxKind.JSDocProtectedTag: - case ts.SyntaxKind.JSDocPrivateTag: - return checkJSDocAccessibilityModifiers(node as ts.JSDocPublicTag | ts.JSDocProtectedTag | ts.JSDocPrivateTag); - case ts.SyntaxKind.IndexedAccessType: - return checkIndexedAccessType(node as ts.IndexedAccessTypeNode); - case ts.SyntaxKind.MappedType: - return checkMappedType(node as ts.MappedTypeNode); - case ts.SyntaxKind.FunctionDeclaration: - return checkFunctionDeclaration(node as ts.FunctionDeclaration); - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - return checkBlock(node as ts.Block); - case ts.SyntaxKind.VariableStatement: - return checkVariableStatement(node as ts.VariableStatement); - case ts.SyntaxKind.ExpressionStatement: - return checkExpressionStatement(node as ts.ExpressionStatement); - case ts.SyntaxKind.IfStatement: - return checkIfStatement(node as ts.IfStatement); - case ts.SyntaxKind.DoStatement: - return checkDoStatement(node as ts.DoStatement); - case ts.SyntaxKind.WhileStatement: - return checkWhileStatement(node as ts.WhileStatement); - case ts.SyntaxKind.ForStatement: - return checkForStatement(node as ts.ForStatement); - case ts.SyntaxKind.ForInStatement: - return checkForInStatement(node as ts.ForInStatement); - case ts.SyntaxKind.ForOfStatement: - return checkForOfStatement(node as ts.ForOfStatement); - case ts.SyntaxKind.ContinueStatement: - case ts.SyntaxKind.BreakStatement: - return checkBreakOrContinueStatement(node as ts.BreakOrContinueStatement); - case ts.SyntaxKind.ReturnStatement: - return checkReturnStatement(node as ts.ReturnStatement); - case ts.SyntaxKind.WithStatement: - return checkWithStatement(node as ts.WithStatement); - case ts.SyntaxKind.SwitchStatement: - return checkSwitchStatement(node as ts.SwitchStatement); - case ts.SyntaxKind.LabeledStatement: - return checkLabeledStatement(node as ts.LabeledStatement); - case ts.SyntaxKind.ThrowStatement: - return checkThrowStatement(node as ts.ThrowStatement); - case ts.SyntaxKind.TryStatement: - return checkTryStatement(node as ts.TryStatement); - case ts.SyntaxKind.VariableDeclaration: - return checkVariableDeclaration(node as ts.VariableDeclaration); - case ts.SyntaxKind.BindingElement: - return checkBindingElement(node as ts.BindingElement); - case ts.SyntaxKind.ClassDeclaration: - return checkClassDeclaration(node as ts.ClassDeclaration); - case ts.SyntaxKind.InterfaceDeclaration: - return checkInterfaceDeclaration(node as ts.InterfaceDeclaration); - case ts.SyntaxKind.TypeAliasDeclaration: - return checkTypeAliasDeclaration(node as ts.TypeAliasDeclaration); - case ts.SyntaxKind.EnumDeclaration: - return checkEnumDeclaration(node as ts.EnumDeclaration); - case ts.SyntaxKind.ModuleDeclaration: - return checkModuleDeclaration(node as ts.ModuleDeclaration); - case ts.SyntaxKind.ImportDeclaration: - return checkImportDeclaration(node as ts.ImportDeclaration); - case ts.SyntaxKind.ImportEqualsDeclaration: - return checkImportEqualsDeclaration(node as ts.ImportEqualsDeclaration); - case ts.SyntaxKind.ExportDeclaration: - return checkExportDeclaration(node as ts.ExportDeclaration); - case ts.SyntaxKind.ExportAssignment: - return checkExportAssignment(node as ts.ExportAssignment); - case ts.SyntaxKind.EmptyStatement: - case ts.SyntaxKind.DebuggerStatement: - checkGrammarStatementInAmbientContext(node); - return; - case ts.SyntaxKind.MissingDeclaration: - return checkMissingDeclaration(node); + // 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 & (ts.SymbolFlags.Namespace | ts.SymbolFlags.Enum)) { + return; + } + const exportedDeclarationsCount = ts.countWhere(declarations, ts.and(isNotOverloadAndNotAccessor, ts.not(ts.isInterfaceDeclaration))); + if (flags & ts.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) { + if (!isDuplicatedCommonJSExport(declarations)) { + for (const declaration of declarations!) { + if (isNotOverload(declaration)) { + diagnostics.add(ts.createDiagnosticForNode(declaration, ts.Diagnostics.Cannot_redeclare_exported_variable_0, ts.unescapeLeadingUnderscores(id))); + } + } + } + } + }); } + links.exportsChecked = true; } + } - function checkJSDocTypeIsInJsFile(node: ts.Node): void { - if (!ts.isInJSFile(node)) { - grammarErrorOnNode(node, ts.Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); - } - } + function isDuplicatedCommonJSExport(declarations: ts.Declaration[] | undefined) { + return declarations + && declarations.length > 1 + && declarations.every(d => ts.isInJSFile(d) && ts.isAccessExpression(d) && (ts.isExportsIdentifier(d.expression) || ts.isModuleExportsAccessExpression(d.expression))); + } - function checkJSDocVariadicType(node: ts.JSDocVariadicType): void { - checkJSDocTypeIsInJsFile(node); - checkSourceElement(node.type); + function checkSourceElement(node: ts.Node | undefined): void { + if (node) { + const saveCurrentNode = currentNode; + currentNode = node; + instantiationCount = 0; + checkSourceElementWorker(node); + currentNode = saveCurrentNode; + } + } - // Only legal location is in the *last* parameter tag or last parameter of a JSDoc function. - const { parent } = node; - if (ts.isParameter(parent) && ts.isJSDocFunctionType(parent.parent)) { - if (ts.last(parent.parent.parameters) !== parent) { - error(node, ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); - } - return; - } + function checkSourceElementWorker(node: ts.Node): void { + if (ts.isInJSFile(node)) { + ts.forEach((node as ts.JSDocContainer).jsDoc, ({ tags }) => ts.forEach(tags, checkSourceElement)); + } - if (!ts.isJSDocTypeExpression(parent)) { - error(node, ts.Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); + 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 ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + cancellationToken.throwIfCancellationRequested(); } - - const paramTag = node.parent.parent; - if (!ts.isJSDocParameterTag(paramTag)) { - error(node, ts.Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); + } + if (kind >= ts.SyntaxKind.FirstStatement && kind <= ts.SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) { + errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, ts.Diagnostics.Unreachable_code_detected); + } + + switch (kind) { + case ts.SyntaxKind.TypeParameter: + return checkTypeParameter(node as ts.TypeParameterDeclaration); + case ts.SyntaxKind.Parameter: + return checkParameter(node as ts.ParameterDeclaration); + case ts.SyntaxKind.PropertyDeclaration: + return checkPropertyDeclaration(node as ts.PropertyDeclaration); + case ts.SyntaxKind.PropertySignature: + return checkPropertySignature(node as ts.PropertySignature); + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.IndexSignature: + return checkSignatureDeclaration(node as ts.SignatureDeclaration); + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return checkMethodDeclaration(node as ts.MethodDeclaration | ts.MethodSignature); + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return checkClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); + case ts.SyntaxKind.Constructor: + return checkConstructorDeclaration(node as ts.ConstructorDeclaration); + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return checkAccessorDeclaration(node as ts.AccessorDeclaration); + case ts.SyntaxKind.TypeReference: + return checkTypeReferenceNode(node as ts.TypeReferenceNode); + case ts.SyntaxKind.TypePredicate: + return checkTypePredicate(node as ts.TypePredicateNode); + case ts.SyntaxKind.TypeQuery: + return checkTypeQuery(node as ts.TypeQueryNode); + case ts.SyntaxKind.TypeLiteral: + return checkTypeLiteral(node as ts.TypeLiteralNode); + case ts.SyntaxKind.ArrayType: + return checkArrayType(node as ts.ArrayTypeNode); + case ts.SyntaxKind.TupleType: + return checkTupleType(node as ts.TupleTypeNode); + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + return checkUnionOrIntersectionType(node as ts.UnionOrIntersectionTypeNode); + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.OptionalType: + case ts.SyntaxKind.RestType: + return checkSourceElement((node as ts.ParenthesizedTypeNode | ts.OptionalTypeNode | ts.RestTypeNode).type); + case ts.SyntaxKind.ThisType: + return checkThisType(node as ts.ThisTypeNode); + case ts.SyntaxKind.TypeOperator: + return checkTypeOperator(node as ts.TypeOperatorNode); + case ts.SyntaxKind.ConditionalType: + return checkConditionalType(node as ts.ConditionalTypeNode); + case ts.SyntaxKind.InferType: + return checkInferType(node as ts.InferTypeNode); + case ts.SyntaxKind.TemplateLiteralType: + return checkTemplateLiteralType(node as ts.TemplateLiteralTypeNode); + case ts.SyntaxKind.ImportType: + return checkImportType(node as ts.ImportTypeNode); + case ts.SyntaxKind.NamedTupleMember: + return checkNamedTupleMember(node as ts.NamedTupleMember); + case ts.SyntaxKind.JSDocAugmentsTag: + return checkJSDocAugmentsTag(node as ts.JSDocAugmentsTag); + case ts.SyntaxKind.JSDocImplementsTag: + return checkJSDocImplementsTag(node as ts.JSDocImplementsTag); + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + return checkJSDocTypeAliasTag(node as ts.JSDocTypedefTag); + case ts.SyntaxKind.JSDocTemplateTag: + return checkJSDocTemplateTag(node as ts.JSDocTemplateTag); + case ts.SyntaxKind.JSDocTypeTag: + return checkJSDocTypeTag(node as ts.JSDocTypeTag); + case ts.SyntaxKind.JSDocParameterTag: + return checkJSDocParameterTag(node as ts.JSDocParameterTag); + case ts.SyntaxKind.JSDocPropertyTag: + return checkJSDocPropertyTag(node as ts.JSDocPropertyTag); + case ts.SyntaxKind.JSDocFunctionType: + checkJSDocFunctionType(node as ts.JSDocFunctionType); + // falls through + case ts.SyntaxKind.JSDocNonNullableType: + case ts.SyntaxKind.JSDocNullableType: + case ts.SyntaxKind.JSDocAllType: + case ts.SyntaxKind.JSDocUnknownType: + case ts.SyntaxKind.JSDocTypeLiteral: + checkJSDocTypeIsInJsFile(node); + ts.forEachChild(node, checkSourceElement); return; - } - - const param = ts.getParameterSymbolFromJSDoc(paramTag); - if (!param) { - // We will error in `checkJSDocParameterTag`. + case ts.SyntaxKind.JSDocVariadicType: + checkJSDocVariadicType(node as ts.JSDocVariadicType); return; - } + case ts.SyntaxKind.JSDocTypeExpression: + return checkSourceElement((node as ts.JSDocTypeExpression).type); + case ts.SyntaxKind.JSDocPublicTag: + case ts.SyntaxKind.JSDocProtectedTag: + case ts.SyntaxKind.JSDocPrivateTag: + return checkJSDocAccessibilityModifiers(node as ts.JSDocPublicTag | ts.JSDocProtectedTag | ts.JSDocPrivateTag); + case ts.SyntaxKind.IndexedAccessType: + return checkIndexedAccessType(node as ts.IndexedAccessTypeNode); + case ts.SyntaxKind.MappedType: + return checkMappedType(node as ts.MappedTypeNode); + case ts.SyntaxKind.FunctionDeclaration: + return checkFunctionDeclaration(node as ts.FunctionDeclaration); + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + return checkBlock(node as ts.Block); + case ts.SyntaxKind.VariableStatement: + return checkVariableStatement(node as ts.VariableStatement); + case ts.SyntaxKind.ExpressionStatement: + return checkExpressionStatement(node as ts.ExpressionStatement); + case ts.SyntaxKind.IfStatement: + return checkIfStatement(node as ts.IfStatement); + case ts.SyntaxKind.DoStatement: + return checkDoStatement(node as ts.DoStatement); + case ts.SyntaxKind.WhileStatement: + return checkWhileStatement(node as ts.WhileStatement); + case ts.SyntaxKind.ForStatement: + return checkForStatement(node as ts.ForStatement); + case ts.SyntaxKind.ForInStatement: + return checkForInStatement(node as ts.ForInStatement); + case ts.SyntaxKind.ForOfStatement: + return checkForOfStatement(node as ts.ForOfStatement); + case ts.SyntaxKind.ContinueStatement: + case ts.SyntaxKind.BreakStatement: + return checkBreakOrContinueStatement(node as ts.BreakOrContinueStatement); + case ts.SyntaxKind.ReturnStatement: + return checkReturnStatement(node as ts.ReturnStatement); + case ts.SyntaxKind.WithStatement: + return checkWithStatement(node as ts.WithStatement); + case ts.SyntaxKind.SwitchStatement: + return checkSwitchStatement(node as ts.SwitchStatement); + case ts.SyntaxKind.LabeledStatement: + return checkLabeledStatement(node as ts.LabeledStatement); + case ts.SyntaxKind.ThrowStatement: + return checkThrowStatement(node as ts.ThrowStatement); + case ts.SyntaxKind.TryStatement: + return checkTryStatement(node as ts.TryStatement); + case ts.SyntaxKind.VariableDeclaration: + return checkVariableDeclaration(node as ts.VariableDeclaration); + case ts.SyntaxKind.BindingElement: + return checkBindingElement(node as ts.BindingElement); + case ts.SyntaxKind.ClassDeclaration: + return checkClassDeclaration(node as ts.ClassDeclaration); + case ts.SyntaxKind.InterfaceDeclaration: + return checkInterfaceDeclaration(node as ts.InterfaceDeclaration); + case ts.SyntaxKind.TypeAliasDeclaration: + return checkTypeAliasDeclaration(node as ts.TypeAliasDeclaration); + case ts.SyntaxKind.EnumDeclaration: + return checkEnumDeclaration(node as ts.EnumDeclaration); + case ts.SyntaxKind.ModuleDeclaration: + return checkModuleDeclaration(node as ts.ModuleDeclaration); + case ts.SyntaxKind.ImportDeclaration: + return checkImportDeclaration(node as ts.ImportDeclaration); + case ts.SyntaxKind.ImportEqualsDeclaration: + return checkImportEqualsDeclaration(node as ts.ImportEqualsDeclaration); + case ts.SyntaxKind.ExportDeclaration: + return checkExportDeclaration(node as ts.ExportDeclaration); + case ts.SyntaxKind.ExportAssignment: + return checkExportAssignment(node as ts.ExportAssignment); + case ts.SyntaxKind.EmptyStatement: + case ts.SyntaxKind.DebuggerStatement: + checkGrammarStatementInAmbientContext(node); + return; + case ts.SyntaxKind.MissingDeclaration: + return checkMissingDeclaration(node); + } + } - const host = ts.getHostSignatureFromJSDoc(paramTag); - if (!host || ts.last(host.parameters).symbol !== param) { + function checkJSDocTypeIsInJsFile(node: ts.Node): void { + if (!ts.isInJSFile(node)) { + grammarErrorOnNode(node, ts.Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + } + } + + function checkJSDocVariadicType(node: ts.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 (ts.isParameter(parent) && ts.isJSDocFunctionType(parent.parent)) { + if (ts.last(parent.parent.parameters) !== parent) { error(node, ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); } + return; } - function getTypeFromJSDocVariadicType(node: ts.JSDocVariadicType): ts.Type { - const type = getTypeFromTypeNode(node.type); - const { parent } = node; - const paramTag = node.parent.parent; - if (ts.isJSDocTypeExpression(node.parent) && ts.isJSDocParameterTag(paramTag)) { - // Else we will add a diagnostic, see `checkJSDocVariadicType`. - const host = ts.getHostSignatureFromJSDoc(paramTag); - const isCallbackTag = ts.isJSDocCallbackTag(paramTag.parent.parent); - if (host || isCallbackTag) { - /* - 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 = isCallbackTag - ? ts.lastOrUndefined((paramTag.parent.parent as unknown as ts.JSDocCallbackTag).typeExpression.parameters) - : ts.lastOrUndefined(host!.parameters); - const symbol = ts.getParameterSymbolFromJSDoc(paramTag); - if (!lastParamDeclaration || - symbol && lastParamDeclaration.symbol === symbol && ts.isRestParameter(lastParamDeclaration)) { - return createArrayType(type); - } - } - } - if (ts.isParameter(parent) && ts.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: ts.Node) { - const enclosingFile = ts.getSourceFileOfNode(node); - const links = getNodeLinks(enclosingFile); - if (!(links.flags & ts.NodeCheckFlags.TypeChecked)) { - links.deferredNodes ||= new ts.Set(); - links.deferredNodes.add(node); - } + if (!ts.isJSDocTypeExpression(parent)) { + error(node, ts.Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); } - function checkDeferredNodes(context: ts.SourceFile) { - const links = getNodeLinks(context); - if (links.deferredNodes) { - links.deferredNodes.forEach(checkDeferredNode); - } + const paramTag = node.parent.parent; + if (!ts.isJSDocParameterTag(paramTag)) { + error(node, ts.Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); + return; } - function checkDeferredNode(node: ts.Node) { - ts.tracing?.push(ts.tracing.Phase.Check, "checkDeferredNode", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); - const saveCurrentNode = currentNode; - currentNode = node; - instantiationCount = 0; - switch (node.kind) { - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.TaggedTemplateExpression: - case ts.SyntaxKind.Decorator: - case ts.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 ts.CallLikeExpression); - break; - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - checkFunctionExpressionOrObjectLiteralMethodDeferred(node as ts.FunctionExpression); - break; - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - checkAccessorDeclaration(node as ts.AccessorDeclaration); - break; - case ts.SyntaxKind.ClassExpression: - checkClassExpressionDeferred(node as ts.ClassExpression); - break; - case ts.SyntaxKind.TypeParameter: - checkTypeParameterDeferred(node as ts.TypeParameterDeclaration); - break; - case ts.SyntaxKind.JsxSelfClosingElement: - checkJsxSelfClosingElementDeferred(node as ts.JsxSelfClosingElement); - break; - case ts.SyntaxKind.JsxElement: - checkJsxElementDeferred(node as ts.JsxElement); - break; - } - currentNode = saveCurrentNode; - ts.tracing?.pop(); + const param = ts.getParameterSymbolFromJSDoc(paramTag); + if (!param) { + // We will error in `checkJSDocParameterTag`. + return; } - function checkSourceFile(node: ts.SourceFile) { - ts.tracing?.push(ts.tracing.Phase.Check, "checkSourceFile", { path: node.path }, /*separateBeginAndEnd*/ true); - ts.performance.mark("beforeCheck"); - checkSourceFileWorker(node); - ts.performance.mark("afterCheck"); - ts.performance.measure("Check", "beforeCheck", "afterCheck"); - ts.tracing?.pop(); + const host = ts.getHostSignatureFromJSDoc(paramTag); + if (!host || ts.last(host.parameters).symbol !== param) { + error(node, ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); } + } - 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 ts.Debug.assertNever(kind); + function getTypeFromJSDocVariadicType(node: ts.JSDocVariadicType): ts.Type { + const type = getTypeFromTypeNode(node.type); + const { parent } = node; + const paramTag = node.parent.parent; + if (ts.isJSDocTypeExpression(node.parent) && ts.isJSDocParameterTag(paramTag)) { + // Else we will add a diagnostic, see `checkJSDocVariadicType`. + const host = ts.getHostSignatureFromJSDoc(paramTag); + const isCallbackTag = ts.isJSDocCallbackTag(paramTag.parent.parent); + if (host || isCallbackTag) { + /* + 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 = isCallbackTag + ? ts.lastOrUndefined((paramTag.parent.parent as unknown as ts.JSDocCallbackTag).typeExpression.parameters) + : ts.lastOrUndefined(host!.parameters); + const symbol = ts.getParameterSymbolFromJSDoc(paramTag); + if (!lastParamDeclaration || + symbol && lastParamDeclaration.symbol === symbol && ts.isRestParameter(lastParamDeclaration)) { + return createArrayType(type); + } } } + if (ts.isParameter(parent) && ts.isJSDocFunctionType(parent.parent)) { + return createArrayType(type); + } + return addOptionality(type); + } - function getPotentiallyUnusedIdentifiers(sourceFile: ts.SourceFile): readonly PotentiallyUnusedIdentifier[] { - return allPotentiallyUnusedIdentifiers.get(sourceFile.path) || ts.emptyArray; + // 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: ts.Node) { + const enclosingFile = ts.getSourceFileOfNode(node); + const links = getNodeLinks(enclosingFile); + if (!(links.flags & ts.NodeCheckFlags.TypeChecked)) { + links.deferredNodes ||= new ts.Set(); + links.deferredNodes.add(node); } + } - // Fully type check a source file and collect the relevant diagnostics. - function checkSourceFileWorker(node: ts.SourceFile) { - const links = getNodeLinks(node); - if (!(links.flags & ts.NodeCheckFlags.TypeChecked)) { - if (ts.skipTypeChecking(node, compilerOptions, host)) { - return; - } + function checkDeferredNodes(context: ts.SourceFile) { + const links = getNodeLinks(context); + if (links.deferredNodes) { + links.deferredNodes.forEach(checkDeferredNode); + } + } - // Grammar checking - checkGrammarSourceFile(node); + function checkDeferredNode(node: ts.Node) { + ts.tracing?.push(ts.tracing.Phase.Check, "checkDeferredNode", { kind: node.kind, pos: node.pos, end: node.end, path: (node as ts.TracingNode).tracingPath }); + const saveCurrentNode = currentNode; + currentNode = node; + instantiationCount = 0; + switch (node.kind) { + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.TaggedTemplateExpression: + case ts.SyntaxKind.Decorator: + case ts.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 ts.CallLikeExpression); + break; + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + checkFunctionExpressionOrObjectLiteralMethodDeferred(node as ts.FunctionExpression); + break; + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + checkAccessorDeclaration(node as ts.AccessorDeclaration); + break; + case ts.SyntaxKind.ClassExpression: + checkClassExpressionDeferred(node as ts.ClassExpression); + break; + case ts.SyntaxKind.TypeParameter: + checkTypeParameterDeferred(node as ts.TypeParameterDeclaration); + break; + case ts.SyntaxKind.JsxSelfClosingElement: + checkJsxSelfClosingElementDeferred(node as ts.JsxSelfClosingElement); + break; + case ts.SyntaxKind.JsxElement: + checkJsxElementDeferred(node as ts.JsxElement); + break; + } + currentNode = saveCurrentNode; + ts.tracing?.pop(); + } - ts.clear(potentialThisCollisions); - ts.clear(potentialNewTargetCollisions); - ts.clear(potentialWeakMapSetCollisions); - ts.clear(potentialReflectCollisions); - ts.forEach(node.statements, checkSourceElement); - checkSourceElement(node.endOfFileToken); + function checkSourceFile(node: ts.SourceFile) { + ts.tracing?.push(ts.tracing.Phase.Check, "checkSourceFile", { path: node.path }, /*separateBeginAndEnd*/ true); + ts.performance.mark("beforeCheck"); + checkSourceFileWorker(node); + ts.performance.mark("afterCheck"); + ts.performance.measure("Check", "beforeCheck", "afterCheck"); + ts.tracing?.pop(); + } - checkDeferredNodes(node); + 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 ts.Debug.assertNever(kind); + } + } - if (ts.isExternalOrCommonJsModule(node)) { - registerForUnusedIdentifiersCheck(node); - } + function getPotentiallyUnusedIdentifiers(sourceFile: ts.SourceFile): readonly PotentiallyUnusedIdentifier[] { + return allPotentiallyUnusedIdentifiers.get(sourceFile.path) || ts.emptyArray; + } - addLazyDiagnostic(() => { - // This relies on the results of other lazy diagnostics, so must be computed after them - if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) { - checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => { - if (!ts.containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & ts.NodeFlags.Ambient))) { - diagnostics.add(diag); - } - }); - } - }); + // Fully type check a source file and collect the relevant diagnostics. + function checkSourceFileWorker(node: ts.SourceFile) { + const links = getNodeLinks(node); + if (!(links.flags & ts.NodeCheckFlags.TypeChecked)) { + if (ts.skipTypeChecking(node, compilerOptions, host)) { + return; + } - if (compilerOptions.importsNotUsedAsValues === ts.ImportsNotUsedAsValues.Error && - !node.isDeclarationFile && - ts.isExternalModule(node)) { - checkImportsForTypeOnlyConversion(node); - } + // Grammar checking + checkGrammarSourceFile(node); - if (ts.isExternalOrCommonJsModule(node)) { - checkExternalModuleExports(node); - } + ts.clear(potentialThisCollisions); + ts.clear(potentialNewTargetCollisions); + ts.clear(potentialWeakMapSetCollisions); + ts.clear(potentialReflectCollisions); + ts.forEach(node.statements, checkSourceElement); + checkSourceElement(node.endOfFileToken); - if (potentialThisCollisions.length) { - ts.forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope); - ts.clear(potentialThisCollisions); - } + checkDeferredNodes(node); - if (potentialNewTargetCollisions.length) { - ts.forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope); - ts.clear(potentialNewTargetCollisions); - } + if (ts.isExternalOrCommonJsModule(node)) { + registerForUnusedIdentifiersCheck(node); + } - if (potentialWeakMapSetCollisions.length) { - ts.forEach(potentialWeakMapSetCollisions, checkWeakMapSetCollision); - ts.clear(potentialWeakMapSetCollisions); + addLazyDiagnostic(() => { + // This relies on the results of other lazy diagnostics, so must be computed after them + if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) { + checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => { + if (!ts.containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & ts.NodeFlags.Ambient))) { + diagnostics.add(diag); + } + }); } + }); - if (potentialReflectCollisions.length) { - ts.forEach(potentialReflectCollisions, checkReflectCollision); - ts.clear(potentialReflectCollisions); - } + if (compilerOptions.importsNotUsedAsValues === ts.ImportsNotUsedAsValues.Error && + !node.isDeclarationFile && + ts.isExternalModule(node)) { + checkImportsForTypeOnlyConversion(node); + } - links.flags |= ts.NodeCheckFlags.TypeChecked; + if (ts.isExternalOrCommonJsModule(node)) { + checkExternalModuleExports(node); } - } - function getDiagnostics(sourceFile: ts.SourceFile, ct: ts.CancellationToken): ts.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); + if (potentialThisCollisions.length) { + ts.forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope); + ts.clear(potentialThisCollisions); } - finally { - cancellationToken = undefined; + + if (potentialNewTargetCollisions.length) { + ts.forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope); + ts.clear(potentialNewTargetCollisions); + } + + if (potentialWeakMapSetCollisions.length) { + ts.forEach(potentialWeakMapSetCollisions, checkWeakMapSetCollision); + ts.clear(potentialWeakMapSetCollisions); } - } - function ensurePendingDiagnosticWorkComplete() { - // Invoke any existing lazy diagnostics to add them, clear the backlog of diagnostics - for (const cb of deferredDiagnosticsCallbacks) { - cb(); + if (potentialReflectCollisions.length) { + ts.forEach(potentialReflectCollisions, checkReflectCollision); + ts.clear(potentialReflectCollisions); } - deferredDiagnosticsCallbacks = []; + + links.flags |= ts.NodeCheckFlags.TypeChecked; } + } - function checkSourceFileWithEagerDiagnostics(sourceFile: ts.SourceFile) { - ensurePendingDiagnosticWorkComplete(); - // then setup diagnostics for immediate invocation (as we are about to collect them, and - // this avoids the overhead of longer-lived callbacks we don't need to allocate) - // This also serves to make the shift to possibly lazy diagnostics transparent to serial command-line scenarios - // (as in those cases, all the diagnostics will still be computed as the appropriate place in the tree, - // thus much more likely retaining the same union ordering as before we had lazy diagnostics) - const oldAddLazyDiagnostics = addLazyDiagnostic; - addLazyDiagnostic = cb => cb(); - checkSourceFile(sourceFile); - addLazyDiagnostic = oldAddLazyDiagnostics; + function getDiagnostics(sourceFile: ts.SourceFile, ct: ts.CancellationToken): ts.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; + } + } + + function ensurePendingDiagnosticWorkComplete() { + // Invoke any existing lazy diagnostics to add them, clear the backlog of diagnostics + for (const cb of deferredDiagnosticsCallbacks) { + cb(); } + deferredDiagnosticsCallbacks = []; + } - function getDiagnosticsWorker(sourceFile: ts.SourceFile): ts.Diagnostic[] { - if (sourceFile) { - ensurePendingDiagnosticWorkComplete(); - // 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; + function checkSourceFileWithEagerDiagnostics(sourceFile: ts.SourceFile) { + ensurePendingDiagnosticWorkComplete(); + // then setup diagnostics for immediate invocation (as we are about to collect them, and + // this avoids the overhead of longer-lived callbacks we don't need to allocate) + // This also serves to make the shift to possibly lazy diagnostics transparent to serial command-line scenarios + // (as in those cases, all the diagnostics will still be computed as the appropriate place in the tree, + // thus much more likely retaining the same union ordering as before we had lazy diagnostics) + const oldAddLazyDiagnostics = addLazyDiagnostic; + addLazyDiagnostic = cb => cb(); + checkSourceFile(sourceFile); + addLazyDiagnostic = oldAddLazyDiagnostics; + } - checkSourceFileWithEagerDiagnostics(sourceFile); + function getDiagnosticsWorker(sourceFile: ts.SourceFile): ts.Diagnostic[] { + if (sourceFile) { + ensurePendingDiagnosticWorkComplete(); + // 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; - 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 = ts.relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, ts.compareDiagnostics); - return ts.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 ts.concatenate(currentGlobalDiagnostics, semanticDiagnostics); - } + checkSourceFileWithEagerDiagnostics(sourceFile); - return semanticDiagnostics; + 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 = ts.relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, ts.compareDiagnostics); + return ts.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 ts.concatenate(currentGlobalDiagnostics, semanticDiagnostics); } - // Global diagnostics are always added when a file is not provided to - // getDiagnostics - ts.forEach(host.getSourceFiles(), checkSourceFileWithEagerDiagnostics); - return diagnostics.getDiagnostics(); + return semanticDiagnostics; } - function getGlobalDiagnostics(): ts.Diagnostic[] { - ensurePendingDiagnosticWorkComplete(); - return diagnostics.getGlobalDiagnostics(); - } + // Global diagnostics are always added when a file is not provided to + // getDiagnostics + ts.forEach(host.getSourceFiles(), checkSourceFileWithEagerDiagnostics); + return diagnostics.getDiagnostics(); + } - // Language service support + function getGlobalDiagnostics(): ts.Diagnostic[] { + ensurePendingDiagnosticWorkComplete(); + return diagnostics.getGlobalDiagnostics(); + } - function getSymbolsInScope(location: ts.Node, meaning: ts.SymbolFlags): ts.Symbol[] { - if (location.flags & ts.NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return []; - } + // Language service support - const symbols = ts.createSymbolTable(); - let isStaticSymbol = false; + function getSymbolsInScope(location: ts.Node, meaning: ts.SymbolFlags): ts.Symbol[] { + if (location.flags & ts.NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return []; + } - populateSymbols(); + const symbols = ts.createSymbolTable(); + let isStaticSymbol = false; - symbols.delete(ts.InternalSymbolName.This); // Not a symbol, a keyword - return symbolsToArray(symbols); + populateSymbols(); - function populateSymbols() { - while (location) { - if (location.locals && !isGlobalSourceFile(location)) { - copySymbols(location.locals, meaning); - } + symbols.delete(ts.InternalSymbolName.This); // Not a symbol, a keyword + return symbolsToArray(symbols); - switch (location.kind) { - case ts.SyntaxKind.SourceFile: - if (!ts.isExternalModule(location as ts.SourceFile)) - break; - // falls through - case ts.SyntaxKind.ModuleDeclaration: - copyLocallyVisibleExportSymbols(getSymbolOfNode(location as ts.ModuleDeclaration | ts.SourceFile).exports!, meaning & ts.SymbolFlags.ModuleMember); - break; - case ts.SyntaxKind.EnumDeclaration: - copySymbols(getSymbolOfNode(location as ts.EnumDeclaration).exports!, meaning & ts.SymbolFlags.EnumMember); - break; - case ts.SyntaxKind.ClassExpression: - const className = (location as ts.ClassExpression).name; - if (className) { - copySymbol(location.symbol, meaning); - } + function populateSymbols() { + while (location) { + if (location.locals && !isGlobalSourceFile(location)) { + copySymbols(location.locals, 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 ts.SyntaxKind.ClassDeclaration: - case ts.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 (!isStaticSymbol) { - copySymbols(getMembersOfSymbol(getSymbolOfNode(location as ts.ClassDeclaration | ts.InterfaceDeclaration)), meaning & ts.SymbolFlags.Type); - } - break; - case ts.SyntaxKind.FunctionExpression: - const funcName = (location as ts.FunctionExpression).name; - if (funcName) { - copySymbol(location.symbol, meaning); - } + switch (location.kind) { + case ts.SyntaxKind.SourceFile: + if (!ts.isExternalModule(location as ts.SourceFile)) break; - } + // falls through + case ts.SyntaxKind.ModuleDeclaration: + copyLocallyVisibleExportSymbols(getSymbolOfNode(location as ts.ModuleDeclaration | ts.SourceFile).exports!, meaning & ts.SymbolFlags.ModuleMember); + break; + case ts.SyntaxKind.EnumDeclaration: + copySymbols(getSymbolOfNode(location as ts.EnumDeclaration).exports!, meaning & ts.SymbolFlags.EnumMember); + break; + case ts.SyntaxKind.ClassExpression: + const className = (location as ts.ClassExpression).name; + if (className) { + copySymbol(location.symbol, meaning); + } - if (ts.introducesArgumentsExoticObject(location)) { - copySymbol(argumentsSymbol, 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 ts.SyntaxKind.ClassDeclaration: + case ts.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 (!isStaticSymbol) { + copySymbols(getMembersOfSymbol(getSymbolOfNode(location as ts.ClassDeclaration | ts.InterfaceDeclaration)), meaning & ts.SymbolFlags.Type); + } + break; + case ts.SyntaxKind.FunctionExpression: + const funcName = (location as ts.FunctionExpression).name; + if (funcName) { + copySymbol(location.symbol, meaning); + } + break; + } - isStaticSymbol = ts.isStatic(location); - location = location.parent; + if (ts.introducesArgumentsExoticObject(location)) { + copySymbol(argumentsSymbol, meaning); } - copySymbols(globals, meaning); + isStaticSymbol = ts.isStatic(location); + location = location.parent; } - /** - * 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: ts.SymbolFlags): void { - if (ts.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); - } + 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: ts.Symbol, meaning: ts.SymbolFlags): void { + if (ts.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); } } + } - function copySymbols(source: ts.SymbolTable, meaning: ts.SymbolFlags): void { - if (meaning) { - source.forEach(symbol => { - copySymbol(symbol, meaning); - }); - } + function copySymbols(source: ts.SymbolTable, meaning: ts.SymbolFlags): void { + if (meaning) { + source.forEach(symbol => { + copySymbol(symbol, meaning); + }); } + } - function copyLocallyVisibleExportSymbols(source: ts.SymbolTable, meaning: ts.SymbolFlags): void { - if (meaning) { - source.forEach(symbol => { - // Similar condition as in `resolveNameHelper` - if (!ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ExportSpecifier) && !ts.getDeclarationOfKind(symbol, ts.SyntaxKind.NamespaceExport)) { - copySymbol(symbol, meaning); - } - }); - } + function copyLocallyVisibleExportSymbols(source: ts.SymbolTable, meaning: ts.SymbolFlags): void { + if (meaning) { + source.forEach(symbol => { + // Similar condition as in `resolveNameHelper` + if (!ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ExportSpecifier) && !ts.getDeclarationOfKind(symbol, ts.SyntaxKind.NamespaceExport)) { + copySymbol(symbol, meaning); + } + }); } } + } + + function isTypeDeclarationName(name: ts.Node): boolean { + return name.kind === ts.SyntaxKind.Identifier && + isTypeDeclaration(name.parent) && + ts.getNameOfDeclaration(name.parent) === name; + } - function isTypeDeclarationName(name: ts.Node): boolean { - return name.kind === ts.SyntaxKind.Identifier && - isTypeDeclaration(name.parent) && - ts.getNameOfDeclaration(name.parent) === name; + function isTypeDeclaration(node: ts.Node): node is ts.TypeParameterDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag | ts.EnumDeclaration | ts.ImportClause | ts.ImportSpecifier | ts.ExportSpecifier { + switch (node.kind) { + case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocEnumTag: + return true; + case ts.SyntaxKind.ImportClause: + return (node as ts.ImportClause).isTypeOnly; + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + return (node as ts.ImportSpecifier | ts.ExportSpecifier).parent.parent.isTypeOnly; + default: + return false; } + } - function isTypeDeclaration(node: ts.Node): node is ts.TypeParameterDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag | ts.EnumDeclaration | ts.ImportClause | ts.ImportSpecifier | ts.ExportSpecifier { - switch (node.kind) { - case ts.SyntaxKind.TypeParameter: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocEnumTag: - return true; - case ts.SyntaxKind.ImportClause: - return (node as ts.ImportClause).isTypeOnly; - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - return (node as ts.ImportSpecifier | ts.ExportSpecifier).parent.parent.isTypeOnly; - default: - return false; - } + // True if the given identifier is part of a type reference + function isTypeReferenceIdentifier(node: ts.EntityName): boolean { + while (node.parent.kind === ts.SyntaxKind.QualifiedName) { + node = node.parent as ts.QualifiedName; } - // True if the given identifier is part of a type reference - function isTypeReferenceIdentifier(node: ts.EntityName): boolean { - while (node.parent.kind === ts.SyntaxKind.QualifiedName) { - node = node.parent as ts.QualifiedName; - } + return node.parent.kind === ts.SyntaxKind.TypeReference; + } - return node.parent.kind === ts.SyntaxKind.TypeReference; + function isHeritageClauseElementIdentifier(node: ts.Node): boolean { + while (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + node = node.parent; } - function isHeritageClauseElementIdentifier(node: ts.Node): boolean { - while (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { - node = node.parent; - } + return node.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments; + } + + function forEachEnclosingClass(node: ts.Node, callback: (node: ts.Node) => T | undefined): T | undefined { + let result: T | undefined; - return node.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments; + while (true) { + node = ts.getContainingClass(node)!; + if (!node) + break; + if (result = callback(node)) + break; } - function forEachEnclosingClass(node: ts.Node, callback: (node: ts.Node) => T | undefined): T | undefined { - let result: T | undefined; + return result; + } - while (true) { - node = ts.getContainingClass(node)!; - if (!node) - break; - if (result = callback(node)) - break; + function isNodeUsedDuringClassInitialization(node: ts.Node) { + return !!ts.findAncestor(node, element => { + if (ts.isConstructorDeclaration(element) && ts.nodeIsPresent(element.body) || ts.isPropertyDeclaration(element)) { + return true; + } + else if (ts.isClassLike(element) || ts.isFunctionLikeDeclaration(element)) { + return "quit"; } - return result; - } + return false; + }); + } - function isNodeUsedDuringClassInitialization(node: ts.Node) { - return !!ts.findAncestor(node, element => { - if (ts.isConstructorDeclaration(element) && ts.nodeIsPresent(element.body) || ts.isPropertyDeclaration(element)) { - return true; - } - else if (ts.isClassLike(element) || ts.isFunctionLikeDeclaration(element)) { - return "quit"; - } + function isNodeWithinClass(node: ts.Node, classDeclaration: ts.ClassLikeDeclaration) { + return !!forEachEnclosingClass(node, n => n === classDeclaration); + } - return false; - }); + function getLeftSideOfImportEqualsOrExportAssignment(nodeOnRightSide: ts.EntityName): ts.ImportEqualsDeclaration | ts.ExportAssignment | undefined { + while (nodeOnRightSide.parent.kind === ts.SyntaxKind.QualifiedName) { + nodeOnRightSide = nodeOnRightSide.parent as ts.QualifiedName; } - function isNodeWithinClass(node: ts.Node, classDeclaration: ts.ClassLikeDeclaration) { - return !!forEachEnclosingClass(node, n => n === classDeclaration); + if (nodeOnRightSide.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration) { + return (nodeOnRightSide.parent as ts.ImportEqualsDeclaration).moduleReference === nodeOnRightSide ? nodeOnRightSide.parent as ts.ImportEqualsDeclaration : undefined; } - function getLeftSideOfImportEqualsOrExportAssignment(nodeOnRightSide: ts.EntityName): ts.ImportEqualsDeclaration | ts.ExportAssignment | undefined { - while (nodeOnRightSide.parent.kind === ts.SyntaxKind.QualifiedName) { - nodeOnRightSide = nodeOnRightSide.parent as ts.QualifiedName; - } + if (nodeOnRightSide.parent.kind === ts.SyntaxKind.ExportAssignment) { + return (nodeOnRightSide.parent as ts.ExportAssignment).expression === nodeOnRightSide as ts.Node ? nodeOnRightSide.parent as ts.ExportAssignment : undefined; + } - if (nodeOnRightSide.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration) { - return (nodeOnRightSide.parent as ts.ImportEqualsDeclaration).moduleReference === nodeOnRightSide ? nodeOnRightSide.parent as ts.ImportEqualsDeclaration : undefined; - } + return undefined; + } - if (nodeOnRightSide.parent.kind === ts.SyntaxKind.ExportAssignment) { - return (nodeOnRightSide.parent as ts.ExportAssignment).expression === nodeOnRightSide as ts.Node ? nodeOnRightSide.parent as ts.ExportAssignment : undefined; - } + function isInRightSideOfImportOrExportAssignment(node: ts.EntityName) { + return getLeftSideOfImportEqualsOrExportAssignment(node) !== undefined; + } - return undefined; + function getSpecialPropertyAssignmentSymbolFromEntityName(entityName: ts.EntityName | ts.PropertyAccessExpression) { + const specialPropertyAssignmentKind = ts.getAssignmentDeclarationKind(entityName.parent.parent as ts.BinaryExpression); + switch (specialPropertyAssignmentKind) { + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.PrototypeProperty: + return getSymbolOfNode(entityName.parent); + case ts.AssignmentDeclarationKind.ThisProperty: + case ts.AssignmentDeclarationKind.ModuleExports: + case ts.AssignmentDeclarationKind.Property: + return getSymbolOfNode(entityName.parent.parent); } + } - function isInRightSideOfImportOrExportAssignment(node: ts.EntityName) { - return getLeftSideOfImportEqualsOrExportAssignment(node) !== undefined; + function isImportTypeQualifierPart(node: ts.EntityName): ts.ImportTypeNode | undefined { + let parent = node.parent; + while (ts.isQualifiedName(parent)) { + node = parent; + parent = parent.parent; } - - function getSpecialPropertyAssignmentSymbolFromEntityName(entityName: ts.EntityName | ts.PropertyAccessExpression) { - const specialPropertyAssignmentKind = ts.getAssignmentDeclarationKind(entityName.parent.parent as ts.BinaryExpression); - switch (specialPropertyAssignmentKind) { - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.PrototypeProperty: - return getSymbolOfNode(entityName.parent); - case ts.AssignmentDeclarationKind.ThisProperty: - case ts.AssignmentDeclarationKind.ModuleExports: - case ts.AssignmentDeclarationKind.Property: - return getSymbolOfNode(entityName.parent.parent); - } + if (parent && parent.kind === ts.SyntaxKind.ImportType && (parent as ts.ImportTypeNode).qualifier === node) { + return parent as ts.ImportTypeNode; } + return undefined; + } - function isImportTypeQualifierPart(node: ts.EntityName): ts.ImportTypeNode | undefined { - let parent = node.parent; - while (ts.isQualifiedName(parent)) { - node = parent; - parent = parent.parent; - } - if (parent && parent.kind === ts.SyntaxKind.ImportType && (parent as ts.ImportTypeNode).qualifier === node) { - return parent as ts.ImportTypeNode; - } - return undefined; + function getSymbolOfNameOrPropertyAccessExpression(name: ts.EntityName | ts.PrivateIdentifier | ts.PropertyAccessExpression | ts.JSDocMemberName): ts.Symbol | undefined { + if (ts.isDeclarationName(name)) { + return getSymbolOfNode(name.parent); } - function getSymbolOfNameOrPropertyAccessExpression(name: ts.EntityName | ts.PrivateIdentifier | ts.PropertyAccessExpression | ts.JSDocMemberName): ts.Symbol | undefined { - if (ts.isDeclarationName(name)) { - return getSymbolOfNode(name.parent); - } - - if (ts.isInJSFile(name) && - name.parent.kind === ts.SyntaxKind.PropertyAccessExpression && - name.parent === (name.parent.parent as ts.BinaryExpression).left) { - // Check if this is a special property assignment - if (!ts.isPrivateIdentifier(name) && !ts.isJSDocMemberName(name)) { - const specialPropertyAssignmentSymbol = getSpecialPropertyAssignmentSymbolFromEntityName(name); - if (specialPropertyAssignmentSymbol) { - return specialPropertyAssignmentSymbol; - } - } - } - - if (name.parent.kind === ts.SyntaxKind.ExportAssignment && ts.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*/ ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, /*ignoreErrors*/ true); - if (success && success !== unknownSymbol) { - return success; + if (ts.isInJSFile(name) && + name.parent.kind === ts.SyntaxKind.PropertyAccessExpression && + name.parent === (name.parent.parent as ts.BinaryExpression).left) { + // Check if this is a special property assignment + if (!ts.isPrivateIdentifier(name) && !ts.isJSDocMemberName(name)) { + const specialPropertyAssignmentSymbol = getSpecialPropertyAssignmentSymbolFromEntityName(name); + if (specialPropertyAssignmentSymbol) { + return specialPropertyAssignmentSymbol; } } - else if (ts.isEntityName(name) && isInRightSideOfImportOrExportAssignment(name)) { - // Since we already checked for ExportAssignment, this really could only be an Import - const importEqualsDeclaration = ts.getAncestor(name, ts.SyntaxKind.ImportEqualsDeclaration); - ts.Debug.assert(importEqualsDeclaration !== undefined); - return getSymbolOfPartOfRightHandSideOfImportEquals(name, /*dontResolveAlias*/ true); - } + } - if (ts.isEntityName(name)) { - const possibleImportNode = isImportTypeQualifierPart(name); - if (possibleImportNode) { - getTypeFromTypeNode(possibleImportNode); - const sym = getNodeLinks(name).resolvedSymbol; - return sym === unknownSymbol ? undefined : sym; - } + if (name.parent.kind === ts.SyntaxKind.ExportAssignment && ts.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*/ ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias, /*ignoreErrors*/ true); + if (success && success !== unknownSymbol) { + return success; } + } + else if (ts.isEntityName(name) && isInRightSideOfImportOrExportAssignment(name)) { + // Since we already checked for ExportAssignment, this really could only be an Import + const importEqualsDeclaration = ts.getAncestor(name, ts.SyntaxKind.ImportEqualsDeclaration); + ts.Debug.assert(importEqualsDeclaration !== undefined); + return getSymbolOfPartOfRightHandSideOfImportEquals(name, /*dontResolveAlias*/ true); + } - while (ts.isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName(name)) { - name = name.parent as ts.QualifiedName | ts.PropertyAccessEntityNameExpression | ts.JSDocMemberName; + if (ts.isEntityName(name)) { + const possibleImportNode = isImportTypeQualifierPart(name); + if (possibleImportNode) { + getTypeFromTypeNode(possibleImportNode); + const sym = getNodeLinks(name).resolvedSymbol; + return sym === unknownSymbol ? undefined : sym; } + } - if (isHeritageClauseElementIdentifier(name)) { - let meaning = ts.SymbolFlags.None; - // In an interface or class, we're definitely interested in a type. - if (name.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { - meaning = ts.SymbolFlags.Type; + while (ts.isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName(name)) { + name = name.parent as ts.QualifiedName | ts.PropertyAccessEntityNameExpression | ts.JSDocMemberName; + } - // In a class 'extends' clause we are also looking for a value. - if (ts.isExpressionWithTypeArgumentsInClassExtendsClause(name.parent)) { - meaning |= ts.SymbolFlags.Value; - } - } - else { - meaning = ts.SymbolFlags.Namespace; - } + if (isHeritageClauseElementIdentifier(name)) { + let meaning = ts.SymbolFlags.None; + // In an interface or class, we're definitely interested in a type. + if (name.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + meaning = ts.SymbolFlags.Type; - meaning |= ts.SymbolFlags.Alias; - const entityNameSymbol = ts.isEntityNameExpression(name) ? resolveEntityName(name, meaning) : undefined; - if (entityNameSymbol) { - return entityNameSymbol; + // In a class 'extends' clause we are also looking for a value. + if (ts.isExpressionWithTypeArgumentsInClassExtendsClause(name.parent)) { + meaning |= ts.SymbolFlags.Value; } } + else { + meaning = ts.SymbolFlags.Namespace; + } - if (name.parent.kind === ts.SyntaxKind.JSDocParameterTag) { - return ts.getParameterSymbolFromJSDoc(name.parent as ts.JSDocParameterTag); + meaning |= ts.SymbolFlags.Alias; + const entityNameSymbol = ts.isEntityNameExpression(name) ? resolveEntityName(name, meaning) : undefined; + if (entityNameSymbol) { + return entityNameSymbol; } + } - if (name.parent.kind === ts.SyntaxKind.TypeParameter && name.parent.parent.kind === ts.SyntaxKind.JSDocTemplateTag) { - ts.Debug.assert(!ts.isInJSFile(name)); // Otherwise `isDeclarationName` would have been true. - const typeParameter = ts.getTypeParameterFromJsDoc(name.parent as ts.TypeParameterDeclaration & { - parent: ts.JSDocTemplateTag; - }); - return typeParameter && typeParameter.symbol; + if (name.parent.kind === ts.SyntaxKind.JSDocParameterTag) { + return ts.getParameterSymbolFromJSDoc(name.parent as ts.JSDocParameterTag); + } + + if (name.parent.kind === ts.SyntaxKind.TypeParameter && name.parent.parent.kind === ts.SyntaxKind.JSDocTemplateTag) { + ts.Debug.assert(!ts.isInJSFile(name)); // Otherwise `isDeclarationName` would have been true. + const typeParameter = ts.getTypeParameterFromJsDoc(name.parent as ts.TypeParameterDeclaration & { + parent: ts.JSDocTemplateTag; + }); + return typeParameter && typeParameter.symbol; + } + + if (ts.isExpressionNode(name)) { + if (ts.nodeIsMissing(name)) { + // Missing entity name. + return undefined; } - if (ts.isExpressionNode(name)) { - if (ts.nodeIsMissing(name)) { - // Missing entity name. - return undefined; + const isJSDoc = ts.findAncestor(name, ts.or(ts.isJSDocLinkLike, ts.isJSDocNameReference, ts.isJSDocMemberName)); + const meaning = isJSDoc ? ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Value : ts.SymbolFlags.Value; + if (name.kind === ts.SyntaxKind.Identifier) { + if (ts.isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) { + const symbol = getIntrinsicTagSymbol(name.parent as ts.JsxOpeningLikeElement); + return symbol === unknownSymbol ? undefined : symbol; } - - const isJSDoc = ts.findAncestor(name, ts.or(ts.isJSDocLinkLike, ts.isJSDocNameReference, ts.isJSDocMemberName)); - const meaning = isJSDoc ? ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Value : ts.SymbolFlags.Value; - if (name.kind === ts.SyntaxKind.Identifier) { - if (ts.isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) { - const symbol = getIntrinsicTagSymbol(name.parent as ts.JsxOpeningLikeElement); - return symbol === unknownSymbol ? undefined : symbol; + const result = resolveEntityName(name, meaning, /*ignoreErrors*/ false, /* dontResolveAlias */ true, ts.getHostSignatureFromJSDoc(name)); + if (!result && isJSDoc) { + const container = ts.findAncestor(name, ts.or(ts.isClassLike, ts.isInterfaceDeclaration)); + if (container) { + return resolveJSDocMemberName(name, getSymbolOfNode(container)); } - const result = resolveEntityName(name, meaning, /*ignoreErrors*/ false, /* dontResolveAlias */ true, ts.getHostSignatureFromJSDoc(name)); - if (!result && isJSDoc) { - const container = ts.findAncestor(name, ts.or(ts.isClassLike, ts.isInterfaceDeclaration)); - if (container) { - return resolveJSDocMemberName(name, getSymbolOfNode(container)); - } - } - return result; } - else if (ts.isPrivateIdentifier(name)) { - return getSymbolForPrivateIdentifierExpression(name); + return result; + } + else if (ts.isPrivateIdentifier(name)) { + return getSymbolForPrivateIdentifierExpression(name); + } + else if (name.kind === ts.SyntaxKind.PropertyAccessExpression || name.kind === ts.SyntaxKind.QualifiedName) { + const links = getNodeLinks(name); + if (links.resolvedSymbol) { + return links.resolvedSymbol; } - else if (name.kind === ts.SyntaxKind.PropertyAccessExpression || name.kind === ts.SyntaxKind.QualifiedName) { - const links = getNodeLinks(name); - if (links.resolvedSymbol) { - return links.resolvedSymbol; - } - if (name.kind === ts.SyntaxKind.PropertyAccessExpression) { - checkPropertyAccessExpression(name, CheckMode.Normal); - } - else { - checkQualifiedName(name, CheckMode.Normal); - } - if (!links.resolvedSymbol && isJSDoc && ts.isQualifiedName(name)) { - return resolveJSDocMemberName(name); - } - return links.resolvedSymbol; + if (name.kind === ts.SyntaxKind.PropertyAccessExpression) { + checkPropertyAccessExpression(name, CheckMode.Normal); + } + else { + checkQualifiedName(name, CheckMode.Normal); } - else if (ts.isJSDocMemberName(name)) { + if (!links.resolvedSymbol && isJSDoc && ts.isQualifiedName(name)) { return resolveJSDocMemberName(name); } + return links.resolvedSymbol; } - else if (isTypeReferenceIdentifier(name as ts.EntityName)) { - const meaning = name.parent.kind === ts.SyntaxKind.TypeReference ? ts.SymbolFlags.Type : ts.SymbolFlags.Namespace; - const symbol = resolveEntityName(name as ts.EntityName, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); - return symbol && symbol !== unknownSymbol ? symbol : getUnresolvedSymbolForEntityName(name as ts.EntityName); - } - if (name.parent.kind === ts.SyntaxKind.TypePredicate) { - return resolveEntityName(name as ts.Identifier, /*meaning*/ ts.SymbolFlags.FunctionScopedVariable); + else if (ts.isJSDocMemberName(name)) { + return resolveJSDocMemberName(name); } - - return undefined; + } + else if (isTypeReferenceIdentifier(name as ts.EntityName)) { + const meaning = name.parent.kind === ts.SyntaxKind.TypeReference ? ts.SymbolFlags.Type : ts.SymbolFlags.Namespace; + const symbol = resolveEntityName(name as ts.EntityName, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); + return symbol && symbol !== unknownSymbol ? symbol : getUnresolvedSymbolForEntityName(name as ts.EntityName); + } + if (name.parent.kind === ts.SyntaxKind.TypePredicate) { + return resolveEntityName(name as ts.Identifier, /*meaning*/ ts.SymbolFlags.FunctionScopedVariable); } - /** - * Recursively resolve entity names and jsdoc instance references: - * 1. K#m as K.prototype.m for a class (or other value) K - * 2. K.m as K.prototype.m - * 3. I.m as I.m for a type I, or any other I.m that fails to resolve in (1) or (2) - * - * For unqualified names, a container K may be provided as a second argument. - */ - function resolveJSDocMemberName(name: ts.EntityName | ts.JSDocMemberName, container?: ts.Symbol): ts.Symbol | undefined { - if (ts.isEntityName(name)) { - // resolve static values first - const meaning = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Value; - let symbol = resolveEntityName(name, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true, ts.getHostSignatureFromJSDoc(name)); - if (!symbol && ts.isIdentifier(name) && container) { - symbol = getMergedSymbol(getSymbol(getExportsOfSymbol(container), name.escapedText, meaning)); - } - if (symbol) { - return symbol; - } + return undefined; + } + + /** + * Recursively resolve entity names and jsdoc instance references: + * 1. K#m as K.prototype.m for a class (or other value) K + * 2. K.m as K.prototype.m + * 3. I.m as I.m for a type I, or any other I.m that fails to resolve in (1) or (2) + * + * For unqualified names, a container K may be provided as a second argument. + */ + function resolveJSDocMemberName(name: ts.EntityName | ts.JSDocMemberName, container?: ts.Symbol): ts.Symbol | undefined { + if (ts.isEntityName(name)) { + // resolve static values first + const meaning = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Value; + let symbol = resolveEntityName(name, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true, ts.getHostSignatureFromJSDoc(name)); + if (!symbol && ts.isIdentifier(name) && container) { + symbol = getMergedSymbol(getSymbol(getExportsOfSymbol(container), name.escapedText, meaning)); } - const left = ts.isIdentifier(name) ? container : resolveJSDocMemberName(name.left); - const right = ts.isIdentifier(name) ? name.escapedText : name.right.escapedText; - if (left) { - const proto = left.flags & ts.SymbolFlags.Value && getPropertyOfType(getTypeOfSymbol(left), "prototype" as ts.__String); - const t = proto ? getTypeOfSymbol(proto) : getDeclaredTypeOfSymbol(left); - return getPropertyOfType(t, right); + if (symbol) { + return symbol; } } + const left = ts.isIdentifier(name) ? container : resolveJSDocMemberName(name.left); + const right = ts.isIdentifier(name) ? name.escapedText : name.right.escapedText; + if (left) { + const proto = left.flags & ts.SymbolFlags.Value && getPropertyOfType(getTypeOfSymbol(left), "prototype" as ts.__String); + const t = proto ? getTypeOfSymbol(proto) : getDeclaredTypeOfSymbol(left); + return getPropertyOfType(t, right); + } + } - function getSymbolAtLocation(node: ts.Node, ignoreErrors?: boolean): ts.Symbol | undefined { - if (node.kind === ts.SyntaxKind.SourceFile) { - return ts.isExternalModule(node as ts.SourceFile) ? getMergedSymbol(node.symbol) : undefined; - } - const { parent } = node; - const grandParent = parent.parent; + function getSymbolAtLocation(node: ts.Node, ignoreErrors?: boolean): ts.Symbol | undefined { + if (node.kind === ts.SyntaxKind.SourceFile) { + return ts.isExternalModule(node as ts.SourceFile) ? getMergedSymbol(node.symbol) : undefined; + } + const { parent } = node; + const grandParent = parent.parent; - if (node.flags & ts.NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return undefined; - } + if (node.flags & ts.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 ts.isImportOrExportSpecifier(node.parent) && node.parent.propertyName === node - ? getImmediateAliasedSymbol(parentSymbol) - : parentSymbol; - } - else if (ts.isLiteralComputedPropertyDeclarationName(node)) { - return getSymbolOfNode(parent.parent); + if (isDeclarationNameOrImportPropertyName(node)) { + // This is a declaration, call getSymbolOfNode + const parentSymbol = getSymbolOfNode(parent)!; + return ts.isImportOrExportSpecifier(node.parent) && node.parent.propertyName === node + ? getImmediateAliasedSymbol(parentSymbol) + : parentSymbol; + } + else if (ts.isLiteralComputedPropertyDeclarationName(node)) { + return getSymbolOfNode(parent.parent); + } + + if (node.kind === ts.SyntaxKind.Identifier) { + if (isInRightSideOfImportOrExportAssignment(node as ts.Identifier)) { + return getSymbolOfNameOrPropertyAccessExpression(node as ts.Identifier); } + else if (parent.kind === ts.SyntaxKind.BindingElement && + grandParent.kind === ts.SyntaxKind.ObjectBindingPattern && + node === (parent as ts.BindingElement).propertyName) { + const typeOfPattern = getTypeOfNode(grandParent); + const propertyDeclaration = getPropertyOfType(typeOfPattern, (node as ts.Identifier).escapedText); - if (node.kind === ts.SyntaxKind.Identifier) { - if (isInRightSideOfImportOrExportAssignment(node as ts.Identifier)) { - return getSymbolOfNameOrPropertyAccessExpression(node as ts.Identifier); + if (propertyDeclaration) { + return propertyDeclaration; } - else if (parent.kind === ts.SyntaxKind.BindingElement && - grandParent.kind === ts.SyntaxKind.ObjectBindingPattern && - node === (parent as ts.BindingElement).propertyName) { - const typeOfPattern = getTypeOfNode(grandParent); - const propertyDeclaration = getPropertyOfType(typeOfPattern, (node as ts.Identifier).escapedText); - - if (propertyDeclaration) { - return propertyDeclaration; - } + } + else if (ts.isMetaProperty(parent) && parent.name === node) { + if (parent.keywordToken === ts.SyntaxKind.NewKeyword && ts.idText(node as ts.Identifier) === "target") { + // `target` in `new.target` + return checkNewTargetMetaProperty(parent).symbol; } - else if (ts.isMetaProperty(parent) && parent.name === node) { - if (parent.keywordToken === ts.SyntaxKind.NewKeyword && ts.idText(node as ts.Identifier) === "target") { - // `target` in `new.target` - return checkNewTargetMetaProperty(parent).symbol; - } - // The `meta` in `import.meta` could be given `getTypeOfNode(parent).symbol` (the `ImportMeta` interface symbol), but - // we have a fake expression type made for other reasons already, whose transient `meta` - // member should more exactly be the kind of (declarationless) symbol we want. - // (See #44364 and #45031 for relevant implementation PRs) - if (parent.keywordToken === ts.SyntaxKind.ImportKeyword && ts.idText(node as ts.Identifier) === "meta") { - return getGlobalImportMetaExpressionType().members!.get("meta" as ts.__String); - } - // no other meta properties are valid syntax, thus no others should have symbols - return undefined; + // The `meta` in `import.meta` could be given `getTypeOfNode(parent).symbol` (the `ImportMeta` interface symbol), but + // we have a fake expression type made for other reasons already, whose transient `meta` + // member should more exactly be the kind of (declarationless) symbol we want. + // (See #44364 and #45031 for relevant implementation PRs) + if (parent.keywordToken === ts.SyntaxKind.ImportKeyword && ts.idText(node as ts.Identifier) === "meta") { + return getGlobalImportMetaExpressionType().members!.get("meta" as ts.__String); } + // no other meta properties are valid syntax, thus no others should have symbols + return undefined; } + } - switch (node.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PrivateIdentifier: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.QualifiedName: - if (!ts.isThisInTypeQuery(node)) { - return getSymbolOfNameOrPropertyAccessExpression(node as ts.EntityName | ts.PrivateIdentifier | ts.PropertyAccessExpression); - } - // falls through + switch (node.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PrivateIdentifier: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.QualifiedName: + if (!ts.isThisInTypeQuery(node)) { + return getSymbolOfNameOrPropertyAccessExpression(node as ts.EntityName | ts.PrivateIdentifier | ts.PropertyAccessExpression); + } + // falls through - case ts.SyntaxKind.ThisKeyword: - const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); - if (ts.isFunctionLike(container)) { - const sig = getSignatureFromDeclaration(container); - if (sig.thisParameter) { - return sig.thisParameter; - } + case ts.SyntaxKind.ThisKeyword: + const container = ts.getThisContainer(node, /*includeArrowFunctions*/ false); + if (ts.isFunctionLike(container)) { + const sig = getSignatureFromDeclaration(container); + if (sig.thisParameter) { + return sig.thisParameter; } - if (ts.isInExpressionContext(node)) { - return checkExpression(node as ts.Expression).symbol; - } - // falls through - - case ts.SyntaxKind.ThisType: - return getTypeFromThisTypeNode(node as ts.ThisExpression | ts.ThisTypeNode).symbol; - case ts.SyntaxKind.SuperKeyword: + } + if (ts.isInExpressionContext(node)) { return checkExpression(node as ts.Expression).symbol; - case ts.SyntaxKind.ConstructorKeyword: - // constructor keyword for an overload, should take us to the definition if it exist - const constructorDeclaration = node.parent; - if (constructorDeclaration && constructorDeclaration.kind === ts.SyntaxKind.Constructor) { - return (constructorDeclaration.parent as ts.ClassDeclaration).symbol; - } - return undefined; + } + // falls through - case ts.SyntaxKind.StringLiteral: - case ts.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 ((ts.isExternalModuleImportEqualsDeclaration(node.parent.parent) && ts.getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) || - ((node.parent.kind === ts.SyntaxKind.ImportDeclaration || node.parent.kind === ts.SyntaxKind.ExportDeclaration) && (node.parent as ts.ImportDeclaration).moduleSpecifier === node) || - ((ts.isInJSFile(node) && ts.isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || ts.isImportCall(node.parent)) || - (ts.isLiteralTypeNode(node.parent) && ts.isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)) { - return resolveExternalModuleName(node, node as ts.LiteralExpression, ignoreErrors); - } - if (ts.isCallExpression(parent) && ts.isBindableObjectDefinePropertyCall(parent) && parent.arguments[1] === node) { - return getSymbolOfNode(parent); - } - // falls through + case ts.SyntaxKind.ThisType: + return getTypeFromThisTypeNode(node as ts.ThisExpression | ts.ThisTypeNode).symbol; + case ts.SyntaxKind.SuperKeyword: + return checkExpression(node as ts.Expression).symbol; + case ts.SyntaxKind.ConstructorKeyword: + // constructor keyword for an overload, should take us to the definition if it exist + const constructorDeclaration = node.parent; + if (constructorDeclaration && constructorDeclaration.kind === ts.SyntaxKind.Constructor) { + return (constructorDeclaration.parent as ts.ClassDeclaration).symbol; + } + return undefined; - case ts.SyntaxKind.NumericLiteral: - // index access - const objectType = ts.isElementAccessExpression(parent) - ? parent.argumentExpression === node ? getTypeOfExpression(parent.expression) : undefined - : ts.isLiteralTypeNode(parent) && ts.isIndexedAccessTypeNode(grandParent) - ? getTypeFromTypeNode(grandParent.objectType) - : undefined; - return objectType && getPropertyOfType(objectType, ts.escapeLeadingUnderscores((node as ts.StringLiteral | ts.NumericLiteral).text)); - case ts.SyntaxKind.DefaultKeyword: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.EqualsGreaterThanToken: - case ts.SyntaxKind.ClassKeyword: - return getSymbolOfNode(node.parent); - case ts.SyntaxKind.ImportType: - return ts.isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal, ignoreErrors) : undefined; - case ts.SyntaxKind.ExportKeyword: - return ts.isExportAssignment(node.parent) ? ts.Debug.checkDefined(node.parent.symbol) : undefined; - case ts.SyntaxKind.ImportKeyword: - case ts.SyntaxKind.NewKeyword: - return ts.isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined; - case ts.SyntaxKind.MetaProperty: - return checkExpression(node as ts.Expression).symbol; + case ts.SyntaxKind.StringLiteral: + case ts.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 ((ts.isExternalModuleImportEqualsDeclaration(node.parent.parent) && ts.getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) || + ((node.parent.kind === ts.SyntaxKind.ImportDeclaration || node.parent.kind === ts.SyntaxKind.ExportDeclaration) && (node.parent as ts.ImportDeclaration).moduleSpecifier === node) || + ((ts.isInJSFile(node) && ts.isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || ts.isImportCall(node.parent)) || + (ts.isLiteralTypeNode(node.parent) && ts.isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)) { + return resolveExternalModuleName(node, node as ts.LiteralExpression, ignoreErrors); + } + if (ts.isCallExpression(parent) && ts.isBindableObjectDefinePropertyCall(parent) && parent.arguments[1] === node) { + return getSymbolOfNode(parent); + } + // falls through + + case ts.SyntaxKind.NumericLiteral: + // index access + const objectType = ts.isElementAccessExpression(parent) + ? parent.argumentExpression === node ? getTypeOfExpression(parent.expression) : undefined + : ts.isLiteralTypeNode(parent) && ts.isIndexedAccessTypeNode(grandParent) + ? getTypeFromTypeNode(grandParent.objectType) + : undefined; + return objectType && getPropertyOfType(objectType, ts.escapeLeadingUnderscores((node as ts.StringLiteral | ts.NumericLiteral).text)); + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.FunctionKeyword: + case ts.SyntaxKind.EqualsGreaterThanToken: + case ts.SyntaxKind.ClassKeyword: + return getSymbolOfNode(node.parent); + case ts.SyntaxKind.ImportType: + return ts.isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal, ignoreErrors) : undefined; + case ts.SyntaxKind.ExportKeyword: + return ts.isExportAssignment(node.parent) ? ts.Debug.checkDefined(node.parent.symbol) : undefined; + case ts.SyntaxKind.ImportKeyword: + case ts.SyntaxKind.NewKeyword: + return ts.isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined; + case ts.SyntaxKind.MetaProperty: + return checkExpression(node as ts.Expression).symbol; - default: - return undefined; - } + default: + return undefined; } + } - function getIndexInfosAtLocation(node: ts.Node): readonly ts.IndexInfo[] | undefined { - if (ts.isIdentifier(node) && ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) { - const keyType = getLiteralTypeFromPropertyName(node); - const objectType = getTypeOfExpression(node.parent.expression); - const objectTypes = objectType.flags & ts.TypeFlags.Union ? (objectType as ts.UnionType).types : [objectType]; - return ts.flatMap(objectTypes, t => ts.filter(getIndexInfosOfType(t), info => isApplicableIndexType(keyType, info.keyType))); - } - return undefined; + function getIndexInfosAtLocation(node: ts.Node): readonly ts.IndexInfo[] | undefined { + if (ts.isIdentifier(node) && ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) { + const keyType = getLiteralTypeFromPropertyName(node); + const objectType = getTypeOfExpression(node.parent.expression); + const objectTypes = objectType.flags & ts.TypeFlags.Union ? (objectType as ts.UnionType).types : [objectType]; + return ts.flatMap(objectTypes, t => ts.filter(getIndexInfosOfType(t), info => isApplicableIndexType(keyType, info.keyType))); } + return undefined; + } - function getShorthandAssignmentValueSymbol(location: ts.Node | undefined): ts.Symbol | undefined { - if (location && location.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - return resolveEntityName((location as ts.ShorthandPropertyAssignment).name, ts.SymbolFlags.Value | ts.SymbolFlags.Alias); - } - return undefined; + function getShorthandAssignmentValueSymbol(location: ts.Node | undefined): ts.Symbol | undefined { + if (location && location.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + return resolveEntityName((location as ts.ShorthandPropertyAssignment).name, ts.SymbolFlags.Value | ts.SymbolFlags.Alias); } + return undefined; + } - /** Returns the target of an export specifier without following aliases */ - function getExportSpecifierLocalTargetSymbol(node: ts.ExportSpecifier | ts.Identifier): ts.Symbol | undefined { - if (ts.isExportSpecifier(node)) { - return node.parent.parent.moduleSpecifier ? - getExternalModuleMember(node.parent.parent, node) : - resolveEntityName(node.propertyName || node.name, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); - } - else { - return resolveEntityName(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); - } + /** Returns the target of an export specifier without following aliases */ + function getExportSpecifierLocalTargetSymbol(node: ts.ExportSpecifier | ts.Identifier): ts.Symbol | undefined { + if (ts.isExportSpecifier(node)) { + return node.parent.parent.moduleSpecifier ? + getExternalModuleMember(node.parent.parent, node) : + resolveEntityName(node.propertyName || node.name, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); } + else { + return resolveEntityName(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias); + } + } - function getTypeOfNode(node: ts.Node): ts.Type { - if (ts.isSourceFile(node) && !ts.isExternalModule(node)) { - return errorType; - } - - if (node.flags & ts.NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return errorType; - } + function getTypeOfNode(node: ts.Node): ts.Type { + if (ts.isSourceFile(node) && !ts.isExternalModule(node)) { + return errorType; + } - const classDecl = ts.tryGetClassImplementingOrExtendingExpressionWithTypeArguments(node); - const classType = classDecl && getDeclaredTypeOfClassOrInterface(getSymbolOfNode(classDecl.class)); - if (ts.isPartOfTypeNode(node)) { - const typeFromTypeNode = getTypeFromTypeNode(node as ts.TypeNode); - return classType ? getTypeWithThisArgument(typeFromTypeNode, classType.thisType) : typeFromTypeNode; - } + if (node.flags & ts.NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return errorType; + } - if (ts.isExpressionNode(node)) { - return getRegularTypeOfExpression(node as ts.Expression); - } + const classDecl = ts.tryGetClassImplementingOrExtendingExpressionWithTypeArguments(node); + const classType = classDecl && getDeclaredTypeOfClassOrInterface(getSymbolOfNode(classDecl.class)); + if (ts.isPartOfTypeNode(node)) { + const typeFromTypeNode = getTypeFromTypeNode(node as ts.TypeNode); + return classType ? getTypeWithThisArgument(typeFromTypeNode, classType.thisType) : typeFromTypeNode; + } - 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 = ts.firstOrUndefined(getBaseTypes(classType)); - return baseType ? getTypeWithThisArgument(baseType, classType.thisType) : errorType; - } + if (ts.isExpressionNode(node)) { + return getRegularTypeOfExpression(node as ts.Expression); + } - 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 (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 = ts.firstOrUndefined(getBaseTypes(classType)); + return baseType ? getTypeWithThisArgument(baseType, classType.thisType) : errorType; + } - if (isTypeDeclarationName(node)) { - const symbol = getSymbolAtLocation(node); - return symbol ? getDeclaredTypeOfSymbol(symbol) : 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 (ts.isDeclaration(node)) { - // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration - const symbol = getSymbolOfNode(node); - return symbol ? getTypeOfSymbol(symbol) : errorType; - } + if (isTypeDeclarationName(node)) { + const symbol = getSymbolAtLocation(node); + return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; + } - if (isDeclarationNameOrImportPropertyName(node)) { - const symbol = getSymbolAtLocation(node); - if (symbol) { - return getTypeOfSymbol(symbol); - } - return errorType; - } + if (ts.isDeclaration(node)) { + // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration + const symbol = getSymbolOfNode(node); + return symbol ? getTypeOfSymbol(symbol) : errorType; + } - if (ts.isBindingPattern(node)) { - return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true, CheckMode.Normal) || errorType; + if (isDeclarationNameOrImportPropertyName(node)) { + const symbol = getSymbolAtLocation(node); + if (symbol) { + return getTypeOfSymbol(symbol); } + return errorType; + } - if (isInRightSideOfImportOrExportAssignment(node as ts.Identifier)) { - const symbol = getSymbolAtLocation(node); - if (symbol) { - const declaredType = getDeclaredTypeOfSymbol(symbol); - return !isErrorType(declaredType) ? declaredType : getTypeOfSymbol(symbol); - } - } + if (ts.isBindingPattern(node)) { + return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true, CheckMode.Normal) || errorType; + } - if (ts.isMetaProperty(node.parent) && node.parent.keywordToken === node.kind) { - return checkMetaPropertyKeyword(node.parent); + if (isInRightSideOfImportOrExportAssignment(node as ts.Identifier)) { + const symbol = getSymbolAtLocation(node); + if (symbol) { + const declaredType = getDeclaredTypeOfSymbol(symbol); + return !isErrorType(declaredType) ? declaredType : getTypeOfSymbol(symbol); } + } - return errorType; + if (ts.isMetaProperty(node.parent) && node.parent.keywordToken === node.kind) { + return checkMetaPropertyKeyword(node.parent); } - // Gets the type of object literal or array literal of destructuring assignment. - // { a } from + 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: ts.AssignmentPattern): ts.Type | undefined { + ts.Debug.assert(expr.kind === ts.SyntaxKind.ObjectLiteralExpression || expr.kind === ts.SyntaxKind.ArrayLiteralExpression); + // If this is from "for of" // for ( { a } of elems) { // } - // [ a ] from - // [a] = [ some array ...] - function getTypeOfAssignmentPattern(expr: ts.AssignmentPattern): ts.Type | undefined { - ts.Debug.assert(expr.kind === ts.SyntaxKind.ObjectLiteralExpression || expr.kind === ts.SyntaxKind.ArrayLiteralExpression); - // If this is from "for of" - // for ( { a } of elems) { - // } - if (expr.parent.kind === ts.SyntaxKind.ForOfStatement) { - const iteratedType = checkRightHandSideOfForOf(expr.parent as ts.ForOfStatement); - return checkDestructuringAssignment(expr, iteratedType || errorType); - } - // If this is from "for" initializer - // for ({a } = elems[0];.....) { } - if (expr.parent.kind === ts.SyntaxKind.BinaryExpression) { - const iteratedType = getTypeOfExpression((expr.parent as ts.BinaryExpression).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 === ts.SyntaxKind.PropertyAssignment) { - const node = ts.cast(expr.parent.parent, ts.isObjectLiteralExpression); - const typeOfParentObjectLiteral = getTypeOfAssignmentPattern(node) || errorType; - const propertyIndex = ts.indexOfNode(node.properties, expr.parent); - return checkObjectLiteralDestructuringPropertyAssignment(node, typeOfParentObjectLiteral, propertyIndex); - } - // Array literal assignment - array destructuring pattern - const node = ts.cast(expr.parent, ts.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: ts.Identifier) { - // Get the type of the object or array literal and then look for property of given name in the type - const typeOfObjectLiteral = getTypeOfAssignmentPattern(ts.cast(location.parent.parent, ts.isAssignmentPattern)); - return typeOfObjectLiteral && getPropertyOfType(typeOfObjectLiteral, location.escapedText); - } + if (expr.parent.kind === ts.SyntaxKind.ForOfStatement) { + const iteratedType = checkRightHandSideOfForOf(expr.parent as ts.ForOfStatement); + return checkDestructuringAssignment(expr, iteratedType || errorType); + } + // If this is from "for" initializer + // for ({a } = elems[0];.....) { } + if (expr.parent.kind === ts.SyntaxKind.BinaryExpression) { + const iteratedType = getTypeOfExpression((expr.parent as ts.BinaryExpression).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 === ts.SyntaxKind.PropertyAssignment) { + const node = ts.cast(expr.parent.parent, ts.isObjectLiteralExpression); + const typeOfParentObjectLiteral = getTypeOfAssignmentPattern(node) || errorType; + const propertyIndex = ts.indexOfNode(node.properties, expr.parent); + return checkObjectLiteralDestructuringPropertyAssignment(node, typeOfParentObjectLiteral, propertyIndex); + } + // Array literal assignment - array destructuring pattern + const node = ts.cast(expr.parent, ts.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); + } - function getRegularTypeOfExpression(expr: ts.Expression): ts.Type { - if (ts.isRightSideOfQualifiedNameOrPropertyAccess(expr)) { - expr = expr.parent as ts.Expression; - } - return getRegularTypeOfLiteralType(getTypeOfExpression(expr)); - } + // 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: ts.Identifier) { + // Get the type of the object or array literal and then look for property of given name in the type + const typeOfObjectLiteral = getTypeOfAssignmentPattern(ts.cast(location.parent.parent, ts.isAssignmentPattern)); + return typeOfObjectLiteral && getPropertyOfType(typeOfObjectLiteral, location.escapedText); + } - /** - * Gets either the static or instance type of a class element, based on - * whether the element is declared as "static". - */ - function getParentTypeOfClassElement(node: ts.ClassElement) { - const classSymbol = getSymbolOfNode(node.parent)!; - return ts.isStatic(node) - ? getTypeOfSymbol(classSymbol) - : getDeclaredTypeOfSymbol(classSymbol); + function getRegularTypeOfExpression(expr: ts.Expression): ts.Type { + if (ts.isRightSideOfQualifiedNameOrPropertyAccess(expr)) { + expr = expr.parent as ts.Expression; } + return getRegularTypeOfLiteralType(getTypeOfExpression(expr)); + } - function getClassElementPropertyKeyType(element: ts.ClassElement) { - const name = element.name!; - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return getStringLiteralType(ts.idText(name)); - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.StringLiteral: - return getStringLiteralType(name.text); - case ts.SyntaxKind.ComputedPropertyName: - const nameType = checkComputedPropertyName(name); - return isTypeAssignableToKind(nameType, ts.TypeFlags.ESSymbolLike) ? nameType : stringType; - default: - return ts.Debug.fail("Unsupported property name."); - } - } + /** + * Gets either the static or instance type of a class element, based on + * whether the element is declared as "static". + */ + function getParentTypeOfClassElement(node: ts.ClassElement) { + const classSymbol = getSymbolOfNode(node.parent)!; + return ts.isStatic(node) + ? getTypeOfSymbol(classSymbol) + : getDeclaredTypeOfSymbol(classSymbol); + } - // 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 = ts.createSymbolTable(getPropertiesOfType(type)); - const functionType = getSignaturesOfType(type, ts.SignatureKind.Call).length ? globalCallableFunctionType : - getSignaturesOfType(type, ts.SignatureKind.Construct).length ? globalNewableFunctionType : - undefined; - if (functionType) { - ts.forEach(getPropertiesOfType(functionType), p => { - if (!propsByName.has(p.escapedName)) { - propsByName.set(p.escapedName, p); - } - }); - } - return getNamedMembers(propsByName); + function getClassElementPropertyKeyType(element: ts.ClassElement) { + const name = element.name!; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return getStringLiteralType(ts.idText(name)); + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.StringLiteral: + return getStringLiteralType(name.text); + case ts.SyntaxKind.ComputedPropertyName: + const nameType = checkComputedPropertyName(name); + return isTypeAssignableToKind(nameType, ts.TypeFlags.ESSymbolLike) ? nameType : stringType; + default: + return ts.Debug.fail("Unsupported property name."); } + } - function typeHasCallOrConstructSignatures(type: ts.Type): boolean { - return ts.typeHasCallOrConstructSignatures(type, checker); + // 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 = ts.createSymbolTable(getPropertiesOfType(type)); + const functionType = getSignaturesOfType(type, ts.SignatureKind.Call).length ? globalCallableFunctionType : + getSignaturesOfType(type, ts.SignatureKind.Construct).length ? globalNewableFunctionType : + undefined; + if (functionType) { + ts.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 ? ts.flatMap(roots, getRootSymbols) : [symbol]; + function getRootSymbols(symbol: ts.Symbol): readonly ts.Symbol[] { + const roots = getImmediateRootSymbols(symbol); + return roots ? ts.flatMap(roots, getRootSymbols) : [symbol]; + } + function getImmediateRootSymbols(symbol: ts.Symbol): readonly ts.Symbol[] | undefined { + if (ts.getCheckFlags(symbol) & ts.CheckFlags.Synthetic) { + return ts.mapDefined(getSymbolLinks(symbol).containingType!.types, type => getPropertyOfType(type, symbol.escapedName)); } - function getImmediateRootSymbols(symbol: ts.Symbol): readonly ts.Symbol[] | undefined { - if (ts.getCheckFlags(symbol) & ts.CheckFlags.Synthetic) { - return ts.mapDefined(getSymbolLinks(symbol).containingType!.types, type => getPropertyOfType(type, symbol.escapedName)); - } - else if (symbol.flags & ts.SymbolFlags.Transient) { - const { leftSpread, rightSpread, syntheticOrigin } = symbol as ts.TransientSymbol; - return leftSpread ? [leftSpread, rightSpread!] - : syntheticOrigin ? [syntheticOrigin] - : ts.singleElementArray(tryGetTarget(symbol)); - } - return undefined; + else if (symbol.flags & ts.SymbolFlags.Transient) { + const { leftSpread, rightSpread, syntheticOrigin } = symbol as ts.TransientSymbol; + return leftSpread ? [leftSpread, rightSpread!] + : syntheticOrigin ? [syntheticOrigin] + : ts.singleElementArray(tryGetTarget(symbol)); } - function tryGetTarget(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; + return undefined; + } + function tryGetTarget(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 + // Emitter support - function isArgumentsLocalBinding(nodeIn: ts.Identifier): boolean { - // Note: does not handle isShorthandPropertyAssignment (and probably a few more) - if (ts.isGeneratedIdentifier(nodeIn)) - return false; - const node = ts.getParseTreeNode(nodeIn, ts.isIdentifier); - if (!node) - return false; - const parent = node.parent; - if (!parent) - return false; - const isPropertyName = ((ts.isPropertyAccessExpression(parent) - || ts.isPropertyAssignment(parent)) - && parent.name === node); - return !isPropertyName && getReferencedValueSymbol(node) === argumentsSymbol; + function isArgumentsLocalBinding(nodeIn: ts.Identifier): boolean { + // Note: does not handle isShorthandPropertyAssignment (and probably a few more) + if (ts.isGeneratedIdentifier(nodeIn)) + return false; + const node = ts.getParseTreeNode(nodeIn, ts.isIdentifier); + if (!node) + return false; + const parent = node.parent; + if (!parent) + return false; + const isPropertyName = ((ts.isPropertyAccessExpression(parent) + || ts.isPropertyAssignment(parent)) + && parent.name === node); + return !isPropertyName && getReferencedValueSymbol(node) === argumentsSymbol; + } + + function moduleExportsSomeValue(moduleReferenceExpression: ts.Expression): boolean { + let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression); + if (!moduleSymbol || ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { + // If the module is not found or is shorthand, assume that it may export a value. + return true; } - function moduleExportsSomeValue(moduleReferenceExpression: ts.Expression): boolean { - let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression); - if (!moduleSymbol || ts.isShorthandAmbientModuleSymbol(moduleSymbol)) { - // If the module is not found or is shorthand, assume that it may export a value. - return true; - } + 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 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 & ts.SymbolFlags.Value) + : ts.forEachEntry(getExportsOfModule(moduleSymbol), isValue); + } - 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 & ts.SymbolFlags.Value) - : ts.forEachEntry(getExportsOfModule(moduleSymbol), isValue); - } + return symbolLinks.exportsSomeValue!; - return symbolLinks.exportsSomeValue!; + function isValue(s: ts.Symbol): boolean { + s = resolveSymbol(s); + return s && !!(s.flags & ts.SymbolFlags.Value); + } + } + + function isNameOfModuleOrEnumDeclaration(node: ts.Identifier) { + return ts.isModuleOrEnumDeclaration(node.parent) && node === node.parent.name; + } - function isValue(s: ts.Symbol): boolean { - s = resolveSymbol(s); - return s && !!(s.flags & ts.SymbolFlags.Value); + // 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: ts.Identifier, prefixLocals?: boolean): ts.SourceFile | ts.ModuleDeclaration | ts.EnumDeclaration | undefined { + const node = ts.getParseTreeNode(nodeIn, ts.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 & ts.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 & ts.SymbolFlags.ExportHasLocal && !(exportSymbol.flags & ts.SymbolFlags.Variable)) { + return undefined; + } + symbol = exportSymbol; + } + const parentSymbol = getParentOfSymbol(symbol); + if (parentSymbol) { + if (parentSymbol.flags & ts.SymbolFlags.ValueModule && parentSymbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile) { + const symbolFile = parentSymbol.valueDeclaration as ts.SourceFile; + const referenceFile = ts.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 ts.findAncestor(node.parent, (n): n is ts.ModuleDeclaration | ts.EnumDeclaration => ts.isModuleOrEnumDeclaration(n) && getSymbolOfNode(n) === parentSymbol); + } } } + } - function isNameOfModuleOrEnumDeclaration(node: ts.Identifier) { - return ts.isModuleOrEnumDeclaration(node.parent) && node === node.parent.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: ts.Identifier): ts.Declaration | undefined { + if (nodeIn.generatedImportReference) { + return nodeIn.generatedImportReference; + } + const node = ts.getParseTreeNode(nodeIn, ts.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*/ ts.SymbolFlags.Value) && !getTypeOnlyAliasDeclaration(symbol)) { + return getDeclarationOfAliasSymbol(symbol); + } } - // 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: ts.Identifier, prefixLocals?: boolean): ts.SourceFile | ts.ModuleDeclaration | ts.EnumDeclaration | undefined { - const node = ts.getParseTreeNode(nodeIn, ts.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 & ts.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 & ts.SymbolFlags.ExportHasLocal && !(exportSymbol.flags & ts.SymbolFlags.Variable)) { - return undefined; - } - symbol = exportSymbol; + return undefined; + } + + function isSymbolOfDestructuredElementOfCatchBinding(symbol: ts.Symbol) { + return symbol.valueDeclaration + && ts.isBindingElement(symbol.valueDeclaration) + && ts.walkUpBindingElementsAndPatterns(symbol.valueDeclaration).parent.kind === ts.SyntaxKind.CatchClause; + } + + function isSymbolOfDeclarationWithCollidingName(symbol: ts.Symbol): boolean { + if (symbol.flags & ts.SymbolFlags.BlockScoped && symbol.valueDeclaration && !ts.isSourceFile(symbol.valueDeclaration)) { + const links = getSymbolLinks(symbol); + if (links.isDeclarationWithCollidingName === undefined) { + const container = ts.getEnclosingBlockScopeContainer(symbol.valueDeclaration); + if (ts.isStatementWithLocals(container) || isSymbolOfDestructuredElementOfCatchBinding(symbol)) { + const nodeLinks = getNodeLinks(symbol.valueDeclaration); + if (resolveName(container.parent, symbol.escapedName, ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)) { + // redeclaration - always should be renamed + links.isDeclarationWithCollidingName = true; + } + else if (nodeLinks.flags & ts.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 & ts.NodeCheckFlags.BlockScopedBindingInLoop; + const inLoopInitializer = ts.isIterationStatement(container, /*lookInLabeledStatements*/ false); + const inLoopBodyBlock = container.kind === ts.SyntaxKind.Block && ts.isIterationStatement(container.parent, /*lookInLabeledStatements*/ false); + links.isDeclarationWithCollidingName = !ts.isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock)); } - const parentSymbol = getParentOfSymbol(symbol); - if (parentSymbol) { - if (parentSymbol.flags & ts.SymbolFlags.ValueModule && parentSymbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile) { - const symbolFile = parentSymbol.valueDeclaration as ts.SourceFile; - const referenceFile = ts.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 ts.findAncestor(node.parent, (n): n is ts.ModuleDeclaration | ts.EnumDeclaration => ts.isModuleOrEnumDeclaration(n) && getSymbolOfNode(n) === parentSymbol); + else { + links.isDeclarationWithCollidingName = false; } } } + return links.isDeclarationWithCollidingName!; } + return false; + } - // 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: ts.Identifier): ts.Declaration | undefined { - if (nodeIn.generatedImportReference) { - return nodeIn.generatedImportReference; - } + // 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: ts.Identifier): ts.Declaration | undefined { + if (!ts.isGeneratedIdentifier(nodeIn)) { const node = ts.getParseTreeNode(nodeIn, ts.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*/ ts.SymbolFlags.Value) && !getTypeOnlyAliasDeclaration(symbol)) { - return getDeclarationOfAliasSymbol(symbol); + if (symbol && isSymbolOfDeclarationWithCollidingName(symbol)) { + return symbol.valueDeclaration; } } - - return undefined; } - function isSymbolOfDestructuredElementOfCatchBinding(symbol: ts.Symbol) { - return symbol.valueDeclaration - && ts.isBindingElement(symbol.valueDeclaration) - && ts.walkUpBindingElementsAndPatterns(symbol.valueDeclaration).parent.kind === ts.SyntaxKind.CatchClause; - } + return undefined; + } - function isSymbolOfDeclarationWithCollidingName(symbol: ts.Symbol): boolean { - if (symbol.flags & ts.SymbolFlags.BlockScoped && symbol.valueDeclaration && !ts.isSourceFile(symbol.valueDeclaration)) { - const links = getSymbolLinks(symbol); - if (links.isDeclarationWithCollidingName === undefined) { - const container = ts.getEnclosingBlockScopeContainer(symbol.valueDeclaration); - if (ts.isStatementWithLocals(container) || isSymbolOfDestructuredElementOfCatchBinding(symbol)) { - const nodeLinks = getNodeLinks(symbol.valueDeclaration); - if (resolveName(container.parent, symbol.escapedName, ts.SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)) { - // redeclaration - always should be renamed - links.isDeclarationWithCollidingName = true; - } - else if (nodeLinks.flags & ts.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 & ts.NodeCheckFlags.BlockScopedBindingInLoop; - const inLoopInitializer = ts.isIterationStatement(container, /*lookInLabeledStatements*/ false); - const inLoopBodyBlock = container.kind === ts.SyntaxKind.Block && ts.isIterationStatement(container.parent, /*lookInLabeledStatements*/ false); - links.isDeclarationWithCollidingName = !ts.isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock)); - } - else { - links.isDeclarationWithCollidingName = false; - } - } - } - return links.isDeclarationWithCollidingName!; + // 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: ts.Declaration): boolean { + const node = ts.getParseTreeNode(nodeIn, ts.isDeclaration); + if (node) { + const symbol = getSymbolOfNode(node); + if (symbol) { + return isSymbolOfDeclarationWithCollidingName(symbol); } - 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: ts.Identifier): ts.Declaration | undefined { - if (!ts.isGeneratedIdentifier(nodeIn)) { - const node = ts.getParseTreeNode(nodeIn, ts.isIdentifier); - if (node) { - const symbol = getReferencedValueSymbol(node); - if (symbol && isSymbolOfDeclarationWithCollidingName(symbol)) { - return symbol.valueDeclaration; - } - } - } - - return undefined; - } + return false; + } - // 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: ts.Declaration): boolean { - const node = ts.getParseTreeNode(nodeIn, ts.isDeclaration); - if (node) { + function isValueAliasDeclaration(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + return isAliasResolvedToValue(getSymbolOfNode(node)); + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: const symbol = getSymbolOfNode(node); - if (symbol) { - return isSymbolOfDeclarationWithCollidingName(symbol); - } - } + return !!symbol && isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol); + case ts.SyntaxKind.ExportDeclaration: + const exportClause = (node as ts.ExportDeclaration).exportClause; + return !!exportClause && (ts.isNamespaceExport(exportClause) || + ts.some(exportClause.elements, isValueAliasDeclaration)); + case ts.SyntaxKind.ExportAssignment: + return (node as ts.ExportAssignment).expression && (node as ts.ExportAssignment).expression.kind === ts.SyntaxKind.Identifier ? + isAliasResolvedToValue(getSymbolOfNode(node)) : + true; + } + return false; + } + function isTopLevelValueImportEqualsWithEntityName(nodeIn: ts.ImportEqualsDeclaration): boolean { + const node = ts.getParseTreeNode(nodeIn, ts.isImportEqualsDeclaration); + if (node === undefined || node.parent.kind !== ts.SyntaxKind.SourceFile || !ts.isInternalModuleImportEqualsDeclaration(node)) { + // parent is not source file or it is not reference to internal module return false; } - function isValueAliasDeclaration(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - return isAliasResolvedToValue(getSymbolOfNode(node)); - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - const symbol = getSymbolOfNode(node); - return !!symbol && isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol); - case ts.SyntaxKind.ExportDeclaration: - const exportClause = (node as ts.ExportDeclaration).exportClause; - return !!exportClause && (ts.isNamespaceExport(exportClause) || - ts.some(exportClause.elements, isValueAliasDeclaration)); - case ts.SyntaxKind.ExportAssignment: - return (node as ts.ExportAssignment).expression && (node as ts.ExportAssignment).expression.kind === ts.SyntaxKind.Identifier ? - isAliasResolvedToValue(getSymbolOfNode(node)) : - true; - } + const isValue = isAliasResolvedToValue(getSymbolOfNode(node)); + return isValue && node.moduleReference && !ts.nodeIsMissing(node.moduleReference); + } + + function isAliasResolvedToValue(symbol: ts.Symbol | undefined): boolean { + if (!symbol) { return false; } - - function isTopLevelValueImportEqualsWithEntityName(nodeIn: ts.ImportEqualsDeclaration): boolean { - const node = ts.getParseTreeNode(nodeIn, ts.isImportEqualsDeclaration); - if (node === undefined || node.parent.kind !== ts.SyntaxKind.SourceFile || !ts.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 && !ts.nodeIsMissing(node.moduleReference); + const target = getExportSymbolOfValueSymbolIfExported(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 & ts.SymbolFlags.Value) && + (ts.shouldPreserveConstEnums(compilerOptions) || !isConstEnumOrConstEnumOnlyModule(target)); + } - function isAliasResolvedToValue(symbol: ts.Symbol | undefined): boolean { - if (!symbol) { - return false; + function isConstEnumOrConstEnumOnlyModule(s: ts.Symbol): boolean { + return isConstEnumSymbol(s) || !!s.constEnumOnlyModule; + } + + function isReferencedAliasDeclaration(node: ts.Node, checkChildren?: boolean): boolean { + if (isAliasSymbolDeclaration(node)) { + const symbol = getSymbolOfNode(node); + const links = symbol && getSymbolLinks(symbol); + if (links?.referenced) { + return true; } - const target = getExportSymbolOfValueSymbolIfExported(resolveAlias(symbol)); - if (target === unknownSymbol) { + const target = getSymbolLinks(symbol!).aliasTarget; // TODO: GH#18217 + if (target && ts.getEffectiveModifierFlags(node) & ts.ModifierFlags.Export && + target.flags & ts.SymbolFlags.Value && + (ts.shouldPreserveConstEnums(compilerOptions) || !isConstEnumOrConstEnumOnlyModule(target))) { + // An `export import ... =` of a value symbol is always considered referenced 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 & ts.SymbolFlags.Value) && - (ts.shouldPreserveConstEnums(compilerOptions) || !isConstEnumOrConstEnumOnlyModule(target)); - } - - function isConstEnumOrConstEnumOnlyModule(s: ts.Symbol): boolean { - return isConstEnumSymbol(s) || !!s.constEnumOnlyModule; } - function isReferencedAliasDeclaration(node: ts.Node, checkChildren?: boolean): boolean { - if (isAliasSymbolDeclaration(node)) { - const symbol = getSymbolOfNode(node); - const links = symbol && getSymbolLinks(symbol); - if (links?.referenced) { - return true; - } - const target = getSymbolLinks(symbol!).aliasTarget; // TODO: GH#18217 - if (target && ts.getEffectiveModifierFlags(node) & ts.ModifierFlags.Export && - target.flags & ts.SymbolFlags.Value && - (ts.shouldPreserveConstEnums(compilerOptions) || !isConstEnumOrConstEnumOnlyModule(target))) { - // An `export import ... =` of a value symbol is always considered referenced - return true; - } - } - - if (checkChildren) { - return !!ts.forEachChild(node, node => isReferencedAliasDeclaration(node, checkChildren)); - } - return false; + if (checkChildren) { + return !!ts.forEachChild(node, node => isReferencedAliasDeclaration(node, checkChildren)); } + return false; + } - function isImplementationOfOverload(node: ts.SignatureDeclaration) { - if (ts.nodeIsPresent((node as ts.FunctionLikeDeclaration).body)) { - if (ts.isGetAccessor(node) || ts.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 + function isImplementationOfOverload(node: ts.SignatureDeclaration) { + if (ts.nodeIsPresent((node as ts.FunctionLikeDeclaration).body)) { + if (ts.isGetAccessor(node) || ts.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: 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; + (signaturesOfSymbol.length === 1 && signaturesOfSymbol[0].declaration !== node); } + return false; + } - function isRequiredInitializedParameter(parameter: ts.ParameterDeclaration | ts.JSDocParameterTag): boolean { - return !!strictNullChecks && - !isOptionalParameter(parameter) && - !ts.isJSDocParameterTag(parameter) && - !!parameter.initializer && - !ts.hasSyntacticModifier(parameter, ts.ModifierFlags.ParameterPropertyModifier); - } + function isRequiredInitializedParameter(parameter: ts.ParameterDeclaration | ts.JSDocParameterTag): boolean { + return !!strictNullChecks && + !isOptionalParameter(parameter) && + !ts.isJSDocParameterTag(parameter) && + !!parameter.initializer && + !ts.hasSyntacticModifier(parameter, ts.ModifierFlags.ParameterPropertyModifier); + } - function isOptionalUninitializedParameterProperty(parameter: ts.ParameterDeclaration) { - return strictNullChecks && - isOptionalParameter(parameter) && - !parameter.initializer && - ts.hasSyntacticModifier(parameter, ts.ModifierFlags.ParameterPropertyModifier); - } + function isOptionalUninitializedParameterProperty(parameter: ts.ParameterDeclaration) { + return strictNullChecks && + isOptionalParameter(parameter) && + !parameter.initializer && + ts.hasSyntacticModifier(parameter, ts.ModifierFlags.ParameterPropertyModifier); + } - function isExpandoFunctionDeclaration(node: ts.Declaration): boolean { - const declaration = ts.getParseTreeNode(node, ts.isFunctionDeclaration); - if (!declaration) { - return false; - } - const symbol = getSymbolOfNode(declaration); - if (!symbol || !(symbol.flags & ts.SymbolFlags.Function)) { - return false; - } - return !!ts.forEachEntry(getExportsOfSymbol(symbol), p => p.flags & ts.SymbolFlags.Value && p.valueDeclaration && ts.isPropertyAccessExpression(p.valueDeclaration)); + function isExpandoFunctionDeclaration(node: ts.Declaration): boolean { + const declaration = ts.getParseTreeNode(node, ts.isFunctionDeclaration); + if (!declaration) { + return false; + } + const symbol = getSymbolOfNode(declaration); + if (!symbol || !(symbol.flags & ts.SymbolFlags.Function)) { + return false; } + return !!ts.forEachEntry(getExportsOfSymbol(symbol), p => p.flags & ts.SymbolFlags.Value && p.valueDeclaration && ts.isPropertyAccessExpression(p.valueDeclaration)); + } - function getPropertiesOfContainerFunction(node: ts.Declaration): ts.Symbol[] { - const declaration = ts.getParseTreeNode(node, ts.isFunctionDeclaration); - if (!declaration) { - return ts.emptyArray; - } - const symbol = getSymbolOfNode(declaration); - return symbol && getPropertiesOfType(getTypeOfSymbol(symbol)) || ts.emptyArray; + function getPropertiesOfContainerFunction(node: ts.Declaration): ts.Symbol[] { + const declaration = ts.getParseTreeNode(node, ts.isFunctionDeclaration); + if (!declaration) { + return ts.emptyArray; } + const symbol = getSymbolOfNode(declaration); + return symbol && getPropertiesOfType(getTypeOfSymbol(symbol)) || ts.emptyArray; + } + + function getNodeCheckFlags(node: ts.Node): ts.NodeCheckFlags { + const nodeId = node.id || 0; + if (nodeId < 0 || nodeId >= nodeLinks.length) + return 0; + return nodeLinks[nodeId]?.flags || 0; + } - function getNodeCheckFlags(node: ts.Node): ts.NodeCheckFlags { - const nodeId = node.id || 0; - if (nodeId < 0 || nodeId >= nodeLinks.length) - return 0; - return nodeLinks[nodeId]?.flags || 0; + function getEnumMemberValue(node: ts.EnumMember): string | number | undefined { + computeEnumMemberValues(node.parent); + return getNodeLinks(node).enumMemberValue; + } + + function canHaveConstantValue(node: ts.Node): node is ts.EnumMember | ts.AccessExpression { + switch (node.kind) { + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return true; } + return false; + } - function getEnumMemberValue(node: ts.EnumMember): string | number | undefined { - computeEnumMemberValues(node.parent); - return getNodeLinks(node).enumMemberValue; + function getConstantValue(node: ts.EnumMember | ts.AccessExpression): string | number | undefined { + if (node.kind === ts.SyntaxKind.EnumMember) { + return getEnumMemberValue(node); } - function canHaveConstantValue(node: ts.Node): node is ts.EnumMember | ts.AccessExpression { - switch (node.kind) { - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return true; + const symbol = getNodeLinks(node).resolvedSymbol; + if (symbol && (symbol.flags & ts.SymbolFlags.EnumMember)) { + // inline property\index accesses only for const enums + const member = symbol.valueDeclaration as ts.EnumMember; + if (ts.isEnumConst(member.parent)) { + return getEnumMemberValue(member); } - return false; } - function getConstantValue(node: ts.EnumMember | ts.AccessExpression): string | number | undefined { - if (node.kind === ts.SyntaxKind.EnumMember) { - return getEnumMemberValue(node); - } + return undefined; + } - const symbol = getNodeLinks(node).resolvedSymbol; - if (symbol && (symbol.flags & ts.SymbolFlags.EnumMember)) { - // inline property\index accesses only for const enums - const member = symbol.valueDeclaration as ts.EnumMember; - if (ts.isEnumConst(member.parent)) { - return getEnumMemberValue(member); - } - } + function isFunctionType(type: ts.Type): boolean { + return !!(type.flags & ts.TypeFlags.Object) && getSignaturesOfType(type, ts.SignatureKind.Call).length > 0; + } - return undefined; - } + function getTypeReferenceSerializationKind(typeNameIn: ts.EntityName, location?: ts.Node): ts.TypeReferenceSerializationKind { + // ensure both `typeName` and `location` are parse tree nodes. + const typeName = ts.getParseTreeNode(typeNameIn, ts.isEntityName); + if (!typeName) + return ts.TypeReferenceSerializationKind.Unknown; - function isFunctionType(type: ts.Type): boolean { - return !!(type.flags & ts.TypeFlags.Object) && getSignaturesOfType(type, ts.SignatureKind.Call).length > 0; + if (location) { + location = ts.getParseTreeNode(location); + if (!location) + return ts.TypeReferenceSerializationKind.Unknown; } - function getTypeReferenceSerializationKind(typeNameIn: ts.EntityName, location?: ts.Node): ts.TypeReferenceSerializationKind { - // ensure both `typeName` and `location` are parse tree nodes. - const typeName = ts.getParseTreeNode(typeNameIn, ts.isEntityName); - if (!typeName) - return ts.TypeReferenceSerializationKind.Unknown; + // Resolve the symbol as a value to ensure the type can be reached at runtime during emit. + let isTypeOnly = false; + if (ts.isQualifiedName(typeName)) { + const rootValueSymbol = resolveEntityName(ts.getFirstIdentifier(typeName), ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, location); + isTypeOnly = !!rootValueSymbol?.declarations?.every(ts.isTypeOnlyImportOrExportDeclaration); + } + const valueSymbol = resolveEntityName(typeName, ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, location); + const resolvedSymbol = valueSymbol && valueSymbol.flags & ts.SymbolFlags.Alias ? resolveAlias(valueSymbol) : valueSymbol; + isTypeOnly ||= !!valueSymbol?.declarations?.every(ts.isTypeOnlyImportOrExportDeclaration); - if (location) { - location = ts.getParseTreeNode(location); - if (!location) - return ts.TypeReferenceSerializationKind.Unknown; + // Resolve the symbol as a type so that we can provide a more useful hint for the type serializer. + const typeSymbol = resolveEntityName(typeName, ts.SymbolFlags.Type, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); + if (resolvedSymbol && resolvedSymbol === typeSymbol) { + const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); + if (globalPromiseSymbol && resolvedSymbol === globalPromiseSymbol) { + return ts.TypeReferenceSerializationKind.Promise; } - // Resolve the symbol as a value to ensure the type can be reached at runtime during emit. - let isTypeOnly = false; - if (ts.isQualifiedName(typeName)) { - const rootValueSymbol = resolveEntityName(ts.getFirstIdentifier(typeName), ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, location); - isTypeOnly = !!rootValueSymbol?.declarations?.every(ts.isTypeOnlyImportOrExportDeclaration); + const constructorType = getTypeOfSymbol(resolvedSymbol); + if (constructorType && isConstructorType(constructorType)) { + return isTypeOnly ? ts.TypeReferenceSerializationKind.TypeWithCallSignature : ts.TypeReferenceSerializationKind.TypeWithConstructSignatureAndValue; } - const valueSymbol = resolveEntityName(typeName, ts.SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, location); - const resolvedSymbol = valueSymbol && valueSymbol.flags & ts.SymbolFlags.Alias ? resolveAlias(valueSymbol) : valueSymbol; - isTypeOnly ||= !!valueSymbol?.declarations?.every(ts.isTypeOnlyImportOrExportDeclaration); + } - // Resolve the symbol as a type so that we can provide a more useful hint for the type serializer. - const typeSymbol = resolveEntityName(typeName, ts.SymbolFlags.Type, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); - if (resolvedSymbol && resolvedSymbol === typeSymbol) { - const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); - if (globalPromiseSymbol && resolvedSymbol === globalPromiseSymbol) { - return ts.TypeReferenceSerializationKind.Promise; - } + // We might not be able to resolve type symbol so use unknown type in that case (eg error case) + if (!typeSymbol) { + return isTypeOnly ? ts.TypeReferenceSerializationKind.ObjectType : ts.TypeReferenceSerializationKind.Unknown; + } + const type = getDeclaredTypeOfSymbol(typeSymbol); + if (isErrorType(type)) { + return isTypeOnly ? ts.TypeReferenceSerializationKind.ObjectType : ts.TypeReferenceSerializationKind.Unknown; + } + else if (type.flags & ts.TypeFlags.AnyOrUnknown) { + return ts.TypeReferenceSerializationKind.ObjectType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.Void | ts.TypeFlags.Nullable | ts.TypeFlags.Never)) { + return ts.TypeReferenceSerializationKind.VoidNullableOrNeverType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.BooleanLike)) { + return ts.TypeReferenceSerializationKind.BooleanType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.NumberLike)) { + return ts.TypeReferenceSerializationKind.NumberLikeType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.BigIntLike)) { + return ts.TypeReferenceSerializationKind.BigIntLikeType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.StringLike)) { + return ts.TypeReferenceSerializationKind.StringLikeType; + } + else if (isTupleType(type)) { + return ts.TypeReferenceSerializationKind.ArrayLikeType; + } + else if (isTypeAssignableToKind(type, ts.TypeFlags.ESSymbolLike)) { + return ts.TypeReferenceSerializationKind.ESSymbolType; + } + else if (isFunctionType(type)) { + return ts.TypeReferenceSerializationKind.TypeWithCallSignature; + } + else if (isArrayType(type)) { + return ts.TypeReferenceSerializationKind.ArrayLikeType; + } + else { + return ts.TypeReferenceSerializationKind.ObjectType; + } + } - const constructorType = getTypeOfSymbol(resolvedSymbol); - if (constructorType && isConstructorType(constructorType)) { - return isTypeOnly ? ts.TypeReferenceSerializationKind.TypeWithCallSignature : ts.TypeReferenceSerializationKind.TypeWithConstructSignatureAndValue; - } - } + function createTypeOfDeclaration(declarationIn: ts.AccessorDeclaration | ts.VariableLikeDeclaration | ts.PropertyAccessExpression, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker, addUndefined?: boolean) { + const declaration = ts.getParseTreeNode(declarationIn, ts.isVariableLikeOrAccessor); + if (!declaration) { + return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.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 & (ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Signature)) + ? getWidenedLiteralType(getTypeOfSymbol(symbol)) + : errorType; + if (type.flags & ts.TypeFlags.UniqueESSymbol && + type.symbol === symbol) { + flags |= ts.NodeBuilderFlags.AllowUniqueESSymbolType; + } + if (addUndefined) { + type = getOptionalType(type); + } + return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); + } - // We might not be able to resolve type symbol so use unknown type in that case (eg error case) - if (!typeSymbol) { - return isTypeOnly ? ts.TypeReferenceSerializationKind.ObjectType : ts.TypeReferenceSerializationKind.Unknown; - } - const type = getDeclaredTypeOfSymbol(typeSymbol); - if (isErrorType(type)) { - return isTypeOnly ? ts.TypeReferenceSerializationKind.ObjectType : ts.TypeReferenceSerializationKind.Unknown; - } - else if (type.flags & ts.TypeFlags.AnyOrUnknown) { - return ts.TypeReferenceSerializationKind.ObjectType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.Void | ts.TypeFlags.Nullable | ts.TypeFlags.Never)) { - return ts.TypeReferenceSerializationKind.VoidNullableOrNeverType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.BooleanLike)) { - return ts.TypeReferenceSerializationKind.BooleanType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.NumberLike)) { - return ts.TypeReferenceSerializationKind.NumberLikeType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.BigIntLike)) { - return ts.TypeReferenceSerializationKind.BigIntLikeType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.StringLike)) { - return ts.TypeReferenceSerializationKind.StringLikeType; - } - else if (isTupleType(type)) { - return ts.TypeReferenceSerializationKind.ArrayLikeType; - } - else if (isTypeAssignableToKind(type, ts.TypeFlags.ESSymbolLike)) { - return ts.TypeReferenceSerializationKind.ESSymbolType; - } - else if (isFunctionType(type)) { - return ts.TypeReferenceSerializationKind.TypeWithCallSignature; - } - else if (isArrayType(type)) { - return ts.TypeReferenceSerializationKind.ArrayLikeType; - } - else { - return ts.TypeReferenceSerializationKind.ObjectType; - } + function createReturnTypeOfSignatureDeclaration(signatureDeclarationIn: ts.SignatureDeclaration, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker) { + const signatureDeclaration = ts.getParseTreeNode(signatureDeclarationIn, ts.isFunctionLike); + if (!signatureDeclaration) { + return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.KeywordTypeNode; } + const signature = getSignatureFromDeclaration(signatureDeclaration); + return nodeBuilder.typeToTypeNode(getReturnTypeOfSignature(signature), enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); + } - function createTypeOfDeclaration(declarationIn: ts.AccessorDeclaration | ts.VariableLikeDeclaration | ts.PropertyAccessExpression, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker, addUndefined?: boolean) { - const declaration = ts.getParseTreeNode(declarationIn, ts.isVariableLikeOrAccessor); - if (!declaration) { - return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.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 & (ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Signature)) - ? getWidenedLiteralType(getTypeOfSymbol(symbol)) - : errorType; - if (type.flags & ts.TypeFlags.UniqueESSymbol && - type.symbol === symbol) { - flags |= ts.NodeBuilderFlags.AllowUniqueESSymbolType; - } - if (addUndefined) { - type = getOptionalType(type); - } - return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); + function createTypeOfExpression(exprIn: ts.Expression, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker) { + const expr = ts.getParseTreeNode(exprIn, ts.isExpression); + if (!expr) { + return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.KeywordTypeNode; + } + const type = getWidenedType(getRegularTypeOfExpression(expr)); + return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); + } + + function hasGlobalName(name: string): boolean { + return globals.has(ts.escapeLeadingUnderscores(name)); + } + + function getReferencedValueSymbol(reference: ts.Identifier, startInDeclarationContainer?: boolean): ts.Symbol | undefined { + const resolvedSymbol = getNodeLinks(reference).resolvedSymbol; + if (resolvedSymbol) { + return resolvedSymbol; } - function createReturnTypeOfSignatureDeclaration(signatureDeclarationIn: ts.SignatureDeclaration, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker) { - const signatureDeclaration = ts.getParseTreeNode(signatureDeclarationIn, ts.isFunctionLike); - if (!signatureDeclaration) { - return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.KeywordTypeNode; + let location: ts.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 (ts.isDeclaration(parent) && reference === parent.name) { + location = getDeclarationContainer(parent); } - const signature = getSignatureFromDeclaration(signatureDeclaration); - return nodeBuilder.typeToTypeNode(getReturnTypeOfSignature(signature), enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); } - function createTypeOfExpression(exprIn: ts.Expression, enclosingDeclaration: ts.Node, flags: ts.NodeBuilderFlags, tracker: ts.SymbolTracker) { - const expr = ts.getParseTreeNode(exprIn, ts.isExpression); - if (!expr) { - return ts.factory.createToken(ts.SyntaxKind.AnyKeyword) as ts.KeywordTypeNode; + return resolveName(location, reference.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + } + + function getReferencedValueDeclaration(referenceIn: ts.Identifier): ts.Declaration | undefined { + if (!ts.isGeneratedIdentifier(referenceIn)) { + const reference = ts.getParseTreeNode(referenceIn, ts.isIdentifier); + if (reference) { + const symbol = getReferencedValueSymbol(reference); + if (symbol) { + return getExportSymbolOfValueSymbolIfExported(symbol).valueDeclaration; + } } - const type = getWidenedType(getRegularTypeOfExpression(expr)); - return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | ts.NodeBuilderFlags.MultilineObjectLiterals, tracker); } - function hasGlobalName(name: string): boolean { - return globals.has(ts.escapeLeadingUnderscores(name)); + return undefined; + } + + function isLiteralConstDeclaration(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration): boolean { + if (ts.isDeclarationReadonly(node) || ts.isVariableDeclaration(node) && ts.isVarConst(node)) { + return isFreshLiteralType(getTypeOfSymbol(getSymbolOfNode(node))); } + return false; + } - function getReferencedValueSymbol(reference: ts.Identifier, startInDeclarationContainer?: boolean): ts.Symbol | undefined { - const resolvedSymbol = getNodeLinks(reference).resolvedSymbol; - if (resolvedSymbol) { - return resolvedSymbol; - } + function literalTypeToNode(type: ts.FreshableType, enclosing: ts.Node, tracker: ts.SymbolTracker): ts.Expression { + const enumResult = type.flags & ts.TypeFlags.EnumLiteral ? nodeBuilder.symbolToExpression(type.symbol, ts.SymbolFlags.Value, enclosing, /*flags*/ undefined, tracker) + : type === trueType ? ts.factory.createTrue() : type === falseType && ts.factory.createFalse(); + if (enumResult) + return enumResult; + const literalValue = (type as ts.LiteralType).value; + return typeof literalValue === "object" ? ts.factory.createBigIntLiteral(literalValue) : + typeof literalValue === "number" ? ts.factory.createNumericLiteral(literalValue) : + ts.factory.createStringLiteral(literalValue); + } + + function createLiteralConstValue(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration, tracker: ts.SymbolTracker) { + const type = getTypeOfSymbol(getSymbolOfNode(node)); + return literalTypeToNode(type as ts.FreshableType, node, tracker); + } + + function getJsxFactoryEntity(location: ts.Node): ts.EntityName | undefined { + return location ? (getJsxNamespace(location), (ts.getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity; + } - let location: ts.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 (ts.isDeclaration(parent) && reference === parent.name) { - location = getDeclarationContainer(parent); + function getJsxFragmentFactoryEntity(location: ts.Node): ts.EntityName | undefined { + if (location) { + const file = ts.getSourceFileOfNode(location); + if (file) { + if (file.localJsxFragmentFactory) { + return file.localJsxFragmentFactory; + } + const jsxFragPragmas = file.pragmas.get("jsxfrag"); + const jsxFragPragma = ts.isArray(jsxFragPragmas) ? jsxFragPragmas[0] : jsxFragPragmas; + if (jsxFragPragma) { + file.localJsxFragmentFactory = ts.parseIsolatedEntityName(jsxFragPragma.arguments.factory, languageVersion); + return file.localJsxFragmentFactory; } } + } - return resolveName(location, reference.escapedText, ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (compilerOptions.jsxFragmentFactory) { + return ts.parseIsolatedEntityName(compilerOptions.jsxFragmentFactory, languageVersion); } + } - function getReferencedValueDeclaration(referenceIn: ts.Identifier): ts.Declaration | undefined { - if (!ts.isGeneratedIdentifier(referenceIn)) { - const reference = ts.getParseTreeNode(referenceIn, ts.isIdentifier); - if (reference) { - const symbol = getReferencedValueSymbol(reference); - if (symbol) { - return getExportSymbolOfValueSymbolIfExported(symbol).valueDeclaration; + function createResolver(): ts.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.ESMap; + if (resolvedTypeReferenceDirectives) { + // populate reverse mapping: file path -> type reference directive that was resolved to this file + fileToDirective = new ts.Map(); + resolvedTypeReferenceDirectives.forEach((resolvedDirective, key, mode) => { + if (!resolvedDirective || !resolvedDirective.resolvedFileName) { + return; + } + const file = host.getSourceFile(resolvedDirective.resolvedFileName); + if (file) { + // 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, mode); + } + }); + } + + return { + getReferencedExportContainer, + getReferencedImportDeclaration, + getReferencedDeclarationWithCollidingName, + isDeclarationWithCollidingName, + isValueAliasDeclaration: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + // Synthesized nodes are always treated like values. + return node ? isValueAliasDeclaration(node) : true; + }, + hasGlobalName, + isReferencedAliasDeclaration: (nodeIn, checkChildren?) => { + const node = ts.getParseTreeNode(nodeIn); + // Synthesized nodes are always treated as referenced. + return node ? isReferencedAliasDeclaration(node, checkChildren) : true; + }, + getNodeCheckFlags: nodeIn => { + const node = ts.getParseTreeNode(nodeIn); + return node ? getNodeCheckFlags(node) : 0; + }, + isTopLevelValueImportEqualsWithEntityName, + isDeclarationVisible, + isImplementationOfOverload, + isRequiredInitializedParameter, + isOptionalUninitializedParameterProperty, + isExpandoFunctionDeclaration, + getPropertiesOfContainerFunction, + createTypeOfDeclaration, + createReturnTypeOfSignatureDeclaration, + createTypeOfExpression, + createLiteralConstValue, + isSymbolAccessible, + isEntityNameVisible, + getConstantValue: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, canHaveConstantValue); + return node ? getConstantValue(node) : undefined; + }, + collectLinkedAliases, + getReferencedValueDeclaration, + getTypeReferenceSerializationKind, + isOptionalParameter, + moduleExportsSomeValue, + isArgumentsLocalBinding, + getExternalModuleFileFromDeclaration: nodeIn => { + const node = ts.getParseTreeNode(nodeIn, ts.hasPossibleExternalModuleReference); + return node && getExternalModuleFileFromDeclaration(node); + }, + getTypeReferenceDirectivesForEntityName, + getTypeReferenceDirectivesForSymbol, + isLiteralConstDeclaration, + isLateBound: (nodeIn: ts.Declaration): nodeIn is ts.LateBoundDeclaration => { + const node = ts.getParseTreeNode(nodeIn, ts.isDeclaration); + const symbol = node && getSymbolOfNode(node); + return !!(symbol && ts.getCheckFlags(symbol) & ts.CheckFlags.Late); + }, + getJsxFactoryEntity, + getJsxFragmentFactoryEntity, + getAllAccessorDeclarations(accessor: ts.AccessorDeclaration): ts.AllAccessorDeclarations { + accessor = ts.getParseTreeNode(accessor, ts.isGetOrSetAccessorDeclaration)!; // TODO: GH#18217 + const otherKind = accessor.kind === ts.SyntaxKind.SetAccessor ? ts.SyntaxKind.GetAccessor : ts.SyntaxKind.SetAccessor; + const otherAccessor = ts.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 === ts.SyntaxKind.SetAccessor ? accessor : otherAccessor as ts.SetAccessorDeclaration; + const getAccessor = accessor.kind === ts.SyntaxKind.GetAccessor ? accessor : otherAccessor as ts.GetAccessorDeclaration; + return { + firstAccessor, + secondAccessor, + setAccessor, + getAccessor + }; + }, + getSymbolOfExternalModuleSpecifier: moduleName => resolveExternalModuleNameWorker(moduleName, moduleName, /*moduleNotFoundError*/ undefined), + isBindingCapturedByNode: (node, decl) => { + const parseNode = ts.getParseTreeNode(node); + const parseDecl = ts.getParseTreeNode(decl); + return !!parseNode && !!parseDecl && (ts.isVariableDeclaration(parseDecl) || ts.isBindingElement(parseDecl)) && isBindingCapturedByNode(parseNode, parseDecl); + }, + getDeclarationStatementsForSourceFile: (node, flags, tracker, bundled) => { + const n = ts.getParseTreeNode(node) as ts.SourceFile; + ts.Debug.assert(n && n.kind === ts.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); + }, + isImportRequiredByAugmentation, + }; + + function isImportRequiredByAugmentation(node: ts.ImportDeclaration) { + const file = ts.getSourceFileOfNode(node); + if (!file.symbol) + return false; + const importTarget = getExternalModuleFileFromDeclaration(node); + if (!importTarget) + return false; + if (importTarget === file) + return false; + const exports = getExportsOfModule(file.symbol); + for (const s of ts.arrayFrom(exports.values())) { + if (s.mergeId) { + const merged = getMergedSymbol(s); + if (merged.declarations) { + for (const d of merged.declarations) { + const declFile = ts.getSourceFileOfNode(d); + if (declFile === importTarget) { + return true; + } + } } } } - - return undefined; - } - - function isLiteralConstDeclaration(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration): boolean { - if (ts.isDeclarationReadonly(node) || ts.isVariableDeclaration(node) && ts.isVarConst(node)) { - return isFreshLiteralType(getTypeOfSymbol(getSymbolOfNode(node))); - } return false; } - function literalTypeToNode(type: ts.FreshableType, enclosing: ts.Node, tracker: ts.SymbolTracker): ts.Expression { - const enumResult = type.flags & ts.TypeFlags.EnumLiteral ? nodeBuilder.symbolToExpression(type.symbol, ts.SymbolFlags.Value, enclosing, /*flags*/ undefined, tracker) - : type === trueType ? ts.factory.createTrue() : type === falseType && ts.factory.createFalse(); - if (enumResult) - return enumResult; - const literalValue = (type as ts.LiteralType).value; - return typeof literalValue === "object" ? ts.factory.createBigIntLiteral(literalValue) : - typeof literalValue === "number" ? ts.factory.createNumericLiteral(literalValue) : - ts.factory.createStringLiteral(literalValue); - } - - function createLiteralConstValue(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration, tracker: ts.SymbolTracker) { - const type = getTypeOfSymbol(getSymbolOfNode(node)); - return literalTypeToNode(type as ts.FreshableType, node, tracker); - } - - function getJsxFactoryEntity(location: ts.Node): ts.EntityName | undefined { - return location ? (getJsxNamespace(location), (ts.getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity; + function isInHeritageClause(node: ts.PropertyAccessEntityNameExpression) { + return node.parent && node.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && node.parent.parent && node.parent.parent.kind === ts.SyntaxKind.HeritageClause; } - function getJsxFragmentFactoryEntity(location: ts.Node): ts.EntityName | undefined { - if (location) { - const file = ts.getSourceFileOfNode(location); - if (file) { - if (file.localJsxFragmentFactory) { - return file.localJsxFragmentFactory; - } - const jsxFragPragmas = file.pragmas.get("jsxfrag"); - const jsxFragPragma = ts.isArray(jsxFragPragmas) ? jsxFragPragmas[0] : jsxFragPragmas; - if (jsxFragPragma) { - file.localJsxFragmentFactory = ts.parseIsolatedEntityName(jsxFragPragma.arguments.factory, languageVersion); - return file.localJsxFragmentFactory; - } - } + // defined here to avoid outer scope pollution + function getTypeReferenceDirectivesForEntityName(node: ts.EntityNameOrEntityNameExpression): [ + specifier: string, + mode: ts.SourceFile["impliedNodeFormat"] | undefined + ][] | undefined { + // program does not have any files with type reference directives - bail out + if (!fileToDirective) { + return undefined; } - - if (compilerOptions.jsxFragmentFactory) { - return ts.parseIsolatedEntityName(compilerOptions.jsxFragmentFactory, languageVersion); + // 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 = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace; + if ((node.kind === ts.SyntaxKind.Identifier && isInTypeQuery(node)) || (node.kind === ts.SyntaxKind.PropertyAccessExpression && !isInHeritageClause(node))) { + meaning = ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue; } + + const symbol = resolveEntityName(node, meaning, /*ignoreErrors*/ true); + return symbol && symbol !== unknownSymbol ? getTypeReferenceDirectivesForSymbol(symbol, meaning) : undefined; } - function createResolver(): ts.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.ESMap; - if (resolvedTypeReferenceDirectives) { - // populate reverse mapping: file path -> type reference directive that was resolved to this file - fileToDirective = new ts.Map(); - resolvedTypeReferenceDirectives.forEach((resolvedDirective, key, mode) => { - if (!resolvedDirective || !resolvedDirective.resolvedFileName) { - return; - } - const file = host.getSourceFile(resolvedDirective.resolvedFileName); - if (file) { - // 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, mode); + ][] | 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 = ts.getSourceFileOfNode(decl); + const typeReferenceDirective = fileToDirective.get(file.path); + if (typeReferenceDirective) { + (typeReferenceDirectives || (typeReferenceDirectives = [])).push(typeReferenceDirective); } - }); - } - - return { - getReferencedExportContainer, - getReferencedImportDeclaration, - getReferencedDeclarationWithCollidingName, - isDeclarationWithCollidingName, - isValueAliasDeclaration: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - // Synthesized nodes are always treated like values. - return node ? isValueAliasDeclaration(node) : true; - }, - hasGlobalName, - isReferencedAliasDeclaration: (nodeIn, checkChildren?) => { - const node = ts.getParseTreeNode(nodeIn); - // Synthesized nodes are always treated as referenced. - return node ? isReferencedAliasDeclaration(node, checkChildren) : true; - }, - getNodeCheckFlags: nodeIn => { - const node = ts.getParseTreeNode(nodeIn); - return node ? getNodeCheckFlags(node) : 0; - }, - isTopLevelValueImportEqualsWithEntityName, - isDeclarationVisible, - isImplementationOfOverload, - isRequiredInitializedParameter, - isOptionalUninitializedParameterProperty, - isExpandoFunctionDeclaration, - getPropertiesOfContainerFunction, - createTypeOfDeclaration, - createReturnTypeOfSignatureDeclaration, - createTypeOfExpression, - createLiteralConstValue, - isSymbolAccessible, - isEntityNameVisible, - getConstantValue: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, canHaveConstantValue); - return node ? getConstantValue(node) : undefined; - }, - collectLinkedAliases, - getReferencedValueDeclaration, - getTypeReferenceSerializationKind, - isOptionalParameter, - moduleExportsSomeValue, - isArgumentsLocalBinding, - getExternalModuleFileFromDeclaration: nodeIn => { - const node = ts.getParseTreeNode(nodeIn, ts.hasPossibleExternalModuleReference); - return node && getExternalModuleFileFromDeclaration(node); - }, - getTypeReferenceDirectivesForEntityName, - getTypeReferenceDirectivesForSymbol, - isLiteralConstDeclaration, - isLateBound: (nodeIn: ts.Declaration): nodeIn is ts.LateBoundDeclaration => { - const node = ts.getParseTreeNode(nodeIn, ts.isDeclaration); - const symbol = node && getSymbolOfNode(node); - return !!(symbol && ts.getCheckFlags(symbol) & ts.CheckFlags.Late); - }, - getJsxFactoryEntity, - getJsxFragmentFactoryEntity, - getAllAccessorDeclarations(accessor: ts.AccessorDeclaration): ts.AllAccessorDeclarations { - accessor = ts.getParseTreeNode(accessor, ts.isGetOrSetAccessorDeclaration)!; // TODO: GH#18217 - const otherKind = accessor.kind === ts.SyntaxKind.SetAccessor ? ts.SyntaxKind.GetAccessor : ts.SyntaxKind.SetAccessor; - const otherAccessor = ts.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 === ts.SyntaxKind.SetAccessor ? accessor : otherAccessor as ts.SetAccessorDeclaration; - const getAccessor = accessor.kind === ts.SyntaxKind.GetAccessor ? accessor : otherAccessor as ts.GetAccessorDeclaration; - return { - firstAccessor, - secondAccessor, - setAccessor, - getAccessor - }; - }, - getSymbolOfExternalModuleSpecifier: moduleName => resolveExternalModuleNameWorker(moduleName, moduleName, /*moduleNotFoundError*/ undefined), - isBindingCapturedByNode: (node, decl) => { - const parseNode = ts.getParseTreeNode(node); - const parseDecl = ts.getParseTreeNode(decl); - return !!parseNode && !!parseDecl && (ts.isVariableDeclaration(parseDecl) || ts.isBindingElement(parseDecl)) && isBindingCapturedByNode(parseNode, parseDecl); - }, - getDeclarationStatementsForSourceFile: (node, flags, tracker, bundled) => { - const n = ts.getParseTreeNode(node) as ts.SourceFile; - ts.Debug.assert(n && n.kind === ts.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); - }, - isImportRequiredByAugmentation, - }; - - function isImportRequiredByAugmentation(node: ts.ImportDeclaration) { - const file = ts.getSourceFileOfNode(node); - if (!file.symbol) - return false; - const importTarget = getExternalModuleFileFromDeclaration(node); - if (!importTarget) - return false; - if (importTarget === file) - return false; - const exports = getExportsOfModule(file.symbol); - for (const s of ts.arrayFrom(exports.values())) { - if (s.mergeId) { - const merged = getMergedSymbol(s); - if (merged.declarations) { - for (const d of merged.declarations) { - const declFile = ts.getSourceFileOfNode(d); - if (declFile === importTarget) { - return true; - } - } - } + else { + // found at least one entry that does not originate from type reference directive + return undefined; } } - return false; } + return typeReferenceDirectives; + } - function isInHeritageClause(node: ts.PropertyAccessEntityNameExpression) { - return node.parent && node.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && node.parent.parent && node.parent.parent.kind === ts.SyntaxKind.HeritageClause; + 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; } - // defined here to avoid outer scope pollution - function getTypeReferenceDirectivesForEntityName(node: ts.EntityNameOrEntityNameExpression): [ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ][] | undefined { - // program does not have any files with type reference directives - bail out - if (!fileToDirective) { - return undefined; + // 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; } - // 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 = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace; - if ((node.kind === ts.SyntaxKind.Identifier && isInTypeQuery(node)) || (node.kind === ts.SyntaxKind.PropertyAccessExpression && !isInHeritageClause(node))) { - meaning = ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue; + else { + break; } - - 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?: ts.SymbolFlags): [ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ][] | undefined { - // program does not have any files with type reference directives - bail out - if (!fileToDirective || !isSymbolFromTypeDeclarationFile(symbol)) { - return undefined; - } - // check what declarations in the symbol can contribute to the target meaning - let typeReferenceDirectives: [ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ][] | 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 = ts.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 typeReferenceDirectives; + if (current.valueDeclaration && current.valueDeclaration.kind === ts.SyntaxKind.SourceFile && current.flags & ts.SymbolFlags.ValueModule) { + return false; } - 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; - } - - // 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 === ts.SyntaxKind.SourceFile && current.flags & ts.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 = ts.getSourceFileOfNode(decl); - if (fileToDirective.has(file.path)) { - return true; - } + // check that at least one declaration of top level symbol originates from type declaration file + for (const decl of symbol.declarations) { + const file = ts.getSourceFileOfNode(decl); + if (fileToDirective.has(file.path)) { + return true; } - return false; } + return false; + } - function addReferencedFilesToTypeDirective(file: ts.SourceFile, key: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined) { - if (fileToDirective.has(file.path)) - return; - fileToDirective.set(file.path, [key, mode]); - for (const { fileName, resolutionMode } of file.referencedFiles) { - const resolvedFile = ts.resolveTripleslashReference(fileName, file.fileName); - const referencedFile = host.getSourceFile(resolvedFile); - if (referencedFile) { - addReferencedFilesToTypeDirective(referencedFile, key, resolutionMode || file.impliedNodeFormat); - } + function addReferencedFilesToTypeDirective(file: ts.SourceFile, key: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined) { + if (fileToDirective.has(file.path)) + return; + fileToDirective.set(file.path, [key, mode]); + for (const { fileName, resolutionMode } of file.referencedFiles) { + const resolvedFile = ts.resolveTripleslashReference(fileName, file.fileName); + const referencedFile = host.getSourceFile(resolvedFile); + if (referencedFile) { + addReferencedFilesToTypeDirective(referencedFile, key, resolutionMode || file.impliedNodeFormat); } } } + } - function getExternalModuleFileFromDeclaration(declaration: ts.AnyImportOrReExport | ts.ModuleDeclaration | ts.ImportTypeNode | ts.ImportCall): ts.SourceFile | undefined { - const specifier = declaration.kind === ts.SyntaxKind.ModuleDeclaration ? ts.tryCast(declaration.name, ts.isStringLiteral) : ts.getExternalModuleName(declaration); - const moduleSymbol = resolveExternalModuleNameWorker(specifier!, specifier!, /*moduleNotFoundError*/ undefined); // TODO: GH#18217 - if (!moduleSymbol) { - return undefined; - } - return ts.getDeclarationOfKind(moduleSymbol, ts.SyntaxKind.SourceFile); + function getExternalModuleFileFromDeclaration(declaration: ts.AnyImportOrReExport | ts.ModuleDeclaration | ts.ImportTypeNode | ts.ImportCall): ts.SourceFile | undefined { + const specifier = declaration.kind === ts.SyntaxKind.ModuleDeclaration ? ts.tryCast(declaration.name, ts.isStringLiteral) : ts.getExternalModuleName(declaration); + const moduleSymbol = resolveExternalModuleNameWorker(specifier!, specifier!, /*moduleNotFoundError*/ undefined); // TODO: GH#18217 + if (!moduleSymbol) { + return undefined; } + return ts.getDeclarationOfKind(moduleSymbol, ts.SyntaxKind.SourceFile); + } - function initializeTypeChecker() { - // Bind all source files and propagate errors - for (const file of host.getSourceFiles()) { - ts.bindSourceFile(file, compilerOptions); - } + function initializeTypeChecker() { + // Bind all source files and propagate errors + for (const file of host.getSourceFiles()) { + ts.bindSourceFile(file, compilerOptions); + } - amalgamatedDuplicates = new ts.Map(); + amalgamatedDuplicates = new ts.Map(); - // Initialize global symbol table - let augmentations: (readonly (ts.StringLiteral | ts.Identifier)[])[] | undefined; - for (const file of host.getSourceFiles()) { - if (file.redirectInfo) { - continue; - } - if (!ts.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 ts.__String); - if (fileGlobalThisSymbol?.declarations) { - for (const declaration of fileGlobalThisSymbol.declarations) { - diagnostics.add(ts.createDiagnosticForNode(declaration, ts.Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0, "globalThis")); - } + // Initialize global symbol table + let augmentations: (readonly (ts.StringLiteral | ts.Identifier)[])[] | undefined; + for (const file of host.getSourceFiles()) { + if (file.redirectInfo) { + continue; + } + if (!ts.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 ts.__String); + if (fileGlobalThisSymbol?.declarations) { + for (const declaration of fileGlobalThisSymbol.declarations) { + diagnostics.add(ts.createDiagnosticForNode(declaration, ts.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 = ts.concatenate(patternAmbientModules, file.patternAmbientModules); - } - 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); - } - }); } + mergeSymbolTable(globals, file.locals!); } - - // 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 (!ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)) - continue; - mergeModuleAugmentation(augmentation); + if (file.jsGlobalAugmentations) { + mergeSymbolTable(globals, file.jsGlobalAugmentations); + } + if (file.patternAmbientModules && file.patternAmbientModules.length) { + patternAmbientModules = ts.concatenate(patternAmbientModules, file.patternAmbientModules); + } + 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); } + }); + } + } + + // 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 (!ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)) + continue; + mergeModuleAugmentation(augmentation); } } + } - // Setup global builtins - addToSymbolTable(globals, builtinGlobals, ts.Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0); + // Setup global builtins + addToSymbolTable(globals, builtinGlobals, ts.Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0); - getSymbolLinks(undefinedSymbol).type = undefinedWideningType; - getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - getSymbolLinks(unknownSymbol).type = errorType; - getSymbolLinks(globalThisSymbol).type = createObjectType(ts.ObjectFlags.Anonymous, globalThisSymbol); + getSymbolLinks(undefinedSymbol).type = undefinedWideningType; + getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + getSymbolLinks(unknownSymbol).type = errorType; + getSymbolLinks(globalThisSymbol).type = createObjectType(ts.ObjectFlags.Anonymous, globalThisSymbol); - // Initialize special types - globalArrayType = getGlobalType("Array" as ts.__String, /*arity*/ 1, /*reportErrors*/ true); - globalObjectType = getGlobalType("Object" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - globalFunctionType = getGlobalType("Function" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - globalCallableFunctionType = strictBindCallApply && getGlobalType("CallableFunction" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; - globalNewableFunctionType = strictBindCallApply && getGlobalType("NewableFunction" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; - globalStringType = getGlobalType("String" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - globalNumberType = getGlobalType("Number" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - globalBooleanType = getGlobalType("Boolean" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - globalRegExpType = getGlobalType("RegExp" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); - anyArrayType = createArrayType(anyType); + // Initialize special types + globalArrayType = getGlobalType("Array" as ts.__String, /*arity*/ 1, /*reportErrors*/ true); + globalObjectType = getGlobalType("Object" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + globalFunctionType = getGlobalType("Function" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + globalCallableFunctionType = strictBindCallApply && getGlobalType("CallableFunction" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; + globalNewableFunctionType = strictBindCallApply && getGlobalType("NewableFunction" as ts.__String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; + globalStringType = getGlobalType("String" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + globalNumberType = getGlobalType("Number" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + globalBooleanType = getGlobalType("Boolean" as ts.__String, /*arity*/ 0, /*reportErrors*/ true); + globalRegExpType = getGlobalType("RegExp" as ts.__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, ts.emptyArray, ts.emptyArray, ts.emptyArray); - } + 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, ts.emptyArray, ts.emptyArray, ts.emptyArray); + } - globalReadonlyArrayType = getGlobalTypeOrUndefined("ReadonlyArray" as ts.__String, /*arity*/ 1) as ts.GenericType || globalArrayType; - anyReadonlyArrayType = globalReadonlyArrayType ? createTypeFromGenericGlobalType(globalReadonlyArrayType, [anyType]) : anyArrayType; - globalThisType = getGlobalTypeOrUndefined("ThisType" as ts.__String, /*arity*/ 1) as ts.GenericType; + globalReadonlyArrayType = getGlobalTypeOrUndefined("ReadonlyArray" as ts.__String, /*arity*/ 1) as ts.GenericType || globalArrayType; + anyReadonlyArrayType = globalReadonlyArrayType ? createTypeFromGenericGlobalType(globalReadonlyArrayType, [anyType]) : anyArrayType; + globalThisType = getGlobalTypeOrUndefined("ThisType" as ts.__String, /*arity*/ 1) as ts.GenericType; - 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 (ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)) - continue; - mergeModuleAugmentation(augmentation); - } + 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 (ts.isGlobalScopeAugmentation(augmentation.parent as ts.ModuleDeclaration)) + continue; + mergeModuleAugmentation(augmentation); } } + } - 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 ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 : ts.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 = ts.arrayFrom(conflictingSymbols.keys()).join(", "); - diagnostics.add(ts.addRelatedInfo(ts.createDiagnosticForNode(firstFile, ts.Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), ts.createDiagnosticForNode(secondFile, ts.Diagnostics.Conflicts_are_in_this_file))); - diagnostics.add(ts.addRelatedInfo(ts.createDiagnosticForNode(secondFile, ts.Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), ts.createDiagnosticForNode(firstFile, ts.Diagnostics.Conflicts_are_in_this_file))); - } - }); - amalgamatedDuplicates = undefined; - } - - function checkExternalEmitHelpers(location: ts.Node, helpers: ts.ExternalEmitHelpers) { - if ((requestedExternalEmitHelpers & helpers) !== helpers && compilerOptions.importHelpers) { - const sourceFile = ts.getSourceFileOfNode(location); - if (ts.isEffectiveExternalModule(sourceFile, compilerOptions) && !(location.flags & ts.NodeFlags.Ambient)) { - const helpersModule = resolveHelpersModule(sourceFile, location); - if (helpersModule !== unknownSymbol) { - const uncheckedHelpers = helpers & ~requestedExternalEmitHelpers; - for (let helper = ts.ExternalEmitHelpers.FirstEmitHelper; helper <= ts.ExternalEmitHelpers.LastEmitHelper; helper <<= 1) { - if (uncheckedHelpers & helper) { - const name = getHelperName(helper); - const symbol = getSymbol(helpersModule.exports!, ts.escapeLeadingUnderscores(name), ts.SymbolFlags.Value); - if (!symbol) { - error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name); - } - else if (helper & ts.ExternalEmitHelpers.ClassPrivateFieldGet) { - if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 3)) { - error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 4); - } + 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 ? ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0 : ts.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 = ts.arrayFrom(conflictingSymbols.keys()).join(", "); + diagnostics.add(ts.addRelatedInfo(ts.createDiagnosticForNode(firstFile, ts.Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), ts.createDiagnosticForNode(secondFile, ts.Diagnostics.Conflicts_are_in_this_file))); + diagnostics.add(ts.addRelatedInfo(ts.createDiagnosticForNode(secondFile, ts.Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), ts.createDiagnosticForNode(firstFile, ts.Diagnostics.Conflicts_are_in_this_file))); + } + }); + amalgamatedDuplicates = undefined; + } + + function checkExternalEmitHelpers(location: ts.Node, helpers: ts.ExternalEmitHelpers) { + if ((requestedExternalEmitHelpers & helpers) !== helpers && compilerOptions.importHelpers) { + const sourceFile = ts.getSourceFileOfNode(location); + if (ts.isEffectiveExternalModule(sourceFile, compilerOptions) && !(location.flags & ts.NodeFlags.Ambient)) { + const helpersModule = resolveHelpersModule(sourceFile, location); + if (helpersModule !== unknownSymbol) { + const uncheckedHelpers = helpers & ~requestedExternalEmitHelpers; + for (let helper = ts.ExternalEmitHelpers.FirstEmitHelper; helper <= ts.ExternalEmitHelpers.LastEmitHelper; helper <<= 1) { + if (uncheckedHelpers & helper) { + const name = getHelperName(helper); + const symbol = getSymbol(helpersModule.exports!, ts.escapeLeadingUnderscores(name), ts.SymbolFlags.Value); + if (!symbol) { + error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name); + } + else if (helper & ts.ExternalEmitHelpers.ClassPrivateFieldGet) { + if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 3)) { + error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 4); } - else if (helper & ts.ExternalEmitHelpers.ClassPrivateFieldSet) { - if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 4)) { - error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 5); - } + } + else if (helper & ts.ExternalEmitHelpers.ClassPrivateFieldSet) { + if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 4)) { + error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 5); } - else if (helper & ts.ExternalEmitHelpers.SpreadArray) { - if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 2)) { - error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 3); - } + } + else if (helper & ts.ExternalEmitHelpers.SpreadArray) { + if (!ts.some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 2)) { + error(location, ts.Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, ts.externalHelpersModuleNameText, name, 3); } } } } - requestedExternalEmitHelpers |= helpers; } + requestedExternalEmitHelpers |= helpers; } } + } + + function getHelperName(helper: ts.ExternalEmitHelpers) { + switch (helper) { + case ts.ExternalEmitHelpers.Extends: return "__extends"; + case ts.ExternalEmitHelpers.Assign: return "__assign"; + case ts.ExternalEmitHelpers.Rest: return "__rest"; + case ts.ExternalEmitHelpers.Decorate: return "__decorate"; + case ts.ExternalEmitHelpers.Metadata: return "__metadata"; + case ts.ExternalEmitHelpers.Param: return "__param"; + case ts.ExternalEmitHelpers.Awaiter: return "__awaiter"; + case ts.ExternalEmitHelpers.Generator: return "__generator"; + case ts.ExternalEmitHelpers.Values: return "__values"; + case ts.ExternalEmitHelpers.Read: return "__read"; + case ts.ExternalEmitHelpers.SpreadArray: return "__spreadArray"; + case ts.ExternalEmitHelpers.Await: return "__await"; + case ts.ExternalEmitHelpers.AsyncGenerator: return "__asyncGenerator"; + case ts.ExternalEmitHelpers.AsyncDelegator: return "__asyncDelegator"; + case ts.ExternalEmitHelpers.AsyncValues: return "__asyncValues"; + case ts.ExternalEmitHelpers.ExportStar: return "__exportStar"; + case ts.ExternalEmitHelpers.ImportStar: return "__importStar"; + case ts.ExternalEmitHelpers.ImportDefault: return "__importDefault"; + case ts.ExternalEmitHelpers.MakeTemplateObject: return "__makeTemplateObject"; + case ts.ExternalEmitHelpers.ClassPrivateFieldGet: return "__classPrivateFieldGet"; + case ts.ExternalEmitHelpers.ClassPrivateFieldSet: return "__classPrivateFieldSet"; + case ts.ExternalEmitHelpers.ClassPrivateFieldIn: return "__classPrivateFieldIn"; + case ts.ExternalEmitHelpers.CreateBinding: return "__createBinding"; + default: return ts.Debug.fail("Unrecognized helper"); + } + } + function resolveHelpersModule(node: ts.SourceFile, errorNode: ts.Node) { + if (!externalHelpersModule) { + externalHelpersModule = resolveExternalModule(node, ts.externalHelpersModuleNameText, ts.Diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found, errorNode) || unknownSymbol; + } + return externalHelpersModule; + } + + // GRAMMAR CHECKING + function checkGrammarDecoratorsAndModifiers(node: ts.Node): boolean { + return checkGrammarDecorators(node) || checkGrammarModifiers(node); + } - function getHelperName(helper: ts.ExternalEmitHelpers) { - switch (helper) { - case ts.ExternalEmitHelpers.Extends: return "__extends"; - case ts.ExternalEmitHelpers.Assign: return "__assign"; - case ts.ExternalEmitHelpers.Rest: return "__rest"; - case ts.ExternalEmitHelpers.Decorate: return "__decorate"; - case ts.ExternalEmitHelpers.Metadata: return "__metadata"; - case ts.ExternalEmitHelpers.Param: return "__param"; - case ts.ExternalEmitHelpers.Awaiter: return "__awaiter"; - case ts.ExternalEmitHelpers.Generator: return "__generator"; - case ts.ExternalEmitHelpers.Values: return "__values"; - case ts.ExternalEmitHelpers.Read: return "__read"; - case ts.ExternalEmitHelpers.SpreadArray: return "__spreadArray"; - case ts.ExternalEmitHelpers.Await: return "__await"; - case ts.ExternalEmitHelpers.AsyncGenerator: return "__asyncGenerator"; - case ts.ExternalEmitHelpers.AsyncDelegator: return "__asyncDelegator"; - case ts.ExternalEmitHelpers.AsyncValues: return "__asyncValues"; - case ts.ExternalEmitHelpers.ExportStar: return "__exportStar"; - case ts.ExternalEmitHelpers.ImportStar: return "__importStar"; - case ts.ExternalEmitHelpers.ImportDefault: return "__importDefault"; - case ts.ExternalEmitHelpers.MakeTemplateObject: return "__makeTemplateObject"; - case ts.ExternalEmitHelpers.ClassPrivateFieldGet: return "__classPrivateFieldGet"; - case ts.ExternalEmitHelpers.ClassPrivateFieldSet: return "__classPrivateFieldSet"; - case ts.ExternalEmitHelpers.ClassPrivateFieldIn: return "__classPrivateFieldIn"; - case ts.ExternalEmitHelpers.CreateBinding: return "__createBinding"; - default: return ts.Debug.fail("Unrecognized helper"); + function checkGrammarDecorators(node: ts.Node): boolean { + if (!node.decorators) { + return false; + } + if (!ts.nodeCanBeDecorated(node, node.parent, node.parent.parent)) { + if (node.kind === ts.SyntaxKind.MethodDeclaration && !ts.nodeIsPresent((node as ts.MethodDeclaration).body)) { + return grammarErrorOnFirstToken(node, ts.Diagnostics.A_decorator_can_only_decorate_a_method_implementation_not_an_overload); + } + else { + return grammarErrorOnFirstToken(node, ts.Diagnostics.Decorators_are_not_valid_here); } } - function resolveHelpersModule(node: ts.SourceFile, errorNode: ts.Node) { - if (!externalHelpersModule) { - externalHelpersModule = resolveExternalModule(node, ts.externalHelpersModuleNameText, ts.Diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found, errorNode) || unknownSymbol; + else if (node.kind === ts.SyntaxKind.GetAccessor || node.kind === ts.SyntaxKind.SetAccessor) { + const accessors = ts.getAllAccessorDeclarations((node.parent as ts.ClassDeclaration).members, node as ts.AccessorDeclaration); + if (accessors.firstAccessor.decorators && node === accessors.secondAccessor) { + return grammarErrorOnFirstToken(node, ts.Diagnostics.Decorators_cannot_be_applied_to_multiple_get_Slashset_accessors_of_the_same_name); } - return externalHelpersModule; } + return false; + } - // GRAMMAR CHECKING - function checkGrammarDecoratorsAndModifiers(node: ts.Node): boolean { - return checkGrammarDecorators(node) || checkGrammarModifiers(node); + function checkGrammarModifiers(node: ts.Node): boolean { + const quickResult = reportObviousModifierErrors(node); + if (quickResult !== undefined) { + return quickResult; } - function checkGrammarDecorators(node: ts.Node): boolean { - if (!node.decorators) { - return false; - } - if (!ts.nodeCanBeDecorated(node, node.parent, node.parent.parent)) { - if (node.kind === ts.SyntaxKind.MethodDeclaration && !ts.nodeIsPresent((node as ts.MethodDeclaration).body)) { - return grammarErrorOnFirstToken(node, ts.Diagnostics.A_decorator_can_only_decorate_a_method_implementation_not_an_overload); + let lastStatic: ts.Node | undefined, lastDeclare: ts.Node | undefined, lastAsync: ts.Node | undefined, lastOverride: ts.Node | undefined; + let flags = ts.ModifierFlags.None; + for (const modifier of node.modifiers!) { + if (modifier.kind !== ts.SyntaxKind.ReadonlyKeyword) { + if (node.kind === ts.SyntaxKind.PropertySignature || node.kind === ts.SyntaxKind.MethodSignature) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_type_member, ts.tokenToString(modifier.kind)); } - else { - return grammarErrorOnFirstToken(node, ts.Diagnostics.Decorators_are_not_valid_here); + if (node.kind === ts.SyntaxKind.IndexSignature && (modifier.kind !== ts.SyntaxKind.StaticKeyword || !ts.isClassLike(node.parent))) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_an_index_signature, ts.tokenToString(modifier.kind)); } } - else if (node.kind === ts.SyntaxKind.GetAccessor || node.kind === ts.SyntaxKind.SetAccessor) { - const accessors = ts.getAllAccessorDeclarations((node.parent as ts.ClassDeclaration).members, node as ts.AccessorDeclaration); - if (accessors.firstAccessor.decorators && node === accessors.secondAccessor) { - return grammarErrorOnFirstToken(node, ts.Diagnostics.Decorators_cannot_be_applied_to_multiple_get_Slashset_accessors_of_the_same_name); + if (modifier.kind !== ts.SyntaxKind.InKeyword && modifier.kind !== ts.SyntaxKind.OutKeyword) { + if (node.kind === ts.SyntaxKind.TypeParameter) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_type_parameter, ts.tokenToString(modifier.kind)); } } - return false; - } - - function checkGrammarModifiers(node: ts.Node): boolean { - const quickResult = reportObviousModifierErrors(node); - if (quickResult !== undefined) { - return quickResult; - } - - let lastStatic: ts.Node | undefined, lastDeclare: ts.Node | undefined, lastAsync: ts.Node | undefined, lastOverride: ts.Node | undefined; - let flags = ts.ModifierFlags.None; - for (const modifier of node.modifiers!) { - if (modifier.kind !== ts.SyntaxKind.ReadonlyKeyword) { - if (node.kind === ts.SyntaxKind.PropertySignature || node.kind === ts.SyntaxKind.MethodSignature) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_type_member, ts.tokenToString(modifier.kind)); + switch (modifier.kind) { + case ts.SyntaxKind.ConstKeyword: + if (node.kind !== ts.SyntaxKind.EnumDeclaration) { + return grammarErrorOnNode(node, ts.Diagnostics.A_class_member_cannot_have_the_0_keyword, ts.tokenToString(ts.SyntaxKind.ConstKeyword)); } - if (node.kind === ts.SyntaxKind.IndexSignature && (modifier.kind !== ts.SyntaxKind.StaticKeyword || !ts.isClassLike(node.parent))) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_an_index_signature, ts.tokenToString(modifier.kind)); + break; + case ts.SyntaxKind.OverrideKeyword: + // If node.kind === SyntaxKind.Parameter, checkParameter reports an error if it's not a parameter property. + if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "override"); } - } - if (modifier.kind !== ts.SyntaxKind.InKeyword && modifier.kind !== ts.SyntaxKind.OutKeyword) { - if (node.kind === ts.SyntaxKind.TypeParameter) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_type_parameter, ts.tokenToString(modifier.kind)); + else if (flags & ts.ModifierFlags.Ambient) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "override", "declare"); } - } - switch (modifier.kind) { - case ts.SyntaxKind.ConstKeyword: - if (node.kind !== ts.SyntaxKind.EnumDeclaration) { - return grammarErrorOnNode(node, ts.Diagnostics.A_class_member_cannot_have_the_0_keyword, ts.tokenToString(ts.SyntaxKind.ConstKeyword)); - } - break; - case ts.SyntaxKind.OverrideKeyword: - // If node.kind === SyntaxKind.Parameter, checkParameter reports an error if it's not a parameter property. - if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "override"); - } - else if (flags & ts.ModifierFlags.Ambient) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "override", "declare"); - } - else if (flags & ts.ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "override", "readonly"); - } - else if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "override", "async"); - } - flags |= ts.ModifierFlags.Override; - lastOverride = modifier; - break; + else if (flags & ts.ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "override", "readonly"); + } + else if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "override", "async"); + } + flags |= ts.ModifierFlags.Override; + lastOverride = modifier; + break; - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.PrivateKeyword: - const text = visibilityToString(ts.modifierToFlag(modifier.kind)); - if (flags & ts.ModifierFlags.AccessibilityModifier) { - return grammarErrorOnNode(modifier, ts.Diagnostics.Accessibility_modifier_already_seen); - } - else if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "override"); - } - else if (flags & ts.ModifierFlags.Static) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "static"); - } - else if (flags & ts.ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "readonly"); - } - else if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "async"); - } - else if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, text); - } - else if (flags & ts.ModifierFlags.Abstract) { - if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, text, "abstract"); - } - else { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "abstract"); - } + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.PrivateKeyword: + const text = visibilityToString(ts.modifierToFlag(modifier.kind)); + if (flags & ts.ModifierFlags.AccessibilityModifier) { + return grammarErrorOnNode(modifier, ts.Diagnostics.Accessibility_modifier_already_seen); + } + else if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "override"); + } + else if (flags & ts.ModifierFlags.Static) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "static"); + } + else if (flags & ts.ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "readonly"); + } + else if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "async"); + } + else if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, text); + } + else if (flags & ts.ModifierFlags.Abstract) { + if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, text, "abstract"); } - else if (ts.isPrivateIdentifierClassElementDeclaration(node)) { - return grammarErrorOnNode(modifier, ts.Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); + else { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, text, "abstract"); } - flags |= ts.modifierToFlag(modifier.kind); - break; + } + else if (ts.isPrivateIdentifierClassElementDeclaration(node)) { + return grammarErrorOnNode(modifier, ts.Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); + } + flags |= ts.modifierToFlag(modifier.kind); + break; - case ts.SyntaxKind.StaticKeyword: - if (flags & ts.ModifierFlags.Static) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "static"); - } - else if (flags & ts.ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "readonly"); - } - else if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "async"); - } - else if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, "static"); - } - else if (node.kind === ts.SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "static"); - } - else if (flags & ts.ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); - } - else if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "override"); - } - flags |= ts.ModifierFlags.Static; - lastStatic = modifier; - break; + case ts.SyntaxKind.StaticKeyword: + if (flags & ts.ModifierFlags.Static) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "static"); + } + else if (flags & ts.ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "readonly"); + } + else if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "async"); + } + else if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, "static"); + } + else if (node.kind === ts.SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "static"); + } + else if (flags & ts.ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); + } + else if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "static", "override"); + } + flags |= ts.ModifierFlags.Static; + lastStatic = modifier; + break; - case ts.SyntaxKind.ReadonlyKeyword: - if (flags & ts.ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "readonly"); - } - else if (node.kind !== ts.SyntaxKind.PropertyDeclaration && node.kind !== ts.SyntaxKind.PropertySignature && node.kind !== ts.SyntaxKind.IndexSignature && node.kind !== ts.SyntaxKind.Parameter) { - // If node.kind === SyntaxKind.Parameter, checkParameter reports an error if it's not a parameter property. - return grammarErrorOnNode(modifier, ts.Diagnostics.readonly_modifier_can_only_appear_on_a_property_declaration_or_index_signature); - } - flags |= ts.ModifierFlags.Readonly; - break; + case ts.SyntaxKind.ReadonlyKeyword: + if (flags & ts.ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "readonly"); + } + else if (node.kind !== ts.SyntaxKind.PropertyDeclaration && node.kind !== ts.SyntaxKind.PropertySignature && node.kind !== ts.SyntaxKind.IndexSignature && node.kind !== ts.SyntaxKind.Parameter) { + // If node.kind === SyntaxKind.Parameter, checkParameter reports an error if it's not a parameter property. + return grammarErrorOnNode(modifier, ts.Diagnostics.readonly_modifier_can_only_appear_on_a_property_declaration_or_index_signature); + } + flags |= ts.ModifierFlags.Readonly; + break; - case ts.SyntaxKind.ExportKeyword: - if (flags & ts.ModifierFlags.Export) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "export"); - } - else if (flags & ts.ModifierFlags.Ambient) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "declare"); - } - else if (flags & ts.ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "abstract"); - } - else if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "async"); - } - else if (ts.isClassLike(node.parent)) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind, "export"); - } - else if (node.kind === ts.SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "export"); - } - flags |= ts.ModifierFlags.Export; - break; - case ts.SyntaxKind.DefaultKeyword: - const container = node.parent.kind === ts.SyntaxKind.SourceFile ? node.parent : node.parent.parent; - if (container.kind === ts.SyntaxKind.ModuleDeclaration && !ts.isAmbientModule(container)) { - return grammarErrorOnNode(modifier, ts.Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); - } - else if (!(flags & ts.ModifierFlags.Export)) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "default"); - } + case ts.SyntaxKind.ExportKeyword: + if (flags & ts.ModifierFlags.Export) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "export"); + } + else if (flags & ts.ModifierFlags.Ambient) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "declare"); + } + else if (flags & ts.ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "abstract"); + } + else if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "async"); + } + else if (ts.isClassLike(node.parent)) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind, "export"); + } + else if (node.kind === ts.SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "export"); + } + flags |= ts.ModifierFlags.Export; + break; + case ts.SyntaxKind.DefaultKeyword: + const container = node.parent.kind === ts.SyntaxKind.SourceFile ? node.parent : node.parent.parent; + if (container.kind === ts.SyntaxKind.ModuleDeclaration && !ts.isAmbientModule(container)) { + return grammarErrorOnNode(modifier, ts.Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); + } + else if (!(flags & ts.ModifierFlags.Export)) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "export", "default"); + } - flags |= ts.ModifierFlags.Default; - break; - case ts.SyntaxKind.DeclareKeyword: - if (flags & ts.ModifierFlags.Ambient) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "declare"); - } - else if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); - } - else if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "override"); - } - else if (ts.isClassLike(node.parent) && !ts.isPropertyDeclaration(node)) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind, "declare"); + flags |= ts.ModifierFlags.Default; + break; + case ts.SyntaxKind.DeclareKeyword: + if (flags & ts.ModifierFlags.Ambient) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "declare"); + } + else if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); + } + else if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "override"); + } + else if (ts.isClassLike(node.parent) && !ts.isPropertyDeclaration(node)) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind, "declare"); + } + else if (node.kind === ts.SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "declare"); + } + else if ((node.parent.flags & ts.NodeFlags.Ambient) && node.parent.kind === ts.SyntaxKind.ModuleBlock) { + return grammarErrorOnNode(modifier, ts.Diagnostics.A_declare_modifier_cannot_be_used_in_an_already_ambient_context); + } + else if (ts.isPrivateIdentifierClassElementDeclaration(node)) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "declare"); + } + flags |= ts.ModifierFlags.Ambient; + lastDeclare = modifier; + break; + + case ts.SyntaxKind.AbstractKeyword: + if (flags & ts.ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "abstract"); + } + if (node.kind !== ts.SyntaxKind.ClassDeclaration && + node.kind !== ts.SyntaxKind.ConstructorType) { + if (node.kind !== ts.SyntaxKind.MethodDeclaration && + node.kind !== ts.SyntaxKind.PropertyDeclaration && + node.kind !== ts.SyntaxKind.GetAccessor && + node.kind !== ts.SyntaxKind.SetAccessor) { + return grammarErrorOnNode(modifier, ts.Diagnostics.abstract_modifier_can_only_appear_on_a_class_method_or_property_declaration); } - else if (node.kind === ts.SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "declare"); + if (!(node.parent.kind === ts.SyntaxKind.ClassDeclaration && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Abstract))) { + return grammarErrorOnNode(modifier, ts.Diagnostics.Abstract_methods_can_only_appear_within_an_abstract_class); } - else if ((node.parent.flags & ts.NodeFlags.Ambient) && node.parent.kind === ts.SyntaxKind.ModuleBlock) { - return grammarErrorOnNode(modifier, ts.Diagnostics.A_declare_modifier_cannot_be_used_in_an_already_ambient_context); + if (flags & ts.ModifierFlags.Static) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); } - else if (ts.isPrivateIdentifierClassElementDeclaration(node)) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "declare"); + if (flags & ts.ModifierFlags.Private) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "private", "abstract"); } - flags |= ts.ModifierFlags.Ambient; - lastDeclare = modifier; - break; - - case ts.SyntaxKind.AbstractKeyword: - if (flags & ts.ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "abstract"); - } - if (node.kind !== ts.SyntaxKind.ClassDeclaration && - node.kind !== ts.SyntaxKind.ConstructorType) { - if (node.kind !== ts.SyntaxKind.MethodDeclaration && - node.kind !== ts.SyntaxKind.PropertyDeclaration && - node.kind !== ts.SyntaxKind.GetAccessor && - node.kind !== ts.SyntaxKind.SetAccessor) { - return grammarErrorOnNode(modifier, ts.Diagnostics.abstract_modifier_can_only_appear_on_a_class_method_or_property_declaration); - } - if (!(node.parent.kind === ts.SyntaxKind.ClassDeclaration && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Abstract))) { - return grammarErrorOnNode(modifier, ts.Diagnostics.Abstract_methods_can_only_appear_within_an_abstract_class); - } - if (flags & ts.ModifierFlags.Static) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); - } - if (flags & ts.ModifierFlags.Private) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "private", "abstract"); - } - if (flags & ts.ModifierFlags.Async && lastAsync) { - return grammarErrorOnNode(lastAsync, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "async", "abstract"); - } - if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "abstract", "override"); - } + if (flags & ts.ModifierFlags.Async && lastAsync) { + return grammarErrorOnNode(lastAsync, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "async", "abstract"); } - if (ts.isNamedDeclaration(node) && node.name.kind === ts.SyntaxKind.PrivateIdentifier) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "abstract"); + if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "abstract", "override"); } + } + if (ts.isNamedDeclaration(node) && node.name.kind === ts.SyntaxKind.PrivateIdentifier) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "abstract"); + } - flags |= ts.ModifierFlags.Abstract; - break; + flags |= ts.ModifierFlags.Abstract; + break; - case ts.SyntaxKind.AsyncKeyword: - if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "async"); - } - else if (flags & ts.ModifierFlags.Ambient || node.parent.flags & ts.NodeFlags.Ambient) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); - } - else if (node.kind === ts.SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "async"); - } - if (flags & ts.ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "async", "abstract"); - } - flags |= ts.ModifierFlags.Async; - lastAsync = modifier; - break; + case ts.SyntaxKind.AsyncKeyword: + if (flags & ts.ModifierFlags.Async) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, "async"); + } + else if (flags & ts.ModifierFlags.Ambient || node.parent.flags & ts.NodeFlags.Ambient) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); + } + else if (node.kind === ts.SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter, "async"); + } + if (flags & ts.ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "async", "abstract"); + } + flags |= ts.ModifierFlags.Async; + lastAsync = modifier; + break; - case ts.SyntaxKind.InKeyword: - case ts.SyntaxKind.OutKeyword: - const inOutFlag = modifier.kind === ts.SyntaxKind.InKeyword ? ts.ModifierFlags.In : ts.ModifierFlags.Out; - const inOutText = modifier.kind === ts.SyntaxKind.InKeyword ? "in" : "out"; - if (node.kind !== ts.SyntaxKind.TypeParameter || !(ts.isInterfaceDeclaration(node.parent) || ts.isClassLike(node.parent) || ts.isTypeAliasDeclaration(node.parent))) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_can_only_appear_on_a_type_parameter_of_a_class_interface_or_type_alias, inOutText); - } - if (flags & inOutFlag) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, inOutText); - } - if (inOutFlag & ts.ModifierFlags.In && flags & ts.ModifierFlags.Out) { - return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "in", "out"); - } - flags |= inOutFlag; - break; - } + case ts.SyntaxKind.InKeyword: + case ts.SyntaxKind.OutKeyword: + const inOutFlag = modifier.kind === ts.SyntaxKind.InKeyword ? ts.ModifierFlags.In : ts.ModifierFlags.Out; + const inOutText = modifier.kind === ts.SyntaxKind.InKeyword ? "in" : "out"; + if (node.kind !== ts.SyntaxKind.TypeParameter || !(ts.isInterfaceDeclaration(node.parent) || ts.isClassLike(node.parent) || ts.isTypeAliasDeclaration(node.parent))) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_can_only_appear_on_a_type_parameter_of_a_class_interface_or_type_alias, inOutText); + } + if (flags & inOutFlag) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_already_seen, inOutText); + } + if (inOutFlag & ts.ModifierFlags.In && flags & ts.ModifierFlags.Out) { + return grammarErrorOnNode(modifier, ts.Diagnostics._0_modifier_must_precede_1_modifier, "in", "out"); + } + flags |= inOutFlag; + break; } + } - if (node.kind === ts.SyntaxKind.Constructor) { - if (flags & ts.ModifierFlags.Static) { - return grammarErrorOnNode(lastStatic!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "static"); - } - if (flags & ts.ModifierFlags.Override) { - return grammarErrorOnNode(lastOverride!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "override"); // TODO: GH#18217 - } - if (flags & ts.ModifierFlags.Async) { - return grammarErrorOnNode(lastAsync!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "async"); - } - return false; - } - else if ((node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration) && flags & ts.ModifierFlags.Ambient) { - return grammarErrorOnNode(lastDeclare!, ts.Diagnostics.A_0_modifier_cannot_be_used_with_an_import_declaration, "declare"); - } - else if (node.kind === ts.SyntaxKind.Parameter && (flags & ts.ModifierFlags.ParameterPropertyModifier) && ts.isBindingPattern((node as ts.ParameterDeclaration).name)) { - return grammarErrorOnNode(node, ts.Diagnostics.A_parameter_property_may_not_be_declared_using_a_binding_pattern); + if (node.kind === ts.SyntaxKind.Constructor) { + if (flags & ts.ModifierFlags.Static) { + return grammarErrorOnNode(lastStatic!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "static"); } - else if (node.kind === ts.SyntaxKind.Parameter && (flags & ts.ModifierFlags.ParameterPropertyModifier) && (node as ts.ParameterDeclaration).dotDotDotToken) { - return grammarErrorOnNode(node, ts.Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter); + if (flags & ts.ModifierFlags.Override) { + return grammarErrorOnNode(lastOverride!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "override"); // TODO: GH#18217 } if (flags & ts.ModifierFlags.Async) { - return checkGrammarAsyncModifier(node, lastAsync!); + return grammarErrorOnNode(lastAsync!, ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "async"); } return false; } - - /** - * true | false: Early return this value from checkGrammarModifiers. - * undefined: Need to do full checking on the modifiers. - */ - function reportObviousModifierErrors(node: ts.Node): boolean | undefined { - return !node.modifiers - ? false - : shouldReportBadModifier(node) - ? grammarErrorOnFirstToken(node, ts.Diagnostics.Modifiers_cannot_appear_here) - : undefined; - } - function shouldReportBadModifier(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.TypeParameter: - return false; - default: - if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { - return false; - } - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.AsyncKeyword); - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ConstructorType: - return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.AbstractKeyword); - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return true; - case ts.SyntaxKind.EnumDeclaration: - return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.ConstKeyword); - default: - ts.Debug.fail(); - } - } + else if ((node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration) && flags & ts.ModifierFlags.Ambient) { + return grammarErrorOnNode(lastDeclare!, ts.Diagnostics.A_0_modifier_cannot_be_used_with_an_import_declaration, "declare"); + } + else if (node.kind === ts.SyntaxKind.Parameter && (flags & ts.ModifierFlags.ParameterPropertyModifier) && ts.isBindingPattern((node as ts.ParameterDeclaration).name)) { + return grammarErrorOnNode(node, ts.Diagnostics.A_parameter_property_may_not_be_declared_using_a_binding_pattern); + } + else if (node.kind === ts.SyntaxKind.Parameter && (flags & ts.ModifierFlags.ParameterPropertyModifier) && (node as ts.ParameterDeclaration).dotDotDotToken) { + return grammarErrorOnNode(node, ts.Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter); } - function nodeHasAnyModifiersExcept(node: ts.Node, allowedModifier: ts.SyntaxKind): boolean { - return node.modifiers!.length > 1 || node.modifiers![0].kind !== allowedModifier; + if (flags & ts.ModifierFlags.Async) { + return checkGrammarAsyncModifier(node, lastAsync!); } + return false; + } - function checkGrammarAsyncModifier(node: ts.Node, asyncModifier: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: + /** + * true | false: Early return this value from checkGrammarModifiers. + * undefined: Need to do full checking on the modifiers. + */ + function reportObviousModifierErrors(node: ts.Node): boolean | undefined { + return !node.modifiers + ? false + : shouldReportBadModifier(node) + ? grammarErrorOnFirstToken(node, ts.Diagnostics.Modifiers_cannot_appear_here) + : undefined; + } + function shouldReportBadModifier(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.TypeParameter: + return false; + default: + if (node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { return false; - } - - return grammarErrorOnNode(asyncModifier, ts.Diagnostics._0_modifier_cannot_be_used_here, "async"); + } + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.AsyncKeyword); + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ConstructorType: + return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.AbstractKeyword); + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return true; + case ts.SyntaxKind.EnumDeclaration: + return nodeHasAnyModifiersExcept(node, ts.SyntaxKind.ConstKeyword); + default: + ts.Debug.fail(); + } } + } + function nodeHasAnyModifiersExcept(node: ts.Node, allowedModifier: ts.SyntaxKind): boolean { + return node.modifiers!.length > 1 || node.modifiers![0].kind !== allowedModifier; + } - function checkGrammarForDisallowedTrailingComma(list: ts.NodeArray | undefined, diag = ts.Diagnostics.Trailing_comma_not_allowed): boolean { - if (list && list.hasTrailingComma) { - return grammarErrorAtPos(list[0], list.end - ",".length, ",".length, diag); - } - return false; + function checkGrammarAsyncModifier(node: ts.Node, asyncModifier: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return false; } - function checkGrammarTypeParameterList(typeParameters: ts.NodeArray | undefined, file: ts.SourceFile): boolean { - if (typeParameters && typeParameters.length === 0) { - const start = typeParameters.pos - "<".length; - const end = ts.skipTrivia(file.text, typeParameters.end) + ">".length; - return grammarErrorAtPos(file, start, end - start, ts.Diagnostics.Type_parameter_list_cannot_be_empty); - } - return false; + return grammarErrorOnNode(asyncModifier, ts.Diagnostics._0_modifier_cannot_be_used_here, "async"); + } + + function checkGrammarForDisallowedTrailingComma(list: ts.NodeArray | undefined, diag = ts.Diagnostics.Trailing_comma_not_allowed): boolean { + if (list && list.hasTrailingComma) { + return grammarErrorAtPos(list[0], list.end - ",".length, ",".length, diag); } + return false; + } - function checkGrammarParameterList(parameters: ts.NodeArray) { - let seenOptionalParameter = false; - const parameterCount = parameters.length; + function checkGrammarTypeParameterList(typeParameters: ts.NodeArray | undefined, file: ts.SourceFile): boolean { + if (typeParameters && typeParameters.length === 0) { + const start = typeParameters.pos - "<".length; + const end = ts.skipTrivia(file.text, typeParameters.end) + ">".length; + return grammarErrorAtPos(file, start, end - start, ts.Diagnostics.Type_parameter_list_cannot_be_empty); + } + return false; + } - for (let i = 0; i < parameterCount; i++) { - const parameter = parameters[i]; - if (parameter.dotDotDotToken) { - if (i !== (parameterCount - 1)) { - return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); - } - if (!(parameter.flags & ts.NodeFlags.Ambient)) { // Allow `...foo,` in ambient declarations; see GH#23070 - checkGrammarForDisallowedTrailingComma(parameters, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - } + function checkGrammarParameterList(parameters: ts.NodeArray) { + let seenOptionalParameter = false; + const parameterCount = parameters.length; - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.A_rest_parameter_cannot_be_optional); - } + for (let i = 0; i < parameterCount; i++) { + const parameter = parameters[i]; + if (parameter.dotDotDotToken) { + if (i !== (parameterCount - 1)) { + return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); + } + if (!(parameter.flags & ts.NodeFlags.Ambient)) { // Allow `...foo,` in ambient declarations; see GH#23070 + checkGrammarForDisallowedTrailingComma(parameters, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + } - if (parameter.initializer) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.A_rest_parameter_cannot_have_an_initializer); - } + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.A_rest_parameter_cannot_be_optional); } - else if (isOptionalParameter(parameter)) { - seenOptionalParameter = true; - if (parameter.questionToken && parameter.initializer) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.Parameter_cannot_have_question_mark_and_initializer); - } + + if (parameter.initializer) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.A_rest_parameter_cannot_have_an_initializer); } - else if (seenOptionalParameter && !parameter.initializer) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.A_required_parameter_cannot_follow_an_optional_parameter); + } + else if (isOptionalParameter(parameter)) { + seenOptionalParameter = true; + if (parameter.questionToken && parameter.initializer) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.Parameter_cannot_have_question_mark_and_initializer); } } + else if (seenOptionalParameter && !parameter.initializer) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.A_required_parameter_cannot_follow_an_optional_parameter); + } } + } - function getNonSimpleParameters(parameters: readonly ts.ParameterDeclaration[]): readonly ts.ParameterDeclaration[] { - return ts.filter(parameters, parameter => !!parameter.initializer || ts.isBindingPattern(parameter.name) || ts.isRestParameter(parameter)); - } + function getNonSimpleParameters(parameters: readonly ts.ParameterDeclaration[]): readonly ts.ParameterDeclaration[] { + return ts.filter(parameters, parameter => !!parameter.initializer || ts.isBindingPattern(parameter.name) || ts.isRestParameter(parameter)); + } - function checkGrammarForUseStrictSimpleParameterList(node: ts.FunctionLikeDeclaration): boolean { - if (languageVersion >= ts.ScriptTarget.ES2016) { - const useStrictDirective = node.body && ts.isBlock(node.body) && ts.findUseStrictPrologue(node.body.statements); - if (useStrictDirective) { - const nonSimpleParameters = getNonSimpleParameters(node.parameters); - if (ts.length(nonSimpleParameters)) { - ts.forEach(nonSimpleParameters, parameter => { - ts.addRelatedInfo(error(parameter, ts.Diagnostics.This_parameter_is_not_allowed_with_use_strict_directive), ts.createDiagnosticForNode(useStrictDirective, ts.Diagnostics.use_strict_directive_used_here)); - }); + function checkGrammarForUseStrictSimpleParameterList(node: ts.FunctionLikeDeclaration): boolean { + if (languageVersion >= ts.ScriptTarget.ES2016) { + const useStrictDirective = node.body && ts.isBlock(node.body) && ts.findUseStrictPrologue(node.body.statements); + if (useStrictDirective) { + const nonSimpleParameters = getNonSimpleParameters(node.parameters); + if (ts.length(nonSimpleParameters)) { + ts.forEach(nonSimpleParameters, parameter => { + ts.addRelatedInfo(error(parameter, ts.Diagnostics.This_parameter_is_not_allowed_with_use_strict_directive), ts.createDiagnosticForNode(useStrictDirective, ts.Diagnostics.use_strict_directive_used_here)); + }); - const diagnostics = nonSimpleParameters.map((parameter, index) => (index === 0 ? ts.createDiagnosticForNode(parameter, ts.Diagnostics.Non_simple_parameter_declared_here) : ts.createDiagnosticForNode(parameter, ts.Diagnostics.and_here))) as [ - ts.DiagnosticWithLocation, - ...ts.DiagnosticWithLocation[] - ]; - ts.addRelatedInfo(error(useStrictDirective, ts.Diagnostics.use_strict_directive_cannot_be_used_with_non_simple_parameter_list), ...diagnostics); - return true; - } + const diagnostics = nonSimpleParameters.map((parameter, index) => (index === 0 ? ts.createDiagnosticForNode(parameter, ts.Diagnostics.Non_simple_parameter_declared_here) : ts.createDiagnosticForNode(parameter, ts.Diagnostics.and_here))) as [ + ts.DiagnosticWithLocation, + ...ts.DiagnosticWithLocation[] + ]; + ts.addRelatedInfo(error(useStrictDirective, ts.Diagnostics.use_strict_directive_cannot_be_used_with_non_simple_parameter_list), ...diagnostics); + return true; } } - return false; } + return false; + } - function checkGrammarFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration | ts.MethodSignature): boolean { - // Prevent cascading error by short-circuit - const file = ts.getSourceFileOfNode(node); - return checkGrammarDecoratorsAndModifiers(node) || - checkGrammarTypeParameterList(node.typeParameters, file) || - checkGrammarParameterList(node.parameters) || - checkGrammarArrowFunction(node, file) || - (ts.isFunctionLikeDeclaration(node) && checkGrammarForUseStrictSimpleParameterList(node)); - } + function checkGrammarFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration | ts.MethodSignature): boolean { + // Prevent cascading error by short-circuit + const file = ts.getSourceFileOfNode(node); + return checkGrammarDecoratorsAndModifiers(node) || + checkGrammarTypeParameterList(node.typeParameters, file) || + checkGrammarParameterList(node.parameters) || + checkGrammarArrowFunction(node, file) || + (ts.isFunctionLikeDeclaration(node) && checkGrammarForUseStrictSimpleParameterList(node)); + } - function checkGrammarClassLikeDeclaration(node: ts.ClassLikeDeclaration): boolean { - const file = ts.getSourceFileOfNode(node); - return checkGrammarClassDeclarationHeritageClauses(node) || - checkGrammarTypeParameterList(node.typeParameters, file); - } + function checkGrammarClassLikeDeclaration(node: ts.ClassLikeDeclaration): boolean { + const file = ts.getSourceFileOfNode(node); + return checkGrammarClassDeclarationHeritageClauses(node) || + checkGrammarTypeParameterList(node.typeParameters, file); + } - function checkGrammarArrowFunction(node: ts.Node, file: ts.SourceFile): boolean { - if (!ts.isArrowFunction(node)) { - return false; - } + function checkGrammarArrowFunction(node: ts.Node, file: ts.SourceFile): boolean { + if (!ts.isArrowFunction(node)) { + return false; + } - if (node.typeParameters && !(ts.length(node.typeParameters) > 1 || node.typeParameters.hasTrailingComma || node.typeParameters[0].constraint)) { - if (file && ts.fileExtensionIsOneOf(file.fileName, [ts.Extension.Mts, ts.Extension.Cts])) { - grammarErrorOnNode(node.typeParameters[0], ts.Diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Add_a_trailing_comma_or_explicit_constraint); - } + if (node.typeParameters && !(ts.length(node.typeParameters) > 1 || node.typeParameters.hasTrailingComma || node.typeParameters[0].constraint)) { + if (file && ts.fileExtensionIsOneOf(file.fileName, [ts.Extension.Mts, ts.Extension.Cts])) { + grammarErrorOnNode(node.typeParameters[0], ts.Diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Add_a_trailing_comma_or_explicit_constraint); } - - const { equalsGreaterThanToken } = node; - const startLine = ts.getLineAndCharacterOfPosition(file, equalsGreaterThanToken.pos).line; - const endLine = ts.getLineAndCharacterOfPosition(file, equalsGreaterThanToken.end).line; - return startLine !== endLine && grammarErrorOnNode(equalsGreaterThanToken, ts.Diagnostics.Line_terminator_not_permitted_before_arrow); } - function checkGrammarIndexSignatureParameters(node: ts.SignatureDeclaration): boolean { - const parameter = node.parameters[0]; - if (node.parameters.length !== 1) { - if (parameter) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_must_have_exactly_one_parameter); - } - else { - return grammarErrorOnNode(node, ts.Diagnostics.An_index_signature_must_have_exactly_one_parameter); - } - } - checkGrammarForDisallowedTrailingComma(node.parameters, ts.Diagnostics.An_index_signature_cannot_have_a_trailing_comma); - if (parameter.dotDotDotToken) { - return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.An_index_signature_cannot_have_a_rest_parameter); - } - if (ts.hasEffectiveModifiers(parameter)) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_cannot_have_an_accessibility_modifier); - } - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.An_index_signature_parameter_cannot_have_a_question_mark); - } - if (parameter.initializer) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_cannot_have_an_initializer); - } - if (!parameter.type) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_must_have_a_type_annotation); - } - const type = getTypeFromTypeNode(parameter.type); - if (someType(type, t => !!(t.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique)) || isGenericType(type)) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_type_cannot_be_a_literal_type_or_generic_type_Consider_using_a_mapped_object_type_instead); - } - if (!everyType(type, isValidIndexKeyType)) { - return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_type_must_be_string_number_symbol_or_a_template_literal_type); + const { equalsGreaterThanToken } = node; + const startLine = ts.getLineAndCharacterOfPosition(file, equalsGreaterThanToken.pos).line; + const endLine = ts.getLineAndCharacterOfPosition(file, equalsGreaterThanToken.end).line; + return startLine !== endLine && grammarErrorOnNode(equalsGreaterThanToken, ts.Diagnostics.Line_terminator_not_permitted_before_arrow); + } + + function checkGrammarIndexSignatureParameters(node: ts.SignatureDeclaration): boolean { + const parameter = node.parameters[0]; + if (node.parameters.length !== 1) { + if (parameter) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_must_have_exactly_one_parameter); } - if (!node.type) { - return grammarErrorOnNode(node, ts.Diagnostics.An_index_signature_must_have_a_type_annotation); + else { + return grammarErrorOnNode(node, ts.Diagnostics.An_index_signature_must_have_exactly_one_parameter); } - return false; } - - function checkGrammarIndexSignature(node: ts.SignatureDeclaration) { - // Prevent cascading error by short-circuit - return checkGrammarDecoratorsAndModifiers(node) || checkGrammarIndexSignatureParameters(node); + checkGrammarForDisallowedTrailingComma(node.parameters, ts.Diagnostics.An_index_signature_cannot_have_a_trailing_comma); + if (parameter.dotDotDotToken) { + return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.An_index_signature_cannot_have_a_rest_parameter); } - - function checkGrammarForAtLeastOneTypeArgument(node: ts.Node, typeArguments: ts.NodeArray | undefined): boolean { - if (typeArguments && typeArguments.length === 0) { - const sourceFile = ts.getSourceFileOfNode(node); - const start = typeArguments.pos - "<".length; - const end = ts.skipTrivia(sourceFile.text, typeArguments.end) + ">".length; - return grammarErrorAtPos(sourceFile, start, end - start, ts.Diagnostics.Type_argument_list_cannot_be_empty); - } - return false; + if (ts.hasEffectiveModifiers(parameter)) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_cannot_have_an_accessibility_modifier); } - - function checkGrammarTypeArguments(node: ts.Node, typeArguments: ts.NodeArray | undefined): boolean { - return checkGrammarForDisallowedTrailingComma(typeArguments) || - checkGrammarForAtLeastOneTypeArgument(node, typeArguments); + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.An_index_signature_parameter_cannot_have_a_question_mark); + } + if (parameter.initializer) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_cannot_have_an_initializer); + } + if (!parameter.type) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_must_have_a_type_annotation); + } + const type = getTypeFromTypeNode(parameter.type); + if (someType(type, t => !!(t.flags & ts.TypeFlags.StringOrNumberLiteralOrUnique)) || isGenericType(type)) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_type_cannot_be_a_literal_type_or_generic_type_Consider_using_a_mapped_object_type_instead); } + if (!everyType(type, isValidIndexKeyType)) { + return grammarErrorOnNode(parameter.name, ts.Diagnostics.An_index_signature_parameter_type_must_be_string_number_symbol_or_a_template_literal_type); + } + if (!node.type) { + return grammarErrorOnNode(node, ts.Diagnostics.An_index_signature_must_have_a_type_annotation); + } + return false; + } - function checkGrammarTaggedTemplateChain(node: ts.TaggedTemplateExpression): boolean { - if (node.questionDotToken || node.flags & ts.NodeFlags.OptionalChain) { - return grammarErrorOnNode(node.template, ts.Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain); - } - return false; + function checkGrammarIndexSignature(node: ts.SignatureDeclaration) { + // Prevent cascading error by short-circuit + return checkGrammarDecoratorsAndModifiers(node) || checkGrammarIndexSignatureParameters(node); + } + + function checkGrammarForAtLeastOneTypeArgument(node: ts.Node, typeArguments: ts.NodeArray | undefined): boolean { + if (typeArguments && typeArguments.length === 0) { + const sourceFile = ts.getSourceFileOfNode(node); + const start = typeArguments.pos - "<".length; + const end = ts.skipTrivia(sourceFile.text, typeArguments.end) + ">".length; + return grammarErrorAtPos(sourceFile, start, end - start, ts.Diagnostics.Type_argument_list_cannot_be_empty); } + return false; + } - function checkGrammarHeritageClause(node: ts.HeritageClause): boolean { - const types = node.types; - if (checkGrammarForDisallowedTrailingComma(types)) { - return true; - } - if (types && types.length === 0) { - const listType = ts.tokenToString(node.token); - return grammarErrorAtPos(node, types.pos, 0, ts.Diagnostics._0_list_cannot_be_empty, listType); - } - return ts.some(types, checkGrammarExpressionWithTypeArguments); + function checkGrammarTypeArguments(node: ts.Node, typeArguments: ts.NodeArray | undefined): boolean { + return checkGrammarForDisallowedTrailingComma(typeArguments) || + checkGrammarForAtLeastOneTypeArgument(node, typeArguments); + } + + function checkGrammarTaggedTemplateChain(node: ts.TaggedTemplateExpression): boolean { + if (node.questionDotToken || node.flags & ts.NodeFlags.OptionalChain) { + return grammarErrorOnNode(node.template, ts.Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain); } + return false; + } - function checkGrammarExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments | ts.TypeQueryNode) { - if (ts.isExpressionWithTypeArguments(node) && ts.isImportKeyword(node.expression) && node.typeArguments) { - return grammarErrorOnNode(node, ts.Diagnostics.This_use_of_import_is_invalid_import_calls_can_be_written_but_they_must_have_parentheses_and_cannot_have_type_arguments); - } - return checkGrammarTypeArguments(node, node.typeArguments); + function checkGrammarHeritageClause(node: ts.HeritageClause): boolean { + const types = node.types; + if (checkGrammarForDisallowedTrailingComma(types)) { + return true; + } + if (types && types.length === 0) { + const listType = ts.tokenToString(node.token); + return grammarErrorAtPos(node, types.pos, 0, ts.Diagnostics._0_list_cannot_be_empty, listType); } + return ts.some(types, checkGrammarExpressionWithTypeArguments); + } - function checkGrammarClassDeclarationHeritageClauses(node: ts.ClassLikeDeclaration) { - let seenExtendsClause = false; - let seenImplementsClause = false; + function checkGrammarExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments | ts.TypeQueryNode) { + if (ts.isExpressionWithTypeArguments(node) && ts.isImportKeyword(node.expression) && node.typeArguments) { + return grammarErrorOnNode(node, ts.Diagnostics.This_use_of_import_is_invalid_import_calls_can_be_written_but_they_must_have_parentheses_and_cannot_have_type_arguments); + } + return checkGrammarTypeArguments(node, node.typeArguments); + } - if (!checkGrammarDecoratorsAndModifiers(node) && node.heritageClauses) { - for (const heritageClause of node.heritageClauses) { - if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { - if (seenExtendsClause) { - return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_already_seen); - } + function checkGrammarClassDeclarationHeritageClauses(node: ts.ClassLikeDeclaration) { + let seenExtendsClause = false; + let seenImplementsClause = false; - if (seenImplementsClause) { - return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_must_precede_implements_clause); - } + if (!checkGrammarDecoratorsAndModifiers(node) && node.heritageClauses) { + for (const heritageClause of node.heritageClauses) { + if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { + if (seenExtendsClause) { + return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_already_seen); + } - if (heritageClause.types.length > 1) { - return grammarErrorOnFirstToken(heritageClause.types[1], ts.Diagnostics.Classes_can_only_extend_a_single_class); - } + if (seenImplementsClause) { + return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_must_precede_implements_clause); + } - seenExtendsClause = true; + if (heritageClause.types.length > 1) { + return grammarErrorOnFirstToken(heritageClause.types[1], ts.Diagnostics.Classes_can_only_extend_a_single_class); } - else { - ts.Debug.assert(heritageClause.token === ts.SyntaxKind.ImplementsKeyword); - if (seenImplementsClause) { - return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.implements_clause_already_seen); - } - seenImplementsClause = true; + seenExtendsClause = true; + } + else { + ts.Debug.assert(heritageClause.token === ts.SyntaxKind.ImplementsKeyword); + if (seenImplementsClause) { + return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.implements_clause_already_seen); } - // Grammar checking heritageClause inside class declaration - checkGrammarHeritageClause(heritageClause); + seenImplementsClause = true; } + + // Grammar checking heritageClause inside class declaration + checkGrammarHeritageClause(heritageClause); } } + } - function checkGrammarInterfaceDeclaration(node: ts.InterfaceDeclaration) { - let seenExtendsClause = false; - - if (node.heritageClauses) { - for (const heritageClause of node.heritageClauses) { - if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { - if (seenExtendsClause) { - return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_already_seen); - } + function checkGrammarInterfaceDeclaration(node: ts.InterfaceDeclaration) { + let seenExtendsClause = false; - seenExtendsClause = true; - } - else { - ts.Debug.assert(heritageClause.token === ts.SyntaxKind.ImplementsKeyword); - return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.Interface_declaration_cannot_have_implements_clause); + if (node.heritageClauses) { + for (const heritageClause of node.heritageClauses) { + if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { + if (seenExtendsClause) { + return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.extends_clause_already_seen); } - // Grammar checking heritageClause inside class declaration - checkGrammarHeritageClause(heritageClause); + seenExtendsClause = true; + } + else { + ts.Debug.assert(heritageClause.token === ts.SyntaxKind.ImplementsKeyword); + return grammarErrorOnFirstToken(heritageClause, ts.Diagnostics.Interface_declaration_cannot_have_implements_clause); } - } - return false; - } - function checkGrammarComputedPropertyName(node: ts.Node): boolean { - // If node is not a computedPropertyName, just skip the grammar checking - if (node.kind !== ts.SyntaxKind.ComputedPropertyName) { - return false; + // Grammar checking heritageClause inside class declaration + checkGrammarHeritageClause(heritageClause); } + } + return false; + } - const computedPropertyName = node as ts.ComputedPropertyName; - if (computedPropertyName.expression.kind === ts.SyntaxKind.BinaryExpression && (computedPropertyName.expression as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken) { - return grammarErrorOnNode(computedPropertyName.expression, ts.Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name); - } + function checkGrammarComputedPropertyName(node: ts.Node): boolean { + // If node is not a computedPropertyName, just skip the grammar checking + if (node.kind !== ts.SyntaxKind.ComputedPropertyName) { return false; } - function checkGrammarForGenerator(node: ts.FunctionLikeDeclaration) { - if (node.asteriskToken) { - ts.Debug.assert(node.kind === ts.SyntaxKind.FunctionDeclaration || - node.kind === ts.SyntaxKind.FunctionExpression || - node.kind === ts.SyntaxKind.MethodDeclaration); - if (node.flags & ts.NodeFlags.Ambient) { - return grammarErrorOnNode(node.asteriskToken, ts.Diagnostics.Generators_are_not_allowed_in_an_ambient_context); - } - if (!node.body) { - return grammarErrorOnNode(node.asteriskToken, ts.Diagnostics.An_overload_signature_cannot_be_declared_as_a_generator); - } - } + const computedPropertyName = node as ts.ComputedPropertyName; + if (computedPropertyName.expression.kind === ts.SyntaxKind.BinaryExpression && (computedPropertyName.expression as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken) { + return grammarErrorOnNode(computedPropertyName.expression, ts.Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name); } + return false; + } - function checkGrammarForInvalidQuestionMark(questionToken: ts.QuestionToken | undefined, message: ts.DiagnosticMessage): boolean { - return !!questionToken && grammarErrorOnNode(questionToken, message); + function checkGrammarForGenerator(node: ts.FunctionLikeDeclaration) { + if (node.asteriskToken) { + ts.Debug.assert(node.kind === ts.SyntaxKind.FunctionDeclaration || + node.kind === ts.SyntaxKind.FunctionExpression || + node.kind === ts.SyntaxKind.MethodDeclaration); + if (node.flags & ts.NodeFlags.Ambient) { + return grammarErrorOnNode(node.asteriskToken, ts.Diagnostics.Generators_are_not_allowed_in_an_ambient_context); + } + if (!node.body) { + return grammarErrorOnNode(node.asteriskToken, ts.Diagnostics.An_overload_signature_cannot_be_declared_as_a_generator); + } } + } - function checkGrammarForInvalidExclamationToken(exclamationToken: ts.ExclamationToken | undefined, message: ts.DiagnosticMessage): boolean { - return !!exclamationToken && grammarErrorOnNode(exclamationToken, message); - } + function checkGrammarForInvalidQuestionMark(questionToken: ts.QuestionToken | undefined, message: ts.DiagnosticMessage): boolean { + return !!questionToken && grammarErrorOnNode(questionToken, message); + } + + function checkGrammarForInvalidExclamationToken(exclamationToken: ts.ExclamationToken | undefined, message: ts.DiagnosticMessage): boolean { + return !!exclamationToken && grammarErrorOnNode(exclamationToken, message); + } - function checkGrammarObjectLiteralExpression(node: ts.ObjectLiteralExpression, inDestructuring: boolean) { - const seen = new ts.Map(); + function checkGrammarObjectLiteralExpression(node: ts.ObjectLiteralExpression, inDestructuring: boolean) { + const seen = new ts.Map(); - for (const prop of node.properties) { - if (prop.kind === ts.SyntaxKind.SpreadAssignment) { - if (inDestructuring) { - // a rest property cannot be destructured any further - const expression = ts.skipParentheses(prop.expression); - if (ts.isArrayLiteralExpression(expression) || ts.isObjectLiteralExpression(expression)) { - return grammarErrorOnNode(prop.expression, ts.Diagnostics.A_rest_element_cannot_contain_a_binding_pattern); - } + for (const prop of node.properties) { + if (prop.kind === ts.SyntaxKind.SpreadAssignment) { + if (inDestructuring) { + // a rest property cannot be destructured any further + const expression = ts.skipParentheses(prop.expression); + if (ts.isArrayLiteralExpression(expression) || ts.isObjectLiteralExpression(expression)) { + return grammarErrorOnNode(prop.expression, ts.Diagnostics.A_rest_element_cannot_contain_a_binding_pattern); } - continue; - } - const name = prop.name; - if (name.kind === ts.SyntaxKind.ComputedPropertyName) { - // If the name is not a ComputedPropertyName, the grammar checking will skip it - checkGrammarComputedPropertyName(name); } + continue; + } + const name = prop.name; + if (name.kind === ts.SyntaxKind.ComputedPropertyName) { + // If the name is not a ComputedPropertyName, the grammar checking will skip it + checkGrammarComputedPropertyName(name); + } + + if (prop.kind === ts.SyntaxKind.ShorthandPropertyAssignment && !inDestructuring && prop.objectAssignmentInitializer) { + // having objectAssignmentInitializer is only valid in ObjectAssignmentPattern + // outside of destructuring it is a syntax error + grammarErrorOnNode(prop.equalsToken!, ts.Diagnostics.Did_you_mean_to_use_a_Colon_An_can_only_follow_a_property_name_when_the_containing_object_literal_is_part_of_a_destructuring_pattern); + } - if (prop.kind === ts.SyntaxKind.ShorthandPropertyAssignment && !inDestructuring && prop.objectAssignmentInitializer) { - // having objectAssignmentInitializer is only valid in ObjectAssignmentPattern - // outside of destructuring it is a syntax error - grammarErrorOnNode(prop.equalsToken!, ts.Diagnostics.Did_you_mean_to_use_a_Colon_An_can_only_follow_a_property_name_when_the_containing_object_literal_is_part_of_a_destructuring_pattern); - } + if (name.kind === ts.SyntaxKind.PrivateIdentifier) { + grammarErrorOnNode(name, ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + } - if (name.kind === ts.SyntaxKind.PrivateIdentifier) { - grammarErrorOnNode(name, ts.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) { + for (const mod of prop.modifiers) { + if (mod.kind !== ts.SyntaxKind.AsyncKeyword || prop.kind !== ts.SyntaxKind.MethodDeclaration) { + grammarErrorOnNode(mod, ts.Diagnostics._0_modifier_cannot_be_used_here, ts.getTextOfNode(mod)); + } } + } - // Modifiers are never allowed on properties except for 'async' on a method declaration - if (prop.modifiers) { - for (const mod of prop.modifiers) { - if (mod.kind !== ts.SyntaxKind.AsyncKeyword || prop.kind !== ts.SyntaxKind.MethodDeclaration) { - grammarErrorOnNode(mod, ts.Diagnostics._0_modifier_cannot_be_used_here, ts.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 ts.SyntaxKind.ShorthandPropertyAssignment: + checkGrammarForInvalidExclamationToken(prop.exclamationToken, ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); + // falls through + case ts.SyntaxKind.PropertyAssignment: + // Grammar checking for computedPropertyName and shorthandPropertyAssignment + checkGrammarForInvalidQuestionMark(prop.questionToken, ts.Diagnostics.An_object_member_cannot_be_declared_optional); + if (name.kind === ts.SyntaxKind.NumericLiteral) { + checkGrammarNumericLiteral(name); } - } + currentKind = DeclarationMeaning.PropertyAssignment; + break; + case ts.SyntaxKind.MethodDeclaration: + currentKind = DeclarationMeaning.Method; + break; + case ts.SyntaxKind.GetAccessor: + currentKind = DeclarationMeaning.GetAccessor; + break; + case ts.SyntaxKind.SetAccessor: + currentKind = DeclarationMeaning.SetAccessor; + break; + default: + throw ts.Debug.assertNever(prop, "Unexpected syntax kind:" + (prop as ts.Node).kind); + } - // 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 ts.SyntaxKind.ShorthandPropertyAssignment: - checkGrammarForInvalidExclamationToken(prop.exclamationToken, ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); - // falls through - case ts.SyntaxKind.PropertyAssignment: - // Grammar checking for computedPropertyName and shorthandPropertyAssignment - checkGrammarForInvalidQuestionMark(prop.questionToken, ts.Diagnostics.An_object_member_cannot_be_declared_optional); - if (name.kind === ts.SyntaxKind.NumericLiteral) { - checkGrammarNumericLiteral(name); - } - currentKind = DeclarationMeaning.PropertyAssignment; - break; - case ts.SyntaxKind.MethodDeclaration: - currentKind = DeclarationMeaning.Method; - break; - case ts.SyntaxKind.GetAccessor: - currentKind = DeclarationMeaning.GetAccessor; - break; - case ts.SyntaxKind.SetAccessor: - currentKind = DeclarationMeaning.SetAccessor; - break; - default: - throw ts.Debug.assertNever(prop, "Unexpected syntax kind:" + (prop as ts.Node).kind); + if (!inDestructuring) { + const effectiveName = ts.getPropertyNameForPropertyNameNode(name); + if (effectiveName === undefined) { + continue; } - if (!inDestructuring) { - const effectiveName = ts.getPropertyNameForPropertyNameNode(name); - if (effectiveName === undefined) { - continue; + const existingKind = seen.get(effectiveName); + if (!existingKind) { + seen.set(effectiveName, currentKind); + } + else { + if ((currentKind & DeclarationMeaning.Method) && (existingKind & DeclarationMeaning.Method)) { + grammarErrorOnNode(name, ts.Diagnostics.Duplicate_identifier_0, ts.getTextOfNode(name)); } - - const existingKind = seen.get(effectiveName); - if (!existingKind) { - seen.set(effectiveName, currentKind); + else if ((currentKind & DeclarationMeaning.PropertyAssignment) && (existingKind & DeclarationMeaning.PropertyAssignment)) { + grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_multiple_properties_with_the_same_name, ts.getTextOfNode(name)); } - else { - if ((currentKind & DeclarationMeaning.Method) && (existingKind & DeclarationMeaning.Method)) { - grammarErrorOnNode(name, ts.Diagnostics.Duplicate_identifier_0, ts.getTextOfNode(name)); - } - else if ((currentKind & DeclarationMeaning.PropertyAssignment) && (existingKind & DeclarationMeaning.PropertyAssignment)) { - grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_multiple_properties_with_the_same_name, ts.getTextOfNode(name)); - } - else if ((currentKind & DeclarationMeaning.GetOrSetAccessor) && (existingKind & DeclarationMeaning.GetOrSetAccessor)) { - if (existingKind !== DeclarationMeaning.GetOrSetAccessor && currentKind !== existingKind) { - seen.set(effectiveName, currentKind | existingKind); - } - else { - return grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_multiple_get_Slashset_accessors_with_the_same_name); - } + else if ((currentKind & DeclarationMeaning.GetOrSetAccessor) && (existingKind & DeclarationMeaning.GetOrSetAccessor)) { + if (existingKind !== DeclarationMeaning.GetOrSetAccessor && currentKind !== existingKind) { + seen.set(effectiveName, currentKind | existingKind); } else { - return grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_property_and_accessor_with_the_same_name); + return grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_multiple_get_Slashset_accessors_with_the_same_name); } } + else { + return grammarErrorOnNode(name, ts.Diagnostics.An_object_literal_cannot_have_property_and_accessor_with_the_same_name); + } } } } + } - function checkGrammarJsxElement(node: ts.JsxOpeningLikeElement) { - checkGrammarJsxName(node.tagName); - checkGrammarTypeArguments(node, node.typeArguments); - const seen = new ts.Map(); + function checkGrammarJsxElement(node: ts.JsxOpeningLikeElement) { + checkGrammarJsxName(node.tagName); + checkGrammarTypeArguments(node, node.typeArguments); + const seen = new ts.Map(); - for (const attr of node.attributes.properties) { - if (attr.kind === ts.SyntaxKind.JsxSpreadAttribute) { - continue; - } + for (const attr of node.attributes.properties) { + if (attr.kind === ts.SyntaxKind.JsxSpreadAttribute) { + continue; + } - const { name, initializer } = attr; - if (!seen.get(name.escapedText)) { - seen.set(name.escapedText, true); - } - else { - return grammarErrorOnNode(name, ts.Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name); - } + const { name, initializer } = attr; + if (!seen.get(name.escapedText)) { + seen.set(name.escapedText, true); + } + else { + return grammarErrorOnNode(name, ts.Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name); + } - if (initializer && initializer.kind === ts.SyntaxKind.JsxExpression && !initializer.expression) { - return grammarErrorOnNode(initializer, ts.Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression); - } + if (initializer && initializer.kind === ts.SyntaxKind.JsxExpression && !initializer.expression) { + return grammarErrorOnNode(initializer, ts.Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression); } } + } - function checkGrammarJsxName(node: ts.JsxTagNameExpression) { - if (ts.isPropertyAccessExpression(node)) { - let propName: ts.JsxTagNameExpression = node; - do { - const check = checkGrammarJsxNestedIdentifier(propName.name); - if (check) { - return check; - } - propName = propName.expression; - } while (ts.isPropertyAccessExpression(propName)); - const check = checkGrammarJsxNestedIdentifier(propName); + function checkGrammarJsxName(node: ts.JsxTagNameExpression) { + if (ts.isPropertyAccessExpression(node)) { + let propName: ts.JsxTagNameExpression = node; + do { + const check = checkGrammarJsxNestedIdentifier(propName.name); if (check) { return check; } + propName = propName.expression; + } while (ts.isPropertyAccessExpression(propName)); + const check = checkGrammarJsxNestedIdentifier(propName); + if (check) { + return check; } + } - function checkGrammarJsxNestedIdentifier(name: ts.MemberName | ts.ThisExpression) { - if (ts.isIdentifier(name) && ts.idText(name).indexOf(":") !== -1) { - return grammarErrorOnNode(name, ts.Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names); - } + function checkGrammarJsxNestedIdentifier(name: ts.MemberName | ts.ThisExpression) { + if (ts.isIdentifier(name) && ts.idText(name).indexOf(":") !== -1) { + return grammarErrorOnNode(name, ts.Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names); } } + } - function checkGrammarJsxExpression(node: ts.JsxExpression) { - if (node.expression && ts.isCommaSequence(node.expression)) { - return grammarErrorOnNode(node.expression, ts.Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array); - } + function checkGrammarJsxExpression(node: ts.JsxExpression) { + if (node.expression && ts.isCommaSequence(node.expression)) { + return grammarErrorOnNode(node.expression, ts.Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array); } + } - function checkGrammarForInOrForOfStatement(forInOrOfStatement: ts.ForInOrOfStatement): boolean { - if (checkGrammarStatementInAmbientContext(forInOrOfStatement)) { - return true; - } + function checkGrammarForInOrForOfStatement(forInOrOfStatement: ts.ForInOrOfStatement): boolean { + if (checkGrammarStatementInAmbientContext(forInOrOfStatement)) { + return true; + } - if (forInOrOfStatement.kind === ts.SyntaxKind.ForOfStatement && forInOrOfStatement.awaitModifier) { - if (!(forInOrOfStatement.flags & ts.NodeFlags.AwaitContext)) { - const sourceFile = ts.getSourceFileOfNode(forInOrOfStatement); - if (ts.isInTopLevelContext(forInOrOfStatement)) { - if (!hasParseDiagnostics(sourceFile)) { - if (!ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { - diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.for_await_loops_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)); - } - switch (moduleKind) { - case ts.ModuleKind.Node16: - case ts.ModuleKind.NodeNext: - if (sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS) { - diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level)); - break; - } - // fallthrough - case ts.ModuleKind.ES2022: - case ts.ModuleKind.ESNext: - case ts.ModuleKind.System: - if (languageVersion >= ts.ScriptTarget.ES2017) { - break; - } - // fallthrough - default: - diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.Top_level_for_await_loops_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_or_nodenext_and_the_target_option_is_set_to_es2017_or_higher)); + if (forInOrOfStatement.kind === ts.SyntaxKind.ForOfStatement && forInOrOfStatement.awaitModifier) { + if (!(forInOrOfStatement.flags & ts.NodeFlags.AwaitContext)) { + const sourceFile = ts.getSourceFileOfNode(forInOrOfStatement); + if (ts.isInTopLevelContext(forInOrOfStatement)) { + if (!hasParseDiagnostics(sourceFile)) { + if (!ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { + diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.for_await_loops_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)); + } + switch (moduleKind) { + case ts.ModuleKind.Node16: + case ts.ModuleKind.NodeNext: + if (sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS) { + diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level)); break; - } + } + // fallthrough + case ts.ModuleKind.ES2022: + case ts.ModuleKind.ESNext: + case ts.ModuleKind.System: + if (languageVersion >= ts.ScriptTarget.ES2017) { + break; + } + // fallthrough + default: + diagnostics.add(ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.Top_level_for_await_loops_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_or_nodenext_and_the_target_option_is_set_to_es2017_or_higher)); + break; } } - else { - // use of 'for-await-of' in non-async function - if (!hasParseDiagnostics(sourceFile)) { - const diagnostic = ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.for_await_loops_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); - const func = ts.getContainingFunction(forInOrOfStatement); - if (func && func.kind !== ts.SyntaxKind.Constructor) { - ts.Debug.assert((ts.getFunctionFlags(func) & ts.FunctionFlags.Async) === 0, "Enclosing function should never be an async function."); - const relatedInfo = ts.createDiagnosticForNode(func, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async); - ts.addRelatedInfo(diagnostic, relatedInfo); - } - diagnostics.add(diagnostic); - return true; + } + else { + // use of 'for-await-of' in non-async function + if (!hasParseDiagnostics(sourceFile)) { + const diagnostic = ts.createDiagnosticForNode(forInOrOfStatement.awaitModifier, ts.Diagnostics.for_await_loops_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); + const func = ts.getContainingFunction(forInOrOfStatement); + if (func && func.kind !== ts.SyntaxKind.Constructor) { + ts.Debug.assert((ts.getFunctionFlags(func) & ts.FunctionFlags.Async) === 0, "Enclosing function should never be an async function."); + const relatedInfo = ts.createDiagnosticForNode(func, ts.Diagnostics.Did_you_mean_to_mark_this_function_as_async); + ts.addRelatedInfo(diagnostic, relatedInfo); } + diagnostics.add(diagnostic); + return true; } - return false; } - } - - if (ts.isForOfStatement(forInOrOfStatement) && !(forInOrOfStatement.flags & ts.NodeFlags.AwaitContext) && - ts.isIdentifier(forInOrOfStatement.initializer) && forInOrOfStatement.initializer.escapedText === "async") { - grammarErrorOnNode(forInOrOfStatement.initializer, ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_async); return false; } + } - if (forInOrOfStatement.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { - const variableList = forInOrOfStatement.initializer as ts.VariableDeclarationList; - 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 (ts.isForOfStatement(forInOrOfStatement) && !(forInOrOfStatement.flags & ts.NodeFlags.AwaitContext) && + ts.isIdentifier(forInOrOfStatement.initializer) && forInOrOfStatement.initializer.escapedText === "async") { + grammarErrorOnNode(forInOrOfStatement.initializer, ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_async); + return false; + } - if (declarations.length > 1) { - const diagnostic = forInOrOfStatement.kind === ts.SyntaxKind.ForInStatement - ? ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement - : ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement; - return grammarErrorOnFirstToken(variableList.declarations[1], diagnostic); - } - const firstDeclaration = declarations[0]; + if (forInOrOfStatement.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { + const variableList = forInOrOfStatement.initializer as ts.VariableDeclarationList; + if (!checkGrammarVariableDeclarationList(variableList)) { + const declarations = variableList.declarations; - if (firstDeclaration.initializer) { - const diagnostic = forInOrOfStatement.kind === ts.SyntaxKind.ForInStatement - ? ts.Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer - : ts.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 === ts.SyntaxKind.ForInStatement - ? ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_use_a_type_annotation - : ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_cannot_use_a_type_annotation; - return grammarErrorOnNode(firstDeclaration, diagnostic); - } + // 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; } - } - - return false; - } - function checkGrammarAccessor(accessor: ts.AccessorDeclaration): boolean { - if (!(accessor.flags & ts.NodeFlags.Ambient) && (accessor.parent.kind !== ts.SyntaxKind.TypeLiteral) && (accessor.parent.kind !== ts.SyntaxKind.InterfaceDeclaration)) { - if (languageVersion < ts.ScriptTarget.ES5) { - return grammarErrorOnNode(accessor.name, ts.Diagnostics.Accessors_are_only_available_when_targeting_ECMAScript_5_and_higher); + if (declarations.length > 1) { + const diagnostic = forInOrOfStatement.kind === ts.SyntaxKind.ForInStatement + ? ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement + : ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement; + return grammarErrorOnFirstToken(variableList.declarations[1], diagnostic); } - if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(accessor.name)) { - return grammarErrorOnNode(accessor.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); + const firstDeclaration = declarations[0]; + + if (firstDeclaration.initializer) { + const diagnostic = forInOrOfStatement.kind === ts.SyntaxKind.ForInStatement + ? ts.Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer + : ts.Diagnostics.The_variable_declaration_of_a_for_of_statement_cannot_have_an_initializer; + return grammarErrorOnNode(firstDeclaration.name, diagnostic); } - if (accessor.body === undefined && !ts.hasSyntacticModifier(accessor, ts.ModifierFlags.Abstract)) { - return grammarErrorAtPos(accessor, accessor.end - 1, ";".length, ts.Diagnostics._0_expected, "{"); + if (firstDeclaration.type) { + const diagnostic = forInOrOfStatement.kind === ts.SyntaxKind.ForInStatement + ? ts.Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_use_a_type_annotation + : ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_cannot_use_a_type_annotation; + return grammarErrorOnNode(firstDeclaration, diagnostic); } } - if (accessor.body) { - if (ts.hasSyntacticModifier(accessor, ts.ModifierFlags.Abstract)) { - return grammarErrorOnNode(accessor, ts.Diagnostics.An_abstract_accessor_cannot_have_an_implementation); - } - if (accessor.parent.kind === ts.SyntaxKind.TypeLiteral || accessor.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { - return grammarErrorOnNode(accessor.body, ts.Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); - } + } + + return false; + } + + function checkGrammarAccessor(accessor: ts.AccessorDeclaration): boolean { + if (!(accessor.flags & ts.NodeFlags.Ambient) && (accessor.parent.kind !== ts.SyntaxKind.TypeLiteral) && (accessor.parent.kind !== ts.SyntaxKind.InterfaceDeclaration)) { + if (languageVersion < ts.ScriptTarget.ES5) { + return grammarErrorOnNode(accessor.name, ts.Diagnostics.Accessors_are_only_available_when_targeting_ECMAScript_5_and_higher); } - if (accessor.typeParameters) { - return grammarErrorOnNode(accessor.name, ts.Diagnostics.An_accessor_cannot_have_type_parameters); + if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(accessor.name)) { + return grammarErrorOnNode(accessor.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); } - if (!doesAccessorHaveCorrectParameterCount(accessor)) { - return grammarErrorOnNode(accessor.name, accessor.kind === ts.SyntaxKind.GetAccessor ? - ts.Diagnostics.A_get_accessor_cannot_have_parameters : - ts.Diagnostics.A_set_accessor_must_have_exactly_one_parameter); + if (accessor.body === undefined && !ts.hasSyntacticModifier(accessor, ts.ModifierFlags.Abstract)) { + return grammarErrorAtPos(accessor, accessor.end - 1, ";".length, ts.Diagnostics._0_expected, "{"); } - if (accessor.kind === ts.SyntaxKind.SetAccessor) { - if (accessor.type) { - return grammarErrorOnNode(accessor.name, ts.Diagnostics.A_set_accessor_cannot_have_a_return_type_annotation); - } - const parameter = ts.Debug.checkDefined(ts.getSetAccessorValueParameter(accessor), "Return value does not match parameter count assertion."); - if (parameter.dotDotDotToken) { - return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.A_set_accessor_cannot_have_rest_parameter); - } - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.A_set_accessor_cannot_have_an_optional_parameter); - } - if (parameter.initializer) { - return grammarErrorOnNode(accessor.name, ts.Diagnostics.A_set_accessor_parameter_cannot_have_an_initializer); - } + } + if (accessor.body) { + if (ts.hasSyntacticModifier(accessor, ts.ModifierFlags.Abstract)) { + return grammarErrorOnNode(accessor, ts.Diagnostics.An_abstract_accessor_cannot_have_an_implementation); + } + if (accessor.parent.kind === ts.SyntaxKind.TypeLiteral || accessor.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { + return grammarErrorOnNode(accessor.body, ts.Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); } - 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: ts.AccessorDeclaration) { - return getAccessorThisParameter(accessor) || accessor.parameters.length === (accessor.kind === ts.SyntaxKind.GetAccessor ? 0 : 1); + if (accessor.typeParameters) { + return grammarErrorOnNode(accessor.name, ts.Diagnostics.An_accessor_cannot_have_type_parameters); } - - function getAccessorThisParameter(accessor: ts.AccessorDeclaration): ts.ParameterDeclaration | undefined { - if (accessor.parameters.length === (accessor.kind === ts.SyntaxKind.GetAccessor ? 1 : 2)) { - return ts.getThisParameter(accessor); + if (!doesAccessorHaveCorrectParameterCount(accessor)) { + return grammarErrorOnNode(accessor.name, accessor.kind === ts.SyntaxKind.GetAccessor ? + ts.Diagnostics.A_get_accessor_cannot_have_parameters : + ts.Diagnostics.A_set_accessor_must_have_exactly_one_parameter); + } + if (accessor.kind === ts.SyntaxKind.SetAccessor) { + if (accessor.type) { + return grammarErrorOnNode(accessor.name, ts.Diagnostics.A_set_accessor_cannot_have_a_return_type_annotation); + } + const parameter = ts.Debug.checkDefined(ts.getSetAccessorValueParameter(accessor), "Return value does not match parameter count assertion."); + if (parameter.dotDotDotToken) { + return grammarErrorOnNode(parameter.dotDotDotToken, ts.Diagnostics.A_set_accessor_cannot_have_rest_parameter); + } + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, ts.Diagnostics.A_set_accessor_cannot_have_an_optional_parameter); + } + if (parameter.initializer) { + return grammarErrorOnNode(accessor.name, ts.Diagnostics.A_set_accessor_parameter_cannot_have_an_initializer); } } + 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: ts.AccessorDeclaration) { + return getAccessorThisParameter(accessor) || accessor.parameters.length === (accessor.kind === ts.SyntaxKind.GetAccessor ? 0 : 1); + } + + function getAccessorThisParameter(accessor: ts.AccessorDeclaration): ts.ParameterDeclaration | undefined { + if (accessor.parameters.length === (accessor.kind === ts.SyntaxKind.GetAccessor ? 1 : 2)) { + return ts.getThisParameter(accessor); + } + } - function checkGrammarTypeOperatorNode(node: ts.TypeOperatorNode) { - if (node.operator === ts.SyntaxKind.UniqueKeyword) { - if (node.type.kind !== ts.SyntaxKind.SymbolKeyword) { - return grammarErrorOnNode(node.type, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SymbolKeyword)); + function checkGrammarTypeOperatorNode(node: ts.TypeOperatorNode) { + if (node.operator === ts.SyntaxKind.UniqueKeyword) { + if (node.type.kind !== ts.SyntaxKind.SymbolKeyword) { + return grammarErrorOnNode(node.type, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SymbolKeyword)); + } + let parent = ts.walkUpParenthesizedTypes(node.parent); + if (ts.isInJSFile(parent) && ts.isJSDocTypeExpression(parent)) { + const host = ts.getJSDocHost(parent); + if (host) { + parent = ts.getSingleVariableOfVariableStatement(host) || host; } - let parent = ts.walkUpParenthesizedTypes(node.parent); - if (ts.isInJSFile(parent) && ts.isJSDocTypeExpression(parent)) { - const host = ts.getJSDocHost(parent); - if (host) { - parent = ts.getSingleVariableOfVariableStatement(host) || host; + } + switch (parent.kind) { + case ts.SyntaxKind.VariableDeclaration: + const decl = parent as ts.VariableDeclaration; + if (decl.name.kind !== ts.SyntaxKind.Identifier) { + return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name); } - } - switch (parent.kind) { - case ts.SyntaxKind.VariableDeclaration: - const decl = parent as ts.VariableDeclaration; - if (decl.name.kind !== ts.SyntaxKind.Identifier) { - return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name); - } - if (!ts.isVariableDeclarationInVariableStatement(decl)) { - return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement); - } - if (!(decl.parent.flags & ts.NodeFlags.Const)) { - return grammarErrorOnNode((parent as ts.VariableDeclaration).name, ts.Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const); - } - break; + if (!ts.isVariableDeclarationInVariableStatement(decl)) { + return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement); + } + if (!(decl.parent.flags & ts.NodeFlags.Const)) { + return grammarErrorOnNode((parent as ts.VariableDeclaration).name, ts.Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const); + } + break; - case ts.SyntaxKind.PropertyDeclaration: - if (!ts.isStatic(parent) || - !ts.hasEffectiveReadonlyModifier(parent)) { - return grammarErrorOnNode((parent as ts.PropertyDeclaration).name, ts.Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly); - } - break; + case ts.SyntaxKind.PropertyDeclaration: + if (!ts.isStatic(parent) || + !ts.hasEffectiveReadonlyModifier(parent)) { + return grammarErrorOnNode((parent as ts.PropertyDeclaration).name, ts.Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly); + } + break; - case ts.SyntaxKind.PropertySignature: - if (!ts.hasSyntacticModifier(parent, ts.ModifierFlags.Readonly)) { - return grammarErrorOnNode((parent as ts.PropertySignature).name, ts.Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly); - } - break; + case ts.SyntaxKind.PropertySignature: + if (!ts.hasSyntacticModifier(parent, ts.ModifierFlags.Readonly)) { + return grammarErrorOnNode((parent as ts.PropertySignature).name, ts.Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly); + } + break; - default: - return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_are_not_allowed_here); - } + default: + return grammarErrorOnNode(node, ts.Diagnostics.unique_symbol_types_are_not_allowed_here); } - else if (node.operator === ts.SyntaxKind.ReadonlyKeyword) { - if (node.type.kind !== ts.SyntaxKind.ArrayType && node.type.kind !== ts.SyntaxKind.TupleType) { - return grammarErrorOnFirstToken(node, ts.Diagnostics.readonly_type_modifier_is_only_permitted_on_array_and_tuple_literal_types, ts.tokenToString(ts.SyntaxKind.SymbolKeyword)); - } + } + else if (node.operator === ts.SyntaxKind.ReadonlyKeyword) { + if (node.type.kind !== ts.SyntaxKind.ArrayType && node.type.kind !== ts.SyntaxKind.TupleType) { + return grammarErrorOnFirstToken(node, ts.Diagnostics.readonly_type_modifier_is_only_permitted_on_array_and_tuple_literal_types, ts.tokenToString(ts.SyntaxKind.SymbolKeyword)); } } + } - function checkGrammarForInvalidDynamicName(node: ts.DeclarationName, message: ts.DiagnosticMessage) { - if (isNonBindableDynamicName(node)) { - return grammarErrorOnNode(node, message); - } + function checkGrammarForInvalidDynamicName(node: ts.DeclarationName, message: ts.DiagnosticMessage) { + if (isNonBindableDynamicName(node)) { + return grammarErrorOnNode(node, message); } + } - function checkGrammarMethod(node: ts.MethodDeclaration | ts.MethodSignature) { - if (checkGrammarFunctionLikeDeclaration(node)) { - return true; - } + function checkGrammarMethod(node: ts.MethodDeclaration | ts.MethodSignature) { + if (checkGrammarFunctionLikeDeclaration(node)) { + return true; + } - if (node.kind === ts.SyntaxKind.MethodDeclaration) { - if (node.parent.kind === ts.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 && ts.first(node.modifiers).kind === ts.SyntaxKind.AsyncKeyword)) { - return grammarErrorOnFirstToken(node, ts.Diagnostics.Modifiers_cannot_appear_here); - } - else if (checkGrammarForInvalidQuestionMark(node.questionToken, ts.Diagnostics.An_object_member_cannot_be_declared_optional)) { - return true; - } - else if (checkGrammarForInvalidExclamationToken(node.exclamationToken, ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context)) { - return true; - } - else if (node.body === undefined) { - return grammarErrorAtPos(node, node.end - 1, ";".length, ts.Diagnostics._0_expected, "{"); - } + if (node.kind === ts.SyntaxKind.MethodDeclaration) { + if (node.parent.kind === ts.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 && ts.first(node.modifiers).kind === ts.SyntaxKind.AsyncKeyword)) { + return grammarErrorOnFirstToken(node, ts.Diagnostics.Modifiers_cannot_appear_here); } - if (checkGrammarForGenerator(node)) { + else if (checkGrammarForInvalidQuestionMark(node.questionToken, ts.Diagnostics.An_object_member_cannot_be_declared_optional)) { return true; } - } - - if (ts.isClassLike(node.parent)) { - if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(node.name)) { - return grammarErrorOnNode(node.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); - } - // 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 & ts.NodeFlags.Ambient) { - return checkGrammarForInvalidDynamicName(node.name, ts.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 (checkGrammarForInvalidExclamationToken(node.exclamationToken, ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context)) { + return true; } - else if (node.kind === ts.SyntaxKind.MethodDeclaration && !node.body) { - return checkGrammarForInvalidDynamicName(node.name, ts.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.body === undefined) { + return grammarErrorAtPos(node, node.end - 1, ";".length, ts.Diagnostics._0_expected, "{"); } } - else if (node.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { - return checkGrammarForInvalidDynamicName(node.name, ts.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 === ts.SyntaxKind.TypeLiteral) { - return checkGrammarForInvalidDynamicName(node.name, ts.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 (checkGrammarForGenerator(node)) { + return true; } } - function checkGrammarBreakOrContinueStatement(node: ts.BreakOrContinueStatement): boolean { - let current: ts.Node = node; - while (current) { - if (ts.isFunctionLikeOrClassStaticBlockDeclaration(current)) { - return grammarErrorOnNode(node, ts.Diagnostics.Jump_target_cannot_cross_function_boundary); - } + if (ts.isClassLike(node.parent)) { + if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(node.name)) { + return grammarErrorOnNode(node.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); + } + // 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 & ts.NodeFlags.Ambient) { + return checkGrammarForInvalidDynamicName(node.name, ts.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 === ts.SyntaxKind.MethodDeclaration && !node.body) { + return checkGrammarForInvalidDynamicName(node.name, ts.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 === ts.SyntaxKind.InterfaceDeclaration) { + return checkGrammarForInvalidDynamicName(node.name, ts.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 === ts.SyntaxKind.TypeLiteral) { + return checkGrammarForInvalidDynamicName(node.name, ts.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); + } + } - switch (current.kind) { - case ts.SyntaxKind.LabeledStatement: - if (node.label && (current as ts.LabeledStatement).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 === ts.SyntaxKind.ContinueStatement - && !ts.isIterationStatement((current as ts.LabeledStatement).statement, /*lookInLabeledStatement*/ true); + function checkGrammarBreakOrContinueStatement(node: ts.BreakOrContinueStatement): boolean { + let current: ts.Node = node; + while (current) { + if (ts.isFunctionLikeOrClassStaticBlockDeclaration(current)) { + return grammarErrorOnNode(node, ts.Diagnostics.Jump_target_cannot_cross_function_boundary); + } - if (isMisplacedContinueLabel) { - return grammarErrorOnNode(node, ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement); - } + switch (current.kind) { + case ts.SyntaxKind.LabeledStatement: + if (node.label && (current as ts.LabeledStatement).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 === ts.SyntaxKind.ContinueStatement + && !ts.isIterationStatement((current as ts.LabeledStatement).statement, /*lookInLabeledStatement*/ true); - return false; - } - break; - case ts.SyntaxKind.SwitchStatement: - if (node.kind === ts.SyntaxKind.BreakStatement && !node.label) { - // unlabeled break within switch statement - ok - return false; - } - break; - default: - if (ts.isIterationStatement(current, /*lookInLabeledStatement*/ false) && !node.label) { - // unlabeled break or continue within iteration statement - ok - return false; + if (isMisplacedContinueLabel) { + return grammarErrorOnNode(node, ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement); } - break; - } - current = current.parent; + return false; + } + break; + case ts.SyntaxKind.SwitchStatement: + if (node.kind === ts.SyntaxKind.BreakStatement && !node.label) { + // unlabeled break within switch statement - ok + return false; + } + break; + default: + if (ts.isIterationStatement(current, /*lookInLabeledStatement*/ false) && !node.label) { + // unlabeled break or continue within iteration statement - ok + return false; + } + break; } - if (node.label) { - const message = node.kind === ts.SyntaxKind.BreakStatement - ? ts.Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement - : ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement; - - return grammarErrorOnNode(node, message); - } - else { - const message = node.kind === ts.SyntaxKind.BreakStatement - ? ts.Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement - : ts.Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement; - return grammarErrorOnNode(node, message); - } + current = current.parent; } - function checkGrammarBindingElement(node: ts.BindingElement) { - if (node.dotDotDotToken) { - const elements = node.parent.elements; - if (node !== ts.last(elements)) { - return grammarErrorOnNode(node, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); - } - checkGrammarForDisallowedTrailingComma(elements, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + if (node.label) { + const message = node.kind === ts.SyntaxKind.BreakStatement + ? ts.Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement + : ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement; - if (node.propertyName) { - return grammarErrorOnNode(node.name, ts.Diagnostics.A_rest_element_cannot_have_a_property_name); - } + return grammarErrorOnNode(node, message); + } + else { + const message = node.kind === ts.SyntaxKind.BreakStatement + ? ts.Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement + : ts.Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement; + return grammarErrorOnNode(node, message); + } + } + + function checkGrammarBindingElement(node: ts.BindingElement) { + if (node.dotDotDotToken) { + const elements = node.parent.elements; + if (node !== ts.last(elements)) { + return grammarErrorOnNode(node, ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); } + checkGrammarForDisallowedTrailingComma(elements, ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - if (node.dotDotDotToken && node.initializer) { - // Error on equals token which immediately precedes the initializer - return grammarErrorAtPos(node, node.initializer.pos - 1, 1, ts.Diagnostics.A_rest_element_cannot_have_an_initializer); + if (node.propertyName) { + return grammarErrorOnNode(node.name, ts.Diagnostics.A_rest_element_cannot_have_a_property_name); } } - function isStringOrNumberLiteralExpression(expr: ts.Expression) { - return ts.isStringOrNumericLiteralLike(expr) || - expr.kind === ts.SyntaxKind.PrefixUnaryExpression && (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && - (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.NumericLiteral; + if (node.dotDotDotToken && node.initializer) { + // Error on equals token which immediately precedes the initializer + return grammarErrorAtPos(node, node.initializer.pos - 1, 1, ts.Diagnostics.A_rest_element_cannot_have_an_initializer); } + } - function isBigIntLiteralExpression(expr: ts.Expression) { - return expr.kind === ts.SyntaxKind.BigIntLiteral || - expr.kind === ts.SyntaxKind.PrefixUnaryExpression && (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && - (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.BigIntLiteral; - } + function isStringOrNumberLiteralExpression(expr: ts.Expression) { + return ts.isStringOrNumericLiteralLike(expr) || + expr.kind === ts.SyntaxKind.PrefixUnaryExpression && (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && + (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.NumericLiteral; + } - function isSimpleLiteralEnumReference(expr: ts.Expression) { - if ((ts.isPropertyAccessExpression(expr) || (ts.isElementAccessExpression(expr) && isStringOrNumberLiteralExpression(expr.argumentExpression))) && - ts.isEntityNameExpression(expr.expression)) { - return !!(checkExpressionCached(expr).flags & ts.TypeFlags.EnumLiteral); - } + function isBigIntLiteralExpression(expr: ts.Expression) { + return expr.kind === ts.SyntaxKind.BigIntLiteral || + expr.kind === ts.SyntaxKind.PrefixUnaryExpression && (expr as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken && + (expr as ts.PrefixUnaryExpression).operand.kind === ts.SyntaxKind.BigIntLiteral; + } + + function isSimpleLiteralEnumReference(expr: ts.Expression) { + if ((ts.isPropertyAccessExpression(expr) || (ts.isElementAccessExpression(expr) && isStringOrNumberLiteralExpression(expr.argumentExpression))) && + ts.isEntityNameExpression(expr.expression)) { + return !!(checkExpressionCached(expr).flags & ts.TypeFlags.EnumLiteral); } + } - function checkAmbientInitializer(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature) { - const {initializer} = node; - if (initializer) { - const isInvalidInitializer = !(isStringOrNumberLiteralExpression(initializer) || - isSimpleLiteralEnumReference(initializer) || - initializer.kind === ts.SyntaxKind.TrueKeyword || initializer.kind === ts.SyntaxKind.FalseKeyword || - isBigIntLiteralExpression(initializer)); - const isConstOrReadonly = ts.isDeclarationReadonly(node) || ts.isVariableDeclaration(node) && ts.isVarConst(node); - if (isConstOrReadonly && !node.type) { - if (isInvalidInitializer) { - return grammarErrorOnNode(initializer, ts.Diagnostics.A_const_initializer_in_an_ambient_context_must_be_a_string_or_numeric_literal_or_literal_enum_reference); - } - } - else { - return grammarErrorOnNode(initializer, ts.Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); - } - if (!isConstOrReadonly || isInvalidInitializer) { - return grammarErrorOnNode(initializer, ts.Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); + function checkAmbientInitializer(node: ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature) { + const {initializer} = node; + if (initializer) { + const isInvalidInitializer = !(isStringOrNumberLiteralExpression(initializer) || + isSimpleLiteralEnumReference(initializer) || + initializer.kind === ts.SyntaxKind.TrueKeyword || initializer.kind === ts.SyntaxKind.FalseKeyword || + isBigIntLiteralExpression(initializer)); + const isConstOrReadonly = ts.isDeclarationReadonly(node) || ts.isVariableDeclaration(node) && ts.isVarConst(node); + if (isConstOrReadonly && !node.type) { + if (isInvalidInitializer) { + return grammarErrorOnNode(initializer, ts.Diagnostics.A_const_initializer_in_an_ambient_context_must_be_a_string_or_numeric_literal_or_literal_enum_reference); } } + else { + return grammarErrorOnNode(initializer, ts.Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); + } + if (!isConstOrReadonly || isInvalidInitializer) { + return grammarErrorOnNode(initializer, ts.Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); + } } + } - function checkGrammarVariableDeclaration(node: ts.VariableDeclaration) { - if (node.parent.parent.kind !== ts.SyntaxKind.ForInStatement && node.parent.parent.kind !== ts.SyntaxKind.ForOfStatement) { - if (node.flags & ts.NodeFlags.Ambient) { - checkAmbientInitializer(node); + function checkGrammarVariableDeclaration(node: ts.VariableDeclaration) { + if (node.parent.parent.kind !== ts.SyntaxKind.ForInStatement && node.parent.parent.kind !== ts.SyntaxKind.ForOfStatement) { + if (node.flags & ts.NodeFlags.Ambient) { + checkAmbientInitializer(node); + } + else if (!node.initializer) { + if (ts.isBindingPattern(node.name) && !ts.isBindingPattern(node.parent)) { + return grammarErrorOnNode(node, ts.Diagnostics.A_destructuring_declaration_must_have_an_initializer); } - else if (!node.initializer) { - if (ts.isBindingPattern(node.name) && !ts.isBindingPattern(node.parent)) { - return grammarErrorOnNode(node, ts.Diagnostics.A_destructuring_declaration_must_have_an_initializer); - } - if (ts.isVarConst(node)) { - return grammarErrorOnNode(node, ts.Diagnostics.const_declarations_must_be_initialized); - } + if (ts.isVarConst(node)) { + return grammarErrorOnNode(node, ts.Diagnostics.const_declarations_must_be_initialized); } } + } - if (node.exclamationToken && (node.parent.parent.kind !== ts.SyntaxKind.VariableStatement || !node.type || node.initializer || node.flags & ts.NodeFlags.Ambient)) { - const message = node.initializer - ? ts.Diagnostics.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions - : !node.type - ? ts.Diagnostics.Declarations_with_definite_assignment_assertions_must_also_have_type_annotations - : ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context; - return grammarErrorOnNode(node.exclamationToken, message); - } + if (node.exclamationToken && (node.parent.parent.kind !== ts.SyntaxKind.VariableStatement || !node.type || node.initializer || node.flags & ts.NodeFlags.Ambient)) { + const message = node.initializer + ? ts.Diagnostics.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions + : !node.type + ? ts.Diagnostics.Declarations_with_definite_assignment_assertions_must_also_have_type_annotations + : ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context; + return grammarErrorOnNode(node.exclamationToken, message); + } - if ((moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && moduleKind !== ts.ModuleKind.System && - !(node.parent.parent.flags & ts.NodeFlags.Ambient) && ts.hasSyntacticModifier(node.parent.parent, ts.ModifierFlags.Export)) { - checkESModuleMarker(node.name); - } + if ((moduleKind < ts.ModuleKind.ES2015 || ts.getSourceFileOfNode(node).impliedNodeFormat === ts.ModuleKind.CommonJS) && moduleKind !== ts.ModuleKind.System && + !(node.parent.parent.flags & ts.NodeFlags.Ambient) && ts.hasSyntacticModifier(node.parent.parent, ts.ModifierFlags.Export)) { + checkESModuleMarker(node.name); + } - const checkLetConstNames = (ts.isLet(node) || ts.isVarConst(node)); + const checkLetConstNames = (ts.isLet(node) || ts.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". + // 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); - } + // 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: ts.Identifier | ts.BindingPattern): boolean { - if (name.kind === ts.SyntaxKind.Identifier) { - if (ts.idText(name) === "__esModule") { - return grammarErrorOnNodeSkippedOn("noEmit", name, ts.Diagnostics.Identifier_expected_esModule_is_reserved_as_an_exported_marker_when_transforming_ECMAScript_modules); - } + function checkESModuleMarker(name: ts.Identifier | ts.BindingPattern): boolean { + if (name.kind === ts.SyntaxKind.Identifier) { + if (ts.idText(name) === "__esModule") { + return grammarErrorOnNodeSkippedOn("noEmit", name, ts.Diagnostics.Identifier_expected_esModule_is_reserved_as_an_exported_marker_when_transforming_ECMAScript_modules); } - else { - const elements = name.elements; - for (const element of elements) { - if (!ts.isOmittedExpression(element)) { - return checkESModuleMarker(element.name); - } + } + else { + const elements = name.elements; + for (const element of elements) { + if (!ts.isOmittedExpression(element)) { + return checkESModuleMarker(element.name); } } - return false; } + return false; + } - function checkGrammarNameInLetOrConstDeclarations(name: ts.Identifier | ts.BindingPattern): boolean { - if (name.kind === ts.SyntaxKind.Identifier) { - if (name.originalKeywordKind === ts.SyntaxKind.LetKeyword) { - return grammarErrorOnNode(name, ts.Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations); - } + function checkGrammarNameInLetOrConstDeclarations(name: ts.Identifier | ts.BindingPattern): boolean { + if (name.kind === ts.SyntaxKind.Identifier) { + if (name.originalKeywordKind === ts.SyntaxKind.LetKeyword) { + return grammarErrorOnNode(name, ts.Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations); } - else { - const elements = name.elements; - for (const element of elements) { - if (!ts.isOmittedExpression(element)) { - checkGrammarNameInLetOrConstDeclarations(element.name); - } + } + else { + const elements = name.elements; + for (const element of elements) { + if (!ts.isOmittedExpression(element)) { + checkGrammarNameInLetOrConstDeclarations(element.name); } } - return false; } + return false; + } - function checkGrammarVariableDeclarationList(declarationList: ts.VariableDeclarationList): boolean { - const declarations = declarationList.declarations; - if (checkGrammarForDisallowedTrailingComma(declarationList.declarations)) { - return true; - } + function checkGrammarVariableDeclarationList(declarationList: ts.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, ts.Diagnostics.Variable_declaration_list_cannot_be_empty); - } - return false; + if (!declarationList.declarations.length) { + return grammarErrorAtPos(declarationList, declarations.pos, declarations.end - declarations.pos, ts.Diagnostics.Variable_declaration_list_cannot_be_empty); } + return false; + } - function allowLetAndConstDeclarations(parent: ts.Node): boolean { - switch (parent.kind) { - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.WithStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - return false; - case ts.SyntaxKind.LabeledStatement: - return allowLetAndConstDeclarations(parent.parent); - } + function allowLetAndConstDeclarations(parent: ts.Node): boolean { + switch (parent.kind) { + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.WithStatement: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + return false; + case ts.SyntaxKind.LabeledStatement: + return allowLetAndConstDeclarations(parent.parent); + } - return true; + return true; + } + + function checkGrammarForDisallowedLetOrConstStatement(node: ts.VariableStatement) { + if (!allowLetAndConstDeclarations(node.parent)) { + if (ts.isLet(node.declarationList)) { + return grammarErrorOnNode(node, ts.Diagnostics.let_declarations_can_only_be_declared_inside_a_block); + } + else if (ts.isVarConst(node.declarationList)) { + return grammarErrorOnNode(node, ts.Diagnostics.const_declarations_can_only_be_declared_inside_a_block); + } } + } - function checkGrammarForDisallowedLetOrConstStatement(node: ts.VariableStatement) { - if (!allowLetAndConstDeclarations(node.parent)) { - if (ts.isLet(node.declarationList)) { - return grammarErrorOnNode(node, ts.Diagnostics.let_declarations_can_only_be_declared_inside_a_block); + function checkGrammarMetaProperty(node: ts.MetaProperty) { + const escapedText = node.name.escapedText; + switch (node.keywordToken) { + case ts.SyntaxKind.NewKeyword: + if (escapedText !== "target") { + return grammarErrorOnNode(node.name, ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, ts.tokenToString(node.keywordToken), "target"); } - else if (ts.isVarConst(node.declarationList)) { - return grammarErrorOnNode(node, ts.Diagnostics.const_declarations_can_only_be_declared_inside_a_block); + break; + case ts.SyntaxKind.ImportKeyword: + if (escapedText !== "meta") { + return grammarErrorOnNode(node.name, ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, ts.tokenToString(node.keywordToken), "meta"); } - } + break; } + } - function checkGrammarMetaProperty(node: ts.MetaProperty) { - const escapedText = node.name.escapedText; - switch (node.keywordToken) { - case ts.SyntaxKind.NewKeyword: - if (escapedText !== "target") { - return grammarErrorOnNode(node.name, ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, ts.tokenToString(node.keywordToken), "target"); - } - break; - case ts.SyntaxKind.ImportKeyword: - if (escapedText !== "meta") { - return grammarErrorOnNode(node.name, ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, ts.tokenToString(node.keywordToken), "meta"); - } - break; - } - } + function hasParseDiagnostics(sourceFile: ts.SourceFile): boolean { + return sourceFile.parseDiagnostics.length > 0; + } - function hasParseDiagnostics(sourceFile: ts.SourceFile): boolean { - return sourceFile.parseDiagnostics.length > 0; + function grammarErrorOnFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); + diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2)); + return true; } + return false; + } - function grammarErrorOnFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(ts.createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2)); - return true; - } - return false; + function grammarErrorAtPos(nodeForSourceFile: ts.Node, start: number, length: number, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = ts.getSourceFileOfNode(nodeForSourceFile); + if (!hasParseDiagnostics(sourceFile)) { + diagnostics.add(ts.createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2)); + return true; } + return false; + } - function grammarErrorAtPos(nodeForSourceFile: ts.Node, start: number, length: number, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = ts.getSourceFileOfNode(nodeForSourceFile); - if (!hasParseDiagnostics(sourceFile)) { - diagnostics.add(ts.createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2)); - return true; - } - return false; + function grammarErrorOnNodeSkippedOn(key: keyof ts.CompilerOptions, node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + errorSkippedOn(key, node, message, arg0, arg1, arg2); + return true; } + return false; + } - function grammarErrorOnNodeSkippedOn(key: keyof ts.CompilerOptions, node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - errorSkippedOn(key, node, message, arg0, arg1, arg2); - return true; - } - return false; + function grammarErrorOnNode(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + diagnostics.add(ts.createDiagnosticForNode(node, message, arg0, arg1, arg2)); + return true; } + return false; + } - function grammarErrorOnNode(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - diagnostics.add(ts.createDiagnosticForNode(node, message, arg0, arg1, arg2)); - return true; - } - return false; + function checkGrammarConstructorTypeParameters(node: ts.ConstructorDeclaration) { + const jsdocTypeParameters = ts.isInJSFile(node) ? ts.getJSDocTypeParameterDeclarations(node) : undefined; + const range = node.typeParameters || jsdocTypeParameters && ts.firstOrUndefined(jsdocTypeParameters); + if (range) { + const pos = range.pos === range.end ? range.pos : ts.skipTrivia(ts.getSourceFileOfNode(node).text, range.pos); + return grammarErrorAtPos(node, pos, range.end - pos, ts.Diagnostics.Type_parameters_cannot_appear_on_a_constructor_declaration); } + } - function checkGrammarConstructorTypeParameters(node: ts.ConstructorDeclaration) { - const jsdocTypeParameters = ts.isInJSFile(node) ? ts.getJSDocTypeParameterDeclarations(node) : undefined; - const range = node.typeParameters || jsdocTypeParameters && ts.firstOrUndefined(jsdocTypeParameters); - if (range) { - const pos = range.pos === range.end ? range.pos : ts.skipTrivia(ts.getSourceFileOfNode(node).text, range.pos); - return grammarErrorAtPos(node, pos, range.end - pos, ts.Diagnostics.Type_parameters_cannot_appear_on_a_constructor_declaration); - } + function checkGrammarConstructorTypeAnnotation(node: ts.ConstructorDeclaration) { + const type = ts.getEffectiveReturnTypeNode(node); + if (type) { + return grammarErrorOnNode(type, ts.Diagnostics.Type_annotation_cannot_appear_on_a_constructor_declaration); } + } - function checkGrammarConstructorTypeAnnotation(node: ts.ConstructorDeclaration) { - const type = ts.getEffectiveReturnTypeNode(node); - if (type) { - return grammarErrorOnNode(type, ts.Diagnostics.Type_annotation_cannot_appear_on_a_constructor_declaration); - } + function checkGrammarProperty(node: ts.PropertyDeclaration | ts.PropertySignature) { + if (ts.isComputedPropertyName(node.name) && ts.isBinaryExpression(node.name.expression) && node.name.expression.operatorToken.kind === ts.SyntaxKind.InKeyword) { + return grammarErrorOnNode((node.parent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode).members[0], ts.Diagnostics.A_mapped_type_may_not_declare_properties_or_methods); } - - function checkGrammarProperty(node: ts.PropertyDeclaration | ts.PropertySignature) { - if (ts.isComputedPropertyName(node.name) && ts.isBinaryExpression(node.name.expression) && node.name.expression.operatorToken.kind === ts.SyntaxKind.InKeyword) { - return grammarErrorOnNode((node.parent as ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode).members[0], ts.Diagnostics.A_mapped_type_may_not_declare_properties_or_methods); - } - if (ts.isClassLike(node.parent)) { - if (ts.isStringLiteral(node.name) && node.name.text === "constructor") { - return grammarErrorOnNode(node.name, ts.Diagnostics.Classes_may_not_have_a_field_named_constructor); - } - if (checkGrammarForInvalidDynamicName(node.name, ts.Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_have_a_simple_literal_type_or_a_unique_symbol_type)) { - return true; - } - if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(node.name)) { - return grammarErrorOnNode(node.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); - } + if (ts.isClassLike(node.parent)) { + if (ts.isStringLiteral(node.name) && node.name.text === "constructor") { + return grammarErrorOnNode(node.name, ts.Diagnostics.Classes_may_not_have_a_field_named_constructor); } - else if (node.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { - if (checkGrammarForInvalidDynamicName(node.name, ts.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, ts.Diagnostics.An_interface_property_cannot_have_an_initializer); - } + if (checkGrammarForInvalidDynamicName(node.name, ts.Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_have_a_simple_literal_type_or_a_unique_symbol_type)) { + return true; } - else if (ts.isTypeLiteralNode(node.parent)) { - if (checkGrammarForInvalidDynamicName(node.name, ts.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, ts.Diagnostics.A_type_literal_property_cannot_have_an_initializer); - } + if (languageVersion < ts.ScriptTarget.ES2015 && ts.isPrivateIdentifier(node.name)) { + return grammarErrorOnNode(node.name, ts.Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); } - - if (node.flags & ts.NodeFlags.Ambient) { - checkAmbientInitializer(node); + } + else if (node.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { + if (checkGrammarForInvalidDynamicName(node.name, ts.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 (ts.isPropertyDeclaration(node) && node.exclamationToken && (!ts.isClassLike(node.parent) || !node.type || node.initializer || - node.flags & ts.NodeFlags.Ambient || ts.isStatic(node) || ts.hasAbstractModifier(node))) { - const message = node.initializer - ? ts.Diagnostics.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions - : !node.type - ? ts.Diagnostics.Declarations_with_definite_assignment_assertions_must_also_have_type_annotations - : ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context; - return grammarErrorOnNode(node.exclamationToken, message); + if (node.initializer) { + return grammarErrorOnNode(node.initializer, ts.Diagnostics.An_interface_property_cannot_have_an_initializer); } } - - function checkGrammarTopLevelElementForRequiredDeclareModifier(node: ts.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 === ts.SyntaxKind.InterfaceDeclaration || - node.kind === ts.SyntaxKind.TypeAliasDeclaration || - node.kind === ts.SyntaxKind.ImportDeclaration || - node.kind === ts.SyntaxKind.ImportEqualsDeclaration || - node.kind === ts.SyntaxKind.ExportDeclaration || - node.kind === ts.SyntaxKind.ExportAssignment || - node.kind === ts.SyntaxKind.NamespaceExportDeclaration || - ts.hasSyntacticModifier(node, ts.ModifierFlags.Ambient | ts.ModifierFlags.Export | ts.ModifierFlags.Default)) { - return false; + else if (ts.isTypeLiteralNode(node.parent)) { + if (checkGrammarForInvalidDynamicName(node.name, ts.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, ts.Diagnostics.A_type_literal_property_cannot_have_an_initializer); } + } - return grammarErrorOnFirstToken(node, ts.Diagnostics.Top_level_declarations_in_d_ts_files_must_start_with_either_a_declare_or_export_modifier); + if (node.flags & ts.NodeFlags.Ambient) { + checkAmbientInitializer(node); } - function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: ts.SourceFile): boolean { - for (const decl of file.statements) { - if (ts.isDeclaration(decl) || decl.kind === ts.SyntaxKind.VariableStatement) { - if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) { - return true; - } - } - } - return false; + if (ts.isPropertyDeclaration(node) && node.exclamationToken && (!ts.isClassLike(node.parent) || !node.type || node.initializer || + node.flags & ts.NodeFlags.Ambient || ts.isStatic(node) || ts.hasAbstractModifier(node))) { + const message = node.initializer + ? ts.Diagnostics.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions + : !node.type + ? ts.Diagnostics.Declarations_with_definite_assignment_assertions_must_also_have_type_annotations + : ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context; + return grammarErrorOnNode(node.exclamationToken, message); } + } - function checkGrammarSourceFile(node: ts.SourceFile): boolean { - return !!(node.flags & ts.NodeFlags.Ambient) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node); + function checkGrammarTopLevelElementForRequiredDeclareModifier(node: ts.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 === ts.SyntaxKind.InterfaceDeclaration || + node.kind === ts.SyntaxKind.TypeAliasDeclaration || + node.kind === ts.SyntaxKind.ImportDeclaration || + node.kind === ts.SyntaxKind.ImportEqualsDeclaration || + node.kind === ts.SyntaxKind.ExportDeclaration || + node.kind === ts.SyntaxKind.ExportAssignment || + node.kind === ts.SyntaxKind.NamespaceExportDeclaration || + ts.hasSyntacticModifier(node, ts.ModifierFlags.Ambient | ts.ModifierFlags.Export | ts.ModifierFlags.Default)) { + return false; } - function checkGrammarStatementInAmbientContext(node: ts.Node): boolean { - if (node.flags & ts.NodeFlags.Ambient) { - // Find containing block which is either Block, ModuleBlock, SourceFile - const links = getNodeLinks(node); - if (!links.hasReportedStatementInAmbientContext && (ts.isFunctionLike(node.parent) || ts.isAccessor(node.parent))) { - return getNodeLinks(node).hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, ts.Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); - } + return grammarErrorOnFirstToken(node, ts.Diagnostics.Top_level_declarations_in_d_ts_files_must_start_with_either_a_declare_or_export_modifier); + } - // 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 === ts.SyntaxKind.Block || node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { - const links = getNodeLinks(node.parent); - // Check if the containing block ever report this error - if (!links.hasReportedStatementInAmbientContext) { - return links.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, ts.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)); + function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: ts.SourceFile): boolean { + for (const decl of file.statements) { + if (ts.isDeclaration(decl) || decl.kind === ts.SyntaxKind.VariableStatement) { + if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) { + return true; } } - return false; } + return false; + } - function checkGrammarNumericLiteral(node: ts.NumericLiteral): boolean { - // Grammar checking - if (node.numericLiteralFlags & ts.TokenFlags.Octal) { - let diagnosticMessage: ts.DiagnosticMessage | undefined; - if (languageVersion >= ts.ScriptTarget.ES5) { - diagnosticMessage = ts.Diagnostics.Octal_literals_are_not_available_when_targeting_ECMAScript_5_and_higher_Use_the_syntax_0; - } - else if (ts.isChildOfNodeWithKind(node, ts.SyntaxKind.LiteralType)) { - diagnosticMessage = ts.Diagnostics.Octal_literal_types_must_use_ES2015_syntax_Use_the_syntax_0; - } - else if (ts.isChildOfNodeWithKind(node, ts.SyntaxKind.EnumMember)) { - diagnosticMessage = ts.Diagnostics.Octal_literals_are_not_allowed_in_enums_members_initializer_Use_the_syntax_0; - } - if (diagnosticMessage) { - const withMinus = ts.isPrefixUnaryExpression(node.parent) && node.parent.operator === ts.SyntaxKind.MinusToken; - const literal = (withMinus ? "-" : "") + "0o" + node.text; - return grammarErrorOnNode(withMinus ? node.parent : node, diagnosticMessage, literal); - } + function checkGrammarSourceFile(node: ts.SourceFile): boolean { + return !!(node.flags & ts.NodeFlags.Ambient) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node); + } + + function checkGrammarStatementInAmbientContext(node: ts.Node): boolean { + if (node.flags & ts.NodeFlags.Ambient) { + // Find containing block which is either Block, ModuleBlock, SourceFile + const links = getNodeLinks(node); + if (!links.hasReportedStatementInAmbientContext && (ts.isFunctionLike(node.parent) || ts.isAccessor(node.parent))) { + return getNodeLinks(node).hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, ts.Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); } - // Realism (size) checking - checkNumericLiteralValueSize(node); + // 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 === ts.SyntaxKind.Block || node.parent.kind === ts.SyntaxKind.ModuleBlock || node.parent.kind === ts.SyntaxKind.SourceFile) { + const links = getNodeLinks(node.parent); + // Check if the containing block ever report this error + if (!links.hasReportedStatementInAmbientContext) { + return links.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, ts.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; + } - return false; + function checkGrammarNumericLiteral(node: ts.NumericLiteral): boolean { + // Grammar checking + if (node.numericLiteralFlags & ts.TokenFlags.Octal) { + let diagnosticMessage: ts.DiagnosticMessage | undefined; + if (languageVersion >= ts.ScriptTarget.ES5) { + diagnosticMessage = ts.Diagnostics.Octal_literals_are_not_available_when_targeting_ECMAScript_5_and_higher_Use_the_syntax_0; + } + else if (ts.isChildOfNodeWithKind(node, ts.SyntaxKind.LiteralType)) { + diagnosticMessage = ts.Diagnostics.Octal_literal_types_must_use_ES2015_syntax_Use_the_syntax_0; + } + else if (ts.isChildOfNodeWithKind(node, ts.SyntaxKind.EnumMember)) { + diagnosticMessage = ts.Diagnostics.Octal_literals_are_not_allowed_in_enums_members_initializer_Use_the_syntax_0; + } + if (diagnosticMessage) { + const withMinus = ts.isPrefixUnaryExpression(node.parent) && node.parent.operator === ts.SyntaxKind.MinusToken; + const literal = (withMinus ? "-" : "") + "0o" + node.text; + return grammarErrorOnNode(withMinus ? node.parent : node, diagnosticMessage, literal); + } } - function checkNumericLiteralValueSize(node: ts.NumericLiteral) { - // We should test against `getTextOfNode(node)` rather than `node.text`, because `node.text` for large numeric literals can contain "." - // e.g. `node.text` for numeric literal `1100000000000000000000` is `1.1e21`. - const isFractional = ts.getTextOfNode(node).indexOf(".") !== -1; - const isScientific = node.numericLiteralFlags & ts.TokenFlags.Scientific; + // Realism (size) checking + checkNumericLiteralValueSize(node); - // Scientific notation (e.g. 2e54 and 1e00000000010) can't be converted to bigint - // Fractional numbers (e.g. 9000000000000000.001) are inherently imprecise anyway - if (isFractional || isScientific) { - return; - } + return false; + } - // Here `node` is guaranteed to be a numeric literal representing an integer. - // We need to judge whether the integer `node` represents is <= 2 ** 53 - 1, which can be accomplished by comparing to `value` defined below because: - // 1) when `node` represents an integer <= 2 ** 53 - 1, `node.text` is its exact string representation and thus `value` precisely represents the integer. - // 2) otherwise, although `node.text` may be imprecise string representation, its mathematical value and consequently `value` cannot be less than 2 ** 53, - // thus the result of the predicate won't be affected. - const value = +node.text; - if (value <= 2 ** 53 - 1) { - return; - } + function checkNumericLiteralValueSize(node: ts.NumericLiteral) { + // We should test against `getTextOfNode(node)` rather than `node.text`, because `node.text` for large numeric literals can contain "." + // e.g. `node.text` for numeric literal `1100000000000000000000` is `1.1e21`. + const isFractional = ts.getTextOfNode(node).indexOf(".") !== -1; + const isScientific = node.numericLiteralFlags & ts.TokenFlags.Scientific; - addErrorOrSuggestion(/*isError*/ false, ts.createDiagnosticForNode(node, ts.Diagnostics.Numeric_literals_with_absolute_values_equal_to_2_53_or_greater_are_too_large_to_be_represented_accurately_as_integers)); + // Scientific notation (e.g. 2e54 and 1e00000000010) can't be converted to bigint + // Fractional numbers (e.g. 9000000000000000.001) are inherently imprecise anyway + if (isFractional || isScientific) { + return; } - function checkGrammarBigIntLiteral(node: ts.BigIntLiteral): boolean { - const literalType = ts.isLiteralTypeNode(node.parent) || - ts.isPrefixUnaryExpression(node.parent) && ts.isLiteralTypeNode(node.parent.parent); - if (!literalType) { - if (languageVersion < ts.ScriptTarget.ES2020) { - if (grammarErrorOnNode(node, ts.Diagnostics.BigInt_literals_are_not_available_when_targeting_lower_than_ES2020)) { - return true; - } - } - } - return false; + // Here `node` is guaranteed to be a numeric literal representing an integer. + // We need to judge whether the integer `node` represents is <= 2 ** 53 - 1, which can be accomplished by comparing to `value` defined below because: + // 1) when `node` represents an integer <= 2 ** 53 - 1, `node.text` is its exact string representation and thus `value` precisely represents the integer. + // 2) otherwise, although `node.text` may be imprecise string representation, its mathematical value and consequently `value` cannot be less than 2 ** 53, + // thus the result of the predicate won't be affected. + const value = +node.text; + if (value <= 2 ** 53 - 1) { + return; } - function grammarErrorAfterFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = ts.getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(ts.createFileDiagnostic(sourceFile, ts.textSpanEnd(span), /*length*/ 0, message, arg0, arg1, arg2)); - return true; - } - return false; - } + addErrorOrSuggestion(/*isError*/ false, ts.createDiagnosticForNode(node, ts.Diagnostics.Numeric_literals_with_absolute_values_equal_to_2_53_or_greater_are_too_large_to_be_represented_accurately_as_integers)); + } - 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); - } - }); + function checkGrammarBigIntLiteral(node: ts.BigIntLiteral): boolean { + const literalType = ts.isLiteralTypeNode(node.parent) || + ts.isPrefixUnaryExpression(node.parent) && ts.isLiteralTypeNode(node.parent.parent); + if (!literalType) { + if (languageVersion < ts.ScriptTarget.ES2020) { + if (grammarErrorOnNode(node, ts.Diagnostics.BigInt_literals_are_not_available_when_targeting_lower_than_ES2020)) { + return true; + } } - return ambientModulesCache; } + return false; + } - function checkGrammarImportClause(node: ts.ImportClause): boolean { - if (node.isTypeOnly && node.name && node.namedBindings) { - return grammarErrorOnNode(node, ts.Diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both); - } - if (node.isTypeOnly && node.namedBindings?.kind === ts.SyntaxKind.NamedImports) { - return checkGrammarNamedImportsOrExports(node.namedBindings); - } - return false; + function grammarErrorAfterFirstToken(node: ts.Node, message: ts.DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = ts.getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = ts.getSpanOfTokenAtPosition(sourceFile, node.pos); + diagnostics.add(ts.createFileDiagnostic(sourceFile, ts.textSpanEnd(span), /*length*/ 0, message, arg0, arg1, arg2)); + return true; } + return false; + } - function checkGrammarNamedImportsOrExports(namedBindings: ts.NamedImportsOrExports): boolean { - return !!ts.forEach(namedBindings.elements, specifier => { - if (specifier.isTypeOnly) { - return grammarErrorOnFirstToken(specifier, specifier.kind === ts.SyntaxKind.ImportSpecifier - ? ts.Diagnostics.The_type_modifier_cannot_be_used_on_a_named_import_when_import_type_is_used_on_its_import_statement - : ts.Diagnostics.The_type_modifier_cannot_be_used_on_a_named_export_when_export_type_is_used_on_its_export_statement); + 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 ambientModulesCache; + } - function checkGrammarImportCallExpression(node: ts.ImportCall): boolean { - if (moduleKind === ts.ModuleKind.ES2015) { - return grammarErrorOnNode(node, ts.Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_es2022_esnext_commonjs_amd_system_umd_node16_or_nodenext); - } + function checkGrammarImportClause(node: ts.ImportClause): boolean { + if (node.isTypeOnly && node.name && node.namedBindings) { + return grammarErrorOnNode(node, ts.Diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both); + } + if (node.isTypeOnly && node.namedBindings?.kind === ts.SyntaxKind.NamedImports) { + return checkGrammarNamedImportsOrExports(node.namedBindings); + } + return false; + } - if (node.typeArguments) { - return grammarErrorOnNode(node, ts.Diagnostics.This_use_of_import_is_invalid_import_calls_can_be_written_but_they_must_have_parentheses_and_cannot_have_type_arguments); + function checkGrammarNamedImportsOrExports(namedBindings: ts.NamedImportsOrExports): boolean { + return !!ts.forEach(namedBindings.elements, specifier => { + if (specifier.isTypeOnly) { + return grammarErrorOnFirstToken(specifier, specifier.kind === ts.SyntaxKind.ImportSpecifier + ? ts.Diagnostics.The_type_modifier_cannot_be_used_on_a_named_import_when_import_type_is_used_on_its_import_statement + : ts.Diagnostics.The_type_modifier_cannot_be_used_on_a_named_export_when_export_type_is_used_on_its_export_statement); } + }); + } - const nodeArguments = node.arguments; - if (moduleKind !== ts.ModuleKind.ESNext && moduleKind !== ts.ModuleKind.NodeNext) { - // We are allowed trailing comma after proposal-import-assertions. - checkGrammarForDisallowedTrailingComma(nodeArguments); + function checkGrammarImportCallExpression(node: ts.ImportCall): boolean { + if (moduleKind === ts.ModuleKind.ES2015) { + return grammarErrorOnNode(node, ts.Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_es2022_esnext_commonjs_amd_system_umd_node16_or_nodenext); + } - if (nodeArguments.length > 1) { - const assertionArgument = nodeArguments[1]; - return grammarErrorOnNode(assertionArgument, ts.Diagnostics.Dynamic_imports_only_support_a_second_argument_when_the_module_option_is_set_to_esnext_node16_or_nodenext); - } - } + if (node.typeArguments) { + return grammarErrorOnNode(node, ts.Diagnostics.This_use_of_import_is_invalid_import_calls_can_be_written_but_they_must_have_parentheses_and_cannot_have_type_arguments); + } - if (nodeArguments.length === 0 || nodeArguments.length > 2) { - return grammarErrorOnNode(node, ts.Diagnostics.Dynamic_imports_can_only_accept_a_module_specifier_and_an_optional_assertion_as_arguments); - } + const nodeArguments = node.arguments; + if (moduleKind !== ts.ModuleKind.ESNext && moduleKind !== ts.ModuleKind.NodeNext) { + // We are allowed trailing comma after proposal-import-assertions. + 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. - const spreadElement = ts.find(nodeArguments, ts.isSpreadElement); - if (spreadElement) { - return grammarErrorOnNode(spreadElement, ts.Diagnostics.Argument_of_dynamic_import_cannot_be_spread_element); + if (nodeArguments.length > 1) { + const assertionArgument = nodeArguments[1]; + return grammarErrorOnNode(assertionArgument, ts.Diagnostics.Dynamic_imports_only_support_a_second_argument_when_the_module_option_is_set_to_esnext_node16_or_nodenext); } - return false; } - function findMatchingTypeReferenceOrTypeAliasReference(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { - const sourceObjectFlags = ts.getObjectFlags(source); - if (sourceObjectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.Anonymous) && unionTarget.flags & ts.TypeFlags.Union) { - return ts.find(unionTarget.types, target => { - if (target.flags & ts.TypeFlags.Object) { - const overlapObjFlags = sourceObjectFlags & ts.getObjectFlags(target); - if (overlapObjFlags & ts.ObjectFlags.Reference) { - return (source as ts.TypeReference).target === (target as ts.TypeReference).target; - } - if (overlapObjFlags & ts.ObjectFlags.Anonymous) { - return !!(source as ts.AnonymousType).aliasSymbol && (source as ts.AnonymousType).aliasSymbol === (target as ts.AnonymousType).aliasSymbol; - } + if (nodeArguments.length === 0 || nodeArguments.length > 2) { + return grammarErrorOnNode(node, ts.Diagnostics.Dynamic_imports_can_only_accept_a_module_specifier_and_an_optional_assertion_as_arguments); + } + + // 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. + const spreadElement = ts.find(nodeArguments, ts.isSpreadElement); + if (spreadElement) { + return grammarErrorOnNode(spreadElement, ts.Diagnostics.Argument_of_dynamic_import_cannot_be_spread_element); + } + return false; + } + + function findMatchingTypeReferenceOrTypeAliasReference(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { + const sourceObjectFlags = ts.getObjectFlags(source); + if (sourceObjectFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.Anonymous) && unionTarget.flags & ts.TypeFlags.Union) { + return ts.find(unionTarget.types, target => { + if (target.flags & ts.TypeFlags.Object) { + const overlapObjFlags = sourceObjectFlags & ts.getObjectFlags(target); + if (overlapObjFlags & ts.ObjectFlags.Reference) { + return (source as ts.TypeReference).target === (target as ts.TypeReference).target; } - return false; - }); - } + if (overlapObjFlags & ts.ObjectFlags.Anonymous) { + return !!(source as ts.AnonymousType).aliasSymbol && (source as ts.AnonymousType).aliasSymbol === (target as ts.AnonymousType).aliasSymbol; + } + } + return false; + }); } + } - function findBestTypeForObjectLiteral(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { - if (ts.getObjectFlags(source) & ts.ObjectFlags.ObjectLiteral && someType(unionTarget, isArrayLikeType)) { - return ts.find(unionTarget.types, t => !isArrayLikeType(t)); - } + function findBestTypeForObjectLiteral(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { + if (ts.getObjectFlags(source) & ts.ObjectFlags.ObjectLiteral && someType(unionTarget, isArrayLikeType)) { + return ts.find(unionTarget.types, t => !isArrayLikeType(t)); } + } - function findBestTypeForInvokable(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { - let signatureKind = ts.SignatureKind.Call; - const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 || - (signatureKind = ts.SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0); - if (hasSignatures) { - return ts.find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0); - } + function findBestTypeForInvokable(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { + let signatureKind = ts.SignatureKind.Call; + const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 || + (signatureKind = ts.SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0); + if (hasSignatures) { + return ts.find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0); } + } - function findMostOverlappyType(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { - let bestMatch: ts.Type | undefined; - if (!(source.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.InstantiablePrimitive))) { - let matchingCount = 0; - for (const target of unionTarget.types) { - if (!(target.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.InstantiablePrimitive))) { - const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]); - if (overlap.flags & ts.TypeFlags.Index) { - // perfect overlap of keys - return target; - } - else if (isUnitType(overlap) || overlap.flags & ts.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 = overlap.flags & ts.TypeFlags.Union ? ts.countWhere((overlap as ts.UnionType).types, isUnitType) : 1; - if (len >= matchingCount) { - bestMatch = target; - matchingCount = len; - } + function findMostOverlappyType(source: ts.Type, unionTarget: ts.UnionOrIntersectionType) { + let bestMatch: ts.Type | undefined; + if (!(source.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.InstantiablePrimitive))) { + let matchingCount = 0; + for (const target of unionTarget.types) { + if (!(target.flags & (ts.TypeFlags.Primitive | ts.TypeFlags.InstantiablePrimitive))) { + const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]); + if (overlap.flags & ts.TypeFlags.Index) { + // perfect overlap of keys + return target; + } + else if (isUnitType(overlap) || overlap.flags & ts.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 = overlap.flags & ts.TypeFlags.Union ? ts.countWhere((overlap as ts.UnionType).types, isUnitType) : 1; + if (len >= matchingCount) { + bestMatch = target; + matchingCount = len; } } } } - return bestMatch; } + return bestMatch; + } - function filterPrimitivesIfContainsNonPrimitive(type: ts.UnionType) { - if (maybeTypeOfKind(type, ts.TypeFlags.NonPrimitive)) { - const result = filterType(type, t => !(t.flags & ts.TypeFlags.Primitive)); - if (!(result.flags & ts.TypeFlags.Never)) { - return result; - } + function filterPrimitivesIfContainsNonPrimitive(type: ts.UnionType) { + if (maybeTypeOfKind(type, ts.TypeFlags.NonPrimitive)) { + const result = filterType(type, t => !(t.flags & ts.TypeFlags.Primitive)); + if (!(result.flags & ts.TypeFlags.Never)) { + return result; } - return type; } + return type; + } - // 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) => ts.Ternary, skipPartial?: boolean) { - if (target.flags & ts.TypeFlags.Union && source.flags & (ts.TypeFlags.Intersection | ts.TypeFlags.Object)) { - const match = getMatchingUnionConstituentForType(target as ts.UnionType, source); - if (match) { - return match; - } - const sourceProperties = getPropertiesOfType(source); - if (sourceProperties) { - const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); - if (sourcePropertiesFiltered) { - return discriminateTypeByDiscriminableItems(target as ts.UnionType, ts.map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [ - () => ts.Type, - ts.__String - ])), isRelatedTo, /*defaultValue*/ undefined, skipPartial); - } + // 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) => ts.Ternary, skipPartial?: boolean) { + if (target.flags & ts.TypeFlags.Union && source.flags & (ts.TypeFlags.Intersection | ts.TypeFlags.Object)) { + const match = getMatchingUnionConstituentForType(target as ts.UnionType, source); + if (match) { + return match; + } + const sourceProperties = getPropertiesOfType(source); + if (sourceProperties) { + const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); + if (sourcePropertiesFiltered) { + return discriminateTypeByDiscriminableItems(target as ts.UnionType, ts.map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [ + () => ts.Type, + ts.__String + ])), isRelatedTo, /*defaultValue*/ undefined, skipPartial); } } - return undefined; } + return undefined; } +} - function isNotAccessor(declaration: ts.Declaration): boolean { - // Accessors check for their own matching duplicates, and in contexts where they are valid, there are already duplicate identifier checks - return !ts.isAccessor(declaration); - } +function isNotAccessor(declaration: ts.Declaration): boolean { + // Accessors check for their own matching duplicates, and in contexts where they are valid, there are already duplicate identifier checks + return !ts.isAccessor(declaration); +} - function isNotOverload(declaration: ts.Declaration): boolean { - return (declaration.kind !== ts.SyntaxKind.FunctionDeclaration && declaration.kind !== ts.SyntaxKind.MethodDeclaration) || - !!(declaration as ts.FunctionDeclaration).body; - } +function isNotOverload(declaration: ts.Declaration): boolean { + return (declaration.kind !== ts.SyntaxKind.FunctionDeclaration && declaration.kind !== ts.SyntaxKind.MethodDeclaration) || + !!(declaration as ts.FunctionDeclaration).body; +} - /** Like 'isDeclarationName', but returns true for LHS of `import { x as y }` or `export { x as y }`. */ - function isDeclarationNameOrImportPropertyName(name: ts.Node): boolean { - switch (name.parent.kind) { - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - return ts.isIdentifier(name); - default: - return ts.isDeclarationName(name); - } +/** Like 'isDeclarationName', but returns true for LHS of `import { x as y }` or `export { x as y }`. */ +function isDeclarationNameOrImportPropertyName(name: ts.Node): boolean { + switch (name.parent.kind) { + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + return ts.isIdentifier(name); + default: + return ts.isDeclarationName(name); } +} - namespace JsxNames { - export const JSX = "JSX" as ts.__String; - export const IntrinsicElements = "IntrinsicElements" as ts.__String; - export const ElementClass = "ElementClass" as ts.__String; - export const ElementAttributesPropertyNameContainer = "ElementAttributesProperty" as ts.__String; // TODO: Deprecate and remove support - export const ElementChildrenAttributeNameContainer = "ElementChildrenAttribute" as ts.__String; - export const Element = "Element" as ts.__String; - export const IntrinsicAttributes = "IntrinsicAttributes" as ts.__String; - export const IntrinsicClassAttributes = "IntrinsicClassAttributes" as ts.__String; - export const LibraryManagedAttributes = "LibraryManagedAttributes" as ts.__String; - } +namespace JsxNames { + export const JSX = "JSX" as ts.__String; + export const IntrinsicElements = "IntrinsicElements" as ts.__String; + export const ElementClass = "ElementClass" as ts.__String; + export const ElementAttributesPropertyNameContainer = "ElementAttributesProperty" as ts.__String; // TODO: Deprecate and remove support + export const ElementChildrenAttributeNameContainer = "ElementChildrenAttribute" as ts.__String; + export const Element = "Element" as ts.__String; + export const IntrinsicAttributes = "IntrinsicAttributes" as ts.__String; + export const IntrinsicClassAttributes = "IntrinsicClassAttributes" as ts.__String; + export const LibraryManagedAttributes = "LibraryManagedAttributes" as ts.__String; +} - function getIterationTypesKeyFromIterationTypeKind(typeKind: IterationTypeKind) { - switch (typeKind) { - case IterationTypeKind.Yield: return "yieldType"; - case IterationTypeKind.Return: return "returnType"; - case IterationTypeKind.Next: return "nextType"; - } +function getIterationTypesKeyFromIterationTypeKind(typeKind: IterationTypeKind) { + switch (typeKind) { + case IterationTypeKind.Yield: return "yieldType"; + case IterationTypeKind.Return: return "returnType"; + case IterationTypeKind.Next: return "nextType"; } +} - export function signatureHasRestParameter(s: ts.Signature) { - return !!(s.flags & ts.SignatureFlags.HasRestParameter); - } +export function signatureHasRestParameter(s: ts.Signature) { + return !!(s.flags & ts.SignatureFlags.HasRestParameter); +} - export function signatureHasLiteralTypes(s: ts.Signature) { - return !!(s.flags & ts.SignatureFlags.HasLiteralTypes); - } +export function signatureHasLiteralTypes(s: ts.Signature) { + return !!(s.flags & ts.SignatureFlags.HasLiteralTypes); +} } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 8b513dfddea47..6447372c1baf6 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1,3529 +1,3529 @@ namespace ts { - /* @internal */ - export const compileOnSaveCommandLineOption: ts.CommandLineOption = { - name: "compileOnSave", +/* @internal */ +export const compileOnSaveCommandLineOption: ts.CommandLineOption = { + name: "compileOnSave", + type: "boolean", + defaultValueDescription: false, +}; + +const jsxOptionMap = new ts.Map(ts.getEntries({ + "preserve": ts.JsxEmit.Preserve, + "react-native": ts.JsxEmit.ReactNative, + "react": ts.JsxEmit.React, + "react-jsx": ts.JsxEmit.ReactJSX, + "react-jsxdev": ts.JsxEmit.ReactJSXDev, +})); + +/* @internal */ +export const inverseJsxOptionMap = new ts.Map(ts.arrayFrom(ts.mapIterator(jsxOptionMap.entries(), ([key, value]: [ + string, + ts.JsxEmit +]) => ["" + value, key] as const))); + +// 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"], + ["es2021", "lib.es2021.d.ts"], + ["es2022", "lib.es2022.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"], + ["webworker.iterable", "lib.webworker.iterable.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.date", "lib.es2020.date.d.ts"], + ["es2020.promise", "lib.es2020.promise.d.ts"], + ["es2020.sharedmemory", "lib.es2020.sharedmemory.d.ts"], + ["es2020.string", "lib.es2020.string.d.ts"], + ["es2020.symbol.wellknown", "lib.es2020.symbol.wellknown.d.ts"], + ["es2020.intl", "lib.es2020.intl.d.ts"], + ["es2020.number", "lib.es2020.number.d.ts"], + ["es2021.promise", "lib.es2021.promise.d.ts"], + ["es2021.string", "lib.es2021.string.d.ts"], + ["es2021.weakref", "lib.es2021.weakref.d.ts"], + ["es2021.intl", "lib.es2021.intl.d.ts"], + ["es2022.array", "lib.es2022.array.d.ts"], + ["es2022.error", "lib.es2022.error.d.ts"], + ["es2022.intl", "lib.es2022.intl.d.ts"], + ["es2022.object", "lib.es2022.object.d.ts"], + ["es2022.string", "lib.es2022.string.d.ts"], + ["esnext.array", "lib.es2022.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"], + ["esnext.string", "lib.es2022.string.d.ts"], + ["esnext.promise", "lib.es2021.promise.d.ts"], + ["esnext.weakref", "lib.es2021.weakref.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 = new ts.Map(libEntries); + +// Watch related options +/* @internal */ +export const optionsForWatch: ts.CommandLineOption[] = [ + { + name: "watchFile", + type: new ts.Map(ts.getEntries({ + fixedpollinginterval: ts.WatchFileKind.FixedPollingInterval, + prioritypollinginterval: ts.WatchFileKind.PriorityPollingInterval, + dynamicprioritypolling: ts.WatchFileKind.DynamicPriorityPolling, + fixedchunksizepolling: ts.WatchFileKind.FixedChunkSizePolling, + usefsevents: ts.WatchFileKind.UseFsEvents, + usefseventsonparentdirectory: ts.WatchFileKind.UseFsEventsOnParentDirectory, + })), + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Specify_how_the_TypeScript_watch_mode_works, + defaultValueDescription: ts.WatchFileKind.UseFsEvents, + }, + { + name: "watchDirectory", + type: new ts.Map(ts.getEntries({ + usefsevents: ts.WatchDirectoryKind.UseFsEvents, + fixedpollinginterval: ts.WatchDirectoryKind.FixedPollingInterval, + dynamicprioritypolling: ts.WatchDirectoryKind.DynamicPriorityPolling, + fixedchunksizepolling: ts.WatchDirectoryKind.FixedChunkSizePolling, + })), + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Specify_how_directories_are_watched_on_systems_that_lack_recursive_file_watching_functionality, + defaultValueDescription: ts.WatchDirectoryKind.UseFsEvents, + }, + { + name: "fallbackPolling", + type: new ts.Map(ts.getEntries({ + fixedinterval: ts.PollingWatchKind.FixedInterval, + priorityinterval: ts.PollingWatchKind.PriorityInterval, + dynamicpriority: ts.PollingWatchKind.DynamicPriority, + fixedchunksize: ts.PollingWatchKind.FixedChunkSize, + })), + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Specify_what_approach_the_watcher_should_use_if_the_system_runs_out_of_native_file_watchers, + defaultValueDescription: ts.PollingWatchKind.PriorityInterval, + }, + { + name: "synchronousWatchDirectory", type: "boolean", + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively, defaultValueDescription: false, - }; - - const jsxOptionMap = new ts.Map(ts.getEntries({ - "preserve": ts.JsxEmit.Preserve, - "react-native": ts.JsxEmit.ReactNative, - "react": ts.JsxEmit.React, - "react-jsx": ts.JsxEmit.ReactJSX, - "react-jsxdev": ts.JsxEmit.ReactJSXDev, - })); - - /* @internal */ - export const inverseJsxOptionMap = new ts.Map(ts.arrayFrom(ts.mapIterator(jsxOptionMap.entries(), ([key, value]: [ - string, - ts.JsxEmit - ]) => ["" + value, key] as const))); - - // 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"], - ["es2021", "lib.es2021.d.ts"], - ["es2022", "lib.es2022.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"], - ["webworker.iterable", "lib.webworker.iterable.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.date", "lib.es2020.date.d.ts"], - ["es2020.promise", "lib.es2020.promise.d.ts"], - ["es2020.sharedmemory", "lib.es2020.sharedmemory.d.ts"], - ["es2020.string", "lib.es2020.string.d.ts"], - ["es2020.symbol.wellknown", "lib.es2020.symbol.wellknown.d.ts"], - ["es2020.intl", "lib.es2020.intl.d.ts"], - ["es2020.number", "lib.es2020.number.d.ts"], - ["es2021.promise", "lib.es2021.promise.d.ts"], - ["es2021.string", "lib.es2021.string.d.ts"], - ["es2021.weakref", "lib.es2021.weakref.d.ts"], - ["es2021.intl", "lib.es2021.intl.d.ts"], - ["es2022.array", "lib.es2022.array.d.ts"], - ["es2022.error", "lib.es2022.error.d.ts"], - ["es2022.intl", "lib.es2022.intl.d.ts"], - ["es2022.object", "lib.es2022.object.d.ts"], - ["es2022.string", "lib.es2022.string.d.ts"], - ["esnext.array", "lib.es2022.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"], - ["esnext.string", "lib.es2022.string.d.ts"], - ["esnext.promise", "lib.es2021.promise.d.ts"], - ["esnext.weakref", "lib.es2021.weakref.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 = new ts.Map(libEntries); - - // Watch related options - /* @internal */ - export const optionsForWatch: ts.CommandLineOption[] = [ - { - name: "watchFile", - type: new ts.Map(ts.getEntries({ - fixedpollinginterval: ts.WatchFileKind.FixedPollingInterval, - prioritypollinginterval: ts.WatchFileKind.PriorityPollingInterval, - dynamicprioritypolling: ts.WatchFileKind.DynamicPriorityPolling, - fixedchunksizepolling: ts.WatchFileKind.FixedChunkSizePolling, - usefsevents: ts.WatchFileKind.UseFsEvents, - usefseventsonparentdirectory: ts.WatchFileKind.UseFsEventsOnParentDirectory, - })), - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Specify_how_the_TypeScript_watch_mode_works, - defaultValueDescription: ts.WatchFileKind.UseFsEvents, - }, - { - name: "watchDirectory", - type: new ts.Map(ts.getEntries({ - usefsevents: ts.WatchDirectoryKind.UseFsEvents, - fixedpollinginterval: ts.WatchDirectoryKind.FixedPollingInterval, - dynamicprioritypolling: ts.WatchDirectoryKind.DynamicPriorityPolling, - fixedchunksizepolling: ts.WatchDirectoryKind.FixedChunkSizePolling, - })), - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Specify_how_directories_are_watched_on_systems_that_lack_recursive_file_watching_functionality, - defaultValueDescription: ts.WatchDirectoryKind.UseFsEvents, - }, - { - name: "fallbackPolling", - type: new ts.Map(ts.getEntries({ - fixedinterval: ts.PollingWatchKind.FixedInterval, - priorityinterval: ts.PollingWatchKind.PriorityInterval, - dynamicpriority: ts.PollingWatchKind.DynamicPriority, - fixedchunksize: ts.PollingWatchKind.FixedChunkSize, - })), - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Specify_what_approach_the_watcher_should_use_if_the_system_runs_out_of_native_file_watchers, - defaultValueDescription: ts.PollingWatchKind.PriorityInterval, - }, - { - name: "synchronousWatchDirectory", - type: "boolean", - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively, - defaultValueDescription: false, - }, - { - name: "excludeDirectories", - type: "list", - element: { - name: "excludeDirectory", - type: "string", - isFilePath: true, - extraValidation: specToDiagnostic - }, - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Remove_a_list_of_directories_from_the_watch_process, - }, - { - name: "excludeFiles", - type: "list", - element: { - name: "excludeFile", - type: "string", - isFilePath: true, - extraValidation: specToDiagnostic - }, - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Remove_a_list_of_files_from_the_watch_mode_s_processing, - }, - ]; - - /* @internal */ - export const commonOptionsWithBuild: ts.CommandLineOption[] = [ - { - name: "help", - shortName: "h", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Print_this_message, - defaultValueDescription: false, - }, - { - name: "help", - shortName: "?", - type: "boolean", - defaultValueDescription: false, - }, - { - name: "watch", - shortName: "w", - type: "boolean", - showInSimplifiedHelpView: true, - isCommandLineOnly: true, - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Watch_input_files, - defaultValueDescription: false, - }, - { - name: "preserveWatchOutput", - type: "boolean", - showInSimplifiedHelpView: false, - category: ts.Diagnostics.Output_Formatting, - description: ts.Diagnostics.Disable_wiping_the_console_in_watch_mode, - defaultValueDescription: false, - }, - { - name: "listFiles", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Print_all_of_the_files_read_during_the_compilation, - defaultValueDescription: false, - }, - { - name: "explainFiles", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Print_files_read_during_the_compilation_including_why_it_was_included, - defaultValueDescription: false, - }, - { - name: "listEmittedFiles", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Print_the_names_of_emitted_files_after_a_compilation, - defaultValueDescription: false, - }, - { - name: "pretty", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Output_Formatting, - description: ts.Diagnostics.Enable_color_and_formatting_in_TypeScript_s_output_to_make_compiler_errors_easier_to_read, - defaultValueDescription: true, - }, - { - name: "traceResolution", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Log_paths_used_during_the_moduleResolution_process, - defaultValueDescription: false, - }, - { - name: "diagnostics", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Output_compiler_performance_information_after_building, - defaultValueDescription: false, - }, - { - name: "extendedDiagnostics", - type: "boolean", - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Output_more_detailed_compiler_performance_information_after_building, - defaultValueDescription: false, - }, - { - name: "generateCpuProfile", + }, + { + name: "excludeDirectories", + type: "list", + element: { + name: "excludeDirectory", type: "string", isFilePath: true, - paramType: ts.Diagnostics.FILE_OR_DIRECTORY, - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Emit_a_v8_CPU_profile_of_the_compiler_run_for_debugging, - defaultValueDescription: "profile.cpuprofile" - }, - { - name: "generateTrace", + extraValidation: specToDiagnostic + }, + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Remove_a_list_of_directories_from_the_watch_process, + }, + { + name: "excludeFiles", + type: "list", + element: { + name: "excludeFile", type: "string", isFilePath: true, - isCommandLineOnly: true, - paramType: ts.Diagnostics.DIRECTORY, - category: ts.Diagnostics.Compiler_Diagnostics, - description: ts.Diagnostics.Generates_an_event_trace_and_a_list_of_types - }, - { - name: "incremental", - shortName: "i", - type: "boolean", - category: ts.Diagnostics.Projects, - description: ts.Diagnostics.Save_tsbuildinfo_files_to_allow_for_incremental_compilation_of_projects, - transpileOptionValue: undefined, - defaultValueDescription: ts.Diagnostics.false_unless_composite_is_set - }, - { - name: "assumeChangesOnlyAffectDirectDependencies", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: ts.Diagnostics.Watch_and_Build_Modes, - description: ts.Diagnostics.Have_recompiles_in_projects_that_use_incremental_and_watch_mode_assume_that_changes_within_a_file_will_only_affect_files_directly_depending_on_it, - defaultValueDescription: false, - }, - { - name: "locale", - type: "string", - category: ts.Diagnostics.Command_line_Options, - isCommandLineOnly: true, - description: ts.Diagnostics.Set_the_language_of_the_messaging_from_TypeScript_This_does_not_affect_emit, - defaultValueDescription: ts.Diagnostics.Platform_specific - }, - ]; + extraValidation: specToDiagnostic + }, + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Remove_a_list_of_files_from_the_watch_mode_s_processing, + }, +]; + +/* @internal */ +export const commonOptionsWithBuild: ts.CommandLineOption[] = [ + { + name: "help", + shortName: "h", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Print_this_message, + defaultValueDescription: false, + }, + { + name: "help", + shortName: "?", + type: "boolean", + defaultValueDescription: false, + }, + { + name: "watch", + shortName: "w", + type: "boolean", + showInSimplifiedHelpView: true, + isCommandLineOnly: true, + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Watch_input_files, + defaultValueDescription: false, + }, + { + name: "preserveWatchOutput", + type: "boolean", + showInSimplifiedHelpView: false, + category: ts.Diagnostics.Output_Formatting, + description: ts.Diagnostics.Disable_wiping_the_console_in_watch_mode, + defaultValueDescription: false, + }, + { + name: "listFiles", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Print_all_of_the_files_read_during_the_compilation, + defaultValueDescription: false, + }, + { + name: "explainFiles", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Print_files_read_during_the_compilation_including_why_it_was_included, + defaultValueDescription: false, + }, + { + name: "listEmittedFiles", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Print_the_names_of_emitted_files_after_a_compilation, + defaultValueDescription: false, + }, + { + name: "pretty", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Output_Formatting, + description: ts.Diagnostics.Enable_color_and_formatting_in_TypeScript_s_output_to_make_compiler_errors_easier_to_read, + defaultValueDescription: true, + }, + { + name: "traceResolution", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Log_paths_used_during_the_moduleResolution_process, + defaultValueDescription: false, + }, + { + name: "diagnostics", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Output_compiler_performance_information_after_building, + defaultValueDescription: false, + }, + { + name: "extendedDiagnostics", + type: "boolean", + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Output_more_detailed_compiler_performance_information_after_building, + defaultValueDescription: false, + }, + { + name: "generateCpuProfile", + type: "string", + isFilePath: true, + paramType: ts.Diagnostics.FILE_OR_DIRECTORY, + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Emit_a_v8_CPU_profile_of_the_compiler_run_for_debugging, + defaultValueDescription: "profile.cpuprofile" + }, + { + name: "generateTrace", + type: "string", + isFilePath: true, + isCommandLineOnly: true, + paramType: ts.Diagnostics.DIRECTORY, + category: ts.Diagnostics.Compiler_Diagnostics, + description: ts.Diagnostics.Generates_an_event_trace_and_a_list_of_types + }, + { + name: "incremental", + shortName: "i", + type: "boolean", + category: ts.Diagnostics.Projects, + description: ts.Diagnostics.Save_tsbuildinfo_files_to_allow_for_incremental_compilation_of_projects, + transpileOptionValue: undefined, + defaultValueDescription: ts.Diagnostics.false_unless_composite_is_set + }, + { + name: "assumeChangesOnlyAffectDirectDependencies", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: ts.Diagnostics.Watch_and_Build_Modes, + description: ts.Diagnostics.Have_recompiles_in_projects_that_use_incremental_and_watch_mode_assume_that_changes_within_a_file_will_only_affect_files_directly_depending_on_it, + defaultValueDescription: false, + }, + { + name: "locale", + type: "string", + category: ts.Diagnostics.Command_line_Options, + isCommandLineOnly: true, + description: ts.Diagnostics.Set_the_language_of_the_messaging_from_TypeScript_This_does_not_affect_emit, + defaultValueDescription: ts.Diagnostics.Platform_specific + }, +]; + +/* @internal */ +export const targetOptionDeclaration: ts.CommandLineOptionOfCustomType = { + name: "target", + shortName: "t", + type: new ts.Map(ts.getEntries({ + es3: ts.ScriptTarget.ES3, + es5: ts.ScriptTarget.ES5, + es6: ts.ScriptTarget.ES2015, + es2015: ts.ScriptTarget.ES2015, + es2016: ts.ScriptTarget.ES2016, + es2017: ts.ScriptTarget.ES2017, + es2018: ts.ScriptTarget.ES2018, + es2019: ts.ScriptTarget.ES2019, + es2020: ts.ScriptTarget.ES2020, + es2021: ts.ScriptTarget.ES2021, + es2022: ts.ScriptTarget.ES2022, + esnext: ts.ScriptTarget.ESNext, + })), + affectsSourceFile: true, + affectsModuleResolution: true, + affectsEmit: true, + paramType: ts.Diagnostics.VERSION, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Set_the_JavaScript_language_version_for_emitted_JavaScript_and_include_compatible_library_declarations, + defaultValueDescription: ts.ScriptTarget.ES3, +}; + +const commandOptionsWithoutBuild: ts.CommandLineOption[] = [ + // CommandLine only options + { + name: "all", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Show_all_compiler_options, + defaultValueDescription: false, + }, + { + name: "version", + shortName: "v", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Print_the_compiler_s_version, + defaultValueDescription: false, + }, + { + name: "init", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Initializes_a_TypeScript_project_and_creates_a_tsconfig_json_file, + defaultValueDescription: false, + }, + { + name: "project", + shortName: "p", + type: "string", + isFilePath: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + paramType: ts.Diagnostics.FILE_OR_DIRECTORY, + description: ts.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: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date, + defaultValueDescription: false, + }, + { + name: "showConfig", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Command_line_Options, + isCommandLineOnly: true, + description: ts.Diagnostics.Print_the_final_configuration_instead_of_building, + defaultValueDescription: false, + }, + { + name: "listFilesOnly", + type: "boolean", + category: ts.Diagnostics.Command_line_Options, + affectsSemanticDiagnostics: true, + affectsEmit: true, + isCommandLineOnly: true, + description: ts.Diagnostics.Print_names_of_files_that_are_part_of_the_compilation_and_then_stop_processing, + defaultValueDescription: false, + }, - /* @internal */ - export const targetOptionDeclaration: ts.CommandLineOptionOfCustomType = { - name: "target", - shortName: "t", + // Basic + targetOptionDeclaration, + { + name: "module", + shortName: "m", type: new ts.Map(ts.getEntries({ - es3: ts.ScriptTarget.ES3, - es5: ts.ScriptTarget.ES5, - es6: ts.ScriptTarget.ES2015, - es2015: ts.ScriptTarget.ES2015, - es2016: ts.ScriptTarget.ES2016, - es2017: ts.ScriptTarget.ES2017, - es2018: ts.ScriptTarget.ES2018, - es2019: ts.ScriptTarget.ES2019, - es2020: ts.ScriptTarget.ES2020, - es2021: ts.ScriptTarget.ES2021, - es2022: ts.ScriptTarget.ES2022, - esnext: ts.ScriptTarget.ESNext, + none: ts.ModuleKind.None, + commonjs: ts.ModuleKind.CommonJS, + amd: ts.ModuleKind.AMD, + system: ts.ModuleKind.System, + umd: ts.ModuleKind.UMD, + es6: ts.ModuleKind.ES2015, + es2015: ts.ModuleKind.ES2015, + es2020: ts.ModuleKind.ES2020, + es2022: ts.ModuleKind.ES2022, + esnext: ts.ModuleKind.ESNext, + node16: ts.ModuleKind.Node16, + nodenext: ts.ModuleKind.NodeNext, })), - affectsSourceFile: true, affectsModuleResolution: true, affectsEmit: true, - paramType: ts.Diagnostics.VERSION, + paramType: ts.Diagnostics.KIND, showInSimplifiedHelpView: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Set_the_JavaScript_language_version_for_emitted_JavaScript_and_include_compatible_library_declarations, - defaultValueDescription: ts.ScriptTarget.ES3, - }; - - const commandOptionsWithoutBuild: ts.CommandLineOption[] = [ - // CommandLine only options - { - name: "all", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Show_all_compiler_options, - defaultValueDescription: false, - }, - { - name: "version", - shortName: "v", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Print_the_compiler_s_version, - defaultValueDescription: false, - }, - { - name: "init", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Initializes_a_TypeScript_project_and_creates_a_tsconfig_json_file, - defaultValueDescription: false, - }, - { - name: "project", - shortName: "p", - type: "string", - isFilePath: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - paramType: ts.Diagnostics.FILE_OR_DIRECTORY, - description: ts.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: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date, - defaultValueDescription: false, - }, - { - name: "showConfig", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Command_line_Options, - isCommandLineOnly: true, - description: ts.Diagnostics.Print_the_final_configuration_instead_of_building, - defaultValueDescription: false, - }, - { - name: "listFilesOnly", - type: "boolean", - category: ts.Diagnostics.Command_line_Options, - affectsSemanticDiagnostics: true, - affectsEmit: true, - isCommandLineOnly: true, - description: ts.Diagnostics.Print_names_of_files_that_are_part_of_the_compilation_and_then_stop_processing, - defaultValueDescription: false, - }, - - // Basic - targetOptionDeclaration, - { - name: "module", - shortName: "m", - type: new ts.Map(ts.getEntries({ - none: ts.ModuleKind.None, - commonjs: ts.ModuleKind.CommonJS, - amd: ts.ModuleKind.AMD, - system: ts.ModuleKind.System, - umd: ts.ModuleKind.UMD, - es6: ts.ModuleKind.ES2015, - es2015: ts.ModuleKind.ES2015, - es2020: ts.ModuleKind.ES2020, - es2022: ts.ModuleKind.ES2022, - esnext: ts.ModuleKind.ESNext, - node16: ts.ModuleKind.Node16, - nodenext: ts.ModuleKind.NodeNext, - })), - affectsModuleResolution: true, - affectsEmit: true, - paramType: ts.Diagnostics.KIND, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_what_module_code_is_generated, - defaultValueDescription: undefined, - }, - { + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_what_module_code_is_generated, + defaultValueDescription: undefined, + }, + { + name: "lib", + type: "list", + element: { name: "lib", - type: "list", - element: { - name: "lib", - type: libMap, - defaultValueDescription: undefined, - }, - affectsProgramStructure: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_a_set_of_bundled_library_declaration_files_that_describe_the_target_runtime_environment, - transpileOptionValue: undefined - }, - { - name: "allowJs", - type: "boolean", - affectsModuleResolution: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.JavaScript_Support, - description: ts.Diagnostics.Allow_JavaScript_files_to_be_a_part_of_your_program_Use_the_checkJS_option_to_get_errors_from_these_files, - defaultValueDescription: false, - }, - { - name: "checkJs", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.JavaScript_Support, - description: ts.Diagnostics.Enable_error_reporting_in_type_checked_JavaScript_files, - defaultValueDescription: false, - }, - { - name: "jsx", - type: jsxOptionMap, - affectsSourceFile: true, - affectsEmit: true, - affectsModuleResolution: true, - paramType: ts.Diagnostics.KIND, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_what_JSX_code_is_generated, + type: libMap, defaultValueDescription: undefined, }, - { - name: "declaration", - shortName: "d", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - transpileOptionValue: undefined, - description: ts.Diagnostics.Generate_d_ts_files_from_TypeScript_and_JavaScript_files_in_your_project, - defaultValueDescription: ts.Diagnostics.false_unless_composite_is_set, - }, - { - name: "declarationMap", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - transpileOptionValue: undefined, - defaultValueDescription: false, - description: ts.Diagnostics.Create_sourcemaps_for_d_ts_files - }, - { - name: "emitDeclarationOnly", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Only_output_d_ts_files_and_not_JavaScript_files, - transpileOptionValue: undefined, - defaultValueDescription: false, - }, - { - name: "sourceMap", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - defaultValueDescription: false, - description: ts.Diagnostics.Create_source_map_files_for_emitted_JavaScript_files, - }, - { - name: "outFile", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: ts.Diagnostics.FILE, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Specify_a_file_that_bundles_all_outputs_into_one_JavaScript_file_If_declaration_is_true_also_designates_a_file_that_bundles_all_d_ts_output, - transpileOptionValue: undefined, - }, - { - name: "outDir", + affectsProgramStructure: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_a_set_of_bundled_library_declaration_files_that_describe_the_target_runtime_environment, + transpileOptionValue: undefined + }, + { + name: "allowJs", + type: "boolean", + affectsModuleResolution: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.JavaScript_Support, + description: ts.Diagnostics.Allow_JavaScript_files_to_be_a_part_of_your_program_Use_the_checkJS_option_to_get_errors_from_these_files, + defaultValueDescription: false, + }, + { + name: "checkJs", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.JavaScript_Support, + description: ts.Diagnostics.Enable_error_reporting_in_type_checked_JavaScript_files, + defaultValueDescription: false, + }, + { + name: "jsx", + type: jsxOptionMap, + affectsSourceFile: true, + affectsEmit: true, + affectsModuleResolution: true, + paramType: ts.Diagnostics.KIND, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_what_JSX_code_is_generated, + defaultValueDescription: undefined, + }, + { + name: "declaration", + shortName: "d", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + transpileOptionValue: undefined, + description: ts.Diagnostics.Generate_d_ts_files_from_TypeScript_and_JavaScript_files_in_your_project, + defaultValueDescription: ts.Diagnostics.false_unless_composite_is_set, + }, + { + name: "declarationMap", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + transpileOptionValue: undefined, + defaultValueDescription: false, + description: ts.Diagnostics.Create_sourcemaps_for_d_ts_files + }, + { + name: "emitDeclarationOnly", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Only_output_d_ts_files_and_not_JavaScript_files, + transpileOptionValue: undefined, + defaultValueDescription: false, + }, + { + name: "sourceMap", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + defaultValueDescription: false, + description: ts.Diagnostics.Create_source_map_files_for_emitted_JavaScript_files, + }, + { + name: "outFile", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: ts.Diagnostics.FILE, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Specify_a_file_that_bundles_all_outputs_into_one_JavaScript_file_If_declaration_is_true_also_designates_a_file_that_bundles_all_d_ts_output, + transpileOptionValue: undefined, + }, + { + name: "outDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: ts.Diagnostics.DIRECTORY, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Specify_an_output_folder_for_all_emitted_files, + }, + { + name: "rootDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: ts.Diagnostics.LOCATION, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_the_root_folder_within_your_source_files, + defaultValueDescription: ts.Diagnostics.Computed_from_the_list_of_input_files + }, + { + name: "composite", + type: "boolean", + affectsEmit: true, + isTSConfigOnly: true, + category: ts.Diagnostics.Projects, + transpileOptionValue: undefined, + defaultValueDescription: false, + description: ts.Diagnostics.Enable_constraints_that_allow_a_TypeScript_project_to_be_used_with_project_references, + }, + { + name: "tsBuildInfoFile", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: ts.Diagnostics.FILE, + category: ts.Diagnostics.Projects, + transpileOptionValue: undefined, + defaultValueDescription: ".tsbuildinfo", + description: ts.Diagnostics.Specify_the_path_to_tsbuildinfo_incremental_compilation_file, + }, + { + name: "removeComments", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + defaultValueDescription: false, + description: ts.Diagnostics.Disable_emitting_comments, + }, + { + name: "noEmit", + type: "boolean", + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Disable_emitting_files_from_a_compilation, + transpileOptionValue: undefined, + defaultValueDescription: false, + }, + { + name: "importHelpers", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Allow_importing_helper_functions_from_tslib_once_per_project_instead_of_including_them_per_file, + defaultValueDescription: false, + }, + { + name: "importsNotUsedAsValues", + type: new ts.Map(ts.getEntries({ + remove: ts.ImportsNotUsedAsValues.Remove, + preserve: ts.ImportsNotUsedAsValues.Preserve, + error: ts.ImportsNotUsedAsValues.Error, + })), + affectsEmit: true, + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Specify_emit_Slashchecking_behavior_for_imports_that_are_only_used_for_types, + defaultValueDescription: ts.ImportsNotUsedAsValues.Remove, + }, + { + name: "downlevelIteration", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Emit_more_compliant_but_verbose_and_less_performant_JavaScript_for_iteration, + defaultValueDescription: false, + }, + { + name: "isolatedModules", + type: "boolean", + category: ts.Diagnostics.Interop_Constraints, + description: ts.Diagnostics.Ensure_that_each_file_can_be_safely_transpiled_without_relying_on_other_imports, + transpileOptionValue: true, + defaultValueDescription: false, + }, + + // Strict Type Checks + { + name: "strict", + type: "boolean", + // Though this affects semantic diagnostics, affectsSemanticDiagnostics is not set here + // The value of each strictFlag depends on own strictFlag value or this and never accessed directly. + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_all_strict_type_checking_options, + defaultValueDescription: false, + }, + { + name: "noImplicitAny", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_error_reporting_for_expressions_and_declarations_with_an_implied_any_type, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "strictNullChecks", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.When_type_checking_take_into_account_null_and_undefined, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "strictFunctionTypes", + type: "boolean", + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.When_assigning_functions_check_to_ensure_parameters_and_the_return_values_are_subtype_compatible, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "strictBindCallApply", + type: "boolean", + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Check_that_the_arguments_for_bind_call_and_apply_methods_match_the_original_function, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "strictPropertyInitialization", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Check_for_class_properties_that_are_declared_but_not_set_in_the_constructor, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "noImplicitThis", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_error_reporting_when_this_is_given_the_type_any, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + { + name: "useUnknownInCatchVariables", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Default_catch_clause_variables_as_unknown_instead_of_any, + defaultValueDescription: false, + }, + { + name: "alwaysStrict", + type: "boolean", + affectsSourceFile: true, + strictFlag: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Ensure_use_strict_is_always_emitted, + defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set + }, + + // Additional Checks + { + name: "noUnusedLocals", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_error_reporting_when_local_variables_aren_t_read, + defaultValueDescription: false, + }, + { + name: "noUnusedParameters", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Raise_an_error_when_a_function_parameter_isn_t_read, + defaultValueDescription: false, + }, + { + name: "exactOptionalPropertyTypes", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Interpret_optional_property_types_as_written_rather_than_adding_undefined, + defaultValueDescription: false, + }, + { + name: "noImplicitReturns", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_error_reporting_for_codepaths_that_do_not_explicitly_return_in_a_function, + defaultValueDescription: false, + }, + { + name: "noFallthroughCasesInSwitch", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enable_error_reporting_for_fallthrough_cases_in_switch_statements, + defaultValueDescription: false, + }, + { + name: "noUncheckedIndexedAccess", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Add_undefined_to_a_type_when_accessed_using_an_index, + defaultValueDescription: false, + }, + { + name: "noImplicitOverride", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Ensure_overriding_members_in_derived_classes_are_marked_with_an_override_modifier, + defaultValueDescription: false, + }, + { + name: "noPropertyAccessFromIndexSignature", + type: "boolean", + showInSimplifiedHelpView: false, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Enforces_using_indexed_accessors_for_keys_declared_using_an_indexed_type, + defaultValueDescription: false, + }, + + // Module Resolution + { + name: "moduleResolution", + type: new ts.Map(ts.getEntries({ + node: ts.ModuleResolutionKind.NodeJs, + classic: ts.ModuleResolutionKind.Classic, + node16: ts.ModuleResolutionKind.Node16, + nodenext: ts.ModuleResolutionKind.NodeNext, + })), + affectsModuleResolution: true, + paramType: ts.Diagnostics.STRATEGY, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_how_TypeScript_looks_up_a_file_from_a_given_module_specifier, + defaultValueDescription: ts.Diagnostics.module_AMD_or_UMD_or_System_or_ES6_then_Classic_Otherwise_Node + }, + { + name: "baseUrl", + type: "string", + affectsModuleResolution: true, + isFilePath: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_the_base_directory_to_resolve_non_relative_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: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_a_set_of_entries_that_re_map_imports_to_additional_lookup_locations, + 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: "string", - affectsEmit: true, - isFilePath: true, - paramType: ts.Diagnostics.DIRECTORY, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Specify_an_output_folder_for_all_emitted_files, + isFilePath: true }, - { - name: "rootDir", + affectsModuleResolution: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Allow_multiple_folders_to_be_treated_as_one_when_resolving_modules, + transpileOptionValue: undefined, + defaultValueDescription: ts.Diagnostics.Computed_from_the_list_of_input_files + }, + { + name: "typeRoots", + type: "list", + element: { + name: "typeRoots", type: "string", - affectsEmit: true, - isFilePath: true, - paramType: ts.Diagnostics.LOCATION, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_the_root_folder_within_your_source_files, - defaultValueDescription: ts.Diagnostics.Computed_from_the_list_of_input_files + isFilePath: true }, - { - name: "composite", - type: "boolean", - affectsEmit: true, - isTSConfigOnly: true, - category: ts.Diagnostics.Projects, - transpileOptionValue: undefined, - defaultValueDescription: false, - description: ts.Diagnostics.Enable_constraints_that_allow_a_TypeScript_project_to_be_used_with_project_references, + affectsModuleResolution: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_multiple_folders_that_act_like_Slashnode_modules_Slash_types + }, + { + name: "types", + type: "list", + element: { + name: "types", + type: "string" }, - { - name: "tsBuildInfoFile", + affectsProgramStructure: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Specify_type_package_names_to_be_included_without_being_referenced_in_a_source_file, + transpileOptionValue: undefined + }, + { + name: "allowSyntheticDefaultImports", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Interop_Constraints, + description: ts.Diagnostics.Allow_import_x_from_y_when_a_module_doesn_t_have_a_default_export, + defaultValueDescription: ts.Diagnostics.module_system_or_esModuleInterop + }, + { + name: "esModuleInterop", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + showInSimplifiedHelpView: true, + category: ts.Diagnostics.Interop_Constraints, + description: ts.Diagnostics.Emit_additional_JavaScript_to_ease_support_for_importing_CommonJS_modules_This_enables_allowSyntheticDefaultImports_for_type_compatibility, + defaultValueDescription: false, + }, + { + name: "preserveSymlinks", + type: "boolean", + category: ts.Diagnostics.Interop_Constraints, + description: ts.Diagnostics.Disable_resolving_symlinks_to_their_realpath_This_correlates_to_the_same_flag_in_node, + defaultValueDescription: false, + }, + { + name: "allowUmdGlobalAccess", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Allow_accessing_UMD_globals_from_modules, + defaultValueDescription: false, + }, + { + name: "moduleSuffixes", + type: "list", + element: { + name: "suffix", type: "string", - affectsEmit: true, - isFilePath: true, - paramType: ts.Diagnostics.FILE, - category: ts.Diagnostics.Projects, - transpileOptionValue: undefined, - defaultValueDescription: ".tsbuildinfo", - description: ts.Diagnostics.Specify_the_path_to_tsbuildinfo_incremental_compilation_file, - }, - { - name: "removeComments", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - defaultValueDescription: false, - description: ts.Diagnostics.Disable_emitting_comments, - }, - { - name: "noEmit", - type: "boolean", - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Disable_emitting_files_from_a_compilation, - transpileOptionValue: undefined, - defaultValueDescription: false, - }, - { - name: "importHelpers", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Allow_importing_helper_functions_from_tslib_once_per_project_instead_of_including_them_per_file, - defaultValueDescription: false, - }, - { - name: "importsNotUsedAsValues", - type: new ts.Map(ts.getEntries({ - remove: ts.ImportsNotUsedAsValues.Remove, - preserve: ts.ImportsNotUsedAsValues.Preserve, - error: ts.ImportsNotUsedAsValues.Error, - })), - affectsEmit: true, - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Specify_emit_Slashchecking_behavior_for_imports_that_are_only_used_for_types, - defaultValueDescription: ts.ImportsNotUsedAsValues.Remove, - }, - { - name: "downlevelIteration", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Emit_more_compliant_but_verbose_and_less_performant_JavaScript_for_iteration, - defaultValueDescription: false, - }, - { - name: "isolatedModules", - type: "boolean", - category: ts.Diagnostics.Interop_Constraints, - description: ts.Diagnostics.Ensure_that_each_file_can_be_safely_transpiled_without_relying_on_other_imports, - transpileOptionValue: true, - defaultValueDescription: false, }, + listPreserveFalsyValues: true, + affectsModuleResolution: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module, + }, + + // Source Maps + { + name: "sourceRoot", + type: "string", + affectsEmit: true, + paramType: ts.Diagnostics.LOCATION, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Specify_the_root_path_for_debuggers_to_find_the_reference_source_code, + }, + { + name: "mapRoot", + type: "string", + affectsEmit: true, + paramType: ts.Diagnostics.LOCATION, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Specify_the_location_where_debugger_should_locate_map_files_instead_of_generated_locations, + }, + { + name: "inlineSourceMap", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Include_sourcemap_files_inside_the_emitted_JavaScript, + defaultValueDescription: false, + }, + { + name: "inlineSources", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Include_source_code_in_the_sourcemaps_inside_the_emitted_JavaScript, + defaultValueDescription: false, + }, + + // Experimental + { + name: "experimentalDecorators", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Enable_experimental_support_for_TC39_stage_2_draft_decorators, + defaultValueDescription: false, + }, + { + name: "emitDecoratorMetadata", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Emit_design_type_metadata_for_decorated_declarations_in_source_files, + defaultValueDescription: false, + }, + + // Advanced + { + name: "jsxFactory", + type: "string", + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_the_JSX_factory_function_used_when_targeting_React_JSX_emit_e_g_React_createElement_or_h, + defaultValueDescription: "`React.createElement`" + }, + { + name: "jsxFragmentFactory", + type: "string", + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_the_JSX_Fragment_reference_used_for_fragments_when_targeting_React_JSX_emit_e_g_React_Fragment_or_Fragment, + defaultValueDescription: "React.Fragment", + }, + { + name: "jsxImportSource", + type: "string", + affectsSemanticDiagnostics: true, + affectsEmit: true, + affectsModuleResolution: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_module_specifier_used_to_import_the_JSX_factory_functions_when_using_jsx_Colon_react_jsx_Asterisk, + defaultValueDescription: "react" + }, + { + name: "resolveJsonModule", + type: "boolean", + affectsModuleResolution: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Enable_importing_json_files, + defaultValueDescription: false, + }, + + { + name: "out", + type: "string", + affectsEmit: true, + isFilePath: false, + // for correct behaviour, please use outFile + category: ts.Diagnostics.Backwards_Compatibility, + paramType: ts.Diagnostics.FILE, + transpileOptionValue: undefined, + description: ts.Diagnostics.Deprecated_setting_Use_outFile_instead, + }, + { + name: "reactNamespace", + type: "string", + affectsEmit: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Specify_the_object_invoked_for_createElement_This_only_applies_when_targeting_react_JSX_emit, + defaultValueDescription: "`React`", + }, + { + name: "skipDefaultLibCheck", + type: "boolean", + category: ts.Diagnostics.Completeness, + description: ts.Diagnostics.Skip_type_checking_d_ts_files_that_are_included_with_TypeScript, + defaultValueDescription: false, + }, + { + name: "charset", + type: "string", + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.No_longer_supported_In_early_versions_manually_set_the_text_encoding_for_reading_files, + defaultValueDescription: "utf8" + }, + { + name: "emitBOM", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Emit_a_UTF_8_Byte_Order_Mark_BOM_in_the_beginning_of_output_files, + defaultValueDescription: false, + }, + { + name: "newLine", + type: new ts.Map(ts.getEntries({ + crlf: ts.NewLineKind.CarriageReturnLineFeed, + lf: ts.NewLineKind.LineFeed + })), + affectsEmit: true, + paramType: ts.Diagnostics.NEWLINE, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Set_the_newline_character_for_emitting_files, + defaultValueDescription: ts.Diagnostics.Platform_specific + }, + { + name: "noErrorTruncation", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Output_Formatting, + description: ts.Diagnostics.Disable_truncating_types_in_error_messages, + defaultValueDescription: false, + }, + { + name: "noLib", + type: "boolean", + category: ts.Diagnostics.Language_and_Environment, + affectsProgramStructure: true, + description: ts.Diagnostics.Disable_including_any_library_files_including_the_default_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, + defaultValueDescription: false, + }, + { + name: "noResolve", + type: "boolean", + affectsModuleResolution: true, + category: ts.Diagnostics.Modules, + description: ts.Diagnostics.Disallow_import_s_require_s_or_reference_s_from_expanding_the_number_of_files_TypeScript_should_add_to_a_project, + // 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, + defaultValueDescription: false, + }, + { + name: "stripInternal", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Disable_emitting_declarations_that_have_internal_in_their_JSDoc_comments, + defaultValueDescription: false, + }, + { + name: "disableSizeLimit", + type: "boolean", + affectsProgramStructure: true, + category: ts.Diagnostics.Editor_Support, + description: ts.Diagnostics.Remove_the_20mb_cap_on_total_source_code_size_for_JavaScript_files_in_the_TypeScript_language_server, + defaultValueDescription: false, + }, + { + name: "disableSourceOfProjectReferenceRedirect", + type: "boolean", + isTSConfigOnly: true, + category: ts.Diagnostics.Projects, + description: ts.Diagnostics.Disable_preferring_source_files_instead_of_declaration_files_when_referencing_composite_projects, + defaultValueDescription: false, + }, + { + name: "disableSolutionSearching", + type: "boolean", + isTSConfigOnly: true, + category: ts.Diagnostics.Projects, + description: ts.Diagnostics.Opt_a_project_out_of_multi_project_reference_checking_when_editing, + defaultValueDescription: false, + }, + { + name: "disableReferencedProjectLoad", + type: "boolean", + isTSConfigOnly: true, + category: ts.Diagnostics.Projects, + description: ts.Diagnostics.Reduce_the_number_of_projects_loaded_automatically_by_TypeScript, + defaultValueDescription: false, + }, + { + name: "noImplicitUseStrict", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Disable_adding_use_strict_directives_in_emitted_JavaScript_files, + defaultValueDescription: false, + }, + { + name: "noEmitHelpers", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Disable_generating_custom_helper_functions_like_extends_in_compiled_output, + defaultValueDescription: false, + }, + { + name: "noEmitOnError", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + transpileOptionValue: undefined, + description: ts.Diagnostics.Disable_emitting_files_if_any_type_checking_errors_are_reported, + defaultValueDescription: false, + }, + { + name: "preserveConstEnums", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Disable_erasing_const_enum_declarations_in_generated_code, + defaultValueDescription: false, + }, + { + name: "declarationDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: ts.Diagnostics.DIRECTORY, + category: ts.Diagnostics.Emit, + transpileOptionValue: undefined, + description: ts.Diagnostics.Specify_the_output_directory_for_generated_declaration_files, + }, + { + name: "skipLibCheck", + type: "boolean", + category: ts.Diagnostics.Completeness, + description: ts.Diagnostics.Skip_type_checking_all_d_ts_files, + defaultValueDescription: false, + }, + { + name: "allowUnusedLabels", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Disable_error_reporting_for_unused_labels, + defaultValueDescription: undefined, + }, + { + name: "allowUnreachableCode", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Type_Checking, + description: ts.Diagnostics.Disable_error_reporting_for_unreachable_code, + defaultValueDescription: undefined, + }, + { + name: "suppressExcessPropertyErrors", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Disable_reporting_of_excess_property_errors_during_the_creation_of_object_literals, + defaultValueDescription: false, + }, + { + name: "suppressImplicitAnyIndexErrors", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Suppress_noImplicitAny_errors_when_indexing_objects_that_lack_index_signatures, + defaultValueDescription: false, + }, + { + name: "forceConsistentCasingInFileNames", + type: "boolean", + affectsModuleResolution: true, + category: ts.Diagnostics.Interop_Constraints, + description: ts.Diagnostics.Ensure_that_casing_is_correct_in_imports, + defaultValueDescription: false, + }, + { + name: "maxNodeModuleJsDepth", + type: "number", + affectsModuleResolution: true, + category: ts.Diagnostics.JavaScript_Support, + description: ts.Diagnostics.Specify_the_maximum_folder_depth_used_for_checking_JavaScript_files_from_node_modules_Only_applicable_with_allowJs, + defaultValueDescription: 0, + }, + { + name: "noStrictGenericChecks", + type: "boolean", + affectsSemanticDiagnostics: true, + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Disable_strict_checking_of_generic_signatures_in_function_types, + defaultValueDescription: false, + }, + { + name: "useDefineForClassFields", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: ts.Diagnostics.Language_and_Environment, + description: ts.Diagnostics.Emit_ECMAScript_standard_compliant_class_fields, + defaultValueDescription: ts.Diagnostics.true_for_ES2022_and_above_including_ESNext + }, + { + name: "preserveValueImports", + type: "boolean", + affectsEmit: true, + category: ts.Diagnostics.Emit, + description: ts.Diagnostics.Preserve_unused_imported_values_in_the_JavaScript_output_that_would_otherwise_be_removed, + defaultValueDescription: false, + }, - // Strict Type Checks - { - name: "strict", - type: "boolean", - // Though this affects semantic diagnostics, affectsSemanticDiagnostics is not set here - // The value of each strictFlag depends on own strictFlag value or this and never accessed directly. - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_all_strict_type_checking_options, - defaultValueDescription: false, - }, - { - name: "noImplicitAny", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_error_reporting_for_expressions_and_declarations_with_an_implied_any_type, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "strictNullChecks", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.When_type_checking_take_into_account_null_and_undefined, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "strictFunctionTypes", - type: "boolean", - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.When_assigning_functions_check_to_ensure_parameters_and_the_return_values_are_subtype_compatible, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "strictBindCallApply", - type: "boolean", - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Check_that_the_arguments_for_bind_call_and_apply_methods_match_the_original_function, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "strictPropertyInitialization", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Check_for_class_properties_that_are_declared_but_not_set_in_the_constructor, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "noImplicitThis", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_error_reporting_when_this_is_given_the_type_any, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, - { - name: "useUnknownInCatchVariables", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Default_catch_clause_variables_as_unknown_instead_of_any, - defaultValueDescription: false, - }, - { - name: "alwaysStrict", - type: "boolean", - affectsSourceFile: true, - strictFlag: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Ensure_use_strict_is_always_emitted, - defaultValueDescription: ts.Diagnostics.false_unless_strict_is_set - }, + { + name: "keyofStringsOnly", + type: "boolean", + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Make_keyof_only_return_strings_instead_of_string_numbers_or_symbols_Legacy_option, + defaultValueDescription: false, + }, + { + // A list of plugins to load in the language service + name: "plugins", + type: "list", + isTSConfigOnly: true, + element: { + name: "plugin", + type: "object" + }, + description: ts.Diagnostics.Specify_a_list_of_language_service_plugins_to_include, + category: ts.Diagnostics.Editor_Support, + + }, + { + name: "moduleDetection", + type: new ts.Map(ts.getEntries({ + auto: ts.ModuleDetectionKind.Auto, + legacy: ts.ModuleDetectionKind.Legacy, + force: ts.ModuleDetectionKind.Force, + })), + affectsModuleResolution: true, + description: ts.Diagnostics.Control_what_method_is_used_to_detect_module_format_JS_files, + category: ts.Diagnostics.Language_and_Environment, + defaultValueDescription: ts.Diagnostics.auto_Colon_Treat_files_with_imports_exports_import_meta_jsx_with_jsx_Colon_react_jsx_or_esm_format_with_module_Colon_node16_as_modules, + } +]; - // Additional Checks - { - name: "noUnusedLocals", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_error_reporting_when_local_variables_aren_t_read, - defaultValueDescription: false, - }, - { - name: "noUnusedParameters", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Raise_an_error_when_a_function_parameter_isn_t_read, - defaultValueDescription: false, - }, - { - name: "exactOptionalPropertyTypes", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Interpret_optional_property_types_as_written_rather_than_adding_undefined, - defaultValueDescription: false, - }, - { - name: "noImplicitReturns", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_error_reporting_for_codepaths_that_do_not_explicitly_return_in_a_function, - defaultValueDescription: false, - }, - { - name: "noFallthroughCasesInSwitch", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enable_error_reporting_for_fallthrough_cases_in_switch_statements, - defaultValueDescription: false, - }, - { - name: "noUncheckedIndexedAccess", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Add_undefined_to_a_type_when_accessed_using_an_index, - defaultValueDescription: false, - }, - { - name: "noImplicitOverride", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Ensure_overriding_members_in_derived_classes_are_marked_with_an_override_modifier, - defaultValueDescription: false, - }, - { - name: "noPropertyAccessFromIndexSignature", - type: "boolean", - showInSimplifiedHelpView: false, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Enforces_using_indexed_accessors_for_keys_declared_using_an_indexed_type, - defaultValueDescription: false, - }, +/* @internal */ +export const optionDeclarations: ts.CommandLineOption[] = [ + ...commonOptionsWithBuild, + ...commandOptionsWithoutBuild, +]; - // Module Resolution - { - name: "moduleResolution", - type: new ts.Map(ts.getEntries({ - node: ts.ModuleResolutionKind.NodeJs, - classic: ts.ModuleResolutionKind.Classic, - node16: ts.ModuleResolutionKind.Node16, - nodenext: ts.ModuleResolutionKind.NodeNext, - })), - affectsModuleResolution: true, - paramType: ts.Diagnostics.STRATEGY, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_how_TypeScript_looks_up_a_file_from_a_given_module_specifier, - defaultValueDescription: ts.Diagnostics.module_AMD_or_UMD_or_System_or_ES6_then_Classic_Otherwise_Node - }, - { - name: "baseUrl", - type: "string", - affectsModuleResolution: true, - isFilePath: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_the_base_directory_to_resolve_non_relative_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: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_a_set_of_entries_that_re_map_imports_to_additional_lookup_locations, - 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: "string", - isFilePath: true - }, - affectsModuleResolution: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Allow_multiple_folders_to_be_treated_as_one_when_resolving_modules, - transpileOptionValue: undefined, - defaultValueDescription: ts.Diagnostics.Computed_from_the_list_of_input_files - }, - { - name: "typeRoots", - type: "list", - element: { - name: "typeRoots", - type: "string", - isFilePath: true - }, - affectsModuleResolution: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_multiple_folders_that_act_like_Slashnode_modules_Slash_types - }, - { - name: "types", - type: "list", - element: { - name: "types", - type: "string" - }, - affectsProgramStructure: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Specify_type_package_names_to_be_included_without_being_referenced_in_a_source_file, - transpileOptionValue: undefined - }, - { - name: "allowSyntheticDefaultImports", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Interop_Constraints, - description: ts.Diagnostics.Allow_import_x_from_y_when_a_module_doesn_t_have_a_default_export, - defaultValueDescription: ts.Diagnostics.module_system_or_esModuleInterop - }, - { - name: "esModuleInterop", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - showInSimplifiedHelpView: true, - category: ts.Diagnostics.Interop_Constraints, - description: ts.Diagnostics.Emit_additional_JavaScript_to_ease_support_for_importing_CommonJS_modules_This_enables_allowSyntheticDefaultImports_for_type_compatibility, - defaultValueDescription: false, - }, - { - name: "preserveSymlinks", - type: "boolean", - category: ts.Diagnostics.Interop_Constraints, - description: ts.Diagnostics.Disable_resolving_symlinks_to_their_realpath_This_correlates_to_the_same_flag_in_node, - defaultValueDescription: false, - }, - { - name: "allowUmdGlobalAccess", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Allow_accessing_UMD_globals_from_modules, - defaultValueDescription: false, - }, - { - name: "moduleSuffixes", - type: "list", - element: { - name: "suffix", - type: "string", - }, - listPreserveFalsyValues: true, - affectsModuleResolution: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module, - }, +/* @internal */ +export const semanticDiagnosticsOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSemanticDiagnostics); - // Source Maps - { - name: "sourceRoot", - type: "string", - affectsEmit: true, - paramType: ts.Diagnostics.LOCATION, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Specify_the_root_path_for_debuggers_to_find_the_reference_source_code, - }, - { - name: "mapRoot", - type: "string", - affectsEmit: true, - paramType: ts.Diagnostics.LOCATION, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Specify_the_location_where_debugger_should_locate_map_files_instead_of_generated_locations, - }, - { - name: "inlineSourceMap", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Include_sourcemap_files_inside_the_emitted_JavaScript, - defaultValueDescription: false, - }, - { - name: "inlineSources", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Include_source_code_in_the_sourcemaps_inside_the_emitted_JavaScript, - defaultValueDescription: false, - }, +/* @internal */ +export const affectsEmitOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsEmit); - // Experimental - { - name: "experimentalDecorators", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Enable_experimental_support_for_TC39_stage_2_draft_decorators, - defaultValueDescription: false, - }, - { - name: "emitDecoratorMetadata", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Emit_design_type_metadata_for_decorated_declarations_in_source_files, - defaultValueDescription: false, - }, +/* @internal */ +export const moduleResolutionOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsModuleResolution); - // Advanced - { - name: "jsxFactory", - type: "string", - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_the_JSX_factory_function_used_when_targeting_React_JSX_emit_e_g_React_createElement_or_h, - defaultValueDescription: "`React.createElement`" - }, - { - name: "jsxFragmentFactory", - type: "string", - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_the_JSX_Fragment_reference_used_for_fragments_when_targeting_React_JSX_emit_e_g_React_Fragment_or_Fragment, - defaultValueDescription: "React.Fragment", - }, - { - name: "jsxImportSource", - type: "string", - affectsSemanticDiagnostics: true, - affectsEmit: true, - affectsModuleResolution: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_module_specifier_used_to_import_the_JSX_factory_functions_when_using_jsx_Colon_react_jsx_Asterisk, - defaultValueDescription: "react" - }, - { - name: "resolveJsonModule", - type: "boolean", - affectsModuleResolution: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Enable_importing_json_files, - defaultValueDescription: false, - }, +/* @internal */ +export const sourceFileAffectingCompilerOptions: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSourceFile || !!option.affectsModuleResolution || !!option.affectsBindDiagnostics); - { - name: "out", - type: "string", - affectsEmit: true, - isFilePath: false, - // for correct behaviour, please use outFile - category: ts.Diagnostics.Backwards_Compatibility, - paramType: ts.Diagnostics.FILE, - transpileOptionValue: undefined, - description: ts.Diagnostics.Deprecated_setting_Use_outFile_instead, - }, - { - name: "reactNamespace", - type: "string", - affectsEmit: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Specify_the_object_invoked_for_createElement_This_only_applies_when_targeting_react_JSX_emit, - defaultValueDescription: "`React`", - }, - { - name: "skipDefaultLibCheck", - type: "boolean", - category: ts.Diagnostics.Completeness, - description: ts.Diagnostics.Skip_type_checking_d_ts_files_that_are_included_with_TypeScript, - defaultValueDescription: false, - }, - { - name: "charset", - type: "string", - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.No_longer_supported_In_early_versions_manually_set_the_text_encoding_for_reading_files, - defaultValueDescription: "utf8" - }, - { - name: "emitBOM", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Emit_a_UTF_8_Byte_Order_Mark_BOM_in_the_beginning_of_output_files, - defaultValueDescription: false, - }, - { - name: "newLine", - type: new ts.Map(ts.getEntries({ - crlf: ts.NewLineKind.CarriageReturnLineFeed, - lf: ts.NewLineKind.LineFeed - })), - affectsEmit: true, - paramType: ts.Diagnostics.NEWLINE, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Set_the_newline_character_for_emitting_files, - defaultValueDescription: ts.Diagnostics.Platform_specific - }, - { - name: "noErrorTruncation", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Output_Formatting, - description: ts.Diagnostics.Disable_truncating_types_in_error_messages, - defaultValueDescription: false, - }, - { - name: "noLib", - type: "boolean", - category: ts.Diagnostics.Language_and_Environment, - affectsProgramStructure: true, - description: ts.Diagnostics.Disable_including_any_library_files_including_the_default_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, - defaultValueDescription: false, - }, - { - name: "noResolve", - type: "boolean", - affectsModuleResolution: true, - category: ts.Diagnostics.Modules, - description: ts.Diagnostics.Disallow_import_s_require_s_or_reference_s_from_expanding_the_number_of_files_TypeScript_should_add_to_a_project, - // 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, - defaultValueDescription: false, - }, - { - name: "stripInternal", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Disable_emitting_declarations_that_have_internal_in_their_JSDoc_comments, - defaultValueDescription: false, - }, - { - name: "disableSizeLimit", - type: "boolean", - affectsProgramStructure: true, - category: ts.Diagnostics.Editor_Support, - description: ts.Diagnostics.Remove_the_20mb_cap_on_total_source_code_size_for_JavaScript_files_in_the_TypeScript_language_server, - defaultValueDescription: false, - }, - { - name: "disableSourceOfProjectReferenceRedirect", - type: "boolean", - isTSConfigOnly: true, - category: ts.Diagnostics.Projects, - description: ts.Diagnostics.Disable_preferring_source_files_instead_of_declaration_files_when_referencing_composite_projects, - defaultValueDescription: false, - }, - { - name: "disableSolutionSearching", - type: "boolean", - isTSConfigOnly: true, - category: ts.Diagnostics.Projects, - description: ts.Diagnostics.Opt_a_project_out_of_multi_project_reference_checking_when_editing, - defaultValueDescription: false, - }, - { - name: "disableReferencedProjectLoad", - type: "boolean", - isTSConfigOnly: true, - category: ts.Diagnostics.Projects, - description: ts.Diagnostics.Reduce_the_number_of_projects_loaded_automatically_by_TypeScript, - defaultValueDescription: false, - }, - { - name: "noImplicitUseStrict", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Disable_adding_use_strict_directives_in_emitted_JavaScript_files, - defaultValueDescription: false, - }, - { - name: "noEmitHelpers", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Disable_generating_custom_helper_functions_like_extends_in_compiled_output, - defaultValueDescription: false, - }, - { - name: "noEmitOnError", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - transpileOptionValue: undefined, - description: ts.Diagnostics.Disable_emitting_files_if_any_type_checking_errors_are_reported, - defaultValueDescription: false, - }, - { - name: "preserveConstEnums", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Disable_erasing_const_enum_declarations_in_generated_code, - defaultValueDescription: false, - }, - { - name: "declarationDir", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: ts.Diagnostics.DIRECTORY, - category: ts.Diagnostics.Emit, - transpileOptionValue: undefined, - description: ts.Diagnostics.Specify_the_output_directory_for_generated_declaration_files, - }, - { - name: "skipLibCheck", - type: "boolean", - category: ts.Diagnostics.Completeness, - description: ts.Diagnostics.Skip_type_checking_all_d_ts_files, - defaultValueDescription: false, - }, - { - name: "allowUnusedLabels", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Disable_error_reporting_for_unused_labels, - defaultValueDescription: undefined, - }, - { - name: "allowUnreachableCode", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Type_Checking, - description: ts.Diagnostics.Disable_error_reporting_for_unreachable_code, - defaultValueDescription: undefined, - }, - { - name: "suppressExcessPropertyErrors", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Disable_reporting_of_excess_property_errors_during_the_creation_of_object_literals, - defaultValueDescription: false, - }, - { - name: "suppressImplicitAnyIndexErrors", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Suppress_noImplicitAny_errors_when_indexing_objects_that_lack_index_signatures, - defaultValueDescription: false, - }, - { - name: "forceConsistentCasingInFileNames", - type: "boolean", - affectsModuleResolution: true, - category: ts.Diagnostics.Interop_Constraints, - description: ts.Diagnostics.Ensure_that_casing_is_correct_in_imports, - defaultValueDescription: false, - }, - { - name: "maxNodeModuleJsDepth", - type: "number", - affectsModuleResolution: true, - category: ts.Diagnostics.JavaScript_Support, - description: ts.Diagnostics.Specify_the_maximum_folder_depth_used_for_checking_JavaScript_files_from_node_modules_Only_applicable_with_allowJs, - defaultValueDescription: 0, - }, - { - name: "noStrictGenericChecks", - type: "boolean", - affectsSemanticDiagnostics: true, - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Disable_strict_checking_of_generic_signatures_in_function_types, - defaultValueDescription: false, - }, - { - name: "useDefineForClassFields", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: ts.Diagnostics.Language_and_Environment, - description: ts.Diagnostics.Emit_ECMAScript_standard_compliant_class_fields, - defaultValueDescription: ts.Diagnostics.true_for_ES2022_and_above_including_ESNext - }, - { - name: "preserveValueImports", - type: "boolean", - affectsEmit: true, - category: ts.Diagnostics.Emit, - description: ts.Diagnostics.Preserve_unused_imported_values_in_the_JavaScript_output_that_would_otherwise_be_removed, - defaultValueDescription: false, - }, +/* @internal */ +export const optionsAffectingProgramStructure: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsProgramStructure); - { - name: "keyofStringsOnly", - type: "boolean", - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Make_keyof_only_return_strings_instead_of_string_numbers_or_symbols_Legacy_option, - defaultValueDescription: false, - }, - { - // A list of plugins to load in the language service - name: "plugins", - type: "list", - isTSConfigOnly: true, - element: { - name: "plugin", - type: "object" - }, - description: ts.Diagnostics.Specify_a_list_of_language_service_plugins_to_include, - category: ts.Diagnostics.Editor_Support, +/* @internal */ +export const transpileOptionValueCompilerOptions: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => ts.hasProperty(option, "transpileOptionValue")); - }, - { - name: "moduleDetection", - type: new ts.Map(ts.getEntries({ - auto: ts.ModuleDetectionKind.Auto, - legacy: ts.ModuleDetectionKind.Legacy, - force: ts.ModuleDetectionKind.Force, - })), - affectsModuleResolution: true, - description: ts.Diagnostics.Control_what_method_is_used_to_detect_module_format_JS_files, - category: ts.Diagnostics.Language_and_Environment, - defaultValueDescription: ts.Diagnostics.auto_Colon_Treat_files_with_imports_exports_import_meta_jsx_with_jsx_Colon_react_jsx_or_esm_format_with_module_Colon_node16_as_modules, - } - ]; - - /* @internal */ - export const optionDeclarations: ts.CommandLineOption[] = [ - ...commonOptionsWithBuild, - ...commandOptionsWithoutBuild, - ]; - - /* @internal */ - export const semanticDiagnosticsOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSemanticDiagnostics); - - /* @internal */ - export const affectsEmitOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsEmit); - - /* @internal */ - export const moduleResolutionOptionDeclarations: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsModuleResolution); - - /* @internal */ - export const sourceFileAffectingCompilerOptions: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSourceFile || !!option.affectsModuleResolution || !!option.affectsBindDiagnostics); - - /* @internal */ - export const optionsAffectingProgramStructure: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsProgramStructure); - - /* @internal */ - export const transpileOptionValueCompilerOptions: readonly ts.CommandLineOption[] = optionDeclarations.filter(option => ts.hasProperty(option, "transpileOptionValue")); - - // Build related options - /* @internal */ - export const optionsForBuild: ts.CommandLineOption[] = [ - { - name: "verbose", - shortName: "v", - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Enable_verbose_logging, - type: "boolean", - defaultValueDescription: false, - }, - { - name: "dry", - shortName: "d", - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, - type: "boolean", - defaultValueDescription: false, - }, - { - name: "force", - shortName: "f", - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, - type: "boolean", - defaultValueDescription: false, - }, - { - name: "clean", - category: ts.Diagnostics.Command_line_Options, - description: ts.Diagnostics.Delete_the_outputs_of_all_projects, - type: "boolean", - defaultValueDescription: false, - } - ]; - - /* @internal */ - export const buildOpts: ts.CommandLineOption[] = [ - ...commonOptionsWithBuild, - ...optionsForBuild - ]; - - /* @internal */ - export const typeAcquisitionDeclarations: ts.CommandLineOption[] = [ - { - /* @deprecated typingOptions.enableAutoDiscovery - * Use typeAcquisition.enable instead. - */ - name: "enableAutoDiscovery", - type: "boolean", - defaultValueDescription: false, - }, - { - name: "enable", - type: "boolean", - defaultValueDescription: false, - }, - { +// Build related options +/* @internal */ +export const optionsForBuild: ts.CommandLineOption[] = [ + { + name: "verbose", + shortName: "v", + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Enable_verbose_logging, + type: "boolean", + defaultValueDescription: false, + }, + { + name: "dry", + shortName: "d", + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean", + defaultValueDescription: false, + }, + { + name: "force", + shortName: "f", + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean", + defaultValueDescription: false, + }, + { + name: "clean", + category: ts.Diagnostics.Command_line_Options, + description: ts.Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean", + defaultValueDescription: false, + } +]; + +/* @internal */ +export const buildOpts: ts.CommandLineOption[] = [ + ...commonOptionsWithBuild, + ...optionsForBuild +]; + +/* @internal */ +export const typeAcquisitionDeclarations: ts.CommandLineOption[] = [ + { + /* @deprecated typingOptions.enableAutoDiscovery + * Use typeAcquisition.enable instead. + */ + name: "enableAutoDiscovery", + type: "boolean", + defaultValueDescription: false, + }, + { + name: "enable", + type: "boolean", + defaultValueDescription: false, + }, + { + 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" - } - }, - { - name: "disableFilenameBasedTypeAcquisition", - type: "boolean", - defaultValueDescription: false, - }, - ]; - - /* @internal */ - export interface OptionsNameMap { - optionsNameMap: ts.ESMap; - shortOptionNames: ts.ESMap; - } - - /*@internal*/ - export function createOptionNameMap(optionDeclarations: readonly ts.CommandLineOption[]): OptionsNameMap { - const optionsNameMap = new ts.Map(); - const shortOptionNames = new ts.Map(); - ts.forEach(optionDeclarations, option => { - optionsNameMap.set(option.name.toLowerCase(), option); - if (option.shortName) { - shortOptionNames.set(option.shortName, option.name); - } - }); + type: "string" + } + }, + { + name: "disableFilenameBasedTypeAcquisition", + type: "boolean", + defaultValueDescription: false, + }, +]; - return { optionsNameMap, shortOptionNames }; - } +/* @internal */ +export interface OptionsNameMap { + optionsNameMap: ts.ESMap; + shortOptionNames: ts.ESMap; +} - let optionsNameMapCache: OptionsNameMap; +/*@internal*/ +export function createOptionNameMap(optionDeclarations: readonly ts.CommandLineOption[]): OptionsNameMap { + const optionsNameMap = new ts.Map(); + const shortOptionNames = new ts.Map(); + ts.forEach(optionDeclarations, option => { + optionsNameMap.set(option.name.toLowerCase(), option); + if (option.shortName) { + shortOptionNames.set(option.shortName, option.name); + } + }); - /* @internal */ - export function getOptionsNameMap(): OptionsNameMap { - return optionsNameMapCache ||= createOptionNameMap(optionDeclarations); - } + return { optionsNameMap, shortOptionNames }; +} - const compilerOptionsAlternateMode: ts.AlternateModeDiagnostics = { - diagnostic: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build, - getOptionsNameMap: getBuildOptionsNameMap - }; +let optionsNameMapCache: OptionsNameMap; - /* @internal */ - export const defaultInitCompilerOptions: ts.CompilerOptions = { - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2016, - strict: true, - esModuleInterop: true, - forceConsistentCasingInFileNames: true, - skipLibCheck: true - }; +/* @internal */ +export function getOptionsNameMap(): OptionsNameMap { + return optionsNameMapCache ||= createOptionNameMap(optionDeclarations); +} - /* @internal */ - export function convertEnableAutoDiscoveryToEnable(typeAcquisition: ts.TypeAcquisition): ts.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; +const compilerOptionsAlternateMode: ts.AlternateModeDiagnostics = { + diagnostic: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build, + getOptionsNameMap: getBuildOptionsNameMap +}; + +/* @internal */ +export const defaultInitCompilerOptions: ts.CompilerOptions = { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2016, + strict: true, + esModuleInterop: true, + forceConsistentCasingInFileNames: true, + skipLibCheck: true +}; + +/* @internal */ +export function convertEnableAutoDiscoveryToEnable(typeAcquisition: ts.TypeAcquisition): ts.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: ts.CommandLineOptionOfCustomType): ts.Diagnostic { - return createDiagnosticForInvalidCustomType(opt, ts.createCompilerDiagnostic); - } +/* @internal */ +export function createCompilerDiagnosticForInvalidCustomType(opt: ts.CommandLineOptionOfCustomType): ts.Diagnostic { + return createDiagnosticForInvalidCustomType(opt, ts.createCompilerDiagnostic); +} - function createDiagnosticForInvalidCustomType(opt: ts.CommandLineOptionOfCustomType, createDiagnostic: (message: ts.DiagnosticMessage, arg0: string, arg1: string) => ts.Diagnostic): ts.Diagnostic { - const namesOfType = ts.arrayFrom(opt.type.keys()).map(key => `'${key}'`).join(", "); - return createDiagnostic(ts.Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); - } +function createDiagnosticForInvalidCustomType(opt: ts.CommandLineOptionOfCustomType, createDiagnostic: (message: ts.DiagnosticMessage, arg0: string, arg1: string) => ts.Diagnostic): ts.Diagnostic { + const namesOfType = ts.arrayFrom(opt.type.keys()).map(key => `'${key}'`).join(", "); + return createDiagnostic(ts.Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); +} - /* @internal */ - export function parseCustomTypeOption(opt: ts.CommandLineOptionOfCustomType, value: string, errors: ts.Push) { - return convertJsonOptionOfCustomType(opt, ts.trimString(value || ""), errors); - } +/* @internal */ +export function parseCustomTypeOption(opt: ts.CommandLineOptionOfCustomType, value: string, errors: ts.Push) { + return convertJsonOptionOfCustomType(opt, ts.trimString(value || ""), errors); +} - /* @internal */ - export function parseListTypeOption(opt: ts.CommandLineOptionOfListType, value = "", errors: ts.Push): (string | number)[] | undefined { - value = ts.trimString(value); - if (ts.startsWith(value, "-")) { - return undefined; - } - if (value === "") { - return []; - } - const values = value.split(","); - switch (opt.element.type) { - case "number": - return ts.mapDefined(values, v => validateJsonOptionValue(opt.element, parseInt(v), errors)); - case "string": - return ts.mapDefined(values, v => validateJsonOptionValue(opt.element, v || "", errors)); - default: - return ts.mapDefined(values, v => parseCustomTypeOption(opt.element as ts.CommandLineOptionOfCustomType, v, errors)); - } +/* @internal */ +export function parseListTypeOption(opt: ts.CommandLineOptionOfListType, value = "", errors: ts.Push): (string | number)[] | undefined { + value = ts.trimString(value); + if (ts.startsWith(value, "-")) { + return undefined; } - - /*@internal*/ - export interface OptionsBase { - [option: string]: ts.CompilerOptionsValue | ts.TsConfigSourceFile | undefined; + if (value === "") { + return []; } - - /*@internal*/ - export interface ParseCommandLineWorkerDiagnostics extends ts.DidYouMeanOptionsDiagnostics { - getOptionsNameMap: () => OptionsNameMap; - optionTypeMismatchDiagnostic: ts.DiagnosticMessage; + const values = value.split(","); + switch (opt.element.type) { + case "number": + return ts.mapDefined(values, v => validateJsonOptionValue(opt.element, parseInt(v), errors)); + case "string": + return ts.mapDefined(values, v => validateJsonOptionValue(opt.element, v || "", errors)); + default: + return ts.mapDefined(values, v => parseCustomTypeOption(opt.element as ts.CommandLineOptionOfCustomType, v, errors)); } +} - function getOptionName(option: ts.CommandLineOption) { - return option.name; - } +/*@internal*/ +export interface OptionsBase { + [option: string]: ts.CompilerOptionsValue | ts.TsConfigSourceFile | undefined; +} - function createUnknownOptionError(unknownOption: string, diagnostics: ts.DidYouMeanOptionsDiagnostics, createDiagnostics: (message: ts.DiagnosticMessage, arg0: string, arg1?: string) => ts.Diagnostic, unknownOptionErrorText?: string) { - if (diagnostics.alternateMode?.getOptionsNameMap().optionsNameMap.has(unknownOption.toLowerCase())) { - return createDiagnostics(diagnostics.alternateMode.diagnostic, unknownOption); - } +/*@internal*/ +export interface ParseCommandLineWorkerDiagnostics extends ts.DidYouMeanOptionsDiagnostics { + getOptionsNameMap: () => OptionsNameMap; + optionTypeMismatchDiagnostic: ts.DiagnosticMessage; +} + +function getOptionName(option: ts.CommandLineOption) { + return option.name; +} - const possibleOption = ts.getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); - return possibleOption ? - createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : - createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); +function createUnknownOptionError(unknownOption: string, diagnostics: ts.DidYouMeanOptionsDiagnostics, createDiagnostics: (message: ts.DiagnosticMessage, arg0: string, arg1?: string) => ts.Diagnostic, unknownOptionErrorText?: string) { + if (diagnostics.alternateMode?.getOptionsNameMap().optionsNameMap.has(unknownOption.toLowerCase())) { + return createDiagnostics(diagnostics.alternateMode.diagnostic, unknownOption); } - /*@internal*/ - export function parseCommandLineWorker(diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { - const options = {} as OptionsBase; - let watchOptions: ts.WatchOptions | undefined; - const fileNames: string[] = []; - const errors: ts.Diagnostic[] = []; + const possibleOption = ts.getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); + return possibleOption ? + createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : + createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); +} - parseStrings(commandLine); - return { - options, - watchOptions, - fileNames, - errors - }; +/*@internal*/ +export function parseCommandLineWorker(diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { + const options = {} as OptionsBase; + let watchOptions: ts.WatchOptions | undefined; + const fileNames: string[] = []; + const errors: ts.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) === ts.CharacterCodes.at) { - parseResponseFile(s.slice(1)); + function parseStrings(args: readonly string[]) { + let i = 0; + while (i < args.length) { + const s = args[i]; + i++; + if (s.charCodeAt(0) === ts.CharacterCodes.at) { + parseResponseFile(s.slice(1)); + } + else if (s.charCodeAt(0) === ts.CharacterCodes.minus) { + const inputOptionName = s.slice(s.charCodeAt(1) === ts.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) === ts.CharacterCodes.minus) { - const inputOptionName = s.slice(s.charCodeAt(1) === ts.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, ts.createCompilerDiagnostic, s)); - } + errors.push(createUnknownOptionError(inputOptionName, diagnostics, ts.createCompilerDiagnostic, s)); } } - else { - fileNames.push(s); - } + } + else { + fileNames.push(s); } } + } - function parseResponseFile(fileName: string) { - const text = tryReadFile(fileName, readFile || (fileName => ts.sys.readFile(fileName))); - if (!ts.isString(text)) { - errors.push(text); - return; - } + function parseResponseFile(fileName: string) { + const text = tryReadFile(fileName, readFile || (fileName => ts.sys.readFile(fileName))); + if (!ts.isString(text)) { + errors.push(text); + return; + } - const args: string[] = []; - let pos = 0; - while (true) { - while (pos < text.length && text.charCodeAt(pos) <= ts.CharacterCodes.space) - pos++; - if (pos >= text.length) - break; - const start = pos; - if (text.charCodeAt(start) === ts.CharacterCodes.doubleQuote) { - pos++; - while (pos < text.length && text.charCodeAt(pos) !== ts.CharacterCodes.doubleQuote) + const args: string[] = []; + let pos = 0; + while (true) { + while (pos < text.length && text.charCodeAt(pos) <= ts.CharacterCodes.space) + pos++; + if (pos >= text.length) + break; + const start = pos; + if (text.charCodeAt(start) === ts.CharacterCodes.doubleQuote) { + pos++; + while (pos < text.length && text.charCodeAt(pos) !== ts.CharacterCodes.doubleQuote) + pos++; + if (pos < text.length) { + args.push(text.substring(start + 1, pos)); pos++; - if (pos < text.length) { - args.push(text.substring(start + 1, pos)); - pos++; - } - else { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Unterminated_quoted_string_in_response_file_0, fileName)); - } } else { - while (text.charCodeAt(pos) > ts.CharacterCodes.space) - pos++; - args.push(text.substring(start, pos)); + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Unterminated_quoted_string_in_response_file_0, fileName)); } } - parseStrings(args); + else { + while (text.charCodeAt(pos) > ts.CharacterCodes.space) + pos++; + args.push(text.substring(start, pos)); + } } + parseStrings(args); } +} - function parseOptionValue(args: readonly string[], i: number, diagnostics: ParseCommandLineWorkerDiagnostics, opt: ts.CommandLineOption, options: OptionsBase, errors: ts.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: ts.CommandLineOption, options: OptionsBase, errors: ts.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] = validateJsonOptionValue(opt, /*value*/ false, errors); i++; } - else if (opt.type === "boolean") { - if (optValue === "false") { - options[opt.name] = validateJsonOptionValue(opt, /*value*/ false, errors); - i++; - } - else { - if (optValue === "true") - i++; - errors.push(ts.createCompilerDiagnostic(ts.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(ts.createCompilerDiagnostic(ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.name)); - if (optValue && !ts.startsWith(optValue, "-")) + if (optValue === "true") i++; + errors.push(ts.createCompilerDiagnostic(ts.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(ts.createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); - } + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.name)); + if (optValue && !ts.startsWith(optValue, "-")) + i++; + } + } + 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(ts.createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); + } - if (args[i] !== "null") { - switch (opt.type) { - case "number": - options[opt.name] = validateJsonOptionValue(opt, parseInt(args[i]), errors); - i++; - break; - case "boolean": - // boolean flag has optional value true, false, others - const optValue = args[i]; - options[opt.name] = validateJsonOptionValue(opt, optValue !== "false", errors); - // consume next argument as boolean flag value - if (optValue === "false" || optValue === "true") { - i++; - } - break; - case "string": - options[opt.name] = validateJsonOptionValue(opt, args[i] || "", errors); + if (args[i] !== "null") { + switch (opt.type) { + case "number": + options[opt.name] = validateJsonOptionValue(opt, parseInt(args[i]), errors); + i++; + break; + case "boolean": + // boolean flag has optional value true, false, others + const optValue = args[i]; + options[opt.name] = validateJsonOptionValue(opt, optValue !== "false", errors); + // consume next argument as boolean flag value + if (optValue === "false" || optValue === "true") { 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 as ts.CommandLineOptionOfCustomType, args[i], errors); + } + break; + case "string": + options[opt.name] = validateJsonOptionValue(opt, args[i] || "", errors); + i++; + break; + case "list": + const result = parseListTypeOption(opt, args[i], errors); + options[opt.name] = result || []; + if (result) { i++; - break; - } - } - else { - options[opt.name] = undefined; - 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 as ts.CommandLineOptionOfCustomType, args[i], errors); + i++; + break; } } - return i; + else { + options[opt.name] = undefined; + i++; + } } + return i; +} - /*@internal*/ - export const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - alternateMode: compilerOptionsAlternateMode, - getOptionsNameMap, - optionDeclarations, - unknownOptionDiagnostic: ts.Diagnostics.Unknown_compiler_option_0, - unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: ts.Diagnostics.Compiler_option_0_expects_an_argument - }; - export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ts.ParsedCommandLine { - return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile); - } +/*@internal*/ +export const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + alternateMode: compilerOptionsAlternateMode, + getOptionsNameMap, + optionDeclarations, + unknownOptionDiagnostic: ts.Diagnostics.Unknown_compiler_option_0, + unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: ts.Diagnostics.Compiler_option_0_expects_an_argument +}; +export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ts.ParsedCommandLine { + return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile); +} - /** @internal */ - export function getOptionFromName(optionName: string, allowShort?: boolean): ts.CommandLineOption | undefined { - return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort); - } +/** @internal */ +export function getOptionFromName(optionName: string, allowShort?: boolean): ts.CommandLineOption | undefined { + return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort); +} - function getOptionDeclarationFromName(getOptionNameMap: () => OptionsNameMap, optionName: string, allowShort = false): ts.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; - } +function getOptionDeclarationFromName(getOptionNameMap: () => OptionsNameMap, optionName: string, allowShort = false): ts.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: ts.BuildOptions; - watchOptions: ts.WatchOptions | undefined; - projects: string[]; - errors: ts.Diagnostic[]; - } - - let buildOptionsNameMapCache: OptionsNameMap; - function getBuildOptionsNameMap(): OptionsNameMap { - return buildOptionsNameMapCache || (buildOptionsNameMapCache = createOptionNameMap(buildOpts)); } + return optionsNameMap.get(optionName); +} - const buildOptionsAlternateMode: ts.AlternateModeDiagnostics = { - diagnostic: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build, - getOptionsNameMap - }; +/*@internal*/ +export interface ParsedBuildCommand { + buildOptions: ts.BuildOptions; + watchOptions: ts.WatchOptions | undefined; + projects: string[]; + errors: ts.Diagnostic[]; +} - const buildOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - alternateMode: buildOptionsAlternateMode, - getOptionsNameMap: getBuildOptionsNameMap, - optionDeclarations: buildOpts, - unknownOptionDiagnostic: ts.Diagnostics.Unknown_build_option_0, - unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_build_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: ts.Diagnostics.Build_option_0_requires_a_value_of_type_1 - }; +let buildOptionsNameMapCache: OptionsNameMap; +function getBuildOptionsNameMap(): OptionsNameMap { + return buildOptionsNameMapCache || (buildOptionsNameMapCache = createOptionNameMap(buildOpts)); +} - /*@internal*/ - export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { - const { options, watchOptions, fileNames: projects, errors } = parseCommandLineWorker(buildOptionsDidYouMeanDiagnostics, args); - const buildOptions = options as ts.BuildOptions; +const buildOptionsAlternateMode: ts.AlternateModeDiagnostics = { + diagnostic: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build, + getOptionsNameMap +}; - if (projects.length === 0) { - // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." - projects.push("."); - } +const buildOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + alternateMode: buildOptionsAlternateMode, + getOptionsNameMap: getBuildOptionsNameMap, + optionDeclarations: buildOpts, + unknownOptionDiagnostic: ts.Diagnostics.Unknown_build_option_0, + unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_build_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: ts.Diagnostics.Build_option_0_requires_a_value_of_type_1 +}; - // Nonsensical combinations - if (buildOptions.clean && buildOptions.force) { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); - } - if (buildOptions.clean && buildOptions.verbose) { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); - } - if (buildOptions.clean && buildOptions.watch) { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); - } - if (buildOptions.watch && buildOptions.dry) { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); - } +/*@internal*/ +export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { + const { options, watchOptions, fileNames: projects, errors } = parseCommandLineWorker(buildOptionsDidYouMeanDiagnostics, args); + const buildOptions = options as ts.BuildOptions; - return { buildOptions, watchOptions, projects, errors }; + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + projects.push("."); } - /* @internal */ - export function getDiagnosticText(_message: ts.DiagnosticMessage, ..._args: any[]): string { - const diagnostic = ts.createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText as string; + // Nonsensical combinations + if (buildOptions.clean && buildOptions.force) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); } - - export type DiagnosticReporter = (diagnostic: ts.Diagnostic) => void; - /** - * Reports config file diagnostics - */ - export interface ConfigFileDiagnosticsReporter { - /** - * Reports unrecoverable error when parsing config file - */ - onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; + if (buildOptions.clean && buildOptions.verbose) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); } - - /** - * Interface extending ParseConfigHost to support ParseConfigFile that reads config file and reports errors - */ - export interface ParseConfigFileHost extends ts.ParseConfigHost, ConfigFileDiagnosticsReporter { - getCurrentDirectory(): string; + if (buildOptions.clean && buildOptions.watch) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + } + if (buildOptions.watch && buildOptions.dry) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); } - /** - * Reads the config file, reports errors if any and exits if the config file cannot be found - */ - export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: ts.CompilerOptions | undefined, host: ParseConfigFileHost, extendedConfigCache?: ts.Map, watchOptionsToExtend?: ts.WatchOptions, extraFileExtensions?: readonly ts.FileExtensionInfo[]): ts.ParsedCommandLine | undefined { - const configFileText = tryReadFile(configFileName, fileName => host.readFile(fileName)); - if (!ts.isString(configFileText)) { - host.onUnRecoverableConfigFileDiagnostic(configFileText); - return undefined; - } + return { buildOptions, watchOptions, projects, errors }; +} - const result = ts.parseJsonText(configFileName, configFileText); - const cwd = host.getCurrentDirectory(); - result.path = ts.toPath(configFileName, cwd, ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames)); - result.resolvedPath = result.path; - result.originalFileName = result.fileName; - return parseJsonSourceFileConfigFileContent(result, host, ts.getNormalizedAbsolutePath(ts.getDirectoryPath(configFileName), cwd), optionsToExtend, ts.getNormalizedAbsolutePath(configFileName, cwd), - /*resolutionStack*/ undefined, extraFileExtensions, extendedConfigCache, watchOptionsToExtend); - } +/* @internal */ +export function getDiagnosticText(_message: ts.DiagnosticMessage, ..._args: any[]): string { + const diagnostic = ts.createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText as string; +} +export type DiagnosticReporter = (diagnostic: ts.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 readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { - config?: any; - error?: ts.Diagnostic; - } { - const textOrDiagnostic = tryReadFile(fileName, readFile); - return ts.isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; - } + onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; +} - /** - * 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?: ts.Diagnostic; - } { - const jsonSourceFile = ts.parseJsonText(fileName, jsonText); - return { - config: convertConfigFileToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics, /*reportOptionsErrors*/ false, /*optionsIterator*/ undefined), - error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : undefined - }; - } +/** + * Interface extending ParseConfigHost to support ParseConfigFile that reads config file and reports errors + */ +export interface ParseConfigFileHost extends ts.ParseConfigHost, ConfigFileDiagnosticsReporter { + getCurrentDirectory(): string; +} - /** - * Read tsconfig.json file - * @param fileName The path to the config file - */ - export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): ts.TsConfigSourceFile { - const textOrDiagnostic = tryReadFile(fileName, readFile); - return ts.isString(textOrDiagnostic) ? ts.parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] } as ts.TsConfigSourceFile; +/** + * Reads the config file, reports errors if any and exits if the config file cannot be found + */ +export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: ts.CompilerOptions | undefined, host: ParseConfigFileHost, extendedConfigCache?: ts.Map, watchOptionsToExtend?: ts.WatchOptions, extraFileExtensions?: readonly ts.FileExtensionInfo[]): ts.ParsedCommandLine | undefined { + const configFileText = tryReadFile(configFileName, fileName => host.readFile(fileName)); + if (!ts.isString(configFileText)) { + host.onUnRecoverableConfigFileDiagnostic(configFileText); + return undefined; } - /*@internal*/ - export function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | ts.Diagnostic { - let text: string | undefined; - try { - text = readFile(fileName); - } - catch (e) { - return ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message); - } - return text === undefined ? ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0, fileName) : text; - } + const result = ts.parseJsonText(configFileName, configFileText); + const cwd = host.getCurrentDirectory(); + result.path = ts.toPath(configFileName, cwd, ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames)); + result.resolvedPath = result.path; + result.originalFileName = result.fileName; + return parseJsonSourceFileConfigFileContent(result, host, ts.getNormalizedAbsolutePath(ts.getDirectoryPath(configFileName), cwd), optionsToExtend, ts.getNormalizedAbsolutePath(configFileName, cwd), + /*resolutionStack*/ undefined, extraFileExtensions, extendedConfigCache, watchOptionsToExtend); +} - function commandLineOptionsToMap(options: readonly ts.CommandLineOption[]) { - return ts.arrayToMap(options, getOptionName); - } +/** + * 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?: ts.Diagnostic; +} { + const textOrDiagnostic = tryReadFile(fileName, readFile); + return ts.isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; +} - const typeAcquisitionDidYouMeanDiagnostics: ts.DidYouMeanOptionsDiagnostics = { - optionDeclarations: typeAcquisitionDeclarations, - unknownOptionDiagnostic: ts.Diagnostics.Unknown_type_acquisition_option_0, - unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1, +/** + * 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?: ts.Diagnostic; +} { + const jsonSourceFile = ts.parseJsonText(fileName, jsonText); + return { + config: convertConfigFileToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics, /*reportOptionsErrors*/ false, /*optionsIterator*/ undefined), + error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : undefined }; +} - let watchOptionsNameMapCache: OptionsNameMap; - function getWatchOptionsNameMap(): OptionsNameMap { - return watchOptionsNameMapCache || (watchOptionsNameMapCache = createOptionNameMap(optionsForWatch)); +/** + * Read tsconfig.json file + * @param fileName The path to the config file + */ +export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): ts.TsConfigSourceFile { + const textOrDiagnostic = tryReadFile(fileName, readFile); + return ts.isString(textOrDiagnostic) ? ts.parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] } as ts.TsConfigSourceFile; +} + +/*@internal*/ +export function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | ts.Diagnostic { + let text: string | undefined; + try { + text = readFile(fileName); } - const watchOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - getOptionsNameMap: getWatchOptionsNameMap, - optionDeclarations: optionsForWatch, - unknownOptionDiagnostic: ts.Diagnostics.Unknown_watch_option_0, - unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_watch_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1 - }; + catch (e) { + return ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message); + } + return text === undefined ? ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0, fileName) : text; +} - let commandLineCompilerOptionsMapCache: ts.ESMap; - function getCommandLineCompilerOptionsMap() { - return commandLineCompilerOptionsMapCache || (commandLineCompilerOptionsMapCache = commandLineOptionsToMap(optionDeclarations)); - } - let commandLineWatchOptionsMapCache: ts.ESMap; - function getCommandLineWatchOptionsMap() { - return commandLineWatchOptionsMapCache || (commandLineWatchOptionsMapCache = commandLineOptionsToMap(optionsForWatch)); - } - let commandLineTypeAcquisitionMapCache: ts.ESMap; - function getCommandLineTypeAcquisitionMap() { - return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); - } - - let _tsconfigRootOptions: ts.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", - category: ts.Diagnostics.File_Management, - }, - { +function commandLineOptionsToMap(options: readonly ts.CommandLineOption[]) { + return ts.arrayToMap(options, getOptionName); +} + +const typeAcquisitionDidYouMeanDiagnostics: ts.DidYouMeanOptionsDiagnostics = { + optionDeclarations: typeAcquisitionDeclarations, + unknownOptionDiagnostic: ts.Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: ts.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: ts.Diagnostics.Unknown_watch_option_0, + unknownDidYouMeanDiagnostic: ts.Diagnostics.Unknown_watch_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1 +}; + +let commandLineCompilerOptionsMapCache: ts.ESMap; +function getCommandLineCompilerOptionsMap() { + return commandLineCompilerOptionsMapCache || (commandLineCompilerOptionsMapCache = commandLineOptionsToMap(optionDeclarations)); +} +let commandLineWatchOptionsMapCache: ts.ESMap; +function getCommandLineWatchOptionsMap() { + return commandLineWatchOptionsMapCache || (commandLineWatchOptionsMapCache = commandLineOptionsToMap(optionsForWatch)); +} +let commandLineTypeAcquisitionMapCache: ts.ESMap; +function getCommandLineTypeAcquisitionMap() { + return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); +} + +let _tsconfigRootOptions: ts.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", + category: ts.Diagnostics.File_Management, + }, + { + name: "references", + type: "list", + element: { name: "references", - type: "list", - element: { - name: "references", - type: "object" - }, - category: ts.Diagnostics.Projects, + type: "object" }, - { + category: ts.Diagnostics.Projects, + }, + { + name: "files", + type: "list", + element: { name: "files", - type: "list", - element: { - name: "files", - type: "string" - }, - category: ts.Diagnostics.File_Management, + type: "string" }, - { + category: ts.Diagnostics.File_Management, + }, + { + name: "include", + type: "list", + element: { name: "include", - type: "list", - element: { - name: "include", - type: "string" - }, - category: ts.Diagnostics.File_Management, - defaultValueDescription: ts.Diagnostics.if_files_is_specified_otherwise_Asterisk_Asterisk_Slash_Asterisk + type: "string" }, - { + category: ts.Diagnostics.File_Management, + defaultValueDescription: ts.Diagnostics.if_files_is_specified_otherwise_Asterisk_Asterisk_Slash_Asterisk + }, + { + name: "exclude", + type: "list", + element: { name: "exclude", - type: "list", - element: { - name: "exclude", - type: "string" - }, - category: ts.Diagnostics.File_Management, - defaultValueDescription: ts.Diagnostics.node_modules_bower_components_jspm_packages_plus_the_value_of_outDir_if_one_is_specified + type: "string" }, - compileOnSaveCommandLineOption - ]) - }; - } - return _tsconfigRootOptions; + category: ts.Diagnostics.File_Management, + defaultValueDescription: ts.Diagnostics.node_modules_bower_components_jspm_packages_plus_the_value_of_outDir_if_one_is_specified + }, + 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: ts.CommandLineOption, value: ts.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: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.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: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.Expression): void; - } - - function convertConfigFileToObject(sourceFile: ts.JsonSourceFile, errors: ts.Push, reportOptionsErrors: boolean, optionsIterator: JsonConversionNotifier | undefined): any { - const rootExpression: ts.Expression | undefined = sourceFile.statements[0]?.expression; - const knownRootOptions = reportOptionsErrors ? getTsconfigRootOptionsMap() : undefined; - if (rootExpression && rootExpression.kind !== ts.SyntaxKind.ObjectLiteralExpression) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, rootExpression, ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, ts.getBaseFileName(sourceFile.fileName) === "jsconfig.json" ? "jsconfig.json" : "tsconfig.json")); - // Last-ditch error recovery. Somewhat useful because the JSON parser will recover from some parse errors by - // synthesizing a top-level array literal expression. There's a reasonable chance the first element of that - // array is a well-formed configuration object, made into an array element by stray characters. - if (ts.isArrayLiteralExpression(rootExpression)) { - const firstObject = ts.find(rootExpression.elements, ts.isObjectLiteralExpression); - if (firstObject) { - return convertToObjectWorker(sourceFile, firstObject, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); - } +/*@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: ts.CommandLineOption, value: ts.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: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.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: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.Expression): void; +} + +function convertConfigFileToObject(sourceFile: ts.JsonSourceFile, errors: ts.Push, reportOptionsErrors: boolean, optionsIterator: JsonConversionNotifier | undefined): any { + const rootExpression: ts.Expression | undefined = sourceFile.statements[0]?.expression; + const knownRootOptions = reportOptionsErrors ? getTsconfigRootOptionsMap() : undefined; + if (rootExpression && rootExpression.kind !== ts.SyntaxKind.ObjectLiteralExpression) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, rootExpression, ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, ts.getBaseFileName(sourceFile.fileName) === "jsconfig.json" ? "jsconfig.json" : "tsconfig.json")); + // Last-ditch error recovery. Somewhat useful because the JSON parser will recover from some parse errors by + // synthesizing a top-level array literal expression. There's a reasonable chance the first element of that + // array is a well-formed configuration object, made into an array element by stray characters. + if (ts.isArrayLiteralExpression(rootExpression)) { + const firstObject = ts.find(rootExpression.elements, ts.isObjectLiteralExpression); + if (firstObject) { + return convertToObjectWorker(sourceFile, firstObject, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); } - return {}; } - return convertToObjectWorker(sourceFile, rootExpression, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); + return {}; } + return convertToObjectWorker(sourceFile, rootExpression, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); +} - /** - * Convert the json syntax tree into the json value - */ - export function convertToObject(sourceFile: ts.JsonSourceFile, errors: ts.Push): any { - return convertToObjectWorker(sourceFile, sourceFile.statements[0]?.expression, errors, /*returnValue*/ true, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); +/** + * Convert the json syntax tree into the json value + */ +export function convertToObject(sourceFile: ts.JsonSourceFile, errors: ts.Push): any { + return convertToObjectWorker(sourceFile, sourceFile.statements[0]?.expression, 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: ts.JsonSourceFile, rootExpression: ts.Expression | undefined, errors: ts.Push, returnValue: boolean, knownRootOptions: ts.CommandLineOption | undefined, jsonConversionNotifier: JsonConversionNotifier | undefined): any { + if (!rootExpression) { + return returnValue ? {} : 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: ts.JsonSourceFile, rootExpression: ts.Expression | undefined, errors: ts.Push, returnValue: boolean, knownRootOptions: ts.CommandLineOption | undefined, jsonConversionNotifier: JsonConversionNotifier | undefined): any { - if (!rootExpression) { - return returnValue ? {} : undefined; - } + return convertPropertyValueToJson(rootExpression, knownRootOptions); - return convertPropertyValueToJson(rootExpression, knownRootOptions); + function isRootOptionMap(knownOptions: ts.ESMap | undefined) { + return knownRootOptions && (knownRootOptions as ts.TsConfigOnlyOption).elementOptions === knownOptions; + } + function convertObjectLiteralExpressionToJson(node: ts.ObjectLiteralExpression, knownOptions: ts.ESMap | undefined, extraKeyDiagnostics: ts.DidYouMeanOptionsDiagnostics | undefined, parentOption: string | undefined): any { + const result: any = returnValue ? {} : undefined; + for (const element of node.properties) { + if (element.kind !== ts.SyntaxKind.PropertyAssignment) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element, ts.Diagnostics.Property_assignment_expected)); + continue; + } - function isRootOptionMap(knownOptions: ts.ESMap | undefined) { - return knownRootOptions && (knownRootOptions as ts.TsConfigOnlyOption).elementOptions === knownOptions; - } - function convertObjectLiteralExpressionToJson(node: ts.ObjectLiteralExpression, knownOptions: ts.ESMap | undefined, extraKeyDiagnostics: ts.DidYouMeanOptionsDiagnostics | undefined, parentOption: string | undefined): any { - const result: any = returnValue ? {} : undefined; - for (const element of node.properties) { - if (element.kind !== ts.SyntaxKind.PropertyAssignment) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element, ts.Diagnostics.Property_assignment_expected)); - continue; - } + if (element.questionToken) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); + } + if (!isDoubleQuotedString(element.name)) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element.name, ts.Diagnostics.String_literal_with_double_quotes_expected)); + } - if (element.questionToken) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); + const textOfKey = ts.isComputedNonLiteralName(element.name) ? undefined : ts.getTextOfPropertyName(element.name); + const keyText = textOfKey && ts.unescapeLeadingUnderscores(textOfKey); + const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; + if (keyText && extraKeyDiagnostics && !option) { + if (knownOptions) { + errors.push(createUnknownOptionError(keyText, extraKeyDiagnostics, (message, arg0, arg1) => ts.createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1))); } - if (!isDoubleQuotedString(element.name)) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element.name, ts.Diagnostics.String_literal_with_double_quotes_expected)); + else { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); } - - const textOfKey = ts.isComputedNonLiteralName(element.name) ? undefined : ts.getTextOfPropertyName(element.name); - const keyText = textOfKey && ts.unescapeLeadingUnderscores(textOfKey); - const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; - if (keyText && extraKeyDiagnostics && !option) { - if (knownOptions) { - errors.push(createUnknownOptionError(keyText, extraKeyDiagnostics, (message, arg0, arg1) => ts.createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1))); - } - else { - errors.push(ts.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; } + return result; + } - function convertArrayLiteralExpressionToJson(elements: ts.NodeArray, elementOption: ts.CommandLineOption | undefined) { - if (!returnValue) { - elements.forEach(element => convertPropertyValueToJson(element, elementOption)); - return undefined; - } - - // Filter out invalid values - return ts.filter(elements.map(element => convertPropertyValueToJson(element, elementOption)), v => v !== undefined); + function convertArrayLiteralExpressionToJson(elements: ts.NodeArray, elementOption: ts.CommandLineOption | undefined) { + if (!returnValue) { + elements.forEach(element => convertPropertyValueToJson(element, elementOption)); + return undefined; } - function convertPropertyValueToJson(valueExpression: ts.Expression, option: ts.CommandLineOption | undefined): any { - let invalidReported: boolean | undefined; - switch (valueExpression.kind) { - case ts.SyntaxKind.TrueKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); - return validateValue(/*value*/ true); + // Filter out invalid values + return ts.filter(elements.map(element => convertPropertyValueToJson(element, elementOption)), v => v !== undefined); + } - case ts.SyntaxKind.FalseKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); - return validateValue(/*value*/ false); + function convertPropertyValueToJson(valueExpression: ts.Expression, option: ts.CommandLineOption | undefined): any { + let invalidReported: boolean | undefined; + switch (valueExpression.kind) { + case ts.SyntaxKind.TrueKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); + return validateValue(/*value*/ true); - case ts.SyntaxKind.NullKeyword: - reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for - return validateValue(/*value*/ null); // eslint-disable-line no-null/no-null + case ts.SyntaxKind.FalseKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); + return validateValue(/*value*/ false); - case ts.SyntaxKind.StringLiteral: - if (!isDoubleQuotedString(valueExpression)) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.String_literal_with_double_quotes_expected)); - } - reportInvalidOptionValue(option && (ts.isString(option.type) && option.type !== "string")); - const text = (valueExpression as ts.StringLiteral).text; - if (option && !ts.isString(option.type)) { - const customOption = option as ts.CommandLineOptionOfCustomType; - // Validate custom option type - if (!customOption.type.has(text.toLowerCase())) { - errors.push(createDiagnosticForInvalidCustomType(customOption, (message, arg0, arg1) => ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, message, arg0, arg1))); - invalidReported = true; - } - } - return validateValue(text); - - case ts.SyntaxKind.NumericLiteral: - reportInvalidOptionValue(option && option.type !== "number"); - return validateValue(Number((valueExpression as ts.NumericLiteral).text)); - case ts.SyntaxKind.PrefixUnaryExpression: - if ((valueExpression as ts.PrefixUnaryExpression).operator !== ts.SyntaxKind.MinusToken || (valueExpression as ts.PrefixUnaryExpression).operand.kind !== ts.SyntaxKind.NumericLiteral) { - break; // not valid JSON syntax - } - reportInvalidOptionValue(option && option.type !== "number"); - return validateValue(-Number(((valueExpression as ts.PrefixUnaryExpression).operand as ts.NumericLiteral).text)); - case ts.SyntaxKind.ObjectLiteralExpression: - reportInvalidOptionValue(option && option.type !== "object"); - const objectLiteralExpression = valueExpression as ts.ObjectLiteralExpression; - - // 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 as ts.TsConfigOnlyOption; - return validateValue(convertObjectLiteralExpressionToJson(objectLiteralExpression, elementOptions, extraKeyDiagnostics, optionName)); - } - else { - return validateValue(convertObjectLiteralExpressionToJson(objectLiteralExpression, /* knownOptions*/ undefined, - /*extraKeyDiagnosticMessage */ undefined, /*parentOption*/ undefined)); - } + case ts.SyntaxKind.NullKeyword: + reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for + return validateValue(/*value*/ null); // eslint-disable-line no-null/no-null - case ts.SyntaxKind.ArrayLiteralExpression: - reportInvalidOptionValue(option && option.type !== "list"); - return validateValue(convertArrayLiteralExpressionToJson((valueExpression as ts.ArrayLiteralExpression).elements, option && (option as ts.CommandLineOptionOfListType).element)); - } + case ts.SyntaxKind.StringLiteral: + if (!isDoubleQuotedString(valueExpression)) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.String_literal_with_double_quotes_expected)); + } + reportInvalidOptionValue(option && (ts.isString(option.type) && option.type !== "string")); + const text = (valueExpression as ts.StringLiteral).text; + if (option && !ts.isString(option.type)) { + const customOption = option as ts.CommandLineOptionOfCustomType; + // Validate custom option type + if (!customOption.type.has(text.toLowerCase())) { + errors.push(createDiagnosticForInvalidCustomType(customOption, (message, arg0, arg1) => ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, message, arg0, arg1))); + invalidReported = true; + } + } + return validateValue(text); + + case ts.SyntaxKind.NumericLiteral: + reportInvalidOptionValue(option && option.type !== "number"); + return validateValue(Number((valueExpression as ts.NumericLiteral).text)); + case ts.SyntaxKind.PrefixUnaryExpression: + if ((valueExpression as ts.PrefixUnaryExpression).operator !== ts.SyntaxKind.MinusToken || (valueExpression as ts.PrefixUnaryExpression).operand.kind !== ts.SyntaxKind.NumericLiteral) { + break; // not valid JSON syntax + } + reportInvalidOptionValue(option && option.type !== "number"); + return validateValue(-Number(((valueExpression as ts.PrefixUnaryExpression).operand as ts.NumericLiteral).text)); + case ts.SyntaxKind.ObjectLiteralExpression: + reportInvalidOptionValue(option && option.type !== "object"); + const objectLiteralExpression = valueExpression as ts.ObjectLiteralExpression; + + // 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 as ts.TsConfigOnlyOption; + return validateValue(convertObjectLiteralExpressionToJson(objectLiteralExpression, elementOptions, extraKeyDiagnostics, optionName)); + } + else { + return validateValue(convertObjectLiteralExpressionToJson(objectLiteralExpression, /* knownOptions*/ undefined, + /*extraKeyDiagnosticMessage */ undefined, /*parentOption*/ undefined)); + } - // Not in expected format - if (option) { - reportInvalidOptionValue(/*isError*/ true); - } - else { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)); - } + case ts.SyntaxKind.ArrayLiteralExpression: + reportInvalidOptionValue(option && option.type !== "list"); + return validateValue(convertArrayLiteralExpressionToJson((valueExpression as ts.ArrayLiteralExpression).elements, option && (option as ts.CommandLineOptionOfListType).element)); + } - return undefined; + // Not in expected format + if (option) { + reportInvalidOptionValue(/*isError*/ true); + } + else { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)); + } - function validateValue(value: ts.CompilerOptionsValue) { - if (!invalidReported) { - const diagnostic = option?.extraValidation?.(value); - if (diagnostic) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ...diagnostic)); - return undefined; - } - } - return value; - } + return undefined; - function reportInvalidOptionValue(isError: boolean | undefined) { - if (isError) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option!.name, getCompilerOptionValueTypeString(option!))); - invalidReported = true; + function validateValue(value: ts.CompilerOptionsValue) { + if (!invalidReported) { + const diagnostic = option?.extraValidation?.(value); + if (diagnostic) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ...diagnostic)); + return undefined; } } + return value; } - function isDoubleQuotedString(node: ts.Node): boolean { - return ts.isStringLiteral(node) && ts.isStringDoubleQuoted(node, sourceFile); + function reportInvalidOptionValue(isError: boolean | undefined) { + if (isError) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option!.name, getCompilerOptionValueTypeString(option!))); + invalidReported = true; + } } } - function getCompilerOptionValueTypeString(option: ts.CommandLineOption) { - return option.type === "list" ? - "Array" : - ts.isString(option.type) ? option.type : "string"; + function isDoubleQuotedString(node: ts.Node): boolean { + return ts.isStringLiteral(node) && ts.isStringDoubleQuoted(node, sourceFile); } +} - function isCompilerOptionsValue(option: ts.CommandLineOption | undefined, value: any): value is ts.CompilerOptionsValue { - if (option) { - if (isNullOrUndefined(value)) - return true; // All options are undefinable/nullable - if (option.type === "list") { - return ts.isArray(value); - } - const expectedType = ts.isString(option.type) ? option.type : "string"; - return typeof value === expectedType; +function getCompilerOptionValueTypeString(option: ts.CommandLineOption) { + return option.type === "list" ? + "Array" : + ts.isString(option.type) ? option.type : "string"; +} + +function isCompilerOptionsValue(option: ts.CommandLineOption | undefined, value: any): value is ts.CompilerOptionsValue { + if (option) { + if (isNullOrUndefined(value)) + return true; // All options are undefinable/nullable + if (option.type === "list") { + return ts.isArray(value); } - return false; + const expectedType = ts.isString(option.type) ? option.type : "string"; + return typeof value === expectedType; } + return false; +} - /** @internal */ - export interface TSConfig { - compilerOptions: ts.CompilerOptions; - compileOnSave: boolean | undefined; - exclude?: readonly string[]; - files: readonly string[] | undefined; - include?: readonly string[]; - references: readonly ts.ProjectReference[] | undefined; - } +/** @internal */ +export interface TSConfig { + compilerOptions: ts.CompilerOptions; + compileOnSave: boolean | undefined; + exclude?: readonly string[]; + files: readonly string[] | undefined; + include?: readonly string[]; + references: readonly ts.ProjectReference[] | undefined; +} - /** @internal */ - export interface ConvertToTSConfigHost { - getCurrentDirectory(): string; - useCaseSensitiveFileNames: boolean; - } +/** @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: ts.ParsedCommandLine, configFileName: string, host: ConvertToTSConfigHost): TSConfig { - const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames); - const files = ts.map(ts.filter(configParseResult.fileNames, !configParseResult.options.configFile?.configFileSpecs?.validatedIncludeSpecs ? ts.returnTrue : matchesSpecs(configFileName, configParseResult.options.configFile.configFileSpecs.validatedIncludeSpecs, configParseResult.options.configFile.configFileSpecs.validatedExcludeSpecs, host)), f => ts.getRelativePathFromFile(ts.getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), ts.getNormalizedAbsolutePath(f, host.getCurrentDirectory()), getCanonicalFileName)); - const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: ts.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: ts.map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath ? r.originalPath : "", originalPath: undefined })), - files: ts.length(files) ? files : undefined, - ...(configParseResult.options.configFile?.configFileSpecs ? { - include: filterSameAsDefaultInclude(configParseResult.options.configFile.configFileSpecs.validatedIncludeSpecs), - exclude: configParseResult.options.configFile.configFileSpecs.validatedExcludeSpecs - } : {}), - compileOnSave: !!configParseResult.compileOnSave ? true : undefined - }; - return config; - } +/** + * 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: ts.ParsedCommandLine, configFileName: string, host: ConvertToTSConfigHost): TSConfig { + const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames); + const files = ts.map(ts.filter(configParseResult.fileNames, !configParseResult.options.configFile?.configFileSpecs?.validatedIncludeSpecs ? ts.returnTrue : matchesSpecs(configFileName, configParseResult.options.configFile.configFileSpecs.validatedIncludeSpecs, configParseResult.options.configFile.configFileSpecs.validatedExcludeSpecs, host)), f => ts.getRelativePathFromFile(ts.getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), ts.getNormalizedAbsolutePath(f, host.getCurrentDirectory()), getCanonicalFileName)); + const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: ts.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: ts.map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath ? r.originalPath : "", originalPath: undefined })), + files: ts.length(files) ? files : undefined, + ...(configParseResult.options.configFile?.configFileSpecs ? { + include: filterSameAsDefaultInclude(configParseResult.options.configFile.configFileSpecs.validatedIncludeSpecs), + exclude: configParseResult.options.configFile.configFileSpecs.validatedExcludeSpecs + } : {}), + compileOnSave: !!configParseResult.compileOnSave ? true : undefined + }; + return config; +} - function optionMapToObject(optionMap: ts.ESMap): object { - return { - ...ts.arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), - }; - } +function optionMapToObject(optionMap: ts.ESMap): object { + return { + ...ts.arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), + }; +} - function filterSameAsDefaultInclude(specs: readonly string[] | undefined) { - if (!ts.length(specs)) - return undefined; - if (ts.length(specs) !== 1) - return specs; - if (specs![0] === defaultIncludeSpec) - return undefined; +function filterSameAsDefaultInclude(specs: readonly string[] | undefined) { + if (!ts.length(specs)) + return undefined; + if (ts.length(specs) !== 1) return specs; - } + if (specs![0] === defaultIncludeSpec) + return undefined; + return specs; +} - function matchesSpecs(path: string, includeSpecs: readonly string[] | undefined, excludeSpecs: readonly string[] | undefined, host: ConvertToTSConfigHost): (path: string) => boolean { - if (!includeSpecs) - return ts.returnTrue; - const patterns = ts.getFileMatcherPatterns(path, excludeSpecs, includeSpecs, host.useCaseSensitiveFileNames, host.getCurrentDirectory()); - const excludeRe = patterns.excludePattern && ts.getRegexFromPattern(patterns.excludePattern, host.useCaseSensitiveFileNames); - const includeRe = patterns.includeFilePattern && ts.getRegexFromPattern(patterns.includeFilePattern, host.useCaseSensitiveFileNames); - if (includeRe) { - if (excludeRe) { - return path => !(includeRe.test(path) && !excludeRe.test(path)); - } - return path => !includeRe.test(path); - } +function matchesSpecs(path: string, includeSpecs: readonly string[] | undefined, excludeSpecs: readonly string[] | undefined, host: ConvertToTSConfigHost): (path: string) => boolean { + if (!includeSpecs) + return ts.returnTrue; + const patterns = ts.getFileMatcherPatterns(path, excludeSpecs, includeSpecs, host.useCaseSensitiveFileNames, host.getCurrentDirectory()); + const excludeRe = patterns.excludePattern && ts.getRegexFromPattern(patterns.excludePattern, host.useCaseSensitiveFileNames); + const includeRe = patterns.includeFilePattern && ts.getRegexFromPattern(patterns.includeFilePattern, host.useCaseSensitiveFileNames); + if (includeRe) { if (excludeRe) { - return path => excludeRe.test(path); + return path => !(includeRe.test(path) && !excludeRe.test(path)); } - return ts.returnTrue; + return path => !includeRe.test(path); } - - function getCustomTypeMapOfCommandLineOption(optionDefinition: ts.CommandLineOption): ts.ESMap | 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; - } + if (excludeRe) { + return path => excludeRe.test(path); } + return ts.returnTrue; +} - function getNameOfCompilerOptionValue(value: ts.CompilerOptionsValue, customTypeMap: ts.ESMap): string | undefined { - // There is a typeMap associated with this command-line option so use it to map value back to its name - return ts.forEachEntry(customTypeMap, (mapValue, key) => { - if (mapValue === value) { - return key; - } - }); +function getCustomTypeMapOfCommandLineOption(optionDefinition: ts.CommandLineOption): ts.ESMap | undefined { + if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean" || optionDefinition.type === "object") { + // this is of a type CommandLineOptionOfPrimitiveType + return undefined; } - - function serializeCompilerOptions(options: ts.CompilerOptions, pathOptions?: { - configFilePath: string; - useCaseSensitiveFileNames: boolean; - }): ts.ESMap { - return serializeOptionBaseObject(options, getOptionsNameMap(), pathOptions); + else if (optionDefinition.type === "list") { + return getCustomTypeMapOfCommandLineOption(optionDefinition.element); } - - function serializeWatchOptions(options: ts.WatchOptions) { - return serializeOptionBaseObject(options, getWatchOptionsNameMap()); + else { + return optionDefinition.type; } +} + +function getNameOfCompilerOptionValue(value: ts.CompilerOptionsValue, customTypeMap: ts.ESMap): string | undefined { + // There is a typeMap associated with this command-line option so use it to map value back to its name + return ts.forEachEntry(customTypeMap, (mapValue, key) => { + if (mapValue === value) { + return key; + } + }); +} + +function serializeCompilerOptions(options: ts.CompilerOptions, pathOptions?: { + configFilePath: string; + useCaseSensitiveFileNames: boolean; +}): ts.ESMap { + return serializeOptionBaseObject(options, getOptionsNameMap(), pathOptions); +} - function serializeOptionBaseObject(options: OptionsBase, { optionsNameMap }: OptionsNameMap, pathOptions?: { - configFilePath: string; - useCaseSensitiveFileNames: boolean; - }): ts.ESMap { - const result = new ts.Map(); - const getCanonicalFileName = pathOptions && ts.createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); +function serializeWatchOptions(options: ts.WatchOptions) { + return serializeOptionBaseObject(options, getWatchOptionsNameMap()); +} - for (const name in options) { - if (ts.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 === ts.Diagnostics.Command_line_Options || optionsNameMap.get(name)!.category === ts.Diagnostics.Output_Formatting)) { - continue; +function serializeOptionBaseObject(options: OptionsBase, { optionsNameMap }: OptionsNameMap, pathOptions?: { + configFilePath: string; + useCaseSensitiveFileNames: boolean; +}): ts.ESMap { + const result = new ts.Map(); + const getCanonicalFileName = pathOptions && ts.createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); + + for (const name in options) { + if (ts.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 === ts.Diagnostics.Command_line_Options || optionsNameMap.get(name)!.category === ts.Diagnostics.Output_Formatting)) { + continue; + } + const value = options[name] as ts.CompilerOptionsValue; + 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, ts.getRelativePathFromFile(pathOptions.configFilePath, ts.getNormalizedAbsolutePath(value as string, ts.getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)); + } + else { + result.set(name, value); + } } - const value = options[name] as ts.CompilerOptionsValue; - 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, ts.getRelativePathFromFile(pathOptions.configFilePath, ts.getNormalizedAbsolutePath(value as string, ts.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 { - 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)); - } + // 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; } + return result; +} - /** - * Generate a list of the compiler options whose value is not the default. - * @param options compilerOptions to be evaluated. - /** @internal */ - export function getCompilerOptionsDiffValue(options: ts.CompilerOptions, newLine: string): string { - const compilerOptionsMap = getSerializedCompilerOption(options); - return getOverwrittenDefaultOptions(); - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - - function getOverwrittenDefaultOptions() { - const result: string[] = []; - const tab = makePadding(2); - commandOptionsWithoutBuild.forEach(cmd => { - if (!compilerOptionsMap.has(cmd.name)) { - return; - } - - const newValue = compilerOptionsMap.get(cmd.name); - const defaultValue = getDefaultValueForOption(cmd); - if (newValue !== defaultValue) { - result.push(`${tab}${cmd.name}: ${newValue}`); - } - else if (ts.hasProperty(defaultInitCompilerOptions, cmd.name)) { - result.push(`${tab}${cmd.name}: ${defaultValue}`); - } - }); - return result.join(newLine) + newLine; - } - } +/** + * Generate a list of the compiler options whose value is not the default. + * @param options compilerOptions to be evaluated. +/** @internal */ +export function getCompilerOptionsDiffValue(options: ts.CompilerOptions, newLine: string): string { + const compilerOptionsMap = getSerializedCompilerOption(options); + return getOverwrittenDefaultOptions(); - /** - * Get the compiler options to be written into the tsconfig.json. - * @param options commandlineOptions to be included in the compileOptions. - */ - function getSerializedCompilerOption(options: ts.CompilerOptions): ts.ESMap { - const compilerOptions = ts.extend(options, defaultInitCompilerOptions); - return serializeCompilerOptions(compilerOptions); + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); } - /** - * 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: ts.CompilerOptions, fileNames: readonly string[], newLine: string): string { - const compilerOptionsMap = getSerializedCompilerOption(options); - return writeConfigurations(); - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - function isAllowedOptionForOutput({ category, name, isCommandLineOnly }: ts.CommandLineOption): boolean { - // Skip options which do not have a category or have categories which are more niche - const categoriesToSkip = [ts.Diagnostics.Command_line_Options, ts.Diagnostics.Editor_Support, ts.Diagnostics.Compiler_Diagnostics, ts.Diagnostics.Backwards_Compatibility, ts.Diagnostics.Watch_and_Build_Modes, ts.Diagnostics.Output_Formatting]; - return !isCommandLineOnly && category !== undefined && (!categoriesToSkip.includes(category) || compilerOptionsMap.has(name)); - } - - function writeConfigurations() { - // Filter applicable options to place in the file - const categorizedOptions = ts.createMultiMap(); - for (const option of optionDeclarations) { - const { category } = option; + function getOverwrittenDefaultOptions() { + const result: string[] = []; + const tab = makePadding(2); + commandOptionsWithoutBuild.forEach(cmd => { + if (!compilerOptionsMap.has(cmd.name)) { + return; + } - if (isAllowedOptionForOutput(option)) { - categorizedOptions.add(ts.getLocaleSpecificMessage(category!), option); - } + const newValue = compilerOptionsMap.get(cmd.name); + const defaultValue = getDefaultValueForOption(cmd); + if (newValue !== defaultValue) { + result.push(`${tab}${cmd.name}: ${newValue}`); + } + else if (ts.hasProperty(defaultInitCompilerOptions, cmd.name)) { + result.push(`${tab}${cmd.name}: ${defaultValue}`); } + }); + return result.join(newLine) + newLine; + } +} - // Serialize all options and their descriptions - let marginLength = 0; - let seenKnownKeys = 0; - const entries: { - value: string; - description?: string; - }[] = []; - categorizedOptions.forEach((options, category) => { - if (entries.length !== 0) { - entries.push({ value: "" }); - } - entries.push({ value: `/* ${category} */` }); - 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))},`; - } - entries.push({ - value: optionName, - description: `/* ${option.description && ts.getLocaleSpecificMessage(option.description) || option.name} */` - }); - marginLength = Math.max(optionName.length, marginLength); +/** + * Get the compiler options to be written into the tsconfig.json. + * @param options commandlineOptions to be included in the compileOptions. + */ +function getSerializedCompilerOption(options: ts.CompilerOptions): ts.ESMap { + const compilerOptions = ts.extend(options, defaultInitCompilerOptions); + return serializeCompilerOptions(compilerOptions); +} +/** + * 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: ts.CompilerOptions, fileNames: readonly string[], newLine: string): string { + const compilerOptionsMap = getSerializedCompilerOption(options); + return writeConfigurations(); + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + + function isAllowedOptionForOutput({ category, name, isCommandLineOnly }: ts.CommandLineOption): boolean { + // Skip options which do not have a category or have categories which are more niche + const categoriesToSkip = [ts.Diagnostics.Command_line_Options, ts.Diagnostics.Editor_Support, ts.Diagnostics.Compiler_Diagnostics, ts.Diagnostics.Backwards_Compatibility, ts.Diagnostics.Watch_and_Build_Modes, ts.Diagnostics.Output_Formatting]; + return !isCommandLineOnly && category !== undefined && (!categoriesToSkip.includes(category) || compilerOptionsMap.has(name)); + } + + function writeConfigurations() { + // Filter applicable options to place in the file + const categorizedOptions = ts.createMultiMap(); + for (const option of optionDeclarations) { + const { category } = option; + + if (isAllowedOptionForOutput(option)) { + categorizedOptions.add(ts.getLocaleSpecificMessage(category!), option); + } + } + + // Serialize all options and their descriptions + let marginLength = 0; + let seenKnownKeys = 0; + const entries: { + value: string; + description?: string; + }[] = []; + categorizedOptions.forEach((options, category) => { + if (entries.length !== 0) { + entries.push({ value: "" }); + } + entries.push({ value: `/* ${category} */` }); + 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 ? "" : ","}`; } - }); - - // Write the output - const tab = makePadding(2); - const result: string[] = []; - result.push(`{`); - result.push(`${tab}"compilerOptions": {`); - result.push(`${tab}${tab}/* ${ts.getLocaleSpecificMessage(ts.Diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file)} */`); - result.push(""); - // Print out each row, aligning all the descriptions on the same column. - for (const entry of entries) { - const { value, description = "" } = entry; - result.push(value && `${tab}${tab}${value}${description && (makePadding(marginLength - value.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 { + optionName = `// "${option.name}": ${JSON.stringify(getDefaultValueForOption(option))},`; } - result.push(`${tab}]`); - } - else { - result.push(`${tab}}`); + entries.push({ + value: optionName, + description: `/* ${option.description && ts.getLocaleSpecificMessage(option.description) || option.name} */` + }); + marginLength = Math.max(optionName.length, marginLength); } - result.push(`}`); + }); - return result.join(newLine) + newLine; + // Write the output + const tab = makePadding(2); + const result: string[] = []; + result.push(`{`); + result.push(`${tab}"compilerOptions": {`); + result.push(`${tab}${tab}/* ${ts.getLocaleSpecificMessage(ts.Diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file)} */`); + result.push(""); + // Print out each row, aligning all the descriptions on the same column. + for (const entry of entries) { + const { value, description = "" } = entry; + result.push(value && `${tab}${tab}${value}${description && (makePadding(marginLength - value.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}]`); } + else { + result.push(`${tab}}`); + } + result.push(`}`); + + return result.join(newLine) + newLine; } +} - /* @internal */ - export function convertToOptionsWithAbsolutePaths(options: ts.CompilerOptions, toAbsolutePath: (path: string) => string) { - const result: ts.CompilerOptions = {}; - const optionsNameMap = getOptionsNameMap().optionsNameMap; +/* @internal */ +export function convertToOptionsWithAbsolutePaths(options: ts.CompilerOptions, toAbsolutePath: (path: string) => string) { + const result: ts.CompilerOptions = {}; + const optionsNameMap = getOptionsNameMap().optionsNameMap; - for (const name in options) { - if (ts.hasProperty(options, name)) { - result[name] = convertToOptionValueWithAbsolutePaths(optionsNameMap.get(name.toLowerCase()), options[name] as ts.CompilerOptionsValue, toAbsolutePath); - } - } - if (result.configFilePath) { - result.configFilePath = toAbsolutePath(result.configFilePath); + for (const name in options) { + if (ts.hasProperty(options, name)) { + result[name] = convertToOptionValueWithAbsolutePaths(optionsNameMap.get(name.toLowerCase()), options[name] as ts.CompilerOptionsValue, toAbsolutePath); } - return result; } + if (result.configFilePath) { + result.configFilePath = toAbsolutePath(result.configFilePath); + } + return result; +} - function convertToOptionValueWithAbsolutePaths(option: ts.CommandLineOption | undefined, value: ts.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); +function convertToOptionValueWithAbsolutePaths(option: ts.CommandLineOption | undefined, value: ts.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); } } - 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: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[], extraFileExtensions?: readonly ts.FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: ts.WatchOptions): ts.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: ts.TsConfigSourceFile, host: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[], extraFileExtensions?: readonly ts.FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: ts.WatchOptions): ts.ParsedCommandLine { - ts.tracing?.push(ts.tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); - const result = parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); - ts.tracing?.pop(); - return result; - } - - /*@internal*/ - export function setConfigFileInOptions(options: ts.CompilerOptions, configFile: ts.TsConfigSourceFile | undefined) { - if (configFile) { - Object.defineProperty(options, "configFile", { enumerable: false, writable: false, value: configFile }); + else if (option.isFilePath) { + return toAbsolutePath(value as string); } } + return value; +} - 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 ts.getDirectoryPath(ts.getNormalizedAbsolutePath(fileName, basePath)); - } +/** + * 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: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[], extraFileExtensions?: readonly ts.FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: ts.WatchOptions): ts.ParsedCommandLine { + return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); +} - /*@internal*/ - export const defaultIncludeSpec = "**/*"; +/** + * 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: ts.TsConfigSourceFile, host: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[], extraFileExtensions?: readonly ts.FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: ts.WatchOptions): ts.ParsedCommandLine { + ts.tracing?.push(ts.tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); + const result = parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); + ts.tracing?.pop(); + return result; +} - /** - * 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: ts.TsConfigSourceFile | undefined, host: ts.ParseConfigHost, basePath: string, existingOptions: ts.CompilerOptions = {}, existingWatchOptions: ts.WatchOptions | undefined, configFileName?: string, resolutionStack: ts.Path[] = [], extraFileExtensions: readonly ts.FileExtensionInfo[] = [], extendedConfigCache?: ts.ESMap): ts.ParsedCommandLine { - ts.Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); - const errors: ts.Diagnostic[] = []; - - const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); - const { raw } = parsedConfig; - const options = ts.extend(existingOptions, parsedConfig.options || {}); - const watchOptions = existingWatchOptions && parsedConfig.watchOptions ? - ts.extend(existingWatchOptions, parsedConfig.watchOptions) : - parsedConfig.watchOptions || existingWatchOptions; - - options.configFilePath = configFileName && ts.normalizeSlashes(configFileName); - const configFileSpecs = getConfigFileSpecs(); - if (sourceFile) - sourceFile.configFileSpecs = configFileSpecs; - setConfigFileInOptions(options, sourceFile); - - const basePathForFileNames = ts.normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath); - return { - options, - watchOptions, - fileNames: getFileNames(basePathForFileNames), - projectReferences: getProjectReferences(basePathForFileNames), - typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), - raw, - errors, - // 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. - wildcardDirectories: getWildcardDirectories(configFileSpecs, basePathForFileNames, host.useCaseSensitiveFileNames), - compileOnSave: !!raw.compileOnSave, - }; +/*@internal*/ +export function setConfigFileInOptions(options: ts.CompilerOptions, configFile: ts.TsConfigSourceFile | undefined) { + if (configFile) { + Object.defineProperty(options, "configFile", { enumerable: false, writable: false, value: configFile }); + } +} - function getConfigFileSpecs(): ts.ConfigFileSpecs { - const referencesOfRaw = getPropFromRaw("references", element => typeof element === "object", "object"); - const filesSpecs = toPropValue(getSpecsFromRaw("files")); - if (filesSpecs) { - const hasZeroOrNoReferences = referencesOfRaw === "no-prop" || ts.isArray(referencesOfRaw) && referencesOfRaw.length === 0; - const hasExtends = ts.hasProperty(raw, "extends"); - if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) { - if (sourceFile) { - const fileName = configFileName || "tsconfig.json"; - const diagnosticMessage = ts.Diagnostics.The_files_list_in_config_file_0_is_empty; - const nodeValue = ts.firstDefined(ts.getTsConfigPropArray(sourceFile, "files"), property => property.initializer); - const error = nodeValue - ? ts.createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName) - : ts.createCompilerDiagnostic(diagnosticMessage, fileName); - errors.push(error); - } - else { - createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); - } - } - } +function isNullOrUndefined(x: any): x is null | undefined { + return x === undefined || x === null; // eslint-disable-line no-null/no-null +} - let includeSpecs = toPropValue(getSpecsFromRaw("include")); +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 ts.getDirectoryPath(ts.getNormalizedAbsolutePath(fileName, basePath)); +} - const excludeOfRaw = getSpecsFromRaw("exclude"); - let isDefaultIncludeSpec = false; - let excludeSpecs = toPropValue(excludeOfRaw); - if (excludeOfRaw === "no-prop" && raw.compilerOptions) { - const outDir = raw.compilerOptions.outDir; - const declarationDir = raw.compilerOptions.declarationDir; +/*@internal*/ +export const defaultIncludeSpec = "**/*"; + +/** + * 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: ts.TsConfigSourceFile | undefined, host: ts.ParseConfigHost, basePath: string, existingOptions: ts.CompilerOptions = {}, existingWatchOptions: ts.WatchOptions | undefined, configFileName?: string, resolutionStack: ts.Path[] = [], extraFileExtensions: readonly ts.FileExtensionInfo[] = [], extendedConfigCache?: ts.ESMap): ts.ParsedCommandLine { + ts.Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); + const errors: ts.Diagnostic[] = []; + + const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); + const { raw } = parsedConfig; + const options = ts.extend(existingOptions, parsedConfig.options || {}); + const watchOptions = existingWatchOptions && parsedConfig.watchOptions ? + ts.extend(existingWatchOptions, parsedConfig.watchOptions) : + parsedConfig.watchOptions || existingWatchOptions; + + options.configFilePath = configFileName && ts.normalizeSlashes(configFileName); + const configFileSpecs = getConfigFileSpecs(); + if (sourceFile) + sourceFile.configFileSpecs = configFileSpecs; + setConfigFileInOptions(options, sourceFile); + + const basePathForFileNames = ts.normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath); + return { + options, + watchOptions, + fileNames: getFileNames(basePathForFileNames), + projectReferences: getProjectReferences(basePathForFileNames), + typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), + raw, + errors, + // 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. + wildcardDirectories: getWildcardDirectories(configFileSpecs, basePathForFileNames, host.useCaseSensitiveFileNames), + compileOnSave: !!raw.compileOnSave, + }; - if (outDir || declarationDir) { - excludeSpecs = [outDir, declarationDir].filter(d => !!d); + function getConfigFileSpecs(): ts.ConfigFileSpecs { + const referencesOfRaw = getPropFromRaw("references", element => typeof element === "object", "object"); + const filesSpecs = toPropValue(getSpecsFromRaw("files")); + if (filesSpecs) { + const hasZeroOrNoReferences = referencesOfRaw === "no-prop" || ts.isArray(referencesOfRaw) && referencesOfRaw.length === 0; + const hasExtends = ts.hasProperty(raw, "extends"); + if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) { + if (sourceFile) { + const fileName = configFileName || "tsconfig.json"; + const diagnosticMessage = ts.Diagnostics.The_files_list_in_config_file_0_is_empty; + const nodeValue = ts.firstDefined(ts.getTsConfigPropArray(sourceFile, "files"), property => property.initializer); + const error = nodeValue + ? ts.createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName) + : ts.createCompilerDiagnostic(diagnosticMessage, fileName); + errors.push(error); + } + else { + createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); } } + } - if (filesSpecs === undefined && includeSpecs === undefined) { - includeSpecs = [defaultIncludeSpec]; - isDefaultIncludeSpec = true; - } - 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. + let includeSpecs = toPropValue(getSpecsFromRaw("include")); - if (includeSpecs) { - validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include"); - } + const excludeOfRaw = getSpecsFromRaw("exclude"); + let isDefaultIncludeSpec = false; + let excludeSpecs = toPropValue(excludeOfRaw); + if (excludeOfRaw === "no-prop" && raw.compilerOptions) { + const outDir = raw.compilerOptions.outDir; + const declarationDir = raw.compilerOptions.declarationDir; - if (excludeSpecs) { - validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude"); + if (outDir || declarationDir) { + excludeSpecs = [outDir, declarationDir].filter(d => !!d); } - - return { - filesSpecs, - includeSpecs, - excludeSpecs, - validatedFilesSpec: ts.filter(filesSpecs, ts.isString), - validatedIncludeSpecs, - validatedExcludeSpecs, - pathPatterns: undefined, - isDefaultIncludeSpec, - }; } - function getFileNames(basePath: string): string[] { - const fileNames = getFileNamesFromConfigSpecs(configFileSpecs, basePath, options, host, extraFileExtensions); - if (shouldReportNoInputFiles(fileNames, canJsonReportNoInputFiles(raw), resolutionStack)) { - errors.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); - } - return fileNames; + if (filesSpecs === undefined && includeSpecs === undefined) { + includeSpecs = [defaultIncludeSpec]; + isDefaultIncludeSpec = true; } + let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined; - function getProjectReferences(basePath: string): readonly ts.ProjectReference[] | undefined { - let projectReferences: ts.ProjectReference[] | undefined; - const referencesOfRaw = getPropFromRaw("references", element => typeof element === "object", "object"); - if (ts.isArray(referencesOfRaw)) { - for (const ref of referencesOfRaw) { - if (typeof ref.path !== "string") { - createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string"); - } - else { - (projectReferences || (projectReferences = [])).push({ - path: ts.getNormalizedAbsolutePath(ref.path, basePath), - originalPath: ref.path, - prepend: ref.prepend, - circular: ref.circular - }); - } - } - } - return projectReferences; + // 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, /*disallowTrailingRecursion*/ true, sourceFile, "include"); } - type PropOfRaw = readonly T[] | "not-array" | "no-prop"; - function toPropValue(specResult: PropOfRaw) { - return ts.isArray(specResult) ? specResult : undefined; + if (excludeSpecs) { + validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude"); } - function getSpecsFromRaw(prop: "files" | "include" | "exclude"): PropOfRaw { - return getPropFromRaw(prop, ts.isString, "string"); + return { + filesSpecs, + includeSpecs, + excludeSpecs, + validatedFilesSpec: ts.filter(filesSpecs, ts.isString), + validatedIncludeSpecs, + validatedExcludeSpecs, + pathPatterns: undefined, + isDefaultIncludeSpec, + }; + } + + function getFileNames(basePath: string): string[] { + const fileNames = getFileNamesFromConfigSpecs(configFileSpecs, basePath, options, host, extraFileExtensions); + if (shouldReportNoInputFiles(fileNames, canJsonReportNoInputFiles(raw), resolutionStack)) { + errors.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); } + return fileNames; + } - function getPropFromRaw(prop: "files" | "include" | "exclude" | "references", validateElement: (value: unknown) => boolean, elementTypeName: string): PropOfRaw { - if (ts.hasProperty(raw, prop) && !isNullOrUndefined(raw[prop])) { - if (ts.isArray(raw[prop])) { - const result = raw[prop] as T[]; - if (!sourceFile && !ts.every(result, validateElement)) { - errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, elementTypeName)); - } - return result; + function getProjectReferences(basePath: string): readonly ts.ProjectReference[] | undefined { + let projectReferences: ts.ProjectReference[] | undefined; + const referencesOfRaw = getPropFromRaw("references", element => typeof element === "object", "object"); + if (ts.isArray(referencesOfRaw)) { + for (const ref of referencesOfRaw) { + if (typeof ref.path !== "string") { + createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string"); } else { - createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, "Array"); - return "not-array"; + (projectReferences || (projectReferences = [])).push({ + path: ts.getNormalizedAbsolutePath(ref.path, basePath), + originalPath: ref.path, + prepend: ref.prepend, + circular: ref.circular + }); } } - return "no-prop"; - } - - function createCompilerDiagnosticOnlyIfJson(message: ts.DiagnosticMessage, arg0?: string, arg1?: string) { - if (!sourceFile) { - errors.push(ts.createCompilerDiagnostic(message, arg0, arg1)); - } } + return projectReferences; } - function isErrorNoInputFiles(error: ts.Diagnostic) { - return error.code === ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; - } - - function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ts.ConfigFileSpecs, configFileName: string | undefined) { - return ts.createCompilerDiagnostic(ts.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 || [])); + type PropOfRaw = readonly T[] | "not-array" | "no-prop"; + function toPropValue(specResult: PropOfRaw) { + return ts.isArray(specResult) ? specResult : undefined; } - function shouldReportNoInputFiles(fileNames: string[], canJsonReportNoInutFiles: boolean, resolutionStack?: ts.Path[]) { - return fileNames.length === 0 && canJsonReportNoInutFiles && (!resolutionStack || resolutionStack.length === 0); + function getSpecsFromRaw(prop: "files" | "include" | "exclude"): PropOfRaw { + return getPropFromRaw(prop, ts.isString, "string"); } - /*@internal*/ - export function canJsonReportNoInputFiles(raw: any) { - return !ts.hasProperty(raw, "files") && !ts.hasProperty(raw, "references"); + function getPropFromRaw(prop: "files" | "include" | "exclude" | "references", validateElement: (value: unknown) => boolean, elementTypeName: string): PropOfRaw { + if (ts.hasProperty(raw, prop) && !isNullOrUndefined(raw[prop])) { + if (ts.isArray(raw[prop])) { + const result = raw[prop] as T[]; + if (!sourceFile && !ts.every(result, validateElement)) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, elementTypeName)); + } + return result; + } + else { + createCompilerDiagnosticOnlyIfJson(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, "Array"); + return "not-array"; + } + } + return "no-prop"; } - /*@internal*/ - export function updateErrorForNoInputFiles(fileNames: string[], configFileName: string, configFileSpecs: ts.ConfigFileSpecs, configParseDiagnostics: ts.Diagnostic[], canJsonReportNoInutFiles: boolean) { - const existingErrors = configParseDiagnostics.length; - if (shouldReportNoInputFiles(fileNames, canJsonReportNoInutFiles)) { - configParseDiagnostics.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); + function createCompilerDiagnosticOnlyIfJson(message: ts.DiagnosticMessage, arg0?: string, arg1?: string) { + if (!sourceFile) { + errors.push(ts.createCompilerDiagnostic(message, arg0, arg1)); } - else { - ts.filterMutate(configParseDiagnostics, error => !isErrorNoInputFiles(error)); - } - return existingErrors !== configParseDiagnostics.length; } +} - export interface ParsedTsconfig { - raw: any; - options?: ts.CompilerOptions; - watchOptions?: ts.WatchOptions; - typeAcquisition?: ts.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 isErrorNoInputFiles(error: ts.Diagnostic) { + return error.code === ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; +} + +function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ts.ConfigFileSpecs, configFileName: string | undefined) { + return ts.createCompilerDiagnostic(ts.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 isSuccessfulParsedTsconfig(value: ParsedTsconfig) { - return !!value.options; +function shouldReportNoInputFiles(fileNames: string[], canJsonReportNoInutFiles: boolean, resolutionStack?: ts.Path[]) { + return fileNames.length === 0 && canJsonReportNoInutFiles && (!resolutionStack || resolutionStack.length === 0); +} + +/*@internal*/ +export function canJsonReportNoInputFiles(raw: any) { + return !ts.hasProperty(raw, "files") && !ts.hasProperty(raw, "references"); +} + +/*@internal*/ +export function updateErrorForNoInputFiles(fileNames: string[], configFileName: string, configFileSpecs: ts.ConfigFileSpecs, configParseDiagnostics: ts.Diagnostic[], canJsonReportNoInutFiles: boolean) { + const existingErrors = configParseDiagnostics.length; + if (shouldReportNoInputFiles(fileNames, canJsonReportNoInutFiles)) { + configParseDiagnostics.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); } + else { + ts.filterMutate(configParseDiagnostics, error => !isErrorNoInputFiles(error)); + } + return existingErrors !== configParseDiagnostics.length; +} +export interface ParsedTsconfig { + raw: any; + options?: ts.CompilerOptions; + watchOptions?: ts.WatchOptions; + typeAcquisition?: ts.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: ts.TsConfigSourceFile | undefined, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, resolutionStack: string[], errors: ts.Push, extendedConfigCache?: ts.ESMap): ParsedTsconfig { - basePath = ts.normalizeSlashes(basePath); - const resolvedPath = ts.getNormalizedAbsolutePath(configFileName || "", basePath); - - if (resolutionStack.indexOf(resolvedPath) >= 0) { - errors.push(ts.createCompilerDiagnostic(ts.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.options?.paths) { - // If we end up needing to resolve relative paths from 'paths' relative to - // the config file location, we'll need to know where that config file was. - // Since 'paths' can be inherited from an extended config in another directory, - // we wouldn't know which directory to use unless we store it here. - ownConfig.options.pathsBasePath = basePath; - } - 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, resolutionStack, errors, extendedConfigCache); - if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { - const baseRaw = extendedConfig.raw; - const raw = ownConfig.raw; - let relativeDifference: string | undefined ; - const setPropertyInRawIfNotUndefined = (propertyName: string) => { - if (!raw[propertyName] && baseRaw[propertyName]) { - raw[propertyName] = ts.map(baseRaw[propertyName], (path: string) => ts.isRootedDiskPath(path) ? path : ts.combinePaths(relativeDifference ||= ts.convertToRelativePath(ts.getDirectoryPath(ownConfig.extendedConfigPath!), basePath, ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames)), path)); - } - }; - 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: ts.TsConfigSourceFile | undefined, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, resolutionStack: string[], errors: ts.Push, extendedConfigCache?: ts.ESMap): ParsedTsconfig { + basePath = ts.normalizeSlashes(basePath); + const resolvedPath = ts.getNormalizedAbsolutePath(configFileName || "", basePath); + + if (resolutionStack.indexOf(resolvedPath) >= 0) { + errors.push(ts.createCompilerDiagnostic(ts.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.options?.paths) { + // If we end up needing to resolve relative paths from 'paths' relative to + // the config file location, we'll need to know where that config file was. + // Since 'paths' can be inherited from an extended config in another directory, + // we wouldn't know which directory to use unless we store it here. + ownConfig.options.pathsBasePath = basePath; + } + 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, resolutionStack, errors, extendedConfigCache); + if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { + const baseRaw = extendedConfig.raw; + const raw = ownConfig.raw; + let relativeDifference: string | undefined ; + const setPropertyInRawIfNotUndefined = (propertyName: string) => { + if (!raw[propertyName] && baseRaw[propertyName]) { + raw[propertyName] = ts.map(baseRaw[propertyName], (path: string) => ts.isRootedDiskPath(path) ? path : ts.combinePaths(relativeDifference ||= ts.convertToRelativePath(ts.getDirectoryPath(ownConfig.extendedConfigPath!), basePath, ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames)), path)); } - ownConfig.options = ts.assign({}, extendedConfig.options, ownConfig.options); - ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? - ts.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 = ts.assign({}, extendedConfig.options, ownConfig.options); + ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? + ts.assign({}, extendedConfig.watchOptions, ownConfig.watchOptions) : + ownConfig.watchOptions || extendedConfig.watchOptions; + // TODO extend type typeAcquisition } + } + + return ownConfig; +} - return ownConfig; +function parseOwnConfigOfJson(json: any, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, errors: ts.Push): ParsedTsconfig { + if (ts.hasProperty(json, "excludes")) { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); } - function parseOwnConfigOfJson(json: any, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, errors: ts.Push): ParsedTsconfig { - if (ts.hasProperty(json, "excludes")) { - errors.push(ts.createCompilerDiagnostic(ts.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; - 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 (!ts.isString(json.extends)) { + errors.push(ts.createCompilerDiagnostic(ts.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, ts.createCompilerDiagnostic); + } + } + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; +} - if (json.extends) { - if (!ts.isString(json.extends)) { - errors.push(ts.createCompilerDiagnostic(ts.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, ts.createCompilerDiagnostic); +function parseOwnConfigOfJsonSourceFile(sourceFile: ts.TsConfigSourceFile, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, errors: ts.Push): ParsedTsconfig { + const options = getDefaultCompilerOptions(configFileName); + let typeAcquisition: ts.TypeAcquisition | undefined, typingOptionstypeAcquisition: ts.TypeAcquisition | undefined; + let watchOptions: ts.WatchOptions | undefined; + let extendedConfigPath: string | undefined; + let rootCompilerOptions: ts.PropertyName[] | undefined; + + const optionsIterator: JsonConversionNotifier = { + onSetValidOptionKeyValueInParent(parentOption: string, option: ts.CommandLineOption, value: ts.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: + ts.Debug.fail("Unknown option"); } - } - return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; - } - - function parseOwnConfigOfJsonSourceFile(sourceFile: ts.TsConfigSourceFile, host: ts.ParseConfigHost, basePath: string, configFileName: string | undefined, errors: ts.Push): ParsedTsconfig { - const options = getDefaultCompilerOptions(configFileName); - let typeAcquisition: ts.TypeAcquisition | undefined, typingOptionstypeAcquisition: ts.TypeAcquisition | undefined; - let watchOptions: ts.WatchOptions | undefined; - let extendedConfigPath: string | undefined; - let rootCompilerOptions: ts.PropertyName[] | undefined; - - const optionsIterator: JsonConversionNotifier = { - onSetValidOptionKeyValueInParent(parentOption: string, option: ts.CommandLineOption, value: ts.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: - ts.Debug.fail("Unknown option"); - } - currentOption[option.name] = normalizeOptionValue(option, basePath, value); - }, - onSetValidOptionKeyValueInRoot(key: string, _keyNode: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.Expression) { - switch (key) { - case "extends": - const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; - extendedConfigPath = getExtendsConfigPath(value as string, host, newBase, errors, (message, arg0) => ts.createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0)); - return; - } - }, - onSetUnknownOptionKeyValueInRoot(key: string, keyNode: ts.PropertyName, _value: ts.CompilerOptionsValue, _valueNode: ts.Expression) { - if (key === "excludes") { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, keyNode, ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); - } - if (ts.find(commandOptionsWithoutBuild, (opt) => opt.name === key)) { - rootCompilerOptions = ts.append(rootCompilerOptions, keyNode); - } + currentOption[option.name] = normalizeOptionValue(option, basePath, value); + }, + onSetValidOptionKeyValueInRoot(key: string, _keyNode: ts.PropertyName, value: ts.CompilerOptionsValue, valueNode: ts.Expression) { + switch (key) { + case "extends": + const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; + extendedConfigPath = getExtendsConfigPath(value as string, host, newBase, errors, (message, arg0) => ts.createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0)); + return; } - }; - const json = convertConfigFileToObject(sourceFile, errors, /*reportOptionsErrors*/ true, optionsIterator); - - if (!typeAcquisition) { - if (typingOptionstypeAcquisition) { - typeAcquisition = (typingOptionstypeAcquisition.enableAutoDiscovery !== undefined) ? - { - enable: typingOptionstypeAcquisition.enableAutoDiscovery, - include: typingOptionstypeAcquisition.include, - exclude: typingOptionstypeAcquisition.exclude - } : - typingOptionstypeAcquisition; + }, + onSetUnknownOptionKeyValueInRoot(key: string, keyNode: ts.PropertyName, _value: ts.CompilerOptionsValue, _valueNode: ts.Expression) { + if (key === "excludes") { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, keyNode, ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); } - else { - typeAcquisition = getDefaultTypeAcquisition(configFileName); + if (ts.find(commandOptionsWithoutBuild, (opt) => opt.name === key)) { + rootCompilerOptions = ts.append(rootCompilerOptions, keyNode); } } - - if (rootCompilerOptions && json && json.compilerOptions === undefined) { - errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, rootCompilerOptions[0], ts.Diagnostics._0_should_be_set_inside_the_compilerOptions_object_of_the_config_json_file, ts.getTextOfPropertyName(rootCompilerOptions[0]) as string)); + }; + const json = convertConfigFileToObject(sourceFile, errors, /*reportOptionsErrors*/ true, optionsIterator); + + if (!typeAcquisition) { + if (typingOptionstypeAcquisition) { + typeAcquisition = (typingOptionstypeAcquisition.enableAutoDiscovery !== undefined) ? + { + enable: typingOptionstypeAcquisition.enableAutoDiscovery, + include: typingOptionstypeAcquisition.include, + exclude: typingOptionstypeAcquisition.exclude + } : + typingOptionstypeAcquisition; + } + else { + typeAcquisition = getDefaultTypeAcquisition(configFileName); } + } - return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; + if (rootCompilerOptions && json && json.compilerOptions === undefined) { + errors.push(ts.createDiagnosticForNodeInSourceFile(sourceFile, rootCompilerOptions[0], ts.Diagnostics._0_should_be_set_inside_the_compilerOptions_object_of_the_config_json_file, ts.getTextOfPropertyName(rootCompilerOptions[0]) as string)); } - function getExtendsConfigPath(extendedConfig: string, host: ts.ParseConfigHost, basePath: string, errors: ts.Push, createDiagnostic: (message: ts.DiagnosticMessage, arg1?: string) => ts.Diagnostic) { - extendedConfig = ts.normalizeSlashes(extendedConfig); - if (ts.isRootedDiskPath(extendedConfig) || ts.startsWith(extendedConfig, "./") || ts.startsWith(extendedConfig, "../")) { - let extendedConfigPath = ts.getNormalizedAbsolutePath(extendedConfig, basePath); - if (!host.fileExists(extendedConfigPath) && !ts.endsWith(extendedConfigPath, ts.Extension.Json)) { - extendedConfigPath = `${extendedConfigPath}.json`; - if (!host.fileExists(extendedConfigPath)) { - errors.push(createDiagnostic(ts.Diagnostics.File_0_not_found, extendedConfig)); - return undefined; - } + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; +} + +function getExtendsConfigPath(extendedConfig: string, host: ts.ParseConfigHost, basePath: string, errors: ts.Push, createDiagnostic: (message: ts.DiagnosticMessage, arg1?: string) => ts.Diagnostic) { + extendedConfig = ts.normalizeSlashes(extendedConfig); + if (ts.isRootedDiskPath(extendedConfig) || ts.startsWith(extendedConfig, "./") || ts.startsWith(extendedConfig, "../")) { + let extendedConfigPath = ts.getNormalizedAbsolutePath(extendedConfig, basePath); + if (!host.fileExists(extendedConfigPath) && !ts.endsWith(extendedConfigPath, ts.Extension.Json)) { + extendedConfigPath = `${extendedConfigPath}.json`; + if (!host.fileExists(extendedConfigPath)) { + errors.push(createDiagnostic(ts.Diagnostics.File_0_not_found, extendedConfig)); + return undefined; } - return extendedConfigPath; - } - // If the path isn't a rooted or relative path, resolve like a module - const resolved = ts.nodeModuleNameResolver(extendedConfig, ts.combinePaths(basePath, "tsconfig.json"), { moduleResolution: ts.ModuleResolutionKind.NodeJs }, host, /*cache*/ undefined, /*projectRefs*/ undefined, /*lookupConfig*/ true); - if (resolved.resolvedModule) { - return resolved.resolvedModule.resolvedFileName; } - errors.push(createDiagnostic(ts.Diagnostics.File_0_not_found, extendedConfig)); - return undefined; + return extendedConfigPath; } - - export interface ExtendedConfigCacheEntry { - extendedResult: ts.TsConfigSourceFile; - extendedConfig: ParsedTsconfig | undefined; + // If the path isn't a rooted or relative path, resolve like a module + const resolved = ts.nodeModuleNameResolver(extendedConfig, ts.combinePaths(basePath, "tsconfig.json"), { moduleResolution: ts.ModuleResolutionKind.NodeJs }, host, /*cache*/ undefined, /*projectRefs*/ undefined, /*lookupConfig*/ true); + if (resolved.resolvedModule) { + return resolved.resolvedModule.resolvedFileName; } + errors.push(createDiagnostic(ts.Diagnostics.File_0_not_found, extendedConfig)); + return undefined; +} - function getExtendedConfig(sourceFile: ts.TsConfigSourceFile | undefined, extendedConfigPath: string, host: ts.ParseConfigHost, resolutionStack: string[], errors: ts.Push, extendedConfigCache?: ts.ESMap): ParsedTsconfig | undefined { - const path = host.useCaseSensitiveFileNames ? extendedConfigPath : ts.toFileNameLowerCase(extendedConfigPath); - let value: ExtendedConfigCacheEntry | undefined; - let extendedResult: ts.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) { - extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, ts.getDirectoryPath(extendedConfigPath), ts.getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache); - } - if (extendedConfigCache) { - extendedConfigCache.set(path, { extendedResult, extendedConfig }); - } - } - if (sourceFile) { - sourceFile.extendedSourceFiles = [extendedResult.fileName]; - if (extendedResult.extendedSourceFiles) { - sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); - } +export interface ExtendedConfigCacheEntry { + extendedResult: ts.TsConfigSourceFile; + extendedConfig: ParsedTsconfig | undefined; +} + +function getExtendedConfig(sourceFile: ts.TsConfigSourceFile | undefined, extendedConfigPath: string, host: ts.ParseConfigHost, resolutionStack: string[], errors: ts.Push, extendedConfigCache?: ts.ESMap): ParsedTsconfig | undefined { + const path = host.useCaseSensitiveFileNames ? extendedConfigPath : ts.toFileNameLowerCase(extendedConfigPath); + let value: ExtendedConfigCacheEntry | undefined; + let extendedResult: ts.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) { + extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, ts.getDirectoryPath(extendedConfigPath), ts.getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache); } - 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: ts.Push): boolean { - if (!ts.hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) { - return false; + if (sourceFile) { + sourceFile.extendedSourceFiles = [extendedResult.fileName]; + if (extendedResult.extendedSourceFiles) { + sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); } - const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption.compileOnSave, basePath, errors); - return typeof result === "boolean" && result; } - - export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { - options: ts.CompilerOptions; - errors: ts.Diagnostic[]; - } { - const errors: ts.Diagnostic[] = []; - const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName); - return { options, errors }; + if (extendedResult.parseDiagnostics.length) { + errors.push(...extendedResult.parseDiagnostics); + return undefined; } + return extendedConfig!; +} - export function convertTypeAcquisitionFromJson(jsonOptions: any, basePath: string, configFileName?: string): { - options: ts.TypeAcquisition; - errors: ts.Diagnostic[]; - } { - const errors: ts.Diagnostic[] = []; - const options = convertTypeAcquisitionFromJsonWorker(jsonOptions, basePath, errors, configFileName); - return { options, errors }; +function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: ts.Push): boolean { + if (!ts.hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) { + return false; } + const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption.compileOnSave, basePath, errors); + return typeof result === "boolean" && result; +} - function getDefaultCompilerOptions(configFileName?: string) { - const options: ts.CompilerOptions = configFileName && ts.getBaseFileName(configFileName) === "jsconfig.json" - ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true } - : {}; - return options; - } +export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { + options: ts.CompilerOptions; + errors: ts.Diagnostic[]; +} { + const errors: ts.Diagnostic[] = []; + const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName); + return { options, errors }; +} - function convertCompilerOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push, configFileName?: string): ts.CompilerOptions { +export function convertTypeAcquisitionFromJson(jsonOptions: any, basePath: string, configFileName?: string): { + options: ts.TypeAcquisition; + errors: ts.Diagnostic[]; +} { + const errors: ts.Diagnostic[] = []; + const options = convertTypeAcquisitionFromJsonWorker(jsonOptions, basePath, errors, configFileName); + return { options, errors }; +} - const options = getDefaultCompilerOptions(configFileName); - convertOptionsFromJson(getCommandLineCompilerOptionsMap(), jsonOptions, basePath, options, compilerOptionsDidYouMeanDiagnostics, errors); - if (configFileName) { - options.configFilePath = ts.normalizeSlashes(configFileName); - } - return options; - } +function getDefaultCompilerOptions(configFileName?: string) { + const options: ts.CompilerOptions = configFileName && ts.getBaseFileName(configFileName) === "jsconfig.json" + ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true } + : {}; + return options; +} - function getDefaultTypeAcquisition(configFileName?: string): ts.TypeAcquisition { - return { enable: !!configFileName && ts.getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; +function convertCompilerOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push, configFileName?: string): ts.CompilerOptions { + + const options = getDefaultCompilerOptions(configFileName); + convertOptionsFromJson(getCommandLineCompilerOptionsMap(), jsonOptions, basePath, options, compilerOptionsDidYouMeanDiagnostics, errors); + if (configFileName) { + options.configFilePath = ts.normalizeSlashes(configFileName); } + return options; +} - function convertTypeAcquisitionFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push, configFileName?: string): ts.TypeAcquisition { +function getDefaultTypeAcquisition(configFileName?: string): ts.TypeAcquisition { + return { enable: !!configFileName && ts.getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; +} - const options = getDefaultTypeAcquisition(configFileName); - const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); +function convertTypeAcquisitionFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push, configFileName?: string): ts.TypeAcquisition { - convertOptionsFromJson(getCommandLineTypeAcquisitionMap(), typeAcquisition, basePath, options, typeAcquisitionDidYouMeanDiagnostics, errors); - return options; - } + const options = getDefaultTypeAcquisition(configFileName); + const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); - function convertWatchOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push): ts.WatchOptions | undefined { - return convertOptionsFromJson(getCommandLineWatchOptionsMap(), jsonOptions, basePath, /*defaultOptions*/ undefined, watchOptionsDidYouMeanDiagnostics, errors); - } + convertOptionsFromJson(getCommandLineTypeAcquisitionMap(), typeAcquisition, basePath, options, typeAcquisitionDidYouMeanDiagnostics, errors); + return options; +} - function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: undefined, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push): ts.WatchOptions | undefined; - function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: ts.CompilerOptions | ts.TypeAcquisition, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push): ts.CompilerOptions | ts.TypeAcquisition; - function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: ts.CompilerOptions | ts.TypeAcquisition | ts.WatchOptions | undefined, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push) { +function convertWatchOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: ts.Push): ts.WatchOptions | undefined { + return convertOptionsFromJson(getCommandLineWatchOptionsMap(), jsonOptions, basePath, /*defaultOptions*/ undefined, watchOptionsDidYouMeanDiagnostics, errors); +} - if (!jsonOptions) { - return; - } +function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: undefined, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push): ts.WatchOptions | undefined; +function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: ts.CompilerOptions | ts.TypeAcquisition, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push): ts.CompilerOptions | ts.TypeAcquisition; +function convertOptionsFromJson(optionsNameMap: ts.ESMap, jsonOptions: any, basePath: string, defaultOptions: ts.CompilerOptions | ts.TypeAcquisition | ts.WatchOptions | undefined, diagnostics: ts.DidYouMeanOptionsDiagnostics, errors: ts.Push) { - 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, ts.createCompilerDiagnostic)); - } - } - return defaultOptions; + if (!jsonOptions) { + return; } - /*@internal*/ - export function convertJsonOption(opt: ts.CommandLineOption, value: any, basePath: string, errors: ts.Push): ts.CompilerOptionsValue { - if (isCompilerOptionsValue(opt, value)) { - const optType = opt.type; - if (optType === "list" && ts.isArray(value)) { - return convertJsonOptionOfListType(opt , value, basePath, errors); - } - else if (!ts.isString(optType)) { - return convertJsonOptionOfCustomType(opt as ts.CommandLineOptionOfCustomType, value as string, errors); - } - const validatedValue = validateJsonOptionValue(opt, value, errors); - return isNullOrUndefined(validatedValue) ? validatedValue : normalizeNonListOptionValue(opt, basePath, validatedValue); + 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(ts.createCompilerDiagnostic(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); + errors.push(createUnknownOptionError(id, diagnostics, ts.createCompilerDiagnostic)); } } + return defaultOptions; +} - function normalizeOptionValue(option: ts.CommandLineOption, basePath: string, value: any): ts.CompilerOptionsValue { - if (isNullOrUndefined(value)) - return undefined; - if (option.type === "list") { - const listOption = option; - if (listOption.element.isFilePath || !ts.isString(listOption.element.type)) { - return ts.filter(ts.map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => listOption.listPreserveFalsyValues ? true : !!v) as ts.CompilerOptionsValue; - } - return value; +/*@internal*/ +export function convertJsonOption(opt: ts.CommandLineOption, value: any, basePath: string, errors: ts.Push): ts.CompilerOptionsValue { + if (isCompilerOptionsValue(opt, value)) { + const optType = opt.type; + if (optType === "list" && ts.isArray(value)) { + return convertJsonOptionOfListType(opt , value, basePath, errors); } - else if (!ts.isString(option.type)) { - return option.type.get(ts.isString(value) ? value.toLowerCase() : value); + else if (!ts.isString(optType)) { + return convertJsonOptionOfCustomType(opt as ts.CommandLineOptionOfCustomType, value as string, errors); } - return normalizeNonListOptionValue(option, basePath, value); + const validatedValue = validateJsonOptionValue(opt, value, errors); + return isNullOrUndefined(validatedValue) ? validatedValue : normalizeNonListOptionValue(opt, basePath, validatedValue); + } + else { + errors.push(ts.createCompilerDiagnostic(ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); } +} - function normalizeNonListOptionValue(option: ts.CommandLineOption, basePath: string, value: any): ts.CompilerOptionsValue { - if (option.isFilePath) { - value = ts.getNormalizedAbsolutePath(value, basePath); - if (value === "") { - value = "."; - } +function normalizeOptionValue(option: ts.CommandLineOption, basePath: string, value: any): ts.CompilerOptionsValue { + if (isNullOrUndefined(value)) + return undefined; + if (option.type === "list") { + const listOption = option; + if (listOption.element.isFilePath || !ts.isString(listOption.element.type)) { + return ts.filter(ts.map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => listOption.listPreserveFalsyValues ? true : !!v) as ts.CompilerOptionsValue; } return value; } - - function validateJsonOptionValue(opt: ts.CommandLineOption, value: T, errors: ts.Push): T | undefined { - if (isNullOrUndefined(value)) - return undefined; - const d = opt.extraValidation?.(value); - if (!d) - return value; - errors.push(ts.createCompilerDiagnostic(...d)); - return undefined; + else if (!ts.isString(option.type)) { + return option.type.get(ts.isString(value) ? value.toLowerCase() : value); } + return normalizeNonListOptionValue(option, basePath, value); +} - function convertJsonOptionOfCustomType(opt: ts.CommandLineOptionOfCustomType, value: string, errors: ts.Push) { - if (isNullOrUndefined(value)) - return undefined; - const key = value.toLowerCase(); - const val = opt.type.get(key); - if (val !== undefined) { - return validateJsonOptionValue(opt, val, errors); - } - else { - errors.push(createCompilerDiagnosticForInvalidCustomType(opt)); +function normalizeNonListOptionValue(option: ts.CommandLineOption, basePath: string, value: any): ts.CompilerOptionsValue { + if (option.isFilePath) { + value = ts.getNormalizedAbsolutePath(value, basePath); + if (value === "") { + value = "."; } } + return value; +} - function convertJsonOptionOfListType(option: ts.CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: ts.Push): any[] { - return ts.filter(ts.map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => option.listPreserveFalsyValues ? true : !!v); - } - - /** - * 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 = /(^|\/)\*\*\/?$/; +function validateJsonOptionValue(opt: ts.CommandLineOption, value: T, errors: ts.Push): T | undefined { + if (isNullOrUndefined(value)) + return undefined; + const d = opt.extraValidation?.(value); + if (!d) + return value; + errors.push(ts.createCompilerDiagnostic(...d)); + return undefined; +} - /** - * 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 = /^[^*?]*(?=\/[^/]*[*?])/; +function convertJsonOptionOfCustomType(opt: ts.CommandLineOptionOfCustomType, value: string, errors: ts.Push) { + if (isNullOrUndefined(value)) + return undefined; + const key = value.toLowerCase(); + const val = opt.type.get(key); + if (val !== undefined) { + return validateJsonOptionValue(opt, val, errors); + } + else { + errors.push(createCompilerDiagnosticForInvalidCustomType(opt)); + } +} - /** - * 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 configFileSpecs 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(configFileSpecs: ts.ConfigFileSpecs, basePath: string, options: ts.CompilerOptions, host: ts.ParseConfigHost, extraFileExtensions: readonly ts.FileExtensionInfo[] = ts.emptyArray): string[] { - basePath = ts.normalizePath(basePath); - const keyMapper = ts.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 = new ts.Map(); - - // 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 = new ts.Map(); - - // 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 = new ts.Map(); - const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = configFileSpecs; - - // Rather than re-query this for each file and filespec, we query the supported extensions - // once and store it on the expansion context. - const supportedExtensions = ts.getSupportedExtensions(options, extraFileExtensions); - const supportedExtensionsWithJsonIfResolveJsonModule = ts.getSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); - - // Literal files are always included verbatim. An "include" or "exclude" specification cannot - // remove a literal file. - if (validatedFilesSpec) { - for (const fileName of validatedFilesSpec) { - const file = ts.getNormalizedAbsolutePath(fileName, basePath); - literalFileMap.set(keyMapper(file), file); - } - } +function convertJsonOptionOfListType(option: ts.CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: ts.Push): any[] { + return ts.filter(ts.map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => option.listPreserveFalsyValues ? true : !!v); +} - let jsonOnlyIncludeRegexes: readonly RegExp[] | undefined; - if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { - for (const file of host.readDirectory(basePath, ts.flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { - if (ts.fileExtensionIs(file, ts.Extension.Json)) { - // Valid only if *.json specified - if (!jsonOnlyIncludeRegexes) { - const includes = validatedIncludeSpecs.filter(s => ts.endsWith(s, ts.Extension.Json)); - const includeFilePatterns = ts.map(ts.getRegularExpressionsForWildcards(includes, basePath, "files"), pattern => `^${pattern}$`); - jsonOnlyIncludeRegexes = includeFilePatterns ? includeFilePatterns.map(pattern => ts.getRegexFromPattern(pattern, host.useCaseSensitiveFileNames)) : ts.emptyArray; - } - const includeIndex = ts.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; +/** + * 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 = /(^|\/)\*\*\/?$/; + +/** + * 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 = /^[^*?]*(?=\/[^/]*[*?])/; + +/** + * 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 configFileSpecs 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(configFileSpecs: ts.ConfigFileSpecs, basePath: string, options: ts.CompilerOptions, host: ts.ParseConfigHost, extraFileExtensions: readonly ts.FileExtensionInfo[] = ts.emptyArray): string[] { + basePath = ts.normalizePath(basePath); + const keyMapper = ts.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 = new ts.Map(); + + // 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 = new ts.Map(); + + // 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 = new ts.Map(); + const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = configFileSpecs; + + // Rather than re-query this for each file and filespec, we query the supported extensions + // once and store it on the expansion context. + const supportedExtensions = ts.getSupportedExtensions(options, extraFileExtensions); + const supportedExtensionsWithJsonIfResolveJsonModule = ts.getSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); + + // Literal files are always included verbatim. An "include" or "exclude" specification cannot + // remove a literal file. + if (validatedFilesSpec) { + for (const fileName of validatedFilesSpec) { + const file = ts.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, ts.flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { + if (ts.fileExtensionIs(file, ts.Extension.Json)) { + // Valid only if *.json specified + if (!jsonOnlyIncludeRegexes) { + const includes = validatedIncludeSpecs.filter(s => ts.endsWith(s, ts.Extension.Json)); + const includeFilePatterns = ts.map(ts.getRegularExpressionsForWildcards(includes, basePath, "files"), pattern => `^${pattern}$`); + jsonOnlyIncludeRegexes = includeFilePatterns ? includeFilePatterns.map(pattern => ts.getRegexFromPattern(pattern, host.useCaseSensitiveFileNames)) : ts.emptyArray; } - // 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; + const includeIndex = ts.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; + } - // 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); + // 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 key = keyMapper(file); + if (!literalFileMap.has(key) && !wildcardFileMap.has(key)) { + wildcardFileMap.set(key, file); } } - - const literalFiles = ts.arrayFrom(literalFileMap.values()); - const wildcardFiles = ts.arrayFrom(wildcardFileMap.values()); - return literalFiles.concat(wildcardFiles, ts.arrayFrom(wildCardJsonFileMap.values())); } - /* @internal */ - export function isExcludedFile(pathToCheck: string, spec: ts.ConfigFileSpecs, basePath: string, useCaseSensitiveFileNames: boolean, currentDirectory: string): boolean { - const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec; - if (!ts.length(validatedIncludeSpecs) || !ts.length(validatedExcludeSpecs)) - return false; - basePath = ts.normalizePath(basePath); - const keyMapper = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - if (validatedFilesSpec) { - for (const fileName of validatedFilesSpec) { - if (keyMapper(ts.getNormalizedAbsolutePath(fileName, basePath)) === pathToCheck) - return false; - } + const literalFiles = ts.arrayFrom(literalFileMap.values()); + const wildcardFiles = ts.arrayFrom(wildcardFileMap.values()); + return literalFiles.concat(wildcardFiles, ts.arrayFrom(wildCardJsonFileMap.values())); +} + +/* @internal */ +export function isExcludedFile(pathToCheck: string, spec: ts.ConfigFileSpecs, basePath: string, useCaseSensitiveFileNames: boolean, currentDirectory: string): boolean { + const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec; + if (!ts.length(validatedIncludeSpecs) || !ts.length(validatedExcludeSpecs)) + return false; + basePath = ts.normalizePath(basePath); + const keyMapper = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + if (validatedFilesSpec) { + for (const fileName of validatedFilesSpec) { + if (keyMapper(ts.getNormalizedAbsolutePath(fileName, basePath)) === pathToCheck) + return false; } + } - return matchesExcludeWorker(pathToCheck, validatedExcludeSpecs, useCaseSensitiveFileNames, currentDirectory, basePath); + return matchesExcludeWorker(pathToCheck, validatedExcludeSpecs, useCaseSensitiveFileNames, currentDirectory, basePath); +} + +function invalidDotDotAfterRecursiveWildcard(s: string) { + // We used to use the regex /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/ to check for this case, but + // in v8, that has polynomial performance because the recursive wildcard match - **/ - + // can be matched in many arbitrary positions when multiple are present, resulting + // in bad backtracking (and we don't care which is matched - just that some /.. segment + // comes after some **/ segment). + const wildcardIndex = ts.startsWith(s, "**/") ? 0 : s.indexOf("/**/"); + if (wildcardIndex === -1) { + return false; } + const lastDotIndex = ts.endsWith(s, "/..") ? s.length : s.lastIndexOf("/../"); + return lastDotIndex > wildcardIndex; +} - function invalidDotDotAfterRecursiveWildcard(s: string) { - // We used to use the regex /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/ to check for this case, but - // in v8, that has polynomial performance because the recursive wildcard match - **/ - - // can be matched in many arbitrary positions when multiple are present, resulting - // in bad backtracking (and we don't care which is matched - just that some /.. segment - // comes after some **/ segment). - const wildcardIndex = ts.startsWith(s, "**/") ? 0 : s.indexOf("/**/"); - if (wildcardIndex === -1) { +/* @internal */ +export function matchesExclude(pathToCheck: string, excludeSpecs: readonly string[] | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string) { + return matchesExcludeWorker(pathToCheck, ts.filter(excludeSpecs, spec => !invalidDotDotAfterRecursiveWildcard(spec)), useCaseSensitiveFileNames, currentDirectory); +} +function matchesExcludeWorker(pathToCheck: string, excludeSpecs: readonly string[] | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, basePath?: string) { + const excludePattern = ts.getRegularExpressionForWildcard(excludeSpecs, ts.combinePaths(ts.normalizePath(currentDirectory), basePath), "exclude"); + const excludeRegex = excludePattern && ts.getRegexFromPattern(excludePattern, useCaseSensitiveFileNames); + if (!excludeRegex) + return false; + if (excludeRegex.test(pathToCheck)) + return true; + return !ts.hasExtension(pathToCheck) && excludeRegex.test(ts.ensureTrailingDirectorySeparator(pathToCheck)); +} +function validateSpecs(specs: readonly string[], errors: ts.Push, disallowTrailingRecursion: boolean, jsonSourceFile: ts.TsConfigSourceFile | undefined, specKey: string): readonly string[] { + return specs.filter(spec => { + if (!ts.isString(spec)) return false; + const diag = specToDiagnostic(spec, disallowTrailingRecursion); + if (diag !== undefined) { + errors.push(createDiagnostic(...diag)); } - const lastDotIndex = ts.endsWith(s, "/..") ? s.length : s.lastIndexOf("/../"); - return lastDotIndex > wildcardIndex; - } + return diag === undefined; + }); - /* @internal */ - export function matchesExclude(pathToCheck: string, excludeSpecs: readonly string[] | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string) { - return matchesExcludeWorker(pathToCheck, ts.filter(excludeSpecs, spec => !invalidDotDotAfterRecursiveWildcard(spec)), useCaseSensitiveFileNames, currentDirectory); + function createDiagnostic(message: ts.DiagnosticMessage, spec: string): ts.Diagnostic { + const element = ts.getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec); + return element ? + ts.createDiagnosticForNodeInSourceFile(jsonSourceFile!, element, message, spec) : + ts.createCompilerDiagnostic(message, spec); } - function matchesExcludeWorker(pathToCheck: string, excludeSpecs: readonly string[] | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, basePath?: string) { - const excludePattern = ts.getRegularExpressionForWildcard(excludeSpecs, ts.combinePaths(ts.normalizePath(currentDirectory), basePath), "exclude"); - const excludeRegex = excludePattern && ts.getRegexFromPattern(excludePattern, useCaseSensitiveFileNames); - if (!excludeRegex) - return false; - if (excludeRegex.test(pathToCheck)) - return true; - return !ts.hasExtension(pathToCheck) && excludeRegex.test(ts.ensureTrailingDirectorySeparator(pathToCheck)); - } - function validateSpecs(specs: readonly string[], errors: ts.Push, disallowTrailingRecursion: boolean, jsonSourceFile: ts.TsConfigSourceFile | undefined, specKey: string): readonly string[] { - return specs.filter(spec => { - if (!ts.isString(spec)) - return false; - const diag = specToDiagnostic(spec, disallowTrailingRecursion); - if (diag !== undefined) { - errors.push(createDiagnostic(...diag)); - } - return diag === undefined; - }); +} - function createDiagnostic(message: ts.DiagnosticMessage, spec: string): ts.Diagnostic { - const element = ts.getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec); - return element ? - ts.createDiagnosticForNodeInSourceFile(jsonSourceFile!, element, message, spec) : - ts.createCompilerDiagnostic(message, spec); - } +function specToDiagnostic(spec: string, disallowTrailingRecursion?: boolean): [ + ts.DiagnosticMessage, + string +] | undefined { + if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { + return [ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec]; } - - function specToDiagnostic(spec: string, disallowTrailingRecursion?: boolean): [ - ts.DiagnosticMessage, - string - ] | undefined { - if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { - return [ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec]; - } - else if (invalidDotDotAfterRecursiveWildcard(spec)) { - return [ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec]; - } + else if (invalidDotDotAfterRecursiveWildcard(spec)) { + return [ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec]; } +} - /** - * Gets directories in a set of include patterns that should be watched for changes. - */ - function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }: ts.ConfigFileSpecs, path: string, useCaseSensitiveFileNames: boolean): ts.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 = ts.getRegularExpressionForWildcard(exclude, path, "exclude"); - const excludeRegex = rawExcludeRegex && new RegExp(rawExcludeRegex, useCaseSensitiveFileNames ? "" : "i"); - const wildcardDirectories: ts.MapLike = {}; - if (include !== undefined) { - const recursiveKeys: string[] = []; - for (const file of include) { - const spec = ts.normalizePath(ts.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 === ts.WatchDirectoryFlags.Recursive) { - recursiveKeys.push(key); - } +/** + * Gets directories in a set of include patterns that should be watched for changes. + */ +function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }: ts.ConfigFileSpecs, path: string, useCaseSensitiveFileNames: boolean): ts.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 = ts.getRegularExpressionForWildcard(exclude, path, "exclude"); + const excludeRegex = rawExcludeRegex && new RegExp(rawExcludeRegex, useCaseSensitiveFileNames ? "" : "i"); + const wildcardDirectories: ts.MapLike = {}; + if (include !== undefined) { + const recursiveKeys: string[] = []; + for (const file of include) { + const spec = ts.normalizePath(ts.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 === ts.WatchDirectoryFlags.Recursive) { + recursiveKeys.push(key); } } } + } - // Remove any subpaths under an existing recursively watched directory. - for (const key in wildcardDirectories) { - if (ts.hasProperty(wildcardDirectories, key)) { - for (const recursiveKey of recursiveKeys) { - if (key !== recursiveKey && ts.containsPath(recursiveKey, key, path, !useCaseSensitiveFileNames)) { - delete wildcardDirectories[key]; - } + // Remove any subpaths under an existing recursively watched directory. + for (const key in wildcardDirectories) { + if (ts.hasProperty(wildcardDirectories, key)) { + for (const recursiveKey of recursiveKeys) { + if (key !== recursiveKey && ts.containsPath(recursiveKey, key, path, !useCaseSensitiveFileNames)) { + delete wildcardDirectories[key]; } } } } + } - return wildcardDirectories; - } - - function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { - key: string; - flags: ts.WatchDirectoryFlags; - } | undefined { - const match = wildcardDirectoryPattern.exec(spec); - if (match) { - // We check this with a few `indexOf` calls because 3 `indexOf`/`lastIndexOf` calls is - // less algorithmically complex (roughly O(3n) worst-case) than the regex we used to use, - // \/[^/]*?[*?][^/]*\/ which was polynominal in v8, since arbitrary sequences of wildcard - // characters could match any of the central patterns, resulting in bad backtracking. - const questionWildcardIndex = spec.indexOf("?"); - const starWildcardIndex = spec.indexOf("*"); - const lastDirectorySeperatorIndex = spec.lastIndexOf(ts.directorySeparator); - return { - key: useCaseSensitiveFileNames ? match[0] : ts.toFileNameLowerCase(match[0]), - flags: (questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex) - || (starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex) - ? ts.WatchDirectoryFlags.Recursive : ts.WatchDirectoryFlags.None - }; - } - if (ts.isImplicitGlob(spec.substring(spec.lastIndexOf(ts.directorySeparator) + 1))) { - return { - key: useCaseSensitiveFileNames ? spec : ts.toFileNameLowerCase(spec), - flags: ts.WatchDirectoryFlags.Recursive - }; - } - return undefined; + return wildcardDirectories; +} + +function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { + key: string; + flags: ts.WatchDirectoryFlags; +} | undefined { + const match = wildcardDirectoryPattern.exec(spec); + if (match) { + // We check this with a few `indexOf` calls because 3 `indexOf`/`lastIndexOf` calls is + // less algorithmically complex (roughly O(3n) worst-case) than the regex we used to use, + // \/[^/]*?[*?][^/]*\/ which was polynominal in v8, since arbitrary sequences of wildcard + // characters could match any of the central patterns, resulting in bad backtracking. + const questionWildcardIndex = spec.indexOf("?"); + const starWildcardIndex = spec.indexOf("*"); + const lastDirectorySeperatorIndex = spec.lastIndexOf(ts.directorySeparator); + return { + key: useCaseSensitiveFileNames ? match[0] : ts.toFileNameLowerCase(match[0]), + flags: (questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex) + || (starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex) + ? ts.WatchDirectoryFlags.Recursive : ts.WatchDirectoryFlags.None + }; + } + if (ts.isImplicitGlob(spec.substring(spec.lastIndexOf(ts.directorySeparator) + 1))) { + return { + key: useCaseSensitiveFileNames ? spec : ts.toFileNameLowerCase(spec), + flags: ts.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. - */ - function hasFileWithHigherPriorityExtension(file: string, literalFiles: ts.ESMap, wildcardFiles: ts.ESMap, extensions: readonly string[][], keyMapper: (value: string) => string) { - const extensionGroup = ts.forEach(extensions, group => ts.fileExtensionIsOneOf(file, group) ? group : undefined); - if (!extensionGroup) { +/** + * Determines whether a literal or wildcard file has already been included that has a higher + * extension priority. + * + * @param file The path to the file. + */ +function hasFileWithHigherPriorityExtension(file: string, literalFiles: ts.ESMap, wildcardFiles: ts.ESMap, extensions: readonly string[][], keyMapper: (value: string) => string) { + const extensionGroup = ts.forEach(extensions, group => ts.fileExtensionIsOneOf(file, group) ? group : undefined); + if (!extensionGroup) { + return false; + } + for (const ext of extensionGroup) { + if (ts.fileExtensionIs(file, ext)) { return false; } - for (const ext of extensionGroup) { - if (ts.fileExtensionIs(file, ext)) { - return false; - } - const higherPriorityPath = keyMapper(ts.changeExtension(file, ext)); - if (literalFiles.has(higherPriorityPath) || wildcardFiles.has(higherPriorityPath)) { - if (ext === ts.Extension.Dts && (ts.fileExtensionIs(file, ts.Extension.Js) || ts.fileExtensionIs(file, ts.Extension.Jsx))) { - // LEGACY BEHAVIOR: An off-by-one bug somewhere in the extension priority system for wildcard module loading allowed declaration - // files to be loaded alongside their js(x) counterparts. We regard this as generally undesirable, but retain the behavior to - // prevent breakage. - continue; - } - return true; + const higherPriorityPath = keyMapper(ts.changeExtension(file, ext)); + if (literalFiles.has(higherPriorityPath) || wildcardFiles.has(higherPriorityPath)) { + if (ext === ts.Extension.Dts && (ts.fileExtensionIs(file, ts.Extension.Js) || ts.fileExtensionIs(file, ts.Extension.Jsx))) { + // LEGACY BEHAVIOR: An off-by-one bug somewhere in the extension priority system for wildcard module loading allowed declaration + // files to be loaded alongside their js(x) counterparts. We regard this as generally undesirable, but retain the behavior to + // prevent breakage. + continue; } + 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. - */ - function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: ts.ESMap, extensions: readonly string[][], keyMapper: (value: string) => string) { - const extensionGroup = ts.forEach(extensions, group => ts.fileExtensionIsOneOf(file, group) ? group : undefined); - if (!extensionGroup) { + 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. + */ +function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: ts.ESMap, extensions: readonly string[][], keyMapper: (value: string) => string) { + const extensionGroup = ts.forEach(extensions, group => ts.fileExtensionIsOneOf(file, group) ? group : undefined); + if (!extensionGroup) { + return; + } + for (let i = extensionGroup.length - 1; i >= 0; i--) { + const ext = extensionGroup[i]; + if (ts.fileExtensionIs(file, ext)) { return; } - for (let i = extensionGroup.length - 1; i >= 0; i--) { - const ext = extensionGroup[i]; - if (ts.fileExtensionIs(file, ext)) { - return; + const lowerPriorityPath = keyMapper(ts.changeExtension(file, ext)); + 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: ts.CompilerOptions): ts.CompilerOptions { + const out: ts.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); } - const lowerPriorityPath = keyMapper(ts.changeExtension(file, ext)); - wildcardFiles.delete(lowerPriorityPath); } } + return out; +} - /** - * 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: ts.CompilerOptions): ts.CompilerOptions { - const out: ts.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); +function getOptionValueWithEmptyStrings(value: any, option: ts.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 ts.isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; + default: + return ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => { + if (optionEnumValue === value) { + return optionStringValue; } - } - } - return out; - } - - function getOptionValueWithEmptyStrings(value: any, option: ts.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 ts.isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; - default: - return ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => { - if (optionEnumValue === value) { - return optionStringValue; - } - })!; // TODO: GH#18217 - } + })!; // TODO: GH#18217 } +} - function getDefaultValueForOption(option: ts.CommandLineOption) { - switch (option.type) { - case "number": - return 1; - case "boolean": - return true; - case "string": - const defaultValue = option.defaultValueDescription; - return option.isFilePath ? `./${defaultValue && typeof defaultValue === "string" ? defaultValue : ""}` : ""; - case "list": - return []; - case "object": - return {}; - default: - const iterResult = option.type.keys().next(); - if (!iterResult.done) - return iterResult.value; - return ts.Debug.fail("Expected 'option.type' to have entries."); - } +function getDefaultValueForOption(option: ts.CommandLineOption) { + switch (option.type) { + case "number": + return 1; + case "boolean": + return true; + case "string": + const defaultValue = option.defaultValueDescription; + return option.isFilePath ? `./${defaultValue && typeof defaultValue === "string" ? defaultValue : ""}` : ""; + case "list": + return []; + case "object": + return {}; + default: + const iterResult = option.type.keys().next(); + if (!iterResult.done) + return iterResult.value; + return ts.Debug.fail("Expected 'option.type' to have entries."); } } +} diff --git a/src/compiler/core.ts b/src/compiler/core.ts index eafdcb28a826f..ca80f19da8ec7 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1,382 +1,423 @@ /* @internal */ namespace ts { - export function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ - K, - V - ] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; - export function getIterator(iterable: ts.ReadonlyESMap): ts.Iterator<[ - K, - V - ]>; - export function getIterator(iterable: ts.ReadonlyESMap | undefined): ts.Iterator<[ - K, - V - ]> | undefined; - export function getIterator(iterable: readonly T[] | ts.ReadonlySet): ts.Iterator; - export function getIterator(iterable: readonly T[] | ts.ReadonlySet | undefined): ts.Iterator | undefined; - export function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { - if (iterable) { - if (isArray(iterable)) - return arrayIterator(iterable); - if (iterable instanceof ts.Map) - return iterable.entries(); - if (iterable instanceof ts.Set) - return iterable.values(); - throw new Error("Iteration not supported."); - } - } - - export const emptyArray: never[] = [] as never[]; - export const emptyMap: ts.ReadonlyESMap = new ts.Map(); - export const emptySet: ts.ReadonlySet = new ts.Set(); - - 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. - */ - 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; +export function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ + K, + V +] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; +export function getIterator(iterable: ts.ReadonlyESMap): ts.Iterator<[ + K, + V +]>; +export function getIterator(iterable: ts.ReadonlyESMap | undefined): ts.Iterator<[ + K, + V +]> | undefined; +export function getIterator(iterable: readonly T[] | ts.ReadonlySet): ts.Iterator; +export function getIterator(iterable: readonly T[] | ts.ReadonlySet | undefined): ts.Iterator | undefined; +export function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { + if (iterable) { + if (isArray(iterable)) + return arrayIterator(iterable); + if (iterable instanceof ts.Map) + return iterable.entries(); + if (iterable instanceof ts.Set) + return iterable.values(); + throw new Error("Iteration not supported."); } +} - /** - * 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; - } +export const emptyArray: never[] = [] as never[]; +export const emptyMap: ts.ReadonlyESMap = new ts.Map(); +export const emptySet: ts.ReadonlySet = new ts.Set(); - /** 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) { - return undefined; - } +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. + */ +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 !== undefined) { + if (result) { return result; } } - return undefined; } + return undefined; +} - export function firstDefinedIterator(iter: ts.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) { +/** + * 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; +} - export function reduceLeftIterator(iterator: ts.Iterator | undefined, f: (memo: U, value: T, i: number) => U, initial: U): U { - let result = initial; - if (iterator) { - for (let step = iterator.next(), pos = 0; !step.done; step = iterator.next(), pos++) { - result = f(result, step.value, pos); - } - } - return result; +/** 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) { + return undefined; } - export function zipWith(arrayA: readonly T[], arrayB: readonly U[], callback: (a: T, b: U, index: number) => V): V[] { - const result: V[] = []; - ts.Debug.assertEqual(arrayA.length, arrayB.length); - for (let i = 0; i < arrayA.length; i++) { - result.push(callback(arrayA[i], arrayB[i], i)); + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result !== undefined) { + return result; } - return result; } + return undefined; +} - export function zipToIterator(arrayA: readonly T[], arrayB: readonly U[]): ts.Iterator<[ - T, - U - ]> { - ts.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 }; - } - }; +export function firstDefinedIterator(iter: ts.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; + } } +} - export function zipToMap(keys: readonly K[], values: readonly V[]): ts.ESMap { - ts.Debug.assert(keys.length === values.length); - const map = new ts.Map(); - for (let i = 0; i < keys.length; ++i) { - map.set(keys[i], values[i]); +export function reduceLeftIterator(iterator: ts.Iterator | undefined, f: (memo: U, value: T, i: number) => U, initial: U): U { + let result = initial; + if (iterator) { + for (let step = iterator.next(), pos = 0; !step.done; step = iterator.next(), pos++) { + result = f(result, step.value, pos); } - return map; } + return result; +} - /** - * Creates a new array with `element` interspersed in between each element of `input` - * if there is more than 1 value in `input`. Otherwise, returns the existing array. - */ - export function intersperse(input: T[], element: T): T[] { - if (input.length <= 1) { - return input; - } - const result: T[] = []; - for (let i = 0, n = input.length; i < n; i++) { - if (i) - result.push(element); - result.push(input[i]); - } - return result; +export function zipWith(arrayA: readonly T[], arrayB: readonly U[], callback: (a: T, b: U, index: number) => V): V[] { + const result: V[] = []; + ts.Debug.assertEqual(arrayA.length, arrayB.length); + for (let i = 0; i < arrayA.length; i++) { + result.push(callback(arrayA[i], arrayB[i], i)); } + return result; +} - /** - * 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[] | undefined, callback: (element: T, index: number) => boolean): boolean { - if (array) { - for (let i = 0; i < array.length; i++) { - if (!callback(array[i], i)) { - return false; - } +export function zipToIterator(arrayA: readonly T[], arrayB: readonly U[]): ts.Iterator<[ + T, + U +]> { + ts.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; +export function zipToMap(keys: readonly K[], values: readonly V[]): ts.ESMap { + ts.Debug.assert(keys.length === values.length); + const map = new ts.Map(); + for (let i = 0; i < keys.length; ++i) { + map.set(keys[i], values[i]); } + return map; +} + +/** + * Creates a new array with `element` interspersed in between each element of `input` + * if there is more than 1 value in `input`. Otherwise, returns the existing array. + */ +export function intersperse(input: T[], element: T): T[] { + if (input.length <= 1) { + return input; + } + const result: T[] = []; + for (let i = 0, n = input.length; i < n; i++) { + if (i) + result.push(element); + result.push(input[i]); + } + return result; +} - /** 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 { +/** + * 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[] | undefined, callback: (element: T, index: number) => boolean): boolean { + if (array) { for (let i = 0; i < array.length; i++) { - const value = array[i]; - if (predicate(value, i)) { - return value; + if (!callback(array[i], i)) { + return false; } } - 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 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 undefined; } + 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; - } +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; } + return undefined; +} - 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; - } +/** 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 -1; } + 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 { - for (let i = 0; i < array.length; i++) { - const result = callback(array[i], i); - if (result) { - return result; - } +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 ts.Debug.fail(); } + return -1; +} - export function contains(array: readonly T[] | undefined, value: T, equalityComparer: ts.EqualityComparer = equateValues): boolean { - if (array) { - for (const v of array) { - if (equalityComparer(v, value)) { - return true; - } +/** + * 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 { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result) { + return result; + } + } + return ts.Debug.fail(); +} + +export function contains(array: readonly T[] | undefined, value: T, equalityComparer: ts.EqualityComparer = equateValues): boolean { + if (array) { + for (const v of array) { + if (equalityComparer(v, value)) { + return true; } } - return false; } + return false; +} + +export function arraysEqual(a: readonly T[], b: readonly T[], equalityComparer: ts.EqualityComparer = equateValues): boolean { + return a.length === b.length && a.every((x, i) => equalityComparer(x, b[i])); +} - export function arraysEqual(a: readonly T[], b: readonly T[], equalityComparer: ts.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 -1; +} - 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; +export function countWhere(array: readonly T[] | undefined, 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++; } } - return -1; } + return count; +} - export function countWhere(array: readonly T[] | undefined, 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++; +/** + * 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) { + const result = array.slice(0, i); + i++; + while (i < len) { + const item = array[i]; + if (f(item)) { + result.push(item); } + i++; } + return result; } - return count; } + return array; +} - /** - * 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) { - const result = array.slice(0, i); - i++; - while (i < len) { - const item = array[i]; - if (f(item)) { - result.push(item); - } - i++; - } - return result; - } +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; } + array.length = outIndex; +} + +export function clear(array: unknown[]): void { + array.length = 0; +} - export function filterMutate(array: T[], f: (x: T, i: number, array: T[]) => boolean): void { - let outIndex = 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++) { - if (f(array[i], i, array)) { - array[outIndex] = array[i]; - outIndex++; - } + result.push(f(array[i], i)); } - array.length = outIndex; } + return result; +} - export function clear(array: unknown[]): 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)); +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 +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; } + return array; +} - - 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 }; +/** + * 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) { + if (v) { + if (isArray(v)) { + addRange(result, v); } - }; + else { + result.push(v); + } + } } + return result; +} - // 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; +/** + * 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 array; } + return result || emptyArray; +} - /** - * 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) { +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); @@ -386,2171 +427,2130 @@ namespace ts { } } } - 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. - */ - 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); - } +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; } - } - } - return result || emptyArray; - } - - 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); - } + const iterRes = iter.next(); + if (iterRes.done) { + return iterRes as { + done: true; + value: never; + }; } + currentIter = getIterator(iterRes.value); } - } - return result; + }, + }; + + function getIterator(x: T): ts.Iterator { + const res = mapfn(x); + return res === undefined ? emptyIterator : isArray(res) ? arrayIterator(res) : res; } +} - 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); +/** + * 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); } - }, - }; - - function getIterator(x: T): ts.Iterator { - const res = mapfn(x); - return res === undefined ? emptyIterator : isArray(res) ? arrayIterator(res) : res; + if (isArray(mapped)) { + addRange(result, mapped); + } + else { + result.push(mapped); + } + } } } + return result || array; +} - /** - * 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); - } - } - } +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; } - return result || array; + result.push(mapped); } + return result; +} - export function mapAllOrFail(array: readonly T[], mapFn: (x: T, i: number) => U | undefined): U[] | undefined { - const result: U[] = []; +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; } + 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); +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; + }; + } + const value = mapFn(res.value); + if (value !== undefined) { + return { value, done: false }; } } } - return result; - } + }; +} - 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; - }; - } - const value = mapFn(res.value); - if (value !== undefined) { - return { value, done: false }; - } - } - } - }; +export function mapDefinedEntries(map: ts.ReadonlyESMap, f: (key: K1, value: V1) => readonly [ + K2, + V2 +] | undefined): ts.ESMap; +export function mapDefinedEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ + K2 | undefined, + V2 | undefined +] | undefined): ts.ESMap | undefined; +export function mapDefinedEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ + K2 | undefined, + V2 | undefined +] | undefined): ts.ESMap | undefined { + if (!map) { + return undefined; } - export function mapDefinedEntries(map: ts.ReadonlyESMap, f: (key: K1, value: V1) => readonly [ - K2, - V2 - ] | undefined): ts.ESMap; - export function mapDefinedEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ - K2 | undefined, - V2 | undefined - ] | undefined): ts.ESMap | undefined; - export function mapDefinedEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ - K2 | undefined, - V2 | undefined - ] | undefined): ts.ESMap | undefined { - if (!map) { - return undefined; + const result = new ts.Map(); + map.forEach((value, key) => { + const entry = f(key, value); + if (entry !== undefined) { + const [newKey, newValue] = entry; + if (newKey !== undefined && newValue !== undefined) { + result.set(newKey, newValue); + } } + }); - const result = new ts.Map(); - map.forEach((value, key) => { - const entry = f(key, value); - if (entry !== undefined) { - const [newKey, newValue] = entry; - if (newKey !== undefined && newValue !== undefined) { - result.set(newKey, newValue); - } + return result; +} + +export function mapDefinedValues(set: ts.ReadonlySet, f: (value: V1) => V2 | undefined): ts.Set; +export function mapDefinedValues(set: ts.ReadonlySet | undefined, f: (value: V1) => V2 | undefined): ts.Set | undefined; +export function mapDefinedValues(set: ts.ReadonlySet | undefined, f: (value: V1) => V2 | undefined): ts.Set | undefined { + if (set) { + const result = new ts.Set(); + set.forEach(value => { + const newValue = f(value); + if (newValue !== undefined) { + result.add(newValue); } }); - return result; } +} - export function mapDefinedValues(set: ts.ReadonlySet, f: (value: V1) => V2 | undefined): ts.Set; - export function mapDefinedValues(set: ts.ReadonlySet | undefined, f: (value: V1) => V2 | undefined): ts.Set | undefined; - export function mapDefinedValues(set: ts.ReadonlySet | undefined, f: (value: V1) => V2 | undefined): ts.Set | undefined { - if (set) { - const result = new ts.Set(); - set.forEach(value => { - const newValue = f(value); - if (newValue !== undefined) { - result.add(newValue); - } - }); - return result; - } +export function getOrUpdate(map: ts.ESMap, key: K, callback: () => V) { + if (map.has(key)) { + return map.get(key)!; } + const value = callback(); + map.set(key, value); + return value; +} - export function getOrUpdate(map: ts.ESMap, key: K, callback: () => V) { - if (map.has(key)) { - return map.get(key)!; - } - const value = callback(); - map.set(key, value); - return value; +export function tryAddToSet(set: ts.Set, value: T) { + if (!set.has(value)) { + set.add(value); + return true; } + return false; +} - export function tryAddToSet(set: ts.Set, value: T) { - if (!set.has(value)) { - set.add(value); - return true; +export const emptyIterator: ts.Iterator = { next: () => ({ value: undefined as never, done: true }) }; +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 }; } - return false; - } - - export const emptyIterator: ts.Iterator = { next: () => ({ value: undefined as never, done: true }) }; - 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. - */ - 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++; +/** + * 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; } - - 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; - } + if (start < pos) { + const v = mapfn(array.slice(start, pos), previousKey!, start, pos); + if (v) { + result.push(v); + } - export function mapEntries(map: ts.ReadonlyESMap, f: (key: K1, value: V1) => readonly [ - K2, - V2 - ]): ts.ESMap; - export function mapEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ - K2, - V2 - ]): ts.ESMap | undefined; - export function mapEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ - K2, - V2 - ]): ts.ESMap | undefined { - if (!map) { - return undefined; + start = pos; + } + + previousKey = key; + pos++; } + } - const result = new ts.Map(); - map.forEach((value, key) => { - const [newKey, newValue] = f(key, value); - result.set(newKey, newValue); - }); - return result; + return result; +} + +export function mapEntries(map: ts.ReadonlyESMap, f: (key: K1, value: V1) => readonly [ + K2, + V2 +]): ts.ESMap; +export function mapEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ + K2, + V2 +]): ts.ESMap | undefined; +export function mapEntries(map: ts.ReadonlyESMap | undefined, f: (key: K1, value: V1) => readonly [ + K2, + V2 +]): ts.ESMap | undefined { + if (!map) { + return undefined; } - 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; - } + const result = new ts.Map(); + 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; } } - else { - return array.length > 0; - } } - return false; + else { + return array.length > 0; + } } + 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; - } +/** 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; } } - if (start !== undefined) - cb(start, arr.length); } + 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]; - } +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; - } +function selectIndex(_: unknown, i: number) { + return i; +} - export function indicesOf(array: readonly unknown[]): number[] { - return array.map(selectIndex); - } +export function indicesOf(array: readonly unknown[]): number[] { + return array.map(selectIndex); +} - function deduplicateRelational(array: readonly T[], equalityComparer: ts.EqualityComparer, comparer: ts.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); +function deduplicateRelational(array: readonly T[], equalityComparer: ts.EqualityComparer, comparer: ts.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; - } + 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]); } - function deduplicateEquality(array: readonly T[], equalityComparer: ts.EqualityComparer) { - const result: T[] = []; - for (const item of array) { - pushIfUnique(result, item, equalityComparer); - } - return result; - } + // restore original order + deduplicated.sort(); + return deduplicated.map(i => array[i]); +} - /** - * 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: ts.EqualityComparer, comparer?: ts.Comparer): T[] { - return array.length === 0 ? [] : - array.length === 1 ? array.slice() : - comparer ? deduplicateRelational(array, equalityComparer, comparer) : - deduplicateEquality(array, equalityComparer); +function deduplicateEquality(array: readonly T[], equalityComparer: ts.EqualityComparer) { + const result: T[] = []; + for (const item of array) { + pushIfUnique(result, item, equalityComparer); } + return result; +} - /** - * Deduplicates an array that has already been sorted. - */ - function deduplicateSorted(array: ts.SortedReadonlyArray, comparer: ts.EqualityComparer | ts.Comparer): ts.SortedReadonlyArray { - if (array.length === 0) - return emptyArray as any as ts.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 ts.Comparison.EqualTo: - continue; +/** + * 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: ts.EqualityComparer, comparer?: ts.Comparer): T[] { + return array.length === 0 ? [] : + array.length === 1 ? array.slice() : + comparer ? deduplicateRelational(array, equalityComparer, comparer) : + deduplicateEquality(array, equalityComparer); +} - case ts.Comparison.LessThan: - // If `array` is sorted, `next` should **never** be less than `last`. - return ts.Debug.fail("Array is unsorted."); - } +/** + * Deduplicates an array that has already been sorted. + */ +function deduplicateSorted(array: ts.SortedReadonlyArray, comparer: ts.EqualityComparer | ts.Comparer): ts.SortedReadonlyArray { + if (array.length === 0) + return emptyArray as any as ts.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 ts.Comparison.EqualTo: + continue; - deduplicated.push(last = next); + case ts.Comparison.LessThan: + // If `array` is sorted, `next` should **never** be less than `last`. + return ts.Debug.fail("Array is unsorted."); } - return deduplicated as any as ts.SortedReadonlyArray; + deduplicated.push(last = next); } - export function createSortedArray(): ts.SortedArray { - return [] as any as ts.SortedArray; // TODO: GH#19873 - } + return deduplicated as any as ts.SortedReadonlyArray; +} - export function insertSorted(array: ts.SortedArray, insert: T, compare: ts.Comparer, allowDuplicates?: boolean): void { - if (array.length === 0) { - array.push(insert); - return; - } +export function createSortedArray(): ts.SortedArray { + return [] as any as ts.SortedArray; // TODO: GH#19873 +} - const insertIndex = binarySearch(array, insert, identity, compare); - if (insertIndex < 0) { - array.splice(~insertIndex, 0, insert); - } - else if (allowDuplicates) { - array.splice(insertIndex, 0, insert); - } +export function insertSorted(array: ts.SortedArray, insert: T, compare: ts.Comparer, allowDuplicates?: boolean): void { + if (array.length === 0) { + array.push(insert); + return; } - export function sortAndDeduplicate(array: readonly string[]): ts.SortedReadonlyArray; - export function sortAndDeduplicate(array: readonly T[], comparer: ts.Comparer, equalityComparer?: ts.EqualityComparer): ts.SortedReadonlyArray; - export function sortAndDeduplicate(array: readonly T[], comparer?: ts.Comparer, equalityComparer?: ts.EqualityComparer): ts.SortedReadonlyArray { - return deduplicateSorted(sort(array, comparer), equalityComparer || comparer || compareStringsCaseSensitive as any as ts.Comparer); + const insertIndex = binarySearch(array, insert, identity, compare); + if (insertIndex < 0) { + array.splice(~insertIndex, 0, insert); } - - export function arrayIsSorted(array: readonly T[], comparer: ts.Comparer) { - if (array.length < 2) - return true; - let prevElement = array[0]; - for (const element of array.slice(1)) { - if (comparer(prevElement, element) === ts.Comparison.GreaterThan) { - return false; - } - prevElement = element; - } - return true; + else if (allowDuplicates) { + 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; - } +export function sortAndDeduplicate(array: readonly string[]): ts.SortedReadonlyArray; +export function sortAndDeduplicate(array: readonly T[], comparer: ts.Comparer, equalityComparer?: ts.EqualityComparer): ts.SortedReadonlyArray; +export function sortAndDeduplicate(array: readonly T[], comparer?: ts.Comparer, equalityComparer?: ts.EqualityComparer): ts.SortedReadonlyArray { + return deduplicateSorted(sort(array, comparer), equalityComparer || comparer || compareStringsCaseSensitive as any as ts.Comparer); +} - if (array1.length !== array2.length) { +export function arrayIsSorted(array: readonly T[], comparer: ts.Comparer) { + if (array.length < 2) + return true; + let prevElement = array[0]; + for (const element of array.slice(1)) { + if (comparer(prevElement, element) === ts.Comparison.GreaterThan) { return false; } + prevElement = element; + } + return true; +} - for (let i = 0; i < array1.length; i++) { - if (!equalityComparer(array1[i], array2[i], i)) { - return false; - } - } +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; + } - return true; + if (array1.length !== array2.length) { + return false; } - /** - * 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); - } - } - } + for (let i = 0; i < array1.length; i++) { + if (!equalityComparer(array1[i], array2[i], i)) { + return false; } - 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: ts.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. - ts.Debug.assertGreaterThanOrEqual(comparer(arrayB[offsetB], arrayB[offsetB - 1]), ts.Comparison.EqualTo); - } + return true; +} - 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. - ts.Debug.assertGreaterThanOrEqual(comparer(arrayA[offsetA], arrayA[offsetA - 1]), ts.Comparison.EqualTo); +/** + * 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); } - - switch (comparer(arrayB[offsetB], arrayA[offsetA])) { - case ts.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 ts.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 ts.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; + if (v) { + result.push(v); } } } - return result; } + return result || array; +} - export function sum, K extends string>(array: readonly T[], prop: K): number { - let result = 0; - for (const v of array) { - result += v[prop]; +/** + * 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: ts.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. + ts.Debug.assertGreaterThanOrEqual(comparer(arrayB[offsetB], arrayB[offsetB - 1]), ts.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. + ts.Debug.assertGreaterThanOrEqual(comparer(arrayA[offsetA], arrayA[offsetA - 1]), ts.Comparison.EqualTo); + } + + switch (comparer(arrayB[offsetB], arrayA[offsetA])) { + case ts.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 ts.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 ts.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; } + 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. - */ - 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: ts.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 to; +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; +} - /** - * Combines two arrays, values, or undefineds into the smallest container that can accommodate the resulting set: - * - * ``` - * undefined -> undefined -> undefined - * T -> undefined -> T - * T -> T -> T[] - * T[] -> undefined -> T[] (no-op) - * T[] -> T -> T[] (append) - * T[] -> T[] -> T[] (concatenate) - * ``` - */ - export function combine(xs: T | readonly T[] | undefined, ys: T | readonly T[] | undefined): T | readonly T[] | undefined; - export function combine(xs: T | T[] | undefined, ys: T | T[] | undefined): T | T[] | undefined; - export function combine(xs: T | T[] | undefined, ys: T | T[] | undefined) { - if (xs === undefined) - return ys; - if (ys === undefined) - return xs; - if (isArray(xs)) - return isArray(ys) ? concatenate(xs, ys) : append(xs, ys); - if (isArray(ys)) - return append(ys, xs); - return [xs, ys]; - } +/** + * 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: ts.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 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; - } +/** + * Combines two arrays, values, or undefineds into the smallest container that can accommodate the resulting set: + * + * ``` + * undefined -> undefined -> undefined + * T -> undefined -> T + * T -> T -> T[] + * T[] -> undefined -> T[] (no-op) + * T[] -> T -> T[] (append) + * T[] -> T[] -> T[] (concatenate) + * ``` + */ +export function combine(xs: T | readonly T[] | undefined, ys: T | readonly T[] | undefined): T | readonly T[] | undefined; +export function combine(xs: T | T[] | undefined, ys: T | T[] | undefined): T | T[] | undefined; +export function combine(xs: T | T[] | undefined, ys: T | T[] | undefined) { + if (xs === undefined) + return ys; + if (ys === undefined) + return xs; + if (isArray(xs)) + return isArray(ys) ? concatenate(xs, ys) : append(xs, ys); + if (isArray(ys)) + return append(ys, xs); + return [xs, ys]; +} - /** - * 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]); - } - } - 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; +} - /** - * @return Whether the value was added. - */ - export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: ts.EqualityComparer): boolean { - if (contains(array, toAdd, equalityComparer)) { - return false; - } - else { - array.push(toAdd); - return true; +/** + * 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]); } } + return to; +} - /** - * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. - */ - export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: ts.EqualityComparer): T[] { - if (array) { - pushIfUnique(array, toAdd, equalityComparer); - return array; - } - else { - return [toAdd]; - } +/** + * @return Whether the value was added. + */ +export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: ts.EqualityComparer): boolean { + if (contains(array, toAdd, equalityComparer)) { + return false; } - - function stableSortIndices(array: readonly T[], indices: number[], comparer: ts.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?: ts.Comparer): ts.SortedReadonlyArray { - return (array.length === 0 ? array : array.slice().sort(comparer)) as ts.SortedReadonlyArray; +/** + * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. + */ +export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: ts.EqualityComparer): T[] { + if (array) { + pushIfUnique(array, toAdd, equalityComparer); + return array; } + else { + return [toAdd]; + } +} - export function arrayIterator(array: readonly T[]): ts.Iterator { - let i = 0; - return { next: () => { - if (i === array.length) { +function stableSortIndices(array: readonly T[], indices: number[], comparer: ts.Comparer) { + // sort indices by value then position + indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); +} + +/** + * Returns a new sorted array. + */ +export function sort(array: readonly T[], comparer?: ts.Comparer): ts.SortedReadonlyArray { + return (array.length === 0 ? array : array.slice().sort(comparer)) as ts.SortedReadonlyArray; +} + +export function arrayIterator(array: readonly T[]): ts.Iterator { + let i = 0; + return { next: () => { + if (i === array.length) { + return { value: undefined as never, done: true }; + } + else { + i++; + return { value: array[i - 1], done: false }; + } + }}; +} + +export function arrayReverseIterator(array: readonly T[]): ts.Iterator { + let i = array.length; + return { + next: () => { + if (i === 0) { return { value: undefined as never, done: true }; } else { - i++; - return { value: array[i - 1], done: false }; + i--; + return { value: array[i], done: false }; } - }}; - } - - export function arrayReverseIterator(array: readonly T[]): ts.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 }; - } - } - }; - } + } + }; +} - /** - * Stable sort of an array. Elements equal to each other maintain their relative position in the array. - */ - export function stableSort(array: readonly T[], comparer: ts.Comparer): ts.SortedReadonlyArray { - const indices = indicesOf(array); - stableSortIndices(array, indices, comparer); - return indices.map(i => array[i]) as ts.SortedArray as ts.SortedReadonlyArray; - } +/** + * Stable sort of an array. Elements equal to each other maintain their relative position in the array. + */ +export function stableSort(array: readonly T[], comparer: ts.Comparer): ts.SortedReadonlyArray { + const indices = indicesOf(array); + stableSortIndices(array, indices, comparer); + return indices.map(i => array[i]) as ts.SortedArray as ts.SortedReadonlyArray; +} - export function rangeEquals(array1: readonly T[], array2: readonly T[], pos: number, end: number) { - while (pos < end) { - if (array1[pos] !== array2[pos]) { - return false; - } - pos++; +export function rangeEquals(array1: readonly T[], array2: readonly T[], pos: number, end: number) { + while (pos < end) { + if (array1[pos] !== array2[pos]) { + return false; } - return true; + 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]; - } +/** + * 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]; } - return undefined; } + 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]; - } +/** + * 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 { - ts.Debug.assert(array.length !== 0); - return array[0]; - } +export function first(array: readonly T[]): T { + ts.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]; - } +/** + * 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 { - ts.Debug.assert(array.length !== 0); - return array[array.length - 1]; - } +export function last(array: readonly T[]): T { + ts.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; - } +/** + * 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; +} - /** - * 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; - } +/** + * 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; +} - /** - * 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: ts.Comparer, offset?: number): number { - return binarySearchKey(array, keySelector(value), keySelector, keyComparer, offset); - } +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 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, i: number) => U, keyComparer: ts.Comparer, offset?: number): number { - if (!some(array)) { - return -1; - } +/** + * 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: ts.Comparer, offset?: number): number { + return binarySearchKey(array, keySelector(value), keySelector, keyComparer, offset); +} - let low = offset || 0; - let high = array.length - 1; - while (low <= high) { - const middle = low + ((high - low) >> 1); - const midKey = keySelector(array[middle], middle); - switch (keyComparer(midKey, key)) { - case ts.Comparison.LessThan: - low = middle + 1; - break; - case ts.Comparison.EqualTo: - return middle; - case ts.Comparison.GreaterThan: - high = middle - 1; - break; - } - } +/** + * 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, i: number) => U, keyComparer: ts.Comparer, offset?: number): number { + if (!some(array)) { + return -1; + } - return ~low; + let low = offset || 0; + let high = array.length - 1; + while (low <= high) { + const middle = low + ((high - low) >> 1); + const midKey = keySelector(array[middle], middle); + switch (keyComparer(midKey, key)) { + case ts.Comparison.LessThan: + low = middle + 1; + break; + case ts.Comparison.EqualTo: + return middle; + case ts.Comparison.GreaterThan: + high = middle - 1; + break; + } } - 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 ~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 initial; } + return initial; +} - const hasOwnProperty = Object.prototype.hasOwnProperty; +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: ts.MapLike, key: string): boolean { - return hasOwnProperty.call(map, key); - } +/** + * 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: ts.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: ts.MapLike, key: string): T | undefined { - return hasOwnProperty.call(map, key) ? map[key] : undefined; - } +/** + * 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: ts.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: ts.MapLike): string[] { - const keys: string[] = []; - for (const key in map) { - if (hasOwnProperty.call(map, key)) { - keys.push(key); - } +/** + * Gets the owned, enumerable property keys of a map-like. + */ +export function getOwnKeys(map: ts.MapLike): string[] { + const keys: string[] = []; + for (const key in map) { + if (hasOwnProperty.call(map, key)) { + keys.push(key); } - - return keys; } - 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; - } + return keys; +} - export function getOwnValues(collection: ts.MapLike | T[]): T[] { - const values: T[] = []; - for (const key in collection) { - if (hasOwnProperty.call(collection, key)) { - values.push((collection as ts.MapLike)[key]); - } +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; +} - return values; +export function getOwnValues(collection: ts.MapLike | T[]): T[] { + const values: T[] = []; + for (const key in collection) { + if (hasOwnProperty.call(collection, key)) { + values.push((collection as ts.MapLike)[key]); + } } - const _entries = Object.entries || ((obj: ts.MapLike) => { - const keys = getOwnKeys(obj); - const result: [ - string, - T - ][] = Array(keys.length); - for (let i = 0; i < keys.length; i++) { - result[i] = [keys[i], obj[keys[i]]]; - } - return result; - }); + return values; +} - export function getEntries(obj: ts.MapLike): [ +const _entries = Object.entries || ((obj: ts.MapLike) => { + const keys = getOwnKeys(obj); + const result: [ string, T - ][] { - return obj ? _entries(obj) : []; - } + ][] = Array(keys.length); + for (let i = 0; i < keys.length; i++) { + result[i] = [keys[i], obj[keys[i]]]; + } + return result; +}); + +export function getEntries(obj: ts.MapLike): [ + string, + T +][] { + return obj ? _entries(obj) : []; +} - export function arrayOf(count: number, f: (index: number) => T): T[] { - const result = new Array(count); - for (let i = 0; i < count; i++) { - result[i] = f(i); - } - return result; +export function arrayOf(count: number, f: (index: number) => T): T[] { + const result = new Array(count); + for (let i = 0; i < count; i++) { + result[i] = f(i); } + return result; +} - /** Shims `Array.from`. */ - export function arrayFrom(iterator: ts.Iterator | IterableIterator, map: (t: T) => U): U[]; - export function arrayFrom(iterator: ts.Iterator | IterableIterator): T[]; - 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); - } - return result; +/** Shims `Array.from`. */ +export function arrayFrom(iterator: ts.Iterator | IterableIterator, map: (t: T) => U): U[]; +export function arrayFrom(iterator: ts.Iterator | IterableIterator): T[]; +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); } + 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]; - } +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]; } } - return t; } + 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: ts.MapLike | undefined, right: ts.MapLike | undefined, equalityComparer: ts.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; - } - } - +/** + * 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: ts.MapLike | undefined, right: ts.MapLike | undefined, equalityComparer: ts.EqualityComparer = equateValues) { + if (left === right) 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 V[], makeKey: (value: V) => K | undefined): ts.ESMap; - export function arrayToMap(array: readonly V1[], makeKey: (value: V1) => K | undefined, makeValue: (value: V1) => V2): ts.ESMap; - export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined): ts.ESMap; - export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => U): ts.ESMap; - export function arrayToMap(array: readonly V1[], makeKey: (value: V1) => K | undefined, makeValue: (value: V1) => V1 | V2 = identity): ts.ESMap { - const result = new ts.Map(); - for (const value of array) { - const key = makeKey(value); - if (key !== undefined) - result.set(key, makeValue(value)); + 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 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); + for (const key in right) { + if (hasOwnProperty.call(right, key)) { + if (!hasOwnProperty.call(left, key)) + return false; } - return result; } - export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K): MultiMap; - export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K, makeValue: (value: V) => U): MultiMap; - export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K, makeValue: (value: V) => V | U = identity): MultiMap { - const result = createMultiMap(); - for (const value of values) { - result.add(makeKey(value), makeValue(value)); - } - return result; + 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 V[], makeKey: (value: V) => K | undefined): ts.ESMap; +export function arrayToMap(array: readonly V1[], makeKey: (value: V1) => K | undefined, makeValue: (value: V1) => V2): ts.ESMap; +export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined): ts.ESMap; +export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => U): ts.ESMap; +export function arrayToMap(array: readonly V1[], makeKey: (value: V1) => K | undefined, makeValue: (value: V1) => V1 | V2 = identity): ts.ESMap { + const result = new ts.Map(); + 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 group(values: readonly T[], getGroupId: (value: T) => K): readonly (readonly T[])[]; - export function group(values: readonly T[], getGroupId: (value: T) => K, resultSelector: (values: readonly T[]) => R): R[]; - 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) => K, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] { - return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector); +export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K): MultiMap; +export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K, makeValue: (value: V) => U): MultiMap; +export function arrayToMultiMap(values: readonly V[], makeKey: (value: V) => K, makeValue: (value: V) => V | U = identity): MultiMap { + const result = createMultiMap(); + for (const value of values) { + result.add(makeKey(value), makeValue(value)); } + return result; +} - export function clone(object: T): T { - const result: any = {}; - for (const id in object) { - if (hasOwnProperty.call(object, id)) { - result[id] = (object as any)[id]; - } +export function group(values: readonly T[], getGroupId: (value: T) => K): readonly (readonly T[])[]; +export function group(values: readonly T[], getGroupId: (value: T) => K, resultSelector: (values: readonly T[]) => R): R[]; +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) => K, 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 as any)[id]; } - return result; } + 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 = {} as any; - for (const id in second) { - if (hasOwnProperty.call(second, id)) { - (result as any)[id] = (second as any)[id]; - } +/** + * 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 = {} as any; + 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]; - } + for (const id in first) { + if (hasOwnProperty.call(first, id)) { + (result as any)[id] = (first as any)[id]; } - - return result; } - 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; +} + +export function copyProperties(first: T1, second: T2) { + for (const id in second) { + if (hasOwnProperty.call(second, id)) { + (first as any)[id] = second[id]; } } +} - export function maybeBind(obj: T, fn: ((this: T, ...args: A) => R) | undefined): ((...args: A) => R) | undefined { - return fn ? fn.bind(obj) : undefined; - } +export function maybeBind(obj: T, fn: ((this: T, ...args: A) => R) | undefined): ((...args: A) => R) | undefined { + return fn ? fn.bind(obj) : undefined; +} - export interface MultiMap extends ts.ESMap { - /** - * 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: K, value: V): V[]; - /** - * 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: K, value: V): void; - } +export interface MultiMap extends ts.ESMap { + /** + * 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: K, value: V): V[]; + /** + * 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: K, value: V): void; +} - export function createMultiMap(): MultiMap; - export function createMultiMap(): MultiMap; - export function createMultiMap(): MultiMap { - const map = new ts.Map() as MultiMap; - map.add = multiMapAdd; - map.remove = multiMapRemove; - return map; +export function createMultiMap(): MultiMap; +export function createMultiMap(): MultiMap; +export function createMultiMap(): MultiMap { + const map = new ts.Map() as MultiMap; + map.add = multiMapAdd; + map.remove = multiMapRemove; + return map; +} +function multiMapAdd(this: MultiMap, key: K, value: V) { + let values = this.get(key); + if (values) { + values.push(value); } - function multiMapAdd(this: MultiMap, key: K, value: V) { - let values = this.get(key); - if (values) { - values.push(value); - } - else { - this.set(key, values = [value]); - } - return values; + else { + this.set(key, values = [value]); } - function multiMapRemove(this: MultiMap, key: K, value: V) { - const values = this.get(key); - if (values) { - unorderedRemoveItem(values, value); - if (!values.length) { - this.delete(key); - } + return values; +} +function multiMapRemove(this: MultiMap, key: K, value: V) { + const values = this.get(key); + if (values) { + unorderedRemoveItem(values, value); + if (!values.length) { + this.delete(key); } } +} - export interface UnderscoreEscapedMultiMap extends ts.UnderscoreEscapedMap { - /** - * 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: ts.__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: ts.__String, value: T): void; - } - - export function createUnderscoreEscapedMultiMap(): UnderscoreEscapedMultiMap { - return createMultiMap() as UnderscoreEscapedMultiMap; - } - +export interface UnderscoreEscapedMultiMap extends ts.UnderscoreEscapedMap { /** - * Creates a Set with custom equality and hash code functionality. This is useful when you - * want to use something looser than object identity - e.g. "has the same span". - * - * If `equals(a, b)`, it must be the case that `getHashCode(a) === getHashCode(b)`. - * The converse is not required. - * - * To facilitate a perf optimization (lazy allocation of bucket arrays), `TElement` is - * assumed not to be an array type. + * 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 function createSet(getHashCode: (element: TElement) => THash, equals: ts.EqualityComparer): ts.Set { - const multiMap = new ts.Map(); - let size = 0; + add(key: ts.__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: ts.__String, value: T): void; +} - function getElementIterator(): ts.Iterator { - const valueIt = multiMap.values(); - let arrayIt: ts.Iterator | undefined; - return { - next: () => { - while (true) { - if (arrayIt) { - const n = arrayIt.next(); - if (!n.done) { - return { value: n.value }; - } - arrayIt = undefined; +export function createUnderscoreEscapedMultiMap(): UnderscoreEscapedMultiMap { + return createMultiMap() as UnderscoreEscapedMultiMap; +} + +/** + * Creates a Set with custom equality and hash code functionality. This is useful when you + * want to use something looser than object identity - e.g. "has the same span". + * + * If `equals(a, b)`, it must be the case that `getHashCode(a) === getHashCode(b)`. + * The converse is not required. + * + * To facilitate a perf optimization (lazy allocation of bucket arrays), `TElement` is + * assumed not to be an array type. + */ +export function createSet(getHashCode: (element: TElement) => THash, equals: ts.EqualityComparer): ts.Set { + const multiMap = new ts.Map(); + let size = 0; + + function getElementIterator(): ts.Iterator { + const valueIt = multiMap.values(); + let arrayIt: ts.Iterator | undefined; + return { + next: () => { + while (true) { + if (arrayIt) { + const n = arrayIt.next(); + if (!n.done) { + return { value: n.value }; } - else { - const n = valueIt.next(); - if (n.done) { - return { value: undefined, done: true }; - } - if (!isArray(n.value)) { - return { value: n.value }; - } - arrayIt = arrayIterator(n.value); + arrayIt = undefined; + } + else { + const n = valueIt.next(); + if (n.done) { + return { value: undefined, done: true }; } + if (!isArray(n.value)) { + return { value: n.value }; + } + arrayIt = arrayIterator(n.value); } } - }; - } + } + }; + } - const set: ts.Set = { - has(element: TElement): boolean { - const hash = getHashCode(element); - if (!multiMap.has(hash)) - return false; - const candidates = multiMap.get(hash)!; - if (!isArray(candidates)) - return equals(candidates, element); + const set: ts.Set = { + has(element: TElement): boolean { + const hash = getHashCode(element); + if (!multiMap.has(hash)) + return false; + const candidates = multiMap.get(hash)!; + if (!isArray(candidates)) + return equals(candidates, element); - for (const candidate of candidates) { - if (equals(candidate, element)) { - return true; - } + for (const candidate of candidates) { + if (equals(candidate, element)) { + return true; } - return false; - }, - add(element: TElement): ts.Set { - const hash = getHashCode(element); - if (multiMap.has(hash)) { - const values = multiMap.get(hash)!; - if (isArray(values)) { - if (!contains(values, element, equals)) { - values.push(element); - size++; - } - } - else { - const value = values; - if (!equals(value, element)) { - multiMap.set(hash, [ value, element ]); - size++; - } + } + return false; + }, + add(element: TElement): ts.Set { + const hash = getHashCode(element); + if (multiMap.has(hash)) { + const values = multiMap.get(hash)!; + if (isArray(values)) { + if (!contains(values, element, equals)) { + values.push(element); + size++; } } else { - multiMap.set(hash, element); - size++; + const value = values; + if (!equals(value, element)) { + multiMap.set(hash, [ value, element ]); + size++; + } } + } + else { + multiMap.set(hash, element); + size++; + } - return this; - }, - delete(element: TElement): boolean { - const hash = getHashCode(element); - if (!multiMap.has(hash)) - return false; - const candidates = multiMap.get(hash)!; - if (isArray(candidates)) { - for (let i = 0; i < candidates.length; i++) { - if (equals(candidates[i], element)) { - if (candidates.length === 1) { - multiMap.delete(hash); - } - else if (candidates.length === 2) { - multiMap.set(hash, candidates[1 - i]); - } - else { - unorderedRemoveItemAt(candidates, i); - } - size--; - return true; + return this; + }, + delete(element: TElement): boolean { + const hash = getHashCode(element); + if (!multiMap.has(hash)) + return false; + const candidates = multiMap.get(hash)!; + if (isArray(candidates)) { + for (let i = 0; i < candidates.length; i++) { + if (equals(candidates[i], element)) { + if (candidates.length === 1) { + multiMap.delete(hash); + } + else if (candidates.length === 2) { + multiMap.set(hash, candidates[1 - i]); + } + else { + unorderedRemoveItemAt(candidates, i); } - } - } - else { - const candidate = candidates; - if (equals(candidate, element)) { - multiMap.delete(hash); size--; return true; } } + } + else { + const candidate = candidates; + if (equals(candidate, element)) { + multiMap.delete(hash); + size--; + return true; + } + } - return false; - }, - clear(): void { - multiMap.clear(); - size = 0; - }, - get size() { - return size; - }, - forEach(action: (value: TElement, key: TElement) => void): void { - for (const elements of arrayFrom(multiMap.values())) { - if (isArray(elements)) { - for (const element of elements) { - action(element, element); - } - } - else { - const element = elements; + return false; + }, + clear(): void { + multiMap.clear(); + size = 0; + }, + get size() { + return size; + }, + forEach(action: (value: TElement, key: TElement) => void): void { + for (const elements of arrayFrom(multiMap.values())) { + if (isArray(elements)) { + for (const element of elements) { action(element, element); } } - }, - keys(): ts.Iterator { - return getElementIterator(); - }, - values(): ts.Iterator { - return getElementIterator(); - }, - entries(): ts.Iterator<[ - TElement, - TElement - ]> { - const it = getElementIterator(); - return { - next: () => { - const n = it.next(); - return n.done ? n : { value: [ n.value, n.value ] }; - } - }; - }, - }; + else { + const element = elements; + action(element, element); + } + } + }, + keys(): ts.Iterator { + return getElementIterator(); + }, + values(): ts.Iterator { + return getElementIterator(); + }, + entries(): ts.Iterator<[ + TElement, + TElement + ]> { + const it = getElementIterator(); + return { + next: () => { + const n = it.next(); + return n.done ? n : { value: [ n.value, n.value ] }; + } + }; + }, + }; - return set; - } + return set; +} - /** - * Tests whether a value is an array. - */ - export function isArray(value: any): value is readonly unknown[] { - return Array.isArray ? Array.isArray(value) : value instanceof Array; - } +/** + * Tests whether a value is an array. + */ +export function isArray(value: any): value is readonly unknown[] { + 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]; - } +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]; +} - /** - * 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"; - } +/** + * 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 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 ts.Debug.fail(`Invalid cast. The supplied value ${value} did not pass the test '${ts.Debug.getFunctionName(test)}'.`); +} + +/** Does nothing. */ +export function noop(_?: unknown): 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 | Upper 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; + }; +} - export function cast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut { - if (value !== undefined && test(value)) - return value; - return ts.Debug.fail(`Invalid cast. The supplied value ${value} did not pass the test '${ts.Debug.getFunctionName(test)}'.`); - } +/** A version of `memoize` that supports a single primitive argument */ +export function memoizeOne(callback: (arg: A) => T): (arg: A) => T { + const map = new ts.Map(); + return (arg: A) => { + const key = `${typeof arg}:${arg}`; + let value = map.get(key); + if (value === undefined && !map.has(key)) { + value = callback(arg); + map.set(key, value); + } + return value!; + }; +} - /** Does nothing. */ - export function noop(_?: unknown): void { } +/** + * 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]; + } - /** Do nothing and return false */ - export function returnFalse(): false { - return false; + return t => reduceLeft(args, (u, f) => f(u), t); } - - /** Do nothing and return true */ - export function returnTrue(): true { - return true; + else if (d) { + return t => d(c(b(a(t)))); } - - /** Do nothing and return undefined */ - export function returnUndefined(): undefined { - return undefined; + else if (c) { + return t => c(b(a(t))); } - - /** 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 | Upper 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; + else if (b) { + return t => b(a(t)); } - - /** Throws an error because a function is not implemented. */ - export function notImplemented(): never { - throw new Error("Not implemented"); + else if (a) { + return t => a(t); } - - export function memoize(callback: () => T): () => T { - let value: T; - return () => { - if (callback) { - value = callback(); - callback = undefined!; - } - return value; - }; + else { + return t => t; } +} - /** A version of `memoize` that supports a single primitive argument */ - export function memoizeOne(callback: (arg: A) => T): (arg: A) => T { - const map = new ts.Map(); - return (arg: A) => { - const key = `${typeof arg}:${arg}`; - let value = map.get(key); - if (value === undefined && !map.has(key)) { - value = callback(arg); - map.set(key, value); - } - return value!; - }; - } +export const enum AssertionLevel { + None = 0, + Normal = 1, + Aggressive = 2, + VeryAggressive = 3 +} - /** - * 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]; - } +/** + * 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. + */ +export type AnyFunction = (...args: never[]) => void; +export type AnyConstructor = new (...args: unknown[]) => unknown; - 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)); - } - else if (a) { - return t => a(t); - } - else { - return t => t; - } - } +export function equateValues(a: T, b: T) { + return a === b; +} - export const enum AssertionLevel { - None = 0, - Normal = 1, - Aggressive = 2, - VeryAggressive = 3 - } +/** + * 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)). + */ +export function equateStringsCaseInsensitive(a: string, b: string) { + return a === b + || a !== undefined + && b !== undefined + && a.toUpperCase() === b.toUpperCase(); +} - /** - * 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. - */ - export type AnyFunction = (...args: never[]) => void; - export type AnyConstructor = new (...args: unknown[]) => unknown; +/** + * 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); +} - export function equateValues(a: T, b: T) { - return a === b; - } +function compareComparableValues(a: string | undefined, b: string | undefined): ts.Comparison; +function compareComparableValues(a: number | undefined, b: number | undefined): ts.Comparison; +function compareComparableValues(a: string | number | undefined, b: string | number | undefined) { + return a === b ? ts.Comparison.EqualTo : + a === undefined ? ts.Comparison.LessThan : + b === undefined ? ts.Comparison.GreaterThan : + a < b ? ts.Comparison.LessThan : + ts.Comparison.GreaterThan; +} - /** - * 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)). - */ - export function equateStringsCaseInsensitive(a: string, b: string) { - return a === b - || a !== undefined - && b !== undefined - && a.toUpperCase() === b.toUpperCase(); - } +/** + * 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): ts.Comparison { + return compareComparableValues(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. - */ - export function equateStringsCaseSensitive(a: string, b: string) { - return equateValues(a, b); - } +/** + * Compare two TextSpans, first by `start`, then by `length`. + */ +export function compareTextSpans(a: Partial | undefined, b: Partial | undefined): ts.Comparison { + return compareValues(a?.start, b?.start) || compareValues(a?.length, b?.length); +} - function compareComparableValues(a: string | undefined, b: string | undefined): ts.Comparison; - function compareComparableValues(a: number | undefined, b: number | undefined): ts.Comparison; - function compareComparableValues(a: string | number | undefined, b: string | number | undefined) { - return a === b ? ts.Comparison.EqualTo : - a === undefined ? ts.Comparison.LessThan : - b === undefined ? ts.Comparison.GreaterThan : - a < b ? ts.Comparison.LessThan : - ts.Comparison.GreaterThan; - } +export function min(a: T, b: T, compare: ts.Comparer): T { + return compare(a, b) === ts.Comparison.LessThan ? a : b; +} - /** - * 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): ts.Comparison { - return compareComparableValues(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)). + */ +export function compareStringsCaseInsensitive(a: string, b: string) { + if (a === b) + return ts.Comparison.EqualTo; + if (a === undefined) + return ts.Comparison.LessThan; + if (b === undefined) + return ts.Comparison.GreaterThan; + a = a.toUpperCase(); + b = b.toUpperCase(); + return a < b ? ts.Comparison.LessThan : a > b ? ts.Comparison.GreaterThan : ts.Comparison.EqualTo; +} - /** - * Compare two TextSpans, first by `start`, then by `length`. - */ - export function compareTextSpans(a: Partial | undefined, b: Partial | undefined): ts.Comparison { - return compareValues(a?.start, b?.start) || compareValues(a?.length, b?.length); - } +/** + * 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): ts.Comparison { + return compareComparableValues(a, b); +} - export function min(a: T, b: T, compare: ts.Comparer): T { - return compare(a, b) === ts.Comparison.LessThan ? a : b; - } +export function getStringComparer(ignoreCase?: boolean) { + return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; +} - /** - * 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) { +/** + * Creates a string comparer for use with string collation in the UI. + */ +const createUIStringComparer = (() => { + let defaultComparer: ts.Comparer | undefined; + let enUSComparer: ts.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 ts.Comparison.EqualTo; if (a === undefined) return ts.Comparison.LessThan; if (b === undefined) return ts.Comparison.GreaterThan; - a = a.toUpperCase(); - b = b.toUpperCase(); - return a < b ? ts.Comparison.LessThan : a > b ? ts.Comparison.GreaterThan : ts.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): ts.Comparison { - return compareComparableValues(a, b); + const value = comparer(a, b); + return value < 0 ? ts.Comparison.LessThan : value > 0 ? ts.Comparison.GreaterThan : ts.Comparison.EqualTo; } - export function getStringComparer(ignoreCase?: boolean) { - return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; + function createIntlCollatorStringComparer(locale: string | undefined): ts.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); } - /** - * Creates a string comparer for use with string collation in the UI. - */ - const createUIStringComparer = (() => { - let defaultComparer: ts.Comparer | undefined; - let enUSComparer: ts.Comparer | undefined; - - const stringComparerFactory = getStringComparerFactory(); - return createStringComparer; + function createLocaleCompareStringComparer(locale: string | undefined): ts.Comparer { + // if the locale is not the default locale (`undefined`), use the fallback comparer. + if (locale !== undefined) + return createFallbackStringComparer(); - function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) { - if (a === b) - return ts.Comparison.EqualTo; - if (a === undefined) - return ts.Comparison.LessThan; - if (b === undefined) - return ts.Comparison.GreaterThan; - const value = comparer(a, b); - return value < 0 ? ts.Comparison.LessThan : value > 0 ? ts.Comparison.GreaterThan : ts.Comparison.EqualTo; - } + return (a, b) => compareWithCallback(a, b, compareStrings); - function createIntlCollatorStringComparer(locale: string | undefined): ts.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 compareStrings(a: string, b: string) { + return a.localeCompare(b); } + } - function createLocaleCompareStringComparer(locale: string | undefined): ts.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 createFallbackStringComparer(): ts.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 compareStrings(a: string, b: string) { - return a.localeCompare(b); - } + function compareDictionaryOrder(a: string, b: string) { + return compareStrings(a.toUpperCase(), b.toUpperCase()) || compareStrings(a, b); } - function createFallbackStringComparer(): ts.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 ? ts.Comparison.LessThan : a > b ? ts.Comparison.GreaterThan : ts.Comparison.EqualTo; - } + function compareStrings(a: string, b: string) { + return a < b ? ts.Comparison.LessThan : a > b ? ts.Comparison.GreaterThan : ts.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 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; } - 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); - } + // 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; } - })(); - - let uiComparerCaseSensitive: ts.Comparer | undefined; - let uiLocale: string | undefined; - export function getUILocale() { - return uiLocale; + // Otherwise, fall back to ordinal comparison: + return createFallbackStringComparer; } - export function setUILocale(value: string | undefined) { - if (uiLocale !== value) { - uiLocale = value; - uiComparerCaseSensitive = undefined; + 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); } } +})(); - /** - * 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); - } +let uiComparerCaseSensitive: ts.Comparer | undefined; +let uiLocale: string | undefined; - export function compareProperties(a: T | undefined, b: T | undefined, key: K, comparer: ts.Comparer): ts.Comparison { - return a === b ? ts.Comparison.EqualTo : - a === undefined ? ts.Comparison.LessThan : - b === undefined ? ts.Comparison.GreaterThan : - comparer(a[key], b[key]); - } +export function getUILocale() { + return uiLocale; +} - /** True is greater than false. */ - export function compareBooleans(a: boolean, b: boolean): ts.Comparison { - return compareValues(a ? 1 : 0, b ? 1 : 0); +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. - * - * find 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 is worse than this, don't bother. - let bestCandidate: T | undefined; - for (const candidate of candidates) { - const candidateName = getName(candidate); - if (candidateName !== undefined && Math.abs(candidateName.length - name.length) <= maximumLengthDifference) { - if (candidateName === name) { - continue; - } - // Only consider candidates less than 3 characters long when they differ by case. - // Otherwise, don't bother, since a user would usually notice differences of a 2-character name. - if (candidateName.length < 3 && candidateName.toLowerCase() !== name.toLowerCase()) { - continue; - } - - const distance = levenshteinWithMax(name, candidateName, bestDistance - 0.1); - if (distance === undefined) { - continue; - } - - ts.Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined - bestDistance = distance; - bestCandidate = candidate; - } - } - return bestCandidate; - } +/** + * 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); +} - 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 + 0.01; +export function compareProperties(a: T | undefined, b: T | undefined, key: K, comparer: ts.Comparer): ts.Comparison { + return a === b ? ts.Comparison.EqualTo : + a === undefined ? ts.Comparison.LessThan : + b === undefined ? ts.Comparison.GreaterThan : + comparer(a[key], b[key]); +} - for (let i = 0; i <= s2.length; i++) { - previous[i] = i; - } +/** True is greater than false. */ +export function compareBooleans(a: boolean, b: boolean): ts.Comparison { + return compareValues(a ? 1 : 0, b ? 1 : 0); +} - for (let i = 1; i <= s1.length; i++) { - const c1 = s1.charCodeAt(i - 1); - const minJ = Math.ceil(i > max ? i - max : 1); - const maxJ = Math.floor(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++) { - // case difference should be significantly cheaper than other differences - const substitutionDistance = s1[i - 1].toLowerCase() === s2[j-1].toLowerCase() - ? (previous[j - 1] + 0.1) - : (previous[j - 1] + 2); - const dist = c1 === s2.charCodeAt(j - 1) - ? previous[j - 1] - : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ substitutionDistance); - current[j] = dist; - colMin = Math.min(colMin, dist); +/** + * 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. + * + * find 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 is worse than this, don't bother. + let bestCandidate: T | undefined; + for (const candidate of candidates) { + const candidateName = getName(candidate); + if (candidateName !== undefined && Math.abs(candidateName.length - name.length) <= maximumLengthDifference) { + if (candidateName === name) { + continue; } - for (let j = maxJ + 1; j <= s2.length; j++) { - current[j] = big; + // Only consider candidates less than 3 characters long when they differ by case. + // Otherwise, don't bother, since a user would usually notice differences of a 2-character name. + if (candidateName.length < 3 && candidateName.toLowerCase() !== name.toLowerCase()) { + continue; } - if (colMin > max) { - // Give up -- everything in this column is > max and it can't get better in future columns. - return undefined; + + const distance = levenshteinWithMax(name, candidateName, bestDistance - 0.1); + if (distance === undefined) { + continue; } - const temp = previous; - previous = current; - current = temp; + ts.Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined + bestDistance = distance; + bestCandidate = candidate; } - - const res = previous[s2.length]; - return res > max ? undefined : res; } + return bestCandidate; +} - export function endsWith(str: string, suffix: string): boolean { - const expectedPos = str.length - suffix.length; - return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; - } +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 + 0.01; + + 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 = Math.ceil(i > max ? i - max : 1); + const maxJ = Math.floor(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++) { + // case difference should be significantly cheaper than other differences + const substitutionDistance = s1[i - 1].toLowerCase() === s2[j-1].toLowerCase() + ? (previous[j - 1] + 0.1) + : (previous[j - 1] + 2); + const dist = c1 === s2.charCodeAt(j - 1) + ? previous[j - 1] + : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ substitutionDistance); + 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; + } - export function removeSuffix(str: string, suffix: string): string { - return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; + 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; +} - export function stringContains(str: string, substring: string): boolean { - return str.indexOf(substring) !== -1; - } +export function endsWith(str: string, suffix: string): boolean { + const expectedPos = str.length - suffix.length; + return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; +} - /** - * Takes a string like "jquery-min.4.2.3" and returns "jquery" - */ - export function removeMinAndVersionNumbers(fileName: string) { - // We used to use the regex /[.-]((min)|(\d+(\.\d+)*))$/ and would just .replace it twice. - // Unfortunately, that regex has O(n^2) performance because v8 doesn't match from the end of the string. - // Instead, we now essentially scan the filename (backwards) ourselves. - - let end: number = fileName.length; - - for (let pos = end - 1; pos > 0; pos--) { - let ch: number = fileName.charCodeAt(pos); - if (ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9) { - // Match a \d+ segment - do { - --pos; - ch = fileName.charCodeAt(pos); - } while (pos > 0 && ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9); - } - else if (pos > 4 && (ch === ts.CharacterCodes.n || ch === ts.CharacterCodes.N)) { - // Looking for "min" or "min" - // Already matched the 'n' - --pos; - ch = fileName.charCodeAt(pos); - if (ch !== ts.CharacterCodes.i && ch !== ts.CharacterCodes.I) { - break; - } - --pos; - ch = fileName.charCodeAt(pos); - if (ch !== ts.CharacterCodes.m && ch !== ts.CharacterCodes.M) { - break; - } +export function removeSuffix(str: string, suffix: string): string { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; +} + +export function tryRemoveSuffix(str: string, suffix: string): string | undefined { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; +} + +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" + */ +export function removeMinAndVersionNumbers(fileName: string) { + // We used to use the regex /[.-]((min)|(\d+(\.\d+)*))$/ and would just .replace it twice. + // Unfortunately, that regex has O(n^2) performance because v8 doesn't match from the end of the string. + // Instead, we now essentially scan the filename (backwards) ourselves. + + let end: number = fileName.length; + + for (let pos = end - 1; pos > 0; pos--) { + let ch: number = fileName.charCodeAt(pos); + if (ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9) { + // Match a \d+ segment + do { --pos; ch = fileName.charCodeAt(pos); - } - else { - // This character is not part of either suffix pattern + } while (pos > 0 && ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9); + } + else if (pos > 4 && (ch === ts.CharacterCodes.n || ch === ts.CharacterCodes.N)) { + // Looking for "min" or "min" + // Already matched the 'n' + --pos; + ch = fileName.charCodeAt(pos); + if (ch !== ts.CharacterCodes.i && ch !== ts.CharacterCodes.I) { break; } - - if (ch !== ts.CharacterCodes.minus && ch !== ts.CharacterCodes.dot) { + --pos; + ch = fileName.charCodeAt(pos); + if (ch !== ts.CharacterCodes.m && ch !== ts.CharacterCodes.M) { break; } + --pos; + ch = fileName.charCodeAt(pos); + } + else { + // This character is not part of either suffix pattern + break; + } - end = pos; + if (ch !== ts.CharacterCodes.minus && ch !== ts.CharacterCodes.dot) { + break; } - // end might be fileName.length, in which case this should internally no-op - return end === fileName.length ? fileName : fileName.slice(0, end); + end = pos; } - /** 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; - } - } - return false; - } + // end might be fileName.length, in which case this should internally no-op + return end === fileName.length ? fileName : fileName.slice(0, end); +} - /** 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]; +/** 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(); } + return false; +} - export function unorderedRemoveItemAt(array: T[], index: number): void { - // Fill in the "hole" left at `index`. - array[index] = array[array.length - 1]; - array.pop(); +/** 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(); +} - /** Remove the *first* occurrence of `item` from the array. */ - export function unorderedRemoveItem(array: T[], item: T) { - return unorderedRemoveFirstItemWhere(array, element => element === item); - } +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* 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 true; - } +/** 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 true; } - return false; } + return false; +} - export type GetCanonicalFileName = (fileName: string) => string; - export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { - return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; - } +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; - } +/** Represents a "prefix*suffix" pattern. */ +export interface Pattern { + prefix: string; + suffix: string; +} - export function patternText({ prefix, suffix }: Pattern): string { - return `${prefix}*${suffix}`; - } +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 { - ts.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; - } - } +/** + * 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 { + ts.Debug.assert(isPatternMatch(pattern, candidate)); + return candidate.substring(pattern.prefix.length, candidate.length - pattern.suffix.length); +} - return matchedValue; - } +/** 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; - export function startsWith(str: string, prefix: string): boolean { - return str.lastIndexOf(prefix, 0) === 0; + for (const v of values) { + const pattern = getPattern(v); + if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) { + longestMatchPrefixLength = pattern.prefix.length; + matchedValue = v; + } } - export function removePrefix(str: string, prefix: string): string { - return startsWith(str, prefix) ? str.substr(prefix.length) : str; - } + return matchedValue; +} - export function tryRemovePrefix(str: string, prefix: string, getCanonicalFileName: GetCanonicalFileName = identity): string | undefined { - return startsWith(getCanonicalFileName(str), getCanonicalFileName(prefix)) ? str.substring(prefix.length) : undefined; - } +export function startsWith(str: string, prefix: string): boolean { + return str.lastIndexOf(prefix, 0) === 0; +} - function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { - return candidate.length >= prefix.length + suffix.length && - startsWith(candidate, prefix) && - endsWith(candidate, suffix); - } +export function removePrefix(str: string, prefix: string): string { + return startsWith(str, prefix) ? str.substr(prefix.length) : str; +} - export function and(f: (arg: T) => boolean, g: (arg: T) => boolean) { - return (arg: T) => f(arg) && g(arg); - } +export function tryRemovePrefix(str: string, prefix: string, getCanonicalFileName: GetCanonicalFileName = identity): string | undefined { + return startsWith(getCanonicalFileName(str), getCanonicalFileName(prefix)) ? str.substring(prefix.length) : undefined; +} - export function or(...fs: ((...args: T) => U)[]): (...args: T) => U { - return (...args) => { - let lastResult: U; - for (const f of fs) { - lastResult = f(...args); - if (lastResult) { - return lastResult; - } +function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { + return candidate.length >= prefix.length + suffix.length && + startsWith(candidate, prefix) && + endsWith(candidate, suffix); +} + +export function and(f: (arg: T) => boolean, g: (arg: T) => boolean) { + return (arg: T) => f(arg) && g(arg); +} + +export function or(...fs: ((...args: T) => U)[]): (...args: T) => U { + return (...args) => { + let lastResult: U; + for (const f of fs) { + lastResult = f(...args); + if (lastResult) { + return lastResult; } - return lastResult!; - }; - } + } + return lastResult!; + }; +} - export function not(fn: (...args: T) => boolean): (...args: T) => boolean { - return (...args) => !fn(...args); - } +export function not(fn: (...args: T) => boolean): (...args: T) => boolean { + return (...args) => !fn(...args); +} - export function assertType(_: T): void { } +export function assertType(_: T): void { } - export function singleElementArray(t: T | undefined): T[] | undefined { - return t === undefined ? undefined : [t]; - } +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) => ts.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; - let hasChanges = false; - while (newIndex < newLen && oldIndex < oldLen) { - const newItem = newItems[newIndex]; - const oldItem = oldItems[oldIndex]; - const compareResult = comparer(newItem, oldItem); - if (compareResult === ts.Comparison.LessThan) { - inserted(newItem); - newIndex++; - hasChanges = true; - } - else if (compareResult === ts.Comparison.GreaterThan) { - deleted(oldItem); - oldIndex++; - hasChanges = true; - } - else { - unchanged(oldItem, newItem); - newIndex++; - oldIndex++; - } - } - while (newIndex < newLen) { - inserted(newItems[newIndex++]); +export function enumerateInsertsAndDeletes(newItems: readonly T[], oldItems: readonly U[], comparer: (a: T, b: U) => ts.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; + let hasChanges = false; + while (newIndex < newLen && oldIndex < oldLen) { + const newItem = newItems[newIndex]; + const oldItem = oldItems[oldIndex]; + const compareResult = comparer(newItem, oldItem); + if (compareResult === ts.Comparison.LessThan) { + inserted(newItem); + newIndex++; hasChanges = true; } - while (oldIndex < oldLen) { - deleted(oldItems[oldIndex++]); + else if (compareResult === ts.Comparison.GreaterThan) { + deleted(oldItem); + oldIndex++; hasChanges = true; } - return hasChanges; - } - - 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); + else { + unchanged(oldItem, newItem); + newIndex++; + oldIndex++; } - return result; } - - export function cartesianProduct(arrays: readonly T[][]) { - const result: T[][] = []; - cartesianProductWorker(arrays, result, /*outer*/ undefined, 0); - return result; + while (newIndex < newLen) { + inserted(newItems[newIndex++]); + hasChanges = true; } - - 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); - } - } + while (oldIndex < oldLen) { + deleted(oldItems[oldIndex++]); + hasChanges = true; } + return hasChanges; +} - - /** - * Returns string left-padded with spaces or zeros until it reaches the given length. - * - * @param s String to pad. - * @param length Final padded length. If less than or equal to 's.length', returns 's' unchanged. - * @param padString Character to use as padding (default " "). - */ - export function padLeft(s: string, length: number, padString: " " | "0" = " ") { - return length <= s.length ? s : padString.repeat(length - s.length) + s; +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); } + return result; +} - /** - * Returns string right-padded with spaces until it reaches the given length. - * - * @param s String to pad. - * @param length Final padded length. If less than or equal to 's.length', returns 's' unchanged. - * @param padString Character to use as padding (default " "). - */ - export function padRight(s: string, length: number, padString: " " = " ") { - return length <= s.length ? s : s + padString.repeat(length - s.length); - } +export function cartesianProduct(arrays: readonly T[][]) { + const result: T[][] = []; + cartesianProductWorker(arrays, result, /*outer*/ undefined, 0); + return result; +} - export function takeWhile(array: readonly T[], predicate: (element: T) => element is U): U[]; - export function takeWhile(array: readonly T[], predicate: (element: T) => boolean): T[] { - const len = array.length; - let index = 0; - while (index < len && predicate(array[index])) { - index++; +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); } - return array.slice(0, index); } +} - /** - * Removes the leading and trailing white space and line terminator characters from a string. - */ - export const trimString = !!String.prototype.trim ? ((s: string) => s.trim()) : (s: string) => trimStringEnd(trimStringStart(s)); - /** - * Returns a copy with trailing whitespace removed. - */ - export const trimStringEnd = !!String.prototype.trimEnd ? ((s: string) => s.trimEnd()) : trimEndImpl; +/** + * Returns string left-padded with spaces or zeros until it reaches the given length. + * + * @param s String to pad. + * @param length Final padded length. If less than or equal to 's.length', returns 's' unchanged. + * @param padString Character to use as padding (default " "). + */ +export function padLeft(s: string, length: number, padString: " " | "0" = " ") { + return length <= s.length ? s : padString.repeat(length - s.length) + s; +} - /** - * Returns a copy with leading whitespace removed. - */ - export const trimStringStart = !!String.prototype.trimStart ? ((s: string) => s.trimStart()) : (s: string) => s.replace(/^\s+/g, ""); +/** + * Returns string right-padded with spaces until it reaches the given length. + * + * @param s String to pad. + * @param length Final padded length. If less than or equal to 's.length', returns 's' unchanged. + * @param padString Character to use as padding (default " "). + */ +export function padRight(s: string, length: number, padString: " " = " ") { + return length <= s.length ? s : s + padString.repeat(length - s.length); +} - /** - * https://jsbench.me/gjkoxld4au/1 - * The simple regex for this, /\s+$/g is O(n^2) in v8. - * The native .trimEnd method is by far best, but since that's technically ES2019, - * we provide a (still much faster than the simple regex) fallback. - */ - function trimEndImpl(s: string) { - let end = s.length - 1; - while (end >= 0) { - if (!ts.isWhiteSpaceLike(s.charCodeAt(end))) - break; - end--; - } - return s.slice(0, end + 1); +export function takeWhile(array: readonly T[], predicate: (element: T) => element is U): U[]; +export function takeWhile(array: readonly T[], predicate: (element: T) => boolean): T[] { + const len = array.length; + let index = 0; + while (index < len && predicate(array[index])) { + index++; } + return array.slice(0, index); +} + +/** + * Removes the leading and trailing white space and line terminator characters from a string. + */ +export const trimString = !!String.prototype.trim ? ((s: string) => s.trim()) : (s: string) => trimStringEnd(trimStringStart(s)); + +/** + * Returns a copy with trailing whitespace removed. + */ +export const trimStringEnd = !!String.prototype.trimEnd ? ((s: string) => s.trimEnd()) : trimEndImpl; + +/** + * Returns a copy with leading whitespace removed. + */ +export const trimStringStart = !!String.prototype.trimStart ? ((s: string) => s.trimStart()) : (s: string) => s.replace(/^\s+/g, ""); + +/** + * https://jsbench.me/gjkoxld4au/1 + * The simple regex for this, /\s+$/g is O(n^2) in v8. + * The native .trimEnd method is by far best, but since that's technically ES2019, + * we provide a (still much faster than the simple regex) fallback. + */ +function trimEndImpl(s: string) { + let end = s.length - 1; + while (end >= 0) { + if (!ts.isWhiteSpaceLike(s.charCodeAt(end))) + break; + end--; + } + return s.slice(0, end + 1); +} } diff --git a/src/compiler/corePublic.ts b/src/compiler/corePublic.ts index 75b43fdcb622d..9cea188e66ef9 100644 --- a/src/compiler/corePublic.ts +++ b/src/compiler/corePublic.ts @@ -1,179 +1,179 @@ 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 = "4.8"; - // The following is baselined as a literal template type without intervention - /** The version of the TypeScript compiler release */ - // eslint-disable-next-line @typescript-eslint/no-inferrable-types - export const version: string = `${versionMajorMinor}.0-dev`; +// 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 = "4.8"; +// The following is baselined as a literal template type without intervention +/** The version of the TypeScript compiler release */ +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const version: string = `${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; +} - /** - * 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 SortedReadonlyArray extends ReadonlyArray { - " __sortedArrayBrand": any; - } +export interface SortedArray extends Array { + " __sortedArrayBrand": any; +} - export interface SortedArray extends Array { - " __sortedArrayBrand": any; - } +/** Common read methods for ES6 Map/Set. */ +export interface ReadonlyCollection { + readonly size: number; + has(key: K): boolean; + keys(): Iterator; +} - /** Common read methods for ES6 Map/Set. */ - export interface ReadonlyCollection { - readonly size: number; - has(key: K): boolean; - keys(): Iterator; - } +/** Common write methods for ES6 Map/Set. */ +export interface Collection extends ReadonlyCollection { + delete(key: K): boolean; + clear(): void; +} - /** Common write methods for ES6 Map/Set. */ - export interface Collection extends ReadonlyCollection { - delete(key: K): boolean; - clear(): void; - } +/** ES6 Map interface, only read methods included. */ +export interface ReadonlyESMap extends ReadonlyCollection { + get(key: K): V | undefined; + values(): Iterator; + entries(): Iterator<[ + K, + V + ]>; + forEach(action: (value: V, key: K) => void): void; +} - /** ES6 Map interface, only read methods included. */ - export interface ReadonlyESMap extends ReadonlyCollection { - get(key: K): V | undefined; - values(): Iterator; - entries(): Iterator<[ - K, - V - ]>; - forEach(action: (value: V, key: K) => void): void; - } +/** + * ES6 Map interface, only read methods included. + */ +export interface ReadonlyMap extends ReadonlyESMap { +} - /** - * ES6 Map interface, only read methods included. - */ - export interface ReadonlyMap extends ReadonlyESMap { - } +/** ES6 Map interface. */ +export interface ESMap extends ReadonlyESMap, Collection { + set(key: K, value: V): this; +} - /** ES6 Map interface. */ - export interface ESMap extends ReadonlyESMap, Collection { - set(key: K, value: V): this; - } +/** + * ES6 Map interface. + */ +export interface Map extends ESMap { +} - /** - * ES6 Map interface. - */ - export interface Map extends ESMap { - } +/* @internal */ +export interface MapConstructor { + // eslint-disable-next-line @typescript-eslint/prefer-function-type + new (iterable?: readonly (readonly [ + K, + V + ])[] | ReadonlyESMap): ESMap; +} - /* @internal */ - export interface MapConstructor { - // eslint-disable-next-line @typescript-eslint/prefer-function-type - new (iterable?: readonly (readonly [ - K, - V - ])[] | ReadonlyESMap): ESMap; - } +/** ES6 Set interface, only read methods included. */ +export interface ReadonlySet extends ReadonlyCollection { + has(value: T): boolean; + values(): Iterator; + entries(): Iterator<[ + T, + T + ]>; + forEach(action: (value: T, key: T) => void): void; +} - /** ES6 Set interface, only read methods included. */ - export interface ReadonlySet extends ReadonlyCollection { - has(value: T): boolean; - values(): Iterator; - entries(): Iterator<[ - T, - T - ]>; - forEach(action: (value: T, key: T) => void): void; - } +/** ES6 Set interface. */ +export interface Set extends ReadonlySet, Collection { + add(value: T): this; + delete(value: T): boolean; +} - /** ES6 Set interface. */ - export interface Set extends ReadonlySet, Collection { - add(value: T): this; - delete(value: T): boolean; - } +/* @internal */ +export interface SetConstructor { + // eslint-disable-next-line @typescript-eslint/prefer-function-type + new (iterable?: readonly T[] | ReadonlySet): Set; +} - /* @internal */ - export interface SetConstructor { - // eslint-disable-next-line @typescript-eslint/prefer-function-type - new (iterable?: readonly T[] | ReadonlySet): Set; - } +/** ES6 Iterator type. */ +export interface Iterator { + next(): { + value: T; + done?: false; + } | { + value: void; + done: true; + }; +} - /** ES6 Iterator type. */ - export interface Iterator { - next(): { - value: T; - done?: false; - } | { - value: void; - done: true; - }; - } +/** Array that is only intended to be pushed to, never read. */ +export interface Push { + push(...values: T[]): void; + /* @internal*/ readonly length: number; +} - /** Array that is only intended to be pushed to, never read. */ - export interface Push { - push(...values: T[]): void; - /* @internal*/ readonly length: number; - } +/* @internal */ +export type EqualityComparer = (a: T, b: T) => boolean; - /* @internal */ - export type EqualityComparer = (a: T, b: T) => boolean; +/* @internal */ +export type Comparer = (a: T, b: T) => Comparison; - /* @internal */ - export type Comparer = (a: T, b: T) => Comparison; +/* @internal */ +export const enum Comparison { + LessThan = -1, + EqualTo = 0, + GreaterThan = 1 +} - /* @internal */ - export const enum Comparison { - LessThan = -1, - EqualTo = 0, - GreaterThan = 1 - } +/* @internal */ +namespace NativeCollections { + declare const self: any; - /* @internal */ - namespace NativeCollections { - declare const self: any; - - const globals = typeof globalThis !== "undefined" ? globalThis : - typeof global !== "undefined" ? global : - typeof self !== "undefined" ? self : - undefined; - - /** - * Returns the native Map implementation if it is available and compatible (i.e. supports iteration). - */ - export function tryGetNativeMap(): MapConstructor | undefined { - // Internet Explorer's Map doesn't support iteration, so don't use it. - const gMap = globals?.Map; - // eslint-disable-next-line no-in-operator - return typeof gMap !== "undefined" && "entries" in gMap.prototype && new gMap([[0, 0]]).size === 1 ? gMap : undefined; - } - - /** - * Returns the native Set implementation if it is available and compatible (i.e. supports iteration). - */ - export function tryGetNativeSet(): SetConstructor | undefined { - // Internet Explorer's Set doesn't support iteration, so don't use it. - const gSet = globals?.Set; - // eslint-disable-next-line no-in-operator - return typeof gSet !== "undefined" && "entries" in gSet.prototype && new gSet([0]).size === 1 ? gSet : undefined; - } - } + const globals = typeof globalThis !== "undefined" ? globalThis : + typeof global !== "undefined" ? global : + typeof self !== "undefined" ? self : + undefined; - /* @internal */ - export const Map = getCollectionImplementation("Map", "tryGetNativeMap", "createMapShim"); - /* @internal */ - export const Set = getCollectionImplementation("Set", "tryGetNativeSet", "createSetShim"); + /** + * Returns the native Map implementation if it is available and compatible (i.e. supports iteration). + */ + export function tryGetNativeMap(): MapConstructor | undefined { + // Internet Explorer's Map doesn't support iteration, so don't use it. + const gMap = globals?.Map; + // eslint-disable-next-line no-in-operator + return typeof gMap !== "undefined" && "entries" in gMap.prototype && new gMap([[0, 0]]).size === 1 ? gMap : undefined; + } - /* @internal */ - type GetIteratorCallback = | ReadonlyESMap | undefined>(iterable: I) => Iterator ? [ - K, - V - ] : I extends ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; - - /* @internal */ - function getCollectionImplementation any>, K2 extends ts.MatchingKeys ReturnType<(typeof NativeCollections)[K1]>>>(name: string, nativeFactory: K1, shimFactory: K2): NonNullable> { - // NOTE: ts.ShimCollections will be defined for typescriptServices.js but not for tsc.js, so we must test for it. - const constructor = NativeCollections[nativeFactory]() ?? ts.ShimCollections?.[shimFactory](ts.getIterator); - if (constructor) - return constructor as NonNullable>; - throw new Error(`TypeScript requires an environment that provides a compatible native ${name} implementation.`); + /** + * Returns the native Set implementation if it is available and compatible (i.e. supports iteration). + */ + export function tryGetNativeSet(): SetConstructor | undefined { + // Internet Explorer's Set doesn't support iteration, so don't use it. + const gSet = globals?.Set; + // eslint-disable-next-line no-in-operator + return typeof gSet !== "undefined" && "entries" in gSet.prototype && new gSet([0]).size === 1 ? gSet : undefined; } } + +/* @internal */ +export const Map = getCollectionImplementation("Map", "tryGetNativeMap", "createMapShim"); +/* @internal */ +export const Set = getCollectionImplementation("Set", "tryGetNativeSet", "createSetShim"); + +/* @internal */ +type GetIteratorCallback = | ReadonlyESMap | undefined>(iterable: I) => Iterator ? [ + K, + V +] : I extends ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; + +/* @internal */ +function getCollectionImplementation any>, K2 extends ts.MatchingKeys ReturnType<(typeof NativeCollections)[K1]>>>(name: string, nativeFactory: K1, shimFactory: K2): NonNullable> { + // NOTE: ts.ShimCollections will be defined for typescriptServices.js but not for tsc.js, so we must test for it. + const constructor = NativeCollections[nativeFactory]() ?? ts.ShimCollections?.[shimFactory](ts.getIterator); + if (constructor) + return constructor as NonNullable>; + throw new Error(`TypeScript requires an environment that provides a compatible native ${name} implementation.`); +} +} diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 598db30ef67a9..4f84e4dfa0a97 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -1,718 +1,718 @@ /* @internal */ namespace ts { - export enum LogLevel { - Off, - Error, - Warning, - Info, - Verbose - } +export enum LogLevel { + Off, + Error, + Warning, + Info, + Verbose +} + +export interface LoggingHost { + log(level: LogLevel, s: string): void; +} + +export interface DeprecationOptions { + message?: string; + error?: boolean; + since?: ts.Version | string; + warnAfter?: ts.Version | string; + errorAfter?: ts.Version | string; + typeScriptVersion?: ts.Version | string; +} + +export namespace Debug { + let typeScriptVersion: ts.Version | undefined; - export interface LoggingHost { - log(level: LogLevel, s: string): void; + /* eslint-disable prefer-const */ + let currentAssertionLevel = ts.AssertionLevel.None; + export let currentLogLevel = LogLevel.Warning; + export let isDebugging = false; + export let loggingHost: LoggingHost | undefined; + /* eslint-enable prefer-const */ + + type AssertionKeys = ts.MatchingKeys; + export function getTypeScriptVersion() { + return typeScriptVersion ?? (typeScriptVersion = new ts.Version(ts.version)); } - export interface DeprecationOptions { - message?: string; - error?: boolean; - since?: ts.Version | string; - warnAfter?: ts.Version | string; - errorAfter?: ts.Version | string; - typeScriptVersion?: ts.Version | string; + export function shouldLog(level: LogLevel): boolean { + return currentLogLevel <= level; } - export namespace Debug { - let typeScriptVersion: ts.Version | undefined; + function logMessage(level: LogLevel, s: string): void { + if (loggingHost && shouldLog(level)) { + loggingHost.log(level, s); + } + } - /* eslint-disable prefer-const */ - let currentAssertionLevel = ts.AssertionLevel.None; - export let currentLogLevel = LogLevel.Warning; - export let isDebugging = false; - export let loggingHost: LoggingHost | undefined; - /* eslint-enable prefer-const */ + export function log(s: string): void { + logMessage(LogLevel.Info, s); + } - type AssertionKeys = ts.MatchingKeys; - export function getTypeScriptVersion() { - return typeScriptVersion ?? (typeScriptVersion = new ts.Version(ts.version)); + export namespace log { + export function error(s: string): void { + logMessage(LogLevel.Error, s); } - export function shouldLog(level: LogLevel): boolean { - return currentLogLevel <= level; - } - - function logMessage(level: LogLevel, s: string): void { - if (loggingHost && shouldLog(level)) { - loggingHost.log(level, s); - } + export function warn(s: string): void { + logMessage(LogLevel.Warning, s); } export function log(s: string): void { logMessage(LogLevel.Info, s); } - export namespace log { - export function error(s: string): void { - logMessage(LogLevel.Error, s); - } - - export function warn(s: string): void { - logMessage(LogLevel.Warning, s); - } - - export function log(s: string): void { - logMessage(LogLevel.Info, s); - } - - export function trace(s: string): void { - logMessage(LogLevel.Verbose, s); - } + export function trace(s: string): void { + logMessage(LogLevel.Verbose, s); } + } - const assertionCache: Partial> = {}; - - export function getAssertionLevel() { - return currentAssertionLevel; - } + const assertionCache: Partial> = {}; - export function setAssertionLevel(level: ts.AssertionLevel) { - const prevAssertionLevel = currentAssertionLevel; - currentAssertionLevel = level; + export function getAssertionLevel() { + return currentAssertionLevel; + } - if (level > prevAssertionLevel) { - // restore assertion functions for the current assertion level (see `shouldAssertFunction`). - for (const key of ts.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 setAssertionLevel(level: ts.AssertionLevel) { + const prevAssertionLevel = currentAssertionLevel; + currentAssertionLevel = level; + + if (level > prevAssertionLevel) { + // restore assertion functions for the current assertion level (see `shouldAssertFunction`). + for (const key of ts.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: ts.AssertionLevel): boolean { - return currentAssertionLevel >= level; - } + export function shouldAssert(level: ts.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: ts.AssertionLevel, name: K): boolean { - if (!shouldAssert(level)) { - assertionCache[name] = { level, assertion: Debug[name] }; - (Debug as any)[name] = ts.noop; - return false; - } - return true; - } + /** + * 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: ts.AssertionLevel, name: K): boolean { + if (!shouldAssert(level)) { + assertionCache[name] = { level, assertion: Debug[name] }; + (Debug as any)[name] = ts.noop; + return false; + } + return true; + } - export function fail(message?: string, stackCrawlMark?: ts.AnyFunction): never { - debugger; - const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure."); - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(e, stackCrawlMark || fail); - } - throw e; + export function fail(message?: string, stackCrawlMark?: ts.AnyFunction): never { + debugger; + const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure."); + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(e, stackCrawlMark || fail); } + throw e; + } - export function failBadSyntaxKind(node: ts.Node, message?: string, stackCrawlMark?: ts.AnyFunction): never { - return fail(`${message || "Unexpected node."}\r\nNode ${formatSyntaxKind(node.kind)} was unexpected.`, stackCrawlMark || failBadSyntaxKind); - } + export function failBadSyntaxKind(node: ts.Node, message?: string, stackCrawlMark?: ts.AnyFunction): never { + return fail(`${message || "Unexpected node."}\r\nNode ${formatSyntaxKind(node.kind)} was unexpected.`, stackCrawlMark || failBadSyntaxKind); + } - export function assert(expression: unknown, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: ts.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 assert(expression: unknown, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: ts.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 assertEqual(a: T, b: T, msg?: string, msg2?: string, stackCrawlMark?: ts.AnyFunction): void { - if (a !== b) { - const message = msg ? msg2 ? `${msg} ${msg2}` : msg : ""; - fail(`Expected ${a} === ${b}. ${message}`, stackCrawlMark || assertEqual); - } + export function assertEqual(a: T, b: T, msg?: string, msg2?: string, stackCrawlMark?: ts.AnyFunction): void { + if (a !== b) { + const message = msg ? msg2 ? `${msg} ${msg2}` : msg : ""; + fail(`Expected ${a} === ${b}. ${message}`, stackCrawlMark || assertEqual); } + } - export function assertLessThan(a: number, b: number, msg?: string, stackCrawlMark?: ts.AnyFunction): void { - if (a >= b) { - fail(`Expected ${a} < ${b}. ${msg || ""}`, stackCrawlMark || assertLessThan); - } + export function assertLessThan(a: number, b: number, msg?: string, stackCrawlMark?: ts.AnyFunction): void { + if (a >= b) { + fail(`Expected ${a} < ${b}. ${msg || ""}`, stackCrawlMark || assertLessThan); } + } - export function assertLessThanOrEqual(a: number, b: number, stackCrawlMark?: ts.AnyFunction): void { - if (a > b) { - fail(`Expected ${a} <= ${b}`, stackCrawlMark || assertLessThanOrEqual); - } + export function assertLessThanOrEqual(a: number, b: number, stackCrawlMark?: ts.AnyFunction): void { + if (a > b) { + fail(`Expected ${a} <= ${b}`, stackCrawlMark || assertLessThanOrEqual); } + } - export function assertGreaterThanOrEqual(a: number, b: number, stackCrawlMark?: ts.AnyFunction): void { - if (a < b) { - fail(`Expected ${a} >= ${b}`, stackCrawlMark || assertGreaterThanOrEqual); - } + export function assertGreaterThanOrEqual(a: number, b: number, stackCrawlMark?: ts.AnyFunction): void { + if (a < b) { + fail(`Expected ${a} >= ${b}`, stackCrawlMark || assertGreaterThanOrEqual); } + } - export function assertIsDefined(value: T, message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is NonNullable { - // eslint-disable-next-line no-null/no-null - if (value === undefined || value === null) { - fail(message, stackCrawlMark || assertIsDefined); - } + export function assertIsDefined(value: T, message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is NonNullable { + // eslint-disable-next-line no-null/no-null + if (value === undefined || value === null) { + fail(message, stackCrawlMark || assertIsDefined); } + } - export function checkDefined(value: T | null | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): T { - assertIsDefined(value, message, stackCrawlMark || checkDefined); - return value; - } + export function checkDefined(value: T | null | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): T { + assertIsDefined(value, message, stackCrawlMark || checkDefined); + return value; + } - export function assertEachIsDefined(value: ts.NodeArray, message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is ts.NodeArray; - export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is readonly NonNullable[]; - export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: ts.AnyFunction) { - for (const v of value) { - assertIsDefined(v, message, stackCrawlMark || assertEachIsDefined); - } + export function assertEachIsDefined(value: ts.NodeArray, message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is ts.NodeArray; + export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: ts.AnyFunction): asserts value is readonly NonNullable[]; + export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: ts.AnyFunction) { + for (const v of value) { + assertIsDefined(v, message, stackCrawlMark || assertEachIsDefined); } + } - export function checkEachDefined(value: A, message?: string, stackCrawlMark?: ts.AnyFunction): A { - assertEachIsDefined(value, message, stackCrawlMark || checkEachDefined); - return value; - } + export function checkEachDefined(value: A, message?: string, stackCrawlMark?: ts.AnyFunction): A { + assertEachIsDefined(value, message, stackCrawlMark || checkEachDefined); + return value; + } - export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: ts.AnyFunction): never { - const detail = typeof member === "object" && ts.hasProperty(member, "kind") && ts.hasProperty(member, "pos") ? "SyntaxKind: " + formatSyntaxKind((member as ts.Node).kind) : JSON.stringify(member); - return fail(`${message} ${detail}`, stackCrawlMark || assertNever); - } + export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: ts.AnyFunction): never { + const detail = typeof member === "object" && ts.hasProperty(member, "kind") && ts.hasProperty(member, "pos") ? "SyntaxKind: " + formatSyntaxKind((member as ts.Node).kind) : JSON.stringify(member); + return fail(`${message} ${detail}`, stackCrawlMark || assertNever); + } - export function assertEachNode(nodes: ts.NodeArray, test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts nodes is ts.NodeArray; - export function assertEachNode(nodes: readonly T[], test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts nodes is readonly U[]; - export function assertEachNode(nodes: readonly ts.Node[], test: (node: ts.Node) => boolean, message?: string, stackCrawlMark?: ts.AnyFunction): void; - export function assertEachNode(nodes: readonly ts.Node[], test: (node: ts.Node) => boolean, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.AssertionLevel.Normal, "assertEachNode")) { - assert(test === undefined || ts.every(nodes, test), message || "Unexpected node.", () => `Node array did not pass test '${getFunctionName(test)}'.`, stackCrawlMark || assertEachNode); - } + export function assertEachNode(nodes: ts.NodeArray, test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts nodes is ts.NodeArray; + export function assertEachNode(nodes: readonly T[], test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts nodes is readonly U[]; + export function assertEachNode(nodes: readonly ts.Node[], test: (node: ts.Node) => boolean, message?: string, stackCrawlMark?: ts.AnyFunction): void; + export function assertEachNode(nodes: readonly ts.Node[], test: (node: ts.Node) => boolean, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.AssertionLevel.Normal, "assertEachNode")) { + assert(test === undefined || ts.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?: ts.AnyFunction): asserts node is U; - export function assertNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; - export function assertNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.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?: ts.AnyFunction): asserts node is U; + export function assertNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; + export function assertNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.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: ts.Node) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is Exclude; - export function assertNotNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; - export function assertNotNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.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: ts.Node) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is Exclude; + export function assertNotNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; + export function assertNotNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.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?: ts.AnyFunction): asserts node is U; - export function assertOptionalNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is U | undefined; - export function assertOptionalNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; - export function assertOptionalNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.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?: ts.AnyFunction): asserts node is U; + export function assertOptionalNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is U | undefined; + export function assertOptionalNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; + export function assertOptionalNode(node: ts.Node | undefined, test: ((node: ts.Node) => boolean) | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.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?: ts.AnyFunction): asserts node is Extract; - export function assertOptionalToken(node: T | undefined, kind: K, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is Extract | undefined; - export function assertOptionalToken(node: ts.Node | undefined, kind: ts.SyntaxKind | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; - export function assertOptionalToken(node: ts.Node | undefined, kind: ts.SyntaxKind | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.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?: ts.AnyFunction): asserts node is Extract; + export function assertOptionalToken(node: T | undefined, kind: K, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is Extract | undefined; + export function assertOptionalToken(node: ts.Node | undefined, kind: ts.SyntaxKind | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): void; + export function assertOptionalToken(node: ts.Node | undefined, kind: ts.SyntaxKind | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.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: ts.Node | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is undefined; - export function assertMissingNode(node: ts.Node | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { - if (shouldAssertFunction(ts.AssertionLevel.Normal, "assertMissingNode")) { - assert(node === undefined, message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} was unexpected'.`, stackCrawlMark || assertMissingNode); - } + } + export function assertMissingNode(node: ts.Node | undefined, message?: string, stackCrawlMark?: ts.AnyFunction): asserts node is undefined; + export function assertMissingNode(node: ts.Node | undefined, message?: string, stackCrawlMark?: ts.AnyFunction) { + if (shouldAssertFunction(ts.AssertionLevel.Normal, "assertMissingNode")) { + assert(node === undefined, message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} was unexpected'.`, stackCrawlMark || assertMissingNode); } + } - /** - * Asserts a value has the specified type in typespace only (does not perform a runtime assertion). - * This is useful in cases where we switch on `node.kind` and can be reasonably sure the type is accurate, and - * as a result can reduce the number of unnecessary casts. - */ - export function type(value: unknown): asserts value is T; - export function type(_value: unknown) { } + /** + * Asserts a value has the specified type in typespace only (does not perform a runtime assertion). + * This is useful in cases where we switch on `node.kind` and can be reasonably sure the type is accurate, and + * as a result can reduce the number of unnecessary casts. + */ + export function type(value: unknown): asserts value is T; + export function type(_value: unknown) { } - export function getFunctionName(func: ts.AnyFunction) { - if (typeof func !== "function") { - return ""; - } - else if (func.hasOwnProperty("name")) { - return (func as any).name; - } - else { - const text = Function.prototype.toString.call(func); - const match = /^function\s+([\w\$]+)\s*\(/.exec(text); - return match ? match[1] : ""; - } + export function getFunctionName(func: ts.AnyFunction) { + if (typeof func !== "function") { + return ""; } - - export function formatSymbol(symbol: ts.Symbol): string { - return `{ name: ${ts.unescapeLeadingUnderscores(symbol.escapedName)}; flags: ${formatSymbolFlags(symbol.flags)}; declarations: ${ts.map(symbol.declarations, node => formatSyntaxKind(node.kind))} }`; + else if (func.hasOwnProperty("name")) { + return (func as any).name; + } + 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: ts.Symbol): string { + return `{ name: ${ts.unescapeLeadingUnderscores(symbol.escapedName)}; flags: ${formatSymbolFlags(symbol.flags)}; declarations: ${ts.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 ts.stableSort<[ - number, - string - ]>(result, (x, y) => ts.compareValues(x[0], y[0])); } + return value.toString(); + } - export function formatSyntaxKind(kind: ts.SyntaxKind | undefined): string { - return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false); + 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]); + } } - export function formatSnippetKind(kind: ts.SnippetKind | undefined): string { - return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false); - } + return ts.stableSort<[ + number, + string + ]>(result, (x, y) => ts.compareValues(x[0], y[0])); + } - export function formatNodeFlags(flags: ts.NodeFlags | undefined): string { - return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true); - } + export function formatSyntaxKind(kind: ts.SyntaxKind | undefined): string { + return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false); + } - export function formatModifierFlags(flags: ts.ModifierFlags | undefined): string { - return formatEnum(flags, (ts as any).ModifierFlags, /*isFlags*/ true); - } + export function formatSnippetKind(kind: ts.SnippetKind | undefined): string { + return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false); + } - export function formatTransformFlags(flags: ts.TransformFlags | undefined): string { - return formatEnum(flags, (ts as any).TransformFlags, /*isFlags*/ true); - } + export function formatNodeFlags(flags: ts.NodeFlags | undefined): string { + return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true); + } - export function formatEmitFlags(flags: ts.EmitFlags | undefined): string { - return formatEnum(flags, (ts as any).EmitFlags, /*isFlags*/ true); - } + export function formatModifierFlags(flags: ts.ModifierFlags | undefined): string { + return formatEnum(flags, (ts as any).ModifierFlags, /*isFlags*/ true); + } - export function formatSymbolFlags(flags: ts.SymbolFlags | undefined): string { - return formatEnum(flags, (ts as any).SymbolFlags, /*isFlags*/ true); - } + export function formatTransformFlags(flags: ts.TransformFlags | undefined): string { + return formatEnum(flags, (ts as any).TransformFlags, /*isFlags*/ true); + } - export function formatTypeFlags(flags: ts.TypeFlags | undefined): string { - return formatEnum(flags, (ts as any).TypeFlags, /*isFlags*/ true); - } + export function formatEmitFlags(flags: ts.EmitFlags | undefined): string { + return formatEnum(flags, (ts as any).EmitFlags, /*isFlags*/ true); + } - export function formatSignatureFlags(flags: ts.SignatureFlags | undefined): string { - return formatEnum(flags, (ts as any).SignatureFlags, /*isFlags*/ true); - } + export function formatSymbolFlags(flags: ts.SymbolFlags | undefined): string { + return formatEnum(flags, (ts as any).SymbolFlags, /*isFlags*/ true); + } - export function formatObjectFlags(flags: ts.ObjectFlags | undefined): string { - return formatEnum(flags, (ts as any).ObjectFlags, /*isFlags*/ true); - } + export function formatTypeFlags(flags: ts.TypeFlags | undefined): string { + return formatEnum(flags, (ts as any).TypeFlags, /*isFlags*/ true); + } - export function formatFlowFlags(flags: ts.FlowFlags | undefined): string { - return formatEnum(flags, (ts as any).FlowFlags, /*isFlags*/ true); - } + export function formatSignatureFlags(flags: ts.SignatureFlags | undefined): string { + return formatEnum(flags, (ts as any).SignatureFlags, /*isFlags*/ true); + } - let isDebugInfoEnabled = false; + export function formatObjectFlags(flags: ts.ObjectFlags | undefined): string { + return formatEnum(flags, (ts as any).ObjectFlags, /*isFlags*/ true); + } - interface ExtendedDebugModule { - init(_ts: typeof ts): void; - formatControlFlowGraph(flowNode: ts.FlowNode): string; - } + export function formatFlowFlags(flags: ts.FlowFlags | undefined): string { + return formatEnum(flags, (ts as any).FlowFlags, /*isFlags*/ true); + } - let extendedDebugModule: ExtendedDebugModule | undefined; + let isDebugInfoEnabled = false; - function extendedDebug() { - enableDebugInfo(); - if (!extendedDebugModule) { - throw new Error("Debugging helpers could not be loaded."); - } - return extendedDebugModule; - } + interface ExtendedDebugModule { + init(_ts: typeof ts): void; + formatControlFlowGraph(flowNode: ts.FlowNode): string; + } - export function printControlFlowGraph(flowNode: ts.FlowNode) { - return console.log(formatControlFlowGraph(flowNode)); - } + let extendedDebugModule: ExtendedDebugModule | undefined; - export function formatControlFlowGraph(flowNode: ts.FlowNode) { - return extendedDebug().formatControlFlowGraph(flowNode); + function extendedDebug() { + enableDebugInfo(); + if (!extendedDebugModule) { + throw new Error("Debugging helpers could not be loaded."); } + return extendedDebugModule; + } - let flowNodeProto: ts.FlowNodeBase | undefined; - function attachFlowNodeDebugInfoWorker(flowNode: ts.FlowNodeBase) { - if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line no-in-operator - Object.defineProperties(flowNode, { - // for use with vscode-js-debug's new customDescriptionGenerator in launch.json - __tsDebuggerDisplay: { - value(this: ts.FlowNodeBase) { - const flowHeader = this.flags & ts.FlowFlags.Start ? "FlowStart" : - this.flags & ts.FlowFlags.BranchLabel ? "FlowBranchLabel" : - this.flags & ts.FlowFlags.LoopLabel ? "FlowLoopLabel" : - this.flags & ts.FlowFlags.Assignment ? "FlowAssignment" : - this.flags & ts.FlowFlags.TrueCondition ? "FlowTrueCondition" : - this.flags & ts.FlowFlags.FalseCondition ? "FlowFalseCondition" : - this.flags & ts.FlowFlags.SwitchClause ? "FlowSwitchClause" : - this.flags & ts.FlowFlags.ArrayMutation ? "FlowArrayMutation" : - this.flags & ts.FlowFlags.Call ? "FlowCall" : - this.flags & ts.FlowFlags.ReduceLabel ? "FlowReduceLabel" : - this.flags & ts.FlowFlags.Unreachable ? "FlowUnreachable" : - "UnknownFlow"; - const remainingFlags = this.flags & ~(ts.FlowFlags.Referenced - 1); - return `${flowHeader}${remainingFlags ? ` (${formatFlowFlags(remainingFlags)})`: ""}`; - } - }, - __debugFlowFlags: { get(this: ts.FlowNodeBase) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } }, - __debugToString: { value(this: ts.FlowNodeBase) { return formatControlFlowGraph(this); } } - }); - } - } + export function printControlFlowGraph(flowNode: ts.FlowNode) { + return console.log(formatControlFlowGraph(flowNode)); + } + + export function formatControlFlowGraph(flowNode: ts.FlowNode) { + return extendedDebug().formatControlFlowGraph(flowNode); + } - export function attachFlowNodeDebugInfo(flowNode: ts.FlowNodeBase) { - if (isDebugInfoEnabled) { - if (typeof Object.setPrototypeOf === "function") { - // if we're in es2015, attach the method to a shared prototype for `FlowNode` - // so the method doesn't show up in the watch window. - if (!flowNodeProto) { - flowNodeProto = Object.create(Object.prototype) as ts.FlowNodeBase; - attachFlowNodeDebugInfoWorker(flowNodeProto); + let flowNodeProto: ts.FlowNodeBase | undefined; + function attachFlowNodeDebugInfoWorker(flowNode: ts.FlowNodeBase) { + if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line no-in-operator + Object.defineProperties(flowNode, { + // for use with vscode-js-debug's new customDescriptionGenerator in launch.json + __tsDebuggerDisplay: { + value(this: ts.FlowNodeBase) { + const flowHeader = this.flags & ts.FlowFlags.Start ? "FlowStart" : + this.flags & ts.FlowFlags.BranchLabel ? "FlowBranchLabel" : + this.flags & ts.FlowFlags.LoopLabel ? "FlowLoopLabel" : + this.flags & ts.FlowFlags.Assignment ? "FlowAssignment" : + this.flags & ts.FlowFlags.TrueCondition ? "FlowTrueCondition" : + this.flags & ts.FlowFlags.FalseCondition ? "FlowFalseCondition" : + this.flags & ts.FlowFlags.SwitchClause ? "FlowSwitchClause" : + this.flags & ts.FlowFlags.ArrayMutation ? "FlowArrayMutation" : + this.flags & ts.FlowFlags.Call ? "FlowCall" : + this.flags & ts.FlowFlags.ReduceLabel ? "FlowReduceLabel" : + this.flags & ts.FlowFlags.Unreachable ? "FlowUnreachable" : + "UnknownFlow"; + const remainingFlags = this.flags & ~(ts.FlowFlags.Referenced - 1); + return `${flowHeader}${remainingFlags ? ` (${formatFlowFlags(remainingFlags)})`: ""}`; } - Object.setPrototypeOf(flowNode, flowNodeProto); - } - else { - // not running in an es2015 environment, attach the method directly. - attachFlowNodeDebugInfoWorker(flowNode); - } - } + }, + __debugFlowFlags: { get(this: ts.FlowNodeBase) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } }, + __debugToString: { value(this: ts.FlowNodeBase) { return formatControlFlowGraph(this); } } + }); } + } - let nodeArrayProto: ts.NodeArray | undefined; - function attachNodeArrayDebugInfoWorker(array: ts.NodeArray) { - if (!("__tsDebuggerDisplay" in array)) { // eslint-disable-line no-in-operator - Object.defineProperties(array, { - __tsDebuggerDisplay: { - value(this: ts.NodeArray, defaultValue: string) { - // An `Array` with extra properties is rendered as `[A, B, prop1: 1, prop2: 2]`. Most of - // these aren't immediately useful so we trim off the `prop1: ..., prop2: ...` part from the - // formatted string. - // This regex can trigger slow backtracking because of overlapping potential captures. - // We don't care, this is debug code that's only enabled with a debugger attached - - // we're just taking note of it for anyone checking regex performance in the future. - defaultValue = String(defaultValue).replace(/(?:,[\s\w\d_]+:[^,]+)+\]$/, "]"); - return `NodeArray ${defaultValue}`; - } - } - }); + export function attachFlowNodeDebugInfo(flowNode: ts.FlowNodeBase) { + if (isDebugInfoEnabled) { + if (typeof Object.setPrototypeOf === "function") { + // if we're in es2015, attach the method to a shared prototype for `FlowNode` + // so the method doesn't show up in the watch window. + if (!flowNodeProto) { + flowNodeProto = Object.create(Object.prototype) as ts.FlowNodeBase; + attachFlowNodeDebugInfoWorker(flowNodeProto); + } + Object.setPrototypeOf(flowNode, flowNodeProto); + } + else { + // not running in an es2015 environment, attach the method directly. + attachFlowNodeDebugInfoWorker(flowNode); } } + } - export function attachNodeArrayDebugInfo(array: ts.NodeArray) { - if (isDebugInfoEnabled) { - if (typeof Object.setPrototypeOf === "function") { - // if we're in es2015, attach the method to a shared prototype for `NodeArray` - // so the method doesn't show up in the watch window. - if (!nodeArrayProto) { - nodeArrayProto = Object.create(Array.prototype) as ts.NodeArray; - attachNodeArrayDebugInfoWorker(nodeArrayProto); + let nodeArrayProto: ts.NodeArray | undefined; + function attachNodeArrayDebugInfoWorker(array: ts.NodeArray) { + if (!("__tsDebuggerDisplay" in array)) { // eslint-disable-line no-in-operator + Object.defineProperties(array, { + __tsDebuggerDisplay: { + value(this: ts.NodeArray, defaultValue: string) { + // An `Array` with extra properties is rendered as `[A, B, prop1: 1, prop2: 2]`. Most of + // these aren't immediately useful so we trim off the `prop1: ..., prop2: ...` part from the + // formatted string. + // This regex can trigger slow backtracking because of overlapping potential captures. + // We don't care, this is debug code that's only enabled with a debugger attached - + // we're just taking note of it for anyone checking regex performance in the future. + defaultValue = String(defaultValue).replace(/(?:,[\s\w\d_]+:[^,]+)+\]$/, "]"); + return `NodeArray ${defaultValue}`; } - Object.setPrototypeOf(array, nodeArrayProto); } - else { - // not running in an es2015 environment, attach the method directly. - attachNodeArrayDebugInfoWorker(array); + }); + } + } + + export function attachNodeArrayDebugInfo(array: ts.NodeArray) { + if (isDebugInfoEnabled) { + if (typeof Object.setPrototypeOf === "function") { + // if we're in es2015, attach the method to a shared prototype for `NodeArray` + // so the method doesn't show up in the watch window. + if (!nodeArrayProto) { + nodeArrayProto = Object.create(Array.prototype) as ts.NodeArray; + attachNodeArrayDebugInfoWorker(nodeArrayProto); } + Object.setPrototypeOf(array, nodeArrayProto); + } + else { + // not running in an es2015 environment, attach the method directly. + attachNodeArrayDebugInfoWorker(array); } } + } - /** - * Injects debug information into frequently used types. - */ - export function enableDebugInfo() { - if (isDebugInfoEnabled) - return; + /** + * Injects debug information into frequently used types. + */ + export function enableDebugInfo() { + if (isDebugInfoEnabled) + return; - // avoid recomputing - let weakTypeTextMap: WeakMap | undefined; - let weakNodeTextMap: WeakMap | undefined; + // avoid recomputing + let weakTypeTextMap: WeakMap | undefined; + let weakNodeTextMap: WeakMap | undefined; - function getWeakTypeTextMap() { - if (weakTypeTextMap === undefined) { - if (typeof WeakMap === "function") - weakTypeTextMap = new WeakMap(); - } - return weakTypeTextMap; + function getWeakTypeTextMap() { + if (weakTypeTextMap === undefined) { + if (typeof WeakMap === "function") + weakTypeTextMap = new WeakMap(); } + return weakTypeTextMap; + } - function getWeakNodeTextMap() { - if (weakNodeTextMap === undefined) { - if (typeof WeakMap === "function") - weakNodeTextMap = new WeakMap(); - } - return weakNodeTextMap; + function getWeakNodeTextMap() { + if (weakNodeTextMap === undefined) { + if (typeof WeakMap === "function") + weakNodeTextMap = new WeakMap(); } + return weakNodeTextMap; + } - // Add additional properties in debug mode to assist with debugging. - Object.defineProperties(ts.objectAllocator.getSymbolConstructor().prototype, { - // for use with vscode-js-debug's new customDescriptionGenerator in launch.json - __tsDebuggerDisplay: { - value(this: ts.Symbol) { - const symbolHeader = this.flags & ts.SymbolFlags.Transient ? "TransientSymbol" : - "Symbol"; - const remainingSymbolFlags = this.flags & ~ts.SymbolFlags.Transient; - return `${symbolHeader} '${ts.symbolName(this)}'${remainingSymbolFlags ? ` (${formatSymbolFlags(remainingSymbolFlags)})` : ""}`; - } - }, - __debugFlags: { get(this: ts.Symbol) { return formatSymbolFlags(this.flags); } } - }); - - Object.defineProperties(ts.objectAllocator.getTypeConstructor().prototype, { - // for use with vscode-js-debug's new customDescriptionGenerator in launch.json - __tsDebuggerDisplay: { - value(this: ts.Type) { - const typeHeader = this.flags & ts.TypeFlags.Nullable ? "NullableType" : - this.flags & ts.TypeFlags.StringOrNumberLiteral ? `LiteralType ${JSON.stringify((this as ts.LiteralType).value)}` : - this.flags & ts.TypeFlags.BigIntLiteral ? `LiteralType ${(this as ts.BigIntLiteralType).value.negative ? "-" : ""}${(this as ts.BigIntLiteralType).value.base10Value}n` : - this.flags & ts.TypeFlags.UniqueESSymbol ? "UniqueESSymbolType" : - this.flags & ts.TypeFlags.Enum ? "EnumType" : - this.flags & ts.TypeFlags.Intrinsic ? `IntrinsicType ${(this as ts.IntrinsicType).intrinsicName}` : - this.flags & ts.TypeFlags.Union ? "UnionType" : - this.flags & ts.TypeFlags.Intersection ? "IntersectionType" : - this.flags & ts.TypeFlags.Index ? "IndexType" : - this.flags & ts.TypeFlags.IndexedAccess ? "IndexedAccessType" : - this.flags & ts.TypeFlags.Conditional ? "ConditionalType" : - this.flags & ts.TypeFlags.Substitution ? "SubstitutionType" : - this.flags & ts.TypeFlags.TypeParameter ? "TypeParameter" : - this.flags & ts.TypeFlags.Object ? - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.ClassOrInterface ? "InterfaceType" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference ? "TypeReference" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Tuple ? "TupleType" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous ? "AnonymousType" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Mapped ? "MappedType" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.ReverseMapped ? "ReverseMappedType" : - (this as ts.ObjectType).objectFlags & ts.ObjectFlags.EvolvingArray ? "EvolvingArrayType" : - "ObjectType" : - "Type"; - const remainingObjectFlags = this.flags & ts.TypeFlags.Object ? (this as ts.ObjectType).objectFlags & ~ts.ObjectFlags.ObjectTypeKindMask : 0; - return `${typeHeader}${this.symbol ? ` '${ts.symbolName(this.symbol)}'` : ""}${remainingObjectFlags ? ` (${formatObjectFlags(remainingObjectFlags)})` : ""}`; + // Add additional properties in debug mode to assist with debugging. + Object.defineProperties(ts.objectAllocator.getSymbolConstructor().prototype, { + // for use with vscode-js-debug's new customDescriptionGenerator in launch.json + __tsDebuggerDisplay: { + value(this: ts.Symbol) { + const symbolHeader = this.flags & ts.SymbolFlags.Transient ? "TransientSymbol" : + "Symbol"; + const remainingSymbolFlags = this.flags & ~ts.SymbolFlags.Transient; + return `${symbolHeader} '${ts.symbolName(this)}'${remainingSymbolFlags ? ` (${formatSymbolFlags(remainingSymbolFlags)})` : ""}`; + } + }, + __debugFlags: { get(this: ts.Symbol) { return formatSymbolFlags(this.flags); } } + }); + + Object.defineProperties(ts.objectAllocator.getTypeConstructor().prototype, { + // for use with vscode-js-debug's new customDescriptionGenerator in launch.json + __tsDebuggerDisplay: { + value(this: ts.Type) { + const typeHeader = this.flags & ts.TypeFlags.Nullable ? "NullableType" : + this.flags & ts.TypeFlags.StringOrNumberLiteral ? `LiteralType ${JSON.stringify((this as ts.LiteralType).value)}` : + this.flags & ts.TypeFlags.BigIntLiteral ? `LiteralType ${(this as ts.BigIntLiteralType).value.negative ? "-" : ""}${(this as ts.BigIntLiteralType).value.base10Value}n` : + this.flags & ts.TypeFlags.UniqueESSymbol ? "UniqueESSymbolType" : + this.flags & ts.TypeFlags.Enum ? "EnumType" : + this.flags & ts.TypeFlags.Intrinsic ? `IntrinsicType ${(this as ts.IntrinsicType).intrinsicName}` : + this.flags & ts.TypeFlags.Union ? "UnionType" : + this.flags & ts.TypeFlags.Intersection ? "IntersectionType" : + this.flags & ts.TypeFlags.Index ? "IndexType" : + this.flags & ts.TypeFlags.IndexedAccess ? "IndexedAccessType" : + this.flags & ts.TypeFlags.Conditional ? "ConditionalType" : + this.flags & ts.TypeFlags.Substitution ? "SubstitutionType" : + this.flags & ts.TypeFlags.TypeParameter ? "TypeParameter" : + this.flags & ts.TypeFlags.Object ? + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.ClassOrInterface ? "InterfaceType" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference ? "TypeReference" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Tuple ? "TupleType" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous ? "AnonymousType" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.Mapped ? "MappedType" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.ReverseMapped ? "ReverseMappedType" : + (this as ts.ObjectType).objectFlags & ts.ObjectFlags.EvolvingArray ? "EvolvingArrayType" : + "ObjectType" : + "Type"; + const remainingObjectFlags = this.flags & ts.TypeFlags.Object ? (this as ts.ObjectType).objectFlags & ~ts.ObjectFlags.ObjectTypeKindMask : 0; + return `${typeHeader}${this.symbol ? ` '${ts.symbolName(this.symbol)}'` : ""}${remainingObjectFlags ? ` (${formatObjectFlags(remainingObjectFlags)})` : ""}`; + } + }, + __debugFlags: { get(this: ts.Type) { return formatTypeFlags(this.flags); } }, + __debugObjectFlags: { get(this: ts.Type) { return this.flags & ts.TypeFlags.Object ? formatObjectFlags((this as ts.ObjectType).objectFlags) : ""; } }, + __debugTypeToString: { + value(this: ts.Type) { + // avoid recomputing + const map = getWeakTypeTextMap(); + let text = map?.get(this); + if (text === undefined) { + text = this.checker.typeToString(this); + map?.set(this, text); } - }, - __debugFlags: { get(this: ts.Type) { return formatTypeFlags(this.flags); } }, - __debugObjectFlags: { get(this: ts.Type) { return this.flags & ts.TypeFlags.Object ? formatObjectFlags((this as ts.ObjectType).objectFlags) : ""; } }, - __debugTypeToString: { - value(this: ts.Type) { - // avoid recomputing - const map = getWeakTypeTextMap(); - let text = map?.get(this); - if (text === undefined) { - text = this.checker.typeToString(this); - map?.set(this, text); + return text; + } + }, + }); + + Object.defineProperties(ts.objectAllocator.getSignatureConstructor().prototype, { + __debugFlags: { get(this: ts.Signature) { return formatSignatureFlags(this.flags); } }, + __debugSignatureToString: { value(this: ts.Signature) { return this.checker?.signatureToString(this); } } + }); + + const nodeConstructors = [ + ts.objectAllocator.getNodeConstructor(), + ts.objectAllocator.getIdentifierConstructor(), + ts.objectAllocator.getTokenConstructor(), + ts.objectAllocator.getSourceFileConstructor() + ]; + + for (const ctor of nodeConstructors) { + if (!ctor.prototype.hasOwnProperty("__debugKind")) { + Object.defineProperties(ctor.prototype, { + // for use with vscode-js-debug's new customDescriptionGenerator in launch.json + __tsDebuggerDisplay: { + value(this: ts.Node) { + const nodeHeader = ts.isGeneratedIdentifier(this) ? "GeneratedIdentifier" : + ts.isIdentifier(this) ? `Identifier '${ts.idText(this)}'` : + ts.isPrivateIdentifier(this) ? `PrivateIdentifier '${ts.idText(this)}'` : + ts.isStringLiteral(this) ? `StringLiteral ${JSON.stringify(this.text.length < 10 ? this.text : this.text.slice(10) + "...")}` : + ts.isNumericLiteral(this) ? `NumericLiteral ${this.text}` : + ts.isBigIntLiteral(this) ? `BigIntLiteral ${this.text}n` : + ts.isTypeParameterDeclaration(this) ? "TypeParameterDeclaration" : + ts.isParameter(this) ? "ParameterDeclaration" : + ts.isConstructorDeclaration(this) ? "ConstructorDeclaration" : + ts.isGetAccessorDeclaration(this) ? "GetAccessorDeclaration" : + ts.isSetAccessorDeclaration(this) ? "SetAccessorDeclaration" : + ts.isCallSignatureDeclaration(this) ? "CallSignatureDeclaration" : + ts.isConstructSignatureDeclaration(this) ? "ConstructSignatureDeclaration" : + ts.isIndexSignatureDeclaration(this) ? "IndexSignatureDeclaration" : + ts.isTypePredicateNode(this) ? "TypePredicateNode" : + ts.isTypeReferenceNode(this) ? "TypeReferenceNode" : + ts.isFunctionTypeNode(this) ? "FunctionTypeNode" : + ts.isConstructorTypeNode(this) ? "ConstructorTypeNode" : + ts.isTypeQueryNode(this) ? "TypeQueryNode" : + ts.isTypeLiteralNode(this) ? "TypeLiteralNode" : + ts.isArrayTypeNode(this) ? "ArrayTypeNode" : + ts.isTupleTypeNode(this) ? "TupleTypeNode" : + ts.isOptionalTypeNode(this) ? "OptionalTypeNode" : + ts.isRestTypeNode(this) ? "RestTypeNode" : + ts.isUnionTypeNode(this) ? "UnionTypeNode" : + ts.isIntersectionTypeNode(this) ? "IntersectionTypeNode" : + ts.isConditionalTypeNode(this) ? "ConditionalTypeNode" : + ts.isInferTypeNode(this) ? "InferTypeNode" : + ts.isParenthesizedTypeNode(this) ? "ParenthesizedTypeNode" : + ts.isThisTypeNode(this) ? "ThisTypeNode" : + ts.isTypeOperatorNode(this) ? "TypeOperatorNode" : + ts.isIndexedAccessTypeNode(this) ? "IndexedAccessTypeNode" : + ts.isMappedTypeNode(this) ? "MappedTypeNode" : + ts.isLiteralTypeNode(this) ? "LiteralTypeNode" : + ts.isNamedTupleMember(this) ? "NamedTupleMember" : + ts.isImportTypeNode(this) ? "ImportTypeNode" : + formatSyntaxKind(this.kind); + return `${nodeHeader}${this.flags ? ` (${formatNodeFlags(this.flags)})` : ""}`; } - return text; - } - }, - }); - - Object.defineProperties(ts.objectAllocator.getSignatureConstructor().prototype, { - __debugFlags: { get(this: ts.Signature) { return formatSignatureFlags(this.flags); } }, - __debugSignatureToString: { value(this: ts.Signature) { return this.checker?.signatureToString(this); } } - }); - - const nodeConstructors = [ - ts.objectAllocator.getNodeConstructor(), - ts.objectAllocator.getIdentifierConstructor(), - ts.objectAllocator.getTokenConstructor(), - ts.objectAllocator.getSourceFileConstructor() - ]; - - for (const ctor of nodeConstructors) { - if (!ctor.prototype.hasOwnProperty("__debugKind")) { - Object.defineProperties(ctor.prototype, { - // for use with vscode-js-debug's new customDescriptionGenerator in launch.json - __tsDebuggerDisplay: { - value(this: ts.Node) { - const nodeHeader = ts.isGeneratedIdentifier(this) ? "GeneratedIdentifier" : - ts.isIdentifier(this) ? `Identifier '${ts.idText(this)}'` : - ts.isPrivateIdentifier(this) ? `PrivateIdentifier '${ts.idText(this)}'` : - ts.isStringLiteral(this) ? `StringLiteral ${JSON.stringify(this.text.length < 10 ? this.text : this.text.slice(10) + "...")}` : - ts.isNumericLiteral(this) ? `NumericLiteral ${this.text}` : - ts.isBigIntLiteral(this) ? `BigIntLiteral ${this.text}n` : - ts.isTypeParameterDeclaration(this) ? "TypeParameterDeclaration" : - ts.isParameter(this) ? "ParameterDeclaration" : - ts.isConstructorDeclaration(this) ? "ConstructorDeclaration" : - ts.isGetAccessorDeclaration(this) ? "GetAccessorDeclaration" : - ts.isSetAccessorDeclaration(this) ? "SetAccessorDeclaration" : - ts.isCallSignatureDeclaration(this) ? "CallSignatureDeclaration" : - ts.isConstructSignatureDeclaration(this) ? "ConstructSignatureDeclaration" : - ts.isIndexSignatureDeclaration(this) ? "IndexSignatureDeclaration" : - ts.isTypePredicateNode(this) ? "TypePredicateNode" : - ts.isTypeReferenceNode(this) ? "TypeReferenceNode" : - ts.isFunctionTypeNode(this) ? "FunctionTypeNode" : - ts.isConstructorTypeNode(this) ? "ConstructorTypeNode" : - ts.isTypeQueryNode(this) ? "TypeQueryNode" : - ts.isTypeLiteralNode(this) ? "TypeLiteralNode" : - ts.isArrayTypeNode(this) ? "ArrayTypeNode" : - ts.isTupleTypeNode(this) ? "TupleTypeNode" : - ts.isOptionalTypeNode(this) ? "OptionalTypeNode" : - ts.isRestTypeNode(this) ? "RestTypeNode" : - ts.isUnionTypeNode(this) ? "UnionTypeNode" : - ts.isIntersectionTypeNode(this) ? "IntersectionTypeNode" : - ts.isConditionalTypeNode(this) ? "ConditionalTypeNode" : - ts.isInferTypeNode(this) ? "InferTypeNode" : - ts.isParenthesizedTypeNode(this) ? "ParenthesizedTypeNode" : - ts.isThisTypeNode(this) ? "ThisTypeNode" : - ts.isTypeOperatorNode(this) ? "TypeOperatorNode" : - ts.isIndexedAccessTypeNode(this) ? "IndexedAccessTypeNode" : - ts.isMappedTypeNode(this) ? "MappedTypeNode" : - ts.isLiteralTypeNode(this) ? "LiteralTypeNode" : - ts.isNamedTupleMember(this) ? "NamedTupleMember" : - ts.isImportTypeNode(this) ? "ImportTypeNode" : - formatSyntaxKind(this.kind); - return `${nodeHeader}${this.flags ? ` (${formatNodeFlags(this.flags)})` : ""}`; - } - }, - __debugKind: { get(this: ts.Node) { return formatSyntaxKind(this.kind); } }, - __debugNodeFlags: { get(this: ts.Node) { return formatNodeFlags(this.flags); } }, - __debugModifierFlags: { get(this: ts.Node) { return formatModifierFlags(ts.getEffectiveModifierFlagsNoCache(this)); } }, - __debugTransformFlags: { get(this: ts.Node) { return formatTransformFlags(this.transformFlags); } }, - __debugIsParseTreeNode: { get(this: ts.Node) { return ts.isParseTreeNode(this); } }, - __debugEmitFlags: { get(this: ts.Node) { return formatEmitFlags(ts.getEmitFlags(this)); } }, - __debugGetText: { - value(this: ts.Node, includeTrivia?: boolean) { - if (ts.nodeIsSynthesized(this)) - return ""; - // avoid recomputing - const map = getWeakNodeTextMap(); - let text = map?.get(this); - if (text === undefined) { - const parseNode = ts.getParseTreeNode(this); - const sourceFile = parseNode && ts.getSourceFileOfNode(parseNode); - text = sourceFile ? ts.getSourceTextOfNodeFromSourceFile(sourceFile, parseNode, includeTrivia) : ""; - map?.set(this, text); - } - return text; + }, + __debugKind: { get(this: ts.Node) { return formatSyntaxKind(this.kind); } }, + __debugNodeFlags: { get(this: ts.Node) { return formatNodeFlags(this.flags); } }, + __debugModifierFlags: { get(this: ts.Node) { return formatModifierFlags(ts.getEffectiveModifierFlagsNoCache(this)); } }, + __debugTransformFlags: { get(this: ts.Node) { return formatTransformFlags(this.transformFlags); } }, + __debugIsParseTreeNode: { get(this: ts.Node) { return ts.isParseTreeNode(this); } }, + __debugEmitFlags: { get(this: ts.Node) { return formatEmitFlags(ts.getEmitFlags(this)); } }, + __debugGetText: { + value(this: ts.Node, includeTrivia?: boolean) { + if (ts.nodeIsSynthesized(this)) + return ""; + // avoid recomputing + const map = getWeakNodeTextMap(); + let text = map?.get(this); + if (text === undefined) { + const parseNode = ts.getParseTreeNode(this); + const sourceFile = parseNode && ts.getSourceFileOfNode(parseNode); + text = sourceFile ? ts.getSourceTextOfNodeFromSourceFile(sourceFile, parseNode, includeTrivia) : ""; + map?.set(this, text); } + return text; } - }); - } + } + }); } + } - // attempt to load extended debugging information - try { - if (ts.sys && ts.sys.require) { - const basePath = ts.getDirectoryPath(ts.resolvePath(ts.sys.getExecutingFilePath())); - const result = ts.sys.require(basePath, "./compiler-debug") as ts.RequireResult; - if (!result.error) { - result.module.init(ts); - extendedDebugModule = result.module; - } + // attempt to load extended debugging information + try { + if (ts.sys && ts.sys.require) { + const basePath = ts.getDirectoryPath(ts.resolvePath(ts.sys.getExecutingFilePath())); + const result = ts.sys.require(basePath, "./compiler-debug") as ts.RequireResult; + if (!result.error) { + result.module.init(ts); + extendedDebugModule = result.module; } } - catch { - // do nothing - } - - isDebugInfoEnabled = true; } - - function formatDeprecationMessage(name: string, error: boolean | undefined, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { - let deprecationMessage = error ? "DeprecationError: " : "DeprecationWarning: "; - deprecationMessage += `'${name}' `; - deprecationMessage += since ? `has been deprecated since v${since}` : "is deprecated"; - deprecationMessage += error ? " and can no longer be used." : errorAfter ? ` and will no longer be usable after v${errorAfter}.` : "."; - deprecationMessage += message ? ` ${ts.formatStringFromArgs(message, [name], 0)}` : ""; - return deprecationMessage; + catch { + // do nothing } - function createErrorDeprecation(name: string, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { - const deprecationMessage = formatDeprecationMessage(name, /*error*/ true, errorAfter, since, message); - return () => { - throw new TypeError(deprecationMessage); - }; - } + isDebugInfoEnabled = true; + } - function createWarningDeprecation(name: string, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { - let hasWrittenDeprecation = false; - return () => { - if (!hasWrittenDeprecation) { - log.warn(formatDeprecationMessage(name, /*error*/ false, errorAfter, since, message)); - hasWrittenDeprecation = true; - } - }; - } + function formatDeprecationMessage(name: string, error: boolean | undefined, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { + let deprecationMessage = error ? "DeprecationError: " : "DeprecationWarning: "; + deprecationMessage += `'${name}' `; + deprecationMessage += since ? `has been deprecated since v${since}` : "is deprecated"; + deprecationMessage += error ? " and can no longer be used." : errorAfter ? ` and will no longer be usable after v${errorAfter}.` : "."; + deprecationMessage += message ? ` ${ts.formatStringFromArgs(message, [name], 0)}` : ""; + return deprecationMessage; + } - function createDeprecation(name: string, options: DeprecationOptions & { - error: true; - }): () => never; - function createDeprecation(name: string, options?: DeprecationOptions): () => void; - function createDeprecation(name: string, options: DeprecationOptions = {}) { - const version = typeof options.typeScriptVersion === "string" ? new ts.Version(options.typeScriptVersion) : options.typeScriptVersion ?? getTypeScriptVersion(); - const errorAfter = typeof options.errorAfter === "string" ? new ts.Version(options.errorAfter) : options.errorAfter; - const warnAfter = typeof options.warnAfter === "string" ? new ts.Version(options.warnAfter) : options.warnAfter; - const since = typeof options.since === "string" ? new ts.Version(options.since) : options.since ?? warnAfter; - const error = options.error || errorAfter && version.compareTo(errorAfter) <= 0; - const warn = !warnAfter || version.compareTo(warnAfter) >= 0; - return error ? createErrorDeprecation(name, errorAfter, since, options.message) : - warn ? createWarningDeprecation(name, errorAfter, since, options.message) : ts.noop; - } + function createErrorDeprecation(name: string, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { + const deprecationMessage = formatDeprecationMessage(name, /*error*/ true, errorAfter, since, message); + return () => { + throw new TypeError(deprecationMessage); + }; + } - function wrapFunction any>(deprecation: () => void, func: F): F { - return function (this: unknown) { - deprecation(); - return func.apply(this, arguments); - } as F; - } + function createWarningDeprecation(name: string, errorAfter: ts.Version | undefined, since: ts.Version | undefined, message: string | undefined) { + let hasWrittenDeprecation = false; + return () => { + if (!hasWrittenDeprecation) { + log.warn(formatDeprecationMessage(name, /*error*/ false, errorAfter, since, message)); + hasWrittenDeprecation = true; + } + }; + } - export function deprecate any>(func: F, options?: DeprecationOptions): F { - const deprecation = createDeprecation(getFunctionName(func), options); - return wrapFunction(deprecation, func); - } + function createDeprecation(name: string, options: DeprecationOptions & { + error: true; + }): () => never; + function createDeprecation(name: string, options?: DeprecationOptions): () => void; + function createDeprecation(name: string, options: DeprecationOptions = {}) { + const version = typeof options.typeScriptVersion === "string" ? new ts.Version(options.typeScriptVersion) : options.typeScriptVersion ?? getTypeScriptVersion(); + const errorAfter = typeof options.errorAfter === "string" ? new ts.Version(options.errorAfter) : options.errorAfter; + const warnAfter = typeof options.warnAfter === "string" ? new ts.Version(options.warnAfter) : options.warnAfter; + const since = typeof options.since === "string" ? new ts.Version(options.since) : options.since ?? warnAfter; + const error = options.error || errorAfter && version.compareTo(errorAfter) <= 0; + const warn = !warnAfter || version.compareTo(warnAfter) >= 0; + return error ? createErrorDeprecation(name, errorAfter, since, options.message) : + warn ? createWarningDeprecation(name, errorAfter, since, options.message) : ts.noop; + } + + function wrapFunction any>(deprecation: () => void, func: F): F { + return function (this: unknown) { + deprecation(); + return func.apply(this, arguments); + } as F; } + + export function deprecate any>(func: F, options?: DeprecationOptions): F { + const deprecation = createDeprecation(getFunctionName(func), options); + return wrapFunction(deprecation, func); + } +} } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index db9574f00b52a..1492ca8d861d4 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1,5837 +1,5837 @@ namespace ts { - const brackets = createBracketsMap(); +const brackets = createBracketsMap(); - /*@internal*/ - export function isBuildInfoFile(file: string) { - return ts.fileExtensionIs(file, ts.Extension.TsBuildInfo); - } +/*@internal*/ +export function isBuildInfoFile(file: string) { + return ts.fileExtensionIs(file, ts.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: ts.EmitHost, action: (emitFileNames: ts.EmitFileNames, sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined) => T, sourceFilesOrTargetSourceFile?: readonly ts.SourceFile[] | ts.SourceFile, forceDtsEmit = false, onlyBuildInfo?: boolean, includeBuildInfo?: boolean) { - const sourceFiles = ts.isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : ts.getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile, forceDtsEmit); - const options = host.getCompilerOptions(); - if (ts.outFile(options)) { - const prepends = host.getPrependNodes(); - if (sourceFiles.length || prepends.length) { - const bundle = ts.factory.createBundle(sourceFiles, prepends); - const result = action(getOutputPathsFor(bundle, host, forceDtsEmit), bundle); +/*@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: ts.EmitHost, action: (emitFileNames: ts.EmitFileNames, sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined) => T, sourceFilesOrTargetSourceFile?: readonly ts.SourceFile[] | ts.SourceFile, forceDtsEmit = false, onlyBuildInfo?: boolean, includeBuildInfo?: boolean) { + const sourceFiles = ts.isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : ts.getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile, forceDtsEmit); + const options = host.getCompilerOptions(); + if (ts.outFile(options)) { + const prepends = host.getPrependNodes(); + if (sourceFiles.length || prepends.length) { + const bundle = ts.factory.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(options); - if (buildInfoPath) - return action({ buildInfoPath }, /*sourceFileOrBundle*/ undefined); - } + if (includeBuildInfo) { + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(options); + if (buildInfoPath) + return action({ buildInfoPath }, /*sourceFileOrBundle*/ undefined); } } +} - export function getTsBuildInfoEmitOutputFilePath(options: ts.CompilerOptions) { - const configFile = options.configFilePath; - if (!ts.isIncrementalCompilation(options)) +export function getTsBuildInfoEmitOutputFilePath(options: ts.CompilerOptions) { + const configFile = options.configFilePath; + if (!ts.isIncrementalCompilation(options)) + return undefined; + if (options.tsBuildInfoFile) + return options.tsBuildInfoFile; + const outPath = ts.outFile(options); + let buildInfoExtensionLess: string; + if (outPath) { + buildInfoExtensionLess = ts.removeFileExtension(outPath); + } + else { + if (!configFile) return undefined; - if (options.tsBuildInfoFile) - return options.tsBuildInfoFile; - const outPath = ts.outFile(options); - let buildInfoExtensionLess: string; - if (outPath) { - buildInfoExtensionLess = ts.removeFileExtension(outPath); - } - else { - if (!configFile) - return undefined; - const configFileExtensionLess = ts.removeFileExtension(configFile); - buildInfoExtensionLess = options.outDir ? - options.rootDir ? - ts.resolvePath(options.outDir, ts.getRelativePathFromDirectory(options.rootDir, configFileExtensionLess, /*ignoreCase*/ true)) : - ts.combinePaths(options.outDir, ts.getBaseFileName(configFileExtensionLess)) : - configFileExtensionLess; - } - return buildInfoExtensionLess + ts.Extension.TsBuildInfo; - } - - /*@internal*/ - export function getOutputPathsForBundle(options: ts.CompilerOptions, forceDtsPaths: boolean): ts.EmitFileNames { - const outPath = ts.outFile(options)!; - const jsFilePath = options.emitDeclarationOnly ? undefined : outPath; - const sourceMapFilePath = jsFilePath && getSourceMapFilePath(jsFilePath, options); - const declarationFilePath = (forceDtsPaths || ts.getEmitDeclarations(options)) ? ts.removeFileExtension(outPath) + ts.Extension.Dts : undefined; + const configFileExtensionLess = ts.removeFileExtension(configFile); + buildInfoExtensionLess = options.outDir ? + options.rootDir ? + ts.resolvePath(options.outDir, ts.getRelativePathFromDirectory(options.rootDir, configFileExtensionLess, /*ignoreCase*/ true)) : + ts.combinePaths(options.outDir, ts.getBaseFileName(configFileExtensionLess)) : + configFileExtensionLess; + } + return buildInfoExtensionLess + ts.Extension.TsBuildInfo; +} + +/*@internal*/ +export function getOutputPathsForBundle(options: ts.CompilerOptions, forceDtsPaths: boolean): ts.EmitFileNames { + const outPath = ts.outFile(options)!; + const jsFilePath = options.emitDeclarationOnly ? undefined : outPath; + const sourceMapFilePath = jsFilePath && getSourceMapFilePath(jsFilePath, options); + const declarationFilePath = (forceDtsPaths || ts.getEmitDeclarations(options)) ? ts.removeFileExtension(outPath) + ts.Extension.Dts : undefined; + const declarationMapPath = declarationFilePath && ts.getAreDeclarationMapsEnabled(options) ? declarationFilePath + ".map" : undefined; + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(options); + return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }; +} + +/*@internal*/ +export function getOutputPathsFor(sourceFile: ts.SourceFile | ts.Bundle, host: ts.EmitHost, forceDtsPaths: boolean): ts.EmitFileNames { + const options = host.getCompilerOptions(); + if (sourceFile.kind === ts.SyntaxKind.Bundle) { + return getOutputPathsForBundle(options, forceDtsPaths); + } + else { + const ownOutputFilePath = ts.getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile.fileName, options)); + const isJsonFile = ts.isJsonSourceFile(sourceFile); + // If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it + const isJsonEmittedToSameLocation = isJsonFile && + ts.comparePaths(sourceFile.fileName, ownOutputFilePath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === ts.Comparison.EqualTo; + const jsFilePath = options.emitDeclarationOnly || isJsonEmittedToSameLocation ? undefined : ownOutputFilePath; + const sourceMapFilePath = !jsFilePath || ts.isJsonSourceFile(sourceFile) ? undefined : getSourceMapFilePath(jsFilePath, options); + const declarationFilePath = (forceDtsPaths || (ts.getEmitDeclarations(options) && !isJsonFile)) ? ts.getDeclarationEmitOutputFilePath(sourceFile.fileName, host) : undefined; const declarationMapPath = declarationFilePath && ts.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: ts.SourceFile | ts.Bundle, host: ts.EmitHost, forceDtsPaths: boolean): ts.EmitFileNames { - const options = host.getCompilerOptions(); - if (sourceFile.kind === ts.SyntaxKind.Bundle) { - return getOutputPathsForBundle(options, forceDtsPaths); - } - else { - const ownOutputFilePath = ts.getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile.fileName, options)); - const isJsonFile = ts.isJsonSourceFile(sourceFile); - // If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it - const isJsonEmittedToSameLocation = isJsonFile && - ts.comparePaths(sourceFile.fileName, ownOutputFilePath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === ts.Comparison.EqualTo; - const jsFilePath = options.emitDeclarationOnly || isJsonEmittedToSameLocation ? undefined : ownOutputFilePath; - const sourceMapFilePath = !jsFilePath || ts.isJsonSourceFile(sourceFile) ? undefined : getSourceMapFilePath(jsFilePath, options); - const declarationFilePath = (forceDtsPaths || (ts.getEmitDeclarations(options) && !isJsonFile)) ? ts.getDeclarationEmitOutputFilePath(sourceFile.fileName, host) : undefined; - const declarationMapPath = declarationFilePath && ts.getAreDeclarationMapsEnabled(options) ? declarationFilePath + ".map" : undefined; - return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath: undefined }; +function getSourceMapFilePath(jsFilePath: string, options: ts.CompilerOptions) { + return (options.sourceMap && !options.inlineSourceMap) ? jsFilePath + ".map" : undefined; +} + +/* @internal */ +export function getOutputExtension(fileName: string, options: ts.CompilerOptions): ts.Extension { + return ts.fileExtensionIs(fileName, ts.Extension.Json) ? ts.Extension.Json : + options.jsx === ts.JsxEmit.Preserve && ts.fileExtensionIsOneOf(fileName, [ts.Extension.Jsx, ts.Extension.Tsx]) ? ts.Extension.Jsx : + ts.fileExtensionIsOneOf(fileName, [ts.Extension.Mts, ts.Extension.Mjs]) ? ts.Extension.Mjs : + ts.fileExtensionIsOneOf(fileName, [ts.Extension.Cts, ts.Extension.Cjs]) ? ts.Extension.Cjs : + ts.Extension.Js; +} +function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined, getCommonSourceDirectory?: () => string) { + return outputDir ? + ts.resolvePath(outputDir, ts.getRelativePathFromDirectory(getCommonSourceDirectory ? getCommonSourceDirectory() : getCommonSourceDirectoryOfConfig(configFile, ignoreCase), inputFileName, ignoreCase)) : + inputFileName; +} + +/* @internal */ +export function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { + return ts.changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir, getCommonSourceDirectory), ts.getDeclarationEmitExtensionForPath(inputFileName)); +} +function getOutputJSFileName(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { + if (configFile.options.emitDeclarationOnly) + return undefined; + const isJsonFile = ts.fileExtensionIs(inputFileName, ts.Extension.Json); + const outputFileName = ts.changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir, getCommonSourceDirectory), getOutputExtension(inputFileName, configFile.options)); + return !isJsonFile || ts.comparePaths(inputFileName, outputFileName, ts.Debug.checkDefined(configFile.options.configFilePath), ignoreCase) !== ts.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 getOutputs(): readonly string[] { + return outputs || ts.emptyArray; + } +} - function getSourceMapFilePath(jsFilePath: string, options: ts.CompilerOptions) { - return (options.sourceMap && !options.inlineSourceMap) ? jsFilePath + ".map" : undefined; +function getSingleOutputFileNames(configFile: ts.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: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"], getCommonSourceDirectory?: () => string) { + if (ts.isDeclarationFileName(inputFileName)) + return; + const js = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); + addOutput(js); + if (ts.fileExtensionIs(inputFileName, ts.Extension.Json)) + return; + if (js && configFile.options.sourceMap) { + addOutput(`${js}.map`); + } + if (ts.getEmitDeclarations(configFile.options)) { + const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); + addOutput(dts); + if (configFile.options.declarationMap) { + addOutput(`${dts}.map`); + } } +} - /* @internal */ - export function getOutputExtension(fileName: string, options: ts.CompilerOptions): ts.Extension { - return ts.fileExtensionIs(fileName, ts.Extension.Json) ? ts.Extension.Json : - options.jsx === ts.JsxEmit.Preserve && ts.fileExtensionIsOneOf(fileName, [ts.Extension.Jsx, ts.Extension.Tsx]) ? ts.Extension.Jsx : - ts.fileExtensionIsOneOf(fileName, [ts.Extension.Mts, ts.Extension.Mjs]) ? ts.Extension.Mjs : - ts.fileExtensionIsOneOf(fileName, [ts.Extension.Cts, ts.Extension.Cjs]) ? ts.Extension.Cjs : - ts.Extension.Js; +/*@internal*/ +export function getCommonSourceDirectory(options: ts.CompilerOptions, emittedFiles: () => readonly string[], currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, checkSourceFilesBelongToPath?: (commonSourceDirectory: string) => void): string { + let commonSourceDirectory; + if (options.rootDir) { + // If a rootDir is specified use it as the commonSourceDirectory + commonSourceDirectory = ts.getNormalizedAbsolutePath(options.rootDir, currentDirectory); + checkSourceFilesBelongToPath?.(options.rootDir); + } + else if (options.composite && options.configFilePath) { + // Project compilations never infer their root from the input source paths + commonSourceDirectory = ts.getDirectoryPath(ts.normalizeSlashes(options.configFilePath)); + checkSourceFilesBelongToPath?.(commonSourceDirectory); } - function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined, getCommonSourceDirectory?: () => string) { - return outputDir ? - ts.resolvePath(outputDir, ts.getRelativePathFromDirectory(getCommonSourceDirectory ? getCommonSourceDirectory() : getCommonSourceDirectoryOfConfig(configFile, ignoreCase), inputFileName, ignoreCase)) : - inputFileName; + else { + commonSourceDirectory = ts.computeCommonSourceDirectoryOfFilenames(emittedFiles(), currentDirectory, getCanonicalFileName); } - /* @internal */ - export function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { - return ts.changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir, getCommonSourceDirectory), ts.getDeclarationEmitExtensionForPath(inputFileName)); + if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== ts.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 += ts.directorySeparator; } - function getOutputJSFileName(inputFileName: string, configFile: ts.ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { - if (configFile.options.emitDeclarationOnly) - return undefined; - const isJsonFile = ts.fileExtensionIs(inputFileName, ts.Extension.Json); - const outputFileName = ts.changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir, getCommonSourceDirectory), getOutputExtension(inputFileName, configFile.options)); - return !isJsonFile || ts.comparePaths(inputFileName, outputFileName, ts.Debug.checkDefined(configFile.options.configFilePath), ignoreCase) !== ts.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 getOutputs(): readonly string[] { - return outputs || ts.emptyArray; + return commonSourceDirectory; +} + +/*@internal*/ +export function getCommonSourceDirectoryOfConfig({ options, fileNames }: ts.ParsedCommandLine, ignoreCase: boolean): string { + return getCommonSourceDirectory(options, () => ts.filter(fileNames, file => !(options.noEmitForJsFiles && ts.fileExtensionIsOneOf(file, ts.supportedJSExtensionsFlat)) && !ts.isDeclarationFileName(file)), ts.getDirectoryPath(ts.normalizeSlashes(ts.Debug.checkDefined(options.configFilePath))), ts.createGetCanonicalFileName(!ignoreCase)); +} + +/*@internal*/ +export function getAllProjectOutputs(configFile: ts.ParsedCommandLine, ignoreCase: boolean): readonly string[] { + const { addOutput, getOutputs } = createAddOutput(); + if (ts.outFile(configFile.options)) { + getSingleOutputFileNames(configFile, addOutput); + } + else { + const getCommonSourceDirectory = ts.memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); + for (const inputFileName of configFile.fileNames) { + getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput, getCommonSourceDirectory); } + addOutput(getTsBuildInfoEmitOutputFilePath(configFile.options)); + } + return getOutputs(); +} + +export function getOutputFileNames(commandLine: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[] { + inputFileName = ts.normalizePath(inputFileName); + ts.Debug.assert(ts.contains(commandLine.fileNames, inputFileName), `Expected fileName to be present in command line`); + const { addOutput, getOutputs } = createAddOutput(); + if (ts.outFile(commandLine.options)) { + getSingleOutputFileNames(commandLine, addOutput); + } + else { + getOwnOutputFileNames(commandLine, inputFileName, ignoreCase, addOutput); } + return getOutputs(); +} - function getSingleOutputFileNames(configFile: ts.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); +/*@internal*/ +export function getFirstProjectOutput(configFile: ts.ParsedCommandLine, ignoreCase: boolean): string { + if (ts.outFile(configFile.options)) { + const { jsFilePath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); + return ts.Debug.checkDefined(jsFilePath, `project ${configFile.options.configFilePath} expected to have at least one output`); } - function getOwnOutputFileNames(configFile: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"], getCommonSourceDirectory?: () => string) { + const getCommonSourceDirectory = ts.memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); + for (const inputFileName of configFile.fileNames) { if (ts.isDeclarationFileName(inputFileName)) - return; - const js = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); - addOutput(js); + continue; + const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); + if (jsFilePath) + return jsFilePath; if (ts.fileExtensionIs(inputFileName, ts.Extension.Json)) - return; - if (js && configFile.options.sourceMap) { - addOutput(`${js}.map`); - } + continue; if (ts.getEmitDeclarations(configFile.options)) { - const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); - addOutput(dts); - if (configFile.options.declarationMap) { - addOutput(`${dts}.map`); - } + return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); } } + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(configFile.options); + if (buildInfoPath) + return buildInfoPath; + return ts.Debug.fail(`project ${configFile.options.configFilePath} expected to have at least one output`); +} - /*@internal*/ - export function getCommonSourceDirectory(options: ts.CompilerOptions, emittedFiles: () => readonly string[], currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, checkSourceFilesBelongToPath?: (commonSourceDirectory: string) => void): string { - let commonSourceDirectory; - if (options.rootDir) { - // If a rootDir is specified use it as the commonSourceDirectory - commonSourceDirectory = ts.getNormalizedAbsolutePath(options.rootDir, currentDirectory); - checkSourceFilesBelongToPath?.(options.rootDir); - } - else if (options.composite && options.configFilePath) { - // Project compilations never infer their root from the input source paths - commonSourceDirectory = ts.getDirectoryPath(ts.normalizeSlashes(options.configFilePath)); - checkSourceFilesBelongToPath?.(commonSourceDirectory); +/*@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: ts.EmitResolver, host: ts.EmitHost, targetSourceFile: ts.SourceFile | undefined, { scriptTransformers, declarationTransformers }: ts.EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): ts.EmitResult { + const compilerOptions = host.getCompilerOptions(); + const sourceMapDataList: ts.SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || ts.getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined; + const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined; + const emitterDiagnostics = ts.createDiagnosticCollection(); + const newLine = ts.getNewLineCharacter(compilerOptions, () => host.getNewLine()); + const writer = ts.createTextWriter(newLine); + const { enter, exit } = ts.performance.createTimer("printTime", "beforePrint", "afterPrint"); + let bundleBuildInfo: ts.BundleBuildInfo | undefined; + let emitSkipped = false; + let exportedModulesFromDeclarationEmit: ts.ExportedModulesFromDeclarationEmit | undefined; + + // Emit each output file + enter(); + forEachEmittedFile(host, emitSourceFileOrBundle, ts.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 }: ts.EmitFileNames, sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined) { + let buildInfoDirectory: string | undefined; + if (buildInfoPath && sourceFileOrBundle && ts.isBundle(sourceFileOrBundle)) { + buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); + bundleBuildInfo = { + commonSourceDirectory: relativeToBuildInfo(host.getCommonSourceDirectory()), + sourceFiles: sourceFileOrBundle.sourceFiles.map(file => relativeToBuildInfo(ts.getNormalizedAbsolutePath(file.fileName, host.getCurrentDirectory()))) + }; } - else { - commonSourceDirectory = ts.computeCommonSourceDirectoryOfFilenames(emittedFiles(), currentDirectory, getCanonicalFileName); + ts.tracing?.push(ts.tracing.Phase.Emit, "emitJsFileOrBundle", { jsFilePath }); + emitJsFileOrBundle(sourceFileOrBundle, jsFilePath, sourceMapFilePath, relativeToBuildInfo); + ts.tracing?.pop(); + ts.tracing?.push(ts.tracing.Phase.Emit, "emitDeclarationFileOrBundle", { declarationFilePath }); + emitDeclarationFileOrBundle(sourceFileOrBundle, declarationFilePath, declarationMapPath, relativeToBuildInfo); + ts.tracing?.pop(); + ts.tracing?.push(ts.tracing.Phase.Emit, "emitBuildInfo", { buildInfoPath }); + emitBuildInfo(bundleBuildInfo, buildInfoPath); + ts.tracing?.pop(); + + 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); + } } - if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== ts.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 += ts.directorySeparator; + function relativeToBuildInfo(path: string) { + return ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(buildInfoDirectory!, path, host.getCanonicalFileName)); } - return commonSourceDirectory; - } - - /*@internal*/ - export function getCommonSourceDirectoryOfConfig({ options, fileNames }: ts.ParsedCommandLine, ignoreCase: boolean): string { - return getCommonSourceDirectory(options, () => ts.filter(fileNames, file => !(options.noEmitForJsFiles && ts.fileExtensionIsOneOf(file, ts.supportedJSExtensionsFlat)) && !ts.isDeclarationFileName(file)), ts.getDirectoryPath(ts.normalizeSlashes(ts.Debug.checkDefined(options.configFilePath))), ts.createGetCanonicalFileName(!ignoreCase)); } - /*@internal*/ - export function getAllProjectOutputs(configFile: ts.ParsedCommandLine, ignoreCase: boolean): readonly string[] { - const { addOutput, getOutputs } = createAddOutput(); - if (ts.outFile(configFile.options)) { - getSingleOutputFileNames(configFile, addOutput); - } - else { - const getCommonSourceDirectory = ts.memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); - for (const inputFileName of configFile.fileNames) { - getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput, getCommonSourceDirectory); - } - addOutput(getTsBuildInfoEmitOutputFilePath(configFile.options)); + function emitBuildInfo(bundle: ts.BundleBuildInfo | undefined, buildInfoPath: string | undefined) { + // Write build information if applicable + if (!buildInfoPath || targetSourceFile || emitSkipped) + return; + const program = host.getProgramBuildInfo(); + if (host.isEmitBlocked(buildInfoPath)) { + emitSkipped = true; + return; } - return getOutputs(); + const version = ts.version; // Extracted into a const so the form is stable between namespace and module + ts.writeFile(host, emitterDiagnostics, buildInfoPath, getBuildInfoText({ bundle, program, version }), /*writeByteOrderMark*/ false); } - - export function getOutputFileNames(commandLine: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[] { - inputFileName = ts.normalizePath(inputFileName); - ts.Debug.assert(ts.contains(commandLine.fileNames, inputFileName), `Expected fileName to be present in command line`); - const { addOutput, getOutputs } = createAddOutput(); - if (ts.outFile(commandLine.options)) { - getSingleOutputFileNames(commandLine, addOutput); - } - else { - getOwnOutputFileNames(commandLine, inputFileName, ignoreCase, addOutput); + function emitJsFileOrBundle(sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined, jsFilePath: string | undefined, sourceMapFilePath: string | undefined, relativeToBuildInfo: (path: string) => string) { + if (!sourceFileOrBundle || emitOnlyDtsFiles || !jsFilePath) { + return; } - return getOutputs(); - } - /*@internal*/ - export function getFirstProjectOutput(configFile: ts.ParsedCommandLine, ignoreCase: boolean): string { - if (ts.outFile(configFile.options)) { - const { jsFilePath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); - return ts.Debug.checkDefined(jsFilePath, `project ${configFile.options.configFilePath} expected to have at least one output`); + // 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 = ts.transformNodes(resolver, host, ts.factory, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false); + const printerOptions: ts.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 + }; - const getCommonSourceDirectory = ts.memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); - for (const inputFileName of configFile.fileNames) { - if (ts.isDeclarationFileName(inputFileName)) - continue; - const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); - if (jsFilePath) - return jsFilePath; - if (ts.fileExtensionIs(inputFileName, ts.Extension.Json)) - continue; - if (ts.getEmitDeclarations(configFile.options)) { - return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); - } + // 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, + }); + + ts.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: ts.SourceFile | ts.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 buildInfoPath = getTsBuildInfoEmitOutputFilePath(configFile.options); - if (buildInfoPath) - return buildInfoPath; - return ts.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: ts.EmitResolver, host: ts.EmitHost, targetSourceFile: ts.SourceFile | undefined, { scriptTransformers, declarationTransformers }: ts.EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): ts.EmitResult { - const compilerOptions = host.getCompilerOptions(); - const sourceMapDataList: ts.SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || ts.getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined; - const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined; - const emitterDiagnostics = ts.createDiagnosticCollection(); - const newLine = ts.getNewLineCharacter(compilerOptions, () => host.getNewLine()); - const writer = ts.createTextWriter(newLine); - const { enter, exit } = ts.performance.createTimer("printTime", "beforePrint", "afterPrint"); - let bundleBuildInfo: ts.BundleBuildInfo | undefined; - let emitSkipped = false; - let exportedModulesFromDeclarationEmit: ts.ExportedModulesFromDeclarationEmit | undefined; - - // Emit each output file - enter(); - forEachEmittedFile(host, emitSourceFileOrBundle, ts.getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit), forceDtsEmit, onlyBuildInfo, !targetSourceFile); - exit(); - - - return { - emitSkipped, - diagnostics: emitterDiagnostics.getDiagnostics(), - emittedFiles: emittedFilesList, - sourceMaps: sourceMapDataList, - exportedModulesFromDeclarationEmit + const sourceFiles = ts.isSourceFile(sourceFileOrBundle) ? [sourceFileOrBundle] : sourceFileOrBundle.sourceFiles; + const filesForEmit = forceDtsEmit ? sourceFiles : ts.filter(sourceFiles, ts.isSourceFileNotJson); + // Setup and perform the transformation to retrieve declarations from the input files + const inputListOrBundle = ts.outFile(compilerOptions) ? [ts.factory.createBundle(filesForEmit, !ts.isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.prepends : undefined)] : filesForEmit; + if (emitOnlyDtsFiles && !ts.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 = ts.transformNodes(resolver, host, ts.factory, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false); + if (ts.length(declarationTransform.diagnostics)) { + for (const diagnostic of declarationTransform.diagnostics!) { + emitterDiagnostics.add(diagnostic); + } + } + + const printerOptions: ts.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 emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }: ts.EmitFileNames, sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined) { - let buildInfoDirectory: string | undefined; - if (buildInfoPath && sourceFileOrBundle && ts.isBundle(sourceFileOrBundle)) { - buildInfoDirectory = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); - bundleBuildInfo = { - commonSourceDirectory: relativeToBuildInfo(host.getCommonSourceDirectory()), - sourceFiles: sourceFileOrBundle.sourceFiles.map(file => relativeToBuildInfo(ts.getNormalizedAbsolutePath(file.fileName, host.getCurrentDirectory()))) - }; - } - ts.tracing?.push(ts.tracing.Phase.Emit, "emitJsFileOrBundle", { jsFilePath }); - emitJsFileOrBundle(sourceFileOrBundle, jsFilePath, sourceMapFilePath, relativeToBuildInfo); - ts.tracing?.pop(); - ts.tracing?.push(ts.tracing.Phase.Emit, "emitDeclarationFileOrBundle", { declarationFilePath }); - emitDeclarationFileOrBundle(sourceFileOrBundle, declarationFilePath, declarationMapPath, relativeToBuildInfo); - ts.tracing?.pop(); - ts.tracing?.push(ts.tracing.Phase.Emit, "emitBuildInfo", { buildInfoPath }); - emitBuildInfo(bundleBuildInfo, buildInfoPath); - ts.tracing?.pop(); - - 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); - } - } + const declarationPrinter = createPrinter(printerOptions, { + // resolver hooks + hasGlobalName: resolver.hasGlobalName, - function relativeToBuildInfo(path: string) { - return ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(buildInfoDirectory!, path, host.getCanonicalFileName)); + // 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) { + ts.Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform"); + printSourceFileOrBundle(declarationFilePath, declarationMapPath, declarationTransform.transformed[0], declarationPrinter, { + sourceMap: !forceDtsEmit && compilerOptions.declarationMap, + sourceRoot: compilerOptions.sourceRoot, + mapRoot: compilerOptions.mapRoot, + extendedDiagnostics: compilerOptions.extendedDiagnostics, + // Explicitly do not passthru either `inline` option + }); + if (forceDtsEmit && declarationTransform.transformed[0].kind === ts.SyntaxKind.SourceFile) { + const sourceFile = declarationTransform.transformed[0]; + exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit; } } + declarationTransform.dispose(); + if (bundleBuildInfo) + bundleBuildInfo.dts = declarationPrinter.bundleFileInfo; + } - function emitBuildInfo(bundle: ts.BundleBuildInfo | undefined, buildInfoPath: string | undefined) { - // Write build information if applicable - if (!buildInfoPath || targetSourceFile || emitSkipped) - return; - const program = host.getProgramBuildInfo(); - if (host.isEmitBlocked(buildInfoPath)) { - emitSkipped = true; - return; + function collectLinkedAliases(node: ts.Node) { + if (ts.isExportAssignment(node)) { + if (node.expression.kind === ts.SyntaxKind.Identifier) { + resolver.collectLinkedAliases(node.expression as ts.Identifier, /*setVisibility*/ true); } - const version = ts.version; // Extracted into a const so the form is stable between namespace and module - ts.writeFile(host, emitterDiagnostics, buildInfoPath, getBuildInfoText({ bundle, program, version }), /*writeByteOrderMark*/ false); + return; } - function emitJsFileOrBundle(sourceFileOrBundle: ts.SourceFile | ts.Bundle | undefined, jsFilePath: string | undefined, sourceMapFilePath: string | undefined, relativeToBuildInfo: (path: string) => string) { - if (!sourceFileOrBundle || emitOnlyDtsFiles || !jsFilePath) { - return; - } + else if (ts.isExportSpecifier(node)) { + resolver.collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); + return; + } + ts.forEachChild(node, collectLinkedAliases); + } - // 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 = ts.transformNodes(resolver, host, ts.factory, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false); - const printerOptions: ts.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 - }; + function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: ts.SourceFile | ts.Bundle, printer: ts.Printer, mapOptions: SourceMapOptions) { + const bundle = sourceFileOrBundle.kind === ts.SyntaxKind.Bundle ? sourceFileOrBundle : undefined; + const sourceFile = sourceFileOrBundle.kind === ts.SyntaxKind.SourceFile ? sourceFileOrBundle : undefined; + const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile!]; - // Create a printer to print the nodes - const printer = createPrinter(printerOptions, { - // resolver hooks - hasGlobalName: resolver.hasGlobalName, + let sourceMapGenerator: ts.SourceMapGenerator | undefined; + if (shouldEmitSourceMaps(mapOptions, sourceFileOrBundle)) { + sourceMapGenerator = ts.createSourceMapGenerator(host, ts.getBaseFileName(ts.normalizeSlashes(jsFilePath)), getSourceRoot(mapOptions), getSourceMapDirectory(mapOptions, jsFilePath, sourceFile), mapOptions); + } - // transform hooks - onEmitNode: transform.emitNodeWithNotification, - isEmitNotificationEnabled: transform.isEmitNotificationEnabled, - substituteNode: transform.substituteNode, - }); + if (bundle) { + printer.writeBundle(bundle, writer, sourceMapGenerator); + } + else { + printer.writeFile(sourceFile!, writer, sourceMapGenerator); + } - ts.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: ts.SourceFile | ts.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 = ts.isSourceFile(sourceFileOrBundle) ? [sourceFileOrBundle] : sourceFileOrBundle.sourceFiles; - const filesForEmit = forceDtsEmit ? sourceFiles : ts.filter(sourceFiles, ts.isSourceFileNotJson); - // Setup and perform the transformation to retrieve declarations from the input files - const inputListOrBundle = ts.outFile(compilerOptions) ? [ts.factory.createBundle(filesForEmit, !ts.isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.prepends : undefined)] : filesForEmit; - if (emitOnlyDtsFiles && !ts.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 = ts.transformNodes(resolver, host, ts.factory, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false); - if (ts.length(declarationTransform.diagnostics)) { - for (const diagnostic of declarationTransform.diagnostics!) { - emitterDiagnostics.add(diagnostic); - } + let sourceMapUrlPos; + if (sourceMapGenerator) { + if (sourceMapDataList) { + sourceMapDataList.push({ + inputSourceFileNames: sourceMapGenerator.getSources(), + sourceMap: sourceMapGenerator.toJSON() + }); } - const printerOptions: ts.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 declarationPrinter = createPrinter(printerOptions, { - // resolver hooks - hasGlobalName: resolver.hasGlobalName, + const sourceMappingURL = getSourceMappingURL(mapOptions, sourceMapGenerator, jsFilePath, sourceMapFilePath, sourceFile); - // 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) { - ts.Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform"); - printSourceFileOrBundle(declarationFilePath, declarationMapPath, declarationTransform.transformed[0], declarationPrinter, { - sourceMap: !forceDtsEmit && compilerOptions.declarationMap, - sourceRoot: compilerOptions.sourceRoot, - mapRoot: compilerOptions.mapRoot, - extendedDiagnostics: compilerOptions.extendedDiagnostics, - // Explicitly do not passthru either `inline` option - }); - if (forceDtsEmit && declarationTransform.transformed[0].kind === ts.SyntaxKind.SourceFile) { - const sourceFile = declarationTransform.transformed[0]; - exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit; - } + if (sourceMappingURL) { + if (!writer.isAtStartOfLine()) + writer.rawWrite(newLine); + sourceMapUrlPos = writer.getTextPos(); + writer.writeComment(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Tools can sometimes see this line as a source mapping url comment } - declarationTransform.dispose(); - if (bundleBuildInfo) - bundleBuildInfo.dts = declarationPrinter.bundleFileInfo; - } - function collectLinkedAliases(node: ts.Node) { - if (ts.isExportAssignment(node)) { - if (node.expression.kind === ts.SyntaxKind.Identifier) { - resolver.collectLinkedAliases(node.expression as ts.Identifier, /*setVisibility*/ true); - } - return; - } - else if (ts.isExportSpecifier(node)) { - resolver.collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); - return; + // Write the source map + if (sourceMapFilePath) { + const sourceMap = sourceMapGenerator.toString(); + ts.writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles); } - ts.forEachChild(node, collectLinkedAliases); + } + else { + writer.writeLine(); } - function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: ts.SourceFile | ts.Bundle, printer: ts.Printer, mapOptions: SourceMapOptions) { - const bundle = sourceFileOrBundle.kind === ts.SyntaxKind.Bundle ? sourceFileOrBundle : undefined; - const sourceFile = sourceFileOrBundle.kind === ts.SyntaxKind.SourceFile ? sourceFileOrBundle : undefined; - const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile!]; - - let sourceMapGenerator: ts.SourceMapGenerator | undefined; - if (shouldEmitSourceMaps(mapOptions, sourceFileOrBundle)) { - sourceMapGenerator = ts.createSourceMapGenerator(host, ts.getBaseFileName(ts.normalizeSlashes(jsFilePath)), getSourceRoot(mapOptions), getSourceMapDirectory(mapOptions, jsFilePath, sourceFile), mapOptions); - } + // Write the output file + ts.writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles, { sourceMapUrlPos }); - if (bundle) { - printer.writeBundle(bundle, writer, sourceMapGenerator); - } - else { - printer.writeFile(sourceFile!, writer, sourceMapGenerator); - } + // Reset state + writer.clear(); + } - let sourceMapUrlPos; - if (sourceMapGenerator) { - if (sourceMapDataList) { - sourceMapDataList.push({ - inputSourceFileNames: sourceMapGenerator.getSources(), - sourceMap: sourceMapGenerator.toJSON() - }); - } + interface SourceMapOptions { + sourceMap?: boolean; + inlineSourceMap?: boolean; + inlineSources?: boolean; + sourceRoot?: string; + mapRoot?: string; + extendedDiagnostics?: boolean; + } - const sourceMappingURL = getSourceMappingURL(mapOptions, sourceMapGenerator, jsFilePath, sourceMapFilePath, sourceFile); + function shouldEmitSourceMaps(mapOptions: SourceMapOptions, sourceFileOrBundle: ts.SourceFile | ts.Bundle) { + return (mapOptions.sourceMap || mapOptions.inlineSourceMap) + && (sourceFileOrBundle.kind !== ts.SyntaxKind.SourceFile || !ts.fileExtensionIs(sourceFileOrBundle.fileName, ts.Extension.Json)); + } - if (sourceMappingURL) { - if (!writer.isAtStartOfLine()) - writer.rawWrite(newLine); - sourceMapUrlPos = writer.getTextPos(); - writer.writeComment(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Tools can sometimes see this line as a source mapping url comment - } + 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 = ts.normalizeSlashes(mapOptions.sourceRoot || ""); + return sourceRoot ? ts.ensureTrailingDirectorySeparator(sourceRoot) : sourceRoot; + } - // Write the source map - if (sourceMapFilePath) { - const sourceMap = sourceMapGenerator.toString(); - ts.writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles); - } + function getSourceMapDirectory(mapOptions: SourceMapOptions, filePath: string, sourceFile: ts.SourceFile | undefined) { + if (mapOptions.sourceRoot) + return host.getCommonSourceDirectory(); + if (mapOptions.mapRoot) { + let sourceMapDir = ts.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 = ts.getDirectoryPath(ts.getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); } - else { - writer.writeLine(); + if (ts.getRootLength(sourceMapDir) === 0) { + // The relative paths are relative to the common directory + sourceMapDir = ts.combinePaths(host.getCommonSourceDirectory(), sourceMapDir); } - - // Write the output file - ts.writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles, { sourceMapUrlPos }); - - // Reset state - writer.clear(); + return sourceMapDir; } + return ts.getDirectoryPath(ts.normalizePath(filePath)); + } - interface SourceMapOptions { - sourceMap?: boolean; - inlineSourceMap?: boolean; - inlineSources?: boolean; - sourceRoot?: string; - mapRoot?: string; - extendedDiagnostics?: boolean; + function getSourceMappingURL(mapOptions: SourceMapOptions, sourceMapGenerator: ts.SourceMapGenerator, filePath: string, sourceMapFilePath: string | undefined, sourceFile: ts.SourceFile | undefined) { + if (mapOptions.inlineSourceMap) { + // Encode the sourceMap into the sourceMap url + const sourceMapText = sourceMapGenerator.toString(); + const base64SourceMapText = ts.base64encode(ts.sys, sourceMapText); + return `data:application/json;base64,${base64SourceMapText}`; } - function shouldEmitSourceMaps(mapOptions: SourceMapOptions, sourceFileOrBundle: ts.SourceFile | ts.Bundle) { - return (mapOptions.sourceMap || mapOptions.inlineSourceMap) - && (sourceFileOrBundle.kind !== ts.SyntaxKind.SourceFile || !ts.fileExtensionIs(sourceFileOrBundle.fileName, ts.Extension.Json)); + const sourceMapFile = ts.getBaseFileName(ts.normalizeSlashes(ts.Debug.checkDefined(sourceMapFilePath))); + if (mapOptions.mapRoot) { + let sourceMapDir = ts.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 = ts.getDirectoryPath(ts.getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); + } + if (ts.getRootLength(sourceMapDir) === 0) { + // The relative paths are relative to the common directory + sourceMapDir = ts.combinePaths(host.getCommonSourceDirectory(), sourceMapDir); + return encodeURI(ts.getRelativePathToDirectoryOrUrl(ts.getDirectoryPath(ts.normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath + ts.combinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap + host.getCurrentDirectory(), host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ true)); + } + else { + return encodeURI(ts.combinePaths(sourceMapDir, sourceMapFile)); + } } + return encodeURI(sourceMapFile); + } +} - 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 = ts.normalizeSlashes(mapOptions.sourceRoot || ""); - return sourceRoot ? ts.ensureTrailingDirectorySeparator(sourceRoot) : sourceRoot; - } +/*@internal*/ +export function getBuildInfoText(buildInfo: ts.BuildInfo) { + return JSON.stringify(buildInfo); +} - function getSourceMapDirectory(mapOptions: SourceMapOptions, filePath: string, sourceFile: ts.SourceFile | undefined) { - if (mapOptions.sourceRoot) - return host.getCommonSourceDirectory(); - if (mapOptions.mapRoot) { - let sourceMapDir = ts.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 = ts.getDirectoryPath(ts.getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); - } - if (ts.getRootLength(sourceMapDir) === 0) { - // The relative paths are relative to the common directory - sourceMapDir = ts.combinePaths(host.getCommonSourceDirectory(), sourceMapDir); - } - return sourceMapDir; - } - return ts.getDirectoryPath(ts.normalizePath(filePath)); - } +/*@internal*/ +export function getBuildInfo(buildInfoText: string) { + return JSON.parse(buildInfoText) as ts.BuildInfo; +} - function getSourceMappingURL(mapOptions: SourceMapOptions, sourceMapGenerator: ts.SourceMapGenerator, filePath: string, sourceMapFilePath: string | undefined, sourceFile: ts.SourceFile | undefined) { - if (mapOptions.inlineSourceMap) { - // Encode the sourceMap into the sourceMap url - const sourceMapText = sourceMapGenerator.toString(); - const base64SourceMapText = ts.base64encode(ts.sys, sourceMapText); - return `data:application/json;base64,${base64SourceMapText}`; - } +/*@internal*/ +export const notImplementedResolver: ts.EmitResolver = { + hasGlobalName: ts.notImplemented, + getReferencedExportContainer: ts.notImplemented, + getReferencedImportDeclaration: ts.notImplemented, + getReferencedDeclarationWithCollidingName: ts.notImplemented, + isDeclarationWithCollidingName: ts.notImplemented, + isValueAliasDeclaration: ts.notImplemented, + isReferencedAliasDeclaration: ts.notImplemented, + isTopLevelValueImportEqualsWithEntityName: ts.notImplemented, + getNodeCheckFlags: ts.notImplemented, + isDeclarationVisible: ts.notImplemented, + isLateBound: (_node): _node is ts.LateBoundDeclaration => false, + collectLinkedAliases: ts.notImplemented, + isImplementationOfOverload: ts.notImplemented, + isRequiredInitializedParameter: ts.notImplemented, + isOptionalUninitializedParameterProperty: ts.notImplemented, + isExpandoFunctionDeclaration: ts.notImplemented, + getPropertiesOfContainerFunction: ts.notImplemented, + createTypeOfDeclaration: ts.notImplemented, + createReturnTypeOfSignatureDeclaration: ts.notImplemented, + createTypeOfExpression: ts.notImplemented, + createLiteralConstValue: ts.notImplemented, + isSymbolAccessible: ts.notImplemented, + isEntityNameVisible: ts.notImplemented, + // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant + getConstantValue: ts.notImplemented, + getReferencedValueDeclaration: ts.notImplemented, + getTypeReferenceSerializationKind: ts.notImplemented, + isOptionalParameter: ts.notImplemented, + moduleExportsSomeValue: ts.notImplemented, + isArgumentsLocalBinding: ts.notImplemented, + getExternalModuleFileFromDeclaration: ts.notImplemented, + getTypeReferenceDirectivesForEntityName: ts.notImplemented, + getTypeReferenceDirectivesForSymbol: ts.notImplemented, + isLiteralConstDeclaration: ts.notImplemented, + getJsxFactoryEntity: ts.notImplemented, + getJsxFragmentFactoryEntity: ts.notImplemented, + getAllAccessorDeclarations: ts.notImplemented, + getSymbolOfExternalModuleSpecifier: ts.notImplemented, + isBindingCapturedByNode: ts.notImplemented, + getDeclarationStatementsForSourceFile: ts.notImplemented, + isImportRequiredByAugmentation: ts.notImplemented, +}; + +/*@internal*/ +/** File that isnt present resulting in error or output files */ +export type EmitUsingBuildInfoResult = string | readonly ts.OutputFile[]; + +/*@internal*/ +export interface EmitUsingBuildInfoHost extends ts.ModuleResolutionHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + useCaseSensitiveFileNames(): boolean; + getNewLine(): string; +} - const sourceMapFile = ts.getBaseFileName(ts.normalizeSlashes(ts.Debug.checkDefined(sourceMapFilePath))); - if (mapOptions.mapRoot) { - let sourceMapDir = ts.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 = ts.getDirectoryPath(ts.getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); - } - if (ts.getRootLength(sourceMapDir) === 0) { - // The relative paths are relative to the common directory - sourceMapDir = ts.combinePaths(host.getCommonSourceDirectory(), sourceMapDir); - return encodeURI(ts.getRelativePathToDirectoryOrUrl(ts.getDirectoryPath(ts.normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath - ts.combinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap - host.getCurrentDirectory(), host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ true)); - } - else { - return encodeURI(ts.combinePaths(sourceMapDir, sourceMapFile)); - } - } - return encodeURI(sourceMapFile); - } - } - - /*@internal*/ - export function getBuildInfoText(buildInfo: ts.BuildInfo) { - return JSON.stringify(buildInfo); - } - - /*@internal*/ - export function getBuildInfo(buildInfoText: string) { - return JSON.parse(buildInfoText) as ts.BuildInfo; - } - - /*@internal*/ - export const notImplementedResolver: ts.EmitResolver = { - hasGlobalName: ts.notImplemented, - getReferencedExportContainer: ts.notImplemented, - getReferencedImportDeclaration: ts.notImplemented, - getReferencedDeclarationWithCollidingName: ts.notImplemented, - isDeclarationWithCollidingName: ts.notImplemented, - isValueAliasDeclaration: ts.notImplemented, - isReferencedAliasDeclaration: ts.notImplemented, - isTopLevelValueImportEqualsWithEntityName: ts.notImplemented, - getNodeCheckFlags: ts.notImplemented, - isDeclarationVisible: ts.notImplemented, - isLateBound: (_node): _node is ts.LateBoundDeclaration => false, - collectLinkedAliases: ts.notImplemented, - isImplementationOfOverload: ts.notImplemented, - isRequiredInitializedParameter: ts.notImplemented, - isOptionalUninitializedParameterProperty: ts.notImplemented, - isExpandoFunctionDeclaration: ts.notImplemented, - getPropertiesOfContainerFunction: ts.notImplemented, - createTypeOfDeclaration: ts.notImplemented, - createReturnTypeOfSignatureDeclaration: ts.notImplemented, - createTypeOfExpression: ts.notImplemented, - createLiteralConstValue: ts.notImplemented, - isSymbolAccessible: ts.notImplemented, - isEntityNameVisible: ts.notImplemented, - // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant - getConstantValue: ts.notImplemented, - getReferencedValueDeclaration: ts.notImplemented, - getTypeReferenceSerializationKind: ts.notImplemented, - isOptionalParameter: ts.notImplemented, - moduleExportsSomeValue: ts.notImplemented, - isArgumentsLocalBinding: ts.notImplemented, - getExternalModuleFileFromDeclaration: ts.notImplemented, - getTypeReferenceDirectivesForEntityName: ts.notImplemented, - getTypeReferenceDirectivesForSymbol: ts.notImplemented, - isLiteralConstDeclaration: ts.notImplemented, - getJsxFactoryEntity: ts.notImplemented, - getJsxFragmentFactoryEntity: ts.notImplemented, - getAllAccessorDeclarations: ts.notImplemented, - getSymbolOfExternalModuleSpecifier: ts.notImplemented, - isBindingCapturedByNode: ts.notImplemented, - getDeclarationStatementsForSourceFile: ts.notImplemented, - isImportRequiredByAugmentation: ts.notImplemented, +function createSourceFilesFromBundleBuildInfo(bundle: ts.BundleBuildInfo, buildInfoDirectory: string, host: EmitUsingBuildInfoHost): readonly ts.SourceFile[] { + const jsBundle = ts.Debug.checkDefined(bundle.js); + const prologueMap = jsBundle.sources?.prologues && ts.arrayToMap(jsBundle.sources.prologues, prologueInfo => prologueInfo.file); + return bundle.sourceFiles.map((fileName, index) => { + const prologueInfo = prologueMap?.get(index); + const statements = prologueInfo?.directives.map(directive => { + const literal = ts.setTextRange(ts.factory.createStringLiteral(directive.expression.text), directive.expression); + const statement = ts.setTextRange(ts.factory.createExpressionStatement(literal), directive); + ts.setParent(literal, statement); + return statement; + }); + const eofToken = ts.factory.createToken(ts.SyntaxKind.EndOfFileToken); + const sourceFile = ts.factory.createSourceFile(statements ?? [], eofToken, ts.NodeFlags.None); + sourceFile.fileName = ts.getRelativePathFromDirectory(host.getCurrentDirectory(), ts.getNormalizedAbsolutePath(fileName, buildInfoDirectory), !host.useCaseSensitiveFileNames()); + sourceFile.text = prologueInfo?.text ?? ""; + ts.setTextRangePosWidth(sourceFile, 0, prologueInfo?.text.length ?? 0); + ts.setEachParent(sourceFile.statements, sourceFile); + ts.setTextRangePosWidth(eofToken, sourceFile.end, 0); + ts.setParent(eofToken, sourceFile); + return sourceFile; + }); +} + +/*@internal*/ +export function emitUsingBuildInfo(config: ts.ParsedCommandLine, host: EmitUsingBuildInfoHost, getCommandLine: (ref: ts.ProjectReference) => ts.ParsedCommandLine | undefined, customTransformers?: ts.CustomTransformers): EmitUsingBuildInfoResult { + const { buildInfoPath, jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath } = getOutputPathsForBundle(config.options, /*forceDtsPaths*/ false); + const buildInfoText = host.readFile(ts.Debug.checkDefined(buildInfoPath)); + if (!buildInfoText) + return buildInfoPath!; + const jsFileText = host.readFile(ts.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 = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath!, host.getCurrentDirectory())); + const ownPrependInput = ts.createInputFiles(jsFileText, declarationText!, sourceMapFilePath, sourceMapText, declarationMapPath, declarationMapText, jsFilePath, declarationFilePath, buildInfoPath, buildInfo, + /*onlyOwnText*/ true); + const outputFiles: ts.OutputFile[] = []; + const prependNodes = ts.createPrependNodes(config.projectReferences, getCommandLine, f => host.readFile(f)); + const sourceFilesForJsEmit = createSourceFilesFromBundleBuildInfo(buildInfo.bundle, buildInfoDirectory, host); + const emitHost: ts.EmitHost = { + getPrependNodes: ts.memoize(() => [...prependNodes, ownPrependInput]), + getCanonicalFileName: host.getCanonicalFileName, + getCommonSourceDirectory: () => ts.getNormalizedAbsolutePath(buildInfo.bundle!.commonSourceDirectory, buildInfoDirectory), + getCompilerOptions: () => config.options, + getCurrentDirectory: () => host.getCurrentDirectory(), + getNewLine: () => host.getNewLine(), + getSourceFile: ts.returnUndefined, + getSourceFileByPath: ts.returnUndefined, + getSourceFiles: () => sourceFilesForJsEmit, + getLibFileFromReference: ts.notImplemented, + isSourceFileFromExternalLibrary: ts.returnFalse, + getResolvedProjectReferenceToRedirect: ts.returnUndefined, + getProjectReferenceRedirect: ts.returnUndefined, + isSourceOfProjectReferenceRedirect: ts.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: + ts.Debug.fail(`Unexpected path: ${name}`); + } + outputFiles.push({ name, text, writeByteOrderMark }); + }, + isEmitBlocked: ts.returnFalse, + readFile: f => host.readFile(f), + fileExists: f => host.fileExists(f), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), + getProgramBuildInfo: ts.returnUndefined, + getSourceFileFromReference: ts.returnUndefined, + redirectTargetsMap: ts.createMultiMap(), + getFileIncludeReasons: ts.notImplemented, }; + emitFiles(notImplementedResolver, emitHost, + /*targetSourceFile*/ undefined, ts.getTransformers(config.options, customTransformers)); + return outputFiles; +} - /*@internal*/ - /** File that isnt present resulting in error or output files */ - export type EmitUsingBuildInfoResult = string | readonly ts.OutputFile[]; - - /*@internal*/ - export interface EmitUsingBuildInfoHost extends ts.ModuleResolutionHost { - getCurrentDirectory(): string; - getCanonicalFileName(fileName: string): string; - useCaseSensitiveFileNames(): boolean; - getNewLine(): string; - } - - function createSourceFilesFromBundleBuildInfo(bundle: ts.BundleBuildInfo, buildInfoDirectory: string, host: EmitUsingBuildInfoHost): readonly ts.SourceFile[] { - const jsBundle = ts.Debug.checkDefined(bundle.js); - const prologueMap = jsBundle.sources?.prologues && ts.arrayToMap(jsBundle.sources.prologues, prologueInfo => prologueInfo.file); - return bundle.sourceFiles.map((fileName, index) => { - const prologueInfo = prologueMap?.get(index); - const statements = prologueInfo?.directives.map(directive => { - const literal = ts.setTextRange(ts.factory.createStringLiteral(directive.expression.text), directive.expression); - const statement = ts.setTextRange(ts.factory.createExpressionStatement(literal), directive); - ts.setParent(literal, statement); - return statement; - }); - const eofToken = ts.factory.createToken(ts.SyntaxKind.EndOfFileToken); - const sourceFile = ts.factory.createSourceFile(statements ?? [], eofToken, ts.NodeFlags.None); - sourceFile.fileName = ts.getRelativePathFromDirectory(host.getCurrentDirectory(), ts.getNormalizedAbsolutePath(fileName, buildInfoDirectory), !host.useCaseSensitiveFileNames()); - sourceFile.text = prologueInfo?.text ?? ""; - ts.setTextRangePosWidth(sourceFile, 0, prologueInfo?.text.length ?? 0); - ts.setEachParent(sourceFile.statements, sourceFile); - ts.setTextRangePosWidth(eofToken, sourceFile.end, 0); - ts.setParent(eofToken, sourceFile); - return sourceFile; - }); +const enum PipelinePhase { + Notification, + Substitution, + Comments, + SourceMaps, + Emit +} +export function createPrinter(printerOptions: ts.PrinterOptions = {}, handlers: ts.PrintHandlers = {}): ts.Printer { + const { hasGlobalName, onEmitNode = ts.noEmitNotification, isEmitNotificationEnabled, substituteNode = ts.noEmitSubstitution, onBeforeEmitNode, onAfterEmitNode, onBeforeEmitNodeArray, onAfterEmitNodeArray, onBeforeEmitToken, onAfterEmitToken } = handlers; + + const extendedDiagnostics = !!printerOptions.extendedDiagnostics; + const newLine = ts.getNewLineCharacter(printerOptions); + const moduleKind = ts.getEmitModuleKind(printerOptions); + const bundledHelpers = new ts.Map(); + let currentSourceFile: ts.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.Set; // 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.Set[]; // Stack of TempFlags reserved in enclosing name generation scopes. + let reservedNames: ts.Set; // TempFlags to reserve in nested name generation scopes. + let preserveSourceNewlines = printerOptions.preserveSourceNewlines; // Can be overridden inside nodes with the `IgnoreSourceNewlines` emit flag. + let nextListElementPos: number | undefined; // See comment in `getLeadingLineTerminatorCount`. + + let writer: ts.EmitTextWriter; + let ownWriter: ts.EmitTextWriter; // Reusable `EmitTextWriter` for basic printing. + let write = writeBase; + let isOwnFileEmit: boolean; + const bundleFileInfo = printerOptions.writeBundleFileInfo ? { sections: [] } as ts.BundleFileInfo : undefined; + const relativeToBuildInfo = bundleFileInfo ? ts.Debug.checkDefined(printerOptions.relativeToBuildInfo) : undefined; + const recordInternalSection = printerOptions.recordInternalSection; + let sourceFileTextPos = 0; + let sourceFileTextKind: ts.BundleFileTextLikeKind = ts.BundleFileSectionKind.Text; + + // Source Maps + let sourceMapsDisabled = true; + let sourceMapGenerator: ts.SourceMapGenerator | undefined; + let sourceMapSource: ts.SourceMapSource; + let sourceMapSourceIndex = -1; + let mostRecentlyAddedSourceMapSource: ts.SourceMapSource; + let mostRecentlyAddedSourceMapSourceIndex = -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 lastSubstitution: ts.Node | undefined; + let currentParenthesizerRule: ((node: ts.Node) => ts.Node) | undefined; + const { enter: enterComment, exit: exitComment } = ts.performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment"); + const parenthesizer = ts.factory.parenthesizer; + const typeArgumentParenthesizerRuleSelector: OrdinalParentheizerRuleSelector = { + select: index => index === 0 ? parenthesizer.parenthesizeLeadingTypeArgument : undefined + }; + const emitBinaryExpression = createEmitBinaryExpression(); + + reset(); + return { + // public API + printNode, + printList, + printFile, + printBundle, + + // internal API + writeNode, + writeList, + writeFile, + writeBundle, + bundleFileInfo + }; + + function printNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile): string { + switch (hint) { + case ts.EmitHint.SourceFile: + ts.Debug.assert(ts.isSourceFile(node), "Expected a SourceFile node."); + break; + case ts.EmitHint.IdentifierName: + ts.Debug.assert(ts.isIdentifier(node), "Expected an Identifier node."); + break; + case ts.EmitHint.Expression: + ts.Debug.assert(ts.isExpression(node), "Expected an Expression node."); + break; + } + switch (node.kind) { + case ts.SyntaxKind.SourceFile: return printFile(node as ts.SourceFile); + case ts.SyntaxKind.Bundle: return printBundle(node as ts.Bundle); + case ts.SyntaxKind.UnparsedSource: return printUnparsedSource(node as ts.UnparsedSource); + } + writeNode(hint, node, sourceFile, beginPrint()); + return endPrint(); } - /*@internal*/ - export function emitUsingBuildInfo(config: ts.ParsedCommandLine, host: EmitUsingBuildInfoHost, getCommandLine: (ref: ts.ProjectReference) => ts.ParsedCommandLine | undefined, customTransformers?: ts.CustomTransformers): EmitUsingBuildInfoResult { - const { buildInfoPath, jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath } = getOutputPathsForBundle(config.options, /*forceDtsPaths*/ false); - const buildInfoText = host.readFile(ts.Debug.checkDefined(buildInfoPath)); - if (!buildInfoText) - return buildInfoPath!; - const jsFileText = host.readFile(ts.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 = ts.getDirectoryPath(ts.getNormalizedAbsolutePath(buildInfoPath!, host.getCurrentDirectory())); - const ownPrependInput = ts.createInputFiles(jsFileText, declarationText!, sourceMapFilePath, sourceMapText, declarationMapPath, declarationMapText, jsFilePath, declarationFilePath, buildInfoPath, buildInfo, - /*onlyOwnText*/ true); - const outputFiles: ts.OutputFile[] = []; - const prependNodes = ts.createPrependNodes(config.projectReferences, getCommandLine, f => host.readFile(f)); - const sourceFilesForJsEmit = createSourceFilesFromBundleBuildInfo(buildInfo.bundle, buildInfoDirectory, host); - const emitHost: ts.EmitHost = { - getPrependNodes: ts.memoize(() => [...prependNodes, ownPrependInput]), - getCanonicalFileName: host.getCanonicalFileName, - getCommonSourceDirectory: () => ts.getNormalizedAbsolutePath(buildInfo.bundle!.commonSourceDirectory, buildInfoDirectory), - getCompilerOptions: () => config.options, - getCurrentDirectory: () => host.getCurrentDirectory(), - getNewLine: () => host.getNewLine(), - getSourceFile: ts.returnUndefined, - getSourceFileByPath: ts.returnUndefined, - getSourceFiles: () => sourceFilesForJsEmit, - getLibFileFromReference: ts.notImplemented, - isSourceFileFromExternalLibrary: ts.returnFalse, - getResolvedProjectReferenceToRedirect: ts.returnUndefined, - getProjectReferenceRedirect: ts.returnUndefined, - isSourceOfProjectReferenceRedirect: ts.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: - ts.Debug.fail(`Unexpected path: ${name}`); - } - outputFiles.push({ name, text, writeByteOrderMark }); - }, - isEmitBlocked: ts.returnFalse, - readFile: f => host.readFile(f), - fileExists: f => host.fileExists(f), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), - getProgramBuildInfo: ts.returnUndefined, - getSourceFileFromReference: ts.returnUndefined, - redirectTargetsMap: ts.createMultiMap(), - getFileIncludeReasons: ts.notImplemented, - }; - emitFiles(notImplementedResolver, emitHost, - /*targetSourceFile*/ undefined, ts.getTransformers(config.options, customTransformers)); - return outputFiles; + function printList(format: ts.ListFormat, nodes: ts.NodeArray, sourceFile: ts.SourceFile) { + writeList(format, nodes, sourceFile, beginPrint()); + return endPrint(); } - const enum PipelinePhase { - Notification, - Substitution, - Comments, - SourceMaps, - Emit + function printBundle(bundle: ts.Bundle): string { + writeBundle(bundle, beginPrint(), /*sourceMapEmitter*/ undefined); + return endPrint(); } - export function createPrinter(printerOptions: ts.PrinterOptions = {}, handlers: ts.PrintHandlers = {}): ts.Printer { - const { hasGlobalName, onEmitNode = ts.noEmitNotification, isEmitNotificationEnabled, substituteNode = ts.noEmitSubstitution, onBeforeEmitNode, onAfterEmitNode, onBeforeEmitNodeArray, onAfterEmitNodeArray, onBeforeEmitToken, onAfterEmitToken } = handlers; - const extendedDiagnostics = !!printerOptions.extendedDiagnostics; - const newLine = ts.getNewLineCharacter(printerOptions); - const moduleKind = ts.getEmitModuleKind(printerOptions); - const bundledHelpers = new ts.Map(); - let currentSourceFile: ts.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.Set; // 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.Set[]; // Stack of TempFlags reserved in enclosing name generation scopes. - let reservedNames: ts.Set; // TempFlags to reserve in nested name generation scopes. - let preserveSourceNewlines = printerOptions.preserveSourceNewlines; // Can be overridden inside nodes with the `IgnoreSourceNewlines` emit flag. - let nextListElementPos: number | undefined; // See comment in `getLeadingLineTerminatorCount`. - - let writer: ts.EmitTextWriter; - let ownWriter: ts.EmitTextWriter; // Reusable `EmitTextWriter` for basic printing. - let write = writeBase; - let isOwnFileEmit: boolean; - const bundleFileInfo = printerOptions.writeBundleFileInfo ? { sections: [] } as ts.BundleFileInfo : undefined; - const relativeToBuildInfo = bundleFileInfo ? ts.Debug.checkDefined(printerOptions.relativeToBuildInfo) : undefined; - const recordInternalSection = printerOptions.recordInternalSection; - let sourceFileTextPos = 0; - let sourceFileTextKind: ts.BundleFileTextLikeKind = ts.BundleFileSectionKind.Text; - - // Source Maps - let sourceMapsDisabled = true; - let sourceMapGenerator: ts.SourceMapGenerator | undefined; - let sourceMapSource: ts.SourceMapSource; - let sourceMapSourceIndex = -1; - let mostRecentlyAddedSourceMapSource: ts.SourceMapSource; - let mostRecentlyAddedSourceMapSourceIndex = -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 lastSubstitution: ts.Node | undefined; - let currentParenthesizerRule: ((node: ts.Node) => ts.Node) | undefined; - const { enter: enterComment, exit: exitComment } = ts.performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment"); - const parenthesizer = ts.factory.parenthesizer; - const typeArgumentParenthesizerRuleSelector: OrdinalParentheizerRuleSelector = { - select: index => index === 0 ? parenthesizer.parenthesizeLeadingTypeArgument : undefined - }; - const emitBinaryExpression = createEmitBinaryExpression(); + function printFile(sourceFile: ts.SourceFile): string { + writeFile(sourceFile, beginPrint(), /*sourceMapEmitter*/ undefined); + return endPrint(); + } + + function printUnparsedSource(unparsed: ts.UnparsedSource): string { + writeUnparsedSource(unparsed, beginPrint()); + return endPrint(); + } + /** + * If `sourceFile` is `undefined`, `node` must be a synthesized `TypeNode`. + */ + function writeNode(hint: ts.EmitHint, node: ts.TypeNode, sourceFile: undefined, output: ts.EmitTextWriter): void; + function writeNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile, output: ts.EmitTextWriter): void; + function writeNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile | undefined, output: ts.EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + print(hint, node, sourceFile); reset(); - return { - // public API - printNode, - printList, - printFile, - printBundle, - - // internal API - writeNode, - writeList, - writeFile, - writeBundle, - bundleFileInfo - }; + writer = previousWriter; + } - function printNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile): string { - switch (hint) { - case ts.EmitHint.SourceFile: - ts.Debug.assert(ts.isSourceFile(node), "Expected a SourceFile node."); - break; - case ts.EmitHint.IdentifierName: - ts.Debug.assert(ts.isIdentifier(node), "Expected an Identifier node."); - break; - case ts.EmitHint.Expression: - ts.Debug.assert(ts.isExpression(node), "Expected an Expression node."); - break; - } - switch (node.kind) { - case ts.SyntaxKind.SourceFile: return printFile(node as ts.SourceFile); - case ts.SyntaxKind.Bundle: return printBundle(node as ts.Bundle); - case ts.SyntaxKind.UnparsedSource: return printUnparsedSource(node as ts.UnparsedSource); - } - writeNode(hint, node, sourceFile, beginPrint()); - return endPrint(); + function writeList(format: ts.ListFormat, nodes: ts.NodeArray, sourceFile: ts.SourceFile | undefined, output: ts.EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + if (sourceFile) { + setSourceFile(sourceFile); } + emitList(/*parentNode*/ undefined, nodes, format); + reset(); + writer = previousWriter; + } - function printList(format: ts.ListFormat, nodes: ts.NodeArray, sourceFile: ts.SourceFile) { - writeList(format, nodes, sourceFile, beginPrint()); - return endPrint(); - } + function getTextPosWithWriteLine() { + return writer.getTextPosWithWriteLine ? writer.getTextPosWithWriteLine() : writer.getTextPos(); + } - function printBundle(bundle: ts.Bundle): string { - writeBundle(bundle, beginPrint(), /*sourceMapEmitter*/ undefined); - return endPrint(); + function updateOrPushBundleFileTextLike(pos: number, end: number, kind: ts.BundleFileTextLikeKind) { + const last = ts.lastOrUndefined(bundleFileInfo!.sections); + if (last && last.kind === kind) { + last.end = end; } - - function printFile(sourceFile: ts.SourceFile): string { - writeFile(sourceFile, beginPrint(), /*sourceMapEmitter*/ undefined); - return endPrint(); + else { + bundleFileInfo!.sections.push({ pos, end, kind }); } + } - function printUnparsedSource(unparsed: ts.UnparsedSource): string { - writeUnparsedSource(unparsed, beginPrint()); - return endPrint(); + function recordBundleFileInternalSectionStart(node: ts.Node) { + if (recordInternalSection && + bundleFileInfo && + currentSourceFile && + (ts.isDeclaration(node) || ts.isVariableStatement(node)) && + ts.isInternalDeclaration(node, currentSourceFile) && + sourceFileTextKind !== ts.BundleFileSectionKind.Internal) { + const prevSourceFileTextKind = sourceFileTextKind; + recordBundleFileTextLikeSection(writer.getTextPos()); + sourceFileTextPos = getTextPosWithWriteLine(); + sourceFileTextKind = ts.BundleFileSectionKind.Internal; + return prevSourceFileTextKind; } + return undefined; + } - /** - * If `sourceFile` is `undefined`, `node` must be a synthesized `TypeNode`. - */ - function writeNode(hint: ts.EmitHint, node: ts.TypeNode, sourceFile: undefined, output: ts.EmitTextWriter): void; - function writeNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile, output: ts.EmitTextWriter): void; - function writeNode(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile | undefined, output: ts.EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - print(hint, node, sourceFile); - reset(); - writer = previousWriter; + function recordBundleFileInternalSectionEnd(prevSourceFileTextKind: ReturnType) { + if (prevSourceFileTextKind) { + recordBundleFileTextLikeSection(writer.getTextPos()); + sourceFileTextPos = getTextPosWithWriteLine(); + sourceFileTextKind = prevSourceFileTextKind; } + } - function writeList(format: ts.ListFormat, nodes: ts.NodeArray, sourceFile: ts.SourceFile | undefined, output: ts.EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - if (sourceFile) { - setSourceFile(sourceFile); - } - emitList(/*parentNode*/ undefined, nodes, format); - reset(); - writer = previousWriter; + function recordBundleFileTextLikeSection(end: number) { + if (sourceFileTextPos < end) { + updateOrPushBundleFileTextLike(sourceFileTextPos, end, sourceFileTextKind); + return true; } + return false; + } - function getTextPosWithWriteLine() { - return writer.getTextPosWithWriteLine ? writer.getTextPosWithWriteLine() : writer.getTextPos(); - } + function writeBundle(bundle: ts.Bundle, output: ts.EmitTextWriter, sourceMapGenerator: ts.SourceMapGenerator | undefined) { + isOwnFileEmit = false; + const previousWriter = writer; + setWriter(output, sourceMapGenerator); + emitShebangIfNeeded(bundle); + emitPrologueDirectivesIfNeeded(bundle); + emitHelpers(bundle); + emitSyntheticTripleSlashReferencesIfNeeded(bundle); - function updateOrPushBundleFileTextLike(pos: number, end: number, kind: ts.BundleFileTextLikeKind) { - const last = ts.lastOrUndefined(bundleFileInfo!.sections); - if (last && last.kind === kind) { - last.end = end; - } - else { - bundleFileInfo!.sections.push({ pos, end, kind }); + for (const prepend of bundle.prepends) { + writeLine(); + const pos = writer.getTextPos(); + const savedSections = bundleFileInfo && bundleFileInfo.sections; + if (savedSections) + bundleFileInfo.sections = []; + print(ts.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 => ts.Debug.assert(ts.isBundleFileTextLike(section))); + bundleFileInfo.sections.push({ + pos, + end: writer.getTextPos(), + kind: ts.BundleFileSectionKind.Prepend, + data: relativeToBuildInfo!((prepend as ts.UnparsedSource).fileName), + texts: newSections as ts.BundleFileTextLike[] + }); + } } } - function recordBundleFileInternalSectionStart(node: ts.Node) { - if (recordInternalSection && - bundleFileInfo && - currentSourceFile && - (ts.isDeclaration(node) || ts.isVariableStatement(node)) && - ts.isInternalDeclaration(node, currentSourceFile) && - sourceFileTextKind !== ts.BundleFileSectionKind.Internal) { - const prevSourceFileTextKind = sourceFileTextKind; - recordBundleFileTextLikeSection(writer.getTextPos()); - sourceFileTextPos = getTextPosWithWriteLine(); - sourceFileTextKind = ts.BundleFileSectionKind.Internal; - return prevSourceFileTextKind; - } - return undefined; + sourceFileTextPos = getTextPosWithWriteLine(); + for (const sourceFile of bundle.sourceFiles) { + print(ts.EmitHint.SourceFile, sourceFile, sourceFile); } + 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; + } - function recordBundleFileInternalSectionEnd(prevSourceFileTextKind: ReturnType) { - if (prevSourceFileTextKind) { - recordBundleFileTextLikeSection(writer.getTextPos()); - sourceFileTextPos = getTextPosWithWriteLine(); - sourceFileTextKind = prevSourceFileTextKind; + // Store helpes + const helpers = getHelpersFromBundledSourceFiles(bundle); + if (helpers) { + if (!bundleFileInfo.sources) + bundleFileInfo.sources = {}; + bundleFileInfo.sources.helpers = helpers; + } } } - function recordBundleFileTextLikeSection(end: number) { - if (sourceFileTextPos < end) { - updateOrPushBundleFileTextLike(sourceFileTextPos, end, sourceFileTextKind); - return true; - } - return false; - } + reset(); + writer = previousWriter; + } - function writeBundle(bundle: ts.Bundle, output: ts.EmitTextWriter, sourceMapGenerator: ts.SourceMapGenerator | undefined) { - isOwnFileEmit = false; - const previousWriter = writer; - setWriter(output, sourceMapGenerator); - emitShebangIfNeeded(bundle); - emitPrologueDirectivesIfNeeded(bundle); - emitHelpers(bundle); - emitSyntheticTripleSlashReferencesIfNeeded(bundle); + function writeUnparsedSource(unparsed: ts.UnparsedSource, output: ts.EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + print(ts.EmitHint.Unspecified, unparsed, /*sourceFile*/ undefined); + reset(); + writer = previousWriter; + } - for (const prepend of bundle.prepends) { - writeLine(); - const pos = writer.getTextPos(); - const savedSections = bundleFileInfo && bundleFileInfo.sections; - if (savedSections) - bundleFileInfo.sections = []; - print(ts.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 => ts.Debug.assert(ts.isBundleFileTextLike(section))); - bundleFileInfo.sections.push({ - pos, - end: writer.getTextPos(), - kind: ts.BundleFileSectionKind.Prepend, - data: relativeToBuildInfo!((prepend as ts.UnparsedSource).fileName), - texts: newSections as ts.BundleFileTextLike[] - }); - } - } - } + function writeFile(sourceFile: ts.SourceFile, output: ts.EmitTextWriter, sourceMapGenerator: ts.SourceMapGenerator | undefined) { + isOwnFileEmit = true; + const previousWriter = writer; + setWriter(output, sourceMapGenerator); + emitShebangIfNeeded(sourceFile); + emitPrologueDirectivesIfNeeded(sourceFile); + print(ts.EmitHint.SourceFile, sourceFile, sourceFile); + reset(); + writer = previousWriter; + } - sourceFileTextPos = getTextPosWithWriteLine(); - for (const sourceFile of bundle.sourceFiles) { - print(ts.EmitHint.SourceFile, sourceFile, sourceFile); - } - 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; - } + function beginPrint() { + return ownWriter || (ownWriter = ts.createTextWriter(newLine)); + } - // Store helpes - const helpers = getHelpersFromBundledSourceFiles(bundle); - if (helpers) { - if (!bundleFileInfo.sources) - bundleFileInfo.sources = {}; - bundleFileInfo.sources.helpers = helpers; - } - } - } + function endPrint() { + const text = ownWriter.getText(); + ownWriter.clear(); + return text; + } - reset(); - writer = previousWriter; + function print(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile | undefined) { + if (sourceFile) { + setSourceFile(sourceFile); } - function writeUnparsedSource(unparsed: ts.UnparsedSource, output: ts.EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - print(ts.EmitHint.Unspecified, unparsed, /*sourceFile*/ undefined); - reset(); - writer = previousWriter; - } + pipelineEmit(hint, node, /*parenthesizerRule*/ undefined); + } - function writeFile(sourceFile: ts.SourceFile, output: ts.EmitTextWriter, sourceMapGenerator: ts.SourceMapGenerator | undefined) { - isOwnFileEmit = true; - const previousWriter = writer; - setWriter(output, sourceMapGenerator); - emitShebangIfNeeded(sourceFile); - emitPrologueDirectivesIfNeeded(sourceFile); - print(ts.EmitHint.SourceFile, sourceFile, sourceFile); - reset(); - writer = previousWriter; + function setSourceFile(sourceFile: ts.SourceFile | undefined) { + currentSourceFile = sourceFile; + currentLineMap = undefined; + detachedCommentsInfo = undefined; + if (sourceFile) { + setSourceMapSource(sourceFile); } + } - function beginPrint() { - return ownWriter || (ownWriter = ts.createTextWriter(newLine)); + function setWriter(_writer: ts.EmitTextWriter | undefined, _sourceMapGenerator: ts.SourceMapGenerator | undefined) { + if (_writer && printerOptions.omitTrailingSemicolon) { + _writer = ts.getTrailingSemicolonDeferringWriter(_writer); } - function endPrint() { - const text = ownWriter.getText(); - ownWriter.clear(); - return text; - } + writer = _writer!; // TODO: GH#18217 + sourceMapGenerator = _sourceMapGenerator; + sourceMapsDisabled = !writer || !sourceMapGenerator; + } - function print(hint: ts.EmitHint, node: ts.Node, sourceFile: ts.SourceFile | undefined) { - if (sourceFile) { - setSourceFile(sourceFile); - } + function reset() { + nodeIdToGeneratedName = []; + autoGeneratedIdToGeneratedName = []; + generatedNames = new ts.Set(); + tempFlagsStack = []; + tempFlags = TempFlags.Auto; + reservedNamesStack = []; + currentSourceFile = undefined; + currentLineMap = undefined; + detachedCommentsInfo = undefined; + setWriter(/*output*/ undefined, /*_sourceMapGenerator*/ undefined); + } - pipelineEmit(hint, node, /*parenthesizerRule*/ undefined); - } + function getCurrentLineMap() { + return currentLineMap || (currentLineMap = ts.getLineStarts(ts.Debug.checkDefined(currentSourceFile))); + } - function setSourceFile(sourceFile: ts.SourceFile | undefined) { - currentSourceFile = sourceFile; - currentLineMap = undefined; - detachedCommentsInfo = undefined; - if (sourceFile) { - setSourceMapSource(sourceFile); - } + function emit(node: ts.Node, parenthesizerRule?: (node: ts.Node) => ts.Node): void; + function emit(node: ts.Node | undefined, parenthesizerRule?: (node: ts.Node) => ts.Node): void; + function emit(node: ts.Node | undefined, parenthesizerRule?: (node: ts.Node) => ts.Node) { + if (node === undefined) + return; + const prevSourceFileTextKind = recordBundleFileInternalSectionStart(node); + pipelineEmit(ts.EmitHint.Unspecified, node, parenthesizerRule); + recordBundleFileInternalSectionEnd(prevSourceFileTextKind); + } + + function emitIdentifierName(node: ts.Identifier): void; + function emitIdentifierName(node: ts.Identifier | undefined): void; + function emitIdentifierName(node: ts.Identifier | undefined) { + if (node === undefined) + return; + pipelineEmit(ts.EmitHint.IdentifierName, node, /*parenthesizerRule*/ undefined); + } + function emitExpression(node: ts.Expression, parenthesizerRule?: (node: ts.Expression) => ts.Expression): void; + function emitExpression(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression): void; + function emitExpression(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { + if (node === undefined) + return; + pipelineEmit(ts.EmitHint.Expression, node, parenthesizerRule); + } + function emitJsxAttributeValue(node: ts.StringLiteral | ts.JsxExpression): void { + pipelineEmit(ts.isStringLiteral(node) ? ts.EmitHint.JsxAttributeValue : ts.EmitHint.Unspecified, node); + } + function beforeEmitNode(node: ts.Node) { + if (preserveSourceNewlines && (ts.getEmitFlags(node) & ts.EmitFlags.IgnoreSourceNewlines)) { + preserveSourceNewlines = false; } + } - function setWriter(_writer: ts.EmitTextWriter | undefined, _sourceMapGenerator: ts.SourceMapGenerator | undefined) { - if (_writer && printerOptions.omitTrailingSemicolon) { - _writer = ts.getTrailingSemicolonDeferringWriter(_writer); - } + function afterEmitNode(savedPreserveSourceNewlines: boolean | undefined) { + preserveSourceNewlines = savedPreserveSourceNewlines; + } - writer = _writer!; // TODO: GH#18217 - sourceMapGenerator = _sourceMapGenerator; - sourceMapsDisabled = !writer || !sourceMapGenerator; - } + function pipelineEmit(emitHint: ts.EmitHint, node: ts.Node, parenthesizerRule?: (node: ts.Node) => ts.Node) { + currentParenthesizerRule = parenthesizerRule; + const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node); + pipelinePhase(emitHint, node); + currentParenthesizerRule = undefined; + } - function reset() { - nodeIdToGeneratedName = []; - autoGeneratedIdToGeneratedName = []; - generatedNames = new ts.Set(); - tempFlagsStack = []; - tempFlags = TempFlags.Auto; - reservedNamesStack = []; - currentSourceFile = undefined; - currentLineMap = undefined; - detachedCommentsInfo = undefined; - setWriter(/*output*/ undefined, /*_sourceMapGenerator*/ undefined); - } + function shouldEmitComments(node: ts.Node) { + return !commentsDisabled && !ts.isSourceFile(node); + } - function getCurrentLineMap() { - return currentLineMap || (currentLineMap = ts.getLineStarts(ts.Debug.checkDefined(currentSourceFile))); - } + function shouldEmitSourceMaps(node: ts.Node) { + return !sourceMapsDisabled && + !ts.isSourceFile(node) && + !ts.isInJsonFile(node) && + !ts.isUnparsedSource(node) && + !ts.isUnparsedPrepend(node); + } - function emit(node: ts.Node, parenthesizerRule?: (node: ts.Node) => ts.Node): void; - function emit(node: ts.Node | undefined, parenthesizerRule?: (node: ts.Node) => ts.Node): void; - function emit(node: ts.Node | undefined, parenthesizerRule?: (node: ts.Node) => ts.Node) { - if (node === undefined) - return; - const prevSourceFileTextKind = recordBundleFileInternalSectionStart(node); - pipelineEmit(ts.EmitHint.Unspecified, node, parenthesizerRule); - recordBundleFileInternalSectionEnd(prevSourceFileTextKind); + function getPipelinePhase(phase: PipelinePhase, emitHint: ts.EmitHint, node: ts.Node) { + switch (phase) { + case PipelinePhase.Notification: + if (onEmitNode !== ts.noEmitNotification && (!isEmitNotificationEnabled || isEmitNotificationEnabled(node))) { + return pipelineEmitWithNotification; + } + // falls through + case PipelinePhase.Substitution: + if (substituteNode !== ts.noEmitSubstitution && (lastSubstitution = substituteNode(emitHint, node) || node) !== node) { + if (currentParenthesizerRule) { + lastSubstitution = currentParenthesizerRule(lastSubstitution); + } + return pipelineEmitWithSubstitution; + } + // falls through + case PipelinePhase.Comments: + if (shouldEmitComments(node)) { + return pipelineEmitWithComments; + } + // falls through + case PipelinePhase.SourceMaps: + if (shouldEmitSourceMaps(node)) { + return pipelineEmitWithSourceMaps; + } + // falls through + case PipelinePhase.Emit: + return pipelineEmitWithHint; + default: + return ts.Debug.assertNever(phase); } + } - function emitIdentifierName(node: ts.Identifier): void; - function emitIdentifierName(node: ts.Identifier | undefined): void; - function emitIdentifierName(node: ts.Identifier | undefined) { - if (node === undefined) - return; - pipelineEmit(ts.EmitHint.IdentifierName, node, /*parenthesizerRule*/ undefined); - } - function emitExpression(node: ts.Expression, parenthesizerRule?: (node: ts.Expression) => ts.Expression): void; - function emitExpression(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression): void; - function emitExpression(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { - if (node === undefined) - return; - pipelineEmit(ts.EmitHint.Expression, node, parenthesizerRule); - } - function emitJsxAttributeValue(node: ts.StringLiteral | ts.JsxExpression): void { - pipelineEmit(ts.isStringLiteral(node) ? ts.EmitHint.JsxAttributeValue : ts.EmitHint.Unspecified, node); + function getNextPipelinePhase(currentPhase: PipelinePhase, emitHint: ts.EmitHint, node: ts.Node) { + return getPipelinePhase(currentPhase + 1, emitHint, node); + } + + function pipelineEmitWithNotification(hint: ts.EmitHint, node: ts.Node) { + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, hint, node); + onEmitNode(hint, node, pipelinePhase); + } + + function pipelineEmitWithHint(hint: ts.EmitHint, node: ts.Node): void { + onBeforeEmitNode?.(node); + if (preserveSourceNewlines) { + const savedPreserveSourceNewlines = preserveSourceNewlines; + beforeEmitNode(node); + pipelineEmitWithHintWorker(hint, node); + afterEmitNode(savedPreserveSourceNewlines); } - function beforeEmitNode(node: ts.Node) { - if (preserveSourceNewlines && (ts.getEmitFlags(node) & ts.EmitFlags.IgnoreSourceNewlines)) { - preserveSourceNewlines = false; - } + else { + pipelineEmitWithHintWorker(hint, node); } + onAfterEmitNode?.(node); + // clear the parenthesizer rule as we ascend + currentParenthesizerRule = undefined; + } - function afterEmitNode(savedPreserveSourceNewlines: boolean | undefined) { - preserveSourceNewlines = savedPreserveSourceNewlines; - } + function pipelineEmitWithHintWorker(hint: ts.EmitHint, node: ts.Node, allowSnippets = true): void { + if (allowSnippets) { + const snippet = ts.getSnippetElement(node); + if (snippet) { + return emitSnippetNode(hint, node, snippet); + } + } + if (hint === ts.EmitHint.SourceFile) + return emitSourceFile(ts.cast(node, ts.isSourceFile)); + if (hint === ts.EmitHint.IdentifierName) + return emitIdentifier(ts.cast(node, ts.isIdentifier)); + if (hint === ts.EmitHint.JsxAttributeValue) + return emitLiteral(ts.cast(node, ts.isStringLiteral), /*jsxAttributeEscape*/ true); + if (hint === ts.EmitHint.MappedTypeParameter) + return emitMappedTypeParameter(ts.cast(node, ts.isTypeParameterDeclaration)); + if (hint === ts.EmitHint.EmbeddedStatement) { + ts.Debug.assertNode(node, ts.isEmptyStatement); + return emitEmptyStatement(/*isEmbeddedStatement*/ true); + } + if (hint === ts.EmitHint.Unspecified) { + switch (node.kind) { + // Pseudo-literals + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateMiddle: + case ts.SyntaxKind.TemplateTail: + return emitLiteral(node as ts.LiteralExpression, /*jsxAttributeEscape*/ false); - function pipelineEmit(emitHint: ts.EmitHint, node: ts.Node, parenthesizerRule?: (node: ts.Node) => ts.Node) { - currentParenthesizerRule = parenthesizerRule; - const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node); - pipelinePhase(emitHint, node); - currentParenthesizerRule = undefined; - } + // Identifiers + case ts.SyntaxKind.Identifier: + return emitIdentifier(node as ts.Identifier); - function shouldEmitComments(node: ts.Node) { - return !commentsDisabled && !ts.isSourceFile(node); - } + // PrivateIdentifiers + case ts.SyntaxKind.PrivateIdentifier: + return emitPrivateIdentifier(node as ts.PrivateIdentifier); - function shouldEmitSourceMaps(node: ts.Node) { - return !sourceMapsDisabled && - !ts.isSourceFile(node) && - !ts.isInJsonFile(node) && - !ts.isUnparsedSource(node) && - !ts.isUnparsedPrepend(node); - } + // Parse tree nodes + // Names + case ts.SyntaxKind.QualifiedName: + return emitQualifiedName(node as ts.QualifiedName); + case ts.SyntaxKind.ComputedPropertyName: + return emitComputedPropertyName(node as ts.ComputedPropertyName); - function getPipelinePhase(phase: PipelinePhase, emitHint: ts.EmitHint, node: ts.Node) { - switch (phase) { - case PipelinePhase.Notification: - if (onEmitNode !== ts.noEmitNotification && (!isEmitNotificationEnabled || isEmitNotificationEnabled(node))) { - return pipelineEmitWithNotification; - } - // falls through - case PipelinePhase.Substitution: - if (substituteNode !== ts.noEmitSubstitution && (lastSubstitution = substituteNode(emitHint, node) || node) !== node) { - if (currentParenthesizerRule) { - lastSubstitution = currentParenthesizerRule(lastSubstitution); - } - return pipelineEmitWithSubstitution; - } - // falls through - case PipelinePhase.Comments: - if (shouldEmitComments(node)) { - return pipelineEmitWithComments; - } - // falls through - case PipelinePhase.SourceMaps: - if (shouldEmitSourceMaps(node)) { - return pipelineEmitWithSourceMaps; - } - // falls through - case PipelinePhase.Emit: - return pipelineEmitWithHint; - default: - return ts.Debug.assertNever(phase); - } - } + // Signature elements + case ts.SyntaxKind.TypeParameter: + return emitTypeParameter(node as ts.TypeParameterDeclaration); + case ts.SyntaxKind.Parameter: + return emitParameter(node as ts.ParameterDeclaration); + case ts.SyntaxKind.Decorator: + return emitDecorator(node as ts.Decorator); - function getNextPipelinePhase(currentPhase: PipelinePhase, emitHint: ts.EmitHint, node: ts.Node) { - return getPipelinePhase(currentPhase + 1, emitHint, node); - } + // Type members + case ts.SyntaxKind.PropertySignature: + return emitPropertySignature(node as ts.PropertySignature); + case ts.SyntaxKind.PropertyDeclaration: + return emitPropertyDeclaration(node as ts.PropertyDeclaration); + case ts.SyntaxKind.MethodSignature: + return emitMethodSignature(node as ts.MethodSignature); + case ts.SyntaxKind.MethodDeclaration: + return emitMethodDeclaration(node as ts.MethodDeclaration); + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return emitClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); + case ts.SyntaxKind.Constructor: + return emitConstructor(node as ts.ConstructorDeclaration); + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return emitAccessorDeclaration(node as ts.AccessorDeclaration); + case ts.SyntaxKind.CallSignature: + return emitCallSignature(node as ts.CallSignatureDeclaration); + case ts.SyntaxKind.ConstructSignature: + return emitConstructSignature(node as ts.ConstructSignatureDeclaration); + case ts.SyntaxKind.IndexSignature: + return emitIndexSignature(node as ts.IndexSignatureDeclaration); + + // Types + case ts.SyntaxKind.TypePredicate: + return emitTypePredicate(node as ts.TypePredicateNode); + case ts.SyntaxKind.TypeReference: + return emitTypeReference(node as ts.TypeReferenceNode); + case ts.SyntaxKind.FunctionType: + return emitFunctionType(node as ts.FunctionTypeNode); + case ts.SyntaxKind.ConstructorType: + return emitConstructorType(node as ts.ConstructorTypeNode); + case ts.SyntaxKind.TypeQuery: + return emitTypeQuery(node as ts.TypeQueryNode); + case ts.SyntaxKind.TypeLiteral: + return emitTypeLiteral(node as ts.TypeLiteralNode); + case ts.SyntaxKind.ArrayType: + return emitArrayType(node as ts.ArrayTypeNode); + case ts.SyntaxKind.TupleType: + return emitTupleType(node as ts.TupleTypeNode); + case ts.SyntaxKind.OptionalType: + return emitOptionalType(node as ts.OptionalTypeNode); + // SyntaxKind.RestType is handled below + case ts.SyntaxKind.UnionType: + return emitUnionType(node as ts.UnionTypeNode); + case ts.SyntaxKind.IntersectionType: + return emitIntersectionType(node as ts.IntersectionTypeNode); + case ts.SyntaxKind.ConditionalType: + return emitConditionalType(node as ts.ConditionalTypeNode); + case ts.SyntaxKind.InferType: + return emitInferType(node as ts.InferTypeNode); + case ts.SyntaxKind.ParenthesizedType: + return emitParenthesizedType(node as ts.ParenthesizedTypeNode); + case ts.SyntaxKind.ExpressionWithTypeArguments: + return emitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); + case ts.SyntaxKind.ThisType: + return emitThisType(); + case ts.SyntaxKind.TypeOperator: + return emitTypeOperator(node as ts.TypeOperatorNode); + case ts.SyntaxKind.IndexedAccessType: + return emitIndexedAccessType(node as ts.IndexedAccessTypeNode); + case ts.SyntaxKind.MappedType: + return emitMappedType(node as ts.MappedTypeNode); + case ts.SyntaxKind.LiteralType: + return emitLiteralType(node as ts.LiteralTypeNode); + case ts.SyntaxKind.NamedTupleMember: + return emitNamedTupleMember(node as ts.NamedTupleMember); + case ts.SyntaxKind.TemplateLiteralType: + return emitTemplateType(node as ts.TemplateLiteralTypeNode); + case ts.SyntaxKind.TemplateLiteralTypeSpan: + return emitTemplateTypeSpan(node as ts.TemplateLiteralTypeSpan); + case ts.SyntaxKind.ImportType: + return emitImportTypeNode(node as ts.ImportTypeNode); + + // Binding patterns + case ts.SyntaxKind.ObjectBindingPattern: + return emitObjectBindingPattern(node as ts.ObjectBindingPattern); + case ts.SyntaxKind.ArrayBindingPattern: + return emitArrayBindingPattern(node as ts.ArrayBindingPattern); + case ts.SyntaxKind.BindingElement: + return emitBindingElement(node as ts.BindingElement); - function pipelineEmitWithNotification(hint: ts.EmitHint, node: ts.Node) { - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, hint, node); - onEmitNode(hint, node, pipelinePhase); - } + // Misc + case ts.SyntaxKind.TemplateSpan: + return emitTemplateSpan(node as ts.TemplateSpan); + case ts.SyntaxKind.SemicolonClassElement: + return emitSemicolonClassElement(); - function pipelineEmitWithHint(hint: ts.EmitHint, node: ts.Node): void { - onBeforeEmitNode?.(node); - if (preserveSourceNewlines) { - const savedPreserveSourceNewlines = preserveSourceNewlines; - beforeEmitNode(node); - pipelineEmitWithHintWorker(hint, node); - afterEmitNode(savedPreserveSourceNewlines); - } - else { - pipelineEmitWithHintWorker(hint, node); - } - onAfterEmitNode?.(node); - // clear the parenthesizer rule as we ascend - currentParenthesizerRule = undefined; - } + // Statements + case ts.SyntaxKind.Block: + return emitBlock(node as ts.Block); + case ts.SyntaxKind.VariableStatement: + return emitVariableStatement(node as ts.VariableStatement); + case ts.SyntaxKind.EmptyStatement: + return emitEmptyStatement(/*isEmbeddedStatement*/ false); + case ts.SyntaxKind.ExpressionStatement: + return emitExpressionStatement(node as ts.ExpressionStatement); + case ts.SyntaxKind.IfStatement: + return emitIfStatement(node as ts.IfStatement); + case ts.SyntaxKind.DoStatement: + return emitDoStatement(node as ts.DoStatement); + case ts.SyntaxKind.WhileStatement: + return emitWhileStatement(node as ts.WhileStatement); + case ts.SyntaxKind.ForStatement: + return emitForStatement(node as ts.ForStatement); + case ts.SyntaxKind.ForInStatement: + return emitForInStatement(node as ts.ForInStatement); + case ts.SyntaxKind.ForOfStatement: + return emitForOfStatement(node as ts.ForOfStatement); + case ts.SyntaxKind.ContinueStatement: + return emitContinueStatement(node as ts.ContinueStatement); + case ts.SyntaxKind.BreakStatement: + return emitBreakStatement(node as ts.BreakStatement); + case ts.SyntaxKind.ReturnStatement: + return emitReturnStatement(node as ts.ReturnStatement); + case ts.SyntaxKind.WithStatement: + return emitWithStatement(node as ts.WithStatement); + case ts.SyntaxKind.SwitchStatement: + return emitSwitchStatement(node as ts.SwitchStatement); + case ts.SyntaxKind.LabeledStatement: + return emitLabeledStatement(node as ts.LabeledStatement); + case ts.SyntaxKind.ThrowStatement: + return emitThrowStatement(node as ts.ThrowStatement); + case ts.SyntaxKind.TryStatement: + return emitTryStatement(node as ts.TryStatement); + case ts.SyntaxKind.DebuggerStatement: + return emitDebuggerStatement(node as ts.DebuggerStatement); - function pipelineEmitWithHintWorker(hint: ts.EmitHint, node: ts.Node, allowSnippets = true): void { - if (allowSnippets) { - const snippet = ts.getSnippetElement(node); - if (snippet) { - return emitSnippetNode(hint, node, snippet); - } - } - if (hint === ts.EmitHint.SourceFile) - return emitSourceFile(ts.cast(node, ts.isSourceFile)); - if (hint === ts.EmitHint.IdentifierName) - return emitIdentifier(ts.cast(node, ts.isIdentifier)); - if (hint === ts.EmitHint.JsxAttributeValue) - return emitLiteral(ts.cast(node, ts.isStringLiteral), /*jsxAttributeEscape*/ true); - if (hint === ts.EmitHint.MappedTypeParameter) - return emitMappedTypeParameter(ts.cast(node, ts.isTypeParameterDeclaration)); - if (hint === ts.EmitHint.EmbeddedStatement) { - ts.Debug.assertNode(node, ts.isEmptyStatement); - return emitEmptyStatement(/*isEmbeddedStatement*/ true); - } - if (hint === ts.EmitHint.Unspecified) { - switch (node.kind) { - // Pseudo-literals - case ts.SyntaxKind.TemplateHead: - case ts.SyntaxKind.TemplateMiddle: - case ts.SyntaxKind.TemplateTail: - return emitLiteral(node as ts.LiteralExpression, /*jsxAttributeEscape*/ false); - - // Identifiers - case ts.SyntaxKind.Identifier: - return emitIdentifier(node as ts.Identifier); - - // PrivateIdentifiers - case ts.SyntaxKind.PrivateIdentifier: - return emitPrivateIdentifier(node as ts.PrivateIdentifier); - - // Parse tree nodes - // Names - case ts.SyntaxKind.QualifiedName: - return emitQualifiedName(node as ts.QualifiedName); - case ts.SyntaxKind.ComputedPropertyName: - return emitComputedPropertyName(node as ts.ComputedPropertyName); - - // Signature elements - case ts.SyntaxKind.TypeParameter: - return emitTypeParameter(node as ts.TypeParameterDeclaration); - case ts.SyntaxKind.Parameter: - return emitParameter(node as ts.ParameterDeclaration); - case ts.SyntaxKind.Decorator: - return emitDecorator(node as ts.Decorator); - - // Type members - case ts.SyntaxKind.PropertySignature: - return emitPropertySignature(node as ts.PropertySignature); - case ts.SyntaxKind.PropertyDeclaration: - return emitPropertyDeclaration(node as ts.PropertyDeclaration); - case ts.SyntaxKind.MethodSignature: - return emitMethodSignature(node as ts.MethodSignature); - case ts.SyntaxKind.MethodDeclaration: - return emitMethodDeclaration(node as ts.MethodDeclaration); - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return emitClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); - case ts.SyntaxKind.Constructor: - return emitConstructor(node as ts.ConstructorDeclaration); - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return emitAccessorDeclaration(node as ts.AccessorDeclaration); - case ts.SyntaxKind.CallSignature: - return emitCallSignature(node as ts.CallSignatureDeclaration); - case ts.SyntaxKind.ConstructSignature: - return emitConstructSignature(node as ts.ConstructSignatureDeclaration); - case ts.SyntaxKind.IndexSignature: - return emitIndexSignature(node as ts.IndexSignatureDeclaration); - - // Types - case ts.SyntaxKind.TypePredicate: - return emitTypePredicate(node as ts.TypePredicateNode); - case ts.SyntaxKind.TypeReference: - return emitTypeReference(node as ts.TypeReferenceNode); - case ts.SyntaxKind.FunctionType: - return emitFunctionType(node as ts.FunctionTypeNode); - case ts.SyntaxKind.ConstructorType: - return emitConstructorType(node as ts.ConstructorTypeNode); - case ts.SyntaxKind.TypeQuery: - return emitTypeQuery(node as ts.TypeQueryNode); - case ts.SyntaxKind.TypeLiteral: - return emitTypeLiteral(node as ts.TypeLiteralNode); - case ts.SyntaxKind.ArrayType: - return emitArrayType(node as ts.ArrayTypeNode); - case ts.SyntaxKind.TupleType: - return emitTupleType(node as ts.TupleTypeNode); - case ts.SyntaxKind.OptionalType: - return emitOptionalType(node as ts.OptionalTypeNode); - // SyntaxKind.RestType is handled below - case ts.SyntaxKind.UnionType: - return emitUnionType(node as ts.UnionTypeNode); - case ts.SyntaxKind.IntersectionType: - return emitIntersectionType(node as ts.IntersectionTypeNode); - case ts.SyntaxKind.ConditionalType: - return emitConditionalType(node as ts.ConditionalTypeNode); - case ts.SyntaxKind.InferType: - return emitInferType(node as ts.InferTypeNode); - case ts.SyntaxKind.ParenthesizedType: - return emitParenthesizedType(node as ts.ParenthesizedTypeNode); - case ts.SyntaxKind.ExpressionWithTypeArguments: - return emitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); - case ts.SyntaxKind.ThisType: - return emitThisType(); - case ts.SyntaxKind.TypeOperator: - return emitTypeOperator(node as ts.TypeOperatorNode); - case ts.SyntaxKind.IndexedAccessType: - return emitIndexedAccessType(node as ts.IndexedAccessTypeNode); - case ts.SyntaxKind.MappedType: - return emitMappedType(node as ts.MappedTypeNode); - case ts.SyntaxKind.LiteralType: - return emitLiteralType(node as ts.LiteralTypeNode); - case ts.SyntaxKind.NamedTupleMember: - return emitNamedTupleMember(node as ts.NamedTupleMember); - case ts.SyntaxKind.TemplateLiteralType: - return emitTemplateType(node as ts.TemplateLiteralTypeNode); - case ts.SyntaxKind.TemplateLiteralTypeSpan: - return emitTemplateTypeSpan(node as ts.TemplateLiteralTypeSpan); - case ts.SyntaxKind.ImportType: - return emitImportTypeNode(node as ts.ImportTypeNode); - - // Binding patterns - case ts.SyntaxKind.ObjectBindingPattern: - return emitObjectBindingPattern(node as ts.ObjectBindingPattern); - case ts.SyntaxKind.ArrayBindingPattern: - return emitArrayBindingPattern(node as ts.ArrayBindingPattern); - case ts.SyntaxKind.BindingElement: - return emitBindingElement(node as ts.BindingElement); - - // Misc - case ts.SyntaxKind.TemplateSpan: - return emitTemplateSpan(node as ts.TemplateSpan); - case ts.SyntaxKind.SemicolonClassElement: - return emitSemicolonClassElement(); - - // Statements - case ts.SyntaxKind.Block: - return emitBlock(node as ts.Block); - case ts.SyntaxKind.VariableStatement: - return emitVariableStatement(node as ts.VariableStatement); - case ts.SyntaxKind.EmptyStatement: - return emitEmptyStatement(/*isEmbeddedStatement*/ false); - case ts.SyntaxKind.ExpressionStatement: - return emitExpressionStatement(node as ts.ExpressionStatement); - case ts.SyntaxKind.IfStatement: - return emitIfStatement(node as ts.IfStatement); - case ts.SyntaxKind.DoStatement: - return emitDoStatement(node as ts.DoStatement); - case ts.SyntaxKind.WhileStatement: - return emitWhileStatement(node as ts.WhileStatement); - case ts.SyntaxKind.ForStatement: - return emitForStatement(node as ts.ForStatement); - case ts.SyntaxKind.ForInStatement: - return emitForInStatement(node as ts.ForInStatement); - case ts.SyntaxKind.ForOfStatement: - return emitForOfStatement(node as ts.ForOfStatement); - case ts.SyntaxKind.ContinueStatement: - return emitContinueStatement(node as ts.ContinueStatement); - case ts.SyntaxKind.BreakStatement: - return emitBreakStatement(node as ts.BreakStatement); - case ts.SyntaxKind.ReturnStatement: - return emitReturnStatement(node as ts.ReturnStatement); - case ts.SyntaxKind.WithStatement: - return emitWithStatement(node as ts.WithStatement); - case ts.SyntaxKind.SwitchStatement: - return emitSwitchStatement(node as ts.SwitchStatement); - case ts.SyntaxKind.LabeledStatement: - return emitLabeledStatement(node as ts.LabeledStatement); - case ts.SyntaxKind.ThrowStatement: - return emitThrowStatement(node as ts.ThrowStatement); - case ts.SyntaxKind.TryStatement: - return emitTryStatement(node as ts.TryStatement); - case ts.SyntaxKind.DebuggerStatement: - return emitDebuggerStatement(node as ts.DebuggerStatement); - - // Declarations - case ts.SyntaxKind.VariableDeclaration: - return emitVariableDeclaration(node as ts.VariableDeclaration); - case ts.SyntaxKind.VariableDeclarationList: - return emitVariableDeclarationList(node as ts.VariableDeclarationList); - case ts.SyntaxKind.FunctionDeclaration: - return emitFunctionDeclaration(node as ts.FunctionDeclaration); - case ts.SyntaxKind.ClassDeclaration: - return emitClassDeclaration(node as ts.ClassDeclaration); - case ts.SyntaxKind.InterfaceDeclaration: - return emitInterfaceDeclaration(node as ts.InterfaceDeclaration); - case ts.SyntaxKind.TypeAliasDeclaration: - return emitTypeAliasDeclaration(node as ts.TypeAliasDeclaration); - case ts.SyntaxKind.EnumDeclaration: - return emitEnumDeclaration(node as ts.EnumDeclaration); - case ts.SyntaxKind.ModuleDeclaration: - return emitModuleDeclaration(node as ts.ModuleDeclaration); - case ts.SyntaxKind.ModuleBlock: - return emitModuleBlock(node as ts.ModuleBlock); - case ts.SyntaxKind.CaseBlock: - return emitCaseBlock(node as ts.CaseBlock); - case ts.SyntaxKind.NamespaceExportDeclaration: - return emitNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration); - case ts.SyntaxKind.ImportEqualsDeclaration: - return emitImportEqualsDeclaration(node as ts.ImportEqualsDeclaration); - case ts.SyntaxKind.ImportDeclaration: - return emitImportDeclaration(node as ts.ImportDeclaration); - case ts.SyntaxKind.ImportClause: - return emitImportClause(node as ts.ImportClause); - case ts.SyntaxKind.NamespaceImport: - return emitNamespaceImport(node as ts.NamespaceImport); - case ts.SyntaxKind.NamespaceExport: - return emitNamespaceExport(node as ts.NamespaceExport); - case ts.SyntaxKind.NamedImports: - return emitNamedImports(node as ts.NamedImports); - case ts.SyntaxKind.ImportSpecifier: - return emitImportSpecifier(node as ts.ImportSpecifier); - case ts.SyntaxKind.ExportAssignment: - return emitExportAssignment(node as ts.ExportAssignment); - case ts.SyntaxKind.ExportDeclaration: - return emitExportDeclaration(node as ts.ExportDeclaration); - case ts.SyntaxKind.NamedExports: - return emitNamedExports(node as ts.NamedExports); - case ts.SyntaxKind.ExportSpecifier: - return emitExportSpecifier(node as ts.ExportSpecifier); - case ts.SyntaxKind.AssertClause: - return emitAssertClause(node as ts.AssertClause); - case ts.SyntaxKind.AssertEntry: - return emitAssertEntry(node as ts.AssertEntry); - case ts.SyntaxKind.MissingDeclaration: - return; + // Declarations + case ts.SyntaxKind.VariableDeclaration: + return emitVariableDeclaration(node as ts.VariableDeclaration); + case ts.SyntaxKind.VariableDeclarationList: + return emitVariableDeclarationList(node as ts.VariableDeclarationList); + case ts.SyntaxKind.FunctionDeclaration: + return emitFunctionDeclaration(node as ts.FunctionDeclaration); + case ts.SyntaxKind.ClassDeclaration: + return emitClassDeclaration(node as ts.ClassDeclaration); + case ts.SyntaxKind.InterfaceDeclaration: + return emitInterfaceDeclaration(node as ts.InterfaceDeclaration); + case ts.SyntaxKind.TypeAliasDeclaration: + return emitTypeAliasDeclaration(node as ts.TypeAliasDeclaration); + case ts.SyntaxKind.EnumDeclaration: + return emitEnumDeclaration(node as ts.EnumDeclaration); + case ts.SyntaxKind.ModuleDeclaration: + return emitModuleDeclaration(node as ts.ModuleDeclaration); + case ts.SyntaxKind.ModuleBlock: + return emitModuleBlock(node as ts.ModuleBlock); + case ts.SyntaxKind.CaseBlock: + return emitCaseBlock(node as ts.CaseBlock); + case ts.SyntaxKind.NamespaceExportDeclaration: + return emitNamespaceExportDeclaration(node as ts.NamespaceExportDeclaration); + case ts.SyntaxKind.ImportEqualsDeclaration: + return emitImportEqualsDeclaration(node as ts.ImportEqualsDeclaration); + case ts.SyntaxKind.ImportDeclaration: + return emitImportDeclaration(node as ts.ImportDeclaration); + case ts.SyntaxKind.ImportClause: + return emitImportClause(node as ts.ImportClause); + case ts.SyntaxKind.NamespaceImport: + return emitNamespaceImport(node as ts.NamespaceImport); + case ts.SyntaxKind.NamespaceExport: + return emitNamespaceExport(node as ts.NamespaceExport); + case ts.SyntaxKind.NamedImports: + return emitNamedImports(node as ts.NamedImports); + case ts.SyntaxKind.ImportSpecifier: + return emitImportSpecifier(node as ts.ImportSpecifier); + case ts.SyntaxKind.ExportAssignment: + return emitExportAssignment(node as ts.ExportAssignment); + case ts.SyntaxKind.ExportDeclaration: + return emitExportDeclaration(node as ts.ExportDeclaration); + case ts.SyntaxKind.NamedExports: + return emitNamedExports(node as ts.NamedExports); + case ts.SyntaxKind.ExportSpecifier: + return emitExportSpecifier(node as ts.ExportSpecifier); + case ts.SyntaxKind.AssertClause: + return emitAssertClause(node as ts.AssertClause); + case ts.SyntaxKind.AssertEntry: + return emitAssertEntry(node as ts.AssertEntry); + case ts.SyntaxKind.MissingDeclaration: + return; + + // Module references + case ts.SyntaxKind.ExternalModuleReference: + return emitExternalModuleReference(node as ts.ExternalModuleReference); + + // JSX (non-expression) + case ts.SyntaxKind.JsxText: + return emitJsxText(node as ts.JsxText); + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxOpeningFragment: + return emitJsxOpeningElementOrFragment(node as ts.JsxOpeningElement); + case ts.SyntaxKind.JsxClosingElement: + case ts.SyntaxKind.JsxClosingFragment: + return emitJsxClosingElementOrFragment(node as ts.JsxClosingElement); + case ts.SyntaxKind.JsxAttribute: + return emitJsxAttribute(node as ts.JsxAttribute); + case ts.SyntaxKind.JsxAttributes: + return emitJsxAttributes(node as ts.JsxAttributes); + case ts.SyntaxKind.JsxSpreadAttribute: + return emitJsxSpreadAttribute(node as ts.JsxSpreadAttribute); + case ts.SyntaxKind.JsxExpression: + return emitJsxExpression(node as ts.JsxExpression); + + // Clauses + case ts.SyntaxKind.CaseClause: + return emitCaseClause(node as ts.CaseClause); + case ts.SyntaxKind.DefaultClause: + return emitDefaultClause(node as ts.DefaultClause); + case ts.SyntaxKind.HeritageClause: + return emitHeritageClause(node as ts.HeritageClause); + case ts.SyntaxKind.CatchClause: + return emitCatchClause(node as ts.CatchClause); - // Module references - case ts.SyntaxKind.ExternalModuleReference: - return emitExternalModuleReference(node as ts.ExternalModuleReference); - - // JSX (non-expression) - case ts.SyntaxKind.JsxText: - return emitJsxText(node as ts.JsxText); - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxOpeningFragment: - return emitJsxOpeningElementOrFragment(node as ts.JsxOpeningElement); - case ts.SyntaxKind.JsxClosingElement: - case ts.SyntaxKind.JsxClosingFragment: - return emitJsxClosingElementOrFragment(node as ts.JsxClosingElement); - case ts.SyntaxKind.JsxAttribute: - return emitJsxAttribute(node as ts.JsxAttribute); - case ts.SyntaxKind.JsxAttributes: - return emitJsxAttributes(node as ts.JsxAttributes); - case ts.SyntaxKind.JsxSpreadAttribute: - return emitJsxSpreadAttribute(node as ts.JsxSpreadAttribute); - case ts.SyntaxKind.JsxExpression: - return emitJsxExpression(node as ts.JsxExpression); - - // Clauses - case ts.SyntaxKind.CaseClause: - return emitCaseClause(node as ts.CaseClause); - case ts.SyntaxKind.DefaultClause: - return emitDefaultClause(node as ts.DefaultClause); - case ts.SyntaxKind.HeritageClause: - return emitHeritageClause(node as ts.HeritageClause); - case ts.SyntaxKind.CatchClause: - return emitCatchClause(node as ts.CatchClause); - - // Property assignments - case ts.SyntaxKind.PropertyAssignment: - return emitPropertyAssignment(node as ts.PropertyAssignment); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return emitShorthandPropertyAssignment(node as ts.ShorthandPropertyAssignment); - case ts.SyntaxKind.SpreadAssignment: - return emitSpreadAssignment(node as ts.SpreadAssignment); - - // Enum - case ts.SyntaxKind.EnumMember: - return emitEnumMember(node as ts.EnumMember); - - // Unparsed - case ts.SyntaxKind.UnparsedPrologue: - return writeUnparsedNode(node as ts.UnparsedNode); - case ts.SyntaxKind.UnparsedSource: - case ts.SyntaxKind.UnparsedPrepend: - return emitUnparsedSourceOrPrepend(node as ts.UnparsedSource); - case ts.SyntaxKind.UnparsedText: - case ts.SyntaxKind.UnparsedInternalText: - return emitUnparsedTextLike(node as ts.UnparsedTextLike); - case ts.SyntaxKind.UnparsedSyntheticReference: - return emitUnparsedSyntheticReference(node as ts.UnparsedSyntheticReference); - - // Top-level nodes - case ts.SyntaxKind.SourceFile: - return emitSourceFile(node as ts.SourceFile); - case ts.SyntaxKind.Bundle: - return ts.Debug.fail("Bundles should be printed using printBundle"); - // SyntaxKind.UnparsedSource (handled above) - case ts.SyntaxKind.InputFiles: - return ts.Debug.fail("InputFiles should not be printed"); - - // JSDoc nodes (only used in codefixes currently) - case ts.SyntaxKind.JSDocTypeExpression: - return emitJSDocTypeExpression(node as ts.JSDocTypeExpression); - case ts.SyntaxKind.JSDocNameReference: - return emitJSDocNameReference(node as ts.JSDocNameReference); - case ts.SyntaxKind.JSDocAllType: - return writePunctuation("*"); - case ts.SyntaxKind.JSDocUnknownType: - return writePunctuation("?"); - case ts.SyntaxKind.JSDocNullableType: - return emitJSDocNullableType(node as ts.JSDocNullableType); - case ts.SyntaxKind.JSDocNonNullableType: - return emitJSDocNonNullableType(node as ts.JSDocNonNullableType); - case ts.SyntaxKind.JSDocOptionalType: - return emitJSDocOptionalType(node as ts.JSDocOptionalType); - case ts.SyntaxKind.JSDocFunctionType: - return emitJSDocFunctionType(node as ts.JSDocFunctionType); - case ts.SyntaxKind.RestType: - case ts.SyntaxKind.JSDocVariadicType: - return emitRestOrJSDocVariadicType(node as ts.RestTypeNode | ts.JSDocVariadicType); - case ts.SyntaxKind.JSDocNamepathType: - return; - case ts.SyntaxKind.JSDoc: - return emitJSDoc(node as ts.JSDoc); - case ts.SyntaxKind.JSDocTypeLiteral: - return emitJSDocTypeLiteral(node as ts.JSDocTypeLiteral); - case ts.SyntaxKind.JSDocSignature: - return emitJSDocSignature(node as ts.JSDocSignature); - case ts.SyntaxKind.JSDocTag: - case ts.SyntaxKind.JSDocClassTag: - case ts.SyntaxKind.JSDocOverrideTag: - return emitJSDocSimpleTag(node as ts.JSDocTag); - case ts.SyntaxKind.JSDocAugmentsTag: - case ts.SyntaxKind.JSDocImplementsTag: - return emitJSDocHeritageTag(node as ts.JSDocImplementsTag | ts.JSDocAugmentsTag); - case ts.SyntaxKind.JSDocAuthorTag: - case ts.SyntaxKind.JSDocDeprecatedTag: - return; - // SyntaxKind.JSDocClassTag (see JSDocTag, above) - case ts.SyntaxKind.JSDocPublicTag: - case ts.SyntaxKind.JSDocPrivateTag: - case ts.SyntaxKind.JSDocProtectedTag: - case ts.SyntaxKind.JSDocReadonlyTag: - return; - case ts.SyntaxKind.JSDocCallbackTag: - return emitJSDocCallbackTag(node as ts.JSDocCallbackTag); - // SyntaxKind.JSDocEnumTag (see below) - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocPropertyTag: - return emitJSDocPropertyLikeTag(node as ts.JSDocPropertyLikeTag); - case ts.SyntaxKind.JSDocEnumTag: - case ts.SyntaxKind.JSDocReturnTag: - case ts.SyntaxKind.JSDocThisTag: - case ts.SyntaxKind.JSDocTypeTag: - return emitJSDocSimpleTypedTag(node as ts.JSDocTypeTag); - case ts.SyntaxKind.JSDocTemplateTag: - return emitJSDocTemplateTag(node as ts.JSDocTemplateTag); - case ts.SyntaxKind.JSDocTypedefTag: - return emitJSDocTypedefTag(node as ts.JSDocTypedefTag); - case ts.SyntaxKind.JSDocSeeTag: - return emitJSDocSeeTag(node as ts.JSDocSeeTag); - // SyntaxKind.JSDocPropertyTag (see JSDocParameterTag, above) - - // Transformation nodes - case ts.SyntaxKind.NotEmittedStatement: - case ts.SyntaxKind.EndOfDeclarationMarker: - case ts.SyntaxKind.MergeDeclarationMarker: - return; - } - if (ts.isExpression(node)) { - hint = ts.EmitHint.Expression; - if (substituteNode !== ts.noEmitSubstitution) { - const substitute = substituteNode(hint, node) || node; - if (substitute !== node) { - node = substitute; - if (currentParenthesizerRule) { - node = currentParenthesizerRule(node); - } + // Property assignments + case ts.SyntaxKind.PropertyAssignment: + return emitPropertyAssignment(node as ts.PropertyAssignment); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return emitShorthandPropertyAssignment(node as ts.ShorthandPropertyAssignment); + case ts.SyntaxKind.SpreadAssignment: + return emitSpreadAssignment(node as ts.SpreadAssignment); + + // Enum + case ts.SyntaxKind.EnumMember: + return emitEnumMember(node as ts.EnumMember); + + // Unparsed + case ts.SyntaxKind.UnparsedPrologue: + return writeUnparsedNode(node as ts.UnparsedNode); + case ts.SyntaxKind.UnparsedSource: + case ts.SyntaxKind.UnparsedPrepend: + return emitUnparsedSourceOrPrepend(node as ts.UnparsedSource); + case ts.SyntaxKind.UnparsedText: + case ts.SyntaxKind.UnparsedInternalText: + return emitUnparsedTextLike(node as ts.UnparsedTextLike); + case ts.SyntaxKind.UnparsedSyntheticReference: + return emitUnparsedSyntheticReference(node as ts.UnparsedSyntheticReference); + + // Top-level nodes + case ts.SyntaxKind.SourceFile: + return emitSourceFile(node as ts.SourceFile); + case ts.SyntaxKind.Bundle: + return ts.Debug.fail("Bundles should be printed using printBundle"); + // SyntaxKind.UnparsedSource (handled above) + case ts.SyntaxKind.InputFiles: + return ts.Debug.fail("InputFiles should not be printed"); + + // JSDoc nodes (only used in codefixes currently) + case ts.SyntaxKind.JSDocTypeExpression: + return emitJSDocTypeExpression(node as ts.JSDocTypeExpression); + case ts.SyntaxKind.JSDocNameReference: + return emitJSDocNameReference(node as ts.JSDocNameReference); + case ts.SyntaxKind.JSDocAllType: + return writePunctuation("*"); + case ts.SyntaxKind.JSDocUnknownType: + return writePunctuation("?"); + case ts.SyntaxKind.JSDocNullableType: + return emitJSDocNullableType(node as ts.JSDocNullableType); + case ts.SyntaxKind.JSDocNonNullableType: + return emitJSDocNonNullableType(node as ts.JSDocNonNullableType); + case ts.SyntaxKind.JSDocOptionalType: + return emitJSDocOptionalType(node as ts.JSDocOptionalType); + case ts.SyntaxKind.JSDocFunctionType: + return emitJSDocFunctionType(node as ts.JSDocFunctionType); + case ts.SyntaxKind.RestType: + case ts.SyntaxKind.JSDocVariadicType: + return emitRestOrJSDocVariadicType(node as ts.RestTypeNode | ts.JSDocVariadicType); + case ts.SyntaxKind.JSDocNamepathType: + return; + case ts.SyntaxKind.JSDoc: + return emitJSDoc(node as ts.JSDoc); + case ts.SyntaxKind.JSDocTypeLiteral: + return emitJSDocTypeLiteral(node as ts.JSDocTypeLiteral); + case ts.SyntaxKind.JSDocSignature: + return emitJSDocSignature(node as ts.JSDocSignature); + case ts.SyntaxKind.JSDocTag: + case ts.SyntaxKind.JSDocClassTag: + case ts.SyntaxKind.JSDocOverrideTag: + return emitJSDocSimpleTag(node as ts.JSDocTag); + case ts.SyntaxKind.JSDocAugmentsTag: + case ts.SyntaxKind.JSDocImplementsTag: + return emitJSDocHeritageTag(node as ts.JSDocImplementsTag | ts.JSDocAugmentsTag); + case ts.SyntaxKind.JSDocAuthorTag: + case ts.SyntaxKind.JSDocDeprecatedTag: + return; + // SyntaxKind.JSDocClassTag (see JSDocTag, above) + case ts.SyntaxKind.JSDocPublicTag: + case ts.SyntaxKind.JSDocPrivateTag: + case ts.SyntaxKind.JSDocProtectedTag: + case ts.SyntaxKind.JSDocReadonlyTag: + return; + case ts.SyntaxKind.JSDocCallbackTag: + return emitJSDocCallbackTag(node as ts.JSDocCallbackTag); + // SyntaxKind.JSDocEnumTag (see below) + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocPropertyTag: + return emitJSDocPropertyLikeTag(node as ts.JSDocPropertyLikeTag); + case ts.SyntaxKind.JSDocEnumTag: + case ts.SyntaxKind.JSDocReturnTag: + case ts.SyntaxKind.JSDocThisTag: + case ts.SyntaxKind.JSDocTypeTag: + return emitJSDocSimpleTypedTag(node as ts.JSDocTypeTag); + case ts.SyntaxKind.JSDocTemplateTag: + return emitJSDocTemplateTag(node as ts.JSDocTemplateTag); + case ts.SyntaxKind.JSDocTypedefTag: + return emitJSDocTypedefTag(node as ts.JSDocTypedefTag); + case ts.SyntaxKind.JSDocSeeTag: + return emitJSDocSeeTag(node as ts.JSDocSeeTag); + // SyntaxKind.JSDocPropertyTag (see JSDocParameterTag, above) + + // Transformation nodes + case ts.SyntaxKind.NotEmittedStatement: + case ts.SyntaxKind.EndOfDeclarationMarker: + case ts.SyntaxKind.MergeDeclarationMarker: + return; + } + if (ts.isExpression(node)) { + hint = ts.EmitHint.Expression; + if (substituteNode !== ts.noEmitSubstitution) { + const substitute = substituteNode(hint, node) || node; + if (substitute !== node) { + node = substitute; + if (currentParenthesizerRule) { + node = currentParenthesizerRule(node); } } } } - if (hint === ts.EmitHint.Expression) { - switch (node.kind) { - // Literals - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - return emitNumericOrBigIntLiteral(node as ts.NumericLiteral | ts.BigIntLiteral); - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.RegularExpressionLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return emitLiteral(node as ts.LiteralExpression, /*jsxAttributeEscape*/ false); - - // Identifiers - case ts.SyntaxKind.Identifier: - return emitIdentifier(node as ts.Identifier); - case ts.SyntaxKind.PrivateIdentifier: - return emitPrivateIdentifier(node as ts.PrivateIdentifier); - - // Expressions - case ts.SyntaxKind.ArrayLiteralExpression: - return emitArrayLiteralExpression(node as ts.ArrayLiteralExpression); - case ts.SyntaxKind.ObjectLiteralExpression: - return emitObjectLiteralExpression(node as ts.ObjectLiteralExpression); - case ts.SyntaxKind.PropertyAccessExpression: - return emitPropertyAccessExpression(node as ts.PropertyAccessExpression); - case ts.SyntaxKind.ElementAccessExpression: - return emitElementAccessExpression(node as ts.ElementAccessExpression); - case ts.SyntaxKind.CallExpression: - return emitCallExpression(node as ts.CallExpression); - case ts.SyntaxKind.NewExpression: - return emitNewExpression(node as ts.NewExpression); - case ts.SyntaxKind.TaggedTemplateExpression: - return emitTaggedTemplateExpression(node as ts.TaggedTemplateExpression); - case ts.SyntaxKind.TypeAssertionExpression: - return emitTypeAssertionExpression(node as ts.TypeAssertion); - case ts.SyntaxKind.ParenthesizedExpression: - return emitParenthesizedExpression(node as ts.ParenthesizedExpression); - case ts.SyntaxKind.FunctionExpression: - return emitFunctionExpression(node as ts.FunctionExpression); - case ts.SyntaxKind.ArrowFunction: - return emitArrowFunction(node as ts.ArrowFunction); - case ts.SyntaxKind.DeleteExpression: - return emitDeleteExpression(node as ts.DeleteExpression); - case ts.SyntaxKind.TypeOfExpression: - return emitTypeOfExpression(node as ts.TypeOfExpression); - case ts.SyntaxKind.VoidExpression: - return emitVoidExpression(node as ts.VoidExpression); - case ts.SyntaxKind.AwaitExpression: - return emitAwaitExpression(node as ts.AwaitExpression); - case ts.SyntaxKind.PrefixUnaryExpression: - return emitPrefixUnaryExpression(node as ts.PrefixUnaryExpression); - case ts.SyntaxKind.PostfixUnaryExpression: - return emitPostfixUnaryExpression(node as ts.PostfixUnaryExpression); - case ts.SyntaxKind.BinaryExpression: - return emitBinaryExpression(node as ts.BinaryExpression); - case ts.SyntaxKind.ConditionalExpression: - return emitConditionalExpression(node as ts.ConditionalExpression); - case ts.SyntaxKind.TemplateExpression: - return emitTemplateExpression(node as ts.TemplateExpression); - case ts.SyntaxKind.YieldExpression: - return emitYieldExpression(node as ts.YieldExpression); - case ts.SyntaxKind.SpreadElement: - return emitSpreadElement(node as ts.SpreadElement); - case ts.SyntaxKind.ClassExpression: - return emitClassExpression(node as ts.ClassExpression); - case ts.SyntaxKind.OmittedExpression: - return; - case ts.SyntaxKind.AsExpression: - return emitAsExpression(node as ts.AsExpression); - case ts.SyntaxKind.NonNullExpression: - return emitNonNullExpression(node as ts.NonNullExpression); - case ts.SyntaxKind.ExpressionWithTypeArguments: - return emitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); - case ts.SyntaxKind.MetaProperty: - return emitMetaProperty(node as ts.MetaProperty); - case ts.SyntaxKind.SyntheticExpression: - return ts.Debug.fail("SyntheticExpression should never be printed."); - - // JSX - case ts.SyntaxKind.JsxElement: - return emitJsxElement(node as ts.JsxElement); - case ts.SyntaxKind.JsxSelfClosingElement: - return emitJsxSelfClosingElement(node as ts.JsxSelfClosingElement); - case ts.SyntaxKind.JsxFragment: - return emitJsxFragment(node as ts.JsxFragment); - - // Synthesized list - case ts.SyntaxKind.SyntaxList: - return ts.Debug.fail("SyntaxList should not be printed"); - - // Transformation nodes - case ts.SyntaxKind.NotEmittedStatement: - return; - case ts.SyntaxKind.PartiallyEmittedExpression: - return emitPartiallyEmittedExpression(node as ts.PartiallyEmittedExpression); - case ts.SyntaxKind.CommaListExpression: - return emitCommaList(node as ts.CommaListExpression); - case ts.SyntaxKind.MergeDeclarationMarker: - case ts.SyntaxKind.EndOfDeclarationMarker: - return; - case ts.SyntaxKind.SyntheticReferenceExpression: - return ts.Debug.fail("SyntheticReferenceExpression should not be printed"); - } - } - if (ts.isKeyword(node.kind)) - return writeTokenNode(node, writeKeyword); - if (ts.isTokenKind(node.kind)) - return writeTokenNode(node, writePunctuation); - ts.Debug.fail(`Unhandled SyntaxKind: ${ts.Debug.formatSyntaxKind(node.kind)}.`); } + if (hint === ts.EmitHint.Expression) { + switch (node.kind) { + // Literals + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + return emitNumericOrBigIntLiteral(node as ts.NumericLiteral | ts.BigIntLiteral); + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.RegularExpressionLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return emitLiteral(node as ts.LiteralExpression, /*jsxAttributeEscape*/ false); + + // Identifiers + case ts.SyntaxKind.Identifier: + return emitIdentifier(node as ts.Identifier); + case ts.SyntaxKind.PrivateIdentifier: + return emitPrivateIdentifier(node as ts.PrivateIdentifier); + + // Expressions + case ts.SyntaxKind.ArrayLiteralExpression: + return emitArrayLiteralExpression(node as ts.ArrayLiteralExpression); + case ts.SyntaxKind.ObjectLiteralExpression: + return emitObjectLiteralExpression(node as ts.ObjectLiteralExpression); + case ts.SyntaxKind.PropertyAccessExpression: + return emitPropertyAccessExpression(node as ts.PropertyAccessExpression); + case ts.SyntaxKind.ElementAccessExpression: + return emitElementAccessExpression(node as ts.ElementAccessExpression); + case ts.SyntaxKind.CallExpression: + return emitCallExpression(node as ts.CallExpression); + case ts.SyntaxKind.NewExpression: + return emitNewExpression(node as ts.NewExpression); + case ts.SyntaxKind.TaggedTemplateExpression: + return emitTaggedTemplateExpression(node as ts.TaggedTemplateExpression); + case ts.SyntaxKind.TypeAssertionExpression: + return emitTypeAssertionExpression(node as ts.TypeAssertion); + case ts.SyntaxKind.ParenthesizedExpression: + return emitParenthesizedExpression(node as ts.ParenthesizedExpression); + case ts.SyntaxKind.FunctionExpression: + return emitFunctionExpression(node as ts.FunctionExpression); + case ts.SyntaxKind.ArrowFunction: + return emitArrowFunction(node as ts.ArrowFunction); + case ts.SyntaxKind.DeleteExpression: + return emitDeleteExpression(node as ts.DeleteExpression); + case ts.SyntaxKind.TypeOfExpression: + return emitTypeOfExpression(node as ts.TypeOfExpression); + case ts.SyntaxKind.VoidExpression: + return emitVoidExpression(node as ts.VoidExpression); + case ts.SyntaxKind.AwaitExpression: + return emitAwaitExpression(node as ts.AwaitExpression); + case ts.SyntaxKind.PrefixUnaryExpression: + return emitPrefixUnaryExpression(node as ts.PrefixUnaryExpression); + case ts.SyntaxKind.PostfixUnaryExpression: + return emitPostfixUnaryExpression(node as ts.PostfixUnaryExpression); + case ts.SyntaxKind.BinaryExpression: + return emitBinaryExpression(node as ts.BinaryExpression); + case ts.SyntaxKind.ConditionalExpression: + return emitConditionalExpression(node as ts.ConditionalExpression); + case ts.SyntaxKind.TemplateExpression: + return emitTemplateExpression(node as ts.TemplateExpression); + case ts.SyntaxKind.YieldExpression: + return emitYieldExpression(node as ts.YieldExpression); + case ts.SyntaxKind.SpreadElement: + return emitSpreadElement(node as ts.SpreadElement); + case ts.SyntaxKind.ClassExpression: + return emitClassExpression(node as ts.ClassExpression); + case ts.SyntaxKind.OmittedExpression: + return; + case ts.SyntaxKind.AsExpression: + return emitAsExpression(node as ts.AsExpression); + case ts.SyntaxKind.NonNullExpression: + return emitNonNullExpression(node as ts.NonNullExpression); + case ts.SyntaxKind.ExpressionWithTypeArguments: + return emitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); + case ts.SyntaxKind.MetaProperty: + return emitMetaProperty(node as ts.MetaProperty); + case ts.SyntaxKind.SyntheticExpression: + return ts.Debug.fail("SyntheticExpression should never be printed."); + + // JSX + case ts.SyntaxKind.JsxElement: + return emitJsxElement(node as ts.JsxElement); + case ts.SyntaxKind.JsxSelfClosingElement: + return emitJsxSelfClosingElement(node as ts.JsxSelfClosingElement); + case ts.SyntaxKind.JsxFragment: + return emitJsxFragment(node as ts.JsxFragment); + + // Synthesized list + case ts.SyntaxKind.SyntaxList: + return ts.Debug.fail("SyntaxList should not be printed"); + + // Transformation nodes + case ts.SyntaxKind.NotEmittedStatement: + return; + case ts.SyntaxKind.PartiallyEmittedExpression: + return emitPartiallyEmittedExpression(node as ts.PartiallyEmittedExpression); + case ts.SyntaxKind.CommaListExpression: + return emitCommaList(node as ts.CommaListExpression); + case ts.SyntaxKind.MergeDeclarationMarker: + case ts.SyntaxKind.EndOfDeclarationMarker: + return; + case ts.SyntaxKind.SyntheticReferenceExpression: + return ts.Debug.fail("SyntheticReferenceExpression should not be printed"); + } + } + if (ts.isKeyword(node.kind)) + return writeTokenNode(node, writeKeyword); + if (ts.isTokenKind(node.kind)) + return writeTokenNode(node, writePunctuation); + ts.Debug.fail(`Unhandled SyntaxKind: ${ts.Debug.formatSyntaxKind(node.kind)}.`); + } - function emitMappedTypeParameter(node: ts.TypeParameterDeclaration): void { - emit(node.name); - writeSpace(); - writeKeyword("in"); - writeSpace(); - emit(node.constraint); - } + function emitMappedTypeParameter(node: ts.TypeParameterDeclaration): void { + emit(node.name); + writeSpace(); + writeKeyword("in"); + writeSpace(); + emit(node.constraint); + } - function pipelineEmitWithSubstitution(hint: ts.EmitHint, node: ts.Node) { - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, hint, node); - ts.Debug.assertIsDefined(lastSubstitution); - node = lastSubstitution; - lastSubstitution = undefined; - pipelinePhase(hint, node); - } + function pipelineEmitWithSubstitution(hint: ts.EmitHint, node: ts.Node) { + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, hint, node); + ts.Debug.assertIsDefined(lastSubstitution); + node = lastSubstitution; + lastSubstitution = undefined; + pipelinePhase(hint, node); + } - function getHelpersFromBundledSourceFiles(bundle: ts.Bundle): string[] | undefined { - let result: string[] | undefined; - if (moduleKind === ts.ModuleKind.None || printerOptions.noEmitHelpers) { - return undefined; - } - const bundledHelpers = new ts.Map(); - for (const sourceFile of bundle.sourceFiles) { - const shouldSkip = ts.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 getHelpersFromBundledSourceFiles(bundle: ts.Bundle): string[] | undefined { + let result: string[] | undefined; + if (moduleKind === ts.ModuleKind.None || printerOptions.noEmitHelpers) { + return undefined; + } + const bundledHelpers = new ts.Map(); + for (const sourceFile of bundle.sourceFiles) { + const shouldSkip = ts.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); } } - - return result; } - function emitHelpers(node: ts.Node) { - let helpersEmitted = false; - const bundle = node.kind === ts.SyntaxKind.Bundle ? node as ts.Bundle : undefined; - if (bundle && moduleKind === ts.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 = ts.isSourceFile(currentNode) ? currentNode : ts.isUnparsedSource(currentNode) ? undefined : currentSourceFile; - const shouldSkip = printerOptions.noEmitHelpers || (!!sourceFile && ts.hasRecordedExternalHelpers(sourceFile)); - const shouldBundle = (ts.isSourceFile(currentNode) || ts.isUnparsedSource(currentNode)) && !isOwnFileEmit; - const helpers = ts.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; + return result; + } - // 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; - } + function emitHelpers(node: ts.Node) { + let helpersEmitted = false; + const bundle = node.kind === ts.SyntaxKind.Bundle ? node as ts.Bundle : undefined; + if (bundle && moduleKind === ts.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 = ts.isSourceFile(currentNode) ? currentNode : ts.isUnparsedSource(currentNode) ? undefined : currentSourceFile; + const shouldSkip = printerOptions.noEmitHelpers || (!!sourceFile && ts.hasRecordedExternalHelpers(sourceFile)); + const shouldBundle = (ts.isSourceFile(currentNode) || ts.isUnparsedSource(currentNode)) && !isOwnFileEmit; + const helpers = ts.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; - bundledHelpers.set(helper.name, true); + // 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: ts.BundleFileSectionKind.EmitHelpers, data: helper.name }); - helpersEmitted = 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: ts.BundleFileSectionKind.EmitHelpers, data: helper.name }); + helpersEmitted = true; } } - - return helpersEmitted; } - function getSortedEmitHelpers(node: ts.Node) { - const helpers = ts.getEmitHelpers(node); - return helpers && ts.stableSort(helpers, ts.compareEmitHelpers); - } + return helpersEmitted; + } - // - // Literals/Pseudo-literals - // + function getSortedEmitHelpers(node: ts.Node) { + const helpers = ts.getEmitHelpers(node); + return helpers && ts.stableSort(helpers, ts.compareEmitHelpers); + } - // SyntaxKind.NumericLiteral - // SyntaxKind.BigIntLiteral - function emitNumericOrBigIntLiteral(node: ts.NumericLiteral | ts.BigIntLiteral) { - emitLiteral(node, /*jsxAttributeEscape*/ false); - } - - // SyntaxKind.StringLiteral - // SyntaxKind.RegularExpressionLiteral - // SyntaxKind.NoSubstitutionTemplateLiteral - // SyntaxKind.TemplateHead - // SyntaxKind.TemplateMiddle - // SyntaxKind.TemplateTail - function emitLiteral(node: ts.LiteralLikeNode, jsxAttributeEscape: boolean) { - const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape); - if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) - && (node.kind === ts.SyntaxKind.StringLiteral || ts.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); - } - } + // + // Literals/Pseudo-literals + // - // SyntaxKind.UnparsedSource - // SyntaxKind.UnparsedPrepend - function emitUnparsedSourceOrPrepend(unparsed: ts.UnparsedSource | ts.UnparsedPrepend) { - for (const text of unparsed.texts) { - writeLine(); - emit(text); - } - } + // SyntaxKind.NumericLiteral + // SyntaxKind.BigIntLiteral + function emitNumericOrBigIntLiteral(node: ts.NumericLiteral | ts.BigIntLiteral) { + emitLiteral(node, /*jsxAttributeEscape*/ false); + } - // SyntaxKind.UnparsedPrologue - // SyntaxKind.UnparsedText - // SyntaxKind.UnparsedInternal - // SyntaxKind.UnparsedSyntheticReference - function writeUnparsedNode(unparsed: ts.UnparsedNode) { - writer.rawWrite(unparsed.parent.text.substring(unparsed.pos, unparsed.end)); + // SyntaxKind.StringLiteral + // SyntaxKind.RegularExpressionLiteral + // SyntaxKind.NoSubstitutionTemplateLiteral + // SyntaxKind.TemplateHead + // SyntaxKind.TemplateMiddle + // SyntaxKind.TemplateTail + function emitLiteral(node: ts.LiteralLikeNode, jsxAttributeEscape: boolean) { + const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape); + if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) + && (node.kind === ts.SyntaxKind.StringLiteral || ts.isTemplateLiteralKind(node.kind))) { + writeLiteral(text); } - - // SyntaxKind.UnparsedText - // SyntaxKind.UnparsedInternal - function emitUnparsedTextLike(unparsed: ts.UnparsedTextLike) { - const pos = getTextPosWithWriteLine(); - writeUnparsedNode(unparsed); - if (bundleFileInfo) { - updateOrPushBundleFileTextLike(pos, writer.getTextPos(), unparsed.kind === ts.SyntaxKind.UnparsedText ? - ts.BundleFileSectionKind.Text : - ts.BundleFileSectionKind.Internal); - } + else { + // Quick info expects all literals to be called with writeStringLiteral, as there's no specific type for numberLiterals + writeStringLiteral(text); } + } - // SyntaxKind.UnparsedSyntheticReference - function emitUnparsedSyntheticReference(unparsed: ts.UnparsedSyntheticReference) { - const pos = getTextPosWithWriteLine(); - writeUnparsedNode(unparsed); - if (bundleFileInfo) { - const section = ts.clone(unparsed.section); - section.pos = pos; - section.end = writer.getTextPos(); - bundleFileInfo.sections.push(section); - } + // SyntaxKind.UnparsedSource + // SyntaxKind.UnparsedPrepend + function emitUnparsedSourceOrPrepend(unparsed: ts.UnparsedSource | ts.UnparsedPrepend) { + for (const text of unparsed.texts) { + writeLine(); + emit(text); } + } - // - // Snippet Elements - // - - function emitSnippetNode(hint: ts.EmitHint, node: ts.Node, snippet: ts.SnippetElement) { - switch (snippet.kind) { - case ts.SnippetKind.Placeholder: - emitPlaceholder(hint, node, snippet); - break; - case ts.SnippetKind.TabStop: - emitTabStop(hint, node, snippet); - break; - } - } + // SyntaxKind.UnparsedPrologue + // SyntaxKind.UnparsedText + // SyntaxKind.UnparsedInternal + // SyntaxKind.UnparsedSyntheticReference + function writeUnparsedNode(unparsed: ts.UnparsedNode) { + writer.rawWrite(unparsed.parent.text.substring(unparsed.pos, unparsed.end)); + } - function emitPlaceholder(hint: ts.EmitHint, node: ts.Node, snippet: ts.Placeholder) { - nonEscapingWrite(`\$\{${snippet.order}:`); // `${2:` - pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); // `...` - nonEscapingWrite(`\}`); // `}` - // `${2:...}` + // SyntaxKind.UnparsedText + // SyntaxKind.UnparsedInternal + function emitUnparsedTextLike(unparsed: ts.UnparsedTextLike) { + const pos = getTextPosWithWriteLine(); + writeUnparsedNode(unparsed); + if (bundleFileInfo) { + updateOrPushBundleFileTextLike(pos, writer.getTextPos(), unparsed.kind === ts.SyntaxKind.UnparsedText ? + ts.BundleFileSectionKind.Text : + ts.BundleFileSectionKind.Internal); } + } - function emitTabStop(hint: ts.EmitHint, node: ts.Node, snippet: ts.TabStop) { - // A tab stop should only be attached to an empty node, i.e. a node that doesn't emit any text. - ts.Debug.assert(node.kind === ts.SyntaxKind.EmptyStatement, `A tab stop cannot be attached to a node of kind ${ts.Debug.formatSyntaxKind(node.kind)}.`); - ts.Debug.assert(hint !== ts.EmitHint.EmbeddedStatement, `A tab stop cannot be attached to an embedded statement.`); - nonEscapingWrite(`\$${snippet.order}`); + // SyntaxKind.UnparsedSyntheticReference + function emitUnparsedSyntheticReference(unparsed: ts.UnparsedSyntheticReference) { + const pos = getTextPosWithWriteLine(); + writeUnparsedNode(unparsed); + if (bundleFileInfo) { + const section = ts.clone(unparsed.section); + section.pos = pos; + section.end = writer.getTextPos(); + bundleFileInfo.sections.push(section); } + } - // - // Identifiers - // + // + // Snippet Elements + // - function emitIdentifier(node: ts.Identifier) { - const writeText = node.symbol ? writeSymbol : write; - writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); - emitList(node, node.typeArguments, ts.ListFormat.TypeParameters); // Call emitList directly since it could be an array of TypeParameterDeclarations _or_ type arguments + function emitSnippetNode(hint: ts.EmitHint, node: ts.Node, snippet: ts.SnippetElement) { + switch (snippet.kind) { + case ts.SnippetKind.Placeholder: + emitPlaceholder(hint, node, snippet); + break; + case ts.SnippetKind.TabStop: + emitTabStop(hint, node, snippet); + break; } + } - // - // Names - // + function emitPlaceholder(hint: ts.EmitHint, node: ts.Node, snippet: ts.Placeholder) { + nonEscapingWrite(`\$\{${snippet.order}:`); // `${2:` + pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); // `...` + nonEscapingWrite(`\}`); // `}` + // `${2:...}` + } - function emitPrivateIdentifier(node: ts.PrivateIdentifier) { - const writeText = node.symbol ? writeSymbol : write; - writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); - } + function emitTabStop(hint: ts.EmitHint, node: ts.Node, snippet: ts.TabStop) { + // A tab stop should only be attached to an empty node, i.e. a node that doesn't emit any text. + ts.Debug.assert(node.kind === ts.SyntaxKind.EmptyStatement, `A tab stop cannot be attached to a node of kind ${ts.Debug.formatSyntaxKind(node.kind)}.`); + ts.Debug.assert(hint !== ts.EmitHint.EmbeddedStatement, `A tab stop cannot be attached to an embedded statement.`); + nonEscapingWrite(`\$${snippet.order}`); + } + // + // Identifiers + // - function emitQualifiedName(node: ts.QualifiedName) { - emitEntityName(node.left); - writePunctuation("."); - emit(node.right); - } + function emitIdentifier(node: ts.Identifier) { + const writeText = node.symbol ? writeSymbol : write; + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); + emitList(node, node.typeArguments, ts.ListFormat.TypeParameters); // Call emitList directly since it could be an array of TypeParameterDeclarations _or_ type arguments + } - function emitEntityName(node: ts.EntityName) { - if (node.kind === ts.SyntaxKind.Identifier) { - emitExpression(node); - } - else { - emit(node); - } - } + // + // Names + // - function emitComputedPropertyName(node: ts.ComputedPropertyName) { - writePunctuation("["); - emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfComputedPropertyName); - writePunctuation("]"); - } + function emitPrivateIdentifier(node: ts.PrivateIdentifier) { + const writeText = node.symbol ? writeSymbol : write; + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); + } - // - // Signature elements - // - function emitTypeParameter(node: ts.TypeParameterDeclaration) { - emitModifiers(node, node.modifiers); - emit(node.name); - if (node.constraint) { - writeSpace(); - writeKeyword("extends"); - writeSpace(); - emit(node.constraint); - } - if (node.default) { - writeSpace(); - writeOperator("="); - writeSpace(); - emit(node.default); - } - } + function emitQualifiedName(node: ts.QualifiedName) { + emitEntityName(node.left); + writePunctuation("."); + emit(node.right); + } - function emitParameter(node: ts.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 === ts.SyntaxKind.JSDocFunctionType && !node.name) { - emit(node.type); - } - else { - emitTypeAnnotation(node.type); - } - // 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, parenthesizer.parenthesizeExpressionForDisallowedComma); + function emitEntityName(node: ts.EntityName) { + if (node.kind === ts.SyntaxKind.Identifier) { + emitExpression(node); } - - function emitDecorator(decorator: ts.Decorator) { - writePunctuation("@"); - emitExpression(decorator.expression, parenthesizer.parenthesizeLeftSideOfAccess); + else { + emit(node); } + } - // - // Type members - // + function emitComputedPropertyName(node: ts.ComputedPropertyName) { + writePunctuation("["); + emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfComputedPropertyName); + writePunctuation("]"); + } - function emitPropertySignature(node: ts.PropertySignature) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitNodeWithWriter(node.name, writeProperty); - emit(node.questionToken); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - } + // + // Signature elements + // - function emitPropertyDeclaration(node: ts.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 emitTypeParameter(node: ts.TypeParameterDeclaration) { + emitModifiers(node, node.modifiers); + emit(node.name); + if (node.constraint) { + writeSpace(); + writeKeyword("extends"); + writeSpace(); + emit(node.constraint); } + if (node.default) { + writeSpace(); + writeOperator("="); + writeSpace(); + emit(node.default); + } + } - function emitMethodSignature(node: ts.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); + function emitParameter(node: ts.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 === ts.SyntaxKind.JSDocFunctionType && !node.name) { + emit(node.type); + } + else { emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - popNameGenerationScope(node); } + // 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, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function emitMethodDeclaration(node: ts.MethodDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emit(node.asteriskToken); - emit(node.name); - emit(node.questionToken); - emitSignatureAndBody(node, emitSignatureHead); - } + function emitDecorator(decorator: ts.Decorator) { + writePunctuation("@"); + emitExpression(decorator.expression, parenthesizer.parenthesizeLeftSideOfAccess); + } - function emitClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("static"); - emitBlockFunctionBody(node.body); - } + // + // Type members + // + + function emitPropertySignature(node: ts.PropertySignature) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitNodeWithWriter(node.name, writeProperty); + emit(node.questionToken); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + } - function emitConstructor(node: ts.ConstructorDeclaration) { - emitModifiers(node, node.modifiers); - writeKeyword("constructor"); - emitSignatureAndBody(node, emitSignatureHead); - } + function emitPropertyDeclaration(node: ts.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 emitAccessorDeclaration(node: ts.AccessorDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword(node.kind === ts.SyntaxKind.GetAccessor ? "get" : "set"); - writeSpace(); - emit(node.name); - emitSignatureAndBody(node, emitSignatureHead); - } + function emitMethodSignature(node: ts.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 emitCallSignature(node: ts.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 emitMethodDeclaration(node: ts.MethodDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emit(node.asteriskToken); + emit(node.name); + emit(node.questionToken); + emitSignatureAndBody(node, emitSignatureHead); + } - function emitConstructSignature(node: ts.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 emitClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("static"); + emitBlockFunctionBody(node.body); + } - function emitIndexSignature(node: ts.IndexSignatureDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitParametersForIndexSignature(node, node.parameters); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - } + function emitConstructor(node: ts.ConstructorDeclaration) { + emitModifiers(node, node.modifiers); + writeKeyword("constructor"); + emitSignatureAndBody(node, emitSignatureHead); + } - function emitTemplateTypeSpan(node: ts.TemplateLiteralTypeSpan) { - emit(node.type); - emit(node.literal); - } + function emitAccessorDeclaration(node: ts.AccessorDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword(node.kind === ts.SyntaxKind.GetAccessor ? "get" : "set"); + writeSpace(); + emit(node.name); + emitSignatureAndBody(node, emitSignatureHead); + } - function emitSemicolonClassElement() { - writeTrailingSemicolon(); - } + function emitCallSignature(node: ts.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); + } - // - // Types - // + function emitConstructSignature(node: ts.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 emitTypePredicate(node: ts.TypePredicateNode) { - if (node.assertsModifier) { - emit(node.assertsModifier); - writeSpace(); - } - emit(node.parameterName); - if (node.type) { - writeSpace(); - writeKeyword("is"); - writeSpace(); - emit(node.type); - } - } + function emitIndexSignature(node: ts.IndexSignatureDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitParametersForIndexSignature(node, node.parameters); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + } - function emitTypeReference(node: ts.TypeReferenceNode) { - emit(node.typeName); - emitTypeArguments(node, node.typeArguments); - } + function emitTemplateTypeSpan(node: ts.TemplateLiteralTypeSpan) { + emit(node.type); + emit(node.literal); + } + + function emitSemicolonClassElement() { + writeTrailingSemicolon(); + } + + // + // Types + // - function emitFunctionType(node: ts.FunctionTypeNode) { - pushNameGenerationScope(node); - emitTypeParameters(node, node.typeParameters); - emitParametersForArrow(node, node.parameters); + function emitTypePredicate(node: ts.TypePredicateNode) { + if (node.assertsModifier) { + emit(node.assertsModifier); + writeSpace(); + } + emit(node.parameterName); + if (node.type) { writeSpace(); - writePunctuation("=>"); + writeKeyword("is"); writeSpace(); emit(node.type); - popNameGenerationScope(node); } + } - function emitJSDocFunctionType(node: ts.JSDocFunctionType) { - writeKeyword("function"); - emitParameters(node, node.parameters); - writePunctuation(":"); - emit(node.type); - } + function emitTypeReference(node: ts.TypeReferenceNode) { + emit(node.typeName); + emitTypeArguments(node, node.typeArguments); + } + function emitFunctionType(node: ts.FunctionTypeNode) { + pushNameGenerationScope(node); + emitTypeParameters(node, node.typeParameters); + emitParametersForArrow(node, node.parameters); + writeSpace(); + writePunctuation("=>"); + writeSpace(); + emit(node.type); + popNameGenerationScope(node); + } - function emitJSDocNullableType(node: ts.JSDocNullableType) { - writePunctuation("?"); - emit(node.type); - } + function emitJSDocFunctionType(node: ts.JSDocFunctionType) { + writeKeyword("function"); + emitParameters(node, node.parameters); + writePunctuation(":"); + emit(node.type); + } - function emitJSDocNonNullableType(node: ts.JSDocNonNullableType) { - writePunctuation("!"); - emit(node.type); - } - function emitJSDocOptionalType(node: ts.JSDocOptionalType) { - emit(node.type); - writePunctuation("="); - } + function emitJSDocNullableType(node: ts.JSDocNullableType) { + writePunctuation("?"); + emit(node.type); + } - function emitConstructorType(node: ts.ConstructorTypeNode) { - pushNameGenerationScope(node); - emitModifiers(node, node.modifiers); - writeKeyword("new"); - writeSpace(); - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - writeSpace(); - writePunctuation("=>"); - writeSpace(); - emit(node.type); - popNameGenerationScope(node); - } + function emitJSDocNonNullableType(node: ts.JSDocNonNullableType) { + writePunctuation("!"); + emit(node.type); + } - function emitTypeQuery(node: ts.TypeQueryNode) { - writeKeyword("typeof"); - writeSpace(); - emit(node.exprName); - emitTypeArguments(node, node.typeArguments); - } + function emitJSDocOptionalType(node: ts.JSDocOptionalType) { + emit(node.type); + writePunctuation("="); + } - function emitTypeLiteral(node: ts.TypeLiteralNode) { - writePunctuation("{"); - const flags = ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineTypeLiteralMembers : ts.ListFormat.MultiLineTypeLiteralMembers; - emitList(node, node.members, flags | ts.ListFormat.NoSpaceIfEmpty); - writePunctuation("}"); - } + function emitConstructorType(node: ts.ConstructorTypeNode) { + pushNameGenerationScope(node); + emitModifiers(node, node.modifiers); + writeKeyword("new"); + writeSpace(); + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + writeSpace(); + writePunctuation("=>"); + writeSpace(); + emit(node.type); + popNameGenerationScope(node); + } - function emitArrayType(node: ts.ArrayTypeNode) { - emit(node.elementType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType); - writePunctuation("["); - writePunctuation("]"); - } + function emitTypeQuery(node: ts.TypeQueryNode) { + writeKeyword("typeof"); + writeSpace(); + emit(node.exprName); + emitTypeArguments(node, node.typeArguments); + } - function emitRestOrJSDocVariadicType(node: ts.RestTypeNode | ts.JSDocVariadicType) { - writePunctuation("..."); - emit(node.type); - } + function emitTypeLiteral(node: ts.TypeLiteralNode) { + writePunctuation("{"); + const flags = ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineTypeLiteralMembers : ts.ListFormat.MultiLineTypeLiteralMembers; + emitList(node, node.members, flags | ts.ListFormat.NoSpaceIfEmpty); + writePunctuation("}"); + } - function emitTupleType(node: ts.TupleTypeNode) { - emitTokenWithComment(ts.SyntaxKind.OpenBracketToken, node.pos, writePunctuation, node); - const flags = ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineTupleTypeElements : ts.ListFormat.MultiLineTupleTypeElements; - emitList(node, node.elements, flags | ts.ListFormat.NoSpaceIfEmpty, parenthesizer.parenthesizeElementTypeOfTupleType); - emitTokenWithComment(ts.SyntaxKind.CloseBracketToken, node.elements.end, writePunctuation, node); - } + function emitArrayType(node: ts.ArrayTypeNode) { + emit(node.elementType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType); + writePunctuation("["); + writePunctuation("]"); + } - function emitNamedTupleMember(node: ts.NamedTupleMember) { - emit(node.dotDotDotToken); - emit(node.name); - emit(node.questionToken); - emitTokenWithComment(ts.SyntaxKind.ColonToken, node.name.end, writePunctuation, node); - writeSpace(); - emit(node.type); - } + function emitRestOrJSDocVariadicType(node: ts.RestTypeNode | ts.JSDocVariadicType) { + writePunctuation("..."); + emit(node.type); + } - function emitOptionalType(node: ts.OptionalTypeNode) { - emit(node.type, parenthesizer.parenthesizeTypeOfOptionalType); - writePunctuation("?"); - } + function emitTupleType(node: ts.TupleTypeNode) { + emitTokenWithComment(ts.SyntaxKind.OpenBracketToken, node.pos, writePunctuation, node); + const flags = ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineTupleTypeElements : ts.ListFormat.MultiLineTupleTypeElements; + emitList(node, node.elements, flags | ts.ListFormat.NoSpaceIfEmpty, parenthesizer.parenthesizeElementTypeOfTupleType); + emitTokenWithComment(ts.SyntaxKind.CloseBracketToken, node.elements.end, writePunctuation, node); + } - function emitUnionType(node: ts.UnionTypeNode) { - emitList(node, node.types, ts.ListFormat.UnionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfUnionType); - } + function emitNamedTupleMember(node: ts.NamedTupleMember) { + emit(node.dotDotDotToken); + emit(node.name); + emit(node.questionToken); + emitTokenWithComment(ts.SyntaxKind.ColonToken, node.name.end, writePunctuation, node); + writeSpace(); + emit(node.type); + } - function emitIntersectionType(node: ts.IntersectionTypeNode) { - emitList(node, node.types, ts.ListFormat.IntersectionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfIntersectionType); - } + function emitOptionalType(node: ts.OptionalTypeNode) { + emit(node.type, parenthesizer.parenthesizeTypeOfOptionalType); + writePunctuation("?"); + } - function emitConditionalType(node: ts.ConditionalTypeNode) { - emit(node.checkType, parenthesizer.parenthesizeCheckTypeOfConditionalType); - writeSpace(); - writeKeyword("extends"); - writeSpace(); - emit(node.extendsType, parenthesizer.parenthesizeExtendsTypeOfConditionalType); - writeSpace(); - writePunctuation("?"); - writeSpace(); - emit(node.trueType); - writeSpace(); - writePunctuation(":"); - writeSpace(); - emit(node.falseType); - } + function emitUnionType(node: ts.UnionTypeNode) { + emitList(node, node.types, ts.ListFormat.UnionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfUnionType); + } - function emitInferType(node: ts.InferTypeNode) { - writeKeyword("infer"); - writeSpace(); - emit(node.typeParameter); - } + function emitIntersectionType(node: ts.IntersectionTypeNode) { + emitList(node, node.types, ts.ListFormat.IntersectionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfIntersectionType); + } - function emitParenthesizedType(node: ts.ParenthesizedTypeNode) { - writePunctuation("("); - emit(node.type); - writePunctuation(")"); - } + function emitConditionalType(node: ts.ConditionalTypeNode) { + emit(node.checkType, parenthesizer.parenthesizeCheckTypeOfConditionalType); + writeSpace(); + writeKeyword("extends"); + writeSpace(); + emit(node.extendsType, parenthesizer.parenthesizeExtendsTypeOfConditionalType); + writeSpace(); + writePunctuation("?"); + writeSpace(); + emit(node.trueType); + writeSpace(); + writePunctuation(":"); + writeSpace(); + emit(node.falseType); + } - function emitThisType() { - writeKeyword("this"); - } + function emitInferType(node: ts.InferTypeNode) { + writeKeyword("infer"); + writeSpace(); + emit(node.typeParameter); + } - function emitTypeOperator(node: ts.TypeOperatorNode) { - writeTokenText(node.operator, writeKeyword); - writeSpace(); + function emitParenthesizedType(node: ts.ParenthesizedTypeNode) { + writePunctuation("("); + emit(node.type); + writePunctuation(")"); + } - const parenthesizerRule = node.operator === ts.SyntaxKind.ReadonlyKeyword ? - parenthesizer.parenthesizeOperandOfReadonlyTypeOperator : - parenthesizer.parenthesizeOperandOfTypeOperator; - emit(node.type, parenthesizerRule); - } + function emitThisType() { + writeKeyword("this"); + } - function emitIndexedAccessType(node: ts.IndexedAccessTypeNode) { - emit(node.objectType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType); - writePunctuation("["); - emit(node.indexType); - writePunctuation("]"); - } + function emitTypeOperator(node: ts.TypeOperatorNode) { + writeTokenText(node.operator, writeKeyword); + writeSpace(); - function emitMappedType(node: ts.MappedTypeNode) { - const emitFlags = ts.getEmitFlags(node); - writePunctuation("{"); - if (emitFlags & ts.EmitFlags.SingleLine) { - writeSpace(); - } - else { - writeLine(); - increaseIndent(); - } - if (node.readonlyToken) { - emit(node.readonlyToken); - if (node.readonlyToken.kind !== ts.SyntaxKind.ReadonlyKeyword) { - writeKeyword("readonly"); - } - writeSpace(); - } - writePunctuation("["); + const parenthesizerRule = node.operator === ts.SyntaxKind.ReadonlyKeyword ? + parenthesizer.parenthesizeOperandOfReadonlyTypeOperator : + parenthesizer.parenthesizeOperandOfTypeOperator; + emit(node.type, parenthesizerRule); + } - pipelineEmit(ts.EmitHint.MappedTypeParameter, node.typeParameter); - if (node.nameType) { - writeSpace(); - writeKeyword("as"); - writeSpace(); - emit(node.nameType); - } + function emitIndexedAccessType(node: ts.IndexedAccessTypeNode) { + emit(node.objectType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType); + writePunctuation("["); + emit(node.indexType); + writePunctuation("]"); + } - writePunctuation("]"); - if (node.questionToken) { - emit(node.questionToken); - if (node.questionToken.kind !== ts.SyntaxKind.QuestionToken) { - writePunctuation("?"); - } - } - writePunctuation(":"); + function emitMappedType(node: ts.MappedTypeNode) { + const emitFlags = ts.getEmitFlags(node); + writePunctuation("{"); + if (emitFlags & ts.EmitFlags.SingleLine) { writeSpace(); - emit(node.type); - writeTrailingSemicolon(); - if (emitFlags & ts.EmitFlags.SingleLine) { - writeSpace(); - } - else { - writeLine(); - decreaseIndent(); - } - emitList(node, node.members, ts.ListFormat.PreserveLines); - writePunctuation("}"); } - - function emitLiteralType(node: ts.LiteralTypeNode) { - emitExpression(node.literal); + else { + writeLine(); + increaseIndent(); } + if (node.readonlyToken) { + emit(node.readonlyToken); + if (node.readonlyToken.kind !== ts.SyntaxKind.ReadonlyKeyword) { + writeKeyword("readonly"); + } + writeSpace(); + } + writePunctuation("["); - function emitTemplateType(node: ts.TemplateLiteralTypeNode) { - emit(node.head); - emitList(node, node.templateSpans, ts.ListFormat.TemplateExpressionSpans); + pipelineEmit(ts.EmitHint.MappedTypeParameter, node.typeParameter); + if (node.nameType) { + writeSpace(); + writeKeyword("as"); + writeSpace(); + emit(node.nameType); } - function emitImportTypeNode(node: ts.ImportTypeNode) { - if (node.isTypeOf) { - writeKeyword("typeof"); - writeSpace(); - } - writeKeyword("import"); - writePunctuation("("); - emit(node.argument); - if (node.assertions) { - writePunctuation(","); - writeSpace(); - writePunctuation("{"); - writeSpace(); - writeKeyword("assert"); - writePunctuation(":"); - writeSpace(); - const elements = node.assertions.assertClause.elements; - emitList(node.assertions.assertClause, elements, ts.ListFormat.ImportClauseEntries); - writeSpace(); - writePunctuation("}"); - } - writePunctuation(")"); - if (node.qualifier) { - writePunctuation("."); - emit(node.qualifier); + writePunctuation("]"); + if (node.questionToken) { + emit(node.questionToken); + if (node.questionToken.kind !== ts.SyntaxKind.QuestionToken) { + writePunctuation("?"); } - emitTypeArguments(node, node.typeArguments); } + writePunctuation(":"); + writeSpace(); + emit(node.type); + writeTrailingSemicolon(); + if (emitFlags & ts.EmitFlags.SingleLine) { + writeSpace(); + } + else { + writeLine(); + decreaseIndent(); + } + emitList(node, node.members, ts.ListFormat.PreserveLines); + writePunctuation("}"); + } - // - // Binding patterns - // + function emitLiteralType(node: ts.LiteralTypeNode) { + emitExpression(node.literal); + } + + function emitTemplateType(node: ts.TemplateLiteralTypeNode) { + emit(node.head); + emitList(node, node.templateSpans, ts.ListFormat.TemplateExpressionSpans); + } - function emitObjectBindingPattern(node: ts.ObjectBindingPattern) { + function emitImportTypeNode(node: ts.ImportTypeNode) { + if (node.isTypeOf) { + writeKeyword("typeof"); + writeSpace(); + } + writeKeyword("import"); + writePunctuation("("); + emit(node.argument); + if (node.assertions) { + writePunctuation(","); + writeSpace(); writePunctuation("{"); - emitList(node, node.elements, ts.ListFormat.ObjectBindingPatternElements); + writeSpace(); + writeKeyword("assert"); + writePunctuation(":"); + writeSpace(); + const elements = node.assertions.assertClause.elements; + emitList(node.assertions.assertClause, elements, ts.ListFormat.ImportClauseEntries); + writeSpace(); writePunctuation("}"); } - - function emitArrayBindingPattern(node: ts.ArrayBindingPattern) { - writePunctuation("["); - emitList(node, node.elements, ts.ListFormat.ArrayBindingPatternElements); - writePunctuation("]"); - } - - function emitBindingElement(node: ts.BindingElement) { - emit(node.dotDotDotToken); - if (node.propertyName) { - emit(node.propertyName); - writePunctuation(":"); - writeSpace(); - } - emit(node.name); - emitInitializer(node.initializer, node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + writePunctuation(")"); + if (node.qualifier) { + writePunctuation("."); + emit(node.qualifier); } + emitTypeArguments(node, node.typeArguments); + } - // - // Expressions - // + // + // Binding patterns + // - function emitArrayLiteralExpression(node: ts.ArrayLiteralExpression) { - const elements = node.elements; - const preferNewLine = node.multiLine ? ts.ListFormat.PreferNewLine : ts.ListFormat.None; - emitExpressionList(node, elements, ts.ListFormat.ArrayLiteralExpressionElements | preferNewLine, parenthesizer.parenthesizeExpressionForDisallowedComma); - } - function emitObjectLiteralExpression(node: ts.ObjectLiteralExpression) { - ts.forEach(node.properties, generateMemberNames); - const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } + function emitObjectBindingPattern(node: ts.ObjectBindingPattern) { + writePunctuation("{"); + emitList(node, node.elements, ts.ListFormat.ObjectBindingPatternElements); + writePunctuation("}"); + } - const preferNewLine = node.multiLine ? ts.ListFormat.PreferNewLine : ts.ListFormat.None; - const allowTrailingComma = currentSourceFile && currentSourceFile.languageVersion >= ts.ScriptTarget.ES5 && !ts.isJsonSourceFile(currentSourceFile) ? ts.ListFormat.AllowTrailingComma : ts.ListFormat.None; - emitList(node, node.properties, ts.ListFormat.ObjectLiteralExpressionProperties | allowTrailingComma | preferNewLine); + function emitArrayBindingPattern(node: ts.ArrayBindingPattern) { + writePunctuation("["); + emitList(node, node.elements, ts.ListFormat.ArrayBindingPatternElements); + writePunctuation("]"); + } - if (indentedFlag) { - decreaseIndent(); - } + function emitBindingElement(node: ts.BindingElement) { + emit(node.dotDotDotToken); + if (node.propertyName) { + emit(node.propertyName); + writePunctuation(":"); + writeSpace(); } + emit(node.name); + emitInitializer(node.initializer, node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function emitPropertyAccessExpression(node: ts.PropertyAccessExpression) { - emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); - const token = node.questionDotToken || ts.setTextRangePosEnd(ts.factory.createToken(ts.SyntaxKind.DotToken) as ts.DotToken, node.expression.end, node.name.pos); - const linesBeforeDot = getLinesBetweenNodes(node, node.expression, token); - const linesAfterDot = getLinesBetweenNodes(node, token, node.name); - - writeLinesAndIndent(linesBeforeDot, /*writeSpaceIfNotIndenting*/ false); + // + // Expressions + // - const shouldEmitDotDot = token.kind !== ts.SyntaxKind.QuestionDotToken && - mayNeedDotDotForPropertyAccess(node.expression) && - !writer.hasTrailingComment() && - !writer.hasTrailingWhitespace(); + function emitArrayLiteralExpression(node: ts.ArrayLiteralExpression) { + const elements = node.elements; + const preferNewLine = node.multiLine ? ts.ListFormat.PreferNewLine : ts.ListFormat.None; + emitExpressionList(node, elements, ts.ListFormat.ArrayLiteralExpressionElements | preferNewLine, parenthesizer.parenthesizeExpressionForDisallowedComma); + } + function emitObjectLiteralExpression(node: ts.ObjectLiteralExpression) { + ts.forEach(node.properties, generateMemberNames); + const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); + } - if (shouldEmitDotDot) { - writePunctuation("."); - } + const preferNewLine = node.multiLine ? ts.ListFormat.PreferNewLine : ts.ListFormat.None; + const allowTrailingComma = currentSourceFile && currentSourceFile.languageVersion >= ts.ScriptTarget.ES5 && !ts.isJsonSourceFile(currentSourceFile) ? ts.ListFormat.AllowTrailingComma : ts.ListFormat.None; + emitList(node, node.properties, ts.ListFormat.ObjectLiteralExpressionProperties | allowTrailingComma | preferNewLine); - if (node.questionDotToken) { - emit(token); - } - else { - emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node); - } - writeLinesAndIndent(linesAfterDot, /*writeSpaceIfNotIndenting*/ false); - emit(node.name); - decreaseIndentIf(linesBeforeDot, linesAfterDot); - } - - // 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: ts.Expression) { - expression = ts.skipPartiallyEmittedExpressions(expression); - if (ts.isNumericLiteral(expression)) { - // check if numeric literal is a decimal literal that was originally written with a dot - const text = getLiteralTextOfNode(expression as ts.LiteralExpression, /*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 && !ts.stringContains(text, ts.tokenToString(ts.SyntaxKind.DotToken)!); - } - else if (ts.isAccessExpression(expression)) { - // check if constant enum value is integer - const constantValue = ts.getConstantValue(expression); - // isFinite handles cases when constantValue is undefined - return typeof constantValue === "number" && isFinite(constantValue) - && Math.floor(constantValue) === constantValue; - } + if (indentedFlag) { + decreaseIndent(); } + } - function emitElementAccessExpression(node: ts.ElementAccessExpression) { - emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); - emit(node.questionDotToken); - emitTokenWithComment(ts.SyntaxKind.OpenBracketToken, node.expression.end, writePunctuation, node); - emitExpression(node.argumentExpression); - emitTokenWithComment(ts.SyntaxKind.CloseBracketToken, node.argumentExpression.end, writePunctuation, node); - } + function emitPropertyAccessExpression(node: ts.PropertyAccessExpression) { + emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); + const token = node.questionDotToken || ts.setTextRangePosEnd(ts.factory.createToken(ts.SyntaxKind.DotToken) as ts.DotToken, node.expression.end, node.name.pos); + const linesBeforeDot = getLinesBetweenNodes(node, node.expression, token); + const linesAfterDot = getLinesBetweenNodes(node, token, node.name); - function emitCallExpression(node: ts.CallExpression) { - const indirectCall = ts.getEmitFlags(node) & ts.EmitFlags.IndirectCall; - if (indirectCall) { - writePunctuation("("); - writeLiteral("0"); - writePunctuation(","); - writeSpace(); - } - emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); - if (indirectCall) { - writePunctuation(")"); - } - emit(node.questionDotToken); - emitTypeArguments(node, node.typeArguments); - emitExpressionList(node, node.arguments, ts.ListFormat.CallExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma); - } + writeLinesAndIndent(linesBeforeDot, /*writeSpaceIfNotIndenting*/ false); - function emitNewExpression(node: ts.NewExpression) { - emitTokenWithComment(ts.SyntaxKind.NewKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfNew); - emitTypeArguments(node, node.typeArguments); - emitExpressionList(node, node.arguments, ts.ListFormat.NewExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma); - } + const shouldEmitDotDot = token.kind !== ts.SyntaxKind.QuestionDotToken && + mayNeedDotDotForPropertyAccess(node.expression) && + !writer.hasTrailingComment() && + !writer.hasTrailingWhitespace(); - function emitTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { - const indirectCall = ts.getEmitFlags(node) & ts.EmitFlags.IndirectCall; - if (indirectCall) { - writePunctuation("("); - writeLiteral("0"); - writePunctuation(","); - writeSpace(); - } - emitExpression(node.tag, parenthesizer.parenthesizeLeftSideOfAccess); - if (indirectCall) { - writePunctuation(")"); - } - emitTypeArguments(node, node.typeArguments); - writeSpace(); - emitExpression(node.template); + if (shouldEmitDotDot) { + writePunctuation("."); } - function emitTypeAssertionExpression(node: ts.TypeAssertion) { - writePunctuation("<"); - emit(node.type); - writePunctuation(">"); - emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + if (node.questionDotToken) { + emit(token); } - - function emitParenthesizedExpression(node: ts.ParenthesizedExpression) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.OpenParenToken, node.pos, writePunctuation, node); - const indented = writeLineSeparatorsAndIndentBefore(node.expression, node); - emitExpression(node.expression, /*parenthesizerRules*/ undefined); - writeLineSeparatorsAfter(node.expression, node); - decreaseIndentIf(indented); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression ? node.expression.end : openParenPos, writePunctuation, node); + else { + emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node); } + writeLinesAndIndent(linesAfterDot, /*writeSpaceIfNotIndenting*/ false); + emit(node.name); + decreaseIndentIf(linesBeforeDot, linesAfterDot); + } - function emitFunctionExpression(node: ts.FunctionExpression) { - generateNameIfNeeded(node.name); - emitFunctionDeclarationOrExpression(node); + // 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: ts.Expression) { + expression = ts.skipPartiallyEmittedExpressions(expression); + if (ts.isNumericLiteral(expression)) { + // check if numeric literal is a decimal literal that was originally written with a dot + const text = getLiteralTextOfNode(expression as ts.LiteralExpression, /*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 && !ts.stringContains(text, ts.tokenToString(ts.SyntaxKind.DotToken)!); + } + else if (ts.isAccessExpression(expression)) { + // check if constant enum value is integer + const constantValue = ts.getConstantValue(expression); + // isFinite handles cases when constantValue is undefined + return typeof constantValue === "number" && isFinite(constantValue) + && Math.floor(constantValue) === constantValue; } + } - function emitArrowFunction(node: ts.ArrowFunction) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitSignatureAndBody(node, emitArrowFunctionHead); - } + function emitElementAccessExpression(node: ts.ElementAccessExpression) { + emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); + emit(node.questionDotToken); + emitTokenWithComment(ts.SyntaxKind.OpenBracketToken, node.expression.end, writePunctuation, node); + emitExpression(node.argumentExpression); + emitTokenWithComment(ts.SyntaxKind.CloseBracketToken, node.argumentExpression.end, writePunctuation, node); + } - function emitArrowFunctionHead(node: ts.ArrowFunction) { - emitTypeParameters(node, node.typeParameters); - emitParametersForArrow(node, node.parameters); - emitTypeAnnotation(node.type); + function emitCallExpression(node: ts.CallExpression) { + const indirectCall = ts.getEmitFlags(node) & ts.EmitFlags.IndirectCall; + if (indirectCall) { + writePunctuation("("); + writeLiteral("0"); + writePunctuation(","); writeSpace(); - emit(node.equalsGreaterThanToken); } - - function emitDeleteExpression(node: ts.DeleteExpression) { - emitTokenWithComment(ts.SyntaxKind.DeleteKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); + if (indirectCall) { + writePunctuation(")"); } + emit(node.questionDotToken); + emitTypeArguments(node, node.typeArguments); + emitExpressionList(node, node.arguments, ts.ListFormat.CallExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function emitTypeOfExpression(node: ts.TypeOfExpression) { - emitTokenWithComment(ts.SyntaxKind.TypeOfKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); - } + function emitNewExpression(node: ts.NewExpression) { + emitTokenWithComment(ts.SyntaxKind.NewKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfNew); + emitTypeArguments(node, node.typeArguments); + emitExpressionList(node, node.arguments, ts.ListFormat.NewExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function emitVoidExpression(node: ts.VoidExpression) { - emitTokenWithComment(ts.SyntaxKind.VoidKeyword, node.pos, writeKeyword, node); + function emitTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { + const indirectCall = ts.getEmitFlags(node) & ts.EmitFlags.IndirectCall; + if (indirectCall) { + writePunctuation("("); + writeLiteral("0"); + writePunctuation(","); writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); } - - function emitAwaitExpression(node: ts.AwaitExpression) { - emitTokenWithComment(ts.SyntaxKind.AwaitKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + emitExpression(node.tag, parenthesizer.parenthesizeLeftSideOfAccess); + if (indirectCall) { + writePunctuation(")"); } + emitTypeArguments(node, node.typeArguments); + writeSpace(); + emitExpression(node.template); + } - function emitPrefixUnaryExpression(node: ts.PrefixUnaryExpression) { - writeTokenText(node.operator, writeOperator); - if (shouldEmitWhitespaceBeforeOperand(node)) { - writeSpace(); - } - emitExpression(node.operand, parenthesizer.parenthesizeOperandOfPrefixUnary); - } + function emitTypeAssertionExpression(node: ts.TypeAssertion) { + writePunctuation("<"); + emit(node.type); + writePunctuation(">"); + emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - function shouldEmitWhitespaceBeforeOperand(node: ts.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 === ts.SyntaxKind.PrefixUnaryExpression - && ((node.operator === ts.SyntaxKind.PlusToken && ((operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusToken || (operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusPlusToken)) - || (node.operator === ts.SyntaxKind.MinusToken && ((operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken || (operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusMinusToken))); - } - - function emitPostfixUnaryExpression(node: ts.PostfixUnaryExpression) { - emitExpression(node.operand, parenthesizer.parenthesizeOperandOfPostfixUnary); - writeTokenText(node.operator, writeOperator); - } - - function createEmitBinaryExpression() { - interface WorkArea { - stackIndex: number; - preserveSourceNewlinesStack: (boolean | undefined)[]; - containerPosStack: number[]; - containerEndStack: number[]; - declarationListContainerEndStack: number[]; - shouldEmitCommentsStack: boolean[]; - shouldEmitSourceMapsStack: boolean[]; - } + function emitParenthesizedExpression(node: ts.ParenthesizedExpression) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.OpenParenToken, node.pos, writePunctuation, node); + const indented = writeLineSeparatorsAndIndentBefore(node.expression, node); + emitExpression(node.expression, /*parenthesizerRules*/ undefined); + writeLineSeparatorsAfter(node.expression, node); + decreaseIndentIf(indented); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression ? node.expression.end : openParenPos, writePunctuation, node); + } - return ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, /*foldState*/ undefined); - function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined) { - if (state) { - state.stackIndex++; - state.preserveSourceNewlinesStack[state.stackIndex] = preserveSourceNewlines; - state.containerPosStack[state.stackIndex] = containerPos; - state.containerEndStack[state.stackIndex] = containerEnd; - state.declarationListContainerEndStack[state.stackIndex] = declarationListContainerEnd; - const emitComments = state.shouldEmitCommentsStack[state.stackIndex] = shouldEmitComments(node); - const emitSourceMaps = state.shouldEmitSourceMapsStack[state.stackIndex] = shouldEmitSourceMaps(node); - onBeforeEmitNode?.(node); - if (emitComments) - emitCommentsBeforeNode(node); - if (emitSourceMaps) - emitSourceMapsBeforeNode(node); - beforeEmitNode(node); - } - else { - state = { - stackIndex: 0, - preserveSourceNewlinesStack: [undefined], - containerPosStack: [-1], - containerEndStack: [-1], - declarationListContainerEndStack: [-1], - shouldEmitCommentsStack: [false], - shouldEmitSourceMapsStack: [false], - }; - } - return state; - } + function emitFunctionExpression(node: ts.FunctionExpression) { + generateNameIfNeeded(node.name); + emitFunctionDeclarationOrExpression(node); + } - function onLeft(next: ts.Expression, _workArea: WorkArea, parent: ts.BinaryExpression) { - return maybeEmitExpression(next, parent, "left"); - } + function emitArrowFunction(node: ts.ArrowFunction) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitSignatureAndBody(node, emitArrowFunctionHead); + } - function onOperator(operatorToken: ts.BinaryOperatorToken, _state: WorkArea, node: ts.BinaryExpression) { - const isCommaOperator = operatorToken.kind !== ts.SyntaxKind.CommaToken; - const linesBeforeOperator = getLinesBetweenNodes(node, node.left, operatorToken); - const linesAfterOperator = getLinesBetweenNodes(node, operatorToken, node.right); - writeLinesAndIndent(linesBeforeOperator, isCommaOperator); - emitLeadingCommentsOfPosition(operatorToken.pos); - writeTokenNode(operatorToken, operatorToken.kind === ts.SyntaxKind.InKeyword ? writeKeyword : writeOperator); - emitTrailingCommentsOfPosition(operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts - writeLinesAndIndent(linesAfterOperator, /*writeSpaceIfNotIndenting*/ true); - } + function emitArrowFunctionHead(node: ts.ArrowFunction) { + emitTypeParameters(node, node.typeParameters); + emitParametersForArrow(node, node.parameters); + emitTypeAnnotation(node.type); + writeSpace(); + emit(node.equalsGreaterThanToken); + } - function onRight(next: ts.Expression, _workArea: WorkArea, parent: ts.BinaryExpression) { - return maybeEmitExpression(next, parent, "right"); - } + function emitDeleteExpression(node: ts.DeleteExpression) { + emitTokenWithComment(ts.SyntaxKind.DeleteKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - function onExit(node: ts.BinaryExpression, state: WorkArea) { - const linesBeforeOperator = getLinesBetweenNodes(node, node.left, node.operatorToken); - const linesAfterOperator = getLinesBetweenNodes(node, node.operatorToken, node.right); - decreaseIndentIf(linesBeforeOperator, linesAfterOperator); - if (state.stackIndex > 0) { - const savedPreserveSourceNewlines = state.preserveSourceNewlinesStack[state.stackIndex]; - const savedContainerPos = state.containerPosStack[state.stackIndex]; - const savedContainerEnd = state.containerEndStack[state.stackIndex]; - const savedDeclarationListContainerEnd = state.declarationListContainerEndStack[state.stackIndex]; - const shouldEmitComments = state.shouldEmitCommentsStack[state.stackIndex]; - const shouldEmitSourceMaps = state.shouldEmitSourceMapsStack[state.stackIndex]; - afterEmitNode(savedPreserveSourceNewlines); - if (shouldEmitSourceMaps) - emitSourceMapsAfterNode(node); - if (shouldEmitComments) - emitCommentsAfterNode(node, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); - onAfterEmitNode?.(node); - state.stackIndex--; - } - } + function emitTypeOfExpression(node: ts.TypeOfExpression) { + emitTokenWithComment(ts.SyntaxKind.TypeOfKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - function maybeEmitExpression(next: ts.Expression, parent: ts.BinaryExpression, side: "left" | "right") { - const parenthesizerRule = side === "left" ? - parenthesizer.getParenthesizeLeftSideOfBinaryForOperator(parent.operatorToken.kind) : - parenthesizer.getParenthesizeRightSideOfBinaryForOperator(parent.operatorToken.kind); - - let pipelinePhase = getPipelinePhase(PipelinePhase.Notification, ts.EmitHint.Expression, next); - if (pipelinePhase === pipelineEmitWithSubstitution) { - ts.Debug.assertIsDefined(lastSubstitution); - next = parenthesizerRule(ts.cast(lastSubstitution, ts.isExpression)); - pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, ts.EmitHint.Expression, next); - lastSubstitution = undefined; - } + function emitVoidExpression(node: ts.VoidExpression) { + emitTokenWithComment(ts.SyntaxKind.VoidKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - if (pipelinePhase === pipelineEmitWithComments || - pipelinePhase === pipelineEmitWithSourceMaps || - pipelinePhase === pipelineEmitWithHint) { - if (ts.isBinaryExpression(next)) { - return next; - } - } + function emitAwaitExpression(node: ts.AwaitExpression) { + emitTokenWithComment(ts.SyntaxKind.AwaitKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - currentParenthesizerRule = parenthesizerRule; - pipelinePhase(ts.EmitHint.Expression, next); - } + function emitPrefixUnaryExpression(node: ts.PrefixUnaryExpression) { + writeTokenText(node.operator, writeOperator); + if (shouldEmitWhitespaceBeforeOperand(node)) { + writeSpace(); } + emitExpression(node.operand, parenthesizer.parenthesizeOperandOfPrefixUnary); + } - function emitConditionalExpression(node: ts.ConditionalExpression) { - const linesBeforeQuestion = getLinesBetweenNodes(node, node.condition, node.questionToken); - const linesAfterQuestion = getLinesBetweenNodes(node, node.questionToken, node.whenTrue); - const linesBeforeColon = getLinesBetweenNodes(node, node.whenTrue, node.colonToken); - const linesAfterColon = getLinesBetweenNodes(node, node.colonToken, node.whenFalse); + function shouldEmitWhitespaceBeforeOperand(node: ts.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 === ts.SyntaxKind.PrefixUnaryExpression + && ((node.operator === ts.SyntaxKind.PlusToken && ((operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusToken || (operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusPlusToken)) + || (node.operator === ts.SyntaxKind.MinusToken && ((operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken || (operand as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusMinusToken))); + } - emitExpression(node.condition, parenthesizer.parenthesizeConditionOfConditionalExpression); - writeLinesAndIndent(linesBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); - emit(node.questionToken); - writeLinesAndIndent(linesAfterQuestion, /*writeSpaceIfNotIndenting*/ true); - emitExpression(node.whenTrue, parenthesizer.parenthesizeBranchOfConditionalExpression); - decreaseIndentIf(linesBeforeQuestion, linesAfterQuestion); + function emitPostfixUnaryExpression(node: ts.PostfixUnaryExpression) { + emitExpression(node.operand, parenthesizer.parenthesizeOperandOfPostfixUnary); + writeTokenText(node.operator, writeOperator); + } - writeLinesAndIndent(linesBeforeColon, /*writeSpaceIfNotIndenting*/ true); - emit(node.colonToken); - writeLinesAndIndent(linesAfterColon, /*writeSpaceIfNotIndenting*/ true); - emitExpression(node.whenFalse, parenthesizer.parenthesizeBranchOfConditionalExpression); - decreaseIndentIf(linesBeforeColon, linesAfterColon); + function createEmitBinaryExpression() { + interface WorkArea { + stackIndex: number; + preserveSourceNewlinesStack: (boolean | undefined)[]; + containerPosStack: number[]; + containerEndStack: number[]; + declarationListContainerEndStack: number[]; + shouldEmitCommentsStack: boolean[]; + shouldEmitSourceMapsStack: boolean[]; + } + + return ts.createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, /*foldState*/ undefined); + function onEnter(node: ts.BinaryExpression, state: WorkArea | undefined) { + if (state) { + state.stackIndex++; + state.preserveSourceNewlinesStack[state.stackIndex] = preserveSourceNewlines; + state.containerPosStack[state.stackIndex] = containerPos; + state.containerEndStack[state.stackIndex] = containerEnd; + state.declarationListContainerEndStack[state.stackIndex] = declarationListContainerEnd; + const emitComments = state.shouldEmitCommentsStack[state.stackIndex] = shouldEmitComments(node); + const emitSourceMaps = state.shouldEmitSourceMapsStack[state.stackIndex] = shouldEmitSourceMaps(node); + onBeforeEmitNode?.(node); + if (emitComments) + emitCommentsBeforeNode(node); + if (emitSourceMaps) + emitSourceMapsBeforeNode(node); + beforeEmitNode(node); + } + else { + state = { + stackIndex: 0, + preserveSourceNewlinesStack: [undefined], + containerPosStack: [-1], + containerEndStack: [-1], + declarationListContainerEndStack: [-1], + shouldEmitCommentsStack: [false], + shouldEmitSourceMapsStack: [false], + }; + } + return state; } - function emitTemplateExpression(node: ts.TemplateExpression) { - emit(node.head); - emitList(node, node.templateSpans, ts.ListFormat.TemplateExpressionSpans); + function onLeft(next: ts.Expression, _workArea: WorkArea, parent: ts.BinaryExpression) { + return maybeEmitExpression(next, parent, "left"); } - function emitYieldExpression(node: ts.YieldExpression) { - emitTokenWithComment(ts.SyntaxKind.YieldKeyword, node.pos, writeKeyword, node); - emit(node.asteriskToken); - emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsiAndDisallowedComma); + function onOperator(operatorToken: ts.BinaryOperatorToken, _state: WorkArea, node: ts.BinaryExpression) { + const isCommaOperator = operatorToken.kind !== ts.SyntaxKind.CommaToken; + const linesBeforeOperator = getLinesBetweenNodes(node, node.left, operatorToken); + const linesAfterOperator = getLinesBetweenNodes(node, operatorToken, node.right); + writeLinesAndIndent(linesBeforeOperator, isCommaOperator); + emitLeadingCommentsOfPosition(operatorToken.pos); + writeTokenNode(operatorToken, operatorToken.kind === ts.SyntaxKind.InKeyword ? writeKeyword : writeOperator); + emitTrailingCommentsOfPosition(operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts + writeLinesAndIndent(linesAfterOperator, /*writeSpaceIfNotIndenting*/ true); } - function emitSpreadElement(node: ts.SpreadElement) { - emitTokenWithComment(ts.SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); - emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); + function onRight(next: ts.Expression, _workArea: WorkArea, parent: ts.BinaryExpression) { + return maybeEmitExpression(next, parent, "right"); } - function emitClassExpression(node: ts.ClassExpression) { - generateNameIfNeeded(node.name); - emitClassDeclarationOrExpression(node); + function onExit(node: ts.BinaryExpression, state: WorkArea) { + const linesBeforeOperator = getLinesBetweenNodes(node, node.left, node.operatorToken); + const linesAfterOperator = getLinesBetweenNodes(node, node.operatorToken, node.right); + decreaseIndentIf(linesBeforeOperator, linesAfterOperator); + if (state.stackIndex > 0) { + const savedPreserveSourceNewlines = state.preserveSourceNewlinesStack[state.stackIndex]; + const savedContainerPos = state.containerPosStack[state.stackIndex]; + const savedContainerEnd = state.containerEndStack[state.stackIndex]; + const savedDeclarationListContainerEnd = state.declarationListContainerEndStack[state.stackIndex]; + const shouldEmitComments = state.shouldEmitCommentsStack[state.stackIndex]; + const shouldEmitSourceMaps = state.shouldEmitSourceMapsStack[state.stackIndex]; + afterEmitNode(savedPreserveSourceNewlines); + if (shouldEmitSourceMaps) + emitSourceMapsAfterNode(node); + if (shouldEmitComments) + emitCommentsAfterNode(node, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); + onAfterEmitNode?.(node); + state.stackIndex--; + } } - function emitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments) { - emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); - emitTypeArguments(node, node.typeArguments); - } + function maybeEmitExpression(next: ts.Expression, parent: ts.BinaryExpression, side: "left" | "right") { + const parenthesizerRule = side === "left" ? + parenthesizer.getParenthesizeLeftSideOfBinaryForOperator(parent.operatorToken.kind) : + parenthesizer.getParenthesizeRightSideOfBinaryForOperator(parent.operatorToken.kind); - function emitAsExpression(node: ts.AsExpression) { - emitExpression(node.expression, /*parenthesizerRules*/ undefined); - if (node.type) { - writeSpace(); - writeKeyword("as"); - writeSpace(); - emit(node.type); + let pipelinePhase = getPipelinePhase(PipelinePhase.Notification, ts.EmitHint.Expression, next); + if (pipelinePhase === pipelineEmitWithSubstitution) { + ts.Debug.assertIsDefined(lastSubstitution); + next = parenthesizerRule(ts.cast(lastSubstitution, ts.isExpression)); + pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, ts.EmitHint.Expression, next); + lastSubstitution = undefined; } - } - function emitNonNullExpression(node: ts.NonNullExpression) { - emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); - writeOperator("!"); - } + if (pipelinePhase === pipelineEmitWithComments || + pipelinePhase === pipelineEmitWithSourceMaps || + pipelinePhase === pipelineEmitWithHint) { + if (ts.isBinaryExpression(next)) { + return next; + } + } - function emitMetaProperty(node: ts.MetaProperty) { - writeToken(node.keywordToken, node.pos, writePunctuation); - writePunctuation("."); - emit(node.name); + currentParenthesizerRule = parenthesizerRule; + pipelinePhase(ts.EmitHint.Expression, next); } + } - // - // Misc - // + function emitConditionalExpression(node: ts.ConditionalExpression) { + const linesBeforeQuestion = getLinesBetweenNodes(node, node.condition, node.questionToken); + const linesAfterQuestion = getLinesBetweenNodes(node, node.questionToken, node.whenTrue); + const linesBeforeColon = getLinesBetweenNodes(node, node.whenTrue, node.colonToken); + const linesAfterColon = getLinesBetweenNodes(node, node.colonToken, node.whenFalse); + + emitExpression(node.condition, parenthesizer.parenthesizeConditionOfConditionalExpression); + writeLinesAndIndent(linesBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); + emit(node.questionToken); + writeLinesAndIndent(linesAfterQuestion, /*writeSpaceIfNotIndenting*/ true); + emitExpression(node.whenTrue, parenthesizer.parenthesizeBranchOfConditionalExpression); + decreaseIndentIf(linesBeforeQuestion, linesAfterQuestion); + + writeLinesAndIndent(linesBeforeColon, /*writeSpaceIfNotIndenting*/ true); + emit(node.colonToken); + writeLinesAndIndent(linesAfterColon, /*writeSpaceIfNotIndenting*/ true); + emitExpression(node.whenFalse, parenthesizer.parenthesizeBranchOfConditionalExpression); + decreaseIndentIf(linesBeforeColon, linesAfterColon); + } - function emitTemplateSpan(node: ts.TemplateSpan) { - emitExpression(node.expression); - emit(node.literal); - } + function emitTemplateExpression(node: ts.TemplateExpression) { + emit(node.head); + emitList(node, node.templateSpans, ts.ListFormat.TemplateExpressionSpans); + } - // - // Statements - // + function emitYieldExpression(node: ts.YieldExpression) { + emitTokenWithComment(ts.SyntaxKind.YieldKeyword, node.pos, writeKeyword, node); + emit(node.asteriskToken); + emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsiAndDisallowedComma); + } - function emitBlock(node: ts.Block) { - emitBlockStatements(node, /*forceSingleLine*/ !node.multiLine && isEmptyBlock(node)); - } + function emitSpreadElement(node: ts.SpreadElement) { + emitTokenWithComment(ts.SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); + emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function emitBlockStatements(node: ts.BlockLike, forceSingleLine: boolean) { - emitTokenWithComment(ts.SyntaxKind.OpenBraceToken, node.pos, writePunctuation, /*contextNode*/ node); - const format = forceSingleLine || ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineBlockStatements : ts.ListFormat.MultiLineBlockStatements; - emitList(node, node.statements, format); - emitTokenWithComment(ts.SyntaxKind.CloseBraceToken, node.statements.end, writePunctuation, /*contextNode*/ node, /*indentLeading*/ !!(format & ts.ListFormat.MultiLine)); - } + function emitClassExpression(node: ts.ClassExpression) { + generateNameIfNeeded(node.name); + emitClassDeclarationOrExpression(node); + } - function emitVariableStatement(node: ts.VariableStatement) { - emitModifiers(node, node.modifiers); - emit(node.declarationList); - writeTrailingSemicolon(); - } + function emitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments) { + emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); + emitTypeArguments(node, node.typeArguments); + } - 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(";"); - } - else { - writeTrailingSemicolon(); - } + function emitAsExpression(node: ts.AsExpression) { + emitExpression(node.expression, /*parenthesizerRules*/ undefined); + if (node.type) { + writeSpace(); + writeKeyword("as"); + writeSpace(); + emit(node.type); } + } - function emitExpressionStatement(node: ts.ExpressionStatement) { - emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfExpressionStatement); - // 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 (!currentSourceFile || !ts.isJsonSourceFile(currentSourceFile) || ts.nodeIsSynthesized(node.expression)) { - writeTrailingSemicolon(); - } - } + function emitNonNullExpression(node: ts.NonNullExpression) { + emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess); + writeOperator("!"); + } - function emitIfStatement(node: ts.IfStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.IfKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.thenStatement); - if (node.elseStatement) { - writeLineOrSpace(node, node.thenStatement, node.elseStatement); - emitTokenWithComment(ts.SyntaxKind.ElseKeyword, node.thenStatement.end, writeKeyword, node); - if (node.elseStatement.kind === ts.SyntaxKind.IfStatement) { - writeSpace(); - emit(node.elseStatement); - } - else { - emitEmbeddedStatement(node, node.elseStatement); - } - } - } + function emitMetaProperty(node: ts.MetaProperty) { + writeToken(node.keywordToken, node.pos, writePunctuation); + writePunctuation("."); + emit(node.name); + } - function emitWhileClause(node: ts.WhileStatement | ts.DoStatement, startPos: number) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.WhileKeyword, startPos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - } + // + // Misc + // - function emitDoStatement(node: ts.DoStatement) { - emitTokenWithComment(ts.SyntaxKind.DoKeyword, node.pos, writeKeyword, node); - emitEmbeddedStatement(node, node.statement); - if (ts.isBlock(node.statement) && !preserveSourceNewlines) { - writeSpace(); - } - else { - writeLineOrSpace(node, node.statement, node.expression); - } + function emitTemplateSpan(node: ts.TemplateSpan) { + emitExpression(node.expression); + emit(node.literal); + } - emitWhileClause(node, node.statement.end); - writeTrailingSemicolon(); - } + // + // Statements + // - function emitWhileStatement(node: ts.WhileStatement) { - emitWhileClause(node, node.pos); - emitEmbeddedStatement(node, node.statement); - } + function emitBlock(node: ts.Block) { + emitBlockStatements(node, /*forceSingleLine*/ !node.multiLine && isEmptyBlock(node)); + } - function emitForStatement(node: ts.ForStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - let pos = emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, /*contextNode*/ node); - emitForBinding(node.initializer); - pos = emitTokenWithComment(ts.SyntaxKind.SemicolonToken, node.initializer ? node.initializer.end : pos, writePunctuation, node); - emitExpressionWithLeadingSpace(node.condition); - pos = emitTokenWithComment(ts.SyntaxKind.SemicolonToken, node.condition ? node.condition.end : pos, writePunctuation, node); - emitExpressionWithLeadingSpace(node.incrementor); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.incrementor ? node.incrementor.end : pos, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); - } - - function emitForInStatement(node: ts.ForInStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitForBinding(node.initializer); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.InKeyword, node.initializer.end, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); - } + function emitBlockStatements(node: ts.BlockLike, forceSingleLine: boolean) { + emitTokenWithComment(ts.SyntaxKind.OpenBraceToken, node.pos, writePunctuation, /*contextNode*/ node); + const format = forceSingleLine || ts.getEmitFlags(node) & ts.EmitFlags.SingleLine ? ts.ListFormat.SingleLineBlockStatements : ts.ListFormat.MultiLineBlockStatements; + emitList(node, node.statements, format); + emitTokenWithComment(ts.SyntaxKind.CloseBraceToken, node.statements.end, writePunctuation, /*contextNode*/ node, /*indentLeading*/ !!(format & ts.ListFormat.MultiLine)); + } - function emitForOfStatement(node: ts.ForOfStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitWithTrailingSpace(node.awaitModifier); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitForBinding(node.initializer); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OfKeyword, node.initializer.end, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); - } + function emitVariableStatement(node: ts.VariableStatement) { + emitModifiers(node, node.modifiers); + emit(node.declarationList); + writeTrailingSemicolon(); + } - function emitForBinding(node: ts.VariableDeclarationList | ts.Expression | undefined) { - if (node !== undefined) { - if (node.kind === ts.SyntaxKind.VariableDeclarationList) { - emit(node); - } - else { - emitExpression(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(";"); } - - function emitContinueStatement(node: ts.ContinueStatement) { - emitTokenWithComment(ts.SyntaxKind.ContinueKeyword, node.pos, writeKeyword, node); - emitWithLeadingSpace(node.label); + else { writeTrailingSemicolon(); } + } - function emitBreakStatement(node: ts.BreakStatement) { - emitTokenWithComment(ts.SyntaxKind.BreakKeyword, node.pos, writeKeyword, node); - emitWithLeadingSpace(node.label); + function emitExpressionStatement(node: ts.ExpressionStatement) { + emitExpression(node.expression, parenthesizer.parenthesizeExpressionOfExpressionStatement); + // 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 (!currentSourceFile || !ts.isJsonSourceFile(currentSourceFile) || ts.nodeIsSynthesized(node.expression)) { writeTrailingSemicolon(); } + } - function emitTokenWithComment(token: ts.SyntaxKind, pos: number, writer: (s: string) => void, contextNode: ts.Node, indentLeading?: boolean) { - const node = ts.getParseTreeNode(contextNode); - const isSimilarNode = node && node.kind === contextNode.kind; - const startPos = pos; - if (isSimilarNode && currentSourceFile) { - pos = ts.skipTrivia(currentSourceFile.text, pos); - } - if (isSimilarNode && contextNode.pos !== startPos) { - const needsIndent = indentLeading && currentSourceFile && !ts.positionsAreOnSameLine(startPos, pos, currentSourceFile); - if (needsIndent) { - increaseIndent(); - } - emitLeadingCommentsOfPosition(startPos); - if (needsIndent) { - decreaseIndent(); - } + function emitIfStatement(node: ts.IfStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.IfKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.thenStatement); + if (node.elseStatement) { + writeLineOrSpace(node, node.thenStatement, node.elseStatement); + emitTokenWithComment(ts.SyntaxKind.ElseKeyword, node.thenStatement.end, writeKeyword, node); + if (node.elseStatement.kind === ts.SyntaxKind.IfStatement) { + writeSpace(); + emit(node.elseStatement); } - pos = writeTokenText(token, writer, pos); - if (isSimilarNode && contextNode.end !== pos) { - const isJsxExprContext = contextNode.kind === ts.SyntaxKind.JsxExpression; - emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); + else { + emitEmbeddedStatement(node, node.elseStatement); } - return pos; } + } - function commentWillEmitNewLine(node: ts.CommentRange) { - return node.kind === ts.SyntaxKind.SingleLineCommentTrivia || !!node.hasTrailingNewLine; - } + function emitWhileClause(node: ts.WhileStatement | ts.DoStatement, startPos: number) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.WhileKeyword, startPos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + } - function willEmitLeadingNewLine(node: ts.Expression): boolean { - if (!currentSourceFile) - return false; - if (ts.some(ts.getLeadingCommentRanges(currentSourceFile.text, node.pos), commentWillEmitNewLine)) - return true; - if (ts.some(ts.getSyntheticLeadingComments(node), commentWillEmitNewLine)) - return true; - if (ts.isPartiallyEmittedExpression(node)) { - if (node.pos !== node.expression.pos) { - if (ts.some(ts.getTrailingCommentRanges(currentSourceFile.text, node.expression.pos), commentWillEmitNewLine)) - return true; - } - return willEmitLeadingNewLine(node.expression); - } - return false; + function emitDoStatement(node: ts.DoStatement) { + emitTokenWithComment(ts.SyntaxKind.DoKeyword, node.pos, writeKeyword, node); + emitEmbeddedStatement(node, node.statement); + if (ts.isBlock(node.statement) && !preserveSourceNewlines) { + writeSpace(); } - - /** - * Wraps an expression in parens if we would emit a leading comment that would introduce a line separator - * between the node and its parent. - */ - function parenthesizeExpressionForNoAsi(node: ts.Expression) { - if (!commentsDisabled && ts.isPartiallyEmittedExpression(node) && willEmitLeadingNewLine(node)) { - const parseNode = ts.getParseTreeNode(node); - if (parseNode && ts.isParenthesizedExpression(parseNode)) { - // If the original node was a parenthesized expression, restore it to preserve comment and source map emit - const parens = ts.factory.createParenthesizedExpression(node.expression); - ts.setOriginalNode(parens, node); - ts.setTextRange(parens, parseNode); - return parens; - } - return ts.factory.createParenthesizedExpression(node); - } - return node; + else { + writeLineOrSpace(node, node.statement, node.expression); } - function parenthesizeExpressionForNoAsiAndDisallowedComma(node: ts.Expression) { - return parenthesizeExpressionForNoAsi(parenthesizer.parenthesizeExpressionForDisallowedComma(node)); - } + emitWhileClause(node, node.statement.end); + writeTrailingSemicolon(); + } - function emitReturnStatement(node: ts.ReturnStatement) { - emitTokenWithComment(ts.SyntaxKind.ReturnKeyword, node.pos, writeKeyword, /*contextNode*/ node); - emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi); - writeTrailingSemicolon(); - } + function emitWhileStatement(node: ts.WhileStatement) { + emitWhileClause(node, node.pos); + emitEmbeddedStatement(node, node.statement); + } - function emitWithStatement(node: ts.WithStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.WithKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); - } + function emitForStatement(node: ts.ForStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + let pos = emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, /*contextNode*/ node); + emitForBinding(node.initializer); + pos = emitTokenWithComment(ts.SyntaxKind.SemicolonToken, node.initializer ? node.initializer.end : pos, writePunctuation, node); + emitExpressionWithLeadingSpace(node.condition); + pos = emitTokenWithComment(ts.SyntaxKind.SemicolonToken, node.condition ? node.condition.end : pos, writePunctuation, node); + emitExpressionWithLeadingSpace(node.incrementor); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.incrementor ? node.incrementor.end : pos, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } - function emitSwitchStatement(node: ts.SwitchStatement) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.SwitchKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - writeSpace(); - emit(node.caseBlock); - } + function emitForInStatement(node: ts.ForInStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitForBinding(node.initializer); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.InKeyword, node.initializer.end, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } - function emitLabeledStatement(node: ts.LabeledStatement) { - emit(node.label); - emitTokenWithComment(ts.SyntaxKind.ColonToken, node.label.end, writePunctuation, node); - writeSpace(); - emit(node.statement); + function emitForOfStatement(node: ts.ForOfStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitWithTrailingSpace(node.awaitModifier); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitForBinding(node.initializer); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OfKeyword, node.initializer.end, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + + function emitForBinding(node: ts.VariableDeclarationList | ts.Expression | undefined) { + if (node !== undefined) { + if (node.kind === ts.SyntaxKind.VariableDeclarationList) { + emit(node); + } + else { + emitExpression(node); + } } + } + + function emitContinueStatement(node: ts.ContinueStatement) { + emitTokenWithComment(ts.SyntaxKind.ContinueKeyword, node.pos, writeKeyword, node); + emitWithLeadingSpace(node.label); + writeTrailingSemicolon(); + } + + function emitBreakStatement(node: ts.BreakStatement) { + emitTokenWithComment(ts.SyntaxKind.BreakKeyword, node.pos, writeKeyword, node); + emitWithLeadingSpace(node.label); + writeTrailingSemicolon(); + } - function emitThrowStatement(node: ts.ThrowStatement) { - emitTokenWithComment(ts.SyntaxKind.ThrowKeyword, node.pos, writeKeyword, node); - emitExpressionWithLeadingSpace(parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi); - writeTrailingSemicolon(); + function emitTokenWithComment(token: ts.SyntaxKind, pos: number, writer: (s: string) => void, contextNode: ts.Node, indentLeading?: boolean) { + const node = ts.getParseTreeNode(contextNode); + const isSimilarNode = node && node.kind === contextNode.kind; + const startPos = pos; + if (isSimilarNode && currentSourceFile) { + pos = ts.skipTrivia(currentSourceFile.text, pos); } - - function emitTryStatement(node: ts.TryStatement) { - emitTokenWithComment(ts.SyntaxKind.TryKeyword, node.pos, writeKeyword, node); - writeSpace(); - emit(node.tryBlock); - if (node.catchClause) { - writeLineOrSpace(node, node.tryBlock, node.catchClause); - emit(node.catchClause); + if (isSimilarNode && contextNode.pos !== startPos) { + const needsIndent = indentLeading && currentSourceFile && !ts.positionsAreOnSameLine(startPos, pos, currentSourceFile); + if (needsIndent) { + increaseIndent(); } - if (node.finallyBlock) { - writeLineOrSpace(node, node.catchClause || node.tryBlock, node.finallyBlock); - emitTokenWithComment(ts.SyntaxKind.FinallyKeyword, (node.catchClause || node.tryBlock).end, writeKeyword, node); - writeSpace(); - emit(node.finallyBlock); + emitLeadingCommentsOfPosition(startPos); + if (needsIndent) { + decreaseIndent(); } } - - function emitDebuggerStatement(node: ts.DebuggerStatement) { - writeToken(ts.SyntaxKind.DebuggerKeyword, node.pos, writeKeyword); - writeTrailingSemicolon(); + pos = writeTokenText(token, writer, pos); + if (isSimilarNode && contextNode.end !== pos) { + const isJsxExprContext = contextNode.kind === ts.SyntaxKind.JsxExpression; + emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); } + return pos; + } - // - // Declarations - // + function commentWillEmitNewLine(node: ts.CommentRange) { + return node.kind === ts.SyntaxKind.SingleLineCommentTrivia || !!node.hasTrailingNewLine; + } - function emitVariableDeclaration(node: ts.VariableDeclaration) { - emit(node.name); - emit(node.exclamationToken); - emitTypeAnnotation(node.type); - emitInitializer(node.initializer, node.type?.end ?? node.name.emitNode?.typeNode?.end ?? node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + function willEmitLeadingNewLine(node: ts.Expression): boolean { + if (!currentSourceFile) + return false; + if (ts.some(ts.getLeadingCommentRanges(currentSourceFile.text, node.pos), commentWillEmitNewLine)) + return true; + if (ts.some(ts.getSyntheticLeadingComments(node), commentWillEmitNewLine)) + return true; + if (ts.isPartiallyEmittedExpression(node)) { + if (node.pos !== node.expression.pos) { + if (ts.some(ts.getTrailingCommentRanges(currentSourceFile.text, node.expression.pos), commentWillEmitNewLine)) + return true; + } + return willEmitLeadingNewLine(node.expression); } + return false; + } - function emitVariableDeclarationList(node: ts.VariableDeclarationList) { - writeKeyword(ts.isLet(node) ? "let" : ts.isVarConst(node) ? "const" : "var"); - writeSpace(); - emitList(node, node.declarations, ts.ListFormat.VariableDeclarationList); - } + /** + * Wraps an expression in parens if we would emit a leading comment that would introduce a line separator + * between the node and its parent. + */ + function parenthesizeExpressionForNoAsi(node: ts.Expression) { + if (!commentsDisabled && ts.isPartiallyEmittedExpression(node) && willEmitLeadingNewLine(node)) { + const parseNode = ts.getParseTreeNode(node); + if (parseNode && ts.isParenthesizedExpression(parseNode)) { + // If the original node was a parenthesized expression, restore it to preserve comment and source map emit + const parens = ts.factory.createParenthesizedExpression(node.expression); + ts.setOriginalNode(parens, node); + ts.setTextRange(parens, parseNode); + return parens; + } + return ts.factory.createParenthesizedExpression(node); + } + return node; + } - function emitFunctionDeclaration(node: ts.FunctionDeclaration) { - emitFunctionDeclarationOrExpression(node); - } + function parenthesizeExpressionForNoAsiAndDisallowedComma(node: ts.Expression) { + return parenthesizeExpressionForNoAsi(parenthesizer.parenthesizeExpressionForDisallowedComma(node)); + } + + function emitReturnStatement(node: ts.ReturnStatement) { + emitTokenWithComment(ts.SyntaxKind.ReturnKeyword, node.pos, writeKeyword, /*contextNode*/ node); + emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi); + writeTrailingSemicolon(); + } + + function emitWithStatement(node: ts.WithStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.WithKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + + function emitSwitchStatement(node: ts.SwitchStatement) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.SwitchKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + writeSpace(); + emit(node.caseBlock); + } - function emitFunctionDeclarationOrExpression(node: ts.FunctionDeclaration | ts.FunctionExpression) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("function"); - emit(node.asteriskToken); + function emitLabeledStatement(node: ts.LabeledStatement) { + emit(node.label); + emitTokenWithComment(ts.SyntaxKind.ColonToken, node.label.end, writePunctuation, node); + writeSpace(); + emit(node.statement); + } + + function emitThrowStatement(node: ts.ThrowStatement) { + emitTokenWithComment(ts.SyntaxKind.ThrowKeyword, node.pos, writeKeyword, node); + emitExpressionWithLeadingSpace(parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi); + writeTrailingSemicolon(); + } + + function emitTryStatement(node: ts.TryStatement) { + emitTokenWithComment(ts.SyntaxKind.TryKeyword, node.pos, writeKeyword, node); + writeSpace(); + emit(node.tryBlock); + if (node.catchClause) { + writeLineOrSpace(node, node.tryBlock, node.catchClause); + emit(node.catchClause); + } + if (node.finallyBlock) { + writeLineOrSpace(node, node.catchClause || node.tryBlock, node.finallyBlock); + emitTokenWithComment(ts.SyntaxKind.FinallyKeyword, (node.catchClause || node.tryBlock).end, writeKeyword, node); writeSpace(); - emitIdentifierName(node.name); - emitSignatureAndBody(node, emitSignatureHead); + emit(node.finallyBlock); } + } - function emitSignatureAndBody(node: ts.FunctionLikeDeclaration, emitSignatureHead: (node: ts.SignatureDeclaration) => void) { - const body = node.body; - if (body) { - if (ts.isBlock(body)) { - const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } + function emitDebuggerStatement(node: ts.DebuggerStatement) { + writeToken(ts.SyntaxKind.DebuggerKeyword, node.pos, writeKeyword); + writeTrailingSemicolon(); + } - pushNameGenerationScope(node); - ts.forEach(node.parameters, generateNames); - generateNames(node.body); + // + // Declarations + // - emitSignatureHead(node); - emitBlockFunctionBody(body); - popNameGenerationScope(node); + function emitVariableDeclaration(node: ts.VariableDeclaration) { + emit(node.name); + emit(node.exclamationToken); + emitTypeAnnotation(node.type); + emitInitializer(node.initializer, node.type?.end ?? node.name.emitNode?.typeNode?.end ?? node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - if (indentedFlag) { - decreaseIndent(); - } + function emitVariableDeclarationList(node: ts.VariableDeclarationList) { + writeKeyword(ts.isLet(node) ? "let" : ts.isVarConst(node) ? "const" : "var"); + writeSpace(); + emitList(node, node.declarations, ts.ListFormat.VariableDeclarationList); + } + + function emitFunctionDeclaration(node: ts.FunctionDeclaration) { + emitFunctionDeclarationOrExpression(node); + } + + function emitFunctionDeclarationOrExpression(node: ts.FunctionDeclaration | ts.FunctionExpression) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("function"); + emit(node.asteriskToken); + writeSpace(); + emitIdentifierName(node.name); + emitSignatureAndBody(node, emitSignatureHead); + } + + function emitSignatureAndBody(node: ts.FunctionLikeDeclaration, emitSignatureHead: (node: ts.SignatureDeclaration) => void) { + const body = node.body; + if (body) { + if (ts.isBlock(body)) { + const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); } - else { - emitSignatureHead(node); - writeSpace(); - emitExpression(body, parenthesizer.parenthesizeConciseBodyOfArrowFunction); + + pushNameGenerationScope(node); + ts.forEach(node.parameters, generateNames); + generateNames(node.body); + + emitSignatureHead(node); + emitBlockFunctionBody(body); + popNameGenerationScope(node); + + if (indentedFlag) { + decreaseIndent(); } } else { emitSignatureHead(node); - writeTrailingSemicolon(); + writeSpace(); + emitExpression(body, parenthesizer.parenthesizeConciseBodyOfArrowFunction); } - } - - function emitSignatureHead(node: ts.FunctionDeclaration | ts.FunctionExpression | ts.MethodDeclaration | ts.AccessorDeclaration | ts.ConstructorDeclaration) { - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - emitTypeAnnotation(node.type); + else { + emitSignatureHead(node); + writeTrailingSemicolon(); } - function shouldEmitBlockFunctionBodyOnSingleLine(body: ts.Block) { - // We must emit a function body as a single-line body in the following case: - // * The body has NodeEmitFlags.SingleLine specified. + } + + function emitSignatureHead(node: ts.FunctionDeclaration | ts.FunctionExpression | ts.MethodDeclaration | ts.AccessorDeclaration | ts.ConstructorDeclaration) { + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + emitTypeAnnotation(node.type); + } - // 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. + function shouldEmitBlockFunctionBodyOnSingleLine(body: ts.Block) { + // We must emit a function body as a single-line body in the following case: + // * The body has NodeEmitFlags.SingleLine specified. - if (ts.getEmitFlags(body) & ts.EmitFlags.SingleLine) { - return true; - } + // 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 (body.multiLine) { - return false; - } + if (ts.getEmitFlags(body) & ts.EmitFlags.SingleLine) { + return true; + } - if (!ts.nodeIsSynthesized(body) && currentSourceFile && !ts.rangeIsOnSingleLine(body, currentSourceFile)) { - return false; - } + if (body.multiLine) { + return false; + } - if (getLeadingLineTerminatorCount(body, body.statements, ts.ListFormat.PreserveLines) - || getClosingLineTerminatorCount(body, body.statements, ts.ListFormat.PreserveLines)) { - return false; - } + if (!ts.nodeIsSynthesized(body) && currentSourceFile && !ts.rangeIsOnSingleLine(body, currentSourceFile)) { + return false; + } - let previousStatement: ts.Statement | undefined; - for (const statement of body.statements) { - if (getSeparatingLineTerminatorCount(previousStatement, statement, ts.ListFormat.PreserveLines) > 0) { - return false; - } + if (getLeadingLineTerminatorCount(body, body.statements, ts.ListFormat.PreserveLines) + || getClosingLineTerminatorCount(body, body.statements, ts.ListFormat.PreserveLines)) { + return false; + } - previousStatement = statement; + let previousStatement: ts.Statement | undefined; + for (const statement of body.statements) { + if (getSeparatingLineTerminatorCount(previousStatement, statement, ts.ListFormat.PreserveLines) > 0) { + return false; } - return true; + previousStatement = statement; } - function emitBlockFunctionBody(body: ts.Block) { - onBeforeEmitNode?.(body); - writeSpace(); - writePunctuation("{"); - increaseIndent(); + return true; + } + + function emitBlockFunctionBody(body: ts.Block) { + onBeforeEmitNode?.(body); + writeSpace(); + writePunctuation("{"); + increaseIndent(); + + const emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) + ? emitBlockFunctionBodyOnSingleLine + : emitBlockFunctionBodyWorker; + + emitBodyWithDetachedComments(body, body.statements, emitBlockFunctionBody); - const emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) - ? emitBlockFunctionBodyOnSingleLine - : emitBlockFunctionBodyWorker; + decreaseIndent(); + writeToken(ts.SyntaxKind.CloseBraceToken, body.statements.end, writePunctuation, body); + onAfterEmitNode?.(body); + } - emitBodyWithDetachedComments(body, body.statements, emitBlockFunctionBody); + function emitBlockFunctionBodyOnSingleLine(body: ts.Block) { + emitBlockFunctionBodyWorker(body, /*emitBlockFunctionBodyOnSingleLine*/ true); + } + function emitBlockFunctionBodyWorker(body: ts.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(); - writeToken(ts.SyntaxKind.CloseBraceToken, body.statements.end, writePunctuation, body); - onAfterEmitNode?.(body); + emitList(body, body.statements, ts.ListFormat.SingleLineFunctionBodyStatements); + increaseIndent(); } - - function emitBlockFunctionBodyOnSingleLine(body: ts.Block) { - emitBlockFunctionBodyWorker(body, /*emitBlockFunctionBodyOnSingleLine*/ true); + else { + emitList(body, body.statements, ts.ListFormat.MultiLineFunctionBodyStatements, /*parenthesizerRule*/ undefined, statementOffset); } + } - function emitBlockFunctionBodyWorker(body: ts.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, ts.ListFormat.SingleLineFunctionBodyStatements); - increaseIndent(); - } - else { - emitList(body, body.statements, ts.ListFormat.MultiLineFunctionBodyStatements, /*parenthesizerRule*/ undefined, statementOffset); - } + function emitClassDeclaration(node: ts.ClassDeclaration) { + emitClassDeclarationOrExpression(node); + } + + function emitClassDeclarationOrExpression(node: ts.ClassDeclaration | ts.ClassExpression) { + ts.forEach(node.members, generateMemberNames); + + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("class"); + if (node.name) { + writeSpace(); + emitIdentifierName(node.name); } - function emitClassDeclaration(node: ts.ClassDeclaration) { - emitClassDeclarationOrExpression(node); + const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); } - function emitClassDeclarationOrExpression(node: ts.ClassDeclaration | ts.ClassExpression) { - ts.forEach(node.members, generateMemberNames); + emitTypeParameters(node, node.typeParameters); + emitList(node, node.heritageClauses, ts.ListFormat.ClassHeritageClauses); - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("class"); - if (node.name) { - writeSpace(); - emitIdentifierName(node.name); - } + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ts.ListFormat.ClassMembers); + writePunctuation("}"); - const indentedFlag = ts.getEmitFlags(node) & ts.EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } + if (indentedFlag) { + decreaseIndent(); + } + } - emitTypeParameters(node, node.typeParameters); - emitList(node, node.heritageClauses, ts.ListFormat.ClassHeritageClauses); + function emitInterfaceDeclaration(node: ts.InterfaceDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("interface"); + writeSpace(); + emit(node.name); + emitTypeParameters(node, node.typeParameters); + emitList(node, node.heritageClauses, ts.ListFormat.HeritageClauses); + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ts.ListFormat.InterfaceMembers); + writePunctuation("}"); + } - writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ts.ListFormat.ClassMembers); - writePunctuation("}"); + function emitTypeAliasDeclaration(node: ts.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(); + } - if (indentedFlag) { - decreaseIndent(); - } - } + function emitEnumDeclaration(node: ts.EnumDeclaration) { + emitModifiers(node, node.modifiers); + writeKeyword("enum"); + writeSpace(); + emit(node.name); - function emitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("interface"); - writeSpace(); - emit(node.name); - emitTypeParameters(node, node.typeParameters); - emitList(node, node.heritageClauses, ts.ListFormat.HeritageClauses); + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ts.ListFormat.EnumMembers); + writePunctuation("}"); + } + + function emitModuleDeclaration(node: ts.ModuleDeclaration) { + emitModifiers(node, node.modifiers); + if (~node.flags & ts.NodeFlags.GlobalAugmentation) { + writeKeyword(node.flags & ts.NodeFlags.Namespace ? "namespace" : "module"); writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ts.ListFormat.InterfaceMembers); - writePunctuation("}"); } + emit(node.name); - function emitTypeAliasDeclaration(node: ts.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(); + let body = node.body; + if (!body) + return writeTrailingSemicolon(); + while (body && ts.isModuleDeclaration(body)) { + writePunctuation("."); + emit(body.name); + body = body.body; } - function emitEnumDeclaration(node: ts.EnumDeclaration) { - emitModifiers(node, node.modifiers); - writeKeyword("enum"); - writeSpace(); - emit(node.name); + writeSpace(); + emit(body); + } - writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ts.ListFormat.EnumMembers); - writePunctuation("}"); - } + function emitModuleBlock(node: ts.ModuleBlock) { + pushNameGenerationScope(node); + ts.forEach(node.statements, generateNames); + emitBlockStatements(node, /*forceSingleLine*/ isEmptyBlock(node)); + popNameGenerationScope(node); + } - function emitModuleDeclaration(node: ts.ModuleDeclaration) { - emitModifiers(node, node.modifiers); - if (~node.flags & ts.NodeFlags.GlobalAugmentation) { - writeKeyword(node.flags & ts.NodeFlags.Namespace ? "namespace" : "module"); - writeSpace(); - } - emit(node.name); - - let body = node.body; - if (!body) - return writeTrailingSemicolon(); - while (body && ts.isModuleDeclaration(body)) { - writePunctuation("."); - emit(body.name); - body = body.body; - } + function emitCaseBlock(node: ts.CaseBlock) { + emitTokenWithComment(ts.SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); + emitList(node, node.clauses, ts.ListFormat.CaseBlockClauses); + emitTokenWithComment(ts.SyntaxKind.CloseBraceToken, node.clauses.end, writePunctuation, node, /*indentLeading*/ true); + } + function emitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { + emitModifiers(node, node.modifiers); + emitTokenWithComment(ts.SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + writeSpace(); + if (node.isTypeOnly) { + emitTokenWithComment(ts.SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); writeSpace(); - emit(body); } + emit(node.name); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.EqualsToken, node.name.end, writePunctuation, node); + writeSpace(); + emitModuleReference(node.moduleReference); + writeTrailingSemicolon(); + } - function emitModuleBlock(node: ts.ModuleBlock) { - pushNameGenerationScope(node); - ts.forEach(node.statements, generateNames); - emitBlockStatements(node, /*forceSingleLine*/ isEmptyBlock(node)); - popNameGenerationScope(node); + function emitModuleReference(node: ts.ModuleReference) { + if (node.kind === ts.SyntaxKind.Identifier) { + emitExpression(node); } - - function emitCaseBlock(node: ts.CaseBlock) { - emitTokenWithComment(ts.SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); - emitList(node, node.clauses, ts.ListFormat.CaseBlockClauses); - emitTokenWithComment(ts.SyntaxKind.CloseBraceToken, node.clauses.end, writePunctuation, node, /*indentLeading*/ true); + else { + emit(node); } + } - function emitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { - emitModifiers(node, node.modifiers); - emitTokenWithComment(ts.SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); - writeSpace(); - if (node.isTypeOnly) { - emitTokenWithComment(ts.SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); - writeSpace(); - } - emit(node.name); + function emitImportDeclaration(node: ts.ImportDeclaration) { + emitModifiers(node, node.modifiers); + emitTokenWithComment(ts.SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + writeSpace(); + if (node.importClause) { + emit(node.importClause); writeSpace(); - emitTokenWithComment(ts.SyntaxKind.EqualsToken, node.name.end, writePunctuation, node); + emitTokenWithComment(ts.SyntaxKind.FromKeyword, node.importClause.end, writeKeyword, node); writeSpace(); - emitModuleReference(node.moduleReference); - writeTrailingSemicolon(); } - - function emitModuleReference(node: ts.ModuleReference) { - if (node.kind === ts.SyntaxKind.Identifier) { - emitExpression(node); - } - else { - emit(node); - } + emitExpression(node.moduleSpecifier); + if (node.assertClause) { + emitWithLeadingSpace(node.assertClause); } + writeTrailingSemicolon(); + } - function emitImportDeclaration(node: ts.ImportDeclaration) { - emitModifiers(node, node.modifiers); - emitTokenWithComment(ts.SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + function emitImportClause(node: ts.ImportClause) { + if (node.isTypeOnly) { + emitTokenWithComment(ts.SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); + writeSpace(); + } + emit(node.name); + if (node.name && node.namedBindings) { + emitTokenWithComment(ts.SyntaxKind.CommaToken, node.name.end, writePunctuation, node); writeSpace(); - if (node.importClause) { - emit(node.importClause); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.FromKeyword, node.importClause.end, writeKeyword, node); - writeSpace(); - } - emitExpression(node.moduleSpecifier); - if (node.assertClause) { - emitWithLeadingSpace(node.assertClause); - } - writeTrailingSemicolon(); } + emit(node.namedBindings); + } - function emitImportClause(node: ts.ImportClause) { - if (node.isTypeOnly) { - emitTokenWithComment(ts.SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); - writeSpace(); - } - emit(node.name); - if (node.name && node.namedBindings) { - emitTokenWithComment(ts.SyntaxKind.CommaToken, node.name.end, writePunctuation, node); - writeSpace(); - } - emit(node.namedBindings); + function emitNamespaceImport(node: ts.NamespaceImport) { + const asPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.AsKeyword, asPos, writeKeyword, node); + writeSpace(); + emit(node.name); + } + + function emitNamedImports(node: ts.NamedImports) { + emitNamedImportsOrExports(node); + } + + function emitImportSpecifier(node: ts.ImportSpecifier) { + emitImportOrExportSpecifier(node); + } + + function emitExportAssignment(node: ts.ExportAssignment) { + const nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.isExportEquals) { + emitTokenWithComment(ts.SyntaxKind.EqualsToken, nextPos, writeOperator, node); } + else { + emitTokenWithComment(ts.SyntaxKind.DefaultKeyword, nextPos, writeKeyword, node); + } + writeSpace(); + emitExpression(node.expression, node.isExportEquals ? + parenthesizer.getParenthesizeRightSideOfBinaryForOperator(ts.SyntaxKind.EqualsToken) : + parenthesizer.parenthesizeExpressionOfExportDefault); + writeTrailingSemicolon(); + } - function emitNamespaceImport(node: ts.NamespaceImport) { - const asPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.AsKeyword, asPos, writeKeyword, node); + function emitExportDeclaration(node: ts.ExportDeclaration) { + let nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.isTypeOnly) { + nextPos = emitTokenWithComment(ts.SyntaxKind.TypeKeyword, nextPos, writeKeyword, node); writeSpace(); - emit(node.name); } - - function emitNamedImports(node: ts.NamedImports) { - emitNamedImportsOrExports(node); + if (node.exportClause) { + emit(node.exportClause); } - - function emitImportSpecifier(node: ts.ImportSpecifier) { - emitImportOrExportSpecifier(node); + else { + nextPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, nextPos, writePunctuation, node); } - - function emitExportAssignment(node: ts.ExportAssignment) { - const nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + if (node.moduleSpecifier) { writeSpace(); - if (node.isExportEquals) { - emitTokenWithComment(ts.SyntaxKind.EqualsToken, nextPos, writeOperator, node); - } - else { - emitTokenWithComment(ts.SyntaxKind.DefaultKeyword, nextPos, writeKeyword, node); - } + const fromPos = node.exportClause ? node.exportClause.end : nextPos; + emitTokenWithComment(ts.SyntaxKind.FromKeyword, fromPos, writeKeyword, node); writeSpace(); - emitExpression(node.expression, node.isExportEquals ? - parenthesizer.getParenthesizeRightSideOfBinaryForOperator(ts.SyntaxKind.EqualsToken) : - parenthesizer.parenthesizeExpressionOfExportDefault); - writeTrailingSemicolon(); + emitExpression(node.moduleSpecifier); } - - function emitExportDeclaration(node: ts.ExportDeclaration) { - let nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); - writeSpace(); - if (node.isTypeOnly) { - nextPos = emitTokenWithComment(ts.SyntaxKind.TypeKeyword, nextPos, writeKeyword, node); - writeSpace(); - } - if (node.exportClause) { - emit(node.exportClause); - } - else { - nextPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, nextPos, writePunctuation, node); - } - if (node.moduleSpecifier) { - writeSpace(); - const fromPos = node.exportClause ? node.exportClause.end : nextPos; - emitTokenWithComment(ts.SyntaxKind.FromKeyword, fromPos, writeKeyword, node); - writeSpace(); - emitExpression(node.moduleSpecifier); - } - if (node.assertClause) { - emitWithLeadingSpace(node.assertClause); - } - writeTrailingSemicolon(); + if (node.assertClause) { + emitWithLeadingSpace(node.assertClause); } + writeTrailingSemicolon(); + } - function emitAssertClause(node: ts.AssertClause) { - emitTokenWithComment(ts.SyntaxKind.AssertKeyword, node.pos, writeKeyword, node); - writeSpace(); - const elements = node.elements; - emitList(node, elements, ts.ListFormat.ImportClauseEntries); - } + function emitAssertClause(node: ts.AssertClause) { + emitTokenWithComment(ts.SyntaxKind.AssertKeyword, node.pos, writeKeyword, node); + writeSpace(); + const elements = node.elements; + emitList(node, elements, ts.ListFormat.ImportClauseEntries); + } - function emitAssertEntry(node: ts.AssertEntry) { - emit(node.name); - writePunctuation(":"); - writeSpace(); + function emitAssertEntry(node: ts.AssertEntry) { + emit(node.name); + writePunctuation(":"); + writeSpace(); - const value = node.value; - /** @see {emitPropertyAssignment} */ - if ((ts.getEmitFlags(value) & ts.EmitFlags.NoLeadingComments) === 0) { - const commentRange = ts.getCommentRange(value); - emitTrailingCommentsOfPosition(commentRange.pos); - } - emit(value); + const value = node.value; + /** @see {emitPropertyAssignment} */ + if ((ts.getEmitFlags(value) & ts.EmitFlags.NoLeadingComments) === 0) { + const commentRange = ts.getCommentRange(value); + emitTrailingCommentsOfPosition(commentRange.pos); } + emit(value); + } - function emitNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration) { - let nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); - writeSpace(); - nextPos = emitTokenWithComment(ts.SyntaxKind.AsKeyword, nextPos, writeKeyword, node); - writeSpace(); - nextPos = emitTokenWithComment(ts.SyntaxKind.NamespaceKeyword, nextPos, writeKeyword, node); + function emitNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration) { + let nextPos = emitTokenWithComment(ts.SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + nextPos = emitTokenWithComment(ts.SyntaxKind.AsKeyword, nextPos, writeKeyword, node); + writeSpace(); + nextPos = emitTokenWithComment(ts.SyntaxKind.NamespaceKeyword, nextPos, writeKeyword, node); + writeSpace(); + emit(node.name); + writeTrailingSemicolon(); + } + + function emitNamespaceExport(node: ts.NamespaceExport) { + const asPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.AsKeyword, asPos, writeKeyword, node); + writeSpace(); + emit(node.name); + } + + function emitNamedExports(node: ts.NamedExports) { + emitNamedImportsOrExports(node); + } + + function emitExportSpecifier(node: ts.ExportSpecifier) { + emitImportOrExportSpecifier(node); + } + + function emitNamedImportsOrExports(node: ts.NamedImportsOrExports) { + writePunctuation("{"); + emitList(node, node.elements, ts.ListFormat.NamedImportsOrExportsElements); + writePunctuation("}"); + } + + function emitImportOrExportSpecifier(node: ts.ImportOrExportSpecifier) { + if (node.isTypeOnly) { + writeKeyword("type"); writeSpace(); - emit(node.name); - writeTrailingSemicolon(); } - - function emitNamespaceExport(node: ts.NamespaceExport) { - const asPos = emitTokenWithComment(ts.SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + if (node.propertyName) { + emit(node.propertyName); writeSpace(); - emitTokenWithComment(ts.SyntaxKind.AsKeyword, asPos, writeKeyword, node); + emitTokenWithComment(ts.SyntaxKind.AsKeyword, node.propertyName.end, writeKeyword, node); writeSpace(); - emit(node.name); } - function emitNamedExports(node: ts.NamedExports) { - emitNamedImportsOrExports(node); - } - - function emitExportSpecifier(node: ts.ExportSpecifier) { - emitImportOrExportSpecifier(node); - } + emit(node.name); + } - function emitNamedImportsOrExports(node: ts.NamedImportsOrExports) { - writePunctuation("{"); - emitList(node, node.elements, ts.ListFormat.NamedImportsOrExportsElements); - writePunctuation("}"); - } + // + // Module references + // - function emitImportOrExportSpecifier(node: ts.ImportOrExportSpecifier) { - if (node.isTypeOnly) { - writeKeyword("type"); - writeSpace(); - } - if (node.propertyName) { - emit(node.propertyName); - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.AsKeyword, node.propertyName.end, writeKeyword, node); - writeSpace(); - } + function emitExternalModuleReference(node: ts.ExternalModuleReference) { + writeKeyword("require"); + writePunctuation("("); + emitExpression(node.expression); + writePunctuation(")"); + } - emit(node.name); - } + // + // JSX + // - // - // Module references - // + function emitJsxElement(node: ts.JsxElement) { + emit(node.openingElement); + emitList(node, node.children, ts.ListFormat.JsxElementOrFragmentChildren); + emit(node.closingElement); + } - function emitExternalModuleReference(node: ts.ExternalModuleReference) { - writeKeyword("require"); - writePunctuation("("); - emitExpression(node.expression); - writePunctuation(")"); - } + function emitJsxSelfClosingElement(node: ts.JsxSelfClosingElement) { + writePunctuation("<"); + emitJsxTagName(node.tagName); + emitTypeArguments(node, node.typeArguments); + writeSpace(); + emit(node.attributes); + writePunctuation("/>"); + } - // - // JSX - // + function emitJsxFragment(node: ts.JsxFragment) { + emit(node.openingFragment); + emitList(node, node.children, ts.ListFormat.JsxElementOrFragmentChildren); + emit(node.closingFragment); + } - function emitJsxElement(node: ts.JsxElement) { - emit(node.openingElement); - emitList(node, node.children, ts.ListFormat.JsxElementOrFragmentChildren); - emit(node.closingElement); - } + function emitJsxOpeningElementOrFragment(node: ts.JsxOpeningElement | ts.JsxOpeningFragment) { + writePunctuation("<"); - function emitJsxSelfClosingElement(node: ts.JsxSelfClosingElement) { - writePunctuation("<"); + if (ts.isJsxOpeningElement(node)) { + const indented = writeLineSeparatorsAndIndentBefore(node.tagName, node); emitJsxTagName(node.tagName); emitTypeArguments(node, node.typeArguments); - writeSpace(); + if (node.attributes.properties && node.attributes.properties.length > 0) { + writeSpace(); + } emit(node.attributes); - writePunctuation("/>"); + writeLineSeparatorsAfter(node.attributes, node); + decreaseIndentIf(indented); } - function emitJsxFragment(node: ts.JsxFragment) { - emit(node.openingFragment); - emitList(node, node.children, ts.ListFormat.JsxElementOrFragmentChildren); - emit(node.closingFragment); + writePunctuation(">"); + } + + function emitJsxText(node: ts.JsxText) { + writer.writeLiteral(node.text); + } + + function emitJsxClosingElementOrFragment(node: ts.JsxClosingElement | ts.JsxClosingFragment) { + writePunctuation(""); + } - function emitJsxOpeningElementOrFragment(node: ts.JsxOpeningElement | ts.JsxOpeningFragment) { - writePunctuation("<"); + function emitJsxAttributes(node: ts.JsxAttributes) { + emitList(node, node.properties, ts.ListFormat.JsxElementAttributes); + } - if (ts.isJsxOpeningElement(node)) { - const indented = writeLineSeparatorsAndIndentBefore(node.tagName, node); - emitJsxTagName(node.tagName); - emitTypeArguments(node, node.typeArguments); - if (node.attributes.properties && node.attributes.properties.length > 0) { - writeSpace(); - } - emit(node.attributes); - writeLineSeparatorsAfter(node.attributes, node); - decreaseIndentIf(indented); - } + function emitJsxAttribute(node: ts.JsxAttribute) { + emit(node.name); + emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue); + } - writePunctuation(">"); - } + function emitJsxSpreadAttribute(node: ts.JsxSpreadAttribute) { + writePunctuation("{..."); + emitExpression(node.expression); + writePunctuation("}"); + } - function emitJsxText(node: ts.JsxText) { - writer.writeLiteral(node.text); - } + function hasTrailingCommentsAtPosition(pos: number) { + let result = false; + ts.forEachTrailingCommentRange(currentSourceFile?.text || "", pos + 1, () => result = true); + return result; + } + + function hasLeadingCommentsAtPosition(pos: number) { + let result = false; + ts.forEachLeadingCommentRange(currentSourceFile?.text || "", pos + 1, () => result = true); + return result; + } + + function hasCommentsAtPosition(pos: number) { + return hasTrailingCommentsAtPosition(pos) || hasLeadingCommentsAtPosition(pos); + } - function emitJsxClosingElementOrFragment(node: ts.JsxClosingElement | ts.JsxClosingFragment) { - writePunctuation(""); } + } - function emitJsxAttributes(node: ts.JsxAttributes) { - emitList(node, node.properties, ts.ListFormat.JsxElementAttributes); + function emitJsxTagName(node: ts.JsxTagNameExpression) { + if (node.kind === ts.SyntaxKind.Identifier) { + emitExpression(node); } - - function emitJsxAttribute(node: ts.JsxAttribute) { - emit(node.name); - emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue); + else { + emit(node); } + } - function emitJsxSpreadAttribute(node: ts.JsxSpreadAttribute) { - writePunctuation("{..."); - emitExpression(node.expression); - writePunctuation("}"); + // + // Clauses + // + + function emitCaseClause(node: ts.CaseClause) { + emitTokenWithComment(ts.SyntaxKind.CaseKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); + + emitCaseOrDefaultClauseRest(node, node.statements, node.expression.end); + } + + function emitDefaultClause(node: ts.DefaultClause) { + const pos = emitTokenWithComment(ts.SyntaxKind.DefaultKeyword, node.pos, writeKeyword, node); + emitCaseOrDefaultClauseRest(node, node.statements, pos); + } + + function emitCaseOrDefaultClauseRest(parentNode: ts.Node, statements: ts.NodeArray, colonPos: number) { + const emitAsSingleStatement = statements.length === 1 && + ( + // treat synthesized nodes as located on the same line for emit purposes + !currentSourceFile || + ts.nodeIsSynthesized(parentNode) || + ts.nodeIsSynthesized(statements[0]) || + ts.rangeStartPositionsAreOnSameLine(parentNode, statements[0], currentSourceFile)); + let format = ts.ListFormat.CaseOrDefaultClauseStatements; + if (emitAsSingleStatement) { + writeToken(ts.SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); + writeSpace(); + format &= ~(ts.ListFormat.MultiLine | ts.ListFormat.Indented); + } + else { + emitTokenWithComment(ts.SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); } + emitList(parentNode, statements, format); + } + + function emitHeritageClause(node: ts.HeritageClause) { + writeSpace(); + writeTokenText(node.token, writeKeyword); + writeSpace(); + emitList(node, node.types, ts.ListFormat.HeritageClauseTypes); + } - function hasTrailingCommentsAtPosition(pos: number) { - let result = false; - ts.forEachTrailingCommentRange(currentSourceFile?.text || "", pos + 1, () => result = true); - return result; + function emitCatchClause(node: ts.CatchClause) { + const openParenPos = emitTokenWithComment(ts.SyntaxKind.CatchKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.variableDeclaration) { + emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emit(node.variableDeclaration); + emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.variableDeclaration.end, writePunctuation, node); + writeSpace(); } + emit(node.block); + } + + // + // Property assignments + // + + function emitPropertyAssignment(node: ts.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 ((ts.getEmitFlags(initializer) & ts.EmitFlags.NoLeadingComments) === 0) { + const commentRange = ts.getCommentRange(initializer); + emitTrailingCommentsOfPosition(commentRange.pos); + } + emitExpression(initializer, parenthesizer.parenthesizeExpressionForDisallowedComma); + } - function hasLeadingCommentsAtPosition(pos: number) { - let result = false; - ts.forEachLeadingCommentRange(currentSourceFile?.text || "", pos + 1, () => result = true); - return result; + function emitShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment) { + emit(node.name); + if (node.objectAssignmentInitializer) { + writeSpace(); + writePunctuation("="); + writeSpace(); + emitExpression(node.objectAssignmentInitializer, parenthesizer.parenthesizeExpressionForDisallowedComma); } + } - function hasCommentsAtPosition(pos: number) { - return hasTrailingCommentsAtPosition(pos) || hasLeadingCommentsAtPosition(pos); + function emitSpreadAssignment(node: ts.SpreadAssignment) { + if (node.expression) { + emitTokenWithComment(ts.SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); + emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); } + } - function emitJsxExpression(node: ts.JsxExpression) { - if (node.expression || (!commentsDisabled && !ts.nodeIsSynthesized(node) && hasCommentsAtPosition(node.pos))) { // preserve empty expressions if they contain comments! - const isMultiline = currentSourceFile && !ts.nodeIsSynthesized(node) && ts.getLineAndCharacterOfPosition(currentSourceFile, node.pos).line !== ts.getLineAndCharacterOfPosition(currentSourceFile, node.end).line; - if (isMultiline) { - writer.increaseIndent(); - } - const end = emitTokenWithComment(ts.SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); - emit(node.dotDotDotToken); - emitExpression(node.expression); - emitTokenWithComment(ts.SyntaxKind.CloseBraceToken, node.expression?.end || end, writePunctuation, node); - if (isMultiline) { - writer.decreaseIndent(); + // + // Enum + // + + function emitEnumMember(node: ts.EnumMember) { + emit(node.name); + emitInitializer(node.initializer, node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + } + + // + // JSDoc + // + function emitJSDoc(node: ts.JSDoc) { + write("/**"); + if (node.comment) { + const text = ts.getTextOfJSDocComment(node.comment); + if (text) { + const lines = text.split(/\r\n?|\n/g); + for (const line of lines) { + writeLine(); + writeSpace(); + writePunctuation("*"); + writeSpace(); + write(line); } } } - - function emitJsxTagName(node: ts.JsxTagNameExpression) { - if (node.kind === ts.SyntaxKind.Identifier) { - emitExpression(node); + if (node.tags) { + if (node.tags.length === 1 && node.tags[0].kind === ts.SyntaxKind.JSDocTypeTag && !node.comment) { + writeSpace(); + emit(node.tags[0]); } else { - emit(node); + emitList(node, node.tags, ts.ListFormat.JSDocComment); } } + writeSpace(); + write("*/"); + } - // - // Clauses - // + function emitJSDocSimpleTypedTag(tag: ts.JSDocTypeTag | ts.JSDocThisTag | ts.JSDocEnumTag | ts.JSDocReturnTag) { + emitJSDocTagName(tag.tagName); + emitJSDocTypeExpression(tag.typeExpression); + emitJSDocComment(tag.comment); + } - function emitCaseClause(node: ts.CaseClause) { - emitTokenWithComment(ts.SyntaxKind.CaseKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); + function emitJSDocSeeTag(tag: ts.JSDocSeeTag) { + emitJSDocTagName(tag.tagName); + emit(tag.name); + emitJSDocComment(tag.comment); + } - emitCaseOrDefaultClauseRest(node, node.statements, node.expression.end); - } + function emitJSDocNameReference(node: ts.JSDocNameReference) { + writeSpace(); + writePunctuation("{"); + emit(node.name); + writePunctuation("}"); + } - function emitDefaultClause(node: ts.DefaultClause) { - const pos = emitTokenWithComment(ts.SyntaxKind.DefaultKeyword, node.pos, writeKeyword, node); - emitCaseOrDefaultClauseRest(node, node.statements, pos); - } + function emitJSDocHeritageTag(tag: ts.JSDocImplementsTag | ts.JSDocAugmentsTag) { + emitJSDocTagName(tag.tagName); + writeSpace(); + writePunctuation("{"); + emit(tag.class); + writePunctuation("}"); + emitJSDocComment(tag.comment); + } - function emitCaseOrDefaultClauseRest(parentNode: ts.Node, statements: ts.NodeArray, colonPos: number) { - const emitAsSingleStatement = statements.length === 1 && - ( - // treat synthesized nodes as located on the same line for emit purposes - !currentSourceFile || - ts.nodeIsSynthesized(parentNode) || - ts.nodeIsSynthesized(statements[0]) || - ts.rangeStartPositionsAreOnSameLine(parentNode, statements[0], currentSourceFile)); - let format = ts.ListFormat.CaseOrDefaultClauseStatements; - if (emitAsSingleStatement) { - writeToken(ts.SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); - writeSpace(); - format &= ~(ts.ListFormat.MultiLine | ts.ListFormat.Indented); + function emitJSDocTemplateTag(tag: ts.JSDocTemplateTag) { + emitJSDocTagName(tag.tagName); + emitJSDocTypeExpression(tag.constraint); + writeSpace(); + emitList(tag, tag.typeParameters, ts.ListFormat.CommaListElements); + emitJSDocComment(tag.comment); + } + + function emitJSDocTypedefTag(tag: ts.JSDocTypedefTag) { + emitJSDocTagName(tag.tagName); + if (tag.typeExpression) { + if (tag.typeExpression.kind === ts.SyntaxKind.JSDocTypeExpression) { + emitJSDocTypeExpression(tag.typeExpression); } else { - emitTokenWithComment(ts.SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); + writeSpace(); + writePunctuation("{"); + write("Object"); + if (tag.typeExpression.isArrayType) { + writePunctuation("["); + writePunctuation("]"); + } + writePunctuation("}"); } - emitList(parentNode, statements, format); } - - function emitHeritageClause(node: ts.HeritageClause) { + if (tag.fullName) { writeSpace(); - writeTokenText(node.token, writeKeyword); - writeSpace(); - emitList(node, node.types, ts.ListFormat.HeritageClauseTypes); + emit(tag.fullName); } - - function emitCatchClause(node: ts.CatchClause) { - const openParenPos = emitTokenWithComment(ts.SyntaxKind.CatchKeyword, node.pos, writeKeyword, node); - writeSpace(); - if (node.variableDeclaration) { - emitTokenWithComment(ts.SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emit(node.variableDeclaration); - emitTokenWithComment(ts.SyntaxKind.CloseParenToken, node.variableDeclaration.end, writePunctuation, node); - writeSpace(); - } - emit(node.block); + emitJSDocComment(tag.comment); + if (tag.typeExpression && tag.typeExpression.kind === ts.SyntaxKind.JSDocTypeLiteral) { + emitJSDocTypeLiteral(tag.typeExpression); } + } - // - // Property assignments - // - - function emitPropertyAssignment(node: ts.PropertyAssignment) { - emit(node.name); - writePunctuation(":"); + function emitJSDocCallbackTag(tag: ts.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 ((ts.getEmitFlags(initializer) & ts.EmitFlags.NoLeadingComments) === 0) { - const commentRange = ts.getCommentRange(initializer); - emitTrailingCommentsOfPosition(commentRange.pos); - } - emitExpression(initializer, parenthesizer.parenthesizeExpressionForDisallowedComma); - } - - function emitShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment) { - emit(node.name); - if (node.objectAssignmentInitializer) { - writeSpace(); - writePunctuation("="); - writeSpace(); - emitExpression(node.objectAssignmentInitializer, parenthesizer.parenthesizeExpressionForDisallowedComma); - } + emit(tag.name); } + emitJSDocComment(tag.comment); + emitJSDocSignature(tag.typeExpression); + } - function emitSpreadAssignment(node: ts.SpreadAssignment) { - if (node.expression) { - emitTokenWithComment(ts.SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); - emitExpression(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma); - } - } + function emitJSDocSimpleTag(tag: ts.JSDocTag) { + emitJSDocTagName(tag.tagName); + emitJSDocComment(tag.comment); + } - // - // Enum - // + function emitJSDocTypeLiteral(lit: ts.JSDocTypeLiteral) { + emitList(lit, ts.factory.createNodeArray(lit.jsDocPropertyTags), ts.ListFormat.JSDocComment); + } - function emitEnumMember(node: ts.EnumMember) { - emit(node.name); - emitInitializer(node.initializer, node.name.end, node, parenthesizer.parenthesizeExpressionForDisallowedComma); + function emitJSDocSignature(sig: ts.JSDocSignature) { + if (sig.typeParameters) { + emitList(sig, ts.factory.createNodeArray(sig.typeParameters), ts.ListFormat.JSDocComment); } - - // - // JSDoc - // - function emitJSDoc(node: ts.JSDoc) { - write("/**"); - if (node.comment) { - const text = ts.getTextOfJSDocComment(node.comment); - if (text) { - const lines = text.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 === ts.SyntaxKind.JSDocTypeTag && !node.comment) { - writeSpace(); - emit(node.tags[0]); - } - else { - emitList(node, node.tags, ts.ListFormat.JSDocComment); - } - } + if (sig.parameters) { + emitList(sig, ts.factory.createNodeArray(sig.parameters), ts.ListFormat.JSDocComment); + } + if (sig.type) { + writeLine(); + writeSpace(); + writePunctuation("*"); writeSpace(); - write("*/"); + emit(sig.type); } + } - function emitJSDocSimpleTypedTag(tag: ts.JSDocTypeTag | ts.JSDocThisTag | ts.JSDocEnumTag | ts.JSDocReturnTag) { - emitJSDocTagName(tag.tagName); - emitJSDocTypeExpression(tag.typeExpression); - emitJSDocComment(tag.comment); + function emitJSDocPropertyLikeTag(param: ts.JSDocPropertyLikeTag) { + emitJSDocTagName(param.tagName); + emitJSDocTypeExpression(param.typeExpression); + writeSpace(); + if (param.isBracketed) { + writePunctuation("["); } - - function emitJSDocSeeTag(tag: ts.JSDocSeeTag) { - emitJSDocTagName(tag.tagName); - emit(tag.name); - emitJSDocComment(tag.comment); + emit(param.name); + if (param.isBracketed) { + writePunctuation("]"); } + emitJSDocComment(param.comment); + } + + function emitJSDocTagName(tagName: ts.Identifier) { + writePunctuation("@"); + emit(tagName); + } - function emitJSDocNameReference(node: ts.JSDocNameReference) { + function emitJSDocComment(comment: string | ts.NodeArray | undefined) { + const text = ts.getTextOfJSDocComment(comment); + if (text) { writeSpace(); - writePunctuation("{"); - emit(node.name); - writePunctuation("}"); + write(text); } + } - function emitJSDocHeritageTag(tag: ts.JSDocImplementsTag | ts.JSDocAugmentsTag) { - emitJSDocTagName(tag.tagName); + function emitJSDocTypeExpression(typeExpression: ts.JSDocTypeExpression | undefined) { + if (typeExpression) { writeSpace(); writePunctuation("{"); - emit(tag.class); + emit(typeExpression.type); writePunctuation("}"); - emitJSDocComment(tag.comment); } + } - function emitJSDocTemplateTag(tag: ts.JSDocTemplateTag) { - emitJSDocTagName(tag.tagName); - emitJSDocTypeExpression(tag.constraint); - writeSpace(); - emitList(tag, tag.typeParameters, ts.ListFormat.CommaListElements); - emitJSDocComment(tag.comment); + // + // Top-level nodes + // + + function emitSourceFile(node: ts.SourceFile) { + writeLine(); + const statements = node.statements; + // 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 || + !ts.isPrologueDirective(statements[0]) || + ts.nodeIsSynthesized(statements[0]); + if (shouldEmitDetachedComment) { + emitBodyWithDetachedComments(node, statements, emitSourceFileWorker); + return; } + emitSourceFileWorker(node); + } - function emitJSDocTypedefTag(tag: ts.JSDocTypedefTag) { - emitJSDocTagName(tag.tagName); - if (tag.typeExpression) { - if (tag.typeExpression.kind === ts.SyntaxKind.JSDocTypeExpression) { - emitJSDocTypeExpression(tag.typeExpression); - } - else { - writeSpace(); - writePunctuation("{"); - write("Object"); - if (tag.typeExpression.isArrayType) { - writePunctuation("["); - writePunctuation("]"); - } - writePunctuation("}"); + function emitSyntheticTripleSlashReferencesIfNeeded(node: ts.Bundle) { + emitTripleSlashDirectives(!!node.hasNoDefaultLib, node.syntheticFileReferences || [], node.syntheticTypeReferences || [], node.syntheticLibReferences || []); + for (const prepend of node.prepends) { + if (ts.isUnparsedSource(prepend) && prepend.syntheticReferences) { + for (const ref of prepend.syntheticReferences) { + emit(ref); + writeLine(); } } - if (tag.fullName) { - writeSpace(); - emit(tag.fullName); - } - emitJSDocComment(tag.comment); - if (tag.typeExpression && tag.typeExpression.kind === ts.SyntaxKind.JSDocTypeLiteral) { - emitJSDocTypeLiteral(tag.typeExpression); - } } + } - function emitJSDocCallbackTag(tag: ts.JSDocCallbackTag) { - emitJSDocTagName(tag.tagName); - if (tag.name) { - writeSpace(); - emit(tag.name); - } - emitJSDocComment(tag.comment); - emitJSDocSignature(tag.typeExpression); - } + function emitTripleSlashDirectivesIfNeeded(node: ts.SourceFile) { + if (node.isDeclarationFile) + emitTripleSlashDirectives(node.hasNoDefaultLib, node.referencedFiles, node.typeReferenceDirectives, node.libReferenceDirectives); + } - function emitJSDocSimpleTag(tag: ts.JSDocTag) { - emitJSDocTagName(tag.tagName); - emitJSDocComment(tag.comment); + function emitTripleSlashDirectives(hasNoDefaultLib: boolean, files: readonly ts.FileReference[], types: readonly ts.FileReference[], libs: readonly ts.FileReference[]) { + if (hasNoDefaultLib) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.BundleFileSectionKind.NoDefaultLib }); + writeLine(); } - - function emitJSDocTypeLiteral(lit: ts.JSDocTypeLiteral) { - emitList(lit, ts.factory.createNodeArray(lit.jsDocPropertyTags), ts.ListFormat.JSDocComment); + if (currentSourceFile && currentSourceFile.moduleName) { + writeComment(`/// `); + writeLine(); } - - function emitJSDocSignature(sig: ts.JSDocSignature) { - if (sig.typeParameters) { - emitList(sig, ts.factory.createNodeArray(sig.typeParameters), ts.ListFormat.JSDocComment); - } - if (sig.parameters) { - emitList(sig, ts.factory.createNodeArray(sig.parameters), ts.ListFormat.JSDocComment); - } - if (sig.type) { + if (currentSourceFile && currentSourceFile.amdDependencies) { + for (const dep of currentSourceFile.amdDependencies) { + if (dep.name) { + writeComment(`/// `); + } + else { + writeComment(`/// `); + } writeLine(); - writeSpace(); - writePunctuation("*"); - writeSpace(); - emit(sig.type); } } - - function emitJSDocPropertyLikeTag(param: ts.JSDocPropertyLikeTag) { - emitJSDocTagName(param.tagName); - emitJSDocTypeExpression(param.typeExpression); - writeSpace(); - if (param.isBracketed) { - writePunctuation("["); - } - emit(param.name); - if (param.isBracketed) { - writePunctuation("]"); - } - emitJSDocComment(param.comment); + for (const directive of files) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.BundleFileSectionKind.Reference, data: directive.fileName }); + writeLine(); } - - function emitJSDocTagName(tagName: ts.Identifier) { - writePunctuation("@"); - emit(tagName); + for (const directive of types) { + const pos = writer.getTextPos(); + const resolutionMode = directive.resolutionMode && directive.resolutionMode !== currentSourceFile?.impliedNodeFormat + ? `resolution-mode="${directive.resolutionMode === ts.ModuleKind.ESNext ? "import" : "require"}"` + : ""; + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: !directive.resolutionMode ? ts.BundleFileSectionKind.Type : directive.resolutionMode === ts.ModuleKind.ESNext ? ts.BundleFileSectionKind.TypeResolutionModeImport : ts.BundleFileSectionKind.TypeResolutionModeRequire, data: directive.fileName }); + writeLine(); } - - function emitJSDocComment(comment: string | ts.NodeArray | undefined) { - const text = ts.getTextOfJSDocComment(comment); - if (text) { - writeSpace(); - write(text); - } + for (const directive of libs) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.BundleFileSectionKind.Lib, data: directive.fileName }); + writeLine(); } + } - function emitJSDocTypeExpression(typeExpression: ts.JSDocTypeExpression | undefined) { - if (typeExpression) { - writeSpace(); - writePunctuation("{"); - emit(typeExpression.type); - writePunctuation("}"); - } - } + function emitSourceFileWorker(node: ts.SourceFile) { + const statements = node.statements; + pushNameGenerationScope(node); + ts.forEach(node.statements, generateNames); + emitHelpers(node); + const index = ts.findIndex(statements, statement => !ts.isPrologueDirective(statement)); + emitTripleSlashDirectivesIfNeeded(node); + emitList(node, statements, ts.ListFormat.MultiLine, /*parenthesizerRule*/ undefined, index === -1 ? statements.length : index); + popNameGenerationScope(node); + } - // - // Top-level nodes - // + // Transformation nodes - function emitSourceFile(node: ts.SourceFile) { - writeLine(); - const statements = node.statements; - // 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 || - !ts.isPrologueDirective(statements[0]) || - ts.nodeIsSynthesized(statements[0]); - if (shouldEmitDetachedComment) { - emitBodyWithDetachedComments(node, statements, emitSourceFileWorker); - return; - } - emitSourceFileWorker(node); + function emitPartiallyEmittedExpression(node: ts.PartiallyEmittedExpression) { + const emitFlags = ts.getEmitFlags(node); + if (!(emitFlags & ts.EmitFlags.NoLeadingComments) && node.pos !== node.expression.pos) { + emitTrailingCommentsOfPosition(node.expression.pos); } - - function emitSyntheticTripleSlashReferencesIfNeeded(node: ts.Bundle) { - emitTripleSlashDirectives(!!node.hasNoDefaultLib, node.syntheticFileReferences || [], node.syntheticTypeReferences || [], node.syntheticLibReferences || []); - for (const prepend of node.prepends) { - if (ts.isUnparsedSource(prepend) && prepend.syntheticReferences) { - for (const ref of prepend.syntheticReferences) { - emit(ref); - writeLine(); - } - } - } + emitExpression(node.expression); + if (!(emitFlags & ts.EmitFlags.NoTrailingComments) && node.end !== node.expression.end) { + emitLeadingCommentsOfPosition(node.expression.end); } + } - function emitTripleSlashDirectivesIfNeeded(node: ts.SourceFile) { - if (node.isDeclarationFile) - emitTripleSlashDirectives(node.hasNoDefaultLib, node.referencedFiles, node.typeReferenceDirectives, node.libReferenceDirectives); - } + function emitCommaList(node: ts.CommaListExpression) { + emitExpressionList(node, node.elements, ts.ListFormat.CommaListElements, /*parenthesizerRule*/ undefined); + } - function emitTripleSlashDirectives(hasNoDefaultLib: boolean, files: readonly ts.FileReference[], types: readonly ts.FileReference[], libs: readonly ts.FileReference[]) { - if (hasNoDefaultLib) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) - bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.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(`/// `); + /** + * 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 ts.Node[], sourceFile?: ts.SourceFile, seenPrologueDirectives?: ts.Set, recordBundleFileSection?: true): number { + let needsToSetSourceFile = !!sourceFile; + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + if (ts.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: ts.BundleFileSectionKind.Prologue, data: statement.expression.text }); + if (seenPrologueDirectives) { + seenPrologueDirectives.add(statement.expression.text); + } } } - for (const directive of files) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) - bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.BundleFileSectionKind.Reference, data: directive.fileName }); - writeLine(); - } - for (const directive of types) { - const pos = writer.getTextPos(); - const resolutionMode = directive.resolutionMode && directive.resolutionMode !== currentSourceFile?.impliedNodeFormat - ? `resolution-mode="${directive.resolutionMode === ts.ModuleKind.ESNext ? "import" : "require"}"` - : ""; - writeComment(`/// `); - if (bundleFileInfo) - bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: !directive.resolutionMode ? ts.BundleFileSectionKind.Type : directive.resolutionMode === ts.ModuleKind.ESNext ? ts.BundleFileSectionKind.TypeResolutionModeImport : ts.BundleFileSectionKind.TypeResolutionModeRequire, data: directive.fileName }); - writeLine(); - } - for (const directive of libs) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) - bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: ts.BundleFileSectionKind.Lib, data: directive.fileName }); - writeLine(); - } - } - - function emitSourceFileWorker(node: ts.SourceFile) { - const statements = node.statements; - pushNameGenerationScope(node); - ts.forEach(node.statements, generateNames); - emitHelpers(node); - const index = ts.findIndex(statements, statement => !ts.isPrologueDirective(statement)); - emitTripleSlashDirectivesIfNeeded(node); - emitList(node, statements, ts.ListFormat.MultiLine, /*parenthesizerRule*/ undefined, index === -1 ? statements.length : index); - popNameGenerationScope(node); - } - - // Transformation nodes - - function emitPartiallyEmittedExpression(node: ts.PartiallyEmittedExpression) { - const emitFlags = ts.getEmitFlags(node); - if (!(emitFlags & ts.EmitFlags.NoLeadingComments) && node.pos !== node.expression.pos) { - emitTrailingCommentsOfPosition(node.expression.pos); - } - emitExpression(node.expression); - if (!(emitFlags & ts.EmitFlags.NoTrailingComments) && node.end !== node.expression.end) { - emitLeadingCommentsOfPosition(node.expression.end); + else { + // return index of the first non prologue directive + return i; } } - function emitCommaList(node: ts.CommaListExpression) { - emitExpressionList(node, node.elements, ts.ListFormat.CommaListElements, /*parenthesizerRule*/ undefined); - } - - /** - * 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 ts.Node[], sourceFile?: ts.SourceFile, seenPrologueDirectives?: ts.Set, recordBundleFileSection?: true): number { - let needsToSetSourceFile = !!sourceFile; - for (let i = 0; i < statements.length; i++) { - const statement = statements[i]; - if (ts.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: ts.BundleFileSectionKind.Prologue, data: statement.expression.text }); - if (seenPrologueDirectives) { - seenPrologueDirectives.add(statement.expression.text); - } - } - } - else { - // return index of the first non prologue directive - return i; - } - } - - return statements.length; - } + return statements.length; + } - function emitUnparsedPrologues(prologues: readonly ts.UnparsedPrologue[], seenPrologueDirectives: ts.Set) { - 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: ts.BundleFileSectionKind.Prologue, data: prologue.data }); - if (seenPrologueDirectives) { - seenPrologueDirectives.add(prologue.data); - } + function emitUnparsedPrologues(prologues: readonly ts.UnparsedPrologue[], seenPrologueDirectives: ts.Set) { + 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: ts.BundleFileSectionKind.Prologue, data: prologue.data }); + if (seenPrologueDirectives) { + seenPrologueDirectives.add(prologue.data); } } } + } - function emitPrologueDirectivesIfNeeded(sourceFileOrBundle: ts.Bundle | ts.SourceFile) { - if (ts.isSourceFile(sourceFileOrBundle)) { - emitPrologueDirectives(sourceFileOrBundle.statements, sourceFileOrBundle); + function emitPrologueDirectivesIfNeeded(sourceFileOrBundle: ts.Bundle | ts.SourceFile) { + if (ts.isSourceFile(sourceFileOrBundle)) { + emitPrologueDirectives(sourceFileOrBundle.statements, sourceFileOrBundle); + } + else { + const seenPrologueDirectives = new ts.Set(); + for (const prepend of sourceFileOrBundle.prepends) { + emitUnparsedPrologues((prepend as ts.UnparsedSource).prologues, seenPrologueDirectives); } - else { - const seenPrologueDirectives = new ts.Set(); - for (const prepend of sourceFileOrBundle.prepends) { - emitUnparsedPrologues((prepend as ts.UnparsedSource).prologues, seenPrologueDirectives); - } - for (const sourceFile of sourceFileOrBundle.sourceFiles) { - emitPrologueDirectives(sourceFile.statements, sourceFile, seenPrologueDirectives, /*recordBundleFileSection*/ true); - } - setSourceFile(undefined); + for (const sourceFile of sourceFileOrBundle.sourceFiles) { + emitPrologueDirectives(sourceFile.statements, sourceFile, seenPrologueDirectives, /*recordBundleFileSection*/ true); } + setSourceFile(undefined); } + } - function getPrologueDirectivesFromBundledSourceFiles(bundle: ts.Bundle): ts.SourceFilePrologueInfo[] | undefined { - const seenPrologueDirectives = new ts.Set(); - let prologues: ts.SourceFilePrologueInfo[] | undefined; - for (let index = 0; index < bundle.sourceFiles.length; index++) { - const sourceFile = bundle.sourceFiles[index]; - let directives: ts.SourceFilePrologueDirective[] | undefined; - let end = 0; - for (const statement of sourceFile.statements) { - if (!ts.isPrologueDirective(statement)) - break; - if (seenPrologueDirectives.has(statement.expression.text)) - continue; - seenPrologueDirectives.add(statement.expression.text); - (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 getPrologueDirectivesFromBundledSourceFiles(bundle: ts.Bundle): ts.SourceFilePrologueInfo[] | undefined { + const seenPrologueDirectives = new ts.Set(); + let prologues: ts.SourceFilePrologueInfo[] | undefined; + for (let index = 0; index < bundle.sourceFiles.length; index++) { + const sourceFile = bundle.sourceFiles[index]; + let directives: ts.SourceFilePrologueDirective[] | undefined; + let end = 0; + for (const statement of sourceFile.statements) { + if (!ts.isPrologueDirective(statement)) + break; + if (seenPrologueDirectives.has(statement.expression.text)) + continue; + seenPrologueDirectives.add(statement.expression.text); + (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; } - return prologues; + if (directives) + (prologues || (prologues = [])).push({ file: index, text: sourceFile.text.substring(0, end), directives }); } + return prologues; + } - function emitShebangIfNeeded(sourceFileOrBundle: ts.Bundle | ts.SourceFile | ts.UnparsedSource) { - if (ts.isSourceFile(sourceFileOrBundle) || ts.isUnparsedSource(sourceFileOrBundle)) { - const shebang = ts.getShebang(sourceFileOrBundle.text); - if (shebang) { - writeComment(shebang); - writeLine(); + function emitShebangIfNeeded(sourceFileOrBundle: ts.Bundle | ts.SourceFile | ts.UnparsedSource) { + if (ts.isSourceFile(sourceFileOrBundle) || ts.isUnparsedSource(sourceFileOrBundle)) { + const shebang = ts.getShebang(sourceFileOrBundle.text); + if (shebang) { + writeComment(shebang); + writeLine(); + return true; + } + } + else { + for (const prepend of sourceFileOrBundle.prepends) { + ts.Debug.assertNode(prepend, ts.isUnparsedSource); + if (emitShebangIfNeeded(prepend)) { return true; } } - else { - for (const prepend of sourceFileOrBundle.prepends) { - ts.Debug.assertNode(prepend, ts.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 - // + // + // Helpers + // - function emitNodeWithWriter(node: ts.Node | undefined, writer: typeof write) { - if (!node) - return; - const savedWrite = write; - write = writer; - emit(node); - write = savedWrite; - } + function emitNodeWithWriter(node: ts.Node | undefined, writer: typeof write) { + if (!node) + return; + const savedWrite = write; + write = writer; + emit(node); + write = savedWrite; + } - function emitModifiers(node: ts.Node, modifiers: ts.NodeArray | undefined) { - if (modifiers && modifiers.length) { - emitList(node, modifiers, ts.ListFormat.Modifiers); - writeSpace(); - } + function emitModifiers(node: ts.Node, modifiers: ts.NodeArray | undefined) { + if (modifiers && modifiers.length) { + emitList(node, modifiers, ts.ListFormat.Modifiers); + writeSpace(); } + } - function emitTypeAnnotation(node: ts.TypeNode | undefined) { - if (node) { - writePunctuation(":"); - writeSpace(); - emit(node); - } + function emitTypeAnnotation(node: ts.TypeNode | undefined) { + if (node) { + writePunctuation(":"); + writeSpace(); + emit(node); } + } - function emitInitializer(node: ts.Expression | undefined, equalCommentStartPos: number, container: ts.Node, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { - if (node) { - writeSpace(); - emitTokenWithComment(ts.SyntaxKind.EqualsToken, equalCommentStartPos, writeOperator, container); - writeSpace(); - emitExpression(node, parenthesizerRule); - } + function emitInitializer(node: ts.Expression | undefined, equalCommentStartPos: number, container: ts.Node, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { + if (node) { + writeSpace(); + emitTokenWithComment(ts.SyntaxKind.EqualsToken, equalCommentStartPos, writeOperator, container); + writeSpace(); + emitExpression(node, parenthesizerRule); } + } - 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: ts.Node | undefined) { - if (node) { - writeSpace(); - emit(node); - } + function emitWithLeadingSpace(node: ts.Node | undefined) { + if (node) { + writeSpace(); + emit(node); } + } - function emitExpressionWithLeadingSpace(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { - if (node) { - writeSpace(); - emitExpression(node, parenthesizerRule); - } + function emitExpressionWithLeadingSpace(node: ts.Expression | undefined, parenthesizerRule?: (node: ts.Expression) => ts.Expression) { + if (node) { + writeSpace(); + emitExpression(node, parenthesizerRule); } + } - function emitWithTrailingSpace(node: ts.Node | undefined) { - if (node) { - emit(node); - writeSpace(); - } + function emitWithTrailingSpace(node: ts.Node | undefined) { + if (node) { + emit(node); + writeSpace(); } + } - function emitEmbeddedStatement(parent: ts.Node, node: ts.Statement) { - if (ts.isBlock(node) || ts.getEmitFlags(parent) & ts.EmitFlags.SingleLine) { - writeSpace(); - emit(node); + function emitEmbeddedStatement(parent: ts.Node, node: ts.Statement) { + if (ts.isBlock(node) || ts.getEmitFlags(parent) & ts.EmitFlags.SingleLine) { + writeSpace(); + emit(node); + } + else { + writeLine(); + increaseIndent(); + if (ts.isEmptyStatement(node)) { + pipelineEmit(ts.EmitHint.EmbeddedStatement, node); } else { - writeLine(); - increaseIndent(); - if (ts.isEmptyStatement(node)) { - pipelineEmit(ts.EmitHint.EmbeddedStatement, node); - } - else { - emit(node); - } - decreaseIndent(); + emit(node); } + decreaseIndent(); } + } + + function emitDecorators(parentNode: ts.Node, decorators: ts.NodeArray | undefined) { + emitList(parentNode, decorators, ts.ListFormat.Decorators); + } + + function emitTypeArguments(parentNode: ts.Node, typeArguments: ts.NodeArray | undefined) { + emitList(parentNode, typeArguments, ts.ListFormat.TypeArguments, typeArgumentParenthesizerRuleSelector); + } - function emitDecorators(parentNode: ts.Node, decorators: ts.NodeArray | undefined) { - emitList(parentNode, decorators, ts.ListFormat.Decorators); + function emitTypeParameters(parentNode: ts.SignatureDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.ClassDeclaration | ts.ClassExpression, typeParameters: ts.NodeArray | undefined) { + if (ts.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, ts.ListFormat.TypeParameters); + } + + function emitParameters(parentNode: ts.Node, parameters: ts.NodeArray) { + emitList(parentNode, parameters, ts.ListFormat.Parameters); + } + + function canEmitSimpleArrowHead(parentNode: ts.FunctionTypeNode | ts.ArrowFunction, parameters: ts.NodeArray) { + const parameter = ts.singleOrUndefined(parameters); + return parameter + && parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter + && ts.isArrowFunction(parentNode) // only arrow functions may have simple arrow head + && !parentNode.type // arrow function may not have return type annotation + && !ts.some(parentNode.decorators) // parent may not have decorators + && !ts.some(parentNode.modifiers) // parent may not have modifiers + && !ts.some(parentNode.typeParameters) // parent may not have type parameters + && !ts.some(parameter.decorators) // parameter may not have decorators + && !ts.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 + && ts.isIdentifier(parameter.name); // parameter name must be identifier + } - function emitTypeArguments(parentNode: ts.Node, typeArguments: ts.NodeArray | undefined) { - emitList(parentNode, typeArguments, ts.ListFormat.TypeArguments, typeArgumentParenthesizerRuleSelector); + function emitParametersForArrow(parentNode: ts.FunctionTypeNode | ts.ArrowFunction, parameters: ts.NodeArray) { + if (canEmitSimpleArrowHead(parentNode, parameters)) { + emitList(parentNode, parameters, ts.ListFormat.Parameters & ~ts.ListFormat.Parenthesis); + } + else { + emitParameters(parentNode, parameters); } + } - function emitTypeParameters(parentNode: ts.SignatureDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.ClassDeclaration | ts.ClassExpression, typeParameters: ts.NodeArray | undefined) { - if (ts.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, ts.ListFormat.TypeParameters); - } - - function emitParameters(parentNode: ts.Node, parameters: ts.NodeArray) { - emitList(parentNode, parameters, ts.ListFormat.Parameters); - } - - function canEmitSimpleArrowHead(parentNode: ts.FunctionTypeNode | ts.ArrowFunction, parameters: ts.NodeArray) { - const parameter = ts.singleOrUndefined(parameters); - return parameter - && parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter - && ts.isArrowFunction(parentNode) // only arrow functions may have simple arrow head - && !parentNode.type // arrow function may not have return type annotation - && !ts.some(parentNode.decorators) // parent may not have decorators - && !ts.some(parentNode.modifiers) // parent may not have modifiers - && !ts.some(parentNode.typeParameters) // parent may not have type parameters - && !ts.some(parameter.decorators) // parameter may not have decorators - && !ts.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 - && ts.isIdentifier(parameter.name); // parameter name must be identifier - } - - function emitParametersForArrow(parentNode: ts.FunctionTypeNode | ts.ArrowFunction, parameters: ts.NodeArray) { - if (canEmitSimpleArrowHead(parentNode, parameters)) { - emitList(parentNode, parameters, ts.ListFormat.Parameters & ~ts.ListFormat.Parenthesis); - } - else { - emitParameters(parentNode, parameters); - } + function emitParametersForIndexSignature(parentNode: ts.Node, parameters: ts.NodeArray) { + emitList(parentNode, parameters, ts.ListFormat.IndexSignatureParameters); + } + + function writeDelimiter(format: ts.ListFormat) { + switch (format & ts.ListFormat.DelimitersMask) { + case ts.ListFormat.None: + break; + case ts.ListFormat.CommaDelimited: + writePunctuation(","); + break; + case ts.ListFormat.BarDelimited: + writeSpace(); + writePunctuation("|"); + break; + case ts.ListFormat.AsteriskDelimited: + writeSpace(); + writePunctuation("*"); + writeSpace(); + break; + case ts.ListFormat.AmpersandDelimited: + writeSpace(); + writePunctuation("&"); + break; } + } + + function emitList(parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector, start?: number, count?: number) { + emitNodeList(emit, parentNode, children, format, parenthesizerRule, start, count); + } + + function emitExpressionList(parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector, start?: number, count?: number) { + emitNodeList(emitExpression, parentNode, children, format, parenthesizerRule, start, count); + } - function emitParametersForIndexSignature(parentNode: ts.Node, parameters: ts.NodeArray) { - emitList(parentNode, parameters, ts.ListFormat.IndexSignatureParameters); + function emitNodeList(emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start = 0, count = children ? children.length - start : 0) { + const isUndefined = children === undefined; + if (isUndefined && format & ts.ListFormat.OptionalIfUndefined) { + return; } - function writeDelimiter(format: ts.ListFormat) { - switch (format & ts.ListFormat.DelimitersMask) { - case ts.ListFormat.None: - break; - case ts.ListFormat.CommaDelimited: - writePunctuation(","); - break; - case ts.ListFormat.BarDelimited: - writeSpace(); - writePunctuation("|"); - break; - case ts.ListFormat.AsteriskDelimited: - writeSpace(); - writePunctuation("*"); - writeSpace(); - break; - case ts.ListFormat.AmpersandDelimited: - writeSpace(); - writePunctuation("&"); - break; + const isEmpty = children === undefined || start >= children.length || count === 0; + if (isEmpty && format & ts.ListFormat.OptionalIfEmpty) { + if (onBeforeEmitNodeArray) { + onBeforeEmitNodeArray(children); + } + if (onAfterEmitNodeArray) { + onAfterEmitNodeArray(children); } + return; } - function emitList(parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector, start?: number, count?: number) { - emitNodeList(emit, parentNode, children, format, parenthesizerRule, start, count); + if (format & ts.ListFormat.BracketsMask) { + writePunctuation(getOpeningBracket(format)); + if (isEmpty && children) { + emitTrailingCommentsOfPosition(children.pos, /*prefixSpace*/ true); // Emit comments within empty bracketed lists + } } - function emitExpressionList(parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector, start?: number, count?: number) { - emitNodeList(emitExpression, parentNode, children, format, parenthesizerRule, start, count); + if (onBeforeEmitNodeArray) { + onBeforeEmitNodeArray(children); } - function emitNodeList(emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parentNode: ts.Node | undefined, children: ts.NodeArray | undefined, format: ts.ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start = 0, count = children ? children.length - start : 0) { - const isUndefined = children === undefined; - if (isUndefined && format & ts.ListFormat.OptionalIfUndefined) { - return; - } - - const isEmpty = children === undefined || start >= children.length || count === 0; - if (isEmpty && format & ts.ListFormat.OptionalIfEmpty) { - if (onBeforeEmitNodeArray) { - onBeforeEmitNodeArray(children); - } - if (onAfterEmitNodeArray) { - onAfterEmitNodeArray(children); - } - return; + if (isEmpty) { + // Write a line terminator if the parent node was multi-line + if (format & ts.ListFormat.MultiLine && !(preserveSourceNewlines && (!parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile)))) { + writeLine(); } - - if (format & ts.ListFormat.BracketsMask) { - writePunctuation(getOpeningBracket(format)); - if (isEmpty && children) { - emitTrailingCommentsOfPosition(children.pos, /*prefixSpace*/ true); // Emit comments within empty bracketed lists - } + else if (format & ts.ListFormat.SpaceBetweenBraces && !(format & ts.ListFormat.NoSpaceIfEmpty)) { + writeSpace(); } - - if (onBeforeEmitNodeArray) { - onBeforeEmitNodeArray(children); + } + else { + ts.Debug.type>(children); + // Write the opening line terminator or leading whitespace. + const mayEmitInterveningComments = (format & ts.ListFormat.NoInterveningComments) === 0; + let shouldEmitInterveningComments = mayEmitInterveningComments; + const leadingLineTerminatorCount = getLeadingLineTerminatorCount(parentNode, children, format); // TODO: GH#18217 + if (leadingLineTerminatorCount) { + writeLine(leadingLineTerminatorCount); + shouldEmitInterveningComments = false; + } + else if (format & ts.ListFormat.SpaceBetweenBraces) { + writeSpace(); } - if (isEmpty) { - // Write a line terminator if the parent node was multi-line - if (format & ts.ListFormat.MultiLine && !(preserveSourceNewlines && (!parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile)))) { - writeLine(); - } - else if (format & ts.ListFormat.SpaceBetweenBraces && !(format & ts.ListFormat.NoSpaceIfEmpty)) { - writeSpace(); - } + // Increase the indent, if requested. + if (format & ts.ListFormat.Indented) { + increaseIndent(); } - else { - ts.Debug.type>(children); - // Write the opening line terminator or leading whitespace. - const mayEmitInterveningComments = (format & ts.ListFormat.NoInterveningComments) === 0; - let shouldEmitInterveningComments = mayEmitInterveningComments; - const leadingLineTerminatorCount = getLeadingLineTerminatorCount(parentNode, children, format); // TODO: GH#18217 - if (leadingLineTerminatorCount) { - writeLine(leadingLineTerminatorCount); - shouldEmitInterveningComments = false; - } - else if (format & ts.ListFormat.SpaceBetweenBraces) { - writeSpace(); - } - - // Increase the indent, if requested. - if (format & ts.ListFormat.Indented) { - increaseIndent(); - } - const emitListItem = getEmitListItem(emit, parenthesizerRule); + const emitListItem = getEmitListItem(emit, parenthesizerRule); - // Emit each child. - let previousSibling: ts.Node | undefined; - let previousSourceFileTextKind: ReturnType; - let shouldDecreaseIndentAfterEmit = false; - for (let i = 0; i < count; i++) { - const child = children[start + i]; + // Emit each child. + let previousSibling: ts.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 & ts.ListFormat.AsteriskDelimited) { - // always write JSDoc in the format "\n *" - writeLine(); - writeDelimiter(format); + // Write the delimiter if this is not the first node. + if (format & ts.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 & ts.ListFormat.DelimitersMask && previousSibling.end !== (parentNode ? parentNode.end : -1)) { + emitLeadingCommentsOfPosition(previousSibling.end); } - 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 & ts.ListFormat.DelimitersMask && previousSibling.end !== (parentNode ? parentNode.end : -1)) { - emitLeadingCommentsOfPosition(previousSibling.end); - } - writeDelimiter(format); - recordBundleFileInternalSectionEnd(previousSourceFileTextKind); - - // Write either a line terminator or whitespace to separate the elements. - const separatingLineTerminatorCount = getSeparatingLineTerminatorCount(previousSibling, child, format); - if (separatingLineTerminatorCount > 0) { - // If a synthesized node in a single-line list starts on a new - // line, we should increase the indent. - if ((format & (ts.ListFormat.LinesMask | ts.ListFormat.Indented)) === ts.ListFormat.SingleLine) { - increaseIndent(); - shouldDecreaseIndentAfterEmit = true; - } - - writeLine(separatingLineTerminatorCount); - shouldEmitInterveningComments = false; + writeDelimiter(format); + recordBundleFileInternalSectionEnd(previousSourceFileTextKind); + + // Write either a line terminator or whitespace to separate the elements. + const separatingLineTerminatorCount = getSeparatingLineTerminatorCount(previousSibling, child, format); + if (separatingLineTerminatorCount > 0) { + // If a synthesized node in a single-line list starts on a new + // line, we should increase the indent. + if ((format & (ts.ListFormat.LinesMask | ts.ListFormat.Indented)) === ts.ListFormat.SingleLine) { + increaseIndent(); + shouldDecreaseIndentAfterEmit = true; } - else if (previousSibling && format & ts.ListFormat.SpaceBetweenSiblings) { - writeSpace(); - } - } - // Emit this child. - previousSourceFileTextKind = recordBundleFileInternalSectionStart(child); - if (shouldEmitInterveningComments) { - const commentRange = ts.getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos); - } - else { - shouldEmitInterveningComments = mayEmitInterveningComments; + writeLine(separatingLineTerminatorCount); + shouldEmitInterveningComments = false; } - - nextListElementPos = child.pos; - emitListItem(child, emit, parenthesizerRule, i); - - if (shouldDecreaseIndentAfterEmit) { - decreaseIndent(); - shouldDecreaseIndentAfterEmit = false; + else if (previousSibling && format & ts.ListFormat.SpaceBetweenSiblings) { + writeSpace(); } - - previousSibling = child; } - // Write a trailing comma, if requested. - const emitFlags = previousSibling ? ts.getEmitFlags(previousSibling) : 0; - const skipTrailingComments = commentsDisabled || !!(emitFlags & ts.EmitFlags.NoTrailingComments); - const hasTrailingComma = children?.hasTrailingComma && (format & ts.ListFormat.AllowTrailingComma) && (format & ts.ListFormat.CommaDelimited); - if (hasTrailingComma) { - if (previousSibling && !skipTrailingComments) { - emitTokenWithComment(ts.SyntaxKind.CommaToken, previousSibling.end, writePunctuation, previousSibling); - } - else { - writePunctuation(","); - } + // Emit this child. + previousSourceFileTextKind = recordBundleFileInternalSectionStart(child); + if (shouldEmitInterveningComments) { + const commentRange = ts.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 && (parentNode ? parentNode.end : -1) !== previousSibling.end && (format & ts.ListFormat.DelimitersMask) && !skipTrailingComments) { - emitLeadingCommentsOfPosition(hasTrailingComma && children?.end ? children.end : previousSibling.end); + else { + shouldEmitInterveningComments = mayEmitInterveningComments; } - // Decrease the indent, if requested. - if (format & ts.ListFormat.Indented) { + nextListElementPos = child.pos; + emitListItem(child, emit, parenthesizerRule, i); + + if (shouldDecreaseIndentAfterEmit) { decreaseIndent(); + shouldDecreaseIndentAfterEmit = false; } - recordBundleFileInternalSectionEnd(previousSourceFileTextKind); + previousSibling = child; + } - // Write the closing line terminator or closing whitespace. - const closingLineTerminatorCount = getClosingLineTerminatorCount(parentNode, children, format); - if (closingLineTerminatorCount) { - writeLine(closingLineTerminatorCount); + // Write a trailing comma, if requested. + const emitFlags = previousSibling ? ts.getEmitFlags(previousSibling) : 0; + const skipTrailingComments = commentsDisabled || !!(emitFlags & ts.EmitFlags.NoTrailingComments); + const hasTrailingComma = children?.hasTrailingComma && (format & ts.ListFormat.AllowTrailingComma) && (format & ts.ListFormat.CommaDelimited); + if (hasTrailingComma) { + if (previousSibling && !skipTrailingComments) { + emitTokenWithComment(ts.SyntaxKind.CommaToken, previousSibling.end, writePunctuation, previousSibling); } - else if (format & (ts.ListFormat.SpaceAfterList | ts.ListFormat.SpaceBetweenBraces)) { - writeSpace(); + else { + writePunctuation(","); } } - if (onAfterEmitNodeArray) { - onAfterEmitNodeArray(children); + // Emit any trailing comment of the last element in the list + // i.e + // var array = [... + // 2 + // /* end of element 2 */ + // ]; + if (previousSibling && (parentNode ? parentNode.end : -1) !== previousSibling.end && (format & ts.ListFormat.DelimitersMask) && !skipTrailingComments) { + emitLeadingCommentsOfPosition(hasTrailingComma && children?.end ? children.end : previousSibling.end); } - if (format & ts.ListFormat.BracketsMask) { - if (isEmpty && children) { - emitLeadingCommentsOfPosition(children.end); // Emit leading comments within empty lists - } - writePunctuation(getClosingBracket(format)); + // Decrease the indent, if requested. + if (format & ts.ListFormat.Indented) { + decreaseIndent(); } - } - // Writers + recordBundleFileInternalSectionEnd(previousSourceFileTextKind); - function writeLiteral(s: string) { - writer.writeLiteral(s); + // Write the closing line terminator or closing whitespace. + const closingLineTerminatorCount = getClosingLineTerminatorCount(parentNode, children, format); + if (closingLineTerminatorCount) { + writeLine(closingLineTerminatorCount); + } + else if (format & (ts.ListFormat.SpaceAfterList | ts.ListFormat.SpaceBetweenBraces)) { + writeSpace(); + } } - function writeStringLiteral(s: string) { - writer.writeStringLiteral(s); + if (onAfterEmitNodeArray) { + onAfterEmitNodeArray(children); } - function writeBase(s: string) { - writer.write(s); + if (format & ts.ListFormat.BracketsMask) { + if (isEmpty && children) { + emitLeadingCommentsOfPosition(children.end); // Emit leading comments within empty lists + } + writePunctuation(getClosingBracket(format)); } + } - function writeSymbol(s: string, sym: ts.Symbol) { - writer.writeSymbol(s, sym); - } + // Writers - function writePunctuation(s: string) { - writer.writePunctuation(s); - } + function writeLiteral(s: string) { + writer.writeLiteral(s); + } - function writeTrailingSemicolon() { - writer.writeTrailingSemicolon(";"); - } + function writeStringLiteral(s: string) { + writer.writeStringLiteral(s); + } - function writeKeyword(s: string) { - writer.writeKeyword(s); - } + function writeBase(s: string) { + writer.write(s); + } - function writeOperator(s: string) { - writer.writeOperator(s); - } + function writeSymbol(s: string, sym: ts.Symbol) { + writer.writeSymbol(s, sym); + } - function writeParameter(s: string) { - writer.writeParameter(s); - } + 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 writeComment(s: string) { - writer.writeComment(s); + function writeProperty(s: string) { + writer.writeProperty(s); + } + + function nonEscapingWrite(s: string) { + // This should be defined in a snippet-escaping text writer. + if (writer.nonEscapingWrite) { + writer.nonEscapingWrite(s); + } + else { + writer.write(s); } + } - function writeSpace() { - writer.writeSpace(" "); + function writeLine(count = 1) { + for (let i = 0; i < count; i++) { + writer.writeLine(i > 0); } + } - function writeProperty(s: string) { - writer.writeProperty(s); + function increaseIndent() { + writer.increaseIndent(); + } + + function decreaseIndent() { + writer.decreaseIndent(); + } + + function writeToken(token: ts.SyntaxKind, pos: number, writer: (s: string) => void, contextNode?: ts.Node) { + return !sourceMapsDisabled + ? emitTokenWithSourceMap(contextNode, token, writer, pos, writeTokenText) + : writeTokenText(token, writer, pos); + } + + function writeTokenNode(node: ts.Node, writer: (s: string) => void) { + if (onBeforeEmitToken) { + onBeforeEmitToken(node); } + writer(ts.tokenToString(node.kind)!); + if (onAfterEmitToken) { + onAfterEmitToken(node); + } + } - function nonEscapingWrite(s: string) { - // This should be defined in a snippet-escaping text writer. - if (writer.nonEscapingWrite) { - writer.nonEscapingWrite(s); + function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void): void; + function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void, pos: number): number; + function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void, pos?: number): number { + const tokenString = ts.tokenToString(token)!; + writer(tokenString); + return pos! < 0 ? pos! : pos! + tokenString.length; + } + + function writeLineOrSpace(parentNode: ts.Node, prevChildNode: ts.Node, nextChildNode: ts.Node) { + if (ts.getEmitFlags(parentNode) & ts.EmitFlags.SingleLine) { + writeSpace(); + } + else if (preserveSourceNewlines) { + const lines = getLinesBetweenNodes(parentNode, prevChildNode, nextChildNode); + if (lines) { + writeLine(lines); } else { - writer.write(s); + writeSpace(); } } + else { + writeLine(); + } + } - function writeLine(count = 1) { - for (let i = 0; i < count; i++) { - writer.writeLine(i > 0); + function writeLines(text: string): void { + const lines = text.split(/\r\n?|\n/g); + const indentation = ts.guessIndentation(lines); + for (const lineText of lines) { + const line = indentation ? lineText.slice(indentation) : lineText; + if (line.length) { + writeLine(); + write(line); } } + } - function increaseIndent() { - writer.increaseIndent(); + function writeLinesAndIndent(lineCount: number, writeSpaceIfNotIndenting: boolean) { + if (lineCount) { + increaseIndent(); + writeLine(lineCount); + } + else if (writeSpaceIfNotIndenting) { + writeSpace(); } + } - function decreaseIndent() { - writer.decreaseIndent(); + // 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 | number | undefined, value2?: boolean | number) { + if (value1) { + decreaseIndent(); } - - function writeToken(token: ts.SyntaxKind, pos: number, writer: (s: string) => void, contextNode?: ts.Node) { - return !sourceMapsDisabled - ? emitTokenWithSourceMap(contextNode, token, writer, pos, writeTokenText) - : writeTokenText(token, writer, pos); + if (value2) { + decreaseIndent(); } + } - function writeTokenNode(node: ts.Node, writer: (s: string) => void) { - if (onBeforeEmitToken) { - onBeforeEmitToken(node); - } - writer(ts.tokenToString(node.kind)!); - if (onAfterEmitToken) { - onAfterEmitToken(node); + function getLeadingLineTerminatorCount(parentNode: ts.Node | undefined, children: readonly ts.Node[], format: ts.ListFormat): number { + if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { + if (format & ts.ListFormat.PreferNewLine) { + return 1; } - } - - function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void): void; - function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void, pos: number): number; - function writeTokenText(token: ts.SyntaxKind, writer: (s: string) => void, pos?: number): number { - const tokenString = ts.tokenToString(token)!; - writer(tokenString); - return pos! < 0 ? pos! : pos! + tokenString.length; - } - function writeLineOrSpace(parentNode: ts.Node, prevChildNode: ts.Node, nextChildNode: ts.Node) { - if (ts.getEmitFlags(parentNode) & ts.EmitFlags.SingleLine) { - writeSpace(); + const firstChild = children[0]; + if (firstChild === undefined) { + return !parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile) ? 0 : 1; } - else if (preserveSourceNewlines) { - const lines = getLinesBetweenNodes(parentNode, prevChildNode, nextChildNode); - if (lines) { - writeLine(lines); - } - else { - writeSpace(); - } + if (firstChild.pos === nextListElementPos) { + // If this child starts at the beginning of a list item in a parent list, its leading + // line terminators have already been written as the separating line terminators of the + // parent list. Example: + // + // class Foo { + // constructor() {} + // public foo() {} + // } + // + // The outer list is the list of class members, with one line terminator between the + // constructor and the method. The constructor is written, the separating line terminator + // is written, and then we start emitting the method. Its modifiers ([public]) constitute an inner + // list, so we look for its leading line terminators. If we didn't know that we had already + // written a newline as part of the parent list, it would appear that we need to write a + // leading newline to start the modifiers. + return 0; } - else { - writeLine(); + if (firstChild.kind === ts.SyntaxKind.JsxText) { + // JsxText will be written with its leading whitespace, so don't add more manually. + return 0; } - } - - function writeLines(text: string): void { - const lines = text.split(/\r\n?|\n/g); - const indentation = ts.guessIndentation(lines); - for (const lineText of lines) { - const line = indentation ? lineText.slice(indentation) : lineText; - if (line.length) { - writeLine(); - write(line); + if (currentSourceFile && parentNode && + !ts.positionIsSynthesized(parentNode.pos) && + !ts.nodeIsSynthesized(firstChild) && + (!firstChild.parent || ts.getOriginalNode(firstChild.parent) === ts.getOriginalNode(parentNode))) { + if (preserveSourceNewlines) { + return getEffectiveLines(includeComments => ts.getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(firstChild.pos, parentNode.pos, currentSourceFile!, includeComments)); } + return ts.rangeStartPositionsAreOnSameLine(parentNode, firstChild, currentSourceFile) ? 0 : 1; } - } - - function writeLinesAndIndent(lineCount: number, writeSpaceIfNotIndenting: boolean) { - if (lineCount) { - increaseIndent(); - writeLine(lineCount); - } - else if (writeSpaceIfNotIndenting) { - writeSpace(); + if (synthesizedNodeStartsOnNewLine(firstChild, format)) { + return 1; } } + return format & ts.ListFormat.MultiLine ? 1 : 0; + } - // 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 | number | undefined, value2?: boolean | number) { - if (value1) { - decreaseIndent(); - } - if (value2) { - decreaseIndent(); + function getSeparatingLineTerminatorCount(previousNode: ts.Node | undefined, nextNode: ts.Node, format: ts.ListFormat): number { + if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { + if (previousNode === undefined || nextNode === undefined) { + return 0; } - } - - function getLeadingLineTerminatorCount(parentNode: ts.Node | undefined, children: readonly ts.Node[], format: ts.ListFormat): number { - if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { - if (format & ts.ListFormat.PreferNewLine) { - return 1; - } - - const firstChild = children[0]; - if (firstChild === undefined) { - return !parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile) ? 0 : 1; - } - if (firstChild.pos === nextListElementPos) { - // If this child starts at the beginning of a list item in a parent list, its leading - // line terminators have already been written as the separating line terminators of the - // parent list. Example: - // - // class Foo { - // constructor() {} - // public foo() {} - // } - // - // The outer list is the list of class members, with one line terminator between the - // constructor and the method. The constructor is written, the separating line terminator - // is written, and then we start emitting the method. Its modifiers ([public]) constitute an inner - // list, so we look for its leading line terminators. If we didn't know that we had already - // written a newline as part of the parent list, it would appear that we need to write a - // leading newline to start the modifiers. - return 0; - } - if (firstChild.kind === ts.SyntaxKind.JsxText) { - // JsxText will be written with its leading whitespace, so don't add more manually. - return 0; - } - if (currentSourceFile && parentNode && - !ts.positionIsSynthesized(parentNode.pos) && - !ts.nodeIsSynthesized(firstChild) && - (!firstChild.parent || ts.getOriginalNode(firstChild.parent) === ts.getOriginalNode(parentNode))) { - if (preserveSourceNewlines) { - return getEffectiveLines(includeComments => ts.getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(firstChild.pos, parentNode.pos, currentSourceFile!, includeComments)); - } - return ts.rangeStartPositionsAreOnSameLine(parentNode, firstChild, currentSourceFile) ? 0 : 1; - } - if (synthesizedNodeStartsOnNewLine(firstChild, format)) { - return 1; - } + if (nextNode.kind === ts.SyntaxKind.JsxText) { + // JsxText will be written with its leading whitespace, so don't add more manually. + return 0; } - return format & ts.ListFormat.MultiLine ? 1 : 0; - } - - function getSeparatingLineTerminatorCount(previousNode: ts.Node | undefined, nextNode: ts.Node, format: ts.ListFormat): number { - if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { - if (previousNode === undefined || nextNode === undefined) { - return 0; - } - if (nextNode.kind === ts.SyntaxKind.JsxText) { - // JsxText will be written with its leading whitespace, so don't add more manually. - return 0; - } - else if (currentSourceFile && !ts.nodeIsSynthesized(previousNode) && !ts.nodeIsSynthesized(nextNode)) { - if (preserveSourceNewlines && siblingNodePositionsAreComparable(previousNode, nextNode)) { - return getEffectiveLines(includeComments => ts.getLinesBetweenRangeEndAndRangeStart(previousNode, nextNode, currentSourceFile!, includeComments)); - } - // If `preserveSourceNewlines` is `false` we do not intend to preserve the effective lines between the - // previous and next node. Instead we naively check whether nodes are on separate lines within the - // same node parent. If so, we intend to preserve a single line terminator. This is less precise and - // expensive than checking with `preserveSourceNewlines` as above, but the goal is not to preserve the - // effective source lines between two sibling nodes. - else if (!preserveSourceNewlines && originalNodesHaveSameParent(previousNode, nextNode)) { - return ts.rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile) ? 0 : 1; - } - // If the two nodes are not comparable, add a line terminator based on the format that can indicate - // whether new lines are preferred or not. - return format & ts.ListFormat.PreferNewLine ? 1 : 0; + else if (currentSourceFile && !ts.nodeIsSynthesized(previousNode) && !ts.nodeIsSynthesized(nextNode)) { + if (preserveSourceNewlines && siblingNodePositionsAreComparable(previousNode, nextNode)) { + return getEffectiveLines(includeComments => ts.getLinesBetweenRangeEndAndRangeStart(previousNode, nextNode, currentSourceFile!, includeComments)); } - else if (synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format)) { - return 1; + // If `preserveSourceNewlines` is `false` we do not intend to preserve the effective lines between the + // previous and next node. Instead we naively check whether nodes are on separate lines within the + // same node parent. If so, we intend to preserve a single line terminator. This is less precise and + // expensive than checking with `preserveSourceNewlines` as above, but the goal is not to preserve the + // effective source lines between two sibling nodes. + else if (!preserveSourceNewlines && originalNodesHaveSameParent(previousNode, nextNode)) { + return ts.rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile) ? 0 : 1; } + // If the two nodes are not comparable, add a line terminator based on the format that can indicate + // whether new lines are preferred or not. + return format & ts.ListFormat.PreferNewLine ? 1 : 0; } - else if (ts.getStartsOnNewLine(nextNode)) { + else if (synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format)) { return 1; } - return format & ts.ListFormat.MultiLine ? 1 : 0; } + else if (ts.getStartsOnNewLine(nextNode)) { + return 1; + } + return format & ts.ListFormat.MultiLine ? 1 : 0; + } - function getClosingLineTerminatorCount(parentNode: ts.Node | undefined, children: readonly ts.Node[], format: ts.ListFormat): number { - if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { - if (format & ts.ListFormat.PreferNewLine) { - return 1; - } + function getClosingLineTerminatorCount(parentNode: ts.Node | undefined, children: readonly ts.Node[], format: ts.ListFormat): number { + if (format & ts.ListFormat.PreserveLines || preserveSourceNewlines) { + if (format & ts.ListFormat.PreferNewLine) { + return 1; + } - const lastChild = ts.lastOrUndefined(children); - if (lastChild === undefined) { - return !parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile) ? 0 : 1; - } - if (currentSourceFile && parentNode && !ts.positionIsSynthesized(parentNode.pos) && !ts.nodeIsSynthesized(lastChild) && (!lastChild.parent || lastChild.parent === parentNode)) { - if (preserveSourceNewlines) { - const end = ts.isNodeArray(children) && !ts.positionIsSynthesized(children.end) ? children.end : lastChild.end; - return getEffectiveLines(includeComments => ts.getLinesBetweenPositionAndNextNonWhitespaceCharacter(end, parentNode.end, currentSourceFile!, includeComments)); - } - return ts.rangeEndPositionsAreOnSameLine(parentNode, lastChild, currentSourceFile) ? 0 : 1; - } - if (synthesizedNodeStartsOnNewLine(lastChild, format)) { - return 1; + const lastChild = ts.lastOrUndefined(children); + if (lastChild === undefined) { + return !parentNode || currentSourceFile && ts.rangeIsOnSingleLine(parentNode, currentSourceFile) ? 0 : 1; + } + if (currentSourceFile && parentNode && !ts.positionIsSynthesized(parentNode.pos) && !ts.nodeIsSynthesized(lastChild) && (!lastChild.parent || lastChild.parent === parentNode)) { + if (preserveSourceNewlines) { + const end = ts.isNodeArray(children) && !ts.positionIsSynthesized(children.end) ? children.end : lastChild.end; + return getEffectiveLines(includeComments => ts.getLinesBetweenPositionAndNextNonWhitespaceCharacter(end, parentNode.end, currentSourceFile!, includeComments)); } + return ts.rangeEndPositionsAreOnSameLine(parentNode, lastChild, currentSourceFile) ? 0 : 1; } - if (format & ts.ListFormat.MultiLine && !(format & ts.ListFormat.NoTrailingNewLine)) { + if (synthesizedNodeStartsOnNewLine(lastChild, format)) { return 1; } - return 0; } + if (format & ts.ListFormat.MultiLine && !(format & ts.ListFormat.NoTrailingNewLine)) { + return 1; + } + return 0; + } - function getEffectiveLines(getLineDifference: (includeComments: boolean) => number) { - // If 'preserveSourceNewlines' is disabled, we should never call this function - // because it could be more expensive than alternative approximations. - ts.Debug.assert(!!preserveSourceNewlines); - // We start by measuring the line difference from a position to its adjacent comments, - // so that this is counted as a one-line difference, not two: + function getEffectiveLines(getLineDifference: (includeComments: boolean) => number) { + // If 'preserveSourceNewlines' is disabled, we should never call this function + // because it could be more expensive than alternative approximations. + ts.Debug.assert(!!preserveSourceNewlines); + // We start by measuring the line difference from a position to its adjacent comments, + // so that this is counted as a one-line difference, not two: + // + // node1; + // // NODE2 COMMENT + // node2; + const lines = getLineDifference(/*includeComments*/ true); + if (lines === 0) { + // However, if the line difference considering comments was 0, we might have this: // - // node1; - // // NODE2 COMMENT + // node1; // NODE2 COMMENT // node2; - const lines = getLineDifference(/*includeComments*/ true); - if (lines === 0) { - // However, if the line difference considering comments was 0, we might have this: - // - // node1; // NODE2 COMMENT - // node2; - // - // in which case we should be ignoring node2's comment, so this too is counted as - // a one-line difference, not zero. - return getLineDifference(/*includeComments*/ false); - } - return lines; + // + // in which case we should be ignoring node2's comment, so this too is counted as + // a one-line difference, not zero. + return getLineDifference(/*includeComments*/ false); } + return lines; + } - function writeLineSeparatorsAndIndentBefore(node: ts.Node, parent: ts.Node): boolean { - const leadingNewlines = preserveSourceNewlines && getLeadingLineTerminatorCount(parent, [node], ts.ListFormat.None); - if (leadingNewlines) { - writeLinesAndIndent(leadingNewlines, /*writeSpaceIfNotIndenting*/ false); - } - return !!leadingNewlines; + function writeLineSeparatorsAndIndentBefore(node: ts.Node, parent: ts.Node): boolean { + const leadingNewlines = preserveSourceNewlines && getLeadingLineTerminatorCount(parent, [node], ts.ListFormat.None); + if (leadingNewlines) { + writeLinesAndIndent(leadingNewlines, /*writeSpaceIfNotIndenting*/ false); } + return !!leadingNewlines; + } - function writeLineSeparatorsAfter(node: ts.Node, parent: ts.Node) { - const trailingNewlines = preserveSourceNewlines && getClosingLineTerminatorCount(parent, [node], ts.ListFormat.None); - if (trailingNewlines) { - writeLine(trailingNewlines); - } + function writeLineSeparatorsAfter(node: ts.Node, parent: ts.Node) { + const trailingNewlines = preserveSourceNewlines && getClosingLineTerminatorCount(parent, [node], ts.ListFormat.None); + if (trailingNewlines) { + writeLine(trailingNewlines); } + } - function synthesizedNodeStartsOnNewLine(node: ts.Node, format: ts.ListFormat) { - if (ts.nodeIsSynthesized(node)) { - const startsOnNewLine = ts.getStartsOnNewLine(node); - if (startsOnNewLine === undefined) { - return (format & ts.ListFormat.PreferNewLine) !== 0; - } - - return startsOnNewLine; + function synthesizedNodeStartsOnNewLine(node: ts.Node, format: ts.ListFormat) { + if (ts.nodeIsSynthesized(node)) { + const startsOnNewLine = ts.getStartsOnNewLine(node); + if (startsOnNewLine === undefined) { + return (format & ts.ListFormat.PreferNewLine) !== 0; } - return (format & ts.ListFormat.PreferNewLine) !== 0; + return startsOnNewLine; } - function getLinesBetweenNodes(parent: ts.Node, node1: ts.Node, node2: ts.Node): number { - if (ts.getEmitFlags(parent) & ts.EmitFlags.NoIndentation) { - return 0; - } - - parent = skipSynthesizedParentheses(parent); - node1 = skipSynthesizedParentheses(node1); - node2 = skipSynthesizedParentheses(node2); - - // Always use a newline for synthesized code if the synthesizer desires it. - if (ts.getStartsOnNewLine(node2)) { - return 1; - } - - if (currentSourceFile && !ts.nodeIsSynthesized(parent) && !ts.nodeIsSynthesized(node1) && !ts.nodeIsSynthesized(node2)) { - if (preserveSourceNewlines) { - return getEffectiveLines(includeComments => ts.getLinesBetweenRangeEndAndRangeStart(node1, node2, currentSourceFile!, includeComments)); - } - return ts.rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile) ? 0 : 1; - } + return (format & ts.ListFormat.PreferNewLine) !== 0; + } + function getLinesBetweenNodes(parent: ts.Node, node1: ts.Node, node2: ts.Node): number { + if (ts.getEmitFlags(parent) & ts.EmitFlags.NoIndentation) { return 0; } - function isEmptyBlock(block: ts.BlockLike) { - return block.statements.length === 0 - && (!currentSourceFile || ts.rangeEndIsOnSameLineAsRangeStart(block, block, currentSourceFile)); + parent = skipSynthesizedParentheses(parent); + node1 = skipSynthesizedParentheses(node1); + node2 = skipSynthesizedParentheses(node2); + + // Always use a newline for synthesized code if the synthesizer desires it. + if (ts.getStartsOnNewLine(node2)) { + return 1; } - function skipSynthesizedParentheses(node: ts.Node) { - while (node.kind === ts.SyntaxKind.ParenthesizedExpression && ts.nodeIsSynthesized(node)) { - node = (node as ts.ParenthesizedExpression).expression; + if (currentSourceFile && !ts.nodeIsSynthesized(parent) && !ts.nodeIsSynthesized(node1) && !ts.nodeIsSynthesized(node2)) { + if (preserveSourceNewlines) { + return getEffectiveLines(includeComments => ts.getLinesBetweenRangeEndAndRangeStart(node1, node2, currentSourceFile!, includeComments)); } + return ts.rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile) ? 0 : 1; + } + + return 0; + } + + function isEmptyBlock(block: ts.BlockLike) { + return block.statements.length === 0 + && (!currentSourceFile || ts.rangeEndIsOnSameLineAsRangeStart(block, block, currentSourceFile)); + } - return node; + function skipSynthesizedParentheses(node: ts.Node) { + while (node.kind === ts.SyntaxKind.ParenthesizedExpression && ts.nodeIsSynthesized(node)) { + node = (node as ts.ParenthesizedExpression).expression; } - function getTextOfNode(node: ts.Identifier | ts.PrivateIdentifier | ts.LiteralExpression, includeTrivia?: boolean): string { - if (ts.isGeneratedIdentifier(node)) { - return generateName(node); + return node; + } + + function getTextOfNode(node: ts.Identifier | ts.PrivateIdentifier | ts.LiteralExpression, includeTrivia?: boolean): string { + if (ts.isGeneratedIdentifier(node)) { + return generateName(node); + } + if (ts.isStringLiteral(node) && node.textSourceNode) { + return getTextOfNode(node.textSourceNode, includeTrivia); + } + const sourceFile = currentSourceFile; // const needed for control flow + const canUseSourceFile = !!sourceFile && !!node.parent && !ts.nodeIsSynthesized(node); + if (ts.isMemberName(node)) { + if (!canUseSourceFile || ts.getSourceFileOfNode(node) !== ts.getOriginalNode(sourceFile)) { + return ts.idText(node); } - if (ts.isStringLiteral(node) && node.textSourceNode) { - return getTextOfNode(node.textSourceNode, includeTrivia); + } + else { + ts.Debug.assertNode(node, ts.isLiteralExpression); // not strictly necessary + if (!canUseSourceFile) { + return node.text; } - const sourceFile = currentSourceFile; // const needed for control flow - const canUseSourceFile = !!sourceFile && !!node.parent && !ts.nodeIsSynthesized(node); - if (ts.isMemberName(node)) { - if (!canUseSourceFile || ts.getSourceFileOfNode(node) !== ts.getOriginalNode(sourceFile)) { - return ts.idText(node); - } + } + return ts.getSourceTextOfNodeFromSourceFile(sourceFile, node, includeTrivia); + } + + function getLiteralTextOfNode(node: ts.LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string { + if (node.kind === ts.SyntaxKind.StringLiteral && (node as ts.StringLiteral).textSourceNode) { + const textSourceNode = (node as ts.StringLiteral).textSourceNode!; + if (ts.isIdentifier(textSourceNode) || ts.isNumericLiteral(textSourceNode)) { + const text = ts.isNumericLiteral(textSourceNode) ? textSourceNode.text : getTextOfNode(textSourceNode); + return jsxAttributeEscape ? `"${ts.escapeJsxAttributeString(text)}"` : + neverAsciiEscape || (ts.getEmitFlags(node) & ts.EmitFlags.NoAsciiEscaping) ? `"${ts.escapeString(text)}"` : + `"${ts.escapeNonAsciiString(text)}"`; } else { - ts.Debug.assertNode(node, ts.isLiteralExpression); // not strictly necessary - if (!canUseSourceFile) { - return node.text; - } + return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape); } - return ts.getSourceTextOfNodeFromSourceFile(sourceFile, node, includeTrivia); } - function getLiteralTextOfNode(node: ts.LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string { - if (node.kind === ts.SyntaxKind.StringLiteral && (node as ts.StringLiteral).textSourceNode) { - const textSourceNode = (node as ts.StringLiteral).textSourceNode!; - if (ts.isIdentifier(textSourceNode) || ts.isNumericLiteral(textSourceNode)) { - const text = ts.isNumericLiteral(textSourceNode) ? textSourceNode.text : getTextOfNode(textSourceNode); - return jsxAttributeEscape ? `"${ts.escapeJsxAttributeString(text)}"` : - neverAsciiEscape || (ts.getEmitFlags(node) & ts.EmitFlags.NoAsciiEscaping) ? `"${ts.escapeString(text)}"` : - `"${ts.escapeNonAsciiString(text)}"`; - } - else { - return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape); - } - } + const flags = (neverAsciiEscape ? ts.GetLiteralTextFlags.NeverAsciiEscape : 0) + | (jsxAttributeEscape ? ts.GetLiteralTextFlags.JsxAttributeEscape : 0) + | (printerOptions.terminateUnterminatedLiterals ? ts.GetLiteralTextFlags.TerminateUnterminatedLiterals : 0) + | (printerOptions.target && printerOptions.target === ts.ScriptTarget.ESNext ? ts.GetLiteralTextFlags.AllowNumericSeparator : 0); + return ts.getLiteralText(node, currentSourceFile, flags); + } - const flags = (neverAsciiEscape ? ts.GetLiteralTextFlags.NeverAsciiEscape : 0) - | (jsxAttributeEscape ? ts.GetLiteralTextFlags.JsxAttributeEscape : 0) - | (printerOptions.terminateUnterminatedLiterals ? ts.GetLiteralTextFlags.TerminateUnterminatedLiterals : 0) - | (printerOptions.target && printerOptions.target === ts.ScriptTarget.ESNext ? ts.GetLiteralTextFlags.AllowNumericSeparator : 0); - return ts.getLiteralText(node, currentSourceFile, flags); + /** + * Push a new name generation scope. + */ + function pushNameGenerationScope(node: ts.Node | undefined) { + if (node && ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { + return; } + tempFlagsStack.push(tempFlags); + tempFlags = 0; + reservedNamesStack.push(reservedNames); + } - /** - * Push a new name generation scope. - */ - function pushNameGenerationScope(node: ts.Node | undefined) { - if (node && ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { - return; - } - tempFlagsStack.push(tempFlags); - tempFlags = 0; - reservedNamesStack.push(reservedNames); + /** + * Pop the current name generation scope. + */ + function popNameGenerationScope(node: ts.Node | undefined) { + if (node && ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { + return; } + tempFlags = tempFlagsStack.pop()!; + reservedNames = reservedNamesStack.pop()!; + } - /** - * Pop the current name generation scope. - */ - function popNameGenerationScope(node: ts.Node | undefined) { - if (node && ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { - return; - } - tempFlags = tempFlagsStack.pop()!; - reservedNames = reservedNamesStack.pop()!; + function reserveNameInNestedScopes(name: string) { + if (!reservedNames || reservedNames === ts.lastOrUndefined(reservedNamesStack)) { + reservedNames = new ts.Set(); } + reservedNames.add(name); + } - function reserveNameInNestedScopes(name: string) { - if (!reservedNames || reservedNames === ts.lastOrUndefined(reservedNamesStack)) { - reservedNames = new ts.Set(); - } - reservedNames.add(name); + function generateNames(node: ts.Node | undefined) { + if (!node) + return; + switch (node.kind) { + case ts.SyntaxKind.Block: + ts.forEach((node as ts.Block).statements, generateNames); + break; + case ts.SyntaxKind.LabeledStatement: + case ts.SyntaxKind.WithStatement: + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WhileStatement: + generateNames((node as ts.LabeledStatement | ts.WithStatement | ts.DoStatement | ts.WhileStatement).statement); + break; + case ts.SyntaxKind.IfStatement: + generateNames((node as ts.IfStatement).thenStatement); + generateNames((node as ts.IfStatement).elseStatement); + break; + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.ForInStatement: + generateNames((node as ts.ForStatement | ts.ForInOrOfStatement).initializer); + generateNames((node as ts.ForStatement | ts.ForInOrOfStatement).statement); + break; + case ts.SyntaxKind.SwitchStatement: + generateNames((node as ts.SwitchStatement).caseBlock); + break; + case ts.SyntaxKind.CaseBlock: + ts.forEach((node as ts.CaseBlock).clauses, generateNames); + break; + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.DefaultClause: + ts.forEach((node as ts.CaseOrDefaultClause).statements, generateNames); + break; + case ts.SyntaxKind.TryStatement: + generateNames((node as ts.TryStatement).tryBlock); + generateNames((node as ts.TryStatement).catchClause); + generateNames((node as ts.TryStatement).finallyBlock); + break; + case ts.SyntaxKind.CatchClause: + generateNames((node as ts.CatchClause).variableDeclaration); + generateNames((node as ts.CatchClause).block); + break; + case ts.SyntaxKind.VariableStatement: + generateNames((node as ts.VariableStatement).declarationList); + break; + case ts.SyntaxKind.VariableDeclarationList: + ts.forEach((node as ts.VariableDeclarationList).declarations, generateNames); + break; + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.ClassDeclaration: + generateNameIfNeeded((node as ts.NamedDeclaration).name); + break; + case ts.SyntaxKind.FunctionDeclaration: + generateNameIfNeeded((node as ts.FunctionDeclaration).name); + if (ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { + ts.forEach((node as ts.FunctionDeclaration).parameters, generateNames); + generateNames((node as ts.FunctionDeclaration).body); + } + break; + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + ts.forEach((node as ts.BindingPattern).elements, generateNames); + break; + case ts.SyntaxKind.ImportDeclaration: + generateNames((node as ts.ImportDeclaration).importClause); + break; + case ts.SyntaxKind.ImportClause: + generateNameIfNeeded((node as ts.ImportClause).name); + generateNames((node as ts.ImportClause).namedBindings); + break; + case ts.SyntaxKind.NamespaceImport: + generateNameIfNeeded((node as ts.NamespaceImport).name); + break; + case ts.SyntaxKind.NamespaceExport: + generateNameIfNeeded((node as ts.NamespaceExport).name); + break; + case ts.SyntaxKind.NamedImports: + ts.forEach((node as ts.NamedImports).elements, generateNames); + break; + case ts.SyntaxKind.ImportSpecifier: + generateNameIfNeeded((node as ts.ImportSpecifier).propertyName || (node as ts.ImportSpecifier).name); + break; } + } - function generateNames(node: ts.Node | undefined) { - if (!node) - return; - switch (node.kind) { - case ts.SyntaxKind.Block: - ts.forEach((node as ts.Block).statements, generateNames); - break; - case ts.SyntaxKind.LabeledStatement: - case ts.SyntaxKind.WithStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WhileStatement: - generateNames((node as ts.LabeledStatement | ts.WithStatement | ts.DoStatement | ts.WhileStatement).statement); - break; - case ts.SyntaxKind.IfStatement: - generateNames((node as ts.IfStatement).thenStatement); - generateNames((node as ts.IfStatement).elseStatement); - break; - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForInStatement: - generateNames((node as ts.ForStatement | ts.ForInOrOfStatement).initializer); - generateNames((node as ts.ForStatement | ts.ForInOrOfStatement).statement); - break; - case ts.SyntaxKind.SwitchStatement: - generateNames((node as ts.SwitchStatement).caseBlock); - break; - case ts.SyntaxKind.CaseBlock: - ts.forEach((node as ts.CaseBlock).clauses, generateNames); - break; - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: - ts.forEach((node as ts.CaseOrDefaultClause).statements, generateNames); - break; - case ts.SyntaxKind.TryStatement: - generateNames((node as ts.TryStatement).tryBlock); - generateNames((node as ts.TryStatement).catchClause); - generateNames((node as ts.TryStatement).finallyBlock); - break; - case ts.SyntaxKind.CatchClause: - generateNames((node as ts.CatchClause).variableDeclaration); - generateNames((node as ts.CatchClause).block); - break; - case ts.SyntaxKind.VariableStatement: - generateNames((node as ts.VariableStatement).declarationList); - break; - case ts.SyntaxKind.VariableDeclarationList: - ts.forEach((node as ts.VariableDeclarationList).declarations, generateNames); - break; - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.ClassDeclaration: - generateNameIfNeeded((node as ts.NamedDeclaration).name); - break; - case ts.SyntaxKind.FunctionDeclaration: - generateNameIfNeeded((node as ts.FunctionDeclaration).name); - if (ts.getEmitFlags(node) & ts.EmitFlags.ReuseTempVariableScope) { - ts.forEach((node as ts.FunctionDeclaration).parameters, generateNames); - generateNames((node as ts.FunctionDeclaration).body); - } - break; - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ArrayBindingPattern: - ts.forEach((node as ts.BindingPattern).elements, generateNames); - break; - case ts.SyntaxKind.ImportDeclaration: - generateNames((node as ts.ImportDeclaration).importClause); - break; - case ts.SyntaxKind.ImportClause: - generateNameIfNeeded((node as ts.ImportClause).name); - generateNames((node as ts.ImportClause).namedBindings); - break; - case ts.SyntaxKind.NamespaceImport: - generateNameIfNeeded((node as ts.NamespaceImport).name); - break; - case ts.SyntaxKind.NamespaceExport: - generateNameIfNeeded((node as ts.NamespaceExport).name); - break; - case ts.SyntaxKind.NamedImports: - ts.forEach((node as ts.NamedImports).elements, generateNames); - break; - case ts.SyntaxKind.ImportSpecifier: - generateNameIfNeeded((node as ts.ImportSpecifier).propertyName || (node as ts.ImportSpecifier).name); - break; - } + function generateMemberNames(node: ts.Node | undefined) { + if (!node) + return; + switch (node.kind) { + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + generateNameIfNeeded((node as ts.NamedDeclaration).name); + break; } + } - function generateMemberNames(node: ts.Node | undefined) { - if (!node) - return; - switch (node.kind) { - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - generateNameIfNeeded((node as ts.NamedDeclaration).name); - break; + function generateNameIfNeeded(name: ts.DeclarationName | undefined) { + if (name) { + if (ts.isGeneratedIdentifier(name)) { + generateName(name); } - } - - function generateNameIfNeeded(name: ts.DeclarationName | undefined) { - if (name) { - if (ts.isGeneratedIdentifier(name)) { - generateName(name); - } - else if (ts.isBindingPattern(name)) { - generateNames(name); - } + else if (ts.isBindingPattern(name)) { + generateNames(name); } } + } - /** - * Generate the text for a generated identifier. - */ - function generateName(name: ts.GeneratedIdentifier) { - if ((name.autoGenerateFlags & ts.GeneratedIdentifierFlags.KindMask) === ts.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)); - } + /** + * Generate the text for a generated identifier. + */ + function generateName(name: ts.GeneratedIdentifier) { + if ((name.autoGenerateFlags & ts.GeneratedIdentifierFlags.KindMask) === ts.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 generateNameCached(node: ts.Node, flags?: ts.GeneratedIdentifierFlags) { - const nodeId = ts.getNodeId(node); - return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = generateNameForNode(node, flags)); + else { + // Auto, Loop, and Unique names are cached based on their unique + // autoGenerateId. + const autoGenerateId = name.autoGenerateId!; + return autoGeneratedIdToGeneratedName[autoGenerateId] || (autoGeneratedIdToGeneratedName[autoGenerateId] = makeName(name)); } + } - /** - * 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)); - } + function generateNameCached(node: ts.Node, flags?: ts.GeneratedIdentifierFlags) { + const nodeId = ts.getNodeId(node); + return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = generateNameForNode(node, flags)); + } - /** - * 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 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 within a container. - */ - function isUniqueLocalName(name: string, container: ts.Node): boolean { - for (let node = container; ts.isNodeDescendantOf(node, container); node = node.nextContainer!) { - if (node.locals) { - const local = node.locals.get(ts.escapeLeadingUnderscores(name)); - // We conservatively include alias symbols to cover cases where they're emitted as locals - if (local && local.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)) { - return false; - } + /** + * 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: ts.Node): boolean { + for (let node = container; ts.isNodeDescendantOf(node, container); node = node.nextContainer!) { + if (node.locals) { + const local = node.locals.get(ts.escapeLeadingUnderscores(name)); + // We conservatively include alias symbols to cover cases where they're emitted as locals + if (local && local.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)) { + return false; } } - return true; } + 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 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(ts.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(ts.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.add(baseName); - } - 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); } + else { + generatedNames.add(baseName); + } + return baseName; } - // Find the first unique 'name_n', where n is a positive number - if (baseName.charCodeAt(baseName.length - 1) !== ts.CharacterCodes._) { - baseName += "_"; - } - let i = 1; - while (true) { - const generatedName = baseName + i; - if (checkFn(generatedName)) { - if (scoped) { - reserveNameInNestedScopes(generatedName); - } - else { - generatedNames.add(generatedName); - } - return generatedName; + } + // Find the first unique 'name_n', where n is a positive number + if (baseName.charCodeAt(baseName.length - 1) !== ts.CharacterCodes._) { + baseName += "_"; + } + let i = 1; + while (true) { + const generatedName = baseName + i; + if (checkFn(generatedName)) { + if (scoped) { + reserveNameInNestedScopes(generatedName); + } + else { + generatedNames.add(generatedName); } - i++; + return generatedName; } + i++; } + } + + function makeFileLevelOptimisticUniqueName(name: string) { + return makeUniqueName(name, isFileLevelUniqueName, /*optimistic*/ true); + } + + /** + * Generates a unique name for a ModuleDeclaration or EnumDeclaration. + */ + function generateNameForModuleOrEnum(node: ts.ModuleDeclaration | ts.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: ts.ImportDeclaration | ts.ExportDeclaration) { + const expr = ts.getExternalModuleName(node)!; // TODO: GH#18217 + const baseName = ts.isStringLiteral(expr) ? + ts.makeIdentifierFromModuleName(expr.text) : "module"; + return makeUniqueName(baseName); + } + + /** + * Generates a unique name for a default export. + */ + function generateNameForExportDefault() { + return makeUniqueName("default"); + } - function makeFileLevelOptimisticUniqueName(name: string) { - return makeUniqueName(name, isFileLevelUniqueName, /*optimistic*/ true); - } + /** + * Generates a unique name for a class expression. + */ + function generateNameForClassExpression() { + return makeUniqueName("class"); + } - /** - * Generates a unique name for a ModuleDeclaration or EnumDeclaration. - */ - function generateNameForModuleOrEnum(node: ts.ModuleDeclaration | ts.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); + function generateNameForMethodOrAccessor(node: ts.MethodDeclaration | ts.AccessorDeclaration) { + if (ts.isIdentifier(node.name)) { + return generateNameCached(node.name); } + return makeTempVariableName(TempFlags.Auto); + } - /** - * Generates a unique name for an ImportDeclaration or ExportDeclaration. - */ - function generateNameForImportOrExportDeclaration(node: ts.ImportDeclaration | ts.ExportDeclaration) { - const expr = ts.getExternalModuleName(node)!; // TODO: GH#18217 - const baseName = ts.isStringLiteral(expr) ? - ts.makeIdentifierFromModuleName(expr.text) : "module"; - return makeUniqueName(baseName); + /** + * Generates a unique name from a node. + */ + function generateNameForNode(node: ts.Node, flags?: ts.GeneratedIdentifierFlags): string { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return makeUniqueName(getTextOfNode(node as ts.Identifier), isUniqueName, !!(flags! & ts.GeneratedIdentifierFlags.Optimistic), !!(flags! & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.EnumDeclaration: + return generateNameForModuleOrEnum(node as ts.ModuleDeclaration | ts.EnumDeclaration); + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportDeclaration: + return generateNameForImportOrExportDeclaration(node as ts.ImportDeclaration | ts.ExportDeclaration); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ExportAssignment: + return generateNameForExportDefault(); + case ts.SyntaxKind.ClassExpression: + return generateNameForClassExpression(); + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return generateNameForMethodOrAccessor(node as ts.MethodDeclaration | ts.AccessorDeclaration); + case ts.SyntaxKind.ComputedPropertyName: + return makeTempVariableName(TempFlags.Auto, /*reserveInNestedScopes*/ true); + default: + return makeTempVariableName(TempFlags.Auto); } + } - /** - * Generates a unique name for a default export. - */ - function generateNameForExportDefault() { - return makeUniqueName("default"); - } + /** + * Generates a unique identifier for a node. + */ + function makeName(name: ts.GeneratedIdentifier) { + switch (name.autoGenerateFlags & ts.GeneratedIdentifierFlags.KindMask) { + case ts.GeneratedIdentifierFlags.Auto: + return makeTempVariableName(TempFlags.Auto, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); + case ts.GeneratedIdentifierFlags.Loop: + return makeTempVariableName(TempFlags._i, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); + case ts.GeneratedIdentifierFlags.Unique: + return makeUniqueName(ts.idText(name), (name.autoGenerateFlags & ts.GeneratedIdentifierFlags.FileLevel) ? isFileLevelUniqueName : isUniqueName, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.Optimistic), !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); + } + return ts.Debug.fail("Unsupported GeneratedIdentifierKind."); + } - /** - * Generates a unique name for a class expression. - */ - function generateNameForClassExpression() { - return makeUniqueName("class"); - } + /** + * Gets the node from which a name should be generated. + */ + function getNodeForGeneratedName(name: ts.GeneratedIdentifier) { + const autoGenerateId = name.autoGenerateId; + let node = name as ts.Node; + let original = node.original; + while (original) { + node = original; - function generateNameForMethodOrAccessor(node: ts.MethodDeclaration | ts.AccessorDeclaration) { - if (ts.isIdentifier(node.name)) { - return generateNameCached(node.name); + // if "node" is a different generated name (having a different + // "autoGenerateId"), use it and stop traversing. + if (ts.isIdentifier(node) + && !!(node.autoGenerateFlags! & ts.GeneratedIdentifierFlags.Node) + && node.autoGenerateId !== autoGenerateId) { + break; } - return makeTempVariableName(TempFlags.Auto); - } - /** - * Generates a unique name from a node. - */ - function generateNameForNode(node: ts.Node, flags?: ts.GeneratedIdentifierFlags): string { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return makeUniqueName(getTextOfNode(node as ts.Identifier), isUniqueName, !!(flags! & ts.GeneratedIdentifierFlags.Optimistic), !!(flags! & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.EnumDeclaration: - return generateNameForModuleOrEnum(node as ts.ModuleDeclaration | ts.EnumDeclaration); - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ExportDeclaration: - return generateNameForImportOrExportDeclaration(node as ts.ImportDeclaration | ts.ExportDeclaration); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ExportAssignment: - return generateNameForExportDefault(); - case ts.SyntaxKind.ClassExpression: - return generateNameForClassExpression(); - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return generateNameForMethodOrAccessor(node as ts.MethodDeclaration | ts.AccessorDeclaration); - case ts.SyntaxKind.ComputedPropertyName: - return makeTempVariableName(TempFlags.Auto, /*reserveInNestedScopes*/ true); - default: - return makeTempVariableName(TempFlags.Auto); - } + original = node.original; } - /** - * Generates a unique identifier for a node. - */ - function makeName(name: ts.GeneratedIdentifier) { - switch (name.autoGenerateFlags & ts.GeneratedIdentifierFlags.KindMask) { - case ts.GeneratedIdentifierFlags.Auto: - return makeTempVariableName(TempFlags.Auto, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); - case ts.GeneratedIdentifierFlags.Loop: - return makeTempVariableName(TempFlags._i, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); - case ts.GeneratedIdentifierFlags.Unique: - return makeUniqueName(ts.idText(name), (name.autoGenerateFlags & ts.GeneratedIdentifierFlags.FileLevel) ? isFileLevelUniqueName : isUniqueName, !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.Optimistic), !!(name.autoGenerateFlags & ts.GeneratedIdentifierFlags.ReservedInNestedScopes)); - } - return ts.Debug.fail("Unsupported GeneratedIdentifierKind."); - } - - /** - * Gets the node from which a name should be generated. - */ - function getNodeForGeneratedName(name: ts.GeneratedIdentifier) { - const autoGenerateId = name.autoGenerateId; - let node = name as ts.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 (ts.isIdentifier(node) - && !!(node.autoGenerateFlags! & ts.GeneratedIdentifierFlags.Node) - && node.autoGenerateId !== autoGenerateId) { - break; - } + // otherwise, return the original node for the source; + return node; + } - original = node.original; - } + // Comments - // otherwise, return the original node for the source; - return node; - } + function pipelineEmitWithComments(hint: ts.EmitHint, node: ts.Node) { + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, hint, node); + const savedContainerPos = containerPos; + const savedContainerEnd = containerEnd; + const savedDeclarationListContainerEnd = declarationListContainerEnd; + emitCommentsBeforeNode(node); + pipelinePhase(hint, node); + emitCommentsAfterNode(node, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); + } - // Comments + function emitCommentsBeforeNode(node: ts.Node) { + const emitFlags = ts.getEmitFlags(node); + const commentRange = ts.getCommentRange(node); - function pipelineEmitWithComments(hint: ts.EmitHint, node: ts.Node) { - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, hint, node); - const savedContainerPos = containerPos; - const savedContainerEnd = containerEnd; - const savedDeclarationListContainerEnd = declarationListContainerEnd; - emitCommentsBeforeNode(node); - pipelinePhase(hint, node); - emitCommentsAfterNode(node, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); + // Emit leading comments + emitLeadingCommentsOfNode(node, emitFlags, commentRange.pos, commentRange.end); + if (emitFlags & ts.EmitFlags.NoNestedComments) { + commentsDisabled = true; } + } - function emitCommentsBeforeNode(node: ts.Node) { - const emitFlags = ts.getEmitFlags(node); - const commentRange = ts.getCommentRange(node); + function emitCommentsAfterNode(node: ts.Node, savedContainerPos: number, savedContainerEnd: number, savedDeclarationListContainerEnd: number) { + const emitFlags = ts.getEmitFlags(node); + const commentRange = ts.getCommentRange(node); - // Emit leading comments - emitLeadingCommentsOfNode(node, emitFlags, commentRange.pos, commentRange.end); - if (emitFlags & ts.EmitFlags.NoNestedComments) { - commentsDisabled = true; - } + // Emit trailing comments + if (emitFlags & ts.EmitFlags.NoNestedComments) { + commentsDisabled = false; } - - function emitCommentsAfterNode(node: ts.Node, savedContainerPos: number, savedContainerEnd: number, savedDeclarationListContainerEnd: number) { - const emitFlags = ts.getEmitFlags(node); - const commentRange = ts.getCommentRange(node); - - // Emit trailing comments - if (emitFlags & ts.EmitFlags.NoNestedComments) { - commentsDisabled = false; - } - emitTrailingCommentsOfNode(node, emitFlags, commentRange.pos, commentRange.end, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); - const typeNode = ts.getTypeNode(node); - if (typeNode) { - emitTrailingCommentsOfNode(node, emitFlags, typeNode.pos, typeNode.end, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); - } + emitTrailingCommentsOfNode(node, emitFlags, commentRange.pos, commentRange.end, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); + const typeNode = ts.getTypeNode(node); + if (typeNode) { + emitTrailingCommentsOfNode(node, emitFlags, typeNode.pos, typeNode.end, savedContainerPos, savedContainerEnd, savedDeclarationListContainerEnd); } + } - function emitLeadingCommentsOfNode(node: ts.Node, emitFlags: ts.EmitFlags, pos: number, end: number) { - enterComment(); - hasWrittenComment = false; + function emitLeadingCommentsOfNode(node: ts.Node, emitFlags: ts.EmitFlags, pos: number, end: number) { + enterComment(); + hasWrittenComment = false; - // 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 & ts.EmitFlags.NoLeadingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; - const skipTrailingComments = end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; + // 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 & ts.EmitFlags.NoLeadingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; + const skipTrailingComments = end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; - // Save current container state on the stack. - 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*/ node.kind !== ts.SyntaxKind.NotEmittedStatement); - } + // Save current container state on the stack. + 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*/ node.kind !== ts.SyntaxKind.NotEmittedStatement); + } - if (!skipLeadingComments || (pos >= 0 && (emitFlags & ts.EmitFlags.NoLeadingComments) !== 0)) { - // Advance the container position if comments get emitted or if they've been disabled explicitly using NoLeadingComments. - containerPos = pos; - } + if (!skipLeadingComments || (pos >= 0 && (emitFlags & ts.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 & ts.EmitFlags.NoTrailingComments) !== 0)) { - // As above. - containerEnd = end; + if (!skipTrailingComments || (end >= 0 && (emitFlags & ts.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 === ts.SyntaxKind.VariableDeclarationList) { - declarationListContainerEnd = end; - } - } - } - ts.forEach(ts.getSyntheticLeadingComments(node), emitLeadingSynthesizedComment); - exitComment(); - } - - function emitTrailingCommentsOfNode(node: ts.Node, emitFlags: ts.EmitFlags, pos: number, end: number, savedContainerPos: number, savedContainerEnd: number, savedDeclarationListContainerEnd: number) { - enterComment(); - const skipTrailingComments = end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; - ts.forEach(ts.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 && node.kind !== ts.SyntaxKind.NotEmittedStatement) { - emitTrailingComments(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 === ts.SyntaxKind.VariableDeclarationList) { + declarationListContainerEnd = end; } } - exitComment(); } + ts.forEach(ts.getSyntheticLeadingComments(node), emitLeadingSynthesizedComment); + exitComment(); + } - function emitLeadingSynthesizedComment(comment: ts.SynthesizedComment) { - if (comment.hasLeadingNewline || comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { - writer.writeLine(); - } - writeSynthesizedComment(comment); - if (comment.hasTrailingNewLine || comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { - writer.writeLine(); - } - else { - writer.writeSpace(" "); - } - } + function emitTrailingCommentsOfNode(node: ts.Node, emitFlags: ts.EmitFlags, pos: number, end: number, savedContainerPos: number, savedContainerEnd: number, savedDeclarationListContainerEnd: number) { + enterComment(); + const skipTrailingComments = end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0 || node.kind === ts.SyntaxKind.JsxText; + ts.forEach(ts.getSyntheticTrailingComments(node), emitTrailingSynthesizedComment); + if ((pos > 0 || end > 0) && pos !== end) { + // Restore previous container state. + containerPos = savedContainerPos; + containerEnd = savedContainerEnd; + declarationListContainerEnd = savedDeclarationListContainerEnd; - function emitTrailingSynthesizedComment(comment: ts.SynthesizedComment) { - if (!writer.isAtStartOfLine()) { - writer.writeSpace(" "); - } - writeSynthesizedComment(comment); - if (comment.hasTrailingNewLine) { - writer.writeLine(); + // 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 && node.kind !== ts.SyntaxKind.NotEmittedStatement) { + emitTrailingComments(end); } } + exitComment(); + } - function writeSynthesizedComment(comment: ts.SynthesizedComment) { - const text = formatSynthesizedComment(comment); - const lineMap = comment.kind === ts.SyntaxKind.MultiLineCommentTrivia ? ts.computeLineStarts(text) : undefined; - ts.writeCommentRange(text, lineMap!, writer, 0, text.length, newLine); + function emitLeadingSynthesizedComment(comment: ts.SynthesizedComment) { + if (comment.hasLeadingNewline || comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { + writer.writeLine(); } - - function formatSynthesizedComment(comment: ts.SynthesizedComment) { - return comment.kind === ts.SyntaxKind.MultiLineCommentTrivia - ? `/*${comment.text}*/` - : `//${comment.text}`; + writeSynthesizedComment(comment); + if (comment.hasTrailingNewLine || comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { + writer.writeLine(); + } + else { + writer.writeSpace(" "); } + } - function emitBodyWithDetachedComments(node: ts.Node, detachedRange: ts.TextRange, emitCallback: (node: ts.Node) => void) { - enterComment(); - const { pos, end } = detachedRange; - const emitFlags = ts.getEmitFlags(node); - const skipLeadingComments = pos < 0 || (emitFlags & ts.EmitFlags.NoLeadingComments) !== 0; - const skipTrailingComments = commentsDisabled || end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0; - if (!skipLeadingComments) { - emitDetachedCommentsAndUpdateCommentsInfo(detachedRange); - } + function emitTrailingSynthesizedComment(comment: ts.SynthesizedComment) { + if (!writer.isAtStartOfLine()) { + writer.writeSpace(" "); + } + writeSynthesizedComment(comment); + if (comment.hasTrailingNewLine) { + writer.writeLine(); + } + } - exitComment(); - if (emitFlags & ts.EmitFlags.NoNestedComments && !commentsDisabled) { - commentsDisabled = true; - emitCallback(node); - commentsDisabled = false; - } - else { - emitCallback(node); - } + function writeSynthesizedComment(comment: ts.SynthesizedComment) { + const text = formatSynthesizedComment(comment); + const lineMap = comment.kind === ts.SyntaxKind.MultiLineCommentTrivia ? ts.computeLineStarts(text) : undefined; + ts.writeCommentRange(text, lineMap!, writer, 0, text.length, newLine); + } - enterComment(); - if (!skipTrailingComments) { - emitLeadingComments(detachedRange.end, /*isEmittedNode*/ true); - if (hasWrittenComment && !writer.isAtStartOfLine()) { - writer.writeLine(); - } - } - exitComment(); + function formatSynthesizedComment(comment: ts.SynthesizedComment) { + return comment.kind === ts.SyntaxKind.MultiLineCommentTrivia + ? `/*${comment.text}*/` + : `//${comment.text}`; + } + function emitBodyWithDetachedComments(node: ts.Node, detachedRange: ts.TextRange, emitCallback: (node: ts.Node) => void) { + enterComment(); + const { pos, end } = detachedRange; + const emitFlags = ts.getEmitFlags(node); + const skipLeadingComments = pos < 0 || (emitFlags & ts.EmitFlags.NoLeadingComments) !== 0; + const skipTrailingComments = commentsDisabled || end < 0 || (emitFlags & ts.EmitFlags.NoTrailingComments) !== 0; + if (!skipLeadingComments) { + emitDetachedCommentsAndUpdateCommentsInfo(detachedRange); } - function originalNodesHaveSameParent(nodeA: ts.Node, nodeB: ts.Node) { - nodeA = ts.getOriginalNode(nodeA); - // For performance, do not call `getOriginalNode` for `nodeB` if `nodeA` doesn't even - // have a parent node. - return nodeA.parent && nodeA.parent === ts.getOriginalNode(nodeB).parent; + exitComment(); + if (emitFlags & ts.EmitFlags.NoNestedComments && !commentsDisabled) { + commentsDisabled = true; + emitCallback(node); + commentsDisabled = false; + } + else { + emitCallback(node); } - function siblingNodePositionsAreComparable(previousNode: ts.Node, nextNode: ts.Node) { - if (nextNode.pos < previousNode.end) { - return false; + enterComment(); + if (!skipTrailingComments) { + emitLeadingComments(detachedRange.end, /*isEmittedNode*/ true); + if (hasWrittenComment && !writer.isAtStartOfLine()) { + writer.writeLine(); } + } + exitComment(); - previousNode = ts.getOriginalNode(previousNode); - nextNode = ts.getOriginalNode(nextNode); - const parent = previousNode.parent; - if (!parent || parent !== nextNode.parent) { - return false; - } + } + + function originalNodesHaveSameParent(nodeA: ts.Node, nodeB: ts.Node) { + nodeA = ts.getOriginalNode(nodeA); + // For performance, do not call `getOriginalNode` for `nodeB` if `nodeA` doesn't even + // have a parent node. + return nodeA.parent && nodeA.parent === ts.getOriginalNode(nodeB).parent; + } - const parentNodeArray = ts.getContainingNodeArray(previousNode); - const prevNodeIndex = parentNodeArray?.indexOf(previousNode); - return prevNodeIndex !== undefined && prevNodeIndex > -1 && parentNodeArray!.indexOf(nextNode) === prevNodeIndex + 1; + function siblingNodePositionsAreComparable(previousNode: ts.Node, nextNode: ts.Node) { + if (nextNode.pos < previousNode.end) { + return false; } - function emitLeadingComments(pos: number, isEmittedNode: boolean) { - hasWrittenComment = false; + previousNode = ts.getOriginalNode(previousNode); + nextNode = ts.getOriginalNode(nextNode); + const parent = previousNode.parent; + if (!parent || parent !== nextNode.parent) { + return false; + } - if (isEmittedNode) { - if (pos === 0 && currentSourceFile?.isDeclarationFile) { - forEachLeadingCommentToEmit(pos, emitNonTripleSlashLeadingComment); - } - else { - forEachLeadingCommentToEmit(pos, emitLeadingComment); - } + const parentNodeArray = ts.getContainingNodeArray(previousNode); + const prevNodeIndex = parentNodeArray?.indexOf(previousNode); + return prevNodeIndex !== undefined && prevNodeIndex > -1 && parentNodeArray!.indexOf(nextNode) === prevNodeIndex + 1; + } + + function emitLeadingComments(pos: number, isEmittedNode: boolean) { + hasWrittenComment = false; + + if (isEmittedNode) { + if (pos === 0 && currentSourceFile?.isDeclarationFile) { + forEachLeadingCommentToEmit(pos, emitNonTripleSlashLeadingComment); } - 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); + else { + forEachLeadingCommentToEmit(pos, emitLeadingComment); } } - - function emitTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { - if (isTripleSlashComment(commentPos, commentEnd)) { - emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos); - } + 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 emitNonTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { - if (!isTripleSlashComment(commentPos, commentEnd)) { - emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos); - } + function emitTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: ts.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 (ts.isJSDocLikeText(text, pos) || ts.isPinnedComment(text, pos)); - } - return true; + function emitNonTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { + if (!isTripleSlashComment(commentPos, commentEnd)) { + emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos); } + } - function emitLeadingComment(commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { - if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) - return; - if (!hasWrittenComment) { - ts.emitNewLineBeforeLeadingCommentOfPosition(getCurrentLineMap(), writer, rangePos, commentPos); - hasWrittenComment = true; - } - - // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space - emitPos(commentPos); - ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + function shouldWriteComment(text: string, pos: number) { + if (printerOptions.onlyPrintJsDocStyle) { + return (ts.isJSDocLikeText(text, pos) || ts.isPinnedComment(text, pos)); + } + return true; + } - if (hasTrailingNewLine) { - writer.writeLine(); - } - else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { - writer.writeSpace(" "); - } + function emitLeadingComment(commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { + if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) + return; + if (!hasWrittenComment) { + ts.emitNewLineBeforeLeadingCommentOfPosition(getCurrentLineMap(), writer, rangePos, commentPos); + hasWrittenComment = true; } - function emitLeadingCommentsOfPosition(pos: number) { - if (commentsDisabled || pos === -1) { - return; - } + // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space + emitPos(commentPos); + ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); - emitLeadingComments(pos, /*isEmittedNode*/ true); + if (hasTrailingNewLine) { + writer.writeLine(); } + else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + writer.writeSpace(" "); + } + } - function emitTrailingComments(pos: number) { - forEachTrailingCommentToEmit(pos, emitTrailingComment); + function emitLeadingCommentsOfPosition(pos: number) { + if (commentsDisabled || pos === -1) { + return; } - function emitTrailingComment(commentPos: number, commentEnd: number, _kind: ts.SyntaxKind, hasTrailingNewLine: boolean) { - if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) - return; - // trailing comments are emitted at space/*trailing comment1 */space/*trailing comment2*/ - if (!writer.isAtStartOfLine()) { - writer.writeSpace(" "); - } + emitLeadingComments(pos, /*isEmittedNode*/ true); + } - emitPos(commentPos); - ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + function emitTrailingComments(pos: number) { + forEachTrailingCommentToEmit(pos, emitTrailingComment); + } - if (hasTrailingNewLine) { - writer.writeLine(); - } + function emitTrailingComment(commentPos: number, commentEnd: number, _kind: ts.SyntaxKind, hasTrailingNewLine: boolean) { + if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) + return; + // trailing comments are emitted at space/*trailing comment1 */space/*trailing comment2*/ + if (!writer.isAtStartOfLine()) { + writer.writeSpace(" "); } - function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { - if (commentsDisabled) { - return; - } - enterComment(); - forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); - exitComment(); + emitPos(commentPos); + ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + + if (hasTrailingNewLine) { + writer.writeLine(); + } + } + + function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { + if (commentsDisabled) { + return; } + enterComment(); + forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); + exitComment(); + } - function emitTrailingCommentOfPositionNoNewline(commentPos: number, commentEnd: number, kind: ts.SyntaxKind) { - if (!currentSourceFile) - return; - // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space + function emitTrailingCommentOfPositionNoNewline(commentPos: number, commentEnd: number, kind: ts.SyntaxKind) { + if (!currentSourceFile) + return; + // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space - emitPos(commentPos); - ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + emitPos(commentPos); + ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); - if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { - writer.writeLine(); // still write a newline for single-line comments, so closing tokens aren't written on the same line - } + if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + writer.writeLine(); // still write a newline for single-line comments, so closing tokens aren't written on the same line } + } - function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: ts.SyntaxKind, hasTrailingNewLine: boolean) { - if (!currentSourceFile) - return; - // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space + function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: ts.SyntaxKind, hasTrailingNewLine: boolean) { + if (!currentSourceFile) + return; + // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space - emitPos(commentPos); - ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + emitPos(commentPos); + ts.writeCommentRange(currentSourceFile.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); - if (hasTrailingNewLine) { - writer.writeLine(); + if (hasTrailingNewLine) { + writer.writeLine(); + } + else { + writer.writeSpace(" "); + } + } + + function forEachLeadingCommentToEmit(pos: number, cb: (commentPos: number, commentEnd: number, kind: ts.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(" "); + ts.forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); } } + } - function forEachLeadingCommentToEmit(pos: number, cb: (commentPos: number, commentEnd: number, kind: ts.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 { - ts.forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); - } - } + function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: ts.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))) { + ts.forEachTrailingCommentRange(currentSourceFile.text, end, cb); } + } - function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: ts.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))) { - ts.forEachTrailingCommentRange(currentSourceFile.text, end, cb); - } - } + function hasDetachedComments(pos: number) { + return detachedCommentsInfo !== undefined && ts.last(detachedCommentsInfo).nodePos === pos; + } - function hasDetachedComments(pos: number) { - return detachedCommentsInfo !== undefined && ts.last(detachedCommentsInfo).nodePos === pos; + function forEachLeadingCommentWithoutDetachedComments(cb: (commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { + if (!currentSourceFile) + return; + // get the leading comments from detachedPos + const pos = ts.last(detachedCommentsInfo!).detachedCommentEndPos; + if (detachedCommentsInfo!.length - 1) { + detachedCommentsInfo!.pop(); + } + else { + detachedCommentsInfo = undefined; } - function forEachLeadingCommentWithoutDetachedComments(cb: (commentPos: number, commentEnd: number, kind: ts.SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { - if (!currentSourceFile) - return; - // get the leading comments from detachedPos - const pos = ts.last(detachedCommentsInfo!).detachedCommentEndPos; - if (detachedCommentsInfo!.length - 1) { - detachedCommentsInfo!.pop(); + ts.forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); + } + + function emitDetachedCommentsAndUpdateCommentsInfo(range: ts.TextRange) { + const currentDetachedCommentInfo = currentSourceFile && ts.emitDetachedComments(currentSourceFile.text, getCurrentLineMap(), writer, emitComment, range, newLine, commentsDisabled); + if (currentDetachedCommentInfo) { + if (detachedCommentsInfo) { + detachedCommentsInfo.push(currentDetachedCommentInfo); } else { - detachedCommentsInfo = undefined; + detachedCommentsInfo = [currentDetachedCommentInfo]; } - - ts.forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); } + } - function emitDetachedCommentsAndUpdateCommentsInfo(range: ts.TextRange) { - const currentDetachedCommentInfo = currentSourceFile && ts.emitDetachedComments(currentSourceFile.text, getCurrentLineMap(), writer, emitComment, range, newLine, commentsDisabled); - if (currentDetachedCommentInfo) { - if (detachedCommentsInfo) { - detachedCommentsInfo.push(currentDetachedCommentInfo); - } - else { - detachedCommentsInfo = [currentDetachedCommentInfo]; - } - } - } + function emitComment(text: string, lineMap: number[], writer: ts.EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) { + if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) + return; + emitPos(commentPos); + ts.writeCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + } - function emitComment(text: string, lineMap: number[], writer: ts.EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) { - if (!currentSourceFile || !shouldWriteComment(currentSourceFile.text, commentPos)) - return; - emitPos(commentPos); - ts.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 !!currentSourceFile && ts.isRecognizedTripleSlashComment(currentSourceFile.text, commentPos, 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 !!currentSourceFile && ts.isRecognizedTripleSlashComment(currentSourceFile.text, commentPos, commentEnd); + // Source Maps + + function getParsedSourceMap(node: ts.UnparsedSource) { + if (node.parsedSourceMap === undefined && node.sourceMapText !== undefined) { + node.parsedSourceMap = ts.tryParseRawSourceMap(node.sourceMapText) || false; } + return node.parsedSourceMap || undefined; + } - // Source Maps + function pipelineEmitWithSourceMaps(hint: ts.EmitHint, node: ts.Node) { + const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, hint, node); + emitSourceMapsBeforeNode(node); + pipelinePhase(hint, node); + emitSourceMapsAfterNode(node); + } + + function emitSourceMapsBeforeNode(node: ts.Node) { + const emitFlags = ts.getEmitFlags(node); + const sourceMapRange = ts.getSourceMapRange(node); - function getParsedSourceMap(node: ts.UnparsedSource) { - if (node.parsedSourceMap === undefined && node.sourceMapText !== undefined) { - node.parsedSourceMap = ts.tryParseRawSourceMap(node.sourceMapText) || false; + // Emit leading sourcemap + if (ts.isUnparsedNode(node)) { + ts.Debug.assertIsDefined(node.parent, "UnparsedNodes must have parent pointers"); + 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; } - - function pipelineEmitWithSourceMaps(hint: ts.EmitHint, node: ts.Node) { - const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, hint, node); - emitSourceMapsBeforeNode(node); - pipelinePhase(hint, node); - emitSourceMapsAfterNode(node); + else { + const source = sourceMapRange.source || sourceMapSource; + if (node.kind !== ts.SyntaxKind.NotEmittedStatement + && (emitFlags & ts.EmitFlags.NoLeadingSourceMap) === 0 + && sourceMapRange.pos >= 0) { + emitSourcePos(sourceMapRange.source || sourceMapSource, skipSourceTrivia(source, sourceMapRange.pos)); + } + if (emitFlags & ts.EmitFlags.NoNestedSourceMaps) { + sourceMapsDisabled = true; + } } + } - function emitSourceMapsBeforeNode(node: ts.Node) { - const emitFlags = ts.getEmitFlags(node); - const sourceMapRange = ts.getSourceMapRange(node); + function emitSourceMapsAfterNode(node: ts.Node) { + const emitFlags = ts.getEmitFlags(node); + const sourceMapRange = ts.getSourceMapRange(node); - // Emit leading sourcemap - if (ts.isUnparsedNode(node)) { - ts.Debug.assertIsDefined(node.parent, "UnparsedNodes must have parent pointers"); - 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)); - } + // Emit trailing sourcemap + if (!ts.isUnparsedNode(node)) { + if (emitFlags & ts.EmitFlags.NoNestedSourceMaps) { + sourceMapsDisabled = false; } - else { - const source = sourceMapRange.source || sourceMapSource; - if (node.kind !== ts.SyntaxKind.NotEmittedStatement - && (emitFlags & ts.EmitFlags.NoLeadingSourceMap) === 0 - && sourceMapRange.pos >= 0) { - emitSourcePos(sourceMapRange.source || sourceMapSource, skipSourceTrivia(source, sourceMapRange.pos)); - } - if (emitFlags & ts.EmitFlags.NoNestedSourceMaps) { - sourceMapsDisabled = true; - } + if (node.kind !== ts.SyntaxKind.NotEmittedStatement + && (emitFlags & ts.EmitFlags.NoTrailingSourceMap) === 0 + && sourceMapRange.end >= 0) { + emitSourcePos(sourceMapRange.source || sourceMapSource, sourceMapRange.end); } } + } - function emitSourceMapsAfterNode(node: ts.Node) { - const emitFlags = ts.getEmitFlags(node); - const sourceMapRange = ts.getSourceMapRange(node); + /** + * Skips trivia such as comments and white-space that can be optionally overridden by the source-map source + */ + function skipSourceTrivia(source: ts.SourceMapSource, pos: number): number { + return source.skipTrivia ? source.skipTrivia(pos) : ts.skipTrivia(source.text, pos); + } - // Emit trailing sourcemap - if (!ts.isUnparsedNode(node)) { - if (emitFlags & ts.EmitFlags.NoNestedSourceMaps) { - sourceMapsDisabled = false; - } - if (node.kind !== ts.SyntaxKind.NotEmittedStatement - && (emitFlags & ts.EmitFlags.NoTrailingSourceMap) === 0 - && sourceMapRange.end >= 0) { - emitSourcePos(sourceMapRange.source || sourceMapSource, sourceMapRange.end); - } - } + /** + * 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 || ts.positionIsSynthesized(pos) || isJsonSourceMapSource(sourceMapSource)) { + return; } - /** - * Skips trivia such as comments and white-space that can be optionally overridden by the source-map source - */ - function skipSourceTrivia(source: ts.SourceMapSource, pos: number): number { - return source.skipTrivia ? source.skipTrivia(pos) : ts.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 || ts.positionIsSynthesized(pos) || isJsonSourceMapSource(sourceMapSource)) { - return; - } + const { line: sourceLine, character: sourceCharacter } = ts.getLineAndCharacterOfPosition(sourceMapSource, pos); + sourceMapGenerator!.addMapping(writer.getLine(), writer.getColumn(), sourceMapSourceIndex, sourceLine, sourceCharacter, + /*nameIndex*/ undefined); + } - const { line: sourceLine, character: sourceCharacter } = ts.getLineAndCharacterOfPosition(sourceMapSource, pos); - sourceMapGenerator!.addMapping(writer.getLine(), writer.getColumn(), sourceMapSourceIndex, sourceLine, sourceCharacter, - /*nameIndex*/ undefined); + function emitSourcePos(source: ts.SourceMapSource, pos: number) { + if (source !== sourceMapSource) { + const savedSourceMapSource = sourceMapSource; + const savedSourceMapSourceIndex = sourceMapSourceIndex; + setSourceMapSource(source); + emitPos(pos); + resetSourceMapSource(savedSourceMapSource, savedSourceMapSourceIndex); } - - function emitSourcePos(source: ts.SourceMapSource, pos: number) { - if (source !== sourceMapSource) { - const savedSourceMapSource = sourceMapSource; - const savedSourceMapSourceIndex = sourceMapSourceIndex; - setSourceMapSource(source); - emitPos(pos); - resetSourceMapSource(savedSourceMapSource, savedSourceMapSourceIndex); - } - else { - emitPos(pos); - } + else { + emitPos(pos); } + } - /** - * 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: ts.Node | undefined, token: ts.SyntaxKind, writer: (s: string) => void, tokenPos: number, emitCallback: (token: ts.SyntaxKind, writer: (s: string) => void, tokenStartPos: number) => number) { - if (sourceMapsDisabled || node && ts.isInJsonFile(node)) { - return emitCallback(token, writer, tokenPos); - } - - const emitNode = node && node.emitNode; - const emitFlags = emitNode && emitNode.flags || ts.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 & ts.EmitFlags.NoTokenLeadingSourceMaps) === 0 && tokenPos >= 0) { - emitSourcePos(source, tokenPos); - } - - tokenPos = emitCallback(token, writer, tokenPos); + /** + * 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: ts.Node | undefined, token: ts.SyntaxKind, writer: (s: string) => void, tokenPos: number, emitCallback: (token: ts.SyntaxKind, writer: (s: string) => void, tokenStartPos: number) => number) { + if (sourceMapsDisabled || node && ts.isInJsonFile(node)) { + return emitCallback(token, writer, tokenPos); + } - if (range) - tokenPos = range.end; - if ((emitFlags & ts.EmitFlags.NoTokenTrailingSourceMaps) === 0 && tokenPos >= 0) { - emitSourcePos(source, tokenPos); - } + const emitNode = node && node.emitNode; + const emitFlags = emitNode && emitNode.flags || ts.EmitFlags.None; + const range = emitNode && emitNode.tokenSourceMapRanges && emitNode.tokenSourceMapRanges[token]; + const source = range && range.source || sourceMapSource; - return tokenPos; + tokenPos = skipSourceTrivia(source, range ? range.pos : tokenPos); + if ((emitFlags & ts.EmitFlags.NoTokenLeadingSourceMaps) === 0 && tokenPos >= 0) { + emitSourcePos(source, tokenPos); } - function setSourceMapSource(source: ts.SourceMapSource) { - if (sourceMapsDisabled) { - return; - } + tokenPos = emitCallback(token, writer, tokenPos); - sourceMapSource = source; + if (range) + tokenPos = range.end; + if ((emitFlags & ts.EmitFlags.NoTokenTrailingSourceMaps) === 0 && tokenPos >= 0) { + emitSourcePos(source, tokenPos); + } - if (source === mostRecentlyAddedSourceMapSource) { - // Fast path for when the new source map is the most recently added, in which case - // we use its captured index without going through the source map generator. - sourceMapSourceIndex = mostRecentlyAddedSourceMapSourceIndex; - return; - } + return tokenPos; + } - if (isJsonSourceMapSource(source)) { - return; - } + function setSourceMapSource(source: ts.SourceMapSource) { + if (sourceMapsDisabled) { + return; + } - sourceMapSourceIndex = sourceMapGenerator!.addSource(source.fileName); - if (printerOptions.inlineSources) { - sourceMapGenerator!.setSourceContent(sourceMapSourceIndex, source.text); - } + sourceMapSource = source; - mostRecentlyAddedSourceMapSource = source; - mostRecentlyAddedSourceMapSourceIndex = sourceMapSourceIndex; + if (source === mostRecentlyAddedSourceMapSource) { + // Fast path for when the new source map is the most recently added, in which case + // we use its captured index without going through the source map generator. + sourceMapSourceIndex = mostRecentlyAddedSourceMapSourceIndex; + return; } - function resetSourceMapSource(source: ts.SourceMapSource, sourceIndex: number) { - sourceMapSource = source; - sourceMapSourceIndex = sourceIndex; + if (isJsonSourceMapSource(source)) { + return; } - function isJsonSourceMapSource(sourceFile: ts.SourceMapSource) { - return ts.fileExtensionIs(sourceFile.fileName, ts.Extension.Json); + sourceMapSourceIndex = sourceMapGenerator!.addSource(source.fileName); + if (printerOptions.inlineSources) { + sourceMapGenerator!.setSourceContent(sourceMapSourceIndex, source.text); } - } - function createBracketsMap() { - const brackets: string[][] = []; - brackets[ts.ListFormat.Braces] = ["{", "}"]; - brackets[ts.ListFormat.Parenthesis] = ["(", ")"]; - brackets[ts.ListFormat.AngleBrackets] = ["<", ">"]; - brackets[ts.ListFormat.SquareBrackets] = ["[", "]"]; - return brackets; + mostRecentlyAddedSourceMapSource = source; + mostRecentlyAddedSourceMapSourceIndex = sourceMapSourceIndex; } - function getOpeningBracket(format: ts.ListFormat) { - return brackets[format & ts.ListFormat.BracketsMask][0]; + function resetSourceMapSource(source: ts.SourceMapSource, sourceIndex: number) { + sourceMapSource = source; + sourceMapSourceIndex = sourceIndex; } - function getClosingBracket(format: ts.ListFormat) { - return brackets[format & ts.ListFormat.BracketsMask][1]; + function isJsonSourceMapSource(sourceFile: ts.SourceMapSource) { + return ts.fileExtensionIs(sourceFile.fileName, ts.Extension.Json); } +} - // Flags enum to track count of temp variables and a few dedicated names - const enum TempFlags { - Auto = 0x00000000, - CountMask = 0x0FFFFFFF, - _i = 0x10000000 - } +function createBracketsMap() { + const brackets: string[][] = []; + brackets[ts.ListFormat.Braces] = ["{", "}"]; + brackets[ts.ListFormat.Parenthesis] = ["(", ")"]; + brackets[ts.ListFormat.AngleBrackets] = ["<", ">"]; + brackets[ts.ListFormat.SquareBrackets] = ["[", "]"]; + return brackets; +} - interface OrdinalParentheizerRuleSelector { - select(index: number): ((node: T) => T) | undefined; - } +function getOpeningBracket(format: ts.ListFormat) { + return brackets[format & ts.ListFormat.BracketsMask][0]; +} - type ParenthesizerRule = (node: T) => T; - type ParenthesizerRuleOrSelector = OrdinalParentheizerRuleSelector | ParenthesizerRule; - function emitListItemNoParenthesizer(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, _parenthesizerRule: ParenthesizerRuleOrSelector | undefined, _index: number) { - emit(node); - } +function getClosingBracket(format: ts.ListFormat) { + return brackets[format & ts.ListFormat.BracketsMask][1]; +} - function emitListItemWithParenthesizerRuleSelector(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRuleSelector: OrdinalParentheizerRuleSelector, index: number) { - emit(node, parenthesizerRuleSelector.select(index)); - } +// Flags enum to track count of temp variables and a few dedicated names +const enum TempFlags { + Auto = 0x00000000, + CountMask = 0x0FFFFFFF, + _i = 0x10000000 +} - function emitListItemWithParenthesizerRule(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: ParenthesizerRule | undefined, _index: number) { - emit(node, parenthesizerRule); - } +interface OrdinalParentheizerRuleSelector { + select(index: number): ((node: T) => T) | undefined; +} - function getEmitListItem | undefined>(emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: R): (node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: R, index: number) => void { - return emit.length === 1 ? emitListItemNoParenthesizer : - typeof parenthesizerRule === "object" ? emitListItemWithParenthesizerRuleSelector : - emitListItemWithParenthesizerRule; - } +type ParenthesizerRule = (node: T) => T; +type ParenthesizerRuleOrSelector = OrdinalParentheizerRuleSelector | ParenthesizerRule; +function emitListItemNoParenthesizer(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, _parenthesizerRule: ParenthesizerRuleOrSelector | undefined, _index: number) { + emit(node); +} + +function emitListItemWithParenthesizerRuleSelector(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRuleSelector: OrdinalParentheizerRuleSelector, index: number) { + emit(node, parenthesizerRuleSelector.select(index)); +} + +function emitListItemWithParenthesizerRule(node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: ParenthesizerRule | undefined, _index: number) { + emit(node, parenthesizerRule); +} + +function getEmitListItem | undefined>(emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: R): (node: ts.Node, emit: (node: ts.Node, parenthesizerRule?: ((node: ts.Node) => ts.Node) | undefined) => void, parenthesizerRule: R, index: number) => void { + return emit.length === 1 ? emitListItemNoParenthesizer : + typeof parenthesizerRule === "object" ? emitListItemWithParenthesizerRuleSelector : + emitListItemWithParenthesizerRule; +} } diff --git a/src/compiler/factory/baseNodeFactory.ts b/src/compiler/factory/baseNodeFactory.ts index 44da062531637..23ede8dc74ed7 100644 --- a/src/compiler/factory/baseNodeFactory.ts +++ b/src/compiler/factory/baseNodeFactory.ts @@ -1,56 +1,56 @@ /* @internal */ namespace ts { - /** - * A `BaseNodeFactory` is an abstraction over an `ObjectAllocator` that handles caching `Node` constructors - * and allocating `Node` instances based on a set of predefined types. - */ - /* @internal */ - export interface BaseNodeFactory { - createBaseSourceFileNode(kind: ts.SyntaxKind): ts.Node; - createBaseIdentifierNode(kind: ts.SyntaxKind): ts.Node; - createBasePrivateIdentifierNode(kind: ts.SyntaxKind): ts.Node; - createBaseTokenNode(kind: ts.SyntaxKind): ts.Node; - createBaseNode(kind: ts.SyntaxKind): ts.Node; - } +/** + * A `BaseNodeFactory` is an abstraction over an `ObjectAllocator` that handles caching `Node` constructors + * and allocating `Node` instances based on a set of predefined types. + */ +/* @internal */ +export interface BaseNodeFactory { + createBaseSourceFileNode(kind: ts.SyntaxKind): ts.Node; + createBaseIdentifierNode(kind: ts.SyntaxKind): ts.Node; + createBasePrivateIdentifierNode(kind: ts.SyntaxKind): ts.Node; + createBaseTokenNode(kind: ts.SyntaxKind): ts.Node; + createBaseNode(kind: ts.SyntaxKind): ts.Node; +} - /** - * Creates a `BaseNodeFactory` which can be used to create `Node` instances from the constructors provided by the object allocator. - */ - export function createBaseNodeFactory(): BaseNodeFactory { - // tslint:disable variable-name - let NodeConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let TokenConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let IdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let SourceFileConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - // tslint:enable variable-name +/** + * Creates a `BaseNodeFactory` which can be used to create `Node` instances from the constructors provided by the object allocator. + */ +export function createBaseNodeFactory(): BaseNodeFactory { + // tslint:disable variable-name + let NodeConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + let TokenConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + let IdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + let SourceFileConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + // tslint:enable variable-name - return { - createBaseSourceFileNode, - createBaseIdentifierNode, - createBasePrivateIdentifierNode, - createBaseTokenNode, - createBaseNode - }; + return { + createBaseSourceFileNode, + createBaseIdentifierNode, + createBasePrivateIdentifierNode, + createBaseTokenNode, + createBaseNode + }; - function createBaseSourceFileNode(kind: ts.SyntaxKind): ts.Node { - return new (SourceFileConstructor || (SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor()))(kind, /*pos*/ -1, /*end*/ -1); - } + function createBaseSourceFileNode(kind: ts.SyntaxKind): ts.Node { + return new (SourceFileConstructor || (SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor()))(kind, /*pos*/ -1, /*end*/ -1); + } - function createBaseIdentifierNode(kind: ts.SyntaxKind): ts.Node { - return new (IdentifierConstructor || (IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor()))(kind, /*pos*/ -1, /*end*/ -1); - } + function createBaseIdentifierNode(kind: ts.SyntaxKind): ts.Node { + return new (IdentifierConstructor || (IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor()))(kind, /*pos*/ -1, /*end*/ -1); + } - function createBasePrivateIdentifierNode(kind: ts.SyntaxKind): ts.Node { - return new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor()))(kind, /*pos*/ -1, /*end*/ -1); - } + function createBasePrivateIdentifierNode(kind: ts.SyntaxKind): ts.Node { + return new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor()))(kind, /*pos*/ -1, /*end*/ -1); + } - function createBaseTokenNode(kind: ts.SyntaxKind): ts.Node { - return new (TokenConstructor || (TokenConstructor = ts.objectAllocator.getTokenConstructor()))(kind, /*pos*/ -1, /*end*/ -1); - } + function createBaseTokenNode(kind: ts.SyntaxKind): ts.Node { + return new (TokenConstructor || (TokenConstructor = ts.objectAllocator.getTokenConstructor()))(kind, /*pos*/ -1, /*end*/ -1); + } - function createBaseNode(kind: ts.SyntaxKind): ts.Node { - return new (NodeConstructor || (NodeConstructor = ts.objectAllocator.getNodeConstructor()))(kind, /*pos*/ -1, /*end*/ -1); - } + function createBaseNode(kind: ts.SyntaxKind): ts.Node { + return new (NodeConstructor || (NodeConstructor = ts.objectAllocator.getNodeConstructor()))(kind, /*pos*/ -1, /*end*/ -1); } -} \ No newline at end of file +} +} diff --git a/src/compiler/factory/emitHelpers.ts b/src/compiler/factory/emitHelpers.ts index 1e23dc5dd775f..f200a4c63cf8f 100644 --- a/src/compiler/factory/emitHelpers.ts +++ b/src/compiler/factory/emitHelpers.ts @@ -1,411 +1,411 @@ /* @internal */ namespace ts { - export interface EmitHelperFactory { - getUnscopedHelperName(name: string): ts.Identifier; +export interface EmitHelperFactory { + getUnscopedHelperName(name: string): ts.Identifier; + // TypeScript Helpers + createDecorateHelper(decoratorExpressions: readonly ts.Expression[], target: ts.Expression, memberName?: ts.Expression, descriptor?: ts.Expression): ts.Expression; + createMetadataHelper(metadataKey: string, metadataValue: ts.Expression): ts.Expression; + createParamHelper(expression: ts.Expression, parameterOffset: number): ts.Expression; + // ES2018 Helpers + createAssignHelper(attributesSegments: readonly ts.Expression[]): ts.Expression; + createAwaitHelper(expression: ts.Expression): ts.Expression; + createAsyncGeneratorHelper(generatorFunc: ts.FunctionExpression, hasLexicalThis: boolean): ts.Expression; + createAsyncDelegatorHelper(expression: ts.Expression): ts.Expression; + createAsyncValuesHelper(expression: ts.Expression): ts.Expression; + // ES2018 Destructuring Helpers + createRestHelper(value: ts.Expression, elements: readonly ts.BindingOrAssignmentElement[], computedTempVariables: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.Expression; + // ES2017 Helpers + createAwaiterHelper(hasLexicalThis: boolean, hasLexicalArguments: boolean, promiseConstructor: ts.EntityName | ts.Expression | undefined, body: ts.Block): ts.Expression; + // ES2015 Helpers + createExtendsHelper(name: ts.Identifier): ts.Expression; + createTemplateObjectHelper(cooked: ts.ArrayLiteralExpression, raw: ts.ArrayLiteralExpression): ts.Expression; + createSpreadArrayHelper(to: ts.Expression, from: ts.Expression, packFrom: boolean): ts.Expression; + // ES2015 Destructuring Helpers + createValuesHelper(expression: ts.Expression): ts.Expression; + createReadHelper(iteratorRecord: ts.Expression, count: number | undefined): ts.Expression; + // ES2015 Generator Helpers + createGeneratorHelper(body: ts.FunctionExpression): ts.Expression; + // ES Module Helpers + createCreateBindingHelper(module: ts.Expression, inputName: ts.Expression, outputName: ts.Expression | undefined): ts.Expression; + createImportStarHelper(expression: ts.Expression): ts.Expression; + createImportStarCallbackHelper(): ts.Expression; + createImportDefaultHelper(expression: ts.Expression): ts.Expression; + createExportStarHelper(moduleExpression: ts.Expression, exportsExpression?: ts.Expression): ts.Expression; + // Class Fields Helpers + createClassPrivateFieldGetHelper(receiver: ts.Expression, state: ts.Identifier, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined): ts.Expression; + createClassPrivateFieldSetHelper(receiver: ts.Expression, state: ts.Identifier, value: ts.Expression, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined): ts.Expression; + createClassPrivateFieldInHelper(state: ts.Identifier, receiver: ts.Expression): ts.Expression; +} + +export function createEmitHelperFactory(context: ts.TransformationContext): EmitHelperFactory { + const factory = context.factory; + const immutableTrue = ts.memoize(() => ts.setEmitFlags(factory.createTrue(), ts.EmitFlags.Immutable)); + const immutableFalse = ts.memoize(() => ts.setEmitFlags(factory.createFalse(), ts.EmitFlags.Immutable)); + + return { + getUnscopedHelperName, // TypeScript Helpers - createDecorateHelper(decoratorExpressions: readonly ts.Expression[], target: ts.Expression, memberName?: ts.Expression, descriptor?: ts.Expression): ts.Expression; - createMetadataHelper(metadataKey: string, metadataValue: ts.Expression): ts.Expression; - createParamHelper(expression: ts.Expression, parameterOffset: number): ts.Expression; + createDecorateHelper, + createMetadataHelper, + createParamHelper, // ES2018 Helpers - createAssignHelper(attributesSegments: readonly ts.Expression[]): ts.Expression; - createAwaitHelper(expression: ts.Expression): ts.Expression; - createAsyncGeneratorHelper(generatorFunc: ts.FunctionExpression, hasLexicalThis: boolean): ts.Expression; - createAsyncDelegatorHelper(expression: ts.Expression): ts.Expression; - createAsyncValuesHelper(expression: ts.Expression): ts.Expression; + createAssignHelper, + createAwaitHelper, + createAsyncGeneratorHelper, + createAsyncDelegatorHelper, + createAsyncValuesHelper, // ES2018 Destructuring Helpers - createRestHelper(value: ts.Expression, elements: readonly ts.BindingOrAssignmentElement[], computedTempVariables: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.Expression; + createRestHelper, // ES2017 Helpers - createAwaiterHelper(hasLexicalThis: boolean, hasLexicalArguments: boolean, promiseConstructor: ts.EntityName | ts.Expression | undefined, body: ts.Block): ts.Expression; + createAwaiterHelper, // ES2015 Helpers - createExtendsHelper(name: ts.Identifier): ts.Expression; - createTemplateObjectHelper(cooked: ts.ArrayLiteralExpression, raw: ts.ArrayLiteralExpression): ts.Expression; - createSpreadArrayHelper(to: ts.Expression, from: ts.Expression, packFrom: boolean): ts.Expression; + createExtendsHelper, + createTemplateObjectHelper, + createSpreadArrayHelper, // ES2015 Destructuring Helpers - createValuesHelper(expression: ts.Expression): ts.Expression; - createReadHelper(iteratorRecord: ts.Expression, count: number | undefined): ts.Expression; + createValuesHelper, + createReadHelper, // ES2015 Generator Helpers - createGeneratorHelper(body: ts.FunctionExpression): ts.Expression; + createGeneratorHelper, // ES Module Helpers - createCreateBindingHelper(module: ts.Expression, inputName: ts.Expression, outputName: ts.Expression | undefined): ts.Expression; - createImportStarHelper(expression: ts.Expression): ts.Expression; - createImportStarCallbackHelper(): ts.Expression; - createImportDefaultHelper(expression: ts.Expression): ts.Expression; - createExportStarHelper(moduleExpression: ts.Expression, exportsExpression?: ts.Expression): ts.Expression; + createCreateBindingHelper, + createImportStarHelper, + createImportStarCallbackHelper, + createImportDefaultHelper, + createExportStarHelper, // Class Fields Helpers - createClassPrivateFieldGetHelper(receiver: ts.Expression, state: ts.Identifier, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined): ts.Expression; - createClassPrivateFieldSetHelper(receiver: ts.Expression, state: ts.Identifier, value: ts.Expression, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined): ts.Expression; - createClassPrivateFieldInHelper(state: ts.Identifier, receiver: ts.Expression): ts.Expression; - } + createClassPrivateFieldGetHelper, + createClassPrivateFieldSetHelper, + createClassPrivateFieldInHelper + }; - export function createEmitHelperFactory(context: ts.TransformationContext): EmitHelperFactory { - const factory = context.factory; - const immutableTrue = ts.memoize(() => ts.setEmitFlags(factory.createTrue(), ts.EmitFlags.Immutable)); - const immutableFalse = ts.memoize(() => ts.setEmitFlags(factory.createFalse(), ts.EmitFlags.Immutable)); - - return { - getUnscopedHelperName, - // TypeScript Helpers - createDecorateHelper, - createMetadataHelper, - createParamHelper, - // ES2018 Helpers - createAssignHelper, - createAwaitHelper, - createAsyncGeneratorHelper, - createAsyncDelegatorHelper, - createAsyncValuesHelper, - // ES2018 Destructuring Helpers - createRestHelper, - // ES2017 Helpers - createAwaiterHelper, - // ES2015 Helpers - createExtendsHelper, - createTemplateObjectHelper, - createSpreadArrayHelper, - // ES2015 Destructuring Helpers - createValuesHelper, - createReadHelper, - // ES2015 Generator Helpers - createGeneratorHelper, - // ES Module Helpers - createCreateBindingHelper, - createImportStarHelper, - createImportStarCallbackHelper, - createImportDefaultHelper, - createExportStarHelper, - // Class Fields Helpers - createClassPrivateFieldGetHelper, - createClassPrivateFieldSetHelper, - createClassPrivateFieldInHelper - }; - - /** - * Gets an identifier for the name of an *unscoped* emit helper. - */ - function getUnscopedHelperName(name: string) { - return ts.setEmitFlags(factory.createIdentifier(name), ts.EmitFlags.HelperName | ts.EmitFlags.AdviseOnEmitNode); - } + /** + * Gets an identifier for the name of an *unscoped* emit helper. + */ + function getUnscopedHelperName(name: string) { + return ts.setEmitFlags(factory.createIdentifier(name), ts.EmitFlags.HelperName | ts.EmitFlags.AdviseOnEmitNode); + } - // TypeScript Helpers + // TypeScript Helpers - function createDecorateHelper(decoratorExpressions: ts.Expression[], target: ts.Expression, memberName?: ts.Expression, descriptor?: ts.Expression) { - context.requestEmitHelper(decorateHelper); + function createDecorateHelper(decoratorExpressions: ts.Expression[], target: ts.Expression, memberName?: ts.Expression, descriptor?: ts.Expression) { + context.requestEmitHelper(decorateHelper); - const argumentsArray: ts.Expression[] = []; - argumentsArray.push(factory.createArrayLiteralExpression(decoratorExpressions, /*multiLine*/ true)); - argumentsArray.push(target); - if (memberName) { - argumentsArray.push(memberName); - if (descriptor) { - argumentsArray.push(descriptor); - } + const argumentsArray: ts.Expression[] = []; + argumentsArray.push(factory.createArrayLiteralExpression(decoratorExpressions, /*multiLine*/ true)); + argumentsArray.push(target); + if (memberName) { + argumentsArray.push(memberName); + if (descriptor) { + argumentsArray.push(descriptor); } - - return factory.createCallExpression(getUnscopedHelperName("__decorate"), - /*typeArguments*/ undefined, argumentsArray); } - function createMetadataHelper(metadataKey: string, metadataValue: ts.Expression) { - context.requestEmitHelper(metadataHelper); - return factory.createCallExpression(getUnscopedHelperName("__metadata"), - /*typeArguments*/ undefined, [ - factory.createStringLiteral(metadataKey), - metadataValue - ]); - } + return factory.createCallExpression(getUnscopedHelperName("__decorate"), + /*typeArguments*/ undefined, argumentsArray); + } - function createParamHelper(expression: ts.Expression, parameterOffset: number, location?: ts.TextRange) { - context.requestEmitHelper(paramHelper); - return ts.setTextRange(factory.createCallExpression(getUnscopedHelperName("__param"), - /*typeArguments*/ undefined, [ - factory.createNumericLiteral(parameterOffset + ""), - expression - ]), location); - } + function createMetadataHelper(metadataKey: string, metadataValue: ts.Expression) { + context.requestEmitHelper(metadataHelper); + return factory.createCallExpression(getUnscopedHelperName("__metadata"), + /*typeArguments*/ undefined, [ + factory.createStringLiteral(metadataKey), + metadataValue + ]); + } - // ES2018 Helpers + function createParamHelper(expression: ts.Expression, parameterOffset: number, location?: ts.TextRange) { + context.requestEmitHelper(paramHelper); + return ts.setTextRange(factory.createCallExpression(getUnscopedHelperName("__param"), + /*typeArguments*/ undefined, [ + factory.createNumericLiteral(parameterOffset + ""), + expression + ]), location); + } - function createAssignHelper(attributesSegments: ts.Expression[]) { - if (ts.getEmitScriptTarget(context.getCompilerOptions()) >= ts.ScriptTarget.ES2015) { - return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Object"), "assign"), - /*typeArguments*/ undefined, attributesSegments); - } - context.requestEmitHelper(assignHelper); - return factory.createCallExpression(getUnscopedHelperName("__assign"), + // ES2018 Helpers + + function createAssignHelper(attributesSegments: ts.Expression[]) { + if (ts.getEmitScriptTarget(context.getCompilerOptions()) >= ts.ScriptTarget.ES2015) { + return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Object"), "assign"), /*typeArguments*/ undefined, attributesSegments); } + context.requestEmitHelper(assignHelper); + return factory.createCallExpression(getUnscopedHelperName("__assign"), + /*typeArguments*/ undefined, attributesSegments); + } - function createAwaitHelper(expression: ts.Expression) { - context.requestEmitHelper(awaitHelper); - return factory.createCallExpression(getUnscopedHelperName("__await"), /*typeArguments*/ undefined, [expression]); - } + function createAwaitHelper(expression: ts.Expression) { + context.requestEmitHelper(awaitHelper); + return factory.createCallExpression(getUnscopedHelperName("__await"), /*typeArguments*/ undefined, [expression]); + } - function createAsyncGeneratorHelper(generatorFunc: ts.FunctionExpression, hasLexicalThis: boolean) { - context.requestEmitHelper(awaitHelper); - context.requestEmitHelper(asyncGeneratorHelper); - - // Mark this node as originally an async function - (generatorFunc.emitNode || (generatorFunc.emitNode = {} as ts.EmitNode)).flags |= ts.EmitFlags.AsyncFunctionBody | ts.EmitFlags.ReuseTempVariableScope; - return factory.createCallExpression(getUnscopedHelperName("__asyncGenerator"), - /*typeArguments*/ undefined, [ - hasLexicalThis ? factory.createThis() : factory.createVoidZero(), - factory.createIdentifier("arguments"), - generatorFunc - ]); - } + function createAsyncGeneratorHelper(generatorFunc: ts.FunctionExpression, hasLexicalThis: boolean) { + context.requestEmitHelper(awaitHelper); + context.requestEmitHelper(asyncGeneratorHelper); + + // Mark this node as originally an async function + (generatorFunc.emitNode || (generatorFunc.emitNode = {} as ts.EmitNode)).flags |= ts.EmitFlags.AsyncFunctionBody | ts.EmitFlags.ReuseTempVariableScope; + return factory.createCallExpression(getUnscopedHelperName("__asyncGenerator"), + /*typeArguments*/ undefined, [ + hasLexicalThis ? factory.createThis() : factory.createVoidZero(), + factory.createIdentifier("arguments"), + generatorFunc + ]); + } - function createAsyncDelegatorHelper(expression: ts.Expression) { - context.requestEmitHelper(awaitHelper); - context.requestEmitHelper(asyncDelegator); - return factory.createCallExpression(getUnscopedHelperName("__asyncDelegator"), - /*typeArguments*/ undefined, [expression]); - } + function createAsyncDelegatorHelper(expression: ts.Expression) { + context.requestEmitHelper(awaitHelper); + context.requestEmitHelper(asyncDelegator); + return factory.createCallExpression(getUnscopedHelperName("__asyncDelegator"), + /*typeArguments*/ undefined, [expression]); + } - function createAsyncValuesHelper(expression: ts.Expression) { - context.requestEmitHelper(asyncValues); - return factory.createCallExpression(getUnscopedHelperName("__asyncValues"), - /*typeArguments*/ undefined, [expression]); - } + function createAsyncValuesHelper(expression: ts.Expression) { + context.requestEmitHelper(asyncValues); + return factory.createCallExpression(getUnscopedHelperName("__asyncValues"), + /*typeArguments*/ undefined, [expression]); + } - // ES2018 Destructuring Helpers + // ES2018 Destructuring Helpers - /** Given value: o, propName: p, pattern: { a, b, ...p } from the original statement - * `{ a, b, ...p } = o`, create `p = __rest(o, ["a", "b"]);` - */ - function createRestHelper(value: ts.Expression, elements: readonly ts.BindingOrAssignmentElement[], computedTempVariables: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.Expression { - context.requestEmitHelper(restHelper); - const propertyNames: ts.Expression[] = []; - let computedTempVariableOffset = 0; - for (let i = 0; i < elements.length - 1; i++) { - const propertyName = ts.getPropertyNameOfBindingOrAssignmentElement(elements[i]); - if (propertyName) { - if (ts.isComputedPropertyName(propertyName)) { - ts.Debug.assertIsDefined(computedTempVariables, "Encountered computed property name but 'computedTempVariables' argument was not provided."); - const temp = computedTempVariables[computedTempVariableOffset]; - computedTempVariableOffset++; - // typeof _tmp === "symbol" ? _tmp : _tmp + "" - propertyNames.push(factory.createConditionalExpression(factory.createTypeCheck(temp, "symbol"), - /*questionToken*/ undefined, temp, - /*colonToken*/ undefined, factory.createAdd(temp, factory.createStringLiteral("")))); - } - else { - propertyNames.push(factory.createStringLiteralFromNode(propertyName)); - } + /** Given value: o, propName: p, pattern: { a, b, ...p } from the original statement + * `{ a, b, ...p } = o`, create `p = __rest(o, ["a", "b"]);` + */ + function createRestHelper(value: ts.Expression, elements: readonly ts.BindingOrAssignmentElement[], computedTempVariables: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.Expression { + context.requestEmitHelper(restHelper); + const propertyNames: ts.Expression[] = []; + let computedTempVariableOffset = 0; + for (let i = 0; i < elements.length - 1; i++) { + const propertyName = ts.getPropertyNameOfBindingOrAssignmentElement(elements[i]); + if (propertyName) { + if (ts.isComputedPropertyName(propertyName)) { + ts.Debug.assertIsDefined(computedTempVariables, "Encountered computed property name but 'computedTempVariables' argument was not provided."); + const temp = computedTempVariables[computedTempVariableOffset]; + computedTempVariableOffset++; + // typeof _tmp === "symbol" ? _tmp : _tmp + "" + propertyNames.push(factory.createConditionalExpression(factory.createTypeCheck(temp, "symbol"), + /*questionToken*/ undefined, temp, + /*colonToken*/ undefined, factory.createAdd(temp, factory.createStringLiteral("")))); + } + else { + propertyNames.push(factory.createStringLiteralFromNode(propertyName)); } } - return factory.createCallExpression(getUnscopedHelperName("__rest"), - /*typeArguments*/ undefined, [ - value, - ts.setTextRange(factory.createArrayLiteralExpression(propertyNames), location) - ]); } + return factory.createCallExpression(getUnscopedHelperName("__rest"), + /*typeArguments*/ undefined, [ + value, + ts.setTextRange(factory.createArrayLiteralExpression(propertyNames), location) + ]); + } - // ES2017 Helpers + // ES2017 Helpers - function createAwaiterHelper(hasLexicalThis: boolean, hasLexicalArguments: boolean, promiseConstructor: ts.EntityName | ts.Expression | undefined, body: ts.Block) { - context.requestEmitHelper(awaiterHelper); - - const generatorFunc = factory.createFunctionExpression( - /*modifiers*/ undefined, factory.createToken(ts.SyntaxKind.AsteriskToken), - /*name*/ undefined, - /*typeParameters*/ undefined, - /*parameters*/ [], - /*type*/ undefined, body); - - // Mark this node as originally an async function - (generatorFunc.emitNode || (generatorFunc.emitNode = {} as ts.EmitNode)).flags |= ts.EmitFlags.AsyncFunctionBody | ts.EmitFlags.ReuseTempVariableScope; - return factory.createCallExpression(getUnscopedHelperName("__awaiter"), - /*typeArguments*/ undefined, [ - hasLexicalThis ? factory.createThis() : factory.createVoidZero(), - hasLexicalArguments ? factory.createIdentifier("arguments") : factory.createVoidZero(), - promiseConstructor ? ts.createExpressionFromEntityName(factory, promiseConstructor) : factory.createVoidZero(), - generatorFunc - ]); - } + function createAwaiterHelper(hasLexicalThis: boolean, hasLexicalArguments: boolean, promiseConstructor: ts.EntityName | ts.Expression | undefined, body: ts.Block) { + context.requestEmitHelper(awaiterHelper); + + const generatorFunc = factory.createFunctionExpression( + /*modifiers*/ undefined, factory.createToken(ts.SyntaxKind.AsteriskToken), + /*name*/ undefined, + /*typeParameters*/ undefined, + /*parameters*/ [], + /*type*/ undefined, body); + + // Mark this node as originally an async function + (generatorFunc.emitNode || (generatorFunc.emitNode = {} as ts.EmitNode)).flags |= ts.EmitFlags.AsyncFunctionBody | ts.EmitFlags.ReuseTempVariableScope; + return factory.createCallExpression(getUnscopedHelperName("__awaiter"), + /*typeArguments*/ undefined, [ + hasLexicalThis ? factory.createThis() : factory.createVoidZero(), + hasLexicalArguments ? factory.createIdentifier("arguments") : factory.createVoidZero(), + promiseConstructor ? ts.createExpressionFromEntityName(factory, promiseConstructor) : factory.createVoidZero(), + generatorFunc + ]); + } - // ES2015 Helpers + // ES2015 Helpers - function createExtendsHelper(name: ts.Identifier) { - context.requestEmitHelper(extendsHelper); - return factory.createCallExpression(getUnscopedHelperName("__extends"), - /*typeArguments*/ undefined, [name, factory.createUniqueName("_super", ts.GeneratedIdentifierFlags.Optimistic | ts.GeneratedIdentifierFlags.FileLevel)]); - } + function createExtendsHelper(name: ts.Identifier) { + context.requestEmitHelper(extendsHelper); + return factory.createCallExpression(getUnscopedHelperName("__extends"), + /*typeArguments*/ undefined, [name, factory.createUniqueName("_super", ts.GeneratedIdentifierFlags.Optimistic | ts.GeneratedIdentifierFlags.FileLevel)]); + } - function createTemplateObjectHelper(cooked: ts.ArrayLiteralExpression, raw: ts.ArrayLiteralExpression) { - context.requestEmitHelper(templateObjectHelper); - return factory.createCallExpression(getUnscopedHelperName("__makeTemplateObject"), - /*typeArguments*/ undefined, [cooked, raw]); - } + function createTemplateObjectHelper(cooked: ts.ArrayLiteralExpression, raw: ts.ArrayLiteralExpression) { + context.requestEmitHelper(templateObjectHelper); + return factory.createCallExpression(getUnscopedHelperName("__makeTemplateObject"), + /*typeArguments*/ undefined, [cooked, raw]); + } - function createSpreadArrayHelper(to: ts.Expression, from: ts.Expression, packFrom: boolean) { - context.requestEmitHelper(spreadArrayHelper); - return factory.createCallExpression(getUnscopedHelperName("__spreadArray"), - /*typeArguments*/ undefined, [to, from, packFrom ? immutableTrue() : immutableFalse()]); - } + function createSpreadArrayHelper(to: ts.Expression, from: ts.Expression, packFrom: boolean) { + context.requestEmitHelper(spreadArrayHelper); + return factory.createCallExpression(getUnscopedHelperName("__spreadArray"), + /*typeArguments*/ undefined, [to, from, packFrom ? immutableTrue() : immutableFalse()]); + } - // ES2015 Destructuring Helpers + // ES2015 Destructuring Helpers - function createValuesHelper(expression: ts.Expression) { - context.requestEmitHelper(valuesHelper); - return factory.createCallExpression(getUnscopedHelperName("__values"), - /*typeArguments*/ undefined, [expression]); - } + function createValuesHelper(expression: ts.Expression) { + context.requestEmitHelper(valuesHelper); + return factory.createCallExpression(getUnscopedHelperName("__values"), + /*typeArguments*/ undefined, [expression]); + } - function createReadHelper(iteratorRecord: ts.Expression, count: number | undefined) { - context.requestEmitHelper(readHelper); - return factory.createCallExpression(getUnscopedHelperName("__read"), - /*typeArguments*/ undefined, count !== undefined - ? [iteratorRecord, factory.createNumericLiteral(count + "")] - : [iteratorRecord]); - } + function createReadHelper(iteratorRecord: ts.Expression, count: number | undefined) { + context.requestEmitHelper(readHelper); + return factory.createCallExpression(getUnscopedHelperName("__read"), + /*typeArguments*/ undefined, count !== undefined + ? [iteratorRecord, factory.createNumericLiteral(count + "")] + : [iteratorRecord]); + } - // ES2015 Generator Helpers + // ES2015 Generator Helpers - function createGeneratorHelper(body: ts.FunctionExpression) { - context.requestEmitHelper(generatorHelper); - return factory.createCallExpression(getUnscopedHelperName("__generator"), - /*typeArguments*/ undefined, [factory.createThis(), body]); - } + function createGeneratorHelper(body: ts.FunctionExpression) { + context.requestEmitHelper(generatorHelper); + return factory.createCallExpression(getUnscopedHelperName("__generator"), + /*typeArguments*/ undefined, [factory.createThis(), body]); + } - // ES Module Helpers + // ES Module Helpers - function createCreateBindingHelper(module: ts.Expression, inputName: ts.Expression, outputName: ts.Expression | undefined) { - context.requestEmitHelper(createBindingHelper); - return factory.createCallExpression(getUnscopedHelperName("__createBinding"), - /*typeArguments*/ undefined, [factory.createIdentifier("exports"), module, inputName, ...(outputName ? [outputName] : [])]); - } + function createCreateBindingHelper(module: ts.Expression, inputName: ts.Expression, outputName: ts.Expression | undefined) { + context.requestEmitHelper(createBindingHelper); + return factory.createCallExpression(getUnscopedHelperName("__createBinding"), + /*typeArguments*/ undefined, [factory.createIdentifier("exports"), module, inputName, ...(outputName ? [outputName] : [])]); + } - function createImportStarHelper(expression: ts.Expression) { - context.requestEmitHelper(importStarHelper); - return factory.createCallExpression(getUnscopedHelperName("__importStar"), - /*typeArguments*/ undefined, [expression]); - } + function createImportStarHelper(expression: ts.Expression) { + context.requestEmitHelper(importStarHelper); + return factory.createCallExpression(getUnscopedHelperName("__importStar"), + /*typeArguments*/ undefined, [expression]); + } - function createImportStarCallbackHelper() { - context.requestEmitHelper(importStarHelper); - return getUnscopedHelperName("__importStar"); - } + function createImportStarCallbackHelper() { + context.requestEmitHelper(importStarHelper); + return getUnscopedHelperName("__importStar"); + } - function createImportDefaultHelper(expression: ts.Expression) { - context.requestEmitHelper(importDefaultHelper); - return factory.createCallExpression(getUnscopedHelperName("__importDefault"), - /*typeArguments*/ undefined, [expression]); - } + function createImportDefaultHelper(expression: ts.Expression) { + context.requestEmitHelper(importDefaultHelper); + return factory.createCallExpression(getUnscopedHelperName("__importDefault"), + /*typeArguments*/ undefined, [expression]); + } - function createExportStarHelper(moduleExpression: ts.Expression, exportsExpression: ts.Expression = factory.createIdentifier("exports")) { - context.requestEmitHelper(exportStarHelper); - context.requestEmitHelper(createBindingHelper); - return factory.createCallExpression(getUnscopedHelperName("__exportStar"), - /*typeArguments*/ undefined, [moduleExpression, exportsExpression]); - } + function createExportStarHelper(moduleExpression: ts.Expression, exportsExpression: ts.Expression = factory.createIdentifier("exports")) { + context.requestEmitHelper(exportStarHelper); + context.requestEmitHelper(createBindingHelper); + return factory.createCallExpression(getUnscopedHelperName("__exportStar"), + /*typeArguments*/ undefined, [moduleExpression, exportsExpression]); + } - // Class Fields Helpers + // Class Fields Helpers - function createClassPrivateFieldGetHelper(receiver: ts.Expression, state: ts.Identifier, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined) { - context.requestEmitHelper(classPrivateFieldGetHelper); - let args; - if (!f) { - args = [receiver, state, factory.createStringLiteral(kind)]; - } - else { - args = [receiver, state, factory.createStringLiteral(kind), f]; - } - return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldGet"), /*typeArguments*/ undefined, args); + function createClassPrivateFieldGetHelper(receiver: ts.Expression, state: ts.Identifier, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined) { + context.requestEmitHelper(classPrivateFieldGetHelper); + let args; + if (!f) { + args = [receiver, state, factory.createStringLiteral(kind)]; } - - function createClassPrivateFieldSetHelper(receiver: ts.Expression, state: ts.Identifier, value: ts.Expression, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined) { - context.requestEmitHelper(classPrivateFieldSetHelper); - let args; - if (!f) { - args = [receiver, state, value, factory.createStringLiteral(kind)]; - } - else { - args = [receiver, state, value, factory.createStringLiteral(kind), f]; - } - return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldSet"), /*typeArguments*/ undefined, args); + else { + args = [receiver, state, factory.createStringLiteral(kind), f]; } + return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldGet"), /*typeArguments*/ undefined, args); + } - function createClassPrivateFieldInHelper(state: ts.Identifier, receiver: ts.Expression) { - context.requestEmitHelper(classPrivateFieldInHelper); - return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldIn"), /* typeArguments*/ undefined, [state, receiver]); + function createClassPrivateFieldSetHelper(receiver: ts.Expression, state: ts.Identifier, value: ts.Expression, kind: ts.PrivateIdentifierKind, f: ts.Identifier | undefined) { + context.requestEmitHelper(classPrivateFieldSetHelper); + let args; + if (!f) { + args = [receiver, state, value, factory.createStringLiteral(kind)]; } + else { + args = [receiver, state, value, factory.createStringLiteral(kind), f]; + } + return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldSet"), /*typeArguments*/ undefined, args); } - /* @internal */ - export function compareEmitHelpers(x: ts.EmitHelper, y: ts.EmitHelper) { - if (x === y) - return ts.Comparison.EqualTo; - if (x.priority === y.priority) - return ts.Comparison.EqualTo; - if (x.priority === undefined) - return ts.Comparison.GreaterThan; - if (y.priority === undefined) - return ts.Comparison.LessThan; - return ts.compareValues(x.priority, y.priority); + function createClassPrivateFieldInHelper(state: ts.Identifier, receiver: ts.Expression) { + context.requestEmitHelper(classPrivateFieldInHelper); + return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldIn"), /* typeArguments*/ undefined, [state, receiver]); } +} - /** - * @param input Template string input strings - * @param args Names which need to be made file-level unique - */ - export function helperString(input: TemplateStringsArray, ...args: string[]) { - return (uniqueName: ts.EmitHelperUniqueNameCallback) => { - let result = ""; - for (let i = 0; i < args.length; i++) { - result += input[i]; - result += uniqueName(args[i]); - } - result += input[input.length - 1]; - return result; - }; - } +/* @internal */ +export function compareEmitHelpers(x: ts.EmitHelper, y: ts.EmitHelper) { + if (x === y) + return ts.Comparison.EqualTo; + if (x.priority === y.priority) + return ts.Comparison.EqualTo; + if (x.priority === undefined) + return ts.Comparison.GreaterThan; + if (y.priority === undefined) + return ts.Comparison.LessThan; + return ts.compareValues(x.priority, y.priority); +} - // TypeScript Helpers +/** + * @param input Template string input strings + * @param args Names which need to be made file-level unique + */ +export function helperString(input: TemplateStringsArray, ...args: string[]) { + return (uniqueName: ts.EmitHelperUniqueNameCallback) => { + let result = ""; + for (let i = 0; i < args.length; i++) { + result += input[i]; + result += uniqueName(args[i]); + } + result += input[input.length - 1]; + return result; + }; +} - export const decorateHelper: ts.UnscopedEmitHelper = { - name: "typescript:decorate", - importName: "__decorate", - scoped: false, - priority: 2, - text: ` +// TypeScript Helpers + +export const decorateHelper: ts.UnscopedEmitHelper = { + name: "typescript:decorate", + importName: "__decorate", + scoped: false, + priority: 2, + text: ` var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };` - }; - - export const metadataHelper: ts.UnscopedEmitHelper = { - name: "typescript:metadata", - importName: "__metadata", - scoped: false, - priority: 3, - text: ` +}; + +export const metadataHelper: ts.UnscopedEmitHelper = { + name: "typescript:metadata", + importName: "__metadata", + scoped: false, + priority: 3, + text: ` var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); };` - }; - - export const paramHelper: ts.UnscopedEmitHelper = { - name: "typescript:param", - importName: "__param", - scoped: false, - priority: 4, - text: ` +}; + +export const paramHelper: ts.UnscopedEmitHelper = { + name: "typescript:param", + importName: "__param", + scoped: false, + priority: 4, + text: ` var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } };` - }; +}; - // ES2018 Helpers +// ES2018 Helpers - export const assignHelper: ts.UnscopedEmitHelper = { - name: "typescript:assign", - importName: "__assign", - scoped: false, - priority: 1, - text: ` +export const assignHelper: ts.UnscopedEmitHelper = { + name: "typescript:assign", + importName: "__assign", + scoped: false, + priority: 1, + text: ` var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { @@ -417,22 +417,22 @@ namespace ts { }; return __assign.apply(this, arguments); };` - }; +}; - export const awaitHelper: ts.UnscopedEmitHelper = { - name: "typescript:await", - importName: "__await", - scoped: false, - text: ` +export const awaitHelper: ts.UnscopedEmitHelper = { + name: "typescript:await", + importName: "__await", + scoped: false, + text: ` var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }` - }; - - export const asyncGeneratorHelper: ts.UnscopedEmitHelper = { - name: "typescript:asyncGenerator", - importName: "__asyncGenerator", - scoped: false, - dependencies: [awaitHelper], - text: ` +}; + +export const asyncGeneratorHelper: ts.UnscopedEmitHelper = { + name: "typescript:asyncGenerator", + importName: "__asyncGenerator", + scoped: false, + dependencies: [awaitHelper], + text: ` var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; @@ -444,26 +444,26 @@ namespace ts { function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } };` - }; - - export const asyncDelegator: ts.UnscopedEmitHelper = { - name: "typescript:asyncDelegator", - importName: "__asyncDelegator", - scoped: false, - dependencies: [awaitHelper], - text: ` +}; + +export const asyncDelegator: ts.UnscopedEmitHelper = { + name: "typescript:asyncDelegator", + importName: "__asyncDelegator", + scoped: false, + dependencies: [awaitHelper], + text: ` var __asyncDelegator = (this && this.__asyncDelegator) || function (o) { var i, p; return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } };` - }; +}; - export const asyncValues: ts.UnscopedEmitHelper = { - name: "typescript:asyncValues", - importName: "__asyncValues", - scoped: false, - text: ` +export const asyncValues: ts.UnscopedEmitHelper = { + name: "typescript:asyncValues", + importName: "__asyncValues", + scoped: false, + text: ` var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; @@ -471,15 +471,15 @@ namespace ts { function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } };` - }; +}; - // ES2018 Destructuring Helpers +// ES2018 Destructuring Helpers - export const restHelper: ts.UnscopedEmitHelper = { - name: "typescript:rest", - importName: "__rest", - scoped: false, - text: ` +export const restHelper: ts.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) @@ -491,16 +491,16 @@ namespace ts { } return t; };` - }; +}; - // ES2017 Helpers +// ES2017 Helpers - export const awaiterHelper: ts.UnscopedEmitHelper = { - name: "typescript:awaiter", - importName: "__awaiter", - scoped: false, - priority: 5, - text: ` +export const awaiterHelper: ts.UnscopedEmitHelper = { + name: "typescript:awaiter", + importName: "__awaiter", + scoped: false, + priority: 5, + text: ` var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -510,16 +510,16 @@ namespace ts { step((generator = generator.apply(thisArg, _arguments || [])).next()); }); };` - }; +}; - // ES2015 Helpers +// ES2015 Helpers - export const extendsHelper: ts.UnscopedEmitHelper = { - name: "typescript:extends", - importName: "__extends", - scoped: false, - priority: 0, - text: ` +export const extendsHelper: ts.UnscopedEmitHelper = { + name: "typescript:extends", + importName: "__extends", + scoped: false, + priority: 0, + text: ` var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || @@ -536,25 +536,25 @@ namespace ts { d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })();` - }; - - export const templateObjectHelper: ts.UnscopedEmitHelper = { - name: "typescript:makeTemplateObject", - importName: "__makeTemplateObject", - scoped: false, - priority: 0, - text: ` +}; + +export const templateObjectHelper: ts.UnscopedEmitHelper = { + name: "typescript:makeTemplateObject", + importName: "__makeTemplateObject", + scoped: false, + priority: 0, + text: ` var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; };` - }; +}; - export const readHelper: ts.UnscopedEmitHelper = { - name: "typescript:read", - importName: "__read", - scoped: false, - text: ` +export const readHelper: ts.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; @@ -571,13 +571,13 @@ namespace ts { } return ar; };` - }; +}; - export const spreadArrayHelper: ts.UnscopedEmitHelper = { - name: "typescript:spreadArray", - importName: "__spreadArray", - scoped: false, - text: ` +export const spreadArrayHelper: ts.UnscopedEmitHelper = { + name: "typescript:spreadArray", + importName: "__spreadArray", + scoped: false, + text: ` var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { @@ -587,15 +587,15 @@ namespace ts { } return to.concat(ar || Array.prototype.slice.call(from)); };` - }; +}; - // ES2015 Destructuring Helpers +// ES2015 Destructuring Helpers - export const valuesHelper: ts.UnscopedEmitHelper = { - name: "typescript:values", - importName: "__values", - scoped: false, - text: ` +export const valuesHelper: ts.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); @@ -607,75 +607,75 @@ namespace ts { }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); };` - }; - - // ES2015 Generator Helpers - - // The __generator helper is used by down-level transformations to emulate the runtime - // semantics of an ES2015 generator function. When called, this helper returns an - // object that implements the Iterator protocol, in that it has `next`, `return`, and - // `throw` methods that step through the generator when invoked. - // - // parameters: - // @param thisArg The value to use as the `this` binding for the transformed generator body. - // @param body A function that acts as the transformed generator body. - // - // variables: - // _ Persistent state for the generator that is shared between the helper and the - // generator body. The state object has the following members: - // sent() - A method that returns or throws the current completion value. - // label - The next point at which to resume evaluation of the generator body. - // trys - A stack of protected regions (try/catch/finally blocks). - // ops - A stack of pending instructions when inside of a finally block. - // f A value indicating whether the generator is executing. - // y An iterator to delegate for a yield*. - // t A temporary variable that holds one of the following values (note that these - // cases do not overlap): - // - The completion value when resuming from a `yield` or `yield*`. - // - The error value for a catch block. - // - The current protected region (array of try/catch/finally/end labels). - // - The verb (`next`, `throw`, or `return` method) to delegate to the expression - // of a `yield*`. - // - The result of evaluating the verb delegated to the expression of a `yield*`. - // - // functions: - // verb(n) Creates a bound callback to the `step` function for opcode `n`. - // step(op) Evaluates opcodes in a generator body until execution is suspended or - // completed. - // - // The __generator helper understands a limited set of instructions: - // 0: next(value?) - Start or resume the generator with the specified value. - // 1: throw(error) - Resume the generator with an exception. If the generator is - // suspended inside of one or more protected regions, evaluates - // any intervening finally blocks between the current label and - // the nearest catch block or function boundary. If uncaught, the - // exception is thrown to the caller. - // 2: return(value?) - Resume the generator as if with a return. If the generator is - // suspended inside of one or more protected regions, evaluates any - // intervening finally blocks. - // 3: break(label) - Jump to the specified label. If the label is outside of the - // current protected region, evaluates any intervening finally - // blocks. - // 4: yield(value?) - Yield execution to the caller with an optional value. When - // resumed, the generator will continue at the next label. - // 5: yield*(value) - Delegates evaluation to the supplied iterator. When - // delegation completes, the generator will continue at the next - // label. - // 6: catch(error) - Handles an exception thrown from within the generator body. If - // the current label is inside of one or more protected regions, - // evaluates any intervening finally blocks between the current - // label and the nearest catch block or function boundary. If - // uncaught, the exception is thrown to the caller. - // 7: endfinally - Ends a finally block, resuming the last instruction prior to - // entering a finally block. - // - // For examples of how these are used, see the comments in ./transformers/generators.ts - export const generatorHelper: ts.UnscopedEmitHelper = { - name: "typescript:generator", - importName: "__generator", - scoped: false, - priority: 6, - text: ` +}; + +// ES2015 Generator Helpers + +// The __generator helper is used by down-level transformations to emulate the runtime +// semantics of an ES2015 generator function. When called, this helper returns an +// object that implements the Iterator protocol, in that it has `next`, `return`, and +// `throw` methods that step through the generator when invoked. +// +// parameters: +// @param thisArg The value to use as the `this` binding for the transformed generator body. +// @param body A function that acts as the transformed generator body. +// +// variables: +// _ Persistent state for the generator that is shared between the helper and the +// generator body. The state object has the following members: +// sent() - A method that returns or throws the current completion value. +// label - The next point at which to resume evaluation of the generator body. +// trys - A stack of protected regions (try/catch/finally blocks). +// ops - A stack of pending instructions when inside of a finally block. +// f A value indicating whether the generator is executing. +// y An iterator to delegate for a yield*. +// t A temporary variable that holds one of the following values (note that these +// cases do not overlap): +// - The completion value when resuming from a `yield` or `yield*`. +// - The error value for a catch block. +// - The current protected region (array of try/catch/finally/end labels). +// - The verb (`next`, `throw`, or `return` method) to delegate to the expression +// of a `yield*`. +// - The result of evaluating the verb delegated to the expression of a `yield*`. +// +// functions: +// verb(n) Creates a bound callback to the `step` function for opcode `n`. +// step(op) Evaluates opcodes in a generator body until execution is suspended or +// completed. +// +// The __generator helper understands a limited set of instructions: +// 0: next(value?) - Start or resume the generator with the specified value. +// 1: throw(error) - Resume the generator with an exception. If the generator is +// suspended inside of one or more protected regions, evaluates +// any intervening finally blocks between the current label and +// the nearest catch block or function boundary. If uncaught, the +// exception is thrown to the caller. +// 2: return(value?) - Resume the generator as if with a return. If the generator is +// suspended inside of one or more protected regions, evaluates any +// intervening finally blocks. +// 3: break(label) - Jump to the specified label. If the label is outside of the +// current protected region, evaluates any intervening finally +// blocks. +// 4: yield(value?) - Yield execution to the caller with an optional value. When +// resumed, the generator will continue at the next label. +// 5: yield*(value) - Delegates evaluation to the supplied iterator. When +// delegation completes, the generator will continue at the next +// label. +// 6: catch(error) - Handles an exception thrown from within the generator body. If +// the current label is inside of one or more protected regions, +// evaluates any intervening finally blocks between the current +// label and the nearest catch block or function boundary. If +// uncaught, the exception is thrown to the caller. +// 7: endfinally - Ends a finally block, resuming the last instruction prior to +// entering a finally block. +// +// For examples of how these are used, see the comments in ./transformers/generators.ts +export const generatorHelper: ts.UnscopedEmitHelper = { + name: "typescript:generator", + importName: "__generator", + scoped: false, + priority: 6, + text: ` var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; @@ -703,16 +703,16 @@ namespace ts { if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } };` - }; +}; - // ES Module Helpers +// ES Module Helpers - export const createBindingHelper: ts.UnscopedEmitHelper = { - name: "typescript:commonjscreatebinding", - importName: "__createBinding", - scoped: false, - priority: 1, - text: ` +export const createBindingHelper: ts.UnscopedEmitHelper = { + name: "typescript:commonjscreatebinding", + importName: "__createBinding", + scoped: false, + priority: 1, + text: ` var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); @@ -724,29 +724,29 @@ namespace ts { if (k2 === undefined) k2 = k; o[k2] = m[k]; }));` - }; - - export const setModuleDefaultHelper: ts.UnscopedEmitHelper = { - name: "typescript:commonjscreatevalue", - importName: "__setModuleDefault", - scoped: false, - priority: 1, - text: ` +}; + +export const setModuleDefaultHelper: ts.UnscopedEmitHelper = { + name: "typescript:commonjscreatevalue", + importName: "__setModuleDefault", + scoped: false, + priority: 1, + text: ` var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; });` - }; - - // emit helper for `import * as Name from "foo"` - export const importStarHelper: ts.UnscopedEmitHelper = { - name: "typescript:commonjsimportstar", - importName: "__importStar", - scoped: false, - dependencies: [createBindingHelper, setModuleDefaultHelper], - priority: 2, - text: ` +}; + +// emit helper for `import * as Name from "foo"` +export const importStarHelper: ts.UnscopedEmitHelper = { + name: "typescript:commonjsimportstar", + importName: "__importStar", + scoped: false, + dependencies: [createBindingHelper, setModuleDefaultHelper], + priority: 2, + text: ` var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; @@ -754,230 +754,230 @@ namespace ts { __setModuleDefault(result, mod); return result; };` - }; - - // emit helper for `import Name from "foo"` - export const importDefaultHelper: ts.UnscopedEmitHelper = { - name: "typescript:commonjsimportdefault", - importName: "__importDefault", - scoped: false, - text: ` +}; + +// emit helper for `import Name from "foo"` +export const importDefaultHelper: ts.UnscopedEmitHelper = { + name: "typescript:commonjsimportdefault", + importName: "__importDefault", + scoped: false, + text: ` var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; };` - }; - - export const exportStarHelper: ts.UnscopedEmitHelper = { - name: "typescript:export-star", - importName: "__exportStar", - scoped: false, - dependencies: [createBindingHelper], - priority: 2, - text: ` +}; + +export const exportStarHelper: ts.UnscopedEmitHelper = { + name: "typescript:export-star", + importName: "__exportStar", + scoped: false, + dependencies: [createBindingHelper], + priority: 2, + text: ` var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); };` - }; - - /** - * Parameters: - * @param receiver — The object from which the private member will be read. - * @param state — One of the following: - * - A WeakMap used to read a private instance field. - * - A WeakSet used as an instance brand for private instance methods and accessors. - * - A function value that should be the undecorated class constructor used to brand check private static fields, methods, and accessors. - * @param kind — (optional pre TS 4.3, required for TS 4.3+) One of the following values: - * - undefined — Indicates a private instance field (pre TS 4.3). - * - "f" — Indicates a private field (instance or static). - * - "m" — Indicates a private method (instance or static). - * - "a" — Indicates a private accessor (instance or static). - * @param f — (optional pre TS 4.3) Depends on the arguments for state and kind: - * - If kind is "m", this should be the function corresponding to the static or instance method. - * - If kind is "a", this should be the function corresponding to the getter method, or undefined if the getter was not defined. - * - If kind is "f" and state is a function, this should be an object holding the value of a static field, or undefined if the static field declaration has not yet been evaluated. - * Usage: - * This helper will only ever be used by the compiler in the following ways: - * - * Reading from a private instance field (pre TS 4.3): - * __classPrivateFieldGet(, ) - * - * Reading from a private instance field (TS 4.3+): - * __classPrivateFieldGet(, , "f") - * - * Reading from a private instance get accessor (when defined, TS 4.3+): - * __classPrivateFieldGet(, , "a", ) - * - * Reading from a private instance get accessor (when not defined, TS 4.3+): - * __classPrivateFieldGet(, , "a", void 0) - * NOTE: This always results in a runtime error. - * - * Reading from a private instance method (TS 4.3+): - * __classPrivateFieldGet(, , "m", ) - * - * Reading from a private static field (TS 4.3+): - * __classPrivateFieldGet(, , "f", <{ value: any }>) - * - * Reading from a private static get accessor (when defined, TS 4.3+): - * __classPrivateFieldGet(, , "a", ) - * - * Reading from a private static get accessor (when not defined, TS 4.3+): - * __classPrivateFieldGet(, , "a", void 0) - * NOTE: This always results in a runtime error. - * - * Reading from a private static method (TS 4.3+): - * __classPrivateFieldGet(, , "m", ) - */ - export const classPrivateFieldGetHelper: ts.UnscopedEmitHelper = { - name: "typescript:classPrivateFieldGet", - importName: "__classPrivateFieldGet", - scoped: false, - text: ` +}; + +/** + * Parameters: + * @param receiver — The object from which the private member will be read. + * @param state — One of the following: + * - A WeakMap used to read a private instance field. + * - A WeakSet used as an instance brand for private instance methods and accessors. + * - A function value that should be the undecorated class constructor used to brand check private static fields, methods, and accessors. + * @param kind — (optional pre TS 4.3, required for TS 4.3+) One of the following values: + * - undefined — Indicates a private instance field (pre TS 4.3). + * - "f" — Indicates a private field (instance or static). + * - "m" — Indicates a private method (instance or static). + * - "a" — Indicates a private accessor (instance or static). + * @param f — (optional pre TS 4.3) Depends on the arguments for state and kind: + * - If kind is "m", this should be the function corresponding to the static or instance method. + * - If kind is "a", this should be the function corresponding to the getter method, or undefined if the getter was not defined. + * - If kind is "f" and state is a function, this should be an object holding the value of a static field, or undefined if the static field declaration has not yet been evaluated. + * Usage: + * This helper will only ever be used by the compiler in the following ways: + * + * Reading from a private instance field (pre TS 4.3): + * __classPrivateFieldGet(, ) + * + * Reading from a private instance field (TS 4.3+): + * __classPrivateFieldGet(, , "f") + * + * Reading from a private instance get accessor (when defined, TS 4.3+): + * __classPrivateFieldGet(, , "a", ) + * + * Reading from a private instance get accessor (when not defined, TS 4.3+): + * __classPrivateFieldGet(, , "a", void 0) + * NOTE: This always results in a runtime error. + * + * Reading from a private instance method (TS 4.3+): + * __classPrivateFieldGet(, , "m", ) + * + * Reading from a private static field (TS 4.3+): + * __classPrivateFieldGet(, , "f", <{ value: any }>) + * + * Reading from a private static get accessor (when defined, TS 4.3+): + * __classPrivateFieldGet(, , "a", ) + * + * Reading from a private static get accessor (when not defined, TS 4.3+): + * __classPrivateFieldGet(, , "a", void 0) + * NOTE: This always results in a runtime error. + * + * Reading from a private static method (TS 4.3+): + * __classPrivateFieldGet(, , "m", ) + */ +export const classPrivateFieldGetHelper: ts.UnscopedEmitHelper = { + name: "typescript:classPrivateFieldGet", + importName: "__classPrivateFieldGet", + scoped: false, + text: ` var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); };` - }; - - /** - * Parameters: - * @param receiver — The object on which the private member will be set. - * @param state — One of the following: - * - A WeakMap used to store a private instance field. - * - A WeakSet used as an instance brand for private instance methods and accessors. - * - A function value that should be the undecorated class constructor used to brand check private static fields, methods, and accessors. - * @param value — The value to set. - * @param kind — (optional pre TS 4.3, required for TS 4.3+) One of the following values: - * - undefined — Indicates a private instance field (pre TS 4.3). - * - "f" — Indicates a private field (instance or static). - * - "m" — Indicates a private method (instance or static). - * - "a" — Indicates a private accessor (instance or static). - * @param f — (optional pre TS 4.3) Depends on the arguments for state and kind: - * - If kind is "m", this should be the function corresponding to the static or instance method. - * - If kind is "a", this should be the function corresponding to the setter method, or undefined if the setter was not defined. - * - If kind is "f" and state is a function, this should be an object holding the value of a static field, or undefined if the static field declaration has not yet been evaluated. - * Usage: - * This helper will only ever be used by the compiler in the following ways: - * - * Writing to a private instance field (pre TS 4.3): - * __classPrivateFieldSet(, , ) - * - * Writing to a private instance field (TS 4.3+): - * __classPrivateFieldSet(, , , "f") - * - * Writing to a private instance set accessor (when defined, TS 4.3+): - * __classPrivateFieldSet(, , , "a", ) - * - * Writing to a private instance set accessor (when not defined, TS 4.3+): - * __classPrivateFieldSet(, , , "a", void 0) - * NOTE: This always results in a runtime error. - * - * Writing to a private instance method (TS 4.3+): - * __classPrivateFieldSet(, , , "m", ) - * NOTE: This always results in a runtime error. - * - * Writing to a private static field (TS 4.3+): - * __classPrivateFieldSet(, , , "f", <{ value: any }>) - * - * Writing to a private static set accessor (when defined, TS 4.3+): - * __classPrivateFieldSet(, , , "a", ) - * - * Writing to a private static set accessor (when not defined, TS 4.3+): - * __classPrivateFieldSet(, , , "a", void 0) - * NOTE: This always results in a runtime error. - * - * Writing to a private static method (TS 4.3+): - * __classPrivateFieldSet(, , , "m", ) - * NOTE: This always results in a runtime error. - */ - export const classPrivateFieldSetHelper: ts.UnscopedEmitHelper = { - name: "typescript:classPrivateFieldSet", - importName: "__classPrivateFieldSet", - scoped: false, - text: ` +}; + +/** + * Parameters: + * @param receiver — The object on which the private member will be set. + * @param state — One of the following: + * - A WeakMap used to store a private instance field. + * - A WeakSet used as an instance brand for private instance methods and accessors. + * - A function value that should be the undecorated class constructor used to brand check private static fields, methods, and accessors. + * @param value — The value to set. + * @param kind — (optional pre TS 4.3, required for TS 4.3+) One of the following values: + * - undefined — Indicates a private instance field (pre TS 4.3). + * - "f" — Indicates a private field (instance or static). + * - "m" — Indicates a private method (instance or static). + * - "a" — Indicates a private accessor (instance or static). + * @param f — (optional pre TS 4.3) Depends on the arguments for state and kind: + * - If kind is "m", this should be the function corresponding to the static or instance method. + * - If kind is "a", this should be the function corresponding to the setter method, or undefined if the setter was not defined. + * - If kind is "f" and state is a function, this should be an object holding the value of a static field, or undefined if the static field declaration has not yet been evaluated. + * Usage: + * This helper will only ever be used by the compiler in the following ways: + * + * Writing to a private instance field (pre TS 4.3): + * __classPrivateFieldSet(, , ) + * + * Writing to a private instance field (TS 4.3+): + * __classPrivateFieldSet(, , , "f") + * + * Writing to a private instance set accessor (when defined, TS 4.3+): + * __classPrivateFieldSet(, , , "a", ) + * + * Writing to a private instance set accessor (when not defined, TS 4.3+): + * __classPrivateFieldSet(, , , "a", void 0) + * NOTE: This always results in a runtime error. + * + * Writing to a private instance method (TS 4.3+): + * __classPrivateFieldSet(, , , "m", ) + * NOTE: This always results in a runtime error. + * + * Writing to a private static field (TS 4.3+): + * __classPrivateFieldSet(, , , "f", <{ value: any }>) + * + * Writing to a private static set accessor (when defined, TS 4.3+): + * __classPrivateFieldSet(, , , "a", ) + * + * Writing to a private static set accessor (when not defined, TS 4.3+): + * __classPrivateFieldSet(, , , "a", void 0) + * NOTE: This always results in a runtime error. + * + * Writing to a private static method (TS 4.3+): + * __classPrivateFieldSet(, , , "m", ) + * NOTE: This always results in a runtime error. + */ +export const classPrivateFieldSetHelper: ts.UnscopedEmitHelper = { + name: "typescript:classPrivateFieldSet", + importName: "__classPrivateFieldSet", + scoped: false, + text: ` var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; };` - }; - - /** - * Parameters: - * @param state — One of the following: - * - A WeakMap when the member is a private instance field. - * - A WeakSet when the member is a private instance method or accessor. - * - A function value that should be the undecorated class constructor when the member is a private static field, method, or accessor. - * @param receiver — The object being checked if it has the private member. - * - * Usage: - * This helper is used to transform `#field in expression` to - * `__classPrivateFieldIn(, expression)` - */ - export const classPrivateFieldInHelper: ts.UnscopedEmitHelper = { - name: "typescript:classPrivateFieldIn", - importName: "__classPrivateFieldIn", - scoped: false, - text: ` +}; + +/** + * Parameters: + * @param state — One of the following: + * - A WeakMap when the member is a private instance field. + * - A WeakSet when the member is a private instance method or accessor. + * - A function value that should be the undecorated class constructor when the member is a private static field, method, or accessor. + * @param receiver — The object being checked if it has the private member. + * + * Usage: + * This helper is used to transform `#field in expression` to + * `__classPrivateFieldIn(, expression)` + */ +export const classPrivateFieldInHelper: ts.UnscopedEmitHelper = { + name: "typescript:classPrivateFieldIn", + importName: "__classPrivateFieldIn", + scoped: false, + text: ` var __classPrivateFieldIn = (this && this.__classPrivateFieldIn) || function(state, receiver) { if (receiver === null || (typeof receiver !== "object" && typeof receiver !== "function")) throw new TypeError("Cannot use 'in' operator on non-object"); return typeof state === "function" ? receiver === state : state.has(receiver); };` - }; - - let allUnscopedEmitHelpers: ts.ReadonlyESMap | undefined; - - export function getAllUnscopedEmitHelpers() { - return allUnscopedEmitHelpers || (allUnscopedEmitHelpers = ts.arrayToMap([ - decorateHelper, - metadataHelper, - paramHelper, - assignHelper, - awaitHelper, - asyncGeneratorHelper, - asyncDelegator, - asyncValues, - restHelper, - awaiterHelper, - extendsHelper, - templateObjectHelper, - spreadArrayHelper, - valuesHelper, - readHelper, - generatorHelper, - importStarHelper, - importDefaultHelper, - exportStarHelper, - classPrivateFieldGetHelper, - classPrivateFieldSetHelper, - classPrivateFieldInHelper, - createBindingHelper, - setModuleDefaultHelper - ], helper => helper.name)); - } +}; + +let allUnscopedEmitHelpers: ts.ReadonlyESMap | undefined; + +export function getAllUnscopedEmitHelpers() { + return allUnscopedEmitHelpers || (allUnscopedEmitHelpers = ts.arrayToMap([ + decorateHelper, + metadataHelper, + paramHelper, + assignHelper, + awaitHelper, + asyncGeneratorHelper, + asyncDelegator, + asyncValues, + restHelper, + awaiterHelper, + extendsHelper, + templateObjectHelper, + spreadArrayHelper, + valuesHelper, + readHelper, + generatorHelper, + importStarHelper, + importDefaultHelper, + exportStarHelper, + classPrivateFieldGetHelper, + classPrivateFieldSetHelper, + classPrivateFieldInHelper, + createBindingHelper, + setModuleDefaultHelper + ], helper => helper.name)); +} - export const asyncSuperHelper: ts.EmitHelper = { - name: "typescript:async-super", - scoped: true, - text: helperString` +export const asyncSuperHelper: ts.EmitHelper = { + name: "typescript:async-super", + scoped: true, + text: helperString` const ${"_superIndex"} = name => super[name];` - }; +}; - export const advancedAsyncSuperHelper: ts.EmitHelper = { - name: "typescript:advanced-async-super", - scoped: true, - text: helperString` +export const advancedAsyncSuperHelper: ts.EmitHelper = { + name: "typescript:advanced-async-super", + scoped: true, + text: helperString` const ${"_superIndex"} = (function (geti, seti) { const cache = Object.create(null); return name => cache[name] || (cache[name] = { get value() { return geti(name); }, set value(v) { seti(name, v); } }); })(name => super[name], (name, value) => super[name] = value);` - }; +}; - export function isCallToHelper(firstSegment: ts.Expression, helperName: ts.__String): boolean { - return ts.isCallExpression(firstSegment) - && ts.isIdentifier(firstSegment.expression) - && (ts.getEmitFlags(firstSegment.expression) & ts.EmitFlags.HelperName) !== 0 - && firstSegment.expression.escapedText === helperName; - } +export function isCallToHelper(firstSegment: ts.Expression, helperName: ts.__String): boolean { + return ts.isCallExpression(firstSegment) + && ts.isIdentifier(firstSegment.expression) + && (ts.getEmitFlags(firstSegment.expression) & ts.EmitFlags.HelperName) !== 0 + && firstSegment.expression.escapedText === helperName; +} } diff --git a/src/compiler/factory/emitNode.ts b/src/compiler/factory/emitNode.ts index 3b37f3fc5f6d3..0c4617defd70e 100644 --- a/src/compiler/factory/emitNode.ts +++ b/src/compiler/factory/emitNode.ts @@ -1,295 +1,295 @@ namespace ts { - /** - * Associates a node with the current transformation, initializing - * various transient transformation properties. - * @internal - */ - export function getOrCreateEmitNode(node: ts.Node): ts.EmitNode { - if (!node.emitNode) { - if (ts.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 === ts.SyntaxKind.SourceFile) { - return node.emitNode = { annotatedNodes: [node] } as ts.EmitNode; - } - - const sourceFile = ts.getSourceFileOfNode(ts.getParseTreeNode(ts.getSourceFileOfNode(node))) ?? ts.Debug.fail("Could not determine parsed source file."); - getOrCreateEmitNode(sourceFile).annotatedNodes!.push(node); +/** + * Associates a node with the current transformation, initializing + * various transient transformation properties. + * @internal + */ +export function getOrCreateEmitNode(node: ts.Node): ts.EmitNode { + if (!node.emitNode) { + if (ts.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 === ts.SyntaxKind.SourceFile) { + return node.emitNode = { annotatedNodes: [node] } as ts.EmitNode; } - node.emitNode = {} as ts.EmitNode; + const sourceFile = ts.getSourceFileOfNode(ts.getParseTreeNode(ts.getSourceFileOfNode(node))) ?? ts.Debug.fail("Could not determine parsed source file."); + getOrCreateEmitNode(sourceFile).annotatedNodes!.push(node); } - else { - ts.Debug.assert(!(node.emitNode.flags & ts.EmitFlags.Immutable), "Invalid attempt to mutate an immutable node."); - } - return node.emitNode; + + node.emitNode = {} as ts.EmitNode; + } + else { + ts.Debug.assert(!(node.emitNode.flags & ts.EmitFlags.Immutable), "Invalid attempt to mutate an immutable node."); } + return node.emitNode; +} - /** - * Clears any `EmitNode` entries from parse-tree nodes. - * @param sourceFile A source file. - */ - export function disposeEmitNodes(sourceFile: ts.SourceFile | undefined) { - // 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. - const annotatedNodes = ts.getSourceFileOfNode(ts.getParseTreeNode(sourceFile))?.emitNode?.annotatedNodes; - if (annotatedNodes) { - for (const node of annotatedNodes) { - node.emitNode = undefined; - } +/** + * Clears any `EmitNode` entries from parse-tree nodes. + * @param sourceFile A source file. + */ +export function disposeEmitNodes(sourceFile: ts.SourceFile | undefined) { + // 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. + const annotatedNodes = ts.getSourceFileOfNode(ts.getParseTreeNode(sourceFile))?.emitNode?.annotatedNodes; + if (annotatedNodes) { + for (const node of annotatedNodes) { + node.emitNode = undefined; } } +} - /** - * 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 |= ts.EmitFlags.NoComments; - emitNode.leadingComments = undefined; - emitNode.trailingComments = undefined; - return node; - } +/** + * 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 |= ts.EmitFlags.NoComments; + emitNode.leadingComments = undefined; + emitNode.trailingComments = undefined; + return node; +} - /** - * Sets flags that control emit behavior of a node. - */ - export function setEmitFlags(node: T, emitFlags: ts.EmitFlags) { - getOrCreateEmitNode(node).flags = emitFlags; - return node; - } +/** + * Sets flags that control emit behavior of a node. + */ +export function setEmitFlags(node: T, emitFlags: ts.EmitFlags) { + getOrCreateEmitNode(node).flags = emitFlags; + return node; +} - /** - * Sets flags that control emit behavior of a node. - */ - /* @internal */ - export function addEmitFlags(node: T, emitFlags: ts.EmitFlags) { - const emitNode = getOrCreateEmitNode(node); - emitNode.flags = emitNode.flags | emitFlags; - return node; - } +/** + * Sets flags that control emit behavior of a node. + */ +/* @internal */ +export function addEmitFlags(node: T, emitFlags: ts.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: ts.Node): ts.SourceMapRange { - return node.emitNode?.sourceMapRange ?? node; - } +/** + * Gets a custom text range to use when emitting source maps. + */ +export function getSourceMapRange(node: ts.Node): ts.SourceMapRange { + return node.emitNode?.sourceMapRange ?? node; +} - /** - * Sets a custom text range to use when emitting source maps. - */ - export function setSourceMapRange(node: T, range: ts.SourceMapRange | undefined) { - getOrCreateEmitNode(node).sourceMapRange = range; - return node; - } +/** + * Sets a custom text range to use when emitting source maps. + */ +export function setSourceMapRange(node: T, range: ts.SourceMapRange | undefined) { + getOrCreateEmitNode(node).sourceMapRange = range; + return node; +} - /** - * Gets the TextRange to use for source maps for a token of a node. - */ - export function getTokenSourceMapRange(node: ts.Node, token: ts.SyntaxKind): ts.SourceMapRange | undefined { - return node.emitNode?.tokenSourceMapRanges?.[token]; - } +/** + * Gets the TextRange to use for source maps for a token of a node. + */ +export function getTokenSourceMapRange(node: ts.Node, token: ts.SyntaxKind): ts.SourceMapRange | undefined { + return node.emitNode?.tokenSourceMapRanges?.[token]; +} - /** - * Sets the TextRange to use for source maps for a token of a node. - */ - export function setTokenSourceMapRange(node: T, token: ts.SyntaxKind, range: ts.SourceMapRange | undefined) { - const emitNode = getOrCreateEmitNode(node); - const tokenSourceMapRanges = emitNode.tokenSourceMapRanges ?? (emitNode.tokenSourceMapRanges = []); - tokenSourceMapRanges[token] = range; - return node; - } +/** + * Sets the TextRange to use for source maps for a token of a node. + */ +export function setTokenSourceMapRange(node: T, token: ts.SyntaxKind, range: ts.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: ts.Node) { - return node.emitNode?.startsOnNewLine; - } +/** + * Gets a custom text range to use when emitting comments. + */ +/*@internal*/ +export function getStartsOnNewLine(node: ts.Node) { + return node.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; - } +/** + * 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: ts.Node): ts.TextRange { - return node.emitNode?.commentRange ?? node; - } +/** + * Gets a custom text range to use when emitting comments. + */ +export function getCommentRange(node: ts.Node): ts.TextRange { + return node.emitNode?.commentRange ?? node; +} - /** - * Sets a custom text range to use when emitting comments. - */ - export function setCommentRange(node: T, range: ts.TextRange) { - getOrCreateEmitNode(node).commentRange = range; - return node; - } +/** + * Sets a custom text range to use when emitting comments. + */ +export function setCommentRange(node: T, range: ts.TextRange) { + getOrCreateEmitNode(node).commentRange = range; + return node; +} - export function getSyntheticLeadingComments(node: ts.Node): ts.SynthesizedComment[] | undefined { - return node.emitNode?.leadingComments; - } +export function getSyntheticLeadingComments(node: ts.Node): ts.SynthesizedComment[] | undefined { + return node.emitNode?.leadingComments; +} - export function setSyntheticLeadingComments(node: T, comments: ts.SynthesizedComment[] | undefined) { - getOrCreateEmitNode(node).leadingComments = comments; - return node; - } +export function setSyntheticLeadingComments(node: T, comments: ts.SynthesizedComment[] | undefined) { + getOrCreateEmitNode(node).leadingComments = comments; + return node; +} - export function addSyntheticLeadingComment(node: T, kind: ts.SyntaxKind.SingleLineCommentTrivia | ts.SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { - return setSyntheticLeadingComments(node, ts.append(getSyntheticLeadingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); - } +export function addSyntheticLeadingComment(node: T, kind: ts.SyntaxKind.SingleLineCommentTrivia | ts.SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { + return setSyntheticLeadingComments(node, ts.append(getSyntheticLeadingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); +} - export function getSyntheticTrailingComments(node: ts.Node): ts.SynthesizedComment[] | undefined { - return node.emitNode?.trailingComments; - } +export function getSyntheticTrailingComments(node: ts.Node): ts.SynthesizedComment[] | undefined { + return node.emitNode?.trailingComments; +} - export function setSyntheticTrailingComments(node: T, comments: ts.SynthesizedComment[] | undefined) { - getOrCreateEmitNode(node).trailingComments = comments; - return node; - } +export function setSyntheticTrailingComments(node: T, comments: ts.SynthesizedComment[] | undefined) { + getOrCreateEmitNode(node).trailingComments = comments; + return node; +} - export function addSyntheticTrailingComment(node: T, kind: ts.SyntaxKind.SingleLineCommentTrivia | ts.SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { - return setSyntheticTrailingComments(node, ts.append(getSyntheticTrailingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); - } +export function addSyntheticTrailingComment(node: T, kind: ts.SyntaxKind.SingleLineCommentTrivia | ts.SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { + return setSyntheticTrailingComments(node, ts.append(getSyntheticTrailingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); +} - export function moveSyntheticComments(node: T, original: ts.Node): T { - setSyntheticLeadingComments(node, getSyntheticLeadingComments(original)); - setSyntheticTrailingComments(node, getSyntheticTrailingComments(original)); - const emit = getOrCreateEmitNode(original); - emit.leadingComments = undefined; - emit.trailingComments = undefined; - return node; - } +export function moveSyntheticComments(node: T, original: ts.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 representing an enum. - */ - export function getConstantValue(node: ts.AccessExpression): string | number | undefined { - return node.emitNode?.constantValue; - } +/** + * Gets the constant value to emit for an expression representing an enum. + */ +export function getConstantValue(node: ts.AccessExpression): string | number | undefined { + return node.emitNode?.constantValue; +} - /** - * Sets the constant value to emit for an expression. - */ - export function setConstantValue(node: ts.AccessExpression, value: string | number): ts.AccessExpression { - const emitNode = getOrCreateEmitNode(node); - emitNode.constantValue = value; - return node; - } +/** + * Sets the constant value to emit for an expression. + */ +export function setConstantValue(node: ts.AccessExpression, value: string | number): ts.AccessExpression { + const emitNode = getOrCreateEmitNode(node); + emitNode.constantValue = value; + return node; +} - /** - * Adds an EmitHelper to a node. - */ - export function addEmitHelper(node: T, helper: ts.EmitHelper): T { - const emitNode = getOrCreateEmitNode(node); - emitNode.helpers = ts.append(emitNode.helpers, helper); - return node; - } +/** + * Adds an EmitHelper to a node. + */ +export function addEmitHelper(node: T, helper: ts.EmitHelper): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.helpers = ts.append(emitNode.helpers, helper); + return node; +} - /** - * Add EmitHelpers to a node. - */ - export function addEmitHelpers(node: T, helpers: ts.EmitHelper[] | undefined): T { - if (ts.some(helpers)) { - const emitNode = getOrCreateEmitNode(node); - for (const helper of helpers) { - emitNode.helpers = ts.appendIfUnique(emitNode.helpers, helper); - } +/** + * Add EmitHelpers to a node. + */ +export function addEmitHelpers(node: T, helpers: ts.EmitHelper[] | undefined): T { + if (ts.some(helpers)) { + const emitNode = getOrCreateEmitNode(node); + for (const helper of helpers) { + emitNode.helpers = ts.appendIfUnique(emitNode.helpers, helper); } - return node; } + return node; +} - /** - * Removes an EmitHelper from a node. - */ - export function removeEmitHelper(node: ts.Node, helper: ts.EmitHelper): boolean { - const helpers = node.emitNode?.helpers; - if (helpers) { - return ts.orderedRemoveItem(helpers, helper); - } - return false; +/** + * Removes an EmitHelper from a node. + */ +export function removeEmitHelper(node: ts.Node, helper: ts.EmitHelper): boolean { + const helpers = node.emitNode?.helpers; + if (helpers) { + return ts.orderedRemoveItem(helpers, helper); } + return false; +} - /** - * Gets the EmitHelpers of a node. - */ - export function getEmitHelpers(node: ts.Node): ts.EmitHelper[] | undefined { - return node.emitNode?.helpers; - } +/** + * Gets the EmitHelpers of a node. + */ +export function getEmitHelpers(node: ts.Node): ts.EmitHelper[] | undefined { + return node.emitNode?.helpers; +} - /** - * Moves matching emit helpers from a source node to a target node. - */ - export function moveEmitHelpers(source: ts.Node, target: ts.Node, predicate: (helper: ts.EmitHelper) => boolean) { - const sourceEmitNode = source.emitNode; - const sourceEmitHelpers = sourceEmitNode && sourceEmitNode.helpers; - if (!ts.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 = ts.appendIfUnique(targetEmitNode.helpers, helper); - } - else if (helpersRemoved > 0) { - sourceEmitHelpers[i - helpersRemoved] = helper; - } +/** + * Moves matching emit helpers from a source node to a target node. + */ +export function moveEmitHelpers(source: ts.Node, target: ts.Node, predicate: (helper: ts.EmitHelper) => boolean) { + const sourceEmitNode = source.emitNode; + const sourceEmitHelpers = sourceEmitNode && sourceEmitNode.helpers; + if (!ts.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 = ts.appendIfUnique(targetEmitNode.helpers, helper); } - - if (helpersRemoved > 0) { - sourceEmitHelpers.length -= helpersRemoved; + else if (helpersRemoved > 0) { + sourceEmitHelpers[i - helpersRemoved] = helper; } } - /** - * Gets the SnippetElement of a node. - */ - /* @internal */ - export function getSnippetElement(node: ts.Node): ts.SnippetElement | undefined { - return node.emitNode?.snippetElement; + if (helpersRemoved > 0) { + sourceEmitHelpers.length -= helpersRemoved; } +} - /** - * Sets the SnippetElement of a node. - */ - /* @internal */ - export function setSnippetElement(node: T, snippet: ts.SnippetElement): T { - const emitNode = getOrCreateEmitNode(node); - emitNode.snippetElement = snippet; - return node; - } +/** + * Gets the SnippetElement of a node. + */ +/* @internal */ +export function getSnippetElement(node: ts.Node): ts.SnippetElement | undefined { + return node.emitNode?.snippetElement; +} - /* @internal */ - export function ignoreSourceNewlines(node: T): T { - getOrCreateEmitNode(node).flags |= ts.EmitFlags.IgnoreSourceNewlines; - return node; - } +/** + * Sets the SnippetElement of a node. + */ +/* @internal */ +export function setSnippetElement(node: T, snippet: ts.SnippetElement): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.snippetElement = snippet; + return node; +} - /* @internal */ - export function setTypeNode(node: T, type: ts.TypeNode): T { - const emitNode = getOrCreateEmitNode(node); - emitNode.typeNode = type; - return node; - } +/* @internal */ +export function ignoreSourceNewlines(node: T): T { + getOrCreateEmitNode(node).flags |= ts.EmitFlags.IgnoreSourceNewlines; + return node; +} - /* @internal */ - export function getTypeNode(node: T): ts.TypeNode | undefined { - return node.emitNode?.typeNode; - } +/* @internal */ +export function setTypeNode(node: T, type: ts.TypeNode): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.typeNode = type; + return node; +} + +/* @internal */ +export function getTypeNode(node: T): ts.TypeNode | undefined { + return node.emitNode?.typeNode; +} } diff --git a/src/compiler/factory/nodeConverters.ts b/src/compiler/factory/nodeConverters.ts index 52b0facacfd13..93482b3b234a7 100644 --- a/src/compiler/factory/nodeConverters.ts +++ b/src/compiler/factory/nodeConverters.ts @@ -1,111 +1,111 @@ /* @internal */ namespace ts { - export function createNodeConverters(factory: ts.NodeFactory): ts.NodeConverters { - return { - convertToFunctionBlock, - convertToFunctionExpression, - convertToArrayAssignmentElement, - convertToObjectAssignmentElement, - convertToAssignmentPattern, - convertToObjectAssignmentPattern, - convertToArrayAssignmentPattern, - convertToAssignmentElementTarget, - }; +export function createNodeConverters(factory: ts.NodeFactory): ts.NodeConverters { + return { + convertToFunctionBlock, + convertToFunctionExpression, + convertToArrayAssignmentElement, + convertToObjectAssignmentElement, + convertToAssignmentPattern, + convertToObjectAssignmentPattern, + convertToArrayAssignmentPattern, + convertToAssignmentElementTarget, + }; - function convertToFunctionBlock(node: ts.ConciseBody, multiLine?: boolean): ts.Block { - if (ts.isBlock(node)) - return node; - const returnStatement = factory.createReturnStatement(node); - ts.setTextRange(returnStatement, node); - const body = factory.createBlock([returnStatement], multiLine); - ts.setTextRange(body, node); - return body; - } + function convertToFunctionBlock(node: ts.ConciseBody, multiLine?: boolean): ts.Block { + if (ts.isBlock(node)) + return node; + const returnStatement = factory.createReturnStatement(node); + ts.setTextRange(returnStatement, node); + const body = factory.createBlock([returnStatement], multiLine); + ts.setTextRange(body, node); + return body; + } - function convertToFunctionExpression(node: ts.FunctionDeclaration) { - if (!node.body) - return ts.Debug.fail(`Cannot convert a FunctionDeclaration without a body`); - const updated = factory.createFunctionExpression(node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body); - ts.setOriginalNode(updated, node); - ts.setTextRange(updated, node); - if (ts.getStartsOnNewLine(node)) { - ts.setStartsOnNewLine(updated, /*newLine*/ true); - } - return updated; + function convertToFunctionExpression(node: ts.FunctionDeclaration) { + if (!node.body) + return ts.Debug.fail(`Cannot convert a FunctionDeclaration without a body`); + const updated = factory.createFunctionExpression(node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body); + ts.setOriginalNode(updated, node); + ts.setTextRange(updated, node); + if (ts.getStartsOnNewLine(node)) { + ts.setStartsOnNewLine(updated, /*newLine*/ true); } + return updated; + } - function convertToArrayAssignmentElement(element: ts.ArrayBindingOrAssignmentElement) { - if (ts.isBindingElement(element)) { - if (element.dotDotDotToken) { - ts.Debug.assertNode(element.name, ts.isIdentifier); - return ts.setOriginalNode(ts.setTextRange(factory.createSpreadElement(element.name), element), element); - } - const expression = convertToAssignmentElementTarget(element.name); - return element.initializer - ? ts.setOriginalNode(ts.setTextRange(factory.createAssignment(expression, element.initializer), element), element) - : expression; + function convertToArrayAssignmentElement(element: ts.ArrayBindingOrAssignmentElement) { + if (ts.isBindingElement(element)) { + if (element.dotDotDotToken) { + ts.Debug.assertNode(element.name, ts.isIdentifier); + return ts.setOriginalNode(ts.setTextRange(factory.createSpreadElement(element.name), element), element); } - return ts.cast(element, ts.isExpression); + const expression = convertToAssignmentElementTarget(element.name); + return element.initializer + ? ts.setOriginalNode(ts.setTextRange(factory.createAssignment(expression, element.initializer), element), element) + : expression; } + return ts.cast(element, ts.isExpression); + } - function convertToObjectAssignmentElement(element: ts.ObjectBindingOrAssignmentElement) { - if (ts.isBindingElement(element)) { - if (element.dotDotDotToken) { - ts.Debug.assertNode(element.name, ts.isIdentifier); - return ts.setOriginalNode(ts.setTextRange(factory.createSpreadAssignment(element.name), element), element); - } - if (element.propertyName) { - const expression = convertToAssignmentElementTarget(element.name); - return ts.setOriginalNode(ts.setTextRange(factory.createPropertyAssignment(element.propertyName, element.initializer ? factory.createAssignment(expression, element.initializer) : expression), element), element); - } + function convertToObjectAssignmentElement(element: ts.ObjectBindingOrAssignmentElement) { + if (ts.isBindingElement(element)) { + if (element.dotDotDotToken) { ts.Debug.assertNode(element.name, ts.isIdentifier); - return ts.setOriginalNode(ts.setTextRange(factory.createShorthandPropertyAssignment(element.name, element.initializer), element), element); + return ts.setOriginalNode(ts.setTextRange(factory.createSpreadAssignment(element.name), element), element); } - - return ts.cast(element, ts.isObjectLiteralElementLike); + if (element.propertyName) { + const expression = convertToAssignmentElementTarget(element.name); + return ts.setOriginalNode(ts.setTextRange(factory.createPropertyAssignment(element.propertyName, element.initializer ? factory.createAssignment(expression, element.initializer) : expression), element), element); + } + ts.Debug.assertNode(element.name, ts.isIdentifier); + return ts.setOriginalNode(ts.setTextRange(factory.createShorthandPropertyAssignment(element.name, element.initializer), element), element); } - function convertToAssignmentPattern(node: ts.BindingOrAssignmentPattern): ts.AssignmentPattern { - switch (node.kind) { - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ArrayLiteralExpression: - return convertToArrayAssignmentPattern(node); + return ts.cast(element, ts.isObjectLiteralElementLike); + } - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ObjectLiteralExpression: - return convertToObjectAssignmentPattern(node); - } + function convertToAssignmentPattern(node: ts.BindingOrAssignmentPattern): ts.AssignmentPattern { + switch (node.kind) { + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ArrayLiteralExpression: + return convertToArrayAssignmentPattern(node); + + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ObjectLiteralExpression: + return convertToObjectAssignmentPattern(node); } + } - function convertToObjectAssignmentPattern(node: ts.ObjectBindingOrAssignmentPattern) { - if (ts.isObjectBindingPattern(node)) { - return ts.setOriginalNode(ts.setTextRange(factory.createObjectLiteralExpression(ts.map(node.elements, convertToObjectAssignmentElement)), node), node); - } - return ts.cast(node, ts.isObjectLiteralExpression); + function convertToObjectAssignmentPattern(node: ts.ObjectBindingOrAssignmentPattern) { + if (ts.isObjectBindingPattern(node)) { + return ts.setOriginalNode(ts.setTextRange(factory.createObjectLiteralExpression(ts.map(node.elements, convertToObjectAssignmentElement)), node), node); } - function convertToArrayAssignmentPattern(node: ts.ArrayBindingOrAssignmentPattern) { - if (ts.isArrayBindingPattern(node)) { - return ts.setOriginalNode(ts.setTextRange(factory.createArrayLiteralExpression(ts.map(node.elements, convertToArrayAssignmentElement)), node), node); - } - return ts.cast(node, ts.isArrayLiteralExpression); + return ts.cast(node, ts.isObjectLiteralExpression); + } + function convertToArrayAssignmentPattern(node: ts.ArrayBindingOrAssignmentPattern) { + if (ts.isArrayBindingPattern(node)) { + return ts.setOriginalNode(ts.setTextRange(factory.createArrayLiteralExpression(ts.map(node.elements, convertToArrayAssignmentElement)), node), node); } - function convertToAssignmentElementTarget(node: ts.BindingOrAssignmentElementTarget): ts.Expression { - if (ts.isBindingPattern(node)) { - return convertToAssignmentPattern(node); - } - - return ts.cast(node, ts.isExpression); + return ts.cast(node, ts.isArrayLiteralExpression); + } + function convertToAssignmentElementTarget(node: ts.BindingOrAssignmentElementTarget): ts.Expression { + if (ts.isBindingPattern(node)) { + return convertToAssignmentPattern(node); } + + return ts.cast(node, ts.isExpression); } +} - export const nullNodeConverters: ts.NodeConverters = { - convertToFunctionBlock: ts.notImplemented, - convertToFunctionExpression: ts.notImplemented, - convertToArrayAssignmentElement: ts.notImplemented, - convertToObjectAssignmentElement: ts.notImplemented, - convertToAssignmentPattern: ts.notImplemented, - convertToObjectAssignmentPattern: ts.notImplemented, - convertToArrayAssignmentPattern: ts.notImplemented, - convertToAssignmentElementTarget: ts.notImplemented, - }; -} \ No newline at end of file +export const nullNodeConverters: ts.NodeConverters = { + convertToFunctionBlock: ts.notImplemented, + convertToFunctionExpression: ts.notImplemented, + convertToArrayAssignmentElement: ts.notImplemented, + convertToObjectAssignmentElement: ts.notImplemented, + convertToAssignmentPattern: ts.notImplemented, + convertToObjectAssignmentPattern: ts.notImplemented, + convertToArrayAssignmentPattern: ts.notImplemented, + convertToAssignmentElementTarget: ts.notImplemented, +}; +} diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index 5814cdb1d3f7d..b54283932415f 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -1,6060 +1,6060 @@ namespace ts { - let nextAutoGenerateId = 0; +let nextAutoGenerateId = 0; + +/* @internal */ +export const enum NodeFactoryFlags { + None = 0, + // Disables the parenthesizer rules for the factory. + NoParenthesizerRules = 1 << 0, + // Disables the node converters for the factory. + NoNodeConverters = 1 << 1, + // Ensures new `PropertyAccessExpression` nodes are created with the `NoIndentation` emit flag set. + NoIndentationOnFreshPropertyAccess = 1 << 2, + // Do not set an `original` pointer when updating a node. + NoOriginalNode = 1 << 3 +} - /* @internal */ - export const enum NodeFactoryFlags { - None = 0, - // Disables the parenthesizer rules for the factory. - NoParenthesizerRules = 1 << 0, - // Disables the node converters for the factory. - NoNodeConverters = 1 << 1, - // Ensures new `PropertyAccessExpression` nodes are created with the `NoIndentation` emit flag set. - NoIndentationOnFreshPropertyAccess = 1 << 2, - // Do not set an `original` pointer when updating a node. - NoOriginalNode = 1 << 3 - } +/** + * Creates a `NodeFactory` that can be used to create and update a syntax tree. + * @param flags Flags that control factory behavior. + * @param baseFactory A `BaseNodeFactory` used to create the base `Node` objects. + */ +/* @internal */ +export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: ts.BaseNodeFactory): ts.NodeFactory { + const update = flags & NodeFactoryFlags.NoOriginalNode ? updateWithoutOriginal : updateWithOriginal; + + // Lazily load the parenthesizer, node converters, and some factory methods until they are used. + const parenthesizerRules = ts.memoize(() => flags & NodeFactoryFlags.NoParenthesizerRules ? ts.nullParenthesizerRules : ts.createParenthesizerRules(factory)); + const converters = ts.memoize(() => flags & NodeFactoryFlags.NoNodeConverters ? ts.nullNodeConverters : ts.createNodeConverters(factory)); + + // lazy initializaton of common operator factories + const getBinaryCreateFunction = ts.memoizeOne((operator: ts.BinaryOperator) => (left: ts.Expression, right: ts.Expression) => createBinaryExpression(left, operator, right)); + const getPrefixUnaryCreateFunction = ts.memoizeOne((operator: ts.PrefixUnaryOperator) => (operand: ts.Expression) => createPrefixUnaryExpression(operator, operand)); + const getPostfixUnaryCreateFunction = ts.memoizeOne((operator: ts.PostfixUnaryOperator) => (operand: ts.Expression) => createPostfixUnaryExpression(operand, operator)); + const getJSDocPrimaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => () => createJSDocPrimaryTypeWorker(kind)); + const getJSDocUnaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => (type: T["type"]) => createJSDocUnaryTypeWorker(kind, type)); + const getJSDocUnaryTypeUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, type: T["type"]) => updateJSDocUnaryTypeWorker(kind, node, type)); + const getJSDocPrePostfixUnaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => (type: T["type"], postfix?: boolean) => createJSDocPrePostfixUnaryTypeWorker(kind, type, postfix)); + const getJSDocPrePostfixUnaryTypeUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, type: T["type"]) => updateJSDocPrePostfixUnaryTypeWorker(kind, node, type)); + const getJSDocSimpleTagCreateFunction = ts.memoizeOne((kind: T["kind"]) => (tagName: ts.Identifier | undefined, comment?: ts.NodeArray) => createJSDocSimpleTagWorker(kind, tagName, comment)); + const getJSDocSimpleTagUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, tagName: ts.Identifier | undefined, comment?: ts.NodeArray) => updateJSDocSimpleTagWorker(kind, node, tagName, comment)); + const getJSDocTypeLikeTagCreateFunction = ts.memoizeOne((kind: T["kind"]) => (tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: ts.NodeArray) => createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment)); + const getJSDocTypeLikeTagUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: ts.NodeArray) => updateJSDocTypeLikeTagWorker(kind, node, tagName, typeExpression, comment)); + const factory: ts.NodeFactory = { + get parenthesizer() { return parenthesizerRules(); }, + get converters() { return converters(); }, + baseFactory, + flags, + createNodeArray, + createNumericLiteral, + createBigIntLiteral, + createStringLiteral, + createStringLiteralFromNode, + createRegularExpressionLiteral, + createLiteralLikeNode, + createIdentifier, + updateIdentifier, + createTempVariable, + createLoopVariable, + createUniqueName, + getGeneratedNameForNode, + createPrivateIdentifier, + createToken, + createSuper, + createThis, + createNull, + createTrue, + createFalse, + createModifier, + createModifiersFromModifierFlags, + createQualifiedName, + updateQualifiedName, + createComputedPropertyName, + updateComputedPropertyName, + createTypeParameterDeclaration, + updateTypeParameterDeclaration, + createParameterDeclaration, + updateParameterDeclaration, + createDecorator, + updateDecorator, + createPropertySignature, + updatePropertySignature, + createPropertyDeclaration, + updatePropertyDeclaration, + createMethodSignature, + updateMethodSignature, + createMethodDeclaration, + updateMethodDeclaration, + createConstructorDeclaration, + updateConstructorDeclaration, + createGetAccessorDeclaration, + updateGetAccessorDeclaration, + createSetAccessorDeclaration, + updateSetAccessorDeclaration, + createCallSignature, + updateCallSignature, + createConstructSignature, + updateConstructSignature, + createIndexSignature, + updateIndexSignature, + createClassStaticBlockDeclaration, + updateClassStaticBlockDeclaration, + createTemplateLiteralTypeSpan, + updateTemplateLiteralTypeSpan, + createKeywordTypeNode, + createTypePredicateNode, + updateTypePredicateNode, + createTypeReferenceNode, + updateTypeReferenceNode, + createFunctionTypeNode, + updateFunctionTypeNode, + createConstructorTypeNode, + updateConstructorTypeNode, + createTypeQueryNode, + updateTypeQueryNode, + createTypeLiteralNode, + updateTypeLiteralNode, + createArrayTypeNode, + updateArrayTypeNode, + createTupleTypeNode, + updateTupleTypeNode, + createNamedTupleMember, + updateNamedTupleMember, + createOptionalTypeNode, + updateOptionalTypeNode, + createRestTypeNode, + updateRestTypeNode, + createUnionTypeNode, + updateUnionTypeNode, + createIntersectionTypeNode, + updateIntersectionTypeNode, + createConditionalTypeNode, + updateConditionalTypeNode, + createInferTypeNode, + updateInferTypeNode, + createImportTypeNode, + updateImportTypeNode, + createParenthesizedType, + updateParenthesizedType, + createThisTypeNode, + createTypeOperatorNode, + updateTypeOperatorNode, + createIndexedAccessTypeNode, + updateIndexedAccessTypeNode, + createMappedTypeNode, + updateMappedTypeNode, + createLiteralTypeNode, + updateLiteralTypeNode, + createTemplateLiteralType, + updateTemplateLiteralType, + createObjectBindingPattern, + updateObjectBindingPattern, + createArrayBindingPattern, + updateArrayBindingPattern, + createBindingElement, + updateBindingElement, + createArrayLiteralExpression, + updateArrayLiteralExpression, + createObjectLiteralExpression, + updateObjectLiteralExpression, + createPropertyAccessExpression: flags & NodeFactoryFlags.NoIndentationOnFreshPropertyAccess ? + (expression, name) => ts.setEmitFlags(createPropertyAccessExpression(expression, name), ts.EmitFlags.NoIndentation) : + createPropertyAccessExpression, + updatePropertyAccessExpression, + createPropertyAccessChain: flags & NodeFactoryFlags.NoIndentationOnFreshPropertyAccess ? + (expression, questionDotToken, name) => ts.setEmitFlags(createPropertyAccessChain(expression, questionDotToken, name), ts.EmitFlags.NoIndentation) : + createPropertyAccessChain, + updatePropertyAccessChain, + createElementAccessExpression, + updateElementAccessExpression, + createElementAccessChain, + updateElementAccessChain, + createCallExpression, + updateCallExpression, + createCallChain, + updateCallChain, + createNewExpression, + updateNewExpression, + createTaggedTemplateExpression, + updateTaggedTemplateExpression, + createTypeAssertion, + updateTypeAssertion, + createParenthesizedExpression, + updateParenthesizedExpression, + createFunctionExpression, + updateFunctionExpression, + createArrowFunction, + updateArrowFunction, + createDeleteExpression, + updateDeleteExpression, + createTypeOfExpression, + updateTypeOfExpression, + createVoidExpression, + updateVoidExpression, + createAwaitExpression, + updateAwaitExpression, + createPrefixUnaryExpression, + updatePrefixUnaryExpression, + createPostfixUnaryExpression, + updatePostfixUnaryExpression, + createBinaryExpression, + updateBinaryExpression, + createConditionalExpression, + updateConditionalExpression, + createTemplateExpression, + updateTemplateExpression, + createTemplateHead, + createTemplateMiddle, + createTemplateTail, + createNoSubstitutionTemplateLiteral, + createTemplateLiteralLikeNode, + createYieldExpression, + updateYieldExpression, + createSpreadElement, + updateSpreadElement, + createClassExpression, + updateClassExpression, + createOmittedExpression, + createExpressionWithTypeArguments, + updateExpressionWithTypeArguments, + createAsExpression, + updateAsExpression, + createNonNullExpression, + updateNonNullExpression, + createNonNullChain, + updateNonNullChain, + createMetaProperty, + updateMetaProperty, + createTemplateSpan, + updateTemplateSpan, + createSemicolonClassElement, + createBlock, + updateBlock, + createVariableStatement, + updateVariableStatement, + createEmptyStatement, + createExpressionStatement, + updateExpressionStatement, + createIfStatement, + updateIfStatement, + createDoStatement, + updateDoStatement, + createWhileStatement, + updateWhileStatement, + createForStatement, + updateForStatement, + createForInStatement, + updateForInStatement, + createForOfStatement, + updateForOfStatement, + createContinueStatement, + updateContinueStatement, + createBreakStatement, + updateBreakStatement, + createReturnStatement, + updateReturnStatement, + createWithStatement, + updateWithStatement, + createSwitchStatement, + updateSwitchStatement, + createLabeledStatement, + updateLabeledStatement, + createThrowStatement, + updateThrowStatement, + createTryStatement, + updateTryStatement, + createDebuggerStatement, + createVariableDeclaration, + updateVariableDeclaration, + createVariableDeclarationList, + updateVariableDeclarationList, + createFunctionDeclaration, + updateFunctionDeclaration, + createClassDeclaration, + updateClassDeclaration, + createInterfaceDeclaration, + updateInterfaceDeclaration, + createTypeAliasDeclaration, + updateTypeAliasDeclaration, + createEnumDeclaration, + updateEnumDeclaration, + createModuleDeclaration, + updateModuleDeclaration, + createModuleBlock, + updateModuleBlock, + createCaseBlock, + updateCaseBlock, + createNamespaceExportDeclaration, + updateNamespaceExportDeclaration, + createImportEqualsDeclaration, + updateImportEqualsDeclaration, + createImportDeclaration, + updateImportDeclaration, + createImportClause, + updateImportClause, + createAssertClause, + updateAssertClause, + createAssertEntry, + updateAssertEntry, + createImportTypeAssertionContainer, + updateImportTypeAssertionContainer, + createNamespaceImport, + updateNamespaceImport, + createNamespaceExport, + updateNamespaceExport, + createNamedImports, + updateNamedImports, + createImportSpecifier, + updateImportSpecifier, + createExportAssignment, + updateExportAssignment, + createExportDeclaration, + updateExportDeclaration, + createNamedExports, + updateNamedExports, + createExportSpecifier, + updateExportSpecifier, + createMissingDeclaration, + createExternalModuleReference, + updateExternalModuleReference, + // lazily load factory members for JSDoc types with similar structure + get createJSDocAllType() { return getJSDocPrimaryTypeCreateFunction(ts.SyntaxKind.JSDocAllType); }, + get createJSDocUnknownType() { return getJSDocPrimaryTypeCreateFunction(ts.SyntaxKind.JSDocUnknownType); }, + get createJSDocNonNullableType() { return getJSDocPrePostfixUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNonNullableType); }, + get updateJSDocNonNullableType() { return getJSDocPrePostfixUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNonNullableType); }, + get createJSDocNullableType() { return getJSDocPrePostfixUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNullableType); }, + get updateJSDocNullableType() { return getJSDocPrePostfixUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNullableType); }, + get createJSDocOptionalType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocOptionalType); }, + get updateJSDocOptionalType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocOptionalType); }, + get createJSDocVariadicType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocVariadicType); }, + get updateJSDocVariadicType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocVariadicType); }, + get createJSDocNamepathType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNamepathType); }, + get updateJSDocNamepathType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNamepathType); }, + createJSDocFunctionType, + updateJSDocFunctionType, + createJSDocTypeLiteral, + updateJSDocTypeLiteral, + createJSDocTypeExpression, + updateJSDocTypeExpression, + createJSDocSignature, + updateJSDocSignature, + createJSDocTemplateTag, + updateJSDocTemplateTag, + createJSDocTypedefTag, + updateJSDocTypedefTag, + createJSDocParameterTag, + updateJSDocParameterTag, + createJSDocPropertyTag, + updateJSDocPropertyTag, + createJSDocCallbackTag, + updateJSDocCallbackTag, + createJSDocAugmentsTag, + updateJSDocAugmentsTag, + createJSDocImplementsTag, + updateJSDocImplementsTag, + createJSDocSeeTag, + updateJSDocSeeTag, + createJSDocNameReference, + updateJSDocNameReference, + createJSDocMemberName, + updateJSDocMemberName, + createJSDocLink, + updateJSDocLink, + createJSDocLinkCode, + updateJSDocLinkCode, + createJSDocLinkPlain, + updateJSDocLinkPlain, + // lazily load factory members for JSDoc tags with similar structure + get createJSDocTypeTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocTypeTag); }, + get updateJSDocTypeTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocTypeTag); }, + get createJSDocReturnTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocReturnTag); }, + get updateJSDocReturnTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocReturnTag); }, + get createJSDocThisTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocThisTag); }, + get updateJSDocThisTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocThisTag); }, + get createJSDocEnumTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocEnumTag); }, + get updateJSDocEnumTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocEnumTag); }, + get createJSDocAuthorTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocAuthorTag); }, + get updateJSDocAuthorTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocAuthorTag); }, + get createJSDocClassTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocClassTag); }, + get updateJSDocClassTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocClassTag); }, + get createJSDocPublicTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocPublicTag); }, + get updateJSDocPublicTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocPublicTag); }, + get createJSDocPrivateTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocPrivateTag); }, + get updateJSDocPrivateTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocPrivateTag); }, + get createJSDocProtectedTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocProtectedTag); }, + get updateJSDocProtectedTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocProtectedTag); }, + get createJSDocReadonlyTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocReadonlyTag); }, + get updateJSDocReadonlyTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocReadonlyTag); }, + get createJSDocOverrideTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocOverrideTag); }, + get updateJSDocOverrideTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocOverrideTag); }, + get createJSDocDeprecatedTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocDeprecatedTag); }, + get updateJSDocDeprecatedTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocDeprecatedTag); }, + createJSDocUnknownTag, + updateJSDocUnknownTag, + createJSDocText, + updateJSDocText, + createJSDocComment, + updateJSDocComment, + createJsxElement, + updateJsxElement, + createJsxSelfClosingElement, + updateJsxSelfClosingElement, + createJsxOpeningElement, + updateJsxOpeningElement, + createJsxClosingElement, + updateJsxClosingElement, + createJsxFragment, + createJsxText, + updateJsxText, + createJsxOpeningFragment, + createJsxJsxClosingFragment, + updateJsxFragment, + createJsxAttribute, + updateJsxAttribute, + createJsxAttributes, + updateJsxAttributes, + createJsxSpreadAttribute, + updateJsxSpreadAttribute, + createJsxExpression, + updateJsxExpression, + createCaseClause, + updateCaseClause, + createDefaultClause, + updateDefaultClause, + createHeritageClause, + updateHeritageClause, + createCatchClause, + updateCatchClause, + createPropertyAssignment, + updatePropertyAssignment, + createShorthandPropertyAssignment, + updateShorthandPropertyAssignment, + createSpreadAssignment, + updateSpreadAssignment, + createEnumMember, + updateEnumMember, + createSourceFile, + updateSourceFile, + createBundle, + updateBundle, + createUnparsedSource, + createUnparsedPrologue, + createUnparsedPrepend, + createUnparsedTextLike, + createUnparsedSyntheticReference, + createInputFiles, + createSyntheticExpression, + createSyntaxList, + createNotEmittedStatement, + createPartiallyEmittedExpression, + updatePartiallyEmittedExpression, + createCommaListExpression, + updateCommaListExpression, + createEndOfDeclarationMarker, + createMergeDeclarationMarker, + createSyntheticReferenceExpression, + updateSyntheticReferenceExpression, + cloneNode, + + // Lazily load factory methods for common operator factories and utilities + get createComma() { return getBinaryCreateFunction(ts.SyntaxKind.CommaToken); }, + get createAssignment() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsToken) as ts.NodeFactory["createAssignment"]; }, + get createLogicalOr() { return getBinaryCreateFunction(ts.SyntaxKind.BarBarToken); }, + get createLogicalAnd() { return getBinaryCreateFunction(ts.SyntaxKind.AmpersandAmpersandToken); }, + get createBitwiseOr() { return getBinaryCreateFunction(ts.SyntaxKind.BarToken); }, + get createBitwiseXor() { return getBinaryCreateFunction(ts.SyntaxKind.CaretToken); }, + get createBitwiseAnd() { return getBinaryCreateFunction(ts.SyntaxKind.AmpersandToken); }, + get createStrictEquality() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsEqualsEqualsToken); }, + get createStrictInequality() { return getBinaryCreateFunction(ts.SyntaxKind.ExclamationEqualsEqualsToken); }, + get createEquality() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsEqualsToken); }, + get createInequality() { return getBinaryCreateFunction(ts.SyntaxKind.ExclamationEqualsToken); }, + get createLessThan() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanToken); }, + get createLessThanEquals() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanEqualsToken); }, + get createGreaterThan() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanToken); }, + get createGreaterThanEquals() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanEqualsToken); }, + get createLeftShift() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanLessThanToken); }, + get createRightShift() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanGreaterThanToken); }, + get createUnsignedRightShift() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken); }, + get createAdd() { return getBinaryCreateFunction(ts.SyntaxKind.PlusToken); }, + get createSubtract() { return getBinaryCreateFunction(ts.SyntaxKind.MinusToken); }, + get createMultiply() { return getBinaryCreateFunction(ts.SyntaxKind.AsteriskToken); }, + get createDivide() { return getBinaryCreateFunction(ts.SyntaxKind.SlashToken); }, + get createModulo() { return getBinaryCreateFunction(ts.SyntaxKind.PercentToken); }, + get createExponent() { return getBinaryCreateFunction(ts.SyntaxKind.AsteriskAsteriskToken); }, + get createPrefixPlus() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.PlusToken); }, + get createPrefixMinus() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.MinusToken); }, + get createPrefixIncrement() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.PlusPlusToken); }, + get createPrefixDecrement() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.MinusMinusToken); }, + get createBitwiseNot() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.TildeToken); }, + get createLogicalNot() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.ExclamationToken); }, + get createPostfixIncrement() { return getPostfixUnaryCreateFunction(ts.SyntaxKind.PlusPlusToken); }, + get createPostfixDecrement() { return getPostfixUnaryCreateFunction(ts.SyntaxKind.MinusMinusToken); }, + + // Compound nodes + createImmediatelyInvokedFunctionExpression, + createImmediatelyInvokedArrowFunction, + createVoidZero, + createExportDefault, + createExternalModuleExport, + createTypeCheck, + createMethodCall, + createGlobalMethodCall, + createFunctionBindCall, + createFunctionCallCall, + createFunctionApplyCall, + createArraySliceCall, + createArrayConcatCall, + createObjectDefinePropertyCall, + createReflectGetCall, + createReflectSetCall, + createPropertyDescriptor, + createCallBinding, + createAssignmentTargetWrapper, - /** - * Creates a `NodeFactory` that can be used to create and update a syntax tree. - * @param flags Flags that control factory behavior. - * @param baseFactory A `BaseNodeFactory` used to create the base `Node` objects. - */ - /* @internal */ - export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: ts.BaseNodeFactory): ts.NodeFactory { - const update = flags & NodeFactoryFlags.NoOriginalNode ? updateWithoutOriginal : updateWithOriginal; - - // Lazily load the parenthesizer, node converters, and some factory methods until they are used. - const parenthesizerRules = ts.memoize(() => flags & NodeFactoryFlags.NoParenthesizerRules ? ts.nullParenthesizerRules : ts.createParenthesizerRules(factory)); - const converters = ts.memoize(() => flags & NodeFactoryFlags.NoNodeConverters ? ts.nullNodeConverters : ts.createNodeConverters(factory)); - - // lazy initializaton of common operator factories - const getBinaryCreateFunction = ts.memoizeOne((operator: ts.BinaryOperator) => (left: ts.Expression, right: ts.Expression) => createBinaryExpression(left, operator, right)); - const getPrefixUnaryCreateFunction = ts.memoizeOne((operator: ts.PrefixUnaryOperator) => (operand: ts.Expression) => createPrefixUnaryExpression(operator, operand)); - const getPostfixUnaryCreateFunction = ts.memoizeOne((operator: ts.PostfixUnaryOperator) => (operand: ts.Expression) => createPostfixUnaryExpression(operand, operator)); - const getJSDocPrimaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => () => createJSDocPrimaryTypeWorker(kind)); - const getJSDocUnaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => (type: T["type"]) => createJSDocUnaryTypeWorker(kind, type)); - const getJSDocUnaryTypeUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, type: T["type"]) => updateJSDocUnaryTypeWorker(kind, node, type)); - const getJSDocPrePostfixUnaryTypeCreateFunction = ts.memoizeOne((kind: T["kind"]) => (type: T["type"], postfix?: boolean) => createJSDocPrePostfixUnaryTypeWorker(kind, type, postfix)); - const getJSDocPrePostfixUnaryTypeUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, type: T["type"]) => updateJSDocPrePostfixUnaryTypeWorker(kind, node, type)); - const getJSDocSimpleTagCreateFunction = ts.memoizeOne((kind: T["kind"]) => (tagName: ts.Identifier | undefined, comment?: ts.NodeArray) => createJSDocSimpleTagWorker(kind, tagName, comment)); - const getJSDocSimpleTagUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, tagName: ts.Identifier | undefined, comment?: ts.NodeArray) => updateJSDocSimpleTagWorker(kind, node, tagName, comment)); - const getJSDocTypeLikeTagCreateFunction = ts.memoizeOne((kind: T["kind"]) => (tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: ts.NodeArray) => createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment)); - const getJSDocTypeLikeTagUpdateFunction = ts.memoizeOne((kind: T["kind"]) => (node: T, tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: ts.NodeArray) => updateJSDocTypeLikeTagWorker(kind, node, tagName, typeExpression, comment)); - const factory: ts.NodeFactory = { - get parenthesizer() { return parenthesizerRules(); }, - get converters() { return converters(); }, - baseFactory, - flags, - createNodeArray, - createNumericLiteral, - createBigIntLiteral, - createStringLiteral, - createStringLiteralFromNode, - createRegularExpressionLiteral, - createLiteralLikeNode, - createIdentifier, - updateIdentifier, - createTempVariable, - createLoopVariable, - createUniqueName, - getGeneratedNameForNode, - createPrivateIdentifier, - createToken, - createSuper, - createThis, - createNull, - createTrue, - createFalse, - createModifier, - createModifiersFromModifierFlags, - createQualifiedName, - updateQualifiedName, - createComputedPropertyName, - updateComputedPropertyName, - createTypeParameterDeclaration, - updateTypeParameterDeclaration, - createParameterDeclaration, - updateParameterDeclaration, - createDecorator, - updateDecorator, - createPropertySignature, - updatePropertySignature, - createPropertyDeclaration, - updatePropertyDeclaration, - createMethodSignature, - updateMethodSignature, - createMethodDeclaration, - updateMethodDeclaration, - createConstructorDeclaration, - updateConstructorDeclaration, - createGetAccessorDeclaration, - updateGetAccessorDeclaration, - createSetAccessorDeclaration, - updateSetAccessorDeclaration, - createCallSignature, - updateCallSignature, - createConstructSignature, - updateConstructSignature, - createIndexSignature, - updateIndexSignature, - createClassStaticBlockDeclaration, - updateClassStaticBlockDeclaration, - createTemplateLiteralTypeSpan, - updateTemplateLiteralTypeSpan, - createKeywordTypeNode, - createTypePredicateNode, - updateTypePredicateNode, - createTypeReferenceNode, - updateTypeReferenceNode, - createFunctionTypeNode, - updateFunctionTypeNode, - createConstructorTypeNode, - updateConstructorTypeNode, - createTypeQueryNode, - updateTypeQueryNode, - createTypeLiteralNode, - updateTypeLiteralNode, - createArrayTypeNode, - updateArrayTypeNode, - createTupleTypeNode, - updateTupleTypeNode, - createNamedTupleMember, - updateNamedTupleMember, - createOptionalTypeNode, - updateOptionalTypeNode, - createRestTypeNode, - updateRestTypeNode, - createUnionTypeNode, - updateUnionTypeNode, - createIntersectionTypeNode, - updateIntersectionTypeNode, - createConditionalTypeNode, - updateConditionalTypeNode, - createInferTypeNode, - updateInferTypeNode, - createImportTypeNode, - updateImportTypeNode, - createParenthesizedType, - updateParenthesizedType, - createThisTypeNode, - createTypeOperatorNode, - updateTypeOperatorNode, - createIndexedAccessTypeNode, - updateIndexedAccessTypeNode, - createMappedTypeNode, - updateMappedTypeNode, - createLiteralTypeNode, - updateLiteralTypeNode, - createTemplateLiteralType, - updateTemplateLiteralType, - createObjectBindingPattern, - updateObjectBindingPattern, - createArrayBindingPattern, - updateArrayBindingPattern, - createBindingElement, - updateBindingElement, - createArrayLiteralExpression, - updateArrayLiteralExpression, - createObjectLiteralExpression, - updateObjectLiteralExpression, - createPropertyAccessExpression: flags & NodeFactoryFlags.NoIndentationOnFreshPropertyAccess ? - (expression, name) => ts.setEmitFlags(createPropertyAccessExpression(expression, name), ts.EmitFlags.NoIndentation) : - createPropertyAccessExpression, - updatePropertyAccessExpression, - createPropertyAccessChain: flags & NodeFactoryFlags.NoIndentationOnFreshPropertyAccess ? - (expression, questionDotToken, name) => ts.setEmitFlags(createPropertyAccessChain(expression, questionDotToken, name), ts.EmitFlags.NoIndentation) : - createPropertyAccessChain, - updatePropertyAccessChain, - createElementAccessExpression, - updateElementAccessExpression, - createElementAccessChain, - updateElementAccessChain, - createCallExpression, - updateCallExpression, - createCallChain, - updateCallChain, - createNewExpression, - updateNewExpression, - createTaggedTemplateExpression, - updateTaggedTemplateExpression, - createTypeAssertion, - updateTypeAssertion, - createParenthesizedExpression, - updateParenthesizedExpression, - createFunctionExpression, - updateFunctionExpression, - createArrowFunction, - updateArrowFunction, - createDeleteExpression, - updateDeleteExpression, - createTypeOfExpression, - updateTypeOfExpression, - createVoidExpression, - updateVoidExpression, - createAwaitExpression, - updateAwaitExpression, - createPrefixUnaryExpression, - updatePrefixUnaryExpression, - createPostfixUnaryExpression, - updatePostfixUnaryExpression, - createBinaryExpression, - updateBinaryExpression, - createConditionalExpression, - updateConditionalExpression, - createTemplateExpression, - updateTemplateExpression, - createTemplateHead, - createTemplateMiddle, - createTemplateTail, - createNoSubstitutionTemplateLiteral, - createTemplateLiteralLikeNode, - createYieldExpression, - updateYieldExpression, - createSpreadElement, - updateSpreadElement, - createClassExpression, - updateClassExpression, - createOmittedExpression, - createExpressionWithTypeArguments, - updateExpressionWithTypeArguments, - createAsExpression, - updateAsExpression, - createNonNullExpression, - updateNonNullExpression, - createNonNullChain, - updateNonNullChain, - createMetaProperty, - updateMetaProperty, - createTemplateSpan, - updateTemplateSpan, - createSemicolonClassElement, - createBlock, - updateBlock, - createVariableStatement, - updateVariableStatement, - createEmptyStatement, - createExpressionStatement, - updateExpressionStatement, - createIfStatement, - updateIfStatement, - createDoStatement, - updateDoStatement, - createWhileStatement, - updateWhileStatement, - createForStatement, - updateForStatement, - createForInStatement, - updateForInStatement, - createForOfStatement, - updateForOfStatement, - createContinueStatement, - updateContinueStatement, - createBreakStatement, - updateBreakStatement, - createReturnStatement, - updateReturnStatement, - createWithStatement, - updateWithStatement, - createSwitchStatement, - updateSwitchStatement, - createLabeledStatement, - updateLabeledStatement, - createThrowStatement, - updateThrowStatement, - createTryStatement, - updateTryStatement, - createDebuggerStatement, - createVariableDeclaration, - updateVariableDeclaration, - createVariableDeclarationList, - updateVariableDeclarationList, - createFunctionDeclaration, - updateFunctionDeclaration, - createClassDeclaration, - updateClassDeclaration, - createInterfaceDeclaration, - updateInterfaceDeclaration, - createTypeAliasDeclaration, - updateTypeAliasDeclaration, - createEnumDeclaration, - updateEnumDeclaration, - createModuleDeclaration, - updateModuleDeclaration, - createModuleBlock, - updateModuleBlock, - createCaseBlock, - updateCaseBlock, - createNamespaceExportDeclaration, - updateNamespaceExportDeclaration, - createImportEqualsDeclaration, - updateImportEqualsDeclaration, - createImportDeclaration, - updateImportDeclaration, - createImportClause, - updateImportClause, - createAssertClause, - updateAssertClause, - createAssertEntry, - updateAssertEntry, - createImportTypeAssertionContainer, - updateImportTypeAssertionContainer, - createNamespaceImport, - updateNamespaceImport, - createNamespaceExport, - updateNamespaceExport, - createNamedImports, - updateNamedImports, - createImportSpecifier, - updateImportSpecifier, - createExportAssignment, - updateExportAssignment, - createExportDeclaration, - updateExportDeclaration, - createNamedExports, - updateNamedExports, - createExportSpecifier, - updateExportSpecifier, - createMissingDeclaration, - createExternalModuleReference, - updateExternalModuleReference, - // lazily load factory members for JSDoc types with similar structure - get createJSDocAllType() { return getJSDocPrimaryTypeCreateFunction(ts.SyntaxKind.JSDocAllType); }, - get createJSDocUnknownType() { return getJSDocPrimaryTypeCreateFunction(ts.SyntaxKind.JSDocUnknownType); }, - get createJSDocNonNullableType() { return getJSDocPrePostfixUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNonNullableType); }, - get updateJSDocNonNullableType() { return getJSDocPrePostfixUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNonNullableType); }, - get createJSDocNullableType() { return getJSDocPrePostfixUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNullableType); }, - get updateJSDocNullableType() { return getJSDocPrePostfixUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNullableType); }, - get createJSDocOptionalType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocOptionalType); }, - get updateJSDocOptionalType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocOptionalType); }, - get createJSDocVariadicType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocVariadicType); }, - get updateJSDocVariadicType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocVariadicType); }, - get createJSDocNamepathType() { return getJSDocUnaryTypeCreateFunction(ts.SyntaxKind.JSDocNamepathType); }, - get updateJSDocNamepathType() { return getJSDocUnaryTypeUpdateFunction(ts.SyntaxKind.JSDocNamepathType); }, - createJSDocFunctionType, - updateJSDocFunctionType, - createJSDocTypeLiteral, - updateJSDocTypeLiteral, - createJSDocTypeExpression, - updateJSDocTypeExpression, - createJSDocSignature, - updateJSDocSignature, - createJSDocTemplateTag, - updateJSDocTemplateTag, - createJSDocTypedefTag, - updateJSDocTypedefTag, - createJSDocParameterTag, - updateJSDocParameterTag, - createJSDocPropertyTag, - updateJSDocPropertyTag, - createJSDocCallbackTag, - updateJSDocCallbackTag, - createJSDocAugmentsTag, - updateJSDocAugmentsTag, - createJSDocImplementsTag, - updateJSDocImplementsTag, - createJSDocSeeTag, - updateJSDocSeeTag, - createJSDocNameReference, - updateJSDocNameReference, - createJSDocMemberName, - updateJSDocMemberName, - createJSDocLink, - updateJSDocLink, - createJSDocLinkCode, - updateJSDocLinkCode, - createJSDocLinkPlain, - updateJSDocLinkPlain, - // lazily load factory members for JSDoc tags with similar structure - get createJSDocTypeTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocTypeTag); }, - get updateJSDocTypeTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocTypeTag); }, - get createJSDocReturnTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocReturnTag); }, - get updateJSDocReturnTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocReturnTag); }, - get createJSDocThisTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocThisTag); }, - get updateJSDocThisTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocThisTag); }, - get createJSDocEnumTag() { return getJSDocTypeLikeTagCreateFunction(ts.SyntaxKind.JSDocEnumTag); }, - get updateJSDocEnumTag() { return getJSDocTypeLikeTagUpdateFunction(ts.SyntaxKind.JSDocEnumTag); }, - get createJSDocAuthorTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocAuthorTag); }, - get updateJSDocAuthorTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocAuthorTag); }, - get createJSDocClassTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocClassTag); }, - get updateJSDocClassTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocClassTag); }, - get createJSDocPublicTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocPublicTag); }, - get updateJSDocPublicTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocPublicTag); }, - get createJSDocPrivateTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocPrivateTag); }, - get updateJSDocPrivateTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocPrivateTag); }, - get createJSDocProtectedTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocProtectedTag); }, - get updateJSDocProtectedTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocProtectedTag); }, - get createJSDocReadonlyTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocReadonlyTag); }, - get updateJSDocReadonlyTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocReadonlyTag); }, - get createJSDocOverrideTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocOverrideTag); }, - get updateJSDocOverrideTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocOverrideTag); }, - get createJSDocDeprecatedTag() { return getJSDocSimpleTagCreateFunction(ts.SyntaxKind.JSDocDeprecatedTag); }, - get updateJSDocDeprecatedTag() { return getJSDocSimpleTagUpdateFunction(ts.SyntaxKind.JSDocDeprecatedTag); }, - createJSDocUnknownTag, - updateJSDocUnknownTag, - createJSDocText, - updateJSDocText, - createJSDocComment, - updateJSDocComment, - createJsxElement, - updateJsxElement, - createJsxSelfClosingElement, - updateJsxSelfClosingElement, - createJsxOpeningElement, - updateJsxOpeningElement, - createJsxClosingElement, - updateJsxClosingElement, - createJsxFragment, - createJsxText, - updateJsxText, - createJsxOpeningFragment, - createJsxJsxClosingFragment, - updateJsxFragment, - createJsxAttribute, - updateJsxAttribute, - createJsxAttributes, - updateJsxAttributes, - createJsxSpreadAttribute, - updateJsxSpreadAttribute, - createJsxExpression, - updateJsxExpression, - createCaseClause, - updateCaseClause, - createDefaultClause, - updateDefaultClause, - createHeritageClause, - updateHeritageClause, - createCatchClause, - updateCatchClause, - createPropertyAssignment, - updatePropertyAssignment, - createShorthandPropertyAssignment, - updateShorthandPropertyAssignment, - createSpreadAssignment, - updateSpreadAssignment, - createEnumMember, - updateEnumMember, - createSourceFile, - updateSourceFile, - createBundle, - updateBundle, - createUnparsedSource, - createUnparsedPrologue, - createUnparsedPrepend, - createUnparsedTextLike, - createUnparsedSyntheticReference, - createInputFiles, - createSyntheticExpression, - createSyntaxList, - createNotEmittedStatement, - createPartiallyEmittedExpression, - updatePartiallyEmittedExpression, - createCommaListExpression, - updateCommaListExpression, - createEndOfDeclarationMarker, - createMergeDeclarationMarker, - createSyntheticReferenceExpression, - updateSyntheticReferenceExpression, - cloneNode, - - // Lazily load factory methods for common operator factories and utilities - get createComma() { return getBinaryCreateFunction(ts.SyntaxKind.CommaToken); }, - get createAssignment() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsToken) as ts.NodeFactory["createAssignment"]; }, - get createLogicalOr() { return getBinaryCreateFunction(ts.SyntaxKind.BarBarToken); }, - get createLogicalAnd() { return getBinaryCreateFunction(ts.SyntaxKind.AmpersandAmpersandToken); }, - get createBitwiseOr() { return getBinaryCreateFunction(ts.SyntaxKind.BarToken); }, - get createBitwiseXor() { return getBinaryCreateFunction(ts.SyntaxKind.CaretToken); }, - get createBitwiseAnd() { return getBinaryCreateFunction(ts.SyntaxKind.AmpersandToken); }, - get createStrictEquality() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsEqualsEqualsToken); }, - get createStrictInequality() { return getBinaryCreateFunction(ts.SyntaxKind.ExclamationEqualsEqualsToken); }, - get createEquality() { return getBinaryCreateFunction(ts.SyntaxKind.EqualsEqualsToken); }, - get createInequality() { return getBinaryCreateFunction(ts.SyntaxKind.ExclamationEqualsToken); }, - get createLessThan() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanToken); }, - get createLessThanEquals() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanEqualsToken); }, - get createGreaterThan() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanToken); }, - get createGreaterThanEquals() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanEqualsToken); }, - get createLeftShift() { return getBinaryCreateFunction(ts.SyntaxKind.LessThanLessThanToken); }, - get createRightShift() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanGreaterThanToken); }, - get createUnsignedRightShift() { return getBinaryCreateFunction(ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken); }, - get createAdd() { return getBinaryCreateFunction(ts.SyntaxKind.PlusToken); }, - get createSubtract() { return getBinaryCreateFunction(ts.SyntaxKind.MinusToken); }, - get createMultiply() { return getBinaryCreateFunction(ts.SyntaxKind.AsteriskToken); }, - get createDivide() { return getBinaryCreateFunction(ts.SyntaxKind.SlashToken); }, - get createModulo() { return getBinaryCreateFunction(ts.SyntaxKind.PercentToken); }, - get createExponent() { return getBinaryCreateFunction(ts.SyntaxKind.AsteriskAsteriskToken); }, - get createPrefixPlus() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.PlusToken); }, - get createPrefixMinus() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.MinusToken); }, - get createPrefixIncrement() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.PlusPlusToken); }, - get createPrefixDecrement() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.MinusMinusToken); }, - get createBitwiseNot() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.TildeToken); }, - get createLogicalNot() { return getPrefixUnaryCreateFunction(ts.SyntaxKind.ExclamationToken); }, - get createPostfixIncrement() { return getPostfixUnaryCreateFunction(ts.SyntaxKind.PlusPlusToken); }, - get createPostfixDecrement() { return getPostfixUnaryCreateFunction(ts.SyntaxKind.MinusMinusToken); }, - - // Compound nodes - createImmediatelyInvokedFunctionExpression, - createImmediatelyInvokedArrowFunction, - createVoidZero, - createExportDefault, - createExternalModuleExport, - createTypeCheck, - createMethodCall, - createGlobalMethodCall, - createFunctionBindCall, - createFunctionCallCall, - createFunctionApplyCall, - createArraySliceCall, - createArrayConcatCall, - createObjectDefinePropertyCall, - createReflectGetCall, - createReflectSetCall, - createPropertyDescriptor, - createCallBinding, - createAssignmentTargetWrapper, - - // Utilities - inlineExpressions, - getInternalName, - getLocalName, - getExportName, - getDeclarationName, - getNamespaceMemberName, - getExternalModuleOrNamespaceExportName, - restoreOuterExpressions, - restoreEnclosingLabel, - createUseStrictPrologue, - copyPrologue, - copyStandardPrologue, - copyCustomPrologue, - ensureUseStrict, - liftToBlock, - mergeLexicalEnvironment, - updateModifiers, - }; + // Utilities + inlineExpressions, + getInternalName, + getLocalName, + getExportName, + getDeclarationName, + getNamespaceMemberName, + getExternalModuleOrNamespaceExportName, + restoreOuterExpressions, + restoreEnclosingLabel, + createUseStrictPrologue, + copyPrologue, + copyStandardPrologue, + copyCustomPrologue, + ensureUseStrict, + liftToBlock, + mergeLexicalEnvironment, + updateModifiers, + }; - return factory; + return factory; - // @api - function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): ts.NodeArray { - if (elements === undefined || elements === ts.emptyArray) { - elements = []; - } - else if (ts.isNodeArray(elements)) { - if (hasTrailingComma === undefined || elements.hasTrailingComma === hasTrailingComma) { - // Ensure the transform flags have been aggregated for this NodeArray - if (elements.transformFlags === undefined) { - aggregateChildrenFlags(elements as ts.MutableNodeArray); - } - ts.Debug.attachNodeArrayDebugInfo(elements); - return elements; + // @api + function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): ts.NodeArray { + if (elements === undefined || elements === ts.emptyArray) { + elements = []; + } + else if (ts.isNodeArray(elements)) { + if (hasTrailingComma === undefined || elements.hasTrailingComma === hasTrailingComma) { + // Ensure the transform flags have been aggregated for this NodeArray + if (elements.transformFlags === undefined) { + aggregateChildrenFlags(elements as ts.MutableNodeArray); } - - // This *was* a `NodeArray`, but the `hasTrailingComma` option differs. Recreate the - // array with the same elements, text range, and transform flags but with the updated - // value for `hasTrailingComma` - const array = elements.slice() as ts.MutableNodeArray; - array.pos = elements.pos; - array.end = elements.end; - array.hasTrailingComma = hasTrailingComma; - array.transformFlags = elements.transformFlags; - ts.Debug.attachNodeArrayDebugInfo(array); - return array; + ts.Debug.attachNodeArrayDebugInfo(elements); + return elements; } - // 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) as ts.MutableNodeArray; - ts.setTextRangePosEnd(array, -1, -1); - array.hasTrailingComma = !!hasTrailingComma; - aggregateChildrenFlags(array); + // This *was* a `NodeArray`, but the `hasTrailingComma` option differs. Recreate the + // array with the same elements, text range, and transform flags but with the updated + // value for `hasTrailingComma` + const array = elements.slice() as ts.MutableNodeArray; + array.pos = elements.pos; + array.end = elements.end; + array.hasTrailingComma = hasTrailingComma; + array.transformFlags = elements.transformFlags; ts.Debug.attachNodeArrayDebugInfo(array); return array; } - function createBaseNode(kind: T["kind"]) { - return baseFactory.createBaseNode(kind) as ts.Mutable; - } + // 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) as ts.MutableNodeArray; + ts.setTextRangePosEnd(array, -1, -1); + array.hasTrailingComma = !!hasTrailingComma; + aggregateChildrenFlags(array); + ts.Debug.attachNodeArrayDebugInfo(array); + return array; + } - function createBaseDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined) { - const node = createBaseNode(kind); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.transformFlags |= - propagateChildrenFlags(node.decorators) | - propagateChildrenFlags(node.modifiers); - // NOTE: The following properties are commonly set by the binder and are added here to - // ensure declarations have a stable shape. - node.symbol = undefined!; // initialized by binder - node.localSymbol = undefined; // initialized by binder - node.locals = undefined; // initialized by binder - node.nextContainer = undefined; // initialized by binder - return node; - } + function createBaseNode(kind: T["kind"]) { + return baseFactory.createBaseNode(kind) as ts.Mutable; + } + + function createBaseDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined) { + const node = createBaseNode(kind); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.transformFlags |= + propagateChildrenFlags(node.decorators) | + propagateChildrenFlags(node.modifiers); + // NOTE: The following properties are commonly set by the binder and are added here to + // ensure declarations have a stable shape. + node.symbol = undefined!; // initialized by binder + node.localSymbol = undefined; // initialized by binder + node.locals = undefined; // initialized by binder + node.nextContainer = undefined; // initialized by binder + return node; + } - function createBaseNamedDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined) { - const node = createBaseDeclaration(kind, decorators, modifiers); - name = asName(name); - node.name = name; - - // The PropertyName of a member is allowed to be `await`. - // We don't need to exclude `await` for type signatures since types - // don't propagate child flags. - if (name) { - switch (node.kind) { - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertyAssignment: - if (ts.isIdentifier(name)) { - node.transformFlags |= propagateIdentifierNameFlags(name); - break; - } - // fall through - default: - node.transformFlags |= propagateChildFlags(name); + function createBaseNamedDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined) { + const node = createBaseDeclaration(kind, decorators, modifiers); + name = asName(name); + node.name = name; + + // The PropertyName of a member is allowed to be `await`. + // We don't need to exclude `await` for type signatures since types + // don't propagate child flags. + if (name) { + switch (node.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertyAssignment: + if (ts.isIdentifier(name)) { + node.transformFlags |= propagateIdentifierNameFlags(name); break; - } + } + // fall through + default: + node.transformFlags |= propagateChildFlags(name); + break; } - return node; } + return node; + } - function createBaseGenericNamedDeclaration; - }>(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined) { - const node = createBaseNamedDeclaration(kind, decorators, modifiers, name); - node.typeParameters = asNodeArray(typeParameters); - node.transformFlags |= propagateChildrenFlags(node.typeParameters); - if (typeParameters) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - return node; - } - function createBaseSignatureDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined) { - const node = createBaseGenericNamedDeclaration(kind, decorators, modifiers, name, typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - node.transformFlags |= - propagateChildrenFlags(node.parameters) | - propagateChildFlags(node.type); - if (type) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - return node; - } + function createBaseGenericNamedDeclaration; + }>(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined) { + const node = createBaseNamedDeclaration(kind, decorators, modifiers, name); + node.typeParameters = asNodeArray(typeParameters); + node.transformFlags |= propagateChildrenFlags(node.typeParameters); + if (typeParameters) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + return node; + } + function createBaseSignatureDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined) { + const node = createBaseGenericNamedDeclaration(kind, decorators, modifiers, name, typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + node.transformFlags |= + propagateChildrenFlags(node.parameters) | + propagateChildFlags(node.type); + if (type) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + return node; + } - function updateBaseSignatureDeclaration(updated: ts.Mutable, original: T) { - // copy children used only for error reporting - if (original.typeArguments) - updated.typeArguments = original.typeArguments; - return update(updated, original); - } + function updateBaseSignatureDeclaration(updated: ts.Mutable, original: T) { + // copy children used only for error reporting + if (original.typeArguments) + updated.typeArguments = original.typeArguments; + return update(updated, original); + } - function createBaseFunctionLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined, body: T["body"]) { - const node = createBaseSignatureDeclaration(kind, decorators, modifiers, name, typeParameters, parameters, type); - node.body = body; - node.transformFlags |= propagateChildFlags(node.body) & ~ts.TransformFlags.ContainsPossibleTopLevelAwait; - if (!body) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - return node; - } + function createBaseFunctionLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteralLike | ts.NumericLiteral | ts.ComputedPropertyName | ts.BindingPattern | string | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined, body: T["body"]) { + const node = createBaseSignatureDeclaration(kind, decorators, modifiers, name, typeParameters, parameters, type); + node.body = body; + node.transformFlags |= propagateChildFlags(node.body) & ~ts.TransformFlags.ContainsPossibleTopLevelAwait; + if (!body) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + return node; + } - function updateBaseFunctionLikeDeclaration(updated: ts.Mutable, original: T) { - // copy children used only for error reporting - if (original.exclamationToken) - updated.exclamationToken = original.exclamationToken; - if (original.typeArguments) - updated.typeArguments = original.typeArguments; - return updateBaseSignatureDeclaration(updated, original); - } + function updateBaseFunctionLikeDeclaration(updated: ts.Mutable, original: T) { + // copy children used only for error reporting + if (original.exclamationToken) + updated.exclamationToken = original.exclamationToken; + if (original.typeArguments) + updated.typeArguments = original.typeArguments; + return updateBaseSignatureDeclaration(updated, original); + } - function createBaseInterfaceOrClassLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined) { - const node = createBaseGenericNamedDeclaration(kind, decorators, modifiers, name, typeParameters); - node.heritageClauses = asNodeArray(heritageClauses); - node.transformFlags |= propagateChildrenFlags(node.heritageClauses); - return node; - } + function createBaseInterfaceOrClassLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined) { + const node = createBaseGenericNamedDeclaration(kind, decorators, modifiers, name, typeParameters); + node.heritageClauses = asNodeArray(heritageClauses); + node.transformFlags |= propagateChildrenFlags(node.heritageClauses); + return node; + } - function createBaseClassLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { - const node = createBaseInterfaceOrClassLikeDeclaration(kind, decorators, modifiers, name, typeParameters, heritageClauses); - node.members = createNodeArray(members); - node.transformFlags |= propagateChildrenFlags(node.members); - return node; - } + function createBaseClassLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { + const node = createBaseInterfaceOrClassLikeDeclaration(kind, decorators, modifiers, name, typeParameters, heritageClauses); + node.members = createNodeArray(members); + node.transformFlags |= propagateChildrenFlags(node.members); + return node; + } - function createBaseBindingLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | T["name"] | undefined, initializer: ts.Expression | undefined) { - const node = createBaseNamedDeclaration(kind, decorators, modifiers, name); - node.initializer = initializer; - node.transformFlags |= propagateChildFlags(node.initializer); - return node; - } + function createBaseBindingLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | T["name"] | undefined, initializer: ts.Expression | undefined) { + const node = createBaseNamedDeclaration(kind, decorators, modifiers, name); + node.initializer = initializer; + node.transformFlags |= propagateChildFlags(node.initializer); + return node; + } - function createBaseVariableLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | T["name"] | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - const node = createBaseBindingLikeDeclaration(kind, decorators, modifiers, name, initializer); - node.type = type; - node.transformFlags |= propagateChildFlags(type); - if (type) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - return node; - } + function createBaseVariableLikeDeclaration(kind: T["kind"], decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | T["name"] | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + const node = createBaseBindingLikeDeclaration(kind, decorators, modifiers, name, initializer); + node.type = type; + node.transformFlags |= propagateChildFlags(type); + if (type) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + return node; + } - // - // Literals - // + // + // Literals + // - function createBaseLiteral(kind: T["kind"], text: string) { - const node = createBaseToken(kind); - node.text = text; - return node; - } + function createBaseLiteral(kind: T["kind"], text: string) { + const node = createBaseToken(kind); + node.text = text; + return node; + } - // @api - function createNumericLiteral(value: string | number, numericLiteralFlags: ts.TokenFlags = ts.TokenFlags.None): ts.NumericLiteral { - const node = createBaseLiteral(ts.SyntaxKind.NumericLiteral, typeof value === "number" ? value + "" : value); - node.numericLiteralFlags = numericLiteralFlags; - if (numericLiteralFlags & ts.TokenFlags.BinaryOrOctalSpecifier) - node.transformFlags |= ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function createNumericLiteral(value: string | number, numericLiteralFlags: ts.TokenFlags = ts.TokenFlags.None): ts.NumericLiteral { + const node = createBaseLiteral(ts.SyntaxKind.NumericLiteral, typeof value === "number" ? value + "" : value); + node.numericLiteralFlags = numericLiteralFlags; + if (numericLiteralFlags & ts.TokenFlags.BinaryOrOctalSpecifier) + node.transformFlags |= ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function createBigIntLiteral(value: string | ts.PseudoBigInt): ts.BigIntLiteral { - const node = createBaseLiteral(ts.SyntaxKind.BigIntLiteral, typeof value === "string" ? value : ts.pseudoBigIntToString(value) + "n"); - node.transformFlags |= ts.TransformFlags.ContainsESNext; - return node; - } + // @api + function createBigIntLiteral(value: string | ts.PseudoBigInt): ts.BigIntLiteral { + const node = createBaseLiteral(ts.SyntaxKind.BigIntLiteral, typeof value === "string" ? value : ts.pseudoBigIntToString(value) + "n"); + node.transformFlags |= ts.TransformFlags.ContainsESNext; + return node; + } - function createBaseStringLiteral(text: string, isSingleQuote?: boolean) { - const node = createBaseLiteral(ts.SyntaxKind.StringLiteral, text); - node.singleQuote = isSingleQuote; - return node; - } + function createBaseStringLiteral(text: string, isSingleQuote?: boolean) { + const node = createBaseLiteral(ts.SyntaxKind.StringLiteral, text); + node.singleQuote = isSingleQuote; + return node; + } - // @api - function createStringLiteral(text: string, isSingleQuote?: boolean, hasExtendedUnicodeEscape?: boolean): ts.StringLiteral { - const node = createBaseStringLiteral(text, isSingleQuote); - node.hasExtendedUnicodeEscape = hasExtendedUnicodeEscape; - if (hasExtendedUnicodeEscape) - node.transformFlags |= ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function createStringLiteral(text: string, isSingleQuote?: boolean, hasExtendedUnicodeEscape?: boolean): ts.StringLiteral { + const node = createBaseStringLiteral(text, isSingleQuote); + node.hasExtendedUnicodeEscape = hasExtendedUnicodeEscape; + if (hasExtendedUnicodeEscape) + node.transformFlags |= ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function createStringLiteralFromNode(sourceNode: ts.PropertyNameLiteral): ts.StringLiteral { - const node = createBaseStringLiteral(ts.getTextOfIdentifierOrLiteral(sourceNode), /*isSingleQuote*/ undefined); - node.textSourceNode = sourceNode; - return node; - } + // @api + function createStringLiteralFromNode(sourceNode: ts.PropertyNameLiteral): ts.StringLiteral { + const node = createBaseStringLiteral(ts.getTextOfIdentifierOrLiteral(sourceNode), /*isSingleQuote*/ undefined); + node.textSourceNode = sourceNode; + return node; + } - // @api - function createRegularExpressionLiteral(text: string): ts.RegularExpressionLiteral { - const node = createBaseLiteral(ts.SyntaxKind.RegularExpressionLiteral, text); - return node; - } + // @api + function createRegularExpressionLiteral(text: string): ts.RegularExpressionLiteral { + const node = createBaseLiteral(ts.SyntaxKind.RegularExpressionLiteral, text); + return node; + } - // @api - function createLiteralLikeNode(kind: ts.LiteralToken["kind"] | ts.SyntaxKind.JsxTextAllWhiteSpaces, text: string): ts.LiteralToken { - switch (kind) { - case ts.SyntaxKind.NumericLiteral: return createNumericLiteral(text, /*numericLiteralFlags*/ 0); - case ts.SyntaxKind.BigIntLiteral: return createBigIntLiteral(text); - case ts.SyntaxKind.StringLiteral: return createStringLiteral(text, /*isSingleQuote*/ undefined); - case ts.SyntaxKind.JsxText: return createJsxText(text, /*containsOnlyTriviaWhiteSpaces*/ false); - case ts.SyntaxKind.JsxTextAllWhiteSpaces: return createJsxText(text, /*containsOnlyTriviaWhiteSpaces*/ true); - case ts.SyntaxKind.RegularExpressionLiteral: return createRegularExpressionLiteral(text); - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return createTemplateLiteralLikeNode(kind, text, /*rawText*/ undefined, /*templateFlags*/ 0) as ts.NoSubstitutionTemplateLiteral; - } + // @api + function createLiteralLikeNode(kind: ts.LiteralToken["kind"] | ts.SyntaxKind.JsxTextAllWhiteSpaces, text: string): ts.LiteralToken { + switch (kind) { + case ts.SyntaxKind.NumericLiteral: return createNumericLiteral(text, /*numericLiteralFlags*/ 0); + case ts.SyntaxKind.BigIntLiteral: return createBigIntLiteral(text); + case ts.SyntaxKind.StringLiteral: return createStringLiteral(text, /*isSingleQuote*/ undefined); + case ts.SyntaxKind.JsxText: return createJsxText(text, /*containsOnlyTriviaWhiteSpaces*/ false); + case ts.SyntaxKind.JsxTextAllWhiteSpaces: return createJsxText(text, /*containsOnlyTriviaWhiteSpaces*/ true); + case ts.SyntaxKind.RegularExpressionLiteral: return createRegularExpressionLiteral(text); + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return createTemplateLiteralLikeNode(kind, text, /*rawText*/ undefined, /*templateFlags*/ 0) as ts.NoSubstitutionTemplateLiteral; } + } - // - // Identifiers - // + // + // Identifiers + // - function createBaseIdentifier(text: string, originalKeywordKind: ts.SyntaxKind | undefined) { - if (originalKeywordKind === undefined && text) { - originalKeywordKind = ts.stringToToken(text); - } - if (originalKeywordKind === ts.SyntaxKind.Identifier) { - originalKeywordKind = undefined; - } - const node = baseFactory.createBaseIdentifierNode(ts.SyntaxKind.Identifier) as ts.Mutable; - node.originalKeywordKind = originalKeywordKind; - node.escapedText = ts.escapeLeadingUnderscores(text); - return node; + function createBaseIdentifier(text: string, originalKeywordKind: ts.SyntaxKind | undefined) { + if (originalKeywordKind === undefined && text) { + originalKeywordKind = ts.stringToToken(text); } - - function createBaseGeneratedIdentifier(text: string, autoGenerateFlags: ts.GeneratedIdentifierFlags) { - const node = createBaseIdentifier(text, /*originalKeywordKind*/ undefined) as ts.Mutable; - node.autoGenerateFlags = autoGenerateFlags; - node.autoGenerateId = nextAutoGenerateId; - nextAutoGenerateId++; - return node; + if (originalKeywordKind === ts.SyntaxKind.Identifier) { + originalKeywordKind = undefined; } + const node = baseFactory.createBaseIdentifierNode(ts.SyntaxKind.Identifier) as ts.Mutable; + node.originalKeywordKind = originalKeywordKind; + node.escapedText = ts.escapeLeadingUnderscores(text); + return node; + } - // @api - function createIdentifier(text: string, typeArguments?: readonly (ts.TypeNode | ts.TypeParameterDeclaration)[], originalKeywordKind?: ts.SyntaxKind): ts.Identifier { - const node = createBaseIdentifier(text, originalKeywordKind); - if (typeArguments) { - // NOTE: we do not use `setChildren` here because typeArguments in an identifier do not contribute to transformations - node.typeArguments = createNodeArray(typeArguments); - } - if (node.originalKeywordKind === ts.SyntaxKind.AwaitKeyword) { - node.transformFlags |= ts.TransformFlags.ContainsPossibleTopLevelAwait; - } - return node; - } + function createBaseGeneratedIdentifier(text: string, autoGenerateFlags: ts.GeneratedIdentifierFlags) { + const node = createBaseIdentifier(text, /*originalKeywordKind*/ undefined) as ts.Mutable; + node.autoGenerateFlags = autoGenerateFlags; + node.autoGenerateId = nextAutoGenerateId; + nextAutoGenerateId++; + return node; + } - // @api - function updateIdentifier(node: ts.Identifier, typeArguments?: ts.NodeArray | undefined): ts.Identifier { - return node.typeArguments !== typeArguments - ? update(createIdentifier(ts.idText(node), typeArguments), node) - : node; + // @api + function createIdentifier(text: string, typeArguments?: readonly (ts.TypeNode | ts.TypeParameterDeclaration)[], originalKeywordKind?: ts.SyntaxKind): ts.Identifier { + const node = createBaseIdentifier(text, originalKeywordKind); + if (typeArguments) { + // NOTE: we do not use `setChildren` here because typeArguments in an identifier do not contribute to transformations + node.typeArguments = createNodeArray(typeArguments); } - - // @api - function createTempVariable(recordTempVariable: ((node: ts.Identifier) => void) | undefined, reservedInNestedScopes?: boolean): ts.GeneratedIdentifier { - let flags = ts.GeneratedIdentifierFlags.Auto; - if (reservedInNestedScopes) - flags |= ts.GeneratedIdentifierFlags.ReservedInNestedScopes; - const name = createBaseGeneratedIdentifier("", flags); - if (recordTempVariable) { - recordTempVariable(name); - } - return name; + if (node.originalKeywordKind === ts.SyntaxKind.AwaitKeyword) { + node.transformFlags |= ts.TransformFlags.ContainsPossibleTopLevelAwait; } + return node; + } - /** Create a unique temporary variable for use in a loop. */ - // @api - function createLoopVariable(reservedInNestedScopes?: boolean): ts.Identifier { - let flags = ts.GeneratedIdentifierFlags.Loop; - if (reservedInNestedScopes) - flags |= ts.GeneratedIdentifierFlags.ReservedInNestedScopes; - return createBaseGeneratedIdentifier("", flags); - } + // @api + function updateIdentifier(node: ts.Identifier, typeArguments?: ts.NodeArray | undefined): ts.Identifier { + return node.typeArguments !== typeArguments + ? update(createIdentifier(ts.idText(node), typeArguments), node) + : node; + } - /** Create a unique name based on the supplied text. */ - // @api - function createUniqueName(text: string, flags: ts.GeneratedIdentifierFlags = ts.GeneratedIdentifierFlags.None): ts.Identifier { - ts.Debug.assert(!(flags & ts.GeneratedIdentifierFlags.KindMask), "Argument out of range: flags"); - ts.Debug.assert((flags & (ts.GeneratedIdentifierFlags.Optimistic | ts.GeneratedIdentifierFlags.FileLevel)) !== ts.GeneratedIdentifierFlags.FileLevel, "GeneratedIdentifierFlags.FileLevel cannot be set without also setting GeneratedIdentifierFlags.Optimistic"); - return createBaseGeneratedIdentifier(text, ts.GeneratedIdentifierFlags.Unique | flags); + // @api + function createTempVariable(recordTempVariable: ((node: ts.Identifier) => void) | undefined, reservedInNestedScopes?: boolean): ts.GeneratedIdentifier { + let flags = ts.GeneratedIdentifierFlags.Auto; + if (reservedInNestedScopes) + flags |= ts.GeneratedIdentifierFlags.ReservedInNestedScopes; + const name = createBaseGeneratedIdentifier("", flags); + if (recordTempVariable) { + recordTempVariable(name); } + return name; + } - /** Create a unique name generated for a node. */ - // @api - function getGeneratedNameForNode(node: ts.Node | undefined, flags: ts.GeneratedIdentifierFlags = 0): ts.Identifier { - ts.Debug.assert(!(flags & ts.GeneratedIdentifierFlags.KindMask), "Argument out of range: flags"); - const name = createBaseGeneratedIdentifier(node && ts.isIdentifier(node) ? ts.idText(node) : "", ts.GeneratedIdentifierFlags.Node | flags); - name.original = node; - return name; - } + /** Create a unique temporary variable for use in a loop. */ + // @api + function createLoopVariable(reservedInNestedScopes?: boolean): ts.Identifier { + let flags = ts.GeneratedIdentifierFlags.Loop; + if (reservedInNestedScopes) + flags |= ts.GeneratedIdentifierFlags.ReservedInNestedScopes; + return createBaseGeneratedIdentifier("", flags); + } - // @api - function createPrivateIdentifier(text: string): ts.PrivateIdentifier { - if (!ts.startsWith(text, "#")) - ts.Debug.fail("First character of private identifier must be #: " + text); - const node = baseFactory.createBasePrivateIdentifierNode(ts.SyntaxKind.PrivateIdentifier) as ts.Mutable; - node.escapedText = ts.escapeLeadingUnderscores(text); - node.transformFlags |= ts.TransformFlags.ContainsClassFields; - return node; - } + /** Create a unique name based on the supplied text. */ + // @api + function createUniqueName(text: string, flags: ts.GeneratedIdentifierFlags = ts.GeneratedIdentifierFlags.None): ts.Identifier { + ts.Debug.assert(!(flags & ts.GeneratedIdentifierFlags.KindMask), "Argument out of range: flags"); + ts.Debug.assert((flags & (ts.GeneratedIdentifierFlags.Optimistic | ts.GeneratedIdentifierFlags.FileLevel)) !== ts.GeneratedIdentifierFlags.FileLevel, "GeneratedIdentifierFlags.FileLevel cannot be set without also setting GeneratedIdentifierFlags.Optimistic"); + return createBaseGeneratedIdentifier(text, ts.GeneratedIdentifierFlags.Unique | flags); + } - // - // Punctuation - // + /** Create a unique name generated for a node. */ + // @api + function getGeneratedNameForNode(node: ts.Node | undefined, flags: ts.GeneratedIdentifierFlags = 0): ts.Identifier { + ts.Debug.assert(!(flags & ts.GeneratedIdentifierFlags.KindMask), "Argument out of range: flags"); + const name = createBaseGeneratedIdentifier(node && ts.isIdentifier(node) ? ts.idText(node) : "", ts.GeneratedIdentifierFlags.Node | flags); + name.original = node; + return name; + } - function createBaseToken(kind: T["kind"]) { - return baseFactory.createBaseTokenNode(kind) as ts.Mutable; - } - // @api - function createToken(token: ts.SyntaxKind.SuperKeyword): ts.SuperExpression; - function createToken(token: ts.SyntaxKind.ThisKeyword): ts.ThisExpression; - function createToken(token: ts.SyntaxKind.NullKeyword): ts.NullLiteral; - function createToken(token: ts.SyntaxKind.TrueKeyword): ts.TrueLiteral; - function createToken(token: ts.SyntaxKind.FalseKeyword): ts.FalseLiteral; - function createToken(token: TKind): ts.PunctuationToken; - function createToken(token: TKind): ts.KeywordTypeNode; - function createToken(token: TKind): ts.ModifierToken; - function createToken(token: TKind): ts.KeywordToken; - function createToken(token: TKind): ts.Token; - function createToken(token: TKind): ts.Token; - function createToken(token: TKind) { - ts.Debug.assert(token >= ts.SyntaxKind.FirstToken && token <= ts.SyntaxKind.LastToken, "Invalid token"); - ts.Debug.assert(token <= ts.SyntaxKind.FirstTemplateToken || token >= ts.SyntaxKind.LastTemplateToken, "Invalid token. Use 'createTemplateLiteralLikeNode' to create template literals."); - ts.Debug.assert(token <= ts.SyntaxKind.FirstLiteralToken || token >= ts.SyntaxKind.LastLiteralToken, "Invalid token. Use 'createLiteralLikeNode' to create literals."); - ts.Debug.assert(token !== ts.SyntaxKind.Identifier, "Invalid token. Use 'createIdentifier' to create identifiers"); - const node = createBaseToken>(token); - let transformFlags = ts.TransformFlags.None; - switch (token) { - case ts.SyntaxKind.AsyncKeyword: - // 'async' modifier is ES2017 (async functions) or ES2018 (async generators) - transformFlags = - ts.TransformFlags.ContainsES2017 | - ts.TransformFlags.ContainsES2018; - break; + // @api + function createPrivateIdentifier(text: string): ts.PrivateIdentifier { + if (!ts.startsWith(text, "#")) + ts.Debug.fail("First character of private identifier must be #: " + text); + const node = baseFactory.createBasePrivateIdentifierNode(ts.SyntaxKind.PrivateIdentifier) as ts.Mutable; + node.escapedText = ts.escapeLeadingUnderscores(text); + node.transformFlags |= ts.TransformFlags.ContainsClassFields; + return node; + } - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.InKeyword: - case ts.SyntaxKind.OutKeyword: - case ts.SyntaxKind.OverrideKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.UnknownKeyword: - case ts.SyntaxKind.UndefinedKeyword: // `undefined` is an Identifier in the expression case. - transformFlags = ts.TransformFlags.ContainsTypeScript; - break; - case ts.SyntaxKind.SuperKeyword: - transformFlags = ts.TransformFlags.ContainsES2015 | ts.TransformFlags.ContainsLexicalSuper; - break; - case ts.SyntaxKind.StaticKeyword: - transformFlags = ts.TransformFlags.ContainsES2015; - break; - case ts.SyntaxKind.ThisKeyword: - // 'this' indicates a lexical 'this' - transformFlags = ts.TransformFlags.ContainsLexicalThis; - break; - } - if (transformFlags) { - node.transformFlags |= transformFlags; - } - return node; - } + // + // Punctuation + // - // - // Reserved words - // + function createBaseToken(kind: T["kind"]) { + return baseFactory.createBaseTokenNode(kind) as ts.Mutable; + } + // @api + function createToken(token: ts.SyntaxKind.SuperKeyword): ts.SuperExpression; + function createToken(token: ts.SyntaxKind.ThisKeyword): ts.ThisExpression; + function createToken(token: ts.SyntaxKind.NullKeyword): ts.NullLiteral; + function createToken(token: ts.SyntaxKind.TrueKeyword): ts.TrueLiteral; + function createToken(token: ts.SyntaxKind.FalseKeyword): ts.FalseLiteral; + function createToken(token: TKind): ts.PunctuationToken; + function createToken(token: TKind): ts.KeywordTypeNode; + function createToken(token: TKind): ts.ModifierToken; + function createToken(token: TKind): ts.KeywordToken; + function createToken(token: TKind): ts.Token; + function createToken(token: TKind): ts.Token; + function createToken(token: TKind) { + ts.Debug.assert(token >= ts.SyntaxKind.FirstToken && token <= ts.SyntaxKind.LastToken, "Invalid token"); + ts.Debug.assert(token <= ts.SyntaxKind.FirstTemplateToken || token >= ts.SyntaxKind.LastTemplateToken, "Invalid token. Use 'createTemplateLiteralLikeNode' to create template literals."); + ts.Debug.assert(token <= ts.SyntaxKind.FirstLiteralToken || token >= ts.SyntaxKind.LastLiteralToken, "Invalid token. Use 'createLiteralLikeNode' to create literals."); + ts.Debug.assert(token !== ts.SyntaxKind.Identifier, "Invalid token. Use 'createIdentifier' to create identifiers"); + const node = createBaseToken>(token); + let transformFlags = ts.TransformFlags.None; + switch (token) { + case ts.SyntaxKind.AsyncKeyword: + // 'async' modifier is ES2017 (async functions) or ES2018 (async generators) + transformFlags = + ts.TransformFlags.ContainsES2017 | + ts.TransformFlags.ContainsES2018; + break; - // @api - function createSuper() { - return createToken(ts.SyntaxKind.SuperKeyword); + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.ConstKeyword: + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.InKeyword: + case ts.SyntaxKind.OutKeyword: + case ts.SyntaxKind.OverrideKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.UndefinedKeyword: // `undefined` is an Identifier in the expression case. + transformFlags = ts.TransformFlags.ContainsTypeScript; + break; + case ts.SyntaxKind.SuperKeyword: + transformFlags = ts.TransformFlags.ContainsES2015 | ts.TransformFlags.ContainsLexicalSuper; + break; + case ts.SyntaxKind.StaticKeyword: + transformFlags = ts.TransformFlags.ContainsES2015; + break; + case ts.SyntaxKind.ThisKeyword: + // 'this' indicates a lexical 'this' + transformFlags = ts.TransformFlags.ContainsLexicalThis; + break; } - - // @api - function createThis() { - return createToken(ts.SyntaxKind.ThisKeyword); + if (transformFlags) { + node.transformFlags |= transformFlags; } + return node; + } - // @api - function createNull() { - return createToken(ts.SyntaxKind.NullKeyword); - } + // + // Reserved words + // - // @api - function createTrue() { - return createToken(ts.SyntaxKind.TrueKeyword); - } + // @api + function createSuper() { + return createToken(ts.SyntaxKind.SuperKeyword); + } - // @api - function createFalse() { - return createToken(ts.SyntaxKind.FalseKeyword); - } + // @api + function createThis() { + return createToken(ts.SyntaxKind.ThisKeyword); + } - // - // Modifiers - // + // @api + function createNull() { + return createToken(ts.SyntaxKind.NullKeyword); + } - // @api - function createModifier(kind: T) { - return createToken(kind); - } + // @api + function createTrue() { + return createToken(ts.SyntaxKind.TrueKeyword); + } - // @api - function createModifiersFromModifierFlags(flags: ts.ModifierFlags) { - const result: ts.Modifier[] = []; - if (flags & ts.ModifierFlags.Export) - result.push(createModifier(ts.SyntaxKind.ExportKeyword)); - if (flags & ts.ModifierFlags.Ambient) - result.push(createModifier(ts.SyntaxKind.DeclareKeyword)); - if (flags & ts.ModifierFlags.Default) - result.push(createModifier(ts.SyntaxKind.DefaultKeyword)); - if (flags & ts.ModifierFlags.Const) - result.push(createModifier(ts.SyntaxKind.ConstKeyword)); - if (flags & ts.ModifierFlags.Public) - result.push(createModifier(ts.SyntaxKind.PublicKeyword)); - if (flags & ts.ModifierFlags.Private) - result.push(createModifier(ts.SyntaxKind.PrivateKeyword)); - if (flags & ts.ModifierFlags.Protected) - result.push(createModifier(ts.SyntaxKind.ProtectedKeyword)); - if (flags & ts.ModifierFlags.Abstract) - result.push(createModifier(ts.SyntaxKind.AbstractKeyword)); - if (flags & ts.ModifierFlags.Static) - result.push(createModifier(ts.SyntaxKind.StaticKeyword)); - if (flags & ts.ModifierFlags.Override) - result.push(createModifier(ts.SyntaxKind.OverrideKeyword)); - if (flags & ts.ModifierFlags.Readonly) - result.push(createModifier(ts.SyntaxKind.ReadonlyKeyword)); - if (flags & ts.ModifierFlags.Async) - result.push(createModifier(ts.SyntaxKind.AsyncKeyword)); - if (flags & ts.ModifierFlags.In) - result.push(createModifier(ts.SyntaxKind.InKeyword)); - if (flags & ts.ModifierFlags.Out) - result.push(createModifier(ts.SyntaxKind.OutKeyword)); - return result.length ? result : undefined; - } + // @api + function createFalse() { + return createToken(ts.SyntaxKind.FalseKeyword); + } - // - // Names - // + // + // Modifiers + // - // @api - function createQualifiedName(left: ts.EntityName, right: string | ts.Identifier) { - const node = createBaseNode(ts.SyntaxKind.QualifiedName); - node.left = left; - node.right = asName(right); - node.transformFlags |= - propagateChildFlags(node.left) | - propagateIdentifierNameFlags(node.right); - return node; - } + // @api + function createModifier(kind: T) { + return createToken(kind); + } - // @api - function updateQualifiedName(node: ts.QualifiedName, left: ts.EntityName, right: ts.Identifier) { - return node.left !== left - || node.right !== right - ? update(createQualifiedName(left, right), node) - : node; - } + // @api + function createModifiersFromModifierFlags(flags: ts.ModifierFlags) { + const result: ts.Modifier[] = []; + if (flags & ts.ModifierFlags.Export) + result.push(createModifier(ts.SyntaxKind.ExportKeyword)); + if (flags & ts.ModifierFlags.Ambient) + result.push(createModifier(ts.SyntaxKind.DeclareKeyword)); + if (flags & ts.ModifierFlags.Default) + result.push(createModifier(ts.SyntaxKind.DefaultKeyword)); + if (flags & ts.ModifierFlags.Const) + result.push(createModifier(ts.SyntaxKind.ConstKeyword)); + if (flags & ts.ModifierFlags.Public) + result.push(createModifier(ts.SyntaxKind.PublicKeyword)); + if (flags & ts.ModifierFlags.Private) + result.push(createModifier(ts.SyntaxKind.PrivateKeyword)); + if (flags & ts.ModifierFlags.Protected) + result.push(createModifier(ts.SyntaxKind.ProtectedKeyword)); + if (flags & ts.ModifierFlags.Abstract) + result.push(createModifier(ts.SyntaxKind.AbstractKeyword)); + if (flags & ts.ModifierFlags.Static) + result.push(createModifier(ts.SyntaxKind.StaticKeyword)); + if (flags & ts.ModifierFlags.Override) + result.push(createModifier(ts.SyntaxKind.OverrideKeyword)); + if (flags & ts.ModifierFlags.Readonly) + result.push(createModifier(ts.SyntaxKind.ReadonlyKeyword)); + if (flags & ts.ModifierFlags.Async) + result.push(createModifier(ts.SyntaxKind.AsyncKeyword)); + if (flags & ts.ModifierFlags.In) + result.push(createModifier(ts.SyntaxKind.InKeyword)); + if (flags & ts.ModifierFlags.Out) + result.push(createModifier(ts.SyntaxKind.OutKeyword)); + return result.length ? result : undefined; + } - // @api - function createComputedPropertyName(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.ComputedPropertyName); - node.expression = parenthesizerRules().parenthesizeExpressionOfComputedPropertyName(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsComputedPropertyName; - return node; - } + // + // Names + // + + // @api + function createQualifiedName(left: ts.EntityName, right: string | ts.Identifier) { + const node = createBaseNode(ts.SyntaxKind.QualifiedName); + node.left = left; + node.right = asName(right); + node.transformFlags |= + propagateChildFlags(node.left) | + propagateIdentifierNameFlags(node.right); + return node; + } - // @api - function updateComputedPropertyName(node: ts.ComputedPropertyName, expression: ts.Expression) { - return node.expression !== expression - ? update(createComputedPropertyName(expression), node) - : node; - } + // @api + function updateQualifiedName(node: ts.QualifiedName, left: ts.EntityName, right: ts.Identifier) { + return node.left !== left + || node.right !== right + ? update(createQualifiedName(left, right), node) + : node; + } - // - // Signature elements - // + // @api + function createComputedPropertyName(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.ComputedPropertyName); + node.expression = parenthesizerRules().parenthesizeExpressionOfComputedPropertyName(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsComputedPropertyName; + return node; + } - // @api - function createTypeParameterDeclaration(modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, constraint?: ts.TypeNode, defaultType?: ts.TypeNode): ts.TypeParameterDeclaration; - /** @deprecated */ - function createTypeParameterDeclaration(name: string | ts.Identifier, constraint?: ts.TypeNode, defaultType?: ts.TypeNode): ts.TypeParameterDeclaration; - function createTypeParameterDeclaration(modifiersOrName: readonly ts.Modifier[] | string | ts.Identifier | undefined, nameOrConstraint?: string | ts.Identifier | ts.TypeNode, constraintOrDefault?: ts.TypeNode, defaultType?: ts.TypeNode) { - let name; - let modifiers; - let constraint; - if (modifiersOrName === undefined || ts.isArray(modifiersOrName)) { - modifiers = modifiersOrName; - name = nameOrConstraint as string | ts.Identifier; - constraint = constraintOrDefault; - } - else { - modifiers = undefined; - name = modifiersOrName; - constraint = nameOrConstraint as ts.TypeNode | undefined; - } - const node = createBaseNamedDeclaration(ts.SyntaxKind.TypeParameter, - /*decorators*/ undefined, modifiers, name); - node.constraint = constraint; - node.default = defaultType; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateComputedPropertyName(node: ts.ComputedPropertyName, expression: ts.Expression) { + return node.expression !== expression + ? update(createComputedPropertyName(expression), node) + : node; + } - // @api - function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, constraint: ts.TypeNode | undefined, defaultType: ts.TypeNode | undefined): ts.TypeParameterDeclaration; - /** @deprecated */ - function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, name: ts.Identifier, constraint: ts.TypeNode | undefined, defaultType: ts.TypeNode | undefined): ts.TypeParameterDeclaration; - function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, modifiersOrName: readonly ts.Modifier[] | ts.Identifier | undefined, nameOrConstraint: ts.Identifier | ts.TypeNode | undefined, constraintOrDefault: ts.TypeNode | undefined, defaultType?: ts.TypeNode | undefined) { - let name; - let modifiers; - let constraint; - if (modifiersOrName === undefined || ts.isArray(modifiersOrName)) { - modifiers = modifiersOrName; - name = nameOrConstraint as ts.Identifier; - constraint = constraintOrDefault; - } - else { - modifiers = undefined; - name = modifiersOrName; - constraint = nameOrConstraint as ts.TypeNode | undefined; - } - return node.modifiers !== modifiers - || node.name !== name - || node.constraint !== constraint - || node.default !== defaultType - ? update(createTypeParameterDeclaration(modifiers, name, constraint, defaultType), node) - : node; + // + // Signature elements + // + + // @api + function createTypeParameterDeclaration(modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, constraint?: ts.TypeNode, defaultType?: ts.TypeNode): ts.TypeParameterDeclaration; + /** @deprecated */ + function createTypeParameterDeclaration(name: string | ts.Identifier, constraint?: ts.TypeNode, defaultType?: ts.TypeNode): ts.TypeParameterDeclaration; + function createTypeParameterDeclaration(modifiersOrName: readonly ts.Modifier[] | string | ts.Identifier | undefined, nameOrConstraint?: string | ts.Identifier | ts.TypeNode, constraintOrDefault?: ts.TypeNode, defaultType?: ts.TypeNode) { + let name; + let modifiers; + let constraint; + if (modifiersOrName === undefined || ts.isArray(modifiersOrName)) { + modifiers = modifiersOrName; + name = nameOrConstraint as string | ts.Identifier; + constraint = constraintOrDefault; } + else { + modifiers = undefined; + name = modifiersOrName; + constraint = nameOrConstraint as ts.TypeNode | undefined; + } + const node = createBaseNamedDeclaration(ts.SyntaxKind.TypeParameter, + /*decorators*/ undefined, modifiers, name); + node.constraint = constraint; + node.default = defaultType; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createParameterDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, dotDotDotToken: ts.DotDotDotToken | undefined, name: string | ts.BindingName, questionToken?: ts.QuestionToken, type?: ts.TypeNode, initializer?: ts.Expression) { - const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.Parameter, decorators, modifiers, name, type, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); - node.dotDotDotToken = dotDotDotToken; - node.questionToken = questionToken; - if (ts.isThisIdentifier(node.name)) { - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - } - else { - node.transformFlags |= - propagateChildFlags(node.dotDotDotToken) | - propagateChildFlags(node.questionToken); - if (questionToken) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.ParameterPropertyModifier) - node.transformFlags |= ts.TransformFlags.ContainsTypeScriptClassSyntax; - if (initializer || dotDotDotToken) - node.transformFlags |= ts.TransformFlags.ContainsES2015; - } - return node; + // @api + function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, constraint: ts.TypeNode | undefined, defaultType: ts.TypeNode | undefined): ts.TypeParameterDeclaration; + /** @deprecated */ + function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, name: ts.Identifier, constraint: ts.TypeNode | undefined, defaultType: ts.TypeNode | undefined): ts.TypeParameterDeclaration; + function updateTypeParameterDeclaration(node: ts.TypeParameterDeclaration, modifiersOrName: readonly ts.Modifier[] | ts.Identifier | undefined, nameOrConstraint: ts.Identifier | ts.TypeNode | undefined, constraintOrDefault: ts.TypeNode | undefined, defaultType?: ts.TypeNode | undefined) { + let name; + let modifiers; + let constraint; + if (modifiersOrName === undefined || ts.isArray(modifiersOrName)) { + modifiers = modifiersOrName; + name = nameOrConstraint as ts.Identifier; + constraint = constraintOrDefault; } + else { + modifiers = undefined; + name = modifiersOrName; + constraint = nameOrConstraint as ts.TypeNode | undefined; + } + return node.modifiers !== modifiers + || node.name !== name + || node.constraint !== constraint + || node.default !== defaultType + ? update(createTypeParameterDeclaration(modifiers, name, constraint, defaultType), node) + : node; + } - // @api - function updateParameterDeclaration(node: ts.ParameterDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, dotDotDotToken: ts.DotDotDotToken | undefined, name: string | ts.BindingName, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.dotDotDotToken !== dotDotDotToken - || node.name !== name - || node.questionToken !== questionToken - || node.type !== type - || node.initializer !== initializer - ? update(createParameterDeclaration(decorators, modifiers, dotDotDotToken, name, questionToken, type, initializer), node) - : node; + // @api + function createParameterDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, dotDotDotToken: ts.DotDotDotToken | undefined, name: string | ts.BindingName, questionToken?: ts.QuestionToken, type?: ts.TypeNode, initializer?: ts.Expression) { + const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.Parameter, decorators, modifiers, name, type, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); + node.dotDotDotToken = dotDotDotToken; + node.questionToken = questionToken; + if (ts.isThisIdentifier(node.name)) { + node.transformFlags = ts.TransformFlags.ContainsTypeScript; } - - // @api - function createDecorator(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.Decorator); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + else { node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsTypeScript | - ts.TransformFlags.ContainsTypeScriptClassSyntax; - return node; + propagateChildFlags(node.dotDotDotToken) | + propagateChildFlags(node.questionToken); + if (questionToken) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.ParameterPropertyModifier) + node.transformFlags |= ts.TransformFlags.ContainsTypeScriptClassSyntax; + if (initializer || dotDotDotToken) + node.transformFlags |= ts.TransformFlags.ContainsES2015; } + return node; + } - // @api - function updateDecorator(node: ts.Decorator, expression: ts.Expression) { - return node.expression !== expression - ? update(createDecorator(expression), node) - : node; - } + // @api + function updateParameterDeclaration(node: ts.ParameterDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, dotDotDotToken: ts.DotDotDotToken | undefined, name: string | ts.BindingName, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.dotDotDotToken !== dotDotDotToken + || node.name !== name + || node.questionToken !== questionToken + || node.type !== type + || node.initializer !== initializer + ? update(createParameterDeclaration(decorators, modifiers, dotDotDotToken, name, questionToken, type, initializer), node) + : node; + } - // - // Type Elements - // + // @api + function createDecorator(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.Decorator); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsTypeScript | + ts.TransformFlags.ContainsTypeScriptClassSyntax; + return node; + } - // @api - function createPropertySignature(modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName | string, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined): ts.PropertySignature { - const node = createBaseNamedDeclaration(ts.SyntaxKind.PropertySignature, - /*decorators*/ undefined, modifiers, name); - node.type = type; - node.questionToken = questionToken; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateDecorator(node: ts.Decorator, expression: ts.Expression) { + return node.expression !== expression + ? update(createDecorator(expression), node) + : node; + } + + // + // Type Elements + // + + // @api + function createPropertySignature(modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName | string, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined): ts.PropertySignature { + const node = createBaseNamedDeclaration(ts.SyntaxKind.PropertySignature, + /*decorators*/ undefined, modifiers, name); + node.type = type; + node.questionToken = questionToken; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } + + // @api + function updatePropertySignature(node: ts.PropertySignature, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined) { + return node.modifiers !== modifiers + || node.name !== name + || node.questionToken !== questionToken + || node.type !== type + ? update(createPropertySignature(modifiers, name, questionToken, type), node) + : node; + } - // @api - function updatePropertySignature(node: ts.PropertySignature, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode | undefined) { - return node.modifiers !== modifiers - || node.name !== name - || node.questionToken !== questionToken - || node.type !== type - ? update(createPropertySignature(modifiers, name, questionToken, type), node) - : node; + // @api + function createPropertyDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.PropertyDeclaration, decorators, modifiers, name, type, initializer); + node.questionToken = questionOrExclamationToken && ts.isQuestionToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined; + node.exclamationToken = questionOrExclamationToken && ts.isExclamationToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined; + node.transformFlags |= + propagateChildFlags(node.questionToken) | + propagateChildFlags(node.exclamationToken) | + ts.TransformFlags.ContainsClassFields; + if (ts.isComputedPropertyName(node.name) || (ts.hasStaticModifier(node) && node.initializer)) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScriptClassSyntax; + } + if (questionOrExclamationToken || ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function createPropertyDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.PropertyDeclaration, decorators, modifiers, name, type, initializer); - node.questionToken = questionOrExclamationToken && ts.isQuestionToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined; - node.exclamationToken = questionOrExclamationToken && ts.isExclamationToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined; - node.transformFlags |= - propagateChildFlags(node.questionToken) | - propagateChildFlags(node.exclamationToken) | - ts.TransformFlags.ContainsClassFields; - if (ts.isComputedPropertyName(node.name) || (ts.hasStaticModifier(node) && node.initializer)) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScriptClassSyntax; - } - if (questionOrExclamationToken || ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - return node; - } - - // @api - function updatePropertyDeclaration(node: ts.PropertyDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.questionToken !== (questionOrExclamationToken !== undefined && ts.isQuestionToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined) - || node.exclamationToken !== (questionOrExclamationToken !== undefined && ts.isExclamationToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined) - || node.type !== type - || node.initializer !== initializer - ? update(createPropertyDeclaration(decorators, modifiers, name, questionOrExclamationToken, type, initializer), node) - : node; - } + // @api + function updatePropertyDeclaration(node: ts.PropertyDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionOrExclamationToken: ts.QuestionToken | ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.questionToken !== (questionOrExclamationToken !== undefined && ts.isQuestionToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined) + || node.exclamationToken !== (questionOrExclamationToken !== undefined && ts.isExclamationToken(questionOrExclamationToken) ? questionOrExclamationToken : undefined) + || node.type !== type + || node.initializer !== initializer + ? update(createPropertyDeclaration(decorators, modifiers, name, questionOrExclamationToken, type, initializer), node) + : node; + } - // @api - function createMethodSignature(modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined) { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.MethodSignature, - /*decorators*/ undefined, modifiers, name, typeParameters, parameters, type); - node.questionToken = questionToken; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createMethodSignature(modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined) { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.MethodSignature, + /*decorators*/ undefined, modifiers, name, typeParameters, parameters, type); + node.questionToken = questionToken; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateMethodSignature(node: ts.MethodSignature, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return node.modifiers !== modifiers - || node.name !== name - || node.questionToken !== questionToken - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateBaseSignatureDeclaration(createMethodSignature(modifiers, name, questionToken, typeParameters, parameters, type), node) - : node; - } + // @api + function updateMethodSignature(node: ts.MethodSignature, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return node.modifiers !== modifiers + || node.name !== name + || node.questionToken !== questionToken + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateBaseSignatureDeclaration(createMethodSignature(modifiers, name, questionToken, typeParameters, parameters, type), node) + : node; + } - // @api - function createMethodDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { - const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.MethodDeclaration, decorators, modifiers, name, typeParameters, parameters, type, body); - node.asteriskToken = asteriskToken; - node.questionToken = questionToken; - node.transformFlags |= - propagateChildFlags(node.asteriskToken) | - propagateChildFlags(node.questionToken) | - ts.TransformFlags.ContainsES2015; - if (questionToken) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { - if (asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsES2018; - } - else { - node.transformFlags |= ts.TransformFlags.ContainsES2017; - } + // @api + function createMethodDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { + const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.MethodDeclaration, decorators, modifiers, name, typeParameters, parameters, type, body); + node.asteriskToken = asteriskToken; + node.questionToken = questionToken; + node.transformFlags |= + propagateChildFlags(node.asteriskToken) | + propagateChildFlags(node.questionToken) | + ts.TransformFlags.ContainsES2015; + if (questionToken) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { + if (asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsES2018; } - else if (asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsGenerator; + else { + node.transformFlags |= ts.TransformFlags.ContainsES2017; } - return node; } - - // @api - function updateMethodDeclaration(node: ts.MethodDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.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 - ? updateBaseFunctionLikeDeclaration(createMethodDeclaration(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body), node) - : node; - } - - // @api - function createClassStaticBlockDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, body: ts.Block): ts.ClassStaticBlockDeclaration { - const node = createBaseGenericNamedDeclaration(ts.SyntaxKind.ClassStaticBlockDeclaration, decorators, modifiers, - /*name*/ undefined, - /*typeParameters*/ undefined); - node.body = body; - node.transformFlags = propagateChildFlags(body) | ts.TransformFlags.ContainsClassFields; - return node; - } - - // @api - function updateClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, body: ts.Block): ts.ClassStaticBlockDeclaration { - return node.decorators !== decorators - || node.modifier !== modifiers - || node.body !== body - ? update(createClassStaticBlockDeclaration(decorators, modifiers, body), node) - : node; + else if (asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsGenerator; } + return node; + } - // @api - function createConstructorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { - const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.Constructor, decorators, modifiers, - /*name*/ undefined, - /*typeParameters*/ undefined, parameters, - /*type*/ undefined, body); - node.transformFlags |= ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function updateMethodDeclaration(node: ts.MethodDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.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 + ? updateBaseFunctionLikeDeclaration(createMethodDeclaration(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body), node) + : node; + } - // @api - function updateConstructorDeclaration(node: ts.ConstructorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.parameters !== parameters - || node.body !== body - ? updateBaseFunctionLikeDeclaration(createConstructorDeclaration(decorators, modifiers, parameters, body), node) - : node; - } + // @api + function createClassStaticBlockDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, body: ts.Block): ts.ClassStaticBlockDeclaration { + const node = createBaseGenericNamedDeclaration(ts.SyntaxKind.ClassStaticBlockDeclaration, decorators, modifiers, + /*name*/ undefined, + /*typeParameters*/ undefined); + node.body = body; + node.transformFlags = propagateChildFlags(body) | ts.TransformFlags.ContainsClassFields; + return node; + } - // @api - function createGetAccessorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { - return createBaseFunctionLikeDeclaration(ts.SyntaxKind.GetAccessor, decorators, modifiers, name, - /*typeParameters*/ undefined, parameters, type, body); - } - // @api - function updateGetAccessorDeclaration(node: ts.GetAccessorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateBaseFunctionLikeDeclaration(createGetAccessorDeclaration(decorators, modifiers, name, parameters, type, body), node) - : node; - } + // @api + function updateClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, body: ts.Block): ts.ClassStaticBlockDeclaration { + return node.decorators !== decorators + || node.modifier !== modifiers + || node.body !== body + ? update(createClassStaticBlockDeclaration(decorators, modifiers, body), node) + : node; + } - // @api - function createSetAccessorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { - return createBaseFunctionLikeDeclaration(ts.SyntaxKind.SetAccessor, decorators, modifiers, name, - /*typeParameters*/ undefined, parameters, - /*type*/ undefined, body); - } - // @api - function updateSetAccessorDeclaration(node: ts.SetAccessorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.parameters !== parameters - || node.body !== body - ? updateBaseFunctionLikeDeclaration(createSetAccessorDeclaration(decorators, modifiers, name, parameters, body), node) - : node; - } + // @api + function createConstructorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { + const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.Constructor, decorators, modifiers, + /*name*/ undefined, + /*typeParameters*/ undefined, parameters, + /*type*/ undefined, body); + node.transformFlags |= ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function createCallSignature(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.CallSignatureDeclaration { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.CallSignature, - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ undefined, typeParameters, parameters, type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateConstructorDeclaration(node: ts.ConstructorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.parameters !== parameters + || node.body !== body + ? updateBaseFunctionLikeDeclaration(createConstructorDeclaration(decorators, modifiers, parameters, body), node) + : node; + } - // @api - function updateCallSignature(node: ts.CallSignatureDeclaration, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateBaseSignatureDeclaration(createCallSignature(typeParameters, parameters, type), node) - : node; - } + // @api + function createGetAccessorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { + return createBaseFunctionLikeDeclaration(ts.SyntaxKind.GetAccessor, decorators, modifiers, name, + /*typeParameters*/ undefined, parameters, type, body); + } + // @api + function updateGetAccessorDeclaration(node: ts.GetAccessorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateBaseFunctionLikeDeclaration(createGetAccessorDeclaration(decorators, modifiers, name, parameters, type, body), node) + : node; + } - // @api - function createConstructSignature(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructSignatureDeclaration { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.ConstructSignature, - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ undefined, typeParameters, parameters, type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createSetAccessorDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { + return createBaseFunctionLikeDeclaration(ts.SyntaxKind.SetAccessor, decorators, modifiers, name, + /*typeParameters*/ undefined, parameters, + /*type*/ undefined, body); + } + // @api + function updateSetAccessorDeclaration(node: ts.SetAccessorDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], body: ts.Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.parameters !== parameters + || node.body !== body + ? updateBaseFunctionLikeDeclaration(createSetAccessorDeclaration(decorators, modifiers, name, parameters, body), node) + : node; + } - // @api - function updateConstructSignature(node: ts.ConstructSignatureDeclaration, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateBaseSignatureDeclaration(createConstructSignature(typeParameters, parameters, type), node) - : node; - } + // @api + function createCallSignature(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.CallSignatureDeclaration { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.CallSignature, + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ undefined, typeParameters, parameters, type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createIndexSignature(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.IndexSignatureDeclaration { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.IndexSignature, decorators, modifiers, - /*name*/ undefined, - /*typeParameters*/ undefined, parameters, type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateCallSignature(node: ts.CallSignatureDeclaration, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateBaseSignatureDeclaration(createCallSignature(typeParameters, parameters, type), node) + : node; + } - // @api - function updateIndexSignature(node: ts.IndexSignatureDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode) { - return node.parameters !== parameters - || node.type !== type - || node.decorators !== decorators - || node.modifiers !== modifiers - ? updateBaseSignatureDeclaration(createIndexSignature(decorators, modifiers, parameters, type), node) - : node; - } + // @api + function createConstructSignature(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructSignatureDeclaration { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.ConstructSignature, + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ undefined, typeParameters, parameters, type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createTemplateLiteralTypeSpan(type: ts.TypeNode, literal: ts.TemplateMiddle | ts.TemplateTail) { - const node = createBaseNode(ts.SyntaxKind.TemplateLiteralTypeSpan); - node.type = type; - node.literal = literal; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateConstructSignature(node: ts.ConstructSignatureDeclaration, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateBaseSignatureDeclaration(createConstructSignature(typeParameters, parameters, type), node) + : node; + } - // @api - function updateTemplateLiteralTypeSpan(node: ts.TemplateLiteralTypeSpan, type: ts.TypeNode, literal: ts.TemplateMiddle | ts.TemplateTail) { - return node.type !== type - || node.literal !== literal - ? update(createTemplateLiteralTypeSpan(type, literal), node) - : node; - } + // @api + function createIndexSignature(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.IndexSignatureDeclaration { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.IndexSignature, decorators, modifiers, + /*name*/ undefined, + /*typeParameters*/ undefined, parameters, type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // - // Types - // + // @api + function updateIndexSignature(node: ts.IndexSignatureDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode) { + return node.parameters !== parameters + || node.type !== type + || node.decorators !== decorators + || node.modifiers !== modifiers + ? updateBaseSignatureDeclaration(createIndexSignature(decorators, modifiers, parameters, type), node) + : node; + } - // @api - function createKeywordTypeNode(kind: TKind) { - return createToken(kind); - } + // @api + function createTemplateLiteralTypeSpan(type: ts.TypeNode, literal: ts.TemplateMiddle | ts.TemplateTail) { + const node = createBaseNode(ts.SyntaxKind.TemplateLiteralTypeSpan); + node.type = type; + node.literal = literal; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createTypePredicateNode(assertsModifier: ts.AssertsKeyword | undefined, parameterName: ts.Identifier | ts.ThisTypeNode | string, type: ts.TypeNode | undefined) { - const node = createBaseNode(ts.SyntaxKind.TypePredicate); - node.assertsModifier = assertsModifier; - node.parameterName = asName(parameterName); - node.type = type; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTemplateLiteralTypeSpan(node: ts.TemplateLiteralTypeSpan, type: ts.TypeNode, literal: ts.TemplateMiddle | ts.TemplateTail) { + return node.type !== type + || node.literal !== literal + ? update(createTemplateLiteralTypeSpan(type, literal), node) + : node; + } - // @api - function updateTypePredicateNode(node: ts.TypePredicateNode, assertsModifier: ts.AssertsKeyword | undefined, parameterName: ts.Identifier | ts.ThisTypeNode, type: ts.TypeNode | undefined) { - return node.assertsModifier !== assertsModifier - || node.parameterName !== parameterName - || node.type !== type - ? update(createTypePredicateNode(assertsModifier, parameterName, type), node) - : node; - } + // + // Types + // - // @api - function createTypeReferenceNode(typeName: string | ts.EntityName, typeArguments: readonly ts.TypeNode[] | undefined) { - const node = createBaseNode(ts.SyntaxKind.TypeReference); - node.typeName = asName(typeName); - node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(createNodeArray(typeArguments)); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createKeywordTypeNode(kind: TKind) { + return createToken(kind); + } - // @api - function updateTypeReferenceNode(node: ts.TypeReferenceNode, typeName: ts.EntityName, typeArguments: ts.NodeArray | undefined) { - return node.typeName !== typeName - || node.typeArguments !== typeArguments - ? update(createTypeReferenceNode(typeName, typeArguments), node) - : node; - } + // @api + function createTypePredicateNode(assertsModifier: ts.AssertsKeyword | undefined, parameterName: ts.Identifier | ts.ThisTypeNode | string, type: ts.TypeNode | undefined) { + const node = createBaseNode(ts.SyntaxKind.TypePredicate); + node.assertsModifier = assertsModifier; + node.parameterName = asName(parameterName); + node.type = type; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createFunctionTypeNode(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.FunctionTypeNode { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.FunctionType, - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ undefined, typeParameters, parameters, type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTypePredicateNode(node: ts.TypePredicateNode, assertsModifier: ts.AssertsKeyword | undefined, parameterName: ts.Identifier | ts.ThisTypeNode, type: ts.TypeNode | undefined) { + return node.assertsModifier !== assertsModifier + || node.parameterName !== parameterName + || node.type !== type + ? update(createTypePredicateNode(assertsModifier, parameterName, type), node) + : node; + } - // @api - function updateFunctionTypeNode(node: ts.FunctionTypeNode, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateBaseSignatureDeclaration(createFunctionTypeNode(typeParameters, parameters, type), node) - : node; - } + // @api + function createTypeReferenceNode(typeName: string | ts.EntityName, typeArguments: readonly ts.TypeNode[] | undefined) { + const node = createBaseNode(ts.SyntaxKind.TypeReference); + node.typeName = asName(typeName); + node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(createNodeArray(typeArguments)); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createConstructorTypeNode(...args: Parameters) { - return args.length === 4 ? createConstructorTypeNode1(...args) : - args.length === 3 ? createConstructorTypeNode2(...args) : - ts.Debug.fail("Incorrect number of arguments specified."); - } - function createConstructorTypeNode1(modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructorTypeNode { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.ConstructorType, - /*decorators*/ undefined, modifiers, - /*name*/ undefined, typeParameters, parameters, type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTypeReferenceNode(node: ts.TypeReferenceNode, typeName: ts.EntityName, typeArguments: ts.NodeArray | undefined) { + return node.typeName !== typeName + || node.typeArguments !== typeArguments + ? update(createTypeReferenceNode(typeName, typeArguments), node) + : node; + } - /** @deprecated */ - function createConstructorTypeNode2(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructorTypeNode { - return createConstructorTypeNode1(/*modifiers*/ undefined, typeParameters, parameters, type); - } + // @api + function createFunctionTypeNode(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.FunctionTypeNode { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.FunctionType, + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ undefined, typeParameters, parameters, type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateConstructorTypeNode(...args: Parameters) { - return args.length === 5 ? updateConstructorTypeNode1(...args) : - args.length === 4 ? updateConstructorTypeNode2(...args) : - ts.Debug.fail("Incorrect number of arguments specified."); - } - function updateConstructorTypeNode1(node: ts.ConstructorTypeNode, modifiers: readonly ts.Modifier[] | undefined, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return node.modifiers !== modifiers - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateBaseSignatureDeclaration(createConstructorTypeNode(modifiers, typeParameters, parameters, type), node) - : node; - } + // @api + function updateFunctionTypeNode(node: ts.FunctionTypeNode, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateBaseSignatureDeclaration(createFunctionTypeNode(typeParameters, parameters, type), node) + : node; + } - /** @deprecated */ - function updateConstructorTypeNode2(node: ts.ConstructorTypeNode, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { - return updateConstructorTypeNode1(node, node.modifiers, typeParameters, parameters, type); - } + // @api + function createConstructorTypeNode(...args: Parameters) { + return args.length === 4 ? createConstructorTypeNode1(...args) : + args.length === 3 ? createConstructorTypeNode2(...args) : + ts.Debug.fail("Incorrect number of arguments specified."); + } + function createConstructorTypeNode1(modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructorTypeNode { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.ConstructorType, + /*decorators*/ undefined, modifiers, + /*name*/ undefined, typeParameters, parameters, type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createTypeQueryNode(exprName: ts.EntityName, typeArguments?: readonly ts.TypeNode[]) { - const node = createBaseNode(ts.SyntaxKind.TypeQuery); - node.exprName = exprName; - node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + /** @deprecated */ + function createConstructorTypeNode2(typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.ConstructorTypeNode { + return createConstructorTypeNode1(/*modifiers*/ undefined, typeParameters, parameters, type); + } - // @api - function updateTypeQueryNode(node: ts.TypeQueryNode, exprName: ts.EntityName, typeArguments?: readonly ts.TypeNode[]) { - return node.exprName !== exprName - || node.typeArguments !== typeArguments - ? update(createTypeQueryNode(exprName, typeArguments), node) - : node; - } + // @api + function updateConstructorTypeNode(...args: Parameters) { + return args.length === 5 ? updateConstructorTypeNode1(...args) : + args.length === 4 ? updateConstructorTypeNode2(...args) : + ts.Debug.fail("Incorrect number of arguments specified."); + } + function updateConstructorTypeNode1(node: ts.ConstructorTypeNode, modifiers: readonly ts.Modifier[] | undefined, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return node.modifiers !== modifiers + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateBaseSignatureDeclaration(createConstructorTypeNode(modifiers, typeParameters, parameters, type), node) + : node; + } - // @api - function createTypeLiteralNode(members: readonly ts.TypeElement[] | undefined) { - const node = createBaseNode(ts.SyntaxKind.TypeLiteral); - node.members = createNodeArray(members); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + /** @deprecated */ + function updateConstructorTypeNode2(node: ts.ConstructorTypeNode, typeParameters: ts.NodeArray | undefined, parameters: ts.NodeArray, type: ts.TypeNode | undefined) { + return updateConstructorTypeNode1(node, node.modifiers, typeParameters, parameters, type); + } - // @api - function updateTypeLiteralNode(node: ts.TypeLiteralNode, members: ts.NodeArray) { - return node.members !== members - ? update(createTypeLiteralNode(members), node) - : node; - } + // @api + function createTypeQueryNode(exprName: ts.EntityName, typeArguments?: readonly ts.TypeNode[]) { + const node = createBaseNode(ts.SyntaxKind.TypeQuery); + node.exprName = exprName; + node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createArrayTypeNode(elementType: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.ArrayType); - node.elementType = parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(elementType); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTypeQueryNode(node: ts.TypeQueryNode, exprName: ts.EntityName, typeArguments?: readonly ts.TypeNode[]) { + return node.exprName !== exprName + || node.typeArguments !== typeArguments + ? update(createTypeQueryNode(exprName, typeArguments), node) + : node; + } - // @api - function updateArrayTypeNode(node: ts.ArrayTypeNode, elementType: ts.TypeNode): ts.ArrayTypeNode { - return node.elementType !== elementType - ? update(createArrayTypeNode(elementType), node) - : node; - } + // @api + function createTypeLiteralNode(members: readonly ts.TypeElement[] | undefined) { + const node = createBaseNode(ts.SyntaxKind.TypeLiteral); + node.members = createNodeArray(members); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createTupleTypeNode(elements: readonly (ts.TypeNode | ts.NamedTupleMember)[]) { - const node = createBaseNode(ts.SyntaxKind.TupleType); - node.elements = createNodeArray(parenthesizerRules().parenthesizeElementTypesOfTupleType(elements)); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTypeLiteralNode(node: ts.TypeLiteralNode, members: ts.NodeArray) { + return node.members !== members + ? update(createTypeLiteralNode(members), node) + : node; + } - // @api - function updateTupleTypeNode(node: ts.TupleTypeNode, elements: readonly (ts.TypeNode | ts.NamedTupleMember)[]) { - return node.elements !== elements - ? update(createTupleTypeNode(elements), node) - : node; - } + // @api + function createArrayTypeNode(elementType: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.ArrayType); + node.elementType = parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(elementType); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createNamedTupleMember(dotDotDotToken: ts.DotDotDotToken | undefined, name: ts.Identifier, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.NamedTupleMember); - node.dotDotDotToken = dotDotDotToken; - node.name = name; - node.questionToken = questionToken; - node.type = type; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateArrayTypeNode(node: ts.ArrayTypeNode, elementType: ts.TypeNode): ts.ArrayTypeNode { + return node.elementType !== elementType + ? update(createArrayTypeNode(elementType), node) + : node; + } - // @api - function updateNamedTupleMember(node: ts.NamedTupleMember, dotDotDotToken: ts.DotDotDotToken | undefined, name: ts.Identifier, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode) { - return node.dotDotDotToken !== dotDotDotToken - || node.name !== name - || node.questionToken !== questionToken - || node.type !== type - ? update(createNamedTupleMember(dotDotDotToken, name, questionToken, type), node) - : node; - } + // @api + function createTupleTypeNode(elements: readonly (ts.TypeNode | ts.NamedTupleMember)[]) { + const node = createBaseNode(ts.SyntaxKind.TupleType); + node.elements = createNodeArray(parenthesizerRules().parenthesizeElementTypesOfTupleType(elements)); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createOptionalTypeNode(type: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.OptionalType); - node.type = parenthesizerRules().parenthesizeTypeOfOptionalType(type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTupleTypeNode(node: ts.TupleTypeNode, elements: readonly (ts.TypeNode | ts.NamedTupleMember)[]) { + return node.elements !== elements + ? update(createTupleTypeNode(elements), node) + : node; + } - // @api - function updateOptionalTypeNode(node: ts.OptionalTypeNode, type: ts.TypeNode): ts.OptionalTypeNode { - return node.type !== type - ? update(createOptionalTypeNode(type), node) - : node; - } + // @api + function createNamedTupleMember(dotDotDotToken: ts.DotDotDotToken | undefined, name: ts.Identifier, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.NamedTupleMember); + node.dotDotDotToken = dotDotDotToken; + node.name = name; + node.questionToken = questionToken; + node.type = type; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createRestTypeNode(type: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.RestType); - node.type = type; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateNamedTupleMember(node: ts.NamedTupleMember, dotDotDotToken: ts.DotDotDotToken | undefined, name: ts.Identifier, questionToken: ts.QuestionToken | undefined, type: ts.TypeNode) { + return node.dotDotDotToken !== dotDotDotToken + || node.name !== name + || node.questionToken !== questionToken + || node.type !== type + ? update(createNamedTupleMember(dotDotDotToken, name, questionToken, type), node) + : node; + } - // @api - function updateRestTypeNode(node: ts.RestTypeNode, type: ts.TypeNode): ts.RestTypeNode { - return node.type !== type - ? update(createRestTypeNode(type), node) - : node; - } + // @api + function createOptionalTypeNode(type: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.OptionalType); + node.type = parenthesizerRules().parenthesizeTypeOfOptionalType(type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - function createUnionOrIntersectionTypeNode(kind: ts.SyntaxKind.UnionType | ts.SyntaxKind.IntersectionType, types: readonly ts.TypeNode[], parenthesize: (nodes: readonly ts.TypeNode[]) => readonly ts.TypeNode[]) { - const node = createBaseNode(kind); - node.types = factory.createNodeArray(parenthesize(types)); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateOptionalTypeNode(node: ts.OptionalTypeNode, type: ts.TypeNode): ts.OptionalTypeNode { + return node.type !== type + ? update(createOptionalTypeNode(type), node) + : node; + } - function updateUnionOrIntersectionTypeNode(node: T, types: ts.NodeArray, parenthesize: (nodes: readonly ts.TypeNode[]) => readonly ts.TypeNode[]): T { - return node.types !== types - ? update(createUnionOrIntersectionTypeNode(node.kind, types, parenthesize) as T, node) - : node; - } + // @api + function createRestTypeNode(type: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.RestType); + node.type = type; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createUnionTypeNode(types: readonly ts.TypeNode[]): ts.UnionTypeNode { - return createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, types, parenthesizerRules().parenthesizeConstituentTypesOfUnionType) as ts.UnionTypeNode; - } + // @api + function updateRestTypeNode(node: ts.RestTypeNode, type: ts.TypeNode): ts.RestTypeNode { + return node.type !== type + ? update(createRestTypeNode(type), node) + : node; + } - // @api - function updateUnionTypeNode(node: ts.UnionTypeNode, types: ts.NodeArray) { - return updateUnionOrIntersectionTypeNode(node, types, parenthesizerRules().parenthesizeConstituentTypesOfUnionType); - } + function createUnionOrIntersectionTypeNode(kind: ts.SyntaxKind.UnionType | ts.SyntaxKind.IntersectionType, types: readonly ts.TypeNode[], parenthesize: (nodes: readonly ts.TypeNode[]) => readonly ts.TypeNode[]) { + const node = createBaseNode(kind); + node.types = factory.createNodeArray(parenthesize(types)); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createIntersectionTypeNode(types: readonly ts.TypeNode[]): ts.IntersectionTypeNode { - return createUnionOrIntersectionTypeNode(ts.SyntaxKind.IntersectionType, types, parenthesizerRules().parenthesizeConstituentTypesOfIntersectionType) as ts.IntersectionTypeNode; - } + function updateUnionOrIntersectionTypeNode(node: T, types: ts.NodeArray, parenthesize: (nodes: readonly ts.TypeNode[]) => readonly ts.TypeNode[]): T { + return node.types !== types + ? update(createUnionOrIntersectionTypeNode(node.kind, types, parenthesize) as T, node) + : node; + } - // @api - function updateIntersectionTypeNode(node: ts.IntersectionTypeNode, types: ts.NodeArray) { - return updateUnionOrIntersectionTypeNode(node, types, parenthesizerRules().parenthesizeConstituentTypesOfIntersectionType); - } + // @api + function createUnionTypeNode(types: readonly ts.TypeNode[]): ts.UnionTypeNode { + return createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, types, parenthesizerRules().parenthesizeConstituentTypesOfUnionType) as ts.UnionTypeNode; + } - // @api - function createConditionalTypeNode(checkType: ts.TypeNode, extendsType: ts.TypeNode, trueType: ts.TypeNode, falseType: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.ConditionalType); - node.checkType = parenthesizerRules().parenthesizeCheckTypeOfConditionalType(checkType); - node.extendsType = parenthesizerRules().parenthesizeExtendsTypeOfConditionalType(extendsType); - node.trueType = trueType; - node.falseType = falseType; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateUnionTypeNode(node: ts.UnionTypeNode, types: ts.NodeArray) { + return updateUnionOrIntersectionTypeNode(node, types, parenthesizerRules().parenthesizeConstituentTypesOfUnionType); + } - // @api - function updateConditionalTypeNode(node: ts.ConditionalTypeNode, checkType: ts.TypeNode, extendsType: ts.TypeNode, trueType: ts.TypeNode, falseType: ts.TypeNode) { - return node.checkType !== checkType - || node.extendsType !== extendsType - || node.trueType !== trueType - || node.falseType !== falseType - ? update(createConditionalTypeNode(checkType, extendsType, trueType, falseType), node) - : node; - } + // @api + function createIntersectionTypeNode(types: readonly ts.TypeNode[]): ts.IntersectionTypeNode { + return createUnionOrIntersectionTypeNode(ts.SyntaxKind.IntersectionType, types, parenthesizerRules().parenthesizeConstituentTypesOfIntersectionType) as ts.IntersectionTypeNode; + } - // @api - function createInferTypeNode(typeParameter: ts.TypeParameterDeclaration) { - const node = createBaseNode(ts.SyntaxKind.InferType); - node.typeParameter = typeParameter; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateIntersectionTypeNode(node: ts.IntersectionTypeNode, types: ts.NodeArray) { + return updateUnionOrIntersectionTypeNode(node, types, parenthesizerRules().parenthesizeConstituentTypesOfIntersectionType); + } - // @api - function updateInferTypeNode(node: ts.InferTypeNode, typeParameter: ts.TypeParameterDeclaration) { - return node.typeParameter !== typeParameter - ? update(createInferTypeNode(typeParameter), node) - : node; - } + // @api + function createConditionalTypeNode(checkType: ts.TypeNode, extendsType: ts.TypeNode, trueType: ts.TypeNode, falseType: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.ConditionalType); + node.checkType = parenthesizerRules().parenthesizeCheckTypeOfConditionalType(checkType); + node.extendsType = parenthesizerRules().parenthesizeExtendsTypeOfConditionalType(extendsType); + node.trueType = trueType; + node.falseType = falseType; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createTemplateLiteralType(head: ts.TemplateHead, templateSpans: readonly ts.TemplateLiteralTypeSpan[]) { - const node = createBaseNode(ts.SyntaxKind.TemplateLiteralType); - node.head = head; - node.templateSpans = createNodeArray(templateSpans); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateConditionalTypeNode(node: ts.ConditionalTypeNode, checkType: ts.TypeNode, extendsType: ts.TypeNode, trueType: ts.TypeNode, falseType: ts.TypeNode) { + return node.checkType !== checkType + || node.extendsType !== extendsType + || node.trueType !== trueType + || node.falseType !== falseType + ? update(createConditionalTypeNode(checkType, extendsType, trueType, falseType), node) + : node; + } - // @api - function updateTemplateLiteralType(node: ts.TemplateLiteralTypeNode, head: ts.TemplateHead, templateSpans: readonly ts.TemplateLiteralTypeSpan[]) { - return node.head !== head - || node.templateSpans !== templateSpans - ? update(createTemplateLiteralType(head, templateSpans), node) - : node; - } + // @api + function createInferTypeNode(typeParameter: ts.TypeParameterDeclaration) { + const node = createBaseNode(ts.SyntaxKind.InferType); + node.typeParameter = typeParameter; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createImportTypeNode(argument: ts.TypeNode, qualifier?: ts.EntityName, typeArguments?: readonly ts.TypeNode[], isTypeOf?: boolean): ts.ImportTypeNode; - function createImportTypeNode(argument: ts.TypeNode, assertions?: ts.ImportTypeAssertionContainer, qualifier?: ts.EntityName, typeArguments?: readonly ts.TypeNode[], isTypeOf?: boolean): ts.ImportTypeNode; - function createImportTypeNode(argument: ts.TypeNode, qualifierOrAssertions?: ts.EntityName | ts.ImportTypeAssertionContainer, typeArgumentsOrQualifier?: readonly ts.TypeNode[] | ts.EntityName, isTypeOfOrTypeArguments?: boolean | readonly ts.TypeNode[], isTypeOf?: boolean) { - const assertion = qualifierOrAssertions && qualifierOrAssertions.kind === ts.SyntaxKind.ImportTypeAssertionContainer ? qualifierOrAssertions : undefined; - const qualifier = qualifierOrAssertions && ts.isEntityName(qualifierOrAssertions) ? qualifierOrAssertions - : typeArgumentsOrQualifier && !ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : undefined; - const typeArguments = ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : ts.isArray(isTypeOfOrTypeArguments) ? isTypeOfOrTypeArguments : undefined; - isTypeOf = typeof isTypeOfOrTypeArguments === "boolean" ? isTypeOfOrTypeArguments : typeof isTypeOf === "boolean" ? isTypeOf : false; - - const node = createBaseNode(ts.SyntaxKind.ImportType); - node.argument = argument; - node.assertions = assertion; - node.qualifier = qualifier; - node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); - node.isTypeOf = isTypeOf; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } - // @api - function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, qualifier: ts.EntityName | undefined, typeArguments: readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined): ts.ImportTypeNode; - function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, assertions: ts.ImportTypeAssertionContainer | undefined, qualifier: ts.EntityName | undefined, typeArguments: readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined): ts.ImportTypeNode; - function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, qualifierOrAssertions: ts.EntityName | ts.ImportTypeAssertionContainer | undefined, typeArgumentsOrQualifier: readonly ts.TypeNode[] | ts.EntityName | undefined, isTypeOfOrTypeArguments: boolean | readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined) { - const assertion = qualifierOrAssertions && qualifierOrAssertions.kind === ts.SyntaxKind.ImportTypeAssertionContainer ? qualifierOrAssertions : undefined; - const qualifier = qualifierOrAssertions && ts.isEntityName(qualifierOrAssertions) ? qualifierOrAssertions - : typeArgumentsOrQualifier && !ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : undefined; - const typeArguments = ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : ts.isArray(isTypeOfOrTypeArguments) ? isTypeOfOrTypeArguments : undefined; - isTypeOf = typeof isTypeOfOrTypeArguments === "boolean" ? isTypeOfOrTypeArguments : typeof isTypeOf === "boolean" ? isTypeOf : node.isTypeOf; - - return node.argument !== argument - || node.assertions !== assertion - || node.qualifier !== qualifier - || node.typeArguments !== typeArguments - || node.isTypeOf !== isTypeOf - ? update(createImportTypeNode(argument, assertion, qualifier, typeArguments, isTypeOf), node) - : node; - } + // @api + function updateInferTypeNode(node: ts.InferTypeNode, typeParameter: ts.TypeParameterDeclaration) { + return node.typeParameter !== typeParameter + ? update(createInferTypeNode(typeParameter), node) + : node; + } - // @api - function createParenthesizedType(type: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.ParenthesizedType); - node.type = type; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createTemplateLiteralType(head: ts.TemplateHead, templateSpans: readonly ts.TemplateLiteralTypeSpan[]) { + const node = createBaseNode(ts.SyntaxKind.TemplateLiteralType); + node.head = head; + node.templateSpans = createNodeArray(templateSpans); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateParenthesizedType(node: ts.ParenthesizedTypeNode, type: ts.TypeNode) { - return node.type !== type - ? update(createParenthesizedType(type), node) - : node; - } + // @api + function updateTemplateLiteralType(node: ts.TemplateLiteralTypeNode, head: ts.TemplateHead, templateSpans: readonly ts.TemplateLiteralTypeSpan[]) { + return node.head !== head + || node.templateSpans !== templateSpans + ? update(createTemplateLiteralType(head, templateSpans), node) + : node; + } - // @api - function createThisTypeNode() { - const node = createBaseNode(ts.SyntaxKind.ThisType); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createImportTypeNode(argument: ts.TypeNode, qualifier?: ts.EntityName, typeArguments?: readonly ts.TypeNode[], isTypeOf?: boolean): ts.ImportTypeNode; + function createImportTypeNode(argument: ts.TypeNode, assertions?: ts.ImportTypeAssertionContainer, qualifier?: ts.EntityName, typeArguments?: readonly ts.TypeNode[], isTypeOf?: boolean): ts.ImportTypeNode; + function createImportTypeNode(argument: ts.TypeNode, qualifierOrAssertions?: ts.EntityName | ts.ImportTypeAssertionContainer, typeArgumentsOrQualifier?: readonly ts.TypeNode[] | ts.EntityName, isTypeOfOrTypeArguments?: boolean | readonly ts.TypeNode[], isTypeOf?: boolean) { + const assertion = qualifierOrAssertions && qualifierOrAssertions.kind === ts.SyntaxKind.ImportTypeAssertionContainer ? qualifierOrAssertions : undefined; + const qualifier = qualifierOrAssertions && ts.isEntityName(qualifierOrAssertions) ? qualifierOrAssertions + : typeArgumentsOrQualifier && !ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : undefined; + const typeArguments = ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : ts.isArray(isTypeOfOrTypeArguments) ? isTypeOfOrTypeArguments : undefined; + isTypeOf = typeof isTypeOfOrTypeArguments === "boolean" ? isTypeOfOrTypeArguments : typeof isTypeOf === "boolean" ? isTypeOf : false; + + const node = createBaseNode(ts.SyntaxKind.ImportType); + node.argument = argument; + node.assertions = assertion; + node.qualifier = qualifier; + node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); + node.isTypeOf = isTypeOf; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } + // @api + function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, qualifier: ts.EntityName | undefined, typeArguments: readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined): ts.ImportTypeNode; + function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, assertions: ts.ImportTypeAssertionContainer | undefined, qualifier: ts.EntityName | undefined, typeArguments: readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined): ts.ImportTypeNode; + function updateImportTypeNode(node: ts.ImportTypeNode, argument: ts.TypeNode, qualifierOrAssertions: ts.EntityName | ts.ImportTypeAssertionContainer | undefined, typeArgumentsOrQualifier: readonly ts.TypeNode[] | ts.EntityName | undefined, isTypeOfOrTypeArguments: boolean | readonly ts.TypeNode[] | undefined, isTypeOf?: boolean | undefined) { + const assertion = qualifierOrAssertions && qualifierOrAssertions.kind === ts.SyntaxKind.ImportTypeAssertionContainer ? qualifierOrAssertions : undefined; + const qualifier = qualifierOrAssertions && ts.isEntityName(qualifierOrAssertions) ? qualifierOrAssertions + : typeArgumentsOrQualifier && !ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : undefined; + const typeArguments = ts.isArray(typeArgumentsOrQualifier) ? typeArgumentsOrQualifier : ts.isArray(isTypeOfOrTypeArguments) ? isTypeOfOrTypeArguments : undefined; + isTypeOf = typeof isTypeOfOrTypeArguments === "boolean" ? isTypeOfOrTypeArguments : typeof isTypeOf === "boolean" ? isTypeOf : node.isTypeOf; + + return node.argument !== argument + || node.assertions !== assertion + || node.qualifier !== qualifier + || node.typeArguments !== typeArguments + || node.isTypeOf !== isTypeOf + ? update(createImportTypeNode(argument, assertion, qualifier, typeArguments, isTypeOf), node) + : node; + } - // @api - function createTypeOperatorNode(operator: ts.SyntaxKind.KeyOfKeyword | ts.SyntaxKind.UniqueKeyword | ts.SyntaxKind.ReadonlyKeyword, type: ts.TypeNode): ts.TypeOperatorNode { - const node = createBaseNode(ts.SyntaxKind.TypeOperator); - node.operator = operator; - node.type = operator === ts.SyntaxKind.ReadonlyKeyword ? - parenthesizerRules().parenthesizeOperandOfReadonlyTypeOperator(type) : - parenthesizerRules().parenthesizeOperandOfTypeOperator(type); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createParenthesizedType(type: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.ParenthesizedType); + node.type = type; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateTypeOperatorNode(node: ts.TypeOperatorNode, type: ts.TypeNode) { - return node.type !== type - ? update(createTypeOperatorNode(node.operator, type), node) - : node; - } + // @api + function updateParenthesizedType(node: ts.ParenthesizedTypeNode, type: ts.TypeNode) { + return node.type !== type + ? update(createParenthesizedType(type), node) + : node; + } - // @api - function createIndexedAccessTypeNode(objectType: ts.TypeNode, indexType: ts.TypeNode) { - const node = createBaseNode(ts.SyntaxKind.IndexedAccessType); - node.objectType = parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(objectType); - node.indexType = indexType; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createThisTypeNode() { + const node = createBaseNode(ts.SyntaxKind.ThisType); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateIndexedAccessTypeNode(node: ts.IndexedAccessTypeNode, objectType: ts.TypeNode, indexType: ts.TypeNode) { - return node.objectType !== objectType - || node.indexType !== indexType - ? update(createIndexedAccessTypeNode(objectType, indexType), node) - : node; - } + // @api + function createTypeOperatorNode(operator: ts.SyntaxKind.KeyOfKeyword | ts.SyntaxKind.UniqueKeyword | ts.SyntaxKind.ReadonlyKeyword, type: ts.TypeNode): ts.TypeOperatorNode { + const node = createBaseNode(ts.SyntaxKind.TypeOperator); + node.operator = operator; + node.type = operator === ts.SyntaxKind.ReadonlyKeyword ? + parenthesizerRules().parenthesizeOperandOfReadonlyTypeOperator(type) : + parenthesizerRules().parenthesizeOperandOfTypeOperator(type); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createMappedTypeNode(readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined, typeParameter: ts.TypeParameterDeclaration, nameType: ts.TypeNode | undefined, questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined, type: ts.TypeNode | undefined, members: readonly ts.TypeElement[] | undefined): ts.MappedTypeNode { - const node = createBaseNode(ts.SyntaxKind.MappedType); - node.readonlyToken = readonlyToken; - node.typeParameter = typeParameter; - node.nameType = nameType; - node.questionToken = questionToken; - node.type = type; - node.members = members && createNodeArray(members); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateTypeOperatorNode(node: ts.TypeOperatorNode, type: ts.TypeNode) { + return node.type !== type + ? update(createTypeOperatorNode(node.operator, type), node) + : node; + } - // @api - function updateMappedTypeNode(node: ts.MappedTypeNode, readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined, typeParameter: ts.TypeParameterDeclaration, nameType: ts.TypeNode | undefined, questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined, type: ts.TypeNode | undefined, members: readonly ts.TypeElement[] | undefined): ts.MappedTypeNode { - return node.readonlyToken !== readonlyToken - || node.typeParameter !== typeParameter - || node.nameType !== nameType - || node.questionToken !== questionToken - || node.type !== type - || node.members !== members - ? update(createMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, type, members), node) - : node; - } + // @api + function createIndexedAccessTypeNode(objectType: ts.TypeNode, indexType: ts.TypeNode) { + const node = createBaseNode(ts.SyntaxKind.IndexedAccessType); + node.objectType = parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(objectType); + node.indexType = indexType; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createLiteralTypeNode(literal: ts.LiteralTypeNode["literal"]) { - const node = createBaseNode(ts.SyntaxKind.LiteralType); - node.literal = literal; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateIndexedAccessTypeNode(node: ts.IndexedAccessTypeNode, objectType: ts.TypeNode, indexType: ts.TypeNode) { + return node.objectType !== objectType + || node.indexType !== indexType + ? update(createIndexedAccessTypeNode(objectType, indexType), node) + : node; + } - // @api - function updateLiteralTypeNode(node: ts.LiteralTypeNode, literal: ts.LiteralTypeNode["literal"]) { - return node.literal !== literal - ? update(createLiteralTypeNode(literal), node) - : node; - } + // @api + function createMappedTypeNode(readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined, typeParameter: ts.TypeParameterDeclaration, nameType: ts.TypeNode | undefined, questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined, type: ts.TypeNode | undefined, members: readonly ts.TypeElement[] | undefined): ts.MappedTypeNode { + const node = createBaseNode(ts.SyntaxKind.MappedType); + node.readonlyToken = readonlyToken; + node.typeParameter = typeParameter; + node.nameType = nameType; + node.questionToken = questionToken; + node.type = type; + node.members = members && createNodeArray(members); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // - // Binding Patterns - // + // @api + function updateMappedTypeNode(node: ts.MappedTypeNode, readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined, typeParameter: ts.TypeParameterDeclaration, nameType: ts.TypeNode | undefined, questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined, type: ts.TypeNode | undefined, members: readonly ts.TypeElement[] | undefined): ts.MappedTypeNode { + return node.readonlyToken !== readonlyToken + || node.typeParameter !== typeParameter + || node.nameType !== nameType + || node.questionToken !== questionToken + || node.type !== type + || node.members !== members + ? update(createMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, type, members), node) + : node; + } - // @api - function createObjectBindingPattern(elements: readonly ts.BindingElement[]) { - const node = createBaseNode(ts.SyntaxKind.ObjectBindingPattern); - node.elements = createNodeArray(elements); - node.transformFlags |= - propagateChildrenFlags(node.elements) | - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsBindingPattern; - if (node.transformFlags & ts.TransformFlags.ContainsRestOrSpread) { - node.transformFlags |= - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsObjectRestOrSpread; - } - return node; - } + // @api + function createLiteralTypeNode(literal: ts.LiteralTypeNode["literal"]) { + const node = createBaseNode(ts.SyntaxKind.LiteralType); + node.literal = literal; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateObjectBindingPattern(node: ts.ObjectBindingPattern, elements: readonly ts.BindingElement[]) { - return node.elements !== elements - ? update(createObjectBindingPattern(elements), node) - : node; - } + // @api + function updateLiteralTypeNode(node: ts.LiteralTypeNode, literal: ts.LiteralTypeNode["literal"]) { + return node.literal !== literal + ? update(createLiteralTypeNode(literal), node) + : node; + } - // @api - function createArrayBindingPattern(elements: readonly ts.ArrayBindingElement[]) { - const node = createBaseNode(ts.SyntaxKind.ArrayBindingPattern); - node.elements = createNodeArray(elements); + // + // Binding Patterns + // + + // @api + function createObjectBindingPattern(elements: readonly ts.BindingElement[]) { + const node = createBaseNode(ts.SyntaxKind.ObjectBindingPattern); + node.elements = createNodeArray(elements); + node.transformFlags |= + propagateChildrenFlags(node.elements) | + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsBindingPattern; + if (node.transformFlags & ts.TransformFlags.ContainsRestOrSpread) { node.transformFlags |= - propagateChildrenFlags(node.elements) | - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsBindingPattern; - return node; + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsObjectRestOrSpread; } + return node; + } - // @api - function updateArrayBindingPattern(node: ts.ArrayBindingPattern, elements: readonly ts.ArrayBindingElement[]) { - return node.elements !== elements - ? update(createArrayBindingPattern(elements), node) - : node; - } + // @api + function updateObjectBindingPattern(node: ts.ObjectBindingPattern, elements: readonly ts.BindingElement[]) { + return node.elements !== elements + ? update(createObjectBindingPattern(elements), node) + : node; + } - // @api - function createBindingElement(dotDotDotToken: ts.DotDotDotToken | undefined, propertyName: string | ts.PropertyName | undefined, name: string | ts.BindingName, initializer?: ts.Expression) { - const node = createBaseBindingLikeDeclaration(ts.SyntaxKind.BindingElement, - /*decorators*/ undefined, - /*modifiers*/ undefined, name, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); - node.propertyName = asName(propertyName); - node.dotDotDotToken = dotDotDotToken; - node.transformFlags |= - propagateChildFlags(node.dotDotDotToken) | - ts.TransformFlags.ContainsES2015; - if (node.propertyName) { - node.transformFlags |= ts.isIdentifier(node.propertyName) ? - propagateIdentifierNameFlags(node.propertyName) : - propagateChildFlags(node.propertyName); - } - if (dotDotDotToken) - node.transformFlags |= ts.TransformFlags.ContainsRestOrSpread; - return node; - } + // @api + function createArrayBindingPattern(elements: readonly ts.ArrayBindingElement[]) { + const node = createBaseNode(ts.SyntaxKind.ArrayBindingPattern); + node.elements = createNodeArray(elements); + node.transformFlags |= + propagateChildrenFlags(node.elements) | + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsBindingPattern; + return node; + } - // @api - function updateBindingElement(node: ts.BindingElement, dotDotDotToken: ts.DotDotDotToken | undefined, propertyName: ts.PropertyName | undefined, name: ts.BindingName, initializer: ts.Expression | undefined) { - return node.propertyName !== propertyName - || node.dotDotDotToken !== dotDotDotToken - || node.name !== name - || node.initializer !== initializer - ? update(createBindingElement(dotDotDotToken, propertyName, name, initializer), node) - : node; - } + // @api + function updateArrayBindingPattern(node: ts.ArrayBindingPattern, elements: readonly ts.ArrayBindingElement[]) { + return node.elements !== elements + ? update(createArrayBindingPattern(elements), node) + : node; + } - // - // Expression - // + // @api + function createBindingElement(dotDotDotToken: ts.DotDotDotToken | undefined, propertyName: string | ts.PropertyName | undefined, name: string | ts.BindingName, initializer?: ts.Expression) { + const node = createBaseBindingLikeDeclaration(ts.SyntaxKind.BindingElement, + /*decorators*/ undefined, + /*modifiers*/ undefined, name, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); + node.propertyName = asName(propertyName); + node.dotDotDotToken = dotDotDotToken; + node.transformFlags |= + propagateChildFlags(node.dotDotDotToken) | + ts.TransformFlags.ContainsES2015; + if (node.propertyName) { + node.transformFlags |= ts.isIdentifier(node.propertyName) ? + propagateIdentifierNameFlags(node.propertyName) : + propagateChildFlags(node.propertyName); + } + if (dotDotDotToken) + node.transformFlags |= ts.TransformFlags.ContainsRestOrSpread; + return node; + } - function createBaseExpression(kind: T["kind"]) { - const node = createBaseNode(kind); - // the following properties are commonly set by the checker/binder - return node; - } + // @api + function updateBindingElement(node: ts.BindingElement, dotDotDotToken: ts.DotDotDotToken | undefined, propertyName: ts.PropertyName | undefined, name: ts.BindingName, initializer: ts.Expression | undefined) { + return node.propertyName !== propertyName + || node.dotDotDotToken !== dotDotDotToken + || node.name !== name + || node.initializer !== initializer + ? update(createBindingElement(dotDotDotToken, propertyName, name, initializer), node) + : node; + } - // @api - function createArrayLiteralExpression(elements?: readonly ts.Expression[], multiLine?: boolean) { - const node = createBaseExpression(ts.SyntaxKind.ArrayLiteralExpression); - // Ensure we add a trailing comma for something like `[NumericLiteral(1), NumericLiteral(2), OmittedExpresion]` so that - // we end up with `[1, 2, ,]` instead of `[1, 2, ]` otherwise the `OmittedExpression` will just end up being treated like - // a trailing comma. - const lastElement = elements && ts.lastOrUndefined(elements); - const elementsArray = createNodeArray(elements, lastElement && ts.isOmittedExpression(lastElement) ? true : undefined); - node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(elementsArray); - node.multiLine = multiLine; - node.transformFlags |= propagateChildrenFlags(node.elements); - return node; - } + // + // Expression + // - // @api - function updateArrayLiteralExpression(node: ts.ArrayLiteralExpression, elements: readonly ts.Expression[]) { - return node.elements !== elements - ? update(createArrayLiteralExpression(elements, node.multiLine), node) - : node; - } + function createBaseExpression(kind: T["kind"]) { + const node = createBaseNode(kind); + // the following properties are commonly set by the checker/binder + return node; + } - // @api - function createObjectLiteralExpression(properties?: readonly ts.ObjectLiteralElementLike[], multiLine?: boolean) { - const node = createBaseExpression(ts.SyntaxKind.ObjectLiteralExpression); - node.properties = createNodeArray(properties); - node.multiLine = multiLine; - node.transformFlags |= propagateChildrenFlags(node.properties); - return node; - } + // @api + function createArrayLiteralExpression(elements?: readonly ts.Expression[], multiLine?: boolean) { + const node = createBaseExpression(ts.SyntaxKind.ArrayLiteralExpression); + // Ensure we add a trailing comma for something like `[NumericLiteral(1), NumericLiteral(2), OmittedExpresion]` so that + // we end up with `[1, 2, ,]` instead of `[1, 2, ]` otherwise the `OmittedExpression` will just end up being treated like + // a trailing comma. + const lastElement = elements && ts.lastOrUndefined(elements); + const elementsArray = createNodeArray(elements, lastElement && ts.isOmittedExpression(lastElement) ? true : undefined); + node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(elementsArray); + node.multiLine = multiLine; + node.transformFlags |= propagateChildrenFlags(node.elements); + return node; + } - // @api - function updateObjectLiteralExpression(node: ts.ObjectLiteralExpression, properties: readonly ts.ObjectLiteralElementLike[]) { - return node.properties !== properties - ? update(createObjectLiteralExpression(properties, node.multiLine), node) - : node; - } + // @api + function updateArrayLiteralExpression(node: ts.ArrayLiteralExpression, elements: readonly ts.Expression[]) { + return node.elements !== elements + ? update(createArrayLiteralExpression(elements, node.multiLine), node) + : node; + } - // @api - function createPropertyAccessExpression(expression: ts.Expression, name: string | ts.Identifier | ts.PrivateIdentifier) { - const node = createBaseExpression(ts.SyntaxKind.PropertyAccessExpression); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.name = asName(name); - node.transformFlags = - propagateChildFlags(node.expression) | - (ts.isIdentifier(node.name) ? - propagateIdentifierNameFlags(node.name) : - propagateChildFlags(node.name)); - if (ts.isSuperKeyword(expression)) { - // super method calls require a lexical 'this' - // super method calls require 'super' hoisting in ES2017 and ES2018 async functions and async generators - node.transformFlags |= - ts.TransformFlags.ContainsES2017 | - ts.TransformFlags.ContainsES2018; - } - return node; - } + // @api + function createObjectLiteralExpression(properties?: readonly ts.ObjectLiteralElementLike[], multiLine?: boolean) { + const node = createBaseExpression(ts.SyntaxKind.ObjectLiteralExpression); + node.properties = createNodeArray(properties); + node.multiLine = multiLine; + node.transformFlags |= propagateChildrenFlags(node.properties); + return node; + } - // @api - function updatePropertyAccessExpression(node: ts.PropertyAccessExpression, expression: ts.Expression, name: ts.Identifier | ts.PrivateIdentifier) { - if (ts.isPropertyAccessChain(node)) { - return updatePropertyAccessChain(node, expression, node.questionDotToken, ts.cast(name, ts.isIdentifier)); - } - return node.expression !== expression - || node.name !== name - ? update(createPropertyAccessExpression(expression, name), node) - : node; - } + // @api + function updateObjectLiteralExpression(node: ts.ObjectLiteralExpression, properties: readonly ts.ObjectLiteralElementLike[]) { + return node.properties !== properties + ? update(createObjectLiteralExpression(properties, node.multiLine), node) + : node; + } - // @api - function createPropertyAccessChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, name: string | ts.Identifier | ts.PrivateIdentifier) { - const node = createBaseExpression(ts.SyntaxKind.PropertyAccessExpression); - node.flags |= ts.NodeFlags.OptionalChain; - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.questionDotToken = questionDotToken; - node.name = asName(name); + // @api + function createPropertyAccessExpression(expression: ts.Expression, name: string | ts.Identifier | ts.PrivateIdentifier) { + const node = createBaseExpression(ts.SyntaxKind.PropertyAccessExpression); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.name = asName(name); + node.transformFlags = + propagateChildFlags(node.expression) | + (ts.isIdentifier(node.name) ? + propagateIdentifierNameFlags(node.name) : + propagateChildFlags(node.name)); + if (ts.isSuperKeyword(expression)) { + // super method calls require a lexical 'this' + // super method calls require 'super' hoisting in ES2017 and ES2018 async functions and async generators node.transformFlags |= - ts.TransformFlags.ContainsES2020 | - propagateChildFlags(node.expression) | - propagateChildFlags(node.questionDotToken) | - (ts.isIdentifier(node.name) ? - propagateIdentifierNameFlags(node.name) : - propagateChildFlags(node.name)); - return node; + ts.TransformFlags.ContainsES2017 | + ts.TransformFlags.ContainsES2018; } + return node; + } - // @api - function updatePropertyAccessChain(node: ts.PropertyAccessChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, name: ts.Identifier | ts.PrivateIdentifier) { - ts.Debug.assert(!!(node.flags & ts.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 - ? update(createPropertyAccessChain(expression, questionDotToken, name), node) - : node; + // @api + function updatePropertyAccessExpression(node: ts.PropertyAccessExpression, expression: ts.Expression, name: ts.Identifier | ts.PrivateIdentifier) { + if (ts.isPropertyAccessChain(node)) { + return updatePropertyAccessChain(node, expression, node.questionDotToken, ts.cast(name, ts.isIdentifier)); } + return node.expression !== expression + || node.name !== name + ? update(createPropertyAccessExpression(expression, name), node) + : node; + } - // @api - function createElementAccessExpression(expression: ts.Expression, index: number | ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.ElementAccessExpression); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.argumentExpression = asExpression(index); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.argumentExpression); - if (ts.isSuperKeyword(expression)) { - // super method calls require a lexical 'this' - // super method calls require 'super' hoisting in ES2017 and ES2018 async functions and async generators - node.transformFlags |= - ts.TransformFlags.ContainsES2017 | - ts.TransformFlags.ContainsES2018; - } - return node; - } + // @api + function createPropertyAccessChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, name: string | ts.Identifier | ts.PrivateIdentifier) { + const node = createBaseExpression(ts.SyntaxKind.PropertyAccessExpression); + node.flags |= ts.NodeFlags.OptionalChain; + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.questionDotToken = questionDotToken; + node.name = asName(name); + node.transformFlags |= + ts.TransformFlags.ContainsES2020 | + propagateChildFlags(node.expression) | + propagateChildFlags(node.questionDotToken) | + (ts.isIdentifier(node.name) ? + propagateIdentifierNameFlags(node.name) : + propagateChildFlags(node.name)); + return node; + } - // @api - function updateElementAccessExpression(node: ts.ElementAccessExpression, expression: ts.Expression, argumentExpression: ts.Expression) { - if (ts.isElementAccessChain(node)) { - return updateElementAccessChain(node, expression, node.questionDotToken, argumentExpression); - } - return node.expression !== expression - || node.argumentExpression !== argumentExpression - ? update(createElementAccessExpression(expression, argumentExpression), node) - : node; - } + // @api + function updatePropertyAccessChain(node: ts.PropertyAccessChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, name: ts.Identifier | ts.PrivateIdentifier) { + ts.Debug.assert(!!(node.flags & ts.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 + ? update(createPropertyAccessChain(expression, questionDotToken, name), node) + : node; + } - // @api - function createElementAccessChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, index: number | ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.ElementAccessExpression); - node.flags |= ts.NodeFlags.OptionalChain; - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.questionDotToken = questionDotToken; - node.argumentExpression = asExpression(index); + // @api + function createElementAccessExpression(expression: ts.Expression, index: number | ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.ElementAccessExpression); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.argumentExpression = asExpression(index); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.argumentExpression); + if (ts.isSuperKeyword(expression)) { + // super method calls require a lexical 'this' + // super method calls require 'super' hoisting in ES2017 and ES2018 async functions and async generators node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.questionDotToken) | - propagateChildFlags(node.argumentExpression) | - ts.TransformFlags.ContainsES2020; - return node; + ts.TransformFlags.ContainsES2017 | + ts.TransformFlags.ContainsES2018; } + return node; + } - // @api - function updateElementAccessChain(node: ts.ElementAccessChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, argumentExpression: ts.Expression) { - ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a ElementAccessExpression using updateElementAccessChain. Use updateElementAccess instead."); - // Because we are updating an existing ElementAccessChain we want to inherit its emitFlags - // instead of using the default from createElementAccess - return node.expression !== expression - || node.questionDotToken !== questionDotToken - || node.argumentExpression !== argumentExpression - ? update(createElementAccessChain(expression, questionDotToken, argumentExpression), node) - : node; + // @api + function updateElementAccessExpression(node: ts.ElementAccessExpression, expression: ts.Expression, argumentExpression: ts.Expression) { + if (ts.isElementAccessChain(node)) { + return updateElementAccessChain(node, expression, node.questionDotToken, argumentExpression); } + return node.expression !== expression + || node.argumentExpression !== argumentExpression + ? update(createElementAccessExpression(expression, argumentExpression), node) + : node; + } - // @api - function createCallExpression(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { - const node = createBaseExpression(ts.SyntaxKind.CallExpression); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.typeArguments = asNodeArray(typeArguments); - node.arguments = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(argumentsArray)); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildrenFlags(node.typeArguments) | - propagateChildrenFlags(node.arguments); - if (node.typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - if (ts.isImportKeyword(node.expression)) { - node.transformFlags |= ts.TransformFlags.ContainsDynamicImport; - } - else if (ts.isSuperProperty(node.expression)) { - node.transformFlags |= ts.TransformFlags.ContainsLexicalThis; - } - return node; - } + // @api + function createElementAccessChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, index: number | ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.ElementAccessExpression); + node.flags |= ts.NodeFlags.OptionalChain; + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.questionDotToken = questionDotToken; + node.argumentExpression = asExpression(index); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.questionDotToken) | + propagateChildFlags(node.argumentExpression) | + ts.TransformFlags.ContainsES2020; + return node; + } - // @api - function updateCallExpression(node: ts.CallExpression, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[]) { - if (ts.isCallChain(node)) { - return updateCallChain(node, expression, node.questionDotToken, typeArguments, argumentsArray); - } - return node.expression !== expression - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? update(createCallExpression(expression, typeArguments, argumentsArray), node) - : node; - } + // @api + function updateElementAccessChain(node: ts.ElementAccessChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, argumentExpression: ts.Expression) { + ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a ElementAccessExpression using updateElementAccessChain. Use updateElementAccess instead."); + // Because we are updating an existing ElementAccessChain we want to inherit its emitFlags + // instead of using the default from createElementAccess + return node.expression !== expression + || node.questionDotToken !== questionDotToken + || node.argumentExpression !== argumentExpression + ? update(createElementAccessChain(expression, questionDotToken, argumentExpression), node) + : node; + } - // @api - function createCallChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { - const node = createBaseExpression(ts.SyntaxKind.CallExpression); - node.flags |= ts.NodeFlags.OptionalChain; - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.questionDotToken = questionDotToken; - node.typeArguments = asNodeArray(typeArguments); - node.arguments = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(argumentsArray)); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.questionDotToken) | - propagateChildrenFlags(node.typeArguments) | - propagateChildrenFlags(node.arguments) | - ts.TransformFlags.ContainsES2020; - if (node.typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - if (ts.isSuperProperty(node.expression)) { - node.transformFlags |= ts.TransformFlags.ContainsLexicalThis; - } - return node; + // @api + function createCallExpression(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { + const node = createBaseExpression(ts.SyntaxKind.CallExpression); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.typeArguments = asNodeArray(typeArguments); + node.arguments = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(argumentsArray)); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildrenFlags(node.typeArguments) | + propagateChildrenFlags(node.arguments); + if (node.typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + if (ts.isImportKeyword(node.expression)) { + node.transformFlags |= ts.TransformFlags.ContainsDynamicImport; + } + else if (ts.isSuperProperty(node.expression)) { + node.transformFlags |= ts.TransformFlags.ContainsLexicalThis; } + return node; + } - // @api - function updateCallChain(node: ts.CallChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[]) { - ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a CallExpression using updateCallChain. Use updateCall instead."); - return node.expression !== expression - || node.questionDotToken !== questionDotToken - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? update(createCallChain(expression, questionDotToken, typeArguments, argumentsArray), node) - : node; + // @api + function updateCallExpression(node: ts.CallExpression, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[]) { + if (ts.isCallChain(node)) { + return updateCallChain(node, expression, node.questionDotToken, typeArguments, argumentsArray); } + return node.expression !== expression + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? update(createCallExpression(expression, typeArguments, argumentsArray), node) + : node; + } - // @api - function createNewExpression(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { - const node = createBaseExpression(ts.SyntaxKind.NewExpression); - node.expression = parenthesizerRules().parenthesizeExpressionOfNew(expression); - node.typeArguments = asNodeArray(typeArguments); - node.arguments = argumentsArray ? parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(argumentsArray) : undefined; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildrenFlags(node.typeArguments) | - propagateChildrenFlags(node.arguments) | - ts.TransformFlags.ContainsES2020; - if (node.typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - return node; + // @api + function createCallChain(expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { + const node = createBaseExpression(ts.SyntaxKind.CallExpression); + node.flags |= ts.NodeFlags.OptionalChain; + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.questionDotToken = questionDotToken; + node.typeArguments = asNodeArray(typeArguments); + node.arguments = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(argumentsArray)); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.questionDotToken) | + propagateChildrenFlags(node.typeArguments) | + propagateChildrenFlags(node.arguments) | + ts.TransformFlags.ContainsES2020; + if (node.typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + if (ts.isSuperProperty(node.expression)) { + node.transformFlags |= ts.TransformFlags.ContainsLexicalThis; } + return node; + } - // @api - function updateNewExpression(node: ts.NewExpression, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { - return node.expression !== expression - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? update(createNewExpression(expression, typeArguments, argumentsArray), node) - : node; - } + // @api + function updateCallChain(node: ts.CallChain, expression: ts.Expression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[]) { + ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a CallExpression using updateCallChain. Use updateCall instead."); + return node.expression !== expression + || node.questionDotToken !== questionDotToken + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? update(createCallChain(expression, questionDotToken, typeArguments, argumentsArray), node) + : node; + } - // @api - function createTaggedTemplateExpression(tag: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, template: ts.TemplateLiteral) { - const node = createBaseExpression(ts.SyntaxKind.TaggedTemplateExpression); - node.tag = parenthesizerRules().parenthesizeLeftSideOfAccess(tag); - node.typeArguments = asNodeArray(typeArguments); - node.template = template; - node.transformFlags |= - propagateChildFlags(node.tag) | - propagateChildrenFlags(node.typeArguments) | - propagateChildFlags(node.template) | - ts.TransformFlags.ContainsES2015; - if (node.typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - if (ts.hasInvalidEscape(node.template)) { - node.transformFlags |= ts.TransformFlags.ContainsES2018; - } - return node; + // @api + function createNewExpression(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { + const node = createBaseExpression(ts.SyntaxKind.NewExpression); + node.expression = parenthesizerRules().parenthesizeExpressionOfNew(expression); + node.typeArguments = asNodeArray(typeArguments); + node.arguments = argumentsArray ? parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(argumentsArray) : undefined; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildrenFlags(node.typeArguments) | + propagateChildrenFlags(node.arguments) | + ts.TransformFlags.ContainsES2020; + if (node.typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function updateTaggedTemplateExpression(node: ts.TaggedTemplateExpression, tag: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, template: ts.TemplateLiteral) { - return node.tag !== tag - || node.typeArguments !== typeArguments - || node.template !== template - ? update(createTaggedTemplateExpression(tag, typeArguments, template), node) - : node; - } + // @api + function updateNewExpression(node: ts.NewExpression, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, argumentsArray: readonly ts.Expression[] | undefined) { + return node.expression !== expression + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? update(createNewExpression(expression, typeArguments, argumentsArray), node) + : node; + } - // @api - function createTypeAssertion(type: ts.TypeNode, expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.TypeAssertionExpression); - node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); - node.type = type; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.type) | - ts.TransformFlags.ContainsTypeScript; - return node; + // @api + function createTaggedTemplateExpression(tag: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, template: ts.TemplateLiteral) { + const node = createBaseExpression(ts.SyntaxKind.TaggedTemplateExpression); + node.tag = parenthesizerRules().parenthesizeLeftSideOfAccess(tag); + node.typeArguments = asNodeArray(typeArguments); + node.template = template; + node.transformFlags |= + propagateChildFlags(node.tag) | + propagateChildrenFlags(node.typeArguments) | + propagateChildFlags(node.template) | + ts.TransformFlags.ContainsES2015; + if (node.typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + if (ts.hasInvalidEscape(node.template)) { + node.transformFlags |= ts.TransformFlags.ContainsES2018; } + return node; + } - // @api - function updateTypeAssertion(node: ts.TypeAssertion, type: ts.TypeNode, expression: ts.Expression) { - return node.type !== type - || node.expression !== expression - ? update(createTypeAssertion(type, expression), node) - : node; - } + // @api + function updateTaggedTemplateExpression(node: ts.TaggedTemplateExpression, tag: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined, template: ts.TemplateLiteral) { + return node.tag !== tag + || node.typeArguments !== typeArguments + || node.template !== template + ? update(createTaggedTemplateExpression(tag, typeArguments, template), node) + : node; + } - // @api - function createParenthesizedExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.ParenthesizedExpression); - node.expression = expression; - node.transformFlags = propagateChildFlags(node.expression); - return node; - } + // @api + function createTypeAssertion(type: ts.TypeNode, expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.TypeAssertionExpression); + node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); + node.type = type; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.type) | + ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateParenthesizedExpression(node: ts.ParenthesizedExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createParenthesizedExpression(expression), node) - : node; - } + // @api + function updateTypeAssertion(node: ts.TypeAssertion, type: ts.TypeNode, expression: ts.Expression) { + return node.type !== type + || node.expression !== expression + ? update(createTypeAssertion(type, expression), node) + : node; + } - // @api - function createFunctionExpression(modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined, body: ts.Block) { - const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.FunctionExpression, - /*decorators*/ undefined, modifiers, name, typeParameters, parameters, type, body); - node.asteriskToken = asteriskToken; - node.transformFlags |= propagateChildFlags(node.asteriskToken); - if (node.typeParameters) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { - if (node.asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsES2018; - } - else { - node.transformFlags |= ts.TransformFlags.ContainsES2017; - } + // @api + function createParenthesizedExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.ParenthesizedExpression); + node.expression = expression; + node.transformFlags = propagateChildFlags(node.expression); + return node; + } + + // @api + function updateParenthesizedExpression(node: ts.ParenthesizedExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createParenthesizedExpression(expression), node) + : node; + } + + // @api + function createFunctionExpression(modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[] | undefined, type: ts.TypeNode | undefined, body: ts.Block) { + const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.FunctionExpression, + /*decorators*/ undefined, modifiers, name, typeParameters, parameters, type, body); + node.asteriskToken = asteriskToken; + node.transformFlags |= propagateChildFlags(node.asteriskToken); + if (node.typeParameters) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { + if (node.asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsES2018; } - else if (node.asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsGenerator; + else { + node.transformFlags |= ts.TransformFlags.ContainsES2017; } - return node; } - - // @api - function updateFunctionExpression(node: ts.FunctionExpression, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block) { - return node.name !== name - || node.modifiers !== modifiers - || node.asteriskToken !== asteriskToken - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateBaseFunctionLikeDeclaration(createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) - : node; + else if (node.asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsGenerator; } + return node; + } - // @api - function createArrowFunction(modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, equalsGreaterThanToken: ts.EqualsGreaterThanToken | undefined, body: ts.ConciseBody) { - const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.ArrowFunction, - /*decorators*/ undefined, modifiers, - /*name*/ undefined, typeParameters, parameters, type, parenthesizerRules().parenthesizeConciseBodyOfArrowFunction(body)); - node.equalsGreaterThanToken = equalsGreaterThanToken ?? createToken(ts.SyntaxKind.EqualsGreaterThanToken); - node.transformFlags |= - propagateChildFlags(node.equalsGreaterThanToken) | - ts.TransformFlags.ContainsES2015; - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { - node.transformFlags |= ts.TransformFlags.ContainsES2017 | ts.TransformFlags.ContainsLexicalThis; - } - return node; - } + // @api + function updateFunctionExpression(node: ts.FunctionExpression, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block) { + return node.name !== name + || node.modifiers !== modifiers + || node.asteriskToken !== asteriskToken + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateBaseFunctionLikeDeclaration(createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) + : node; + } - // @api - function updateArrowFunction(node: ts.ArrowFunction, modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, equalsGreaterThanToken: ts.EqualsGreaterThanToken, body: ts.ConciseBody): ts.ArrowFunction { - return node.modifiers !== modifiers - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.equalsGreaterThanToken !== equalsGreaterThanToken - || node.body !== body - ? updateBaseFunctionLikeDeclaration(createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body), node) - : node; + // @api + function createArrowFunction(modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, equalsGreaterThanToken: ts.EqualsGreaterThanToken | undefined, body: ts.ConciseBody) { + const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.ArrowFunction, + /*decorators*/ undefined, modifiers, + /*name*/ undefined, typeParameters, parameters, type, parenthesizerRules().parenthesizeConciseBodyOfArrowFunction(body)); + node.equalsGreaterThanToken = equalsGreaterThanToken ?? createToken(ts.SyntaxKind.EqualsGreaterThanToken); + node.transformFlags |= + propagateChildFlags(node.equalsGreaterThanToken) | + ts.TransformFlags.ContainsES2015; + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { + node.transformFlags |= ts.TransformFlags.ContainsES2017 | ts.TransformFlags.ContainsLexicalThis; } + return node; + } - // @api - function createDeleteExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.DeleteExpression); - node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); - node.transformFlags |= propagateChildFlags(node.expression); - return node; - } + // @api + function updateArrowFunction(node: ts.ArrowFunction, modifiers: readonly ts.Modifier[] | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, equalsGreaterThanToken: ts.EqualsGreaterThanToken, body: ts.ConciseBody): ts.ArrowFunction { + return node.modifiers !== modifiers + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.equalsGreaterThanToken !== equalsGreaterThanToken + || node.body !== body + ? updateBaseFunctionLikeDeclaration(createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body), node) + : node; + } - // @api - function updateDeleteExpression(node: ts.DeleteExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createDeleteExpression(expression), node) - : node; - } + // @api + function createDeleteExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.DeleteExpression); + node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); + node.transformFlags |= propagateChildFlags(node.expression); + return node; + } - // @api - function createTypeOfExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.TypeOfExpression); - node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); - node.transformFlags |= propagateChildFlags(node.expression); - return node; - } + // @api + function updateDeleteExpression(node: ts.DeleteExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createDeleteExpression(expression), node) + : node; + } - // @api - function updateTypeOfExpression(node: ts.TypeOfExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createTypeOfExpression(expression), node) - : node; - } + // @api + function createTypeOfExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.TypeOfExpression); + node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); + node.transformFlags |= propagateChildFlags(node.expression); + return node; + } - // @api - function createVoidExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.VoidExpression); - node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); - node.transformFlags |= propagateChildFlags(node.expression); - return node; - } + // @api + function updateTypeOfExpression(node: ts.TypeOfExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createTypeOfExpression(expression), node) + : node; + } - // @api - function updateVoidExpression(node: ts.VoidExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createVoidExpression(expression), node) - : node; - } + // @api + function createVoidExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.VoidExpression); + node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); + node.transformFlags |= propagateChildFlags(node.expression); + return node; + } - // @api - function createAwaitExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.AwaitExpression); - node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsES2017 | - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsAwait; - return node; - } + // @api + function updateVoidExpression(node: ts.VoidExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createVoidExpression(expression), node) + : node; + } - // @api - function updateAwaitExpression(node: ts.AwaitExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createAwaitExpression(expression), node) - : node; - } + // @api + function createAwaitExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.AwaitExpression); + node.expression = parenthesizerRules().parenthesizeOperandOfPrefixUnary(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsES2017 | + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsAwait; + return node; + } - // @api - function createPrefixUnaryExpression(operator: ts.PrefixUnaryOperator, operand: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.PrefixUnaryExpression); - node.operator = operator; - node.operand = parenthesizerRules().parenthesizeOperandOfPrefixUnary(operand); - node.transformFlags |= propagateChildFlags(node.operand); - // Only set this flag for non-generated identifiers and non-"local" names. See the - // comment in `visitPreOrPostfixUnaryExpression` in module.ts - if ((operator === ts.SyntaxKind.PlusPlusToken || operator === ts.SyntaxKind.MinusMinusToken) && - ts.isIdentifier(node.operand) && - !ts.isGeneratedIdentifier(node.operand) && - !ts.isLocalName(node.operand)) { - node.transformFlags |= ts.TransformFlags.ContainsUpdateExpressionForIdentifier; - } - return node; - } + // @api + function updateAwaitExpression(node: ts.AwaitExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createAwaitExpression(expression), node) + : node; + } - // @api - function updatePrefixUnaryExpression(node: ts.PrefixUnaryExpression, operand: ts.Expression) { - return node.operand !== operand - ? update(createPrefixUnaryExpression(node.operator, operand), node) - : node; + // @api + function createPrefixUnaryExpression(operator: ts.PrefixUnaryOperator, operand: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.PrefixUnaryExpression); + node.operator = operator; + node.operand = parenthesizerRules().parenthesizeOperandOfPrefixUnary(operand); + node.transformFlags |= propagateChildFlags(node.operand); + // Only set this flag for non-generated identifiers and non-"local" names. See the + // comment in `visitPreOrPostfixUnaryExpression` in module.ts + if ((operator === ts.SyntaxKind.PlusPlusToken || operator === ts.SyntaxKind.MinusMinusToken) && + ts.isIdentifier(node.operand) && + !ts.isGeneratedIdentifier(node.operand) && + !ts.isLocalName(node.operand)) { + node.transformFlags |= ts.TransformFlags.ContainsUpdateExpressionForIdentifier; } + return node; + } - // @api - function createPostfixUnaryExpression(operand: ts.Expression, operator: ts.PostfixUnaryOperator) { - const node = createBaseExpression(ts.SyntaxKind.PostfixUnaryExpression); - node.operator = operator; - node.operand = parenthesizerRules().parenthesizeOperandOfPostfixUnary(operand); - node.transformFlags |= propagateChildFlags(node.operand); - // Only set this flag for non-generated identifiers and non-"local" names. See the - // comment in `visitPreOrPostfixUnaryExpression` in module.ts - if (ts.isIdentifier(node.operand) && - !ts.isGeneratedIdentifier(node.operand) && - !ts.isLocalName(node.operand)) { - node.transformFlags |= ts.TransformFlags.ContainsUpdateExpressionForIdentifier; - } - return node; - } + // @api + function updatePrefixUnaryExpression(node: ts.PrefixUnaryExpression, operand: ts.Expression) { + return node.operand !== operand + ? update(createPrefixUnaryExpression(node.operator, operand), node) + : node; + } - // @api - function updatePostfixUnaryExpression(node: ts.PostfixUnaryExpression, operand: ts.Expression) { - return node.operand !== operand - ? update(createPostfixUnaryExpression(operand, node.operator), node) - : node; + // @api + function createPostfixUnaryExpression(operand: ts.Expression, operator: ts.PostfixUnaryOperator) { + const node = createBaseExpression(ts.SyntaxKind.PostfixUnaryExpression); + node.operator = operator; + node.operand = parenthesizerRules().parenthesizeOperandOfPostfixUnary(operand); + node.transformFlags |= propagateChildFlags(node.operand); + // Only set this flag for non-generated identifiers and non-"local" names. See the + // comment in `visitPreOrPostfixUnaryExpression` in module.ts + if (ts.isIdentifier(node.operand) && + !ts.isGeneratedIdentifier(node.operand) && + !ts.isLocalName(node.operand)) { + node.transformFlags |= ts.TransformFlags.ContainsUpdateExpressionForIdentifier; } + return node; + } - // @api - function createBinaryExpression(left: ts.Expression, operator: ts.BinaryOperator | ts.BinaryOperatorToken, right: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.BinaryExpression); - const operatorToken = asToken(operator); - const operatorKind = operatorToken.kind; - node.left = parenthesizerRules().parenthesizeLeftSideOfBinary(operatorKind, left); - node.operatorToken = operatorToken; - node.right = parenthesizerRules().parenthesizeRightSideOfBinary(operatorKind, node.left, right); - node.transformFlags |= - propagateChildFlags(node.left) | - propagateChildFlags(node.operatorToken) | - propagateChildFlags(node.right); - if (operatorKind === ts.SyntaxKind.QuestionQuestionToken) { - node.transformFlags |= ts.TransformFlags.ContainsES2020; - } - else if (operatorKind === ts.SyntaxKind.EqualsToken) { - if (ts.isObjectLiteralExpression(node.left)) { - node.transformFlags |= - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsDestructuringAssignment | - propagateAssignmentPatternFlags(node.left); - } - else if (ts.isArrayLiteralExpression(node.left)) { - node.transformFlags |= - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsDestructuringAssignment | - propagateAssignmentPatternFlags(node.left); - } - } - else if (operatorKind === ts.SyntaxKind.AsteriskAsteriskToken || operatorKind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) { - node.transformFlags |= ts.TransformFlags.ContainsES2016; + // @api + function updatePostfixUnaryExpression(node: ts.PostfixUnaryExpression, operand: ts.Expression) { + return node.operand !== operand + ? update(createPostfixUnaryExpression(operand, node.operator), node) + : node; + } + + // @api + function createBinaryExpression(left: ts.Expression, operator: ts.BinaryOperator | ts.BinaryOperatorToken, right: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.BinaryExpression); + const operatorToken = asToken(operator); + const operatorKind = operatorToken.kind; + node.left = parenthesizerRules().parenthesizeLeftSideOfBinary(operatorKind, left); + node.operatorToken = operatorToken; + node.right = parenthesizerRules().parenthesizeRightSideOfBinary(operatorKind, node.left, right); + node.transformFlags |= + propagateChildFlags(node.left) | + propagateChildFlags(node.operatorToken) | + propagateChildFlags(node.right); + if (operatorKind === ts.SyntaxKind.QuestionQuestionToken) { + node.transformFlags |= ts.TransformFlags.ContainsES2020; + } + else if (operatorKind === ts.SyntaxKind.EqualsToken) { + if (ts.isObjectLiteralExpression(node.left)) { + node.transformFlags |= + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsDestructuringAssignment | + propagateAssignmentPatternFlags(node.left); } - else if (ts.isLogicalOrCoalescingAssignmentOperator(operatorKind)) { - node.transformFlags |= ts.TransformFlags.ContainsES2021; + else if (ts.isArrayLiteralExpression(node.left)) { + node.transformFlags |= + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsDestructuringAssignment | + propagateAssignmentPatternFlags(node.left); } - return node; } + else if (operatorKind === ts.SyntaxKind.AsteriskAsteriskToken || operatorKind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) { + node.transformFlags |= ts.TransformFlags.ContainsES2016; + } + else if (ts.isLogicalOrCoalescingAssignmentOperator(operatorKind)) { + node.transformFlags |= ts.TransformFlags.ContainsES2021; + } + return node; + } - function propagateAssignmentPatternFlags(node: ts.AssignmentPattern): ts.TransformFlags { - if (node.transformFlags & ts.TransformFlags.ContainsObjectRestOrSpread) - return ts.TransformFlags.ContainsObjectRestOrSpread; - if (node.transformFlags & ts.TransformFlags.ContainsES2018) { - // check for nested spread assignments, otherwise '{ x: { a, ...b } = foo } = c' - // will not be correctly interpreted by the ES2018 transformer - for (const element of ts.getElementsOfBindingOrAssignmentPattern(node)) { - const target = ts.getTargetOfBindingOrAssignmentElement(element); - if (target && ts.isAssignmentPattern(target)) { - if (target.transformFlags & ts.TransformFlags.ContainsObjectRestOrSpread) { - return ts.TransformFlags.ContainsObjectRestOrSpread; - } - if (target.transformFlags & ts.TransformFlags.ContainsES2018) { - const flags = propagateAssignmentPatternFlags(target); - if (flags) - return flags; - } + function propagateAssignmentPatternFlags(node: ts.AssignmentPattern): ts.TransformFlags { + if (node.transformFlags & ts.TransformFlags.ContainsObjectRestOrSpread) + return ts.TransformFlags.ContainsObjectRestOrSpread; + if (node.transformFlags & ts.TransformFlags.ContainsES2018) { + // check for nested spread assignments, otherwise '{ x: { a, ...b } = foo } = c' + // will not be correctly interpreted by the ES2018 transformer + for (const element of ts.getElementsOfBindingOrAssignmentPattern(node)) { + const target = ts.getTargetOfBindingOrAssignmentElement(element); + if (target && ts.isAssignmentPattern(target)) { + if (target.transformFlags & ts.TransformFlags.ContainsObjectRestOrSpread) { + return ts.TransformFlags.ContainsObjectRestOrSpread; + } + if (target.transformFlags & ts.TransformFlags.ContainsES2018) { + const flags = propagateAssignmentPatternFlags(target); + if (flags) + return flags; } } } - return ts.TransformFlags.None; } + return ts.TransformFlags.None; + } - // @api - function updateBinaryExpression(node: ts.BinaryExpression, left: ts.Expression, operator: ts.BinaryOperatorToken, right: ts.Expression) { - return node.left !== left - || node.operatorToken !== operator - || node.right !== right - ? update(createBinaryExpression(left, operator, right), node) - : node; - } + // @api + function updateBinaryExpression(node: ts.BinaryExpression, left: ts.Expression, operator: ts.BinaryOperatorToken, right: ts.Expression) { + return node.left !== left + || node.operatorToken !== operator + || node.right !== right + ? update(createBinaryExpression(left, operator, right), node) + : node; + } - // @api - function createConditionalExpression(condition: ts.Expression, questionToken: ts.QuestionToken | undefined, whenTrue: ts.Expression, colonToken: ts.ColonToken | undefined, whenFalse: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.ConditionalExpression); - node.condition = parenthesizerRules().parenthesizeConditionOfConditionalExpression(condition); - node.questionToken = questionToken ?? createToken(ts.SyntaxKind.QuestionToken); - node.whenTrue = parenthesizerRules().parenthesizeBranchOfConditionalExpression(whenTrue); - node.colonToken = colonToken ?? createToken(ts.SyntaxKind.ColonToken); - node.whenFalse = parenthesizerRules().parenthesizeBranchOfConditionalExpression(whenFalse); - node.transformFlags |= - propagateChildFlags(node.condition) | - propagateChildFlags(node.questionToken) | - propagateChildFlags(node.whenTrue) | - propagateChildFlags(node.colonToken) | - propagateChildFlags(node.whenFalse); - return node; - } + // @api + function createConditionalExpression(condition: ts.Expression, questionToken: ts.QuestionToken | undefined, whenTrue: ts.Expression, colonToken: ts.ColonToken | undefined, whenFalse: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.ConditionalExpression); + node.condition = parenthesizerRules().parenthesizeConditionOfConditionalExpression(condition); + node.questionToken = questionToken ?? createToken(ts.SyntaxKind.QuestionToken); + node.whenTrue = parenthesizerRules().parenthesizeBranchOfConditionalExpression(whenTrue); + node.colonToken = colonToken ?? createToken(ts.SyntaxKind.ColonToken); + node.whenFalse = parenthesizerRules().parenthesizeBranchOfConditionalExpression(whenFalse); + node.transformFlags |= + propagateChildFlags(node.condition) | + propagateChildFlags(node.questionToken) | + propagateChildFlags(node.whenTrue) | + propagateChildFlags(node.colonToken) | + propagateChildFlags(node.whenFalse); + return node; + } - // @api - function updateConditionalExpression(node: ts.ConditionalExpression, condition: ts.Expression, questionToken: ts.Token, whenTrue: ts.Expression, colonToken: ts.Token, whenFalse: ts.Expression): ts.ConditionalExpression { - return node.condition !== condition - || node.questionToken !== questionToken - || node.whenTrue !== whenTrue - || node.colonToken !== colonToken - || node.whenFalse !== whenFalse - ? update(createConditionalExpression(condition, questionToken, whenTrue, colonToken, whenFalse), node) - : node; - } + // @api + function updateConditionalExpression(node: ts.ConditionalExpression, condition: ts.Expression, questionToken: ts.Token, whenTrue: ts.Expression, colonToken: ts.Token, whenFalse: ts.Expression): ts.ConditionalExpression { + return node.condition !== condition + || node.questionToken !== questionToken + || node.whenTrue !== whenTrue + || node.colonToken !== colonToken + || node.whenFalse !== whenFalse + ? update(createConditionalExpression(condition, questionToken, whenTrue, colonToken, whenFalse), node) + : node; + } - // @api - function createTemplateExpression(head: ts.TemplateHead, templateSpans: readonly ts.TemplateSpan[]) { - const node = createBaseExpression(ts.SyntaxKind.TemplateExpression); - node.head = head; - node.templateSpans = createNodeArray(templateSpans); - node.transformFlags |= - propagateChildFlags(node.head) | - propagateChildrenFlags(node.templateSpans) | - ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function createTemplateExpression(head: ts.TemplateHead, templateSpans: readonly ts.TemplateSpan[]) { + const node = createBaseExpression(ts.SyntaxKind.TemplateExpression); + node.head = head; + node.templateSpans = createNodeArray(templateSpans); + node.transformFlags |= + propagateChildFlags(node.head) | + propagateChildrenFlags(node.templateSpans) | + ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function updateTemplateExpression(node: ts.TemplateExpression, head: ts.TemplateHead, templateSpans: readonly ts.TemplateSpan[]) { - return node.head !== head - || node.templateSpans !== templateSpans - ? update(createTemplateExpression(head, templateSpans), node) - : node; - } + // @api + function updateTemplateExpression(node: ts.TemplateExpression, head: ts.TemplateHead, templateSpans: readonly ts.TemplateSpan[]) { + return node.head !== head + || node.templateSpans !== templateSpans + ? update(createTemplateExpression(head, templateSpans), node) + : node; + } - function createTemplateLiteralLikeNodeChecked(kind: ts.TemplateLiteralToken["kind"], text: string | undefined, rawText: string | undefined, templateFlags = ts.TokenFlags.None) { - ts.Debug.assert(!(templateFlags & ~ts.TokenFlags.TemplateLiteralLikeFlags), "Unsupported template flags."); - // NOTE: without the assignment to `undefined`, we don't narrow the initial type of `cooked`. - // eslint-disable-next-line no-undef-init - let cooked: string | object | undefined = undefined; - if (rawText !== undefined && rawText !== text) { - cooked = getCookedText(kind, rawText); - if (typeof cooked === "object") { - return ts.Debug.fail("Invalid raw text"); - } - } - if (text === undefined) { - if (cooked === undefined) { - return ts.Debug.fail("Arguments 'text' and 'rawText' may not both be undefined."); - } - text = cooked; - } - else if (cooked !== undefined) { - ts.Debug.assert(text === cooked, "Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'."); + function createTemplateLiteralLikeNodeChecked(kind: ts.TemplateLiteralToken["kind"], text: string | undefined, rawText: string | undefined, templateFlags = ts.TokenFlags.None) { + ts.Debug.assert(!(templateFlags & ~ts.TokenFlags.TemplateLiteralLikeFlags), "Unsupported template flags."); + // NOTE: without the assignment to `undefined`, we don't narrow the initial type of `cooked`. + // eslint-disable-next-line no-undef-init + let cooked: string | object | undefined = undefined; + if (rawText !== undefined && rawText !== text) { + cooked = getCookedText(kind, rawText); + if (typeof cooked === "object") { + return ts.Debug.fail("Invalid raw text"); } - return createTemplateLiteralLikeNode(kind, text, rawText, templateFlags); } - - // @api - function createTemplateLiteralLikeNode(kind: ts.TemplateLiteralToken["kind"], text: string, rawText: string | undefined, templateFlags: ts.TokenFlags | undefined) { - const node = createBaseToken(kind); - node.text = text; - node.rawText = rawText; - node.templateFlags = templateFlags! & ts.TokenFlags.TemplateLiteralLikeFlags; - node.transformFlags |= ts.TransformFlags.ContainsES2015; - if (node.templateFlags) { - node.transformFlags |= ts.TransformFlags.ContainsES2018; + if (text === undefined) { + if (cooked === undefined) { + return ts.Debug.fail("Arguments 'text' and 'rawText' may not both be undefined."); } - return node; + text = cooked; } - - // @api - function createTemplateHead(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { - return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateHead, text, rawText, templateFlags) as ts.TemplateHead; + else if (cooked !== undefined) { + ts.Debug.assert(text === cooked, "Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'."); } + return createTemplateLiteralLikeNode(kind, text, rawText, templateFlags); + } - // @api - function createTemplateMiddle(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { - return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateMiddle, text, rawText, templateFlags) as ts.TemplateMiddle; + // @api + function createTemplateLiteralLikeNode(kind: ts.TemplateLiteralToken["kind"], text: string, rawText: string | undefined, templateFlags: ts.TokenFlags | undefined) { + const node = createBaseToken(kind); + node.text = text; + node.rawText = rawText; + node.templateFlags = templateFlags! & ts.TokenFlags.TemplateLiteralLikeFlags; + node.transformFlags |= ts.TransformFlags.ContainsES2015; + if (node.templateFlags) { + node.transformFlags |= ts.TransformFlags.ContainsES2018; } + return node; + } - // @api - function createTemplateTail(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { - return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateTail, text, rawText, templateFlags) as ts.TemplateTail; - } + // @api + function createTemplateHead(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { + return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateHead, text, rawText, templateFlags) as ts.TemplateHead; + } - // @api - function createNoSubstitutionTemplateLiteral(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { - return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.NoSubstitutionTemplateLiteral, text, rawText, templateFlags) as ts.NoSubstitutionTemplateLiteral; - } + // @api + function createTemplateMiddle(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { + return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateMiddle, text, rawText, templateFlags) as ts.TemplateMiddle; + } - // @api - function createYieldExpression(asteriskToken: ts.AsteriskToken | undefined, expression: ts.Expression | undefined): ts.YieldExpression { - ts.Debug.assert(!asteriskToken || !!expression, "A `YieldExpression` with an asteriskToken must have an expression."); - const node = createBaseExpression(ts.SyntaxKind.YieldExpression); - node.expression = expression && parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.asteriskToken = asteriskToken; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.asteriskToken) | - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsYield; - return node; - } + // @api + function createTemplateTail(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { + return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.TemplateTail, text, rawText, templateFlags) as ts.TemplateTail; + } - // @api - function updateYieldExpression(node: ts.YieldExpression, asteriskToken: ts.AsteriskToken | undefined, expression: ts.Expression) { - return node.expression !== expression - || node.asteriskToken !== asteriskToken - ? update(createYieldExpression(asteriskToken, expression), node) - : node; - } + // @api + function createNoSubstitutionTemplateLiteral(text: string | undefined, rawText?: string, templateFlags?: ts.TokenFlags) { + return createTemplateLiteralLikeNodeChecked(ts.SyntaxKind.NoSubstitutionTemplateLiteral, text, rawText, templateFlags) as ts.NoSubstitutionTemplateLiteral; + } - // @api - function createSpreadElement(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.SpreadElement); - node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsRestOrSpread; - return node; - } + // @api + function createYieldExpression(asteriskToken: ts.AsteriskToken | undefined, expression: ts.Expression | undefined): ts.YieldExpression { + ts.Debug.assert(!asteriskToken || !!expression, "A `YieldExpression` with an asteriskToken must have an expression."); + const node = createBaseExpression(ts.SyntaxKind.YieldExpression); + node.expression = expression && parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.asteriskToken = asteriskToken; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.asteriskToken) | + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsYield; + return node; + } - // @api - function updateSpreadElement(node: ts.SpreadElement, expression: ts.Expression) { - return node.expression !== expression - ? update(createSpreadElement(expression), node) - : node; - } + // @api + function updateYieldExpression(node: ts.YieldExpression, asteriskToken: ts.AsteriskToken | undefined, expression: ts.Expression) { + return node.expression !== expression + || node.asteriskToken !== asteriskToken + ? update(createYieldExpression(asteriskToken, expression), node) + : node; + } - // @api - function createClassExpression(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { - const node = createBaseClassLikeDeclaration(ts.SyntaxKind.ClassExpression, decorators, modifiers, name, typeParameters, heritageClauses, members); - node.transformFlags |= ts.TransformFlags.ContainsES2015; - return node; - } - // @api - function updateClassExpression(node: ts.ClassExpression, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? update(createClassExpression(decorators, modifiers, name, typeParameters, heritageClauses, members), node) - : node; - } + // @api + function createSpreadElement(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.SpreadElement); + node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsRestOrSpread; + return node; + } - // @api - function createOmittedExpression() { - return createBaseExpression(ts.SyntaxKind.OmittedExpression); - } + // @api + function updateSpreadElement(node: ts.SpreadElement, expression: ts.Expression) { + return node.expression !== expression + ? update(createSpreadElement(expression), node) + : node; + } + + // @api + function createClassExpression(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { + const node = createBaseClassLikeDeclaration(ts.SyntaxKind.ClassExpression, decorators, modifiers, name, typeParameters, heritageClauses, members); + node.transformFlags |= ts.TransformFlags.ContainsES2015; + return node; + } + // @api + function updateClassExpression(node: ts.ClassExpression, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? update(createClassExpression(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; + } - // @api - function createExpressionWithTypeArguments(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined) { - const node = createBaseNode(ts.SyntaxKind.ExpressionWithTypeArguments); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildrenFlags(node.typeArguments) | - ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function createOmittedExpression() { + return createBaseExpression(ts.SyntaxKind.OmittedExpression); + } - // @api - function updateExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined) { - return node.expression !== expression - || node.typeArguments !== typeArguments - ? update(createExpressionWithTypeArguments(expression, typeArguments), node) - : node; - } + // @api + function createExpressionWithTypeArguments(expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined) { + const node = createBaseNode(ts.SyntaxKind.ExpressionWithTypeArguments); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildrenFlags(node.typeArguments) | + ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function createAsExpression(expression: ts.Expression, type: ts.TypeNode) { - const node = createBaseExpression(ts.SyntaxKind.AsExpression); - node.expression = expression; - node.type = type; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.type) | - ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments, expression: ts.Expression, typeArguments: readonly ts.TypeNode[] | undefined) { + return node.expression !== expression + || node.typeArguments !== typeArguments + ? update(createExpressionWithTypeArguments(expression, typeArguments), node) + : node; + } - // @api - function updateAsExpression(node: ts.AsExpression, expression: ts.Expression, type: ts.TypeNode) { - return node.expression !== expression - || node.type !== type - ? update(createAsExpression(expression, type), node) - : node; - } + // @api + function createAsExpression(expression: ts.Expression, type: ts.TypeNode) { + const node = createBaseExpression(ts.SyntaxKind.AsExpression); + node.expression = expression; + node.type = type; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.type) | + ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createNonNullExpression(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.NonNullExpression); - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateAsExpression(node: ts.AsExpression, expression: ts.Expression, type: ts.TypeNode) { + return node.expression !== expression + || node.type !== type + ? update(createAsExpression(expression, type), node) + : node; + } - // @api - function updateNonNullExpression(node: ts.NonNullExpression, expression: ts.Expression) { - if (ts.isNonNullChain(node)) { - return updateNonNullChain(node, expression); - } - return node.expression !== expression - ? update(createNonNullExpression(expression), node) - : node; - } + // @api + function createNonNullExpression(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.NonNullExpression); + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createNonNullChain(expression: ts.Expression) { - const node = createBaseExpression(ts.SyntaxKind.NonNullExpression); - node.flags |= ts.NodeFlags.OptionalChain; - node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsTypeScript; - return node; + // @api + function updateNonNullExpression(node: ts.NonNullExpression, expression: ts.Expression) { + if (ts.isNonNullChain(node)) { + return updateNonNullChain(node, expression); } + return node.expression !== expression + ? update(createNonNullExpression(expression), node) + : node; + } - // @api - function updateNonNullChain(node: ts.NonNullChain, expression: ts.Expression) { - ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a NonNullExpression using updateNonNullChain. Use updateNonNullExpression instead."); - return node.expression !== expression - ? update(createNonNullChain(expression), node) - : node; - } + // @api + function createNonNullChain(expression: ts.Expression) { + const node = createBaseExpression(ts.SyntaxKind.NonNullExpression); + node.flags |= ts.NodeFlags.OptionalChain; + node.expression = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createMetaProperty(keywordToken: ts.MetaProperty["keywordToken"], name: ts.Identifier) { - const node = createBaseExpression(ts.SyntaxKind.MetaProperty); - node.keywordToken = keywordToken; - node.name = name; - node.transformFlags |= propagateChildFlags(node.name); - switch (keywordToken) { - case ts.SyntaxKind.NewKeyword: - node.transformFlags |= ts.TransformFlags.ContainsES2015; - break; - case ts.SyntaxKind.ImportKeyword: - node.transformFlags |= ts.TransformFlags.ContainsESNext; - break; - default: - return ts.Debug.assertNever(keywordToken); - } - return node; - } + // @api + function updateNonNullChain(node: ts.NonNullChain, expression: ts.Expression) { + ts.Debug.assert(!!(node.flags & ts.NodeFlags.OptionalChain), "Cannot update a NonNullExpression using updateNonNullChain. Use updateNonNullExpression instead."); + return node.expression !== expression + ? update(createNonNullChain(expression), node) + : node; + } - // @api - function updateMetaProperty(node: ts.MetaProperty, name: ts.Identifier) { - return node.name !== name - ? update(createMetaProperty(node.keywordToken, name), node) - : node; + // @api + function createMetaProperty(keywordToken: ts.MetaProperty["keywordToken"], name: ts.Identifier) { + const node = createBaseExpression(ts.SyntaxKind.MetaProperty); + node.keywordToken = keywordToken; + node.name = name; + node.transformFlags |= propagateChildFlags(node.name); + switch (keywordToken) { + case ts.SyntaxKind.NewKeyword: + node.transformFlags |= ts.TransformFlags.ContainsES2015; + break; + case ts.SyntaxKind.ImportKeyword: + node.transformFlags |= ts.TransformFlags.ContainsESNext; + break; + default: + return ts.Debug.assertNever(keywordToken); } + return node; + } - // - // Misc - // + // @api + function updateMetaProperty(node: ts.MetaProperty, name: ts.Identifier) { + return node.name !== name + ? update(createMetaProperty(node.keywordToken, name), node) + : node; + } - // @api - function createTemplateSpan(expression: ts.Expression, literal: ts.TemplateMiddle | ts.TemplateTail) { - const node = createBaseNode(ts.SyntaxKind.TemplateSpan); - node.expression = expression; - node.literal = literal; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.literal) | - ts.TransformFlags.ContainsES2015; - return node; - } + // + // Misc + // + + // @api + function createTemplateSpan(expression: ts.Expression, literal: ts.TemplateMiddle | ts.TemplateTail) { + const node = createBaseNode(ts.SyntaxKind.TemplateSpan); + node.expression = expression; + node.literal = literal; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.literal) | + ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function updateTemplateSpan(node: ts.TemplateSpan, expression: ts.Expression, literal: ts.TemplateMiddle | ts.TemplateTail) { - return node.expression !== expression - || node.literal !== literal - ? update(createTemplateSpan(expression, literal), node) - : node; - } + // @api + function updateTemplateSpan(node: ts.TemplateSpan, expression: ts.Expression, literal: ts.TemplateMiddle | ts.TemplateTail) { + return node.expression !== expression + || node.literal !== literal + ? update(createTemplateSpan(expression, literal), node) + : node; + } - // @api - function createSemicolonClassElement() { - const node = createBaseNode(ts.SyntaxKind.SemicolonClassElement); - node.transformFlags |= ts.TransformFlags.ContainsES2015; - return node; - } + // @api + function createSemicolonClassElement() { + const node = createBaseNode(ts.SyntaxKind.SemicolonClassElement); + node.transformFlags |= ts.TransformFlags.ContainsES2015; + return node; + } - // - // Element - // + // + // Element + // - // @api - function createBlock(statements: readonly ts.Statement[], multiLine?: boolean): ts.Block { - const node = createBaseNode(ts.SyntaxKind.Block); - node.statements = createNodeArray(statements); - node.multiLine = multiLine; - node.transformFlags |= propagateChildrenFlags(node.statements); - return node; - } + // @api + function createBlock(statements: readonly ts.Statement[], multiLine?: boolean): ts.Block { + const node = createBaseNode(ts.SyntaxKind.Block); + node.statements = createNodeArray(statements); + node.multiLine = multiLine; + node.transformFlags |= propagateChildrenFlags(node.statements); + return node; + } - // @api - function updateBlock(node: ts.Block, statements: readonly ts.Statement[]) { - return node.statements !== statements - ? update(createBlock(statements, node.multiLine), node) - : node; - } + // @api + function updateBlock(node: ts.Block, statements: readonly ts.Statement[]) { + return node.statements !== statements + ? update(createBlock(statements, node.multiLine), node) + : node; + } - // @api - function createVariableStatement(modifiers: readonly ts.Modifier[] | undefined, declarationList: ts.VariableDeclarationList | readonly ts.VariableDeclaration[]) { - const node = createBaseDeclaration(ts.SyntaxKind.VariableStatement, /*decorators*/ undefined, modifiers); - node.declarationList = ts.isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList; - node.transformFlags |= - propagateChildFlags(node.declarationList); - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - } - return node; + // @api + function createVariableStatement(modifiers: readonly ts.Modifier[] | undefined, declarationList: ts.VariableDeclarationList | readonly ts.VariableDeclaration[]) { + const node = createBaseDeclaration(ts.SyntaxKind.VariableStatement, /*decorators*/ undefined, modifiers); + node.declarationList = ts.isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList; + node.transformFlags |= + propagateChildFlags(node.declarationList); + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { + node.transformFlags = ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function updateVariableStatement(node: ts.VariableStatement, modifiers: readonly ts.Modifier[] | undefined, declarationList: ts.VariableDeclarationList) { - return node.modifiers !== modifiers - || node.declarationList !== declarationList - ? update(createVariableStatement(modifiers, declarationList), node) - : node; - } + // @api + function updateVariableStatement(node: ts.VariableStatement, modifiers: readonly ts.Modifier[] | undefined, declarationList: ts.VariableDeclarationList) { + return node.modifiers !== modifiers + || node.declarationList !== declarationList + ? update(createVariableStatement(modifiers, declarationList), node) + : node; + } - // @api - function createEmptyStatement() { - return createBaseNode(ts.SyntaxKind.EmptyStatement); - } + // @api + function createEmptyStatement() { + return createBaseNode(ts.SyntaxKind.EmptyStatement); + } - // @api - function createExpressionStatement(expression: ts.Expression): ts.ExpressionStatement { - const node = createBaseNode(ts.SyntaxKind.ExpressionStatement); - node.expression = parenthesizerRules().parenthesizeExpressionOfExpressionStatement(expression); - node.transformFlags |= propagateChildFlags(node.expression); - return node; - } + // @api + function createExpressionStatement(expression: ts.Expression): ts.ExpressionStatement { + const node = createBaseNode(ts.SyntaxKind.ExpressionStatement); + node.expression = parenthesizerRules().parenthesizeExpressionOfExpressionStatement(expression); + node.transformFlags |= propagateChildFlags(node.expression); + return node; + } - // @api - function updateExpressionStatement(node: ts.ExpressionStatement, expression: ts.Expression) { - return node.expression !== expression - ? update(createExpressionStatement(expression), node) - : node; - } + // @api + function updateExpressionStatement(node: ts.ExpressionStatement, expression: ts.Expression) { + return node.expression !== expression + ? update(createExpressionStatement(expression), node) + : node; + } - // @api - function createIfStatement(expression: ts.Expression, thenStatement: ts.Statement, elseStatement?: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.IfStatement); - node.expression = expression; - node.thenStatement = asEmbeddedStatement(thenStatement); - node.elseStatement = asEmbeddedStatement(elseStatement); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.thenStatement) | - propagateChildFlags(node.elseStatement); - return node; - } + // @api + function createIfStatement(expression: ts.Expression, thenStatement: ts.Statement, elseStatement?: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.IfStatement); + node.expression = expression; + node.thenStatement = asEmbeddedStatement(thenStatement); + node.elseStatement = asEmbeddedStatement(elseStatement); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.thenStatement) | + propagateChildFlags(node.elseStatement); + return node; + } - // @api - function updateIfStatement(node: ts.IfStatement, expression: ts.Expression, thenStatement: ts.Statement, elseStatement: ts.Statement | undefined) { - return node.expression !== expression - || node.thenStatement !== thenStatement - || node.elseStatement !== elseStatement - ? update(createIfStatement(expression, thenStatement, elseStatement), node) - : node; - } + // @api + function updateIfStatement(node: ts.IfStatement, expression: ts.Expression, thenStatement: ts.Statement, elseStatement: ts.Statement | undefined) { + return node.expression !== expression + || node.thenStatement !== thenStatement + || node.elseStatement !== elseStatement + ? update(createIfStatement(expression, thenStatement, elseStatement), node) + : node; + } - // @api - function createDoStatement(statement: ts.Statement, expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.DoStatement); - node.statement = asEmbeddedStatement(statement); - node.expression = expression; - node.transformFlags |= - propagateChildFlags(node.statement) | - propagateChildFlags(node.expression); - return node; - } + // @api + function createDoStatement(statement: ts.Statement, expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.DoStatement); + node.statement = asEmbeddedStatement(statement); + node.expression = expression; + node.transformFlags |= + propagateChildFlags(node.statement) | + propagateChildFlags(node.expression); + return node; + } - // @api - function updateDoStatement(node: ts.DoStatement, statement: ts.Statement, expression: ts.Expression) { - return node.statement !== statement - || node.expression !== expression - ? update(createDoStatement(statement, expression), node) - : node; - } + // @api + function updateDoStatement(node: ts.DoStatement, statement: ts.Statement, expression: ts.Expression) { + return node.statement !== statement + || node.expression !== expression + ? update(createDoStatement(statement, expression), node) + : node; + } - // @api - function createWhileStatement(expression: ts.Expression, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.WhileStatement); - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.statement); - return node; - } + // @api + function createWhileStatement(expression: ts.Expression, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.WhileStatement); + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.statement); + return node; + } - // @api - function updateWhileStatement(node: ts.WhileStatement, expression: ts.Expression, statement: ts.Statement) { - return node.expression !== expression - || node.statement !== statement - ? update(createWhileStatement(expression, statement), node) - : node; - } + // @api + function updateWhileStatement(node: ts.WhileStatement, expression: ts.Expression, statement: ts.Statement) { + return node.expression !== expression + || node.statement !== statement + ? update(createWhileStatement(expression, statement), node) + : node; + } - // @api - function createForStatement(initializer: ts.ForInitializer | undefined, condition: ts.Expression | undefined, incrementor: ts.Expression | undefined, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.ForStatement); - node.initializer = initializer; - node.condition = condition; - node.incrementor = incrementor; - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.initializer) | - propagateChildFlags(node.condition) | - propagateChildFlags(node.incrementor) | - propagateChildFlags(node.statement); - return node; - } + // @api + function createForStatement(initializer: ts.ForInitializer | undefined, condition: ts.Expression | undefined, incrementor: ts.Expression | undefined, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.ForStatement); + node.initializer = initializer; + node.condition = condition; + node.incrementor = incrementor; + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.initializer) | + propagateChildFlags(node.condition) | + propagateChildFlags(node.incrementor) | + propagateChildFlags(node.statement); + return node; + } - // @api - function updateForStatement(node: ts.ForStatement, initializer: ts.ForInitializer | undefined, condition: ts.Expression | undefined, incrementor: ts.Expression | undefined, statement: ts.Statement) { - return node.initializer !== initializer - || node.condition !== condition - || node.incrementor !== incrementor - || node.statement !== statement - ? update(createForStatement(initializer, condition, incrementor, statement), node) - : node; - } + // @api + function updateForStatement(node: ts.ForStatement, initializer: ts.ForInitializer | undefined, condition: ts.Expression | undefined, incrementor: ts.Expression | undefined, statement: ts.Statement) { + return node.initializer !== initializer + || node.condition !== condition + || node.incrementor !== incrementor + || node.statement !== statement + ? update(createForStatement(initializer, condition, incrementor, statement), node) + : node; + } - // @api - function createForInStatement(initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.ForInStatement); - node.initializer = initializer; - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.initializer) | - propagateChildFlags(node.expression) | - propagateChildFlags(node.statement); - return node; - } + // @api + function createForInStatement(initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.ForInStatement); + node.initializer = initializer; + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.initializer) | + propagateChildFlags(node.expression) | + propagateChildFlags(node.statement); + return node; + } - // @api - function updateForInStatement(node: ts.ForInStatement, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { - return node.initializer !== initializer - || node.expression !== expression - || node.statement !== statement - ? update(createForInStatement(initializer, expression, statement), node) - : node; - } + // @api + function updateForInStatement(node: ts.ForInStatement, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { + return node.initializer !== initializer + || node.expression !== expression + || node.statement !== statement + ? update(createForInStatement(initializer, expression, statement), node) + : node; + } - // @api - function createForOfStatement(awaitModifier: ts.AwaitKeyword | undefined, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.ForOfStatement); - node.awaitModifier = awaitModifier; - node.initializer = initializer; - node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.awaitModifier) | - propagateChildFlags(node.initializer) | - propagateChildFlags(node.expression) | - propagateChildFlags(node.statement) | - ts.TransformFlags.ContainsES2015; - if (awaitModifier) - node.transformFlags |= ts.TransformFlags.ContainsES2018; - return node; - } + // @api + function createForOfStatement(awaitModifier: ts.AwaitKeyword | undefined, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.ForOfStatement); + node.awaitModifier = awaitModifier; + node.initializer = initializer; + node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.awaitModifier) | + propagateChildFlags(node.initializer) | + propagateChildFlags(node.expression) | + propagateChildFlags(node.statement) | + ts.TransformFlags.ContainsES2015; + if (awaitModifier) + node.transformFlags |= ts.TransformFlags.ContainsES2018; + return node; + } - // @api - function updateForOfStatement(node: ts.ForOfStatement, awaitModifier: ts.AwaitKeyword | undefined, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { - return node.awaitModifier !== awaitModifier - || node.initializer !== initializer - || node.expression !== expression - || node.statement !== statement - ? update(createForOfStatement(awaitModifier, initializer, expression, statement), node) - : node; - } + // @api + function updateForOfStatement(node: ts.ForOfStatement, awaitModifier: ts.AwaitKeyword | undefined, initializer: ts.ForInitializer, expression: ts.Expression, statement: ts.Statement) { + return node.awaitModifier !== awaitModifier + || node.initializer !== initializer + || node.expression !== expression + || node.statement !== statement + ? update(createForOfStatement(awaitModifier, initializer, expression, statement), node) + : node; + } - // @api - function createContinueStatement(label?: string | ts.Identifier): ts.ContinueStatement { - const node = createBaseNode(ts.SyntaxKind.ContinueStatement); - node.label = asName(label); - node.transformFlags |= - propagateChildFlags(node.label) | - ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; - return node; - } + // @api + function createContinueStatement(label?: string | ts.Identifier): ts.ContinueStatement { + const node = createBaseNode(ts.SyntaxKind.ContinueStatement); + node.label = asName(label); + node.transformFlags |= + propagateChildFlags(node.label) | + ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; + return node; + } - // @api - function updateContinueStatement(node: ts.ContinueStatement, label: ts.Identifier | undefined) { - return node.label !== label - ? update(createContinueStatement(label), node) - : node; - } + // @api + function updateContinueStatement(node: ts.ContinueStatement, label: ts.Identifier | undefined) { + return node.label !== label + ? update(createContinueStatement(label), node) + : node; + } - // @api - function createBreakStatement(label?: string | ts.Identifier): ts.BreakStatement { - const node = createBaseNode(ts.SyntaxKind.BreakStatement); - node.label = asName(label); - node.transformFlags |= - propagateChildFlags(node.label) | - ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; - return node; - } + // @api + function createBreakStatement(label?: string | ts.Identifier): ts.BreakStatement { + const node = createBaseNode(ts.SyntaxKind.BreakStatement); + node.label = asName(label); + node.transformFlags |= + propagateChildFlags(node.label) | + ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; + return node; + } - // @api - function updateBreakStatement(node: ts.BreakStatement, label: ts.Identifier | undefined) { - return node.label !== label - ? update(createBreakStatement(label), node) - : node; - } + // @api + function updateBreakStatement(node: ts.BreakStatement, label: ts.Identifier | undefined) { + return node.label !== label + ? update(createBreakStatement(label), node) + : node; + } - // @api - function createReturnStatement(expression?: ts.Expression): ts.ReturnStatement { - const node = createBaseNode(ts.SyntaxKind.ReturnStatement); - node.expression = expression; - // return in an ES2018 async generator must be awaited - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; - return node; - } + // @api + function createReturnStatement(expression?: ts.Expression): ts.ReturnStatement { + const node = createBaseNode(ts.SyntaxKind.ReturnStatement); + node.expression = expression; + // return in an ES2018 async generator must be awaited + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; + return node; + } - // @api - function updateReturnStatement(node: ts.ReturnStatement, expression: ts.Expression | undefined) { - return node.expression !== expression - ? update(createReturnStatement(expression), node) - : node; - } + // @api + function updateReturnStatement(node: ts.ReturnStatement, expression: ts.Expression | undefined) { + return node.expression !== expression + ? update(createReturnStatement(expression), node) + : node; + } - // @api - function createWithStatement(expression: ts.Expression, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.WithStatement); - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.statement); - return node; - } + // @api + function createWithStatement(expression: ts.Expression, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.WithStatement); + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.statement); + return node; + } - // @api - function updateWithStatement(node: ts.WithStatement, expression: ts.Expression, statement: ts.Statement) { - return node.expression !== expression - || node.statement !== statement - ? update(createWithStatement(expression, statement), node) - : node; - } + // @api + function updateWithStatement(node: ts.WithStatement, expression: ts.Expression, statement: ts.Statement) { + return node.expression !== expression + || node.statement !== statement + ? update(createWithStatement(expression, statement), node) + : node; + } - // @api - function createSwitchStatement(expression: ts.Expression, caseBlock: ts.CaseBlock): ts.SwitchStatement { - const node = createBaseNode(ts.SyntaxKind.SwitchStatement); - node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.caseBlock = caseBlock; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.caseBlock); - return node; - } + // @api + function createSwitchStatement(expression: ts.Expression, caseBlock: ts.CaseBlock): ts.SwitchStatement { + const node = createBaseNode(ts.SyntaxKind.SwitchStatement); + node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.caseBlock = caseBlock; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.caseBlock); + return node; + } - // @api - function updateSwitchStatement(node: ts.SwitchStatement, expression: ts.Expression, caseBlock: ts.CaseBlock) { - return node.expression !== expression - || node.caseBlock !== caseBlock - ? update(createSwitchStatement(expression, caseBlock), node) - : node; - } + // @api + function updateSwitchStatement(node: ts.SwitchStatement, expression: ts.Expression, caseBlock: ts.CaseBlock) { + return node.expression !== expression + || node.caseBlock !== caseBlock + ? update(createSwitchStatement(expression, caseBlock), node) + : node; + } - // @api - function createLabeledStatement(label: string | ts.Identifier, statement: ts.Statement) { - const node = createBaseNode(ts.SyntaxKind.LabeledStatement); - node.label = asName(label); - node.statement = asEmbeddedStatement(statement); - node.transformFlags |= - propagateChildFlags(node.label) | - propagateChildFlags(node.statement); - return node; - } + // @api + function createLabeledStatement(label: string | ts.Identifier, statement: ts.Statement) { + const node = createBaseNode(ts.SyntaxKind.LabeledStatement); + node.label = asName(label); + node.statement = asEmbeddedStatement(statement); + node.transformFlags |= + propagateChildFlags(node.label) | + propagateChildFlags(node.statement); + return node; + } - // @api - function updateLabeledStatement(node: ts.LabeledStatement, label: ts.Identifier, statement: ts.Statement) { - return node.label !== label - || node.statement !== statement - ? update(createLabeledStatement(label, statement), node) - : node; - } + // @api + function updateLabeledStatement(node: ts.LabeledStatement, label: ts.Identifier, statement: ts.Statement) { + return node.label !== label + || node.statement !== statement + ? update(createLabeledStatement(label, statement), node) + : node; + } - // @api - function createThrowStatement(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.ThrowStatement); - node.expression = expression; - node.transformFlags |= propagateChildFlags(node.expression); - return node; - } + // @api + function createThrowStatement(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.ThrowStatement); + node.expression = expression; + node.transformFlags |= propagateChildFlags(node.expression); + return node; + } - // @api - function updateThrowStatement(node: ts.ThrowStatement, expression: ts.Expression) { - return node.expression !== expression - ? update(createThrowStatement(expression), node) - : node; - } + // @api + function updateThrowStatement(node: ts.ThrowStatement, expression: ts.Expression) { + return node.expression !== expression + ? update(createThrowStatement(expression), node) + : node; + } - // @api - function createTryStatement(tryBlock: ts.Block, catchClause: ts.CatchClause | undefined, finallyBlock: ts.Block | undefined) { - const node = createBaseNode(ts.SyntaxKind.TryStatement); - node.tryBlock = tryBlock; - node.catchClause = catchClause; - node.finallyBlock = finallyBlock; - node.transformFlags |= - propagateChildFlags(node.tryBlock) | - propagateChildFlags(node.catchClause) | - propagateChildFlags(node.finallyBlock); - return node; - } + // @api + function createTryStatement(tryBlock: ts.Block, catchClause: ts.CatchClause | undefined, finallyBlock: ts.Block | undefined) { + const node = createBaseNode(ts.SyntaxKind.TryStatement); + node.tryBlock = tryBlock; + node.catchClause = catchClause; + node.finallyBlock = finallyBlock; + node.transformFlags |= + propagateChildFlags(node.tryBlock) | + propagateChildFlags(node.catchClause) | + propagateChildFlags(node.finallyBlock); + return node; + } - // @api - function updateTryStatement(node: ts.TryStatement, tryBlock: ts.Block, catchClause: ts.CatchClause | undefined, finallyBlock: ts.Block | undefined) { - return node.tryBlock !== tryBlock - || node.catchClause !== catchClause - || node.finallyBlock !== finallyBlock - ? update(createTryStatement(tryBlock, catchClause, finallyBlock), node) - : node; - } + // @api + function updateTryStatement(node: ts.TryStatement, tryBlock: ts.Block, catchClause: ts.CatchClause | undefined, finallyBlock: ts.Block | undefined) { + return node.tryBlock !== tryBlock + || node.catchClause !== catchClause + || node.finallyBlock !== finallyBlock + ? update(createTryStatement(tryBlock, catchClause, finallyBlock), node) + : node; + } - // @api - function createDebuggerStatement() { - return createBaseNode(ts.SyntaxKind.DebuggerStatement); - } + // @api + function createDebuggerStatement() { + return createBaseNode(ts.SyntaxKind.DebuggerStatement); + } - // @api - function createVariableDeclaration(name: string | ts.BindingName, exclamationToken: ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.VariableDeclaration, - /*decorators*/ undefined, - /*modifiers*/ undefined, name, type, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); - node.exclamationToken = exclamationToken; - node.transformFlags |= propagateChildFlags(node.exclamationToken); - if (exclamationToken) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - return node; + // @api + function createVariableDeclaration(name: string | ts.BindingName, exclamationToken: ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + const node = createBaseVariableLikeDeclaration(ts.SyntaxKind.VariableDeclaration, + /*decorators*/ undefined, + /*modifiers*/ undefined, name, type, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer)); + node.exclamationToken = exclamationToken; + node.transformFlags |= propagateChildFlags(node.exclamationToken); + if (exclamationToken) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function updateVariableDeclaration(node: ts.VariableDeclaration, name: ts.BindingName, exclamationToken: ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - return node.name !== name - || node.type !== type - || node.exclamationToken !== exclamationToken - || node.initializer !== initializer - ? update(createVariableDeclaration(name, exclamationToken, type, initializer), node) - : node; - } + // @api + function updateVariableDeclaration(node: ts.VariableDeclaration, name: ts.BindingName, exclamationToken: ts.ExclamationToken | undefined, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + return node.name !== name + || node.type !== type + || node.exclamationToken !== exclamationToken + || node.initializer !== initializer + ? update(createVariableDeclaration(name, exclamationToken, type, initializer), node) + : node; + } - // @api - function createVariableDeclarationList(declarations: readonly ts.VariableDeclaration[], flags = ts.NodeFlags.None) { - const node = createBaseNode(ts.SyntaxKind.VariableDeclarationList); - node.flags |= flags & ts.NodeFlags.BlockScoped; - node.declarations = createNodeArray(declarations); + // @api + function createVariableDeclarationList(declarations: readonly ts.VariableDeclaration[], flags = ts.NodeFlags.None) { + const node = createBaseNode(ts.SyntaxKind.VariableDeclarationList); + node.flags |= flags & ts.NodeFlags.BlockScoped; + node.declarations = createNodeArray(declarations); + node.transformFlags |= + propagateChildrenFlags(node.declarations) | + ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; + if (flags & ts.NodeFlags.BlockScoped) { node.transformFlags |= - propagateChildrenFlags(node.declarations) | - ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; - if (flags & ts.NodeFlags.BlockScoped) { - node.transformFlags |= - ts.TransformFlags.ContainsES2015 | - ts.TransformFlags.ContainsBlockScopedBinding; - } - return node; + ts.TransformFlags.ContainsES2015 | + ts.TransformFlags.ContainsBlockScopedBinding; } + return node; + } - // @api - function updateVariableDeclarationList(node: ts.VariableDeclarationList, declarations: readonly ts.VariableDeclaration[]) { - return node.declarations !== declarations - ? update(createVariableDeclarationList(declarations, node.flags), node) - : node; - } + // @api + function updateVariableDeclarationList(node: ts.VariableDeclarationList, declarations: readonly ts.VariableDeclaration[]) { + return node.declarations !== declarations + ? update(createVariableDeclarationList(declarations, node.flags), node) + : node; + } - // @api - function createFunctionDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { - const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.FunctionDeclaration, decorators, modifiers, name, typeParameters, parameters, type, body); - node.asteriskToken = asteriskToken; - if (!node.body || ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - } - else { - node.transformFlags |= - propagateChildFlags(node.asteriskToken) | - ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { - if (node.asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsES2018; - } - else { - node.transformFlags |= ts.TransformFlags.ContainsES2017; - } + // @api + function createFunctionDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block | undefined) { + const node = createBaseFunctionLikeDeclaration(ts.SyntaxKind.FunctionDeclaration, decorators, modifiers, name, typeParameters, parameters, type, body); + node.asteriskToken = asteriskToken; + if (!node.body || ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + } + else { + node.transformFlags |= + propagateChildFlags(node.asteriskToken) | + ts.TransformFlags.ContainsHoistedDeclarationOrCompletion; + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Async) { + if (node.asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsES2018; } - else if (node.asteriskToken) { - node.transformFlags |= ts.TransformFlags.ContainsGenerator; + else { + node.transformFlags |= ts.TransformFlags.ContainsES2017; } } - return node; - } - - // @api - function updateFunctionDeclaration(node: ts.FunctionDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.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 - ? updateBaseFunctionLikeDeclaration(createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) - : node; - } - - // @api - function createClassDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { - const node = createBaseClassLikeDeclaration(ts.SyntaxKind.ClassDeclaration, decorators, modifiers, name, typeParameters, heritageClauses, members); - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - } - else { - node.transformFlags |= ts.TransformFlags.ContainsES2015; - if (node.transformFlags & ts.TransformFlags.ContainsTypeScriptClassSyntax) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } + else if (node.asteriskToken) { + node.transformFlags |= ts.TransformFlags.ContainsGenerator; } - return node; } + return node; + } - // @api - function updateClassDeclaration(node: ts.ClassDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? update(createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) - : node; - } + // @api + function updateFunctionDeclaration(node: ts.FunctionDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.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 + ? updateBaseFunctionLikeDeclaration(createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) + : node; + } - // @api - function createInterfaceDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.TypeElement[]) { - const node = createBaseInterfaceOrClassLikeDeclaration(ts.SyntaxKind.InterfaceDeclaration, decorators, modifiers, name, typeParameters, heritageClauses); - node.members = createNodeArray(members); + // @api + function createClassDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { + const node = createBaseClassLikeDeclaration(ts.SyntaxKind.ClassDeclaration, decorators, modifiers, name, typeParameters, heritageClauses, members); + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } - - // @api - function updateInterfaceDeclaration(node: ts.InterfaceDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.TypeElement[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? update(createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) - : node; } - - // @api - function createTypeAliasDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, type: ts.TypeNode) { - const node = createBaseGenericNamedDeclaration(ts.SyntaxKind.TypeAliasDeclaration, decorators, modifiers, name, typeParameters); - node.type = type; - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; + else { + node.transformFlags |= ts.TransformFlags.ContainsES2015; + if (node.transformFlags & ts.TransformFlags.ContainsTypeScriptClassSyntax) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } } + return node; + } - // @api - function updateTypeAliasDeclaration(node: ts.TypeAliasDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, type: ts.TypeNode) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.type !== type - ? update(createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type), node) - : node; - } + // @api + function updateClassDeclaration(node: ts.ClassDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier | undefined, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.ClassElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? update(createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; + } - // @api - function createEnumDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, members: readonly ts.EnumMember[]) { - const node = createBaseNamedDeclaration(ts.SyntaxKind.EnumDeclaration, decorators, modifiers, name); - node.members = createNodeArray(members); - node.transformFlags |= - propagateChildrenFlags(node.members) | - ts.TransformFlags.ContainsTypeScript; - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Enum declarations cannot contain `await` - return node; - } + // @api + function createInterfaceDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.TypeElement[]) { + const node = createBaseInterfaceOrClassLikeDeclaration(ts.SyntaxKind.InterfaceDeclaration, decorators, modifiers, name, typeParameters, heritageClauses); + node.members = createNodeArray(members); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateEnumDeclaration(node: ts.EnumDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, members: readonly ts.EnumMember[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.members !== members - ? update(createEnumDeclaration(decorators, modifiers, name, members), node) - : node; - } + // @api + function updateInterfaceDeclaration(node: ts.InterfaceDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, heritageClauses: readonly ts.HeritageClause[] | undefined, members: readonly ts.TypeElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? update(createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; + } - // @api - function createModuleDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.ModuleName, body: ts.ModuleBody | undefined, flags = ts.NodeFlags.None) { - const node = createBaseDeclaration(ts.SyntaxKind.ModuleDeclaration, decorators, modifiers); - node.flags |= flags & (ts.NodeFlags.Namespace | ts.NodeFlags.NestedNamespace | ts.NodeFlags.GlobalAugmentation); - node.name = name; - node.body = body; - if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - } - else { - node.transformFlags |= - propagateChildFlags(node.name) | - propagateChildFlags(node.body) | - ts.TransformFlags.ContainsTypeScript; - } - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Module declarations cannot contain `await`. - return node; - } + // @api + function createTypeAliasDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, type: ts.TypeNode) { + const node = createBaseGenericNamedDeclaration(ts.SyntaxKind.TypeAliasDeclaration, decorators, modifiers, name, typeParameters); + node.type = type; + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function updateModuleDeclaration(node: ts.ModuleDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.ModuleName, body: ts.ModuleBody | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.body !== body - ? update(createModuleDeclaration(decorators, modifiers, name, body, node.flags), node) - : node; - } + // @api + function updateTypeAliasDeclaration(node: ts.TypeAliasDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, typeParameters: readonly ts.TypeParameterDeclaration[] | undefined, type: ts.TypeNode) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.type !== type + ? update(createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type), node) + : node; + } - // @api - function createModuleBlock(statements: readonly ts.Statement[]) { - const node = createBaseNode(ts.SyntaxKind.ModuleBlock); - node.statements = createNodeArray(statements); - node.transformFlags |= propagateChildrenFlags(node.statements); - return node; - } + // @api + function createEnumDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: string | ts.Identifier, members: readonly ts.EnumMember[]) { + const node = createBaseNamedDeclaration(ts.SyntaxKind.EnumDeclaration, decorators, modifiers, name); + node.members = createNodeArray(members); + node.transformFlags |= + propagateChildrenFlags(node.members) | + ts.TransformFlags.ContainsTypeScript; + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Enum declarations cannot contain `await` + return node; + } - // @api - function updateModuleBlock(node: ts.ModuleBlock, statements: readonly ts.Statement[]) { - return node.statements !== statements - ? update(createModuleBlock(statements), node) - : node; - } + // @api + function updateEnumDeclaration(node: ts.EnumDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.Identifier, members: readonly ts.EnumMember[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.members !== members + ? update(createEnumDeclaration(decorators, modifiers, name, members), node) + : node; + } - // @api - function createCaseBlock(clauses: readonly ts.CaseOrDefaultClause[]): ts.CaseBlock { - const node = createBaseNode(ts.SyntaxKind.CaseBlock); - node.clauses = createNodeArray(clauses); - node.transformFlags |= propagateChildrenFlags(node.clauses); - return node; + // @api + function createModuleDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.ModuleName, body: ts.ModuleBody | undefined, flags = ts.NodeFlags.None) { + const node = createBaseDeclaration(ts.SyntaxKind.ModuleDeclaration, decorators, modifiers); + node.flags |= flags & (ts.NodeFlags.Namespace | ts.NodeFlags.NestedNamespace | ts.NodeFlags.GlobalAugmentation); + node.name = name; + node.body = body; + if (ts.modifiersToFlags(node.modifiers) & ts.ModifierFlags.Ambient) { + node.transformFlags = ts.TransformFlags.ContainsTypeScript; } - - // @api - function updateCaseBlock(node: ts.CaseBlock, clauses: readonly ts.CaseOrDefaultClause[]) { - return node.clauses !== clauses - ? update(createCaseBlock(clauses), node) - : node; + else { + node.transformFlags |= + propagateChildFlags(node.name) | + propagateChildFlags(node.body) | + ts.TransformFlags.ContainsTypeScript; } + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Module declarations cannot contain `await`. + return node; + } - // @api - function createNamespaceExportDeclaration(name: string | ts.Identifier) { - const node = createBaseNamedDeclaration(ts.SyntaxKind.NamespaceExportDeclaration, - /*decorators*/ undefined, - /*modifiers*/ undefined, name); - node.transformFlags = ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function updateModuleDeclaration(node: ts.ModuleDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, name: ts.ModuleName, body: ts.ModuleBody | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.body !== body + ? update(createModuleDeclaration(decorators, modifiers, name, body, node.flags), node) + : node; + } - // @api - function updateNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration, name: ts.Identifier) { - return node.name !== name - ? update(createNamespaceExportDeclaration(name), node) - : node; - } + // @api + function createModuleBlock(statements: readonly ts.Statement[]) { + const node = createBaseNode(ts.SyntaxKind.ModuleBlock); + node.statements = createNodeArray(statements); + node.transformFlags |= propagateChildrenFlags(node.statements); + return node; + } - // @api - function createImportEqualsDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, name: string | ts.Identifier, moduleReference: ts.ModuleReference) { - const node = createBaseNamedDeclaration(ts.SyntaxKind.ImportEqualsDeclaration, decorators, modifiers, name); - node.isTypeOnly = isTypeOnly; - node.moduleReference = moduleReference; - node.transformFlags |= propagateChildFlags(node.moduleReference); - if (!ts.isExternalModuleReference(node.moduleReference)) - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Import= declaration is always parsed in an Await context - return node; - } + // @api + function updateModuleBlock(node: ts.ModuleBlock, statements: readonly ts.Statement[]) { + return node.statements !== statements + ? update(createModuleBlock(statements), node) + : node; + } - // @api - function updateImportEqualsDeclaration(node: ts.ImportEqualsDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, name: ts.Identifier, moduleReference: ts.ModuleReference) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.isTypeOnly !== isTypeOnly - || node.name !== name - || node.moduleReference !== moduleReference - ? update(createImportEqualsDeclaration(decorators, modifiers, isTypeOnly, name, moduleReference), node) - : node; - } + // @api + function createCaseBlock(clauses: readonly ts.CaseOrDefaultClause[]): ts.CaseBlock { + const node = createBaseNode(ts.SyntaxKind.CaseBlock); + node.clauses = createNodeArray(clauses); + node.transformFlags |= propagateChildrenFlags(node.clauses); + return node; + } - // @api - function createImportDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, importClause: ts.ImportClause | undefined, moduleSpecifier: ts.Expression, assertClause: ts.AssertClause | undefined): ts.ImportDeclaration { - const node = createBaseDeclaration(ts.SyntaxKind.ImportDeclaration, decorators, modifiers); - node.importClause = importClause; - node.moduleSpecifier = moduleSpecifier; - node.assertClause = assertClause; - node.transformFlags |= - propagateChildFlags(node.importClause) | - propagateChildFlags(node.moduleSpecifier); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateCaseBlock(node: ts.CaseBlock, clauses: readonly ts.CaseOrDefaultClause[]) { + return node.clauses !== clauses + ? update(createCaseBlock(clauses), node) + : node; + } - // @api - function updateImportDeclaration(node: ts.ImportDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, importClause: ts.ImportClause | undefined, moduleSpecifier: ts.Expression, assertClause: ts.AssertClause | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.importClause !== importClause - || node.moduleSpecifier !== moduleSpecifier - || node.assertClause !== assertClause - ? update(createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier, assertClause), node) - : node; - } + // @api + function createNamespaceExportDeclaration(name: string | ts.Identifier) { + const node = createBaseNamedDeclaration(ts.SyntaxKind.NamespaceExportDeclaration, + /*decorators*/ undefined, + /*modifiers*/ undefined, name); + node.transformFlags = ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createImportClause(isTypeOnly: boolean, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined): ts.ImportClause { - const node = createBaseNode(ts.SyntaxKind.ImportClause); - node.isTypeOnly = isTypeOnly; - node.name = name; - node.namedBindings = namedBindings; - node.transformFlags |= - propagateChildFlags(node.name) | - propagateChildFlags(node.namedBindings); - if (isTypeOnly) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration, name: ts.Identifier) { + return node.name !== name + ? update(createNamespaceExportDeclaration(name), node) + : node; + } - // @api - function updateImportClause(node: ts.ImportClause, isTypeOnly: boolean, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined) { - return node.isTypeOnly !== isTypeOnly - || node.name !== name - || node.namedBindings !== namedBindings - ? update(createImportClause(isTypeOnly, name, namedBindings), node) - : node; - } + // @api + function createImportEqualsDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, name: string | ts.Identifier, moduleReference: ts.ModuleReference) { + const node = createBaseNamedDeclaration(ts.SyntaxKind.ImportEqualsDeclaration, decorators, modifiers, name); + node.isTypeOnly = isTypeOnly; + node.moduleReference = moduleReference; + node.transformFlags |= propagateChildFlags(node.moduleReference); + if (!ts.isExternalModuleReference(node.moduleReference)) + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // Import= declaration is always parsed in an Await context + return node; + } - // @api - function createAssertClause(elements: readonly ts.AssertEntry[], multiLine?: boolean): ts.AssertClause { - const node = createBaseNode(ts.SyntaxKind.AssertClause); - node.elements = createNodeArray(elements); - node.multiLine = multiLine; - node.transformFlags |= ts.TransformFlags.ContainsESNext; - return node; - } + // @api + function updateImportEqualsDeclaration(node: ts.ImportEqualsDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, name: ts.Identifier, moduleReference: ts.ModuleReference) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.isTypeOnly !== isTypeOnly + || node.name !== name + || node.moduleReference !== moduleReference + ? update(createImportEqualsDeclaration(decorators, modifiers, isTypeOnly, name, moduleReference), node) + : node; + } - // @api - function updateAssertClause(node: ts.AssertClause, elements: readonly ts.AssertEntry[], multiLine?: boolean): ts.AssertClause { - return node.elements !== elements - || node.multiLine !== multiLine - ? update(createAssertClause(elements, multiLine), node) - : node; - } + // @api + function createImportDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, importClause: ts.ImportClause | undefined, moduleSpecifier: ts.Expression, assertClause: ts.AssertClause | undefined): ts.ImportDeclaration { + const node = createBaseDeclaration(ts.SyntaxKind.ImportDeclaration, decorators, modifiers); + node.importClause = importClause; + node.moduleSpecifier = moduleSpecifier; + node.assertClause = assertClause; + node.transformFlags |= + propagateChildFlags(node.importClause) | + propagateChildFlags(node.moduleSpecifier); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createAssertEntry(name: ts.AssertionKey, value: ts.Expression): ts.AssertEntry { - const node = createBaseNode(ts.SyntaxKind.AssertEntry); - node.name = name; - node.value = value; - node.transformFlags |= ts.TransformFlags.ContainsESNext; - return node; - } + // @api + function updateImportDeclaration(node: ts.ImportDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, importClause: ts.ImportClause | undefined, moduleSpecifier: ts.Expression, assertClause: ts.AssertClause | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.importClause !== importClause + || node.moduleSpecifier !== moduleSpecifier + || node.assertClause !== assertClause + ? update(createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier, assertClause), node) + : node; + } - // @api - function updateAssertEntry(node: ts.AssertEntry, name: ts.AssertionKey, value: ts.Expression): ts.AssertEntry { - return node.name !== name - || node.value !== value - ? update(createAssertEntry(name, value), node) - : node; - } + // @api + function createImportClause(isTypeOnly: boolean, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined): ts.ImportClause { + const node = createBaseNode(ts.SyntaxKind.ImportClause); + node.isTypeOnly = isTypeOnly; + node.name = name; + node.namedBindings = namedBindings; + node.transformFlags |= + propagateChildFlags(node.name) | + propagateChildFlags(node.namedBindings); + if (isTypeOnly) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + } + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createImportTypeAssertionContainer(clause: ts.AssertClause, multiLine?: boolean): ts.ImportTypeAssertionContainer { - const node = createBaseNode(ts.SyntaxKind.ImportTypeAssertionContainer); - node.assertClause = clause; - node.multiLine = multiLine; - return node; - } + // @api + function updateImportClause(node: ts.ImportClause, isTypeOnly: boolean, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined) { + return node.isTypeOnly !== isTypeOnly + || node.name !== name + || node.namedBindings !== namedBindings + ? update(createImportClause(isTypeOnly, name, namedBindings), node) + : node; + } - // @api - function updateImportTypeAssertionContainer(node: ts.ImportTypeAssertionContainer, clause: ts.AssertClause, multiLine?: boolean): ts.ImportTypeAssertionContainer { - return node.assertClause !== clause - || node.multiLine !== multiLine - ? update(createImportTypeAssertionContainer(clause, multiLine), node) - : node; - } + // @api + function createAssertClause(elements: readonly ts.AssertEntry[], multiLine?: boolean): ts.AssertClause { + const node = createBaseNode(ts.SyntaxKind.AssertClause); + node.elements = createNodeArray(elements); + node.multiLine = multiLine; + node.transformFlags |= ts.TransformFlags.ContainsESNext; + return node; + } - // @api - function createNamespaceImport(name: ts.Identifier): ts.NamespaceImport { - const node = createBaseNode(ts.SyntaxKind.NamespaceImport); - node.name = name; - node.transformFlags |= propagateChildFlags(node.name); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateAssertClause(node: ts.AssertClause, elements: readonly ts.AssertEntry[], multiLine?: boolean): ts.AssertClause { + return node.elements !== elements + || node.multiLine !== multiLine + ? update(createAssertClause(elements, multiLine), node) + : node; + } - // @api - function updateNamespaceImport(node: ts.NamespaceImport, name: ts.Identifier) { - return node.name !== name - ? update(createNamespaceImport(name), node) - : node; - } + // @api + function createAssertEntry(name: ts.AssertionKey, value: ts.Expression): ts.AssertEntry { + const node = createBaseNode(ts.SyntaxKind.AssertEntry); + node.name = name; + node.value = value; + node.transformFlags |= ts.TransformFlags.ContainsESNext; + return node; + } - // @api - function createNamespaceExport(name: ts.Identifier): ts.NamespaceExport { - const node = createBaseNode(ts.SyntaxKind.NamespaceExport); - node.name = name; - node.transformFlags |= - propagateChildFlags(node.name) | - ts.TransformFlags.ContainsESNext; - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateAssertEntry(node: ts.AssertEntry, name: ts.AssertionKey, value: ts.Expression): ts.AssertEntry { + return node.name !== name + || node.value !== value + ? update(createAssertEntry(name, value), node) + : node; + } - // @api - function updateNamespaceExport(node: ts.NamespaceExport, name: ts.Identifier) { - return node.name !== name - ? update(createNamespaceExport(name), node) - : node; - } + // @api + function createImportTypeAssertionContainer(clause: ts.AssertClause, multiLine?: boolean): ts.ImportTypeAssertionContainer { + const node = createBaseNode(ts.SyntaxKind.ImportTypeAssertionContainer); + node.assertClause = clause; + node.multiLine = multiLine; + return node; + } - // @api - function createNamedImports(elements: readonly ts.ImportSpecifier[]): ts.NamedImports { - const node = createBaseNode(ts.SyntaxKind.NamedImports); - node.elements = createNodeArray(elements); - node.transformFlags |= propagateChildrenFlags(node.elements); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateImportTypeAssertionContainer(node: ts.ImportTypeAssertionContainer, clause: ts.AssertClause, multiLine?: boolean): ts.ImportTypeAssertionContainer { + return node.assertClause !== clause + || node.multiLine !== multiLine + ? update(createImportTypeAssertionContainer(clause, multiLine), node) + : node; + } - // @api - function updateNamedImports(node: ts.NamedImports, elements: readonly ts.ImportSpecifier[]) { - return node.elements !== elements - ? update(createNamedImports(elements), node) - : node; - } + // @api + function createNamespaceImport(name: ts.Identifier): ts.NamespaceImport { + const node = createBaseNode(ts.SyntaxKind.NamespaceImport); + node.name = name; + node.transformFlags |= propagateChildFlags(node.name); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createImportSpecifier(isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { - const node = createBaseNode(ts.SyntaxKind.ImportSpecifier); - node.isTypeOnly = isTypeOnly; - node.propertyName = propertyName; - node.name = name; - node.transformFlags |= - propagateChildFlags(node.propertyName) | - propagateChildFlags(node.name); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateNamespaceImport(node: ts.NamespaceImport, name: ts.Identifier) { + return node.name !== name + ? update(createNamespaceImport(name), node) + : node; + } - // @api - function updateImportSpecifier(node: ts.ImportSpecifier, isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { - return node.isTypeOnly !== isTypeOnly - || node.propertyName !== propertyName - || node.name !== name - ? update(createImportSpecifier(isTypeOnly, propertyName, name), node) - : node; - } + // @api + function createNamespaceExport(name: ts.Identifier): ts.NamespaceExport { + const node = createBaseNode(ts.SyntaxKind.NamespaceExport); + node.name = name; + node.transformFlags |= + propagateChildFlags(node.name) | + ts.TransformFlags.ContainsESNext; + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createExportAssignment(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isExportEquals: boolean | undefined, expression: ts.Expression) { - const node = createBaseDeclaration(ts.SyntaxKind.ExportAssignment, decorators, modifiers); - node.isExportEquals = isExportEquals; - node.expression = isExportEquals - ? parenthesizerRules().parenthesizeRightSideOfBinary(ts.SyntaxKind.EqualsToken, /*leftSide*/ undefined, expression) - : parenthesizerRules().parenthesizeExpressionOfExportDefault(expression); - node.transformFlags |= propagateChildFlags(node.expression); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateNamespaceExport(node: ts.NamespaceExport, name: ts.Identifier) { + return node.name !== name + ? update(createNamespaceExport(name), node) + : node; + } - // @api - function updateExportAssignment(node: ts.ExportAssignment, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, expression: ts.Expression) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.expression !== expression - ? update(createExportAssignment(decorators, modifiers, node.isExportEquals, expression), node) - : node; - } + // @api + function createNamedImports(elements: readonly ts.ImportSpecifier[]): ts.NamedImports { + const node = createBaseNode(ts.SyntaxKind.NamedImports); + node.elements = createNodeArray(elements); + node.transformFlags |= propagateChildrenFlags(node.elements); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createExportDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, exportClause: ts.NamedExportBindings | undefined, moduleSpecifier?: ts.Expression, assertClause?: ts.AssertClause) { - const node = createBaseDeclaration(ts.SyntaxKind.ExportDeclaration, decorators, modifiers); - node.isTypeOnly = isTypeOnly; - node.exportClause = exportClause; - node.moduleSpecifier = moduleSpecifier; - node.assertClause = assertClause; - node.transformFlags |= - propagateChildFlags(node.exportClause) | - propagateChildFlags(node.moduleSpecifier); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateNamedImports(node: ts.NamedImports, elements: readonly ts.ImportSpecifier[]) { + return node.elements !== elements + ? update(createNamedImports(elements), node) + : node; + } - // @api - function updateExportDeclaration(node: ts.ExportDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, exportClause: ts.NamedExportBindings | undefined, moduleSpecifier: ts.Expression | undefined, assertClause: ts.AssertClause | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.isTypeOnly !== isTypeOnly - || node.exportClause !== exportClause - || node.moduleSpecifier !== moduleSpecifier - || node.assertClause !== assertClause - ? update(createExportDeclaration(decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier, assertClause), node) - : node; - } + // @api + function createImportSpecifier(isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { + const node = createBaseNode(ts.SyntaxKind.ImportSpecifier); + node.isTypeOnly = isTypeOnly; + node.propertyName = propertyName; + node.name = name; + node.transformFlags |= + propagateChildFlags(node.propertyName) | + propagateChildFlags(node.name); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createNamedExports(elements: readonly ts.ExportSpecifier[]) { - const node = createBaseNode(ts.SyntaxKind.NamedExports); - node.elements = createNodeArray(elements); - node.transformFlags |= propagateChildrenFlags(node.elements); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateImportSpecifier(node: ts.ImportSpecifier, isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { + return node.isTypeOnly !== isTypeOnly + || node.propertyName !== propertyName + || node.name !== name + ? update(createImportSpecifier(isTypeOnly, propertyName, name), node) + : node; + } - // @api - function updateNamedExports(node: ts.NamedExports, elements: readonly ts.ExportSpecifier[]) { - return node.elements !== elements - ? update(createNamedExports(elements), node) - : node; - } + // @api + function createExportAssignment(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isExportEquals: boolean | undefined, expression: ts.Expression) { + const node = createBaseDeclaration(ts.SyntaxKind.ExportAssignment, decorators, modifiers); + node.isExportEquals = isExportEquals; + node.expression = isExportEquals + ? parenthesizerRules().parenthesizeRightSideOfBinary(ts.SyntaxKind.EqualsToken, /*leftSide*/ undefined, expression) + : parenthesizerRules().parenthesizeExpressionOfExportDefault(expression); + node.transformFlags |= propagateChildFlags(node.expression); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createExportSpecifier(isTypeOnly: boolean, propertyName: string | ts.Identifier | undefined, name: string | ts.Identifier) { - const node = createBaseNode(ts.SyntaxKind.ExportSpecifier); - node.isTypeOnly = isTypeOnly; - node.propertyName = asName(propertyName); - node.name = asName(name); - node.transformFlags |= - propagateChildFlags(node.propertyName) | - propagateChildFlags(node.name); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateExportAssignment(node: ts.ExportAssignment, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, expression: ts.Expression) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.expression !== expression + ? update(createExportAssignment(decorators, modifiers, node.isExportEquals, expression), node) + : node; + } - // @api - function updateExportSpecifier(node: ts.ExportSpecifier, isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { - return node.isTypeOnly !== isTypeOnly - || node.propertyName !== propertyName - || node.name !== name - ? update(createExportSpecifier(isTypeOnly, propertyName, name), node) - : node; - } + // @api + function createExportDeclaration(decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, exportClause: ts.NamedExportBindings | undefined, moduleSpecifier?: ts.Expression, assertClause?: ts.AssertClause) { + const node = createBaseDeclaration(ts.SyntaxKind.ExportDeclaration, decorators, modifiers); + node.isTypeOnly = isTypeOnly; + node.exportClause = exportClause; + node.moduleSpecifier = moduleSpecifier; + node.assertClause = assertClause; + node.transformFlags |= + propagateChildFlags(node.exportClause) | + propagateChildFlags(node.moduleSpecifier); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createMissingDeclaration() { - const node = createBaseDeclaration(ts.SyntaxKind.MissingDeclaration, - /*decorators*/ undefined, - /*modifiers*/ undefined); - return node; - } + // @api + function updateExportDeclaration(node: ts.ExportDeclaration, decorators: readonly ts.Decorator[] | undefined, modifiers: readonly ts.Modifier[] | undefined, isTypeOnly: boolean, exportClause: ts.NamedExportBindings | undefined, moduleSpecifier: ts.Expression | undefined, assertClause: ts.AssertClause | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.isTypeOnly !== isTypeOnly + || node.exportClause !== exportClause + || node.moduleSpecifier !== moduleSpecifier + || node.assertClause !== assertClause + ? update(createExportDeclaration(decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier, assertClause), node) + : node; + } - // - // Module references - // + // @api + function createNamedExports(elements: readonly ts.ExportSpecifier[]) { + const node = createBaseNode(ts.SyntaxKind.NamedExports); + node.elements = createNodeArray(elements); + node.transformFlags |= propagateChildrenFlags(node.elements); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - function createExternalModuleReference(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.ExternalModuleReference); - node.expression = expression; - node.transformFlags |= propagateChildFlags(node.expression); - node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context - return node; - } + // @api + function updateNamedExports(node: ts.NamedExports, elements: readonly ts.ExportSpecifier[]) { + return node.elements !== elements + ? update(createNamedExports(elements), node) + : node; + } - // @api - function updateExternalModuleReference(node: ts.ExternalModuleReference, expression: ts.Expression) { - return node.expression !== expression - ? update(createExternalModuleReference(expression), node) - : node; - } + // @api + function createExportSpecifier(isTypeOnly: boolean, propertyName: string | ts.Identifier | undefined, name: string | ts.Identifier) { + const node = createBaseNode(ts.SyntaxKind.ExportSpecifier); + node.isTypeOnly = isTypeOnly; + node.propertyName = asName(propertyName); + node.name = asName(name); + node.transformFlags |= + propagateChildFlags(node.propertyName) | + propagateChildFlags(node.name); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // - // JSDoc - // + // @api + function updateExportSpecifier(node: ts.ExportSpecifier, isTypeOnly: boolean, propertyName: ts.Identifier | undefined, name: ts.Identifier) { + return node.isTypeOnly !== isTypeOnly + || node.propertyName !== propertyName + || node.name !== name + ? update(createExportSpecifier(isTypeOnly, propertyName, name), node) + : node; + } - // @api - // createJSDocAllType - // createJSDocUnknownType - function createJSDocPrimaryTypeWorker(kind: T["kind"]) { - return createBaseNode(kind); - } + // @api + function createMissingDeclaration() { + const node = createBaseDeclaration(ts.SyntaxKind.MissingDeclaration, + /*decorators*/ undefined, + /*modifiers*/ undefined); + return node; + } - // @api - // createJSDocNullableType - // createJSDocNonNullableType - function createJSDocPrePostfixUnaryTypeWorker(kind: T["kind"], type: T["type"], postfix = false): T { - const node = createJSDocUnaryTypeWorker(kind, postfix ? type && parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(type) : type) as ts.Mutable; - node.postfix = postfix; - return node; - } + // + // Module references + // - // @api - // createJSDocOptionalType - // createJSDocVariadicType - // createJSDocNamepathType - function createJSDocUnaryTypeWorker(kind: T["kind"], type: T["type"]): T { - const node = createBaseNode(kind); - node.type = type; - return node; - } + // @api + function createExternalModuleReference(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.ExternalModuleReference); + node.expression = expression; + node.transformFlags |= propagateChildFlags(node.expression); + node.transformFlags &= ~ts.TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context + return node; + } - // @api - // updateJSDocNonNullableType - // updateJSDocNullableType - function updateJSDocPrePostfixUnaryTypeWorker(kind: T["kind"], node: T, type: T["type"]): T { - return node.type !== type - ? update(createJSDocPrePostfixUnaryTypeWorker(kind, type, node.postfix), node) + // @api + function updateExternalModuleReference(node: ts.ExternalModuleReference, expression: ts.Expression) { + return node.expression !== expression + ? update(createExternalModuleReference(expression), node) : node; - } + } - // @api - // updateJSDocOptionalType - // updateJSDocVariadicType - // updateJSDocNamepathType - function updateJSDocUnaryTypeWorker(kind: T["kind"], node: T, type: T["type"]): T { - return node.type !== type - ? update(createJSDocUnaryTypeWorker(kind, type), node) - : node; - } + // + // JSDoc + // - // @api - function createJSDocFunctionType(parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.JSDocFunctionType { - const node = createBaseSignatureDeclaration(ts.SyntaxKind.JSDocFunctionType, - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, parameters, type); - return node; - } + // @api + // createJSDocAllType + // createJSDocUnknownType + function createJSDocPrimaryTypeWorker(kind: T["kind"]) { + return createBaseNode(kind); + } - // @api - function updateJSDocFunctionType(node: ts.JSDocFunctionType, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.JSDocFunctionType { - return node.parameters !== parameters - || node.type !== type - ? update(createJSDocFunctionType(parameters, type), node) - : node; - } + // @api + // createJSDocNullableType + // createJSDocNonNullableType + function createJSDocPrePostfixUnaryTypeWorker(kind: T["kind"], type: T["type"], postfix = false): T { + const node = createJSDocUnaryTypeWorker(kind, postfix ? type && parenthesizerRules().parenthesizeNonArrayTypeOfPostfixType(type) : type) as ts.Mutable; + node.postfix = postfix; + return node; + } - // @api - function createJSDocTypeLiteral(propertyTags?: readonly ts.JSDocPropertyLikeTag[], isArrayType = false): ts.JSDocTypeLiteral { - const node = createBaseNode(ts.SyntaxKind.JSDocTypeLiteral); - node.jsDocPropertyTags = asNodeArray(propertyTags); - node.isArrayType = isArrayType; - return node; - } + // @api + // createJSDocOptionalType + // createJSDocVariadicType + // createJSDocNamepathType + function createJSDocUnaryTypeWorker(kind: T["kind"], type: T["type"]): T { + const node = createBaseNode(kind); + node.type = type; + return node; + } - // @api - function updateJSDocTypeLiteral(node: ts.JSDocTypeLiteral, propertyTags: readonly ts.JSDocPropertyLikeTag[] | undefined, isArrayType: boolean): ts.JSDocTypeLiteral { - return node.jsDocPropertyTags !== propertyTags - || node.isArrayType !== isArrayType - ? update(createJSDocTypeLiteral(propertyTags, isArrayType), node) - : node; - } + // @api + // updateJSDocNonNullableType + // updateJSDocNullableType + function updateJSDocPrePostfixUnaryTypeWorker(kind: T["kind"], node: T, type: T["type"]): T { + return node.type !== type + ? update(createJSDocPrePostfixUnaryTypeWorker(kind, type, node.postfix), node) + : node; + } - // @api - function createJSDocTypeExpression(type: ts.TypeNode): ts.JSDocTypeExpression { - const node = createBaseNode(ts.SyntaxKind.JSDocTypeExpression); - node.type = type; - return node; - } + // @api + // updateJSDocOptionalType + // updateJSDocVariadicType + // updateJSDocNamepathType + function updateJSDocUnaryTypeWorker(kind: T["kind"], node: T, type: T["type"]): T { + return node.type !== type + ? update(createJSDocUnaryTypeWorker(kind, type), node) + : node; + } - // @api - function updateJSDocTypeExpression(node: ts.JSDocTypeExpression, type: ts.TypeNode): ts.JSDocTypeExpression { - return node.type !== type - ? update(createJSDocTypeExpression(type), node) - : node; - } + // @api + function createJSDocFunctionType(parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.JSDocFunctionType { + const node = createBaseSignatureDeclaration(ts.SyntaxKind.JSDocFunctionType, + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, parameters, type); + return node; + } - // @api - function createJSDocSignature(typeParameters: readonly ts.JSDocTemplateTag[] | undefined, parameters: readonly ts.JSDocParameterTag[], type?: ts.JSDocReturnTag): ts.JSDocSignature { - const node = createBaseNode(ts.SyntaxKind.JSDocSignature); - node.typeParameters = asNodeArray(typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - return node; - } + // @api + function updateJSDocFunctionType(node: ts.JSDocFunctionType, parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode | undefined): ts.JSDocFunctionType { + return node.parameters !== parameters + || node.type !== type + ? update(createJSDocFunctionType(parameters, type), node) + : node; + } - // @api - function updateJSDocSignature(node: ts.JSDocSignature, typeParameters: readonly ts.JSDocTemplateTag[] | undefined, parameters: readonly ts.JSDocParameterTag[], type: ts.JSDocReturnTag | undefined): ts.JSDocSignature { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? update(createJSDocSignature(typeParameters, parameters, type), node) - : node; - } + // @api + function createJSDocTypeLiteral(propertyTags?: readonly ts.JSDocPropertyLikeTag[], isArrayType = false): ts.JSDocTypeLiteral { + const node = createBaseNode(ts.SyntaxKind.JSDocTypeLiteral); + node.jsDocPropertyTags = asNodeArray(propertyTags); + node.isArrayType = isArrayType; + return node; + } - function getDefaultTagName(node: ts.JSDocTag) { - const defaultTagName = getDefaultTagNameForKind(node.kind); - return node.tagName.escapedText === ts.escapeLeadingUnderscores(defaultTagName) - ? node.tagName - : createIdentifier(defaultTagName); - } + // @api + function updateJSDocTypeLiteral(node: ts.JSDocTypeLiteral, propertyTags: readonly ts.JSDocPropertyLikeTag[] | undefined, isArrayType: boolean): ts.JSDocTypeLiteral { + return node.jsDocPropertyTags !== propertyTags + || node.isArrayType !== isArrayType + ? update(createJSDocTypeLiteral(propertyTags, isArrayType), node) + : node; + } - // @api - function createBaseJSDocTag(kind: T["kind"], tagName: ts.Identifier, comment: string | ts.NodeArray | undefined) { - const node = createBaseNode(kind); - node.tagName = tagName; - node.comment = comment; - return node; - } + // @api + function createJSDocTypeExpression(type: ts.TypeNode): ts.JSDocTypeExpression { + const node = createBaseNode(ts.SyntaxKind.JSDocTypeExpression); + node.type = type; + return node; + } - // @api - function createJSDocTemplateTag(tagName: ts.Identifier | undefined, constraint: ts.JSDocTypeExpression | undefined, typeParameters: readonly ts.TypeParameterDeclaration[], comment?: string | ts.NodeArray): ts.JSDocTemplateTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTemplateTag, tagName ?? createIdentifier("template"), comment); - node.constraint = constraint; - node.typeParameters = createNodeArray(typeParameters); - return node; - } + // @api + function updateJSDocTypeExpression(node: ts.JSDocTypeExpression, type: ts.TypeNode): ts.JSDocTypeExpression { + return node.type !== type + ? update(createJSDocTypeExpression(type), node) + : node; + } - // @api - function updateJSDocTemplateTag(node: ts.JSDocTemplateTag, tagName: ts.Identifier = getDefaultTagName(node), constraint: ts.JSDocTypeExpression | undefined, typeParameters: readonly ts.TypeParameterDeclaration[], comment: string | ts.NodeArray | undefined): ts.JSDocTemplateTag { - return node.tagName !== tagName - || node.constraint !== constraint - || node.typeParameters !== typeParameters - || node.comment !== comment - ? update(createJSDocTemplateTag(tagName, constraint, typeParameters, comment), node) - : node; - } + // @api + function createJSDocSignature(typeParameters: readonly ts.JSDocTemplateTag[] | undefined, parameters: readonly ts.JSDocParameterTag[], type?: ts.JSDocReturnTag): ts.JSDocSignature { + const node = createBaseNode(ts.SyntaxKind.JSDocSignature); + node.typeParameters = asNodeArray(typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + return node; + } - // @api - function createJSDocTypedefTag(tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, fullName?: ts.Identifier | ts.JSDocNamespaceDeclaration, comment?: string | ts.NodeArray): ts.JSDocTypedefTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTypedefTag, tagName ?? createIdentifier("typedef"), comment); - node.typeExpression = typeExpression; - node.fullName = fullName; - node.name = ts.getJSDocTypeAliasName(fullName); - return node; - } + // @api + function updateJSDocSignature(node: ts.JSDocSignature, typeParameters: readonly ts.JSDocTemplateTag[] | undefined, parameters: readonly ts.JSDocParameterTag[], type: ts.JSDocReturnTag | undefined): ts.JSDocSignature { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? update(createJSDocSignature(typeParameters, parameters, type), node) + : node; + } - // @api - function updateJSDocTypedefTag(node: ts.JSDocTypedefTag, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocTypeExpression | undefined, fullName: ts.Identifier | ts.JSDocNamespaceDeclaration | undefined, comment: string | ts.NodeArray | undefined): ts.JSDocTypedefTag { - return node.tagName !== tagName - || node.typeExpression !== typeExpression - || node.fullName !== fullName - || node.comment !== comment - ? update(createJSDocTypedefTag(tagName, typeExpression, fullName, comment), node) - : node; - } + function getDefaultTagName(node: ts.JSDocTag) { + const defaultTagName = getDefaultTagNameForKind(node.kind); + return node.tagName.escapedText === ts.escapeLeadingUnderscores(defaultTagName) + ? node.tagName + : createIdentifier(defaultTagName); + } - // @api - function createJSDocParameterTag(tagName: ts.Identifier | undefined, name: ts.EntityName, isBracketed: boolean, typeExpression?: ts.JSDocTypeExpression, isNameFirst?: boolean, comment?: string | ts.NodeArray): ts.JSDocParameterTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocParameterTag, tagName ?? createIdentifier("param"), comment); - node.typeExpression = typeExpression; - node.name = name; - node.isNameFirst = !!isNameFirst; - node.isBracketed = isBracketed; - return node; - } + // @api + function createBaseJSDocTag(kind: T["kind"], tagName: ts.Identifier, comment: string | ts.NodeArray | undefined) { + const node = createBaseNode(kind); + node.tagName = tagName; + node.comment = comment; + return node; + } - // @api - function updateJSDocParameterTag(node: ts.JSDocParameterTag, tagName: ts.Identifier = getDefaultTagName(node), name: ts.EntityName, isBracketed: boolean, typeExpression: ts.JSDocTypeExpression | undefined, isNameFirst: boolean, comment: string | ts.NodeArray | undefined): ts.JSDocParameterTag { - return node.tagName !== tagName - || node.name !== name - || node.isBracketed !== isBracketed - || node.typeExpression !== typeExpression - || node.isNameFirst !== isNameFirst - || node.comment !== comment - ? update(createJSDocParameterTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment), node) - : node; - } + // @api + function createJSDocTemplateTag(tagName: ts.Identifier | undefined, constraint: ts.JSDocTypeExpression | undefined, typeParameters: readonly ts.TypeParameterDeclaration[], comment?: string | ts.NodeArray): ts.JSDocTemplateTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTemplateTag, tagName ?? createIdentifier("template"), comment); + node.constraint = constraint; + node.typeParameters = createNodeArray(typeParameters); + return node; + } - // @api - function createJSDocPropertyTag(tagName: ts.Identifier | undefined, name: ts.EntityName, isBracketed: boolean, typeExpression?: ts.JSDocTypeExpression, isNameFirst?: boolean, comment?: string | ts.NodeArray): ts.JSDocPropertyTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocPropertyTag, tagName ?? createIdentifier("prop"), comment); - node.typeExpression = typeExpression; - node.name = name; - node.isNameFirst = !!isNameFirst; - node.isBracketed = isBracketed; - return node; - } + // @api + function updateJSDocTemplateTag(node: ts.JSDocTemplateTag, tagName: ts.Identifier = getDefaultTagName(node), constraint: ts.JSDocTypeExpression | undefined, typeParameters: readonly ts.TypeParameterDeclaration[], comment: string | ts.NodeArray | undefined): ts.JSDocTemplateTag { + return node.tagName !== tagName + || node.constraint !== constraint + || node.typeParameters !== typeParameters + || node.comment !== comment + ? update(createJSDocTemplateTag(tagName, constraint, typeParameters, comment), node) + : node; + } - // @api - function updateJSDocPropertyTag(node: ts.JSDocPropertyTag, tagName: ts.Identifier = getDefaultTagName(node), name: ts.EntityName, isBracketed: boolean, typeExpression: ts.JSDocTypeExpression | undefined, isNameFirst: boolean, comment: string | ts.NodeArray | undefined): ts.JSDocPropertyTag { - return node.tagName !== tagName - || node.name !== name - || node.isBracketed !== isBracketed - || node.typeExpression !== typeExpression - || node.isNameFirst !== isNameFirst - || node.comment !== comment - ? update(createJSDocPropertyTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment), node) - : node; - } + // @api + function createJSDocTypedefTag(tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, fullName?: ts.Identifier | ts.JSDocNamespaceDeclaration, comment?: string | ts.NodeArray): ts.JSDocTypedefTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTypedefTag, tagName ?? createIdentifier("typedef"), comment); + node.typeExpression = typeExpression; + node.fullName = fullName; + node.name = ts.getJSDocTypeAliasName(fullName); + return node; + } - // @api - function createJSDocCallbackTag(tagName: ts.Identifier | undefined, typeExpression: ts.JSDocSignature, fullName?: ts.Identifier | ts.JSDocNamespaceDeclaration, comment?: string | ts.NodeArray): ts.JSDocCallbackTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocCallbackTag, tagName ?? createIdentifier("callback"), comment); - node.typeExpression = typeExpression; - node.fullName = fullName; - node.name = ts.getJSDocTypeAliasName(fullName); - return node; - } + // @api + function updateJSDocTypedefTag(node: ts.JSDocTypedefTag, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocTypeExpression | undefined, fullName: ts.Identifier | ts.JSDocNamespaceDeclaration | undefined, comment: string | ts.NodeArray | undefined): ts.JSDocTypedefTag { + return node.tagName !== tagName + || node.typeExpression !== typeExpression + || node.fullName !== fullName + || node.comment !== comment + ? update(createJSDocTypedefTag(tagName, typeExpression, fullName, comment), node) + : node; + } - // @api - function updateJSDocCallbackTag(node: ts.JSDocCallbackTag, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocSignature, fullName: ts.Identifier | ts.JSDocNamespaceDeclaration | undefined, comment: string | ts.NodeArray | undefined): ts.JSDocCallbackTag { - return node.tagName !== tagName - || node.typeExpression !== typeExpression - || node.fullName !== fullName - || node.comment !== comment - ? update(createJSDocCallbackTag(tagName, typeExpression, fullName, comment), node) - : node; - } + // @api + function createJSDocParameterTag(tagName: ts.Identifier | undefined, name: ts.EntityName, isBracketed: boolean, typeExpression?: ts.JSDocTypeExpression, isNameFirst?: boolean, comment?: string | ts.NodeArray): ts.JSDocParameterTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocParameterTag, tagName ?? createIdentifier("param"), comment); + node.typeExpression = typeExpression; + node.name = name; + node.isNameFirst = !!isNameFirst; + node.isBracketed = isBracketed; + return node; + } - // @api - function createJSDocAugmentsTag(tagName: ts.Identifier | undefined, className: ts.JSDocAugmentsTag["class"], comment?: string | ts.NodeArray): ts.JSDocAugmentsTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocAugmentsTag, tagName ?? createIdentifier("augments"), comment); - node.class = className; - return node; - } + // @api + function updateJSDocParameterTag(node: ts.JSDocParameterTag, tagName: ts.Identifier = getDefaultTagName(node), name: ts.EntityName, isBracketed: boolean, typeExpression: ts.JSDocTypeExpression | undefined, isNameFirst: boolean, comment: string | ts.NodeArray | undefined): ts.JSDocParameterTag { + return node.tagName !== tagName + || node.name !== name + || node.isBracketed !== isBracketed + || node.typeExpression !== typeExpression + || node.isNameFirst !== isNameFirst + || node.comment !== comment + ? update(createJSDocParameterTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment), node) + : node; + } - // @api - function updateJSDocAugmentsTag(node: ts.JSDocAugmentsTag, tagName: ts.Identifier = getDefaultTagName(node), className: ts.JSDocAugmentsTag["class"], comment: string | ts.NodeArray | undefined): ts.JSDocAugmentsTag { - return node.tagName !== tagName - || node.class !== className - || node.comment !== comment - ? update(createJSDocAugmentsTag(tagName, className, comment), node) - : node; - } + // @api + function createJSDocPropertyTag(tagName: ts.Identifier | undefined, name: ts.EntityName, isBracketed: boolean, typeExpression?: ts.JSDocTypeExpression, isNameFirst?: boolean, comment?: string | ts.NodeArray): ts.JSDocPropertyTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocPropertyTag, tagName ?? createIdentifier("prop"), comment); + node.typeExpression = typeExpression; + node.name = name; + node.isNameFirst = !!isNameFirst; + node.isBracketed = isBracketed; + return node; + } - // @api - function createJSDocImplementsTag(tagName: ts.Identifier | undefined, className: ts.JSDocImplementsTag["class"], comment?: string | ts.NodeArray): ts.JSDocImplementsTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocImplementsTag, tagName ?? createIdentifier("implements"), comment); - node.class = className; - return node; - } + // @api + function updateJSDocPropertyTag(node: ts.JSDocPropertyTag, tagName: ts.Identifier = getDefaultTagName(node), name: ts.EntityName, isBracketed: boolean, typeExpression: ts.JSDocTypeExpression | undefined, isNameFirst: boolean, comment: string | ts.NodeArray | undefined): ts.JSDocPropertyTag { + return node.tagName !== tagName + || node.name !== name + || node.isBracketed !== isBracketed + || node.typeExpression !== typeExpression + || node.isNameFirst !== isNameFirst + || node.comment !== comment + ? update(createJSDocPropertyTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment), node) + : node; + } - // @api - function createJSDocSeeTag(tagName: ts.Identifier | undefined, name: ts.JSDocNameReference | undefined, comment?: string | ts.NodeArray): ts.JSDocSeeTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocSeeTag, tagName ?? createIdentifier("see"), comment); - node.name = name; - return node; - } + // @api + function createJSDocCallbackTag(tagName: ts.Identifier | undefined, typeExpression: ts.JSDocSignature, fullName?: ts.Identifier | ts.JSDocNamespaceDeclaration, comment?: string | ts.NodeArray): ts.JSDocCallbackTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocCallbackTag, tagName ?? createIdentifier("callback"), comment); + node.typeExpression = typeExpression; + node.fullName = fullName; + node.name = ts.getJSDocTypeAliasName(fullName); + return node; + } - // @api - function updateJSDocSeeTag(node: ts.JSDocSeeTag, tagName: ts.Identifier | undefined, name: ts.JSDocNameReference | undefined, comment?: string | ts.NodeArray): ts.JSDocSeeTag { - return node.tagName !== tagName - || node.name !== name - || node.comment !== comment - ? update(createJSDocSeeTag(tagName, name, comment), node) - : node; - } + // @api + function updateJSDocCallbackTag(node: ts.JSDocCallbackTag, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocSignature, fullName: ts.Identifier | ts.JSDocNamespaceDeclaration | undefined, comment: string | ts.NodeArray | undefined): ts.JSDocCallbackTag { + return node.tagName !== tagName + || node.typeExpression !== typeExpression + || node.fullName !== fullName + || node.comment !== comment + ? update(createJSDocCallbackTag(tagName, typeExpression, fullName, comment), node) + : node; + } - // @api - function createJSDocNameReference(name: ts.EntityName | ts.JSDocMemberName): ts.JSDocNameReference { - const node = createBaseNode(ts.SyntaxKind.JSDocNameReference); - node.name = name; - return node; - } + // @api + function createJSDocAugmentsTag(tagName: ts.Identifier | undefined, className: ts.JSDocAugmentsTag["class"], comment?: string | ts.NodeArray): ts.JSDocAugmentsTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocAugmentsTag, tagName ?? createIdentifier("augments"), comment); + node.class = className; + return node; + } - // @api - function updateJSDocNameReference(node: ts.JSDocNameReference, name: ts.EntityName | ts.JSDocMemberName): ts.JSDocNameReference { - return node.name !== name - ? update(createJSDocNameReference(name), node) - : node; - } + // @api + function updateJSDocAugmentsTag(node: ts.JSDocAugmentsTag, tagName: ts.Identifier = getDefaultTagName(node), className: ts.JSDocAugmentsTag["class"], comment: string | ts.NodeArray | undefined): ts.JSDocAugmentsTag { + return node.tagName !== tagName + || node.class !== className + || node.comment !== comment + ? update(createJSDocAugmentsTag(tagName, className, comment), node) + : node; + } - // @api - function createJSDocMemberName(left: ts.EntityName | ts.JSDocMemberName, right: ts.Identifier) { - const node = createBaseNode(ts.SyntaxKind.JSDocMemberName); - node.left = left; - node.right = right; - node.transformFlags |= - propagateChildFlags(node.left) | - propagateChildFlags(node.right); - return node; - } + // @api + function createJSDocImplementsTag(tagName: ts.Identifier | undefined, className: ts.JSDocImplementsTag["class"], comment?: string | ts.NodeArray): ts.JSDocImplementsTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocImplementsTag, tagName ?? createIdentifier("implements"), comment); + node.class = className; + return node; + } - // @api - function updateJSDocMemberName(node: ts.JSDocMemberName, left: ts.EntityName | ts.JSDocMemberName, right: ts.Identifier) { - return node.left !== left - || node.right !== right - ? update(createJSDocMemberName(left, right), node) - : node; - } + // @api + function createJSDocSeeTag(tagName: ts.Identifier | undefined, name: ts.JSDocNameReference | undefined, comment?: string | ts.NodeArray): ts.JSDocSeeTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocSeeTag, tagName ?? createIdentifier("see"), comment); + node.name = name; + return node; + } - // @api - function createJSDocLink(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLink { - const node = createBaseNode(ts.SyntaxKind.JSDocLink); - node.name = name; - node.text = text; - return node; - } + // @api + function updateJSDocSeeTag(node: ts.JSDocSeeTag, tagName: ts.Identifier | undefined, name: ts.JSDocNameReference | undefined, comment?: string | ts.NodeArray): ts.JSDocSeeTag { + return node.tagName !== tagName + || node.name !== name + || node.comment !== comment + ? update(createJSDocSeeTag(tagName, name, comment), node) + : node; + } - // @api - function updateJSDocLink(node: ts.JSDocLink, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLink { - return node.name !== name - ? update(createJSDocLink(name, text), node) - : node; - } + // @api + function createJSDocNameReference(name: ts.EntityName | ts.JSDocMemberName): ts.JSDocNameReference { + const node = createBaseNode(ts.SyntaxKind.JSDocNameReference); + node.name = name; + return node; + } - // @api - function createJSDocLinkCode(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkCode { - const node = createBaseNode(ts.SyntaxKind.JSDocLinkCode); - node.name = name; - node.text = text; - return node; - } + // @api + function updateJSDocNameReference(node: ts.JSDocNameReference, name: ts.EntityName | ts.JSDocMemberName): ts.JSDocNameReference { + return node.name !== name + ? update(createJSDocNameReference(name), node) + : node; + } - // @api - function updateJSDocLinkCode(node: ts.JSDocLinkCode, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkCode { - return node.name !== name - ? update(createJSDocLinkCode(name, text), node) - : node; - } + // @api + function createJSDocMemberName(left: ts.EntityName | ts.JSDocMemberName, right: ts.Identifier) { + const node = createBaseNode(ts.SyntaxKind.JSDocMemberName); + node.left = left; + node.right = right; + node.transformFlags |= + propagateChildFlags(node.left) | + propagateChildFlags(node.right); + return node; + } - // @api - function createJSDocLinkPlain(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkPlain { - const node = createBaseNode(ts.SyntaxKind.JSDocLinkPlain); - node.name = name; - node.text = text; - return node; - } + // @api + function updateJSDocMemberName(node: ts.JSDocMemberName, left: ts.EntityName | ts.JSDocMemberName, right: ts.Identifier) { + return node.left !== left + || node.right !== right + ? update(createJSDocMemberName(left, right), node) + : node; + } - // @api - function updateJSDocLinkPlain(node: ts.JSDocLinkPlain, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkPlain { - return node.name !== name - ? update(createJSDocLinkPlain(name, text), node) - : node; - } + // @api + function createJSDocLink(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLink { + const node = createBaseNode(ts.SyntaxKind.JSDocLink); + node.name = name; + node.text = text; + return node; + } - // @api - function updateJSDocImplementsTag(node: ts.JSDocImplementsTag, tagName: ts.Identifier = getDefaultTagName(node), className: ts.JSDocImplementsTag["class"], comment: string | ts.NodeArray | undefined): ts.JSDocImplementsTag { - return node.tagName !== tagName - || node.class !== className - || node.comment !== comment - ? update(createJSDocImplementsTag(tagName, className, comment), node) - : node; - } + // @api + function updateJSDocLink(node: ts.JSDocLink, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLink { + return node.name !== name + ? update(createJSDocLink(name, text), node) + : node; + } - // @api - // createJSDocAuthorTag - // createJSDocClassTag - // createJSDocPublicTag - // createJSDocPrivateTag - // createJSDocProtectedTag - // createJSDocReadonlyTag - // createJSDocDeprecatedTag - function createJSDocSimpleTagWorker(kind: T["kind"], tagName: ts.Identifier | undefined, comment?: string | ts.NodeArray) { - const node = createBaseJSDocTag(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment); - return node; - } + // @api + function createJSDocLinkCode(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkCode { + const node = createBaseNode(ts.SyntaxKind.JSDocLinkCode); + node.name = name; + node.text = text; + return node; + } - // @api - // updateJSDocAuthorTag - // updateJSDocClassTag - // updateJSDocPublicTag - // updateJSDocPrivateTag - // updateJSDocProtectedTag - // updateJSDocReadonlyTag - // updateJSDocDeprecatedTag - function updateJSDocSimpleTagWorker(kind: T["kind"], node: T, tagName: ts.Identifier = getDefaultTagName(node), comment: string | ts.NodeArray | undefined) { - return node.tagName !== tagName - || node.comment !== comment - ? update(createJSDocSimpleTagWorker(kind, tagName, comment), node) : - node; - } + // @api + function updateJSDocLinkCode(node: ts.JSDocLinkCode, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkCode { + return node.name !== name + ? update(createJSDocLinkCode(name, text), node) + : node; + } - // @api - // createJSDocTypeTag - // createJSDocReturnTag - // createJSDocThisTag - // createJSDocEnumTag - function createJSDocTypeLikeTagWorker(kind: T["kind"], tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: string | ts.NodeArray) { - const node = createBaseJSDocTag(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment); - node.typeExpression = typeExpression; - return node; - } + // @api + function createJSDocLinkPlain(name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkPlain { + const node = createBaseNode(ts.SyntaxKind.JSDocLinkPlain); + node.name = name; + node.text = text; + return node; + } - // @api - // updateJSDocTypeTag - // updateJSDocReturnTag - // updateJSDocThisTag - // updateJSDocEnumTag - function updateJSDocTypeLikeTagWorker(kind: T["kind"], node: T, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocTypeExpression | undefined, comment: string | ts.NodeArray | undefined) { - return node.tagName !== tagName - || node.typeExpression !== typeExpression - || node.comment !== comment - ? update(createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment), node) - : node; - } + // @api + function updateJSDocLinkPlain(node: ts.JSDocLinkPlain, name: ts.EntityName | ts.JSDocMemberName | undefined, text: string): ts.JSDocLinkPlain { + return node.name !== name + ? update(createJSDocLinkPlain(name, text), node) + : node; + } - // @api - function createJSDocUnknownTag(tagName: ts.Identifier, comment?: string | ts.NodeArray): ts.JSDocUnknownTag { - const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTag, tagName, comment); - return node; - } + // @api + function updateJSDocImplementsTag(node: ts.JSDocImplementsTag, tagName: ts.Identifier = getDefaultTagName(node), className: ts.JSDocImplementsTag["class"], comment: string | ts.NodeArray | undefined): ts.JSDocImplementsTag { + return node.tagName !== tagName + || node.class !== className + || node.comment !== comment + ? update(createJSDocImplementsTag(tagName, className, comment), node) + : node; + } - // @api - function updateJSDocUnknownTag(node: ts.JSDocUnknownTag, tagName: ts.Identifier, comment: string | ts.NodeArray | undefined): ts.JSDocUnknownTag { - return node.tagName !== tagName - || node.comment !== comment - ? update(createJSDocUnknownTag(tagName, comment), node) - : node; - } + // @api + // createJSDocAuthorTag + // createJSDocClassTag + // createJSDocPublicTag + // createJSDocPrivateTag + // createJSDocProtectedTag + // createJSDocReadonlyTag + // createJSDocDeprecatedTag + function createJSDocSimpleTagWorker(kind: T["kind"], tagName: ts.Identifier | undefined, comment?: string | ts.NodeArray) { + const node = createBaseJSDocTag(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment); + return node; + } - // @api - function createJSDocText(text: string): ts.JSDocText { - const node = createBaseNode(ts.SyntaxKind.JSDocText); - node.text = text; - return node; - } + // @api + // updateJSDocAuthorTag + // updateJSDocClassTag + // updateJSDocPublicTag + // updateJSDocPrivateTag + // updateJSDocProtectedTag + // updateJSDocReadonlyTag + // updateJSDocDeprecatedTag + function updateJSDocSimpleTagWorker(kind: T["kind"], node: T, tagName: ts.Identifier = getDefaultTagName(node), comment: string | ts.NodeArray | undefined) { + return node.tagName !== tagName + || node.comment !== comment + ? update(createJSDocSimpleTagWorker(kind, tagName, comment), node) : + node; + } - // @api - function updateJSDocText(node: ts.JSDocText, text: string): ts.JSDocText { - return node.text !== text - ? update(createJSDocText(text), node) - : node; - } + // @api + // createJSDocTypeTag + // createJSDocReturnTag + // createJSDocThisTag + // createJSDocEnumTag + function createJSDocTypeLikeTagWorker(kind: T["kind"], tagName: ts.Identifier | undefined, typeExpression?: ts.JSDocTypeExpression, comment?: string | ts.NodeArray) { + const node = createBaseJSDocTag(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment); + node.typeExpression = typeExpression; + return node; + } - // @api - function createJSDocComment(comment?: string | ts.NodeArray | undefined, tags?: readonly ts.JSDocTag[] | undefined) { - const node = createBaseNode(ts.SyntaxKind.JSDoc); - node.comment = comment; - node.tags = asNodeArray(tags); - return node; - } + // @api + // updateJSDocTypeTag + // updateJSDocReturnTag + // updateJSDocThisTag + // updateJSDocEnumTag + function updateJSDocTypeLikeTagWorker(kind: T["kind"], node: T, tagName: ts.Identifier = getDefaultTagName(node), typeExpression: ts.JSDocTypeExpression | undefined, comment: string | ts.NodeArray | undefined) { + return node.tagName !== tagName + || node.typeExpression !== typeExpression + || node.comment !== comment + ? update(createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment), node) + : node; + } - // @api - function updateJSDocComment(node: ts.JSDoc, comment: string | ts.NodeArray | undefined, tags: readonly ts.JSDocTag[] | undefined) { - return node.comment !== comment - || node.tags !== tags - ? update(createJSDocComment(comment, tags), node) - : node; - } + // @api + function createJSDocUnknownTag(tagName: ts.Identifier, comment?: string | ts.NodeArray): ts.JSDocUnknownTag { + const node = createBaseJSDocTag(ts.SyntaxKind.JSDocTag, tagName, comment); + return node; + } - // - // JSX - // + // @api + function updateJSDocUnknownTag(node: ts.JSDocUnknownTag, tagName: ts.Identifier, comment: string | ts.NodeArray | undefined): ts.JSDocUnknownTag { + return node.tagName !== tagName + || node.comment !== comment + ? update(createJSDocUnknownTag(tagName, comment), node) + : node; + } - // @api - function createJsxElement(openingElement: ts.JsxOpeningElement, children: readonly ts.JsxChild[], closingElement: ts.JsxClosingElement) { - const node = createBaseNode(ts.SyntaxKind.JsxElement); - node.openingElement = openingElement; - node.children = createNodeArray(children); - node.closingElement = closingElement; - node.transformFlags |= - propagateChildFlags(node.openingElement) | - propagateChildrenFlags(node.children) | - propagateChildFlags(node.closingElement) | - ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJSDocText(text: string): ts.JSDocText { + const node = createBaseNode(ts.SyntaxKind.JSDocText); + node.text = text; + return node; + } - // @api - function updateJsxElement(node: ts.JsxElement, openingElement: ts.JsxOpeningElement, children: readonly ts.JsxChild[], closingElement: ts.JsxClosingElement) { - return node.openingElement !== openingElement - || node.children !== children - || node.closingElement !== closingElement - ? update(createJsxElement(openingElement, children, closingElement), node) - : node; - } + // @api + function updateJSDocText(node: ts.JSDocText, text: string): ts.JSDocText { + return node.text !== text + ? update(createJSDocText(text), node) + : node; + } - // @api - function createJsxSelfClosingElement(tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { - const node = createBaseNode(ts.SyntaxKind.JsxSelfClosingElement); - node.tagName = tagName; - node.typeArguments = asNodeArray(typeArguments); - node.attributes = attributes; - node.transformFlags |= - propagateChildFlags(node.tagName) | - propagateChildrenFlags(node.typeArguments) | - propagateChildFlags(node.attributes) | - ts.TransformFlags.ContainsJsx; - if (node.typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - return node; - } + // @api + function createJSDocComment(comment?: string | ts.NodeArray | undefined, tags?: readonly ts.JSDocTag[] | undefined) { + const node = createBaseNode(ts.SyntaxKind.JSDoc); + node.comment = comment; + node.tags = asNodeArray(tags); + return node; + } - // @api - function updateJsxSelfClosingElement(node: ts.JsxSelfClosingElement, tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { - return node.tagName !== tagName - || node.typeArguments !== typeArguments - || node.attributes !== attributes - ? update(createJsxSelfClosingElement(tagName, typeArguments, attributes), node) - : node; - } + // @api + function updateJSDocComment(node: ts.JSDoc, comment: string | ts.NodeArray | undefined, tags: readonly ts.JSDocTag[] | undefined) { + return node.comment !== comment + || node.tags !== tags + ? update(createJSDocComment(comment, tags), node) + : node; + } - // @api - function createJsxOpeningElement(tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { - const node = createBaseNode(ts.SyntaxKind.JsxOpeningElement); - node.tagName = tagName; - node.typeArguments = asNodeArray(typeArguments); - node.attributes = attributes; - node.transformFlags |= - propagateChildFlags(node.tagName) | - propagateChildrenFlags(node.typeArguments) | - propagateChildFlags(node.attributes) | - ts.TransformFlags.ContainsJsx; - if (typeArguments) { - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - } - return node; - } + // + // JSX + // + + // @api + function createJsxElement(openingElement: ts.JsxOpeningElement, children: readonly ts.JsxChild[], closingElement: ts.JsxClosingElement) { + const node = createBaseNode(ts.SyntaxKind.JsxElement); + node.openingElement = openingElement; + node.children = createNodeArray(children); + node.closingElement = closingElement; + node.transformFlags |= + propagateChildFlags(node.openingElement) | + propagateChildrenFlags(node.children) | + propagateChildFlags(node.closingElement) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxOpeningElement(node: ts.JsxOpeningElement, tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { - return node.tagName !== tagName - || node.typeArguments !== typeArguments - || node.attributes !== attributes - ? update(createJsxOpeningElement(tagName, typeArguments, attributes), node) - : node; - } + // @api + function updateJsxElement(node: ts.JsxElement, openingElement: ts.JsxOpeningElement, children: readonly ts.JsxChild[], closingElement: ts.JsxClosingElement) { + return node.openingElement !== openingElement + || node.children !== children + || node.closingElement !== closingElement + ? update(createJsxElement(openingElement, children, closingElement), node) + : node; + } - // @api - function createJsxClosingElement(tagName: ts.JsxTagNameExpression) { - const node = createBaseNode(ts.SyntaxKind.JsxClosingElement); - node.tagName = tagName; - node.transformFlags |= - propagateChildFlags(node.tagName) | - ts.TransformFlags.ContainsJsx; - return node; + // @api + function createJsxSelfClosingElement(tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { + const node = createBaseNode(ts.SyntaxKind.JsxSelfClosingElement); + node.tagName = tagName; + node.typeArguments = asNodeArray(typeArguments); + node.attributes = attributes; + node.transformFlags |= + propagateChildFlags(node.tagName) | + propagateChildrenFlags(node.typeArguments) | + propagateChildFlags(node.attributes) | + ts.TransformFlags.ContainsJsx; + if (node.typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function updateJsxClosingElement(node: ts.JsxClosingElement, tagName: ts.JsxTagNameExpression) { - return node.tagName !== tagName - ? update(createJsxClosingElement(tagName), node) - : node; - } + // @api + function updateJsxSelfClosingElement(node: ts.JsxSelfClosingElement, tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { + return node.tagName !== tagName + || node.typeArguments !== typeArguments + || node.attributes !== attributes + ? update(createJsxSelfClosingElement(tagName, typeArguments, attributes), node) + : node; + } - // @api - function createJsxFragment(openingFragment: ts.JsxOpeningFragment, children: readonly ts.JsxChild[], closingFragment: ts.JsxClosingFragment) { - const node = createBaseNode(ts.SyntaxKind.JsxFragment); - node.openingFragment = openingFragment; - node.children = createNodeArray(children); - node.closingFragment = closingFragment; - node.transformFlags |= - propagateChildFlags(node.openingFragment) | - propagateChildrenFlags(node.children) | - propagateChildFlags(node.closingFragment) | - ts.TransformFlags.ContainsJsx; - return node; + // @api + function createJsxOpeningElement(tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { + const node = createBaseNode(ts.SyntaxKind.JsxOpeningElement); + node.tagName = tagName; + node.typeArguments = asNodeArray(typeArguments); + node.attributes = attributes; + node.transformFlags |= + propagateChildFlags(node.tagName) | + propagateChildrenFlags(node.typeArguments) | + propagateChildFlags(node.attributes) | + ts.TransformFlags.ContainsJsx; + if (typeArguments) { + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; } + return node; + } - // @api - function updateJsxFragment(node: ts.JsxFragment, openingFragment: ts.JsxOpeningFragment, children: readonly ts.JsxChild[], closingFragment: ts.JsxClosingFragment) { - return node.openingFragment !== openingFragment - || node.children !== children - || node.closingFragment !== closingFragment - ? update(createJsxFragment(openingFragment, children, closingFragment), node) - : node; - } + // @api + function updateJsxOpeningElement(node: ts.JsxOpeningElement, tagName: ts.JsxTagNameExpression, typeArguments: readonly ts.TypeNode[] | undefined, attributes: ts.JsxAttributes) { + return node.tagName !== tagName + || node.typeArguments !== typeArguments + || node.attributes !== attributes + ? update(createJsxOpeningElement(tagName, typeArguments, attributes), node) + : node; + } - // @api - function createJsxText(text: string, containsOnlyTriviaWhiteSpaces?: boolean) { - const node = createBaseNode(ts.SyntaxKind.JsxText); - node.text = text; - node.containsOnlyTriviaWhiteSpaces = !!containsOnlyTriviaWhiteSpaces; - node.transformFlags |= ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxClosingElement(tagName: ts.JsxTagNameExpression) { + const node = createBaseNode(ts.SyntaxKind.JsxClosingElement); + node.tagName = tagName; + node.transformFlags |= + propagateChildFlags(node.tagName) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxText(node: ts.JsxText, text: string, containsOnlyTriviaWhiteSpaces?: boolean) { - return node.text !== text - || node.containsOnlyTriviaWhiteSpaces !== containsOnlyTriviaWhiteSpaces - ? update(createJsxText(text, containsOnlyTriviaWhiteSpaces), node) - : node; - } + // @api + function updateJsxClosingElement(node: ts.JsxClosingElement, tagName: ts.JsxTagNameExpression) { + return node.tagName !== tagName + ? update(createJsxClosingElement(tagName), node) + : node; + } - // @api - function createJsxOpeningFragment() { - const node = createBaseNode(ts.SyntaxKind.JsxOpeningFragment); - node.transformFlags |= ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxFragment(openingFragment: ts.JsxOpeningFragment, children: readonly ts.JsxChild[], closingFragment: ts.JsxClosingFragment) { + const node = createBaseNode(ts.SyntaxKind.JsxFragment); + node.openingFragment = openingFragment; + node.children = createNodeArray(children); + node.closingFragment = closingFragment; + node.transformFlags |= + propagateChildFlags(node.openingFragment) | + propagateChildrenFlags(node.children) | + propagateChildFlags(node.closingFragment) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function createJsxJsxClosingFragment() { - const node = createBaseNode(ts.SyntaxKind.JsxClosingFragment); - node.transformFlags |= ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function updateJsxFragment(node: ts.JsxFragment, openingFragment: ts.JsxOpeningFragment, children: readonly ts.JsxChild[], closingFragment: ts.JsxClosingFragment) { + return node.openingFragment !== openingFragment + || node.children !== children + || node.closingFragment !== closingFragment + ? update(createJsxFragment(openingFragment, children, closingFragment), node) + : node; + } - // @api - function createJsxAttribute(name: ts.Identifier, initializer: ts.JsxAttributeValue | undefined) { - const node = createBaseNode(ts.SyntaxKind.JsxAttribute); - node.name = name; - node.initializer = initializer; - node.transformFlags |= - propagateChildFlags(node.name) | - propagateChildFlags(node.initializer) | - ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxText(text: string, containsOnlyTriviaWhiteSpaces?: boolean) { + const node = createBaseNode(ts.SyntaxKind.JsxText); + node.text = text; + node.containsOnlyTriviaWhiteSpaces = !!containsOnlyTriviaWhiteSpaces; + node.transformFlags |= ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxAttribute(node: ts.JsxAttribute, name: ts.Identifier, initializer: ts.JsxAttributeValue | undefined) { - return node.name !== name - || node.initializer !== initializer - ? update(createJsxAttribute(name, initializer), node) - : node; - } + // @api + function updateJsxText(node: ts.JsxText, text: string, containsOnlyTriviaWhiteSpaces?: boolean) { + return node.text !== text + || node.containsOnlyTriviaWhiteSpaces !== containsOnlyTriviaWhiteSpaces + ? update(createJsxText(text, containsOnlyTriviaWhiteSpaces), node) + : node; + } - // @api - function createJsxAttributes(properties: readonly ts.JsxAttributeLike[]) { - const node = createBaseNode(ts.SyntaxKind.JsxAttributes); - node.properties = createNodeArray(properties); - node.transformFlags |= - propagateChildrenFlags(node.properties) | - ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxOpeningFragment() { + const node = createBaseNode(ts.SyntaxKind.JsxOpeningFragment); + node.transformFlags |= ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxAttributes(node: ts.JsxAttributes, properties: readonly ts.JsxAttributeLike[]) { - return node.properties !== properties - ? update(createJsxAttributes(properties), node) - : node; - } + // @api + function createJsxJsxClosingFragment() { + const node = createBaseNode(ts.SyntaxKind.JsxClosingFragment); + node.transformFlags |= ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function createJsxSpreadAttribute(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.JsxSpreadAttribute); - node.expression = expression; - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxAttribute(name: ts.Identifier, initializer: ts.JsxAttributeValue | undefined) { + const node = createBaseNode(ts.SyntaxKind.JsxAttribute); + node.name = name; + node.initializer = initializer; + node.transformFlags |= + propagateChildFlags(node.name) | + propagateChildFlags(node.initializer) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxSpreadAttribute(node: ts.JsxSpreadAttribute, expression: ts.Expression) { - return node.expression !== expression - ? update(createJsxSpreadAttribute(expression), node) - : node; - } + // @api + function updateJsxAttribute(node: ts.JsxAttribute, name: ts.Identifier, initializer: ts.JsxAttributeValue | undefined) { + return node.name !== name + || node.initializer !== initializer + ? update(createJsxAttribute(name, initializer), node) + : node; + } - // @api - function createJsxExpression(dotDotDotToken: ts.DotDotDotToken | undefined, expression: ts.Expression | undefined) { - const node = createBaseNode(ts.SyntaxKind.JsxExpression); - node.dotDotDotToken = dotDotDotToken; - node.expression = expression; - node.transformFlags |= - propagateChildFlags(node.dotDotDotToken) | - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsJsx; - return node; - } + // @api + function createJsxAttributes(properties: readonly ts.JsxAttributeLike[]) { + const node = createBaseNode(ts.SyntaxKind.JsxAttributes); + node.properties = createNodeArray(properties); + node.transformFlags |= + propagateChildrenFlags(node.properties) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function updateJsxExpression(node: ts.JsxExpression, expression: ts.Expression | undefined) { - return node.expression !== expression - ? update(createJsxExpression(node.dotDotDotToken, expression), node) - : node; - } + // @api + function updateJsxAttributes(node: ts.JsxAttributes, properties: readonly ts.JsxAttributeLike[]) { + return node.properties !== properties + ? update(createJsxAttributes(properties), node) + : node; + } - // - // Clauses - // + // @api + function createJsxSpreadAttribute(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.JsxSpreadAttribute); + node.expression = expression; + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function createCaseClause(expression: ts.Expression, statements: readonly ts.Statement[]) { - const node = createBaseNode(ts.SyntaxKind.CaseClause); - node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.statements = createNodeArray(statements); - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildrenFlags(node.statements); - return node; - } + // @api + function updateJsxSpreadAttribute(node: ts.JsxSpreadAttribute, expression: ts.Expression) { + return node.expression !== expression + ? update(createJsxSpreadAttribute(expression), node) + : node; + } - // @api - function updateCaseClause(node: ts.CaseClause, expression: ts.Expression, statements: readonly ts.Statement[]) { - return node.expression !== expression - || node.statements !== statements - ? update(createCaseClause(expression, statements), node) - : node; - } + // @api + function createJsxExpression(dotDotDotToken: ts.DotDotDotToken | undefined, expression: ts.Expression | undefined) { + const node = createBaseNode(ts.SyntaxKind.JsxExpression); + node.dotDotDotToken = dotDotDotToken; + node.expression = expression; + node.transformFlags |= + propagateChildFlags(node.dotDotDotToken) | + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsJsx; + return node; + } - // @api - function createDefaultClause(statements: readonly ts.Statement[]) { - const node = createBaseNode(ts.SyntaxKind.DefaultClause); - node.statements = createNodeArray(statements); - node.transformFlags = propagateChildrenFlags(node.statements); - return node; - } + // @api + function updateJsxExpression(node: ts.JsxExpression, expression: ts.Expression | undefined) { + return node.expression !== expression + ? update(createJsxExpression(node.dotDotDotToken, expression), node) + : node; + } - // @api - function updateDefaultClause(node: ts.DefaultClause, statements: readonly ts.Statement[]) { - return node.statements !== statements - ? update(createDefaultClause(statements), node) - : node; - } + // + // Clauses + // + + // @api + function createCaseClause(expression: ts.Expression, statements: readonly ts.Statement[]) { + const node = createBaseNode(ts.SyntaxKind.CaseClause); + node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.statements = createNodeArray(statements); + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildrenFlags(node.statements); + return node; + } - // @api - function createHeritageClause(token: ts.HeritageClause["token"], types: readonly ts.ExpressionWithTypeArguments[]) { - const node = createBaseNode(ts.SyntaxKind.HeritageClause); - node.token = token; - node.types = createNodeArray(types); - node.transformFlags |= propagateChildrenFlags(node.types); - switch (token) { - case ts.SyntaxKind.ExtendsKeyword: - node.transformFlags |= ts.TransformFlags.ContainsES2015; - break; - case ts.SyntaxKind.ImplementsKeyword: - node.transformFlags |= ts.TransformFlags.ContainsTypeScript; - break; - default: - return ts.Debug.assertNever(token); - } - return node; - } + // @api + function updateCaseClause(node: ts.CaseClause, expression: ts.Expression, statements: readonly ts.Statement[]) { + return node.expression !== expression + || node.statements !== statements + ? update(createCaseClause(expression, statements), node) + : node; + } - // @api - function updateHeritageClause(node: ts.HeritageClause, types: readonly ts.ExpressionWithTypeArguments[]) { - return node.types !== types - ? update(createHeritageClause(node.token, types), node) - : node; - } + // @api + function createDefaultClause(statements: readonly ts.Statement[]) { + const node = createBaseNode(ts.SyntaxKind.DefaultClause); + node.statements = createNodeArray(statements); + node.transformFlags = propagateChildrenFlags(node.statements); + return node; + } - // @api - function createCatchClause(variableDeclaration: string | ts.BindingName | ts.VariableDeclaration | undefined, block: ts.Block) { - const node = createBaseNode(ts.SyntaxKind.CatchClause); - if (typeof variableDeclaration === "string" || variableDeclaration && !ts.isVariableDeclaration(variableDeclaration)) { - variableDeclaration = createVariableDeclaration(variableDeclaration, - /*exclamationToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined); - } - node.variableDeclaration = variableDeclaration; - node.block = block; - node.transformFlags |= - propagateChildFlags(node.variableDeclaration) | - propagateChildFlags(node.block); - if (!variableDeclaration) - node.transformFlags |= ts.TransformFlags.ContainsES2019; - return node; - } + // @api + function updateDefaultClause(node: ts.DefaultClause, statements: readonly ts.Statement[]) { + return node.statements !== statements + ? update(createDefaultClause(statements), node) + : node; + } - // @api - function updateCatchClause(node: ts.CatchClause, variableDeclaration: ts.VariableDeclaration | undefined, block: ts.Block) { - return node.variableDeclaration !== variableDeclaration - || node.block !== block - ? update(createCatchClause(variableDeclaration, block), node) - : node; + // @api + function createHeritageClause(token: ts.HeritageClause["token"], types: readonly ts.ExpressionWithTypeArguments[]) { + const node = createBaseNode(ts.SyntaxKind.HeritageClause); + node.token = token; + node.types = createNodeArray(types); + node.transformFlags |= propagateChildrenFlags(node.types); + switch (token) { + case ts.SyntaxKind.ExtendsKeyword: + node.transformFlags |= ts.TransformFlags.ContainsES2015; + break; + case ts.SyntaxKind.ImplementsKeyword: + node.transformFlags |= ts.TransformFlags.ContainsTypeScript; + break; + default: + return ts.Debug.assertNever(token); } + return node; + } - // - // Property assignments - // - - // @api - function createPropertyAssignment(name: string | ts.PropertyName, initializer: ts.Expression) { - const node = createBaseNamedDeclaration(ts.SyntaxKind.PropertyAssignment, - /*decorators*/ undefined, - /*modifiers*/ undefined, name); - node.initializer = parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer); - node.transformFlags |= - propagateChildFlags(node.name) | - propagateChildFlags(node.initializer); - return node; - } + // @api + function updateHeritageClause(node: ts.HeritageClause, types: readonly ts.ExpressionWithTypeArguments[]) { + return node.types !== types + ? update(createHeritageClause(node.token, types), node) + : node; + } - function finishUpdatePropertyAssignment(updated: ts.Mutable, original: ts.PropertyAssignment) { - // copy children used only for error reporting - if (original.decorators) - updated.decorators = original.decorators; - if (original.modifiers) - updated.modifiers = original.modifiers; - if (original.questionToken) - updated.questionToken = original.questionToken; - if (original.exclamationToken) - updated.exclamationToken = original.exclamationToken; - return update(updated, original); - } + // @api + function createCatchClause(variableDeclaration: string | ts.BindingName | ts.VariableDeclaration | undefined, block: ts.Block) { + const node = createBaseNode(ts.SyntaxKind.CatchClause); + if (typeof variableDeclaration === "string" || variableDeclaration && !ts.isVariableDeclaration(variableDeclaration)) { + variableDeclaration = createVariableDeclaration(variableDeclaration, + /*exclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined); + } + node.variableDeclaration = variableDeclaration; + node.block = block; + node.transformFlags |= + propagateChildFlags(node.variableDeclaration) | + propagateChildFlags(node.block); + if (!variableDeclaration) + node.transformFlags |= ts.TransformFlags.ContainsES2019; + return node; + } - // @api - function updatePropertyAssignment(node: ts.PropertyAssignment, name: ts.PropertyName, initializer: ts.Expression) { - return node.name !== name - || node.initializer !== initializer - ? finishUpdatePropertyAssignment(createPropertyAssignment(name, initializer), node) - : node; - } + // @api + function updateCatchClause(node: ts.CatchClause, variableDeclaration: ts.VariableDeclaration | undefined, block: ts.Block) { + return node.variableDeclaration !== variableDeclaration + || node.block !== block + ? update(createCatchClause(variableDeclaration, block), node) + : node; + } - // @api - function createShorthandPropertyAssignment(name: string | ts.Identifier, objectAssignmentInitializer?: ts.Expression) { - const node = createBaseNamedDeclaration(ts.SyntaxKind.ShorthandPropertyAssignment, - /*decorators*/ undefined, - /*modifiers*/ undefined, name); - node.objectAssignmentInitializer = objectAssignmentInitializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(objectAssignmentInitializer); - node.transformFlags |= - propagateChildFlags(node.objectAssignmentInitializer) | - ts.TransformFlags.ContainsES2015; - return node; - } + // + // Property assignments + // + + // @api + function createPropertyAssignment(name: string | ts.PropertyName, initializer: ts.Expression) { + const node = createBaseNamedDeclaration(ts.SyntaxKind.PropertyAssignment, + /*decorators*/ undefined, + /*modifiers*/ undefined, name); + node.initializer = parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer); + node.transformFlags |= + propagateChildFlags(node.name) | + propagateChildFlags(node.initializer); + return node; + } - function finishUpdateShorthandPropertyAssignment(updated: ts.Mutable, original: ts.ShorthandPropertyAssignment) { - // copy children used only for error reporting - if (original.decorators) - updated.decorators = original.decorators; - if (original.modifiers) - updated.modifiers = original.modifiers; - if (original.equalsToken) - updated.equalsToken = original.equalsToken; - if (original.questionToken) - updated.questionToken = original.questionToken; - if (original.exclamationToken) - updated.exclamationToken = original.exclamationToken; - return update(updated, original); - } + function finishUpdatePropertyAssignment(updated: ts.Mutable, original: ts.PropertyAssignment) { + // copy children used only for error reporting + if (original.decorators) + updated.decorators = original.decorators; + if (original.modifiers) + updated.modifiers = original.modifiers; + if (original.questionToken) + updated.questionToken = original.questionToken; + if (original.exclamationToken) + updated.exclamationToken = original.exclamationToken; + return update(updated, original); + } - // @api - function updateShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment, name: ts.Identifier, objectAssignmentInitializer: ts.Expression | undefined) { - return node.name !== name - || node.objectAssignmentInitializer !== objectAssignmentInitializer - ? finishUpdateShorthandPropertyAssignment(createShorthandPropertyAssignment(name, objectAssignmentInitializer), node) - : node; - } + // @api + function updatePropertyAssignment(node: ts.PropertyAssignment, name: ts.PropertyName, initializer: ts.Expression) { + return node.name !== name + || node.initializer !== initializer + ? finishUpdatePropertyAssignment(createPropertyAssignment(name, initializer), node) + : node; + } - // @api - function createSpreadAssignment(expression: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.SpreadAssignment); - node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsES2018 | - ts.TransformFlags.ContainsObjectRestOrSpread; - return node; - } + // @api + function createShorthandPropertyAssignment(name: string | ts.Identifier, objectAssignmentInitializer?: ts.Expression) { + const node = createBaseNamedDeclaration(ts.SyntaxKind.ShorthandPropertyAssignment, + /*decorators*/ undefined, + /*modifiers*/ undefined, name); + node.objectAssignmentInitializer = objectAssignmentInitializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(objectAssignmentInitializer); + node.transformFlags |= + propagateChildFlags(node.objectAssignmentInitializer) | + ts.TransformFlags.ContainsES2015; + return node; + } - // @api - function updateSpreadAssignment(node: ts.SpreadAssignment, expression: ts.Expression) { - return node.expression !== expression - ? update(createSpreadAssignment(expression), node) - : node; - } + function finishUpdateShorthandPropertyAssignment(updated: ts.Mutable, original: ts.ShorthandPropertyAssignment) { + // copy children used only for error reporting + if (original.decorators) + updated.decorators = original.decorators; + if (original.modifiers) + updated.modifiers = original.modifiers; + if (original.equalsToken) + updated.equalsToken = original.equalsToken; + if (original.questionToken) + updated.questionToken = original.questionToken; + if (original.exclamationToken) + updated.exclamationToken = original.exclamationToken; + return update(updated, original); + } - // - // Enum - // + // @api + function updateShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment, name: ts.Identifier, objectAssignmentInitializer: ts.Expression | undefined) { + return node.name !== name + || node.objectAssignmentInitializer !== objectAssignmentInitializer + ? finishUpdateShorthandPropertyAssignment(createShorthandPropertyAssignment(name, objectAssignmentInitializer), node) + : node; + } - // @api - function createEnumMember(name: string | ts.PropertyName, initializer?: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.EnumMember); - node.name = asName(name); - node.initializer = initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer); - node.transformFlags |= - propagateChildFlags(node.name) | - propagateChildFlags(node.initializer) | - ts.TransformFlags.ContainsTypeScript; - return node; - } + // @api + function createSpreadAssignment(expression: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.SpreadAssignment); + node.expression = parenthesizerRules().parenthesizeExpressionForDisallowedComma(expression); + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsES2018 | + ts.TransformFlags.ContainsObjectRestOrSpread; + return node; + } - // @api - function updateEnumMember(node: ts.EnumMember, name: ts.PropertyName, initializer: ts.Expression | undefined) { - return node.name !== name - || node.initializer !== initializer - ? update(createEnumMember(name, initializer), node) - : node; - } + // @api + function updateSpreadAssignment(node: ts.SpreadAssignment, expression: ts.Expression) { + return node.expression !== expression + ? update(createSpreadAssignment(expression), node) + : node; + } - // - // Top-level nodes - // + // + // Enum + // + + // @api + function createEnumMember(name: string | ts.PropertyName, initializer?: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.EnumMember); + node.name = asName(name); + node.initializer = initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer); + node.transformFlags |= + propagateChildFlags(node.name) | + propagateChildFlags(node.initializer) | + ts.TransformFlags.ContainsTypeScript; + return node; + } - // @api - function createSourceFile(statements: readonly ts.Statement[], endOfFileToken: ts.EndOfFileToken, flags: ts.NodeFlags) { - const node = baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile) as ts.Mutable; - node.statements = createNodeArray(statements); - node.endOfFileToken = endOfFileToken; - node.flags |= flags; - node.fileName = ""; - node.text = ""; - node.languageVersion = 0; - node.languageVariant = 0; - node.scriptKind = 0; - node.isDeclarationFile = false; - node.hasNoDefaultLib = false; - node.transformFlags |= - propagateChildrenFlags(node.statements) | - propagateChildFlags(node.endOfFileToken); - return node; - } + // @api + function updateEnumMember(node: ts.EnumMember, name: ts.PropertyName, initializer: ts.Expression | undefined) { + return node.name !== name + || node.initializer !== initializer + ? update(createEnumMember(name, initializer), node) + : node; + } - function cloneSourceFileWithChanges(source: ts.SourceFile, statements: readonly ts.Statement[], isDeclarationFile: boolean, referencedFiles: readonly ts.FileReference[], typeReferences: readonly ts.FileReference[], hasNoDefaultLib: boolean, libReferences: readonly ts.FileReference[]) { - const node = (source.redirectInfo ? Object.create(source.redirectInfo.redirectTarget) : baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile)) as ts.Mutable; - for (const p in source) { - if (p === "emitNode" || ts.hasProperty(node, p) || !ts.hasProperty(source, p)) - continue; - (node as any)[p] = (source as any)[p]; - } - node.flags |= source.flags; - node.statements = createNodeArray(statements); - node.endOfFileToken = source.endOfFileToken; - node.isDeclarationFile = isDeclarationFile; - node.referencedFiles = referencedFiles; - node.typeReferenceDirectives = typeReferences; - node.hasNoDefaultLib = hasNoDefaultLib; - node.libReferenceDirectives = libReferences; - node.transformFlags = - propagateChildrenFlags(node.statements) | - propagateChildFlags(node.endOfFileToken); - node.impliedNodeFormat = source.impliedNodeFormat; - return node; - } + // + // Top-level nodes + // + + // @api + function createSourceFile(statements: readonly ts.Statement[], endOfFileToken: ts.EndOfFileToken, flags: ts.NodeFlags) { + const node = baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile) as ts.Mutable; + node.statements = createNodeArray(statements); + node.endOfFileToken = endOfFileToken; + node.flags |= flags; + node.fileName = ""; + node.text = ""; + node.languageVersion = 0; + node.languageVariant = 0; + node.scriptKind = 0; + node.isDeclarationFile = false; + node.hasNoDefaultLib = false; + node.transformFlags |= + propagateChildrenFlags(node.statements) | + propagateChildFlags(node.endOfFileToken); + return node; + } - // @api - function updateSourceFile(node: ts.SourceFile, statements: readonly ts.Statement[], isDeclarationFile = node.isDeclarationFile, referencedFiles = node.referencedFiles, typeReferenceDirectives = node.typeReferenceDirectives, hasNoDefaultLib = node.hasNoDefaultLib, libReferenceDirectives = node.libReferenceDirectives) { - return node.statements !== statements - || node.isDeclarationFile !== isDeclarationFile - || node.referencedFiles !== referencedFiles - || node.typeReferenceDirectives !== typeReferenceDirectives - || node.hasNoDefaultLib !== hasNoDefaultLib - || node.libReferenceDirectives !== libReferenceDirectives - ? update(cloneSourceFileWithChanges(node, statements, isDeclarationFile, referencedFiles, typeReferenceDirectives, hasNoDefaultLib, libReferenceDirectives), node) - : node; - } + function cloneSourceFileWithChanges(source: ts.SourceFile, statements: readonly ts.Statement[], isDeclarationFile: boolean, referencedFiles: readonly ts.FileReference[], typeReferences: readonly ts.FileReference[], hasNoDefaultLib: boolean, libReferences: readonly ts.FileReference[]) { + const node = (source.redirectInfo ? Object.create(source.redirectInfo.redirectTarget) : baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile)) as ts.Mutable; + for (const p in source) { + if (p === "emitNode" || ts.hasProperty(node, p) || !ts.hasProperty(source, p)) + continue; + (node as any)[p] = (source as any)[p]; + } + node.flags |= source.flags; + node.statements = createNodeArray(statements); + node.endOfFileToken = source.endOfFileToken; + node.isDeclarationFile = isDeclarationFile; + node.referencedFiles = referencedFiles; + node.typeReferenceDirectives = typeReferences; + node.hasNoDefaultLib = hasNoDefaultLib; + node.libReferenceDirectives = libReferences; + node.transformFlags = + propagateChildrenFlags(node.statements) | + propagateChildFlags(node.endOfFileToken); + node.impliedNodeFormat = source.impliedNodeFormat; + return node; + } - // @api - function createBundle(sourceFiles: readonly ts.SourceFile[], prepends: readonly (ts.UnparsedSource | ts.InputFiles)[] = ts.emptyArray) { - const node = createBaseNode(ts.SyntaxKind.Bundle); - node.prepends = prepends; - node.sourceFiles = sourceFiles; - return node; - } + // @api + function updateSourceFile(node: ts.SourceFile, statements: readonly ts.Statement[], isDeclarationFile = node.isDeclarationFile, referencedFiles = node.referencedFiles, typeReferenceDirectives = node.typeReferenceDirectives, hasNoDefaultLib = node.hasNoDefaultLib, libReferenceDirectives = node.libReferenceDirectives) { + return node.statements !== statements + || node.isDeclarationFile !== isDeclarationFile + || node.referencedFiles !== referencedFiles + || node.typeReferenceDirectives !== typeReferenceDirectives + || node.hasNoDefaultLib !== hasNoDefaultLib + || node.libReferenceDirectives !== libReferenceDirectives + ? update(cloneSourceFileWithChanges(node, statements, isDeclarationFile, referencedFiles, typeReferenceDirectives, hasNoDefaultLib, libReferenceDirectives), node) + : node; + } - // @api - function updateBundle(node: ts.Bundle, sourceFiles: readonly ts.SourceFile[], prepends: readonly (ts.UnparsedSource | ts.InputFiles)[] = ts.emptyArray) { - return node.sourceFiles !== sourceFiles - || node.prepends !== prepends - ? update(createBundle(sourceFiles, prepends), node) - : node; - } + // @api + function createBundle(sourceFiles: readonly ts.SourceFile[], prepends: readonly (ts.UnparsedSource | ts.InputFiles)[] = ts.emptyArray) { + const node = createBaseNode(ts.SyntaxKind.Bundle); + node.prepends = prepends; + node.sourceFiles = sourceFiles; + return node; + } - // @api - function createUnparsedSource(prologues: readonly ts.UnparsedPrologue[], syntheticReferences: readonly ts.UnparsedSyntheticReference[] | undefined, texts: readonly ts.UnparsedSourceText[]) { - const node = createBaseNode(ts.SyntaxKind.UnparsedSource); - node.prologues = prologues; - node.syntheticReferences = syntheticReferences; - node.texts = texts; - node.fileName = ""; - node.text = ""; - node.referencedFiles = ts.emptyArray; - node.libReferenceDirectives = ts.emptyArray; - node.getLineAndCharacterOfPosition = pos => ts.getLineAndCharacterOfPosition(node, pos); - return node; - } + // @api + function updateBundle(node: ts.Bundle, sourceFiles: readonly ts.SourceFile[], prepends: readonly (ts.UnparsedSource | ts.InputFiles)[] = ts.emptyArray) { + return node.sourceFiles !== sourceFiles + || node.prepends !== prepends + ? update(createBundle(sourceFiles, prepends), node) + : node; + } - function createBaseUnparsedNode(kind: T["kind"], data?: string) { - const node = createBaseNode(kind); - node.data = data; - return node; - } + // @api + function createUnparsedSource(prologues: readonly ts.UnparsedPrologue[], syntheticReferences: readonly ts.UnparsedSyntheticReference[] | undefined, texts: readonly ts.UnparsedSourceText[]) { + const node = createBaseNode(ts.SyntaxKind.UnparsedSource); + node.prologues = prologues; + node.syntheticReferences = syntheticReferences; + node.texts = texts; + node.fileName = ""; + node.text = ""; + node.referencedFiles = ts.emptyArray; + node.libReferenceDirectives = ts.emptyArray; + node.getLineAndCharacterOfPosition = pos => ts.getLineAndCharacterOfPosition(node, pos); + return node; + } - // @api - function createUnparsedPrologue(data?: string): ts.UnparsedPrologue { - return createBaseUnparsedNode(ts.SyntaxKind.UnparsedPrologue, data); - } + function createBaseUnparsedNode(kind: T["kind"], data?: string) { + const node = createBaseNode(kind); + node.data = data; + return node; + } - // @api - function createUnparsedPrepend(data: string | undefined, texts: readonly ts.UnparsedTextLike[]): ts.UnparsedPrepend { - const node = createBaseUnparsedNode(ts.SyntaxKind.UnparsedPrepend, data); - node.texts = texts; - return node; - } + // @api + function createUnparsedPrologue(data?: string): ts.UnparsedPrologue { + return createBaseUnparsedNode(ts.SyntaxKind.UnparsedPrologue, data); + } - // @api - function createUnparsedTextLike(data: string | undefined, internal: boolean): ts.UnparsedTextLike { - return createBaseUnparsedNode(internal ? ts.SyntaxKind.UnparsedInternalText : ts.SyntaxKind.UnparsedText, data); - } + // @api + function createUnparsedPrepend(data: string | undefined, texts: readonly ts.UnparsedTextLike[]): ts.UnparsedPrepend { + const node = createBaseUnparsedNode(ts.SyntaxKind.UnparsedPrepend, data); + node.texts = texts; + return node; + } - // @api - function createUnparsedSyntheticReference(section: ts.BundleFileHasNoDefaultLib | ts.BundleFileReference): ts.UnparsedSyntheticReference { - const node = createBaseNode(ts.SyntaxKind.UnparsedSyntheticReference); - node.data = section.data; - node.section = section; - return node; - } + // @api + function createUnparsedTextLike(data: string | undefined, internal: boolean): ts.UnparsedTextLike { + return createBaseUnparsedNode(internal ? ts.SyntaxKind.UnparsedInternalText : ts.SyntaxKind.UnparsedText, data); + } - // @api - function createInputFiles(): ts.InputFiles { - const node = createBaseNode(ts.SyntaxKind.InputFiles); - node.javascriptText = ""; - node.declarationText = ""; - return node; - } + // @api + function createUnparsedSyntheticReference(section: ts.BundleFileHasNoDefaultLib | ts.BundleFileReference): ts.UnparsedSyntheticReference { + const node = createBaseNode(ts.SyntaxKind.UnparsedSyntheticReference); + node.data = section.data; + node.section = section; + return node; + } - // - // Synthetic Nodes (used by checker) - // + // @api + function createInputFiles(): ts.InputFiles { + const node = createBaseNode(ts.SyntaxKind.InputFiles); + node.javascriptText = ""; + node.declarationText = ""; + return node; + } - // @api - function createSyntheticExpression(type: ts.Type, isSpread = false, tupleNameSource?: ts.ParameterDeclaration | ts.NamedTupleMember) { - const node = createBaseNode(ts.SyntaxKind.SyntheticExpression); - node.type = type; - node.isSpread = isSpread; - node.tupleNameSource = tupleNameSource; - return node; - } + // + // Synthetic Nodes (used by checker) + // - // @api - function createSyntaxList(children: ts.Node[]) { - const node = createBaseNode(ts.SyntaxKind.SyntaxList); - node._children = children; - return node; - } + // @api + function createSyntheticExpression(type: ts.Type, isSpread = false, tupleNameSource?: ts.ParameterDeclaration | ts.NamedTupleMember) { + const node = createBaseNode(ts.SyntaxKind.SyntheticExpression); + node.type = type; + node.isSpread = isSpread; + node.tupleNameSource = tupleNameSource; + return node; + } - // - // Transformation nodes - // + // @api + function createSyntaxList(children: ts.Node[]) { + const node = createBaseNode(ts.SyntaxKind.SyntaxList); + node._children = children; + return node; + } - /** - * Creates a synthetic statement to act as a placeholder for a not-emitted statement in - * order to preserve comments. - * - * @param original The original statement. - */ - // @api - function createNotEmittedStatement(original: ts.Node) { - const node = createBaseNode(ts.SyntaxKind.NotEmittedStatement); - node.original = original; - ts.setTextRange(node, original); - return node; - } + // + // Transformation nodes + // - /** - * 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. - */ - // @api - function createPartiallyEmittedExpression(expression: ts.Expression, original?: ts.Node) { - const node = createBaseNode(ts.SyntaxKind.PartiallyEmittedExpression); - node.expression = expression; - node.original = original; - node.transformFlags |= - propagateChildFlags(node.expression) | - ts.TransformFlags.ContainsTypeScript; - ts.setTextRange(node, original); - return node; - } + /** + * Creates a synthetic statement to act as a placeholder for a not-emitted statement in + * order to preserve comments. + * + * @param original The original statement. + */ + // @api + function createNotEmittedStatement(original: ts.Node) { + const node = createBaseNode(ts.SyntaxKind.NotEmittedStatement); + node.original = original; + ts.setTextRange(node, original); + return node; + } - // @api - function updatePartiallyEmittedExpression(node: ts.PartiallyEmittedExpression, expression: ts.Expression) { - return node.expression !== expression - ? update(createPartiallyEmittedExpression(expression, node.original), node) - : 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. + */ + // @api + function createPartiallyEmittedExpression(expression: ts.Expression, original?: ts.Node) { + const node = createBaseNode(ts.SyntaxKind.PartiallyEmittedExpression); + node.expression = expression; + node.original = original; + node.transformFlags |= + propagateChildFlags(node.expression) | + ts.TransformFlags.ContainsTypeScript; + ts.setTextRange(node, original); + return node; + } - function flattenCommaElements(node: ts.Expression): ts.Expression | readonly ts.Expression[] { - if (ts.nodeIsSynthesized(node) && !ts.isParseTreeNode(node) && !node.original && !node.emitNode && !node.id) { - if (ts.isCommaListExpression(node)) { - return node.elements; - } - if (ts.isBinaryExpression(node) && ts.isCommaToken(node.operatorToken)) { - return [node.left, node.right]; - } - } - return node; - } + // @api + function updatePartiallyEmittedExpression(node: ts.PartiallyEmittedExpression, expression: ts.Expression) { + return node.expression !== expression + ? update(createPartiallyEmittedExpression(expression, node.original), node) + : node; + } - // @api - function createCommaListExpression(elements: readonly ts.Expression[]) { - const node = createBaseNode(ts.SyntaxKind.CommaListExpression); - node.elements = createNodeArray(ts.sameFlatMap(elements, flattenCommaElements)); - node.transformFlags |= propagateChildrenFlags(node.elements); - return node; + function flattenCommaElements(node: ts.Expression): ts.Expression | readonly ts.Expression[] { + if (ts.nodeIsSynthesized(node) && !ts.isParseTreeNode(node) && !node.original && !node.emitNode && !node.id) { + if (ts.isCommaListExpression(node)) { + return node.elements; + } + if (ts.isBinaryExpression(node) && ts.isCommaToken(node.operatorToken)) { + return [node.left, node.right]; + } } + return node; + } - // @api - function updateCommaListExpression(node: ts.CommaListExpression, elements: readonly ts.Expression[]) { - return node.elements !== elements - ? update(createCommaListExpression(elements), node) - : node; - } + // @api + function createCommaListExpression(elements: readonly ts.Expression[]) { + const node = createBaseNode(ts.SyntaxKind.CommaListExpression); + node.elements = createNodeArray(ts.sameFlatMap(elements, flattenCommaElements)); + node.transformFlags |= propagateChildrenFlags(node.elements); + return node; + } - /** - * Creates a synthetic element to act as a placeholder for the end of an emitted declaration in - * order to properly emit exports. - */ - // @api - function createEndOfDeclarationMarker(original: ts.Node) { - const node = createBaseNode(ts.SyntaxKind.EndOfDeclarationMarker); - node.emitNode = {} as ts.EmitNode; - node.original = original; - return node; - } + // @api + function updateCommaListExpression(node: ts.CommaListExpression, elements: readonly ts.Expression[]) { + return node.elements !== elements + ? update(createCommaListExpression(elements), node) + : node; + } - /** - * Creates a synthetic element to act as a placeholder for the beginning of a merged declaration in - * order to properly emit exports. - */ - // @api - function createMergeDeclarationMarker(original: ts.Node) { - const node = createBaseNode(ts.SyntaxKind.MergeDeclarationMarker); - node.emitNode = {} as ts.EmitNode; - node.original = 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. + */ + // @api + function createEndOfDeclarationMarker(original: ts.Node) { + const node = createBaseNode(ts.SyntaxKind.EndOfDeclarationMarker); + node.emitNode = {} as ts.EmitNode; + node.original = original; + return node; + } - // @api - function createSyntheticReferenceExpression(expression: ts.Expression, thisArg: ts.Expression) { - const node = createBaseNode(ts.SyntaxKind.SyntheticReferenceExpression); - node.expression = expression; - node.thisArg = thisArg; - node.transformFlags |= - propagateChildFlags(node.expression) | - propagateChildFlags(node.thisArg); - return node; - } + /** + * Creates a synthetic element to act as a placeholder for the beginning of a merged declaration in + * order to properly emit exports. + */ + // @api + function createMergeDeclarationMarker(original: ts.Node) { + const node = createBaseNode(ts.SyntaxKind.MergeDeclarationMarker); + node.emitNode = {} as ts.EmitNode; + node.original = original; + return node; + } - // @api - function updateSyntheticReferenceExpression(node: ts.SyntheticReferenceExpression, expression: ts.Expression, thisArg: ts.Expression) { - return node.expression !== expression - || node.thisArg !== thisArg - ? update(createSyntheticReferenceExpression(expression, thisArg), node) - : node; - } + // @api + function createSyntheticReferenceExpression(expression: ts.Expression, thisArg: ts.Expression) { + const node = createBaseNode(ts.SyntaxKind.SyntheticReferenceExpression); + node.expression = expression; + node.thisArg = thisArg; + node.transformFlags |= + propagateChildFlags(node.expression) | + propagateChildFlags(node.thisArg); + return node; + } - // @api - function cloneNode(node: T): T; - function cloneNode(node: 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; - } + // @api + function updateSyntheticReferenceExpression(node: ts.SyntheticReferenceExpression, expression: ts.Expression, thisArg: ts.Expression) { + return node.expression !== expression + || node.thisArg !== thisArg + ? update(createSyntheticReferenceExpression(expression, thisArg), node) + : node; + } - const clone = ts.isSourceFile(node) ? baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile) as T : - ts.isIdentifier(node) ? baseFactory.createBaseIdentifierNode(ts.SyntaxKind.Identifier) as T : - ts.isPrivateIdentifier(node) ? baseFactory.createBasePrivateIdentifierNode(ts.SyntaxKind.PrivateIdentifier) as T : - !ts.isNodeKind(node.kind) ? baseFactory.createBaseTokenNode(node.kind) as T : - baseFactory.createBaseNode(node.kind) as T; + // @api + function cloneNode(node: T): T; + function cloneNode(node: 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; + } - (clone as ts.Mutable).flags |= (node.flags & ~ts.NodeFlags.Synthesized); - (clone as ts.Mutable).transformFlags = node.transformFlags; - setOriginalNode(clone, node); + const clone = ts.isSourceFile(node) ? baseFactory.createBaseSourceFileNode(ts.SyntaxKind.SourceFile) as T : + ts.isIdentifier(node) ? baseFactory.createBaseIdentifierNode(ts.SyntaxKind.Identifier) as T : + ts.isPrivateIdentifier(node) ? baseFactory.createBasePrivateIdentifierNode(ts.SyntaxKind.PrivateIdentifier) as T : + !ts.isNodeKind(node.kind) ? baseFactory.createBaseTokenNode(node.kind) as T : + baseFactory.createBaseNode(node.kind) as T; - for (const key in node) { - if (clone.hasOwnProperty(key) || !node.hasOwnProperty(key)) { - continue; - } + (clone as ts.Mutable).flags |= (node.flags & ~ts.NodeFlags.Synthesized); + (clone as ts.Mutable).transformFlags = node.transformFlags; + setOriginalNode(clone, node); - clone[key] = node[key]; + for (const key in node) { + if (clone.hasOwnProperty(key) || !node.hasOwnProperty(key)) { + continue; } - return clone; - } - - // compound nodes - function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[]): ts.CallExpression; - function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[], param: ts.ParameterDeclaration, paramValue: ts.Expression): ts.CallExpression; - function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[], param?: ts.ParameterDeclaration, paramValue?: ts.Expression) { - return createCallExpression(createFunctionExpression( - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, - /*parameters*/ param ? [param] : [], - /*type*/ undefined, createBlock(statements, /*multiLine*/ true)), - /*typeArguments*/ undefined, - /*argumentsArray*/ paramValue ? [paramValue] : []); - } - function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[]): ts.CallExpression; - function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[], param: ts.ParameterDeclaration, paramValue: ts.Expression): ts.CallExpression; - function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[], param?: ts.ParameterDeclaration, paramValue?: ts.Expression) { - return createCallExpression(createArrowFunction( - /*modifiers*/ undefined, - /*typeParameters*/ undefined, - /*parameters*/ param ? [param] : [], - /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, createBlock(statements, /*multiLine*/ true)), - /*typeArguments*/ undefined, - /*argumentsArray*/ paramValue ? [paramValue] : []); + clone[key] = node[key]; } - function createVoidZero() { - return createVoidExpression(createNumericLiteral("0")); - } + return clone; + } - function createExportDefault(expression: ts.Expression) { - return createExportAssignment( - /*decorators*/ undefined, + // compound nodes + function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[]): ts.CallExpression; + function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[], param: ts.ParameterDeclaration, paramValue: ts.Expression): ts.CallExpression; + function createImmediatelyInvokedFunctionExpression(statements: readonly ts.Statement[], param?: ts.ParameterDeclaration, paramValue?: ts.Expression) { + return createCallExpression(createFunctionExpression( /*modifiers*/ undefined, - /*isExportEquals*/ false, expression); - } - - function createExternalModuleExport(exportName: ts.Identifier) { - return createExportDeclaration( - /*decorators*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, + /*parameters*/ param ? [param] : [], + /*type*/ undefined, createBlock(statements, /*multiLine*/ true)), + /*typeArguments*/ undefined, + /*argumentsArray*/ paramValue ? [paramValue] : []); + } + function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[]): ts.CallExpression; + function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[], param: ts.ParameterDeclaration, paramValue: ts.Expression): ts.CallExpression; + function createImmediatelyInvokedArrowFunction(statements: readonly ts.Statement[], param?: ts.ParameterDeclaration, paramValue?: ts.Expression) { + return createCallExpression(createArrowFunction( /*modifiers*/ undefined, - /*isTypeOnly*/ false, createNamedExports([ - createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, exportName) - ])); - } + /*typeParameters*/ undefined, + /*parameters*/ param ? [param] : [], + /*type*/ undefined, + /*equalsGreaterThanToken*/ undefined, createBlock(statements, /*multiLine*/ true)), + /*typeArguments*/ undefined, + /*argumentsArray*/ paramValue ? [paramValue] : []); + } - // - // Utilities - // + function createVoidZero() { + return createVoidExpression(createNumericLiteral("0")); + } - function createTypeCheck(value: ts.Expression, tag: ts.TypeOfTag) { - return tag === "undefined" - ? factory.createStrictEquality(value, createVoidZero()) - : factory.createStrictEquality(createTypeOfExpression(value), createStringLiteral(tag)); - } + function createExportDefault(expression: ts.Expression) { + return createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isExportEquals*/ false, expression); + } - function createMethodCall(object: ts.Expression, methodName: string | ts.Identifier, argumentsList: readonly ts.Expression[]) { - // Preserve the optionality of `object`. - if (ts.isCallChain(object)) { - return createCallChain(createPropertyAccessChain(object, /*questionDotToken*/ undefined, methodName), - /*questionDotToken*/ undefined, - /*typeArguments*/ undefined, argumentsList); - } - return createCallExpression(createPropertyAccessExpression(object, methodName), + function createExternalModuleExport(exportName: ts.Identifier) { + return createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, createNamedExports([ + createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, exportName) + ])); + } + + // + // Utilities + // + + function createTypeCheck(value: ts.Expression, tag: ts.TypeOfTag) { + return tag === "undefined" + ? factory.createStrictEquality(value, createVoidZero()) + : factory.createStrictEquality(createTypeOfExpression(value), createStringLiteral(tag)); + } + + function createMethodCall(object: ts.Expression, methodName: string | ts.Identifier, argumentsList: readonly ts.Expression[]) { + // Preserve the optionality of `object`. + if (ts.isCallChain(object)) { + return createCallChain(createPropertyAccessChain(object, /*questionDotToken*/ undefined, methodName), + /*questionDotToken*/ undefined, /*typeArguments*/ undefined, argumentsList); } + return createCallExpression(createPropertyAccessExpression(object, methodName), + /*typeArguments*/ undefined, argumentsList); + } - function createFunctionBindCall(target: ts.Expression, thisArg: ts.Expression, argumentsList: readonly ts.Expression[]) { - return createMethodCall(target, "bind", [thisArg, ...argumentsList]); - } + function createFunctionBindCall(target: ts.Expression, thisArg: ts.Expression, argumentsList: readonly ts.Expression[]) { + return createMethodCall(target, "bind", [thisArg, ...argumentsList]); + } - function createFunctionCallCall(target: ts.Expression, thisArg: ts.Expression, argumentsList: readonly ts.Expression[]) { - return createMethodCall(target, "call", [thisArg, ...argumentsList]); - } + function createFunctionCallCall(target: ts.Expression, thisArg: ts.Expression, argumentsList: readonly ts.Expression[]) { + return createMethodCall(target, "call", [thisArg, ...argumentsList]); + } - function createFunctionApplyCall(target: ts.Expression, thisArg: ts.Expression, argumentsExpression: ts.Expression) { - return createMethodCall(target, "apply", [thisArg, argumentsExpression]); - } + function createFunctionApplyCall(target: ts.Expression, thisArg: ts.Expression, argumentsExpression: ts.Expression) { + return createMethodCall(target, "apply", [thisArg, argumentsExpression]); + } - function createGlobalMethodCall(globalObjectName: string, methodName: string, argumentsList: readonly ts.Expression[]) { - return createMethodCall(createIdentifier(globalObjectName), methodName, argumentsList); - } + function createGlobalMethodCall(globalObjectName: string, methodName: string, argumentsList: readonly ts.Expression[]) { + return createMethodCall(createIdentifier(globalObjectName), methodName, argumentsList); + } - function createArraySliceCall(array: ts.Expression, start?: number | ts.Expression) { - return createMethodCall(array, "slice", start === undefined ? [] : [asExpression(start)]); - } + function createArraySliceCall(array: ts.Expression, start?: number | ts.Expression) { + return createMethodCall(array, "slice", start === undefined ? [] : [asExpression(start)]); + } - function createArrayConcatCall(array: ts.Expression, argumentsList: readonly ts.Expression[]) { - return createMethodCall(array, "concat", argumentsList); - } + function createArrayConcatCall(array: ts.Expression, argumentsList: readonly ts.Expression[]) { + return createMethodCall(array, "concat", argumentsList); + } - function createObjectDefinePropertyCall(target: ts.Expression, propertyName: string | ts.Expression, attributes: ts.Expression) { - return createGlobalMethodCall("Object", "defineProperty", [target, asExpression(propertyName), attributes]); - } + function createObjectDefinePropertyCall(target: ts.Expression, propertyName: string | ts.Expression, attributes: ts.Expression) { + return createGlobalMethodCall("Object", "defineProperty", [target, asExpression(propertyName), attributes]); + } - function createReflectGetCall(target: ts.Expression, propertyKey: ts.Expression, receiver?: ts.Expression): ts.CallExpression { - return createGlobalMethodCall("Reflect", "get", receiver ? [target, propertyKey, receiver] : [target, propertyKey]); - } + function createReflectGetCall(target: ts.Expression, propertyKey: ts.Expression, receiver?: ts.Expression): ts.CallExpression { + return createGlobalMethodCall("Reflect", "get", receiver ? [target, propertyKey, receiver] : [target, propertyKey]); + } - function createReflectSetCall(target: ts.Expression, propertyKey: ts.Expression, value: ts.Expression, receiver?: ts.Expression): ts.CallExpression { - return createGlobalMethodCall("Reflect", "set", receiver ? [target, propertyKey, value, receiver] : [target, propertyKey, value]); - } + function createReflectSetCall(target: ts.Expression, propertyKey: ts.Expression, value: ts.Expression, receiver?: ts.Expression): ts.CallExpression { + return createGlobalMethodCall("Reflect", "set", receiver ? [target, propertyKey, value, receiver] : [target, propertyKey, value]); + } - function tryAddPropertyAssignment(properties: ts.Push, propertyName: string, expression: ts.Expression | undefined) { - if (expression) { - properties.push(createPropertyAssignment(propertyName, expression)); - return true; - } - return false; + function tryAddPropertyAssignment(properties: ts.Push, propertyName: string, expression: ts.Expression | undefined) { + if (expression) { + properties.push(createPropertyAssignment(propertyName, expression)); + return true; } + return false; + } - function createPropertyDescriptor(attributes: ts.PropertyDescriptorAttributes, singleLine?: boolean) { - const properties: ts.PropertyAssignment[] = []; - tryAddPropertyAssignment(properties, "enumerable", asExpression(attributes.enumerable)); - tryAddPropertyAssignment(properties, "configurable", asExpression(attributes.configurable)); + function createPropertyDescriptor(attributes: ts.PropertyDescriptorAttributes, singleLine?: boolean) { + const properties: ts.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 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; + let isAccessor = tryAddPropertyAssignment(properties, "get", attributes.get); + isAccessor = tryAddPropertyAssignment(properties, "set", attributes.set) || isAccessor; - ts.Debug.assert(!(isData && isAccessor), "A PropertyDescriptor may not be both an accessor descriptor and a data descriptor."); - return createObjectLiteralExpression(properties, !singleLine); - } + ts.Debug.assert(!(isData && isAccessor), "A PropertyDescriptor may not be both an accessor descriptor and a data descriptor."); + return createObjectLiteralExpression(properties, !singleLine); + } - function updateOuterExpression(outerExpression: ts.OuterExpression, expression: ts.Expression) { - switch (outerExpression.kind) { - case ts.SyntaxKind.ParenthesizedExpression: return updateParenthesizedExpression(outerExpression, expression); - case ts.SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression); - case ts.SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type); - case ts.SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression); - case ts.SyntaxKind.PartiallyEmittedExpression: return updatePartiallyEmittedExpression(outerExpression, expression); - } + function updateOuterExpression(outerExpression: ts.OuterExpression, expression: ts.Expression) { + switch (outerExpression.kind) { + case ts.SyntaxKind.ParenthesizedExpression: return updateParenthesizedExpression(outerExpression, expression); + case ts.SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression); + case ts.SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type); + case ts.SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression); + case ts.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. - */ - function isIgnorableParen(node: ts.Expression) { - return ts.isParenthesizedExpression(node) - && ts.nodeIsSynthesized(node) - && ts.nodeIsSynthesized(ts.getSourceMapRange(node)) - && ts.nodeIsSynthesized(ts.getCommentRange(node)) - && !ts.some(ts.getSyntheticLeadingComments(node)) - && !ts.some(ts.getSyntheticTrailingComments(node)); - } - function restoreOuterExpressions(outerExpression: ts.Expression | undefined, innerExpression: ts.Expression, kinds = ts.OuterExpressionKinds.All): ts.Expression { - if (outerExpression && ts.isOuterExpression(outerExpression, kinds) && !isIgnorableParen(outerExpression)) { - return updateOuterExpression(outerExpression, restoreOuterExpressions(outerExpression.expression, innerExpression)); - } - return innerExpression; + /** + * 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: ts.Expression) { + return ts.isParenthesizedExpression(node) + && ts.nodeIsSynthesized(node) + && ts.nodeIsSynthesized(ts.getSourceMapRange(node)) + && ts.nodeIsSynthesized(ts.getCommentRange(node)) + && !ts.some(ts.getSyntheticLeadingComments(node)) + && !ts.some(ts.getSyntheticTrailingComments(node)); + } + function restoreOuterExpressions(outerExpression: ts.Expression | undefined, innerExpression: ts.Expression, kinds = ts.OuterExpressionKinds.All): ts.Expression { + if (outerExpression && ts.isOuterExpression(outerExpression, kinds) && !isIgnorableParen(outerExpression)) { + return updateOuterExpression(outerExpression, restoreOuterExpressions(outerExpression.expression, innerExpression)); } + return innerExpression; + } - function restoreEnclosingLabel(node: ts.Statement, outermostLabeledStatement: ts.LabeledStatement | undefined, afterRestoreLabelCallback?: (node: ts.LabeledStatement) => void): ts.Statement { - if (!outermostLabeledStatement) { - return node; - } - const updated = updateLabeledStatement(outermostLabeledStatement, outermostLabeledStatement.label, ts.isLabeledStatement(outermostLabeledStatement.statement) - ? restoreEnclosingLabel(node, outermostLabeledStatement.statement) - : node); - if (afterRestoreLabelCallback) { - afterRestoreLabelCallback(outermostLabeledStatement); - } - return updated; + function restoreEnclosingLabel(node: ts.Statement, outermostLabeledStatement: ts.LabeledStatement | undefined, afterRestoreLabelCallback?: (node: ts.LabeledStatement) => void): ts.Statement { + if (!outermostLabeledStatement) { + return node; } + const updated = updateLabeledStatement(outermostLabeledStatement, outermostLabeledStatement.label, ts.isLabeledStatement(outermostLabeledStatement.statement) + ? restoreEnclosingLabel(node, outermostLabeledStatement.statement) + : node); + if (afterRestoreLabelCallback) { + afterRestoreLabelCallback(outermostLabeledStatement); + } + return updated; + } - function shouldBeCapturedInTempVariable(node: ts.Expression, cacheIdentifiers: boolean): boolean { - const target = ts.skipParentheses(node); - switch (target.kind) { - case ts.SyntaxKind.Identifier: - return cacheIdentifiers; - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.StringLiteral: + function shouldBeCapturedInTempVariable(node: ts.Expression, cacheIdentifiers: boolean): boolean { + const target = ts.skipParentheses(node); + switch (target.kind) { + case ts.SyntaxKind.Identifier: + return cacheIdentifiers; + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.StringLiteral: + return false; + case ts.SyntaxKind.ArrayLiteralExpression: + const elements = (target as ts.ArrayLiteralExpression).elements; + if (elements.length === 0) { return false; - case ts.SyntaxKind.ArrayLiteralExpression: - const elements = (target as ts.ArrayLiteralExpression).elements; - if (elements.length === 0) { - return false; - } - return true; - case ts.SyntaxKind.ObjectLiteralExpression: - return (target as ts.ObjectLiteralExpression).properties.length > 0; - default: - return true; - } + } + return true; + case ts.SyntaxKind.ObjectLiteralExpression: + return (target as ts.ObjectLiteralExpression).properties.length > 0; + default: + return true; } + } - function createCallBinding(expression: ts.Expression, recordTempVariable: (temp: ts.Identifier) => void, languageVersion?: ts.ScriptTarget, cacheIdentifiers = false): ts.CallBinding { - const callee = ts.skipOuterExpressions(expression, ts.OuterExpressionKinds.All); - let thisArg: ts.Expression; - let target: ts.LeftHandSideExpression; - if (ts.isSuperProperty(callee)) { - thisArg = createThis(); - target = callee; - } - else if (ts.isSuperKeyword(callee)) { - thisArg = createThis(); - target = languageVersion !== undefined && languageVersion < ts.ScriptTarget.ES2015 - ? ts.setTextRange(createIdentifier("_super"), callee) - : callee as ts.PrimaryExpression; - } - else if (ts.getEmitFlags(callee) & ts.EmitFlags.HelperName) { - thisArg = createVoidZero(); - target = parenthesizerRules().parenthesizeLeftSideOfAccess(callee); - } - else if (ts.isPropertyAccessExpression(callee)) { - if (shouldBeCapturedInTempVariable(callee.expression, cacheIdentifiers)) { - // for `a.b()` target is `(_a = a).b` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createPropertyAccessExpression(ts.setTextRange(factory.createAssignment(thisArg, callee.expression), callee.expression), callee.name); - ts.setTextRange(target, callee); - } - else { - thisArg = callee.expression; - target = callee; - } - } - else if (ts.isElementAccessExpression(callee)) { - if (shouldBeCapturedInTempVariable(callee.expression, cacheIdentifiers)) { - // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createElementAccessExpression(ts.setTextRange(factory.createAssignment(thisArg, callee.expression), callee.expression), callee.argumentExpression); - ts.setTextRange(target, callee); - } - else { - thisArg = callee.expression; - target = callee; - } + function createCallBinding(expression: ts.Expression, recordTempVariable: (temp: ts.Identifier) => void, languageVersion?: ts.ScriptTarget, cacheIdentifiers = false): ts.CallBinding { + const callee = ts.skipOuterExpressions(expression, ts.OuterExpressionKinds.All); + let thisArg: ts.Expression; + let target: ts.LeftHandSideExpression; + if (ts.isSuperProperty(callee)) { + thisArg = createThis(); + target = callee; + } + else if (ts.isSuperKeyword(callee)) { + thisArg = createThis(); + target = languageVersion !== undefined && languageVersion < ts.ScriptTarget.ES2015 + ? ts.setTextRange(createIdentifier("_super"), callee) + : callee as ts.PrimaryExpression; + } + else if (ts.getEmitFlags(callee) & ts.EmitFlags.HelperName) { + thisArg = createVoidZero(); + target = parenthesizerRules().parenthesizeLeftSideOfAccess(callee); + } + else if (ts.isPropertyAccessExpression(callee)) { + if (shouldBeCapturedInTempVariable(callee.expression, cacheIdentifiers)) { + // for `a.b()` target is `(_a = a).b` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = createPropertyAccessExpression(ts.setTextRange(factory.createAssignment(thisArg, callee.expression), callee.expression), callee.name); + ts.setTextRange(target, callee); } else { - // for `a()` target is `a` and thisArg is `void 0` - thisArg = createVoidZero(); - target = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); + thisArg = callee.expression; + target = callee; } - - return { target, thisArg }; - } - - function createAssignmentTargetWrapper(paramName: ts.Identifier, expression: ts.Expression): ts.LeftHandSideExpression { - return createPropertyAccessExpression( - // Explicit parens required because of v8 regression (https://bugs.chromium.org/p/v8/issues/detail?id=9560) - createParenthesizedExpression(createObjectLiteralExpression([ - createSetAccessorDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, "value", [createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, paramName, - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined)], createBlock([ - createExpressionStatement(expression) - ])) - ])), "value"); - } - function inlineExpressions(expressions: readonly ts.Expression[]) { - // Avoid deeply nested comma expressions as traversing them during emit can result in "Maximum call - // stack size exceeded" errors. - return expressions.length > 10 - ? createCommaListExpression(expressions) - : ts.reduceLeft(expressions, factory.createComma)!; } - - function getName(node: ts.Declaration | undefined, allowComments?: boolean, allowSourceMaps?: boolean, emitFlags: ts.EmitFlags = 0) { - const nodeName = ts.getNameOfDeclaration(node); - if (nodeName && ts.isIdentifier(nodeName) && !ts.isGeneratedIdentifier(nodeName)) { - // TODO(rbuckton): Does this need to be parented? - const name = ts.setParent(ts.setTextRange(cloneNode(nodeName), nodeName), nodeName.parent); - emitFlags |= ts.getEmitFlags(nodeName); - if (!allowSourceMaps) - emitFlags |= ts.EmitFlags.NoSourceMap; - if (!allowComments) - emitFlags |= ts.EmitFlags.NoComments; - if (emitFlags) - ts.setEmitFlags(name, emitFlags); - return name; + else if (ts.isElementAccessExpression(callee)) { + if (shouldBeCapturedInTempVariable(callee.expression, cacheIdentifiers)) { + // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = createElementAccessExpression(ts.setTextRange(factory.createAssignment(thisArg, callee.expression), callee.expression), callee.argumentExpression); + ts.setTextRange(target, callee); + } + else { + thisArg = callee.expression; + target = callee; } - return getGeneratedNameForNode(node); - } - - /** - * 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. - */ - function getInternalName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.LocalName | ts.EmitFlags.InternalName); } - - /** - * 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. - */ - function getLocalName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.LocalName); + else { + // for `a()` target is `a` and thisArg is `void 0` + thisArg = createVoidZero(); + target = parenthesizerRules().parenthesizeLeftSideOfAccess(expression); } - /** - * 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. - */ - function getExportName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean): ts.Identifier { - return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.ExportName); - } + return { target, thisArg }; + } - /** - * 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. - */ - function getDeclarationName(node: ts.Declaration | undefined, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps); - } + function createAssignmentTargetWrapper(paramName: ts.Identifier, expression: ts.Expression): ts.LeftHandSideExpression { + return createPropertyAccessExpression( + // Explicit parens required because of v8 regression (https://bugs.chromium.org/p/v8/issues/detail?id=9560) + createParenthesizedExpression(createObjectLiteralExpression([ + createSetAccessorDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, "value", [createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, paramName, + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)], createBlock([ + createExpressionStatement(expression) + ])) + ])), "value"); + } + function inlineExpressions(expressions: readonly ts.Expression[]) { + // Avoid deeply nested comma expressions as traversing them during emit can result in "Maximum call + // stack size exceeded" errors. + return expressions.length > 10 + ? createCommaListExpression(expressions) + : ts.reduceLeft(expressions, factory.createComma)!; + } - /** - * 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. - */ - function getNamespaceMemberName(ns: ts.Identifier, name: ts.Identifier, allowComments?: boolean, allowSourceMaps?: boolean): ts.PropertyAccessExpression { - const qualifiedName = createPropertyAccessExpression(ns, ts.nodeIsSynthesized(name) ? name : cloneNode(name)); - ts.setTextRange(qualifiedName, name); - let emitFlags: ts.EmitFlags = 0; + function getName(node: ts.Declaration | undefined, allowComments?: boolean, allowSourceMaps?: boolean, emitFlags: ts.EmitFlags = 0) { + const nodeName = ts.getNameOfDeclaration(node); + if (nodeName && ts.isIdentifier(nodeName) && !ts.isGeneratedIdentifier(nodeName)) { + // TODO(rbuckton): Does this need to be parented? + const name = ts.setParent(ts.setTextRange(cloneNode(nodeName), nodeName), nodeName.parent); + emitFlags |= ts.getEmitFlags(nodeName); if (!allowSourceMaps) emitFlags |= ts.EmitFlags.NoSourceMap; if (!allowComments) emitFlags |= ts.EmitFlags.NoComments; if (emitFlags) - ts.setEmitFlags(qualifiedName, emitFlags); - return qualifiedName; + ts.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. - */ - function getExternalModuleOrNamespaceExportName(ns: ts.Identifier | undefined, node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean): ts.Identifier | ts.PropertyAccessExpression { - if (ns && ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { - return getNamespaceMemberName(ns, getName(node), allowComments, allowSourceMaps); - } - return getExportName(node, allowComments, allowSourceMaps); - } + /** + * 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. + */ + function getInternalName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.LocalName | ts.EmitFlags.InternalName); + } - /** - * Copies any necessary standard and custom prologue-directives into target array. - * @param source origin statements array - * @param target result 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. - */ - function copyPrologue(source: readonly ts.Statement[], target: ts.Push, ensureUseStrict?: boolean, visitor?: (node: ts.Node) => ts.VisitResult): number { - const offset = copyStandardPrologue(source, target, 0, ensureUseStrict); - return copyCustomPrologue(source, target, offset, visitor); - } + /** + * 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. + */ + function getLocalName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.LocalName); + } - function isUseStrictPrologue(node: ts.ExpressionStatement): boolean { - return ts.isStringLiteral(node.expression) && node.expression.text === "use strict"; - } + /** + * 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. + */ + function getExportName(node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean): ts.Identifier { + return getName(node, allowComments, allowSourceMaps, ts.EmitFlags.ExportName); + } + + /** + * 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. + */ + function getDeclarationName(node: ts.Declaration | undefined, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(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. + */ + function getNamespaceMemberName(ns: ts.Identifier, name: ts.Identifier, allowComments?: boolean, allowSourceMaps?: boolean): ts.PropertyAccessExpression { + const qualifiedName = createPropertyAccessExpression(ns, ts.nodeIsSynthesized(name) ? name : cloneNode(name)); + ts.setTextRange(qualifiedName, name); + let emitFlags: ts.EmitFlags = 0; + if (!allowSourceMaps) + emitFlags |= ts.EmitFlags.NoSourceMap; + if (!allowComments) + emitFlags |= ts.EmitFlags.NoComments; + if (emitFlags) + ts.setEmitFlags(qualifiedName, emitFlags); + return qualifiedName; + } - function createUseStrictPrologue() { - return ts.startOnNewLine(createExpressionStatement(createStringLiteral("use strict"))) as ts.PrologueDirective; + /** + * 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. + */ + function getExternalModuleOrNamespaceExportName(ns: ts.Identifier | undefined, node: ts.Declaration, allowComments?: boolean, allowSourceMaps?: boolean): ts.Identifier | ts.PropertyAccessExpression { + if (ns && ts.hasSyntacticModifier(node, ts.ModifierFlags.Export)) { + return getNamespaceMemberName(ns, getName(node), allowComments, allowSourceMaps); } + return getExportName(node, allowComments, allowSourceMaps); + } - /** - * Copies only the standard (string-expression) prologue-directives into the target statement-array. - * @param source origin statements array - * @param target result statements array - * @param statementOffset The offset at which to begin the copy. - * @param ensureUseStrict boolean determining whether the function need to add prologue-directives - * @returns Count of how many directive statements were copied. - */ - function copyStandardPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset = 0, ensureUseStrict?: boolean): number { - ts.Debug.assert(target.length === 0, "Prologue directives should be at the first statement in the target statements array"); - let foundUseStrict = false; - const numStatements = source.length; - while (statementOffset < numStatements) { - const statement = source[statementOffset]; - if (ts.isPrologueDirective(statement)) { - if (isUseStrictPrologue(statement)) { - foundUseStrict = true; - } - target.push(statement); - } - else { - break; + /** + * Copies any necessary standard and custom prologue-directives into target array. + * @param source origin statements array + * @param target result 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. + */ + function copyPrologue(source: readonly ts.Statement[], target: ts.Push, ensureUseStrict?: boolean, visitor?: (node: ts.Node) => ts.VisitResult): number { + const offset = copyStandardPrologue(source, target, 0, ensureUseStrict); + return copyCustomPrologue(source, target, offset, visitor); + } + + function isUseStrictPrologue(node: ts.ExpressionStatement): boolean { + return ts.isStringLiteral(node.expression) && node.expression.text === "use strict"; + } + + function createUseStrictPrologue() { + return ts.startOnNewLine(createExpressionStatement(createStringLiteral("use strict"))) as ts.PrologueDirective; + } + + /** + * Copies only the standard (string-expression) prologue-directives into the target statement-array. + * @param source origin statements array + * @param target result statements array + * @param statementOffset The offset at which to begin the copy. + * @param ensureUseStrict boolean determining whether the function need to add prologue-directives + * @returns Count of how many directive statements were copied. + */ + function copyStandardPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset = 0, ensureUseStrict?: boolean): number { + ts.Debug.assert(target.length === 0, "Prologue directives should be at the first statement in the target statements array"); + let foundUseStrict = false; + const numStatements = source.length; + while (statementOffset < numStatements) { + const statement = source[statementOffset]; + if (ts.isPrologueDirective(statement)) { + if (isUseStrictPrologue(statement)) { + foundUseStrict = true; } - statementOffset++; + target.push(statement); } - if (ensureUseStrict && !foundUseStrict) { - target.push(createUseStrictPrologue()); + else { + break; } - return statementOffset; + statementOffset++; } + if (ensureUseStrict && !foundUseStrict) { + target.push(createUseStrictPrologue()); + } + return statementOffset; + } - /** - * Copies only the custom prologue-directives into target statement-array. - * @param source origin statements array - * @param target result statements array - * @param statementOffset The offset at which to begin the copy. - * @param visitor Optional callback used to visit any custom prologue directives. - */ - function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number, visitor?: (node: ts.Node) => ts.VisitResult, filter?: (node: ts.Node) => boolean): number; - function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number | undefined, visitor?: (node: ts.Node) => ts.VisitResult, filter?: (node: ts.Node) => boolean): number | undefined; - function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number | undefined, visitor?: (node: ts.Node) => ts.VisitResult, filter: (node: ts.Node) => boolean = ts.returnTrue): number | undefined { - const numStatements = source.length; - while (statementOffset !== undefined && statementOffset < numStatements) { - const statement = source[statementOffset]; - if (ts.getEmitFlags(statement) & ts.EmitFlags.CustomPrologue && filter(statement)) { - ts.append(target, visitor ? ts.visitNode(statement, visitor, ts.isStatement) : statement); - } - else { - break; - } - statementOffset++; + /** + * Copies only the custom prologue-directives into target statement-array. + * @param source origin statements array + * @param target result statements array + * @param statementOffset The offset at which to begin the copy. + * @param visitor Optional callback used to visit any custom prologue directives. + */ + function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number, visitor?: (node: ts.Node) => ts.VisitResult, filter?: (node: ts.Node) => boolean): number; + function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number | undefined, visitor?: (node: ts.Node) => ts.VisitResult, filter?: (node: ts.Node) => boolean): number | undefined; + function copyCustomPrologue(source: readonly ts.Statement[], target: ts.Push, statementOffset: number | undefined, visitor?: (node: ts.Node) => ts.VisitResult, filter: (node: ts.Node) => boolean = ts.returnTrue): number | undefined { + const numStatements = source.length; + while (statementOffset !== undefined && statementOffset < numStatements) { + const statement = source[statementOffset]; + if (ts.getEmitFlags(statement) & ts.EmitFlags.CustomPrologue && filter(statement)) { + ts.append(target, visitor ? ts.visitNode(statement, visitor, ts.isStatement) : statement); + } + else { + break; } - return statementOffset; + statementOffset++; } + return statementOffset; + } - /** - * Ensures "use strict" directive is added - * - * @param statements An array of statements - */ - function ensureUseStrict(statements: ts.NodeArray): ts.NodeArray { - const foundUseStrict = ts.findUseStrictPrologue(statements); - - if (!foundUseStrict) { - return ts.setTextRange(createNodeArray([createUseStrictPrologue(), ...statements]), statements); - } + /** + * Ensures "use strict" directive is added + * + * @param statements An array of statements + */ + function ensureUseStrict(statements: ts.NodeArray): ts.NodeArray { + const foundUseStrict = ts.findUseStrictPrologue(statements); - return statements; + if (!foundUseStrict) { + return ts.setTextRange(createNodeArray([createUseStrictPrologue(), ...statements]), statements); } - /** - * Lifts a NodeArray containing only Statement nodes to a block. - * - * @param nodes The NodeArray. - */ - function liftToBlock(nodes: readonly ts.Node[]): ts.Statement { - ts.Debug.assert(ts.every(nodes, ts.isStatementOrBlock), "Cannot lift nodes to a Block."); - return ts.singleOrUndefined(nodes) as ts.Statement || createBlock(nodes as readonly ts.Statement[]); + return statements; + } + + /** + * Lifts a NodeArray containing only Statement nodes to a block. + * + * @param nodes The NodeArray. + */ + function liftToBlock(nodes: readonly ts.Node[]): ts.Statement { + ts.Debug.assert(ts.every(nodes, ts.isStatementOrBlock), "Cannot lift nodes to a Block."); + return ts.singleOrUndefined(nodes) as ts.Statement || createBlock(nodes as readonly ts.Statement[]); + } + + function findSpanEnd(array: readonly T[], test: (value: T) => boolean, start: number) { + let i = start; + while (i < array.length && test(array[i])) { + i++; } + return i; + } - function findSpanEnd(array: readonly T[], test: (value: T) => boolean, start: number) { - let i = start; - while (i < array.length && test(array[i])) { - i++; - } - return i; + function mergeLexicalEnvironment(statements: ts.NodeArray, declarations: readonly ts.Statement[] | undefined): ts.NodeArray; + function mergeLexicalEnvironment(statements: ts.Statement[], declarations: readonly ts.Statement[] | undefined): ts.Statement[]; + function mergeLexicalEnvironment(statements: ts.Statement[] | ts.NodeArray, declarations: readonly ts.Statement[] | undefined) { + if (!ts.some(declarations)) { + return statements; } - function mergeLexicalEnvironment(statements: ts.NodeArray, declarations: readonly ts.Statement[] | undefined): ts.NodeArray; - function mergeLexicalEnvironment(statements: ts.Statement[], declarations: readonly ts.Statement[] | undefined): ts.Statement[]; - function mergeLexicalEnvironment(statements: ts.Statement[] | ts.NodeArray, declarations: readonly ts.Statement[] | undefined) { - if (!ts.some(declarations)) { - return statements; - } + // When we merge new lexical statements into an existing statement list, we merge them in the following manner: + // + // Given: + // + // | Left | Right | + // |------------------------------------|-------------------------------------| + // | [standard prologues (left)] | [standard prologues (right)] | + // | [hoisted functions (left)] | [hoisted functions (right)] | + // | [hoisted variables (left)] | [hoisted variables (right)] | + // | [lexical init statements (left)] | [lexical init statements (right)] | + // | [other statements (left)] | | + // + // The resulting statement list will be: + // + // | Result | + // |-------------------------------------| + // | [standard prologues (right)] | + // | [standard prologues (left)] | + // | [hoisted functions (right)] | + // | [hoisted functions (left)] | + // | [hoisted variables (right)] | + // | [hoisted variables (left)] | + // | [lexical init statements (right)] | + // | [lexical init statements (left)] | + // | [other statements (left)] | + // + // NOTE: It is expected that new lexical init statements must be evaluated before existing lexical init statements, + // as the prior transformation may depend on the evaluation of the lexical init statements to be in the correct state. - // When we merge new lexical statements into an existing statement list, we merge them in the following manner: - // - // Given: - // - // | Left | Right | - // |------------------------------------|-------------------------------------| - // | [standard prologues (left)] | [standard prologues (right)] | - // | [hoisted functions (left)] | [hoisted functions (right)] | - // | [hoisted variables (left)] | [hoisted variables (right)] | - // | [lexical init statements (left)] | [lexical init statements (right)] | - // | [other statements (left)] | | - // - // The resulting statement list will be: - // - // | Result | - // |-------------------------------------| - // | [standard prologues (right)] | - // | [standard prologues (left)] | - // | [hoisted functions (right)] | - // | [hoisted functions (left)] | - // | [hoisted variables (right)] | - // | [hoisted variables (left)] | - // | [lexical init statements (right)] | - // | [lexical init statements (left)] | - // | [other statements (left)] | - // - // NOTE: It is expected that new lexical init statements must be evaluated before existing lexical init statements, - // as the prior transformation may depend on the evaluation of the lexical init statements to be in the correct state. - - // find standard prologues on left in the following order: standard directives, hoisted functions, hoisted variables, other custom - const leftStandardPrologueEnd = findSpanEnd(statements, ts.isPrologueDirective, 0); - const leftHoistedFunctionsEnd = findSpanEnd(statements, ts.isHoistedFunction, leftStandardPrologueEnd); - const leftHoistedVariablesEnd = findSpanEnd(statements, ts.isHoistedVariableStatement, leftHoistedFunctionsEnd); - - // find standard prologues on right in the following order: standard directives, hoisted functions, hoisted variables, other custom - const rightStandardPrologueEnd = findSpanEnd(declarations, ts.isPrologueDirective, 0); - const rightHoistedFunctionsEnd = findSpanEnd(declarations, ts.isHoistedFunction, rightStandardPrologueEnd); - const rightHoistedVariablesEnd = findSpanEnd(declarations, ts.isHoistedVariableStatement, rightHoistedFunctionsEnd); - const rightCustomPrologueEnd = findSpanEnd(declarations, ts.isCustomPrologue, rightHoistedVariablesEnd); - ts.Debug.assert(rightCustomPrologueEnd === declarations.length, "Expected declarations to be valid standard or custom prologues"); - - // splice prologues from the right into the left. We do this in reverse order - // so that we don't need to recompute the index on the left when we insert items. - const left = ts.isNodeArray(statements) ? statements.slice() : statements; - - // splice other custom prologues from right into left - if (rightCustomPrologueEnd > rightHoistedVariablesEnd) { - left.splice(leftHoistedVariablesEnd, 0, ...declarations.slice(rightHoistedVariablesEnd, rightCustomPrologueEnd)); - } + // find standard prologues on left in the following order: standard directives, hoisted functions, hoisted variables, other custom + const leftStandardPrologueEnd = findSpanEnd(statements, ts.isPrologueDirective, 0); + const leftHoistedFunctionsEnd = findSpanEnd(statements, ts.isHoistedFunction, leftStandardPrologueEnd); + const leftHoistedVariablesEnd = findSpanEnd(statements, ts.isHoistedVariableStatement, leftHoistedFunctionsEnd); - // splice hoisted variables from right into left - if (rightHoistedVariablesEnd > rightHoistedFunctionsEnd) { - left.splice(leftHoistedFunctionsEnd, 0, ...declarations.slice(rightHoistedFunctionsEnd, rightHoistedVariablesEnd)); - } + // find standard prologues on right in the following order: standard directives, hoisted functions, hoisted variables, other custom + const rightStandardPrologueEnd = findSpanEnd(declarations, ts.isPrologueDirective, 0); + const rightHoistedFunctionsEnd = findSpanEnd(declarations, ts.isHoistedFunction, rightStandardPrologueEnd); + const rightHoistedVariablesEnd = findSpanEnd(declarations, ts.isHoistedVariableStatement, rightHoistedFunctionsEnd); + const rightCustomPrologueEnd = findSpanEnd(declarations, ts.isCustomPrologue, rightHoistedVariablesEnd); + ts.Debug.assert(rightCustomPrologueEnd === declarations.length, "Expected declarations to be valid standard or custom prologues"); - // splice hoisted functions from right into left - if (rightHoistedFunctionsEnd > rightStandardPrologueEnd) { - left.splice(leftStandardPrologueEnd, 0, ...declarations.slice(rightStandardPrologueEnd, rightHoistedFunctionsEnd)); - } + // splice prologues from the right into the left. We do this in reverse order + // so that we don't need to recompute the index on the left when we insert items. + const left = ts.isNodeArray(statements) ? statements.slice() : statements; - // splice standard prologues from right into left (that are not already in left) - if (rightStandardPrologueEnd > 0) { - if (leftStandardPrologueEnd === 0) { - left.splice(0, 0, ...declarations.slice(0, rightStandardPrologueEnd)); - } - else { - const leftPrologues = new ts.Map(); - for (let i = 0; i < leftStandardPrologueEnd; i++) { - const leftPrologue = statements[i] as ts.PrologueDirective; - leftPrologues.set(leftPrologue.expression.text, true); - } - for (let i = rightStandardPrologueEnd - 1; i >= 0; i--) { - const rightPrologue = declarations[i] as ts.PrologueDirective; - if (!leftPrologues.has(rightPrologue.expression.text)) { - left.unshift(rightPrologue); - } - } - } - } + // splice other custom prologues from right into left + if (rightCustomPrologueEnd > rightHoistedVariablesEnd) { + left.splice(leftHoistedVariablesEnd, 0, ...declarations.slice(rightHoistedVariablesEnd, rightCustomPrologueEnd)); + } - if (ts.isNodeArray(statements)) { - return ts.setTextRange(createNodeArray(left, statements.hasTrailingComma), statements); - } + // splice hoisted variables from right into left + if (rightHoistedVariablesEnd > rightHoistedFunctionsEnd) { + left.splice(leftHoistedFunctionsEnd, 0, ...declarations.slice(rightHoistedFunctionsEnd, rightHoistedVariablesEnd)); + } - return statements; + // splice hoisted functions from right into left + if (rightHoistedFunctionsEnd > rightStandardPrologueEnd) { + left.splice(leftStandardPrologueEnd, 0, ...declarations.slice(rightStandardPrologueEnd, rightHoistedFunctionsEnd)); } - function updateModifiers(node: T, modifiers: readonly ts.Modifier[] | ts.ModifierFlags): T; - function updateModifiers(node: ts.HasModifiers, modifiers: readonly ts.Modifier[] | ts.ModifierFlags) { - let modifierArray; - if (typeof modifiers === "number") { - modifierArray = createModifiersFromModifierFlags(modifiers); + // splice standard prologues from right into left (that are not already in left) + if (rightStandardPrologueEnd > 0) { + if (leftStandardPrologueEnd === 0) { + left.splice(0, 0, ...declarations.slice(0, rightStandardPrologueEnd)); } else { - modifierArray = modifiers; + const leftPrologues = new ts.Map(); + for (let i = 0; i < leftStandardPrologueEnd; i++) { + const leftPrologue = statements[i] as ts.PrologueDirective; + leftPrologues.set(leftPrologue.expression.text, true); + } + for (let i = rightStandardPrologueEnd - 1; i >= 0; i--) { + const rightPrologue = declarations[i] as ts.PrologueDirective; + if (!leftPrologues.has(rightPrologue.expression.text)) { + left.unshift(rightPrologue); + } + } } - return ts.isParameter(node) ? updateParameterDeclaration(node, node.decorators, modifierArray, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) : - ts.isPropertySignature(node) ? updatePropertySignature(node, modifierArray, node.name, node.questionToken, node.type) : - ts.isPropertyDeclaration(node) ? updatePropertyDeclaration(node, node.decorators, modifierArray, node.name, node.questionToken ?? node.exclamationToken, node.type, node.initializer) : - ts.isMethodSignature(node) ? updateMethodSignature(node, modifierArray, node.name, node.questionToken, node.typeParameters, node.parameters, node.type) : - ts.isMethodDeclaration(node) ? updateMethodDeclaration(node, node.decorators, modifierArray, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body) : - ts.isConstructorDeclaration(node) ? updateConstructorDeclaration(node, node.decorators, modifierArray, node.parameters, node.body) : - ts.isGetAccessorDeclaration(node) ? updateGetAccessorDeclaration(node, node.decorators, modifierArray, node.name, node.parameters, node.type, node.body) : - ts.isSetAccessorDeclaration(node) ? updateSetAccessorDeclaration(node, node.decorators, modifierArray, node.name, node.parameters, node.body) : - ts.isIndexSignatureDeclaration(node) ? updateIndexSignature(node, node.decorators, modifierArray, node.parameters, node.type) : - ts.isFunctionExpression(node) ? updateFunctionExpression(node, modifierArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body) : - ts.isArrowFunction(node) ? updateArrowFunction(node, modifierArray, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, node.body) : - ts.isClassExpression(node) ? updateClassExpression(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : - ts.isVariableStatement(node) ? updateVariableStatement(node, modifierArray, node.declarationList) : - ts.isFunctionDeclaration(node) ? updateFunctionDeclaration(node, node.decorators, modifierArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body) : - ts.isClassDeclaration(node) ? updateClassDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : - ts.isInterfaceDeclaration(node) ? updateInterfaceDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : - ts.isTypeAliasDeclaration(node) ? updateTypeAliasDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.type) : - ts.isEnumDeclaration(node) ? updateEnumDeclaration(node, node.decorators, modifierArray, node.name, node.members) : - ts.isModuleDeclaration(node) ? updateModuleDeclaration(node, node.decorators, modifierArray, node.name, node.body) : - ts.isImportEqualsDeclaration(node) ? updateImportEqualsDeclaration(node, node.decorators, modifierArray, node.isTypeOnly, node.name, node.moduleReference) : - ts.isImportDeclaration(node) ? updateImportDeclaration(node, node.decorators, modifierArray, node.importClause, node.moduleSpecifier, node.assertClause) : - ts.isExportAssignment(node) ? updateExportAssignment(node, node.decorators, modifierArray, node.expression) : - ts.isExportDeclaration(node) ? updateExportDeclaration(node, node.decorators, modifierArray, node.isTypeOnly, node.exportClause, node.moduleSpecifier, node.assertClause) : - ts.Debug.assertNever(node); - } - function asNodeArray(array: readonly T[]): ts.NodeArray; - function asNodeArray(array: readonly T[] | undefined): ts.NodeArray | undefined; - function asNodeArray(array: readonly T[] | undefined): ts.NodeArray | undefined { - return array ? createNodeArray(array) : undefined; } - function asName(name: string | T): T | ts.Identifier { - return typeof name === "string" ? createIdentifier(name) : - name; + if (ts.isNodeArray(statements)) { + return ts.setTextRange(createNodeArray(left, statements.hasTrailingComma), statements); } - function asExpression(value: string | number | boolean | T): T | ts.StringLiteral | ts.NumericLiteral | ts.BooleanLiteral { - return typeof value === "string" ? createStringLiteral(value) : - typeof value === "number" ? createNumericLiteral(value) : - typeof value === "boolean" ? value ? createTrue() : createFalse() : - value; - } + return statements; + } - function asToken(value: TKind | ts.Token): ts.Token { - return typeof value === "number" ? createToken(value) : value; + function updateModifiers(node: T, modifiers: readonly ts.Modifier[] | ts.ModifierFlags): T; + function updateModifiers(node: ts.HasModifiers, modifiers: readonly ts.Modifier[] | ts.ModifierFlags) { + let modifierArray; + if (typeof modifiers === "number") { + modifierArray = createModifiersFromModifierFlags(modifiers); } + else { + modifierArray = modifiers; + } + return ts.isParameter(node) ? updateParameterDeclaration(node, node.decorators, modifierArray, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) : + ts.isPropertySignature(node) ? updatePropertySignature(node, modifierArray, node.name, node.questionToken, node.type) : + ts.isPropertyDeclaration(node) ? updatePropertyDeclaration(node, node.decorators, modifierArray, node.name, node.questionToken ?? node.exclamationToken, node.type, node.initializer) : + ts.isMethodSignature(node) ? updateMethodSignature(node, modifierArray, node.name, node.questionToken, node.typeParameters, node.parameters, node.type) : + ts.isMethodDeclaration(node) ? updateMethodDeclaration(node, node.decorators, modifierArray, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body) : + ts.isConstructorDeclaration(node) ? updateConstructorDeclaration(node, node.decorators, modifierArray, node.parameters, node.body) : + ts.isGetAccessorDeclaration(node) ? updateGetAccessorDeclaration(node, node.decorators, modifierArray, node.name, node.parameters, node.type, node.body) : + ts.isSetAccessorDeclaration(node) ? updateSetAccessorDeclaration(node, node.decorators, modifierArray, node.name, node.parameters, node.body) : + ts.isIndexSignatureDeclaration(node) ? updateIndexSignature(node, node.decorators, modifierArray, node.parameters, node.type) : + ts.isFunctionExpression(node) ? updateFunctionExpression(node, modifierArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body) : + ts.isArrowFunction(node) ? updateArrowFunction(node, modifierArray, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, node.body) : + ts.isClassExpression(node) ? updateClassExpression(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : + ts.isVariableStatement(node) ? updateVariableStatement(node, modifierArray, node.declarationList) : + ts.isFunctionDeclaration(node) ? updateFunctionDeclaration(node, node.decorators, modifierArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body) : + ts.isClassDeclaration(node) ? updateClassDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : + ts.isInterfaceDeclaration(node) ? updateInterfaceDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : + ts.isTypeAliasDeclaration(node) ? updateTypeAliasDeclaration(node, node.decorators, modifierArray, node.name, node.typeParameters, node.type) : + ts.isEnumDeclaration(node) ? updateEnumDeclaration(node, node.decorators, modifierArray, node.name, node.members) : + ts.isModuleDeclaration(node) ? updateModuleDeclaration(node, node.decorators, modifierArray, node.name, node.body) : + ts.isImportEqualsDeclaration(node) ? updateImportEqualsDeclaration(node, node.decorators, modifierArray, node.isTypeOnly, node.name, node.moduleReference) : + ts.isImportDeclaration(node) ? updateImportDeclaration(node, node.decorators, modifierArray, node.importClause, node.moduleSpecifier, node.assertClause) : + ts.isExportAssignment(node) ? updateExportAssignment(node, node.decorators, modifierArray, node.expression) : + ts.isExportDeclaration(node) ? updateExportDeclaration(node, node.decorators, modifierArray, node.isTypeOnly, node.exportClause, node.moduleSpecifier, node.assertClause) : + ts.Debug.assertNever(node); + } + function asNodeArray(array: readonly T[]): ts.NodeArray; + function asNodeArray(array: readonly T[] | undefined): ts.NodeArray | undefined; + function asNodeArray(array: readonly T[] | undefined): ts.NodeArray | undefined { + return array ? createNodeArray(array) : undefined; + } - function asEmbeddedStatement(statement: T): T | ts.EmptyStatement; - function asEmbeddedStatement(statement: T | undefined): T | ts.EmptyStatement | undefined; - function asEmbeddedStatement(statement: T | undefined): T | ts.EmptyStatement | undefined { - return statement && ts.isNotEmittedStatement(statement) ? ts.setTextRange(setOriginalNode(createEmptyStatement(), statement), statement) : statement; - } + function asName(name: string | T): T | ts.Identifier { + return typeof name === "string" ? createIdentifier(name) : + name; } - function updateWithoutOriginal(updated: T, original: T): T { - if (updated !== original) { - ts.setTextRange(updated, original); - } - return updated; + function asExpression(value: string | number | boolean | T): T | ts.StringLiteral | ts.NumericLiteral | ts.BooleanLiteral { + return typeof value === "string" ? createStringLiteral(value) : + typeof value === "number" ? createNumericLiteral(value) : + typeof value === "boolean" ? value ? createTrue() : createFalse() : + value; } - function updateWithOriginal(updated: T, original: T): T { - if (updated !== original) { - setOriginalNode(updated, original); - ts.setTextRange(updated, original); - } - return updated; + function asToken(value: TKind | ts.Token): ts.Token { + return typeof value === "number" ? createToken(value) : value; } - function getDefaultTagNameForKind(kind: ts.JSDocTag["kind"]): string { - switch (kind) { - case ts.SyntaxKind.JSDocTypeTag: return "type"; - case ts.SyntaxKind.JSDocReturnTag: return "returns"; - case ts.SyntaxKind.JSDocThisTag: return "this"; - case ts.SyntaxKind.JSDocEnumTag: return "enum"; - case ts.SyntaxKind.JSDocAuthorTag: return "author"; - case ts.SyntaxKind.JSDocClassTag: return "class"; - case ts.SyntaxKind.JSDocPublicTag: return "public"; - case ts.SyntaxKind.JSDocPrivateTag: return "private"; - case ts.SyntaxKind.JSDocProtectedTag: return "protected"; - case ts.SyntaxKind.JSDocReadonlyTag: return "readonly"; - case ts.SyntaxKind.JSDocOverrideTag: return "override"; - case ts.SyntaxKind.JSDocTemplateTag: return "template"; - case ts.SyntaxKind.JSDocTypedefTag: return "typedef"; - case ts.SyntaxKind.JSDocParameterTag: return "param"; - case ts.SyntaxKind.JSDocPropertyTag: return "prop"; - case ts.SyntaxKind.JSDocCallbackTag: return "callback"; - case ts.SyntaxKind.JSDocAugmentsTag: return "augments"; - case ts.SyntaxKind.JSDocImplementsTag: return "implements"; - default: - return ts.Debug.fail(`Unsupported kind: ${ts.Debug.formatSyntaxKind(kind)}`); - } + function asEmbeddedStatement(statement: T): T | ts.EmptyStatement; + function asEmbeddedStatement(statement: T | undefined): T | ts.EmptyStatement | undefined; + function asEmbeddedStatement(statement: T | undefined): T | ts.EmptyStatement | undefined { + return statement && ts.isNotEmittedStatement(statement) ? ts.setTextRange(setOriginalNode(createEmptyStatement(), statement), statement) : statement; } +} - let rawTextScanner: ts.Scanner | undefined; - const invalidValueSentinel: object = { }; +function updateWithoutOriginal(updated: T, original: T): T { + if (updated !== original) { + ts.setTextRange(updated, original); + } + return updated; +} - function getCookedText(kind: ts.TemplateLiteralToken["kind"], rawText: string) { - if (!rawTextScanner) { - rawTextScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.Standard); - } - switch (kind) { - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - rawTextScanner.setText("`" + rawText + "`"); - break; - case ts.SyntaxKind.TemplateHead: - // tslint:disable-next-line no-invalid-template-strings - rawTextScanner.setText("`" + rawText + "${"); - break; - case ts.SyntaxKind.TemplateMiddle: - // tslint:disable-next-line no-invalid-template-strings - rawTextScanner.setText("}" + rawText + "${"); - break; - case ts.SyntaxKind.TemplateTail: - rawTextScanner.setText("}" + rawText + "`"); - break; - } +function updateWithOriginal(updated: T, original: T): T { + if (updated !== original) { + setOriginalNode(updated, original); + ts.setTextRange(updated, original); + } + return updated; +} - let token = rawTextScanner.scan(); - if (token === ts.SyntaxKind.CloseBraceToken) { - token = rawTextScanner.reScanTemplateToken(/*isTaggedTemplate*/ false); - } +function getDefaultTagNameForKind(kind: ts.JSDocTag["kind"]): string { + switch (kind) { + case ts.SyntaxKind.JSDocTypeTag: return "type"; + case ts.SyntaxKind.JSDocReturnTag: return "returns"; + case ts.SyntaxKind.JSDocThisTag: return "this"; + case ts.SyntaxKind.JSDocEnumTag: return "enum"; + case ts.SyntaxKind.JSDocAuthorTag: return "author"; + case ts.SyntaxKind.JSDocClassTag: return "class"; + case ts.SyntaxKind.JSDocPublicTag: return "public"; + case ts.SyntaxKind.JSDocPrivateTag: return "private"; + case ts.SyntaxKind.JSDocProtectedTag: return "protected"; + case ts.SyntaxKind.JSDocReadonlyTag: return "readonly"; + case ts.SyntaxKind.JSDocOverrideTag: return "override"; + case ts.SyntaxKind.JSDocTemplateTag: return "template"; + case ts.SyntaxKind.JSDocTypedefTag: return "typedef"; + case ts.SyntaxKind.JSDocParameterTag: return "param"; + case ts.SyntaxKind.JSDocPropertyTag: return "prop"; + case ts.SyntaxKind.JSDocCallbackTag: return "callback"; + case ts.SyntaxKind.JSDocAugmentsTag: return "augments"; + case ts.SyntaxKind.JSDocImplementsTag: return "implements"; + default: + return ts.Debug.fail(`Unsupported kind: ${ts.Debug.formatSyntaxKind(kind)}`); + } +} - if (rawTextScanner.isUnterminated()) { - rawTextScanner.setText(undefined); - return invalidValueSentinel; - } +let rawTextScanner: ts.Scanner | undefined; +const invalidValueSentinel: object = { }; - let tokenValue: string | undefined; - switch (token) { - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TemplateHead: - case ts.SyntaxKind.TemplateMiddle: - case ts.SyntaxKind.TemplateTail: - tokenValue = rawTextScanner.getTokenValue(); - break; - } +function getCookedText(kind: ts.TemplateLiteralToken["kind"], rawText: string) { + if (!rawTextScanner) { + rawTextScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.Standard); + } + switch (kind) { + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + rawTextScanner.setText("`" + rawText + "`"); + break; + case ts.SyntaxKind.TemplateHead: + // tslint:disable-next-line no-invalid-template-strings + rawTextScanner.setText("`" + rawText + "${"); + break; + case ts.SyntaxKind.TemplateMiddle: + // tslint:disable-next-line no-invalid-template-strings + rawTextScanner.setText("}" + rawText + "${"); + break; + case ts.SyntaxKind.TemplateTail: + rawTextScanner.setText("}" + rawText + "`"); + break; + } - if (tokenValue === undefined || rawTextScanner.scan() !== ts.SyntaxKind.EndOfFileToken) { - rawTextScanner.setText(undefined); - return invalidValueSentinel; - } + let token = rawTextScanner.scan(); + if (token === ts.SyntaxKind.CloseBraceToken) { + token = rawTextScanner.reScanTemplateToken(/*isTaggedTemplate*/ false); + } + if (rawTextScanner.isUnterminated()) { rawTextScanner.setText(undefined); - return tokenValue; + return invalidValueSentinel; } - function propagateIdentifierNameFlags(node: ts.Identifier) { - // An IdentifierName is allowed to be `await` - return propagateChildFlags(node) & ~ts.TransformFlags.ContainsPossibleTopLevelAwait; + let tokenValue: string | undefined; + switch (token) { + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateMiddle: + case ts.SyntaxKind.TemplateTail: + tokenValue = rawTextScanner.getTokenValue(); + break; } - function propagatePropertyNameFlagsOfChild(node: ts.PropertyName, transformFlags: ts.TransformFlags) { - return transformFlags | (node.transformFlags & ts.TransformFlags.PropertyNamePropagatingFlags); + if (tokenValue === undefined || rawTextScanner.scan() !== ts.SyntaxKind.EndOfFileToken) { + rawTextScanner.setText(undefined); + return invalidValueSentinel; } - function propagateChildFlags(child: ts.Node | undefined): ts.TransformFlags { - if (!child) - return ts.TransformFlags.None; - const childFlags = child.transformFlags & ~getTransformFlagsSubtreeExclusions(child.kind); - return ts.isNamedDeclaration(child) && ts.isPropertyName(child.name) ? propagatePropertyNameFlagsOfChild(child.name, childFlags) : childFlags; - } + rawTextScanner.setText(undefined); + return tokenValue; +} - function propagateChildrenFlags(children: ts.NodeArray | undefined): ts.TransformFlags { - return children ? children.transformFlags : ts.TransformFlags.None; - } +function propagateIdentifierNameFlags(node: ts.Identifier) { + // An IdentifierName is allowed to be `await` + return propagateChildFlags(node) & ~ts.TransformFlags.ContainsPossibleTopLevelAwait; +} - function aggregateChildrenFlags(children: ts.MutableNodeArray) { - let subtreeFlags = ts.TransformFlags.None; - for (const child of children) { - subtreeFlags |= propagateChildFlags(child); - } - children.transformFlags = subtreeFlags; - } +function propagatePropertyNameFlagsOfChild(node: ts.PropertyName, transformFlags: ts.TransformFlags) { + return transformFlags | (node.transformFlags & ts.TransformFlags.PropertyNamePropagatingFlags); +} - /** - * Gets the transform flags to exclude when unioning the transform flags of a subtree. - */ - /* @internal */ - export function getTransformFlagsSubtreeExclusions(kind: ts.SyntaxKind) { - if (kind >= ts.SyntaxKind.FirstTypeNode && kind <= ts.SyntaxKind.LastTypeNode) { - return ts.TransformFlags.TypeExcludes; - } +function propagateChildFlags(child: ts.Node | undefined): ts.TransformFlags { + if (!child) + return ts.TransformFlags.None; + const childFlags = child.transformFlags & ~getTransformFlagsSubtreeExclusions(child.kind); + return ts.isNamedDeclaration(child) && ts.isPropertyName(child.name) ? propagatePropertyNameFlagsOfChild(child.name, childFlags) : childFlags; +} - switch (kind) { - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.ArrayLiteralExpression: - return ts.TransformFlags.ArrayLiteralOrCallOrNewExcludes; - case ts.SyntaxKind.ModuleDeclaration: - return ts.TransformFlags.ModuleExcludes; - case ts.SyntaxKind.Parameter: - return ts.TransformFlags.ParameterExcludes; - case ts.SyntaxKind.ArrowFunction: - return ts.TransformFlags.ArrowFunctionExcludes; - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - return ts.TransformFlags.FunctionExcludes; - case ts.SyntaxKind.VariableDeclarationList: - return ts.TransformFlags.VariableDeclarationListExcludes; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return ts.TransformFlags.ClassExcludes; - case ts.SyntaxKind.Constructor: - return ts.TransformFlags.ConstructorExcludes; - case ts.SyntaxKind.PropertyDeclaration: - return ts.TransformFlags.PropertyExcludes; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return ts.TransformFlags.MethodOrAccessorExcludes; - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.TypeParameter: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - return ts.TransformFlags.TypeExcludes; - case ts.SyntaxKind.ObjectLiteralExpression: - return ts.TransformFlags.ObjectLiteralExcludes; - case ts.SyntaxKind.CatchClause: - return ts.TransformFlags.CatchClauseExcludes; - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ArrayBindingPattern: - return ts.TransformFlags.BindingPatternExcludes; - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.AsExpression: - case ts.SyntaxKind.PartiallyEmittedExpression: - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.SuperKeyword: - return ts.TransformFlags.OuterExpressionExcludes; - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return ts.TransformFlags.PropertyAccessExcludes; - default: - return ts.TransformFlags.NodeExcludes; - } +function propagateChildrenFlags(children: ts.NodeArray | undefined): ts.TransformFlags { + return children ? children.transformFlags : ts.TransformFlags.None; +} + +function aggregateChildrenFlags(children: ts.MutableNodeArray) { + let subtreeFlags = ts.TransformFlags.None; + for (const child of children) { + subtreeFlags |= propagateChildFlags(child); } + children.transformFlags = subtreeFlags; +} - const baseFactory = ts.createBaseNodeFactory(); - function makeSynthetic(node: ts.Node) { - (node as ts.Mutable).flags |= ts.NodeFlags.Synthesized; - return node; +/** + * Gets the transform flags to exclude when unioning the transform flags of a subtree. + */ +/* @internal */ +export function getTransformFlagsSubtreeExclusions(kind: ts.SyntaxKind) { + if (kind >= ts.SyntaxKind.FirstTypeNode && kind <= ts.SyntaxKind.LastTypeNode) { + return ts.TransformFlags.TypeExcludes; } - const syntheticFactory: ts.BaseNodeFactory = { - createBaseSourceFileNode: kind => makeSynthetic(baseFactory.createBaseSourceFileNode(kind)), - createBaseIdentifierNode: kind => makeSynthetic(baseFactory.createBaseIdentifierNode(kind)), - createBasePrivateIdentifierNode: kind => makeSynthetic(baseFactory.createBasePrivateIdentifierNode(kind)), - createBaseTokenNode: kind => makeSynthetic(baseFactory.createBaseTokenNode(kind)), - createBaseNode: kind => makeSynthetic(baseFactory.createBaseNode(kind)), - }; + switch (kind) { + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.ArrayLiteralExpression: + return ts.TransformFlags.ArrayLiteralOrCallOrNewExcludes; + case ts.SyntaxKind.ModuleDeclaration: + return ts.TransformFlags.ModuleExcludes; + case ts.SyntaxKind.Parameter: + return ts.TransformFlags.ParameterExcludes; + case ts.SyntaxKind.ArrowFunction: + return ts.TransformFlags.ArrowFunctionExcludes; + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + return ts.TransformFlags.FunctionExcludes; + case ts.SyntaxKind.VariableDeclarationList: + return ts.TransformFlags.VariableDeclarationListExcludes; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return ts.TransformFlags.ClassExcludes; + case ts.SyntaxKind.Constructor: + return ts.TransformFlags.ConstructorExcludes; + case ts.SyntaxKind.PropertyDeclaration: + return ts.TransformFlags.PropertyExcludes; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return ts.TransformFlags.MethodOrAccessorExcludes; + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + return ts.TransformFlags.TypeExcludes; + case ts.SyntaxKind.ObjectLiteralExpression: + return ts.TransformFlags.ObjectLiteralExcludes; + case ts.SyntaxKind.CatchClause: + return ts.TransformFlags.CatchClauseExcludes; + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + return ts.TransformFlags.BindingPatternExcludes; + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.AsExpression: + case ts.SyntaxKind.PartiallyEmittedExpression: + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.SuperKeyword: + return ts.TransformFlags.OuterExpressionExcludes; + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return ts.TransformFlags.PropertyAccessExcludes; + default: + return ts.TransformFlags.NodeExcludes; + } +} - export const factory = createNodeFactory(NodeFactoryFlags.NoIndentationOnFreshPropertyAccess, syntheticFactory); - - export function createUnparsedSourceFile(text: string): ts.UnparsedSource; - export function createUnparsedSourceFile(inputFile: ts.InputFiles, type: "js" | "dts", stripInternal?: boolean): ts.UnparsedSource; - export function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): ts.UnparsedSource; - export function createUnparsedSourceFile(textOrInputFiles: string | ts.InputFiles, mapPathOrType?: string, mapTextOrStripInternal?: string | boolean): ts.UnparsedSource { - let stripInternal: boolean | undefined; - let bundleFileInfo: ts.BundleFileInfo | undefined; - let fileName: string; - let text: string | undefined; - let length: number | (() => number); - let sourceMapPath: string | undefined; - let sourceMapText: string | undefined; - let getText: (() => string) | undefined; - let getSourceMapText: (() => string | undefined) | undefined; - let oldFileOfCurrentEmit: boolean | undefined; - - if (!ts.isString(textOrInputFiles)) { - ts.Debug.assert(mapPathOrType === "js" || mapPathOrType === "dts"); - fileName = (mapPathOrType === "js" ? textOrInputFiles.javascriptPath : textOrInputFiles.declarationPath) || ""; - sourceMapPath = mapPathOrType === "js" ? textOrInputFiles.javascriptMapPath : textOrInputFiles.declarationMapPath; - getText = () => mapPathOrType === "js" ? textOrInputFiles.javascriptText : textOrInputFiles.declarationText; - getSourceMapText = () => mapPathOrType === "js" ? textOrInputFiles.javascriptMapText : textOrInputFiles.declarationMapText; - length = () => getText!().length; - if (textOrInputFiles.buildInfo && textOrInputFiles.buildInfo.bundle) { - ts.Debug.assert(mapTextOrStripInternal === undefined || typeof mapTextOrStripInternal === "boolean"); - stripInternal = mapTextOrStripInternal; - bundleFileInfo = mapPathOrType === "js" ? textOrInputFiles.buildInfo.bundle.js : textOrInputFiles.buildInfo.bundle.dts; - oldFileOfCurrentEmit = textOrInputFiles.oldFileOfCurrentEmit; - } - } - else { - fileName = ""; - text = textOrInputFiles; - length = textOrInputFiles.length; - sourceMapPath = mapPathOrType; - sourceMapText = mapTextOrStripInternal as string; - } - const node = oldFileOfCurrentEmit ? - parseOldFileOfCurrentEmit(ts.Debug.checkDefined(bundleFileInfo)) : - parseUnparsedSourceFile(bundleFileInfo, stripInternal, length); - node.fileName = fileName; - node.sourceMapPath = sourceMapPath; - node.oldFileOfCurrentEmit = oldFileOfCurrentEmit; - if (getText && getSourceMapText) { - Object.defineProperty(node, "text", { get: getText }); - Object.defineProperty(node, "sourceMapText", { get: getSourceMapText }); - } - else { - ts.Debug.assert(!oldFileOfCurrentEmit); - node.text = text ?? ""; - node.sourceMapText = sourceMapText; - } +const baseFactory = ts.createBaseNodeFactory(); +function makeSynthetic(node: ts.Node) { + (node as ts.Mutable).flags |= ts.NodeFlags.Synthesized; + return node; +} - return node; +const syntheticFactory: ts.BaseNodeFactory = { + createBaseSourceFileNode: kind => makeSynthetic(baseFactory.createBaseSourceFileNode(kind)), + createBaseIdentifierNode: kind => makeSynthetic(baseFactory.createBaseIdentifierNode(kind)), + createBasePrivateIdentifierNode: kind => makeSynthetic(baseFactory.createBasePrivateIdentifierNode(kind)), + createBaseTokenNode: kind => makeSynthetic(baseFactory.createBaseTokenNode(kind)), + createBaseNode: kind => makeSynthetic(baseFactory.createBaseNode(kind)), +}; + +export const factory = createNodeFactory(NodeFactoryFlags.NoIndentationOnFreshPropertyAccess, syntheticFactory); + +export function createUnparsedSourceFile(text: string): ts.UnparsedSource; +export function createUnparsedSourceFile(inputFile: ts.InputFiles, type: "js" | "dts", stripInternal?: boolean): ts.UnparsedSource; +export function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): ts.UnparsedSource; +export function createUnparsedSourceFile(textOrInputFiles: string | ts.InputFiles, mapPathOrType?: string, mapTextOrStripInternal?: string | boolean): ts.UnparsedSource { + let stripInternal: boolean | undefined; + let bundleFileInfo: ts.BundleFileInfo | undefined; + let fileName: string; + let text: string | undefined; + let length: number | (() => number); + let sourceMapPath: string | undefined; + let sourceMapText: string | undefined; + let getText: (() => string) | undefined; + let getSourceMapText: (() => string | undefined) | undefined; + let oldFileOfCurrentEmit: boolean | undefined; + + if (!ts.isString(textOrInputFiles)) { + ts.Debug.assert(mapPathOrType === "js" || mapPathOrType === "dts"); + fileName = (mapPathOrType === "js" ? textOrInputFiles.javascriptPath : textOrInputFiles.declarationPath) || ""; + sourceMapPath = mapPathOrType === "js" ? textOrInputFiles.javascriptMapPath : textOrInputFiles.declarationMapPath; + getText = () => mapPathOrType === "js" ? textOrInputFiles.javascriptText : textOrInputFiles.declarationText; + getSourceMapText = () => mapPathOrType === "js" ? textOrInputFiles.javascriptMapText : textOrInputFiles.declarationMapText; + length = () => getText!().length; + if (textOrInputFiles.buildInfo && textOrInputFiles.buildInfo.bundle) { + ts.Debug.assert(mapTextOrStripInternal === undefined || typeof mapTextOrStripInternal === "boolean"); + stripInternal = mapTextOrStripInternal; + bundleFileInfo = mapPathOrType === "js" ? textOrInputFiles.buildInfo.bundle.js : textOrInputFiles.buildInfo.bundle.dts; + oldFileOfCurrentEmit = textOrInputFiles.oldFileOfCurrentEmit; + } + } + else { + fileName = ""; + text = textOrInputFiles; + length = textOrInputFiles.length; + sourceMapPath = mapPathOrType; + sourceMapText = mapTextOrStripInternal as string; + } + const node = oldFileOfCurrentEmit ? + parseOldFileOfCurrentEmit(ts.Debug.checkDefined(bundleFileInfo)) : + parseUnparsedSourceFile(bundleFileInfo, stripInternal, length); + node.fileName = fileName; + node.sourceMapPath = sourceMapPath; + node.oldFileOfCurrentEmit = oldFileOfCurrentEmit; + if (getText && getSourceMapText) { + Object.defineProperty(node, "text", { get: getText }); + Object.defineProperty(node, "sourceMapText", { get: getSourceMapText }); + } + else { + ts.Debug.assert(!oldFileOfCurrentEmit); + node.text = text ?? ""; + node.sourceMapText = sourceMapText; } - function parseUnparsedSourceFile(bundleFileInfo: ts.BundleFileInfo | undefined, stripInternal: boolean | undefined, length: number | (() => number)) { - let prologues: ts.UnparsedPrologue[] | undefined; - let helpers: ts.UnscopedEmitHelper[] | undefined; - let referencedFiles: ts.FileReference[] | undefined; - let typeReferenceDirectives: ts.FileReference[] | undefined; - let libReferenceDirectives: ts.FileReference[] | undefined; - let prependChildren: ts.UnparsedTextLike[] | undefined; - let texts: ts.UnparsedSourceText[] | undefined; - let hasNoDefaultLib: boolean | undefined; + return node; +} - for (const section of bundleFileInfo ? bundleFileInfo.sections : ts.emptyArray) { - switch (section.kind) { - case ts.BundleFileSectionKind.Prologue: - prologues = ts.append(prologues, ts.setTextRange(factory.createUnparsedPrologue(section.data), section)); - break; - case ts.BundleFileSectionKind.EmitHelpers: - helpers = ts.append(helpers, ts.getAllUnscopedEmitHelpers().get(section.data)!); - break; - case ts.BundleFileSectionKind.NoDefaultLib: - hasNoDefaultLib = true; - break; - case ts.BundleFileSectionKind.Reference: - referencedFiles = ts.append(referencedFiles, { pos: -1, end: -1, fileName: section.data }); - break; - case ts.BundleFileSectionKind.Type: - typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data }); - break; - case ts.BundleFileSectionKind.TypeResolutionModeImport: - typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ts.ModuleKind.ESNext }); - break; - case ts.BundleFileSectionKind.TypeResolutionModeRequire: - typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ts.ModuleKind.CommonJS }); - break; - case ts.BundleFileSectionKind.Lib: - libReferenceDirectives = ts.append(libReferenceDirectives, { pos: -1, end: -1, fileName: section.data }); - break; - case ts.BundleFileSectionKind.Prepend: - let prependTexts: ts.UnparsedTextLike[] | undefined; - for (const text of section.texts) { - if (!stripInternal || text.kind !== ts.BundleFileSectionKind.Internal) { - prependTexts = ts.append(prependTexts, ts.setTextRange(factory.createUnparsedTextLike(text.data, text.kind === ts.BundleFileSectionKind.Internal), text)); - } - } - prependChildren = ts.addRange(prependChildren, prependTexts); - texts = ts.append(texts, factory.createUnparsedPrepend(section.data, prependTexts ?? ts.emptyArray)); - break; - case ts.BundleFileSectionKind.Internal: - if (stripInternal) { - if (!texts) - texts = []; - break; +function parseUnparsedSourceFile(bundleFileInfo: ts.BundleFileInfo | undefined, stripInternal: boolean | undefined, length: number | (() => number)) { + let prologues: ts.UnparsedPrologue[] | undefined; + let helpers: ts.UnscopedEmitHelper[] | undefined; + let referencedFiles: ts.FileReference[] | undefined; + let typeReferenceDirectives: ts.FileReference[] | undefined; + let libReferenceDirectives: ts.FileReference[] | undefined; + let prependChildren: ts.UnparsedTextLike[] | undefined; + let texts: ts.UnparsedSourceText[] | undefined; + let hasNoDefaultLib: boolean | undefined; + + for (const section of bundleFileInfo ? bundleFileInfo.sections : ts.emptyArray) { + switch (section.kind) { + case ts.BundleFileSectionKind.Prologue: + prologues = ts.append(prologues, ts.setTextRange(factory.createUnparsedPrologue(section.data), section)); + break; + case ts.BundleFileSectionKind.EmitHelpers: + helpers = ts.append(helpers, ts.getAllUnscopedEmitHelpers().get(section.data)!); + break; + case ts.BundleFileSectionKind.NoDefaultLib: + hasNoDefaultLib = true; + break; + case ts.BundleFileSectionKind.Reference: + referencedFiles = ts.append(referencedFiles, { pos: -1, end: -1, fileName: section.data }); + break; + case ts.BundleFileSectionKind.Type: + typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data }); + break; + case ts.BundleFileSectionKind.TypeResolutionModeImport: + typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ts.ModuleKind.ESNext }); + break; + case ts.BundleFileSectionKind.TypeResolutionModeRequire: + typeReferenceDirectives = ts.append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ts.ModuleKind.CommonJS }); + break; + case ts.BundleFileSectionKind.Lib: + libReferenceDirectives = ts.append(libReferenceDirectives, { pos: -1, end: -1, fileName: section.data }); + break; + case ts.BundleFileSectionKind.Prepend: + let prependTexts: ts.UnparsedTextLike[] | undefined; + for (const text of section.texts) { + if (!stripInternal || text.kind !== ts.BundleFileSectionKind.Internal) { + prependTexts = ts.append(prependTexts, ts.setTextRange(factory.createUnparsedTextLike(text.data, text.kind === ts.BundleFileSectionKind.Internal), text)); } - // falls through - - case ts.BundleFileSectionKind.Text: - texts = ts.append(texts, ts.setTextRange(factory.createUnparsedTextLike(section.data, section.kind === ts.BundleFileSectionKind.Internal), section)); + } + prependChildren = ts.addRange(prependChildren, prependTexts); + texts = ts.append(texts, factory.createUnparsedPrepend(section.data, prependTexts ?? ts.emptyArray)); + break; + case ts.BundleFileSectionKind.Internal: + if (stripInternal) { + if (!texts) + texts = []; break; - default: - ts.Debug.assertNever(section); - } - } + } + // falls through - if (!texts) { - const textNode = factory.createUnparsedTextLike(/*data*/ undefined, /*internal*/ false); - ts.setTextRangePosWidth(textNode, 0, typeof length === "function" ? length() : length); - texts = [textNode]; + case ts.BundleFileSectionKind.Text: + texts = ts.append(texts, ts.setTextRange(factory.createUnparsedTextLike(section.data, section.kind === ts.BundleFileSectionKind.Internal), section)); + break; + default: + ts.Debug.assertNever(section); } + } - const node = ts.parseNodeFactory.createUnparsedSource(prologues ?? ts.emptyArray, /*syntheticReferences*/ undefined, texts); - ts.setEachParent(prologues, node); - ts.setEachParent(texts, node); - ts.setEachParent(prependChildren, node); - node.hasNoDefaultLib = hasNoDefaultLib; - node.helpers = helpers; - node.referencedFiles = referencedFiles || ts.emptyArray; - node.typeReferenceDirectives = typeReferenceDirectives; - node.libReferenceDirectives = libReferenceDirectives || ts.emptyArray; - return node; + if (!texts) { + const textNode = factory.createUnparsedTextLike(/*data*/ undefined, /*internal*/ false); + ts.setTextRangePosWidth(textNode, 0, typeof length === "function" ? length() : length); + texts = [textNode]; } - function parseOldFileOfCurrentEmit(bundleFileInfo: ts.BundleFileInfo) { - let texts: ts.UnparsedTextLike[] | undefined; - let syntheticReferences: ts.UnparsedSyntheticReference[] | undefined; - for (const section of bundleFileInfo.sections) { - switch (section.kind) { - case ts.BundleFileSectionKind.Internal: - case ts.BundleFileSectionKind.Text: - texts = ts.append(texts, ts.setTextRange(factory.createUnparsedTextLike(section.data, section.kind === ts.BundleFileSectionKind.Internal), section)); - break; + const node = ts.parseNodeFactory.createUnparsedSource(prologues ?? ts.emptyArray, /*syntheticReferences*/ undefined, texts); + ts.setEachParent(prologues, node); + ts.setEachParent(texts, node); + ts.setEachParent(prependChildren, node); + node.hasNoDefaultLib = hasNoDefaultLib; + node.helpers = helpers; + node.referencedFiles = referencedFiles || ts.emptyArray; + node.typeReferenceDirectives = typeReferenceDirectives; + node.libReferenceDirectives = libReferenceDirectives || ts.emptyArray; + return node; +} - case ts.BundleFileSectionKind.NoDefaultLib: - case ts.BundleFileSectionKind.Reference: - case ts.BundleFileSectionKind.Type: - case ts.BundleFileSectionKind.TypeResolutionModeImport: - case ts.BundleFileSectionKind.TypeResolutionModeRequire: - case ts.BundleFileSectionKind.Lib: - syntheticReferences = ts.append(syntheticReferences, ts.setTextRange(factory.createUnparsedSyntheticReference(section), section)); - break; +function parseOldFileOfCurrentEmit(bundleFileInfo: ts.BundleFileInfo) { + let texts: ts.UnparsedTextLike[] | undefined; + let syntheticReferences: ts.UnparsedSyntheticReference[] | undefined; + for (const section of bundleFileInfo.sections) { + switch (section.kind) { + case ts.BundleFileSectionKind.Internal: + case ts.BundleFileSectionKind.Text: + texts = ts.append(texts, ts.setTextRange(factory.createUnparsedTextLike(section.data, section.kind === ts.BundleFileSectionKind.Internal), section)); + break; - // Ignore - case ts.BundleFileSectionKind.Prologue: - case ts.BundleFileSectionKind.EmitHelpers: - case ts.BundleFileSectionKind.Prepend: - break; + case ts.BundleFileSectionKind.NoDefaultLib: + case ts.BundleFileSectionKind.Reference: + case ts.BundleFileSectionKind.Type: + case ts.BundleFileSectionKind.TypeResolutionModeImport: + case ts.BundleFileSectionKind.TypeResolutionModeRequire: + case ts.BundleFileSectionKind.Lib: + syntheticReferences = ts.append(syntheticReferences, ts.setTextRange(factory.createUnparsedSyntheticReference(section), section)); + break; - default: - ts.Debug.assertNever(section); - } - } + // Ignore + case ts.BundleFileSectionKind.Prologue: + case ts.BundleFileSectionKind.EmitHelpers: + case ts.BundleFileSectionKind.Prepend: + break; - const node = factory.createUnparsedSource(ts.emptyArray, syntheticReferences, texts ?? ts.emptyArray); - ts.setEachParent(syntheticReferences, node); - ts.setEachParent(texts, node); - node.helpers = ts.map(bundleFileInfo.sources && bundleFileInfo.sources.helpers, name => ts.getAllUnscopedEmitHelpers().get(name)!); - return node; - } - - // TODO(rbuckton): Move part of this to factory - export function createInputFiles(javascriptText: string, declarationText: string): ts.InputFiles; - export function createInputFiles(readFileText: (path: string) => string | undefined, javascriptPath: string, javascriptMapPath: string | undefined, declarationPath: string, declarationMapPath: string | undefined, buildInfoPath: string | undefined): ts.InputFiles; - export function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined): ts.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?: ts.BuildInfo, oldFileOfCurrentEmit?: boolean): ts.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?: ts.BuildInfo, oldFileOfCurrentEmit?: boolean): ts.InputFiles { - const node = ts.parseNodeFactory.createInputFiles(); - if (!ts.isString(javascriptTextOrReadFileText)) { - const cache = new ts.Map(); - 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: ts.BuildInfo | false; - const getAndCacheBuildInfo = (getText: () => string | undefined) => { - if (buildInfo === undefined) { - const result = getText(); - buildInfo = result !== undefined ? ts.getBuildInfo(result) : false; - } - return buildInfo || undefined; - }; - node.javascriptPath = declarationTextOrJavascriptPath; - node.javascriptMapPath = javascriptMapPath; - node.declarationPath = ts.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(ts.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; + default: + ts.Debug.assertNever(section); } - return node; } - // tslint:disable-next-line variable-name - let SourceMapSource: new (fileName: string, text: string, skipTrivia?: (pos: number) => number) => ts.SourceMapSource; + const node = factory.createUnparsedSource(ts.emptyArray, syntheticReferences, texts ?? ts.emptyArray); + ts.setEachParent(syntheticReferences, node); + ts.setEachParent(texts, node); + node.helpers = ts.map(bundleFileInfo.sources && bundleFileInfo.sources.helpers, name => ts.getAllUnscopedEmitHelpers().get(name)!); + return node; +} - /** - * 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 = ts.objectAllocator.getSourceMapSourceConstructor()))(fileName, text, skipTrivia); +// TODO(rbuckton): Move part of this to factory +export function createInputFiles(javascriptText: string, declarationText: string): ts.InputFiles; +export function createInputFiles(readFileText: (path: string) => string | undefined, javascriptPath: string, javascriptMapPath: string | undefined, declarationPath: string, declarationMapPath: string | undefined, buildInfoPath: string | undefined): ts.InputFiles; +export function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined): ts.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?: ts.BuildInfo, oldFileOfCurrentEmit?: boolean): ts.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?: ts.BuildInfo, oldFileOfCurrentEmit?: boolean): ts.InputFiles { + const node = ts.parseNodeFactory.createInputFiles(); + if (!ts.isString(javascriptTextOrReadFileText)) { + const cache = new ts.Map(); + 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: ts.BuildInfo | false; + const getAndCacheBuildInfo = (getText: () => string | undefined) => { + if (buildInfo === undefined) { + const result = getText(); + buildInfo = result !== undefined ? ts.getBuildInfo(result) : false; + } + return buildInfo || undefined; + }; + node.javascriptPath = declarationTextOrJavascriptPath; + node.javascriptMapPath = javascriptMapPath; + node.declarationPath = ts.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(ts.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; +} - // Utilities +// tslint:disable-next-line variable-name +let SourceMapSource: new (fileName: string, text: string, skipTrivia?: (pos: number) => number) => ts.SourceMapSource; - export function setOriginalNode(node: T, original: ts.Node | undefined): T { - node.original = original; - if (original) { - const emitNode = original.emitNode; - if (emitNode) - node.emitNode = mergeEmitNode(emitNode, node.emitNode); - } - return node; +/** + * 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 = ts.objectAllocator.getSourceMapSourceConstructor()))(fileName, text, skipTrivia); +} + +// Utilities + +export function setOriginalNode(node: T, original: ts.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: ts.EmitNode, destEmitNode: ts.EmitNode | undefined) { - const { flags, leadingComments, trailingComments, commentRange, sourceMapRange, tokenSourceMapRanges, constantValue, helpers, startsOnNewLine, } = sourceEmitNode; - if (!destEmitNode) - destEmitNode = {} as ts.EmitNode; - // We are using `.slice()` here in case `destEmitNode.leadingComments` is pushed to later. - if (leadingComments) - destEmitNode.leadingComments = ts.addRange(leadingComments.slice(), destEmitNode.leadingComments); - if (trailingComments) - destEmitNode.trailingComments = ts.addRange(trailingComments.slice(), destEmitNode.trailingComments); - if (flags) - destEmitNode.flags = flags & ~ts.EmitFlags.Immutable; - 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) { - for (const helper of helpers) { - destEmitNode.helpers = ts.appendIfUnique(destEmitNode.helpers, helper); - } +function mergeEmitNode(sourceEmitNode: ts.EmitNode, destEmitNode: ts.EmitNode | undefined) { + const { flags, leadingComments, trailingComments, commentRange, sourceMapRange, tokenSourceMapRanges, constantValue, helpers, startsOnNewLine, } = sourceEmitNode; + if (!destEmitNode) + destEmitNode = {} as ts.EmitNode; + // We are using `.slice()` here in case `destEmitNode.leadingComments` is pushed to later. + if (leadingComments) + destEmitNode.leadingComments = ts.addRange(leadingComments.slice(), destEmitNode.leadingComments); + if (trailingComments) + destEmitNode.trailingComments = ts.addRange(trailingComments.slice(), destEmitNode.trailingComments); + if (flags) + destEmitNode.flags = flags & ~ts.EmitFlags.Immutable; + 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) { + for (const helper of helpers) { + destEmitNode.helpers = ts.appendIfUnique(destEmitNode.helpers, helper); } - if (startsOnNewLine !== undefined) - destEmitNode.startsOnNewLine = startsOnNewLine; - return destEmitNode; } + if (startsOnNewLine !== undefined) + destEmitNode.startsOnNewLine = startsOnNewLine; + return destEmitNode; +} - function mergeTokenSourceMapRanges(sourceRanges: (ts.TextRange | undefined)[], destRanges: (ts.TextRange | undefined)[]) { - if (!destRanges) - destRanges = []; - for (const key in sourceRanges) { - destRanges[key] = sourceRanges[key]; - } - return destRanges; +function mergeTokenSourceMapRanges(sourceRanges: (ts.TextRange | undefined)[], destRanges: (ts.TextRange | undefined)[]) { + if (!destRanges) + destRanges = []; + for (const key in sourceRanges) { + destRanges[key] = sourceRanges[key]; } + return destRanges; +} } diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index 8fe19d157441e..acc49cd898788 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -1,942 +1,942 @@ namespace ts { - // Literals +// Literals - export function isNumericLiteral(node: ts.Node): node is ts.NumericLiteral { - return node.kind === ts.SyntaxKind.NumericLiteral; - } +export function isNumericLiteral(node: ts.Node): node is ts.NumericLiteral { + return node.kind === ts.SyntaxKind.NumericLiteral; +} - export function isBigIntLiteral(node: ts.Node): node is ts.BigIntLiteral { - return node.kind === ts.SyntaxKind.BigIntLiteral; - } +export function isBigIntLiteral(node: ts.Node): node is ts.BigIntLiteral { + return node.kind === ts.SyntaxKind.BigIntLiteral; +} - export function isStringLiteral(node: ts.Node): node is ts.StringLiteral { - return node.kind === ts.SyntaxKind.StringLiteral; - } +export function isStringLiteral(node: ts.Node): node is ts.StringLiteral { + return node.kind === ts.SyntaxKind.StringLiteral; +} - export function isJsxText(node: ts.Node): node is ts.JsxText { - return node.kind === ts.SyntaxKind.JsxText; - } +export function isJsxText(node: ts.Node): node is ts.JsxText { + return node.kind === ts.SyntaxKind.JsxText; +} - export function isRegularExpressionLiteral(node: ts.Node): node is ts.RegularExpressionLiteral { - return node.kind === ts.SyntaxKind.RegularExpressionLiteral; - } +export function isRegularExpressionLiteral(node: ts.Node): node is ts.RegularExpressionLiteral { + return node.kind === ts.SyntaxKind.RegularExpressionLiteral; +} - export function isNoSubstitutionTemplateLiteral(node: ts.Node): node is ts.NoSubstitutionTemplateLiteral { - return node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral; - } +export function isNoSubstitutionTemplateLiteral(node: ts.Node): node is ts.NoSubstitutionTemplateLiteral { + return node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral; +} - // Pseudo-literals - - export function isTemplateHead(node: ts.Node): node is ts.TemplateHead { - return node.kind === ts.SyntaxKind.TemplateHead; - } - - export function isTemplateMiddle(node: ts.Node): node is ts.TemplateMiddle { - return node.kind === ts.SyntaxKind.TemplateMiddle; - } - - export function isTemplateTail(node: ts.Node): node is ts.TemplateTail { - return node.kind === ts.SyntaxKind.TemplateTail; - } - - // Punctuation - - export function isDotDotDotToken(node: ts.Node): node is ts.DotDotDotToken { - return node.kind === ts.SyntaxKind.DotDotDotToken; - } - - /*@internal*/ - export function isCommaToken(node: ts.Node): node is ts.Token { - return node.kind === ts.SyntaxKind.CommaToken; - } +// Pseudo-literals - export function isPlusToken(node: ts.Node): node is ts.PlusToken { - return node.kind === ts.SyntaxKind.PlusToken; - } +export function isTemplateHead(node: ts.Node): node is ts.TemplateHead { + return node.kind === ts.SyntaxKind.TemplateHead; +} - export function isMinusToken(node: ts.Node): node is ts.MinusToken { - return node.kind === ts.SyntaxKind.MinusToken; - } +export function isTemplateMiddle(node: ts.Node): node is ts.TemplateMiddle { + return node.kind === ts.SyntaxKind.TemplateMiddle; +} - export function isAsteriskToken(node: ts.Node): node is ts.AsteriskToken { - return node.kind === ts.SyntaxKind.AsteriskToken; - } +export function isTemplateTail(node: ts.Node): node is ts.TemplateTail { + return node.kind === ts.SyntaxKind.TemplateTail; +} - /*@internal*/ - export function isExclamationToken(node: ts.Node): node is ts.ExclamationToken { - return node.kind === ts.SyntaxKind.ExclamationToken; - } - - /*@internal*/ - export function isQuestionToken(node: ts.Node): node is ts.QuestionToken { - return node.kind === ts.SyntaxKind.QuestionToken; - } +// Punctuation - /*@internal*/ - export function isColonToken(node: ts.Node): node is ts.ColonToken { - return node.kind === ts.SyntaxKind.ColonToken; - } +export function isDotDotDotToken(node: ts.Node): node is ts.DotDotDotToken { + return node.kind === ts.SyntaxKind.DotDotDotToken; +} - /*@internal*/ - export function isQuestionDotToken(node: ts.Node): node is ts.QuestionDotToken { - return node.kind === ts.SyntaxKind.QuestionDotToken; - } +/*@internal*/ +export function isCommaToken(node: ts.Node): node is ts.Token { + return node.kind === ts.SyntaxKind.CommaToken; +} - /*@internal*/ - export function isEqualsGreaterThanToken(node: ts.Node): node is ts.EqualsGreaterThanToken { - return node.kind === ts.SyntaxKind.EqualsGreaterThanToken; - } +export function isPlusToken(node: ts.Node): node is ts.PlusToken { + return node.kind === ts.SyntaxKind.PlusToken; +} - // Identifiers +export function isMinusToken(node: ts.Node): node is ts.MinusToken { + return node.kind === ts.SyntaxKind.MinusToken; +} - export function isIdentifier(node: ts.Node): node is ts.Identifier { - return node.kind === ts.SyntaxKind.Identifier; - } +export function isAsteriskToken(node: ts.Node): node is ts.AsteriskToken { + return node.kind === ts.SyntaxKind.AsteriskToken; +} - export function isPrivateIdentifier(node: ts.Node): node is ts.PrivateIdentifier { - return node.kind === ts.SyntaxKind.PrivateIdentifier; - } +/*@internal*/ +export function isExclamationToken(node: ts.Node): node is ts.ExclamationToken { + return node.kind === ts.SyntaxKind.ExclamationToken; +} - // Reserved Words +/*@internal*/ +export function isQuestionToken(node: ts.Node): node is ts.QuestionToken { + return node.kind === ts.SyntaxKind.QuestionToken; +} - /* @internal */ - export function isExportModifier(node: ts.Node): node is ts.ExportKeyword { - return node.kind === ts.SyntaxKind.ExportKeyword; - } +/*@internal*/ +export function isColonToken(node: ts.Node): node is ts.ColonToken { + return node.kind === ts.SyntaxKind.ColonToken; +} - /* @internal */ - export function isAsyncModifier(node: ts.Node): node is ts.AsyncKeyword { - return node.kind === ts.SyntaxKind.AsyncKeyword; - } +/*@internal*/ +export function isQuestionDotToken(node: ts.Node): node is ts.QuestionDotToken { + return node.kind === ts.SyntaxKind.QuestionDotToken; +} - /* @internal */ - export function isAssertsKeyword(node: ts.Node): node is ts.AssertsKeyword { - return node.kind === ts.SyntaxKind.AssertsKeyword; - } - - /* @internal */ - export function isAwaitKeyword(node: ts.Node): node is ts.AwaitKeyword { - return node.kind === ts.SyntaxKind.AwaitKeyword; - } +/*@internal*/ +export function isEqualsGreaterThanToken(node: ts.Node): node is ts.EqualsGreaterThanToken { + return node.kind === ts.SyntaxKind.EqualsGreaterThanToken; +} - /* @internal */ - export function isReadonlyKeyword(node: ts.Node): node is ts.ReadonlyKeyword { - return node.kind === ts.SyntaxKind.ReadonlyKeyword; - } +// Identifiers - /* @internal */ - export function isStaticModifier(node: ts.Node): node is ts.StaticKeyword { - return node.kind === ts.SyntaxKind.StaticKeyword; - } +export function isIdentifier(node: ts.Node): node is ts.Identifier { + return node.kind === ts.SyntaxKind.Identifier; +} - /* @internal */ - export function isAbstractModifier(node: ts.Node): node is ts.AbstractKeyword { - return node.kind === ts.SyntaxKind.AbstractKeyword; - } +export function isPrivateIdentifier(node: ts.Node): node is ts.PrivateIdentifier { + return node.kind === ts.SyntaxKind.PrivateIdentifier; +} - /*@internal*/ - export function isSuperKeyword(node: ts.Node): node is ts.SuperExpression { - return node.kind === ts.SyntaxKind.SuperKeyword; - } +// Reserved Words - /*@internal*/ - export function isImportKeyword(node: ts.Node): node is ts.ImportExpression { - return node.kind === ts.SyntaxKind.ImportKeyword; - } - - // Names - - export function isQualifiedName(node: ts.Node): node is ts.QualifiedName { - return node.kind === ts.SyntaxKind.QualifiedName; - } +/* @internal */ +export function isExportModifier(node: ts.Node): node is ts.ExportKeyword { + return node.kind === ts.SyntaxKind.ExportKeyword; +} - export function isComputedPropertyName(node: ts.Node): node is ts.ComputedPropertyName { - return node.kind === ts.SyntaxKind.ComputedPropertyName; - } +/* @internal */ +export function isAsyncModifier(node: ts.Node): node is ts.AsyncKeyword { + return node.kind === ts.SyntaxKind.AsyncKeyword; +} - // Signature elements - - export function isTypeParameterDeclaration(node: ts.Node): node is ts.TypeParameterDeclaration { - return node.kind === ts.SyntaxKind.TypeParameter; - } - - // TODO(rbuckton): Rename to 'isParameterDeclaration' - export function isParameter(node: ts.Node): node is ts.ParameterDeclaration { - return node.kind === ts.SyntaxKind.Parameter; - } +/* @internal */ +export function isAssertsKeyword(node: ts.Node): node is ts.AssertsKeyword { + return node.kind === ts.SyntaxKind.AssertsKeyword; +} - export function isDecorator(node: ts.Node): node is ts.Decorator { - return node.kind === ts.SyntaxKind.Decorator; - } +/* @internal */ +export function isAwaitKeyword(node: ts.Node): node is ts.AwaitKeyword { + return node.kind === ts.SyntaxKind.AwaitKeyword; +} - // TypeMember +/* @internal */ +export function isReadonlyKeyword(node: ts.Node): node is ts.ReadonlyKeyword { + return node.kind === ts.SyntaxKind.ReadonlyKeyword; +} - export function isPropertySignature(node: ts.Node): node is ts.PropertySignature { - return node.kind === ts.SyntaxKind.PropertySignature; - } +/* @internal */ +export function isStaticModifier(node: ts.Node): node is ts.StaticKeyword { + return node.kind === ts.SyntaxKind.StaticKeyword; +} - export function isPropertyDeclaration(node: ts.Node): node is ts.PropertyDeclaration { - return node.kind === ts.SyntaxKind.PropertyDeclaration; - } +/* @internal */ +export function isAbstractModifier(node: ts.Node): node is ts.AbstractKeyword { + return node.kind === ts.SyntaxKind.AbstractKeyword; +} - export function isMethodSignature(node: ts.Node): node is ts.MethodSignature { - return node.kind === ts.SyntaxKind.MethodSignature; - } +/*@internal*/ +export function isSuperKeyword(node: ts.Node): node is ts.SuperExpression { + return node.kind === ts.SyntaxKind.SuperKeyword; +} - export function isMethodDeclaration(node: ts.Node): node is ts.MethodDeclaration { - return node.kind === ts.SyntaxKind.MethodDeclaration; - } +/*@internal*/ +export function isImportKeyword(node: ts.Node): node is ts.ImportExpression { + return node.kind === ts.SyntaxKind.ImportKeyword; +} - export function isClassStaticBlockDeclaration(node: ts.Node): node is ts.ClassStaticBlockDeclaration { - return node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; - } +// Names - export function isConstructorDeclaration(node: ts.Node): node is ts.ConstructorDeclaration { - return node.kind === ts.SyntaxKind.Constructor; - } +export function isQualifiedName(node: ts.Node): node is ts.QualifiedName { + return node.kind === ts.SyntaxKind.QualifiedName; +} - export function isGetAccessorDeclaration(node: ts.Node): node is ts.GetAccessorDeclaration { - return node.kind === ts.SyntaxKind.GetAccessor; - } +export function isComputedPropertyName(node: ts.Node): node is ts.ComputedPropertyName { + return node.kind === ts.SyntaxKind.ComputedPropertyName; +} - export function isSetAccessorDeclaration(node: ts.Node): node is ts.SetAccessorDeclaration { - return node.kind === ts.SyntaxKind.SetAccessor; - } +// Signature elements - export function isCallSignatureDeclaration(node: ts.Node): node is ts.CallSignatureDeclaration { - return node.kind === ts.SyntaxKind.CallSignature; - } +export function isTypeParameterDeclaration(node: ts.Node): node is ts.TypeParameterDeclaration { + return node.kind === ts.SyntaxKind.TypeParameter; +} - export function isConstructSignatureDeclaration(node: ts.Node): node is ts.ConstructSignatureDeclaration { - return node.kind === ts.SyntaxKind.ConstructSignature; - } +// TODO(rbuckton): Rename to 'isParameterDeclaration' +export function isParameter(node: ts.Node): node is ts.ParameterDeclaration { + return node.kind === ts.SyntaxKind.Parameter; +} - export function isIndexSignatureDeclaration(node: ts.Node): node is ts.IndexSignatureDeclaration { - return node.kind === ts.SyntaxKind.IndexSignature; - } +export function isDecorator(node: ts.Node): node is ts.Decorator { + return node.kind === ts.SyntaxKind.Decorator; +} - // Type +// TypeMember - export function isTypePredicateNode(node: ts.Node): node is ts.TypePredicateNode { - return node.kind === ts.SyntaxKind.TypePredicate; - } +export function isPropertySignature(node: ts.Node): node is ts.PropertySignature { + return node.kind === ts.SyntaxKind.PropertySignature; +} - export function isTypeReferenceNode(node: ts.Node): node is ts.TypeReferenceNode { - return node.kind === ts.SyntaxKind.TypeReference; - } +export function isPropertyDeclaration(node: ts.Node): node is ts.PropertyDeclaration { + return node.kind === ts.SyntaxKind.PropertyDeclaration; +} - export function isFunctionTypeNode(node: ts.Node): node is ts.FunctionTypeNode { - return node.kind === ts.SyntaxKind.FunctionType; - } +export function isMethodSignature(node: ts.Node): node is ts.MethodSignature { + return node.kind === ts.SyntaxKind.MethodSignature; +} - export function isConstructorTypeNode(node: ts.Node): node is ts.ConstructorTypeNode { - return node.kind === ts.SyntaxKind.ConstructorType; - } +export function isMethodDeclaration(node: ts.Node): node is ts.MethodDeclaration { + return node.kind === ts.SyntaxKind.MethodDeclaration; +} - export function isTypeQueryNode(node: ts.Node): node is ts.TypeQueryNode { - return node.kind === ts.SyntaxKind.TypeQuery; - } +export function isClassStaticBlockDeclaration(node: ts.Node): node is ts.ClassStaticBlockDeclaration { + return node.kind === ts.SyntaxKind.ClassStaticBlockDeclaration; +} - export function isTypeLiteralNode(node: ts.Node): node is ts.TypeLiteralNode { - return node.kind === ts.SyntaxKind.TypeLiteral; - } +export function isConstructorDeclaration(node: ts.Node): node is ts.ConstructorDeclaration { + return node.kind === ts.SyntaxKind.Constructor; +} - export function isArrayTypeNode(node: ts.Node): node is ts.ArrayTypeNode { - return node.kind === ts.SyntaxKind.ArrayType; - } +export function isGetAccessorDeclaration(node: ts.Node): node is ts.GetAccessorDeclaration { + return node.kind === ts.SyntaxKind.GetAccessor; +} - export function isTupleTypeNode(node: ts.Node): node is ts.TupleTypeNode { - return node.kind === ts.SyntaxKind.TupleType; - } +export function isSetAccessorDeclaration(node: ts.Node): node is ts.SetAccessorDeclaration { + return node.kind === ts.SyntaxKind.SetAccessor; +} - export function isNamedTupleMember(node: ts.Node): node is ts.NamedTupleMember { - return node.kind === ts.SyntaxKind.NamedTupleMember; - } +export function isCallSignatureDeclaration(node: ts.Node): node is ts.CallSignatureDeclaration { + return node.kind === ts.SyntaxKind.CallSignature; +} - export function isOptionalTypeNode(node: ts.Node): node is ts.OptionalTypeNode { - return node.kind === ts.SyntaxKind.OptionalType; - } +export function isConstructSignatureDeclaration(node: ts.Node): node is ts.ConstructSignatureDeclaration { + return node.kind === ts.SyntaxKind.ConstructSignature; +} - export function isRestTypeNode(node: ts.Node): node is ts.RestTypeNode { - return node.kind === ts.SyntaxKind.RestType; - } +export function isIndexSignatureDeclaration(node: ts.Node): node is ts.IndexSignatureDeclaration { + return node.kind === ts.SyntaxKind.IndexSignature; +} - export function isUnionTypeNode(node: ts.Node): node is ts.UnionTypeNode { - return node.kind === ts.SyntaxKind.UnionType; - } +// Type - export function isIntersectionTypeNode(node: ts.Node): node is ts.IntersectionTypeNode { - return node.kind === ts.SyntaxKind.IntersectionType; - } +export function isTypePredicateNode(node: ts.Node): node is ts.TypePredicateNode { + return node.kind === ts.SyntaxKind.TypePredicate; +} - export function isConditionalTypeNode(node: ts.Node): node is ts.ConditionalTypeNode { - return node.kind === ts.SyntaxKind.ConditionalType; - } +export function isTypeReferenceNode(node: ts.Node): node is ts.TypeReferenceNode { + return node.kind === ts.SyntaxKind.TypeReference; +} - export function isInferTypeNode(node: ts.Node): node is ts.InferTypeNode { - return node.kind === ts.SyntaxKind.InferType; - } +export function isFunctionTypeNode(node: ts.Node): node is ts.FunctionTypeNode { + return node.kind === ts.SyntaxKind.FunctionType; +} - export function isParenthesizedTypeNode(node: ts.Node): node is ts.ParenthesizedTypeNode { - return node.kind === ts.SyntaxKind.ParenthesizedType; - } +export function isConstructorTypeNode(node: ts.Node): node is ts.ConstructorTypeNode { + return node.kind === ts.SyntaxKind.ConstructorType; +} - export function isThisTypeNode(node: ts.Node): node is ts.ThisTypeNode { - return node.kind === ts.SyntaxKind.ThisType; - } +export function isTypeQueryNode(node: ts.Node): node is ts.TypeQueryNode { + return node.kind === ts.SyntaxKind.TypeQuery; +} - export function isTypeOperatorNode(node: ts.Node): node is ts.TypeOperatorNode { - return node.kind === ts.SyntaxKind.TypeOperator; - } +export function isTypeLiteralNode(node: ts.Node): node is ts.TypeLiteralNode { + return node.kind === ts.SyntaxKind.TypeLiteral; +} - export function isIndexedAccessTypeNode(node: ts.Node): node is ts.IndexedAccessTypeNode { - return node.kind === ts.SyntaxKind.IndexedAccessType; - } +export function isArrayTypeNode(node: ts.Node): node is ts.ArrayTypeNode { + return node.kind === ts.SyntaxKind.ArrayType; +} - export function isMappedTypeNode(node: ts.Node): node is ts.MappedTypeNode { - return node.kind === ts.SyntaxKind.MappedType; - } +export function isTupleTypeNode(node: ts.Node): node is ts.TupleTypeNode { + return node.kind === ts.SyntaxKind.TupleType; +} - export function isLiteralTypeNode(node: ts.Node): node is ts.LiteralTypeNode { - return node.kind === ts.SyntaxKind.LiteralType; - } +export function isNamedTupleMember(node: ts.Node): node is ts.NamedTupleMember { + return node.kind === ts.SyntaxKind.NamedTupleMember; +} - export function isImportTypeNode(node: ts.Node): node is ts.ImportTypeNode { - return node.kind === ts.SyntaxKind.ImportType; - } +export function isOptionalTypeNode(node: ts.Node): node is ts.OptionalTypeNode { + return node.kind === ts.SyntaxKind.OptionalType; +} - export function isTemplateLiteralTypeSpan(node: ts.Node): node is ts.TemplateLiteralTypeSpan { - return node.kind === ts.SyntaxKind.TemplateLiteralTypeSpan; - } +export function isRestTypeNode(node: ts.Node): node is ts.RestTypeNode { + return node.kind === ts.SyntaxKind.RestType; +} - export function isTemplateLiteralTypeNode(node: ts.Node): node is ts.TemplateLiteralTypeNode { - return node.kind === ts.SyntaxKind.TemplateLiteralType; - } +export function isUnionTypeNode(node: ts.Node): node is ts.UnionTypeNode { + return node.kind === ts.SyntaxKind.UnionType; +} - // Binding patterns +export function isIntersectionTypeNode(node: ts.Node): node is ts.IntersectionTypeNode { + return node.kind === ts.SyntaxKind.IntersectionType; +} - export function isObjectBindingPattern(node: ts.Node): node is ts.ObjectBindingPattern { - return node.kind === ts.SyntaxKind.ObjectBindingPattern; - } +export function isConditionalTypeNode(node: ts.Node): node is ts.ConditionalTypeNode { + return node.kind === ts.SyntaxKind.ConditionalType; +} - export function isArrayBindingPattern(node: ts.Node): node is ts.ArrayBindingPattern { - return node.kind === ts.SyntaxKind.ArrayBindingPattern; - } +export function isInferTypeNode(node: ts.Node): node is ts.InferTypeNode { + return node.kind === ts.SyntaxKind.InferType; +} - export function isBindingElement(node: ts.Node): node is ts.BindingElement { - return node.kind === ts.SyntaxKind.BindingElement; - } +export function isParenthesizedTypeNode(node: ts.Node): node is ts.ParenthesizedTypeNode { + return node.kind === ts.SyntaxKind.ParenthesizedType; +} - // Expression +export function isThisTypeNode(node: ts.Node): node is ts.ThisTypeNode { + return node.kind === ts.SyntaxKind.ThisType; +} - export function isArrayLiteralExpression(node: ts.Node): node is ts.ArrayLiteralExpression { - return node.kind === ts.SyntaxKind.ArrayLiteralExpression; - } +export function isTypeOperatorNode(node: ts.Node): node is ts.TypeOperatorNode { + return node.kind === ts.SyntaxKind.TypeOperator; +} - export function isObjectLiteralExpression(node: ts.Node): node is ts.ObjectLiteralExpression { - return node.kind === ts.SyntaxKind.ObjectLiteralExpression; - } +export function isIndexedAccessTypeNode(node: ts.Node): node is ts.IndexedAccessTypeNode { + return node.kind === ts.SyntaxKind.IndexedAccessType; +} - export function isPropertyAccessExpression(node: ts.Node): node is ts.PropertyAccessExpression { - return node.kind === ts.SyntaxKind.PropertyAccessExpression; - } +export function isMappedTypeNode(node: ts.Node): node is ts.MappedTypeNode { + return node.kind === ts.SyntaxKind.MappedType; +} - export function isElementAccessExpression(node: ts.Node): node is ts.ElementAccessExpression { - return node.kind === ts.SyntaxKind.ElementAccessExpression; - } +export function isLiteralTypeNode(node: ts.Node): node is ts.LiteralTypeNode { + return node.kind === ts.SyntaxKind.LiteralType; +} - export function isCallExpression(node: ts.Node): node is ts.CallExpression { - return node.kind === ts.SyntaxKind.CallExpression; - } +export function isImportTypeNode(node: ts.Node): node is ts.ImportTypeNode { + return node.kind === ts.SyntaxKind.ImportType; +} - export function isNewExpression(node: ts.Node): node is ts.NewExpression { - return node.kind === ts.SyntaxKind.NewExpression; - } +export function isTemplateLiteralTypeSpan(node: ts.Node): node is ts.TemplateLiteralTypeSpan { + return node.kind === ts.SyntaxKind.TemplateLiteralTypeSpan; +} - export function isTaggedTemplateExpression(node: ts.Node): node is ts.TaggedTemplateExpression { - return node.kind === ts.SyntaxKind.TaggedTemplateExpression; - } +export function isTemplateLiteralTypeNode(node: ts.Node): node is ts.TemplateLiteralTypeNode { + return node.kind === ts.SyntaxKind.TemplateLiteralType; +} - export function isTypeAssertionExpression(node: ts.Node): node is ts.TypeAssertion { - return node.kind === ts.SyntaxKind.TypeAssertionExpression; - } +// Binding patterns - export function isParenthesizedExpression(node: ts.Node): node is ts.ParenthesizedExpression { - return node.kind === ts.SyntaxKind.ParenthesizedExpression; - } +export function isObjectBindingPattern(node: ts.Node): node is ts.ObjectBindingPattern { + return node.kind === ts.SyntaxKind.ObjectBindingPattern; +} - export function isFunctionExpression(node: ts.Node): node is ts.FunctionExpression { - return node.kind === ts.SyntaxKind.FunctionExpression; - } +export function isArrayBindingPattern(node: ts.Node): node is ts.ArrayBindingPattern { + return node.kind === ts.SyntaxKind.ArrayBindingPattern; +} - export function isArrowFunction(node: ts.Node): node is ts.ArrowFunction { - return node.kind === ts.SyntaxKind.ArrowFunction; - } +export function isBindingElement(node: ts.Node): node is ts.BindingElement { + return node.kind === ts.SyntaxKind.BindingElement; +} - export function isDeleteExpression(node: ts.Node): node is ts.DeleteExpression { - return node.kind === ts.SyntaxKind.DeleteExpression; - } +// Expression - export function isTypeOfExpression(node: ts.Node): node is ts.TypeOfExpression { - return node.kind === ts.SyntaxKind.TypeOfExpression; - } +export function isArrayLiteralExpression(node: ts.Node): node is ts.ArrayLiteralExpression { + return node.kind === ts.SyntaxKind.ArrayLiteralExpression; +} - export function isVoidExpression(node: ts.Node): node is ts.VoidExpression { - return node.kind === ts.SyntaxKind.VoidExpression; - } +export function isObjectLiteralExpression(node: ts.Node): node is ts.ObjectLiteralExpression { + return node.kind === ts.SyntaxKind.ObjectLiteralExpression; +} - export function isAwaitExpression(node: ts.Node): node is ts.AwaitExpression { - return node.kind === ts.SyntaxKind.AwaitExpression; - } +export function isPropertyAccessExpression(node: ts.Node): node is ts.PropertyAccessExpression { + return node.kind === ts.SyntaxKind.PropertyAccessExpression; +} - export function isPrefixUnaryExpression(node: ts.Node): node is ts.PrefixUnaryExpression { - return node.kind === ts.SyntaxKind.PrefixUnaryExpression; - } +export function isElementAccessExpression(node: ts.Node): node is ts.ElementAccessExpression { + return node.kind === ts.SyntaxKind.ElementAccessExpression; +} - export function isPostfixUnaryExpression(node: ts.Node): node is ts.PostfixUnaryExpression { - return node.kind === ts.SyntaxKind.PostfixUnaryExpression; - } +export function isCallExpression(node: ts.Node): node is ts.CallExpression { + return node.kind === ts.SyntaxKind.CallExpression; +} - export function isBinaryExpression(node: ts.Node): node is ts.BinaryExpression { - return node.kind === ts.SyntaxKind.BinaryExpression; - } +export function isNewExpression(node: ts.Node): node is ts.NewExpression { + return node.kind === ts.SyntaxKind.NewExpression; +} - export function isConditionalExpression(node: ts.Node): node is ts.ConditionalExpression { - return node.kind === ts.SyntaxKind.ConditionalExpression; - } +export function isTaggedTemplateExpression(node: ts.Node): node is ts.TaggedTemplateExpression { + return node.kind === ts.SyntaxKind.TaggedTemplateExpression; +} - export function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { - return node.kind === ts.SyntaxKind.TemplateExpression; - } +export function isTypeAssertionExpression(node: ts.Node): node is ts.TypeAssertion { + return node.kind === ts.SyntaxKind.TypeAssertionExpression; +} - export function isYieldExpression(node: ts.Node): node is ts.YieldExpression { - return node.kind === ts.SyntaxKind.YieldExpression; - } +export function isParenthesizedExpression(node: ts.Node): node is ts.ParenthesizedExpression { + return node.kind === ts.SyntaxKind.ParenthesizedExpression; +} - export function isSpreadElement(node: ts.Node): node is ts.SpreadElement { - return node.kind === ts.SyntaxKind.SpreadElement; - } +export function isFunctionExpression(node: ts.Node): node is ts.FunctionExpression { + return node.kind === ts.SyntaxKind.FunctionExpression; +} - export function isClassExpression(node: ts.Node): node is ts.ClassExpression { - return node.kind === ts.SyntaxKind.ClassExpression; - } +export function isArrowFunction(node: ts.Node): node is ts.ArrowFunction { + return node.kind === ts.SyntaxKind.ArrowFunction; +} - export function isOmittedExpression(node: ts.Node): node is ts.OmittedExpression { - return node.kind === ts.SyntaxKind.OmittedExpression; - } +export function isDeleteExpression(node: ts.Node): node is ts.DeleteExpression { + return node.kind === ts.SyntaxKind.DeleteExpression; +} - export function isExpressionWithTypeArguments(node: ts.Node): node is ts.ExpressionWithTypeArguments { - return node.kind === ts.SyntaxKind.ExpressionWithTypeArguments; - } +export function isTypeOfExpression(node: ts.Node): node is ts.TypeOfExpression { + return node.kind === ts.SyntaxKind.TypeOfExpression; +} - export function isAsExpression(node: ts.Node): node is ts.AsExpression { - return node.kind === ts.SyntaxKind.AsExpression; - } +export function isVoidExpression(node: ts.Node): node is ts.VoidExpression { + return node.kind === ts.SyntaxKind.VoidExpression; +} - export function isNonNullExpression(node: ts.Node): node is ts.NonNullExpression { - return node.kind === ts.SyntaxKind.NonNullExpression; - } +export function isAwaitExpression(node: ts.Node): node is ts.AwaitExpression { + return node.kind === ts.SyntaxKind.AwaitExpression; +} - export function isMetaProperty(node: ts.Node): node is ts.MetaProperty { - return node.kind === ts.SyntaxKind.MetaProperty; - } +export function isPrefixUnaryExpression(node: ts.Node): node is ts.PrefixUnaryExpression { + return node.kind === ts.SyntaxKind.PrefixUnaryExpression; +} - export function isSyntheticExpression(node: ts.Node): node is ts.SyntheticExpression { - return node.kind === ts.SyntaxKind.SyntheticExpression; - } +export function isPostfixUnaryExpression(node: ts.Node): node is ts.PostfixUnaryExpression { + return node.kind === ts.SyntaxKind.PostfixUnaryExpression; +} - export function isPartiallyEmittedExpression(node: ts.Node): node is ts.PartiallyEmittedExpression { - return node.kind === ts.SyntaxKind.PartiallyEmittedExpression; - } +export function isBinaryExpression(node: ts.Node): node is ts.BinaryExpression { + return node.kind === ts.SyntaxKind.BinaryExpression; +} - export function isCommaListExpression(node: ts.Node): node is ts.CommaListExpression { - return node.kind === ts.SyntaxKind.CommaListExpression; - } +export function isConditionalExpression(node: ts.Node): node is ts.ConditionalExpression { + return node.kind === ts.SyntaxKind.ConditionalExpression; +} - // Misc +export function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { + return node.kind === ts.SyntaxKind.TemplateExpression; +} - export function isTemplateSpan(node: ts.Node): node is ts.TemplateSpan { - return node.kind === ts.SyntaxKind.TemplateSpan; - } +export function isYieldExpression(node: ts.Node): node is ts.YieldExpression { + return node.kind === ts.SyntaxKind.YieldExpression; +} - export function isSemicolonClassElement(node: ts.Node): node is ts.SemicolonClassElement { - return node.kind === ts.SyntaxKind.SemicolonClassElement; - } +export function isSpreadElement(node: ts.Node): node is ts.SpreadElement { + return node.kind === ts.SyntaxKind.SpreadElement; +} - // Elements +export function isClassExpression(node: ts.Node): node is ts.ClassExpression { + return node.kind === ts.SyntaxKind.ClassExpression; +} - export function isBlock(node: ts.Node): node is ts.Block { - return node.kind === ts.SyntaxKind.Block; - } +export function isOmittedExpression(node: ts.Node): node is ts.OmittedExpression { + return node.kind === ts.SyntaxKind.OmittedExpression; +} - export function isVariableStatement(node: ts.Node): node is ts.VariableStatement { - return node.kind === ts.SyntaxKind.VariableStatement; - } +export function isExpressionWithTypeArguments(node: ts.Node): node is ts.ExpressionWithTypeArguments { + return node.kind === ts.SyntaxKind.ExpressionWithTypeArguments; +} - export function isEmptyStatement(node: ts.Node): node is ts.EmptyStatement { - return node.kind === ts.SyntaxKind.EmptyStatement; - } +export function isAsExpression(node: ts.Node): node is ts.AsExpression { + return node.kind === ts.SyntaxKind.AsExpression; +} - export function isExpressionStatement(node: ts.Node): node is ts.ExpressionStatement { - return node.kind === ts.SyntaxKind.ExpressionStatement; - } +export function isNonNullExpression(node: ts.Node): node is ts.NonNullExpression { + return node.kind === ts.SyntaxKind.NonNullExpression; +} - export function isIfStatement(node: ts.Node): node is ts.IfStatement { - return node.kind === ts.SyntaxKind.IfStatement; - } +export function isMetaProperty(node: ts.Node): node is ts.MetaProperty { + return node.kind === ts.SyntaxKind.MetaProperty; +} - export function isDoStatement(node: ts.Node): node is ts.DoStatement { - return node.kind === ts.SyntaxKind.DoStatement; - } +export function isSyntheticExpression(node: ts.Node): node is ts.SyntheticExpression { + return node.kind === ts.SyntaxKind.SyntheticExpression; +} - export function isWhileStatement(node: ts.Node): node is ts.WhileStatement { - return node.kind === ts.SyntaxKind.WhileStatement; - } +export function isPartiallyEmittedExpression(node: ts.Node): node is ts.PartiallyEmittedExpression { + return node.kind === ts.SyntaxKind.PartiallyEmittedExpression; +} - export function isForStatement(node: ts.Node): node is ts.ForStatement { - return node.kind === ts.SyntaxKind.ForStatement; - } +export function isCommaListExpression(node: ts.Node): node is ts.CommaListExpression { + return node.kind === ts.SyntaxKind.CommaListExpression; +} - export function isForInStatement(node: ts.Node): node is ts.ForInStatement { - return node.kind === ts.SyntaxKind.ForInStatement; - } +// Misc - export function isForOfStatement(node: ts.Node): node is ts.ForOfStatement { - return node.kind === ts.SyntaxKind.ForOfStatement; - } +export function isTemplateSpan(node: ts.Node): node is ts.TemplateSpan { + return node.kind === ts.SyntaxKind.TemplateSpan; +} - export function isContinueStatement(node: ts.Node): node is ts.ContinueStatement { - return node.kind === ts.SyntaxKind.ContinueStatement; - } +export function isSemicolonClassElement(node: ts.Node): node is ts.SemicolonClassElement { + return node.kind === ts.SyntaxKind.SemicolonClassElement; +} - export function isBreakStatement(node: ts.Node): node is ts.BreakStatement { - return node.kind === ts.SyntaxKind.BreakStatement; - } +// Elements - export function isReturnStatement(node: ts.Node): node is ts.ReturnStatement { - return node.kind === ts.SyntaxKind.ReturnStatement; - } +export function isBlock(node: ts.Node): node is ts.Block { + return node.kind === ts.SyntaxKind.Block; +} - export function isWithStatement(node: ts.Node): node is ts.WithStatement { - return node.kind === ts.SyntaxKind.WithStatement; - } +export function isVariableStatement(node: ts.Node): node is ts.VariableStatement { + return node.kind === ts.SyntaxKind.VariableStatement; +} - export function isSwitchStatement(node: ts.Node): node is ts.SwitchStatement { - return node.kind === ts.SyntaxKind.SwitchStatement; - } +export function isEmptyStatement(node: ts.Node): node is ts.EmptyStatement { + return node.kind === ts.SyntaxKind.EmptyStatement; +} - export function isLabeledStatement(node: ts.Node): node is ts.LabeledStatement { - return node.kind === ts.SyntaxKind.LabeledStatement; - } +export function isExpressionStatement(node: ts.Node): node is ts.ExpressionStatement { + return node.kind === ts.SyntaxKind.ExpressionStatement; +} - export function isThrowStatement(node: ts.Node): node is ts.ThrowStatement { - return node.kind === ts.SyntaxKind.ThrowStatement; - } +export function isIfStatement(node: ts.Node): node is ts.IfStatement { + return node.kind === ts.SyntaxKind.IfStatement; +} - export function isTryStatement(node: ts.Node): node is ts.TryStatement { - return node.kind === ts.SyntaxKind.TryStatement; - } +export function isDoStatement(node: ts.Node): node is ts.DoStatement { + return node.kind === ts.SyntaxKind.DoStatement; +} - export function isDebuggerStatement(node: ts.Node): node is ts.DebuggerStatement { - return node.kind === ts.SyntaxKind.DebuggerStatement; - } +export function isWhileStatement(node: ts.Node): node is ts.WhileStatement { + return node.kind === ts.SyntaxKind.WhileStatement; +} - export function isVariableDeclaration(node: ts.Node): node is ts.VariableDeclaration { - return node.kind === ts.SyntaxKind.VariableDeclaration; - } +export function isForStatement(node: ts.Node): node is ts.ForStatement { + return node.kind === ts.SyntaxKind.ForStatement; +} - export function isVariableDeclarationList(node: ts.Node): node is ts.VariableDeclarationList { - return node.kind === ts.SyntaxKind.VariableDeclarationList; - } +export function isForInStatement(node: ts.Node): node is ts.ForInStatement { + return node.kind === ts.SyntaxKind.ForInStatement; +} - export function isFunctionDeclaration(node: ts.Node): node is ts.FunctionDeclaration { - return node.kind === ts.SyntaxKind.FunctionDeclaration; - } +export function isForOfStatement(node: ts.Node): node is ts.ForOfStatement { + return node.kind === ts.SyntaxKind.ForOfStatement; +} - export function isClassDeclaration(node: ts.Node): node is ts.ClassDeclaration { - return node.kind === ts.SyntaxKind.ClassDeclaration; - } +export function isContinueStatement(node: ts.Node): node is ts.ContinueStatement { + return node.kind === ts.SyntaxKind.ContinueStatement; +} - export function isInterfaceDeclaration(node: ts.Node): node is ts.InterfaceDeclaration { - return node.kind === ts.SyntaxKind.InterfaceDeclaration; - } +export function isBreakStatement(node: ts.Node): node is ts.BreakStatement { + return node.kind === ts.SyntaxKind.BreakStatement; +} - export function isTypeAliasDeclaration(node: ts.Node): node is ts.TypeAliasDeclaration { - return node.kind === ts.SyntaxKind.TypeAliasDeclaration; - } +export function isReturnStatement(node: ts.Node): node is ts.ReturnStatement { + return node.kind === ts.SyntaxKind.ReturnStatement; +} - export function isEnumDeclaration(node: ts.Node): node is ts.EnumDeclaration { - return node.kind === ts.SyntaxKind.EnumDeclaration; - } +export function isWithStatement(node: ts.Node): node is ts.WithStatement { + return node.kind === ts.SyntaxKind.WithStatement; +} - export function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration { - return node.kind === ts.SyntaxKind.ModuleDeclaration; - } +export function isSwitchStatement(node: ts.Node): node is ts.SwitchStatement { + return node.kind === ts.SyntaxKind.SwitchStatement; +} - export function isModuleBlock(node: ts.Node): node is ts.ModuleBlock { - return node.kind === ts.SyntaxKind.ModuleBlock; - } +export function isLabeledStatement(node: ts.Node): node is ts.LabeledStatement { + return node.kind === ts.SyntaxKind.LabeledStatement; +} - export function isCaseBlock(node: ts.Node): node is ts.CaseBlock { - return node.kind === ts.SyntaxKind.CaseBlock; - } +export function isThrowStatement(node: ts.Node): node is ts.ThrowStatement { + return node.kind === ts.SyntaxKind.ThrowStatement; +} - export function isNamespaceExportDeclaration(node: ts.Node): node is ts.NamespaceExportDeclaration { - return node.kind === ts.SyntaxKind.NamespaceExportDeclaration; - } +export function isTryStatement(node: ts.Node): node is ts.TryStatement { + return node.kind === ts.SyntaxKind.TryStatement; +} - export function isImportEqualsDeclaration(node: ts.Node): node is ts.ImportEqualsDeclaration { - return node.kind === ts.SyntaxKind.ImportEqualsDeclaration; - } +export function isDebuggerStatement(node: ts.Node): node is ts.DebuggerStatement { + return node.kind === ts.SyntaxKind.DebuggerStatement; +} - export function isImportDeclaration(node: ts.Node): node is ts.ImportDeclaration { - return node.kind === ts.SyntaxKind.ImportDeclaration; - } +export function isVariableDeclaration(node: ts.Node): node is ts.VariableDeclaration { + return node.kind === ts.SyntaxKind.VariableDeclaration; +} - export function isImportClause(node: ts.Node): node is ts.ImportClause { - return node.kind === ts.SyntaxKind.ImportClause; - } +export function isVariableDeclarationList(node: ts.Node): node is ts.VariableDeclarationList { + return node.kind === ts.SyntaxKind.VariableDeclarationList; +} - export function isAssertClause(node: ts.Node): node is ts.AssertClause { - return node.kind === ts.SyntaxKind.AssertClause; - } +export function isFunctionDeclaration(node: ts.Node): node is ts.FunctionDeclaration { + return node.kind === ts.SyntaxKind.FunctionDeclaration; +} - export function isAssertEntry(node: ts.Node): node is ts.AssertEntry { - return node.kind === ts.SyntaxKind.AssertEntry; - } +export function isClassDeclaration(node: ts.Node): node is ts.ClassDeclaration { + return node.kind === ts.SyntaxKind.ClassDeclaration; +} - export function isNamespaceImport(node: ts.Node): node is ts.NamespaceImport { - return node.kind === ts.SyntaxKind.NamespaceImport; - } +export function isInterfaceDeclaration(node: ts.Node): node is ts.InterfaceDeclaration { + return node.kind === ts.SyntaxKind.InterfaceDeclaration; +} - export function isNamespaceExport(node: ts.Node): node is ts.NamespaceExport { - return node.kind === ts.SyntaxKind.NamespaceExport; - } +export function isTypeAliasDeclaration(node: ts.Node): node is ts.TypeAliasDeclaration { + return node.kind === ts.SyntaxKind.TypeAliasDeclaration; +} - export function isNamedImports(node: ts.Node): node is ts.NamedImports { - return node.kind === ts.SyntaxKind.NamedImports; - } +export function isEnumDeclaration(node: ts.Node): node is ts.EnumDeclaration { + return node.kind === ts.SyntaxKind.EnumDeclaration; +} - export function isImportSpecifier(node: ts.Node): node is ts.ImportSpecifier { - return node.kind === ts.SyntaxKind.ImportSpecifier; - } +export function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration { + return node.kind === ts.SyntaxKind.ModuleDeclaration; +} - export function isExportAssignment(node: ts.Node): node is ts.ExportAssignment { - return node.kind === ts.SyntaxKind.ExportAssignment; - } +export function isModuleBlock(node: ts.Node): node is ts.ModuleBlock { + return node.kind === ts.SyntaxKind.ModuleBlock; +} - export function isExportDeclaration(node: ts.Node): node is ts.ExportDeclaration { - return node.kind === ts.SyntaxKind.ExportDeclaration; - } +export function isCaseBlock(node: ts.Node): node is ts.CaseBlock { + return node.kind === ts.SyntaxKind.CaseBlock; +} - export function isNamedExports(node: ts.Node): node is ts.NamedExports { - return node.kind === ts.SyntaxKind.NamedExports; - } +export function isNamespaceExportDeclaration(node: ts.Node): node is ts.NamespaceExportDeclaration { + return node.kind === ts.SyntaxKind.NamespaceExportDeclaration; +} - export function isExportSpecifier(node: ts.Node): node is ts.ExportSpecifier { - return node.kind === ts.SyntaxKind.ExportSpecifier; - } +export function isImportEqualsDeclaration(node: ts.Node): node is ts.ImportEqualsDeclaration { + return node.kind === ts.SyntaxKind.ImportEqualsDeclaration; +} - export function isMissingDeclaration(node: ts.Node): node is ts.MissingDeclaration { - return node.kind === ts.SyntaxKind.MissingDeclaration; - } +export function isImportDeclaration(node: ts.Node): node is ts.ImportDeclaration { + return node.kind === ts.SyntaxKind.ImportDeclaration; +} - export function isNotEmittedStatement(node: ts.Node): node is ts.NotEmittedStatement { - return node.kind === ts.SyntaxKind.NotEmittedStatement; - } +export function isImportClause(node: ts.Node): node is ts.ImportClause { + return node.kind === ts.SyntaxKind.ImportClause; +} - /* @internal */ - export function isSyntheticReference(node: ts.Node): node is ts.SyntheticReferenceExpression { - return node.kind === ts.SyntaxKind.SyntheticReferenceExpression; - } +export function isAssertClause(node: ts.Node): node is ts.AssertClause { + return node.kind === ts.SyntaxKind.AssertClause; +} - /* @internal */ - export function isMergeDeclarationMarker(node: ts.Node): node is ts.MergeDeclarationMarker { - return node.kind === ts.SyntaxKind.MergeDeclarationMarker; - } +export function isAssertEntry(node: ts.Node): node is ts.AssertEntry { + return node.kind === ts.SyntaxKind.AssertEntry; +} - /* @internal */ - export function isEndOfDeclarationMarker(node: ts.Node): node is ts.EndOfDeclarationMarker { - return node.kind === ts.SyntaxKind.EndOfDeclarationMarker; - } +export function isNamespaceImport(node: ts.Node): node is ts.NamespaceImport { + return node.kind === ts.SyntaxKind.NamespaceImport; +} - // Module References +export function isNamespaceExport(node: ts.Node): node is ts.NamespaceExport { + return node.kind === ts.SyntaxKind.NamespaceExport; +} - export function isExternalModuleReference(node: ts.Node): node is ts.ExternalModuleReference { - return node.kind === ts.SyntaxKind.ExternalModuleReference; - } +export function isNamedImports(node: ts.Node): node is ts.NamedImports { + return node.kind === ts.SyntaxKind.NamedImports; +} - // JSX +export function isImportSpecifier(node: ts.Node): node is ts.ImportSpecifier { + return node.kind === ts.SyntaxKind.ImportSpecifier; +} + +export function isExportAssignment(node: ts.Node): node is ts.ExportAssignment { + return node.kind === ts.SyntaxKind.ExportAssignment; +} + +export function isExportDeclaration(node: ts.Node): node is ts.ExportDeclaration { + return node.kind === ts.SyntaxKind.ExportDeclaration; +} + +export function isNamedExports(node: ts.Node): node is ts.NamedExports { + return node.kind === ts.SyntaxKind.NamedExports; +} + +export function isExportSpecifier(node: ts.Node): node is ts.ExportSpecifier { + return node.kind === ts.SyntaxKind.ExportSpecifier; +} + +export function isMissingDeclaration(node: ts.Node): node is ts.MissingDeclaration { + return node.kind === ts.SyntaxKind.MissingDeclaration; +} + +export function isNotEmittedStatement(node: ts.Node): node is ts.NotEmittedStatement { + return node.kind === ts.SyntaxKind.NotEmittedStatement; +} + +/* @internal */ +export function isSyntheticReference(node: ts.Node): node is ts.SyntheticReferenceExpression { + return node.kind === ts.SyntaxKind.SyntheticReferenceExpression; +} + +/* @internal */ +export function isMergeDeclarationMarker(node: ts.Node): node is ts.MergeDeclarationMarker { + return node.kind === ts.SyntaxKind.MergeDeclarationMarker; +} + +/* @internal */ +export function isEndOfDeclarationMarker(node: ts.Node): node is ts.EndOfDeclarationMarker { + return node.kind === ts.SyntaxKind.EndOfDeclarationMarker; +} + +// Module References + +export function isExternalModuleReference(node: ts.Node): node is ts.ExternalModuleReference { + return node.kind === ts.SyntaxKind.ExternalModuleReference; +} - export function isJsxElement(node: ts.Node): node is ts.JsxElement { - return node.kind === ts.SyntaxKind.JsxElement; - } +// JSX - export function isJsxSelfClosingElement(node: ts.Node): node is ts.JsxSelfClosingElement { - return node.kind === ts.SyntaxKind.JsxSelfClosingElement; - } +export function isJsxElement(node: ts.Node): node is ts.JsxElement { + return node.kind === ts.SyntaxKind.JsxElement; +} - export function isJsxOpeningElement(node: ts.Node): node is ts.JsxOpeningElement { - return node.kind === ts.SyntaxKind.JsxOpeningElement; - } +export function isJsxSelfClosingElement(node: ts.Node): node is ts.JsxSelfClosingElement { + return node.kind === ts.SyntaxKind.JsxSelfClosingElement; +} - export function isJsxClosingElement(node: ts.Node): node is ts.JsxClosingElement { - return node.kind === ts.SyntaxKind.JsxClosingElement; - } +export function isJsxOpeningElement(node: ts.Node): node is ts.JsxOpeningElement { + return node.kind === ts.SyntaxKind.JsxOpeningElement; +} - export function isJsxFragment(node: ts.Node): node is ts.JsxFragment { - return node.kind === ts.SyntaxKind.JsxFragment; - } +export function isJsxClosingElement(node: ts.Node): node is ts.JsxClosingElement { + return node.kind === ts.SyntaxKind.JsxClosingElement; +} + +export function isJsxFragment(node: ts.Node): node is ts.JsxFragment { + return node.kind === ts.SyntaxKind.JsxFragment; +} - export function isJsxOpeningFragment(node: ts.Node): node is ts.JsxOpeningFragment { - return node.kind === ts.SyntaxKind.JsxOpeningFragment; - } +export function isJsxOpeningFragment(node: ts.Node): node is ts.JsxOpeningFragment { + return node.kind === ts.SyntaxKind.JsxOpeningFragment; +} - export function isJsxClosingFragment(node: ts.Node): node is ts.JsxClosingFragment { - return node.kind === ts.SyntaxKind.JsxClosingFragment; - } +export function isJsxClosingFragment(node: ts.Node): node is ts.JsxClosingFragment { + return node.kind === ts.SyntaxKind.JsxClosingFragment; +} - export function isJsxAttribute(node: ts.Node): node is ts.JsxAttribute { - return node.kind === ts.SyntaxKind.JsxAttribute; - } +export function isJsxAttribute(node: ts.Node): node is ts.JsxAttribute { + return node.kind === ts.SyntaxKind.JsxAttribute; +} - export function isJsxAttributes(node: ts.Node): node is ts.JsxAttributes { - return node.kind === ts.SyntaxKind.JsxAttributes; - } +export function isJsxAttributes(node: ts.Node): node is ts.JsxAttributes { + return node.kind === ts.SyntaxKind.JsxAttributes; +} - export function isJsxSpreadAttribute(node: ts.Node): node is ts.JsxSpreadAttribute { - return node.kind === ts.SyntaxKind.JsxSpreadAttribute; - } +export function isJsxSpreadAttribute(node: ts.Node): node is ts.JsxSpreadAttribute { + return node.kind === ts.SyntaxKind.JsxSpreadAttribute; +} - export function isJsxExpression(node: ts.Node): node is ts.JsxExpression { - return node.kind === ts.SyntaxKind.JsxExpression; - } +export function isJsxExpression(node: ts.Node): node is ts.JsxExpression { + return node.kind === ts.SyntaxKind.JsxExpression; +} - // Clauses +// Clauses - export function isCaseClause(node: ts.Node): node is ts.CaseClause { - return node.kind === ts.SyntaxKind.CaseClause; - } +export function isCaseClause(node: ts.Node): node is ts.CaseClause { + return node.kind === ts.SyntaxKind.CaseClause; +} - export function isDefaultClause(node: ts.Node): node is ts.DefaultClause { - return node.kind === ts.SyntaxKind.DefaultClause; - } +export function isDefaultClause(node: ts.Node): node is ts.DefaultClause { + return node.kind === ts.SyntaxKind.DefaultClause; +} - export function isHeritageClause(node: ts.Node): node is ts.HeritageClause { - return node.kind === ts.SyntaxKind.HeritageClause; - } +export function isHeritageClause(node: ts.Node): node is ts.HeritageClause { + return node.kind === ts.SyntaxKind.HeritageClause; +} - export function isCatchClause(node: ts.Node): node is ts.CatchClause { - return node.kind === ts.SyntaxKind.CatchClause; - } +export function isCatchClause(node: ts.Node): node is ts.CatchClause { + return node.kind === ts.SyntaxKind.CatchClause; +} - // Property assignments +// Property assignments - export function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { - return node.kind === ts.SyntaxKind.PropertyAssignment; - } +export function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { + return node.kind === ts.SyntaxKind.PropertyAssignment; +} - export function isShorthandPropertyAssignment(node: ts.Node): node is ts.ShorthandPropertyAssignment { - return node.kind === ts.SyntaxKind.ShorthandPropertyAssignment; - } +export function isShorthandPropertyAssignment(node: ts.Node): node is ts.ShorthandPropertyAssignment { + return node.kind === ts.SyntaxKind.ShorthandPropertyAssignment; +} - export function isSpreadAssignment(node: ts.Node): node is ts.SpreadAssignment { - return node.kind === ts.SyntaxKind.SpreadAssignment; - } +export function isSpreadAssignment(node: ts.Node): node is ts.SpreadAssignment { + return node.kind === ts.SyntaxKind.SpreadAssignment; +} - // Enum +// Enum - export function isEnumMember(node: ts.Node): node is ts.EnumMember { - return node.kind === ts.SyntaxKind.EnumMember; - } +export function isEnumMember(node: ts.Node): node is ts.EnumMember { + return node.kind === ts.SyntaxKind.EnumMember; +} - // Unparsed +// Unparsed - // TODO(rbuckton): isUnparsedPrologue +// TODO(rbuckton): isUnparsedPrologue - export function isUnparsedPrepend(node: ts.Node): node is ts.UnparsedPrepend { - return node.kind === ts.SyntaxKind.UnparsedPrepend; - } +export function isUnparsedPrepend(node: ts.Node): node is ts.UnparsedPrepend { + return node.kind === ts.SyntaxKind.UnparsedPrepend; +} - // TODO(rbuckton): isUnparsedText - // TODO(rbuckton): isUnparsedInternalText - // TODO(rbuckton): isUnparsedSyntheticReference +// TODO(rbuckton): isUnparsedText +// TODO(rbuckton): isUnparsedInternalText +// TODO(rbuckton): isUnparsedSyntheticReference - // Top-level nodes - export function isSourceFile(node: ts.Node): node is ts.SourceFile { - return node.kind === ts.SyntaxKind.SourceFile; - } +// Top-level nodes +export function isSourceFile(node: ts.Node): node is ts.SourceFile { + return node.kind === ts.SyntaxKind.SourceFile; +} - export function isBundle(node: ts.Node): node is ts.Bundle { - return node.kind === ts.SyntaxKind.Bundle; - } +export function isBundle(node: ts.Node): node is ts.Bundle { + return node.kind === ts.SyntaxKind.Bundle; +} - export function isUnparsedSource(node: ts.Node): node is ts.UnparsedSource { - return node.kind === ts.SyntaxKind.UnparsedSource; - } +export function isUnparsedSource(node: ts.Node): node is ts.UnparsedSource { + return node.kind === ts.SyntaxKind.UnparsedSource; +} - // TODO(rbuckton): isInputFiles +// TODO(rbuckton): isInputFiles - // JSDoc Elements +// JSDoc Elements - export function isJSDocTypeExpression(node: ts.Node): node is ts.JSDocTypeExpression { - return node.kind === ts.SyntaxKind.JSDocTypeExpression; - } +export function isJSDocTypeExpression(node: ts.Node): node is ts.JSDocTypeExpression { + return node.kind === ts.SyntaxKind.JSDocTypeExpression; +} - export function isJSDocNameReference(node: ts.Node): node is ts.JSDocNameReference { - return node.kind === ts.SyntaxKind.JSDocNameReference; - } +export function isJSDocNameReference(node: ts.Node): node is ts.JSDocNameReference { + return node.kind === ts.SyntaxKind.JSDocNameReference; +} - export function isJSDocMemberName(node: ts.Node): node is ts.JSDocMemberName { - return node.kind === ts.SyntaxKind.JSDocMemberName; - } +export function isJSDocMemberName(node: ts.Node): node is ts.JSDocMemberName { + return node.kind === ts.SyntaxKind.JSDocMemberName; +} - export function isJSDocLink(node: ts.Node): node is ts.JSDocLink { - return node.kind === ts.SyntaxKind.JSDocLink; - } +export function isJSDocLink(node: ts.Node): node is ts.JSDocLink { + return node.kind === ts.SyntaxKind.JSDocLink; +} - export function isJSDocLinkCode(node: ts.Node): node is ts.JSDocLinkCode { - return node.kind === ts.SyntaxKind.JSDocLinkCode; - } +export function isJSDocLinkCode(node: ts.Node): node is ts.JSDocLinkCode { + return node.kind === ts.SyntaxKind.JSDocLinkCode; +} - export function isJSDocLinkPlain(node: ts.Node): node is ts.JSDocLinkPlain { - return node.kind === ts.SyntaxKind.JSDocLinkPlain; - } +export function isJSDocLinkPlain(node: ts.Node): node is ts.JSDocLinkPlain { + return node.kind === ts.SyntaxKind.JSDocLinkPlain; +} - export function isJSDocAllType(node: ts.Node): node is ts.JSDocAllType { - return node.kind === ts.SyntaxKind.JSDocAllType; - } +export function isJSDocAllType(node: ts.Node): node is ts.JSDocAllType { + return node.kind === ts.SyntaxKind.JSDocAllType; +} - export function isJSDocUnknownType(node: ts.Node): node is ts.JSDocUnknownType { - return node.kind === ts.SyntaxKind.JSDocUnknownType; - } +export function isJSDocUnknownType(node: ts.Node): node is ts.JSDocUnknownType { + return node.kind === ts.SyntaxKind.JSDocUnknownType; +} - export function isJSDocNullableType(node: ts.Node): node is ts.JSDocNullableType { - return node.kind === ts.SyntaxKind.JSDocNullableType; - } +export function isJSDocNullableType(node: ts.Node): node is ts.JSDocNullableType { + return node.kind === ts.SyntaxKind.JSDocNullableType; +} - export function isJSDocNonNullableType(node: ts.Node): node is ts.JSDocNonNullableType { - return node.kind === ts.SyntaxKind.JSDocNonNullableType; - } +export function isJSDocNonNullableType(node: ts.Node): node is ts.JSDocNonNullableType { + return node.kind === ts.SyntaxKind.JSDocNonNullableType; +} - export function isJSDocOptionalType(node: ts.Node): node is ts.JSDocOptionalType { - return node.kind === ts.SyntaxKind.JSDocOptionalType; - } +export function isJSDocOptionalType(node: ts.Node): node is ts.JSDocOptionalType { + return node.kind === ts.SyntaxKind.JSDocOptionalType; +} - export function isJSDocFunctionType(node: ts.Node): node is ts.JSDocFunctionType { - return node.kind === ts.SyntaxKind.JSDocFunctionType; - } +export function isJSDocFunctionType(node: ts.Node): node is ts.JSDocFunctionType { + return node.kind === ts.SyntaxKind.JSDocFunctionType; +} - export function isJSDocVariadicType(node: ts.Node): node is ts.JSDocVariadicType { - return node.kind === ts.SyntaxKind.JSDocVariadicType; - } +export function isJSDocVariadicType(node: ts.Node): node is ts.JSDocVariadicType { + return node.kind === ts.SyntaxKind.JSDocVariadicType; +} - export function isJSDocNamepathType(node: ts.Node): node is ts.JSDocNamepathType { - return node.kind === ts.SyntaxKind.JSDocNamepathType; - } +export function isJSDocNamepathType(node: ts.Node): node is ts.JSDocNamepathType { + return node.kind === ts.SyntaxKind.JSDocNamepathType; +} - export function isJSDoc(node: ts.Node): node is ts.JSDoc { - return node.kind === ts.SyntaxKind.JSDoc; - } +export function isJSDoc(node: ts.Node): node is ts.JSDoc { + return node.kind === ts.SyntaxKind.JSDoc; +} - export function isJSDocTypeLiteral(node: ts.Node): node is ts.JSDocTypeLiteral { - return node.kind === ts.SyntaxKind.JSDocTypeLiteral; - } +export function isJSDocTypeLiteral(node: ts.Node): node is ts.JSDocTypeLiteral { + return node.kind === ts.SyntaxKind.JSDocTypeLiteral; +} - export function isJSDocSignature(node: ts.Node): node is ts.JSDocSignature { - return node.kind === ts.SyntaxKind.JSDocSignature; - } +export function isJSDocSignature(node: ts.Node): node is ts.JSDocSignature { + return node.kind === ts.SyntaxKind.JSDocSignature; +} - // JSDoc Tags +// JSDoc Tags - export function isJSDocAugmentsTag(node: ts.Node): node is ts.JSDocAugmentsTag { - return node.kind === ts.SyntaxKind.JSDocAugmentsTag; - } +export function isJSDocAugmentsTag(node: ts.Node): node is ts.JSDocAugmentsTag { + return node.kind === ts.SyntaxKind.JSDocAugmentsTag; +} - export function isJSDocAuthorTag(node: ts.Node): node is ts.JSDocAuthorTag { - return node.kind === ts.SyntaxKind.JSDocAuthorTag; - } +export function isJSDocAuthorTag(node: ts.Node): node is ts.JSDocAuthorTag { + return node.kind === ts.SyntaxKind.JSDocAuthorTag; +} - export function isJSDocClassTag(node: ts.Node): node is ts.JSDocClassTag { - return node.kind === ts.SyntaxKind.JSDocClassTag; - } +export function isJSDocClassTag(node: ts.Node): node is ts.JSDocClassTag { + return node.kind === ts.SyntaxKind.JSDocClassTag; +} - export function isJSDocCallbackTag(node: ts.Node): node is ts.JSDocCallbackTag { - return node.kind === ts.SyntaxKind.JSDocCallbackTag; - } +export function isJSDocCallbackTag(node: ts.Node): node is ts.JSDocCallbackTag { + return node.kind === ts.SyntaxKind.JSDocCallbackTag; +} - export function isJSDocPublicTag(node: ts.Node): node is ts.JSDocPublicTag { - return node.kind === ts.SyntaxKind.JSDocPublicTag; - } +export function isJSDocPublicTag(node: ts.Node): node is ts.JSDocPublicTag { + return node.kind === ts.SyntaxKind.JSDocPublicTag; +} - export function isJSDocPrivateTag(node: ts.Node): node is ts.JSDocPrivateTag { - return node.kind === ts.SyntaxKind.JSDocPrivateTag; - } +export function isJSDocPrivateTag(node: ts.Node): node is ts.JSDocPrivateTag { + return node.kind === ts.SyntaxKind.JSDocPrivateTag; +} - export function isJSDocProtectedTag(node: ts.Node): node is ts.JSDocProtectedTag { - return node.kind === ts.SyntaxKind.JSDocProtectedTag; - } +export function isJSDocProtectedTag(node: ts.Node): node is ts.JSDocProtectedTag { + return node.kind === ts.SyntaxKind.JSDocProtectedTag; +} - export function isJSDocReadonlyTag(node: ts.Node): node is ts.JSDocReadonlyTag { - return node.kind === ts.SyntaxKind.JSDocReadonlyTag; - } +export function isJSDocReadonlyTag(node: ts.Node): node is ts.JSDocReadonlyTag { + return node.kind === ts.SyntaxKind.JSDocReadonlyTag; +} - export function isJSDocOverrideTag(node: ts.Node): node is ts.JSDocOverrideTag { - return node.kind === ts.SyntaxKind.JSDocOverrideTag; - } +export function isJSDocOverrideTag(node: ts.Node): node is ts.JSDocOverrideTag { + return node.kind === ts.SyntaxKind.JSDocOverrideTag; +} - export function isJSDocDeprecatedTag(node: ts.Node): node is ts.JSDocDeprecatedTag { - return node.kind === ts.SyntaxKind.JSDocDeprecatedTag; - } +export function isJSDocDeprecatedTag(node: ts.Node): node is ts.JSDocDeprecatedTag { + return node.kind === ts.SyntaxKind.JSDocDeprecatedTag; +} - export function isJSDocSeeTag(node: ts.Node): node is ts.JSDocSeeTag { - return node.kind === ts.SyntaxKind.JSDocSeeTag; - } +export function isJSDocSeeTag(node: ts.Node): node is ts.JSDocSeeTag { + return node.kind === ts.SyntaxKind.JSDocSeeTag; +} - export function isJSDocEnumTag(node: ts.Node): node is ts.JSDocEnumTag { - return node.kind === ts.SyntaxKind.JSDocEnumTag; - } +export function isJSDocEnumTag(node: ts.Node): node is ts.JSDocEnumTag { + return node.kind === ts.SyntaxKind.JSDocEnumTag; +} - export function isJSDocParameterTag(node: ts.Node): node is ts.JSDocParameterTag { - return node.kind === ts.SyntaxKind.JSDocParameterTag; - } +export function isJSDocParameterTag(node: ts.Node): node is ts.JSDocParameterTag { + return node.kind === ts.SyntaxKind.JSDocParameterTag; +} - export function isJSDocReturnTag(node: ts.Node): node is ts.JSDocReturnTag { - return node.kind === ts.SyntaxKind.JSDocReturnTag; - } +export function isJSDocReturnTag(node: ts.Node): node is ts.JSDocReturnTag { + return node.kind === ts.SyntaxKind.JSDocReturnTag; +} - export function isJSDocThisTag(node: ts.Node): node is ts.JSDocThisTag { - return node.kind === ts.SyntaxKind.JSDocThisTag; - } +export function isJSDocThisTag(node: ts.Node): node is ts.JSDocThisTag { + return node.kind === ts.SyntaxKind.JSDocThisTag; +} - export function isJSDocTypeTag(node: ts.Node): node is ts.JSDocTypeTag { - return node.kind === ts.SyntaxKind.JSDocTypeTag; - } +export function isJSDocTypeTag(node: ts.Node): node is ts.JSDocTypeTag { + return node.kind === ts.SyntaxKind.JSDocTypeTag; +} - export function isJSDocTemplateTag(node: ts.Node): node is ts.JSDocTemplateTag { - return node.kind === ts.SyntaxKind.JSDocTemplateTag; - } +export function isJSDocTemplateTag(node: ts.Node): node is ts.JSDocTemplateTag { + return node.kind === ts.SyntaxKind.JSDocTemplateTag; +} - export function isJSDocTypedefTag(node: ts.Node): node is ts.JSDocTypedefTag { - return node.kind === ts.SyntaxKind.JSDocTypedefTag; - } +export function isJSDocTypedefTag(node: ts.Node): node is ts.JSDocTypedefTag { + return node.kind === ts.SyntaxKind.JSDocTypedefTag; +} - export function isJSDocUnknownTag(node: ts.Node): node is ts.JSDocUnknownTag { - return node.kind === ts.SyntaxKind.JSDocTag; - } +export function isJSDocUnknownTag(node: ts.Node): node is ts.JSDocUnknownTag { + return node.kind === ts.SyntaxKind.JSDocTag; +} - export function isJSDocPropertyTag(node: ts.Node): node is ts.JSDocPropertyTag { - return node.kind === ts.SyntaxKind.JSDocPropertyTag; - } +export function isJSDocPropertyTag(node: ts.Node): node is ts.JSDocPropertyTag { + return node.kind === ts.SyntaxKind.JSDocPropertyTag; +} - export function isJSDocImplementsTag(node: ts.Node): node is ts.JSDocImplementsTag { - return node.kind === ts.SyntaxKind.JSDocImplementsTag; - } +export function isJSDocImplementsTag(node: ts.Node): node is ts.JSDocImplementsTag { + return node.kind === ts.SyntaxKind.JSDocImplementsTag; +} - // Synthesized list +// Synthesized list - /* @internal */ - export function isSyntaxList(n: ts.Node): n is ts.SyntaxList { - return n.kind === ts.SyntaxKind.SyntaxList; - } +/* @internal */ +export function isSyntaxList(n: ts.Node): n is ts.SyntaxList { + return n.kind === ts.SyntaxKind.SyntaxList; +} } diff --git a/src/compiler/factory/parenthesizerRules.ts b/src/compiler/factory/parenthesizerRules.ts index 8f04537161906..7b62d32a3bbf3 100644 --- a/src/compiler/factory/parenthesizerRules.ts +++ b/src/compiler/factory/parenthesizerRules.ts @@ -1,633 +1,633 @@ /* @internal */ namespace ts { - export function createParenthesizerRules(factory: ts.NodeFactory): ts.ParenthesizerRules { - interface BinaryPlusExpression extends ts.BinaryExpression { - cachedLiteralKind: ts.SyntaxKind; - } - let binaryLeftOperandParenthesizerCache: ts.ESMap ts.Expression> | undefined; - let binaryRightOperandParenthesizerCache: ts.ESMap ts.Expression> | undefined; - - return { - getParenthesizeLeftSideOfBinaryForOperator, - getParenthesizeRightSideOfBinaryForOperator, - parenthesizeLeftSideOfBinary, - parenthesizeRightSideOfBinary, - parenthesizeExpressionOfComputedPropertyName, - parenthesizeConditionOfConditionalExpression, - parenthesizeBranchOfConditionalExpression, - parenthesizeExpressionOfExportDefault, - parenthesizeExpressionOfNew, - parenthesizeLeftSideOfAccess, - parenthesizeOperandOfPostfixUnary, - parenthesizeOperandOfPrefixUnary, - parenthesizeExpressionsOfCommaDelimitedList, - parenthesizeExpressionForDisallowedComma, - parenthesizeExpressionOfExpressionStatement, - parenthesizeConciseBodyOfArrowFunction, - parenthesizeCheckTypeOfConditionalType, - parenthesizeExtendsTypeOfConditionalType, - parenthesizeConstituentTypesOfUnionType, - parenthesizeConstituentTypeOfUnionType, - parenthesizeConstituentTypesOfIntersectionType, - parenthesizeConstituentTypeOfIntersectionType, - parenthesizeOperandOfTypeOperator, - parenthesizeOperandOfReadonlyTypeOperator, - parenthesizeNonArrayTypeOfPostfixType, - parenthesizeElementTypesOfTupleType, - parenthesizeElementTypeOfTupleType, - parenthesizeTypeOfOptionalType, - parenthesizeTypeArguments, - parenthesizeLeadingTypeArgument, - }; - - function getParenthesizeLeftSideOfBinaryForOperator(operatorKind: ts.BinaryOperator) { - binaryLeftOperandParenthesizerCache ||= new ts.Map(); - let parenthesizerRule = binaryLeftOperandParenthesizerCache.get(operatorKind); - if (!parenthesizerRule) { - parenthesizerRule = node => parenthesizeLeftSideOfBinary(operatorKind, node); - binaryLeftOperandParenthesizerCache.set(operatorKind, parenthesizerRule); - } - return parenthesizerRule; +export function createParenthesizerRules(factory: ts.NodeFactory): ts.ParenthesizerRules { + interface BinaryPlusExpression extends ts.BinaryExpression { + cachedLiteralKind: ts.SyntaxKind; + } + let binaryLeftOperandParenthesizerCache: ts.ESMap ts.Expression> | undefined; + let binaryRightOperandParenthesizerCache: ts.ESMap ts.Expression> | undefined; + + return { + getParenthesizeLeftSideOfBinaryForOperator, + getParenthesizeRightSideOfBinaryForOperator, + parenthesizeLeftSideOfBinary, + parenthesizeRightSideOfBinary, + parenthesizeExpressionOfComputedPropertyName, + parenthesizeConditionOfConditionalExpression, + parenthesizeBranchOfConditionalExpression, + parenthesizeExpressionOfExportDefault, + parenthesizeExpressionOfNew, + parenthesizeLeftSideOfAccess, + parenthesizeOperandOfPostfixUnary, + parenthesizeOperandOfPrefixUnary, + parenthesizeExpressionsOfCommaDelimitedList, + parenthesizeExpressionForDisallowedComma, + parenthesizeExpressionOfExpressionStatement, + parenthesizeConciseBodyOfArrowFunction, + parenthesizeCheckTypeOfConditionalType, + parenthesizeExtendsTypeOfConditionalType, + parenthesizeConstituentTypesOfUnionType, + parenthesizeConstituentTypeOfUnionType, + parenthesizeConstituentTypesOfIntersectionType, + parenthesizeConstituentTypeOfIntersectionType, + parenthesizeOperandOfTypeOperator, + parenthesizeOperandOfReadonlyTypeOperator, + parenthesizeNonArrayTypeOfPostfixType, + parenthesizeElementTypesOfTupleType, + parenthesizeElementTypeOfTupleType, + parenthesizeTypeOfOptionalType, + parenthesizeTypeArguments, + parenthesizeLeadingTypeArgument, + }; + + function getParenthesizeLeftSideOfBinaryForOperator(operatorKind: ts.BinaryOperator) { + binaryLeftOperandParenthesizerCache ||= new ts.Map(); + let parenthesizerRule = binaryLeftOperandParenthesizerCache.get(operatorKind); + if (!parenthesizerRule) { + parenthesizerRule = node => parenthesizeLeftSideOfBinary(operatorKind, node); + binaryLeftOperandParenthesizerCache.set(operatorKind, parenthesizerRule); } + return parenthesizerRule; + } - function getParenthesizeRightSideOfBinaryForOperator(operatorKind: ts.BinaryOperator) { - binaryRightOperandParenthesizerCache ||= new ts.Map(); - let parenthesizerRule = binaryRightOperandParenthesizerCache.get(operatorKind); - if (!parenthesizerRule) { - parenthesizerRule = node => parenthesizeRightSideOfBinary(operatorKind, /*leftSide*/ undefined, node); - binaryRightOperandParenthesizerCache.set(operatorKind, parenthesizerRule); - } - return parenthesizerRule; + function getParenthesizeRightSideOfBinaryForOperator(operatorKind: ts.BinaryOperator) { + binaryRightOperandParenthesizerCache ||= new ts.Map(); + let parenthesizerRule = binaryRightOperandParenthesizerCache.get(operatorKind); + if (!parenthesizerRule) { + parenthesizerRule = node => parenthesizeRightSideOfBinary(operatorKind, /*leftSide*/ undefined, node); + binaryRightOperandParenthesizerCache.set(operatorKind, parenthesizerRule); } + return parenthesizerRule; + } - /** - * 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: ts.SyntaxKind, operand: ts.Expression, isLeftSideOfBinary: boolean, leftOperand: ts.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 = ts.getOperatorPrecedence(ts.SyntaxKind.BinaryExpression, binaryOperator); - const binaryOperatorAssociativity = ts.getOperatorAssociativity(ts.SyntaxKind.BinaryExpression, binaryOperator); - const emittedOperand = ts.skipPartiallyEmittedExpressions(operand); - if (!isLeftSideOfBinary && operand.kind === ts.SyntaxKind.ArrowFunction && binaryOperatorPrecedence > ts.OperatorPrecedence.Assignment) { - // We need to parenthesize arrow functions on the right side to avoid it being - // parsed as parenthesized expression: `a && (() => {})` - return true; - } - const operandPrecedence = ts.getExpressionPrecedence(emittedOperand); - switch (ts.compareValues(operandPrecedence, binaryOperatorPrecedence)) { - case ts.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 === ts.Associativity.Right - && operand.kind === ts.SyntaxKind.YieldExpression) { - return false; - } + /** + * 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: ts.SyntaxKind, operand: ts.Expression, isLeftSideOfBinary: boolean, leftOperand: ts.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 = ts.getOperatorPrecedence(ts.SyntaxKind.BinaryExpression, binaryOperator); + const binaryOperatorAssociativity = ts.getOperatorAssociativity(ts.SyntaxKind.BinaryExpression, binaryOperator); + const emittedOperand = ts.skipPartiallyEmittedExpressions(operand); + if (!isLeftSideOfBinary && operand.kind === ts.SyntaxKind.ArrowFunction && binaryOperatorPrecedence > ts.OperatorPrecedence.Assignment) { + // We need to parenthesize arrow functions on the right side to avoid it being + // parsed as parenthesized expression: `a && (() => {})` + return true; + } + const operandPrecedence = ts.getExpressionPrecedence(emittedOperand); + switch (ts.compareValues(operandPrecedence, binaryOperatorPrecedence)) { + case ts.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 === ts.Associativity.Right + && operand.kind === ts.SyntaxKind.YieldExpression) { + return false; + } - return true; + return true; - case ts.Comparison.GreaterThan: - return false; + case ts.Comparison.GreaterThan: + return false; + + case ts.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 === ts.Associativity.Right; + } + else { + if (ts.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; + } - case ts.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 === ts.Associativity.Right; - } - else { - if (ts.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)) { + // 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 === ts.SyntaxKind.PlusToken) { + const leftKind = leftOperand ? getLiteralKindOfBinaryPlusOperand(leftOperand) : ts.SyntaxKind.Unknown; + if (ts.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 === ts.SyntaxKind.PlusToken) { - const leftKind = leftOperand ? getLiteralKindOfBinaryPlusOperand(leftOperand) : ts.SyntaxKind.Unknown; - if (ts.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 = ts.getExpressionAssociativity(emittedOperand); - return operandAssociativity === ts.Associativity.Left; } - } - } - /** - * Determines whether a binary operator is mathematically associative. - * - * @param binaryOperator The binary operator. - */ - function operatorHasAssociativeProperty(binaryOperator: ts.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 === ts.SyntaxKind.AsteriskToken - || binaryOperator === ts.SyntaxKind.BarToken - || binaryOperator === ts.SyntaxKind.AmpersandToken - || binaryOperator === ts.SyntaxKind.CaretToken; - } - - /** - * 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: ts.Expression): ts.SyntaxKind { - node = ts.skipPartiallyEmittedExpressions(node); - if (ts.isLiteralKind(node.kind)) { - return node.kind; - } - - if (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.PlusToken) { - if ((node as BinaryPlusExpression).cachedLiteralKind !== undefined) { - return (node as BinaryPlusExpression).cachedLiteralKind; + // 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 = ts.getExpressionAssociativity(emittedOperand); + return operandAssociativity === ts.Associativity.Left; } + } + } - const leftKind = getLiteralKindOfBinaryPlusOperand((node as ts.BinaryExpression).left); - const literalKind = ts.isLiteralKind(leftKind) - && leftKind === getLiteralKindOfBinaryPlusOperand((node as ts.BinaryExpression).right) - ? leftKind - : ts.SyntaxKind.Unknown; + /** + * Determines whether a binary operator is mathematically associative. + * + * @param binaryOperator The binary operator. + */ + function operatorHasAssociativeProperty(binaryOperator: ts.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 === ts.SyntaxKind.AsteriskToken + || binaryOperator === ts.SyntaxKind.BarToken + || binaryOperator === ts.SyntaxKind.AmpersandToken + || binaryOperator === ts.SyntaxKind.CaretToken; + } - (node as BinaryPlusExpression).cachedLiteralKind = literalKind; - return literalKind; + /** + * 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: ts.Expression): ts.SyntaxKind { + node = ts.skipPartiallyEmittedExpressions(node); + if (ts.isLiteralKind(node.kind)) { + return node.kind; + } + + if (node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.PlusToken) { + if ((node as BinaryPlusExpression).cachedLiteralKind !== undefined) { + return (node as BinaryPlusExpression).cachedLiteralKind; } - return ts.SyntaxKind.Unknown; + const leftKind = getLiteralKindOfBinaryPlusOperand((node as ts.BinaryExpression).left); + const literalKind = ts.isLiteralKind(leftKind) + && leftKind === getLiteralKindOfBinaryPlusOperand((node as ts.BinaryExpression).right) + ? leftKind + : ts.SyntaxKind.Unknown; + + (node as BinaryPlusExpression).cachedLiteralKind = literalKind; + return literalKind; } - /** - * 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. - */ - function parenthesizeBinaryOperand(binaryOperator: ts.SyntaxKind, operand: ts.Expression, isLeftSideOfBinary: boolean, leftOperand?: ts.Expression) { - const skipped = ts.skipPartiallyEmittedExpressions(operand); - - // If the resulting expression is already parenthesized, we do not need to do any further processing. - if (skipped.kind === ts.SyntaxKind.ParenthesizedExpression) { - return operand; - } + return ts.SyntaxKind.Unknown; + } - return binaryOperandNeedsParentheses(binaryOperator, operand, isLeftSideOfBinary, leftOperand) - ? factory.createParenthesizedExpression(operand) - : operand; - } + /** + * 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. + */ + function parenthesizeBinaryOperand(binaryOperator: ts.SyntaxKind, operand: ts.Expression, isLeftSideOfBinary: boolean, leftOperand?: ts.Expression) { + const skipped = ts.skipPartiallyEmittedExpressions(operand); + + // If the resulting expression is already parenthesized, we do not need to do any further processing. + if (skipped.kind === ts.SyntaxKind.ParenthesizedExpression) { + return operand; + } + + return binaryOperandNeedsParentheses(binaryOperator, operand, isLeftSideOfBinary, leftOperand) + ? factory.createParenthesizedExpression(operand) + : operand; + } - function parenthesizeLeftSideOfBinary(binaryOperator: ts.SyntaxKind, leftSide: ts.Expression): ts.Expression { - return parenthesizeBinaryOperand(binaryOperator, leftSide, /*isLeftSideOfBinary*/ true); - } + function parenthesizeLeftSideOfBinary(binaryOperator: ts.SyntaxKind, leftSide: ts.Expression): ts.Expression { + return parenthesizeBinaryOperand(binaryOperator, leftSide, /*isLeftSideOfBinary*/ true); + } - function parenthesizeRightSideOfBinary(binaryOperator: ts.SyntaxKind, leftSide: ts.Expression | undefined, rightSide: ts.Expression): ts.Expression { - return parenthesizeBinaryOperand(binaryOperator, rightSide, /*isLeftSideOfBinary*/ false, leftSide); - } + function parenthesizeRightSideOfBinary(binaryOperator: ts.SyntaxKind, leftSide: ts.Expression | undefined, rightSide: ts.Expression): ts.Expression { + return parenthesizeBinaryOperand(binaryOperator, rightSide, /*isLeftSideOfBinary*/ false, leftSide); + } - function parenthesizeExpressionOfComputedPropertyName(expression: ts.Expression): ts.Expression { - return ts.isCommaSequence(expression) ? factory.createParenthesizedExpression(expression) : expression; - } + function parenthesizeExpressionOfComputedPropertyName(expression: ts.Expression): ts.Expression { + return ts.isCommaSequence(expression) ? factory.createParenthesizedExpression(expression) : expression; + } - function parenthesizeConditionOfConditionalExpression(condition: ts.Expression): ts.Expression { - const conditionalPrecedence = ts.getOperatorPrecedence(ts.SyntaxKind.ConditionalExpression, ts.SyntaxKind.QuestionToken); - const emittedCondition = ts.skipPartiallyEmittedExpressions(condition); - const conditionPrecedence = ts.getExpressionPrecedence(emittedCondition); - if (ts.compareValues(conditionPrecedence, conditionalPrecedence) !== ts.Comparison.GreaterThan) { - return factory.createParenthesizedExpression(condition); - } - return condition; + function parenthesizeConditionOfConditionalExpression(condition: ts.Expression): ts.Expression { + const conditionalPrecedence = ts.getOperatorPrecedence(ts.SyntaxKind.ConditionalExpression, ts.SyntaxKind.QuestionToken); + const emittedCondition = ts.skipPartiallyEmittedExpressions(condition); + const conditionPrecedence = ts.getExpressionPrecedence(emittedCondition); + if (ts.compareValues(conditionPrecedence, conditionalPrecedence) !== ts.Comparison.GreaterThan) { + return factory.createParenthesizedExpression(condition); } + return condition; + } - function parenthesizeBranchOfConditionalExpression(branch: ts.Expression): ts.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 = ts.skipPartiallyEmittedExpressions(branch); - return ts.isCommaSequence(emittedExpression) - ? factory.createParenthesizedExpression(branch) - : branch; - } + function parenthesizeBranchOfConditionalExpression(branch: ts.Expression): ts.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 = ts.skipPartiallyEmittedExpressions(branch); + return ts.isCommaSequence(emittedExpression) + ? factory.createParenthesizedExpression(branch) + : branch; + } - /** - * [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 - */ - function parenthesizeExpressionOfExportDefault(expression: ts.Expression): ts.Expression { - const check = ts.skipPartiallyEmittedExpressions(expression); - let needsParens = ts.isCommaSequence(check); - if (!needsParens) { - switch (ts.getLeftmostExpression(check, /*stopAtCallExpression*/ false).kind) { - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.FunctionExpression: - needsParens = true; - } + /** + * [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 + */ + function parenthesizeExpressionOfExportDefault(expression: ts.Expression): ts.Expression { + const check = ts.skipPartiallyEmittedExpressions(expression); + let needsParens = ts.isCommaSequence(check); + if (!needsParens) { + switch (ts.getLeftmostExpression(check, /*stopAtCallExpression*/ false).kind) { + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.FunctionExpression: + needsParens = true; } - return needsParens ? factory.createParenthesizedExpression(expression) : expression; } + return needsParens ? factory.createParenthesizedExpression(expression) : expression; + } - /** - * Wraps an expression in parentheses if it is needed in order to use the expression - * as the expression of a `NewExpression` node. - */ - function parenthesizeExpressionOfNew(expression: ts.Expression): ts.LeftHandSideExpression { - const leftmostExpr = ts.getLeftmostExpression(expression, /*stopAtCallExpressions*/ true); - switch (leftmostExpr.kind) { - case ts.SyntaxKind.CallExpression: - return factory.createParenthesizedExpression(expression); - - case ts.SyntaxKind.NewExpression: - return !(leftmostExpr as ts.NewExpression).arguments - ? factory.createParenthesizedExpression(expression) - : expression as ts.LeftHandSideExpression; // TODO(rbuckton): Verify this assertion holds - } + /** + * Wraps an expression in parentheses if it is needed in order to use the expression + * as the expression of a `NewExpression` node. + */ + function parenthesizeExpressionOfNew(expression: ts.Expression): ts.LeftHandSideExpression { + const leftmostExpr = ts.getLeftmostExpression(expression, /*stopAtCallExpressions*/ true); + switch (leftmostExpr.kind) { + case ts.SyntaxKind.CallExpression: + return factory.createParenthesizedExpression(expression); - return parenthesizeLeftSideOfAccess(expression); + case ts.SyntaxKind.NewExpression: + return !(leftmostExpr as ts.NewExpression).arguments + ? factory.createParenthesizedExpression(expression) + : expression as ts.LeftHandSideExpression; // TODO(rbuckton): Verify this assertion holds } - /** - * Wraps an expression in parentheses if it is needed in order to use the expression for - * property or element access. - */ - function parenthesizeLeftSideOfAccess(expression: ts.Expression): ts.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 = ts.skipPartiallyEmittedExpressions(expression); - if (ts.isLeftHandSideExpression(emittedExpression) - && (emittedExpression.kind !== ts.SyntaxKind.NewExpression || (emittedExpression as ts.NewExpression).arguments)) { - // TODO(rbuckton): Verify whether this assertion holds. - return expression as ts.LeftHandSideExpression; - } - - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return ts.setTextRange(factory.createParenthesizedExpression(expression), expression); - } + return parenthesizeLeftSideOfAccess(expression); + } - function parenthesizeOperandOfPostfixUnary(operand: ts.Expression): ts.LeftHandSideExpression { - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return ts.isLeftHandSideExpression(operand) ? operand : ts.setTextRange(factory.createParenthesizedExpression(operand), operand); + /** + * Wraps an expression in parentheses if it is needed in order to use the expression for + * property or element access. + */ + function parenthesizeLeftSideOfAccess(expression: ts.Expression): ts.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 = ts.skipPartiallyEmittedExpressions(expression); + if (ts.isLeftHandSideExpression(emittedExpression) + && (emittedExpression.kind !== ts.SyntaxKind.NewExpression || (emittedExpression as ts.NewExpression).arguments)) { + // TODO(rbuckton): Verify whether this assertion holds. + return expression as ts.LeftHandSideExpression; } - function parenthesizeOperandOfPrefixUnary(operand: ts.Expression): ts.UnaryExpression { - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return ts.isUnaryExpression(operand) ? operand : ts.setTextRange(factory.createParenthesizedExpression(operand), operand); - } - function parenthesizeExpressionsOfCommaDelimitedList(elements: ts.NodeArray): ts.NodeArray { - const result = ts.sameMap(elements, parenthesizeExpressionForDisallowedComma); - return ts.setTextRange(factory.createNodeArray(result, elements.hasTrailingComma), elements); - } - function parenthesizeExpressionForDisallowedComma(expression: ts.Expression): ts.Expression { - const emittedExpression = ts.skipPartiallyEmittedExpressions(expression); - const expressionPrecedence = ts.getExpressionPrecedence(emittedExpression); - const commaPrecedence = ts.getOperatorPrecedence(ts.SyntaxKind.BinaryExpression, ts.SyntaxKind.CommaToken); - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return expressionPrecedence > commaPrecedence ? expression : ts.setTextRange(factory.createParenthesizedExpression(expression), expression); - } + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return ts.setTextRange(factory.createParenthesizedExpression(expression), expression); + } - function parenthesizeExpressionOfExpressionStatement(expression: ts.Expression): ts.Expression { - const emittedExpression = ts.skipPartiallyEmittedExpressions(expression); - if (ts.isCallExpression(emittedExpression)) { - const callee = emittedExpression.expression; - const kind = ts.skipPartiallyEmittedExpressions(callee).kind; - if (kind === ts.SyntaxKind.FunctionExpression || kind === ts.SyntaxKind.ArrowFunction) { - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - const updated = factory.updateCallExpression(emittedExpression, ts.setTextRange(factory.createParenthesizedExpression(callee), callee), emittedExpression.typeArguments, emittedExpression.arguments); - return factory.restoreOuterExpressions(expression, updated, ts.OuterExpressionKinds.PartiallyEmittedExpressions); - } - } - const leftmostExpressionKind = ts.getLeftmostExpression(emittedExpression, /*stopAtCallExpressions*/ false).kind; - if (leftmostExpressionKind === ts.SyntaxKind.ObjectLiteralExpression || leftmostExpressionKind === ts.SyntaxKind.FunctionExpression) { - // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return ts.setTextRange(factory.createParenthesizedExpression(expression), expression); - } + function parenthesizeOperandOfPostfixUnary(operand: ts.Expression): ts.LeftHandSideExpression { + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return ts.isLeftHandSideExpression(operand) ? operand : ts.setTextRange(factory.createParenthesizedExpression(operand), operand); + } - return expression; - } + function parenthesizeOperandOfPrefixUnary(operand: ts.Expression): ts.UnaryExpression { + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return ts.isUnaryExpression(operand) ? operand : ts.setTextRange(factory.createParenthesizedExpression(operand), operand); + } + function parenthesizeExpressionsOfCommaDelimitedList(elements: ts.NodeArray): ts.NodeArray { + const result = ts.sameMap(elements, parenthesizeExpressionForDisallowedComma); + return ts.setTextRange(factory.createNodeArray(result, elements.hasTrailingComma), elements); + } + function parenthesizeExpressionForDisallowedComma(expression: ts.Expression): ts.Expression { + const emittedExpression = ts.skipPartiallyEmittedExpressions(expression); + const expressionPrecedence = ts.getExpressionPrecedence(emittedExpression); + const commaPrecedence = ts.getOperatorPrecedence(ts.SyntaxKind.BinaryExpression, ts.SyntaxKind.CommaToken); + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return expressionPrecedence > commaPrecedence ? expression : ts.setTextRange(factory.createParenthesizedExpression(expression), expression); + } - function parenthesizeConciseBodyOfArrowFunction(body: ts.Expression): ts.Expression; - function parenthesizeConciseBodyOfArrowFunction(body: ts.ConciseBody): ts.ConciseBody; - function parenthesizeConciseBodyOfArrowFunction(body: ts.ConciseBody): ts.ConciseBody { - if (!ts.isBlock(body) && (ts.isCommaSequence(body) || ts.getLeftmostExpression(body, /*stopAtCallExpressions*/ false).kind === ts.SyntaxKind.ObjectLiteralExpression)) { + function parenthesizeExpressionOfExpressionStatement(expression: ts.Expression): ts.Expression { + const emittedExpression = ts.skipPartiallyEmittedExpressions(expression); + if (ts.isCallExpression(emittedExpression)) { + const callee = emittedExpression.expression; + const kind = ts.skipPartiallyEmittedExpressions(callee).kind; + if (kind === ts.SyntaxKind.FunctionExpression || kind === ts.SyntaxKind.ArrowFunction) { // TODO(rbuckton): Verifiy whether `setTextRange` is needed. - return ts.setTextRange(factory.createParenthesizedExpression(body), body); + const updated = factory.updateCallExpression(emittedExpression, ts.setTextRange(factory.createParenthesizedExpression(callee), callee), emittedExpression.typeArguments, emittedExpression.arguments); + return factory.restoreOuterExpressions(expression, updated, ts.OuterExpressionKinds.PartiallyEmittedExpressions); } - - return body; } - - // Type[Extends] : - // FunctionOrConstructorType - // ConditionalType[?Extends] - - // ConditionalType[Extends] : - // UnionType[?Extends] - // [~Extends] UnionType[~Extends] `extends` Type[+Extends] `?` Type[~Extends] `:` Type[~Extends] - // - // - The check type (the `UnionType`, above) does not allow function, constructor, or conditional types (they must be parenthesized) - // - The extends type (the first `Type`, above) does not allow conditional types (they must be parenthesized). Function and constructor types are fine. - // - The true and false branch types (the second and third `Type` non-terminals, above) allow any type - function parenthesizeCheckTypeOfConditionalType(checkType: ts.TypeNode): ts.TypeNode { - switch (checkType.kind) { - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ConditionalType: - return factory.createParenthesizedType(checkType); - } - return checkType; + const leftmostExpressionKind = ts.getLeftmostExpression(emittedExpression, /*stopAtCallExpressions*/ false).kind; + if (leftmostExpressionKind === ts.SyntaxKind.ObjectLiteralExpression || leftmostExpressionKind === ts.SyntaxKind.FunctionExpression) { + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return ts.setTextRange(factory.createParenthesizedExpression(expression), expression); } - function parenthesizeExtendsTypeOfConditionalType(extendsType: ts.TypeNode): ts.TypeNode { - switch (extendsType.kind) { - case ts.SyntaxKind.ConditionalType: - return factory.createParenthesizedType(extendsType); - } - return extendsType; - } + return expression; + } - // UnionType[Extends] : - // `|`? IntersectionType[?Extends] - // UnionType[?Extends] `|` IntersectionType[?Extends] - // - // - A union type constituent has the same precedence as the check type of a conditional type - function parenthesizeConstituentTypeOfUnionType(type: ts.TypeNode) { - switch (type.kind) { - case ts.SyntaxKind.UnionType: // Not strictly necessary, but a union containing a union should have been flattened - case ts.SyntaxKind.IntersectionType: // Not strictly necessary, but makes generated output more readable and avoids breaks in DT tests - return factory.createParenthesizedType(type); - } - return parenthesizeCheckTypeOfConditionalType(type); + function parenthesizeConciseBodyOfArrowFunction(body: ts.Expression): ts.Expression; + function parenthesizeConciseBodyOfArrowFunction(body: ts.ConciseBody): ts.ConciseBody; + function parenthesizeConciseBodyOfArrowFunction(body: ts.ConciseBody): ts.ConciseBody { + if (!ts.isBlock(body) && (ts.isCommaSequence(body) || ts.getLeftmostExpression(body, /*stopAtCallExpressions*/ false).kind === ts.SyntaxKind.ObjectLiteralExpression)) { + // TODO(rbuckton): Verifiy whether `setTextRange` is needed. + return ts.setTextRange(factory.createParenthesizedExpression(body), body); } - function parenthesizeConstituentTypesOfUnionType(members: readonly ts.TypeNode[]): ts.NodeArray { - return factory.createNodeArray(ts.sameMap(members, parenthesizeConstituentTypeOfUnionType)); - } + return body; + } - // IntersectionType[Extends] : - // `&`? TypeOperator[?Extends] - // IntersectionType[?Extends] `&` TypeOperator[?Extends] - // - // - An intersection type constituent does not allow function, constructor, conditional, or union types (they must be parenthesized) - function parenthesizeConstituentTypeOfIntersectionType(type: ts.TypeNode) { - switch (type.kind) { - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: // Not strictly necessary, but an intersection containing an intersection should have been flattened - return factory.createParenthesizedType(type); - } - return parenthesizeConstituentTypeOfUnionType(type); - } + // Type[Extends] : + // FunctionOrConstructorType + // ConditionalType[?Extends] + + // ConditionalType[Extends] : + // UnionType[?Extends] + // [~Extends] UnionType[~Extends] `extends` Type[+Extends] `?` Type[~Extends] `:` Type[~Extends] + // + // - The check type (the `UnionType`, above) does not allow function, constructor, or conditional types (they must be parenthesized) + // - The extends type (the first `Type`, above) does not allow conditional types (they must be parenthesized). Function and constructor types are fine. + // - The true and false branch types (the second and third `Type` non-terminals, above) allow any type + function parenthesizeCheckTypeOfConditionalType(checkType: ts.TypeNode): ts.TypeNode { + switch (checkType.kind) { + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ConditionalType: + return factory.createParenthesizedType(checkType); + } + return checkType; + } - function parenthesizeConstituentTypesOfIntersectionType(members: readonly ts.TypeNode[]): ts.NodeArray { - return factory.createNodeArray(ts.sameMap(members, parenthesizeConstituentTypeOfIntersectionType)); + function parenthesizeExtendsTypeOfConditionalType(extendsType: ts.TypeNode): ts.TypeNode { + switch (extendsType.kind) { + case ts.SyntaxKind.ConditionalType: + return factory.createParenthesizedType(extendsType); } + return extendsType; + } - // TypeOperator[Extends] : - // PostfixType - // InferType[?Extends] - // `keyof` TypeOperator[?Extends] - // `unique` TypeOperator[?Extends] - // `readonly` TypeOperator[?Extends] - // - function parenthesizeOperandOfTypeOperator(type: ts.TypeNode) { - switch (type.kind) { - case ts.SyntaxKind.IntersectionType: - return factory.createParenthesizedType(type); - } - return parenthesizeConstituentTypeOfIntersectionType(type); + // UnionType[Extends] : + // `|`? IntersectionType[?Extends] + // UnionType[?Extends] `|` IntersectionType[?Extends] + // + // - A union type constituent has the same precedence as the check type of a conditional type + function parenthesizeConstituentTypeOfUnionType(type: ts.TypeNode) { + switch (type.kind) { + case ts.SyntaxKind.UnionType: // Not strictly necessary, but a union containing a union should have been flattened + case ts.SyntaxKind.IntersectionType: // Not strictly necessary, but makes generated output more readable and avoids breaks in DT tests + return factory.createParenthesizedType(type); } + return parenthesizeCheckTypeOfConditionalType(type); + } - function parenthesizeOperandOfReadonlyTypeOperator(type: ts.TypeNode) { - switch (type.kind) { - case ts.SyntaxKind.TypeOperator: - return factory.createParenthesizedType(type); - } - return parenthesizeOperandOfTypeOperator(type); - } + function parenthesizeConstituentTypesOfUnionType(members: readonly ts.TypeNode[]): ts.NodeArray { + return factory.createNodeArray(ts.sameMap(members, parenthesizeConstituentTypeOfUnionType)); + } - // PostfixType : - // NonArrayType - // NonArrayType [no LineTerminator here] `!` // JSDoc - // NonArrayType [no LineTerminator here] `?` // JSDoc - // IndexedAccessType - // ArrayType - // - // IndexedAccessType : - // NonArrayType `[` Type[~Extends] `]` - // - // ArrayType : - // NonArrayType `[` `]` - // - function parenthesizeNonArrayTypeOfPostfixType(type: ts.TypeNode) { - switch (type.kind) { - case ts.SyntaxKind.InferType: - case ts.SyntaxKind.TypeOperator: - case ts.SyntaxKind.TypeQuery: // Not strictly necessary, but makes generated output more readable and avoids breaks in DT tests - return factory.createParenthesizedType(type); - } - return parenthesizeOperandOfTypeOperator(type); + // IntersectionType[Extends] : + // `&`? TypeOperator[?Extends] + // IntersectionType[?Extends] `&` TypeOperator[?Extends] + // + // - An intersection type constituent does not allow function, constructor, conditional, or union types (they must be parenthesized) + function parenthesizeConstituentTypeOfIntersectionType(type: ts.TypeNode) { + switch (type.kind) { + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: // Not strictly necessary, but an intersection containing an intersection should have been flattened + return factory.createParenthesizedType(type); } + return parenthesizeConstituentTypeOfUnionType(type); + } - // TupleType : - // `[` Elision? `]` - // `[` NamedTupleElementTypes `]` - // `[` NamedTupleElementTypes `,` Elision? `]` - // `[` TupleElementTypes `]` - // `[` TupleElementTypes `,` Elision? `]` - // - // NamedTupleElementTypes : - // Elision? NamedTupleMember - // NamedTupleElementTypes `,` Elision? NamedTupleMember - // - // NamedTupleMember : - // Identifier `?`? `:` Type[~Extends] - // `...` Identifier `:` Type[~Extends] - // - // TupleElementTypes : - // Elision? TupleElementType - // TupleElementTypes `,` Elision? TupleElementType - // - // TupleElementType : - // Type[~Extends] // NOTE: Needs cover grammar to disallow JSDoc postfix-optional - // OptionalType - // RestType - // - // OptionalType : - // Type[~Extends] `?` // NOTE: Needs cover grammar to disallow JSDoc postfix-optional - // - // RestType : - // `...` Type[~Extends] - // - function parenthesizeElementTypesOfTupleType(types: readonly (ts.TypeNode | ts.NamedTupleMember)[]): ts.NodeArray { - return factory.createNodeArray(ts.sameMap(types, parenthesizeElementTypeOfTupleType)); - } + function parenthesizeConstituentTypesOfIntersectionType(members: readonly ts.TypeNode[]): ts.NodeArray { + return factory.createNodeArray(ts.sameMap(members, parenthesizeConstituentTypeOfIntersectionType)); + } - function parenthesizeElementTypeOfTupleType(type: ts.TypeNode | ts.NamedTupleMember): ts.TypeNode { - if (hasJSDocPostfixQuestion(type)) + // TypeOperator[Extends] : + // PostfixType + // InferType[?Extends] + // `keyof` TypeOperator[?Extends] + // `unique` TypeOperator[?Extends] + // `readonly` TypeOperator[?Extends] + // + function parenthesizeOperandOfTypeOperator(type: ts.TypeNode) { + switch (type.kind) { + case ts.SyntaxKind.IntersectionType: return factory.createParenthesizedType(type); - return type; } + return parenthesizeConstituentTypeOfIntersectionType(type); + } - function hasJSDocPostfixQuestion(type: ts.TypeNode | ts.NamedTupleMember): boolean { - if (ts.isJSDocNullableType(type)) - return type.postfix; - if (ts.isNamedTupleMember(type)) - return hasJSDocPostfixQuestion(type.type); - if (ts.isFunctionTypeNode(type) || ts.isConstructorTypeNode(type) || ts.isTypeOperatorNode(type)) - return hasJSDocPostfixQuestion(type.type); - if (ts.isConditionalTypeNode(type)) - return hasJSDocPostfixQuestion(type.falseType); - if (ts.isUnionTypeNode(type)) - return hasJSDocPostfixQuestion(ts.last(type.types)); - if (ts.isIntersectionTypeNode(type)) - return hasJSDocPostfixQuestion(ts.last(type.types)); - if (ts.isInferTypeNode(type)) - return !!type.typeParameter.constraint && hasJSDocPostfixQuestion(type.typeParameter.constraint); - return false; + function parenthesizeOperandOfReadonlyTypeOperator(type: ts.TypeNode) { + switch (type.kind) { + case ts.SyntaxKind.TypeOperator: + return factory.createParenthesizedType(type); } + return parenthesizeOperandOfTypeOperator(type); + } - function parenthesizeTypeOfOptionalType(type: ts.TypeNode): ts.TypeNode { - if (hasJSDocPostfixQuestion(type)) + // PostfixType : + // NonArrayType + // NonArrayType [no LineTerminator here] `!` // JSDoc + // NonArrayType [no LineTerminator here] `?` // JSDoc + // IndexedAccessType + // ArrayType + // + // IndexedAccessType : + // NonArrayType `[` Type[~Extends] `]` + // + // ArrayType : + // NonArrayType `[` `]` + // + function parenthesizeNonArrayTypeOfPostfixType(type: ts.TypeNode) { + switch (type.kind) { + case ts.SyntaxKind.InferType: + case ts.SyntaxKind.TypeOperator: + case ts.SyntaxKind.TypeQuery: // Not strictly necessary, but makes generated output more readable and avoids breaks in DT tests return factory.createParenthesizedType(type); - return parenthesizeNonArrayTypeOfPostfixType(type); } + return parenthesizeOperandOfTypeOperator(type); + } - // function parenthesizeMemberOfElementType(member: TypeNode): TypeNode { - // switch (member.kind) { - // case SyntaxKind.UnionType: - // case SyntaxKind.IntersectionType: - // case SyntaxKind.FunctionType: - // case SyntaxKind.ConstructorType: - // return factory.createParenthesizedType(member); - // } - // return parenthesizeMemberOfConditionalType(member); - // } - - // function parenthesizeElementTypeOfArrayType(member: TypeNode): TypeNode { - // switch (member.kind) { - // case SyntaxKind.TypeQuery: - // case SyntaxKind.TypeOperator: - // case SyntaxKind.InferType: - // return factory.createParenthesizedType(member); - // } - // return parenthesizeMemberOfElementType(member); - // } - - function parenthesizeLeadingTypeArgument(node: ts.TypeNode) { - return ts.isFunctionOrConstructorTypeNode(node) && node.typeParameters ? factory.createParenthesizedType(node) : node; - } + // TupleType : + // `[` Elision? `]` + // `[` NamedTupleElementTypes `]` + // `[` NamedTupleElementTypes `,` Elision? `]` + // `[` TupleElementTypes `]` + // `[` TupleElementTypes `,` Elision? `]` + // + // NamedTupleElementTypes : + // Elision? NamedTupleMember + // NamedTupleElementTypes `,` Elision? NamedTupleMember + // + // NamedTupleMember : + // Identifier `?`? `:` Type[~Extends] + // `...` Identifier `:` Type[~Extends] + // + // TupleElementTypes : + // Elision? TupleElementType + // TupleElementTypes `,` Elision? TupleElementType + // + // TupleElementType : + // Type[~Extends] // NOTE: Needs cover grammar to disallow JSDoc postfix-optional + // OptionalType + // RestType + // + // OptionalType : + // Type[~Extends] `?` // NOTE: Needs cover grammar to disallow JSDoc postfix-optional + // + // RestType : + // `...` Type[~Extends] + // + function parenthesizeElementTypesOfTupleType(types: readonly (ts.TypeNode | ts.NamedTupleMember)[]): ts.NodeArray { + return factory.createNodeArray(ts.sameMap(types, parenthesizeElementTypeOfTupleType)); + } - function parenthesizeOrdinalTypeArgument(node: ts.TypeNode, i: number) { - return i === 0 ? parenthesizeLeadingTypeArgument(node) : node; - } + function parenthesizeElementTypeOfTupleType(type: ts.TypeNode | ts.NamedTupleMember): ts.TypeNode { + if (hasJSDocPostfixQuestion(type)) + return factory.createParenthesizedType(type); + return type; + } - function parenthesizeTypeArguments(typeArguments: ts.NodeArray | undefined): ts.NodeArray | undefined { - if (ts.some(typeArguments)) { - return factory.createNodeArray(ts.sameMap(typeArguments, parenthesizeOrdinalTypeArgument)); - } + function hasJSDocPostfixQuestion(type: ts.TypeNode | ts.NamedTupleMember): boolean { + if (ts.isJSDocNullableType(type)) + return type.postfix; + if (ts.isNamedTupleMember(type)) + return hasJSDocPostfixQuestion(type.type); + if (ts.isFunctionTypeNode(type) || ts.isConstructorTypeNode(type) || ts.isTypeOperatorNode(type)) + return hasJSDocPostfixQuestion(type.type); + if (ts.isConditionalTypeNode(type)) + return hasJSDocPostfixQuestion(type.falseType); + if (ts.isUnionTypeNode(type)) + return hasJSDocPostfixQuestion(ts.last(type.types)); + if (ts.isIntersectionTypeNode(type)) + return hasJSDocPostfixQuestion(ts.last(type.types)); + if (ts.isInferTypeNode(type)) + return !!type.typeParameter.constraint && hasJSDocPostfixQuestion(type.typeParameter.constraint); + return false; + } + + function parenthesizeTypeOfOptionalType(type: ts.TypeNode): ts.TypeNode { + if (hasJSDocPostfixQuestion(type)) + return factory.createParenthesizedType(type); + return parenthesizeNonArrayTypeOfPostfixType(type); + } + + // function parenthesizeMemberOfElementType(member: TypeNode): TypeNode { + // switch (member.kind) { + // case SyntaxKind.UnionType: + // case SyntaxKind.IntersectionType: + // case SyntaxKind.FunctionType: + // case SyntaxKind.ConstructorType: + // return factory.createParenthesizedType(member); + // } + // return parenthesizeMemberOfConditionalType(member); + // } + + // function parenthesizeElementTypeOfArrayType(member: TypeNode): TypeNode { + // switch (member.kind) { + // case SyntaxKind.TypeQuery: + // case SyntaxKind.TypeOperator: + // case SyntaxKind.InferType: + // return factory.createParenthesizedType(member); + // } + // return parenthesizeMemberOfElementType(member); + // } + + function parenthesizeLeadingTypeArgument(node: ts.TypeNode) { + return ts.isFunctionOrConstructorTypeNode(node) && node.typeParameters ? factory.createParenthesizedType(node) : node; + } + + function parenthesizeOrdinalTypeArgument(node: ts.TypeNode, i: number) { + return i === 0 ? parenthesizeLeadingTypeArgument(node) : node; + } + + function parenthesizeTypeArguments(typeArguments: ts.NodeArray | undefined): ts.NodeArray | undefined { + if (ts.some(typeArguments)) { + return factory.createNodeArray(ts.sameMap(typeArguments, parenthesizeOrdinalTypeArgument)); } } +} - export const nullParenthesizerRules: ts.ParenthesizerRules = { - getParenthesizeLeftSideOfBinaryForOperator: _ => ts.identity, - getParenthesizeRightSideOfBinaryForOperator: _ => ts.identity, - parenthesizeLeftSideOfBinary: (_binaryOperator, leftSide) => leftSide, - parenthesizeRightSideOfBinary: (_binaryOperator, _leftSide, rightSide) => rightSide, - parenthesizeExpressionOfComputedPropertyName: ts.identity, - parenthesizeConditionOfConditionalExpression: ts.identity, - parenthesizeBranchOfConditionalExpression: ts.identity, - parenthesizeExpressionOfExportDefault: ts.identity, - parenthesizeExpressionOfNew: expression => ts.cast(expression, ts.isLeftHandSideExpression), - parenthesizeLeftSideOfAccess: expression => ts.cast(expression, ts.isLeftHandSideExpression), - parenthesizeOperandOfPostfixUnary: operand => ts.cast(operand, ts.isLeftHandSideExpression), - parenthesizeOperandOfPrefixUnary: operand => ts.cast(operand, ts.isUnaryExpression), - parenthesizeExpressionsOfCommaDelimitedList: nodes => ts.cast(nodes, ts.isNodeArray), - parenthesizeExpressionForDisallowedComma: ts.identity, - parenthesizeExpressionOfExpressionStatement: ts.identity, - parenthesizeConciseBodyOfArrowFunction: ts.identity, - parenthesizeCheckTypeOfConditionalType: ts.identity, - parenthesizeExtendsTypeOfConditionalType: ts.identity, - parenthesizeConstituentTypesOfUnionType: nodes => ts.cast(nodes, ts.isNodeArray), - parenthesizeConstituentTypeOfUnionType: ts.identity, - parenthesizeConstituentTypesOfIntersectionType: nodes => ts.cast(nodes, ts.isNodeArray), - parenthesizeConstituentTypeOfIntersectionType: ts.identity, - parenthesizeOperandOfTypeOperator: ts.identity, - parenthesizeOperandOfReadonlyTypeOperator: ts.identity, - parenthesizeNonArrayTypeOfPostfixType: ts.identity, - parenthesizeElementTypesOfTupleType: nodes => ts.cast(nodes, ts.isNodeArray), - parenthesizeElementTypeOfTupleType: ts.identity, - parenthesizeTypeOfOptionalType: ts.identity, - parenthesizeTypeArguments: nodes => nodes && ts.cast(nodes, ts.isNodeArray), - parenthesizeLeadingTypeArgument: ts.identity, - }; +export const nullParenthesizerRules: ts.ParenthesizerRules = { + getParenthesizeLeftSideOfBinaryForOperator: _ => ts.identity, + getParenthesizeRightSideOfBinaryForOperator: _ => ts.identity, + parenthesizeLeftSideOfBinary: (_binaryOperator, leftSide) => leftSide, + parenthesizeRightSideOfBinary: (_binaryOperator, _leftSide, rightSide) => rightSide, + parenthesizeExpressionOfComputedPropertyName: ts.identity, + parenthesizeConditionOfConditionalExpression: ts.identity, + parenthesizeBranchOfConditionalExpression: ts.identity, + parenthesizeExpressionOfExportDefault: ts.identity, + parenthesizeExpressionOfNew: expression => ts.cast(expression, ts.isLeftHandSideExpression), + parenthesizeLeftSideOfAccess: expression => ts.cast(expression, ts.isLeftHandSideExpression), + parenthesizeOperandOfPostfixUnary: operand => ts.cast(operand, ts.isLeftHandSideExpression), + parenthesizeOperandOfPrefixUnary: operand => ts.cast(operand, ts.isUnaryExpression), + parenthesizeExpressionsOfCommaDelimitedList: nodes => ts.cast(nodes, ts.isNodeArray), + parenthesizeExpressionForDisallowedComma: ts.identity, + parenthesizeExpressionOfExpressionStatement: ts.identity, + parenthesizeConciseBodyOfArrowFunction: ts.identity, + parenthesizeCheckTypeOfConditionalType: ts.identity, + parenthesizeExtendsTypeOfConditionalType: ts.identity, + parenthesizeConstituentTypesOfUnionType: nodes => ts.cast(nodes, ts.isNodeArray), + parenthesizeConstituentTypeOfUnionType: ts.identity, + parenthesizeConstituentTypesOfIntersectionType: nodes => ts.cast(nodes, ts.isNodeArray), + parenthesizeConstituentTypeOfIntersectionType: ts.identity, + parenthesizeOperandOfTypeOperator: ts.identity, + parenthesizeOperandOfReadonlyTypeOperator: ts.identity, + parenthesizeNonArrayTypeOfPostfixType: ts.identity, + parenthesizeElementTypesOfTupleType: nodes => ts.cast(nodes, ts.isNodeArray), + parenthesizeElementTypeOfTupleType: ts.identity, + parenthesizeTypeOfOptionalType: ts.identity, + parenthesizeTypeArguments: nodes => nodes && ts.cast(nodes, ts.isNodeArray), + parenthesizeLeadingTypeArgument: ts.identity, +}; } diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index 0ce268602704d..2be591cf4af20 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -1,1110 +1,1110 @@ /* @internal */ namespace ts { - // Compound nodes +// Compound nodes - export function createEmptyExports(factory: ts.NodeFactory) { - return factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, factory.createNamedExports([]), /*moduleSpecifier*/ undefined); - } +export function createEmptyExports(factory: ts.NodeFactory) { + return factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, factory.createNamedExports([]), /*moduleSpecifier*/ undefined); +} - export function createMemberAccessForPropertyName(factory: ts.NodeFactory, target: ts.Expression, memberName: ts.PropertyName, location?: ts.TextRange): ts.MemberExpression { - if (ts.isComputedPropertyName(memberName)) { - return ts.setTextRange(factory.createElementAccessExpression(target, memberName.expression), location); - } - else { - const expression = ts.setTextRange(ts.isMemberName(memberName) - ? factory.createPropertyAccessExpression(target, memberName) - : factory.createElementAccessExpression(target, memberName), memberName); - ts.getOrCreateEmitNode(expression).flags |= ts.EmitFlags.NoNestedSourceMaps; - return expression; - } +export function createMemberAccessForPropertyName(factory: ts.NodeFactory, target: ts.Expression, memberName: ts.PropertyName, location?: ts.TextRange): ts.MemberExpression { + if (ts.isComputedPropertyName(memberName)) { + return ts.setTextRange(factory.createElementAccessExpression(target, memberName.expression), location); } - - function createReactNamespace(reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.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 = ts.parseNodeFactory.createIdentifier(reactNamespace || "React"); - // Set the parent that is in parse tree - // this makes sure that parent chain is intact for checker to traverse complete scope tree - ts.setParent(react, ts.getParseTreeNode(parent)); - return react; + else { + const expression = ts.setTextRange(ts.isMemberName(memberName) + ? factory.createPropertyAccessExpression(target, memberName) + : factory.createElementAccessExpression(target, memberName), memberName); + ts.getOrCreateEmitNode(expression).flags |= ts.EmitFlags.NoNestedSourceMaps; + return expression; } +} - function createJsxFactoryExpressionFromEntityName(factory: ts.NodeFactory, jsxFactory: ts.EntityName, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { - if (ts.isQualifiedName(jsxFactory)) { - const left = createJsxFactoryExpressionFromEntityName(factory, jsxFactory.left, parent); - const right = factory.createIdentifier(ts.idText(jsxFactory.right)) as ts.Mutable; - right.escapedText = jsxFactory.right.escapedText; - return factory.createPropertyAccessExpression(left, right); - } - else { - return createReactNamespace(ts.idText(jsxFactory), parent); - } - } +function createReactNamespace(reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.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 = ts.parseNodeFactory.createIdentifier(reactNamespace || "React"); + // Set the parent that is in parse tree + // this makes sure that parent chain is intact for checker to traverse complete scope tree + ts.setParent(react, ts.getParseTreeNode(parent)); + return react; +} - export function createJsxFactoryExpression(factory: ts.NodeFactory, jsxFactoryEntity: ts.EntityName | undefined, reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { - return jsxFactoryEntity ? - createJsxFactoryExpressionFromEntityName(factory, jsxFactoryEntity, parent) : - factory.createPropertyAccessExpression(createReactNamespace(reactNamespace, parent), "createElement"); +function createJsxFactoryExpressionFromEntityName(factory: ts.NodeFactory, jsxFactory: ts.EntityName, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { + if (ts.isQualifiedName(jsxFactory)) { + const left = createJsxFactoryExpressionFromEntityName(factory, jsxFactory.left, parent); + const right = factory.createIdentifier(ts.idText(jsxFactory.right)) as ts.Mutable; + right.escapedText = jsxFactory.right.escapedText; + return factory.createPropertyAccessExpression(left, right); } - - function createJsxFragmentFactoryExpression(factory: ts.NodeFactory, jsxFragmentFactoryEntity: ts.EntityName | undefined, reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { - return jsxFragmentFactoryEntity ? - createJsxFactoryExpressionFromEntityName(factory, jsxFragmentFactoryEntity, parent) : - factory.createPropertyAccessExpression(createReactNamespace(reactNamespace, parent), "Fragment"); + else { + return createReactNamespace(ts.idText(jsxFactory), parent); } +} - export function createExpressionForJsxElement(factory: ts.NodeFactory, callee: ts.Expression, tagName: ts.Expression, props: ts.Expression | undefined, children: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.LeftHandSideExpression { - const argumentsList = [tagName]; - if (props) { - argumentsList.push(props); - } - - if (children && children.length > 0) { - if (!props) { - argumentsList.push(factory.createNull()); - } +export function createJsxFactoryExpression(factory: ts.NodeFactory, jsxFactoryEntity: ts.EntityName | undefined, reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { + return jsxFactoryEntity ? + createJsxFactoryExpressionFromEntityName(factory, jsxFactoryEntity, parent) : + factory.createPropertyAccessExpression(createReactNamespace(reactNamespace, parent), "createElement"); +} - if (children.length > 1) { - for (const child of children) { - startOnNewLine(child); - argumentsList.push(child); - } - } - else { - argumentsList.push(children[0]); - } - } +function createJsxFragmentFactoryExpression(factory: ts.NodeFactory, jsxFragmentFactoryEntity: ts.EntityName | undefined, reactNamespace: string, parent: ts.JsxOpeningLikeElement | ts.JsxOpeningFragment): ts.Expression { + return jsxFragmentFactoryEntity ? + createJsxFactoryExpressionFromEntityName(factory, jsxFragmentFactoryEntity, parent) : + factory.createPropertyAccessExpression(createReactNamespace(reactNamespace, parent), "Fragment"); +} - return ts.setTextRange(factory.createCallExpression(callee, - /*typeArguments*/ undefined, argumentsList), location); +export function createExpressionForJsxElement(factory: ts.NodeFactory, callee: ts.Expression, tagName: ts.Expression, props: ts.Expression | undefined, children: readonly ts.Expression[] | undefined, location: ts.TextRange): ts.LeftHandSideExpression { + const argumentsList = [tagName]; + if (props) { + argumentsList.push(props); } - export function createExpressionForJsxFragment(factory: ts.NodeFactory, jsxFactoryEntity: ts.EntityName | undefined, jsxFragmentFactoryEntity: ts.EntityName | undefined, reactNamespace: string, children: readonly ts.Expression[], parentElement: ts.JsxOpeningFragment, location: ts.TextRange): ts.LeftHandSideExpression { - const tagName = createJsxFragmentFactoryExpression(factory, jsxFragmentFactoryEntity, reactNamespace, parentElement); - const argumentsList = [tagName, factory.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]); - } + if (children && children.length > 0) { + if (!props) { + argumentsList.push(factory.createNull()); } - return ts.setTextRange(factory.createCallExpression(createJsxFactoryExpression(factory, jsxFactoryEntity, reactNamespace, parentElement), - /*typeArguments*/ undefined, argumentsList), location); - } - - // Utilities - - export function createForOfBindingStatement(factory: ts.NodeFactory, node: ts.ForInitializer, boundValue: ts.Expression): ts.Statement { - if (ts.isVariableDeclarationList(node)) { - const firstDeclaration = ts.first(node.declarations); - const updatedDeclaration = factory.updateVariableDeclaration(firstDeclaration, firstDeclaration.name, - /*exclamationToken*/ undefined, - /*type*/ undefined, boundValue); - return ts.setTextRange(factory.createVariableStatement( - /*modifiers*/ undefined, factory.updateVariableDeclarationList(node, [updatedDeclaration])), - /*location*/ node); + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); + } } else { - const updatedExpression = ts.setTextRange(factory.createAssignment(node, boundValue), /*location*/ node); - return ts.setTextRange(factory.createExpressionStatement(updatedExpression), /*location*/ node); + argumentsList.push(children[0]); } } - export function insertLeadingStatement(factory: ts.NodeFactory, dest: ts.Statement, source: ts.Statement) { - if (ts.isBlock(dest)) { - return factory.updateBlock(dest, ts.setTextRange(factory.createNodeArray([source, ...dest.statements]), dest.statements)); - } - else { - return factory.createBlock(factory.createNodeArray([dest, source]), /*multiLine*/ true); - } - } + return ts.setTextRange(factory.createCallExpression(callee, + /*typeArguments*/ undefined, argumentsList), location); +} - export function createExpressionFromEntityName(factory: ts.NodeFactory, node: ts.EntityName | ts.Expression): ts.Expression { - if (ts.isQualifiedName(node)) { - const left = createExpressionFromEntityName(factory, node.left); - // TODO(rbuckton): Does this need to be parented? - const right = ts.setParent(ts.setTextRange(factory.cloneNode(node.right), node.right), node.right.parent); - return ts.setTextRange(factory.createPropertyAccessExpression(left, right), node); - } - else { - // TODO(rbuckton): Does this need to be parented? - return ts.setParent(ts.setTextRange(factory.cloneNode(node), node), node.parent); - } - } +export function createExpressionForJsxFragment(factory: ts.NodeFactory, jsxFactoryEntity: ts.EntityName | undefined, jsxFragmentFactoryEntity: ts.EntityName | undefined, reactNamespace: string, children: readonly ts.Expression[], parentElement: ts.JsxOpeningFragment, location: ts.TextRange): ts.LeftHandSideExpression { + const tagName = createJsxFragmentFactoryExpression(factory, jsxFragmentFactoryEntity, reactNamespace, parentElement); + const argumentsList = [tagName, factory.createNull()]; - export function createExpressionForPropertyName(factory: ts.NodeFactory, memberName: Exclude): ts.Expression { - if (ts.isIdentifier(memberName)) { - return factory.createStringLiteralFromNode(memberName); - } - else if (ts.isComputedPropertyName(memberName)) { - // TODO(rbuckton): Does this need to be parented? - return ts.setParent(ts.setTextRange(factory.cloneNode(memberName.expression), memberName.expression), memberName.expression.parent); + if (children && children.length > 0) { + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); + } } else { - // TODO(rbuckton): Does this need to be parented? - return ts.setParent(ts.setTextRange(factory.cloneNode(memberName), memberName), memberName.parent); + argumentsList.push(children[0]); } } - function createExpressionForAccessorDeclaration(factory: ts.NodeFactory, properties: ts.NodeArray, property: ts.AccessorDeclaration & { - readonly name: Exclude; - }, receiver: ts.Expression, multiLine: boolean) { - const { firstAccessor, getAccessor, setAccessor } = ts.getAllAccessorDeclarations(properties, property); - if (property === firstAccessor) { - return ts.setTextRange(factory.createObjectDefinePropertyCall(receiver, createExpressionForPropertyName(factory, property.name), factory.createPropertyDescriptor({ - enumerable: factory.createFalse(), - configurable: true, - get: getAccessor && ts.setTextRange(ts.setOriginalNode(factory.createFunctionExpression(getAccessor.modifiers, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, getAccessor.parameters, - /*type*/ undefined, getAccessor.body! // TODO: GH#18217 - ), getAccessor), getAccessor), - set: setAccessor && ts.setTextRange(ts.setOriginalNode(factory.createFunctionExpression(setAccessor.modifiers, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, setAccessor.parameters, - /*type*/ undefined, setAccessor.body! // TODO: GH#18217 - ), setAccessor), setAccessor) - }, !multiLine)), firstAccessor); - } + return ts.setTextRange(factory.createCallExpression(createJsxFactoryExpression(factory, jsxFactoryEntity, reactNamespace, parentElement), + /*typeArguments*/ undefined, argumentsList), location); +} - return undefined; +// Utilities + +export function createForOfBindingStatement(factory: ts.NodeFactory, node: ts.ForInitializer, boundValue: ts.Expression): ts.Statement { + if (ts.isVariableDeclarationList(node)) { + const firstDeclaration = ts.first(node.declarations); + const updatedDeclaration = factory.updateVariableDeclaration(firstDeclaration, firstDeclaration.name, + /*exclamationToken*/ undefined, + /*type*/ undefined, boundValue); + return ts.setTextRange(factory.createVariableStatement( + /*modifiers*/ undefined, factory.updateVariableDeclarationList(node, [updatedDeclaration])), + /*location*/ node); + } + else { + const updatedExpression = ts.setTextRange(factory.createAssignment(node, boundValue), /*location*/ node); + return ts.setTextRange(factory.createExpressionStatement(updatedExpression), /*location*/ node); } +} - function createExpressionForPropertyAssignment(factory: ts.NodeFactory, property: ts.PropertyAssignment, receiver: ts.Expression) { - return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, property.name, /*location*/ property.name), property.initializer), property), property); +export function insertLeadingStatement(factory: ts.NodeFactory, dest: ts.Statement, source: ts.Statement) { + if (ts.isBlock(dest)) { + return factory.updateBlock(dest, ts.setTextRange(factory.createNodeArray([source, ...dest.statements]), dest.statements)); } - function createExpressionForShorthandPropertyAssignment(factory: ts.NodeFactory, property: ts.ShorthandPropertyAssignment, receiver: ts.Expression) { - return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, property.name, /*location*/ property.name), factory.cloneNode(property.name)), - /*location*/ property), - /*original*/ property); + else { + return factory.createBlock(factory.createNodeArray([dest, source]), /*multiLine*/ true); } - function createExpressionForMethodDeclaration(factory: ts.NodeFactory, method: ts.MethodDeclaration, receiver: ts.Expression) { - return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, method.name, /*location*/ method.name), ts.setOriginalNode(ts.setTextRange(factory.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); +} + +export function createExpressionFromEntityName(factory: ts.NodeFactory, node: ts.EntityName | ts.Expression): ts.Expression { + if (ts.isQualifiedName(node)) { + const left = createExpressionFromEntityName(factory, node.left); + // TODO(rbuckton): Does this need to be parented? + const right = ts.setParent(ts.setTextRange(factory.cloneNode(node.right), node.right), node.right.parent); + return ts.setTextRange(factory.createPropertyAccessExpression(left, right), node); + } + else { + // TODO(rbuckton): Does this need to be parented? + return ts.setParent(ts.setTextRange(factory.cloneNode(node), node), node.parent); } +} - export function createExpressionForObjectLiteralElementLike(factory: ts.NodeFactory, node: ts.ObjectLiteralExpression, property: ts.ObjectLiteralElementLike, receiver: ts.Expression): ts.Expression | undefined { - if (property.name && ts.isPrivateIdentifier(property.name)) { - ts.Debug.failBadSyntaxKind(property.name, "Private identifiers are not allowed in object literals."); - } - switch (property.kind) { - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return createExpressionForAccessorDeclaration(factory, node.properties, property as typeof property & { - readonly name: Exclude; - }, receiver, !!node.multiLine); - case ts.SyntaxKind.PropertyAssignment: - return createExpressionForPropertyAssignment(factory, property, receiver); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return createExpressionForShorthandPropertyAssignment(factory, property, receiver); - case ts.SyntaxKind.MethodDeclaration: - return createExpressionForMethodDeclaration(factory, property, receiver); - } +export function createExpressionForPropertyName(factory: ts.NodeFactory, memberName: Exclude): ts.Expression { + if (ts.isIdentifier(memberName)) { + return factory.createStringLiteralFromNode(memberName); } + else if (ts.isComputedPropertyName(memberName)) { + // TODO(rbuckton): Does this need to be parented? + return ts.setParent(ts.setTextRange(factory.cloneNode(memberName.expression), memberName.expression), memberName.expression.parent); + } + else { + // TODO(rbuckton): Does this need to be parented? + return ts.setParent(ts.setTextRange(factory.cloneNode(memberName), memberName), memberName.parent); + } +} - /** - * Expand the read and increment/decrement operations a pre- or post-increment or pre- or post-decrement expression. - * - * ```ts - * // input - * ++ - * // output (if result is not discarded) - * var ; - * ( = , = ++, ) - * // output (if result is discarded) - * var ; - * ( = , ++, ) - * - * // input - * ++ - * // output (if result is not discarded) - * var ; - * ( = , = ++) - * // output (if result is discarded) - * var ; - * ( = , ++) - * ``` - * - * It is up to the caller to supply a temporary variable for `` if one is needed. - * The temporary variable `` is injected so that `++` and `--` work uniformly with `number` and `bigint`. - * The result of the expression is always the final result of incrementing or decrementing the expression, so that it can be used for storage. - * - * @param factory {@link NodeFactory} used to create the expanded representation. - * @param node The original prefix or postfix unary node. - * @param expression The expression to use as the value to increment or decrement - * @param resultVariable A temporary variable in which to store the result. Pass `undefined` if the result is discarded, or if the value of `` is the expected result. - */ - export function expandPreOrPostfixIncrementOrDecrementExpression(factory: ts.NodeFactory, node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, expression: ts.Expression, recordTempVariable: (node: ts.Identifier) => void, resultVariable: ts.Identifier | undefined) { - const operator = node.operator; - ts.Debug.assert(operator === ts.SyntaxKind.PlusPlusToken || operator === ts.SyntaxKind.MinusMinusToken, "Expected 'node' to be a pre- or post-increment or pre- or post-decrement expression"); - - const temp = factory.createTempVariable(recordTempVariable); - expression = factory.createAssignment(temp, expression); - ts.setTextRange(expression, node.operand); - let operation: ts.Expression = ts.isPrefixUnaryExpression(node) ? - factory.createPrefixUnaryExpression(operator, temp) : - factory.createPostfixUnaryExpression(temp, operator); - ts.setTextRange(operation, node); +function createExpressionForAccessorDeclaration(factory: ts.NodeFactory, properties: ts.NodeArray, property: ts.AccessorDeclaration & { + readonly name: Exclude; +}, receiver: ts.Expression, multiLine: boolean) { + const { firstAccessor, getAccessor, setAccessor } = ts.getAllAccessorDeclarations(properties, property); + if (property === firstAccessor) { + return ts.setTextRange(factory.createObjectDefinePropertyCall(receiver, createExpressionForPropertyName(factory, property.name), factory.createPropertyDescriptor({ + enumerable: factory.createFalse(), + configurable: true, + get: getAccessor && ts.setTextRange(ts.setOriginalNode(factory.createFunctionExpression(getAccessor.modifiers, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, getAccessor.parameters, + /*type*/ undefined, getAccessor.body! // TODO: GH#18217 + ), getAccessor), getAccessor), + set: setAccessor && ts.setTextRange(ts.setOriginalNode(factory.createFunctionExpression(setAccessor.modifiers, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, setAccessor.parameters, + /*type*/ undefined, setAccessor.body! // TODO: GH#18217 + ), setAccessor), setAccessor) + }, !multiLine)), firstAccessor); + } - if (resultVariable) { - operation = factory.createAssignment(resultVariable, operation); - ts.setTextRange(operation, node); - } + return undefined; +} - expression = factory.createComma(expression, operation); - ts.setTextRange(expression, node); - if (ts.isPostfixUnaryExpression(node)) { - expression = factory.createComma(expression, temp); - ts.setTextRange(expression, node); - } +function createExpressionForPropertyAssignment(factory: ts.NodeFactory, property: ts.PropertyAssignment, receiver: ts.Expression) { + return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, property.name, /*location*/ property.name), property.initializer), property), property); +} +function createExpressionForShorthandPropertyAssignment(factory: ts.NodeFactory, property: ts.ShorthandPropertyAssignment, receiver: ts.Expression) { + return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, property.name, /*location*/ property.name), factory.cloneNode(property.name)), + /*location*/ property), + /*original*/ property); +} +function createExpressionForMethodDeclaration(factory: ts.NodeFactory, method: ts.MethodDeclaration, receiver: ts.Expression) { + return ts.setOriginalNode(ts.setTextRange(factory.createAssignment(createMemberAccessForPropertyName(factory, receiver, method.name, /*location*/ method.name), ts.setOriginalNode(ts.setTextRange(factory.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); +} - return expression; +export function createExpressionForObjectLiteralElementLike(factory: ts.NodeFactory, node: ts.ObjectLiteralExpression, property: ts.ObjectLiteralElementLike, receiver: ts.Expression): ts.Expression | undefined { + if (property.name && ts.isPrivateIdentifier(property.name)) { + ts.Debug.failBadSyntaxKind(property.name, "Private identifiers are not allowed in object literals."); + } + switch (property.kind) { + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return createExpressionForAccessorDeclaration(factory, node.properties, property as typeof property & { + readonly name: Exclude; + }, receiver, !!node.multiLine); + case ts.SyntaxKind.PropertyAssignment: + return createExpressionForPropertyAssignment(factory, property, receiver); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return createExpressionForShorthandPropertyAssignment(factory, property, receiver); + case ts.SyntaxKind.MethodDeclaration: + return createExpressionForMethodDeclaration(factory, property, receiver); } +} - /** - * Gets whether an identifier should only be referred to by its internal name. - */ - export function isInternalName(node: ts.Identifier) { - return (ts.getEmitFlags(node) & ts.EmitFlags.InternalName) !== 0; +/** + * Expand the read and increment/decrement operations a pre- or post-increment or pre- or post-decrement expression. + * + * ```ts + * // input + * ++ + * // output (if result is not discarded) + * var ; + * ( = , = ++, ) + * // output (if result is discarded) + * var ; + * ( = , ++, ) + * + * // input + * ++ + * // output (if result is not discarded) + * var ; + * ( = , = ++) + * // output (if result is discarded) + * var ; + * ( = , ++) + * ``` + * + * It is up to the caller to supply a temporary variable for `` if one is needed. + * The temporary variable `` is injected so that `++` and `--` work uniformly with `number` and `bigint`. + * The result of the expression is always the final result of incrementing or decrementing the expression, so that it can be used for storage. + * + * @param factory {@link NodeFactory} used to create the expanded representation. + * @param node The original prefix or postfix unary node. + * @param expression The expression to use as the value to increment or decrement + * @param resultVariable A temporary variable in which to store the result. Pass `undefined` if the result is discarded, or if the value of `` is the expected result. + */ +export function expandPreOrPostfixIncrementOrDecrementExpression(factory: ts.NodeFactory, node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, expression: ts.Expression, recordTempVariable: (node: ts.Identifier) => void, resultVariable: ts.Identifier | undefined) { + const operator = node.operator; + ts.Debug.assert(operator === ts.SyntaxKind.PlusPlusToken || operator === ts.SyntaxKind.MinusMinusToken, "Expected 'node' to be a pre- or post-increment or pre- or post-decrement expression"); + + const temp = factory.createTempVariable(recordTempVariable); + expression = factory.createAssignment(temp, expression); + ts.setTextRange(expression, node.operand); + let operation: ts.Expression = ts.isPrefixUnaryExpression(node) ? + factory.createPrefixUnaryExpression(operator, temp) : + factory.createPostfixUnaryExpression(temp, operator); + ts.setTextRange(operation, node); + + if (resultVariable) { + operation = factory.createAssignment(resultVariable, operation); + ts.setTextRange(operation, node); } - /** - * Gets whether an identifier should only be referred to by its local name. - */ - export function isLocalName(node: ts.Identifier) { - return (ts.getEmitFlags(node) & ts.EmitFlags.LocalName) !== 0; + expression = factory.createComma(expression, operation); + ts.setTextRange(expression, node); + if (ts.isPostfixUnaryExpression(node)) { + expression = factory.createComma(expression, temp); + ts.setTextRange(expression, node); } - /** - * 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: ts.Identifier) { - return (ts.getEmitFlags(node) & ts.EmitFlags.ExportName) !== 0; - } + return expression; +} - function isUseStrictPrologue(node: ts.ExpressionStatement): boolean { - return ts.isStringLiteral(node.expression) && node.expression.text === "use strict"; - } +/** + * Gets whether an identifier should only be referred to by its internal name. + */ +export function isInternalName(node: ts.Identifier) { + return (ts.getEmitFlags(node) & ts.EmitFlags.InternalName) !== 0; +} - export function findUseStrictPrologue(statements: readonly ts.Statement[]): ts.Statement | undefined { - for (const statement of statements) { - if (ts.isPrologueDirective(statement)) { - if (isUseStrictPrologue(statement)) { - return statement; - } - } - else { - break; +/** + * Gets whether an identifier should only be referred to by its local name. + */ +export function isLocalName(node: ts.Identifier) { + return (ts.getEmitFlags(node) & ts.EmitFlags.LocalName) !== 0; +} + +/** + * 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: ts.Identifier) { + return (ts.getEmitFlags(node) & ts.EmitFlags.ExportName) !== 0; +} + +function isUseStrictPrologue(node: ts.ExpressionStatement): boolean { + return ts.isStringLiteral(node.expression) && node.expression.text === "use strict"; +} + +export function findUseStrictPrologue(statements: readonly ts.Statement[]): ts.Statement | undefined { + for (const statement of statements) { + if (ts.isPrologueDirective(statement)) { + if (isUseStrictPrologue(statement)) { + return statement; } } - return undefined; + else { + break; + } } + return undefined; +} - export function startsWithUseStrict(statements: readonly ts.Statement[]) { - const firstStatement = ts.firstOrUndefined(statements); - return firstStatement !== undefined - && ts.isPrologueDirective(firstStatement) - && isUseStrictPrologue(firstStatement); - } +export function startsWithUseStrict(statements: readonly ts.Statement[]) { + const firstStatement = ts.firstOrUndefined(statements); + return firstStatement !== undefined + && ts.isPrologueDirective(firstStatement) + && isUseStrictPrologue(firstStatement); +} - export function isCommaSequence(node: ts.Expression): node is (ts.BinaryExpression & { - operatorToken: ts.Token; - }) | ts.CommaListExpression { - return node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken || - node.kind === ts.SyntaxKind.CommaListExpression; - } - export function isJSDocTypeAssertion(node: ts.Node): node is ts.JSDocTypeAssertion { - return ts.isParenthesizedExpression(node) - && ts.isInJSFile(node) - && !!ts.getJSDocTypeTag(node); - } - export function getJSDocTypeAssertionType(node: ts.JSDocTypeAssertion) { - const type = ts.getJSDocType(node); - ts.Debug.assertIsDefined(type); - return type; - } +export function isCommaSequence(node: ts.Expression): node is (ts.BinaryExpression & { + operatorToken: ts.Token; +}) | ts.CommaListExpression { + return node.kind === ts.SyntaxKind.BinaryExpression && (node as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.CommaToken || + node.kind === ts.SyntaxKind.CommaListExpression; +} +export function isJSDocTypeAssertion(node: ts.Node): node is ts.JSDocTypeAssertion { + return ts.isParenthesizedExpression(node) + && ts.isInJSFile(node) + && !!ts.getJSDocTypeTag(node); +} +export function getJSDocTypeAssertionType(node: ts.JSDocTypeAssertion) { + const type = ts.getJSDocType(node); + ts.Debug.assertIsDefined(type); + return type; +} - export function isOuterExpression(node: ts.Node, kinds = ts.OuterExpressionKinds.All): node is ts.OuterExpression { - switch (node.kind) { - case ts.SyntaxKind.ParenthesizedExpression: - if (kinds & ts.OuterExpressionKinds.ExcludeJSDocTypeAssertion && isJSDocTypeAssertion(node)) { - return false; - } - return (kinds & ts.OuterExpressionKinds.Parentheses) !== 0; - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.AsExpression: - return (kinds & ts.OuterExpressionKinds.TypeAssertions) !== 0; - case ts.SyntaxKind.NonNullExpression: - return (kinds & ts.OuterExpressionKinds.NonNullAssertions) !== 0; - case ts.SyntaxKind.PartiallyEmittedExpression: - return (kinds & ts.OuterExpressionKinds.PartiallyEmittedExpressions) !== 0; - } - return false; - } +export function isOuterExpression(node: ts.Node, kinds = ts.OuterExpressionKinds.All): node is ts.OuterExpression { + switch (node.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + if (kinds & ts.OuterExpressionKinds.ExcludeJSDocTypeAssertion && isJSDocTypeAssertion(node)) { + return false; + } + return (kinds & ts.OuterExpressionKinds.Parentheses) !== 0; + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.AsExpression: + return (kinds & ts.OuterExpressionKinds.TypeAssertions) !== 0; + case ts.SyntaxKind.NonNullExpression: + return (kinds & ts.OuterExpressionKinds.NonNullAssertions) !== 0; + case ts.SyntaxKind.PartiallyEmittedExpression: + return (kinds & ts.OuterExpressionKinds.PartiallyEmittedExpressions) !== 0; + } + return false; +} - export function skipOuterExpressions(node: ts.Expression, kinds?: ts.OuterExpressionKinds): ts.Expression; - export function skipOuterExpressions(node: ts.Node, kinds?: ts.OuterExpressionKinds): ts.Node; - export function skipOuterExpressions(node: ts.Node, kinds = ts.OuterExpressionKinds.All) { - while (isOuterExpression(node, kinds)) { - node = node.expression; - } - return node; +export function skipOuterExpressions(node: ts.Expression, kinds?: ts.OuterExpressionKinds): ts.Expression; +export function skipOuterExpressions(node: ts.Node, kinds?: ts.OuterExpressionKinds): ts.Node; +export function skipOuterExpressions(node: ts.Node, kinds = ts.OuterExpressionKinds.All) { + while (isOuterExpression(node, kinds)) { + node = node.expression; } + return node; +} - export function skipAssertions(node: ts.Expression): ts.Expression; - export function skipAssertions(node: ts.Node): ts.Node; - export function skipAssertions(node: ts.Node): ts.Node { - return skipOuterExpressions(node, ts.OuterExpressionKinds.Assertions); - } +export function skipAssertions(node: ts.Expression): ts.Expression; +export function skipAssertions(node: ts.Node): ts.Node; +export function skipAssertions(node: ts.Node): ts.Node { + return skipOuterExpressions(node, ts.OuterExpressionKinds.Assertions); +} - export function startOnNewLine(node: T): T { - return ts.setStartsOnNewLine(node, /*newLine*/ true); - } +export function startOnNewLine(node: T): T { + return ts.setStartsOnNewLine(node, /*newLine*/ true); +} - export function getExternalHelpersModuleName(node: ts.SourceFile) { - const parseNode = ts.getOriginalNode(node, ts.isSourceFile); - const emitNode = parseNode && parseNode.emitNode; - return emitNode && emitNode.externalHelpersModuleName; - } +export function getExternalHelpersModuleName(node: ts.SourceFile) { + const parseNode = ts.getOriginalNode(node, ts.isSourceFile); + const emitNode = parseNode && parseNode.emitNode; + return emitNode && emitNode.externalHelpersModuleName; +} - export function hasRecordedExternalHelpers(sourceFile: ts.SourceFile) { - const parseNode = ts.getOriginalNode(sourceFile, ts.isSourceFile); - const emitNode = parseNode && parseNode.emitNode; - return !!emitNode && (!!emitNode.externalHelpersModuleName || !!emitNode.externalHelpers); - } +export function hasRecordedExternalHelpers(sourceFile: ts.SourceFile) { + const parseNode = ts.getOriginalNode(sourceFile, ts.isSourceFile); + const emitNode = parseNode && parseNode.emitNode; + return !!emitNode && (!!emitNode.externalHelpersModuleName || !!emitNode.externalHelpers); +} - export function createExternalHelpersImportDeclarationIfNeeded(nodeFactory: ts.NodeFactory, helperFactory: ts.EmitHelperFactory, sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStar?: boolean, hasImportDefault?: boolean) { - if (compilerOptions.importHelpers && ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { - let namedBindings: ts.NamedImportBindings | undefined; - const moduleKind = ts.getEmitModuleKind(compilerOptions); - if ((moduleKind >= ts.ModuleKind.ES2015 && moduleKind <= ts.ModuleKind.ESNext) || sourceFile.impliedNodeFormat === ts.ModuleKind.ESNext) { - // use named imports - const helpers = ts.getEmitHelpers(sourceFile); - if (helpers) { - const helperNames: string[] = []; - for (const helper of helpers) { - if (!helper.scoped) { - const importName = helper.importName; - if (importName) { - ts.pushIfUnique(helperNames, importName); - } +export function createExternalHelpersImportDeclarationIfNeeded(nodeFactory: ts.NodeFactory, helperFactory: ts.EmitHelperFactory, sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStar?: boolean, hasImportDefault?: boolean) { + if (compilerOptions.importHelpers && ts.isEffectiveExternalModule(sourceFile, compilerOptions)) { + let namedBindings: ts.NamedImportBindings | undefined; + const moduleKind = ts.getEmitModuleKind(compilerOptions); + if ((moduleKind >= ts.ModuleKind.ES2015 && moduleKind <= ts.ModuleKind.ESNext) || sourceFile.impliedNodeFormat === ts.ModuleKind.ESNext) { + // use named imports + const helpers = ts.getEmitHelpers(sourceFile); + if (helpers) { + const helperNames: string[] = []; + for (const helper of helpers) { + if (!helper.scoped) { + const importName = helper.importName; + if (importName) { + ts.pushIfUnique(helperNames, importName); } } - if (ts.some(helperNames)) { - helperNames.sort(ts.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 = nodeFactory.createNamedImports(ts.map(helperNames, name => ts.isFileLevelUniqueName(sourceFile, name) - ? nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, nodeFactory.createIdentifier(name)) - : nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, nodeFactory.createIdentifier(name), helperFactory.getUnscopedHelperName(name)))); - const parseNode = ts.getOriginalNode(sourceFile, ts.isSourceFile); - const emitNode = ts.getOrCreateEmitNode(parseNode); - emitNode.externalHelpers = true; - } } - } - else { - // use a namespace import - const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(nodeFactory, sourceFile, compilerOptions, hasExportStarsToExportValues, hasImportStar || hasImportDefault); - if (externalHelpersModuleName) { - namedBindings = nodeFactory.createNamespaceImport(externalHelpersModuleName); + if (ts.some(helperNames)) { + helperNames.sort(ts.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 = nodeFactory.createNamedImports(ts.map(helperNames, name => ts.isFileLevelUniqueName(sourceFile, name) + ? nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, nodeFactory.createIdentifier(name)) + : nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, nodeFactory.createIdentifier(name), helperFactory.getUnscopedHelperName(name)))); + const parseNode = ts.getOriginalNode(sourceFile, ts.isSourceFile); + const emitNode = ts.getOrCreateEmitNode(parseNode); + emitNode.externalHelpers = true; } } - if (namedBindings) { - const externalHelpersImportDeclaration = nodeFactory.createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, nodeFactory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, namedBindings), nodeFactory.createStringLiteral(ts.externalHelpersModuleNameText), - /*assertClause*/ undefined); - ts.addEmitFlags(externalHelpersImportDeclaration, ts.EmitFlags.NeverApplyImportHelper); - return externalHelpersImportDeclaration; + } + else { + // use a namespace import + const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(nodeFactory, sourceFile, compilerOptions, hasExportStarsToExportValues, hasImportStar || hasImportDefault); + if (externalHelpersModuleName) { + namedBindings = nodeFactory.createNamespaceImport(externalHelpersModuleName); } } + if (namedBindings) { + const externalHelpersImportDeclaration = nodeFactory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, nodeFactory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, namedBindings), nodeFactory.createStringLiteral(ts.externalHelpersModuleNameText), + /*assertClause*/ undefined); + ts.addEmitFlags(externalHelpersImportDeclaration, ts.EmitFlags.NeverApplyImportHelper); + return externalHelpersImportDeclaration; + } } +} - export function getOrCreateExternalHelpersModuleNameIfNeeded(factory: ts.NodeFactory, node: ts.SourceFile, compilerOptions: ts.CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) { - if (compilerOptions.importHelpers && ts.isEffectiveExternalModule(node, compilerOptions)) { - const externalHelpersModuleName = getExternalHelpersModuleName(node); - if (externalHelpersModuleName) { - return externalHelpersModuleName; - } +export function getOrCreateExternalHelpersModuleNameIfNeeded(factory: ts.NodeFactory, node: ts.SourceFile, compilerOptions: ts.CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) { + if (compilerOptions.importHelpers && ts.isEffectiveExternalModule(node, compilerOptions)) { + const externalHelpersModuleName = getExternalHelpersModuleName(node); + if (externalHelpersModuleName) { + return externalHelpersModuleName; + } - const moduleKind = ts.getEmitModuleKind(compilerOptions); - let create = (hasExportStarsToExportValues || (ts.getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault)) - && moduleKind !== ts.ModuleKind.System - && (moduleKind < ts.ModuleKind.ES2015 || node.impliedNodeFormat === ts.ModuleKind.CommonJS); - if (!create) { - const helpers = ts.getEmitHelpers(node); - if (helpers) { - for (const helper of helpers) { - if (!helper.scoped) { - create = true; - break; - } + const moduleKind = ts.getEmitModuleKind(compilerOptions); + let create = (hasExportStarsToExportValues || (ts.getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault)) + && moduleKind !== ts.ModuleKind.System + && (moduleKind < ts.ModuleKind.ES2015 || node.impliedNodeFormat === ts.ModuleKind.CommonJS); + if (!create) { + const helpers = ts.getEmitHelpers(node); + if (helpers) { + for (const helper of helpers) { + if (!helper.scoped) { + create = true; + break; } } } - - if (create) { - const parseNode = ts.getOriginalNode(node, ts.isSourceFile); - const emitNode = ts.getOrCreateEmitNode(parseNode); - return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = factory.createUniqueName(ts.externalHelpersModuleNameText)); - } } - } - /** - * Get the name of that target module from an import or export declaration - */ - export function getLocalNameForExternalImport(factory: ts.NodeFactory, node: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportEqualsDeclaration, sourceFile: ts.SourceFile): ts.Identifier | undefined { - const namespaceDeclaration = ts.getNamespaceDeclarationNode(node); - if (namespaceDeclaration && !ts.isDefaultImport(node) && !ts.isExportNamespaceAsDefaultDeclaration(node)) { - const name = namespaceDeclaration.name; - return ts.isGeneratedIdentifier(name) ? name : factory.createIdentifier(ts.getSourceTextOfNodeFromSourceFile(sourceFile, name) || ts.idText(name)); - } - if (node.kind === ts.SyntaxKind.ImportDeclaration && node.importClause) { - return factory.getGeneratedNameForNode(node); + if (create) { + const parseNode = ts.getOriginalNode(node, ts.isSourceFile); + const emitNode = ts.getOrCreateEmitNode(parseNode); + return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = factory.createUniqueName(ts.externalHelpersModuleNameText)); } - if (node.kind === ts.SyntaxKind.ExportDeclaration && node.moduleSpecifier) { - return factory.getGeneratedNameForNode(node); - } - 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(factory: ts.NodeFactory, importNode: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportEqualsDeclaration | ts.ImportCall, sourceFile: ts.SourceFile, host: ts.EmitHost, resolver: ts.EmitResolver, compilerOptions: ts.CompilerOptions) { - const moduleName = ts.getExternalModuleName(importNode); - if (moduleName && ts.isStringLiteral(moduleName)) { - return tryGetModuleNameFromDeclaration(importNode, host, factory, resolver, compilerOptions) - || tryRenameExternalModule(factory, moduleName, sourceFile) - || factory.cloneNode(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. - */ - function tryRenameExternalModule(factory: ts.NodeFactory, moduleName: ts.LiteralExpression, sourceFile: ts.SourceFile) { - const rename = sourceFile.renamedDependencies && sourceFile.renamedDependencies.get(moduleName.text); - return rename ? factory.createStringLiteral(rename) : undefined; +/** + * Get the name of that target module from an import or export declaration + */ +export function getLocalNameForExternalImport(factory: ts.NodeFactory, node: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportEqualsDeclaration, sourceFile: ts.SourceFile): ts.Identifier | undefined { + const namespaceDeclaration = ts.getNamespaceDeclarationNode(node); + if (namespaceDeclaration && !ts.isDefaultImport(node) && !ts.isExportNamespaceAsDefaultDeclaration(node)) { + const name = namespaceDeclaration.name; + return ts.isGeneratedIdentifier(name) ? name : factory.createIdentifier(ts.getSourceTextOfNodeFromSourceFile(sourceFile, name) || ts.idText(name)); } - - /** - * 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(factory: ts.NodeFactory, file: ts.SourceFile | undefined, host: ts.EmitHost, options: ts.CompilerOptions): ts.StringLiteral | undefined { - if (!file) { - return undefined; - } - if (file.moduleName) { - return factory.createStringLiteral(file.moduleName); - } - if (!file.isDeclarationFile && ts.outFile(options)) { - return factory.createStringLiteral(ts.getExternalModuleNameFromPath(host, file.fileName)); - } - return undefined; + if (node.kind === ts.SyntaxKind.ImportDeclaration && node.importClause) { + return factory.getGeneratedNameForNode(node); } - - function tryGetModuleNameFromDeclaration(declaration: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportCall, host: ts.EmitHost, factory: ts.NodeFactory, resolver: ts.EmitResolver, compilerOptions: ts.CompilerOptions) { - return tryGetModuleNameFromFile(factory, resolver.getExternalModuleFileFromDeclaration(declaration), host, compilerOptions); + if (node.kind === ts.SyntaxKind.ExportDeclaration && node.moduleSpecifier) { + return factory.getGeneratedNameForNode(node); } + return undefined; +} - /** - * Gets the initializer of an BindingOrAssignmentElement. - */ - export function getInitializerOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.Expression | undefined { - if (ts.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 (ts.isPropertyAssignment(bindingElement)) { - // `1` in `({ a: b = 1 } = ...)` - // `1` in `({ a: {b} = 1 } = ...)` - // `1` in `({ a: [b] = 1 } = ...)` - const initializer = bindingElement.initializer; - return ts.isAssignmentExpression(initializer, /*excludeCompoundAssignment*/ true) - ? initializer.right - : undefined; - } - - if (ts.isShorthandPropertyAssignment(bindingElement)) { - // `1` in `({ a = 1 } = ...)` - return bindingElement.objectAssignmentInitializer; - } +/** + * 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(factory: ts.NodeFactory, importNode: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportEqualsDeclaration | ts.ImportCall, sourceFile: ts.SourceFile, host: ts.EmitHost, resolver: ts.EmitResolver, compilerOptions: ts.CompilerOptions) { + const moduleName = ts.getExternalModuleName(importNode); + if (moduleName && ts.isStringLiteral(moduleName)) { + return tryGetModuleNameFromDeclaration(importNode, host, factory, resolver, compilerOptions) + || tryRenameExternalModule(factory, moduleName, sourceFile) + || factory.cloneNode(moduleName); + } + + return undefined; +} - if (ts.isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { - // `1` in `[a = 1] = ...` - // `1` in `[{a} = 1] = ...` - // `1` in `[[a] = 1] = ...` - return bindingElement.right; - } +/** + * 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(factory: ts.NodeFactory, moduleName: ts.LiteralExpression, sourceFile: ts.SourceFile) { + const rename = sourceFile.renamedDependencies && sourceFile.renamedDependencies.get(moduleName.text); + return rename ? factory.createStringLiteral(rename) : undefined; +} - if (ts.isSpreadElement(bindingElement)) { - // Recovery consistent with existing emit. - return getInitializerOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); - } +/** + * 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(factory: ts.NodeFactory, file: ts.SourceFile | undefined, host: ts.EmitHost, options: ts.CompilerOptions): ts.StringLiteral | undefined { + if (!file) { + return undefined; + } + if (file.moduleName) { + return factory.createStringLiteral(file.moduleName); } + if (!file.isDeclarationFile && ts.outFile(options)) { + return factory.createStringLiteral(ts.getExternalModuleNameFromPath(host, file.fileName)); + } + return undefined; +} - /** - * Gets the name of an BindingOrAssignmentElement. - */ - export function getTargetOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.BindingOrAssignmentElementTarget | undefined { - if (ts.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; - } +function tryGetModuleNameFromDeclaration(declaration: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportCall, host: ts.EmitHost, factory: ts.NodeFactory, resolver: ts.EmitResolver, compilerOptions: ts.CompilerOptions) { + return tryGetModuleNameFromFile(factory, resolver.getExternalModuleFileFromDeclaration(declaration), host, compilerOptions); +} - if (ts.isObjectLiteralElementLike(bindingElement)) { - switch (bindingElement.kind) { - case ts.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 as ts.BindingOrAssignmentElement); - case ts.SyntaxKind.ShorthandPropertyAssignment: - // `a` in `({ a } = ...)` - // `a` in `({ a = 1 } = ...)` - return bindingElement.name; - - case ts.SyntaxKind.SpreadAssignment: - // `a` in `({ ...a } = ...)` - return getTargetOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); - } +/** + * Gets the initializer of an BindingOrAssignmentElement. + */ +export function getInitializerOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.Expression | undefined { + if (ts.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; + } - // no target - return undefined; - } + if (ts.isPropertyAssignment(bindingElement)) { + // `1` in `({ a: b = 1 } = ...)` + // `1` in `({ a: {b} = 1 } = ...)` + // `1` in `({ a: [b] = 1 } = ...)` + const initializer = bindingElement.initializer; + return ts.isAssignmentExpression(initializer, /*excludeCompoundAssignment*/ true) + ? initializer.right + : undefined; + } - if (ts.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 as ts.BindingOrAssignmentElement); - } + if (ts.isShorthandPropertyAssignment(bindingElement)) { + // `1` in `({ a = 1 } = ...)` + return bindingElement.objectAssignmentInitializer; + } - if (ts.isSpreadElement(bindingElement)) { - // `a` in `[...a] = ...` - return getTargetOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); - } + if (ts.isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { + // `1` in `[a = 1] = ...` + // `1` in `[{a} = 1] = ...` + // `1` in `[[a] = 1] = ...` + return bindingElement.right; + } - // `a` in `[a] = ...` - // `{a}` in `[{a}] = ...` - // `[a]` in `[[a]] = ...` - // `a.b` in `[a.b] = ...` - // `a[0]` in `[a[0]] = ...` - return bindingElement; + if (ts.isSpreadElement(bindingElement)) { + // Recovery consistent with existing emit. + return getInitializerOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); } +} - /** - * Determines whether an BindingOrAssignmentElement is a rest element. - */ - export function getRestIndicatorOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.BindingOrAssignmentElementRestIndicator | undefined { +/** + * Gets the name of an BindingOrAssignmentElement. + */ +export function getTargetOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.BindingOrAssignmentElementTarget | undefined { + if (ts.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 (ts.isObjectLiteralElementLike(bindingElement)) { switch (bindingElement.kind) { - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.BindingElement: - // `...` in `let [...a] = ...` - return bindingElement.dotDotDotToken; + case ts.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 as ts.BindingOrAssignmentElement); + case ts.SyntaxKind.ShorthandPropertyAssignment: + // `a` in `({ a } = ...)` + // `a` in `({ a = 1 } = ...)` + return bindingElement.name; - case ts.SyntaxKind.SpreadElement: case ts.SyntaxKind.SpreadAssignment: - // `...` in `[...a] = ...` - return bindingElement; + // `a` in `({ ...a } = ...)` + return getTargetOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); } + // no target return undefined; } - /** - * Gets the property name of a BindingOrAssignmentElement - */ - export function getPropertyNameOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): Exclude | undefined { - const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement); - ts.Debug.assert(!!propertyName || ts.isSpreadAssignment(bindingElement), "Invalid property name for binding element."); - return propertyName; + if (ts.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 as ts.BindingOrAssignmentElement); } - export function tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): Exclude | undefined { - switch (bindingElement.kind) { - case ts.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 (ts.isPrivateIdentifier(propertyName)) { - return ts.Debug.failBadSyntaxKind(propertyName); - } - return ts.isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) - ? propertyName.expression - : propertyName; - } + if (ts.isSpreadElement(bindingElement)) { + // `a` in `[...a] = ...` + return getTargetOfBindingOrAssignmentElement(bindingElement.expression as ts.BindingOrAssignmentElement); + } - break; + // `a` in `[a] = ...` + // `{a}` in `[{a}] = ...` + // `[a]` in `[[a]] = ...` + // `a.b` in `[a.b] = ...` + // `a[0]` in `[a[0]] = ...` + return bindingElement; +} - case ts.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 (ts.isPrivateIdentifier(propertyName)) { - return ts.Debug.failBadSyntaxKind(propertyName); - } - return ts.isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) - ? propertyName.expression - : propertyName; - } +/** + * Determines whether an BindingOrAssignmentElement is a rest element. + */ +export function getRestIndicatorOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): ts.BindingOrAssignmentElementRestIndicator | undefined { + switch (bindingElement.kind) { + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.BindingElement: + // `...` in `let [...a] = ...` + return bindingElement.dotDotDotToken; - break; + case ts.SyntaxKind.SpreadElement: + case ts.SyntaxKind.SpreadAssignment: + // `...` in `[...a] = ...` + return bindingElement; + } - case ts.SyntaxKind.SpreadAssignment: - // `a` in `({ ...a } = ...)` - if (bindingElement.name && ts.isPrivateIdentifier(bindingElement.name)) { - return ts.Debug.failBadSyntaxKind(bindingElement.name); + return undefined; +} + +/** + * Gets the property name of a BindingOrAssignmentElement + */ +export function getPropertyNameOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): Exclude | undefined { + const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement); + ts.Debug.assert(!!propertyName || ts.isSpreadAssignment(bindingElement), "Invalid property name for binding element."); + return propertyName; +} + +export function tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement: ts.BindingOrAssignmentElement): Exclude | undefined { + switch (bindingElement.kind) { + case ts.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 (ts.isPrivateIdentifier(propertyName)) { + return ts.Debug.failBadSyntaxKind(propertyName); } - return bindingElement.name; - } + return ts.isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) + ? propertyName.expression + : propertyName; + } - const target = getTargetOfBindingOrAssignmentElement(bindingElement); - if (target && ts.isPropertyName(target)) { - return target; - } + break; + + case ts.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 (ts.isPrivateIdentifier(propertyName)) { + return ts.Debug.failBadSyntaxKind(propertyName); + } + return ts.isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) + ? propertyName.expression + : propertyName; + } + + break; + + case ts.SyntaxKind.SpreadAssignment: + // `a` in `({ ...a } = ...)` + if (bindingElement.name && ts.isPrivateIdentifier(bindingElement.name)) { + return ts.Debug.failBadSyntaxKind(bindingElement.name); + } + return bindingElement.name; } - function isStringOrNumericLiteral(node: ts.Node): node is ts.StringLiteral | ts.NumericLiteral { - const kind = node.kind; - return kind === ts.SyntaxKind.StringLiteral - || kind === ts.SyntaxKind.NumericLiteral; + const target = getTargetOfBindingOrAssignmentElement(bindingElement); + if (target && ts.isPropertyName(target)) { + return target; } +} - /** - * Gets the elements of a BindingOrAssignmentPattern - */ - export function getElementsOfBindingOrAssignmentPattern(name: ts.BindingOrAssignmentPattern): readonly ts.BindingOrAssignmentElement[] { - switch (name.kind) { - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ArrayLiteralExpression: - // `a` in `{a}` - // `a` in `[a]` - return name.elements as readonly ts.BindingOrAssignmentElement[]; - case ts.SyntaxKind.ObjectLiteralExpression: - // `a` in `{a}` - return name.properties as readonly ts.BindingOrAssignmentElement[]; - } +function isStringOrNumericLiteral(node: ts.Node): node is ts.StringLiteral | ts.NumericLiteral { + const kind = node.kind; + return kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.NumericLiteral; +} + +/** + * Gets the elements of a BindingOrAssignmentPattern + */ +export function getElementsOfBindingOrAssignmentPattern(name: ts.BindingOrAssignmentPattern): readonly ts.BindingOrAssignmentElement[] { + switch (name.kind) { + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ArrayLiteralExpression: + // `a` in `{a}` + // `a` in `[a]` + return name.elements as readonly ts.BindingOrAssignmentElement[]; + case ts.SyntaxKind.ObjectLiteralExpression: + // `a` in `{a}` + return name.properties as readonly ts.BindingOrAssignmentElement[]; } +} - /* @internal */ - export function getJSDocTypeAliasName(fullName: ts.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; +/* @internal */ +export function getJSDocTypeAliasName(fullName: ts.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; } } +} - export function canHaveModifiers(node: ts.Node): node is ts.HasModifiers { - const kind = node.kind; - return kind === ts.SyntaxKind.Parameter - || kind === ts.SyntaxKind.PropertySignature - || kind === ts.SyntaxKind.PropertyDeclaration - || kind === ts.SyntaxKind.MethodSignature - || kind === ts.SyntaxKind.MethodDeclaration - || kind === ts.SyntaxKind.Constructor - || kind === ts.SyntaxKind.GetAccessor - || kind === ts.SyntaxKind.SetAccessor - || kind === ts.SyntaxKind.IndexSignature - || kind === ts.SyntaxKind.FunctionExpression - || kind === ts.SyntaxKind.ArrowFunction - || kind === ts.SyntaxKind.ClassExpression - || kind === ts.SyntaxKind.VariableStatement - || kind === ts.SyntaxKind.FunctionDeclaration - || kind === ts.SyntaxKind.ClassDeclaration - || kind === ts.SyntaxKind.InterfaceDeclaration - || kind === ts.SyntaxKind.TypeAliasDeclaration - || kind === ts.SyntaxKind.EnumDeclaration - || kind === ts.SyntaxKind.ModuleDeclaration - || kind === ts.SyntaxKind.ImportEqualsDeclaration - || kind === ts.SyntaxKind.ImportDeclaration - || kind === ts.SyntaxKind.ExportAssignment - || kind === ts.SyntaxKind.ExportDeclaration; - } - export const isTypeNodeOrTypeParameterDeclaration = ts.or(ts.isTypeNode, ts.isTypeParameterDeclaration) as (node: ts.Node) => node is ts.TypeNode | ts.TypeParameterDeclaration; - export const isQuestionOrExclamationToken = ts.or(ts.isQuestionToken, ts.isExclamationToken) as (node: ts.Node) => node is ts.QuestionToken | ts.ExclamationToken; - export const isIdentifierOrThisTypeNode = ts.or(ts.isIdentifier, ts.isThisTypeNode) as (node: ts.Node) => node is ts.Identifier | ts.ThisTypeNode; - export const isReadonlyKeywordOrPlusOrMinusToken = ts.or(ts.isReadonlyKeyword, ts.isPlusToken, ts.isMinusToken) as (node: ts.Node) => node is ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken; - export const isQuestionOrPlusOrMinusToken = ts.or(ts.isQuestionToken, ts.isPlusToken, ts.isMinusToken) as (node: ts.Node) => node is ts.QuestionToken | ts.PlusToken | ts.MinusToken; - export const isModuleName = ts.or(ts.isIdentifier, ts.isStringLiteral) as (node: ts.Node) => node is ts.ModuleName; - export function isLiteralTypeLikeExpression(node: ts.Node): node is ts.NullLiteral | ts.BooleanLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression { - const kind = node.kind; - return kind === ts.SyntaxKind.NullKeyword - || kind === ts.SyntaxKind.TrueKeyword - || kind === ts.SyntaxKind.FalseKeyword - || ts.isLiteralExpression(node) - || ts.isPrefixUnaryExpression(node); - } - function isExponentiationOperator(kind: ts.SyntaxKind): kind is ts.ExponentiationOperator { - return kind === ts.SyntaxKind.AsteriskAsteriskToken; - } - function isMultiplicativeOperator(kind: ts.SyntaxKind): kind is ts.MultiplicativeOperator { - return kind === ts.SyntaxKind.AsteriskToken - || kind === ts.SyntaxKind.SlashToken - || kind === ts.SyntaxKind.PercentToken; - } - function isMultiplicativeOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.MultiplicativeOperatorOrHigher { - return isExponentiationOperator(kind) - || isMultiplicativeOperator(kind); - } +export function canHaveModifiers(node: ts.Node): node is ts.HasModifiers { + const kind = node.kind; + return kind === ts.SyntaxKind.Parameter + || kind === ts.SyntaxKind.PropertySignature + || kind === ts.SyntaxKind.PropertyDeclaration + || kind === ts.SyntaxKind.MethodSignature + || kind === ts.SyntaxKind.MethodDeclaration + || kind === ts.SyntaxKind.Constructor + || kind === ts.SyntaxKind.GetAccessor + || kind === ts.SyntaxKind.SetAccessor + || kind === ts.SyntaxKind.IndexSignature + || kind === ts.SyntaxKind.FunctionExpression + || kind === ts.SyntaxKind.ArrowFunction + || kind === ts.SyntaxKind.ClassExpression + || kind === ts.SyntaxKind.VariableStatement + || kind === ts.SyntaxKind.FunctionDeclaration + || kind === ts.SyntaxKind.ClassDeclaration + || kind === ts.SyntaxKind.InterfaceDeclaration + || kind === ts.SyntaxKind.TypeAliasDeclaration + || kind === ts.SyntaxKind.EnumDeclaration + || kind === ts.SyntaxKind.ModuleDeclaration + || kind === ts.SyntaxKind.ImportEqualsDeclaration + || kind === ts.SyntaxKind.ImportDeclaration + || kind === ts.SyntaxKind.ExportAssignment + || kind === ts.SyntaxKind.ExportDeclaration; +} +export const isTypeNodeOrTypeParameterDeclaration = ts.or(ts.isTypeNode, ts.isTypeParameterDeclaration) as (node: ts.Node) => node is ts.TypeNode | ts.TypeParameterDeclaration; +export const isQuestionOrExclamationToken = ts.or(ts.isQuestionToken, ts.isExclamationToken) as (node: ts.Node) => node is ts.QuestionToken | ts.ExclamationToken; +export const isIdentifierOrThisTypeNode = ts.or(ts.isIdentifier, ts.isThisTypeNode) as (node: ts.Node) => node is ts.Identifier | ts.ThisTypeNode; +export const isReadonlyKeywordOrPlusOrMinusToken = ts.or(ts.isReadonlyKeyword, ts.isPlusToken, ts.isMinusToken) as (node: ts.Node) => node is ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken; +export const isQuestionOrPlusOrMinusToken = ts.or(ts.isQuestionToken, ts.isPlusToken, ts.isMinusToken) as (node: ts.Node) => node is ts.QuestionToken | ts.PlusToken | ts.MinusToken; +export const isModuleName = ts.or(ts.isIdentifier, ts.isStringLiteral) as (node: ts.Node) => node is ts.ModuleName; +export function isLiteralTypeLikeExpression(node: ts.Node): node is ts.NullLiteral | ts.BooleanLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression { + const kind = node.kind; + return kind === ts.SyntaxKind.NullKeyword + || kind === ts.SyntaxKind.TrueKeyword + || kind === ts.SyntaxKind.FalseKeyword + || ts.isLiteralExpression(node) + || ts.isPrefixUnaryExpression(node); +} +function isExponentiationOperator(kind: ts.SyntaxKind): kind is ts.ExponentiationOperator { + return kind === ts.SyntaxKind.AsteriskAsteriskToken; +} +function isMultiplicativeOperator(kind: ts.SyntaxKind): kind is ts.MultiplicativeOperator { + return kind === ts.SyntaxKind.AsteriskToken + || kind === ts.SyntaxKind.SlashToken + || kind === ts.SyntaxKind.PercentToken; +} +function isMultiplicativeOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.MultiplicativeOperatorOrHigher { + return isExponentiationOperator(kind) + || isMultiplicativeOperator(kind); +} - function isAdditiveOperator(kind: ts.SyntaxKind): kind is ts.AdditiveOperator { - return kind === ts.SyntaxKind.PlusToken - || kind === ts.SyntaxKind.MinusToken; - } +function isAdditiveOperator(kind: ts.SyntaxKind): kind is ts.AdditiveOperator { + return kind === ts.SyntaxKind.PlusToken + || kind === ts.SyntaxKind.MinusToken; +} - function isAdditiveOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.AdditiveOperatorOrHigher { - return isAdditiveOperator(kind) - || isMultiplicativeOperatorOrHigher(kind); - } +function isAdditiveOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.AdditiveOperatorOrHigher { + return isAdditiveOperator(kind) + || isMultiplicativeOperatorOrHigher(kind); +} - function isShiftOperator(kind: ts.SyntaxKind): kind is ts.ShiftOperator { - return kind === ts.SyntaxKind.LessThanLessThanToken - || kind === ts.SyntaxKind.GreaterThanGreaterThanToken - || kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken; - } +function isShiftOperator(kind: ts.SyntaxKind): kind is ts.ShiftOperator { + return kind === ts.SyntaxKind.LessThanLessThanToken + || kind === ts.SyntaxKind.GreaterThanGreaterThanToken + || kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken; +} - function isShiftOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.ShiftOperatorOrHigher { - return isShiftOperator(kind) - || isAdditiveOperatorOrHigher(kind); - } +function isShiftOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.ShiftOperatorOrHigher { + return isShiftOperator(kind) + || isAdditiveOperatorOrHigher(kind); +} - function isRelationalOperator(kind: ts.SyntaxKind): kind is ts.RelationalOperator { - return kind === ts.SyntaxKind.LessThanToken - || kind === ts.SyntaxKind.LessThanEqualsToken - || kind === ts.SyntaxKind.GreaterThanToken - || kind === ts.SyntaxKind.GreaterThanEqualsToken - || kind === ts.SyntaxKind.InstanceOfKeyword - || kind === ts.SyntaxKind.InKeyword; - } - function isRelationalOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.RelationalOperatorOrHigher { - return isRelationalOperator(kind) - || isShiftOperatorOrHigher(kind); - } +function isRelationalOperator(kind: ts.SyntaxKind): kind is ts.RelationalOperator { + return kind === ts.SyntaxKind.LessThanToken + || kind === ts.SyntaxKind.LessThanEqualsToken + || kind === ts.SyntaxKind.GreaterThanToken + || kind === ts.SyntaxKind.GreaterThanEqualsToken + || kind === ts.SyntaxKind.InstanceOfKeyword + || kind === ts.SyntaxKind.InKeyword; +} +function isRelationalOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.RelationalOperatorOrHigher { + return isRelationalOperator(kind) + || isShiftOperatorOrHigher(kind); +} - function isEqualityOperator(kind: ts.SyntaxKind): kind is ts.EqualityOperator { - return kind === ts.SyntaxKind.EqualsEqualsToken - || kind === ts.SyntaxKind.EqualsEqualsEqualsToken - || kind === ts.SyntaxKind.ExclamationEqualsToken - || kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; - } +function isEqualityOperator(kind: ts.SyntaxKind): kind is ts.EqualityOperator { + return kind === ts.SyntaxKind.EqualsEqualsToken + || kind === ts.SyntaxKind.EqualsEqualsEqualsToken + || kind === ts.SyntaxKind.ExclamationEqualsToken + || kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; +} - function isEqualityOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.EqualityOperatorOrHigher { - return isEqualityOperator(kind) - || isRelationalOperatorOrHigher(kind); - } +function isEqualityOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.EqualityOperatorOrHigher { + return isEqualityOperator(kind) + || isRelationalOperatorOrHigher(kind); +} - function isBitwiseOperator(kind: ts.SyntaxKind): kind is ts.BitwiseOperator { - return kind === ts.SyntaxKind.AmpersandToken - || kind === ts.SyntaxKind.BarToken - || kind === ts.SyntaxKind.CaretToken; - } +function isBitwiseOperator(kind: ts.SyntaxKind): kind is ts.BitwiseOperator { + return kind === ts.SyntaxKind.AmpersandToken + || kind === ts.SyntaxKind.BarToken + || kind === ts.SyntaxKind.CaretToken; +} - function isBitwiseOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.BitwiseOperatorOrHigher { - return isBitwiseOperator(kind) - || isEqualityOperatorOrHigher(kind); - } +function isBitwiseOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.BitwiseOperatorOrHigher { + return isBitwiseOperator(kind) + || isEqualityOperatorOrHigher(kind); +} - // NOTE: The version in utilities includes ExclamationToken, which is not a binary operator. - function isLogicalOperator(kind: ts.SyntaxKind): kind is ts.LogicalOperator { - return kind === ts.SyntaxKind.AmpersandAmpersandToken - || kind === ts.SyntaxKind.BarBarToken; - } +// NOTE: The version in utilities includes ExclamationToken, which is not a binary operator. +function isLogicalOperator(kind: ts.SyntaxKind): kind is ts.LogicalOperator { + return kind === ts.SyntaxKind.AmpersandAmpersandToken + || kind === ts.SyntaxKind.BarBarToken; +} - function isLogicalOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.LogicalOperatorOrHigher { - return isLogicalOperator(kind) - || isBitwiseOperatorOrHigher(kind); - } +function isLogicalOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.LogicalOperatorOrHigher { + return isLogicalOperator(kind) + || isBitwiseOperatorOrHigher(kind); +} - function isAssignmentOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.AssignmentOperatorOrHigher { - return kind === ts.SyntaxKind.QuestionQuestionToken - || isLogicalOperatorOrHigher(kind) - || ts.isAssignmentOperator(kind); - } +function isAssignmentOperatorOrHigher(kind: ts.SyntaxKind): kind is ts.AssignmentOperatorOrHigher { + return kind === ts.SyntaxKind.QuestionQuestionToken + || isLogicalOperatorOrHigher(kind) + || ts.isAssignmentOperator(kind); +} - function isBinaryOperator(kind: ts.SyntaxKind): kind is ts.BinaryOperator { - return isAssignmentOperatorOrHigher(kind) - || kind === ts.SyntaxKind.CommaToken; - } +function isBinaryOperator(kind: ts.SyntaxKind): kind is ts.BinaryOperator { + return isAssignmentOperatorOrHigher(kind) + || kind === ts.SyntaxKind.CommaToken; +} - export function isBinaryOperatorToken(node: ts.Node): node is ts.BinaryOperatorToken { - return isBinaryOperator(node.kind); - } +export function isBinaryOperatorToken(node: ts.Node): node is ts.BinaryOperatorToken { + return isBinaryOperator(node.kind); +} - type BinaryExpressionState = (machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], resultHolder: { +type BinaryExpressionState = (machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], resultHolder: { + value: TResult; +}, outerState: TOuterState) => number; + +namespace BinaryExpressionState { + /** + * Handles walking into a `BinaryExpression`. + * @param machine State machine handler functions + * @param frame The current frame + * @returns The new frame + */ + export function enter(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult; - }, outerState: TOuterState) => number; - - namespace BinaryExpressionState { - /** - * Handles walking into a `BinaryExpression`. - * @param machine State machine handler functions - * @param frame The current frame - * @returns The new frame - */ - export function enter(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { - value: TResult; - }, outerState: TOuterState): number { - const prevUserState = stackIndex > 0 ? userStateStack[stackIndex - 1] : undefined; - ts.Debug.assertEqual(stateStack[stackIndex], enter); - userStateStack[stackIndex] = machine.onEnter(nodeStack[stackIndex], prevUserState, outerState); - stateStack[stackIndex] = nextState(machine, enter); - return stackIndex; - } + }, outerState: TOuterState): number { + const prevUserState = stackIndex > 0 ? userStateStack[stackIndex - 1] : undefined; + ts.Debug.assertEqual(stateStack[stackIndex], enter); + userStateStack[stackIndex] = machine.onEnter(nodeStack[stackIndex], prevUserState, outerState); + stateStack[stackIndex] = nextState(machine, enter); + return stackIndex; + } - /** - * Handles walking the `left` side of a `BinaryExpression`. - * @param machine State machine handler functions - * @param frame The current frame - * @returns The new frame - */ - export function left(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { - value: TResult; - }, _outerState: TOuterState): number { - ts.Debug.assertEqual(stateStack[stackIndex], left); - ts.Debug.assertIsDefined(machine.onLeft); - stateStack[stackIndex] = nextState(machine, left); - const nextNode = machine.onLeft(nodeStack[stackIndex].left, userStateStack[stackIndex], nodeStack[stackIndex]); - if (nextNode) { - checkCircularity(stackIndex, nodeStack, nextNode); - return pushStack(stackIndex, stateStack, nodeStack, userStateStack, nextNode); - } - return stackIndex; + /** + * Handles walking the `left` side of a `BinaryExpression`. + * @param machine State machine handler functions + * @param frame The current frame + * @returns The new frame + */ + export function left(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { + value: TResult; + }, _outerState: TOuterState): number { + ts.Debug.assertEqual(stateStack[stackIndex], left); + ts.Debug.assertIsDefined(machine.onLeft); + stateStack[stackIndex] = nextState(machine, left); + const nextNode = machine.onLeft(nodeStack[stackIndex].left, userStateStack[stackIndex], nodeStack[stackIndex]); + if (nextNode) { + checkCircularity(stackIndex, nodeStack, nextNode); + return pushStack(stackIndex, stateStack, nodeStack, userStateStack, nextNode); } + return stackIndex; + } - /** - * Handles walking the `operatorToken` of a `BinaryExpression`. - * @param machine State machine handler functions - * @param frame The current frame - * @returns The new frame - */ - export function operator(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { - value: TResult; - }, _outerState: TOuterState): number { - ts.Debug.assertEqual(stateStack[stackIndex], operator); - ts.Debug.assertIsDefined(machine.onOperator); - stateStack[stackIndex] = nextState(machine, operator); - machine.onOperator(nodeStack[stackIndex].operatorToken, userStateStack[stackIndex], nodeStack[stackIndex]); - return stackIndex; - } + /** + * Handles walking the `operatorToken` of a `BinaryExpression`. + * @param machine State machine handler functions + * @param frame The current frame + * @returns The new frame + */ + export function operator(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { + value: TResult; + }, _outerState: TOuterState): number { + ts.Debug.assertEqual(stateStack[stackIndex], operator); + ts.Debug.assertIsDefined(machine.onOperator); + stateStack[stackIndex] = nextState(machine, operator); + machine.onOperator(nodeStack[stackIndex].operatorToken, userStateStack[stackIndex], nodeStack[stackIndex]); + return stackIndex; + } - /** - * Handles walking the `right` side of a `BinaryExpression`. - * @param machine State machine handler functions - * @param frame The current frame - * @returns The new frame - */ - export function right(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { - value: TResult; - }, _outerState: TOuterState): number { - ts.Debug.assertEqual(stateStack[stackIndex], right); - ts.Debug.assertIsDefined(machine.onRight); - stateStack[stackIndex] = nextState(machine, right); - const nextNode = machine.onRight(nodeStack[stackIndex].right, userStateStack[stackIndex], nodeStack[stackIndex]); - if (nextNode) { - checkCircularity(stackIndex, nodeStack, nextNode); - return pushStack(stackIndex, stateStack, nodeStack, userStateStack, nextNode); - } - return stackIndex; + /** + * Handles walking the `right` side of a `BinaryExpression`. + * @param machine State machine handler functions + * @param frame The current frame + * @returns The new frame + */ + export function right(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], _resultHolder: { + value: TResult; + }, _outerState: TOuterState): number { + ts.Debug.assertEqual(stateStack[stackIndex], right); + ts.Debug.assertIsDefined(machine.onRight); + stateStack[stackIndex] = nextState(machine, right); + const nextNode = machine.onRight(nodeStack[stackIndex].right, userStateStack[stackIndex], nodeStack[stackIndex]); + if (nextNode) { + checkCircularity(stackIndex, nodeStack, nextNode); + return pushStack(stackIndex, stateStack, nodeStack, userStateStack, nextNode); } + return stackIndex; + } - /** - * Handles walking out of a `BinaryExpression`. - * @param machine State machine handler functions - * @param frame The current frame - * @returns The new frame - */ - export function exit(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], resultHolder: { - value: TResult; - }, _outerState: TOuterState): number { - ts.Debug.assertEqual(stateStack[stackIndex], exit); - stateStack[stackIndex] = nextState(machine, exit); - const result = machine.onExit(nodeStack[stackIndex], userStateStack[stackIndex]); - if (stackIndex > 0) { - stackIndex--; - if (machine.foldState) { - const side = stateStack[stackIndex] === exit ? "right" : "left"; - userStateStack[stackIndex] = machine.foldState(userStateStack[stackIndex], result, side); - } - } - else { - resultHolder.value = result; + /** + * Handles walking out of a `BinaryExpression`. + * @param machine State machine handler functions + * @param frame The current frame + * @returns The new frame + */ + export function exit(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], resultHolder: { + value: TResult; + }, _outerState: TOuterState): number { + ts.Debug.assertEqual(stateStack[stackIndex], exit); + stateStack[stackIndex] = nextState(machine, exit); + const result = machine.onExit(nodeStack[stackIndex], userStateStack[stackIndex]); + if (stackIndex > 0) { + stackIndex--; + if (machine.foldState) { + const side = stateStack[stackIndex] === exit ? "right" : "left"; + userStateStack[stackIndex] = machine.foldState(userStateStack[stackIndex], result, side); } - return stackIndex; } - - /** - * Handles a frame that is already done. - * @returns The `done` state. - */ - export function done(_machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], _nodeStack: ts.BinaryExpression[], _userStateStack: TState[], _resultHolder: { - value: TResult; - }, _outerState: TOuterState): number { - ts.Debug.assertEqual(stateStack[stackIndex], done); - return stackIndex; + else { + resultHolder.value = result; } + return stackIndex; + } - export function nextState(machine: BinaryExpressionStateMachine, currentState: BinaryExpressionState) { - switch (currentState) { - case enter: - if (machine.onLeft) - return left; - // falls through - case left: - if (machine.onOperator) - return operator; - // falls through - case operator: - if (machine.onRight) - return right; - // falls through - case right: return exit; - case exit: return done; - case done: return done; - default: ts.Debug.fail("Invalid state"); - } + /** + * Handles a frame that is already done. + * @returns The `done` state. + */ + export function done(_machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], _nodeStack: ts.BinaryExpression[], _userStateStack: TState[], _resultHolder: { + value: TResult; + }, _outerState: TOuterState): number { + ts.Debug.assertEqual(stateStack[stackIndex], done); + return stackIndex; + } + + export function nextState(machine: BinaryExpressionStateMachine, currentState: BinaryExpressionState) { + switch (currentState) { + case enter: + if (machine.onLeft) + return left; + // falls through + case left: + if (machine.onOperator) + return operator; + // falls through + case operator: + if (machine.onRight) + return right; + // falls through + case right: return exit; + case exit: return done; + case done: return done; + default: ts.Debug.fail("Invalid state"); } + } - function pushStack(stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], node: ts.BinaryExpression) { - stackIndex++; - stateStack[stackIndex] = enter; - nodeStack[stackIndex] = node; - userStateStack[stackIndex] = undefined!; - return stackIndex; - } + function pushStack(stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: ts.BinaryExpression[], userStateStack: TState[], node: ts.BinaryExpression) { + stackIndex++; + stateStack[stackIndex] = enter; + nodeStack[stackIndex] = node; + userStateStack[stackIndex] = undefined!; + return stackIndex; + } - function checkCircularity(stackIndex: number, nodeStack: ts.BinaryExpression[], node: ts.BinaryExpression) { - if (ts.Debug.shouldAssert(ts.AssertionLevel.Aggressive)) { - while (stackIndex >= 0) { - ts.Debug.assert(nodeStack[stackIndex] !== node, "Circular traversal detected."); - stackIndex--; - } + function checkCircularity(stackIndex: number, nodeStack: ts.BinaryExpression[], node: ts.BinaryExpression) { + if (ts.Debug.shouldAssert(ts.AssertionLevel.Aggressive)) { + while (stackIndex >= 0) { + ts.Debug.assert(nodeStack[stackIndex] !== node, "Circular traversal detected."); + stackIndex--; } } } +} - /** - * Holds state machine handler functions - */ - class BinaryExpressionStateMachine { - constructor(readonly onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, readonly onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, readonly onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, readonly onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, readonly onExit: (node: ts.BinaryExpression, userState: TState) => TResult, readonly foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined) { - } +/** + * Holds state machine handler functions + */ +class BinaryExpressionStateMachine { + constructor(readonly onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, readonly onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, readonly onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, readonly onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, readonly onExit: (node: ts.BinaryExpression, userState: TState) => TResult, readonly foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined) { } +} - /** - * Creates a state machine that walks a `BinaryExpression` using the heap to reduce call-stack depth on a large tree. - * @param onEnter Callback evaluated when entering a `BinaryExpression`. Returns new user-defined state to associate with the node while walking. - * @param onLeft Callback evaluated when walking the left side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the right side. - * @param onRight Callback evaluated when walking the right side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the end of the node. - * @param onExit Callback evaluated when exiting a `BinaryExpression`. The result returned will either be folded into the parent's state, or returned from the walker if at the top frame. - * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. - * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. - */ - export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined): (node: ts.BinaryExpression) => TResult; - /** - * Creates a state machine that walks a `BinaryExpression` using the heap to reduce call-stack depth on a large tree. - * @param onEnter Callback evaluated when entering a `BinaryExpression`. Returns new user-defined state to associate with the node while walking. - * @param onLeft Callback evaluated when walking the left side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the right side. - * @param onRight Callback evaluated when walking the right side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the end of the node. - * @param onExit Callback evaluated when exiting a `BinaryExpression`. The result returned will either be folded into the parent's state, or returned from the walker if at the top frame. - * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. - * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. - */ - export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined): (node: ts.BinaryExpression, outerState: TOuterState) => TResult; - export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined) { - const machine = new BinaryExpressionStateMachine(onEnter, onLeft, onOperator, onRight, onExit, foldState); - return trampoline; - - function trampoline(node: ts.BinaryExpression, outerState?: TOuterState) { - const resultHolder: { - value: TResult; - } = { value: undefined! }; - const stateStack: BinaryExpressionState[] = [BinaryExpressionState.enter]; - const nodeStack: ts.BinaryExpression[] = [node]; - const userStateStack: TState[] = [undefined!]; - let stackIndex = 0; - while (stateStack[stackIndex] !== BinaryExpressionState.done) { - stackIndex = stateStack[stackIndex](machine, stackIndex, stateStack, nodeStack, userStateStack, resultHolder, outerState); - } - ts.Debug.assertEqual(stackIndex, 0); - return resultHolder.value; +/** + * Creates a state machine that walks a `BinaryExpression` using the heap to reduce call-stack depth on a large tree. + * @param onEnter Callback evaluated when entering a `BinaryExpression`. Returns new user-defined state to associate with the node while walking. + * @param onLeft Callback evaluated when walking the left side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the right side. + * @param onRight Callback evaluated when walking the right side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the end of the node. + * @param onExit Callback evaluated when exiting a `BinaryExpression`. The result returned will either be folded into the parent's state, or returned from the walker if at the top frame. + * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. + * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. + */ +export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined): (node: ts.BinaryExpression) => TResult; +/** + * Creates a state machine that walks a `BinaryExpression` using the heap to reduce call-stack depth on a large tree. + * @param onEnter Callback evaluated when entering a `BinaryExpression`. Returns new user-defined state to associate with the node while walking. + * @param onLeft Callback evaluated when walking the left side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the right side. + * @param onRight Callback evaluated when walking the right side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the end of the node. + * @param onExit Callback evaluated when exiting a `BinaryExpression`. The result returned will either be folded into the parent's state, or returned from the walker if at the top frame. + * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. + * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. + */ +export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined): (node: ts.BinaryExpression, outerState: TOuterState) => TResult; +export function createBinaryExpressionTrampoline(onEnter: (node: ts.BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, onLeft: ((left: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onOperator: ((operatorToken: ts.BinaryOperatorToken, userState: TState, node: ts.BinaryExpression) => void) | undefined, onRight: ((right: ts.Expression, userState: TState, node: ts.BinaryExpression) => ts.BinaryExpression | void) | undefined, onExit: (node: ts.BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined) { + const machine = new BinaryExpressionStateMachine(onEnter, onLeft, onOperator, onRight, onExit, foldState); + return trampoline; + + function trampoline(node: ts.BinaryExpression, outerState?: TOuterState) { + const resultHolder: { + value: TResult; + } = { value: undefined! }; + const stateStack: BinaryExpressionState[] = [BinaryExpressionState.enter]; + const nodeStack: ts.BinaryExpression[] = [node]; + const userStateStack: TState[] = [undefined!]; + let stackIndex = 0; + while (stateStack[stackIndex] !== BinaryExpressionState.done) { + stackIndex = stateStack[stackIndex](machine, stackIndex, stateStack, nodeStack, userStateStack, resultHolder, outerState); } + ts.Debug.assertEqual(stackIndex, 0); + return resultHolder.value; } } +} diff --git a/src/compiler/factory/utilitiesPublic.ts b/src/compiler/factory/utilitiesPublic.ts index 9e87cbfa68dff..fd36bcd739ac8 100644 --- a/src/compiler/factory/utilitiesPublic.ts +++ b/src/compiler/factory/utilitiesPublic.ts @@ -1,5 +1,5 @@ namespace ts { - export function setTextRange(range: T, location: ts.TextRange | undefined): T { - return location ? ts.setTextRangePosEnd(range, location.pos, location.end) : range; - } -} \ No newline at end of file +export function setTextRange(range: T, location: ts.TextRange | undefined): T { + return location ? ts.setTextRangePosEnd(range, location.pos, location.end) : range; +} +} diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 3f4053701f336..b7df8ba16fea6 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1,2231 +1,2230 @@ namespace ts { - /* @internal */ - export function trace(host: ts.ModuleResolutionHost, message: ts.DiagnosticMessage, ...args: any[]): void; - export function trace(host: ts.ModuleResolutionHost): void { - host.trace!(ts.formatMessage.apply(undefined, arguments)); - } - - /* @internal */ - export function isTraceEnabled(compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost): boolean { - return !!compilerOptions.traceResolution && host.trace !== undefined; - } - - function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExtension | undefined): Resolved | undefined { - let packageId: ts.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 + ts.directorySeparator.length), - version: packageJsonContent.version - }; - } - } - return r && { path: r.path, extension: r.ext, packageId }; - } +/* @internal */ +export function trace(host: ts.ModuleResolutionHost, message: ts.DiagnosticMessage, ...args: any[]): void; +export function trace(host: ts.ModuleResolutionHost): void { + host.trace!(ts.formatMessage.apply(undefined, arguments)); +} - function noPackageId(r: PathAndExtension | undefined): Resolved | undefined { - return withPackageId(/*packageInfo*/ undefined, r); - } +/* @internal */ +export function isTraceEnabled(compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost): boolean { + return !!compilerOptions.traceResolution && host.trace !== undefined; +} - function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined { - if (r) { - ts.Debug.assert(r.packageId === undefined); - return { path: r.path, ext: r.extension }; +function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExtension | undefined): Resolved | undefined { + let packageId: ts.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 + ts.directorySeparator.length), + version: packageJsonContent.version + }; } } + return r && { path: r.path, extension: r.ext, packageId }; +} - /** Result of trying to resolve a module. */ - interface Resolved { - path: string; - extension: ts.Extension; - packageId: ts.PackageId | undefined; - /** - * When the resolved is not created from cache, the value is - * - string 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 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 - * Note: This is a file name with preserved original casing, not a normalized `Path`. - */ - originalPath?: string | true; - } +function noPackageId(r: PathAndExtension | undefined): Resolved | undefined { + return withPackageId(/*packageInfo*/ undefined, r); +} - /** 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: ts.Extension; +function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined { + if (r) { + ts.Debug.assert(r.packageId === undefined); + return { path: r.path, ext: r.extension }; } +} +/** Result of trying to resolve a module. */ +interface Resolved { + path: string; + extension: ts.Extension; + packageId: ts.PackageId | undefined; /** - * Kinds of file that we are currently looking for. - * Typically there is one pass with Extensions.TypeScript, then a second pass with Extensions.JavaScript. + * When the resolved is not created from cache, the value is + * - string 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 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 + * Note: This is a file name with preserved original casing, not a normalized `Path`. */ - enum Extensions { - TypeScript, - JavaScript, - Json, - TSConfig, - DtsOnly, - TsOnly - } - - interface PathAndPackageId { - readonly fileName: string; - readonly packageId: ts.PackageId | undefined; - } - /** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */ - function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined { - if (!resolved) { - return undefined; - } - ts.Debug.assert(ts.extensionIsTS(resolved.extension)); - return { fileName: resolved.path, packageId: resolved.packageId }; - } + originalPath?: string | true; +} - function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean | undefined, failedLookupLocations: string[], diagnostics: ts.Diagnostic[], resultFromCache: ts.ResolvedModuleWithFailedLookupLocations | undefined): ts.ResolvedModuleWithFailedLookupLocations { - if (resultFromCache) { - resultFromCache.failedLookupLocations.push(...failedLookupLocations); - return resultFromCache; - } - return { - resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId }, - failedLookupLocations, - resolutionDiagnostics: diagnostics - }; - } +/** 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: ts.Extension; +} - /*@internal*/ - interface ModuleResolutionState { - host: ts.ModuleResolutionHost; - compilerOptions: ts.CompilerOptions; - traceEnabled: boolean; - failedLookupLocations: ts.Push; - resultFromCache?: ts.ResolvedModuleWithFailedLookupLocations; - packageJsonInfoCache: PackageJsonInfoCache | undefined; - features: NodeResolutionFeatures; - conditions: string[]; - requestContainingDirectory: string | undefined; - reportDiagnostic: ts.DiagnosticReporter; - } +/** + * 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, + TsOnly +} - /** Just the fields that we use for module resolution. */ - /*@internal*/ - interface PackageJsonPathFields { - typings?: string; - types?: string; - typesVersions?: ts.MapLike>; - main?: string; - tsconfig?: string; - type?: string; - imports?: object; - exports?: object; - name?: string; +interface PathAndPackageId { + readonly fileName: string; + readonly packageId: ts.PackageId | undefined; +} +/** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */ +function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined { + if (!resolved) { + return undefined; } + ts.Debug.assert(ts.extensionIsTS(resolved.extension)); + return { fileName: resolved.path, packageId: resolved.packageId }; +} - interface PackageJson extends PackageJsonPathFields { - name?: string; - version?: string; +function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean | undefined, failedLookupLocations: string[], diagnostics: ts.Diagnostic[], resultFromCache: ts.ResolvedModuleWithFailedLookupLocations | undefined): ts.ResolvedModuleWithFailedLookupLocations { + if (resultFromCache) { + resultFromCache.failedLookupLocations.push(...failedLookupLocations); + return resultFromCache; } + return { + resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId }, + failedLookupLocations, + resolutionDiagnostics: diagnostics + }; +} - 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 (!ts.hasProperty(jsonContent, fieldName)) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_does_not_have_a_0_field, fieldName); - } - return; +/*@internal*/ +interface ModuleResolutionState { + host: ts.ModuleResolutionHost; + compilerOptions: ts.CompilerOptions; + traceEnabled: boolean; + failedLookupLocations: ts.Push; + resultFromCache?: ts.ResolvedModuleWithFailedLookupLocations; + packageJsonInfoCache: PackageJsonInfoCache | undefined; + features: NodeResolutionFeatures; + conditions: string[]; + requestContainingDirectory: string | undefined; + reportDiagnostic: ts.DiagnosticReporter; +} + +/** Just the fields that we use for module resolution. */ +/*@internal*/ +interface PackageJsonPathFields { + typings?: string; + types?: string; + typesVersions?: ts.MapLike>; + main?: string; + tsconfig?: string; + type?: string; + imports?: object; + exports?: object; + name?: 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 (!ts.hasProperty(jsonContent, fieldName)) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_does_not_have_a_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, ts.Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, typeOfTag, value === null ? "null" : typeof value); - } - return; + return; + } + 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, ts.Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, typeOfTag, value === null ? "null" : typeof value); } - return value; + 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, ts.Diagnostics.package_json_had_a_falsy_0_field, fieldName); - } - return; - } - const path = ts.normalizePath(ts.combinePaths(baseDirectory, fileName)); +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, ts.Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path); + trace(state.host, ts.Diagnostics.package_json_had_a_falsy_0_field, fieldName); } - return path; + return; } - - function readPackageJsonTypesFields(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "typings", baseDirectory, state) - || readPackageJsonPathField(jsonContent, "types", baseDirectory, state); + const path = ts.normalizePath(ts.combinePaths(baseDirectory, fileName)); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path); } + return path; +} - function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state); - } +function readPackageJsonTypesFields(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "typings", baseDirectory, state) + || readPackageJsonPathField(jsonContent, "types", baseDirectory, state); +} - function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "main", baseDirectory, state); - } +function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state); +} - function readPackageJsonTypesVersionsField(jsonContent: PackageJson, state: ModuleResolutionState) { - const typesVersions = readPackageJsonField(jsonContent, "typesVersions", "object", state); - if (typesVersions === undefined) - return; +function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "main", baseDirectory, state); +} - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_field_with_version_specific_path_mappings); - } +function readPackageJsonTypesVersionsField(jsonContent: PackageJson, state: ModuleResolutionState) { + const typesVersions = readPackageJsonField(jsonContent, "typesVersions", "object", state); + if (typesVersions === undefined) + return; - return typesVersions; + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_field_with_version_specific_path_mappings); } - /*@internal*/ - interface VersionPaths { - version: string; - paths: ts.MapLike; - } + return typesVersions; +} - function readPackageJsonTypesVersionPaths(jsonContent: PackageJson, state: ModuleResolutionState): VersionPaths | undefined { - const typesVersions = readPackageJsonTypesVersionsField(jsonContent, state); - if (typesVersions === undefined) - return; +/*@internal*/ +interface VersionPaths { + version: string; + paths: ts.MapLike; +} - if (state.traceEnabled) { - for (const key in typesVersions) { - if (ts.hasProperty(typesVersions, key) && !ts.VersionRange.tryParse(key)) { - trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range, key); - } - } - } +function readPackageJsonTypesVersionPaths(jsonContent: PackageJson, state: ModuleResolutionState): VersionPaths | undefined { + const typesVersions = readPackageJsonTypesVersionsField(jsonContent, state); + if (typesVersions === undefined) + return; - const result = getPackageJsonTypesVersionsPaths(typesVersions); - if (!result) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_does_not_have_a_typesVersions_entry_that_matches_version_0, ts.versionMajorMinor); + if (state.traceEnabled) { + for (const key in typesVersions) { + if (ts.hasProperty(typesVersions, key) && !ts.VersionRange.tryParse(key)) { + trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range, key); } - return; } + } - const { version: bestVersionKey, paths: bestVersionPaths } = result; - if (typeof bestVersionPaths !== "object") { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, `typesVersions['${bestVersionKey}']`, "object", typeof bestVersionPaths); - } - return; + const result = getPackageJsonTypesVersionsPaths(typesVersions); + if (!result) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_does_not_have_a_typesVersions_entry_that_matches_version_0, ts.versionMajorMinor); } - - return result; + return; } - let typeScriptVersion: ts.Version | undefined; + const { version: bestVersionKey, paths: bestVersionPaths } = result; + if (typeof bestVersionPaths !== "object") { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, `typesVersions['${bestVersionKey}']`, "object", typeof bestVersionPaths); + } + return; + } - /* @internal */ - export function getPackageJsonTypesVersionsPaths(typesVersions: ts.MapLike>) { - if (!typeScriptVersion) - typeScriptVersion = new ts.Version(ts.version); + return result; +} - for (const key in typesVersions) { - if (!ts.hasProperty(typesVersions, key)) - continue; - const keyRange = ts.VersionRange.tryParse(key); - if (keyRange === undefined) { - continue; - } +let typeScriptVersion: ts.Version | undefined; - // return the first entry whose range matches the current compiler version. - if (keyRange.test(typeScriptVersion)) { - return { version: key, paths: typesVersions[key] }; - } - } - } +/* @internal */ +export function getPackageJsonTypesVersionsPaths(typesVersions: ts.MapLike>) { + if (!typeScriptVersion) + typeScriptVersion = new ts.Version(ts.version); - export function getEffectiveTypeRoots(options: ts.CompilerOptions, host: ts.GetEffectiveTypeRootsHost): string[] | undefined { - if (options.typeRoots) { - return options.typeRoots; + for (const key in typesVersions) { + if (!ts.hasProperty(typesVersions, key)) + continue; + const keyRange = ts.VersionRange.tryParse(key); + if (keyRange === undefined) { + continue; } - let currentDirectory: string | undefined; - if (options.configFilePath) { - currentDirectory = ts.getDirectoryPath(options.configFilePath); - } - else if (host.getCurrentDirectory) { - currentDirectory = host.getCurrentDirectory(); + // return the first entry whose range matches the current compiler version. + if (keyRange.test(typeScriptVersion)) { + return { version: key, paths: typesVersions[key] }; } + } +} - if (currentDirectory !== undefined) { - return getDefaultTypeRoots(currentDirectory, host); - } +export function getEffectiveTypeRoots(options: ts.CompilerOptions, host: ts.GetEffectiveTypeRootsHost): string[] | undefined { + if (options.typeRoots) { + return options.typeRoots; } - /** - * 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 [ts.combinePaths(currentDirectory, nodeModulesAtTypes)]; - // And if it doesn't exist, tough. - } + let currentDirectory: string | undefined; + if (options.configFilePath) { + currentDirectory = ts.getDirectoryPath(options.configFilePath); + } + else if (host.getCurrentDirectory) { + currentDirectory = host.getCurrentDirectory(); + } - let typeRoots: string[] | undefined; - ts.forEachAncestorDirectory(ts.normalizePath(currentDirectory), directory => { - const atTypes = ts.combinePaths(directory, nodeModulesAtTypes); - if (host.directoryExists!(atTypes)) { - (typeRoots || (typeRoots = [])).push(atTypes); - } - return undefined; - }); - return typeRoots; + if (currentDirectory !== undefined) { + return getDefaultTypeRoots(currentDirectory, host); } - const nodeModulesAtTypes = ts.combinePaths("node_modules", "@types"); - function arePathsEqual(path1: string, path2: string, host: ts.ModuleResolutionHost): boolean { - const useCaseSensitiveFileNames = typeof host.useCaseSensitiveFileNames === "function" ? host.useCaseSensitiveFileNames() : host.useCaseSensitiveFileNames; - return ts.comparePaths(path1, path2, !useCaseSensitiveFileNames) === ts.Comparison.EqualTo; +} + +/** + * 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 [ts.combinePaths(currentDirectory, nodeModulesAtTypes)]; + // And if it doesn't exist, tough. } - /** - * @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: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache, resolutionMode?: ts.SourceFile["impliedNodeFormat"]): ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations { - ts.Debug.assert(typeof typeReferenceDirectiveName === "string", "Non-string value passed to `ts.resolveTypeReferenceDirective`, likely by a wrapping package working with an outdated `resolveTypeReferenceDirectives` signature. This is probably not a problem in TS itself."); - const traceEnabled = isTraceEnabled(options, host); - if (redirectedReference) { - options = redirectedReference.commandLine.options; + let typeRoots: string[] | undefined; + ts.forEachAncestorDirectory(ts.normalizePath(currentDirectory), directory => { + const atTypes = ts.combinePaths(directory, nodeModulesAtTypes); + if (host.directoryExists!(atTypes)) { + (typeRoots || (typeRoots = [])).push(atTypes); } + return undefined; + }); + return typeRoots; +} +const nodeModulesAtTypes = ts.combinePaths("node_modules", "@types"); +function arePathsEqual(path1: string, path2: string, host: ts.ModuleResolutionHost): boolean { + const useCaseSensitiveFileNames = typeof host.useCaseSensitiveFileNames === "function" ? host.useCaseSensitiveFileNames() : host.useCaseSensitiveFileNames; + return ts.comparePaths(path1, path2, !useCaseSensitiveFileNames) === ts.Comparison.EqualTo; +} - const containingDirectory = containingFile ? ts.getDirectoryPath(containingFile) : undefined; - const perFolderCache = containingDirectory ? cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference) : undefined; - let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ resolutionMode); - if (result) { - if (traceEnabled) { - trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1, typeReferenceDirectiveName, containingFile); - if (redirectedReference) - trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); - trace(host, ts.Diagnostics.Resolution_for_type_reference_directive_0_was_found_in_cache_from_location_1, typeReferenceDirectiveName, containingDirectory); - traceResult(result); - } - return result; +/** + * @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: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache, resolutionMode?: ts.SourceFile["impliedNodeFormat"]): ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations { + ts.Debug.assert(typeof typeReferenceDirectiveName === "string", "Non-string value passed to `ts.resolveTypeReferenceDirective`, likely by a wrapping package working with an outdated `resolveTypeReferenceDirectives` signature. This is probably not a problem in TS itself."); + const traceEnabled = isTraceEnabled(options, host); + if (redirectedReference) { + options = redirectedReference.commandLine.options; + } + + const containingDirectory = containingFile ? ts.getDirectoryPath(containingFile) : undefined; + const perFolderCache = containingDirectory ? cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference) : undefined; + let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ resolutionMode); + if (result) { + if (traceEnabled) { + trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1, typeReferenceDirectiveName, containingFile); + if (redirectedReference) + trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); + trace(host, ts.Diagnostics.Resolution_for_type_reference_directive_0_was_found_in_cache_from_location_1, typeReferenceDirectiveName, containingDirectory); + traceResult(result); } + return result; + } - const typeRoots = getEffectiveTypeRoots(options, host); - if (traceEnabled) { - if (containingFile === undefined) { - if (typeRoots === undefined) { - trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName); - } - else { - trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots); - } + const typeRoots = getEffectiveTypeRoots(options, host); + if (traceEnabled) { + if (containingFile === undefined) { + if (typeRoots === undefined) { + trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName); } else { - if (typeRoots === undefined) { - trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile); - } - else { - trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots); - } + trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots); } - if (redirectedReference) { - trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); + } + else { + if (typeRoots === undefined) { + trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile); + } + else { + trace(host, ts.Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots); } } - - const failedLookupLocations: string[] = []; - let features = getDefaultNodeResolutionFeatures(options); - // Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution - // setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being - // set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports - // are present in a non-modal project; while in theory we'd like to either ignore the mode or provide faithful modern resolution, depending on what we feel is best, - // in practice, not every cache has the options available to intelligently make the choice to ignore the mode request, and it's unclear how modern "faithful modern - // resolution" should be (`node16`? `nodenext`?). As such, witnessing a mode-overriding triple-slash reference in a non-modal module resolution - // context should _probably_ be an error - and that should likely be handled by the `Program` (which is what we do). - if (resolutionMode === ts.ModuleKind.ESNext && (ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext)) { - features |= NodeResolutionFeatures.EsmMode; - } - const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : []; - const diagnostics: ts.Diagnostic[] = []; - const moduleResolutionState: ModuleResolutionState = { - compilerOptions: options, - host, - traceEnabled, - failedLookupLocations, - packageJsonInfoCache: cache, - features, - conditions, - requestContainingDirectory: containingDirectory, - reportDiagnostic: diag => void diagnostics.push(diag), + if (redirectedReference) { + trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); + } + } + + const failedLookupLocations: string[] = []; + let features = getDefaultNodeResolutionFeatures(options); + // Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution + // setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being + // set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports + // are present in a non-modal project; while in theory we'd like to either ignore the mode or provide faithful modern resolution, depending on what we feel is best, + // in practice, not every cache has the options available to intelligently make the choice to ignore the mode request, and it's unclear how modern "faithful modern + // resolution" should be (`node16`? `nodenext`?). As such, witnessing a mode-overriding triple-slash reference in a non-modal module resolution + // context should _probably_ be an error - and that should likely be handled by the `Program` (which is what we do). + if (resolutionMode === ts.ModuleKind.ESNext && (ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext)) { + features |= NodeResolutionFeatures.EsmMode; + } + const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : []; + const diagnostics: ts.Diagnostic[] = []; + const moduleResolutionState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled, + failedLookupLocations, + packageJsonInfoCache: cache, + features, + conditions, + requestContainingDirectory: containingDirectory, + reportDiagnostic: diag => void diagnostics.push(diag), + }; + let resolved = primaryLookup(); + let primary = true; + if (!resolved) { + resolved = secondaryLookup(); + primary = false; + } + + let resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined; + if (resolved) { + const { fileName, packageId } = resolved; + const resolvedFileName = options.preserveSymlinks ? fileName : realPath(fileName, host, traceEnabled); + resolvedTypeReferenceDirective = { + primary, + resolvedFileName, + originalPath: arePathsEqual(fileName, resolvedFileName, host) ? undefined : fileName, + packageId, + isExternalLibraryImport: pathContainsNodeModules(fileName), }; - let resolved = primaryLookup(); - let primary = true; - if (!resolved) { - resolved = secondaryLookup(); - primary = false; - } + } + result = { resolvedTypeReferenceDirective, failedLookupLocations, resolutionDiagnostics: diagnostics }; + perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ resolutionMode, result); + if (traceEnabled) + traceResult(result); + return result; - let resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined; - if (resolved) { - const { fileName, packageId } = resolved; - const resolvedFileName = options.preserveSymlinks ? fileName : realPath(fileName, host, traceEnabled); - resolvedTypeReferenceDirective = { - primary, - resolvedFileName, - originalPath: arePathsEqual(fileName, resolvedFileName, host) ? undefined : fileName, - packageId, - isExternalLibraryImport: pathContainsNodeModules(fileName), - }; + function traceResult(result: ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations) { + if (!result.resolvedTypeReferenceDirective?.resolvedFileName) { + trace(host, ts.Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName); } - result = { resolvedTypeReferenceDirective, failedLookupLocations, resolutionDiagnostics: diagnostics }; - perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ resolutionMode, result); - if (traceEnabled) - traceResult(result); - return result; - - function traceResult(result: ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations) { - if (!result.resolvedTypeReferenceDirective?.resolvedFileName) { - trace(host, ts.Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName); - } - else if (result.resolvedTypeReferenceDirective.packageId) { - trace(host, ts.Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, ts.packageIdToString(result.resolvedTypeReferenceDirective.packageId), result.resolvedTypeReferenceDirective.primary); - } - else { - trace(host, ts.Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, result.resolvedTypeReferenceDirective.primary); - } + else if (result.resolvedTypeReferenceDirective.packageId) { + trace(host, ts.Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, ts.packageIdToString(result.resolvedTypeReferenceDirective.packageId), result.resolvedTypeReferenceDirective.primary); } + else { + trace(host, ts.Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, result.resolvedTypeReferenceDirective.primary); + } + } - function primaryLookup(): PathAndPackageId | undefined { - // Check primary library paths - if (typeRoots && typeRoots.length) { - if (traceEnabled) { - trace(host, ts.Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", ")); - } - return ts.firstDefined(typeRoots, typeRoot => { - const candidate = ts.combinePaths(typeRoot, typeReferenceDirectiveName); - const candidateDirectory = ts.getDirectoryPath(candidate); - const directoryExists = ts.directoryProbablyExists(candidateDirectory, host); - if (!directoryExists && traceEnabled) { - trace(host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory); - } - return resolvedTypeScriptOnly(loadNodeModuleFromDirectory(Extensions.DtsOnly, candidate, !directoryExists, moduleResolutionState)); - }); + function primaryLookup(): PathAndPackageId | undefined { + // Check primary library paths + if (typeRoots && typeRoots.length) { + if (traceEnabled) { + trace(host, ts.Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", ")); } - else { - if (traceEnabled) { - trace(host, ts.Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths); + return ts.firstDefined(typeRoots, typeRoot => { + const candidate = ts.combinePaths(typeRoot, typeReferenceDirectiveName); + const candidateDirectory = ts.getDirectoryPath(candidate); + const directoryExists = ts.directoryProbablyExists(candidateDirectory, host); + if (!directoryExists && traceEnabled) { + trace(host, ts.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, ts.Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths); } } + } - function secondaryLookup(): PathAndPackageId | undefined { - const initialLocationForSecondaryLookup = containingFile && ts.getDirectoryPath(containingFile); + function secondaryLookup(): PathAndPackageId | undefined { + const initialLocationForSecondaryLookup = containingFile && ts.getDirectoryPath(containingFile); - if (initialLocationForSecondaryLookup !== undefined) { - // check secondary locations - if (traceEnabled) { - trace(host, ts.Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup); - } - let result: Resolved | undefined; - if (!ts.isExternalModuleNameRelative(typeReferenceDirectiveName)) { - const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined); - result = searchResult && searchResult.value; - } - else { - const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName); - result = nodeLoadModuleByRelativeName(Extensions.DtsOnly, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true); - } - return resolvedTypeScriptOnly(result); + if (initialLocationForSecondaryLookup !== undefined) { + // check secondary locations + if (traceEnabled) { + trace(host, ts.Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup); + } + let result: Resolved | undefined; + if (!ts.isExternalModuleNameRelative(typeReferenceDirectiveName)) { + const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined); + result = searchResult && searchResult.value; } else { - if (traceEnabled) { - trace(host, ts.Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder); - } + const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName); + result = nodeLoadModuleByRelativeName(Extensions.DtsOnly, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true); + } + return resolvedTypeScriptOnly(result); + } + else { + if (traceEnabled) { + trace(host, ts.Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder); } } } +} - function getDefaultNodeResolutionFeatures(options: ts.CompilerOptions) { - return ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 ? NodeResolutionFeatures.Node16Default : - ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : - NodeResolutionFeatures.None; - } - - /** - * @internal - * Does not try `@types/${packageName}` - use a second pass if needed. - */ - export function resolvePackageNameToPackageJson(packageName: string, containingDirectory: string, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined): PackageJsonInfo | undefined { - const moduleResolutionState: ModuleResolutionState = { - compilerOptions: options, - host, - traceEnabled: isTraceEnabled(options, host), - failedLookupLocations: [], - packageJsonInfoCache: cache?.getPackageJsonInfoCache(), - conditions: ts.emptyArray, - features: NodeResolutionFeatures.None, - requestContainingDirectory: containingDirectory, - reportDiagnostic: ts.noop - }; +function getDefaultNodeResolutionFeatures(options: ts.CompilerOptions) { + return ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 ? NodeResolutionFeatures.Node16Default : + ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : + NodeResolutionFeatures.None; +} - return ts.forEachAncestorDirectory(containingDirectory, ancestorDirectory => { - if (ts.getBaseFileName(ancestorDirectory) !== "node_modules") { - const nodeModulesFolder = ts.combinePaths(ancestorDirectory, "node_modules"); - const candidate = ts.combinePaths(nodeModulesFolder, packageName); - return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState); - } - }); - } +/** + * @internal + * Does not try `@types/${packageName}` - use a second pass if needed. + */ +export function resolvePackageNameToPackageJson(packageName: string, containingDirectory: string, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined): PackageJsonInfo | undefined { + const moduleResolutionState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: ts.emptyArray, + features: NodeResolutionFeatures.None, + requestContainingDirectory: containingDirectory, + reportDiagnostic: ts.noop + }; + + return ts.forEachAncestorDirectory(containingDirectory, ancestorDirectory => { + if (ts.getBaseFileName(ancestorDirectory) !== "node_modules") { + const nodeModulesFolder = ts.combinePaths(ancestorDirectory, "node_modules"); + const candidate = ts.combinePaths(nodeModulesFolder, packageName); + return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState); + } + }); +} - /** - * 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: ts.CompilerOptions, host: ts.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 = ts.normalizePath(typeDirectivePath); - const packageJsonPath = ts.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) && (ts.readJson(packageJsonPath, host) as PackageJson).typings === null; - if (!isNotNeededPackage) { - const baseFileName = ts.getBaseFileName(normalized); - - // At this stage, skip results with leading dot. - if (baseFileName.charCodeAt(0) !== ts.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: ts.CompilerOptions, host: ts.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 = ts.normalizePath(typeDirectivePath); + const packageJsonPath = ts.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) && (ts.readJson(packageJsonPath, host) as PackageJson).typings === null; + if (!isNotNeededPackage) { + const baseFileName = ts.getBaseFileName(normalized); + + // At this stage, skip results with leading dot. + if (baseFileName.charCodeAt(0) !== ts.CharacterCodes.dot) { + // Return just the type directive names + result.push(baseFileName); } } } } } } - return result; } + return result; +} - export interface TypeReferenceDirectiveResolutionCache extends PerDirectoryResolutionCache, PackageJsonInfoCache { - } +export interface TypeReferenceDirectiveResolutionCache extends PerDirectoryResolutionCache, PackageJsonInfoCache { +} - export interface ModeAwareCache { - get(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): T | undefined; - set(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, value: T): this; - delete(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): this; - has(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): boolean; - forEach(cb: (elem: T, key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) => void): void; - size(): number; - } +export interface ModeAwareCache { + get(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): T | undefined; + set(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, value: T): this; + delete(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): this; + has(key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): boolean; + forEach(cb: (elem: T, key: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) => void): void; + size(): number; +} +/** + * Cached 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 PerDirectoryResolutionCache { + getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ts.ResolvedProjectReference): ModeAwareCache; + clear(): void; /** - * Cached resolutions per containing directory. - * This assumes that any module id will have the same resolution for sibling files located in the same folder. + * Updates with the current compilerOptions the cache will operate with. + * This updates the redirects map as well if needed so module resolutions are cached if they can across the projects */ - export interface PerDirectoryResolutionCache { - getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ts.ResolvedProjectReference): ModeAwareCache; - clear(): void; - /** - * Updates with the current compilerOptions the cache will operate with. - * This updates the redirects map as well if needed so module resolutions are cached if they can across the projects - */ - update(options: ts.CompilerOptions): void; - } + update(options: ts.CompilerOptions): void; +} - export interface ModuleResolutionCache extends PerDirectoryResolutionCache, NonRelativeModuleNameResolutionCache, PackageJsonInfoCache { - getPackageJsonInfoCache(): PackageJsonInfoCache; - } +export interface ModuleResolutionCache extends PerDirectoryResolutionCache, NonRelativeModuleNameResolutionCache, PackageJsonInfoCache { + getPackageJsonInfoCache(): PackageJsonInfoCache; +} - /** - * 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 extends PackageJsonInfoCache { - getOrCreateCacheForModuleName(nonRelativeModuleName: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, redirectedReference?: ts.ResolvedProjectReference): PerModuleNameCache; - } - - export interface PackageJsonInfoCache { - /*@internal*/ getPackageJsonInfo(packageJsonPath: string): PackageJsonInfo | boolean | undefined; - /*@internal*/ setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean): void; - /*@internal*/ entries(): [ - ts.Path, - PackageJsonInfo | boolean - ][]; - clear(): void; - } - - export interface PerModuleNameCache { - get(directory: string): ts.ResolvedModuleWithFailedLookupLocations | undefined; - set(directory: string, result: ts.ResolvedModuleWithFailedLookupLocations): void; - } - - /*@internal*/ - export interface CacheWithRedirects { - getOwnMap: () => ts.ESMap; - redirectsMap: ts.ESMap>; - getOrCreateMapOfCacheRedirects(redirectedReference: ts.ResolvedProjectReference | undefined): ts.ESMap; - clear(): void; - setOwnOptions(newOptions: ts.CompilerOptions): void; - setOwnMap(newOwnMap: ts.ESMap): void; - } - - /*@internal*/ - export function createCacheWithRedirects(options?: ts.CompilerOptions): CacheWithRedirects { - let ownMap: ts.ESMap = new ts.Map(); - const redirectsMap = new ts.Map>(); - return { - getOwnMap, - redirectsMap, - getOrCreateMapOfCacheRedirects, - clear, - setOwnOptions, - setOwnMap - }; +/** + * 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 extends PackageJsonInfoCache { + getOrCreateCacheForModuleName(nonRelativeModuleName: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, redirectedReference?: ts.ResolvedProjectReference): PerModuleNameCache; +} - function getOwnMap() { - return ownMap; - } +export interface PackageJsonInfoCache { + /*@internal*/ getPackageJsonInfo(packageJsonPath: string): PackageJsonInfo | boolean | undefined; + /*@internal*/ setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean): void; + /*@internal*/ entries(): [ + ts.Path, + PackageJsonInfo | boolean + ][]; + clear(): void; +} - function setOwnOptions(newOptions: ts.CompilerOptions) { - options = newOptions; - } +export interface PerModuleNameCache { + get(directory: string): ts.ResolvedModuleWithFailedLookupLocations | undefined; + set(directory: string, result: ts.ResolvedModuleWithFailedLookupLocations): void; +} - function setOwnMap(newOwnMap: ts.ESMap) { - ownMap = newOwnMap; - } +/*@internal*/ +export interface CacheWithRedirects { + getOwnMap: () => ts.ESMap; + redirectsMap: ts.ESMap>; + getOrCreateMapOfCacheRedirects(redirectedReference: ts.ResolvedProjectReference | undefined): ts.ESMap; + clear(): void; + setOwnOptions(newOptions: ts.CompilerOptions): void; + setOwnMap(newOwnMap: ts.ESMap): void; +} - function getOrCreateMapOfCacheRedirects(redirectedReference: ts.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 || ts.optionsHaveModuleResolutionChanges(options, redirectedReference.commandLine.options) ? new ts.Map() : ownMap; - redirectsMap.set(path, redirects); - } - return redirects; - } +/*@internal*/ +export function createCacheWithRedirects(options?: ts.CompilerOptions): CacheWithRedirects { + let ownMap: ts.ESMap = new ts.Map(); + const redirectsMap = new ts.Map>(); + return { + getOwnMap, + redirectsMap, + getOrCreateMapOfCacheRedirects, + clear, + setOwnOptions, + setOwnMap + }; - function clear() { - ownMap.clear(); - redirectsMap.clear(); - } + function getOwnMap() { + return ownMap; } - function createPackageJsonInfoCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): PackageJsonInfoCache { - let cache: ts.ESMap | undefined; - return { getPackageJsonInfo, setPackageJsonInfo, clear, entries }; - function getPackageJsonInfo(packageJsonPath: string) { - return cache?.get(ts.toPath(packageJsonPath, currentDirectory, getCanonicalFileName)); - } - function setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean) { - (cache ||= new ts.Map()).set(ts.toPath(packageJsonPath, currentDirectory, getCanonicalFileName), info); - } - function clear() { - cache = undefined; - } - function entries() { - const iter = cache?.entries(); - return iter ? ts.arrayFrom(iter) : []; - } + function setOwnOptions(newOptions: ts.CompilerOptions) { + options = newOptions; } - function getOrCreateCache(cacheWithRedirects: CacheWithRedirects, redirectedReference: ts.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 setOwnMap(newOwnMap: ts.ESMap) { + ownMap = newOwnMap; } - function updateRedirectsMap(options: ts.CompilerOptions, directoryToModuleNameMap: CacheWithRedirects>, moduleNameToDirectoryMap?: CacheWithRedirects) { - if (!options.configFile) - return; - if (directoryToModuleNameMap.redirectsMap.size === 0) { - // The own map will be for projectCompilerOptions - ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size === 0); - ts.Debug.assert(directoryToModuleNameMap.getOwnMap().size === 0); - ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.getOwnMap().size === 0); - directoryToModuleNameMap.redirectsMap.set(options.configFile.path, directoryToModuleNameMap.getOwnMap()); - moduleNameToDirectoryMap?.redirectsMap.set(options.configFile.path, moduleNameToDirectoryMap.getOwnMap()); + function getOrCreateMapOfCacheRedirects(redirectedReference: ts.ResolvedProjectReference | undefined) { + if (!redirectedReference) { + return ownMap; } - else { - // Set correct own map - ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size > 0); - const ref: ts.ResolvedProjectReference = { - sourceFile: options.configFile, - commandLine: { options } as ts.ParsedCommandLine - }; - directoryToModuleNameMap.setOwnMap(directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); - moduleNameToDirectoryMap?.setOwnMap(moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); + const path = redirectedReference.sourceFile.path; + let redirects = redirectsMap.get(path); + if (!redirects) { + // Reuse map if redirected reference map uses same resolution + redirects = !options || ts.optionsHaveModuleResolutionChanges(options, redirectedReference.commandLine.options) ? new ts.Map() : ownMap; + redirectsMap.set(path, redirects); } - directoryToModuleNameMap.setOwnOptions(options); - moduleNameToDirectoryMap?.setOwnOptions(options); + return redirects; } - function createPerDirectoryResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, directoryToModuleNameMap: CacheWithRedirects>): PerDirectoryResolutionCache { - return { - getOrCreateCacheForDirectory, - clear, - update, - }; + function clear() { + ownMap.clear(); + redirectsMap.clear(); + } +} - function clear() { - directoryToModuleNameMap.clear(); - } - - function update(options: ts.CompilerOptions) { - updateRedirectsMap(options, directoryToModuleNameMap); - } - - function getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ts.ResolvedProjectReference) { - const path = ts.toPath(directoryName, currentDirectory, getCanonicalFileName); - return getOrCreateCache>(directoryToModuleNameMap, redirectedReference, path, () => createModeAwareCache()); - } - } - - /* @internal */ - export function createModeAwareCache(): ModeAwareCache { - const underlying = new ts.Map(); - const memoizedReverseKeys = new ts.Map(); - - const cache: ModeAwareCache = { - get(specifier, mode) { - return underlying.get(getUnderlyingCacheKey(specifier, mode)); - }, - set(specifier, mode, value) { - underlying.set(getUnderlyingCacheKey(specifier, mode), value); - return cache; - }, - delete(specifier, mode) { - underlying.delete(getUnderlyingCacheKey(specifier, mode)); - return cache; - }, - has(specifier, mode) { - return underlying.has(getUnderlyingCacheKey(specifier, mode)); - }, - forEach(cb) { - return underlying.forEach((elem, key) => { - const [specifier, mode] = memoizedReverseKeys.get(key)!; - return cb(elem, specifier, mode); - }); - }, - size() { - return underlying.size; - } - }; - return cache; +function createPackageJsonInfoCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): PackageJsonInfoCache { + let cache: ts.ESMap | undefined; + return { getPackageJsonInfo, setPackageJsonInfo, clear, entries }; + function getPackageJsonInfo(packageJsonPath: string) { + return cache?.get(ts.toPath(packageJsonPath, currentDirectory, getCanonicalFileName)); + } + function setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean) { + (cache ||= new ts.Map()).set(ts.toPath(packageJsonPath, currentDirectory, getCanonicalFileName), info); + } + function clear() { + cache = undefined; + } + function entries() { + const iter = cache?.entries(); + return iter ? ts.arrayFrom(iter) : []; + } +} - function getUnderlyingCacheKey(specifier: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) { - const result = mode === undefined ? specifier : `${mode}|${specifier}`; - memoizedReverseKeys.set(result, [specifier, mode]); - return result; - } +function getOrCreateCache(cacheWithRedirects: CacheWithRedirects, redirectedReference: ts.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; +} - /* @internal */ - export function zipToModeAwareCache(file: ts.SourceFile, keys: readonly string[] | readonly ts.FileReference[], values: readonly V[]): ModeAwareCache { - ts.Debug.assert(keys.length === values.length); - const map = createModeAwareCache(); - for (let i = 0; i < keys.length; ++i) { - const entry = keys[i]; - // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. - const name = !ts.isString(entry) ? entry.fileName.toLowerCase() : entry; - const mode = !ts.isString(entry) ? entry.resolutionMode || file.impliedNodeFormat : ts.getModeForResolutionAtIndex(file, i); - map.set(name, mode, values[i]); - } - return map; - } - - export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: ts.CompilerOptions): ModuleResolutionCache; - /*@internal*/ - export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options: undefined, directoryToModuleNameMap: CacheWithRedirects>, moduleNameToDirectoryMap: CacheWithRedirects): ModuleResolutionCache; - export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options?: ts.CompilerOptions, directoryToModuleNameMap?: CacheWithRedirects>, moduleNameToDirectoryMap?: CacheWithRedirects): ModuleResolutionCache { - const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options)); - moduleNameToDirectoryMap ||= createCacheWithRedirects(options); - const packageJsonInfoCache = createPackageJsonInfoCache(currentDirectory, getCanonicalFileName); - - return { - ...packageJsonInfoCache, - ...preDirectoryResolutionCache, - getOrCreateCacheForModuleName, - clear, - update, - getPackageJsonInfoCache: () => packageJsonInfoCache, +function updateRedirectsMap(options: ts.CompilerOptions, directoryToModuleNameMap: CacheWithRedirects>, moduleNameToDirectoryMap?: CacheWithRedirects) { + if (!options.configFile) + return; + if (directoryToModuleNameMap.redirectsMap.size === 0) { + // The own map will be for projectCompilerOptions + ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size === 0); + ts.Debug.assert(directoryToModuleNameMap.getOwnMap().size === 0); + ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.getOwnMap().size === 0); + directoryToModuleNameMap.redirectsMap.set(options.configFile.path, directoryToModuleNameMap.getOwnMap()); + moduleNameToDirectoryMap?.redirectsMap.set(options.configFile.path, moduleNameToDirectoryMap.getOwnMap()); + } + else { + // Set correct own map + ts.Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size > 0); + const ref: ts.ResolvedProjectReference = { + sourceFile: options.configFile, + commandLine: { options } as ts.ParsedCommandLine }; + directoryToModuleNameMap.setOwnMap(directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); + moduleNameToDirectoryMap?.setOwnMap(moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); + } + directoryToModuleNameMap.setOwnOptions(options); + moduleNameToDirectoryMap?.setOwnOptions(options); +} - function clear() { - preDirectoryResolutionCache.clear(); - moduleNameToDirectoryMap!.clear(); - packageJsonInfoCache.clear(); - } +function createPerDirectoryResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, directoryToModuleNameMap: CacheWithRedirects>): PerDirectoryResolutionCache { + return { + getOrCreateCacheForDirectory, + clear, + update, + }; - function update(options: ts.CompilerOptions) { - updateRedirectsMap(options, directoryToModuleNameMap!, moduleNameToDirectoryMap); - } + function clear() { + directoryToModuleNameMap.clear(); + } + + function update(options: ts.CompilerOptions) { + updateRedirectsMap(options, directoryToModuleNameMap); + } + + function getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ts.ResolvedProjectReference) { + const path = ts.toPath(directoryName, currentDirectory, getCanonicalFileName); + return getOrCreateCache>(directoryToModuleNameMap, redirectedReference, path, () => createModeAwareCache()); + } +} - function getOrCreateCacheForModuleName(nonRelativeModuleName: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, redirectedReference?: ts.ResolvedProjectReference): PerModuleNameCache { - ts.Debug.assert(!ts.isExternalModuleNameRelative(nonRelativeModuleName)); - return getOrCreateCache(moduleNameToDirectoryMap!, redirectedReference, mode === undefined ? nonRelativeModuleName : `${mode}|${nonRelativeModuleName}`, createPerModuleNameCache); +/* @internal */ +export function createModeAwareCache(): ModeAwareCache { + const underlying = new ts.Map(); + const memoizedReverseKeys = new ts.Map(); + + const cache: ModeAwareCache = { + get(specifier, mode) { + return underlying.get(getUnderlyingCacheKey(specifier, mode)); + }, + set(specifier, mode, value) { + underlying.set(getUnderlyingCacheKey(specifier, mode), value); + return cache; + }, + delete(specifier, mode) { + underlying.delete(getUnderlyingCacheKey(specifier, mode)); + return cache; + }, + has(specifier, mode) { + return underlying.has(getUnderlyingCacheKey(specifier, mode)); + }, + forEach(cb) { + return underlying.forEach((elem, key) => { + const [specifier, mode] = memoizedReverseKeys.get(key)!; + return cb(elem, specifier, mode); + }); + }, + size() { + return underlying.size; } + }; + return cache; - function createPerModuleNameCache(): PerModuleNameCache { - const directoryPathMap = new ts.Map(); + function getUnderlyingCacheKey(specifier: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) { + const result = mode === undefined ? specifier : `${mode}|${specifier}`; + memoizedReverseKeys.set(result, [specifier, mode]); + return result; + } +} - return { get, set }; +/* @internal */ +export function zipToModeAwareCache(file: ts.SourceFile, keys: readonly string[] | readonly ts.FileReference[], values: readonly V[]): ModeAwareCache { + ts.Debug.assert(keys.length === values.length); + const map = createModeAwareCache(); + for (let i = 0; i < keys.length; ++i) { + const entry = keys[i]; + // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. + const name = !ts.isString(entry) ? entry.fileName.toLowerCase() : entry; + const mode = !ts.isString(entry) ? entry.resolutionMode || file.impliedNodeFormat : ts.getModeForResolutionAtIndex(file, i); + map.set(name, mode, values[i]); + } + return map; +} - function get(directory: string): ts.ResolvedModuleWithFailedLookupLocations | undefined { - return directoryPathMap.get(ts.toPath(directory, currentDirectory, getCanonicalFileName)); - } +export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: ts.CompilerOptions): ModuleResolutionCache; +/*@internal*/ +export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options: undefined, directoryToModuleNameMap: CacheWithRedirects>, moduleNameToDirectoryMap: CacheWithRedirects): ModuleResolutionCache; +export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options?: ts.CompilerOptions, directoryToModuleNameMap?: CacheWithRedirects>, moduleNameToDirectoryMap?: CacheWithRedirects): ModuleResolutionCache { + const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options)); + moduleNameToDirectoryMap ||= createCacheWithRedirects(options); + const packageJsonInfoCache = createPackageJsonInfoCache(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: ts.ResolvedModuleWithFailedLookupLocations): void { - const path = ts.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 = ts.getDirectoryPath(current); - if (parent === current || directoryPathMap.has(parent)) { - break; - } - directoryPathMap.set(parent, result); - current = parent; + return { + ...packageJsonInfoCache, + ...preDirectoryResolutionCache, + getOrCreateCacheForModuleName, + clear, + update, + getPackageJsonInfoCache: () => packageJsonInfoCache, + }; + + function clear() { + preDirectoryResolutionCache.clear(); + moduleNameToDirectoryMap!.clear(); + packageJsonInfoCache.clear(); + } + + function update(options: ts.CompilerOptions) { + updateRedirectsMap(options, directoryToModuleNameMap!, moduleNameToDirectoryMap); + } + + function getOrCreateCacheForModuleName(nonRelativeModuleName: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, redirectedReference?: ts.ResolvedProjectReference): PerModuleNameCache { + ts.Debug.assert(!ts.isExternalModuleNameRelative(nonRelativeModuleName)); + return getOrCreateCache(moduleNameToDirectoryMap!, redirectedReference, mode === undefined ? nonRelativeModuleName : `${mode}|${nonRelativeModuleName}`, createPerModuleNameCache); + } + + function createPerModuleNameCache(): PerModuleNameCache { + const directoryPathMap = new ts.Map(); + + return { get, set }; + + function get(directory: string): ts.ResolvedModuleWithFailedLookupLocations | undefined { + return directoryPathMap.get(ts.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: ts.ResolvedModuleWithFailedLookupLocations): void { + const path = ts.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 = ts.getDirectoryPath(current); + if (parent === current || directoryPathMap.has(parent)) { + break; } + directoryPathMap.set(parent, result); + current = parent; } + } - function getCommonPrefix(directory: ts.Path, resolution: string) { - const resolutionDirectory = ts.toPath(ts.getDirectoryPath(resolution), currentDirectory, getCanonicalFileName); + function getCommonPrefix(directory: ts.Path, resolution: string) { + const resolutionDirectory = ts.toPath(ts.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] === ts.directorySeparator)) { - return directory; - } - const rootLength = ts.getRootLength(directory); - if (i < rootLength) { - return undefined; - } - const sep = directory.lastIndexOf(ts.directorySeparator, i - 1); - if (sep === -1) { - return undefined; - } - return directory.substr(0, Math.max(sep, rootLength)); + // 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] === ts.directorySeparator)) { + return directory; + } + const rootLength = ts.getRootLength(directory); + if (i < rootLength) { + return undefined; } + const sep = directory.lastIndexOf(ts.directorySeparator, i - 1); + if (sep === -1) { + return undefined; + } + return directory.substr(0, Math.max(sep, rootLength)); } } +} - export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: ts.CompilerOptions, packageJsonInfoCache?: PackageJsonInfoCache): TypeReferenceDirectiveResolutionCache; - /*@internal*/ - export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options: undefined, packageJsonInfoCache: PackageJsonInfoCache | undefined, directoryToModuleNameMap: CacheWithRedirects>): TypeReferenceDirectiveResolutionCache; - export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options?: ts.CompilerOptions, packageJsonInfoCache?: PackageJsonInfoCache | undefined, directoryToModuleNameMap?: CacheWithRedirects>): TypeReferenceDirectiveResolutionCache { - const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options)); - packageJsonInfoCache ||= createPackageJsonInfoCache(currentDirectory, getCanonicalFileName); +export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: ts.CompilerOptions, packageJsonInfoCache?: PackageJsonInfoCache): TypeReferenceDirectiveResolutionCache; +/*@internal*/ +export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options: undefined, packageJsonInfoCache: PackageJsonInfoCache | undefined, directoryToModuleNameMap: CacheWithRedirects>): TypeReferenceDirectiveResolutionCache; +export function createTypeReferenceDirectiveResolutionCache(currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, options?: ts.CompilerOptions, packageJsonInfoCache?: PackageJsonInfoCache | undefined, directoryToModuleNameMap?: CacheWithRedirects>): TypeReferenceDirectiveResolutionCache { + const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options)); + packageJsonInfoCache ||= createPackageJsonInfoCache(currentDirectory, getCanonicalFileName); - return { - ...packageJsonInfoCache, - ...preDirectoryResolutionCache, - clear, - }; + return { + ...packageJsonInfoCache, + ...preDirectoryResolutionCache, + clear, + }; - function clear() { - preDirectoryResolutionCache.clear(); - packageJsonInfoCache!.clear(); - } + function clear() { + preDirectoryResolutionCache.clear(); + packageJsonInfoCache!.clear(); } +} - export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined { - const containingDirectory = ts.getDirectoryPath(containingFile); - const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory); - if (!perFolderCache) - return undefined; - return perFolderCache.get(moduleName, mode); - } +export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined { + const containingDirectory = ts.getDirectoryPath(containingFile); + const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory); + if (!perFolderCache) + return undefined; + return perFolderCache.get(moduleName, mode); +} - export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); +export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + if (redirectedReference) { + compilerOptions = redirectedReference.commandLine.options; + } + if (traceEnabled) { + trace(host, ts.Diagnostics.Resolving_module_0_from_1, moduleName, containingFile); if (redirectedReference) { - compilerOptions = redirectedReference.commandLine.options; + trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); } - if (traceEnabled) { - trace(host, ts.Diagnostics.Resolving_module_0_from_1, moduleName, containingFile); - if (redirectedReference) { - trace(host, ts.Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); - } - } - const containingDirectory = ts.getDirectoryPath(containingFile); - const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference); - let result = perFolderCache && perFolderCache.get(moduleName, resolutionMode); + } + const containingDirectory = ts.getDirectoryPath(containingFile); + const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference); + let result = perFolderCache && perFolderCache.get(moduleName, resolutionMode); - if (result) { - if (traceEnabled) { - trace(host, ts.Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); - } + if (result) { + if (traceEnabled) { + trace(host, ts.Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); } - else { - let moduleResolution = compilerOptions.moduleResolution; - if (moduleResolution === undefined) { - switch (ts.getEmitModuleKind(compilerOptions)) { - case ts.ModuleKind.CommonJS: - moduleResolution = ts.ModuleResolutionKind.NodeJs; - break; - case ts.ModuleKind.Node16: - moduleResolution = ts.ModuleResolutionKind.Node16; - break; - case ts.ModuleKind.NodeNext: - moduleResolution = ts.ModuleResolutionKind.NodeNext; - break; - default: - moduleResolution = ts.ModuleResolutionKind.Classic; - break; - } - if (traceEnabled) { - trace(host, ts.Diagnostics.Module_resolution_kind_is_not_specified_using_0, ts.ModuleResolutionKind[moduleResolution]); - } - } - else { - if (traceEnabled) { - trace(host, ts.Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ts.ModuleResolutionKind[moduleResolution]); - } - } - - ts.perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/); - switch (moduleResolution) { - case ts.ModuleResolutionKind.Node16: - result = node16ModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); - break; - case ts.ModuleResolutionKind.NodeNext: - result = nodeNextModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); + } + else { + let moduleResolution = compilerOptions.moduleResolution; + if (moduleResolution === undefined) { + switch (ts.getEmitModuleKind(compilerOptions)) { + case ts.ModuleKind.CommonJS: + moduleResolution = ts.ModuleResolutionKind.NodeJs; break; - case ts.ModuleResolutionKind.NodeJs: - result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + case ts.ModuleKind.Node16: + moduleResolution = ts.ModuleResolutionKind.Node16; break; - case ts.ModuleResolutionKind.Classic: - result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + case ts.ModuleKind.NodeNext: + moduleResolution = ts.ModuleResolutionKind.NodeNext; break; default: - return ts.Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`); + moduleResolution = ts.ModuleResolutionKind.Classic; + break; } - if (result && result.resolvedModule) - ts.perfLogger.logInfoEvent(`Module "${moduleName}" resolved to "${result.resolvedModule.resolvedFileName}"`); - ts.perfLogger.logStopResolveModule((result && result.resolvedModule) ? "" + result.resolvedModule.resolvedFileName : "null"); - - if (perFolderCache) { - perFolderCache.set(moduleName, resolutionMode, result); - if (!ts.isExternalModuleNameRelative(moduleName)) { - // put result in per-module name cache - cache.getOrCreateCacheForModuleName(moduleName, resolutionMode, redirectedReference).set(containingDirectory, result); - } + if (traceEnabled) { + trace(host, ts.Diagnostics.Module_resolution_kind_is_not_specified_using_0, ts.ModuleResolutionKind[moduleResolution]); } } + else { + if (traceEnabled) { + trace(host, ts.Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ts.ModuleResolutionKind[moduleResolution]); + } + } + + ts.perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/); + switch (moduleResolution) { + case ts.ModuleResolutionKind.Node16: + result = node16ModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); + break; + case ts.ModuleResolutionKind.NodeNext: + result = nodeNextModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); + break; + case ts.ModuleResolutionKind.NodeJs: + result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + break; + case ts.ModuleResolutionKind.Classic: + result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + break; + default: + return ts.Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`); + } + if (result && result.resolvedModule) + ts.perfLogger.logInfoEvent(`Module "${moduleName}" resolved to "${result.resolvedModule.resolvedFileName}"`); + ts.perfLogger.logStopResolveModule((result && result.resolvedModule) ? "" + result.resolvedModule.resolvedFileName : "null"); + + if (perFolderCache) { + perFolderCache.set(moduleName, resolutionMode, result); + if (!ts.isExternalModuleNameRelative(moduleName)) { + // put result in per-module name cache + cache.getOrCreateCacheForModuleName(moduleName, resolutionMode, redirectedReference).set(containingDirectory, result); + } + } + } - if (traceEnabled) { - if (result.resolvedModule) { - if (result.resolvedModule.packageId) { - trace(host, ts.Diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.resolvedModule.resolvedFileName, ts.packageIdToString(result.resolvedModule.packageId)); - } - else { - trace(host, ts.Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName); - } + if (traceEnabled) { + if (result.resolvedModule) { + if (result.resolvedModule.packageId) { + trace(host, ts.Diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.resolvedModule.resolvedFileName, ts.packageIdToString(result.resolvedModule.packageId)); } else { - trace(host, ts.Diagnostics.Module_name_0_was_not_resolved, moduleName); + trace(host, ts.Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName); } } - - return result; + else { + trace(host, ts.Diagnostics.Module_name_0_was_not_resolved, moduleName); + } } - /* - * 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 { + return result; +} - const resolved = tryLoadModuleUsingPathsIfEligible(extensions, moduleName, loader, state); - if (resolved) - return resolved.value; - if (!ts.isExternalModuleNameRelative(moduleName)) { - return tryLoadModuleUsingBaseUrl(extensions, moduleName, loader, state); - } - else { - return tryLoadModuleUsingRootDirs(extensions, moduleName, containingDirectory, loader, state); - } +/* + * 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 (!ts.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, configFile } = state.compilerOptions; - if (paths && !ts.pathIsRelative(moduleName)) { - if (state.traceEnabled) { - if (baseUrl) { - trace(state.host, ts.Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); - } - trace(state.host, ts.Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); +function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) { + const { baseUrl, paths, configFile } = state.compilerOptions; + if (paths && !ts.pathIsRelative(moduleName)) { + if (state.traceEnabled) { + if (baseUrl) { + trace(state.host, ts.Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); } - const baseDirectory = ts.getPathsBasePath(state.compilerOptions, state.host)!; // Always defined when 'paths' is defined - const pathPatterns = configFile?.configFileSpecs ? configFile.configFileSpecs.pathPatterns ||= ts.tryParsePatterns(paths) : undefined; - return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, pathPatterns, loader, /*onlyRecordFailures*/ false, state); + trace(state.host, ts.Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); } + const baseDirectory = ts.getPathsBasePath(state.compilerOptions, state.host)!; // Always defined when 'paths' is defined + const pathPatterns = configFile?.configFileSpecs ? configFile.configFileSpecs.pathPatterns ||= ts.tryParsePatterns(paths) : undefined; + return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, pathPatterns, loader, /*onlyRecordFailures*/ false, state); } +} - function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { +function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { - if (!state.compilerOptions.rootDirs) { - return undefined; + if (!state.compilerOptions.rootDirs) { + return undefined; + } + + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName); + } + + const candidate = ts.normalizePath(ts.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 = ts.normalizePath(rootDir); + if (!ts.endsWith(normalizedRoot, ts.directorySeparator)) { + normalizedRoot += ts.directorySeparator; } + const isLongestMatchingPrefix = ts.startsWith(candidate, normalizedRoot) && + (matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length); if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName); + trace(state.host, ts.Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix); } - const candidate = ts.normalizePath(ts.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 = ts.normalizePath(rootDir); - if (!ts.endsWith(normalizedRoot, ts.directorySeparator)) { - normalizedRoot += ts.directorySeparator; - } - const isLongestMatchingPrefix = ts.startsWith(candidate, normalizedRoot) && - (matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length); + if (isLongestMatchingPrefix) { + matchedNormalizedPrefix = normalizedRoot; + matchedRootDir = rootDir; + } + } + if (matchedNormalizedPrefix) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix); + } + const suffix = candidate.substr(matchedNormalizedPrefix.length); - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix); - } + // first - try to load from a initial location + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate); + } + const resolvedFileName = loader(extensions, candidate, !ts.directoryProbablyExists(containingDirectory, state.host), state); + if (resolvedFileName) { + return resolvedFileName; + } - if (isLongestMatchingPrefix) { - matchedNormalizedPrefix = normalizedRoot; - matchedRootDir = rootDir; - } + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Trying_other_entries_in_rootDirs); } - if (matchedNormalizedPrefix) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix); + // 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 = ts.combinePaths(ts.normalizePath(rootDir), suffix); if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate); + trace(state.host, ts.Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate); } - const resolvedFileName = loader(extensions, candidate, !ts.directoryProbablyExists(containingDirectory, state.host), state); + const baseDirectory = ts.getDirectoryPath(candidate); + const resolvedFileName = loader(extensions, candidate, !ts.directoryProbablyExists(baseDirectory, state.host), state); if (resolvedFileName) { return resolvedFileName; } - - if (state.traceEnabled) { - trace(state.host, ts.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 = ts.combinePaths(ts.normalizePath(rootDir), suffix); - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate); - } - const baseDirectory = ts.getDirectoryPath(candidate); - const resolvedFileName = loader(extensions, candidate, !ts.directoryProbablyExists(baseDirectory, state.host), state); - if (resolvedFileName) { - return resolvedFileName; - } - } - if (state.traceEnabled) { - trace(state.host, ts.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, ts.Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); } - const candidate = ts.normalizePath(ts.combinePaths(baseUrl, moduleName)); if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, baseUrl, candidate); + trace(state.host, ts.Diagnostics.Module_resolution_using_rootDirs_has_failed); } - return loader(extensions, candidate, !ts.directoryProbablyExists(ts.getDirectoryPath(candidate), state.host), state); } + return undefined; +} - /** - * 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: ts.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; +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, ts.Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); + } + const candidate = ts.normalizePath(ts.combinePaths(baseUrl, moduleName)); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, baseUrl, candidate); + } + return loader(extensions, candidate, !ts.directoryProbablyExists(ts.getDirectoryPath(candidate), state.host), state); +} - /* @internal */ - enum NodeResolutionFeatures { - None = 0, - // resolving `#local` names in your own package.json - Imports = 1 << 1, - // resolving `your-own-name` from your own package.json - SelfName = 1 << 2, - // respecting the `.exports` member of packages' package.json files and its (conditional) mappings of export names - Exports = 1 << 3, - // allowing `*` in the LHS of an export to be followed by more content, eg `"./whatever/*.js"` - // not supported in node 12 - https://github.com/nodejs/Release/issues/690 - ExportsPatternTrailers = 1 << 4, - AllFeatures = Imports | SelfName | Exports | ExportsPatternTrailers, +/** + * 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: ts.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; +} - Node16Default = Imports | SelfName | Exports | ExportsPatternTrailers, +/* @internal */ +enum NodeResolutionFeatures { + None = 0, + // resolving `#local` names in your own package.json + Imports = 1 << 1, + // resolving `your-own-name` from your own package.json + SelfName = 1 << 2, + // respecting the `.exports` member of packages' package.json files and its (conditional) mappings of export names + Exports = 1 << 3, + // allowing `*` in the LHS of an export to be followed by more content, eg `"./whatever/*.js"` + // not supported in node 12 - https://github.com/nodejs/Release/issues/690 + ExportsPatternTrailers = 1 << 4, + AllFeatures = Imports | SelfName | Exports | ExportsPatternTrailers, + + Node16Default = Imports | SelfName | Exports | ExportsPatternTrailers, + + NodeNextDefault = AllFeatures, + + EsmMode = 1 << 5 +} +function node16ModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { + return nodeNextModuleNameResolverWorker(NodeResolutionFeatures.Node16Default, moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); +} +function nodeNextModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { + return nodeNextModuleNameResolverWorker(NodeResolutionFeatures.NodeNextDefault, moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); +} +function nodeNextModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { + const containingDirectory = ts.getDirectoryPath(containingFile); - NodeNextDefault = AllFeatures, + // es module file or cjs-like input file, use a variant of the legacy cjs resolver that supports the selected modern features + const esmMode = resolutionMode === ts.ModuleKind.ESNext ? NodeResolutionFeatures.EsmMode : 0; + return nodeModuleNameResolverWorker(features | esmMode, moduleName, containingDirectory, compilerOptions, host, cache, compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions, redirectedReference); +} - EsmMode = 1 << 5 - } - function node16ModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { - return nodeNextModuleNameResolverWorker(NodeResolutionFeatures.Node16Default, moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); - } - function nodeNextModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { - return nodeNextModuleNameResolverWorker(NodeResolutionFeatures.NodeNextDefault, moduleName, containingFile, compilerOptions, host, cache, redirectedReference, resolutionMode); - } - function nodeNextModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations { - const containingDirectory = ts.getDirectoryPath(containingFile); +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: ts.ModuleResolutionHost): ts.ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, initialDir, { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined); +} - // es module file or cjs-like input file, use a variant of the legacy cjs resolver that supports the selected modern features - const esmMode = resolutionMode === ts.ModuleKind.ESNext ? NodeResolutionFeatures.EsmMode : 0; - return nodeModuleNameResolverWorker(features | esmMode, moduleName, containingDirectory, compilerOptions, host, cache, compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions, redirectedReference); +export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference): ts.ResolvedModuleWithFailedLookupLocations; +/* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, lookupConfig?: boolean): ts.ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures +export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, lookupConfig?: boolean): ts.ResolvedModuleWithFailedLookupLocations { + let extensions; + if (lookupConfig) { + extensions = tsconfigExtensions; } - - 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: ts.ModuleResolutionHost): ts.ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, initialDir, { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined); + else if (compilerOptions.noDtsResolution) { + extensions = [Extensions.TsOnly]; + if (compilerOptions.allowJs) + extensions.push(Extensions.JavaScript); + if (compilerOptions.resolveJsonModule) + extensions.push(Extensions.Json); } - - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference): ts.ResolvedModuleWithFailedLookupLocations; - /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, lookupConfig?: boolean): ts.ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ts.ResolvedProjectReference, lookupConfig?: boolean): ts.ResolvedModuleWithFailedLookupLocations { - let extensions; - if (lookupConfig) { - extensions = tsconfigExtensions; - } - else if (compilerOptions.noDtsResolution) { - extensions = [Extensions.TsOnly]; - if (compilerOptions.allowJs) - extensions.push(Extensions.JavaScript); - if (compilerOptions.resolveJsonModule) - extensions.push(Extensions.Json); - } - else { - extensions = compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions; - } - return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, ts.getDirectoryPath(containingFile), compilerOptions, host, cache, extensions, redirectedReference); + else { + extensions = compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions; } + return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, ts.getDirectoryPath(containingFile), compilerOptions, host, cache, extensions, redirectedReference); +} - function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingDirectory: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ts.ResolvedProjectReference | undefined): ts.ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - - const failedLookupLocations: string[] = []; - // conditions are only used by the node16/nodenext resolver - there's no priority order in the list, - //it's essentially a set (priority is determined by object insertion order in the object we look at). - const conditions = features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"]; - if (compilerOptions.noDtsResolution) { - conditions.pop(); +function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingDirectory: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ts.ResolvedProjectReference | undefined): ts.ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + + const failedLookupLocations: string[] = []; + // conditions are only used by the node16/nodenext resolver - there's no priority order in the list, + //it's essentially a set (priority is determined by object insertion order in the object we look at). + const conditions = features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"]; + if (compilerOptions.noDtsResolution) { + conditions.pop(); + } + + const diagnostics: ts.Diagnostic[] = []; + const state: ModuleResolutionState = { + compilerOptions, + host, + traceEnabled, + failedLookupLocations, + packageJsonInfoCache: cache, + features, + conditions, + requestContainingDirectory: containingDirectory, + reportDiagnostic: diag => void diagnostics.push(diag), + }; + + const result = ts.forEach(extensions, ext => tryResolve(ext)); + return createResolvedModuleWithFailedLookupLocations(result?.value?.resolved, result?.value?.isExternalLibraryImport, failedLookupLocations, diagnostics, state.resultFromCache); + + 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) }); } - const diagnostics: ts.Diagnostic[] = []; - const state: ModuleResolutionState = { - compilerOptions, - host, - traceEnabled, - failedLookupLocations, - packageJsonInfoCache: cache, - features, - conditions, - requestContainingDirectory: containingDirectory, - reportDiagnostic: diag => void diagnostics.push(diag), - }; - - const result = ts.forEach(extensions, ext => tryResolve(ext)); - return createResolvedModuleWithFailedLookupLocations(result?.value?.resolved, result?.value?.isExternalLibraryImport, failedLookupLocations, diagnostics, state.resultFromCache); - - 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 (!ts.isExternalModuleNameRelative(moduleName)) { + let resolved: SearchResult | undefined; + if (features & NodeResolutionFeatures.Imports && ts.startsWith(moduleName, "#")) { + resolved = loadModuleFromImports(extensions, moduleName, containingDirectory, state, cache, redirectedReference); } - - if (!ts.isExternalModuleNameRelative(moduleName)) { - let resolved: SearchResult | undefined; - if (features & NodeResolutionFeatures.Imports && ts.startsWith(moduleName, "#")) { - resolved = loadModuleFromImports(extensions, moduleName, containingDirectory, state, cache, redirectedReference); - } - if (!resolved && features & NodeResolutionFeatures.SelfName) { - resolved = loadModuleFromSelfNameReference(extensions, moduleName, containingDirectory, state, cache, redirectedReference); - } - if (!resolved) { - if (traceEnabled) { - trace(host, ts.Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]); - } - 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 = arePathsEqual(path, resolvedValue.path, host) ? undefined : resolvedValue.path; - resolvedValue = { ...resolvedValue, path, originalPath }; + if (!resolved && features & NodeResolutionFeatures.SelfName) { + resolved = loadModuleFromSelfNameReference(extensions, moduleName, containingDirectory, state, cache, redirectedReference); + } + if (!resolved) { + if (traceEnabled) { + trace(host, ts.Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]); } - // 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 } }; + resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference); } - else { - const { path: candidate, parts } = normalizePathForCJSResolution(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: ts.contains(parts, "node_modules") }); + if (!resolved) + return undefined; + + let resolvedValue = resolved.value; + if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) { + const path = realPath(resolvedValue.path, host, traceEnabled); + const originalPath = arePathsEqual(path, resolvedValue.path, host) ? 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 } = normalizePathForCJSResolution(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: ts.contains(parts, "node_modules") }); } - } - // If you import from "." inside a containing directory "/foo", the result of `normalizePath` - // would be "/foo", but this loses the information that `foo` is a directory and we intended - // to look inside of it. The Node CommonJS resolution algorithm doesn't call this out - // (https://nodejs.org/api/modules.html#all-together), but it seems that module paths ending - // in `.` are actually normalized to `./` before proceeding with the resolution algorithm. - function normalizePathForCJSResolution(containingDirectory: string, moduleName: string) { - const combined = ts.combinePaths(containingDirectory, moduleName); - const parts = ts.getPathComponents(combined); - const lastPart = ts.lastOrUndefined(parts); - const path = lastPart === "." || lastPart === ".." ? ts.ensureTrailingDirectorySeparator(ts.normalizePath(combined)) : ts.normalizePath(combined); - return { path, parts }; - } +} - function realPath(path: string, host: ts.ModuleResolutionHost, traceEnabled: boolean): string { - if (!host.realpath) { - return path; - } +// If you import from "." inside a containing directory "/foo", the result of `normalizePath` +// would be "/foo", but this loses the information that `foo` is a directory and we intended +// to look inside of it. The Node CommonJS resolution algorithm doesn't call this out +// (https://nodejs.org/api/modules.html#all-together), but it seems that module paths ending +// in `.` are actually normalized to `./` before proceeding with the resolution algorithm. +function normalizePathForCJSResolution(containingDirectory: string, moduleName: string) { + const combined = ts.combinePaths(containingDirectory, moduleName); + const parts = ts.getPathComponents(combined); + const lastPart = ts.lastOrUndefined(parts); + const path = lastPart === "." || lastPart === ".." ? ts.ensureTrailingDirectorySeparator(ts.normalizePath(combined)) : ts.normalizePath(combined); + return { path, parts }; +} - const real = ts.normalizePath(host.realpath(path)); - if (traceEnabled) { - trace(host, ts.Diagnostics.Resolving_real_path_for_0_result_1, path, real); - } - ts.Debug.assert(host.fileExists(real), `${path} linked to nonexistent file ${real}`); - return real; +function realPath(path: string, host: ts.ModuleResolutionHost, traceEnabled: boolean): string { + if (!host.realpath) { + return path; } - function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson: boolean): Resolved | undefined { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]); - } - if (!ts.hasTrailingDirectorySeparator(candidate)) { - if (!onlyRecordFailures) { - const parentOfCandidate = ts.getDirectoryPath(candidate); - if (!ts.directoryProbablyExists(parentOfCandidate, state.host)) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate); - } - onlyRecordFailures = true; - } - } - const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state); - if (resolvedFromFile) { - const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile.path) : undefined; - const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined; - return withPackageId(packageInfo, resolvedFromFile); - } - } + const real = ts.normalizePath(host.realpath(path)); + if (traceEnabled) { + trace(host, ts.Diagnostics.Resolving_real_path_for_0_result_1, path, real); + } + ts.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, ts.Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]); + } + if (!ts.hasTrailingDirectorySeparator(candidate)) { if (!onlyRecordFailures) { - const candidateExists = ts.directoryProbablyExists(candidate, state.host); - if (!candidateExists) { + const parentOfCandidate = ts.getDirectoryPath(candidate); + if (!ts.directoryProbablyExists(parentOfCandidate, state.host)) { if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate); + trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate); } onlyRecordFailures = true; } } - // esm mode relative imports shouldn't do any directory lookups (either inside `package.json` - // files or implicit `index.js`es). This is a notable depature from cjs norms, where `./foo/pkg` - // could have been redirected by `./foo/pkg/package.json` to an arbitrary location! - if (!(state.features & NodeResolutionFeatures.EsmMode)) { - return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson); + const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state); + if (resolvedFromFile) { + const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile.path) : undefined; + const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined; + return withPackageId(packageInfo, resolvedFromFile); } - return undefined; } - - /*@internal*/ - export const nodeModulesPathPart = "/node_modules/"; - /*@internal*/ - export function pathContainsNodeModules(path: string): boolean { - return ts.stringContains(path, nodeModulesPathPart); + if (!onlyRecordFailures) { + const candidateExists = ts.directoryProbablyExists(candidate, state.host); + if (!candidateExists) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate); + } + onlyRecordFailures = true; + } + } + // esm mode relative imports shouldn't do any directory lookups (either inside `package.json` + // files or implicit `index.js`es). This is a notable depature from cjs norms, where `./foo/pkg` + // could have been redirected by `./foo/pkg/package.json` to an arbitrary location! + if (!(state.features & NodeResolutionFeatures.EsmMode)) { + return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson); } + return undefined; +} - /** - * This will be called on the successfully resolved path from `loadModuleFromFile`. - * (Not needed 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" - */ - /* @internal */ - export function parseNodeModuleFromPath(resolved: string): string | undefined { - const path = ts.normalizePath(resolved); - const idx = path.lastIndexOf(nodeModulesPathPart); - if (idx === -1) { - return undefined; - } +/*@internal*/ +export const nodeModulesPathPart = "/node_modules/"; +/*@internal*/ +export function pathContainsNodeModules(path: string): boolean { + return ts.stringContains(path, nodeModulesPathPart); +} - const indexAfterNodeModules = idx + nodeModulesPathPart.length; - let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules); - if (path.charCodeAt(indexAfterNodeModules) === ts.CharacterCodes.at) { - indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName); - } - return path.slice(0, indexAfterPackageName); +/** + * This will be called on the successfully resolved path from `loadModuleFromFile`. + * (Not needed 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" + */ +/* @internal */ +export function parseNodeModuleFromPath(resolved: string): string | undefined { + const path = ts.normalizePath(resolved); + const idx = path.lastIndexOf(nodeModulesPathPart); + if (idx === -1) { + return undefined; } - function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number { - const nextSeparatorIndex = path.indexOf(ts.directorySeparator, prevSeparatorIndex + 1); - return nextSeparatorIndex === -1 ? prevSeparatorIndex : nextSeparatorIndex; + const indexAfterNodeModules = idx + nodeModulesPathPart.length; + let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules); + if (path.charCodeAt(indexAfterNodeModules) === ts.CharacterCodes.at) { + indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName); } + return path.slice(0, indexAfterPackageName); +} + +function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number { + const nextSeparatorIndex = path.indexOf(ts.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)); +} - 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 = ts.tryRemoveExtension(candidate, ts.Extension.Json); + const extension = extensionLess ? candidate.substring(extensionLess.length) : ""; + return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, extension, 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 = ts.tryRemoveExtension(candidate, ts.Extension.Json); - const extension = extensionLess ? candidate.substring(extensionLess.length) : ""; - return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, extension, onlyRecordFailures, state); + // esm mode resolutions don't include automatic extension lookup (without additional flags, at least) + if (!(state.features & NodeResolutionFeatures.EsmMode)) { + // 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; } + } - // esm mode resolutions don't include automatic extension lookup (without additional flags, at least) - if (!(state.features & NodeResolutionFeatures.EsmMode)) { - // 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; - } + return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); +} + +function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { + // 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 (ts.hasJSFileExtension(candidate) || (ts.fileExtensionIs(candidate, ts.Extension.Json) && state.compilerOptions.resolveJsonModule)) { + const extensionless = ts.removeFileExtension(candidate); + const extension = candidate.substring(extensionless.length); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension); } + return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state); + } +} - return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); +function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { + if ((extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) && ts.fileExtensionIsOneOf(candidate, ts.supportedTSExtensionsFlat)) { + const result = tryFile(candidate, onlyRecordFailures, state); + return result !== undefined ? { path: candidate, ext: ts.tryExtractTSExtension(candidate) as ts.Extension } : undefined; } - function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { - // 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 (ts.hasJSFileExtension(candidate) || (ts.fileExtensionIs(candidate, ts.Extension.Json) && state.compilerOptions.resolveJsonModule)) { - const extensionless = ts.removeFileExtension(candidate); - const extension = candidate.substring(extensionless.length); - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension); + return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); +} + +/** Try to return an existing file that adds one of the `extensions` to `candidate`. */ +function tryAddingExtensions(candidate: string, extensions: Extensions, originalExtension: string, 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 = ts.getDirectoryPath(candidate); + if (directory) { + onlyRecordFailures = !ts.directoryProbablyExists(directory, state.host); + } + } + + switch (extensions) { + case Extensions.DtsOnly: + switch (originalExtension) { + case ts.Extension.Mjs: + case ts.Extension.Mts: + case ts.Extension.Dmts: + return tryExtension(ts.Extension.Dmts); + case ts.Extension.Cjs: + case ts.Extension.Cts: + case ts.Extension.Dcts: + return tryExtension(ts.Extension.Dcts); + case ts.Extension.Json: + candidate += ts.Extension.Json; + return tryExtension(ts.Extension.Dts); + default: return tryExtension(ts.Extension.Dts); + } + case Extensions.TypeScript: + case Extensions.TsOnly: + const useDts = extensions === Extensions.TypeScript; + switch (originalExtension) { + case ts.Extension.Mjs: + case ts.Extension.Mts: + case ts.Extension.Dmts: + return tryExtension(ts.Extension.Mts) || (useDts ? tryExtension(ts.Extension.Dmts) : undefined); + case ts.Extension.Cjs: + case ts.Extension.Cts: + case ts.Extension.Dcts: + return tryExtension(ts.Extension.Cts) || (useDts ? tryExtension(ts.Extension.Dcts) : undefined); + case ts.Extension.Json: + candidate += ts.Extension.Json; + return useDts ? tryExtension(ts.Extension.Dts) : undefined; + default: + return tryExtension(ts.Extension.Ts) || tryExtension(ts.Extension.Tsx) || (useDts ? tryExtension(ts.Extension.Dts) : undefined); + } + case Extensions.JavaScript: + switch (originalExtension) { + case ts.Extension.Mjs: + case ts.Extension.Mts: + case ts.Extension.Dmts: + return tryExtension(ts.Extension.Mjs); + case ts.Extension.Cjs: + case ts.Extension.Cts: + case ts.Extension.Dcts: + return tryExtension(ts.Extension.Cjs); + case ts.Extension.Json: + return tryExtension(ts.Extension.Json); + default: + return tryExtension(ts.Extension.Js) || tryExtension(ts.Extension.Jsx); } - return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state); - } + case Extensions.TSConfig: + case Extensions.Json: + return tryExtension(ts.Extension.Json); } - function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { - if ((extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) && ts.fileExtensionIsOneOf(candidate, ts.supportedTSExtensionsFlat)) { - const result = tryFile(candidate, onlyRecordFailures, state); - return result !== undefined ? { path: candidate, ext: ts.tryExtractTSExtension(candidate) as ts.Extension } : undefined; - } + function tryExtension(ext: ts.Extension): PathAndExtension | undefined { + const path = tryFile(candidate + ext, onlyRecordFailures, state); + return path === undefined ? undefined : { path, ext }; + } +} - return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); +/** Return the file if it exists. */ +function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { + if (!state.compilerOptions.moduleSuffixes?.length) { + return tryFileLookup(fileName, onlyRecordFailures, state); } - /** Try to return an existing file that adds one of the `extensions` to `candidate`. */ - function tryAddingExtensions(candidate: string, extensions: Extensions, originalExtension: string, 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 = ts.getDirectoryPath(candidate); - if (directory) { - onlyRecordFailures = !ts.directoryProbablyExists(directory, state.host); - } - } + const ext = ts.tryGetExtensionFromPath(fileName) ?? ""; + const fileNameNoExtension = ext ? ts.removeExtension(fileName, ext) : fileName; + return ts.forEach(state.compilerOptions.moduleSuffixes, suffix => tryFileLookup(fileNameNoExtension + suffix + ext, onlyRecordFailures, state)); +} - switch (extensions) { - case Extensions.DtsOnly: - switch (originalExtension) { - case ts.Extension.Mjs: - case ts.Extension.Mts: - case ts.Extension.Dmts: - return tryExtension(ts.Extension.Dmts); - case ts.Extension.Cjs: - case ts.Extension.Cts: - case ts.Extension.Dcts: - return tryExtension(ts.Extension.Dcts); - case ts.Extension.Json: - candidate += ts.Extension.Json; - return tryExtension(ts.Extension.Dts); - default: return tryExtension(ts.Extension.Dts); - } - case Extensions.TypeScript: - case Extensions.TsOnly: - const useDts = extensions === Extensions.TypeScript; - switch (originalExtension) { - case ts.Extension.Mjs: - case ts.Extension.Mts: - case ts.Extension.Dmts: - return tryExtension(ts.Extension.Mts) || (useDts ? tryExtension(ts.Extension.Dmts) : undefined); - case ts.Extension.Cjs: - case ts.Extension.Cts: - case ts.Extension.Dcts: - return tryExtension(ts.Extension.Cts) || (useDts ? tryExtension(ts.Extension.Dcts) : undefined); - case ts.Extension.Json: - candidate += ts.Extension.Json; - return useDts ? tryExtension(ts.Extension.Dts) : undefined; - default: - return tryExtension(ts.Extension.Ts) || tryExtension(ts.Extension.Tsx) || (useDts ? tryExtension(ts.Extension.Dts) : undefined); - } - case Extensions.JavaScript: - switch (originalExtension) { - case ts.Extension.Mjs: - case ts.Extension.Mts: - case ts.Extension.Dmts: - return tryExtension(ts.Extension.Mjs); - case ts.Extension.Cjs: - case ts.Extension.Cts: - case ts.Extension.Dcts: - return tryExtension(ts.Extension.Cjs); - case ts.Extension.Json: - return tryExtension(ts.Extension.Json); - default: - return tryExtension(ts.Extension.Js) || tryExtension(ts.Extension.Jsx); - } - case Extensions.TSConfig: - case Extensions.Json: - return tryExtension(ts.Extension.Json); +function tryFileLookup(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { + if (!onlyRecordFailures) { + if (state.host.fileExists(fileName)) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName); + } + return fileName; } - - function tryExtension(ext: ts.Extension): PathAndExtension | undefined { - const path = tryFile(candidate + ext, onlyRecordFailures, state); - return path === undefined ? undefined : { path, ext }; + else { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.File_0_does_not_exist, fileName); + } } } + state.failedLookupLocations.push(fileName); + return undefined; +} - /** Return the file if it exists. */ - function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { - if (!state.compilerOptions.moduleSuffixes?.length) { - return tryFileLookup(fileName, 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)); +} - const ext = ts.tryGetExtensionFromPath(fileName) ?? ""; - const fileNameNoExtension = ext ? ts.removeExtension(fileName, ext) : fileName; - return ts.forEach(state.compilerOptions.moduleSuffixes, suffix => tryFileLookup(fileNameNoExtension + suffix + ext, onlyRecordFailures, state)); - } +/* @internal */ +export function getEntrypointsFromPackageJsonInfo(packageJsonInfo: PackageJsonInfo, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined, resolveJs?: boolean): string[] | false { + if (!resolveJs && packageJsonInfo.resolvedEntrypoints !== undefined) { + // Cached value excludes resolutions to JS files - those could be + // cached separately, but they're used rarely. + return packageJsonInfo.resolvedEntrypoints; + } + + let entrypoints: string[] | undefined; + const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript; + const features = getDefaultNodeResolutionFeatures(options); + const requireState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: ["node", "require", "types"], + features, + requestContainingDirectory: packageJsonInfo.packageDirectory, + reportDiagnostic: ts.noop + }; + const requireResolution = loadNodeModuleFromDirectoryWorker(extensions, packageJsonInfo.packageDirectory, + /*onlyRecordFailures*/ false, requireState, packageJsonInfo.packageJsonContent, packageJsonInfo.versionPaths); + entrypoints = ts.append(entrypoints, requireResolution?.path); + + if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { + for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) { + const exportState = { ...requireState, failedLookupLocations: [], conditions }; + const exportResolutions = loadEntrypointsFromExportMap(packageJsonInfo, packageJsonInfo.packageJsonContent.exports, exportState, extensions); + if (exportResolutions) { + for (const resolution of exportResolutions) { + entrypoints = ts.appendIfUnique(entrypoints, resolution.path); + } + } + } + } + + return packageJsonInfo.resolvedEntrypoints = entrypoints || false; +} - function tryFileLookup(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { - if (!onlyRecordFailures) { - if (state.host.fileExists(fileName)) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName); - } - return fileName; - } - else { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.File_0_does_not_exist, fileName); - } - } +function loadEntrypointsFromExportMap(scope: PackageJsonInfo, exports: object, state: ModuleResolutionState, extensions: Extensions): PathAndExtension[] | undefined { + let entrypoints: PathAndExtension[] | undefined; + if (ts.isArray(exports)) { + for (const target of exports) { + loadEntrypointsFromTargetExports(target); } - 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)); - } - - /* @internal */ - export function getEntrypointsFromPackageJsonInfo(packageJsonInfo: PackageJsonInfo, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache: ModuleResolutionCache | undefined, resolveJs?: boolean): string[] | false { - if (!resolveJs && packageJsonInfo.resolvedEntrypoints !== undefined) { - // Cached value excludes resolutions to JS files - those could be - // cached separately, but they're used rarely. - return packageJsonInfo.resolvedEntrypoints; - } - - let entrypoints: string[] | undefined; - const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript; - const features = getDefaultNodeResolutionFeatures(options); - const requireState: ModuleResolutionState = { - compilerOptions: options, - host, - traceEnabled: isTraceEnabled(options, host), - failedLookupLocations: [], - packageJsonInfoCache: cache?.getPackageJsonInfoCache(), - conditions: ["node", "require", "types"], - features, - requestContainingDirectory: packageJsonInfo.packageDirectory, - reportDiagnostic: ts.noop - }; - const requireResolution = loadNodeModuleFromDirectoryWorker(extensions, packageJsonInfo.packageDirectory, - /*onlyRecordFailures*/ false, requireState, packageJsonInfo.packageJsonContent, packageJsonInfo.versionPaths); - entrypoints = ts.append(entrypoints, requireResolution?.path); - - if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { - for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) { - const exportState = { ...requireState, failedLookupLocations: [], conditions }; - const exportResolutions = loadEntrypointsFromExportMap(packageJsonInfo, packageJsonInfo.packageJsonContent.exports, exportState, extensions); - if (exportResolutions) { - for (const resolution of exportResolutions) { - entrypoints = ts.appendIfUnique(entrypoints, resolution.path); - } - } - } + // eslint-disable-next-line no-null/no-null + else if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as ts.MapLike)) { + for (const key in exports) { + loadEntrypointsFromTargetExports((exports as ts.MapLike)[key]); } - - return packageJsonInfo.resolvedEntrypoints = entrypoints || false; } + else { + loadEntrypointsFromTargetExports(exports); + } + return entrypoints; - function loadEntrypointsFromExportMap(scope: PackageJsonInfo, exports: object, state: ModuleResolutionState, extensions: Extensions): PathAndExtension[] | undefined { - let entrypoints: PathAndExtension[] | undefined; - if (ts.isArray(exports)) { - for (const target of exports) { - loadEntrypointsFromTargetExports(target); + function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined { + if (typeof target === "string" && ts.startsWith(target, "./") && target.indexOf("*") === -1) { + const partsAfterFirst = ts.getPathComponents(target).slice(2); + if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { + return false; } - } - // eslint-disable-next-line no-null/no-null - else if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as ts.MapLike)) { - for (const key in exports) { - loadEntrypointsFromTargetExports((exports as ts.MapLike)[key]); + const resolvedTarget = ts.combinePaths(scope.packageDirectory, target); + const finalPath = ts.getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); + const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); + if (result) { + entrypoints = ts.appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + return true; } } - else { - loadEntrypointsFromTargetExports(exports); - } - return entrypoints; - - function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined { - if (typeof target === "string" && ts.startsWith(target, "./") && target.indexOf("*") === -1) { - const partsAfterFirst = ts.getPathComponents(target).slice(2); - if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { - return false; - } - const resolvedTarget = ts.combinePaths(scope.packageDirectory, target); - const finalPath = ts.getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); - if (result) { - entrypoints = ts.appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + else if (Array.isArray(target)) { + for (const t of target) { + const success = loadEntrypointsFromTargetExports(t); + if (success) { return true; } } - else if (Array.isArray(target)) { - for (const t of target) { - const success = loadEntrypointsFromTargetExports(t); - if (success) { - return true; - } + } + // eslint-disable-next-line no-null/no-null + else if (typeof target === "object" && target !== null) { + return ts.forEach(ts.getOwnKeys(target as ts.MapLike), key => { + if (key === "default" || ts.contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { + loadEntrypointsFromTargetExports((target as ts.MapLike)[key]); + return true; } - } - // eslint-disable-next-line no-null/no-null - else if (typeof target === "object" && target !== null) { - return ts.forEach(ts.getOwnKeys(target as ts.MapLike), key => { - if (key === "default" || ts.contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { - loadEntrypointsFromTargetExports((target as ts.MapLike)[key]); - return true; - } - }); - } + }); } } +} - /*@internal*/ - interface PackageJsonInfo { - packageDirectory: string; - packageJsonContent: PackageJsonPathFields; - versionPaths: VersionPaths | undefined; - /** false: resolved to nothing. undefined: not yet resolved */ - resolvedEntrypoints: string[] | false | undefined; - } +/*@internal*/ +interface PackageJsonInfo { + packageDirectory: string; + packageJsonContent: PackageJsonPathFields; + versionPaths: VersionPaths | undefined; + /** false: resolved to nothing. undefined: not yet resolved */ + resolvedEntrypoints: string[] | false | undefined; +} - /** - * A function for locating the package.json scope for a given path - */ - /*@internal*/ - export function getPackageScopeForPath(fileName: ts.Path, packageJsonInfoCache: PackageJsonInfoCache | undefined, host: ts.ModuleResolutionHost, options: ts.CompilerOptions): PackageJsonInfo | undefined { - const state: { - host: ts.ModuleResolutionHost; - compilerOptions: ts.CompilerOptions; - traceEnabled: boolean; - failedLookupLocations: ts.Push; - resultFromCache?: ts.ResolvedModuleWithFailedLookupLocations; - packageJsonInfoCache: PackageJsonInfoCache | undefined; - features: number; - conditions: never[]; - requestContainingDirectory: string | undefined; - reportDiagnostic: ts.DiagnosticReporter; - } = { - host, - compilerOptions: options, - traceEnabled: isTraceEnabled(options, host), - failedLookupLocations: [], - packageJsonInfoCache, - features: 0, - conditions: [], - requestContainingDirectory: undefined, - reportDiagnostic: ts.noop - }; - const parts = ts.getPathComponents(fileName); - parts.pop(); - while (parts.length > 0) { - const pkg = getPackageJsonInfo(ts.getPathFromPathComponents(parts), /*onlyRecordFailures*/ false, state); - if (pkg) { - return pkg; - } - parts.pop(); +/** + * A function for locating the package.json scope for a given path + */ +/*@internal*/ +export function getPackageScopeForPath(fileName: ts.Path, packageJsonInfoCache: PackageJsonInfoCache | undefined, host: ts.ModuleResolutionHost, options: ts.CompilerOptions): PackageJsonInfo | undefined { + const state: { + host: ts.ModuleResolutionHost; + compilerOptions: ts.CompilerOptions; + traceEnabled: boolean; + failedLookupLocations: ts.Push; + resultFromCache?: ts.ResolvedModuleWithFailedLookupLocations; + packageJsonInfoCache: PackageJsonInfoCache | undefined; + features: number; + conditions: never[]; + requestContainingDirectory: string | undefined; + reportDiagnostic: ts.DiagnosticReporter; + } = { + host, + compilerOptions: options, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache, + features: 0, + conditions: [], + requestContainingDirectory: undefined, + reportDiagnostic: ts.noop + }; + const parts = ts.getPathComponents(fileName); + parts.pop(); + while (parts.length > 0) { + const pkg = getPackageJsonInfo(ts.getPathFromPathComponents(parts), /*onlyRecordFailures*/ false, state); + if (pkg) { + return pkg; } - return undefined; + parts.pop(); } + return undefined; +} - /*@internal*/ - export function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined { - const { host, traceEnabled } = state; - const packageJsonPath = ts.combinePaths(packageDirectory, "package.json"); - if (onlyRecordFailures) { - state.failedLookupLocations.push(packageJsonPath); - return undefined; - } +/*@internal*/ +export function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined { + const { host, traceEnabled } = state; + const packageJsonPath = ts.combinePaths(packageDirectory, "package.json"); + if (onlyRecordFailures) { + state.failedLookupLocations.push(packageJsonPath); + return undefined; + } - const existing = state.packageJsonInfoCache?.getPackageJsonInfo(packageJsonPath); - if (existing !== undefined) { - if (typeof existing !== "boolean") { - if (traceEnabled) - trace(host, ts.Diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageJsonPath); - return existing; - } - else { - if (existing && traceEnabled) - trace(host, ts.Diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageJsonPath); - state.failedLookupLocations.push(packageJsonPath); - return undefined; - } - } - const directoryExists = ts.directoryProbablyExists(packageDirectory, host); - if (directoryExists && host.fileExists(packageJsonPath)) { - const packageJsonContent = ts.readJson(packageJsonPath, host) as PackageJson; - if (traceEnabled) { - trace(host, ts.Diagnostics.Found_package_json_at_0, packageJsonPath); - } - const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); - const result = { packageDirectory, packageJsonContent, versionPaths, resolvedEntrypoints: undefined }; - state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result); - return result; + const existing = state.packageJsonInfoCache?.getPackageJsonInfo(packageJsonPath); + if (existing !== undefined) { + if (typeof existing !== "boolean") { + if (traceEnabled) + trace(host, ts.Diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageJsonPath); + return existing; } else { - if (directoryExists && traceEnabled) { - trace(host, ts.Diagnostics.File_0_does_not_exist, packageJsonPath); - } - state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, directoryExists); - // record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results + if (existing && traceEnabled) + trace(host, ts.Diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageJsonPath); state.failedLookupLocations.push(packageJsonPath); + return undefined; } - } - - 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: - case Extensions.TsOnly: - 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 ts.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, ts.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. - - // Disable `EsmMode` for the resolution of the package path for cjs-mode packages (so the `main` field can omit extensions) - // (technically it only emits a deprecation warning in esm packages right now, but that's probably - // enough to mean we don't need to support it) - const features = state.features; - if (jsonContent?.type !== "module") { - state.features &= ~NodeResolutionFeatures.EsmMode; - } - const result = nodeLoadModuleByRelativeName(nextExtensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ false); - state.features = features; - return result; - }; - - const onlyRecordFailuresForPackageFile = packageFile ? !ts.directoryProbablyExists(ts.getDirectoryPath(packageFile), state.host) : undefined; - const onlyRecordFailuresForIndex = onlyRecordFailures || !ts.directoryProbablyExists(candidate, state.host); - const indexPath = ts.combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"); - if (versionPaths && (!packageFile || ts.containsPath(candidate, packageFile))) { - const moduleName = ts.getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false); - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, ts.version, moduleName); - } - const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, /*pathPatterns*/ undefined, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state); - if (result) { - return removeIgnoredPackageId(result.value); - } - } - - // It won't have a `packageId` set, because we disabled `considerPackageJson`. - const packageFileResult = packageFile && removeIgnoredPackageId(loader(extensions, packageFile, onlyRecordFailuresForPackageFile!, state)); - if (packageFileResult) - return packageFileResult; - - // esm mode resolutions don't do package `index` lookups - if (!(state.features & NodeResolutionFeatures.EsmMode)) { - return loadModuleFromFile(extensions, indexPath, onlyRecordFailuresForIndex, state); + } + const directoryExists = ts.directoryProbablyExists(packageDirectory, host); + if (directoryExists && host.fileExists(packageJsonPath)) { + const packageJsonContent = ts.readJson(packageJsonPath, host) as PackageJson; + if (traceEnabled) { + trace(host, ts.Diagnostics.Found_package_json_at_0, packageJsonPath); } + const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); + const result = { packageDirectory, packageJsonContent, versionPaths, resolvedEntrypoints: undefined }; + state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result); + return result; } - - /** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */ - function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined { - const ext = ts.tryGetExtensionFromPath(path); - return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined; + else { + if (directoryExists && traceEnabled) { + trace(host, ts.Diagnostics.File_0_does_not_exist, packageJsonPath); + } + state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, directoryExists); + // 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); } +} - /** True if `extension` is one of the supported `extensions`. */ - function extensionIsOk(extensions: Extensions, extension: ts.Extension): boolean { +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 === ts.Extension.Js || extension === ts.Extension.Jsx || extension === ts.Extension.Mjs || extension === ts.Extension.Cjs; - case Extensions.TSConfig: case Extensions.Json: - return extension === ts.Extension.Json; - case Extensions.TypeScript: - return extension === ts.Extension.Ts || extension === ts.Extension.Tsx || extension === ts.Extension.Mts || extension === ts.Extension.Cts || extension === ts.Extension.Dts || extension === ts.Extension.Dmts || extension === ts.Extension.Dcts; case Extensions.TsOnly: - return extension === ts.Extension.Ts || extension === ts.Extension.Tsx || extension === ts.Extension.Mts || extension === ts.Extension.Cts; + 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: - return extension === ts.Extension.Dts || extension === ts.Extension.Dmts || extension === ts.Extension.Dcts; + packageFile = readPackageJsonTypesFields(jsonContent, candidate, state); + break; + case Extensions.TSConfig: + packageFile = readPackageJsonTSConfigField(jsonContent, candidate, state); + break; + default: + return ts.Debug.assertNever(extensions); } } - /* @internal */ - export function parsePackageName(moduleName: string): { - packageName: string; - rest: string; - } { - let idx = moduleName.indexOf(ts.directorySeparator); - if (moduleName[0] === "@") { - idx = moduleName.indexOf(ts.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, ts.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. + + // Disable `EsmMode` for the resolution of the package path for cjs-mode packages (so the `main` field can omit extensions) + // (technically it only emits a deprecation warning in esm packages right now, but that's probably + // enough to mean we don't need to support it) + const features = state.features; + if (jsonContent?.type !== "module") { + state.features &= ~NodeResolutionFeatures.EsmMode; + } + const result = nodeLoadModuleByRelativeName(nextExtensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ false); + state.features = features; + return result; + }; + + const onlyRecordFailuresForPackageFile = packageFile ? !ts.directoryProbablyExists(ts.getDirectoryPath(packageFile), state.host) : undefined; + const onlyRecordFailuresForIndex = onlyRecordFailures || !ts.directoryProbablyExists(candidate, state.host); + const indexPath = ts.combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"); + if (versionPaths && (!packageFile || ts.containsPath(candidate, packageFile))) { + const moduleName = ts.getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, ts.version, moduleName); + } + const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, /*pathPatterns*/ undefined, 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) }; } - /* @internal */ - export function allKeysStartWithDot(obj: ts.MapLike) { - return ts.every(ts.getOwnKeys(obj), k => ts.startsWith(k, ".")); + // It won't have a `packageId` set, because we disabled `considerPackageJson`. + const packageFileResult = packageFile && removeIgnoredPackageId(loader(extensions, packageFile, onlyRecordFailuresForPackageFile!, state)); + if (packageFileResult) + return packageFileResult; + + // esm mode resolutions don't do package `index` lookups + if (!(state.features & NodeResolutionFeatures.EsmMode)) { + return loadModuleFromFile(extensions, indexPath, onlyRecordFailuresForIndex, state); } +} - function noKeyStartsWithDot(obj: ts.MapLike) { - return !ts.some(ts.getOwnKeys(obj), k => ts.startsWith(k, ".")); +/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */ +function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined { + const ext = ts.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: ts.Extension): boolean { + switch (extensions) { + case Extensions.JavaScript: + return extension === ts.Extension.Js || extension === ts.Extension.Jsx || extension === ts.Extension.Mjs || extension === ts.Extension.Cjs; + case Extensions.TSConfig: + case Extensions.Json: + return extension === ts.Extension.Json; + case Extensions.TypeScript: + return extension === ts.Extension.Ts || extension === ts.Extension.Tsx || extension === ts.Extension.Mts || extension === ts.Extension.Cts || extension === ts.Extension.Dts || extension === ts.Extension.Dmts || extension === ts.Extension.Dcts; + case Extensions.TsOnly: + return extension === ts.Extension.Ts || extension === ts.Extension.Tsx || extension === ts.Extension.Mts || extension === ts.Extension.Cts; + case Extensions.DtsOnly: + return extension === ts.Extension.Dts || extension === ts.Extension.Dmts || extension === ts.Extension.Dcts; } +} - function loadModuleFromSelfNameReference(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { - const useCaseSensitiveFileNames = typeof state.host.useCaseSensitiveFileNames === "function" ? state.host.useCaseSensitiveFileNames() : state.host.useCaseSensitiveFileNames; - const directoryPath = ts.toPath(ts.combinePaths(directory, "dummy"), state.host.getCurrentDirectory?.(), ts.createGetCanonicalFileName(useCaseSensitiveFileNames === undefined ? true : useCaseSensitiveFileNames)); - const scope = getPackageScopeForPath(directoryPath, state.packageJsonInfoCache, state.host, state.compilerOptions); - if (!scope || !scope.packageJsonContent.exports) { - return undefined; - } - if (typeof scope.packageJsonContent.name !== "string") { - return undefined; - } - const parts = ts.getPathComponents(moduleName); // unrooted paths should have `""` as their 0th entry - const nameParts = ts.getPathComponents(scope.packageJsonContent.name); - if (!ts.every(nameParts, (p, i) => parts[i] === p)) { - return undefined; - } - const trailingParts = parts.slice(nameParts.length); - return loadModuleFromExports(scope, extensions, !ts.length(trailingParts) ? "." : `.${ts.directorySeparator}${trailingParts.join(ts.directorySeparator)}`, state, cache, redirectedReference); +/* @internal */ +export function parsePackageName(moduleName: string): { + packageName: string; + rest: string; +} { + let idx = moduleName.indexOf(ts.directorySeparator); + if (moduleName[0] === "@") { + idx = moduleName.indexOf(ts.directorySeparator, idx + 1); } + return idx === -1 ? { packageName: moduleName, rest: "" } : { packageName: moduleName.slice(0, idx), rest: moduleName.slice(idx + 1) }; +} - function loadModuleFromExports(scope: PackageJsonInfo, extensions: Extensions, subpath: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { - if (!scope.packageJsonContent.exports) { - return undefined; - } +/* @internal */ +export function allKeysStartWithDot(obj: ts.MapLike) { + return ts.every(ts.getOwnKeys(obj), k => ts.startsWith(k, ".")); +} - if (subpath === ".") { - let mainExport; - if (typeof scope.packageJsonContent.exports === "string" || Array.isArray(scope.packageJsonContent.exports) || (typeof scope.packageJsonContent.exports === "object" && noKeyStartsWithDot(scope.packageJsonContent.exports as ts.MapLike))) { - mainExport = scope.packageJsonContent.exports; - } - else if (ts.hasProperty(scope.packageJsonContent.exports as ts.MapLike, ".")) { - mainExport = (scope.packageJsonContent.exports as ts.MapLike)["."]; - } - if (mainExport) { - const loadModuleFromTargetImportOrExport = getLoadModuleFromTargetImportOrExport(extensions, state, cache, redirectedReference, subpath, scope, /*isImports*/ false); - return loadModuleFromTargetImportOrExport(mainExport, "", /*pattern*/ false); - } - } - else if (allKeysStartWithDot(scope.packageJsonContent.exports as ts.MapLike)) { - if (typeof scope.packageJsonContent.exports !== "object") { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Export_specifier_0_does_not_exist_in_package_json_scope_at_path_1, subpath, scope.packageDirectory); - } - return toSearchResult(/*value*/ undefined); - } - const result = loadModuleFromImportsOrExports(extensions, state, cache, redirectedReference, subpath, scope.packageJsonContent.exports, scope, /*isImports*/ false); - if (result) { - return result; - } - } +function noKeyStartsWithDot(obj: ts.MapLike) { + return !ts.some(ts.getOwnKeys(obj), k => ts.startsWith(k, ".")); +} - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Export_specifier_0_does_not_exist_in_package_json_scope_at_path_1, subpath, scope.packageDirectory); - } - return toSearchResult(/*value*/ undefined); +function loadModuleFromSelfNameReference(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { + const useCaseSensitiveFileNames = typeof state.host.useCaseSensitiveFileNames === "function" ? state.host.useCaseSensitiveFileNames() : state.host.useCaseSensitiveFileNames; + const directoryPath = ts.toPath(ts.combinePaths(directory, "dummy"), state.host.getCurrentDirectory?.(), ts.createGetCanonicalFileName(useCaseSensitiveFileNames === undefined ? true : useCaseSensitiveFileNames)); + const scope = getPackageScopeForPath(directoryPath, state.packageJsonInfoCache, state.host, state.compilerOptions); + if (!scope || !scope.packageJsonContent.exports) { + return undefined; + } + if (typeof scope.packageJsonContent.name !== "string") { + return undefined; + } + const parts = ts.getPathComponents(moduleName); // unrooted paths should have `""` as their 0th entry + const nameParts = ts.getPathComponents(scope.packageJsonContent.name); + if (!ts.every(nameParts, (p, i) => parts[i] === p)) { + return undefined; } + const trailingParts = parts.slice(nameParts.length); + return loadModuleFromExports(scope, extensions, !ts.length(trailingParts) ? "." : `.${ts.directorySeparator}${trailingParts.join(ts.directorySeparator)}`, state, cache, redirectedReference); +} - function loadModuleFromImports(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { - if (moduleName === "#" || ts.startsWith(moduleName, "#/")) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Invalid_import_specifier_0_has_no_possible_resolutions, moduleName); - } - return toSearchResult(/*value*/ undefined); +function loadModuleFromExports(scope: PackageJsonInfo, extensions: Extensions, subpath: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { + if (!scope.packageJsonContent.exports) { + return undefined; + } + + if (subpath === ".") { + let mainExport; + if (typeof scope.packageJsonContent.exports === "string" || Array.isArray(scope.packageJsonContent.exports) || (typeof scope.packageJsonContent.exports === "object" && noKeyStartsWithDot(scope.packageJsonContent.exports as ts.MapLike))) { + mainExport = scope.packageJsonContent.exports; } - const useCaseSensitiveFileNames = typeof state.host.useCaseSensitiveFileNames === "function" ? state.host.useCaseSensitiveFileNames() : state.host.useCaseSensitiveFileNames; - const directoryPath = ts.toPath(ts.combinePaths(directory, "dummy"), state.host.getCurrentDirectory?.(), ts.createGetCanonicalFileName(useCaseSensitiveFileNames === undefined ? true : useCaseSensitiveFileNames)); - const scope = getPackageScopeForPath(directoryPath, state.packageJsonInfoCache, state.host, state.compilerOptions); - if (!scope) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Directory_0_has_no_containing_package_json_scope_Imports_will_not_resolve, directoryPath); - } - return toSearchResult(/*value*/ undefined); + else if (ts.hasProperty(scope.packageJsonContent.exports as ts.MapLike, ".")) { + mainExport = (scope.packageJsonContent.exports as ts.MapLike)["."]; + } + if (mainExport) { + const loadModuleFromTargetImportOrExport = getLoadModuleFromTargetImportOrExport(extensions, state, cache, redirectedReference, subpath, scope, /*isImports*/ false); + return loadModuleFromTargetImportOrExport(mainExport, "", /*pattern*/ false); } - if (!scope.packageJsonContent.imports) { + } + else if (allKeysStartWithDot(scope.packageJsonContent.exports as ts.MapLike)) { + if (typeof scope.packageJsonContent.exports !== "object") { if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_no_imports_defined, scope.packageDirectory); + trace(state.host, ts.Diagnostics.Export_specifier_0_does_not_exist_in_package_json_scope_at_path_1, subpath, scope.packageDirectory); } return toSearchResult(/*value*/ undefined); } - - const result = loadModuleFromImportsOrExports(extensions, state, cache, redirectedReference, moduleName, scope.packageJsonContent.imports, scope, /*isImports*/ true); + const result = loadModuleFromImportsOrExports(extensions, state, cache, redirectedReference, subpath, scope.packageJsonContent.exports, scope, /*isImports*/ false); if (result) { return result; } + } + + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Export_specifier_0_does_not_exist_in_package_json_scope_at_path_1, subpath, scope.packageDirectory); + } + return toSearchResult(/*value*/ undefined); +} +function loadModuleFromImports(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { + if (moduleName === "#" || ts.startsWith(moduleName, "#/")) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Invalid_import_specifier_0_has_no_possible_resolutions, moduleName); + } + return toSearchResult(/*value*/ undefined); + } + const useCaseSensitiveFileNames = typeof state.host.useCaseSensitiveFileNames === "function" ? state.host.useCaseSensitiveFileNames() : state.host.useCaseSensitiveFileNames; + const directoryPath = ts.toPath(ts.combinePaths(directory, "dummy"), state.host.getCurrentDirectory?.(), ts.createGetCanonicalFileName(useCaseSensitiveFileNames === undefined ? true : useCaseSensitiveFileNames)); + const scope = getPackageScopeForPath(directoryPath, state.packageJsonInfoCache, state.host, state.compilerOptions); + if (!scope) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Directory_0_has_no_containing_package_json_scope_Imports_will_not_resolve, directoryPath); + } + return toSearchResult(/*value*/ undefined); + } + if (!scope.packageJsonContent.imports) { if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Import_specifier_0_does_not_exist_in_package_json_scope_at_path_1, moduleName, scope.packageDirectory); + trace(state.host, ts.Diagnostics.package_json_scope_0_has_no_imports_defined, scope.packageDirectory); } return toSearchResult(/*value*/ undefined); } - function loadModuleFromImportsOrExports(extensions: Extensions, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, moduleName: string, lookupTable: object, scope: PackageJsonInfo, isImports: boolean): SearchResult | undefined { - const loadModuleFromTargetImportOrExport = getLoadModuleFromTargetImportOrExport(extensions, state, cache, redirectedReference, moduleName, scope, isImports); + const result = loadModuleFromImportsOrExports(extensions, state, cache, redirectedReference, moduleName, scope.packageJsonContent.imports, scope, /*isImports*/ true); + if (result) { + return result; + } + + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Import_specifier_0_does_not_exist_in_package_json_scope_at_path_1, moduleName, scope.packageDirectory); + } + return toSearchResult(/*value*/ undefined); +} + +function loadModuleFromImportsOrExports(extensions: Extensions, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, moduleName: string, lookupTable: object, scope: PackageJsonInfo, isImports: boolean): SearchResult | undefined { + const loadModuleFromTargetImportOrExport = getLoadModuleFromTargetImportOrExport(extensions, state, cache, redirectedReference, moduleName, scope, isImports); - if (!ts.endsWith(moduleName, ts.directorySeparator) && moduleName.indexOf("*") === -1 && ts.hasProperty(lookupTable, moduleName)) { + if (!ts.endsWith(moduleName, ts.directorySeparator) && moduleName.indexOf("*") === -1 && ts.hasProperty(lookupTable, moduleName)) { + const target = (lookupTable as { + [idx: string]: unknown; + })[moduleName]; + return loadModuleFromTargetImportOrExport(target, /*subpath*/ "", /*pattern*/ false); + } + const expandingKeys = ts.sort(ts.filter(ts.getOwnKeys(lookupTable as ts.MapLike), k => k.indexOf("*") !== -1 || ts.endsWith(k, "/")), (a, b) => a.length - b.length); + for (const potentialTarget of expandingKeys) { + if (state.features & NodeResolutionFeatures.ExportsPatternTrailers && matchesPatternWithTrailer(potentialTarget, moduleName)) { const target = (lookupTable as { [idx: string]: unknown; - })[moduleName]; - return loadModuleFromTargetImportOrExport(target, /*subpath*/ "", /*pattern*/ false); + })[potentialTarget]; + const starPos = potentialTarget.indexOf("*"); + const subpath = moduleName.substring(potentialTarget.substring(0, starPos).length, moduleName.length - (potentialTarget.length - 1 - starPos)); + return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ true); } - const expandingKeys = ts.sort(ts.filter(ts.getOwnKeys(lookupTable as ts.MapLike), k => k.indexOf("*") !== -1 || ts.endsWith(k, "/")), (a, b) => a.length - b.length); - for (const potentialTarget of expandingKeys) { - if (state.features & NodeResolutionFeatures.ExportsPatternTrailers && matchesPatternWithTrailer(potentialTarget, moduleName)) { - const target = (lookupTable as { - [idx: string]: unknown; - })[potentialTarget]; - const starPos = potentialTarget.indexOf("*"); - const subpath = moduleName.substring(potentialTarget.substring(0, starPos).length, moduleName.length - (potentialTarget.length - 1 - starPos)); - return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ true); - } - else if (ts.endsWith(potentialTarget, "*") && ts.startsWith(moduleName, potentialTarget.substring(0, potentialTarget.length - 1))) { - const target = (lookupTable as { - [idx: string]: unknown; - })[potentialTarget]; - const subpath = moduleName.substring(potentialTarget.length - 1); - return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ true); - } - else if (ts.startsWith(moduleName, potentialTarget)) { - const target = (lookupTable as { - [idx: string]: unknown; - })[potentialTarget]; - const subpath = moduleName.substring(potentialTarget.length); - return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ false); - } + else if (ts.endsWith(potentialTarget, "*") && ts.startsWith(moduleName, potentialTarget.substring(0, potentialTarget.length - 1))) { + const target = (lookupTable as { + [idx: string]: unknown; + })[potentialTarget]; + const subpath = moduleName.substring(potentialTarget.length - 1); + return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ true); } - - function matchesPatternWithTrailer(target: string, name: string) { - if (ts.endsWith(target, "*")) - return false; // handled by next case in loop - const starPos = target.indexOf("*"); - if (starPos === -1) - return false; // handled by last case in loop - return ts.startsWith(name, target.substring(0, starPos)) && ts.endsWith(name, target.substring(starPos + 1)); + else if (ts.startsWith(moduleName, potentialTarget)) { + const target = (lookupTable as { + [idx: string]: unknown; + })[potentialTarget]; + const subpath = moduleName.substring(potentialTarget.length); + return loadModuleFromTargetImportOrExport(target, subpath, /*pattern*/ false); } } - /** - * Gets the self-recursive function specialized to retrieving the targeted import/export element for the given resolution configuration - */ - function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, moduleName: string, scope: PackageJsonInfo, isImports: boolean) { - return loadModuleFromTargetImportOrExport; - function loadModuleFromTargetImportOrExport(target: unknown, subpath: string, pattern: boolean): SearchResult | undefined { - if (typeof target === "string") { - if (!pattern && subpath.length > 0 && !ts.endsWith(target, "/")) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); - } - return toSearchResult(/*value*/ undefined); + function matchesPatternWithTrailer(target: string, name: string) { + if (ts.endsWith(target, "*")) + return false; // handled by next case in loop + const starPos = target.indexOf("*"); + if (starPos === -1) + return false; // handled by last case in loop + return ts.startsWith(name, target.substring(0, starPos)) && ts.endsWith(name, target.substring(starPos + 1)); + } +} + +/** + * Gets the self-recursive function specialized to retrieving the targeted import/export element for the given resolution configuration + */ +function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, moduleName: string, scope: PackageJsonInfo, isImports: boolean) { + return loadModuleFromTargetImportOrExport; + function loadModuleFromTargetImportOrExport(target: unknown, subpath: string, pattern: boolean): SearchResult | undefined { + if (typeof target === "string") { + if (!pattern && subpath.length > 0 && !ts.endsWith(target, "/")) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); } - if (!ts.startsWith(target, "./")) { - if (isImports && !ts.startsWith(target, "../") && !ts.startsWith(target, "/") && !ts.isRootedDiskPath(target)) { - const combinedLookup = pattern ? target.replace(/\*/g, subpath) : target + subpath; - const result = nodeModuleNameResolverWorker(state.features, combinedLookup, scope.packageDirectory + "/", state.compilerOptions, state.host, cache, [extensions], redirectedReference); - return toSearchResult(result.resolvedModule ? { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId, originalPath: result.resolvedModule.originalPath } : undefined); - } - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); - } - return toSearchResult(/*value*/ undefined); + return toSearchResult(/*value*/ undefined); + } + if (!ts.startsWith(target, "./")) { + if (isImports && !ts.startsWith(target, "../") && !ts.startsWith(target, "/") && !ts.isRootedDiskPath(target)) { + const combinedLookup = pattern ? target.replace(/\*/g, subpath) : target + subpath; + const result = nodeModuleNameResolverWorker(state.features, combinedLookup, scope.packageDirectory + "/", state.compilerOptions, state.host, cache, [extensions], redirectedReference); + return toSearchResult(result.resolvedModule ? { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId, originalPath: result.resolvedModule.originalPath } : undefined); } - const parts = ts.pathIsRelative(target) ? ts.getPathComponents(target).slice(1) : ts.getPathComponents(target); - const partsAfterFirst = parts.slice(1); - if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); - } - return toSearchResult(/*value*/ undefined); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); } - const resolvedTarget = ts.combinePaths(scope.packageDirectory, target); - // TODO: Assert that `resolvedTarget` is actually within the package directory? That's what the spec says.... but I'm not sure we need - // to be in the business of validating everyone's import and export map correctness. - const subpathParts = ts.getPathComponents(subpath); - if (subpathParts.indexOf("..") >= 0 || subpathParts.indexOf(".") >= 0 || subpathParts.indexOf("node_modules") >= 0) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); - } - return toSearchResult(/*value*/ undefined); + return toSearchResult(/*value*/ undefined); + } + const parts = ts.pathIsRelative(target) ? ts.getPathComponents(target).slice(1) : ts.getPathComponents(target); + const partsAfterFirst = parts.slice(1); + if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); } - const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath); - const inputLink = tryLoadInputFileForPath(finalPath, subpath, ts.combinePaths(scope.packageDirectory, "package.json"), isImports); - if (inputLink) - return inputLink; - return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, finalPath, /*onlyRecordFailures*/ false, state))); - } - else if (typeof target === "object" && target !== null) { // eslint-disable-line no-null/no-null - if (!Array.isArray(target)) { - for (const key of ts.getOwnKeys(target as ts.MapLike)) { - if (key === "default" || state.conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(state.conditions, key)) { - const subTarget = (target as ts.MapLike)[key]; - const result = loadModuleFromTargetImportOrExport(subTarget, subpath, pattern); - if (result) { - return result; - } - } - } - return undefined; + return toSearchResult(/*value*/ undefined); + } + const resolvedTarget = ts.combinePaths(scope.packageDirectory, target); + // TODO: Assert that `resolvedTarget` is actually within the package directory? That's what the spec says.... but I'm not sure we need + // to be in the business of validating everyone's import and export map correctness. + const subpathParts = ts.getPathComponents(subpath); + if (subpathParts.indexOf("..") >= 0 || subpathParts.indexOf(".") >= 0 || subpathParts.indexOf("node_modules") >= 0) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); } - else { - if (!ts.length(target)) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); - } - return toSearchResult(/*value*/ undefined); - } - for (const elem of target) { - const result = loadModuleFromTargetImportOrExport(elem, subpath, pattern); + return toSearchResult(/*value*/ undefined); + } + const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath); + const inputLink = tryLoadInputFileForPath(finalPath, subpath, ts.combinePaths(scope.packageDirectory, "package.json"), isImports); + if (inputLink) + return inputLink; + return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, finalPath, /*onlyRecordFailures*/ false, state))); + } + else if (typeof target === "object" && target !== null) { // eslint-disable-line no-null/no-null + if (!Array.isArray(target)) { + for (const key of ts.getOwnKeys(target as ts.MapLike)) { + if (key === "default" || state.conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(state.conditions, key)) { + const subTarget = (target as ts.MapLike)[key]; + const result = loadModuleFromTargetImportOrExport(subTarget, subpath, pattern); if (result) { return result; } } } + return undefined; } - else if (target === null) { // eslint-disable-line no-null/no-null - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_explicitly_maps_specifier_1_to_null, scope.packageDirectory, moduleName); + else { + if (!ts.length(target)) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); + } + return toSearchResult(/*value*/ undefined); + } + for (const elem of target) { + const result = loadModuleFromTargetImportOrExport(elem, subpath, pattern); + if (result) { + return result; + } } - return toSearchResult(/*value*/ undefined); } + } + else if (target === null) { // eslint-disable-line no-null/no-null if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); + trace(state.host, ts.Diagnostics.package_json_scope_0_explicitly_maps_specifier_1_to_null, scope.packageDirectory, moduleName); } return toSearchResult(/*value*/ undefined); + } + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName); + } + return toSearchResult(/*value*/ undefined); - function toAbsolutePath(path: string): string; - function toAbsolutePath(path: string | undefined): string | undefined; - function toAbsolutePath(path: string | undefined): string | undefined { - if (path === undefined) - return path; - return ts.hostGetCanonicalFileName({ useCaseSensitiveFileNames })(ts.getNormalizedAbsolutePath(path, state.host.getCurrentDirectory?.())); - } - - function combineDirectoryPath(root: string, dir: string) { - return ts.ensureTrailingDirectorySeparator(ts.combinePaths(root, dir)); - } - - function useCaseSensitiveFileNames() { - return !state.host.useCaseSensitiveFileNames ? true : - typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames : - state.host.useCaseSensitiveFileNames(); - } - - function tryLoadInputFileForPath(finalPath: string, entry: string, packagePath: string, isImports: boolean) { - // Replace any references to outputs for files in the program with the input files to support package self-names used with outDir - // PROBLEM: We don't know how to calculate the output paths yet, because the "common source directory" we use as the base of the file structure - // we reproduce into the output directory is based on the set of input files, which we're still in the process of traversing and resolving! - // _Given that_, we have to guess what the base of the output directory is (obviously the user wrote the export map, so has some idea what it is!). - // We are going to probe _so many_ possible paths. We limit where we'll do this to try to reduce the possibilities of false positive lookups. - if ((extensions === Extensions.TypeScript || extensions === Extensions.JavaScript || extensions === Extensions.Json) - && (state.compilerOptions.declarationDir || state.compilerOptions.outDir) - && finalPath.indexOf("/node_modules/") === -1 - && (state.compilerOptions.configFile ? ts.startsWith(toAbsolutePath(state.compilerOptions.configFile.fileName), scope.packageDirectory) : true)) { - // So that all means we'll only try these guesses for files outside `node_modules` in a directory where the `package.json` and `tsconfig.json` are siblings. - // Even with all that, we still don't know if the root of the output file structure will be (relative to the package file) - // `.`, `./src` or any other deeper directory structure. (If project references are used, it's definitely `.` by fiat, so that should be pretty common.) - - const getCanonicalFileName = ts.hostGetCanonicalFileName({ useCaseSensitiveFileNames }); - const commonSourceDirGuesses: string[] = []; - // A `rootDir` compiler option strongly indicates the root location - // A `composite` project is using project references and has it's common src dir set to `.`, so it shouldn't need to check any other locations - if (state.compilerOptions.rootDir || (state.compilerOptions.composite && state.compilerOptions.configFilePath)) { - const commonDir = toAbsolutePath(ts.getCommonSourceDirectory(state.compilerOptions, () => [], state.host.getCurrentDirectory?.() || "", getCanonicalFileName)); - commonSourceDirGuesses.push(commonDir); - } - else if (state.requestContainingDirectory) { - // However without either of those set we're in the dark. Let's say you have - // - // ./tools/index.ts - // ./src/index.ts - // ./dist/index.js - // ./package.json <-- references ./dist/index.js - // ./tsconfig.json <-- loads ./src/index.ts - // - // How do we know `./src` is the common src dir, and not `./tools`, given only the `./dist` out dir and `./dist/index.js` filename? - // Answer: We... don't. We know we're looking for an `index.ts` input file, but we have _no clue_ which subfolder it's supposed to be loaded from - // without more context. - // But we do have more context! Just a tiny bit more! We're resolving an import _for some other input file_! And that input file, too - // must be inside the common source directory! So we propagate that tidbit of info all the way to here via state.requestContainingDirectory - - const requestingFile = toAbsolutePath(ts.combinePaths(state.requestContainingDirectory, "index.ts")); - // And we can try every folder above the common folder for the request folder and the config/package base directory - // This technically can be wrong - we may load ./src/index.ts when ./src/sub/index.ts was right because we don't - // know if only `./src/sub` files were loaded by the program; but this has the best chance to be right of just about anything - // else we have. And, given that we're about to load `./src/index.ts` because we choose it as likely correct, there will then - // be a file outside of `./src/sub` in the program (the file we resolved to), making us de-facto right. So this fallback lookup - // logic may influence what files are pulled in by self-names, which in turn influences the output path shape, but it's all - // internally consistent so the paths should be stable so long as we prefer the "most general" (meaning: top-most-level directory) possible results first. - const commonDir = toAbsolutePath(ts.getCommonSourceDirectory(state.compilerOptions, () => [requestingFile, toAbsolutePath(packagePath)], state.host.getCurrentDirectory?.() || "", getCanonicalFileName)); - commonSourceDirGuesses.push(commonDir); - - let fragment = ts.ensureTrailingDirectorySeparator(commonDir); - while (fragment && fragment.length > 1) { - const parts = ts.getPathComponents(fragment); - parts.pop(); // remove a directory - const commonDir = ts.getPathFromPathComponents(parts); - commonSourceDirGuesses.unshift(commonDir); - fragment = ts.ensureTrailingDirectorySeparator(commonDir); - } - } - if (commonSourceDirGuesses.length > 1) { - state.reportDiagnostic(ts.createCompilerDiagnostic(isImports - ? ts.Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_import_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate - : ts.Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate, entry === "" ? "." : entry, // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird - packagePath)); + function toAbsolutePath(path: string): string; + function toAbsolutePath(path: string | undefined): string | undefined; + function toAbsolutePath(path: string | undefined): string | undefined { + if (path === undefined) + return path; + return ts.hostGetCanonicalFileName({ useCaseSensitiveFileNames })(ts.getNormalizedAbsolutePath(path, state.host.getCurrentDirectory?.())); + } + + function combineDirectoryPath(root: string, dir: string) { + return ts.ensureTrailingDirectorySeparator(ts.combinePaths(root, dir)); + } + + function useCaseSensitiveFileNames() { + return !state.host.useCaseSensitiveFileNames ? true : + typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames : + state.host.useCaseSensitiveFileNames(); + } + + function tryLoadInputFileForPath(finalPath: string, entry: string, packagePath: string, isImports: boolean) { + // Replace any references to outputs for files in the program with the input files to support package self-names used with outDir + // PROBLEM: We don't know how to calculate the output paths yet, because the "common source directory" we use as the base of the file structure + // we reproduce into the output directory is based on the set of input files, which we're still in the process of traversing and resolving! + // _Given that_, we have to guess what the base of the output directory is (obviously the user wrote the export map, so has some idea what it is!). + // We are going to probe _so many_ possible paths. We limit where we'll do this to try to reduce the possibilities of false positive lookups. + if ((extensions === Extensions.TypeScript || extensions === Extensions.JavaScript || extensions === Extensions.Json) + && (state.compilerOptions.declarationDir || state.compilerOptions.outDir) + && finalPath.indexOf("/node_modules/") === -1 + && (state.compilerOptions.configFile ? ts.startsWith(toAbsolutePath(state.compilerOptions.configFile.fileName), scope.packageDirectory) : true)) { + // So that all means we'll only try these guesses for files outside `node_modules` in a directory where the `package.json` and `tsconfig.json` are siblings. + // Even with all that, we still don't know if the root of the output file structure will be (relative to the package file) + // `.`, `./src` or any other deeper directory structure. (If project references are used, it's definitely `.` by fiat, so that should be pretty common.) + + const getCanonicalFileName = ts.hostGetCanonicalFileName({ useCaseSensitiveFileNames }); + const commonSourceDirGuesses: string[] = []; + // A `rootDir` compiler option strongly indicates the root location + // A `composite` project is using project references and has it's common src dir set to `.`, so it shouldn't need to check any other locations + if (state.compilerOptions.rootDir || (state.compilerOptions.composite && state.compilerOptions.configFilePath)) { + const commonDir = toAbsolutePath(ts.getCommonSourceDirectory(state.compilerOptions, () => [], state.host.getCurrentDirectory?.() || "", getCanonicalFileName)); + commonSourceDirGuesses.push(commonDir); + } + else if (state.requestContainingDirectory) { + // However without either of those set we're in the dark. Let's say you have + // + // ./tools/index.ts + // ./src/index.ts + // ./dist/index.js + // ./package.json <-- references ./dist/index.js + // ./tsconfig.json <-- loads ./src/index.ts + // + // How do we know `./src` is the common src dir, and not `./tools`, given only the `./dist` out dir and `./dist/index.js` filename? + // Answer: We... don't. We know we're looking for an `index.ts` input file, but we have _no clue_ which subfolder it's supposed to be loaded from + // without more context. + // But we do have more context! Just a tiny bit more! We're resolving an import _for some other input file_! And that input file, too + // must be inside the common source directory! So we propagate that tidbit of info all the way to here via state.requestContainingDirectory + + const requestingFile = toAbsolutePath(ts.combinePaths(state.requestContainingDirectory, "index.ts")); + // And we can try every folder above the common folder for the request folder and the config/package base directory + // This technically can be wrong - we may load ./src/index.ts when ./src/sub/index.ts was right because we don't + // know if only `./src/sub` files were loaded by the program; but this has the best chance to be right of just about anything + // else we have. And, given that we're about to load `./src/index.ts` because we choose it as likely correct, there will then + // be a file outside of `./src/sub` in the program (the file we resolved to), making us de-facto right. So this fallback lookup + // logic may influence what files are pulled in by self-names, which in turn influences the output path shape, but it's all + // internally consistent so the paths should be stable so long as we prefer the "most general" (meaning: top-most-level directory) possible results first. + const commonDir = toAbsolutePath(ts.getCommonSourceDirectory(state.compilerOptions, () => [requestingFile, toAbsolutePath(packagePath)], state.host.getCurrentDirectory?.() || "", getCanonicalFileName)); + commonSourceDirGuesses.push(commonDir); + + let fragment = ts.ensureTrailingDirectorySeparator(commonDir); + while (fragment && fragment.length > 1) { + const parts = ts.getPathComponents(fragment); + parts.pop(); // remove a directory + const commonDir = ts.getPathFromPathComponents(parts); + commonSourceDirGuesses.unshift(commonDir); + fragment = ts.ensureTrailingDirectorySeparator(commonDir); } - for (const commonSourceDirGuess of commonSourceDirGuesses) { - const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess); - for (const candidateDir of candidateDirectories) { - if (ts.startsWith(finalPath, candidateDir)) { - // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension - const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator - const possibleInputBase = ts.combinePaths(commonSourceDirGuess, pathFragment); - const jsAndDtsExtensions = [ts.Extension.Mjs, ts.Extension.Cjs, ts.Extension.Js, ts.Extension.Json, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Dts]; - for (const ext of jsAndDtsExtensions) { - if (ts.fileExtensionIs(possibleInputBase, ext)) { - const inputExts = ts.getPossibleOriginalInputExtensionForExtension(possibleInputBase); - for (const possibleExt of inputExts) { - const possibleInputWithInputExtension = ts.changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames()); - if ((extensions === Extensions.TypeScript && ts.hasJSFileExtension(possibleInputWithInputExtension)) || - (extensions === Extensions.JavaScript && ts.hasTSFileExtension(possibleInputWithInputExtension))) { - continue; - } - if (state.host.fileExists(possibleInputWithInputExtension)) { - return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); - } + } + if (commonSourceDirGuesses.length > 1) { + state.reportDiagnostic(ts.createCompilerDiagnostic(isImports + ? ts.Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_import_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate + : ts.Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate, entry === "" ? "." : entry, // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird + packagePath)); + } + for (const commonSourceDirGuess of commonSourceDirGuesses) { + const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess); + for (const candidateDir of candidateDirectories) { + if (ts.startsWith(finalPath, candidateDir)) { + // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension + const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator + const possibleInputBase = ts.combinePaths(commonSourceDirGuess, pathFragment); + const jsAndDtsExtensions = [ts.Extension.Mjs, ts.Extension.Cjs, ts.Extension.Js, ts.Extension.Json, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Dts]; + for (const ext of jsAndDtsExtensions) { + if (ts.fileExtensionIs(possibleInputBase, ext)) { + const inputExts = ts.getPossibleOriginalInputExtensionForExtension(possibleInputBase); + for (const possibleExt of inputExts) { + const possibleInputWithInputExtension = ts.changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames()); + if ((extensions === Extensions.TypeScript && ts.hasJSFileExtension(possibleInputWithInputExtension)) || + (extensions === Extensions.JavaScript && ts.hasTSFileExtension(possibleInputWithInputExtension))) { + continue; + } + if (state.host.fileExists(possibleInputWithInputExtension)) { + return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); } } } @@ -2233,302 +2232,303 @@ namespace ts { } } } - return undefined; + } + return undefined; - function getOutputDirectoriesForBaseDirectory(commonSourceDirGuess: string) { - // Config file ouput paths are processed to be relative to the host's current directory, while - // otherwise the paths are resolved relative to the common source dir the compiler puts together - const currentDir = state.compilerOptions.configFile ? state.host.getCurrentDirectory?.() || "" : commonSourceDirGuess; - const candidateDirectories = []; - if (state.compilerOptions.declarationDir) { - candidateDirectories.push(toAbsolutePath(combineDirectoryPath(currentDir, state.compilerOptions.declarationDir))); - } - if (state.compilerOptions.outDir && state.compilerOptions.outDir !== state.compilerOptions.declarationDir) { - candidateDirectories.push(toAbsolutePath(combineDirectoryPath(currentDir, state.compilerOptions.outDir))); - } - return candidateDirectories; + function getOutputDirectoriesForBaseDirectory(commonSourceDirGuess: string) { + // Config file ouput paths are processed to be relative to the host's current directory, while + // otherwise the paths are resolved relative to the common source dir the compiler puts together + const currentDir = state.compilerOptions.configFile ? state.host.getCurrentDirectory?.() || "" : commonSourceDirGuess; + const candidateDirectories = []; + if (state.compilerOptions.declarationDir) { + candidateDirectories.push(toAbsolutePath(combineDirectoryPath(currentDir, state.compilerOptions.declarationDir))); + } + if (state.compilerOptions.outDir && state.compilerOptions.outDir !== state.compilerOptions.declarationDir) { + candidateDirectories.push(toAbsolutePath(combineDirectoryPath(currentDir, state.compilerOptions.outDir))); } + return candidateDirectories; } } } +} - /* @internal */ - export function isApplicableVersionedTypesKey(conditions: string[], key: string) { - if (conditions.indexOf("types") === -1) - return false; // only apply versioned types conditions if the types condition is applied - if (!ts.startsWith(key, "types@")) - return false; - const range = ts.VersionRange.tryParse(key.substring("types@".length)); - if (!range) - return false; - return range.test(ts.version); - } - function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { - return loadModuleFromNearestNodeModulesDirectoryWorker(extensions, moduleName, directory, state, /*typesScopeOnly*/ false, cache, redirectedReference); - } +/* @internal */ +export function isApplicableVersionedTypesKey(conditions: string[], key: string) { + if (conditions.indexOf("types") === -1) + return false; // only apply versioned types conditions if the types condition is applied + if (!ts.startsWith(key, "types@")) + return false; + const range = ts.VersionRange.tryParse(key.substring("types@".length)); + if (!range) + return false; + return range.test(ts.version); +} +function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.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 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: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { - const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, state.features === 0 ? undefined : state.features & NodeResolutionFeatures.EsmMode ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS, redirectedReference); - return ts.forEachAncestorDirectory(ts.normalizeSlashes(directory), ancestorDirectory => { - if (ts.getBaseFileName(ancestorDirectory) !== "node_modules") { - const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, ancestorDirectory, state); - if (resolutionFromCache) { - return resolutionFromCache; - } - return toSearchResult(loadModuleFromImmediateNodeModulesDirectory(extensions, moduleName, ancestorDirectory, state, typesScopeOnly, cache, redirectedReference)); +function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): SearchResult { + const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, state.features === 0 ? undefined : state.features & NodeResolutionFeatures.EsmMode ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS, redirectedReference); + return ts.forEachAncestorDirectory(ts.normalizeSlashes(directory), ancestorDirectory => { + if (ts.getBaseFileName(ancestorDirectory) !== "node_modules") { + const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, ancestorDirectory, state); + if (resolutionFromCache) { + return resolutionFromCache; } - }); - } - - function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): Resolved | undefined { - const nodeModulesFolder = ts.combinePaths(directory, "node_modules"); - const nodeModulesFolderExists = ts.directoryProbablyExists(nodeModulesFolder, state.host); - if (!nodeModulesFolderExists && state.traceEnabled) { - trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder); + return toSearchResult(loadModuleFromImmediateNodeModulesDirectory(extensions, moduleName, ancestorDirectory, state, typesScopeOnly, cache, redirectedReference)); } + }); +} - const packageResult = typesScopeOnly ? undefined : loadModuleFromSpecificNodeModulesDirectory(extensions, moduleName, nodeModulesFolder, nodeModulesFolderExists, state, cache, redirectedReference); - if (packageResult) { - return packageResult; - } - if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) { - const nodeModulesAtTypes = ts.combinePaths(nodeModulesFolder, "@types"); - let nodeModulesAtTypesExists = nodeModulesFolderExists; - if (nodeModulesFolderExists && !ts.directoryProbablyExists(nodeModulesAtTypes, state.host)) { - if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes); - } - nodeModulesAtTypesExists = false; +function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): Resolved | undefined { + const nodeModulesFolder = ts.combinePaths(directory, "node_modules"); + const nodeModulesFolderExists = ts.directoryProbablyExists(nodeModulesFolder, state.host); + if (!nodeModulesFolderExists && state.traceEnabled) { + trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder); + } + + const packageResult = typesScopeOnly ? undefined : loadModuleFromSpecificNodeModulesDirectory(extensions, moduleName, nodeModulesFolder, nodeModulesFolderExists, state, cache, redirectedReference); + if (packageResult) { + return packageResult; + } + if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) { + const nodeModulesAtTypes = ts.combinePaths(nodeModulesFolder, "@types"); + let nodeModulesAtTypesExists = nodeModulesFolderExists; + if (nodeModulesFolderExists && !ts.directoryProbablyExists(nodeModulesAtTypes, state.host)) { + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes); } - return loadModuleFromSpecificNodeModulesDirectory(Extensions.DtsOnly, mangleScopedPackageNameWithTrace(moduleName, state), nodeModulesAtTypes, nodeModulesAtTypesExists, state, cache, redirectedReference); + nodeModulesAtTypesExists = false; } + return loadModuleFromSpecificNodeModulesDirectory(Extensions.DtsOnly, mangleScopedPackageNameWithTrace(moduleName, state), nodeModulesAtTypes, nodeModulesAtTypesExists, state, cache, redirectedReference); } +} - function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): Resolved | undefined { - const candidate = ts.normalizePath(ts.combinePaths(nodeModulesDirectory, moduleName)); - - // First look for a nested package.json, as in `node_modules/foo/bar/package.json`. - let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state); - // But only if we're not respecting export maps (if we are, we might redirect around this location) - if (!(state.features & NodeResolutionFeatures.Exports)) { - if (packageInfo) { - const fromFile = loadModuleFromFile(extensions, candidate, !nodeModulesDirectoryExists, state); - if (fromFile) { - return noPackageId(fromFile); - } +function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ts.ResolvedProjectReference | undefined): Resolved | undefined { + const candidate = ts.normalizePath(ts.combinePaths(nodeModulesDirectory, moduleName)); - const fromDirectory = loadNodeModuleFromDirectoryWorker(extensions, candidate, !nodeModulesDirectoryExists, state, packageInfo.packageJsonContent, packageInfo.versionPaths); - return withPackageId(packageInfo, fromDirectory); + // First look for a nested package.json, as in `node_modules/foo/bar/package.json`. + let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state); + // But only if we're not respecting export maps (if we are, we might redirect around this location) + if (!(state.features & NodeResolutionFeatures.Exports)) { + 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 { packageName, rest } = parsePackageName(moduleName); - const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { - // package exports are higher priority than file/directory lookups (and, if there's exports present, blocks them) - if (packageInfo && packageInfo.packageJsonContent.exports && state.features & NodeResolutionFeatures.Exports) { - return loadModuleFromExports(packageInfo, extensions, ts.combinePaths(".", rest), state, cache, redirectedReference)?.value; - } - let pathAndExtension = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state) || - loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageInfo && packageInfo.packageJsonContent, packageInfo && packageInfo.versionPaths); - if (!pathAndExtension && packageInfo - && packageInfo.packageJsonContent.exports === undefined - && packageInfo.packageJsonContent.main === undefined - && state.features & NodeResolutionFeatures.EsmMode) { - // EsmMode disables index lookup in `loadNodeModuleFromDirectoryWorker` generally, however non-relative package resolutions still assume - // a default `index.js` entrypoint if no `main` or `exports` are present - pathAndExtension = loadModuleFromFile(extensions, ts.combinePaths(candidate, "index.js"), onlyRecordFailures, state); - } - return withPackageId(packageInfo, pathAndExtension); - }; + const { packageName, rest } = parsePackageName(moduleName); + const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { + // package exports are higher priority than file/directory lookups (and, if there's exports present, blocks them) + if (packageInfo && packageInfo.packageJsonContent.exports && state.features & NodeResolutionFeatures.Exports) { + return loadModuleFromExports(packageInfo, extensions, ts.combinePaths(".", rest), state, cache, redirectedReference)?.value; + } + let pathAndExtension = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state) || + loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageInfo && packageInfo.packageJsonContent, packageInfo && packageInfo.versionPaths); + if (!pathAndExtension && packageInfo + && packageInfo.packageJsonContent.exports === undefined + && packageInfo.packageJsonContent.main === undefined + && state.features & NodeResolutionFeatures.EsmMode) { + // EsmMode disables index lookup in `loadNodeModuleFromDirectoryWorker` generally, however non-relative package resolutions still assume + // a default `index.js` entrypoint if no `main` or `exports` are present + pathAndExtension = loadModuleFromFile(extensions, ts.combinePaths(candidate, "index.js"), onlyRecordFailures, state); + } + return withPackageId(packageInfo, pathAndExtension); + }; - if (rest !== "") { // If "rest" is empty, we just did this search above. - const packageDirectory = ts.combinePaths(nodeModulesDirectory, packageName); + if (rest !== "") { // If "rest" is empty, we just did this search above. + const packageDirectory = ts.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, ts.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, ts.version, rest); - } - const packageDirectoryExists = nodeModulesDirectoryExists && ts.directoryProbablyExists(packageDirectory, state.host); - const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, /*pathPatterns*/ undefined, loader, !packageDirectoryExists, state); - if (fromPaths) { - return fromPaths.value; - } + // 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, ts.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, ts.version, rest); + } + const packageDirectoryExists = nodeModulesDirectoryExists && ts.directoryProbablyExists(packageDirectory, state.host); + const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, /*pathPatterns*/ undefined, loader, !packageDirectoryExists, state); + if (fromPaths) { + return fromPaths.value; } } - - return loader(extensions, candidate, !nodeModulesDirectoryExists, state); } - function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: ts.MapLike, pathPatterns: readonly (string | ts.Pattern)[] | undefined, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { - pathPatterns ||= ts.tryParsePatterns(paths); - const matchedPattern = ts.matchPatternOrExact(pathPatterns, moduleName); - if (matchedPattern) { - const matchedStar = ts.isString(matchedPattern) ? undefined : ts.matchedText(matchedPattern, moduleName); - const matchedPatternText = ts.isString(matchedPattern) ? matchedPattern : ts.patternText(matchedPattern); + return loader(extensions, candidate, !nodeModulesDirectoryExists, state); +} + +function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: ts.MapLike, pathPatterns: readonly (string | ts.Pattern)[] | undefined, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { + pathPatterns ||= ts.tryParsePatterns(paths); + const matchedPattern = ts.matchPatternOrExact(pathPatterns, moduleName); + if (matchedPattern) { + const matchedStar = ts.isString(matchedPattern) ? undefined : ts.matchedText(matchedPattern, moduleName); + const matchedPatternText = ts.isString(matchedPattern) ? matchedPattern : ts.patternText(matchedPattern); + if (state.traceEnabled) { + trace(state.host, ts.Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); + } + const resolved = ts.forEach(paths[matchedPatternText], subst => { + const path = matchedStar ? subst.replace("*", matchedStar) : subst; + // When baseUrl is not specified, the command line parser resolves relative paths to the config file location. + const candidate = ts.normalizePath(ts.combinePaths(baseDirectory, path)); if (state.traceEnabled) { - trace(state.host, ts.Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); + trace(state.host, ts.Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path); } - const resolved = ts.forEach(paths[matchedPatternText], subst => { - const path = matchedStar ? subst.replace("*", matchedStar) : subst; - // When baseUrl is not specified, the command line parser resolves relative paths to the config file location. - const candidate = ts.normalizePath(ts.combinePaths(baseDirectory, path)); - if (state.traceEnabled) { - trace(state.host, ts.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 = ts.tryGetExtensionFromPath(subst); - if (extension !== undefined) { - const path = tryFile(candidate, onlyRecordFailures, state); - if (path !== undefined) { - return noPackageId({ path, ext: extension }); - } + // A path mapping may have an extension, in contrast to an import, which should omit it. + const extension = ts.tryGetExtensionFromPath(subst); + if (extension !== undefined) { + const path = tryFile(candidate, onlyRecordFailures, state); + if (path !== undefined) { + return noPackageId({ path, ext: extension }); } - return loader(extensions, candidate, onlyRecordFailures || !ts.directoryProbablyExists(ts.getDirectoryPath(candidate), state.host), state); - }); - return { value: resolved }; - } + } + return loader(extensions, candidate, onlyRecordFailures || !ts.directoryProbablyExists(ts.getDirectoryPath(candidate), state.host), state); + }); + return { value: resolved }; } +} - /** Double underscores are used in DefinitelyTyped to delimit scoped packages. */ - const mangledScopedPackageSeparator = "__"; +/** 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, ts.Diagnostics.Scoped_package_detected_looking_in_0, mangled); - } - return mangled; +/** 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, ts.Diagnostics.Scoped_package_detected_looking_in_0, mangled); } + return mangled; +} - /* @internal */ - export function getTypesPackageName(packageName: string): string { - return `@types/${mangleScopedPackageName(packageName)}`; - } +/* @internal */ +export function getTypesPackageName(packageName: string): string { + return `@types/${mangleScopedPackageName(packageName)}`; +} - /* @internal */ - export function mangleScopedPackageName(packageName: string): string { - if (ts.startsWith(packageName, "@")) { - const replaceSlash = packageName.replace(ts.directorySeparator, mangledScopedPackageSeparator); - if (replaceSlash !== packageName) { - return replaceSlash.slice(1); // Take off the "@" - } +/* @internal */ +export function mangleScopedPackageName(packageName: string): string { + if (ts.startsWith(packageName, "@")) { + const replaceSlash = packageName.replace(ts.directorySeparator, mangledScopedPackageSeparator); + if (replaceSlash !== packageName) { + return replaceSlash.slice(1); // Take off the "@" } - return packageName; } + return packageName; +} - /* @internal */ - export function getPackageNameFromTypesPackageName(mangledName: string): string { - const withoutAtTypePrefix = ts.removePrefix(mangledName, "@types/"); - if (withoutAtTypePrefix !== mangledName) { - return unmangleScopedPackageName(withoutAtTypePrefix); - } - return mangledName; +/* @internal */ +export function getPackageNameFromTypesPackageName(mangledName: string): string { + const withoutAtTypePrefix = ts.removePrefix(mangledName, "@types/"); + if (withoutAtTypePrefix !== mangledName) { + return unmangleScopedPackageName(withoutAtTypePrefix); } + return mangledName; +} - /* @internal */ - export function unmangleScopedPackageName(typesPackageName: string): string { - return ts.stringContains(typesPackageName, mangledScopedPackageSeparator) ? - "@" + typesPackageName.replace(mangledScopedPackageSeparator, ts.directorySeparator) : - typesPackageName; - } +/* @internal */ +export function unmangleScopedPackageName(typesPackageName: string): string { + return ts.stringContains(typesPackageName, mangledScopedPackageSeparator) ? + "@" + typesPackageName.replace(mangledScopedPackageSeparator, ts.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, ts.Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); - } - state.resultFromCache = result; - return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } }; +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, ts.Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); } + state.resultFromCache = result; + 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: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ts.ResolvedProjectReference): ts.ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - const failedLookupLocations: string[] = []; - const containingDirectory = ts.getDirectoryPath(containingFile); - const diagnostics: ts.Diagnostic[] = []; - const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features: NodeResolutionFeatures.None, conditions: [], requestContainingDirectory: containingDirectory, reportDiagnostic: diag => void diagnostics.push(diag) }; +export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ts.ResolvedProjectReference): ts.ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + const failedLookupLocations: string[] = []; + const containingDirectory = ts.getDirectoryPath(containingFile); + const diagnostics: ts.Diagnostic[] = []; + const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features: NodeResolutionFeatures.None, conditions: [], requestContainingDirectory: containingDirectory, reportDiagnostic: diag => void diagnostics.push(diag) }; - 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, diagnostics, state.resultFromCache); + 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, diagnostics, state.resultFromCache); - function tryResolve(extensions: Extensions): SearchResult { - const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, state); - if (resolvedUsingSettings) { - return { value: resolvedUsingSettings }; - } + function tryResolve(extensions: Extensions): SearchResult { + const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, state); + if (resolvedUsingSettings) { + return { value: resolvedUsingSettings }; + } - if (!ts.isExternalModuleNameRelative(moduleName)) { - const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, /*mode*/ undefined, redirectedReference); - // Climb up parent directories looking for a module. - const resolved = ts.forEachAncestorDirectory(containingDirectory, directory => { - const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, directory, state); - if (resolutionFromCache) { - return resolutionFromCache; - } - const searchName = ts.normalizePath(ts.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); + if (!ts.isExternalModuleNameRelative(moduleName)) { + const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, /*mode*/ undefined, redirectedReference); + // Climb up parent directories looking for a module. + const resolved = ts.forEachAncestorDirectory(containingDirectory, directory => { + const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, directory, state); + if (resolutionFromCache) { + return resolutionFromCache; } + const searchName = ts.normalizePath(ts.combinePaths(directory, moduleName)); + return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state)); + }); + if (resolved) { + return resolved; } - else { - const candidate = ts.normalizePath(ts.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: ts.CompilerOptions, host: ts.ModuleResolutionHost, globalCache: string, packageJsonInfoCache: PackageJsonInfoCache): ts.ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - if (traceEnabled) { - trace(host, ts.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 = ts.normalizePath(ts.combinePaths(containingDirectory, moduleName)); + return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, /*onlyRecordFailures*/ false, state)); } - const failedLookupLocations: string[] = []; - const diagnostics: ts.Diagnostic[] = []; - const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache, features: NodeResolutionFeatures.None, conditions: [], requestContainingDirectory: undefined, reportDiagnostic: diag => void diagnostics.push(diag) }; - const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false, /*cache*/ undefined, /*redirectedReference*/ undefined); - return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations, diagnostics, state.resultFromCache); } +} - /** - * 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; +/** + * 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: ts.CompilerOptions, host: ts.ModuleResolutionHost, globalCache: string, packageJsonInfoCache: PackageJsonInfoCache): ts.ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + if (traceEnabled) { + trace(host, ts.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 diagnostics: ts.Diagnostic[] = []; + const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache, features: NodeResolutionFeatures.None, conditions: [], requestContainingDirectory: undefined, reportDiagnostic: diag => void diagnostics.push(diag) }; + const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false, /*cache*/ undefined, /*redirectedReference*/ undefined); + return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations, diagnostics, state.resultFromCache); +} - /** - * Wraps value to SearchResult. - * @returns undefined if value is undefined or { value } otherwise - */ - function toSearchResult(value: T | undefined): SearchResult { - return value !== undefined ? { value } : undefined; - } +/** + * 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 40451fd75313a..5b0e512f9c9d2 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -1,804 +1,804 @@ // Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers. /* @internal */ namespace ts.moduleSpecifiers { - const enum RelativePreference { - Relative, - NonRelative, - Shortest, - ExternalNonRelative - } - // See UserPreferences#importPathEnding - const enum Ending { - Minimal, - Index, - JsExtension - } - - // Processed preferences - interface Preferences { - readonly relativePreference: RelativePreference; - readonly ending: Ending; - } - - function getPreferences(host: ts.ModuleSpecifierResolutionHost, { importModuleSpecifierPreference, importModuleSpecifierEnding }: ts.UserPreferences, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile): Preferences { - return { - relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : - importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : - importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative : - RelativePreference.Shortest, - 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) || isFormatRequiringExtensions(compilerOptions, importingSourceFile.path, host) ? Ending.JsExtension - : ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; - } +const enum RelativePreference { + Relative, + NonRelative, + Shortest, + ExternalNonRelative +} +// See UserPreferences#importPathEnding +const enum Ending { + Minimal, + Index, + JsExtension +} + +// Processed preferences +interface Preferences { + readonly relativePreference: RelativePreference; + readonly ending: Ending; +} + +function getPreferences(host: ts.ModuleSpecifierResolutionHost, { importModuleSpecifierPreference, importModuleSpecifierEnding }: ts.UserPreferences, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile): Preferences { + return { + relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : + importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : + importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative : + RelativePreference.Shortest, + 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) || isFormatRequiringExtensions(compilerOptions, importingSourceFile.path, host) ? Ending.JsExtension + : ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; } } +} + +function getPreferencesForUpdate(compilerOptions: ts.CompilerOptions, oldImportSpecifier: string, importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost): Preferences { + return { + relativePreference: ts.isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, + ending: ts.hasJSFileExtension(oldImportSpecifier) || isFormatRequiringExtensions(compilerOptions, importingSourceFileName, host) ? + Ending.JsExtension : + ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeJs || ts.endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, + }; +} - function getPreferencesForUpdate(compilerOptions: ts.CompilerOptions, oldImportSpecifier: string, importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost): Preferences { - return { - relativePreference: ts.isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, - ending: ts.hasJSFileExtension(oldImportSpecifier) || isFormatRequiringExtensions(compilerOptions, importingSourceFileName, host) ? - Ending.JsExtension : - ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeJs || ts.endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, - }; +function isFormatRequiringExtensions(compilerOptions: ts.CompilerOptions, importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost) { + if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 + && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { + return false; } + return ts.getImpliedNodeFormatForFile(importingSourceFileName, host.getPackageJsonInfoCache?.(), getModuleResolutionHost(host), compilerOptions) !== ts.ModuleKind.CommonJS; +} - function isFormatRequiringExtensions(compilerOptions: ts.CompilerOptions, importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost) { - if (ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.Node16 - && ts.getEmitModuleResolutionKind(compilerOptions) !== ts.ModuleResolutionKind.NodeNext) { - return false; - } - return ts.getImpliedNodeFormatForFile(importingSourceFileName, host.getPackageJsonInfoCache?.(), getModuleResolutionHost(host), compilerOptions) !== ts.ModuleKind.CommonJS; - } - - function getModuleResolutionHost(host: ts.ModuleSpecifierResolutionHost): ts.ModuleResolutionHost { - return { - fileExists: host.fileExists, - readFile: ts.Debug.checkDefined(host.readFile), - directoryExists: host.directoryExists, - getCurrentDirectory: host.getCurrentDirectory, - realpath: host.realpath, - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.(), - }; - } - - // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? - // Because when this is called by the file renamer, `importingSourceFile` is the file being renamed, - // while `importingSourceFileName` its *new* name. We need a source file just to get its - // `impliedNodeFormat` and to detect certain preferences from existing import module specifiers. - export function updateModuleSpecifier(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, oldImportSpecifier: string, options: ts.ModuleSpecifierOptions = {}): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}, options); - if (res === oldImportSpecifier) - return undefined; - return res; - } - - // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? - // Because when this is called by the declaration emitter, `importingSourceFile` is the implementation - // file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the - // one currently being produced; the latter to the one being imported). We need an implementation file - // just to get its `impliedNodeFormat` and to detect certain preferences from existing import module - // specifiers. - export function getModuleSpecifier(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, options: ts.ModuleSpecifierOptions = {}): string { - return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}, options); - } - - export function getNodeModulesPackageName(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, nodeModulesFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): string | undefined { - const info = getInfo(importingSourceFile.path, host); - const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences, options); - return ts.firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, /*packageNameOnly*/ true, options.overrideImportMode)); - } - function getModuleSpecifierWorker(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: Preferences, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): string { - const info = getInfo(importingSourceFileName, host); - const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences, options); - return ts.firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode)) || - getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); - } - - export function tryGetModuleSpecifiersFromCache(moduleSymbol: ts.Symbol, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] | undefined { - return tryGetModuleSpecifiersFromCacheWorker(moduleSymbol, importingSourceFile, host, userPreferences, options)[0]; - } - function tryGetModuleSpecifiersFromCacheWorker(moduleSymbol: ts.Symbol, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly [ - specifiers?: readonly string[], - moduleFile?: ts.SourceFile, - modulePaths?: readonly ts.ModulePath[], - cache?: ts.ModuleSpecifierCache - ] { - const moduleSourceFile = ts.getSourceFileOfModule(moduleSymbol); - if (!moduleSourceFile) { - return ts.emptyArray as [ - ]; - } +function getModuleResolutionHost(host: ts.ModuleSpecifierResolutionHost): ts.ModuleResolutionHost { + return { + fileExists: host.fileExists, + readFile: ts.Debug.checkDefined(host.readFile), + directoryExists: host.directoryExists, + getCurrentDirectory: host.getCurrentDirectory, + realpath: host.realpath, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.(), + }; +} - const cache = host.getModuleSpecifierCache?.(); - const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); - return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; - } +// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? +// Because when this is called by the file renamer, `importingSourceFile` is the file being renamed, +// while `importingSourceFileName` its *new* name. We need a source file just to get its +// `impliedNodeFormat` and to detect certain preferences from existing import module specifiers. +export function updateModuleSpecifier(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, oldImportSpecifier: string, options: ts.ModuleSpecifierOptions = {}): string | undefined { + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}, options); + if (res === oldImportSpecifier) + return undefined; + return res; +} - /** Returns an import for each symlink and for the realpath. */ - export function getModuleSpecifiers(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] { - return getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, importingSourceFile, host, userPreferences, options).moduleSpecifiers; - } - export function getModuleSpecifiersWithCacheInfo(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): { - moduleSpecifiers: readonly string[]; - computedWithoutCache: boolean; - } { - let computedWithoutCache = false; - const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker); - if (ambient) - return { moduleSpecifiers: [ambient], computedWithoutCache }; - - // eslint-disable-next-line prefer-const - let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(moduleSymbol, importingSourceFile, host, userPreferences, options); - if (specifiers) - return { moduleSpecifiers: specifiers, computedWithoutCache }; - if (!moduleSourceFile) - return { moduleSpecifiers: ts.emptyArray, computedWithoutCache }; - - computedWithoutCache = true; - modulePaths ||= getAllModulePathsWorker(importingSourceFile.path, moduleSourceFile.originalFileName, host); - const result = computeModuleSpecifiers(modulePaths, compilerOptions, importingSourceFile, host, userPreferences, options); - cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, options, modulePaths, result); - return { moduleSpecifiers: result, computedWithoutCache }; - } - - function computeModuleSpecifiers(modulePaths: readonly ts.ModulePath[], compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] { - const info = getInfo(importingSourceFile.path, host); - const preferences = getPreferences(host, userPreferences, compilerOptions, importingSourceFile); - const existingSpecifier = ts.forEach(modulePaths, modulePath => ts.forEach(host.getFileIncludeReasons().get(ts.toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { - if (reason.kind !== ts.FileIncludeKind.Import || reason.file !== importingSourceFile.path) - return undefined; - // If the candidate import mode doesn't match the mode we're generating for, don't consider it - // TODO: maybe useful to keep around as an alternative option for certain contexts where the mode is overridable - if (importingSourceFile.impliedNodeFormat && importingSourceFile.impliedNodeFormat !== ts.getModeForResolutionAtIndex(importingSourceFile, reason.index)) - return undefined; - const specifier = ts.getModuleNameStringLiteralAt(importingSourceFile, reason.index).text; - // If the preference is for non relative and the module specifier is relative, ignore it - return preferences.relativePreference !== RelativePreference.NonRelative || !ts.pathIsRelative(specifier) ? - specifier : - undefined; - })); - if (existingSpecifier) { - const moduleSpecifiers = [existingSpecifier]; - return moduleSpecifiers; - } +// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? +// Because when this is called by the declaration emitter, `importingSourceFile` is the implementation +// file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the +// one currently being produced; the latter to the one being imported). We need an implementation file +// just to get its `impliedNodeFormat` and to detect certain preferences from existing import module +// specifiers. +export function getModuleSpecifier(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, options: ts.ModuleSpecifierOptions = {}): string { + return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}, options); +} - const importedFileIsInNodeModules = ts.some(modulePaths, p => p.isInNodeModules); - - // Module specifier priority: - // 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry - // 2. Specifiers generated using "paths" from tsconfig - // 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file") - // 4. Relative paths - let nodeModulesSpecifiers: string[] | undefined; - let pathsSpecifiers: string[] | undefined; - let relativeSpecifiers: string[] | undefined; - for (const modulePath of modulePaths) { - const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode); - nodeModulesSpecifiers = ts.append(nodeModulesSpecifiers, specifier); - if (specifier && modulePath.isRedirect) { - // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", - // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. - return nodeModulesSpecifiers!; - } +export function getNodeModulesPackageName(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, nodeModulesFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): string | undefined { + const info = getInfo(importingSourceFile.path, host); + const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences, options); + return ts.firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, /*packageNameOnly*/ true, options.overrideImportMode)); +} +function getModuleSpecifierWorker(compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, importingSourceFileName: ts.Path, toFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: Preferences, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): string { + const info = getInfo(importingSourceFileName, host); + const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences, options); + return ts.firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode)) || + getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); +} - if (!specifier && !modulePath.isRedirect) { - const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, preferences); - if (ts.pathIsBareSpecifier(local)) { - pathsSpecifiers = ts.append(pathsSpecifiers, local); - } - else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) { - // Why this extra conditional, not just an `else`? If some path to the file contained - // 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"), - // that means we had to go through a *sibling's* node_modules, not one we can access directly. - // If some path to the file was in node_modules but another was not, this likely indicates that - // we have a monorepo structure with symlinks. In this case, the non-node_modules path is - // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package - // in a monorepo is probably not portable. So, the module specifier we actually go with will be - // the relative path through node_modules, so that the declaration emitter can produce a - // portability error. (See declarationEmitReexportedSymlinkReference3) - relativeSpecifiers = ts.append(relativeSpecifiers, local); - } - } - } +export function tryGetModuleSpecifiersFromCache(moduleSymbol: ts.Symbol, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] | undefined { + return tryGetModuleSpecifiersFromCacheWorker(moduleSymbol, importingSourceFile, host, userPreferences, options)[0]; +} +function tryGetModuleSpecifiersFromCacheWorker(moduleSymbol: ts.Symbol, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly [ + specifiers?: readonly string[], + moduleFile?: ts.SourceFile, + modulePaths?: readonly ts.ModulePath[], + cache?: ts.ModuleSpecifierCache +] { + const moduleSourceFile = ts.getSourceFileOfModule(moduleSymbol); + if (!moduleSourceFile) { + return ts.emptyArray as [ + ]; + } + + const cache = host.getModuleSpecifierCache?.(); + const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); + return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; +} - return pathsSpecifiers?.length ? pathsSpecifiers : - nodeModulesSpecifiers?.length ? nodeModulesSpecifiers : - ts.Debug.checkDefined(relativeSpecifiers); - } +/** Returns an import for each symlink and for the realpath. */ +export function getModuleSpecifiers(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] { + return getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, importingSourceFile, host, userPreferences, options).moduleSpecifiers; +} +export function getModuleSpecifiersWithCacheInfo(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): { + moduleSpecifiers: readonly string[]; + computedWithoutCache: boolean; +} { + let computedWithoutCache = false; + const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker); + if (ambient) + return { moduleSpecifiers: [ambient], computedWithoutCache }; + + // eslint-disable-next-line prefer-const + let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(moduleSymbol, importingSourceFile, host, userPreferences, options); + if (specifiers) + return { moduleSpecifiers: specifiers, computedWithoutCache }; + if (!moduleSourceFile) + return { moduleSpecifiers: ts.emptyArray, computedWithoutCache }; + + computedWithoutCache = true; + modulePaths ||= getAllModulePathsWorker(importingSourceFile.path, moduleSourceFile.originalFileName, host); + const result = computeModuleSpecifiers(modulePaths, compilerOptions, importingSourceFile, host, userPreferences, options); + cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, options, modulePaths, result); + return { moduleSpecifiers: result, computedWithoutCache }; +} - interface Info { - readonly getCanonicalFileName: ts.GetCanonicalFileName; - readonly importingSourceFileName: ts.Path; - readonly sourceDirectory: ts.Path; - } - // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path - function getInfo(importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost): Info { - const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); - const sourceDirectory = ts.getDirectoryPath(importingSourceFileName); - return { getCanonicalFileName, importingSourceFileName, sourceDirectory }; +function computeModuleSpecifiers(modulePaths: readonly ts.ModulePath[], compilerOptions: ts.CompilerOptions, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, userPreferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}): readonly string[] { + const info = getInfo(importingSourceFile.path, host); + const preferences = getPreferences(host, userPreferences, compilerOptions, importingSourceFile); + const existingSpecifier = ts.forEach(modulePaths, modulePath => ts.forEach(host.getFileIncludeReasons().get(ts.toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { + if (reason.kind !== ts.FileIncludeKind.Import || reason.file !== importingSourceFile.path) + return undefined; + // If the candidate import mode doesn't match the mode we're generating for, don't consider it + // TODO: maybe useful to keep around as an alternative option for certain contexts where the mode is overridable + if (importingSourceFile.impliedNodeFormat && importingSourceFile.impliedNodeFormat !== ts.getModeForResolutionAtIndex(importingSourceFile, reason.index)) + return undefined; + const specifier = ts.getModuleNameStringLiteralAt(importingSourceFile, reason.index).text; + // If the preference is for non relative and the module specifier is relative, ignore it + return preferences.relativePreference !== RelativePreference.NonRelative || !ts.pathIsRelative(specifier) ? + specifier : + undefined; + })); + if (existingSpecifier) { + const moduleSpecifiers = [existingSpecifier]; + return moduleSpecifiers; + } + + const importedFileIsInNodeModules = ts.some(modulePaths, p => p.isInNodeModules); + + // Module specifier priority: + // 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry + // 2. Specifiers generated using "paths" from tsconfig + // 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file") + // 4. Relative paths + let nodeModulesSpecifiers: string[] | undefined; + let pathsSpecifiers: string[] | undefined; + let relativeSpecifiers: string[] | undefined; + for (const modulePath of modulePaths) { + const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode); + nodeModulesSpecifiers = ts.append(nodeModulesSpecifiers, specifier); + if (specifier && modulePath.isRedirect) { + // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", + // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. + return nodeModulesSpecifiers!; + } + + if (!specifier && !modulePath.isRedirect) { + const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, preferences); + if (ts.pathIsBareSpecifier(local)) { + pathsSpecifiers = ts.append(pathsSpecifiers, local); + } + else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) { + // Why this extra conditional, not just an `else`? If some path to the file contained + // 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"), + // that means we had to go through a *sibling's* node_modules, not one we can access directly. + // If some path to the file was in node_modules but another was not, this likely indicates that + // we have a monorepo structure with symlinks. In this case, the non-node_modules path is + // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package + // in a monorepo is probably not portable. So, the module specifier we actually go with will be + // the relative path through node_modules, so that the declaration emitter can produce a + // portability error. (See declarationEmitReexportedSymlinkReference3) + relativeSpecifiers = ts.append(relativeSpecifiers, local); + } + } } - function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: ts.CompilerOptions, host: ts.ModuleSpecifierResolutionHost, { ending, relativePreference }: Preferences): string { - const { baseUrl, paths, rootDirs } = compilerOptions; - const { sourceDirectory, getCanonicalFileName } = info; - const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || - removeExtensionAndIndexPostFix(ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); - if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) { - return relativePath; - } + return pathsSpecifiers?.length ? pathsSpecifiers : + nodeModulesSpecifiers?.length ? nodeModulesSpecifiers : + ts.Debug.checkDefined(relativeSpecifiers); +} - const baseDirectory = ts.getNormalizedAbsolutePath(ts.getPathsBasePath(compilerOptions, host) || baseUrl!, host.getCurrentDirectory()); - const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName); - if (!relativeToBaseUrl) { - return relativePath; - } +interface Info { + readonly getCanonicalFileName: ts.GetCanonicalFileName; + readonly importingSourceFileName: ts.Path; + readonly sourceDirectory: ts.Path; +} +// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path +function getInfo(importingSourceFileName: ts.Path, host: ts.ModuleSpecifierResolutionHost): Info { + const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); + const sourceDirectory = ts.getDirectoryPath(importingSourceFileName); + return { getCanonicalFileName, importingSourceFileName, sourceDirectory }; +} - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); - const fromPaths = paths && tryGetModuleNameFromPaths(ts.removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); - const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths; - if (!nonRelative) { - return relativePath; +function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: ts.CompilerOptions, host: ts.ModuleSpecifierResolutionHost, { ending, relativePreference }: Preferences): string { + const { baseUrl, paths, rootDirs } = compilerOptions; + const { sourceDirectory, getCanonicalFileName } = info; + const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || + removeExtensionAndIndexPostFix(ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); + if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) { + return relativePath; + } + + const baseDirectory = ts.getNormalizedAbsolutePath(ts.getPathsBasePath(compilerOptions, host) || baseUrl!, host.getCurrentDirectory()); + const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName); + if (!relativeToBaseUrl) { + return relativePath; + } + + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); + const fromPaths = paths && tryGetModuleNameFromPaths(ts.removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); + const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths; + if (!nonRelative) { + return relativePath; + } + + if (relativePreference === RelativePreference.NonRelative) { + return nonRelative; + } + + if (relativePreference === RelativePreference.ExternalNonRelative) { + const projectDirectory = compilerOptions.configFilePath ? + ts.toPath(ts.getDirectoryPath(compilerOptions.configFilePath), host.getCurrentDirectory(), info.getCanonicalFileName) : + info.getCanonicalFileName(host.getCurrentDirectory()); + const modulePath = ts.toPath(moduleFileName, projectDirectory, getCanonicalFileName); + const sourceIsInternal = ts.startsWith(sourceDirectory, projectDirectory); + const targetIsInternal = ts.startsWith(modulePath, projectDirectory); + if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) { + // 1. The import path crosses the boundary of the tsconfig.json-containing directory. + // + // src/ + // tsconfig.json + // index.ts ------- + // lib/ | (path crosses tsconfig.json) + // imported.ts <--- + // + return nonRelative; } - if (relativePreference === RelativePreference.NonRelative) { + const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, ts.getDirectoryPath(modulePath)); + const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory); + if (nearestSourcePackageJson !== nearestTargetPackageJson) { + // 2. The importing and imported files are part of different packages. + // + // packages/a/ + // package.json + // index.ts -------- + // packages/b/ | (path crosses package.json) + // package.json | + // component.ts <--- + // return nonRelative; } - if (relativePreference === RelativePreference.ExternalNonRelative) { - const projectDirectory = compilerOptions.configFilePath ? - ts.toPath(ts.getDirectoryPath(compilerOptions.configFilePath), host.getCurrentDirectory(), info.getCanonicalFileName) : - info.getCanonicalFileName(host.getCurrentDirectory()); - const modulePath = ts.toPath(moduleFileName, projectDirectory, getCanonicalFileName); - const sourceIsInternal = ts.startsWith(sourceDirectory, projectDirectory); - const targetIsInternal = ts.startsWith(modulePath, projectDirectory); - if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) { - // 1. The import path crosses the boundary of the tsconfig.json-containing directory. - // - // src/ - // tsconfig.json - // index.ts ------- - // lib/ | (path crosses tsconfig.json) - // imported.ts <--- - // - return nonRelative; - } - - const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, ts.getDirectoryPath(modulePath)); - const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory); - if (nearestSourcePackageJson !== nearestTargetPackageJson) { - // 2. The importing and imported files are part of different packages. - // - // packages/a/ - // package.json - // index.ts -------- - // packages/b/ | (path crosses package.json) - // package.json | - // component.ts <--- - // - return nonRelative; - } + return relativePath; + } - return relativePath; - } + if (relativePreference !== RelativePreference.Shortest) + ts.Debug.assertNever(relativePreference); - if (relativePreference !== RelativePreference.Shortest) - ts.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; +} - // 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 = ts.startsWith(path, "./") ? 2 : 0; i < path.length; i++) { + if (path.charCodeAt(i) === ts.CharacterCodes.slash) + count++; } + return count; +} - export function countPathComponents(path: string): number { - let count = 0; - for (let i = ts.startsWith(path, "./") ? 2 : 0; i < path.length; i++) { - if (path.charCodeAt(i) === ts.CharacterCodes.slash) - count++; - } - return count; - } +function usesJsExtensionOnImports({ imports }: ts.SourceFile): boolean { + return ts.firstDefined(imports, ({ text }) => ts.pathIsRelative(text) ? ts.hasJSFileExtension(text) : undefined) || false; +} - function usesJsExtensionOnImports({ imports }: ts.SourceFile): boolean { - return ts.firstDefined(imports, ({ text }) => ts.pathIsRelative(text) ? ts.hasJSFileExtension(text) : undefined) || false; - } +function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ts.ModulePath, b: ts.ModulePath) { + return ts.compareBooleans(b.isRedirect, a.isRedirect) || ts.compareNumberOfDirectorySeparators(a.path, b.path); +} - function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ts.ModulePath, b: ts.ModulePath) { - return ts.compareBooleans(b.isRedirect, a.isRedirect) || ts.compareNumberOfDirectorySeparators(a.path, b.path); +function getNearestAncestorDirectoryWithPackageJson(host: ts.ModuleSpecifierResolutionHost, fileName: string) { + if (host.getNearestAncestorDirectoryWithPackageJson) { + return host.getNearestAncestorDirectoryWithPackageJson(fileName); } + return !!ts.forEachAncestorDirectory(fileName, directory => { + return host.fileExists(ts.combinePaths(directory, "package.json")) ? true : undefined; + }); +} - function getNearestAncestorDirectoryWithPackageJson(host: ts.ModuleSpecifierResolutionHost, fileName: string) { - if (host.getNearestAncestorDirectoryWithPackageJson) { - return host.getNearestAncestorDirectoryWithPackageJson(fileName); - } - return !!ts.forEachAncestorDirectory(fileName, directory => { - return host.fileExists(ts.combinePaths(directory, "package.json")) ? true : undefined; - }); - } +export function forEachFileNameOfModule(importingFileName: string, importedFileName: string, host: ts.ModuleSpecifierResolutionHost, preferSymlinks: boolean, cb: (fileName: string, isRedirect: boolean) => T | undefined): T | undefined { + const getCanonicalFileName = ts.hostGetCanonicalFileName(host); + const cwd = host.getCurrentDirectory(); + const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; + const importedPath = ts.toPath(importedFileName, cwd, getCanonicalFileName); + const redirects = host.redirectTargetsMap.get(importedPath) || ts.emptyArray; + const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : ts.emptyArray), importedFileName, ...redirects]; + const targets = importedFileNames.map(f => ts.getNormalizedAbsolutePath(f, cwd)); + let shouldFilterIgnoredPaths = !ts.every(targets, ts.containsIgnoredPath); + + if (!preferSymlinks) { + // Symlinks inside ignored paths are already filtered out of the symlink cache, + // so we only need to remove them from the realpath filenames. + const result = ts.forEach(targets, p => !(shouldFilterIgnoredPaths && ts.containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); + if (result) + return result; + } + + const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); + const fullImportedFileName = ts.getNormalizedAbsolutePath(importedFileName, cwd); + const result = symlinkedDirectories && ts.forEachAncestorDirectory(ts.getDirectoryPath(fullImportedFileName), realPathDirectory => { + const symlinkDirectories = symlinkedDirectories.get(ts.ensureTrailingDirectorySeparator(ts.toPath(realPathDirectory, cwd, getCanonicalFileName))); + if (!symlinkDirectories) + return undefined; // Continue to ancestor directory + + // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) + if (ts.startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { + return false; // Stop search, each ancestor directory will also hit this condition + } + + return ts.forEach(targets, target => { + if (!ts.startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { + return; + } - export function forEachFileNameOfModule(importingFileName: string, importedFileName: string, host: ts.ModuleSpecifierResolutionHost, preferSymlinks: boolean, cb: (fileName: string, isRedirect: boolean) => T | undefined): T | undefined { - const getCanonicalFileName = ts.hostGetCanonicalFileName(host); - const cwd = host.getCurrentDirectory(); - const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; - const importedPath = ts.toPath(importedFileName, cwd, getCanonicalFileName); - const redirects = host.redirectTargetsMap.get(importedPath) || ts.emptyArray; - const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : ts.emptyArray), importedFileName, ...redirects]; - const targets = importedFileNames.map(f => ts.getNormalizedAbsolutePath(f, cwd)); - let shouldFilterIgnoredPaths = !ts.every(targets, ts.containsIgnoredPath); - - if (!preferSymlinks) { - // Symlinks inside ignored paths are already filtered out of the symlink cache, - // so we only need to remove them from the realpath filenames. - const result = ts.forEach(targets, p => !(shouldFilterIgnoredPaths && ts.containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); - if (result) - return result; - } + const relative = ts.getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); + for (const symlinkDirectory of symlinkDirectories) { + const option = ts.resolvePath(symlinkDirectory, relative); + const result = cb(option, target === referenceRedirect); + shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths + if (result) + return result; + } + }); + }); + return result || (preferSymlinks + ? ts.forEach(targets, p => shouldFilterIgnoredPaths && ts.containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) + : undefined); +} - const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); - const fullImportedFileName = ts.getNormalizedAbsolutePath(importedFileName, cwd); - const result = symlinkedDirectories && ts.forEachAncestorDirectory(ts.getDirectoryPath(fullImportedFileName), realPathDirectory => { - const symlinkDirectories = symlinkedDirectories.get(ts.ensureTrailingDirectorySeparator(ts.toPath(realPathDirectory, cwd, getCanonicalFileName))); - if (!symlinkDirectories) - return undefined; // Continue to ancestor directory +/** + * 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(importingFilePath: ts.Path, importedFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}) { + const importedFilePath = ts.toPath(importedFileName, host.getCurrentDirectory(), ts.hostGetCanonicalFileName(host)); + const cache = host.getModuleSpecifierCache?.(); + if (cache) { + const cached = cache.get(importingFilePath, importedFilePath, preferences, options); + if (cached?.modulePaths) + return cached.modulePaths; + } + const modulePaths = getAllModulePathsWorker(importingFilePath, importedFileName, host); + if (cache) { + cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); + } + return modulePaths; +} - // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) - if (ts.startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { - return false; // Stop search, each ancestor directory will also hit this condition +function getAllModulePathsWorker(importingFileName: ts.Path, importedFileName: string, host: ts.ModuleSpecifierResolutionHost): readonly ts.ModulePath[] { + const getCanonicalFileName = ts.hostGetCanonicalFileName(host); + const allFileNames = new ts.Map(); + let importedFileFromNodeModules = false; + forEachFileNameOfModule(importingFileName, importedFileName, host, + /*preferSymlinks*/ true, (path, isRedirect) => { + const isInNodeModules = ts.pathContainsNodeModules(path); + allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); + importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; + // don't return value, so we collect everything + }); + + // Sort by paths closest to importing file Name directory + const sortedPaths: ts.ModulePath[] = []; + for (let directory = ts.getDirectoryPath(importingFileName); allFileNames.size !== 0;) { + const directoryStart = ts.ensureTrailingDirectorySeparator(directory); + let pathsInDirectory: ts.ModulePath[] | undefined; + allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { + if (ts.startsWith(path, directoryStart)) { + (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); + allFileNames.delete(fileName); + } + }); + if (pathsInDirectory) { + if (pathsInDirectory.length > 1) { + pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); } + sortedPaths.push(...pathsInDirectory); + } + const newDirectory = ts.getDirectoryPath(directory); + if (newDirectory === directory) + break; + directory = newDirectory; + } + if (allFileNames.size) { + const remainingPaths = ts.arrayFrom(allFileNames.values()); + if (remainingPaths.length > 1) + remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); + sortedPaths.push(...remainingPaths); + } - return ts.forEach(targets, target => { - if (!ts.startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { - return; - } + return sortedPaths; +} - const relative = ts.getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); - for (const symlinkDirectory of symlinkDirectories) { - const option = ts.resolvePath(symlinkDirectory, relative); - const result = cb(option, target === referenceRedirect); - shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths - if (result) - return result; - } - }); - }); - return result || (preferSymlinks - ? ts.forEach(targets, p => shouldFilterIgnoredPaths && ts.containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) - : undefined); +function tryGetModuleNameFromAmbientModule(moduleSymbol: ts.Symbol, checker: ts.TypeChecker): string | undefined { + const decl = moduleSymbol.declarations?.find(d => ts.isNonGlobalAmbientModule(d) && (!ts.isExternalModuleAugmentation(d) || !ts.isExternalModuleNameRelative(ts.getTextOfIdentifierOrLiteral(d.name)))) as (ts.ModuleDeclaration & { + name: ts.StringLiteral; + }) | undefined; + if (decl) { + return decl.name.text; } + // the module could be a namespace, which is export through "export=" from an ambient module. /** - * Looks for existing imports that use symlinks to this module. - * Symlinks will be returned first so they are preferred over the real path. + * declare module "m" { + * namespace ns { + * class c {} + * } + * export = ns; + * } */ - function getAllModulePaths(importingFilePath: ts.Path, importedFileName: string, host: ts.ModuleSpecifierResolutionHost, preferences: ts.UserPreferences, options: ts.ModuleSpecifierOptions = {}) { - const importedFilePath = ts.toPath(importedFileName, host.getCurrentDirectory(), ts.hostGetCanonicalFileName(host)); - const cache = host.getModuleSpecifierCache?.(); - if (cache) { - const cached = cache.get(importingFilePath, importedFilePath, preferences, options); - if (cached?.modulePaths) - return cached.modulePaths; - } - const modulePaths = getAllModulePathsWorker(importingFilePath, importedFileName, host); - if (cache) { - cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); - } - return modulePaths; - } - - function getAllModulePathsWorker(importingFileName: ts.Path, importedFileName: string, host: ts.ModuleSpecifierResolutionHost): readonly ts.ModulePath[] { - const getCanonicalFileName = ts.hostGetCanonicalFileName(host); - const allFileNames = new ts.Map(); - let importedFileFromNodeModules = false; - forEachFileNameOfModule(importingFileName, importedFileName, host, - /*preferSymlinks*/ true, (path, isRedirect) => { - const isInNodeModules = ts.pathContainsNodeModules(path); - allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); - importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; - // don't return value, so we collect everything - }); - - // Sort by paths closest to importing file Name directory - const sortedPaths: ts.ModulePath[] = []; - for (let directory = ts.getDirectoryPath(importingFileName); allFileNames.size !== 0;) { - const directoryStart = ts.ensureTrailingDirectorySeparator(directory); - let pathsInDirectory: ts.ModulePath[] | undefined; - allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { - if (ts.startsWith(path, directoryStart)) { - (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); - allFileNames.delete(fileName); - } - }); - if (pathsInDirectory) { - if (pathsInDirectory.length > 1) { - pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); + // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" + const ambientModuleDeclareCandidates = ts.mapDefined(moduleSymbol.declarations, d => { + if (!ts.isModuleDeclaration(d)) + return; + const topNamespace = getTopNamespace(d); + if (!(topNamespace?.parent?.parent + && ts.isModuleBlock(topNamespace.parent) && ts.isAmbientModule(topNamespace.parent.parent) && ts.isSourceFile(topNamespace.parent.parent.parent))) + return; + const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as ts.__String)?.valueDeclaration as ts.ExportAssignment)?.expression as ts.PropertyAccessExpression | ts.Identifier); + if (!exportAssignment) + return; + const exportSymbol = checker.getSymbolAtLocation(exportAssignment); + if (!exportSymbol) + return; + const originalExportSymbol = exportSymbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; + if (originalExportSymbol === d.symbol) + return topNamespace.parent.parent; + function getTopNamespace(namespaceDeclaration: ts.ModuleDeclaration) { + while (namespaceDeclaration.flags & ts.NodeFlags.NestedNamespace) { + namespaceDeclaration = namespaceDeclaration.parent as ts.ModuleDeclaration; } - sortedPaths.push(...pathsInDirectory); + return namespaceDeclaration; } - const newDirectory = ts.getDirectoryPath(directory); - if (newDirectory === directory) - break; - directory = newDirectory; - } - if (allFileNames.size) { - const remainingPaths = ts.arrayFrom(allFileNames.values()); - if (remainingPaths.length > 1) - remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); - sortedPaths.push(...remainingPaths); - } - - return sortedPaths; + }); + const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (ts.AmbientModuleDeclaration & { + name: ts.StringLiteral; + }) | undefined; + if (ambientModuleDeclare) { + return ambientModuleDeclare.name.text; } +} - function tryGetModuleNameFromAmbientModule(moduleSymbol: ts.Symbol, checker: ts.TypeChecker): string | undefined { - const decl = moduleSymbol.declarations?.find(d => ts.isNonGlobalAmbientModule(d) && (!ts.isExternalModuleAugmentation(d) || !ts.isExternalModuleNameRelative(ts.getTextOfIdentifierOrLiteral(d.name)))) as (ts.ModuleDeclaration & { - name: ts.StringLiteral; - }) | undefined; - if (decl) { - return decl.name.text; - } - - // the module could be a namespace, which is export through "export=" from an ambient module. - /** - * declare module "m" { - * namespace ns { - * class c {} - * } - * export = ns; - * } - */ - // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" - const ambientModuleDeclareCandidates = ts.mapDefined(moduleSymbol.declarations, d => { - if (!ts.isModuleDeclaration(d)) - return; - const topNamespace = getTopNamespace(d); - if (!(topNamespace?.parent?.parent - && ts.isModuleBlock(topNamespace.parent) && ts.isAmbientModule(topNamespace.parent.parent) && ts.isSourceFile(topNamespace.parent.parent.parent))) - return; - const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as ts.__String)?.valueDeclaration as ts.ExportAssignment)?.expression as ts.PropertyAccessExpression | ts.Identifier); - if (!exportAssignment) - return; - const exportSymbol = checker.getSymbolAtLocation(exportAssignment); - if (!exportSymbol) - return; - const originalExportSymbol = exportSymbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; - if (originalExportSymbol === d.symbol) - return topNamespace.parent.parent; - function getTopNamespace(namespaceDeclaration: ts.ModuleDeclaration) { - while (namespaceDeclaration.flags & ts.NodeFlags.NestedNamespace) { - namespaceDeclaration = namespaceDeclaration.parent as ts.ModuleDeclaration; - } - return namespaceDeclaration; +function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: ts.MapLike): string | undefined { + for (const key in paths) { + for (const patternText of paths[key]) { + const pattern = ts.removeFileExtension(ts.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 && + ts.startsWith(relativeToBaseUrl, prefix) && + ts.endsWith(relativeToBaseUrl, suffix) || + !suffix && relativeToBaseUrl === ts.removeTrailingDirectorySeparator(prefix)) { + const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length - prefix.length); + return key.replace("*", matchedStar); } - }); - const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (ts.AmbientModuleDeclaration & { - name: ts.StringLiteral; - }) | undefined; - if (ambientModuleDeclare) { - return ambientModuleDeclare.name.text; + } + else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { + return key; + } } } +} - function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: ts.MapLike): string | undefined { - for (const key in paths) { - for (const patternText of paths[key]) { - const pattern = ts.removeFileExtension(ts.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 && - ts.startsWith(relativeToBaseUrl, prefix) && - ts.endsWith(relativeToBaseUrl, suffix) || - !suffix && relativeToBaseUrl === ts.removeTrailingDirectorySeparator(prefix)) { - const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length - prefix.length); - return key.replace("*", matchedStar); - } +const enum MatchingMode { + Exact, + Directory, + Pattern +} + +function tryGetModuleNameFromExports(options: ts.CompilerOptions, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode = MatchingMode.Exact): { + moduleFileToTry: string; +} | undefined { + if (typeof exports === "string") { + const pathOrPattern = ts.getNormalizedAbsolutePath(ts.combinePaths(packageDirectory, exports), /*currentDirectory*/ undefined); + const extensionSwappedTarget = ts.hasTSFileExtension(targetFilePath) ? ts.removeFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) : undefined; + switch (mode) { + case MatchingMode.Exact: + if (ts.comparePaths(targetFilePath, pathOrPattern) === ts.Comparison.EqualTo || (extensionSwappedTarget && ts.comparePaths(extensionSwappedTarget, pathOrPattern) === ts.Comparison.EqualTo)) { + return { moduleFileToTry: packageName }; } - else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { - return key; + break; + case MatchingMode.Directory: + if (ts.containsPath(pathOrPattern, targetFilePath)) { + const fragment = ts.getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false); + return { moduleFileToTry: ts.getNormalizedAbsolutePath(ts.combinePaths(ts.combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) }; } - } + break; + case MatchingMode.Pattern: + const starPos = pathOrPattern.indexOf("*"); + const leadingSlice = pathOrPattern.slice(0, starPos); + const trailingSlice = pathOrPattern.slice(starPos + 1); + if (ts.startsWith(targetFilePath, leadingSlice) && ts.endsWith(targetFilePath, trailingSlice)) { + const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length); + return { moduleFileToTry: packageName.replace("*", starReplacement) }; + } + if (extensionSwappedTarget && ts.startsWith(extensionSwappedTarget, leadingSlice) && ts.endsWith(extensionSwappedTarget, trailingSlice)) { + const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length); + return { moduleFileToTry: packageName.replace("*", starReplacement) }; + } + break; } } - - const enum MatchingMode { - Exact, - Directory, - Pattern - } - - function tryGetModuleNameFromExports(options: ts.CompilerOptions, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode = MatchingMode.Exact): { - moduleFileToTry: string; - } | undefined { - if (typeof exports === "string") { - const pathOrPattern = ts.getNormalizedAbsolutePath(ts.combinePaths(packageDirectory, exports), /*currentDirectory*/ undefined); - const extensionSwappedTarget = ts.hasTSFileExtension(targetFilePath) ? ts.removeFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) : undefined; - switch (mode) { - case MatchingMode.Exact: - if (ts.comparePaths(targetFilePath, pathOrPattern) === ts.Comparison.EqualTo || (extensionSwappedTarget && ts.comparePaths(extensionSwappedTarget, pathOrPattern) === ts.Comparison.EqualTo)) { - return { moduleFileToTry: packageName }; - } - break; - case MatchingMode.Directory: - if (ts.containsPath(pathOrPattern, targetFilePath)) { - const fragment = ts.getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false); - return { moduleFileToTry: ts.getNormalizedAbsolutePath(ts.combinePaths(ts.combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) }; - } - break; - case MatchingMode.Pattern: - const starPos = pathOrPattern.indexOf("*"); - const leadingSlice = pathOrPattern.slice(0, starPos); - const trailingSlice = pathOrPattern.slice(starPos + 1); - if (ts.startsWith(targetFilePath, leadingSlice) && ts.endsWith(targetFilePath, trailingSlice)) { - const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length); - return { moduleFileToTry: packageName.replace("*", starReplacement) }; - } - if (extensionSwappedTarget && ts.startsWith(extensionSwappedTarget, leadingSlice) && ts.endsWith(extensionSwappedTarget, trailingSlice)) { - const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length); - return { moduleFileToTry: packageName.replace("*", starReplacement) }; - } - break; - } - } - else if (Array.isArray(exports)) { - return ts.forEach(exports, e => tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, e, conditions)); + else if (Array.isArray(exports)) { + return ts.forEach(exports, e => tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, e, conditions)); + } + else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-null/no-null + if (ts.allKeysStartWithDot(exports as ts.MapLike)) { + // sub-mappings + // 3 cases: + // * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode)) + // * pattern mappings (contains a *) + // * exact mappings (no *, does not end with /) + return ts.forEach(ts.getOwnKeys(exports as ts.MapLike), k => { + const subPackageName = ts.getNormalizedAbsolutePath(ts.combinePaths(packageName, k), /*currentDirectory*/ undefined); + const mode = ts.endsWith(k, "/") ? MatchingMode.Directory + : ts.stringContains(k, "*") ? MatchingMode.Pattern + : MatchingMode.Exact; + return tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, subPackageName, (exports as ts.MapLike)[k], conditions, mode); + }); } - else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-null/no-null - if (ts.allKeysStartWithDot(exports as ts.MapLike)) { - // sub-mappings - // 3 cases: - // * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode)) - // * pattern mappings (contains a *) - // * exact mappings (no *, does not end with /) - return ts.forEach(ts.getOwnKeys(exports as ts.MapLike), k => { - const subPackageName = ts.getNormalizedAbsolutePath(ts.combinePaths(packageName, k), /*currentDirectory*/ undefined); - const mode = ts.endsWith(k, "/") ? MatchingMode.Directory - : ts.stringContains(k, "*") ? MatchingMode.Pattern - : MatchingMode.Exact; - return tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, subPackageName, (exports as ts.MapLike)[k], conditions, mode); - }); - } - else { - // conditional mapping - for (const key of ts.getOwnKeys(exports as ts.MapLike)) { - if (key === "default" || conditions.indexOf(key) >= 0 || ts.isApplicableVersionedTypesKey(conditions, key)) { - const subTarget = (exports as ts.MapLike)[key]; - const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions); - if (result) { - return result; - } + else { + // conditional mapping + for (const key of ts.getOwnKeys(exports as ts.MapLike)) { + if (key === "default" || conditions.indexOf(key) >= 0 || ts.isApplicableVersionedTypesKey(conditions, key)) { + const subTarget = (exports as ts.MapLike)[key]; + const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions); + if (result) { + return result; } } } } + } + return undefined; +} + +function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: ts.CompilerOptions): string | undefined { + const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); + if (normalizedTargetPath === undefined) { return undefined; } - function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: ts.CompilerOptions): string | undefined { - const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); - if (normalizedTargetPath === undefined) { - return undefined; - } + const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); + const relativePath = normalizedSourcePath !== undefined ? ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; + return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeJs + ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) + : ts.removeFileExtension(relativePath); +} - const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); - const relativePath = normalizedSourcePath !== undefined ? ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; - return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeJs - ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) - : ts.removeFileExtension(relativePath); +function tryGetModuleNameAsNodeModule({ path, isRedirect }: ts.ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, options: ts.CompilerOptions, userPreferences: ts.UserPreferences, packageNameOnly?: boolean, overrideMode?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS): string | undefined { + if (!host.fileExists || !host.readFile) { + return undefined; + } + const parts: ts.NodeModulePathParts = ts.getNodeModulePathParts(path)!; + if (!parts) { + return undefined; } - function tryGetModuleNameAsNodeModule({ path, isRedirect }: ts.ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: ts.SourceFile, host: ts.ModuleSpecifierResolutionHost, options: ts.CompilerOptions, userPreferences: ts.UserPreferences, packageNameOnly?: boolean, overrideMode?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS): string | undefined { - if (!host.fileExists || !host.readFile) { - return undefined; - } - const parts: ts.NodeModulePathParts = ts.getNodeModulePathParts(path)!; - if (!parts) { - return undefined; - } - - // Simplify the full file path to something that can be resolved by Node. - - let moduleSpecifier = path; - let isPackageRootPath = false; - if (!packageNameOnly) { - const preferences = getPreferences(host, userPreferences, options, importingSourceFile); - let packageRootIndex = parts.packageRootIndex; - let moduleFileName: string | undefined; - while (true) { - // If the module could be imported by a directory name, use that directory's name - const { moduleFileToTry, packageRootPath, blockedByExports, verbatimFromExports } = tryDirectoryWithPackageJson(packageRootIndex); - if (ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Classic) { - if (blockedByExports) { - return undefined; // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution - } - if (verbatimFromExports) { - return moduleFileToTry; - } - } - if (packageRootPath) { - moduleSpecifier = packageRootPath; - isPackageRootPath = true; - break; + // Simplify the full file path to something that can be resolved by Node. + + let moduleSpecifier = path; + let isPackageRootPath = false; + if (!packageNameOnly) { + const preferences = getPreferences(host, userPreferences, options, importingSourceFile); + let packageRootIndex = parts.packageRootIndex; + let moduleFileName: string | undefined; + while (true) { + // If the module could be imported by a directory name, use that directory's name + const { moduleFileToTry, packageRootPath, blockedByExports, verbatimFromExports } = tryDirectoryWithPackageJson(packageRootIndex); + if (ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Classic) { + if (blockedByExports) { + return undefined; // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution } - if (!moduleFileName) - moduleFileName = moduleFileToTry; - - // try with next level of directory - packageRootIndex = path.indexOf(ts.directorySeparator, packageRootIndex + 1); - if (packageRootIndex === -1) { - moduleSpecifier = removeExtensionAndIndexPostFix(moduleFileName, preferences.ending, options, host); - break; + if (verbatimFromExports) { + return moduleFileToTry; } } - } + if (packageRootPath) { + moduleSpecifier = packageRootPath; + isPackageRootPath = true; + break; + } + if (!moduleFileName) + moduleFileName = moduleFileToTry; - if (isRedirect && !isPackageRootPath) { - return undefined; + // try with next level of directory + packageRootIndex = path.indexOf(ts.directorySeparator, packageRootIndex + 1); + if (packageRootIndex === -1) { + moduleSpecifier = removeExtensionAndIndexPostFix(moduleFileName, preferences.ending, options, host); + break; + } } + } - 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 (!(ts.startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && ts.startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { - return undefined; - } + if (isRedirect && !isPackageRootPath) { + return undefined; + } - // If the module was found in @types, get the actual Node package name - const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); - const packageName = ts.getPackageNameFromTypesPackageName(nodeModulesDirectoryName); - // For classic resolution, only allow importing from node_modules/@types, not other node_modules - return ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName; - function tryDirectoryWithPackageJson(packageRootIndex: number): { - moduleFileToTry: string; - packageRootPath?: string; - blockedByExports?: true; - verbatimFromExports?: true; - } { - const packageRootPath = path.substring(0, packageRootIndex); - const packageJsonPath = ts.combinePaths(packageRootPath, "package.json"); - let moduleFileToTry = path; - const cachedPackageJson = host.getPackageJsonInfoCache?.()?.getPackageJsonInfo(packageJsonPath); - if (typeof cachedPackageJson === "object" || cachedPackageJson === undefined && host.fileExists(packageJsonPath)) { - const packageJsonContent = cachedPackageJson?.packageJsonContent || JSON.parse(host.readFile!(packageJsonPath)!); - if (ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext) { - // `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate - // an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is - // usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex` - // with, so for now we just stick with the mode of the file. - const conditions = ["node", overrideMode || importingSourceFile.impliedNodeFormat === ts.ModuleKind.ESNext ? "import" : "require", "types"]; - const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" - ? tryGetModuleNameFromExports(options, path, packageRootPath, ts.getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions) - : undefined; - if (fromExports) { - const withJsExtension = !ts.hasTSFileExtension(fromExports.moduleFileToTry) - ? fromExports - : { moduleFileToTry: ts.removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; - return { ...withJsExtension, verbatimFromExports: true }; - } - if (packageJsonContent.exports) { - return { moduleFileToTry: path, blockedByExports: true }; - } - } - const versionPaths = packageJsonContent.typesVersions - ? ts.getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) + 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 (!(ts.startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && ts.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 = ts.getPackageNameFromTypesPackageName(nodeModulesDirectoryName); + // For classic resolution, only allow importing from node_modules/@types, not other node_modules + return ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName; + function tryDirectoryWithPackageJson(packageRootIndex: number): { + moduleFileToTry: string; + packageRootPath?: string; + blockedByExports?: true; + verbatimFromExports?: true; + } { + const packageRootPath = path.substring(0, packageRootIndex); + const packageJsonPath = ts.combinePaths(packageRootPath, "package.json"); + let moduleFileToTry = path; + const cachedPackageJson = host.getPackageJsonInfoCache?.()?.getPackageJsonInfo(packageJsonPath); + if (typeof cachedPackageJson === "object" || cachedPackageJson === undefined && host.fileExists(packageJsonPath)) { + const packageJsonContent = cachedPackageJson?.packageJsonContent || JSON.parse(host.readFile!(packageJsonPath)!); + if (ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.Node16 || ts.getEmitModuleResolutionKind(options) === ts.ModuleResolutionKind.NodeNext) { + // `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate + // an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is + // usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex` + // with, so for now we just stick with the mode of the file. + const conditions = ["node", overrideMode || importingSourceFile.impliedNodeFormat === ts.ModuleKind.ESNext ? "import" : "require", "types"]; + const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" + ? tryGetModuleNameFromExports(options, path, packageRootPath, ts.getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions) : undefined; - if (versionPaths) { - const subModuleName = path.slice(packageRootPath.length + 1); - const fromPaths = tryGetModuleNameFromPaths(ts.removeFileExtension(subModuleName), removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), versionPaths.paths); - if (fromPaths !== undefined) { - moduleFileToTry = ts.combinePaths(packageRootPath, fromPaths); - } + if (fromExports) { + const withJsExtension = !ts.hasTSFileExtension(fromExports.moduleFileToTry) + ? fromExports + : { moduleFileToTry: ts.removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; + return { ...withJsExtension, verbatimFromExports: true }; } - // If the file is the main module, it can be imported by the package name - const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main || "index.js"; - if (ts.isString(mainFileRelative)) { - const mainExportFile = ts.toPath(mainFileRelative, packageRootPath, getCanonicalFileName); - if (ts.removeFileExtension(mainExportFile) === ts.removeFileExtension(getCanonicalFileName(moduleFileToTry))) { - return { packageRootPath, moduleFileToTry }; - } + if (packageJsonContent.exports) { + return { moduleFileToTry: path, blockedByExports: true }; } } - else { - // No package.json exists; an index.js will still resolve as the package name - const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1)); - if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") { - return { moduleFileToTry, packageRootPath }; + const versionPaths = packageJsonContent.typesVersions + ? ts.getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) + : undefined; + if (versionPaths) { + const subModuleName = path.slice(packageRootPath.length + 1); + const fromPaths = tryGetModuleNameFromPaths(ts.removeFileExtension(subModuleName), removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), versionPaths.paths); + if (fromPaths !== undefined) { + moduleFileToTry = ts.combinePaths(packageRootPath, fromPaths); + } + } + // If the file is the main module, it can be imported by the package name + const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main || "index.js"; + if (ts.isString(mainFileRelative)) { + const mainExportFile = ts.toPath(mainFileRelative, packageRootPath, getCanonicalFileName); + if (ts.removeFileExtension(mainExportFile) === ts.removeFileExtension(getCanonicalFileName(moduleFileToTry))) { + return { packageRootPath, moduleFileToTry }; } } - return { moduleFileToTry }; } - } - - function tryGetAnyFileFromPath(host: ts.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 = ts.flatten(ts.getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ts.ScriptKind.JSON }])); - for (const e of extensions) { - const fullPath = path + e; - if (host.fileExists(fullPath)) { - return fullPath; + else { + // No package.json exists; an index.js will still resolve as the package name + const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1)); + if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") { + return { moduleFileToTry, packageRootPath }; } } + return { moduleFileToTry }; } +} - function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: ts.GetCanonicalFileName): string | undefined { - return ts.firstDefined(rootDirs, rootDir => { - const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName); - return relativePath !== undefined && isPathRelativeToParent(relativePath) ? undefined : relativePath; - }); +function tryGetAnyFileFromPath(host: ts.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 = ts.flatten(ts.getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ts.ScriptKind.JSON }])); + for (const e of extensions) { + const fullPath = path + e; + if (host.fileExists(fullPath)) { + return fullPath; + } } +} - function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: ts.CompilerOptions, host?: ts.ModuleSpecifierResolutionHost): string { - if (ts.fileExtensionIsOneOf(fileName, [ts.Extension.Json, ts.Extension.Mjs, ts.Extension.Cjs])) - return fileName; - const noExtension = ts.removeFileExtension(fileName); - if (fileName === noExtension) - return fileName; - if (ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dmts, ts.Extension.Mts, ts.Extension.Dcts, ts.Extension.Cts])) - return noExtension + getJSExtensionForFile(fileName, options); - switch (ending) { - case Ending.Minimal: - const withoutIndex = ts.removeSuffix(noExtension, "/index"); - if (host && withoutIndex !== noExtension && tryGetAnyFileFromPath(host, withoutIndex)) { - // Can't remove index if there's a file by the same name as the directory. - // Probably more callers should pass `host` so we can determine this? - return noExtension; - } - return withoutIndex; - case Ending.Index: +function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: ts.GetCanonicalFileName): string | undefined { + return ts.firstDefined(rootDirs, rootDir => { + const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName); + return relativePath !== undefined && isPathRelativeToParent(relativePath) ? undefined : relativePath; + }); +} + +function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: ts.CompilerOptions, host?: ts.ModuleSpecifierResolutionHost): string { + if (ts.fileExtensionIsOneOf(fileName, [ts.Extension.Json, ts.Extension.Mjs, ts.Extension.Cjs])) + return fileName; + const noExtension = ts.removeFileExtension(fileName); + if (fileName === noExtension) + return fileName; + if (ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dmts, ts.Extension.Mts, ts.Extension.Dcts, ts.Extension.Cts])) + return noExtension + getJSExtensionForFile(fileName, options); + switch (ending) { + case Ending.Minimal: + const withoutIndex = ts.removeSuffix(noExtension, "/index"); + if (host && withoutIndex !== noExtension && tryGetAnyFileFromPath(host, withoutIndex)) { + // Can't remove index if there's a file by the same name as the directory. + // Probably more callers should pass `host` so we can determine this? return noExtension; - case Ending.JsExtension: - return noExtension + getJSExtensionForFile(fileName, options); - default: - return ts.Debug.assertNever(ending); - } + } + return withoutIndex; + case Ending.Index: + return noExtension; + case Ending.JsExtension: + return noExtension + getJSExtensionForFile(fileName, options); + default: + return ts.Debug.assertNever(ending); } +} - function getJSExtensionForFile(fileName: string, options: ts.CompilerOptions): ts.Extension { - return tryGetJSExtensionForFile(fileName, options) ?? ts.Debug.fail(`Extension ${ts.extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`); - } - - export function tryGetJSExtensionForFile(fileName: string, options: ts.CompilerOptions): ts.Extension | undefined { - const ext = ts.tryGetExtensionFromPath(fileName); - switch (ext) { - case ts.Extension.Ts: - case ts.Extension.Dts: - return ts.Extension.Js; - case ts.Extension.Tsx: - return options.jsx === ts.JsxEmit.Preserve ? ts.Extension.Jsx : ts.Extension.Js; - case ts.Extension.Js: - case ts.Extension.Jsx: - case ts.Extension.Json: - return ext; - case ts.Extension.Dmts: - case ts.Extension.Mts: - case ts.Extension.Mjs: - return ts.Extension.Mjs; - case ts.Extension.Dcts: - case ts.Extension.Cts: - case ts.Extension.Cjs: - return ts.Extension.Cjs; - default: - return undefined; - } - } +function getJSExtensionForFile(fileName: string, options: ts.CompilerOptions): ts.Extension { + return tryGetJSExtensionForFile(fileName, options) ?? ts.Debug.fail(`Extension ${ts.extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`); +} - function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: ts.GetCanonicalFileName): string | undefined { - const relativePath = ts.getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return ts.isRootedDiskPath(relativePath) ? undefined : relativePath; +export function tryGetJSExtensionForFile(fileName: string, options: ts.CompilerOptions): ts.Extension | undefined { + const ext = ts.tryGetExtensionFromPath(fileName); + switch (ext) { + case ts.Extension.Ts: + case ts.Extension.Dts: + return ts.Extension.Js; + case ts.Extension.Tsx: + return options.jsx === ts.JsxEmit.Preserve ? ts.Extension.Jsx : ts.Extension.Js; + case ts.Extension.Js: + case ts.Extension.Jsx: + case ts.Extension.Json: + return ext; + case ts.Extension.Dmts: + case ts.Extension.Mts: + case ts.Extension.Mjs: + return ts.Extension.Mjs; + case ts.Extension.Dcts: + case ts.Extension.Cts: + case ts.Extension.Cjs: + return ts.Extension.Cjs; + default: + return undefined; } +} - function isPathRelativeToParent(path: string): boolean { - return ts.startsWith(path, ".."); - } +function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: ts.GetCanonicalFileName): string | undefined { + const relativePath = ts.getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return ts.isRootedDiskPath(relativePath) ? undefined : relativePath; +} + +function isPathRelativeToParent(path: string): boolean { + return ts.startsWith(path, ".."); +} } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 4d7f7b80d95f9..418eed0c04f46 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1,5383 +1,5268 @@ namespace ts { - const enum SignatureFlags { - None = 0, - Yield = 1 << 0, - Await = 1 << 1, - Type = 1 << 2, - IgnoreMissingOpenBrace = 1 << 4, - JSDoc = 1 << 5 - } - - const enum SpeculationKind { - TryParse, - Lookahead, - Reparse - } - - let NodeConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let TokenConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let IdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; - let SourceFileConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; +const enum SignatureFlags { + None = 0, + Yield = 1 << 0, + Await = 1 << 1, + Type = 1 << 2, + IgnoreMissingOpenBrace = 1 << 4, + JSDoc = 1 << 5 +} - /** - * NOTE: You should not use this, it is only exported to support `createNode` in `~/src/deprecatedCompat/deprecations.ts`. - */ - /* @internal */ - export const parseBaseNodeFactory: ts.BaseNodeFactory = { - createBaseSourceFileNode: kind => new (SourceFileConstructor || (SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor()))(kind, -1, -1), - createBaseIdentifierNode: kind => new (IdentifierConstructor || (IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor()))(kind, -1, -1), - createBasePrivateIdentifierNode: kind => new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor()))(kind, -1, -1), - createBaseTokenNode: kind => new (TokenConstructor || (TokenConstructor = ts.objectAllocator.getTokenConstructor()))(kind, -1, -1), - createBaseNode: kind => new (NodeConstructor || (NodeConstructor = ts.objectAllocator.getNodeConstructor()))(kind, -1, -1), - }; +const enum SpeculationKind { + TryParse, + Lookahead, + Reparse +} - /* @internal */ - export const parseNodeFactory = ts.createNodeFactory(ts.NodeFactoryFlags.NoParenthesizerRules, parseBaseNodeFactory); - function visitNode(cbNode: (node: ts.Node) => T, node: ts.Node | undefined): T | undefined { - return node && cbNode(node); - } +let NodeConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; +let TokenConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; +let IdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; +let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; +let SourceFileConstructor: new (kind: ts.SyntaxKind, pos?: number, end?: number) => ts.Node; + +/** + * NOTE: You should not use this, it is only exported to support `createNode` in `~/src/deprecatedCompat/deprecations.ts`. + */ +/* @internal */ +export const parseBaseNodeFactory: ts.BaseNodeFactory = { + createBaseSourceFileNode: kind => new (SourceFileConstructor || (SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor()))(kind, -1, -1), + createBaseIdentifierNode: kind => new (IdentifierConstructor || (IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor()))(kind, -1, -1), + createBasePrivateIdentifierNode: kind => new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor()))(kind, -1, -1), + createBaseTokenNode: kind => new (TokenConstructor || (TokenConstructor = ts.objectAllocator.getTokenConstructor()))(kind, -1, -1), + createBaseNode: kind => new (NodeConstructor || (NodeConstructor = ts.objectAllocator.getNodeConstructor()))(kind, -1, -1), +}; + +/* @internal */ +export const parseNodeFactory = ts.createNodeFactory(ts.NodeFactoryFlags.NoParenthesizerRules, parseBaseNodeFactory); +function visitNode(cbNode: (node: ts.Node) => T, node: ts.Node | undefined): T | undefined { + return node && cbNode(node); +} - function visitNodes(cbNode: (node: ts.Node) => T, cbNodes: ((node: ts.NodeArray) => T | undefined) | undefined, nodes: ts.NodeArray | undefined): T | undefined { - if (nodes) { - if (cbNodes) { - return cbNodes(nodes); - } - for (const node of nodes) { - const result = cbNode(node); - if (result) { - return result; - } +function visitNodes(cbNode: (node: ts.Node) => T, cbNodes: ((node: ts.NodeArray) => T | undefined) | undefined, nodes: ts.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) === ts.CharacterCodes.asterisk && - text.charCodeAt(start + 2) === ts.CharacterCodes.asterisk && - text.charCodeAt(start + 3) !== ts.CharacterCodes.slash; - } +/*@internal*/ +export function isJSDocLikeText(text: string, start: number) { + return text.charCodeAt(start + 1) === ts.CharacterCodes.asterisk && + text.charCodeAt(start + 2) === ts.CharacterCodes.asterisk && + text.charCodeAt(start + 3) !== ts.CharacterCodes.slash; +} - /*@internal*/ - export function isFileProbablyExternalModule(sourceFile: ts.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. - return ts.forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) || - getImportMetaIfNecessary(sourceFile); - } +/*@internal*/ +export function isFileProbablyExternalModule(sourceFile: ts.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. + return ts.forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) || + getImportMetaIfNecessary(sourceFile); +} - function isAnExternalModuleIndicatorNode(node: ts.Node) { - return hasModifierOfKind(node, ts.SyntaxKind.ExportKeyword) - || ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) - || ts.isImportDeclaration(node) - || ts.isExportAssignment(node) - || ts.isExportDeclaration(node) ? node : undefined; - } - function getImportMetaIfNecessary(sourceFile: ts.SourceFile) { - return sourceFile.flags & ts.NodeFlags.PossiblyContainsImportMeta ? - walkTreeForImportMeta(sourceFile) : - undefined; - } +function isAnExternalModuleIndicatorNode(node: ts.Node) { + return hasModifierOfKind(node, ts.SyntaxKind.ExportKeyword) + || ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) + || ts.isImportDeclaration(node) + || ts.isExportAssignment(node) + || ts.isExportDeclaration(node) ? node : undefined; +} +function getImportMetaIfNecessary(sourceFile: ts.SourceFile) { + return sourceFile.flags & ts.NodeFlags.PossiblyContainsImportMeta ? + walkTreeForImportMeta(sourceFile) : + undefined; +} - function walkTreeForImportMeta(node: ts.Node): ts.Node | undefined { - return isImportMeta(node) ? node : forEachChild(node, walkTreeForImportMeta); - } +function walkTreeForImportMeta(node: ts.Node): ts.Node | undefined { + return isImportMeta(node) ? node : forEachChild(node, walkTreeForImportMeta); +} - /** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */ - function hasModifierOfKind(node: ts.Node, kind: ts.SyntaxKind) { - return ts.some(node.modifiers, m => m.kind === kind); - } +/** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */ +function hasModifierOfKind(node: ts.Node, kind: ts.SyntaxKind) { + return ts.some(node.modifiers, m => m.kind === kind); +} + +function isImportMeta(node: ts.Node): boolean { + return ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword && node.name.escapedText === "meta"; +} - function isImportMeta(node: ts.Node): boolean { - return ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword && node.name.escapedText === "meta"; +/** + * 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: ts.Node, cbNode: (node: ts.Node) => T | undefined, cbNodes?: (nodes: ts.NodeArray) => T | undefined): T | undefined { + if (!node || node.kind <= ts.SyntaxKind.LastToken) { + return; } + switch (node.kind) { + case ts.SyntaxKind.QualifiedName: + return visitNode(cbNode, (node as ts.QualifiedName).left) || + visitNode(cbNode, (node as ts.QualifiedName).right); + case ts.SyntaxKind.TypeParameter: + return visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.TypeParameterDeclaration).name) || + visitNode(cbNode, (node as ts.TypeParameterDeclaration).constraint) || + visitNode(cbNode, (node as ts.TypeParameterDeclaration).default) || + visitNode(cbNode, (node as ts.TypeParameterDeclaration).expression); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).name) || + visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).questionToken) || + visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).exclamationToken) || + visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).equalsToken) || + visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).objectAssignmentInitializer); + case ts.SyntaxKind.SpreadAssignment: + return visitNode(cbNode, (node as ts.SpreadAssignment).expression); + case ts.SyntaxKind.Parameter: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ParameterDeclaration).dotDotDotToken) || + visitNode(cbNode, (node as ts.ParameterDeclaration).name) || + visitNode(cbNode, (node as ts.ParameterDeclaration).questionToken) || + visitNode(cbNode, (node as ts.ParameterDeclaration).type) || + visitNode(cbNode, (node as ts.ParameterDeclaration).initializer); + case ts.SyntaxKind.PropertyDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.PropertyDeclaration).name) || + visitNode(cbNode, (node as ts.PropertyDeclaration).questionToken) || + visitNode(cbNode, (node as ts.PropertyDeclaration).exclamationToken) || + visitNode(cbNode, (node as ts.PropertyDeclaration).type) || + visitNode(cbNode, (node as ts.PropertyDeclaration).initializer); + case ts.SyntaxKind.PropertySignature: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.PropertySignature).name) || + visitNode(cbNode, (node as ts.PropertySignature).questionToken) || + visitNode(cbNode, (node as ts.PropertySignature).type) || + visitNode(cbNode, (node as ts.PropertySignature).initializer); + case ts.SyntaxKind.PropertyAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.PropertyAssignment).name) || + visitNode(cbNode, (node as ts.PropertyAssignment).questionToken) || + visitNode(cbNode, (node as ts.PropertyAssignment).initializer); + case ts.SyntaxKind.VariableDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.VariableDeclaration).name) || + visitNode(cbNode, (node as ts.VariableDeclaration).exclamationToken) || + visitNode(cbNode, (node as ts.VariableDeclaration).type) || + visitNode(cbNode, (node as ts.VariableDeclaration).initializer); + case ts.SyntaxKind.BindingElement: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.BindingElement).dotDotDotToken) || + visitNode(cbNode, (node as ts.BindingElement).propertyName) || + visitNode(cbNode, (node as ts.BindingElement).name) || + visitNode(cbNode, (node as ts.BindingElement).initializer); + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.IndexSignature: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNodes(cbNode, cbNodes, (node as ts.SignatureDeclaration).typeParameters) || + visitNodes(cbNode, cbNodes, (node as ts.SignatureDeclaration).parameters) || + visitNode(cbNode, (node as ts.SignatureDeclaration).type); + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ArrowFunction: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).asteriskToken) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).name) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).questionToken) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).exclamationToken) || + visitNodes(cbNode, cbNodes, (node as ts.FunctionLikeDeclaration).typeParameters) || + visitNodes(cbNode, cbNodes, (node as ts.FunctionLikeDeclaration).parameters) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).type) || + visitNode(cbNode, (node as ts.ArrowFunction).equalsGreaterThanToken) || + visitNode(cbNode, (node as ts.FunctionLikeDeclaration).body); + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ClassStaticBlockDeclaration).body); + case ts.SyntaxKind.TypeReference: + return visitNode(cbNode, (node as ts.TypeReferenceNode).typeName) || + visitNodes(cbNode, cbNodes, (node as ts.TypeReferenceNode).typeArguments); + case ts.SyntaxKind.TypePredicate: + return visitNode(cbNode, (node as ts.TypePredicateNode).assertsModifier) || + visitNode(cbNode, (node as ts.TypePredicateNode).parameterName) || + visitNode(cbNode, (node as ts.TypePredicateNode).type); + case ts.SyntaxKind.TypeQuery: + return visitNode(cbNode, (node as ts.TypeQueryNode).exprName) || + visitNodes(cbNode, cbNodes, (node as ts.TypeQueryNode).typeArguments); + case ts.SyntaxKind.TypeLiteral: + return visitNodes(cbNode, cbNodes, (node as ts.TypeLiteralNode).members); + case ts.SyntaxKind.ArrayType: + return visitNode(cbNode, (node as ts.ArrayTypeNode).elementType); + case ts.SyntaxKind.TupleType: + return visitNodes(cbNode, cbNodes, (node as ts.TupleTypeNode).elements); + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + return visitNodes(cbNode, cbNodes, (node as ts.UnionOrIntersectionTypeNode).types); + case ts.SyntaxKind.ConditionalType: + return visitNode(cbNode, (node as ts.ConditionalTypeNode).checkType) || + visitNode(cbNode, (node as ts.ConditionalTypeNode).extendsType) || + visitNode(cbNode, (node as ts.ConditionalTypeNode).trueType) || + visitNode(cbNode, (node as ts.ConditionalTypeNode).falseType); + case ts.SyntaxKind.InferType: + return visitNode(cbNode, (node as ts.InferTypeNode).typeParameter); + case ts.SyntaxKind.ImportType: + return visitNode(cbNode, (node as ts.ImportTypeNode).argument) || + visitNode(cbNode, (node as ts.ImportTypeNode).assertions) || + visitNode(cbNode, (node as ts.ImportTypeNode).qualifier) || + visitNodes(cbNode, cbNodes, (node as ts.ImportTypeNode).typeArguments); + case ts.SyntaxKind.ImportTypeAssertionContainer: + return visitNode(cbNode, (node as ts.ImportTypeAssertionContainer).assertClause); + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.TypeOperator: + return visitNode(cbNode, (node as ts.ParenthesizedTypeNode | ts.TypeOperatorNode).type); + case ts.SyntaxKind.IndexedAccessType: + return visitNode(cbNode, (node as ts.IndexedAccessTypeNode).objectType) || + visitNode(cbNode, (node as ts.IndexedAccessTypeNode).indexType); + case ts.SyntaxKind.MappedType: + return visitNode(cbNode, (node as ts.MappedTypeNode).readonlyToken) || + visitNode(cbNode, (node as ts.MappedTypeNode).typeParameter) || + visitNode(cbNode, (node as ts.MappedTypeNode).nameType) || + visitNode(cbNode, (node as ts.MappedTypeNode).questionToken) || + visitNode(cbNode, (node as ts.MappedTypeNode).type) || + visitNodes(cbNode, cbNodes, (node as ts.MappedTypeNode).members); + case ts.SyntaxKind.LiteralType: + return visitNode(cbNode, (node as ts.LiteralTypeNode).literal); + case ts.SyntaxKind.NamedTupleMember: + return visitNode(cbNode, (node as ts.NamedTupleMember).dotDotDotToken) || + visitNode(cbNode, (node as ts.NamedTupleMember).name) || + visitNode(cbNode, (node as ts.NamedTupleMember).questionToken) || + visitNode(cbNode, (node as ts.NamedTupleMember).type); + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + return visitNodes(cbNode, cbNodes, (node as ts.BindingPattern).elements); + case ts.SyntaxKind.ArrayLiteralExpression: + return visitNodes(cbNode, cbNodes, (node as ts.ArrayLiteralExpression).elements); + case ts.SyntaxKind.ObjectLiteralExpression: + return visitNodes(cbNode, cbNodes, (node as ts.ObjectLiteralExpression).properties); + case ts.SyntaxKind.PropertyAccessExpression: + return visitNode(cbNode, (node as ts.PropertyAccessExpression).expression) || + visitNode(cbNode, (node as ts.PropertyAccessExpression).questionDotToken) || + visitNode(cbNode, (node as ts.PropertyAccessExpression).name); + case ts.SyntaxKind.ElementAccessExpression: + return visitNode(cbNode, (node as ts.ElementAccessExpression).expression) || + visitNode(cbNode, (node as ts.ElementAccessExpression).questionDotToken) || + visitNode(cbNode, (node as ts.ElementAccessExpression).argumentExpression); + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + return visitNode(cbNode, (node as ts.CallExpression).expression) || + visitNode(cbNode, (node as ts.CallExpression).questionDotToken) || + visitNodes(cbNode, cbNodes, (node as ts.CallExpression).typeArguments) || + visitNodes(cbNode, cbNodes, (node as ts.CallExpression).arguments); + case ts.SyntaxKind.TaggedTemplateExpression: + return visitNode(cbNode, (node as ts.TaggedTemplateExpression).tag) || + visitNode(cbNode, (node as ts.TaggedTemplateExpression).questionDotToken) || + visitNodes(cbNode, cbNodes, (node as ts.TaggedTemplateExpression).typeArguments) || + visitNode(cbNode, (node as ts.TaggedTemplateExpression).template); + case ts.SyntaxKind.TypeAssertionExpression: + return visitNode(cbNode, (node as ts.TypeAssertion).type) || + visitNode(cbNode, (node as ts.TypeAssertion).expression); + case ts.SyntaxKind.ParenthesizedExpression: + return visitNode(cbNode, (node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.DeleteExpression: + return visitNode(cbNode, (node as ts.DeleteExpression).expression); + case ts.SyntaxKind.TypeOfExpression: + return visitNode(cbNode, (node as ts.TypeOfExpression).expression); + case ts.SyntaxKind.VoidExpression: + return visitNode(cbNode, (node as ts.VoidExpression).expression); + case ts.SyntaxKind.PrefixUnaryExpression: + return visitNode(cbNode, (node as ts.PrefixUnaryExpression).operand); + case ts.SyntaxKind.YieldExpression: + return visitNode(cbNode, (node as ts.YieldExpression).asteriskToken) || + visitNode(cbNode, (node as ts.YieldExpression).expression); + case ts.SyntaxKind.AwaitExpression: + return visitNode(cbNode, (node as ts.AwaitExpression).expression); + case ts.SyntaxKind.PostfixUnaryExpression: + return visitNode(cbNode, (node as ts.PostfixUnaryExpression).operand); + case ts.SyntaxKind.BinaryExpression: + return visitNode(cbNode, (node as ts.BinaryExpression).left) || + visitNode(cbNode, (node as ts.BinaryExpression).operatorToken) || + visitNode(cbNode, (node as ts.BinaryExpression).right); + case ts.SyntaxKind.AsExpression: + return visitNode(cbNode, (node as ts.AsExpression).expression) || + visitNode(cbNode, (node as ts.AsExpression).type); + case ts.SyntaxKind.NonNullExpression: + return visitNode(cbNode, (node as ts.NonNullExpression).expression); + case ts.SyntaxKind.MetaProperty: + return visitNode(cbNode, (node as ts.MetaProperty).name); + case ts.SyntaxKind.ConditionalExpression: + return visitNode(cbNode, (node as ts.ConditionalExpression).condition) || + visitNode(cbNode, (node as ts.ConditionalExpression).questionToken) || + visitNode(cbNode, (node as ts.ConditionalExpression).whenTrue) || + visitNode(cbNode, (node as ts.ConditionalExpression).colonToken) || + visitNode(cbNode, (node as ts.ConditionalExpression).whenFalse); + case ts.SyntaxKind.SpreadElement: + return visitNode(cbNode, (node as ts.SpreadElement).expression); + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + return visitNodes(cbNode, cbNodes, (node as ts.Block).statements); + case ts.SyntaxKind.SourceFile: + return visitNodes(cbNode, cbNodes, (node as ts.SourceFile).statements) || + visitNode(cbNode, (node as ts.SourceFile).endOfFileToken); + case ts.SyntaxKind.VariableStatement: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.VariableStatement).declarationList); + case ts.SyntaxKind.VariableDeclarationList: + return visitNodes(cbNode, cbNodes, (node as ts.VariableDeclarationList).declarations); + case ts.SyntaxKind.ExpressionStatement: + return visitNode(cbNode, (node as ts.ExpressionStatement).expression); + case ts.SyntaxKind.IfStatement: + return visitNode(cbNode, (node as ts.IfStatement).expression) || + visitNode(cbNode, (node as ts.IfStatement).thenStatement) || + visitNode(cbNode, (node as ts.IfStatement).elseStatement); + case ts.SyntaxKind.DoStatement: + return visitNode(cbNode, (node as ts.DoStatement).statement) || + visitNode(cbNode, (node as ts.DoStatement).expression); + case ts.SyntaxKind.WhileStatement: + return visitNode(cbNode, (node as ts.WhileStatement).expression) || + visitNode(cbNode, (node as ts.WhileStatement).statement); + case ts.SyntaxKind.ForStatement: + return visitNode(cbNode, (node as ts.ForStatement).initializer) || + visitNode(cbNode, (node as ts.ForStatement).condition) || + visitNode(cbNode, (node as ts.ForStatement).incrementor) || + visitNode(cbNode, (node as ts.ForStatement).statement); + case ts.SyntaxKind.ForInStatement: + return visitNode(cbNode, (node as ts.ForInStatement).initializer) || + visitNode(cbNode, (node as ts.ForInStatement).expression) || + visitNode(cbNode, (node as ts.ForInStatement).statement); + case ts.SyntaxKind.ForOfStatement: + return visitNode(cbNode, (node as ts.ForOfStatement).awaitModifier) || + visitNode(cbNode, (node as ts.ForOfStatement).initializer) || + visitNode(cbNode, (node as ts.ForOfStatement).expression) || + visitNode(cbNode, (node as ts.ForOfStatement).statement); + case ts.SyntaxKind.ContinueStatement: + case ts.SyntaxKind.BreakStatement: + return visitNode(cbNode, (node as ts.BreakOrContinueStatement).label); + case ts.SyntaxKind.ReturnStatement: + return visitNode(cbNode, (node as ts.ReturnStatement).expression); + case ts.SyntaxKind.WithStatement: + return visitNode(cbNode, (node as ts.WithStatement).expression) || + visitNode(cbNode, (node as ts.WithStatement).statement); + case ts.SyntaxKind.SwitchStatement: + return visitNode(cbNode, (node as ts.SwitchStatement).expression) || + visitNode(cbNode, (node as ts.SwitchStatement).caseBlock); + case ts.SyntaxKind.CaseBlock: + return visitNodes(cbNode, cbNodes, (node as ts.CaseBlock).clauses); + case ts.SyntaxKind.CaseClause: + return visitNode(cbNode, (node as ts.CaseClause).expression) || + visitNodes(cbNode, cbNodes, (node as ts.CaseClause).statements); + case ts.SyntaxKind.DefaultClause: + return visitNodes(cbNode, cbNodes, (node as ts.DefaultClause).statements); + case ts.SyntaxKind.LabeledStatement: + return visitNode(cbNode, (node as ts.LabeledStatement).label) || + visitNode(cbNode, (node as ts.LabeledStatement).statement); + case ts.SyntaxKind.ThrowStatement: + return visitNode(cbNode, (node as ts.ThrowStatement).expression); + case ts.SyntaxKind.TryStatement: + return visitNode(cbNode, (node as ts.TryStatement).tryBlock) || + visitNode(cbNode, (node as ts.TryStatement).catchClause) || + visitNode(cbNode, (node as ts.TryStatement).finallyBlock); + case ts.SyntaxKind.CatchClause: + return visitNode(cbNode, (node as ts.CatchClause).variableDeclaration) || + visitNode(cbNode, (node as ts.CatchClause).block); + case ts.SyntaxKind.Decorator: + return visitNode(cbNode, (node as ts.Decorator).expression); + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ClassLikeDeclaration).name) || + visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).typeParameters) || + visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).heritageClauses) || + visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).members); + case ts.SyntaxKind.InterfaceDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.InterfaceDeclaration).name) || + visitNodes(cbNode, cbNodes, (node as ts.InterfaceDeclaration).typeParameters) || + visitNodes(cbNode, cbNodes, (node as ts.ClassDeclaration).heritageClauses) || + visitNodes(cbNode, cbNodes, (node as ts.InterfaceDeclaration).members); + case ts.SyntaxKind.TypeAliasDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.TypeAliasDeclaration).name) || + visitNodes(cbNode, cbNodes, (node as ts.TypeAliasDeclaration).typeParameters) || + visitNode(cbNode, (node as ts.TypeAliasDeclaration).type); + case ts.SyntaxKind.EnumDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.EnumDeclaration).name) || + visitNodes(cbNode, cbNodes, (node as ts.EnumDeclaration).members); + case ts.SyntaxKind.EnumMember: + return visitNode(cbNode, (node as ts.EnumMember).name) || + visitNode(cbNode, (node as ts.EnumMember).initializer); + case ts.SyntaxKind.ModuleDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ModuleDeclaration).name) || + visitNode(cbNode, (node as ts.ModuleDeclaration).body); + case ts.SyntaxKind.ImportEqualsDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ImportEqualsDeclaration).name) || + visitNode(cbNode, (node as ts.ImportEqualsDeclaration).moduleReference); + case ts.SyntaxKind.ImportDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ImportDeclaration).importClause) || + visitNode(cbNode, (node as ts.ImportDeclaration).moduleSpecifier) || + visitNode(cbNode, (node as ts.ImportDeclaration).assertClause); + case ts.SyntaxKind.ImportClause: + return visitNode(cbNode, (node as ts.ImportClause).name) || + visitNode(cbNode, (node as ts.ImportClause).namedBindings); + case ts.SyntaxKind.AssertClause: + return visitNodes(cbNode, cbNodes, (node as ts.AssertClause).elements); + case ts.SyntaxKind.AssertEntry: + return visitNode(cbNode, (node as ts.AssertEntry).name) || + visitNode(cbNode, (node as ts.AssertEntry).value); + case ts.SyntaxKind.NamespaceExportDeclaration: + return visitNode(cbNode, (node as ts.NamespaceExportDeclaration).name); + case ts.SyntaxKind.NamespaceImport: + return visitNode(cbNode, (node as ts.NamespaceImport).name); + case ts.SyntaxKind.NamespaceExport: + return visitNode(cbNode, (node as ts.NamespaceExport).name); + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.NamedExports: + return visitNodes(cbNode, cbNodes, (node as ts.NamedImportsOrExports).elements); + case ts.SyntaxKind.ExportDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ExportDeclaration).exportClause) || + visitNode(cbNode, (node as ts.ExportDeclaration).moduleSpecifier) || + visitNode(cbNode, (node as ts.ExportDeclaration).assertClause); + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + return visitNode(cbNode, (node as ts.ImportOrExportSpecifier).propertyName) || + visitNode(cbNode, (node as ts.ImportOrExportSpecifier).name); + case ts.SyntaxKind.ExportAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node as ts.ExportAssignment).expression); + case ts.SyntaxKind.TemplateExpression: + return visitNode(cbNode, (node as ts.TemplateExpression).head) || visitNodes(cbNode, cbNodes, (node as ts.TemplateExpression).templateSpans); + case ts.SyntaxKind.TemplateSpan: + return visitNode(cbNode, (node as ts.TemplateSpan).expression) || visitNode(cbNode, (node as ts.TemplateSpan).literal); + case ts.SyntaxKind.TemplateLiteralType: + return visitNode(cbNode, (node as ts.TemplateLiteralTypeNode).head) || visitNodes(cbNode, cbNodes, (node as ts.TemplateLiteralTypeNode).templateSpans); + case ts.SyntaxKind.TemplateLiteralTypeSpan: + return visitNode(cbNode, (node as ts.TemplateLiteralTypeSpan).type) || visitNode(cbNode, (node as ts.TemplateLiteralTypeSpan).literal); + case ts.SyntaxKind.ComputedPropertyName: + return visitNode(cbNode, (node as ts.ComputedPropertyName).expression); + case ts.SyntaxKind.HeritageClause: + return visitNodes(cbNode, cbNodes, (node as ts.HeritageClause).types); + case ts.SyntaxKind.ExpressionWithTypeArguments: + return visitNode(cbNode, (node as ts.ExpressionWithTypeArguments).expression) || + visitNodes(cbNode, cbNodes, (node as ts.ExpressionWithTypeArguments).typeArguments); + case ts.SyntaxKind.ExternalModuleReference: + return visitNode(cbNode, (node as ts.ExternalModuleReference).expression); + case ts.SyntaxKind.MissingDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators); + case ts.SyntaxKind.CommaListExpression: + return visitNodes(cbNode, cbNodes, (node as ts.CommaListExpression).elements); + case ts.SyntaxKind.JsxElement: + return visitNode(cbNode, (node as ts.JsxElement).openingElement) || + visitNodes(cbNode, cbNodes, (node as ts.JsxElement).children) || + visitNode(cbNode, (node as ts.JsxElement).closingElement); + case ts.SyntaxKind.JsxFragment: + return visitNode(cbNode, (node as ts.JsxFragment).openingFragment) || + visitNodes(cbNode, cbNodes, (node as ts.JsxFragment).children) || + visitNode(cbNode, (node as ts.JsxFragment).closingFragment); + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxOpeningElement: + return visitNode(cbNode, (node as ts.JsxOpeningLikeElement).tagName) || + visitNodes(cbNode, cbNodes, (node as ts.JsxOpeningLikeElement).typeArguments) || + visitNode(cbNode, (node as ts.JsxOpeningLikeElement).attributes); + case ts.SyntaxKind.JsxAttributes: + return visitNodes(cbNode, cbNodes, (node as ts.JsxAttributes).properties); + case ts.SyntaxKind.JsxAttribute: + return visitNode(cbNode, (node as ts.JsxAttribute).name) || + visitNode(cbNode, (node as ts.JsxAttribute).initializer); + case ts.SyntaxKind.JsxSpreadAttribute: + return visitNode(cbNode, (node as ts.JsxSpreadAttribute).expression); + case ts.SyntaxKind.JsxExpression: + return visitNode(cbNode, (node as ts.JsxExpression).dotDotDotToken) || + visitNode(cbNode, (node as ts.JsxExpression).expression); + case ts.SyntaxKind.JsxClosingElement: + return visitNode(cbNode, (node as ts.JsxClosingElement).tagName); + case ts.SyntaxKind.OptionalType: + case ts.SyntaxKind.RestType: + case ts.SyntaxKind.JSDocTypeExpression: + case ts.SyntaxKind.JSDocNonNullableType: + case ts.SyntaxKind.JSDocNullableType: + case ts.SyntaxKind.JSDocOptionalType: + case ts.SyntaxKind.JSDocVariadicType: + return visitNode(cbNode, (node as ts.OptionalTypeNode | ts.RestTypeNode | ts.JSDocTypeExpression | ts.JSDocTypeReferencingNode).type); + case ts.SyntaxKind.JSDocFunctionType: + return visitNodes(cbNode, cbNodes, (node as ts.JSDocFunctionType).parameters) || + visitNode(cbNode, (node as ts.JSDocFunctionType).type); + case ts.SyntaxKind.JSDoc: + return (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) + || visitNodes(cbNode, cbNodes, (node as ts.JSDoc).tags); + case ts.SyntaxKind.JSDocSeeTag: + return visitNode(cbNode, (node as ts.JSDocSeeTag).tagName) || + visitNode(cbNode, (node as ts.JSDocSeeTag).name) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocNameReference: + return visitNode(cbNode, (node as ts.JSDocNameReference).name); + case ts.SyntaxKind.JSDocMemberName: + return visitNode(cbNode, (node as ts.JSDocMemberName).left) || + visitNode(cbNode, (node as ts.JSDocMemberName).right); + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocPropertyTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + ((node as ts.JSDocPropertyLikeTag).isNameFirst + ? visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).name) || + visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).typeExpression) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) + : visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).typeExpression) || + visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).name) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined))); + case ts.SyntaxKind.JSDocAuthorTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocImplementsTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + visitNode(cbNode, (node as ts.JSDocImplementsTag).class) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocAugmentsTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + visitNode(cbNode, (node as ts.JSDocAugmentsTag).class) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocTemplateTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + visitNode(cbNode, (node as ts.JSDocTemplateTag).constraint) || + visitNodes(cbNode, cbNodes, (node as ts.JSDocTemplateTag).typeParameters) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocTypedefTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + ((node as ts.JSDocTypedefTag).typeExpression && + (node as ts.JSDocTypedefTag).typeExpression!.kind === ts.SyntaxKind.JSDocTypeExpression + ? visitNode(cbNode, (node as ts.JSDocTypedefTag).typeExpression) || + visitNode(cbNode, (node as ts.JSDocTypedefTag).fullName) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) + : visitNode(cbNode, (node as ts.JSDocTypedefTag).fullName) || + visitNode(cbNode, (node as ts.JSDocTypedefTag).typeExpression) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined))); + case ts.SyntaxKind.JSDocCallbackTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + visitNode(cbNode, (node as ts.JSDocCallbackTag).fullName) || + visitNode(cbNode, (node as ts.JSDocCallbackTag).typeExpression) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocReturnTag: + case ts.SyntaxKind.JSDocTypeTag: + case ts.SyntaxKind.JSDocThisTag: + case ts.SyntaxKind.JSDocEnumTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) || + visitNode(cbNode, (node as ts.JSDocReturnTag | ts.JSDocTypeTag | ts.JSDocThisTag | ts.JSDocEnumTag).typeExpression) || + (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.JSDocSignature: + return ts.forEach((node as ts.JSDocSignature).typeParameters, cbNode) || + ts.forEach((node as ts.JSDocSignature).parameters, cbNode) || + visitNode(cbNode, (node as ts.JSDocSignature).type); + case ts.SyntaxKind.JSDocLink: + case ts.SyntaxKind.JSDocLinkCode: + case ts.SyntaxKind.JSDocLinkPlain: + return visitNode(cbNode, (node as ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain).name); + case ts.SyntaxKind.JSDocTypeLiteral: + return ts.forEach((node as ts.JSDocTypeLiteral).jsDocPropertyTags, cbNode); + case ts.SyntaxKind.JSDocTag: + case ts.SyntaxKind.JSDocClassTag: + case ts.SyntaxKind.JSDocPublicTag: + case ts.SyntaxKind.JSDocPrivateTag: + case ts.SyntaxKind.JSDocProtectedTag: + case ts.SyntaxKind.JSDocReadonlyTag: + case ts.SyntaxKind.JSDocDeprecatedTag: + return visitNode(cbNode, (node as ts.JSDocTag).tagName) + || (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); + case ts.SyntaxKind.PartiallyEmittedExpression: + return visitNode(cbNode, (node as ts.PartiallyEmittedExpression).expression); + } +} - /** - * 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: ts.Node, cbNode: (node: ts.Node) => T | undefined, cbNodes?: (nodes: ts.NodeArray) => T | undefined): T | undefined { - if (!node || node.kind <= ts.SyntaxKind.LastToken) { - return; - } - switch (node.kind) { - case ts.SyntaxKind.QualifiedName: - return visitNode(cbNode, (node as ts.QualifiedName).left) || - visitNode(cbNode, (node as ts.QualifiedName).right); - case ts.SyntaxKind.TypeParameter: - return visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.TypeParameterDeclaration).name) || - visitNode(cbNode, (node as ts.TypeParameterDeclaration).constraint) || - visitNode(cbNode, (node as ts.TypeParameterDeclaration).default) || - visitNode(cbNode, (node as ts.TypeParameterDeclaration).expression); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).name) || - visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).questionToken) || - visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).exclamationToken) || - visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).equalsToken) || - visitNode(cbNode, (node as ts.ShorthandPropertyAssignment).objectAssignmentInitializer); - case ts.SyntaxKind.SpreadAssignment: - return visitNode(cbNode, (node as ts.SpreadAssignment).expression); - case ts.SyntaxKind.Parameter: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ParameterDeclaration).dotDotDotToken) || - visitNode(cbNode, (node as ts.ParameterDeclaration).name) || - visitNode(cbNode, (node as ts.ParameterDeclaration).questionToken) || - visitNode(cbNode, (node as ts.ParameterDeclaration).type) || - visitNode(cbNode, (node as ts.ParameterDeclaration).initializer); - case ts.SyntaxKind.PropertyDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.PropertyDeclaration).name) || - visitNode(cbNode, (node as ts.PropertyDeclaration).questionToken) || - visitNode(cbNode, (node as ts.PropertyDeclaration).exclamationToken) || - visitNode(cbNode, (node as ts.PropertyDeclaration).type) || - visitNode(cbNode, (node as ts.PropertyDeclaration).initializer); - case ts.SyntaxKind.PropertySignature: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.PropertySignature).name) || - visitNode(cbNode, (node as ts.PropertySignature).questionToken) || - visitNode(cbNode, (node as ts.PropertySignature).type) || - visitNode(cbNode, (node as ts.PropertySignature).initializer); - case ts.SyntaxKind.PropertyAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.PropertyAssignment).name) || - visitNode(cbNode, (node as ts.PropertyAssignment).questionToken) || - visitNode(cbNode, (node as ts.PropertyAssignment).initializer); - case ts.SyntaxKind.VariableDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.VariableDeclaration).name) || - visitNode(cbNode, (node as ts.VariableDeclaration).exclamationToken) || - visitNode(cbNode, (node as ts.VariableDeclaration).type) || - visitNode(cbNode, (node as ts.VariableDeclaration).initializer); - case ts.SyntaxKind.BindingElement: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.BindingElement).dotDotDotToken) || - visitNode(cbNode, (node as ts.BindingElement).propertyName) || - visitNode(cbNode, (node as ts.BindingElement).name) || - visitNode(cbNode, (node as ts.BindingElement).initializer); - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.IndexSignature: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNodes(cbNode, cbNodes, (node as ts.SignatureDeclaration).typeParameters) || - visitNodes(cbNode, cbNodes, (node as ts.SignatureDeclaration).parameters) || - visitNode(cbNode, (node as ts.SignatureDeclaration).type); - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ArrowFunction: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).asteriskToken) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).name) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).questionToken) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).exclamationToken) || - visitNodes(cbNode, cbNodes, (node as ts.FunctionLikeDeclaration).typeParameters) || - visitNodes(cbNode, cbNodes, (node as ts.FunctionLikeDeclaration).parameters) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).type) || - visitNode(cbNode, (node as ts.ArrowFunction).equalsGreaterThanToken) || - visitNode(cbNode, (node as ts.FunctionLikeDeclaration).body); - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ClassStaticBlockDeclaration).body); - case ts.SyntaxKind.TypeReference: - return visitNode(cbNode, (node as ts.TypeReferenceNode).typeName) || - visitNodes(cbNode, cbNodes, (node as ts.TypeReferenceNode).typeArguments); - case ts.SyntaxKind.TypePredicate: - return visitNode(cbNode, (node as ts.TypePredicateNode).assertsModifier) || - visitNode(cbNode, (node as ts.TypePredicateNode).parameterName) || - visitNode(cbNode, (node as ts.TypePredicateNode).type); - case ts.SyntaxKind.TypeQuery: - return visitNode(cbNode, (node as ts.TypeQueryNode).exprName) || - visitNodes(cbNode, cbNodes, (node as ts.TypeQueryNode).typeArguments); - case ts.SyntaxKind.TypeLiteral: - return visitNodes(cbNode, cbNodes, (node as ts.TypeLiteralNode).members); - case ts.SyntaxKind.ArrayType: - return visitNode(cbNode, (node as ts.ArrayTypeNode).elementType); - case ts.SyntaxKind.TupleType: - return visitNodes(cbNode, cbNodes, (node as ts.TupleTypeNode).elements); - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - return visitNodes(cbNode, cbNodes, (node as ts.UnionOrIntersectionTypeNode).types); - case ts.SyntaxKind.ConditionalType: - return visitNode(cbNode, (node as ts.ConditionalTypeNode).checkType) || - visitNode(cbNode, (node as ts.ConditionalTypeNode).extendsType) || - visitNode(cbNode, (node as ts.ConditionalTypeNode).trueType) || - visitNode(cbNode, (node as ts.ConditionalTypeNode).falseType); - case ts.SyntaxKind.InferType: - return visitNode(cbNode, (node as ts.InferTypeNode).typeParameter); - case ts.SyntaxKind.ImportType: - return visitNode(cbNode, (node as ts.ImportTypeNode).argument) || - visitNode(cbNode, (node as ts.ImportTypeNode).assertions) || - visitNode(cbNode, (node as ts.ImportTypeNode).qualifier) || - visitNodes(cbNode, cbNodes, (node as ts.ImportTypeNode).typeArguments); - case ts.SyntaxKind.ImportTypeAssertionContainer: - return visitNode(cbNode, (node as ts.ImportTypeAssertionContainer).assertClause); - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.TypeOperator: - return visitNode(cbNode, (node as ts.ParenthesizedTypeNode | ts.TypeOperatorNode).type); - case ts.SyntaxKind.IndexedAccessType: - return visitNode(cbNode, (node as ts.IndexedAccessTypeNode).objectType) || - visitNode(cbNode, (node as ts.IndexedAccessTypeNode).indexType); - case ts.SyntaxKind.MappedType: - return visitNode(cbNode, (node as ts.MappedTypeNode).readonlyToken) || - visitNode(cbNode, (node as ts.MappedTypeNode).typeParameter) || - visitNode(cbNode, (node as ts.MappedTypeNode).nameType) || - visitNode(cbNode, (node as ts.MappedTypeNode).questionToken) || - visitNode(cbNode, (node as ts.MappedTypeNode).type) || - visitNodes(cbNode, cbNodes, (node as ts.MappedTypeNode).members); - case ts.SyntaxKind.LiteralType: - return visitNode(cbNode, (node as ts.LiteralTypeNode).literal); - case ts.SyntaxKind.NamedTupleMember: - return visitNode(cbNode, (node as ts.NamedTupleMember).dotDotDotToken) || - visitNode(cbNode, (node as ts.NamedTupleMember).name) || - visitNode(cbNode, (node as ts.NamedTupleMember).questionToken) || - visitNode(cbNode, (node as ts.NamedTupleMember).type); - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ArrayBindingPattern: - return visitNodes(cbNode, cbNodes, (node as ts.BindingPattern).elements); - case ts.SyntaxKind.ArrayLiteralExpression: - return visitNodes(cbNode, cbNodes, (node as ts.ArrayLiteralExpression).elements); - case ts.SyntaxKind.ObjectLiteralExpression: - return visitNodes(cbNode, cbNodes, (node as ts.ObjectLiteralExpression).properties); - case ts.SyntaxKind.PropertyAccessExpression: - return visitNode(cbNode, (node as ts.PropertyAccessExpression).expression) || - visitNode(cbNode, (node as ts.PropertyAccessExpression).questionDotToken) || - visitNode(cbNode, (node as ts.PropertyAccessExpression).name); - case ts.SyntaxKind.ElementAccessExpression: - return visitNode(cbNode, (node as ts.ElementAccessExpression).expression) || - visitNode(cbNode, (node as ts.ElementAccessExpression).questionDotToken) || - visitNode(cbNode, (node as ts.ElementAccessExpression).argumentExpression); - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - return visitNode(cbNode, (node as ts.CallExpression).expression) || - visitNode(cbNode, (node as ts.CallExpression).questionDotToken) || - visitNodes(cbNode, cbNodes, (node as ts.CallExpression).typeArguments) || - visitNodes(cbNode, cbNodes, (node as ts.CallExpression).arguments); - case ts.SyntaxKind.TaggedTemplateExpression: - return visitNode(cbNode, (node as ts.TaggedTemplateExpression).tag) || - visitNode(cbNode, (node as ts.TaggedTemplateExpression).questionDotToken) || - visitNodes(cbNode, cbNodes, (node as ts.TaggedTemplateExpression).typeArguments) || - visitNode(cbNode, (node as ts.TaggedTemplateExpression).template); - case ts.SyntaxKind.TypeAssertionExpression: - return visitNode(cbNode, (node as ts.TypeAssertion).type) || - visitNode(cbNode, (node as ts.TypeAssertion).expression); - case ts.SyntaxKind.ParenthesizedExpression: - return visitNode(cbNode, (node as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.DeleteExpression: - return visitNode(cbNode, (node as ts.DeleteExpression).expression); - case ts.SyntaxKind.TypeOfExpression: - return visitNode(cbNode, (node as ts.TypeOfExpression).expression); - case ts.SyntaxKind.VoidExpression: - return visitNode(cbNode, (node as ts.VoidExpression).expression); - case ts.SyntaxKind.PrefixUnaryExpression: - return visitNode(cbNode, (node as ts.PrefixUnaryExpression).operand); - case ts.SyntaxKind.YieldExpression: - return visitNode(cbNode, (node as ts.YieldExpression).asteriskToken) || - visitNode(cbNode, (node as ts.YieldExpression).expression); - case ts.SyntaxKind.AwaitExpression: - return visitNode(cbNode, (node as ts.AwaitExpression).expression); - case ts.SyntaxKind.PostfixUnaryExpression: - return visitNode(cbNode, (node as ts.PostfixUnaryExpression).operand); - case ts.SyntaxKind.BinaryExpression: - return visitNode(cbNode, (node as ts.BinaryExpression).left) || - visitNode(cbNode, (node as ts.BinaryExpression).operatorToken) || - visitNode(cbNode, (node as ts.BinaryExpression).right); - case ts.SyntaxKind.AsExpression: - return visitNode(cbNode, (node as ts.AsExpression).expression) || - visitNode(cbNode, (node as ts.AsExpression).type); - case ts.SyntaxKind.NonNullExpression: - return visitNode(cbNode, (node as ts.NonNullExpression).expression); - case ts.SyntaxKind.MetaProperty: - return visitNode(cbNode, (node as ts.MetaProperty).name); - case ts.SyntaxKind.ConditionalExpression: - return visitNode(cbNode, (node as ts.ConditionalExpression).condition) || - visitNode(cbNode, (node as ts.ConditionalExpression).questionToken) || - visitNode(cbNode, (node as ts.ConditionalExpression).whenTrue) || - visitNode(cbNode, (node as ts.ConditionalExpression).colonToken) || - visitNode(cbNode, (node as ts.ConditionalExpression).whenFalse); - case ts.SyntaxKind.SpreadElement: - return visitNode(cbNode, (node as ts.SpreadElement).expression); - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - return visitNodes(cbNode, cbNodes, (node as ts.Block).statements); - case ts.SyntaxKind.SourceFile: - return visitNodes(cbNode, cbNodes, (node as ts.SourceFile).statements) || - visitNode(cbNode, (node as ts.SourceFile).endOfFileToken); - case ts.SyntaxKind.VariableStatement: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.VariableStatement).declarationList); - case ts.SyntaxKind.VariableDeclarationList: - return visitNodes(cbNode, cbNodes, (node as ts.VariableDeclarationList).declarations); - case ts.SyntaxKind.ExpressionStatement: - return visitNode(cbNode, (node as ts.ExpressionStatement).expression); - case ts.SyntaxKind.IfStatement: - return visitNode(cbNode, (node as ts.IfStatement).expression) || - visitNode(cbNode, (node as ts.IfStatement).thenStatement) || - visitNode(cbNode, (node as ts.IfStatement).elseStatement); - case ts.SyntaxKind.DoStatement: - return visitNode(cbNode, (node as ts.DoStatement).statement) || - visitNode(cbNode, (node as ts.DoStatement).expression); - case ts.SyntaxKind.WhileStatement: - return visitNode(cbNode, (node as ts.WhileStatement).expression) || - visitNode(cbNode, (node as ts.WhileStatement).statement); - case ts.SyntaxKind.ForStatement: - return visitNode(cbNode, (node as ts.ForStatement).initializer) || - visitNode(cbNode, (node as ts.ForStatement).condition) || - visitNode(cbNode, (node as ts.ForStatement).incrementor) || - visitNode(cbNode, (node as ts.ForStatement).statement); - case ts.SyntaxKind.ForInStatement: - return visitNode(cbNode, (node as ts.ForInStatement).initializer) || - visitNode(cbNode, (node as ts.ForInStatement).expression) || - visitNode(cbNode, (node as ts.ForInStatement).statement); - case ts.SyntaxKind.ForOfStatement: - return visitNode(cbNode, (node as ts.ForOfStatement).awaitModifier) || - visitNode(cbNode, (node as ts.ForOfStatement).initializer) || - visitNode(cbNode, (node as ts.ForOfStatement).expression) || - visitNode(cbNode, (node as ts.ForOfStatement).statement); - case ts.SyntaxKind.ContinueStatement: - case ts.SyntaxKind.BreakStatement: - return visitNode(cbNode, (node as ts.BreakOrContinueStatement).label); - case ts.SyntaxKind.ReturnStatement: - return visitNode(cbNode, (node as ts.ReturnStatement).expression); - case ts.SyntaxKind.WithStatement: - return visitNode(cbNode, (node as ts.WithStatement).expression) || - visitNode(cbNode, (node as ts.WithStatement).statement); - case ts.SyntaxKind.SwitchStatement: - return visitNode(cbNode, (node as ts.SwitchStatement).expression) || - visitNode(cbNode, (node as ts.SwitchStatement).caseBlock); - case ts.SyntaxKind.CaseBlock: - return visitNodes(cbNode, cbNodes, (node as ts.CaseBlock).clauses); - case ts.SyntaxKind.CaseClause: - return visitNode(cbNode, (node as ts.CaseClause).expression) || - visitNodes(cbNode, cbNodes, (node as ts.CaseClause).statements); - case ts.SyntaxKind.DefaultClause: - return visitNodes(cbNode, cbNodes, (node as ts.DefaultClause).statements); - case ts.SyntaxKind.LabeledStatement: - return visitNode(cbNode, (node as ts.LabeledStatement).label) || - visitNode(cbNode, (node as ts.LabeledStatement).statement); - case ts.SyntaxKind.ThrowStatement: - return visitNode(cbNode, (node as ts.ThrowStatement).expression); - case ts.SyntaxKind.TryStatement: - return visitNode(cbNode, (node as ts.TryStatement).tryBlock) || - visitNode(cbNode, (node as ts.TryStatement).catchClause) || - visitNode(cbNode, (node as ts.TryStatement).finallyBlock); - case ts.SyntaxKind.CatchClause: - return visitNode(cbNode, (node as ts.CatchClause).variableDeclaration) || - visitNode(cbNode, (node as ts.CatchClause).block); - case ts.SyntaxKind.Decorator: - return visitNode(cbNode, (node as ts.Decorator).expression); - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ClassLikeDeclaration).name) || - visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).typeParameters) || - visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).heritageClauses) || - visitNodes(cbNode, cbNodes, (node as ts.ClassLikeDeclaration).members); - case ts.SyntaxKind.InterfaceDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.InterfaceDeclaration).name) || - visitNodes(cbNode, cbNodes, (node as ts.InterfaceDeclaration).typeParameters) || - visitNodes(cbNode, cbNodes, (node as ts.ClassDeclaration).heritageClauses) || - visitNodes(cbNode, cbNodes, (node as ts.InterfaceDeclaration).members); - case ts.SyntaxKind.TypeAliasDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.TypeAliasDeclaration).name) || - visitNodes(cbNode, cbNodes, (node as ts.TypeAliasDeclaration).typeParameters) || - visitNode(cbNode, (node as ts.TypeAliasDeclaration).type); - case ts.SyntaxKind.EnumDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.EnumDeclaration).name) || - visitNodes(cbNode, cbNodes, (node as ts.EnumDeclaration).members); - case ts.SyntaxKind.EnumMember: - return visitNode(cbNode, (node as ts.EnumMember).name) || - visitNode(cbNode, (node as ts.EnumMember).initializer); - case ts.SyntaxKind.ModuleDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ModuleDeclaration).name) || - visitNode(cbNode, (node as ts.ModuleDeclaration).body); - case ts.SyntaxKind.ImportEqualsDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ImportEqualsDeclaration).name) || - visitNode(cbNode, (node as ts.ImportEqualsDeclaration).moduleReference); - case ts.SyntaxKind.ImportDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ImportDeclaration).importClause) || - visitNode(cbNode, (node as ts.ImportDeclaration).moduleSpecifier) || - visitNode(cbNode, (node as ts.ImportDeclaration).assertClause); - case ts.SyntaxKind.ImportClause: - return visitNode(cbNode, (node as ts.ImportClause).name) || - visitNode(cbNode, (node as ts.ImportClause).namedBindings); - case ts.SyntaxKind.AssertClause: - return visitNodes(cbNode, cbNodes, (node as ts.AssertClause).elements); - case ts.SyntaxKind.AssertEntry: - return visitNode(cbNode, (node as ts.AssertEntry).name) || - visitNode(cbNode, (node as ts.AssertEntry).value); - case ts.SyntaxKind.NamespaceExportDeclaration: - return visitNode(cbNode, (node as ts.NamespaceExportDeclaration).name); - case ts.SyntaxKind.NamespaceImport: - return visitNode(cbNode, (node as ts.NamespaceImport).name); - case ts.SyntaxKind.NamespaceExport: - return visitNode(cbNode, (node as ts.NamespaceExport).name); - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.NamedExports: - return visitNodes(cbNode, cbNodes, (node as ts.NamedImportsOrExports).elements); - case ts.SyntaxKind.ExportDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ExportDeclaration).exportClause) || - visitNode(cbNode, (node as ts.ExportDeclaration).moduleSpecifier) || - visitNode(cbNode, (node as ts.ExportDeclaration).assertClause); - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - return visitNode(cbNode, (node as ts.ImportOrExportSpecifier).propertyName) || - visitNode(cbNode, (node as ts.ImportOrExportSpecifier).name); - case ts.SyntaxKind.ExportAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node as ts.ExportAssignment).expression); - case ts.SyntaxKind.TemplateExpression: - return visitNode(cbNode, (node as ts.TemplateExpression).head) || visitNodes(cbNode, cbNodes, (node as ts.TemplateExpression).templateSpans); - case ts.SyntaxKind.TemplateSpan: - return visitNode(cbNode, (node as ts.TemplateSpan).expression) || visitNode(cbNode, (node as ts.TemplateSpan).literal); - case ts.SyntaxKind.TemplateLiteralType: - return visitNode(cbNode, (node as ts.TemplateLiteralTypeNode).head) || visitNodes(cbNode, cbNodes, (node as ts.TemplateLiteralTypeNode).templateSpans); - case ts.SyntaxKind.TemplateLiteralTypeSpan: - return visitNode(cbNode, (node as ts.TemplateLiteralTypeSpan).type) || visitNode(cbNode, (node as ts.TemplateLiteralTypeSpan).literal); - case ts.SyntaxKind.ComputedPropertyName: - return visitNode(cbNode, (node as ts.ComputedPropertyName).expression); - case ts.SyntaxKind.HeritageClause: - return visitNodes(cbNode, cbNodes, (node as ts.HeritageClause).types); - case ts.SyntaxKind.ExpressionWithTypeArguments: - return visitNode(cbNode, (node as ts.ExpressionWithTypeArguments).expression) || - visitNodes(cbNode, cbNodes, (node as ts.ExpressionWithTypeArguments).typeArguments); - case ts.SyntaxKind.ExternalModuleReference: - return visitNode(cbNode, (node as ts.ExternalModuleReference).expression); - case ts.SyntaxKind.MissingDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators); - case ts.SyntaxKind.CommaListExpression: - return visitNodes(cbNode, cbNodes, (node as ts.CommaListExpression).elements); - case ts.SyntaxKind.JsxElement: - return visitNode(cbNode, (node as ts.JsxElement).openingElement) || - visitNodes(cbNode, cbNodes, (node as ts.JsxElement).children) || - visitNode(cbNode, (node as ts.JsxElement).closingElement); - case ts.SyntaxKind.JsxFragment: - return visitNode(cbNode, (node as ts.JsxFragment).openingFragment) || - visitNodes(cbNode, cbNodes, (node as ts.JsxFragment).children) || - visitNode(cbNode, (node as ts.JsxFragment).closingFragment); - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxOpeningElement: - return visitNode(cbNode, (node as ts.JsxOpeningLikeElement).tagName) || - visitNodes(cbNode, cbNodes, (node as ts.JsxOpeningLikeElement).typeArguments) || - visitNode(cbNode, (node as ts.JsxOpeningLikeElement).attributes); - case ts.SyntaxKind.JsxAttributes: - return visitNodes(cbNode, cbNodes, (node as ts.JsxAttributes).properties); - case ts.SyntaxKind.JsxAttribute: - return visitNode(cbNode, (node as ts.JsxAttribute).name) || - visitNode(cbNode, (node as ts.JsxAttribute).initializer); - case ts.SyntaxKind.JsxSpreadAttribute: - return visitNode(cbNode, (node as ts.JsxSpreadAttribute).expression); - case ts.SyntaxKind.JsxExpression: - return visitNode(cbNode, (node as ts.JsxExpression).dotDotDotToken) || - visitNode(cbNode, (node as ts.JsxExpression).expression); - case ts.SyntaxKind.JsxClosingElement: - return visitNode(cbNode, (node as ts.JsxClosingElement).tagName); - case ts.SyntaxKind.OptionalType: - case ts.SyntaxKind.RestType: - case ts.SyntaxKind.JSDocTypeExpression: - case ts.SyntaxKind.JSDocNonNullableType: - case ts.SyntaxKind.JSDocNullableType: - case ts.SyntaxKind.JSDocOptionalType: - case ts.SyntaxKind.JSDocVariadicType: - return visitNode(cbNode, (node as ts.OptionalTypeNode | ts.RestTypeNode | ts.JSDocTypeExpression | ts.JSDocTypeReferencingNode).type); - case ts.SyntaxKind.JSDocFunctionType: - return visitNodes(cbNode, cbNodes, (node as ts.JSDocFunctionType).parameters) || - visitNode(cbNode, (node as ts.JSDocFunctionType).type); - case ts.SyntaxKind.JSDoc: - return (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) - || visitNodes(cbNode, cbNodes, (node as ts.JSDoc).tags); - case ts.SyntaxKind.JSDocSeeTag: - return visitNode(cbNode, (node as ts.JSDocSeeTag).tagName) || - visitNode(cbNode, (node as ts.JSDocSeeTag).name) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocNameReference: - return visitNode(cbNode, (node as ts.JSDocNameReference).name); - case ts.SyntaxKind.JSDocMemberName: - return visitNode(cbNode, (node as ts.JSDocMemberName).left) || - visitNode(cbNode, (node as ts.JSDocMemberName).right); - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocPropertyTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - ((node as ts.JSDocPropertyLikeTag).isNameFirst - ? visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).name) || - visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).typeExpression) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) - : visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).typeExpression) || - visitNode(cbNode, (node as ts.JSDocPropertyLikeTag).name) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined))); - case ts.SyntaxKind.JSDocAuthorTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocImplementsTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - visitNode(cbNode, (node as ts.JSDocImplementsTag).class) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocAugmentsTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - visitNode(cbNode, (node as ts.JSDocAugmentsTag).class) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocTemplateTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - visitNode(cbNode, (node as ts.JSDocTemplateTag).constraint) || - visitNodes(cbNode, cbNodes, (node as ts.JSDocTemplateTag).typeParameters) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocTypedefTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - ((node as ts.JSDocTypedefTag).typeExpression && - (node as ts.JSDocTypedefTag).typeExpression!.kind === ts.SyntaxKind.JSDocTypeExpression - ? visitNode(cbNode, (node as ts.JSDocTypedefTag).typeExpression) || - visitNode(cbNode, (node as ts.JSDocTypedefTag).fullName) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)) - : visitNode(cbNode, (node as ts.JSDocTypedefTag).fullName) || - visitNode(cbNode, (node as ts.JSDocTypedefTag).typeExpression) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined))); - case ts.SyntaxKind.JSDocCallbackTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - visitNode(cbNode, (node as ts.JSDocCallbackTag).fullName) || - visitNode(cbNode, (node as ts.JSDocCallbackTag).typeExpression) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocReturnTag: - case ts.SyntaxKind.JSDocTypeTag: - case ts.SyntaxKind.JSDocThisTag: - case ts.SyntaxKind.JSDocEnumTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) || - visitNode(cbNode, (node as ts.JSDocReturnTag | ts.JSDocTypeTag | ts.JSDocThisTag | ts.JSDocEnumTag).typeExpression) || - (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.JSDocSignature: - return ts.forEach((node as ts.JSDocSignature).typeParameters, cbNode) || - ts.forEach((node as ts.JSDocSignature).parameters, cbNode) || - visitNode(cbNode, (node as ts.JSDocSignature).type); - case ts.SyntaxKind.JSDocLink: - case ts.SyntaxKind.JSDocLinkCode: - case ts.SyntaxKind.JSDocLinkPlain: - return visitNode(cbNode, (node as ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain).name); - case ts.SyntaxKind.JSDocTypeLiteral: - return ts.forEach((node as ts.JSDocTypeLiteral).jsDocPropertyTags, cbNode); - case ts.SyntaxKind.JSDocTag: - case ts.SyntaxKind.JSDocClassTag: - case ts.SyntaxKind.JSDocPublicTag: - case ts.SyntaxKind.JSDocPrivateTag: - case ts.SyntaxKind.JSDocProtectedTag: - case ts.SyntaxKind.JSDocReadonlyTag: - case ts.SyntaxKind.JSDocDeprecatedTag: - return visitNode(cbNode, (node as ts.JSDocTag).tagName) - || (typeof (node as ts.JSDoc).comment === "string" ? undefined : visitNodes(cbNode, cbNodes, (node as ts.JSDoc).comment as ts.NodeArray | undefined)); - case ts.SyntaxKind.PartiallyEmittedExpression: - return visitNode(cbNode, (node as ts.PartiallyEmittedExpression).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: ts.Node, cbNode: (node: ts.Node, parent: ts.Node) => T | "skip" | undefined, cbNodes?: (nodes: ts.NodeArray, parent: ts.Node) => T | "skip" | undefined): T | undefined { - const queue: (ts.Node | ts.NodeArray)[] = gatherPossibleChildren(rootNode); - const parents: ts.Node[] = []; // tracks parent references for elements in queue - while (parents.length < queue.length) { - parents.push(rootNode); - } - while (queue.length !== 0) { - const current = queue.pop()!; - const parent = parents.pop()!; - if (ts.isArray(current)) { - if (cbNodes) { - const res = cbNodes(current, parent); - if (res) { - if (res === "skip") - continue; - return res; - } - } - for (let i = current.length - 1; i >= 0; --i) { - queue.push(current[i]); - parents.push(parent); - } - } - else { - const res = cbNode(current, parent); +/** @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: ts.Node, cbNode: (node: ts.Node, parent: ts.Node) => T | "skip" | undefined, cbNodes?: (nodes: ts.NodeArray, parent: ts.Node) => T | "skip" | undefined): T | undefined { + const queue: (ts.Node | ts.NodeArray)[] = gatherPossibleChildren(rootNode); + const parents: ts.Node[] = []; // tracks parent references for elements in queue + while (parents.length < queue.length) { + parents.push(rootNode); + } + while (queue.length !== 0) { + const current = queue.pop()!; + const parent = parents.pop()!; + if (ts.isArray(current)) { + if (cbNodes) { + const res = cbNodes(current, parent); if (res) { if (res === "skip") continue; return res; } - if (current.kind >= ts.SyntaxKind.FirstNode) { - // add children in reverse order to the queue, so popping gives the first child - for (const child of gatherPossibleChildren(current)) { - queue.push(child); - parents.push(current); - } + } + for (let i = current.length - 1; i >= 0; --i) { + queue.push(current[i]); + parents.push(parent); + } + } + else { + const res = cbNode(current, parent); + if (res) { + if (res === "skip") + continue; + return res; + } + if (current.kind >= ts.SyntaxKind.FirstNode) { + // add children in reverse order to the queue, so popping gives the first child + for (const child of gatherPossibleChildren(current)) { + queue.push(child); + parents.push(current); } } } } +} - function gatherPossibleChildren(node: ts.Node) { - const children: (ts.Node | ts.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: ts.Node | ts.NodeArray) { - children.unshift(n); - } - } +function gatherPossibleChildren(node: ts.Node) { + const children: (ts.Node | ts.NodeArray)[] = []; + forEachChild(node, addWorkItem, addWorkItem); // By using a stack above and `unshift` here, we emulate a depth-first preorder traversal + return children; - export interface CreateSourceFileOptions { - languageVersion: ts.ScriptTarget; - /** - * Controls the format the file is detected as - this can be derived from only the path - * and files on disk, but needs to be done with a module resolution cache in scope to be performant. - * This is usually `undefined` for compilations that do not have `moduleResolution` values of `node16` or `nodenext`. - */ - impliedNodeFormat?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS; - /** - * Controls how module-y-ness is set for the given file. Usually the result of calling - * `getSetExternalModuleIndicator` on a valid `CompilerOptions` object. If not present, the default - * check specified by `isFileProbablyExternalModule` will be used to set the field. - */ - setExternalModuleIndicator?: (file: ts.SourceFile) => void; + function addWorkItem(n: ts.Node | ts.NodeArray) { + children.unshift(n); } +} - function setExternalModuleIndicator(sourceFile: ts.SourceFile) { - sourceFile.externalModuleIndicator = isFileProbablyExternalModule(sourceFile); - } +export interface CreateSourceFileOptions { + languageVersion: ts.ScriptTarget; + /** + * Controls the format the file is detected as - this can be derived from only the path + * and files on disk, but needs to be done with a module resolution cache in scope to be performant. + * This is usually `undefined` for compilations that do not have `moduleResolution` values of `node16` or `nodenext`. + */ + impliedNodeFormat?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS; + /** + * Controls how module-y-ness is set for the given file. Usually the result of calling + * `getSetExternalModuleIndicator` on a valid `CompilerOptions` object. If not present, the default + * check specified by `isFileProbablyExternalModule` will be used to set the field. + */ + setExternalModuleIndicator?: (file: ts.SourceFile) => void; +} - export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ts.ScriptTarget | CreateSourceFileOptions, setParentNodes = false, scriptKind?: ts.ScriptKind): ts.SourceFile { - ts.tracing?.push(ts.tracing.Phase.Parse, "createSourceFile", { path: fileName }, /*separateBeginAndEnd*/ true); - ts.performance.mark("beforeParse"); - let result: ts.SourceFile; - ts.perfLogger.logStartParseSourceFile(fileName); - const { languageVersion, setExternalModuleIndicator: overrideSetExternalModuleIndicator, impliedNodeFormat: format } = typeof languageVersionOrOptions === "object" ? languageVersionOrOptions : ({ languageVersion: languageVersionOrOptions } as CreateSourceFileOptions); - if (languageVersion === ts.ScriptTarget.JSON) { - result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ts.ScriptKind.JSON, ts.noop); - } - else { - const setIndicator = format === undefined ? overrideSetExternalModuleIndicator : (file: ts.SourceFile) => { - file.impliedNodeFormat = format; - return (overrideSetExternalModuleIndicator || setExternalModuleIndicator)(file); - }; - result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind, setIndicator); - } - ts.perfLogger.logStopParseSourceFile(); - ts.performance.mark("afterParse"); - ts.performance.measure("Parse", "beforeParse", "afterParse"); - ts.tracing?.pop(); - return result; - } +function setExternalModuleIndicator(sourceFile: ts.SourceFile) { + sourceFile.externalModuleIndicator = isFileProbablyExternalModule(sourceFile); +} - export function parseIsolatedEntityName(text: string, languageVersion: ts.ScriptTarget): ts.EntityName | undefined { - return Parser.parseIsolatedEntityName(text, languageVersion); +export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ts.ScriptTarget | CreateSourceFileOptions, setParentNodes = false, scriptKind?: ts.ScriptKind): ts.SourceFile { + ts.tracing?.push(ts.tracing.Phase.Parse, "createSourceFile", { path: fileName }, /*separateBeginAndEnd*/ true); + ts.performance.mark("beforeParse"); + let result: ts.SourceFile; + ts.perfLogger.logStartParseSourceFile(fileName); + const { languageVersion, setExternalModuleIndicator: overrideSetExternalModuleIndicator, impliedNodeFormat: format } = typeof languageVersionOrOptions === "object" ? languageVersionOrOptions : ({ languageVersion: languageVersionOrOptions } as CreateSourceFileOptions); + if (languageVersion === ts.ScriptTarget.JSON) { + result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ts.ScriptKind.JSON, ts.noop); } - - /** - * Parse json text into SyntaxTree and return node and parse errors if any - * @param fileName - * @param sourceText - */ - export function parseJsonText(fileName: string, sourceText: string): ts.JsonSourceFile { - return Parser.parseJsonText(fileName, sourceText); + else { + const setIndicator = format === undefined ? overrideSetExternalModuleIndicator : (file: ts.SourceFile) => { + file.impliedNodeFormat = format; + return (overrideSetExternalModuleIndicator || setExternalModuleIndicator)(file); + }; + result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind, setIndicator); } + ts.perfLogger.logStopParseSourceFile(); + ts.performance.mark("afterParse"); + ts.performance.measure("Parse", "beforeParse", "afterParse"); + ts.tracing?.pop(); + return result; +} - // See also `isExternalOrCommonJsModule` in utilities.ts - export function isExternalModule(file: ts.SourceFile): boolean { - return file.externalModuleIndicator !== undefined; - } +export function parseIsolatedEntityName(text: string, languageVersion: ts.ScriptTarget): ts.EntityName | undefined { + return Parser.parseIsolatedEntityName(text, languageVersion); +} - // 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: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks = false): ts.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 as ts.Mutable).flags |= (sourceFile.flags & ts.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); - } +/** + * Parse json text into SyntaxTree and return node and parse errors if any + * @param fileName + * @param sourceText + */ +export function parseJsonText(fileName: string, sourceText: string): ts.JsonSourceFile { + return Parser.parseJsonText(fileName, sourceText); +} - return result; - } +// See also `isExternalOrCommonJsModule` in utilities.ts +export function isExternalModule(file: ts.SourceFile): boolean { + return file.externalModuleIndicator !== undefined; +} - /* @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 = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true); - const disallowInAndDecoratorContext = ts.NodeFlags.DisallowInContext | ts.NodeFlags.DecoratorContext; - - // capture constructors in 'initializeState' to avoid null checks - // tslint:disable variable-name - let NodeConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; - let TokenConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; - let IdentifierConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; - let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; - let SourceFileConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; - // tslint:enable variable-name - - function countNode(node: ts.Node) { - nodeCount++; - return node; - } +// 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: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks = false): ts.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 as ts.Mutable).flags |= (sourceFile.flags & ts.NodeFlags.PermanentlySetIncrementalFlags); + return newSourceFile; +} - // Rather than using `createBaseNodeFactory` here, we establish a `BaseNodeFactory` that closes over the - // constructors above, which are reset each time `initializeState` is called. - const baseNodeFactory: ts.BaseNodeFactory = { - createBaseSourceFileNode: kind => countNode(new SourceFileConstructor(kind, /*pos*/ 0, /*end*/ 0)), - createBaseIdentifierNode: kind => countNode(new IdentifierConstructor(kind, /*pos*/ 0, /*end*/ 0)), - createBasePrivateIdentifierNode: kind => countNode(new PrivateIdentifierConstructor(kind, /*pos*/ 0, /*end*/ 0)), - createBaseTokenNode: kind => countNode(new TokenConstructor(kind, /*pos*/ 0, /*end*/ 0)), - createBaseNode: kind => countNode(new NodeConstructor(kind, /*pos*/ 0, /*end*/ 0)) - }; +/* @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); + } - const factory = ts.createNodeFactory(ts.NodeFactoryFlags.NoParenthesizerRules | ts.NodeFactoryFlags.NoNodeConverters | ts.NodeFactoryFlags.NoOriginalNode, baseNodeFactory); + return result; +} - let fileName: string; - let sourceFlags: ts.NodeFlags; - let sourceText: string; - let languageVersion: ts.ScriptTarget; - let scriptKind: ts.ScriptKind; - let languageVariant: ts.LanguageVariant; - let parseDiagnostics: ts.DiagnosticWithDetachedLocation[]; - let jsDocDiagnostics: ts.DiagnosticWithDetachedLocation[]; - let syntaxCursor: IncrementalParser.SyntaxCursor | undefined; +/* @internal */ +// Exposed only for testing. +export function parseJSDocTypeExpressionForTests(content: string, start?: number, length?: number) { + return Parser.JSDocParser.parseJSDocTypeExpressionForTests(content, start, length); +} - let currentToken: ts.SyntaxKind; - let nodeCount: number; - let identifiers: ts.ESMap; - let privateIdentifiers: ts.ESMap; - let identifierCount: number; +// 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 = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true); + const disallowInAndDecoratorContext = ts.NodeFlags.DisallowInContext | ts.NodeFlags.DecoratorContext; + + // capture constructors in 'initializeState' to avoid null checks + // tslint:disable variable-name + let NodeConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; + let TokenConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; + let IdentifierConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; + let PrivateIdentifierConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; + let SourceFileConstructor: new (kind: ts.SyntaxKind, pos: number, end: number) => ts.Node; + // tslint:enable variable-name + + function countNode(node: ts.Node) { + nodeCount++; + return node; + } - let parsingContext: ParsingContext; + // Rather than using `createBaseNodeFactory` here, we establish a `BaseNodeFactory` that closes over the + // constructors above, which are reset each time `initializeState` is called. + const baseNodeFactory: ts.BaseNodeFactory = { + createBaseSourceFileNode: kind => countNode(new SourceFileConstructor(kind, /*pos*/ 0, /*end*/ 0)), + createBaseIdentifierNode: kind => countNode(new IdentifierConstructor(kind, /*pos*/ 0, /*end*/ 0)), + createBasePrivateIdentifierNode: kind => countNode(new PrivateIdentifierConstructor(kind, /*pos*/ 0, /*end*/ 0)), + createBaseTokenNode: kind => countNode(new TokenConstructor(kind, /*pos*/ 0, /*end*/ 0)), + createBaseNode: kind => countNode(new NodeConstructor(kind, /*pos*/ 0, /*end*/ 0)) + }; - let notParenthesizedArrow: ts.Set | undefined; + const factory = ts.createNodeFactory(ts.NodeFactoryFlags.NoParenthesizerRules | ts.NodeFactoryFlags.NoNodeConverters | ts.NodeFactoryFlags.NoOriginalNode, baseNodeFactory); - // 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: ts.NodeFlags; + let fileName: string; + let sourceFlags: ts.NodeFlags; + let sourceText: string; + let languageVersion: ts.ScriptTarget; + let scriptKind: ts.ScriptKind; + let languageVariant: ts.LanguageVariant; + let parseDiagnostics: ts.DiagnosticWithDetachedLocation[]; + let jsDocDiagnostics: ts.DiagnosticWithDetachedLocation[]; + let syntaxCursor: IncrementalParser.SyntaxCursor | undefined; - // Indicates whether we are currently parsing top-level statements. - let topLevel = true; + let currentToken: ts.SyntaxKind; + let nodeCount: number; + let identifiers: ts.ESMap; + let privateIdentifiers: ts.ESMap; + let identifierCount: number; - // 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: ts.ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ts.ScriptKind, setExternalModuleIndicatorOverride?: (file: ts.SourceFile) => void): ts.SourceFile { - scriptKind = ts.ensureScriptKind(fileName, scriptKind); - if (scriptKind === ts.ScriptKind.JSON) { - const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes); - ts.convertToObjectWorker(result, result.statements[0]?.expression, result.parseDiagnostics, /*returnValue*/ false, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); - result.referencedFiles = ts.emptyArray; - result.typeReferenceDirectives = ts.emptyArray; - result.libReferenceDirectives = ts.emptyArray; - result.amdDependencies = ts.emptyArray; - result.hasNoDefaultLib = false; - result.pragmas = ts.emptyMap as ts.ReadonlyPragmaMap; - return result; - } + let parsingContext: ParsingContext; - initializeState(fileName, sourceText, languageVersion, syntaxCursor, scriptKind); + let notParenthesizedArrow: ts.Set | undefined; - const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind, setExternalModuleIndicatorOverride || setExternalModuleIndicator); + // 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: ts.NodeFlags; - clearState(); + // Indicates whether we are currently parsing top-level statements. + let topLevel = true; + // 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: ts.ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ts.ScriptKind, setExternalModuleIndicatorOverride?: (file: ts.SourceFile) => void): ts.SourceFile { + scriptKind = ts.ensureScriptKind(fileName, scriptKind); + if (scriptKind === ts.ScriptKind.JSON) { + const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes); + ts.convertToObjectWorker(result, result.statements[0]?.expression, result.parseDiagnostics, /*returnValue*/ false, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); + result.referencedFiles = ts.emptyArray; + result.typeReferenceDirectives = ts.emptyArray; + result.libReferenceDirectives = ts.emptyArray; + result.amdDependencies = ts.emptyArray; + result.hasNoDefaultLib = false; + result.pragmas = ts.emptyMap as ts.ReadonlyPragmaMap; return result; } - export function parseIsolatedEntityName(content: string, languageVersion: ts.ScriptTarget): ts.EntityName | undefined { - // Choice of `isDeclarationFile` should be arbitrary - initializeState("", content, languageVersion, /*syntaxCursor*/ undefined, ts.ScriptKind.JS); - // Prime the scanner. - nextToken(); - const entityName = parseEntityName(/*allowReservedWords*/ true); - const isInvalid = token() === ts.SyntaxKind.EndOfFileToken && !parseDiagnostics.length; - clearState(); - return isInvalid ? entityName : undefined; - } + initializeState(fileName, sourceText, languageVersion, syntaxCursor, scriptKind); - export function parseJsonText(fileName: string, sourceText: string, languageVersion: ts.ScriptTarget = ts.ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes = false): ts.JsonSourceFile { - initializeState(fileName, sourceText, languageVersion, syntaxCursor, ts.ScriptKind.JSON); - sourceFlags = contextFlags; + const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind, setExternalModuleIndicatorOverride || setExternalModuleIndicator); - // Prime the scanner. - nextToken(); - const pos = getNodePos(); - let statements, endOfFileToken; - if (token() === ts.SyntaxKind.EndOfFileToken) { - statements = createNodeArray([], pos, pos); - endOfFileToken = parseTokenNode(); - } - else { - // Loop and synthesize an ArrayLiteralExpression if there are more than - // one top-level expressions to ensure all input text is consumed. - let expressions: ts.Expression[] | ts.Expression | undefined; - while (token() !== ts.SyntaxKind.EndOfFileToken) { - let expression; - switch (token()) { - case ts.SyntaxKind.OpenBracketToken: - expression = parseArrayLiteralExpression(); - break; - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NullKeyword: - expression = parseTokenNode(); - break; - case ts.SyntaxKind.MinusToken: - if (lookAhead(() => nextToken() === ts.SyntaxKind.NumericLiteral && nextToken() !== ts.SyntaxKind.ColonToken)) { - expression = parsePrefixUnaryExpression() as ts.JsonMinusNumericLiteral; - } - else { - expression = parseObjectLiteralExpression(); - } - break; - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.StringLiteral: - if (lookAhead(() => nextToken() !== ts.SyntaxKind.ColonToken)) { - expression = parseLiteralNode() as ts.StringLiteral | ts.NumericLiteral; - break; - } - // falls through - default: - expression = parseObjectLiteralExpression(); - break; - } + clearState(); - // Error recovery: collect multiple top-level expressions - if (expressions && ts.isArray(expressions)) { - expressions.push(expression); - } - else if (expressions) { - expressions = [expressions, expression]; - } - else { - expressions = expression; - if (token() !== ts.SyntaxKind.EndOfFileToken) { - parseErrorAtCurrentToken(ts.Diagnostics.Unexpected_token); - } - } - } + return result; + } - const expression = ts.isArray(expressions) ? finishNode(factory.createArrayLiteralExpression(expressions), pos) : ts.Debug.checkDefined(expressions); - const statement = factory.createExpressionStatement(expression) as ts.JsonObjectExpressionStatement; - finishNode(statement, pos); - statements = createNodeArray([statement], pos); - endOfFileToken = parseExpectedToken(ts.SyntaxKind.EndOfFileToken, ts.Diagnostics.Unexpected_token); - } + export function parseIsolatedEntityName(content: string, languageVersion: ts.ScriptTarget): ts.EntityName | undefined { + // Choice of `isDeclarationFile` should be arbitrary + initializeState("", content, languageVersion, /*syntaxCursor*/ undefined, ts.ScriptKind.JS); + // Prime the scanner. + nextToken(); + const entityName = parseEntityName(/*allowReservedWords*/ true); + const isInvalid = token() === ts.SyntaxKind.EndOfFileToken && !parseDiagnostics.length; + clearState(); + return isInvalid ? entityName : undefined; + } - // Set source file so that errors will be reported with this file name - const sourceFile = createSourceFile(fileName, ts.ScriptTarget.ES2015, ts.ScriptKind.JSON, /*isDeclaration*/ false, statements, endOfFileToken, sourceFlags, ts.noop); + export function parseJsonText(fileName: string, sourceText: string, languageVersion: ts.ScriptTarget = ts.ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes = false): ts.JsonSourceFile { + initializeState(fileName, sourceText, languageVersion, syntaxCursor, ts.ScriptKind.JSON); + sourceFlags = contextFlags; - if (setParentNodes) { - fixupParentReferences(sourceFile); - } + // Prime the scanner. + nextToken(); + const pos = getNodePos(); + let statements, endOfFileToken; + if (token() === ts.SyntaxKind.EndOfFileToken) { + statements = createNodeArray([], pos, pos); + endOfFileToken = parseTokenNode(); + } + else { + // Loop and synthesize an ArrayLiteralExpression if there are more than + // one top-level expressions to ensure all input text is consumed. + let expressions: ts.Expression[] | ts.Expression | undefined; + while (token() !== ts.SyntaxKind.EndOfFileToken) { + let expression; + switch (token()) { + case ts.SyntaxKind.OpenBracketToken: + expression = parseArrayLiteralExpression(); + break; + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + expression = parseTokenNode(); + break; + case ts.SyntaxKind.MinusToken: + if (lookAhead(() => nextToken() === ts.SyntaxKind.NumericLiteral && nextToken() !== ts.SyntaxKind.ColonToken)) { + expression = parsePrefixUnaryExpression() as ts.JsonMinusNumericLiteral; + } + else { + expression = parseObjectLiteralExpression(); + } + break; + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.StringLiteral: + if (lookAhead(() => nextToken() !== ts.SyntaxKind.ColonToken)) { + expression = parseLiteralNode() as ts.StringLiteral | ts.NumericLiteral; + break; + } + // falls through + default: + expression = parseObjectLiteralExpression(); + break; + } - sourceFile.nodeCount = nodeCount; - sourceFile.identifierCount = identifierCount; - sourceFile.identifiers = identifiers; - sourceFile.parseDiagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); - if (jsDocDiagnostics) { - sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); + // Error recovery: collect multiple top-level expressions + if (expressions && ts.isArray(expressions)) { + expressions.push(expression); + } + else if (expressions) { + expressions = [expressions, expression]; + } + else { + expressions = expression; + if (token() !== ts.SyntaxKind.EndOfFileToken) { + parseErrorAtCurrentToken(ts.Diagnostics.Unexpected_token); + } + } } - const result = sourceFile as ts.JsonSourceFile; - clearState(); - return result; + const expression = ts.isArray(expressions) ? finishNode(factory.createArrayLiteralExpression(expressions), pos) : ts.Debug.checkDefined(expressions); + const statement = factory.createExpressionStatement(expression) as ts.JsonObjectExpressionStatement; + finishNode(statement, pos); + statements = createNodeArray([statement], pos); + endOfFileToken = parseExpectedToken(ts.SyntaxKind.EndOfFileToken, ts.Diagnostics.Unexpected_token); } - function initializeState(_fileName: string, _sourceText: string, _languageVersion: ts.ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, _scriptKind: ts.ScriptKind) { - NodeConstructor = ts.objectAllocator.getNodeConstructor(); - TokenConstructor = ts.objectAllocator.getTokenConstructor(); - IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor(); - PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor(); - SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor(); - fileName = ts.normalizePath(_fileName); - sourceText = _sourceText; - languageVersion = _languageVersion; - syntaxCursor = _syntaxCursor; - scriptKind = _scriptKind; - languageVariant = ts.getLanguageVariant(_scriptKind); - - parseDiagnostics = []; - parsingContext = 0; - identifiers = new ts.Map(); - privateIdentifiers = new ts.Map(); - identifierCount = 0; - nodeCount = 0; - sourceFlags = 0; - topLevel = true; - - switch (scriptKind) { - case ts.ScriptKind.JS: - case ts.ScriptKind.JSX: - contextFlags = ts.NodeFlags.JavaScriptFile; - break; - case ts.ScriptKind.JSON: - contextFlags = ts.NodeFlags.JavaScriptFile | ts.NodeFlags.JsonFile; - break; - default: - contextFlags = ts.NodeFlags.None; - break; - } - parseErrorBeforeNextFinishedNode = false; + // Set source file so that errors will be reported with this file name + const sourceFile = createSourceFile(fileName, ts.ScriptTarget.ES2015, ts.ScriptKind.JSON, /*isDeclaration*/ false, statements, endOfFileToken, sourceFlags, ts.noop); - // Initialize and prime the scanner before parsing the source elements. - scanner.setText(sourceText); - scanner.setOnError(scanError); - scanner.setScriptTarget(languageVersion); - scanner.setLanguageVariant(languageVariant); + if (setParentNodes) { + fixupParentReferences(sourceFile); } - function clearState() { - // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. - scanner.clearCommentDirectives(); - scanner.setText(""); - scanner.setOnError(undefined); - - // Clear any data. We don't want to accidentally hold onto it for too long. - sourceText = undefined!; - languageVersion = undefined!; - syntaxCursor = undefined; - scriptKind = undefined!; - languageVariant = undefined!; - sourceFlags = 0; - parseDiagnostics = undefined!; - jsDocDiagnostics = undefined!; - parsingContext = 0; - identifiers = undefined!; - notParenthesizedArrow = undefined; - topLevel = true; + sourceFile.nodeCount = nodeCount; + sourceFile.identifierCount = identifierCount; + sourceFile.identifiers = identifiers; + sourceFile.parseDiagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); + if (jsDocDiagnostics) { + sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); } - function parseSourceFileWorker(languageVersion: ts.ScriptTarget, setParentNodes: boolean, scriptKind: ts.ScriptKind, setExternalModuleIndicator: (file: ts.SourceFile) => void): ts.SourceFile { - const isDeclarationFile = isDeclarationFileName(fileName); - if (isDeclarationFile) { - contextFlags |= ts.NodeFlags.Ambient; - } + const result = sourceFile as ts.JsonSourceFile; + clearState(); + return result; + } - sourceFlags = contextFlags; + function initializeState(_fileName: string, _sourceText: string, _languageVersion: ts.ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, _scriptKind: ts.ScriptKind) { + NodeConstructor = ts.objectAllocator.getNodeConstructor(); + TokenConstructor = ts.objectAllocator.getTokenConstructor(); + IdentifierConstructor = ts.objectAllocator.getIdentifierConstructor(); + PrivateIdentifierConstructor = ts.objectAllocator.getPrivateIdentifierConstructor(); + SourceFileConstructor = ts.objectAllocator.getSourceFileConstructor(); + fileName = ts.normalizePath(_fileName); + sourceText = _sourceText; + languageVersion = _languageVersion; + syntaxCursor = _syntaxCursor; + scriptKind = _scriptKind; + languageVariant = ts.getLanguageVariant(_scriptKind); + + parseDiagnostics = []; + parsingContext = 0; + identifiers = new ts.Map(); + privateIdentifiers = new ts.Map(); + identifierCount = 0; + nodeCount = 0; + sourceFlags = 0; + topLevel = true; + + switch (scriptKind) { + case ts.ScriptKind.JS: + case ts.ScriptKind.JSX: + contextFlags = ts.NodeFlags.JavaScriptFile; + break; + case ts.ScriptKind.JSON: + contextFlags = ts.NodeFlags.JavaScriptFile | ts.NodeFlags.JsonFile; + break; + default: + contextFlags = ts.NodeFlags.None; + break; + } + parseErrorBeforeNextFinishedNode = false; - // Prime the scanner. - nextToken(); + // Initialize and prime the scanner before parsing the source elements. + scanner.setText(sourceText); + scanner.setOnError(scanError); + scanner.setScriptTarget(languageVersion); + scanner.setLanguageVariant(languageVariant); + } - const statements = parseList(ParsingContext.SourceElements, parseStatement); - ts.Debug.assert(token() === ts.SyntaxKind.EndOfFileToken); - const endOfFileToken = addJSDocComment(parseTokenNode()); + function clearState() { + // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. + scanner.clearCommentDirectives(); + scanner.setText(""); + scanner.setOnError(undefined); + + // Clear any data. We don't want to accidentally hold onto it for too long. + sourceText = undefined!; + languageVersion = undefined!; + syntaxCursor = undefined; + scriptKind = undefined!; + languageVariant = undefined!; + sourceFlags = 0; + parseDiagnostics = undefined!; + jsDocDiagnostics = undefined!; + parsingContext = 0; + identifiers = undefined!; + notParenthesizedArrow = undefined; + topLevel = true; + } - const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags, setExternalModuleIndicator); + function parseSourceFileWorker(languageVersion: ts.ScriptTarget, setParentNodes: boolean, scriptKind: ts.ScriptKind, setExternalModuleIndicator: (file: ts.SourceFile) => void): ts.SourceFile { + const isDeclarationFile = isDeclarationFileName(fileName); + if (isDeclarationFile) { + contextFlags |= ts.NodeFlags.Ambient; + } - // 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); + sourceFlags = contextFlags; - sourceFile.commentDirectives = scanner.getCommentDirectives(); - sourceFile.nodeCount = nodeCount; - sourceFile.identifierCount = identifierCount; - sourceFile.identifiers = identifiers; - sourceFile.parseDiagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); - if (jsDocDiagnostics) { - sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); - } + // Prime the scanner. + nextToken(); - if (setParentNodes) { - fixupParentReferences(sourceFile); - } + const statements = parseList(ParsingContext.SourceElements, parseStatement); + ts.Debug.assert(token() === ts.SyntaxKind.EndOfFileToken); + const endOfFileToken = addJSDocComment(parseTokenNode()); - return sourceFile; + const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags, setExternalModuleIndicator); - function reportPragmaDiagnostic(pos: number, end: number, diagnostic: ts.DiagnosticMessage) { - parseDiagnostics.push(ts.createDetachedDiagnostic(fileName, pos, end, diagnostic)); - } - } + // 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); - function withJSDoc(node: T, hasJSDoc: boolean): T { - return hasJSDoc ? addJSDocComment(node) : node; + sourceFile.commentDirectives = scanner.getCommentDirectives(); + sourceFile.nodeCount = nodeCount; + sourceFile.identifierCount = identifierCount; + sourceFile.identifiers = identifiers; + sourceFile.parseDiagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); + if (jsDocDiagnostics) { + sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); } - let hasDeprecatedTag = false; - function addJSDocComment(node: T): T { - ts.Debug.assert(!node.jsDoc); // Should only be called once per node - const jsDoc = ts.mapDefined(ts.getJSDocCommentRanges(node, sourceText), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); - if (jsDoc.length) - node.jsDoc = jsDoc; - if (hasDeprecatedTag) { - hasDeprecatedTag = false; - (node as ts.Mutable).flags |= ts.NodeFlags.Deprecated; - } - return node; + if (setParentNodes) { + fixupParentReferences(sourceFile); } - function reparseTopLevelAwait(sourceFile: ts.SourceFile) { - const savedSyntaxCursor = syntaxCursor; - const baseSyntaxCursor = IncrementalParser.createSyntaxCursor(sourceFile); - syntaxCursor = { currentNode }; + return sourceFile; - const statements: ts.Statement[] = []; - const savedParseDiagnostics = parseDiagnostics; + function reportPragmaDiagnostic(pos: number, end: number, diagnostic: ts.DiagnosticMessage) { + parseDiagnostics.push(ts.createDetachedDiagnostic(fileName, pos, end, diagnostic)); + } + } - parseDiagnostics = []; + function withJSDoc(node: T, hasJSDoc: boolean): T { + return hasJSDoc ? addJSDocComment(node) : node; + } - let pos = 0; - let start = findNextStatementWithAwait(sourceFile.statements, 0); - while (start !== -1) { - // append all statements between pos and start - const prevStatement = sourceFile.statements[pos]; - const nextStatement = sourceFile.statements[start]; - ts.addRange(statements, sourceFile.statements, pos, start); - pos = findNextStatementWithoutAwait(sourceFile.statements, start); + let hasDeprecatedTag = false; + function addJSDocComment(node: T): T { + ts.Debug.assert(!node.jsDoc); // Should only be called once per node + const jsDoc = ts.mapDefined(ts.getJSDocCommentRanges(node, sourceText), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); + if (jsDoc.length) + node.jsDoc = jsDoc; + if (hasDeprecatedTag) { + hasDeprecatedTag = false; + (node as ts.Mutable).flags |= ts.NodeFlags.Deprecated; + } + return node; + } - // append all diagnostics associated with the copied range - const diagnosticStart = ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= prevStatement.pos); - const diagnosticEnd = diagnosticStart >= 0 ? ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= nextStatement.pos, diagnosticStart) : -1; - if (diagnosticStart >= 0) { - ts.addRange(parseDiagnostics, savedParseDiagnostics, diagnosticStart, diagnosticEnd >= 0 ? diagnosticEnd : undefined); - } + function reparseTopLevelAwait(sourceFile: ts.SourceFile) { + const savedSyntaxCursor = syntaxCursor; + const baseSyntaxCursor = IncrementalParser.createSyntaxCursor(sourceFile); + syntaxCursor = { currentNode }; + + const statements: ts.Statement[] = []; + const savedParseDiagnostics = parseDiagnostics; + + parseDiagnostics = []; + + let pos = 0; + let start = findNextStatementWithAwait(sourceFile.statements, 0); + while (start !== -1) { + // append all statements between pos and start + const prevStatement = sourceFile.statements[pos]; + const nextStatement = sourceFile.statements[start]; + ts.addRange(statements, sourceFile.statements, pos, start); + pos = findNextStatementWithoutAwait(sourceFile.statements, start); + + // append all diagnostics associated with the copied range + const diagnosticStart = ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= prevStatement.pos); + const diagnosticEnd = diagnosticStart >= 0 ? ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= nextStatement.pos, diagnosticStart) : -1; + if (diagnosticStart >= 0) { + ts.addRange(parseDiagnostics, savedParseDiagnostics, diagnosticStart, diagnosticEnd >= 0 ? diagnosticEnd : undefined); + } + + // reparse all statements between start and pos. We skip existing diagnostics for the same range and allow the parser to generate new ones. + speculationHelper(() => { + const savedContextFlags = contextFlags; + contextFlags |= ts.NodeFlags.AwaitContext; + scanner.setTextPos(nextStatement.pos); + nextToken(); - // reparse all statements between start and pos. We skip existing diagnostics for the same range and allow the parser to generate new ones. - speculationHelper(() => { - const savedContextFlags = contextFlags; - contextFlags |= ts.NodeFlags.AwaitContext; - scanner.setTextPos(nextStatement.pos); - nextToken(); + while (token() !== ts.SyntaxKind.EndOfFileToken) { + const startPos = scanner.getStartPos(); + const statement = parseListElement(ParsingContext.SourceElements, parseStatement); + statements.push(statement); + if (startPos === scanner.getStartPos()) { + nextToken(); + } - while (token() !== ts.SyntaxKind.EndOfFileToken) { - const startPos = scanner.getStartPos(); - const statement = parseListElement(ParsingContext.SourceElements, parseStatement); - statements.push(statement); - if (startPos === scanner.getStartPos()) { - nextToken(); + if (pos >= 0) { + const nonAwaitStatement = sourceFile.statements[pos]; + if (statement.end === nonAwaitStatement.pos) { + // done reparsing this section + break; } - - if (pos >= 0) { - const nonAwaitStatement = sourceFile.statements[pos]; - if (statement.end === nonAwaitStatement.pos) { - // done reparsing this section - break; - } - if (statement.end > nonAwaitStatement.pos) { - // we ate into the next statement, so we must reparse it. - pos = findNextStatementWithoutAwait(sourceFile.statements, pos + 1); - } + if (statement.end > nonAwaitStatement.pos) { + // we ate into the next statement, so we must reparse it. + pos = findNextStatementWithoutAwait(sourceFile.statements, pos + 1); } } + } - contextFlags = savedContextFlags; - }, SpeculationKind.Reparse); + contextFlags = savedContextFlags; + }, SpeculationKind.Reparse); - // find the next statement containing an `await` - start = pos >= 0 ? findNextStatementWithAwait(sourceFile.statements, pos) : -1; - } + // find the next statement containing an `await` + start = pos >= 0 ? findNextStatementWithAwait(sourceFile.statements, pos) : -1; + } - // append all statements between pos and the end of the list - if (pos >= 0) { - const prevStatement = sourceFile.statements[pos]; - ts.addRange(statements, sourceFile.statements, pos); + // append all statements between pos and the end of the list + if (pos >= 0) { + const prevStatement = sourceFile.statements[pos]; + ts.addRange(statements, sourceFile.statements, pos); - // append all diagnostics associated with the copied range - const diagnosticStart = ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= prevStatement.pos); - if (diagnosticStart >= 0) { - ts.addRange(parseDiagnostics, savedParseDiagnostics, diagnosticStart); - } + // append all diagnostics associated with the copied range + const diagnosticStart = ts.findIndex(savedParseDiagnostics, diagnostic => diagnostic.start >= prevStatement.pos); + if (diagnosticStart >= 0) { + ts.addRange(parseDiagnostics, savedParseDiagnostics, diagnosticStart); } + } - syntaxCursor = savedSyntaxCursor; - return factory.updateSourceFile(sourceFile, ts.setTextRange(factory.createNodeArray(statements), sourceFile.statements)); - function containsPossibleTopLevelAwait(node: ts.Node) { - return !(node.flags & ts.NodeFlags.AwaitContext) - && !!(node.transformFlags & ts.TransformFlags.ContainsPossibleTopLevelAwait); - } - function findNextStatementWithAwait(statements: ts.NodeArray, start: number) { - for (let i = start; i < statements.length; i++) { - if (containsPossibleTopLevelAwait(statements[i])) { - return i; - } + syntaxCursor = savedSyntaxCursor; + return factory.updateSourceFile(sourceFile, ts.setTextRange(factory.createNodeArray(statements), sourceFile.statements)); + function containsPossibleTopLevelAwait(node: ts.Node) { + return !(node.flags & ts.NodeFlags.AwaitContext) + && !!(node.transformFlags & ts.TransformFlags.ContainsPossibleTopLevelAwait); + } + function findNextStatementWithAwait(statements: ts.NodeArray, start: number) { + for (let i = start; i < statements.length; i++) { + if (containsPossibleTopLevelAwait(statements[i])) { + return i; } - return -1; } + return -1; + } - function findNextStatementWithoutAwait(statements: ts.NodeArray, start: number) { - for (let i = start; i < statements.length; i++) { - if (!containsPossibleTopLevelAwait(statements[i])) { - return i; - } + function findNextStatementWithoutAwait(statements: ts.NodeArray, start: number) { + for (let i = start; i < statements.length; i++) { + if (!containsPossibleTopLevelAwait(statements[i])) { + return i; } - return -1; } + return -1; + } - function currentNode(position: number) { - const node = baseSyntaxCursor.currentNode(position); - if (topLevel && node && containsPossibleTopLevelAwait(node)) { - node.intersectsChange = true; - } - return node; + function currentNode(position: number) { + const node = baseSyntaxCursor.currentNode(position); + if (topLevel && node && containsPossibleTopLevelAwait(node)) { + node.intersectsChange = true; } - + return node; } - export function fixupParentReferences(rootNode: ts.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. - ts.setParentRecursive(rootNode, /*incremental*/ true); - } - function createSourceFile(fileName: string, languageVersion: ts.ScriptTarget, scriptKind: ts.ScriptKind, isDeclarationFile: boolean, statements: readonly ts.Statement[], endOfFileToken: ts.EndOfFileToken, flags: ts.NodeFlags, setExternalModuleIndicator: (sourceFile: ts.SourceFile) => void): ts.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 - let sourceFile = factory.createSourceFile(statements, endOfFileToken, flags); - ts.setTextRangePosWidth(sourceFile, 0, sourceText.length); - setFields(sourceFile); - - // If we parsed this as an external module, it may contain top-level await - if (!isDeclarationFile && isExternalModule(sourceFile) && sourceFile.transformFlags & ts.TransformFlags.ContainsPossibleTopLevelAwait) { - sourceFile = reparseTopLevelAwait(sourceFile); - setFields(sourceFile); - } - - return sourceFile; - - function setFields(sourceFile: ts.SourceFile) { - sourceFile.text = sourceText; - sourceFile.bindDiagnostics = []; - sourceFile.bindSuggestionDiagnostics = undefined; - sourceFile.languageVersion = languageVersion; - sourceFile.fileName = fileName; - sourceFile.languageVariant = ts.getLanguageVariant(scriptKind); - sourceFile.isDeclarationFile = isDeclarationFile; - sourceFile.scriptKind = scriptKind; + } - setExternalModuleIndicator(sourceFile); - sourceFile.setExternalModuleIndicator = setExternalModuleIndicator; - } + export function fixupParentReferences(rootNode: ts.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. + ts.setParentRecursive(rootNode, /*incremental*/ true); + } + function createSourceFile(fileName: string, languageVersion: ts.ScriptTarget, scriptKind: ts.ScriptKind, isDeclarationFile: boolean, statements: readonly ts.Statement[], endOfFileToken: ts.EndOfFileToken, flags: ts.NodeFlags, setExternalModuleIndicator: (sourceFile: ts.SourceFile) => void): ts.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 + let sourceFile = factory.createSourceFile(statements, endOfFileToken, flags); + ts.setTextRangePosWidth(sourceFile, 0, sourceText.length); + setFields(sourceFile); + + // If we parsed this as an external module, it may contain top-level await + if (!isDeclarationFile && isExternalModule(sourceFile) && sourceFile.transformFlags & ts.TransformFlags.ContainsPossibleTopLevelAwait) { + sourceFile = reparseTopLevelAwait(sourceFile); + setFields(sourceFile); } - function setContextFlag(val: boolean, flag: ts.NodeFlags) { - if (val) { - contextFlags |= flag; - } - else { - contextFlags &= ~flag; - } - } + return sourceFile; - function setDisallowInContext(val: boolean) { - setContextFlag(val, ts.NodeFlags.DisallowInContext); - } + function setFields(sourceFile: ts.SourceFile) { + sourceFile.text = sourceText; + sourceFile.bindDiagnostics = []; + sourceFile.bindSuggestionDiagnostics = undefined; + sourceFile.languageVersion = languageVersion; + sourceFile.fileName = fileName; + sourceFile.languageVariant = ts.getLanguageVariant(scriptKind); + sourceFile.isDeclarationFile = isDeclarationFile; + sourceFile.scriptKind = scriptKind; - function setYieldContext(val: boolean) { - setContextFlag(val, ts.NodeFlags.YieldContext); + setExternalModuleIndicator(sourceFile); + sourceFile.setExternalModuleIndicator = setExternalModuleIndicator; } + } - function setDecoratorContext(val: boolean) { - setContextFlag(val, ts.NodeFlags.DecoratorContext); + function setContextFlag(val: boolean, flag: ts.NodeFlags) { + if (val) { + contextFlags |= flag; } - - function setAwaitContext(val: boolean) { - setContextFlag(val, ts.NodeFlags.AwaitContext); + else { + contextFlags &= ~flag; } + } - function doOutsideOfContext(context: ts.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; - } + function setDisallowInContext(val: boolean) { + setContextFlag(val, ts.NodeFlags.DisallowInContext); + } - // no need to do anything special as we are not in any of the requested contexts - return func(); - } - - function doInsideOfContext(context: ts.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; - } + function setYieldContext(val: boolean) { + setContextFlag(val, ts.NodeFlags.YieldContext); + } - // no need to do anything special as we are already in all of the requested contexts - return func(); - } + function setDecoratorContext(val: boolean) { + setContextFlag(val, ts.NodeFlags.DecoratorContext); + } - function allowInAnd(func: () => T): T { - return doOutsideOfContext(ts.NodeFlags.DisallowInContext, func); - } + function setAwaitContext(val: boolean) { + setContextFlag(val, ts.NodeFlags.AwaitContext); + } - function disallowInAnd(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.DisallowInContext, func); + function doOutsideOfContext(context: ts.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; } - function allowConditionalTypesAnd(func: () => T): T { - return doOutsideOfContext(ts.NodeFlags.DisallowConditionalTypesContext, func); - } + // no need to do anything special as we are not in any of the requested contexts + return func(); + } - function disallowConditionalTypesAnd(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.DisallowConditionalTypesContext, func); + function doInsideOfContext(context: ts.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; } - function doInYieldContext(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.YieldContext, func); - } + // no need to do anything special as we are already in all of the requested contexts + return func(); + } - function doInDecoratorContext(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.DecoratorContext, func); - } + function allowInAnd(func: () => T): T { + return doOutsideOfContext(ts.NodeFlags.DisallowInContext, func); + } - function doInAwaitContext(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.AwaitContext, func); - } + function disallowInAnd(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.DisallowInContext, func); + } - function doOutsideOfAwaitContext(func: () => T): T { - return doOutsideOfContext(ts.NodeFlags.AwaitContext, func); - } + function allowConditionalTypesAnd(func: () => T): T { + return doOutsideOfContext(ts.NodeFlags.DisallowConditionalTypesContext, func); + } - function doInYieldAndAwaitContext(func: () => T): T { - return doInsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext, func); - } + function disallowConditionalTypesAnd(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.DisallowConditionalTypesContext, func); + } - function doOutsideOfYieldAndAwaitContext(func: () => T): T { - return doOutsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext, func); - } + function doInYieldContext(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.YieldContext, func); + } - function inContext(flags: ts.NodeFlags) { - return (contextFlags & flags) !== 0; - } + function doInDecoratorContext(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.DecoratorContext, func); + } - function inYieldContext() { - return inContext(ts.NodeFlags.YieldContext); - } + function doInAwaitContext(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.AwaitContext, func); + } - function inDisallowInContext() { - return inContext(ts.NodeFlags.DisallowInContext); - } + function doOutsideOfAwaitContext(func: () => T): T { + return doOutsideOfContext(ts.NodeFlags.AwaitContext, func); + } - function inDisallowConditionalTypesContext() { - return inContext(ts.NodeFlags.DisallowConditionalTypesContext); - } + function doInYieldAndAwaitContext(func: () => T): T { + return doInsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext, func); + } - function inDecoratorContext() { - return inContext(ts.NodeFlags.DecoratorContext); - } + function doOutsideOfYieldAndAwaitContext(func: () => T): T { + return doOutsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext, func); + } - function inAwaitContext() { - return inContext(ts.NodeFlags.AwaitContext); - } + function inContext(flags: ts.NodeFlags) { + return (contextFlags & flags) !== 0; + } - function parseErrorAtCurrentToken(message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { - return parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), message, arg0); - } + function inYieldContext() { + return inContext(ts.NodeFlags.YieldContext); + } - function parseErrorAtPosition(start: number, length: number, message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { - // Don't report another error if it would just be at the same position as the last error. - const lastError = ts.lastOrUndefined(parseDiagnostics); - let result: ts.DiagnosticWithDetachedLocation | undefined; - if (!lastError || start !== lastError.start) { - result = ts.createDetachedDiagnostic(fileName, start, length, message, arg0); - parseDiagnostics.push(result); - } + function inDisallowInContext() { + return inContext(ts.NodeFlags.DisallowInContext); + } - // 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; - return result; - } + function inDisallowConditionalTypesContext() { + return inContext(ts.NodeFlags.DisallowConditionalTypesContext); + } - function parseErrorAt(start: number, end: number, message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { - return parseErrorAtPosition(start, end - start, message, arg0); - } + function inDecoratorContext() { + return inContext(ts.NodeFlags.DecoratorContext); + } - function parseErrorAtRange(range: ts.TextRange, message: ts.DiagnosticMessage, arg0?: any): void { - parseErrorAt(range.pos, range.end, message, arg0); - } + function inAwaitContext() { + return inContext(ts.NodeFlags.AwaitContext); + } - function scanError(message: ts.DiagnosticMessage, length: number): void { - parseErrorAtPosition(scanner.getTextPos(), length, message); - } + function parseErrorAtCurrentToken(message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { + return parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), message, arg0); + } - function getNodePos(): number { - return scanner.getStartPos(); + function parseErrorAtPosition(start: number, length: number, message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { + // Don't report another error if it would just be at the same position as the last error. + const lastError = ts.lastOrUndefined(parseDiagnostics); + let result: ts.DiagnosticWithDetachedLocation | undefined; + if (!lastError || start !== lastError.start) { + result = ts.createDetachedDiagnostic(fileName, start, length, message, arg0); + parseDiagnostics.push(result); } - function hasPrecedingJSDocComment() { - return scanner.hasPrecedingJSDocComment(); - } + // 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; + return result; + } - // 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(): ts.SyntaxKind { - return currentToken; - } + function parseErrorAt(start: number, end: number, message: ts.DiagnosticMessage, arg0?: any): ts.DiagnosticWithDetachedLocation | undefined { + return parseErrorAtPosition(start, end - start, message, arg0); + } - function nextTokenWithoutCheck() { - return currentToken = scanner.scan(); - } + function parseErrorAtRange(range: ts.TextRange, message: ts.DiagnosticMessage, arg0?: any): void { + parseErrorAt(range.pos, range.end, message, arg0); + } - function nextTokenAnd(func: () => T): T { - nextToken(); - return func(); - } + function scanError(message: ts.DiagnosticMessage, length: number): void { + parseErrorAtPosition(scanner.getTextPos(), length, message); + } - function nextToken(): ts.SyntaxKind { - // if the keyword had an escape - if (ts.isKeyword(currentToken) && (scanner.hasUnicodeEscape() || scanner.hasExtendedUnicodeEscape())) { - // issue a parse error for the escape - parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), ts.Diagnostics.Keywords_cannot_contain_escape_characters); - } - return nextTokenWithoutCheck(); - } + function getNodePos(): number { + return scanner.getStartPos(); + } - function nextTokenJSDoc(): ts.JSDocSyntaxKind { - return currentToken = scanner.scanJsDocToken(); - } + function hasPrecedingJSDocComment() { + return scanner.hasPrecedingJSDocComment(); + } - function reScanGreaterToken(): ts.SyntaxKind { - return currentToken = scanner.reScanGreaterToken(); - } + // 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(): ts.SyntaxKind { + return currentToken; + } - function reScanSlashToken(): ts.SyntaxKind { - return currentToken = scanner.reScanSlashToken(); - } + function nextTokenWithoutCheck() { + return currentToken = scanner.scan(); + } - function reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind { - return currentToken = scanner.reScanTemplateToken(isTaggedTemplate); - } + function nextTokenAnd(func: () => T): T { + nextToken(); + return func(); + } - function reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind { - return currentToken = scanner.reScanTemplateHeadOrNoSubstitutionTemplate(); + function nextToken(): ts.SyntaxKind { + // if the keyword had an escape + if (ts.isKeyword(currentToken) && (scanner.hasUnicodeEscape() || scanner.hasExtendedUnicodeEscape())) { + // issue a parse error for the escape + parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), ts.Diagnostics.Keywords_cannot_contain_escape_characters); } + return nextTokenWithoutCheck(); + } - function reScanLessThanToken(): ts.SyntaxKind { - return currentToken = scanner.reScanLessThanToken(); - } + function nextTokenJSDoc(): ts.JSDocSyntaxKind { + return currentToken = scanner.scanJsDocToken(); + } - function reScanHashToken(): ts.SyntaxKind { - return currentToken = scanner.reScanHashToken(); - } + function reScanGreaterToken(): ts.SyntaxKind { + return currentToken = scanner.reScanGreaterToken(); + } - function scanJsxIdentifier(): ts.SyntaxKind { - return currentToken = scanner.scanJsxIdentifier(); - } + function reScanSlashToken(): ts.SyntaxKind { + return currentToken = scanner.reScanSlashToken(); + } - function scanJsxText(): ts.SyntaxKind { - return currentToken = scanner.scanJsxToken(); - } + function reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind { + return currentToken = scanner.reScanTemplateToken(isTaggedTemplate); + } - function scanJsxAttributeValue(): ts.SyntaxKind { - return currentToken = scanner.scanJsxAttributeValue(); - } + function reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind { + return currentToken = scanner.reScanTemplateHeadOrNoSubstitutionTemplate(); + } - function speculationHelper(callback: () => T, speculationKind: SpeculationKind): 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; + function reScanLessThanToken(): ts.SyntaxKind { + return currentToken = scanner.reScanLessThanToken(); + } - // 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; + function reScanHashToken(): ts.SyntaxKind { + return currentToken = scanner.reScanHashToken(); + } - // 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 = speculationKind !== SpeculationKind.TryParse - ? scanner.lookAhead(callback) - : scanner.tryScan(callback); + function scanJsxIdentifier(): ts.SyntaxKind { + return currentToken = scanner.scanJsxIdentifier(); + } - ts.Debug.assert(saveContextFlags === contextFlags); + function scanJsxText(): ts.SyntaxKind { + return currentToken = scanner.scanJsxToken(); + } - // If our callback returned something 'falsy' or we're just looking ahead, - // then unconditionally restore us to where we were. - if (!result || speculationKind !== SpeculationKind.TryParse) { - currentToken = saveToken; - if (speculationKind !== SpeculationKind.Reparse) { - parseDiagnostics.length = saveParseDiagnosticsLength; - } - parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; - } + function scanJsxAttributeValue(): ts.SyntaxKind { + return currentToken = scanner.scanJsxAttributeValue(); + } - return result; + function speculationHelper(callback: () => T, speculationKind: SpeculationKind): 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 = speculationKind !== SpeculationKind.TryParse + ? scanner.lookAhead(callback) + : scanner.tryScan(callback); + + ts.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 || speculationKind !== SpeculationKind.TryParse) { + currentToken = saveToken; + if (speculationKind !== SpeculationKind.Reparse) { + parseDiagnostics.length = saveParseDiagnosticsLength; + } + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; } - /** 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, SpeculationKind.Lookahead); - } + return result; + } - /** 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, SpeculationKind.TryParse); - } + /** 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, SpeculationKind.Lookahead); + } - function isBindingIdentifier(): boolean { - if (token() === ts.SyntaxKind.Identifier) { - return 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, SpeculationKind.TryParse); + } - // `let await`/`let yield` in [Yield] or [Await] are allowed here and disallowed in the binder. - return token() > ts.SyntaxKind.LastReservedWord; + function isBindingIdentifier(): boolean { + if (token() === ts.SyntaxKind.Identifier) { + return true; } - // Ignore strict mode flag because we will report an error in type checker instead. - function isIdentifier(): boolean { - if (token() === ts.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() === ts.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() === ts.SyntaxKind.AwaitKeyword && inAwaitContext()) { - return false; - } + // `let await`/`let yield` in [Yield] or [Await] are allowed here and disallowed in the binder. + return token() > ts.SyntaxKind.LastReservedWord; + } - return token() > ts.SyntaxKind.LastReservedWord; + // Ignore strict mode flag because we will report an error in type checker instead. + function isIdentifier(): boolean { + if (token() === ts.SyntaxKind.Identifier) { + return true; } - function parseExpected(kind: ts.SyntaxKind, diagnosticMessage?: ts.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(ts.Diagnostics._0_expected, ts.tokenToString(kind)); - } + // 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() === ts.SyntaxKind.YieldKeyword && inYieldContext()) { return false; } - const viableKeywordSuggestions = Object.keys(ts.textToKeywordObj).filter(keyword => keyword.length > 2); - - /** - * Provides a better error message than the generic "';' expected" if possible for - * known common variants of a missing semicolon, such as from a mispelled names. - * - * @param node Node preceding the expected semicolon location. - */ - function parseErrorForMissingSemicolonAfter(node: ts.Expression | ts.PropertyName): void { - // Tagged template literals are sometimes used in places where only simple strings are allowed, i.e.: - // module `M1` { - // ^^^^^^^^^^^ This block is parsed as a template literal like module`M1`. - if (ts.isTaggedTemplateExpression(node)) { - parseErrorAt(ts.skipTrivia(sourceText, node.template.pos), node.template.end, ts.Diagnostics.Module_declaration_names_may_only_use_or_quoted_strings); - return; - } - - // Otherwise, if this isn't a well-known keyword-like identifier, give the generic fallback message. - const expressionText = ts.isIdentifier(node) ? ts.idText(node) : undefined; - if (!expressionText || !ts.isIdentifierText(expressionText, languageVersion)) { - parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); - return; - } - - const pos = ts.skipTrivia(sourceText, node.pos); - - // Some known keywords are likely signs of syntax being used improperly. - switch (expressionText) { - case "const": - case "let": - case "var": - parseErrorAt(pos, node.end, ts.Diagnostics.Variable_declaration_not_allowed_at_this_location); - return; - - case "declare": - // If a declared node failed to parse, it would have emitted a diagnostic already. - return; - - case "interface": - parseErrorForInvalidName(ts.Diagnostics.Interface_name_cannot_be_0, ts.Diagnostics.Interface_must_be_given_a_name, ts.SyntaxKind.OpenBraceToken); - return; - - case "is": - parseErrorAt(pos, scanner.getTextPos(), ts.Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); - return; + // 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() === ts.SyntaxKind.AwaitKeyword && inAwaitContext()) { + return false; + } - case "module": - case "namespace": - parseErrorForInvalidName(ts.Diagnostics.Namespace_name_cannot_be_0, ts.Diagnostics.Namespace_must_be_given_a_name, ts.SyntaxKind.OpenBraceToken); - return; + return token() > ts.SyntaxKind.LastReservedWord; + } - case "type": - parseErrorForInvalidName(ts.Diagnostics.Type_alias_name_cannot_be_0, ts.Diagnostics.Type_alias_must_be_given_a_name, ts.SyntaxKind.EqualsToken); - return; + function parseExpected(kind: ts.SyntaxKind, diagnosticMessage?: ts.DiagnosticMessage, shouldAdvance = true): boolean { + if (token() === kind) { + if (shouldAdvance) { + nextToken(); } + return true; + } - // The user alternatively might have misspelled or forgotten to add a space after a common keyword. - const suggestion = ts.getSpellingSuggestion(expressionText, viableKeywordSuggestions, n => n) ?? getSpaceSuggestion(expressionText); - if (suggestion) { - parseErrorAt(pos, node.end, ts.Diagnostics.Unknown_keyword_or_identifier_Did_you_mean_0, suggestion); - return; - } + // Report specific message if provided with one. Otherwise, report generic fallback message. + if (diagnosticMessage) { + parseErrorAtCurrentToken(diagnosticMessage); + } + else { + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(kind)); + } + return false; + } - // Unknown tokens are handled with their own errors in the scanner - if (token() === ts.SyntaxKind.Unknown) { - return; - } + const viableKeywordSuggestions = Object.keys(ts.textToKeywordObj).filter(keyword => keyword.length > 2); - // Otherwise, we know this some kind of unknown word, not just a missing expected semicolon. - parseErrorAt(pos, node.end, ts.Diagnostics.Unexpected_keyword_or_identifier); + /** + * Provides a better error message than the generic "';' expected" if possible for + * known common variants of a missing semicolon, such as from a mispelled names. + * + * @param node Node preceding the expected semicolon location. + */ + function parseErrorForMissingSemicolonAfter(node: ts.Expression | ts.PropertyName): void { + // Tagged template literals are sometimes used in places where only simple strings are allowed, i.e.: + // module `M1` { + // ^^^^^^^^^^^ This block is parsed as a template literal like module`M1`. + if (ts.isTaggedTemplateExpression(node)) { + parseErrorAt(ts.skipTrivia(sourceText, node.template.pos), node.template.end, ts.Diagnostics.Module_declaration_names_may_only_use_or_quoted_strings); + return; } - /** - * Reports a diagnostic error for the current token being an invalid name. - * - * @param blankDiagnostic Diagnostic to report for the case of the name being blank (matched tokenIfBlankName). - * @param nameDiagnostic Diagnostic to report for all other cases. - * @param tokenIfBlankName Current token if the name was invalid for being blank (not provided / skipped). - */ - function parseErrorForInvalidName(nameDiagnostic: ts.DiagnosticMessage, blankDiagnostic: ts.DiagnosticMessage, tokenIfBlankName: ts.SyntaxKind) { - if (token() === tokenIfBlankName) { - parseErrorAtCurrentToken(blankDiagnostic); - } - else { - parseErrorAtCurrentToken(nameDiagnostic, scanner.getTokenValue()); - } + // Otherwise, if this isn't a well-known keyword-like identifier, give the generic fallback message. + const expressionText = ts.isIdentifier(node) ? ts.idText(node) : undefined; + if (!expressionText || !ts.isIdentifierText(expressionText, languageVersion)) { + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); + return; } - function getSpaceSuggestion(expressionText: string) { - for (const keyword of viableKeywordSuggestions) { - if (expressionText.length > keyword.length + 2 && ts.startsWith(expressionText, keyword)) { - return `${keyword} ${expressionText.slice(keyword.length)}`; - } - } - - return undefined; - } + const pos = ts.skipTrivia(sourceText, node.pos); - function parseSemicolonAfterPropertyName(name: ts.PropertyName, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { - if (token() === ts.SyntaxKind.AtToken && !scanner.hasPrecedingLineBreak()) { - parseErrorAtCurrentToken(ts.Diagnostics.Decorators_must_precede_the_name_and_all_keywords_of_property_declarations); + // Some known keywords are likely signs of syntax being used improperly. + switch (expressionText) { + case "const": + case "let": + case "var": + parseErrorAt(pos, node.end, ts.Diagnostics.Variable_declaration_not_allowed_at_this_location); return; - } - if (token() === ts.SyntaxKind.OpenParenToken) { - parseErrorAtCurrentToken(ts.Diagnostics.Cannot_start_a_function_call_in_a_type_annotation); - nextToken(); + case "declare": + // If a declared node failed to parse, it would have emitted a diagnostic already. return; - } - if (type && !canParseSemicolon()) { - if (initializer) { - parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); - } - else { - parseErrorAtCurrentToken(ts.Diagnostics.Expected_for_property_initializer); - } + case "interface": + parseErrorForInvalidName(ts.Diagnostics.Interface_name_cannot_be_0, ts.Diagnostics.Interface_must_be_given_a_name, ts.SyntaxKind.OpenBraceToken); return; - } - if (tryParseSemicolon()) { + case "is": + parseErrorAt(pos, scanner.getTextPos(), ts.Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); return; - } - if (initializer) { - parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); + case "module": + case "namespace": + parseErrorForInvalidName(ts.Diagnostics.Namespace_name_cannot_be_0, ts.Diagnostics.Namespace_must_be_given_a_name, ts.SyntaxKind.OpenBraceToken); return; - } - parseErrorForMissingSemicolonAfter(name); + case "type": + parseErrorForInvalidName(ts.Diagnostics.Type_alias_name_cannot_be_0, ts.Diagnostics.Type_alias_must_be_given_a_name, ts.SyntaxKind.EqualsToken); + return; } - function parseExpectedJSDoc(kind: ts.JSDocSyntaxKind) { - if (token() === kind) { - nextTokenJSDoc(); - return true; - } - parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(kind)); - return false; + // The user alternatively might have misspelled or forgotten to add a space after a common keyword. + const suggestion = ts.getSpellingSuggestion(expressionText, viableKeywordSuggestions, n => n) ?? getSpaceSuggestion(expressionText); + if (suggestion) { + parseErrorAt(pos, node.end, ts.Diagnostics.Unknown_keyword_or_identifier_Did_you_mean_0, suggestion); + return; } - function parseExpectedMatchingBrackets(openKind: ts.SyntaxKind, closeKind: ts.SyntaxKind, openParsed: boolean, openPosition: number) { - if (token() === closeKind) { - nextToken(); - return; - } - const lastError = parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(closeKind)); - if (!openParsed) { - return; - } - if (lastError) { - ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openPosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, ts.tokenToString(openKind), ts.tokenToString(closeKind))); - } + // Unknown tokens are handled with their own errors in the scanner + if (token() === ts.SyntaxKind.Unknown) { + return; } - function parseOptional(t: ts.SyntaxKind): boolean { - if (token() === t) { - nextToken(); - return true; - } - return false; - } + // Otherwise, we know this some kind of unknown word, not just a missing expected semicolon. + parseErrorAt(pos, node.end, ts.Diagnostics.Unexpected_keyword_or_identifier); + } - function parseOptionalToken(t: TKind): ts.Token; - function parseOptionalToken(t: ts.SyntaxKind): ts.Node | undefined { - if (token() === t) { - return parseTokenNode(); - } - return undefined; + /** + * Reports a diagnostic error for the current token being an invalid name. + * + * @param blankDiagnostic Diagnostic to report for the case of the name being blank (matched tokenIfBlankName). + * @param nameDiagnostic Diagnostic to report for all other cases. + * @param tokenIfBlankName Current token if the name was invalid for being blank (not provided / skipped). + */ + function parseErrorForInvalidName(nameDiagnostic: ts.DiagnosticMessage, blankDiagnostic: ts.DiagnosticMessage, tokenIfBlankName: ts.SyntaxKind) { + if (token() === tokenIfBlankName) { + parseErrorAtCurrentToken(blankDiagnostic); + } + else { + parseErrorAtCurrentToken(nameDiagnostic, scanner.getTokenValue()); } + } - function parseOptionalTokenJSDoc(t: TKind): ts.Token; - function parseOptionalTokenJSDoc(t: ts.JSDocSyntaxKind): ts.Node | undefined { - if (token() === t) { - return parseTokenNodeJSDoc(); + function getSpaceSuggestion(expressionText: string) { + for (const keyword of viableKeywordSuggestions) { + if (expressionText.length > keyword.length + 2 && ts.startsWith(expressionText, keyword)) { + return `${keyword} ${expressionText.slice(keyword.length)}`; } - return undefined; } - function parseExpectedToken(t: TKind, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): ts.Token; - function parseExpectedToken(t: ts.SyntaxKind, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): ts.Node { - return parseOptionalToken(t) || - createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || ts.Diagnostics._0_expected, arg0 || ts.tokenToString(t)); - } + return undefined; + } - function parseExpectedTokenJSDoc(t: TKind): ts.Token; - function parseExpectedTokenJSDoc(t: ts.JSDocSyntaxKind): ts.Node { - return parseOptionalTokenJSDoc(t) || - createMissingNode(t, /*reportAtCurrentPosition*/ false, ts.Diagnostics._0_expected, ts.tokenToString(t)); + function parseSemicolonAfterPropertyName(name: ts.PropertyName, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined) { + if (token() === ts.SyntaxKind.AtToken && !scanner.hasPrecedingLineBreak()) { + parseErrorAtCurrentToken(ts.Diagnostics.Decorators_must_precede_the_name_and_all_keywords_of_property_declarations); + return; } - function parseTokenNode(): T { - const pos = getNodePos(); - const kind = token(); + if (token() === ts.SyntaxKind.OpenParenToken) { + parseErrorAtCurrentToken(ts.Diagnostics.Cannot_start_a_function_call_in_a_type_annotation); nextToken(); - return finishNode(factory.createToken(kind), pos) as T; - } - - function parseTokenNodeJSDoc(): T { - const pos = getNodePos(); - const kind = token(); - nextTokenJSDoc(); - return finishNode(factory.createToken(kind), pos) as T; + return; } - function canParseSemicolon() { - // If there's a real semicolon, then we can always parse it out. - if (token() === ts.SyntaxKind.SemicolonToken) { - return true; + if (type && !canParseSemicolon()) { + if (initializer) { + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); + } + else { + parseErrorAtCurrentToken(ts.Diagnostics.Expected_for_property_initializer); } + return; + } - // We can parse out an optional semicolon in ASI cases in the following cases. - return token() === ts.SyntaxKind.CloseBraceToken || token() === ts.SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak(); + if (tryParseSemicolon()) { + return; } - function tryParseSemicolon() { - if (!canParseSemicolon()) { - return false; - } + if (initializer) { + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.SemicolonToken)); + return; + } - if (token() === ts.SyntaxKind.SemicolonToken) { - // consume the semicolon if it was explicitly provided. - nextToken(); - } + parseErrorForMissingSemicolonAfter(name); + } + function parseExpectedJSDoc(kind: ts.JSDocSyntaxKind) { + if (token() === kind) { + nextTokenJSDoc(); return true; } + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(kind)); + return false; + } - function parseSemicolon(): boolean { - return tryParseSemicolon() || parseExpected(ts.SyntaxKind.SemicolonToken); + function parseExpectedMatchingBrackets(openKind: ts.SyntaxKind, closeKind: ts.SyntaxKind, openParsed: boolean, openPosition: number) { + if (token() === closeKind) { + nextToken(); + return; } - - function createNodeArray(elements: T[], pos: number, end?: number, hasTrailingComma?: boolean): ts.NodeArray { - const array = factory.createNodeArray(elements, hasTrailingComma); - ts.setTextRangePosEnd(array, pos, end ?? scanner.getStartPos()); - return array; + const lastError = parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(closeKind)); + if (!openParsed) { + return; } - - function finishNode(node: T, pos: number, end?: number): T { - ts.setTextRangePosEnd(node, pos, end ?? scanner.getStartPos()); - if (contextFlags) { - (node as ts.Mutable).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 as ts.Mutable).flags |= ts.NodeFlags.ThisNodeHasError; - } - - return node; + if (lastError) { + ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openPosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, ts.tokenToString(openKind), ts.tokenToString(closeKind))); } + } - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: false, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): T; - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: ts.DiagnosticMessage, arg0?: any): T; - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: ts.DiagnosticMessage, arg0?: any): T { - if (reportAtCurrentPosition) { - parseErrorAtPosition(scanner.getStartPos(), 0, diagnosticMessage, arg0); - } - else if (diagnosticMessage) { - parseErrorAtCurrentToken(diagnosticMessage, arg0); - } - - const pos = getNodePos(); - const result = kind === ts.SyntaxKind.Identifier ? factory.createIdentifier("", /*typeArguments*/ undefined, /*originalKeywordKind*/ undefined) : - ts.isTemplateLiteralKind(kind) ? factory.createTemplateLiteralLikeNode(kind, "", "", /*templateFlags*/ undefined) : - kind === ts.SyntaxKind.NumericLiteral ? factory.createNumericLiteral("", /*numericLiteralFlags*/ undefined) : - kind === ts.SyntaxKind.StringLiteral ? factory.createStringLiteral("", /*isSingleQuote*/ undefined) : - kind === ts.SyntaxKind.MissingDeclaration ? factory.createMissingDeclaration() : - factory.createToken(kind); - return finishNode(result, pos) as T; + function parseOptional(t: ts.SyntaxKind): boolean { + if (token() === t) { + nextToken(); + return true; } + return false; + } - function internIdentifier(text: string): string { - let identifier = identifiers.get(text); - if (identifier === undefined) { - identifiers.set(text, identifier = text); - } - return identifier; + function parseOptionalToken(t: TKind): ts.Token; + function parseOptionalToken(t: ts.SyntaxKind): ts.Node | undefined { + if (token() === t) { + return parseTokenNode(); } + return undefined; + } - // 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?: ts.DiagnosticMessage, privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { - if (isIdentifier) { - identifierCount++; - const pos = getNodePos(); - // Store original token kind if it is not just an Identifier so we can report appropriate error later in type checker - const originalKeywordKind = token(); - const text = internIdentifier(scanner.getTokenValue()); - nextTokenWithoutCheck(); - return finishNode(factory.createIdentifier(text, /*typeArguments*/ undefined, originalKeywordKind), pos); - } + function parseOptionalTokenJSDoc(t: TKind): ts.Token; + function parseOptionalTokenJSDoc(t: ts.JSDocSyntaxKind): ts.Node | undefined { + if (token() === t) { + return parseTokenNodeJSDoc(); + } + return undefined; + } - if (token() === ts.SyntaxKind.PrivateIdentifier) { - parseErrorAtCurrentToken(privateIdentifierDiagnosticMessage || ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - return createIdentifier(/*isIdentifier*/ true); - } + function parseExpectedToken(t: TKind, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): ts.Token; + function parseExpectedToken(t: ts.SyntaxKind, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): ts.Node { + return parseOptionalToken(t) || + createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || ts.Diagnostics._0_expected, arg0 || ts.tokenToString(t)); + } - if (token() === ts.SyntaxKind.Unknown && scanner.tryScan(() => scanner.reScanInvalidIdentifier() === ts.SyntaxKind.Identifier)) { - // Scanner has already recorded an 'Invalid character' error, so no need to add another from the parser. - return createIdentifier(/*isIdentifier*/ true); - } + function parseExpectedTokenJSDoc(t: TKind): ts.Token; + function parseExpectedTokenJSDoc(t: ts.JSDocSyntaxKind): ts.Node { + return parseOptionalTokenJSDoc(t) || + createMissingNode(t, /*reportAtCurrentPosition*/ false, ts.Diagnostics._0_expected, ts.tokenToString(t)); + } - identifierCount++; - // Only for end of file because the error gets reported incorrectly on embedded script tags. - const reportAtCurrentPosition = token() === ts.SyntaxKind.EndOfFileToken; + function parseTokenNode(): T { + const pos = getNodePos(); + const kind = token(); + nextToken(); + return finishNode(factory.createToken(kind), pos) as T; + } - const isReservedWord = scanner.isReservedWord(); - const msgArg = scanner.getTokenText(); + function parseTokenNodeJSDoc(): T { + const pos = getNodePos(); + const kind = token(); + nextTokenJSDoc(); + return finishNode(factory.createToken(kind), pos) as T; + } - const defaultMessage = isReservedWord ? - ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here : - ts.Diagnostics.Identifier_expected; - return createMissingNode(ts.SyntaxKind.Identifier, reportAtCurrentPosition, diagnosticMessage || defaultMessage, msgArg); + function canParseSemicolon() { + // If there's a real semicolon, then we can always parse it out. + if (token() === ts.SyntaxKind.SemicolonToken) { + return true; } - function parseBindingIdentifier(privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage) { - return createIdentifier(isBindingIdentifier(), /*diagnosticMessage*/ undefined, privateIdentifierDiagnosticMessage); - } + // We can parse out an optional semicolon in ASI cases in the following cases. + return token() === ts.SyntaxKind.CloseBraceToken || token() === ts.SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak(); + } - function parseIdentifier(diagnosticMessage?: ts.DiagnosticMessage, privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { - return createIdentifier(isIdentifier(), diagnosticMessage, privateIdentifierDiagnosticMessage); + function tryParseSemicolon() { + if (!canParseSemicolon()) { + return false; } - function parseIdentifierName(diagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { - return createIdentifier(ts.tokenIsIdentifierOrKeyword(token()), diagnosticMessage); + if (token() === ts.SyntaxKind.SemicolonToken) { + // consume the semicolon if it was explicitly provided. + nextToken(); } - function isLiteralPropertyName(): boolean { - return ts.tokenIsIdentifierOrKeyword(token()) || - token() === ts.SyntaxKind.StringLiteral || - token() === ts.SyntaxKind.NumericLiteral; - } + return true; + } - function isAssertionKey(): boolean { - return ts.tokenIsIdentifierOrKeyword(token()) || - token() === ts.SyntaxKind.StringLiteral; - } + function parseSemicolon(): boolean { + return tryParseSemicolon() || parseExpected(ts.SyntaxKind.SemicolonToken); + } - function parsePropertyNameWorker(allowComputedPropertyNames: boolean): ts.PropertyName { - if (token() === ts.SyntaxKind.StringLiteral || token() === ts.SyntaxKind.NumericLiteral) { - const node = parseLiteralNode() as ts.StringLiteral | ts.NumericLiteral; - node.text = internIdentifier(node.text); - return node; - } - if (allowComputedPropertyNames && token() === ts.SyntaxKind.OpenBracketToken) { - return parseComputedPropertyName(); - } - if (token() === ts.SyntaxKind.PrivateIdentifier) { - return parsePrivateIdentifier(); - } - return parseIdentifierName(); - } + function createNodeArray(elements: T[], pos: number, end?: number, hasTrailingComma?: boolean): ts.NodeArray { + const array = factory.createNodeArray(elements, hasTrailingComma); + ts.setTextRangePosEnd(array, pos, end ?? scanner.getStartPos()); + return array; + } - function parsePropertyName(): ts.PropertyName { - return parsePropertyNameWorker(/*allowComputedPropertyNames*/ true); + function finishNode(node: T, pos: number, end?: number): T { + ts.setTextRangePosEnd(node, pos, end ?? scanner.getStartPos()); + if (contextFlags) { + (node as ts.Mutable).flags |= contextFlags; } - function parseComputedPropertyName(): ts.ComputedPropertyName { - // PropertyName [Yield]: - // LiteralPropertyName - // ComputedPropertyName[?Yield] - const pos = getNodePos(); - parseExpected(ts.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. - const expression = allowInAnd(parseExpression); - parseExpected(ts.SyntaxKind.CloseBracketToken); - return finishNode(factory.createComputedPropertyName(expression), pos); + // 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 as ts.Mutable).flags |= ts.NodeFlags.ThisNodeHasError; } - function internPrivateIdentifier(text: string): string { - let privateIdentifier = privateIdentifiers.get(text); - if (privateIdentifier === undefined) { - privateIdentifiers.set(text, privateIdentifier = text); - } - return privateIdentifier; - } + return node; + } - function parsePrivateIdentifier(): ts.PrivateIdentifier { - const pos = getNodePos(); - const node = factory.createPrivateIdentifier(internPrivateIdentifier(scanner.getTokenText())); - nextToken(); - return finishNode(node, pos); + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: false, diagnosticMessage?: ts.DiagnosticMessage, arg0?: any): T; + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: ts.DiagnosticMessage, arg0?: any): T; + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: ts.DiagnosticMessage, arg0?: any): T { + if (reportAtCurrentPosition) { + parseErrorAtPosition(scanner.getStartPos(), 0, diagnosticMessage, arg0); } - - function parseContextualModifier(t: ts.SyntaxKind): boolean { - return token() === t && tryParse(nextTokenCanFollowModifier); + else if (diagnosticMessage) { + parseErrorAtCurrentToken(diagnosticMessage, arg0); } - function nextTokenIsOnSameLineAndCanFollowModifier() { - nextToken(); - if (scanner.hasPrecedingLineBreak()) { - return false; - } - return canFollowModifier(); - } + const pos = getNodePos(); + const result = kind === ts.SyntaxKind.Identifier ? factory.createIdentifier("", /*typeArguments*/ undefined, /*originalKeywordKind*/ undefined) : + ts.isTemplateLiteralKind(kind) ? factory.createTemplateLiteralLikeNode(kind, "", "", /*templateFlags*/ undefined) : + kind === ts.SyntaxKind.NumericLiteral ? factory.createNumericLiteral("", /*numericLiteralFlags*/ undefined) : + kind === ts.SyntaxKind.StringLiteral ? factory.createStringLiteral("", /*isSingleQuote*/ undefined) : + kind === ts.SyntaxKind.MissingDeclaration ? factory.createMissingDeclaration() : + factory.createToken(kind); + return finishNode(result, pos) as T; + } - function nextTokenCanFollowModifier() { - switch (token()) { - case ts.SyntaxKind.ConstKeyword: - // 'const' is only a modifier if followed by 'enum'. - return nextToken() === ts.SyntaxKind.EnumKeyword; - case ts.SyntaxKind.ExportKeyword: - nextToken(); - if (token() === ts.SyntaxKind.DefaultKeyword) { - return lookAhead(nextTokenCanFollowDefaultKeyword); - } - if (token() === ts.SyntaxKind.TypeKeyword) { - return lookAhead(nextTokenCanFollowExportModifier); - } - return canFollowExportModifier(); - case ts.SyntaxKind.DefaultKeyword: - return nextTokenCanFollowDefaultKeyword(); - case ts.SyntaxKind.StaticKeyword: - case ts.SyntaxKind.GetKeyword: - case ts.SyntaxKind.SetKeyword: - nextToken(); - return canFollowModifier(); - default: - return nextTokenIsOnSameLineAndCanFollowModifier(); - } + function internIdentifier(text: string): string { + let identifier = identifiers.get(text); + if (identifier === undefined) { + identifiers.set(text, identifier = text); } + return identifier; + } - function canFollowExportModifier(): boolean { - return token() !== ts.SyntaxKind.AsteriskToken - && token() !== ts.SyntaxKind.AsKeyword - && token() !== ts.SyntaxKind.OpenBraceToken - && canFollowModifier(); + // 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?: ts.DiagnosticMessage, privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { + if (isIdentifier) { + identifierCount++; + const pos = getNodePos(); + // Store original token kind if it is not just an Identifier so we can report appropriate error later in type checker + const originalKeywordKind = token(); + const text = internIdentifier(scanner.getTokenValue()); + nextTokenWithoutCheck(); + return finishNode(factory.createIdentifier(text, /*typeArguments*/ undefined, originalKeywordKind), pos); } - function nextTokenCanFollowExportModifier(): boolean { - nextToken(); - return canFollowExportModifier(); + if (token() === ts.SyntaxKind.PrivateIdentifier) { + parseErrorAtCurrentToken(privateIdentifierDiagnosticMessage || ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + return createIdentifier(/*isIdentifier*/ true); } - function parseAnyContextualModifier(): boolean { - return ts.isModifierKind(token()) && tryParse(nextTokenCanFollowModifier); + if (token() === ts.SyntaxKind.Unknown && scanner.tryScan(() => scanner.reScanInvalidIdentifier() === ts.SyntaxKind.Identifier)) { + // Scanner has already recorded an 'Invalid character' error, so no need to add another from the parser. + return createIdentifier(/*isIdentifier*/ true); } - function canFollowModifier(): boolean { - return token() === ts.SyntaxKind.OpenBracketToken - || token() === ts.SyntaxKind.OpenBraceToken - || token() === ts.SyntaxKind.AsteriskToken - || token() === ts.SyntaxKind.DotDotDotToken - || isLiteralPropertyName(); - } + identifierCount++; + // Only for end of file because the error gets reported incorrectly on embedded script tags. + const reportAtCurrentPosition = token() === ts.SyntaxKind.EndOfFileToken; - function nextTokenCanFollowDefaultKeyword(): boolean { - nextToken(); - return token() === ts.SyntaxKind.ClassKeyword || token() === ts.SyntaxKind.FunctionKeyword || - token() === ts.SyntaxKind.InterfaceKeyword || - (token() === ts.SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsClassKeywordOnSameLine)) || - (token() === ts.SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsFunctionKeywordOnSameLine)); - } + const isReservedWord = scanner.isReservedWord(); + const msgArg = scanner.getTokenText(); - // 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; - } + const defaultMessage = isReservedWord ? + ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here : + ts.Diagnostics.Identifier_expected; + return createMissingNode(ts.SyntaxKind.Identifier, reportAtCurrentPosition, diagnosticMessage || defaultMessage, msgArg); + } - 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() === ts.SyntaxKind.SemicolonToken && inErrorRecovery) && isStartOfStatement(); - case ParsingContext.SwitchClauses: - return token() === ts.SyntaxKind.CaseKeyword || token() === ts.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() === ts.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() === ts.SyntaxKind.OpenBracketToken || isLiteralPropertyName(); - case ParsingContext.ObjectLiteralMembers: - switch (token()) { - case ts.SyntaxKind.OpenBracketToken: - case ts.SyntaxKind.AsteriskToken: - case ts.SyntaxKind.DotDotDotToken: - case ts.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() === ts.SyntaxKind.OpenBracketToken || token() === ts.SyntaxKind.DotDotDotToken || isLiteralPropertyName(); - case ParsingContext.AssertEntries: - return isAssertionKey(); - 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() === ts.SyntaxKind.OpenBraceToken) { - return lookAhead(isValidHeritageClauseObjectLiteral); - } + function parseBindingIdentifier(privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage) { + return createIdentifier(isBindingIdentifier(), /*diagnosticMessage*/ undefined, privateIdentifierDiagnosticMessage); + } - 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 isBindingIdentifierOrPrivateIdentifierOrPattern(); - case ParsingContext.ArrayBindingElements: - return token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.DotDotDotToken || isBindingIdentifierOrPrivateIdentifierOrPattern(); - case ParsingContext.TypeParameters: - return token() === ts.SyntaxKind.InKeyword || isIdentifier(); - case ParsingContext.ArrayLiteralMembers: - switch (token()) { - case ts.SyntaxKind.CommaToken: - case ts.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() === ts.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() === ts.SyntaxKind.CommaToken || isStartOfType(); - case ParsingContext.HeritageClauses: - return isHeritageClause(); - case ParsingContext.ImportOrExportSpecifiers: - return ts.tokenIsIdentifierOrKeyword(token()); - case ParsingContext.JsxAttributes: - return ts.tokenIsIdentifierOrKeyword(token()) || token() === ts.SyntaxKind.OpenBraceToken; - case ParsingContext.JsxChildren: - return true; - } + function parseIdentifier(diagnosticMessage?: ts.DiagnosticMessage, privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { + return createIdentifier(isIdentifier(), diagnosticMessage, privateIdentifierDiagnosticMessage); + } - return ts.Debug.fail("Non-exhaustive case in 'isListElement'."); - } + function parseIdentifierName(diagnosticMessage?: ts.DiagnosticMessage): ts.Identifier { + return createIdentifier(ts.tokenIsIdentifierOrKeyword(token()), diagnosticMessage); + } - function isValidHeritageClauseObjectLiteral() { - ts.Debug.assert(token() === ts.SyntaxKind.OpenBraceToken); - if (nextToken() === ts.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 + function isLiteralPropertyName(): boolean { + return ts.tokenIsIdentifierOrKeyword(token()) || + token() === ts.SyntaxKind.StringLiteral || + token() === ts.SyntaxKind.NumericLiteral; + } - const next = nextToken(); - return next === ts.SyntaxKind.CommaToken || next === ts.SyntaxKind.OpenBraceToken || next === ts.SyntaxKind.ExtendsKeyword || next === ts.SyntaxKind.ImplementsKeyword; - } + function isAssertionKey(): boolean { + return ts.tokenIsIdentifierOrKeyword(token()) || + token() === ts.SyntaxKind.StringLiteral; + } - return true; + function parsePropertyNameWorker(allowComputedPropertyNames: boolean): ts.PropertyName { + if (token() === ts.SyntaxKind.StringLiteral || token() === ts.SyntaxKind.NumericLiteral) { + const node = parseLiteralNode() as ts.StringLiteral | ts.NumericLiteral; + node.text = internIdentifier(node.text); + return node; } - - function nextTokenIsIdentifier() { - nextToken(); - return isIdentifier(); + if (allowComputedPropertyNames && token() === ts.SyntaxKind.OpenBracketToken) { + return parseComputedPropertyName(); } - - function nextTokenIsIdentifierOrKeyword() { - nextToken(); - return ts.tokenIsIdentifierOrKeyword(token()); + if (token() === ts.SyntaxKind.PrivateIdentifier) { + return parsePrivateIdentifier(); } + return parseIdentifierName(); + } - function nextTokenIsIdentifierOrKeywordOrGreaterThan() { - nextToken(); - return ts.tokenIsIdentifierOrKeywordOrGreaterThan(token()); + function parsePropertyName(): ts.PropertyName { + return parsePropertyNameWorker(/*allowComputedPropertyNames*/ true); + } + + function parseComputedPropertyName(): ts.ComputedPropertyName { + // PropertyName [Yield]: + // LiteralPropertyName + // ComputedPropertyName[?Yield] + const pos = getNodePos(); + parseExpected(ts.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. + const expression = allowInAnd(parseExpression); + parseExpected(ts.SyntaxKind.CloseBracketToken); + return finishNode(factory.createComputedPropertyName(expression), pos); + } + + function internPrivateIdentifier(text: string): string { + let privateIdentifier = privateIdentifiers.get(text); + if (privateIdentifier === undefined) { + privateIdentifiers.set(text, privateIdentifier = text); } + return privateIdentifier; + } - function isHeritageClauseExtendsOrImplementsKeyword(): boolean { - if (token() === ts.SyntaxKind.ImplementsKeyword || - token() === ts.SyntaxKind.ExtendsKeyword) { + function parsePrivateIdentifier(): ts.PrivateIdentifier { + const pos = getNodePos(); + const node = factory.createPrivateIdentifier(internPrivateIdentifier(scanner.getTokenText())); + nextToken(); + return finishNode(node, pos); + } - return lookAhead(nextTokenIsStartOfExpression); - } + function parseContextualModifier(t: ts.SyntaxKind): boolean { + return token() === t && tryParse(nextTokenCanFollowModifier); + } + function nextTokenIsOnSameLineAndCanFollowModifier() { + nextToken(); + if (scanner.hasPrecedingLineBreak()) { return false; } + return canFollowModifier(); + } - function nextTokenIsStartOfExpression() { - nextToken(); - return isStartOfExpression(); - } - - function nextTokenIsStartOfType() { - nextToken(); - return isStartOfType(); + function nextTokenCanFollowModifier() { + switch (token()) { + case ts.SyntaxKind.ConstKeyword: + // 'const' is only a modifier if followed by 'enum'. + return nextToken() === ts.SyntaxKind.EnumKeyword; + case ts.SyntaxKind.ExportKeyword: + nextToken(); + if (token() === ts.SyntaxKind.DefaultKeyword) { + return lookAhead(nextTokenCanFollowDefaultKeyword); + } + if (token() === ts.SyntaxKind.TypeKeyword) { + return lookAhead(nextTokenCanFollowExportModifier); + } + return canFollowExportModifier(); + case ts.SyntaxKind.DefaultKeyword: + return nextTokenCanFollowDefaultKeyword(); + case ts.SyntaxKind.StaticKeyword: + case ts.SyntaxKind.GetKeyword: + case ts.SyntaxKind.SetKeyword: + nextToken(); + return canFollowModifier(); + default: + return nextTokenIsOnSameLineAndCanFollowModifier(); } + } - // True if positioned at a list terminator - function isListTerminator(kind: ParsingContext): boolean { - if (token() === ts.SyntaxKind.EndOfFileToken) { - // Being at the end of the file ends all lists. - return true; - } + function canFollowExportModifier(): boolean { + return token() !== ts.SyntaxKind.AsteriskToken + && token() !== ts.SyntaxKind.AsKeyword + && token() !== ts.SyntaxKind.OpenBraceToken + && canFollowModifier(); + } - 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: - case ParsingContext.AssertEntries: - return token() === ts.SyntaxKind.CloseBraceToken; - case ParsingContext.SwitchClauseStatements: - return token() === ts.SyntaxKind.CloseBraceToken || token() === ts.SyntaxKind.CaseKeyword || token() === ts.SyntaxKind.DefaultKeyword; - case ParsingContext.HeritageClauseElement: - return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; - case ParsingContext.VariableDeclarations: - return isVariableDeclaratorListTerminator(); - case ParsingContext.TypeParameters: - // Tokens other than '>' are here for better error recovery - return token() === ts.SyntaxKind.GreaterThanToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; - case ParsingContext.ArgumentExpressions: - // Tokens other than ')' are here for better error recovery - return token() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.SemicolonToken; - case ParsingContext.ArrayLiteralMembers: - case ParsingContext.TupleElementTypes: - case ParsingContext.ArrayBindingElements: - return token() === ts.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() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.CloseBracketToken /*|| token === SyntaxKind.OpenBraceToken*/; - case ParsingContext.TypeArguments: - // All other tokens should cause the type-argument to terminate except comma token - return token() !== ts.SyntaxKind.CommaToken; - case ParsingContext.HeritageClauses: - return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.CloseBraceToken; - case ParsingContext.JsxAttributes: - return token() === ts.SyntaxKind.GreaterThanToken || token() === ts.SyntaxKind.SlashToken; - case ParsingContext.JsxChildren: - return token() === ts.SyntaxKind.LessThanToken && lookAhead(nextTokenIsSlash); - default: - return false; - } - } + function nextTokenCanFollowExportModifier(): boolean { + nextToken(); + return canFollowExportModifier(); + } - 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 parseAnyContextualModifier(): boolean { + return ts.isModifierKind(token()) && tryParse(nextTokenCanFollowModifier); + } - // 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; - } + function canFollowModifier(): boolean { + return token() === ts.SyntaxKind.OpenBracketToken + || token() === ts.SyntaxKind.OpenBraceToken + || token() === ts.SyntaxKind.AsteriskToken + || token() === ts.SyntaxKind.DotDotDotToken + || isLiteralPropertyName(); + } - // 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() === ts.SyntaxKind.EqualsGreaterThanToken) { - return true; - } + function nextTokenCanFollowDefaultKeyword(): boolean { + nextToken(); + return token() === ts.SyntaxKind.ClassKeyword || token() === ts.SyntaxKind.FunctionKeyword || + token() === ts.SyntaxKind.InterfaceKeyword || + (token() === ts.SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsClassKeywordOnSameLine)) || + (token() === ts.SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsFunctionKeywordOnSameLine)); + } - // Keep trying to parse out variable declarators. - return false; + // 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; } - // 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)) { + 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() === ts.SyntaxKind.SemicolonToken && inErrorRecovery) && isStartOfStatement(); + case ParsingContext.SwitchClauses: + return token() === ts.SyntaxKind.CaseKeyword || token() === ts.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() === ts.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() === ts.SyntaxKind.OpenBracketToken || isLiteralPropertyName(); + case ParsingContext.ObjectLiteralMembers: + switch (token()) { + case ts.SyntaxKind.OpenBracketToken: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.DotDotDotToken: + case ts.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() === ts.SyntaxKind.OpenBracketToken || token() === ts.SyntaxKind.DotDotDotToken || isLiteralPropertyName(); + case ParsingContext.AssertEntries: + return isAssertionKey(); + 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() === ts.SyntaxKind.OpenBraceToken) { + return lookAhead(isValidHeritageClauseObjectLiteral); } - } - return false; + 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 isBindingIdentifierOrPrivateIdentifierOrPattern(); + case ParsingContext.ArrayBindingElements: + return token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.DotDotDotToken || isBindingIdentifierOrPrivateIdentifierOrPattern(); + case ParsingContext.TypeParameters: + return token() === ts.SyntaxKind.InKeyword || isIdentifier(); + case ParsingContext.ArrayLiteralMembers: + switch (token()) { + case ts.SyntaxKind.CommaToken: + case ts.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() === ts.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() === ts.SyntaxKind.CommaToken || isStartOfType(); + case ParsingContext.HeritageClauses: + return isHeritageClause(); + case ParsingContext.ImportOrExportSpecifiers: + return ts.tokenIsIdentifierOrKeyword(token()); + case ParsingContext.JsxAttributes: + return ts.tokenIsIdentifierOrKeyword(token()) || token() === ts.SyntaxKind.OpenBraceToken; + case ParsingContext.JsxChildren: + return true; } - // Parses a list of elements - function parseList(kind: ParsingContext, parseElement: () => T): ts.NodeArray { - const saveParsingContext = parsingContext; - parsingContext |= 1 << kind; - const list = []; - const listPos = getNodePos(); + return ts.Debug.fail("Non-exhaustive case in 'isListElement'."); + } - while (!isListTerminator(kind)) { - if (isListElement(kind, /*inErrorRecovery*/ false)) { - list.push(parseListElement(kind, parseElement)); + function isValidHeritageClauseObjectLiteral() { + ts.Debug.assert(token() === ts.SyntaxKind.OpenBraceToken); + if (nextToken() === ts.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 - continue; - } + const next = nextToken(); + return next === ts.SyntaxKind.CommaToken || next === ts.SyntaxKind.OpenBraceToken || next === ts.SyntaxKind.ExtendsKeyword || next === ts.SyntaxKind.ImplementsKeyword; + } - if (abortParsingListOrMoveToNextToken(kind)) { - break; - } - } + return true; + } - parsingContext = saveParsingContext; - return createNodeArray(list, listPos); - } + function nextTokenIsIdentifier() { + nextToken(); + return isIdentifier(); + } - function parseListElement(parsingContext: ParsingContext, parseElement: () => T): T { - const node = currentNode(parsingContext); - if (node) { - return consumeNode(node) as T; - } + function nextTokenIsIdentifierOrKeyword() { + nextToken(); + return ts.tokenIsIdentifierOrKeyword(token()); + } + + function nextTokenIsIdentifierOrKeywordOrGreaterThan() { + nextToken(); + return ts.tokenIsIdentifierOrKeywordOrGreaterThan(token()); + } + + function isHeritageClauseExtendsOrImplementsKeyword(): boolean { + if (token() === ts.SyntaxKind.ImplementsKeyword || + token() === ts.SyntaxKind.ExtendsKeyword) { - return parseElement(); + return lookAhead(nextTokenIsStartOfExpression); } - function currentNode(parsingContext: ParsingContext): ts.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; - } + return false; + } - const node = syntaxCursor.currentNode(scanner.getStartPos()); + function nextTokenIsStartOfExpression() { + nextToken(); + return isStartOfExpression(); + } - // 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 (ts.nodeIsMissing(node) || node.intersectsChange || ts.containsParseError(node)) { - return undefined; - } + function nextTokenIsStartOfType() { + nextToken(); + return isStartOfType(); + } - // 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 & ts.NodeFlags.ContextFlags; - if (nodeContextFlags !== contextFlags) { - return undefined; - } + // True if positioned at a list terminator + function isListTerminator(kind: ParsingContext): boolean { + if (token() === ts.SyntaxKind.EndOfFileToken) { + // Being at the end of the file ends all lists. + return true; + } - // 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; - } + 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: + case ParsingContext.AssertEntries: + return token() === ts.SyntaxKind.CloseBraceToken; + case ParsingContext.SwitchClauseStatements: + return token() === ts.SyntaxKind.CloseBraceToken || token() === ts.SyntaxKind.CaseKeyword || token() === ts.SyntaxKind.DefaultKeyword; + case ParsingContext.HeritageClauseElement: + return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; + case ParsingContext.VariableDeclarations: + return isVariableDeclaratorListTerminator(); + case ParsingContext.TypeParameters: + // Tokens other than '>' are here for better error recovery + return token() === ts.SyntaxKind.GreaterThanToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; + case ParsingContext.ArgumentExpressions: + // Tokens other than ')' are here for better error recovery + return token() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.SemicolonToken; + case ParsingContext.ArrayLiteralMembers: + case ParsingContext.TupleElementTypes: + case ParsingContext.ArrayBindingElements: + return token() === ts.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() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.CloseBracketToken /*|| token === SyntaxKind.OpenBraceToken*/; + case ParsingContext.TypeArguments: + // All other tokens should cause the type-argument to terminate except comma token + return token() !== ts.SyntaxKind.CommaToken; + case ParsingContext.HeritageClauses: + return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.CloseBraceToken; + case ParsingContext.JsxAttributes: + return token() === ts.SyntaxKind.GreaterThanToken || token() === ts.SyntaxKind.SlashToken; + case ParsingContext.JsxChildren: + return token() === ts.SyntaxKind.LessThanToken && lookAhead(nextTokenIsSlash); + default: + return false; + } + } - if ((node as ts.JSDocContainer).jsDocCache) { - // jsDocCache may include tags from parent nodes, which might have been modified. - (node as ts.JSDocContainer).jsDocCache = undefined; - } + 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; + } - return node; + // 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; } - function consumeNode(node: ts.Node) { - // Move the scanner so it is after the node we just consumed. - scanner.setTextPos(node.end); - nextToken(); - return node; + // 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() === ts.SyntaxKind.EqualsGreaterThanToken) { + 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: + // 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; } - function canReuseNode(node: ts.Node, parsingContext: ParsingContext): boolean { - switch (parsingContext) { - case ParsingContext.ClassMembers: - return isReusableClassMember(node); - - case ParsingContext.SwitchClauses: - return isReusableSwitchClause(node); + return false; + } - case ParsingContext.SourceElements: - case ParsingContext.BlockStatements: - case ParsingContext.SwitchClauseStatements: - return isReusableStatement(node); + // Parses a list of elements + function parseList(kind: ParsingContext, parseElement: () => T): ts.NodeArray { + const saveParsingContext = parsingContext; + parsingContext |= 1 << kind; + const list = []; + const listPos = getNodePos(); - case ParsingContext.EnumMembers: - return isReusableEnumMember(node); + while (!isListTerminator(kind)) { + if (isListElement(kind, /*inErrorRecovery*/ false)) { + list.push(parseListElement(kind, parseElement)); - case ParsingContext.TypeMembers: - return isReusableTypeMember(node); + continue; + } - case ParsingContext.VariableDeclarations: - return isReusableVariableDeclaration(node); + if (abortParsingListOrMoveToNextToken(kind)) { + break; + } + } - case ParsingContext.JSDocParameters: - case ParsingContext.Parameters: - return isReusableParameter(node); + parsingContext = saveParsingContext; + return createNodeArray(list, listPos); + } - // 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). + function parseListElement(parsingContext: ParsingContext, parseElement: () => T): T { + const node = currentNode(parsingContext); + if (node) { + return consumeNode(node) as T; + } - // case ParsingContext.HeritageClauses: - // This would probably be safe to reuse. There is no speculative parsing with - // heritage clauses. + return parseElement(); + } - // 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. + function currentNode(parsingContext: ParsingContext): ts.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; + } - // case ParsingContext.TupleElementTypes: - // This would probably be safe to reuse. There is no speculative parsing with - // tuple types. + const node = syntaxCursor.currentNode(scanner.getStartPos()); - // 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: + // 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 (ts.nodeIsMissing(node) || node.intersectsChange || ts.containsParseError(node)) { + return undefined; + } - // 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: + // 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 & ts.NodeFlags.ContextFlags; + if (nodeContextFlags !== contextFlags) { + return undefined; + } - // 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: + // 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; + } - // 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: + if ((node as ts.JSDocContainer).jsDocCache) { + // jsDocCache may include tags from parent nodes, which might have been modified. + (node as ts.JSDocContainer).jsDocCache = undefined; + } - // 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 node; + } - } + function consumeNode(node: ts.Node) { + // Move the scanner so it is after the node we just consumed. + scanner.setTextPos(node.end); + nextToken(); + return node; + } - return false; + 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; } + return false; + } - function isReusableClassMember(node: ts.Node) { - if (node) { - switch (node.kind) { - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.SemicolonClassElement: - return true; - case ts.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 as ts.MethodDeclaration; - const nameIsConstructor = methodDeclaration.name.kind === ts.SyntaxKind.Identifier && - methodDeclaration.name.originalKeywordKind === ts.SyntaxKind.ConstructorKeyword; + function canReuseNode(node: ts.Node, parsingContext: ParsingContext): boolean { + switch (parsingContext) { + case ParsingContext.ClassMembers: + return isReusableClassMember(node); - return !nameIsConstructor; - } - } + case ParsingContext.SwitchClauses: + return isReusableSwitchClause(node); - return false; - } + case ParsingContext.SourceElements: + case ParsingContext.BlockStatements: + case ParsingContext.SwitchClauseStatements: + return isReusableStatement(node); - function isReusableSwitchClause(node: ts.Node) { - if (node) { - switch (node.kind) { - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: - return true; - } - } + case ParsingContext.EnumMembers: + return isReusableEnumMember(node); - return false; - } + case ParsingContext.TypeMembers: + return isReusableTypeMember(node); - function isReusableStatement(node: ts.Node) { - if (node) { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.ExpressionStatement: - case ts.SyntaxKind.ThrowStatement: - case ts.SyntaxKind.ReturnStatement: - case ts.SyntaxKind.SwitchStatement: - case ts.SyntaxKind.BreakStatement: - case ts.SyntaxKind.ContinueStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.WithStatement: - case ts.SyntaxKind.EmptyStatement: - case ts.SyntaxKind.TryStatement: - case ts.SyntaxKind.LabeledStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.DebuggerStatement: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - return true; - } - } + case ParsingContext.VariableDeclarations: + return isReusableVariableDeclaration(node); - return false; - } + 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: - function isReusableEnumMember(node: ts.Node) { - return node.kind === ts.SyntaxKind.EnumMember; } - function isReusableTypeMember(node: ts.Node) { - if (node) { - switch (node.kind) { - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.CallSignature: - return true; - } - } + return false; + } - return false; + function isReusableClassMember(node: ts.Node) { + if (node) { + switch (node.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.SemicolonClassElement: + return true; + case ts.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 as ts.MethodDeclaration; + const nameIsConstructor = methodDeclaration.name.kind === ts.SyntaxKind.Identifier && + methodDeclaration.name.originalKeywordKind === ts.SyntaxKind.ConstructorKeyword; + + return !nameIsConstructor; + } } - function isReusableVariableDeclaration(node: ts.Node) { - if (node.kind !== ts.SyntaxKind.VariableDeclaration) { - return false; + return false; + } + + function isReusableSwitchClause(node: ts.Node) { + if (node) { + switch (node.kind) { + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.DefaultClause: + 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 List() + // + // then we have a problem. "v = new List(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray; - function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray> | undefined; - function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray> | undefined { - 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(); - const result = parseListElement(kind, parseElement); - if (!result) { - parsingContext = saveParsingContext; - return undefined; - } - list.push(result as NonNullable); - commaStart = scanner.getTokenPos(); + // See the comment in isReusableVariableDeclaration for why we do this. + const parameter = node as ts.ParameterDeclaration; + return parameter.initializer === undefined; + } - if (parseOptional(ts.SyntaxKind.CommaToken)) { - // No need to check for a zero length node since we know we parsed a comma - continue; - } + // Returns true if we should abort parsing. + function abortParsingListOrMoveToNextToken(kind: ParsingContext) { + parsingContextErrors(kind); + if (isInSomeParsingContext()) { + return true; + } - commaStart = -1; // Back to the state where the last token was not a comma - if (isListTerminator(kind)) { - break; - } + nextToken(); + return false; + } - // 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(ts.SyntaxKind.CommaToken, getExpectedCommaDiagnostic(kind)); + function parsingContextErrors(context: ParsingContext) { + switch (context) { + case ParsingContext.SourceElements: + return token() === ts.SyntaxKind.DefaultKeyword + ? parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.ExportKeyword)) + : parseErrorAtCurrentToken(ts.Diagnostics.Declaration_or_statement_expected); + case ParsingContext.BlockStatements: return parseErrorAtCurrentToken(ts.Diagnostics.Declaration_or_statement_expected); + case ParsingContext.SwitchClauses: return parseErrorAtCurrentToken(ts.Diagnostics.case_or_default_expected); + case ParsingContext.SwitchClauseStatements: return parseErrorAtCurrentToken(ts.Diagnostics.Statement_expected); + case ParsingContext.RestProperties: // fallthrough + case ParsingContext.TypeMembers: return parseErrorAtCurrentToken(ts.Diagnostics.Property_or_signature_expected); + case ParsingContext.ClassMembers: return parseErrorAtCurrentToken(ts.Diagnostics.Unexpected_token_A_constructor_method_accessor_or_property_was_expected); + case ParsingContext.EnumMembers: return parseErrorAtCurrentToken(ts.Diagnostics.Enum_member_expected); + case ParsingContext.HeritageClauseElement: return parseErrorAtCurrentToken(ts.Diagnostics.Expression_expected); + case ParsingContext.VariableDeclarations: + return ts.isKeyword(token()) + ? parseErrorAtCurrentToken(ts.Diagnostics._0_is_not_allowed_as_a_variable_declaration_name, ts.tokenToString(token())) + : parseErrorAtCurrentToken(ts.Diagnostics.Variable_declaration_expected); + case ParsingContext.ObjectBindingElements: return parseErrorAtCurrentToken(ts.Diagnostics.Property_destructuring_pattern_expected); + case ParsingContext.ArrayBindingElements: return parseErrorAtCurrentToken(ts.Diagnostics.Array_element_destructuring_pattern_expected); + case ParsingContext.ArgumentExpressions: return parseErrorAtCurrentToken(ts.Diagnostics.Argument_expression_expected); + case ParsingContext.ObjectLiteralMembers: return parseErrorAtCurrentToken(ts.Diagnostics.Property_assignment_expected); + case ParsingContext.ArrayLiteralMembers: return parseErrorAtCurrentToken(ts.Diagnostics.Expression_or_comma_expected); + case ParsingContext.JSDocParameters: return parseErrorAtCurrentToken(ts.Diagnostics.Parameter_declaration_expected); + case ParsingContext.Parameters: + return ts.isKeyword(token()) + ? parseErrorAtCurrentToken(ts.Diagnostics._0_is_not_allowed_as_a_parameter_name, ts.tokenToString(token())) + : parseErrorAtCurrentToken(ts.Diagnostics.Parameter_declaration_expected); + case ParsingContext.TypeParameters: return parseErrorAtCurrentToken(ts.Diagnostics.Type_parameter_declaration_expected); + case ParsingContext.TypeArguments: return parseErrorAtCurrentToken(ts.Diagnostics.Type_argument_expected); + case ParsingContext.TupleElementTypes: return parseErrorAtCurrentToken(ts.Diagnostics.Type_expected); + case ParsingContext.HeritageClauses: return parseErrorAtCurrentToken(ts.Diagnostics.Unexpected_token_expected); + case ParsingContext.ImportOrExportSpecifiers: return parseErrorAtCurrentToken(ts.Diagnostics.Identifier_expected); + case ParsingContext.JsxAttributes: return parseErrorAtCurrentToken(ts.Diagnostics.Identifier_expected); + case ParsingContext.JsxChildren: return parseErrorAtCurrentToken(ts.Diagnostics.Identifier_expected); + default: return [undefined!]; // TODO: GH#18217 `default: Debug.assertNever(context);` + } + } - // 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() === ts.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(); - } + // Parses a comma-delimited list of elements + function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray; + function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray> | undefined; + function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): ts.NodeArray> | undefined { + 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(); + const result = parseListElement(kind, parseElement); + if (!result) { + parsingContext = saveParsingContext; + return undefined; + } + list.push(result as NonNullable); + commaStart = scanner.getTokenPos(); + + if (parseOptional(ts.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; } - if (abortParsingListOrMoveToNextToken(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(ts.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() === ts.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; } - parsingContext = saveParsingContext; - // 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. - // Always preserve a trailing comma by marking it on the NodeArray - return createNodeArray(list, listPos, /*end*/ undefined, commaStart >= 0); - } + if (isListTerminator(kind)) { + break; + } - function getExpectedCommaDiagnostic(kind: ParsingContext) { - return kind === ParsingContext.EnumMembers ? ts.Diagnostics.An_enum_member_name_must_be_followed_by_a_or : undefined; + if (abortParsingListOrMoveToNextToken(kind)) { + break; + } } - interface MissingList extends ts.NodeArray { - isMissingList: true; - } + parsingContext = saveParsingContext; + // 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. + // Always preserve a trailing comma by marking it on the NodeArray + return createNodeArray(list, listPos, /*end*/ undefined, commaStart >= 0); + } - function createMissingList(): MissingList { - const list = createNodeArray([], getNodePos()) as MissingList; - list.isMissingList = true; - return list; - } + function getExpectedCommaDiagnostic(kind: ParsingContext) { + return kind === ParsingContext.EnumMembers ? ts.Diagnostics.An_enum_member_name_must_be_followed_by_a_or : undefined; + } + + interface MissingList extends ts.NodeArray { + isMissingList: true; + } + + function createMissingList(): MissingList { + const list = createNodeArray([], getNodePos()) as MissingList; + list.isMissingList = true; + return list; + } + + function isMissingList(arr: ts.NodeArray): boolean { + return !!(arr as MissingList).isMissingList; + } - function isMissingList(arr: ts.NodeArray): boolean { - return !!(arr as MissingList).isMissingList; + function parseBracketedList(kind: ParsingContext, parseElement: () => T, open: ts.SyntaxKind, close: ts.SyntaxKind): ts.NodeArray { + if (parseExpected(open)) { + const result = parseDelimitedList(kind, parseElement); + parseExpected(close); + return result; } - function parseBracketedList(kind: ParsingContext, parseElement: () => T, open: ts.SyntaxKind, close: ts.SyntaxKind): ts.NodeArray { - if (parseExpected(open)) { - const result = parseDelimitedList(kind, parseElement); - parseExpected(close); - return result; - } + return createMissingList(); + } - return createMissingList(); + function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: ts.DiagnosticMessage): ts.EntityName { + const pos = getNodePos(); + let entity: ts.EntityName = allowReservedWords ? parseIdentifierName(diagnosticMessage) : parseIdentifier(diagnosticMessage); + let dotPos = getNodePos(); + while (parseOptional(ts.SyntaxKind.DotToken)) { + if (token() === ts.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 = getNodePos(); + entity = finishNode(factory.createQualifiedName(entity, parseRightSideOfDot(allowReservedWords, /* allowPrivateIdentifiers */ false) as ts.Identifier), pos); } + return entity; + } - function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: ts.DiagnosticMessage): ts.EntityName { - const pos = getNodePos(); - let entity: ts.EntityName = allowReservedWords ? parseIdentifierName(diagnosticMessage) : parseIdentifier(diagnosticMessage); - let dotPos = getNodePos(); - while (parseOptional(ts.SyntaxKind.DotToken)) { - if (token() === ts.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 = getNodePos(); - entity = finishNode(factory.createQualifiedName(entity, parseRightSideOfDot(allowReservedWords, /* allowPrivateIdentifiers */ false) as ts.Identifier), pos); + function createQualifiedName(entity: ts.EntityName, name: ts.Identifier): ts.QualifiedName { + return finishNode(factory.createQualifiedName(entity, name), entity.pos); + } + + function parseRightSideOfDot(allowIdentifierNames: boolean, allowPrivateIdentifiers: boolean): ts.Identifier | ts.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() && ts.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(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Identifier_expected); } - return entity; } - function createQualifiedName(entity: ts.EntityName, name: ts.Identifier): ts.QualifiedName { - return finishNode(factory.createQualifiedName(entity, name), entity.pos); + if (token() === ts.SyntaxKind.PrivateIdentifier) { + const node = parsePrivateIdentifier(); + return allowPrivateIdentifiers ? node : createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Identifier_expected); } - function parseRightSideOfDot(allowIdentifierNames: boolean, allowPrivateIdentifiers: boolean): ts.Identifier | ts.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() && ts.tokenIsIdentifierOrKeyword(token())) { - const matchesPattern = lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine); + return allowIdentifierNames ? parseIdentifierName() : parseIdentifier(); + } - 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(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Identifier_expected); - } - } + function parseTemplateSpans(isTaggedTemplate: boolean) { + const pos = getNodePos(); + const list = []; + let node: ts.TemplateSpan; + do { + node = parseTemplateSpan(isTaggedTemplate); + list.push(node); + } while (node.literal.kind === ts.SyntaxKind.TemplateMiddle); + return createNodeArray(list, pos); + } - if (token() === ts.SyntaxKind.PrivateIdentifier) { - const node = parsePrivateIdentifier(); - return allowPrivateIdentifiers ? node : createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Identifier_expected); - } + function parseTemplateExpression(isTaggedTemplate: boolean): ts.TemplateExpression { + const pos = getNodePos(); + return finishNode(factory.createTemplateExpression(parseTemplateHead(isTaggedTemplate), parseTemplateSpans(isTaggedTemplate)), pos); + } + function parseTemplateType(): ts.TemplateLiteralTypeNode { + const pos = getNodePos(); + return finishNode(factory.createTemplateLiteralType(parseTemplateHead(/*isTaggedTemplate*/ false), parseTemplateTypeSpans()), pos); + } - return allowIdentifierNames ? parseIdentifierName() : parseIdentifier(); - } + function parseTemplateTypeSpans() { + const pos = getNodePos(); + const list = []; + let node: ts.TemplateLiteralTypeSpan; + do { + node = parseTemplateTypeSpan(); + list.push(node); + } while (node.literal.kind === ts.SyntaxKind.TemplateMiddle); + return createNodeArray(list, pos); + } - function parseTemplateSpans(isTaggedTemplate: boolean) { - const pos = getNodePos(); - const list = []; - let node: ts.TemplateSpan; - do { - node = parseTemplateSpan(isTaggedTemplate); - list.push(node); - } while (node.literal.kind === ts.SyntaxKind.TemplateMiddle); - return createNodeArray(list, pos); - } + function parseTemplateTypeSpan(): ts.TemplateLiteralTypeSpan { + const pos = getNodePos(); + return finishNode(factory.createTemplateLiteralTypeSpan(parseType(), parseLiteralOfTemplateSpan(/*isTaggedTemplate*/ false)), pos); + } - function parseTemplateExpression(isTaggedTemplate: boolean): ts.TemplateExpression { - const pos = getNodePos(); - return finishNode(factory.createTemplateExpression(parseTemplateHead(isTaggedTemplate), parseTemplateSpans(isTaggedTemplate)), pos); + function parseLiteralOfTemplateSpan(isTaggedTemplate: boolean) { + if (token() === ts.SyntaxKind.CloseBraceToken) { + reScanTemplateToken(isTaggedTemplate); + return parseTemplateMiddleOrTemplateTail(); } - function parseTemplateType(): ts.TemplateLiteralTypeNode { - const pos = getNodePos(); - return finishNode(factory.createTemplateLiteralType(parseTemplateHead(/*isTaggedTemplate*/ false), parseTemplateTypeSpans()), pos); + else { + // TODO(rbuckton): Do we need to call `parseExpectedToken` or can we just call `createMissingNode` directly? + return parseExpectedToken(ts.SyntaxKind.TemplateTail, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.CloseBraceToken)) as ts.TemplateTail; } + } - function parseTemplateTypeSpans() { - const pos = getNodePos(); - const list = []; - let node: ts.TemplateLiteralTypeSpan; - do { - node = parseTemplateTypeSpan(); - list.push(node); - } while (node.literal.kind === ts.SyntaxKind.TemplateMiddle); - return createNodeArray(list, pos); - } + function parseTemplateSpan(isTaggedTemplate: boolean): ts.TemplateSpan { + const pos = getNodePos(); + return finishNode(factory.createTemplateSpan(allowInAnd(parseExpression), parseLiteralOfTemplateSpan(isTaggedTemplate)), pos); + } - function parseTemplateTypeSpan(): ts.TemplateLiteralTypeSpan { - const pos = getNodePos(); - return finishNode(factory.createTemplateLiteralTypeSpan(parseType(), parseLiteralOfTemplateSpan(/*isTaggedTemplate*/ false)), pos); - } + function parseLiteralNode(): ts.LiteralExpression { + return parseLiteralLikeNode(token()) as ts.LiteralExpression; + } - function parseLiteralOfTemplateSpan(isTaggedTemplate: boolean) { - if (token() === ts.SyntaxKind.CloseBraceToken) { - reScanTemplateToken(isTaggedTemplate); - return parseTemplateMiddleOrTemplateTail(); - } - else { - // TODO(rbuckton): Do we need to call `parseExpectedToken` or can we just call `createMissingNode` directly? - return parseExpectedToken(ts.SyntaxKind.TemplateTail, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.CloseBraceToken)) as ts.TemplateTail; - } + function parseTemplateHead(isTaggedTemplate: boolean): ts.TemplateHead { + if (isTaggedTemplate) { + reScanTemplateHeadOrNoSubstitutionTemplate(); } + const fragment = parseLiteralLikeNode(token()); + ts.Debug.assert(fragment.kind === ts.SyntaxKind.TemplateHead, "Template head has wrong token kind"); + return fragment as ts.TemplateHead; + } - function parseTemplateSpan(isTaggedTemplate: boolean): ts.TemplateSpan { - const pos = getNodePos(); - return finishNode(factory.createTemplateSpan(allowInAnd(parseExpression), parseLiteralOfTemplateSpan(isTaggedTemplate)), pos); - } + function parseTemplateMiddleOrTemplateTail(): ts.TemplateMiddle | ts.TemplateTail { + const fragment = parseLiteralLikeNode(token()); + ts.Debug.assert(fragment.kind === ts.SyntaxKind.TemplateMiddle || fragment.kind === ts.SyntaxKind.TemplateTail, "Template fragment has wrong token kind"); + return fragment as ts.TemplateMiddle | ts.TemplateTail; + } - function parseLiteralNode(): ts.LiteralExpression { - return parseLiteralLikeNode(token()) as ts.LiteralExpression; - } + function getTemplateLiteralRawText(kind: ts.TemplateLiteralToken["kind"]) { + const isLast = kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || kind === ts.SyntaxKind.TemplateTail; + const tokenText = scanner.getTokenText(); + return tokenText.substring(1, tokenText.length - (scanner.isUnterminated() ? 0 : isLast ? 1 : 2)); + } - function parseTemplateHead(isTaggedTemplate: boolean): ts.TemplateHead { - if (isTaggedTemplate) { - reScanTemplateHeadOrNoSubstitutionTemplate(); - } - const fragment = parseLiteralLikeNode(token()); - ts.Debug.assert(fragment.kind === ts.SyntaxKind.TemplateHead, "Template head has wrong token kind"); - return fragment as ts.TemplateHead; - } + function parseLiteralLikeNode(kind: ts.SyntaxKind): ts.LiteralLikeNode { + const pos = getNodePos(); + const node = ts.isTemplateLiteralKind(kind) ? factory.createTemplateLiteralLikeNode(kind, scanner.getTokenValue(), getTemplateLiteralRawText(kind), scanner.getTokenFlags() & ts.TokenFlags.TemplateLiteralLikeFlags) : + // 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. + kind === ts.SyntaxKind.NumericLiteral ? factory.createNumericLiteral(scanner.getTokenValue(), scanner.getNumericLiteralFlags()) : + kind === ts.SyntaxKind.StringLiteral ? factory.createStringLiteral(scanner.getTokenValue(), /*isSingleQuote*/ undefined, scanner.hasExtendedUnicodeEscape()) : + ts.isLiteralKind(kind) ? factory.createLiteralLikeNode(kind, scanner.getTokenValue()) : + ts.Debug.fail(); - function parseTemplateMiddleOrTemplateTail(): ts.TemplateMiddle | ts.TemplateTail { - const fragment = parseLiteralLikeNode(token()); - ts.Debug.assert(fragment.kind === ts.SyntaxKind.TemplateMiddle || fragment.kind === ts.SyntaxKind.TemplateTail, "Template fragment has wrong token kind"); - return fragment as ts.TemplateMiddle | ts.TemplateTail; + if (scanner.hasExtendedUnicodeEscape()) { + node.hasExtendedUnicodeEscape = true; } - function getTemplateLiteralRawText(kind: ts.TemplateLiteralToken["kind"]) { - const isLast = kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || kind === ts.SyntaxKind.TemplateTail; - const tokenText = scanner.getTokenText(); - return tokenText.substring(1, tokenText.length - (scanner.isUnterminated() ? 0 : isLast ? 1 : 2)); + if (scanner.isUnterminated()) { + node.isUnterminated = true; } - function parseLiteralLikeNode(kind: ts.SyntaxKind): ts.LiteralLikeNode { - const pos = getNodePos(); - const node = ts.isTemplateLiteralKind(kind) ? factory.createTemplateLiteralLikeNode(kind, scanner.getTokenValue(), getTemplateLiteralRawText(kind), scanner.getTokenFlags() & ts.TokenFlags.TemplateLiteralLikeFlags) : - // 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. - kind === ts.SyntaxKind.NumericLiteral ? factory.createNumericLiteral(scanner.getTokenValue(), scanner.getNumericLiteralFlags()) : - kind === ts.SyntaxKind.StringLiteral ? factory.createStringLiteral(scanner.getTokenValue(), /*isSingleQuote*/ undefined, scanner.hasExtendedUnicodeEscape()) : - ts.isLiteralKind(kind) ? factory.createLiteralLikeNode(kind, scanner.getTokenValue()) : - ts.Debug.fail(); + nextToken(); + return finishNode(node, pos); + } - if (scanner.hasExtendedUnicodeEscape()) { - node.hasExtendedUnicodeEscape = true; - } + // TYPES - if (scanner.isUnterminated()) { - node.isUnterminated = true; - } + function parseEntityNameOfTypeReference() { + return parseEntityName(/*allowReservedWords*/ true, ts.Diagnostics.Type_expected); + } - nextToken(); - return finishNode(node, pos); + function parseTypeArgumentsOfTypeReference() { + if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === ts.SyntaxKind.LessThanToken) { + return parseBracketedList(ParsingContext.TypeArguments, parseType, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken); } + } - // TYPES - - function parseEntityNameOfTypeReference() { - return parseEntityName(/*allowReservedWords*/ true, ts.Diagnostics.Type_expected); - } + function parseTypeReference(): ts.TypeReferenceNode { + const pos = getNodePos(); + return finishNode(factory.createTypeReferenceNode(parseEntityNameOfTypeReference(), parseTypeArgumentsOfTypeReference()), pos); + } - function parseTypeArgumentsOfTypeReference() { - if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === ts.SyntaxKind.LessThanToken) { - return parseBracketedList(ParsingContext.TypeArguments, parseType, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken); + // If true, we should abort parsing an error function. + function typeHasArrowFunctionBlockingParseError(node: ts.TypeNode): boolean { + switch (node.kind) { + case ts.SyntaxKind.TypeReference: + return ts.nodeIsMissing((node as ts.TypeReferenceNode).typeName); + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: { + const { parameters, type } = node as ts.FunctionOrConstructorTypeNode; + return isMissingList(parameters) || typeHasArrowFunctionBlockingParseError(type); } + case ts.SyntaxKind.ParenthesizedType: + return typeHasArrowFunctionBlockingParseError((node as ts.ParenthesizedTypeNode).type); + default: + return false; } + } - function parseTypeReference(): ts.TypeReferenceNode { - const pos = getNodePos(); - return finishNode(factory.createTypeReferenceNode(parseEntityNameOfTypeReference(), parseTypeArgumentsOfTypeReference()), pos); - } + function parseThisTypePredicate(lhs: ts.ThisTypeNode): ts.TypePredicateNode { + nextToken(); + return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType()), lhs.pos); + } - // If true, we should abort parsing an error function. - function typeHasArrowFunctionBlockingParseError(node: ts.TypeNode): boolean { - switch (node.kind) { - case ts.SyntaxKind.TypeReference: - return ts.nodeIsMissing((node as ts.TypeReferenceNode).typeName); - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: { - const { parameters, type } = node as ts.FunctionOrConstructorTypeNode; - return isMissingList(parameters) || typeHasArrowFunctionBlockingParseError(type); - } - case ts.SyntaxKind.ParenthesizedType: - return typeHasArrowFunctionBlockingParseError((node as ts.ParenthesizedTypeNode).type); - default: - return false; - } - } + function parseThisTypeNode(): ts.ThisTypeNode { + const pos = getNodePos(); + nextToken(); + return finishNode(factory.createThisTypeNode(), pos); + } - function parseThisTypePredicate(lhs: ts.ThisTypeNode): ts.TypePredicateNode { - nextToken(); - return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType()), lhs.pos); + function parseJSDocAllType(): ts.JSDocAllType | ts.JSDocOptionalType { + const pos = getNodePos(); + nextToken(); + return finishNode(factory.createJSDocAllType(), pos); + } + + function parseJSDocNonNullableType(): ts.TypeNode { + const pos = getNodePos(); + nextToken(); + return finishNode(factory.createJSDocNonNullableType(parseNonArrayType(), /*postfix*/ false), pos); + } + + function parseJSDocUnknownOrNullableType(): ts.JSDocUnknownType | ts.JSDocNullableType { + const pos = getNodePos(); + // 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() === ts.SyntaxKind.CommaToken || + token() === ts.SyntaxKind.CloseBraceToken || + token() === ts.SyntaxKind.CloseParenToken || + token() === ts.SyntaxKind.GreaterThanToken || + token() === ts.SyntaxKind.EqualsToken || + token() === ts.SyntaxKind.BarToken) { + return finishNode(factory.createJSDocUnknownType(), pos); + } + else { + return finishNode(factory.createJSDocNullableType(parseType(), /*postfix*/ false), pos); } + } - function parseThisTypeNode(): ts.ThisTypeNode { - const pos = getNodePos(); + function parseJSDocFunctionType(): ts.JSDocFunctionType | ts.TypeReferenceNode { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + if (lookAhead(nextTokenIsOpenParen)) { nextToken(); - return finishNode(factory.createThisTypeNode(), pos); + const parameters = parseParameters(SignatureFlags.Type | SignatureFlags.JSDoc); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + return withJSDoc(finishNode(factory.createJSDocFunctionType(parameters, type), pos), hasJSDoc); } + return finishNode(factory.createTypeReferenceNode(parseIdentifierName(), /*typeArguments*/ undefined), pos); + } - function parseJSDocAllType(): ts.JSDocAllType | ts.JSDocOptionalType { - const pos = getNodePos(); - nextToken(); - return finishNode(factory.createJSDocAllType(), pos); + function parseJSDocParameter(): ts.ParameterDeclaration { + const pos = getNodePos(); + let name: ts.Identifier | undefined; + if (token() === ts.SyntaxKind.ThisKeyword || token() === ts.SyntaxKind.NewKeyword) { + name = parseIdentifierName(); + parseExpected(ts.SyntaxKind.ColonToken); } + return finishNode(factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + // TODO(rbuckton): JSDoc parameters don't have names (except `this`/`new`), should we manufacture an empty identifier? + name!, + /*questionToken*/ undefined, parseJSDocType(), + /*initializer*/ undefined), pos); + } + function parseJSDocType(): ts.TypeNode { + scanner.setInJSDocType(true); + const pos = getNodePos(); + if (parseOptional(ts.SyntaxKind.ModuleKeyword)) { + // TODO(rbuckton): We never set the type for a JSDocNamepathType. What should we put here? + const moduleTag = factory.createJSDocNamepathType(/*type*/ undefined!); + terminate: while (true) { + switch (token()) { + case ts.SyntaxKind.CloseBraceToken: + case ts.SyntaxKind.EndOfFileToken: + case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.WhitespaceTrivia: + break terminate; + default: + nextTokenJSDoc(); + } + } - function parseJSDocNonNullableType(): ts.TypeNode { - const pos = getNodePos(); - nextToken(); - return finishNode(factory.createJSDocNonNullableType(parseNonArrayType(), /*postfix*/ false), pos); + scanner.setInJSDocType(false); + return finishNode(moduleTag, pos); } - function parseJSDocUnknownOrNullableType(): ts.JSDocUnknownType | ts.JSDocNullableType { - const pos = getNodePos(); - // skip the ? + const hasDotDotDot = parseOptional(ts.SyntaxKind.DotDotDotToken); + let type = parseTypeOrTypePredicate(); + scanner.setInJSDocType(false); + if (hasDotDotDot) { + type = finishNode(factory.createJSDocVariadicType(type), pos); + } + if (token() === ts.SyntaxKind.EqualsToken) { nextToken(); + return finishNode(factory.createJSDocOptionalType(type), pos); + } + return type; + } - // Need to lookahead to decide if this is a nullable or unknown type. + function parseTypeQuery(): ts.TypeQueryNode { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.TypeOfKeyword); + const entityName = parseEntityName(/*allowReservedWords*/ true); + // Make sure we perform ASI to prevent parsing the next line's type arguments as part of an instantiation expression. + const typeArguments = !scanner.hasPrecedingLineBreak() ? tryParseTypeArguments() : undefined; + return finishNode(factory.createTypeQueryNode(entityName, typeArguments), pos); + } - // Here are cases where we'll pick the unknown type: - // - // Foo(?, - // { a: ? } - // Foo(?) - // Foo - // Foo(?= - // (?| - if (token() === ts.SyntaxKind.CommaToken || - token() === ts.SyntaxKind.CloseBraceToken || - token() === ts.SyntaxKind.CloseParenToken || - token() === ts.SyntaxKind.GreaterThanToken || - token() === ts.SyntaxKind.EqualsToken || - token() === ts.SyntaxKind.BarToken) { - return finishNode(factory.createJSDocUnknownType(), pos); + function parseTypeParameter(): ts.TypeParameterDeclaration { + const pos = getNodePos(); + const modifiers = parseModifiers(); + const name = parseIdentifier(); + let constraint: ts.TypeNode | undefined; + let expression: ts.Expression | undefined; + if (parseOptional(ts.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()) { + constraint = parseType(); } else { - return finishNode(factory.createJSDocNullableType(parseType(), /*postfix*/ false), pos); + // 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 "". + expression = parseUnaryExpressionOrHigher(); } } - function parseJSDocFunctionType(): ts.JSDocFunctionType | ts.TypeReferenceNode { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - if (lookAhead(nextTokenIsOpenParen)) { - nextToken(); - const parameters = parseParameters(SignatureFlags.Type | SignatureFlags.JSDoc); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - return withJSDoc(finishNode(factory.createJSDocFunctionType(parameters, type), pos), hasJSDoc); - } - return finishNode(factory.createTypeReferenceNode(parseIdentifierName(), /*typeArguments*/ undefined), pos); + const defaultType = parseOptional(ts.SyntaxKind.EqualsToken) ? parseType() : undefined; + const node = factory.createTypeParameterDeclaration(modifiers, name, constraint, defaultType); + node.expression = expression; + return finishNode(node, pos); + } + + function parseTypeParameters(): ts.NodeArray | undefined { + if (token() === ts.SyntaxKind.LessThanToken) { + return parseBracketedList(ParsingContext.TypeParameters, parseTypeParameter, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken); } + } - function parseJSDocParameter(): ts.ParameterDeclaration { - const pos = getNodePos(); - let name: ts.Identifier | undefined; - if (token() === ts.SyntaxKind.ThisKeyword || token() === ts.SyntaxKind.NewKeyword) { - name = parseIdentifierName(); - parseExpected(ts.SyntaxKind.ColonToken); - } - return finishNode(factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - // TODO(rbuckton): JSDoc parameters don't have names (except `this`/`new`), should we manufacture an empty identifier? - name!, - /*questionToken*/ undefined, parseJSDocType(), - /*initializer*/ undefined), pos); - } - function parseJSDocType(): ts.TypeNode { - scanner.setInJSDocType(true); - const pos = getNodePos(); - if (parseOptional(ts.SyntaxKind.ModuleKeyword)) { - // TODO(rbuckton): We never set the type for a JSDocNamepathType. What should we put here? - const moduleTag = factory.createJSDocNamepathType(/*type*/ undefined!); - terminate: while (true) { - switch (token()) { - case ts.SyntaxKind.CloseBraceToken: - case ts.SyntaxKind.EndOfFileToken: - case ts.SyntaxKind.CommaToken: - case ts.SyntaxKind.WhitespaceTrivia: - break terminate; - default: - nextTokenJSDoc(); - } - } + function isStartOfParameter(isJSDocParameter: boolean): boolean { + return token() === ts.SyntaxKind.DotDotDotToken || + isBindingIdentifierOrPrivateIdentifierOrPattern() || + ts.isModifierKind(token()) || + token() === ts.SyntaxKind.AtToken || + isStartOfType(/*inStartOfParameter*/ !isJSDocParameter); + } - scanner.setInJSDocType(false); - return finishNode(moduleTag, pos); - } - - const hasDotDotDot = parseOptional(ts.SyntaxKind.DotDotDotToken); - let type = parseTypeOrTypePredicate(); - scanner.setInJSDocType(false); - if (hasDotDotDot) { - type = finishNode(factory.createJSDocVariadicType(type), pos); - } - if (token() === ts.SyntaxKind.EqualsToken) { - nextToken(); - return finishNode(factory.createJSDocOptionalType(type), pos); - } - return type; - } - - function parseTypeQuery(): ts.TypeQueryNode { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.TypeOfKeyword); - const entityName = parseEntityName(/*allowReservedWords*/ true); - // Make sure we perform ASI to prevent parsing the next line's type arguments as part of an instantiation expression. - const typeArguments = !scanner.hasPrecedingLineBreak() ? tryParseTypeArguments() : undefined; - return finishNode(factory.createTypeQueryNode(entityName, typeArguments), pos); - } - - function parseTypeParameter(): ts.TypeParameterDeclaration { - const pos = getNodePos(); - const modifiers = parseModifiers(); - const name = parseIdentifier(); - let constraint: ts.TypeNode | undefined; - let expression: ts.Expression | undefined; - if (parseOptional(ts.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()) { - 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 "". - expression = parseUnaryExpressionOrHigher(); - } - } - - const defaultType = parseOptional(ts.SyntaxKind.EqualsToken) ? parseType() : undefined; - const node = factory.createTypeParameterDeclaration(modifiers, name, constraint, defaultType); - node.expression = expression; - return finishNode(node, pos); - } - - function parseTypeParameters(): ts.NodeArray | undefined { - if (token() === ts.SyntaxKind.LessThanToken) { - return parseBracketedList(ParsingContext.TypeParameters, parseTypeParameter, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken); - } - } - - function isStartOfParameter(isJSDocParameter: boolean): boolean { - return token() === ts.SyntaxKind.DotDotDotToken || - isBindingIdentifierOrPrivateIdentifierOrPattern() || - ts.isModifierKind(token()) || - token() === ts.SyntaxKind.AtToken || - isStartOfType(/*inStartOfParameter*/ !isJSDocParameter); - } - - function parseNameOfParameter(modifiers: ts.ModifiersArray | undefined) { - // FormalParameter [Yield,Await]: - // BindingElement[?Yield,?Await] - const name = parseIdentifierOrPattern(ts.Diagnostics.Private_identifiers_cannot_be_used_as_parameters); - if (ts.getFullWidth(name) === 0 && !ts.some(modifiers) && ts.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(); - } - return name; - } - - function isParameterNameStart() { - // Be permissive about await and yield by calling isBindingIdentifier instead of isIdentifier; disallowing - // them during a speculative parse leads to many more follow-on errors than allowing the function to parse then later - // complaining about the use of the keywords. - return isBindingIdentifier() || token() === ts.SyntaxKind.OpenBracketToken || token() === ts.SyntaxKind.OpenBraceToken; + function parseNameOfParameter(modifiers: ts.ModifiersArray | undefined) { + // FormalParameter [Yield,Await]: + // BindingElement[?Yield,?Await] + const name = parseIdentifierOrPattern(ts.Diagnostics.Private_identifiers_cannot_be_used_as_parameters); + if (ts.getFullWidth(name) === 0 && !ts.some(modifiers) && ts.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(); } + return name; + } - function parseParameter(inOuterAwaitContext: boolean): ts.ParameterDeclaration { - return parseParameterWorker(inOuterAwaitContext); - } + function isParameterNameStart() { + // Be permissive about await and yield by calling isBindingIdentifier instead of isIdentifier; disallowing + // them during a speculative parse leads to many more follow-on errors than allowing the function to parse then later + // complaining about the use of the keywords. + return isBindingIdentifier() || token() === ts.SyntaxKind.OpenBracketToken || token() === ts.SyntaxKind.OpenBraceToken; + } - function parseParameterForSpeculation(inOuterAwaitContext: boolean): ts.ParameterDeclaration | undefined { - return parseParameterWorker(inOuterAwaitContext, /*allowAmbiguity*/ false); - } + function parseParameter(inOuterAwaitContext: boolean): ts.ParameterDeclaration { + return parseParameterWorker(inOuterAwaitContext); + } - function parseParameterWorker(inOuterAwaitContext: boolean): ts.ParameterDeclaration; - function parseParameterWorker(inOuterAwaitContext: boolean, allowAmbiguity: false): ts.ParameterDeclaration | undefined; - function parseParameterWorker(inOuterAwaitContext: boolean, allowAmbiguity = true): ts.ParameterDeclaration | undefined { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); + function parseParameterForSpeculation(inOuterAwaitContext: boolean): ts.ParameterDeclaration | undefined { + return parseParameterWorker(inOuterAwaitContext, /*allowAmbiguity*/ false); + } - // FormalParameter [Yield,Await]: - // BindingElement[?Yield,?Await] + function parseParameterWorker(inOuterAwaitContext: boolean): ts.ParameterDeclaration; + function parseParameterWorker(inOuterAwaitContext: boolean, allowAmbiguity: false): ts.ParameterDeclaration | undefined; + function parseParameterWorker(inOuterAwaitContext: boolean, allowAmbiguity = true): ts.ParameterDeclaration | undefined { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); - // Decorators are parsed in the outer [Await] context, the rest of the parameter is parsed in the function's [Await] context. - const decorators = inOuterAwaitContext ? doInAwaitContext(parseDecorators) : parseDecorators(); + // FormalParameter [Yield,Await]: + // BindingElement[?Yield,?Await] - if (token() === ts.SyntaxKind.ThisKeyword) { - const node = factory.createParameterDeclaration(decorators, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, createIdentifier(/*isIdentifier*/ true), - /*questionToken*/ undefined, parseTypeAnnotation(), - /*initializer*/ undefined); + // Decorators are parsed in the outer [Await] context, the rest of the parameter is parsed in the function's [Await] context. + const decorators = inOuterAwaitContext ? doInAwaitContext(parseDecorators) : parseDecorators(); - if (decorators) { - parseErrorAtRange(decorators[0], ts.Diagnostics.Decorators_may_not_be_applied_to_this_parameters); - } + if (token() === ts.SyntaxKind.ThisKeyword) { + const node = factory.createParameterDeclaration(decorators, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier(/*isIdentifier*/ true), + /*questionToken*/ undefined, parseTypeAnnotation(), + /*initializer*/ undefined); - return withJSDoc(finishNode(node, pos), hasJSDoc); + if (decorators) { + parseErrorAtRange(decorators[0], ts.Diagnostics.Decorators_may_not_be_applied_to_this_parameters); } - const savedTopLevel = topLevel; - topLevel = false; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - const modifiers = parseModifiers(); - const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); + const savedTopLevel = topLevel; + topLevel = false; - if (!allowAmbiguity && !isParameterNameStart()) { - return undefined; - } + const modifiers = parseModifiers(); + const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); - const node = withJSDoc(finishNode(factory.createParameterDeclaration(decorators, modifiers, dotDotDotToken, parseNameOfParameter(modifiers), parseOptionalToken(ts.SyntaxKind.QuestionToken), parseTypeAnnotation(), parseInitializer()), pos), hasJSDoc); - topLevel = savedTopLevel; - return node; + if (!allowAmbiguity && !isParameterNameStart()) { + return undefined; } - function parseReturnType(returnToken: ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): ts.TypeNode; - function parseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): ts.TypeNode | undefined; - function parseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean) { - if (shouldParseReturnType(returnToken, isType)) { - return allowConditionalTypesAnd(parseTypeOrTypePredicate); - } + const node = withJSDoc(finishNode(factory.createParameterDeclaration(decorators, modifiers, dotDotDotToken, parseNameOfParameter(modifiers), parseOptionalToken(ts.SyntaxKind.QuestionToken), parseTypeAnnotation(), parseInitializer()), pos), hasJSDoc); + topLevel = savedTopLevel; + return node; + } + + function parseReturnType(returnToken: ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): ts.TypeNode; + function parseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): ts.TypeNode | undefined; + function parseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean) { + if (shouldParseReturnType(returnToken, isType)) { + return allowConditionalTypesAnd(parseTypeOrTypePredicate); } + } - function shouldParseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): boolean { - if (returnToken === ts.SyntaxKind.EqualsGreaterThanToken) { - parseExpected(returnToken); - return true; - } - else if (parseOptional(ts.SyntaxKind.ColonToken)) { - return true; - } - else if (isType && token() === ts.SyntaxKind.EqualsGreaterThanToken) { - // This is easy to get backward, especially in type contexts, so parse the type anyway - parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.ColonToken)); - nextToken(); - return true; - } - return false; + function shouldParseReturnType(returnToken: ts.SyntaxKind.ColonToken | ts.SyntaxKind.EqualsGreaterThanToken, isType: boolean): boolean { + if (returnToken === ts.SyntaxKind.EqualsGreaterThanToken) { + parseExpected(returnToken); + return true; + } + else if (parseOptional(ts.SyntaxKind.ColonToken)) { + return true; + } + else if (isType && token() === ts.SyntaxKind.EqualsGreaterThanToken) { + // This is easy to get backward, especially in type contexts, so parse the type anyway + parseErrorAtCurrentToken(ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.ColonToken)); + nextToken(); + return true; } + return false; + } - function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: true): ts.NodeArray; - function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: false): ts.NodeArray | undefined; - function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: boolean): ts.NodeArray | undefined { - // 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 - const savedYieldContext = inYieldContext(); - const savedAwaitContext = inAwaitContext(); + function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: true): ts.NodeArray; + function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: false): ts.NodeArray | undefined; + function parseParametersWorker(flags: SignatureFlags, allowAmbiguity: boolean): ts.NodeArray | undefined { + // 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 + const savedYieldContext = inYieldContext(); + const savedAwaitContext = inAwaitContext(); + + setYieldContext(!!(flags & SignatureFlags.Yield)); + setAwaitContext(!!(flags & SignatureFlags.Await)); - setYieldContext(!!(flags & SignatureFlags.Yield)); - setAwaitContext(!!(flags & SignatureFlags.Await)); + const parameters = flags & SignatureFlags.JSDoc ? + parseDelimitedList(ParsingContext.JSDocParameters, parseJSDocParameter) : + parseDelimitedList(ParsingContext.Parameters, () => allowAmbiguity ? parseParameter(savedAwaitContext) : parseParameterForSpeculation(savedAwaitContext)); - const parameters = flags & SignatureFlags.JSDoc ? - parseDelimitedList(ParsingContext.JSDocParameters, parseJSDocParameter) : - parseDelimitedList(ParsingContext.Parameters, () => allowAmbiguity ? parseParameter(savedAwaitContext) : parseParameterForSpeculation(savedAwaitContext)); + setYieldContext(savedYieldContext); + setAwaitContext(savedAwaitContext); - setYieldContext(savedYieldContext); - setAwaitContext(savedAwaitContext); + return parameters; + } - return parameters; + function parseParameters(flags: SignatureFlags): ts.NodeArray { + // 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(ts.SyntaxKind.OpenParenToken)) { + return createMissingList(); } - function parseParameters(flags: SignatureFlags): ts.NodeArray { - // 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(ts.SyntaxKind.OpenParenToken)) { - return createMissingList(); - } + const parameters = parseParametersWorker(flags, /*allowAmbiguity*/ true); + parseExpected(ts.SyntaxKind.CloseParenToken); + return parameters; + } - const parameters = parseParametersWorker(flags, /*allowAmbiguity*/ true); - parseExpected(ts.SyntaxKind.CloseParenToken); - return parameters; + 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(ts.SyntaxKind.CommaToken)) { + return; } - 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(ts.SyntaxKind.CommaToken)) { - return; - } + // Didn't have a comma. We must have a (possible ASI) semicolon. + parseSemicolon(); + } - // Didn't have a comma. We must have a (possible ASI) semicolon. - parseSemicolon(); + function parseSignatureMember(kind: ts.SyntaxKind.CallSignature | ts.SyntaxKind.ConstructSignature): ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + if (kind === ts.SyntaxKind.ConstructSignature) { + parseExpected(ts.SyntaxKind.NewKeyword); } - function parseSignatureMember(kind: ts.SyntaxKind.CallSignature | ts.SyntaxKind.ConstructSignature): ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - if (kind === ts.SyntaxKind.ConstructSignature) { - parseExpected(ts.SyntaxKind.NewKeyword); - } + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(SignatureFlags.Type); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ true); + parseTypeMemberSemicolon(); + const node = kind === ts.SyntaxKind.CallSignature + ? factory.createCallSignature(typeParameters, parameters, type) + : factory.createConstructSignature(typeParameters, parameters, type); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(SignatureFlags.Type); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ true); - parseTypeMemberSemicolon(); - const node = kind === ts.SyntaxKind.CallSignature - ? factory.createCallSignature(typeParameters, parameters, type) - : factory.createConstructSignature(typeParameters, parameters, type); - return withJSDoc(finishNode(node, pos), hasJSDoc); - } + function isIndexSignature(): boolean { + return token() === ts.SyntaxKind.OpenBracketToken && lookAhead(isUnambiguouslyIndexSignature); + } - function isIndexSignature(): boolean { - return token() === ts.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() === ts.SyntaxKind.DotDotDotToken || token() === ts.SyntaxKind.CloseBracketToken) { + 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 - // [] - // + if (ts.isModifierKind(token())) { nextToken(); - if (token() === ts.SyntaxKind.DotDotDotToken || token() === ts.SyntaxKind.CloseBracketToken) { - return true; - } - - if (ts.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() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken) { + if (isIdentifier()) { 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() !== ts.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. - nextToken(); - return token() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.CloseBracketToken; } - - function parseIndexSignatureDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.IndexSignatureDeclaration { - const parameters = parseBracketedList(ParsingContext.Parameters, () => parseParameter(/*inOuterAwaitContext*/ false), ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken); - const type = parseTypeAnnotation(); - parseTypeMemberSemicolon(); - const node = factory.createIndexSignature(decorators, modifiers, parameters, type); - return withJSDoc(finishNode(node, pos), hasJSDoc); + else if (!isIdentifier()) { + return false; + } + else { + // Skip the identifier + nextToken(); } - function parsePropertyOrMethodSignature(pos: number, hasJSDoc: boolean, modifiers: ts.NodeArray | undefined): ts.PropertySignature | ts.MethodSignature { - const name = parsePropertyName(); - const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); - let node: ts.PropertySignature | ts.MethodSignature; - if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { - // Method signatures don't exist in expression contexts. So they have neither - // [Yield] nor [Await] - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(SignatureFlags.Type); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ true); - node = factory.createMethodSignature(modifiers, name, questionToken, typeParameters, parameters, type); - } - else { - const type = parseTypeAnnotation(); - node = factory.createPropertySignature(modifiers, name, questionToken, type); - // 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. - if (token() === ts.SyntaxKind.EqualsToken) - node.initializer = parseInitializer(); - } - parseTypeMemberSemicolon(); - return withJSDoc(finishNode(node, pos), hasJSDoc); + // 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() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken) { + return true; } - function isTypeMemberStart(): boolean { - // Return true if we have the start of a signature member - if (token() === ts.SyntaxKind.OpenParenToken || - token() === ts.SyntaxKind.LessThanToken || - token() === ts.SyntaxKind.GetKeyword || - token() === ts.SyntaxKind.SetKeyword) { - return true; - } - let idToken = false; - // Eat up all modifiers, but hold on to the last one in case it is actually an identifier - while (ts.isModifierKind(token())) { - idToken = true; - nextToken(); - } - // Index signatures and computed property names are type members - if (token() === ts.SyntaxKind.OpenBracketToken) { - 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() === ts.SyntaxKind.OpenParenToken || - token() === ts.SyntaxKind.LessThanToken || - token() === ts.SyntaxKind.QuestionToken || - token() === ts.SyntaxKind.ColonToken || - token() === ts.SyntaxKind.CommaToken || - canParseSemicolon(); - } + // Question mark could be an indexer with an optional property, + // or it could be a conditional expression in a computed property. + if (token() !== ts.SyntaxKind.QuestionToken) { return false; } - function parseTypeMember(): ts.TypeElement { - if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { - return parseSignatureMember(ts.SyntaxKind.CallSignature); - } - if (token() === ts.SyntaxKind.NewKeyword && lookAhead(nextTokenIsOpenParenOrLessThan)) { - return parseSignatureMember(ts.SyntaxKind.ConstructSignature); - } - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const modifiers = parseModifiers(); - if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers, ts.SyntaxKind.GetAccessor); - } + // 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() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.CloseBracketToken; + } - if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers, ts.SyntaxKind.SetAccessor); - } + function parseIndexSignatureDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.IndexSignatureDeclaration { + const parameters = parseBracketedList(ParsingContext.Parameters, () => parseParameter(/*inOuterAwaitContext*/ false), ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken); + const type = parseTypeAnnotation(); + parseTypeMemberSemicolon(); + const node = factory.createIndexSignature(decorators, modifiers, parameters, type); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - if (isIndexSignature()) { - return parseIndexSignatureDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers); - } - return parsePropertyOrMethodSignature(pos, hasJSDoc, modifiers); + function parsePropertyOrMethodSignature(pos: number, hasJSDoc: boolean, modifiers: ts.NodeArray | undefined): ts.PropertySignature | ts.MethodSignature { + const name = parsePropertyName(); + const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); + let node: ts.PropertySignature | ts.MethodSignature; + if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { + // Method signatures don't exist in expression contexts. So they have neither + // [Yield] nor [Await] + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(SignatureFlags.Type); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ true); + node = factory.createMethodSignature(modifiers, name, questionToken, typeParameters, parameters, type); } + else { + const type = parseTypeAnnotation(); + node = factory.createPropertySignature(modifiers, name, questionToken, type); + // 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. + if (token() === ts.SyntaxKind.EqualsToken) + node.initializer = parseInitializer(); + } + parseTypeMemberSemicolon(); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - function nextTokenIsOpenParenOrLessThan() { + function isTypeMemberStart(): boolean { + // Return true if we have the start of a signature member + if (token() === ts.SyntaxKind.OpenParenToken || + token() === ts.SyntaxKind.LessThanToken || + token() === ts.SyntaxKind.GetKeyword || + token() === ts.SyntaxKind.SetKeyword) { + return true; + } + let idToken = false; + // Eat up all modifiers, but hold on to the last one in case it is actually an identifier + while (ts.isModifierKind(token())) { + idToken = true; nextToken(); - return token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken; } + // Index signatures and computed property names are type members + if (token() === ts.SyntaxKind.OpenBracketToken) { + 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() === ts.SyntaxKind.OpenParenToken || + token() === ts.SyntaxKind.LessThanToken || + token() === ts.SyntaxKind.QuestionToken || + token() === ts.SyntaxKind.ColonToken || + token() === ts.SyntaxKind.CommaToken || + canParseSemicolon(); + } + return false; + } - function nextTokenIsDot() { - return nextToken() === ts.SyntaxKind.DotToken; + function parseTypeMember(): ts.TypeElement { + if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { + return parseSignatureMember(ts.SyntaxKind.CallSignature); + } + if (token() === ts.SyntaxKind.NewKeyword && lookAhead(nextTokenIsOpenParenOrLessThan)) { + return parseSignatureMember(ts.SyntaxKind.ConstructSignature); + } + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const modifiers = parseModifiers(); + if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers, ts.SyntaxKind.GetAccessor); } - function nextTokenIsOpenParenOrLessThanOrDot() { - switch (nextToken()) { - case ts.SyntaxKind.OpenParenToken: - case ts.SyntaxKind.LessThanToken: - case ts.SyntaxKind.DotToken: - return true; - } - return false; + if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers, ts.SyntaxKind.SetAccessor); } - function parseTypeLiteral(): ts.TypeLiteralNode { - const pos = getNodePos(); - return finishNode(factory.createTypeLiteralNode(parseObjectTypeMembers()), pos); + if (isIndexSignature()) { + return parseIndexSignatureDeclaration(pos, hasJSDoc, /*decorators*/ undefined, modifiers); } + return parsePropertyOrMethodSignature(pos, hasJSDoc, modifiers); + } - function parseObjectTypeMembers(): ts.NodeArray { - let members: ts.NodeArray; - if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { - members = parseList(ParsingContext.TypeMembers, parseTypeMember); - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - else { - members = createMissingList(); - } + function nextTokenIsOpenParenOrLessThan() { + nextToken(); + return token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken; + } - return members; - } + function nextTokenIsDot() { + return nextToken() === ts.SyntaxKind.DotToken; + } - function isStartOfMappedType() { - nextToken(); - if (token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { - return nextToken() === ts.SyntaxKind.ReadonlyKeyword; - } - if (token() === ts.SyntaxKind.ReadonlyKeyword) { - nextToken(); - } - return token() === ts.SyntaxKind.OpenBracketToken && nextTokenIsIdentifier() && nextToken() === ts.SyntaxKind.InKeyword; + function nextTokenIsOpenParenOrLessThanOrDot() { + switch (nextToken()) { + case ts.SyntaxKind.OpenParenToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.DotToken: + return true; } + return false; + } - function parseMappedTypeParameter() { - const pos = getNodePos(); - const name = parseIdentifierName(); - parseExpected(ts.SyntaxKind.InKeyword); - const type = parseType(); - return finishNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, type, /*defaultType*/ undefined), pos); - } + function parseTypeLiteral(): ts.TypeLiteralNode { + const pos = getNodePos(); + return finishNode(factory.createTypeLiteralNode(parseObjectTypeMembers()), pos); + } - function parseMappedType() { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenBraceToken); - let readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined; - if (token() === ts.SyntaxKind.ReadonlyKeyword || token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { - readonlyToken = parseTokenNode(); - if (readonlyToken.kind !== ts.SyntaxKind.ReadonlyKeyword) { - parseExpected(ts.SyntaxKind.ReadonlyKeyword); - } - } - parseExpected(ts.SyntaxKind.OpenBracketToken); - const typeParameter = parseMappedTypeParameter(); - const nameType = parseOptional(ts.SyntaxKind.AsKeyword) ? parseType() : undefined; - parseExpected(ts.SyntaxKind.CloseBracketToken); - let questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined; - if (token() === ts.SyntaxKind.QuestionToken || token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { - questionToken = parseTokenNode(); - if (questionToken.kind !== ts.SyntaxKind.QuestionToken) { - parseExpected(ts.SyntaxKind.QuestionToken); - } - } - const type = parseTypeAnnotation(); - parseSemicolon(); - const members = parseList(ParsingContext.TypeMembers, parseTypeMember); + function parseObjectTypeMembers(): ts.NodeArray { + let members: ts.NodeArray; + if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { + members = parseList(ParsingContext.TypeMembers, parseTypeMember); parseExpected(ts.SyntaxKind.CloseBraceToken); - return finishNode(factory.createMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, type, members), pos); } - - function parseTupleElementType() { - const pos = getNodePos(); - if (parseOptional(ts.SyntaxKind.DotDotDotToken)) { - return finishNode(factory.createRestTypeNode(parseType()), pos); - } - const type = parseType(); - if (ts.isJSDocNullableType(type) && type.pos === type.type.pos) { - const node = factory.createOptionalTypeNode(type.type); - ts.setTextRange(node, type); - (node as ts.Mutable).flags = type.flags; - return node; - } - return type; + else { + members = createMissingList(); } - function isNextTokenColonOrQuestionColon() { - return nextToken() === ts.SyntaxKind.ColonToken || (token() === ts.SyntaxKind.QuestionToken && nextToken() === ts.SyntaxKind.ColonToken); - } + return members; + } - function isTupleElementName() { - if (token() === ts.SyntaxKind.DotDotDotToken) { - return ts.tokenIsIdentifierOrKeyword(nextToken()) && isNextTokenColonOrQuestionColon(); - } - return ts.tokenIsIdentifierOrKeyword(token()) && isNextTokenColonOrQuestionColon(); + function isStartOfMappedType() { + nextToken(); + if (token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { + return nextToken() === ts.SyntaxKind.ReadonlyKeyword; + } + if (token() === ts.SyntaxKind.ReadonlyKeyword) { + nextToken(); } + return token() === ts.SyntaxKind.OpenBracketToken && nextTokenIsIdentifier() && nextToken() === ts.SyntaxKind.InKeyword; + } - function parseTupleElementNameOrTupleElementType() { - if (lookAhead(isTupleElementName)) { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); - const name = parseIdentifierName(); - const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); - parseExpected(ts.SyntaxKind.ColonToken); - const type = parseTupleElementType(); - const node = factory.createNamedTupleMember(dotDotDotToken, name, questionToken, type); - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseMappedTypeParameter() { + const pos = getNodePos(); + const name = parseIdentifierName(); + parseExpected(ts.SyntaxKind.InKeyword); + const type = parseType(); + return finishNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, type, /*defaultType*/ undefined), pos); + } + + function parseMappedType() { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenBraceToken); + let readonlyToken: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken | undefined; + if (token() === ts.SyntaxKind.ReadonlyKeyword || token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { + readonlyToken = parseTokenNode(); + if (readonlyToken.kind !== ts.SyntaxKind.ReadonlyKeyword) { + parseExpected(ts.SyntaxKind.ReadonlyKeyword); + } + } + parseExpected(ts.SyntaxKind.OpenBracketToken); + const typeParameter = parseMappedTypeParameter(); + const nameType = parseOptional(ts.SyntaxKind.AsKeyword) ? parseType() : undefined; + parseExpected(ts.SyntaxKind.CloseBracketToken); + let questionToken: ts.QuestionToken | ts.PlusToken | ts.MinusToken | undefined; + if (token() === ts.SyntaxKind.QuestionToken || token() === ts.SyntaxKind.PlusToken || token() === ts.SyntaxKind.MinusToken) { + questionToken = parseTokenNode(); + if (questionToken.kind !== ts.SyntaxKind.QuestionToken) { + parseExpected(ts.SyntaxKind.QuestionToken); } - return parseTupleElementType(); } + const type = parseTypeAnnotation(); + parseSemicolon(); + const members = parseList(ParsingContext.TypeMembers, parseTypeMember); + parseExpected(ts.SyntaxKind.CloseBraceToken); + return finishNode(factory.createMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, type, members), pos); + } - function parseTupleType(): ts.TupleTypeNode { - const pos = getNodePos(); - return finishNode(factory.createTupleTypeNode(parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementNameOrTupleElementType, ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken)), pos); + function parseTupleElementType() { + const pos = getNodePos(); + if (parseOptional(ts.SyntaxKind.DotDotDotToken)) { + return finishNode(factory.createRestTypeNode(parseType()), pos); } - - function parseParenthesizedType(): ts.TypeNode { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenParenToken); - const type = parseType(); - parseExpected(ts.SyntaxKind.CloseParenToken); - return finishNode(factory.createParenthesizedType(type), pos); + const type = parseType(); + if (ts.isJSDocNullableType(type) && type.pos === type.type.pos) { + const node = factory.createOptionalTypeNode(type.type); + ts.setTextRange(node, type); + (node as ts.Mutable).flags = type.flags; + return node; } + return type; + } - function parseModifiersForConstructorType(): ts.NodeArray | undefined { - let modifiers: ts.NodeArray | undefined; - if (token() === ts.SyntaxKind.AbstractKeyword) { - const pos = getNodePos(); - nextToken(); - const modifier = finishNode(factory.createToken(ts.SyntaxKind.AbstractKeyword), pos); - modifiers = createNodeArray([modifier], pos); - } - return modifiers; + function isNextTokenColonOrQuestionColon() { + return nextToken() === ts.SyntaxKind.ColonToken || (token() === ts.SyntaxKind.QuestionToken && nextToken() === ts.SyntaxKind.ColonToken); + } + + function isTupleElementName() { + if (token() === ts.SyntaxKind.DotDotDotToken) { + return ts.tokenIsIdentifierOrKeyword(nextToken()) && isNextTokenColonOrQuestionColon(); } + return ts.tokenIsIdentifierOrKeyword(token()) && isNextTokenColonOrQuestionColon(); + } - function parseFunctionOrConstructorType(): ts.TypeNode { + function parseTupleElementNameOrTupleElementType() { + if (lookAhead(isTupleElementName)) { const pos = getNodePos(); const hasJSDoc = hasPrecedingJSDocComment(); - const modifiers = parseModifiersForConstructorType(); - const isConstructorType = parseOptional(ts.SyntaxKind.NewKeyword); - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(SignatureFlags.Type); - const type = parseReturnType(ts.SyntaxKind.EqualsGreaterThanToken, /*isType*/ false); - const node = isConstructorType - ? factory.createConstructorTypeNode(modifiers, typeParameters, parameters, type) - : factory.createFunctionTypeNode(typeParameters, parameters, type); - if (!isConstructorType) - (node as ts.Mutable).modifiers = modifiers; + const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); + const name = parseIdentifierName(); + const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); + parseExpected(ts.SyntaxKind.ColonToken); + const type = parseTupleElementType(); + const node = factory.createNamedTupleMember(dotDotDotToken, name, questionToken, type); return withJSDoc(finishNode(node, pos), hasJSDoc); } + return parseTupleElementType(); + } - function parseKeywordAndNoDot(): ts.TypeNode | undefined { - const node = parseTokenNode(); - return token() === ts.SyntaxKind.DotToken ? undefined : node; - } + function parseTupleType(): ts.TupleTypeNode { + const pos = getNodePos(); + return finishNode(factory.createTupleTypeNode(parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementNameOrTupleElementType, ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken)), pos); + } + + function parseParenthesizedType(): ts.TypeNode { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenParenToken); + const type = parseType(); + parseExpected(ts.SyntaxKind.CloseParenToken); + return finishNode(factory.createParenthesizedType(type), pos); + } - function parseLiteralTypeNode(negative?: boolean): ts.LiteralTypeNode { + function parseModifiersForConstructorType(): ts.NodeArray | undefined { + let modifiers: ts.NodeArray | undefined; + if (token() === ts.SyntaxKind.AbstractKeyword) { const pos = getNodePos(); - if (negative) { - nextToken(); - } - let expression: ts.BooleanLiteral | ts.NullLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression = token() === ts.SyntaxKind.TrueKeyword || token() === ts.SyntaxKind.FalseKeyword || token() === ts.SyntaxKind.NullKeyword ? - parseTokenNode() : - parseLiteralLikeNode(token()) as ts.LiteralExpression; - if (negative) { - expression = finishNode(factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, expression), pos); - } - return finishNode(factory.createLiteralTypeNode(expression), pos); + nextToken(); + const modifier = finishNode(factory.createToken(ts.SyntaxKind.AbstractKeyword), pos); + modifiers = createNodeArray([modifier], pos); } + return modifiers; + } + + function parseFunctionOrConstructorType(): ts.TypeNode { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const modifiers = parseModifiersForConstructorType(); + const isConstructorType = parseOptional(ts.SyntaxKind.NewKeyword); + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(SignatureFlags.Type); + const type = parseReturnType(ts.SyntaxKind.EqualsGreaterThanToken, /*isType*/ false); + const node = isConstructorType + ? factory.createConstructorTypeNode(modifiers, typeParameters, parameters, type) + : factory.createFunctionTypeNode(typeParameters, parameters, type); + if (!isConstructorType) + (node as ts.Mutable).modifiers = modifiers; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } + + function parseKeywordAndNoDot(): ts.TypeNode | undefined { + const node = parseTokenNode(); + return token() === ts.SyntaxKind.DotToken ? undefined : node; + } - function isStartOfTypeOfImportType() { + function parseLiteralTypeNode(negative?: boolean): ts.LiteralTypeNode { + const pos = getNodePos(); + if (negative) { nextToken(); - return token() === ts.SyntaxKind.ImportKeyword; } + let expression: ts.BooleanLiteral | ts.NullLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression = token() === ts.SyntaxKind.TrueKeyword || token() === ts.SyntaxKind.FalseKeyword || token() === ts.SyntaxKind.NullKeyword ? + parseTokenNode() : + parseLiteralLikeNode(token()) as ts.LiteralExpression; + if (negative) { + expression = finishNode(factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, expression), pos); + } + return finishNode(factory.createLiteralTypeNode(expression), pos); + } - function parseImportTypeAssertions(): ts.ImportTypeAssertionContainer { - const pos = getNodePos(); - const openBracePosition = scanner.getTokenPos(); - parseExpected(ts.SyntaxKind.OpenBraceToken); - const multiLine = scanner.hasPrecedingLineBreak(); - parseExpected(ts.SyntaxKind.AssertKeyword); - parseExpected(ts.SyntaxKind.ColonToken); - const clause = parseAssertClause(/*skipAssertKeyword*/ true); - if (!parseExpected(ts.SyntaxKind.CloseBraceToken)) { - const lastError = ts.lastOrUndefined(parseDiagnostics); - if (lastError && lastError.code === ts.Diagnostics._0_expected.code) { - ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openBracePosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, "{", "}")); - } + function isStartOfTypeOfImportType() { + nextToken(); + return token() === ts.SyntaxKind.ImportKeyword; + } + + function parseImportTypeAssertions(): ts.ImportTypeAssertionContainer { + const pos = getNodePos(); + const openBracePosition = scanner.getTokenPos(); + parseExpected(ts.SyntaxKind.OpenBraceToken); + const multiLine = scanner.hasPrecedingLineBreak(); + parseExpected(ts.SyntaxKind.AssertKeyword); + parseExpected(ts.SyntaxKind.ColonToken); + const clause = parseAssertClause(/*skipAssertKeyword*/ true); + if (!parseExpected(ts.SyntaxKind.CloseBraceToken)) { + const lastError = ts.lastOrUndefined(parseDiagnostics); + if (lastError && lastError.code === ts.Diagnostics._0_expected.code) { + ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openBracePosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, "{", "}")); } - return finishNode(factory.createImportTypeAssertionContainer(clause, multiLine), pos); } + return finishNode(factory.createImportTypeAssertionContainer(clause, multiLine), pos); + } - function parseImportType(): ts.ImportTypeNode { - sourceFlags |= ts.NodeFlags.PossiblyContainsDynamicImport; - const pos = getNodePos(); - const isTypeOf = parseOptional(ts.SyntaxKind.TypeOfKeyword); - parseExpected(ts.SyntaxKind.ImportKeyword); - parseExpected(ts.SyntaxKind.OpenParenToken); - const type = parseType(); - let assertions: ts.ImportTypeAssertionContainer | undefined; - if (parseOptional(ts.SyntaxKind.CommaToken)) { - assertions = parseImportTypeAssertions(); - } - parseExpected(ts.SyntaxKind.CloseParenToken); - const qualifier = parseOptional(ts.SyntaxKind.DotToken) ? parseEntityNameOfTypeReference() : undefined; - const typeArguments = parseTypeArgumentsOfTypeReference(); - return finishNode(factory.createImportTypeNode(type, assertions, qualifier, typeArguments, isTypeOf), pos); + function parseImportType(): ts.ImportTypeNode { + sourceFlags |= ts.NodeFlags.PossiblyContainsDynamicImport; + const pos = getNodePos(); + const isTypeOf = parseOptional(ts.SyntaxKind.TypeOfKeyword); + parseExpected(ts.SyntaxKind.ImportKeyword); + parseExpected(ts.SyntaxKind.OpenParenToken); + const type = parseType(); + let assertions: ts.ImportTypeAssertionContainer | undefined; + if (parseOptional(ts.SyntaxKind.CommaToken)) { + assertions = parseImportTypeAssertions(); + } + parseExpected(ts.SyntaxKind.CloseParenToken); + const qualifier = parseOptional(ts.SyntaxKind.DotToken) ? parseEntityNameOfTypeReference() : undefined; + const typeArguments = parseTypeArgumentsOfTypeReference(); + return finishNode(factory.createImportTypeNode(type, assertions, qualifier, typeArguments, isTypeOf), pos); + } + + function nextTokenIsNumericOrBigIntLiteral() { + nextToken(); + return token() === ts.SyntaxKind.NumericLiteral || token() === ts.SyntaxKind.BigIntLiteral; + } + + function parseNonArrayType(): ts.TypeNode { + switch (token()) { + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.UndefinedKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.ObjectKeyword: + // If these are followed by a dot, then parse these out as a dotted type reference instead. + return tryParse(parseKeywordAndNoDot) || parseTypeReference(); + case ts.SyntaxKind.AsteriskEqualsToken: + // If there is '*=', treat it as * followed by postfix = + scanner.reScanAsteriskEqualsToken(); + // falls through + case ts.SyntaxKind.AsteriskToken: + return parseJSDocAllType(); + case ts.SyntaxKind.QuestionQuestionToken: + // If there is '??', treat it as prefix-'?' in JSDoc type. + scanner.reScanQuestionToken(); + // falls through + case ts.SyntaxKind.QuestionToken: + return parseJSDocUnknownOrNullableType(); + case ts.SyntaxKind.FunctionKeyword: + return parseJSDocFunctionType(); + case ts.SyntaxKind.ExclamationToken: + return parseJSDocNonNullableType(); + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + return parseLiteralTypeNode(); + case ts.SyntaxKind.MinusToken: + return lookAhead(nextTokenIsNumericOrBigIntLiteral) ? parseLiteralTypeNode(/*negative*/ true) : parseTypeReference(); + case ts.SyntaxKind.VoidKeyword: + return parseTokenNode(); + case ts.SyntaxKind.ThisKeyword: { + const thisKeyword = parseThisTypeNode(); + if (token() === ts.SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { + return parseThisTypePredicate(thisKeyword); + } + else { + return thisKeyword; + } + } + case ts.SyntaxKind.TypeOfKeyword: + return lookAhead(isStartOfTypeOfImportType) ? parseImportType() : parseTypeQuery(); + case ts.SyntaxKind.OpenBraceToken: + return lookAhead(isStartOfMappedType) ? parseMappedType() : parseTypeLiteral(); + case ts.SyntaxKind.OpenBracketToken: + return parseTupleType(); + case ts.SyntaxKind.OpenParenToken: + return parseParenthesizedType(); + case ts.SyntaxKind.ImportKeyword: + return parseImportType(); + case ts.SyntaxKind.AssertsKeyword: + return lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine) ? parseAssertsTypePredicate() : parseTypeReference(); + case ts.SyntaxKind.TemplateHead: + return parseTemplateType(); + default: + return parseTypeReference(); } + } - function nextTokenIsNumericOrBigIntLiteral() { - nextToken(); - return token() === ts.SyntaxKind.NumericLiteral || token() === ts.SyntaxKind.BigIntLiteral; + function isStartOfType(inStartOfParameter?: boolean): boolean { + switch (token()) { + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.UniqueKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.UndefinedKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.TypeOfKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.OpenBraceToken: + case ts.SyntaxKind.OpenBracketToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.NewKeyword: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.QuestionToken: + case ts.SyntaxKind.ExclamationToken: + case ts.SyntaxKind.DotDotDotToken: + case ts.SyntaxKind.InferKeyword: + case ts.SyntaxKind.ImportKeyword: + case ts.SyntaxKind.AssertsKeyword: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateHead: + return true; + case ts.SyntaxKind.FunctionKeyword: + return !inStartOfParameter; + case ts.SyntaxKind.MinusToken: + return !inStartOfParameter && lookAhead(nextTokenIsNumericOrBigIntLiteral); + case ts.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() === ts.SyntaxKind.CloseParenToken || isStartOfParameter(/*isJSDocParameter*/ false) || isStartOfType(); + } - function parseNonArrayType(): ts.TypeNode { + function parsePostfixTypeOrHigher(): ts.TypeNode { + const pos = getNodePos(); + let type = parseNonArrayType(); + while (!scanner.hasPrecedingLineBreak()) { switch (token()) { - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.UnknownKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.UndefinedKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.ObjectKeyword: - // If these are followed by a dot, then parse these out as a dotted type reference instead. - return tryParse(parseKeywordAndNoDot) || parseTypeReference(); - case ts.SyntaxKind.AsteriskEqualsToken: - // If there is '*=', treat it as * followed by postfix = - scanner.reScanAsteriskEqualsToken(); - // falls through - case ts.SyntaxKind.AsteriskToken: - return parseJSDocAllType(); - case ts.SyntaxKind.QuestionQuestionToken: - // If there is '??', treat it as prefix-'?' in JSDoc type. - scanner.reScanQuestionToken(); - // falls through - case ts.SyntaxKind.QuestionToken: - return parseJSDocUnknownOrNullableType(); - case ts.SyntaxKind.FunctionKeyword: - return parseJSDocFunctionType(); case ts.SyntaxKind.ExclamationToken: - return parseJSDocNonNullableType(); - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NullKeyword: - return parseLiteralTypeNode(); - case ts.SyntaxKind.MinusToken: - return lookAhead(nextTokenIsNumericOrBigIntLiteral) ? parseLiteralTypeNode(/*negative*/ true) : parseTypeReference(); - case ts.SyntaxKind.VoidKeyword: - return parseTokenNode(); - case ts.SyntaxKind.ThisKeyword: { - const thisKeyword = parseThisTypeNode(); - if (token() === ts.SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { - return parseThisTypePredicate(thisKeyword); + nextToken(); + type = finishNode(factory.createJSDocNonNullableType(type, /*postfix*/ true), pos); + break; + case ts.SyntaxKind.QuestionToken: + // If next token is start of a type we have a conditional type + if (lookAhead(nextTokenIsStartOfType)) { + return type; + } + nextToken(); + type = finishNode(factory.createJSDocNullableType(type, /*postfix*/ true), pos); + break; + case ts.SyntaxKind.OpenBracketToken: + parseExpected(ts.SyntaxKind.OpenBracketToken); + if (isStartOfType()) { + const indexType = parseType(); + parseExpected(ts.SyntaxKind.CloseBracketToken); + type = finishNode(factory.createIndexedAccessTypeNode(type, indexType), pos); } else { - return thisKeyword; + parseExpected(ts.SyntaxKind.CloseBracketToken); + type = finishNode(factory.createArrayTypeNode(type), pos); } - } - case ts.SyntaxKind.TypeOfKeyword: - return lookAhead(isStartOfTypeOfImportType) ? parseImportType() : parseTypeQuery(); - case ts.SyntaxKind.OpenBraceToken: - return lookAhead(isStartOfMappedType) ? parseMappedType() : parseTypeLiteral(); - case ts.SyntaxKind.OpenBracketToken: - return parseTupleType(); - case ts.SyntaxKind.OpenParenToken: - return parseParenthesizedType(); - case ts.SyntaxKind.ImportKeyword: - return parseImportType(); - case ts.SyntaxKind.AssertsKeyword: - return lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine) ? parseAssertsTypePredicate() : parseTypeReference(); - case ts.SyntaxKind.TemplateHead: - return parseTemplateType(); + break; default: - return parseTypeReference(); + return type; } } + return type; + } - function isStartOfType(inStartOfParameter?: boolean): boolean { - switch (token()) { - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.UnknownKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.UniqueKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.UndefinedKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.TypeOfKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.OpenBraceToken: - case ts.SyntaxKind.OpenBracketToken: - case ts.SyntaxKind.LessThanToken: - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.NewKeyword: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.AsteriskToken: - case ts.SyntaxKind.QuestionToken: - case ts.SyntaxKind.ExclamationToken: - case ts.SyntaxKind.DotDotDotToken: - case ts.SyntaxKind.InferKeyword: - case ts.SyntaxKind.ImportKeyword: - case ts.SyntaxKind.AssertsKeyword: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TemplateHead: - return true; - case ts.SyntaxKind.FunctionKeyword: - return !inStartOfParameter; - case ts.SyntaxKind.MinusToken: - return !inStartOfParameter && lookAhead(nextTokenIsNumericOrBigIntLiteral); - case ts.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 parseTypeOperator(operator: ts.SyntaxKind.KeyOfKeyword | ts.SyntaxKind.UniqueKeyword | ts.SyntaxKind.ReadonlyKeyword) { + const pos = getNodePos(); + parseExpected(operator); + return finishNode(factory.createTypeOperatorNode(operator, parseTypeOperatorOrHigher()), pos); + } + + function tryParseConstraintOfInferType() { + if (parseOptional(ts.SyntaxKind.ExtendsKeyword)) { + const constraint = disallowConditionalTypesAnd(parseType); + if (inDisallowConditionalTypesContext() || token() !== ts.SyntaxKind.QuestionToken) { + return constraint; } } + } - function isStartOfParenthesizedOrFunctionType() { - nextToken(); - return token() === ts.SyntaxKind.CloseParenToken || isStartOfParameter(/*isJSDocParameter*/ false) || isStartOfType(); - } + function parseTypeParameterOfInferType(): ts.TypeParameterDeclaration { + const pos = getNodePos(); + const name = parseIdentifier(); + const constraint = tryParse(tryParseConstraintOfInferType); + const node = factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, constraint); + return finishNode(node, pos); + } - function parsePostfixTypeOrHigher(): ts.TypeNode { - const pos = getNodePos(); - let type = parseNonArrayType(); - while (!scanner.hasPrecedingLineBreak()) { - switch (token()) { - case ts.SyntaxKind.ExclamationToken: - nextToken(); - type = finishNode(factory.createJSDocNonNullableType(type, /*postfix*/ true), pos); - break; - case ts.SyntaxKind.QuestionToken: - // If next token is start of a type we have a conditional type - if (lookAhead(nextTokenIsStartOfType)) { - return type; - } - nextToken(); - type = finishNode(factory.createJSDocNullableType(type, /*postfix*/ true), pos); - break; - case ts.SyntaxKind.OpenBracketToken: - parseExpected(ts.SyntaxKind.OpenBracketToken); - if (isStartOfType()) { - const indexType = parseType(); - parseExpected(ts.SyntaxKind.CloseBracketToken); - type = finishNode(factory.createIndexedAccessTypeNode(type, indexType), pos); - } - else { - parseExpected(ts.SyntaxKind.CloseBracketToken); - type = finishNode(factory.createArrayTypeNode(type), pos); - } - break; - default: - return type; - } - } - return type; - } + function parseInferType(): ts.InferTypeNode { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.InferKeyword); + return finishNode(factory.createInferTypeNode(parseTypeParameterOfInferType()), pos); + } - function parseTypeOperator(operator: ts.SyntaxKind.KeyOfKeyword | ts.SyntaxKind.UniqueKeyword | ts.SyntaxKind.ReadonlyKeyword) { - const pos = getNodePos(); - parseExpected(operator); - return finishNode(factory.createTypeOperatorNode(operator, parseTypeOperatorOrHigher()), pos); - } + function parseTypeOperatorOrHigher(): ts.TypeNode { + const operator = token(); + switch (operator) { + case ts.SyntaxKind.KeyOfKeyword: + case ts.SyntaxKind.UniqueKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + return parseTypeOperator(operator); + case ts.SyntaxKind.InferKeyword: + return parseInferType(); + } + return allowConditionalTypesAnd(parsePostfixTypeOrHigher); + } - function tryParseConstraintOfInferType() { - if (parseOptional(ts.SyntaxKind.ExtendsKeyword)) { - const constraint = disallowConditionalTypesAnd(parseType); - if (inDisallowConditionalTypesContext() || token() !== ts.SyntaxKind.QuestionToken) { - return constraint; - } + function parseFunctionOrConstructorTypeToError(isInUnionType: boolean): ts.TypeNode | undefined { + // the function type and constructor type shorthand notation + // are not allowed directly in unions and intersections, but we'll + // try to parse them gracefully and issue a helpful message. + if (isStartOfFunctionTypeOrConstructorType()) { + const type = parseFunctionOrConstructorType(); + let diagnostic: ts.DiagnosticMessage; + if (ts.isFunctionTypeNode(type)) { + diagnostic = isInUnionType + ? ts.Diagnostics.Function_type_notation_must_be_parenthesized_when_used_in_a_union_type + : ts.Diagnostics.Function_type_notation_must_be_parenthesized_when_used_in_an_intersection_type; } - } - - function parseTypeParameterOfInferType(): ts.TypeParameterDeclaration { - const pos = getNodePos(); - const name = parseIdentifier(); - const constraint = tryParse(tryParseConstraintOfInferType); - const node = factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, constraint); - return finishNode(node, pos); - } + else { + diagnostic = isInUnionType + ? ts.Diagnostics.Constructor_type_notation_must_be_parenthesized_when_used_in_a_union_type + : ts.Diagnostics.Constructor_type_notation_must_be_parenthesized_when_used_in_an_intersection_type; - function parseInferType(): ts.InferTypeNode { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.InferKeyword); - return finishNode(factory.createInferTypeNode(parseTypeParameterOfInferType()), pos); + } + parseErrorAtRange(type, diagnostic); + return type; } + return undefined; + } - function parseTypeOperatorOrHigher(): ts.TypeNode { - const operator = token(); - switch (operator) { - case ts.SyntaxKind.KeyOfKeyword: - case ts.SyntaxKind.UniqueKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - return parseTypeOperator(operator); - case ts.SyntaxKind.InferKeyword: - return parseInferType(); + function parseUnionOrIntersectionType(operator: ts.SyntaxKind.BarToken | ts.SyntaxKind.AmpersandToken, parseConstituentType: () => ts.TypeNode, createTypeNode: (types: ts.NodeArray) => ts.UnionOrIntersectionTypeNode): ts.TypeNode { + const pos = getNodePos(); + const isUnionType = operator === ts.SyntaxKind.BarToken; + const hasLeadingOperator = parseOptional(operator); + let type = hasLeadingOperator && parseFunctionOrConstructorTypeToError(isUnionType) + || parseConstituentType(); + if (token() === operator || hasLeadingOperator) { + const types = [type]; + while (parseOptional(operator)) { + types.push(parseFunctionOrConstructorTypeToError(isUnionType) || parseConstituentType()); } - return allowConditionalTypesAnd(parsePostfixTypeOrHigher); + type = finishNode(createTypeNode(createNodeArray(types, pos)), pos); } + return type; + } - function parseFunctionOrConstructorTypeToError(isInUnionType: boolean): ts.TypeNode | undefined { - // the function type and constructor type shorthand notation - // are not allowed directly in unions and intersections, but we'll - // try to parse them gracefully and issue a helpful message. - if (isStartOfFunctionTypeOrConstructorType()) { - const type = parseFunctionOrConstructorType(); - let diagnostic: ts.DiagnosticMessage; - if (ts.isFunctionTypeNode(type)) { - diagnostic = isInUnionType - ? ts.Diagnostics.Function_type_notation_must_be_parenthesized_when_used_in_a_union_type - : ts.Diagnostics.Function_type_notation_must_be_parenthesized_when_used_in_an_intersection_type; - } - else { - diagnostic = isInUnionType - ? ts.Diagnostics.Constructor_type_notation_must_be_parenthesized_when_used_in_a_union_type - : ts.Diagnostics.Constructor_type_notation_must_be_parenthesized_when_used_in_an_intersection_type; + function parseIntersectionTypeOrHigher(): ts.TypeNode { + return parseUnionOrIntersectionType(ts.SyntaxKind.AmpersandToken, parseTypeOperatorOrHigher, factory.createIntersectionTypeNode); + } - } - parseErrorAtRange(type, diagnostic); - return type; - } - return undefined; - } + function parseUnionTypeOrHigher(): ts.TypeNode { + return parseUnionOrIntersectionType(ts.SyntaxKind.BarToken, parseIntersectionTypeOrHigher, factory.createUnionTypeNode); + } - function parseUnionOrIntersectionType(operator: ts.SyntaxKind.BarToken | ts.SyntaxKind.AmpersandToken, parseConstituentType: () => ts.TypeNode, createTypeNode: (types: ts.NodeArray) => ts.UnionOrIntersectionTypeNode): ts.TypeNode { - const pos = getNodePos(); - const isUnionType = operator === ts.SyntaxKind.BarToken; - const hasLeadingOperator = parseOptional(operator); - let type = hasLeadingOperator && parseFunctionOrConstructorTypeToError(isUnionType) - || parseConstituentType(); - if (token() === operator || hasLeadingOperator) { - const types = [type]; - while (parseOptional(operator)) { - types.push(parseFunctionOrConstructorTypeToError(isUnionType) || parseConstituentType()); - } - type = finishNode(createTypeNode(createNodeArray(types, pos)), pos); - } - return type; - } + function nextTokenIsNewKeyword(): boolean { + nextToken(); + return token() === ts.SyntaxKind.NewKeyword; + } - function parseIntersectionTypeOrHigher(): ts.TypeNode { - return parseUnionOrIntersectionType(ts.SyntaxKind.AmpersandToken, parseTypeOperatorOrHigher, factory.createIntersectionTypeNode); + function isStartOfFunctionTypeOrConstructorType(): boolean { + if (token() === ts.SyntaxKind.LessThanToken) { + return true; } - - function parseUnionTypeOrHigher(): ts.TypeNode { - return parseUnionOrIntersectionType(ts.SyntaxKind.BarToken, parseIntersectionTypeOrHigher, factory.createUnionTypeNode); + if (token() === ts.SyntaxKind.OpenParenToken && lookAhead(isUnambiguouslyStartOfFunctionType)) { + return true; } + return token() === ts.SyntaxKind.NewKeyword || + token() === ts.SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsNewKeyword); + } - function nextTokenIsNewKeyword(): boolean { + function skipParameterStart(): boolean { + if (ts.isModifierKind(token())) { + // Skip modifiers + parseModifiers(); + } + if (isIdentifier() || token() === ts.SyntaxKind.ThisKeyword) { nextToken(); - return token() === ts.SyntaxKind.NewKeyword; + return true; } - - function isStartOfFunctionTypeOrConstructorType(): boolean { - if (token() === ts.SyntaxKind.LessThanToken) { - return true; - } - if (token() === ts.SyntaxKind.OpenParenToken && lookAhead(isUnambiguouslyStartOfFunctionType)) { - return true; - } - return token() === ts.SyntaxKind.NewKeyword || - token() === ts.SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsNewKeyword); + if (token() === ts.SyntaxKind.OpenBracketToken || token() === ts.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 skipParameterStart(): boolean { - if (ts.isModifierKind(token())) { - // Skip modifiers - parseModifiers(); - } - if (isIdentifier() || token() === ts.SyntaxKind.ThisKeyword) { - nextToken(); - return true; - } - if (token() === ts.SyntaxKind.OpenBracketToken || token() === ts.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() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.DotDotDotToken) { + // ( ) + // ( ... + return true; } - - function isUnambiguouslyStartOfFunctionType() { - nextToken(); - if (token() === ts.SyntaxKind.CloseParenToken || token() === ts.SyntaxKind.DotDotDotToken) { - // ( ) - // ( ... + 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() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || + token() === ts.SyntaxKind.QuestionToken || token() === ts.SyntaxKind.EqualsToken) { + // ( xxx : + // ( xxx , + // ( xxx ? + // ( xxx = 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() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || - token() === ts.SyntaxKind.QuestionToken || token() === ts.SyntaxKind.EqualsToken) { - // ( xxx : - // ( xxx , - // ( xxx ? - // ( xxx = + if (token() === ts.SyntaxKind.CloseParenToken) { + nextToken(); + if (token() === ts.SyntaxKind.EqualsGreaterThanToken) { + // ( xxx ) => return true; } - if (token() === ts.SyntaxKind.CloseParenToken) { - nextToken(); - if (token() === ts.SyntaxKind.EqualsGreaterThanToken) { - // ( xxx ) => - return true; - } - } } - return false; } + return false; + } - function parseTypeOrTypePredicate(): ts.TypeNode { - const pos = getNodePos(); - const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix); - const type = parseType(); - if (typePredicateVariable) { - return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, type), pos); - } - else { - return type; - } + function parseTypeOrTypePredicate(): ts.TypeNode { + const pos = getNodePos(); + const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix); + const type = parseType(); + if (typePredicateVariable) { + return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, type), pos); + } + else { + return type; } + } - function parseTypePredicatePrefix() { - const id = parseIdentifier(); - if (token() === ts.SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { - nextToken(); - return id; - } + function parseTypePredicatePrefix() { + const id = parseIdentifier(); + if (token() === ts.SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { + nextToken(); + return id; } + } - function parseAssertsTypePredicate(): ts.TypeNode { - const pos = getNodePos(); - const assertsModifier = parseExpectedToken(ts.SyntaxKind.AssertsKeyword); - const parameterName = token() === ts.SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier(); - const type = parseOptional(ts.SyntaxKind.IsKeyword) ? parseType() : undefined; - return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type), pos); + function parseAssertsTypePredicate(): ts.TypeNode { + const pos = getNodePos(); + const assertsModifier = parseExpectedToken(ts.SyntaxKind.AssertsKeyword); + const parameterName = token() === ts.SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier(); + const type = parseOptional(ts.SyntaxKind.IsKeyword) ? parseType() : undefined; + return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type), pos); + } + + function parseType(): ts.TypeNode { + if (contextFlags & ts.NodeFlags.TypeExcludesFlags) { + return doOutsideOfContext(ts.NodeFlags.TypeExcludesFlags, parseType); } - function parseType(): ts.TypeNode { - if (contextFlags & ts.NodeFlags.TypeExcludesFlags) { - return doOutsideOfContext(ts.NodeFlags.TypeExcludesFlags, parseType); - } + if (isStartOfFunctionTypeOrConstructorType()) { + return parseFunctionOrConstructorType(); + } + const pos = getNodePos(); + const type = parseUnionTypeOrHigher(); + if (!inDisallowConditionalTypesContext() && !scanner.hasPrecedingLineBreak() && parseOptional(ts.SyntaxKind.ExtendsKeyword)) { + // The type following 'extends' is not permitted to be another conditional type + const extendsType = disallowConditionalTypesAnd(parseType); + parseExpected(ts.SyntaxKind.QuestionToken); + const trueType = allowConditionalTypesAnd(parseType); + parseExpected(ts.SyntaxKind.ColonToken); + const falseType = allowConditionalTypesAnd(parseType); + return finishNode(factory.createConditionalTypeNode(type, extendsType, trueType, falseType), pos); + } + return type; + } - if (isStartOfFunctionTypeOrConstructorType()) { - return parseFunctionOrConstructorType(); - } - const pos = getNodePos(); - const type = parseUnionTypeOrHigher(); - if (!inDisallowConditionalTypesContext() && !scanner.hasPrecedingLineBreak() && parseOptional(ts.SyntaxKind.ExtendsKeyword)) { - // The type following 'extends' is not permitted to be another conditional type - const extendsType = disallowConditionalTypesAnd(parseType); - parseExpected(ts.SyntaxKind.QuestionToken); - const trueType = allowConditionalTypesAnd(parseType); - parseExpected(ts.SyntaxKind.ColonToken); - const falseType = allowConditionalTypesAnd(parseType); - return finishNode(factory.createConditionalTypeNode(type, extendsType, trueType, falseType), pos); - } - return type; + function parseTypeAnnotation(): ts.TypeNode | undefined { + return parseOptional(ts.SyntaxKind.ColonToken) ? parseType() : undefined; + } + + // EXPRESSIONS + function isStartOfLeftHandSideExpression(): boolean { + switch (token()) { + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.OpenParenToken: + case ts.SyntaxKind.OpenBracketToken: + case ts.SyntaxKind.OpenBraceToken: + case ts.SyntaxKind.FunctionKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.NewKeyword: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.SlashEqualsToken: + case ts.SyntaxKind.Identifier: + return true; + case ts.SyntaxKind.ImportKeyword: + return lookAhead(nextTokenIsOpenParenOrLessThanOrDot); + default: + return isIdentifier(); } + } - function parseTypeAnnotation(): ts.TypeNode | undefined { - return parseOptional(ts.SyntaxKind.ColonToken) ? parseType() : undefined; + function isStartOfExpression(): boolean { + if (isStartOfLeftHandSideExpression()) { + return true; } - // EXPRESSIONS - function isStartOfLeftHandSideExpression(): boolean { - switch (token()) { - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.SuperKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TemplateHead: - case ts.SyntaxKind.OpenParenToken: - case ts.SyntaxKind.OpenBracketToken: - case ts.SyntaxKind.OpenBraceToken: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.ClassKeyword: - case ts.SyntaxKind.NewKeyword: - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.SlashEqualsToken: - case ts.SyntaxKind.Identifier: + switch (token()) { + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.TildeToken: + case ts.SyntaxKind.ExclamationToken: + case ts.SyntaxKind.DeleteKeyword: + case ts.SyntaxKind.TypeOfKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.PlusPlusToken: + case ts.SyntaxKind.MinusMinusToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.AwaitKeyword: + case ts.SyntaxKind.YieldKeyword: + case ts.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; - case ts.SyntaxKind.ImportKeyword: - return lookAhead(nextTokenIsOpenParenOrLessThanOrDot); - default: - return isIdentifier(); - } + } + + return isIdentifier(); } + } - function isStartOfExpression(): boolean { - if (isStartOfLeftHandSideExpression()) { - return true; - } + function isStartOfExpressionStatement(): boolean { + // As per the grammar, none of '{' or 'function' or 'class' can start an expression statement. + return token() !== ts.SyntaxKind.OpenBraceToken && + token() !== ts.SyntaxKind.FunctionKeyword && + token() !== ts.SyntaxKind.ClassKeyword && + token() !== ts.SyntaxKind.AtToken && + isStartOfExpression(); + } - switch (token()) { - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.TildeToken: - case ts.SyntaxKind.ExclamationToken: - case ts.SyntaxKind.DeleteKeyword: - case ts.SyntaxKind.TypeOfKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.PlusPlusToken: - case ts.SyntaxKind.MinusMinusToken: - case ts.SyntaxKind.LessThanToken: - case ts.SyntaxKind.AwaitKeyword: - case ts.SyntaxKind.YieldKeyword: - case ts.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; - } + function parseExpression(): ts.Expression { + // Expression[in]: + // AssignmentExpression[in] + // Expression[in] , AssignmentExpression[in] - return isIdentifier(); - } + // clear the decorator context when parsing Expression, as it should be unambiguous when parsing a decorator + const saveDecoratorContext = inDecoratorContext(); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ false); } - function isStartOfExpressionStatement(): boolean { - // As per the grammar, none of '{' or 'function' or 'class' can start an expression statement. - return token() !== ts.SyntaxKind.OpenBraceToken && - token() !== ts.SyntaxKind.FunctionKeyword && - token() !== ts.SyntaxKind.ClassKeyword && - token() !== ts.SyntaxKind.AtToken && - isStartOfExpression(); + const pos = getNodePos(); + let expr = parseAssignmentExpressionOrHigher(); + let operatorToken: ts.BinaryOperatorToken; + while ((operatorToken = parseOptionalToken(ts.SyntaxKind.CommaToken))) { + expr = makeBinaryExpression(expr, operatorToken, parseAssignmentExpressionOrHigher(), pos); } - function parseExpression(): ts.Expression { - // Expression[in]: - // AssignmentExpression[in] - // Expression[in] , AssignmentExpression[in] + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ true); + } + return expr; + } - // clear the decorator context when parsing Expression, as it should be unambiguous when parsing a decorator - const saveDecoratorContext = inDecoratorContext(); - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ false); - } + function parseInitializer(): ts.Expression | undefined { + return parseOptional(ts.SyntaxKind.EqualsToken) ? parseAssignmentExpressionOrHigher() : undefined; + } - const pos = getNodePos(); - let expr = parseAssignmentExpressionOrHigher(); - let operatorToken: ts.BinaryOperatorToken; - while ((operatorToken = parseOptionalToken(ts.SyntaxKind.CommaToken))) { - expr = makeBinaryExpression(expr, operatorToken, parseAssignmentExpressionOrHigher(), pos); - } + function parseAssignmentExpressionOrHigher(): ts.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). - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ true); - } - return expr; + // First, do the simple check if we have a YieldExpression (production '6'). + if (isYieldExpression()) { + return parseYieldExpression(); } - function parseInitializer(): ts.Expression | undefined { - return parseOptional(ts.SyntaxKind.EqualsToken) ? parseAssignmentExpressionOrHigher() : undefined; + // 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; } - function parseAssignmentExpressionOrHigher(): ts.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). + // 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 pos = getNodePos(); + const expr = parseBinaryExpressionOrHigher(ts.OperatorPrecedence.Lowest); + + // 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 === ts.SyntaxKind.Identifier && token() === ts.SyntaxKind.EqualsGreaterThanToken) { + return parseSimpleArrowFunctionExpression(pos, expr as ts.Identifier, /*asyncModifier*/ undefined); + } + + // 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 (ts.isLeftHandSideExpression(expr) && ts.isAssignmentOperator(reScanGreaterToken())) { + return makeBinaryExpression(expr, parseTokenNode(), parseAssignmentExpressionOrHigher(), pos); + } - // First, do the simple check if we have a YieldExpression (production '6'). - if (isYieldExpression()) { - return parseYieldExpression(); - } + // It wasn't an assignment or a lambda. This is a conditional expression: + return parseConditionalExpressionRest(expr, pos); + } - // 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; + function isYieldExpression(): boolean { + if (token() === ts.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; } - // 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. + // 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. // - // 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 pos = getNodePos(); - const expr = parseBinaryExpressionOrHigher(ts.OperatorPrecedence.Lowest); - - // 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 === ts.SyntaxKind.Identifier && token() === ts.SyntaxKind.EqualsGreaterThanToken) { - return parseSimpleArrowFunctionExpression(pos, expr as ts.Identifier, /*asyncModifier*/ undefined); - } - - // 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. + // 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. // - // Note: we call reScanGreaterToken so that we get an appropriately merged token - // for cases like `> > =` becoming `>>=` - if (ts.isLeftHandSideExpression(expr) && ts.isAssignmentOperator(reScanGreaterToken())) { - return makeBinaryExpression(expr, parseTokenNode(), parseAssignmentExpressionOrHigher(), pos); - } - - // It wasn't an assignment or a lambda. This is a conditional expression: - return parseConditionalExpressionRest(expr, pos); + // 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 isYieldExpression(): boolean { - if (token() === ts.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; - } + return false; + } - // 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 nextTokenIsIdentifierOnSameLine() { + nextToken(); + return !scanner.hasPrecedingLineBreak() && isIdentifier(); + } - return false; - } + function parseYieldExpression(): ts.YieldExpression { + const pos = getNodePos(); - function nextTokenIsIdentifierOnSameLine() { - nextToken(); - return !scanner.hasPrecedingLineBreak() && isIdentifier(); - } + // YieldExpression[In] : + // yield + // yield [no LineTerminator here] [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] + // yield [no LineTerminator here] * [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] + nextToken(); - function parseYieldExpression(): ts.YieldExpression { - const pos = getNodePos(); + if (!scanner.hasPrecedingLineBreak() && + (token() === ts.SyntaxKind.AsteriskToken || isStartOfExpression())) { + return finishNode(factory.createYieldExpression(parseOptionalToken(ts.SyntaxKind.AsteriskToken), parseAssignmentExpressionOrHigher()), pos); + } + 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(factory.createYieldExpression(/*asteriskToken*/ undefined, /*expression*/ undefined), pos); + } + } - // YieldExpression[In] : - // yield - // yield [no LineTerminator here] [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] - // yield [no LineTerminator here] * [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] - nextToken(); + function parseSimpleArrowFunctionExpression(pos: number, identifier: ts.Identifier, asyncModifier?: ts.NodeArray | undefined): ts.ArrowFunction { + ts.Debug.assert(token() === ts.SyntaxKind.EqualsGreaterThanToken, "parseSimpleArrowFunctionExpression should only have been called if we had a =>"); + const parameter = factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, identifier, + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined); + finishNode(parameter, identifier.pos); + + const parameters = createNodeArray([parameter], parameter.pos, parameter.end); + const equalsGreaterThanToken = parseExpectedToken(ts.SyntaxKind.EqualsGreaterThanToken); + const body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); + const node = factory.createArrowFunction(asyncModifier, /*typeParameters*/ undefined, parameters, /*type*/ undefined, equalsGreaterThanToken, body); + return addJSDocComment(finishNode(node, pos)); + } - if (!scanner.hasPrecedingLineBreak() && - (token() === ts.SyntaxKind.AsteriskToken || isStartOfExpression())) { - return finishNode(factory.createYieldExpression(parseOptionalToken(ts.SyntaxKind.AsteriskToken), parseAssignmentExpressionOrHigher()), pos); - } - 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(factory.createYieldExpression(/*asteriskToken*/ undefined, /*expression*/ undefined), pos); - } + function tryParseParenthesizedArrowFunctionExpression(): ts.Expression | undefined { + const triState = isParenthesizedArrowFunctionExpression(); + if (triState === Tristate.False) { + // It's definitely not a parenthesized arrow function expression. + return undefined; } - function parseSimpleArrowFunctionExpression(pos: number, identifier: ts.Identifier, asyncModifier?: ts.NodeArray | undefined): ts.ArrowFunction { - ts.Debug.assert(token() === ts.SyntaxKind.EqualsGreaterThanToken, "parseSimpleArrowFunctionExpression should only have been called if we had a =>"); - const parameter = factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, identifier, - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined); - finishNode(parameter, identifier.pos); + // 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. + return triState === Tristate.True ? + parseParenthesizedArrowFunctionExpression(/*allowAmbiguity*/ true) : + tryParse(parsePossibleParenthesizedArrowFunctionExpression); + } - const parameters = createNodeArray([parameter], parameter.pos, parameter.end); - const equalsGreaterThanToken = parseExpectedToken(ts.SyntaxKind.EqualsGreaterThanToken); - const body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); - const node = factory.createArrowFunction(asyncModifier, /*typeParameters*/ undefined, parameters, /*type*/ undefined, equalsGreaterThanToken, body); - return addJSDocComment(finishNode(node, pos)); + // 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() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken || token() === ts.SyntaxKind.AsyncKeyword) { + return lookAhead(isParenthesizedArrowFunctionExpressionWorker); } - function tryParseParenthesizedArrowFunctionExpression(): ts.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. - return triState === Tristate.True ? - parseParenthesizedArrowFunctionExpression(/*allowAmbiguity*/ true) : - tryParse(parsePossibleParenthesizedArrowFunctionExpression); + if (token() === ts.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; } + // Definitely not a parenthesized arrow function. + return Tristate.False; + } - // 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() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken || token() === ts.SyntaxKind.AsyncKeyword) { - return lookAhead(isParenthesizedArrowFunctionExpressionWorker); + function isParenthesizedArrowFunctionExpressionWorker() { + if (token() === ts.SyntaxKind.AsyncKeyword) { + nextToken(); + if (scanner.hasPrecedingLineBreak()) { + return Tristate.False; } - - if (token() === ts.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() !== ts.SyntaxKind.OpenParenToken && token() !== ts.SyntaxKind.LessThanToken) { + return Tristate.False; } - // Definitely not a parenthesized arrow function. - return Tristate.False; } - function isParenthesizedArrowFunctionExpressionWorker() { - if (token() === ts.SyntaxKind.AsyncKeyword) { - nextToken(); - if (scanner.hasPrecedingLineBreak()) { - return Tristate.False; - } - if (token() !== ts.SyntaxKind.OpenParenToken && token() !== ts.SyntaxKind.LessThanToken) { - return Tristate.False; - } - } - - const first = token(); - const second = nextToken(); - - if (first === ts.SyntaxKind.OpenParenToken) { - if (second === ts.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 ts.SyntaxKind.EqualsGreaterThanToken: - case ts.SyntaxKind.ColonToken: - case ts.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 === ts.SyntaxKind.OpenBracketToken || second === ts.SyntaxKind.OpenBraceToken) { - return Tristate.Unknown; - } - - // Simple case: "(..." - // This is an arrow function with a rest parameter. - if (second === ts.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 (ts.isModifierKind(second) && second !== ts.SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsIdentifier)) { - if (nextToken() === ts.SyntaxKind.AsKeyword) { - // https://github.com/microsoft/TypeScript/issues/44466 - return Tristate.False; - } - 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 !== ts.SyntaxKind.ThisKeyword) { - return Tristate.False; - } + const first = token(); + const second = nextToken(); - switch (nextToken()) { + if (first === ts.SyntaxKind.OpenParenToken) { + if (second === ts.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 ts.SyntaxKind.EqualsGreaterThanToken: case ts.SyntaxKind.ColonToken: - // If we have something like "(a:", then we must have a - // type-annotated parameter in an arrow function expression. + case ts.SyntaxKind.OpenBraceToken: return Tristate.True; - case ts.SyntaxKind.QuestionToken: - nextToken(); - // If we have "(a?:" or "(a?," or "(a?=" or "(a?)" then it is definitely a lambda. - if (token() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.EqualsToken || token() === ts.SyntaxKind.CloseParenToken) { - return Tristate.True; - } - // Otherwise it is definitely not a lambda. + default: return Tristate.False; - case ts.SyntaxKind.CommaToken: - case ts.SyntaxKind.EqualsToken: - case ts.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 { - ts.Debug.assert(first === ts.SyntaxKind.LessThanToken); - // If we have "<" not followed by an identifier, - // then this definitely is not an arrow function. - if (!isIdentifier()) { + // If encounter "([" or "({", this could be the start of a binding pattern. + // Examples: + // ([ x ]) => { } + // ({ x }) => { } + // ([ x ]) + // ({ x }) + if (second === ts.SyntaxKind.OpenBracketToken || second === ts.SyntaxKind.OpenBraceToken) { + return Tristate.Unknown; + } + + // Simple case: "(..." + // This is an arrow function with a rest parameter. + if (second === ts.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 (ts.isModifierKind(second) && second !== ts.SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsIdentifier)) { + if (nextToken() === ts.SyntaxKind.AsKeyword) { + // https://github.com/microsoft/TypeScript/issues/44466 return Tristate.False; } + return Tristate.True; + } - // JSX overrides - if (languageVariant === ts.LanguageVariant.JSX) { - const isArrowFunctionInJsx = lookAhead(() => { - const third = nextToken(); - if (third === ts.SyntaxKind.ExtendsKeyword) { - const fourth = nextToken(); - switch (fourth) { - case ts.SyntaxKind.EqualsToken: - case ts.SyntaxKind.GreaterThanToken: - return false; - default: - return true; - } - } - else if (third === ts.SyntaxKind.CommaToken || third === ts.SyntaxKind.EqualsToken) { - return true; - } - return false; - }); + // 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 !== ts.SyntaxKind.ThisKeyword) { + return Tristate.False; + } - if (isArrowFunctionInJsx) { + switch (nextToken()) { + case ts.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 ts.SyntaxKind.QuestionToken: + nextToken(); + // If we have "(a?:" or "(a?," or "(a?=" or "(a?)" then it is definitely a lambda. + if (token() === ts.SyntaxKind.ColonToken || token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.EqualsToken || token() === ts.SyntaxKind.CloseParenToken) { return Tristate.True; } - + // Otherwise it is definitely not a lambda. return Tristate.False; - } - - // This *could* be a parenthesized arrow function. - return Tristate.Unknown; + case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.EqualsToken: + case ts.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 { + ts.Debug.assert(first === ts.SyntaxKind.LessThanToken); - function parsePossibleParenthesizedArrowFunctionExpression(): ts.ArrowFunction | undefined { - const tokenPos = scanner.getTokenPos(); - if (notParenthesizedArrow?.has(tokenPos)) { - return undefined; + // If we have "<" not followed by an identifier, + // then this definitely is not an arrow function. + if (!isIdentifier()) { + return Tristate.False; } - const result = parseParenthesizedArrowFunctionExpression(/*allowAmbiguity*/ false); - if (!result) { - (notParenthesizedArrow || (notParenthesizedArrow = new ts.Set())).add(tokenPos); + // JSX overrides + if (languageVariant === ts.LanguageVariant.JSX) { + const isArrowFunctionInJsx = lookAhead(() => { + const third = nextToken(); + if (third === ts.SyntaxKind.ExtendsKeyword) { + const fourth = nextToken(); + switch (fourth) { + case ts.SyntaxKind.EqualsToken: + case ts.SyntaxKind.GreaterThanToken: + return false; + default: + return true; + } + } + else if (third === ts.SyntaxKind.CommaToken || third === ts.SyntaxKind.EqualsToken) { + return true; + } + return false; + }); + + if (isArrowFunctionInJsx) { + return Tristate.True; + } + + return Tristate.False; } - return result; + // This *could* be a parenthesized arrow function. + return Tristate.Unknown; } + } - function tryParseAsyncSimpleArrowFunctionExpression(): ts.ArrowFunction | undefined { - // We do a check here so that we won't be doing unnecessarily call to "lookAhead" - if (token() === ts.SyntaxKind.AsyncKeyword) { - if (lookAhead(isUnParenthesizedAsyncArrowFunctionWorker) === Tristate.True) { - const pos = getNodePos(); - const asyncModifier = parseModifiersForArrowFunction(); - const expr = parseBinaryExpressionOrHigher(ts.OperatorPrecedence.Lowest); - return parseSimpleArrowFunctionExpression(pos, expr as ts.Identifier, asyncModifier); - } - } + function parsePossibleParenthesizedArrowFunctionExpression(): ts.ArrowFunction | undefined { + const tokenPos = scanner.getTokenPos(); + if (notParenthesizedArrow?.has(tokenPos)) { 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() === ts.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() === ts.SyntaxKind.EqualsGreaterThanToken) { - return Tristate.False; - } - // Check for un-parenthesized AsyncArrowFunction + const result = parseParenthesizedArrowFunctionExpression(/*allowAmbiguity*/ false); + if (!result) { + (notParenthesizedArrow || (notParenthesizedArrow = new ts.Set())).add(tokenPos); + } + + return result; + } + + function tryParseAsyncSimpleArrowFunctionExpression(): ts.ArrowFunction | undefined { + // We do a check here so that we won't be doing unnecessarily call to "lookAhead" + if (token() === ts.SyntaxKind.AsyncKeyword) { + if (lookAhead(isUnParenthesizedAsyncArrowFunctionWorker) === Tristate.True) { + const pos = getNodePos(); + const asyncModifier = parseModifiersForArrowFunction(); const expr = parseBinaryExpressionOrHigher(ts.OperatorPrecedence.Lowest); - if (!scanner.hasPrecedingLineBreak() && expr.kind === ts.SyntaxKind.Identifier && token() === ts.SyntaxKind.EqualsGreaterThanToken) { - return Tristate.True; - } + return parseSimpleArrowFunctionExpression(pos, expr as ts.Identifier, asyncModifier); } + } + return undefined; + } - return Tristate.False; + 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() === ts.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() === ts.SyntaxKind.EqualsGreaterThanToken) { + return Tristate.False; + } + // Check for un-parenthesized AsyncArrowFunction + const expr = parseBinaryExpressionOrHigher(ts.OperatorPrecedence.Lowest); + if (!scanner.hasPrecedingLineBreak() && expr.kind === ts.SyntaxKind.Identifier && token() === ts.SyntaxKind.EqualsGreaterThanToken) { + return Tristate.True; + } } - function parseParenthesizedArrowFunctionExpression(allowAmbiguity: boolean): ts.ArrowFunction | undefined { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const modifiers = parseModifiersForArrowFunction(); - const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? 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. - const typeParameters = parseTypeParameters(); + return Tristate.False; + } - let parameters: ts.NodeArray; - if (!parseExpected(ts.SyntaxKind.OpenParenToken)) { - if (!allowAmbiguity) { + function parseParenthesizedArrowFunctionExpression(allowAmbiguity: boolean): ts.ArrowFunction | undefined { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const modifiers = parseModifiersForArrowFunction(); + const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? 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. + const typeParameters = parseTypeParameters(); + + let parameters: ts.NodeArray; + if (!parseExpected(ts.SyntaxKind.OpenParenToken)) { + if (!allowAmbiguity) { + return undefined; + } + parameters = createMissingList(); + } + else { + if (!allowAmbiguity) { + const maybeParameters = parseParametersWorker(isAsync, allowAmbiguity); + if (!maybeParameters) { return undefined; } - parameters = createMissingList(); + parameters = maybeParameters; } else { - if (!allowAmbiguity) { - const maybeParameters = parseParametersWorker(isAsync, allowAmbiguity); - if (!maybeParameters) { - return undefined; - } - parameters = maybeParameters; - } - else { - parameters = parseParametersWorker(isAsync, allowAmbiguity); - } - if (!parseExpected(ts.SyntaxKind.CloseParenToken) && !allowAmbiguity) { - return undefined; - } + parameters = parseParametersWorker(isAsync, allowAmbiguity); } - - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - if (type && !allowAmbiguity && typeHasArrowFunctionBlockingParseError(type)) { + if (!parseExpected(ts.SyntaxKind.CloseParenToken) && !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. - // - "a ? (b): (function() {})" as well, but inside of a parenthesized type with an arbitrary amount of nesting. - // - // So we need just a bit of lookahead to ensure that it can only be a signature. - - let unwrappedType = type; - while (unwrappedType?.kind === ts.SyntaxKind.ParenthesizedType) { - unwrappedType = (unwrappedType as ts.ParenthesizedTypeNode).type; // Skip parens if need be - } + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + if (type && !allowAmbiguity && typeHasArrowFunctionBlockingParseError(type)) { + return undefined; + } - const hasJSDocFunctionType = unwrappedType && ts.isJSDocFunctionType(unwrappedType); - if (!allowAmbiguity && token() !== ts.SyntaxKind.EqualsGreaterThanToken && (hasJSDocFunctionType || token() !== ts.SyntaxKind.OpenBraceToken)) { - // Returning undefined here will cause our caller to rewind to where we started from. - 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. + // - "a ? (b): (function() {})" as well, but inside of a parenthesized type with an arbitrary amount of nesting. + // + // So we need just a bit of lookahead to ensure that it can only be a signature. - // 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(); - const equalsGreaterThanToken = parseExpectedToken(ts.SyntaxKind.EqualsGreaterThanToken); - const body = (lastToken === ts.SyntaxKind.EqualsGreaterThanToken || lastToken === ts.SyntaxKind.OpenBraceToken) - ? parseArrowFunctionExpressionBody(ts.some(modifiers, ts.isAsyncModifier)) - : parseIdentifier(); + let unwrappedType = type; + while (unwrappedType?.kind === ts.SyntaxKind.ParenthesizedType) { + unwrappedType = (unwrappedType as ts.ParenthesizedTypeNode).type; // Skip parens if need be + } - const node = factory.createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body); - return withJSDoc(finishNode(node, pos), hasJSDoc); + const hasJSDocFunctionType = unwrappedType && ts.isJSDocFunctionType(unwrappedType); + if (!allowAmbiguity && token() !== ts.SyntaxKind.EqualsGreaterThanToken && (hasJSDocFunctionType || token() !== ts.SyntaxKind.OpenBraceToken)) { + // Returning undefined here will cause our caller to rewind to where we started from. + return undefined; } - function parseArrowFunctionExpressionBody(isAsync: boolean): ts.Block | ts.Expression { - if (token() === ts.SyntaxKind.OpenBraceToken) { - return parseFunctionBlock(isAsync ? SignatureFlags.Await : SignatureFlags.None); - } + // 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(); + const equalsGreaterThanToken = parseExpectedToken(ts.SyntaxKind.EqualsGreaterThanToken); + const body = (lastToken === ts.SyntaxKind.EqualsGreaterThanToken || lastToken === ts.SyntaxKind.OpenBraceToken) + ? parseArrowFunctionExpressionBody(ts.some(modifiers, ts.isAsyncModifier)) + : parseIdentifier(); - if (token() !== ts.SyntaxKind.SemicolonToken && - token() !== ts.SyntaxKind.FunctionKeyword && - token() !== ts.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)); - } + const node = factory.createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - const savedTopLevel = topLevel; - topLevel = false; - const node = isAsync - ? doInAwaitContext(parseAssignmentExpressionOrHigher) - : doOutsideOfAwaitContext(parseAssignmentExpressionOrHigher); - topLevel = savedTopLevel; - return node; + function parseArrowFunctionExpressionBody(isAsync: boolean): ts.Block | ts.Expression { + if (token() === ts.SyntaxKind.OpenBraceToken) { + return parseFunctionBlock(isAsync ? SignatureFlags.Await : SignatureFlags.None); } - function parseConditionalExpressionRest(leftOperand: ts.Expression, pos: number): ts.Expression { - // Note: we are passed in an expression which was produced from parseBinaryExpressionOrHigher. - const questionToken = parseOptionalToken(ts.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. - let colonToken; - return finishNode(factory.createConditionalExpression(leftOperand, questionToken, doOutsideOfContext(disallowInAndDecoratorContext, parseAssignmentExpressionOrHigher), colonToken = parseExpectedToken(ts.SyntaxKind.ColonToken), ts.nodeIsPresent(colonToken) - ? parseAssignmentExpressionOrHigher() - : createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.ColonToken))), pos); + if (token() !== ts.SyntaxKind.SemicolonToken && + token() !== ts.SyntaxKind.FunctionKeyword && + token() !== ts.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)); } - function parseBinaryExpressionOrHigher(precedence: ts.OperatorPrecedence): ts.Expression { - const pos = getNodePos(); - const leftOperand = parseUnaryExpressionOrHigher(); - return parseBinaryExpressionRest(precedence, leftOperand, pos); - } + const savedTopLevel = topLevel; + topLevel = false; + const node = isAsync + ? doInAwaitContext(parseAssignmentExpressionOrHigher) + : doOutsideOfAwaitContext(parseAssignmentExpressionOrHigher); + topLevel = savedTopLevel; + return node; + } - function isInOrOfKeyword(t: ts.SyntaxKind) { - return t === ts.SyntaxKind.InKeyword || t === ts.SyntaxKind.OfKeyword; + function parseConditionalExpressionRest(leftOperand: ts.Expression, pos: number): ts.Expression { + // Note: we are passed in an expression which was produced from parseBinaryExpressionOrHigher. + const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); + if (!questionToken) { + return leftOperand; } - function parseBinaryExpressionRest(precedence: ts.OperatorPrecedence, leftOperand: ts.Expression, pos: number): ts.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 = ts.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() === ts.SyntaxKind.AsteriskAsteriskToken ? - newPrecedence >= precedence : - newPrecedence > precedence; - - if (!consumeCurrentOperator) { - break; - } + // Note: we explicitly 'allowIn' in the whenTrue part of the condition expression, and + // we do not that for the 'whenFalse' part. + let colonToken; + return finishNode(factory.createConditionalExpression(leftOperand, questionToken, doOutsideOfContext(disallowInAndDecoratorContext, parseAssignmentExpressionOrHigher), colonToken = parseExpectedToken(ts.SyntaxKind.ColonToken), ts.nodeIsPresent(colonToken) + ? parseAssignmentExpressionOrHigher() + : createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, ts.Diagnostics._0_expected, ts.tokenToString(ts.SyntaxKind.ColonToken))), pos); + } - if (token() === ts.SyntaxKind.InKeyword && inDisallowInContext()) { - break; - } + function parseBinaryExpressionOrHigher(precedence: ts.OperatorPrecedence): ts.Expression { + const pos = getNodePos(); + const leftOperand = parseUnaryExpressionOrHigher(); + return parseBinaryExpressionRest(precedence, leftOperand, pos); + } - if (token() === ts.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()); - } + function isInOrOfKeyword(t: ts.SyntaxKind) { + return t === ts.SyntaxKind.InKeyword || t === ts.SyntaxKind.OfKeyword; + } + + function parseBinaryExpressionRest(precedence: ts.OperatorPrecedence, leftOperand: ts.Expression, pos: number): ts.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 = ts.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() === ts.SyntaxKind.AsteriskAsteriskToken ? + newPrecedence >= precedence : + newPrecedence > precedence; + + if (!consumeCurrentOperator) { + break; + } + + if (token() === ts.SyntaxKind.InKeyword && inDisallowInContext()) { + break; + } + + if (token() === ts.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 { - leftOperand = makeBinaryExpression(leftOperand, parseTokenNode(), parseBinaryExpressionOrHigher(newPrecedence), pos); + nextToken(); + leftOperand = makeAsExpression(leftOperand, parseType()); } } - - return leftOperand; + else { + leftOperand = makeBinaryExpression(leftOperand, parseTokenNode(), parseBinaryExpressionOrHigher(newPrecedence), pos); + } } - function isBinaryOperator() { - if (inDisallowInContext() && token() === ts.SyntaxKind.InKeyword) { - return false; - } + return leftOperand; + } - return ts.getBinaryOperatorPrecedence(token()) > 0; + function isBinaryOperator() { + if (inDisallowInContext() && token() === ts.SyntaxKind.InKeyword) { + return false; } - function makeBinaryExpression(left: ts.Expression, operatorToken: ts.BinaryOperatorToken, right: ts.Expression, pos: number): ts.BinaryExpression { - return finishNode(factory.createBinaryExpression(left, operatorToken, right), pos); - } + return ts.getBinaryOperatorPrecedence(token()) > 0; + } - function makeAsExpression(left: ts.Expression, right: ts.TypeNode): ts.AsExpression { - return finishNode(factory.createAsExpression(left, right), left.pos); - } + function makeBinaryExpression(left: ts.Expression, operatorToken: ts.BinaryOperatorToken, right: ts.Expression, pos: number): ts.BinaryExpression { + return finishNode(factory.createBinaryExpression(left, operatorToken, right), pos); + } - function parsePrefixUnaryExpression() { - const pos = getNodePos(); - return finishNode(factory.createPrefixUnaryExpression(token() as ts.PrefixUnaryOperator, nextTokenAnd(parseSimpleUnaryExpression)), pos); - } + function makeAsExpression(left: ts.Expression, right: ts.TypeNode): ts.AsExpression { + return finishNode(factory.createAsExpression(left, right), left.pos); + } - function parseDeleteExpression() { - const pos = getNodePos(); - return finishNode(factory.createDeleteExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); - } + function parsePrefixUnaryExpression() { + const pos = getNodePos(); + return finishNode(factory.createPrefixUnaryExpression(token() as ts.PrefixUnaryOperator, nextTokenAnd(parseSimpleUnaryExpression)), pos); + } - function parseTypeOfExpression() { - const pos = getNodePos(); - return finishNode(factory.createTypeOfExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); - } + function parseDeleteExpression() { + const pos = getNodePos(); + return finishNode(factory.createDeleteExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); + } - function parseVoidExpression() { - const pos = getNodePos(); - return finishNode(factory.createVoidExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); - } + function parseTypeOfExpression() { + const pos = getNodePos(); + return finishNode(factory.createTypeOfExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); + } - function isAwaitExpression(): boolean { - if (token() === ts.SyntaxKind.AwaitKeyword) { - if (inAwaitContext()) { - return true; - } + function parseVoidExpression() { + const pos = getNodePos(); + return finishNode(factory.createVoidExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); + } - // here we are using similar heuristics as 'isYieldExpression' - return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); + function isAwaitExpression(): boolean { + if (token() === ts.SyntaxKind.AwaitKeyword) { + if (inAwaitContext()) { + return true; } - return false; + // here we are using similar heuristics as 'isYieldExpression' + return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); } - function parseAwaitExpression() { - const pos = getNodePos(); - return finishNode(factory.createAwaitExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); - } + return false; + } + + function parseAwaitExpression() { + const pos = getNodePos(); + return finishNode(factory.createAwaitExpression(nextTokenAnd(parseSimpleUnaryExpression)), pos); + } + /** + * Parse ES7 exponential expression and await expression + * + * ES7 ExponentiationExpression: + * 1) UnaryExpression[?Yield] + * 2) UpdateExpression[?Yield] ** ExponentiationExpression[?Yield] + * + */ + function parseUnaryExpressionOrHigher(): ts.UnaryExpression | ts.BinaryExpression { /** - * Parse ES7 exponential expression and await expression - * - * ES7 ExponentiationExpression: - * 1) UnaryExpression[?Yield] - * 2) UpdateExpression[?Yield] ** ExponentiationExpression[?Yield] - * + * ES7 UpdateExpression: + * 1) LeftHandSideExpression[?Yield] + * 2) LeftHandSideExpression[?Yield][no LineTerminator here]++ + * 3) LeftHandSideExpression[?Yield][no LineTerminator here]-- + * 4) ++UnaryExpression[?Yield] + * 5) --UnaryExpression[?Yield] */ - function parseUnaryExpressionOrHigher(): ts.UnaryExpression | ts.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 pos = getNodePos(); - const updateExpression = parseUpdateExpression(); - return token() === ts.SyntaxKind.AsteriskAsteriskToken ? - parseBinaryExpressionRest(ts.getBinaryOperatorPrecedence(token()), updateExpression, pos) as ts.BinaryExpression : - 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() === ts.SyntaxKind.AsteriskAsteriskToken) { - const pos = ts.skipTrivia(sourceText, simpleUnaryExpression.pos); - const { end } = simpleUnaryExpression; - if (simpleUnaryExpression.kind === ts.SyntaxKind.TypeAssertionExpression) { - parseErrorAt(pos, end, ts.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, ts.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, ts.tokenToString(unaryOperator)); - } - } - return simpleUnaryExpression; + if (isUpdateExpression()) { + const pos = getNodePos(); + const updateExpression = parseUpdateExpression(); + return token() === ts.SyntaxKind.AsteriskAsteriskToken ? + parseBinaryExpressionRest(ts.getBinaryOperatorPrecedence(token()), updateExpression, pos) as ts.BinaryExpression : + updateExpression; } /** - * 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] + * 2) delete UpdateExpression[?yield] + * 3) void UpdateExpression[?yield] + * 4) typeof UpdateExpression[?yield] + * 5) + UpdateExpression[?yield] + * 6) - UpdateExpression[?yield] + * 7) ~ UpdateExpression[?yield] + * 8) ! UpdateExpression[?yield] */ - function parseSimpleUnaryExpression(): ts.UnaryExpression { - switch (token()) { - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.TildeToken: - case ts.SyntaxKind.ExclamationToken: - return parsePrefixUnaryExpression(); - case ts.SyntaxKind.DeleteKeyword: - return parseDeleteExpression(); - case ts.SyntaxKind.TypeOfKeyword: - return parseTypeOfExpression(); - case ts.SyntaxKind.VoidKeyword: - return parseVoidExpression(); - case ts.SyntaxKind.LessThanToken: - // This is modified UnaryExpression grammar in TypeScript - // UnaryExpression (modified): - // < type > UnaryExpression - return parseTypeAssertion(); - case ts.SyntaxKind.AwaitKeyword: - if (isAwaitExpression()) { - return parseAwaitExpression(); - } - // falls through - default: - return parseUpdateExpression(); + const unaryOperator = token(); + const simpleUnaryExpression = parseSimpleUnaryExpression(); + if (token() === ts.SyntaxKind.AsteriskAsteriskToken) { + const pos = ts.skipTrivia(sourceText, simpleUnaryExpression.pos); + const { end } = simpleUnaryExpression; + if (simpleUnaryExpression.kind === ts.SyntaxKind.TypeAssertionExpression) { + parseErrorAt(pos, end, ts.Diagnostics.A_type_assertion_expression_is_not_allowed_in_the_left_hand_side_of_an_exponentiation_expression_Consider_enclosing_the_expression_in_parentheses); } - } - - /** - * 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 ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.MinusToken: - case ts.SyntaxKind.TildeToken: - case ts.SyntaxKind.ExclamationToken: - case ts.SyntaxKind.DeleteKeyword: - case ts.SyntaxKind.TypeOfKeyword: - case ts.SyntaxKind.VoidKeyword: - case ts.SyntaxKind.AwaitKeyword: - return false; - case ts.SyntaxKind.LessThanToken: - // If we are not in JSX context, we are parsing TypeAssertion which is an UnaryExpression - if (languageVariant !== ts.LanguageVariant.JSX) { - return false; - } - // We are in JSX context and the token is part of JSXElement. - // falls through - default: - return true; + else { + parseErrorAt(pos, end, ts.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, ts.tokenToString(unaryOperator)); } } + return simpleUnaryExpression; + } - /** - * 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(): ts.UpdateExpression { - if (token() === ts.SyntaxKind.PlusPlusToken || token() === ts.SyntaxKind.MinusMinusToken) { - const pos = getNodePos(); - return finishNode(factory.createPrefixUnaryExpression(token() as ts.PrefixUnaryOperator, nextTokenAnd(parseLeftHandSideExpressionOrHigher)), pos); - } - else if (languageVariant === ts.LanguageVariant.JSX && token() === ts.SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeywordOrGreaterThan)) { - // JSXElement is part of primaryExpression - return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); - } - - const expression = parseLeftHandSideExpressionOrHigher(); + /** + * 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(): ts.UnaryExpression { + switch (token()) { + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.TildeToken: + case ts.SyntaxKind.ExclamationToken: + return parsePrefixUnaryExpression(); + case ts.SyntaxKind.DeleteKeyword: + return parseDeleteExpression(); + case ts.SyntaxKind.TypeOfKeyword: + return parseTypeOfExpression(); + case ts.SyntaxKind.VoidKeyword: + return parseVoidExpression(); + case ts.SyntaxKind.LessThanToken: + // This is modified UnaryExpression grammar in TypeScript + // UnaryExpression (modified): + // < type > UnaryExpression + return parseTypeAssertion(); + case ts.SyntaxKind.AwaitKeyword: + if (isAwaitExpression()) { + return parseAwaitExpression(); + } + // falls through + default: + return parseUpdateExpression(); + } + } - ts.Debug.assert(ts.isLeftHandSideExpression(expression)); - if ((token() === ts.SyntaxKind.PlusPlusToken || token() === ts.SyntaxKind.MinusMinusToken) && !scanner.hasPrecedingLineBreak()) { - const operator = token() as ts.PostfixUnaryOperator; - nextToken(); - return finishNode(factory.createPostfixUnaryExpression(expression, operator), expression.pos); - } - - return expression; - } - - function parseLeftHandSideExpressionOrHigher(): ts.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. - const pos = getNodePos(); - let expression: ts.MemberExpression; - if (token() === ts.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 - sourceFlags |= ts.NodeFlags.PossiblyContainsDynamicImport; - expression = parseTokenNode(); - } - else if (lookAhead(nextTokenIsDot)) { - // This is an 'import.*' metaproperty (i.e. 'import.meta') - nextToken(); // advance past the 'import' - nextToken(); // advance past the dot - expression = finishNode(factory.createMetaProperty(ts.SyntaxKind.ImportKeyword, parseIdentifierName()), pos); - sourceFlags |= ts.NodeFlags.PossiblyContainsImportMeta; - } - else { - expression = parseMemberExpressionOrHigher(); + /** + * 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 ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.TildeToken: + case ts.SyntaxKind.ExclamationToken: + case ts.SyntaxKind.DeleteKeyword: + case ts.SyntaxKind.TypeOfKeyword: + case ts.SyntaxKind.VoidKeyword: + case ts.SyntaxKind.AwaitKeyword: + return false; + case ts.SyntaxKind.LessThanToken: + // If we are not in JSX context, we are parsing TypeAssertion which is an UnaryExpression + if (languageVariant !== ts.LanguageVariant.JSX) { + return false; } - } - else { - expression = token() === ts.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(pos, expression); + // We are in JSX context and the token is part of JSXElement. + // falls through + default: + return true; } + } - function parseMemberExpressionOrHigher(): ts.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. + /** + * 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(): ts.UpdateExpression { + if (token() === ts.SyntaxKind.PlusPlusToken || token() === ts.SyntaxKind.MinusMinusToken) { const pos = getNodePos(); - const expression = parsePrimaryExpression(); - return parseMemberExpressionRest(pos, expression, /*allowOptionalChain*/ true); + return finishNode(factory.createPrefixUnaryExpression(token() as ts.PrefixUnaryOperator, nextTokenAnd(parseLeftHandSideExpressionOrHigher)), pos); + } + else if (languageVariant === ts.LanguageVariant.JSX && token() === ts.SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeywordOrGreaterThan)) { + // JSXElement is part of primaryExpression + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); } - function parseSuperExpression(): ts.MemberExpression { - const pos = getNodePos(); - const expression = parseTokenNode(); - if (token() === ts.SyntaxKind.LessThanToken) { - const startPos = getNodePos(); - const typeArguments = tryParse(parseTypeArgumentsInExpression); - if (typeArguments !== undefined) { - parseErrorAt(startPos, getNodePos(), ts.Diagnostics.super_may_not_use_type_arguments); - } - } - - if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.DotToken || token() === ts.SyntaxKind.OpenBracketToken) { - return expression; - } + const expression = parseLeftHandSideExpressionOrHigher(); - // 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. - parseExpectedToken(ts.SyntaxKind.DotToken, ts.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 - return finishNode(factory.createPropertyAccessExpression(expression, parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true)), pos); + ts.Debug.assert(ts.isLeftHandSideExpression(expression)); + if ((token() === ts.SyntaxKind.PlusPlusToken || token() === ts.SyntaxKind.MinusMinusToken) && !scanner.hasPrecedingLineBreak()) { + const operator = token() as ts.PostfixUnaryOperator; + nextToken(); + return finishNode(factory.createPostfixUnaryExpression(expression, operator), expression.pos); } - function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean, topInvalidNodePosition?: number, openingTag?: ts.JsxOpeningElement | ts.JsxOpeningFragment): ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment { - const pos = getNodePos(); - const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); - let result: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment; - if (opening.kind === ts.SyntaxKind.JsxOpeningElement) { - let children = parseJsxChildren(opening); - let closingElement: ts.JsxClosingElement; - const lastChild: ts.JsxChild | undefined = children[children.length - 1]; - if (lastChild?.kind === ts.SyntaxKind.JsxElement - && !tagNamesAreEquivalent(lastChild.openingElement.tagName, lastChild.closingElement.tagName) - && tagNamesAreEquivalent(opening.tagName, lastChild.closingElement.tagName)) { - // when an unclosed JsxOpeningElement incorrectly parses its parent's JsxClosingElement, - // restructure (
(......
)) --> (
(......)
) - // (no need to error; the parent will error) - const end = lastChild.children.end; - const newLast = finishNode(factory.createJsxElement(lastChild.openingElement, lastChild.children, finishNode(factory.createJsxClosingElement(finishNode(factory.createIdentifier(""), end, end)), end, end)), lastChild.openingElement.pos, end); - - children = createNodeArray([...children.slice(0, children.length - 1), newLast], children.pos, end); - closingElement = lastChild.closingElement; - } - else { - closingElement = parseJsxClosingElement(opening, inExpressionContext); - if (!tagNamesAreEquivalent(opening.tagName, closingElement.tagName)) { - if (openingTag && ts.isJsxOpeningElement(openingTag) && tagNamesAreEquivalent(closingElement.tagName, openingTag.tagName)) { - // opening incorrectly matched with its parent's closing -- put error on opening - parseErrorAtRange(opening.tagName, ts.Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, ts.getTextOfNodeFromSourceText(sourceText, opening.tagName)); - } - else { - // other opening/closing mismatches -- put error on closing - parseErrorAtRange(closingElement.tagName, ts.Diagnostics.Expected_corresponding_JSX_closing_tag_for_0, ts.getTextOfNodeFromSourceText(sourceText, opening.tagName)); - } - } - } - result = finishNode(factory.createJsxElement(opening, children, closingElement), pos); - } - else if (opening.kind === ts.SyntaxKind.JsxOpeningFragment) { - result = finishNode(factory.createJsxFragment(opening, parseJsxChildren(opening), parseJsxClosingFragment(inExpressionContext)), pos); + return expression; + } + + function parseLeftHandSideExpressionOrHigher(): ts.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. + const pos = getNodePos(); + let expression: ts.MemberExpression; + if (token() === ts.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 + sourceFlags |= ts.NodeFlags.PossiblyContainsDynamicImport; + expression = parseTokenNode(); + } + else if (lookAhead(nextTokenIsDot)) { + // This is an 'import.*' metaproperty (i.e. 'import.meta') + nextToken(); // advance past the 'import' + nextToken(); // advance past the dot + expression = finishNode(factory.createMetaProperty(ts.SyntaxKind.ImportKeyword, parseIdentifierName()), pos); + sourceFlags |= ts.NodeFlags.PossiblyContainsImportMeta; } else { - ts.Debug.assert(opening.kind === ts.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() === ts.SyntaxKind.LessThanToken) { - const topBadPos = typeof topInvalidNodePosition === "undefined" ? result.pos : topInvalidNodePosition; - const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true, topBadPos)); - if (invalidElement) { - const operatorToken = createMissingNode(ts.SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false); - ts.setTextRangePosWidth(operatorToken, invalidElement.pos, 0); - parseErrorAt(ts.skipTrivia(sourceText, topBadPos), invalidElement.end, ts.Diagnostics.JSX_expressions_must_have_one_parent_element); - return finishNode(factory.createBinaryExpression(result, operatorToken as ts.Token, invalidElement), pos) as ts.Node as ts.JsxElement; - } + expression = parseMemberExpressionOrHigher(); } - - return result; } - - function parseJsxText(): ts.JsxText { - const pos = getNodePos(); - const node = factory.createJsxText(scanner.getTokenValue(), currentToken === ts.SyntaxKind.JsxTextAllWhiteSpaces); - currentToken = scanner.scanJsxToken(); - return finishNode(node, pos); - } - - function parseJsxChild(openingTag: ts.JsxOpeningElement | ts.JsxOpeningFragment, token: ts.JsxTokenSyntaxKind): ts.JsxChild | undefined { - switch (token) { - case ts.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 (ts.isJsxOpeningFragment(openingTag)) { - parseErrorAtRange(openingTag, ts.Diagnostics.JSX_fragment_has_no_corresponding_closing_tag); - } - else { - // We want the error span to cover only 'Foo.Bar' in < Foo.Bar > - // or to cover only 'Foo' in < Foo > - const tag = openingTag.tagName; - const start = ts.skipTrivia(sourceText, tag.pos); - parseErrorAt(start, tag.end, ts.Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, ts.getTextOfNodeFromSourceText(sourceText, openingTag.tagName)); - } - return undefined; - case ts.SyntaxKind.LessThanSlashToken: - case ts.SyntaxKind.ConflictMarkerTrivia: - return undefined; - case ts.SyntaxKind.JsxText: - case ts.SyntaxKind.JsxTextAllWhiteSpaces: - return parseJsxText(); - case ts.SyntaxKind.OpenBraceToken: - return parseJsxExpression(/*inExpressionContext*/ false); - case ts.SyntaxKind.LessThanToken: - return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ false, /*topInvalidNodePosition*/ undefined, openingTag); - default: - return ts.Debug.assertNever(token); - } + else { + expression = token() === ts.SyntaxKind.SuperKeyword ? parseSuperExpression() : parseMemberExpressionOrHigher(); } - function parseJsxChildren(openingTag: ts.JsxOpeningElement | ts.JsxOpeningFragment): ts.NodeArray { - const list = []; - const listPos = getNodePos(); - const saveParsingContext = parsingContext; - parsingContext |= 1 << ParsingContext.JsxChildren; + // 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(pos, expression); + } - while (true) { - const child = parseJsxChild(openingTag, currentToken = scanner.reScanJsxToken()); - if (!child) - break; - list.push(child); - if (ts.isJsxOpeningElement(openingTag) - && child?.kind === ts.SyntaxKind.JsxElement - && !tagNamesAreEquivalent(child.openingElement.tagName, child.closingElement.tagName) - && tagNamesAreEquivalent(openingTag.tagName, child.closingElement.tagName)) { - // stop after parsing a mismatched child like
...(
) in order to reattach the
higher - break; - } - } + function parseMemberExpressionOrHigher(): ts.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 pos = getNodePos(); + const expression = parsePrimaryExpression(); + return parseMemberExpressionRest(pos, expression, /*allowOptionalChain*/ true); + } - parsingContext = saveParsingContext; - return createNodeArray(list, listPos); + function parseSuperExpression(): ts.MemberExpression { + const pos = getNodePos(); + const expression = parseTokenNode(); + if (token() === ts.SyntaxKind.LessThanToken) { + const startPos = getNodePos(); + const typeArguments = tryParse(parseTypeArgumentsInExpression); + if (typeArguments !== undefined) { + parseErrorAt(startPos, getNodePos(), ts.Diagnostics.super_may_not_use_type_arguments); + } } - function parseJsxAttributes(): ts.JsxAttributes { - const pos = getNodePos(); - return finishNode(factory.createJsxAttributes(parseList(ParsingContext.JsxAttributes, parseJsxAttribute)), pos); + if (token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.DotToken || token() === ts.SyntaxKind.OpenBracketToken) { + return expression; } - function parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext: boolean): ts.JsxOpeningElement | ts.JsxSelfClosingElement | ts.JsxOpeningFragment { - const pos = getNodePos(); - - parseExpected(ts.SyntaxKind.LessThanToken); - if (token() === ts.SyntaxKind.GreaterThanToken) { - // See below for explanation of scanJsxText - scanJsxText(); - return finishNode(factory.createJsxOpeningFragment(), pos); - } - const tagName = parseJsxElementName(); - const typeArguments = (contextFlags & ts.NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments() : undefined; - const attributes = parseJsxAttributes(); + // 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. + parseExpectedToken(ts.SyntaxKind.DotToken, ts.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 + return finishNode(factory.createPropertyAccessExpression(expression, parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true)), pos); + } - let node: ts.JsxOpeningLikeElement; - if (token() === ts.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 - scanJsxText(); - node = factory.createJsxOpeningElement(tagName, typeArguments, attributes); + function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean, topInvalidNodePosition?: number, openingTag?: ts.JsxOpeningElement | ts.JsxOpeningFragment): ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment { + const pos = getNodePos(); + const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); + let result: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment; + if (opening.kind === ts.SyntaxKind.JsxOpeningElement) { + let children = parseJsxChildren(opening); + let closingElement: ts.JsxClosingElement; + const lastChild: ts.JsxChild | undefined = children[children.length - 1]; + if (lastChild?.kind === ts.SyntaxKind.JsxElement + && !tagNamesAreEquivalent(lastChild.openingElement.tagName, lastChild.closingElement.tagName) + && tagNamesAreEquivalent(opening.tagName, lastChild.closingElement.tagName)) { + // when an unclosed JsxOpeningElement incorrectly parses its parent's JsxClosingElement, + // restructure (
(......
)) --> (
(......)
) + // (no need to error; the parent will error) + const end = lastChild.children.end; + const newLast = finishNode(factory.createJsxElement(lastChild.openingElement, lastChild.children, finishNode(factory.createJsxClosingElement(finishNode(factory.createIdentifier(""), end, end)), end, end)), lastChild.openingElement.pos, end); + + children = createNodeArray([...children.slice(0, children.length - 1), newLast], children.pos, end); + closingElement = lastChild.closingElement; } else { - parseExpected(ts.SyntaxKind.SlashToken); - if (parseExpected(ts.SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false)) { - // manually advance the scanner in order to look for jsx text inside jsx - if (inExpressionContext) { - nextToken(); + closingElement = parseJsxClosingElement(opening, inExpressionContext); + if (!tagNamesAreEquivalent(opening.tagName, closingElement.tagName)) { + if (openingTag && ts.isJsxOpeningElement(openingTag) && tagNamesAreEquivalent(closingElement.tagName, openingTag.tagName)) { + // opening incorrectly matched with its parent's closing -- put error on opening + parseErrorAtRange(opening.tagName, ts.Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, ts.getTextOfNodeFromSourceText(sourceText, opening.tagName)); } else { - scanJsxText(); + // other opening/closing mismatches -- put error on closing + parseErrorAtRange(closingElement.tagName, ts.Diagnostics.Expected_corresponding_JSX_closing_tag_for_0, ts.getTextOfNodeFromSourceText(sourceText, opening.tagName)); } } - node = factory.createJsxSelfClosingElement(tagName, typeArguments, attributes); } - - return finishNode(node, pos); + result = finishNode(factory.createJsxElement(opening, children, closingElement), pos); } - - function parseJsxElementName(): ts.JsxTagNameExpression { - const pos = getNodePos(); - 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: ts.JsxTagNameExpression = token() === ts.SyntaxKind.ThisKeyword ? - parseTokenNode() : parseIdentifierName(); - while (parseOptional(ts.SyntaxKind.DotToken)) { - expression = finishNode(factory.createPropertyAccessExpression(expression, parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ false)), pos) as ts.JsxTagNamePropertyAccess; - } - return expression; + else if (opening.kind === ts.SyntaxKind.JsxOpeningFragment) { + result = finishNode(factory.createJsxFragment(opening, parseJsxChildren(opening), parseJsxClosingFragment(inExpressionContext)), pos); + } + else { + ts.Debug.assert(opening.kind === ts.SyntaxKind.JsxSelfClosingElement); + // Nothing else to do for self-closing elements + result = opening; } - function parseJsxExpression(inExpressionContext: boolean): ts.JsxExpression | undefined { - const pos = getNodePos(); - if (!parseExpected(ts.SyntaxKind.OpenBraceToken)) { - return undefined; - } - - let dotDotDotToken: ts.DotDotDotToken | undefined; - let expression: ts.Expression | undefined; - if (token() !== ts.SyntaxKind.CloseBraceToken) { - dotDotDotToken = parseOptionalToken(ts.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. - expression = parseExpression(); - } - if (inExpressionContext) { - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - else { - if (parseExpected(ts.SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false)) { - scanJsxText(); - } + // 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() === ts.SyntaxKind.LessThanToken) { + const topBadPos = typeof topInvalidNodePosition === "undefined" ? result.pos : topInvalidNodePosition; + const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true, topBadPos)); + if (invalidElement) { + const operatorToken = createMissingNode(ts.SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false); + ts.setTextRangePosWidth(operatorToken, invalidElement.pos, 0); + parseErrorAt(ts.skipTrivia(sourceText, topBadPos), invalidElement.end, ts.Diagnostics.JSX_expressions_must_have_one_parent_element); + return finishNode(factory.createBinaryExpression(result, operatorToken as ts.Token, invalidElement), pos) as ts.Node as ts.JsxElement; } - - return finishNode(factory.createJsxExpression(dotDotDotToken, expression), pos); } - function parseJsxAttribute(): ts.JsxAttribute | ts.JsxSpreadAttribute { - if (token() === ts.SyntaxKind.OpenBraceToken) { - return parseJsxSpreadAttribute(); - } + return result; + } - scanJsxIdentifier(); - const pos = getNodePos(); - return finishNode(factory.createJsxAttribute(parseIdentifierName(), parseJsxAttributeValue()), pos); - } + function parseJsxText(): ts.JsxText { + const pos = getNodePos(); + const node = factory.createJsxText(scanner.getTokenValue(), currentToken === ts.SyntaxKind.JsxTextAllWhiteSpaces); + currentToken = scanner.scanJsxToken(); + return finishNode(node, pos); + } - function parseJsxAttributeValue(): ts.JsxAttributeValue | undefined { - if (token() === ts.SyntaxKind.EqualsToken) { - if (scanJsxAttributeValue() === ts.SyntaxKind.StringLiteral) { - return parseLiteralNode() as ts.StringLiteral; + function parseJsxChild(openingTag: ts.JsxOpeningElement | ts.JsxOpeningFragment, token: ts.JsxTokenSyntaxKind): ts.JsxChild | undefined { + switch (token) { + case ts.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 (ts.isJsxOpeningFragment(openingTag)) { + parseErrorAtRange(openingTag, ts.Diagnostics.JSX_fragment_has_no_corresponding_closing_tag); } - if (token() === ts.SyntaxKind.OpenBraceToken) { - return parseJsxExpression(/*inExpressionContext*/ true); - } - if (token() === ts.SyntaxKind.LessThanToken) { - return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); + else { + // We want the error span to cover only 'Foo.Bar' in < Foo.Bar > + // or to cover only 'Foo' in < Foo > + const tag = openingTag.tagName; + const start = ts.skipTrivia(sourceText, tag.pos); + parseErrorAt(start, tag.end, ts.Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, ts.getTextOfNodeFromSourceText(sourceText, openingTag.tagName)); } - parseErrorAtCurrentToken(ts.Diagnostics.or_JSX_element_expected); - } - return undefined; + return undefined; + case ts.SyntaxKind.LessThanSlashToken: + case ts.SyntaxKind.ConflictMarkerTrivia: + return undefined; + case ts.SyntaxKind.JsxText: + case ts.SyntaxKind.JsxTextAllWhiteSpaces: + return parseJsxText(); + case ts.SyntaxKind.OpenBraceToken: + return parseJsxExpression(/*inExpressionContext*/ false); + case ts.SyntaxKind.LessThanToken: + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ false, /*topInvalidNodePosition*/ undefined, openingTag); + default: + return ts.Debug.assertNever(token); } + } - function parseJsxSpreadAttribute(): ts.JsxSpreadAttribute { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenBraceToken); - parseExpected(ts.SyntaxKind.DotDotDotToken); - const expression = parseExpression(); - parseExpected(ts.SyntaxKind.CloseBraceToken); - return finishNode(factory.createJsxSpreadAttribute(expression), pos); - } + function parseJsxChildren(openingTag: ts.JsxOpeningElement | ts.JsxOpeningFragment): ts.NodeArray { + const list = []; + const listPos = getNodePos(); + const saveParsingContext = parsingContext; + parsingContext |= 1 << ParsingContext.JsxChildren; - function parseJsxClosingElement(open: ts.JsxOpeningElement, inExpressionContext: boolean): ts.JsxClosingElement { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.LessThanSlashToken); - const tagName = parseJsxElementName(); - if (parseExpected(ts.SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false)) { - // manually advance the scanner in order to look for jsx text inside jsx - if (inExpressionContext || !tagNamesAreEquivalent(open.tagName, tagName)) { - nextToken(); - } - else { - scanJsxText(); - } + while (true) { + const child = parseJsxChild(openingTag, currentToken = scanner.reScanJsxToken()); + if (!child) + break; + list.push(child); + if (ts.isJsxOpeningElement(openingTag) + && child?.kind === ts.SyntaxKind.JsxElement + && !tagNamesAreEquivalent(child.openingElement.tagName, child.closingElement.tagName) + && tagNamesAreEquivalent(openingTag.tagName, child.closingElement.tagName)) { + // stop after parsing a mismatched child like
...(
) in order to reattach the
higher + break; } - return finishNode(factory.createJsxClosingElement(tagName), pos); } - function parseJsxClosingFragment(inExpressionContext: boolean): ts.JsxClosingFragment { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.LessThanSlashToken); - if (ts.tokenIsIdentifierOrKeyword(token())) { - parseErrorAtRange(parseJsxElementName(), ts.Diagnostics.Expected_corresponding_closing_tag_for_JSX_fragment); - } + parsingContext = saveParsingContext; + return createNodeArray(list, listPos); + } + + function parseJsxAttributes(): ts.JsxAttributes { + const pos = getNodePos(); + return finishNode(factory.createJsxAttributes(parseList(ParsingContext.JsxAttributes, parseJsxAttribute)), pos); + } + + function parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext: boolean): ts.JsxOpeningElement | ts.JsxSelfClosingElement | ts.JsxOpeningFragment { + const pos = getNodePos(); + + parseExpected(ts.SyntaxKind.LessThanToken); + if (token() === ts.SyntaxKind.GreaterThanToken) { + // See below for explanation of scanJsxText + scanJsxText(); + return finishNode(factory.createJsxOpeningFragment(), pos); + } + const tagName = parseJsxElementName(); + const typeArguments = (contextFlags & ts.NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments() : undefined; + const attributes = parseJsxAttributes(); + + let node: ts.JsxOpeningLikeElement; + if (token() === ts.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 + scanJsxText(); + node = factory.createJsxOpeningElement(tagName, typeArguments, attributes); + } + else { + parseExpected(ts.SyntaxKind.SlashToken); if (parseExpected(ts.SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false)) { // manually advance the scanner in order to look for jsx text inside jsx if (inExpressionContext) { @@ -5387,4084 +5272,4199 @@ namespace ts { scanJsxText(); } } - return finishNode(factory.createJsxJsxClosingFragment(), pos); + node = factory.createJsxSelfClosingElement(tagName, typeArguments, attributes); } - function parseTypeAssertion(): ts.TypeAssertion { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.LessThanToken); - const type = parseType(); - parseExpected(ts.SyntaxKind.GreaterThanToken); - const expression = parseSimpleUnaryExpression(); - return finishNode(factory.createTypeAssertion(type, expression), pos); + return finishNode(node, pos); + } + + function parseJsxElementName(): ts.JsxTagNameExpression { + const pos = getNodePos(); + 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: ts.JsxTagNameExpression = token() === ts.SyntaxKind.ThisKeyword ? + parseTokenNode() : parseIdentifierName(); + while (parseOptional(ts.SyntaxKind.DotToken)) { + expression = finishNode(factory.createPropertyAccessExpression(expression, parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ false)), pos) as ts.JsxTagNamePropertyAccess; + } + return expression; + } + + function parseJsxExpression(inExpressionContext: boolean): ts.JsxExpression | undefined { + const pos = getNodePos(); + if (!parseExpected(ts.SyntaxKind.OpenBraceToken)) { + return undefined; } - function nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate() { - nextToken(); - return ts.tokenIsIdentifierOrKeyword(token()) - || token() === ts.SyntaxKind.OpenBracketToken - || isTemplateStartOfTaggedTemplate(); + let dotDotDotToken: ts.DotDotDotToken | undefined; + let expression: ts.Expression | undefined; + if (token() !== ts.SyntaxKind.CloseBraceToken) { + dotDotDotToken = parseOptionalToken(ts.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. + expression = parseExpression(); + } + if (inExpressionContext) { + parseExpected(ts.SyntaxKind.CloseBraceToken); + } + else { + if (parseExpected(ts.SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false)) { + scanJsxText(); + } } - function isStartOfOptionalPropertyOrElementAccessChain() { - return token() === ts.SyntaxKind.QuestionDotToken - && lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate); + return finishNode(factory.createJsxExpression(dotDotDotToken, expression), pos); + } + + function parseJsxAttribute(): ts.JsxAttribute | ts.JsxSpreadAttribute { + if (token() === ts.SyntaxKind.OpenBraceToken) { + return parseJsxSpreadAttribute(); } - function tryReparseOptionalChain(node: ts.Expression) { - if (node.flags & ts.NodeFlags.OptionalChain) { - return true; + scanJsxIdentifier(); + const pos = getNodePos(); + return finishNode(factory.createJsxAttribute(parseIdentifierName(), parseJsxAttributeValue()), pos); + } + + function parseJsxAttributeValue(): ts.JsxAttributeValue | undefined { + if (token() === ts.SyntaxKind.EqualsToken) { + if (scanJsxAttributeValue() === ts.SyntaxKind.StringLiteral) { + return parseLiteralNode() as ts.StringLiteral; } - // check for an optional chain in a non-null expression - if (ts.isNonNullExpression(node)) { - let expr = node.expression; - while (ts.isNonNullExpression(expr) && !(expr.flags & ts.NodeFlags.OptionalChain)) { - expr = expr.expression; - } - if (expr.flags & ts.NodeFlags.OptionalChain) { - // this is part of an optional chain. Walk down from `node` to `expression` and set the flag. - while (ts.isNonNullExpression(node)) { - (node as ts.Mutable).flags |= ts.NodeFlags.OptionalChain; - node = node.expression; - } - return true; - } + if (token() === ts.SyntaxKind.OpenBraceToken) { + return parseJsxExpression(/*inExpressionContext*/ true); } - return false; + if (token() === ts.SyntaxKind.LessThanToken) { + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); + } + parseErrorAtCurrentToken(ts.Diagnostics.or_JSX_element_expected); } + return undefined; + } - function parsePropertyAccessExpressionRest(pos: number, expression: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined) { - const name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); - const isOptionalChain = questionDotToken || tryReparseOptionalChain(expression); - const propertyAccess = isOptionalChain ? - factory.createPropertyAccessChain(expression, questionDotToken, name) : - factory.createPropertyAccessExpression(expression, name); - if (isOptionalChain && ts.isPrivateIdentifier(propertyAccess.name)) { - parseErrorAtRange(propertyAccess.name, ts.Diagnostics.An_optional_chain_cannot_contain_private_identifiers); + function parseJsxSpreadAttribute(): ts.JsxSpreadAttribute { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenBraceToken); + parseExpected(ts.SyntaxKind.DotDotDotToken); + const expression = parseExpression(); + parseExpected(ts.SyntaxKind.CloseBraceToken); + return finishNode(factory.createJsxSpreadAttribute(expression), pos); + } + + function parseJsxClosingElement(open: ts.JsxOpeningElement, inExpressionContext: boolean): ts.JsxClosingElement { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.LessThanSlashToken); + const tagName = parseJsxElementName(); + if (parseExpected(ts.SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false)) { + // manually advance the scanner in order to look for jsx text inside jsx + if (inExpressionContext || !tagNamesAreEquivalent(open.tagName, tagName)) { + nextToken(); + } + else { + scanJsxText(); } - return finishNode(propertyAccess, pos); } + return finishNode(factory.createJsxClosingElement(tagName), pos); + } - function parseElementAccessExpressionRest(pos: number, expression: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined) { - let argumentExpression: ts.Expression; - if (token() === ts.SyntaxKind.CloseBracketToken) { - argumentExpression = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.An_element_access_expression_should_take_an_argument); + function parseJsxClosingFragment(inExpressionContext: boolean): ts.JsxClosingFragment { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.LessThanSlashToken); + if (ts.tokenIsIdentifierOrKeyword(token())) { + parseErrorAtRange(parseJsxElementName(), ts.Diagnostics.Expected_corresponding_closing_tag_for_JSX_fragment); + } + if (parseExpected(ts.SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false)) { + // manually advance the scanner in order to look for jsx text inside jsx + if (inExpressionContext) { + nextToken(); } else { - const argument = allowInAnd(parseExpression); - if (ts.isStringOrNumericLiteralLike(argument)) { - argument.text = internIdentifier(argument.text); - } - argumentExpression = argument; + scanJsxText(); } - - parseExpected(ts.SyntaxKind.CloseBracketToken); - - const indexedAccess = questionDotToken || tryReparseOptionalChain(expression) ? - factory.createElementAccessChain(expression, questionDotToken, argumentExpression) : - factory.createElementAccessExpression(expression, argumentExpression); - return finishNode(indexedAccess, pos); } + return finishNode(factory.createJsxJsxClosingFragment(), pos); + } - function parseMemberExpressionRest(pos: number, expression: ts.LeftHandSideExpression, allowOptionalChain: boolean): ts.MemberExpression { - while (true) { - let questionDotToken: ts.QuestionDotToken | undefined; - let isPropertyAccess = false; - if (allowOptionalChain && isStartOfOptionalPropertyOrElementAccessChain()) { - questionDotToken = parseExpectedToken(ts.SyntaxKind.QuestionDotToken); - isPropertyAccess = ts.tokenIsIdentifierOrKeyword(token()); - } - else { - isPropertyAccess = parseOptional(ts.SyntaxKind.DotToken); - } - - if (isPropertyAccess) { - expression = parsePropertyAccessExpressionRest(pos, expression, questionDotToken); - continue; - } + function parseTypeAssertion(): ts.TypeAssertion { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.LessThanToken); + const type = parseType(); + parseExpected(ts.SyntaxKind.GreaterThanToken); + const expression = parseSimpleUnaryExpression(); + return finishNode(factory.createTypeAssertion(type, expression), pos); + } - // when in the [Decorator] context, we do not parse ElementAccess as it could be part of a ComputedPropertyName - if ((questionDotToken || !inDecoratorContext()) && parseOptional(ts.SyntaxKind.OpenBracketToken)) { - expression = parseElementAccessExpressionRest(pos, expression, questionDotToken); - continue; - } + function nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate() { + nextToken(); + return ts.tokenIsIdentifierOrKeyword(token()) + || token() === ts.SyntaxKind.OpenBracketToken + || isTemplateStartOfTaggedTemplate(); + } - if (isTemplateStartOfTaggedTemplate()) { - // Absorb type arguments into TemplateExpression when preceding expression is ExpressionWithTypeArguments - expression = !questionDotToken && expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments ? - parseTaggedTemplateRest(pos, (expression as ts.ExpressionWithTypeArguments).expression, questionDotToken, (expression as ts.ExpressionWithTypeArguments).typeArguments) : - parseTaggedTemplateRest(pos, expression, questionDotToken, /*typeArguments*/ undefined); - continue; - } + function isStartOfOptionalPropertyOrElementAccessChain() { + return token() === ts.SyntaxKind.QuestionDotToken + && lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate); + } - if (!questionDotToken) { - if (token() === ts.SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - nextToken(); - expression = finishNode(factory.createNonNullExpression(expression), pos); - continue; - } - const typeArguments = tryParse(parseTypeArgumentsInExpression); - if (typeArguments) { - expression = finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos); - continue; - } + function tryReparseOptionalChain(node: ts.Expression) { + if (node.flags & ts.NodeFlags.OptionalChain) { + return true; + } + // check for an optional chain in a non-null expression + if (ts.isNonNullExpression(node)) { + let expr = node.expression; + while (ts.isNonNullExpression(expr) && !(expr.flags & ts.NodeFlags.OptionalChain)) { + expr = expr.expression; + } + if (expr.flags & ts.NodeFlags.OptionalChain) { + // this is part of an optional chain. Walk down from `node` to `expression` and set the flag. + while (ts.isNonNullExpression(node)) { + (node as ts.Mutable).flags |= ts.NodeFlags.OptionalChain; + node = node.expression; } - - return expression as ts.MemberExpression; + return true; } } + return false; + } - function isTemplateStartOfTaggedTemplate() { - return token() === ts.SyntaxKind.NoSubstitutionTemplateLiteral || token() === ts.SyntaxKind.TemplateHead; - } - function parseTaggedTemplateRest(pos: number, tag: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: ts.NodeArray | undefined) { - const tagExpression = factory.createTaggedTemplateExpression(tag, typeArguments, token() === ts.SyntaxKind.NoSubstitutionTemplateLiteral ? - (reScanTemplateHeadOrNoSubstitutionTemplate(), parseLiteralNode() as ts.NoSubstitutionTemplateLiteral) : - parseTemplateExpression(/*isTaggedTemplate*/ true)); - if (questionDotToken || tag.flags & ts.NodeFlags.OptionalChain) { - (tagExpression as ts.Mutable).flags |= ts.NodeFlags.OptionalChain; - } - tagExpression.questionDotToken = questionDotToken; - return finishNode(tagExpression, pos); + function parsePropertyAccessExpressionRest(pos: number, expression: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined) { + const name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); + const isOptionalChain = questionDotToken || tryReparseOptionalChain(expression); + const propertyAccess = isOptionalChain ? + factory.createPropertyAccessChain(expression, questionDotToken, name) : + factory.createPropertyAccessExpression(expression, name); + if (isOptionalChain && ts.isPrivateIdentifier(propertyAccess.name)) { + parseErrorAtRange(propertyAccess.name, ts.Diagnostics.An_optional_chain_cannot_contain_private_identifiers); } + return finishNode(propertyAccess, pos); + } - function parseCallExpressionRest(pos: number, expression: ts.LeftHandSideExpression): ts.LeftHandSideExpression { - while (true) { - expression = parseMemberExpressionRest(pos, expression, /*allowOptionalChain*/ true); - let typeArguments: ts.NodeArray | undefined; - const questionDotToken = parseOptionalToken(ts.SyntaxKind.QuestionDotToken); - if (questionDotToken) { - typeArguments = tryParse(parseTypeArgumentsInExpression); - if (isTemplateStartOfTaggedTemplate()) { - expression = parseTaggedTemplateRest(pos, expression, questionDotToken, typeArguments); - continue; - } - } - if (typeArguments || token() === ts.SyntaxKind.OpenParenToken) { - // Absorb type arguments into CallExpression when preceding expression is ExpressionWithTypeArguments - if (!questionDotToken && expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { - typeArguments = (expression as ts.ExpressionWithTypeArguments).typeArguments; - expression = (expression as ts.ExpressionWithTypeArguments).expression; - } - const argumentList = parseArgumentList(); - const callExpr = questionDotToken || tryReparseOptionalChain(expression) ? - factory.createCallChain(expression, questionDotToken, typeArguments, argumentList) : - factory.createCallExpression(expression, typeArguments, argumentList); - expression = finishNode(callExpr, pos); - continue; - } - if (questionDotToken) { - // We parsed `?.` but then failed to parse anything, so report a missing identifier here. - const name = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, ts.Diagnostics.Identifier_expected); - expression = finishNode(factory.createPropertyAccessChain(expression, questionDotToken, name), pos); - } - break; + function parseElementAccessExpressionRest(pos: number, expression: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined) { + let argumentExpression: ts.Expression; + if (token() === ts.SyntaxKind.CloseBracketToken) { + argumentExpression = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.An_element_access_expression_should_take_an_argument); + } + else { + const argument = allowInAnd(parseExpression); + if (ts.isStringOrNumericLiteralLike(argument)) { + argument.text = internIdentifier(argument.text); } - return expression; + argumentExpression = argument; } - function parseArgumentList() { - parseExpected(ts.SyntaxKind.OpenParenToken); - const result = parseDelimitedList(ParsingContext.ArgumentExpressions, parseArgumentExpression); - parseExpected(ts.SyntaxKind.CloseParenToken); - return result; - } + parseExpected(ts.SyntaxKind.CloseBracketToken); - function parseTypeArgumentsInExpression() { - if ((contextFlags & ts.NodeFlags.JavaScriptFile) !== 0) { - // TypeArguments must not be parsed in JavaScript files to avoid ambiguity with binary operators. - return undefined; - } + const indexedAccess = questionDotToken || tryReparseOptionalChain(expression) ? + factory.createElementAccessChain(expression, questionDotToken, argumentExpression) : + factory.createElementAccessExpression(expression, argumentExpression); + return finishNode(indexedAccess, pos); + } - if (reScanLessThanToken() !== ts.SyntaxKind.LessThanToken) { - return undefined; + function parseMemberExpressionRest(pos: number, expression: ts.LeftHandSideExpression, allowOptionalChain: boolean): ts.MemberExpression { + while (true) { + let questionDotToken: ts.QuestionDotToken | undefined; + let isPropertyAccess = false; + if (allowOptionalChain && isStartOfOptionalPropertyOrElementAccessChain()) { + questionDotToken = parseExpectedToken(ts.SyntaxKind.QuestionDotToken); + isPropertyAccess = ts.tokenIsIdentifierOrKeyword(token()); } - nextToken(); - - const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType); - if (!parseExpected(ts.SyntaxKind.GreaterThanToken)) { - // If it doesn't have the closing `>` then it's definitely not an type argument list. - return undefined; + else { + isPropertyAccess = parseOptional(ts.SyntaxKind.DotToken); } - // We successfully parsed a type argument list. The next token determines whether we want to - // treat it as such. If the type argument list is followed by `(` or a template literal, as in - // `f(42)`, we favor the type argument interpretation even though JavaScript would view - // it as a relational expression. - return typeArguments && canFollowTypeArgumentsInExpression() ? typeArguments : undefined; - } + if (isPropertyAccess) { + expression = parsePropertyAccessExpressionRest(pos, expression, questionDotToken); + continue; + } - function canFollowTypeArgumentsInExpression(): boolean { - switch (token()) { - // These tokens can follow a type argument list in a call expression. - case ts.SyntaxKind.OpenParenToken: // foo( - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: // foo `...` - case ts.SyntaxKind.TemplateHead: // foo `...${100}...` - return true; + // when in the [Decorator] context, we do not parse ElementAccess as it could be part of a ComputedPropertyName + if ((questionDotToken || !inDecoratorContext()) && parseOptional(ts.SyntaxKind.OpenBracketToken)) { + expression = parseElementAccessExpressionRest(pos, expression, questionDotToken); + continue; } - // Consider something a type argument list only if the following token can't start an expression. - return !isStartOfExpression(); - } - function parsePrimaryExpression(): ts.PrimaryExpression { - switch (token()) { - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return parseLiteralNode(); - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.SuperKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - return parseTokenNode(); - case ts.SyntaxKind.OpenParenToken: - return parseParenthesizedExpression(); - case ts.SyntaxKind.OpenBracketToken: - return parseArrayLiteralExpression(); - case ts.SyntaxKind.OpenBraceToken: - return parseObjectLiteralExpression(); - case ts.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; - } + if (isTemplateStartOfTaggedTemplate()) { + // Absorb type arguments into TemplateExpression when preceding expression is ExpressionWithTypeArguments + expression = !questionDotToken && expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments ? + parseTaggedTemplateRest(pos, (expression as ts.ExpressionWithTypeArguments).expression, questionDotToken, (expression as ts.ExpressionWithTypeArguments).typeArguments) : + parseTaggedTemplateRest(pos, expression, questionDotToken, /*typeArguments*/ undefined); + continue; + } - return parseFunctionExpression(); - case ts.SyntaxKind.ClassKeyword: - return parseClassExpression(); - case ts.SyntaxKind.FunctionKeyword: - return parseFunctionExpression(); - case ts.SyntaxKind.NewKeyword: - return parseNewExpressionOrNewDotTarget(); - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.SlashEqualsToken: - if (reScanSlashToken() === ts.SyntaxKind.RegularExpressionLiteral) { - return parseLiteralNode(); - } - break; - case ts.SyntaxKind.TemplateHead: - return parseTemplateExpression(/* isTaggedTemplate */ false); - case ts.SyntaxKind.PrivateIdentifier: - return parsePrivateIdentifier(); + if (!questionDotToken) { + if (token() === ts.SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { + nextToken(); + expression = finishNode(factory.createNonNullExpression(expression), pos); + continue; + } + const typeArguments = tryParse(parseTypeArgumentsInExpression); + if (typeArguments) { + expression = finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos); + continue; + } } - return parseIdentifier(ts.Diagnostics.Expression_expected); + return expression as ts.MemberExpression; } + } - function parseParenthesizedExpression(): ts.ParenthesizedExpression { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpected(ts.SyntaxKind.CloseParenToken); - return withJSDoc(finishNode(factory.createParenthesizedExpression(expression), pos), hasJSDoc); - } + function isTemplateStartOfTaggedTemplate() { + return token() === ts.SyntaxKind.NoSubstitutionTemplateLiteral || token() === ts.SyntaxKind.TemplateHead; + } + function parseTaggedTemplateRest(pos: number, tag: ts.LeftHandSideExpression, questionDotToken: ts.QuestionDotToken | undefined, typeArguments: ts.NodeArray | undefined) { + const tagExpression = factory.createTaggedTemplateExpression(tag, typeArguments, token() === ts.SyntaxKind.NoSubstitutionTemplateLiteral ? + (reScanTemplateHeadOrNoSubstitutionTemplate(), parseLiteralNode() as ts.NoSubstitutionTemplateLiteral) : + parseTemplateExpression(/*isTaggedTemplate*/ true)); + if (questionDotToken || tag.flags & ts.NodeFlags.OptionalChain) { + (tagExpression as ts.Mutable).flags |= ts.NodeFlags.OptionalChain; + } + tagExpression.questionDotToken = questionDotToken; + return finishNode(tagExpression, pos); + } - function parseSpreadElement(): ts.Expression { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.DotDotDotToken); - const expression = parseAssignmentExpressionOrHigher(); - return finishNode(factory.createSpreadElement(expression), pos); + function parseCallExpressionRest(pos: number, expression: ts.LeftHandSideExpression): ts.LeftHandSideExpression { + while (true) { + expression = parseMemberExpressionRest(pos, expression, /*allowOptionalChain*/ true); + let typeArguments: ts.NodeArray | undefined; + const questionDotToken = parseOptionalToken(ts.SyntaxKind.QuestionDotToken); + if (questionDotToken) { + typeArguments = tryParse(parseTypeArgumentsInExpression); + if (isTemplateStartOfTaggedTemplate()) { + expression = parseTaggedTemplateRest(pos, expression, questionDotToken, typeArguments); + continue; + } + } + if (typeArguments || token() === ts.SyntaxKind.OpenParenToken) { + // Absorb type arguments into CallExpression when preceding expression is ExpressionWithTypeArguments + if (!questionDotToken && expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + typeArguments = (expression as ts.ExpressionWithTypeArguments).typeArguments; + expression = (expression as ts.ExpressionWithTypeArguments).expression; + } + const argumentList = parseArgumentList(); + const callExpr = questionDotToken || tryReparseOptionalChain(expression) ? + factory.createCallChain(expression, questionDotToken, typeArguments, argumentList) : + factory.createCallExpression(expression, typeArguments, argumentList); + expression = finishNode(callExpr, pos); + continue; + } + if (questionDotToken) { + // We parsed `?.` but then failed to parse anything, so report a missing identifier here. + const name = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, ts.Diagnostics.Identifier_expected); + expression = finishNode(factory.createPropertyAccessChain(expression, questionDotToken, name), pos); + } + break; } + return expression; + } - function parseArgumentOrArrayLiteralElement(): ts.Expression { - return token() === ts.SyntaxKind.DotDotDotToken ? parseSpreadElement() : - token() === ts.SyntaxKind.CommaToken ? finishNode(factory.createOmittedExpression(), getNodePos()) : - parseAssignmentExpressionOrHigher(); + function parseArgumentList() { + parseExpected(ts.SyntaxKind.OpenParenToken); + const result = parseDelimitedList(ParsingContext.ArgumentExpressions, parseArgumentExpression); + parseExpected(ts.SyntaxKind.CloseParenToken); + return result; + } + + function parseTypeArgumentsInExpression() { + if ((contextFlags & ts.NodeFlags.JavaScriptFile) !== 0) { + // TypeArguments must not be parsed in JavaScript files to avoid ambiguity with binary operators. + return undefined; } - function parseArgumentExpression(): ts.Expression { - return doOutsideOfContext(disallowInAndDecoratorContext, parseArgumentOrArrayLiteralElement); + if (reScanLessThanToken() !== ts.SyntaxKind.LessThanToken) { + return undefined; } + nextToken(); - function parseArrayLiteralExpression(): ts.ArrayLiteralExpression { - const pos = getNodePos(); - const openBracketPosition = scanner.getTokenPos(); - const openBracketParsed = parseExpected(ts.SyntaxKind.OpenBracketToken); - const multiLine = scanner.hasPrecedingLineBreak(); - const elements = parseDelimitedList(ParsingContext.ArrayLiteralMembers, parseArgumentOrArrayLiteralElement); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken, openBracketParsed, openBracketPosition); - return finishNode(factory.createArrayLiteralExpression(elements, multiLine), pos); + const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType); + if (!parseExpected(ts.SyntaxKind.GreaterThanToken)) { + // If it doesn't have the closing `>` then it's definitely not an type argument list. + return undefined; } - function parseObjectLiteralElement(): ts.ObjectLiteralElementLike { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); + // We successfully parsed a type argument list. The next token determines whether we want to + // treat it as such. If the type argument list is followed by `(` or a template literal, as in + // `f(42)`, we favor the type argument interpretation even though JavaScript would view + // it as a relational expression. + return typeArguments && canFollowTypeArgumentsInExpression() ? typeArguments : undefined; + } - if (parseOptionalToken(ts.SyntaxKind.DotDotDotToken)) { - const expression = parseAssignmentExpressionOrHigher(); - return withJSDoc(finishNode(factory.createSpreadAssignment(expression), pos), hasJSDoc); - } + function canFollowTypeArgumentsInExpression(): boolean { + switch (token()) { + // These tokens can follow a type argument list in a call expression. + case ts.SyntaxKind.OpenParenToken: // foo( + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: // foo `...` + case ts.SyntaxKind.TemplateHead: // foo `...${100}...` + return true; + } + // Consider something a type argument list only if the following token can't start an expression. + return !isStartOfExpression(); + } - const decorators = parseDecorators(); - const modifiers = parseModifiers(); + function parsePrimaryExpression(): ts.PrimaryExpression { + switch (token()) { + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return parseLiteralNode(); + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + return parseTokenNode(); + case ts.SyntaxKind.OpenParenToken: + return parseParenthesizedExpression(); + case ts.SyntaxKind.OpenBracketToken: + return parseArrayLiteralExpression(); + case ts.SyntaxKind.OpenBraceToken: + return parseObjectLiteralExpression(); + case ts.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; + } - if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.GetAccessor); - } - if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.SetAccessor); - } + return parseFunctionExpression(); + case ts.SyntaxKind.ClassKeyword: + return parseClassExpression(); + case ts.SyntaxKind.FunctionKeyword: + return parseFunctionExpression(); + case ts.SyntaxKind.NewKeyword: + return parseNewExpressionOrNewDotTarget(); + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.SlashEqualsToken: + if (reScanSlashToken() === ts.SyntaxKind.RegularExpressionLiteral) { + return parseLiteralNode(); + } + break; + case ts.SyntaxKind.TemplateHead: + return parseTemplateExpression(/* isTaggedTemplate */ false); + case ts.SyntaxKind.PrivateIdentifier: + return parsePrivateIdentifier(); + } - const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); - const tokenIsIdentifier = isIdentifier(); - const name = parsePropertyName(); + return parseIdentifier(ts.Diagnostics.Expression_expected); + } - // Disallowing of optional property assignments and definite assignment assertion happens in the grammar checker. - const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); - const exclamationToken = parseOptionalToken(ts.SyntaxKind.ExclamationToken); - if (asteriskToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { - return parseMethodDeclaration(pos, hasJSDoc, decorators, modifiers, asteriskToken, name, questionToken, exclamationToken); - } - - // 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 - let node: ts.Mutable; - const isShorthandPropertyAssignment = tokenIsIdentifier && (token() !== ts.SyntaxKind.ColonToken); - if (isShorthandPropertyAssignment) { - const equalsToken = parseOptionalToken(ts.SyntaxKind.EqualsToken); - const objectAssignmentInitializer = equalsToken ? allowInAnd(parseAssignmentExpressionOrHigher) : undefined; - node = factory.createShorthandPropertyAssignment(name as ts.Identifier, objectAssignmentInitializer); - // Save equals token for error reporting. - // TODO(rbuckton): Consider manufacturing this when we need to report an error as it is otherwise not useful. - node.equalsToken = equalsToken; - } - else { - parseExpected(ts.SyntaxKind.ColonToken); - const initializer = allowInAnd(parseAssignmentExpressionOrHigher); - node = factory.createPropertyAssignment(name, initializer); - } - // Decorators, Modifiers, questionToken, and exclamationToken are not supported by property assignments and are reported in the grammar checker - node.decorators = decorators; - node.modifiers = modifiers; - node.questionToken = questionToken; - node.exclamationToken = exclamationToken; - return withJSDoc(finishNode(node, pos), hasJSDoc); - } + function parseParenthesizedExpression(): ts.ParenthesizedExpression { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpected(ts.SyntaxKind.CloseParenToken); + return withJSDoc(finishNode(factory.createParenthesizedExpression(expression), pos), hasJSDoc); + } - function parseObjectLiteralExpression(): ts.ObjectLiteralExpression { - const pos = getNodePos(); - const openBracePosition = scanner.getTokenPos(); - const openBraceParsed = parseExpected(ts.SyntaxKind.OpenBraceToken); - const multiLine = scanner.hasPrecedingLineBreak(); - const properties = parseDelimitedList(ParsingContext.ObjectLiteralMembers, parseObjectLiteralElement, /*considerSemicolonAsDelimiter*/ true); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, openBraceParsed, openBracePosition); - return finishNode(factory.createObjectLiteralExpression(properties, multiLine), pos); - } + function parseSpreadElement(): ts.Expression { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.DotDotDotToken); + const expression = parseAssignmentExpressionOrHigher(); + return finishNode(factory.createSpreadElement(expression), pos); + } - function parseFunctionExpression(): ts.FunctionExpression { - // GeneratorExpression: - // function* BindingIdentifier [Yield][opt](FormalParameters[Yield]){ GeneratorBody } - // - // FunctionExpression: - // function BindingIdentifier[opt](FormalParameters){ FunctionBody } - const savedDecoratorContext = inDecoratorContext(); - setDecoratorContext(/*val*/ false); + function parseArgumentOrArrayLiteralElement(): ts.Expression { + return token() === ts.SyntaxKind.DotDotDotToken ? parseSpreadElement() : + token() === ts.SyntaxKind.CommaToken ? finishNode(factory.createOmittedExpression(), getNodePos()) : + parseAssignmentExpressionOrHigher(); + } - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const modifiers = parseModifiers(); - parseExpected(ts.SyntaxKind.FunctionKeyword); - const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); - const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? SignatureFlags.Await : SignatureFlags.None; - const name = isGenerator && isAsync ? doInYieldAndAwaitContext(parseOptionalBindingIdentifier) : - isGenerator ? doInYieldContext(parseOptionalBindingIdentifier) : - isAsync ? doInAwaitContext(parseOptionalBindingIdentifier) : - parseOptionalBindingIdentifier(); + function parseArgumentExpression(): ts.Expression { + return doOutsideOfContext(disallowInAndDecoratorContext, parseArgumentOrArrayLiteralElement); + } - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(isGenerator | isAsync); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - const body = parseFunctionBlock(isGenerator | isAsync); + function parseArrayLiteralExpression(): ts.ArrayLiteralExpression { + const pos = getNodePos(); + const openBracketPosition = scanner.getTokenPos(); + const openBracketParsed = parseExpected(ts.SyntaxKind.OpenBracketToken); + const multiLine = scanner.hasPrecedingLineBreak(); + const elements = parseDelimitedList(ParsingContext.ArrayLiteralMembers, parseArgumentOrArrayLiteralElement); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken, openBracketParsed, openBracketPosition); + return finishNode(factory.createArrayLiteralExpression(elements, multiLine), pos); + } - setDecoratorContext(savedDecoratorContext); + function parseObjectLiteralElement(): ts.ObjectLiteralElementLike { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); - const node = factory.createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body); - return withJSDoc(finishNode(node, pos), hasJSDoc); + if (parseOptionalToken(ts.SyntaxKind.DotDotDotToken)) { + const expression = parseAssignmentExpressionOrHigher(); + return withJSDoc(finishNode(factory.createSpreadAssignment(expression), pos), hasJSDoc); } - function parseOptionalBindingIdentifier(): ts.Identifier | undefined { - return isBindingIdentifier() ? parseBindingIdentifier() : undefined; - } + const decorators = parseDecorators(); + const modifiers = parseModifiers(); - function parseNewExpressionOrNewDotTarget(): ts.NewExpression | ts.MetaProperty { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.NewKeyword); - if (parseOptional(ts.SyntaxKind.DotToken)) { - const name = parseIdentifierName(); - return finishNode(factory.createMetaProperty(ts.SyntaxKind.NewKeyword, name), pos); - } - const expressionPos = getNodePos(); - let expression: ts.LeftHandSideExpression = parseMemberExpressionRest(expressionPos, parsePrimaryExpression(), /*allowOptionalChain*/ false); - let typeArguments: ts.NodeArray | undefined; - // Absorb type arguments into NewExpression when preceding expression is ExpressionWithTypeArguments - if (expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { - typeArguments = (expression as ts.ExpressionWithTypeArguments).typeArguments; - expression = (expression as ts.ExpressionWithTypeArguments).expression; - } - const argumentList = token() === ts.SyntaxKind.OpenParenToken ? parseArgumentList() : undefined; - return finishNode(factory.createNewExpression(expression, typeArguments, argumentList), pos); + if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.GetAccessor); + } + if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.SetAccessor); } - // STATEMENTS - function parseBlock(ignoreMissingOpenBrace: boolean, diagnosticMessage?: ts.DiagnosticMessage): ts.Block { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const openBracePosition = scanner.getTokenPos(); - const openBraceParsed = parseExpected(ts.SyntaxKind.OpenBraceToken, diagnosticMessage); - if (openBraceParsed || ignoreMissingOpenBrace) { - const multiLine = scanner.hasPrecedingLineBreak(); - const statements = parseList(ParsingContext.BlockStatements, parseStatement); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, openBraceParsed, openBracePosition); - const result = withJSDoc(finishNode(factory.createBlock(statements, multiLine), pos), hasJSDoc); - if (token() === ts.SyntaxKind.EqualsToken) { - parseErrorAtCurrentToken(ts.Diagnostics.Declaration_or_statement_expected_This_follows_a_block_of_statements_so_if_you_intended_to_write_a_destructuring_assignment_you_might_need_to_wrap_the_the_whole_assignment_in_parentheses); - nextToken(); - } + const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); + const tokenIsIdentifier = isIdentifier(); + const name = parsePropertyName(); - return result; - } - else { - const statements = createMissingList(); - return withJSDoc(finishNode(factory.createBlock(statements, /*multiLine*/ undefined), pos), hasJSDoc); - } + // Disallowing of optional property assignments and definite assignment assertion happens in the grammar checker. + const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); + const exclamationToken = parseOptionalToken(ts.SyntaxKind.ExclamationToken); + if (asteriskToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { + return parseMethodDeclaration(pos, hasJSDoc, decorators, modifiers, asteriskToken, name, questionToken, exclamationToken); } - function parseFunctionBlock(flags: SignatureFlags, diagnosticMessage?: ts.DiagnosticMessage): ts.Block { - const savedYieldContext = inYieldContext(); - setYieldContext(!!(flags & SignatureFlags.Yield)); + // 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 + let node: ts.Mutable; + const isShorthandPropertyAssignment = tokenIsIdentifier && (token() !== ts.SyntaxKind.ColonToken); + if (isShorthandPropertyAssignment) { + const equalsToken = parseOptionalToken(ts.SyntaxKind.EqualsToken); + const objectAssignmentInitializer = equalsToken ? allowInAnd(parseAssignmentExpressionOrHigher) : undefined; + node = factory.createShorthandPropertyAssignment(name as ts.Identifier, objectAssignmentInitializer); + // Save equals token for error reporting. + // TODO(rbuckton): Consider manufacturing this when we need to report an error as it is otherwise not useful. + node.equalsToken = equalsToken; + } + else { + parseExpected(ts.SyntaxKind.ColonToken); + const initializer = allowInAnd(parseAssignmentExpressionOrHigher); + node = factory.createPropertyAssignment(name, initializer); + } + // Decorators, Modifiers, questionToken, and exclamationToken are not supported by property assignments and are reported in the grammar checker + node.decorators = decorators; + node.modifiers = modifiers; + node.questionToken = questionToken; + node.exclamationToken = exclamationToken; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - const savedAwaitContext = inAwaitContext(); - setAwaitContext(!!(flags & SignatureFlags.Await)); + function parseObjectLiteralExpression(): ts.ObjectLiteralExpression { + const pos = getNodePos(); + const openBracePosition = scanner.getTokenPos(); + const openBraceParsed = parseExpected(ts.SyntaxKind.OpenBraceToken); + const multiLine = scanner.hasPrecedingLineBreak(); + const properties = parseDelimitedList(ParsingContext.ObjectLiteralMembers, parseObjectLiteralElement, /*considerSemicolonAsDelimiter*/ true); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, openBraceParsed, openBracePosition); + return finishNode(factory.createObjectLiteralExpression(properties, multiLine), pos); + } - const savedTopLevel = topLevel; - topLevel = false; + function parseFunctionExpression(): ts.FunctionExpression { + // GeneratorExpression: + // function* BindingIdentifier [Yield][opt](FormalParameters[Yield]){ GeneratorBody } + // + // FunctionExpression: + // function BindingIdentifier[opt](FormalParameters){ FunctionBody } + const savedDecoratorContext = inDecoratorContext(); + setDecoratorContext(/*val*/ false); + + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const modifiers = parseModifiers(); + parseExpected(ts.SyntaxKind.FunctionKeyword); + const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); + const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? SignatureFlags.Await : SignatureFlags.None; + const name = isGenerator && isAsync ? doInYieldAndAwaitContext(parseOptionalBindingIdentifier) : + isGenerator ? doInYieldContext(parseOptionalBindingIdentifier) : + isAsync ? doInAwaitContext(parseOptionalBindingIdentifier) : + parseOptionalBindingIdentifier(); + + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(isGenerator | isAsync); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + const body = parseFunctionBlock(isGenerator | isAsync); + + setDecoratorContext(savedDecoratorContext); + + const node = factory.createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - // 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); - } + function parseOptionalBindingIdentifier(): ts.Identifier | undefined { + return isBindingIdentifier() ? parseBindingIdentifier() : undefined; + } - const block = parseBlock(!!(flags & SignatureFlags.IgnoreMissingOpenBrace), diagnosticMessage); + function parseNewExpressionOrNewDotTarget(): ts.NewExpression | ts.MetaProperty { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.NewKeyword); + if (parseOptional(ts.SyntaxKind.DotToken)) { + const name = parseIdentifierName(); + return finishNode(factory.createMetaProperty(ts.SyntaxKind.NewKeyword, name), pos); + } + const expressionPos = getNodePos(); + let expression: ts.LeftHandSideExpression = parseMemberExpressionRest(expressionPos, parsePrimaryExpression(), /*allowOptionalChain*/ false); + let typeArguments: ts.NodeArray | undefined; + // Absorb type arguments into NewExpression when preceding expression is ExpressionWithTypeArguments + if (expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + typeArguments = (expression as ts.ExpressionWithTypeArguments).typeArguments; + expression = (expression as ts.ExpressionWithTypeArguments).expression; + } + const argumentList = token() === ts.SyntaxKind.OpenParenToken ? parseArgumentList() : undefined; + return finishNode(factory.createNewExpression(expression, typeArguments, argumentList), pos); + } - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ true); + // STATEMENTS + function parseBlock(ignoreMissingOpenBrace: boolean, diagnosticMessage?: ts.DiagnosticMessage): ts.Block { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const openBracePosition = scanner.getTokenPos(); + const openBraceParsed = parseExpected(ts.SyntaxKind.OpenBraceToken, diagnosticMessage); + if (openBraceParsed || ignoreMissingOpenBrace) { + const multiLine = scanner.hasPrecedingLineBreak(); + const statements = parseList(ParsingContext.BlockStatements, parseStatement); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, openBraceParsed, openBracePosition); + const result = withJSDoc(finishNode(factory.createBlock(statements, multiLine), pos), hasJSDoc); + if (token() === ts.SyntaxKind.EqualsToken) { + parseErrorAtCurrentToken(ts.Diagnostics.Declaration_or_statement_expected_This_follows_a_block_of_statements_so_if_you_intended_to_write_a_destructuring_assignment_you_might_need_to_wrap_the_the_whole_assignment_in_parentheses); + nextToken(); } - topLevel = savedTopLevel; - setYieldContext(savedYieldContext); - setAwaitContext(savedAwaitContext); - - return block; + return result; } - - function parseEmptyStatement(): ts.Statement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.SemicolonToken); - return withJSDoc(finishNode(factory.createEmptyStatement(), pos), hasJSDoc); + else { + const statements = createMissingList(); + return withJSDoc(finishNode(factory.createBlock(statements, /*multiLine*/ undefined), pos), hasJSDoc); } + } - function parseIfStatement(): ts.IfStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.IfKeyword); - const openParenPosition = scanner.getTokenPos(); - const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); - const thenStatement = parseStatement(); - const elseStatement = parseOptional(ts.SyntaxKind.ElseKeyword) ? parseStatement() : undefined; - return withJSDoc(finishNode(factory.createIfStatement(expression, thenStatement, elseStatement), pos), hasJSDoc); - } + function parseFunctionBlock(flags: SignatureFlags, diagnosticMessage?: ts.DiagnosticMessage): ts.Block { + const savedYieldContext = inYieldContext(); + setYieldContext(!!(flags & SignatureFlags.Yield)); - function parseDoStatement(): ts.DoStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.DoKeyword); - const statement = parseStatement(); - parseExpected(ts.SyntaxKind.WhileKeyword); - const openParenPosition = scanner.getTokenPos(); - const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); + const savedAwaitContext = inAwaitContext(); + setAwaitContext(!!(flags & SignatureFlags.Await)); - // 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(ts.SyntaxKind.SemicolonToken); - return withJSDoc(finishNode(factory.createDoStatement(statement, expression), pos), hasJSDoc); + const savedTopLevel = topLevel; + topLevel = false; + + // 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); } - function parseWhileStatement(): ts.WhileStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.WhileKeyword); - const openParenPosition = scanner.getTokenPos(); - const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); - const statement = parseStatement(); - return withJSDoc(finishNode(factory.createWhileStatement(expression, statement), pos), hasJSDoc); + const block = parseBlock(!!(flags & SignatureFlags.IgnoreMissingOpenBrace), diagnosticMessage); + + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ true); } - function parseForOrForInOrForOfStatement(): ts.Statement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.ForKeyword); - const awaitToken = parseOptionalToken(ts.SyntaxKind.AwaitKeyword); - parseExpected(ts.SyntaxKind.OpenParenToken); - let initializer!: ts.VariableDeclarationList | ts.Expression; - if (token() !== ts.SyntaxKind.SemicolonToken) { - if (token() === ts.SyntaxKind.VarKeyword || token() === ts.SyntaxKind.LetKeyword || token() === ts.SyntaxKind.ConstKeyword) { - initializer = parseVariableDeclarationList(/*inForStatementInitializer*/ true); - } - else { - initializer = disallowInAnd(parseExpression); - } - } + topLevel = savedTopLevel; + setYieldContext(savedYieldContext); + setAwaitContext(savedAwaitContext); - let node: ts.IterationStatement; - if (awaitToken ? parseExpected(ts.SyntaxKind.OfKeyword) : parseOptional(ts.SyntaxKind.OfKeyword)) { - const expression = allowInAnd(parseAssignmentExpressionOrHigher); - parseExpected(ts.SyntaxKind.CloseParenToken); - node = factory.createForOfStatement(awaitToken, initializer, expression, parseStatement()); - } - else if (parseOptional(ts.SyntaxKind.InKeyword)) { - const expression = allowInAnd(parseExpression); - parseExpected(ts.SyntaxKind.CloseParenToken); - node = factory.createForInStatement(initializer, expression, parseStatement()); - } - else { - parseExpected(ts.SyntaxKind.SemicolonToken); - const condition = token() !== ts.SyntaxKind.SemicolonToken && token() !== ts.SyntaxKind.CloseParenToken - ? allowInAnd(parseExpression) - : undefined; - parseExpected(ts.SyntaxKind.SemicolonToken); - const incrementor = token() !== ts.SyntaxKind.CloseParenToken - ? allowInAnd(parseExpression) - : undefined; - parseExpected(ts.SyntaxKind.CloseParenToken); - node = factory.createForStatement(initializer, condition, incrementor, parseStatement()); - } + return block; + } - return withJSDoc(finishNode(node, pos) as ts.ForStatement | ts.ForInOrOfStatement, hasJSDoc); - } + function parseEmptyStatement(): ts.Statement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.SemicolonToken); + return withJSDoc(finishNode(factory.createEmptyStatement(), pos), hasJSDoc); + } - function parseBreakOrContinueStatement(kind: ts.SyntaxKind): ts.BreakOrContinueStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); + function parseIfStatement(): ts.IfStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.IfKeyword); + const openParenPosition = scanner.getTokenPos(); + const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); + const thenStatement = parseStatement(); + const elseStatement = parseOptional(ts.SyntaxKind.ElseKeyword) ? parseStatement() : undefined; + return withJSDoc(finishNode(factory.createIfStatement(expression, thenStatement, elseStatement), pos), hasJSDoc); + } - parseExpected(kind === ts.SyntaxKind.BreakStatement ? ts.SyntaxKind.BreakKeyword : ts.SyntaxKind.ContinueKeyword); - const label = canParseSemicolon() ? undefined : parseIdentifier(); + function parseDoStatement(): ts.DoStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.DoKeyword); + const statement = parseStatement(); + parseExpected(ts.SyntaxKind.WhileKeyword); + const openParenPosition = scanner.getTokenPos(); + const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); + + // 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(ts.SyntaxKind.SemicolonToken); + return withJSDoc(finishNode(factory.createDoStatement(statement, expression), pos), hasJSDoc); + } - parseSemicolon(); - const node = kind === ts.SyntaxKind.BreakStatement - ? factory.createBreakStatement(label) - : factory.createContinueStatement(label); - return withJSDoc(finishNode(node, pos), hasJSDoc); - } + function parseWhileStatement(): ts.WhileStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.WhileKeyword); + const openParenPosition = scanner.getTokenPos(); + const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); + const statement = parseStatement(); + return withJSDoc(finishNode(factory.createWhileStatement(expression, statement), pos), hasJSDoc); + } - function parseReturnStatement(): ts.ReturnStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.ReturnKeyword); - const expression = canParseSemicolon() ? undefined : allowInAnd(parseExpression); - parseSemicolon(); - return withJSDoc(finishNode(factory.createReturnStatement(expression), pos), hasJSDoc); + function parseForOrForInOrForOfStatement(): ts.Statement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.ForKeyword); + const awaitToken = parseOptionalToken(ts.SyntaxKind.AwaitKeyword); + parseExpected(ts.SyntaxKind.OpenParenToken); + let initializer!: ts.VariableDeclarationList | ts.Expression; + if (token() !== ts.SyntaxKind.SemicolonToken) { + if (token() === ts.SyntaxKind.VarKeyword || token() === ts.SyntaxKind.LetKeyword || token() === ts.SyntaxKind.ConstKeyword) { + initializer = parseVariableDeclarationList(/*inForStatementInitializer*/ true); + } + else { + initializer = disallowInAnd(parseExpression); + } } - function parseWithStatement(): ts.WithStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.WithKeyword); - const openParenPosition = scanner.getTokenPos(); - const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); - const statement = doInsideOfContext(ts.NodeFlags.InWithStatement, parseStatement); - return withJSDoc(finishNode(factory.createWithStatement(expression, statement), pos), hasJSDoc); + let node: ts.IterationStatement; + if (awaitToken ? parseExpected(ts.SyntaxKind.OfKeyword) : parseOptional(ts.SyntaxKind.OfKeyword)) { + const expression = allowInAnd(parseAssignmentExpressionOrHigher); + parseExpected(ts.SyntaxKind.CloseParenToken); + node = factory.createForOfStatement(awaitToken, initializer, expression, parseStatement()); } - - function parseCaseClause(): ts.CaseClause { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.CaseKeyword); + else if (parseOptional(ts.SyntaxKind.InKeyword)) { const expression = allowInAnd(parseExpression); - parseExpected(ts.SyntaxKind.ColonToken); - const statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); - return withJSDoc(finishNode(factory.createCaseClause(expression, statements), pos), hasJSDoc); + parseExpected(ts.SyntaxKind.CloseParenToken); + node = factory.createForInStatement(initializer, expression, parseStatement()); } - - function parseDefaultClause(): ts.DefaultClause { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.DefaultKeyword); - parseExpected(ts.SyntaxKind.ColonToken); - const statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); - return finishNode(factory.createDefaultClause(statements), pos); + else { + parseExpected(ts.SyntaxKind.SemicolonToken); + const condition = token() !== ts.SyntaxKind.SemicolonToken && token() !== ts.SyntaxKind.CloseParenToken + ? allowInAnd(parseExpression) + : undefined; + parseExpected(ts.SyntaxKind.SemicolonToken); + const incrementor = token() !== ts.SyntaxKind.CloseParenToken + ? allowInAnd(parseExpression) + : undefined; + parseExpected(ts.SyntaxKind.CloseParenToken); + node = factory.createForStatement(initializer, condition, incrementor, parseStatement()); } - function parseCaseOrDefaultClause(): ts.CaseOrDefaultClause { - return token() === ts.SyntaxKind.CaseKeyword ? parseCaseClause() : parseDefaultClause(); - } + return withJSDoc(finishNode(node, pos) as ts.ForStatement | ts.ForInOrOfStatement, hasJSDoc); + } - function parseCaseBlock(): ts.CaseBlock { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenBraceToken); - const clauses = parseList(ParsingContext.SwitchClauses, parseCaseOrDefaultClause); - parseExpected(ts.SyntaxKind.CloseBraceToken); - return finishNode(factory.createCaseBlock(clauses), pos); - } + function parseBreakOrContinueStatement(kind: ts.SyntaxKind): ts.BreakOrContinueStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); - function parseSwitchStatement(): ts.SwitchStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.SwitchKeyword); - parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = allowInAnd(parseExpression); - parseExpected(ts.SyntaxKind.CloseParenToken); - const caseBlock = parseCaseBlock(); - return withJSDoc(finishNode(factory.createSwitchStatement(expression, caseBlock), pos), hasJSDoc); - } + parseExpected(kind === ts.SyntaxKind.BreakStatement ? ts.SyntaxKind.BreakKeyword : ts.SyntaxKind.ContinueKeyword); + const label = canParseSemicolon() ? undefined : parseIdentifier(); - function parseThrowStatement(): ts.ThrowStatement { - // ThrowStatement[Yield] : - // throw [no LineTerminator here]Expression[In, ?Yield]; + parseSemicolon(); + const node = kind === ts.SyntaxKind.BreakStatement + ? factory.createBreakStatement(label) + : factory.createContinueStatement(label); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.ThrowKeyword); - - // 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. - // Instead, we create a "missing" identifier, but don't report an error. The actual error - // will be reported in the grammar walker. - let expression = scanner.hasPrecedingLineBreak() ? undefined : allowInAnd(parseExpression); - if (expression === undefined) { - identifierCount++; - expression = finishNode(factory.createIdentifier(""), getNodePos()); - } - if (!tryParseSemicolon()) { - parseErrorForMissingSemicolonAfter(expression); - } - return withJSDoc(finishNode(factory.createThrowStatement(expression), pos), hasJSDoc); - } + function parseReturnStatement(): ts.ReturnStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.ReturnKeyword); + const expression = canParseSemicolon() ? undefined : allowInAnd(parseExpression); + parseSemicolon(); + return withJSDoc(finishNode(factory.createReturnStatement(expression), pos), hasJSDoc); + } - // TODO: Review for error recovery - function parseTryStatement(): ts.TryStatement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); + function parseWithStatement(): ts.WithStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.WithKeyword); + const openParenPosition = scanner.getTokenPos(); + const openParenParsed = parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpectedMatchingBrackets(ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, openParenParsed, openParenPosition); + const statement = doInsideOfContext(ts.NodeFlags.InWithStatement, parseStatement); + return withJSDoc(finishNode(factory.createWithStatement(expression, statement), pos), hasJSDoc); + } - parseExpected(ts.SyntaxKind.TryKeyword); - const tryBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); - const catchClause = token() === ts.SyntaxKind.CatchKeyword ? parseCatchClause() : undefined; + function parseCaseClause(): ts.CaseClause { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.CaseKeyword); + const expression = allowInAnd(parseExpression); + parseExpected(ts.SyntaxKind.ColonToken); + const statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); + return withJSDoc(finishNode(factory.createCaseClause(expression, statements), pos), hasJSDoc); + } - // If we don't have a catch clause, then we must have a finally clause. Try to parse - // one out no matter what. - let finallyBlock: ts.Block | undefined; - if (!catchClause || token() === ts.SyntaxKind.FinallyKeyword) { - parseExpected(ts.SyntaxKind.FinallyKeyword, ts.Diagnostics.catch_or_finally_expected); - finallyBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); - } + function parseDefaultClause(): ts.DefaultClause { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.DefaultKeyword); + parseExpected(ts.SyntaxKind.ColonToken); + const statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); + return finishNode(factory.createDefaultClause(statements), pos); + } - return withJSDoc(finishNode(factory.createTryStatement(tryBlock, catchClause, finallyBlock), pos), hasJSDoc); - } + function parseCaseOrDefaultClause(): ts.CaseOrDefaultClause { + return token() === ts.SyntaxKind.CaseKeyword ? parseCaseClause() : parseDefaultClause(); + } - function parseCatchClause(): ts.CatchClause { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.CatchKeyword); + function parseCaseBlock(): ts.CaseBlock { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenBraceToken); + const clauses = parseList(ParsingContext.SwitchClauses, parseCaseOrDefaultClause); + parseExpected(ts.SyntaxKind.CloseBraceToken); + return finishNode(factory.createCaseBlock(clauses), pos); + } - let variableDeclaration; - if (parseOptional(ts.SyntaxKind.OpenParenToken)) { - variableDeclaration = parseVariableDeclaration(); - parseExpected(ts.SyntaxKind.CloseParenToken); - } - else { - // Keep shape of node to avoid degrading performance. - variableDeclaration = undefined; - } + function parseSwitchStatement(): ts.SwitchStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.SwitchKeyword); + parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = allowInAnd(parseExpression); + parseExpected(ts.SyntaxKind.CloseParenToken); + const caseBlock = parseCaseBlock(); + return withJSDoc(finishNode(factory.createSwitchStatement(expression, caseBlock), pos), hasJSDoc); + } - const block = parseBlock(/*ignoreMissingOpenBrace*/ false); - return finishNode(factory.createCatchClause(variableDeclaration, block), pos); + function parseThrowStatement(): ts.ThrowStatement { + // ThrowStatement[Yield] : + // throw [no LineTerminator here]Expression[In, ?Yield]; + + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.ThrowKeyword); + + // 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. + // Instead, we create a "missing" identifier, but don't report an error. The actual error + // will be reported in the grammar walker. + let expression = scanner.hasPrecedingLineBreak() ? undefined : allowInAnd(parseExpression); + if (expression === undefined) { + identifierCount++; + expression = finishNode(factory.createIdentifier(""), getNodePos()); } - - function parseDebuggerStatement(): ts.Statement { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - parseExpected(ts.SyntaxKind.DebuggerKeyword); - parseSemicolon(); - return withJSDoc(finishNode(factory.createDebuggerStatement(), pos), hasJSDoc); + if (!tryParseSemicolon()) { + parseErrorForMissingSemicolonAfter(expression); } + return withJSDoc(finishNode(factory.createThrowStatement(expression), pos), hasJSDoc); + } - function parseExpressionOrLabeledStatement(): ts.ExpressionStatement | ts.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 pos = getNodePos(); - let hasJSDoc = hasPrecedingJSDocComment(); - let node: ts.ExpressionStatement | ts.LabeledStatement; - const hasParen = token() === ts.SyntaxKind.OpenParenToken; - const expression = allowInAnd(parseExpression); - if (ts.isIdentifier(expression) && parseOptional(ts.SyntaxKind.ColonToken)) { - node = factory.createLabeledStatement(expression, parseStatement()); - } - else { - if (!tryParseSemicolon()) { - parseErrorForMissingSemicolonAfter(expression); - } - node = factory.createExpressionStatement(expression); - if (hasParen) { - // do not parse the same jsdoc twice - hasJSDoc = false; - } - } - return withJSDoc(finishNode(node, pos), hasJSDoc); - } + // TODO: Review for error recovery + function parseTryStatement(): ts.TryStatement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); - function nextTokenIsIdentifierOrKeywordOnSameLine() { - nextToken(); - return ts.tokenIsIdentifierOrKeyword(token()) && !scanner.hasPrecedingLineBreak(); - } + parseExpected(ts.SyntaxKind.TryKeyword); + const tryBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); + const catchClause = token() === ts.SyntaxKind.CatchKeyword ? parseCatchClause() : undefined; - function nextTokenIsClassKeywordOnSameLine() { - nextToken(); - return token() === ts.SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak(); + // If we don't have a catch clause, then we must have a finally clause. Try to parse + // one out no matter what. + let finallyBlock: ts.Block | undefined; + if (!catchClause || token() === ts.SyntaxKind.FinallyKeyword) { + parseExpected(ts.SyntaxKind.FinallyKeyword, ts.Diagnostics.catch_or_finally_expected); + finallyBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); } - function nextTokenIsFunctionKeywordOnSameLine() { - nextToken(); - return token() === ts.SyntaxKind.FunctionKeyword && !scanner.hasPrecedingLineBreak(); - } + return withJSDoc(finishNode(factory.createTryStatement(tryBlock, catchClause, finallyBlock), pos), hasJSDoc); + } - function nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine() { - nextToken(); - return (ts.tokenIsIdentifierOrKeyword(token()) || token() === ts.SyntaxKind.NumericLiteral || token() === ts.SyntaxKind.BigIntLiteral || token() === ts.SyntaxKind.StringLiteral) && !scanner.hasPrecedingLineBreak(); + function parseCatchClause(): ts.CatchClause { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.CatchKeyword); + + let variableDeclaration; + if (parseOptional(ts.SyntaxKind.OpenParenToken)) { + variableDeclaration = parseVariableDeclaration(); + parseExpected(ts.SyntaxKind.CloseParenToken); + } + else { + // Keep shape of node to avoid degrading performance. + variableDeclaration = undefined; } - function isDeclaration(): boolean { - while (true) { - switch (token()) { - case ts.SyntaxKind.VarKeyword: - case ts.SyntaxKind.LetKeyword: - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.ClassKeyword: - case ts.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 ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.TypeKeyword: - return nextTokenIsIdentifierOnSameLine(); - case ts.SyntaxKind.ModuleKeyword: - case ts.SyntaxKind.NamespaceKeyword: - return nextTokenIsIdentifierOrStringLiteralOnSameLine(); - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.AsyncKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - nextToken(); - // ASI takes effect for this modifier. - if (scanner.hasPrecedingLineBreak()) { - return false; - } - continue; + const block = parseBlock(/*ignoreMissingOpenBrace*/ false); + return finishNode(factory.createCatchClause(variableDeclaration, block), pos); + } - case ts.SyntaxKind.GlobalKeyword: - nextToken(); - return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.Identifier || token() === ts.SyntaxKind.ExportKeyword; - case ts.SyntaxKind.ImportKeyword: - nextToken(); - return token() === ts.SyntaxKind.StringLiteral || token() === ts.SyntaxKind.AsteriskToken || - token() === ts.SyntaxKind.OpenBraceToken || ts.tokenIsIdentifierOrKeyword(token()); - case ts.SyntaxKind.ExportKeyword: - let currentToken = nextToken(); - if (currentToken === ts.SyntaxKind.TypeKeyword) { - currentToken = lookAhead(nextToken); - } - if (currentToken === ts.SyntaxKind.EqualsToken || currentToken === ts.SyntaxKind.AsteriskToken || - currentToken === ts.SyntaxKind.OpenBraceToken || currentToken === ts.SyntaxKind.DefaultKeyword || - currentToken === ts.SyntaxKind.AsKeyword) { - return true; - } - continue; + function parseDebuggerStatement(): ts.Statement { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + parseExpected(ts.SyntaxKind.DebuggerKeyword); + parseSemicolon(); + return withJSDoc(finishNode(factory.createDebuggerStatement(), pos), hasJSDoc); + } - case ts.SyntaxKind.StaticKeyword: - nextToken(); - continue; - default: - return false; - } - } + function parseExpressionOrLabeledStatement(): ts.ExpressionStatement | ts.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 pos = getNodePos(); + let hasJSDoc = hasPrecedingJSDocComment(); + let node: ts.ExpressionStatement | ts.LabeledStatement; + const hasParen = token() === ts.SyntaxKind.OpenParenToken; + const expression = allowInAnd(parseExpression); + if (ts.isIdentifier(expression) && parseOptional(ts.SyntaxKind.ColonToken)) { + node = factory.createLabeledStatement(expression, parseStatement()); } - - function isStartOfDeclaration(): boolean { - return lookAhead(isDeclaration); + else { + if (!tryParseSemicolon()) { + parseErrorForMissingSemicolonAfter(expression); + } + node = factory.createExpressionStatement(expression); + if (hasParen) { + // do not parse the same jsdoc twice + hasJSDoc = false; + } } + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - function isStartOfStatement(): boolean { - switch (token()) { - case ts.SyntaxKind.AtToken: - case ts.SyntaxKind.SemicolonToken: - case ts.SyntaxKind.OpenBraceToken: - case ts.SyntaxKind.VarKeyword: - case ts.SyntaxKind.LetKeyword: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.ClassKeyword: - case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.IfKeyword: - case ts.SyntaxKind.DoKeyword: - case ts.SyntaxKind.WhileKeyword: - case ts.SyntaxKind.ForKeyword: - case ts.SyntaxKind.ContinueKeyword: - case ts.SyntaxKind.BreakKeyword: - case ts.SyntaxKind.ReturnKeyword: - case ts.SyntaxKind.WithKeyword: - case ts.SyntaxKind.SwitchKeyword: - case ts.SyntaxKind.ThrowKeyword: - case ts.SyntaxKind.TryKeyword: - case ts.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 ts.SyntaxKind.CatchKeyword: - case ts.SyntaxKind.FinallyKeyword: - return true; - - case ts.SyntaxKind.ImportKeyword: - return isStartOfDeclaration() || lookAhead(nextTokenIsOpenParenOrLessThanOrDot); - - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.ExportKeyword: - return isStartOfDeclaration(); - - case ts.SyntaxKind.AsyncKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.ModuleKeyword: - case ts.SyntaxKind.NamespaceKeyword: - case ts.SyntaxKind.TypeKeyword: - case ts.SyntaxKind.GlobalKeyword: - // When these don't start a declaration, they're an identifier in an expression statement - return true; - - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.StaticKeyword: - case ts.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); + function nextTokenIsIdentifierOrKeywordOnSameLine() { + nextToken(); + return ts.tokenIsIdentifierOrKeyword(token()) && !scanner.hasPrecedingLineBreak(); + } - default: - return isStartOfExpression(); - } - } + function nextTokenIsClassKeywordOnSameLine() { + nextToken(); + return token() === ts.SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak(); + } - function nextTokenIsBindingIdentifierOrStartOfDestructuring() { - nextToken(); - return isBindingIdentifier() || token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.OpenBracketToken; - } + function nextTokenIsFunctionKeywordOnSameLine() { + nextToken(); + return token() === ts.SyntaxKind.FunctionKeyword && !scanner.hasPrecedingLineBreak(); + } - function isLetDeclaration() { - // In ES6 'let' always starts a lexical declaration if followed by an identifier or { - // or [. - return lookAhead(nextTokenIsBindingIdentifierOrStartOfDestructuring); - } + function nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine() { + nextToken(); + return (ts.tokenIsIdentifierOrKeyword(token()) || token() === ts.SyntaxKind.NumericLiteral || token() === ts.SyntaxKind.BigIntLiteral || token() === ts.SyntaxKind.StringLiteral) && !scanner.hasPrecedingLineBreak(); + } - function parseStatement(): ts.Statement { + function isDeclaration(): boolean { + while (true) { switch (token()) { - case ts.SyntaxKind.SemicolonToken: - return parseEmptyStatement(); - case ts.SyntaxKind.OpenBraceToken: - return parseBlock(/*ignoreMissingOpenBrace*/ false); case ts.SyntaxKind.VarKeyword: - return parseVariableStatement(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); case ts.SyntaxKind.LetKeyword: - if (isLetDeclaration()) { - return parseVariableStatement(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); - } - break; + case ts.SyntaxKind.ConstKeyword: case ts.SyntaxKind.FunctionKeyword: - return parseFunctionDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); case ts.SyntaxKind.ClassKeyword: - return parseClassDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); - case ts.SyntaxKind.IfKeyword: - return parseIfStatement(); - case ts.SyntaxKind.DoKeyword: - return parseDoStatement(); - case ts.SyntaxKind.WhileKeyword: - return parseWhileStatement(); - case ts.SyntaxKind.ForKeyword: - return parseForOrForInOrForOfStatement(); - case ts.SyntaxKind.ContinueKeyword: - return parseBreakOrContinueStatement(ts.SyntaxKind.ContinueStatement); - case ts.SyntaxKind.BreakKeyword: - return parseBreakOrContinueStatement(ts.SyntaxKind.BreakStatement); - case ts.SyntaxKind.ReturnKeyword: - return parseReturnStatement(); - case ts.SyntaxKind.WithKeyword: - return parseWithStatement(); - case ts.SyntaxKind.SwitchKeyword: - return parseSwitchStatement(); - case ts.SyntaxKind.ThrowKeyword: - return parseThrowStatement(); - case ts.SyntaxKind.TryKeyword: - // Include 'catch' and 'finally' for error recovery. - // falls through - case ts.SyntaxKind.CatchKeyword: - case ts.SyntaxKind.FinallyKeyword: - return parseTryStatement(); - case ts.SyntaxKind.DebuggerKeyword: - return parseDebuggerStatement(); - case ts.SyntaxKind.AtToken: - return parseDeclaration(); - case ts.SyntaxKind.AsyncKeyword: + case ts.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 ts.SyntaxKind.InterfaceKeyword: case ts.SyntaxKind.TypeKeyword: + return nextTokenIsIdentifierOnSameLine(); case ts.SyntaxKind.ModuleKeyword: case ts.SyntaxKind.NamespaceKeyword: + return nextTokenIsIdentifierOrStringLiteralOnSameLine(); + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.AsyncKeyword: case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.ExportKeyword: - case ts.SyntaxKind.ImportKeyword: case ts.SyntaxKind.PrivateKeyword: case ts.SyntaxKind.ProtectedKeyword: case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.StaticKeyword: case ts.SyntaxKind.ReadonlyKeyword: - case ts.SyntaxKind.GlobalKeyword: - if (isStartOfDeclaration()) { - return parseDeclaration(); + nextToken(); + // ASI takes effect for this modifier. + if (scanner.hasPrecedingLineBreak()) { + return false; } - break; - } - return parseExpressionOrLabeledStatement(); - } - - function isDeclareModifier(modifier: ts.Modifier) { - return modifier.kind === ts.SyntaxKind.DeclareKeyword; - } - - function parseDeclaration(): ts.Statement { - // TODO: Can we hold onto the parsed decorators/modifiers and advance the scanner - // if we can't reuse the declaration, so that we don't do this work twice? - // - // `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 = ts.some(lookAhead(() => (parseDecorators(), parseModifiers())), isDeclareModifier); - if (isAmbient) { - const node = tryReuseAmbientDeclaration(); - if (node) { - return node; - } - } - - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const decorators = parseDecorators(); - const modifiers = parseModifiers(); - if (isAmbient) { - for (const m of modifiers!) { - (m as ts.Mutable).flags |= ts.NodeFlags.Ambient; - } - return doInsideOfContext(ts.NodeFlags.Ambient, () => parseDeclarationWorker(pos, hasJSDoc, decorators, modifiers)); - } - else { - return parseDeclarationWorker(pos, hasJSDoc, decorators, modifiers); - } - } - - function tryReuseAmbientDeclaration(): ts.Statement | undefined { - return doInsideOfContext(ts.NodeFlags.Ambient, () => { - const node = currentNode(parsingContext); - if (node) { - return consumeNode(node) as ts.Statement; - } - }); - } + continue; - function parseDeclarationWorker(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.Statement { - switch (token()) { - case ts.SyntaxKind.VarKeyword: - case ts.SyntaxKind.LetKeyword: - case ts.SyntaxKind.ConstKeyword: - return parseVariableStatement(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.FunctionKeyword: - return parseFunctionDeclaration(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.ClassKeyword: - return parseClassDeclaration(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.InterfaceKeyword: - return parseInterfaceDeclaration(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.TypeKeyword: - return parseTypeAliasDeclaration(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.EnumKeyword: - return parseEnumDeclaration(pos, hasJSDoc, decorators, modifiers); case ts.SyntaxKind.GlobalKeyword: - case ts.SyntaxKind.ModuleKeyword: - case ts.SyntaxKind.NamespaceKeyword: - return parseModuleDeclaration(pos, hasJSDoc, decorators, modifiers); + nextToken(); + return token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.Identifier || token() === ts.SyntaxKind.ExportKeyword; case ts.SyntaxKind.ImportKeyword: - return parseImportDeclarationOrImportEqualsDeclaration(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.ExportKeyword: nextToken(); - switch (token()) { - case ts.SyntaxKind.DefaultKeyword: - case ts.SyntaxKind.EqualsToken: - return parseExportAssignment(pos, hasJSDoc, decorators, modifiers); - case ts.SyntaxKind.AsKeyword: - return parseNamespaceExportDeclaration(pos, hasJSDoc, decorators, modifiers); - default: - return parseExportDeclaration(pos, hasJSDoc, decorators, modifiers); + return token() === ts.SyntaxKind.StringLiteral || token() === ts.SyntaxKind.AsteriskToken || + token() === ts.SyntaxKind.OpenBraceToken || ts.tokenIsIdentifierOrKeyword(token()); + case ts.SyntaxKind.ExportKeyword: + let currentToken = nextToken(); + if (currentToken === ts.SyntaxKind.TypeKeyword) { + currentToken = lookAhead(nextToken); } - default: - if (decorators || 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(ts.SyntaxKind.MissingDeclaration, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Declaration_expected); - ts.setTextRangePos(missing, pos); - missing.decorators = decorators; - missing.modifiers = modifiers; - return missing; + if (currentToken === ts.SyntaxKind.EqualsToken || currentToken === ts.SyntaxKind.AsteriskToken || + currentToken === ts.SyntaxKind.OpenBraceToken || currentToken === ts.SyntaxKind.DefaultKeyword || + currentToken === ts.SyntaxKind.AsKeyword) { + return true; } - return undefined!; // TODO: GH#18217 + continue; + + case ts.SyntaxKind.StaticKeyword: + nextToken(); + continue; + default: + return false; } } + } - function nextTokenIsIdentifierOrStringLiteralOnSameLine() { - nextToken(); - return !scanner.hasPrecedingLineBreak() && (isIdentifier() || token() === ts.SyntaxKind.StringLiteral); - } + function isStartOfDeclaration(): boolean { + return lookAhead(isDeclaration); + } - function parseFunctionBlockOrSemicolon(flags: SignatureFlags, diagnosticMessage?: ts.DiagnosticMessage): ts.Block | undefined { - if (token() !== ts.SyntaxKind.OpenBraceToken && canParseSemicolon()) { - parseSemicolon(); - return; - } + function isStartOfStatement(): boolean { + switch (token()) { + case ts.SyntaxKind.AtToken: + case ts.SyntaxKind.SemicolonToken: + case ts.SyntaxKind.OpenBraceToken: + case ts.SyntaxKind.VarKeyword: + case ts.SyntaxKind.LetKeyword: + case ts.SyntaxKind.FunctionKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.IfKeyword: + case ts.SyntaxKind.DoKeyword: + case ts.SyntaxKind.WhileKeyword: + case ts.SyntaxKind.ForKeyword: + case ts.SyntaxKind.ContinueKeyword: + case ts.SyntaxKind.BreakKeyword: + case ts.SyntaxKind.ReturnKeyword: + case ts.SyntaxKind.WithKeyword: + case ts.SyntaxKind.SwitchKeyword: + case ts.SyntaxKind.ThrowKeyword: + case ts.SyntaxKind.TryKeyword: + case ts.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 ts.SyntaxKind.CatchKeyword: + case ts.SyntaxKind.FinallyKeyword: + return true; - return parseFunctionBlock(flags, diagnosticMessage); - } + case ts.SyntaxKind.ImportKeyword: + return isStartOfDeclaration() || lookAhead(nextTokenIsOpenParenOrLessThanOrDot); + + case ts.SyntaxKind.ConstKeyword: + case ts.SyntaxKind.ExportKeyword: + return isStartOfDeclaration(); + + case ts.SyntaxKind.AsyncKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.GlobalKeyword: + // When these don't start a declaration, they're an identifier in an expression statement + return true; - // DECLARATIONS + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.StaticKeyword: + case ts.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); - function parseArrayBindingElement(): ts.ArrayBindingElement { - const pos = getNodePos(); - if (token() === ts.SyntaxKind.CommaToken) { - return finishNode(factory.createOmittedExpression(), pos); - } - const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); - const name = parseIdentifierOrPattern(); - const initializer = parseInitializer(); - return finishNode(factory.createBindingElement(dotDotDotToken, /*propertyName*/ undefined, name, initializer), pos); + default: + return isStartOfExpression(); } + } - function parseObjectBindingElement(): ts.BindingElement { - const pos = getNodePos(); - const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); - const tokenIsIdentifier = isBindingIdentifier(); - let propertyName: ts.PropertyName | undefined = parsePropertyName(); - let name: ts.BindingName; - if (tokenIsIdentifier && token() !== ts.SyntaxKind.ColonToken) { - name = propertyName as ts.Identifier; - propertyName = undefined; - } - else { - parseExpected(ts.SyntaxKind.ColonToken); - name = parseIdentifierOrPattern(); - } - const initializer = parseInitializer(); - return finishNode(factory.createBindingElement(dotDotDotToken, propertyName, name, initializer), pos); - } + function nextTokenIsBindingIdentifierOrStartOfDestructuring() { + nextToken(); + return isBindingIdentifier() || token() === ts.SyntaxKind.OpenBraceToken || token() === ts.SyntaxKind.OpenBracketToken; + } - function parseObjectBindingPattern(): ts.ObjectBindingPattern { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenBraceToken); - const elements = parseDelimitedList(ParsingContext.ObjectBindingElements, parseObjectBindingElement); - parseExpected(ts.SyntaxKind.CloseBraceToken); - return finishNode(factory.createObjectBindingPattern(elements), pos); - } + function isLetDeclaration() { + // In ES6 'let' always starts a lexical declaration if followed by an identifier or { + // or [. + return lookAhead(nextTokenIsBindingIdentifierOrStartOfDestructuring); + } - function parseArrayBindingPattern(): ts.ArrayBindingPattern { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.OpenBracketToken); - const elements = parseDelimitedList(ParsingContext.ArrayBindingElements, parseArrayBindingElement); - parseExpected(ts.SyntaxKind.CloseBracketToken); - return finishNode(factory.createArrayBindingPattern(elements), pos); + function parseStatement(): ts.Statement { + switch (token()) { + case ts.SyntaxKind.SemicolonToken: + return parseEmptyStatement(); + case ts.SyntaxKind.OpenBraceToken: + return parseBlock(/*ignoreMissingOpenBrace*/ false); + case ts.SyntaxKind.VarKeyword: + return parseVariableStatement(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); + case ts.SyntaxKind.LetKeyword: + if (isLetDeclaration()) { + return parseVariableStatement(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); + } + break; + case ts.SyntaxKind.FunctionKeyword: + return parseFunctionDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); + case ts.SyntaxKind.ClassKeyword: + return parseClassDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined); + case ts.SyntaxKind.IfKeyword: + return parseIfStatement(); + case ts.SyntaxKind.DoKeyword: + return parseDoStatement(); + case ts.SyntaxKind.WhileKeyword: + return parseWhileStatement(); + case ts.SyntaxKind.ForKeyword: + return parseForOrForInOrForOfStatement(); + case ts.SyntaxKind.ContinueKeyword: + return parseBreakOrContinueStatement(ts.SyntaxKind.ContinueStatement); + case ts.SyntaxKind.BreakKeyword: + return parseBreakOrContinueStatement(ts.SyntaxKind.BreakStatement); + case ts.SyntaxKind.ReturnKeyword: + return parseReturnStatement(); + case ts.SyntaxKind.WithKeyword: + return parseWithStatement(); + case ts.SyntaxKind.SwitchKeyword: + return parseSwitchStatement(); + case ts.SyntaxKind.ThrowKeyword: + return parseThrowStatement(); + case ts.SyntaxKind.TryKeyword: + // Include 'catch' and 'finally' for error recovery. + // falls through + case ts.SyntaxKind.CatchKeyword: + case ts.SyntaxKind.FinallyKeyword: + return parseTryStatement(); + case ts.SyntaxKind.DebuggerKeyword: + return parseDebuggerStatement(); + case ts.SyntaxKind.AtToken: + return parseDeclaration(); + case ts.SyntaxKind.AsyncKeyword: + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.ConstKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.ImportKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.StaticKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + case ts.SyntaxKind.GlobalKeyword: + if (isStartOfDeclaration()) { + return parseDeclaration(); + } + break; } + return parseExpressionOrLabeledStatement(); + } - function isBindingIdentifierOrPrivateIdentifierOrPattern() { - return token() === ts.SyntaxKind.OpenBraceToken - || token() === ts.SyntaxKind.OpenBracketToken - || token() === ts.SyntaxKind.PrivateIdentifier - || isBindingIdentifier(); - } + function isDeclareModifier(modifier: ts.Modifier) { + return modifier.kind === ts.SyntaxKind.DeclareKeyword; + } - function parseIdentifierOrPattern(privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier | ts.BindingPattern { - if (token() === ts.SyntaxKind.OpenBracketToken) { - return parseArrayBindingPattern(); - } - if (token() === ts.SyntaxKind.OpenBraceToken) { - return parseObjectBindingPattern(); + function parseDeclaration(): ts.Statement { + // TODO: Can we hold onto the parsed decorators/modifiers and advance the scanner + // if we can't reuse the declaration, so that we don't do this work twice? + // + // `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 = ts.some(lookAhead(() => (parseDecorators(), parseModifiers())), isDeclareModifier); + if (isAmbient) { + const node = tryReuseAmbientDeclaration(); + if (node) { + return node; } - return parseBindingIdentifier(privateIdentifierDiagnosticMessage); } - function parseVariableDeclarationAllowExclamation() { - return parseVariableDeclaration(/*allowExclamation*/ true); - } - - function parseVariableDeclaration(allowExclamation?: boolean): ts.VariableDeclaration { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const name = parseIdentifierOrPattern(ts.Diagnostics.Private_identifiers_are_not_allowed_in_variable_declarations); - let exclamationToken: ts.ExclamationToken | undefined; - if (allowExclamation && name.kind === ts.SyntaxKind.Identifier && - token() === ts.SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - exclamationToken = parseTokenNode>(); + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const decorators = parseDecorators(); + const modifiers = parseModifiers(); + if (isAmbient) { + for (const m of modifiers!) { + (m as ts.Mutable).flags |= ts.NodeFlags.Ambient; } - const type = parseTypeAnnotation(); - const initializer = isInOrOfKeyword(token()) ? undefined : parseInitializer(); - const node = factory.createVariableDeclaration(name, exclamationToken, type, initializer); - return withJSDoc(finishNode(node, pos), hasJSDoc); + return doInsideOfContext(ts.NodeFlags.Ambient, () => parseDeclarationWorker(pos, hasJSDoc, decorators, modifiers)); } + else { + return parseDeclarationWorker(pos, hasJSDoc, decorators, modifiers); + } + } - function parseVariableDeclarationList(inForStatementInitializer: boolean): ts.VariableDeclarationList { - const pos = getNodePos(); - - let flags: ts.NodeFlags = 0; - switch (token()) { - case ts.SyntaxKind.VarKeyword: - break; - case ts.SyntaxKind.LetKeyword: - flags |= ts.NodeFlags.Let; - break; - case ts.SyntaxKind.ConstKeyword: - flags |= ts.NodeFlags.Const; - break; - default: - ts.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. - let declarations: readonly ts.VariableDeclaration[]; - if (token() === ts.SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) { - declarations = createMissingList(); - } - else { - const savedDisallowIn = inDisallowInContext(); - setDisallowInContext(inForStatementInitializer); - - declarations = parseDelimitedList(ParsingContext.VariableDeclarations, inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation); - - setDisallowInContext(savedDisallowIn); + function tryReuseAmbientDeclaration(): ts.Statement | undefined { + return doInsideOfContext(ts.NodeFlags.Ambient, () => { + const node = currentNode(parsingContext); + if (node) { + return consumeNode(node) as ts.Statement; } + }); + } - return finishNode(factory.createVariableDeclarationList(declarations, flags), pos); + function parseDeclarationWorker(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.Statement { + switch (token()) { + case ts.SyntaxKind.VarKeyword: + case ts.SyntaxKind.LetKeyword: + case ts.SyntaxKind.ConstKeyword: + return parseVariableStatement(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.FunctionKeyword: + return parseFunctionDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.ClassKeyword: + return parseClassDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.InterfaceKeyword: + return parseInterfaceDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.TypeKeyword: + return parseTypeAliasDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.EnumKeyword: + return parseEnumDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.GlobalKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.NamespaceKeyword: + return parseModuleDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.ImportKeyword: + return parseImportDeclarationOrImportEqualsDeclaration(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.ExportKeyword: + nextToken(); + switch (token()) { + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.EqualsToken: + return parseExportAssignment(pos, hasJSDoc, decorators, modifiers); + case ts.SyntaxKind.AsKeyword: + return parseNamespaceExportDeclaration(pos, hasJSDoc, decorators, modifiers); + default: + return parseExportDeclaration(pos, hasJSDoc, decorators, modifiers); + } + default: + if (decorators || 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(ts.SyntaxKind.MissingDeclaration, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Declaration_expected); + ts.setTextRangePos(missing, pos); + missing.decorators = decorators; + missing.modifiers = modifiers; + return missing; + } + return undefined!; // TODO: GH#18217 } + } - function canFollowContextualOfKeyword(): boolean { - return nextTokenIsIdentifier() && nextToken() === ts.SyntaxKind.CloseParenToken; - } + function nextTokenIsIdentifierOrStringLiteralOnSameLine() { + nextToken(); + return !scanner.hasPrecedingLineBreak() && (isIdentifier() || token() === ts.SyntaxKind.StringLiteral); + } - function parseVariableStatement(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.VariableStatement { - const declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false); + function parseFunctionBlockOrSemicolon(flags: SignatureFlags, diagnosticMessage?: ts.DiagnosticMessage): ts.Block | undefined { + if (token() !== ts.SyntaxKind.OpenBraceToken && canParseSemicolon()) { parseSemicolon(); - const node = factory.createVariableStatement(modifiers, declarationList); - // Decorators are not allowed on a variable statement, so we keep track of them to report them in the grammar checker. - node.decorators = decorators; - return withJSDoc(finishNode(node, pos), hasJSDoc); - } - - function parseFunctionDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.FunctionDeclaration { - const savedAwaitContext = inAwaitContext(); - - const modifierFlags = ts.modifiersToFlags(modifiers); - parseExpected(ts.SyntaxKind.FunctionKeyword); - const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); - // We don't parse the name here in await context, instead we will report a grammar error in the checker. - const name = modifierFlags & ts.ModifierFlags.Default ? parseOptionalBindingIdentifier() : parseBindingIdentifier(); - const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = modifierFlags & ts.ModifierFlags.Async ? SignatureFlags.Await : SignatureFlags.None; - const typeParameters = parseTypeParameters(); - if (modifierFlags & ts.ModifierFlags.Export) - setAwaitContext(/*value*/ true); - const parameters = parseParameters(isGenerator | isAsync); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - const body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, ts.Diagnostics.or_expected); - setAwaitContext(savedAwaitContext); - const node = factory.createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body); - return withJSDoc(finishNode(node, pos), hasJSDoc); + return; } - function parseConstructorName() { - if (token() === ts.SyntaxKind.ConstructorKeyword) { - return parseExpected(ts.SyntaxKind.ConstructorKeyword); - } - if (token() === ts.SyntaxKind.StringLiteral && lookAhead(nextToken) === ts.SyntaxKind.OpenParenToken) { - return tryParse(() => { - const literalNode = parseLiteralNode(); - return literalNode.text === "constructor" ? literalNode : undefined; - }); - } - } + return parseFunctionBlock(flags, diagnosticMessage); + } - function tryParseConstructorDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ConstructorDeclaration | undefined { - return tryParse(() => { - if (parseConstructorName()) { - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(SignatureFlags.None); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - const body = parseFunctionBlockOrSemicolon(SignatureFlags.None, ts.Diagnostics.or_expected); - const node = factory.createConstructorDeclaration(decorators, modifiers, parameters, body); - // Attach `typeParameters` and `type` if they exist so that we can report them in the grammar checker. - node.typeParameters = typeParameters; - node.type = type; - return withJSDoc(finishNode(node, pos), hasJSDoc); - } - }); - } + // DECLARATIONS - function parseMethodDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, exclamationToken: ts.ExclamationToken | undefined, diagnosticMessage?: ts.DiagnosticMessage): ts.MethodDeclaration { - const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? SignatureFlags.Await : SignatureFlags.None; - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(isGenerator | isAsync); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - const body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, diagnosticMessage); - const node = factory.createMethodDeclaration(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body); - // An exclamation token on a method is invalid syntax and will be handled by the grammar checker - node.exclamationToken = exclamationToken; - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseArrayBindingElement(): ts.ArrayBindingElement { + const pos = getNodePos(); + if (token() === ts.SyntaxKind.CommaToken) { + return finishNode(factory.createOmittedExpression(), pos); } + const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); + const name = parseIdentifierOrPattern(); + const initializer = parseInitializer(); + return finishNode(factory.createBindingElement(dotDotDotToken, /*propertyName*/ undefined, name, initializer), pos); + } - function parsePropertyDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined): ts.PropertyDeclaration { - const exclamationToken = !questionToken && !scanner.hasPrecedingLineBreak() ? parseOptionalToken(ts.SyntaxKind.ExclamationToken) : undefined; - const type = parseTypeAnnotation(); - const initializer = doOutsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext | ts.NodeFlags.DisallowInContext, parseInitializer); - parseSemicolonAfterPropertyName(name, type, initializer); - const node = factory.createPropertyDeclaration(decorators, modifiers, name, questionToken || exclamationToken, type, initializer); - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseObjectBindingElement(): ts.BindingElement { + const pos = getNodePos(); + const dotDotDotToken = parseOptionalToken(ts.SyntaxKind.DotDotDotToken); + const tokenIsIdentifier = isBindingIdentifier(); + let propertyName: ts.PropertyName | undefined = parsePropertyName(); + let name: ts.BindingName; + if (tokenIsIdentifier && token() !== ts.SyntaxKind.ColonToken) { + name = propertyName as ts.Identifier; + propertyName = undefined; } - - function parsePropertyOrMethodDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.PropertyDeclaration | ts.MethodDeclaration { - const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); - const 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. - const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); - if (asteriskToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { - return parseMethodDeclaration(pos, hasJSDoc, decorators, modifiers, asteriskToken, name, questionToken, /*exclamationToken*/ undefined, ts.Diagnostics.or_expected); - } - return parsePropertyDeclaration(pos, hasJSDoc, decorators, modifiers, name, questionToken); + else { + parseExpected(ts.SyntaxKind.ColonToken); + name = parseIdentifierOrPattern(); } + const initializer = parseInitializer(); + return finishNode(factory.createBindingElement(dotDotDotToken, propertyName, name, initializer), pos); + } - function parseAccessorDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, kind: ts.AccessorDeclaration["kind"]): ts.AccessorDeclaration { - const name = parsePropertyName(); - const typeParameters = parseTypeParameters(); - const parameters = parseParameters(SignatureFlags.None); - const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); - const body = parseFunctionBlockOrSemicolon(SignatureFlags.None); - const node = kind === ts.SyntaxKind.GetAccessor - ? factory.createGetAccessorDeclaration(decorators, modifiers, name, parameters, type, body) - : factory.createSetAccessorDeclaration(decorators, modifiers, name, parameters, body); - // Keep track of `typeParameters` (for both) and `type` (for setters) if they were parsed those indicate grammar errors - node.typeParameters = typeParameters; - if (type && node.kind === ts.SyntaxKind.SetAccessor) - (node as ts.Mutable).type = type; - return withJSDoc(finishNode(node, pos), hasJSDoc); - } + function parseObjectBindingPattern(): ts.ObjectBindingPattern { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenBraceToken); + const elements = parseDelimitedList(ParsingContext.ObjectBindingElements, parseObjectBindingElement); + parseExpected(ts.SyntaxKind.CloseBraceToken); + return finishNode(factory.createObjectBindingPattern(elements), pos); + } - function isClassMemberStart(): boolean { - let idToken: ts.SyntaxKind | undefined; - if (token() === ts.SyntaxKind.AtToken) { - return true; - } + function parseArrayBindingPattern(): ts.ArrayBindingPattern { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.OpenBracketToken); + const elements = parseDelimitedList(ParsingContext.ArrayBindingElements, parseArrayBindingElement); + parseExpected(ts.SyntaxKind.CloseBracketToken); + return finishNode(factory.createArrayBindingPattern(elements), pos); + } - // Eat up all modifiers, but hold on to the last one in case it is actually an identifier. - while (ts.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 (ts.isClassMemberModifier(idToken)) { - return true; - } + function isBindingIdentifierOrPrivateIdentifierOrPattern() { + return token() === ts.SyntaxKind.OpenBraceToken + || token() === ts.SyntaxKind.OpenBracketToken + || token() === ts.SyntaxKind.PrivateIdentifier + || isBindingIdentifier(); + } - nextToken(); - } + function parseIdentifierOrPattern(privateIdentifierDiagnosticMessage?: ts.DiagnosticMessage): ts.Identifier | ts.BindingPattern { + if (token() === ts.SyntaxKind.OpenBracketToken) { + return parseArrayBindingPattern(); + } + if (token() === ts.SyntaxKind.OpenBraceToken) { + return parseObjectBindingPattern(); + } + return parseBindingIdentifier(privateIdentifierDiagnosticMessage); + } - if (token() === ts.SyntaxKind.AsteriskToken) { - return true; - } + function parseVariableDeclarationAllowExclamation() { + return parseVariableDeclaration(/*allowExclamation*/ 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(); - } + function parseVariableDeclaration(allowExclamation?: boolean): ts.VariableDeclaration { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const name = parseIdentifierOrPattern(ts.Diagnostics.Private_identifiers_are_not_allowed_in_variable_declarations); + let exclamationToken: ts.ExclamationToken | undefined; + if (allowExclamation && name.kind === ts.SyntaxKind.Identifier && + token() === ts.SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { + exclamationToken = parseTokenNode>(); + } + const type = parseTypeAnnotation(); + const initializer = isInOrOfKeyword(token()) ? undefined : parseInitializer(); + const node = factory.createVariableDeclaration(name, exclamationToken, type, initializer); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - // Index signatures and computed properties are class members; we can parse. - if (token() === ts.SyntaxKind.OpenBracketToken) { - return true; - } + function parseVariableDeclarationList(inForStatementInitializer: boolean): ts.VariableDeclarationList { + const pos = getNodePos(); - // 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 (!ts.isKeyword(idToken) || idToken === ts.SyntaxKind.SetKeyword || idToken === ts.SyntaxKind.GetKeyword) { - return true; - } + let flags: ts.NodeFlags = 0; + switch (token()) { + case ts.SyntaxKind.VarKeyword: + break; + case ts.SyntaxKind.LetKeyword: + flags |= ts.NodeFlags.Let; + break; + case ts.SyntaxKind.ConstKeyword: + flags |= ts.NodeFlags.Const; + break; + default: + ts.Debug.fail(); + } - // 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 ts.SyntaxKind.OpenParenToken: // Method declaration - case ts.SyntaxKind.LessThanToken: // Generic Method declaration - case ts.SyntaxKind.ExclamationToken: // Non-null assertion on property name - case ts.SyntaxKind.ColonToken: // Type Annotation for declaration - case ts.SyntaxKind.EqualsToken: // Initializer for declaration - case ts.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(); - } - } + nextToken(); - return false; + // 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. + let declarations: readonly ts.VariableDeclaration[]; + if (token() === ts.SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) { + declarations = createMissingList(); } + else { + const savedDisallowIn = inDisallowInContext(); + setDisallowInContext(inForStatementInitializer); - function parseClassStaticBlockDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.ModifiersArray | undefined): ts.ClassStaticBlockDeclaration { - parseExpectedToken(ts.SyntaxKind.StaticKeyword); - const body = parseClassStaticBlockBody(); - return withJSDoc(finishNode(factory.createClassStaticBlockDeclaration(decorators, modifiers, body), pos), hasJSDoc); + declarations = parseDelimitedList(ParsingContext.VariableDeclarations, inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation); + + setDisallowInContext(savedDisallowIn); } - function parseClassStaticBlockBody() { - const savedYieldContext = inYieldContext(); - const savedAwaitContext = inAwaitContext(); + return finishNode(factory.createVariableDeclarationList(declarations, flags), pos); + } - setYieldContext(false); - setAwaitContext(true); + function canFollowContextualOfKeyword(): boolean { + return nextTokenIsIdentifier() && nextToken() === ts.SyntaxKind.CloseParenToken; + } - const body = parseBlock(/*ignoreMissingOpenBrace*/ false); + function parseVariableStatement(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.VariableStatement { + const declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false); + parseSemicolon(); + const node = factory.createVariableStatement(modifiers, declarationList); + // Decorators are not allowed on a variable statement, so we keep track of them to report them in the grammar checker. + node.decorators = decorators; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - setYieldContext(savedYieldContext); - setAwaitContext(savedAwaitContext); + function parseFunctionDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.FunctionDeclaration { + const savedAwaitContext = inAwaitContext(); + + const modifierFlags = ts.modifiersToFlags(modifiers); + parseExpected(ts.SyntaxKind.FunctionKeyword); + const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); + // We don't parse the name here in await context, instead we will report a grammar error in the checker. + const name = modifierFlags & ts.ModifierFlags.Default ? parseOptionalBindingIdentifier() : parseBindingIdentifier(); + const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = modifierFlags & ts.ModifierFlags.Async ? SignatureFlags.Await : SignatureFlags.None; + const typeParameters = parseTypeParameters(); + if (modifierFlags & ts.ModifierFlags.Export) + setAwaitContext(/*value*/ true); + const parameters = parseParameters(isGenerator | isAsync); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + const body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, ts.Diagnostics.or_expected); + setAwaitContext(savedAwaitContext); + const node = factory.createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - return body; + function parseConstructorName() { + if (token() === ts.SyntaxKind.ConstructorKeyword) { + return parseExpected(ts.SyntaxKind.ConstructorKeyword); } - - function parseDecoratorExpression() { - if (inAwaitContext() && token() === ts.SyntaxKind.AwaitKeyword) { - // `@await` is is disallowed in an [Await] context, but can cause parsing to go off the rails - // This simply parses the missing identifier and moves on. - const pos = getNodePos(); - const awaitExpression = parseIdentifier(ts.Diagnostics.Expression_expected); - nextToken(); - const memberExpression = parseMemberExpressionRest(pos, awaitExpression, /*allowOptionalChain*/ true); - return parseCallExpressionRest(pos, memberExpression); - } - return parseLeftHandSideExpressionOrHigher(); - } - - function tryParseDecorator(): ts.Decorator | undefined { - const pos = getNodePos(); - if (!parseOptional(ts.SyntaxKind.AtToken)) { - return undefined; - } - const expression = doInDecoratorContext(parseDecoratorExpression); - return finishNode(factory.createDecorator(expression), pos); + if (token() === ts.SyntaxKind.StringLiteral && lookAhead(nextToken) === ts.SyntaxKind.OpenParenToken) { + return tryParse(() => { + const literalNode = parseLiteralNode(); + return literalNode.text === "constructor" ? literalNode : undefined; + }); } + } - function parseDecorators(): ts.NodeArray | undefined { - const pos = getNodePos(); - let list, decorator; - while (decorator = tryParseDecorator()) { - list = ts.append(list, decorator); + function tryParseConstructorDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ConstructorDeclaration | undefined { + return tryParse(() => { + if (parseConstructorName()) { + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(SignatureFlags.None); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + const body = parseFunctionBlockOrSemicolon(SignatureFlags.None, ts.Diagnostics.or_expected); + const node = factory.createConstructorDeclaration(decorators, modifiers, parameters, body); + // Attach `typeParameters` and `type` if they exist so that we can report them in the grammar checker. + node.typeParameters = typeParameters; + node.type = type; + return withJSDoc(finishNode(node, pos), hasJSDoc); } - return list && createNodeArray(list, pos); - } + }); + } - function tryParseModifier(permitInvalidConstAsModifier?: boolean, stopOnStartOfClassStaticBlock?: boolean, hasSeenStaticModifier?: boolean): ts.Modifier | undefined { - const pos = getNodePos(); - const kind = token(); + function parseMethodDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, asteriskToken: ts.AsteriskToken | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined, exclamationToken: ts.ExclamationToken | undefined, diagnosticMessage?: ts.DiagnosticMessage): ts.MethodDeclaration { + const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = ts.some(modifiers, ts.isAsyncModifier) ? SignatureFlags.Await : SignatureFlags.None; + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(isGenerator | isAsync); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + const body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, diagnosticMessage); + const node = factory.createMethodDeclaration(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body); + // An exclamation token on a method is invalid syntax and will be handled by the grammar checker + node.exclamationToken = exclamationToken; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - if (token() === ts.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)) { - return undefined; - } - } - else if (stopOnStartOfClassStaticBlock && token() === ts.SyntaxKind.StaticKeyword && lookAhead(nextTokenIsOpenBrace)) { - return undefined; - } - else if (hasSeenStaticModifier && token() === ts.SyntaxKind.StaticKeyword) { - return undefined; - } - else { - if (!parseAnyContextualModifier()) { - return undefined; - } - } + function parsePropertyDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, name: ts.PropertyName, questionToken: ts.QuestionToken | undefined): ts.PropertyDeclaration { + const exclamationToken = !questionToken && !scanner.hasPrecedingLineBreak() ? parseOptionalToken(ts.SyntaxKind.ExclamationToken) : undefined; + const type = parseTypeAnnotation(); + const initializer = doOutsideOfContext(ts.NodeFlags.YieldContext | ts.NodeFlags.AwaitContext | ts.NodeFlags.DisallowInContext, parseInitializer); + parseSemicolonAfterPropertyName(name, type, initializer); + const node = factory.createPropertyDeclaration(decorators, modifiers, name, questionToken || exclamationToken, type, initializer); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - return finishNode(factory.createToken(kind as ts.Modifier["kind"]), pos); + function parsePropertyOrMethodDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.PropertyDeclaration | ts.MethodDeclaration { + const asteriskToken = parseOptionalToken(ts.SyntaxKind.AsteriskToken); + const 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. + const questionToken = parseOptionalToken(ts.SyntaxKind.QuestionToken); + if (asteriskToken || token() === ts.SyntaxKind.OpenParenToken || token() === ts.SyntaxKind.LessThanToken) { + return parseMethodDeclaration(pos, hasJSDoc, decorators, modifiers, asteriskToken, name, questionToken, /*exclamationToken*/ undefined, ts.Diagnostics.or_expected); } + return parsePropertyDeclaration(pos, hasJSDoc, decorators, modifiers, name, questionToken); + } - /* - * 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, stopOnStartOfClassStaticBlock?: boolean): ts.NodeArray | undefined { - const pos = getNodePos(); - let list, modifier, hasSeenStatic = false; - while (modifier = tryParseModifier(permitInvalidConstAsModifier, stopOnStartOfClassStaticBlock, hasSeenStatic)) { - if (modifier.kind === ts.SyntaxKind.StaticKeyword) - hasSeenStatic = true; - list = ts.append(list, modifier); - } - return list && createNodeArray(list, pos); - } + function parseAccessorDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, kind: ts.AccessorDeclaration["kind"]): ts.AccessorDeclaration { + const name = parsePropertyName(); + const typeParameters = parseTypeParameters(); + const parameters = parseParameters(SignatureFlags.None); + const type = parseReturnType(ts.SyntaxKind.ColonToken, /*isType*/ false); + const body = parseFunctionBlockOrSemicolon(SignatureFlags.None); + const node = kind === ts.SyntaxKind.GetAccessor + ? factory.createGetAccessorDeclaration(decorators, modifiers, name, parameters, type, body) + : factory.createSetAccessorDeclaration(decorators, modifiers, name, parameters, body); + // Keep track of `typeParameters` (for both) and `type` (for setters) if they were parsed those indicate grammar errors + node.typeParameters = typeParameters; + if (type && node.kind === ts.SyntaxKind.SetAccessor) + (node as ts.Mutable).type = type; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - function parseModifiersForArrowFunction(): ts.NodeArray | undefined { - let modifiers: ts.NodeArray | undefined; - if (token() === ts.SyntaxKind.AsyncKeyword) { - const pos = getNodePos(); - nextToken(); - const modifier = finishNode(factory.createToken(ts.SyntaxKind.AsyncKeyword), pos); - modifiers = createNodeArray([modifier], pos); - } - return modifiers; + function isClassMemberStart(): boolean { + let idToken: ts.SyntaxKind | undefined; + if (token() === ts.SyntaxKind.AtToken) { + return true; } - function parseClassElement(): ts.ClassElement { - const pos = getNodePos(); - if (token() === ts.SyntaxKind.SemicolonToken) { - nextToken(); - return finishNode(factory.createSemicolonClassElement(), pos); + // Eat up all modifiers, but hold on to the last one in case it is actually an identifier. + while (ts.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 (ts.isClassMemberModifier(idToken)) { + return true; } - const hasJSDoc = hasPrecedingJSDocComment(); - const decorators = parseDecorators(); - const modifiers = parseModifiers(/*permitInvalidConstAsModifier*/ true, /*stopOnStartOfClassStaticBlock*/ true); - if (token() === ts.SyntaxKind.StaticKeyword && lookAhead(nextTokenIsOpenBrace)) { - return parseClassStaticBlockDeclaration(pos, hasJSDoc, decorators, modifiers); - } + nextToken(); + } - if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.GetAccessor); - } + if (token() === ts.SyntaxKind.AsteriskToken) { + return true; + } - if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.SetAccessor); - } + // 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(); + } - if (token() === ts.SyntaxKind.ConstructorKeyword || token() === ts.SyntaxKind.StringLiteral) { - const constructorDeclaration = tryParseConstructorDeclaration(pos, hasJSDoc, decorators, modifiers); - if (constructorDeclaration) { - return constructorDeclaration; - } - } + // Index signatures and computed properties are class members; we can parse. + if (token() === ts.SyntaxKind.OpenBracketToken) { + return true; + } - if (isIndexSignature()) { - return parseIndexSignatureDeclaration(pos, hasJSDoc, decorators, modifiers); + // 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 (!ts.isKeyword(idToken) || idToken === ts.SyntaxKind.SetKeyword || idToken === ts.SyntaxKind.GetKeyword) { + return true; } - // 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 (ts.tokenIsIdentifierOrKeyword(token()) || - token() === ts.SyntaxKind.StringLiteral || - token() === ts.SyntaxKind.NumericLiteral || - token() === ts.SyntaxKind.AsteriskToken || - token() === ts.SyntaxKind.OpenBracketToken) { - const isAmbient = ts.some(modifiers, isDeclareModifier); - if (isAmbient) { - for (const m of modifiers!) { - (m as ts.Mutable).flags |= ts.NodeFlags.Ambient; - } - return doInsideOfContext(ts.NodeFlags.Ambient, () => parsePropertyOrMethodDeclaration(pos, hasJSDoc, decorators, modifiers)); - } - else { - return parsePropertyOrMethodDeclaration(pos, hasJSDoc, decorators, modifiers); - } + // 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 ts.SyntaxKind.OpenParenToken: // Method declaration + case ts.SyntaxKind.LessThanToken: // Generic Method declaration + case ts.SyntaxKind.ExclamationToken: // Non-null assertion on property name + case ts.SyntaxKind.ColonToken: // Type Annotation for declaration + case ts.SyntaxKind.EqualsToken: // Initializer for declaration + case ts.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(); } + } - if (decorators || modifiers) { - // treat this as a property declaration with a missing name. - const name = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Declaration_expected); - return parsePropertyDeclaration(pos, hasJSDoc, decorators, modifiers, name, /*questionToken*/ undefined); - } + return false; + } - // 'isClassMemberStart' should have hinted not to attempt parsing. - return ts.Debug.fail("Should not have attempted to parse class member declaration."); - } + function parseClassStaticBlockDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.ModifiersArray | undefined): ts.ClassStaticBlockDeclaration { + parseExpectedToken(ts.SyntaxKind.StaticKeyword); + const body = parseClassStaticBlockBody(); + return withJSDoc(finishNode(factory.createClassStaticBlockDeclaration(decorators, modifiers, body), pos), hasJSDoc); + } - function parseClassExpression(): ts.ClassExpression { - return parseClassDeclarationOrExpression(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined, ts.SyntaxKind.ClassExpression) as ts.ClassExpression; - } + function parseClassStaticBlockBody() { + const savedYieldContext = inYieldContext(); + const savedAwaitContext = inAwaitContext(); - function parseClassDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ClassDeclaration { - return parseClassDeclarationOrExpression(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration; - } + setYieldContext(false); + setAwaitContext(true); - function parseClassDeclarationOrExpression(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, kind: ts.ClassLikeDeclaration["kind"]): ts.ClassLikeDeclaration { - const savedAwaitContext = inAwaitContext(); - parseExpected(ts.SyntaxKind.ClassKeyword); + const body = parseBlock(/*ignoreMissingOpenBrace*/ false); - // We don't parse the name here in await context, instead we will report a grammar error in the checker. - const name = parseNameOfClassDeclarationOrExpression(); - const typeParameters = parseTypeParameters(); - if (ts.some(modifiers, ts.isExportModifier)) - setAwaitContext(/*value*/ true); - const heritageClauses = parseHeritageClauses(); + setYieldContext(savedYieldContext); + setAwaitContext(savedAwaitContext); - let members; - if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { - // ClassTail[Yield,Await] : (Modified) See 14.5 - // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } - members = parseClassMembers(); - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - else { - members = createMissingList(); - } - setAwaitContext(savedAwaitContext); - const node = kind === ts.SyntaxKind.ClassDeclaration - ? factory.createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members) - : factory.createClassExpression(decorators, modifiers, name, typeParameters, heritageClauses, members); - return withJSDoc(finishNode(node, pos), hasJSDoc); + return body; + } + + function parseDecoratorExpression() { + if (inAwaitContext() && token() === ts.SyntaxKind.AwaitKeyword) { + // `@await` is is disallowed in an [Await] context, but can cause parsing to go off the rails + // This simply parses the missing identifier and moves on. + const pos = getNodePos(); + const awaitExpression = parseIdentifier(ts.Diagnostics.Expression_expected); + nextToken(); + const memberExpression = parseMemberExpressionRest(pos, awaitExpression, /*allowOptionalChain*/ true); + return parseCallExpressionRest(pos, memberExpression); } + return parseLeftHandSideExpressionOrHigher(); + } - function parseNameOfClassDeclarationOrExpression(): ts.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 isBindingIdentifier() && !isImplementsClause() - ? createIdentifier(isBindingIdentifier()) - : undefined; + function tryParseDecorator(): ts.Decorator | undefined { + const pos = getNodePos(); + if (!parseOptional(ts.SyntaxKind.AtToken)) { + return undefined; } + const expression = doInDecoratorContext(parseDecoratorExpression); + return finishNode(factory.createDecorator(expression), pos); + } - function isImplementsClause() { - return token() === ts.SyntaxKind.ImplementsKeyword && lookAhead(nextTokenIsIdentifierOrKeyword); + function parseDecorators(): ts.NodeArray | undefined { + const pos = getNodePos(); + let list, decorator; + while (decorator = tryParseDecorator()) { + list = ts.append(list, decorator); } + return list && createNodeArray(list, pos); + } - function parseHeritageClauses(): ts.NodeArray | undefined { - // ClassTail[Yield,Await] : (Modified) See 14.5 - // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } + function tryParseModifier(permitInvalidConstAsModifier?: boolean, stopOnStartOfClassStaticBlock?: boolean, hasSeenStaticModifier?: boolean): ts.Modifier | undefined { + const pos = getNodePos(); + const kind = token(); - if (isHeritageClause()) { - return parseList(ParsingContext.HeritageClauses, parseHeritageClause); + if (token() === ts.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)) { + return undefined; } - + } + else if (stopOnStartOfClassStaticBlock && token() === ts.SyntaxKind.StaticKeyword && lookAhead(nextTokenIsOpenBrace)) { return undefined; } - - function parseHeritageClause(): ts.HeritageClause { - const pos = getNodePos(); - const tok = token(); - ts.Debug.assert(tok === ts.SyntaxKind.ExtendsKeyword || tok === ts.SyntaxKind.ImplementsKeyword); // isListElement() should ensure this. - nextToken(); - const types = parseDelimitedList(ParsingContext.HeritageClauseElement, parseExpressionWithTypeArguments); - return finishNode(factory.createHeritageClause(tok, types), pos); + else if (hasSeenStaticModifier && token() === ts.SyntaxKind.StaticKeyword) { + return undefined; } - - function parseExpressionWithTypeArguments(): ts.ExpressionWithTypeArguments { - const pos = getNodePos(); - const expression = parseLeftHandSideExpressionOrHigher(); - if (expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { - return expression as ts.ExpressionWithTypeArguments; + else { + if (!parseAnyContextualModifier()) { + return undefined; } - const typeArguments = tryParseTypeArguments(); - return finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos); } - function tryParseTypeArguments(): ts.NodeArray | undefined { - return token() === ts.SyntaxKind.LessThanToken ? - parseBracketedList(ParsingContext.TypeArguments, parseType, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken) : undefined; - } + return finishNode(factory.createToken(kind as ts.Modifier["kind"]), pos); + } - function isHeritageClause(): boolean { - return token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; - } + /* + * 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, stopOnStartOfClassStaticBlock?: boolean): ts.NodeArray | undefined { + const pos = getNodePos(); + let list, modifier, hasSeenStatic = false; + while (modifier = tryParseModifier(permitInvalidConstAsModifier, stopOnStartOfClassStaticBlock, hasSeenStatic)) { + if (modifier.kind === ts.SyntaxKind.StaticKeyword) + hasSeenStatic = true; + list = ts.append(list, modifier); + } + return list && createNodeArray(list, pos); + } - function parseClassMembers(): ts.NodeArray { - return parseList(ParsingContext.ClassMembers, parseClassElement); + function parseModifiersForArrowFunction(): ts.NodeArray | undefined { + let modifiers: ts.NodeArray | undefined; + if (token() === ts.SyntaxKind.AsyncKeyword) { + const pos = getNodePos(); + nextToken(); + const modifier = finishNode(factory.createToken(ts.SyntaxKind.AsyncKeyword), pos); + modifiers = createNodeArray([modifier], pos); } + return modifiers; + } - function parseInterfaceDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.InterfaceDeclaration { - parseExpected(ts.SyntaxKind.InterfaceKeyword); - const name = parseIdentifier(); - const typeParameters = parseTypeParameters(); - const heritageClauses = parseHeritageClauses(); - const members = parseObjectTypeMembers(); - const node = factory.createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members); - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseClassElement(): ts.ClassElement { + const pos = getNodePos(); + if (token() === ts.SyntaxKind.SemicolonToken) { + nextToken(); + return finishNode(factory.createSemicolonClassElement(), pos); } - function parseTypeAliasDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.TypeAliasDeclaration { - parseExpected(ts.SyntaxKind.TypeKeyword); - const name = parseIdentifier(); - const typeParameters = parseTypeParameters(); - parseExpected(ts.SyntaxKind.EqualsToken); - const type = token() === ts.SyntaxKind.IntrinsicKeyword && tryParse(parseKeywordAndNoDot) || parseType(); - parseSemicolon(); - const node = factory.createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type); - return withJSDoc(finishNode(node, pos), hasJSDoc); + const hasJSDoc = hasPrecedingJSDocComment(); + const decorators = parseDecorators(); + const modifiers = parseModifiers(/*permitInvalidConstAsModifier*/ true, /*stopOnStartOfClassStaticBlock*/ true); + if (token() === ts.SyntaxKind.StaticKeyword && lookAhead(nextTokenIsOpenBrace)) { + return parseClassStaticBlockDeclaration(pos, hasJSDoc, decorators, modifiers); } - // 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(): ts.EnumMember { - const pos = getNodePos(); - const hasJSDoc = hasPrecedingJSDocComment(); - const name = parsePropertyName(); - const initializer = allowInAnd(parseInitializer); - return withJSDoc(finishNode(factory.createEnumMember(name, initializer), pos), hasJSDoc); + if (parseContextualModifier(ts.SyntaxKind.GetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.GetAccessor); } - function parseEnumDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.EnumDeclaration { - parseExpected(ts.SyntaxKind.EnumKeyword); - const name = parseIdentifier(); - let members; - if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { - members = doOutsideOfYieldAndAwaitContext(() => parseDelimitedList(ParsingContext.EnumMembers, parseEnumMember)); - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - else { - members = createMissingList(); - } - const node = factory.createEnumDeclaration(decorators, modifiers, name, members); - return withJSDoc(finishNode(node, pos), hasJSDoc); + if (parseContextualModifier(ts.SyntaxKind.SetKeyword)) { + return parseAccessorDeclaration(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.SetAccessor); } - function parseModuleBlock(): ts.ModuleBlock { - const pos = getNodePos(); - let statements; - if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { - statements = parseList(ParsingContext.BlockStatements, parseStatement); - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - else { - statements = createMissingList(); + if (token() === ts.SyntaxKind.ConstructorKeyword || token() === ts.SyntaxKind.StringLiteral) { + const constructorDeclaration = tryParseConstructorDeclaration(pos, hasJSDoc, decorators, modifiers); + if (constructorDeclaration) { + return constructorDeclaration; } - return finishNode(factory.createModuleBlock(statements), pos); } - function parseModuleOrNamespaceDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, flags: ts.NodeFlags): ts.ModuleDeclaration { - // If we are parsing a dotted namespace name, we want to - // propagate the 'Namespace' flag across the names if set. - const namespaceFlag = flags & ts.NodeFlags.Namespace; - const name = parseIdentifier(); - const body = parseOptional(ts.SyntaxKind.DotToken) - ? parseModuleOrNamespaceDeclaration(getNodePos(), /*hasJSDoc*/ false, /*decorators*/ undefined, /*modifiers*/ undefined, ts.NodeFlags.NestedNamespace | namespaceFlag) as ts.NamespaceDeclaration - : parseModuleBlock(); - const node = factory.createModuleDeclaration(decorators, modifiers, name, body, flags); - return withJSDoc(finishNode(node, pos), hasJSDoc); + if (isIndexSignature()) { + return parseIndexSignatureDeclaration(pos, hasJSDoc, decorators, modifiers); } - function parseAmbientExternalModuleDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ModuleDeclaration { - let flags: ts.NodeFlags = 0; - let name; - if (token() === ts.SyntaxKind.GlobalKeyword) { - // parse 'global' as name of global scope augmentation - name = parseIdentifier(); - flags |= ts.NodeFlags.GlobalAugmentation; - } - else { - name = parseLiteralNode() as ts.StringLiteral; - name.text = internIdentifier(name.text); - } - let body: ts.ModuleBlock | undefined; - if (token() === ts.SyntaxKind.OpenBraceToken) { - body = parseModuleBlock(); + // 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 (ts.tokenIsIdentifierOrKeyword(token()) || + token() === ts.SyntaxKind.StringLiteral || + token() === ts.SyntaxKind.NumericLiteral || + token() === ts.SyntaxKind.AsteriskToken || + token() === ts.SyntaxKind.OpenBracketToken) { + const isAmbient = ts.some(modifiers, isDeclareModifier); + if (isAmbient) { + for (const m of modifiers!) { + (m as ts.Mutable).flags |= ts.NodeFlags.Ambient; + } + return doInsideOfContext(ts.NodeFlags.Ambient, () => parsePropertyOrMethodDeclaration(pos, hasJSDoc, decorators, modifiers)); } else { - parseSemicolon(); + return parsePropertyOrMethodDeclaration(pos, hasJSDoc, decorators, modifiers); } - const node = factory.createModuleDeclaration(decorators, modifiers, name, body, flags); - return withJSDoc(finishNode(node, pos), hasJSDoc); } - function parseModuleDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ModuleDeclaration { - let flags: ts.NodeFlags = 0; - if (token() === ts.SyntaxKind.GlobalKeyword) { - // global augmentation - return parseAmbientExternalModuleDeclaration(pos, hasJSDoc, decorators, modifiers); - } - else if (parseOptional(ts.SyntaxKind.NamespaceKeyword)) { - flags |= ts.NodeFlags.Namespace; - } - else { - parseExpected(ts.SyntaxKind.ModuleKeyword); - if (token() === ts.SyntaxKind.StringLiteral) { - return parseAmbientExternalModuleDeclaration(pos, hasJSDoc, decorators, modifiers); - } - } - return parseModuleOrNamespaceDeclaration(pos, hasJSDoc, decorators, modifiers, flags); + if (decorators || modifiers) { + // treat this as a property declaration with a missing name. + const name = createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, ts.Diagnostics.Declaration_expected); + return parsePropertyDeclaration(pos, hasJSDoc, decorators, modifiers, name, /*questionToken*/ undefined); } - function isExternalModuleReference() { - return token() === ts.SyntaxKind.RequireKeyword && - lookAhead(nextTokenIsOpenParen); - } + // 'isClassMemberStart' should have hinted not to attempt parsing. + return ts.Debug.fail("Should not have attempted to parse class member declaration."); + } - function nextTokenIsOpenParen() { - return nextToken() === ts.SyntaxKind.OpenParenToken; - } + function parseClassExpression(): ts.ClassExpression { + return parseClassDeclarationOrExpression(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined, ts.SyntaxKind.ClassExpression) as ts.ClassExpression; + } + + function parseClassDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ClassDeclaration { + return parseClassDeclarationOrExpression(pos, hasJSDoc, decorators, modifiers, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration; + } + + function parseClassDeclarationOrExpression(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, kind: ts.ClassLikeDeclaration["kind"]): ts.ClassLikeDeclaration { + const savedAwaitContext = inAwaitContext(); + parseExpected(ts.SyntaxKind.ClassKeyword); - function nextTokenIsOpenBrace() { - return nextToken() === ts.SyntaxKind.OpenBraceToken; + // We don't parse the name here in await context, instead we will report a grammar error in the checker. + const name = parseNameOfClassDeclarationOrExpression(); + const typeParameters = parseTypeParameters(); + if (ts.some(modifiers, ts.isExportModifier)) + setAwaitContext(/*value*/ true); + const heritageClauses = parseHeritageClauses(); + + let members; + if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { + // ClassTail[Yield,Await] : (Modified) See 14.5 + // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } + members = parseClassMembers(); + parseExpected(ts.SyntaxKind.CloseBraceToken); + } + else { + members = createMissingList(); } + setAwaitContext(savedAwaitContext); + const node = kind === ts.SyntaxKind.ClassDeclaration + ? factory.createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members) + : factory.createClassExpression(decorators, modifiers, name, typeParameters, heritageClauses, members); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } + + function parseNameOfClassDeclarationOrExpression(): ts.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 isBindingIdentifier() && !isImplementsClause() + ? createIdentifier(isBindingIdentifier()) + : undefined; + } + + function isImplementsClause() { + return token() === ts.SyntaxKind.ImplementsKeyword && lookAhead(nextTokenIsIdentifierOrKeyword); + } - function nextTokenIsSlash() { - return nextToken() === ts.SyntaxKind.SlashToken; + function parseHeritageClauses(): ts.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 parseNamespaceExportDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.NamespaceExportDeclaration { - parseExpected(ts.SyntaxKind.AsKeyword); - parseExpected(ts.SyntaxKind.NamespaceKeyword); - const name = parseIdentifier(); - parseSemicolon(); - const node = factory.createNamespaceExportDeclaration(name); - // NamespaceExportDeclaration nodes cannot have decorators or modifiers, so we attach them here so we can report them in the grammar checker - node.decorators = decorators; - node.modifiers = modifiers; - return withJSDoc(finishNode(node, pos), hasJSDoc); + return undefined; + } + + function parseHeritageClause(): ts.HeritageClause { + const pos = getNodePos(); + const tok = token(); + ts.Debug.assert(tok === ts.SyntaxKind.ExtendsKeyword || tok === ts.SyntaxKind.ImplementsKeyword); // isListElement() should ensure this. + nextToken(); + const types = parseDelimitedList(ParsingContext.HeritageClauseElement, parseExpressionWithTypeArguments); + return finishNode(factory.createHeritageClause(tok, types), pos); + } + + function parseExpressionWithTypeArguments(): ts.ExpressionWithTypeArguments { + const pos = getNodePos(); + const expression = parseLeftHandSideExpressionOrHigher(); + if (expression.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + return expression as ts.ExpressionWithTypeArguments; } + const typeArguments = tryParseTypeArguments(); + return finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos); + } - function parseImportDeclarationOrImportEqualsDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ImportEqualsDeclaration | ts.ImportDeclaration { - parseExpected(ts.SyntaxKind.ImportKeyword); + function tryParseTypeArguments(): ts.NodeArray | undefined { + return token() === ts.SyntaxKind.LessThanToken ? + parseBracketedList(ParsingContext.TypeArguments, parseType, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken) : undefined; + } - const afterImportPos = scanner.getStartPos(); + function isHeritageClause(): boolean { + return token() === ts.SyntaxKind.ExtendsKeyword || token() === ts.SyntaxKind.ImplementsKeyword; + } - // We don't parse the identifier here in await context, instead we will report a grammar error in the checker. - let identifier: ts.Identifier | undefined; - if (isIdentifier()) { - identifier = parseIdentifier(); - } + function parseClassMembers(): ts.NodeArray { + return parseList(ParsingContext.ClassMembers, parseClassElement); + } - let isTypeOnly = false; - if (token() !== ts.SyntaxKind.FromKeyword && - identifier?.escapedText === "type" && - (isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration())) { - isTypeOnly = true; - identifier = isIdentifier() ? parseIdentifier() : undefined; - } + function parseInterfaceDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.InterfaceDeclaration { + parseExpected(ts.SyntaxKind.InterfaceKeyword); + const name = parseIdentifier(); + const typeParameters = parseTypeParameters(); + const heritageClauses = parseHeritageClauses(); + const members = parseObjectTypeMembers(); + const node = factory.createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) { - return parseImportEqualsDeclaration(pos, hasJSDoc, decorators, modifiers, identifier, isTypeOnly); - } + function parseTypeAliasDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.TypeAliasDeclaration { + parseExpected(ts.SyntaxKind.TypeKeyword); + const name = parseIdentifier(); + const typeParameters = parseTypeParameters(); + parseExpected(ts.SyntaxKind.EqualsToken); + const type = token() === ts.SyntaxKind.IntrinsicKeyword && tryParse(parseKeywordAndNoDot) || parseType(); + parseSemicolon(); + const node = factory.createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - // ImportDeclaration: - // import ImportClause from ModuleSpecifier ; - // import ModuleSpecifier; - let importClause: ts.ImportClause | undefined; - if (identifier || // import id - token() === ts.SyntaxKind.AsteriskToken || // import * - token() === ts.SyntaxKind.OpenBraceToken // import { - ) { - importClause = parseImportClause(identifier, afterImportPos, isTypeOnly); - parseExpected(ts.SyntaxKind.FromKeyword); - } - const moduleSpecifier = parseModuleSpecifier(); + // 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(): ts.EnumMember { + const pos = getNodePos(); + const hasJSDoc = hasPrecedingJSDocComment(); + const name = parsePropertyName(); + const initializer = allowInAnd(parseInitializer); + return withJSDoc(finishNode(factory.createEnumMember(name, initializer), pos), hasJSDoc); + } - let assertClause: ts.AssertClause | undefined; - if (token() === ts.SyntaxKind.AssertKeyword && !scanner.hasPrecedingLineBreak()) { - assertClause = parseAssertClause(); - } + function parseEnumDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.EnumDeclaration { + parseExpected(ts.SyntaxKind.EnumKeyword); + const name = parseIdentifier(); + let members; + if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { + members = doOutsideOfYieldAndAwaitContext(() => parseDelimitedList(ParsingContext.EnumMembers, parseEnumMember)); + parseExpected(ts.SyntaxKind.CloseBraceToken); + } + else { + members = createMissingList(); + } + const node = factory.createEnumDeclaration(decorators, modifiers, name, members); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - parseSemicolon(); - const node = factory.createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier, assertClause); - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseModuleBlock(): ts.ModuleBlock { + const pos = getNodePos(); + let statements; + if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { + statements = parseList(ParsingContext.BlockStatements, parseStatement); + parseExpected(ts.SyntaxKind.CloseBraceToken); } + else { + statements = createMissingList(); + } + return finishNode(factory.createModuleBlock(statements), pos); + } - function parseAssertEntry() { - const pos = getNodePos(); - const name = ts.tokenIsIdentifierOrKeyword(token()) ? parseIdentifierName() : parseLiteralLikeNode(ts.SyntaxKind.StringLiteral) as ts.StringLiteral; - parseExpected(ts.SyntaxKind.ColonToken); - const value = parseAssignmentExpressionOrHigher(); - return finishNode(factory.createAssertEntry(name, value), pos); + function parseModuleOrNamespaceDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, flags: ts.NodeFlags): ts.ModuleDeclaration { + // If we are parsing a dotted namespace name, we want to + // propagate the 'Namespace' flag across the names if set. + const namespaceFlag = flags & ts.NodeFlags.Namespace; + const name = parseIdentifier(); + const body = parseOptional(ts.SyntaxKind.DotToken) + ? parseModuleOrNamespaceDeclaration(getNodePos(), /*hasJSDoc*/ false, /*decorators*/ undefined, /*modifiers*/ undefined, ts.NodeFlags.NestedNamespace | namespaceFlag) as ts.NamespaceDeclaration + : parseModuleBlock(); + const node = factory.createModuleDeclaration(decorators, modifiers, name, body, flags); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } + + function parseAmbientExternalModuleDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ModuleDeclaration { + let flags: ts.NodeFlags = 0; + let name; + if (token() === ts.SyntaxKind.GlobalKeyword) { + // parse 'global' as name of global scope augmentation + name = parseIdentifier(); + flags |= ts.NodeFlags.GlobalAugmentation; + } + else { + name = parseLiteralNode() as ts.StringLiteral; + name.text = internIdentifier(name.text); + } + let body: ts.ModuleBlock | undefined; + if (token() === ts.SyntaxKind.OpenBraceToken) { + body = parseModuleBlock(); + } + else { + parseSemicolon(); } + const node = factory.createModuleDeclaration(decorators, modifiers, name, body, flags); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - function parseAssertClause(skipAssertKeyword?: true) { - const pos = getNodePos(); - if (!skipAssertKeyword) { - parseExpected(ts.SyntaxKind.AssertKeyword); - } - const openBracePosition = scanner.getTokenPos(); - if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { - const multiLine = scanner.hasPrecedingLineBreak(); - const elements = parseDelimitedList(ParsingContext.AssertEntries, parseAssertEntry, /*considerSemicolonAsDelimiter*/ true); - if (!parseExpected(ts.SyntaxKind.CloseBraceToken)) { - const lastError = ts.lastOrUndefined(parseDiagnostics); - if (lastError && lastError.code === ts.Diagnostics._0_expected.code) { - ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openBracePosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, "{", "}")); - } - } - return finishNode(factory.createAssertClause(elements, multiLine), pos); - } - else { - const elements = createNodeArray([], getNodePos(), /*end*/ undefined, /*hasTrailingComma*/ false); - return finishNode(factory.createAssertClause(elements, /*multiLine*/ false), pos); + function parseModuleDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ModuleDeclaration { + let flags: ts.NodeFlags = 0; + if (token() === ts.SyntaxKind.GlobalKeyword) { + // global augmentation + return parseAmbientExternalModuleDeclaration(pos, hasJSDoc, decorators, modifiers); + } + else if (parseOptional(ts.SyntaxKind.NamespaceKeyword)) { + flags |= ts.NodeFlags.Namespace; + } + else { + parseExpected(ts.SyntaxKind.ModuleKeyword); + if (token() === ts.SyntaxKind.StringLiteral) { + return parseAmbientExternalModuleDeclaration(pos, hasJSDoc, decorators, modifiers); } } + return parseModuleOrNamespaceDeclaration(pos, hasJSDoc, decorators, modifiers, flags); + } - function tokenAfterImportDefinitelyProducesImportDeclaration() { - return token() === ts.SyntaxKind.AsteriskToken || token() === ts.SyntaxKind.OpenBraceToken; - } + function isExternalModuleReference() { + return token() === ts.SyntaxKind.RequireKeyword && + lookAhead(nextTokenIsOpenParen); + } - function tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() { - // In `import id ___`, the current token decides whether to produce - // an ImportDeclaration or ImportEqualsDeclaration. - return token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.FromKeyword; - } + function nextTokenIsOpenParen() { + return nextToken() === ts.SyntaxKind.OpenParenToken; + } - function parseImportEqualsDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, identifier: ts.Identifier, isTypeOnly: boolean): ts.ImportEqualsDeclaration { - parseExpected(ts.SyntaxKind.EqualsToken); - const moduleReference = parseModuleReference(); - parseSemicolon(); - const node = factory.createImportEqualsDeclaration(decorators, modifiers, isTypeOnly, identifier, moduleReference); - const finished = withJSDoc(finishNode(node, pos), hasJSDoc); - return finished; - } + function nextTokenIsOpenBrace() { + return nextToken() === ts.SyntaxKind.OpenBraceToken; + } - function parseImportClause(identifier: ts.Identifier | undefined, pos: number, isTypeOnly: boolean) { - // ImportClause: - // ImportedDefaultBinding - // NameSpaceImport - // NamedImports - // ImportedDefaultBinding, NameSpaceImport - // ImportedDefaultBinding, NamedImports + function nextTokenIsSlash() { + return nextToken() === ts.SyntaxKind.SlashToken; + } - // If there was no default import or if there is comma token after default import - // parse namespace or named imports - let namedBindings: ts.NamespaceImport | ts.NamedImports | undefined; - if (!identifier || - parseOptional(ts.SyntaxKind.CommaToken)) { - namedBindings = token() === ts.SyntaxKind.AsteriskToken ? parseNamespaceImport() : parseNamedImportsOrExports(ts.SyntaxKind.NamedImports); - } + function parseNamespaceExportDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.NamespaceExportDeclaration { + parseExpected(ts.SyntaxKind.AsKeyword); + parseExpected(ts.SyntaxKind.NamespaceKeyword); + const name = parseIdentifier(); + parseSemicolon(); + const node = factory.createNamespaceExportDeclaration(name); + // NamespaceExportDeclaration nodes cannot have decorators or modifiers, so we attach them here so we can report them in the grammar checker + node.decorators = decorators; + node.modifiers = modifiers; + return withJSDoc(finishNode(node, pos), hasJSDoc); + } + + function parseImportDeclarationOrImportEqualsDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ImportEqualsDeclaration | ts.ImportDeclaration { + parseExpected(ts.SyntaxKind.ImportKeyword); - return finishNode(factory.createImportClause(isTypeOnly, identifier, namedBindings), pos); + const afterImportPos = scanner.getStartPos(); + + // We don't parse the identifier here in await context, instead we will report a grammar error in the checker. + let identifier: ts.Identifier | undefined; + if (isIdentifier()) { + identifier = parseIdentifier(); } - function parseModuleReference() { - return isExternalModuleReference() - ? parseExternalModuleReference() - : parseEntityName(/*allowReservedWords*/ false); + let isTypeOnly = false; + if (token() !== ts.SyntaxKind.FromKeyword && + identifier?.escapedText === "type" && + (isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration())) { + isTypeOnly = true; + identifier = isIdentifier() ? parseIdentifier() : undefined; } - function parseExternalModuleReference() { - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.RequireKeyword); - parseExpected(ts.SyntaxKind.OpenParenToken); - const expression = parseModuleSpecifier(); - parseExpected(ts.SyntaxKind.CloseParenToken); - return finishNode(factory.createExternalModuleReference(expression), pos); + if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) { + return parseImportEqualsDeclaration(pos, hasJSDoc, decorators, modifiers, identifier, isTypeOnly); } - function parseModuleSpecifier(): ts.Expression { - if (token() === ts.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(); - } + // ImportDeclaration: + // import ImportClause from ModuleSpecifier ; + // import ModuleSpecifier; + let importClause: ts.ImportClause | undefined; + if (identifier || // import id + token() === ts.SyntaxKind.AsteriskToken || // import * + token() === ts.SyntaxKind.OpenBraceToken // import { + ) { + importClause = parseImportClause(identifier, afterImportPos, isTypeOnly); + parseExpected(ts.SyntaxKind.FromKeyword); } + const moduleSpecifier = parseModuleSpecifier(); - function parseNamespaceImport(): ts.NamespaceImport { - // NameSpaceImport: - // * as ImportedBinding - const pos = getNodePos(); - parseExpected(ts.SyntaxKind.AsteriskToken); - parseExpected(ts.SyntaxKind.AsKeyword); - const name = parseIdentifier(); - return finishNode(factory.createNamespaceImport(name), pos); + let assertClause: ts.AssertClause | undefined; + if (token() === ts.SyntaxKind.AssertKeyword && !scanner.hasPrecedingLineBreak()) { + assertClause = parseAssertClause(); } - function parseNamedImportsOrExports(kind: ts.SyntaxKind.NamedImports): ts.NamedImports; - function parseNamedImportsOrExports(kind: ts.SyntaxKind.NamedExports): ts.NamedExports; - function parseNamedImportsOrExports(kind: ts.SyntaxKind): ts.NamedImportsOrExports { - const pos = getNodePos(); + parseSemicolon(); + const node = factory.createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier, assertClause); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - // NamedImports: - // { } - // { ImportsList } - // { ImportsList, } + function parseAssertEntry() { + const pos = getNodePos(); + const name = ts.tokenIsIdentifierOrKeyword(token()) ? parseIdentifierName() : parseLiteralLikeNode(ts.SyntaxKind.StringLiteral) as ts.StringLiteral; + parseExpected(ts.SyntaxKind.ColonToken); + const value = parseAssignmentExpressionOrHigher(); + return finishNode(factory.createAssertEntry(name, value), pos); + } - // ImportsList: - // ImportSpecifier - // ImportsList, ImportSpecifier - const node = kind === ts.SyntaxKind.NamedImports - ? factory.createNamedImports(parseBracketedList(ParsingContext.ImportOrExportSpecifiers, parseImportSpecifier, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken)) - : factory.createNamedExports(parseBracketedList(ParsingContext.ImportOrExportSpecifiers, parseExportSpecifier, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken)); - return finishNode(node, pos); + function parseAssertClause(skipAssertKeyword?: true) { + const pos = getNodePos(); + if (!skipAssertKeyword) { + parseExpected(ts.SyntaxKind.AssertKeyword); + } + const openBracePosition = scanner.getTokenPos(); + if (parseExpected(ts.SyntaxKind.OpenBraceToken)) { + const multiLine = scanner.hasPrecedingLineBreak(); + const elements = parseDelimitedList(ParsingContext.AssertEntries, parseAssertEntry, /*considerSemicolonAsDelimiter*/ true); + if (!parseExpected(ts.SyntaxKind.CloseBraceToken)) { + const lastError = ts.lastOrUndefined(parseDiagnostics); + if (lastError && lastError.code === ts.Diagnostics._0_expected.code) { + ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, openBracePosition, 1, ts.Diagnostics.The_parser_expected_to_find_a_1_to_match_the_0_token_here, "{", "}")); + } + } + return finishNode(factory.createAssertClause(elements, multiLine), pos); + } + else { + const elements = createNodeArray([], getNodePos(), /*end*/ undefined, /*hasTrailingComma*/ false); + return finishNode(factory.createAssertClause(elements, /*multiLine*/ false), pos); } + } - function parseExportSpecifier() { - const hasJSDoc = hasPrecedingJSDocComment(); - return withJSDoc(parseImportOrExportSpecifier(ts.SyntaxKind.ExportSpecifier) as ts.ExportSpecifier, hasJSDoc); + function tokenAfterImportDefinitelyProducesImportDeclaration() { + return token() === ts.SyntaxKind.AsteriskToken || token() === ts.SyntaxKind.OpenBraceToken; + } + + function tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() { + // In `import id ___`, the current token decides whether to produce + // an ImportDeclaration or ImportEqualsDeclaration. + return token() === ts.SyntaxKind.CommaToken || token() === ts.SyntaxKind.FromKeyword; + } + + function parseImportEqualsDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined, identifier: ts.Identifier, isTypeOnly: boolean): ts.ImportEqualsDeclaration { + parseExpected(ts.SyntaxKind.EqualsToken); + const moduleReference = parseModuleReference(); + parseSemicolon(); + const node = factory.createImportEqualsDeclaration(decorators, modifiers, isTypeOnly, identifier, moduleReference); + const finished = withJSDoc(finishNode(node, pos), hasJSDoc); + return finished; + } + + function parseImportClause(identifier: ts.Identifier | undefined, pos: number, isTypeOnly: boolean) { + // ImportClause: + // ImportedDefaultBinding + // NameSpaceImport + // NamedImports + // ImportedDefaultBinding, NameSpaceImport + // ImportedDefaultBinding, NamedImports + + // If there was no default import or if there is comma token after default import + // parse namespace or named imports + let namedBindings: ts.NamespaceImport | ts.NamedImports | undefined; + if (!identifier || + parseOptional(ts.SyntaxKind.CommaToken)) { + namedBindings = token() === ts.SyntaxKind.AsteriskToken ? parseNamespaceImport() : parseNamedImportsOrExports(ts.SyntaxKind.NamedImports); } - function parseImportSpecifier() { - return parseImportOrExportSpecifier(ts.SyntaxKind.ImportSpecifier) as ts.ImportSpecifier; + return finishNode(factory.createImportClause(isTypeOnly, identifier, namedBindings), pos); + } + + function parseModuleReference() { + return isExternalModuleReference() + ? parseExternalModuleReference() + : parseEntityName(/*allowReservedWords*/ false); + } + + function parseExternalModuleReference() { + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.RequireKeyword); + parseExpected(ts.SyntaxKind.OpenParenToken); + const expression = parseModuleSpecifier(); + parseExpected(ts.SyntaxKind.CloseParenToken); + return finishNode(factory.createExternalModuleReference(expression), pos); + } + + function parseModuleSpecifier(): ts.Expression { + if (token() === ts.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 parseImportOrExportSpecifier(kind: ts.SyntaxKind): ts.ImportOrExportSpecifier { - const pos = getNodePos(); - // ImportSpecifier: - // BindingIdentifier - // IdentifierName as BindingIdentifier - // ExportSpecifier: - // IdentifierName - // IdentifierName as IdentifierName - let checkIdentifierIsKeyword = ts.isKeyword(token()) && !isIdentifier(); - let checkIdentifierStart = scanner.getTokenPos(); - let checkIdentifierEnd = scanner.getTextPos(); - let isTypeOnly = false; - let propertyName: ts.Identifier | undefined; - let canParseAsKeyword = true; - let name = parseIdentifierName(); - if (name.escapedText === "type") { - // If the first token of an import specifier is 'type', there are a lot of possibilities, - // especially if we see 'as' afterwards: - // - // import { type } from "mod"; - isTypeOnly: false, name: type - // import { type as } from "mod"; - isTypeOnly: true, name: as - // import { type as as } from "mod"; - isTypeOnly: false, name: as, propertyName: type - // import { type as as as } from "mod"; - isTypeOnly: true, name: as, propertyName: as + function parseNamespaceImport(): ts.NamespaceImport { + // NameSpaceImport: + // * as ImportedBinding + const pos = getNodePos(); + parseExpected(ts.SyntaxKind.AsteriskToken); + parseExpected(ts.SyntaxKind.AsKeyword); + const name = parseIdentifier(); + return finishNode(factory.createNamespaceImport(name), pos); + } + + function parseNamedImportsOrExports(kind: ts.SyntaxKind.NamedImports): ts.NamedImports; + function parseNamedImportsOrExports(kind: ts.SyntaxKind.NamedExports): ts.NamedExports; + function parseNamedImportsOrExports(kind: ts.SyntaxKind): ts.NamedImportsOrExports { + const pos = getNodePos(); + + // NamedImports: + // { } + // { ImportsList } + // { ImportsList, } + + // ImportsList: + // ImportSpecifier + // ImportsList, ImportSpecifier + const node = kind === ts.SyntaxKind.NamedImports + ? factory.createNamedImports(parseBracketedList(ParsingContext.ImportOrExportSpecifiers, parseImportSpecifier, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken)) + : factory.createNamedExports(parseBracketedList(ParsingContext.ImportOrExportSpecifiers, parseExportSpecifier, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken)); + return finishNode(node, pos); + } + + function parseExportSpecifier() { + const hasJSDoc = hasPrecedingJSDocComment(); + return withJSDoc(parseImportOrExportSpecifier(ts.SyntaxKind.ExportSpecifier) as ts.ExportSpecifier, hasJSDoc); + } + + function parseImportSpecifier() { + return parseImportOrExportSpecifier(ts.SyntaxKind.ImportSpecifier) as ts.ImportSpecifier; + } + + function parseImportOrExportSpecifier(kind: ts.SyntaxKind): ts.ImportOrExportSpecifier { + const pos = getNodePos(); + // ImportSpecifier: + // BindingIdentifier + // IdentifierName as BindingIdentifier + // ExportSpecifier: + // IdentifierName + // IdentifierName as IdentifierName + let checkIdentifierIsKeyword = ts.isKeyword(token()) && !isIdentifier(); + let checkIdentifierStart = scanner.getTokenPos(); + let checkIdentifierEnd = scanner.getTextPos(); + let isTypeOnly = false; + let propertyName: ts.Identifier | undefined; + let canParseAsKeyword = true; + let name = parseIdentifierName(); + if (name.escapedText === "type") { + // If the first token of an import specifier is 'type', there are a lot of possibilities, + // especially if we see 'as' afterwards: + // + // import { type } from "mod"; - isTypeOnly: false, name: type + // import { type as } from "mod"; - isTypeOnly: true, name: as + // import { type as as } from "mod"; - isTypeOnly: false, name: as, propertyName: type + // import { type as as as } from "mod"; - isTypeOnly: true, name: as, propertyName: as + if (token() === ts.SyntaxKind.AsKeyword) { + // { type as ...? } + const firstAs = parseIdentifierName(); if (token() === ts.SyntaxKind.AsKeyword) { - // { type as ...? } - const firstAs = parseIdentifierName(); - if (token() === ts.SyntaxKind.AsKeyword) { - // { type as as ...? } - const secondAs = parseIdentifierName(); - if (ts.tokenIsIdentifierOrKeyword(token())) { - // { type as as something } - isTypeOnly = true; - propertyName = firstAs; - name = parseNameWithKeywordCheck(); - canParseAsKeyword = false; - } - else { - // { type as as } - propertyName = name; - name = secondAs; - canParseAsKeyword = false; - } - } - else if (ts.tokenIsIdentifierOrKeyword(token())) { - // { type as something } - propertyName = name; - canParseAsKeyword = false; + // { type as as ...? } + const secondAs = parseIdentifierName(); + if (ts.tokenIsIdentifierOrKeyword(token())) { + // { type as as something } + isTypeOnly = true; + propertyName = firstAs; name = parseNameWithKeywordCheck(); + canParseAsKeyword = false; } else { - // { type as } - isTypeOnly = true; - name = firstAs; + // { type as as } + propertyName = name; + name = secondAs; + canParseAsKeyword = false; } } else if (ts.tokenIsIdentifierOrKeyword(token())) { - // { type something ...? } - isTypeOnly = true; + // { type as something } + propertyName = name; + canParseAsKeyword = false; name = parseNameWithKeywordCheck(); } + else { + // { type as } + isTypeOnly = true; + name = firstAs; + } } - - if (canParseAsKeyword && token() === ts.SyntaxKind.AsKeyword) { - propertyName = name; - parseExpected(ts.SyntaxKind.AsKeyword); + else if (ts.tokenIsIdentifierOrKeyword(token())) { + // { type something ...? } + isTypeOnly = true; name = parseNameWithKeywordCheck(); } - if (kind === ts.SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) { - parseErrorAt(checkIdentifierStart, checkIdentifierEnd, ts.Diagnostics.Identifier_expected); - } - const node = kind === ts.SyntaxKind.ImportSpecifier - ? factory.createImportSpecifier(isTypeOnly, propertyName, name) - : factory.createExportSpecifier(isTypeOnly, propertyName, name); - return finishNode(node, pos); + } - function parseNameWithKeywordCheck() { - checkIdentifierIsKeyword = ts.isKeyword(token()) && !isIdentifier(); - checkIdentifierStart = scanner.getTokenPos(); - checkIdentifierEnd = scanner.getTextPos(); - return parseIdentifierName(); - } + if (canParseAsKeyword && token() === ts.SyntaxKind.AsKeyword) { + propertyName = name; + parseExpected(ts.SyntaxKind.AsKeyword); + name = parseNameWithKeywordCheck(); + } + if (kind === ts.SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) { + parseErrorAt(checkIdentifierStart, checkIdentifierEnd, ts.Diagnostics.Identifier_expected); } + const node = kind === ts.SyntaxKind.ImportSpecifier + ? factory.createImportSpecifier(isTypeOnly, propertyName, name) + : factory.createExportSpecifier(isTypeOnly, propertyName, name); + return finishNode(node, pos); - function parseNamespaceExport(pos: number): ts.NamespaceExport { - return finishNode(factory.createNamespaceExport(parseIdentifierName()), pos); + function parseNameWithKeywordCheck() { + checkIdentifierIsKeyword = ts.isKeyword(token()) && !isIdentifier(); + checkIdentifierStart = scanner.getTokenPos(); + checkIdentifierEnd = scanner.getTextPos(); + return parseIdentifierName(); } + } - function parseExportDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ExportDeclaration { - const savedAwaitContext = inAwaitContext(); - setAwaitContext(/*value*/ true); - let exportClause: ts.NamedExportBindings | undefined; - let moduleSpecifier: ts.Expression | undefined; - let assertClause: ts.AssertClause | undefined; - const isTypeOnly = parseOptional(ts.SyntaxKind.TypeKeyword); - const namespaceExportPos = getNodePos(); - if (parseOptional(ts.SyntaxKind.AsteriskToken)) { - if (parseOptional(ts.SyntaxKind.AsKeyword)) { - exportClause = parseNamespaceExport(namespaceExportPos); - } + function parseNamespaceExport(pos: number): ts.NamespaceExport { + return finishNode(factory.createNamespaceExport(parseIdentifierName()), pos); + } + + function parseExportDeclaration(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ExportDeclaration { + const savedAwaitContext = inAwaitContext(); + setAwaitContext(/*value*/ true); + let exportClause: ts.NamedExportBindings | undefined; + let moduleSpecifier: ts.Expression | undefined; + let assertClause: ts.AssertClause | undefined; + const isTypeOnly = parseOptional(ts.SyntaxKind.TypeKeyword); + const namespaceExportPos = getNodePos(); + if (parseOptional(ts.SyntaxKind.AsteriskToken)) { + if (parseOptional(ts.SyntaxKind.AsKeyword)) { + exportClause = parseNamespaceExport(namespaceExportPos); + } + parseExpected(ts.SyntaxKind.FromKeyword); + moduleSpecifier = parseModuleSpecifier(); + } + else { + exportClause = parseNamedImportsOrExports(ts.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() === ts.SyntaxKind.FromKeyword || (token() === ts.SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) { parseExpected(ts.SyntaxKind.FromKeyword); moduleSpecifier = parseModuleSpecifier(); } - else { - exportClause = parseNamedImportsOrExports(ts.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() === ts.SyntaxKind.FromKeyword || (token() === ts.SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) { - parseExpected(ts.SyntaxKind.FromKeyword); - moduleSpecifier = parseModuleSpecifier(); - } - } - if (moduleSpecifier && token() === ts.SyntaxKind.AssertKeyword && !scanner.hasPrecedingLineBreak()) { - assertClause = parseAssertClause(); - } - parseSemicolon(); - setAwaitContext(savedAwaitContext); - const node = factory.createExportDeclaration(decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier, assertClause); - return withJSDoc(finishNode(node, pos), hasJSDoc); } + if (moduleSpecifier && token() === ts.SyntaxKind.AssertKeyword && !scanner.hasPrecedingLineBreak()) { + assertClause = parseAssertClause(); + } + parseSemicolon(); + setAwaitContext(savedAwaitContext); + const node = factory.createExportDeclaration(decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier, assertClause); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - function parseExportAssignment(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ExportAssignment { - const savedAwaitContext = inAwaitContext(); - setAwaitContext(/*value*/ true); - let isExportEquals: boolean | undefined; - if (parseOptional(ts.SyntaxKind.EqualsToken)) { - isExportEquals = true; - } - else { - parseExpected(ts.SyntaxKind.DefaultKeyword); - } - const expression = parseAssignmentExpressionOrHigher(); - parseSemicolon(); - setAwaitContext(savedAwaitContext); - const node = factory.createExportAssignment(decorators, modifiers, isExportEquals, expression); - return withJSDoc(finishNode(node, pos), hasJSDoc); + function parseExportAssignment(pos: number, hasJSDoc: boolean, decorators: ts.NodeArray | undefined, modifiers: ts.NodeArray | undefined): ts.ExportAssignment { + const savedAwaitContext = inAwaitContext(); + setAwaitContext(/*value*/ true); + let isExportEquals: boolean | undefined; + if (parseOptional(ts.SyntaxKind.EqualsToken)) { + isExportEquals = true; + } + else { + parseExpected(ts.SyntaxKind.DefaultKeyword); } + const expression = parseAssignmentExpressionOrHigher(); + parseSemicolon(); + setAwaitContext(savedAwaitContext); + const node = factory.createExportAssignment(decorators, modifiers, isExportEquals, expression); + return withJSDoc(finishNode(node, pos), hasJSDoc); + } - 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, - AssertEntries, - 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: ts.JSDocTypeExpression; - diagnostics: ts.Diagnostic[]; - } | undefined { - initializeState("file.js", content, ts.ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ts.ScriptKind.JS); - scanner.setText(content, start, length); - currentToken = scanner.scan(); - const jsDocTypeExpression = parseJSDocTypeExpression(); - - const sourceFile = createSourceFile("file.js", ts.ScriptTarget.Latest, ts.ScriptKind.JS, /*isDeclarationFile*/ false, [], factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None, ts.noop); - const diagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); - if (jsDocDiagnostics) { - sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); - } - - clearState(); - - return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; - } - - // Parses out a JSDoc type expression. - export function parseJSDocTypeExpression(mayOmitBraces?: boolean): ts.JSDocTypeExpression { - const pos = getNodePos(); - const hasBrace = (mayOmitBraces ? parseOptional : parseExpected)(ts.SyntaxKind.OpenBraceToken); - const type = doInsideOfContext(ts.NodeFlags.JSDoc, parseJSDocType); - if (!mayOmitBraces || hasBrace) { - parseExpectedJSDoc(ts.SyntaxKind.CloseBraceToken); - } + 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, + AssertEntries, + Count // Number of parsing contexts + } + + const enum Tristate { + False, + True, + Unknown + } - const result = factory.createJSDocTypeExpression(type); - fixupParentReferences(result); - return finishNode(result, pos); + export namespace JSDocParser { + export function parseJSDocTypeExpressionForTests(content: string, start: number | undefined, length: number | undefined): { + jsDocTypeExpression: ts.JSDocTypeExpression; + diagnostics: ts.Diagnostic[]; + } | undefined { + initializeState("file.js", content, ts.ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ts.ScriptKind.JS); + scanner.setText(content, start, length); + currentToken = scanner.scan(); + const jsDocTypeExpression = parseJSDocTypeExpression(); + + const sourceFile = createSourceFile("file.js", ts.ScriptTarget.Latest, ts.ScriptKind.JS, /*isDeclarationFile*/ false, [], factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None, ts.noop); + const diagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); + if (jsDocDiagnostics) { + sourceFile.jsDocDiagnostics = ts.attachFileToDiagnostics(jsDocDiagnostics, sourceFile); } - export function parseJSDocNameReference(): ts.JSDocNameReference { - const pos = getNodePos(); - const hasBrace = parseOptional(ts.SyntaxKind.OpenBraceToken); - const p2 = getNodePos(); - let entityName: ts.EntityName | ts.JSDocMemberName = parseEntityName(/* allowReservedWords*/ false); - while (token() === ts.SyntaxKind.PrivateIdentifier) { - reScanHashToken(); // rescan #id as # id - nextTokenJSDoc(); // then skip the # - entityName = finishNode(factory.createJSDocMemberName(entityName, parseIdentifier()), p2); - } - if (hasBrace) { - parseExpectedJSDoc(ts.SyntaxKind.CloseBraceToken); - } + clearState(); + + return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; + } - const result = factory.createJSDocNameReference(entityName); - fixupParentReferences(result); - return finishNode(result, pos); + // Parses out a JSDoc type expression. + export function parseJSDocTypeExpression(mayOmitBraces?: boolean): ts.JSDocTypeExpression { + const pos = getNodePos(); + const hasBrace = (mayOmitBraces ? parseOptional : parseExpected)(ts.SyntaxKind.OpenBraceToken); + const type = doInsideOfContext(ts.NodeFlags.JSDoc, parseJSDocType); + if (!mayOmitBraces || hasBrace) { + parseExpectedJSDoc(ts.SyntaxKind.CloseBraceToken); } - export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { - jsDoc: ts.JSDoc; - diagnostics: ts.Diagnostic[]; - } | undefined { - initializeState("", content, ts.ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ts.ScriptKind.JS); - const jsDoc = doInsideOfContext(ts.NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); - const sourceFile = { languageVariant: ts.LanguageVariant.Standard, text: content } as ts.SourceFile; - const diagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); - clearState(); + const result = factory.createJSDocTypeExpression(type); + fixupParentReferences(result); + return finishNode(result, pos); + } - return jsDoc ? { jsDoc, diagnostics } : undefined; - } + export function parseJSDocNameReference(): ts.JSDocNameReference { + const pos = getNodePos(); + const hasBrace = parseOptional(ts.SyntaxKind.OpenBraceToken); + const p2 = getNodePos(); + let entityName: ts.EntityName | ts.JSDocMemberName = parseEntityName(/* allowReservedWords*/ false); + while (token() === ts.SyntaxKind.PrivateIdentifier) { + reScanHashToken(); // rescan #id as # id + nextTokenJSDoc(); // then skip the # + entityName = finishNode(factory.createJSDocMemberName(entityName, parseIdentifier()), p2); + } + if (hasBrace) { + parseExpectedJSDoc(ts.SyntaxKind.CloseBraceToken); + } + + const result = factory.createJSDocNameReference(entityName); + fixupParentReferences(result); + return finishNode(result, pos); + } + + export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { + jsDoc: ts.JSDoc; + diagnostics: ts.Diagnostic[]; + } | undefined { + initializeState("", content, ts.ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ts.ScriptKind.JS); + const jsDoc = doInsideOfContext(ts.NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); + const sourceFile = { languageVariant: ts.LanguageVariant.Standard, text: content } as ts.SourceFile; + const diagnostics = ts.attachFileToDiagnostics(parseDiagnostics, sourceFile); + clearState(); - export function parseJSDocComment(parent: ts.HasJSDoc, start: number, length: number): ts.JSDoc | undefined { - const saveToken = currentToken; - const saveParseDiagnosticsLength = parseDiagnostics.length; - const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + return jsDoc ? { jsDoc, diagnostics } : undefined; + } - const comment = doInsideOfContext(ts.NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); - ts.setParent(comment, parent); - if (contextFlags & ts.NodeFlags.JavaScriptFile) { - if (!jsDocDiagnostics) { - jsDocDiagnostics = []; - } - jsDocDiagnostics.push(...parseDiagnostics); + export function parseJSDocComment(parent: ts.HasJSDoc, start: number, length: number): ts.JSDoc | undefined { + const saveToken = currentToken; + const saveParseDiagnosticsLength = parseDiagnostics.length; + const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + + const comment = doInsideOfContext(ts.NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); + ts.setParent(comment, parent); + if (contextFlags & ts.NodeFlags.JavaScriptFile) { + if (!jsDocDiagnostics) { + jsDocDiagnostics = []; } - currentToken = saveToken; - parseDiagnostics.length = saveParseDiagnosticsLength; - parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; - return comment; + jsDocDiagnostics.push(...parseDiagnostics); } + currentToken = saveToken; + parseDiagnostics.length = saveParseDiagnosticsLength; + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; + return comment; + } - const enum JSDocState { - BeginningOfLine, - SawAsterisk, - SavingComments, - SavingBackticks - } + const enum JSDocState { + BeginningOfLine, + SawAsterisk, + SavingComments, + SavingBackticks + } - const enum PropertyLikeParse { - Property = 1 << 0, - Parameter = 1 << 1, - CallbackParameter = 1 << 2 - } + const enum PropertyLikeParse { + Property = 1 << 0, + Parameter = 1 << 1, + CallbackParameter = 1 << 2 + } - function parseJSDocCommentWorker(start = 0, length: number | undefined): ts.JSDoc | undefined { - const content = sourceText; - const end = length === undefined ? content.length : start + length; - length = end - start; + function parseJSDocCommentWorker(start = 0, length: number | undefined): ts.JSDoc | undefined { + const content = sourceText; + const end = length === undefined ? content.length : start + length; + length = end - start; - ts.Debug.assert(start >= 0); - ts.Debug.assert(start <= end); - ts.Debug.assert(end <= content.length); + ts.Debug.assert(start >= 0); + ts.Debug.assert(start <= end); + ts.Debug.assert(end <= content.length); - // Check for /** (JSDoc opening part) - if (!isJSDocLikeText(content, start)) { - return undefined; - } + // Check for /** (JSDoc opening part) + if (!isJSDocLikeText(content, start)) { + return undefined; + } - let tags: ts.JSDocTag[]; - let tagsPos: number; - let tagsEnd: number; - let linkEnd: number; - let commentsPos: number | undefined; - let comments: string[] = []; - const parts: ts.JSDocComment[] = []; + let tags: ts.JSDocTag[]; + let tagsPos: number; + let tagsEnd: number; + let linkEnd: number; + let commentsPos: number | undefined; + let comments: string[] = []; + const parts: ts.JSDocComment[] = []; - // + 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 '/** ' - // + 1 because the last index of \n is always one index before the first character in the line and coincidentally, if there is no \n before start, it is -1, which is also one index before the first character - let indent = start - (content.lastIndexOf("\n", start) + 1) + 4; - function pushComment(text: string) { - if (!margin) { - margin = indent; - } - comments.push(text); - indent += text.length; + // + 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 '/** ' + // + 1 because the last index of \n is always one index before the first character in the line and coincidentally, if there is no \n before start, it is -1, which is also one index before the first character + let indent = start - (content.lastIndexOf("\n", start) + 1) + 4; + function pushComment(text: string) { + if (!margin) { + margin = indent; } + comments.push(text); + indent += text.length; + } - nextTokenJSDoc(); - while (parseOptionalJsdoc(ts.SyntaxKind.WhitespaceTrivia)) - ; - if (parseOptionalJsdoc(ts.SyntaxKind.NewLineTrivia)) { - state = JSDocState.BeginningOfLine; - indent = 0; - } - loop: while (true) { - switch (token()) { - case ts.SyntaxKind.AtToken: - if (state === JSDocState.BeginningOfLine || state === JSDocState.SawAsterisk) { - removeTrailingWhitespace(comments); - if (!commentsPos) - commentsPos = getNodePos(); - 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 ts.SyntaxKind.NewLineTrivia: - comments.push(scanner.getTokenText()); + nextTokenJSDoc(); + while (parseOptionalJsdoc(ts.SyntaxKind.WhitespaceTrivia)) + ; + if (parseOptionalJsdoc(ts.SyntaxKind.NewLineTrivia)) { + state = JSDocState.BeginningOfLine; + indent = 0; + } + loop: while (true) { + switch (token()) { + case ts.SyntaxKind.AtToken: + if (state === JSDocState.BeginningOfLine || state === JSDocState.SawAsterisk) { + removeTrailingWhitespace(comments); + if (!commentsPos) + commentsPos = getNodePos(); + 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 ts.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 ts.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)); - } - indent += whitespace.length; - break; - case ts.SyntaxKind.EndOfFileToken: - break loop; - case ts.SyntaxKind.OpenBraceToken: + margin = undefined; + } + else { + pushComment(scanner.getTokenText()); + } + break; + case ts.SyntaxKind.NewLineTrivia: + comments.push(scanner.getTokenText()); + state = JSDocState.BeginningOfLine; + indent = 0; + break; + case ts.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; - const commentEnd = scanner.getStartPos(); - const linkStart = scanner.getTextPos() - 1; - const link = parseJSDocLink(linkStart); - if (link) { - if (!linkEnd) { - removeLeadingNewlines(comments); - } - parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? start, commentEnd)); - parts.push(link); - comments = []; - linkEnd = scanner.getTextPos(); - break; + pushComment(asterisk); + } + else { + // Ignore the first asterisk on a line + state = JSDocState.SawAsterisk; + indent += asterisk.length; + } + break; + case ts.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)); + } + indent += whitespace.length; + break; + case ts.SyntaxKind.EndOfFileToken: + break loop; + case ts.SyntaxKind.OpenBraceToken: + state = JSDocState.SavingComments; + const commentEnd = scanner.getStartPos(); + const linkStart = scanner.getTextPos() - 1; + const link = parseJSDocLink(linkStart); + if (link) { + if (!linkEnd) { + removeLeadingNewlines(comments); } - // fallthrough if it's not a {@link sequence - 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; - pushComment(scanner.getTokenText()); + parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? start, commentEnd)); + parts.push(link); + comments = []; + linkEnd = scanner.getTextPos(); break; - } - nextTokenJSDoc(); - } - removeTrailingWhitespace(comments); - if (parts.length && comments.length) { - parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? start, commentsPos)); - } - if (parts.length && tags) - ts.Debug.assertIsDefined(commentsPos, "having parsed tags implies that the end of the comment span should be set"); - const tagsArray = tags && createNodeArray(tags, tagsPos, tagsEnd); - return finishNode(factory.createJSDocComment(parts.length ? createNodeArray(parts, start, commentsPos) : comments.length ? comments.join("") : undefined, tagsArray), start, end); - }); - - function removeLeadingNewlines(comments: string[]) { - while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { - comments.shift(); + } + // fallthrough if it's not a {@link sequence + 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; + pushComment(scanner.getTokenText()); + break; } + nextTokenJSDoc(); } + removeTrailingWhitespace(comments); + if (parts.length && comments.length) { + parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? start, commentsPos)); + } + if (parts.length && tags) + ts.Debug.assertIsDefined(commentsPos, "having parsed tags implies that the end of the comment span should be set"); + const tagsArray = tags && createNodeArray(tags, tagsPos, tagsEnd); + return finishNode(factory.createJSDocComment(parts.length ? createNodeArray(parts, start, commentsPos) : comments.length ? comments.join("") : undefined, tagsArray), start, end); + }); - function removeTrailingWhitespace(comments: string[]) { - while (comments.length && comments[comments.length - 1].trim() === "") { - comments.pop(); - } + function removeLeadingNewlines(comments: string[]) { + while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { + comments.shift(); } + } - function isNextNonwhitespaceTokenEndOfFile(): boolean { - // We must use infinite lookahead, as there could be any number of newlines :( - while (true) { - nextTokenJSDoc(); - if (token() === ts.SyntaxKind.EndOfFileToken) { - return true; - } - if (!(token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia)) { - return false; - } - } + function removeTrailingWhitespace(comments: string[]) { + while (comments.length && comments[comments.length - 1].trim() === "") { + comments.pop(); } + } - function skipWhitespace(): void { - if (token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.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 - } + function isNextNonwhitespaceTokenEndOfFile(): boolean { + // We must use infinite lookahead, as there could be any number of newlines :( + while (true) { + nextTokenJSDoc(); + if (token() === ts.SyntaxKind.EndOfFileToken) { + return true; } - while (token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia) { - nextTokenJSDoc(); + if (!(token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia)) { + return false; } } + } - function skipWhitespaceOrAsterisk(): string { - if (token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.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 - } + function skipWhitespace(): void { + if (token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.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() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia) { + nextTokenJSDoc(); + } + } - let precedingLineBreak = scanner.hasPrecedingLineBreak(); - let seenLineBreak = false; - let indentText = ""; - while ((precedingLineBreak && token() === ts.SyntaxKind.AsteriskToken) || token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia) { - indentText += scanner.getTokenText(); - if (token() === ts.SyntaxKind.NewLineTrivia) { - precedingLineBreak = true; - seenLineBreak = true; - indentText = ""; - } - else if (token() === ts.SyntaxKind.AsteriskToken) { - precedingLineBreak = false; - } - nextTokenJSDoc(); + function skipWhitespaceOrAsterisk(): string { + if (token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.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 } - return seenLineBreak ? indentText : ""; } - function parseTag(margin: number) { - ts.Debug.assert(token() === ts.SyntaxKind.AtToken); - const start = scanner.getTokenPos(); + let precedingLineBreak = scanner.hasPrecedingLineBreak(); + let seenLineBreak = false; + let indentText = ""; + while ((precedingLineBreak && token() === ts.SyntaxKind.AsteriskToken) || token() === ts.SyntaxKind.WhitespaceTrivia || token() === ts.SyntaxKind.NewLineTrivia) { + indentText += scanner.getTokenText(); + if (token() === ts.SyntaxKind.NewLineTrivia) { + precedingLineBreak = true; + seenLineBreak = true; + indentText = ""; + } + else if (token() === ts.SyntaxKind.AsteriskToken) { + precedingLineBreak = false; + } nextTokenJSDoc(); + } + return seenLineBreak ? indentText : ""; + } - const tagName = parseJSDocIdentifierName(/*message*/ undefined); - const indentText = skipWhitespaceOrAsterisk(); + function parseTag(margin: number) { + ts.Debug.assert(token() === ts.SyntaxKind.AtToken); + const start = scanner.getTokenPos(); + nextTokenJSDoc(); - let tag: ts.JSDocTag | undefined; - switch (tagName.escapedText) { - case "author": - tag = parseAuthorTag(start, tagName, margin, indentText); - break; - case "implements": - tag = parseImplementsTag(start, tagName, margin, indentText); - break; - case "augments": - case "extends": - tag = parseAugmentsTag(start, tagName, margin, indentText); - break; - case "class": - case "constructor": - tag = parseSimpleTag(start, factory.createJSDocClassTag, tagName, margin, indentText); - break; - case "public": - tag = parseSimpleTag(start, factory.createJSDocPublicTag, tagName, margin, indentText); - break; - case "private": - tag = parseSimpleTag(start, factory.createJSDocPrivateTag, tagName, margin, indentText); - break; - case "protected": - tag = parseSimpleTag(start, factory.createJSDocProtectedTag, tagName, margin, indentText); - break; - case "readonly": - tag = parseSimpleTag(start, factory.createJSDocReadonlyTag, tagName, margin, indentText); - break; - case "override": - tag = parseSimpleTag(start, factory.createJSDocOverrideTag, tagName, margin, indentText); - break; - case "deprecated": - hasDeprecatedTag = true; - tag = parseSimpleTag(start, factory.createJSDocDeprecatedTag, tagName, margin, indentText); - break; - case "this": - tag = parseThisTag(start, tagName, margin, indentText); - break; - case "enum": - tag = parseEnumTag(start, tagName, margin, indentText); - break; - case "arg": - case "argument": - case "param": - return parseParameterOrPropertyTag(start, tagName, PropertyLikeParse.Parameter, margin); - case "return": - case "returns": - tag = parseReturnTag(start, tagName, margin, indentText); - break; - case "template": - tag = parseTemplateTag(start, tagName, margin, indentText); - break; - case "type": - tag = parseTypeTag(start, tagName, margin, indentText); - break; - case "typedef": - tag = parseTypedefTag(start, tagName, margin, indentText); - break; - case "callback": - tag = parseCallbackTag(start, tagName, margin, indentText); - break; - case "see": - tag = parseSeeTag(start, tagName, margin, indentText); - break; - default: - tag = parseUnknownTag(start, tagName, margin, indentText); - break; - } - return tag; + const tagName = parseJSDocIdentifierName(/*message*/ undefined); + const indentText = skipWhitespaceOrAsterisk(); + + let tag: ts.JSDocTag | undefined; + switch (tagName.escapedText) { + case "author": + tag = parseAuthorTag(start, tagName, margin, indentText); + break; + case "implements": + tag = parseImplementsTag(start, tagName, margin, indentText); + break; + case "augments": + case "extends": + tag = parseAugmentsTag(start, tagName, margin, indentText); + break; + case "class": + case "constructor": + tag = parseSimpleTag(start, factory.createJSDocClassTag, tagName, margin, indentText); + break; + case "public": + tag = parseSimpleTag(start, factory.createJSDocPublicTag, tagName, margin, indentText); + break; + case "private": + tag = parseSimpleTag(start, factory.createJSDocPrivateTag, tagName, margin, indentText); + break; + case "protected": + tag = parseSimpleTag(start, factory.createJSDocProtectedTag, tagName, margin, indentText); + break; + case "readonly": + tag = parseSimpleTag(start, factory.createJSDocReadonlyTag, tagName, margin, indentText); + break; + case "override": + tag = parseSimpleTag(start, factory.createJSDocOverrideTag, tagName, margin, indentText); + break; + case "deprecated": + hasDeprecatedTag = true; + tag = parseSimpleTag(start, factory.createJSDocDeprecatedTag, tagName, margin, indentText); + break; + case "this": + tag = parseThisTag(start, tagName, margin, indentText); + break; + case "enum": + tag = parseEnumTag(start, tagName, margin, indentText); + break; + case "arg": + case "argument": + case "param": + return parseParameterOrPropertyTag(start, tagName, PropertyLikeParse.Parameter, margin); + case "return": + case "returns": + tag = parseReturnTag(start, tagName, margin, indentText); + break; + case "template": + tag = parseTemplateTag(start, tagName, margin, indentText); + break; + case "type": + tag = parseTypeTag(start, tagName, margin, indentText); + break; + case "typedef": + tag = parseTypedefTag(start, tagName, margin, indentText); + break; + case "callback": + tag = parseCallbackTag(start, tagName, margin, indentText); + break; + case "see": + tag = parseSeeTag(start, tagName, margin, indentText); + break; + default: + tag = parseUnknownTag(start, tagName, margin, indentText); + break; } + return tag; + } - function parseTrailingTagComments(pos: number, end: number, margin: number, indentText: string) { - // some tags, like typedef and callback, have already parsed their comments earlier - if (!indentText) { - margin += end - pos; - } - return parseTagComments(margin, indentText.slice(margin)); - } - - function parseTagComments(indent: number, initialMargin?: string): string | ts.NodeArray | undefined { - const commentsPos = getNodePos(); - let comments: string[] = []; - const parts: ts.JSDocComment[] = []; - let linkEnd; - let state = JSDocState.BeginningOfLine; - let previousWhitespace = true; - 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 ts.JSDocSyntaxKind; - loop: while (true) { - switch (tok) { - case ts.SyntaxKind.NewLineTrivia: - state = JSDocState.BeginningOfLine; - // don't use pushComment here because we want to keep the margin unchanged + function parseTrailingTagComments(pos: number, end: number, margin: number, indentText: string) { + // some tags, like typedef and callback, have already parsed their comments earlier + if (!indentText) { + margin += end - pos; + } + return parseTagComments(margin, indentText.slice(margin)); + } + + function parseTagComments(indent: number, initialMargin?: string): string | ts.NodeArray | undefined { + const commentsPos = getNodePos(); + let comments: string[] = []; + const parts: ts.JSDocComment[] = []; + let linkEnd; + let state = JSDocState.BeginningOfLine; + let previousWhitespace = true; + 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 ts.JSDocSyntaxKind; + loop: while (true) { + switch (tok) { + case ts.SyntaxKind.NewLineTrivia: + state = JSDocState.BeginningOfLine; + // don't use pushComment here because we want to keep the margin unchanged + comments.push(scanner.getTokenText()); + indent = 0; + break; + case ts.SyntaxKind.AtToken: + if (state === JSDocState.SavingBackticks + || state === JSDocState.SavingComments && (!previousWhitespace || lookAhead(isNextJSDocTokenWhitespace))) { + // @ doesn't start a new tag inside ``, and inside a comment, only after whitespace or not before whitespace comments.push(scanner.getTokenText()); - indent = 0; - break; - case ts.SyntaxKind.AtToken: - if (state === JSDocState.SavingBackticks - || state === JSDocState.SavingComments && (!previousWhitespace || lookAhead(isNextJSDocTokenWhitespace))) { - // @ doesn't start a new tag inside ``, and inside a comment, only after whitespace or not before whitespace - comments.push(scanner.getTokenText()); - break; - } - scanner.setTextPos(scanner.getTextPos() - 1); - // falls through - case ts.SyntaxKind.EndOfFileToken: - // Done - break loop; - case ts.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 ts.SyntaxKind.OpenBraceToken: - state = JSDocState.SavingComments; - const commentEnd = scanner.getStartPos(); - const linkStart = scanner.getTextPos() - 1; - const link = parseJSDocLink(linkStart); - if (link) { - parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? commentsPos, commentEnd)); - parts.push(link); - comments = []; - linkEnd = scanner.getTextPos(); - } - else { - pushComment(scanner.getTokenText()); - } break; - case ts.SyntaxKind.BacktickToken: - if (state === JSDocState.SavingBackticks) { - state = JSDocState.SavingComments; - } - else { - state = JSDocState.SavingBackticks; - } + } + scanner.setTextPos(scanner.getTextPos() - 1); + // falls through + case ts.SyntaxKind.EndOfFileToken: + // Done + break loop; + case ts.SyntaxKind.WhitespaceTrivia: + if (state === JSDocState.SavingComments || state === JSDocState.SavingBackticks) { pushComment(scanner.getTokenText()); - break; - case ts.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 + } + 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 ts.SyntaxKind.OpenBraceToken: + state = JSDocState.SavingComments; + const commentEnd = scanner.getStartPos(); + const linkStart = scanner.getTextPos() - 1; + const link = parseJSDocLink(linkStart); + if (link) { + parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? commentsPos, commentEnd)); + parts.push(link); + comments = []; + linkEnd = scanner.getTextPos(); + } + else { pushComment(scanner.getTokenText()); + } + break; + case ts.SyntaxKind.BacktickToken: + if (state === JSDocState.SavingBackticks) { + state = JSDocState.SavingComments; + } + else { + state = JSDocState.SavingBackticks; + } + pushComment(scanner.getTokenText()); + break; + case ts.SyntaxKind.AsteriskToken: + if (state === JSDocState.BeginningOfLine) { + // leading asterisks start recording on the *next* (non-whitespace) token + state = JSDocState.SawAsterisk; + indent += 1; break; - } - previousWhitespace = token() === ts.SyntaxKind.WhitespaceTrivia; - tok = nextTokenJSDoc(); + } + // 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; } + previousWhitespace = token() === ts.SyntaxKind.WhitespaceTrivia; + tok = nextTokenJSDoc(); + } - removeLeadingNewlines(comments); - removeTrailingWhitespace(comments); - if (parts.length) { - if (comments.length) { - parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? commentsPos)); - } - return createNodeArray(parts, commentsPos, scanner.getTextPos()); - } - else if (comments.length) { - return comments.join(""); + removeLeadingNewlines(comments); + removeTrailingWhitespace(comments); + if (parts.length) { + if (comments.length) { + parts.push(finishNode(factory.createJSDocText(comments.join("")), linkEnd ?? commentsPos)); } + return createNodeArray(parts, commentsPos, scanner.getTextPos()); } - - function isNextJSDocTokenWhitespace() { - const next = nextTokenJSDoc(); - return next === ts.SyntaxKind.WhitespaceTrivia || next === ts.SyntaxKind.NewLineTrivia; + else if (comments.length) { + return comments.join(""); } + } - function parseJSDocLink(start: number) { - const linkType = tryParse(parseJSDocLinkPrefix); - if (!linkType) { - return undefined; - } - nextTokenJSDoc(); // start at token after link, then skip any whitespace - skipWhitespace(); - // parseEntityName logs an error for non-identifier, so create a MissingNode ourselves to avoid the error - const p2 = getNodePos(); - let name: ts.EntityName | ts.JSDocMemberName | undefined = ts.tokenIsIdentifierOrKeyword(token()) - ? parseEntityName(/*allowReservedWords*/ true) - : undefined; - if (name) { - while (token() === ts.SyntaxKind.PrivateIdentifier) { - reScanHashToken(); // rescan #id as # id - nextTokenJSDoc(); // then skip the # - name = finishNode(factory.createJSDocMemberName(name, parseIdentifier()), p2); - } - } - const text = []; - while (token() !== ts.SyntaxKind.CloseBraceToken && token() !== ts.SyntaxKind.NewLineTrivia && token() !== ts.SyntaxKind.EndOfFileToken) { - text.push(scanner.getTokenText()); - nextTokenJSDoc(); - } - const create = linkType === "link" ? factory.createJSDocLink - : linkType === "linkcode" ? factory.createJSDocLinkCode - : factory.createJSDocLinkPlain; - return finishNode(create(name, text.join("")), start, scanner.getTextPos()); - } + function isNextJSDocTokenWhitespace() { + const next = nextTokenJSDoc(); + return next === ts.SyntaxKind.WhitespaceTrivia || next === ts.SyntaxKind.NewLineTrivia; + } - function parseJSDocLinkPrefix() { - skipWhitespaceOrAsterisk(); - if (token() === ts.SyntaxKind.OpenBraceToken - && nextTokenJSDoc() === ts.SyntaxKind.AtToken - && ts.tokenIsIdentifierOrKeyword(nextTokenJSDoc())) { - const kind = scanner.getTokenValue(); - if (isJSDocLinkTag(kind)) - return kind; + function parseJSDocLink(start: number) { + const linkType = tryParse(parseJSDocLinkPrefix); + if (!linkType) { + return undefined; + } + nextTokenJSDoc(); // start at token after link, then skip any whitespace + skipWhitespace(); + // parseEntityName logs an error for non-identifier, so create a MissingNode ourselves to avoid the error + const p2 = getNodePos(); + let name: ts.EntityName | ts.JSDocMemberName | undefined = ts.tokenIsIdentifierOrKeyword(token()) + ? parseEntityName(/*allowReservedWords*/ true) + : undefined; + if (name) { + while (token() === ts.SyntaxKind.PrivateIdentifier) { + reScanHashToken(); // rescan #id as # id + nextTokenJSDoc(); // then skip the # + name = finishNode(factory.createJSDocMemberName(name, parseIdentifier()), p2); } } - - function isJSDocLinkTag(kind: string) { - return kind === "link" || kind === "linkcode" || kind === "linkplain"; + const text = []; + while (token() !== ts.SyntaxKind.CloseBraceToken && token() !== ts.SyntaxKind.NewLineTrivia && token() !== ts.SyntaxKind.EndOfFileToken) { + text.push(scanner.getTokenText()); + nextTokenJSDoc(); } + const create = linkType === "link" ? factory.createJSDocLink + : linkType === "linkcode" ? factory.createJSDocLinkCode + : factory.createJSDocLinkPlain; + return finishNode(create(name, text.join("")), start, scanner.getTextPos()); + } - function parseUnknownTag(start: number, tagName: ts.Identifier, indent: number, indentText: string) { - return finishNode(factory.createJSDocUnknownTag(tagName, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + function parseJSDocLinkPrefix() { + skipWhitespaceOrAsterisk(); + if (token() === ts.SyntaxKind.OpenBraceToken + && nextTokenJSDoc() === ts.SyntaxKind.AtToken + && ts.tokenIsIdentifierOrKeyword(nextTokenJSDoc())) { + const kind = scanner.getTokenValue(); + if (isJSDocLinkTag(kind)) + return kind; } + } - function addTag(tag: ts.JSDocTag | undefined): void { - if (!tag) { - return; - } - if (!tags) { - tags = [tag]; - tagsPos = tag.pos; - } - else { - tags.push(tag); - } - tagsEnd = tag.end; - } + function isJSDocLinkTag(kind: string) { + return kind === "link" || kind === "linkcode" || kind === "linkplain"; + } - function tryParseTypeExpression(): ts.JSDocTypeExpression | undefined { - skipWhitespaceOrAsterisk(); - return token() === ts.SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; + function parseUnknownTag(start: number, tagName: ts.Identifier, indent: number, indentText: string) { + return finishNode(factory.createJSDocUnknownTag(tagName, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + } + + function addTag(tag: ts.JSDocTag | undefined): void { + if (!tag) { + return; + } + if (!tags) { + tags = [tag]; + tagsPos = tag.pos; } + else { + tags.push(tag); + } + tagsEnd = tag.end; + } - function parseBracketNameInPropertyAndParamTag(): { - name: ts.EntityName; - isBracketed: boolean; - } { - // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' - const isBracketed = parseOptionalJsdoc(ts.SyntaxKind.OpenBracketToken); - if (isBracketed) { - skipWhitespace(); - } - // a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild - const isBackquoted = parseOptionalJsdoc(ts.SyntaxKind.BacktickToken); - const name = parseJSDocEntityName(); - if (isBackquoted) { - parseExpectedTokenJSDoc(ts.SyntaxKind.BacktickToken); - } - if (isBracketed) { - skipWhitespace(); - // May have an optional default, e.g. '[foo = 42]' - if (parseOptionalToken(ts.SyntaxKind.EqualsToken)) { - parseExpression(); - } + function tryParseTypeExpression(): ts.JSDocTypeExpression | undefined { + skipWhitespaceOrAsterisk(); + return token() === ts.SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; + } - parseExpected(ts.SyntaxKind.CloseBracketToken); + function parseBracketNameInPropertyAndParamTag(): { + name: ts.EntityName; + isBracketed: boolean; + } { + // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' + const isBracketed = parseOptionalJsdoc(ts.SyntaxKind.OpenBracketToken); + if (isBracketed) { + skipWhitespace(); + } + // a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild + const isBackquoted = parseOptionalJsdoc(ts.SyntaxKind.BacktickToken); + const name = parseJSDocEntityName(); + if (isBackquoted) { + parseExpectedTokenJSDoc(ts.SyntaxKind.BacktickToken); + } + if (isBracketed) { + skipWhitespace(); + // May have an optional default, e.g. '[foo = 42]' + if (parseOptionalToken(ts.SyntaxKind.EqualsToken)) { + parseExpression(); } - return { name, isBracketed }; + parseExpected(ts.SyntaxKind.CloseBracketToken); } - function isObjectOrObjectArrayTypeReference(node: ts.TypeNode): boolean { - switch (node.kind) { - case ts.SyntaxKind.ObjectKeyword: - return true; - case ts.SyntaxKind.ArrayType: - return isObjectOrObjectArrayTypeReference((node as ts.ArrayTypeNode).elementType); - default: - return ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "Object" && !node.typeArguments; - } + return { name, isBracketed }; + } + + function isObjectOrObjectArrayTypeReference(node: ts.TypeNode): boolean { + switch (node.kind) { + case ts.SyntaxKind.ObjectKeyword: + return true; + case ts.SyntaxKind.ArrayType: + return isObjectOrObjectArrayTypeReference((node as ts.ArrayTypeNode).elementType); + default: + return ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "Object" && !node.typeArguments; } + } - function parseParameterOrPropertyTag(start: number, tagName: ts.Identifier, target: PropertyLikeParse, indent: number): ts.JSDocParameterTag | ts.JSDocPropertyTag { - let typeExpression = tryParseTypeExpression(); - let isNameFirst = !typeExpression; - skipWhitespaceOrAsterisk(); + function parseParameterOrPropertyTag(start: number, tagName: ts.Identifier, target: PropertyLikeParse, indent: number): ts.JSDocParameterTag | ts.JSDocPropertyTag { + let typeExpression = tryParseTypeExpression(); + let isNameFirst = !typeExpression; + skipWhitespaceOrAsterisk(); - const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); - const indentText = skipWhitespaceOrAsterisk(); + const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); + const indentText = skipWhitespaceOrAsterisk(); - if (isNameFirst && !lookAhead(parseJSDocLinkPrefix)) { - typeExpression = tryParseTypeExpression(); - } + if (isNameFirst && !lookAhead(parseJSDocLinkPrefix)) { + typeExpression = tryParseTypeExpression(); + } - const comment = parseTrailingTagComments(start, getNodePos(), indent, indentText); + const comment = parseTrailingTagComments(start, getNodePos(), indent, indentText); - const nestedTypeLiteral = target !== PropertyLikeParse.CallbackParameter && parseNestedTypeLiteral(typeExpression, name, target, indent); - if (nestedTypeLiteral) { - typeExpression = nestedTypeLiteral; - isNameFirst = true; - } - const result = target === PropertyLikeParse.Property - ? factory.createJSDocPropertyTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment) - : factory.createJSDocParameterTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment); - return finishNode(result, start); - } - - function parseNestedTypeLiteral(typeExpression: ts.JSDocTypeExpression | undefined, name: ts.EntityName, target: PropertyLikeParse, indent: number) { - if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { - const pos = getNodePos(); - let child: ts.JSDocPropertyLikeTag | ts.JSDocTypeTag | false; - let children: ts.JSDocPropertyLikeTag[] | undefined; - while (child = tryParse(() => parseChildParameterOrPropertyTag(target, indent, name))) { - if (child.kind === ts.SyntaxKind.JSDocParameterTag || child.kind === ts.SyntaxKind.JSDocPropertyTag) { - children = ts.append(children, child); - } - } - if (children) { - const literal = finishNode(factory.createJSDocTypeLiteral(children, typeExpression.type.kind === ts.SyntaxKind.ArrayType), pos); - return finishNode(factory.createJSDocTypeExpression(literal), pos); - } - } + const nestedTypeLiteral = target !== PropertyLikeParse.CallbackParameter && parseNestedTypeLiteral(typeExpression, name, target, indent); + if (nestedTypeLiteral) { + typeExpression = nestedTypeLiteral; + isNameFirst = true; } + const result = target === PropertyLikeParse.Property + ? factory.createJSDocPropertyTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment) + : factory.createJSDocParameterTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment); + return finishNode(result, start); + } - function parseReturnTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocReturnTag { - if (ts.some(tags, ts.isJSDocReturnTag)) { - parseErrorAt(tagName.pos, scanner.getTokenPos(), ts.Diagnostics._0_tag_already_specified, tagName.escapedText); + function parseNestedTypeLiteral(typeExpression: ts.JSDocTypeExpression | undefined, name: ts.EntityName, target: PropertyLikeParse, indent: number) { + if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { + const pos = getNodePos(); + let child: ts.JSDocPropertyLikeTag | ts.JSDocTypeTag | false; + let children: ts.JSDocPropertyLikeTag[] | undefined; + while (child = tryParse(() => parseChildParameterOrPropertyTag(target, indent, name))) { + if (child.kind === ts.SyntaxKind.JSDocParameterTag || child.kind === ts.SyntaxKind.JSDocPropertyTag) { + children = ts.append(children, child); + } + } + if (children) { + const literal = finishNode(factory.createJSDocTypeLiteral(children, typeExpression.type.kind === ts.SyntaxKind.ArrayType), pos); + return finishNode(factory.createJSDocTypeExpression(literal), pos); } + } + } - const typeExpression = tryParseTypeExpression(); - return finishNode(factory.createJSDocReturnTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + function parseReturnTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocReturnTag { + if (ts.some(tags, ts.isJSDocReturnTag)) { + parseErrorAt(tagName.pos, scanner.getTokenPos(), ts.Diagnostics._0_tag_already_specified, tagName.escapedText); } - function parseTypeTag(start: number, tagName: ts.Identifier, indent?: number, indentText?: string): ts.JSDocTypeTag { - if (ts.some(tags, ts.isJSDocTypeTag)) { - parseErrorAt(tagName.pos, scanner.getTokenPos(), ts.Diagnostics._0_tag_already_specified, tagName.escapedText); - } + const typeExpression = tryParseTypeExpression(); + return finishNode(factory.createJSDocReturnTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + } - const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - const comments = indent !== undefined && indentText !== undefined ? parseTrailingTagComments(start, getNodePos(), indent, indentText) : undefined; - return finishNode(factory.createJSDocTypeTag(tagName, typeExpression, comments), start); + function parseTypeTag(start: number, tagName: ts.Identifier, indent?: number, indentText?: string): ts.JSDocTypeTag { + if (ts.some(tags, ts.isJSDocTypeTag)) { + parseErrorAt(tagName.pos, scanner.getTokenPos(), ts.Diagnostics._0_tag_already_specified, tagName.escapedText); } - function parseSeeTag(start: number, tagName: ts.Identifier, indent?: number, indentText?: string): ts.JSDocSeeTag { - const isMarkdownOrJSDocLink = token() === ts.SyntaxKind.OpenBracketToken - || lookAhead(() => nextTokenJSDoc() === ts.SyntaxKind.AtToken && ts.tokenIsIdentifierOrKeyword(nextTokenJSDoc()) && isJSDocLinkTag(scanner.getTokenValue())); - const nameExpression = isMarkdownOrJSDocLink ? undefined : parseJSDocNameReference(); - const comments = indent !== undefined && indentText !== undefined ? parseTrailingTagComments(start, getNodePos(), indent, indentText) : undefined; - return finishNode(factory.createJSDocSeeTag(tagName, nameExpression, comments), start); + const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + const comments = indent !== undefined && indentText !== undefined ? parseTrailingTagComments(start, getNodePos(), indent, indentText) : undefined; + return finishNode(factory.createJSDocTypeTag(tagName, typeExpression, comments), start); + } + + function parseSeeTag(start: number, tagName: ts.Identifier, indent?: number, indentText?: string): ts.JSDocSeeTag { + const isMarkdownOrJSDocLink = token() === ts.SyntaxKind.OpenBracketToken + || lookAhead(() => nextTokenJSDoc() === ts.SyntaxKind.AtToken && ts.tokenIsIdentifierOrKeyword(nextTokenJSDoc()) && isJSDocLinkTag(scanner.getTokenValue())); + const nameExpression = isMarkdownOrJSDocLink ? undefined : parseJSDocNameReference(); + const comments = indent !== undefined && indentText !== undefined ? parseTrailingTagComments(start, getNodePos(), indent, indentText) : undefined; + return finishNode(factory.createJSDocSeeTag(tagName, nameExpression, comments), start); + } + + function parseAuthorTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocAuthorTag { + const commentStart = getNodePos(); + const textOnly = parseAuthorNameAndEmail(); + let commentEnd = scanner.getStartPos(); + const comments = parseTrailingTagComments(start, commentEnd, indent, indentText); + if (!comments) { + commentEnd = scanner.getStartPos(); } + const allParts = typeof comments !== "string" + ? createNodeArray(ts.concatenate([finishNode(textOnly, commentStart, commentEnd)], comments) as ts.JSDocComment[], commentStart) // cast away readonly + : textOnly.text + comments; + return finishNode(factory.createJSDocAuthorTag(tagName, allParts), start); + } - function parseAuthorTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocAuthorTag { - const commentStart = getNodePos(); - const textOnly = parseAuthorNameAndEmail(); - let commentEnd = scanner.getStartPos(); - const comments = parseTrailingTagComments(start, commentEnd, indent, indentText); - if (!comments) { - commentEnd = scanner.getStartPos(); + function parseAuthorNameAndEmail(): ts.JSDocText { + const comments: string[] = []; + let inEmail = false; + let token = scanner.getToken(); + while (token !== ts.SyntaxKind.EndOfFileToken && token !== ts.SyntaxKind.NewLineTrivia) { + if (token === ts.SyntaxKind.LessThanToken) { + inEmail = true; } - const allParts = typeof comments !== "string" - ? createNodeArray(ts.concatenate([finishNode(textOnly, commentStart, commentEnd)], comments) as ts.JSDocComment[], commentStart) // cast away readonly - : textOnly.text + comments; - return finishNode(factory.createJSDocAuthorTag(tagName, allParts), start); - } - - function parseAuthorNameAndEmail(): ts.JSDocText { - const comments: string[] = []; - let inEmail = false; - let token = scanner.getToken(); - while (token !== ts.SyntaxKind.EndOfFileToken && token !== ts.SyntaxKind.NewLineTrivia) { - if (token === ts.SyntaxKind.LessThanToken) { - inEmail = true; - } - else if (token === ts.SyntaxKind.AtToken && !inEmail) { - break; - } - else if (token === ts.SyntaxKind.GreaterThanToken && inEmail) { - comments.push(scanner.getTokenText()); - scanner.setTextPos(scanner.getTokenPos() + 1); - break; - } + else if (token === ts.SyntaxKind.AtToken && !inEmail) { + break; + } + else if (token === ts.SyntaxKind.GreaterThanToken && inEmail) { comments.push(scanner.getTokenText()); - token = nextTokenJSDoc(); + scanner.setTextPos(scanner.getTokenPos() + 1); + break; } - - return factory.createJSDocText(comments.join("")); + comments.push(scanner.getTokenText()); + token = nextTokenJSDoc(); } - function parseImplementsTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocImplementsTag { - const className = parseExpressionWithTypeArgumentsForAugments(); - return finishNode(factory.createJSDocImplementsTag(tagName, className, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); - } + return factory.createJSDocText(comments.join("")); + } - function parseAugmentsTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocAugmentsTag { - const className = parseExpressionWithTypeArgumentsForAugments(); - return finishNode(factory.createJSDocAugmentsTag(tagName, className, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); - } + function parseImplementsTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocImplementsTag { + const className = parseExpressionWithTypeArgumentsForAugments(); + return finishNode(factory.createJSDocImplementsTag(tagName, className, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); + } + + function parseAugmentsTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocAugmentsTag { + const className = parseExpressionWithTypeArgumentsForAugments(); + return finishNode(factory.createJSDocAugmentsTag(tagName, className, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); + } - function parseExpressionWithTypeArgumentsForAugments(): ts.ExpressionWithTypeArguments & { + function parseExpressionWithTypeArgumentsForAugments(): ts.ExpressionWithTypeArguments & { + expression: ts.Identifier | ts.PropertyAccessEntityNameExpression; + } { + const usedBrace = parseOptional(ts.SyntaxKind.OpenBraceToken); + const pos = getNodePos(); + const expression = parsePropertyAccessEntityNameExpression(); + const typeArguments = tryParseTypeArguments(); + const node = factory.createExpressionWithTypeArguments(expression, typeArguments) as ts.ExpressionWithTypeArguments & { expression: ts.Identifier | ts.PropertyAccessEntityNameExpression; - } { - const usedBrace = parseOptional(ts.SyntaxKind.OpenBraceToken); - const pos = getNodePos(); - const expression = parsePropertyAccessEntityNameExpression(); - const typeArguments = tryParseTypeArguments(); - const node = factory.createExpressionWithTypeArguments(expression, typeArguments) as ts.ExpressionWithTypeArguments & { - expression: ts.Identifier | ts.PropertyAccessEntityNameExpression; - }; - const res = finishNode(node, pos); - if (usedBrace) { - parseExpected(ts.SyntaxKind.CloseBraceToken); - } - return res; + }; + const res = finishNode(node, pos); + if (usedBrace) { + parseExpected(ts.SyntaxKind.CloseBraceToken); } + return res; + } - function parsePropertyAccessEntityNameExpression() { - const pos = getNodePos(); - let node: ts.Identifier | ts.PropertyAccessEntityNameExpression = parseJSDocIdentifierName(); - while (parseOptional(ts.SyntaxKind.DotToken)) { - const name = parseJSDocIdentifierName(); - node = finishNode(factory.createPropertyAccessExpression(node, name), pos) as ts.PropertyAccessEntityNameExpression; - } - return node; + function parsePropertyAccessEntityNameExpression() { + const pos = getNodePos(); + let node: ts.Identifier | ts.PropertyAccessEntityNameExpression = parseJSDocIdentifierName(); + while (parseOptional(ts.SyntaxKind.DotToken)) { + const name = parseJSDocIdentifierName(); + node = finishNode(factory.createPropertyAccessExpression(node, name), pos) as ts.PropertyAccessEntityNameExpression; } + return node; + } - function parseSimpleTag(start: number, createTag: (tagName: ts.Identifier | undefined, comment?: string | ts.NodeArray) => ts.JSDocTag, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocTag { - return finishNode(createTag(tagName, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); - } + function parseSimpleTag(start: number, createTag: (tagName: ts.Identifier | undefined, comment?: string | ts.NodeArray) => ts.JSDocTag, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocTag { + return finishNode(createTag(tagName, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); + } - function parseThisTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocThisTag { - const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - skipWhitespace(); - return finishNode(factory.createJSDocThisTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); - } + function parseThisTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocThisTag { + const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + skipWhitespace(); + return finishNode(factory.createJSDocThisTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); + } - function parseEnumTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocEnumTag { - const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - skipWhitespace(); - return finishNode(factory.createJSDocEnumTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); - } + function parseEnumTag(start: number, tagName: ts.Identifier, margin: number, indentText: string): ts.JSDocEnumTag { + const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + skipWhitespace(); + return finishNode(factory.createJSDocEnumTag(tagName, typeExpression, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start); + } - function parseTypedefTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocTypedefTag { - let typeExpression: ts.JSDocTypeExpression | ts.JSDocTypeLiteral | undefined = tryParseTypeExpression(); - skipWhitespaceOrAsterisk(); + function parseTypedefTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocTypedefTag { + let typeExpression: ts.JSDocTypeExpression | ts.JSDocTypeLiteral | undefined = tryParseTypeExpression(); + skipWhitespaceOrAsterisk(); - const fullName = parseJSDocTypeNameWithNamespace(); - skipWhitespace(); - let comment = parseTagComments(indent); - - let end: number | undefined; - if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { - let child: ts.JSDocTypeTag | ts.JSDocPropertyTag | false; - let childTypeTag: ts.JSDocTypeTag | undefined; - let jsDocPropertyTags: ts.JSDocPropertyTag[] | undefined; - let hasChildren = false; - while (child = tryParse(() => parseChildPropertyTag(indent))) { - hasChildren = true; - if (child.kind === ts.SyntaxKind.JSDocTypeTag) { - if (childTypeTag) { - const lastError = parseErrorAtCurrentToken(ts.Diagnostics.A_JSDoc_typedef_comment_may_not_contain_multiple_type_tags); - if (lastError) { - ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, 0, 0, ts.Diagnostics.The_tag_was_first_specified_here)); - } - break; - } - else { - childTypeTag = child; + const fullName = parseJSDocTypeNameWithNamespace(); + skipWhitespace(); + let comment = parseTagComments(indent); + + let end: number | undefined; + if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: ts.JSDocTypeTag | ts.JSDocPropertyTag | false; + let childTypeTag: ts.JSDocTypeTag | undefined; + let jsDocPropertyTags: ts.JSDocPropertyTag[] | undefined; + let hasChildren = false; + while (child = tryParse(() => parseChildPropertyTag(indent))) { + hasChildren = true; + if (child.kind === ts.SyntaxKind.JSDocTypeTag) { + if (childTypeTag) { + const lastError = parseErrorAtCurrentToken(ts.Diagnostics.A_JSDoc_typedef_comment_may_not_contain_multiple_type_tags); + if (lastError) { + ts.addRelatedInfo(lastError, ts.createDetachedDiagnostic(fileName, 0, 0, ts.Diagnostics.The_tag_was_first_specified_here)); } + break; } else { - jsDocPropertyTags = ts.append(jsDocPropertyTags, child); + childTypeTag = child; } } - if (hasChildren) { - const isArrayType = typeExpression && typeExpression.type.kind === ts.SyntaxKind.ArrayType; - const jsdocTypeLiteral = factory.createJSDocTypeLiteral(jsDocPropertyTags, isArrayType); - typeExpression = childTypeTag && childTypeTag.typeExpression && !isObjectOrObjectArrayTypeReference(childTypeTag.typeExpression.type) ? - childTypeTag.typeExpression : - finishNode(jsdocTypeLiteral, start); - end = typeExpression.end; + else { + jsDocPropertyTags = ts.append(jsDocPropertyTags, 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 - end = end || comment !== undefined ? - getNodePos() : - (fullName ?? typeExpression ?? tagName).end; - - if (!comment) { - comment = parseTrailingTagComments(start, end, indent, indentText); + if (hasChildren) { + const isArrayType = typeExpression && typeExpression.type.kind === ts.SyntaxKind.ArrayType; + const jsdocTypeLiteral = factory.createJSDocTypeLiteral(jsDocPropertyTags, isArrayType); + typeExpression = childTypeTag && childTypeTag.typeExpression && !isObjectOrObjectArrayTypeReference(childTypeTag.typeExpression.type) ? + childTypeTag.typeExpression : + finishNode(jsdocTypeLiteral, start); + end = typeExpression.end; } - - const typedefTag = factory.createJSDocTypedefTag(tagName, typeExpression, fullName, comment); - return finishNode(typedefTag, start, end); } - function parseJSDocTypeNameWithNamespace(nested?: boolean) { - const pos = scanner.getTokenPos(); - if (!ts.tokenIsIdentifierOrKeyword(token())) { - return undefined; - } - const typeNameOrNamespaceName = parseJSDocIdentifierName(); - if (parseOptional(ts.SyntaxKind.DotToken)) { - const body = parseJSDocTypeNameWithNamespace(/*nested*/ true); - const jsDocNamespaceNode = factory.createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, typeNameOrNamespaceName, body, nested ? ts.NodeFlags.NestedNamespace : undefined) as ts.JSDocNamespaceDeclaration; - return finishNode(jsDocNamespaceNode, pos); - } + // Only include the characters between the name end and the next token if a comment was actually parsed out - otherwise it's just whitespace + end = end || comment !== undefined ? + getNodePos() : + (fullName ?? typeExpression ?? tagName).end; - if (nested) { - typeNameOrNamespaceName.isInJSDocNamespace = true; - } - return typeNameOrNamespaceName; + if (!comment) { + comment = parseTrailingTagComments(start, end, indent, indentText); } + const typedefTag = factory.createJSDocTypedefTag(tagName, typeExpression, fullName, comment); + return finishNode(typedefTag, start, end); + } - function parseCallbackTagParameters(indent: number) { - const pos = getNodePos(); - let child: ts.JSDocParameterTag | false; - let parameters; - while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.CallbackParameter, indent) as ts.JSDocParameterTag)) { - parameters = ts.append(parameters, child); - } - return createNodeArray(parameters || [], pos); + function parseJSDocTypeNameWithNamespace(nested?: boolean) { + const pos = scanner.getTokenPos(); + if (!ts.tokenIsIdentifierOrKeyword(token())) { + return undefined; + } + const typeNameOrNamespaceName = parseJSDocIdentifierName(); + if (parseOptional(ts.SyntaxKind.DotToken)) { + const body = parseJSDocTypeNameWithNamespace(/*nested*/ true); + const jsDocNamespaceNode = factory.createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, typeNameOrNamespaceName, body, nested ? ts.NodeFlags.NestedNamespace : undefined) as ts.JSDocNamespaceDeclaration; + return finishNode(jsDocNamespaceNode, pos); } - function parseCallbackTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocCallbackTag { - const fullName = parseJSDocTypeNameWithNamespace(); - skipWhitespace(); - let comment = parseTagComments(indent); - const parameters = parseCallbackTagParameters(indent); - const returnTag = tryParse(() => { - if (parseOptionalJsdoc(ts.SyntaxKind.AtToken)) { - const tag = parseTag(indent); - if (tag && tag.kind === ts.SyntaxKind.JSDocReturnTag) { - return tag as ts.JSDocReturnTag; - } + if (nested) { + typeNameOrNamespaceName.isInJSDocNamespace = true; + } + return typeNameOrNamespaceName; + } + + + function parseCallbackTagParameters(indent: number) { + const pos = getNodePos(); + let child: ts.JSDocParameterTag | false; + let parameters; + while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.CallbackParameter, indent) as ts.JSDocParameterTag)) { + parameters = ts.append(parameters, child); + } + return createNodeArray(parameters || [], pos); + } + + function parseCallbackTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocCallbackTag { + const fullName = parseJSDocTypeNameWithNamespace(); + skipWhitespace(); + let comment = parseTagComments(indent); + const parameters = parseCallbackTagParameters(indent); + const returnTag = tryParse(() => { + if (parseOptionalJsdoc(ts.SyntaxKind.AtToken)) { + const tag = parseTag(indent); + if (tag && tag.kind === ts.SyntaxKind.JSDocReturnTag) { + return tag as ts.JSDocReturnTag; } - }); - const typeExpression = finishNode(factory.createJSDocSignature(/*typeParameters*/ undefined, parameters, returnTag), start); - if (!comment) { - comment = parseTrailingTagComments(start, getNodePos(), indent, indentText); } - const end = comment !== undefined ? getNodePos() : typeExpression.end; - return finishNode(factory.createJSDocCallbackTag(tagName, typeExpression, fullName, comment), start, end); + }); + const typeExpression = finishNode(factory.createJSDocSignature(/*typeParameters*/ undefined, parameters, returnTag), start); + if (!comment) { + comment = parseTrailingTagComments(start, getNodePos(), indent, indentText); } + const end = comment !== undefined ? getNodePos() : typeExpression.end; + return finishNode(factory.createJSDocCallbackTag(tagName, typeExpression, fullName, comment), start, end); + } - function escapedTextsEqual(a: ts.EntityName, b: ts.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: ts.EntityName, b: ts.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; } - return a.escapedText === b.escapedText; - } - - function parseChildPropertyTag(indent: number) { - return parseChildParameterOrPropertyTag(PropertyLikeParse.Property, indent) as ts.JSDocTypeTag | ts.JSDocPropertyTag | false; - } - - function parseChildParameterOrPropertyTag(target: PropertyLikeParse, indent: number, name?: ts.EntityName): ts.JSDocTypeTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | false { - let canParseTag = true; - let seenAsterisk = false; - while (true) { - switch (nextTokenJSDoc()) { - case ts.SyntaxKind.AtToken: - if (canParseTag) { - const child = tryParseChildTag(target, indent); - if (child && (child.kind === ts.SyntaxKind.JSDocParameterTag || child.kind === ts.SyntaxKind.JSDocPropertyTag) && - target !== PropertyLikeParse.CallbackParameter && - name && (ts.isIdentifier(child.name) || !escapedTextsEqual(name, child.name.left))) { - return false; - } - return child; - } - seenAsterisk = false; - break; - case ts.SyntaxKind.NewLineTrivia: - canParseTag = true; - seenAsterisk = false; - break; - case ts.SyntaxKind.AsteriskToken: - if (seenAsterisk) { - canParseTag = false; - } - seenAsterisk = true; - break; - case ts.SyntaxKind.Identifier: - canParseTag = false; - break; - case ts.SyntaxKind.EndOfFileToken: - return false; - } + else { + return false; } } + return a.escapedText === b.escapedText; + } - function tryParseChildTag(target: PropertyLikeParse, indent: number): ts.JSDocTypeTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | false { - ts.Debug.assert(token() === ts.SyntaxKind.AtToken); - const start = scanner.getStartPos(); - nextTokenJSDoc(); + function parseChildPropertyTag(indent: number) { + return parseChildParameterOrPropertyTag(PropertyLikeParse.Property, indent) as ts.JSDocTypeTag | ts.JSDocPropertyTag | false; + } - 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; + function parseChildParameterOrPropertyTag(target: PropertyLikeParse, indent: number, name?: ts.EntityName): ts.JSDocTypeTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | false { + let canParseTag = true; + let seenAsterisk = false; + while (true) { + switch (nextTokenJSDoc()) { + case ts.SyntaxKind.AtToken: + if (canParseTag) { + const child = tryParseChildTag(target, indent); + if (child && (child.kind === ts.SyntaxKind.JSDocParameterTag || child.kind === ts.SyntaxKind.JSDocPropertyTag) && + target !== PropertyLikeParse.CallbackParameter && + name && (ts.isIdentifier(child.name) || !escapedTextsEqual(name, child.name.left))) { + return false; + } + return child; + } + seenAsterisk = false; break; - case "arg": - case "argument": - case "param": - t = PropertyLikeParse.Parameter | PropertyLikeParse.CallbackParameter; + case ts.SyntaxKind.NewLineTrivia: + canParseTag = true; + seenAsterisk = false; break; - default: + case ts.SyntaxKind.AsteriskToken: + if (seenAsterisk) { + canParseTag = false; + } + seenAsterisk = true; + break; + case ts.SyntaxKind.Identifier: + canParseTag = false; + break; + case ts.SyntaxKind.EndOfFileToken: return false; } - if (!(target & t)) { - return false; - } - return parseParameterOrPropertyTag(start, tagName, target, indent); } + } - function parseTemplateTagTypeParameter() { - const typeParameterPos = getNodePos(); - const isBracketed = parseOptionalJsdoc(ts.SyntaxKind.OpenBracketToken); - if (isBracketed) { - skipWhitespace(); - } - const name = parseJSDocIdentifierName(ts.Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces); - let defaultType: ts.TypeNode | undefined; - if (isBracketed) { - skipWhitespace(); - parseExpected(ts.SyntaxKind.EqualsToken); - defaultType = doInsideOfContext(ts.NodeFlags.JSDoc, parseJSDocType); - parseExpected(ts.SyntaxKind.CloseBracketToken); - } + function tryParseChildTag(target: PropertyLikeParse, indent: number): ts.JSDocTypeTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | false { + ts.Debug.assert(token() === ts.SyntaxKind.AtToken); + const start = scanner.getStartPos(); + nextTokenJSDoc(); - if (ts.nodeIsMissing(name)) { - return undefined; - } - return finishNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, /*constraint*/ undefined, defaultType), typeParameterPos); + 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; + } + if (!(target & t)) { + return false; } + return parseParameterOrPropertyTag(start, tagName, target, indent); + } - function parseTemplateTagTypeParameters() { - const pos = getNodePos(); - const typeParameters = []; - do { - skipWhitespace(); - const node = parseTemplateTagTypeParameter(); - if (node !== undefined) { - typeParameters.push(node); - } - skipWhitespaceOrAsterisk(); - } while (parseOptionalJsdoc(ts.SyntaxKind.CommaToken)); - return createNodeArray(typeParameters, pos); + function parseTemplateTagTypeParameter() { + const typeParameterPos = getNodePos(); + const isBracketed = parseOptionalJsdoc(ts.SyntaxKind.OpenBracketToken); + if (isBracketed) { + skipWhitespace(); + } + const name = parseJSDocIdentifierName(ts.Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces); + let defaultType: ts.TypeNode | undefined; + if (isBracketed) { + skipWhitespace(); + parseExpected(ts.SyntaxKind.EqualsToken); + defaultType = doInsideOfContext(ts.NodeFlags.JSDoc, parseJSDocType); + parseExpected(ts.SyntaxKind.CloseBracketToken); } - function parseTemplateTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocTemplateTag { - // The template tag looks like one of the following: - // @template T,U,V - // @template {Constraint} T - // - // According to the [closure docs](https://github.com/google/closure-compiler/wiki/Generic-Types#multiple-bounded-template-types): - // > Multiple bounded generics cannot be declared on the same line. For the sake of clarity, if multiple templates share the same - // > type bound they must be declared on separate lines. - // - // TODO: Determine whether we should enforce this in the checker. - // TODO: Consider moving the `constraint` to the first type parameter as we could then remove `getEffectiveConstraintOfTypeParameter`. - // TODO: Consider only parsing a single type parameter if there is a constraint. - const constraint = token() === ts.SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; - const typeParameters = parseTemplateTagTypeParameters(); - return finishNode(factory.createJSDocTemplateTag(tagName, constraint, typeParameters, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + if (ts.nodeIsMissing(name)) { + return undefined; } + return finishNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, name, /*constraint*/ undefined, defaultType), typeParameterPos); + } - function parseOptionalJsdoc(t: ts.JSDocSyntaxKind): boolean { - if (token() === t) { - nextTokenJSDoc(); - return true; + function parseTemplateTagTypeParameters() { + const pos = getNodePos(); + const typeParameters = []; + do { + skipWhitespace(); + const node = parseTemplateTagTypeParameter(); + if (node !== undefined) { + typeParameters.push(node); } - return false; + skipWhitespaceOrAsterisk(); + } while (parseOptionalJsdoc(ts.SyntaxKind.CommaToken)); + return createNodeArray(typeParameters, pos); + } + + function parseTemplateTag(start: number, tagName: ts.Identifier, indent: number, indentText: string): ts.JSDocTemplateTag { + // The template tag looks like one of the following: + // @template T,U,V + // @template {Constraint} T + // + // According to the [closure docs](https://github.com/google/closure-compiler/wiki/Generic-Types#multiple-bounded-template-types): + // > Multiple bounded generics cannot be declared on the same line. For the sake of clarity, if multiple templates share the same + // > type bound they must be declared on separate lines. + // + // TODO: Determine whether we should enforce this in the checker. + // TODO: Consider moving the `constraint` to the first type parameter as we could then remove `getEffectiveConstraintOfTypeParameter`. + // TODO: Consider only parsing a single type parameter if there is a constraint. + const constraint = token() === ts.SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; + const typeParameters = parseTemplateTagTypeParameters(); + return finishNode(factory.createJSDocTemplateTag(tagName, constraint, typeParameters, parseTrailingTagComments(start, getNodePos(), indent, indentText)), start); + } + + function parseOptionalJsdoc(t: ts.JSDocSyntaxKind): boolean { + if (token() === t) { + nextTokenJSDoc(); + return true; } + return false; + } - function parseJSDocEntityName(): ts.EntityName { - let entity: ts.EntityName = parseJSDocIdentifierName(); + function parseJSDocEntityName(): ts.EntityName { + let entity: ts.EntityName = parseJSDocIdentifierName(); + if (parseOptional(ts.SyntaxKind.OpenBracketToken)) { + parseExpected(ts.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(ts.SyntaxKind.DotToken)) { + const name = parseJSDocIdentifierName(); if (parseOptional(ts.SyntaxKind.OpenBracketToken)) { parseExpected(ts.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(ts.SyntaxKind.DotToken)) { - const name = parseJSDocIdentifierName(); - if (parseOptional(ts.SyntaxKind.OpenBracketToken)) { - parseExpected(ts.SyntaxKind.CloseBracketToken); - } - entity = createQualifiedName(entity, name); } - return entity; + entity = createQualifiedName(entity, name); } + return entity; + } - function parseJSDocIdentifierName(message?: ts.DiagnosticMessage): ts.Identifier { - if (!ts.tokenIsIdentifierOrKeyword(token())) { - return createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ !message, message || ts.Diagnostics.Identifier_expected); - } - - identifierCount++; - const pos = scanner.getTokenPos(); - const end = scanner.getTextPos(); - const originalKeywordKind = token(); - const text = internIdentifier(scanner.getTokenValue()); - const result = finishNode(factory.createIdentifier(text, /*typeArguments*/ undefined, originalKeywordKind), pos, end); - nextTokenJSDoc(); - return result; + function parseJSDocIdentifierName(message?: ts.DiagnosticMessage): ts.Identifier { + if (!ts.tokenIsIdentifierOrKeyword(token())) { + return createMissingNode(ts.SyntaxKind.Identifier, /*reportAtCurrentPosition*/ !message, message || ts.Diagnostics.Identifier_expected); } + + identifierCount++; + const pos = scanner.getTokenPos(); + const end = scanner.getTextPos(); + const originalKeywordKind = token(); + const text = internIdentifier(scanner.getTokenValue()); + const result = finishNode(factory.createIdentifier(text, /*typeArguments*/ undefined, originalKeywordKind), pos, end); + nextTokenJSDoc(); + return result; } } } +} + +namespace IncrementalParser { + export function updateSourceFile(sourceFile: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks: boolean): ts.SourceFile { + aggressiveChecks = aggressiveChecks || ts.Debug.shouldAssert(ts.AssertionLevel.Aggressive); + + checkChangeRange(sourceFile, newText, textChangeRange, aggressiveChecks); + if (ts.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, sourceFile.setExternalModuleIndicator); + } + + // 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 as ts.Node as IncrementalNode; + ts.Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); + incrementalSourceFile.hasBeenIncrementallyParsed = true; + Parser.fixupParentReferences(incrementalSourceFile); + 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. + ts.Debug.assert(changeRange.span.start <= textChangeRange.span.start); + ts.Debug.assert(ts.textSpanEnd(changeRange.span) === ts.textSpanEnd(textChangeRange.span)); + ts.Debug.assert(ts.textSpanEnd(ts.textChangeRangeNewSpan(changeRange)) === ts.textSpanEnd(ts.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 = ts.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, ts.textSpanEnd(changeRange.span), ts.textSpanEnd(ts.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, sourceFile.setExternalModuleIndicator); + result.commentDirectives = getNewCommentDirectives(sourceFile.commentDirectives, result.commentDirectives, changeRange.span.start, ts.textSpanEnd(changeRange.span), delta, oldText, newText, aggressiveChecks); + result.impliedNodeFormat = sourceFile.impliedNodeFormat; + return result; + } - namespace IncrementalParser { - export function updateSourceFile(sourceFile: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks: boolean): ts.SourceFile { - aggressiveChecks = aggressiveChecks || ts.Debug.shouldAssert(ts.AssertionLevel.Aggressive); + function getNewCommentDirectives(oldDirectives: ts.CommentDirective[] | undefined, newDirectives: ts.CommentDirective[] | undefined, changeStart: number, changeRangeOldEnd: number, delta: number, oldText: string, newText: string, aggressiveChecks: boolean): ts.CommentDirective[] | undefined { + if (!oldDirectives) + return newDirectives; + let commentDirectives: ts.CommentDirective[] | undefined; + let addedNewlyScannedDirectives = false; + for (const directive of oldDirectives) { + const { range, type } = directive; + // Range before the change + if (range.end < changeStart) { + commentDirectives = ts.append(commentDirectives, directive); + } + else if (range.pos > changeRangeOldEnd) { + addNewlyScannedDirectives(); + // Node is entirely past the change range. We need to move both its pos and + // end, forward or backward appropriately. + const updatedDirective: ts.CommentDirective = { + range: { pos: range.pos + delta, end: range.end + delta }, + type + }; + commentDirectives = ts.append(commentDirectives, updatedDirective); + if (aggressiveChecks) { + ts.Debug.assert(oldText.substring(range.pos, range.end) === newText.substring(updatedDirective.range.pos, updatedDirective.range.end)); + } + } + // Ignore ranges that fall in change range + } + addNewlyScannedDirectives(); + return commentDirectives; - checkChangeRange(sourceFile, newText, textChangeRange, aggressiveChecks); - if (ts.textChangeRangeIsUnchanged(textChangeRange)) { - // if the text didn't change, then we can just return our current source file as-is. - return sourceFile; + function addNewlyScannedDirectives() { + if (addedNewlyScannedDirectives) + return; + addedNewlyScannedDirectives = true; + if (!commentDirectives) { + commentDirectives = newDirectives; + } + else if (newDirectives) { + commentDirectives.push(...newDirectives); } + } + } + + function moveElementEntirelyPastChangeRange(element: IncrementalElement, isArray: boolean, delta: number, oldText: string, newText: string, aggressiveChecks: boolean) { + if (isArray) { + visitArray(element as IncrementalNodeArray); + } + else { + visitNode(element as IncrementalNode); + } + return; - 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, sourceFile.setExternalModuleIndicator); + function visitNode(node: IncrementalNode) { + let text = ""; + if (aggressiveChecks && shouldCheckNode(node)) { + text = oldText.substring(node.pos, node.end); } - // 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 as ts.Node as IncrementalNode; - ts.Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); - incrementalSourceFile.hasBeenIncrementallyParsed = true; - Parser.fixupParentReferences(incrementalSourceFile); - 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. - ts.Debug.assert(changeRange.span.start <= textChangeRange.span.start); - ts.Debug.assert(ts.textSpanEnd(changeRange.span) === ts.textSpanEnd(textChangeRange.span)); - ts.Debug.assert(ts.textSpanEnd(ts.textChangeRangeNewSpan(changeRange)) === ts.textSpanEnd(ts.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 = ts.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, ts.textSpanEnd(changeRange.span), ts.textSpanEnd(ts.textChangeRangeNewSpan(changeRange)), delta, oldText, newText, aggressiveChecks); + // Ditch any existing LS children we may have created. This way we can avoid + // moving them forward. + if (node._children) { + node._children = undefined; + } - // 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, sourceFile.setExternalModuleIndicator); - result.commentDirectives = getNewCommentDirectives(sourceFile.commentDirectives, result.commentDirectives, changeRange.span.start, ts.textSpanEnd(changeRange.span), delta, oldText, newText, aggressiveChecks); - result.impliedNodeFormat = sourceFile.impliedNodeFormat; - return result; - } + ts.setTextRangePosEnd(node, node.pos + delta, node.end + delta); - function getNewCommentDirectives(oldDirectives: ts.CommentDirective[] | undefined, newDirectives: ts.CommentDirective[] | undefined, changeStart: number, changeRangeOldEnd: number, delta: number, oldText: string, newText: string, aggressiveChecks: boolean): ts.CommentDirective[] | undefined { - if (!oldDirectives) - return newDirectives; - let commentDirectives: ts.CommentDirective[] | undefined; - let addedNewlyScannedDirectives = false; - for (const directive of oldDirectives) { - const { range, type } = directive; - // Range before the change - if (range.end < changeStart) { - commentDirectives = ts.append(commentDirectives, directive); - } - else if (range.pos > changeRangeOldEnd) { - addNewlyScannedDirectives(); - // Node is entirely past the change range. We need to move both its pos and - // end, forward or backward appropriately. - const updatedDirective: ts.CommentDirective = { - range: { pos: range.pos + delta, end: range.end + delta }, - type - }; - commentDirectives = ts.append(commentDirectives, updatedDirective); - if (aggressiveChecks) { - ts.Debug.assert(oldText.substring(range.pos, range.end) === newText.substring(updatedDirective.range.pos, updatedDirective.range.end)); - } - } - // Ignore ranges that fall in change range + if (aggressiveChecks && shouldCheckNode(node)) { + ts.Debug.assert(text === newText.substring(node.pos, node.end)); } - addNewlyScannedDirectives(); - return commentDirectives; - function addNewlyScannedDirectives() { - if (addedNewlyScannedDirectives) - return; - addedNewlyScannedDirectives = true; - if (!commentDirectives) { - commentDirectives = newDirectives; - } - else if (newDirectives) { - commentDirectives.push(...newDirectives); + forEachChild(node, visitNode, visitArray); + if (ts.hasJSDocNodes(node)) { + for (const jsDocComment of node.jsDoc!) { + visitNode(jsDocComment as ts.Node as IncrementalNode); } } + checkNodePositions(node, aggressiveChecks); } - function moveElementEntirelyPastChangeRange(element: IncrementalElement, isArray: boolean, delta: number, oldText: string, newText: string, aggressiveChecks: boolean) { - if (isArray) { - visitArray(element as IncrementalNodeArray); - } - else { - visitNode(element as IncrementalNode); + function visitArray(array: IncrementalNodeArray) { + array._children = undefined; + ts.setTextRangePosEnd(array, array.pos + delta, array.end + delta); + + for (const node of array) { + visitNode(node); } - return; + } + } - function visitNode(node: IncrementalNode) { - let text = ""; - if (aggressiveChecks && shouldCheckNode(node)) { - text = oldText.substring(node.pos, node.end); - } + function shouldCheckNode(node: ts.Node) { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.Identifier: + return true; + } - // Ditch any existing LS children we may have created. This way we can avoid - // moving them forward. - if (node._children) { - node._children = undefined; - } + return false; + } - ts.setTextRangePosEnd(node, node.pos + delta, node.end + delta); + function adjustIntersectingElement(element: IncrementalElement, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number) { + ts.Debug.assert(element.end >= changeStart, "Adjusting an element that was entirely before the change range"); + ts.Debug.assert(element.pos <= changeRangeOldEnd, "Adjusting an element that was entirely after the change range"); + ts.Debug.assert(element.pos <= element.end); - if (aggressiveChecks && shouldCheckNode(node)) { - ts.Debug.assert(text === newText.substring(node.pos, node.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. - forEachChild(node, visitNode, visitArray); - if (ts.hasJSDocNodes(node)) { - for (const jsDocComment of node.jsDoc!) { - visitNode(jsDocComment as ts.Node as IncrementalNode); - } - } - checkNodePositions(node, aggressiveChecks); - } + // We may need to update both the 'pos' and the 'end' of the element. - function visitArray(array: IncrementalNodeArray) { - array._children = undefined; - ts.setTextRangePosEnd(array, array.pos + delta, array.end + delta); + // 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. + const 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. + const end = element.end >= changeRangeOldEnd ? + // Element ends after the change range. Always adjust the end pos. + element.end + delta : + // 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. + Math.min(element.end, changeRangeNewEnd); + + ts.Debug.assert(pos <= end); + if (element.parent) { + ts.Debug.assertGreaterThanOrEqual(pos, element.parent.pos); + ts.Debug.assertLessThanOrEqual(end, element.parent.end); + } + + ts.setTextRangePosEnd(element, pos, end); + } - for (const node of array) { - visitNode(node); + function checkNodePositions(node: ts.Node, aggressiveChecks: boolean) { + if (aggressiveChecks) { + let pos = node.pos; + const visitNode = (child: ts.Node) => { + ts.Debug.assert(child.pos >= pos); + pos = child.end; + }; + if (ts.hasJSDocNodes(node)) { + for (const jsDocComment of node.jsDoc!) { + visitNode(jsDocComment); } } + forEachChild(node, visitNode); + ts.Debug.assert(pos <= node.end); } + } + function updateTokenPositionsAndMarkElements(sourceFile: IncrementalNode, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number, oldText: string, newText: string, aggressiveChecks: boolean): void { - function shouldCheckNode(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.Identifier: - return true; - } - - return false; - } - - function adjustIntersectingElement(element: IncrementalElement, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number) { - ts.Debug.assert(element.end >= changeStart, "Adjusting an element that was entirely before the change range"); - ts.Debug.assert(element.pos <= changeRangeOldEnd, "Adjusting an element that was entirely after the change range"); - ts.Debug.assert(element.pos <= element.end); + visitNode(sourceFile); + return; - // 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. + function visitNode(child: IncrementalNode) { + ts.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; + } - // We may need to update both the 'pos' and the 'end' of the element. + // 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; - // 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. - const 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. - const end = element.end >= changeRangeOldEnd ? - // Element ends after the change range. Always adjust the end pos. - element.end + delta : - // 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. - Math.min(element.end, changeRangeNewEnd); - - ts.Debug.assert(pos <= end); - if (element.parent) { - ts.Debug.assertGreaterThanOrEqual(pos, element.parent.pos); - ts.Debug.assertLessThanOrEqual(end, element.parent.end); - } - - ts.setTextRangePosEnd(element, pos, end); - } - - function checkNodePositions(node: ts.Node, aggressiveChecks: boolean) { - if (aggressiveChecks) { - let pos = node.pos; - const visitNode = (child: ts.Node) => { - ts.Debug.assert(child.pos >= pos); - pos = child.end; - }; - if (ts.hasJSDocNodes(node)) { - for (const jsDocComment of node.jsDoc!) { - visitNode(jsDocComment); + // Adjust the pos or end (or both) of the intersecting element accordingly. + adjustIntersectingElement(child, changeStart, changeRangeOldEnd, changeRangeNewEnd, delta); + forEachChild(child, visitNode, visitArray); + if (ts.hasJSDocNodes(child)) { + for (const jsDocComment of child.jsDoc!) { + visitNode(jsDocComment as ts.Node as IncrementalNode); } } - forEachChild(node, visitNode); - ts.Debug.assert(pos <= node.end); + checkNodePositions(child, aggressiveChecks); + return; } + + // Otherwise, the node is entirely before the change range. No need to do anything with it. + ts.Debug.assert(fullEnd < changeStart); } - function updateTokenPositionsAndMarkElements(sourceFile: IncrementalNode, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number, oldText: string, newText: string, aggressiveChecks: boolean): void { - visitNode(sourceFile); - return; + function visitArray(array: IncrementalNodeArray) { + ts.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 visitNode(child: IncrementalNode) { - ts.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 = array.end; + if (fullEnd >= changeStart) { + array.intersectsChange = true; + array._children = undefined; - // 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 (ts.hasJSDocNodes(child)) { - for (const jsDocComment of child.jsDoc!) { - visitNode(jsDocComment as ts.Node as IncrementalNode); - } - } - checkNodePositions(child, aggressiveChecks); - return; + // 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); } - - // Otherwise, the node is entirely before the change range. No need to do anything with it. - ts.Debug.assert(fullEnd < changeStart); + return; } - function visitArray(array: IncrementalNodeArray) { - ts.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; - } + // Otherwise, the array is entirely before the change range. No need to do anything with it. + ts.Debug.assert(fullEnd < changeStart); + } + } - // 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; + function extendToAffectedRange(sourceFile: ts.SourceFile, changeRange: ts.TextChangeRange): ts.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; - // 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; - } + let start = changeRange.span.start; - // Otherwise, the array is entirely before the change range. No need to do anything with it. - ts.Debug.assert(fullEnd < changeStart); - } + // 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); + ts.Debug.assert(nearestNode.pos <= start); + const position = nearestNode.pos; + + start = Math.max(0, position - 1); } - function extendToAffectedRange(sourceFile: ts.SourceFile, changeRange: ts.TextChangeRange): ts.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; + const finalSpan = ts.createTextSpanFromBounds(start, ts.textSpanEnd(changeRange.span)); + const finalLength = changeRange.newLength + (changeRange.span.start - start); - let start = changeRange.span.start; + return ts.createTextChangeRange(finalSpan, finalLength); + } + function findNearestNodeStartingBeforeOrAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { + let bestResult: ts.Node = sourceFile; + let lastNodeEntirelyBeforePosition: ts.Node | undefined; - // 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); - ts.Debug.assert(nearestNode.pos <= start); - const position = nearestNode.pos; + forEachChild(sourceFile, visit); - start = Math.max(0, position - 1); + if (lastNodeEntirelyBeforePosition) { + const lastChildOfLastEntireNodeBeforePosition = getLastDescendant(lastNodeEntirelyBeforePosition); + if (lastChildOfLastEntireNodeBeforePosition.pos > bestResult.pos) { + bestResult = lastChildOfLastEntireNodeBeforePosition; } - - const finalSpan = ts.createTextSpanFromBounds(start, ts.textSpanEnd(changeRange.span)); - const finalLength = changeRange.newLength + (changeRange.span.start - start); - - return ts.createTextChangeRange(finalSpan, finalLength); } - function findNearestNodeStartingBeforeOrAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { - let bestResult: ts.Node = sourceFile; - let lastNodeEntirelyBeforePosition: ts.Node | undefined; - forEachChild(sourceFile, visit); + return bestResult; - if (lastNodeEntirelyBeforePosition) { - const lastChildOfLastEntireNodeBeforePosition = getLastDescendant(lastNodeEntirelyBeforePosition); - if (lastChildOfLastEntireNodeBeforePosition.pos > bestResult.pos) { - bestResult = lastChildOfLastEntireNodeBeforePosition; + function getLastDescendant(node: ts.Node): ts.Node { + while (true) { + const lastChild = ts.getLastChild(node); + if (lastChild) { + node = lastChild; + } + else { + return node; } } + } - return bestResult; - - function getLastDescendant(node: ts.Node): ts.Node { - while (true) { - const lastChild = ts.getLastChild(node); - if (lastChild) { - node = lastChild; - } - else { - return node; - } - } + function visit(child: ts.Node) { + if (ts.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: ts.Node) { - if (ts.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; } - // 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); - // 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 { - ts.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; - } + // Once we look at the children of this node, then there's no need to + // continue any further. + return true; } else { - ts.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; + ts.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; } } - } - - function checkChangeRange(sourceFile: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks: boolean) { - const oldText = sourceFile.text; - if (textChangeRange) { - ts.Debug.assert((oldText.length - textChangeRange.span.length + textChangeRange.newLength) === newText.length); - if (aggressiveChecks || ts.Debug.shouldAssert(ts.AssertionLevel.VeryAggressive)) { - const oldTextPrefix = oldText.substr(0, textChangeRange.span.start); - const newTextPrefix = newText.substr(0, textChangeRange.span.start); - ts.Debug.assert(oldTextPrefix === newTextPrefix); - const oldTextSuffix = oldText.substring(ts.textSpanEnd(textChangeRange.span), oldText.length); - const newTextSuffix = newText.substring(ts.textSpanEnd(ts.textChangeRangeNewSpan(textChangeRange)), newText.length); - ts.Debug.assert(oldTextSuffix === newTextSuffix); - } + else { + ts.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; } } + } - interface IncrementalElement extends ts.ReadonlyTextRange { - readonly parent: ts.Node; - intersectsChange: boolean; - length?: number; - _children: ts.Node[] | undefined; + function checkChangeRange(sourceFile: ts.SourceFile, newText: string, textChangeRange: ts.TextChangeRange, aggressiveChecks: boolean) { + const oldText = sourceFile.text; + if (textChangeRange) { + ts.Debug.assert((oldText.length - textChangeRange.span.length + textChangeRange.newLength) === newText.length); + if (aggressiveChecks || ts.Debug.shouldAssert(ts.AssertionLevel.VeryAggressive)) { + const oldTextPrefix = oldText.substr(0, textChangeRange.span.start); + const newTextPrefix = newText.substr(0, textChangeRange.span.start); + ts.Debug.assert(oldTextPrefix === newTextPrefix); + const oldTextSuffix = oldText.substring(ts.textSpanEnd(textChangeRange.span), oldText.length); + const newTextSuffix = newText.substring(ts.textSpanEnd(ts.textChangeRangeNewSpan(textChangeRange)), newText.length); + ts.Debug.assert(oldTextSuffix === newTextSuffix); + } } + } - export interface IncrementalNode extends ts.Node, IncrementalElement { - hasBeenIncrementallyParsed: boolean; - } + interface IncrementalElement extends ts.ReadonlyTextRange { + readonly parent: ts.Node; + intersectsChange: boolean; + length?: number; + _children: ts.Node[] | undefined; + } - interface IncrementalNodeArray extends ts.NodeArray, IncrementalElement { - length: number; - } + export interface IncrementalNode extends ts.Node, IncrementalElement { + hasBeenIncrementallyParsed: boolean; + } - // 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; - } + interface IncrementalNodeArray extends ts.NodeArray, IncrementalElement { + length: number; + } - export function createSyntaxCursor(sourceFile: ts.SourceFile): SyntaxCursor { - let currentArray: ts.NodeArray = sourceFile.statements; - let currentArrayIndex = 0; + // 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; + } - ts.Debug.assert(currentArrayIndex < currentArray.length); - let current = currentArray[currentArrayIndex]; - let lastQueriedPosition = InvalidPosition.Value; + export function createSyntaxCursor(sourceFile: ts.SourceFile): SyntaxCursor { + let currentArray: ts.NodeArray = sourceFile.statements; + let currentArrayIndex = 0; - 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]; - } + ts.Debug.assert(currentArrayIndex < currentArray.length); + let current = currentArray[currentArrayIndex]; + let lastQueriedPosition = InvalidPosition.Value; - // 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); - } + 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. - ts.Debug.assert(!current || current.pos === position); - return current as IncrementalNode; + // 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); + } } - }; - // 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; + // 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; - function visitNode(node: ts.Node) { - if (position >= node.pos && position < node.end) { - // Position was within this node. Keep searching deeper to find the node. - forEachChild(node, visitNode, visitArray); + // Either we don'd have a node, or we have a node at the position being asked for. + ts.Debug.assert(!current || current.pos === position); + return current as IncrementalNode; + } + }; - // don't proceed any further in the search. - return true; - } + // 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; - // position wasn't in this node, have to keep searching. - return false; + function visitNode(node: ts.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; } - function visitArray(array: ts.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; + // position wasn't in this node, have to keep searching. + return false; + } + + function visitArray(array: ts.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; } - } - } - const enum InvalidPosition { - Value = -1 + // position wasn't in this array, have to keep searching. + return false; + } } } - /** @internal */ - export function isDeclarationFileName(fileName: string): boolean { - return ts.fileExtensionIsOneOf(fileName, ts.supportedDeclarationExtensions); + const enum InvalidPosition { + Value = -1 } +} - /*@internal*/ - export interface PragmaContext { - languageVersion: ts.ScriptTarget; - pragmas?: ts.PragmaMap; - checkJsDirective?: ts.CheckJsDirective; - referencedFiles: ts.FileReference[]; - typeReferenceDirectives: ts.FileReference[]; - libReferenceDirectives: ts.FileReference[]; - amdDependencies: ts.AmdDependency[]; - hasNoDefaultLib?: boolean; - moduleName?: string; - } +/** @internal */ +export function isDeclarationFileName(fileName: string): boolean { + return ts.fileExtensionIsOneOf(fileName, ts.supportedDeclarationExtensions); +} - function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { - if (!mode) { - return undefined; - } - if (mode === "import") { - return ts.ModuleKind.ESNext; - } - if (mode === "require") { - return ts.ModuleKind.CommonJS; - } - reportDiagnostic(pos, end - pos, ts.Diagnostics.resolution_mode_should_be_either_require_or_import); +/*@internal*/ +export interface PragmaContext { + languageVersion: ts.ScriptTarget; + pragmas?: ts.PragmaMap; + checkJsDirective?: ts.CheckJsDirective; + referencedFiles: ts.FileReference[]; + typeReferenceDirectives: ts.FileReference[]; + libReferenceDirectives: ts.FileReference[]; + amdDependencies: ts.AmdDependency[]; + hasNoDefaultLib?: boolean; + moduleName?: string; +} + +function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { + if (!mode) { return undefined; } + if (mode === "import") { + return ts.ModuleKind.ESNext; + } + if (mode === "require") { + return ts.ModuleKind.CommonJS; + } + reportDiagnostic(pos, end - pos, ts.Diagnostics.resolution_mode_should_be_either_require_or_import); + return undefined; +} - /*@internal*/ - export function processCommentPragmas(context: PragmaContext, sourceText: string): void { - const pragmas: ts.PragmaPseudoMapEntry[] = []; - for (const range of ts.getLeadingCommentRanges(sourceText, 0) || ts.emptyArray) { - const comment = sourceText.substring(range.pos, range.end); - extractPragmas(pragmas, range, comment); - } +/*@internal*/ +export function processCommentPragmas(context: PragmaContext, sourceText: string): void { + const pragmas: ts.PragmaPseudoMapEntry[] = []; + for (const range of ts.getLeadingCommentRanges(sourceText, 0) || ts.emptyArray) { + const comment = sourceText.substring(range.pos, range.end); + extractPragmas(pragmas, range, comment); + } - context.pragmas = new ts.Map() as ts.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; + context.pragmas = new ts.Map() as ts.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: ts.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; - ts.forEach(ts.toArray(entryOrList) as ts.PragmaPseudoMap["reference"][], arg => { - const { types, lib, path, ["resolution-mode"]: res } = arg.arguments; - if (arg.arguments["no-default-lib"]) { - context.hasNoDefaultLib = true; - } - else if (types) { - const parsed = parseResolutionMode(res, types.pos, types.end, reportDiagnostic); - typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value, ...(parsed ? { resolutionMode: parsed } : {}) }); - } - 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, ts.Diagnostics.Invalid_reference_directive_syntax); - } - }); - break; - } - case "amd-dependency": { - context.amdDependencies = ts.map(ts.toArray(entryOrList) as ts.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, ts.Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments); - } - context.moduleName = (entry as ts.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: ts.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; + ts.forEach(ts.toArray(entryOrList) as ts.PragmaPseudoMap["reference"][], arg => { + const { types, lib, path, ["resolution-mode"]: res } = arg.arguments; + if (arg.arguments["no-default-lib"]) { + context.hasNoDefaultLib = true; + } + else if (types) { + const parsed = parseResolutionMode(res, types.pos, types.end, reportDiagnostic); + typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value, ...(parsed ? { resolutionMode: parsed } : {}) }); + } + 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 ts.PragmaPseudoMap["amd-module"]).arguments.name; + reportDiagnostic(arg.range.pos, arg.range.end - arg.range.pos, ts.Diagnostics.Invalid_reference_directive_syntax); } - break; - } - case "ts-nocheck": - case "ts-check": { - // _last_ of either nocheck or check in a file is the "winner" - ts.forEach(ts.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 "amd-dependency": { + context.amdDependencies = ts.map(ts.toArray(entryOrList) as ts.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, ts.Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments); } - }); - break; + context.moduleName = (entry as ts.PragmaPseudoMap["amd-module"]).arguments.name; + } } - case "jsx": - case "jsxfrag": - case "jsximportsource": - case "jsxruntime": - return; // Accessed directly - default: ts.Debug.fail("Unhandled pragma kind"); // Can this be made into an assertNever in the future? + else { + context.moduleName = (entryOrList as ts.PragmaPseudoMap["amd-module"]).arguments.name; + } + break; } - }); - } - - const namedArgRegExCache = new ts.Map(); - function getNamedArgRegEx(name: string): RegExp { - if (namedArgRegExCache.has(name)) { - return namedArgRegExCache.get(name)!; + case "ts-nocheck": + case "ts-check": { + // _last_ of either nocheck or check in a file is the "winner" + ts.forEach(ts.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": + case "jsxfrag": + case "jsximportsource": + case "jsxruntime": + return; // Accessed directly + default: ts.Debug.fail("Unhandled pragma kind"); // Can this be made into an assertNever in the future? } - const result = new RegExp(`(\\s${name}\\s*=\\s*)(?:(?:'([^']*)')|(?:"([^"]*)"))`, "im"); - namedArgRegExCache.set(name, result); - return result; + }); +} + +const namedArgRegExCache = new ts.Map(); +function getNamedArgRegEx(name: string): RegExp { + if (namedArgRegExCache.has(name)) { + return namedArgRegExCache.get(name)!; } + const result = new RegExp(`(\\s${name}\\s*=\\s*)(?:(?:'([^']*)')|(?:"([^"]*)"))`, "im"); + namedArgRegExCache.set(name, result); + return result; +} - const tripleSlashXMLCommentStartRegEx = /^\/\/\/\s*<(\S+)\s.*?\/>/im; - const singleLinePragmaRegEx = /^\/\/\/?\s*@(\S+)\s*(.*)\s*$/im; - function extractPragmas(pragmas: ts.PragmaPseudoMapEntry[], range: ts.CommentRange, text: string) { - const tripleSlash = range.kind === ts.SyntaxKind.SingleLineCommentTrivia && tripleSlashXMLCommentStartRegEx.exec(text); - if (tripleSlash) { - const name = tripleSlash[1].toLowerCase() as keyof ts.PragmaPseudoMap; // Technically unsafe cast, but we do it so the below check to make it safe typechecks - const pragma = ts.commentPragmas[name] as ts.PragmaDefinition; - if (!pragma || !(pragma.kind! & ts.PragmaKindFlags.TripleSlashXML)) { - return; - } - 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 +const tripleSlashXMLCommentStartRegEx = /^\/\/\/\s*<(\S+)\s.*?\/>/im; +const singleLinePragmaRegEx = /^\/\/\/?\s*@(\S+)\s*(.*)\s*$/im; +function extractPragmas(pragmas: ts.PragmaPseudoMapEntry[], range: ts.CommentRange, text: string) { + const tripleSlash = range.kind === ts.SyntaxKind.SingleLineCommentTrivia && tripleSlashXMLCommentStartRegEx.exec(text); + if (tripleSlash) { + const name = tripleSlash[1].toLowerCase() as keyof ts.PragmaPseudoMap; // Technically unsafe cast, but we do it so the below check to make it safe typechecks + const pragma = ts.commentPragmas[name] as ts.PragmaDefinition; + if (!pragma || !(pragma.kind! & ts.PragmaKindFlags.TripleSlashXML)) { + return; + } + 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) { + const value = matchResult[2] || matchResult[3]; + if (arg.captureSpan) { + const startPos = range.pos + matchResult.index + matchResult[1].length + 1; + argument[arg.name] = { + value, + pos: startPos, + end: startPos + value.length + }; } - else if (matchResult) { - const value = matchResult[2] || matchResult[3]; - if (arg.captureSpan) { - const startPos = range.pos + matchResult.index + matchResult[1].length + 1; - argument[arg.name] = { - value, - pos: startPos, - end: startPos + value.length - }; - } - else { - argument[arg.name] = value; - } + else { + argument[arg.name] = value; } } - pragmas.push({ name, args: { arguments: argument, range } } as ts.PragmaPseudoMapEntry); } - else { - pragmas.push({ name, args: { arguments: {}, range } } as ts.PragmaPseudoMapEntry); - } - return; + pragmas.push({ name, args: { arguments: argument, range } } as ts.PragmaPseudoMapEntry); } - - const singleLine = range.kind === ts.SyntaxKind.SingleLineCommentTrivia && singleLinePragmaRegEx.exec(text); - if (singleLine) { - return addPragmaForMatch(pragmas, range, ts.PragmaKindFlags.SingleLine, singleLine); + else { + pragmas.push({ name, args: { arguments: {}, range } } as ts.PragmaPseudoMapEntry); } + return; + } - if (range.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - const multiLinePragmaRegEx = /@(\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, ts.PragmaKindFlags.MultiLine, multiLineMatch); - } - } + const singleLine = range.kind === ts.SyntaxKind.SingleLineCommentTrivia && singleLinePragmaRegEx.exec(text); + if (singleLine) { + return addPragmaForMatch(pragmas, range, ts.PragmaKindFlags.SingleLine, singleLine); } - function addPragmaForMatch(pragmas: ts.PragmaPseudoMapEntry[], range: ts.CommentRange, kind: ts.PragmaKindFlags, match: RegExpExecArray) { - if (!match) - return; - const name = match[1].toLowerCase() as keyof ts.PragmaPseudoMap; // Technically unsafe cast, but we do it so they below check to make it safe typechecks - const pragma = ts.commentPragmas[name] as ts.PragmaDefinition; - if (!pragma || !(pragma.kind! & kind)) { - return; + if (range.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + const multiLinePragmaRegEx = /@(\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, ts.PragmaKindFlags.MultiLine, multiLineMatch); } - 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 ts.PragmaPseudoMapEntry); + } +} + +function addPragmaForMatch(pragmas: ts.PragmaPseudoMapEntry[], range: ts.CommentRange, kind: ts.PragmaKindFlags, match: RegExpExecArray) { + if (!match) + return; + const name = match[1].toLowerCase() as keyof ts.PragmaPseudoMap; // Technically unsafe cast, but we do it so they below check to make it safe typechecks + const pragma = ts.commentPragmas[name] as ts.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 ts.PragmaPseudoMapEntry); + return; +} - function getNamedPragmaArguments(pragma: ts.PragmaDefinition, text: string | undefined): { +function getNamedPragmaArguments(pragma: ts.PragmaDefinition, text: string | undefined): { + [index: string]: string; +} | "fail" { + if (!text) + return {}; + if (!pragma.args) + return {}; + const args = ts.trimString(text).split(/\s+/); + const argMap: { [index: string]: string; - } | "fail" { - if (!text) - return {}; - if (!pragma.args) - return {}; - const args = ts.trimString(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 ts.Debug.fail("Capture spans not yet implemented for non-xml pragmas"); - } - argMap[argument.name] = args[i]; - } - return argMap; - } - - /** @internal */ - export function tagNamesAreEquivalent(lhs: ts.JsxTagNameExpression, rhs: ts.JsxTagNameExpression): boolean { - if (lhs.kind !== rhs.kind) { - return false; + } = {}; + for (let i = 0; i < pragma.args.length; i++) { + const argument = pragma.args[i]; + if (!args[i] && !argument.optional) { + return "fail"; } - - if (lhs.kind === ts.SyntaxKind.Identifier) { - return lhs.escapedText === (rhs as ts.Identifier).escapedText; + if (argument.captureSpan) { + return ts.Debug.fail("Capture spans not yet implemented for non-xml pragmas"); } + argMap[argument.name] = args[i]; + } + return argMap; +} - if (lhs.kind === ts.SyntaxKind.ThisKeyword) { - return true; - } +/** @internal */ +export function tagNamesAreEquivalent(lhs: ts.JsxTagNameExpression, rhs: ts.JsxTagNameExpression): boolean { + if (lhs.kind !== rhs.kind) { + return false; + } - // 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 as ts.PropertyAccessExpression).name.escapedText === (rhs as ts.PropertyAccessExpression).name.escapedText && - tagNamesAreEquivalent((lhs as ts.PropertyAccessExpression).expression as ts.JsxTagNameExpression, (rhs as ts.PropertyAccessExpression).expression as ts.JsxTagNameExpression); + if (lhs.kind === ts.SyntaxKind.Identifier) { + return lhs.escapedText === (rhs as ts.Identifier).escapedText; } + + if (lhs.kind === ts.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 as ts.PropertyAccessExpression).name.escapedText === (rhs as ts.PropertyAccessExpression).name.escapedText && + tagNamesAreEquivalent((lhs as ts.PropertyAccessExpression).expression as ts.JsxTagNameExpression, (rhs as ts.PropertyAccessExpression).expression as ts.JsxTagNameExpression); +} } diff --git a/src/compiler/path.ts b/src/compiler/path.ts index ea63b81c93f41..5e060f025c1c0 100644 --- a/src/compiler/path.ts +++ b/src/compiler/path.ts @@ -1,894 +1,894 @@ /* @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 = "/"; - export const altDirectorySeparator = "\\"; - const urlSchemeSeparator = "://"; - const backslashRegExp = /\\/g; - - //// Path Tests - - /** - * Determines whether a charCode corresponds to `/` or `\`. - */ - export function isAnyDirectorySeparator(charCode: number): boolean { - return charCode === ts.CharacterCodes.slash || charCode === ts.CharacterCodes.backslash; - } +/** + * 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 = "/"; +export const altDirectorySeparator = "\\"; +const urlSchemeSeparator = "://"; +const backslashRegExp = /\\/g; + +//// Path Tests + +/** + * Determines whether a charCode corresponds to `/` or `\`. + */ +export function isAnyDirectorySeparator(charCode: number): boolean { + return charCode === ts.CharacterCodes.slash || charCode === ts.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 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 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 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 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); - } +/** + * Determines whether a path starts with a relative path component (i.e. `.` or `..`). + */ +export function pathIsRelative(path: string): boolean { + return /^\.\.?($|[\\/])/.test(path); +} - /** - * Determines whether a path is neither relative nor absolute, e.g. "path/to/file". - * Also known misleadingly as "non-relative". - */ - export function pathIsBareSpecifier(path: string): boolean { - return !pathIsAbsolute(path) && !pathIsRelative(path); - } +/** + * Determines whether a path is neither relative nor absolute, e.g. "path/to/file". + * Also known misleadingly as "non-relative". + */ +export function pathIsBareSpecifier(path: string): boolean { + return !pathIsAbsolute(path) && !pathIsRelative(path); +} - export function hasExtension(fileName: string): boolean { - return ts.stringContains(getBaseFileName(fileName), "."); - } +export function hasExtension(fileName: string): boolean { + return ts.stringContains(getBaseFileName(fileName), "."); +} - export function fileExtensionIs(path: string, extension: string): boolean { - return path.length > extension.length && ts.endsWith(path, extension); - } +export function fileExtensionIs(path: string, extension: string): boolean { + return path.length > extension.length && ts.endsWith(path, extension); +} - export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { - for (const extension of extensions) { - if (fileExtensionIs(path, extension)) { - return true; - } +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)); - } + return false; +} - //// Path Parsing +/** + * Determines whether a path has a trailing separator (`/` or `\\`). + */ +export function hasTrailingDirectorySeparator(path: string) { + return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); +} - function isVolumeCharacter(charCode: number) { - return (charCode >= ts.CharacterCodes.a && charCode <= ts.CharacterCodes.z) || - (charCode >= ts.CharacterCodes.A && charCode <= ts.CharacterCodes.Z); - } +//// Path Parsing - function getFileUrlVolumeSeparatorEnd(url: string, start: number) { - const ch0 = url.charCodeAt(start); - if (ch0 === ts.CharacterCodes.colon) - return start + 1; - if (ch0 === ts.CharacterCodes.percent && url.charCodeAt(start + 1) === ts.CharacterCodes._3) { - const ch2 = url.charCodeAt(start + 2); - if (ch2 === ts.CharacterCodes.a || ch2 === ts.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. - */ - function getEncodedRootLength(path: string): number { - if (!path) - return 0; - const ch0 = path.charCodeAt(0); - - // POSIX or UNC - if (ch0 === ts.CharacterCodes.slash || ch0 === ts.CharacterCodes.backslash) { - if (path.charCodeAt(1) !== ch0) - return 1; // POSIX: "/" (or non-normalized "\") - const p1 = path.indexOf(ch0 === ts.CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); - if (p1 < 0) - return path.length; // UNC: "//server" or "\\server" - - return p1 + 1; // UNC: "//server/" or "\\server\" - } +function isVolumeCharacter(charCode: number) { + return (charCode >= ts.CharacterCodes.a && charCode <= ts.CharacterCodes.z) || + (charCode >= ts.CharacterCodes.A && charCode <= ts.CharacterCodes.Z); +} - // DOS - if (isVolumeCharacter(ch0) && path.charCodeAt(1) === ts.CharacterCodes.colon) { - const ch2 = path.charCodeAt(2); - if (ch2 === ts.CharacterCodes.slash || ch2 === ts.CharacterCodes.backslash) - return 3; // DOS: "c:/" or "c:\" - if (path.length === 2) - return 2; // DOS: "c:" (but not "c:d") - } +function getFileUrlVolumeSeparatorEnd(url: string, start: number) { + const ch0 = url.charCodeAt(start); + if (ch0 === ts.CharacterCodes.colon) + return start + 1; + if (ch0 === ts.CharacterCodes.percent && url.charCodeAt(start + 1) === ts.CharacterCodes._3) { + const ch2 = url.charCodeAt(start + 2); + if (ch2 === ts.CharacterCodes.a || ch2 === ts.CharacterCodes.A) + return start + 3; + } + return -1; +} - // 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) === ts.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; - } +/** + * 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 === ts.CharacterCodes.slash || ch0 === ts.CharacterCodes.backslash) { + if (path.charCodeAt(1) !== ch0) + return 1; // POSIX: "/" (or non-normalized "\") + const p1 = path.indexOf(ch0 === ts.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) === ts.CharacterCodes.colon) { + const ch2 = path.charCodeAt(2); + if (ch2 === ts.CharacterCodes.slash || ch2 === ts.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) === ts.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" + return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" } - - // relative - return 0; + return ~path.length; // URL: "file://server", "http://server" } - /** - * 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; - } + // relative + return 0; +} - /** - * 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: ts.Path): ts.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); +/** + * 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; +} - // If the path provided is itself the root, then return it. - const rootLength = getRootLength(path); - if (rootLength === path.length) - return 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" + * ``` + */ +export function getDirectoryPath(path: ts.Path): ts.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))); - } + // 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); +/** + * 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 ""; - // 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; - } + // 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 (!ts.startsWith(extension, ".")) - extension = "." + extension; - if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === ts.CharacterCodes.dot) { - const pathExtension = path.slice(path.length - extension.length); - if (stringEqualityComparer(pathExtension, extension)) { - return pathExtension; - } +function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { + if (!ts.startsWith(extension, ".")) + extension = "." + extension; + if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === ts.CharacterCodes.dot) { + const pathExtension = path.slice(path.length - extension.length); + if (stringEqualityComparer(pathExtension, extension)) { + return pathExtension; } } +} - 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 ""; +function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { + if (typeof extensions === "string") { + return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; } - - /** - * 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 ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive); - } - const baseFileName = getBaseFileName(path); - const extensionIndex = baseFileName.lastIndexOf("."); - if (extensionIndex >= 0) { - return baseFileName.substring(extensionIndex); - } - return ""; + for (const extension of extensions) { + const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); + if (result) + return result; } + return ""; +} - function pathComponents(path: string, rootLength: number) { - const root = path.substring(0, rootLength); - const rest = path.substring(rootLength).split(directorySeparator); - if (rest.length && !ts.lastOrUndefined(rest)) - rest.pop(); - return [root, ...rest]; - } +/** + * 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 ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive); + } + const baseFileName = getBaseFileName(path); + const extensionIndex = baseFileName.lastIndexOf("."); + if (extensionIndex >= 0) { + return baseFileName.substring(extensionIndex); + } + return ""; +} - /** - * 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)); - } +function pathComponents(path: string, rootLength: number) { + const root = path.substring(0, rootLength); + const rest = path.substring(rootLength).split(directorySeparator); + if (rest.length && !ts.lastOrUndefined(rest)) + rest.pop(); + return [root, ...rest]; +} - //// 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); - } +/** + * 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 Normalization +//// 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 ""; - /** - * Normalize path separators, converting `\` into `/`. - */ - export function normalizeSlashes(path: string): string { - const index = path.indexOf("\\"); - if (index === -1) { - return path; - } - backslashRegExp.lastIndex = index; // prime regex with known position - return path.replace(backslashRegExp, directorySeparator); + const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); + return root + pathComponents.slice(1).join(directorySeparator); +} + +//// Path Normalization + +/** + * Normalize path separators, converting `\` into `/`. + */ +export function normalizeSlashes(path: string): string { + const index = path.indexOf("\\"); + if (index === -1) { + return path; } + backslashRegExp.lastIndex = index; // prime regex with known position + return path.replace(backslashRegExp, directorySeparator); +} - /** - * 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 (!ts.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]) +/** + * 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 (!ts.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; + } } - reduced.push(component); + else if (reduced[0]) + continue; } - return reduced; + 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; - } +/** + * 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; } - return path; } + 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(ts.some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(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(ts.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)); - } +/** + * 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 getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { + return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); +} - export function normalizePath(path: string): string { - path = normalizeSlashes(path); - // Most paths don't require normalization +export function normalizePath(path: string): string { + path = normalizeSlashes(path); + // Most paths don't require normalization + if (!relativePathSegmentRegExp.test(path)) { + return path; + } + // Some paths only require cleanup of `/./` or leading `./` + const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, ""); + if (simplified !== path) { + path = simplified; if (!relativePathSegmentRegExp.test(path)) { return path; } - // Some paths only require cleanup of `/./` or leading `./` - const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, ""); - if (simplified !== path) { - path = simplified; - if (!relativePathSegmentRegExp.test(path)) { - return path; - } - } - // Other paths require full normalization - 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): ts.Path { - const nonCanonicalizedPath = isRootedDiskPath(fileName) - ? normalizePath(fileName) - : getNormalizedAbsolutePath(fileName, basePath); - return getCanonicalFileName(nonCanonicalizedPath) as ts.Path; } + // Other paths require full normalization + const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); + return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; +} - //// 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: ts.Path): ts.Path; - export function removeTrailingDirectorySeparator(path: string): string; - export function removeTrailingDirectorySeparator(path: string) { - if (hasTrailingDirectorySeparator(path)) { - return path.substr(0, path.length - 1); - } +function getPathWithoutRoot(pathComponents: readonly string[]) { + if (pathComponents.length === 0) + return ""; + return pathComponents.slice(1).join(directorySeparator); +} - return path; - } +export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { + return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); +} - /** - * 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: ts.Path): ts.Path; - export function ensureTrailingDirectorySeparator(path: string): string; - export function ensureTrailingDirectorySeparator(path: string) { - if (!hasTrailingDirectorySeparator(path)) { - return path + directorySeparator; - } +export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): ts.Path { + const nonCanonicalizedPath = isRootedDiskPath(fileName) + ? normalizePath(fileName) + : getNormalizedAbsolutePath(fileName, basePath); + return getCanonicalFileName(nonCanonicalizedPath) as ts.Path; +} - return path; - } +//// Path Mutation - /** - * 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; +/** + * 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: ts.Path): ts.Path; +export function removeTrailingDirectorySeparator(path: string): string; +export function removeTrailingDirectorySeparator(path: string) { + if (hasTrailingDirectorySeparator(path)) { + return path.substr(0, path.length - 1); } - /** - * 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) + (ts.startsWith(ext, ".") ? ext : "." + ext) : path; - } + return path; +} - //// Path Comparisons +/** + * 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: ts.Path): ts.Path; +export function ensureTrailingDirectorySeparator(path: string): string; +export function ensureTrailingDirectorySeparator(path: string) { + if (!hasTrailingDirectorySeparator(path)) { + return path + directorySeparator; + } + + return path; +} - // check path for these segments: '', '.'. '..' - const relativePathSegmentRegExp = /(?:\/\/)|(?:^|\/)\.\.?(?:$|\/)/; +/** + * 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; +} - function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => ts.Comparison) { - if (a === b) - return ts.Comparison.EqualTo; - if (a === undefined) - return ts.Comparison.LessThan; - if (b === undefined) - return ts.Comparison.GreaterThan; +/** + * 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) + (ts.startsWith(ext, ".") ? ext : "." + ext) : path; +} - // 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 = ts.compareStringsCaseInsensitive(aRoot, bRoot); +//// Path Comparisons + +// check path for these segments: '', '.'. '..' +const relativePathSegmentRegExp = /(?:\/\/)|(?:^|\/)\.\.?(?:$|\/)/; + +function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => ts.Comparison) { + if (a === b) + return ts.Comparison.EqualTo; + if (a === undefined) + return ts.Comparison.LessThan; + if (b === undefined) + return ts.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 = ts.compareStringsCaseInsensitive(aRoot, bRoot); + if (result !== ts.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 !== ts.Comparison.EqualTo) { return result; } + } + return ts.compareValues(aComponents.length, bComponents.length); +} - // 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); - } +/** + * 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, ts.compareStringsCaseSensitive); +} - // 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 !== ts.Comparison.EqualTo) { - return result; - } - } - return ts.compareValues(aComponents.length, bComponents.length); - } +/** + * Performs a case-insensitive comparison of two paths. + */ +export function comparePathsCaseInsensitive(a: string, b: string) { + return comparePathsWorker(a, b, ts.compareStringsCaseInsensitive); +} - /** - * 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, ts.compareStringsCaseSensitive); +/** + * Compare two paths using the provided case sensitivity. + */ +export function comparePaths(a: string, b: string, ignoreCase?: boolean): ts.Comparison; +export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): ts.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); } - - /** - * Performs a case-insensitive comparison of two paths. - */ - export function comparePathsCaseInsensitive(a: string, b: string) { - return comparePathsWorker(a, b, ts.compareStringsCaseInsensitive); + else if (typeof currentDirectory === "boolean") { + ignoreCase = currentDirectory; } + return comparePathsWorker(a, b, ts.getStringComparer(ignoreCase)); +} - /** - * Compare two paths using the provided case sensitivity. - */ - export function comparePaths(a: string, b: string, ignoreCase?: boolean): ts.Comparison; - export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): ts.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, ts.getStringComparer(ignoreCase)); +/** + * 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); } - - /** - * 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 ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive; - for (let i = 0; i < parentComponents.length; i++) { - const equalityComparer = i === 0 ? ts.equateStringsCaseInsensitive : componentEqualityComparer; - if (!equalityComparer(parentComponents[i], childComponents[i])) { - return false; - } - } - + 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; } - /** - * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. - * Comparison is case-sensitive between the canonical paths. - * - * Use `containsPath` if file names are not already reduced and absolute. - */ - export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: ts.GetCanonicalFileName): boolean { - const canonicalFileName = getCanonicalFileName(fileName); - const canonicalDirectoryName = getCanonicalFileName(directoryName); - return ts.startsWith(canonicalFileName, canonicalDirectoryName + "/") || ts.startsWith(canonicalFileName, canonicalDirectoryName + "\\"); + const componentEqualityComparer = ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive; + for (let i = 0; i < parentComponents.length; i++) { + const equalityComparer = i === 0 ? ts.equateStringsCaseInsensitive : componentEqualityComparer; + if (!equalityComparer(parentComponents[i], childComponents[i])) { + return false; + } } - //// Relative Paths + return true; +} - export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: ts.GetCanonicalFileName) { - const fromComponents = reducePathComponents(getPathComponents(from)); - const toComponents = reducePathComponents(getPathComponents(to)); +/** + * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. + * Comparison is case-sensitive between the canonical paths. + * + * Use `containsPath` if file names are not already reduced and absolute. + */ +export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: ts.GetCanonicalFileName): boolean { + const canonicalFileName = getCanonicalFileName(fileName); + const canonicalDirectoryName = getCanonicalFileName(directoryName); + return ts.startsWith(canonicalFileName, canonicalDirectoryName + "/") || ts.startsWith(canonicalFileName, canonicalDirectoryName + "\\"); +} - 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 ? ts.equateStringsCaseInsensitive : stringEqualityComparer; - if (!comparer(fromComponent, toComponent)) - break; - } +//// Relative Paths - if (start === 0) { - return toComponents; - } +export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: ts.GetCanonicalFileName) { + const fromComponents = reducePathComponents(getPathComponents(from)); + const toComponents = reducePathComponents(getPathComponents(to)); - const components = toComponents.slice(start); - const relative: string[] = []; - for (; start < fromComponents.length; start++) { - relative.push(".."); - } - return ["", ...relative, ...components]; + 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 ? ts.equateStringsCaseInsensitive : stringEqualityComparer; + if (!comparer(fromComponent, toComponent)) + break; } - /** - * 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: ts.GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures - export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: ts.GetCanonicalFileName | boolean) { - ts.Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); - const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : ts.identity; - const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; - const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive, getCanonicalFileName); - return getPathFromPathComponents(pathComponents); + if (start === 0) { + return toComponents; } - export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { - return !isRootedDiskPath(absoluteOrRelativePath) - ? absoluteOrRelativePath - : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + const components = toComponents.slice(start); + const relative: string[] = []; + for (; start < fromComponents.length; start++) { + relative.push(".."); } + return ["", ...relative, ...components]; +} - export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: ts.GetCanonicalFileName) { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); - } +/** + * 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: ts.GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures +export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: ts.GetCanonicalFileName | boolean) { + ts.Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); + const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : ts.identity; + const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; + const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive, getCanonicalFileName); + return getPathFromPathComponents(pathComponents); +} - export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { - const pathComponents = getPathComponentsRelativeTo(resolvePath(currentDirectory, directoryPathOrUrl), resolvePath(currentDirectory, relativeOrAbsolutePath), ts.equateStringsCaseSensitive, getCanonicalFileName); +export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { + return !isRootedDiskPath(absoluteOrRelativePath) + ? absoluteOrRelativePath + : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); +} - const firstComponent = pathComponents[0]; - if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { - const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; - pathComponents[0] = prefix + firstComponent; - } +export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: ts.GetCanonicalFileName) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); +} - return getPathFromPathComponents(pathComponents); +export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { + const pathComponents = getPathComponentsRelativeTo(resolvePath(currentDirectory, directoryPathOrUrl), resolvePath(currentDirectory, relativeOrAbsolutePath), ts.equateStringsCaseSensitive, getCanonicalFileName); + + const firstComponent = pathComponents[0]; + if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { + const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; + pathComponents[0] = prefix + firstComponent; } - //// Path Traversal - - /** - * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. - */ - export function forEachAncestorDirectory(directory: ts.Path, callback: (directory: ts.Path) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: ts.Path, callback: (directory: ts.Path) => T | undefined): T | undefined { - while (true) { - const result = callback(directory); - if (result !== undefined) { - return result; - } + return getPathFromPathComponents(pathComponents); +} - const parentPath = getDirectoryPath(directory); - if (parentPath === directory) { - return undefined; - } +//// Path Traversal + +/** + * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. + */ +export function forEachAncestorDirectory(directory: ts.Path, callback: (directory: ts.Path) => T | undefined): T | undefined; +export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; +export function forEachAncestorDirectory(directory: ts.Path, callback: (directory: ts.Path) => T | undefined): T | undefined { + while (true) { + const result = callback(directory); + if (result !== undefined) { + return result; + } - directory = parentPath; + const parentPath = getDirectoryPath(directory); + if (parentPath === directory) { + return undefined; } - } - export function isNodeModulesDirectory(dirPath: ts.Path) { - return ts.endsWith(dirPath, "/node_modules"); + directory = parentPath; } } + +export function isNodeModulesDirectory(dirPath: ts.Path) { + return ts.endsWith(dirPath, "/node_modules"); +} +} diff --git a/src/compiler/perfLogger.ts b/src/compiler/perfLogger.ts index f6efab44b8b23..8ef902ea3e636 100644 --- a/src/compiler/perfLogger.ts +++ b/src/compiler/perfLogger.ts @@ -1,43 +1,43 @@ /* @internal */ namespace ts { - type PerfLogger = typeof import("@microsoft/typescript-etw"); - const nullLogger: PerfLogger = { - logEvent: ts.noop, - logErrEvent: ts.noop, - logPerfEvent: ts.noop, - logInfoEvent: ts.noop, - logStartCommand: ts.noop, - logStopCommand: ts.noop, - logStartUpdateProgram: ts.noop, - logStopUpdateProgram: ts.noop, - logStartUpdateGraph: ts.noop, - logStopUpdateGraph: ts.noop, - logStartResolveModule: ts.noop, - logStopResolveModule: ts.noop, - logStartParseSourceFile: ts.noop, - logStopParseSourceFile: ts.noop, - logStartReadFile: ts.noop, - logStopReadFile: ts.noop, - logStartBindFile: ts.noop, - logStopBindFile: ts.noop, - logStartScheduledOperation: ts.noop, - logStopScheduledOperation: ts.noop, - }; +type PerfLogger = typeof import("@microsoft/typescript-etw"); +const nullLogger: PerfLogger = { + logEvent: ts.noop, + logErrEvent: ts.noop, + logPerfEvent: ts.noop, + logInfoEvent: ts.noop, + logStartCommand: ts.noop, + logStopCommand: ts.noop, + logStartUpdateProgram: ts.noop, + logStopUpdateProgram: ts.noop, + logStartUpdateGraph: ts.noop, + logStopUpdateGraph: ts.noop, + logStartResolveModule: ts.noop, + logStopResolveModule: ts.noop, + logStartParseSourceFile: ts.noop, + logStopParseSourceFile: ts.noop, + logStartReadFile: ts.noop, + logStopReadFile: ts.noop, + logStartBindFile: ts.noop, + logStopBindFile: ts.noop, + logStartScheduledOperation: ts.noop, + logStopScheduledOperation: ts.noop, +}; - // Load optional module to enable Event Tracing for Windows - // See https://github.com/microsoft/typescript-etw for more information - let etwModule; - try { - const etwModulePath = process.env.TS_ETW_MODULE_PATH ?? "./node_modules/@microsoft/typescript-etw"; +// Load optional module to enable Event Tracing for Windows +// See https://github.com/microsoft/typescript-etw for more information +let etwModule; +try { + const etwModulePath = process.env.TS_ETW_MODULE_PATH ?? "./node_modules/@microsoft/typescript-etw"; - // require() will throw an exception if the module is not found - // It may also return undefined if not installed properly - etwModule = require(etwModulePath); - } - catch (e) { - etwModule = undefined; - } + // require() will throw an exception if the module is not found + // It may also return undefined if not installed properly + etwModule = require(etwModulePath); +} +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; +/** 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; } diff --git a/src/compiler/performance.ts b/src/compiler/performance.ts index 7a450f1899da1..1585a7948b274 100644 --- a/src/compiler/performance.ts +++ b/src/compiler/performance.ts @@ -1,146 +1,146 @@ /*@internal*/ /** Performance measurements for the compiler. */ namespace ts.performance { - let perfHooks: ts.PerformanceHooks | undefined; - // when set, indicates the implementation of `Performance` to use for user timing. - // when unset, indicates user timing is unavailable or disabled. - let performanceImpl: ts.Performance | undefined; +let perfHooks: ts.PerformanceHooks | undefined; +// when set, indicates the implementation of `Performance` to use for user timing. +// when unset, indicates user timing is unavailable or disabled. +let performanceImpl: ts.Performance | undefined; - export interface Timer { - enter(): void; - exit(): void; - } +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 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 - }; +export function createTimer(measureName: string, startMarkName: string, endMarkName: string): Timer { + let enterCount = 0; + return { + enter, + exit + }; - function enter() { - if (++enterCount === 1) { - mark(startMarkName); - } + function enter() { + if (++enterCount === 1) { + mark(startMarkName); } + } - function exit() { - if (--enterCount === 0) { - mark(endMarkName); - measure(measureName, startMarkName, endMarkName); - } - else if (enterCount < 0) { - ts.Debug.fail("enter/exit count does not match."); - } + function exit() { + if (--enterCount === 0) { + mark(endMarkName); + measure(measureName, startMarkName, endMarkName); + } + else if (enterCount < 0) { + ts.Debug.fail("enter/exit count does not match."); } } +} - export const nullTimer: Timer = { enter: ts.noop, exit: ts.noop }; +export const nullTimer: Timer = { enter: ts.noop, exit: ts.noop }; - let enabled = false; - let timeorigin = ts.timestamp(); - const marks = new ts.Map(); - const counts = new ts.Map(); - const durations = new ts.Map(); +let enabled = false; +let timeorigin = ts.timestamp(); +const marks = new ts.Map(); +const counts = new ts.Map(); +const durations = new ts.Map(); - /** - * Marks a performance event. - * - * @param markName The name of the mark. - */ - export function mark(markName: string) { - if (enabled) { - const count = counts.get(markName) ?? 0; - counts.set(markName, count + 1); - marks.set(markName, ts.timestamp()); - performanceImpl?.mark(markName); - } +/** + * Marks a performance event. + * + * @param markName The name of the mark. + */ +export function mark(markName: string) { + if (enabled) { + const count = counts.get(markName) ?? 0; + counts.set(markName, count + 1); + marks.set(markName, ts.timestamp()); + performanceImpl?.mark(markName); } +} - /** - * 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 !== undefined ? marks.get(endMarkName) : undefined) ?? ts.timestamp(); - const start = (startMarkName !== undefined ? marks.get(startMarkName) : undefined) ?? timeorigin; - const previousDuration = durations.get(measureName) || 0; - durations.set(measureName, previousDuration + (end - start)); - performanceImpl?.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 !== undefined ? marks.get(endMarkName) : undefined) ?? ts.timestamp(); + const start = (startMarkName !== undefined ? marks.get(startMarkName) : undefined) ?? timeorigin; + const previousDuration = durations.get(measureName) || 0; + durations.set(measureName, previousDuration + (end - start)); + performanceImpl?.measure(measureName, startMarkName, endMarkName); } +} - /** - * Gets the number of times a marker was encountered. - * - * @param markName The name of the mark. - */ - export function getCount(markName: string) { - return counts.get(markName) || 0; - } +/** + * Gets the number of times a marker was encountered. + * + * @param markName The name of the mark. + */ +export function getCount(markName: string) { + return 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 durations.get(measureName) || 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 durations.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) { - durations.forEach((duration, measureName) => cb(measureName, duration)); - } +/** + * 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) { + durations.forEach((duration, measureName) => cb(measureName, duration)); +} - /** - * Indicates whether the performance API is enabled. - */ - export function isEnabled() { - return enabled; - } +/** + * Indicates whether the performance API is enabled. + */ +export function isEnabled() { + return enabled; +} - /** Enables (and resets) performance measurements for the compiler. */ - export function enable(system: ts.System = ts.sys) { - if (!enabled) { - enabled = true; - perfHooks ||= ts.tryGetNativePerformanceHooks(); - if (perfHooks) { - timeorigin = perfHooks.performance.timeOrigin; - // NodeJS's Web Performance API is currently slower than expected, but we'd still like - // to be able to leverage native trace events when node is run with either `--cpu-prof` - // or `--prof`, if we're running with our own `--generateCpuProfile` flag, or when - // running in debug mode (since its possible to generate a cpu profile while debugging). - if (perfHooks.shouldWriteNativeEvents || system?.cpuProfilingEnabled?.() || system?.debugMode) { - performanceImpl = perfHooks.performance; - } +/** Enables (and resets) performance measurements for the compiler. */ +export function enable(system: ts.System = ts.sys) { + if (!enabled) { + enabled = true; + perfHooks ||= ts.tryGetNativePerformanceHooks(); + if (perfHooks) { + timeorigin = perfHooks.performance.timeOrigin; + // NodeJS's Web Performance API is currently slower than expected, but we'd still like + // to be able to leverage native trace events when node is run with either `--cpu-prof` + // or `--prof`, if we're running with our own `--generateCpuProfile` flag, or when + // running in debug mode (since its possible to generate a cpu profile while debugging). + if (perfHooks.shouldWriteNativeEvents || system?.cpuProfilingEnabled?.() || system?.debugMode) { + performanceImpl = perfHooks.performance; } } - return true; } + return true; +} - /** Disables performance measurements for the compiler. */ - export function disable() { - if (enabled) { - marks.clear(); - counts.clear(); - durations.clear(); - performanceImpl = undefined; - enabled = false; - } +/** Disables performance measurements for the compiler. */ +export function disable() { + if (enabled) { + marks.clear(); + counts.clear(); + durations.clear(); + performanceImpl = undefined; + enabled = false; } } +} diff --git a/src/compiler/performanceCore.ts b/src/compiler/performanceCore.ts index 301bfd67a2aa6..7f0f319801bac 100644 --- a/src/compiler/performanceCore.ts +++ b/src/compiler/performanceCore.ts @@ -1,130 +1,130 @@ /*@internal*/ namespace ts { - // The following definitions provide the minimum compatible support for the Web Performance User Timings API - // between browsers and NodeJS: +// The following definitions provide the minimum compatible support for the Web Performance User Timings API +// between browsers and NodeJS: - export interface PerformanceHooks { - /** Indicates whether we should write native performance events */ - shouldWriteNativeEvents: boolean; - performance: Performance; - PerformanceObserver: PerformanceObserverConstructor; - } +export interface PerformanceHooks { + /** Indicates whether we should write native performance events */ + shouldWriteNativeEvents: boolean; + performance: Performance; + PerformanceObserver: PerformanceObserverConstructor; +} - export interface Performance { - mark(name: string): void; - measure(name: string, startMark?: string, endMark?: string): void; - now(): number; - timeOrigin: number; - } +export interface Performance { + mark(name: string): void; + measure(name: string, startMark?: string, endMark?: string): void; + now(): number; + timeOrigin: number; +} - export interface PerformanceEntry { - name: string; - entryType: string; - startTime: number; - duration: number; - } +export interface PerformanceEntry { + name: string; + entryType: string; + startTime: number; + duration: number; +} - export interface PerformanceObserverEntryList { - getEntries(): PerformanceEntryList; - getEntriesByName(name: string, type?: string): PerformanceEntryList; - getEntriesByType(type: string): PerformanceEntryList; - } +export interface PerformanceObserverEntryList { + getEntries(): PerformanceEntryList; + getEntriesByName(name: string, type?: string): PerformanceEntryList; + getEntriesByType(type: string): PerformanceEntryList; +} - export interface PerformanceObserver { - disconnect(): void; - observe(options: { - entryTypes: readonly ("mark" | "measure")[]; - }): void; - } +export interface PerformanceObserver { + disconnect(): void; + observe(options: { + entryTypes: readonly ("mark" | "measure")[]; + }): void; +} - export type PerformanceObserverConstructor = new (callback: (list: PerformanceObserverEntryList, observer: PerformanceObserver) => void) => PerformanceObserver; - export type PerformanceEntryList = PerformanceEntry[]; +export type PerformanceObserverConstructor = new (callback: (list: PerformanceObserverEntryList, observer: PerformanceObserver) => void) => PerformanceObserver; +export type PerformanceEntryList = PerformanceEntry[]; - // Browser globals for the Web Performance User Timings API - declare const process: any; - declare const performance: Performance | undefined; - declare const PerformanceObserver: PerformanceObserverConstructor | undefined; +// Browser globals for the Web Performance User Timings API +declare const process: any; +declare const performance: Performance | undefined; +declare const PerformanceObserver: PerformanceObserverConstructor | undefined; - // eslint-disable-next-line @typescript-eslint/naming-convention - function hasRequiredAPI(performance: Performance | undefined, PerformanceObserver: PerformanceObserverConstructor | undefined) { - return typeof performance === "object" && - typeof performance.timeOrigin === "number" && - typeof performance.mark === "function" && - typeof performance.measure === "function" && - typeof performance.now === "function" && - typeof PerformanceObserver === "function"; - } +// eslint-disable-next-line @typescript-eslint/naming-convention +function hasRequiredAPI(performance: Performance | undefined, PerformanceObserver: PerformanceObserverConstructor | undefined) { + return typeof performance === "object" && + typeof performance.timeOrigin === "number" && + typeof performance.mark === "function" && + typeof performance.measure === "function" && + typeof performance.now === "function" && + typeof PerformanceObserver === "function"; +} - function tryGetWebPerformanceHooks(): PerformanceHooks | undefined { - if (typeof performance === "object" && - typeof PerformanceObserver === "function" && - hasRequiredAPI(performance, PerformanceObserver)) { - return { - // For now we always write native performance events when running in the browser. We may - // make this conditional in the future if we find that native web performance hooks - // in the browser also slow down compilation. - shouldWriteNativeEvents: true, - performance, - PerformanceObserver - }; - } +function tryGetWebPerformanceHooks(): PerformanceHooks | undefined { + if (typeof performance === "object" && + typeof PerformanceObserver === "function" && + hasRequiredAPI(performance, PerformanceObserver)) { + return { + // For now we always write native performance events when running in the browser. We may + // make this conditional in the future if we find that native web performance hooks + // in the browser also slow down compilation. + shouldWriteNativeEvents: true, + performance, + PerformanceObserver + }; } +} - function tryGetNodePerformanceHooks(): PerformanceHooks | undefined { - if (typeof process !== "undefined" && process.nextTick && !process.browser && typeof module === "object" && typeof require === "function") { - try { - let performance: Performance; - const { performance: nodePerformance, PerformanceObserver } = require("perf_hooks") as typeof import("perf_hooks"); - if (hasRequiredAPI(nodePerformance, PerformanceObserver)) { - performance = nodePerformance; - // There is a bug in Node's performance.measure prior to 12.16.3/13.13.0 that does not - // match the Web Performance API specification. Node's implementation did not allow - // optional `start` and `end` arguments for `performance.measure`. - // See https://github.com/nodejs/node/pull/32651 for more information. - const version = new ts.Version(process.versions.node); - const range = new ts.VersionRange("<12.16.3 || 13 <13.13"); - if (range.test(version)) { - performance = { - get timeOrigin() { return nodePerformance.timeOrigin; }, - now() { return nodePerformance.now(); }, - mark(name) { return nodePerformance.mark(name); }, - measure(name, start = "nodeStart", end?) { - if (end === undefined) { - end = "__performance.measure-fix__"; - nodePerformance.mark(end); - } - nodePerformance.measure(name, start, end); - if (end === "__performance.measure-fix__") { - nodePerformance.clearMarks("__performance.measure-fix__"); - } +function tryGetNodePerformanceHooks(): PerformanceHooks | undefined { + if (typeof process !== "undefined" && process.nextTick && !process.browser && typeof module === "object" && typeof require === "function") { + try { + let performance: Performance; + const { performance: nodePerformance, PerformanceObserver } = require("perf_hooks") as typeof import("perf_hooks"); + if (hasRequiredAPI(nodePerformance, PerformanceObserver)) { + performance = nodePerformance; + // There is a bug in Node's performance.measure prior to 12.16.3/13.13.0 that does not + // match the Web Performance API specification. Node's implementation did not allow + // optional `start` and `end` arguments for `performance.measure`. + // See https://github.com/nodejs/node/pull/32651 for more information. + const version = new ts.Version(process.versions.node); + const range = new ts.VersionRange("<12.16.3 || 13 <13.13"); + if (range.test(version)) { + performance = { + get timeOrigin() { return nodePerformance.timeOrigin; }, + now() { return nodePerformance.now(); }, + mark(name) { return nodePerformance.mark(name); }, + measure(name, start = "nodeStart", end?) { + if (end === undefined) { + end = "__performance.measure-fix__"; + nodePerformance.mark(end); + } + nodePerformance.measure(name, start, end); + if (end === "__performance.measure-fix__") { + nodePerformance.clearMarks("__performance.measure-fix__"); } - }; - } - return { - // By default, only write native events when generating a cpu profile or using the v8 profiler. - shouldWriteNativeEvents: false, - performance, - PerformanceObserver + } }; } - } - catch { - // ignore errors + return { + // By default, only write native events when generating a cpu profile or using the v8 profiler. + shouldWriteNativeEvents: false, + performance, + PerformanceObserver + }; } } + catch { + // ignore errors + } } +} - // Unlike with the native Map/Set 'tryGet' functions in corePublic.ts, we eagerly evaluate these - // since we will need them for `timestamp`, below. - const nativePerformanceHooks = tryGetWebPerformanceHooks() || tryGetNodePerformanceHooks(); - const nativePerformance = nativePerformanceHooks?.performance; +// Unlike with the native Map/Set 'tryGet' functions in corePublic.ts, we eagerly evaluate these +// since we will need them for `timestamp`, below. +const nativePerformanceHooks = tryGetWebPerformanceHooks() || tryGetNodePerformanceHooks(); +const nativePerformance = nativePerformanceHooks?.performance; - export function tryGetNativePerformanceHooks() { - return nativePerformanceHooks; - } +export function tryGetNativePerformanceHooks() { + return nativePerformanceHooks; +} - /** Gets a timestamp with (at least) ms resolution */ - export const timestamp = nativePerformance ? () => nativePerformance.now() : - Date.now ? Date.now : - () => +(new Date()); -} \ No newline at end of file +/** Gets a timestamp with (at least) ms resolution */ +export const timestamp = nativePerformance ? () => nativePerformance.now() : + Date.now ? Date.now : + () => +(new Date()); +} diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 476c605c58c40..c8e795fc8ab11 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1,4246 +1,4246 @@ namespace ts { - export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string | undefined { - return ts.forEachAncestorDirectory(searchPath, ancestor => { - const fileName = ts.combinePaths(ancestor, configName); - return fileExists(fileName) ? fileName : undefined; - }); - } - - export function resolveTripleslashReference(moduleName: string, containingFile: string): string { - const basePath = ts.getDirectoryPath(containingFile); - const referencedFileName = ts.isRootedDiskPath(moduleName) ? moduleName : ts.combinePaths(basePath, moduleName); - return ts.normalizePath(referencedFileName); - } - - /* @internal */ - export function computeCommonSourceDirectoryOfFilenames(fileNames: readonly string[], currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName): string { - let commonPathComponents: string[] | undefined; - const failed = ts.forEach(fileNames, sourceFile => { - // Each file contributes into common source file path - const sourcePathComponents = ts.getNormalizedPathComponents(sourceFile, currentDirectory); - sourcePathComponents.pop(); // The base file name is not part of the common directory path +export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string | undefined { + return ts.forEachAncestorDirectory(searchPath, ancestor => { + const fileName = ts.combinePaths(ancestor, configName); + return fileExists(fileName) ? fileName : undefined; + }); +} - if (!commonPathComponents) { - // first file - commonPathComponents = sourcePathComponents; - return; - } +export function resolveTripleslashReference(moduleName: string, containingFile: string): string { + const basePath = ts.getDirectoryPath(containingFile); + const referencedFileName = ts.isRootedDiskPath(moduleName) ? moduleName : ts.combinePaths(basePath, moduleName); + return ts.normalizePath(referencedFileName); +} - 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; - } +/* @internal */ +export function computeCommonSourceDirectoryOfFilenames(fileNames: readonly string[], currentDirectory: string, getCanonicalFileName: ts.GetCanonicalFileName): string { + let commonPathComponents: string[] | undefined; + const failed = ts.forEach(fileNames, sourceFile => { + // Each file contributes into common source file path + const sourcePathComponents = ts.getNormalizedPathComponents(sourceFile, currentDirectory); + sourcePathComponents.pop(); // The base file name is not part of the common directory path + + if (!commonPathComponents) { + // first file + commonPathComponents = sourcePathComponents; + return; + } - // New common path found that is 0 -> i-1 - commonPathComponents.length = i; - break; + 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; } - } - // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents - if (sourcePathComponents.length < commonPathComponents.length) { - commonPathComponents.length = sourcePathComponents.length; + // New common path found that is 0 -> i-1 + commonPathComponents.length = i; + break; } - }); - - // 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; + // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents + if (sourcePathComponents.length < commonPathComponents.length) { + commonPathComponents.length = sourcePathComponents.length; } + }); - return ts.getPathFromPathComponents(commonPathComponents); + // A common path can not be found when paths span multiple drives on windows, for example + if (failed) { + return ""; } - interface OutputFingerprint { - hash: string; - byteOrderMark: boolean; - mtime: Date; + if (!commonPathComponents) { // Can happen when all input files are .d.ts files + return currentDirectory; } - export function createCompilerHost(options: ts.CompilerOptions, setParentNodes?: boolean): ts.CompilerHost { - return createCompilerHostWorker(options, setParentNodes); + return ts.getPathFromPathComponents(commonPathComponents); +} + +interface OutputFingerprint { + hash: string; + byteOrderMark: boolean; + mtime: Date; +} + +export function createCompilerHost(options: ts.CompilerOptions, setParentNodes?: boolean): ts.CompilerHost { + return createCompilerHostWorker(options, setParentNodes); +} + +/*@internal*/ +// TODO(shkamat): update this after reworking ts build API +export function createCompilerHostWorker(options: ts.CompilerOptions, setParentNodes?: boolean, system = ts.sys): ts.CompilerHost { + const existingDirectories = new ts.Map(); + const getCanonicalFileName = ts.createGetCanonicalFileName(system.useCaseSensitiveFileNames); + const computeHash = ts.maybeBind(system, system.createHash) || ts.generateDjb2Hash; + function getSourceFile(fileName: string, languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, onError?: (message: string) => void): ts.SourceFile | undefined { + let text: string | undefined; + try { + ts.performance.mark("beforeIORead"); + text = compilerHost.readFile(fileName); + ts.performance.mark("afterIORead"); + ts.performance.measure("I/O Read", "beforeIORead", "afterIORead"); + } + catch (e) { + if (onError) { + onError(e.message); + } + text = ""; + } + return text !== undefined ? ts.createSourceFile(fileName, text, languageVersionOrOptions, setParentNodes) : undefined; } - /*@internal*/ - // TODO(shkamat): update this after reworking ts build API - export function createCompilerHostWorker(options: ts.CompilerOptions, setParentNodes?: boolean, system = ts.sys): ts.CompilerHost { - const existingDirectories = new ts.Map(); - const getCanonicalFileName = ts.createGetCanonicalFileName(system.useCaseSensitiveFileNames); - const computeHash = ts.maybeBind(system, system.createHash) || ts.generateDjb2Hash; - function getSourceFile(fileName: string, languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, onError?: (message: string) => void): ts.SourceFile | undefined { - let text: string | undefined; - try { - ts.performance.mark("beforeIORead"); - text = compilerHost.readFile(fileName); - ts.performance.mark("afterIORead"); - ts.performance.measure("I/O Read", "beforeIORead", "afterIORead"); - } - catch (e) { - if (onError) { - onError(e.message); - } - text = ""; - } - return text !== undefined ? ts.createSourceFile(fileName, text, languageVersionOrOptions, setParentNodes) : undefined; + function directoryExists(directoryPath: string): boolean { + if (existingDirectories.has(directoryPath)) { + return true; } - - 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; + 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 { - ts.performance.mark("beforeIOWrite"); + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { + try { + ts.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. - ts.writeFileEnsuringDirectories(fileName, data, writeByteOrderMark, (path, data, writeByteOrderMark) => writeFileWorker(path, data, writeByteOrderMark), path => (compilerHost.createDirectory || system.createDirectory)(path), path => directoryExists(path)); - ts.performance.mark("afterIOWrite"); - ts.performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - if (onError) { - onError(e.message); - } + // NOTE: If patchWriteFileEnsuringDirectory has been called, + // the system.writeFile will do its own directory creation and + // the ensureDirectoriesExist call will always be redundant. + ts.writeFileEnsuringDirectories(fileName, data, writeByteOrderMark, (path, data, writeByteOrderMark) => writeFileWorker(path, data, writeByteOrderMark), path => (compilerHost.createDirectory || system.createDirectory)(path), path => directoryExists(path)); + ts.performance.mark("afterIOWrite"); + ts.performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + if (onError) { + onError(e.message); } } + } - let outputFingerprints: ts.ESMap; - function writeFileWorker(fileName: string, data: string, writeByteOrderMark: boolean) { - if (!ts.isWatchSet(options) || !system.getModifiedTime) { - system.writeFile(fileName, data, writeByteOrderMark); - return; - } + let outputFingerprints: ts.ESMap; + function writeFileWorker(fileName: string, data: string, writeByteOrderMark: boolean) { + if (!ts.isWatchSet(options) || !system.getModifiedTime) { + system.writeFile(fileName, data, writeByteOrderMark); + return; + } - if (!outputFingerprints) { - outputFingerprints = new ts.Map(); - } + if (!outputFingerprints) { + outputFingerprints = new ts.Map(); + } - const hash = computeHash(data); - const mtimeBefore = system.getModifiedTime(fileName); + const hash = computeHash(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 (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); + system.writeFile(fileName, data, writeByteOrderMark); - const mtimeAfter = system.getModifiedTime(fileName) || ts.missingFileModifiedTime; + const mtimeAfter = system.getModifiedTime(fileName) || ts.missingFileModifiedTime; - outputFingerprints.set(fileName, { - hash, - byteOrderMark: writeByteOrderMark, - mtime: mtimeAfter - }); - } + outputFingerprints.set(fileName, { + hash, + byteOrderMark: writeByteOrderMark, + mtime: mtimeAfter + }); + } - function getDefaultLibLocation(): string { - return ts.getDirectoryPath(ts.normalizePath(system.getExecutingFilePath())); - } + function getDefaultLibLocation(): string { + return ts.getDirectoryPath(ts.normalizePath(system.getExecutingFilePath())); + } - const newLine = ts.getNewLineCharacter(options, () => system.newLine); - const realpath = system.realpath && ((path: string) => system.realpath!(path)); - const compilerHost: ts.CompilerHost = { - getSourceFile, - getDefaultLibLocation, - getDefaultLibFileName: options => ts.combinePaths(getDefaultLibLocation(), ts.getDefaultLibFileName(options)), - writeFile, - getCurrentDirectory: ts.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: ts.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?: ts.WriteFileCallback; - } - - /*@internal*/ - export function changeCompilerHostLikeToUseCache(host: CompilerHostLikeForCache, toPath: (fileName: string) => ts.Path, getSourceFile?: ts.CompilerHost["getSourceFile"]) { - const originalReadFile = host.readFile; - const originalFileExists = host.fileExists; - const originalDirectoryExists = host.directoryExists; - const originalCreateDirectory = host.createDirectory; - const originalWriteFile = host.writeFile; - const readFileCache = new ts.Map(); - const fileExistsCache = new ts.Map(); - const directoryExistsCache = new ts.Map(); - const sourceFileCache = new ts.Map(); - - 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: ts.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 (!ts.fileExtensionIs(fileName, ts.Extension.Json) && !ts.isBuildInfoFile(fileName)) { - return originalReadFile.call(host, fileName); - } + const newLine = ts.getNewLineCharacter(options, () => system.newLine); + const realpath = system.realpath && ((path: string) => system.realpath!(path)); + const compilerHost: ts.CompilerHost = { + getSourceFile, + getDefaultLibLocation, + getDefaultLibFileName: options => ts.combinePaths(getDefaultLibLocation(), ts.getDefaultLibFileName(options)), + writeFile, + getCurrentDirectory: ts.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: ts.maybeBind(system, system.createHash) + }; + return compilerHost; +} - return setReadFileCache(key, fileName); - }; +/*@internal*/ +interface CompilerHostLikeForCache { + fileExists(fileName: string): boolean; + readFile(fileName: string, encoding?: string): string | undefined; + directoryExists?(directory: string): boolean; + createDirectory?(directory: string): void; + writeFile?: ts.WriteFileCallback; +} - const getSourceFileWithCache: ts.CompilerHost["getSourceFile"] | undefined = getSourceFile ? (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { +/*@internal*/ +export function changeCompilerHostLikeToUseCache(host: CompilerHostLikeForCache, toPath: (fileName: string) => ts.Path, getSourceFile?: ts.CompilerHost["getSourceFile"]) { + const originalReadFile = host.readFile; + const originalFileExists = host.fileExists; + const originalDirectoryExists = host.directoryExists; + const originalCreateDirectory = host.createDirectory; + const originalWriteFile = host.writeFile; + const readFileCache = new ts.Map(); + const fileExistsCache = new ts.Map(); + const directoryExistsCache = new ts.Map(); + const sourceFileCache = new ts.Map(); + + 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: ts.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 (!ts.fileExtensionIs(fileName, ts.Extension.Json) && !ts.isBuildInfoFile(fileName)) { + return originalReadFile.call(host, fileName); + } + + return setReadFileCache(key, fileName); + }; + + const getSourceFileWithCache: ts.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 && (ts.isDeclarationFileName(fileName) || ts.fileExtensionIs(fileName, ts.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, ...rest) => { const key = toPath(fileName); - const value = sourceFileCache.get(key); - if (value) - return value; + fileExistsCache.delete(key); - const sourceFile = getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); - if (sourceFile && (ts.isDeclarationFileName(fileName) || ts.fileExtensionIs(fileName, ts.Extension.Json))) { - sourceFileCache.set(key, sourceFile); + const value = readFileCache.get(key); + if (value !== undefined && value !== data) { + readFileCache.delete(key); + sourceFileCache.delete(key); } - return sourceFile; - } : undefined; + else if (getSourceFileWithCache) { + const sourceFile = sourceFileCache.get(key); + if (sourceFile && sourceFile.text !== data) { + sourceFileCache.delete(key); + } + } + originalWriteFile.call(host, fileName, data, ...rest); + }; + } - // fileExists for any kind of extension - host.fileExists = fileName => { - const key = toPath(fileName); - const value = fileExistsCache.get(key); + // directoryExists + if (originalDirectoryExists && originalCreateDirectory) { + host.directoryExists = directory => { + const key = toPath(directory); + const value = directoryExistsCache.get(key); if (value !== undefined) return value; - const newValue = originalFileExists.call(host, fileName); - fileExistsCache.set(key, !!newValue); + const newValue = originalDirectoryExists.call(host, directory); + directoryExistsCache.set(key, !!newValue); return newValue; }; - if (originalWriteFile) { - host.writeFile = (fileName, data, ...rest) => { - 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, ...rest); - }; - } - - // 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); - }; - } - - return { - originalReadFile, - originalFileExists, - originalDirectoryExists, - originalCreateDirectory, - originalWriteFile, - getSourceFileWithCache, - readFileWithCache + host.createDirectory = directory => { + const key = toPath(directory); + directoryExistsCache.delete(key); + originalCreateDirectory.call(host, directory); }; } - export function getPreEmitDiagnostics(program: ts.Program, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; - /*@internal*/ export function getPreEmitDiagnostics(program: ts.BuilderProgram, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; // eslint-disable-line @typescript-eslint/unified-signatures - export function getPreEmitDiagnostics(program: ts.Program | ts.BuilderProgram, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { - let diagnostics: ts.Diagnostic[] | undefined; - diagnostics = ts.addRange(diagnostics, program.getConfigFileParsingDiagnostics()); - diagnostics = ts.addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken)); - diagnostics = ts.addRange(diagnostics, program.getSyntacticDiagnostics(sourceFile, cancellationToken)); - diagnostics = ts.addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken)); - diagnostics = ts.addRange(diagnostics, program.getSemanticDiagnostics(sourceFile, cancellationToken)); - if (ts.getEmitDeclarations(program.getCompilerOptions())) { - diagnostics = ts.addRange(diagnostics, program.getDeclarationDiagnostics(sourceFile, cancellationToken)); - } - return ts.sortAndDeduplicateDiagnostics(diagnostics || ts.emptyArray); - } + return { + originalReadFile, + originalFileExists, + originalDirectoryExists, + originalCreateDirectory, + originalWriteFile, + getSourceFileWithCache, + readFileWithCache + }; +} - export interface FormatDiagnosticsHost { - getCurrentDirectory(): string; - getCanonicalFileName(fileName: string): string; - getNewLine(): string; +export function getPreEmitDiagnostics(program: ts.Program, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; +/*@internal*/ export function getPreEmitDiagnostics(program: ts.BuilderProgram, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[]; // eslint-disable-line @typescript-eslint/unified-signatures +export function getPreEmitDiagnostics(program: ts.Program | ts.BuilderProgram, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { + let diagnostics: ts.Diagnostic[] | undefined; + diagnostics = ts.addRange(diagnostics, program.getConfigFileParsingDiagnostics()); + diagnostics = ts.addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken)); + diagnostics = ts.addRange(diagnostics, program.getSyntacticDiagnostics(sourceFile, cancellationToken)); + diagnostics = ts.addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken)); + diagnostics = ts.addRange(diagnostics, program.getSemanticDiagnostics(sourceFile, cancellationToken)); + if (ts.getEmitDeclarations(program.getCompilerOptions())) { + diagnostics = ts.addRange(diagnostics, program.getDeclarationDiagnostics(sourceFile, cancellationToken)); } + return ts.sortAndDeduplicateDiagnostics(diagnostics || ts.emptyArray); +} - export function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], host: FormatDiagnosticsHost): string { - let output = ""; +export interface FormatDiagnosticsHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + getNewLine(): string; +} - for (const diagnostic of diagnostics) { - output += formatDiagnostic(diagnostic, host); - } - return output; - } +export function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], host: FormatDiagnosticsHost): string { + let output = ""; - export function formatDiagnostic(diagnostic: ts.Diagnostic, host: FormatDiagnosticsHost): string { - const errorMessage = `${ts.diagnosticCategoryName(diagnostic)} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; + for (const diagnostic of diagnostics) { + output += formatDiagnostic(diagnostic, host); + } + return output; +} - if (diagnostic.file) { - const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!); // TODO: GH#18217 - const fileName = diagnostic.file.fileName; - const relativeFileName = ts.convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); - return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage; - } +export function formatDiagnostic(diagnostic: ts.Diagnostic, host: FormatDiagnosticsHost): string { + const errorMessage = `${ts.diagnosticCategoryName(diagnostic)} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; - return errorMessage; + if (diagnostic.file) { + const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!); // TODO: GH#18217 + const fileName = diagnostic.file.fileName; + const relativeFileName = ts.convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); + return `${relativeFileName}(${line + 1},${character + 1}): ` + 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: ts.DiagnosticCategory): ForegroundColorEscapeSequences { - switch (category) { - case ts.DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; - case ts.DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; - case ts.DiagnosticCategory.Suggestion: return ts.Debug.fail("Should never get an Info diagnostic on the command line."); - case ts.DiagnosticCategory.Message: return ForegroundColorEscapeSequences.Blue; - } - } + return errorMessage; +} - /** @internal */ - export function formatColorAndReset(text: string, formatStyle: string) { - return formatStyle + text + resetEscapeSequence; +/** @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: ts.DiagnosticCategory): ForegroundColorEscapeSequences { + switch (category) { + case ts.DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; + case ts.DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; + case ts.DiagnosticCategory.Suggestion: return ts.Debug.fail("Should never get an Info diagnostic on the command line."); + case ts.DiagnosticCategory.Message: return ForegroundColorEscapeSequences.Blue; } +} + +/** @internal */ +export function formatColorAndReset(text: string, formatStyle: string) { + return formatStyle + text + resetEscapeSequence; +} - function formatCodeSpan(file: ts.SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: FormatDiagnosticsHost) { - const { line: firstLine, character: firstLineChar } = ts.getLineAndCharacterOfPosition(file, start); - const { line: lastLine, character: lastLineChar } = ts.getLineAndCharacterOfPosition(file, start + length); - const lastLineInFile = ts.getLineAndCharacterOfPosition(file, file.text.length).line; +function formatCodeSpan(file: ts.SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: FormatDiagnosticsHost) { + const { line: firstLine, character: firstLineChar } = ts.getLineAndCharacterOfPosition(file, start); + const { line: lastLine, character: lastLineChar } = ts.getLineAndCharacterOfPosition(file, start + length); + const lastLineInFile = ts.getLineAndCharacterOfPosition(file, file.text.length).line; - const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; - let gutterWidth = (lastLine + 1 + "").length; - if (hasMoreThanFiveLines) { - gutterWidth = Math.max(ellipsis.length, gutterWidth); - } + 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(ts.padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); - i = lastLine - 1; - } + 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(ts.padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); + i = lastLine - 1; + } - const lineStart = ts.getPositionOfLineAndCharacter(file, i, 0); - const lineEnd = i < lastLineInFile ? ts.getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; - let lineContent = file.text.slice(lineStart, lineEnd); - lineContent = ts.trimStringEnd(lineContent); // trim from end - lineContent = lineContent.replace(/\t/g, " "); // convert tabs to single spaces + const lineStart = ts.getPositionOfLineAndCharacter(file, i, 0); + const lineEnd = i < lastLineInFile ? ts.getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; + let lineContent = file.text.slice(lineStart, lineEnd); + lineContent = ts.trimStringEnd(lineContent); // trim from end + lineContent = lineContent.replace(/\t/g, " "); // convert tabs to single spaces - // Output the gutter and the actual contents of the line. - context += indent + formatColorAndReset(ts.padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; - context += lineContent + host.getNewLine(); + // Output the gutter and the actual contents of the line. + context += indent + formatColorAndReset(ts.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(ts.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; + // Output the gutter and the error span for the line using tildes. + context += indent + formatColorAndReset(ts.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; + 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, "~"); } - return context; + context += resetEscapeSequence; } + return context; +} - /* @internal */ - export function formatLocation(file: ts.SourceFile, start: number, host: FormatDiagnosticsHost, color = formatColorAndReset) { - const { line: firstLine, character: firstLineChar } = ts.getLineAndCharacterOfPosition(file, start); // TODO: GH#18217 - const relativeFileName = host ? ts.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; - } +/* @internal */ +export function formatLocation(file: ts.SourceFile, start: number, host: FormatDiagnosticsHost, color = formatColorAndReset) { + const { line: firstLine, character: firstLineChar } = ts.getLineAndCharacterOfPosition(file, start); // TODO: GH#18217 + const relativeFileName = host ? ts.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 ts.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 += " - "; - } +export function formatDiagnosticsWithColorAndContext(diagnostics: readonly ts.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(ts.diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); - output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); - output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); + output += formatColorAndReset(ts.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(); - 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 - } + 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(); + 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(); } + return output; +} - export function flattenDiagnosticMessageText(diag: string | ts.DiagnosticMessageChain | undefined, newLine: string, indent = 0): string { - if (ts.isString(diag)) { - return diag; - } - else if (diag === undefined) { - return ""; - } - let result = ""; - if (indent) { - result += newLine; +export function flattenDiagnosticMessageText(diag: string | ts.DiagnosticMessageChain | undefined, newLine: string, indent = 0): string { + if (ts.isString(diag)) { + return diag; + } + else if (diag === undefined) { + return ""; + } + let result = ""; + if (indent) { + result += newLine; - for (let i = 0; i < indent; i++) { - result += " "; - } + 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; } + return result; +} - /* @internal */ - export function loadWithTypeDirectiveCache(names: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, containingFileMode: ts.SourceFile["impliedNodeFormat"], loader: (name: string, containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, resolutionMode: ts.SourceFile["impliedNodeFormat"]) => T): T[] { - if (names.length === 0) { - return []; +/* @internal */ +export function loadWithTypeDirectiveCache(names: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, containingFileMode: ts.SourceFile["impliedNodeFormat"], loader: (name: string, containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, resolutionMode: ts.SourceFile["impliedNodeFormat"]) => T): T[] { + if (names.length === 0) { + return []; + } + const resolutions: T[] = []; + const cache = new ts.Map(); + for (const name of names) { + let result: T; + const mode = getModeForFileReference(name, containingFileMode); + // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. + const strName = ts.isString(name) ? name : name.fileName.toLowerCase(); + const cacheKey = mode !== undefined ? `${mode}|${strName}` : strName; + if (cache.has(cacheKey)) { + result = cache.get(cacheKey)!; } - const resolutions: T[] = []; - const cache = new ts.Map(); - for (const name of names) { - let result: T; - const mode = getModeForFileReference(name, containingFileMode); - // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. - const strName = ts.isString(name) ? name : name.fileName.toLowerCase(); - const cacheKey = mode !== undefined ? `${mode}|${strName}` : strName; - if (cache.has(cacheKey)) { - result = cache.get(cacheKey)!; - } - else { - cache.set(cacheKey, result = loader(strName, containingFile, redirectedReference, mode)); - } - resolutions.push(result); + else { + cache.set(cacheKey, result = loader(strName, containingFile, redirectedReference, mode)); } - return resolutions; + resolutions.push(result); } + return resolutions; +} - /* @internal */ - interface SourceFileImportsList { - imports: ts.SourceFile["imports"]; - moduleAugmentations: ts.SourceFile["moduleAugmentations"]; - impliedNodeFormat?: ts.SourceFile["impliedNodeFormat"]; - } - ; +/* @internal */ +interface SourceFileImportsList { + imports: ts.SourceFile["imports"]; + moduleAugmentations: ts.SourceFile["moduleAugmentations"]; + impliedNodeFormat?: ts.SourceFile["impliedNodeFormat"]; +} +; - /* @internal */ - export function getModeForFileReference(ref: ts.FileReference | string, containingFileMode: ts.SourceFile["impliedNodeFormat"]) { - return (ts.isString(ref) ? containingFileMode : ref.resolutionMode) || containingFileMode; - } +/* @internal */ +export function getModeForFileReference(ref: ts.FileReference | string, containingFileMode: ts.SourceFile["impliedNodeFormat"]) { + return (ts.isString(ref) ? containingFileMode : ref.resolutionMode) || containingFileMode; +} - /* @internal */ - export function getModeForResolutionAtIndex(file: SourceFileImportsList, index: number) { - if (file.impliedNodeFormat === undefined) - return undefined; - // we ensure all elements of file.imports and file.moduleAugmentations have the relevant parent pointers set during program setup, - // so it's safe to use them even pre-bind - return getModeForUsageLocation(file, getModuleNameStringLiteralAt(file, index)); - } +/* @internal */ +export function getModeForResolutionAtIndex(file: SourceFileImportsList, index: number) { + if (file.impliedNodeFormat === undefined) + return undefined; + // we ensure all elements of file.imports and file.moduleAugmentations have the relevant parent pointers set during program setup, + // so it's safe to use them even pre-bind + return getModeForUsageLocation(file, getModuleNameStringLiteralAt(file, index)); +} - /* @internal */ - export function isExclusivelyTypeOnlyImportOrExport(decl: ts.ImportDeclaration | ts.ExportDeclaration) { - if (ts.isExportDeclaration(decl)) { - return decl.isTypeOnly; - } - if (decl.importClause?.isTypeOnly) { - return true; - } - return false; +/* @internal */ +export function isExclusivelyTypeOnlyImportOrExport(decl: ts.ImportDeclaration | ts.ExportDeclaration) { + if (ts.isExportDeclaration(decl)) { + return decl.isTypeOnly; + } + if (decl.importClause?.isTypeOnly) { + return true; } + return false; +} - /* @internal */ - export function getModeForUsageLocation(file: { - impliedNodeFormat?: ts.SourceFile["impliedNodeFormat"]; - }, usage: ts.StringLiteralLike) { - if (file.impliedNodeFormat === undefined) - return undefined; - if ((ts.isImportDeclaration(usage.parent) || ts.isExportDeclaration(usage.parent))) { - const isTypeOnly = isExclusivelyTypeOnlyImportOrExport(usage.parent); - if (isTypeOnly) { - const override = getResolutionModeOverrideForClause(usage.parent.assertClause); - if (override) { - return override; - } - } - } - if (usage.parent.parent && ts.isImportTypeNode(usage.parent.parent)) { - const override = getResolutionModeOverrideForClause(usage.parent.parent.assertions?.assertClause); +/* @internal */ +export function getModeForUsageLocation(file: { + impliedNodeFormat?: ts.SourceFile["impliedNodeFormat"]; +}, usage: ts.StringLiteralLike) { + if (file.impliedNodeFormat === undefined) + return undefined; + if ((ts.isImportDeclaration(usage.parent) || ts.isExportDeclaration(usage.parent))) { + const isTypeOnly = isExclusivelyTypeOnlyImportOrExport(usage.parent); + if (isTypeOnly) { + const override = getResolutionModeOverrideForClause(usage.parent.assertClause); if (override) { return override; } } - if (file.impliedNodeFormat !== ts.ModuleKind.ESNext) { - // in cjs files, import call expressions are esm format, otherwise everything is cjs - return ts.isImportCall(ts.walkUpParenthesizedExpressions(usage.parent)) ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; + } + if (usage.parent.parent && ts.isImportTypeNode(usage.parent.parent)) { + const override = getResolutionModeOverrideForClause(usage.parent.parent.assertions?.assertClause); + if (override) { + return override; } - // in esm files, import=require statements are cjs format, otherwise everything is esm - // imports are only parent'd up to their containing declaration/expression, so access farther parents with care - const exprParentParent = ts.walkUpParenthesizedExpressions(usage.parent)?.parent; - return exprParentParent && ts.isImportEqualsDeclaration(exprParentParent) ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext; } + if (file.impliedNodeFormat !== ts.ModuleKind.ESNext) { + // in cjs files, import call expressions are esm format, otherwise everything is cjs + return ts.isImportCall(ts.walkUpParenthesizedExpressions(usage.parent)) ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; + } + // in esm files, import=require statements are cjs format, otherwise everything is esm + // imports are only parent'd up to their containing declaration/expression, so access farther parents with care + const exprParentParent = ts.walkUpParenthesizedExpressions(usage.parent)?.parent; + return exprParentParent && ts.isImportEqualsDeclaration(exprParentParent) ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext; +} - /* @internal */ - export function getResolutionModeOverrideForClause(clause: ts.AssertClause | undefined, grammarErrorOnNode?: (node: ts.Node, diagnostic: ts.DiagnosticMessage) => void) { - if (!clause) - return undefined; - if (ts.length(clause.elements) !== 1) { - grammarErrorOnNode?.(clause, ts.Diagnostics.Type_import_assertions_should_have_exactly_one_key_resolution_mode_with_value_import_or_require); - return undefined; - } - const elem = clause.elements[0]; - if (!ts.isStringLiteralLike(elem.name)) - return undefined; - if (elem.name.text !== "resolution-mode") { - grammarErrorOnNode?.(elem.name, ts.Diagnostics.resolution_mode_is_the_only_valid_key_for_type_import_assertions); - return undefined; - } - if (!ts.isStringLiteralLike(elem.value)) - return undefined; - if (elem.value.text !== "import" && elem.value.text !== "require") { - grammarErrorOnNode?.(elem.value, ts.Diagnostics.resolution_mode_should_be_either_require_or_import); - return undefined; - } - return elem.value.text === "import" ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; +/* @internal */ +export function getResolutionModeOverrideForClause(clause: ts.AssertClause | undefined, grammarErrorOnNode?: (node: ts.Node, diagnostic: ts.DiagnosticMessage) => void) { + if (!clause) + return undefined; + if (ts.length(clause.elements) !== 1) { + grammarErrorOnNode?.(clause, ts.Diagnostics.Type_import_assertions_should_have_exactly_one_key_resolution_mode_with_value_import_or_require); + return undefined; + } + const elem = clause.elements[0]; + if (!ts.isStringLiteralLike(elem.name)) + return undefined; + if (elem.name.text !== "resolution-mode") { + grammarErrorOnNode?.(elem.name, ts.Diagnostics.resolution_mode_is_the_only_valid_key_for_type_import_assertions); + return undefined; } + if (!ts.isStringLiteralLike(elem.value)) + return undefined; + if (elem.value.text !== "import" && elem.value.text !== "require") { + grammarErrorOnNode?.(elem.value, ts.Diagnostics.resolution_mode_should_be_either_require_or_import); + return undefined; + } + return elem.value.text === "import" ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; +} - /* @internal */ - export function loadWithModeAwareCache(names: string[], containingFile: ts.SourceFile, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined, loader: (name: string, resolverMode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined) => T): T[] { - if (names.length === 0) { - return []; +/* @internal */ +export function loadWithModeAwareCache(names: string[], containingFile: ts.SourceFile, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined, loader: (name: string, resolverMode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined) => T): T[] { + if (names.length === 0) { + return []; + } + const resolutions: T[] = []; + const cache = new ts.Map(); + let i = 0; + for (const name of names) { + let result: T; + const mode = getModeForResolutionAtIndex(containingFile, i); + i++; + const cacheKey = mode !== undefined ? `${mode}|${name}` : name; + if (cache.has(cacheKey)) { + result = cache.get(cacheKey)!; } - const resolutions: T[] = []; - const cache = new ts.Map(); - let i = 0; - for (const name of names) { - let result: T; - const mode = getModeForResolutionAtIndex(containingFile, i); - i++; - const cacheKey = mode !== undefined ? `${mode}|${name}` : name; - if (cache.has(cacheKey)) { - result = cache.get(cacheKey)!; - } - else { - cache.set(cacheKey, result = loader(name, mode, containingFileName, redirectedReference)); - } - resolutions.push(result); + else { + cache.set(cacheKey, result = loader(name, mode, containingFileName, redirectedReference)); } - return resolutions; + resolutions.push(result); } + return resolutions; +} - /* @internal */ - export function forEachResolvedProjectReference(resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, cb: (resolvedProjectReference: ts.ResolvedProjectReference, parent: ts.ResolvedProjectReference | undefined) => T | undefined): T | undefined { - return forEachProjectReference(/*projectReferences*/ undefined, resolvedProjectReferences, (resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent)); - } +/* @internal */ +export function forEachResolvedProjectReference(resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, cb: (resolvedProjectReference: ts.ResolvedProjectReference, parent: ts.ResolvedProjectReference | undefined) => T | undefined): T | undefined { + return forEachProjectReference(/*projectReferences*/ undefined, resolvedProjectReferences, (resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent)); +} - function forEachProjectReference(projectReferences: readonly ts.ProjectReference[] | undefined, resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, cbResolvedRef: (resolvedRef: ts.ResolvedProjectReference | undefined, parent: ts.ResolvedProjectReference | undefined, index: number) => T | undefined, cbRef?: (projectReferences: readonly ts.ProjectReference[] | undefined, parent: ts.ResolvedProjectReference | undefined) => T | undefined): T | undefined { - let seenResolvedRefs: ts.Set | undefined; +function forEachProjectReference(projectReferences: readonly ts.ProjectReference[] | undefined, resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, cbResolvedRef: (resolvedRef: ts.ResolvedProjectReference | undefined, parent: ts.ResolvedProjectReference | undefined, index: number) => T | undefined, cbRef?: (projectReferences: readonly ts.ProjectReference[] | undefined, parent: ts.ResolvedProjectReference | undefined) => T | undefined): T | undefined { + let seenResolvedRefs: ts.Set | undefined; - return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined); + return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined); - function worker(projectReferences: readonly ts.ProjectReference[] | undefined, resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, parent: ts.ResolvedProjectReference | undefined): T | undefined { + function worker(projectReferences: readonly ts.ProjectReference[] | undefined, resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined, parent: ts.ResolvedProjectReference | undefined): T | undefined { - // Visit project references first - if (cbRef) { - const result = cbRef(projectReferences, parent); - if (result) - return result; - } + // Visit project references first + if (cbRef) { + const result = cbRef(projectReferences, parent); + if (result) + return result; + } - return ts.forEach(resolvedProjectReferences, (resolvedRef, index) => { - if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) { - // ignore recursives - return undefined; - } + return ts.forEach(resolvedProjectReferences, (resolvedRef, index) => { + if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) { + // ignore recursives + return undefined; + } - const result = cbResolvedRef(resolvedRef, parent, index); - if (result || !resolvedRef) - return result; - (seenResolvedRefs ||= new ts.Set()).add(resolvedRef.sourceFile.path); - return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef); - }); - } + const result = cbResolvedRef(resolvedRef, parent, index); + if (result || !resolvedRef) + return result; + (seenResolvedRefs ||= new ts.Set()).add(resolvedRef.sourceFile.path); + return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef); + }); } +} - /* @internal */ - export const inferredTypesContainingFile = "__inferred type names__.ts"; +/* @internal */ +export const inferredTypesContainingFile = "__inferred type names__.ts"; - interface DiagnosticCache { - perFile?: ts.ESMap; - allDiagnostics?: readonly T[]; - } +interface DiagnosticCache { + perFile?: ts.ESMap; + allDiagnostics?: readonly T[]; +} - /*@internal*/ - export function isReferencedFile(reason: ts.FileIncludeReason | undefined): reason is ts.ReferencedFile { - switch (reason?.kind) { - case ts.FileIncludeKind.Import: - case ts.FileIncludeKind.ReferenceFile: - case ts.FileIncludeKind.TypeReferenceDirective: - case ts.FileIncludeKind.LibReferenceDirective: - return true; - default: - return false; - } +/*@internal*/ +export function isReferencedFile(reason: ts.FileIncludeReason | undefined): reason is ts.ReferencedFile { + switch (reason?.kind) { + case ts.FileIncludeKind.Import: + case ts.FileIncludeKind.ReferenceFile: + case ts.FileIncludeKind.TypeReferenceDirective: + case ts.FileIncludeKind.LibReferenceDirective: + return true; + default: + return false; } +} - /*@internal*/ - export interface ReferenceFileLocation { - file: ts.SourceFile; - pos: number; - end: number; - packageId: ts.PackageId | undefined; - } +/*@internal*/ +export interface ReferenceFileLocation { + file: ts.SourceFile; + pos: number; + end: number; + packageId: ts.PackageId | undefined; +} - /*@internal*/ - export interface SyntheticReferenceFileLocation { - file: ts.SourceFile; - packageId: ts.PackageId | undefined; - text: string; - } +/*@internal*/ +export interface SyntheticReferenceFileLocation { + file: ts.SourceFile; + packageId: ts.PackageId | undefined; + text: string; +} - /*@internal*/ - export function isReferenceFileLocation(location: ReferenceFileLocation | SyntheticReferenceFileLocation): location is ReferenceFileLocation { - return (location as ReferenceFileLocation).pos !== undefined; - } +/*@internal*/ +export function isReferenceFileLocation(location: ReferenceFileLocation | SyntheticReferenceFileLocation): location is ReferenceFileLocation { + return (location as ReferenceFileLocation).pos !== undefined; +} - /*@internal*/ - export function getReferencedFileLocation(getSourceFileByPath: (path: ts.Path) => ts.SourceFile | undefined, ref: ts.ReferencedFile): ReferenceFileLocation | SyntheticReferenceFileLocation { - const file = ts.Debug.checkDefined(getSourceFileByPath(ref.file)); - const { kind, index } = ref; - let pos: number | undefined, end: number | undefined, packageId: ts.PackageId | undefined, resolutionMode: ts.FileReference["resolutionMode"] | undefined; - switch (kind) { - case ts.FileIncludeKind.Import: - const importLiteral = getModuleNameStringLiteralAt(file, index); - packageId = file.resolvedModules?.get(importLiteral.text, getModeForResolutionAtIndex(file, index))?.packageId; - if (importLiteral.pos === -1) - return { file, packageId, text: importLiteral.text }; - pos = ts.skipTrivia(file.text, importLiteral.pos); - end = importLiteral.end; - break; - case ts.FileIncludeKind.ReferenceFile: - ({ pos, end } = file.referencedFiles[index]); - break; - case ts.FileIncludeKind.TypeReferenceDirective: - ({ pos, end, resolutionMode } = file.typeReferenceDirectives[index]); - packageId = file.resolvedTypeReferenceDirectiveNames?.get(ts.toFileNameLowerCase(file.typeReferenceDirectives[index].fileName), resolutionMode || file.impliedNodeFormat)?.packageId; - break; - case ts.FileIncludeKind.LibReferenceDirective: - ({ pos, end } = file.libReferenceDirectives[index]); - break; - default: - return ts.Debug.assertNever(kind); - } - return { file, pos, end, packageId }; +/*@internal*/ +export function getReferencedFileLocation(getSourceFileByPath: (path: ts.Path) => ts.SourceFile | undefined, ref: ts.ReferencedFile): ReferenceFileLocation | SyntheticReferenceFileLocation { + const file = ts.Debug.checkDefined(getSourceFileByPath(ref.file)); + const { kind, index } = ref; + let pos: number | undefined, end: number | undefined, packageId: ts.PackageId | undefined, resolutionMode: ts.FileReference["resolutionMode"] | undefined; + switch (kind) { + case ts.FileIncludeKind.Import: + const importLiteral = getModuleNameStringLiteralAt(file, index); + packageId = file.resolvedModules?.get(importLiteral.text, getModeForResolutionAtIndex(file, index))?.packageId; + if (importLiteral.pos === -1) + return { file, packageId, text: importLiteral.text }; + pos = ts.skipTrivia(file.text, importLiteral.pos); + end = importLiteral.end; + break; + case ts.FileIncludeKind.ReferenceFile: + ({ pos, end } = file.referencedFiles[index]); + break; + case ts.FileIncludeKind.TypeReferenceDirective: + ({ pos, end, resolutionMode } = file.typeReferenceDirectives[index]); + packageId = file.resolvedTypeReferenceDirectiveNames?.get(ts.toFileNameLowerCase(file.typeReferenceDirectives[index].fileName), resolutionMode || file.impliedNodeFormat)?.packageId; + break; + case ts.FileIncludeKind.LibReferenceDirective: + ({ pos, end } = file.libReferenceDirectives[index]); + break; + default: + return ts.Debug.assertNever(kind); } + return { file, pos, end, packageId }; +} - /** - * Determines if program structure is upto date or needs to be recreated - */ - /* @internal */ - export function isProgramUptoDate(program: ts.Program | undefined, rootFileNames: string[], newOptions: ts.CompilerOptions, getSourceVersion: (path: ts.Path, fileName: string) => string | undefined, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: ts.HasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames: ts.HasChangedAutomaticTypeDirectiveNames | undefined, getParsedCommandLine: (fileName: string) => ts.ParsedCommandLine | undefined, projectReferences: readonly ts.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; +/** + * Determines if program structure is upto date or needs to be recreated + */ +/* @internal */ +export function isProgramUptoDate(program: ts.Program | undefined, rootFileNames: string[], newOptions: ts.CompilerOptions, getSourceVersion: (path: ts.Path, fileName: string) => string | undefined, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: ts.HasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames: ts.HasChangedAutomaticTypeDirectiveNames | undefined, getParsedCommandLine: (fileName: string) => ts.ParsedCommandLine | undefined, projectReferences: readonly ts.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 (!ts.arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) - return false; - let seenResolvedRefs: ts.ResolvedProjectReference[] | undefined; + // If root file names don't match + if (!ts.arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) + return false; + let seenResolvedRefs: ts.ResolvedProjectReference[] | undefined; - // If project references don't match - if (!ts.arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) - return false; + // If project references don't match + if (!ts.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 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; + // 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 (!ts.compareDataObjects(currentOptions, newOptions)) - return false; + const currentOptions = program.getCompilerOptions(); + // If the compilation settings do no match, then the program is not up-to-date + if (!ts.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; + // 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; + return true; - function sourceFileNotUptoDate(sourceFile: ts.SourceFile) { - return !sourceFileVersionUptoDate(sourceFile) || - hasInvalidatedResolution(sourceFile.path); - } + function sourceFileNotUptoDate(sourceFile: ts.SourceFile) { + return !sourceFileVersionUptoDate(sourceFile) || + hasInvalidatedResolution(sourceFile.path); + } - function sourceFileVersionUptoDate(sourceFile: ts.SourceFile) { - return sourceFile.version === getSourceVersion(sourceFile.resolvedPath, sourceFile.fileName); - } + function sourceFileVersionUptoDate(sourceFile: ts.SourceFile) { + return sourceFile.version === getSourceVersion(sourceFile.resolvedPath, sourceFile.fileName); + } - function projectReferenceUptoDate(oldRef: ts.ProjectReference, newRef: ts.ProjectReference, index: number) { - return ts.projectReferenceIsEqualTo(oldRef, newRef) && - resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef); - } - - function resolvedProjectReferenceUptoDate(oldResolvedRef: ts.ResolvedProjectReference | undefined, oldRef: ts.ProjectReference): boolean { - if (oldResolvedRef) { - // Assume true - if (ts.contains(seenResolvedRefs, oldResolvedRef)) - return true; + function projectReferenceUptoDate(oldRef: ts.ProjectReference, newRef: ts.ProjectReference, index: number) { + return ts.projectReferenceIsEqualTo(oldRef, newRef) && + resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef); + } - const refPath = resolveProjectReferencePath(oldRef); - const newParsedCommandLine = getParsedCommandLine(refPath); + function resolvedProjectReferenceUptoDate(oldResolvedRef: ts.ResolvedProjectReference | undefined, oldRef: ts.ProjectReference): boolean { + if (oldResolvedRef) { + // Assume true + if (ts.contains(seenResolvedRefs, oldResolvedRef)) + return true; - // Check if config file exists - if (!newParsedCommandLine) - return false; + const refPath = resolveProjectReferencePath(oldRef); + const newParsedCommandLine = getParsedCommandLine(refPath); - // If change in source file - if (oldResolvedRef.commandLine.options.configFile !== newParsedCommandLine.options.configFile) - return false; + // Check if config file exists + if (!newParsedCommandLine) + return false; - // check file names - if (!ts.arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newParsedCommandLine.fileNames)) - return false; + // If change in source file + if (oldResolvedRef.commandLine.options.configFile !== newParsedCommandLine.options.configFile) + return false; - // Add to seen before checking the referenced paths of this config file - (seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef); + // check file names + if (!ts.arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newParsedCommandLine.fileNames)) + return false; - // If child project references are upto date, this project reference is uptodate - return !ts.forEach(oldResolvedRef.references, (childResolvedRef, index) => !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index])); - } + // Add to seen before checking the referenced paths of this config file + (seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef); - // In old program, not able to resolve project reference path, - // so if config file doesnt exist, it is uptodate. - const refPath = resolveProjectReferencePath(oldRef); - return !getParsedCommandLine(refPath); + // If child project references are upto date, this project reference is uptodate + return !ts.forEach(oldResolvedRef.references, (childResolvedRef, index) => !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index])); } - } - export function getConfigFileParsingDiagnostics(configFileParseResult: ts.ParsedCommandLine): readonly ts.Diagnostic[] { - return configFileParseResult.options.configFile ? - [...configFileParseResult.options.configFile.parseDiagnostics, ...configFileParseResult.errors] : - configFileParseResult.errors; + // In old program, not able to resolve project reference path, + // so if config file doesnt exist, it is uptodate. + const refPath = resolveProjectReferencePath(oldRef); + return !getParsedCommandLine(refPath); } +} - /** - * A function for determining if a given file is esm or cjs format, assuming modern node module resolution rules, as configured by the - * `options` parameter. - * - * @param fileName The normalized absolute path to check the format of (it need not exist on disk) - * @param [packageJsonInfoCache] A cache for package file lookups - it's best to have a cache when this function is called often - * @param host The ModuleResolutionHost which can perform the filesystem lookups for package json data - * @param options The compiler options to perform the analysis under - relevant options are `moduleResolution` and `traceResolution` - * @returns `undefined` if the path has no relevant implied format, `ModuleKind.ESNext` for esm format, and `ModuleKind.CommonJS` for cjs format - */ - export function getImpliedNodeFormatForFile(fileName: ts.Path, packageJsonInfoCache: ts.PackageJsonInfoCache | undefined, host: ts.ModuleResolutionHost, options: ts.CompilerOptions): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { - switch (ts.getEmitModuleResolutionKind(options)) { - case ts.ModuleResolutionKind.Node16: - case ts.ModuleResolutionKind.NodeNext: - return ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dmts, ts.Extension.Mts, ts.Extension.Mjs]) ? ts.ModuleKind.ESNext : - ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dcts, ts.Extension.Cts, ts.Extension.Cjs]) ? ts.ModuleKind.CommonJS : - ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dts, ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Js, ts.Extension.Jsx]) ? lookupFromPackageJson() : - undefined; // other extensions, like `json` or `tsbuildinfo`, are set as `undefined` here but they should never be fed through the transformer pipeline - default: - return undefined; - } - function lookupFromPackageJson(): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS { - const scope = ts.getPackageScopeForPath(fileName, packageJsonInfoCache, host, options); - return scope?.packageJsonContent.type === "module" ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; - - } - } - - /** @internal */ - export const plainJSErrors: ts.Set = new ts.Set([ - // binder errors - ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0.code, - ts.Diagnostics.A_module_cannot_have_multiple_default_exports.code, - ts.Diagnostics.Another_export_default_is_here.code, - ts.Diagnostics.The_first_export_default_is_here.code, - ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_at_the_top_level_of_a_module.code, - ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode.code, - ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here.code, - ts.Diagnostics.constructor_is_a_reserved_word.code, - ts.Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode.code, - ts.Diagnostics.Code_contained_in_a_class_is_evaluated_in_JavaScript_s_strict_mode_which_does_not_allow_this_use_of_0_For_more_information_see_https_Colon_Slash_Slashdeveloper_mozilla_org_Slashen_US_Slashdocs_SlashWeb_SlashJavaScript_SlashReference_SlashStrict_mode.code, - ts.Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode.code, - ts.Diagnostics.Invalid_use_of_0_in_strict_mode.code, - ts.Diagnostics.A_label_is_not_allowed_here.code, - ts.Diagnostics.Octal_literals_are_not_allowed_in_strict_mode.code, - ts.Diagnostics.with_statements_are_not_allowed_in_strict_mode.code, - // grammar errors - ts.Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement.code, - ts.Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement.code, - ts.Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name.code, - ts.Diagnostics.A_class_member_cannot_have_the_0_keyword.code, - ts.Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name.code, - ts.Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement.code, - ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement.code, - ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement.code, - ts.Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement.code, - ts.Diagnostics.A_default_export_must_be_at_the_top_level_of_a_file_or_module_declaration.code, - ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context.code, - ts.Diagnostics.A_destructuring_declaration_must_have_an_initializer.code, - ts.Diagnostics.A_get_accessor_cannot_have_parameters.code, - ts.Diagnostics.A_rest_element_cannot_contain_a_binding_pattern.code, - ts.Diagnostics.A_rest_element_cannot_have_a_property_name.code, - ts.Diagnostics.A_rest_element_cannot_have_an_initializer.code, - ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern.code, - ts.Diagnostics.A_rest_parameter_cannot_have_an_initializer.code, - ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list.code, - ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma.code, - ts.Diagnostics.A_return_statement_cannot_be_used_inside_a_class_static_block.code, - ts.Diagnostics.A_set_accessor_cannot_have_rest_parameter.code, - ts.Diagnostics.A_set_accessor_must_have_exactly_one_parameter.code, - ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_module.code, - ts.Diagnostics.An_export_declaration_cannot_have_modifiers.code, - ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module.code, - ts.Diagnostics.An_import_declaration_cannot_have_modifiers.code, - ts.Diagnostics.An_object_member_cannot_be_declared_optional.code, - ts.Diagnostics.Argument_of_dynamic_import_cannot_be_spread_element.code, - ts.Diagnostics.Cannot_assign_to_private_method_0_Private_methods_are_not_writable.code, - ts.Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause.code, - ts.Diagnostics.Catch_clause_variable_cannot_have_an_initializer.code, - ts.Diagnostics.Class_decorators_can_t_be_used_with_static_private_identifier_Consider_removing_the_experimental_decorator.code, - ts.Diagnostics.Classes_can_only_extend_a_single_class.code, - ts.Diagnostics.Classes_may_not_have_a_field_named_constructor.code, - ts.Diagnostics.Did_you_mean_to_use_a_Colon_An_can_only_follow_a_property_name_when_the_containing_object_literal_is_part_of_a_destructuring_pattern.code, - ts.Diagnostics.Duplicate_label_0.code, - ts.Diagnostics.Dynamic_imports_can_only_accept_a_module_specifier_and_an_optional_assertion_as_arguments.code, - ts.Diagnostics.For_await_loops_cannot_be_used_inside_a_class_static_block.code, - ts.Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression.code, - ts.Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name.code, - ts.Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array.code, - ts.Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names.code, - ts.Diagnostics.Jump_target_cannot_cross_function_boundary.code, - ts.Diagnostics.Line_terminator_not_permitted_before_arrow.code, - ts.Diagnostics.Modifiers_cannot_appear_here.code, - ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement.code, - ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement.code, - ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies.code, - ts.Diagnostics.Private_identifiers_are_only_allowed_in_class_bodies_and_may_only_be_used_as_part_of_a_class_member_declaration_property_access_or_on_the_left_hand_side_of_an_in_expression.code, - ts.Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_identifier.code, - ts.Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain.code, - ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_async.code, - ts.Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer.code, - ts.Diagnostics.The_variable_declaration_of_a_for_of_statement_cannot_have_an_initializer.code, - ts.Diagnostics.Trailing_comma_not_allowed.code, - ts.Diagnostics.Variable_declaration_list_cannot_be_empty.code, - ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses.code, - ts.Diagnostics._0_expected.code, - ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2.code, - ts.Diagnostics._0_list_cannot_be_empty.code, - ts.Diagnostics._0_modifier_already_seen.code, - ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration.code, - ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element.code, - ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter.code, - ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind.code, - ts.Diagnostics._0_modifier_cannot_be_used_here.code, - ts.Diagnostics._0_modifier_must_precede_1_modifier.code, - ts.Diagnostics.const_declarations_can_only_be_declared_inside_a_block.code, - ts.Diagnostics.const_declarations_must_be_initialized.code, - ts.Diagnostics.extends_clause_already_seen.code, - ts.Diagnostics.let_declarations_can_only_be_declared_inside_a_block.code, - ts.Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations.code, - ]); +export function getConfigFileParsingDiagnostics(configFileParseResult: ts.ParsedCommandLine): readonly ts.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: ts.Program | undefined, newOptions: ts.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`. - return ts.optionsHaveChanges(program.getCompilerOptions(), newOptions, ts.sourceFileAffectingCompilerOptions); +/** + * A function for determining if a given file is esm or cjs format, assuming modern node module resolution rules, as configured by the + * `options` parameter. + * + * @param fileName The normalized absolute path to check the format of (it need not exist on disk) + * @param [packageJsonInfoCache] A cache for package file lookups - it's best to have a cache when this function is called often + * @param host The ModuleResolutionHost which can perform the filesystem lookups for package json data + * @param options The compiler options to perform the analysis under - relevant options are `moduleResolution` and `traceResolution` + * @returns `undefined` if the path has no relevant implied format, `ModuleKind.ESNext` for esm format, and `ModuleKind.CommonJS` for cjs format + */ +export function getImpliedNodeFormatForFile(fileName: ts.Path, packageJsonInfoCache: ts.PackageJsonInfoCache | undefined, host: ts.ModuleResolutionHost, options: ts.CompilerOptions): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { + switch (ts.getEmitModuleResolutionKind(options)) { + case ts.ModuleResolutionKind.Node16: + case ts.ModuleResolutionKind.NodeNext: + return ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dmts, ts.Extension.Mts, ts.Extension.Mjs]) ? ts.ModuleKind.ESNext : + ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dcts, ts.Extension.Cts, ts.Extension.Cjs]) ? ts.ModuleKind.CommonJS : + ts.fileExtensionIsOneOf(fileName, [ts.Extension.Dts, ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Js, ts.Extension.Jsx]) ? lookupFromPackageJson() : + undefined; // other extensions, like `json` or `tsbuildinfo`, are set as `undefined` here but they should never be fed through the transformer pipeline + default: + return undefined; } + function lookupFromPackageJson(): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS { + const scope = ts.getPackageScopeForPath(fileName, packageJsonInfoCache, host, options); + return scope?.packageJsonContent.type === "module" ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; - function createCreateProgramOptions(rootNames: readonly string[], options: ts.CompilerOptions, host?: ts.CompilerHost, oldProgram?: ts.Program, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.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: ts.CreateProgramOptions): ts.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: ts.CompilerOptions, host?: ts.CompilerHost, oldProgram?: ts.Program, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program; - export function createProgram(rootNamesOrOptions: readonly string[] | ts.CreateProgramOptions, _options?: ts.CompilerOptions, _host?: ts.CompilerHost, _oldProgram?: ts.Program, _configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program { - const createProgramOptions = ts.isArray(rootNamesOrOptions) ? createCreateProgramOptions(rootNamesOrOptions, _options!, _host, _oldProgram, _configFileParsingDiagnostics) : rootNamesOrOptions; // TODO: GH#18217 - const { rootNames, options, configFileParsingDiagnostics, projectReferences } = createProgramOptions; - let { oldProgram } = createProgramOptions; - - let processingDefaultLibFiles: ts.SourceFile[] | undefined; - let processingOtherFiles: ts.SourceFile[] | undefined; - let files: ts.SourceFile[]; - let symlinks: ts.SymlinkCache | undefined; - let commonSourceDirectory: string; - let typeChecker: ts.TypeChecker; - let classifiableNames: ts.Set; - const ambientModuleNameToUnmodifiedFileName = new ts.Map(); - let fileReasons = ts.createMultiMap(); - const cachedBindAndCheckDiagnosticsForFile: DiagnosticCache = {}; - const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; - let resolvedTypeReferenceDirectives = ts.createModeAwareCache(); - let fileProcessingDiagnostics: ts.FilePreprocessingDiagnostics[] | undefined; - - // 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 = new ts.Map(); - - // Track source files that are source files found by searching under node_modules, as these shouldn't be compiled. - const sourceFilesFoundSearchingNodeModules = new ts.Map(); - ts.tracing?.push(ts.tracing.Phase.Program, "createProgram", { configFilePath: options.configFilePath, rootDir: options.rootDir }, /*separateBeginAndEnd*/ true); - ts.performance.mark("beforeProgram"); - - const host = createProgramOptions.host || createCompilerHost(options); - const configParsingHost = parseConfigHostFromCompilerHostLike(host); - - let skipDefaultLib = options.noLib; - const getDefaultLibraryFileName = ts.memoize(() => host.getDefaultLibFileName(options)); - const defaultLibraryPath = host.getDefaultLibLocation ? host.getDefaultLibLocation() : ts.getDirectoryPath(getDefaultLibraryFileName()); - const programDiagnostics = ts.createDiagnosticCollection(); - const currentDirectory = host.getCurrentDirectory(); - const supportedExtensions = ts.getSupportedExtensions(options); - const supportedExtensionsWithJsonIfResolveJsonModule = ts.getSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); - - // Map storing if there is emit blocking diagnostics for given input - const hasEmitBlockingDiagnostics = new ts.Map(); - let _compilerOptionsObjectLiteralSyntax: ts.ObjectLiteralExpression | false | undefined; - let moduleResolutionCache: ts.ModuleResolutionCache | undefined; - let typeReferenceDirectiveResolutionCache: ts.TypeReferenceDirectiveResolutionCache | undefined; - let actualResolveModuleNamesWorker: (moduleNames: string[], containingFile: ts.SourceFile, containingFileName: string, reusedNames?: string[], redirectedReference?: ts.ResolvedProjectReference) => ts.ResolvedModuleFull[]; - const hasInvalidatedResolution = host.hasInvalidatedResolution || ts.returnFalse; - if (host.resolveModuleNames) { - actualResolveModuleNamesWorker = (moduleNames, containingFile, containingFileName, reusedNames, redirectedReference) => host.resolveModuleNames!(ts.Debug.checkEachDefined(moduleNames), containingFileName, reusedNames, redirectedReference, options, containingFile).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 ts.ResolvedModuleFull).extension !== undefined) { - return resolved as ts.ResolvedModuleFull; - } - const withExtension = ts.clone(resolved) as ts.ResolvedModuleFull; - withExtension.extension = ts.extensionFromPath(resolved.resolvedFileName); - return withExtension; - }); - moduleResolutionCache = host.getModuleResolutionCache?.(); - } - else { - moduleResolutionCache = ts.createModuleResolutionCache(currentDirectory, getCanonicalFileName, options); - const loader = (moduleName: string, resolverMode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined) => ts.resolveModuleName(moduleName, containingFileName, options, host, moduleResolutionCache, redirectedReference, resolverMode).resolvedModule!; // TODO: GH#18217 - actualResolveModuleNamesWorker = (moduleNames, containingFile, containingFileName, _reusedNames, redirectedReference) => loadWithModeAwareCache(ts.Debug.checkEachDefined(moduleNames), containingFile, containingFileName, redirectedReference, loader); - } - - let actualResolveTypeReferenceDirectiveNamesWorker: (typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"] | undefined) => (ts.ResolvedTypeReferenceDirective | undefined)[]; - if (host.resolveTypeReferenceDirectives) { - actualResolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile, redirectedReference, containingFileMode) => host.resolveTypeReferenceDirectives!(ts.Debug.checkEachDefined(typeDirectiveNames), containingFile, redirectedReference, options, containingFileMode); - } - else { - typeReferenceDirectiveResolutionCache = ts.createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()); - const loader = (typesRef: string, containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, resolutionMode: ts.SourceFile["impliedNodeFormat"] | undefined) => ts.resolveTypeReferenceDirective(typesRef, containingFile, options, host, redirectedReference, typeReferenceDirectiveResolutionCache, resolutionMode).resolvedTypeReferenceDirective!; // TODO: GH#18217 - actualResolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile, redirectedReference, containingFileMode) => loadWithTypeDirectiveCache(ts.Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, containingFileMode, loader); - } +/** @internal */ +export const plainJSErrors: ts.Set = new ts.Set([ + // binder errors + ts.Diagnostics.Cannot_redeclare_block_scoped_variable_0.code, + ts.Diagnostics.A_module_cannot_have_multiple_default_exports.code, + ts.Diagnostics.Another_export_default_is_here.code, + ts.Diagnostics.The_first_export_default_is_here.code, + ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_at_the_top_level_of_a_module.code, + ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode.code, + ts.Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here.code, + ts.Diagnostics.constructor_is_a_reserved_word.code, + ts.Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode.code, + ts.Diagnostics.Code_contained_in_a_class_is_evaluated_in_JavaScript_s_strict_mode_which_does_not_allow_this_use_of_0_For_more_information_see_https_Colon_Slash_Slashdeveloper_mozilla_org_Slashen_US_Slashdocs_SlashWeb_SlashJavaScript_SlashReference_SlashStrict_mode.code, + ts.Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode.code, + ts.Diagnostics.Invalid_use_of_0_in_strict_mode.code, + ts.Diagnostics.A_label_is_not_allowed_here.code, + ts.Diagnostics.Octal_literals_are_not_allowed_in_strict_mode.code, + ts.Diagnostics.with_statements_are_not_allowed_in_strict_mode.code, + // grammar errors + ts.Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement.code, + ts.Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement.code, + ts.Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name.code, + ts.Diagnostics.A_class_member_cannot_have_the_0_keyword.code, + ts.Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name.code, + ts.Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement.code, + ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement.code, + ts.Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement.code, + ts.Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement.code, + ts.Diagnostics.A_default_export_must_be_at_the_top_level_of_a_file_or_module_declaration.code, + ts.Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context.code, + ts.Diagnostics.A_destructuring_declaration_must_have_an_initializer.code, + ts.Diagnostics.A_get_accessor_cannot_have_parameters.code, + ts.Diagnostics.A_rest_element_cannot_contain_a_binding_pattern.code, + ts.Diagnostics.A_rest_element_cannot_have_a_property_name.code, + ts.Diagnostics.A_rest_element_cannot_have_an_initializer.code, + ts.Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern.code, + ts.Diagnostics.A_rest_parameter_cannot_have_an_initializer.code, + ts.Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list.code, + ts.Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma.code, + ts.Diagnostics.A_return_statement_cannot_be_used_inside_a_class_static_block.code, + ts.Diagnostics.A_set_accessor_cannot_have_rest_parameter.code, + ts.Diagnostics.A_set_accessor_must_have_exactly_one_parameter.code, + ts.Diagnostics.An_export_declaration_can_only_be_used_at_the_top_level_of_a_module.code, + ts.Diagnostics.An_export_declaration_cannot_have_modifiers.code, + ts.Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module.code, + ts.Diagnostics.An_import_declaration_cannot_have_modifiers.code, + ts.Diagnostics.An_object_member_cannot_be_declared_optional.code, + ts.Diagnostics.Argument_of_dynamic_import_cannot_be_spread_element.code, + ts.Diagnostics.Cannot_assign_to_private_method_0_Private_methods_are_not_writable.code, + ts.Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause.code, + ts.Diagnostics.Catch_clause_variable_cannot_have_an_initializer.code, + ts.Diagnostics.Class_decorators_can_t_be_used_with_static_private_identifier_Consider_removing_the_experimental_decorator.code, + ts.Diagnostics.Classes_can_only_extend_a_single_class.code, + ts.Diagnostics.Classes_may_not_have_a_field_named_constructor.code, + ts.Diagnostics.Did_you_mean_to_use_a_Colon_An_can_only_follow_a_property_name_when_the_containing_object_literal_is_part_of_a_destructuring_pattern.code, + ts.Diagnostics.Duplicate_label_0.code, + ts.Diagnostics.Dynamic_imports_can_only_accept_a_module_specifier_and_an_optional_assertion_as_arguments.code, + ts.Diagnostics.For_await_loops_cannot_be_used_inside_a_class_static_block.code, + ts.Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression.code, + ts.Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name.code, + ts.Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array.code, + ts.Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names.code, + ts.Diagnostics.Jump_target_cannot_cross_function_boundary.code, + ts.Diagnostics.Line_terminator_not_permitted_before_arrow.code, + ts.Diagnostics.Modifiers_cannot_appear_here.code, + ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement.code, + ts.Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement.code, + ts.Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies.code, + ts.Diagnostics.Private_identifiers_are_only_allowed_in_class_bodies_and_may_only_be_used_as_part_of_a_class_member_declaration_property_access_or_on_the_left_hand_side_of_an_in_expression.code, + ts.Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_identifier.code, + ts.Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain.code, + ts.Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_async.code, + ts.Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer.code, + ts.Diagnostics.The_variable_declaration_of_a_for_of_statement_cannot_have_an_initializer.code, + ts.Diagnostics.Trailing_comma_not_allowed.code, + ts.Diagnostics.Variable_declaration_list_cannot_be_empty.code, + ts.Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses.code, + ts.Diagnostics._0_expected.code, + ts.Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2.code, + ts.Diagnostics._0_list_cannot_be_empty.code, + ts.Diagnostics._0_modifier_already_seen.code, + ts.Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration.code, + ts.Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element.code, + ts.Diagnostics._0_modifier_cannot_appear_on_a_parameter.code, + ts.Diagnostics._0_modifier_cannot_appear_on_class_elements_of_this_kind.code, + ts.Diagnostics._0_modifier_cannot_be_used_here.code, + ts.Diagnostics._0_modifier_must_precede_1_modifier.code, + ts.Diagnostics.const_declarations_can_only_be_declared_inside_a_block.code, + ts.Diagnostics.const_declarations_must_be_initialized.code, + ts.Diagnostics.extends_clause_already_seen.code, + ts.Diagnostics.let_declarations_can_only_be_declared_inside_a_block.code, + ts.Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations.code, +]); + +/** + * Determine if source file needs to be re-created even if its text hasn't changed + */ +function shouldProgramCreateNewSourceFiles(program: ts.Program | undefined, newOptions: ts.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`. + return ts.optionsHaveChanges(program.getCompilerOptions(), newOptions, ts.sourceFileAffectingCompilerOptions); +} - // 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 = new ts.Map(); - // Maps from a SourceFile's `.path` to the name of the package it was imported with. - let sourceFileToPackageName = new ts.Map(); - // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. - let redirectTargetsMap = ts.createMultiMap(); - let usesUriStyleNodeCoreModules = false; +function createCreateProgramOptions(rootNames: readonly string[], options: ts.CompilerOptions, host?: ts.CompilerHost, oldProgram?: ts.Program, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.CreateProgramOptions { + return { + rootNames, + options, + host, + oldProgram, + configFileParsingDiagnostics + }; +} - /** - * map with - * - SourceFile if present - * - false if sourceFile missing for source of project reference redirect - * - undefined otherwise - */ - const filesByName = new ts.Map(); - let missingFilePaths: readonly ts.Path[] | undefined; - // stores 'filename -> file association' ignoring case - // used to track cases when two file names differ only in casing - const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? new ts.Map() : undefined; - - // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files - let resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined; - let projectReferenceRedirects: ts.ESMap | undefined; - let mapFromFileToProjectReferenceRedirects: ts.ESMap | undefined; - let mapFromToProjectReferenceRedirectSource: ts.ESMap | undefined; - - const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() && - !options.disableSourceOfProjectReferenceRedirect; - const { onProgramCreateComplete, fileExists, directoryExists } = updateHostForUseSourceOfProjectReferenceRedirect({ - compilerHost: host, - getSymlinkCache, - useSourceOfProjectReferenceRedirect, - toPath, - getResolvedProjectReferences, - getSourceOfProjectReferenceRedirect, - forEachResolvedProjectReference +/** + * 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: ts.CreateProgramOptions): ts.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: ts.CompilerOptions, host?: ts.CompilerHost, oldProgram?: ts.Program, configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program; +export function createProgram(rootNamesOrOptions: readonly string[] | ts.CreateProgramOptions, _options?: ts.CompilerOptions, _host?: ts.CompilerHost, _oldProgram?: ts.Program, _configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program { + const createProgramOptions = ts.isArray(rootNamesOrOptions) ? createCreateProgramOptions(rootNamesOrOptions, _options!, _host, _oldProgram, _configFileParsingDiagnostics) : rootNamesOrOptions; // TODO: GH#18217 + const { rootNames, options, configFileParsingDiagnostics, projectReferences } = createProgramOptions; + let { oldProgram } = createProgramOptions; + + let processingDefaultLibFiles: ts.SourceFile[] | undefined; + let processingOtherFiles: ts.SourceFile[] | undefined; + let files: ts.SourceFile[]; + let symlinks: ts.SymlinkCache | undefined; + let commonSourceDirectory: string; + let typeChecker: ts.TypeChecker; + let classifiableNames: ts.Set; + const ambientModuleNameToUnmodifiedFileName = new ts.Map(); + let fileReasons = ts.createMultiMap(); + const cachedBindAndCheckDiagnosticsForFile: DiagnosticCache = {}; + const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; + let resolvedTypeReferenceDirectives = ts.createModeAwareCache(); + let fileProcessingDiagnostics: ts.FilePreprocessingDiagnostics[] | undefined; + + // 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 = new ts.Map(); + + // Track source files that are source files found by searching under node_modules, as these shouldn't be compiled. + const sourceFilesFoundSearchingNodeModules = new ts.Map(); + ts.tracing?.push(ts.tracing.Phase.Program, "createProgram", { configFilePath: options.configFilePath, rootDir: options.rootDir }, /*separateBeginAndEnd*/ true); + ts.performance.mark("beforeProgram"); + + const host = createProgramOptions.host || createCompilerHost(options); + const configParsingHost = parseConfigHostFromCompilerHostLike(host); + + let skipDefaultLib = options.noLib; + const getDefaultLibraryFileName = ts.memoize(() => host.getDefaultLibFileName(options)); + const defaultLibraryPath = host.getDefaultLibLocation ? host.getDefaultLibLocation() : ts.getDirectoryPath(getDefaultLibraryFileName()); + const programDiagnostics = ts.createDiagnosticCollection(); + const currentDirectory = host.getCurrentDirectory(); + const supportedExtensions = ts.getSupportedExtensions(options); + const supportedExtensionsWithJsonIfResolveJsonModule = ts.getSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); + + // Map storing if there is emit blocking diagnostics for given input + const hasEmitBlockingDiagnostics = new ts.Map(); + let _compilerOptionsObjectLiteralSyntax: ts.ObjectLiteralExpression | false | undefined; + let moduleResolutionCache: ts.ModuleResolutionCache | undefined; + let typeReferenceDirectiveResolutionCache: ts.TypeReferenceDirectiveResolutionCache | undefined; + let actualResolveModuleNamesWorker: (moduleNames: string[], containingFile: ts.SourceFile, containingFileName: string, reusedNames?: string[], redirectedReference?: ts.ResolvedProjectReference) => ts.ResolvedModuleFull[]; + const hasInvalidatedResolution = host.hasInvalidatedResolution || ts.returnFalse; + if (host.resolveModuleNames) { + actualResolveModuleNamesWorker = (moduleNames, containingFile, containingFileName, reusedNames, redirectedReference) => host.resolveModuleNames!(ts.Debug.checkEachDefined(moduleNames), containingFileName, reusedNames, redirectedReference, options, containingFile).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 ts.ResolvedModuleFull).extension !== undefined) { + return resolved as ts.ResolvedModuleFull; + } + const withExtension = ts.clone(resolved) as ts.ResolvedModuleFull; + withExtension.extension = ts.extensionFromPath(resolved.resolvedFileName); + return withExtension; }); - const readFile = host.readFile.bind(host) as typeof host.readFile; + moduleResolutionCache = host.getModuleResolutionCache?.(); + } + else { + moduleResolutionCache = ts.createModuleResolutionCache(currentDirectory, getCanonicalFileName, options); + const loader = (moduleName: string, resolverMode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, containingFileName: string, redirectedReference: ts.ResolvedProjectReference | undefined) => ts.resolveModuleName(moduleName, containingFileName, options, host, moduleResolutionCache, redirectedReference, resolverMode).resolvedModule!; // TODO: GH#18217 + actualResolveModuleNamesWorker = (moduleNames, containingFile, containingFileName, _reusedNames, redirectedReference) => loadWithModeAwareCache(ts.Debug.checkEachDefined(moduleNames), containingFile, containingFileName, redirectedReference, loader); + } - ts.tracing?.push(ts.tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram }); - const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); - ts.tracing?.pop(); - // 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 structureIsReused: ts.StructureIsReused; - ts.tracing?.push(ts.tracing.Phase.Program, "tryReuseStructureFromOldProgram", {}); - structureIsReused = tryReuseStructureFromOldProgram(); // eslint-disable-line prefer-const - ts.tracing?.pop(); - if (structureIsReused !== ts.StructureIsReused.Completely) { - processingDefaultLibFiles = []; - processingOtherFiles = []; + let actualResolveTypeReferenceDirectiveNamesWorker: (typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"] | undefined) => (ts.ResolvedTypeReferenceDirective | undefined)[]; + if (host.resolveTypeReferenceDirectives) { + actualResolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile, redirectedReference, containingFileMode) => host.resolveTypeReferenceDirectives!(ts.Debug.checkEachDefined(typeDirectiveNames), containingFile, redirectedReference, options, containingFileMode); + } + else { + typeReferenceDirectiveResolutionCache = ts.createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()); + const loader = (typesRef: string, containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, resolutionMode: ts.SourceFile["impliedNodeFormat"] | undefined) => ts.resolveTypeReferenceDirective(typesRef, containingFile, options, host, redirectedReference, typeReferenceDirectiveResolutionCache, resolutionMode).resolvedTypeReferenceDirective!; // TODO: GH#18217 + actualResolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile, redirectedReference, containingFileMode) => loadWithTypeDirectiveCache(ts.Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, containingFileMode, loader); + } - if (projectReferences) { - if (!resolvedProjectReferences) { - resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); - } - if (rootNames.length) { - resolvedProjectReferences?.forEach((parsedRef, index) => { - if (!parsedRef) - return; - const out = ts.outFile(parsedRef.commandLine.options); - if (useSourceOfProjectReferenceRedirect) { - if (out || ts.getEmitModuleKind(parsedRef.commandLine.options) === ts.ModuleKind.None) { - for (const fileName of parsedRef.commandLine.fileNames) { - processProjectReferenceFile(fileName, { kind: ts.FileIncludeKind.SourceFromProjectReference, index }); - } + // 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 = new ts.Map(); + // Maps from a SourceFile's `.path` to the name of the package it was imported with. + let sourceFileToPackageName = new ts.Map(); + // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. + let redirectTargetsMap = ts.createMultiMap(); + let usesUriStyleNodeCoreModules = false; + + /** + * map with + * - SourceFile if present + * - false if sourceFile missing for source of project reference redirect + * - undefined otherwise + */ + const filesByName = new ts.Map(); + let missingFilePaths: readonly ts.Path[] | undefined; + // stores 'filename -> file association' ignoring case + // used to track cases when two file names differ only in casing + const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? new ts.Map() : undefined; + + // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files + let resolvedProjectReferences: readonly (ts.ResolvedProjectReference | undefined)[] | undefined; + let projectReferenceRedirects: ts.ESMap | undefined; + let mapFromFileToProjectReferenceRedirects: ts.ESMap | undefined; + let mapFromToProjectReferenceRedirectSource: ts.ESMap | undefined; + + const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() && + !options.disableSourceOfProjectReferenceRedirect; + const { onProgramCreateComplete, fileExists, directoryExists } = updateHostForUseSourceOfProjectReferenceRedirect({ + compilerHost: host, + getSymlinkCache, + useSourceOfProjectReferenceRedirect, + toPath, + getResolvedProjectReferences, + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); + const readFile = host.readFile.bind(host) as typeof host.readFile; + + ts.tracing?.push(ts.tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram }); + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); + ts.tracing?.pop(); + // 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 structureIsReused: ts.StructureIsReused; + ts.tracing?.push(ts.tracing.Phase.Program, "tryReuseStructureFromOldProgram", {}); + structureIsReused = tryReuseStructureFromOldProgram(); // eslint-disable-line prefer-const + ts.tracing?.pop(); + if (structureIsReused !== ts.StructureIsReused.Completely) { + processingDefaultLibFiles = []; + processingOtherFiles = []; + + if (projectReferences) { + if (!resolvedProjectReferences) { + resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + } + if (rootNames.length) { + resolvedProjectReferences?.forEach((parsedRef, index) => { + if (!parsedRef) + return; + const out = ts.outFile(parsedRef.commandLine.options); + if (useSourceOfProjectReferenceRedirect) { + if (out || ts.getEmitModuleKind(parsedRef.commandLine.options) === ts.ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + processProjectReferenceFile(fileName, { kind: ts.FileIncludeKind.SourceFromProjectReference, index }); } } - else { - if (out) { - processProjectReferenceFile(ts.changeExtension(out, ".d.ts"), { kind: ts.FileIncludeKind.OutputFromProjectReference, index }); - } - else if (ts.getEmitModuleKind(parsedRef.commandLine.options) === ts.ModuleKind.None) { - const getCommonSourceDirectory = ts.memoize(() => ts.getCommonSourceDirectoryOfConfig(parsedRef.commandLine, !host.useCaseSensitiveFileNames())); - for (const fileName of parsedRef.commandLine.fileNames) { - if (!ts.isDeclarationFileName(fileName) && !ts.fileExtensionIs(fileName, ts.Extension.Json)) { - processProjectReferenceFile(ts.getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory), { kind: ts.FileIncludeKind.OutputFromProjectReference, index }); - } + } + else { + if (out) { + processProjectReferenceFile(ts.changeExtension(out, ".d.ts"), { kind: ts.FileIncludeKind.OutputFromProjectReference, index }); + } + else if (ts.getEmitModuleKind(parsedRef.commandLine.options) === ts.ModuleKind.None) { + const getCommonSourceDirectory = ts.memoize(() => ts.getCommonSourceDirectoryOfConfig(parsedRef.commandLine, !host.useCaseSensitiveFileNames())); + for (const fileName of parsedRef.commandLine.fileNames) { + if (!ts.isDeclarationFileName(fileName) && !ts.fileExtensionIs(fileName, ts.Extension.Json)) { + processProjectReferenceFile(ts.getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory), { kind: ts.FileIncludeKind.OutputFromProjectReference, index }); } } } - }); - } + } + }); } + } - ts.tracing?.push(ts.tracing.Phase.Program, "processRootFiles", { count: rootNames.length }); - ts.forEach(rootNames, (name, index) => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.RootFile, index })); - ts.tracing?.pop(); + ts.tracing?.push(ts.tracing.Phase.Program, "processRootFiles", { count: rootNames.length }); + ts.forEach(rootNames, (name, index) => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.RootFile, index })); + ts.tracing?.pop(); - // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders - const typeReferences: string[] = rootNames.length ? ts.getAutomaticTypeDirectiveNames(options, host) : ts.emptyArray; - - if (typeReferences.length) { - ts.tracing?.push(ts.tracing.Phase.Program, "processTypeReferences", { count: typeReferences.length }); - // This containingFilename needs to match with the one used in managed-side - const containingDirectory = options.configFilePath ? ts.getDirectoryPath(options.configFilePath) : host.getCurrentDirectory(); - const containingFilename = ts.combinePaths(containingDirectory, inferredTypesContainingFile); - const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeReferences, containingFilename); - for (let i = 0; i < typeReferences.length; i++) { - // under node16/nodenext module resolution, load `types`/ata include names as cjs resolution results by passing an `undefined` mode - processTypeReferenceDirective(typeReferences[i], /*mode*/ undefined, resolutions[i], { kind: ts.FileIncludeKind.AutomaticTypeDirectiveFile, typeReference: typeReferences[i], packageId: resolutions[i]?.packageId }); - } - ts.tracing?.pop(); - } - - // 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, { kind: ts.FileIncludeKind.LibFile }); - } - else { - ts.forEach(options.lib, (libFileName, index) => { - processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.LibFile, index }); - }); - } - } + // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders + const typeReferences: string[] = rootNames.length ? ts.getAutomaticTypeDirectiveNames(options, host) : ts.emptyArray; - missingFilePaths = ts.arrayFrom(ts.mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as ts.Path : undefined)); - files = ts.stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); - processingDefaultLibFiles = undefined; - processingOtherFiles = undefined; + if (typeReferences.length) { + ts.tracing?.push(ts.tracing.Phase.Program, "processTypeReferences", { count: typeReferences.length }); + // This containingFilename needs to match with the one used in managed-side + const containingDirectory = options.configFilePath ? ts.getDirectoryPath(options.configFilePath) : host.getCurrentDirectory(); + const containingFilename = ts.combinePaths(containingDirectory, inferredTypesContainingFile); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeReferences, containingFilename); + for (let i = 0; i < typeReferences.length; i++) { + // under node16/nodenext module resolution, load `types`/ata include names as cjs resolution results by passing an `undefined` mode + processTypeReferenceDirective(typeReferences[i], /*mode*/ undefined, resolutions[i], { kind: ts.FileIncludeKind.AutomaticTypeDirectiveFile, typeReference: typeReferences[i], packageId: resolutions[i]?.packageId }); + } + ts.tracing?.pop(); } - ts.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 wasn't redirect but new file is - (oldSourceFile.resolvedPath === oldSourceFile.path && newFile.resolvedPath !== oldSourceFile.path)) { - host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path)); - } + // 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, { kind: ts.FileIncludeKind.LibFile }); } - if (!host.getParsedCommandLine) { - oldProgram.forEachResolvedProjectReference(resolvedProjectReference => { - if (!getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) { - host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false); - } + else { + ts.forEach(options.lib, (libFileName, index) => { + processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.LibFile, index }); }); } } - // Release commandlines that new program does not use - if (oldProgram && host.onReleaseParsedCommandLine) { - forEachProjectReference(oldProgram.getProjectReferences(), oldProgram.getResolvedProjectReferences(), (oldResolvedRef, parent, index) => { - const oldReference = parent?.commandLine.projectReferences![index] || oldProgram!.getProjectReferences()![index]; - const oldRefPath = resolveProjectReferencePath(oldReference); - if (!projectReferenceRedirects?.has(toPath(oldRefPath))) { - host.onReleaseParsedCommandLine!(oldRefPath, oldResolvedRef, oldProgram!.getCompilerOptions()); - } - }); - } - - typeReferenceDirectiveResolutionCache = undefined; - - // unconditionally set oldProgram to undefined to prevent it from being captured in closure - oldProgram = undefined; - - const program: ts.Program = { - getRootFileNames: () => rootNames, - getSourceFile, - getSourceFileByPath, - getSourceFiles: () => files, - getMissingFilePaths: () => missingFilePaths!, - getModuleResolutionCache: () => moduleResolutionCache, - getFilesByNameMap: () => filesByName, - getCompilerOptions: () => options, - getSyntacticDiagnostics, - getOptionsDiagnostics, - getGlobalDiagnostics, - getSemanticDiagnostics, - getCachedSemanticDiagnostics, - getSuggestionDiagnostics, - getDeclarationDiagnostics, - getBindAndCheckDiagnostics, - getProgramDiagnostics, - getTypeChecker, - getClassifiableNames, - getCommonSourceDirectory, - emit, - getCurrentDirectory: () => currentDirectory, - getNodeCount: () => getTypeChecker().getNodeCount(), - getIdentifierCount: () => getTypeChecker().getIdentifierCount(), - getSymbolCount: () => getTypeChecker().getSymbolCount(), - getTypeCount: () => getTypeChecker().getTypeCount(), - getInstantiationCount: () => getTypeChecker().getInstantiationCount(), - getRelationCacheSizes: () => getTypeChecker().getRelationCacheSizes(), - getFileProcessingDiagnostics: () => fileProcessingDiagnostics, - getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives, - isSourceFileFromExternalLibrary, - isSourceFileDefaultLibrary, - getSourceFileFromReference, - getLibFileFromReference, - sourceFileToPackageName, - redirectTargetsMap, - usesUriStyleNodeCoreModules, - isEmittedFile, - getConfigFileParsingDiagnostics, - getResolvedModuleWithFailedLookupLocationsFromCache, - getProjectReferences, - getResolvedProjectReferences, - getProjectReferenceRedirect, - getResolvedProjectReferenceToRedirect, - getResolvedProjectReferenceByPath, - forEachResolvedProjectReference, - isSourceOfProjectReferenceRedirect, - emitBuildInfo, - fileExists, - readFile, - directoryExists, - getSymlinkCache, - realpath: host.realpath?.bind(host), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), - getFileIncludeReasons: () => fileReasons, - structureIsReused, - writeFile, - }; + missingFilePaths = ts.arrayFrom(ts.mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as ts.Path : undefined)); + files = ts.stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); + processingDefaultLibFiles = undefined; + processingOtherFiles = undefined; + } - onProgramCreateComplete(); + ts.Debug.assert(!!missingFilePaths); - // Add file processingDiagnostics - fileProcessingDiagnostics?.forEach(diagnostic => { - switch (diagnostic.kind) { - case ts.FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic: - return programDiagnostics.add(createDiagnosticExplainingFile(diagnostic.file && getSourceFileByPath(diagnostic.file), diagnostic.fileProcessingReason, diagnostic.diagnostic, diagnostic.args || ts.emptyArray)); - case ts.FilePreprocessingDiagnosticsKind.FilePreprocessingReferencedDiagnostic: - const { file, pos, end } = getReferencedFileLocation(getSourceFileByPath, diagnostic.reason) as ReferenceFileLocation; - return programDiagnostics.add(ts.createFileDiagnostic(file, ts.Debug.checkDefined(pos), ts.Debug.checkDefined(end) - pos, diagnostic.diagnostic, ...diagnostic.args || ts.emptyArray)); - default: - ts.Debug.assertNever(diagnostic); + // 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 wasn't redirect but new file is + (oldSourceFile.resolvedPath === oldSourceFile.path && newFile.resolvedPath !== oldSourceFile.path)) { + host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path)); } + } + if (!host.getParsedCommandLine) { + oldProgram.forEachResolvedProjectReference(resolvedProjectReference => { + if (!getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) { + host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false); + } + }); + } + } + + // Release commandlines that new program does not use + if (oldProgram && host.onReleaseParsedCommandLine) { + forEachProjectReference(oldProgram.getProjectReferences(), oldProgram.getResolvedProjectReferences(), (oldResolvedRef, parent, index) => { + const oldReference = parent?.commandLine.projectReferences![index] || oldProgram!.getProjectReferences()![index]; + const oldRefPath = resolveProjectReferencePath(oldReference); + if (!projectReferenceRedirects?.has(toPath(oldRefPath))) { + host.onReleaseParsedCommandLine!(oldRefPath, oldResolvedRef, oldProgram!.getCompilerOptions()); + } }); + } - verifyCompilerOptions(); - ts.performance.mark("afterProgram"); - ts.performance.measure("Program", "beforeProgram", "afterProgram"); - ts.tracing?.pop(); + typeReferenceDirectiveResolutionCache = undefined; + + // unconditionally set oldProgram to undefined to prevent it from being captured in closure + oldProgram = undefined; + + const program: ts.Program = { + getRootFileNames: () => rootNames, + getSourceFile, + getSourceFileByPath, + getSourceFiles: () => files, + getMissingFilePaths: () => missingFilePaths!, + getModuleResolutionCache: () => moduleResolutionCache, + getFilesByNameMap: () => filesByName, + getCompilerOptions: () => options, + getSyntacticDiagnostics, + getOptionsDiagnostics, + getGlobalDiagnostics, + getSemanticDiagnostics, + getCachedSemanticDiagnostics, + getSuggestionDiagnostics, + getDeclarationDiagnostics, + getBindAndCheckDiagnostics, + getProgramDiagnostics, + getTypeChecker, + getClassifiableNames, + getCommonSourceDirectory, + emit, + getCurrentDirectory: () => currentDirectory, + getNodeCount: () => getTypeChecker().getNodeCount(), + getIdentifierCount: () => getTypeChecker().getIdentifierCount(), + getSymbolCount: () => getTypeChecker().getSymbolCount(), + getTypeCount: () => getTypeChecker().getTypeCount(), + getInstantiationCount: () => getTypeChecker().getInstantiationCount(), + getRelationCacheSizes: () => getTypeChecker().getRelationCacheSizes(), + getFileProcessingDiagnostics: () => fileProcessingDiagnostics, + getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives, + isSourceFileFromExternalLibrary, + isSourceFileDefaultLibrary, + getSourceFileFromReference, + getLibFileFromReference, + sourceFileToPackageName, + redirectTargetsMap, + usesUriStyleNodeCoreModules, + isEmittedFile, + getConfigFileParsingDiagnostics, + getResolvedModuleWithFailedLookupLocationsFromCache, + getProjectReferences, + getResolvedProjectReferences, + getProjectReferenceRedirect, + getResolvedProjectReferenceToRedirect, + getResolvedProjectReferenceByPath, + forEachResolvedProjectReference, + isSourceOfProjectReferenceRedirect, + emitBuildInfo, + fileExists, + readFile, + directoryExists, + getSymlinkCache, + realpath: host.realpath?.bind(host), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), + getFileIncludeReasons: () => fileReasons, + structureIsReused, + writeFile, + }; + + onProgramCreateComplete(); + + // Add file processingDiagnostics + fileProcessingDiagnostics?.forEach(diagnostic => { + switch (diagnostic.kind) { + case ts.FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic: + return programDiagnostics.add(createDiagnosticExplainingFile(diagnostic.file && getSourceFileByPath(diagnostic.file), diagnostic.fileProcessingReason, diagnostic.diagnostic, diagnostic.args || ts.emptyArray)); + case ts.FilePreprocessingDiagnosticsKind.FilePreprocessingReferencedDiagnostic: + const { file, pos, end } = getReferencedFileLocation(getSourceFileByPath, diagnostic.reason) as ReferenceFileLocation; + return programDiagnostics.add(ts.createFileDiagnostic(file, ts.Debug.checkDefined(pos), ts.Debug.checkDefined(end) - pos, diagnostic.diagnostic, ...diagnostic.args || ts.emptyArray)); + default: + ts.Debug.assertNever(diagnostic); + } + }); - return program; + verifyCompilerOptions(); + ts.performance.mark("afterProgram"); + ts.performance.measure("Program", "beforeProgram", "afterProgram"); + ts.tracing?.pop(); - function addResolutionDiagnostics(list: ts.Diagnostic[] | undefined) { - if (!list) - return; - for (const elem of list) { - programDiagnostics.add(elem); - } - } + return program; - function pullDiagnosticsFromCache(names: string[] | readonly ts.FileReference[], containingFile: ts.SourceFile) { - if (!moduleResolutionCache) - return; - const containingFileName = ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory); - const containingFileMode = !ts.isString(containingFile) ? containingFile.impliedNodeFormat : undefined; - const containingDir = ts.getDirectoryPath(containingFileName); - const redirectedReference = getRedirectReferenceForResolution(containingFile); - let i = 0; - for (const n of names) { - // mimics logic done in the resolution cache, should be resilient to upgrading it to use `FileReference`s for non-type-reference modal lookups to make it rely on the index in the list less - const mode = typeof n === "string" ? getModeForResolutionAtIndex(containingFile, i) : getModeForFileReference(n, containingFileMode); - const name = typeof n === "string" ? n : n.fileName; - i++; - // only nonrelative names hit the cache, and, at least as of right now, only nonrelative names can issue diagnostics - // (Since diagnostics are only issued via import or export map lookup) - // This may totally change if/when the issue of output paths not mapping to input files is fixed in a broader context - // When it is, how we extract diagnostics from the module name resolver will have the be refined - the current cache - // APIs wrapping the underlying resolver make it almost impossible to smuggle the diagnostics out in a generalized way - if (ts.isExternalModuleNameRelative(name)) - continue; - const diags = moduleResolutionCache.getOrCreateCacheForModuleName(name, mode, redirectedReference).get(containingDir)?.resolutionDiagnostics; - addResolutionDiagnostics(diags); - } + function addResolutionDiagnostics(list: ts.Diagnostic[] | undefined) { + if (!list) + return; + for (const elem of list) { + programDiagnostics.add(elem); } + } - function resolveModuleNamesWorker(moduleNames: string[], containingFile: ts.SourceFile, reusedNames: string[] | undefined): readonly ts.ResolvedModuleFull[] { - if (!moduleNames.length) - return ts.emptyArray; - const containingFileName = ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory); - const redirectedReference = getRedirectReferenceForResolution(containingFile); - ts.tracing?.push(ts.tracing.Phase.Program, "resolveModuleNamesWorker", { containingFileName }); - ts.performance.mark("beforeResolveModule"); - const result = actualResolveModuleNamesWorker(moduleNames, containingFile, containingFileName, reusedNames, redirectedReference); - ts.performance.mark("afterResolveModule"); - ts.performance.measure("ResolveModule", "beforeResolveModule", "afterResolveModule"); - ts.tracing?.pop(); - pullDiagnosticsFromCache(moduleNames, containingFile); - return result; + function pullDiagnosticsFromCache(names: string[] | readonly ts.FileReference[], containingFile: ts.SourceFile) { + if (!moduleResolutionCache) + return; + const containingFileName = ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory); + const containingFileMode = !ts.isString(containingFile) ? containingFile.impliedNodeFormat : undefined; + const containingDir = ts.getDirectoryPath(containingFileName); + const redirectedReference = getRedirectReferenceForResolution(containingFile); + let i = 0; + for (const n of names) { + // mimics logic done in the resolution cache, should be resilient to upgrading it to use `FileReference`s for non-type-reference modal lookups to make it rely on the index in the list less + const mode = typeof n === "string" ? getModeForResolutionAtIndex(containingFile, i) : getModeForFileReference(n, containingFileMode); + const name = typeof n === "string" ? n : n.fileName; + i++; + // only nonrelative names hit the cache, and, at least as of right now, only nonrelative names can issue diagnostics + // (Since diagnostics are only issued via import or export map lookup) + // This may totally change if/when the issue of output paths not mapping to input files is fixed in a broader context + // When it is, how we extract diagnostics from the module name resolver will have the be refined - the current cache + // APIs wrapping the underlying resolver make it almost impossible to smuggle the diagnostics out in a generalized way + if (ts.isExternalModuleNameRelative(name)) + continue; + const diags = moduleResolutionCache.getOrCreateCacheForModuleName(name, mode, redirectedReference).get(containingDir)?.resolutionDiagnostics; + addResolutionDiagnostics(diags); } + } - function resolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string | ts.SourceFile): readonly (ts.ResolvedTypeReferenceDirective | undefined)[] { - if (!typeDirectiveNames.length) - return []; - const containingFileName = !ts.isString(containingFile) ? ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory) : containingFile; - const redirectedReference = !ts.isString(containingFile) ? getRedirectReferenceForResolution(containingFile) : undefined; - const containingFileMode = !ts.isString(containingFile) ? containingFile.impliedNodeFormat : undefined; - ts.tracing?.push(ts.tracing.Phase.Program, "resolveTypeReferenceDirectiveNamesWorker", { containingFileName }); - ts.performance.mark("beforeResolveTypeReference"); - const result = actualResolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames, containingFileName, redirectedReference, containingFileMode); - ts.performance.mark("afterResolveTypeReference"); - ts.performance.measure("ResolveTypeReference", "beforeResolveTypeReference", "afterResolveTypeReference"); - ts.tracing?.pop(); - return result; - } + function resolveModuleNamesWorker(moduleNames: string[], containingFile: ts.SourceFile, reusedNames: string[] | undefined): readonly ts.ResolvedModuleFull[] { + if (!moduleNames.length) + return ts.emptyArray; + const containingFileName = ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory); + const redirectedReference = getRedirectReferenceForResolution(containingFile); + ts.tracing?.push(ts.tracing.Phase.Program, "resolveModuleNamesWorker", { containingFileName }); + ts.performance.mark("beforeResolveModule"); + const result = actualResolveModuleNamesWorker(moduleNames, containingFile, containingFileName, reusedNames, redirectedReference); + ts.performance.mark("afterResolveModule"); + ts.performance.measure("ResolveModule", "beforeResolveModule", "afterResolveModule"); + ts.tracing?.pop(); + pullDiagnosticsFromCache(moduleNames, containingFile); + return result; + } - function getRedirectReferenceForResolution(file: ts.SourceFile) { - const redirect = getResolvedProjectReferenceToRedirect(file.originalFileName); - if (redirect || !ts.isDeclarationFileName(file.originalFileName)) - return redirect; + function resolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string | ts.SourceFile): readonly (ts.ResolvedTypeReferenceDirective | undefined)[] { + if (!typeDirectiveNames.length) + return []; + const containingFileName = !ts.isString(containingFile) ? ts.getNormalizedAbsolutePath(containingFile.originalFileName, currentDirectory) : containingFile; + const redirectedReference = !ts.isString(containingFile) ? getRedirectReferenceForResolution(containingFile) : undefined; + const containingFileMode = !ts.isString(containingFile) ? containingFile.impliedNodeFormat : undefined; + ts.tracing?.push(ts.tracing.Phase.Program, "resolveTypeReferenceDirectiveNamesWorker", { containingFileName }); + ts.performance.mark("beforeResolveTypeReference"); + const result = actualResolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames, containingFileName, redirectedReference, containingFileMode); + ts.performance.mark("afterResolveTypeReference"); + ts.performance.measure("ResolveTypeReference", "beforeResolveTypeReference", "afterResolveTypeReference"); + ts.tracing?.pop(); + return result; + } - // The originalFileName could not be actual source file name if file found was d.ts from referecned project - // So in this case try to look up if this is output from referenced project, if it is use the redirected project in that case - const resultFromDts = getRedirectReferenceForResolutionFromSourceOfProject(file.path); - if (resultFromDts) - return resultFromDts; + function getRedirectReferenceForResolution(file: ts.SourceFile) { + const redirect = getResolvedProjectReferenceToRedirect(file.originalFileName); + if (redirect || !ts.isDeclarationFileName(file.originalFileName)) + return redirect; - // 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 (!host.realpath || !options.preserveSymlinks || !ts.stringContains(file.originalFileName, ts.nodeModulesPathPart)) - return undefined; - const realDeclarationPath = toPath(host.realpath(file.originalFileName)); - return realDeclarationPath === file.path ? undefined : getRedirectReferenceForResolutionFromSourceOfProject(realDeclarationPath); - } + // The originalFileName could not be actual source file name if file found was d.ts from referecned project + // So in this case try to look up if this is output from referenced project, if it is use the redirected project in that case + const resultFromDts = getRedirectReferenceForResolutionFromSourceOfProject(file.path); + if (resultFromDts) + return resultFromDts; + + // 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 (!host.realpath || !options.preserveSymlinks || !ts.stringContains(file.originalFileName, ts.nodeModulesPathPart)) + return undefined; + const realDeclarationPath = toPath(host.realpath(file.originalFileName)); + return realDeclarationPath === file.path ? undefined : getRedirectReferenceForResolutionFromSourceOfProject(realDeclarationPath); + } - function getRedirectReferenceForResolutionFromSourceOfProject(filePath: ts.Path) { - const source = getSourceOfProjectReferenceRedirect(filePath); - if (ts.isString(source)) - return getResolvedProjectReferenceToRedirect(source); - if (!source) + function getRedirectReferenceForResolutionFromSourceOfProject(filePath: ts.Path) { + const source = getSourceOfProjectReferenceRedirect(filePath); + if (ts.isString(source)) + return getResolvedProjectReferenceToRedirect(source); + if (!source) + return undefined; + // Output of .d.ts file so return resolved ref that matches the out file name + return forEachResolvedProjectReference(resolvedRef => { + const out = ts.outFile(resolvedRef.commandLine.options); + if (!out) return undefined; - // Output of .d.ts file so return resolved ref that matches the out file name - return forEachResolvedProjectReference(resolvedRef => { - const out = ts.outFile(resolvedRef.commandLine.options); - if (!out) - return undefined; - return toPath(out) === filePath ? resolvedRef : undefined; - }); - } + return toPath(out) === filePath ? resolvedRef : undefined; + }); + } - function compareDefaultLibFiles(a: ts.SourceFile, b: ts.SourceFile) { - return ts.compareValues(getDefaultLibFilePriority(a), getDefaultLibFilePriority(b)); - } + function compareDefaultLibFiles(a: ts.SourceFile, b: ts.SourceFile) { + return ts.compareValues(getDefaultLibFilePriority(a), getDefaultLibFilePriority(b)); + } - function getDefaultLibFilePriority(a: ts.SourceFile) { - if (ts.containsPath(defaultLibraryPath, a.fileName, /*ignoreCase*/ false)) { - const basename = ts.getBaseFileName(a.fileName); - if (basename === "lib.d.ts" || basename === "lib.es6.d.ts") - return 0; - const name = ts.removeSuffix(ts.removePrefix(basename, "lib."), ".d.ts"); - const index = ts.libs.indexOf(name); - if (index !== -1) - return index + 1; - } - return ts.libs.length + 2; - } + function getDefaultLibFilePriority(a: ts.SourceFile) { + if (ts.containsPath(defaultLibraryPath, a.fileName, /*ignoreCase*/ false)) { + const basename = ts.getBaseFileName(a.fileName); + if (basename === "lib.d.ts" || basename === "lib.es6.d.ts") + return 0; + const name = ts.removeSuffix(ts.removePrefix(basename, "lib."), ".d.ts"); + const index = ts.libs.indexOf(name); + if (index !== -1) + return index + 1; + } + return ts.libs.length + 2; + } - function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined { - return moduleResolutionCache && ts.resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache, mode); - } + function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined { + return moduleResolutionCache && ts.resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache, mode); + } - function toPath(fileName: string): ts.Path { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); + function toPath(fileName: string): ts.Path { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); + } + + function getCommonSourceDirectory() { + if (commonSourceDirectory === undefined) { + const emittedFiles = ts.filter(files, file => ts.sourceFileMayBeEmitted(file, program)); + commonSourceDirectory = ts.getCommonSourceDirectory(options, () => ts.mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), currentDirectory, getCanonicalFileName, commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory)); } + return commonSourceDirectory; + } + + function getClassifiableNames() { + if (!classifiableNames) { + // Initialize a checker so that all our files are bound. + getTypeChecker(); + classifiableNames = new ts.Set(); - function getCommonSourceDirectory() { - if (commonSourceDirectory === undefined) { - const emittedFiles = ts.filter(files, file => ts.sourceFileMayBeEmitted(file, program)); - commonSourceDirectory = ts.getCommonSourceDirectory(options, () => ts.mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), currentDirectory, getCanonicalFileName, commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory)); + for (const sourceFile of files) { + sourceFile.classifiableNames?.forEach(value => classifiableNames.add(value)); } - return commonSourceDirectory; } - function getClassifiableNames() { - if (!classifiableNames) { - // Initialize a checker so that all our files are bound. - getTypeChecker(); - classifiableNames = new ts.Set(); + return classifiableNames; + } - for (const sourceFile of files) { - sourceFile.classifiableNames?.forEach(value => classifiableNames.add(value)); - } - } + function resolveModuleNamesReusingOldState(moduleNames: string[], file: ts.SourceFile): readonly ts.ResolvedModuleFull[] { + if (structureIsReused === ts.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, file, /*reusedNames*/ undefined); + } - return classifiableNames; - } - - function resolveModuleNamesReusingOldState(moduleNames: string[], file: ts.SourceFile): readonly ts.ResolvedModuleFull[] { - if (structureIsReused === ts.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, file, /*reusedNames*/ undefined); - } - - const oldSourceFile = oldProgram && oldProgram.getSourceFile(file.fileName); - 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: ts.ResolvedModuleFull[] = []; - let i = 0; - for (const moduleName of moduleNames) { - const resolvedModule = file.resolvedModules.get(moduleName, getModeForResolutionAtIndex(file, i))!; - i++; - result.push(resolvedModule); - } - return result; + const oldSourceFile = oldProgram && oldProgram.getSourceFile(file.fileName); + 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: ts.ResolvedModuleFull[] = []; + let i = 0; + for (const moduleName of moduleNames) { + const resolvedModule = file.resolvedModules.get(moduleName, getModeForResolutionAtIndex(file, i))!; + i++; + result.push(resolvedModule); } - // 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: ts.ResolvedModuleFull[] | undefined; - let reusedNames: string[] | undefined; - /** A transient placeholder used to mark predicted resolution in the result list. */ - const predictedToResolveToAmbientModuleMarker: ts.ResolvedModuleFull = {} as any; - - 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 = ts.getResolvedModule(oldSourceFile, moduleName, getModeForResolutionAtIndex(oldSourceFile, i)); - if (oldResolvedModule) { - if (ts.isTraceEnabled(options, host)) { - ts.trace(host, oldResolvedModule.packageId ? - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2, moduleName, ts.getNormalizedAbsolutePath(file.originalFileName, currentDirectory), oldResolvedModule.resolvedFileName, oldResolvedModule.packageId && ts.packageIdToString(oldResolvedModule.packageId)); - } - (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 (ts.contains(file.ambientModuleNames, moduleName)) { - resolvesToAmbientModuleInNonModifiedFile = true; + 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: ts.ResolvedModuleFull[] | undefined; + let reusedNames: string[] | undefined; + /** A transient placeholder used to mark predicted resolution in the result list. */ + const predictedToResolveToAmbientModuleMarker: ts.ResolvedModuleFull = {} as any; + + 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 = ts.getResolvedModule(oldSourceFile, moduleName, getModeForResolutionAtIndex(oldSourceFile, i)); + if (oldResolvedModule) { if (ts.isTraceEnabled(options, host)) { - ts.trace(host, ts.Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, ts.getNormalizedAbsolutePath(file.originalFileName, currentDirectory)); + ts.trace(host, oldResolvedModule.packageId ? + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2, moduleName, ts.getNormalizedAbsolutePath(file.originalFileName, currentDirectory), oldResolvedModule.resolvedFileName, oldResolvedModule.packageId && ts.packageIdToString(oldResolvedModule.packageId)); } + (result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule; + (reusedNames || (reusedNames = [])).push(moduleName); + continue; } - else { - resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, i); - } - - 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); + } + // 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 (ts.contains(file.ambientModuleNames, moduleName)) { + resolvesToAmbientModuleInNonModifiedFile = true; + if (ts.isTraceEnabled(options, host)) { + ts.trace(host, ts.Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, ts.getNormalizedAbsolutePath(file.originalFileName, currentDirectory)); } } - - const resolutions = unknownModuleNames && unknownModuleNames.length - ? resolveModuleNamesWorker(unknownModuleNames, file, reusedNames) - : ts.emptyArray; - - // Combine results of resolutions and predicted results - if (!result) { - // There were no unresolved/ambient resolutions. - ts.Debug.assert(resolutions.length === moduleNames.length); - return resolutions; + else { + resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, i); } - 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 - } - } - else { - result[i] = resolutions[j]; - j++; - } + if (resolvesToAmbientModuleInNonModifiedFile) { + (result || (result = new Array(moduleNames.length)))[i] = predictedToResolveToAmbientModuleMarker; } - ts.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, index: number): boolean { - if (index >= ts.length(oldSourceFile?.imports) + ts.length(oldSourceFile?.moduleAugmentations)) - return false; // mode index out of bounds, don't reuse resolution - const resolutionToFile = ts.getResolvedModule(oldSourceFile, moduleName, oldSourceFile && getModeForResolutionAtIndex(oldSourceFile, index)); - 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; - } - - if (ts.isTraceEnabled(options, host)) { - ts.trace(host, ts.Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, unmodifiedFile); - } - return true; + 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, parent, index) => { - 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 || - !ts.arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newResolvedRef.commandLine.fileNames); - } - 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 !ts.arrayIsEqualTo(oldProjectReferences, newReferences, ts.projectReferenceIsEqualTo); - }); + const resolutions = unknownModuleNames && unknownModuleNames.length + ? resolveModuleNamesWorker(unknownModuleNames, file, reusedNames) + : ts.emptyArray; + + // Combine results of resolutions and predicted results + if (!result) { + // There were no unresolved/ambient resolutions. + ts.Debug.assert(resolutions.length === moduleNames.length); + return resolutions; } - function tryReuseStructureFromOldProgram(): ts.StructureIsReused { - if (!oldProgram) { - return ts.StructureIsReused.Not; + 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 + } } - - // 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 (ts.changesAffectModuleResolution(oldOptions, options)) { - return ts.StructureIsReused.Not; + else { + result[i] = resolutions[j]; + j++; } + } + ts.Debug.assert(j === resolutions.length); - // there is an old program, check if we can reuse its structure - const oldRootNames = oldProgram.getRootFileNames(); - if (!ts.arrayIsEqualTo(oldRootNames, rootNames)) { - return ts.StructureIsReused.Not; - } + return result; - // Check if any referenced project tsconfig files are different - if (!canReuseProjectReferences()) { - return ts.StructureIsReused.Not; - } - if (projectReferences) { - resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + // If we change our policy of rechecking failed lookups on each program create, + // we should adjust the value returned here. + function moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName: string, index: number): boolean { + if (index >= ts.length(oldSourceFile?.imports) + ts.length(oldSourceFile?.moduleAugmentations)) + return false; // mode index out of bounds, don't reuse resolution + const resolutionToFile = ts.getResolvedModule(oldSourceFile, moduleName, oldSourceFile && getModeForResolutionAtIndex(oldSourceFile, index)); + 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 program source files has changed in the way that can affect structure of the program - const newSourceFiles: ts.SourceFile[] = []; - const modifiedSourceFiles: { - oldFile: ts.SourceFile; - newFile: ts.SourceFile; - }[] = []; - structureIsReused = ts.StructureIsReused.Completely; + // at least one of declarations should come from non-modified source file + const unmodifiedFile = ambientModuleNameToUnmodifiedFileName.get(moduleName); - // 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 ts.StructureIsReused.Not; + if (!unmodifiedFile) { + return false; } - const oldSourceFiles = oldProgram.getSourceFiles(); - const enum SeenPackageName { - Exists, - Modified + if (ts.isTraceEnabled(options, host)) { + ts.trace(host, ts.Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, unmodifiedFile); } - const seenPackageNames = new ts.Map(); - - for (const oldSourceFile of oldSourceFiles) { - let newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options), /*onError*/ undefined, shouldCreateNewSourceFile) - : host.getSourceFile(oldSourceFile.fileName, getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options), /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 - - if (!newSourceFile) { - return ts.StructureIsReused.Not; - } - - ts.Debug.assert(!newSourceFile.redirectInfo, "Host should not return a redirect source file from `getSourceFile`"); + return true; + } + } - 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 ts.StructureIsReused.Not; - } - 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 ts.StructureIsReused.Not; - } - fileChanged = false; + function canReuseProjectReferences(): boolean { + return !forEachProjectReference(oldProgram!.getProjectReferences(), oldProgram!.getResolvedProjectReferences(), (oldResolvedRef, parent, index) => { + 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 || + !ts.arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newResolvedRef.commandLine.fileNames); } 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 ts.StructureIsReused.Not; - } - seenPackageNames.set(packageName, newKind); + // 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 !ts.arrayIsEqualTo(oldProjectReferences, newReferences, ts.projectReferenceIsEqualTo); + }); + } - if (fileChanged) { - // The `newSourceFile` object was created for the new program. + function tryReuseStructureFromOldProgram(): ts.StructureIsReused { + if (!oldProgram) { + return ts.StructureIsReused.Not; + } - if (!ts.arrayIsEqualTo(oldSourceFile.libReferenceDirectives, newSourceFile.libReferenceDirectives, fileReferenceIsEqualTo)) { - // 'lib' references has changed. Matches behavior in changesAffectModuleResolution - structureIsReused = ts.StructureIsReused.SafeModules; - } + // 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 (ts.changesAffectModuleResolution(oldOptions, options)) { + return ts.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 - structureIsReused = ts.StructureIsReused.SafeModules; - } + // there is an old program, check if we can reuse its structure + const oldRootNames = oldProgram.getRootFileNames(); + if (!ts.arrayIsEqualTo(oldRootNames, rootNames)) { + return ts.StructureIsReused.Not; + } - // check tripleslash references - if (!ts.arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { - // tripleslash references has changed - structureIsReused = ts.StructureIsReused.SafeModules; - } + // Check if any referenced project tsconfig files are different + if (!canReuseProjectReferences()) { + return ts.StructureIsReused.Not; + } + if (projectReferences) { + resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + } - // check imports and module augmentations - collectExternalModuleReferences(newSourceFile); - if (!ts.arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { - // imports has changed - structureIsReused = ts.StructureIsReused.SafeModules; - } - if (!ts.arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { - // moduleAugmentations has changed - structureIsReused = ts.StructureIsReused.SafeModules; - } - if ((oldSourceFile.flags & ts.NodeFlags.PermanentlySetIncrementalFlags) !== (newSourceFile.flags & ts.NodeFlags.PermanentlySetIncrementalFlags)) { - // dynamicImport has changed - structureIsReused = ts.StructureIsReused.SafeModules; - } + // check if program source files has changed in the way that can affect structure of the program + const newSourceFiles: ts.SourceFile[] = []; + const modifiedSourceFiles: { + oldFile: ts.SourceFile; + newFile: ts.SourceFile; + }[] = []; + structureIsReused = ts.StructureIsReused.Completely; - if (!ts.arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { - // 'types' references has changed - structureIsReused = ts.StructureIsReused.SafeModules; - } + // 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 ts.StructureIsReused.Not; + } - // tentatively approve the file - modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); - } - else if (hasInvalidatedResolution(oldSourceFile.path)) { - // 'module/types' references could have changed - structureIsReused = ts.StructureIsReused.SafeModules; + const oldSourceFiles = oldProgram.getSourceFiles(); + const enum SeenPackageName { + Exists, + Modified + } + const seenPackageNames = new ts.Map(); - // add file to the modified list so that we will resolve it later - modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); - } + for (const oldSourceFile of oldSourceFiles) { + let newSourceFile = host.getSourceFileByPath + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options), /*onError*/ undefined, shouldCreateNewSourceFile) + : host.getSourceFile(oldSourceFile.fileName, getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options), /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 - // if file has passed all checks it should be safe to reuse it - newSourceFiles.push(newSourceFile); + if (!newSourceFile) { + return ts.StructureIsReused.Not; } - if (structureIsReused !== ts.StructureIsReused.Completely) { - return structureIsReused; - } + ts.Debug.assert(!newSourceFile.redirectInfo, "Host should not return a redirect source file from `getSourceFile`"); - const modifiedFiles = modifiedSourceFiles.map(f => f.oldFile); - for (const oldFile of oldSourceFiles) { - if (!ts.contains(modifiedFiles, oldFile)) { - for (const moduleName of oldFile.ambientModuleNames) { - ambientModuleNameToUnmodifiedFileName.set(moduleName, oldFile.fileName); - } + 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 ts.StructureIsReused.Not; } + fileChanged = false; + newSourceFile = oldSourceFile; // Use the redirect. } - // try to verify results of module resolution - for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { - const moduleNames = getModuleNames(newSourceFile); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFile); - // ensure that module resolution results are still correct - const resolutionsChanged = ts.hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, oldSourceFile, ts.moduleResolutionIsEqualTo); - if (resolutionsChanged) { - structureIsReused = ts.StructureIsReused.SafeModules; - newSourceFile.resolvedModules = ts.zipToModeAwareCache(newSourceFile, moduleNames, resolutions); - } - else { - newSourceFile.resolvedModules = oldSourceFile.resolvedModules; - } - const typesReferenceDirectives = newSourceFile.typeReferenceDirectives; - const typeReferenceResolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFile); - // ensure that types resolutions are still correct - const typeReferenceResolutionsChanged = ts.hasChangesInResolutions(typesReferenceDirectives, typeReferenceResolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, oldSourceFile, ts.typeDirectiveIsEqualTo); - if (typeReferenceResolutionsChanged) { - structureIsReused = ts.StructureIsReused.SafeModules; - newSourceFile.resolvedTypeReferenceDirectiveNames = ts.zipToModeAwareCache(newSourceFile, typesReferenceDirectives, typeReferenceResolutions); + else if (oldProgram.redirectTargetsMap.has(oldSourceFile.path)) { + // If a redirected-to source file changes, the redirect may be broken. + if (newSourceFile !== oldSourceFile) { + return ts.StructureIsReused.Not; } - else { - newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; + 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 ts.StructureIsReused.Not; } + seenPackageNames.set(packageName, newKind); } - if (structureIsReused !== ts.StructureIsReused.Completely) { - return structureIsReused; - } + if (fileChanged) { + // The `newSourceFile` object was created for the new program. - if (ts.changesAffectingProgramStructure(oldOptions, options) || host.hasChangedAutomaticTypeDirectiveNames?.()) { - return ts.StructureIsReused.SafeModules; - } + if (!ts.arrayIsEqualTo(oldSourceFile.libReferenceDirectives, newSourceFile.libReferenceDirectives, fileReferenceIsEqualTo)) { + // 'lib' references has changed. Matches behavior in changesAffectModuleResolution + structureIsReused = ts.StructureIsReused.SafeModules; + } - missingFilePaths = oldProgram.getMissingFilePaths(); + 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 + structureIsReused = ts.StructureIsReused.SafeModules; + } - // update fileName -> file mapping - ts.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; + // check tripleslash references + if (!ts.arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { + // tripleslash references has changed + structureIsReused = ts.StructureIsReused.SafeModules; } - 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; + + // check imports and module augmentations + collectExternalModuleReferences(newSourceFile); + if (!ts.arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { + // imports has changed + structureIsReused = ts.StructureIsReused.SafeModules; + } + if (!ts.arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { + // moduleAugmentations has changed + structureIsReused = ts.StructureIsReused.SafeModules; + } + if ((oldSourceFile.flags & ts.NodeFlags.PermanentlySetIncrementalFlags) !== (newSourceFile.flags & ts.NodeFlags.PermanentlySetIncrementalFlags)) { + // dynamicImport has changed + structureIsReused = ts.StructureIsReused.SafeModules; } - filesByName.set(path, filesByName.get(oldFile.path)); - }); - files = newSourceFiles; - fileReasons = oldProgram.getFileIncludeReasons(); - fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); - resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); - - sourceFileToPackageName = oldProgram.sourceFileToPackageName; - redirectTargetsMap = oldProgram.redirectTargetsMap; - usesUriStyleNodeCoreModules = oldProgram.usesUriStyleNodeCoreModules; - - return ts.StructureIsReused.Completely; - } - - function getEmitHost(writeFileCallback?: ts.WriteFileCallback): ts.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, - getProjectReferenceRedirect, - isSourceOfProjectReferenceRedirect, - getSymlinkCache, - writeFile: writeFileCallback || writeFile, - isEmitBlocked, - readFile: f => host.readFile(f), - fileExists: f => { - // Use local caches - const path = toPath(f); - if (getSourceFileByPath(path)) - return true; - if (ts.contains(missingFilePaths, path)) - return false; - // Before falling back to the host - return host.fileExists(f); - }, - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), - getProgramBuildInfo: () => program.getProgramBuildInfo && program.getProgramBuildInfo(), - getSourceFileFromReference: (file, ref) => program.getSourceFileFromReference(file, ref), - redirectTargetsMap, - getFileIncludeReasons: program.getFileIncludeReasons, - }; - } + if (!ts.arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { + // 'types' references has changed + structureIsReused = ts.StructureIsReused.SafeModules; + } - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: readonly ts.SourceFile[], data?: ts.WriteFileCallbackData) { - host.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); - } + // tentatively approve the file + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } + else if (hasInvalidatedResolution(oldSourceFile.path)) { + // 'module/types' references could have changed + structureIsReused = ts.StructureIsReused.SafeModules; - function emitBuildInfo(writeFileCallback?: ts.WriteFileCallback): ts.EmitResult { - ts.Debug.assert(!ts.outFile(options)); - ts.tracing?.push(ts.tracing.Phase.Emit, "emitBuildInfo", {}, /*separateBeginAndEnd*/ true); - ts.performance.mark("beforeEmit"); - const emitResult = ts.emitFiles(ts.notImplementedResolver, getEmitHost(writeFileCallback), - /*targetSourceFile*/ undefined, ts.noTransformers, - /*emitOnlyDtsFiles*/ false, - /*onlyBuildInfo*/ true); - ts.performance.mark("afterEmit"); - ts.performance.measure("Emit", "beforeEmit", "afterEmit"); - ts.tracing?.pop(); - return emitResult; - } + // add file to the modified list so that we will resolve it later + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } - function getResolvedProjectReferences() { - return resolvedProjectReferences; + // if file has passed all checks it should be safe to reuse it + newSourceFiles.push(newSourceFile); } - function getProjectReferences() { - return projectReferences; + if (structureIsReused !== ts.StructureIsReused.Completely) { + return structureIsReused; } - 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); - }); + const modifiedFiles = modifiedSourceFiles.map(f => f.oldFile); + for (const oldFile of oldSourceFiles) { + if (!ts.contains(modifiedFiles, oldFile)) { + for (const moduleName of oldFile.ambientModuleNames) { + ambientModuleNameToUnmodifiedFileName.set(moduleName, oldFile.fileName); } - function isSourceFileFromExternalLibrary(file: ts.SourceFile): boolean { - return !!sourceFilesFoundSearchingNodeModules.get(file.path); - } - - function isSourceFileDefaultLibrary(file: ts.SourceFile): boolean { - if (!file.isDeclarationFile) { - return false; } - - if (file.hasNoDefaultLib) { - return true; + } + // try to verify results of module resolution + for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { + const moduleNames = getModuleNames(newSourceFile); + const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFile); + // ensure that module resolution results are still correct + const resolutionsChanged = ts.hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, oldSourceFile, ts.moduleResolutionIsEqualTo); + if (resolutionsChanged) { + structureIsReused = ts.StructureIsReused.SafeModules; + newSourceFile.resolvedModules = ts.zipToModeAwareCache(newSourceFile, moduleNames, resolutions); } - - if (!options.noLib) { - return false; + else { + newSourceFile.resolvedModules = oldSourceFile.resolvedModules; } - - // 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() ? ts.equateStringsCaseSensitive : ts.equateStringsCaseInsensitive; - if (!options.lib) { - return equalityComparer(file.fileName, getDefaultLibraryFileName()); + const typesReferenceDirectives = newSourceFile.typeReferenceDirectives; + const typeReferenceResolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFile); + // ensure that types resolutions are still correct + const typeReferenceResolutionsChanged = ts.hasChangesInResolutions(typesReferenceDirectives, typeReferenceResolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, oldSourceFile, ts.typeDirectiveIsEqualTo); + if (typeReferenceResolutionsChanged) { + structureIsReused = ts.StructureIsReused.SafeModules; + newSourceFile.resolvedTypeReferenceDirectiveNames = ts.zipToModeAwareCache(newSourceFile, typesReferenceDirectives, typeReferenceResolutions); } else { - return ts.some(options.lib, libFileName => equalityComparer(file.fileName, pathForLibFile(libFileName))); + newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; } } - function getTypeChecker() { - return typeChecker || (typeChecker = ts.createTypeChecker(program)); - } - - function emit(sourceFile?: ts.SourceFile, writeFileCallback?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitResult { - ts.tracing?.push(ts.tracing.Phase.Emit, "emit", { path: sourceFile?.path }, /*separateBeginAndEnd*/ true); - const result = runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers, forceDtsEmit)); - ts.tracing?.pop(); - return result; - } - - function isEmitBlocked(emitFileName: string): boolean { - return hasEmitBlockingDiagnostics.has(toPath(emitFileName)); + if (structureIsReused !== ts.StructureIsReused.Completely) { + return structureIsReused; } - function emitWorker(program: ts.Program, sourceFile: ts.SourceFile | undefined, writeFileCallback: ts.WriteFileCallback | undefined, cancellationToken: ts.CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitResult { - if (!forceDtsEmit) { - const result = handleNoEmitOptions(program, sourceFile, writeFileCallback, 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 = getTypeChecker().getEmitResolver(ts.outFile(options) ? undefined : sourceFile, cancellationToken); - ts.performance.mark("beforeEmit"); - const emitResult = ts.emitFiles(emitResolver, getEmitHost(writeFileCallback), sourceFile, ts.getTransformers(options, customTransformers, emitOnlyDtsFiles), emitOnlyDtsFiles, - /*onlyBuildInfo*/ false, forceDtsEmit); - ts.performance.mark("afterEmit"); - ts.performance.measure("Emit", "beforeEmit", "afterEmit"); - return emitResult; + if (ts.changesAffectingProgramStructure(oldOptions, options) || host.hasChangedAutomaticTypeDirectiveNames?.()) { + return ts.StructureIsReused.SafeModules; } - function getSourceFile(fileName: string): ts.SourceFile | undefined { - return getSourceFileByPath(toPath(fileName)); - } + missingFilePaths = oldProgram.getMissingFilePaths(); - function getSourceFileByPath(path: ts.Path): ts.SourceFile | undefined { - return filesByName.get(path) || undefined; + // update fileName -> file mapping + ts.Debug.assert(newSourceFiles.length === oldProgram.getSourceFiles().length); + for (const newSourceFile of newSourceFiles) { + filesByName.set(newSourceFile.path, newSourceFile); } - - function getDiagnosticsHelper(sourceFile: ts.SourceFile | undefined, getDiagnostics: (sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined) => readonly T[], cancellationToken: ts.CancellationToken | undefined): readonly T[] { - if (sourceFile) { - return getDiagnostics(sourceFile, cancellationToken); + const oldFilesByNameMap = oldProgram.getFilesByNameMap(); + oldFilesByNameMap.forEach((oldFile, path) => { + if (!oldFile) { + filesByName.set(path, oldFile); + return; } - return ts.sortAndDeduplicateDiagnostics(ts.flatMap(program.getSourceFiles(), sourceFile => { - if (cancellationToken) { - cancellationToken.throwIfCancellationRequested(); + 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 getDiagnostics(sourceFile, cancellationToken); - })); - } - - function getSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { - return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken); - } - - function getSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { - return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken); - } + return; + } + filesByName.set(path, filesByName.get(oldFile.path)); + }); - function getCachedSemanticDiagnostics(sourceFile?: ts.SourceFile): readonly ts.Diagnostic[] | undefined { - return sourceFile - ? cachedBindAndCheckDiagnosticsForFile.perFile?.get(sourceFile.path) - : cachedBindAndCheckDiagnosticsForFile.allDiagnostics; - } + files = newSourceFiles; + fileReasons = oldProgram.getFileIncludeReasons(); + fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); + resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); - function getBindAndCheckDiagnostics(sourceFile: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { - return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken); - } + sourceFileToPackageName = oldProgram.sourceFileToPackageName; + redirectTargetsMap = oldProgram.redirectTargetsMap; + usesUriStyleNodeCoreModules = oldProgram.usesUriStyleNodeCoreModules; - function getProgramDiagnostics(sourceFile: ts.SourceFile): readonly ts.Diagnostic[] { - if (ts.skipTypeChecking(sourceFile, options, program)) { - return ts.emptyArray; - } + return ts.StructureIsReused.Completely; + } - const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName); - if (!sourceFile.commentDirectives?.length) { - return programDiagnosticsInFile; - } + function getEmitHost(writeFileCallback?: ts.WriteFileCallback): ts.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, + getProjectReferenceRedirect, + isSourceOfProjectReferenceRedirect, + getSymlinkCache, + writeFile: writeFileCallback || writeFile, + isEmitBlocked, + readFile: f => host.readFile(f), + fileExists: f => { + // Use local caches + const path = toPath(f); + if (getSourceFileByPath(path)) + return true; + if (ts.contains(missingFilePaths, path)) + return false; + // Before falling back to the host + return host.fileExists(f); + }, + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), + getProgramBuildInfo: () => program.getProgramBuildInfo && program.getProgramBuildInfo(), + getSourceFileFromReference: (file, ref) => program.getSourceFileFromReference(file, ref), + redirectTargetsMap, + getFileIncludeReasons: program.getFileIncludeReasons, + }; + } - return getDiagnosticsWithPrecedingDirectives(sourceFile, sourceFile.commentDirectives, programDiagnosticsInFile).diagnostics; - } + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: readonly ts.SourceFile[], data?: ts.WriteFileCallbackData) { + host.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); + } - function getDeclarationDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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 || ts.outFile(options)) { - return getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); - } - else { - return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile, cancellationToken); - } - } + function emitBuildInfo(writeFileCallback?: ts.WriteFileCallback): ts.EmitResult { + ts.Debug.assert(!ts.outFile(options)); + ts.tracing?.push(ts.tracing.Phase.Emit, "emitBuildInfo", {}, /*separateBeginAndEnd*/ true); + ts.performance.mark("beforeEmit"); + const emitResult = ts.emitFiles(ts.notImplementedResolver, getEmitHost(writeFileCallback), + /*targetSourceFile*/ undefined, ts.noTransformers, + /*emitOnlyDtsFiles*/ false, + /*onlyBuildInfo*/ true); + ts.performance.mark("afterEmit"); + ts.performance.measure("Emit", "beforeEmit", "afterEmit"); + ts.tracing?.pop(); + return emitResult; + } - function getSyntacticDiagnosticsForFile(sourceFile: ts.SourceFile): readonly ts.DiagnosticWithLocation[] { - // For JavaScript files, we report semantic errors for using TypeScript-only - // constructs from within a JavaScript file as syntactic errors. - if (ts.isSourceFileJS(sourceFile)) { - if (!sourceFile.additionalSyntacticDiagnostics) { - sourceFile.additionalSyntacticDiagnostics = getJSSyntacticDiagnosticsForFile(sourceFile); - } - return ts.concatenate(sourceFile.additionalSyntacticDiagnostics, sourceFile.parseDiagnostics); - } - return sourceFile.parseDiagnostics; - } + function getResolvedProjectReferences() { + return resolvedProjectReferences; + } - function runWithCancellationToken(func: () => T): T { - try { - return func(); - } - catch (e) { - if (e instanceof ts.OperationCanceledException) { - // We were canceled while performing the operation. Because our type checker - // might be a bad state, we need to throw it away. - typeChecker = undefined!; - } + function getProjectReferences() { + return projectReferences; + } - throw e; + 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 getSemanticDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { - return ts.concatenate(filterSemanticDiagnostics(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), options), getProgramDiagnostics(sourceFile)); - } + function isSourceFileFromExternalLibrary(file: ts.SourceFile): boolean { + return !!sourceFilesFoundSearchingNodeModules.get(file.path); + } - function getBindAndCheckDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { - return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedBindAndCheckDiagnosticsForFile, getBindAndCheckDiagnosticsForFileNoCache); + function isSourceFileDefaultLibrary(file: ts.SourceFile): boolean { + if (!file.isDeclarationFile) { + return false; } - function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { - return runWithCancellationToken(() => { - if (ts.skipTypeChecking(sourceFile, options, program)) { - return ts.emptyArray; - } - - const typeChecker = getTypeChecker(); - - ts.Debug.assert(!!sourceFile.bindDiagnostics); - const isJs = sourceFile.scriptKind === ts.ScriptKind.JS || sourceFile.scriptKind === ts.ScriptKind.JSX; - const isCheckJs = isJs && ts.isCheckJsEnabledForFile(sourceFile, options); - const isPlainJs = ts.isPlainJsFile(sourceFile, options.checkJs); - const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false; - - // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External - // - plain JS: .js files with no // ts-check and checkJs: undefined - // - check JS: .js files with either // ts-check or checkJs: true - // - external: files that are added by plugins - const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ts.ScriptKind.TS || sourceFile.scriptKind === ts.ScriptKind.TSX - || sourceFile.scriptKind === ts.ScriptKind.External || isPlainJs || isCheckJs || sourceFile.scriptKind === ts.ScriptKind.Deferred); - let bindDiagnostics: readonly ts.Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : ts.emptyArray; - let checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : ts.emptyArray; - if (isPlainJs) { - bindDiagnostics = ts.filter(bindDiagnostics, d => plainJSErrors.has(d.code)); - checkDiagnostics = ts.filter(checkDiagnostics, d => plainJSErrors.has(d.code)); - } - // skip ts-expect-error errors in plain JS files, and skip JSDoc errors except in checked JS - return getMergedBindAndCheckDiagnostics(sourceFile, includeBindAndCheckDiagnostics && !isPlainJs, bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined); - }); + if (file.hasNoDefaultLib) { + return true; } - function getMergedBindAndCheckDiagnostics(sourceFile: ts.SourceFile, includeBindAndCheckDiagnostics: boolean, ...allDiagnostics: (readonly ts.Diagnostic[] | undefined)[]) { - const flatDiagnostics = ts.flatten(allDiagnostics); - if (!includeBindAndCheckDiagnostics || !sourceFile.commentDirectives?.length) { - return flatDiagnostics; - } - - const { diagnostics, directives } = getDiagnosticsWithPrecedingDirectives(sourceFile, sourceFile.commentDirectives, flatDiagnostics); - - for (const errorExpectation of directives.getUnusedExpectations()) { - diagnostics.push(ts.createDiagnosticForRange(sourceFile, errorExpectation.range, ts.Diagnostics.Unused_ts_expect_error_directive)); - } - - return diagnostics; + if (!options.noLib) { + return false; } - /** - * Creates a map of comment directives along with the diagnostics immediately preceded by one of them. - * Comments that match to any of those diagnostics are marked as used. - */ - function getDiagnosticsWithPrecedingDirectives(sourceFile: ts.SourceFile, commentDirectives: ts.CommentDirective[], flatDiagnostics: ts.Diagnostic[]) { - // Diagnostics are only reported if there is no comment directive preceding them - // This will modify the directives map by marking "used" ones with a corresponding diagnostic - const directives = ts.createCommentDirectivesMap(sourceFile, commentDirectives); - const diagnostics = flatDiagnostics.filter(diagnostic => markPrecedingCommentDirectiveLine(diagnostic, directives) === -1); - - return { diagnostics, directives }; + // 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() ? ts.equateStringsCaseSensitive : ts.equateStringsCaseInsensitive; + if (!options.lib) { + return equalityComparer(file.fileName, getDefaultLibraryFileName()); } - - function getSuggestionDiagnostics(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - return getTypeChecker().getSuggestionDiagnostics(sourceFile, cancellationToken); - }); + else { + return ts.some(options.lib, libFileName => equalityComparer(file.fileName, pathForLibFile(libFileName))); } + } - /** - * @returns The line index marked as preceding the diagnostic, or -1 if none was. - */ - function markPrecedingCommentDirectiveLine(diagnostic: ts.Diagnostic, directives: ts.CommentDirectivesMap) { - const { file, start } = diagnostic; - if (!file) { - return -1; - } - - // Start out with the line just before the text - const lineStarts = ts.getLineStarts(file); - let line = ts.computeLineAndCharacterOfPosition(lineStarts, start!).line - 1; // TODO: GH#18217 - while (line >= 0) { - // As soon as that line is known to have a comment directive, use that - if (directives.markUsed(line)) { - return line; - } + function getTypeChecker() { + return typeChecker || (typeChecker = ts.createTypeChecker(program)); + } - // Stop searching if the line is not empty and not a comment - const lineText = file.text.slice(lineStarts[line], lineStarts[line + 1]).trim(); - if (lineText !== "" && !/^(\s*)\/\/(.*)$/.test(lineText)) { - return -1; - } + function emit(sourceFile?: ts.SourceFile, writeFileCallback?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitResult { + ts.tracing?.push(ts.tracing.Phase.Emit, "emit", { path: sourceFile?.path }, /*separateBeginAndEnd*/ true); + const result = runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers, forceDtsEmit)); + ts.tracing?.pop(); + return result; + } - line--; - } + function isEmitBlocked(emitFileName: string): boolean { + return hasEmitBlockingDiagnostics.has(toPath(emitFileName)); + } - return -1; + function emitWorker(program: ts.Program, sourceFile: ts.SourceFile | undefined, writeFileCallback: ts.WriteFileCallback | undefined, cancellationToken: ts.CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: ts.CustomTransformers, forceDtsEmit?: boolean): ts.EmitResult { + if (!forceDtsEmit) { + const result = handleNoEmitOptions(program, sourceFile, writeFileCallback, cancellationToken); + if (result) + return result; } - function getJSSyntacticDiagnosticsForFile(sourceFile: ts.SourceFile): ts.DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - const diagnostics: ts.DiagnosticWithLocation[] = []; - walk(sourceFile, sourceFile); - ts.forEachChildRecursively(sourceFile, walk, walkArray); - - return diagnostics; - - function walk(node: ts.Node, parent: ts.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 ts.SyntaxKind.Parameter: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - if ((parent as ts.ParameterDeclaration | ts.PropertyDeclaration | ts.MethodDeclaration).questionToken === node) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); - return "skip"; - } - // falls through - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.VariableDeclaration: - // type annotation - if ((parent as ts.FunctionLikeDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration).type === node) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - } + // 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 = getTypeChecker().getEmitResolver(ts.outFile(options) ? undefined : sourceFile, cancellationToken); + ts.performance.mark("beforeEmit"); + const emitResult = ts.emitFiles(emitResolver, getEmitHost(writeFileCallback), sourceFile, ts.getTransformers(options, customTransformers, emitOnlyDtsFiles), emitOnlyDtsFiles, + /*onlyBuildInfo*/ false, forceDtsEmit); + ts.performance.mark("afterEmit"); + ts.performance.measure("Emit", "beforeEmit", "afterEmit"); + return emitResult; + } - switch (node.kind) { - case ts.SyntaxKind.ImportClause: - if ((node as ts.ImportClause).isTypeOnly) { - diagnostics.push(createDiagnosticForNode(parent, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "import type")); - return "skip"; - } - break; - case ts.SyntaxKind.ExportDeclaration: - if ((node as ts.ExportDeclaration).isTypeOnly) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "export type")); - return "skip"; - } - break; - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - if ((node as ts.ImportOrExportSpecifier).isTypeOnly) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, ts.isImportSpecifier(node) ? "import...type" : "export...type")); - return "skip"; - } - break; - case ts.SyntaxKind.ImportEqualsDeclaration: - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.import_can_only_be_used_in_TypeScript_files)); - return "skip"; - case ts.SyntaxKind.ExportAssignment: - if ((node as ts.ExportAssignment).isExportEquals) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.export_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - case ts.SyntaxKind.HeritageClause: - const heritageClause = node as ts.HeritageClause; - if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.implements_clauses_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - case ts.SyntaxKind.InterfaceDeclaration: - const interfaceKeyword = ts.tokenToString(ts.SyntaxKind.InterfaceKeyword); - ts.Debug.assertIsDefined(interfaceKeyword); - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, interfaceKeyword)); - return "skip"; - case ts.SyntaxKind.ModuleDeclaration: - const moduleKeyword = node.flags & ts.NodeFlags.Namespace ? ts.tokenToString(ts.SyntaxKind.NamespaceKeyword) : ts.tokenToString(ts.SyntaxKind.ModuleKeyword); - ts.Debug.assertIsDefined(moduleKeyword); - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, moduleKeyword)); - return "skip"; - case ts.SyntaxKind.TypeAliasDeclaration: - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files)); - return "skip"; - case ts.SyntaxKind.EnumDeclaration: - const enumKeyword = ts.Debug.checkDefined(ts.tokenToString(ts.SyntaxKind.EnumKeyword)); - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, enumKeyword)); - return "skip"; - case ts.SyntaxKind.NonNullExpression: - diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files)); - return "skip"; - case ts.SyntaxKind.AsExpression: - diagnostics.push(createDiagnosticForNode((node as ts.AsExpression).type, ts.Diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files)); - return "skip"; - case ts.SyntaxKind.TypeAssertionExpression: - ts.Debug.fail(); // Won't parse these in a JS file anyway, as they are interpreted as JSX. - } - } + function getSourceFile(fileName: string): ts.SourceFile | undefined { + return getSourceFileByPath(toPath(fileName)); + } - function walkArray(nodes: ts.NodeArray, parent: ts.Node) { - if (parent.decorators === nodes && !options.experimentalDecorators) { - diagnostics.push(createDiagnosticForNode(parent, ts.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)); - } + function getSourceFileByPath(path: ts.Path): ts.SourceFile | undefined { + return filesByName.get(path) || undefined; + } - switch (parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ArrowFunction: - // Check type parameters - if (nodes === (parent as ts.DeclarationWithTypeParameterChildren).typeParameters) { - diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Type_parameter_declarations_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - // falls through + function getDiagnosticsHelper(sourceFile: ts.SourceFile | undefined, getDiagnostics: (sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined) => readonly T[], cancellationToken: ts.CancellationToken | undefined): readonly T[] { + if (sourceFile) { + return getDiagnostics(sourceFile, cancellationToken); + } + return ts.sortAndDeduplicateDiagnostics(ts.flatMap(program.getSourceFiles(), sourceFile => { + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + return getDiagnostics(sourceFile, cancellationToken); + })); + } - case ts.SyntaxKind.VariableStatement: - // Check modifiers - if (nodes === parent.modifiers) { - checkModifiers(parent.modifiers, parent.kind === ts.SyntaxKind.VariableStatement); - return "skip"; - } - break; - case ts.SyntaxKind.PropertyDeclaration: - // Check modifiers of property declaration - if (nodes === (parent as ts.PropertyDeclaration).modifiers) { - for (const modifier of nodes as ts.NodeArray) { - if (modifier.kind !== ts.SyntaxKind.StaticKeyword) { - diagnostics.push(createDiagnosticForNode(modifier, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, ts.tokenToString(modifier.kind))); - } - } - return "skip"; - } - break; - case ts.SyntaxKind.Parameter: - // Check modifiers of parameter declaration - if (nodes === (parent as ts.ParameterDeclaration).modifiers) { - diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Parameter_modifiers_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.ExpressionWithTypeArguments: - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.TaggedTemplateExpression: - // Check type arguments - if (nodes === (parent as ts.NodeWithTypeArguments).typeArguments) { - diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Type_arguments_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - } - } + function getSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { + return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken); + } - function checkModifiers(modifiers: ts.NodeArray, isConstValid: boolean) { - for (const modifier of modifiers) { - switch (modifier.kind) { - case ts.SyntaxKind.ConstKeyword: - if (isConstValid) { - continue; - } - // to report error, - // falls through - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.OverrideKeyword: - case ts.SyntaxKind.InKeyword: - case ts.SyntaxKind.OutKeyword: - diagnostics.push(createDiagnosticForNode(modifier, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, ts.tokenToString(modifier.kind))); - break; - - // These are all legal modifiers. - case ts.SyntaxKind.StaticKeyword: - case ts.SyntaxKind.ExportKeyword: - case ts.SyntaxKind.DefaultKeyword: - } - } - } + function getSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { + return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken); + } - function createDiagnosticForNodeArray(nodes: ts.NodeArray, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { - const start = nodes.pos; - return ts.createFileDiagnostic(sourceFile, start, nodes.end - start, message, arg0, arg1, arg2); - } + function getCachedSemanticDiagnostics(sourceFile?: ts.SourceFile): readonly ts.Diagnostic[] | undefined { + return sourceFile + ? cachedBindAndCheckDiagnosticsForFile.perFile?.get(sourceFile.path) + : cachedBindAndCheckDiagnosticsForFile.allDiagnostics; + } - // Since these are syntactic diagnostics, parent might not have been set - // this means the sourceFile cannot be infered from the node - function createDiagnosticForNode(node: ts.Node, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { - return ts.createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2); - } - }); - } + function getBindAndCheckDiagnostics(sourceFile: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.Diagnostic[] { + return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken); + } - function getDeclarationDiagnosticsWorker(sourceFile: ts.SourceFile | undefined, cancellationToken: ts.CancellationToken | undefined): readonly ts.DiagnosticWithLocation[] { - return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedDeclarationDiagnosticsForFile, getDeclarationDiagnosticsForFileNoCache); + function getProgramDiagnostics(sourceFile: ts.SourceFile): readonly ts.Diagnostic[] { + if (ts.skipTypeChecking(sourceFile, options, program)) { + return ts.emptyArray; } - function getDeclarationDiagnosticsForFileNoCache(sourceFile: ts.SourceFile | undefined, cancellationToken: ts.CancellationToken | undefined): readonly ts.DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - const resolver = getTypeChecker().getEmitResolver(sourceFile, cancellationToken); - // Don't actually write any files since we're just getting diagnostics. - return ts.getDeclarationDiagnostics(getEmitHost(ts.noop), resolver, sourceFile) || ts.emptyArray; - }); + const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName); + if (!sourceFile.commentDirectives?.length) { + return programDiagnosticsInFile; } - function getAndCacheDiagnostics(sourceFile: T, cancellationToken: ts.CancellationToken | undefined, cache: DiagnosticCache, getDiagnostics: (sourceFile: T, cancellationToken: ts.CancellationToken | undefined) => readonly U[]): readonly U[] { + return getDiagnosticsWithPrecedingDirectives(sourceFile, sourceFile.commentDirectives, programDiagnosticsInFile).diagnostics; + } - const cachedResult = sourceFile - ? cache.perFile?.get(sourceFile.path) - : cache.allDiagnostics; + function getDeclarationDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): readonly ts.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 || ts.outFile(options)) { + return getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); + } + else { + return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile, cancellationToken); + } + } - if (cachedResult) { - return cachedResult; - } - const result = getDiagnostics(sourceFile, cancellationToken); - if (sourceFile) { - (cache.perFile || (cache.perFile = new ts.Map())).set(sourceFile.path, result); + function getSyntacticDiagnosticsForFile(sourceFile: ts.SourceFile): readonly ts.DiagnosticWithLocation[] { + // For JavaScript files, we report semantic errors for using TypeScript-only + // constructs from within a JavaScript file as syntactic errors. + if (ts.isSourceFileJS(sourceFile)) { + if (!sourceFile.additionalSyntacticDiagnostics) { + sourceFile.additionalSyntacticDiagnostics = getJSSyntacticDiagnosticsForFile(sourceFile); } - else { - cache.allDiagnostics = result; - } - return result; + return ts.concatenate(sourceFile.additionalSyntacticDiagnostics, sourceFile.parseDiagnostics); } + return sourceFile.parseDiagnostics; + } - function getDeclarationDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { - return sourceFile.isDeclarationFile ? [] : getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); + function runWithCancellationToken(func: () => T): T { + try { + return func(); } + catch (e) { + if (e instanceof ts.OperationCanceledException) { + // We were canceled while performing the operation. Because our type checker + // might be a bad state, we need to throw it away. + typeChecker = undefined!; + } - function getOptionsDiagnostics(): ts.SortedReadonlyArray { - return ts.sortAndDeduplicateDiagnostics(ts.concatenate(programDiagnostics.getGlobalDiagnostics(), getOptionsDiagnosticsOfConfigFile())); + throw e; } + } + + function getSemanticDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { + return ts.concatenate(filterSemanticDiagnostics(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), options), getProgramDiagnostics(sourceFile)); + } - function getOptionsDiagnosticsOfConfigFile() { - if (!options.configFile) + function getBindAndCheckDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { + return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedBindAndCheckDiagnosticsForFile, getBindAndCheckDiagnosticsForFileNoCache); + } + + function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken | undefined): readonly ts.Diagnostic[] { + return runWithCancellationToken(() => { + if (ts.skipTypeChecking(sourceFile, options, program)) { return ts.emptyArray; - let diagnostics = programDiagnostics.getDiagnostics(options.configFile.fileName); - forEachResolvedProjectReference(resolvedRef => { - diagnostics = ts.concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); - }); - return diagnostics; - } + } - function getGlobalDiagnostics(): ts.SortedReadonlyArray { - return rootNames.length ? ts.sortAndDeduplicateDiagnostics(getTypeChecker().getGlobalDiagnostics().slice()) : ts.emptyArray as any as ts.SortedReadonlyArray; - } + const typeChecker = getTypeChecker(); + + ts.Debug.assert(!!sourceFile.bindDiagnostics); + const isJs = sourceFile.scriptKind === ts.ScriptKind.JS || sourceFile.scriptKind === ts.ScriptKind.JSX; + const isCheckJs = isJs && ts.isCheckJsEnabledForFile(sourceFile, options); + const isPlainJs = ts.isPlainJsFile(sourceFile, options.checkJs); + const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false; + + // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External + // - plain JS: .js files with no // ts-check and checkJs: undefined + // - check JS: .js files with either // ts-check or checkJs: true + // - external: files that are added by plugins + const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ts.ScriptKind.TS || sourceFile.scriptKind === ts.ScriptKind.TSX + || sourceFile.scriptKind === ts.ScriptKind.External || isPlainJs || isCheckJs || sourceFile.scriptKind === ts.ScriptKind.Deferred); + let bindDiagnostics: readonly ts.Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : ts.emptyArray; + let checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : ts.emptyArray; + if (isPlainJs) { + bindDiagnostics = ts.filter(bindDiagnostics, d => plainJSErrors.has(d.code)); + checkDiagnostics = ts.filter(checkDiagnostics, d => plainJSErrors.has(d.code)); + } + // skip ts-expect-error errors in plain JS files, and skip JSDoc errors except in checked JS + return getMergedBindAndCheckDiagnostics(sourceFile, includeBindAndCheckDiagnostics && !isPlainJs, bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined); + }); + } - function getConfigFileParsingDiagnostics(): readonly ts.Diagnostic[] { - return configFileParsingDiagnostics || ts.emptyArray; + function getMergedBindAndCheckDiagnostics(sourceFile: ts.SourceFile, includeBindAndCheckDiagnostics: boolean, ...allDiagnostics: (readonly ts.Diagnostic[] | undefined)[]) { + const flatDiagnostics = ts.flatten(allDiagnostics); + if (!includeBindAndCheckDiagnostics || !sourceFile.commentDirectives?.length) { + return flatDiagnostics; } - function processRootFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason) { - processSourceFile(ts.normalizePath(fileName), isDefaultLib, ignoreNoDefaultLib, /*packageId*/ undefined, reason); - } + const { diagnostics, directives } = getDiagnosticsWithPrecedingDirectives(sourceFile, sourceFile.commentDirectives, flatDiagnostics); - function fileReferenceIsEqualTo(a: ts.FileReference, b: ts.FileReference): boolean { - return a.fileName === b.fileName; + for (const errorExpectation of directives.getUnusedExpectations()) { + diagnostics.push(ts.createDiagnosticForRange(sourceFile, errorExpectation.range, ts.Diagnostics.Unused_ts_expect_error_directive)); } - function moduleNameIsEqualTo(a: ts.StringLiteralLike | ts.Identifier, b: ts.StringLiteralLike | ts.Identifier): boolean { - return a.kind === ts.SyntaxKind.Identifier - ? b.kind === ts.SyntaxKind.Identifier && a.escapedText === b.escapedText - : b.kind === ts.SyntaxKind.StringLiteral && a.text === b.text; - } - function createSyntheticImport(text: string, file: ts.SourceFile) { - const externalHelpersModuleReference = ts.factory.createStringLiteral(text); - const importDecl = ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined, externalHelpersModuleReference, /*assertClause*/ undefined); - ts.addEmitFlags(importDecl, ts.EmitFlags.NeverApplyImportHelper); - ts.setParent(externalHelpersModuleReference, importDecl); - ts.setParent(importDecl, file); - // explicitly unset the synthesized flag on these declarations so the checker API will answer questions about them - // (which is required to build the dependency graph for incremental emit) - (externalHelpersModuleReference as ts.Mutable).flags &= ~ts.NodeFlags.Synthesized; - (importDecl as ts.Mutable).flags &= ~ts.NodeFlags.Synthesized; - return externalHelpersModuleReference; - } + return diagnostics; + } - function collectExternalModuleReferences(file: ts.SourceFile): void { - if (file.imports) { - return; - } + /** + * Creates a map of comment directives along with the diagnostics immediately preceded by one of them. + * Comments that match to any of those diagnostics are marked as used. + */ + function getDiagnosticsWithPrecedingDirectives(sourceFile: ts.SourceFile, commentDirectives: ts.CommentDirective[], flatDiagnostics: ts.Diagnostic[]) { + // Diagnostics are only reported if there is no comment directive preceding them + // This will modify the directives map by marking "used" ones with a corresponding diagnostic + const directives = ts.createCommentDirectivesMap(sourceFile, commentDirectives); + const diagnostics = flatDiagnostics.filter(diagnostic => markPrecedingCommentDirectiveLine(diagnostic, directives) === -1); - const isJavaScriptFile = ts.isSourceFileJS(file); - const isExternalModuleFile = ts.isExternalModule(file); + return { diagnostics, directives }; + } - // file.imports may not be undefined if there exists dynamic import - let imports: ts.StringLiteralLike[] | undefined; - let moduleAugmentations: (ts.StringLiteral | ts.Identifier)[] | undefined; - let ambientModules: string[] | undefined; + function getSuggestionDiagnostics(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + return getTypeChecker().getSuggestionDiagnostics(sourceFile, cancellationToken); + }); + } - // If we are importing helpers, we need to add a synthetic reference to resolve the - // helpers library. - if ((options.isolatedModules || isExternalModuleFile) - && !file.isDeclarationFile) { - if (options.importHelpers) { - // synthesize 'import "tslib"' declaration - imports = [createSyntheticImport(ts.externalHelpersModuleNameText, file)]; - } - const jsxImport = ts.getJSXRuntimeImport(ts.getJSXImplicitImportBase(options, file), options); - if (jsxImport) { - // synthesize `import "base/jsx-runtime"` declaration - (imports ||= []).push(createSyntheticImport(jsxImport, file)); - } - } + /** + * @returns The line index marked as preceding the diagnostic, or -1 if none was. + */ + function markPrecedingCommentDirectiveLine(diagnostic: ts.Diagnostic, directives: ts.CommentDirectivesMap) { + const { file, start } = diagnostic; + if (!file) { + return -1; + } - for (const node of file.statements) { - collectModuleReferences(node, /*inAmbientModule*/ false); + // Start out with the line just before the text + const lineStarts = ts.getLineStarts(file); + let line = ts.computeLineAndCharacterOfPosition(lineStarts, start!).line - 1; // TODO: GH#18217 + while (line >= 0) { + // As soon as that line is known to have a comment directive, use that + if (directives.markUsed(line)) { + return line; } - if ((file.flags & ts.NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) { - collectDynamicImportOrRequireCalls(file); + + // Stop searching if the line is not empty and not a comment + const lineText = file.text.slice(lineStarts[line], lineStarts[line + 1]).trim(); + if (lineText !== "" && !/^(\s*)\/\/(.*)$/.test(lineText)) { + return -1; } - file.imports = imports || ts.emptyArray; - file.moduleAugmentations = moduleAugmentations || ts.emptyArray; - file.ambientModuleNames = ambientModules || ts.emptyArray; + line--; + } + + return -1; + } + + function getJSSyntacticDiagnosticsForFile(sourceFile: ts.SourceFile): ts.DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + const diagnostics: ts.DiagnosticWithLocation[] = []; + walk(sourceFile, sourceFile); + ts.forEachChildRecursively(sourceFile, walk, walkArray); - return; + return diagnostics; + + function walk(node: ts.Node, parent: ts.Node) { + // Return directly from the case if the given node doesnt want to visit each child + // Otherwise break to visit each child - function collectModuleReferences(node: ts.Statement, inAmbientModule: boolean): void { - if (ts.isAnyImportOrReExport(node)) { - const moduleNameExpr = ts.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 && ts.isStringLiteral(moduleNameExpr) && moduleNameExpr.text && (!inAmbientModule || !ts.isExternalModuleNameRelative(moduleNameExpr.text))) { - ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here - imports = ts.append(imports, moduleNameExpr); - if (!usesUriStyleNodeCoreModules && currentNodeModulesDepth === 0 && !file.isDeclarationFile) { - usesUriStyleNodeCoreModules = ts.startsWith(moduleNameExpr.text, "node:"); + switch (parent.kind) { + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + if ((parent as ts.ParameterDeclaration | ts.PropertyDeclaration | ts.MethodDeclaration).questionToken === node) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); + return "skip"; + } + // falls through + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.VariableDeclaration: + // type annotation + if ((parent as ts.FunctionLikeDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration).type === node) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files)); + return "skip"; } - } } - else if (ts.isModuleDeclaration(node)) { - if (ts.isAmbientModule(node) && (inAmbientModule || ts.hasSyntacticModifier(node, ts.ModifierFlags.Ambient) || file.isDeclarationFile)) { - (node.name as ts.Mutable).parent = node; - const nameText = ts.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 && !ts.isExternalModuleNameRelative(nameText))) { - (moduleAugmentations || (moduleAugmentations = [])).push(node.name); + + switch (node.kind) { + case ts.SyntaxKind.ImportClause: + if ((node as ts.ImportClause).isTypeOnly) { + diagnostics.push(createDiagnosticForNode(parent, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "import type")); + return "skip"; } - 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 as ts.ModuleDeclaration).body as ts.ModuleBlock; - if (body) { - for (const statement of body.statements) { - collectModuleReferences(statement, /*inAmbientModule*/ true); + break; + case ts.SyntaxKind.ExportDeclaration: + if ((node as ts.ExportDeclaration).isTypeOnly) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "export type")); + return "skip"; + } + break; + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + if ((node as ts.ImportOrExportSpecifier).isTypeOnly) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, ts.isImportSpecifier(node) ? "import...type" : "export...type")); + return "skip"; + } + break; + case ts.SyntaxKind.ImportEqualsDeclaration: + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.import_can_only_be_used_in_TypeScript_files)); + return "skip"; + case ts.SyntaxKind.ExportAssignment: + if ((node as ts.ExportAssignment).isExportEquals) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.export_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; + case ts.SyntaxKind.HeritageClause: + const heritageClause = node as ts.HeritageClause; + if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.implements_clauses_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; + case ts.SyntaxKind.InterfaceDeclaration: + const interfaceKeyword = ts.tokenToString(ts.SyntaxKind.InterfaceKeyword); + ts.Debug.assertIsDefined(interfaceKeyword); + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, interfaceKeyword)); + return "skip"; + case ts.SyntaxKind.ModuleDeclaration: + const moduleKeyword = node.flags & ts.NodeFlags.Namespace ? ts.tokenToString(ts.SyntaxKind.NamespaceKeyword) : ts.tokenToString(ts.SyntaxKind.ModuleKeyword); + ts.Debug.assertIsDefined(moduleKeyword); + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, moduleKeyword)); + return "skip"; + case ts.SyntaxKind.TypeAliasDeclaration: + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files)); + return "skip"; + case ts.SyntaxKind.EnumDeclaration: + const enumKeyword = ts.Debug.checkDefined(ts.tokenToString(ts.SyntaxKind.EnumKeyword)); + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, enumKeyword)); + return "skip"; + case ts.SyntaxKind.NonNullExpression: + diagnostics.push(createDiagnosticForNode(node, ts.Diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files)); + return "skip"; + case ts.SyntaxKind.AsExpression: + diagnostics.push(createDiagnosticForNode((node as ts.AsExpression).type, ts.Diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files)); + return "skip"; + case ts.SyntaxKind.TypeAssertionExpression: + ts.Debug.fail(); // Won't parse these in a JS file anyway, as they are interpreted as JSX. + } + } + + function walkArray(nodes: ts.NodeArray, parent: ts.Node) { + if (parent.decorators === nodes && !options.experimentalDecorators) { + diagnostics.push(createDiagnosticForNode(parent, ts.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 ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ArrowFunction: + // Check type parameters + if (nodes === (parent as ts.DeclarationWithTypeParameterChildren).typeParameters) { + diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Type_parameter_declarations_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + // falls through + + case ts.SyntaxKind.VariableStatement: + // Check modifiers + if (nodes === parent.modifiers) { + checkModifiers(parent.modifiers, parent.kind === ts.SyntaxKind.VariableStatement); + return "skip"; + } + break; + case ts.SyntaxKind.PropertyDeclaration: + // Check modifiers of property declaration + if (nodes === (parent as ts.PropertyDeclaration).modifiers) { + for (const modifier of nodes as ts.NodeArray) { + if (modifier.kind !== ts.SyntaxKind.StaticKeyword) { + diagnostics.push(createDiagnosticForNode(modifier, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, ts.tokenToString(modifier.kind))); } } + return "skip"; } - } + break; + case ts.SyntaxKind.Parameter: + // Check modifiers of parameter declaration + if (nodes === (parent as ts.ParameterDeclaration).modifiers) { + diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Parameter_modifiers_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.ExpressionWithTypeArguments: + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.TaggedTemplateExpression: + // Check type arguments + if (nodes === (parent as ts.NodeWithTypeArguments).typeArguments) { + diagnostics.push(createDiagnosticForNodeArray(nodes, ts.Diagnostics.Type_arguments_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; } } - function collectDynamicImportOrRequireCalls(file: ts.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 (isJavaScriptFile && ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { - ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here - imports = ts.append(imports, node.arguments[0]); - } - // we have to check the argument list has length of at least 1. We will still have to process these even though we have parsing error. - else if (ts.isImportCall(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])) { - ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here - imports = ts.append(imports, node.arguments[0]); - } - else if (ts.isLiteralImportTypeNode(node)) { - ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here - imports = ts.append(imports, node.argument.literal); + function checkModifiers(modifiers: ts.NodeArray, isConstValid: boolean) { + for (const modifier of modifiers) { + switch (modifier.kind) { + case ts.SyntaxKind.ConstKeyword: + if (isConstValid) { + continue; + } + // to report error, + // falls through + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.OverrideKeyword: + case ts.SyntaxKind.InKeyword: + case ts.SyntaxKind.OutKeyword: + diagnostics.push(createDiagnosticForNode(modifier, ts.Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, ts.tokenToString(modifier.kind))); + break; + + // These are all legal modifiers. + case ts.SyntaxKind.StaticKeyword: + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.DefaultKeyword: } } } - /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ - function getNodeAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { - let current: ts.Node = sourceFile; - const getContainingChild = (child: ts.Node) => { - if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === ts.SyntaxKind.EndOfFileToken)))) { - return child; - } - }; - while (true) { - const child = isJavaScriptFile && ts.hasJSDocNodes(current) && ts.forEach(current.jsDoc, getContainingChild) || ts.forEachChild(current, getContainingChild); - if (!child) { - return current; - } - current = child; - } + function createDiagnosticForNodeArray(nodes: ts.NodeArray, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { + const start = nodes.pos; + return ts.createFileDiagnostic(sourceFile, start, nodes.end - start, message, arg0, arg1, arg2); } - } - function getLibFileFromReference(ref: ts.FileReference) { - const libName = ts.toFileNameLowerCase(ref.fileName); - const libFileName = ts.libMap.get(libName); - if (libFileName) { - return getSourceFile(pathForLibFile(libFileName)); + // Since these are syntactic diagnostics, parent might not have been set + // this means the sourceFile cannot be infered from the node + function createDiagnosticForNode(node: ts.Node, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): ts.DiagnosticWithLocation { + return ts.createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2); } - } + }); + } - /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ - function getSourceFileFromReference(referencingFile: ts.SourceFile | ts.UnparsedSource, ref: ts.FileReference): ts.SourceFile | undefined { - return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), getSourceFile); - } + function getDeclarationDiagnosticsWorker(sourceFile: ts.SourceFile | undefined, cancellationToken: ts.CancellationToken | undefined): readonly ts.DiagnosticWithLocation[] { + return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedDeclarationDiagnosticsForFile, getDeclarationDiagnosticsForFileNoCache); + } - function getSourceFileFromReferenceWorker(fileName: string, getSourceFile: (fileName: string) => ts.SourceFile | undefined, fail?: (diagnostic: ts.DiagnosticMessage, ...argument: string[]) => void, reason?: ts.FileIncludeReason): ts.SourceFile | undefined { - if (ts.hasExtension(fileName)) { - const canonicalFileName = host.getCanonicalFileName(fileName); - if (!options.allowNonTsExtensions && !ts.forEach(ts.flatten(supportedExtensionsWithJsonIfResolveJsonModule), extension => ts.fileExtensionIs(canonicalFileName, extension))) { - if (fail) { - if (ts.hasJSFileExtension(canonicalFileName)) { - fail(ts.Diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, fileName); - } - else { - fail(ts.Diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, fileName, "'" + ts.flatten(supportedExtensions).join("', '") + "'"); - } - } - return undefined; - } + function getDeclarationDiagnosticsForFileNoCache(sourceFile: ts.SourceFile | undefined, cancellationToken: ts.CancellationToken | undefined): readonly ts.DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + const resolver = getTypeChecker().getEmitResolver(sourceFile, cancellationToken); + // Don't actually write any files since we're just getting diagnostics. + return ts.getDeclarationDiagnostics(getEmitHost(ts.noop), resolver, sourceFile) || ts.emptyArray; + }); + } - const sourceFile = getSourceFile(fileName); - if (fail) { - if (!sourceFile) { - const redirect = getProjectReferenceRedirect(fileName); - if (redirect) { - fail(ts.Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, fileName); - } - else { - fail(ts.Diagnostics.File_0_not_found, fileName); - } - } - else if (isReferencedFile(reason) && canonicalFileName === host.getCanonicalFileName(getSourceFileByPath(reason.file)!.fileName)) { - fail(ts.Diagnostics.A_file_cannot_have_a_reference_to_itself); - } - } - return sourceFile; - } - else { - const sourceFileNoExtension = options.allowNonTsExtensions && getSourceFile(fileName); - if (sourceFileNoExtension) - return sourceFileNoExtension; + function getAndCacheDiagnostics(sourceFile: T, cancellationToken: ts.CancellationToken | undefined, cache: DiagnosticCache, getDiagnostics: (sourceFile: T, cancellationToken: ts.CancellationToken | undefined) => readonly U[]): readonly U[] { - if (fail && options.allowNonTsExtensions) { - fail(ts.Diagnostics.File_0_not_found, fileName); - return undefined; - } + const cachedResult = sourceFile + ? cache.perFile?.get(sourceFile.path) + : cache.allDiagnostics; - // Only try adding extensions from the first supported group (which should be .ts/.tsx/.d.ts) - const sourceFileWithAddedExtension = ts.forEach(supportedExtensions[0], extension => getSourceFile(fileName + extension)); - if (fail && !sourceFileWithAddedExtension) - fail(ts.Diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, fileName, "'" + ts.flatten(supportedExtensions).join("', '") + "'"); - return sourceFileWithAddedExtension; - } + if (cachedResult) { + return cachedResult; } - - /** This has side effects through `findSourceFile`. */ - function processSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, packageId: ts.PackageId | undefined, reason: ts.FileIncludeReason): void { - getSourceFileFromReferenceWorker(fileName, fileName => findSourceFile(fileName, isDefaultLib, ignoreNoDefaultLib, reason, packageId), // TODO: GH#18217 - (diagnostic, ...args) => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, diagnostic, args), reason); + const result = getDiagnostics(sourceFile, cancellationToken); + if (sourceFile) { + (cache.perFile || (cache.perFile = new ts.Map())).set(sourceFile.path, result); + } + else { + cache.allDiagnostics = result; } + return result; + } + + function getDeclarationDiagnosticsForFile(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): readonly ts.DiagnosticWithLocation[] { + return sourceFile.isDeclarationFile ? [] : getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); + } + + function getOptionsDiagnostics(): ts.SortedReadonlyArray { + return ts.sortAndDeduplicateDiagnostics(ts.concatenate(programDiagnostics.getGlobalDiagnostics(), getOptionsDiagnosticsOfConfigFile())); + } + + function getOptionsDiagnosticsOfConfigFile() { + if (!options.configFile) + return ts.emptyArray; + let diagnostics = programDiagnostics.getDiagnostics(options.configFile.fileName); + forEachResolvedProjectReference(resolvedRef => { + diagnostics = ts.concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); + }); + return diagnostics; + } + + function getGlobalDiagnostics(): ts.SortedReadonlyArray { + return rootNames.length ? ts.sortAndDeduplicateDiagnostics(getTypeChecker().getGlobalDiagnostics().slice()) : ts.emptyArray as any as ts.SortedReadonlyArray; + } + + function getConfigFileParsingDiagnostics(): readonly ts.Diagnostic[] { + return configFileParsingDiagnostics || ts.emptyArray; + } + + function processRootFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason) { + processSourceFile(ts.normalizePath(fileName), isDefaultLib, ignoreNoDefaultLib, /*packageId*/ undefined, reason); + } + + function fileReferenceIsEqualTo(a: ts.FileReference, b: ts.FileReference): boolean { + return a.fileName === b.fileName; + } + + function moduleNameIsEqualTo(a: ts.StringLiteralLike | ts.Identifier, b: ts.StringLiteralLike | ts.Identifier): boolean { + return a.kind === ts.SyntaxKind.Identifier + ? b.kind === ts.SyntaxKind.Identifier && a.escapedText === b.escapedText + : b.kind === ts.SyntaxKind.StringLiteral && a.text === b.text; + } + function createSyntheticImport(text: string, file: ts.SourceFile) { + const externalHelpersModuleReference = ts.factory.createStringLiteral(text); + const importDecl = ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined, externalHelpersModuleReference, /*assertClause*/ undefined); + ts.addEmitFlags(importDecl, ts.EmitFlags.NeverApplyImportHelper); + ts.setParent(externalHelpersModuleReference, importDecl); + ts.setParent(importDecl, file); + // explicitly unset the synthesized flag on these declarations so the checker API will answer questions about them + // (which is required to build the dependency graph for incremental emit) + (externalHelpersModuleReference as ts.Mutable).flags &= ~ts.NodeFlags.Synthesized; + (importDecl as ts.Mutable).flags &= ~ts.NodeFlags.Synthesized; + return externalHelpersModuleReference; + } - function processProjectReferenceFile(fileName: string, reason: ts.ProjectReferenceFile) { - return processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined, reason); + function collectExternalModuleReferences(file: ts.SourceFile): void { + if (file.imports) { + return; } - function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFile: ts.SourceFile, reason: ts.FileIncludeReason): void { - const hasExistingReasonToReportErrorOn = !isReferencedFile(reason) && ts.some(fileReasons.get(existingFile.path), isReferencedFile); - if (hasExistingReasonToReportErrorOn) { - addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, [existingFile.fileName, fileName]); + const isJavaScriptFile = ts.isSourceFileJS(file); + const isExternalModuleFile = ts.isExternalModule(file); + + // file.imports may not be undefined if there exists dynamic import + let imports: ts.StringLiteralLike[] | undefined; + let moduleAugmentations: (ts.StringLiteral | ts.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.isolatedModules || isExternalModuleFile) + && !file.isDeclarationFile) { + if (options.importHelpers) { + // synthesize 'import "tslib"' declaration + imports = [createSyntheticImport(ts.externalHelpersModuleNameText, file)]; } - else { - addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, [fileName, existingFile.fileName]); + const jsxImport = ts.getJSXRuntimeImport(ts.getJSXImplicitImportBase(options, file), options); + if (jsxImport) { + // synthesize `import "base/jsx-runtime"` declaration + (imports ||= []).push(createSyntheticImport(jsxImport, file)); } } - function createRedirectSourceFile(redirectTarget: ts.SourceFile, unredirected: ts.SourceFile, fileName: string, path: ts.Path, resolvedPath: ts.Path, originalFileName: string): ts.SourceFile { - const redirect: ts.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: ts.SourceFile) { return this.redirectInfo!.redirectTarget.id; }, - set(this: ts.SourceFile, value: ts.SourceFile["id"]) { this.redirectInfo!.redirectTarget.id = value; }, - }, - symbol: { - get(this: ts.SourceFile) { return this.redirectInfo!.redirectTarget.symbol; }, - set(this: ts.SourceFile, value: ts.SourceFile["symbol"]) { this.redirectInfo!.redirectTarget.symbol = value; }, - }, - }); - return redirect; + for (const node of file.statements) { + collectModuleReferences(node, /*inAmbientModule*/ false); } - - // Get source file from normalized fileName - function findSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason, packageId: ts.PackageId | undefined): ts.SourceFile | undefined { - ts.tracing?.push(ts.tracing.Phase.Program, "findSourceFile", { - fileName, - isDefaultLib: isDefaultLib || undefined, - fileIncludeKind: (ts.FileIncludeKind as any)[reason.kind], - }); - const result = findSourceFileWorker(fileName, isDefaultLib, ignoreNoDefaultLib, reason, packageId); - ts.tracing?.pop(); - return result; + if ((file.flags & ts.NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) { + collectDynamicImportOrRequireCalls(file); } - function getCreateSourceFileOptions(fileName: string, moduleResolutionCache: ts.ModuleResolutionCache | undefined, host: ts.CompilerHost, options: ts.CompilerOptions) { - // It's a _little odd_ that we can't set `impliedNodeFormat` until the program step - but it's the first and only time we have a resolution cache - // and a freshly made source file node on hand at the same time, and we need both to set the field. Persisting the resolution cache all the way - // to the check and emit steps would be bad - so we much prefer detecting and storing the format information on the source file node upfront. - const impliedNodeFormat = getImpliedNodeFormatForFile(toPath(fileName), moduleResolutionCache?.getPackageJsonInfoCache(), host, options); - return { - languageVersion: ts.getEmitScriptTarget(options), - impliedNodeFormat, - setExternalModuleIndicator: ts.getSetExternalModuleIndicator(options) - }; - } + file.imports = imports || ts.emptyArray; + file.moduleAugmentations = moduleAugmentations || ts.emptyArray; + file.ambientModuleNames = ambientModules || ts.emptyArray; - function findSourceFileWorker(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason, packageId: ts.PackageId | undefined): ts.SourceFile | undefined { - const path = toPath(fileName); - if (useSourceOfProjectReferenceRedirect) { - let source = getSourceOfProjectReferenceRedirect(path); - // 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 && - ts.isDeclarationFileName(fileName) && - ts.stringContains(fileName, ts.nodeModulesPathPart)) { - const realPath = toPath(host.realpath(fileName)); - if (realPath !== path) - source = getSourceOfProjectReferenceRedirect(realPath); - } - if (source) { - const file = ts.isString(source) ? - findSourceFile(source, isDefaultLib, ignoreNoDefaultLib, reason, packageId) : - undefined; - if (file) - addFileToFilesByName(file, path, /*redirectedPath*/ undefined); - return file; + return; + + function collectModuleReferences(node: ts.Statement, inAmbientModule: boolean): void { + if (ts.isAnyImportOrReExport(node)) { + const moduleNameExpr = ts.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 && ts.isStringLiteral(moduleNameExpr) && moduleNameExpr.text && (!inAmbientModule || !ts.isExternalModuleNameRelative(moduleNameExpr.text))) { + ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here + imports = ts.append(imports, moduleNameExpr); + if (!usesUriStyleNodeCoreModules && currentNodeModulesDepth === 0 && !file.isDeclarationFile) { + usesUriStyleNodeCoreModules = ts.startsWith(moduleNameExpr.text, "node:"); + } } } - const originalFileName = fileName; - if (filesByName.has(path)) { - const file = filesByName.get(path); - addFileIncludeReason(file || undefined, reason); - // 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; + else if (ts.isModuleDeclaration(node)) { + if (ts.isAmbientModule(node) && (inAmbientModule || ts.hasSyntacticModifier(node, ts.ModifierFlags.Ambient) || file.isDeclarationFile)) { + (node.name as ts.Mutable).parent = node; + const nameText = ts.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 && !ts.isExternalModuleNameRelative(nameText))) { + (moduleAugmentations || (moduleAugmentations = [])).push(node.name); } - // Check if it differs only in drive letters its ok to ignore that error: - const checkedAbsolutePath = ts.getNormalizedAbsolutePathWithoutRoot(checkedName, currentDirectory); - const inputAbsolutePath = ts.getNormalizedAbsolutePathWithoutRoot(fileName, currentDirectory); - if (checkedAbsolutePath !== inputAbsolutePath) { - reportFileNamesDifferOnlyInCasingError(fileName, file, reason); + 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 as ts.ModuleDeclaration).body as ts.ModuleBlock; + if (body) { + for (const statement of body.statements) { + collectModuleReferences(statement, /*inAmbientModule*/ true); + } + } } } + } + } - // 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); - } - if (!options.noLib) { - processLibReferenceDirectives(file); - } - - modulesWithElidedImports.set(file.path, false); - processImportedModules(file); + function collectDynamicImportOrRequireCalls(file: ts.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 (isJavaScriptFile && ts.isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { + ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here + imports = ts.append(imports, node.arguments[0]); } - // 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); - } + // we have to check the argument list has length of at least 1. We will still have to process these even though we have parsing error. + else if (ts.isImportCall(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])) { + ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here + imports = ts.append(imports, node.arguments[0]); } - - return file || undefined; - } - - let redirectedPath: ts.Path | undefined; - if (isReferencedFile(reason) && !useSourceOfProjectReferenceRedirect) { - const redirectProject = getProjectReferenceRedirectProject(fileName); - if (redirectProject) { - if (ts.outFile(redirectProject.commandLine.options)) { - // 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); + else if (ts.isLiteralImportTypeNode(node)) { + ts.setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here + imports = ts.append(imports, node.argument.literal); } } + } - // We haven't looked for this file, do so now and cache result - const file = host.getSourceFile(fileName, getCreateSourceFileOptions(fileName, moduleResolutionCache, host, options), hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, ts.Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]), shouldCreateNewSourceFile); - - if (packageId) { - const packageIdKey = ts.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); - addFileIncludeReason(dupFile, reason); - sourceFileToPackageName.set(path, ts.packageIdToPackageName(packageId)); - processingOtherFiles!.push(dupFile); - return dupFile; + /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ + function getNodeAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { + let current: ts.Node = sourceFile; + const getContainingChild = (child: ts.Node) => { + if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === ts.SyntaxKind.EndOfFileToken)))) { + return child; } - else if (file) { - // This is the first source file to have this packageId. - packageIdToSourceFile.set(packageIdKey, file); - sourceFileToPackageName.set(path, ts.packageIdToPackageName(packageId)); + }; + while (true) { + const child = isJavaScriptFile && ts.hasJSDocNodes(current) && ts.forEach(current.jsDoc, getContainingChild) || ts.forEachChild(current, getContainingChild); + if (!child) { + return current; } + current = child; } - 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; - addFileIncludeReason(file, reason); - - if (host.useCaseSensitiveFileNames()) { - const pathLowerCase = ts.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, reason); + } + } + + function getLibFileFromReference(ref: ts.FileReference) { + const libName = ts.toFileNameLowerCase(ref.fileName); + const libFileName = ts.libMap.get(libName); + if (libFileName) { + return getSourceFile(pathForLibFile(libFileName)); + } + } + + /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ + function getSourceFileFromReference(referencingFile: ts.SourceFile | ts.UnparsedSource, ref: ts.FileReference): ts.SourceFile | undefined { + return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), getSourceFile); + } + + function getSourceFileFromReferenceWorker(fileName: string, getSourceFile: (fileName: string) => ts.SourceFile | undefined, fail?: (diagnostic: ts.DiagnosticMessage, ...argument: string[]) => void, reason?: ts.FileIncludeReason): ts.SourceFile | undefined { + if (ts.hasExtension(fileName)) { + const canonicalFileName = host.getCanonicalFileName(fileName); + if (!options.allowNonTsExtensions && !ts.forEach(ts.flatten(supportedExtensionsWithJsonIfResolveJsonModule), extension => ts.fileExtensionIs(canonicalFileName, extension))) { + if (fail) { + if (ts.hasJSFileExtension(canonicalFileName)) { + fail(ts.Diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, fileName); } else { - filesByNameIgnoreCase!.set(pathLowerCase, file); + fail(ts.Diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, fileName, "'" + ts.flatten(supportedExtensions).join("', '") + "'"); } } + return undefined; + } - 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); + const sourceFile = getSourceFile(fileName); + if (fail) { + if (!sourceFile) { + const redirect = getProjectReferenceRedirect(fileName); + if (redirect) { + fail(ts.Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, fileName); + } + else { + fail(ts.Diagnostics.File_0_not_found, fileName); + } } - else { - processingOtherFiles!.push(file); + else if (isReferencedFile(reason) && canonicalFileName === host.getCanonicalFileName(getSourceFileByPath(reason.file)!.fileName)) { + fail(ts.Diagnostics.A_file_cannot_have_a_reference_to_itself); } } - return file; - } - - function addFileIncludeReason(file: ts.SourceFile | undefined, reason: ts.FileIncludeReason) { - if (file) - fileReasons.add(file.path, reason); - } - - function addFileToFilesByName(file: ts.SourceFile | undefined, path: ts.Path, redirectedPath: ts.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); + return sourceFile; } + else { + const sourceFileNoExtension = options.allowNonTsExtensions && getSourceFile(fileName); + if (sourceFileNoExtension) + return sourceFileNoExtension; - function getProjectReferenceRedirectProject(fileName: string) { - // Ignore dts or any json files - if (!resolvedProjectReferences || !resolvedProjectReferences.length || ts.isDeclarationFileName(fileName) || ts.fileExtensionIs(fileName, ts.Extension.Json)) { + if (fail && options.allowNonTsExtensions) { + fail(ts.Diagnostics.File_0_not_found, fileName); 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); + // Only try adding extensions from the first supported group (which should be .ts/.tsx/.d.ts) + const sourceFileWithAddedExtension = ts.forEach(supportedExtensions[0], extension => getSourceFile(fileName + extension)); + if (fail && !sourceFileWithAddedExtension) + fail(ts.Diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, fileName, "'" + ts.flatten(supportedExtensions).join("', '") + "'"); + return sourceFileWithAddedExtension; } + } + /** This has side effects through `findSourceFile`. */ + function processSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, packageId: ts.PackageId | undefined, reason: ts.FileIncludeReason): void { + getSourceFileFromReferenceWorker(fileName, fileName => findSourceFile(fileName, isDefaultLib, ignoreNoDefaultLib, reason, packageId), // TODO: GH#18217 + (diagnostic, ...args) => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, diagnostic, args), reason); + } - function getProjectReferenceOutputName(referencedProject: ts.ResolvedProjectReference, fileName: string) { - const out = ts.outFile(referencedProject.commandLine.options); - return out ? - ts.changeExtension(out, ts.Extension.Dts) : - ts.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 = new ts.Map(); - forEachResolvedProjectReference(referencedProject => { - // not input file from the referenced project, ignore - if (toPath(options.configFilePath!) !== referencedProject.sourceFile.path) { - referencedProject.commandLine.fileNames.forEach(f => mapFromFileToProjectReferenceRedirects!.set(toPath(f), referencedProject.sourceFile.path)); - } - }); - } + function processProjectReferenceFile(fileName: string, reason: ts.ProjectReferenceFile) { + return processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined, reason); + } - const referencedProjectPath = mapFromFileToProjectReferenceRedirects.get(toPath(fileName)); - return referencedProjectPath && getResolvedProjectReferenceByPath(referencedProjectPath); + function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFile: ts.SourceFile, reason: ts.FileIncludeReason): void { + const hasExistingReasonToReportErrorOn = !isReferencedFile(reason) && ts.some(fileReasons.get(existingFile.path), isReferencedFile); + if (hasExistingReasonToReportErrorOn) { + addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, [existingFile.fileName, fileName]); } - - function forEachResolvedProjectReference(cb: (resolvedProjectReference: ts.ResolvedProjectReference) => T | undefined): T | undefined { - return ts.forEachResolvedProjectReference(resolvedProjectReferences, cb); + else { + addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, [fileName, existingFile.fileName]); } + } - function getSourceOfProjectReferenceRedirect(path: ts.Path) { - if (!ts.isDeclarationFileName(path)) - return undefined; - if (mapFromToProjectReferenceRedirectSource === undefined) { - mapFromToProjectReferenceRedirectSource = new ts.Map(); - forEachResolvedProjectReference(resolvedRef => { - const out = ts.outFile(resolvedRef.commandLine.options); - if (out) { - // Dont know which source file it means so return true? - const outputDts = ts.changeExtension(out, ts.Extension.Dts); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); - } - else { - const getCommonSourceDirectory = ts.memoize(() => ts.getCommonSourceDirectoryOfConfig(resolvedRef.commandLine, !host.useCaseSensitiveFileNames())); - ts.forEach(resolvedRef.commandLine.fileNames, fileName => { - if (!ts.isDeclarationFileName(fileName) && !ts.fileExtensionIs(fileName, ts.Extension.Json)) { - const outputDts = ts.getOutputDeclarationFileName(fileName, resolvedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); - } - }); - } - }); - } - return mapFromToProjectReferenceRedirectSource.get(path); - } + function createRedirectSourceFile(redirectTarget: ts.SourceFile, unredirected: ts.SourceFile, fileName: string, path: ts.Path, resolvedPath: ts.Path, originalFileName: string): ts.SourceFile { + const redirect: ts.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: ts.SourceFile) { return this.redirectInfo!.redirectTarget.id; }, + set(this: ts.SourceFile, value: ts.SourceFile["id"]) { this.redirectInfo!.redirectTarget.id = value; }, + }, + symbol: { + get(this: ts.SourceFile) { return this.redirectInfo!.redirectTarget.symbol; }, + set(this: ts.SourceFile, value: ts.SourceFile["symbol"]) { this.redirectInfo!.redirectTarget.symbol = value; }, + }, + }); + return redirect; + } + + // Get source file from normalized fileName + function findSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason, packageId: ts.PackageId | undefined): ts.SourceFile | undefined { + ts.tracing?.push(ts.tracing.Phase.Program, "findSourceFile", { + fileName, + isDefaultLib: isDefaultLib || undefined, + fileIncludeKind: (ts.FileIncludeKind as any)[reason.kind], + }); + const result = findSourceFileWorker(fileName, isDefaultLib, ignoreNoDefaultLib, reason, packageId); + ts.tracing?.pop(); + return result; + } + + function getCreateSourceFileOptions(fileName: string, moduleResolutionCache: ts.ModuleResolutionCache | undefined, host: ts.CompilerHost, options: ts.CompilerOptions) { + // It's a _little odd_ that we can't set `impliedNodeFormat` until the program step - but it's the first and only time we have a resolution cache + // and a freshly made source file node on hand at the same time, and we need both to set the field. Persisting the resolution cache all the way + // to the check and emit steps would be bad - so we much prefer detecting and storing the format information on the source file node upfront. + const impliedNodeFormat = getImpliedNodeFormatForFile(toPath(fileName), moduleResolutionCache?.getPackageJsonInfoCache(), host, options); + return { + languageVersion: ts.getEmitScriptTarget(options), + impliedNodeFormat, + setExternalModuleIndicator: ts.getSetExternalModuleIndicator(options) + }; + } - function isSourceOfProjectReferenceRedirect(fileName: string) { - return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); - } + function findSourceFileWorker(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: ts.FileIncludeReason, packageId: ts.PackageId | undefined): ts.SourceFile | undefined { + const path = toPath(fileName); + if (useSourceOfProjectReferenceRedirect) { + let source = getSourceOfProjectReferenceRedirect(path); + // 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 && + ts.isDeclarationFileName(fileName) && + ts.stringContains(fileName, ts.nodeModulesPathPart)) { + const realPath = toPath(host.realpath(fileName)); + if (realPath !== path) + source = getSourceOfProjectReferenceRedirect(realPath); + } + if (source) { + const file = ts.isString(source) ? + findSourceFile(source, isDefaultLib, ignoreNoDefaultLib, reason, packageId) : + undefined; + if (file) + addFileToFilesByName(file, path, /*redirectedPath*/ undefined); + return file; + } + } + const originalFileName = fileName; + if (filesByName.has(path)) { + const file = filesByName.get(path); + addFileIncludeReason(file || undefined, reason); + // 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 = ts.getNormalizedAbsolutePathWithoutRoot(checkedName, currentDirectory); + const inputAbsolutePath = ts.getNormalizedAbsolutePathWithoutRoot(fileName, currentDirectory); + if (checkedAbsolutePath !== inputAbsolutePath) { + reportFileNamesDifferOnlyInCasingError(fileName, file, reason); + } + } + + // 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); + } + if (!options.noLib) { + processLibReferenceDirectives(file); + } - function getResolvedProjectReferenceByPath(projectReferencePath: ts.Path): ts.ResolvedProjectReference | undefined { - if (!projectReferenceRedirects) { - return undefined; + 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 projectReferenceRedirects.get(projectReferencePath) || undefined; - } - - function processReferencedFiles(file: ts.SourceFile, isDefaultLib: boolean) { - ts.forEach(file.referencedFiles, (ref, index) => { - processSourceFile(resolveTripleslashReference(ref.fileName, file.fileName), isDefaultLib, - /*ignoreNoDefaultLib*/ false, - /*packageId*/ undefined, { kind: ts.FileIncludeKind.ReferenceFile, file: file.path, index, }); - }); + return file || undefined; } - function processTypeReferenceDirectives(file: ts.SourceFile) { - const typeDirectives = file.typeReferenceDirectives; - if (!typeDirectives) { - return; - } - - const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeDirectives, file); - for (let index = 0; index < typeDirectives.length; index++) { - const ref = file.typeReferenceDirectives[index]; - const resolvedTypeReferenceDirective = resolutions[index]; - // store resolved type directive on the file - const fileName = ts.toFileNameLowerCase(ref.fileName); - ts.setResolvedTypeReferenceDirective(file, fileName, resolvedTypeReferenceDirective); - const mode = ref.resolutionMode || file.impliedNodeFormat; - if (mode && ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeNext) { - programDiagnostics.add(ts.createDiagnosticForRange(file, ref, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext)); + let redirectedPath: ts.Path | undefined; + if (isReferencedFile(reason) && !useSourceOfProjectReferenceRedirect) { + const redirectProject = getProjectReferenceRedirectProject(fileName); + if (redirectProject) { + if (ts.outFile(redirectProject.commandLine.options)) { + // Shouldnt create many to 1 mapping file in --out scenario + return undefined; } - processTypeReferenceDirective(fileName, mode, resolvedTypeReferenceDirective, { kind: ts.FileIncludeKind.TypeReferenceDirective, file: file.path, index, }); + 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); } } - function processTypeReferenceDirective(typeReferenceDirective: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined, resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined, reason: ts.FileIncludeReason): void { - ts.tracing?.push(ts.tracing.Phase.Program, "processTypeReferenceDirective", { directive: typeReferenceDirective, hasResolved: !!resolveModuleNamesReusingOldState, refKind: reason.kind, refPath: isReferencedFile(reason) ? reason.file : undefined }); - processTypeReferenceDirectiveWorker(typeReferenceDirective, mode, resolvedTypeReferenceDirective, reason); - ts.tracing?.pop(); - } - function processTypeReferenceDirectiveWorker(typeReferenceDirective: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined, resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined, reason: ts.FileIncludeReason): void { + // We haven't looked for this file, do so now and cache result + const file = host.getSourceFile(fileName, getCreateSourceFileOptions(fileName, moduleResolutionCache, host, options), hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, ts.Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]), shouldCreateNewSourceFile); - // If we already found this library as a primary reference - nothing to do - const previousResolution = resolvedTypeReferenceDirectives.get(typeReferenceDirective, mode); - if (previousResolution && previousResolution.primary) { - return; + if (packageId) { + const packageIdKey = ts.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); + addFileIncludeReason(dupFile, reason); + sourceFileToPackageName.set(path, ts.packageIdToPackageName(packageId)); + processingOtherFiles!.push(dupFile); + return dupFile; } - let saveResolution = true; - if (resolvedTypeReferenceDirective) { - if (resolvedTypeReferenceDirective.isExternalLibraryImport) - currentNodeModulesDepth++; + else if (file) { + // This is the first source file to have this packageId. + packageIdToSourceFile.set(packageIdKey, file); + sourceFileToPackageName.set(path, ts.packageIdToPackageName(packageId)); + } + } + addFileToFilesByName(file, path, redirectedPath); - if (resolvedTypeReferenceDirective.primary) { - // resolved from the primary path - processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, reason); // TODO: GH#18217 + 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; + addFileIncludeReason(file, reason); + + if (host.useCaseSensitiveFileNames()) { + const pathLowerCase = ts.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, reason); } 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) { - addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.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, reason); - } + filesByNameIgnoreCase!.set(pathLowerCase, file); } + } - if (resolvedTypeReferenceDirective.isExternalLibraryImport) - currentNodeModulesDepth--; + skipDefaultLib = skipDefaultLib || (file.hasNoDefaultLib && !ignoreNoDefaultLib); + + if (!options.noResolve) { + processReferencedFiles(file, isDefaultLib); + processTypeReferenceDirectives(file); } - else { - addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, ts.Diagnostics.Cannot_find_type_definition_file_for_0, [typeReferenceDirective]); + if (!options.noLib) { + processLibReferenceDirectives(file); } - if (saveResolution) { - resolvedTypeReferenceDirectives.set(typeReferenceDirective, mode, resolvedTypeReferenceDirective); - } - } - function pathForLibFile(libFileName: string): string { - // Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and - // lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable - // lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown - const components = libFileName.split("."); - let path = components[1]; - let i = 2; - while (components[i] && components[i] !== "d") { - path += (i === 2 ? "/" : "-") + components[i]; - i++; + // always process imported modules to record module name resolutions + processImportedModules(file); + + if (isDefaultLib) { + processingDefaultLibFiles!.push(file); } - const resolveFrom = ts.combinePaths(currentDirectory, `__lib_node_modules_lookup_${libFileName}__.ts`); - const localOverrideModuleResult = ts.resolveModuleName("@typescript/lib-" + path, resolveFrom, { moduleResolution: ts.ModuleResolutionKind.NodeJs }, host, moduleResolutionCache); - if (localOverrideModuleResult?.resolvedModule) { - return localOverrideModuleResult.resolvedModule.resolvedFileName; + else { + processingOtherFiles!.push(file); } - return ts.combinePaths(defaultLibraryPath, libFileName); } + return file; + } - function processLibReferenceDirectives(file: ts.SourceFile) { - ts.forEach(file.libReferenceDirectives, (libReference, index) => { - const libName = ts.toFileNameLowerCase(libReference.fileName); - const libFileName = ts.libMap.get(libName); - if (libFileName) { - // we ignore any 'no-default-lib' reference set on this file. - processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true, { kind: ts.FileIncludeKind.LibReferenceDirective, file: file.path, index, }); - } - else { - const unqualifiedLibName = ts.removeSuffix(ts.removePrefix(libName, "lib."), ".d.ts"); - const suggestion = ts.getSpellingSuggestion(unqualifiedLibName, ts.libs, ts.identity); - const diagnostic = suggestion ? ts.Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : ts.Diagnostics.Cannot_find_lib_definition_for_0; - (fileProcessingDiagnostics ||= []).push({ - kind: ts.FilePreprocessingDiagnosticsKind.FilePreprocessingReferencedDiagnostic, - reason: { kind: ts.FileIncludeKind.LibReferenceDirective, file: file.path, index, }, - diagnostic, - args: [libName, suggestion] - }); - } - }); - } + function addFileIncludeReason(file: ts.SourceFile | undefined, reason: ts.FileIncludeReason) { + if (file) + fileReasons.add(file.path, reason); + } - function getCanonicalFileName(fileName: string): string { - return host.getCanonicalFileName(fileName); + function addFileToFilesByName(file: ts.SourceFile | undefined, path: ts.Path, redirectedPath: ts.Path | undefined) { + if (redirectedPath) { + filesByName.set(redirectedPath, file); + filesByName.set(path, file || false); } + else { + filesByName.set(path, file); + } + } - function processImportedModules(file: ts.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, file); - ts.Debug.assert(resolutions.length === moduleNames.length); - const optionsForFile = (useSourceOfProjectReferenceRedirect ? getRedirectReferenceForResolution(file)?.commandLine.options : undefined) || options; - for (let index = 0; index < moduleNames.length; index++) { - const resolution = resolutions[index]; - ts.setResolvedModule(file, moduleNames[index], resolution, getModeForResolutionAtIndex(file, index)); - - if (!resolution) { - continue; - } + function getProjectReferenceRedirect(fileName: string): string | undefined { + const referencedProject = getProjectReferenceRedirectProject(fileName); + return referencedProject && getProjectReferenceOutputName(referencedProject, fileName); + } - const isFromNodeModulesSearch = resolution.isExternalLibraryImport; - const isJsFile = !ts.resolutionExtensionIsTSOrJson(resolution.extension); - const isJsFileFromNodeModules = isFromNodeModulesSearch && isJsFile; - const resolvedFileName = resolution.resolvedFileName; + function getProjectReferenceRedirectProject(fileName: string) { + // Ignore dts or any json files + if (!resolvedProjectReferences || !resolvedProjectReferences.length || ts.isDeclarationFileName(fileName) || ts.fileExtensionIs(fileName, ts.Extension.Json)) { + return undefined; + } - 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); + } - // 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(optionsForFile, resolution) - && !optionsForFile.noResolve - && index < file.imports.length - && !elideImport - && !(isJsFile && !ts.getAllowJSCompilerOption(optionsForFile)) - && (ts.isInJSFile(file.imports[index]) || !(file.imports[index].flags & ts.NodeFlags.JSDoc)); - - if (elideImport) { - modulesWithElidedImports.set(file.path, true); - } - else if (shouldAddFile) { - findSourceFile(resolvedFileName, - /*isDefaultLib*/ false, - /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.Import, file: file.path, index, }, resolution.packageId); - } - if (isFromNodeModulesSearch) { - currentNodeModulesDepth--; - } - } - } - else { - // no imports - drop cached module resolutions - file.resolvedModules = undefined; - } - } + function getProjectReferenceOutputName(referencedProject: ts.ResolvedProjectReference, fileName: string) { + const out = ts.outFile(referencedProject.commandLine.options); + return out ? + ts.changeExtension(out, ts.Extension.Dts) : + ts.getOutputDeclarationFileName(fileName, referencedProject.commandLine, !host.useCaseSensitiveFileNames()); + } - function checkSourceFilesBelongToPath(sourceFiles: readonly ts.SourceFile[], rootDirectory: string): boolean { - let allFilesBelongToPath = true; - const absoluteRootDirectoryPath = host.getCanonicalFileName(ts.getNormalizedAbsolutePath(rootDirectory, currentDirectory)); - for (const sourceFile of sourceFiles) { - if (!sourceFile.isDeclarationFile) { - const absoluteSourceFilePath = host.getCanonicalFileName(ts.getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); - if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { - addProgramDiagnosticExplainingFile(sourceFile, ts.Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, [sourceFile.fileName, rootDirectory]); - allFilesBelongToPath = false; - } + /** + * Get the referenced project if the file is input file from that reference project + */ + function getResolvedProjectReferenceToRedirect(fileName: string) { + if (mapFromFileToProjectReferenceRedirects === undefined) { + mapFromFileToProjectReferenceRedirects = new ts.Map(); + forEachResolvedProjectReference(referencedProject => { + // not input file from the referenced project, ignore + if (toPath(options.configFilePath!) !== referencedProject.sourceFile.path) { + referencedProject.commandLine.fileNames.forEach(f => mapFromFileToProjectReferenceRedirects!.set(toPath(f), referencedProject.sourceFile.path)); } - } - - return allFilesBelongToPath; + }); } - function parseProjectReferenceConfigFile(ref: ts.ProjectReference): ts.ResolvedProjectReference | undefined { - if (!projectReferenceRedirects) { - projectReferenceRedirects = new ts.Map(); - } + const referencedProjectPath = mapFromFileToProjectReferenceRedirects.get(toPath(fileName)); + return referencedProjectPath && getResolvedProjectReferenceByPath(referencedProjectPath); + } - // 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; - } + function forEachResolvedProjectReference(cb: (resolvedProjectReference: ts.ResolvedProjectReference) => T | undefined): T | undefined { + return ts.forEachResolvedProjectReference(resolvedProjectReferences, cb); + } - let commandLine: ts.ParsedCommandLine | undefined; - let sourceFile: ts.JsonSourceFile | undefined; - if (host.getParsedCommandLine) { - commandLine = host.getParsedCommandLine(refPath); - if (!commandLine) { - addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined); - projectReferenceRedirects.set(sourceFilePath, false); - return undefined; + function getSourceOfProjectReferenceRedirect(path: ts.Path) { + if (!ts.isDeclarationFileName(path)) + return undefined; + if (mapFromToProjectReferenceRedirectSource === undefined) { + mapFromToProjectReferenceRedirectSource = new ts.Map(); + forEachResolvedProjectReference(resolvedRef => { + const out = ts.outFile(resolvedRef.commandLine.options); + if (out) { + // Dont know which source file it means so return true? + const outputDts = ts.changeExtension(out, ts.Extension.Dts); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); } - sourceFile = ts.Debug.checkDefined(commandLine.options.configFile); - ts.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 = ts.getNormalizedAbsolutePath(ts.getDirectoryPath(refPath), host.getCurrentDirectory()); - sourceFile = host.getSourceFile(refPath, ts.ScriptTarget.JSON) as ts.JsonSourceFile | undefined; - addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); - if (sourceFile === undefined) { - projectReferenceRedirects.set(sourceFilePath, false); - return undefined; + else { + const getCommonSourceDirectory = ts.memoize(() => ts.getCommonSourceDirectoryOfConfig(resolvedRef.commandLine, !host.useCaseSensitiveFileNames())); + ts.forEach(resolvedRef.commandLine.fileNames, fileName => { + if (!ts.isDeclarationFileName(fileName) && !ts.fileExtensionIs(fileName, ts.Extension.Json)) { + const outputDts = ts.getOutputDeclarationFileName(fileName, resolvedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); + } + }); } - commandLine = ts.parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); - } - sourceFile.fileName = refPath; - sourceFile.path = sourceFilePath; - sourceFile.resolvedPath = sourceFilePath; - sourceFile.originalFileName = refPath; - - const resolvedRef: ts.ResolvedProjectReference = { commandLine, sourceFile }; - projectReferenceRedirects.set(sourceFilePath, resolvedRef); - if (commandLine.projectReferences) { - resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile); - } - return resolvedRef; + }); } + return mapFromToProjectReferenceRedirectSource.get(path); + } - function verifyCompilerOptions() { - if (options.strictPropertyInitialization && !ts.getStrictOptionValue(options, "strictNullChecks")) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "strictPropertyInitialization", "strictNullChecks"); - } - if (options.exactOptionalPropertyTypes && !ts.getStrictOptionValue(options, "strictNullChecks")) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "exactOptionalPropertyTypes", "strictNullChecks"); - } + function isSourceOfProjectReferenceRedirect(fileName: string) { + return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); + } - if (options.isolatedModules) { - if (options.out) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "isolatedModules"); - } + function getResolvedProjectReferenceByPath(projectReferencePath: ts.Path): ts.ResolvedProjectReference | undefined { + if (!projectReferenceRedirects) { + return undefined; + } - if (options.outFile) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "outFile", "isolatedModules"); - } - } + return projectReferenceRedirects.get(projectReferencePath) || undefined; + } - if (options.inlineSourceMap) { - if (options.sourceMap) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "sourceMap", "inlineSourceMap"); - } - if (options.mapRoot) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "mapRoot", "inlineSourceMap"); - } - } + function processReferencedFiles(file: ts.SourceFile, isDefaultLib: boolean) { + ts.forEach(file.referencedFiles, (ref, index) => { + processSourceFile(resolveTripleslashReference(ref.fileName, file.fileName), isDefaultLib, + /*ignoreNoDefaultLib*/ false, + /*packageId*/ undefined, { kind: ts.FileIncludeKind.ReferenceFile, file: file.path, index, }); + }); + } - if (options.composite) { - if (options.declaration === false) { - createDiagnosticForOptionName(ts.Diagnostics.Composite_projects_may_not_disable_declaration_emit, "declaration"); - } - if (options.incremental === false) { - createDiagnosticForOptionName(ts.Diagnostics.Composite_projects_may_not_disable_incremental_compilation, "declaration"); - } - } + function processTypeReferenceDirectives(file: ts.SourceFile) { + const typeDirectives = file.typeReferenceDirectives; + if (!typeDirectives) { + return; + } - const outputFile = ts.outFile(options); - if (options.tsBuildInfoFile) { - if (!ts.isIncrementalCompilation(options)) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "tsBuildInfoFile", "incremental", "composite"); - } - } - else if (options.incremental && !outputFile && !options.configFilePath) { - programDiagnostics.add(ts.createCompilerDiagnostic(ts.Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeDirectives, file); + for (let index = 0; index < typeDirectives.length; index++) { + const ref = file.typeReferenceDirectives[index]; + const resolvedTypeReferenceDirective = resolutions[index]; + // store resolved type directive on the file + const fileName = ts.toFileNameLowerCase(ref.fileName); + ts.setResolvedTypeReferenceDirective(file, fileName, resolvedTypeReferenceDirective); + const mode = ref.resolutionMode || file.impliedNodeFormat; + if (mode && ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Node16 && ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeNext) { + programDiagnostics.add(ts.createDiagnosticForRange(file, ref, ts.Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node16_or_nodenext)); } + processTypeReferenceDirective(fileName, mode, resolvedTypeReferenceDirective, { kind: ts.FileIncludeKind.TypeReferenceDirective, file: file.path, index, }); + } + } - verifyProjectReferences(); + function processTypeReferenceDirective(typeReferenceDirective: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined, resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined, reason: ts.FileIncludeReason): void { + ts.tracing?.push(ts.tracing.Phase.Program, "processTypeReferenceDirective", { directive: typeReferenceDirective, hasResolved: !!resolveModuleNamesReusingOldState, refKind: reason.kind, refPath: isReferencedFile(reason) ? reason.file : undefined }); + processTypeReferenceDirectiveWorker(typeReferenceDirective, mode, resolvedTypeReferenceDirective, reason); + ts.tracing?.pop(); + } + function processTypeReferenceDirectiveWorker(typeReferenceDirective: string, mode: ts.SourceFile["impliedNodeFormat"] | undefined, resolvedTypeReferenceDirective: ts.ResolvedTypeReferenceDirective | undefined, reason: ts.FileIncludeReason): void { - // List of collected files is complete; validate exhautiveness if this is a project with a file list - if (options.composite) { - const rootPaths = new ts.Set(rootNames.map(toPath)); - for (const file of files) { - // Ignore file that is not emitted - if (ts.sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { - addProgramDiagnosticExplainingFile(file, ts.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 we already found this library as a primary reference - nothing to do + const previousResolution = resolvedTypeReferenceDirectives.get(typeReferenceDirective, mode); + if (previousResolution && previousResolution.primary) { + return; + } + let saveResolution = true; + if (resolvedTypeReferenceDirective) { + if (resolvedTypeReferenceDirective.isExternalLibraryImport) + currentNodeModulesDepth++; - if (options.paths) { - for (const key in options.paths) { - if (!ts.hasProperty(options.paths, key)) { - continue; - } - if (!ts.hasZeroOrOneAsteriskCharacter(key)) { - createDiagnosticForOptionPaths(/*onKey*/ true, key, ts.Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, key); - } - if (ts.isArray(options.paths[key])) { - const len = options.paths[key].length; - if (len === 0) { - createDiagnosticForOptionPaths(/*onKey*/ false, key, ts.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 (!ts.hasZeroOrOneAsteriskCharacter(subst)) { - createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Substitution_0_in_pattern_1_can_have_at_most_one_Asterisk_character, subst, key); - } - if (!options.baseUrl && !ts.pathIsRelative(subst) && !ts.pathIsAbsolute(subst)) { - createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Non_relative_paths_are_not_allowed_when_baseUrl_is_not_set_Did_you_forget_a_leading_Slash); - } - } - else { - createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Substitution_0_for_pattern_1_has_incorrect_type_expected_string_got_2, subst, key, typeOfSubst); - } + if (resolvedTypeReferenceDirective.primary) { + // resolved from the primary path + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, reason); // 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) { + addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, ts.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]); } } - else { - createDiagnosticForOptionPaths(/*onKey*/ false, key, ts.Diagnostics.Substitutions_for_pattern_0_should_be_an_array, key); - } + // don't overwrite previous resolution result + saveResolution = false; } - } - - if (!options.sourceMap && !options.inlineSourceMap) { - if (options.inlineSources) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "inlineSources"); - } - if (options.sourceRoot) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "sourceRoot"); + else { + // First resolution of this library + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, reason); } } - if (options.out && options.outFile) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "outFile"); - } + if (resolvedTypeReferenceDirective.isExternalLibraryImport) + currentNodeModulesDepth--; + } + else { + addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, ts.Diagnostics.Cannot_find_type_definition_file_for_0, [typeReferenceDirective]); + } - if (options.mapRoot && !(options.sourceMap || options.declarationMap)) { - // Error to specify --mapRoot without --sourcemap - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "mapRoot", "sourceMap", "declarationMap"); - } + if (saveResolution) { + resolvedTypeReferenceDirectives.set(typeReferenceDirective, mode, resolvedTypeReferenceDirective); + } + } - if (options.declarationDir) { - if (!ts.getEmitDeclarations(options)) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationDir", "declaration", "composite"); - } - if (outputFile) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "declarationDir", options.out ? "out" : "outFile"); - } - } + function pathForLibFile(libFileName: string): string { + // Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and + // lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable + // lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown + const components = libFileName.split("."); + let path = components[1]; + let i = 2; + while (components[i] && components[i] !== "d") { + path += (i === 2 ? "/" : "-") + components[i]; + i++; + } + const resolveFrom = ts.combinePaths(currentDirectory, `__lib_node_modules_lookup_${libFileName}__.ts`); + const localOverrideModuleResult = ts.resolveModuleName("@typescript/lib-" + path, resolveFrom, { moduleResolution: ts.ModuleResolutionKind.NodeJs }, host, moduleResolutionCache); + if (localOverrideModuleResult?.resolvedModule) { + return localOverrideModuleResult.resolvedModule.resolvedFileName; + } + return ts.combinePaths(defaultLibraryPath, libFileName); + } - if (options.declarationMap && !ts.getEmitDeclarations(options)) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationMap", "declaration", "composite"); + function processLibReferenceDirectives(file: ts.SourceFile) { + ts.forEach(file.libReferenceDirectives, (libReference, index) => { + const libName = ts.toFileNameLowerCase(libReference.fileName); + const libFileName = ts.libMap.get(libName); + if (libFileName) { + // we ignore any 'no-default-lib' reference set on this file. + processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true, { kind: ts.FileIncludeKind.LibReferenceDirective, file: file.path, index, }); } - - if (options.lib && options.noLib) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "lib", "noLib"); + else { + const unqualifiedLibName = ts.removeSuffix(ts.removePrefix(libName, "lib."), ".d.ts"); + const suggestion = ts.getSpellingSuggestion(unqualifiedLibName, ts.libs, ts.identity); + const diagnostic = suggestion ? ts.Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : ts.Diagnostics.Cannot_find_lib_definition_for_0; + (fileProcessingDiagnostics ||= []).push({ + kind: ts.FilePreprocessingDiagnosticsKind.FilePreprocessingReferencedDiagnostic, + reason: { kind: ts.FileIncludeKind.LibReferenceDirective, file: file.path, index, }, + diagnostic, + args: [libName, suggestion] + }); } + }); + } - if (options.noImplicitUseStrict && ts.getStrictOptionValue(options, "alwaysStrict")) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "noImplicitUseStrict", "alwaysStrict"); - } + function getCanonicalFileName(fileName: string): string { + return host.getCanonicalFileName(fileName); + } - const languageVersion = ts.getEmitScriptTarget(options); - const firstNonAmbientExternalModuleSourceFile = ts.find(files, f => ts.isExternalModule(f) && !f.isDeclarationFile); - if (options.isolatedModules) { - if (options.module === ts.ModuleKind.None && languageVersion < ts.ScriptTarget.ES2015) { - createDiagnosticForOptionName(ts.Diagnostics.Option_isolatedModules_can_only_be_used_when_either_option_module_is_provided_or_option_target_is_ES2015_or_higher, "isolatedModules", "target"); + function processImportedModules(file: ts.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, file); + ts.Debug.assert(resolutions.length === moduleNames.length); + const optionsForFile = (useSourceOfProjectReferenceRedirect ? getRedirectReferenceForResolution(file)?.commandLine.options : undefined) || options; + for (let index = 0; index < moduleNames.length; index++) { + const resolution = resolutions[index]; + ts.setResolvedModule(file, moduleNames[index], resolution, getModeForResolutionAtIndex(file, index)); + + if (!resolution) { + continue; } - if (options.preserveConstEnums === false) { - createDiagnosticForOptionName(ts.Diagnostics.Option_preserveConstEnums_cannot_be_disabled_when_isolatedModules_is_enabled, "preserveConstEnums", "isolatedModules"); - } + const isFromNodeModulesSearch = resolution.isExternalLibraryImport; + const isJsFile = !ts.resolutionExtensionIsTSOrJson(resolution.extension); + const isJsFileFromNodeModules = isFromNodeModulesSearch && isJsFile; + const resolvedFileName = resolution.resolvedFileName; - const firstNonExternalModuleSourceFile = ts.find(files, f => !ts.isExternalModule(f) && !ts.isSourceFileJS(f) && !f.isDeclarationFile && f.scriptKind !== ts.ScriptKind.JSON); - if (firstNonExternalModuleSourceFile) { - const span = ts.getErrorSpanForNode(firstNonExternalModuleSourceFile, firstNonExternalModuleSourceFile); - programDiagnostics.add(ts.createFileDiagnostic(firstNonExternalModuleSourceFile, span.start, span.length, ts.Diagnostics._0_cannot_be_compiled_under_isolatedModules_because_it_is_considered_a_global_script_file_Add_an_import_export_or_an_empty_export_statement_to_make_it_a_module, ts.getBaseFileName(firstNonExternalModuleSourceFile.fileName))); + if (isFromNodeModulesSearch) { + currentNodeModulesDepth++; } - } - else if (firstNonAmbientExternalModuleSourceFile && languageVersion < ts.ScriptTarget.ES2015 && options.module === ts.ModuleKind.None) { - // We cannot use createDiagnosticFromNode because nodes do not have parents yet - const span = ts.getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(ts.createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, ts.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 (outputFile && !options.emitDeclarationOnly) { - if (options.module && !(options.module === ts.ModuleKind.AMD || options.module === ts.ModuleKind.System)) { - createDiagnosticForOptionName(ts.Diagnostics.Only_amd_and_system_modules_are_supported_alongside_0, options.out ? "out" : "outFile", "module"); - } - else if (options.module === undefined && firstNonAmbientExternalModuleSourceFile) { - const span = ts.getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(ts.createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, ts.Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, options.out ? "out" : "outFile")); + // 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(optionsForFile, resolution) + && !optionsForFile.noResolve + && index < file.imports.length + && !elideImport + && !(isJsFile && !ts.getAllowJSCompilerOption(optionsForFile)) + && (ts.isInJSFile(file.imports[index]) || !(file.imports[index].flags & ts.NodeFlags.JSDoc)); + + if (elideImport) { + modulesWithElidedImports.set(file.path, true); + } + else if (shouldAddFile) { + findSourceFile(resolvedFileName, + /*isDefaultLib*/ false, + /*ignoreNoDefaultLib*/ false, { kind: ts.FileIncludeKind.Import, file: file.path, index, }, resolution.packageId); + } + + if (isFromNodeModulesSearch) { + currentNodeModulesDepth--; } } + } + else { + // no imports - drop cached module resolutions + file.resolvedModules = undefined; + } + } - if (options.resolveJsonModule) { - if (ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeJs && - ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Node16 && - ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeNext) { - createDiagnosticForOptionName(ts.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 (!ts.hasJsonModuleEmitEnabled(options)) { - createDiagnosticForOptionName(ts.Diagnostics.Option_resolveJsonModule_can_only_be_specified_when_module_code_generation_is_commonjs_amd_es2015_or_esNext, "resolveJsonModule", "module"); + function checkSourceFilesBelongToPath(sourceFiles: readonly ts.SourceFile[], rootDirectory: string): boolean { + let allFilesBelongToPath = true; + const absoluteRootDirectoryPath = host.getCanonicalFileName(ts.getNormalizedAbsolutePath(rootDirectory, currentDirectory)); + for (const sourceFile of sourceFiles) { + if (!sourceFile.isDeclarationFile) { + const absoluteSourceFilePath = host.getCanonicalFileName(ts.getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); + if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { + addProgramDiagnosticExplainingFile(sourceFile, ts.Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, [sourceFile.fileName, rootDirectory]); + allFilesBelongToPath = false; } } + } - // there has to be common source directory if user specified --outdir || --rootDir || --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.rootDir || // there is --rootDir specified - options.sourceRoot || // there is --sourceRoot specified - options.mapRoot) { // there is --mapRoot specified - - // Precalculate and cache the common source directory - const dir = getCommonSourceDirectory(); + return allFilesBelongToPath; + } - // 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 => ts.getRootLength(file.fileName) > 1)) { - createDiagnosticForOptionName(ts.Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files, "outDir"); - } - } + function parseProjectReferenceConfigFile(ref: ts.ProjectReference): ts.ResolvedProjectReference | undefined { + if (!projectReferenceRedirects) { + projectReferenceRedirects = new ts.Map(); + } - if (options.useDefineForClassFields && languageVersion === ts.ScriptTarget.ES3) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_target_is_ES3, "useDefineForClassFields"); - } + // 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; + } - if (options.checkJs && !ts.getAllowJSCompilerOption(options)) { - programDiagnostics.add(ts.createCompilerDiagnostic(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "checkJs", "allowJs")); + let commandLine: ts.ParsedCommandLine | undefined; + let sourceFile: ts.JsonSourceFile | undefined; + if (host.getParsedCommandLine) { + commandLine = host.getParsedCommandLine(refPath); + if (!commandLine) { + addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined); + projectReferenceRedirects.set(sourceFilePath, false); + return undefined; } - - if (options.emitDeclarationOnly) { - if (!ts.getEmitDeclarations(options)) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "emitDeclarationOnly", "declaration", "composite"); - } - - if (options.noEmit) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "emitDeclarationOnly", "noEmit"); - } + sourceFile = ts.Debug.checkDefined(commandLine.options.configFile); + ts.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 = ts.getNormalizedAbsolutePath(ts.getDirectoryPath(refPath), host.getCurrentDirectory()); + sourceFile = host.getSourceFile(refPath, ts.ScriptTarget.JSON) as ts.JsonSourceFile | undefined; + addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); + if (sourceFile === undefined) { + projectReferenceRedirects.set(sourceFilePath, false); + return undefined; } + commandLine = ts.parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); + } + sourceFile.fileName = refPath; + sourceFile.path = sourceFilePath; + sourceFile.resolvedPath = sourceFilePath; + sourceFile.originalFileName = refPath; - if (options.emitDecoratorMetadata && - !options.experimentalDecorators) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"); - } + const resolvedRef: ts.ResolvedProjectReference = { commandLine, sourceFile }; + projectReferenceRedirects.set(sourceFilePath, resolvedRef); + if (commandLine.projectReferences) { + resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile); + } + return resolvedRef; + } - if (options.jsxFactory) { - if (options.reactNamespace) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"); - } - if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxFactory", ts.inverseJsxOptionMap.get("" + options.jsx)); - } - if (!ts.parseIsolatedEntityName(options.jsxFactory, languageVersion)) { - createOptionValueDiagnostic("jsxFactory", ts.Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory); - } - } - else if (options.reactNamespace && !ts.isIdentifierText(options.reactNamespace, languageVersion)) { - createOptionValueDiagnostic("reactNamespace", ts.Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace); - } + function verifyCompilerOptions() { + if (options.strictPropertyInitialization && !ts.getStrictOptionValue(options, "strictNullChecks")) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "strictPropertyInitialization", "strictNullChecks"); + } + if (options.exactOptionalPropertyTypes && !ts.getStrictOptionValue(options, "strictNullChecks")) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "exactOptionalPropertyTypes", "strictNullChecks"); + } - if (options.jsxFragmentFactory) { - if (!options.jsxFactory) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "jsxFragmentFactory", "jsxFactory"); - } - if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxFragmentFactory", ts.inverseJsxOptionMap.get("" + options.jsx)); - } - if (!ts.parseIsolatedEntityName(options.jsxFragmentFactory, languageVersion)) { - createOptionValueDiagnostic("jsxFragmentFactory", ts.Diagnostics.Invalid_value_for_jsxFragmentFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFragmentFactory); - } + if (options.isolatedModules) { + if (options.out) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "isolatedModules"); } - if (options.reactNamespace) { - if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "reactNamespace", ts.inverseJsxOptionMap.get("" + options.jsx)); - } + if (options.outFile) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "outFile", "isolatedModules"); } + } - if (options.jsxImportSource) { - if (options.jsx === ts.JsxEmit.React) { - createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxImportSource", ts.inverseJsxOptionMap.get("" + options.jsx)); - } + if (options.inlineSourceMap) { + if (options.sourceMap) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "sourceMap", "inlineSourceMap"); } - - if (options.preserveValueImports && ts.getEmitModuleKind(options) < ts.ModuleKind.ES2015) { - createOptionValueDiagnostic("importsNotUsedAsValues", ts.Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later); + if (options.mapRoot) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "mapRoot", "inlineSourceMap"); } + } - // 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 = new ts.Set(); - ts.forEachEmittedFile(emitHost, (emitFileNames) => { - if (!options.emitDeclarationOnly) { - verifyEmitFilePath(emitFileNames.jsFilePath, emitFilesSeen); - } - verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen); - }); + if (options.composite) { + if (options.declaration === false) { + createDiagnosticForOptionName(ts.Diagnostics.Composite_projects_may_not_disable_declaration_emit, "declaration"); } - - // Verify that all the emit files are unique and don't overwrite input files - function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: ts.Set) { - if (emitFileName) { - const emitFilePath = toPath(emitFileName); - // Report error if the output overwrites input file - if (filesByName.has(emitFilePath)) { - let chain: ts.DiagnosticMessageChain | undefined; - if (!options.configFilePath) { - // The program is from either an inferred project or an external project - chain = ts.chainDiagnosticMessages(/*details*/ undefined, ts.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 = ts.chainDiagnosticMessages(chain, ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file, emitFileName); - blockEmittingOfFile(emitFileName, ts.createCompilerDiagnosticFromMessageChain(chain)); - } - - const emitFileKey = !host.useCaseSensitiveFileNames() ? ts.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, ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, emitFileName)); - } - else { - emitFilesSeen.add(emitFileKey); - } - } + if (options.incremental === false) { + createDiagnosticForOptionName(ts.Diagnostics.Composite_projects_may_not_disable_incremental_compilation, "declaration"); } } - function createDiagnosticExplainingFile(file: ts.SourceFile | undefined, fileProcessingReason: ts.FileIncludeReason | undefined, diagnostic: ts.DiagnosticMessage, args: (string | number | undefined)[] | undefined): ts.Diagnostic { - let fileIncludeReasons: ts.DiagnosticMessageChain[] | undefined; - let relatedInfo: ts.Diagnostic[] | undefined; - let locationReason = isReferencedFile(fileProcessingReason) ? fileProcessingReason : undefined; - if (file) - fileReasons.get(file.path)?.forEach(processReason); - if (fileProcessingReason) - processReason(fileProcessingReason); - // If we have location and there is only one reason file is in which is the location, dont add details for file include - if (locationReason && fileIncludeReasons?.length === 1) - fileIncludeReasons = undefined; - const location = locationReason && getReferencedFileLocation(getSourceFileByPath, locationReason); - const fileIncludeReasonDetails = fileIncludeReasons && ts.chainDiagnosticMessages(fileIncludeReasons, ts.Diagnostics.The_file_is_in_the_program_because_Colon); - const redirectInfo = file && ts.explainIfFileIsRedirect(file); - const chain = ts.chainDiagnosticMessages(redirectInfo ? fileIncludeReasonDetails ? [fileIncludeReasonDetails, ...redirectInfo] : redirectInfo : fileIncludeReasonDetails, diagnostic, ...args || ts.emptyArray); - return location && isReferenceFileLocation(location) ? - ts.createFileDiagnosticFromMessageChain(location.file, location.pos, location.end - location.pos, chain, relatedInfo) : - ts.createCompilerDiagnosticFromMessageChain(chain, relatedInfo); - function processReason(reason: ts.FileIncludeReason) { - (fileIncludeReasons ||= []).push(ts.fileIncludeReasonToDiagnostics(program, reason)); - if (!locationReason && isReferencedFile(reason)) { - // Report error at first reference file or file currently in processing and dont report in related information - locationReason = reason; - } - else if (locationReason !== reason) { - relatedInfo = ts.append(relatedInfo, fileIncludeReasonToRelatedInformation(reason)); - } - // Remove fileProcessingReason if its already included in fileReasons of the program - if (reason === fileProcessingReason) - fileProcessingReason = undefined; + const outputFile = ts.outFile(options); + if (options.tsBuildInfoFile) { + if (!ts.isIncrementalCompilation(options)) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "tsBuildInfoFile", "incremental", "composite"); } } - - function addFilePreprocessingFileExplainingDiagnostic(file: ts.SourceFile | undefined, fileProcessingReason: ts.FileIncludeReason, diagnostic: ts.DiagnosticMessage, args?: (string | number | undefined)[]) { - (fileProcessingDiagnostics ||= []).push({ - kind: ts.FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic, - file: file && file.path, - fileProcessingReason, - diagnostic, - args - }); + else if (options.incremental && !outputFile && !options.configFilePath) { + programDiagnostics.add(ts.createCompilerDiagnostic(ts.Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); } - function addProgramDiagnosticExplainingFile(file: ts.SourceFile, diagnostic: ts.DiagnosticMessage, args?: (string | number | undefined)[]) { - programDiagnostics.add(createDiagnosticExplainingFile(file, /*fileProcessingReason*/ undefined, diagnostic, args)); - } + verifyProjectReferences(); - function fileIncludeReasonToRelatedInformation(reason: ts.FileIncludeReason): ts.DiagnosticWithLocation | undefined { - if (isReferencedFile(reason)) { - const referenceLocation = getReferencedFileLocation(getSourceFileByPath, reason); - let message: ts.DiagnosticMessage; - switch (reason.kind) { - case ts.FileIncludeKind.Import: - message = ts.Diagnostics.File_is_included_via_import_here; - break; - case ts.FileIncludeKind.ReferenceFile: - message = ts.Diagnostics.File_is_included_via_reference_here; - break; - case ts.FileIncludeKind.TypeReferenceDirective: - message = ts.Diagnostics.File_is_included_via_type_library_reference_here; - break; - case ts.FileIncludeKind.LibReferenceDirective: - message = ts.Diagnostics.File_is_included_via_library_reference_here; - break; - default: - ts.Debug.assertNever(reason); + // List of collected files is complete; validate exhautiveness if this is a project with a file list + if (options.composite) { + const rootPaths = new ts.Set(rootNames.map(toPath)); + for (const file of files) { + // Ignore file that is not emitted + if (ts.sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { + addProgramDiagnosticExplainingFile(file, ts.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 || ""]); } - return isReferenceFileLocation(referenceLocation) ? ts.createFileDiagnostic(referenceLocation.file, referenceLocation.pos, referenceLocation.end - referenceLocation.pos, message) : undefined; - } - if (!options.configFile) - return undefined; - let configFileNode: ts.Node | undefined; - let message: ts.DiagnosticMessage; - switch (reason.kind) { - case ts.FileIncludeKind.RootFile: - if (!options.configFile.configFileSpecs) - return undefined; - const fileName = ts.getNormalizedAbsolutePath(rootNames[reason.index], currentDirectory); - const matchedByFiles = ts.getMatchedFileSpec(program, fileName); - if (matchedByFiles) { - configFileNode = ts.getTsConfigPropArrayElementValue(options.configFile, "files", matchedByFiles); - message = ts.Diagnostics.File_is_matched_by_files_list_specified_here; - break; - } - const matchedByInclude = ts.getMatchedIncludeSpec(program, fileName); - // Could be additional files specified as roots - if (!matchedByInclude || !ts.isString(matchedByInclude)) - return undefined; - configFileNode = ts.getTsConfigPropArrayElementValue(options.configFile, "include", matchedByInclude); - message = ts.Diagnostics.File_is_matched_by_include_pattern_specified_here; - break; - case ts.FileIncludeKind.SourceFromProjectReference: - case ts.FileIncludeKind.OutputFromProjectReference: - const referencedResolvedRef = ts.Debug.checkDefined(resolvedProjectReferences?.[reason.index]); - const referenceInfo = forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => resolvedRef === referencedResolvedRef ? { sourceFile: parent?.sourceFile || options.configFile!, index } : undefined); - if (!referenceInfo) - return undefined; - const { sourceFile, index } = referenceInfo; - const referencesSyntax = ts.firstDefined(ts.getTsConfigPropArray(sourceFile as ts.TsConfigSourceFile, "references"), property => ts.isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); - return referencesSyntax && referencesSyntax.elements.length > index ? - ts.createDiagnosticForNodeInSourceFile(sourceFile, referencesSyntax.elements[index], reason.kind === ts.FileIncludeKind.OutputFromProjectReference ? - ts.Diagnostics.File_is_output_from_referenced_project_specified_here : - ts.Diagnostics.File_is_source_from_referenced_project_specified_here) : - undefined; - case ts.FileIncludeKind.AutomaticTypeDirectiveFile: - if (!options.types) - return undefined; - configFileNode = getOptionsSyntaxByArrayElementValue("types", reason.typeReference); - message = ts.Diagnostics.File_is_entry_point_of_type_library_specified_here; - break; - case ts.FileIncludeKind.LibFile: - if (reason.index !== undefined) { - configFileNode = getOptionsSyntaxByArrayElementValue("lib", options.lib![reason.index]); - message = ts.Diagnostics.File_is_library_specified_here; - break; - } - const target = ts.forEachEntry(ts.targetOptionDeclaration.type, (value, key) => value === ts.getEmitScriptTarget(options) ? key : undefined); - configFileNode = target ? getOptionsSyntaxByValue("target", target) : undefined; - message = ts.Diagnostics.File_is_default_library_for_target_specified_here; - break; - default: - ts.Debug.assertNever(reason); } - return configFileNode && ts.createDiagnosticForNodeInSourceFile(options.configFile, configFileNode, message); } - function verifyProjectReferences() { - const buildInfoPath = !options.suppressOutputPathCheck ? ts.getTsBuildInfoEmitOutputFilePath(options) : undefined; - forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => { - const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; - const parentFile = parent && parent.sourceFile as ts.JsonSourceFile; - if (!resolvedRef) { - createDiagnosticForReference(parentFile, index, ts.Diagnostics.File_0_not_found, ref.path); - return; + if (options.paths) { + for (const key in options.paths) { + if (!ts.hasProperty(options.paths, key)) { + continue; } - const options = resolvedRef.commandLine.options; - if (!options.composite || options.noEmit) { - // ok to not have composite if the current program is container only - const inputs = parent ? parent.commandLine.fileNames : rootNames; - if (inputs.length) { - if (!options.composite) - createDiagnosticForReference(parentFile, index, ts.Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path); - if (options.noEmit) - createDiagnosticForReference(parentFile, index, ts.Diagnostics.Referenced_project_0_may_not_disable_emit, ref.path); - } + if (!ts.hasZeroOrOneAsteriskCharacter(key)) { + createDiagnosticForOptionPaths(/*onKey*/ true, key, ts.Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, key); } - if (ref.prepend) { - const out = ts.outFile(options); - if (out) { - if (!host.fileExists(out)) { - createDiagnosticForReference(parentFile, index, ts.Diagnostics.Output_file_0_from_project_1_does_not_exist, out, ref.path); + if (ts.isArray(options.paths[key])) { + const len = options.paths[key].length; + if (len === 0) { + createDiagnosticForOptionPaths(/*onKey*/ false, key, ts.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 (!ts.hasZeroOrOneAsteriskCharacter(subst)) { + createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Substitution_0_in_pattern_1_can_have_at_most_one_Asterisk_character, subst, key); + } + if (!options.baseUrl && !ts.pathIsRelative(subst) && !ts.pathIsAbsolute(subst)) { + createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Non_relative_paths_are_not_allowed_when_baseUrl_is_not_set_Did_you_forget_a_leading_Slash); + } + } + else { + createDiagnosticForOptionPathKeyValue(key, i, ts.Diagnostics.Substitution_0_for_pattern_1_has_incorrect_type_expected_string_got_2, subst, key, typeOfSubst); } } - else { - createDiagnosticForReference(parentFile, index, ts.Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set, ref.path); - } } - if (!parent && buildInfoPath && buildInfoPath === ts.getTsBuildInfoEmitOutputFilePath(options)) { - createDiagnosticForReference(parentFile, index, ts.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 createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number) { - let needCompilerDiagnostic = true; - const pathsSyntax = getOptionPathsSyntax(); - for (const pathProp of pathsSyntax) { - if (ts.isObjectLiteralExpression(pathProp.initializer)) { - for (const keyProps of ts.getPropertyAssignment(pathProp.initializer, key)) { - const initializer = keyProps.initializer; - if (ts.isArrayLiteralExpression(initializer) && initializer.elements.length > valueIndex) { - programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(options.configFile!, initializer.elements[valueIndex], message, arg0, arg1, arg2)); - needCompilerDiagnostic = false; - } - } + else { + createDiagnosticForOptionPaths(/*onKey*/ false, key, ts.Diagnostics.Substitutions_for_pattern_0_should_be_an_array, key); } } - - if (needCompilerDiagnostic) { - programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1, arg2)); - } } - function createDiagnosticForOptionPaths(onKey: boolean, key: string, message: ts.DiagnosticMessage, arg0: string | number) { - let needCompilerDiagnostic = true; - const pathsSyntax = getOptionPathsSyntax(); - for (const pathProp of pathsSyntax) { - if (ts.isObjectLiteralExpression(pathProp.initializer) && - createOptionDiagnosticInObjectLiteralSyntax(pathProp.initializer, onKey, key, /*key2*/ undefined, message, arg0)) { - needCompilerDiagnostic = false; - } + if (!options.sourceMap && !options.inlineSourceMap) { + if (options.inlineSources) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "inlineSources"); } - if (needCompilerDiagnostic) { - programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0)); + if (options.sourceRoot) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "sourceRoot"); } } - function getOptionsSyntaxByName(name: string) { - const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); - return compilerOptionsObjectLiteralSyntax && ts.getPropertyAssignment(compilerOptionsObjectLiteralSyntax, name); + if (options.out && options.outFile) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "outFile"); } - function getOptionPathsSyntax() { - return getOptionsSyntaxByName("paths") || ts.emptyArray; + if (options.mapRoot && !(options.sourceMap || options.declarationMap)) { + // Error to specify --mapRoot without --sourcemap + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "mapRoot", "sourceMap", "declarationMap"); } - function getOptionsSyntaxByValue(name: string, value: string) { - const syntaxByName = getOptionsSyntaxByName(name); - return syntaxByName && ts.firstDefined(syntaxByName, property => ts.isStringLiteral(property.initializer) && property.initializer.text === value ? property.initializer : undefined); + if (options.declarationDir) { + if (!ts.getEmitDeclarations(options)) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationDir", "declaration", "composite"); + } + if (outputFile) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "declarationDir", options.out ? "out" : "outFile"); + } } - function getOptionsSyntaxByArrayElementValue(name: string, value: string) { - const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); - return compilerOptionsObjectLiteralSyntax && ts.getPropertyArrayElementValue(compilerOptionsObjectLiteralSyntax, name, value); + if (options.declarationMap && !ts.getEmitDeclarations(options)) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationMap", "declaration", "composite"); } - function createDiagnosticForOptionName(message: ts.DiagnosticMessage, option1: string, option2?: string, option3?: string) { - createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3); + if (options.lib && options.noLib) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "lib", "noLib"); } - function createOptionValueDiagnostic(option1: string, message: ts.DiagnosticMessage, arg0?: string, arg1?: string) { - createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0, arg1); + if (options.noImplicitUseStrict && ts.getStrictOptionValue(options, "alwaysStrict")) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "noImplicitUseStrict", "alwaysStrict"); } - function createDiagnosticForReference(sourceFile: ts.JsonSourceFile | undefined, index: number, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number) { - const referencesSyntax = ts.firstDefined(ts.getTsConfigPropArray(sourceFile || options.configFile, "references"), property => ts.isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); - if (referencesSyntax && referencesSyntax.elements.length > index) { - programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, arg0, arg1)); + const languageVersion = ts.getEmitScriptTarget(options); + const firstNonAmbientExternalModuleSourceFile = ts.find(files, f => ts.isExternalModule(f) && !f.isDeclarationFile); + if (options.isolatedModules) { + if (options.module === ts.ModuleKind.None && languageVersion < ts.ScriptTarget.ES2015) { + createDiagnosticForOptionName(ts.Diagnostics.Option_isolatedModules_can_only_be_used_when_either_option_module_is_provided_or_option_target_is_ES2015_or_higher, "isolatedModules", "target"); } - else { - programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1)); + + if (options.preserveConstEnums === false) { + createDiagnosticForOptionName(ts.Diagnostics.Option_preserveConstEnums_cannot_be_disabled_when_isolatedModules_is_enabled, "preserveConstEnums", "isolatedModules"); } - } - function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: ts.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); + const firstNonExternalModuleSourceFile = ts.find(files, f => !ts.isExternalModule(f) && !ts.isSourceFileJS(f) && !f.isDeclarationFile && f.scriptKind !== ts.ScriptKind.JSON); + if (firstNonExternalModuleSourceFile) { + const span = ts.getErrorSpanForNode(firstNonExternalModuleSourceFile, firstNonExternalModuleSourceFile); + programDiagnostics.add(ts.createFileDiagnostic(firstNonExternalModuleSourceFile, span.start, span.length, ts.Diagnostics._0_cannot_be_compiled_under_isolatedModules_because_it_is_considered_a_global_script_file_Add_an_import_export_or_an_empty_export_statement_to_make_it_a_module, ts.getBaseFileName(firstNonExternalModuleSourceFile.fileName))); + } + } + else if (firstNonAmbientExternalModuleSourceFile && languageVersion < ts.ScriptTarget.ES2015 && options.module === ts.ModuleKind.None) { + // We cannot use createDiagnosticFromNode because nodes do not have parents yet + const span = ts.getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); + programDiagnostics.add(ts.createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, ts.Diagnostics.Cannot_use_imports_exports_or_module_augmentations_when_module_is_none)); + } - if (needCompilerDiagnostic) { - programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1, arg2)); + // Cannot specify module gen that isn't amd or system with --out + if (outputFile && !options.emitDeclarationOnly) { + if (options.module && !(options.module === ts.ModuleKind.AMD || options.module === ts.ModuleKind.System)) { + createDiagnosticForOptionName(ts.Diagnostics.Only_amd_and_system_modules_are_supported_alongside_0, options.out ? "out" : "outFile", "module"); + } + else if (options.module === undefined && firstNonAmbientExternalModuleSourceFile) { + const span = ts.getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); + programDiagnostics.add(ts.createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, ts.Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, options.out ? "out" : "outFile")); } } - function getCompilerOptionsObjectLiteralSyntax() { - if (_compilerOptionsObjectLiteralSyntax === undefined) { - _compilerOptionsObjectLiteralSyntax = false; - const jsonObjectLiteral = ts.getTsConfigObjectLiteralExpression(options.configFile); - if (jsonObjectLiteral) { - for (const prop of ts.getPropertyAssignment(jsonObjectLiteral, "compilerOptions")) { - if (ts.isObjectLiteralExpression(prop.initializer)) { - _compilerOptionsObjectLiteralSyntax = prop.initializer; - break; - } - } - } + if (options.resolveJsonModule) { + if (ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeJs && + ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.Node16 && + ts.getEmitModuleResolutionKind(options) !== ts.ModuleResolutionKind.NodeNext) { + createDiagnosticForOptionName(ts.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 (!ts.hasJsonModuleEmitEnabled(options)) { + createDiagnosticForOptionName(ts.Diagnostics.Option_resolveJsonModule_can_only_be_specified_when_module_code_generation_is_commonjs_amd_es2015_or_esNext, "resolveJsonModule", "module"); } - return _compilerOptionsObjectLiteralSyntax || undefined; } - function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ts.ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): boolean { - const props = ts.getPropertyAssignment(objectLiteral, key1, key2); - for (const prop of props) { - programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2)); + // there has to be common source directory if user specified --outdir || --rootDir || --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.rootDir || // there is --rootDir 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 => ts.getRootLength(file.fileName) > 1)) { + createDiagnosticForOptionName(ts.Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files, "outDir"); } - return !!props.length; } - function blockEmittingOfFile(emitFileName: string, diag: ts.Diagnostic) { - hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); - programDiagnostics.add(diag); + if (options.useDefineForClassFields && languageVersion === ts.ScriptTarget.ES3) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_target_is_ES3, "useDefineForClassFields"); + } + + if (options.checkJs && !ts.getAllowJSCompilerOption(options)) { + programDiagnostics.add(ts.createCompilerDiagnostic(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "checkJs", "allowJs")); } - function isEmittedFile(file: string): boolean { + if (options.emitDeclarationOnly) { + if (!ts.getEmitDeclarations(options)) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "emitDeclarationOnly", "declaration", "composite"); + } + if (options.noEmit) { - return false; + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "emitDeclarationOnly", "noEmit"); } + } - // If this is source file, its not emitted file - const filePath = toPath(file); - if (getSourceFileByPath(filePath)) { - return false; + if (options.emitDecoratorMetadata && + !options.experimentalDecorators) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"); + } + + if (options.jsxFactory) { + if (options.reactNamespace) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"); + } + if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxFactory", ts.inverseJsxOptionMap.get("" + options.jsx)); } + if (!ts.parseIsolatedEntityName(options.jsxFactory, languageVersion)) { + createOptionValueDiagnostic("jsxFactory", ts.Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory); + } + } + else if (options.reactNamespace && !ts.isIdentifierText(options.reactNamespace, languageVersion)) { + createOptionValueDiagnostic("reactNamespace", ts.Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace); + } - // If options have --outFile or --out just check that - const out = ts.outFile(options); - if (out) { - return isSameFile(filePath, out) || isSameFile(filePath, ts.removeFileExtension(out) + ts.Extension.Dts); + if (options.jsxFragmentFactory) { + if (!options.jsxFactory) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "jsxFragmentFactory", "jsxFactory"); + } + if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxFragmentFactory", ts.inverseJsxOptionMap.get("" + options.jsx)); } + if (!ts.parseIsolatedEntityName(options.jsxFragmentFactory, languageVersion)) { + createOptionValueDiagnostic("jsxFragmentFactory", ts.Diagnostics.Invalid_value_for_jsxFragmentFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFragmentFactory); + } + } - // If declarationDir is specified, return if its a file in that directory - if (options.declarationDir && ts.containsPath(options.declarationDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; + if (options.reactNamespace) { + if (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "reactNamespace", ts.inverseJsxOptionMap.get("" + options.jsx)); } + } - // If --outDir, check if file is in that directory - if (options.outDir) { - return ts.containsPath(options.outDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames()); + if (options.jsxImportSource) { + if (options.jsx === ts.JsxEmit.React) { + createDiagnosticForOptionName(ts.Diagnostics.Option_0_cannot_be_specified_when_option_jsx_is_1, "jsxImportSource", ts.inverseJsxOptionMap.get("" + options.jsx)); } + } + + if (options.preserveValueImports && ts.getEmitModuleKind(options) < ts.ModuleKind.ES2015) { + createOptionValueDiagnostic("importsNotUsedAsValues", ts.Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later); + } + + // 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 = new ts.Set(); + ts.forEachEmittedFile(emitHost, (emitFileNames) => { + if (!options.emitDeclarationOnly) { + verifyEmitFilePath(emitFileNames.jsFilePath, emitFilesSeen); + } + 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.Set) { + if (emitFileName) { + const emitFilePath = toPath(emitFileName); + // Report error if the output overwrites input file + if (filesByName.has(emitFilePath)) { + let chain: ts.DiagnosticMessageChain | undefined; + if (!options.configFilePath) { + // The program is from either an inferred project or an external project + chain = ts.chainDiagnosticMessages(/*details*/ undefined, ts.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 = ts.chainDiagnosticMessages(chain, ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file, emitFileName); + blockEmittingOfFile(emitFileName, ts.createCompilerDiagnosticFromMessageChain(chain)); + } - if (ts.fileExtensionIsOneOf(filePath, ts.supportedJSExtensionsFlat) || ts.isDeclarationFileName(filePath)) { - // Otherwise just check if sourceFile with the name exists - const filePathWithoutExtension = ts.removeFileExtension(filePath); - return !!getSourceFileByPath((filePathWithoutExtension + ts.Extension.Ts) as ts.Path) || - !!getSourceFileByPath((filePathWithoutExtension + ts.Extension.Tsx) as ts.Path); + const emitFileKey = !host.useCaseSensitiveFileNames() ? ts.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, ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, emitFileName)); + } + else { + emitFilesSeen.add(emitFileKey); + } } - return false; } + } + + function createDiagnosticExplainingFile(file: ts.SourceFile | undefined, fileProcessingReason: ts.FileIncludeReason | undefined, diagnostic: ts.DiagnosticMessage, args: (string | number | undefined)[] | undefined): ts.Diagnostic { + let fileIncludeReasons: ts.DiagnosticMessageChain[] | undefined; + let relatedInfo: ts.Diagnostic[] | undefined; + let locationReason = isReferencedFile(fileProcessingReason) ? fileProcessingReason : undefined; + if (file) + fileReasons.get(file.path)?.forEach(processReason); + if (fileProcessingReason) + processReason(fileProcessingReason); + // If we have location and there is only one reason file is in which is the location, dont add details for file include + if (locationReason && fileIncludeReasons?.length === 1) + fileIncludeReasons = undefined; + const location = locationReason && getReferencedFileLocation(getSourceFileByPath, locationReason); + const fileIncludeReasonDetails = fileIncludeReasons && ts.chainDiagnosticMessages(fileIncludeReasons, ts.Diagnostics.The_file_is_in_the_program_because_Colon); + const redirectInfo = file && ts.explainIfFileIsRedirect(file); + const chain = ts.chainDiagnosticMessages(redirectInfo ? fileIncludeReasonDetails ? [fileIncludeReasonDetails, ...redirectInfo] : redirectInfo : fileIncludeReasonDetails, diagnostic, ...args || ts.emptyArray); + return location && isReferenceFileLocation(location) ? + ts.createFileDiagnosticFromMessageChain(location.file, location.pos, location.end - location.pos, chain, relatedInfo) : + ts.createCompilerDiagnosticFromMessageChain(chain, relatedInfo); + function processReason(reason: ts.FileIncludeReason) { + (fileIncludeReasons ||= []).push(ts.fileIncludeReasonToDiagnostics(program, reason)); + if (!locationReason && isReferencedFile(reason)) { + // Report error at first reference file or file currently in processing and dont report in related information + locationReason = reason; + } + else if (locationReason !== reason) { + relatedInfo = ts.append(relatedInfo, fileIncludeReasonToRelatedInformation(reason)); + } + // Remove fileProcessingReason if its already included in fileReasons of the program + if (reason === fileProcessingReason) + fileProcessingReason = undefined; + } + } + + function addFilePreprocessingFileExplainingDiagnostic(file: ts.SourceFile | undefined, fileProcessingReason: ts.FileIncludeReason, diagnostic: ts.DiagnosticMessage, args?: (string | number | undefined)[]) { + (fileProcessingDiagnostics ||= []).push({ + kind: ts.FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic, + file: file && file.path, + fileProcessingReason, + diagnostic, + args + }); + } + + function addProgramDiagnosticExplainingFile(file: ts.SourceFile, diagnostic: ts.DiagnosticMessage, args?: (string | number | undefined)[]) { + programDiagnostics.add(createDiagnosticExplainingFile(file, /*fileProcessingReason*/ undefined, diagnostic, args)); + } - function isSameFile(file1: string, file2: string) { - return ts.comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === ts.Comparison.EqualTo; + function fileIncludeReasonToRelatedInformation(reason: ts.FileIncludeReason): ts.DiagnosticWithLocation | undefined { + if (isReferencedFile(reason)) { + const referenceLocation = getReferencedFileLocation(getSourceFileByPath, reason); + let message: ts.DiagnosticMessage; + switch (reason.kind) { + case ts.FileIncludeKind.Import: + message = ts.Diagnostics.File_is_included_via_import_here; + break; + case ts.FileIncludeKind.ReferenceFile: + message = ts.Diagnostics.File_is_included_via_reference_here; + break; + case ts.FileIncludeKind.TypeReferenceDirective: + message = ts.Diagnostics.File_is_included_via_type_library_reference_here; + break; + case ts.FileIncludeKind.LibReferenceDirective: + message = ts.Diagnostics.File_is_included_via_library_reference_here; + break; + default: + ts.Debug.assertNever(reason); + } + return isReferenceFileLocation(referenceLocation) ? ts.createFileDiagnostic(referenceLocation.file, referenceLocation.pos, referenceLocation.end - referenceLocation.pos, message) : undefined; } + if (!options.configFile) + return undefined; + let configFileNode: ts.Node | undefined; + let message: ts.DiagnosticMessage; + switch (reason.kind) { + case ts.FileIncludeKind.RootFile: + if (!options.configFile.configFileSpecs) + return undefined; + const fileName = ts.getNormalizedAbsolutePath(rootNames[reason.index], currentDirectory); + const matchedByFiles = ts.getMatchedFileSpec(program, fileName); + if (matchedByFiles) { + configFileNode = ts.getTsConfigPropArrayElementValue(options.configFile, "files", matchedByFiles); + message = ts.Diagnostics.File_is_matched_by_files_list_specified_here; + break; + } + const matchedByInclude = ts.getMatchedIncludeSpec(program, fileName); + // Could be additional files specified as roots + if (!matchedByInclude || !ts.isString(matchedByInclude)) + return undefined; + configFileNode = ts.getTsConfigPropArrayElementValue(options.configFile, "include", matchedByInclude); + message = ts.Diagnostics.File_is_matched_by_include_pattern_specified_here; + break; + case ts.FileIncludeKind.SourceFromProjectReference: + case ts.FileIncludeKind.OutputFromProjectReference: + const referencedResolvedRef = ts.Debug.checkDefined(resolvedProjectReferences?.[reason.index]); + const referenceInfo = forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => resolvedRef === referencedResolvedRef ? { sourceFile: parent?.sourceFile || options.configFile!, index } : undefined); + if (!referenceInfo) + return undefined; + const { sourceFile, index } = referenceInfo; + const referencesSyntax = ts.firstDefined(ts.getTsConfigPropArray(sourceFile as ts.TsConfigSourceFile, "references"), property => ts.isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); + return referencesSyntax && referencesSyntax.elements.length > index ? + ts.createDiagnosticForNodeInSourceFile(sourceFile, referencesSyntax.elements[index], reason.kind === ts.FileIncludeKind.OutputFromProjectReference ? + ts.Diagnostics.File_is_output_from_referenced_project_specified_here : + ts.Diagnostics.File_is_source_from_referenced_project_specified_here) : + undefined; + case ts.FileIncludeKind.AutomaticTypeDirectiveFile: + if (!options.types) + return undefined; + configFileNode = getOptionsSyntaxByArrayElementValue("types", reason.typeReference); + message = ts.Diagnostics.File_is_entry_point_of_type_library_specified_here; + break; + case ts.FileIncludeKind.LibFile: + if (reason.index !== undefined) { + configFileNode = getOptionsSyntaxByArrayElementValue("lib", options.lib![reason.index]); + message = ts.Diagnostics.File_is_library_specified_here; + break; + } + const target = ts.forEachEntry(ts.targetOptionDeclaration.type, (value, key) => value === ts.getEmitScriptTarget(options) ? key : undefined); + configFileNode = target ? getOptionsSyntaxByValue("target", target) : undefined; + message = ts.Diagnostics.File_is_default_library_for_target_specified_here; + break; + default: + ts.Debug.assertNever(reason); + } + return configFileNode && ts.createDiagnosticForNodeInSourceFile(options.configFile, configFileNode, message); + } - function getSymlinkCache(): ts.SymlinkCache { - if (host.getSymlinkCache) { - return host.getSymlinkCache(); + function verifyProjectReferences() { + const buildInfoPath = !options.suppressOutputPathCheck ? ts.getTsBuildInfoEmitOutputFilePath(options) : undefined; + forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => { + const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; + const parentFile = parent && parent.sourceFile as ts.JsonSourceFile; + if (!resolvedRef) { + createDiagnosticForReference(parentFile, index, ts.Diagnostics.File_0_not_found, ref.path); + return; } - if (!symlinks) { - symlinks = ts.createSymlinkCache(currentDirectory, getCanonicalFileName); + const options = resolvedRef.commandLine.options; + if (!options.composite || options.noEmit) { + // ok to not have composite if the current program is container only + const inputs = parent ? parent.commandLine.fileNames : rootNames; + if (inputs.length) { + if (!options.composite) + createDiagnosticForReference(parentFile, index, ts.Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path); + if (options.noEmit) + createDiagnosticForReference(parentFile, index, ts.Diagnostics.Referenced_project_0_may_not_disable_emit, ref.path); + } + } + if (ref.prepend) { + const out = ts.outFile(options); + if (out) { + if (!host.fileExists(out)) { + createDiagnosticForReference(parentFile, index, ts.Diagnostics.Output_file_0_from_project_1_does_not_exist, out, ref.path); + } + } + else { + createDiagnosticForReference(parentFile, index, ts.Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set, ref.path); + } } - if (files && resolvedTypeReferenceDirectives && !symlinks.hasProcessedResolutions()) { - symlinks.setSymlinksFromResolutions(files, resolvedTypeReferenceDirectives); + if (!parent && buildInfoPath && buildInfoPath === ts.getTsBuildInfoEmitOutputFilePath(options)) { + createDiagnosticForReference(parentFile, index, ts.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 createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number) { + let needCompilerDiagnostic = true; + const pathsSyntax = getOptionPathsSyntax(); + for (const pathProp of pathsSyntax) { + if (ts.isObjectLiteralExpression(pathProp.initializer)) { + for (const keyProps of ts.getPropertyAssignment(pathProp.initializer, key)) { + const initializer = keyProps.initializer; + if (ts.isArrayLiteralExpression(initializer) && initializer.elements.length > valueIndex) { + programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(options.configFile!, initializer.elements[valueIndex], message, arg0, arg1, arg2)); + needCompilerDiagnostic = false; + } + } } - return symlinks; + } + + if (needCompilerDiagnostic) { + programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1, arg2)); } } - interface HostForUseSourceOfProjectReferenceRedirect { - compilerHost: ts.CompilerHost; - getSymlinkCache: () => ts.SymlinkCache; - useSourceOfProjectReferenceRedirect: boolean; - toPath(fileName: string): ts.Path; - getResolvedProjectReferences(): readonly (ts.ResolvedProjectReference | undefined)[] | undefined; - getSourceOfProjectReferenceRedirect(path: ts.Path): ts.SourceOfProjectReferenceRedirect | undefined; - forEachResolvedProjectReference(cb: (resolvedProjectReference: ts.ResolvedProjectReference) => T | undefined): T | undefined; + function createDiagnosticForOptionPaths(onKey: boolean, key: string, message: ts.DiagnosticMessage, arg0: string | number) { + let needCompilerDiagnostic = true; + const pathsSyntax = getOptionPathsSyntax(); + for (const pathProp of pathsSyntax) { + if (ts.isObjectLiteralExpression(pathProp.initializer) && + createOptionDiagnosticInObjectLiteralSyntax(pathProp.initializer, onKey, key, /*key2*/ undefined, message, arg0)) { + needCompilerDiagnostic = false; + } + } + if (needCompilerDiagnostic) { + programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0)); + } } - function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) { - let setOfDeclarationDirectories: ts.Set | undefined; - const originalFileExists = host.compilerHost.fileExists; - const originalDirectoryExists = host.compilerHost.directoryExists; - const originalGetDirectories = host.compilerHost.getDirectories; - const originalRealpath = host.compilerHost.realpath; + function getOptionsSyntaxByName(name: string) { + const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); + return compilerOptionsObjectLiteralSyntax && ts.getPropertyAssignment(compilerOptionsObjectLiteralSyntax, name); + } - if (!host.useSourceOfProjectReferenceRedirect) - return { onProgramCreateComplete: ts.noop, fileExists }; + function getOptionPathsSyntax() { + return getOptionsSyntaxByName("paths") || ts.emptyArray; + } - host.compilerHost.fileExists = fileExists; + function getOptionsSyntaxByValue(name: string, value: string) { + const syntaxByName = getOptionsSyntaxByName(name); + return syntaxByName && ts.firstDefined(syntaxByName, property => ts.isStringLiteral(property.initializer) && property.initializer.text === value ? property.initializer : undefined); + } - let directoryExists; - if (originalDirectoryExists) { - // This implementation of directoryExists checks if the directory being requested is - // directory of .d.ts file for the referenced Project. - // If it is it returns true irrespective of whether that directory exists on host - directoryExists = host.compilerHost.directoryExists = path => { - if (originalDirectoryExists.call(host.compilerHost, path)) { - handleDirectoryCouldBeSymlink(path); - return true; - } + function getOptionsSyntaxByArrayElementValue(name: string, value: string) { + const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); + return compilerOptionsObjectLiteralSyntax && ts.getPropertyArrayElementValue(compilerOptionsObjectLiteralSyntax, name, value); + } - if (!host.getResolvedProjectReferences()) - return false; + function createDiagnosticForOptionName(message: ts.DiagnosticMessage, option1: string, option2?: string, option3?: string) { + createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3); + } - if (!setOfDeclarationDirectories) { - setOfDeclarationDirectories = new ts.Set(); - host.forEachResolvedProjectReference(ref => { - const out = ts.outFile(ref.commandLine.options); - if (out) { - setOfDeclarationDirectories!.add(ts.getDirectoryPath(host.toPath(out))); - } - else { - // Set declaration's in different locations only, if they are next to source the directory present doesnt change - const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; - if (declarationDir) { - setOfDeclarationDirectories!.add(host.toPath(declarationDir)); - } - } - }); - } + function createOptionValueDiagnostic(option1: string, message: ts.DiagnosticMessage, arg0?: string, arg1?: string) { + createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0, arg1); + } - return fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); - }; + function createDiagnosticForReference(sourceFile: ts.JsonSourceFile | undefined, index: number, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number) { + const referencesSyntax = ts.firstDefined(ts.getTsConfigPropArray(sourceFile || options.configFile, "references"), property => ts.isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); + if (referencesSyntax && referencesSyntax.elements.length > index) { + programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, arg0, arg1)); } + else { + programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1)); + } + } + + function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: ts.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 (originalGetDirectories) { - // Call getDirectories only if directory actually present on the host - // This is needed to ensure that we arent getting directories that we fake about presence for - host.compilerHost.getDirectories = path => !host.getResolvedProjectReferences() || (originalDirectoryExists && originalDirectoryExists.call(host.compilerHost, path)) ? - originalGetDirectories.call(host.compilerHost, path) : - []; + if (needCompilerDiagnostic) { + programDiagnostics.add(ts.createCompilerDiagnostic(message, arg0, arg1, arg2)); + } + } + + function getCompilerOptionsObjectLiteralSyntax() { + if (_compilerOptionsObjectLiteralSyntax === undefined) { + _compilerOptionsObjectLiteralSyntax = false; + const jsonObjectLiteral = ts.getTsConfigObjectLiteralExpression(options.configFile); + if (jsonObjectLiteral) { + for (const prop of ts.getPropertyAssignment(jsonObjectLiteral, "compilerOptions")) { + if (ts.isObjectLiteralExpression(prop.initializer)) { + _compilerOptionsObjectLiteralSyntax = prop.initializer; + break; + } + } + } } + return _compilerOptionsObjectLiteralSyntax || undefined; + } - // This is something we keep for life time of the host - if (originalRealpath) { - host.compilerHost.realpath = s => host.getSymlinkCache().getSymlinkedFiles()?.get(host.toPath(s)) || - originalRealpath.call(host.compilerHost, s); + function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ts.ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: ts.DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): boolean { + const props = ts.getPropertyAssignment(objectLiteral, key1, key2); + for (const prop of props) { + programDiagnostics.add(ts.createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2)); } + return !!props.length; + } - return { onProgramCreateComplete, fileExists, directoryExists }; + function blockEmittingOfFile(emitFileName: string, diag: ts.Diagnostic) { + hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); + programDiagnostics.add(diag); + } - function onProgramCreateComplete() { - host.compilerHost.fileExists = originalFileExists; - host.compilerHost.directoryExists = originalDirectoryExists; - host.compilerHost.getDirectories = originalGetDirectories; - // DO not revert realpath as it could be used later + function isEmittedFile(file: string): boolean { + if (options.noEmit) { + return false; } - // This implementation of fileExists checks if the file being requested is - // .d.ts file for the referenced Project. - // If it is it returns true irrespective of whether that file exists on host - function fileExists(file: string) { - if (originalFileExists.call(host.compilerHost, file)) - return true; - if (!host.getResolvedProjectReferences()) - return false; - if (!ts.isDeclarationFileName(file)) - return false; + // If this is source file, its not emitted file + const filePath = toPath(file); + if (getSourceFileByPath(filePath)) { + return false; + } - // Project references go to source file instead of .d.ts file - return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true); + // If options have --outFile or --out just check that + const out = ts.outFile(options); + if (out) { + return isSameFile(filePath, out) || isSameFile(filePath, ts.removeFileExtension(out) + ts.Extension.Dts); } - function fileExistsIfProjectReferenceDts(file: string) { - const source = host.getSourceOfProjectReferenceRedirect(host.toPath(file)); - return source !== undefined ? - ts.isString(source) ? originalFileExists.call(host.compilerHost, source) as boolean : true : - undefined; + // If declarationDir is specified, return if its a file in that directory + if (options.declarationDir && ts.containsPath(options.declarationDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames())) { + return true; } - function directoryExistsIfProjectReferenceDeclDir(dir: string) { - const dirPath = host.toPath(dir); - const dirPathWithTrailingDirectorySeparator = `${dirPath}${ts.directorySeparator}`; - return ts.forEachKey(setOfDeclarationDirectories!, declDirPath => dirPath === declDirPath || - // Any parent directory of declaration dir - ts.startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || - // Any directory inside declaration dir - ts.startsWith(dirPath, `${declDirPath}/`)); + // If --outDir, check if file is in that directory + if (options.outDir) { + return ts.containsPath(options.outDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames()); } - function handleDirectoryCouldBeSymlink(directory: string) { - if (!host.getResolvedProjectReferences() || ts.containsIgnoredPath(directory)) - return; + if (ts.fileExtensionIsOneOf(filePath, ts.supportedJSExtensionsFlat) || ts.isDeclarationFileName(filePath)) { + // Otherwise just check if sourceFile with the name exists + const filePathWithoutExtension = ts.removeFileExtension(filePath); + return !!getSourceFileByPath((filePathWithoutExtension + ts.Extension.Ts) as ts.Path) || + !!getSourceFileByPath((filePathWithoutExtension + ts.Extension.Tsx) as ts.Path); + } + return false; + } - // Because we already watch node_modules, handle symlinks in there - if (!originalRealpath || !ts.stringContains(directory, ts.nodeModulesPathPart)) - return; - const symlinkCache = host.getSymlinkCache(); - const directoryPath = ts.ensureTrailingDirectorySeparator(host.toPath(directory)); - if (symlinkCache.getSymlinkedDirectories()?.has(directoryPath)) - return; - const real = ts.normalizePath(originalRealpath.call(host.compilerHost, directory)); - let realPath: ts.Path; - if (real === directory || - (realPath = ts.ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) { - // not symlinked - symlinkCache.setSymlinkedDirectory(directoryPath, false); - return; - } + function isSameFile(file1: string, file2: string) { + return ts.comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === ts.Comparison.EqualTo; + } - symlinkCache.setSymlinkedDirectory(directory, { - real: ts.ensureTrailingDirectorySeparator(real), - realPath - }); + function getSymlinkCache(): ts.SymlinkCache { + if (host.getSymlinkCache) { + return host.getSymlinkCache(); + } + if (!symlinks) { + symlinks = ts.createSymlinkCache(currentDirectory, getCanonicalFileName); } + if (files && resolvedTypeReferenceDirectives && !symlinks.hasProcessedResolutions()) { + symlinks.setSymlinksFromResolutions(files, resolvedTypeReferenceDirectives); + } + return symlinks; + } +} - function fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean { - const fileOrDirectoryExistsUsingSource = isFile ? - (file: string) => fileExistsIfProjectReferenceDts(file) : - (dir: string) => directoryExistsIfProjectReferenceDeclDir(dir); - // Check current directory or file - const result = fileOrDirectoryExistsUsingSource(fileOrDirectory); - if (result !== undefined) - return result; +interface HostForUseSourceOfProjectReferenceRedirect { + compilerHost: ts.CompilerHost; + getSymlinkCache: () => ts.SymlinkCache; + useSourceOfProjectReferenceRedirect: boolean; + toPath(fileName: string): ts.Path; + getResolvedProjectReferences(): readonly (ts.ResolvedProjectReference | undefined)[] | undefined; + getSourceOfProjectReferenceRedirect(path: ts.Path): ts.SourceOfProjectReferenceRedirect | undefined; + forEachResolvedProjectReference(cb: (resolvedProjectReference: ts.ResolvedProjectReference) => T | undefined): T | undefined; +} - const symlinkCache = host.getSymlinkCache(); - const symlinkedDirectories = symlinkCache.getSymlinkedDirectories(); - if (!symlinkedDirectories) - return false; - const fileOrDirectoryPath = host.toPath(fileOrDirectory); - if (!ts.stringContains(fileOrDirectoryPath, ts.nodeModulesPathPart)) - return false; - if (isFile && symlinkCache.getSymlinkedFiles()?.has(fileOrDirectoryPath)) +function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) { + let setOfDeclarationDirectories: ts.Set | undefined; + const originalFileExists = host.compilerHost.fileExists; + const originalDirectoryExists = host.compilerHost.directoryExists; + const originalGetDirectories = host.compilerHost.getDirectories; + const originalRealpath = host.compilerHost.realpath; + + if (!host.useSourceOfProjectReferenceRedirect) + return { onProgramCreateComplete: ts.noop, fileExists }; + + host.compilerHost.fileExists = fileExists; + + let directoryExists; + if (originalDirectoryExists) { + // This implementation of directoryExists checks if the directory being requested is + // directory of .d.ts file for the referenced Project. + // If it is it returns true irrespective of whether that directory exists on host + directoryExists = host.compilerHost.directoryExists = path => { + if (originalDirectoryExists.call(host.compilerHost, path)) { + handleDirectoryCouldBeSymlink(path); return true; + } - // If it contains node_modules check if its one of the symlinked path we know of - return ts.firstDefinedIterator(symlinkedDirectories.entries(), ([directoryPath, symlinkedDirectory]) => { - if (!symlinkedDirectory || !ts.startsWith(fileOrDirectoryPath, directoryPath)) - return undefined; - const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath)); - if (isFile && result) { - // Store the real path for the file' - const absolutePath = ts.getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory()); - symlinkCache.setSymlinkedFile(fileOrDirectoryPath, `${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`); + if (!host.getResolvedProjectReferences()) + return false; + + if (!setOfDeclarationDirectories) { + setOfDeclarationDirectories = new ts.Set(); + host.forEachResolvedProjectReference(ref => { + const out = ts.outFile(ref.commandLine.options); + if (out) { + setOfDeclarationDirectories!.add(ts.getDirectoryPath(host.toPath(out))); } - return result; - }) || false; - } - } + else { + // Set declaration's in different locations only, if they are next to source the directory present doesnt change + const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; + if (declarationDir) { + setOfDeclarationDirectories!.add(host.toPath(declarationDir)); + } + } + }); + } - /*@internal*/ - export const emitSkippedWithNoDiagnostics: ts.EmitResult = { diagnostics: ts.emptyArray, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; + return fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); + }; + } - /*@internal*/ - export function handleNoEmitOptions(program: ts.Program | T, sourceFile: ts.SourceFile | undefined, writeFile: ts.WriteFileCallback | undefined, cancellationToken: ts.CancellationToken | undefined): ts.EmitResult | undefined { - const options = program.getCompilerOptions(); - if (options.noEmit) { - // Cache the semantic diagnostics - program.getSemanticDiagnostics(sourceFile, cancellationToken); - return sourceFile || ts.outFile(options) ? - emitSkippedWithNoDiagnostics : - program.emitBuildInfo(writeFile, cancellationToken); - } + if (originalGetDirectories) { + // Call getDirectories only if directory actually present on the host + // This is needed to ensure that we arent getting directories that we fake about presence for + host.compilerHost.getDirectories = path => !host.getResolvedProjectReferences() || (originalDirectoryExists && originalDirectoryExists.call(host.compilerHost, path)) ? + originalGetDirectories.call(host.compilerHost, path) : + []; + } - // 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 ts.Diagnostic[] = [ - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSemanticDiagnostics(sourceFile, cancellationToken) - ]; + // This is something we keep for life time of the host + if (originalRealpath) { + host.compilerHost.realpath = s => host.getSymlinkCache().getSymlinkedFiles()?.get(host.toPath(s)) || + originalRealpath.call(host.compilerHost, s); + } - if (diagnostics.length === 0 && ts.getEmitDeclarations(program.getCompilerOptions())) { - diagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); - } + return { onProgramCreateComplete, fileExists, directoryExists }; - if (!diagnostics.length) - return undefined; - let emittedFiles: string[] | undefined; - if (!sourceFile && !ts.outFile(options)) { - const emitResult = program.emitBuildInfo(writeFile, cancellationToken); - if (emitResult.diagnostics) - diagnostics = [...diagnostics, ...emitResult.diagnostics]; - emittedFiles = emitResult.emittedFiles; - } - return { diagnostics, sourceMaps: undefined, emittedFiles, emitSkipped: true }; + function onProgramCreateComplete() { + host.compilerHost.fileExists = originalFileExists; + host.compilerHost.directoryExists = originalDirectoryExists; + host.compilerHost.getDirectories = originalGetDirectories; + // DO not revert realpath as it could be used later } - /*@internal*/ - export function filterSemanticDiagnostics(diagnostic: readonly ts.Diagnostic[], option: ts.CompilerOptions): readonly ts.Diagnostic[] { - return ts.filter(diagnostic, d => !d.skippedOn || !option[d.skippedOn]); + // This implementation of fileExists checks if the file being requested is + // .d.ts file for the referenced Project. + // If it is it returns true irrespective of whether that file exists on host + function fileExists(file: string) { + if (originalFileExists.call(host.compilerHost, file)) + return true; + if (!host.getResolvedProjectReferences()) + return false; + if (!ts.isDeclarationFileName(file)) + return false; + + // Project references go to source file instead of .d.ts file + return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true); } - /*@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?: ts.DiagnosticReporter; + function fileExistsIfProjectReferenceDts(file: string) { + const source = host.getSourceOfProjectReferenceRedirect(host.toPath(file)); + return source !== undefined ? + ts.isString(source) ? originalFileExists.call(host.compilerHost, source) as boolean : true : + undefined; } - /* @internal */ - export function parseConfigHostFromCompilerHostLike(host: CompilerHostLike, directoryStructureHost: ts.DirectoryStructureHost = host): ts.ParseConfigFileHost { - return { - fileExists: f => directoryStructureHost.fileExists(f), - readDirectory(root, extensions, excludes, includes, depth) { - ts.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 || ts.returnUndefined, - trace: host.trace ? (s) => host.trace!(s) : undefined - }; + function directoryExistsIfProjectReferenceDeclDir(dir: string) { + const dirPath = host.toPath(dir); + const dirPathWithTrailingDirectorySeparator = `${dirPath}${ts.directorySeparator}`; + return ts.forEachKey(setOfDeclarationDirectories!, declDirPath => dirPath === declDirPath || + // Any parent directory of declaration dir + ts.startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || + // Any directory inside declaration dir + ts.startsWith(dirPath, `${declDirPath}/`)); } - // For backward compatibility - /** @deprecated */ export interface ResolveProjectReferencePathHost { - fileExists(fileName: string): boolean; + function handleDirectoryCouldBeSymlink(directory: string) { + if (!host.getResolvedProjectReferences() || ts.containsIgnoredPath(directory)) + return; + + // Because we already watch node_modules, handle symlinks in there + if (!originalRealpath || !ts.stringContains(directory, ts.nodeModulesPathPart)) + return; + const symlinkCache = host.getSymlinkCache(); + const directoryPath = ts.ensureTrailingDirectorySeparator(host.toPath(directory)); + if (symlinkCache.getSymlinkedDirectories()?.has(directoryPath)) + return; + const real = ts.normalizePath(originalRealpath.call(host.compilerHost, directory)); + let realPath: ts.Path; + if (real === directory || + (realPath = ts.ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) { + // not symlinked + symlinkCache.setSymlinkedDirectory(directoryPath, false); + return; + } + + symlinkCache.setSymlinkedDirectory(directory, { + real: ts.ensureTrailingDirectorySeparator(real), + realPath + }); } - /* @internal */ - export function createPrependNodes(projectReferences: readonly ts.ProjectReference[] | undefined, getCommandLine: (ref: ts.ProjectReference, index: number) => ts.ParsedCommandLine | undefined, readFile: (path: string) => string | undefined) { - if (!projectReferences) - return ts.emptyArray; - let nodes: ts.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 = ts.outFile(resolvedRefOpts.options); - // Upstream project didn't have outFile set -- skip (error will have been issued earlier) - if (!out) - continue; - const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = ts.getOutputPathsForBundle(resolvedRefOpts.options, /*forceDtsPaths*/ true); - const node = ts.createInputFiles(readFile, jsFilePath!, sourceMapFilePath, declarationFilePath!, declarationMapPath, buildInfoPath); - (nodes || (nodes = [])).push(node); + function fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean { + const fileOrDirectoryExistsUsingSource = isFile ? + (file: string) => fileExistsIfProjectReferenceDts(file) : + (dir: string) => directoryExistsIfProjectReferenceDeclDir(dir); + // Check current directory or file + const result = fileOrDirectoryExistsUsingSource(fileOrDirectory); + if (result !== undefined) + return result; + + const symlinkCache = host.getSymlinkCache(); + const symlinkedDirectories = symlinkCache.getSymlinkedDirectories(); + if (!symlinkedDirectories) + return false; + const fileOrDirectoryPath = host.toPath(fileOrDirectory); + if (!ts.stringContains(fileOrDirectoryPath, ts.nodeModulesPathPart)) + return false; + if (isFile && symlinkCache.getSymlinkedFiles()?.has(fileOrDirectoryPath)) + return true; + + // If it contains node_modules check if its one of the symlinked path we know of + return ts.firstDefinedIterator(symlinkedDirectories.entries(), ([directoryPath, symlinkedDirectory]) => { + if (!symlinkedDirectory || !ts.startsWith(fileOrDirectoryPath, directoryPath)) + return undefined; + const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath)); + if (isFile && result) { + // Store the real path for the file' + const absolutePath = ts.getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory()); + symlinkCache.setSymlinkedFile(fileOrDirectoryPath, `${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`); + } + return result; + }) || false; } - } - return nodes || ts.emptyArray; } - /** - * Returns the target config filename of a project reference. - * Note: The file might not exist. - */ - export function resolveProjectReferencePath(ref: ts.ProjectReference): ts.ResolvedConfigFileName; - /** @deprecated */ export function resolveProjectReferencePath(host: ResolveProjectReferencePathHost, ref: ts.ProjectReference): ts.ResolvedConfigFileName; - export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePathHost | ts.ProjectReference, ref?: ts.ProjectReference): ts.ResolvedConfigFileName { - const passedInRef = ref ? ref : hostOrRef as ts.ProjectReference; - return ts.resolveConfigFileProjectName(passedInRef.path); + +/*@internal*/ +export const emitSkippedWithNoDiagnostics: ts.EmitResult = { diagnostics: ts.emptyArray, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; + +/*@internal*/ +export function handleNoEmitOptions(program: ts.Program | T, sourceFile: ts.SourceFile | undefined, writeFile: ts.WriteFileCallback | undefined, cancellationToken: ts.CancellationToken | undefined): ts.EmitResult | undefined { + const options = program.getCompilerOptions(); + if (options.noEmit) { + // Cache the semantic diagnostics + program.getSemanticDiagnostics(sourceFile, cancellationToken); + return sourceFile || ts.outFile(options) ? + emitSkippedWithNoDiagnostics : + program.emitBuildInfo(writeFile, cancellationToken); } - /* @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: ts.CompilerOptions, { extension }: ts.ResolvedModuleFull): ts.DiagnosticMessage | undefined { - switch (extension) { - case ts.Extension.Ts: - case ts.Extension.Dts: - // These are always allowed. - return undefined; - case ts.Extension.Tsx: - return needJsx(); - case ts.Extension.Jsx: - return needJsx() || needAllowJs(); - case ts.Extension.Js: - return needAllowJs(); - case ts.Extension.Json: - return needResolveJsonModule(); - } + // 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 ts.Diagnostic[] = [ + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSemanticDiagnostics(sourceFile, cancellationToken) + ]; + + if (diagnostics.length === 0 && ts.getEmitDeclarations(program.getCompilerOptions())) { + diagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); + } - function needJsx() { - return options.jsx ? undefined : ts.Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set; - } - function needAllowJs() { - return ts.getAllowJSCompilerOption(options) || !ts.getStrictOptionValue(options, "noImplicitAny") ? undefined : ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type; - } - function needResolveJsonModule() { - return options.resolveJsonModule ? undefined : ts.Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used; - } + if (!diagnostics.length) + return undefined; + let emittedFiles: string[] | undefined; + if (!sourceFile && !ts.outFile(options)) { + const emitResult = program.emitBuildInfo(writeFile, cancellationToken); + if (emitResult.diagnostics) + diagnostics = [...diagnostics, ...emitResult.diagnostics]; + emittedFiles = emitResult.emittedFiles; } + return { diagnostics, sourceMaps: undefined, emittedFiles, emitSkipped: true }; +} - function getModuleNames({ imports, moduleAugmentations }: ts.SourceFile): string[] { - const res = imports.map(i => i.text); - for (const aug of moduleAugmentations) { - if (aug.kind === ts.SyntaxKind.StringLiteral) { - res.push(aug.text); - } - // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. +/*@internal*/ +export function filterSemanticDiagnostics(diagnostic: readonly ts.Diagnostic[], option: ts.CompilerOptions): readonly ts.Diagnostic[] { + return ts.filter(diagnostic, d => !d.skippedOn || !option[d.skippedOn]); +} + +/*@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?: ts.DiagnosticReporter; +} + +/* @internal */ +export function parseConfigHostFromCompilerHostLike(host: CompilerHostLike, directoryStructureHost: ts.DirectoryStructureHost = host): ts.ParseConfigFileHost { + return { + fileExists: f => directoryStructureHost.fileExists(f), + readDirectory(root, extensions, excludes, includes, depth) { + ts.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 || ts.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 ts.ProjectReference[] | undefined, getCommandLine: (ref: ts.ProjectReference, index: number) => ts.ParsedCommandLine | undefined, readFile: (path: string) => string | undefined) { + if (!projectReferences) + return ts.emptyArray; + let nodes: ts.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 = ts.outFile(resolvedRefOpts.options); + // Upstream project didn't have outFile set -- skip (error will have been issued earlier) + if (!out) + continue; + const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = ts.getOutputPathsForBundle(resolvedRefOpts.options, /*forceDtsPaths*/ true); + const node = ts.createInputFiles(readFile, jsFilePath!, sourceMapFilePath, declarationFilePath!, declarationMapPath, buildInfoPath); + (nodes || (nodes = [])).push(node); } - return res; } + return nodes || ts.emptyArray; +} +/** + * Returns the target config filename of a project reference. + * Note: The file might not exist. + */ +export function resolveProjectReferencePath(ref: ts.ProjectReference): ts.ResolvedConfigFileName; +/** @deprecated */ export function resolveProjectReferencePath(host: ResolveProjectReferencePathHost, ref: ts.ProjectReference): ts.ResolvedConfigFileName; +export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePathHost | ts.ProjectReference, ref?: ts.ProjectReference): ts.ResolvedConfigFileName { + const passedInRef = ref ? ref : hostOrRef as ts.ProjectReference; + return ts.resolveConfigFileProjectName(passedInRef.path); +} - /* @internal */ - export function getModuleNameStringLiteralAt({ imports, moduleAugmentations }: SourceFileImportsList, index: number): ts.StringLiteralLike { - if (index < imports.length) - return imports[index]; - let augIndex = imports.length; - for (const aug of moduleAugmentations) { - if (aug.kind === ts.SyntaxKind.StringLiteral) { - if (index === augIndex) - return aug; - augIndex++; - } - // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. +/* @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: ts.CompilerOptions, { extension }: ts.ResolvedModuleFull): ts.DiagnosticMessage | undefined { + switch (extension) { + case ts.Extension.Ts: + case ts.Extension.Dts: + // These are always allowed. + return undefined; + case ts.Extension.Tsx: + return needJsx(); + case ts.Extension.Jsx: + return needJsx() || needAllowJs(); + case ts.Extension.Js: + return needAllowJs(); + case ts.Extension.Json: + return needResolveJsonModule(); + } + + function needJsx() { + return options.jsx ? undefined : ts.Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set; + } + function needAllowJs() { + return ts.getAllowJSCompilerOption(options) || !ts.getStrictOptionValue(options, "noImplicitAny") ? undefined : ts.Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type; + } + function needResolveJsonModule() { + return options.resolveJsonModule ? undefined : ts.Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used; + } +} + +function getModuleNames({ imports, moduleAugmentations }: ts.SourceFile): string[] { + const res = imports.map(i => i.text); + for (const aug of moduleAugmentations) { + if (aug.kind === ts.SyntaxKind.StringLiteral) { + res.push(aug.text); } - ts.Debug.fail("should never ask for module name at index higher than possible module name"); + // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. } + return res; +} + +/* @internal */ +export function getModuleNameStringLiteralAt({ imports, moduleAugmentations }: SourceFileImportsList, index: number): ts.StringLiteralLike { + if (index < imports.length) + return imports[index]; + let augIndex = imports.length; + for (const aug of moduleAugmentations) { + if (aug.kind === ts.SyntaxKind.StringLiteral) { + if (index === augIndex) + return aug; + augIndex++; + } + // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. + } + ts.Debug.fail("should never ask for module name at index higher than possible module name"); +} } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 881d4dcf9b000..fc6f08abd24cd 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -1,930 +1,930 @@ /*@internal*/ namespace ts { - /** This is the cache of module/typedirectives resolution that can be retained across program */ - export interface ResolutionCache { - startRecordingFilesWithChangedResolutions(): void; - finishRecordingFilesWithChangedResolutions(): ts.Path[] | undefined; - resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile): (ts.ResolvedModuleFull | undefined)[]; - getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined; - resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"]): (ts.ResolvedTypeReferenceDirective | undefined)[]; +/** This is the cache of module/typedirectives resolution that can be retained across program */ +export interface ResolutionCache { + startRecordingFilesWithChangedResolutions(): void; + finishRecordingFilesWithChangedResolutions(): ts.Path[] | undefined; + resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile): (ts.ResolvedModuleFull | undefined)[]; + getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined; + resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"]): (ts.ResolvedTypeReferenceDirective | undefined)[]; - invalidateResolutionsOfFailedLookupLocations(): boolean; - invalidateResolutionOfFile(filePath: ts.Path): void; - removeResolutionsOfFile(filePath: ts.Path): void; - removeResolutionsFromProjectReferenceRedirects(filePath: ts.Path): void; - setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ts.ESMap): void; - createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): ts.HasInvalidatedResolution; - hasChangedAutomaticTypeDirectiveNames(): boolean; - isFileWithInvalidatedNonRelativeUnresolvedImports(path: ts.Path): boolean; + invalidateResolutionsOfFailedLookupLocations(): boolean; + invalidateResolutionOfFile(filePath: ts.Path): void; + removeResolutionsOfFile(filePath: ts.Path): void; + removeResolutionsFromProjectReferenceRedirects(filePath: ts.Path): void; + setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ts.ESMap): void; + createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): ts.HasInvalidatedResolution; + hasChangedAutomaticTypeDirectiveNames(): boolean; + isFileWithInvalidatedNonRelativeUnresolvedImports(path: ts.Path): boolean; - startCachingPerDirectoryResolution(): void; - finishCachingPerDirectoryResolution(): void; + startCachingPerDirectoryResolution(): void; + finishCachingPerDirectoryResolution(): void; - updateTypeRootsWatch(): void; - closeTypeRootsWatch(): void; + updateTypeRootsWatch(): void; + closeTypeRootsWatch(): void; - getModuleResolutionCache(): ts.ModuleResolutionCache; + getModuleResolutionCache(): ts.ModuleResolutionCache; - clear(): void; - } + clear(): void; +} - interface ResolutionWithFailedLookupLocations { - readonly failedLookupLocations: string[]; - isInvalidated?: boolean; - refCount?: number; - // Files that have this resolution using - files?: ts.Path[]; - } +interface ResolutionWithFailedLookupLocations { + readonly failedLookupLocations: string[]; + isInvalidated?: boolean; + refCount?: number; + // Files that have this resolution using + files?: ts.Path[]; +} - interface ResolutionWithResolvedFileName { - resolvedFileName: string | undefined; - packagetId?: ts.PackageId; - } +interface ResolutionWithResolvedFileName { + resolvedFileName: string | undefined; + packagetId?: ts.PackageId; +} - interface CachedResolvedModuleWithFailedLookupLocations extends ts.ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { - } +interface CachedResolvedModuleWithFailedLookupLocations extends ts.ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { +} - interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { - } +interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { +} - export interface ResolutionCacheHost extends ts.MinimalResolutionCacheHost { - toPath(fileName: string): ts.Path; - getCanonicalFileName: ts.GetCanonicalFileName; - getCompilationSettings(): ts.CompilerOptions; - watchDirectoryOfFailedLookupLocation(directory: string, cb: ts.DirectoryWatcherCallback, flags: ts.WatchDirectoryFlags): ts.FileWatcher; - onInvalidatedResolution(): void; - watchTypeRootsDirectory(directory: string, cb: ts.DirectoryWatcherCallback, flags: ts.WatchDirectoryFlags): ts.FileWatcher; - onChangedAutomaticTypeDirectiveNames(): void; - scheduleInvalidateResolutionsOfFailedLookupLocations(): void; - getCachedDirectoryStructureHost(): ts.CachedDirectoryStructureHost | undefined; - projectName?: string; - getGlobalCache?(): string | undefined; - globalCacheResolutionModuleName?(externalModuleName: string): string; - writeLog(s: string): void; - getCurrentProgram(): ts.Program | undefined; - fileIsOpen(filePath: ts.Path): boolean; - onDiscoveredSymlink?(): void; - } +export interface ResolutionCacheHost extends ts.MinimalResolutionCacheHost { + toPath(fileName: string): ts.Path; + getCanonicalFileName: ts.GetCanonicalFileName; + getCompilationSettings(): ts.CompilerOptions; + watchDirectoryOfFailedLookupLocation(directory: string, cb: ts.DirectoryWatcherCallback, flags: ts.WatchDirectoryFlags): ts.FileWatcher; + onInvalidatedResolution(): void; + watchTypeRootsDirectory(directory: string, cb: ts.DirectoryWatcherCallback, flags: ts.WatchDirectoryFlags): ts.FileWatcher; + onChangedAutomaticTypeDirectiveNames(): void; + scheduleInvalidateResolutionsOfFailedLookupLocations(): void; + getCachedDirectoryStructureHost(): ts.CachedDirectoryStructureHost | undefined; + projectName?: string; + getGlobalCache?(): string | undefined; + globalCacheResolutionModuleName?(externalModuleName: string): string; + writeLog(s: string): void; + getCurrentProgram(): ts.Program | undefined; + fileIsOpen(filePath: ts.Path): boolean; + onDiscoveredSymlink?(): void; +} - interface DirectoryWatchesOfFailedLookup { - /** watcher for the directory of failed lookup */ - watcher: ts.FileWatcher; - /** ref count keeping this directory watch alive */ - refCount: number; - /** is the directory watched being non recursive */ - nonRecursive?: boolean; - } +interface DirectoryWatchesOfFailedLookup { + /** watcher for the directory of failed lookup */ + watcher: ts.FileWatcher; + /** ref count keeping this directory watch alive */ + refCount: number; + /** is the directory watched being non recursive */ + nonRecursive?: boolean; +} + +interface DirectoryOfFailedLookupWatch { + dir: string; + dirPath: ts.Path; + nonRecursive?: boolean; +} - interface DirectoryOfFailedLookupWatch { - dir: string; - dirPath: ts.Path; - nonRecursive?: boolean; +export function removeIgnoredPath(path: ts.Path): ts.Path | undefined { + // Consider whole staging folder as if node_modules changed. + if (ts.endsWith(path, "/node_modules/.staging")) { + return ts.removeSuffix(path, "/.staging") as ts.Path; } - export function removeIgnoredPath(path: ts.Path): ts.Path | undefined { - // Consider whole staging folder as if node_modules changed. - if (ts.endsWith(path, "/node_modules/.staging")) { - return ts.removeSuffix(path, "/.staging") as ts.Path; - } + return ts.some(ts.ignoredPaths, searchPath => ts.stringContains(path, searchPath)) ? + undefined : + path; +} - return ts.some(ts.ignoredPaths, searchPath => ts.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: ts.Path) { + const rootLength = ts.getRootLength(dirPath); + if (dirPath.length === rootLength) { + // Ignore "/", "c:/" + return false; } - /** - * 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: ts.Path) { - const rootLength = ts.getRootLength(dirPath); - if (dirPath.length === rootLength) { - // Ignore "/", "c:/" - return false; - } + let nextDirectorySeparator = dirPath.indexOf(ts.directorySeparator, rootLength); + if (nextDirectorySeparator === -1) { + // ignore "/user", "c:/users" or "c:/folderAtRoot" + return false; + } - let nextDirectorySeparator = dirPath.indexOf(ts.directorySeparator, rootLength); + let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1); + const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== ts.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(ts.directorySeparator, nextDirectorySeparator + 1); if (nextDirectorySeparator === -1) { - // ignore "/user", "c:/users" or "c:/folderAtRoot" + // ignore "//vda1cs4850/c$/folderAtRoot" return false; } - let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1); - const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== ts.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(ts.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; - } + pathPartForUserCheck = dirPath.substring(rootLength + pathPartForUserCheck.length, nextDirectorySeparator + 1); + } - for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { - searchIndex = dirPath.indexOf(ts.directorySeparator, searchIndex) + 1; - if (searchIndex === 0) { - // Folder isnt at expected minimum levels - return false; - } - } + if (isNonDirectorySeparatorRoot && + pathPartForUserCheck.search(/users\//i) !== 0) { + // Paths like c:/folderAtRoot/subFolder are allowed return true; } - type GetResolutionWithResolvedFileName = (resolution: T) => R | undefined; - - export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { - let filesWithChangedSetOfUnresolvedImports: ts.Path[] | undefined; - let filesWithInvalidatedResolutions: ts.Set | undefined; - let filesWithInvalidatedNonRelativeUnresolvedImports: ts.ReadonlyESMap | undefined; - const nonRelativeExternalModuleResolutions = ts.createMultiMap(); - - const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = []; - const resolvedFileToResolution = ts.createMultiMap(); - - let hasChangedAutomaticTypeDirectiveNames = false; - let failedLookupChecks: ts.Path[] | undefined; - let startsWithPathChecks: ts.Set | undefined; - let isInDirectoryChecks: ts.Path[] | undefined; - const getCurrentDirectory = ts.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 = new ts.Map>(); - const perDirectoryResolvedModuleNames: ts.CacheWithRedirects> = ts.createCacheWithRedirects(); - const nonRelativeModuleNameCache: ts.CacheWithRedirects = ts.createCacheWithRedirects(); - const moduleResolutionCache = ts.createModuleResolutionCache(getCurrentDirectory(), resolutionHost.getCanonicalFileName, - /*options*/ undefined, perDirectoryResolvedModuleNames, nonRelativeModuleNameCache); - const resolvedTypeReferenceDirectives = new ts.Map>(); - const perDirectoryResolvedTypeReferenceDirectives: ts.CacheWithRedirects> = ts.createCacheWithRedirects(); - const typeReferenceDirectiveResolutionCache = ts.createTypeReferenceDirectiveResolutionCache(getCurrentDirectory(), resolutionHost.getCanonicalFileName, - /*options*/ undefined, moduleResolutionCache.getPackageJsonInfoCache(), perDirectoryResolvedTypeReferenceDirectives); - - /** - * 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 = [ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Json]; - const customFailedLookupPaths = new ts.Map(); - const directoryWatchesOfFailedLookups = new ts.Map(); - const rootDir = rootDirForResolution && ts.removeTrailingDirectorySeparator(ts.getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); - const rootPath = (rootDir && resolutionHost.toPath(rootDir)) as ts.Path; // TODO: GH#18217 - const rootSplitLength = rootPath !== undefined ? rootPath.split(ts.directorySeparator).length : 0; - - // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames - const typeRootsWatches = new ts.Map(); - - return { - getModuleResolutionCache: () => moduleResolutionCache, - 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, - hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames, - invalidateResolutionOfFile, - invalidateResolutionsOfFailedLookupLocations, - setFilesWithInvalidatedNonRelativeUnresolvedImports, - createHasInvalidatedResolution, - isFileWithInvalidatedNonRelativeUnresolvedImports, - updateTypeRootsWatch, - closeTypeRootsWatch, - clear - }; - - function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) { - return resolution.resolvedModule; - } - - function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) { - return resolution.resolvedTypeReferenceDirective; - } - - function isInDirectoryPath(dir: ts.Path | undefined, file: ts.Path) { - if (dir === undefined || file.length <= dir.length) { - return false; - } - return ts.startsWith(file, dir) && file[dir.length] === ts.directorySeparator; + for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { + searchIndex = dirPath.indexOf(ts.directorySeparator, searchIndex) + 1; + if (searchIndex === 0) { + // Folder isnt at expected minimum levels + return false; } + } + return true; +} - function clear() { - ts.clearMap(directoryWatchesOfFailedLookups, ts.closeFileWatcherOf); - customFailedLookupPaths.clear(); - nonRelativeExternalModuleResolutions.clear(); - closeTypeRootsWatch(); - resolvedModuleNames.clear(); - resolvedTypeReferenceDirectives.clear(); - resolvedFileToResolution.clear(); - resolutionsWithFailedLookups.length = 0; - failedLookupChecks = undefined; - startsWithPathChecks = undefined; - isInDirectoryChecks = undefined; - // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update - // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) - clearPerDirectoryResolutions(); - hasChangedAutomaticTypeDirectiveNames = false; - } +type GetResolutionWithResolvedFileName = (resolution: T) => R | undefined; + +export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { + let filesWithChangedSetOfUnresolvedImports: ts.Path[] | undefined; + let filesWithInvalidatedResolutions: ts.Set | undefined; + let filesWithInvalidatedNonRelativeUnresolvedImports: ts.ReadonlyESMap | undefined; + const nonRelativeExternalModuleResolutions = ts.createMultiMap(); + + const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = []; + const resolvedFileToResolution = ts.createMultiMap(); + + let hasChangedAutomaticTypeDirectiveNames = false; + let failedLookupChecks: ts.Path[] | undefined; + let startsWithPathChecks: ts.Set | undefined; + let isInDirectoryChecks: ts.Path[] | undefined; + const getCurrentDirectory = ts.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 = new ts.Map>(); + const perDirectoryResolvedModuleNames: ts.CacheWithRedirects> = ts.createCacheWithRedirects(); + const nonRelativeModuleNameCache: ts.CacheWithRedirects = ts.createCacheWithRedirects(); + const moduleResolutionCache = ts.createModuleResolutionCache(getCurrentDirectory(), resolutionHost.getCanonicalFileName, + /*options*/ undefined, perDirectoryResolvedModuleNames, nonRelativeModuleNameCache); + const resolvedTypeReferenceDirectives = new ts.Map>(); + const perDirectoryResolvedTypeReferenceDirectives: ts.CacheWithRedirects> = ts.createCacheWithRedirects(); + const typeReferenceDirectiveResolutionCache = ts.createTypeReferenceDirectiveResolutionCache(getCurrentDirectory(), resolutionHost.getCanonicalFileName, + /*options*/ undefined, moduleResolutionCache.getPackageJsonInfoCache(), perDirectoryResolvedTypeReferenceDirectives); - function startRecordingFilesWithChangedResolutions() { - filesWithChangedSetOfUnresolvedImports = []; - } + /** + * 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 = [ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Json]; + const customFailedLookupPaths = new ts.Map(); + const directoryWatchesOfFailedLookups = new ts.Map(); + const rootDir = rootDirForResolution && ts.removeTrailingDirectorySeparator(ts.getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); + const rootPath = (rootDir && resolutionHost.toPath(rootDir)) as ts.Path; // TODO: GH#18217 + const rootSplitLength = rootPath !== undefined ? rootPath.split(ts.directorySeparator).length : 0; + + // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames + const typeRootsWatches = new ts.Map(); + + return { + getModuleResolutionCache: () => moduleResolutionCache, + 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, + hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames, + invalidateResolutionOfFile, + invalidateResolutionsOfFailedLookupLocations, + setFilesWithInvalidatedNonRelativeUnresolvedImports, + createHasInvalidatedResolution, + isFileWithInvalidatedNonRelativeUnresolvedImports, + updateTypeRootsWatch, + closeTypeRootsWatch, + clear + }; + + function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) { + return resolution.resolvedModule; + } + + function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) { + return resolution.resolvedTypeReferenceDirective; + } - function finishRecordingFilesWithChangedResolutions() { - const collected = filesWithChangedSetOfUnresolvedImports; - filesWithChangedSetOfUnresolvedImports = undefined; - return collected; + function isInDirectoryPath(dir: ts.Path | undefined, file: ts.Path) { + if (dir === undefined || file.length <= dir.length) { + return false; } + return ts.startsWith(file, dir) && file[dir.length] === ts.directorySeparator; + } - function isFileWithInvalidatedNonRelativeUnresolvedImports(path: ts.Path): boolean { - if (!filesWithInvalidatedNonRelativeUnresolvedImports) { - return false; - } + function clear() { + ts.clearMap(directoryWatchesOfFailedLookups, ts.closeFileWatcherOf); + customFailedLookupPaths.clear(); + nonRelativeExternalModuleResolutions.clear(); + closeTypeRootsWatch(); + resolvedModuleNames.clear(); + resolvedTypeReferenceDirectives.clear(); + resolvedFileToResolution.clear(); + resolutionsWithFailedLookups.length = 0; + failedLookupChecks = undefined; + startsWithPathChecks = undefined; + isInDirectoryChecks = undefined; + // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update + // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) + clearPerDirectoryResolutions(); + hasChangedAutomaticTypeDirectiveNames = false; + } - // Invalidated if file has unresolved imports - const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path); - return !!value && !!value.length; - } + function startRecordingFilesWithChangedResolutions() { + filesWithChangedSetOfUnresolvedImports = []; + } - function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): ts.HasInvalidatedResolution { - // Ensure pending resolutions are applied - invalidateResolutionsOfFailedLookupLocations(); - if (forceAllFilesAsInvalidated) { - // Any file asked would have invalidated resolution - filesWithInvalidatedResolutions = undefined; - return ts.returnTrue; - } - const collected = filesWithInvalidatedResolutions; - filesWithInvalidatedResolutions = undefined; - return path => (!!collected && collected.has(path)) || - isFileWithInvalidatedNonRelativeUnresolvedImports(path); - } + function finishRecordingFilesWithChangedResolutions() { + const collected = filesWithChangedSetOfUnresolvedImports; + filesWithChangedSetOfUnresolvedImports = undefined; + return collected; + } - function clearPerDirectoryResolutions() { - moduleResolutionCache.clear(); - typeReferenceDirectiveResolutionCache.clear(); - nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions); - nonRelativeExternalModuleResolutions.clear(); + function isFileWithInvalidatedNonRelativeUnresolvedImports(path: ts.Path): boolean { + if (!filesWithInvalidatedNonRelativeUnresolvedImports) { + return false; } - function finishCachingPerDirectoryResolution() { - filesWithInvalidatedNonRelativeUnresolvedImports = undefined; - clearPerDirectoryResolutions(); - directoryWatchesOfFailedLookups.forEach((watcher, path) => { - if (watcher.refCount === 0) { - directoryWatchesOfFailedLookups.delete(path); - watcher.watcher.close(); - } - }); - hasChangedAutomaticTypeDirectiveNames = false; + // Invalidated if file has unresolved imports + const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path); + return !!value && !!value.length; + } + + function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): ts.HasInvalidatedResolution { + // Ensure pending resolutions are applied + invalidateResolutionsOfFailedLookupLocations(); + if (forceAllFilesAsInvalidated) { + // Any file asked would have invalidated resolution + filesWithInvalidatedResolutions = undefined; + return ts.returnTrue; } + const collected = filesWithInvalidatedResolutions; + filesWithInvalidatedResolutions = undefined; + return path => (!!collected && collected.has(path)) || + isFileWithInvalidatedNonRelativeUnresolvedImports(path); + } - function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, _containingSourceFile?: never, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): CachedResolvedModuleWithFailedLookupLocations { - const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference, mode); - // 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 clearPerDirectoryResolutions() { + moduleResolutionCache.clear(); + typeReferenceDirectiveResolutionCache.clear(); + nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions); + nonRelativeExternalModuleResolutions.clear(); + } - // otherwise try to load typings from @types - const globalCache = resolutionHost.getGlobalCache(); - if (globalCache !== undefined && !ts.isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && ts.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 } = ts.loadModuleFromGlobalCache(ts.Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName), resolutionHost.projectName, compilerOptions, host, globalCache, moduleResolutionCache); - if (resolvedModule) { - // Modify existing resolution so its saved in the directory cache as well - (primaryResult.resolvedModule as any) = resolvedModule; - primaryResult.failedLookupLocations.push(...failedLookupLocations); - return primaryResult; - } + function finishCachingPerDirectoryResolution() { + filesWithInvalidatedNonRelativeUnresolvedImports = undefined; + clearPerDirectoryResolutions(); + directoryWatchesOfFailedLookups.forEach((watcher, path) => { + if (watcher.refCount === 0) { + directoryWatchesOfFailedLookups.delete(path); + watcher.watcher.close(); } + }); + hasChangedAutomaticTypeDirectiveNames = false; + } - // Default return the result from the first pass + function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, _containingSourceFile?: never, mode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined): CachedResolvedModuleWithFailedLookupLocations { + const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference, mode); + // 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 resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, _containingSourceFile?: ts.SourceFile, resolutionMode?: ts.SourceFile["impliedNodeFormat"] | undefined): CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations { - return ts.resolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, options, host, redirectedReference, typeReferenceDirectiveResolutionCache, resolutionMode); - } - - interface ResolveNamesWithLocalCacheInput { - names: readonly string[] | readonly ts.FileReference[]; - containingFile: string; - redirectedReference: ts.ResolvedProjectReference | undefined; - cache: ts.ESMap>; - perDirectoryCacheWithRedirects: ts.CacheWithRedirects>; - loader: (name: string, containingFile: string, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) => T; - getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName; - shouldRetryResolution: (t: T) => boolean; - reusedNames?: readonly string[]; - logChanges?: boolean; - containingSourceFile?: ts.SourceFile; - containingSourceFileMode?: ts.SourceFile["impliedNodeFormat"]; - } - function resolveNamesWithLocalCache({ names, containingFile, redirectedReference, cache, perDirectoryCacheWithRedirects, loader, getResolutionWithResolvedFileName, shouldRetryResolution, reusedNames, logChanges, containingSourceFile, containingSourceFileMode }: ResolveNamesWithLocalCacheInput): (R | undefined)[] { - const path = resolutionHost.toPath(containingFile); - const resolutionsInFile = cache.get(path) || cache.set(path, ts.createModeAwareCache()).get(path)!; - const dirPath = ts.getDirectoryPath(path); - const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); - let perDirectoryResolution = perDirectoryCache.get(dirPath); - if (!perDirectoryResolution) { - perDirectoryResolution = ts.createModeAwareCache(); - perDirectoryCache.set(dirPath, perDirectoryResolution); + // otherwise try to load typings from @types + const globalCache = resolutionHost.getGlobalCache(); + if (globalCache !== undefined && !ts.isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && ts.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 } = ts.loadModuleFromGlobalCache(ts.Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName), resolutionHost.projectName, compilerOptions, host, globalCache, moduleResolutionCache); + if (resolvedModule) { + // Modify existing resolution so its saved in the directory cache as well + (primaryResult.resolvedModule as any) = resolvedModule; + primaryResult.failedLookupLocations.push(...failedLookupLocations); + return primaryResult; } - const resolvedModules: (R | undefined)[] = []; - const compilerOptions = resolutionHost.getCompilationSettings(); - const hasInvalidatedNonRelativeUnresolvedImport = logChanges && isFileWithInvalidatedNonRelativeUnresolvedImports(path); - - // All the resolutions in this file are invalidated if this file wasn't 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 = ts.createModeAwareCache(); - let i = 0; - for (const entry of names) { - const name = ts.isString(entry) ? entry : entry.fileName.toLowerCase(); - // Imports supply a `containingSourceFile` but no `containingSourceFileMode` - it would be redundant - // they require calculating the mode for a given import from it's position in the resolution table, since a given - // import's syntax may override the file's default mode. - // Type references instead supply a `containingSourceFileMode` and a non-string entry which contains - // a default file mode override if applicable. - const mode = !ts.isString(entry) ? ts.getModeForFileReference(entry, containingSourceFileMode) : - containingSourceFile ? ts.getModeForResolutionAtIndex(containingSourceFile, i) : undefined; - i++; - let resolution = resolutionsInFile.get(name, mode); - // Resolution is valid if it is present and not invalidated - if (!seenNamesInFile.has(name, mode) && - unmatchedRedirects || !resolution || resolution.isInvalidated || - // If the name is unresolved import that was invalidated, recalculate - (hasInvalidatedNonRelativeUnresolvedImport && !ts.isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) { - const existingResolution = resolution; - const resolutionInDirectory = perDirectoryResolution.get(name, mode); - if (resolutionInDirectory) { - resolution = resolutionInDirectory; - const host = resolutionHost.getCompilerHost?.() || resolutionHost; - if (ts.isTraceEnabled(compilerOptions, host)) { - const resolved = getResolutionWithResolvedFileName(resolution); - ts.trace(host, loader === resolveModuleName as unknown ? - resolved?.resolvedFileName ? - resolved.packagetId ? - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4 : - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3 : - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_not_resolved : - resolved?.resolvedFileName ? - resolved.packagetId ? - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4 : - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3 : - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_not_resolved, name, containingFile, ts.getDirectoryPath(containingFile), resolved?.resolvedFileName, resolved?.packagetId && ts.packageIdToString(resolved.packagetId)); - } - } - else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference, containingSourceFile, mode); - perDirectoryResolution.set(name, mode, resolution); - if (resolutionHost.onDiscoveredSymlink && resolutionIsSymlink(resolution)) { - resolutionHost.onDiscoveredSymlink(); - } - } - resolutionsInFile.set(name, mode, resolution); - watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution, path, getResolutionWithResolvedFileName); - if (existingResolution) { - stopWatchFailedLookupLocationOfResolution(existingResolution, path, getResolutionWithResolvedFileName); - } + } - if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { - filesWithChangedSetOfUnresolvedImports.push(path); - // reset log changes to avoid recording the same file multiple times - logChanges = false; - } - } - else { + // Default return the result from the first pass + return primaryResult; + } + + function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, _containingSourceFile?: ts.SourceFile, resolutionMode?: ts.SourceFile["impliedNodeFormat"] | undefined): CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations { + return ts.resolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, options, host, redirectedReference, typeReferenceDirectiveResolutionCache, resolutionMode); + } + + interface ResolveNamesWithLocalCacheInput { + names: readonly string[] | readonly ts.FileReference[]; + containingFile: string; + redirectedReference: ts.ResolvedProjectReference | undefined; + cache: ts.ESMap>; + perDirectoryCacheWithRedirects: ts.CacheWithRedirects>; + loader: (name: string, containingFile: string, options: ts.CompilerOptions, host: ts.ModuleResolutionHost, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined) => T; + getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName; + shouldRetryResolution: (t: T) => boolean; + reusedNames?: readonly string[]; + logChanges?: boolean; + containingSourceFile?: ts.SourceFile; + containingSourceFileMode?: ts.SourceFile["impliedNodeFormat"]; + } + function resolveNamesWithLocalCache({ names, containingFile, redirectedReference, cache, perDirectoryCacheWithRedirects, loader, getResolutionWithResolvedFileName, shouldRetryResolution, reusedNames, logChanges, containingSourceFile, containingSourceFileMode }: ResolveNamesWithLocalCacheInput): (R | undefined)[] { + const path = resolutionHost.toPath(containingFile); + const resolutionsInFile = cache.get(path) || cache.set(path, ts.createModeAwareCache()).get(path)!; + const dirPath = ts.getDirectoryPath(path); + const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); + let perDirectoryResolution = perDirectoryCache.get(dirPath); + if (!perDirectoryResolution) { + perDirectoryResolution = ts.createModeAwareCache(); + 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 wasn't 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 = ts.createModeAwareCache(); + let i = 0; + for (const entry of names) { + const name = ts.isString(entry) ? entry : entry.fileName.toLowerCase(); + // Imports supply a `containingSourceFile` but no `containingSourceFileMode` - it would be redundant + // they require calculating the mode for a given import from it's position in the resolution table, since a given + // import's syntax may override the file's default mode. + // Type references instead supply a `containingSourceFileMode` and a non-string entry which contains + // a default file mode override if applicable. + const mode = !ts.isString(entry) ? ts.getModeForFileReference(entry, containingSourceFileMode) : + containingSourceFile ? ts.getModeForResolutionAtIndex(containingSourceFile, i) : undefined; + i++; + let resolution = resolutionsInFile.get(name, mode); + // Resolution is valid if it is present and not invalidated + if (!seenNamesInFile.has(name, mode) && + unmatchedRedirects || !resolution || resolution.isInvalidated || + // If the name is unresolved import that was invalidated, recalculate + (hasInvalidatedNonRelativeUnresolvedImport && !ts.isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) { + const existingResolution = resolution; + const resolutionInDirectory = perDirectoryResolution.get(name, mode); + if (resolutionInDirectory) { + resolution = resolutionInDirectory; const host = resolutionHost.getCompilerHost?.() || resolutionHost; - if (ts.isTraceEnabled(compilerOptions, host) && !seenNamesInFile.has(name, mode)) { + if (ts.isTraceEnabled(compilerOptions, host)) { const resolved = getResolutionWithResolvedFileName(resolution); ts.trace(host, loader === resolveModuleName as unknown ? resolved?.resolvedFileName ? resolved.packagetId ? - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2 : - ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_not_resolved : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4 : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3 : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_not_resolved : resolved?.resolvedFileName ? resolved.packagetId ? - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2 : - ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_not_resolved, name, containingFile, resolved?.resolvedFileName, resolved?.packagetId && ts.packageIdToString(resolved.packagetId)); + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4 : + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3 : + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_not_resolved, name, containingFile, ts.getDirectoryPath(containingFile), resolved?.resolvedFileName, resolved?.packagetId && ts.packageIdToString(resolved.packagetId)); } } - ts.Debug.assert(resolution !== undefined && !resolution.isInvalidated); - seenNamesInFile.set(name, mode, true); - resolvedModules.push(getResolutionWithResolvedFileName(resolution)); - } - - // Stop watching and remove the unused name - resolutionsInFile.forEach((resolution, name, mode) => { - if (!seenNamesInFile.has(name, mode) && !ts.contains(reusedNames, name)) { - stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName); - resolutionsInFile.delete(name, mode); - } - }); - - return resolvedModules; - - function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean { - if (oldResolution === newResolution) { - return true; + else { + resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference, containingSourceFile, mode); + perDirectoryResolution.set(name, mode, resolution); + if (resolutionHost.onDiscoveredSymlink && resolutionIsSymlink(resolution)) { + resolutionHost.onDiscoveredSymlink(); + } } - if (!oldResolution || !newResolution) { - return false; + resolutionsInFile.set(name, mode, resolution); + watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution, path, getResolutionWithResolvedFileName); + if (existingResolution) { + stopWatchFailedLookupLocationOfResolution(existingResolution, path, getResolutionWithResolvedFileName); } - const oldResult = getResolutionWithResolvedFileName(oldResolution); - const newResult = getResolutionWithResolvedFileName(newResolution); - if (oldResult === newResult) { - return true; + + if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { + filesWithChangedSetOfUnresolvedImports.push(path); + // reset log changes to avoid recording the same file multiple times + logChanges = false; } - if (!oldResult || !newResult) { - return false; + } + else { + const host = resolutionHost.getCompilerHost?.() || resolutionHost; + if (ts.isTraceEnabled(compilerOptions, host) && !seenNamesInFile.has(name, mode)) { + const resolved = getResolutionWithResolvedFileName(resolution); + ts.trace(host, loader === resolveModuleName as unknown ? + resolved?.resolvedFileName ? + resolved.packagetId ? + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2 : + ts.Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_not_resolved : + resolved?.resolvedFileName ? + resolved.packagetId ? + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 : + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2 : + ts.Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_not_resolved, name, containingFile, resolved?.resolvedFileName, resolved?.packagetId && ts.packageIdToString(resolved.packagetId)); } - return oldResult.resolvedFileName === newResult.resolvedFileName; } + ts.Debug.assert(resolution !== undefined && !resolution.isInvalidated); + seenNamesInFile.set(name, mode, true); + resolvedModules.push(getResolutionWithResolvedFileName(resolution)); } - function resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"]): (ts.ResolvedTypeReferenceDirective | undefined)[] { - return resolveNamesWithLocalCache({ - names: typeDirectiveNames, - containingFile, - redirectedReference, - cache: resolvedTypeReferenceDirectives, - perDirectoryCacheWithRedirects: perDirectoryResolvedTypeReferenceDirectives, - loader: resolveTypeReferenceDirective, - getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective, - shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined, - containingSourceFileMode: containingFileMode - }); - } + // Stop watching and remove the unused name + resolutionsInFile.forEach((resolution, name, mode) => { + if (!seenNamesInFile.has(name, mode) && !ts.contains(reusedNames, name)) { + stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName); + resolutionsInFile.delete(name, mode); + } + }); - function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile): (ts.ResolvedModuleFull | undefined)[] { - return resolveNamesWithLocalCache({ - names: moduleNames, - containingFile, - redirectedReference, - cache: resolvedModuleNames, - perDirectoryCacheWithRedirects: perDirectoryResolvedModuleNames, - loader: resolveModuleName, - getResolutionWithResolvedFileName: getResolvedModule, - shouldRetryResolution: resolution => !resolution.resolvedModule || !ts.resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), - reusedNames, - logChanges: logChangesWhenResolvingModule, - containingSourceFile, - }); - } + return resolvedModules; - function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined { - const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile)); - if (!cache) - return undefined; - return cache.get(moduleName, resolutionMode); + function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean { + if (oldResolution === newResolution) { + return true; + } + if (!oldResolution || !newResolution) { + return false; + } + const oldResult = getResolutionWithResolvedFileName(oldResolution); + const newResult = getResolutionWithResolvedFileName(newResolution); + if (oldResult === newResult) { + return true; + } + if (!oldResult || !newResult) { + return false; + } + return oldResult.resolvedFileName === newResult.resolvedFileName; } + } - function isNodeModulesAtTypesDirectory(dirPath: ts.Path) { - return ts.endsWith(dirPath, "/node_modules/@types"); - } + function resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string, redirectedReference?: ts.ResolvedProjectReference, containingFileMode?: ts.SourceFile["impliedNodeFormat"]): (ts.ResolvedTypeReferenceDirective | undefined)[] { + return resolveNamesWithLocalCache({ + names: typeDirectiveNames, + containingFile, + redirectedReference, + cache: resolvedTypeReferenceDirectives, + perDirectoryCacheWithRedirects: perDirectoryResolvedTypeReferenceDirectives, + loader: resolveTypeReferenceDirective, + getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective, + shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined, + containingSourceFileMode: containingFileMode + }); + } - function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: ts.Path): DirectoryOfFailedLookupWatch | undefined { - if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { - // Ensure failed look up is normalized path - failedLookupLocation = ts.isRootedDiskPath(failedLookupLocation) ? ts.normalizePath(failedLookupLocation) : ts.getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); - const failedLookupPathSplit = failedLookupLocationPath.split(ts.directorySeparator); - const failedLookupSplit = failedLookupLocation.split(ts.directorySeparator); - ts.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(ts.directorySeparator), - dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(ts.directorySeparator) as ts.Path - }; - } - else { - // Always watch root directory non recursively - return { - dir: rootDir!, - dirPath: rootPath, - nonRecursive: false - }; - } - } + function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ts.ResolvedProjectReference, containingSourceFile?: ts.SourceFile): (ts.ResolvedModuleFull | undefined)[] { + return resolveNamesWithLocalCache({ + names: moduleNames, + containingFile, + redirectedReference, + cache: resolvedModuleNames, + perDirectoryCacheWithRedirects: perDirectoryResolvedModuleNames, + loader: resolveModuleName, + getResolutionWithResolvedFileName: getResolvedModule, + shouldRetryResolution: resolution => !resolution.resolvedModule || !ts.resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), + reusedNames, + logChanges: logChangesWhenResolvingModule, + containingSourceFile, + }); + } - return getDirectoryToWatchFromFailedLookupLocationDirectory(ts.getDirectoryPath(ts.getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), ts.getDirectoryPath(failedLookupLocationPath)); - } + function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined { + const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile)); + if (!cache) + return undefined; + return cache.get(moduleName, resolutionMode); + } - function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: ts.Path): DirectoryOfFailedLookupWatch | undefined { - // If directory path contains node module, get the most parent node_modules directory for watching - while (ts.pathContainsNodeModules(dirPath)) { - dir = ts.getDirectoryPath(dir); - dirPath = ts.getDirectoryPath(dirPath); - } + function isNodeModulesAtTypesDirectory(dirPath: ts.Path) { + return ts.endsWith(dirPath, "/node_modules/@types"); + } - // If the directory is node_modules use it to watch, always watch it recursively - if (ts.isNodeModulesDirectory(dirPath)) { - return canWatchDirectory(ts.getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; + function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: ts.Path): DirectoryOfFailedLookupWatch | undefined { + if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { + // Ensure failed look up is normalized path + failedLookupLocation = ts.isRootedDiskPath(failedLookupLocation) ? ts.normalizePath(failedLookupLocation) : ts.getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); + const failedLookupPathSplit = failedLookupLocationPath.split(ts.directorySeparator); + const failedLookupSplit = failedLookupLocation.split(ts.directorySeparator); + ts.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(ts.directorySeparator), + dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(ts.directorySeparator) as ts.Path + }; } - - let nonRecursive = true; - // Use some ancestor of the root directory - let subDirectoryPath: ts.Path | undefined, subDirectory: string | undefined; - if (rootPath !== undefined) { - while (!isInDirectoryPath(dirPath, rootPath)) { - const parentPath = ts.getDirectoryPath(dirPath); - if (parentPath === dirPath) { - break; - } - nonRecursive = false; - subDirectoryPath = dirPath; - subDirectory = dir; - dirPath = parentPath; - dir = ts.getDirectoryPath(dir); - } + else { + // Always watch root directory non recursively + return { + dir: rootDir!, + dirPath: rootPath, + nonRecursive: false + }; } - - return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; } - function isPathWithDefaultFailedLookupExtension(path: ts.Path) { - return ts.fileExtensionIsOneOf(path, failedLookupDefaultExtensions); + return getDirectoryToWatchFromFailedLookupLocationDirectory(ts.getDirectoryPath(ts.getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), ts.getDirectoryPath(failedLookupLocationPath)); + } + + function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: ts.Path): DirectoryOfFailedLookupWatch | undefined { + // If directory path contains node module, get the most parent node_modules directory for watching + while (ts.pathContainsNodeModules(dirPath)) { + dir = ts.getDirectoryPath(dir); + dirPath = ts.getDirectoryPath(dirPath); } - function watchFailedLookupLocationsOfExternalModuleResolutions(name: string, resolution: T, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { - if (resolution.refCount) { - resolution.refCount++; - ts.Debug.assertIsDefined(resolution.files); - } - else { - resolution.refCount = 1; - ts.Debug.assert(ts.length(resolution.files) === 0); // This resolution shouldnt be referenced by any file yet - if (ts.isExternalModuleNameRelative(name)) { - watchFailedLookupLocationOfResolution(resolution); - } - else { - nonRelativeExternalModuleResolutions.add(name, resolution); - } - const resolved = getResolutionWithResolvedFileName(resolution); - if (resolved && resolved.resolvedFileName) { - resolvedFileToResolution.add(resolutionHost.toPath(resolved.resolvedFileName), resolution); - } - } - (resolution.files || (resolution.files = [])).push(filePath); - } - - function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { - ts.Debug.assert(!!resolution.refCount); - - const { failedLookupLocations } = resolution; - if (!failedLookupLocations.length) - return; - resolutionsWithFailedLookups.push(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) { - ts.Debug.assert(!nonRecursive); - setAtRoot = true; - } - else { - setDirectoryWatcher(dir, dirPath, nonRecursive); - } - } - } - if (setAtRoot) { - // This is always non recursive - setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 - } + // If the directory is node_modules use it to watch, always watch it recursively + if (ts.isNodeModulesDirectory(dirPath)) { + return canWatchDirectory(ts.getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; } - function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { - const program = resolutionHost.getCurrentProgram(); - if (!program || !program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name)) { - resolutions.forEach(watchFailedLookupLocationOfResolution); + let nonRecursive = true; + // Use some ancestor of the root directory + let subDirectoryPath: ts.Path | undefined, subDirectory: string | undefined; + if (rootPath !== undefined) { + while (!isInDirectoryPath(dirPath, rootPath)) { + const parentPath = ts.getDirectoryPath(dirPath); + if (parentPath === dirPath) { + break; + } + nonRecursive = false; + subDirectoryPath = dirPath; + subDirectory = dir; + dirPath = parentPath; + dir = ts.getDirectoryPath(dir); } } - function setDirectoryWatcher(dir: string, dirPath: ts.Path, nonRecursive?: boolean) { - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); - if (dirWatcher) { - ts.Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); - dirWatcher.refCount++; + return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; + } + + function isPathWithDefaultFailedLookupExtension(path: ts.Path) { + return ts.fileExtensionIsOneOf(path, failedLookupDefaultExtensions); + } + function watchFailedLookupLocationsOfExternalModuleResolutions(name: string, resolution: T, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { + if (resolution.refCount) { + resolution.refCount++; + ts.Debug.assertIsDefined(resolution.files); + } + else { + resolution.refCount = 1; + ts.Debug.assert(ts.length(resolution.files) === 0); // This resolution shouldnt be referenced by any file yet + if (ts.isExternalModuleNameRelative(name)) { + watchFailedLookupLocationOfResolution(resolution); } else { - directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); - } - } - - function stopWatchFailedLookupLocationOfResolution(resolution: T, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { - ts.unorderedRemoveItem(ts.Debug.checkDefined(resolution.files), filePath); - resolution.refCount!--; - if (resolution.refCount) { - return; + nonRelativeExternalModuleResolutions.add(name, resolution); } const resolved = getResolutionWithResolvedFileName(resolution); if (resolved && resolved.resolvedFileName) { - resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution); + resolvedFileToResolution.add(resolutionHost.toPath(resolved.resolvedFileName), resolution); } + } + (resolution.files || (resolution.files = [])).push(filePath); + } - if (!ts.unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) { - // If not watching failed lookups, it wont be there in resolutionsWithFailedLookups - return; + function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { + ts.Debug.assert(!!resolution.refCount); + + const { failedLookupLocations } = resolution; + if (!failedLookupLocations.length) + return; + resolutionsWithFailedLookups.push(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) { + ts.Debug.assert(!nonRecursive); + setAtRoot = true; + } + else { + setDirectoryWatcher(dir, dirPath, nonRecursive); + } } + } - 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 { - ts.Debug.assert(refCount > 1); - customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); - } - } + if (setAtRoot) { + // This is always non recursive + setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 + } + } + + function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { + const program = resolutionHost.getCurrentProgram(); + if (!program || !program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name)) { + resolutions.forEach(watchFailedLookupLocationOfResolution); + } + } + + function setDirectoryWatcher(dir: string, dirPath: ts.Path, nonRecursive?: boolean) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + if (dirWatcher) { + ts.Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); + dirWatcher.refCount++; + } + else { + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); + } + } - if (dirPath === rootPath) { - removeAtRoot = true; + function stopWatchFailedLookupLocationOfResolution(resolution: T, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { + ts.unorderedRemoveItem(ts.Debug.checkDefined(resolution.files), filePath); + resolution.refCount!--; + if (resolution.refCount) { + return; + } + const resolved = getResolutionWithResolvedFileName(resolution); + if (resolved && resolved.resolvedFileName) { + resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution); + } + + if (!ts.unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) { + // If not watching failed lookups, it wont be there in resolutionsWithFailedLookups + 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); } else { - removeDirectoryWatcher(dirPath); + ts.Debug.assert(refCount > 1); + customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); } } - } - if (removeAtRoot) { - removeDirectoryWatcher(rootPath); + + if (dirPath === rootPath) { + removeAtRoot = true; + } + else { + removeDirectoryWatcher(dirPath); + } } } - - 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--; + if (removeAtRoot) { + removeDirectoryWatcher(rootPath); } + } - function createDirectoryWatcher(directory: string, dirPath: ts.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 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--; + } - scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); - }, nonRecursive ? ts.WatchDirectoryFlags.None : ts.WatchDirectoryFlags.Recursive); - } - function removeResolutionsOfFileFromCache(cache: ts.ESMap>, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { - // Deleted file, stop watching failed lookups for all the resolutions in the file - const resolutions = cache.get(filePath); - if (resolutions) { - resolutions.forEach(resolution => stopWatchFailedLookupLocationOfResolution(resolution, filePath, getResolutionWithResolvedFileName)); - cache.delete(filePath); + function createDirectoryWatcher(directory: string, dirPath: ts.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); } + + scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); + }, nonRecursive ? ts.WatchDirectoryFlags.None : ts.WatchDirectoryFlags.Recursive); + } + function removeResolutionsOfFileFromCache(cache: ts.ESMap>, filePath: ts.Path, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { + // Deleted file, stop watching failed lookups for all the resolutions in the file + const resolutions = cache.get(filePath); + if (resolutions) { + resolutions.forEach(resolution => stopWatchFailedLookupLocationOfResolution(resolution, filePath, getResolutionWithResolvedFileName)); + cache.delete(filePath); } + } - function removeResolutionsFromProjectReferenceRedirects(filePath: ts.Path) { - if (!ts.fileExtensionIs(filePath, ts.Extension.Json)) - return; + function removeResolutionsFromProjectReferenceRedirects(filePath: ts.Path) { + if (!ts.fileExtensionIs(filePath, ts.Extension.Json)) + return; - const program = resolutionHost.getCurrentProgram(); - if (!program) - 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; + // 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))); - } + // 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: ts.Path) { - removeResolutionsOfFileFromCache(resolvedModuleNames, filePath, getResolvedModule); - removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective); - } + function removeResolutionsOfFile(filePath: ts.Path) { + removeResolutionsOfFileFromCache(resolvedModuleNames, filePath, getResolvedModule); + removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective); + } - function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) { - if (!resolutions) - return false; - let invalidated = false; - for (const resolution of resolutions) { - if (resolution.isInvalidated || !canInvalidate(resolution)) - continue; - resolution.isInvalidated = invalidated = true; - for (const containingFilePath of ts.Debug.checkDefined(resolution.files)) { - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = new ts.Set())).add(containingFilePath); - // When its a file with inferred types resolution, invalidate type reference directive resolution - hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || ts.endsWith(containingFilePath, ts.inferredTypesContainingFile); - } - } - return invalidated; - } + function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) { + if (!resolutions) + return false; + let invalidated = false; + for (const resolution of resolutions) { + if (resolution.isInvalidated || !canInvalidate(resolution)) + continue; + resolution.isInvalidated = invalidated = true; + for (const containingFilePath of ts.Debug.checkDefined(resolution.files)) { + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = new ts.Set())).add(containingFilePath); + // When its a file with inferred types resolution, invalidate type reference directive resolution + hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || ts.endsWith(containingFilePath, ts.inferredTypesContainingFile); + } + } + return invalidated; + } - function invalidateResolutionOfFile(filePath: ts.Path) { - removeResolutionsOfFile(filePath); - // Resolution is invalidated if the resulting file name is same as the deleted file path - const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; - if (invalidateResolutions(resolvedFileToResolution.get(filePath), ts.returnTrue) && - hasChangedAutomaticTypeDirectiveNames && - !prevHasChangedAutomaticTypeDirectiveNames) { - resolutionHost.onChangedAutomaticTypeDirectiveNames(); - } + function invalidateResolutionOfFile(filePath: ts.Path) { + removeResolutionsOfFile(filePath); + // Resolution is invalidated if the resulting file name is same as the deleted file path + const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; + if (invalidateResolutions(resolvedFileToResolution.get(filePath), ts.returnTrue) && + hasChangedAutomaticTypeDirectiveNames && + !prevHasChangedAutomaticTypeDirectiveNames) { + resolutionHost.onChangedAutomaticTypeDirectiveNames(); } + } + + function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ts.ReadonlyESMap) { + ts.Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); + filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; + } - function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ts.ReadonlyESMap) { - ts.Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); - filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; + function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: ts.Path, isCreatingWatchedDirectory: boolean) { + if (isCreatingWatchedDirectory) { + // Watching directory is created + // Invalidate any resolution has failed lookup in this directory + (isInDirectoryChecks ||= []).push(fileOrDirectoryPath); } + 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; + } - function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: ts.Path, isCreatingWatchedDirectory: boolean) { - if (isCreatingWatchedDirectory) { - // Watching directory is created - // Invalidate any resolution has failed lookup in this directory - (isInDirectoryChecks ||= []).push(fileOrDirectoryPath); + // 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 = ts.getDirectoryPath(fileOrDirectoryPath); + if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || ts.isNodeModulesDirectory(fileOrDirectoryPath) || + isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || ts.isNodeModulesDirectory(dirOfFileOrDirectory)) { + // Invalidate any resolution from this directory + (failedLookupChecks ||= []).push(fileOrDirectoryPath); + (startsWithPathChecks ||= new ts.Set()).add(fileOrDirectoryPath); } else { - // If something to do with folder/file starting with "." in node_modules folder, skip it - const updatedPath = removeIgnoredPath(fileOrDirectoryPath); - if (!updatedPath) + if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { return false; - fileOrDirectoryPath = updatedPath; - - // prevent saving an open file from over-eagerly triggering invalidation - if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) { + } + // Ignore emits from the program + if (ts.isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { return false; } + // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created + (failedLookupChecks ||= []).push(fileOrDirectoryPath); - // 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 = ts.getDirectoryPath(fileOrDirectoryPath); - if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || ts.isNodeModulesDirectory(fileOrDirectoryPath) || - isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || ts.isNodeModulesDirectory(dirOfFileOrDirectory)) { - // Invalidate any resolution from this directory - (failedLookupChecks ||= []).push(fileOrDirectoryPath); - (startsWithPathChecks ||= new ts.Set()).add(fileOrDirectoryPath); - } - else { - if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { - return false; - } - // Ignore emits from the program - if (ts.isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { - return false; - } - // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created - (failedLookupChecks ||= []).push(fileOrDirectoryPath); - - // If the invalidated file is from a node_modules package, invalidate everything else - // in the package since we might not get notifications for other files in the package. - // This hardens our logic against unreliable file watchers. - const packagePath = ts.parseNodeModuleFromPath(fileOrDirectoryPath); - if (packagePath) - (startsWithPathChecks ||= new ts.Set()).add(packagePath as ts.Path); - } + // If the invalidated file is from a node_modules package, invalidate everything else + // in the package since we might not get notifications for other files in the package. + // This hardens our logic against unreliable file watchers. + const packagePath = ts.parseNodeModuleFromPath(fileOrDirectoryPath); + if (packagePath) + (startsWithPathChecks ||= new ts.Set()).add(packagePath as ts.Path); } - resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations(); } + resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations(); + } - function invalidateResolutionsOfFailedLookupLocations() { - if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks) { - return false; - } - - const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution); - failedLookupChecks = undefined; - startsWithPathChecks = undefined; - isInDirectoryChecks = undefined; - return invalidated; + function invalidateResolutionsOfFailedLookupLocations() { + if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks) { + return false; } - function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) { - return resolution.failedLookupLocations.some(location => { - const locationPath = resolutionHost.toPath(location); - return ts.contains(failedLookupChecks, locationPath) || - ts.firstDefinedIterator(startsWithPathChecks?.keys() || ts.emptyIterator, fileOrDirectoryPath => ts.startsWith(locationPath, fileOrDirectoryPath) ? true : undefined) || - isInDirectoryChecks?.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath)); - }); - } + const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution); + failedLookupChecks = undefined; + startsWithPathChecks = undefined; + isInDirectoryChecks = undefined; + return invalidated; + } - function closeTypeRootsWatch() { - ts.clearMap(typeRootsWatches, ts.closeFileWatcher); - } + function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) { + return resolution.failedLookupLocations.some(location => { + const locationPath = resolutionHost.toPath(location); + return ts.contains(failedLookupChecks, locationPath) || + ts.firstDefinedIterator(startsWithPathChecks?.keys() || ts.emptyIterator, fileOrDirectoryPath => ts.startsWith(locationPath, fileOrDirectoryPath) ? true : undefined) || + isInDirectoryChecks?.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath)); + }); + } - function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: ts.Path): ts.Path | undefined { - if (isInDirectoryPath(rootPath, typeRootPath)) { - return rootPath; - } - const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); - return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; - } + function closeTypeRootsWatch() { + ts.clearMap(typeRootsWatches, ts.closeFileWatcher); + } - function createTypeRootsWatch(typeRootPath: ts.Path, typeRoot: string): ts.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); - } + function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: ts.Path): ts.Path | undefined { + if (isInDirectoryPath(rootPath, typeRootPath)) { + return rootPath; + } + const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); + return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; + } - // 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 - hasChangedAutomaticTypeDirectiveNames = true; - 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) { - scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); - } - }, ts.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 createTypeRootsWatch(typeRootPath: ts.Path, typeRoot: string): ts.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 + hasChangedAutomaticTypeDirectiveNames = true; + 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) { + scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); } + }, ts.WatchDirectoryFlags.Recursive); + } - // 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 = ts.getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); - if (typeRoots) { - ts.mutateMap(typeRootsWatches, ts.arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), { - createNewValue: createTypeRootsWatch, - onDeleteValue: ts.closeFileWatcher - }); - } - else { - closeTypeRootsWatch(); - } + /** + * 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; } - /** - * 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 = ts.getDirectoryPath(ts.getDirectoryPath(nodeTypesDirectory)); - const dirPath = resolutionHost.toPath(dir); - return dirPath === rootPath || canWatchDirectory(dirPath); + // 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 = ts.getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); + if (typeRoots) { + ts.mutateMap(typeRootsWatches, ts.arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), { + createNewValue: createTypeRootsWatch, + onDeleteValue: ts.closeFileWatcher + }); + } + else { + closeTypeRootsWatch(); } } - function resolutionIsSymlink(resolution: ResolutionWithFailedLookupLocations) { - return !!((resolution as ts.ResolvedModuleWithFailedLookupLocations).resolvedModule?.originalPath || - (resolution as ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations).resolvedTypeReferenceDirective?.originalPath); + /** + * 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 = ts.getDirectoryPath(ts.getDirectoryPath(nodeTypesDirectory)); + const dirPath = resolutionHost.toPath(dir); + return dirPath === rootPath || canWatchDirectory(dirPath); } } + +function resolutionIsSymlink(resolution: ResolutionWithFailedLookupLocations) { + return !!((resolution as ts.ResolvedModuleWithFailedLookupLocations).resolvedModule?.originalPath || + (resolution as ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations).resolvedTypeReferenceDirective?.originalPath); +} +} diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index ea468b4b489c2..799e78ccd346c 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1,609 +1,809 @@ namespace ts { - export type ErrorCallback = (message: ts.DiagnosticMessage, length: number) => void; +export type ErrorCallback = (message: ts.DiagnosticMessage, length: number) => void; - /* @internal */ - export function tokenIsIdentifierOrKeyword(token: ts.SyntaxKind): boolean { - return token >= ts.SyntaxKind.Identifier; - } - - /* @internal */ - export function tokenIsIdentifierOrKeywordOrGreaterThan(token: ts.SyntaxKind): boolean { - return token === ts.SyntaxKind.GreaterThanToken || tokenIsIdentifierOrKeyword(token); - } - - export interface Scanner { - getStartPos(): number; - getToken(): ts.SyntaxKind; - getTextPos(): number; - getTokenPos(): number; - getTokenText(): string; - getTokenValue(): string; - hasUnicodeEscape(): boolean; - hasExtendedUnicodeEscape(): boolean; - hasPrecedingLineBreak(): boolean; - /* @internal */ - hasPrecedingJSDocComment(): boolean; - isIdentifier(): boolean; - isReservedWord(): boolean; - isUnterminated(): boolean; - /* @internal */ - getNumericLiteralFlags(): ts.TokenFlags; - /* @internal */ - getCommentDirectives(): ts.CommentDirective[] | undefined; - /* @internal */ - getTokenFlags(): ts.TokenFlags; - reScanGreaterToken(): ts.SyntaxKind; - reScanSlashToken(): ts.SyntaxKind; - reScanAsteriskEqualsToken(): ts.SyntaxKind; - reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind; - reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind; - scanJsxIdentifier(): ts.SyntaxKind; - scanJsxAttributeValue(): ts.SyntaxKind; - reScanJsxAttributeValue(): ts.SyntaxKind; - reScanJsxToken(allowMultilineJsxText?: boolean): ts.JsxTokenSyntaxKind; - reScanLessThanToken(): ts.SyntaxKind; - reScanHashToken(): ts.SyntaxKind; - reScanQuestionToken(): ts.SyntaxKind; - reScanInvalidIdentifier(): ts.SyntaxKind; - scanJsxToken(): ts.JsxTokenSyntaxKind; - scanJsDocToken(): ts.JSDocSyntaxKind; - scan(): ts.SyntaxKind; - - getText(): string; - /* @internal */ - clearCommentDirectives(): void; - // 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: ts.ScriptTarget): void; - setLanguageVariant(variant: ts.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; - } - - /** @internal */ - export const textToKeywordObj: ts.MapLike = { - abstract: ts.SyntaxKind.AbstractKeyword, - any: ts.SyntaxKind.AnyKeyword, - as: ts.SyntaxKind.AsKeyword, - asserts: ts.SyntaxKind.AssertsKeyword, - assert: ts.SyntaxKind.AssertKeyword, - bigint: ts.SyntaxKind.BigIntKeyword, - boolean: ts.SyntaxKind.BooleanKeyword, - break: ts.SyntaxKind.BreakKeyword, - case: ts.SyntaxKind.CaseKeyword, - catch: ts.SyntaxKind.CatchKeyword, - class: ts.SyntaxKind.ClassKeyword, - continue: ts.SyntaxKind.ContinueKeyword, - const: ts.SyntaxKind.ConstKeyword, - ["" + "constructor"]: ts.SyntaxKind.ConstructorKeyword, - debugger: ts.SyntaxKind.DebuggerKeyword, - declare: ts.SyntaxKind.DeclareKeyword, - default: ts.SyntaxKind.DefaultKeyword, - delete: ts.SyntaxKind.DeleteKeyword, - do: ts.SyntaxKind.DoKeyword, - else: ts.SyntaxKind.ElseKeyword, - enum: ts.SyntaxKind.EnumKeyword, - export: ts.SyntaxKind.ExportKeyword, - extends: ts.SyntaxKind.ExtendsKeyword, - false: ts.SyntaxKind.FalseKeyword, - finally: ts.SyntaxKind.FinallyKeyword, - for: ts.SyntaxKind.ForKeyword, - from: ts.SyntaxKind.FromKeyword, - function: ts.SyntaxKind.FunctionKeyword, - get: ts.SyntaxKind.GetKeyword, - if: ts.SyntaxKind.IfKeyword, - implements: ts.SyntaxKind.ImplementsKeyword, - import: ts.SyntaxKind.ImportKeyword, - in: ts.SyntaxKind.InKeyword, - infer: ts.SyntaxKind.InferKeyword, - instanceof: ts.SyntaxKind.InstanceOfKeyword, - interface: ts.SyntaxKind.InterfaceKeyword, - intrinsic: ts.SyntaxKind.IntrinsicKeyword, - is: ts.SyntaxKind.IsKeyword, - keyof: ts.SyntaxKind.KeyOfKeyword, - let: ts.SyntaxKind.LetKeyword, - module: ts.SyntaxKind.ModuleKeyword, - namespace: ts.SyntaxKind.NamespaceKeyword, - never: ts.SyntaxKind.NeverKeyword, - new: ts.SyntaxKind.NewKeyword, - null: ts.SyntaxKind.NullKeyword, - number: ts.SyntaxKind.NumberKeyword, - object: ts.SyntaxKind.ObjectKeyword, - package: ts.SyntaxKind.PackageKeyword, - private: ts.SyntaxKind.PrivateKeyword, - protected: ts.SyntaxKind.ProtectedKeyword, - public: ts.SyntaxKind.PublicKeyword, - override: ts.SyntaxKind.OverrideKeyword, - out: ts.SyntaxKind.OutKeyword, - readonly: ts.SyntaxKind.ReadonlyKeyword, - require: ts.SyntaxKind.RequireKeyword, - global: ts.SyntaxKind.GlobalKeyword, - return: ts.SyntaxKind.ReturnKeyword, - set: ts.SyntaxKind.SetKeyword, - static: ts.SyntaxKind.StaticKeyword, - string: ts.SyntaxKind.StringKeyword, - super: ts.SyntaxKind.SuperKeyword, - switch: ts.SyntaxKind.SwitchKeyword, - symbol: ts.SyntaxKind.SymbolKeyword, - this: ts.SyntaxKind.ThisKeyword, - throw: ts.SyntaxKind.ThrowKeyword, - true: ts.SyntaxKind.TrueKeyword, - try: ts.SyntaxKind.TryKeyword, - type: ts.SyntaxKind.TypeKeyword, - typeof: ts.SyntaxKind.TypeOfKeyword, - undefined: ts.SyntaxKind.UndefinedKeyword, - unique: ts.SyntaxKind.UniqueKeyword, - unknown: ts.SyntaxKind.UnknownKeyword, - var: ts.SyntaxKind.VarKeyword, - void: ts.SyntaxKind.VoidKeyword, - while: ts.SyntaxKind.WhileKeyword, - with: ts.SyntaxKind.WithKeyword, - yield: ts.SyntaxKind.YieldKeyword, - async: ts.SyntaxKind.AsyncKeyword, - await: ts.SyntaxKind.AwaitKeyword, - of: ts.SyntaxKind.OfKeyword, - }; +/* @internal */ +export function tokenIsIdentifierOrKeyword(token: ts.SyntaxKind): boolean { + return token >= ts.SyntaxKind.Identifier; +} - const textToKeyword = new ts.Map(ts.getEntries(textToKeywordObj)); - const textToToken = new ts.Map(ts.getEntries({ - ...textToKeywordObj, - "{": ts.SyntaxKind.OpenBraceToken, - "}": ts.SyntaxKind.CloseBraceToken, - "(": ts.SyntaxKind.OpenParenToken, - ")": ts.SyntaxKind.CloseParenToken, - "[": ts.SyntaxKind.OpenBracketToken, - "]": ts.SyntaxKind.CloseBracketToken, - ".": ts.SyntaxKind.DotToken, - "...": ts.SyntaxKind.DotDotDotToken, - ";": ts.SyntaxKind.SemicolonToken, - ",": ts.SyntaxKind.CommaToken, - "<": ts.SyntaxKind.LessThanToken, - ">": ts.SyntaxKind.GreaterThanToken, - "<=": ts.SyntaxKind.LessThanEqualsToken, - ">=": ts.SyntaxKind.GreaterThanEqualsToken, - "==": ts.SyntaxKind.EqualsEqualsToken, - "!=": ts.SyntaxKind.ExclamationEqualsToken, - "===": ts.SyntaxKind.EqualsEqualsEqualsToken, - "!==": ts.SyntaxKind.ExclamationEqualsEqualsToken, - "=>": ts.SyntaxKind.EqualsGreaterThanToken, - "+": ts.SyntaxKind.PlusToken, - "-": ts.SyntaxKind.MinusToken, - "**": ts.SyntaxKind.AsteriskAsteriskToken, - "*": ts.SyntaxKind.AsteriskToken, - "/": ts.SyntaxKind.SlashToken, - "%": ts.SyntaxKind.PercentToken, - "++": ts.SyntaxKind.PlusPlusToken, - "--": ts.SyntaxKind.MinusMinusToken, - "<<": ts.SyntaxKind.LessThanLessThanToken, - ">": ts.SyntaxKind.GreaterThanGreaterThanToken, - ">>>": ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, - "&": ts.SyntaxKind.AmpersandToken, - "|": ts.SyntaxKind.BarToken, - "^": ts.SyntaxKind.CaretToken, - "!": ts.SyntaxKind.ExclamationToken, - "~": ts.SyntaxKind.TildeToken, - "&&": ts.SyntaxKind.AmpersandAmpersandToken, - "||": ts.SyntaxKind.BarBarToken, - "?": ts.SyntaxKind.QuestionToken, - "??": ts.SyntaxKind.QuestionQuestionToken, - "?.": ts.SyntaxKind.QuestionDotToken, - ":": ts.SyntaxKind.ColonToken, - "=": ts.SyntaxKind.EqualsToken, - "+=": ts.SyntaxKind.PlusEqualsToken, - "-=": ts.SyntaxKind.MinusEqualsToken, - "*=": ts.SyntaxKind.AsteriskEqualsToken, - "**=": ts.SyntaxKind.AsteriskAsteriskEqualsToken, - "/=": ts.SyntaxKind.SlashEqualsToken, - "%=": ts.SyntaxKind.PercentEqualsToken, - "<<=": ts.SyntaxKind.LessThanLessThanEqualsToken, - ">>=": ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, - ">>>=": ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, - "&=": ts.SyntaxKind.AmpersandEqualsToken, - "|=": ts.SyntaxKind.BarEqualsToken, - "^=": ts.SyntaxKind.CaretEqualsToken, - "||=": ts.SyntaxKind.BarBarEqualsToken, - "&&=": ts.SyntaxKind.AmpersandAmpersandEqualsToken, - "??=": ts.SyntaxKind.QuestionQuestionEqualsToken, - "@": ts.SyntaxKind.AtToken, - "#": ts.SyntaxKind.HashToken, - "`": ts.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, ]; +/* @internal */ +export function tokenIsIdentifierOrKeywordOrGreaterThan(token: ts.SyntaxKind): boolean { + return token === ts.SyntaxKind.GreaterThanToken || tokenIsIdentifierOrKeyword(token); +} - /** - * 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]; +export interface Scanner { + getStartPos(): number; + getToken(): ts.SyntaxKind; + getTextPos(): number; + getTokenPos(): number; + getTokenText(): string; + getTokenValue(): string; + hasUnicodeEscape(): boolean; + hasExtendedUnicodeEscape(): boolean; + hasPrecedingLineBreak(): boolean; + /* @internal */ + hasPrecedingJSDocComment(): boolean; + isIdentifier(): boolean; + isReservedWord(): boolean; + isUnterminated(): boolean; + /* @internal */ + getNumericLiteralFlags(): ts.TokenFlags; + /* @internal */ + getCommentDirectives(): ts.CommentDirective[] | undefined; + /* @internal */ + getTokenFlags(): ts.TokenFlags; + reScanGreaterToken(): ts.SyntaxKind; + reScanSlashToken(): ts.SyntaxKind; + reScanAsteriskEqualsToken(): ts.SyntaxKind; + reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind; + reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind; + scanJsxIdentifier(): ts.SyntaxKind; + scanJsxAttributeValue(): ts.SyntaxKind; + reScanJsxAttributeValue(): ts.SyntaxKind; + reScanJsxToken(allowMultilineJsxText?: boolean): ts.JsxTokenSyntaxKind; + reScanLessThanToken(): ts.SyntaxKind; + reScanHashToken(): ts.SyntaxKind; + reScanQuestionToken(): ts.SyntaxKind; + reScanInvalidIdentifier(): ts.SyntaxKind; + scanJsxToken(): ts.JsxTokenSyntaxKind; + scanJsDocToken(): ts.JSDocSyntaxKind; + scan(): ts.SyntaxKind; + + getText(): string; + /* @internal */ + clearCommentDirectives(): void; + // 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: ts.ScriptTarget): void; + setLanguageVariant(variant: ts.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; +} - /** - * Test for whether a single line comment with leading whitespace trimmed's text contains a directive. - */ - const commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/; +/** @internal */ +export const textToKeywordObj: ts.MapLike = { + abstract: ts.SyntaxKind.AbstractKeyword, + any: ts.SyntaxKind.AnyKeyword, + as: ts.SyntaxKind.AsKeyword, + asserts: ts.SyntaxKind.AssertsKeyword, + assert: ts.SyntaxKind.AssertKeyword, + bigint: ts.SyntaxKind.BigIntKeyword, + boolean: ts.SyntaxKind.BooleanKeyword, + break: ts.SyntaxKind.BreakKeyword, + case: ts.SyntaxKind.CaseKeyword, + catch: ts.SyntaxKind.CatchKeyword, + class: ts.SyntaxKind.ClassKeyword, + continue: ts.SyntaxKind.ContinueKeyword, + const: ts.SyntaxKind.ConstKeyword, + ["" + "constructor"]: ts.SyntaxKind.ConstructorKeyword, + debugger: ts.SyntaxKind.DebuggerKeyword, + declare: ts.SyntaxKind.DeclareKeyword, + default: ts.SyntaxKind.DefaultKeyword, + delete: ts.SyntaxKind.DeleteKeyword, + do: ts.SyntaxKind.DoKeyword, + else: ts.SyntaxKind.ElseKeyword, + enum: ts.SyntaxKind.EnumKeyword, + export: ts.SyntaxKind.ExportKeyword, + extends: ts.SyntaxKind.ExtendsKeyword, + false: ts.SyntaxKind.FalseKeyword, + finally: ts.SyntaxKind.FinallyKeyword, + for: ts.SyntaxKind.ForKeyword, + from: ts.SyntaxKind.FromKeyword, + function: ts.SyntaxKind.FunctionKeyword, + get: ts.SyntaxKind.GetKeyword, + if: ts.SyntaxKind.IfKeyword, + implements: ts.SyntaxKind.ImplementsKeyword, + import: ts.SyntaxKind.ImportKeyword, + in: ts.SyntaxKind.InKeyword, + infer: ts.SyntaxKind.InferKeyword, + instanceof: ts.SyntaxKind.InstanceOfKeyword, + interface: ts.SyntaxKind.InterfaceKeyword, + intrinsic: ts.SyntaxKind.IntrinsicKeyword, + is: ts.SyntaxKind.IsKeyword, + keyof: ts.SyntaxKind.KeyOfKeyword, + let: ts.SyntaxKind.LetKeyword, + module: ts.SyntaxKind.ModuleKeyword, + namespace: ts.SyntaxKind.NamespaceKeyword, + never: ts.SyntaxKind.NeverKeyword, + new: ts.SyntaxKind.NewKeyword, + null: ts.SyntaxKind.NullKeyword, + number: ts.SyntaxKind.NumberKeyword, + object: ts.SyntaxKind.ObjectKeyword, + package: ts.SyntaxKind.PackageKeyword, + private: ts.SyntaxKind.PrivateKeyword, + protected: ts.SyntaxKind.ProtectedKeyword, + public: ts.SyntaxKind.PublicKeyword, + override: ts.SyntaxKind.OverrideKeyword, + out: ts.SyntaxKind.OutKeyword, + readonly: ts.SyntaxKind.ReadonlyKeyword, + require: ts.SyntaxKind.RequireKeyword, + global: ts.SyntaxKind.GlobalKeyword, + return: ts.SyntaxKind.ReturnKeyword, + set: ts.SyntaxKind.SetKeyword, + static: ts.SyntaxKind.StaticKeyword, + string: ts.SyntaxKind.StringKeyword, + super: ts.SyntaxKind.SuperKeyword, + switch: ts.SyntaxKind.SwitchKeyword, + symbol: ts.SyntaxKind.SymbolKeyword, + this: ts.SyntaxKind.ThisKeyword, + throw: ts.SyntaxKind.ThrowKeyword, + true: ts.SyntaxKind.TrueKeyword, + try: ts.SyntaxKind.TryKeyword, + type: ts.SyntaxKind.TypeKeyword, + typeof: ts.SyntaxKind.TypeOfKeyword, + undefined: ts.SyntaxKind.UndefinedKeyword, + unique: ts.SyntaxKind.UniqueKeyword, + unknown: ts.SyntaxKind.UnknownKeyword, + var: ts.SyntaxKind.VarKeyword, + void: ts.SyntaxKind.VoidKeyword, + while: ts.SyntaxKind.WhileKeyword, + with: ts.SyntaxKind.WithKeyword, + yield: ts.SyntaxKind.YieldKeyword, + async: ts.SyntaxKind.AsyncKeyword, + await: ts.SyntaxKind.AwaitKeyword, + of: ts.SyntaxKind.OfKeyword, +}; + +const textToKeyword = new ts.Map(ts.getEntries(textToKeywordObj)); +const textToToken = new ts.Map(ts.getEntries({ + ...textToKeywordObj, + "{": ts.SyntaxKind.OpenBraceToken, + "}": ts.SyntaxKind.CloseBraceToken, + "(": ts.SyntaxKind.OpenParenToken, + ")": ts.SyntaxKind.CloseParenToken, + "[": ts.SyntaxKind.OpenBracketToken, + "]": ts.SyntaxKind.CloseBracketToken, + ".": ts.SyntaxKind.DotToken, + "...": ts.SyntaxKind.DotDotDotToken, + ";": ts.SyntaxKind.SemicolonToken, + ",": ts.SyntaxKind.CommaToken, + "<": ts.SyntaxKind.LessThanToken, + ">": ts.SyntaxKind.GreaterThanToken, + "<=": ts.SyntaxKind.LessThanEqualsToken, + ">=": ts.SyntaxKind.GreaterThanEqualsToken, + "==": ts.SyntaxKind.EqualsEqualsToken, + "!=": ts.SyntaxKind.ExclamationEqualsToken, + "===": ts.SyntaxKind.EqualsEqualsEqualsToken, + "!==": ts.SyntaxKind.ExclamationEqualsEqualsToken, + "=>": ts.SyntaxKind.EqualsGreaterThanToken, + "+": ts.SyntaxKind.PlusToken, + "-": ts.SyntaxKind.MinusToken, + "**": ts.SyntaxKind.AsteriskAsteriskToken, + "*": ts.SyntaxKind.AsteriskToken, + "/": ts.SyntaxKind.SlashToken, + "%": ts.SyntaxKind.PercentToken, + "++": ts.SyntaxKind.PlusPlusToken, + "--": ts.SyntaxKind.MinusMinusToken, + "<<": ts.SyntaxKind.LessThanLessThanToken, + ">": ts.SyntaxKind.GreaterThanGreaterThanToken, + ">>>": ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, + "&": ts.SyntaxKind.AmpersandToken, + "|": ts.SyntaxKind.BarToken, + "^": ts.SyntaxKind.CaretToken, + "!": ts.SyntaxKind.ExclamationToken, + "~": ts.SyntaxKind.TildeToken, + "&&": ts.SyntaxKind.AmpersandAmpersandToken, + "||": ts.SyntaxKind.BarBarToken, + "?": ts.SyntaxKind.QuestionToken, + "??": ts.SyntaxKind.QuestionQuestionToken, + "?.": ts.SyntaxKind.QuestionDotToken, + ":": ts.SyntaxKind.ColonToken, + "=": ts.SyntaxKind.EqualsToken, + "+=": ts.SyntaxKind.PlusEqualsToken, + "-=": ts.SyntaxKind.MinusEqualsToken, + "*=": ts.SyntaxKind.AsteriskEqualsToken, + "**=": ts.SyntaxKind.AsteriskAsteriskEqualsToken, + "/=": ts.SyntaxKind.SlashEqualsToken, + "%=": ts.SyntaxKind.PercentEqualsToken, + "<<=": ts.SyntaxKind.LessThanLessThanEqualsToken, + ">>=": ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, + ">>>=": ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, + "&=": ts.SyntaxKind.AmpersandEqualsToken, + "|=": ts.SyntaxKind.BarEqualsToken, + "^=": ts.SyntaxKind.CaretEqualsToken, + "||=": ts.SyntaxKind.BarBarEqualsToken, + "&&=": ts.SyntaxKind.AmpersandAmpersandEqualsToken, + "??=": ts.SyntaxKind.QuestionQuestionEqualsToken, + "@": ts.SyntaxKind.AtToken, + "#": ts.SyntaxKind.HashToken, + "`": ts.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]; + +/** + * Test for whether a single line comment with leading whitespace trimmed's text contains a directive. + */ +const commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/; + +/** + * Test for whether a multi-line comment with leading whitespace trimmed's last line contains a directive. + */ +const commentDirectiveRegExMultiLine = /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/; + +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; + } - /** - * Test for whether a multi-line comment with leading whitespace trimmed's last line contains a directive. - */ - const commentDirectiveRegExMultiLine = /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/; + // Perform binary search in one of the Unicode range maps + let lo = 0; + let hi: number = map.length; + let mid: number; - 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; + 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; } - // 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; - } + if (code < map[mid]) { + hi = mid; + } + else { + lo = mid + 2; } - - return false; } - /* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ts.ScriptTarget | undefined) { - return languageVersion! >= ts.ScriptTarget.ES2015 ? - lookupInUnicodeMap(code, unicodeESNextIdentifierStart) : - languageVersion === ts.ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : - lookupInUnicodeMap(code, unicodeES3IdentifierStart); - } + return false; +} - function isUnicodeIdentifierPart(code: number, languageVersion: ts.ScriptTarget | undefined) { - return languageVersion! >= ts.ScriptTarget.ES2015 ? - lookupInUnicodeMap(code, unicodeESNextIdentifierPart) : - languageVersion === ts.ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) : - lookupInUnicodeMap(code, unicodeES3IdentifierPart); - } +/* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ts.ScriptTarget | undefined) { + return languageVersion! >= ts.ScriptTarget.ES2015 ? + lookupInUnicodeMap(code, unicodeESNextIdentifierStart) : + languageVersion === ts.ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : + lookupInUnicodeMap(code, unicodeES3IdentifierStart); +} - function makeReverseMap(source: ts.ESMap): string[] { - const result: string[] = []; - source.forEach((value, name) => { - result[value] = name; - }); - return result; - } +function isUnicodeIdentifierPart(code: number, languageVersion: ts.ScriptTarget | undefined) { + return languageVersion! >= ts.ScriptTarget.ES2015 ? + lookupInUnicodeMap(code, unicodeESNextIdentifierPart) : + languageVersion === ts.ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) : + lookupInUnicodeMap(code, unicodeES3IdentifierPart); +} - const tokenStrings = makeReverseMap(textToToken); - export function tokenToString(t: ts.SyntaxKind): string | undefined { - return tokenStrings[t]; - } +function makeReverseMap(source: ts.ESMap): string[] { + const result: string[] = []; + source.forEach((value, name) => { + result[value] = name; + }); + return result; +} - /* @internal */ - export function stringToToken(s: string): ts.SyntaxKind | undefined { - return textToToken.get(s); - } +const tokenStrings = makeReverseMap(textToToken); +export function tokenToString(t: ts.SyntaxKind): string | undefined { + return tokenStrings[t]; +} - /* @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 ts.CharacterCodes.carriageReturn: - if (text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { - pos++; - } - // falls through - case ts.CharacterCodes.lineFeed: +/* @internal */ +export function stringToToken(s: string): ts.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 ts.CharacterCodes.carriageReturn: + if (text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { + pos++; + } + // falls through + case ts.CharacterCodes.lineFeed: + result.push(lineStart); + lineStart = pos; + break; + default: + if (ch > ts.CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) { result.push(lineStart); lineStart = pos; - break; - default: - if (ch > ts.CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) { - result.push(lineStart); - lineStart = pos; - } - break; - } + } + break; } - result.push(lineStart); - return result; - } - - export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number): number; - /* @internal */ - export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number, allowEdits?: true): number; // eslint-disable-line @typescript-eslint/unified-signatures - export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number, allowEdits?: true): number { - return sourceFile.getPositionOfLineAndCharacter ? - sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) : - computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits); } + result.push(lineStart); + return result; +} - /* @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 { - ts.Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? ts.arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`); - } - } +export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number): number; +/* @internal */ +export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number, allowEdits?: true): number; // eslint-disable-line @typescript-eslint/unified-signatures +export function getPositionOfLineAndCharacter(sourceFile: ts.SourceFileLike, line: number, character: number, allowEdits?: true): number { + return sourceFile.getPositionOfLineAndCharacter ? + sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) : + computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits); +} - const res = lineStarts[line] + character; +/* @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) { - ts.Debug.assert(res < lineStarts[line + 1]); - } - else if (debugText !== undefined) { - ts.Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline + else { + ts.Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? ts.arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`); } - return res; } - /* @internal */ - export function getLineStarts(sourceFile: ts.SourceFileLike): readonly number[] { - return sourceFile.lineMap || (sourceFile.lineMap = computeLineStarts(sourceFile.text)); + 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; } - - /* @internal */ - export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): ts.LineAndCharacter { - const lineNumber = computeLineOfPosition(lineStarts, position); - return { - line: lineNumber, - character: position - lineStarts[lineNumber] - }; + if (line < lineStarts.length - 1) { + ts.Debug.assert(res < lineStarts[line + 1]); } - - /** - * @internal - * We assume the first line starts at position 0 and 'position' is non-negative. - */ - export function computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number) { - let lineNumber = ts.binarySearch(lineStarts, position, ts.identity, ts.compareValues, lowerBound); - 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; - ts.Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file"); - } - return lineNumber; + else if (debugText !== undefined) { + ts.Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline } + return res; +} - /** @internal */ - export function getLinesBetweenPositions(sourceFile: ts.SourceFileLike, pos1: number, pos2: number) { - if (pos1 === pos2) - return 0; - const lineStarts = getLineStarts(sourceFile); - const lower = Math.min(pos1, pos2); - const isNegative = lower === pos2; - const upper = isNegative ? pos1 : pos2; - const lowerLine = computeLineOfPosition(lineStarts, lower); - const upperLine = computeLineOfPosition(lineStarts, upper, lowerLine); - return isNegative ? lowerLine - upperLine : upperLine - lowerLine; - } +/* @internal */ +export function getLineStarts(sourceFile: ts.SourceFileLike): readonly number[] { + return sourceFile.lineMap || (sourceFile.lineMap = computeLineStarts(sourceFile.text)); +} - export function getLineAndCharacterOfPosition(sourceFile: ts.SourceFileLike, position: number): ts.LineAndCharacter { - return computeLineAndCharacterOfPosition(getLineStarts(sourceFile), position); - } +/* @internal */ +export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): ts.LineAndCharacter { + const lineNumber = computeLineOfPosition(lineStarts, position); + return { + line: lineNumber, + character: position - lineStarts[lineNumber] + }; +} - export function isWhiteSpaceLike(ch: number): boolean { - return isWhiteSpaceSingleLine(ch) || isLineBreak(ch); +/** + * @internal + * We assume the first line starts at position 0 and 'position' is non-negative. + */ +export function computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number) { + let lineNumber = ts.binarySearch(lineStarts, position, ts.identity, ts.compareValues, lowerBound); + 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; + ts.Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file"); } + return lineNumber; +} - /** 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 === ts.CharacterCodes.space || - ch === ts.CharacterCodes.tab || - ch === ts.CharacterCodes.verticalTab || - ch === ts.CharacterCodes.formFeed || - ch === ts.CharacterCodes.nonBreakingSpace || - ch === ts.CharacterCodes.nextLine || - ch === ts.CharacterCodes.ogham || - ch >= ts.CharacterCodes.enQuad && ch <= ts.CharacterCodes.zeroWidthSpace || - ch === ts.CharacterCodes.narrowNoBreakSpace || - ch === ts.CharacterCodes.mathematicalSpace || - ch === ts.CharacterCodes.ideographicSpace || - ch === ts.CharacterCodes.byteOrderMark; - } +/** @internal */ +export function getLinesBetweenPositions(sourceFile: ts.SourceFileLike, pos1: number, pos2: number) { + if (pos1 === pos2) + return 0; + const lineStarts = getLineStarts(sourceFile); + const lower = Math.min(pos1, pos2); + const isNegative = lower === pos2; + const upper = isNegative ? pos1 : pos2; + const lowerLine = computeLineOfPosition(lineStarts, lower); + const upperLine = computeLineOfPosition(lineStarts, upper, lowerLine); + return isNegative ? lowerLine - upperLine : upperLine - lowerLine; +} + +export function getLineAndCharacterOfPosition(sourceFile: ts.SourceFileLike, position: number): ts.LineAndCharacter { + return computeLineAndCharacterOfPosition(getLineStarts(sourceFile), position); +} - 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. +export function isWhiteSpaceLike(ch: number): boolean { + return isWhiteSpaceSingleLine(ch) || isLineBreak(ch); +} - return ch === ts.CharacterCodes.lineFeed || - ch === ts.CharacterCodes.carriageReturn || - ch === ts.CharacterCodes.lineSeparator || - ch === ts.CharacterCodes.paragraphSeparator; - } +/** 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 === ts.CharacterCodes.space || + ch === ts.CharacterCodes.tab || + ch === ts.CharacterCodes.verticalTab || + ch === ts.CharacterCodes.formFeed || + ch === ts.CharacterCodes.nonBreakingSpace || + ch === ts.CharacterCodes.nextLine || + ch === ts.CharacterCodes.ogham || + ch >= ts.CharacterCodes.enQuad && ch <= ts.CharacterCodes.zeroWidthSpace || + ch === ts.CharacterCodes.narrowNoBreakSpace || + ch === ts.CharacterCodes.mathematicalSpace || + ch === ts.CharacterCodes.ideographicSpace || + ch === ts.CharacterCodes.byteOrderMark; +} - function isDigit(ch: number): boolean { - return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9; - } +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 === ts.CharacterCodes.lineFeed || + ch === ts.CharacterCodes.carriageReturn || + ch === ts.CharacterCodes.lineSeparator || + ch === ts.CharacterCodes.paragraphSeparator; +} - function isHexDigit(ch: number): boolean { - return isDigit(ch) || ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.F || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.f; - } +function isDigit(ch: number): boolean { + return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9; +} - function isCodePoint(code: number): boolean { - return code <= 0x10FFFF; +function isHexDigit(ch: number): boolean { + return isDigit(ch) || ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.F || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.f; +} + +function isCodePoint(code: number): boolean { + return code <= 0x10FFFF; +} + +/* @internal */ +export function isOctalDigit(ch: number): boolean { + return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._7; +} + +export function couldStartTrivia(text: string, pos: number): boolean { + // Keep in sync with skipTrivia + const ch = text.charCodeAt(pos); + switch (ch) { + case ts.CharacterCodes.carriageReturn: + case ts.CharacterCodes.lineFeed: + case ts.CharacterCodes.tab: + case ts.CharacterCodes.verticalTab: + case ts.CharacterCodes.formFeed: + case ts.CharacterCodes.space: + case ts.CharacterCodes.slash: + // starts of normal trivia + // falls through + case ts.CharacterCodes.lessThan: + case ts.CharacterCodes.bar: + case ts.CharacterCodes.equals: + case ts.CharacterCodes.greaterThan: + // Starts of conflict marker trivia + return true; + case ts.CharacterCodes.hash: + // Only if its the beginning can we have #! trivia + return pos === 0; + default: + return ch > ts.CharacterCodes.maxAsciiCharacter; } +} - /* @internal */ - export function isOctalDigit(ch: number): boolean { - return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._7; +/* @internal */ +export function skipTrivia(text: string, pos: number, stopAfterLineBreak?: boolean, stopAtComments?: boolean, inJSDoc?: boolean): number { + if (ts.positionIsSynthesized(pos)) { + return pos; } - export function couldStartTrivia(text: string, pos: number): boolean { - // Keep in sync with skipTrivia + let canConsumeStar = false; + // Keep in sync with couldStartTrivia + while (true) { const ch = text.charCodeAt(pos); switch (ch) { case ts.CharacterCodes.carriageReturn: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { + pos++; + } + // falls through case ts.CharacterCodes.lineFeed: + pos++; + if (stopAfterLineBreak) { + return pos; + } + canConsumeStar = !!inJSDoc; + continue; case ts.CharacterCodes.tab: case ts.CharacterCodes.verticalTab: case ts.CharacterCodes.formFeed: case ts.CharacterCodes.space: + pos++; + continue; case ts.CharacterCodes.slash: - // starts of normal trivia - // falls through + if (stopAtComments) { + break; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { + pos += 2; + while (pos < text.length) { + if (isLineBreak(text.charCodeAt(pos))) { + break; + } + pos++; + } + canConsumeStar = false; + continue; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { + pos += 2; + while (pos < text.length) { + if (text.charCodeAt(pos) === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { + pos += 2; + break; + } + pos++; + } + canConsumeStar = false; + continue; + } + break; + case ts.CharacterCodes.lessThan: case ts.CharacterCodes.bar: case ts.CharacterCodes.equals: case ts.CharacterCodes.greaterThan: - // Starts of conflict marker trivia - return true; + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos); + canConsumeStar = false; + continue; + } + break; + case ts.CharacterCodes.hash: - // Only if its the beginning can we have #! trivia - return pos === 0; + if (pos === 0 && isShebangTrivia(text, pos)) { + pos = scanShebangTrivia(text, pos); + canConsumeStar = false; + continue; + } + break; + + case ts.CharacterCodes.asterisk: + if (canConsumeStar) { + pos++; + canConsumeStar = false; + continue; + } + break; + default: - return ch > ts.CharacterCodes.maxAsciiCharacter; + if (ch > ts.CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { + pos++; + continue; + } + break; } + return pos; } +} - /* @internal */ - export function skipTrivia(text: string, pos: number, stopAfterLineBreak?: boolean, stopAtComments?: boolean, inJSDoc?: boolean): number { - if (ts.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) { + ts.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 === ts.CharacterCodes.equals || + text.charCodeAt(pos + mergeConflictMarkerLength) === ts.CharacterCodes.space; } + } - let canConsumeStar = false; - // Keep in sync with couldStartTrivia - while (true) { - const ch = text.charCodeAt(pos); - switch (ch) { - case ts.CharacterCodes.carriageReturn: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { - pos++; - } - // falls through - case ts.CharacterCodes.lineFeed: - pos++; - if (stopAfterLineBreak) { - return pos; - } - canConsumeStar = !!inJSDoc; - continue; - case ts.CharacterCodes.tab: - case ts.CharacterCodes.verticalTab: - case ts.CharacterCodes.formFeed: - case ts.CharacterCodes.space: + return false; +} + +function scanConflictMarkerTrivia(text: string, pos: number, error?: (diag: ts.DiagnosticMessage, pos?: number, len?: number) => void) { + if (error) { + error(ts.Diagnostics.Merge_conflict_marker_encountered, pos, mergeConflictMarkerLength); + } + + const ch = text.charCodeAt(pos); + const len = text.length; + + if (ch === ts.CharacterCodes.lessThan || ch === ts.CharacterCodes.greaterThan) { + while (pos < len && !isLineBreak(text.charCodeAt(pos))) { + pos++; + } + } + else { + ts.Debug.assert(ch === ts.CharacterCodes.bar || ch === ts.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 === ts.CharacterCodes.equals || currentChar === ts.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 + ts.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: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U | undefined) => U, state: T, initial?: U): U | undefined { + let pendingPos!: number; + let pendingEnd!: number; + let pendingKind!: ts.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 ts.CharacterCodes.carriageReturn: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { pos++; - continue; - case ts.CharacterCodes.slash: - if (stopAtComments) { - break; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { - pos += 2; + } + // falls through + case ts.CharacterCodes.lineFeed: + pos++; + if (trailing) { + break scan; + } + + collecting = true; + if (hasPendingCommentRange) { + pendingHasTrailingNewLine = true; + } + + continue; + case ts.CharacterCodes.tab: + case ts.CharacterCodes.verticalTab: + case ts.CharacterCodes.formFeed: + case ts.CharacterCodes.space: + pos++; + continue; + case ts.CharacterCodes.slash: + const nextChar = text.charCodeAt(pos + 1); + let hasTrailingNewLine = false; + if (nextChar === ts.CharacterCodes.slash || nextChar === ts.CharacterCodes.asterisk) { + const kind = nextChar === ts.CharacterCodes.slash ? ts.SyntaxKind.SingleLineCommentTrivia : ts.SyntaxKind.MultiLineCommentTrivia; + const startPos = pos; + pos += 2; + if (nextChar === ts.CharacterCodes.slash) { while (pos < text.length) { if (isLineBreak(text.charCodeAt(pos))) { + hasTrailingNewLine = true; break; } pos++; } - canConsumeStar = false; - continue; } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { - pos += 2; + else { while (pos < text.length) { if (text.charCodeAt(pos) === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { pos += 2; @@ -611,2008 +811,1808 @@ namespace ts { } pos++; } - canConsumeStar = false; - continue; } - break; - case ts.CharacterCodes.lessThan: - case ts.CharacterCodes.bar: - case ts.CharacterCodes.equals: - case ts.CharacterCodes.greaterThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos); - canConsumeStar = false; - continue; - } - break; + 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; + } + } - case ts.CharacterCodes.hash: - if (pos === 0 && isShebangTrivia(text, pos)) { - pos = scanShebangTrivia(text, pos); - canConsumeStar = false; - continue; + pendingPos = startPos; + pendingEnd = pos; + pendingKind = kind; + pendingHasTrailingNewLine = hasTrailingNewLine; + hasPendingCommentRange = true; } - break; - - case ts.CharacterCodes.asterisk: - if (canConsumeStar) { - pos++; - canConsumeStar = false; - continue; - } - break; - default: - if (ch > ts.CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { - pos++; - continue; + continue; + } + break scan; + default: + if (ch > ts.CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { + if (hasPendingCommentRange && isLineBreak(ch)) { + pendingHasTrailingNewLine = true; } - break; - } - return pos; + pos++; + continue; + } + break scan; } } - // 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; + if (hasPendingCommentRange) { + accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); + } - function isConflictMarkerTrivia(text: string, pos: number) { - ts.Debug.assert(pos >= 0); + return accumulator; +} - // Conflict markers must be at the start of a line. - if (pos === 0 || isLineBreak(text.charCodeAt(pos - 1))) { - const ch = text.charCodeAt(pos); +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { + return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ false, cb, state); +} - if ((pos + mergeConflictMarkerLength) < text.length) { - for (let i = 0; i < mergeConflictMarkerLength; i++) { - if (text.charCodeAt(pos + i) !== ch) { - return false; - } - } +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { + return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ true, cb, state); +} - return ch === ts.CharacterCodes.equals || - text.charCodeAt(pos + mergeConflictMarkerLength) === ts.CharacterCodes.space; - } - } +export function reduceEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { + return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ false, cb, state, initial); +} - return false; - } +export function reduceEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { + return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ true, cb, state, initial); +} - function scanConflictMarkerTrivia(text: string, pos: number, error?: (diag: ts.DiagnosticMessage, pos?: number, len?: number) => void) { - if (error) { - error(ts.Diagnostics.Merge_conflict_marker_encountered, pos, mergeConflictMarkerLength); - } +function appendCommentRange(pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, _state: any, comments: ts.CommentRange[]) { + if (!comments) { + comments = []; + } - const ch = text.charCodeAt(pos); - const len = text.length; + comments.push({ kind, pos, end, hasTrailingNewLine }); + return comments; +} - if (ch === ts.CharacterCodes.lessThan || ch === ts.CharacterCodes.greaterThan) { - while (pos < len && !isLineBreak(text.charCodeAt(pos))) { - pos++; - } - } - else { - ts.Debug.assert(ch === ts.CharacterCodes.bar || ch === ts.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 === ts.CharacterCodes.equals || currentChar === ts.CharacterCodes.greaterThan) && currentChar !== ch && isConflictMarkerTrivia(text, pos)) { - break; - } +export function getLeadingCommentRanges(text: string, pos: number): ts.CommentRange[] | undefined { + return reduceEachLeadingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); +} - pos++; - } - } +export function getTrailingCommentRanges(text: string, pos: number): ts.CommentRange[] | undefined { + return reduceEachTrailingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); +} - return pos; +/** Optionally, get the shebang */ +export function getShebang(text: string): string | undefined { + const match = shebangTriviaRegex.exec(text); + if (match) { + return match[0]; } +} - const shebangTriviaRegex = /^#!.*/; +export function isIdentifierStart(ch: number, languageVersion: ts.ScriptTarget | undefined): boolean { + return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z || + ch === ts.CharacterCodes.$ || ch === ts.CharacterCodes._ || + ch > ts.CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion); +} - /*@internal*/ - export function isShebangTrivia(text: string, pos: number) { - // Shebangs check must only be done at the start of the file - ts.Debug.assert(pos === 0); - return shebangTriviaRegex.test(text); - } +export function isIdentifierPart(ch: number, languageVersion: ts.ScriptTarget | undefined, identifierVariant?: ts.LanguageVariant): boolean { + return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z || + ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9 || ch === ts.CharacterCodes.$ || ch === ts.CharacterCodes._ || + // "-" and ":" are valid in JSX Identifiers + (identifierVariant === ts.LanguageVariant.JSX ? (ch === ts.CharacterCodes.minus || ch === ts.CharacterCodes.colon) : false) || + ch > ts.CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); +} - /*@internal*/ - export function scanShebangTrivia(text: string, pos: number) { - const shebang = shebangTriviaRegex.exec(text)![0]; - pos = pos + shebang.length; - return pos; +/* @internal */ +export function isIdentifierText(name: string, languageVersion: ts.ScriptTarget | undefined, identifierVariant?: ts.LanguageVariant): boolean { + let ch = codePointAt(name, 0); + if (!isIdentifierStart(ch, languageVersion)) { + return false; } - /** - * 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: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U | undefined) => U, state: T, initial?: U): U | undefined { - let pendingPos!: number; - let pendingEnd!: number; - let pendingKind!: ts.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; - } + for (let i = charSize(ch); i < name.length; i += charSize(ch)) { + if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion, identifierVariant)) { + return false; } - scan: while (pos >= 0 && pos < text.length) { - const ch = text.charCodeAt(pos); - switch (ch) { - case ts.CharacterCodes.carriageReturn: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { - pos++; - } - // falls through - case ts.CharacterCodes.lineFeed: - pos++; - if (trailing) { - break scan; - } - - collecting = true; - if (hasPendingCommentRange) { - pendingHasTrailingNewLine = true; - } + } - continue; - case ts.CharacterCodes.tab: - case ts.CharacterCodes.verticalTab: - case ts.CharacterCodes.formFeed: - case ts.CharacterCodes.space: - pos++; - continue; - case ts.CharacterCodes.slash: - const nextChar = text.charCodeAt(pos + 1); - let hasTrailingNewLine = false; - if (nextChar === ts.CharacterCodes.slash || nextChar === ts.CharacterCodes.asterisk) { - const kind = nextChar === ts.CharacterCodes.slash ? ts.SyntaxKind.SingleLineCommentTrivia : ts.SyntaxKind.MultiLineCommentTrivia; - const startPos = pos; - pos += 2; - if (nextChar === ts.CharacterCodes.slash) { - while (pos < text.length) { - if (isLineBreak(text.charCodeAt(pos))) { - hasTrailingNewLine = true; - break; - } - pos++; - } - } - else { - while (pos < text.length) { - if (text.charCodeAt(pos) === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { - pos += 2; - break; - } - pos++; - } - } + return true; +} - 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; - } - } +// Creates a scanner over a (possibly unspecified) range of a piece of text. +export function createScanner(languageVersion: ts.ScriptTarget, skipTrivia: boolean, languageVariant = ts.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: ts.SyntaxKind; + let tokenValue!: string; + let tokenFlags: ts.TokenFlags; + let commentDirectives: ts.CommentDirective[] | undefined; + 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 & ts.TokenFlags.UnicodeEscape) !== 0, + hasExtendedUnicodeEscape: () => (tokenFlags & ts.TokenFlags.ExtendedUnicodeEscape) !== 0, + hasPrecedingLineBreak: () => (tokenFlags & ts.TokenFlags.PrecedingLineBreak) !== 0, + hasPrecedingJSDocComment: () => (tokenFlags & ts.TokenFlags.PrecedingJSDocComment) !== 0, + isIdentifier: () => token === ts.SyntaxKind.Identifier || token > ts.SyntaxKind.LastReservedWord, + isReservedWord: () => token >= ts.SyntaxKind.FirstReservedWord && token <= ts.SyntaxKind.LastReservedWord, + isUnterminated: () => (tokenFlags & ts.TokenFlags.Unterminated) !== 0, + getCommentDirectives: () => commentDirectives, + getNumericLiteralFlags: () => tokenFlags & ts.TokenFlags.NumericLiteralFlags, + getTokenFlags: () => tokenFlags, + reScanGreaterToken, + reScanAsteriskEqualsToken, + reScanSlashToken, + reScanTemplateToken, + reScanTemplateHeadOrNoSubstitutionTemplate, + scanJsxIdentifier, + scanJsxAttributeValue, + reScanJsxAttributeValue, + reScanJsxToken, + reScanLessThanToken, + reScanHashToken, + reScanQuestionToken, + reScanInvalidIdentifier, + scanJsxToken, + scanJsDocToken, + scan, + getText, + clearCommentDirectives, + setText, + setScriptTarget, + setLanguageVariant, + setOnError, + setTextPos, + setInJSDocType, + tryScan, + lookAhead, + scanRange, + }; - pendingPos = startPos; - pendingEnd = pos; - pendingKind = kind; - pendingHasTrailingNewLine = hasTrailingNewLine; - hasPendingCommentRange = true; - } + if (ts.Debug.isDebugging) { + Object.defineProperty(scanner, "__debugShowCurrentPositionInText", { + get: () => { + const text = scanner.getText(); + return text.slice(0, scanner.getStartPos()) + "║" + text.slice(scanner.getStartPos()); + }, + }); + } - continue; - } - break scan; - default: - if (ch > ts.CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { - if (hasPendingCommentRange && isLineBreak(ch)) { - pendingHasTrailingNewLine = true; - } - pos++; - continue; - } - break scan; - } - } + return scanner; - if (hasPendingCommentRange) { - accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); + function error(message: ts.DiagnosticMessage): void; + function error(message: ts.DiagnosticMessage, errPos: number, length: number): void; + function error(message: ts.DiagnosticMessage, errPos: number = pos, length?: number): void { + if (onError) { + const oldPos = pos; + pos = errPos; + onError(message, length || 0); + pos = oldPos; } - - return accumulator; - } - - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.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: ts.CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; - export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; - export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { - return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ true, cb, state); + function scanNumberFragment(): string { + let start = pos; + let allowSeparator = false; + let isPreviousTokenSeparator = false; + let result = ""; + while (true) { + const ch = text.charCodeAt(pos); + if (ch === ts.CharacterCodes._) { + tokenFlags |= ts.TokenFlags.ContainsSeparator; + if (allowSeparator) { + allowSeparator = false; + isPreviousTokenSeparator = true; + result += text.substring(start, pos); + } + else if (isPreviousTokenSeparator) { + error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); + } + else { + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); + } + pos++; + start = pos; + continue; + } + if (isDigit(ch)) { + allowSeparator = true; + isPreviousTokenSeparator = false; + pos++; + continue; + } + break; + } + if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + } + return result + text.substring(start, pos); } - export function reduceEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { - return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ false, cb, state, initial); - } + function scanNumber(): { + type: ts.SyntaxKind; + value: string; + } { + const start = pos; + const mainFragment = scanNumberFragment(); + let decimalFragment: string | undefined; + let scientificFragment: string | undefined; + if (text.charCodeAt(pos) === ts.CharacterCodes.dot) { + pos++; + decimalFragment = scanNumberFragment(); + } + let end = pos; + if (text.charCodeAt(pos) === ts.CharacterCodes.E || text.charCodeAt(pos) === ts.CharacterCodes.e) { + pos++; + tokenFlags |= ts.TokenFlags.Scientific; + if (text.charCodeAt(pos) === ts.CharacterCodes.plus || text.charCodeAt(pos) === ts.CharacterCodes.minus) + pos++; + const preNumericPart = pos; + const finalFragment = scanNumberFragment(); + if (!finalFragment) { + error(ts.Diagnostics.Digit_expected); + } + else { + scientificFragment = text.substring(end, preNumericPart) + finalFragment; + end = pos; + } + } + let result: string; + if (tokenFlags & ts.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 + } - export function reduceEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { - return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ true, cb, state, initial); + if (decimalFragment !== undefined || tokenFlags & ts.TokenFlags.Scientific) { + checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & ts.TokenFlags.Scientific)); + return { + type: ts.SyntaxKind.NumericLiteral, + value: "" + +result // if value is not an integer, it can be safely coerced to a number + }; + } + else { + tokenValue = result; + const type = checkBigIntSuffix(); // if value is an integer, check whether it is a bigint + checkForIdentifierStartAfterNumericLiteral(start); + return { type, value: tokenValue }; + } } - function appendCommentRange(pos: number, end: number, kind: ts.CommentKind, hasTrailingNewLine: boolean, _state: any, comments: ts.CommentRange[]) { - if (!comments) { - comments = []; + function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { + if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) { + return; } - comments.push({ kind, pos, end, hasTrailingNewLine }); - return comments; - } - - export function getLeadingCommentRanges(text: string, pos: number): ts.CommentRange[] | undefined { - return reduceEachLeadingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); - } + const identifierStart = pos; + const { length } = scanIdentifierParts(); - export function getTrailingCommentRanges(text: string, pos: number): ts.CommentRange[] | undefined { - return reduceEachTrailingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); + if (length === 1 && text[identifierStart] === "n") { + if (isScientific) { + error(ts.Diagnostics.A_bigint_literal_cannot_use_exponential_notation, numericStart, identifierStart - numericStart + 1); + } + else { + error(ts.Diagnostics.A_bigint_literal_must_be_an_integer, numericStart, identifierStart - numericStart + 1); + } + } + else { + error(ts.Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); + pos = identifierStart; + } } - /** Optionally, get the shebang */ - export function getShebang(text: string): string | undefined { - const match = shebangTriviaRegex.exec(text); - if (match) { - return match[0]; + function scanOctalDigits(): number { + const start = pos; + while (isOctalDigit(text.charCodeAt(pos))) { + pos++; } + return +(text.substring(start, pos)); } - export function isIdentifierStart(ch: number, languageVersion: ts.ScriptTarget | undefined): boolean { - return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z || - ch === ts.CharacterCodes.$ || ch === ts.CharacterCodes._ || - ch > ts.CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion); + /** + * 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; } - export function isIdentifierPart(ch: number, languageVersion: ts.ScriptTarget | undefined, identifierVariant?: ts.LanguageVariant): boolean { - return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z || ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z || - ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9 || ch === ts.CharacterCodes.$ || ch === ts.CharacterCodes._ || - // "-" and ":" are valid in JSX Identifiers - (identifierVariant === ts.LanguageVariant.JSX ? (ch === ts.CharacterCodes.minus || ch === ts.CharacterCodes.colon) : false) || - ch > ts.CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); + /** + * 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); } - /* @internal */ - export function isIdentifierText(name: string, languageVersion: ts.ScriptTarget | undefined, identifierVariant?: ts.LanguageVariant): boolean { - let ch = codePointAt(name, 0); - if (!isIdentifierStart(ch, languageVersion)) { - return false; + 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 === ts.CharacterCodes._) { + tokenFlags |= ts.TokenFlags.ContainsSeparator; + if (allowSeparator) { + allowSeparator = false; + isPreviousTokenSeparator = true; + } + else if (isPreviousTokenSeparator) { + error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); + } + else { + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); + } + pos++; + continue; + } + allowSeparator = canHaveSeparators; + if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.F) { + ch += ts.CharacterCodes.a - ts.CharacterCodes.A; // standardize hex literals to lowercase + } + else if (!((ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9) || + (ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.f))) { + break; + } + valueChars.push(ch); + pos++; + isPreviousTokenSeparator = false; + } + if (valueChars.length < minCount) { + valueChars = []; + } + if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); } + return String.fromCharCode(...valueChars); + } - for (let i = charSize(ch); i < name.length; i += charSize(ch)) { - if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion, identifierVariant)) { - return false; + 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 |= ts.TokenFlags.Unterminated; + error(ts.Diagnostics.Unterminated_string_literal); + break; } + const ch = text.charCodeAt(pos); + if (ch === quote) { + result += text.substring(start, pos); + pos++; + break; + } + if (ch === ts.CharacterCodes.backslash && !jsxAttributeString) { + result += text.substring(start, pos); + result += scanEscapeSequence(); + start = pos; + continue; + } + if (isLineBreak(ch) && !jsxAttributeString) { + result += text.substring(start, pos); + tokenFlags |= ts.TokenFlags.Unterminated; + error(ts.Diagnostics.Unterminated_string_literal); + break; + } + pos++; } - - return true; + return result; } - // Creates a scanner over a (possibly unspecified) range of a piece of text. - export function createScanner(languageVersion: ts.ScriptTarget, skipTrivia: boolean, languageVariant = ts.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; + /** + * Sets the current 'tokenValue' and returns a NoSubstitutionTemplateLiteral or + * a literal component of a TemplateExpression. + */ + function scanTemplateAndSetTokenValue(isTaggedTemplate: boolean): ts.SyntaxKind { + const startedWithBacktick = text.charCodeAt(pos) === ts.CharacterCodes.backtick; - // Start position of text of current token - let tokenPos: number; + pos++; + let start = pos; + let contents = ""; + let resultingToken: ts.SyntaxKind; - let token: ts.SyntaxKind; - let tokenValue!: string; - let tokenFlags: ts.TokenFlags; - let commentDirectives: ts.CommentDirective[] | undefined; - let inJSDocType = 0; + while (true) { + if (pos >= end) { + contents += text.substring(start, pos); + tokenFlags |= ts.TokenFlags.Unterminated; + error(ts.Diagnostics.Unterminated_template_literal); + resultingToken = startedWithBacktick ? ts.SyntaxKind.NoSubstitutionTemplateLiteral : ts.SyntaxKind.TemplateTail; + break; + } - setText(text, start, length); + const currChar = text.charCodeAt(pos); - const scanner: Scanner = { - getStartPos: () => startPos, - getTextPos: () => pos, - getToken: () => token, - getTokenPos: () => tokenPos, - getTokenText: () => text.substring(tokenPos, pos), - getTokenValue: () => tokenValue, - hasUnicodeEscape: () => (tokenFlags & ts.TokenFlags.UnicodeEscape) !== 0, - hasExtendedUnicodeEscape: () => (tokenFlags & ts.TokenFlags.ExtendedUnicodeEscape) !== 0, - hasPrecedingLineBreak: () => (tokenFlags & ts.TokenFlags.PrecedingLineBreak) !== 0, - hasPrecedingJSDocComment: () => (tokenFlags & ts.TokenFlags.PrecedingJSDocComment) !== 0, - isIdentifier: () => token === ts.SyntaxKind.Identifier || token > ts.SyntaxKind.LastReservedWord, - isReservedWord: () => token >= ts.SyntaxKind.FirstReservedWord && token <= ts.SyntaxKind.LastReservedWord, - isUnterminated: () => (tokenFlags & ts.TokenFlags.Unterminated) !== 0, - getCommentDirectives: () => commentDirectives, - getNumericLiteralFlags: () => tokenFlags & ts.TokenFlags.NumericLiteralFlags, - getTokenFlags: () => tokenFlags, - reScanGreaterToken, - reScanAsteriskEqualsToken, - reScanSlashToken, - reScanTemplateToken, - reScanTemplateHeadOrNoSubstitutionTemplate, - scanJsxIdentifier, - scanJsxAttributeValue, - reScanJsxAttributeValue, - reScanJsxToken, - reScanLessThanToken, - reScanHashToken, - reScanQuestionToken, - reScanInvalidIdentifier, - scanJsxToken, - scanJsDocToken, - scan, - getText, - clearCommentDirectives, - setText, - setScriptTarget, - setLanguageVariant, - setOnError, - setTextPos, - setInJSDocType, - tryScan, - lookAhead, - scanRange, - }; - - if (ts.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: ts.DiagnosticMessage): void; - function error(message: ts.DiagnosticMessage, errPos: number, length: number): void; - function error(message: ts.DiagnosticMessage, errPos: number = pos, length?: number): void { - if (onError) { - const oldPos = pos; - pos = errPos; - onError(message, length || 0); - pos = oldPos; + // '`' + if (currChar === ts.CharacterCodes.backtick) { + contents += text.substring(start, pos); + pos++; + resultingToken = startedWithBacktick ? ts.SyntaxKind.NoSubstitutionTemplateLiteral : ts.SyntaxKind.TemplateTail; + break; } - } - function scanNumberFragment(): string { - let start = pos; - let allowSeparator = false; - let isPreviousTokenSeparator = false; - let result = ""; - while (true) { - const ch = text.charCodeAt(pos); - if (ch === ts.CharacterCodes._) { - tokenFlags |= ts.TokenFlags.ContainsSeparator; - if (allowSeparator) { - allowSeparator = false; - isPreviousTokenSeparator = true; - result += text.substring(start, pos); - } - else if (isPreviousTokenSeparator) { - error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } - pos++; - start = pos; - continue; - } - if (isDigit(ch)) { - allowSeparator = true; - isPreviousTokenSeparator = false; - pos++; - continue; - } + // '${' + if (currChar === ts.CharacterCodes.$ && pos + 1 < end && text.charCodeAt(pos + 1) === ts.CharacterCodes.openBrace) { + contents += text.substring(start, pos); + pos += 2; + resultingToken = startedWithBacktick ? ts.SyntaxKind.TemplateHead : ts.SyntaxKind.TemplateMiddle; break; } - if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { - error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); - } - return result + text.substring(start, pos); - } - function scanNumber(): { - type: ts.SyntaxKind; - value: string; - } { - const start = pos; - const mainFragment = scanNumberFragment(); - let decimalFragment: string | undefined; - let scientificFragment: string | undefined; - if (text.charCodeAt(pos) === ts.CharacterCodes.dot) { - pos++; - decimalFragment = scanNumberFragment(); + // Escape character + if (currChar === ts.CharacterCodes.backslash) { + contents += text.substring(start, pos); + contents += scanEscapeSequence(isTaggedTemplate); + start = pos; + continue; } - let end = pos; - if (text.charCodeAt(pos) === ts.CharacterCodes.E || text.charCodeAt(pos) === ts.CharacterCodes.e) { - pos++; - tokenFlags |= ts.TokenFlags.Scientific; - if (text.charCodeAt(pos) === ts.CharacterCodes.plus || text.charCodeAt(pos) === ts.CharacterCodes.minus) + + // Speculated ECMAScript 6 Spec 11.8.6.1: + // and LineTerminatorSequences are normalized to for Template Values + if (currChar === ts.CharacterCodes.carriageReturn) { + contents += text.substring(start, pos); pos++; - const preNumericPart = pos; - const finalFragment = scanNumberFragment(); - if (!finalFragment) { - error(ts.Diagnostics.Digit_expected); - } - else { - scientificFragment = text.substring(end, preNumericPart) + finalFragment; - end = pos; - } - } - let result: string; - if (tokenFlags & ts.TokenFlags.ContainsSeparator) { - result = mainFragment; - if (decimalFragment) { - result += "." + decimalFragment; - } - if (scientificFragment) { - result += scientificFragment; + + if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { + pos++; } - } - else { - result = text.substring(start, end); // No need to use all the fragments; no _ removal needed - } - if (decimalFragment !== undefined || tokenFlags & ts.TokenFlags.Scientific) { - checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & ts.TokenFlags.Scientific)); - return { - type: ts.SyntaxKind.NumericLiteral, - value: "" + +result // if value is not an integer, it can be safely coerced to a number - }; - } - else { - tokenValue = result; - const type = checkBigIntSuffix(); // if value is an integer, check whether it is a bigint - checkForIdentifierStartAfterNumericLiteral(start); - return { type, value: tokenValue }; + contents += "\n"; + start = pos; + continue; } + + pos++; } - function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { - if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) { - return; - } + ts.Debug.assert(resultingToken !== undefined); - const identifierStart = pos; - const { length } = scanIdentifierParts(); + tokenValue = contents; + return resultingToken; + } - if (length === 1 && text[identifierStart] === "n") { - if (isScientific) { - error(ts.Diagnostics.A_bigint_literal_cannot_use_exponential_notation, numericStart, identifierStart - numericStart + 1); - } - else { - error(ts.Diagnostics.A_bigint_literal_must_be_an_integer, numericStart, identifierStart - numericStart + 1); - } - } - else { - error(ts.Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); - pos = identifierStart; - } + function scanEscapeSequence(isTaggedTemplate?: boolean): string { + const start = pos; + pos++; + if (pos >= end) { + error(ts.Diagnostics.Unexpected_end_of_text); + return ""; } - - function scanOctalDigits(): number { - const start = pos; - while (isOctalDigit(text.charCodeAt(pos))) { - pos++; - } - 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 === ts.CharacterCodes._) { - tokenFlags |= ts.TokenFlags.ContainsSeparator; - if (allowSeparator) { - allowSeparator = false; - isPreviousTokenSeparator = true; - } - else if (isPreviousTokenSeparator) { - error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } + const ch = text.charCodeAt(pos); + pos++; + switch (ch) { + case ts.CharacterCodes._0: + // '\01' + if (isTaggedTemplate && pos < end && isDigit(text.charCodeAt(pos))) { pos++; - continue; - } - allowSeparator = canHaveSeparators; - if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.F) { - ch += ts.CharacterCodes.a - ts.CharacterCodes.A; // standardize hex literals to lowercase + tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); } - else if (!((ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9) || - (ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.f))) { - break; + return "\0"; + case ts.CharacterCodes.b: + return "\b"; + case ts.CharacterCodes.t: + return "\t"; + case ts.CharacterCodes.n: + return "\n"; + case ts.CharacterCodes.v: + return "\v"; + case ts.CharacterCodes.f: + return "\f"; + case ts.CharacterCodes.r: + return "\r"; + case ts.CharacterCodes.singleQuote: + return "\'"; + case ts.CharacterCodes.doubleQuote: + return "\""; + case ts.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) !== ts.CharacterCodes.openBrace) { + pos = escapePos; + tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); + } + } } - valueChars.push(ch); - pos++; - isPreviousTokenSeparator = false; - } - if (valueChars.length < minCount) { - valueChars = []; - } - if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { - error(ts.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 |= ts.TokenFlags.Unterminated; - error(ts.Diagnostics.Unterminated_string_literal); - break; - } - const ch = text.charCodeAt(pos); - if (ch === quote) { - result += text.substring(start, pos); - pos++; - break; - } - if (ch === ts.CharacterCodes.backslash && !jsxAttributeString) { - result += text.substring(start, pos); - result += scanEscapeSequence(); - start = pos; - continue; - } - if (isLineBreak(ch) && !jsxAttributeString) { - result += text.substring(start, pos); - tokenFlags |= ts.TokenFlags.Unterminated; - error(ts.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): ts.SyntaxKind { - const startedWithBacktick = text.charCodeAt(pos) === ts.CharacterCodes.backtick; - - pos++; - let start = pos; - let contents = ""; - let resultingToken: ts.SyntaxKind; - - while (true) { - if (pos >= end) { - contents += text.substring(start, pos); - tokenFlags |= ts.TokenFlags.Unterminated; - error(ts.Diagnostics.Unterminated_template_literal); - resultingToken = startedWithBacktick ? ts.SyntaxKind.NoSubstitutionTemplateLiteral : ts.SyntaxKind.TemplateTail; - break; - } - - const currChar = text.charCodeAt(pos); - - // '`' - if (currChar === ts.CharacterCodes.backtick) { - contents += text.substring(start, pos); - pos++; - resultingToken = startedWithBacktick ? ts.SyntaxKind.NoSubstitutionTemplateLiteral : ts.SyntaxKind.TemplateTail; - break; - } - - // '${' - if (currChar === ts.CharacterCodes.$ && pos + 1 < end && text.charCodeAt(pos + 1) === ts.CharacterCodes.openBrace) { - contents += text.substring(start, pos); - pos += 2; - resultingToken = startedWithBacktick ? ts.SyntaxKind.TemplateHead : ts.SyntaxKind.TemplateMiddle; - break; - } - - // Escape character - if (currChar === ts.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 === ts.CharacterCodes.carriageReturn) { - contents += text.substring(start, pos); + // '\u{DDDDDDDD}' + if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.openBrace) { pos++; - if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { - pos++; - } - - contents += "\n"; - start = pos; - continue; - } - - pos++; - } - - ts.Debug.assert(resultingToken !== undefined); - - tokenValue = contents; - return resultingToken; - } - - function scanEscapeSequence(isTaggedTemplate?: boolean): string { - const start = pos; - pos++; - if (pos >= end) { - error(ts.Diagnostics.Unexpected_end_of_text); - return ""; - } - const ch = text.charCodeAt(pos); - pos++; - switch (ch) { - case ts.CharacterCodes._0: - // '\01' - if (isTaggedTemplate && pos < end && isDigit(text.charCodeAt(pos))) { - pos++; + // '\u{' + if (isTaggedTemplate && !isHexDigit(text.charCodeAt(pos))) { tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; return text.substring(start, pos); } - return "\0"; - case ts.CharacterCodes.b: - return "\b"; - case ts.CharacterCodes.t: - return "\t"; - case ts.CharacterCodes.n: - return "\n"; - case ts.CharacterCodes.v: - return "\v"; - case ts.CharacterCodes.f: - return "\f"; - case ts.CharacterCodes.r: - return "\r"; - case ts.CharacterCodes.singleQuote: - return "\'"; - case ts.CharacterCodes.doubleQuote: - return "\""; - case ts.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) !== ts.CharacterCodes.openBrace) { - pos = escapePos; - tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - } - } - // '\u{DDDDDDDD}' - if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.openBrace) { - pos++; + const savePos = pos; + const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); + const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; - // '\u{' - if (isTaggedTemplate && !isHexDigit(text.charCodeAt(pos))) { + // '\u{Not Code Point' or '\u{CodePoint' + if (!isCodePoint(escapedValue) || text.charCodeAt(pos) !== ts.CharacterCodes.closeBrace) { tokenFlags |= ts.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) !== ts.CharacterCodes.closeBrace) { - tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - else { - pos = savePos; - } + else { + pos = savePos; } - tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; - return scanExtendedUnicodeEscape(); } + tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; + return scanExtendedUnicodeEscape(); + } - tokenFlags |= ts.TokenFlags.UnicodeEscape; - // '\uDDDD' - return scanHexadecimalEscape(/*numDigits*/ 4); + tokenFlags |= ts.TokenFlags.UnicodeEscape; + // '\uDDDD' + return scanHexadecimalEscape(/*numDigits*/ 4); - case ts.CharacterCodes.x: - if (isTaggedTemplate) { - if (!isHexDigit(text.charCodeAt(pos))) { - tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - else if (!isHexDigit(text.charCodeAt(pos + 1))) { - pos++; - tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } + case ts.CharacterCodes.x: + if (isTaggedTemplate) { + if (!isHexDigit(text.charCodeAt(pos))) { + tokenFlags |= ts.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 ts.CharacterCodes.carriageReturn: - if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { + else if (!isHexDigit(text.charCodeAt(pos + 1))) { pos++; + tokenFlags |= ts.TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); } - // falls through - case ts.CharacterCodes.lineFeed: - case ts.CharacterCodes.lineSeparator: - case ts.CharacterCodes.paragraphSeparator: - return ""; - default: - return String.fromCharCode(ch); - } - } - - function scanHexadecimalEscape(numDigits: number): string { - const escapedValue = scanExactNumberOfHexDigits(numDigits, /*canHaveSeparators*/ false); + } + // '\xDD' + return scanHexadecimalEscape(/*numDigits*/ 2); - if (escapedValue >= 0) { - return String.fromCharCode(escapedValue); - } - else { - error(ts.Diagnostics.Hexadecimal_digit_expected); + // 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 ts.CharacterCodes.carriageReturn: + if (pos < end && text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { + pos++; + } + // falls through + case ts.CharacterCodes.lineFeed: + case ts.CharacterCodes.lineSeparator: + case ts.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; + function scanHexadecimalEscape(numDigits: number): string { + const escapedValue = scanExactNumberOfHexDigits(numDigits, /*canHaveSeparators*/ false); - // Validate the value of the digit - if (escapedValue < 0) { - error(ts.Diagnostics.Hexadecimal_digit_expected); - isInvalidExtendedEscape = true; - } - else if (escapedValue > 0x10FFFF) { - error(ts.Diagnostics.An_extended_Unicode_escape_value_must_be_between_0x0_and_0x10FFFF_inclusive); - isInvalidExtendedEscape = true; - } + if (escapedValue >= 0) { + return String.fromCharCode(escapedValue); + } + else { + error(ts.Diagnostics.Hexadecimal_digit_expected); + return ""; + } + } - if (pos >= end) { - error(ts.Diagnostics.Unexpected_end_of_text); - isInvalidExtendedEscape = true; - } - else if (text.charCodeAt(pos) === ts.CharacterCodes.closeBrace) { - // Only swallow the following character up if it's a '}'. - pos++; - } - else { - error(ts.Diagnostics.Unterminated_Unicode_escape_sequence); - isInvalidExtendedEscape = true; - } + function scanExtendedUnicodeEscape(): string { + const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); + const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; + let isInvalidExtendedEscape = false; - if (isInvalidExtendedEscape) { - return ""; - } + // Validate the value of the digit + if (escapedValue < 0) { + error(ts.Diagnostics.Hexadecimal_digit_expected); + isInvalidExtendedEscape = true; + } + else if (escapedValue > 0x10FFFF) { + error(ts.Diagnostics.An_extended_Unicode_escape_value_must_be_between_0x0_and_0x10FFFF_inclusive); + isInvalidExtendedEscape = true; + } - return utf16EncodeAsString(escapedValue); + if (pos >= end) { + error(ts.Diagnostics.Unexpected_end_of_text); + isInvalidExtendedEscape = true; + } + else if (text.charCodeAt(pos) === ts.CharacterCodes.closeBrace) { + // Only swallow the following character up if it's a '}'. + pos++; + } + else { + error(ts.Diagnostics.Unterminated_Unicode_escape_sequence); + isInvalidExtendedEscape = true; } - // 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) === ts.CharacterCodes.u) { - const start = pos; - pos += 2; - const value = scanExactNumberOfHexDigits(4, /*canHaveSeparators*/ false); - pos = start; - return value; - } - return -1; + if (isInvalidExtendedEscape) { + return ""; } + return utf16EncodeAsString(escapedValue); + } - function peekExtendedUnicodeEscape(): number { - if (languageVersion >= ts.ScriptTarget.ES2015 && codePointAt(text, pos + 1) === ts.CharacterCodes.u && codePointAt(text, pos + 2) === ts.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; + // 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) === ts.CharacterCodes.u) { + const start = pos; + pos += 2; + const value = scanExactNumberOfHexDigits(4, /*canHaveSeparators*/ false); + pos = start; + return value; } + 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 === ts.CharacterCodes.backslash) { - ch = peekExtendedUnicodeEscape(); - if (ch >= 0 && isIdentifierPart(ch, languageVersion)) { - pos += 3; - tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; - result += scanExtendedUnicodeEscape(); - start = pos; - continue; - } - ch = peekUnicodeEscape(); - if (!(ch >= 0 && isIdentifierPart(ch, languageVersion))) { - break; - } - tokenFlags |= ts.TokenFlags.UnicodeEscape; - result += text.substring(start, pos); - result += utf16EncodeAsString(ch); - // Valid Unicode escape is always six characters - pos += 6; + + function peekExtendedUnicodeEscape(): number { + if (languageVersion >= ts.ScriptTarget.ES2015 && codePointAt(text, pos + 1) === ts.CharacterCodes.u && codePointAt(text, pos + 2) === ts.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 === ts.CharacterCodes.backslash) { + ch = peekExtendedUnicodeEscape(); + if (ch >= 0 && isIdentifierPart(ch, languageVersion)) { + pos += 3; + tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; + result += scanExtendedUnicodeEscape(); start = pos; + continue; } - else { + ch = peekUnicodeEscape(); + if (!(ch >= 0 && isIdentifierPart(ch, languageVersion))) { break; } + tokenFlags |= ts.TokenFlags.UnicodeEscape; + result += text.substring(start, pos); + result += utf16EncodeAsString(ch); + // Valid Unicode escape is always six characters + pos += 6; + start = pos; } - result += text.substring(start, pos); - return result; - } - - function getIdentifierToken(): ts.SyntaxKind.Identifier | ts.KeywordSyntaxKind { - // Reserved words are between 2 and 12 characters long and start with a lowercase letter - const len = tokenValue.length; - if (len >= 2 && len <= 12) { - const ch = tokenValue.charCodeAt(0); - if (ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z) { - const keyword = textToKeyword.get(tokenValue); - if (keyword !== undefined) { - return token = keyword; - } + else { + break; + } + } + result += text.substring(start, pos); + return result; + } + + function getIdentifierToken(): ts.SyntaxKind.Identifier | ts.KeywordSyntaxKind { + // Reserved words are between 2 and 12 characters long and start with a lowercase letter + const len = tokenValue.length; + if (len >= 2 && len <= 12) { + const ch = tokenValue.charCodeAt(0); + if (ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z) { + const keyword = textToKeyword.get(tokenValue); + if (keyword !== undefined) { + return token = keyword; } } - return token = ts.SyntaxKind.Identifier; } + return token = ts.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 === ts.CharacterCodes._) { - tokenFlags |= ts.TokenFlags.ContainsSeparator; - if (separatorAllowed) { - separatorAllowed = false; - isPreviousTokenSeparator = true; - } - else if (isPreviousTokenSeparator) { - error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } - pos++; - continue; + 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 === ts.CharacterCodes._) { + tokenFlags |= ts.TokenFlags.ContainsSeparator; + if (separatorAllowed) { + separatorAllowed = false; + isPreviousTokenSeparator = true; } - separatorAllowed = true; - if (!isDigit(ch) || ch - ts.CharacterCodes._0 >= base) { - break; + else if (isPreviousTokenSeparator) { + error(ts.Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); + } + else { + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); } - value += text[pos]; pos++; - isPreviousTokenSeparator = false; + continue; } - if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { - // Literal ends with underscore - not allowed - error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + separatorAllowed = true; + if (!isDigit(ch) || ch - ts.CharacterCodes._0 >= base) { + break; } - return value; + value += text[pos]; + pos++; + isPreviousTokenSeparator = false; } + if (text.charCodeAt(pos - 1) === ts.CharacterCodes._) { + // Literal ends with underscore - not allowed + error(ts.Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + } + return value; + } - function checkBigIntSuffix(): ts.SyntaxKind { - if (text.charCodeAt(pos) === ts.CharacterCodes.n) { - tokenValue += "n"; - // Use base 10 instead of base 2 or base 8 for shorter literals - if (tokenFlags & ts.TokenFlags.BinaryOrOctalSpecifier) { - tokenValue = ts.parsePseudoBigInt(tokenValue) + "n"; - } - pos++; - return ts.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 & ts.TokenFlags.BinarySpecifier - ? parseInt(tokenValue.slice(2), 2) // skip "0b" - : tokenFlags & ts.TokenFlags.OctalSpecifier - ? parseInt(tokenValue.slice(2), 8) // skip "0o" - : +tokenValue; - tokenValue = "" + numericValue; - return ts.SyntaxKind.NumericLiteral; + function checkBigIntSuffix(): ts.SyntaxKind { + if (text.charCodeAt(pos) === ts.CharacterCodes.n) { + tokenValue += "n"; + // Use base 10 instead of base 2 or base 8 for shorter literals + if (tokenFlags & ts.TokenFlags.BinaryOrOctalSpecifier) { + tokenValue = ts.parsePseudoBigInt(tokenValue) + "n"; } + pos++; + return ts.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 & ts.TokenFlags.BinarySpecifier + ? parseInt(tokenValue.slice(2), 2) // skip "0b" + : tokenFlags & ts.TokenFlags.OctalSpecifier + ? parseInt(tokenValue.slice(2), 8) // skip "0o" + : +tokenValue; + tokenValue = "" + numericValue; + return ts.SyntaxKind.NumericLiteral; } + } - function scan(): ts.SyntaxKind { - startPos = pos; - tokenFlags = ts.TokenFlags.None; - let asteriskSeen = false; - while (true) { - tokenPos = pos; - if (pos >= end) { - return token = ts.SyntaxKind.EndOfFileToken; + function scan(): ts.SyntaxKind { + startPos = pos; + tokenFlags = ts.TokenFlags.None; + let asteriskSeen = false; + while (true) { + tokenPos = pos; + if (pos >= end) { + return token = ts.SyntaxKind.EndOfFileToken; + } + const ch = codePointAt(text, pos); + + // Special handling for shebang + if (ch === ts.CharacterCodes.hash && pos === 0 && isShebangTrivia(text, pos)) { + pos = scanShebangTrivia(text, pos); + if (skipTrivia) { + continue; + } + else { + return token = ts.SyntaxKind.ShebangTrivia; } - const ch = codePointAt(text, pos); + } - // Special handling for shebang - if (ch === ts.CharacterCodes.hash && pos === 0 && isShebangTrivia(text, pos)) { - pos = scanShebangTrivia(text, pos); + switch (ch) { + case ts.CharacterCodes.lineFeed: + case ts.CharacterCodes.carriageReturn: + tokenFlags |= ts.TokenFlags.PrecedingLineBreak; if (skipTrivia) { + pos++; continue; } else { - return token = ts.SyntaxKind.ShebangTrivia; - } - } - - switch (ch) { - case ts.CharacterCodes.lineFeed: - case ts.CharacterCodes.carriageReturn: - tokenFlags |= ts.TokenFlags.PrecedingLineBreak; - if (skipTrivia) { - pos++; - continue; + if (ch === ts.CharacterCodes.carriageReturn && pos + 1 < end && text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { + // consume both CR and LF + pos += 2; } else { - if (ch === ts.CharacterCodes.carriageReturn && pos + 1 < end && text.charCodeAt(pos + 1) === ts.CharacterCodes.lineFeed) { - // consume both CR and LF - pos += 2; - } - else { - pos++; - } - return token = ts.SyntaxKind.NewLineTrivia; - } - case ts.CharacterCodes.tab: - case ts.CharacterCodes.verticalTab: - case ts.CharacterCodes.formFeed: - case ts.CharacterCodes.space: - case ts.CharacterCodes.nonBreakingSpace: - case ts.CharacterCodes.ogham: - case ts.CharacterCodes.enQuad: - case ts.CharacterCodes.emQuad: - case ts.CharacterCodes.enSpace: - case ts.CharacterCodes.emSpace: - case ts.CharacterCodes.threePerEmSpace: - case ts.CharacterCodes.fourPerEmSpace: - case ts.CharacterCodes.sixPerEmSpace: - case ts.CharacterCodes.figureSpace: - case ts.CharacterCodes.punctuationSpace: - case ts.CharacterCodes.thinSpace: - case ts.CharacterCodes.hairSpace: - case ts.CharacterCodes.zeroWidthSpace: - case ts.CharacterCodes.narrowNoBreakSpace: - case ts.CharacterCodes.mathematicalSpace: - case ts.CharacterCodes.ideographicSpace: - case ts.CharacterCodes.byteOrderMark: - if (skipTrivia) { pos++; - continue; - } - else { - while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { - pos++; - } - return token = ts.SyntaxKind.WhitespaceTrivia; - } - case ts.CharacterCodes.exclamation: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.ExclamationEqualsEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.ExclamationEqualsToken; - } - pos++; - return token = ts.SyntaxKind.ExclamationToken; - case ts.CharacterCodes.doubleQuote: - case ts.CharacterCodes.singleQuote: - tokenValue = scanString(); - return token = ts.SyntaxKind.StringLiteral; - case ts.CharacterCodes.backtick: - return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ false); - case ts.CharacterCodes.percent: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.PercentEqualsToken; } + return token = ts.SyntaxKind.NewLineTrivia; + } + case ts.CharacterCodes.tab: + case ts.CharacterCodes.verticalTab: + case ts.CharacterCodes.formFeed: + case ts.CharacterCodes.space: + case ts.CharacterCodes.nonBreakingSpace: + case ts.CharacterCodes.ogham: + case ts.CharacterCodes.enQuad: + case ts.CharacterCodes.emQuad: + case ts.CharacterCodes.enSpace: + case ts.CharacterCodes.emSpace: + case ts.CharacterCodes.threePerEmSpace: + case ts.CharacterCodes.fourPerEmSpace: + case ts.CharacterCodes.sixPerEmSpace: + case ts.CharacterCodes.figureSpace: + case ts.CharacterCodes.punctuationSpace: + case ts.CharacterCodes.thinSpace: + case ts.CharacterCodes.hairSpace: + case ts.CharacterCodes.zeroWidthSpace: + case ts.CharacterCodes.narrowNoBreakSpace: + case ts.CharacterCodes.mathematicalSpace: + case ts.CharacterCodes.ideographicSpace: + case ts.CharacterCodes.byteOrderMark: + if (skipTrivia) { pos++; - return token = ts.SyntaxKind.PercentToken; - case ts.CharacterCodes.ampersand: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.ampersand) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.AmpersandAmpersandEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.AmpersandAmpersandToken; + continue; + } + else { + while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { + pos++; } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.AmpersandEqualsToken; + return token = ts.SyntaxKind.WhitespaceTrivia; + } + case ts.CharacterCodes.exclamation: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.ExclamationEqualsEqualsToken; } - pos++; - return token = ts.SyntaxKind.AmpersandToken; - case ts.CharacterCodes.openParen: - pos++; - return token = ts.SyntaxKind.OpenParenToken; - case ts.CharacterCodes.closeParen: - pos++; - return token = ts.SyntaxKind.CloseParenToken; - case ts.CharacterCodes.asterisk: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.AsteriskEqualsToken; + return pos += 2, token = ts.SyntaxKind.ExclamationEqualsToken; + } + pos++; + return token = ts.SyntaxKind.ExclamationToken; + case ts.CharacterCodes.doubleQuote: + case ts.CharacterCodes.singleQuote: + tokenValue = scanString(); + return token = ts.SyntaxKind.StringLiteral; + case ts.CharacterCodes.backtick: + return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ false); + case ts.CharacterCodes.percent: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.PercentEqualsToken; + } + pos++; + return token = ts.SyntaxKind.PercentToken; + case ts.CharacterCodes.ampersand: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.ampersand) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.AmpersandAmpersandEqualsToken; } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.AsteriskAsteriskEqualsToken; + return pos += 2, token = ts.SyntaxKind.AmpersandAmpersandToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.AmpersandEqualsToken; + } + pos++; + return token = ts.SyntaxKind.AmpersandToken; + case ts.CharacterCodes.openParen: + pos++; + return token = ts.SyntaxKind.OpenParenToken; + case ts.CharacterCodes.closeParen: + pos++; + return token = ts.SyntaxKind.CloseParenToken; + case ts.CharacterCodes.asterisk: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.AsteriskEqualsToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.AsteriskAsteriskEqualsToken; + } + return pos += 2, token = ts.SyntaxKind.AsteriskAsteriskToken; + } + pos++; + if (inJSDocType && !asteriskSeen && (tokenFlags & ts.TokenFlags.PrecedingLineBreak)) { + // decoration at the start of a JSDoc comment line + asteriskSeen = true; + continue; + } + return token = ts.SyntaxKind.AsteriskToken; + case ts.CharacterCodes.plus: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.plus) { + return pos += 2, token = ts.SyntaxKind.PlusPlusToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.PlusEqualsToken; + } + pos++; + return token = ts.SyntaxKind.PlusToken; + case ts.CharacterCodes.comma: + pos++; + return token = ts.SyntaxKind.CommaToken; + case ts.CharacterCodes.minus: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.minus) { + return pos += 2, token = ts.SyntaxKind.MinusMinusToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.MinusEqualsToken; + } + pos++; + return token = ts.SyntaxKind.MinusToken; + case ts.CharacterCodes.dot: + if (isDigit(text.charCodeAt(pos + 1))) { + tokenValue = scanNumber().value; + return token = ts.SyntaxKind.NumericLiteral; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.dot && text.charCodeAt(pos + 2) === ts.CharacterCodes.dot) { + return pos += 3, token = ts.SyntaxKind.DotDotDotToken; + } + pos++; + return token = ts.SyntaxKind.DotToken; + case ts.CharacterCodes.slash: + // Single-line comment + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { + pos += 2; + + while (pos < end) { + if (isLineBreak(text.charCodeAt(pos))) { + break; } - return pos += 2, token = ts.SyntaxKind.AsteriskAsteriskToken; + pos++; } - pos++; - if (inJSDocType && !asteriskSeen && (tokenFlags & ts.TokenFlags.PrecedingLineBreak)) { - // decoration at the start of a JSDoc comment line - asteriskSeen = true; + + commentDirectives = appendIfCommentDirective(commentDirectives, text.slice(tokenPos, pos), commentDirectiveRegExSingleLine, tokenPos); + + if (skipTrivia) { continue; } - return token = ts.SyntaxKind.AsteriskToken; - case ts.CharacterCodes.plus: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.plus) { - return pos += 2, token = ts.SyntaxKind.PlusPlusToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.PlusEqualsToken; - } - pos++; - return token = ts.SyntaxKind.PlusToken; - case ts.CharacterCodes.comma: - pos++; - return token = ts.SyntaxKind.CommaToken; - case ts.CharacterCodes.minus: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.minus) { - return pos += 2, token = ts.SyntaxKind.MinusMinusToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.MinusEqualsToken; - } - pos++; - return token = ts.SyntaxKind.MinusToken; - case ts.CharacterCodes.dot: - if (isDigit(text.charCodeAt(pos + 1))) { - tokenValue = scanNumber().value; - return token = ts.SyntaxKind.NumericLiteral; + else { + return token = ts.SyntaxKind.SingleLineCommentTrivia; } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.dot && text.charCodeAt(pos + 2) === ts.CharacterCodes.dot) { - return pos += 3, token = ts.SyntaxKind.DotDotDotToken; + } + // Multi-line comment + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { + pos += 2; + if (text.charCodeAt(pos) === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== ts.CharacterCodes.slash) { + tokenFlags |= ts.TokenFlags.PrecedingJSDocComment; } - pos++; - return token = ts.SyntaxKind.DotToken; - case ts.CharacterCodes.slash: - // Single-line comment - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { - pos += 2; - while (pos < end) { - if (isLineBreak(text.charCodeAt(pos))) { - break; - } - pos++; + let commentClosed = false; + let lastLineStart = tokenPos; + while (pos < end) { + const ch = text.charCodeAt(pos); + + if (ch === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { + pos += 2; + commentClosed = true; + break; } - commentDirectives = appendIfCommentDirective(commentDirectives, text.slice(tokenPos, pos), commentDirectiveRegExSingleLine, tokenPos); + pos++; - if (skipTrivia) { - continue; - } - else { - return token = ts.SyntaxKind.SingleLineCommentTrivia; + if (isLineBreak(ch)) { + lastLineStart = pos; + tokenFlags |= ts.TokenFlags.PrecedingLineBreak; } } - // Multi-line comment - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.asterisk) { - pos += 2; - if (text.charCodeAt(pos) === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== ts.CharacterCodes.slash) { - tokenFlags |= ts.TokenFlags.PrecedingJSDocComment; - } - let commentClosed = false; - let lastLineStart = tokenPos; - while (pos < end) { - const ch = text.charCodeAt(pos); - - if (ch === ts.CharacterCodes.asterisk && text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { - pos += 2; - commentClosed = true; - break; - } - - pos++; - - if (isLineBreak(ch)) { - lastLineStart = pos; - tokenFlags |= ts.TokenFlags.PrecedingLineBreak; - } - } + commentDirectives = appendIfCommentDirective(commentDirectives, text.slice(lastLineStart, pos), commentDirectiveRegExMultiLine, lastLineStart); - commentDirectives = appendIfCommentDirective(commentDirectives, text.slice(lastLineStart, pos), commentDirectiveRegExMultiLine, lastLineStart); + if (!commentClosed) { + error(ts.Diagnostics.Asterisk_Slash_expected); + } + if (skipTrivia) { + continue; + } + else { if (!commentClosed) { - error(ts.Diagnostics.Asterisk_Slash_expected); - } - - if (skipTrivia) { - continue; - } - else { - if (!commentClosed) { - tokenFlags |= ts.TokenFlags.Unterminated; - } - return token = ts.SyntaxKind.MultiLineCommentTrivia; + tokenFlags |= ts.TokenFlags.Unterminated; } + return token = ts.SyntaxKind.MultiLineCommentTrivia; } + } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.SlashEqualsToken; - } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.SlashEqualsToken; + } - pos++; - return token = ts.SyntaxKind.SlashToken; - case ts.CharacterCodes._0: - if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.X || text.charCodeAt(pos + 1) === ts.CharacterCodes.x)) { - pos += 2; - tokenValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ true); - if (!tokenValue) { - error(ts.Diagnostics.Hexadecimal_digit_expected); - tokenValue = "0"; - } - tokenValue = "0x" + tokenValue; - tokenFlags |= ts.TokenFlags.HexSpecifier; - return token = checkBigIntSuffix(); + pos++; + return token = ts.SyntaxKind.SlashToken; + case ts.CharacterCodes._0: + if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.X || text.charCodeAt(pos + 1) === ts.CharacterCodes.x)) { + pos += 2; + tokenValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ true); + if (!tokenValue) { + error(ts.Diagnostics.Hexadecimal_digit_expected); + tokenValue = "0"; } - else if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.B || text.charCodeAt(pos + 1) === ts.CharacterCodes.b)) { - pos += 2; - tokenValue = scanBinaryOrOctalDigits(/* base */ 2); - if (!tokenValue) { - error(ts.Diagnostics.Binary_digit_expected); - tokenValue = "0"; - } - tokenValue = "0b" + tokenValue; - tokenFlags |= ts.TokenFlags.BinarySpecifier; - return token = checkBigIntSuffix(); + tokenValue = "0x" + tokenValue; + tokenFlags |= ts.TokenFlags.HexSpecifier; + return token = checkBigIntSuffix(); + } + else if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.B || text.charCodeAt(pos + 1) === ts.CharacterCodes.b)) { + pos += 2; + tokenValue = scanBinaryOrOctalDigits(/* base */ 2); + if (!tokenValue) { + error(ts.Diagnostics.Binary_digit_expected); + tokenValue = "0"; } - else if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.O || text.charCodeAt(pos + 1) === ts.CharacterCodes.o)) { - pos += 2; - tokenValue = scanBinaryOrOctalDigits(/* base */ 8); - if (!tokenValue) { - error(ts.Diagnostics.Octal_digit_expected); - tokenValue = "0"; - } - tokenValue = "0o" + tokenValue; - tokenFlags |= ts.TokenFlags.OctalSpecifier; - return token = checkBigIntSuffix(); + tokenValue = "0b" + tokenValue; + tokenFlags |= ts.TokenFlags.BinarySpecifier; + return token = checkBigIntSuffix(); + } + else if (pos + 2 < end && (text.charCodeAt(pos + 1) === ts.CharacterCodes.O || text.charCodeAt(pos + 1) === ts.CharacterCodes.o)) { + pos += 2; + tokenValue = scanBinaryOrOctalDigits(/* base */ 8); + if (!tokenValue) { + error(ts.Diagnostics.Octal_digit_expected); + tokenValue = "0"; } - // Try to parse as an octal - if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) { - tokenValue = "" + scanOctalDigits(); - tokenFlags |= ts.TokenFlags.Octal; - return token = ts.SyntaxKind.NumericLiteral; + tokenValue = "0o" + tokenValue; + tokenFlags |= ts.TokenFlags.OctalSpecifier; + return token = checkBigIntSuffix(); + } + // Try to parse as an octal + if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) { + tokenValue = "" + scanOctalDigits(); + tokenFlags |= ts.TokenFlags.Octal; + return token = ts.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 ts.CharacterCodes._1: + case ts.CharacterCodes._2: + case ts.CharacterCodes._3: + case ts.CharacterCodes._4: + case ts.CharacterCodes._5: + case ts.CharacterCodes._6: + case ts.CharacterCodes._7: + case ts.CharacterCodes._8: + case ts.CharacterCodes._9: + ({ type: token, value: tokenValue } = scanNumber()); + return token; + case ts.CharacterCodes.colon: + pos++; + return token = ts.SyntaxKind.ColonToken; + case ts.CharacterCodes.semicolon: + pos++; + return token = ts.SyntaxKind.SemicolonToken; + case ts.CharacterCodes.lessThan: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; } - // 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 ts.CharacterCodes._1: - case ts.CharacterCodes._2: - case ts.CharacterCodes._3: - case ts.CharacterCodes._4: - case ts.CharacterCodes._5: - case ts.CharacterCodes._6: - case ts.CharacterCodes._7: - case ts.CharacterCodes._8: - case ts.CharacterCodes._9: - ({ type: token, value: tokenValue } = scanNumber()); - return token; - case ts.CharacterCodes.colon: - pos++; - return token = ts.SyntaxKind.ColonToken; - case ts.CharacterCodes.semicolon: - pos++; - return token = ts.SyntaxKind.SemicolonToken; - case ts.CharacterCodes.lessThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = ts.SyntaxKind.ConflictMarkerTrivia; - } + else { + return token = ts.SyntaxKind.ConflictMarkerTrivia; } + } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lessThan) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.LessThanLessThanEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.LessThanLessThanToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.LessThanEqualsToken; + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.lessThan) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.LessThanLessThanEqualsToken; } - if (languageVariant === ts.LanguageVariant.JSX && - text.charCodeAt(pos + 1) === ts.CharacterCodes.slash && - text.charCodeAt(pos + 2) !== ts.CharacterCodes.asterisk) { - return pos += 2, token = ts.SyntaxKind.LessThanSlashToken; + return pos += 2, token = ts.SyntaxKind.LessThanLessThanToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.LessThanEqualsToken; + } + if (languageVariant === ts.LanguageVariant.JSX && + text.charCodeAt(pos + 1) === ts.CharacterCodes.slash && + text.charCodeAt(pos + 2) !== ts.CharacterCodes.asterisk) { + return pos += 2, token = ts.SyntaxKind.LessThanSlashToken; + } + pos++; + return token = ts.SyntaxKind.LessThanToken; + case ts.CharacterCodes.equals: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; } - pos++; - return token = ts.SyntaxKind.LessThanToken; - case ts.CharacterCodes.equals: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = ts.SyntaxKind.ConflictMarkerTrivia; - } + else { + return token = ts.SyntaxKind.ConflictMarkerTrivia; } + } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.EqualsEqualsEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.EqualsEqualsToken; + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.EqualsEqualsEqualsToken; } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.greaterThan) { - return pos += 2, token = ts.SyntaxKind.EqualsGreaterThanToken; + return pos += 2, token = ts.SyntaxKind.EqualsEqualsToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.greaterThan) { + return pos += 2, token = ts.SyntaxKind.EqualsGreaterThanToken; + } + pos++; + return token = ts.SyntaxKind.EqualsToken; + case ts.CharacterCodes.greaterThan: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; } - pos++; - return token = ts.SyntaxKind.EqualsToken; - case ts.CharacterCodes.greaterThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = ts.SyntaxKind.ConflictMarkerTrivia; - } + else { + return token = ts.SyntaxKind.ConflictMarkerTrivia; } + } - pos++; - return token = ts.SyntaxKind.GreaterThanToken; - case ts.CharacterCodes.question: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.dot && !isDigit(text.charCodeAt(pos + 2))) { - return pos += 2, token = ts.SyntaxKind.QuestionDotToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.question) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.QuestionQuestionEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.QuestionQuestionToken; + pos++; + return token = ts.SyntaxKind.GreaterThanToken; + case ts.CharacterCodes.question: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.dot && !isDigit(text.charCodeAt(pos + 2))) { + return pos += 2, token = ts.SyntaxKind.QuestionDotToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.question) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.QuestionQuestionEqualsToken; } - pos++; - return token = ts.SyntaxKind.QuestionToken; - case ts.CharacterCodes.openBracket: - pos++; - return token = ts.SyntaxKind.OpenBracketToken; - case ts.CharacterCodes.closeBracket: - pos++; - return token = ts.SyntaxKind.CloseBracketToken; - case ts.CharacterCodes.caret: - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.CaretEqualsToken; + return pos += 2, token = ts.SyntaxKind.QuestionQuestionToken; + } + pos++; + return token = ts.SyntaxKind.QuestionToken; + case ts.CharacterCodes.openBracket: + pos++; + return token = ts.SyntaxKind.OpenBracketToken; + case ts.CharacterCodes.closeBracket: + pos++; + return token = ts.SyntaxKind.CloseBracketToken; + case ts.CharacterCodes.caret: + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.CaretEqualsToken; + } + pos++; + return token = ts.SyntaxKind.CaretToken; + case ts.CharacterCodes.openBrace: + pos++; + return token = ts.SyntaxKind.OpenBraceToken; + case ts.CharacterCodes.bar: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; } - pos++; - return token = ts.SyntaxKind.CaretToken; - case ts.CharacterCodes.openBrace: - pos++; - return token = ts.SyntaxKind.OpenBraceToken; - case ts.CharacterCodes.bar: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = ts.SyntaxKind.ConflictMarkerTrivia; - } + else { + return token = ts.SyntaxKind.ConflictMarkerTrivia; } + } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.bar) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.BarBarEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.BarBarToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.BarEqualsToken; - } - pos++; - return token = ts.SyntaxKind.BarToken; - case ts.CharacterCodes.closeBrace: - pos++; - return token = ts.SyntaxKind.CloseBraceToken; - case ts.CharacterCodes.tilde: - pos++; - return token = ts.SyntaxKind.TildeToken; - case ts.CharacterCodes.at: - pos++; - return token = ts.SyntaxKind.AtToken; - case ts.CharacterCodes.backslash: - const extendedCookedChar = peekExtendedUnicodeEscape(); - if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { - pos += 3; - tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; - tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); - return token = getIdentifierToken(); + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.bar) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.BarBarEqualsToken; } + return pos += 2, token = ts.SyntaxKind.BarBarToken; + } + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.BarEqualsToken; + } + pos++; + return token = ts.SyntaxKind.BarToken; + case ts.CharacterCodes.closeBrace: + pos++; + return token = ts.SyntaxKind.CloseBraceToken; + case ts.CharacterCodes.tilde: + pos++; + return token = ts.SyntaxKind.TildeToken; + case ts.CharacterCodes.at: + pos++; + return token = ts.SyntaxKind.AtToken; + case ts.CharacterCodes.backslash: + const extendedCookedChar = peekExtendedUnicodeEscape(); + if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { + pos += 3; + tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; + tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); + return token = getIdentifierToken(); + } - const cookedChar = peekUnicodeEscape(); - if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { - pos += 6; - tokenFlags |= ts.TokenFlags.UnicodeEscape; - tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); - return token = getIdentifierToken(); - } + const cookedChar = peekUnicodeEscape(); + if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { + pos += 6; + tokenFlags |= ts.TokenFlags.UnicodeEscape; + tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); + return token = getIdentifierToken(); + } - error(ts.Diagnostics.Invalid_character); + error(ts.Diagnostics.Invalid_character); + pos++; + return token = ts.SyntaxKind.Unknown; + case ts.CharacterCodes.hash: + if (pos !== 0 && text[pos + 1] === "!") { + error(ts.Diagnostics.can_only_be_used_at_the_start_of_a_file); pos++; return token = ts.SyntaxKind.Unknown; - case ts.CharacterCodes.hash: - if (pos !== 0 && text[pos + 1] === "!") { - error(ts.Diagnostics.can_only_be_used_at_the_start_of_a_file); - pos++; - return token = ts.SyntaxKind.Unknown; - } + } - if (isIdentifierStart(codePointAt(text, pos + 1), languageVersion)) { - pos++; - scanIdentifier(codePointAt(text, pos), languageVersion); - } - else { - tokenValue = String.fromCharCode(codePointAt(text, pos)); - error(ts.Diagnostics.Invalid_character, pos++, charSize(ch)); - } - return token = ts.SyntaxKind.PrivateIdentifier; - default: - const identifierKind = scanIdentifier(ch, languageVersion); - if (identifierKind) { - return token = identifierKind; - } - else if (isWhiteSpaceSingleLine(ch)) { - pos += charSize(ch); - continue; - } - else if (isLineBreak(ch)) { - tokenFlags |= ts.TokenFlags.PrecedingLineBreak; - pos += charSize(ch); - continue; - } - const size = charSize(ch); - error(ts.Diagnostics.Invalid_character, pos, size); - pos += size; - return token = ts.SyntaxKind.Unknown; - } + if (isIdentifierStart(codePointAt(text, pos + 1), languageVersion)) { + pos++; + scanIdentifier(codePointAt(text, pos), languageVersion); + } + else { + tokenValue = String.fromCharCode(codePointAt(text, pos)); + error(ts.Diagnostics.Invalid_character, pos++, charSize(ch)); + } + return token = ts.SyntaxKind.PrivateIdentifier; + default: + const identifierKind = scanIdentifier(ch, languageVersion); + if (identifierKind) { + return token = identifierKind; + } + else if (isWhiteSpaceSingleLine(ch)) { + pos += charSize(ch); + continue; + } + else if (isLineBreak(ch)) { + tokenFlags |= ts.TokenFlags.PrecedingLineBreak; + pos += charSize(ch); + continue; + } + const size = charSize(ch); + error(ts.Diagnostics.Invalid_character, pos, size); + pos += size; + return token = ts.SyntaxKind.Unknown; } } + } - function reScanInvalidIdentifier(): ts.SyntaxKind { - ts.Debug.assert(token === ts.SyntaxKind.Unknown, "'reScanInvalidIdentifier' should only be called when the current token is 'SyntaxKind.Unknown'."); - pos = tokenPos = startPos; - tokenFlags = 0; - const ch = codePointAt(text, pos); - const identifierKind = scanIdentifier(ch, ts.ScriptTarget.ESNext); - if (identifierKind) { - return token = identifierKind; - } - pos += charSize(ch); - return token; // Still `SyntaKind.Unknown` - } + function reScanInvalidIdentifier(): ts.SyntaxKind { + ts.Debug.assert(token === ts.SyntaxKind.Unknown, "'reScanInvalidIdentifier' should only be called when the current token is 'SyntaxKind.Unknown'."); + pos = tokenPos = startPos; + tokenFlags = 0; + const ch = codePointAt(text, pos); + const identifierKind = scanIdentifier(ch, ts.ScriptTarget.ESNext); + if (identifierKind) { + return token = identifierKind; + } + pos += charSize(ch); + return token; // Still `SyntaKind.Unknown` + } - function scanIdentifier(startCharacter: number, languageVersion: ts.ScriptTarget) { - let ch = startCharacter; - if (isIdentifierStart(ch, languageVersion)) { + function scanIdentifier(startCharacter: number, languageVersion: ts.ScriptTarget) { + let ch = startCharacter; + if (isIdentifierStart(ch, languageVersion)) { + pos += charSize(ch); + while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) pos += charSize(ch); - while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) - pos += charSize(ch); - tokenValue = text.substring(tokenPos, pos); - if (ch === ts.CharacterCodes.backslash) { - tokenValue += scanIdentifierParts(); - } - return getIdentifierToken(); + tokenValue = text.substring(tokenPos, pos); + if (ch === ts.CharacterCodes.backslash) { + tokenValue += scanIdentifierParts(); } + return getIdentifierToken(); } + } - function reScanGreaterToken(): ts.SyntaxKind { - if (token === ts.SyntaxKind.GreaterThanToken) { - if (text.charCodeAt(pos) === ts.CharacterCodes.greaterThan) { - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.greaterThan) { - if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { - return pos += 3, token = ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; - } - return pos += 2, token = ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken; - } - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { - return pos += 2, token = ts.SyntaxKind.GreaterThanGreaterThanEqualsToken; + function reScanGreaterToken(): ts.SyntaxKind { + if (token === ts.SyntaxKind.GreaterThanToken) { + if (text.charCodeAt(pos) === ts.CharacterCodes.greaterThan) { + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.greaterThan) { + if (text.charCodeAt(pos + 2) === ts.CharacterCodes.equals) { + return pos += 3, token = ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; } - pos++; - return token = ts.SyntaxKind.GreaterThanGreaterThanToken; + return pos += 2, token = ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken; } - if (text.charCodeAt(pos) === ts.CharacterCodes.equals) { - pos++; - return token = ts.SyntaxKind.GreaterThanEqualsToken; + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.equals) { + return pos += 2, token = ts.SyntaxKind.GreaterThanGreaterThanEqualsToken; } + pos++; + return token = ts.SyntaxKind.GreaterThanGreaterThanToken; + } + if (text.charCodeAt(pos) === ts.CharacterCodes.equals) { + pos++; + return token = ts.SyntaxKind.GreaterThanEqualsToken; } - return token; } + return token; + } - function reScanAsteriskEqualsToken(): ts.SyntaxKind { - ts.Debug.assert(token === ts.SyntaxKind.AsteriskEqualsToken, "'reScanAsteriskEqualsToken' should only be called on a '*='"); - pos = tokenPos + 1; - return token = ts.SyntaxKind.EqualsToken; - } - - function reScanSlashToken(): ts.SyntaxKind { - if (token === ts.SyntaxKind.SlashToken || token === ts.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 |= ts.TokenFlags.Unterminated; - error(ts.Diagnostics.Unterminated_regular_expression_literal); - break; - } + function reScanAsteriskEqualsToken(): ts.SyntaxKind { + ts.Debug.assert(token === ts.SyntaxKind.AsteriskEqualsToken, "'reScanAsteriskEqualsToken' should only be called on a '*='"); + pos = tokenPos + 1; + return token = ts.SyntaxKind.EqualsToken; + } - const ch = text.charCodeAt(p); - if (isLineBreak(ch)) { - tokenFlags |= ts.TokenFlags.Unterminated; - error(ts.Diagnostics.Unterminated_regular_expression_literal); - break; - } + function reScanSlashToken(): ts.SyntaxKind { + if (token === ts.SyntaxKind.SlashToken || token === ts.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 |= ts.TokenFlags.Unterminated; + error(ts.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 === ts.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 === ts.CharacterCodes.openBracket) { - inCharacterClass = true; - } - else if (ch === ts.CharacterCodes.backslash) { - inEscape = true; - } - else if (ch === ts.CharacterCodes.closeBracket) { - inCharacterClass = false; - } - p++; + const ch = text.charCodeAt(p); + if (isLineBreak(ch)) { + tokenFlags |= ts.TokenFlags.Unterminated; + error(ts.Diagnostics.Unterminated_regular_expression_literal); + break; } - while (p < end && isIdentifierPart(text.charCodeAt(p), languageVersion)) { + if (inEscape) { + // Parsing an escape character; + // reset the flag and just advance to the next char. + inEscape = false; + } + else if (ch === ts.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 === ts.CharacterCodes.openBracket) { + inCharacterClass = true; + } + else if (ch === ts.CharacterCodes.backslash) { + inEscape = true; + } + else if (ch === ts.CharacterCodes.closeBracket) { + inCharacterClass = false; } - pos = p; - tokenValue = text.substring(tokenPos, pos); - token = ts.SyntaxKind.RegularExpressionLiteral; + p++; } - return token; - } - function appendIfCommentDirective(commentDirectives: ts.CommentDirective[] | undefined, text: string, commentDirectiveRegEx: RegExp, lineStart: number) { - const type = getDirectiveFromComment(ts.trimStringStart(text), commentDirectiveRegEx); - if (type === undefined) { - return commentDirectives; + while (p < end && isIdentifierPart(text.charCodeAt(p), languageVersion)) { + p++; } - - return ts.append(commentDirectives, { - range: { pos: lineStart, end: pos }, - type, - }); + pos = p; + tokenValue = text.substring(tokenPos, pos); + token = ts.SyntaxKind.RegularExpressionLiteral; } + return token; + } - function getDirectiveFromComment(text: string, commentDirectiveRegEx: RegExp) { - const match = commentDirectiveRegEx.exec(text); - if (!match) { - return undefined; - } - - switch (match[1]) { - case "ts-expect-error": - return ts.CommentDirectiveType.ExpectError; + function appendIfCommentDirective(commentDirectives: ts.CommentDirective[] | undefined, text: string, commentDirectiveRegEx: RegExp, lineStart: number) { + const type = getDirectiveFromComment(ts.trimStringStart(text), commentDirectiveRegEx); + if (type === undefined) { + return commentDirectives; + } - case "ts-ignore": - return ts.CommentDirectiveType.Ignore; - } + return ts.append(commentDirectives, { + range: { pos: lineStart, end: pos }, + type, + }); + } + function getDirectiveFromComment(text: string, commentDirectiveRegEx: RegExp) { + const match = commentDirectiveRegEx.exec(text); + if (!match) { return undefined; } - /** - * Unconditionally back up and scan a template expression portion. - */ - function reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind { - ts.Debug.assert(token === ts.SyntaxKind.CloseBraceToken, "'reScanTemplateToken' should only be called on a '}'"); - pos = tokenPos; - return token = scanTemplateAndSetTokenValue(isTaggedTemplate); + switch (match[1]) { + case "ts-expect-error": + return ts.CommentDirectiveType.ExpectError; + + case "ts-ignore": + return ts.CommentDirectiveType.Ignore; } - function reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind { - pos = tokenPos; - return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ true); + return undefined; + } + + /** + * Unconditionally back up and scan a template expression portion. + */ + function reScanTemplateToken(isTaggedTemplate: boolean): ts.SyntaxKind { + ts.Debug.assert(token === ts.SyntaxKind.CloseBraceToken, "'reScanTemplateToken' should only be called on a '}'"); + pos = tokenPos; + return token = scanTemplateAndSetTokenValue(isTaggedTemplate); + } + + function reScanTemplateHeadOrNoSubstitutionTemplate(): ts.SyntaxKind { + pos = tokenPos; + return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ true); + } + + function reScanJsxToken(allowMultilineJsxText = true): ts.JsxTokenSyntaxKind { + pos = tokenPos = startPos; + return token = scanJsxToken(allowMultilineJsxText); + } + + function reScanLessThanToken(): ts.SyntaxKind { + if (token === ts.SyntaxKind.LessThanLessThanToken) { + pos = tokenPos + 1; + return token = ts.SyntaxKind.LessThanToken; } + return token; + } - function reScanJsxToken(allowMultilineJsxText = true): ts.JsxTokenSyntaxKind { - pos = tokenPos = startPos; - return token = scanJsxToken(allowMultilineJsxText); + function reScanHashToken(): ts.SyntaxKind { + if (token === ts.SyntaxKind.PrivateIdentifier) { + pos = tokenPos + 1; + return token = ts.SyntaxKind.HashToken; } + return token; + } - function reScanLessThanToken(): ts.SyntaxKind { - if (token === ts.SyntaxKind.LessThanLessThanToken) { - pos = tokenPos + 1; - return token = ts.SyntaxKind.LessThanToken; - } - return token; + function reScanQuestionToken(): ts.SyntaxKind { + ts.Debug.assert(token === ts.SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'"); + pos = tokenPos + 1; + return token = ts.SyntaxKind.QuestionToken; + } + + function scanJsxToken(allowMultilineJsxText = true): ts.JsxTokenSyntaxKind { + startPos = tokenPos = pos; + + if (pos >= end) { + return token = ts.SyntaxKind.EndOfFileToken; } - function reScanHashToken(): ts.SyntaxKind { - if (token === ts.SyntaxKind.PrivateIdentifier) { - pos = tokenPos + 1; - return token = ts.SyntaxKind.HashToken; + let char = text.charCodeAt(pos); + if (char === ts.CharacterCodes.lessThan) { + if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { + pos += 2; + return token = ts.SyntaxKind.LessThanSlashToken; } - return token; + pos++; + return token = ts.SyntaxKind.LessThanToken; } - function reScanQuestionToken(): ts.SyntaxKind { - ts.Debug.assert(token === ts.SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'"); - pos = tokenPos + 1; - return token = ts.SyntaxKind.QuestionToken; + if (char === ts.CharacterCodes.openBrace) { + pos++; + return token = ts.SyntaxKind.OpenBraceToken; } - function scanJsxToken(allowMultilineJsxText = true): ts.JsxTokenSyntaxKind { - startPos = tokenPos = pos; + // First non-whitespace character on this line. + let firstNonWhitespace = 0; - if (pos >= end) { - return token = ts.SyntaxKind.EndOfFileToken; - } + // These initial values are special because the first line is: + // firstNonWhitespace = 0 to indicate that we want leading whitespace, - let char = text.charCodeAt(pos); + while (pos < end) { + char = text.charCodeAt(pos); + if (char === ts.CharacterCodes.openBrace) { + break; + } if (char === ts.CharacterCodes.lessThan) { - if (text.charCodeAt(pos + 1) === ts.CharacterCodes.slash) { - pos += 2; - return token = ts.SyntaxKind.LessThanSlashToken; + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + return token = ts.SyntaxKind.ConflictMarkerTrivia; } - pos++; - return token = ts.SyntaxKind.LessThanToken; + break; + } + if (char === ts.CharacterCodes.greaterThan) { + error(ts.Diagnostics.Unexpected_token_Did_you_mean_or_gt, pos, 1); + } + if (char === ts.CharacterCodes.closeBrace) { + error(ts.Diagnostics.Unexpected_token_Did_you_mean_or_rbrace, pos, 1); } - if (char === ts.CharacterCodes.openBrace) { - pos++; - return token = ts.SyntaxKind.OpenBraceToken; + // 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 (!allowMultilineJsxText && isLineBreak(char) && firstNonWhitespace > 0) { + // Stop JsxText on each line during formatting. This allows the formatter to + // indent each line correctly. + break; + } + else if (!isWhiteSpaceLike(char)) { + firstNonWhitespace = pos; } - // First non-whitespace character on this line. - let firstNonWhitespace = 0; + pos++; + } + + tokenValue = text.substring(startPos, pos); - // These initial values are special because the first line is: - // firstNonWhitespace = 0 to indicate that we want leading whitespace, + return firstNonWhitespace === -1 ? ts.SyntaxKind.JsxTextAllWhiteSpaces : ts.SyntaxKind.JsxText; + } + // Scans a JSX identifier; these differ from normal identifiers in that + // they allow dashes + function scanJsxIdentifier(): ts.SyntaxKind { + if (tokenIsIdentifierOrKeyword(token)) { + // An identifier or keyword has already been parsed - check for a `-` or a single instance of `:` 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. + let namespaceSeparator = false; while (pos < end) { - char = text.charCodeAt(pos); - if (char === ts.CharacterCodes.openBrace) { - break; - } - if (char === ts.CharacterCodes.lessThan) { - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - return token = ts.SyntaxKind.ConflictMarkerTrivia; - } - break; - } - if (char === ts.CharacterCodes.greaterThan) { - error(ts.Diagnostics.Unexpected_token_Did_you_mean_or_gt, pos, 1); - } - if (char === ts.CharacterCodes.closeBrace) { - error(ts.Diagnostics.Unexpected_token_Did_you_mean_or_rbrace, pos, 1); + const ch = text.charCodeAt(pos); + if (ch === ts.CharacterCodes.minus) { + tokenValue += "-"; + pos++; + continue; } - - // 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 (ch === ts.CharacterCodes.colon && !namespaceSeparator) { + tokenValue += ":"; + pos++; + namespaceSeparator = true; + token = ts.SyntaxKind.Identifier; // swap from keyword kind to identifier kind + continue; } - else if (!allowMultilineJsxText && isLineBreak(char) && firstNonWhitespace > 0) { - // Stop JsxText on each line during formatting. This allows the formatter to - // indent each line correctly. + const oldPos = pos; + tokenValue += scanIdentifierParts(); // reuse `scanIdentifierParts` so unicode escapes are handled + if (pos === oldPos) { break; } - else if (!isWhiteSpaceLike(char)) { - firstNonWhitespace = pos; - } - - pos++; } - - tokenValue = text.substring(startPos, pos); - - return firstNonWhitespace === -1 ? ts.SyntaxKind.JsxTextAllWhiteSpaces : ts.SyntaxKind.JsxText; - } - - // Scans a JSX identifier; these differ from normal identifiers in that - // they allow dashes - function scanJsxIdentifier(): ts.SyntaxKind { - if (tokenIsIdentifierOrKeyword(token)) { - // An identifier or keyword has already been parsed - check for a `-` or a single instance of `:` 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. - let namespaceSeparator = false; - while (pos < end) { - const ch = text.charCodeAt(pos); - if (ch === ts.CharacterCodes.minus) { - tokenValue += "-"; - pos++; - continue; - } - else if (ch === ts.CharacterCodes.colon && !namespaceSeparator) { - tokenValue += ":"; - pos++; - namespaceSeparator = true; - token = ts.SyntaxKind.Identifier; // swap from keyword kind to identifier kind - continue; - } - const oldPos = pos; - tokenValue += scanIdentifierParts(); // reuse `scanIdentifierParts` so unicode escapes are handled - if (pos === oldPos) { - break; - } - } - // Do not include a trailing namespace separator in the token, since this is against the spec. - if (tokenValue.slice(-1) === ":") { - tokenValue = tokenValue.slice(0, -1); - pos--; - } - return getIdentifierToken(); + // Do not include a trailing namespace separator in the token, since this is against the spec. + if (tokenValue.slice(-1) === ":") { + tokenValue = tokenValue.slice(0, -1); + pos--; } - return token; + return getIdentifierToken(); } + return token; + } - function scanJsxAttributeValue(): ts.SyntaxKind { - startPos = pos; - - switch (text.charCodeAt(pos)) { - case ts.CharacterCodes.doubleQuote: - case ts.CharacterCodes.singleQuote: - tokenValue = scanString(/*jsxAttributeString*/ true); - return token = ts.SyntaxKind.StringLiteral; - default: - // If this scans anything other than `{`, it's a parse error. - return scan(); - } - } + function scanJsxAttributeValue(): ts.SyntaxKind { + startPos = pos; - function reScanJsxAttributeValue(): ts.SyntaxKind { - pos = tokenPos = startPos; - return scanJsxAttributeValue(); + switch (text.charCodeAt(pos)) { + case ts.CharacterCodes.doubleQuote: + case ts.CharacterCodes.singleQuote: + tokenValue = scanString(/*jsxAttributeString*/ true); + return token = ts.SyntaxKind.StringLiteral; + default: + // If this scans anything other than `{`, it's a parse error. + return scan(); } + } - function scanJsDocToken(): ts.JSDocSyntaxKind { - startPos = tokenPos = pos; - tokenFlags = ts.TokenFlags.None; - if (pos >= end) { - return token = ts.SyntaxKind.EndOfFileToken; - } + function reScanJsxAttributeValue(): ts.SyntaxKind { + pos = tokenPos = startPos; + return scanJsxAttributeValue(); + } - const ch = codePointAt(text, pos); - pos += charSize(ch); - switch (ch) { - case ts.CharacterCodes.tab: - case ts.CharacterCodes.verticalTab: - case ts.CharacterCodes.formFeed: - case ts.CharacterCodes.space: - while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { - pos++; - } - return token = ts.SyntaxKind.WhitespaceTrivia; - case ts.CharacterCodes.at: - return token = ts.SyntaxKind.AtToken; - case ts.CharacterCodes.carriageReturn: - if (text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { - pos++; - } - // falls through - case ts.CharacterCodes.lineFeed: - tokenFlags |= ts.TokenFlags.PrecedingLineBreak; - return token = ts.SyntaxKind.NewLineTrivia; - case ts.CharacterCodes.asterisk: - return token = ts.SyntaxKind.AsteriskToken; - case ts.CharacterCodes.openBrace: - return token = ts.SyntaxKind.OpenBraceToken; - case ts.CharacterCodes.closeBrace: - return token = ts.SyntaxKind.CloseBraceToken; - case ts.CharacterCodes.openBracket: - return token = ts.SyntaxKind.OpenBracketToken; - case ts.CharacterCodes.closeBracket: - return token = ts.SyntaxKind.CloseBracketToken; - case ts.CharacterCodes.lessThan: - return token = ts.SyntaxKind.LessThanToken; - case ts.CharacterCodes.greaterThan: - return token = ts.SyntaxKind.GreaterThanToken; - case ts.CharacterCodes.equals: - return token = ts.SyntaxKind.EqualsToken; - case ts.CharacterCodes.comma: - return token = ts.SyntaxKind.CommaToken; - case ts.CharacterCodes.dot: - return token = ts.SyntaxKind.DotToken; - case ts.CharacterCodes.backtick: - return token = ts.SyntaxKind.BacktickToken; - case ts.CharacterCodes.hash: - return token = ts.SyntaxKind.HashToken; - case ts.CharacterCodes.backslash: - pos--; - const extendedCookedChar = peekExtendedUnicodeEscape(); - if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { - pos += 3; - tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; - tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); - return token = getIdentifierToken(); - } + function scanJsDocToken(): ts.JSDocSyntaxKind { + startPos = tokenPos = pos; + tokenFlags = ts.TokenFlags.None; + if (pos >= end) { + return token = ts.SyntaxKind.EndOfFileToken; + } - const cookedChar = peekUnicodeEscape(); - if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { - pos += 6; - tokenFlags |= ts.TokenFlags.UnicodeEscape; - tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); - return token = getIdentifierToken(); - } + const ch = codePointAt(text, pos); + pos += charSize(ch); + switch (ch) { + case ts.CharacterCodes.tab: + case ts.CharacterCodes.verticalTab: + case ts.CharacterCodes.formFeed: + case ts.CharacterCodes.space: + while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { pos++; - return token = ts.SyntaxKind.Unknown; - } + } + return token = ts.SyntaxKind.WhitespaceTrivia; + case ts.CharacterCodes.at: + return token = ts.SyntaxKind.AtToken; + case ts.CharacterCodes.carriageReturn: + if (text.charCodeAt(pos) === ts.CharacterCodes.lineFeed) { + pos++; + } + // falls through + case ts.CharacterCodes.lineFeed: + tokenFlags |= ts.TokenFlags.PrecedingLineBreak; + return token = ts.SyntaxKind.NewLineTrivia; + case ts.CharacterCodes.asterisk: + return token = ts.SyntaxKind.AsteriskToken; + case ts.CharacterCodes.openBrace: + return token = ts.SyntaxKind.OpenBraceToken; + case ts.CharacterCodes.closeBrace: + return token = ts.SyntaxKind.CloseBraceToken; + case ts.CharacterCodes.openBracket: + return token = ts.SyntaxKind.OpenBracketToken; + case ts.CharacterCodes.closeBracket: + return token = ts.SyntaxKind.CloseBracketToken; + case ts.CharacterCodes.lessThan: + return token = ts.SyntaxKind.LessThanToken; + case ts.CharacterCodes.greaterThan: + return token = ts.SyntaxKind.GreaterThanToken; + case ts.CharacterCodes.equals: + return token = ts.SyntaxKind.EqualsToken; + case ts.CharacterCodes.comma: + return token = ts.SyntaxKind.CommaToken; + case ts.CharacterCodes.dot: + return token = ts.SyntaxKind.DotToken; + case ts.CharacterCodes.backtick: + return token = ts.SyntaxKind.BacktickToken; + case ts.CharacterCodes.hash: + return token = ts.SyntaxKind.HashToken; + case ts.CharacterCodes.backslash: + pos--; + const extendedCookedChar = peekExtendedUnicodeEscape(); + if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { + pos += 3; + tokenFlags |= ts.TokenFlags.ExtendedUnicodeEscape; + tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); + return token = getIdentifierToken(); + } - if (isIdentifierStart(ch, languageVersion)) { - let char = ch; - while (pos < end && isIdentifierPart(char = codePointAt(text, pos), languageVersion) || text.charCodeAt(pos) === ts.CharacterCodes.minus) - pos += charSize(char); - tokenValue = text.substring(tokenPos, pos); - if (char === ts.CharacterCodes.backslash) { - tokenValue += scanIdentifierParts(); + const cookedChar = peekUnicodeEscape(); + if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { + pos += 6; + tokenFlags |= ts.TokenFlags.UnicodeEscape; + tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); + return token = getIdentifierToken(); } - return token = getIdentifierToken(); - } - else { + pos++; return token = ts.SyntaxKind.Unknown; - } } - 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; + if (isIdentifierStart(ch, languageVersion)) { + let char = ch; + while (pos < end && isIdentifierPart(char = codePointAt(text, pos), languageVersion) || text.charCodeAt(pos) === ts.CharacterCodes.minus) + pos += charSize(char); + tokenValue = text.substring(tokenPos, pos); + if (char === ts.CharacterCodes.backslash) { + tokenValue += scanIdentifierParts(); } - return result; + return token = getIdentifierToken(); } + else { + return token = ts.SyntaxKind.Unknown; + } + } - 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; - const saveErrorExpectations = commentDirectives; - - setText(text, start, length); - const result = callback(); - - end = saveEnd; + 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; - commentDirectives = saveErrorExpectations; - - return result; } + return result; + } - function lookAhead(callback: () => T): T { - return speculationHelper(callback, /*isLookahead*/ true); - } + 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; + const saveErrorExpectations = commentDirectives; - function tryScan(callback: () => T): T { - return speculationHelper(callback, /*isLookahead*/ false); - } + setText(text, start, length); + const result = callback(); - function getText(): string { - return text; - } + end = saveEnd; + pos = savePos; + startPos = saveStartPos; + tokenPos = saveTokenPos; + token = saveToken; + tokenValue = saveTokenValue; + tokenFlags = saveTokenFlags; + commentDirectives = saveErrorExpectations; - function clearCommentDirectives() { - commentDirectives = undefined; - } + return result; + } - function setText(newText: string | undefined, start: number | undefined, length: number | undefined) { - text = newText || ""; - end = length === undefined ? text.length : start! + length; - setTextPos(start || 0); - } + function lookAhead(callback: () => T): T { + return speculationHelper(callback, /*isLookahead*/ true); + } - function setOnError(errorCallback: ErrorCallback | undefined) { - onError = errorCallback; - } + function tryScan(callback: () => T): T { + return speculationHelper(callback, /*isLookahead*/ false); + } - function setScriptTarget(scriptTarget: ts.ScriptTarget) { - languageVersion = scriptTarget; - } + function getText(): string { + return text; + } - function setLanguageVariant(variant: ts.LanguageVariant) { - languageVariant = variant; - } + function clearCommentDirectives() { + commentDirectives = undefined; + } - function setTextPos(textPos: number) { - ts.Debug.assert(textPos >= 0); - pos = textPos; - startPos = textPos; - tokenPos = textPos; - token = ts.SyntaxKind.Unknown; - tokenValue = undefined!; - tokenFlags = ts.TokenFlags.None; - } + function setText(newText: string | undefined, start: number | undefined, length: number | undefined) { + text = newText || ""; + end = length === undefined ? text.length : start! + length; + setTextPos(start || 0); + } - function setInJSDocType(inType: boolean) { - inJSDocType += inType ? 1 : -1; - } + function setOnError(errorCallback: ErrorCallback | undefined) { + onError = errorCallback; } - /* @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; - }; + function setScriptTarget(scriptTarget: ts.ScriptTarget) { + languageVersion = scriptTarget; + } - /* @internal */ - function charSize(ch: number) { - if (ch >= 0x10000) { - return 2; - } - return 1; + function setLanguageVariant(variant: ts.LanguageVariant) { + languageVariant = variant; } - // Derived from the 10.1.1 UTF16Encoding of the ES6 Spec. - function utf16EncodeAsStringFallback(codePoint: number) { - ts.Debug.assert(0x0 <= codePoint && codePoint <= 0x10FFFF); + function setTextPos(textPos: number) { + ts.Debug.assert(textPos >= 0); + pos = textPos; + startPos = textPos; + tokenPos = textPos; + token = ts.SyntaxKind.Unknown; + tokenValue = undefined!; + tokenFlags = ts.TokenFlags.None; + } - if (codePoint <= 65535) { - return String.fromCharCode(codePoint); - } + function setInJSDocType(inType: boolean) { + inJSDocType += inType ? 1 : -1; + } +} - const codeUnit1 = Math.floor((codePoint - 65536) / 1024) + 0xD800; - const codeUnit2 = ((codePoint - 65536) % 1024) + 0xDC00; +/* @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; +}; - return String.fromCharCode(codeUnit1, codeUnit2); +/* @internal */ +function charSize(ch: number) { + if (ch >= 0x10000) { + return 2; } + return 1; +} - const utf16EncodeAsStringWorker: (codePoint: number) => string = (String as any).fromCodePoint ? codePoint => (String as any).fromCodePoint(codePoint) : utf16EncodeAsStringFallback; +// Derived from the 10.1.1 UTF16Encoding of the ES6 Spec. +function utf16EncodeAsStringFallback(codePoint: number) { + ts.Debug.assert(0x0 <= codePoint && codePoint <= 0x10FFFF); - /* @internal */ - export function utf16EncodeAsString(codePoint: number) { - return utf16EncodeAsStringWorker(codePoint); + 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 f3d18dcc24426..3022691e0c261 100644 --- a/src/compiler/semver.ts +++ b/src/compiler/semver.ts @@ -1,409 +1,409 @@ /* @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 = ts.Debug.checkDefined(tryParseComponents(major), "Invalid version"); - ({ major, minor, patch, prerelease, build } = result); - } - - ts.Debug.assert(major >= 0, "Invalid argument: major"); - ts.Debug.assert(minor >= 0, "Invalid argument: minor"); - ts.Debug.assert(patch >= 0, "Invalid argument: patch"); - ts.Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease"); - ts.Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build"); - this.major = major; - this.minor = minor; - this.patch = patch; - this.prerelease = prerelease ? prerelease.split(".") : ts.emptyArray; - this.build = build ? build.split(".") : ts.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 ts.Comparison.EqualTo; - if (other === undefined) - return ts.Comparison.GreaterThan; - return ts.compareValues(this.major, other.major) - || ts.compareValues(this.minor, other.minor) - || ts.compareValues(this.patch, other.patch) - || comparePrereleaseIdentifiers(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 ts.Debug.assertNever(field); - } +// 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 = ts.Debug.checkDefined(tryParseComponents(major), "Invalid version"); + ({ major, minor, patch, prerelease, build } = result); } - toString() { - let result = `${this.major}.${this.minor}.${this.patch}`; - if (ts.some(this.prerelease)) - result += `-${this.prerelease.join(".")}`; - if (ts.some(this.build)) - result += `+${this.build.join(".")}`; - return result; - } + ts.Debug.assert(major >= 0, "Invalid argument: major"); + ts.Debug.assert(minor >= 0, "Invalid argument: minor"); + ts.Debug.assert(patch >= 0, "Invalid argument: patch"); + ts.Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease"); + ts.Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build"); + this.major = major; + this.minor = minor; + this.patch = patch; + this.prerelease = prerelease ? prerelease.split(".") : ts.emptyArray; + this.build = build ? build.split(".") : ts.emptyArray; } - function tryParseComponents(text: string) { - const match = versionRegExp.exec(text); - if (!match) + static tryParse(text: string) { + const result = tryParseComponents(text); + if (!result) 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 - }; + const { major, minor, patch, prerelease, build } = result; + return new Version(major, minor, patch, prerelease, build); } - function comparePrereleaseIdentifiers(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 ts.Comparison.EqualTo; - if (left.length === 0) - return right.length === 0 ? ts.Comparison.EqualTo : ts.Comparison.GreaterThan; - if (right.length === 0) - return ts.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 ? ts.Comparison.LessThan : ts.Comparison.GreaterThan; - - // https://semver.org/#spec-item-11 - // > identifiers consisting of only digits are compared numerically - const result = ts.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 = ts.compareStringsCaseSensitive(leftIdentifier, rightIdentifier); - if (result) - return result; - } + // + // https://semver.org/#spec-item-11 + // > Build metadata does not figure into precedence + if (this === other) + return ts.Comparison.EqualTo; + if (other === undefined) + return ts.Comparison.GreaterThan; + return ts.compareValues(this.major, other.major) + || ts.compareValues(this.minor, other.minor) + || ts.compareValues(this.patch, other.patch) + || comparePrereleaseIdentifiers(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 ts.Debug.assertNever(field); } + } - // 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 ts.compareValues(left.length, right.length); + toString() { + let result = `${this.major}.${this.minor}.${this.patch}`; + if (ts.some(this.prerelease)) + result += `-${this.prerelease.join(".")}`; + if (ts.some(this.build)) + result += `+${this.build.join(".")}`; + return result; } +} - /** - * Describes a semantic version range, per https://github.com/npm/node-semver#ranges - */ - export class VersionRange { - private _alternatives: readonly (readonly Comparator[])[]; +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 + }; +} - constructor(spec: string) { - this._alternatives = spec ? ts.Debug.checkDefined(parseRange(spec), "Invalid range spec.") : ts.emptyArray; - } +function comparePrereleaseIdentifiers(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 ts.Comparison.EqualTo; + if (left.length === 0) + return right.length === 0 ? ts.Comparison.EqualTo : ts.Comparison.GreaterThan; + if (right.length === 0) + return ts.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 ? ts.Comparison.LessThan : ts.Comparison.GreaterThan; - static tryParse(text: string) { - const sets = parseRange(text); - if (sets) { - const range = new VersionRange(""); - range._alternatives = sets; - return range; - } - return undefined; + // https://semver.org/#spec-item-11 + // > identifiers consisting of only digits are compared numerically + const result = ts.compareValues(+leftIdentifier, +rightIdentifier); + if (result) + return result; } - - test(version: Version | string) { - if (typeof version === "string") - version = new Version(version); - return testDisjunction(version, this._alternatives); + else { + // https://semver.org/#spec-item-11 + // > identifiers with letters or hyphens are compared lexically in ASCII sort order. + const result = ts.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 ts.compareValues(left.length, right.length); +} + +/** + * 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 ? ts.Debug.checkDefined(parseRange(spec), "Invalid range spec.") : ts.emptyArray; + } - toString() { - return formatDisjunction(this._alternatives); + static tryParse(text: string) { + const sets = parseRange(text); + if (sets) { + const range = new VersionRange(""); + range._alternatives = sets; + return range; } + return undefined; } - interface Comparator { - readonly operator: "<" | "<=" | ">" | ">=" | "="; - readonly operand: Version; + test(version: Version | string) { + if (typeof version === "string") + version = new Version(version); + return testDisjunction(version, this._alternatives); } - // https://github.com/npm/node-semver#range-grammar - // - // range-set ::= range ( logical-or range ) * - // range ::= hyphen | simple ( ' ' simple ) * | '' - // logical-or ::= ( ' ' ) * '||' ( ' ' ) * - const logicalOrRegExp = /\|\|/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*([a-z0-9-+.*]+)$/i; - - function parseRange(text: string) { - const alternatives: Comparator[][] = []; - for (let range of ts.trimString(text).split(logicalOrRegExp)) { - if (!range) - continue; - const comparators: Comparator[] = []; - range = ts.trimString(range); - const match = hyphenRegExp.exec(range); - if (match) { - if (!parseHyphen(match[1], match[2], comparators)) + toString() { + return formatDisjunction(this._alternatives); + } +} + +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 ::= ( ' ' ) * '||' ( ' ' ) * +const logicalOrRegExp = /\|\|/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*([a-z0-9-+.*]+)$/i; + +function parseRange(text: string) { + const alternatives: Comparator[][] = []; + for (let range of ts.trimString(text).split(logicalOrRegExp)) { + if (!range) + continue; + const comparators: Comparator[] = []; + range = ts.trimString(range); + 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(ts.trimString(simple)); + if (!match || !parseComparator(match[1], match[2], comparators)) return undefined; } - else { - for (const simple of range.split(whitespaceRegExp)) { - const match = rangeRegExp.exec(ts.trimString(simple)); - if (!match || !parseComparator(match[1], match[2], comparators)) - return undefined; - } - } - alternatives.push(comparators); } - return alternatives; + alternatives.push(comparators); } + return alternatives; +} - 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); +function parsePartial(text: string) { + const match = partialRegExp.exec(text); + if (!match) + return undefined; - return { version, major, minor, patch }; - } + 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); - function parseHyphen(left: string, right: string, comparators: Comparator[]) { - const leftResult = parsePartial(left); - if (!leftResult) - return false; + return { version, major, minor, patch }; +} - const rightResult = parsePartial(right); - if (!rightResult) - return false; +function parseHyphen(left: string, right: string, comparators: Comparator[]) { + const leftResult = parsePartial(left); + if (!leftResult) + return false; - if (!isWildcard(leftResult.major)) { - comparators.push(createComparator(">=", leftResult.version)); - } + const rightResult = parsePartial(right); + if (!rightResult) + return false; - 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)); - } + if (!isWildcard(leftResult.major)) { + comparators.push(createComparator(">=", leftResult.version)); + } - 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 parseComparator(operator: string, text: string, comparators: Comparator[]) { - const result = parsePartial(text); - if (!result) - return false; + return true; +} - 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 "^": +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(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; - } - } - else if (operator === "<" || operator === ">") { - comparators.push(createComparator("<", Version.zero)); + comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor"))); + } + else { + comparators.push(createComparator("=", version)); + } + break; + default: + // unrecognized + return false; } - - return true; } - - function isWildcard(part: string) { - return part === "*" || part === "x" || part === "X"; + else if (operator === "<" || operator === ">") { + comparators.push(createComparator("<", Version.zero)); } - function createComparator(operator: Comparator["operator"], operand: Version) { - return { operator, operand }; - } + return true; +} - 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 false; - } +function isWildcard(part: string) { + return part === "*" || part === "x" || part === "X"; +} - function testAlternative(version: Version, comparators: readonly Comparator[]) { - for (const comparator of comparators) { - if (!testComparator(version, comparator.operator, comparator.operand)) - return false; - } +function createComparator(operator: Comparator["operator"], operand: Version) { + return { operator, operand }; +} + +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 false; +} - 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 ts.Debug.assertNever(operator); - } +function testAlternative(version: Version, comparators: readonly Comparator[]) { + for (const comparator of comparators) { + if (!testComparator(version, comparator.operator, comparator.operand)) + return false; } + return true; +} - function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) { - return ts.map(alternatives, formatAlternative).join(" || ") || "*"; +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 ts.Debug.assertNever(operator); } +} - function formatAlternative(comparators: readonly Comparator[]) { - return ts.map(comparators, formatComparator).join(" "); - } +function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) { + return ts.map(alternatives, formatAlternative).join(" || ") || "*"; +} - function formatComparator(comparator: Comparator) { - return `${comparator.operator}${comparator.operand}`; - } +function formatAlternative(comparators: readonly Comparator[]) { + return ts.map(comparators, formatComparator).join(" "); +} + +function formatComparator(comparator: Comparator) { + return `${comparator.operator}${comparator.operand}`; +} } diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 21168a764acfc..426cc0bd0f6d2 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -1,777 +1,777 @@ /* @internal */ namespace ts { - export interface SourceMapGeneratorOptions { - extendedDiagnostics?: boolean; - } - - export function createSourceMapGenerator(host: ts.EmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, generatorOptions: SourceMapGeneratorOptions): ts.SourceMapGenerator { - const { enter, exit } = generatorOptions.extendedDiagnostics - ? ts.performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap") - : ts.performance.nullTimer; - - // Current source map file and its index in the sources list - const rawSources: string[] = []; - const sources: string[] = []; - const sourceToSourceIndexMap = new ts.Map(); - let sourcesContent: (string | null)[] | undefined; - - const names: string[] = []; - let nameToNameIndexMap: ts.ESMap | undefined; - const mappingCharCodes: number[] = []; - 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; +export interface SourceMapGeneratorOptions { + extendedDiagnostics?: boolean; +} - return { - getSources: () => rawSources, - addSource, - setSourceContent, - addName, - addMapping, - appendSourceMap, - toJSON, - toString: () => JSON.stringify(toJSON()) - }; +export function createSourceMapGenerator(host: ts.EmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, generatorOptions: SourceMapGeneratorOptions): ts.SourceMapGenerator { + const { enter, exit } = generatorOptions.extendedDiagnostics + ? ts.performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap") + : ts.performance.nullTimer; + + // Current source map file and its index in the sources list + const rawSources: string[] = []; + const sources: string[] = []; + const sourceToSourceIndexMap = new ts.Map(); + let sourcesContent: (string | null)[] | undefined; + + const names: string[] = []; + let nameToNameIndexMap: ts.ESMap | undefined; + const mappingCharCodes: number[] = []; + 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 = ts.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; + function addSource(fileName: string) { + enter(); + const source = ts.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; + } - /* 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; + /* 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 = new ts.Map(); - 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 = new ts.Map(); + let nameIndex = nameToNameIndexMap.get(name); + if (nameIndex === undefined) { + nameIndex = names.length; + names.push(name); + nameToNameIndexMap.set(name, nameIndex); } + exit(); + return nameIndex; + } - function isNewGeneratedPosition(generatedLine: number, generatedCharacter: number) { - return !hasPending - || pendingGeneratedLine !== generatedLine - || pendingGeneratedCharacter !== generatedCharacter; - } + 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 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) { - ts.Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); - ts.Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); - ts.Debug.assert(sourceIndex === undefined || sourceIndex >= 0, "sourceIndex cannot be negative"); - ts.Debug.assert(sourceLine === undefined || sourceLine >= 0, "sourceLine cannot be negative"); - ts.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 addMapping(generatedLine: number, generatedCharacter: number, sourceIndex?: number, sourceLine?: number, sourceCharacter?: number, nameIndex?: number) { + ts.Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); + ts.Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); + ts.Debug.assert(sourceIndex === undefined || sourceIndex >= 0, "sourceIndex cannot be negative"); + ts.Debug.assert(sourceLine === undefined || sourceLine >= 0, "sourceLine cannot be negative"); + ts.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; + } - if (sourceIndex !== undefined && sourceLine !== undefined && sourceCharacter !== undefined) { - pendingSourceIndex = sourceIndex; - pendingSourceLine = sourceLine; - pendingSourceCharacter = sourceCharacter; - hasPendingSource = true; - if (nameIndex !== undefined) { - pendingNameIndex = nameIndex; - hasPendingName = true; - } + if (sourceIndex !== undefined && sourceLine !== undefined && sourceCharacter !== undefined) { + pendingSourceIndex = sourceIndex; + pendingSourceLine = sourceLine; + pendingSourceCharacter = sourceCharacter; + hasPendingSource = true; + if (nameIndex !== undefined) { + pendingNameIndex = nameIndex; + hasPendingName = true; } - exit(); } + exit(); + } - function appendSourceMap(generatedLine: number, generatedCharacter: number, map: ts.RawSourceMap, sourceMapPath: string, start?: ts.LineAndCharacter, end?: ts.LineAndCharacter) { - ts.Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); - ts.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; - } + function appendSourceMap(generatedLine: number, generatedCharacter: number, map: ts.RawSourceMap, sourceMapPath: string, start?: ts.LineAndCharacter, end?: ts.LineAndCharacter) { + ts.Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); + ts.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 ? ts.combinePaths(map.sourceRoot, rawPath) : rawPath; - const combinedPath = ts.combinePaths(ts.getDirectoryPath(sourceMapPath), relativePath); - sourceIndexToNewSourceIndexMap[raw.sourceIndex] = newSourceIndex = addSource(combinedPath); - if (map.sourcesContent && typeof map.sourcesContent[raw.sourceIndex] === "string") { - setSourceContent(newSourceIndex, map.sourcesContent[raw.sourceIndex]); - } + 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 ? ts.combinePaths(map.sourceRoot, rawPath) : rawPath; + const combinedPath = ts.combinePaths(ts.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); } + exit(); + } + + function shouldCommitMapping() { + return !hasLast + || lastGeneratedLine !== pendingGeneratedLine + || lastGeneratedCharacter !== pendingGeneratedCharacter + || lastSourceIndex !== pendingSourceIndex + || lastSourceLine !== pendingSourceLine + || lastSourceCharacter !== pendingSourceCharacter + || lastNameIndex !== pendingNameIndex; + } - function shouldCommitMapping() { - return !hasLast - || lastGeneratedLine !== pendingGeneratedLine - || lastGeneratedCharacter !== pendingGeneratedCharacter - || lastSourceIndex !== pendingSourceIndex - || lastSourceLine !== pendingSourceLine - || lastSourceCharacter !== pendingSourceCharacter - || lastNameIndex !== pendingNameIndex; + function appendMappingCharCode(charCode: number) { + mappingCharCodes.push(charCode); + // String.fromCharCode accepts its arguments on the stack, so we have to chunk the input, + // otherwise we can get stack overflows for large source maps + if (mappingCharCodes.length >= 1024) { + flushMappingBuffer(); } + } - function appendMappingCharCode(charCode: number) { - mappingCharCodes.push(charCode); - // String.fromCharCode accepts its arguments on the stack, so we have to chunk the input, - // otherwise we can get stack overflows for large source maps - if (mappingCharCodes.length >= 1024) { - flushMappingBuffer(); - } + function commitPendingMapping() { + if (!hasPending || !shouldCommitMapping()) { + return; } - function commitPendingMapping() { - if (!hasPending || !shouldCommitMapping()) { - return; - } + enter(); - enter(); - - // Line/Comma delimiters - if (lastGeneratedLine < pendingGeneratedLine) { - // Emit line delimiters - do { - appendMappingCharCode(ts.CharacterCodes.semicolon); - lastGeneratedLine++; - } while (lastGeneratedLine < pendingGeneratedLine); - // Only need to set this once - lastGeneratedCharacter = 0; - } - else { - ts.Debug.assertEqual(lastGeneratedLine, pendingGeneratedLine, "generatedLine cannot backtrack"); - // Emit comma to separate the entry - if (hasLast) { - appendMappingCharCode(ts.CharacterCodes.comma); - } + // Line/Comma delimiters + if (lastGeneratedLine < pendingGeneratedLine) { + // Emit line delimiters + do { + appendMappingCharCode(ts.CharacterCodes.semicolon); + lastGeneratedLine++; + } while (lastGeneratedLine < pendingGeneratedLine); + // Only need to set this once + lastGeneratedCharacter = 0; + } + else { + ts.Debug.assertEqual(lastGeneratedLine, pendingGeneratedLine, "generatedLine cannot backtrack"); + // Emit comma to separate the entry + if (hasLast) { + appendMappingCharCode(ts.CharacterCodes.comma); } + } - // 1. Relative generated character - appendBase64VLQ(pendingGeneratedCharacter - lastGeneratedCharacter); - lastGeneratedCharacter = pendingGeneratedCharacter; + // 1. Relative generated character + appendBase64VLQ(pendingGeneratedCharacter - lastGeneratedCharacter); + lastGeneratedCharacter = pendingGeneratedCharacter; - if (hasPendingSource) { - // 2. Relative sourceIndex - appendBase64VLQ(pendingSourceIndex - lastSourceIndex); - lastSourceIndex = pendingSourceIndex; + if (hasPendingSource) { + // 2. Relative sourceIndex + appendBase64VLQ(pendingSourceIndex - lastSourceIndex); + lastSourceIndex = pendingSourceIndex; - // 3. Relative source line - appendBase64VLQ(pendingSourceLine - lastSourceLine); - lastSourceLine = pendingSourceLine; + // 3. Relative source line + appendBase64VLQ(pendingSourceLine - lastSourceLine); + lastSourceLine = pendingSourceLine; - // 4. Relative source character - appendBase64VLQ(pendingSourceCharacter - lastSourceCharacter); - lastSourceCharacter = pendingSourceCharacter; + // 4. Relative source character + appendBase64VLQ(pendingSourceCharacter - lastSourceCharacter); + lastSourceCharacter = pendingSourceCharacter; - if (hasPendingName) { - // 5. Relative nameIndex - appendBase64VLQ(pendingNameIndex - lastNameIndex); - lastNameIndex = pendingNameIndex; - } + if (hasPendingName) { + // 5. Relative nameIndex + appendBase64VLQ(pendingNameIndex - lastNameIndex); + lastNameIndex = pendingNameIndex; } - - hasLast = true; - exit(); } - function flushMappingBuffer(): void { - if (mappingCharCodes.length > 0) { - mappings += String.fromCharCode.apply(undefined, mappingCharCodes); - mappingCharCodes.length = 0; - } - } + hasLast = true; + exit(); + } - function toJSON(): ts.RawSourceMap { - commitPendingMapping(); - flushMappingBuffer(); - return { - version: 3, - file, - sourceRoot, - sources, - names, - mappings, - sourcesContent, - }; + function flushMappingBuffer(): void { + if (mappingCharCodes.length > 0) { + mappings += String.fromCharCode.apply(undefined, mappingCharCodes); + mappingCharCodes.length = 0; } + } - function appendBase64VLQ(inValue: number): void { - // 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; - } + function toJSON(): ts.RawSourceMap { + commitPendingMapping(); + flushMappingBuffer(); + return { + version: 3, + file, + sourceRoot, + sources, + names, + mappings, + sourcesContent, + }; + } - // Encode 5 bits at a time starting from least significant bits - 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; - } - appendMappingCharCode(base64FormatEncode(currentDigit)); - } while (inValue > 0); + function appendBase64VLQ(inValue: number): void { + // 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 + 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; + } + appendMappingCharCode(base64FormatEncode(currentDigit)); + } while (inValue > 0); } +} - // 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=(.+)\r?\n?$/; +// 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=(.+)\r?\n?$/; - const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; +const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; - export interface LineInfo { - getLineCount(): number; - getLineText(line: number): string; - } +export interface LineInfo { + getLineCount(): number; + getLineText(line: number): string; +} - export function getLineInfo(text: string, lineStarts: readonly number[]): LineInfo { - return { - getLineCount: () => lineStarts.length, - getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1]) - }; - } +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. - */ - 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 ts.trimStringEnd(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; - } +/** + * 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 ts.trimStringEnd(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; - } +/* eslint-disable no-null/no-null */ +function isStringOrNull(x: any) { + return typeof x === "string" || x === null; +} - export function isRawSourceMap(x: any): x is ts.RawSourceMap { - return x !== null - && typeof x === "object" - && x.version === 3 - && typeof x.file === "string" - && typeof x.mappings === "string" - && ts.isArray(x.sources) && ts.every(x.sources, ts.isString) - && (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === "string") - && (x.sourcesContent === undefined || x.sourcesContent === null || ts.isArray(x.sourcesContent) && ts.every(x.sourcesContent, isStringOrNull)) - && (x.names === undefined || x.names === null || ts.isArray(x.names) && ts.every(x.names, ts.isString)); - } - /* eslint-enable no-null/no-null */ +export function isRawSourceMap(x: any): x is ts.RawSourceMap { + return x !== null + && typeof x === "object" + && x.version === 3 + && typeof x.file === "string" + && typeof x.mappings === "string" + && ts.isArray(x.sources) && ts.every(x.sources, ts.isString) + && (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === "string") + && (x.sourcesContent === undefined || x.sourcesContent === null || ts.isArray(x.sourcesContent) && ts.every(x.sourcesContent, isStringOrNull)) + && (x.names === undefined || x.names === null || ts.isArray(x.names) && ts.every(x.names, ts.isString)); +} +/* eslint-enable no-null/no-null */ - export function tryParseRawSourceMap(text: string) { - try { - const parsed = JSON.parse(text); - if (isRawSourceMap(parsed)) { - return parsed; - } +export function tryParseRawSourceMap(text: string) { + try { + const parsed = JSON.parse(text); + if (isRawSourceMap(parsed)) { + return parsed; } - catch { - // empty - } - - return undefined; } - - export interface MappingsDecoder extends ts.Iterator { - readonly pos: number; - readonly error: string | undefined; - readonly state: Required; + catch { + // empty } - export interface Mapping { - generatedLine: number; - generatedCharacter: number; - sourceIndex?: number; - sourceLine?: number; - sourceCharacter?: number; - nameIndex?: number; - } + return undefined; +} - export interface SourceMapping extends Mapping { - sourceIndex: number; - sourceLine: number; - sourceCharacter: number; - } +export interface MappingsDecoder extends ts.Iterator { + readonly pos: number; + readonly error: string | undefined; + readonly state: Required; +} - 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; +export interface Mapping { + generatedLine: number; + generatedCharacter: number; + sourceIndex?: number; + sourceLine?: number; + sourceCharacter?: number; + nameIndex?: number; +} - 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 === ts.CharacterCodes.semicolon) { - // new line - generatedLine++; - generatedCharacter = 0; - pos++; - continue; - } +export interface SourceMapping extends Mapping { + sourceIndex: number; + sourceLine: number; + sourceCharacter: number; +} - if (ch === ts.CharacterCodes.comma) { - // Next entry is on same line - no action needed - pos++; - continue; - } +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 === ts.CharacterCodes.semicolon) { + // new line + generatedLine++; + generatedCharacter = 0; + pos++; + continue; + } - let hasSource = false; - let hasName = false; + if (ch === ts.CharacterCodes.comma) { + // Next entry is on same line - no action needed + pos++; + continue; + } + + let hasSource = false; + let hasName = false; - generatedCharacter += base64VLQFormatDecode(); + 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 (generatedCharacter < 0) - return setErrorAndStopIterating("Invalid generatedCharacter found"); - - if (!isSourceMappingSegmentEnd()) { - hasSource = true; + if (sourceIndex < 0) + return setErrorAndStopIterating("Invalid sourceIndex found"); + if (isSourceMappingSegmentEnd()) + return setErrorAndStopIterating("Unsupported Format: No entries after sourceIndex"); - 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"); - 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"); - sourceCharacter += base64VLQFormatDecode(); + if (!isSourceMappingSegmentEnd()) { + hasName = true; + nameIndex += 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"); - } + 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(); + return { value: captureMapping(hasSource, hasName), done }; } - }; - 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 - }; + return stopIterating(); } + }; - function stopIterating(): { - value: never; - done: true; - } { - done = true; - return { value: undefined!, done: true }; - } + 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 setError(message: string) { - if (error === undefined) { - error = message; - } - } + function stopIterating(): { + value: never; + done: true; + } { + done = true; + return { value: undefined!, done: true }; + } - function setErrorAndStopIterating(message: string) { - setError(message); - return stopIterating(); + function setError(message: string) { + if (error === undefined) { + error = message; } + } - function hasReportedError() { - return error !== undefined; - } + function setErrorAndStopIterating(message: string) { + setError(message); + return stopIterating(); + } - function isSourceMappingSegmentEnd() { - return (pos === mappings.length || - mappings.charCodeAt(pos) === ts.CharacterCodes.comma || - mappings.charCodeAt(pos) === ts.CharacterCodes.semicolon); - } + function hasReportedError() { + return error !== undefined; + } - function base64VLQFormatDecode(): number { - let moreDigits = true; - let shiftCount = 0; - let value = 0; + function isSourceMappingSegmentEnd() { + return (pos === mappings.length || + mappings.charCodeAt(pos) === ts.CharacterCodes.comma || + mappings.charCodeAt(pos) === ts.CharacterCodes.semicolon); + } - for (; moreDigits; pos++) { - if (pos >= mappings.length) - return setError("Error in decoding base64VLQFormatDecode, past the mapping string"), -1; + function base64VLQFormatDecode(): number { + let moreDigits = true; + let shiftCount = 0; + let value = 0; - // 6 digit number - const currentByte = base64FormatDecode(mappings.charCodeAt(pos)); - if (currentByte === -1) - return setError("Invalid character in VLQ"), -1; + for (; moreDigits; pos++) { + if (pos >= mappings.length) + return setError("Error in decoding base64VLQFormatDecode, past the mapping string"), -1; - // If msb is set, we still have more bits to continue - moreDigits = (currentByte & 32) !== 0; + // 6 digit number + const currentByte = base64FormatDecode(mappings.charCodeAt(pos)); + if (currentByte === -1) + return setError("Invalid character in VLQ"), -1; - // least significant 5 bits are the next msbs in the final value. - value = value | ((currentByte & 31) << shiftCount); - shiftCount += 5; - } + // If msb is set, we still have more bits to continue + moreDigits = (currentByte & 32) !== 0; - // 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; - } + // least significant 5 bits are the next msbs in the final value. + value = value | ((currentByte & 31) << shiftCount); + shiftCount += 5; + } - 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; } - } - 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; + return value; } +} - export function isSourceMapping(mapping: Mapping): mapping is SourceMapping { - return mapping.sourceIndex !== undefined - && mapping.sourceLine !== undefined - && mapping.sourceCharacter !== undefined; - } +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; +} - function base64FormatEncode(value: number) { - return value >= 0 && value < 26 ? ts.CharacterCodes.A + value : - value >= 26 && value < 52 ? ts.CharacterCodes.a + value - 26 : - value >= 52 && value < 62 ? ts.CharacterCodes._0 + value - 52 : - value === 62 ? ts.CharacterCodes.plus : - value === 63 ? ts.CharacterCodes.slash : - ts.Debug.fail(`${value}: not a base64 value`); - } +export function isSourceMapping(mapping: Mapping): mapping is SourceMapping { + return mapping.sourceIndex !== undefined + && mapping.sourceLine !== undefined + && mapping.sourceCharacter !== undefined; +} - function base64FormatDecode(ch: number) { - return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z ? ch - ts.CharacterCodes.A : - ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z ? ch - ts.CharacterCodes.a + 26 : - ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9 ? ch - ts.CharacterCodes._0 + 52 : - ch === ts.CharacterCodes.plus ? 62 : - ch === ts.CharacterCodes.slash ? 63 : - -1; - } +function base64FormatEncode(value: number) { + return value >= 0 && value < 26 ? ts.CharacterCodes.A + value : + value >= 26 && value < 52 ? ts.CharacterCodes.a + value - 26 : + value >= 52 && value < 62 ? ts.CharacterCodes._0 + value - 52 : + value === 62 ? ts.CharacterCodes.plus : + value === 63 ? ts.CharacterCodes.slash : + ts.Debug.fail(`${value}: not a base64 value`); +} - interface MappedPosition { - generatedPosition: number; - source: string | undefined; - sourceIndex: number | undefined; - sourcePosition: number | undefined; - nameIndex: number | undefined; - } +function base64FormatDecode(ch: number) { + return ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z ? ch - ts.CharacterCodes.A : + ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z ? ch - ts.CharacterCodes.a + 26 : + ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9 ? ch - ts.CharacterCodes._0 + 52 : + ch === ts.CharacterCodes.plus ? 62 : + ch === ts.CharacterCodes.slash ? 63 : + -1; +} - interface SourceMappedPosition extends MappedPosition { - source: string; - sourceIndex: number; - sourcePosition: number; - } +interface MappedPosition { + generatedPosition: number; + source: string | undefined; + sourceIndex: number | undefined; + sourcePosition: number | undefined; + nameIndex: number | undefined; +} - function isSourceMappedPosition(value: MappedPosition): value is SourceMappedPosition { - return value.sourceIndex !== undefined - && value.sourcePosition !== undefined; - } +interface SourceMappedPosition extends MappedPosition { + source: string; + sourceIndex: number; + sourcePosition: number; +} - function sameMappedPosition(left: MappedPosition, right: MappedPosition) { - return left.generatedPosition === right.generatedPosition - && left.sourceIndex === right.sourceIndex - && left.sourcePosition === right.sourcePosition; - } +function isSourceMappedPosition(value: MappedPosition): value is SourceMappedPosition { + return value.sourceIndex !== undefined + && value.sourcePosition !== undefined; +} - function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) { - // Compares sourcePosition without comparing sourceIndex - // since the mappings are grouped by sourceIndex - ts.Debug.assert(left.sourceIndex === right.sourceIndex); - return ts.compareValues(left.sourcePosition, right.sourcePosition); - } +function sameMappedPosition(left: MappedPosition, right: MappedPosition) { + return left.generatedPosition === right.generatedPosition + && left.sourceIndex === right.sourceIndex + && left.sourcePosition === right.sourcePosition; +} - function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) { - return ts.compareValues(left.generatedPosition, right.generatedPosition); - } +function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) { + // Compares sourcePosition without comparing sourceIndex + // since the mappings are grouped by sourceIndex + ts.Debug.assert(left.sourceIndex === right.sourceIndex); + return ts.compareValues(left.sourcePosition, right.sourcePosition); +} - function getSourcePositionOfMapping(value: SourceMappedPosition) { - return value.sourcePosition; - } +function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) { + return ts.compareValues(left.generatedPosition, right.generatedPosition); +} - function getGeneratedPositionOfMapping(value: MappedPosition) { - return value.generatedPosition; - } +function getSourcePositionOfMapping(value: SourceMappedPosition) { + return value.sourcePosition; +} - export function createDocumentPositionMapper(host: ts.DocumentPositionMapperHost, map: ts.RawSourceMap, mapPath: string): ts.DocumentPositionMapper { - const mapDirectory = ts.getDirectoryPath(mapPath); - const sourceRoot = map.sourceRoot ? ts.getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory; - const generatedAbsoluteFilePath = ts.getNormalizedAbsolutePath(map.file, mapDirectory); - const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath); - const sourceFileAbsolutePaths = map.sources.map(source => ts.getNormalizedAbsolutePath(source, sourceRoot)); - const sourceToSourceIndexMap = new ts.Map(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i])); - let decodedMappings: readonly MappedPosition[] | undefined; - let generatedMappings: ts.SortedReadonlyArray | undefined; - let sourceMappings: readonly ts.SortedReadonlyArray[] | undefined; +function getGeneratedPositionOfMapping(value: MappedPosition) { + return value.generatedPosition; +} - return { - getSourcePosition, - getGeneratedPosition - }; +export function createDocumentPositionMapper(host: ts.DocumentPositionMapperHost, map: ts.RawSourceMap, mapPath: string): ts.DocumentPositionMapper { + const mapDirectory = ts.getDirectoryPath(mapPath); + const sourceRoot = map.sourceRoot ? ts.getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory; + const generatedAbsoluteFilePath = ts.getNormalizedAbsolutePath(map.file, mapDirectory); + const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath); + const sourceFileAbsolutePaths = map.sources.map(source => ts.getNormalizedAbsolutePath(source, sourceRoot)); + const sourceToSourceIndexMap = new ts.Map(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i])); + let decodedMappings: readonly MappedPosition[] | undefined; + let generatedMappings: ts.SortedReadonlyArray | undefined; + let sourceMappings: readonly ts.SortedReadonlyArray[] | undefined; + + return { + getSourcePosition, + getGeneratedPosition + }; - function processMapping(mapping: Mapping): MappedPosition { - const generatedPosition = generatedFile !== undefined - ? ts.getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true) + function processMapping(mapping: Mapping): MappedPosition { + const generatedPosition = generatedFile !== undefined + ? ts.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 + ? ts.getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*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 - ? ts.getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true) - : -1; - } - return { - generatedPosition, - source, - sourceIndex: mapping.sourceIndex, - sourcePosition, - nameIndex: mapping.nameIndex - }; } + return { + generatedPosition, + source, + sourceIndex: mapping.sourceIndex, + sourcePosition, + nameIndex: mapping.nameIndex + }; + } - function getDecodedMappings() { - if (decodedMappings === undefined) { - const decoder = decodeMappings(map.mappings); - const mappings = ts.arrayFrom(decoder, processMapping); - if (decoder.error !== undefined) { - if (host.log) { - host.log(`Encountered error while decoding sourcemap: ${decoder.error}`); - } - decodedMappings = ts.emptyArray; - } - else { - decodedMappings = mappings; + function getDecodedMappings() { + if (decodedMappings === undefined) { + const decoder = decodeMappings(map.mappings); + const mappings = ts.arrayFrom(decoder, processMapping); + if (decoder.error !== undefined) { + if (host.log) { + host.log(`Encountered error while decoding sourcemap: ${decoder.error}`); } + decodedMappings = ts.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 => ts.sortAndDeduplicate(list, compareSourcePositions, sameMappedPosition)); + else { + decodedMappings = mappings; } - return sourceMappings[sourceIndex]; } + return decodedMappings; + } - function getGeneratedMappings() { - if (generatedMappings === undefined) { - const list: MappedPosition[] = []; - for (const mapping of getDecodedMappings()) { - list.push(mapping); - } - generatedMappings = ts.sortAndDeduplicate(list, compareGeneratedPositions, sameMappedPosition); + 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 => ts.sortAndDeduplicate(list, compareSourcePositions, sameMappedPosition)); } + return sourceMappings[sourceIndex]; + } - function getGeneratedPosition(loc: ts.DocumentPosition): ts.DocumentPosition { - const sourceIndex = sourceToSourceIndexMap.get(host.getCanonicalFileName(loc.fileName)); - if (sourceIndex === undefined) - return loc; - - const sourceMappings = getSourceMappings(sourceIndex); - if (!ts.some(sourceMappings)) - return loc; - let targetIndex = ts.binarySearchKey(sourceMappings, loc.pos, getSourcePositionOfMapping, ts.compareValues); - if (targetIndex < 0) { - // if no exact match, closest is 2's complement of result - targetIndex = ~targetIndex; + function getGeneratedMappings() { + if (generatedMappings === undefined) { + const list: MappedPosition[] = []; + for (const mapping of getDecodedMappings()) { + list.push(mapping); } + generatedMappings = ts.sortAndDeduplicate(list, compareGeneratedPositions, sameMappedPosition); + } + return generatedMappings; + } - const mapping = sourceMappings[targetIndex]; - if (mapping === undefined || mapping.sourceIndex !== sourceIndex) { - return loc; - } + function getGeneratedPosition(loc: ts.DocumentPosition): ts.DocumentPosition { + const sourceIndex = sourceToSourceIndexMap.get(host.getCanonicalFileName(loc.fileName)); + if (sourceIndex === undefined) + return loc; + + const sourceMappings = getSourceMappings(sourceIndex); + if (!ts.some(sourceMappings)) + return loc; + let targetIndex = ts.binarySearchKey(sourceMappings, loc.pos, getSourcePositionOfMapping, ts.compareValues); + if (targetIndex < 0) { + // if no exact match, closest is 2's complement of result + targetIndex = ~targetIndex; + } - return { fileName: generatedAbsoluteFilePath, pos: mapping.generatedPosition }; // Closest pos + const mapping = sourceMappings[targetIndex]; + if (mapping === undefined || mapping.sourceIndex !== sourceIndex) { + return loc; } - function getSourcePosition(loc: ts.DocumentPosition): ts.DocumentPosition { - const generatedMappings = getGeneratedMappings(); - if (!ts.some(generatedMappings)) - return loc; - let targetIndex = ts.binarySearchKey(generatedMappings, loc.pos, getGeneratedPositionOfMapping, ts.compareValues); - if (targetIndex < 0) { - // if no exact match, closest is 2's complement of result - targetIndex = ~targetIndex; - } + return { fileName: generatedAbsoluteFilePath, pos: mapping.generatedPosition }; // Closest pos + } - const mapping = generatedMappings[targetIndex]; - if (mapping === undefined || !isSourceMappedPosition(mapping)) { - return loc; - } + function getSourcePosition(loc: ts.DocumentPosition): ts.DocumentPosition { + const generatedMappings = getGeneratedMappings(); + if (!ts.some(generatedMappings)) + return loc; + let targetIndex = ts.binarySearchKey(generatedMappings, loc.pos, getGeneratedPositionOfMapping, ts.compareValues); + if (targetIndex < 0) { + // if no exact match, closest is 2's complement of result + targetIndex = ~targetIndex; + } - return { fileName: sourceFileAbsolutePaths[mapping.sourceIndex], pos: mapping.sourcePosition }; // Closest pos + const mapping = generatedMappings[targetIndex]; + if (mapping === undefined || !isSourceMappedPosition(mapping)) { + return loc; } + + return { fileName: sourceFileAbsolutePaths[mapping.sourceIndex], pos: mapping.sourcePosition }; // Closest pos } +} - export const identitySourceMapConsumer: ts.DocumentPositionMapper = { - getSourcePosition: ts.identity, - getGeneratedPosition: ts.identity - }; +export const identitySourceMapConsumer: ts.DocumentPositionMapper = { + getSourcePosition: ts.identity, + getGeneratedPosition: ts.identity +}; } diff --git a/src/compiler/symbolWalker.ts b/src/compiler/symbolWalker.ts index af933f038e7f8..ec8827aeb897f 100644 --- a/src/compiler/symbolWalker.ts +++ b/src/compiler/symbolWalker.ts @@ -1,181 +1,181 @@ /** @internal */ namespace ts { - export function createGetSymbolWalker(getRestTypeOfSignature: (sig: ts.Signature) => ts.Type, getTypePredicateOfSignature: (sig: ts.Signature) => ts.TypePredicate | undefined, getReturnTypeOfSignature: (sig: ts.Signature) => ts.Type, getBaseTypes: (type: ts.Type) => ts.Type[], resolveStructuredTypeMembers: (type: ts.ObjectType) => ts.ResolvedType, getTypeOfSymbol: (sym: ts.Symbol) => ts.Type, getResolvedSymbol: (node: ts.Node) => ts.Symbol, getConstraintOfTypeParameter: (typeParameter: ts.TypeParameter) => ts.Type | undefined, getFirstIdentifier: (node: ts.EntityNameOrEntityNameExpression) => ts.Identifier, getTypeArguments: (type: ts.TypeReference) => readonly ts.Type[]) { - - return getSymbolWalker; - - function getSymbolWalker(accept: (symbol: ts.Symbol) => boolean = () => true): ts.SymbolWalker { - const visitedTypes: ts.Type[] = []; // Sparse array from id to type - const visitedSymbols: ts.Symbol[] = []; // Sparse array from id to symbol - - return { - walkType: type => { - try { - visitType(type); - return { visitedTypes: ts.getOwnValues(visitedTypes), visitedSymbols: ts.getOwnValues(visitedSymbols) }; - } - finally { - ts.clear(visitedTypes); - ts.clear(visitedSymbols); - } - }, - walkSymbol: symbol => { - try { - visitSymbol(symbol); - return { visitedTypes: ts.getOwnValues(visitedTypes), visitedSymbols: ts.getOwnValues(visitedSymbols) }; - } - finally { - ts.clear(visitedTypes); - ts.clear(visitedSymbols); - } - }, - }; - - function visitType(type: ts.Type | undefined): void { - if (!type) { - return; - } +export function createGetSymbolWalker(getRestTypeOfSignature: (sig: ts.Signature) => ts.Type, getTypePredicateOfSignature: (sig: ts.Signature) => ts.TypePredicate | undefined, getReturnTypeOfSignature: (sig: ts.Signature) => ts.Type, getBaseTypes: (type: ts.Type) => ts.Type[], resolveStructuredTypeMembers: (type: ts.ObjectType) => ts.ResolvedType, getTypeOfSymbol: (sym: ts.Symbol) => ts.Type, getResolvedSymbol: (node: ts.Node) => ts.Symbol, getConstraintOfTypeParameter: (typeParameter: ts.TypeParameter) => ts.Type | undefined, getFirstIdentifier: (node: ts.EntityNameOrEntityNameExpression) => ts.Identifier, getTypeArguments: (type: ts.TypeReference) => readonly ts.Type[]) { - 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 & ts.TypeFlags.Object) { - const objectType = type as ts.ObjectType; - const objectFlags = objectType.objectFlags; - if (objectFlags & ts.ObjectFlags.Reference) { - visitTypeReference(type as ts.TypeReference); - } - if (objectFlags & ts.ObjectFlags.Mapped) { - visitMappedType(type as ts.MappedType); - } - if (objectFlags & (ts.ObjectFlags.Class | ts.ObjectFlags.Interface)) { - visitInterfaceType(type as ts.InterfaceType); - } - if (objectFlags & (ts.ObjectFlags.Tuple | ts.ObjectFlags.Anonymous)) { - visitObjectType(objectType); - } - } - if (type.flags & ts.TypeFlags.TypeParameter) { - visitTypeParameter(type as ts.TypeParameter); + return getSymbolWalker; + + function getSymbolWalker(accept: (symbol: ts.Symbol) => boolean = () => true): ts.SymbolWalker { + const visitedTypes: ts.Type[] = []; // Sparse array from id to type + const visitedSymbols: ts.Symbol[] = []; // Sparse array from id to symbol + + return { + walkType: type => { + try { + visitType(type); + return { visitedTypes: ts.getOwnValues(visitedTypes), visitedSymbols: ts.getOwnValues(visitedSymbols) }; } - if (type.flags & ts.TypeFlags.UnionOrIntersection) { - visitUnionOrIntersectionType(type as ts.UnionOrIntersectionType); + finally { + ts.clear(visitedTypes); + ts.clear(visitedSymbols); } - if (type.flags & ts.TypeFlags.Index) { - visitIndexType(type as ts.IndexType); + }, + walkSymbol: symbol => { + try { + visitSymbol(symbol); + return { visitedTypes: ts.getOwnValues(visitedTypes), visitedSymbols: ts.getOwnValues(visitedSymbols) }; } - if (type.flags & ts.TypeFlags.IndexedAccess) { - visitIndexedAccessType(type as ts.IndexedAccessType); + finally { + ts.clear(visitedTypes); + ts.clear(visitedSymbols); } - } + }, + }; - function visitTypeReference(type: ts.TypeReference): void { - visitType(type.target); - ts.forEach(getTypeArguments(type), visitType); + function visitType(type: ts.Type | undefined): void { + if (!type) { + return; } - function visitTypeParameter(type: ts.TypeParameter): void { - visitType(getConstraintOfTypeParameter(type)); + if (visitedTypes[type.id]) { + return; } + visitedTypes[type.id] = type; - function visitUnionOrIntersectionType(type: ts.UnionOrIntersectionType): void { - ts.forEach(type.types, visitType); - } + // 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; - function visitIndexType(type: ts.IndexType): void { - visitType(type.type); + // Visit the type's related types, if any + if (type.flags & ts.TypeFlags.Object) { + const objectType = type as ts.ObjectType; + const objectFlags = objectType.objectFlags; + if (objectFlags & ts.ObjectFlags.Reference) { + visitTypeReference(type as ts.TypeReference); + } + if (objectFlags & ts.ObjectFlags.Mapped) { + visitMappedType(type as ts.MappedType); + } + if (objectFlags & (ts.ObjectFlags.Class | ts.ObjectFlags.Interface)) { + visitInterfaceType(type as ts.InterfaceType); + } + if (objectFlags & (ts.ObjectFlags.Tuple | ts.ObjectFlags.Anonymous)) { + visitObjectType(objectType); + } } - - function visitIndexedAccessType(type: ts.IndexedAccessType): void { - visitType(type.objectType); - visitType(type.indexType); - visitType(type.constraint); + if (type.flags & ts.TypeFlags.TypeParameter) { + visitTypeParameter(type as ts.TypeParameter); } - - function visitMappedType(type: ts.MappedType): void { - visitType(type.typeParameter); - visitType(type.constraintType); - visitType(type.templateType); - visitType(type.modifiersType); + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + visitUnionOrIntersectionType(type as ts.UnionOrIntersectionType); + } + if (type.flags & ts.TypeFlags.Index) { + visitIndexType(type as ts.IndexType); + } + if (type.flags & ts.TypeFlags.IndexedAccess) { + visitIndexedAccessType(type as ts.IndexedAccessType); } + } - function visitSignature(signature: ts.Signature): void { - const typePredicate = getTypePredicateOfSignature(signature); - if (typePredicate) { - visitType(typePredicate.type); - } - ts.forEach(signature.typeParameters, visitType); + function visitTypeReference(type: ts.TypeReference): void { + visitType(type.target); + ts.forEach(getTypeArguments(type), visitType); + } - for (const parameter of signature.parameters) { - visitSymbol(parameter); - } - visitType(getRestTypeOfSignature(signature)); - visitType(getReturnTypeOfSignature(signature)); + function visitTypeParameter(type: ts.TypeParameter): void { + visitType(getConstraintOfTypeParameter(type)); + } + + function visitUnionOrIntersectionType(type: ts.UnionOrIntersectionType): void { + ts.forEach(type.types, visitType); + } + + function visitIndexType(type: ts.IndexType): void { + visitType(type.type); + } + + function visitIndexedAccessType(type: ts.IndexedAccessType): void { + visitType(type.objectType); + visitType(type.indexType); + visitType(type.constraint); + } + + function visitMappedType(type: ts.MappedType): void { + visitType(type.typeParameter); + visitType(type.constraintType); + visitType(type.templateType); + visitType(type.modifiersType); + } + + function visitSignature(signature: ts.Signature): void { + const typePredicate = getTypePredicateOfSignature(signature); + if (typePredicate) { + visitType(typePredicate.type); } + ts.forEach(signature.typeParameters, visitType); - function visitInterfaceType(interfaceT: ts.InterfaceType): void { - visitObjectType(interfaceT); - ts.forEach(interfaceT.typeParameters, visitType); - ts.forEach(getBaseTypes(interfaceT), visitType); - visitType(interfaceT.thisType); + for (const parameter of signature.parameters) { + visitSymbol(parameter); } + visitType(getRestTypeOfSignature(signature)); + visitType(getReturnTypeOfSignature(signature)); + } - function visitObjectType(type: ts.ObjectType): void { - const resolved = resolveStructuredTypeMembers(type); - for (const info of resolved.indexInfos) { - visitType(info.keyType); - visitType(info.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 visitInterfaceType(interfaceT: ts.InterfaceType): void { + visitObjectType(interfaceT); + ts.forEach(interfaceT.typeParameters, visitType); + ts.forEach(getBaseTypes(interfaceT), visitType); + visitType(interfaceT.thisType); + } + + function visitObjectType(type: ts.ObjectType): void { + const resolved = resolveStructuredTypeMembers(type); + for (const info of resolved.indexInfos) { + visitType(info.keyType); + visitType(info.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: ts.Symbol | undefined): boolean { - if (!symbol) { - return false; - } - const symbolId = ts.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); - } - ts.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 === ts.SyntaxKind.TypeQuery) { - const query = (d as any).type as ts.TypeQueryNode; - const entity = getResolvedSymbol(getFirstIdentifier(query.exprName)); - visitSymbol(entity); - } - }); + function visitSymbol(symbol: ts.Symbol | undefined): boolean { + if (!symbol) { + return false; + } + const symbolId = ts.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); + } + ts.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 === ts.SyntaxKind.TypeQuery) { + const query = (d as any).type as ts.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 ffaf2e92b50bb..48e22f73ce7f5 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -2,1807 +2,1807 @@ declare function setTimeout(handler: (...args: any[]) => void, timeout: number): 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); } + 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; - } +/** + * 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 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; - } +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 enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250 +} - /* @internal */ - export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: ts.WatchOptions | undefined) => FileWatcher; - /* @internal */ - export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined) => FileWatcher; +/* @internal */ +export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: ts.WatchOptions | undefined) => FileWatcher; +/* @internal */ +export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined) => FileWatcher; - /* @internal */ - export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time +/* @internal */ +export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time - /* @internal */ - export function getModifiedTime(host: { - getModifiedTime: NonNullable; - }, fileName: string) { - return host.getModifiedTime(fileName) || missingFileModifiedTime; - } +/* @internal */ +export function getModifiedTime(host: { + getModifiedTime: NonNullable; +}, fileName: string) { + return host.getModifiedTime(fileName) || missingFileModifiedTime; +} - interface Levels { - Low: number; - Medium: number; - High: number; - } +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 - }; +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; - const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; - let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); - /* @internal */ - export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels); + function getLevel(envVar: string, level: keyof Levels) { + return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); + } - /* @internal */ - export function setCustomPollingValues(system: System) { - if (!system.getEnvironmentVariable) { - return; + 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); + } } - 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 setCustomLevels(baseVariable: string, levels: Levels) { + const customLevels = getCustomLevels(baseVariable); + if (customLevels) { + setLevel("Low"); + setLevel("Medium"); + setLevel("High"); + return true; } + return false; - 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 setLevel(level: keyof Levels) { + levels[level] = customLevels![level] || levels[level]; } + } - function setCustomLevels(baseVariable: string, levels: Levels) { - const customLevels = getCustomLevels(baseVariable); - if (customLevels) { - setLevel("Low"); - setLevel("Medium"); - setLevel("High"); - return true; - } - return false; + function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { + const customLevels = getCustomLevels(baseVariable); + return (pollingIntervalChanged || customLevels) && + createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); + } +} - function setLevel(level: keyof Levels) { - levels[level] = customLevels![level] || levels[level]; - } +interface WatchedFileWithIsClosed extends WatchedFile { + isClosed?: boolean; +} +function pollWatchedFileQueue(host: { + getModifiedTime: NonNullable; +}, queue: (T | undefined)[], pollIndex: number, chunkSize: number, callbackOnWatchFileStat?: (watchedFile: T, pollIndex: number, fileChanged: boolean) => void) { + let definedValueCopyToIndex = pollIndex; + // Max visit would be all elements of the queue + for (let canVisit = queue.length; chunkSize && canVisit; nextPollIndex(), canVisit--) { + const watchedFile = queue[pollIndex]; + if (!watchedFile) { + continue; + } + else if (watchedFile.isClosed) { + queue[pollIndex] = undefined; + continue; } - function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { - const customLevels = getCustomLevels(baseVariable); - return (pollingIntervalChanged || customLevels) && - createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); + // Only files polled count towards chunkSize + chunkSize--; + const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(host, watchedFile.fileName)); + if (watchedFile.isClosed) { + // Closed watcher as part of callback + queue[pollIndex] = undefined; + continue; } - } - interface WatchedFileWithIsClosed extends WatchedFile { - isClosed?: boolean; - } - function pollWatchedFileQueue(host: { - getModifiedTime: NonNullable; - }, queue: (T | undefined)[], pollIndex: number, chunkSize: number, callbackOnWatchFileStat?: (watchedFile: T, pollIndex: number, fileChanged: boolean) => void) { - let definedValueCopyToIndex = pollIndex; - // Max visit would be all elements of the queue - for (let canVisit = queue.length; chunkSize && canVisit; nextPollIndex(), canVisit--) { - const watchedFile = queue[pollIndex]; - if (!watchedFile) { - continue; - } - else if (watchedFile.isClosed) { + callbackOnWatchFileStat?.(watchedFile, pollIndex, fileChanged); + // Defragment the queue while we are at it + if (queue[pollIndex]) { + // Copy this file to the non hole location + if (definedValueCopyToIndex < pollIndex) { + queue[definedValueCopyToIndex] = watchedFile; queue[pollIndex] = undefined; - continue; } + definedValueCopyToIndex++; + } + } - // Only files polled count towards chunkSize - chunkSize--; - const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(host, watchedFile.fileName)); - if (watchedFile.isClosed) { - // Closed watcher as part of callback - queue[pollIndex] = undefined; - continue; - } + // Return next poll index + return pollIndex; - callbackOnWatchFileStat?.(watchedFile, pollIndex, fileChanged); - // Defragment the queue while we are at it - if (queue[pollIndex]) { - // Copy this file to the non hole location - if (definedValueCopyToIndex < pollIndex) { - queue[definedValueCopyToIndex] = watchedFile; - queue[pollIndex] = undefined; - } - definedValueCopyToIndex++; + function nextPollIndex() { + pollIndex++; + if (pollIndex === queue.length) { + if (definedValueCopyToIndex < pollIndex) { + // There are holes from definedValueCopyToIndex to end of queue, change queue size + queue.length = definedValueCopyToIndex; } + pollIndex = 0; + definedValueCopyToIndex = 0; } + } +} - // Return next poll index - return pollIndex; +/* @internal */ +export function createDynamicPriorityPollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; +}): HostWatchFile { + interface WatchedFile extends ts.WatchedFile { + isClosed?: boolean; + unchangedPolls: number; + } - function nextPollIndex() { - pollIndex++; - if (pollIndex === queue.length) { - if (definedValueCopyToIndex < pollIndex) { - // There are holes from definedValueCopyToIndex to end of queue, change queue size - queue.length = definedValueCopyToIndex; - } - pollIndex = 0; - definedValueCopyToIndex = 0; - } - } + interface PollingIntervalQueue extends Array { + pollingInterval: PollingInterval; + pollIndex: number; + pollScheduled: boolean; } - /* @internal */ - export function createDynamicPriorityPollingWatchFile(host: { - getModifiedTime: NonNullable; - setTimeout: NonNullable; - }): HostWatchFile { - interface WatchedFile extends ts.WatchedFile { - isClosed?: boolean; - unchangedPolls: number; - } + 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(host, fileName) + }; + watchedFiles.push(file); - interface PollingIntervalQueue extends Array { - pollingInterval: PollingInterval; - pollIndex: number; - pollScheduled: boolean; - } + addToPollingIntervalQueue(file, defaultPollingInterval); + return { + close: () => { + file.isClosed = true; + // Remove from watchedFiles + ts.unorderedRemoveItem(watchedFiles, file); + // Do not update polling interval queue since that will happen as part of polling + } + }; + } - 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(host, fileName) - }; - watchedFiles.push(file); + function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { + const queue = [] as WatchedFile[] as PollingIntervalQueue; + queue.pollingInterval = pollingInterval; + queue.pollIndex = 0; + queue.pollScheduled = false; + return queue; + } - addToPollingIntervalQueue(file, defaultPollingInterval); - return { - close: () => { - file.isClosed = true; - // Remove from watchedFiles - ts.unorderedRemoveItem(watchedFiles, file); - // Do not update polling interval queue since that will happen as part of polling - } - }; + 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 createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { - const queue = [] as WatchedFile[] as PollingIntervalQueue; - queue.pollingInterval = pollingInterval; - queue.pollIndex = 0; + else { + ts.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); - } - else { - ts.Debug.assert(queue.pollIndex === 0); - queue.pollScheduled = false; - } } + } - function pollLowPollingIntervalQueue(queue: PollingIntervalQueue) { - // Always poll complete list of changedFilesInLastPoll - pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); + 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); - } + // 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) { - return pollWatchedFileQueue(host, queue, pollIndex, chunkSize, onWatchFileStat); + function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { + return pollWatchedFileQueue(host, queue, pollIndex, chunkSize, onWatchFileStat); - function onWatchFileStat(watchedFile: WatchedFile, pollIndex: number, fileChanged: boolean) { - 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++; + function onWatchFileStat(watchedFile: WatchedFile, pollIndex: number, fileChanged: boolean) { + 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); + addChangedFileToLowPollingIntervalQueue(watchedFile); } } - } - - function pollingIntervalQueue(pollingInterval: PollingInterval) { - switch (pollingInterval) { - case PollingInterval.Low: - return lowPollingIntervalQueue; - case PollingInterval.Medium: - return mediumPollingIntervalQueue; - case PollingInterval.High: - return highPollingIntervalQueue; + 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++; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); } } + } - function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).push(file); - scheduleNextPollIfNotAlreadyScheduled(pollingInterval); - } - - function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { - changedFilesInLastPoll.push(file); - scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); + function pollingIntervalQueue(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return lowPollingIntervalQueue; + case PollingInterval.Medium: + return mediumPollingIntervalQueue; + case PollingInterval.High: + return highPollingIntervalQueue; } + } - function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { - if (!pollingIntervalQueue(pollingInterval).pollScheduled) { - scheduleNextPoll(pollingInterval); - } - } + function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).push(file); + scheduleNextPollIfNotAlreadyScheduled(pollingInterval); + } - function scheduleNextPoll(pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); - } + function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); } - function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { - // One file can have multiple watchers - const fileWatcherCallbacks = ts.createMultiMap(); - const dirWatchers = new ts.Map(); - const toCanonicalName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - return nonPollingWatchFile; - - function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: ts.WatchOptions | undefined): FileWatcher { - const filePath = toCanonicalName(fileName); - fileWatcherCallbacks.add(filePath, callback); - const dirPath = ts.getDirectoryPath(filePath) || "."; - const watcher = dirWatchers.get(dirPath) || - createDirectoryWatcher(ts.getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); - watcher.referenceCount++; - return { - close: () => { - if (watcher.referenceCount === 1) { - watcher.close(); - dirWatchers.delete(dirPath); - } - else { - watcher.referenceCount--; - } - fileWatcherCallbacks.remove(filePath, callback); - } - }; + function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { + if (!pollingIntervalQueue(pollingInterval).pollScheduled) { + scheduleNextPoll(pollingInterval); } + } - function createDirectoryWatcher(dirName: string, dirPath: string, fallbackOptions: ts.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 (!ts.isString(relativeFileName)) - return; - const fileName = ts.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 scheduleNextPoll(pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); } +} - function createFixedChunkSizePollingWatchFile(host: { - getModifiedTime: NonNullable; - setTimeout: NonNullable; - }): HostWatchFile { - const watchedFiles: (WatchedFileWithIsClosed | undefined)[] = []; - let pollIndex = 0; - let pollScheduled: any; - return watchFile; - - function watchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { - const file: WatchedFileWithIsClosed = { - fileName, - callback, - mtime: getModifiedTime(host, fileName) - }; - watchedFiles.push(file); - scheduleNextPoll(); - return { - close: () => { - file.isClosed = true; - ts.unorderedRemoveItem(watchedFiles, file); +function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { + // One file can have multiple watchers + const fileWatcherCallbacks = ts.createMultiMap(); + const dirWatchers = new ts.Map(); + const toCanonicalName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: ts.WatchOptions | undefined): FileWatcher { + const filePath = toCanonicalName(fileName); + fileWatcherCallbacks.add(filePath, callback); + const dirPath = ts.getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || + createDirectoryWatcher(ts.getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); } - }; - } - - function pollQueue() { - pollScheduled = undefined; - pollIndex = pollWatchedFileQueue(host, watchedFiles, pollIndex, pollingChunkSize[PollingInterval.Low]); - scheduleNextPoll(); - } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); + } + }; + } - function scheduleNextPoll() { - if (!watchedFiles.length || pollScheduled) + function createDirectoryWatcher(dirName: string, dirPath: string, fallbackOptions: ts.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 (!ts.isString(relativeFileName)) return; - pollScheduled = host.setTimeout(pollQueue, PollingInterval.High); - } + const fileName = ts.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; } +} - /* @internal */ - export function createSingleFileWatcherPerName(watchFile: HostWatchFile, useCaseSensitiveFileNames: boolean): HostWatchFile { - interface SingleFileWatcher { - watcher: FileWatcher; - refCount: number; - } - const cache = new ts.Map(); - const callbacksCache = ts.createMultiMap(); - const toCanonicalFileName = ts.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) => ts.forEach(callbacksCache.get(path), cb => cb(fileName, eventKind)), pollingInterval, options), - refCount: 1 - }); +function createFixedChunkSizePollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; +}): HostWatchFile { + const watchedFiles: (WatchedFileWithIsClosed | undefined)[] = []; + let pollIndex = 0; + let pollScheduled: any; + return watchFile; + + function watchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { + const file: WatchedFileWithIsClosed = { + fileName, + callback, + mtime: getModifiedTime(host, fileName) + }; + watchedFiles.push(file); + scheduleNextPoll(); + return { + close: () => { + file.isClosed = true; + ts.unorderedRemoveItem(watchedFiles, file); } - callbacksCache.add(path, callback); - - return { - close: () => { - const watcher = ts.Debug.checkDefined(cache.get(path)); - callbacksCache.remove(path, callback); - watcher.refCount--; - if (watcher.refCount) - return; - cache.delete(path); - ts.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; - } - - return false; + function pollQueue() { + pollScheduled = undefined; + pollIndex = pollWatchedFileQueue(host, watchedFiles, pollIndex, pollingChunkSize[PollingInterval.Low]); + scheduleNextPoll(); } - /*@internal*/ - export function getFileWatcherEventKind(oldTime: number, newTime: number) { - return oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; + function scheduleNextPoll() { + if (!watchedFiles.length || pollScheduled) + return; + pollScheduled = host.setTimeout(pollQueue, PollingInterval.High); } +} - /*@internal*/ - export const ignoredPaths = ["/node_modules/.", "/.git", "/.#"]; +/* @internal */ +export function createSingleFileWatcherPerName(watchFile: HostWatchFile, useCaseSensitiveFileNames: boolean): HostWatchFile { + interface SingleFileWatcher { + watcher: FileWatcher; + refCount: number; + } + const cache = new ts.Map(); + const callbacksCache = ts.createMultiMap(); + const toCanonicalFileName = ts.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) => ts.forEach(callbacksCache.get(path), cb => cb(fileName, eventKind)), pollingInterval, options), + refCount: 1 + }); + } + callbacksCache.add(path, callback); - let curSysLog: (s: string) => void = ts.noop; // eslint-disable-line prefer-const + return { + close: () => { + const watcher = ts.Debug.checkDefined(cache.get(path)); + callbacksCache.remove(path, callback); + watcher.refCount--; + if (watcher.refCount) + return; + cache.delete(path); + ts.closeFileWatcherOf(watcher); + } + }; + }; +} - /*@internal*/ - export function sysLog(s: string) { - return curSysLog(s); +/** + * 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 function setSysLog(logger: typeof sysLog) { - curSysLog = logger; - } + return false; +} - /*@internal*/ - export interface RecursiveDirectoryWatcherHost { - watchDirectory: HostWatchDirectory; - useCaseSensitiveFileNames: boolean; - getCurrentDirectory: System["getCurrentDirectory"]; - getAccessibleSortedChildDirectories(path: string): readonly string[]; - directoryExists(dir: string): boolean; - realpath(s: string): string; - setTimeout: NonNullable; - clearTimeout: NonNullable; - } +/*@internal*/ +export function getFileWatcherEventKind(oldTime: number, newTime: number) { + return oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; +} - /** - * 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({ watchDirectory, useCaseSensitiveFileNames, getCurrentDirectory, getAccessibleSortedChildDirectories, directoryExists, realpath, setTimeout, clearTimeout }: RecursiveDirectoryWatcherHost): HostWatchDirectory { - interface ChildDirectoryWatcher extends FileWatcher { - dirName: string; - } - type ChildWatches = readonly ChildDirectoryWatcher[]; - interface HostDirectoryWatcher { - watcher: FileWatcher; - childWatches: ChildWatches; - refCount: number; - } +/*@internal*/ +export const ignoredPaths = ["/node_modules/.", "/.git", "/.#"]; - const cache = new ts.Map(); - const callbackCache = ts.createMultiMap(); - const cacheToUpdateChildWatches = new ts.Map(); - let timerToUpdateChildWatches: any; - - const filePathComparer = ts.getStringComparer(!useCaseSensitiveFileNames); - const toCanonicalFilePath = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - - return (dirName, callback, recursive, options) => recursive ? - createDirectoryWatcher(dirName, options, callback) : - watchDirectory(dirName, callback, recursive, options); +let curSysLog: (s: string) => void = ts.noop; // eslint-disable-line prefer-const - /** - * Create the directory watcher for the dirPath. - */ - function createDirectoryWatcher(dirName: string, options: ts.WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { - const dirPath = toCanonicalFilePath(dirName) as ts.Path; - let directoryWatcher = cache.get(dirPath); - if (directoryWatcher) { - directoryWatcher.refCount++; - } - else { - directoryWatcher = { - watcher: watchDirectory(dirName, fileName => { - if (isIgnoredPath(fileName, options)) - return; +/*@internal*/ +export function sysLog(s: string) { + return curSysLog(s); +} - if (options?.synchronousWatchDirectory) { - // Call the actual callback - invokeCallbacks(dirPath, fileName); +/*@internal*/ +export function setSysLog(logger: typeof sysLog) { + curSysLog = logger; +} - // 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: ts.emptyArray - }; - cache.set(dirPath, directoryWatcher); - updateChildWatches(dirName, dirPath, options); - } +/*@internal*/ +export interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + useCaseSensitiveFileNames: boolean; + getCurrentDirectory: System["getCurrentDirectory"]; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + directoryExists(dir: string): boolean; + realpath(s: string): string; + setTimeout: NonNullable; + clearTimeout: NonNullable; +} - const callbackToAdd = callback && { dirName, callback }; - if (callbackToAdd) { - callbackCache.add(dirPath, callbackToAdd); - } +/** + * 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({ watchDirectory, useCaseSensitiveFileNames, getCurrentDirectory, getAccessibleSortedChildDirectories, directoryExists, realpath, setTimeout, clearTimeout }: RecursiveDirectoryWatcherHost): HostWatchDirectory { + interface ChildDirectoryWatcher extends FileWatcher { + dirName: string; + } + type ChildWatches = readonly ChildDirectoryWatcher[]; + interface HostDirectoryWatcher { + watcher: FileWatcher; + childWatches: ChildWatches; + refCount: number; + } - return { - dirName, - close: () => { - const directoryWatcher = ts.Debug.checkDefined(cache.get(dirPath)); - if (callbackToAdd) - callbackCache.remove(dirPath, callbackToAdd); - directoryWatcher.refCount--; + const cache = new ts.Map(); + const callbackCache = ts.createMultiMap(); + const cacheToUpdateChildWatches = new ts.Map(); + let timerToUpdateChildWatches: any; + + const filePathComparer = ts.getStringComparer(!useCaseSensitiveFileNames); + const toCanonicalFilePath = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + + return (dirName, callback, recursive, options) => recursive ? + createDirectoryWatcher(dirName, options, callback) : + watchDirectory(dirName, callback, recursive, options); - if (directoryWatcher.refCount) + /** + * Create the directory watcher for the dirPath. + */ + function createDirectoryWatcher(dirName: string, options: ts.WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { + const dirPath = toCanonicalFilePath(dirName) as ts.Path; + let directoryWatcher = cache.get(dirPath); + if (directoryWatcher) { + directoryWatcher.refCount++; + } + else { + directoryWatcher = { + watcher: watchDirectory(dirName, fileName => { + if (isIgnoredPath(fileName, options)) return; - cache.delete(dirPath); - ts.closeFileWatcherOf(directoryWatcher); - directoryWatcher.childWatches.forEach(ts.closeFileWatcher); - } - }; - } + if (options?.synchronousWatchDirectory) { + // Call the actual callback + invokeCallbacks(dirPath, fileName); - type InvokeMap = ts.ESMap; - function invokeCallbacks(dirPath: ts.Path, fileName: string): void; - function invokeCallbacks(dirPath: ts.Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void; - function invokeCallbacks(dirPath: ts.Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) { - let fileName: string | undefined; - let invokeMap: InvokeMap | undefined; - if (ts.isString(fileNameOrInvokeMap)) { - fileName = fileNameOrInvokeMap; - } - else { - invokeMap = fileNameOrInvokeMap; - } - // Call the actual callback - callbackCache.forEach((callbacks, rootDirName) => { - if (invokeMap && invokeMap.get(rootDirName) === true) - return; - if (rootDirName === dirPath || (ts.startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === ts.directorySeparator)) { - if (invokeMap) { - if (fileNames) { - const existing = invokeMap.get(rootDirName); - if (existing) { - (existing as string[]).push(...fileNames); - } - else { - invokeMap.set(rootDirName, fileNames.slice()); - } - } - else { - invokeMap.set(rootDirName, true); - } + // 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: ts.emptyArray + }; + cache.set(dirPath, directoryWatcher); + updateChildWatches(dirName, dirPath, options); } - function nonSyncUpdateChildWatches(dirName: string, dirPath: ts.Path, fileName: string, options: ts.WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(dirPath); - if (parentWatcher && directoryExists(dirName)) { - // Schedule the update and postpone invoke for callbacks - scheduleUpdateChildWatches(dirName, dirPath, fileName, 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: ts.Path, fileName: string, options: ts.WatchOptions | undefined) { - const existing = cacheToUpdateChildWatches.get(dirPath); - if (existing) { - existing.fileNames.push(fileName); - } - else { - cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] }); - } - if (timerToUpdateChildWatches) { - clearTimeout(timerToUpdateChildWatches); - timerToUpdateChildWatches = undefined; - } - timerToUpdateChildWatches = setTimeout(onTimerToUpdateChildWatches, 1000); - } + return { + dirName, + close: () => { + const directoryWatcher = ts.Debug.checkDefined(cache.get(dirPath)); + if (callbackToAdd) + callbackCache.remove(dirPath, callbackToAdd); + directoryWatcher.refCount--; + + if (directoryWatcher.refCount) + return; - function onTimerToUpdateChildWatches() { - timerToUpdateChildWatches = undefined; - sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); - const start = ts.timestamp(); - const invokeMap = new ts.Map(); - - while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { - const result = cacheToUpdateChildWatches.entries().next(); - ts.Debug.assert(!result.done); - const { value: [dirPath, { dirName, options, fileNames }] } = result; - 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 - const hasChanges = updateChildWatches(dirName, dirPath, options); - invokeCallbacks(dirPath, invokeMap, hasChanges ? undefined : fileNames); + cache.delete(dirPath); + ts.closeFileWatcherOf(directoryWatcher); + directoryWatcher.childWatches.forEach(ts.closeFileWatcher); } + }; + } - sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${ts.timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); - callbackCache.forEach((callbacks, rootDirName) => { - const existing = invokeMap.get(rootDirName); - if (existing) { - callbacks.forEach(({ callback, dirName }) => { - if (ts.isArray(existing)) { - existing.forEach(callback); + type InvokeMap = ts.ESMap; + function invokeCallbacks(dirPath: ts.Path, fileName: string): void; + function invokeCallbacks(dirPath: ts.Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void; + function invokeCallbacks(dirPath: ts.Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) { + let fileName: string | undefined; + let invokeMap: InvokeMap | undefined; + if (ts.isString(fileNameOrInvokeMap)) { + fileName = fileNameOrInvokeMap; + } + else { + invokeMap = fileNameOrInvokeMap; + } + // Call the actual callback + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap && invokeMap.get(rootDirName) === true) + return; + if (rootDirName === dirPath || (ts.startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === ts.directorySeparator)) { + if (invokeMap) { + if (fileNames) { + const existing = invokeMap.get(rootDirName); + if (existing) { + (existing as string[]).push(...fileNames); } else { - callback(dirName); + invokeMap.set(rootDirName, fileNames.slice()); } - }); + } + else { + invokeMap.set(rootDirName, true); + } + } + else { + callbacks.forEach(({ callback }) => callback(fileName!)); } - }); - - const elapsed = ts.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 = ts.emptyArray; - for (const childWatcher of existingChildWatches) { - childWatcher.close(); - removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); } - } + }); + } - function updateChildWatches(parentDir: string, parentDirPath: ts.Path, options: ts.WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(parentDirPath); - if (!parentWatcher) - return false; - let newChildWatches: ChildDirectoryWatcher[] | undefined; - const hasChanges = ts.enumerateInsertsAndDeletes(directoryExists(parentDir) ? ts.mapDefined(getAccessibleSortedChildDirectories(parentDir), child => { - const childFullName = ts.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, options) && filePathComparer(childFullName, ts.normalizePath(realpath(childFullName))) === ts.Comparison.EqualTo ? childFullName : undefined; - }) : ts.emptyArray, parentWatcher.childWatches, (child, childWatcher) => filePathComparer(child, childWatcher.dirName), createAndAddChildDirectoryWatcher, ts.closeFileWatcher, addChildDirectoryWatcher); - parentWatcher.childWatches = newChildWatches || ts.emptyArray; - return hasChanges; + function nonSyncUpdateChildWatches(dirName: string, dirPath: ts.Path, fileName: string, options: ts.WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(dirPath); + if (parentWatcher && directoryExists(dirName)) { + // Schedule the update and postpone invoke for callbacks + scheduleUpdateChildWatches(dirName, dirPath, fileName, options); + return; + } - /** - * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list - */ - function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(childName, options); - addChildDirectoryWatcher(result); - } + // Call the actual callbacks and remove child watches + invokeCallbacks(dirPath, fileName); + removeChildWatches(parentWatcher); + } - /** - * Add child directory watcher to the new ChildDirectoryWatcher list - */ - function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { - (newChildWatches || (newChildWatches = [])).push(childWatcher); - } + function scheduleUpdateChildWatches(dirName: string, dirPath: ts.Path, fileName: string, options: ts.WatchOptions | undefined) { + const existing = cacheToUpdateChildWatches.get(dirPath); + if (existing) { + existing.fileNames.push(fileName); } - - function isIgnoredPath(path: string, options: ts.WatchOptions | undefined) { - return ts.some(ignoredPaths, searchPath => isInPath(path, searchPath)) || - isIgnoredByWatchOptions(path, options, useCaseSensitiveFileNames, getCurrentDirectory); + else { + cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] }); } - - function isInPath(path: string, searchPath: string) { - if (ts.stringContains(path, searchPath)) - return true; - if (useCaseSensitiveFileNames) - return false; - return ts.stringContains(toCanonicalFilePath(path), searchPath); + if (timerToUpdateChildWatches) { + clearTimeout(timerToUpdateChildWatches); + timerToUpdateChildWatches = undefined; } + timerToUpdateChildWatches = setTimeout(onTimerToUpdateChildWatches, 1000); } - /*@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: ts.WatchOptions | undefined) => FileWatcher; + function onTimerToUpdateChildWatches() { + timerToUpdateChildWatches = undefined; + sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); + const start = ts.timestamp(); + const invokeMap = new ts.Map(); + + while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { + const result = cacheToUpdateChildWatches.entries().next(); + ts.Debug.assert(!result.done); + const { value: [dirPath, { dirName, options, fileNames }] } = result; + 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 + const hasChanges = updateChildWatches(dirName, dirPath, options); + invokeCallbacks(dirPath, invokeMap, hasChanges ? undefined : fileNames); + } + + sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${ts.timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); + callbackCache.forEach((callbacks, rootDirName) => { + const existing = invokeMap.get(rootDirName); + if (existing) { + callbacks.forEach(({ callback, dirName }) => { + if (ts.isArray(existing)) { + existing.forEach(callback); + } + else { + callback(dirName); + } + }); + } + }); - /*@internal*/ - export const enum FileSystemEntryKind { - File, - Directory + const elapsed = ts.timestamp() - start; + sysLog(`sysLog:: Elapsed:: ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); } - /*@internal*/ - export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { - return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); + function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { + if (!parentWatcher) + return; + const existingChildWatches = parentWatcher.childWatches; + parentWatcher.childWatches = ts.emptyArray; + for (const childWatcher of existingChildWatches) { + childWatcher.close(); + removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); + } } - 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 updateChildWatches(parentDir: string, parentDirPath: ts.Path, options: ts.WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(parentDirPath); + if (!parentWatcher) + return false; + let newChildWatches: ChildDirectoryWatcher[] | undefined; + const hasChanges = ts.enumerateInsertsAndDeletes(directoryExists(parentDir) ? ts.mapDefined(getAccessibleSortedChildDirectories(parentDir), child => { + const childFullName = ts.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, options) && filePathComparer(childFullName, ts.normalizePath(realpath(childFullName))) === ts.Comparison.EqualTo ? childFullName : undefined; + }) : ts.emptyArray, parentWatcher.childWatches, (child, childWatcher) => filePathComparer(child, childWatcher.dirName), createAndAddChildDirectoryWatcher, ts.closeFileWatcher, addChildDirectoryWatcher); + parentWatcher.childWatches = newChildWatches || ts.emptyArray; + return hasChanges; - function isIgnoredByWatchOptions(pathToCheck: string, options: ts.WatchOptions | undefined, useCaseSensitiveFileNames: boolean, getCurrentDirectory: System["getCurrentDirectory"]) { - return (options?.excludeDirectories || options?.excludeFiles) && (ts.matchesExclude(pathToCheck, options?.excludeFiles, useCaseSensitiveFileNames, getCurrentDirectory()) || - ts.matchesExclude(pathToCheck, options?.excludeDirectories, useCaseSensitiveFileNames, getCurrentDirectory())); - } - function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback, options: ts.WatchOptions | undefined, useCaseSensitiveFileNames: boolean, getCurrentDirectory: System["getCurrentDirectory"]): 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 - const fileName = !relativeFileName ? directoryName : ts.normalizePath(ts.combinePaths(directoryName, relativeFileName)); - if (!relativeFileName || !isIgnoredByWatchOptions(fileName, options, useCaseSensitiveFileNames, getCurrentDirectory)) { - callback(fileName); - } - } - }; + /** + * 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); + } } - /*@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; - getCurrentDirectory: System["getCurrentDirectory"]; - 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; - defaultWatchFileKind: System["defaultWatchFileKind"]; + function isIgnoredPath(path: string, options: ts.WatchOptions | undefined) { + return ts.some(ignoredPaths, searchPath => isInPath(path, searchPath)) || + isIgnoredByWatchOptions(path, options, useCaseSensitiveFileNames, getCurrentDirectory); } - /*@internal*/ - export function createSystemWatchFunctions({ pollingWatchFile, getModifiedTime, setTimeout, clearTimeout, fsWatch, fileExists, useCaseSensitiveFileNames, getCurrentDirectory, fsSupportsRecursiveFsWatch, directoryExists, getAccessibleSortedChildDirectories, realpath, tscWatchFile, useNonPollingWatchers, tscWatchDirectory, defaultWatchFileKind, }: CreateSystemWatchFunctions): { - watchFile: HostWatchFile; - watchDirectory: HostWatchDirectory; - } { - let dynamicPollingWatchFile: HostWatchFile | undefined; - let fixedChunkSizePollingWatchFile: HostWatchFile | undefined; - let nonPollingWatchFile: HostWatchFile | undefined; - let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; - return { - watchFile, - watchDirectory - }; + function isInPath(path: string, searchPath: string) { + if (ts.stringContains(path, searchPath)) + return true; + if (useCaseSensitiveFileNames) + return false; + return ts.stringContains(toCanonicalFilePath(path), searchPath); + } +} - function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: ts.WatchOptions | undefined): FileWatcher { - options = updateOptionsForWatchFile(options, useNonPollingWatchers); - const watchFileKind = ts.Debug.checkDefined(options.watchFile); - switch (watchFileKind) { - case ts.WatchFileKind.FixedPollingInterval: - return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); - case ts.WatchFileKind.PriorityPollingInterval: - return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); - case ts.WatchFileKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); - case ts.WatchFileKind.FixedChunkSizePolling: - return ensureFixedChunkSizePollingWatchFile()(fileName, callback, /* pollingInterval */ undefined!, /*options*/ undefined); - case ts.WatchFileKind.UseFsEvents: - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists), - /*recursive*/ false, pollingInterval, ts.getFallbackOptions(options)); - case ts.WatchFileKind.UseFsEventsOnParentDirectory: - if (!nonPollingWatchFile) { - nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); - } - return nonPollingWatchFile(fileName, callback, pollingInterval, ts.getFallbackOptions(options)); - default: - ts.Debug.assertNever(watchFileKind); - } - } +/*@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: ts.WatchOptions | undefined) => FileWatcher; - function ensureDynamicPollingWatchFile() { - return dynamicPollingWatchFile ||= createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); - } +/*@internal*/ +export const enum FileSystemEntryKind { + File, + Directory +} - function ensureFixedChunkSizePollingWatchFile() { - return fixedChunkSizePollingWatchFile ||= createFixedChunkSizePollingWatchFile({ getModifiedTime, setTimeout }); - } +/*@internal*/ +export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); +} - function updateOptionsForWatchFile(options: ts.WatchOptions | undefined, useNonPollingWatchers?: boolean): ts.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: ts.WatchFileKind.PriorityPollingInterval }; - case "DynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchFile: ts.WatchFileKind.DynamicPriorityPolling }; - case "UseFsEvents": - // Use notifications from FS to watch with falling back to fs.watchFile - return generateWatchFileOptions(ts.WatchFileKind.UseFsEvents, ts.PollingWatchKind.PriorityInterval, options); - case "UseFsEventsWithFallbackDynamicPolling": - // Use notifications from FS to watch with falling back to dynamic watch file - return generateWatchFileOptions(ts.WatchFileKind.UseFsEvents, ts.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(ts.WatchFileKind.UseFsEventsOnParentDirectory, ts.PollingWatchKind.PriorityInterval, options) : - // Default to do not use fixed polling interval - { watchFile: defaultWatchFileKind?.() || ts.WatchFileKind.FixedPollingInterval }; - } +function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback, fileExists: System["fileExists"]): FsWatchCallback { + return eventName => { + if (eventName === "rename") { + callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); } - - function generateWatchFileOptions(watchFile: ts.WatchFileKind, fallbackPolling: ts.PollingWatchKind, options: ts.WatchOptions | undefined): ts.WatchOptions { - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchFile, - fallbackPolling: defaultFallbackPolling === undefined ? - fallbackPolling : - defaultFallbackPolling - }; + else { + // Change + callback(fileName, FileWatcherEventKind.Changed); } + }; +} - function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined): FileWatcher { - if (fsSupportsRecursiveFsWatch) { - return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), recursive, PollingInterval.Medium, ts.getFallbackOptions(options)); - } - - if (!hostRecursiveDirectoryWatcher) { - hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ - useCaseSensitiveFileNames, - getCurrentDirectory, - directoryExists, - getAccessibleSortedChildDirectories, - watchDirectory: nonRecursiveWatchDirectory, - realpath, - setTimeout, - clearTimeout - }); +function isIgnoredByWatchOptions(pathToCheck: string, options: ts.WatchOptions | undefined, useCaseSensitiveFileNames: boolean, getCurrentDirectory: System["getCurrentDirectory"]) { + return (options?.excludeDirectories || options?.excludeFiles) && (ts.matchesExclude(pathToCheck, options?.excludeFiles, useCaseSensitiveFileNames, getCurrentDirectory()) || + ts.matchesExclude(pathToCheck, options?.excludeDirectories, useCaseSensitiveFileNames, getCurrentDirectory())); +} +function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback, options: ts.WatchOptions | undefined, useCaseSensitiveFileNames: boolean, getCurrentDirectory: System["getCurrentDirectory"]): 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 + const fileName = !relativeFileName ? directoryName : ts.normalizePath(ts.combinePaths(directoryName, relativeFileName)); + if (!relativeFileName || !isIgnoredByWatchOptions(fileName, options, useCaseSensitiveFileNames, getCurrentDirectory)) { + callback(fileName); } - return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); } + }; +} - function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined): FileWatcher { - ts.Debug.assert(!recursive); - const watchDirectoryOptions = updateOptionsForWatchDirectory(options); - const watchDirectoryKind = ts.Debug.checkDefined(watchDirectoryOptions.watchDirectory); - switch (watchDirectoryKind) { - case ts.WatchDirectoryKind.FixedPollingInterval: - return pollingWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium, - /*options*/ undefined); - case ts.WatchDirectoryKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()(directoryName, () => callback(directoryName), PollingInterval.Medium, - /*options*/ undefined); - case ts.WatchDirectoryKind.FixedChunkSizePolling: - return ensureFixedChunkSizePollingWatchFile()(directoryName, () => callback(directoryName), - /* pollingInterval */ undefined!, - /*options*/ undefined); - case ts.WatchDirectoryKind.UseFsEvents: - return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), recursive, PollingInterval.Medium, ts.getFallbackOptions(watchDirectoryOptions)); - default: - ts.Debug.assertNever(watchDirectoryKind); - } - } +/*@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; + getCurrentDirectory: System["getCurrentDirectory"]; + 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; + defaultWatchFileKind: System["defaultWatchFileKind"]; +} - function updateOptionsForWatchDirectory(options: ts.WatchOptions | undefined): ts.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: ts.WatchDirectoryKind.FixedPollingInterval }; - case "RecursiveDirectoryUsingDynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchDirectory: ts.WatchDirectoryKind.DynamicPriorityPolling }; - default: - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchDirectory: ts.WatchDirectoryKind.UseFsEvents, - fallbackPolling: defaultFallbackPolling !== undefined ? - defaultFallbackPolling : - undefined - }; - } +/*@internal*/ +export function createSystemWatchFunctions({ pollingWatchFile, getModifiedTime, setTimeout, clearTimeout, fsWatch, fileExists, useCaseSensitiveFileNames, getCurrentDirectory, fsSupportsRecursiveFsWatch, directoryExists, getAccessibleSortedChildDirectories, realpath, tscWatchFile, useNonPollingWatchers, tscWatchDirectory, defaultWatchFileKind, }: CreateSystemWatchFunctions): { + watchFile: HostWatchFile; + watchDirectory: HostWatchDirectory; +} { + let dynamicPollingWatchFile: HostWatchFile | undefined; + let fixedChunkSizePollingWatchFile: HostWatchFile | undefined; + let nonPollingWatchFile: HostWatchFile | undefined; + let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; + return { + watchFile, + watchDirectory + }; + + function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: ts.WatchOptions | undefined): FileWatcher { + options = updateOptionsForWatchFile(options, useNonPollingWatchers); + const watchFileKind = ts.Debug.checkDefined(options.watchFile); + switch (watchFileKind) { + case ts.WatchFileKind.FixedPollingInterval: + return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); + case ts.WatchFileKind.PriorityPollingInterval: + return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); + case ts.WatchFileKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); + case ts.WatchFileKind.FixedChunkSizePolling: + return ensureFixedChunkSizePollingWatchFile()(fileName, callback, /* pollingInterval */ undefined!, /*options*/ undefined); + case ts.WatchFileKind.UseFsEvents: + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists), + /*recursive*/ false, pollingInterval, ts.getFallbackOptions(options)); + case ts.WatchFileKind.UseFsEventsOnParentDirectory: + if (!nonPollingWatchFile) { + nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); + } + return nonPollingWatchFile(fileName, callback, pollingInterval, ts.getFallbackOptions(options)); + default: + ts.Debug.assertNever(watchFileKind); } } - /** - * 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) => ts.writeFileEnsuringDirectories(path, data, !!writeBom, (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), path => sys.createDirectory(path), path => sys.directoryExists(path)); + function ensureDynamicPollingWatchFile() { + return dynamicPollingWatchFile ||= createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); } - /*@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; + function ensureFixedChunkSizePollingWatchFile() { + return fixedChunkSizePollingWatchFile ||= createFixedChunkSizePollingWatchFile({ getModifiedTime, setTimeout }); } - /*@internal*/ - interface Buffer extends NodeBuffer { + function updateOptionsForWatchFile(options: ts.WatchOptions | undefined, useNonPollingWatchers?: boolean): ts.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: ts.WatchFileKind.PriorityPollingInterval }; + case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchFile: ts.WatchFileKind.DynamicPriorityPolling }; + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return generateWatchFileOptions(ts.WatchFileKind.UseFsEvents, ts.PollingWatchKind.PriorityInterval, options); + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + return generateWatchFileOptions(ts.WatchFileKind.UseFsEvents, ts.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(ts.WatchFileKind.UseFsEventsOnParentDirectory, ts.PollingWatchKind.PriorityInterval, options) : + // Default to do not use fixed polling interval + { watchFile: defaultWatchFileKind?.() || ts.WatchFileKind.FixedPollingInterval }; + } } - // 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; - getWidthOfTerminal?(): number; - 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?: ts.WatchOptions): FileWatcher; - watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: ts.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; - /*@internal*/ cpuProfilingEnabled?(): 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; - /*@internal*/ require?(baseDir: string, moduleName: string): ts.RequireResult; - /*@internal*/ defaultWatchFileKind?(): ts.WatchFileKind | undefined; - - // For testing - /*@internal*/ now?(): Date; - /*@internal*/ disableUseFileVersionAsSignature?: boolean; - /*@internal*/ storeFilesChangingSignatureDuringEmit?: boolean; + function generateWatchFileOptions(watchFile: ts.WatchFileKind, fallbackPolling: ts.PollingWatchKind, options: ts.WatchOptions | undefined): ts.WatchOptions { + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchFile, + fallbackPolling: defaultFallbackPolling === undefined ? + fallbackPolling : + defaultFallbackPolling + }; } - export interface FileWatcher { - close(): void; - } + function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined): FileWatcher { + if (fsSupportsRecursiveFsWatch) { + return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), recursive, PollingInterval.Medium, ts.getFallbackOptions(options)); + } - interface DirectoryWatcher extends FileWatcher { - referenceCount: number; + if (!hostRecursiveDirectoryWatcher) { + hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ + useCaseSensitiveFileNames, + getCurrentDirectory, + directoryExists, + getAccessibleSortedChildDirectories, + watchDirectory: nonRecursiveWatchDirectory, + realpath, + setTimeout, + clearTimeout + }); + } + return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); } - 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; + function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: ts.WatchOptions | undefined): FileWatcher { + ts.Debug.assert(!recursive); + const watchDirectoryOptions = updateOptionsForWatchDirectory(options); + const watchDirectoryKind = ts.Debug.checkDefined(watchDirectoryOptions.watchDirectory); + switch (watchDirectoryKind) { + case ts.WatchDirectoryKind.FixedPollingInterval: + return pollingWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium, + /*options*/ undefined); + case ts.WatchDirectoryKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(directoryName, () => callback(directoryName), PollingInterval.Medium, + /*options*/ undefined); + case ts.WatchDirectoryKind.FixedChunkSizePolling: + return ensureFixedChunkSizePollingWatchFile()(directoryName, () => callback(directoryName), + /* pollingInterval */ undefined!, + /*options*/ undefined); + case ts.WatchDirectoryKind.UseFsEvents: + return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), recursive, PollingInterval.Medium, ts.getFallbackOptions(watchDirectoryOptions)); + default: + ts.Debug.assertNever(watchDirectoryKind); } - const dot = version.indexOf("."); - if (dot === -1) { - return undefined; + } + + function updateOptionsForWatchDirectory(options: ts.WatchOptions | undefined): ts.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: ts.WatchDirectoryKind.FixedPollingInterval }; + case "RecursiveDirectoryUsingDynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchDirectory: ts.WatchDirectoryKind.DynamicPriorityPolling }; + default: + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchDirectory: ts.WatchDirectoryKind.UseFsEvents, + fallbackPolling: defaultFallbackPolling !== undefined ? + defaultFallbackPolling : + 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"; +/** + * 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) => ts.writeFileEnsuringDirectories(path, data, !!writeBom, (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), path => sys.createDirectory(path), path => sys.directoryExists(path)); +} - let hitSystemWatcherLimit = false; +/*@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; +} - const Buffer: { - new (input: string, encoding?: string): any; - from?(input: string, encoding?: string): any; - } = require("buffer").Buffer; +/*@internal*/ +interface Buffer extends NodeBuffer { +} - const nodeVersion = getNodeMajorVersion(); - const isNode4OrLater = nodeVersion! >= 4; - const isLinuxOrMacOs = process.platform === "linux" || process.platform === "darwin"; +// 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; + getWidthOfTerminal?(): number; + readFile(path: string, encoding?: string): string | undefined; + getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); - const realpathSync = _fs.realpathSync.native ?? _fs.realpathSync; + /** + * @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?: ts.WatchOptions): FileWatcher; + watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: ts.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; + /*@internal*/ cpuProfilingEnabled?(): 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; + /*@internal*/ require?(baseDir: string, moduleName: string): ts.RequireResult; + /*@internal*/ defaultWatchFileKind?(): ts.WatchFileKind | undefined; + + // For testing + /*@internal*/ now?(): Date; + /*@internal*/ disableUseFileVersionAsSignature?: boolean; + /*@internal*/ storeFilesChangingSignatureDuringEmit?: boolean; +} - const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); - const getCurrentDirectory = ts.memoize(() => process.cwd()); - const { watchFile, watchDirectory } = createSystemWatchFunctions({ - pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames), - getModifiedTime, - setTimeout, - clearTimeout, - fsWatch, - useCaseSensitiveFileNames, - getCurrentDirectory, - 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, - defaultWatchFileKind: () => sys!.defaultWatchFileKind?.(), - }); - const nodeSystem: System = { - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - getWidthOfTerminal(){ - return process.stdout.columns; - }, - 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, - 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 = statSync(path); - if (stat?.isFile()) { - return stat.size; - } - } - catch { /*ignore*/ } - return 0; - }, - exit(exitCode?: number): void { - disableCPUProfiler(() => process.exit(exitCode)); - }, - enableCPUProfiler, - disableCPUProfiler, - cpuProfilingEnabled: () => !!activeSession || ts.contains(process.execArgv, "--cpu-prof") || ts.contains(process.execArgv, "--prof"), - realpath, - debugMode: !!process.env.NODE_INSPECTOR_IPC || !!process.env.VSCODE_INSPECTOR_OPTIONS || ts.some(process.execArgv as string[], arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), - tryEnableSourceMapsForHost() { +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; + } + const version: string = process.version; + if (!version) { + return undefined; + } + const dot = version.indexOf("."); + if (dot === -1) { + 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"); + } + catch { + _crypto = undefined; + } + let activeSession: import("inspector").Session | "stopping" | undefined; + let profilePath = "./profile.cpuprofile"; + + let hitSystemWatcherLimit = false; + + 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 realpathSync = _fs.realpathSync.native ?? _fs.realpathSync; + + const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + const getCurrentDirectory = ts.memoize(() => process.cwd()); + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames), + getModifiedTime, + setTimeout, + clearTimeout, + fsWatch, + useCaseSensitiveFileNames, + getCurrentDirectory, + 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, + defaultWatchFileKind: () => sys!.defaultWatchFileKind?.(), + }); + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + getWidthOfTerminal(){ + return process.stdout.columns; + }, + 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 = ts.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, + 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 = statSync(path); + if (stat?.isFile()) { + return stat.size; } } - }; - return nodeSystem; + catch { /*ignore*/ } + return 0; + }, + exit(exitCode?: number): void { + disableCPUProfiler(() => process.exit(exitCode)); + }, + enableCPUProfiler, + disableCPUProfiler, + cpuProfilingEnabled: () => !!activeSession || ts.contains(process.execArgv, "--cpu-prof") || ts.contains(process.execArgv, "--prof"), + realpath, + debugMode: !!process.env.NODE_INSPECTOR_IPC || !!process.env.VSCODE_INSPECTOR_OPTIONS || ts.some(process.execArgv as string[], arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), + tryEnableSourceMapsForHost() { + try { + require("source-map-support").install(); + } + catch { + // Could not enable source maps. + } + }, + 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 = ts.resolveJSModule(moduleName, baseDir, nodeSystem); + return { module: require(modulePath), modulePath, error: undefined }; + } + catch (error) { + return { module: undefined, modulePath: undefined, error }; + } + } + }; + return nodeSystem; - /** - * `throwIfNoEntry` was added so recently that it's not in the node types. - * This helper encapsulates the mitigating usage of `any`. - * See https://github.com/nodejs/node/pull/33716 - */ - function statSync(path: string): import("fs").Stats | undefined { - // throwIfNoEntry will be ignored by older versions of node - return (_fs as any).statSync(path, { throwIfNoEntry: false }); + /** + * `throwIfNoEntry` was added so recently that it's not in the node types. + * This helper encapsulates the mitigating usage of `any`. + * See https://github.com/nodejs/node/pull/33716 + */ + function statSync(path: string): import("fs").Stats | undefined { + // throwIfNoEntry will be ignored by older versions of node + return (_fs as any).statSync(path, { throwIfNoEntry: false }); + } + + /** + * 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; } + const inspector: typeof import("inspector") = require("inspector"); + if (!inspector || !inspector.Session) { + cb(); + return false; + } + const session = new inspector.Session(); + session.connect(); - /** - * 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) { + session.post("Profiler.enable", () => { + session.post("Profiler.start", () => { + activeSession = session; + profilePath = path; cb(); - return false; - } - const inspector: typeof import("inspector") = require("inspector"); - if (!inspector || !inspector.Session) { - cb(); - return false; - } - const session = new inspector.Session(); - session.connect(); - - session.post("Profiler.enable", () => { - session.post("Profiler.start", () => { - activeSession = session; - profilePath = path; - cb(); - }); }); - return true; - } + }); + 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 = new ts.Map(); - const normalizedDir = ts.normalizeSlashes(__dirname); - // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls - const fileUrlRoot = `file://${ts.getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; - for (const node of profile.nodes) { - if (node.callFrame.url) { - const url = ts.normalizeSlashes(node.callFrame.url); - if (ts.containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { - node.callFrame.url = ts.getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, ts.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++; - } + /** + * 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 = new ts.Map(); + const normalizedDir = ts.normalizeSlashes(__dirname); + // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls + const fileUrlRoot = `file://${ts.getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; + for (const node of profile.nodes) { + if (node.callFrame.url) { + const url = ts.normalizeSlashes(node.callFrame.url); + if (ts.containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { + node.callFrame.url = ts.getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, ts.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++; } } - return profile; } + return profile; + } - function disableCPUProfiler(cb: () => void) { - if (activeSession && activeSession !== "stopping") { - const s = activeSession; - activeSession.post("Profiler.stop", (err, { profile }) => { - if (!err) { - try { - if (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 + function disableCPUProfiler(cb: () => void) { + if (activeSession && activeSession !== "stopping") { + const s = activeSession; + activeSession.post("Profiler.stop", (err, { profile }) => { + if (!err) { + try { + if (statSync(profilePath)?.isDirectory()) { + profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`); } - _fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile))); } - activeSession = undefined; - s.disconnect(); - cb(); - }); - activeSession = "stopping"; - return true; - } - else { + 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(); - return false; - } + }); + activeSession = "stopping"; + return true; } - - 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); + else { + 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)); - } + 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); + } - /** 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 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)); + } - 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) - }; + /** 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 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) { + 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; } - else { - // File changed - eventKind = FileWatcherEventKind.Changed; - } - callback(fileName, eventKind); + 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; + } + else { + // File changed + eventKind = FileWatcherEventKind.Changed; + } + callback(fileName, eventKind); } + } - function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: ts.WatchOptions | undefined): FileWatcher { - let options: any; - let lastDirectoryPartWithDirectorySeparator: string | undefined; - let lastDirectoryPart: string | undefined; - if (isLinuxOrMacOs) { - lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(ts.directorySeparator)); - lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(ts.directorySeparator.length); + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: ts.WatchOptions | undefined): FileWatcher { + let options: any; + let lastDirectoryPartWithDirectorySeparator: string | undefined; + let lastDirectoryPart: string | undefined; + if (isLinuxOrMacOs) { + lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(ts.directorySeparator)); + lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(ts.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!; } - /** 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(); - } + /** + * 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 }; - } - else { - options = { persistent: true }; - } - } - - if (hitSystemWatcherLimit) { - sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to fsWatchFile`); - return watchPresentFileSystemEntryWithFsWatchFile(); - } - 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; + /** + * 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 }; } - 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 - hitSystemWatcherLimit ||= e.code === "ENOSPC"; - sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`); - return watchPresentFileSystemEntryWithFsWatchFile(); + else { + options = { persistent: true }; } } - 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!) !== -1 && relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length)) && - !fileSystemEntryExists(fileOrDirectory, entryKind) ? - invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry) : - callback(event, relativeName); - } - - /** - * 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 { - 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); + if (hitSystemWatcherLimit) { + sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to fsWatchFile`); + return watchPresentFileSystemEntryWithFsWatchFile(); } - } - - function readFileWorker(fileName: string, _encoding?: string): string | undefined { - let buffer: Buffer; try { - buffer = _fs.readFileSync(fileName); + 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) { - 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); - } - 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); + // 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 + hitSystemWatcherLimit ||= e.code === "ENOSPC"; + sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`); + return watchPresentFileSystemEntryWithFsWatchFile(); } - // Default is UTF-8 with no byte order mark - return buffer.toString("utf8"); } - function readFile(fileName: string, _encoding?: string): string | undefined { - ts.perfLogger.logStartReadFile(fileName); - const file = readFileWorker(fileName, _encoding); - ts.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!) !== -1 && relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length)) && + !fileSystemEntryExists(fileOrDirectory, entryKind) ? + invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry) : + callback(event, relativeName); } - function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { - ts.perfLogger.logEvent("WriteFile: " + fileName); - // If a BOM is required, emit one - if (writeByteOrderMark) { - data = byteOrderMarkIndicator + data; - } + /** + * 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 { + return watchFile(fileOrDirectory, createFileWatcherCallback(callback), fallbackPollingInterval, fallbackOptions); + } - let fd: number | undefined; + /** + * 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); + } + } - try { - fd = _fs.openSync(fileName, "w"); - _fs.writeSync(fd, data, /*position*/ undefined, "utf8"); + 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; } - finally { - if (fd !== undefined) { - _fs.closeSync(fd); - } + 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); + } + 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 { + ts.perfLogger.logStartReadFile(fileName); + const file = readFileWorker(fileName, _encoding); + ts.perfLogger.logStopReadFile(); + return file; + } + + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + ts.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): ts.FileSystemEntries { - ts.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; - } + function getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries { + ts.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 = ts.combinePaths(path, entry); + let stat: any; + if (typeof dirent === "string" || dirent.isSymbolicLink()) { + const name = ts.combinePaths(path, entry); - try { - stat = statSync(name); - if (!stat) { - continue; - } - } - catch (e) { + try { + stat = statSync(name); + if (!stat) { continue; } } - else { - stat = dirent; + catch (e) { + continue; } + } + else { + stat = dirent; + } - if (stat.isFile()) { - files.push(entry); - } - else if (stat.isDirectory()) { - directories.push(entry); - } + if (stat.isFile()) { + files.push(entry); + } + else if (stat.isDirectory()) { + directories.push(entry); } - files.sort(); - directories.sort(); - return { files, directories }; - } - catch (e) { - return ts.emptyFileSystemEntries; } + files.sort(); + directories.sort(); + return { files, directories }; } - - function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { - return ts.matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); + catch (e) { + return ts.emptyFileSystemEntries; } + } - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { - // Since the error thrown by fs.statSync isn't used, we can avoid collecting a stack trace to improve - // the CPU time performance. - const originalStackTraceLimit = Error.stackTraceLimit; - Error.stackTraceLimit = 0; + function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { + return ts.matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); + } - try { - const stat = statSync(path); - if (!stat) { - return false; - } - switch (entryKind) { - case FileSystemEntryKind.File: return stat.isFile(); - case FileSystemEntryKind.Directory: return stat.isDirectory(); - default: return false; - } - } - catch (e) { + function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { + // Since the error thrown by fs.statSync isn't used, we can avoid collecting a stack trace to improve + // the CPU time performance. + const originalStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + + try { + const stat = statSync(path); + if (!stat) { return false; } - finally { - Error.stackTraceLimit = originalStackTraceLimit; + 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); + catch (e) { + return false; } - - function directoryExists(path: string): boolean { - return fileSystemEntryExists(path, FileSystemEntryKind.Directory); + finally { + Error.stackTraceLimit = originalStackTraceLimit; } + } - function getDirectories(path: string): string[] { - return getAccessibleFileSystemEntries(path).directories.slice(); - } + function fileExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.File); + } - function realpath(path: string): string { - try { - return realpathSync(path); - } - catch { - return path; - } - } + function directoryExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.Directory); + } - function getModifiedTime(path: string) { - try { - return statSync(path)?.mtime; - } - catch (e) { - return undefined; - } - } + function getDirectories(path: string): string[] { + return getAccessibleFileSystemEntries(path).directories.slice(); + } - function setModifiedTime(path: string, time: Date) { - try { - _fs.utimesSync(path, time, time); - } - catch (e) { - return; - } + function realpath(path: string): string { + try { + return realpathSync(path); } - - function deleteFile(path: string) { - try { - return _fs.unlinkSync(path); - } - catch (e) { - return; - } + catch { + return path; } + } - function createSHA256Hash(data: string): string { - const hash = _crypto!.createHash("sha256"); - hash.update(data); - return hash.digest("hex"); + function getModifiedTime(path: string) { + try { + return statSync(path)?.mtime; + } + catch (e) { + return undefined; } } - 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 setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); + } + catch (e) { + return; + } } - if (sys) { - // patch writefile to create folder before writing the file - patchWriteFileEnsuringDirectory(sys); + + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch (e) { + return; + } } - return sys!; - })(); - /*@internal*/ - export function setSys(s: System) { - sys = s; + function createSHA256Hash(data: string): string { + const hash = _crypto!.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); + } } - if (sys && sys.getEnvironmentVariable) { - setCustomPollingValues(sys); - ts.Debug.setAssertionLevel(/^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) - ? ts.AssertionLevel.Normal - : ts.AssertionLevel.None); + 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 && sys.debugMode) { - ts.Debug.isDebugging = true; + if (sys) { + // patch writefile to create folder before writing the file + patchWriteFileEnsuringDirectory(sys); } + return sys!; +})(); + +/*@internal*/ +export function setSys(s: System) { + sys = s; +} + +if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); + ts.Debug.setAssertionLevel(/^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) + ? ts.AssertionLevel.Normal + : ts.AssertionLevel.None); +} +if (sys && sys.debugMode) { + ts.Debug.isDebugging = true; +} } diff --git a/src/compiler/tracing.ts b/src/compiler/tracing.ts index e507ec8978c1d..64a31585a933c 100644 --- a/src/compiler/tracing.ts +++ b/src/compiler/tracing.ts @@ -2,353 +2,353 @@ /*@internal*/ namespace ts { // eslint-disable-line one-namespace-per-file - // should be used as tracing?.___ - export let tracing: typeof tracingEnabled | undefined; - // enable the above using startTracing() +// should be used as tracing?.___ +export let tracing: typeof tracingEnabled | undefined; +// enable the above using startTracing() - // `tracingEnabled` should never be used directly, only through the above - namespace tracingEnabled { // eslint-disable-line one-namespace-per-file - type Mode = "project" | "build" | "server"; +// `tracingEnabled` should never be used directly, only through the above +namespace tracingEnabled { // eslint-disable-line one-namespace-per-file + type Mode = "project" | "build" | "server"; - let fs: typeof import("fs"); + let fs: typeof import("fs"); - let traceCount = 0; - let traceFd = 0; + let traceCount = 0; + let traceFd = 0; - let mode: Mode; + let mode: Mode; - const typeCatalog: ts.Type[] = []; // NB: id is index + 1 + const typeCatalog: ts.Type[] = []; // NB: id is index + 1 - let legendPath: string | undefined; - const legend: TraceRecord[] = []; + let legendPath: string | undefined; + const legend: TraceRecord[] = []; - // The actual constraint is that JSON.stringify be able to serialize it without throwing. - interface Args { - [key: string]: string | number | boolean | null | undefined | Args | readonly (string | number | boolean | null | undefined | Args)[]; - } - ; + // The actual constraint is that JSON.stringify be able to serialize it without throwing. + interface Args { + [key: string]: string | number | boolean | null | undefined | Args | readonly (string | number | boolean | null | undefined | Args)[]; + } + ; - /** Starts tracing for the given project. */ - export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) { - ts.Debug.assert(!tracing, "Tracing already started"); + /** Starts tracing for the given project. */ + export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) { + ts.Debug.assert(!tracing, "Tracing already started"); - if (fs === undefined) { - try { - fs = require("fs"); - } - catch (e) { - throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`); - } + if (fs === undefined) { + try { + fs = require("fs"); } - - mode = tracingMode; - typeCatalog.length = 0; - - if (legendPath === undefined) { - legendPath = ts.combinePaths(traceDir, "legend.json"); + catch (e) { + throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`); } + } - // Note that writing will fail later on if it exists and is not a directory - if (!fs.existsSync(traceDir)) { - fs.mkdirSync(traceDir, { recursive: true }); - } + mode = tracingMode; + typeCatalog.length = 0; - const countPart = mode === "build" ? `.${process.pid}-${++traceCount}` - : mode === "server" ? `.${process.pid}` - : ``; - const tracePath = ts.combinePaths(traceDir, `trace${countPart}.json`); - const typesPath = ts.combinePaths(traceDir, `types${countPart}.json`); - - legend.push({ - configFilePath, - tracePath, - typesPath, - }); - - traceFd = fs.openSync(tracePath, "w"); - tracing = tracingEnabled; // only when traceFd is properly set - - // Start with a prefix that contains some metadata that the devtools profiler expects (also avoids a warning on import) - const meta = { cat: "__metadata", ph: "M", ts: 1000 * ts.timestamp(), pid: 1, tid: 1 }; - fs.writeSync(traceFd, "[\n" - + [{ name: "process_name", args: { name: "tsc" }, ...meta }, - { name: "thread_name", args: { name: "Main" }, ...meta }, - { name: "TracingStartedInBrowser", ...meta, cat: "disabled-by-default-devtools.timeline" }] - .map(v => JSON.stringify(v)).join(",\n")); + if (legendPath === undefined) { + legendPath = ts.combinePaths(traceDir, "legend.json"); } - /** Stops tracing for the in-progress project and dumps the type catalog. */ - export function stopTracing() { - ts.Debug.assert(tracing, "Tracing is not in progress"); - ts.Debug.assert(!!typeCatalog.length === (mode !== "server")); // Have a type catalog iff not in server mode + // Note that writing will fail later on if it exists and is not a directory + if (!fs.existsSync(traceDir)) { + fs.mkdirSync(traceDir, { recursive: true }); + } - fs.writeSync(traceFd, `\n]\n`); - fs.closeSync(traceFd); - tracing = undefined; + const countPart = mode === "build" ? `.${process.pid}-${++traceCount}` + : mode === "server" ? `.${process.pid}` + : ``; + const tracePath = ts.combinePaths(traceDir, `trace${countPart}.json`); + const typesPath = ts.combinePaths(traceDir, `types${countPart}.json`); + + legend.push({ + configFilePath, + tracePath, + typesPath, + }); + + traceFd = fs.openSync(tracePath, "w"); + tracing = tracingEnabled; // only when traceFd is properly set + + // Start with a prefix that contains some metadata that the devtools profiler expects (also avoids a warning on import) + const meta = { cat: "__metadata", ph: "M", ts: 1000 * ts.timestamp(), pid: 1, tid: 1 }; + fs.writeSync(traceFd, "[\n" + + [{ name: "process_name", args: { name: "tsc" }, ...meta }, + { name: "thread_name", args: { name: "Main" }, ...meta }, + { name: "TracingStartedInBrowser", ...meta, cat: "disabled-by-default-devtools.timeline" }] + .map(v => JSON.stringify(v)).join(",\n")); + } - if (typeCatalog.length) { - dumpTypes(typeCatalog); - } - else { - // We pre-computed this path for convenience, but clear it - // now that the file won't be created. - legend[legend.length - 1].typesPath = undefined; - } - } + /** Stops tracing for the in-progress project and dumps the type catalog. */ + export function stopTracing() { + ts.Debug.assert(tracing, "Tracing is not in progress"); + ts.Debug.assert(!!typeCatalog.length === (mode !== "server")); // Have a type catalog iff not in server mode - export function recordType(type: ts.Type): void { - if (mode !== "server") { - typeCatalog.push(type); - } - } + fs.writeSync(traceFd, `\n]\n`); + fs.closeSync(traceFd); + tracing = undefined; - export const enum Phase { - Parse = "parse", - Program = "program", - Bind = "bind", - Check = "check", - CheckTypes = "checkTypes", - Emit = "emit", - Session = "session" + if (typeCatalog.length) { + dumpTypes(typeCatalog); } - - export function instant(phase: Phase, name: string, args?: Args) { - writeEvent("I", phase, name, args, `"s":"g"`); + else { + // We pre-computed this path for convenience, but clear it + // now that the file won't be created. + legend[legend.length - 1].typesPath = undefined; } + } - const eventStack: { - phase: Phase; - name: string; - args?: Args; - time: number; - separateBeginAndEnd: boolean; - }[] = []; - - /** - * @param separateBeginAndEnd - used for special cases where we need the trace point even if the event - * never terminates (typically for reducing a scenario too big to trace to one that can be completed). - * In the future we might implement an exit handler to dump unfinished events which would deprecate - * these operations. - */ - export function push(phase: Phase, name: string, args?: Args, separateBeginAndEnd = false) { - if (separateBeginAndEnd) { - writeEvent("B", phase, name, args); - } - eventStack.push({ phase, name, args, time: 1000 * ts.timestamp(), separateBeginAndEnd }); + export function recordType(type: ts.Type): void { + if (mode !== "server") { + typeCatalog.push(type); } - export function pop() { - ts.Debug.assert(eventStack.length > 0); - writeStackEvent(eventStack.length - 1, 1000 * ts.timestamp()); - eventStack.length--; + } + + export const enum Phase { + Parse = "parse", + Program = "program", + Bind = "bind", + Check = "check", + CheckTypes = "checkTypes", + Emit = "emit", + Session = "session" + } + + export function instant(phase: Phase, name: string, args?: Args) { + writeEvent("I", phase, name, args, `"s":"g"`); + } + + const eventStack: { + phase: Phase; + name: string; + args?: Args; + time: number; + separateBeginAndEnd: boolean; + }[] = []; + + /** + * @param separateBeginAndEnd - used for special cases where we need the trace point even if the event + * never terminates (typically for reducing a scenario too big to trace to one that can be completed). + * In the future we might implement an exit handler to dump unfinished events which would deprecate + * these operations. + */ + export function push(phase: Phase, name: string, args?: Args, separateBeginAndEnd = false) { + if (separateBeginAndEnd) { + writeEvent("B", phase, name, args); } - export function popAll() { - const endTime = 1000 * ts.timestamp(); - for (let i = eventStack.length - 1; i >= 0; i--) { - writeStackEvent(i, endTime); - } - eventStack.length = 0; + eventStack.push({ phase, name, args, time: 1000 * ts.timestamp(), separateBeginAndEnd }); + } + export function pop() { + ts.Debug.assert(eventStack.length > 0); + writeStackEvent(eventStack.length - 1, 1000 * ts.timestamp()); + eventStack.length--; + } + export function popAll() { + const endTime = 1000 * ts.timestamp(); + for (let i = eventStack.length - 1; i >= 0; i--) { + writeStackEvent(i, endTime); } - // sample every 10ms - const sampleInterval = 1000 * 10; - function writeStackEvent(index: number, endTime: number) { - const { phase, name, args, time, separateBeginAndEnd } = eventStack[index]; - if (separateBeginAndEnd) { - writeEvent("E", phase, name, args, /*extras*/ undefined, endTime); - } - // test if [time,endTime) straddles a sampling point - else if (sampleInterval - (time % sampleInterval) <= endTime - time) { - writeEvent("X", phase, name, args, `"dur":${endTime - time}`, time); - } + eventStack.length = 0; + } + // sample every 10ms + const sampleInterval = 1000 * 10; + function writeStackEvent(index: number, endTime: number) { + const { phase, name, args, time, separateBeginAndEnd } = eventStack[index]; + if (separateBeginAndEnd) { + writeEvent("E", phase, name, args, /*extras*/ undefined, endTime); } - - function writeEvent(eventType: string, phase: Phase, name: string, args: Args | undefined, extras?: string, time: number = 1000 * ts.timestamp()) { - - // In server mode, there's no easy way to dump type information, so we drop events that would require it. - if (mode === "server" && phase === Phase.CheckTypes) - return; - ts.performance.mark("beginTracing"); - fs.writeSync(traceFd, `,\n{"pid":1,"tid":1,"ph":"${eventType}","cat":"${phase}","ts":${time},"name":"${name}"`); - if (extras) - fs.writeSync(traceFd, `,${extras}`); - if (args) - fs.writeSync(traceFd, `,"args":${JSON.stringify(args)}`); - fs.writeSync(traceFd, `}`); - ts.performance.mark("endTracing"); - ts.performance.measure("Tracing", "beginTracing", "endTracing"); + // test if [time,endTime) straddles a sampling point + else if (sampleInterval - (time % sampleInterval) <= endTime - time) { + writeEvent("X", phase, name, args, `"dur":${endTime - time}`, time); } + } - function getLocation(node: ts.Node | undefined) { - const file = ts.getSourceFileOfNode(node); - return !file - ? undefined - : { - path: file.path, - start: indexFromOne(ts.getLineAndCharacterOfPosition(file, node!.pos)), - end: indexFromOne(ts.getLineAndCharacterOfPosition(file, node!.end)), - }; + function writeEvent(eventType: string, phase: Phase, name: string, args: Args | undefined, extras?: string, time: number = 1000 * ts.timestamp()) { + + // In server mode, there's no easy way to dump type information, so we drop events that would require it. + if (mode === "server" && phase === Phase.CheckTypes) + return; + ts.performance.mark("beginTracing"); + fs.writeSync(traceFd, `,\n{"pid":1,"tid":1,"ph":"${eventType}","cat":"${phase}","ts":${time},"name":"${name}"`); + if (extras) + fs.writeSync(traceFd, `,${extras}`); + if (args) + fs.writeSync(traceFd, `,"args":${JSON.stringify(args)}`); + fs.writeSync(traceFd, `}`); + ts.performance.mark("endTracing"); + ts.performance.measure("Tracing", "beginTracing", "endTracing"); + } - function indexFromOne(lc: ts.LineAndCharacter): ts.LineAndCharacter { - return { - line: lc.line + 1, - character: lc.character + 1, - }; - } + function getLocation(node: ts.Node | undefined) { + const file = ts.getSourceFileOfNode(node); + return !file + ? undefined + : { + path: file.path, + start: indexFromOne(ts.getLineAndCharacterOfPosition(file, node!.pos)), + end: indexFromOne(ts.getLineAndCharacterOfPosition(file, node!.end)), + }; + + function indexFromOne(lc: ts.LineAndCharacter): ts.LineAndCharacter { + return { + line: lc.line + 1, + character: lc.character + 1, + }; } + } - function dumpTypes(types: readonly ts.Type[]) { - ts.performance.mark("beginDumpTypes"); - - const typesPath = legend[legend.length - 1].typesPath!; - const typesFd = fs.openSync(typesPath, "w"); + function dumpTypes(types: readonly ts.Type[]) { + ts.performance.mark("beginDumpTypes"); - const recursionIdentityMap = new ts.Map(); + const typesPath = legend[legend.length - 1].typesPath!; + const typesFd = fs.openSync(typesPath, "w"); - // Cleverness: no line break here so that the type ID will match the line number - fs.writeSync(typesFd, "["); + const recursionIdentityMap = new ts.Map(); - const numTypes = types.length; - for (let i = 0; i < numTypes; i++) { - const type = types[i]; - const objectFlags = (type as any).objectFlags; - const symbol = type.aliasSymbol ?? type.symbol; + // Cleverness: no line break here so that the type ID will match the line number + fs.writeSync(typesFd, "["); - // It's slow to compute the display text, so skip it unless it's really valuable (or cheap) - let display: string | undefined; - if ((objectFlags & ts.ObjectFlags.Anonymous) | (type.flags & ts.TypeFlags.Literal)) { - try { - display = type.checker?.typeToString(type); - } - catch { - display = undefined; - } - } + const numTypes = types.length; + for (let i = 0; i < numTypes; i++) { + const type = types[i]; + const objectFlags = (type as any).objectFlags; + const symbol = type.aliasSymbol ?? type.symbol; - let indexedAccessProperties: object = {}; - if (type.flags & ts.TypeFlags.IndexedAccess) { - const indexedAccessType = type as ts.IndexedAccessType; - indexedAccessProperties = { - indexedAccessObjectType: indexedAccessType.objectType?.id, - indexedAccessIndexType: indexedAccessType.indexType?.id, - }; + // It's slow to compute the display text, so skip it unless it's really valuable (or cheap) + let display: string | undefined; + if ((objectFlags & ts.ObjectFlags.Anonymous) | (type.flags & ts.TypeFlags.Literal)) { + try { + display = type.checker?.typeToString(type); } - - let referenceProperties: object = {}; - if (objectFlags & ts.ObjectFlags.Reference) { - const referenceType = type as ts.TypeReference; - referenceProperties = { - instantiatedType: referenceType.target?.id, - typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id), - referenceLocation: getLocation(referenceType.node), - }; + catch { + display = undefined; } + } - let conditionalProperties: object = {}; - if (type.flags & ts.TypeFlags.Conditional) { - const conditionalType = type as ts.ConditionalType; - conditionalProperties = { - conditionalCheckType: conditionalType.checkType?.id, - conditionalExtendsType: conditionalType.extendsType?.id, - conditionalTrueType: conditionalType.resolvedTrueType?.id ?? -1, - conditionalFalseType: conditionalType.resolvedFalseType?.id ?? -1, - }; - } + let indexedAccessProperties: object = {}; + if (type.flags & ts.TypeFlags.IndexedAccess) { + const indexedAccessType = type as ts.IndexedAccessType; + indexedAccessProperties = { + indexedAccessObjectType: indexedAccessType.objectType?.id, + indexedAccessIndexType: indexedAccessType.indexType?.id, + }; + } - let substitutionProperties: object = {}; - if (type.flags & ts.TypeFlags.Substitution) { - const substitutionType = type as ts.SubstitutionType; - substitutionProperties = { - substitutionBaseType: substitutionType.baseType?.id, - substituteType: substitutionType.substitute?.id, - }; - } + let referenceProperties: object = {}; + if (objectFlags & ts.ObjectFlags.Reference) { + const referenceType = type as ts.TypeReference; + referenceProperties = { + instantiatedType: referenceType.target?.id, + typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id), + referenceLocation: getLocation(referenceType.node), + }; + } - let reverseMappedProperties: object = {}; - if (objectFlags & ts.ObjectFlags.ReverseMapped) { - const reverseMappedType = type as ts.ReverseMappedType; - reverseMappedProperties = { - reverseMappedSourceType: reverseMappedType.source?.id, - reverseMappedMappedType: reverseMappedType.mappedType?.id, - reverseMappedConstraintType: reverseMappedType.constraintType?.id, - }; - } + let conditionalProperties: object = {}; + if (type.flags & ts.TypeFlags.Conditional) { + const conditionalType = type as ts.ConditionalType; + conditionalProperties = { + conditionalCheckType: conditionalType.checkType?.id, + conditionalExtendsType: conditionalType.extendsType?.id, + conditionalTrueType: conditionalType.resolvedTrueType?.id ?? -1, + conditionalFalseType: conditionalType.resolvedFalseType?.id ?? -1, + }; + } - let evolvingArrayProperties: object = {}; - if (objectFlags & ts.ObjectFlags.EvolvingArray) { - const evolvingArrayType = type as ts.EvolvingArrayType; - evolvingArrayProperties = { - evolvingArrayElementType: evolvingArrayType.elementType.id, - evolvingArrayFinalType: evolvingArrayType.finalArrayType?.id, - }; - } + let substitutionProperties: object = {}; + if (type.flags & ts.TypeFlags.Substitution) { + const substitutionType = type as ts.SubstitutionType; + substitutionProperties = { + substitutionBaseType: substitutionType.baseType?.id, + substituteType: substitutionType.substitute?.id, + }; + } - // We can't print out an arbitrary object, so just assign each one a unique number. - // Don't call it an "id" so people don't treat it as a type id. - let recursionToken: number | undefined; - const recursionIdentity = type.checker.getRecursionIdentity(type); - if (recursionIdentity) { - recursionToken = recursionIdentityMap.get(recursionIdentity); - if (!recursionToken) { - recursionToken = recursionIdentityMap.size; - recursionIdentityMap.set(recursionIdentity, recursionToken); - } - } + let reverseMappedProperties: object = {}; + if (objectFlags & ts.ObjectFlags.ReverseMapped) { + const reverseMappedType = type as ts.ReverseMappedType; + reverseMappedProperties = { + reverseMappedSourceType: reverseMappedType.source?.id, + reverseMappedMappedType: reverseMappedType.mappedType?.id, + reverseMappedConstraintType: reverseMappedType.constraintType?.id, + }; + } - const descriptor = { - id: type.id, - intrinsicName: (type as any).intrinsicName, - symbolName: symbol?.escapedName && ts.unescapeLeadingUnderscores(symbol.escapedName), - recursionId: recursionToken, - isTuple: objectFlags & ts.ObjectFlags.Tuple ? true : undefined, - unionTypes: (type.flags & ts.TypeFlags.Union) ? (type as ts.UnionType).types?.map(t => t.id) : undefined, - intersectionTypes: (type.flags & ts.TypeFlags.Intersection) ? (type as ts.IntersectionType).types.map(t => t.id) : undefined, - aliasTypeArguments: type.aliasTypeArguments?.map(t => t.id), - keyofType: (type.flags & ts.TypeFlags.Index) ? (type as ts.IndexType).type?.id : undefined, - ...indexedAccessProperties, - ...referenceProperties, - ...conditionalProperties, - ...substitutionProperties, - ...reverseMappedProperties, - ...evolvingArrayProperties, - destructuringPattern: getLocation(type.pattern), - firstDeclaration: getLocation(symbol?.declarations?.[0]), - flags: ts.Debug.formatTypeFlags(type.flags).split("|"), - display, + let evolvingArrayProperties: object = {}; + if (objectFlags & ts.ObjectFlags.EvolvingArray) { + const evolvingArrayType = type as ts.EvolvingArrayType; + evolvingArrayProperties = { + evolvingArrayElementType: evolvingArrayType.elementType.id, + evolvingArrayFinalType: evolvingArrayType.finalArrayType?.id, }; + } - fs.writeSync(typesFd, JSON.stringify(descriptor)); - if (i < numTypes - 1) { - fs.writeSync(typesFd, ",\n"); + // We can't print out an arbitrary object, so just assign each one a unique number. + // Don't call it an "id" so people don't treat it as a type id. + let recursionToken: number | undefined; + const recursionIdentity = type.checker.getRecursionIdentity(type); + if (recursionIdentity) { + recursionToken = recursionIdentityMap.get(recursionIdentity); + if (!recursionToken) { + recursionToken = recursionIdentityMap.size; + recursionIdentityMap.set(recursionIdentity, recursionToken); } } - fs.writeSync(typesFd, "]\n"); + const descriptor = { + id: type.id, + intrinsicName: (type as any).intrinsicName, + symbolName: symbol?.escapedName && ts.unescapeLeadingUnderscores(symbol.escapedName), + recursionId: recursionToken, + isTuple: objectFlags & ts.ObjectFlags.Tuple ? true : undefined, + unionTypes: (type.flags & ts.TypeFlags.Union) ? (type as ts.UnionType).types?.map(t => t.id) : undefined, + intersectionTypes: (type.flags & ts.TypeFlags.Intersection) ? (type as ts.IntersectionType).types.map(t => t.id) : undefined, + aliasTypeArguments: type.aliasTypeArguments?.map(t => t.id), + keyofType: (type.flags & ts.TypeFlags.Index) ? (type as ts.IndexType).type?.id : undefined, + ...indexedAccessProperties, + ...referenceProperties, + ...conditionalProperties, + ...substitutionProperties, + ...reverseMappedProperties, + ...evolvingArrayProperties, + destructuringPattern: getLocation(type.pattern), + firstDeclaration: getLocation(symbol?.declarations?.[0]), + flags: ts.Debug.formatTypeFlags(type.flags).split("|"), + display, + }; + + fs.writeSync(typesFd, JSON.stringify(descriptor)); + if (i < numTypes - 1) { + fs.writeSync(typesFd, ",\n"); + } + } - fs.closeSync(typesFd); + fs.writeSync(typesFd, "]\n"); - ts.performance.mark("endDumpTypes"); - ts.performance.measure("Dump types", "beginDumpTypes", "endDumpTypes"); - } + fs.closeSync(typesFd); - export function dumpLegend() { - if (!legendPath) { - return; - } + ts.performance.mark("endDumpTypes"); + ts.performance.measure("Dump types", "beginDumpTypes", "endDumpTypes"); + } - fs.writeFileSync(legendPath, JSON.stringify(legend)); + export function dumpLegend() { + if (!legendPath) { + return; } - interface TraceRecord { - configFilePath?: string; - tracePath: string; - typesPath?: string; - } + fs.writeFileSync(legendPath, JSON.stringify(legend)); } - // define after tracingEnabled is initialized - export const startTracing = tracingEnabled.startTracing; - export const dumpTracingLegend = tracingEnabled.dumpLegend; - - export interface TracingNode { - tracingPath?: ts.Path; + interface TraceRecord { + configFilePath?: string; + tracePath: string; + typesPath?: string; } } + +// define after tracingEnabled is initialized +export const startTracing = tracingEnabled.startTracing; +export const dumpTracingLegend = tracingEnabled.dumpLegend; + +export interface TracingNode { + tracingPath?: ts.Path; +} +} diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index 1b456f7fec165..ecdfbf353f68b 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -1,575 +1,575 @@ /* @internal */ namespace ts { - function getModuleTransformer(moduleKind: ts.ModuleKind): ts.TransformerFactory { - switch (moduleKind) { - case ts.ModuleKind.ESNext: - case ts.ModuleKind.ES2022: - case ts.ModuleKind.ES2020: - case ts.ModuleKind.ES2015: - return ts.transformECMAScriptModule; - case ts.ModuleKind.System: - return ts.transformSystemModule; - case ts.ModuleKind.Node16: - case ts.ModuleKind.NodeNext: - return ts.transformNodeModule; - default: - return ts.transformModule; - } +function getModuleTransformer(moduleKind: ts.ModuleKind): ts.TransformerFactory { + switch (moduleKind) { + case ts.ModuleKind.ESNext: + case ts.ModuleKind.ES2022: + case ts.ModuleKind.ES2020: + case ts.ModuleKind.ES2015: + return ts.transformECMAScriptModule; + case ts.ModuleKind.System: + return ts.transformSystemModule; + case ts.ModuleKind.Node16: + case ts.ModuleKind.NodeNext: + return ts.transformNodeModule; + default: + return ts.transformModule; } +} - const enum TransformationState { - Uninitialized, - Initialized, - Completed, - Disposed - } +const enum TransformationState { + Uninitialized, + Initialized, + Completed, + Disposed +} + +const enum SyntaxKindFeatureFlags { + Substitution = 1 << 0, + EmitNotifications = 1 << 1 +} - const enum SyntaxKindFeatureFlags { - Substitution = 1 << 0, - EmitNotifications = 1 << 1 +export const noTransformers: ts.EmitTransformers = { scriptTransformers: ts.emptyArray, declarationTransformers: ts.emptyArray }; +export function getTransformers(compilerOptions: ts.CompilerOptions, customTransformers?: ts.CustomTransformers, emitOnlyDtsFiles?: boolean): ts.EmitTransformers { + return { + scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles), + declarationTransformers: getDeclarationTransformers(customTransformers), + }; +} + +function getScriptTransformers(compilerOptions: ts.CompilerOptions, customTransformers?: ts.CustomTransformers, emitOnlyDtsFiles?: boolean) { + if (emitOnlyDtsFiles) + return ts.emptyArray; + const languageVersion = ts.getEmitScriptTarget(compilerOptions); + const moduleKind = ts.getEmitModuleKind(compilerOptions); + const transformers: ts.TransformerFactory[] = []; + ts.addRange(transformers, customTransformers && ts.map(customTransformers.before, wrapScriptTransformerFactory)); + transformers.push(ts.transformTypeScript); + transformers.push(ts.transformClassFields); + if (ts.getJSXTransformEnabled(compilerOptions)) { + transformers.push(ts.transformJsx); + } + if (languageVersion < ts.ScriptTarget.ESNext) { + transformers.push(ts.transformESNext); + } + if (languageVersion < ts.ScriptTarget.ES2021) { + transformers.push(ts.transformES2021); + } + if (languageVersion < ts.ScriptTarget.ES2020) { + transformers.push(ts.transformES2020); + } + if (languageVersion < ts.ScriptTarget.ES2019) { + transformers.push(ts.transformES2019); + } + if (languageVersion < ts.ScriptTarget.ES2018) { + transformers.push(ts.transformES2018); + } + if (languageVersion < ts.ScriptTarget.ES2017) { + transformers.push(ts.transformES2017); + } + if (languageVersion < ts.ScriptTarget.ES2016) { + transformers.push(ts.transformES2016); + } + if (languageVersion < ts.ScriptTarget.ES2015) { + transformers.push(ts.transformES2015); + transformers.push(ts.transformGenerators); } - export const noTransformers: ts.EmitTransformers = { scriptTransformers: ts.emptyArray, declarationTransformers: ts.emptyArray }; - export function getTransformers(compilerOptions: ts.CompilerOptions, customTransformers?: ts.CustomTransformers, emitOnlyDtsFiles?: boolean): ts.EmitTransformers { - return { - scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles), - declarationTransformers: getDeclarationTransformers(customTransformers), - }; + transformers.push(getModuleTransformer(moduleKind)); + + // The ES5 transformer is last so that it can substitute expressions like `exports.default` + // for ES3. + if (languageVersion < ts.ScriptTarget.ES5) { + transformers.push(ts.transformES5); } - function getScriptTransformers(compilerOptions: ts.CompilerOptions, customTransformers?: ts.CustomTransformers, emitOnlyDtsFiles?: boolean) { - if (emitOnlyDtsFiles) - return ts.emptyArray; - const languageVersion = ts.getEmitScriptTarget(compilerOptions); - const moduleKind = ts.getEmitModuleKind(compilerOptions); - const transformers: ts.TransformerFactory[] = []; - ts.addRange(transformers, customTransformers && ts.map(customTransformers.before, wrapScriptTransformerFactory)); - transformers.push(ts.transformTypeScript); - transformers.push(ts.transformClassFields); - if (ts.getJSXTransformEnabled(compilerOptions)) { - transformers.push(ts.transformJsx); - } - if (languageVersion < ts.ScriptTarget.ESNext) { - transformers.push(ts.transformESNext); - } - if (languageVersion < ts.ScriptTarget.ES2021) { - transformers.push(ts.transformES2021); - } - if (languageVersion < ts.ScriptTarget.ES2020) { - transformers.push(ts.transformES2020); - } - if (languageVersion < ts.ScriptTarget.ES2019) { - transformers.push(ts.transformES2019); - } - if (languageVersion < ts.ScriptTarget.ES2018) { - transformers.push(ts.transformES2018); - } - if (languageVersion < ts.ScriptTarget.ES2017) { - transformers.push(ts.transformES2017); - } - if (languageVersion < ts.ScriptTarget.ES2016) { - transformers.push(ts.transformES2016); - } - if (languageVersion < ts.ScriptTarget.ES2015) { - transformers.push(ts.transformES2015); - transformers.push(ts.transformGenerators); + ts.addRange(transformers, customTransformers && ts.map(customTransformers.after, wrapScriptTransformerFactory)); + return transformers; +} + +function getDeclarationTransformers(customTransformers?: ts.CustomTransformers) { + const transformers: ts.TransformerFactory[] = []; + transformers.push(ts.transformDeclarations); + ts.addRange(transformers, customTransformers && ts.map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory)); + return transformers; +} + +/** + * Wrap a custom script or declaration transformer object in a `Transformer` callback with fallback support for transforming bundles. + */ +function wrapCustomTransformer(transformer: ts.CustomTransformer): ts.Transformer { + return node => ts.isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node); +} + +/** + * Wrap a transformer factory that may return a custom script or declaration transformer object. + */ +function wrapCustomTransformerFactory(transformer: ts.TransformerFactory | ts.CustomTransformerFactory, handleDefault: (context: ts.TransformationContext, tx: ts.Transformer) => ts.Transformer): ts.TransformerFactory { + return context => { + const customTransformer = transformer(context); + return typeof customTransformer === "function" + ? handleDefault(context, customTransformer) + : wrapCustomTransformer(customTransformer); + }; +} + +function wrapScriptTransformerFactory(transformer: ts.TransformerFactory | ts.CustomTransformerFactory): ts.TransformerFactory { + return wrapCustomTransformerFactory(transformer, ts.chainBundle); +} + +function wrapDeclarationTransformerFactory(transformer: ts.TransformerFactory | ts.CustomTransformerFactory): ts.TransformerFactory { + return wrapCustomTransformerFactory(transformer, (_, node) => node); +} + +export function noEmitSubstitution(_hint: ts.EmitHint, node: ts.Node) { + return node; +} + +export function noEmitNotification(hint: ts.EmitHint, node: ts.Node, callback: (hint: ts.EmitHint, node: ts.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. + */ +export function transformNodes(resolver: ts.EmitResolver | undefined, host: ts.EmitHost | undefined, factory: ts.NodeFactory, options: ts.CompilerOptions, nodes: readonly T[], transformers: readonly ts.TransformerFactory[], allowDtsFiles: boolean): ts.TransformationResult { + const enabledSyntaxKindFeatures = new Array(ts.SyntaxKind.Count); + let lexicalEnvironmentVariableDeclarations: ts.VariableDeclaration[]; + let lexicalEnvironmentFunctionDeclarations: ts.FunctionDeclaration[]; + let lexicalEnvironmentStatements: ts.Statement[]; + let lexicalEnvironmentFlags = ts.LexicalEnvironmentFlags.None; + let lexicalEnvironmentVariableDeclarationsStack: ts.VariableDeclaration[][] = []; + let lexicalEnvironmentFunctionDeclarationsStack: ts.FunctionDeclaration[][] = []; + let lexicalEnvironmentStatementsStack: ts.Statement[][] = []; + let lexicalEnvironmentFlagsStack: ts.LexicalEnvironmentFlags[] = []; + let lexicalEnvironmentStackOffset = 0; + let lexicalEnvironmentSuspended = false; + let blockScopedVariableDeclarationsStack: ts.Identifier[][] = []; + let blockScopeStackOffset = 0; + let blockScopedVariableDeclarations: ts.Identifier[]; + let emitHelpers: ts.EmitHelper[] | undefined; + let onSubstituteNode: ts.TransformationContext["onSubstituteNode"] = noEmitSubstitution; + let onEmitNode: ts.TransformationContext["onEmitNode"] = noEmitNotification; + let state = TransformationState.Uninitialized; + const diagnostics: ts.DiagnosticWithLocation[] = []; + + // The transformation context is provided to each transformer as part of transformer + // initialization. + const context: ts.TransformationContext = { + factory, + getCompilerOptions: () => options, + getEmitResolver: () => resolver!, + getEmitHost: () => host!, + getEmitHelperFactory: ts.memoize(() => ts.createEmitHelperFactory(context)), + startLexicalEnvironment, + suspendLexicalEnvironment, + resumeLexicalEnvironment, + endLexicalEnvironment, + setLexicalEnvironmentFlags, + getLexicalEnvironmentFlags, + hoistVariableDeclaration, + hoistFunctionDeclaration, + addInitializationStatement, + startBlockScope, + endBlockScope, + addBlockScopedVariable, + requestEmitHelper, + readEmitHelpers, + enableSubstitution, + enableEmitNotification, + isSubstitutionEnabled, + isEmitNotificationEnabled, + get onSubstituteNode() { return onSubstituteNode; }, + set onSubstituteNode(value) { + ts.Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); + ts.Debug.assert(value !== undefined, "Value must not be 'undefined'"); + onSubstituteNode = value; + }, + get onEmitNode() { return onEmitNode; }, + set onEmitNode(value) { + ts.Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); + ts.Debug.assert(value !== undefined, "Value must not be 'undefined'"); + onEmitNode = value; + }, + addDiagnostic(diag) { + diagnostics.push(diag); } + }; + + // Ensure the parse tree is clean before applying transformations + for (const node of nodes) { + ts.disposeEmitNodes(ts.getSourceFileOfNode(ts.getParseTreeNode(node))); + } - transformers.push(getModuleTransformer(moduleKind)); + ts.performance.mark("beforeTransform"); - // The ES5 transformer is last so that it can substitute expressions like `exports.default` - // for ES3. - if (languageVersion < ts.ScriptTarget.ES5) { - transformers.push(ts.transformES5); + // 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; + }; - ts.addRange(transformers, customTransformers && ts.map(customTransformers.after, wrapScriptTransformerFactory)); - return transformers; + // prevent modification of transformation hooks. + state = TransformationState.Initialized; + + // Transform each node. + const transformed: T[] = []; + for (const node of nodes) { + ts.tracing?.push(ts.tracing.Phase.Emit, "transformNodes", node.kind === ts.SyntaxKind.SourceFile ? { path: (node as any as ts.SourceFile).path } : { kind: node.kind, pos: node.pos, end: node.end }); + transformed.push((allowDtsFiles ? transformation : transformRoot)(node)); + ts.tracing?.pop(); } - function getDeclarationTransformers(customTransformers?: ts.CustomTransformers) { - const transformers: ts.TransformerFactory[] = []; - transformers.push(ts.transformDeclarations); - ts.addRange(transformers, customTransformers && ts.map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory)); - return transformers; + // prevent modification of the lexical environment. + state = TransformationState.Completed; + + ts.performance.mark("afterTransform"); + ts.performance.measure("transformTime", "beforeTransform", "afterTransform"); + + return { + transformed, + substituteNode, + emitNodeWithNotification, + isEmitNotificationEnabled, + dispose, + diagnostics + }; + + function transformRoot(node: T) { + return node && (!ts.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: ts.CustomTransformer): ts.Transformer { - return node => ts.isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node); + function enableSubstitution(kind: ts.SyntaxKind) { + ts.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: ts.TransformerFactory | ts.CustomTransformerFactory, handleDefault: (context: ts.TransformationContext, tx: ts.Transformer) => ts.Transformer): ts.TransformerFactory { - return context => { - const customTransformer = transformer(context); - return typeof customTransformer === "function" - ? handleDefault(context, customTransformer) - : wrapCustomTransformer(customTransformer); - }; + function isSubstitutionEnabled(node: ts.Node) { + return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.Substitution) !== 0 + && (ts.getEmitFlags(node) & ts.EmitFlags.NoSubstitution) === 0; } - function wrapScriptTransformerFactory(transformer: ts.TransformerFactory | ts.CustomTransformerFactory): ts.TransformerFactory { - return wrapCustomTransformerFactory(transformer, ts.chainBundle); - } - - function wrapDeclarationTransformerFactory(transformer: ts.TransformerFactory | ts.CustomTransformerFactory): ts.TransformerFactory { - return wrapCustomTransformerFactory(transformer, (_, node) => node); + /** + * 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: ts.EmitHint, node: ts.Node) { + ts.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: ts.EmitHint, node: ts.Node) { - return node; + /** + * Enables before/after emit notifications in the pretty printer for the provided SyntaxKind. + */ + function enableEmitNotification(kind: ts.SyntaxKind) { + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.EmitNotifications; } - export function noEmitNotification(hint: ts.EmitHint, node: ts.Node, callback: (hint: ts.EmitHint, node: ts.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: ts.Node) { + return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.EmitNotifications) !== 0 + || (ts.getEmitFlags(node) & ts.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: ts.EmitResolver | undefined, host: ts.EmitHost | undefined, factory: ts.NodeFactory, options: ts.CompilerOptions, nodes: readonly T[], transformers: readonly ts.TransformerFactory[], allowDtsFiles: boolean): ts.TransformationResult { - const enabledSyntaxKindFeatures = new Array(ts.SyntaxKind.Count); - let lexicalEnvironmentVariableDeclarations: ts.VariableDeclaration[]; - let lexicalEnvironmentFunctionDeclarations: ts.FunctionDeclaration[]; - let lexicalEnvironmentStatements: ts.Statement[]; - let lexicalEnvironmentFlags = ts.LexicalEnvironmentFlags.None; - let lexicalEnvironmentVariableDeclarationsStack: ts.VariableDeclaration[][] = []; - let lexicalEnvironmentFunctionDeclarationsStack: ts.FunctionDeclaration[][] = []; - let lexicalEnvironmentStatementsStack: ts.Statement[][] = []; - let lexicalEnvironmentFlagsStack: ts.LexicalEnvironmentFlags[] = []; - let lexicalEnvironmentStackOffset = 0; - let lexicalEnvironmentSuspended = false; - let blockScopedVariableDeclarationsStack: ts.Identifier[][] = []; - let blockScopeStackOffset = 0; - let blockScopedVariableDeclarations: ts.Identifier[]; - let emitHelpers: ts.EmitHelper[] | undefined; - let onSubstituteNode: ts.TransformationContext["onSubstituteNode"] = noEmitSubstitution; - let onEmitNode: ts.TransformationContext["onEmitNode"] = noEmitNotification; - let state = TransformationState.Uninitialized; - const diagnostics: ts.DiagnosticWithLocation[] = []; - - // The transformation context is provided to each transformer as part of transformer - // initialization. - const context: ts.TransformationContext = { - factory, - getCompilerOptions: () => options, - getEmitResolver: () => resolver!, - getEmitHost: () => host!, - getEmitHelperFactory: ts.memoize(() => ts.createEmitHelperFactory(context)), - startLexicalEnvironment, - suspendLexicalEnvironment, - resumeLexicalEnvironment, - endLexicalEnvironment, - setLexicalEnvironmentFlags, - getLexicalEnvironmentFlags, - hoistVariableDeclaration, - hoistFunctionDeclaration, - addInitializationStatement, - startBlockScope, - endBlockScope, - addBlockScopedVariable, - requestEmitHelper, - readEmitHelpers, - enableSubstitution, - enableEmitNotification, - isSubstitutionEnabled, - isEmitNotificationEnabled, - get onSubstituteNode() { return onSubstituteNode; }, - set onSubstituteNode(value) { - ts.Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); - ts.Debug.assert(value !== undefined, "Value must not be 'undefined'"); - onSubstituteNode = value; - }, - get onEmitNode() { return onEmitNode; }, - set onEmitNode(value) { - ts.Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); - ts.Debug.assert(value !== undefined, "Value must not be 'undefined'"); - onEmitNode = value; - }, - addDiagnostic(diag) { - diagnostics.push(diag); + function emitNodeWithNotification(hint: ts.EmitHint, node: ts.Node, emitCallback: (hint: ts.EmitHint, node: ts.Node) => void) { + ts.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) { - ts.disposeEmitNodes(ts.getSourceFileOfNode(ts.getParseTreeNode(node))); - } - - ts.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: T[] = []; - for (const node of nodes) { - ts.tracing?.push(ts.tracing.Phase.Emit, "transformNodes", node.kind === ts.SyntaxKind.SourceFile ? { path: (node as any as ts.SourceFile).path } : { kind: node.kind, pos: node.pos, end: node.end }); - transformed.push((allowDtsFiles ? transformation : transformRoot)(node)); - ts.tracing?.pop(); - } - - // prevent modification of the lexical environment. - state = TransformationState.Completed; - - ts.performance.mark("afterTransform"); - ts.performance.measure("transformTime", "beforeTransform", "afterTransform"); - - return { - transformed, - substituteNode, - emitNodeWithNotification, - isEmitNotificationEnabled, - dispose, - diagnostics - }; - - function transformRoot(node: T) { - return node && (!ts.isSourceFile(node) || !node.isDeclarationFile) ? transformation(node) : node; - } - - /** - * Enables expression substitutions in the pretty printer for the provided SyntaxKind. - */ - function enableSubstitution(kind: ts.SyntaxKind) { - ts.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: ts.Node) { - return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.Substitution) !== 0 - && (ts.getEmitFlags(node) & ts.EmitFlags.NoSubstitution) === 0; + /** + * Records a hoisted variable declaration for the provided name within a lexical environment. + */ + function hoistVariableDeclaration(name: ts.Identifier): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + const decl = ts.setEmitFlags(factory.createVariableDeclaration(name), ts.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: ts.EmitHint, node: ts.Node) { - ts.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: ts.SyntaxKind) { - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.EmitNotifications; + if (lexicalEnvironmentFlags & ts.LexicalEnvironmentFlags.InParameters) { + lexicalEnvironmentFlags |= ts.LexicalEnvironmentFlags.VariablesHoistedInParameters; } + } - /** - * Determines whether before/after emit notifications should be raised in the pretty - * printer when it emits a node. - */ - function isEmitNotificationEnabled(node: ts.Node) { - return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.EmitNotifications) !== 0 - || (ts.getEmitFlags(node) & ts.EmitFlags.AdviseOnEmitNode) !== 0; + /** + * Records a hoisted function declaration within a lexical environment. + */ + function hoistFunctionDeclaration(func: ts.FunctionDeclaration): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.setEmitFlags(func, ts.EmitFlags.CustomPrologue); + if (!lexicalEnvironmentFunctionDeclarations) { + lexicalEnvironmentFunctionDeclarations = [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: ts.EmitHint, node: ts.Node, emitCallback: (hint: ts.EmitHint, node: ts.Node) => void) { - ts.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); - } - else { - emitCallback(hint, node); - } - } + else { + lexicalEnvironmentFunctionDeclarations.push(func); } + } - /** - * Records a hoisted variable declaration for the provided name within a lexical environment. - */ - function hoistVariableDeclaration(name: ts.Identifier): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - const decl = ts.setEmitFlags(factory.createVariableDeclaration(name), ts.EmitFlags.NoNestedSourceMaps); - if (!lexicalEnvironmentVariableDeclarations) { - lexicalEnvironmentVariableDeclarations = [decl]; - } - else { - lexicalEnvironmentVariableDeclarations.push(decl); - } - if (lexicalEnvironmentFlags & ts.LexicalEnvironmentFlags.InParameters) { - lexicalEnvironmentFlags |= ts.LexicalEnvironmentFlags.VariablesHoistedInParameters; - } + /** + * Adds an initialization statement to the top of the lexical environment. + */ + function addInitializationStatement(node: ts.Statement): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.setEmitFlags(node, ts.EmitFlags.CustomPrologue); + if (!lexicalEnvironmentStatements) { + lexicalEnvironmentStatements = [node]; } - - /** - * Records a hoisted function declaration within a lexical environment. - */ - function hoistFunctionDeclaration(func: ts.FunctionDeclaration): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.setEmitFlags(func, ts.EmitFlags.CustomPrologue); - if (!lexicalEnvironmentFunctionDeclarations) { - lexicalEnvironmentFunctionDeclarations = [func]; - } - else { - lexicalEnvironmentFunctionDeclarations.push(func); - } + else { + lexicalEnvironmentStatements.push(node); } + } - /** - * Adds an initialization statement to the top of the lexical environment. - */ - function addInitializationStatement(node: ts.Statement): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.setEmitFlags(node, ts.EmitFlags.CustomPrologue); - if (!lexicalEnvironmentStatements) { - lexicalEnvironmentStatements = [node]; - } - else { - lexicalEnvironmentStatements.push(node); - } - } + /** + * 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 { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.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; + lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentStatements; + lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFlags; + lexicalEnvironmentStackOffset++; + lexicalEnvironmentVariableDeclarations = undefined!; + lexicalEnvironmentFunctionDeclarations = undefined!; + lexicalEnvironmentStatements = undefined!; + lexicalEnvironmentFlags = ts.LexicalEnvironmentFlags.None; + } - /** - * 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 { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.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; - lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentStatements; - lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFlags; - lexicalEnvironmentStackOffset++; - lexicalEnvironmentVariableDeclarations = undefined!; - lexicalEnvironmentFunctionDeclarations = undefined!; - lexicalEnvironmentStatements = undefined!; - lexicalEnvironmentFlags = ts.LexicalEnvironmentFlags.None; - } + /** Suspends the current lexical environment, usually after visiting a parameter list. */ + function suspendLexicalEnvironment(): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is already suspended."); + lexicalEnvironmentSuspended = true; + } - /** Suspends the current lexical environment, usually after visiting a parameter list. */ - function suspendLexicalEnvironment(): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is already suspended."); - lexicalEnvironmentSuspended = true; - } + /** Resumes a suspended lexical environment, usually before visiting a function body. */ + function resumeLexicalEnvironment(): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.Debug.assert(lexicalEnvironmentSuspended, "Lexical environment is not suspended."); + lexicalEnvironmentSuspended = false; + } - /** Resumes a suspended lexical environment, usually before visiting a function body. */ - function resumeLexicalEnvironment(): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.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(): ts.Statement[] | undefined { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + ts.Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); + let statements: ts.Statement[] | undefined; + if (lexicalEnvironmentVariableDeclarations || + lexicalEnvironmentFunctionDeclarations || + lexicalEnvironmentStatements) { + if (lexicalEnvironmentFunctionDeclarations) { + statements = [...lexicalEnvironmentFunctionDeclarations]; + } - /** - * Ends a lexical environment. The previous set of hoisted declarations are restored and - * any hoisted declarations added in this environment are returned. - */ - function endLexicalEnvironment(): ts.Statement[] | undefined { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - ts.Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); - let statements: ts.Statement[] | undefined; - if (lexicalEnvironmentVariableDeclarations || - lexicalEnvironmentFunctionDeclarations || - lexicalEnvironmentStatements) { - if (lexicalEnvironmentFunctionDeclarations) { - statements = [...lexicalEnvironmentFunctionDeclarations]; - } + if (lexicalEnvironmentVariableDeclarations) { + const statement = factory.createVariableStatement( + /*modifiers*/ undefined, factory.createVariableDeclarationList(lexicalEnvironmentVariableDeclarations)); + ts.setEmitFlags(statement, ts.EmitFlags.CustomPrologue); - if (lexicalEnvironmentVariableDeclarations) { - const statement = factory.createVariableStatement( - /*modifiers*/ undefined, factory.createVariableDeclarationList(lexicalEnvironmentVariableDeclarations)); - ts.setEmitFlags(statement, ts.EmitFlags.CustomPrologue); - - if (!statements) { - statements = [statement]; - } - else { - statements.push(statement); - } + if (!statements) { + statements = [statement]; } - - if (lexicalEnvironmentStatements) { - if (!statements) { - statements = [...lexicalEnvironmentStatements]; - } - else { - statements = [...statements, ...lexicalEnvironmentStatements]; - } + else { + statements.push(statement); } } - // Restore the previous lexical environment. - lexicalEnvironmentStackOffset--; - lexicalEnvironmentVariableDeclarations = lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset]; - lexicalEnvironmentFunctionDeclarations = lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset]; - lexicalEnvironmentStatements = lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset]; - lexicalEnvironmentFlags = lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset]; - if (lexicalEnvironmentStackOffset === 0) { - lexicalEnvironmentVariableDeclarationsStack = []; - lexicalEnvironmentFunctionDeclarationsStack = []; - lexicalEnvironmentStatementsStack = []; - lexicalEnvironmentFlagsStack = []; + if (lexicalEnvironmentStatements) { + if (!statements) { + statements = [...lexicalEnvironmentStatements]; + } + else { + statements = [...statements, ...lexicalEnvironmentStatements]; + } } - return statements; } - function setLexicalEnvironmentFlags(flags: ts.LexicalEnvironmentFlags, value: boolean): void { - lexicalEnvironmentFlags = value ? - lexicalEnvironmentFlags | flags : - lexicalEnvironmentFlags & ~flags; + // Restore the previous lexical environment. + lexicalEnvironmentStackOffset--; + lexicalEnvironmentVariableDeclarations = lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset]; + lexicalEnvironmentFunctionDeclarations = lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset]; + lexicalEnvironmentStatements = lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset]; + lexicalEnvironmentFlags = lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset]; + if (lexicalEnvironmentStackOffset === 0) { + lexicalEnvironmentVariableDeclarationsStack = []; + lexicalEnvironmentFunctionDeclarationsStack = []; + lexicalEnvironmentStatementsStack = []; + lexicalEnvironmentFlagsStack = []; } + return statements; + } - function getLexicalEnvironmentFlags(): ts.LexicalEnvironmentFlags { - return lexicalEnvironmentFlags; - } + function setLexicalEnvironmentFlags(flags: ts.LexicalEnvironmentFlags, value: boolean): void { + lexicalEnvironmentFlags = value ? + lexicalEnvironmentFlags | flags : + lexicalEnvironmentFlags & ~flags; + } - /** - * Starts a block scope. Any existing block hoisted variables are pushed onto the stack and the related storage variables are reset. - */ - function startBlockScope() { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot start a block scope during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot start a block scope after transformation has completed."); - blockScopedVariableDeclarationsStack[blockScopeStackOffset] = blockScopedVariableDeclarations; - blockScopeStackOffset++; - blockScopedVariableDeclarations = undefined!; - } + function getLexicalEnvironmentFlags(): ts.LexicalEnvironmentFlags { + return lexicalEnvironmentFlags; + } - /** - * Ends a block scope. The previous set of block hoisted variables are restored. Any hoisted declarations are returned. - */ - function endBlockScope() { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot end a block scope during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot end a block scope after transformation has completed."); - const statements: ts.Statement[] | undefined = ts.some(blockScopedVariableDeclarations) ? - [ - factory.createVariableStatement( - /*modifiers*/ undefined, factory.createVariableDeclarationList(blockScopedVariableDeclarations.map(identifier => factory.createVariableDeclaration(identifier)), ts.NodeFlags.Let)) - ] : undefined; - blockScopeStackOffset--; - blockScopedVariableDeclarations = blockScopedVariableDeclarationsStack[blockScopeStackOffset]; - if (blockScopeStackOffset === 0) { - blockScopedVariableDeclarationsStack = []; + /** + * Starts a block scope. Any existing block hoisted variables are pushed onto the stack and the related storage variables are reset. + */ + function startBlockScope() { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot start a block scope during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot start a block scope after transformation has completed."); + blockScopedVariableDeclarationsStack[blockScopeStackOffset] = blockScopedVariableDeclarations; + blockScopeStackOffset++; + blockScopedVariableDeclarations = undefined!; + } + + /** + * Ends a block scope. The previous set of block hoisted variables are restored. Any hoisted declarations are returned. + */ + function endBlockScope() { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot end a block scope during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot end a block scope after transformation has completed."); + const statements: ts.Statement[] | undefined = ts.some(blockScopedVariableDeclarations) ? + [ + factory.createVariableStatement( + /*modifiers*/ undefined, factory.createVariableDeclarationList(blockScopedVariableDeclarations.map(identifier => factory.createVariableDeclaration(identifier)), ts.NodeFlags.Let)) + ] : undefined; + blockScopeStackOffset--; + blockScopedVariableDeclarations = blockScopedVariableDeclarationsStack[blockScopeStackOffset]; + if (blockScopeStackOffset === 0) { + blockScopedVariableDeclarationsStack = []; + } + return statements; + } + + function addBlockScopedVariable(name: ts.Identifier): void { + ts.Debug.assert(blockScopeStackOffset > 0, "Cannot add a block scoped variable outside of an iteration body."); + (blockScopedVariableDeclarations || (blockScopedVariableDeclarations = [])).push(name); + } + + function requestEmitHelper(helper: ts.EmitHelper): void { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + ts.Debug.assert(!helper.scoped, "Cannot request a scoped emit helper."); + if (helper.dependencies) { + for (const h of helper.dependencies) { + requestEmitHelper(h); } - return statements; } + emitHelpers = ts.append(emitHelpers, helper); + } - function addBlockScopedVariable(name: ts.Identifier): void { - ts.Debug.assert(blockScopeStackOffset > 0, "Cannot add a block scoped variable outside of an iteration body."); - (blockScopedVariableDeclarations || (blockScopedVariableDeclarations = [])).push(name); - } + function readEmitHelpers(): ts.EmitHelper[] | undefined { + ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); + ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + const helpers = emitHelpers; + emitHelpers = undefined; + return helpers; + } - function requestEmitHelper(helper: ts.EmitHelper): void { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - ts.Debug.assert(!helper.scoped, "Cannot request a scoped emit helper."); - if (helper.dependencies) { - for (const h of helper.dependencies) { - requestEmitHelper(h); - } + function dispose() { + if (state < TransformationState.Disposed) { + // Clean up emit nodes on parse tree + for (const node of nodes) { + ts.disposeEmitNodes(ts.getSourceFileOfNode(ts.getParseTreeNode(node))); } - emitHelpers = ts.append(emitHelpers, helper); - } - function readEmitHelpers(): ts.EmitHelper[] | undefined { - ts.Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); - ts.Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - const helpers = emitHelpers; + // Release references to external entries for GC purposes. + lexicalEnvironmentVariableDeclarations = undefined!; + lexicalEnvironmentVariableDeclarationsStack = undefined!; + lexicalEnvironmentFunctionDeclarations = undefined!; + 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) { - ts.disposeEmitNodes(ts.getSourceFileOfNode(ts.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; } } +} - export const nullTransformationContext: ts.TransformationContext = { - factory: ts.factory, - getCompilerOptions: () => ({}), - getEmitResolver: ts.notImplemented, - getEmitHost: ts.notImplemented, - getEmitHelperFactory: ts.notImplemented, - startLexicalEnvironment: ts.noop, - resumeLexicalEnvironment: ts.noop, - suspendLexicalEnvironment: ts.noop, - endLexicalEnvironment: ts.returnUndefined, - setLexicalEnvironmentFlags: ts.noop, - getLexicalEnvironmentFlags: () => 0, - hoistVariableDeclaration: ts.noop, - hoistFunctionDeclaration: ts.noop, - addInitializationStatement: ts.noop, - startBlockScope: ts.noop, - endBlockScope: ts.returnUndefined, - addBlockScopedVariable: ts.noop, - requestEmitHelper: ts.noop, - readEmitHelpers: ts.notImplemented, - enableSubstitution: ts.noop, - enableEmitNotification: ts.noop, - isSubstitutionEnabled: ts.notImplemented, - isEmitNotificationEnabled: ts.notImplemented, - onSubstituteNode: noEmitSubstitution, - onEmitNode: noEmitNotification, - addDiagnostic: ts.noop, - }; +export const nullTransformationContext: ts.TransformationContext = { + factory: ts.factory, + getCompilerOptions: () => ({}), + getEmitResolver: ts.notImplemented, + getEmitHost: ts.notImplemented, + getEmitHelperFactory: ts.notImplemented, + startLexicalEnvironment: ts.noop, + resumeLexicalEnvironment: ts.noop, + suspendLexicalEnvironment: ts.noop, + endLexicalEnvironment: ts.returnUndefined, + setLexicalEnvironmentFlags: ts.noop, + getLexicalEnvironmentFlags: () => 0, + hoistVariableDeclaration: ts.noop, + hoistFunctionDeclaration: ts.noop, + addInitializationStatement: ts.noop, + startBlockScope: ts.noop, + endBlockScope: ts.returnUndefined, + addBlockScopedVariable: ts.noop, + requestEmitHelper: ts.noop, + readEmitHelpers: ts.notImplemented, + enableSubstitution: ts.noop, + enableEmitNotification: ts.noop, + isSubstitutionEnabled: ts.notImplemented, + isEmitNotificationEnabled: ts.notImplemented, + onSubstituteNode: noEmitSubstitution, + onEmitNode: noEmitNotification, + addDiagnostic: ts.noop, +}; } diff --git a/src/compiler/transformers/classFields.ts b/src/compiler/transformers/classFields.ts index 6e606463e28c3..062bab481a57a 100644 --- a/src/compiler/transformers/classFields.ts +++ b/src/compiler/transformers/classFields.ts @@ -1,349 +1,403 @@ /*@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, - /** - * Enables substitutions for class expressions with static fields - * which have initializers that reference the 'this' or 'super'. - */ - ClassStaticThisOrSuperReference = 1 << 1 - } - export const enum PrivateIdentifierKind { - Field = "f", - Method = "m", - Accessor = "a" - } - interface PrivateIdentifierInfoBase { - /** - * brandCheckIdentifier can contain: - * - For instance field: The WeakMap that will be the storage for the field. - * - For instance methods or accessors: The WeakSet that will be used for brand checking. - * - For static members: The constructor that will be used for brand checking. - */ - brandCheckIdentifier: ts.Identifier; - /** - * Stores if the identifier is static or not - */ - isStatic: boolean; - /** - * Stores if the identifier declaration is valid or not. Reserved names (e.g. #constructor) - * or duplicate identifiers are considered invalid. - */ - isValid: boolean; - } - interface PrivateIdentifierAccessorInfo extends PrivateIdentifierInfoBase { - kind: PrivateIdentifierKind.Accessor; - /** - * Identifier for a variable that will contain the private get accessor implementation, if any. - */ - getterName?: ts.Identifier; - /** - * Identifier for a variable that will contain the private set accessor implementation, if any. - */ - setterName?: ts.Identifier; - } - interface PrivateIdentifierMethodInfo extends PrivateIdentifierInfoBase { - kind: PrivateIdentifierKind.Method; - /** - * Identifier for a variable that will contain the private method implementation. - */ - methodName: ts.Identifier; - } - interface PrivateIdentifierInstanceFieldInfo extends PrivateIdentifierInfoBase { - kind: PrivateIdentifierKind.Field; - isStatic: false; - /** - * Defined for ease of access when in a union with PrivateIdentifierStaticFieldInfo. - */ - variableName: undefined; - } - interface PrivateIdentifierStaticFieldInfo extends PrivateIdentifierInfoBase { - kind: PrivateIdentifierKind.Field; - isStatic: true; - /** - * Contains the variable that will server as the storage for the field. - */ - variableName: ts.Identifier; - } - type PrivateIdentifierInfo = PrivateIdentifierMethodInfo | PrivateIdentifierInstanceFieldInfo | PrivateIdentifierStaticFieldInfo | PrivateIdentifierAccessorInfo; - - interface PrivateIdentifierEnvironment { - /** - * Used for prefixing generated variable names. - */ - className: string; - /** - * Used for brand check on private methods. - */ - weakSetName?: ts.Identifier; - /** - * A mapping of private names to information needed for transformation. - */ - identifiers: ts.UnderscoreEscapedMap; - } - - interface ClassLexicalEnvironment { - facts: ClassFacts; - /** - * Used for brand checks on static members, and `this` references in static initializers - */ - classConstructor: ts.Identifier | undefined; - /** - * Used for `super` references in static initializers. - */ - superClassReference: ts.Identifier | undefined; - privateIdentifierEnvironment: PrivateIdentifierEnvironment | undefined; - } - - const enum ClassFacts { - None = 0, - ClassWasDecorated = 1 << 0, - NeedsClassConstructorReference = 1 << 1, - NeedsClassSuperReference = 1 << 2, - NeedsSubstitutionForThisInClassStaticField = 1 << 3 - } +const enum ClassPropertySubstitutionFlags { + /** + * Enables substitutions for class expressions with static fields + * which have initializers that reference the class name. + */ + ClassAliases = 1 << 0, + /** + * Enables substitutions for class expressions with static fields + * which have initializers that reference the 'this' or 'super'. + */ + ClassStaticThisOrSuperReference = 1 << 1 +} +export const enum PrivateIdentifierKind { + Field = "f", + Method = "m", + Accessor = "a" +} +interface PrivateIdentifierInfoBase { + /** + * brandCheckIdentifier can contain: + * - For instance field: The WeakMap that will be the storage for the field. + * - For instance methods or accessors: The WeakSet that will be used for brand checking. + * - For static members: The constructor that will be used for brand checking. + */ + brandCheckIdentifier: ts.Identifier; + /** + * Stores if the identifier is static or not + */ + isStatic: boolean; + /** + * Stores if the identifier declaration is valid or not. Reserved names (e.g. #constructor) + * or duplicate identifiers are considered invalid. + */ + isValid: boolean; +} +interface PrivateIdentifierAccessorInfo extends PrivateIdentifierInfoBase { + kind: PrivateIdentifierKind.Accessor; + /** + * Identifier for a variable that will contain the private get accessor implementation, if any. + */ + getterName?: ts.Identifier; + /** + * Identifier for a variable that will contain the private set accessor implementation, if any. + */ + setterName?: ts.Identifier; +} +interface PrivateIdentifierMethodInfo extends PrivateIdentifierInfoBase { + kind: PrivateIdentifierKind.Method; + /** + * Identifier for a variable that will contain the private method implementation. + */ + methodName: ts.Identifier; +} +interface PrivateIdentifierInstanceFieldInfo extends PrivateIdentifierInfoBase { + kind: PrivateIdentifierKind.Field; + isStatic: false; + /** + * Defined for ease of access when in a union with PrivateIdentifierStaticFieldInfo. + */ + variableName: undefined; +} +interface PrivateIdentifierStaticFieldInfo extends PrivateIdentifierInfoBase { + kind: PrivateIdentifierKind.Field; + isStatic: true; + /** + * Contains the variable that will server as the storage for the field. + */ + variableName: ts.Identifier; +} +type PrivateIdentifierInfo = PrivateIdentifierMethodInfo | PrivateIdentifierInstanceFieldInfo | PrivateIdentifierStaticFieldInfo | PrivateIdentifierAccessorInfo; +interface PrivateIdentifierEnvironment { /** - * 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. + * Used for prefixing generated variable names. */ - export function transformClassFields(context: ts.TransformationContext) { - const { factory, hoistVariableDeclaration, endLexicalEnvironment, startLexicalEnvironment, resumeLexicalEnvironment, addBlockScopedVariable } = context; - const resolver = context.getEmitResolver(); - const compilerOptions = context.getCompilerOptions(); - const languageVersion = ts.getEmitScriptTarget(compilerOptions); - const useDefineForClassFields = ts.getUseDefineForClassFields(compilerOptions); - const shouldTransformPrivateElementsOrClassStaticBlocks = languageVersion < ts.ScriptTarget.ES2022; - - // We need to transform `this` in a static initializer into a reference to the class - // when targeting < ES2022 since the assignment will be moved outside of the class body. - const shouldTransformThisInStaticInitializers = languageVersion < ts.ScriptTarget.ES2022; - - // We don't need to transform `super` property access when targeting ES5, ES3 because - // the es2015 transformation handles those. - const shouldTransformSuperInStaticInitializers = shouldTransformThisInStaticInitializers && languageVersion >= ts.ScriptTarget.ES2015; - - const previousOnSubstituteNode = context.onSubstituteNode; - context.onSubstituteNode = onSubstituteNode; - const previousOnEmitNode = context.onEmitNode; - context.onEmitNode = onEmitNode; - - let enabledSubstitutions: ClassPropertySubstitutionFlags; - - let classAliases: ts.Identifier[]; - - /** - * Tracks what computed name expressions originating from elided names must be inlined - * at the next execution site, in document order - */ - let pendingExpressions: ts.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: ts.Statement[] | undefined; - - const classLexicalEnvironmentStack: (ClassLexicalEnvironment | undefined)[] = []; - const classLexicalEnvironmentMap = new ts.Map(); - let currentClassLexicalEnvironment: ClassLexicalEnvironment | undefined; - let currentComputedPropertyNameClassLexicalEnvironment: ClassLexicalEnvironment | undefined; - let currentStaticPropertyDeclarationOrStaticBlock: ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration | undefined; - return ts.chainBundle(context, transformSourceFile); - function transformSourceFile(node: ts.SourceFile) { - const options = context.getCompilerOptions(); - if (node.isDeclarationFile - || useDefineForClassFields && ts.getEmitScriptTarget(options) >= ts.ScriptTarget.ES2022) { - return node; - } - const visited = ts.visitEachChild(node, visitor, context); - ts.addEmitHelpers(visited, context.readEmitHelpers()); - return visited; - } - - function visitorWorker(node: ts.Node, valueIsDiscarded: boolean): ts.VisitResult { - if (node.transformFlags & ts.TransformFlags.ContainsClassFields) { - switch (node.kind) { - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ClassDeclaration: - return visitClassLike(node as ts.ClassLikeDeclaration); - case ts.SyntaxKind.PropertyDeclaration: - return visitPropertyDeclaration(node as ts.PropertyDeclaration); - case ts.SyntaxKind.VariableStatement: - return visitVariableStatement(node as ts.VariableStatement); - case ts.SyntaxKind.PrivateIdentifier: - return visitPrivateIdentifier(node as ts.PrivateIdentifier); - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return visitClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); - } + className: string; + /** + * Used for brand check on private methods. + */ + weakSetName?: ts.Identifier; + /** + * A mapping of private names to information needed for transformation. + */ + identifiers: ts.UnderscoreEscapedMap; +} + +interface ClassLexicalEnvironment { + facts: ClassFacts; + /** + * Used for brand checks on static members, and `this` references in static initializers + */ + classConstructor: ts.Identifier | undefined; + /** + * Used for `super` references in static initializers. + */ + superClassReference: ts.Identifier | undefined; + privateIdentifierEnvironment: PrivateIdentifierEnvironment | undefined; +} + +const enum ClassFacts { + None = 0, + ClassWasDecorated = 1 << 0, + NeedsClassConstructorReference = 1 << 1, + NeedsClassSuperReference = 1 << 2, + NeedsSubstitutionForThisInClassStaticField = 1 << 3 +} + +/** + * 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. + */ +export function transformClassFields(context: ts.TransformationContext) { + const { factory, hoistVariableDeclaration, endLexicalEnvironment, startLexicalEnvironment, resumeLexicalEnvironment, addBlockScopedVariable } = context; + const resolver = context.getEmitResolver(); + const compilerOptions = context.getCompilerOptions(); + const languageVersion = ts.getEmitScriptTarget(compilerOptions); + const useDefineForClassFields = ts.getUseDefineForClassFields(compilerOptions); + const shouldTransformPrivateElementsOrClassStaticBlocks = languageVersion < ts.ScriptTarget.ES2022; + + // We need to transform `this` in a static initializer into a reference to the class + // when targeting < ES2022 since the assignment will be moved outside of the class body. + const shouldTransformThisInStaticInitializers = languageVersion < ts.ScriptTarget.ES2022; + + // We don't need to transform `super` property access when targeting ES5, ES3 because + // the es2015 transformation handles those. + const shouldTransformSuperInStaticInitializers = shouldTransformThisInStaticInitializers && languageVersion >= ts.ScriptTarget.ES2015; + + const previousOnSubstituteNode = context.onSubstituteNode; + context.onSubstituteNode = onSubstituteNode; + const previousOnEmitNode = context.onEmitNode; + context.onEmitNode = onEmitNode; + + let enabledSubstitutions: ClassPropertySubstitutionFlags; + + let classAliases: ts.Identifier[]; + + /** + * Tracks what computed name expressions originating from elided names must be inlined + * at the next execution site, in document order + */ + let pendingExpressions: ts.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: ts.Statement[] | undefined; + + const classLexicalEnvironmentStack: (ClassLexicalEnvironment | undefined)[] = []; + const classLexicalEnvironmentMap = new ts.Map(); + let currentClassLexicalEnvironment: ClassLexicalEnvironment | undefined; + let currentComputedPropertyNameClassLexicalEnvironment: ClassLexicalEnvironment | undefined; + let currentStaticPropertyDeclarationOrStaticBlock: ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration | undefined; + return ts.chainBundle(context, transformSourceFile); + function transformSourceFile(node: ts.SourceFile) { + const options = context.getCompilerOptions(); + if (node.isDeclarationFile + || useDefineForClassFields && ts.getEmitScriptTarget(options) >= ts.ScriptTarget.ES2022) { + return node; + } + const visited = ts.visitEachChild(node, visitor, context); + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; + } + + function visitorWorker(node: ts.Node, valueIsDiscarded: boolean): ts.VisitResult { + if (node.transformFlags & ts.TransformFlags.ContainsClassFields) { + switch (node.kind) { + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + return visitClassLike(node as ts.ClassLikeDeclaration); + case ts.SyntaxKind.PropertyDeclaration: + return visitPropertyDeclaration(node as ts.PropertyDeclaration); + case ts.SyntaxKind.VariableStatement: + return visitVariableStatement(node as ts.VariableStatement); + case ts.SyntaxKind.PrivateIdentifier: + return visitPrivateIdentifier(node as ts.PrivateIdentifier); + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return visitClassStaticBlockDeclaration(node as ts.ClassStaticBlockDeclaration); } - if (node.transformFlags & ts.TransformFlags.ContainsClassFields || - node.transformFlags & ts.TransformFlags.ContainsLexicalSuper && - shouldTransformSuperInStaticInitializers && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment) { - switch (node.kind) { - case ts.SyntaxKind.PrefixUnaryExpression: - case ts.SyntaxKind.PostfixUnaryExpression: - return visitPreOrPostfixUnaryExpression(node as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, valueIsDiscarded); - case ts.SyntaxKind.BinaryExpression: - return visitBinaryExpression(node as ts.BinaryExpression, valueIsDiscarded); - case ts.SyntaxKind.CallExpression: - return visitCallExpression(node as ts.CallExpression); - case ts.SyntaxKind.TaggedTemplateExpression: - return visitTaggedTemplateExpression(node as ts.TaggedTemplateExpression); - case ts.SyntaxKind.PropertyAccessExpression: - return visitPropertyAccessExpression(node as ts.PropertyAccessExpression); - case ts.SyntaxKind.ElementAccessExpression: - return visitElementAccessExpression(node as ts.ElementAccessExpression); - case ts.SyntaxKind.ExpressionStatement: - return visitExpressionStatement(node as ts.ExpressionStatement); - case ts.SyntaxKind.ForStatement: - return visitForStatement(node as ts.ForStatement); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: { - const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; - currentStaticPropertyDeclarationOrStaticBlock = undefined; - const result = ts.visitEachChild(node, visitor, context); - currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; - return result; - } + } + if (node.transformFlags & ts.TransformFlags.ContainsClassFields || + node.transformFlags & ts.TransformFlags.ContainsLexicalSuper && + shouldTransformSuperInStaticInitializers && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment) { + switch (node.kind) { + case ts.SyntaxKind.PrefixUnaryExpression: + case ts.SyntaxKind.PostfixUnaryExpression: + return visitPreOrPostfixUnaryExpression(node as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, valueIsDiscarded); + case ts.SyntaxKind.BinaryExpression: + return visitBinaryExpression(node as ts.BinaryExpression, valueIsDiscarded); + case ts.SyntaxKind.CallExpression: + return visitCallExpression(node as ts.CallExpression); + case ts.SyntaxKind.TaggedTemplateExpression: + return visitTaggedTemplateExpression(node as ts.TaggedTemplateExpression); + case ts.SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression(node as ts.PropertyAccessExpression); + case ts.SyntaxKind.ElementAccessExpression: + return visitElementAccessExpression(node as ts.ElementAccessExpression); + case ts.SyntaxKind.ExpressionStatement: + return visitExpressionStatement(node as ts.ExpressionStatement); + case ts.SyntaxKind.ForStatement: + return visitForStatement(node as ts.ForStatement); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: { + const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; + currentStaticPropertyDeclarationOrStaticBlock = undefined; + const result = ts.visitEachChild(node, visitor, context); + currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; + return result; } } - return ts.visitEachChild(node, visitor, context); } + return ts.visitEachChild(node, visitor, context); + } + + function discardedValueVisitor(node: ts.Node): ts.VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ true); + } - function discardedValueVisitor(node: ts.Node): ts.VisitResult { - return visitorWorker(node, /*valueIsDiscarded*/ true); + function visitor(node: ts.Node): ts.VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ false); + } + + function heritageClauseVisitor(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.HeritageClause: + return ts.visitEachChild(node, heritageClauseVisitor, context); + case ts.SyntaxKind.ExpressionWithTypeArguments: + return visitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); } + return visitor(node); + } - function visitor(node: ts.Node): ts.VisitResult { - return visitorWorker(node, /*valueIsDiscarded*/ false); + function visitorDestructuringTarget(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ArrayLiteralExpression: + return visitAssignmentPattern(node as ts.AssignmentPattern); + default: + return visitor(node); } + } - function heritageClauseVisitor(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.HeritageClause: - return ts.visitEachChild(node, heritageClauseVisitor, context); - case ts.SyntaxKind.ExpressionWithTypeArguments: - return visitExpressionWithTypeArguments(node as ts.ExpressionWithTypeArguments); - } - 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, + * unless we are in a statement position - otherwise this will not trigger + * a SyntaxError. + */ + function visitPrivateIdentifier(node: ts.PrivateIdentifier) { + if (!shouldTransformPrivateElementsOrClassStaticBlocks) { + return node; } + if (ts.isStatement(node.parent)) { + return node; + } + return ts.setOriginalNode(factory.createIdentifier(""), node); + } - function visitorDestructuringTarget(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ArrayLiteralExpression: - return visitAssignmentPattern(node as ts.AssignmentPattern); - default: - return visitor(node); - } + /** + * Visits `#id in expr` + */ + function visitPrivateIdentifierInInExpression(node: ts.BinaryExpression) { + if (!shouldTransformPrivateElementsOrClassStaticBlocks) { + return node; + } + const privId = node.left; + ts.Debug.assertNode(privId, ts.isPrivateIdentifier); + ts.Debug.assert(node.operatorToken.kind === ts.SyntaxKind.InKeyword); + const info = accessPrivateIdentifier(privId); + if (info) { + const receiver = ts.visitNode(node.right, visitor, ts.isExpression); + return ts.setOriginalNode(context.getEmitHelperFactory().createClassPrivateFieldInHelper(info.brandCheckIdentifier, receiver), 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, - * unless we are in a statement position - otherwise this will not trigger - * a SyntaxError. - */ - function visitPrivateIdentifier(node: ts.PrivateIdentifier) { - if (!shouldTransformPrivateElementsOrClassStaticBlocks) { - return node; - } - if (ts.isStatement(node.parent)) { + // Private name has not been declared. Subsequent transformers will handle this error + return ts.visitEachChild(node, visitor, context); + } + + /** + * Visits the members of a class that has fields. + * + * @param node The node to visit. + */ + function classElementVisitor(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.Constructor: + // Constructors for classes using class fields are transformed in + // `visitClassDeclaration` or `visitClassExpression`. + return undefined; + + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: + return visitMethodOrAccessorDeclaration(node as ts.MethodDeclaration | ts.AccessorDeclaration); + case ts.SyntaxKind.PropertyDeclaration: + return visitPropertyDeclaration(node as ts.PropertyDeclaration); + case ts.SyntaxKind.ComputedPropertyName: + return visitComputedPropertyName(node as ts.ComputedPropertyName); + case ts.SyntaxKind.SemicolonClassElement: return node; - } - return ts.setOriginalNode(factory.createIdentifier(""), node); + + default: + return visitor(node); } + } - /** - * Visits `#id in expr` - */ - function visitPrivateIdentifierInInExpression(node: ts.BinaryExpression) { - if (!shouldTransformPrivateElementsOrClassStaticBlocks) { - return node; - } - const privId = node.left; - ts.Debug.assertNode(privId, ts.isPrivateIdentifier); - ts.Debug.assert(node.operatorToken.kind === ts.SyntaxKind.InKeyword); - const info = accessPrivateIdentifier(privId); - if (info) { - const receiver = ts.visitNode(node.right, visitor, ts.isExpression); - return ts.setOriginalNode(context.getEmitHelperFactory().createClassPrivateFieldInHelper(info.brandCheckIdentifier, receiver), node); - } + function visitVariableStatement(node: ts.VariableStatement) { + const savedPendingStatements = pendingStatements; + pendingStatements = []; - // Private name has not been declared. Subsequent transformers will handle this error - return ts.visitEachChild(node, visitor, context); + const visitedNode = ts.visitEachChild(node, visitor, context); + const statement = ts.some(pendingStatements) ? + [visitedNode, ...pendingStatements] : + visitedNode; + + pendingStatements = savedPendingStatements; + return statement; + } + + function visitComputedPropertyName(name: ts.ComputedPropertyName) { + let node = ts.visitEachChild(name, visitor, context); + if (ts.some(pendingExpressions)) { + const expressions = pendingExpressions; + expressions.push(node.expression); + pendingExpressions = []; + node = factory.updateComputedPropertyName(node, factory.inlineExpressions(expressions)); } + return node; + } - /** - * Visits the members of a class that has fields. - * - * @param node The node to visit. - */ - function classElementVisitor(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.Constructor: - // Constructors for classes using class fields are transformed in - // `visitClassDeclaration` or `visitClassExpression`. - return undefined; + function visitMethodOrAccessorDeclaration(node: ts.MethodDeclaration | ts.AccessorDeclaration) { + ts.Debug.assert(!ts.some(node.decorators)); + if (!shouldTransformPrivateElementsOrClassStaticBlocks || !ts.isPrivateIdentifier(node.name)) { + return ts.visitEachChild(node, classElementVisitor, context); + } - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: - return visitMethodOrAccessorDeclaration(node as ts.MethodDeclaration | ts.AccessorDeclaration); - case ts.SyntaxKind.PropertyDeclaration: - return visitPropertyDeclaration(node as ts.PropertyDeclaration); - case ts.SyntaxKind.ComputedPropertyName: - return visitComputedPropertyName(node as ts.ComputedPropertyName); - case ts.SyntaxKind.SemicolonClassElement: - return node; + // leave invalid code untransformed + const info = accessPrivateIdentifier(node.name); + ts.Debug.assert(info, "Undeclared private name for property declaration."); + if (!info.isValid) { + return node; + } - default: - return visitor(node); - } + const functionName = getHoistedFunctionName(node); + if (functionName) { + getPendingExpressions().push(factory.createAssignment(functionName, factory.createFunctionExpression(ts.filter(node.modifiers, m => !ts.isStaticModifier(m)), node.asteriskToken, functionName, + /* typeParameters */ undefined, ts.visitParameterList(node.parameters, classElementVisitor, context), + /* type */ undefined, ts.visitFunctionBody(node.body!, classElementVisitor, context)))); } - function visitVariableStatement(node: ts.VariableStatement) { - const savedPendingStatements = pendingStatements; - pendingStatements = []; + // remove method declaration from class + return undefined; + } - const visitedNode = ts.visitEachChild(node, visitor, context); - const statement = ts.some(pendingStatements) ? - [visitedNode, ...pendingStatements] : - visitedNode; + function getHoistedFunctionName(node: ts.MethodDeclaration | ts.AccessorDeclaration) { + ts.Debug.assert(ts.isPrivateIdentifier(node.name)); + const info = accessPrivateIdentifier(node.name); + ts.Debug.assert(info, "Undeclared private name for property declaration."); - pendingStatements = savedPendingStatements; - return statement; + if (info.kind === PrivateIdentifierKind.Method) { + return info.methodName; } - function visitComputedPropertyName(name: ts.ComputedPropertyName) { - let node = ts.visitEachChild(name, visitor, context); - if (ts.some(pendingExpressions)) { - const expressions = pendingExpressions; - expressions.push(node.expression); - pendingExpressions = []; - node = factory.updateComputedPropertyName(node, factory.inlineExpressions(expressions)); + if (info.kind === PrivateIdentifierKind.Accessor) { + if (ts.isGetAccessor(node)) { + return info.getterName; + } + if (ts.isSetAccessor(node)) { + return info.setterName; } - return node; } + } + + function visitPropertyDeclaration(node: ts.PropertyDeclaration) { + ts.Debug.assert(!ts.some(node.decorators)); + if (ts.isPrivateIdentifier(node.name)) { + if (!shouldTransformPrivateElementsOrClassStaticBlocks) { + if (ts.isStatic(node)) { + // static fields are left as is + return ts.visitEachChild(node, visitor, context); + } - function visitMethodOrAccessorDeclaration(node: ts.MethodDeclaration | ts.AccessorDeclaration) { - ts.Debug.assert(!ts.some(node.decorators)); - if (!shouldTransformPrivateElementsOrClassStaticBlocks || !ts.isPrivateIdentifier(node.name)) { - return ts.visitEachChild(node, classElementVisitor, context); + // Initializer is elided as the field is initialized in transformConstructor. + return factory.updatePropertyDeclaration(node, + /*decorators*/ undefined, ts.visitNodes(node.modifiers, visitor, ts.isModifier), node.name, + /*questionOrExclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined); } // leave invalid code untransformed @@ -352,169 +406,167 @@ namespace ts { if (!info.isValid) { return node; } - - const functionName = getHoistedFunctionName(node); - if (functionName) { - getPendingExpressions().push(factory.createAssignment(functionName, factory.createFunctionExpression(ts.filter(node.modifiers, m => !ts.isStaticModifier(m)), node.asteriskToken, functionName, - /* typeParameters */ undefined, ts.visitParameterList(node.parameters, classElementVisitor, context), - /* type */ undefined, ts.visitFunctionBody(node.body!, classElementVisitor, context)))); - } - - // remove method declaration from class - return 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 || useDefineForClassFields); + if (expr && !ts.isSimpleInlineableExpression(expr)) { + getPendingExpressions().push(expr); } - function getHoistedFunctionName(node: ts.MethodDeclaration | ts.AccessorDeclaration) { - ts.Debug.assert(ts.isPrivateIdentifier(node.name)); - const info = accessPrivateIdentifier(node.name); - ts.Debug.assert(info, "Undeclared private name for property declaration."); - - if (info.kind === PrivateIdentifierKind.Method) { - return info.methodName; - } + if (ts.isStatic(node) && !shouldTransformPrivateElementsOrClassStaticBlocks && !useDefineForClassFields) { + const initializerStatement = transformPropertyOrClassStaticBlock(node, factory.createThis()); + if (initializerStatement) { + const staticBlock = factory.createClassStaticBlockDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, factory.createBlock([initializerStatement])); + ts.setOriginalNode(staticBlock, node); + ts.setCommentRange(staticBlock, node); - if (info.kind === PrivateIdentifierKind.Accessor) { - if (ts.isGetAccessor(node)) { - return info.getterName; - } - if (ts.isSetAccessor(node)) { - return info.setterName; - } + // Set the comment range for the statement to an empty synthetic range + // and drop synthetic comments from the statement to avoid printing them twice. + ts.setCommentRange(initializerStatement, { pos: -1, end: -1 }); + ts.setSyntheticLeadingComments(initializerStatement, undefined); + ts.setSyntheticTrailingComments(initializerStatement, undefined); + return staticBlock; } } - function visitPropertyDeclaration(node: ts.PropertyDeclaration) { - ts.Debug.assert(!ts.some(node.decorators)); - if (ts.isPrivateIdentifier(node.name)) { - if (!shouldTransformPrivateElementsOrClassStaticBlocks) { - if (ts.isStatic(node)) { - // static fields are left as is - return ts.visitEachChild(node, visitor, context); - } - - // Initializer is elided as the field is initialized in transformConstructor. - return factory.updatePropertyDeclaration(node, - /*decorators*/ undefined, ts.visitNodes(node.modifiers, visitor, ts.isModifier), node.name, - /*questionOrExclamationToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined); - } + return undefined; + } - // leave invalid code untransformed - const info = accessPrivateIdentifier(node.name); - ts.Debug.assert(info, "Undeclared private name for property declaration."); - if (!info.isValid) { - return node; - } - } - // 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 || useDefineForClassFields); - if (expr && !ts.isSimpleInlineableExpression(expr)) { - getPendingExpressions().push(expr); - } + function createPrivateIdentifierAccess(info: PrivateIdentifierInfo, receiver: ts.Expression): ts.Expression { + return createPrivateIdentifierAccessHelper(info, ts.visitNode(receiver, visitor, ts.isExpression)); + } - if (ts.isStatic(node) && !shouldTransformPrivateElementsOrClassStaticBlocks && !useDefineForClassFields) { - const initializerStatement = transformPropertyOrClassStaticBlock(node, factory.createThis()); - if (initializerStatement) { - const staticBlock = factory.createClassStaticBlockDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, factory.createBlock([initializerStatement])); - ts.setOriginalNode(staticBlock, node); - ts.setCommentRange(staticBlock, node); - - // Set the comment range for the statement to an empty synthetic range - // and drop synthetic comments from the statement to avoid printing them twice. - ts.setCommentRange(initializerStatement, { pos: -1, end: -1 }); - ts.setSyntheticLeadingComments(initializerStatement, undefined); - ts.setSyntheticTrailingComments(initializerStatement, undefined); - return staticBlock; - } - } + function createPrivateIdentifierAccessHelper(info: PrivateIdentifierInfo, receiver: ts.Expression): ts.Expression { + ts.setCommentRange(receiver, ts.moveRangePos(receiver, -1)); - return undefined; + switch(info.kind) { + case PrivateIdentifierKind.Accessor: + return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.getterName); + case PrivateIdentifierKind.Method: + return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.methodName); + case PrivateIdentifierKind.Field: + return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.variableName); + default: + ts.Debug.assertNever(info, "Unknown private element type"); } + } - function createPrivateIdentifierAccess(info: PrivateIdentifierInfo, receiver: ts.Expression): ts.Expression { - return createPrivateIdentifierAccessHelper(info, ts.visitNode(receiver, visitor, ts.isExpression)); + function visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifier(node.name)) { + const privateIdentifierInfo = accessPrivateIdentifier(node.name); + if (privateIdentifierInfo) { + return ts.setTextRange(ts.setOriginalNode(createPrivateIdentifierAccess(privateIdentifierInfo, node.expression), node), node); + } } - - function createPrivateIdentifierAccessHelper(info: PrivateIdentifierInfo, receiver: ts.Expression): ts.Expression { - ts.setCommentRange(receiver, ts.moveRangePos(receiver, -1)); - - switch(info.kind) { - case PrivateIdentifierKind.Accessor: - return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.getterName); - case PrivateIdentifierKind.Method: - return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.methodName); - case PrivateIdentifierKind.Field: - return context.getEmitHelperFactory().createClassPrivateFieldGetHelper(receiver, info.brandCheckIdentifier, info.kind, info.variableName); - default: - ts.Debug.assertNever(info, "Unknown private element type"); + if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node) && + ts.isIdentifier(node.name) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment) { + const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; + if (facts & ClassFacts.ClassWasDecorated) { + return visitInvalidSuperProperty(node); + } + if (classConstructor && superClassReference) { + // converts `super.x` into `Reflect.get(_baseTemp, "x", _classTemp)` + const superProperty = factory.createReflectGetCall(superClassReference, factory.createStringLiteralFromNode(node.name), classConstructor); + ts.setOriginalNode(superProperty, node.expression); + ts.setTextRange(superProperty, node.expression); + return superProperty; } } + return ts.visitEachChild(node, visitor, context); + } - function visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifier(node.name)) { - const privateIdentifierInfo = accessPrivateIdentifier(node.name); - if (privateIdentifierInfo) { - return ts.setTextRange(ts.setOriginalNode(createPrivateIdentifierAccess(privateIdentifierInfo, node.expression), node), node); - } + function visitElementAccessExpression(node: ts.ElementAccessExpression) { + if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment) { + const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; + if (facts & ClassFacts.ClassWasDecorated) { + return visitInvalidSuperProperty(node); } - if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node) && - ts.isIdentifier(node.name) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment) { - const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; - if (facts & ClassFacts.ClassWasDecorated) { - return visitInvalidSuperProperty(node); - } - if (classConstructor && superClassReference) { - // converts `super.x` into `Reflect.get(_baseTemp, "x", _classTemp)` - const superProperty = factory.createReflectGetCall(superClassReference, factory.createStringLiteralFromNode(node.name), classConstructor); - ts.setOriginalNode(superProperty, node.expression); - ts.setTextRange(superProperty, node.expression); - return superProperty; - } + + if (classConstructor && superClassReference) { + // converts `super[x]` into `Reflect.get(_baseTemp, x, _classTemp)` + const superProperty = factory.createReflectGetCall(superClassReference, ts.visitNode(node.argumentExpression, visitor, ts.isExpression), classConstructor); + ts.setOriginalNode(superProperty, node.expression); + ts.setTextRange(superProperty, node.expression); + return superProperty; } - return ts.visitEachChild(node, visitor, context); } + return ts.visitEachChild(node, visitor, context); + } - function visitElementAccessExpression(node: ts.ElementAccessExpression) { - if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node) && + function visitPreOrPostfixUnaryExpression(node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, valueIsDiscarded: boolean) { + if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.operand)) { + let info: PrivateIdentifierInfo | undefined; + if (info = accessPrivateIdentifier(node.operand.name)) { + const receiver = ts.visitNode(node.operand.expression, visitor, ts.isExpression); + const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + + let expression: ts.Expression = createPrivateIdentifierAccess(info, readExpression); + const temp = ts.isPrefixUnaryExpression(node) || valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); + expression = ts.expandPreOrPostfixIncrementOrDecrementExpression(factory, node, expression, hoistVariableDeclaration, temp); + expression = createPrivateIdentifierAssignment(info, initializeExpression || readExpression, expression, ts.SyntaxKind.EqualsToken); + ts.setOriginalNode(expression, node); + ts.setTextRange(expression, node); + if (temp) { + expression = factory.createComma(expression, temp); + ts.setTextRange(expression, node); + } + return expression; + } + } + else if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node.operand) && currentStaticPropertyDeclarationOrStaticBlock && currentClassLexicalEnvironment) { + // converts `++super.a` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = ++_a), _classTemp), _b)` + // converts `++super[f()]` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = ++_b), _classTemp), _c)` + // converts `--super.a` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = --_a), _classTemp), _b)` + // converts `--super[f()]` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = --_b), _classTemp), _c)` + // converts `super.a++` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = _a++), _classTemp), _b)` + // converts `super[f()]++` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = _b++), _classTemp), _c)` + // converts `super.a--` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = _a--), _classTemp), _b)` + // converts `super[f()]--` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = _b--), _classTemp), _c)` const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; if (facts & ClassFacts.ClassWasDecorated) { - return visitInvalidSuperProperty(node); + const operand = visitInvalidSuperProperty(node.operand); + return ts.isPrefixUnaryExpression(node) ? + factory.updatePrefixUnaryExpression(node, operand) : + factory.updatePostfixUnaryExpression(node, operand); } - if (classConstructor && superClassReference) { - // converts `super[x]` into `Reflect.get(_baseTemp, x, _classTemp)` - const superProperty = factory.createReflectGetCall(superClassReference, ts.visitNode(node.argumentExpression, visitor, ts.isExpression), classConstructor); - ts.setOriginalNode(superProperty, node.expression); - ts.setTextRange(superProperty, node.expression); - return superProperty; - } - } - return ts.visitEachChild(node, visitor, context); - } - - function visitPreOrPostfixUnaryExpression(node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, valueIsDiscarded: boolean) { - if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) { - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.operand)) { - let info: PrivateIdentifierInfo | undefined; - if (info = accessPrivateIdentifier(node.operand.name)) { - const receiver = ts.visitNode(node.operand.expression, visitor, ts.isExpression); - const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + let setterName: ts.Expression | undefined; + let getterName: ts.Expression | undefined; + if (ts.isPropertyAccessExpression(node.operand)) { + if (ts.isIdentifier(node.operand.name)) { + getterName = setterName = factory.createStringLiteralFromNode(node.operand.name); + } + } + else { + if (ts.isSimpleInlineableExpression(node.operand.argumentExpression)) { + getterName = setterName = node.operand.argumentExpression; + } + else { + getterName = factory.createTempVariable(hoistVariableDeclaration); + setterName = factory.createAssignment(getterName, ts.visitNode(node.operand.argumentExpression, visitor, ts.isExpression)); + } + } + if (setterName && getterName) { + let expression: ts.Expression = factory.createReflectGetCall(superClassReference, getterName, classConstructor); + ts.setTextRange(expression, node.operand); - let expression: ts.Expression = createPrivateIdentifierAccess(info, readExpression); - const temp = ts.isPrefixUnaryExpression(node) || valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); + const temp = valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); expression = ts.expandPreOrPostfixIncrementOrDecrementExpression(factory, node, expression, hoistVariableDeclaration, temp); - expression = createPrivateIdentifierAssignment(info, initializeExpression || readExpression, expression, ts.SyntaxKind.EqualsToken); + expression = factory.createReflectSetCall(superClassReference, setterName, expression, classConstructor); ts.setOriginalNode(expression, node); ts.setTextRange(expression, node); if (temp) { @@ -524,1243 +576,1232 @@ namespace ts { return expression; } } - else if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node.operand) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment) { - // converts `++super.a` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = ++_a), _classTemp), _b)` - // converts `++super[f()]` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = ++_b), _classTemp), _c)` - // converts `--super.a` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = --_a), _classTemp), _b)` - // converts `--super[f()]` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = --_b), _classTemp), _c)` - // converts `super.a++` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = _a++), _classTemp), _b)` - // converts `super[f()]++` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = _b++), _classTemp), _c)` - // converts `super.a--` into `(Reflect.set(_baseTemp, "a", (_a = Reflect.get(_baseTemp, "a", _classTemp), _b = _a--), _classTemp), _b)` - // converts `super[f()]--` into `(Reflect.set(_baseTemp, _a = f(), (_b = Reflect.get(_baseTemp, _a, _classTemp), _c = _b--), _classTemp), _c)` - const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; - if (facts & ClassFacts.ClassWasDecorated) { - const operand = visitInvalidSuperProperty(node.operand); - return ts.isPrefixUnaryExpression(node) ? - factory.updatePrefixUnaryExpression(node, operand) : - factory.updatePostfixUnaryExpression(node, operand); - } - if (classConstructor && superClassReference) { - let setterName: ts.Expression | undefined; - let getterName: ts.Expression | undefined; - if (ts.isPropertyAccessExpression(node.operand)) { - if (ts.isIdentifier(node.operand.name)) { - getterName = setterName = factory.createStringLiteralFromNode(node.operand.name); - } - } - else { - if (ts.isSimpleInlineableExpression(node.operand.argumentExpression)) { - getterName = setterName = node.operand.argumentExpression; - } - else { - getterName = factory.createTempVariable(hoistVariableDeclaration); - setterName = factory.createAssignment(getterName, ts.visitNode(node.operand.argumentExpression, visitor, ts.isExpression)); - } - } - if (setterName && getterName) { - let expression: ts.Expression = factory.createReflectGetCall(superClassReference, getterName, classConstructor); - ts.setTextRange(expression, node.operand); - - const temp = valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); - expression = ts.expandPreOrPostfixIncrementOrDecrementExpression(factory, node, expression, hoistVariableDeclaration, temp); - expression = factory.createReflectSetCall(superClassReference, setterName, expression, classConstructor); - ts.setOriginalNode(expression, node); - ts.setTextRange(expression, node); - if (temp) { - expression = factory.createComma(expression, temp); - ts.setTextRange(expression, node); - } - return expression; - } - } - } } - return ts.visitEachChild(node, visitor, context); } + return ts.visitEachChild(node, visitor, context); + } - function visitForStatement(node: ts.ForStatement) { - return factory.updateForStatement(node, ts.visitNode(node.initializer, discardedValueVisitor, ts.isForInitializer), ts.visitNode(node.condition, visitor, ts.isExpression), ts.visitNode(node.incrementor, discardedValueVisitor, ts.isExpression), ts.visitIterationBody(node.statement, visitor, context)); - } + function visitForStatement(node: ts.ForStatement) { + return factory.updateForStatement(node, ts.visitNode(node.initializer, discardedValueVisitor, ts.isForInitializer), ts.visitNode(node.condition, visitor, ts.isExpression), ts.visitNode(node.incrementor, discardedValueVisitor, ts.isExpression), ts.visitIterationBody(node.statement, visitor, context)); + } - function visitExpressionStatement(node: ts.ExpressionStatement) { - return factory.updateExpressionStatement(node, ts.visitNode(node.expression, discardedValueVisitor, ts.isExpression)); - } + function visitExpressionStatement(node: ts.ExpressionStatement) { + return factory.updateExpressionStatement(node, ts.visitNode(node.expression, discardedValueVisitor, ts.isExpression)); + } - function createCopiableReceiverExpr(receiver: ts.Expression): { - readExpression: ts.Expression; - initializeExpression: ts.Expression | undefined; - } { - const clone = ts.nodeIsSynthesized(receiver) ? receiver : factory.cloneNode(receiver); - if (ts.isSimpleInlineableExpression(receiver)) { - return { readExpression: clone, initializeExpression: undefined }; - } - const readExpression = factory.createTempVariable(hoistVariableDeclaration); - const initializeExpression = factory.createAssignment(readExpression, clone); - return { readExpression, initializeExpression }; - } - - function visitCallExpression(node: ts.CallExpression) { - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.expression)) { - // Transform call expressions of private names to properly bind the `this` parameter. - const { thisArg, target } = factory.createCallBinding(node.expression, hoistVariableDeclaration, languageVersion); - if (ts.isCallChain(node)) { - return factory.updateCallChain(node, factory.createPropertyAccessChain(ts.visitNode(target, visitor), node.questionDotToken, "call"), - /*questionDotToken*/ undefined, - /*typeArguments*/ undefined, [ts.visitNode(thisArg, visitor, ts.isExpression), ...ts.visitNodes(node.arguments, visitor, ts.isExpression)]); - } - return factory.updateCallExpression(node, factory.createPropertyAccessExpression(ts.visitNode(target, visitor), "call"), + function createCopiableReceiverExpr(receiver: ts.Expression): { + readExpression: ts.Expression; + initializeExpression: ts.Expression | undefined; + } { + const clone = ts.nodeIsSynthesized(receiver) ? receiver : factory.cloneNode(receiver); + if (ts.isSimpleInlineableExpression(receiver)) { + return { readExpression: clone, initializeExpression: undefined }; + } + const readExpression = factory.createTempVariable(hoistVariableDeclaration); + const initializeExpression = factory.createAssignment(readExpression, clone); + return { readExpression, initializeExpression }; + } + + function visitCallExpression(node: ts.CallExpression) { + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.expression)) { + // Transform call expressions of private names to properly bind the `this` parameter. + const { thisArg, target } = factory.createCallBinding(node.expression, hoistVariableDeclaration, languageVersion); + if (ts.isCallChain(node)) { + return factory.updateCallChain(node, factory.createPropertyAccessChain(ts.visitNode(target, visitor), node.questionDotToken, "call"), + /*questionDotToken*/ undefined, /*typeArguments*/ undefined, [ts.visitNode(thisArg, visitor, ts.isExpression), ...ts.visitNodes(node.arguments, visitor, ts.isExpression)]); } + return factory.updateCallExpression(node, factory.createPropertyAccessExpression(ts.visitNode(target, visitor), "call"), + /*typeArguments*/ undefined, [ts.visitNode(thisArg, visitor, ts.isExpression), ...ts.visitNodes(node.arguments, visitor, ts.isExpression)]); + } - if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node.expression) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment?.classConstructor) { - - // converts `super.f(...)` into `Reflect.get(_baseTemp, "f", _classTemp).call(_classTemp, ...)` - const invocation = factory.createFunctionCallCall(ts.visitNode(node.expression, visitor, ts.isExpression), currentClassLexicalEnvironment.classConstructor, ts.visitNodes(node.arguments, visitor, ts.isExpression)); - ts.setOriginalNode(invocation, node); - ts.setTextRange(invocation, node); - return invocation; - } + if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node.expression) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment?.classConstructor) { - return ts.visitEachChild(node, visitor, context); + // converts `super.f(...)` into `Reflect.get(_baseTemp, "f", _classTemp).call(_classTemp, ...)` + const invocation = factory.createFunctionCallCall(ts.visitNode(node.expression, visitor, ts.isExpression), currentClassLexicalEnvironment.classConstructor, ts.visitNodes(node.arguments, visitor, ts.isExpression)); + ts.setOriginalNode(invocation, node); + ts.setTextRange(invocation, node); + return invocation; } - function visitTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.tag)) { - // Bind the `this` correctly for tagged template literals when the tag is a private identifier property access. - const { thisArg, target } = factory.createCallBinding(node.tag, hoistVariableDeclaration, languageVersion); - return factory.updateTaggedTemplateExpression(node, factory.createCallExpression(factory.createPropertyAccessExpression(ts.visitNode(target, visitor), "bind"), - /*typeArguments*/ undefined, [ts.visitNode(thisArg, visitor, ts.isExpression)]), - /*typeArguments*/ undefined, ts.visitNode(node.template, visitor, ts.isTemplateLiteral)); - } - if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node.tag) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment?.classConstructor) { - - // converts `` super.f`x` `` into `` Reflect.get(_baseTemp, "f", _classTemp).bind(_classTemp)`x` `` - const invocation = factory.createFunctionBindCall(ts.visitNode(node.tag, visitor, ts.isExpression), currentClassLexicalEnvironment.classConstructor, []); - ts.setOriginalNode(invocation, node); - ts.setTextRange(invocation, node); - return factory.updateTaggedTemplateExpression(node, invocation, - /*typeArguments*/ undefined, ts.visitNode(node.template, visitor, ts.isTemplateLiteral)); - } - return ts.visitEachChild(node, visitor, context); - } - function transformClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { - if (shouldTransformPrivateElementsOrClassStaticBlocks) { - if (currentClassLexicalEnvironment) { - classLexicalEnvironmentMap.set(ts.getOriginalNodeId(node), currentClassLexicalEnvironment); - } + return ts.visitEachChild(node, visitor, context); + } - startLexicalEnvironment(); - const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; - currentStaticPropertyDeclarationOrStaticBlock = node; - let statements = ts.visitNodes(node.body.statements, visitor, ts.isStatement); - statements = factory.mergeLexicalEnvironment(statements, endLexicalEnvironment()); - currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; - - const iife = factory.createImmediatelyInvokedArrowFunction(statements); - ts.setOriginalNode(iife, node); - ts.setTextRange(iife, node); - ts.addEmitFlags(iife, ts.EmitFlags.AdviseOnEmitNode); - return iife; + function visitTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.tag)) { + // Bind the `this` correctly for tagged template literals when the tag is a private identifier property access. + const { thisArg, target } = factory.createCallBinding(node.tag, hoistVariableDeclaration, languageVersion); + return factory.updateTaggedTemplateExpression(node, factory.createCallExpression(factory.createPropertyAccessExpression(ts.visitNode(target, visitor), "bind"), + /*typeArguments*/ undefined, [ts.visitNode(thisArg, visitor, ts.isExpression)]), + /*typeArguments*/ undefined, ts.visitNode(node.template, visitor, ts.isTemplateLiteral)); + } + if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node.tag) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment?.classConstructor) { + + // converts `` super.f`x` `` into `` Reflect.get(_baseTemp, "f", _classTemp).bind(_classTemp)`x` `` + const invocation = factory.createFunctionBindCall(ts.visitNode(node.tag, visitor, ts.isExpression), currentClassLexicalEnvironment.classConstructor, []); + ts.setOriginalNode(invocation, node); + ts.setTextRange(invocation, node); + return factory.updateTaggedTemplateExpression(node, invocation, + /*typeArguments*/ undefined, ts.visitNode(node.template, visitor, ts.isTemplateLiteral)); + } + return ts.visitEachChild(node, visitor, context); + } + function transformClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { + if (shouldTransformPrivateElementsOrClassStaticBlocks) { + if (currentClassLexicalEnvironment) { + classLexicalEnvironmentMap.set(ts.getOriginalNodeId(node), currentClassLexicalEnvironment); } + + startLexicalEnvironment(); + const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; + currentStaticPropertyDeclarationOrStaticBlock = node; + let statements = ts.visitNodes(node.body.statements, visitor, ts.isStatement); + statements = factory.mergeLexicalEnvironment(statements, endLexicalEnvironment()); + currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; + + const iife = factory.createImmediatelyInvokedArrowFunction(statements); + ts.setOriginalNode(iife, node); + ts.setTextRange(iife, node); + ts.addEmitFlags(iife, ts.EmitFlags.AdviseOnEmitNode); + return iife; } + } - function visitBinaryExpression(node: ts.BinaryExpression, valueIsDiscarded: boolean) { - if (ts.isDestructuringAssignment(node)) { - const savedPendingExpressions = pendingExpressions; - pendingExpressions = undefined; - node = factory.updateBinaryExpression(node, ts.visitNode(node.left, visitorDestructuringTarget), node.operatorToken, ts.visitNode(node.right, visitor)); - const expr = ts.some(pendingExpressions) ? - factory.inlineExpressions(ts.compact([...pendingExpressions, node])) : - node; - pendingExpressions = savedPendingExpressions; - return expr; + function visitBinaryExpression(node: ts.BinaryExpression, valueIsDiscarded: boolean) { + if (ts.isDestructuringAssignment(node)) { + const savedPendingExpressions = pendingExpressions; + pendingExpressions = undefined; + node = factory.updateBinaryExpression(node, ts.visitNode(node.left, visitorDestructuringTarget), node.operatorToken, ts.visitNode(node.right, visitor)); + const expr = ts.some(pendingExpressions) ? + factory.inlineExpressions(ts.compact([...pendingExpressions, node])) : + node; + pendingExpressions = savedPendingExpressions; + return expr; + } + if (ts.isAssignmentExpression(node)) { + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.left)) { + const info = accessPrivateIdentifier(node.left.name); + if (info) { + return ts.setTextRange(ts.setOriginalNode(createPrivateIdentifierAssignment(info, node.left.expression, node.right, node.operatorToken.kind), node), node); + } } - if (ts.isAssignmentExpression(node)) { - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierPropertyAccessExpression(node.left)) { - const info = accessPrivateIdentifier(node.left.name); - if (info) { - return ts.setTextRange(ts.setOriginalNode(createPrivateIdentifierAssignment(info, node.left.expression, node.right, node.operatorToken.kind), node), node); - } + else if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(node.left) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment) { + const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; + if (facts & ClassFacts.ClassWasDecorated) { + return factory.updateBinaryExpression(node, visitInvalidSuperProperty(node.left), node.operatorToken, ts.visitNode(node.right, visitor, ts.isExpression)); } - else if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(node.left) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment) { - const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; - if (facts & ClassFacts.ClassWasDecorated) { - return factory.updateBinaryExpression(node, visitInvalidSuperProperty(node.left), node.operatorToken, ts.visitNode(node.right, visitor, ts.isExpression)); - } - if (classConstructor && superClassReference) { - let setterName = ts.isElementAccessExpression(node.left) ? ts.visitNode(node.left.argumentExpression, visitor, ts.isExpression) : - ts.isIdentifier(node.left.name) ? factory.createStringLiteralFromNode(node.left.name) : - undefined; - if (setterName) { - // converts `super.x = 1` into `(Reflect.set(_baseTemp, "x", _a = 1, _classTemp), _a)` - // converts `super[f()] = 1` into `(Reflect.set(_baseTemp, f(), _a = 1, _classTemp), _a)` - // converts `super.x += 1` into `(Reflect.set(_baseTemp, "x", _a = Reflect.get(_baseTemp, "x", _classtemp) + 1, _classTemp), _a)` - // converts `super[f()] += 1` into `(Reflect.set(_baseTemp, _a = f(), _b = Reflect.get(_baseTemp, _a, _classtemp) + 1, _classTemp), _b)` - - let expression = ts.visitNode(node.right, visitor, ts.isExpression); - if (ts.isCompoundAssignment(node.operatorToken.kind)) { - let getterName = setterName; - if (!ts.isSimpleInlineableExpression(setterName)) { - getterName = factory.createTempVariable(hoistVariableDeclaration); - setterName = factory.createAssignment(getterName, setterName); - } - const superPropertyGet = factory.createReflectGetCall(superClassReference, getterName, classConstructor); - ts.setOriginalNode(superPropertyGet, node.left); - ts.setTextRange(superPropertyGet, node.left); - expression = factory.createBinaryExpression(superPropertyGet, ts.getNonAssignmentOperatorForCompoundAssignment(node.operatorToken.kind), expression); - ts.setTextRange(expression, node); - } - - const temp = valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); - if (temp) { - expression = factory.createAssignment(temp, expression); - ts.setTextRange(temp, node); + if (classConstructor && superClassReference) { + let setterName = ts.isElementAccessExpression(node.left) ? ts.visitNode(node.left.argumentExpression, visitor, ts.isExpression) : + ts.isIdentifier(node.left.name) ? factory.createStringLiteralFromNode(node.left.name) : + undefined; + if (setterName) { + // converts `super.x = 1` into `(Reflect.set(_baseTemp, "x", _a = 1, _classTemp), _a)` + // converts `super[f()] = 1` into `(Reflect.set(_baseTemp, f(), _a = 1, _classTemp), _a)` + // converts `super.x += 1` into `(Reflect.set(_baseTemp, "x", _a = Reflect.get(_baseTemp, "x", _classtemp) + 1, _classTemp), _a)` + // converts `super[f()] += 1` into `(Reflect.set(_baseTemp, _a = f(), _b = Reflect.get(_baseTemp, _a, _classtemp) + 1, _classTemp), _b)` + + let expression = ts.visitNode(node.right, visitor, ts.isExpression); + if (ts.isCompoundAssignment(node.operatorToken.kind)) { + let getterName = setterName; + if (!ts.isSimpleInlineableExpression(setterName)) { + getterName = factory.createTempVariable(hoistVariableDeclaration); + setterName = factory.createAssignment(getterName, setterName); } - - expression = factory.createReflectSetCall(superClassReference, setterName, expression, classConstructor); - ts.setOriginalNode(expression, node); + const superPropertyGet = factory.createReflectGetCall(superClassReference, getterName, classConstructor); + ts.setOriginalNode(superPropertyGet, node.left); + ts.setTextRange(superPropertyGet, node.left); + expression = factory.createBinaryExpression(superPropertyGet, ts.getNonAssignmentOperatorForCompoundAssignment(node.operatorToken.kind), expression); ts.setTextRange(expression, node); + } - if (temp) { - expression = factory.createComma(expression, temp); - ts.setTextRange(expression, node); - } + const temp = valueIsDiscarded ? undefined : factory.createTempVariable(hoistVariableDeclaration); + if (temp) { + expression = factory.createAssignment(temp, expression); + ts.setTextRange(temp, node); + } + + expression = factory.createReflectSetCall(superClassReference, setterName, expression, classConstructor); + ts.setOriginalNode(expression, node); + ts.setTextRange(expression, node); - return expression; + if (temp) { + expression = factory.createComma(expression, temp); + ts.setTextRange(expression, node); } + + return expression; } } } - if (node.operatorToken.kind === ts.SyntaxKind.InKeyword && ts.isPrivateIdentifier(node.left)) { - return visitPrivateIdentifierInInExpression(node); - } - return ts.visitEachChild(node, visitor, context); } + if (node.operatorToken.kind === ts.SyntaxKind.InKeyword && ts.isPrivateIdentifier(node.left)) { + return visitPrivateIdentifierInInExpression(node); + } + return ts.visitEachChild(node, visitor, context); + } - function createPrivateIdentifierAssignment(info: PrivateIdentifierInfo, receiver: ts.Expression, right: ts.Expression, operator: ts.AssignmentOperator): ts.Expression { - receiver = ts.visitNode(receiver, visitor, ts.isExpression); - right = ts.visitNode(right, visitor, ts.isExpression); - if (ts.isCompoundAssignment(operator)) { - const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); - receiver = initializeExpression || readExpression; - right = factory.createBinaryExpression(createPrivateIdentifierAccessHelper(info, readExpression), ts.getNonAssignmentOperatorForCompoundAssignment(operator), right); - } - ts.setCommentRange(receiver, ts.moveRangePos(receiver, -1)); - - switch(info.kind) { - case PrivateIdentifierKind.Accessor: - return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, info.setterName); - case PrivateIdentifierKind.Method: - return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, - /* f */ undefined); - case PrivateIdentifierKind.Field: - return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, info.variableName); - default: - ts.Debug.assertNever(info, "Unknown private element type"); - } + function createPrivateIdentifierAssignment(info: PrivateIdentifierInfo, receiver: ts.Expression, right: ts.Expression, operator: ts.AssignmentOperator): ts.Expression { + receiver = ts.visitNode(receiver, visitor, ts.isExpression); + right = ts.visitNode(right, visitor, ts.isExpression); + if (ts.isCompoundAssignment(operator)) { + const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + receiver = initializeExpression || readExpression; + right = factory.createBinaryExpression(createPrivateIdentifierAccessHelper(info, readExpression), ts.getNonAssignmentOperatorForCompoundAssignment(operator), right); + } + ts.setCommentRange(receiver, ts.moveRangePos(receiver, -1)); + + switch(info.kind) { + case PrivateIdentifierKind.Accessor: + return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, info.setterName); + case PrivateIdentifierKind.Method: + return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, + /* f */ undefined); + case PrivateIdentifierKind.Field: + return context.getEmitHelperFactory().createClassPrivateFieldSetHelper(receiver, info.brandCheckIdentifier, right, info.kind, info.variableName); + default: + ts.Debug.assertNever(info, "Unknown private element type"); } + } - /** - * Set up the environment for a class. - */ - function visitClassLike(node: ts.ClassLikeDeclaration) { - if (!ts.forEach(node.members, doesClassElementNeedTransform)) { - return ts.visitEachChild(node, visitor, context); - } + /** + * Set up the environment for a class. + */ + function visitClassLike(node: ts.ClassLikeDeclaration) { + if (!ts.forEach(node.members, doesClassElementNeedTransform)) { + return ts.visitEachChild(node, visitor, context); + } - const savedPendingExpressions = pendingExpressions; - pendingExpressions = undefined; - startClassLexicalEnvironment(); + const savedPendingExpressions = pendingExpressions; + pendingExpressions = undefined; + startClassLexicalEnvironment(); - if (shouldTransformPrivateElementsOrClassStaticBlocks) { - const name = ts.getNameOfDeclaration(node); - if (name && ts.isIdentifier(name)) { - getPrivateIdentifierEnvironment().className = ts.idText(name); - } + if (shouldTransformPrivateElementsOrClassStaticBlocks) { + const name = ts.getNameOfDeclaration(node); + if (name && ts.isIdentifier(name)) { + getPrivateIdentifierEnvironment().className = ts.idText(name); + } - const privateInstanceMethodsAndAccessors = getPrivateInstanceMethodsAndAccessors(node); - if (ts.some(privateInstanceMethodsAndAccessors)) { - getPrivateIdentifierEnvironment().weakSetName = createHoistedVariableForClass("instances", privateInstanceMethodsAndAccessors[0].name); - } + const privateInstanceMethodsAndAccessors = getPrivateInstanceMethodsAndAccessors(node); + if (ts.some(privateInstanceMethodsAndAccessors)) { + getPrivateIdentifierEnvironment().weakSetName = createHoistedVariableForClass("instances", privateInstanceMethodsAndAccessors[0].name); } + } - const result = ts.isClassDeclaration(node) ? - visitClassDeclaration(node) : - visitClassExpression(node); + const result = ts.isClassDeclaration(node) ? + visitClassDeclaration(node) : + visitClassExpression(node); - endClassLexicalEnvironment(); - pendingExpressions = savedPendingExpressions; - return result; - } + endClassLexicalEnvironment(); + pendingExpressions = savedPendingExpressions; + return result; + } - function doesClassElementNeedTransform(node: ts.ClassElement) { - return ts.isPropertyDeclaration(node) || ts.isClassStaticBlockDeclaration(node) || (shouldTransformPrivateElementsOrClassStaticBlocks && node.name && ts.isPrivateIdentifier(node.name)); - } + function doesClassElementNeedTransform(node: ts.ClassElement) { + return ts.isPropertyDeclaration(node) || ts.isClassStaticBlockDeclaration(node) || (shouldTransformPrivateElementsOrClassStaticBlocks && node.name && ts.isPrivateIdentifier(node.name)); + } - function getPrivateInstanceMethodsAndAccessors(node: ts.ClassLikeDeclaration) { - return ts.filter(node.members, ts.isNonStaticMethodOrAccessorWithPrivateName); - } + function getPrivateInstanceMethodsAndAccessors(node: ts.ClassLikeDeclaration) { + return ts.filter(node.members, ts.isNonStaticMethodOrAccessorWithPrivateName); + } - function getClassFacts(node: ts.ClassLikeDeclaration) { - let facts = ClassFacts.None; - const original = ts.getOriginalNode(node); - if (ts.isClassDeclaration(original) && ts.classOrConstructorParameterIsDecorated(original)) { - facts |= ClassFacts.ClassWasDecorated; - } - for (const member of node.members) { - if (!ts.isStatic(member)) - continue; - if (member.name && ts.isPrivateIdentifier(member.name) && shouldTransformPrivateElementsOrClassStaticBlocks) { - facts |= ClassFacts.NeedsClassConstructorReference; - } - if (ts.isPropertyDeclaration(member) || ts.isClassStaticBlockDeclaration(member)) { - if (shouldTransformThisInStaticInitializers && member.transformFlags & ts.TransformFlags.ContainsLexicalThis) { - facts |= ClassFacts.NeedsSubstitutionForThisInClassStaticField; - if (!(facts & ClassFacts.ClassWasDecorated)) { - facts |= ClassFacts.NeedsClassConstructorReference; - } + function getClassFacts(node: ts.ClassLikeDeclaration) { + let facts = ClassFacts.None; + const original = ts.getOriginalNode(node); + if (ts.isClassDeclaration(original) && ts.classOrConstructorParameterIsDecorated(original)) { + facts |= ClassFacts.ClassWasDecorated; + } + for (const member of node.members) { + if (!ts.isStatic(member)) + continue; + if (member.name && ts.isPrivateIdentifier(member.name) && shouldTransformPrivateElementsOrClassStaticBlocks) { + facts |= ClassFacts.NeedsClassConstructorReference; + } + if (ts.isPropertyDeclaration(member) || ts.isClassStaticBlockDeclaration(member)) { + if (shouldTransformThisInStaticInitializers && member.transformFlags & ts.TransformFlags.ContainsLexicalThis) { + facts |= ClassFacts.NeedsSubstitutionForThisInClassStaticField; + if (!(facts & ClassFacts.ClassWasDecorated)) { + facts |= ClassFacts.NeedsClassConstructorReference; } - if (shouldTransformSuperInStaticInitializers && member.transformFlags & ts.TransformFlags.ContainsLexicalSuper) { - if (!(facts & ClassFacts.ClassWasDecorated)) { - facts |= ClassFacts.NeedsClassConstructorReference | ClassFacts.NeedsClassSuperReference; - } + } + if (shouldTransformSuperInStaticInitializers && member.transformFlags & ts.TransformFlags.ContainsLexicalSuper) { + if (!(facts & ClassFacts.ClassWasDecorated)) { + facts |= ClassFacts.NeedsClassConstructorReference | ClassFacts.NeedsClassSuperReference; } } } - return facts; } + return facts; + } - function visitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments) { - const facts = currentClassLexicalEnvironment?.facts || ClassFacts.None; - if (facts & ClassFacts.NeedsClassSuperReference) { - const temp = factory.createTempVariable(hoistVariableDeclaration, /*reserveInNestedScopes*/ true); - getClassLexicalEnvironment().superClassReference = temp; - return factory.updateExpressionWithTypeArguments(node, factory.createAssignment(temp, ts.visitNode(node.expression, visitor, ts.isExpression)), - /*typeArguments*/ undefined); - } - return ts.visitEachChild(node, visitor, context); + function visitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments) { + const facts = currentClassLexicalEnvironment?.facts || ClassFacts.None; + if (facts & ClassFacts.NeedsClassSuperReference) { + const temp = factory.createTempVariable(hoistVariableDeclaration, /*reserveInNestedScopes*/ true); + getClassLexicalEnvironment().superClassReference = temp; + return factory.updateExpressionWithTypeArguments(node, factory.createAssignment(temp, ts.visitNode(node.expression, visitor, ts.isExpression)), + /*typeArguments*/ undefined); + } + return ts.visitEachChild(node, visitor, context); + } + function visitClassDeclaration(node: ts.ClassDeclaration) { + const facts = getClassFacts(node); + if (facts) { + getClassLexicalEnvironment().facts = facts; + } + if (facts & ClassFacts.NeedsSubstitutionForThisInClassStaticField) { + enableSubstitutionForClassStaticThisOrSuperReference(); } - function visitClassDeclaration(node: ts.ClassDeclaration) { - const facts = getClassFacts(node); - if (facts) { - getClassLexicalEnvironment().facts = facts; - } - if (facts & ClassFacts.NeedsSubstitutionForThisInClassStaticField) { - enableSubstitutionForClassStaticThisOrSuperReference(); - } - - // If a class has private static fields, or a static field has a `this` or `super` reference, - // then we need to allocate a temp variable to hold on to that reference. - let pendingClassReferenceAssignment: ts.BinaryExpression | undefined; - if (facts & ClassFacts.NeedsClassConstructorReference) { - const temp = factory.createTempVariable(hoistVariableDeclaration, /*reservedInNestedScopes*/ true); - getClassLexicalEnvironment().classConstructor = factory.cloneNode(temp); - pendingClassReferenceAssignment = factory.createAssignment(temp, factory.getInternalName(node)); - } - const extendsClauseElement = ts.getEffectiveBaseTypeNode(node); - const isDerivedClass = !!(extendsClauseElement && ts.skipOuterExpressions(extendsClauseElement.expression).kind !== ts.SyntaxKind.NullKeyword); - const statements: ts.Statement[] = [ - factory.updateClassDeclaration(node, - /*decorators*/ undefined, node.modifiers, node.name, - /*typeParameters*/ undefined, ts.visitNodes(node.heritageClauses, heritageClauseVisitor, ts.isHeritageClause), transformClassMembers(node, isDerivedClass)) - ]; + // If a class has private static fields, or a static field has a `this` or `super` reference, + // then we need to allocate a temp variable to hold on to that reference. + let pendingClassReferenceAssignment: ts.BinaryExpression | undefined; + if (facts & ClassFacts.NeedsClassConstructorReference) { + const temp = factory.createTempVariable(hoistVariableDeclaration, /*reservedInNestedScopes*/ true); + getClassLexicalEnvironment().classConstructor = factory.cloneNode(temp); + pendingClassReferenceAssignment = factory.createAssignment(temp, factory.getInternalName(node)); + } - if (pendingClassReferenceAssignment) { - getPendingExpressions().unshift(pendingClassReferenceAssignment); - } + const extendsClauseElement = ts.getEffectiveBaseTypeNode(node); + const isDerivedClass = !!(extendsClauseElement && ts.skipOuterExpressions(extendsClauseElement.expression).kind !== ts.SyntaxKind.NullKeyword); + const statements: ts.Statement[] = [ + factory.updateClassDeclaration(node, + /*decorators*/ undefined, node.modifiers, node.name, + /*typeParameters*/ undefined, ts.visitNodes(node.heritageClauses, heritageClauseVisitor, ts.isHeritageClause), transformClassMembers(node, isDerivedClass)) + ]; - // Write any pending expressions from elided or moved computed property names - if (ts.some(pendingExpressions)) { - statements.push(factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions))); - } + if (pendingClassReferenceAssignment) { + getPendingExpressions().unshift(pendingClassReferenceAssignment); + } - // 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. + // Write any pending expressions from elided or moved computed property names + if (ts.some(pendingExpressions)) { + statements.push(factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions))); + } - const staticProperties = ts.getStaticPropertiesAndClassStaticBlock(node); - if (ts.some(staticProperties)) { - addPropertyOrClassStaticBlockStatements(statements, staticProperties, factory.getInternalName(node)); - } + // 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. - return statements; + const staticProperties = ts.getStaticPropertiesAndClassStaticBlock(node); + if (ts.some(staticProperties)) { + addPropertyOrClassStaticBlockStatements(statements, staticProperties, factory.getInternalName(node)); } - function visitClassExpression(node: ts.ClassExpression): ts.Expression { - const facts = getClassFacts(node); - if (facts) { - getClassLexicalEnvironment().facts = facts; - } + return statements; + } - if (facts & ClassFacts.NeedsSubstitutionForThisInClassStaticField) { - enableSubstitutionForClassStaticThisOrSuperReference(); - } + function visitClassExpression(node: ts.ClassExpression): ts.Expression { + const facts = getClassFacts(node); + if (facts) { + getClassLexicalEnvironment().facts = facts; + } - // 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 = !!(facts & ClassFacts.ClassWasDecorated); - - const staticPropertiesOrClassStaticBlocks = ts.getStaticPropertiesAndClassStaticBlock(node); - const extendsClauseElement = ts.getEffectiveBaseTypeNode(node); - const isDerivedClass = !!(extendsClauseElement && ts.skipOuterExpressions(extendsClauseElement.expression).kind !== ts.SyntaxKind.NullKeyword); - const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & ts.NodeCheckFlags.ClassWithConstructorReference; - let temp: ts.Identifier | undefined; - function createClassTempVar() { - const classCheckFlags = resolver.getNodeCheckFlags(node); - const isClassWithConstructorReference = classCheckFlags & ts.NodeCheckFlags.ClassWithConstructorReference; - const requiresBlockScopedVar = classCheckFlags & ts.NodeCheckFlags.BlockScopedBindingInLoop; - return factory.createTempVariable(requiresBlockScopedVar ? addBlockScopedVariable : hoistVariableDeclaration, !!isClassWithConstructorReference); - } + if (facts & ClassFacts.NeedsSubstitutionForThisInClassStaticField) { + enableSubstitutionForClassStaticThisOrSuperReference(); + } - if (facts & ClassFacts.NeedsClassConstructorReference) { - temp = createClassTempVar(); - getClassLexicalEnvironment().classConstructor = factory.cloneNode(temp); - } + // 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 = !!(facts & ClassFacts.ClassWasDecorated); - const classExpression = factory.updateClassExpression(node, ts.visitNodes(node.decorators, visitor, ts.isDecorator), node.modifiers, node.name, - /*typeParameters*/ undefined, ts.visitNodes(node.heritageClauses, heritageClauseVisitor, ts.isHeritageClause), transformClassMembers(node, isDerivedClass)); - const hasTransformableStatics = shouldTransformPrivateElementsOrClassStaticBlocks && ts.some(staticPropertiesOrClassStaticBlocks, p => ts.isClassStaticBlockDeclaration(p) || !!p.initializer || ts.isPrivateIdentifier(p.name)); - if (hasTransformableStatics || ts.some(pendingExpressions)) { - if (isDecoratedClassDeclaration) { - ts.Debug.assertIsDefined(pendingStatements, "Decorated classes transformed by TypeScript are expected to be within a variable declaration."); + const staticPropertiesOrClassStaticBlocks = ts.getStaticPropertiesAndClassStaticBlock(node); + const extendsClauseElement = ts.getEffectiveBaseTypeNode(node); + const isDerivedClass = !!(extendsClauseElement && ts.skipOuterExpressions(extendsClauseElement.expression).kind !== ts.SyntaxKind.NullKeyword); + const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & ts.NodeCheckFlags.ClassWithConstructorReference; + let temp: ts.Identifier | undefined; + function createClassTempVar() { + const classCheckFlags = resolver.getNodeCheckFlags(node); + const isClassWithConstructorReference = classCheckFlags & ts.NodeCheckFlags.ClassWithConstructorReference; + const requiresBlockScopedVar = classCheckFlags & ts.NodeCheckFlags.BlockScopedBindingInLoop; + return factory.createTempVariable(requiresBlockScopedVar ? addBlockScopedVariable : hoistVariableDeclaration, !!isClassWithConstructorReference); + } - // Write any pending expressions from elided or moved computed property names - if (pendingStatements && pendingExpressions && ts.some(pendingExpressions)) { - pendingStatements.push(factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions))); - } + if (facts & ClassFacts.NeedsClassConstructorReference) { + temp = createClassTempVar(); + getClassLexicalEnvironment().classConstructor = factory.cloneNode(temp); + } - if (pendingStatements && ts.some(staticPropertiesOrClassStaticBlocks)) { - addPropertyOrClassStaticBlockStatements(pendingStatements, staticPropertiesOrClassStaticBlocks, factory.getInternalName(node)); - } - if (temp) { - return factory.inlineExpressions([factory.createAssignment(temp, classExpression), temp]); - } - return classExpression; - } - else { - const expressions: ts.Expression[] = []; - temp ||= createClassTempVar(); - if (isClassWithConstructorReference) { - // record an alias as the class name is not in scope for statics. - enableSubstitutionForClassAliases(); - const alias = factory.cloneNode(temp) as ts.GeneratedIdentifier; - alias.autoGenerateFlags &= ~ts.GeneratedIdentifierFlags.ReservedInNestedScopes; - classAliases[ts.getOriginalNodeId(node)] = alias; - } + const classExpression = factory.updateClassExpression(node, ts.visitNodes(node.decorators, visitor, ts.isDecorator), node.modifiers, node.name, + /*typeParameters*/ undefined, ts.visitNodes(node.heritageClauses, heritageClauseVisitor, ts.isHeritageClause), transformClassMembers(node, isDerivedClass)); + const hasTransformableStatics = shouldTransformPrivateElementsOrClassStaticBlocks && ts.some(staticPropertiesOrClassStaticBlocks, p => ts.isClassStaticBlockDeclaration(p) || !!p.initializer || ts.isPrivateIdentifier(p.name)); + if (hasTransformableStatics || ts.some(pendingExpressions)) { + if (isDecoratedClassDeclaration) { + ts.Debug.assertIsDefined(pendingStatements, "Decorated classes transformed by TypeScript are expected to be within a variable declaration."); - // To preserve the behavior of the old emitter, we explicitly indent - // the body of a class with static initializers. - ts.setEmitFlags(classExpression, ts.EmitFlags.Indented | ts.getEmitFlags(classExpression)); - expressions.push(ts.startOnNewLine(factory.createAssignment(temp, classExpression))); - // Add any pending expressions leftover from elided or relocated computed property names - ts.addRange(expressions, ts.map(pendingExpressions, ts.startOnNewLine)); - ts.addRange(expressions, generateInitializedPropertyExpressionsOrClassStaticBlock(staticPropertiesOrClassStaticBlocks, temp)); - expressions.push(ts.startOnNewLine(temp)); + // Write any pending expressions from elided or moved computed property names + if (pendingStatements && pendingExpressions && ts.some(pendingExpressions)) { + pendingStatements.push(factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions))); + } - return factory.inlineExpressions(expressions); + if (pendingStatements && ts.some(staticPropertiesOrClassStaticBlocks)) { + addPropertyOrClassStaticBlockStatements(pendingStatements, staticPropertiesOrClassStaticBlocks, factory.getInternalName(node)); + } + if (temp) { + return factory.inlineExpressions([factory.createAssignment(temp, classExpression), temp]); } + return classExpression; } + else { + const expressions: ts.Expression[] = []; + temp ||= createClassTempVar(); + if (isClassWithConstructorReference) { + // record an alias as the class name is not in scope for statics. + enableSubstitutionForClassAliases(); + const alias = factory.cloneNode(temp) as ts.GeneratedIdentifier; + alias.autoGenerateFlags &= ~ts.GeneratedIdentifierFlags.ReservedInNestedScopes; + classAliases[ts.getOriginalNodeId(node)] = alias; + } - return classExpression; - } + // To preserve the behavior of the old emitter, we explicitly indent + // the body of a class with static initializers. + ts.setEmitFlags(classExpression, ts.EmitFlags.Indented | ts.getEmitFlags(classExpression)); + expressions.push(ts.startOnNewLine(factory.createAssignment(temp, classExpression))); + // Add any pending expressions leftover from elided or relocated computed property names + ts.addRange(expressions, ts.map(pendingExpressions, ts.startOnNewLine)); + ts.addRange(expressions, generateInitializedPropertyExpressionsOrClassStaticBlock(staticPropertiesOrClassStaticBlocks, temp)); + expressions.push(ts.startOnNewLine(temp)); - function visitClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { - if (!shouldTransformPrivateElementsOrClassStaticBlocks) { - return ts.visitEachChild(node, classElementVisitor, context); + return factory.inlineExpressions(expressions); } - // ClassStaticBlockDeclaration for classes are transformed in `visitClassDeclaration` or `visitClassExpression`. - return undefined; } - function transformClassMembers(node: ts.ClassDeclaration | ts.ClassExpression, isDerivedClass: boolean) { - const members: ts.ClassElement[] = []; - if (shouldTransformPrivateElementsOrClassStaticBlocks) { - // Declare private names. - for (const member of node.members) { - if (ts.isPrivateIdentifierClassElementDeclaration(member)) { - addPrivateIdentifierToEnvironment(member); - } - } + return classExpression; + } + + function visitClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration) { + if (!shouldTransformPrivateElementsOrClassStaticBlocks) { + return ts.visitEachChild(node, classElementVisitor, context); + } + // ClassStaticBlockDeclaration for classes are transformed in `visitClassDeclaration` or `visitClassExpression`. + return undefined; + } - if (ts.some(getPrivateInstanceMethodsAndAccessors(node))) { - createBrandCheckWeakSetForPrivateMethods(); + function transformClassMembers(node: ts.ClassDeclaration | ts.ClassExpression, isDerivedClass: boolean) { + const members: ts.ClassElement[] = []; + if (shouldTransformPrivateElementsOrClassStaticBlocks) { + // Declare private names. + for (const member of node.members) { + if (ts.isPrivateIdentifierClassElementDeclaration(member)) { + addPrivateIdentifierToEnvironment(member); } } - const constructor = transformConstructor(node, isDerivedClass); - const visitedMembers = ts.visitNodes(node.members, classElementVisitor, ts.isClassElement); - - if (constructor) { - members.push(constructor); + if (ts.some(getPrivateInstanceMethodsAndAccessors(node))) { + createBrandCheckWeakSetForPrivateMethods(); } + } - if (!shouldTransformPrivateElementsOrClassStaticBlocks && ts.some(pendingExpressions)) { - members.push(factory.createClassStaticBlockDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, factory.createBlock([ - factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions)) - ]))); - pendingExpressions = undefined; - } + const constructor = transformConstructor(node, isDerivedClass); + const visitedMembers = ts.visitNodes(node.members, classElementVisitor, ts.isClassElement); - ts.addRange(members, visitedMembers); - return ts.setTextRange(factory.createNodeArray(members), /*location*/ node.members); + if (constructor) { + members.push(constructor); } - function createBrandCheckWeakSetForPrivateMethods() { - const { weakSetName } = getPrivateIdentifierEnvironment(); - ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); - getPendingExpressions().push(factory.createAssignment(weakSetName, factory.createNewExpression(factory.createIdentifier("WeakSet"), - /*typeArguments*/ undefined, []))); + if (!shouldTransformPrivateElementsOrClassStaticBlocks && ts.some(pendingExpressions)) { + members.push(factory.createClassStaticBlockDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, factory.createBlock([ + factory.createExpressionStatement(factory.inlineExpressions(pendingExpressions)) + ]))); + pendingExpressions = undefined; } - function isClassElementThatRequiresConstructorStatement(member: ts.ClassElement) { - if (ts.isStatic(member) || ts.hasSyntacticModifier(ts.getOriginalNode(member), ts.ModifierFlags.Abstract)) { - return false; - } - if (useDefineForClassFields) { - // If we are using define semantics and targeting ESNext or higher, - // then we don't need to transform any class properties. - return languageVersion < ts.ScriptTarget.ES2022; - } - return ts.isInitializedProperty(member) || shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierClassElementDeclaration(member); + + ts.addRange(members, visitedMembers); + return ts.setTextRange(factory.createNodeArray(members), /*location*/ node.members); + } + + function createBrandCheckWeakSetForPrivateMethods() { + const { weakSetName } = getPrivateIdentifierEnvironment(); + ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); + getPendingExpressions().push(factory.createAssignment(weakSetName, factory.createNewExpression(factory.createIdentifier("WeakSet"), + /*typeArguments*/ undefined, []))); + } + function isClassElementThatRequiresConstructorStatement(member: ts.ClassElement) { + if (ts.isStatic(member) || ts.hasSyntacticModifier(ts.getOriginalNode(member), ts.ModifierFlags.Abstract)) { + return false; + } + if (useDefineForClassFields) { + // If we are using define semantics and targeting ESNext or higher, + // then we don't need to transform any class properties. + return languageVersion < ts.ScriptTarget.ES2022; } + return ts.isInitializedProperty(member) || shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifierClassElementDeclaration(member); + } - function transformConstructor(node: ts.ClassDeclaration | ts.ClassExpression, isDerivedClass: boolean) { - const constructor = ts.visitNode(ts.getFirstConstructorWithBody(node), visitor, ts.isConstructorDeclaration); - const elements = node.members.filter(isClassElementThatRequiresConstructorStatement); - if (!ts.some(elements)) { - return constructor; - } - const parameters = ts.visitParameterList(constructor ? constructor.parameters : undefined, visitor, context); - const body = transformConstructorBody(node, constructor, isDerivedClass); - if (!body) { - return undefined; - } - return ts.startOnNewLine(ts.setOriginalNode(ts.setTextRange(factory.createConstructorDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, parameters ?? [], body), constructor || node), constructor)); - } - function transformConstructorBody(node: ts.ClassDeclaration | ts.ClassExpression, constructor: ts.ConstructorDeclaration | undefined, isDerivedClass: boolean) { - let properties = ts.getProperties(node, /*requireInitializer*/ false, /*isStatic*/ false); - if (!useDefineForClassFields) { - properties = ts.filter(properties, property => !!property.initializer || ts.isPrivateIdentifier(property.name)); - } + function transformConstructor(node: ts.ClassDeclaration | ts.ClassExpression, isDerivedClass: boolean) { + const constructor = ts.visitNode(ts.getFirstConstructorWithBody(node), visitor, ts.isConstructorDeclaration); + const elements = node.members.filter(isClassElementThatRequiresConstructorStatement); + if (!ts.some(elements)) { + return constructor; + } + const parameters = ts.visitParameterList(constructor ? constructor.parameters : undefined, visitor, context); + const body = transformConstructorBody(node, constructor, isDerivedClass); + if (!body) { + return undefined; + } + return ts.startOnNewLine(ts.setOriginalNode(ts.setTextRange(factory.createConstructorDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, parameters ?? [], body), constructor || node), constructor)); + } + function transformConstructorBody(node: ts.ClassDeclaration | ts.ClassExpression, constructor: ts.ConstructorDeclaration | undefined, isDerivedClass: boolean) { + let properties = ts.getProperties(node, /*requireInitializer*/ false, /*isStatic*/ false); + if (!useDefineForClassFields) { + properties = ts.filter(properties, property => !!property.initializer || ts.isPrivateIdentifier(property.name)); + } - const privateMethodsAndAccessors = getPrivateInstanceMethodsAndAccessors(node); - const needsConstructorBody = ts.some(properties) || ts.some(privateMethodsAndAccessors); + const privateMethodsAndAccessors = getPrivateInstanceMethodsAndAccessors(node); + const needsConstructorBody = ts.some(properties) || ts.some(privateMethodsAndAccessors); - // Only generate synthetic constructor when there are property initializers to move. - if (!constructor && !needsConstructorBody) { - return ts.visitFunctionBody(/*node*/ undefined, visitor, context); - } + // Only generate synthetic constructor when there are property initializers to move. + if (!constructor && !needsConstructorBody) { + return ts.visitFunctionBody(/*node*/ undefined, visitor, context); + } - resumeLexicalEnvironment(); - - const needsSyntheticConstructor = !constructor && isDerivedClass; - let indexOfFirstStatementAfterSuperAndPrologue = 0; - let prologueStatementCount = 0; - let superStatementIndex = -1; - let statements: ts.Statement[] = []; - - if (constructor?.body?.statements) { - prologueStatementCount = factory.copyPrologue(constructor.body.statements, statements, /*ensureUseStrict*/ false, visitor); - superStatementIndex = ts.findSuperStatementIndex(constructor.body.statements, prologueStatementCount); - - // If there was a super call, visit existing statements up to and including it - if (superStatementIndex >= 0) { - indexOfFirstStatementAfterSuperAndPrologue = superStatementIndex + 1; - statements = [ - ...statements.slice(0, prologueStatementCount), - ...ts.visitNodes(constructor.body.statements, visitor, ts.isStatement, prologueStatementCount, indexOfFirstStatementAfterSuperAndPrologue - prologueStatementCount), - ...statements.slice(prologueStatementCount), - ]; - } - else if (prologueStatementCount >= 0) { - indexOfFirstStatementAfterSuperAndPrologue = prologueStatementCount; - } - } + resumeLexicalEnvironment(); - if (needsSyntheticConstructor) { - // Add a synthetic `super` call: - // - // super(...arguments); - // - statements.push(factory.createExpressionStatement(factory.createCallExpression(factory.createSuper(), - /*typeArguments*/ undefined, [factory.createSpreadElement(factory.createIdentifier("arguments"))]))); + const needsSyntheticConstructor = !constructor && isDerivedClass; + let indexOfFirstStatementAfterSuperAndPrologue = 0; + let prologueStatementCount = 0; + let superStatementIndex = -1; + let statements: ts.Statement[] = []; + + if (constructor?.body?.statements) { + prologueStatementCount = factory.copyPrologue(constructor.body.statements, statements, /*ensureUseStrict*/ false, visitor); + superStatementIndex = ts.findSuperStatementIndex(constructor.body.statements, prologueStatementCount); + + // If there was a super call, visit existing statements up to and including it + if (superStatementIndex >= 0) { + indexOfFirstStatementAfterSuperAndPrologue = superStatementIndex + 1; + statements = [ + ...statements.slice(0, prologueStatementCount), + ...ts.visitNodes(constructor.body.statements, visitor, ts.isStatement, prologueStatementCount, indexOfFirstStatementAfterSuperAndPrologue - prologueStatementCount), + ...statements.slice(prologueStatementCount), + ]; } + else if (prologueStatementCount >= 0) { + indexOfFirstStatementAfterSuperAndPrologue = prologueStatementCount; + } + } - // Add the property initializers. Transforms this: + if (needsSyntheticConstructor) { + // Add a synthetic `super` call: // - // public x = 1; + // super(...arguments); // - // Into this: - // - // constructor() { - // this.x = 1; - // } - // - // If we do useDefineForClassFields, they'll be converted elsewhere. - // We instead *remove* them from the transformed output at this stage. - let parameterPropertyDeclarationCount = 0; - if (constructor?.body) { - if (useDefineForClassFields) { - statements = statements.filter(statement => !ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor)); - } - else { - for (const statement of constructor.body.statements) { - if (ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor)) { - parameterPropertyDeclarationCount++; - } - } - if (parameterPropertyDeclarationCount > 0) { - const parameterProperties = ts.visitNodes(constructor.body.statements, visitor, ts.isStatement, indexOfFirstStatementAfterSuperAndPrologue, parameterPropertyDeclarationCount); - - // If there was a super() call found, add parameter properties immediately after it - if (superStatementIndex >= 0) { - ts.addRange(statements, parameterProperties); - } - else { - // Add add parameter properties to the top of the constructor after the prologue - let superAndPrologueStatementCount = prologueStatementCount; - // If a synthetic super() call was added, need to account for that - if (needsSyntheticConstructor) - superAndPrologueStatementCount++; - statements = [ - ...statements.slice(0, superAndPrologueStatementCount), - ...parameterProperties, - ...statements.slice(superAndPrologueStatementCount), - ]; - } - - indexOfFirstStatementAfterSuperAndPrologue += parameterPropertyDeclarationCount; + statements.push(factory.createExpressionStatement(factory.createCallExpression(factory.createSuper(), + /*typeArguments*/ undefined, [factory.createSpreadElement(factory.createIdentifier("arguments"))]))); + } + + // Add the property initializers. Transforms this: + // + // public x = 1; + // + // Into this: + // + // constructor() { + // this.x = 1; + // } + // + // If we do useDefineForClassFields, they'll be converted elsewhere. + // We instead *remove* them from the transformed output at this stage. + let parameterPropertyDeclarationCount = 0; + if (constructor?.body) { + if (useDefineForClassFields) { + statements = statements.filter(statement => !ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor)); + } + else { + for (const statement of constructor.body.statements) { + if (ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor)) { + parameterPropertyDeclarationCount++; } } - } + if (parameterPropertyDeclarationCount > 0) { + const parameterProperties = ts.visitNodes(constructor.body.statements, visitor, ts.isStatement, indexOfFirstStatementAfterSuperAndPrologue, parameterPropertyDeclarationCount); - const receiver = factory.createThis(); - // private methods can be called in property initializers, they should execute first. - addMethodStatements(statements, privateMethodsAndAccessors, receiver); - addPropertyOrClassStaticBlockStatements(statements, properties, receiver); - - // Add existing statements after the initial prologues and super call - if (constructor) { - ts.addRange(statements, ts.visitNodes(constructor.body!.statements, visitBodyStatement, ts.isStatement, indexOfFirstStatementAfterSuperAndPrologue)); - } - - statements = factory.mergeLexicalEnvironment(statements, endLexicalEnvironment()); + // If there was a super() call found, add parameter properties immediately after it + if (superStatementIndex >= 0) { + ts.addRange(statements, parameterProperties); + } + else { + // Add add parameter properties to the top of the constructor after the prologue + let superAndPrologueStatementCount = prologueStatementCount; + // If a synthetic super() call was added, need to account for that + if (needsSyntheticConstructor) + superAndPrologueStatementCount++; + statements = [ + ...statements.slice(0, superAndPrologueStatementCount), + ...parameterProperties, + ...statements.slice(superAndPrologueStatementCount), + ]; + } - return ts.setTextRange(factory.createBlock(ts.setTextRange(factory.createNodeArray(statements), - /*location*/ constructor ? constructor.body!.statements : node.members), - /*multiLine*/ true), - /*location*/ constructor ? constructor.body : undefined); - function visitBodyStatement(statement: ts.Node) { - if (useDefineForClassFields && ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor!)) { - return undefined; + indexOfFirstStatementAfterSuperAndPrologue += parameterPropertyDeclarationCount; } - - return visitor(statement); } } - /** - * 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 addPropertyOrClassStaticBlockStatements(statements: ts.Statement[], properties: readonly (ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration)[], receiver: ts.LeftHandSideExpression) { - for (const property of properties) { - if (ts.isStatic(property) && !shouldTransformPrivateElementsOrClassStaticBlocks && !useDefineForClassFields) { - continue; - } + const receiver = factory.createThis(); + // private methods can be called in property initializers, they should execute first. + addMethodStatements(statements, privateMethodsAndAccessors, receiver); + addPropertyOrClassStaticBlockStatements(statements, properties, receiver); - const statement = transformPropertyOrClassStaticBlock(property, receiver); - if (!statement) { - continue; - } + // Add existing statements after the initial prologues and super call + if (constructor) { + ts.addRange(statements, ts.visitNodes(constructor.body!.statements, visitBodyStatement, ts.isStatement, indexOfFirstStatementAfterSuperAndPrologue)); + } - statements.push(statement); + statements = factory.mergeLexicalEnvironment(statements, endLexicalEnvironment()); + + return ts.setTextRange(factory.createBlock(ts.setTextRange(factory.createNodeArray(statements), + /*location*/ constructor ? constructor.body!.statements : node.members), + /*multiLine*/ true), + /*location*/ constructor ? constructor.body : undefined); + function visitBodyStatement(statement: ts.Node) { + if (useDefineForClassFields && ts.isParameterPropertyDeclaration(ts.getOriginalNode(statement), constructor!)) { + return undefined; } + + return visitor(statement); } + } - function transformPropertyOrClassStaticBlock(property: ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration, receiver: ts.LeftHandSideExpression) { - const expression = ts.isClassStaticBlockDeclaration(property) ? - transformClassStaticBlockDeclaration(property) : - transformProperty(property, receiver); - if (!expression) { - return 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 addPropertyOrClassStaticBlockStatements(statements: ts.Statement[], properties: readonly (ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration)[], receiver: ts.LeftHandSideExpression) { + for (const property of properties) { + if (ts.isStatic(property) && !shouldTransformPrivateElementsOrClassStaticBlocks && !useDefineForClassFields) { + continue; } - const statement = factory.createExpressionStatement(expression); - ts.setSourceMapRange(statement, ts.moveRangePastModifiers(property)); - ts.setCommentRange(statement, property); - ts.setOriginalNode(statement, property); - - // `setOriginalNode` *copies* the `emitNode` from `property`, so now both - // `statement` and `expression` have a copy of the synthesized comments. - // Drop the comments from expression to avoid printing them twice. - ts.setSyntheticLeadingComments(expression, undefined); - ts.setSyntheticTrailingComments(expression, undefined); - - return statement; - } - - /** - * Generates assignment expressions for property initializers. - * - * @param propertiesOrClassStaticBlocks An array of property declarations to transform. - * @param receiver The receiver on which each property should be assigned. - */ - function generateInitializedPropertyExpressionsOrClassStaticBlock(propertiesOrClassStaticBlocks: readonly (ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration)[], receiver: ts.LeftHandSideExpression) { - const expressions: ts.Expression[] = []; - for (const property of propertiesOrClassStaticBlocks) { - const expression = ts.isClassStaticBlockDeclaration(property) ? transformClassStaticBlockDeclaration(property) : transformProperty(property, receiver); - if (!expression) { - continue; - } - ts.startOnNewLine(expression); - ts.setSourceMapRange(expression, ts.moveRangePastModifiers(property)); - ts.setCommentRange(expression, property); - ts.setOriginalNode(expression, property); - expressions.push(expression); + const statement = transformPropertyOrClassStaticBlock(property, receiver); + if (!statement) { + continue; } - return expressions; + statements.push(statement); } + } - /** - * Transforms a property initializer into an assignment statement. - * - * @param property The property declaration. - * @param receiver The object receiving the property assignment. - */ - function transformProperty(property: ts.PropertyDeclaration, receiver: ts.LeftHandSideExpression) { - const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; - const transformed = transformPropertyWorker(property, receiver); - if (transformed && ts.hasStaticModifier(property) && currentClassLexicalEnvironment?.facts) { - // capture the lexical environment for the member - ts.setOriginalNode(transformed, property); - ts.addEmitFlags(transformed, ts.EmitFlags.AdviseOnEmitNode); - classLexicalEnvironmentMap.set(ts.getOriginalNodeId(transformed), currentClassLexicalEnvironment); - } - currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; - return transformed; + function transformPropertyOrClassStaticBlock(property: ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration, receiver: ts.LeftHandSideExpression) { + const expression = ts.isClassStaticBlockDeclaration(property) ? + transformClassStaticBlockDeclaration(property) : + transformProperty(property, receiver); + if (!expression) { + return undefined; } - function transformPropertyWorker(property: ts.PropertyDeclaration, receiver: ts.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 = !useDefineForClassFields; - const propertyName = ts.isComputedPropertyName(property.name) && !ts.isSimpleInlineableExpression(property.name.expression) - ? factory.updateComputedPropertyName(property.name, factory.getGeneratedNameForNode(property.name)) - : property.name; + const statement = factory.createExpressionStatement(expression); + ts.setSourceMapRange(statement, ts.moveRangePastModifiers(property)); + ts.setCommentRange(statement, property); + ts.setOriginalNode(statement, property); - if (ts.hasStaticModifier(property)) { - currentStaticPropertyDeclarationOrStaticBlock = property; + // `setOriginalNode` *copies* the `emitNode` from `property`, so now both + // `statement` and `expression` have a copy of the synthesized comments. + // Drop the comments from expression to avoid printing them twice. + ts.setSyntheticLeadingComments(expression, undefined); + ts.setSyntheticTrailingComments(expression, undefined); + + return statement; + } + + /** + * Generates assignment expressions for property initializers. + * + * @param propertiesOrClassStaticBlocks An array of property declarations to transform. + * @param receiver The receiver on which each property should be assigned. + */ + function generateInitializedPropertyExpressionsOrClassStaticBlock(propertiesOrClassStaticBlocks: readonly (ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration)[], receiver: ts.LeftHandSideExpression) { + const expressions: ts.Expression[] = []; + for (const property of propertiesOrClassStaticBlocks) { + const expression = ts.isClassStaticBlockDeclaration(property) ? transformClassStaticBlockDeclaration(property) : transformProperty(property, receiver); + if (!expression) { + continue; } + ts.startOnNewLine(expression); + ts.setSourceMapRange(expression, ts.moveRangePastModifiers(property)); + ts.setCommentRange(expression, property); + ts.setOriginalNode(expression, property); + expressions.push(expression); + } - if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifier(propertyName)) { - const privateIdentifierInfo = accessPrivateIdentifier(propertyName); - if (privateIdentifierInfo) { - if (privateIdentifierInfo.kind === PrivateIdentifierKind.Field) { - if (!privateIdentifierInfo.isStatic) { - return createPrivateInstanceFieldInitializer(receiver, ts.visitNode(property.initializer, visitor, ts.isExpression), privateIdentifierInfo.brandCheckIdentifier); - } - else { - return createPrivateStaticFieldInitializer(privateIdentifierInfo.variableName, ts.visitNode(property.initializer, visitor, ts.isExpression)); - } + 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: ts.PropertyDeclaration, receiver: ts.LeftHandSideExpression) { + const savedCurrentStaticPropertyDeclarationOrStaticBlock = currentStaticPropertyDeclarationOrStaticBlock; + const transformed = transformPropertyWorker(property, receiver); + if (transformed && ts.hasStaticModifier(property) && currentClassLexicalEnvironment?.facts) { + // capture the lexical environment for the member + ts.setOriginalNode(transformed, property); + ts.addEmitFlags(transformed, ts.EmitFlags.AdviseOnEmitNode); + classLexicalEnvironmentMap.set(ts.getOriginalNodeId(transformed), currentClassLexicalEnvironment); + } + currentStaticPropertyDeclarationOrStaticBlock = savedCurrentStaticPropertyDeclarationOrStaticBlock; + return transformed; + } + + function transformPropertyWorker(property: ts.PropertyDeclaration, receiver: ts.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 = !useDefineForClassFields; + const propertyName = ts.isComputedPropertyName(property.name) && !ts.isSimpleInlineableExpression(property.name.expression) + ? factory.updateComputedPropertyName(property.name, factory.getGeneratedNameForNode(property.name)) + : property.name; + + if (ts.hasStaticModifier(property)) { + currentStaticPropertyDeclarationOrStaticBlock = property; + } + + if (shouldTransformPrivateElementsOrClassStaticBlocks && ts.isPrivateIdentifier(propertyName)) { + const privateIdentifierInfo = accessPrivateIdentifier(propertyName); + if (privateIdentifierInfo) { + if (privateIdentifierInfo.kind === PrivateIdentifierKind.Field) { + if (!privateIdentifierInfo.isStatic) { + return createPrivateInstanceFieldInitializer(receiver, ts.visitNode(property.initializer, visitor, ts.isExpression), privateIdentifierInfo.brandCheckIdentifier); } else { - return undefined; + return createPrivateStaticFieldInitializer(privateIdentifierInfo.variableName, ts.visitNode(property.initializer, visitor, ts.isExpression)); } } else { - ts.Debug.fail("Undeclared private name for property declaration."); + return undefined; } } - if ((ts.isPrivateIdentifier(propertyName) || ts.hasStaticModifier(property)) && !property.initializer) { - return undefined; + else { + ts.Debug.fail("Undeclared private name for property declaration."); } + } + if ((ts.isPrivateIdentifier(propertyName) || ts.hasStaticModifier(property)) && !property.initializer) { + return undefined; + } - const propertyOriginalNode = ts.getOriginalNode(property); - if (ts.hasSyntacticModifier(propertyOriginalNode, ts.ModifierFlags.Abstract)) { - return undefined; - } + const propertyOriginalNode = ts.getOriginalNode(property); + if (ts.hasSyntacticModifier(propertyOriginalNode, ts.ModifierFlags.Abstract)) { + return undefined; + } - const initializer = property.initializer || emitAssignment ? ts.visitNode(property.initializer, visitor, ts.isExpression) ?? factory.createVoidZero() - : ts.isParameterPropertyDeclaration(propertyOriginalNode, propertyOriginalNode.parent) && ts.isIdentifier(propertyName) ? propertyName - : factory.createVoidZero(); + const initializer = property.initializer || emitAssignment ? ts.visitNode(property.initializer, visitor, ts.isExpression) ?? factory.createVoidZero() + : ts.isParameterPropertyDeclaration(propertyOriginalNode, propertyOriginalNode.parent) && ts.isIdentifier(propertyName) ? propertyName + : factory.createVoidZero(); - if (emitAssignment || ts.isPrivateIdentifier(propertyName)) { - const memberAccess = ts.createMemberAccessForPropertyName(factory, receiver, propertyName, /*location*/ propertyName); - return factory.createAssignment(memberAccess, initializer); - } - else { - const name = ts.isComputedPropertyName(propertyName) ? propertyName.expression - : ts.isIdentifier(propertyName) ? factory.createStringLiteral(ts.unescapeLeadingUnderscores(propertyName.escapedText)) - : propertyName; - const descriptor = factory.createPropertyDescriptor({ value: initializer, configurable: true, writable: true, enumerable: true }); - return factory.createObjectDefinePropertyCall(receiver, name, descriptor); - } + if (emitAssignment || ts.isPrivateIdentifier(propertyName)) { + const memberAccess = ts.createMemberAccessForPropertyName(factory, receiver, propertyName, /*location*/ propertyName); + return factory.createAssignment(memberAccess, initializer); } + else { + const name = ts.isComputedPropertyName(propertyName) ? propertyName.expression + : ts.isIdentifier(propertyName) ? factory.createStringLiteral(ts.unescapeLeadingUnderscores(propertyName.escapedText)) + : propertyName; + const descriptor = factory.createPropertyDescriptor({ value: initializer, configurable: true, writable: true, enumerable: true }); + return factory.createObjectDefinePropertyCall(receiver, name, descriptor); + } + } - function enableSubstitutionForClassAliases() { - if ((enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) === 0) { - enabledSubstitutions |= ClassPropertySubstitutionFlags.ClassAliases; + 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(ts.SyntaxKind.Identifier); + // We need to enable substitutions for identifiers. This allows us to + // substitute class names inside of a class declaration. + context.enableSubstitution(ts.SyntaxKind.Identifier); - // Keep track of class aliases. - classAliases = []; - } + // Keep track of class aliases. + classAliases = []; } + } - function enableSubstitutionForClassStaticThisOrSuperReference() { - if ((enabledSubstitutions & ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference) === 0) { - enabledSubstitutions |= ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference; + function enableSubstitutionForClassStaticThisOrSuperReference() { + if ((enabledSubstitutions & ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference) === 0) { + enabledSubstitutions |= ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference; - // substitute `this` in a static field initializer - context.enableSubstitution(ts.SyntaxKind.ThisKeyword); + // substitute `this` in a static field initializer + context.enableSubstitution(ts.SyntaxKind.ThisKeyword); - // these push a new lexical environment that is not the class lexical environment - context.enableEmitNotification(ts.SyntaxKind.FunctionDeclaration); - context.enableEmitNotification(ts.SyntaxKind.FunctionExpression); - context.enableEmitNotification(ts.SyntaxKind.Constructor); + // these push a new lexical environment that is not the class lexical environment + context.enableEmitNotification(ts.SyntaxKind.FunctionDeclaration); + context.enableEmitNotification(ts.SyntaxKind.FunctionExpression); + context.enableEmitNotification(ts.SyntaxKind.Constructor); - // these push a new lexical environment that is not the class lexical environment, except - // when they have a computed property name - context.enableEmitNotification(ts.SyntaxKind.GetAccessor); - context.enableEmitNotification(ts.SyntaxKind.SetAccessor); - context.enableEmitNotification(ts.SyntaxKind.MethodDeclaration); - context.enableEmitNotification(ts.SyntaxKind.PropertyDeclaration); + // these push a new lexical environment that is not the class lexical environment, except + // when they have a computed property name + context.enableEmitNotification(ts.SyntaxKind.GetAccessor); + context.enableEmitNotification(ts.SyntaxKind.SetAccessor); + context.enableEmitNotification(ts.SyntaxKind.MethodDeclaration); + context.enableEmitNotification(ts.SyntaxKind.PropertyDeclaration); - // class lexical environments are restored when entering a computed property name - context.enableEmitNotification(ts.SyntaxKind.ComputedPropertyName); - } + // class lexical environments are restored when entering a computed property name + context.enableEmitNotification(ts.SyntaxKind.ComputedPropertyName); + } + } + + /** + * Generates brand-check initializer for private methods. + * + * @param statements Statement list that should be used to append new statements. + * @param methods An array of method declarations. + * @param receiver The receiver on which each method should be assigned. + */ + function addMethodStatements(statements: ts.Statement[], methods: readonly (ts.MethodDeclaration | ts.AccessorDeclaration)[], receiver: ts.LeftHandSideExpression) { + if (!shouldTransformPrivateElementsOrClassStaticBlocks || !ts.some(methods)) { + return; } - /** - * Generates brand-check initializer for private methods. - * - * @param statements Statement list that should be used to append new statements. - * @param methods An array of method declarations. - * @param receiver The receiver on which each method should be assigned. - */ - function addMethodStatements(statements: ts.Statement[], methods: readonly (ts.MethodDeclaration | ts.AccessorDeclaration)[], receiver: ts.LeftHandSideExpression) { - if (!shouldTransformPrivateElementsOrClassStaticBlocks || !ts.some(methods)) { + const { weakSetName } = getPrivateIdentifierEnvironment(); + ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); + statements.push(factory.createExpressionStatement(createPrivateInstanceMethodInitializer(receiver, weakSetName))); + } + function visitInvalidSuperProperty(node: ts.SuperProperty) { + return ts.isPropertyAccessExpression(node) ? + factory.updatePropertyAccessExpression(node, factory.createVoidZero(), node.name) : + factory.updateElementAccessExpression(node, factory.createVoidZero(), ts.visitNode(node.argumentExpression, visitor, ts.isExpression)); + } + function onEmitNode(hint: ts.EmitHint, node: ts.Node, emitCallback: (hint: ts.EmitHint, node: ts.Node) => void) { + const original = ts.getOriginalNode(node); + if (original.id) { + const classLexicalEnvironment = classLexicalEnvironmentMap.get(original.id); + if (classLexicalEnvironment) { + const savedClassLexicalEnvironment = currentClassLexicalEnvironment; + const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; + currentClassLexicalEnvironment = classLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = classLexicalEnvironment; + previousOnEmitNode(hint, node, emitCallback); + currentClassLexicalEnvironment = savedClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; return; } + } - const { weakSetName } = getPrivateIdentifierEnvironment(); - ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); - statements.push(factory.createExpressionStatement(createPrivateInstanceMethodInitializer(receiver, weakSetName))); - } - function visitInvalidSuperProperty(node: ts.SuperProperty) { - return ts.isPropertyAccessExpression(node) ? - factory.updatePropertyAccessExpression(node, factory.createVoidZero(), node.name) : - factory.updateElementAccessExpression(node, factory.createVoidZero(), ts.visitNode(node.argumentExpression, visitor, ts.isExpression)); - } - function onEmitNode(hint: ts.EmitHint, node: ts.Node, emitCallback: (hint: ts.EmitHint, node: ts.Node) => void) { - const original = ts.getOriginalNode(node); - if (original.id) { - const classLexicalEnvironment = classLexicalEnvironmentMap.get(original.id); - if (classLexicalEnvironment) { - const savedClassLexicalEnvironment = currentClassLexicalEnvironment; - const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; - currentClassLexicalEnvironment = classLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = classLexicalEnvironment; - previousOnEmitNode(hint, node, emitCallback); - currentClassLexicalEnvironment = savedClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; - return; + switch (node.kind) { + case ts.SyntaxKind.FunctionExpression: + if (ts.isArrowFunction(original) || ts.getEmitFlags(node) & ts.EmitFlags.AsyncFunctionBody) { + break; } - } - - switch (node.kind) { - case ts.SyntaxKind.FunctionExpression: - if (ts.isArrowFunction(original) || ts.getEmitFlags(node) & ts.EmitFlags.AsyncFunctionBody) { - break; - } - // falls through - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.Constructor: { - const savedClassLexicalEnvironment = currentClassLexicalEnvironment; - const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; - currentClassLexicalEnvironment = undefined; - currentComputedPropertyNameClassLexicalEnvironment = undefined; - previousOnEmitNode(hint, node, emitCallback); - currentClassLexicalEnvironment = savedClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; - return; - } + // falls through + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Constructor: { + const savedClassLexicalEnvironment = currentClassLexicalEnvironment; + const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; + currentClassLexicalEnvironment = undefined; + currentComputedPropertyNameClassLexicalEnvironment = undefined; + previousOnEmitNode(hint, node, emitCallback); + currentClassLexicalEnvironment = savedClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; + return; + } - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.PropertyDeclaration: { - const savedClassLexicalEnvironment = currentClassLexicalEnvironment; - const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = currentClassLexicalEnvironment; - currentClassLexicalEnvironment = undefined; - previousOnEmitNode(hint, node, emitCallback); - currentClassLexicalEnvironment = savedClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; - return; - } - case ts.SyntaxKind.ComputedPropertyName: { - const savedClassLexicalEnvironment = currentClassLexicalEnvironment; - const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; - currentClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = undefined; - previousOnEmitNode(hint, node, emitCallback); - currentClassLexicalEnvironment = savedClassLexicalEnvironment; - currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; - return; - } + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.PropertyDeclaration: { + const savedClassLexicalEnvironment = currentClassLexicalEnvironment; + const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = currentClassLexicalEnvironment; + currentClassLexicalEnvironment = undefined; + previousOnEmitNode(hint, node, emitCallback); + currentClassLexicalEnvironment = savedClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; + return; } - previousOnEmitNode(hint, node, emitCallback); - } - - /** - * Hooks node substitutions. - * - * @param hint The context for the emitter. - * @param node The node to substitute. - */ - function onSubstituteNode(hint: ts.EmitHint, node: ts.Node) { - node = previousOnSubstituteNode(hint, node); - if (hint === ts.EmitHint.Expression) { - return substituteExpression(node as ts.Expression); + case ts.SyntaxKind.ComputedPropertyName: { + const savedClassLexicalEnvironment = currentClassLexicalEnvironment; + const savedCurrentComputedPropertyNameClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; + currentClassLexicalEnvironment = currentComputedPropertyNameClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = undefined; + previousOnEmitNode(hint, node, emitCallback); + currentClassLexicalEnvironment = savedClassLexicalEnvironment; + currentComputedPropertyNameClassLexicalEnvironment = savedCurrentComputedPropertyNameClassLexicalEnvironment; + return; } - return node; } + previousOnEmitNode(hint, node, emitCallback); + } - function substituteExpression(node: ts.Expression) { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return substituteExpressionIdentifier(node as ts.Identifier); - case ts.SyntaxKind.ThisKeyword: - return substituteThisExpression(node as ts.ThisExpression); - } - return node; + /** + * Hooks node substitutions. + * + * @param hint The context for the emitter. + * @param node The node to substitute. + */ + function onSubstituteNode(hint: ts.EmitHint, node: ts.Node) { + node = previousOnSubstituteNode(hint, node); + if (hint === ts.EmitHint.Expression) { + return substituteExpression(node as ts.Expression); } + return node; + } - function substituteThisExpression(node: ts.ThisExpression) { - if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference && currentClassLexicalEnvironment) { - const { facts, classConstructor } = currentClassLexicalEnvironment; - if (facts & ClassFacts.ClassWasDecorated) { - return factory.createParenthesizedExpression(factory.createVoidZero()); - } - if (classConstructor) { - return ts.setTextRange(ts.setOriginalNode(factory.cloneNode(classConstructor), node), node); - } + function substituteExpression(node: ts.Expression) { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return substituteExpressionIdentifier(node as ts.Identifier); + case ts.SyntaxKind.ThisKeyword: + return substituteThisExpression(node as ts.ThisExpression); + } + return node; + } + + function substituteThisExpression(node: ts.ThisExpression) { + if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassStaticThisOrSuperReference && currentClassLexicalEnvironment) { + const { facts, classConstructor } = currentClassLexicalEnvironment; + if (facts & ClassFacts.ClassWasDecorated) { + return factory.createParenthesizedExpression(factory.createVoidZero()); + } + if (classConstructor) { + return ts.setTextRange(ts.setOriginalNode(factory.cloneNode(classConstructor), node), node); } - return node; } + return node; + } - function substituteExpressionIdentifier(node: ts.Identifier): ts.Expression { - return trySubstituteClassAlias(node) || node; - } - - function trySubstituteClassAlias(node: ts.Identifier): ts.Expression | undefined { - if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) { - if (resolver.getNodeCheckFlags(node) & ts.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 = factory.cloneNode(classAlias); - ts.setSourceMapRange(clone, node); - ts.setCommentRange(clone, node); - return clone; - } + function substituteExpressionIdentifier(node: ts.Identifier): ts.Expression { + return trySubstituteClassAlias(node) || node; + } + + function trySubstituteClassAlias(node: ts.Identifier): ts.Expression | undefined { + if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) { + if (resolver.getNodeCheckFlags(node) & ts.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 = factory.cloneNode(classAlias); + ts.setSourceMapRange(clone, node); + ts.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: ts.PropertyName, shouldHoist: boolean): ts.Expression | undefined { - if (ts.isComputedPropertyName(name)) { - const expression = ts.visitNode(name.expression, visitor, ts.isExpression); - const innerExpression = ts.skipPartiallyEmittedExpressions(expression); - const inlinable = ts.isSimpleInlineableExpression(innerExpression); - const alreadyTransformed = ts.isAssignmentExpression(innerExpression) && ts.isGeneratedIdentifier(innerExpression.left); - if (!alreadyTransformed && !inlinable && shouldHoist) { - const generatedName = factory.getGeneratedNameForNode(name); - if (resolver.getNodeCheckFlags(name) & ts.NodeCheckFlags.BlockScopedBindingInLoop) { - addBlockScopedVariable(generatedName); - } - else { - hoistVariableDeclaration(generatedName); - } - return factory.createAssignment(generatedName, expression); + 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: ts.PropertyName, shouldHoist: boolean): ts.Expression | undefined { + if (ts.isComputedPropertyName(name)) { + const expression = ts.visitNode(name.expression, visitor, ts.isExpression); + const innerExpression = ts.skipPartiallyEmittedExpressions(expression); + const inlinable = ts.isSimpleInlineableExpression(innerExpression); + const alreadyTransformed = ts.isAssignmentExpression(innerExpression) && ts.isGeneratedIdentifier(innerExpression.left); + if (!alreadyTransformed && !inlinable && shouldHoist) { + const generatedName = factory.getGeneratedNameForNode(name); + if (resolver.getNodeCheckFlags(name) & ts.NodeCheckFlags.BlockScopedBindingInLoop) { + addBlockScopedVariable(generatedName); + } + else { + hoistVariableDeclaration(generatedName); } - return (inlinable || ts.isIdentifier(innerExpression)) ? undefined : expression; + return factory.createAssignment(generatedName, expression); } + return (inlinable || ts.isIdentifier(innerExpression)) ? undefined : expression; } + } - function startClassLexicalEnvironment() { - classLexicalEnvironmentStack.push(currentClassLexicalEnvironment); - currentClassLexicalEnvironment = undefined; - } + function startClassLexicalEnvironment() { + classLexicalEnvironmentStack.push(currentClassLexicalEnvironment); + currentClassLexicalEnvironment = undefined; + } - function endClassLexicalEnvironment() { - currentClassLexicalEnvironment = classLexicalEnvironmentStack.pop(); - } + function endClassLexicalEnvironment() { + currentClassLexicalEnvironment = classLexicalEnvironmentStack.pop(); + } - function getClassLexicalEnvironment() { - return currentClassLexicalEnvironment ||= { - facts: ClassFacts.None, - classConstructor: undefined, - superClassReference: undefined, - privateIdentifierEnvironment: undefined, - }; - } + function getClassLexicalEnvironment() { + return currentClassLexicalEnvironment ||= { + facts: ClassFacts.None, + classConstructor: undefined, + superClassReference: undefined, + privateIdentifierEnvironment: undefined, + }; + } - function getPrivateIdentifierEnvironment() { - const lex = getClassLexicalEnvironment(); - lex.privateIdentifierEnvironment ||= { - className: "", - identifiers: new ts.Map() - }; - return lex.privateIdentifierEnvironment; - } + function getPrivateIdentifierEnvironment() { + const lex = getClassLexicalEnvironment(); + lex.privateIdentifierEnvironment ||= { + className: "", + identifiers: new ts.Map() + }; + return lex.privateIdentifierEnvironment; + } - function getPendingExpressions() { - return pendingExpressions || (pendingExpressions = []); - } + function getPendingExpressions() { + return pendingExpressions || (pendingExpressions = []); + } - function addPrivateIdentifierToEnvironment(node: ts.PrivateClassElementDeclaration) { - const text = ts.getTextOfPropertyName(node.name) as string; - const lex = getClassLexicalEnvironment(); - const { classConstructor } = lex; + function addPrivateIdentifierToEnvironment(node: ts.PrivateClassElementDeclaration) { + const text = ts.getTextOfPropertyName(node.name) as string; + const lex = getClassLexicalEnvironment(); + const { classConstructor } = lex; - const privateEnv = getPrivateIdentifierEnvironment(); - const { weakSetName } = privateEnv; + const privateEnv = getPrivateIdentifierEnvironment(); + const { weakSetName } = privateEnv; - const assignmentExpressions: ts.Expression[] = []; + const assignmentExpressions: ts.Expression[] = []; - const privateName = node.name.escapedText; - const previousInfo = privateEnv.identifiers.get(privateName); - const isValid = !isReservedPrivateName(node.name) && previousInfo === undefined; + const privateName = node.name.escapedText; + const previousInfo = privateEnv.identifiers.get(privateName); + const isValid = !isReservedPrivateName(node.name) && previousInfo === undefined; - if (ts.hasStaticModifier(node)) { - ts.Debug.assert(classConstructor, "weakSetName should be set in private identifier environment"); - if (ts.isPropertyDeclaration(node)) { - const variableName = createHoistedVariableForPrivateName(text, node); + if (ts.hasStaticModifier(node)) { + ts.Debug.assert(classConstructor, "weakSetName should be set in private identifier environment"); + if (ts.isPropertyDeclaration(node)) { + const variableName = createHoistedVariableForPrivateName(text, node); + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Field, + variableName, + brandCheckIdentifier: classConstructor, + isStatic: true, + isValid, + }); + } + else if (ts.isMethodDeclaration(node)) { + const functionName = createHoistedVariableForPrivateName(text, node); + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Method, + methodName: functionName, + brandCheckIdentifier: classConstructor, + isStatic: true, + isValid, + }); + } + else if (ts.isGetAccessorDeclaration(node)) { + const getterName = createHoistedVariableForPrivateName(text + "_get", node); + if (previousInfo?.kind === PrivateIdentifierKind.Accessor && previousInfo.isStatic && !previousInfo.getterName) { + previousInfo.getterName = getterName; + } + else { privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Field, - variableName, + kind: PrivateIdentifierKind.Accessor, + getterName, + setterName: undefined, brandCheckIdentifier: classConstructor, isStatic: true, isValid, }); } - else if (ts.isMethodDeclaration(node)) { - const functionName = createHoistedVariableForPrivateName(text, node); + } + else if (ts.isSetAccessorDeclaration(node)) { + const setterName = createHoistedVariableForPrivateName(text + "_set", node); + if (previousInfo?.kind === PrivateIdentifierKind.Accessor && previousInfo.isStatic && !previousInfo.setterName) { + previousInfo.setterName = setterName; + } + else { privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Method, - methodName: functionName, + kind: PrivateIdentifierKind.Accessor, + getterName: undefined, + setterName, brandCheckIdentifier: classConstructor, isStatic: true, isValid, }); } - else if (ts.isGetAccessorDeclaration(node)) { - const getterName = createHoistedVariableForPrivateName(text + "_get", node); - if (previousInfo?.kind === PrivateIdentifierKind.Accessor && previousInfo.isStatic && !previousInfo.getterName) { - previousInfo.getterName = getterName; - } - else { - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Accessor, - getterName, - setterName: undefined, - brandCheckIdentifier: classConstructor, - isStatic: true, - isValid, - }); - } - } - else if (ts.isSetAccessorDeclaration(node)) { - const setterName = createHoistedVariableForPrivateName(text + "_set", node); - if (previousInfo?.kind === PrivateIdentifierKind.Accessor && previousInfo.isStatic && !previousInfo.setterName) { - previousInfo.setterName = setterName; - } - else { - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Accessor, - getterName: undefined, - setterName, - brandCheckIdentifier: classConstructor, - isStatic: true, - isValid, - }); - } - } - else { - ts.Debug.assertNever(node, "Unknown class element type."); - } } - else if (ts.isPropertyDeclaration(node)) { - const weakMapName = createHoistedVariableForPrivateName(text, node); - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Field, - brandCheckIdentifier: weakMapName, - isStatic: false, - variableName: undefined, - isValid, - }); - - assignmentExpressions.push(factory.createAssignment(weakMapName, factory.createNewExpression(factory.createIdentifier("WeakMap"), - /*typeArguments*/ undefined, []))); + else { + ts.Debug.assertNever(node, "Unknown class element type."); } - else if (ts.isMethodDeclaration(node)) { - ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); + } + else if (ts.isPropertyDeclaration(node)) { + const weakMapName = createHoistedVariableForPrivateName(text, node); + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Field, + brandCheckIdentifier: weakMapName, + isStatic: false, + variableName: undefined, + isValid, + }); - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Method, - methodName: createHoistedVariableForPrivateName(text, node), - brandCheckIdentifier: weakSetName, - isStatic: false, - isValid, - }); - } - else if (ts.isAccessor(node)) { - ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); - if (ts.isGetAccessor(node)) { - const getterName = createHoistedVariableForPrivateName(text + "_get", node); + assignmentExpressions.push(factory.createAssignment(weakMapName, factory.createNewExpression(factory.createIdentifier("WeakMap"), + /*typeArguments*/ undefined, []))); + } + else if (ts.isMethodDeclaration(node)) { + ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); - if (previousInfo?.kind === PrivateIdentifierKind.Accessor && !previousInfo.isStatic && !previousInfo.getterName) { - previousInfo.getterName = getterName; - } - else { - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Accessor, - getterName, - setterName: undefined, - brandCheckIdentifier: weakSetName, - isStatic: false, - isValid, - }); - } + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Method, + methodName: createHoistedVariableForPrivateName(text, node), + brandCheckIdentifier: weakSetName, + isStatic: false, + isValid, + }); + } + else if (ts.isAccessor(node)) { + ts.Debug.assert(weakSetName, "weakSetName should be set in private identifier environment"); + if (ts.isGetAccessor(node)) { + const getterName = createHoistedVariableForPrivateName(text + "_get", node); + + if (previousInfo?.kind === PrivateIdentifierKind.Accessor && !previousInfo.isStatic && !previousInfo.getterName) { + previousInfo.getterName = getterName; } else { - const setterName = createHoistedVariableForPrivateName(text + "_set", node); - - if (previousInfo?.kind === PrivateIdentifierKind.Accessor && !previousInfo.isStatic && !previousInfo.setterName) { - previousInfo.setterName = setterName; - } - else { - privateEnv.identifiers.set(privateName, { - kind: PrivateIdentifierKind.Accessor, - getterName: undefined, - setterName, - brandCheckIdentifier: weakSetName, - isStatic: false, - isValid, - }); - } + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Accessor, + getterName, + setterName: undefined, + brandCheckIdentifier: weakSetName, + isStatic: false, + isValid, + }); } } else { - ts.Debug.assertNever(node, "Unknown class element type."); + const setterName = createHoistedVariableForPrivateName(text + "_set", node); + + if (previousInfo?.kind === PrivateIdentifierKind.Accessor && !previousInfo.isStatic && !previousInfo.setterName) { + previousInfo.setterName = setterName; + } + else { + privateEnv.identifiers.set(privateName, { + kind: PrivateIdentifierKind.Accessor, + getterName: undefined, + setterName, + brandCheckIdentifier: weakSetName, + isStatic: false, + isValid, + }); + } } + } + else { + ts.Debug.assertNever(node, "Unknown class element type."); + } + + getPendingExpressions().push(...assignmentExpressions); + } - getPendingExpressions().push(...assignmentExpressions); + function createHoistedVariableForClass(name: string, node: ts.PrivateIdentifier | ts.ClassStaticBlockDeclaration): ts.Identifier { + const { className } = getPrivateIdentifierEnvironment(); + const prefix = className ? `_${className}` : ""; + const identifier = factory.createUniqueName(`${prefix}_${name}`, ts.GeneratedIdentifierFlags.Optimistic); + if (resolver.getNodeCheckFlags(node) & ts.NodeCheckFlags.BlockScopedBindingInLoop) { + addBlockScopedVariable(identifier); + } + else { + hoistVariableDeclaration(identifier); } - function createHoistedVariableForClass(name: string, node: ts.PrivateIdentifier | ts.ClassStaticBlockDeclaration): ts.Identifier { - const { className } = getPrivateIdentifierEnvironment(); - const prefix = className ? `_${className}` : ""; - const identifier = factory.createUniqueName(`${prefix}_${name}`, ts.GeneratedIdentifierFlags.Optimistic); - if (resolver.getNodeCheckFlags(node) & ts.NodeCheckFlags.BlockScopedBindingInLoop) { - addBlockScopedVariable(identifier); + return identifier; + } + + function createHoistedVariableForPrivateName(privateName: string, node: ts.PrivateClassElementDeclaration): ts.Identifier { + return createHoistedVariableForClass(privateName.substring(1), node.name); + } + + function accessPrivateIdentifier(name: ts.PrivateIdentifier) { + if (currentClassLexicalEnvironment?.privateIdentifierEnvironment) { + const info = currentClassLexicalEnvironment.privateIdentifierEnvironment.identifiers.get(name.escapedText); + if (info) { + return info; } - else { - hoistVariableDeclaration(identifier); + } + for (let i = classLexicalEnvironmentStack.length - 1; i >= 0; --i) { + const env = classLexicalEnvironmentStack[i]; + if (!env) { + continue; + } + const info = env.privateIdentifierEnvironment?.identifiers.get(name.escapedText); + if (info) { + return info; } - - return identifier; } + return undefined; + } - function createHoistedVariableForPrivateName(privateName: string, node: ts.PrivateClassElementDeclaration): ts.Identifier { - return createHoistedVariableForClass(privateName.substring(1), node.name); + function wrapPrivateIdentifierForDestructuringTarget(node: ts.PrivateIdentifierPropertyAccessExpression) { + const parameter = factory.getGeneratedNameForNode(node); + const info = accessPrivateIdentifier(node.name); + if (!info) { + return ts.visitEachChild(node, visitor, context); } - - function accessPrivateIdentifier(name: ts.PrivateIdentifier) { - if (currentClassLexicalEnvironment?.privateIdentifierEnvironment) { - const info = currentClassLexicalEnvironment.privateIdentifierEnvironment.identifiers.get(name.escapedText); - if (info) { - return info; + let receiver = node.expression; + // We cannot copy `this` or `super` into the function because they will be bound + // differently inside the function. + if (ts.isThisProperty(node) || ts.isSuperProperty(node) || !ts.isSimpleCopiableExpression(node.expression)) { + receiver = factory.createTempVariable(hoistVariableDeclaration, /*reservedInNestedScopes*/ true); + getPendingExpressions().push(factory.createBinaryExpression(receiver, ts.SyntaxKind.EqualsToken, ts.visitNode(node.expression, visitor, ts.isExpression))); + } + return factory.createAssignmentTargetWrapper(parameter, createPrivateIdentifierAssignment(info, receiver, parameter, ts.SyntaxKind.EqualsToken)); + } + function visitArrayAssignmentTarget(node: ts.BindingOrAssignmentElement) { + const target = ts.getTargetOfBindingOrAssignmentElement(node); + if (target) { + let wrapped: ts.LeftHandSideExpression | undefined; + if (ts.isPrivateIdentifierPropertyAccessExpression(target)) { + wrapped = wrapPrivateIdentifierForDestructuringTarget(target); + } + else if (shouldTransformSuperInStaticInitializers && + ts.isSuperProperty(target) && + currentStaticPropertyDeclarationOrStaticBlock && + currentClassLexicalEnvironment) { + const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; + if (facts & ClassFacts.ClassWasDecorated) { + wrapped = visitInvalidSuperProperty(target); + } + else if (classConstructor && superClassReference) { + const name = ts.isElementAccessExpression(target) ? ts.visitNode(target.argumentExpression, visitor, ts.isExpression) : + ts.isIdentifier(target.name) ? factory.createStringLiteralFromNode(target.name) : + undefined; + if (name) { + const temp = factory.createTempVariable(/*recordTempVariable*/ undefined); + wrapped = factory.createAssignmentTargetWrapper(temp, factory.createReflectSetCall(superClassReference, name, temp, classConstructor)); + } } } - for (let i = classLexicalEnvironmentStack.length - 1; i >= 0; --i) { - const env = classLexicalEnvironmentStack[i]; - if (!env) { - continue; + if (wrapped) { + if (ts.isAssignmentExpression(node)) { + return factory.updateBinaryExpression(node, wrapped, node.operatorToken, ts.visitNode(node.right, visitor, ts.isExpression)); } - const info = env.privateIdentifierEnvironment?.identifiers.get(name.escapedText); - if (info) { - return info; + else if (ts.isSpreadElement(node)) { + return factory.updateSpreadElement(node, wrapped); + } + else { + return wrapped; } } - return undefined; } + return ts.visitNode(node, visitorDestructuringTarget); + } - function wrapPrivateIdentifierForDestructuringTarget(node: ts.PrivateIdentifierPropertyAccessExpression) { - const parameter = factory.getGeneratedNameForNode(node); - const info = accessPrivateIdentifier(node.name); - if (!info) { - return ts.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 (ts.isThisProperty(node) || ts.isSuperProperty(node) || !ts.isSimpleCopiableExpression(node.expression)) { - receiver = factory.createTempVariable(hoistVariableDeclaration, /*reservedInNestedScopes*/ true); - getPendingExpressions().push(factory.createBinaryExpression(receiver, ts.SyntaxKind.EqualsToken, ts.visitNode(node.expression, visitor, ts.isExpression))); - } - return factory.createAssignmentTargetWrapper(parameter, createPrivateIdentifierAssignment(info, receiver, parameter, ts.SyntaxKind.EqualsToken)); - } - function visitArrayAssignmentTarget(node: ts.BindingOrAssignmentElement) { + function visitObjectAssignmentTarget(node: ts.ObjectLiteralElementLike) { + if (ts.isObjectBindingOrAssignmentElement(node) && !ts.isShorthandPropertyAssignment(node)) { const target = ts.getTargetOfBindingOrAssignmentElement(node); + let wrapped: ts.LeftHandSideExpression | undefined; if (target) { - let wrapped: ts.LeftHandSideExpression | undefined; if (ts.isPrivateIdentifierPropertyAccessExpression(target)) { wrapped = wrapPrivateIdentifierForDestructuringTarget(target); } @@ -1782,101 +1823,60 @@ namespace ts { } } } - if (wrapped) { - if (ts.isAssignmentExpression(node)) { - return factory.updateBinaryExpression(node, wrapped, node.operatorToken, ts.visitNode(node.right, visitor, ts.isExpression)); - } - else if (ts.isSpreadElement(node)) { - return factory.updateSpreadElement(node, wrapped); - } - else { - return wrapped; - } - } - } - return ts.visitNode(node, visitorDestructuringTarget); - } - - function visitObjectAssignmentTarget(node: ts.ObjectLiteralElementLike) { - if (ts.isObjectBindingOrAssignmentElement(node) && !ts.isShorthandPropertyAssignment(node)) { - const target = ts.getTargetOfBindingOrAssignmentElement(node); - let wrapped: ts.LeftHandSideExpression | undefined; - if (target) { - if (ts.isPrivateIdentifierPropertyAccessExpression(target)) { - wrapped = wrapPrivateIdentifierForDestructuringTarget(target); - } - else if (shouldTransformSuperInStaticInitializers && - ts.isSuperProperty(target) && - currentStaticPropertyDeclarationOrStaticBlock && - currentClassLexicalEnvironment) { - const { classConstructor, superClassReference, facts } = currentClassLexicalEnvironment; - if (facts & ClassFacts.ClassWasDecorated) { - wrapped = visitInvalidSuperProperty(target); - } - else if (classConstructor && superClassReference) { - const name = ts.isElementAccessExpression(target) ? ts.visitNode(target.argumentExpression, visitor, ts.isExpression) : - ts.isIdentifier(target.name) ? factory.createStringLiteralFromNode(target.name) : - undefined; - if (name) { - const temp = factory.createTempVariable(/*recordTempVariable*/ undefined); - wrapped = factory.createAssignmentTargetWrapper(temp, factory.createReflectSetCall(superClassReference, name, temp, classConstructor)); - } - } - } - } - if (ts.isPropertyAssignment(node)) { - const initializer = ts.getInitializerOfBindingOrAssignmentElement(node); - return factory.updatePropertyAssignment(node, ts.visitNode(node.name, visitor, ts.isPropertyName), wrapped ? - initializer ? factory.createAssignment(wrapped, ts.visitNode(initializer, visitor)) : wrapped : - ts.visitNode(node.initializer, visitorDestructuringTarget, ts.isExpression)); - } - if (ts.isSpreadAssignment(node)) { - return factory.updateSpreadAssignment(node, wrapped || ts.visitNode(node.expression, visitorDestructuringTarget, ts.isExpression)); - } - ts.Debug.assert(wrapped === undefined, "Should not have generated a wrapped target"); } - return ts.visitNode(node, visitor); - } - function visitAssignmentPattern(node: ts.AssignmentPattern) { - if (ts.isArrayLiteralExpression(node)) { - // Transforms private names in destructuring assignment array bindings. - // Transforms SuperProperty assignments in destructuring assignment array bindings in static initializers. - // - // Source: - // ([ this.#myProp ] = [ "hello" ]); - // - // Transformation: - // [ { set value(x) { this.#myProp = x; } }.value ] = [ "hello" ]; - return factory.updateArrayLiteralExpression(node, ts.visitNodes(node.elements, visitArrayAssignmentTarget, ts.isExpression)); + if (ts.isPropertyAssignment(node)) { + const initializer = ts.getInitializerOfBindingOrAssignmentElement(node); + return factory.updatePropertyAssignment(node, ts.visitNode(node.name, visitor, ts.isPropertyName), wrapped ? + initializer ? factory.createAssignment(wrapped, ts.visitNode(initializer, visitor)) : wrapped : + ts.visitNode(node.initializer, visitorDestructuringTarget, ts.isExpression)); } - else { - // Transforms private names in destructuring assignment object bindings. - // Transforms SuperProperty assignments in destructuring assignment object bindings in static initializers. - // - // Source: - // ({ stringProperty: this.#myProp } = { stringProperty: "hello" }); - // - // Transformation: - // ({ stringProperty: { set value(x) { this.#myProp = x; } }.value }) = { stringProperty: "hello" }; - return factory.updateObjectLiteralExpression(node, ts.visitNodes(node.properties, visitObjectAssignmentTarget, ts.isObjectLiteralElementLike)); + if (ts.isSpreadAssignment(node)) { + return factory.updateSpreadAssignment(node, wrapped || ts.visitNode(node.expression, visitorDestructuringTarget, ts.isExpression)); } + ts.Debug.assert(wrapped === undefined, "Should not have generated a wrapped target"); } + return ts.visitNode(node, visitor); } - - function createPrivateStaticFieldInitializer(variableName: ts.Identifier, initializer: ts.Expression | undefined) { - return ts.factory.createAssignment(variableName, ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment("value", initializer || ts.factory.createVoidZero()) - ])); - } - function createPrivateInstanceFieldInitializer(receiver: ts.LeftHandSideExpression, initializer: ts.Expression | undefined, weakMapName: ts.Identifier) { - return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(weakMapName, "set"), - /*typeArguments*/ undefined, [receiver, initializer || ts.factory.createVoidZero()]); - } - function createPrivateInstanceMethodInitializer(receiver: ts.LeftHandSideExpression, weakSetName: ts.Identifier) { - return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(weakSetName, "add"), - /*typeArguments*/ undefined, [receiver]); - } - function isReservedPrivateName(node: ts.PrivateIdentifier) { - return node.escapedText === "#constructor"; + function visitAssignmentPattern(node: ts.AssignmentPattern) { + if (ts.isArrayLiteralExpression(node)) { + // Transforms private names in destructuring assignment array bindings. + // Transforms SuperProperty assignments in destructuring assignment array bindings in static initializers. + // + // Source: + // ([ this.#myProp ] = [ "hello" ]); + // + // Transformation: + // [ { set value(x) { this.#myProp = x; } }.value ] = [ "hello" ]; + return factory.updateArrayLiteralExpression(node, ts.visitNodes(node.elements, visitArrayAssignmentTarget, ts.isExpression)); + } + else { + // Transforms private names in destructuring assignment object bindings. + // Transforms SuperProperty assignments in destructuring assignment object bindings in static initializers. + // + // Source: + // ({ stringProperty: this.#myProp } = { stringProperty: "hello" }); + // + // Transformation: + // ({ stringProperty: { set value(x) { this.#myProp = x; } }.value }) = { stringProperty: "hello" }; + return factory.updateObjectLiteralExpression(node, ts.visitNodes(node.properties, visitObjectAssignmentTarget, ts.isObjectLiteralElementLike)); + } } } + +function createPrivateStaticFieldInitializer(variableName: ts.Identifier, initializer: ts.Expression | undefined) { + return ts.factory.createAssignment(variableName, ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment("value", initializer || ts.factory.createVoidZero()) + ])); +} +function createPrivateInstanceFieldInitializer(receiver: ts.LeftHandSideExpression, initializer: ts.Expression | undefined, weakMapName: ts.Identifier) { + return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(weakMapName, "set"), + /*typeArguments*/ undefined, [receiver, initializer || ts.factory.createVoidZero()]); +} +function createPrivateInstanceMethodInitializer(receiver: ts.LeftHandSideExpression, weakSetName: ts.Identifier) { + return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(weakSetName, "add"), + /*typeArguments*/ undefined, [receiver]); +} +function isReservedPrivateName(node: ts.PrivateIdentifier) { + return node.escapedText === "#constructor"; +} +} diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index e1385abd802af..cc5d846ff418c 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -1,1504 +1,1504 @@ /*@internal*/ namespace ts { - export function getDeclarationDiagnostics(host: ts.EmitHost, resolver: ts.EmitResolver, file: ts.SourceFile | undefined): ts.DiagnosticWithLocation[] | undefined { - const compilerOptions = host.getCompilerOptions(); - const result = ts.transformNodes(resolver, host, ts.factory, compilerOptions, file ? [file] : ts.filter(host.getSourceFiles(), ts.isSourceFileNotJson), [transformDeclarations], /*allowDtsFiles*/ false); - return result.diagnostics; - } +export function getDeclarationDiagnostics(host: ts.EmitHost, resolver: ts.EmitResolver, file: ts.SourceFile | undefined): ts.DiagnosticWithLocation[] | undefined { + const compilerOptions = host.getCompilerOptions(); + const result = ts.transformNodes(resolver, host, ts.factory, compilerOptions, file ? [file] : ts.filter(host.getSourceFiles(), ts.isSourceFileNotJson), [transformDeclarations], /*allowDtsFiles*/ false); + return result.diagnostics; +} - function hasInternalAnnotation(range: ts.CommentRange, currentSourceFile: ts.SourceFile) { - const comment = currentSourceFile.text.substring(range.pos, range.end); - return ts.stringContains(comment, "@internal"); - } - export function isInternalDeclaration(node: ts.Node, currentSourceFile: ts.SourceFile) { - const parseTreeNode = ts.getParseTreeNode(node); - if (parseTreeNode && parseTreeNode.kind === ts.SyntaxKind.Parameter) { - const paramIdx = (parseTreeNode.parent as ts.SignatureDeclaration).parameters.indexOf(parseTreeNode as ts.ParameterDeclaration); - const previousSibling = paramIdx > 0 ? (parseTreeNode.parent as ts.SignatureDeclaration).parameters[paramIdx - 1] : undefined; - const text = currentSourceFile.text; - const commentRanges = previousSibling - ? ts.concatenate( - // to handle - // ... parameters, /* @internal */ - // public param: string - ts.getTrailingCommentRanges(text, ts.skipTrivia(text, previousSibling.end + 1, /* stopAfterLineBreak */ false, /* stopAtComments */ true)), ts.getLeadingCommentRanges(text, node.pos)) - : ts.getTrailingCommentRanges(text, ts.skipTrivia(text, node.pos, /* stopAfterLineBreak */ false, /* stopAtComments */ true)); - return commentRanges && commentRanges.length && hasInternalAnnotation(ts.last(commentRanges), currentSourceFile); - } - const leadingCommentRanges = parseTreeNode && ts.getLeadingCommentRangesOfNode(parseTreeNode, currentSourceFile); - return !!ts.forEach(leadingCommentRanges, range => { - return hasInternalAnnotation(range, currentSourceFile); - }); +function hasInternalAnnotation(range: ts.CommentRange, currentSourceFile: ts.SourceFile) { + const comment = currentSourceFile.text.substring(range.pos, range.end); + return ts.stringContains(comment, "@internal"); +} +export function isInternalDeclaration(node: ts.Node, currentSourceFile: ts.SourceFile) { + const parseTreeNode = ts.getParseTreeNode(node); + if (parseTreeNode && parseTreeNode.kind === ts.SyntaxKind.Parameter) { + const paramIdx = (parseTreeNode.parent as ts.SignatureDeclaration).parameters.indexOf(parseTreeNode as ts.ParameterDeclaration); + const previousSibling = paramIdx > 0 ? (parseTreeNode.parent as ts.SignatureDeclaration).parameters[paramIdx - 1] : undefined; + const text = currentSourceFile.text; + const commentRanges = previousSibling + ? ts.concatenate( + // to handle + // ... parameters, /* @internal */ + // public param: string + ts.getTrailingCommentRanges(text, ts.skipTrivia(text, previousSibling.end + 1, /* stopAfterLineBreak */ false, /* stopAtComments */ true)), ts.getLeadingCommentRanges(text, node.pos)) + : ts.getTrailingCommentRanges(text, ts.skipTrivia(text, node.pos, /* stopAfterLineBreak */ false, /* stopAtComments */ true)); + return commentRanges && commentRanges.length && hasInternalAnnotation(ts.last(commentRanges), currentSourceFile); } + const leadingCommentRanges = parseTreeNode && ts.getLeadingCommentRangesOfNode(parseTreeNode, currentSourceFile); + return !!ts.forEach(leadingCommentRanges, range => { + return hasInternalAnnotation(range, currentSourceFile); + }); +} - const declarationEmitNodeBuilderFlags = ts.NodeBuilderFlags.MultilineObjectLiterals | - ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral | - ts.NodeBuilderFlags.UseTypeOfFunction | - ts.NodeBuilderFlags.UseStructuralFallback | - ts.NodeBuilderFlags.AllowEmptyTuple | - ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams | - ts.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: ts.TransformationContext) { - const throwDiagnostic = () => ts.Debug.fail("Diagnostic emitted without context"); - let getSymbolAccessibilityDiagnostic: ts.GetSymbolAccessibilityDiagnostic = throwDiagnostic; - let needsDeclare = true; - let isBundledEmit = false; - let resultHasExternalModuleIndicator = false; - let needsScopeFixMarker = false; - let resultHasScopeMarker = false; - let enclosingDeclaration: ts.Node; - let necessaryTypeReferences: ts.Set<[ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ]> | undefined; - let lateMarkedStatements: ts.LateVisibilityPaintedStatement[] | undefined; - let lateStatementReplacementMap: ts.ESMap>; - let suppressNewDiagnosticContexts: boolean; - let exportedModulesFromDeclarationEmit: ts.Symbol[] | undefined; - - const { factory } = context; - const host = context.getEmitHost(); - const symbolTracker: ts.SymbolTracker = { - trackSymbol, - reportInaccessibleThisError, - reportInaccessibleUniqueSymbolError, - reportCyclicStructureError, - reportPrivateInBaseOfClassExpression, - reportLikelyUnsafeImportRequiredError, - reportTruncationError, - moduleResolverHost: host, - trackReferencedAmbientModule, - trackExternalModuleSymbolOfImportTypeNode, - reportNonlocalAugmentation, - reportNonSerializableProperty - }; - let errorNameNode: ts.DeclarationName | undefined; - let errorFallbackNode: ts.Declaration | undefined; - let currentSourceFile: ts.SourceFile; - let refs: ts.ESMap; - let libs: ts.ESMap; - let emittedImports: readonly ts.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 [ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ][] | undefined): void { - if (!typeReferenceDirectives) { - return; - } - necessaryTypeReferences = necessaryTypeReferences || new ts.Set(); - for (const ref of typeReferenceDirectives) { - necessaryTypeReferences.add(ref); - } +const declarationEmitNodeBuilderFlags = ts.NodeBuilderFlags.MultilineObjectLiterals | + ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral | + ts.NodeBuilderFlags.UseTypeOfFunction | + ts.NodeBuilderFlags.UseStructuralFallback | + ts.NodeBuilderFlags.AllowEmptyTuple | + ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams | + ts.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: ts.TransformationContext) { + const throwDiagnostic = () => ts.Debug.fail("Diagnostic emitted without context"); + let getSymbolAccessibilityDiagnostic: ts.GetSymbolAccessibilityDiagnostic = throwDiagnostic; + let needsDeclare = true; + let isBundledEmit = false; + let resultHasExternalModuleIndicator = false; + let needsScopeFixMarker = false; + let resultHasScopeMarker = false; + let enclosingDeclaration: ts.Node; + let necessaryTypeReferences: ts.Set<[ + specifier: string, + mode: ts.SourceFile["impliedNodeFormat"] | undefined + ]> | undefined; + let lateMarkedStatements: ts.LateVisibilityPaintedStatement[] | undefined; + let lateStatementReplacementMap: ts.ESMap>; + let suppressNewDiagnosticContexts: boolean; + let exportedModulesFromDeclarationEmit: ts.Symbol[] | undefined; + + const { factory } = context; + const host = context.getEmitHost(); + const symbolTracker: ts.SymbolTracker = { + trackSymbol, + reportInaccessibleThisError, + reportInaccessibleUniqueSymbolError, + reportCyclicStructureError, + reportPrivateInBaseOfClassExpression, + reportLikelyUnsafeImportRequiredError, + reportTruncationError, + moduleResolverHost: host, + trackReferencedAmbientModule, + trackExternalModuleSymbolOfImportTypeNode, + reportNonlocalAugmentation, + reportNonSerializableProperty + }; + let errorNameNode: ts.DeclarationName | undefined; + let errorFallbackNode: ts.Declaration | undefined; + let currentSourceFile: ts.SourceFile; + let refs: ts.ESMap; + let libs: ts.ESMap; + let emittedImports: readonly ts.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 [ + specifier: string, + mode: ts.SourceFile["impliedNodeFormat"] | undefined + ][] | undefined): void { + if (!typeReferenceDirectives) { + return; + } + necessaryTypeReferences = necessaryTypeReferences || new ts.Set(); + for (const ref of typeReferenceDirectives) { + necessaryTypeReferences.add(ref); } + } - function trackReferencedAmbientModule(node: ts.ModuleDeclaration, symbol: ts.Symbol) { - // If it is visible via `// `, then we should just use that - const directives = resolver.getTypeReferenceDirectivesForSymbol(symbol, ts.SymbolFlags.All); - if (ts.length(directives)) { - return recordTypeReferenceDirectivesIfNecessary(directives); - } - // Otherwise we should emit a path-based reference - const container = ts.getSourceFileOfNode(node); - refs.set(ts.getOriginalNodeId(container), container); + function trackReferencedAmbientModule(node: ts.ModuleDeclaration, symbol: ts.Symbol) { + // If it is visible via `// `, then we should just use that + const directives = resolver.getTypeReferenceDirectivesForSymbol(symbol, ts.SymbolFlags.All); + if (ts.length(directives)) { + return recordTypeReferenceDirectivesIfNecessary(directives); } + // Otherwise we should emit a path-based reference + const container = ts.getSourceFileOfNode(node); + refs.set(ts.getOriginalNodeId(container), container); + } - function handleSymbolAccessibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { - if (symbolAccessibilityResult.accessibility === ts.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) { - ts.pushIfUnique(lateMarkedStatements, ref); - } - } + function handleSymbolAccessibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { + if (symbolAccessibilityResult.accessibility === ts.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(ts.createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, ts.getTextOfNode(errorInfo.typeName), symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); - } - else { - context.addDiagnostic(ts.createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); + else { + for (const ref of symbolAccessibilityResult.aliasesToMakeVisible) { + ts.pushIfUnique(lateMarkedStatements, ref); } - return true; } } - return false; - } - function trackExternalModuleSymbolOfImportTypeNode(symbol: ts.Symbol) { - if (!isBundledEmit) { - (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + // 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(ts.createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, ts.getTextOfNode(errorInfo.typeName), symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); + } + else { + context.addDiagnostic(ts.createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); + } + return true; } } + return false; + } - function trackSymbol(symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags) { - if (symbol.flags & ts.SymbolFlags.TypeParameter) - return false; - const issuedDiagnostic = handleSymbolAccessibilityError(resolver.isSymbolAccessible(symbol, enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ true)); - recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForSymbol(symbol, meaning)); - return issuedDiagnostic; + function trackExternalModuleSymbolOfImportTypeNode(symbol: ts.Symbol) { + if (!isBundledEmit) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); } + } - function reportPrivateInBaseOfClassExpression(propertyName: string) { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.Property_0_of_exported_class_expression_may_not_be_private_or_protected, propertyName)); - } - } + function trackSymbol(symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags) { + if (symbol.flags & ts.SymbolFlags.TypeParameter) + return false; + const issuedDiagnostic = handleSymbolAccessibilityError(resolver.isSymbolAccessible(symbol, enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ true)); + recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForSymbol(symbol, meaning)); + return issuedDiagnostic; + } - function errorDeclarationNameWithFallback() { - return errorNameNode ? ts.declarationNameToString(errorNameNode) : - errorFallbackNode && ts.getNameOfDeclaration(errorFallbackNode) ? ts.declarationNameToString(ts.getNameOfDeclaration(errorFallbackNode)) : - errorFallbackNode && ts.isExportAssignment(errorFallbackNode) ? errorFallbackNode.isExportEquals ? "export=" : "default" : - "(Missing)"; // same fallback declarationNameToString uses when node is zero-width (ie, nameless) + function reportPrivateInBaseOfClassExpression(propertyName: string) { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.Property_0_of_exported_class_expression_may_not_be_private_or_protected, propertyName)); } + } - function reportInaccessibleUniqueSymbolError() { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), "unique symbol")); - } - } + function errorDeclarationNameWithFallback() { + return errorNameNode ? ts.declarationNameToString(errorNameNode) : + errorFallbackNode && ts.getNameOfDeclaration(errorFallbackNode) ? ts.declarationNameToString(ts.getNameOfDeclaration(errorFallbackNode)) : + errorFallbackNode && ts.isExportAssignment(errorFallbackNode) ? errorFallbackNode.isExportEquals ? "export=" : "default" : + "(Missing)"; // same fallback declarationNameToString uses when node is zero-width (ie, nameless) + } - function reportCyclicStructureError() { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_a_type_with_a_cyclic_structure_which_cannot_be_trivially_serialized_A_type_annotation_is_necessary, errorDeclarationNameWithFallback())); - } + function reportInaccessibleUniqueSymbolError() { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), "unique symbol")); } + } - function reportInaccessibleThisError() { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), "this")); - } + function reportCyclicStructureError() { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_a_type_with_a_cyclic_structure_which_cannot_be_trivially_serialized_A_type_annotation_is_necessary, errorDeclarationNameWithFallback())); } + } - function reportLikelyUnsafeImportRequiredError(specifier: string) { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_cannot_be_named_without_a_reference_to_1_This_is_likely_not_portable_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), specifier)); - } + function reportInaccessibleThisError() { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), "this")); } + } - function reportTruncationError() { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_this_node_exceeds_the_maximum_length_the_compiler_will_serialize_An_explicit_type_annotation_is_needed)); - } + function reportLikelyUnsafeImportRequiredError(specifier: string) { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_0_cannot_be_named_without_a_reference_to_1_This_is_likely_not_portable_A_type_annotation_is_necessary, errorDeclarationNameWithFallback(), specifier)); } + } - function reportNonlocalAugmentation(containingFile: ts.SourceFile, parentSymbol: ts.Symbol, symbol: ts.Symbol) { - const primaryDeclaration = parentSymbol.declarations?.find(d => ts.getSourceFileOfNode(d) === containingFile)!; - const augmentingDeclarations = ts.filter(symbol.declarations, d => ts.getSourceFileOfNode(d) !== containingFile); - if (augmentingDeclarations) { - for (const augmentations of augmentingDeclarations) { - context.addDiagnostic(ts.addRelatedInfo(ts.createDiagnosticForNode(augmentations, ts.Diagnostics.Declaration_augments_declaration_in_another_file_This_cannot_be_serialized), ts.createDiagnosticForNode(primaryDeclaration, ts.Diagnostics.This_is_the_declaration_being_augmented_Consider_moving_the_augmenting_declaration_into_the_same_file))); - } - } + function reportTruncationError() { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_inferred_type_of_this_node_exceeds_the_maximum_length_the_compiler_will_serialize_An_explicit_type_annotation_is_needed)); } + } - function reportNonSerializableProperty(propertyName: string) { - if (errorNameNode || errorFallbackNode) { - context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_type_of_this_node_cannot_be_serialized_because_its_property_0_cannot_be_serialized, propertyName)); + function reportNonlocalAugmentation(containingFile: ts.SourceFile, parentSymbol: ts.Symbol, symbol: ts.Symbol) { + const primaryDeclaration = parentSymbol.declarations?.find(d => ts.getSourceFileOfNode(d) === containingFile)!; + const augmentingDeclarations = ts.filter(symbol.declarations, d => ts.getSourceFileOfNode(d) !== containingFile); + if (augmentingDeclarations) { + for (const augmentations of augmentingDeclarations) { + context.addDiagnostic(ts.addRelatedInfo(ts.createDiagnosticForNode(augmentations, ts.Diagnostics.Declaration_augments_declaration_in_another_file_This_cannot_be_serialized), ts.createDiagnosticForNode(primaryDeclaration, ts.Diagnostics.This_is_the_declaration_being_augmented_Consider_moving_the_augmenting_declaration_into_the_same_file))); } } + } - function transformDeclarationsForJS(sourceFile: ts.SourceFile, bundled?: boolean) { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = (s) => (s.errorNode && ts.canProduceDiagnostics(s.errorNode) ? ts.createGetSymbolAccessibilityDiagnosticForNode(s.errorNode)(s) : ({ - diagnosticMessage: s.errorModuleName - ? ts.Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_from_module_1_An_explicit_type_annotation_may_unblock_declaration_emit - : ts.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 reportNonSerializableProperty(propertyName: string) { + if (errorNameNode || errorFallbackNode) { + context.addDiagnostic(ts.createDiagnosticForNode((errorNameNode || errorFallbackNode)!, ts.Diagnostics.The_type_of_this_node_cannot_be_serialized_because_its_property_0_cannot_be_serialized, propertyName)); } + } - function transformRoot(node: ts.Bundle): ts.Bundle; - function transformRoot(node: ts.SourceFile): ts.SourceFile; - function transformRoot(node: ts.SourceFile | ts.Bundle): ts.SourceFile | ts.Bundle; - function transformRoot(node: ts.SourceFile | ts.Bundle) { - if (node.kind === ts.SyntaxKind.SourceFile && node.isDeclarationFile) { - return node; - } + function transformDeclarationsForJS(sourceFile: ts.SourceFile, bundled?: boolean) { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = (s) => (s.errorNode && ts.canProduceDiagnostics(s.errorNode) ? ts.createGetSymbolAccessibilityDiagnosticForNode(s.errorNode)(s) : ({ + diagnosticMessage: s.errorModuleName + ? ts.Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_from_module_1_An_explicit_type_annotation_may_unblock_declaration_emit + : ts.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; + } - if (node.kind === ts.SyntaxKind.Bundle) { - isBundledEmit = true; - refs = new ts.Map(); - libs = new ts.Map(); - let hasNoDefaultLib = false; - const bundle = factory.createBundle(ts.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 = new ts.Map(); - getSymbolAccessibilityDiagnostic = throwDiagnostic; - needsScopeFixMarker = false; - resultHasScopeMarker = false; - collectReferences(sourceFile, refs); - collectLibs(sourceFile, libs); - if (ts.isExternalOrCommonJsModule(sourceFile) || ts.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 = ts.isSourceFileJS(sourceFile) ? factory.createNodeArray(transformDeclarationsForJS(sourceFile, /*bundled*/ true)) : ts.visitNodes(sourceFile.statements, visitDeclarationStatements); - const newFile = factory.updateSourceFile(sourceFile, [factory.createModuleDeclaration([], [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createStringLiteral(ts.getResolvedExternalModuleName(context.getEmitHost(), sourceFile)), factory.createModuleBlock(ts.setTextRange(factory.createNodeArray(transformAndReplaceLatePaintedStatements(statements)), sourceFile.statements)))], /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); - return newFile; - } - needsDeclare = true; - const updated = ts.isSourceFileJS(sourceFile) ? factory.createNodeArray(transformDeclarationsForJS(sourceFile)) : ts.visitNodes(sourceFile.statements, visitDeclarationStatements); - return factory.updateSourceFile(sourceFile, transformAndReplaceLatePaintedStatements(updated), /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); - }), ts.mapDefined(node.prepends, prepend => { - if (prepend.kind === ts.SyntaxKind.InputFiles) { - const sourceFile = ts.createUnparsedSourceFile(prepend, "dts", stripInternal); - hasNoDefaultLib = hasNoDefaultLib || !!sourceFile.hasNoDefaultLib; - collectReferences(sourceFile, refs); - recordTypeReferenceDirectivesIfNecessary(ts.map(sourceFile.typeReferenceDirectives, ref => [ref.fileName, ref.resolutionMode])); - collectLibs(sourceFile, libs); - return sourceFile; - } - return prepend; - })); - bundle.syntheticFileReferences = []; - bundle.syntheticTypeReferences = getFileReferencesForUsedTypeReferences(); - bundle.syntheticLibReferences = getLibReferences(); - bundle.hasNoDefaultLib = hasNoDefaultLib; - const outputFilePath = ts.getDirectoryPath(ts.normalizeSlashes(ts.getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); - const referenceVisitor = mapReferencesIntoArray(bundle.syntheticFileReferences as ts.FileReference[], outputFilePath); - refs.forEach(referenceVisitor); - return bundle; - } + function transformRoot(node: ts.Bundle): ts.Bundle; + function transformRoot(node: ts.SourceFile): ts.SourceFile; + function transformRoot(node: ts.SourceFile | ts.Bundle): ts.SourceFile | ts.Bundle; + function transformRoot(node: ts.SourceFile | ts.Bundle) { + if (node.kind === ts.SyntaxKind.SourceFile && node.isDeclarationFile) { + return node; + } - // Single source file - needsDeclare = true; - needsScopeFixMarker = false; - resultHasScopeMarker = false; - enclosingDeclaration = node; - currentSourceFile = node; - getSymbolAccessibilityDiagnostic = throwDiagnostic; - isBundledEmit = false; - resultHasExternalModuleIndicator = false; - suppressNewDiagnosticContexts = false; - lateMarkedStatements = undefined; - lateStatementReplacementMap = new ts.Map(); - necessaryTypeReferences = undefined; - refs = collectReferences(currentSourceFile, new ts.Map()); - libs = collectLibs(currentSourceFile, new ts.Map()); - const references: ts.FileReference[] = []; - const outputFilePath = ts.getDirectoryPath(ts.normalizeSlashes(ts.getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); - const referenceVisitor = mapReferencesIntoArray(references, outputFilePath); - let combinedStatements: ts.NodeArray; - if (ts.isSourceFileJS(currentSourceFile)) { - combinedStatements = factory.createNodeArray(transformDeclarationsForJS(node)); - refs.forEach(referenceVisitor); - emittedImports = ts.filter(combinedStatements, ts.isAnyImportSyntax); - } - else { - const statements = ts.visitNodes(node.statements, visitDeclarationStatements); - combinedStatements = ts.setTextRange(factory.createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements); - refs.forEach(referenceVisitor); - emittedImports = ts.filter(combinedStatements, ts.isAnyImportSyntax); - if (ts.isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) { - combinedStatements = ts.setTextRange(factory.createNodeArray([...combinedStatements, ts.createEmptyExports(factory)]), combinedStatements); + if (node.kind === ts.SyntaxKind.Bundle) { + isBundledEmit = true; + refs = new ts.Map(); + libs = new ts.Map(); + let hasNoDefaultLib = false; + const bundle = factory.createBundle(ts.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 = new ts.Map(); + getSymbolAccessibilityDiagnostic = throwDiagnostic; + needsScopeFixMarker = false; + resultHasScopeMarker = false; + collectReferences(sourceFile, refs); + collectLibs(sourceFile, libs); + if (ts.isExternalOrCommonJsModule(sourceFile) || ts.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 = ts.isSourceFileJS(sourceFile) ? factory.createNodeArray(transformDeclarationsForJS(sourceFile, /*bundled*/ true)) : ts.visitNodes(sourceFile.statements, visitDeclarationStatements); + const newFile = factory.updateSourceFile(sourceFile, [factory.createModuleDeclaration([], [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createStringLiteral(ts.getResolvedExternalModuleName(context.getEmitHost(), sourceFile)), factory.createModuleBlock(ts.setTextRange(factory.createNodeArray(transformAndReplaceLatePaintedStatements(statements)), sourceFile.statements)))], /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); + return newFile; + } + needsDeclare = true; + const updated = ts.isSourceFileJS(sourceFile) ? factory.createNodeArray(transformDeclarationsForJS(sourceFile)) : ts.visitNodes(sourceFile.statements, visitDeclarationStatements); + return factory.updateSourceFile(sourceFile, transformAndReplaceLatePaintedStatements(updated), /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); + }), ts.mapDefined(node.prepends, prepend => { + if (prepend.kind === ts.SyntaxKind.InputFiles) { + const sourceFile = ts.createUnparsedSourceFile(prepend, "dts", stripInternal); + hasNoDefaultLib = hasNoDefaultLib || !!sourceFile.hasNoDefaultLib; + collectReferences(sourceFile, refs); + recordTypeReferenceDirectivesIfNecessary(ts.map(sourceFile.typeReferenceDirectives, ref => [ref.fileName, ref.resolutionMode])); + collectLibs(sourceFile, libs); + return sourceFile; } - } - const updated = factory.updateSourceFile(node, combinedStatements, /*isDeclarationFile*/ true, references, getFileReferencesForUsedTypeReferences(), node.hasNoDefaultLib, getLibReferences()); - updated.exportedModulesFromDeclarationEmit = exportedModulesFromDeclarationEmit; - return updated; + return prepend; + })); + bundle.syntheticFileReferences = []; + bundle.syntheticTypeReferences = getFileReferencesForUsedTypeReferences(); + bundle.syntheticLibReferences = getLibReferences(); + bundle.hasNoDefaultLib = hasNoDefaultLib; + const outputFilePath = ts.getDirectoryPath(ts.normalizeSlashes(ts.getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); + const referenceVisitor = mapReferencesIntoArray(bundle.syntheticFileReferences as ts.FileReference[], outputFilePath); + refs.forEach(referenceVisitor); + return bundle; + } - function getLibReferences() { - return ts.map(ts.arrayFrom(libs.keys()), lib => ({ fileName: lib, pos: -1, end: -1 })); + // Single source file + needsDeclare = true; + needsScopeFixMarker = false; + resultHasScopeMarker = false; + enclosingDeclaration = node; + currentSourceFile = node; + getSymbolAccessibilityDiagnostic = throwDiagnostic; + isBundledEmit = false; + resultHasExternalModuleIndicator = false; + suppressNewDiagnosticContexts = false; + lateMarkedStatements = undefined; + lateStatementReplacementMap = new ts.Map(); + necessaryTypeReferences = undefined; + refs = collectReferences(currentSourceFile, new ts.Map()); + libs = collectLibs(currentSourceFile, new ts.Map()); + const references: ts.FileReference[] = []; + const outputFilePath = ts.getDirectoryPath(ts.normalizeSlashes(ts.getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); + const referenceVisitor = mapReferencesIntoArray(references, outputFilePath); + let combinedStatements: ts.NodeArray; + if (ts.isSourceFileJS(currentSourceFile)) { + combinedStatements = factory.createNodeArray(transformDeclarationsForJS(node)); + refs.forEach(referenceVisitor); + emittedImports = ts.filter(combinedStatements, ts.isAnyImportSyntax); + } + else { + const statements = ts.visitNodes(node.statements, visitDeclarationStatements); + combinedStatements = ts.setTextRange(factory.createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements); + refs.forEach(referenceVisitor); + emittedImports = ts.filter(combinedStatements, ts.isAnyImportSyntax); + if (ts.isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) { + combinedStatements = ts.setTextRange(factory.createNodeArray([...combinedStatements, ts.createEmptyExports(factory)]), combinedStatements); } + } + const updated = factory.updateSourceFile(node, combinedStatements, /*isDeclarationFile*/ true, references, getFileReferencesForUsedTypeReferences(), node.hasNoDefaultLib, getLibReferences()); + updated.exportedModulesFromDeclarationEmit = exportedModulesFromDeclarationEmit; + return updated; - function getFileReferencesForUsedTypeReferences() { - return necessaryTypeReferences ? ts.mapDefined(ts.arrayFrom(necessaryTypeReferences.keys()), getFileReferenceForSpecifierModeTuple) : []; - } + function getLibReferences() { + return ts.map(ts.arrayFrom(libs.keys()), lib => ({ fileName: lib, pos: -1, end: -1 })); + } - function getFileReferenceForSpecifierModeTuple([typeName, mode]: [ - specifier: string, - mode: ts.SourceFile["impliedNodeFormat"] | undefined - ]): ts.FileReference | undefined { - // Elide type references for which we have imports - if (emittedImports) { - for (const importStatement of emittedImports) { - if (ts.isImportEqualsDeclaration(importStatement) && ts.isExternalModuleReference(importStatement.moduleReference)) { - const expr = importStatement.moduleReference.expression; - if (ts.isStringLiteralLike(expr) && expr.text === typeName) { - return undefined; - } - } - else if (ts.isImportDeclaration(importStatement) && ts.isStringLiteral(importStatement.moduleSpecifier) && importStatement.moduleSpecifier.text === typeName) { + function getFileReferencesForUsedTypeReferences() { + return necessaryTypeReferences ? ts.mapDefined(ts.arrayFrom(necessaryTypeReferences.keys()), getFileReferenceForSpecifierModeTuple) : []; + } + + function getFileReferenceForSpecifierModeTuple([typeName, mode]: [ + specifier: string, + mode: ts.SourceFile["impliedNodeFormat"] | undefined + ]): ts.FileReference | undefined { + // Elide type references for which we have imports + if (emittedImports) { + for (const importStatement of emittedImports) { + if (ts.isImportEqualsDeclaration(importStatement) && ts.isExternalModuleReference(importStatement.moduleReference)) { + const expr = importStatement.moduleReference.expression; + if (ts.isStringLiteralLike(expr) && expr.text === typeName) { return undefined; } } + else if (ts.isImportDeclaration(importStatement) && ts.isStringLiteral(importStatement.moduleSpecifier) && importStatement.moduleSpecifier.text === typeName) { + return undefined; + } } - return { fileName: typeName, pos: -1, end: -1, ...(mode ? { resolutionMode: mode } : undefined) }; } + return { fileName: typeName, pos: -1, end: -1, ...(mode ? { resolutionMode: mode } : undefined) }; + } - function mapReferencesIntoArray(references: ts.FileReference[], outputFilePath: string): (file: ts.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 && ts.contains((node as ts.Bundle).sourceFiles, file)) - return; // Omit references to files which are being merged - const paths = ts.getOutputPathsFor(file, host, /*forceDtsPaths*/ true); - declFileName = paths.declarationFilePath || paths.jsFilePath || file.fileName; - } - - if (declFileName) { - const specifier = ts.moduleSpecifiers.getModuleSpecifier(options, currentSourceFile, ts.toPath(outputFilePath, host.getCurrentDirectory(), host.getCanonicalFileName), ts.toPath(declFileName, host.getCurrentDirectory(), host.getCanonicalFileName), host); - if (!ts.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, /*mode*/ undefined]]); - return; - } + function mapReferencesIntoArray(references: ts.FileReference[], outputFilePath: string): (file: ts.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 && ts.contains((node as ts.Bundle).sourceFiles, file)) + return; // Omit references to files which are being merged + const paths = ts.getOutputPathsFor(file, host, /*forceDtsPaths*/ true); + declFileName = paths.declarationFilePath || paths.jsFilePath || file.fileName; + } - let fileName = ts.getRelativePathToDirectoryOrUrl(outputFilePath, declFileName, host.getCurrentDirectory(), host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ false); - if (ts.startsWith(fileName, "./") && ts.hasExtension(fileName)) { - fileName = fileName.substring(2); - } + if (declFileName) { + const specifier = ts.moduleSpecifiers.getModuleSpecifier(options, currentSourceFile, ts.toPath(outputFilePath, host.getCurrentDirectory(), host.getCanonicalFileName), ts.toPath(declFileName, host.getCurrentDirectory(), host.getCanonicalFileName), host); + if (!ts.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, /*mode*/ undefined]]); + return; + } - // omit references to files from node_modules (npm may disambiguate module - // references when installing this package, making the path is unreliable). - if (ts.startsWith(fileName, "node_modules/") || ts.pathContainsNodeModules(fileName)) { - return; - } + let fileName = ts.getRelativePathToDirectoryOrUrl(outputFilePath, declFileName, host.getCurrentDirectory(), host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ false); + if (ts.startsWith(fileName, "./") && ts.hasExtension(fileName)) { + fileName = fileName.substring(2); + } - 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 (ts.startsWith(fileName, "node_modules/") || ts.pathContainsNodeModules(fileName)) { + return; } - }; - } - } - function collectReferences(sourceFile: ts.SourceFile | ts.UnparsedSource, ret: ts.ESMap) { - if (noResolve || (!ts.isUnparsedSource(sourceFile) && ts.isSourceFileJS(sourceFile))) - return ret; - ts.forEach(sourceFile.referencedFiles, f => { - const elem = host.getSourceFileFromReference(sourceFile, f); - if (elem) { - ret.set(ts.getOriginalNodeId(elem), elem); + references.push({ pos: -1, end: -1, fileName }); } - }); - return ret; + }; } + } - function collectLibs(sourceFile: ts.SourceFile | ts.UnparsedSource, ret: ts.ESMap) { - ts.forEach(sourceFile.libReferenceDirectives, ref => { - const lib = host.getLibFileFromReference(ref); - if (lib) { - ret.set(ts.toFileNameLowerCase(ref.fileName), true); - } - }); + function collectReferences(sourceFile: ts.SourceFile | ts.UnparsedSource, ret: ts.ESMap) { + if (noResolve || (!ts.isUnparsedSource(sourceFile) && ts.isSourceFileJS(sourceFile))) return ret; - } + ts.forEach(sourceFile.referencedFiles, f => { + const elem = host.getSourceFileFromReference(sourceFile, f); + if (elem) { + ret.set(ts.getOriginalNodeId(elem), elem); + } + }); + return ret; + } + + function collectLibs(sourceFile: ts.SourceFile | ts.UnparsedSource, ret: ts.ESMap) { + ts.forEach(sourceFile.libReferenceDirectives, ref => { + const lib = host.getLibFileFromReference(ref); + if (lib) { + ret.set(ts.toFileNameLowerCase(ref.fileName), true); + } + }); + return ret; + } - function filterBindingPatternInitializers(name: ts.BindingName) { - if (name.kind === ts.SyntaxKind.Identifier) { - return name; + function filterBindingPatternInitializers(name: ts.BindingName) { + if (name.kind === ts.SyntaxKind.Identifier) { + return name; + } + else { + if (name.kind === ts.SyntaxKind.ArrayBindingPattern) { + return factory.updateArrayBindingPattern(name, ts.visitNodes(name.elements, visitBindingElement)); } else { - if (name.kind === ts.SyntaxKind.ArrayBindingPattern) { - return factory.updateArrayBindingPattern(name, ts.visitNodes(name.elements, visitBindingElement)); - } - else { - return factory.updateObjectBindingPattern(name, ts.visitNodes(name.elements, visitBindingElement)); - } + return factory.updateObjectBindingPattern(name, ts.visitNodes(name.elements, visitBindingElement)); } + } - function visitBindingElement(elem: T): T; - function visitBindingElement(elem: ts.ArrayBindingElement): ts.ArrayBindingElement { - if (elem.kind === ts.SyntaxKind.OmittedExpression) { - return elem; - } - return factory.updateBindingElement(elem, elem.dotDotDotToken, elem.propertyName, filterBindingPatternInitializers(elem.name), shouldPrintWithInitializer(elem) ? elem.initializer : undefined); + function visitBindingElement(elem: T): T; + function visitBindingElement(elem: ts.ArrayBindingElement): ts.ArrayBindingElement { + if (elem.kind === ts.SyntaxKind.OmittedExpression) { + return elem; } + return factory.updateBindingElement(elem, elem.dotDotDotToken, elem.propertyName, filterBindingPatternInitializers(elem.name), shouldPrintWithInitializer(elem) ? elem.initializer : undefined); } + } - function ensureParameter(p: ts.ParameterDeclaration, modifierMask?: ts.ModifierFlags, type?: ts.TypeNode): ts.ParameterDeclaration { - let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; - if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(p); - } - const newParam = factory.updateParameterDeclaration(p, - /*decorators*/ undefined, maskModifiers(p, modifierMask), p.dotDotDotToken, filterBindingPatternInitializers(p.name), resolver.isOptionalParameter(p) ? (p.questionToken || factory.createToken(ts.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 ensureParameter(p: ts.ParameterDeclaration, modifierMask?: ts.ModifierFlags, type?: ts.TypeNode): ts.ParameterDeclaration { + let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(p); + } + const newParam = factory.updateParameterDeclaration(p, + /*decorators*/ undefined, maskModifiers(p, modifierMask), p.dotDotDotToken, filterBindingPatternInitializers(p.name), resolver.isOptionalParameter(p) ? (p.questionToken || factory.createToken(ts.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: ts.Node) { + return canHaveLiteralInitializer(node) && resolver.isLiteralConstDeclaration(ts.getParseTreeNode(node) as CanHaveLiteralInitializer); // TODO: Make safe + } - function shouldPrintWithInitializer(node: ts.Node) { - return canHaveLiteralInitializer(node) && resolver.isLiteralConstDeclaration(ts.getParseTreeNode(node) as CanHaveLiteralInitializer); // TODO: Make safe + function ensureNoInitializer(node: CanHaveLiteralInitializer) { + if (shouldPrintWithInitializer(node)) { + return resolver.createLiteralConstValue(ts.getParseTreeNode(node) as CanHaveLiteralInitializer, symbolTracker); // TODO: Make safe } + return undefined; + } - function ensureNoInitializer(node: CanHaveLiteralInitializer) { - if (shouldPrintWithInitializer(node)) { - return resolver.createLiteralConstValue(ts.getParseTreeNode(node) as CanHaveLiteralInitializer, symbolTracker); // TODO: Make safe - } - return undefined; + type HasInferredType = ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.BindingElement | ts.ConstructSignatureDeclaration | ts.VariableDeclaration | ts.MethodSignature | ts.CallSignatureDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature; + function ensureType(node: HasInferredType, type: ts.TypeNode | undefined, ignorePrivate?: boolean): ts.TypeNode | undefined { + if (!ignorePrivate && ts.hasEffectiveModifier(node, ts.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 === ts.SyntaxKind.Parameter && + (resolver.isRequiredInitializedParameter(node) || + resolver.isOptionalUninitializedParameterProperty(node)); + if (type && !shouldUseResolverType) { + return ts.visitNode(type, visitDeclarationSubtree); + } + if (!ts.getParseTreeNode(node)) { + return type ? ts.visitNode(type, visitDeclarationSubtree) : factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } + if (node.kind === ts.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 factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } + errorNameNode = node.name; + let oldDiag: typeof getSymbolAccessibilityDiagnostic; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(node); + } + if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { + return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + } + if (node.kind === ts.SyntaxKind.Parameter + || node.kind === ts.SyntaxKind.PropertyDeclaration + || node.kind === ts.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)); - type HasInferredType = ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.BindingElement | ts.ConstructSignatureDeclaration | ts.VariableDeclaration | ts.MethodSignature | ts.CallSignatureDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature; - function ensureType(node: HasInferredType, type: ts.TypeNode | undefined, ignorePrivate?: boolean): ts.TypeNode | undefined { - if (!ignorePrivate && ts.hasEffectiveModifier(node, ts.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 === ts.SyntaxKind.Parameter && - (resolver.isRequiredInitializedParameter(node) || - resolver.isOptionalUninitializedParameterProperty(node)); - if (type && !shouldUseResolverType) { - return ts.visitNode(type, visitDeclarationSubtree); - } - if (!ts.getParseTreeNode(node)) { - return type ? ts.visitNode(type, visitDeclarationSubtree) : factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } - if (node.kind === ts.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 factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } - errorNameNode = node.name; - let oldDiag: typeof getSymbolAccessibilityDiagnostic; + function cleanup(returnValue: ts.TypeNode | undefined) { + errorNameNode = undefined; if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(node); - } - if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { - return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); - } - if (node.kind === ts.SyntaxKind.Parameter - || node.kind === ts.SyntaxKind.PropertyDeclaration - || node.kind === ts.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)); + getSymbolAccessibilityDiagnostic = oldDiag; } - return cleanup(resolver.createReturnTypeOfSignatureDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + return returnValue || factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + } + } - function cleanup(returnValue: ts.TypeNode | undefined) { - errorNameNode = undefined; - if (!suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag; - } - return returnValue || factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - } + function isDeclarationAndNotVisible(node: ts.NamedDeclaration) { + node = ts.getParseTreeNode(node) as ts.NamedDeclaration; + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.EnumDeclaration: + return !resolver.isDeclarationVisible(node); + // The following should be doing their own visibility checks based on filtering their members + case ts.SyntaxKind.VariableDeclaration: + return !getBindingNameVisible(node as ts.VariableDeclaration); + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.ExportAssignment: + return false; + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return true; } + return false; + } - function isDeclarationAndNotVisible(node: ts.NamedDeclaration) { - node = ts.getParseTreeNode(node) as ts.NamedDeclaration; - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.EnumDeclaration: - return !resolver.isDeclarationVisible(node); - // The following should be doing their own visibility checks based on filtering their members - case ts.SyntaxKind.VariableDeclaration: - return !getBindingNameVisible(node as ts.VariableDeclaration); - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ExportAssignment: - return false; - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return true; - } - return false; + // If the ExpandoFunctionDeclaration have multiple overloads, then we only need to emit properties for the last one. + function shouldEmitFunctionProperties(input: ts.FunctionDeclaration) { + if (input.body) { + return true; } - // If the ExpandoFunctionDeclaration have multiple overloads, then we only need to emit properties for the last one. - function shouldEmitFunctionProperties(input: ts.FunctionDeclaration) { - if (input.body) { - return true; - } + const overloadSignatures = input.symbol.declarations?.filter(decl => ts.isFunctionDeclaration(decl) && !decl.body); + return !overloadSignatures || overloadSignatures.indexOf(input) === overloadSignatures.length - 1; + } - const overloadSignatures = input.symbol.declarations?.filter(decl => ts.isFunctionDeclaration(decl) && !decl.body); - return !overloadSignatures || overloadSignatures.indexOf(input) === overloadSignatures.length - 1; + function getBindingNameVisible(elem: ts.BindingElement | ts.VariableDeclaration | ts.OmittedExpression): boolean { + if (ts.isOmittedExpression(elem)) { + return false; + } + if (ts.isBindingPattern(elem.name)) { + // If any child binding pattern element has been marked visible (usually by collect linked aliases), then this is visible + return ts.some(elem.name.elements, getBindingNameVisible); } + else { + return resolver.isDeclarationVisible(elem); + } + } - function getBindingNameVisible(elem: ts.BindingElement | ts.VariableDeclaration | ts.OmittedExpression): boolean { - if (ts.isOmittedExpression(elem)) { - return false; - } - if (ts.isBindingPattern(elem.name)) { - // If any child binding pattern element has been marked visible (usually by collect linked aliases), then this is visible - return ts.some(elem.name.elements, getBindingNameVisible); - } - else { - return resolver.isDeclarationVisible(elem); - } + function updateParamsList(node: ts.Node, params: ts.NodeArray, modifierMask?: ts.ModifierFlags) { + if (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private)) { + return undefined!; // TODO: GH#18217 + } + const newParams = ts.map(params, p => ensureParameter(p, modifierMask)); + if (!newParams) { + return undefined!; // TODO: GH#18217 } + return factory.createNodeArray(newParams, params.hasTrailingComma); + } - function updateParamsList(node: ts.Node, params: ts.NodeArray, modifierMask?: ts.ModifierFlags) { - if (ts.hasEffectiveModifier(node, ts.ModifierFlags.Private)) { - return undefined!; // TODO: GH#18217 - } - const newParams = ts.map(params, p => ensureParameter(p, modifierMask)); - if (!newParams) { - return undefined!; // TODO: GH#18217 + function updateAccessorParamsList(input: ts.AccessorDeclaration, isPrivate: boolean) { + let newParams: ts.ParameterDeclaration[] | undefined; + if (!isPrivate) { + const thisParameter = ts.getThisParameter(input); + if (thisParameter) { + newParams = [ensureParameter(thisParameter)]; } - return factory.createNodeArray(newParams, params.hasTrailingComma); } - - function updateAccessorParamsList(input: ts.AccessorDeclaration, isPrivate: boolean) { - let newParams: ts.ParameterDeclaration[] | undefined; + if (ts.isSetAccessorDeclaration(input)) { + let newValueParameter: ts.ParameterDeclaration | undefined; if (!isPrivate) { - const thisParameter = ts.getThisParameter(input); - if (thisParameter) { - newParams = [ensureParameter(thisParameter)]; - } - } - if (ts.isSetAccessorDeclaration(input)) { - let newValueParameter: ts.ParameterDeclaration | undefined; - if (!isPrivate) { - const valueParameter = ts.getSetAccessorValueParameter(input); - if (valueParameter) { - const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); - newValueParameter = ensureParameter(valueParameter, /*modifierMask*/ undefined, accessorType); - } + const valueParameter = ts.getSetAccessorValueParameter(input); + if (valueParameter) { + const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); + newValueParameter = ensureParameter(valueParameter, /*modifierMask*/ undefined, accessorType); } - if (!newValueParameter) { - newValueParameter = factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, "value"); - } - newParams = ts.append(newParams, newValueParameter); - } - return factory.createNodeArray(newParams || ts.emptyArray); - } - function ensureTypeParams(node: ts.Node, params: ts.NodeArray | undefined) { - return ts.hasEffectiveModifier(node, ts.ModifierFlags.Private) ? undefined : ts.visitNodes(params, visitDeclarationSubtree); - } - function isEnclosingDeclaration(node: ts.Node) { - return ts.isSourceFile(node) - || ts.isTypeAliasDeclaration(node) - || ts.isModuleDeclaration(node) - || ts.isClassDeclaration(node) - || ts.isInterfaceDeclaration(node) - || ts.isFunctionLike(node) - || ts.isIndexSignatureDeclaration(node) - || ts.isMappedTypeNode(node); - } - function checkEntityNameVisibility(entityName: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node) { - const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration); - handleSymbolAccessibilityError(visibilityResult); - recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForEntityName(entityName)); - } - - function preserveJsDoc(updated: T, original: ts.Node): T { - if (ts.hasJSDocNodes(updated) && ts.hasJSDocNodes(original)) { - updated.jsDoc = original.jsDoc; } - return ts.setCommentRange(updated, ts.getCommentRange(original)); - } - - function rewriteModuleSpecifier(parent: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode, input: T | undefined): T | ts.StringLiteral { - if (!input) - return undefined!; // TODO: GH#18217 - resultHasExternalModuleIndicator = resultHasExternalModuleIndicator || (parent.kind !== ts.SyntaxKind.ModuleDeclaration && parent.kind !== ts.SyntaxKind.ImportType); - if (ts.isStringLiteralLike(input)) { - if (isBundledEmit) { - const newName = ts.getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); - if (newName) { - return factory.createStringLiteral(newName); - } - } - else { - const symbol = resolver.getSymbolOfExternalModuleSpecifier(input); - if (symbol) { - (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); - } - } + if (!newValueParameter) { + newValueParameter = factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, "value"); } - return input; + newParams = ts.append(newParams, newValueParameter); + } + return factory.createNodeArray(newParams || ts.emptyArray); + } + function ensureTypeParams(node: ts.Node, params: ts.NodeArray | undefined) { + return ts.hasEffectiveModifier(node, ts.ModifierFlags.Private) ? undefined : ts.visitNodes(params, visitDeclarationSubtree); + } + function isEnclosingDeclaration(node: ts.Node) { + return ts.isSourceFile(node) + || ts.isTypeAliasDeclaration(node) + || ts.isModuleDeclaration(node) + || ts.isClassDeclaration(node) + || ts.isInterfaceDeclaration(node) + || ts.isFunctionLike(node) + || ts.isIndexSignatureDeclaration(node) + || ts.isMappedTypeNode(node); + } + function checkEntityNameVisibility(entityName: ts.EntityNameOrEntityNameExpression, enclosingDeclaration: ts.Node) { + const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration); + handleSymbolAccessibilityError(visibilityResult); + recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForEntityName(entityName)); + } + + function preserveJsDoc(updated: T, original: ts.Node): T { + if (ts.hasJSDocNodes(updated) && ts.hasJSDocNodes(original)) { + updated.jsDoc = original.jsDoc; } + return ts.setCommentRange(updated, ts.getCommentRange(original)); + } - function transformImportEqualsDeclaration(decl: ts.ImportEqualsDeclaration) { - if (!resolver.isDeclarationVisible(decl)) - return; - if (decl.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - // Rewrite external module names if necessary - const specifier = ts.getExternalModuleImportEqualsDeclarationExpression(decl); - return factory.updateImportEqualsDeclaration(decl, - /*decorators*/ undefined, decl.modifiers, decl.isTypeOnly, decl.name, factory.updateExternalModuleReference(decl.moduleReference, rewriteModuleSpecifier(decl, specifier))); + function rewriteModuleSpecifier(parent: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode, input: T | undefined): T | ts.StringLiteral { + if (!input) + return undefined!; // TODO: GH#18217 + resultHasExternalModuleIndicator = resultHasExternalModuleIndicator || (parent.kind !== ts.SyntaxKind.ModuleDeclaration && parent.kind !== ts.SyntaxKind.ImportType); + if (ts.isStringLiteralLike(input)) { + if (isBundledEmit) { + const newName = ts.getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); + if (newName) { + return factory.createStringLiteral(newName); + } } else { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(decl); - checkEntityNameVisibility(decl.moduleReference, enclosingDeclaration); - getSymbolAccessibilityDiagnostic = oldDiag; - return decl; + const symbol = resolver.getSymbolOfExternalModuleSpecifier(input); + if (symbol) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + } } } + return input; + } - function transformImportDeclaration(decl: ts.ImportDeclaration) { - if (!decl.importClause) { - // import "mod" - possibly needed for side effects? (global interface patches, module augmentations, etc) - return factory.updateImportDeclaration(decl, - /*decorators*/ undefined, decl.modifiers, decl.importClause, rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); - } - // 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 && factory.updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, - /*namedBindings*/ undefined), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); - } - if (decl.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { - // Namespace import (optionally with visible default) - const namedBindings = resolver.isDeclarationVisible(decl.importClause.namedBindings) ? decl.importClause.namedBindings : /*namedBindings*/ undefined; - return visibleDefaultBinding || namedBindings ? factory.updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, namedBindings), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)) : undefined; - } - // Named imports (optionally with visible default) - const bindingList = ts.mapDefined(decl.importClause.namedBindings.elements, b => resolver.isDeclarationVisible(b) ? b : undefined); - if ((bindingList && bindingList.length) || visibleDefaultBinding) { - return factory.updateImportDeclaration(decl, - /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, bindingList && bindingList.length ? factory.updateNamedImports(decl.importClause.namedBindings, bindingList) : undefined), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); - } - // Augmentation of export depends on import - if (resolver.isImportRequiredByAugmentation(decl)) { - return factory.updateImportDeclaration(decl, - /*decorators*/ undefined, decl.modifiers, - /*importClause*/ undefined, rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); - } - // Nothing visible + function transformImportEqualsDeclaration(decl: ts.ImportEqualsDeclaration) { + if (!resolver.isDeclarationVisible(decl)) + return; + if (decl.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { + // Rewrite external module names if necessary + const specifier = ts.getExternalModuleImportEqualsDeclarationExpression(decl); + return factory.updateImportEqualsDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, decl.isTypeOnly, decl.name, factory.updateExternalModuleReference(decl.moduleReference, rewriteModuleSpecifier(decl, specifier))); } + else { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(decl); + checkEntityNameVisibility(decl.moduleReference, enclosingDeclaration); + getSymbolAccessibilityDiagnostic = oldDiag; + return decl; + } + } - function getResolutionModeOverrideForClauseInNightly(assertClause: ts.AssertClause | undefined) { - const mode = ts.getResolutionModeOverrideForClause(assertClause); - if (mode !== undefined) { - if (!ts.isNightly()) { - context.addDiagnostic(ts.createDiagnosticForNode(assertClause!, ts.Diagnostics.Resolution_mode_assertions_are_unstable_Use_nightly_TypeScript_to_silence_this_error_Try_updating_with_npm_install_D_typescript_next)); - } - return assertClause; - } - return undefined; - } - - function transformAndReplaceLatePaintedStatements(statements: ts.NodeArray): ts.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 (ts.length(lateMarkedStatements)) { - const i = lateMarkedStatements!.shift()!; - if (!ts.isLateVisibilityPaintedStatement(i)) { - return ts.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 && ts.isSourceFile(i.parent) && !(ts.isExternalModule(i.parent) && isBundledEmit); - const result = transformTopLevelDeclaration(i); - needsDeclare = priorNeedsDeclare; - lateStatementReplacementMap.set(ts.getOriginalNodeId(i), result); + function transformImportDeclaration(decl: ts.ImportDeclaration) { + if (!decl.importClause) { + // import "mod" - possibly needed for side effects? (global interface patches, module augmentations, etc) + return factory.updateImportDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, decl.importClause, rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); + } + // 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 && factory.updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, + /*namedBindings*/ undefined), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); + } + if (decl.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { + // Namespace import (optionally with visible default) + const namedBindings = resolver.isDeclarationVisible(decl.importClause.namedBindings) ? decl.importClause.namedBindings : /*namedBindings*/ undefined; + return visibleDefaultBinding || namedBindings ? factory.updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, namedBindings), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)) : undefined; + } + // Named imports (optionally with visible default) + const bindingList = ts.mapDefined(decl.importClause.namedBindings.elements, b => resolver.isDeclarationVisible(b) ? b : undefined); + if ((bindingList && bindingList.length) || visibleDefaultBinding) { + return factory.updateImportDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, factory.updateImportClause(decl.importClause, decl.importClause.isTypeOnly, visibleDefaultBinding, bindingList && bindingList.length ? factory.updateNamedImports(decl.importClause.namedBindings, bindingList) : undefined), rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); + } + // Augmentation of export depends on import + if (resolver.isImportRequiredByAugmentation(decl)) { + return factory.updateImportDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, + /*importClause*/ undefined, rewriteModuleSpecifier(decl, decl.moduleSpecifier), getResolutionModeOverrideForClauseInNightly(decl.assertClause)); + } + // Nothing visible + } + + function getResolutionModeOverrideForClauseInNightly(assertClause: ts.AssertClause | undefined) { + const mode = ts.getResolutionModeOverrideForClause(assertClause); + if (mode !== undefined) { + if (!ts.isNightly()) { + context.addDiagnostic(ts.createDiagnosticForNode(assertClause!, ts.Diagnostics.Resolution_mode_assertions_are_unstable_Use_nightly_TypeScript_to_silence_this_error_Try_updating_with_npm_install_D_typescript_next)); } + return assertClause; + } + return undefined; + } - // 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 ts.visitNodes(statements, visitLateVisibilityMarkedStatements); - function visitLateVisibilityMarkedStatements(statement: ts.Statement) { - if (ts.isLateVisibilityPaintedStatement(statement)) { - const key = ts.getOriginalNodeId(statement); - if (lateStatementReplacementMap.has(key)) { - const result = lateStatementReplacementMap.get(key); - lateStatementReplacementMap.delete(key); - if (result) { - if (ts.isArray(result) ? ts.some(result, ts.needsScopeMarker) : ts.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 (ts.isSourceFile(statement.parent) && (ts.isArray(result) ? ts.some(result, ts.isExternalModuleIndicator) : ts.isExternalModuleIndicator(result))) { - resultHasExternalModuleIndicator = true; - } + function transformAndReplaceLatePaintedStatements(statements: ts.NodeArray): ts.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 (ts.length(lateMarkedStatements)) { + const i = lateMarkedStatements!.shift()!; + if (!ts.isLateVisibilityPaintedStatement(i)) { + return ts.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 && ts.isSourceFile(i.parent) && !(ts.isExternalModule(i.parent) && isBundledEmit); + const result = transformTopLevelDeclaration(i); + needsDeclare = priorNeedsDeclare; + lateStatementReplacementMap.set(ts.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 ts.visitNodes(statements, visitLateVisibilityMarkedStatements); + function visitLateVisibilityMarkedStatements(statement: ts.Statement) { + if (ts.isLateVisibilityPaintedStatement(statement)) { + const key = ts.getOriginalNodeId(statement); + if (lateStatementReplacementMap.has(key)) { + const result = lateStatementReplacementMap.get(key); + lateStatementReplacementMap.delete(key); + if (result) { + if (ts.isArray(result) ? ts.some(result, ts.needsScopeMarker) : ts.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 (ts.isSourceFile(statement.parent) && (ts.isArray(result) ? ts.some(result, ts.isExternalModuleIndicator) : ts.isExternalModuleIndicator(result))) { + resultHasExternalModuleIndicator = true; } - return result; } + return result; } - return statement; } + return statement; } + } - function visitDeclarationSubtree(input: ts.Node): ts.VisitResult { - if (shouldStripInternal(input)) + function visitDeclarationSubtree(input: ts.Node): ts.VisitResult { + if (shouldStripInternal(input)) + return; + if (ts.isDeclaration(input)) { + if (isDeclarationAndNotVisible(input)) + return; + if (ts.hasDynamicName(input) && !resolver.isLateBound(ts.getParseTreeNode(input) as ts.Declaration)) { return; - if (ts.isDeclaration(input)) { - if (isDeclarationAndNotVisible(input)) - return; - if (ts.hasDynamicName(input) && !resolver.isLateBound(ts.getParseTreeNode(input) as ts.Declaration)) { - return; - } } + } - // Elide implementation signatures from overload sets - if (ts.isFunctionLike(input) && resolver.isImplementationOfOverload(input)) - return; + // Elide implementation signatures from overload sets + if (ts.isFunctionLike(input) && resolver.isImplementationOfOverload(input)) + return; - // Elide semicolon class statements - if (ts.isSemicolonClassElement(input)) - return; + // Elide semicolon class statements + if (ts.isSemicolonClassElement(input)) + return; - let previousEnclosingDeclaration: typeof enclosingDeclaration; - if (isEnclosingDeclaration(input)) { - previousEnclosingDeclaration = enclosingDeclaration; - enclosingDeclaration = input as ts.Declaration; - } - const oldDiag = getSymbolAccessibilityDiagnostic; + let previousEnclosingDeclaration: typeof enclosingDeclaration; + if (isEnclosingDeclaration(input)) { + previousEnclosingDeclaration = enclosingDeclaration; + enclosingDeclaration = input as ts.Declaration; + } + const oldDiag = getSymbolAccessibilityDiagnostic; - // Setup diagnostic-related flags before first potential `cleanup` call, otherwise - // We'd see a TDZ violation at runtime - const canProduceDiagnostic = ts.canProduceDiagnostics(input); - const oldWithinObjectLiteralType = suppressNewDiagnosticContexts; - let shouldEnterSuppressNewDiagnosticsContextContext = ((input.kind === ts.SyntaxKind.TypeLiteral || input.kind === ts.SyntaxKind.MappedType) && input.parent.kind !== ts.SyntaxKind.TypeAliasDeclaration); - - // Emit methods which are private as properties with no type information - if (ts.isMethodDeclaration(input) || ts.isMethodSignature(input)) { - if (ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)) { - if (input.symbol && input.symbol.declarations && input.symbol.declarations[0] !== input) - return; // Elide all but the first overload - return cleanup(factory.createPropertyDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); - } - } + // Setup diagnostic-related flags before first potential `cleanup` call, otherwise + // We'd see a TDZ violation at runtime + const canProduceDiagnostic = ts.canProduceDiagnostics(input); + const oldWithinObjectLiteralType = suppressNewDiagnosticContexts; + let shouldEnterSuppressNewDiagnosticsContextContext = ((input.kind === ts.SyntaxKind.TypeLiteral || input.kind === ts.SyntaxKind.MappedType) && input.parent.kind !== ts.SyntaxKind.TypeAliasDeclaration); - if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(input); + // Emit methods which are private as properties with no type information + if (ts.isMethodDeclaration(input) || ts.isMethodSignature(input)) { + if (ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)) { + if (input.symbol && input.symbol.declarations && input.symbol.declarations[0] !== input) + return; // Elide all but the first overload + return cleanup(factory.createPropertyDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); } + } - if (ts.isTypeQueryNode(input)) { - checkEntityNameVisibility(input.exprName, enclosingDeclaration); - } + if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(input); + } - 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 (ts.isTypeQueryNode(input)) { + checkEntityNameVisibility(input.exprName, enclosingDeclaration); + } - if (isProcessedComponent(input)) { - switch (input.kind) { - case ts.SyntaxKind.ExpressionWithTypeArguments: { - if ((ts.isEntityName(input.expression) || ts.isEntityNameExpression(input.expression))) { - checkEntityNameVisibility(input.expression, enclosingDeclaration); - } - const node = ts.visitEachChild(input, visitDeclarationSubtree, context); - return cleanup(factory.updateExpressionWithTypeArguments(node, node.expression, node.typeArguments)); - } - case ts.SyntaxKind.TypeReference: { - checkEntityNameVisibility(input.typeName, enclosingDeclaration); - const node = ts.visitEachChild(input, visitDeclarationSubtree, context); - return cleanup(factory.updateTypeReferenceNode(node, node.typeName, node.typeArguments)); - } - case ts.SyntaxKind.ConstructSignature: - return cleanup(factory.updateConstructSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); - case ts.SyntaxKind.Constructor: { - // A constructor declaration may not have a type annotation - const ctor = factory.createConstructorDeclaration( - /*decorators*/ undefined, - /*modifiers*/ ensureModifiers(input), updateParamsList(input, input.parameters, ts.ModifierFlags.None), - /*body*/ undefined); - return cleanup(ctor); - } - case ts.SyntaxKind.MethodDeclaration: { - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - const sig = factory.createMethodDeclaration( - /*decorators*/ undefined, ensureModifiers(input), - /*asteriskToken*/ undefined, input.name, input.questionToken, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type), - /*body*/ undefined); - return cleanup(sig); - } - case ts.SyntaxKind.GetAccessor: { - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); - return cleanup(factory.updateGetAccessorDeclaration(input, - /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)), ensureType(input, accessorType), - /*body*/ undefined)); - } - case ts.SyntaxKind.SetAccessor: { - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(factory.updateSetAccessorDeclaration(input, - /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)), - /*body*/ undefined)); - } - case ts.SyntaxKind.PropertyDeclaration: - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(factory.updatePropertyDeclaration(input, - /*decorators*/ undefined, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type), ensureNoInitializer(input))); - case ts.SyntaxKind.PropertySignature: - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(factory.updatePropertySignature(input, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type))); - case ts.SyntaxKind.MethodSignature: { - if (ts.isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(factory.updateMethodSignature(input, ensureModifiers(input), input.name, input.questionToken, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); - } - case ts.SyntaxKind.CallSignature: { - return cleanup(factory.updateCallSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); + 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 ts.SyntaxKind.ExpressionWithTypeArguments: { + if ((ts.isEntityName(input.expression) || ts.isEntityNameExpression(input.expression))) { + checkEntityNameVisibility(input.expression, enclosingDeclaration); } - case ts.SyntaxKind.IndexSignature: { - return cleanup(factory.updateIndexSignature(input, - /*decorators*/ undefined, ensureModifiers(input), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree) || factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); + const node = ts.visitEachChild(input, visitDeclarationSubtree, context); + return cleanup(factory.updateExpressionWithTypeArguments(node, node.expression, node.typeArguments)); + } + case ts.SyntaxKind.TypeReference: { + checkEntityNameVisibility(input.typeName, enclosingDeclaration); + const node = ts.visitEachChild(input, visitDeclarationSubtree, context); + return cleanup(factory.updateTypeReferenceNode(node, node.typeName, node.typeArguments)); + } + case ts.SyntaxKind.ConstructSignature: + return cleanup(factory.updateConstructSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); + case ts.SyntaxKind.Constructor: { + // A constructor declaration may not have a type annotation + const ctor = factory.createConstructorDeclaration( + /*decorators*/ undefined, + /*modifiers*/ ensureModifiers(input), updateParamsList(input, input.parameters, ts.ModifierFlags.None), + /*body*/ undefined); + return cleanup(ctor); + } + case ts.SyntaxKind.MethodDeclaration: { + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.SyntaxKind.VariableDeclaration: { - if (ts.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(factory.updateVariableDeclaration(input, input.name, /*exclamationToken*/ undefined, ensureType(input, input.type), ensureNoInitializer(input))); + const sig = factory.createMethodDeclaration( + /*decorators*/ undefined, ensureModifiers(input), + /*asteriskToken*/ undefined, input.name, input.questionToken, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type), + /*body*/ undefined); + return cleanup(sig); + } + case ts.SyntaxKind.GetAccessor: { + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.SyntaxKind.TypeParameter: { - if (isPrivateMethodTypeParameter(input) && (input.default || input.constraint)) { - return cleanup(factory.updateTypeParameterDeclaration(input, input.modifiers, input.name, /*constraint*/ undefined, /*defaultType*/ undefined)); - } - return cleanup(ts.visitEachChild(input, visitDeclarationSubtree, context)); + const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); + return cleanup(factory.updateGetAccessorDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)), ensureType(input, accessorType), + /*body*/ undefined)); + } + case ts.SyntaxKind.SetAccessor: { + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.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 = ts.visitNode(input.checkType, visitDeclarationSubtree); - const extendsType = ts.visitNode(input.extendsType, visitDeclarationSubtree); - const oldEnclosingDecl = enclosingDeclaration; - enclosingDeclaration = input.trueType; - const trueType = ts.visitNode(input.trueType, visitDeclarationSubtree); - enclosingDeclaration = oldEnclosingDecl; - const falseType = ts.visitNode(input.falseType, visitDeclarationSubtree); - return cleanup(factory.updateConditionalTypeNode(input, checkType, extendsType, trueType, falseType)); + return cleanup(factory.updateSetAccessorDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, ts.hasEffectiveModifier(input, ts.ModifierFlags.Private)), + /*body*/ undefined)); + } + case ts.SyntaxKind.PropertyDeclaration: + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.SyntaxKind.FunctionType: { - return cleanup(factory.updateFunctionTypeNode(input, ts.visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree))); + return cleanup(factory.updatePropertyDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type), ensureNoInitializer(input))); + case ts.SyntaxKind.PropertySignature: + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.SyntaxKind.ConstructorType: { - return cleanup(factory.updateConstructorTypeNode(input, ensureModifiers(input), ts.visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree))); + return cleanup(factory.updatePropertySignature(input, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type))); + case ts.SyntaxKind.MethodSignature: { + if (ts.isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } - case ts.SyntaxKind.ImportType: { - if (!ts.isLiteralImportTypeNode(input)) - return cleanup(input); - return cleanup(factory.updateImportTypeNode(input, factory.updateLiteralTypeNode(input.argument, rewriteModuleSpecifier(input, input.argument.literal)), input.assertions, input.qualifier, ts.visitNodes(input.typeArguments, visitDeclarationSubtree, ts.isTypeNode), input.isTypeOf)); + return cleanup(factory.updateMethodSignature(input, ensureModifiers(input), input.name, input.questionToken, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); + } + case ts.SyntaxKind.CallSignature: { + return cleanup(factory.updateCallSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); + } + case ts.SyntaxKind.IndexSignature: { + return cleanup(factory.updateIndexSignature(input, + /*decorators*/ undefined, ensureModifiers(input), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree) || factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); + } + case ts.SyntaxKind.VariableDeclaration: { + if (ts.isBindingPattern(input.name)) { + return recreateBindingPattern(input.name); } - default: ts.Debug.assertNever(input, `Attempted to process unhandled node kind: ${(ts as any).SyntaxKind[(input as any).kind]}`); + 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(factory.updateVariableDeclaration(input, input.name, /*exclamationToken*/ undefined, ensureType(input, input.type), ensureNoInitializer(input))); } - } - - if (ts.isTupleTypeNode(input) && (ts.getLineAndCharacterOfPosition(currentSourceFile, input.pos).line === ts.getLineAndCharacterOfPosition(currentSourceFile, input.end).line)) { - ts.setEmitFlags(input, ts.EmitFlags.SingleLine); - } - - return cleanup(ts.visitEachChild(input, visitDeclarationSubtree, context)); - function cleanup(returnValue: T | undefined): T | undefined { - if (returnValue && canProduceDiagnostic && ts.hasDynamicName(input as ts.Declaration)) { - checkName(input); + case ts.SyntaxKind.TypeParameter: { + if (isPrivateMethodTypeParameter(input) && (input.default || input.constraint)) { + return cleanup(factory.updateTypeParameterDeclaration(input, input.modifiers, input.name, /*constraint*/ undefined, /*defaultType*/ undefined)); + } + return cleanup(ts.visitEachChild(input, visitDeclarationSubtree, context)); } - if (isEnclosingDeclaration(input)) { - enclosingDeclaration = previousEnclosingDeclaration; + case ts.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 = ts.visitNode(input.checkType, visitDeclarationSubtree); + const extendsType = ts.visitNode(input.extendsType, visitDeclarationSubtree); + const oldEnclosingDecl = enclosingDeclaration; + enclosingDeclaration = input.trueType; + const trueType = ts.visitNode(input.trueType, visitDeclarationSubtree); + enclosingDeclaration = oldEnclosingDecl; + const falseType = ts.visitNode(input.falseType, visitDeclarationSubtree); + return cleanup(factory.updateConditionalTypeNode(input, checkType, extendsType, trueType, falseType)); } - if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag; + case ts.SyntaxKind.FunctionType: { + return cleanup(factory.updateFunctionTypeNode(input, ts.visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree))); } - if (shouldEnterSuppressNewDiagnosticsContextContext) { - suppressNewDiagnosticContexts = oldWithinObjectLiteralType; + case ts.SyntaxKind.ConstructorType: { + return cleanup(factory.updateConstructorTypeNode(input, ensureModifiers(input), ts.visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), ts.visitNode(input.type, visitDeclarationSubtree))); } - if (returnValue === input) { - return returnValue; + case ts.SyntaxKind.ImportType: { + if (!ts.isLiteralImportTypeNode(input)) + return cleanup(input); + return cleanup(factory.updateImportTypeNode(input, factory.updateLiteralTypeNode(input.argument, rewriteModuleSpecifier(input, input.argument.literal)), input.assertions, input.qualifier, ts.visitNodes(input.typeArguments, visitDeclarationSubtree, ts.isTypeNode), input.isTypeOf)); } - return returnValue && ts.setOriginalNode(preserveJsDoc(returnValue, input), input); + default: ts.Debug.assertNever(input, `Attempted to process unhandled node kind: ${(ts as any).SyntaxKind[(input as any).kind]}`); } } - function isPrivateMethodTypeParameter(node: ts.TypeParameterDeclaration) { - return node.parent.kind === ts.SyntaxKind.MethodDeclaration && ts.hasEffectiveModifier(node.parent, ts.ModifierFlags.Private); + if (ts.isTupleTypeNode(input) && (ts.getLineAndCharacterOfPosition(currentSourceFile, input.pos).line === ts.getLineAndCharacterOfPosition(currentSourceFile, input.end).line)) { + ts.setEmitFlags(input, ts.EmitFlags.SingleLine); } - function visitDeclarationStatements(input: ts.Node): ts.VisitResult { - if (!isPreservedDeclarationStatement(input)) { - // return undefined for unmatched kinds to omit them from the tree - return; + return cleanup(ts.visitEachChild(input, visitDeclarationSubtree, context)); + function cleanup(returnValue: T | undefined): T | undefined { + if (returnValue && canProduceDiagnostic && ts.hasDynamicName(input as ts.Declaration)) { + checkName(input); } - if (shouldStripInternal(input)) - return; - - switch (input.kind) { - case ts.SyntaxKind.ExportDeclaration: { - if (ts.isSourceFile(input.parent)) { - resultHasExternalModuleIndicator = true; - } - resultHasScopeMarker = true; - // Always visible if the parent node isn't dropped for being not visible - // Rewrite external module names if necessary - return factory.updateExportDeclaration(input, - /*decorators*/ undefined, input.modifiers, input.isTypeOnly, input.exportClause, rewriteModuleSpecifier(input, input.moduleSpecifier), ts.getResolutionModeOverrideForClause(input.assertClause) ? input.assertClause : undefined); - } - case ts.SyntaxKind.ExportAssignment: { - // Always visible if the parent node isn't dropped for being not visible - if (ts.isSourceFile(input.parent)) { - resultHasExternalModuleIndicator = true; - } - resultHasScopeMarker = true; - if (input.expression.kind === ts.SyntaxKind.Identifier) { - return input; - } - else { - const newId = factory.createUniqueName("_default", ts.GeneratedIdentifierFlags.Optimistic); - getSymbolAccessibilityDiagnostic = () => ({ - diagnosticMessage: ts.Diagnostics.Default_export_of_the_module_has_or_is_using_private_name_0, - errorNode: input - }); - errorFallbackNode = input; - const varDecl = factory.createVariableDeclaration(newId, /*exclamationToken*/ undefined, resolver.createTypeOfExpression(input.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); - errorFallbackNode = undefined; - const statement = factory.createVariableStatement(needsDeclare ? [factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [], factory.createVariableDeclarationList([varDecl], ts.NodeFlags.Const)); - - preserveJsDoc(statement, input); - ts.removeAllComments(input); - return [statement, factory.updateExportAssignment(input, input.decorators, input.modifiers, newId)]; - } - } + if (isEnclosingDeclaration(input)) { + enclosingDeclaration = previousEnclosingDeclaration; } - - const result = transformTopLevelDeclaration(input); - // Don't actually transform yet; just leave as original node - will be elided/swapped by late pass - lateStatementReplacementMap.set(ts.getOriginalNodeId(input), result); - return input; + if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = oldDiag; + } + if (shouldEnterSuppressNewDiagnosticsContextContext) { + suppressNewDiagnosticContexts = oldWithinObjectLiteralType; + } + if (returnValue === input) { + return returnValue; + } + return returnValue && ts.setOriginalNode(preserveJsDoc(returnValue, input), input); } + } - function stripExportModifiers(statement: ts.Statement): ts.Statement { - if (ts.isImportEqualsDeclaration(statement) || ts.hasEffectiveModifier(statement, ts.ModifierFlags.Default) || !ts.canHaveModifiers(statement)) { - // `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 isPrivateMethodTypeParameter(node: ts.TypeParameterDeclaration) { + return node.parent.kind === ts.SyntaxKind.MethodDeclaration && ts.hasEffectiveModifier(node.parent, ts.ModifierFlags.Private); + } - const modifiers = factory.createModifiersFromModifierFlags(ts.getEffectiveModifierFlags(statement) & (ts.ModifierFlags.All ^ ts.ModifierFlags.Export)); - return factory.updateModifiers(statement, modifiers); + function visitDeclarationStatements(input: ts.Node): ts.VisitResult { + if (!isPreservedDeclarationStatement(input)) { + // return undefined for unmatched kinds to omit them from the tree + return; } + if (shouldStripInternal(input)) + return; - function transformTopLevelDeclaration(input: ts.LateVisibilityPaintedStatement) { - if (lateMarkedStatements) { - while (ts.orderedRemoveItem(lateMarkedStatements, input)) - ; + switch (input.kind) { + case ts.SyntaxKind.ExportDeclaration: { + if (ts.isSourceFile(input.parent)) { + resultHasExternalModuleIndicator = true; + } + resultHasScopeMarker = true; + // Always visible if the parent node isn't dropped for being not visible + // Rewrite external module names if necessary + return factory.updateExportDeclaration(input, + /*decorators*/ undefined, input.modifiers, input.isTypeOnly, input.exportClause, rewriteModuleSpecifier(input, input.moduleSpecifier), ts.getResolutionModeOverrideForClause(input.assertClause) ? input.assertClause : undefined); } - if (shouldStripInternal(input)) - return; - switch (input.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: { - return transformImportEqualsDeclaration(input); + case ts.SyntaxKind.ExportAssignment: { + // Always visible if the parent node isn't dropped for being not visible + if (ts.isSourceFile(input.parent)) { + resultHasExternalModuleIndicator = true; } - case ts.SyntaxKind.ImportDeclaration: { - return transformImportDeclaration(input); + resultHasScopeMarker = true; + if (input.expression.kind === ts.SyntaxKind.Identifier) { + return input; + } + else { + const newId = factory.createUniqueName("_default", ts.GeneratedIdentifierFlags.Optimistic); + getSymbolAccessibilityDiagnostic = () => ({ + diagnosticMessage: ts.Diagnostics.Default_export_of_the_module_has_or_is_using_private_name_0, + errorNode: input + }); + errorFallbackNode = input; + const varDecl = factory.createVariableDeclaration(newId, /*exclamationToken*/ undefined, resolver.createTypeOfExpression(input.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); + errorFallbackNode = undefined; + const statement = factory.createVariableStatement(needsDeclare ? [factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [], factory.createVariableDeclarationList([varDecl], ts.NodeFlags.Const)); + + preserveJsDoc(statement, input); + ts.removeAllComments(input); + return [statement, factory.updateExportAssignment(input, input.decorators, input.modifiers, newId)]; } } - if (ts.isDeclaration(input) && isDeclarationAndNotVisible(input)) - return; + } - // Elide implementation signatures from overload sets - if (ts.isFunctionLike(input) && resolver.isImplementationOfOverload(input)) - return; + const result = transformTopLevelDeclaration(input); + // Don't actually transform yet; just leave as original node - will be elided/swapped by late pass + lateStatementReplacementMap.set(ts.getOriginalNodeId(input), result); + return input; + } - let previousEnclosingDeclaration: typeof enclosingDeclaration; - if (isEnclosingDeclaration(input)) { - previousEnclosingDeclaration = enclosingDeclaration; - enclosingDeclaration = input as ts.Declaration; - } + function stripExportModifiers(statement: ts.Statement): ts.Statement { + if (ts.isImportEqualsDeclaration(statement) || ts.hasEffectiveModifier(statement, ts.ModifierFlags.Default) || !ts.canHaveModifiers(statement)) { + // `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 canProdiceDiagnostic = ts.canProduceDiagnostics(input); - const oldDiag = getSymbolAccessibilityDiagnostic; - if (canProdiceDiagnostic) { - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(input as ts.DeclarationDiagnosticProducing); + const modifiers = factory.createModifiersFromModifierFlags(ts.getEffectiveModifierFlags(statement) & (ts.ModifierFlags.All ^ ts.ModifierFlags.Export)); + return factory.updateModifiers(statement, modifiers); + } + + function transformTopLevelDeclaration(input: ts.LateVisibilityPaintedStatement) { + if (lateMarkedStatements) { + while (ts.orderedRemoveItem(lateMarkedStatements, input)) + ; + } + if (shouldStripInternal(input)) + return; + switch (input.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: { + return transformImportEqualsDeclaration(input); } + case ts.SyntaxKind.ImportDeclaration: { + return transformImportDeclaration(input); + } + } + if (ts.isDeclaration(input) && isDeclarationAndNotVisible(input)) + return; - const previousNeedsDeclare = needsDeclare; - switch (input.kind) { - case ts.SyntaxKind.TypeAliasDeclaration: // Type aliases get `declare`d if need be (for legacy support), but that's all - return cleanup(factory.updateTypeAliasDeclaration(input, - /*decorators*/ undefined, ensureModifiers(input), input.name, ts.visitNodes(input.typeParameters, visitDeclarationSubtree, ts.isTypeParameterDeclaration), ts.visitNode(input.type, visitDeclarationSubtree, ts.isTypeNode))); - case ts.SyntaxKind.InterfaceDeclaration: { - return cleanup(factory.updateInterfaceDeclaration(input, - /*decorators*/ undefined, ensureModifiers(input), input.name, ensureTypeParams(input, input.typeParameters), transformHeritageClauses(input.heritageClauses), ts.visitNodes(input.members, visitDeclarationSubtree))); - } - case ts.SyntaxKind.FunctionDeclaration: { - // Generators lose their generator-ness, excepting their return type - const clean = cleanup(factory.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) && shouldEmitFunctionProperties(input)) { - const props = resolver.getPropertiesOfContainerFunction(input); - // Use parseNodeFactory so it is usable as an enclosing declaration - const fakespace = ts.parseNodeFactory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, clean.name || factory.createIdentifier("_default"), factory.createModuleBlock([]), ts.NodeFlags.Namespace); - ts.setParent(fakespace, enclosingDeclaration as ts.SourceFile | ts.NamespaceDeclaration); - fakespace.locals = ts.createSymbolTable(props); - fakespace.symbol = props[0].parent!; - const exportMappings: [ - ts.Identifier, - string - ][] = []; - let declarations: (ts.VariableStatement | ts.ExportDeclaration)[] = ts.mapDefined(props, p => { - if (!p.valueDeclaration || !ts.isPropertyAccessExpression(p.valueDeclaration)) { - return undefined; // TODO GH#33569: Handle element access expressions that created late bound names (rather than silently omitting them) - } - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(p.valueDeclaration); - const type = resolver.createTypeOfDeclaration(p.valueDeclaration, fakespace, declarationEmitNodeBuilderFlags, symbolTracker); - getSymbolAccessibilityDiagnostic = oldDiag; - const nameStr = ts.unescapeLeadingUnderscores(p.escapedName); - const isNonContextualKeywordName = ts.isStringANonContextualKeyword(nameStr); - const name = isNonContextualKeywordName ? factory.getGeneratedNameForNode(p.valueDeclaration) : factory.createIdentifier(nameStr); - if (isNonContextualKeywordName) { - exportMappings.push([name, nameStr]); - } - const varDecl = factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, /*initializer*/ undefined); - return factory.createVariableStatement(isNonContextualKeywordName ? undefined : [factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList([varDecl])); - }); - if (!exportMappings.length) { - declarations = ts.mapDefined(declarations, declaration => factory.updateModifiers(declaration, ts.ModifierFlags.None)); - } - else { - declarations.push(factory.createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isTypeOnly*/ false, factory.createNamedExports(ts.map(exportMappings, ([gen, exp]) => { - return factory.createExportSpecifier(/*isTypeOnly*/ false, gen, exp); - })))); - } - const namespaceDecl = factory.createModuleDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name!, factory.createModuleBlock(declarations), ts.NodeFlags.Namespace); - if (!ts.hasEffectiveModifier(clean, ts.ModifierFlags.Default)) { - return [clean, namespaceDecl]; - } + // Elide implementation signatures from overload sets + if (ts.isFunctionLike(input) && resolver.isImplementationOfOverload(input)) + return; - const modifiers = factory.createModifiersFromModifierFlags((ts.getEffectiveModifierFlags(clean) & ~ts.ModifierFlags.ExportDefault) | ts.ModifierFlags.Ambient); - const cleanDeclaration = factory.updateFunctionDeclaration(clean, - /*decorators*/ undefined, modifiers, - /*asteriskToken*/ undefined, clean.name, clean.typeParameters, clean.parameters, clean.type, - /*body*/ undefined); - const namespaceDeclaration = factory.updateModuleDeclaration(namespaceDecl, - /*decorators*/ undefined, modifiers, namespaceDecl.name, namespaceDecl.body); + let previousEnclosingDeclaration: typeof enclosingDeclaration; + if (isEnclosingDeclaration(input)) { + previousEnclosingDeclaration = enclosingDeclaration; + enclosingDeclaration = input as ts.Declaration; + } - const exportDefaultDeclaration = factory.createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isExportEquals*/ false, namespaceDecl.name); - if (ts.isSourceFile(input.parent)) { - resultHasExternalModuleIndicator = true; - } - resultHasScopeMarker = true; + const canProdiceDiagnostic = ts.canProduceDiagnostics(input); + const oldDiag = getSymbolAccessibilityDiagnostic; + if (canProdiceDiagnostic) { + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(input as ts.DeclarationDiagnosticProducing); + } - return [cleanDeclaration, namespaceDeclaration, exportDefaultDeclaration]; - } - else { - return clean; - } - } - case ts.SyntaxKind.ModuleDeclaration: { - needsDeclare = false; - const inner = input.body; - if (inner && inner.kind === ts.SyntaxKind.ModuleBlock) { - const oldNeedsScopeFix = needsScopeFixMarker; - const oldHasScopeFix = resultHasScopeMarker; - resultHasScopeMarker = false; - needsScopeFixMarker = false; - const statements = ts.visitNodes(inner.statements, visitDeclarationStatements); - let lateStatements = transformAndReplaceLatePaintedStatements(statements); - if (input.flags & ts.NodeFlags.Ambient) { - needsScopeFixMarker = false; // If it was `declare`'d everything is implicitly exported already, ignore late printed "privates" + const previousNeedsDeclare = needsDeclare; + switch (input.kind) { + case ts.SyntaxKind.TypeAliasDeclaration: // Type aliases get `declare`d if need be (for legacy support), but that's all + return cleanup(factory.updateTypeAliasDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, ts.visitNodes(input.typeParameters, visitDeclarationSubtree, ts.isTypeParameterDeclaration), ts.visitNode(input.type, visitDeclarationSubtree, ts.isTypeNode))); + case ts.SyntaxKind.InterfaceDeclaration: { + return cleanup(factory.updateInterfaceDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, ensureTypeParams(input, input.typeParameters), transformHeritageClauses(input.heritageClauses), ts.visitNodes(input.members, visitDeclarationSubtree))); + } + case ts.SyntaxKind.FunctionDeclaration: { + // Generators lose their generator-ness, excepting their return type + const clean = cleanup(factory.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) && shouldEmitFunctionProperties(input)) { + const props = resolver.getPropertiesOfContainerFunction(input); + // Use parseNodeFactory so it is usable as an enclosing declaration + const fakespace = ts.parseNodeFactory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, clean.name || factory.createIdentifier("_default"), factory.createModuleBlock([]), ts.NodeFlags.Namespace); + ts.setParent(fakespace, enclosingDeclaration as ts.SourceFile | ts.NamespaceDeclaration); + fakespace.locals = ts.createSymbolTable(props); + fakespace.symbol = props[0].parent!; + const exportMappings: [ + ts.Identifier, + string + ][] = []; + let declarations: (ts.VariableStatement | ts.ExportDeclaration)[] = ts.mapDefined(props, p => { + if (!p.valueDeclaration || !ts.isPropertyAccessExpression(p.valueDeclaration)) { + return undefined; // TODO GH#33569: Handle element access expressions that created late bound names (rather than silently omitting them) } - // 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 (!ts.isGlobalScopeAugmentation(input) && !hasScopeMarker(lateStatements) && !resultHasScopeMarker) { - if (needsScopeFixMarker) { - lateStatements = factory.createNodeArray([...lateStatements, ts.createEmptyExports(factory)]); - } - else { - lateStatements = ts.visitNodes(lateStatements, stripExportModifiers); - } + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(p.valueDeclaration); + const type = resolver.createTypeOfDeclaration(p.valueDeclaration, fakespace, declarationEmitNodeBuilderFlags, symbolTracker); + getSymbolAccessibilityDiagnostic = oldDiag; + const nameStr = ts.unescapeLeadingUnderscores(p.escapedName); + const isNonContextualKeywordName = ts.isStringANonContextualKeyword(nameStr); + const name = isNonContextualKeywordName ? factory.getGeneratedNameForNode(p.valueDeclaration) : factory.createIdentifier(nameStr); + if (isNonContextualKeywordName) { + exportMappings.push([name, nameStr]); } - const body = factory.updateModuleBlock(inner, lateStatements); - needsDeclare = previousNeedsDeclare; - needsScopeFixMarker = oldNeedsScopeFix; - resultHasScopeMarker = oldHasScopeFix; - const mods = ensureModifiers(input); - return cleanup(factory.updateModuleDeclaration(input, - /*decorators*/ undefined, mods, ts.isExternalModuleAugmentation(input) ? rewriteModuleSpecifier(input, input.name) : input.name, body)); + const varDecl = factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, /*initializer*/ undefined); + return factory.createVariableStatement(isNonContextualKeywordName ? undefined : [factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList([varDecl])); + }); + if (!exportMappings.length) { + declarations = ts.mapDefined(declarations, declaration => factory.updateModifiers(declaration, ts.ModifierFlags.None)); } else { - needsDeclare = previousNeedsDeclare; - const mods = ensureModifiers(input); - needsDeclare = false; - ts.visitNode(inner, visitDeclarationStatements); - // eagerly transform nested namespaces (the nesting doesn't need any elision or painting done) - const id = ts.getOriginalNodeId(inner!); // TODO: GH#18217 - const body = lateStatementReplacementMap.get(id); - lateStatementReplacementMap.delete(id); - return cleanup(factory.updateModuleDeclaration(input, - /*decorators*/ undefined, mods, input.name, body as ts.ModuleBody)); + declarations.push(factory.createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isTypeOnly*/ false, factory.createNamedExports(ts.map(exportMappings, ([gen, exp]) => { + return factory.createExportSpecifier(/*isTypeOnly*/ false, gen, exp); + })))); } - } - case ts.SyntaxKind.ClassDeclaration: { - errorNameNode = input.name; - errorFallbackNode = input; - const modifiers = factory.createNodeArray(ensureModifiers(input)); - const typeParameters = ensureTypeParams(input, input.typeParameters); - const ctor = ts.getFirstConstructorWithBody(input); - let parameterProperties: readonly ts.PropertyDeclaration[] | undefined; - if (ctor) { - const oldDiag = getSymbolAccessibilityDiagnostic; - parameterProperties = ts.compact(ts.flatMap(ctor.parameters, (param) => { - if (!ts.hasSyntacticModifier(param, ts.ModifierFlags.ParameterPropertyModifier) || shouldStripInternal(param)) - return; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(param); - if (param.name.kind === ts.SyntaxKind.Identifier) { - return preserveJsDoc(factory.createPropertyDeclaration( - /*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: ts.BindingPattern) { - let elems: ts.PropertyDeclaration[] | undefined; - for (const elem of pattern.elements) { - if (ts.isOmittedExpression(elem)) - continue; - if (ts.isBindingPattern(elem.name)) { - elems = ts.concatenate(elems, walkBindingPattern(elem.name)); - } - elems = elems || []; - elems.push(factory.createPropertyDeclaration( - /*decorators*/ undefined, ensureModifiers(param), elem.name as ts.Identifier, - /*questionToken*/ undefined, ensureType(elem, /*type*/ undefined), - /*initializer*/ undefined)); - } - return elems; - } - })); - getSymbolAccessibilityDiagnostic = oldDiag; + const namespaceDecl = factory.createModuleDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name!, factory.createModuleBlock(declarations), ts.NodeFlags.Namespace); + if (!ts.hasEffectiveModifier(clean, ts.ModifierFlags.Default)) { + return [clean, namespaceDecl]; } - const hasPrivateIdentifier = ts.some(input.members, member => !!member.name && ts.isPrivateIdentifier(member.name)); - // When the class has at least one private identifier, create a unique constant identifier to retain the nominal typing behavior - // Prevents other classes with the same public members from being used in place of the current class - const privateIdentifier = hasPrivateIdentifier ? [ - factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, factory.createPrivateIdentifier("#private"), - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined) - ] : undefined; - const memberNodes = ts.concatenate(ts.concatenate(privateIdentifier, parameterProperties), ts.visitNodes(input.members, visitDeclarationSubtree)); - const members = factory.createNodeArray(memberNodes); - - const extendsClause = ts.getEffectiveBaseTypeNode(input); - if (extendsClause && !ts.isEntityNameExpression(extendsClause.expression) && extendsClause.expression.kind !== ts.SyntaxKind.NullKeyword) { - // We must add a temporary declaration for the extends clause expression - - const oldId = input.name ? ts.unescapeLeadingUnderscores(input.name.escapedText) : "default"; - const newId = factory.createUniqueName(`${oldId}_base`, ts.GeneratedIdentifierFlags.Optimistic); - getSymbolAccessibilityDiagnostic = () => ({ - diagnosticMessage: ts.Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1, - errorNode: extendsClause, - typeName: input.name - }); - const varDecl = factory.createVariableDeclaration(newId, /*exclamationToken*/ undefined, resolver.createTypeOfExpression(extendsClause.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); - const statement = factory.createVariableStatement(needsDeclare ? [factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [], factory.createVariableDeclarationList([varDecl], ts.NodeFlags.Const)); - const heritageClauses = factory.createNodeArray(ts.map(input.heritageClauses, clause => { - if (clause.token === ts.SyntaxKind.ExtendsKeyword) { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(clause.types[0]); - const newClause = factory.updateHeritageClause(clause, ts.map(clause.types, t => factory.updateExpressionWithTypeArguments(t, newId, ts.visitNodes(t.typeArguments, visitDeclarationSubtree)))); - getSymbolAccessibilityDiagnostic = oldDiag; - return newClause; - } - return factory.updateHeritageClause(clause, ts.visitNodes(factory.createNodeArray(ts.filter(clause.types, t => ts.isEntityNameExpression(t.expression) || t.expression.kind === ts.SyntaxKind.NullKeyword)), visitDeclarationSubtree)); - })); - return [statement, cleanup(factory.updateClassDeclaration(input, - /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members))!]; // TODO: GH#18217 - } - else { - const heritageClauses = transformHeritageClauses(input.heritageClauses); - return cleanup(factory.updateClassDeclaration(input, - /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members)); + const modifiers = factory.createModifiersFromModifierFlags((ts.getEffectiveModifierFlags(clean) & ~ts.ModifierFlags.ExportDefault) | ts.ModifierFlags.Ambient); + const cleanDeclaration = factory.updateFunctionDeclaration(clean, + /*decorators*/ undefined, modifiers, + /*asteriskToken*/ undefined, clean.name, clean.typeParameters, clean.parameters, clean.type, + /*body*/ undefined); + const namespaceDeclaration = factory.updateModuleDeclaration(namespaceDecl, + /*decorators*/ undefined, modifiers, namespaceDecl.name, namespaceDecl.body); + + const exportDefaultDeclaration = factory.createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isExportEquals*/ false, namespaceDecl.name); + if (ts.isSourceFile(input.parent)) { + resultHasExternalModuleIndicator = true; } + resultHasScopeMarker = true; + + return [cleanDeclaration, namespaceDeclaration, exportDefaultDeclaration]; } - case ts.SyntaxKind.VariableStatement: { - return cleanup(transformVariableStatement(input)); - } - case ts.SyntaxKind.EnumDeclaration: { - return cleanup(factory.updateEnumDeclaration(input, /*decorators*/ undefined, factory.createNodeArray(ensureModifiers(input)), input.name, factory.createNodeArray(ts.mapDefined(input.members, m => { - if (shouldStripInternal(m)) - return; - // Rewrite enum values to their constants, if available - const constValue = resolver.getConstantValue(m); - return preserveJsDoc(factory.updateEnumMember(m, m.name, constValue !== undefined ? typeof constValue === "string" ? factory.createStringLiteral(constValue) : factory.createNumericLiteral(constValue) : undefined), m); - })))); + else { + return clean; } } - // Anything left unhandled is an error, so this should be unreachable - return ts.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) { - getSymbolAccessibilityDiagnostic = oldDiag; - } - if (input.kind === ts.SyntaxKind.ModuleDeclaration) { + case ts.SyntaxKind.ModuleDeclaration: { + needsDeclare = false; + const inner = input.body; + if (inner && inner.kind === ts.SyntaxKind.ModuleBlock) { + const oldNeedsScopeFix = needsScopeFixMarker; + const oldHasScopeFix = resultHasScopeMarker; + resultHasScopeMarker = false; + needsScopeFixMarker = false; + const statements = ts.visitNodes(inner.statements, visitDeclarationStatements); + let lateStatements = transformAndReplaceLatePaintedStatements(statements); + if (input.flags & ts.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 (!ts.isGlobalScopeAugmentation(input) && !hasScopeMarker(lateStatements) && !resultHasScopeMarker) { + if (needsScopeFixMarker) { + lateStatements = factory.createNodeArray([...lateStatements, ts.createEmptyExports(factory)]); + } + else { + lateStatements = ts.visitNodes(lateStatements, stripExportModifiers); + } + } + const body = factory.updateModuleBlock(inner, lateStatements); needsDeclare = previousNeedsDeclare; + needsScopeFixMarker = oldNeedsScopeFix; + resultHasScopeMarker = oldHasScopeFix; + const mods = ensureModifiers(input); + return cleanup(factory.updateModuleDeclaration(input, + /*decorators*/ undefined, mods, ts.isExternalModuleAugmentation(input) ? rewriteModuleSpecifier(input, input.name) : input.name, body)); } - if (node as ts.Node === input) { - return node; + else { + needsDeclare = previousNeedsDeclare; + const mods = ensureModifiers(input); + needsDeclare = false; + ts.visitNode(inner, visitDeclarationStatements); + // eagerly transform nested namespaces (the nesting doesn't need any elision or painting done) + const id = ts.getOriginalNodeId(inner!); // TODO: GH#18217 + const body = lateStatementReplacementMap.get(id); + lateStatementReplacementMap.delete(id); + return cleanup(factory.updateModuleDeclaration(input, + /*decorators*/ undefined, mods, input.name, body as ts.ModuleBody)); } - errorFallbackNode = undefined; - errorNameNode = undefined; - return node && ts.setOriginalNode(preserveJsDoc(node, input), input); } - } - - function transformVariableStatement(input: ts.VariableStatement) { - if (!ts.forEach(input.declarationList.declarations, getBindingNameVisible)) - return; - const nodes = ts.visitNodes(input.declarationList.declarations, visitDeclarationSubtree); - if (!ts.length(nodes)) - return; - return factory.updateVariableStatement(input, factory.createNodeArray(ensureModifiers(input)), factory.updateVariableDeclarationList(input.declarationList, nodes)); - } + case ts.SyntaxKind.ClassDeclaration: { + errorNameNode = input.name; + errorFallbackNode = input; + const modifiers = factory.createNodeArray(ensureModifiers(input)); + const typeParameters = ensureTypeParams(input, input.typeParameters); + const ctor = ts.getFirstConstructorWithBody(input); + let parameterProperties: readonly ts.PropertyDeclaration[] | undefined; + if (ctor) { + const oldDiag = getSymbolAccessibilityDiagnostic; + parameterProperties = ts.compact(ts.flatMap(ctor.parameters, (param) => { + if (!ts.hasSyntacticModifier(param, ts.ModifierFlags.ParameterPropertyModifier) || shouldStripInternal(param)) + return; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(param); + if (param.name.kind === ts.SyntaxKind.Identifier) { + return preserveJsDoc(factory.createPropertyDeclaration( + /*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 recreateBindingPattern(d: ts.BindingPattern): ts.VariableDeclaration[] { - return ts.flatten(ts.mapDefined(d.elements, e => recreateBindingElement(e))); - } + function walkBindingPattern(pattern: ts.BindingPattern) { + let elems: ts.PropertyDeclaration[] | undefined; + for (const elem of pattern.elements) { + if (ts.isOmittedExpression(elem)) + continue; + if (ts.isBindingPattern(elem.name)) { + elems = ts.concatenate(elems, walkBindingPattern(elem.name)); + } + elems = elems || []; + elems.push(factory.createPropertyDeclaration( + /*decorators*/ undefined, ensureModifiers(param), elem.name as ts.Identifier, + /*questionToken*/ undefined, ensureType(elem, /*type*/ undefined), + /*initializer*/ undefined)); + } + return elems; + } + })); + getSymbolAccessibilityDiagnostic = oldDiag; + } - function recreateBindingElement(e: ts.ArrayBindingElement) { - if (e.kind === ts.SyntaxKind.OmittedExpression) { - return; - } - if (e.name) { - if (!getBindingNameVisible(e)) - return; - if (ts.isBindingPattern(e.name)) { - return recreateBindingPattern(e.name); + const hasPrivateIdentifier = ts.some(input.members, member => !!member.name && ts.isPrivateIdentifier(member.name)); + // When the class has at least one private identifier, create a unique constant identifier to retain the nominal typing behavior + // Prevents other classes with the same public members from being used in place of the current class + const privateIdentifier = hasPrivateIdentifier ? [ + factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, factory.createPrivateIdentifier("#private"), + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined) + ] : undefined; + const memberNodes = ts.concatenate(ts.concatenate(privateIdentifier, parameterProperties), ts.visitNodes(input.members, visitDeclarationSubtree)); + const members = factory.createNodeArray(memberNodes); + + const extendsClause = ts.getEffectiveBaseTypeNode(input); + if (extendsClause && !ts.isEntityNameExpression(extendsClause.expression) && extendsClause.expression.kind !== ts.SyntaxKind.NullKeyword) { + // We must add a temporary declaration for the extends clause expression + + const oldId = input.name ? ts.unescapeLeadingUnderscores(input.name.escapedText) : "default"; + const newId = factory.createUniqueName(`${oldId}_base`, ts.GeneratedIdentifierFlags.Optimistic); + getSymbolAccessibilityDiagnostic = () => ({ + diagnosticMessage: ts.Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1, + errorNode: extendsClause, + typeName: input.name + }); + const varDecl = factory.createVariableDeclaration(newId, /*exclamationToken*/ undefined, resolver.createTypeOfExpression(extendsClause.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); + const statement = factory.createVariableStatement(needsDeclare ? [factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [], factory.createVariableDeclarationList([varDecl], ts.NodeFlags.Const)); + const heritageClauses = factory.createNodeArray(ts.map(input.heritageClauses, clause => { + if (clause.token === ts.SyntaxKind.ExtendsKeyword) { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNode(clause.types[0]); + const newClause = factory.updateHeritageClause(clause, ts.map(clause.types, t => factory.updateExpressionWithTypeArguments(t, newId, ts.visitNodes(t.typeArguments, visitDeclarationSubtree)))); + getSymbolAccessibilityDiagnostic = oldDiag; + return newClause; + } + return factory.updateHeritageClause(clause, ts.visitNodes(factory.createNodeArray(ts.filter(clause.types, t => ts.isEntityNameExpression(t.expression) || t.expression.kind === ts.SyntaxKind.NullKeyword)), visitDeclarationSubtree)); + })); + return [statement, cleanup(factory.updateClassDeclaration(input, + /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members))!]; // TODO: GH#18217 } else { - return factory.createVariableDeclaration(e.name, /*exclamationToken*/ undefined, ensureType(e, /*type*/ undefined), /*initializer*/ undefined); + const heritageClauses = transformHeritageClauses(input.heritageClauses); + return cleanup(factory.updateClassDeclaration(input, + /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members)); } } + case ts.SyntaxKind.VariableStatement: { + return cleanup(transformVariableStatement(input)); + } + case ts.SyntaxKind.EnumDeclaration: { + return cleanup(factory.updateEnumDeclaration(input, /*decorators*/ undefined, factory.createNodeArray(ensureModifiers(input)), input.name, factory.createNodeArray(ts.mapDefined(input.members, m => { + if (shouldStripInternal(m)) + return; + // Rewrite enum values to their constants, if available + const constValue = resolver.getConstantValue(m); + return preserveJsDoc(factory.updateEnumMember(m, m.name, constValue !== undefined ? typeof constValue === "string" ? factory.createStringLiteral(constValue) : factory.createNumericLiteral(constValue) : undefined), m); + })))); + } } - - function checkName(node: ts.DeclarationDiagnosticProducing) { - let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; - if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNodeName(node); + // Anything left unhandled is an error, so this should be unreachable + return ts.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; } - errorNameNode = (node as ts.NamedDeclaration).name; - ts.Debug.assert(resolver.isLateBound(ts.getParseTreeNode(node) as ts.Declaration)); // Should only be called with dynamic names - const decl = node as ts.NamedDeclaration as ts.LateBoundDeclaration; - const entityName = decl.name.expression; - checkEntityNameVisibility(entityName, enclosingDeclaration); - if (!suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag!; + if (canProdiceDiagnostic) { + getSymbolAccessibilityDiagnostic = oldDiag; + } + if (input.kind === ts.SyntaxKind.ModuleDeclaration) { + needsDeclare = previousNeedsDeclare; + } + if (node as ts.Node === input) { + return node; } + errorFallbackNode = undefined; errorNameNode = undefined; + return node && ts.setOriginalNode(preserveJsDoc(node, input), input); } + } - function shouldStripInternal(node: ts.Node) { - return !!stripInternal && !!node && isInternalDeclaration(node, currentSourceFile); - } + function transformVariableStatement(input: ts.VariableStatement) { + if (!ts.forEach(input.declarationList.declarations, getBindingNameVisible)) + return; + const nodes = ts.visitNodes(input.declarationList.declarations, visitDeclarationSubtree); + if (!ts.length(nodes)) + return; + return factory.updateVariableStatement(input, factory.createNodeArray(ensureModifiers(input)), factory.updateVariableDeclarationList(input.declarationList, nodes)); + } - function isScopeMarker(node: ts.Node) { - return ts.isExportAssignment(node) || ts.isExportDeclaration(node); - } + function recreateBindingPattern(d: ts.BindingPattern): ts.VariableDeclaration[] { + return ts.flatten(ts.mapDefined(d.elements, e => recreateBindingElement(e))); + } - function hasScopeMarker(statements: readonly ts.Statement[]) { - return ts.some(statements, isScopeMarker); + function recreateBindingElement(e: ts.ArrayBindingElement) { + if (e.kind === ts.SyntaxKind.OmittedExpression) { + return; } - - function ensureModifiers(node: ts.Node): readonly ts.Modifier[] | undefined { - const currentFlags = ts.getEffectiveModifierFlags(node); - const newFlags = ensureModifierFlags(node); - if (currentFlags === newFlags) { - return node.modifiers; + if (e.name) { + if (!getBindingNameVisible(e)) + return; + if (ts.isBindingPattern(e.name)) { + return recreateBindingPattern(e.name); } - return factory.createModifiersFromModifierFlags(newFlags); - } - - function ensureModifierFlags(node: ts.Node): ts.ModifierFlags { - let mask = ts.ModifierFlags.All ^ (ts.ModifierFlags.Public | ts.ModifierFlags.Async | ts.ModifierFlags.Override); // No async and override modifiers in declaration files - let additions = (needsDeclare && !isAlwaysType(node)) ? ts.ModifierFlags.Ambient : ts.ModifierFlags.None; - const parentIsFile = node.parent.kind === ts.SyntaxKind.SourceFile; - if (!parentIsFile || (isBundledEmit && parentIsFile && ts.isExternalModule(node.parent as ts.SourceFile))) { - mask ^= ts.ModifierFlags.Ambient; - additions = ts.ModifierFlags.None; + else { + return factory.createVariableDeclaration(e.name, /*exclamationToken*/ undefined, ensureType(e, /*type*/ undefined), /*initializer*/ undefined); } - return maskModifierFlags(node, mask, additions); } + } - function getTypeAnnotationFromAllAccessorDeclarations(node: ts.AccessorDeclaration, accessors: ts.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 = ts.createGetSymbolAccessibilityDiagnosticForNode(accessors.firstAccessor); - } - 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 = ts.createGetSymbolAccessibilityDiagnosticForNode(accessors.secondAccessor); - } - return accessorType; + function checkName(node: ts.DeclarationDiagnosticProducing) { + let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = ts.createGetSymbolAccessibilityDiagnosticForNodeName(node); } - - function transformHeritageClauses(nodes: ts.NodeArray | undefined) { - return factory.createNodeArray(ts.filter(ts.map(nodes, clause => factory.updateHeritageClause(clause, ts.visitNodes(factory.createNodeArray(ts.filter(clause.types, t => { - return ts.isEntityNameExpression(t.expression) || (clause.token === ts.SyntaxKind.ExtendsKeyword && t.expression.kind === ts.SyntaxKind.NullKeyword); - })), visitDeclarationSubtree))), clause => clause.types && !!clause.types.length)); + errorNameNode = (node as ts.NamedDeclaration).name; + ts.Debug.assert(resolver.isLateBound(ts.getParseTreeNode(node) as ts.Declaration)); // Should only be called with dynamic names + const decl = node as ts.NamedDeclaration as ts.LateBoundDeclaration; + const entityName = decl.name.expression; + checkEntityNameVisibility(entityName, enclosingDeclaration); + if (!suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = oldDiag!; } + errorNameNode = undefined; } - function isAlwaysType(node: ts.Node) { - if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { - return true; - } - return false; + function shouldStripInternal(node: ts.Node) { + return !!stripInternal && !!node && isInternalDeclaration(node, currentSourceFile); } - // Elide "public" modifier, as it is the default - function maskModifiers(node: ts.Node, modifierMask?: ts.ModifierFlags, modifierAdditions?: ts.ModifierFlags): ts.Modifier[] | undefined { - return ts.factory.createModifiersFromModifierFlags(maskModifierFlags(node, modifierMask, modifierAdditions)); + function isScopeMarker(node: ts.Node) { + return ts.isExportAssignment(node) || ts.isExportDeclaration(node); } - function maskModifierFlags(node: ts.Node, modifierMask: ts.ModifierFlags = ts.ModifierFlags.All ^ ts.ModifierFlags.Public, modifierAdditions: ts.ModifierFlags = ts.ModifierFlags.None): ts.ModifierFlags { - let flags = (ts.getEffectiveModifierFlags(node) & modifierMask) | modifierAdditions; - if (flags & ts.ModifierFlags.Default && !(flags & ts.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 ^= ts.ModifierFlags.Export; - } - if (flags & ts.ModifierFlags.Default && flags & ts.ModifierFlags.Ambient) { - flags ^= ts.ModifierFlags.Ambient; // `declare` is never required alongside `default` (and would be an error if printed) - } - return flags; + function hasScopeMarker(statements: readonly ts.Statement[]) { + return ts.some(statements, isScopeMarker); } - function getTypeAnnotationFromAccessor(accessor: ts.AccessorDeclaration): ts.TypeNode | undefined { - if (accessor) { - return accessor.kind === ts.SyntaxKind.GetAccessor - ? accessor.type // Getter - return type - : accessor.parameters.length > 0 - ? accessor.parameters[0].type // Setter parameter type - : undefined; + function ensureModifiers(node: ts.Node): readonly ts.Modifier[] | undefined { + const currentFlags = ts.getEffectiveModifierFlags(node); + const newFlags = ensureModifierFlags(node); + if (currentFlags === newFlags) { + return node.modifiers; } + return factory.createModifiersFromModifierFlags(newFlags); } - type CanHaveLiteralInitializer = ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration; - function canHaveLiteralInitializer(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - return !ts.hasEffectiveModifier(node, ts.ModifierFlags.Private); - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.VariableDeclaration: - return true; + function ensureModifierFlags(node: ts.Node): ts.ModifierFlags { + let mask = ts.ModifierFlags.All ^ (ts.ModifierFlags.Public | ts.ModifierFlags.Async | ts.ModifierFlags.Override); // No async and override modifiers in declaration files + let additions = (needsDeclare && !isAlwaysType(node)) ? ts.ModifierFlags.Ambient : ts.ModifierFlags.None; + const parentIsFile = node.parent.kind === ts.SyntaxKind.SourceFile; + if (!parentIsFile || (isBundledEmit && parentIsFile && ts.isExternalModule(node.parent as ts.SourceFile))) { + mask ^= ts.ModifierFlags.Ambient; + additions = ts.ModifierFlags.None; } - return false; + return maskModifierFlags(node, mask, additions); } - type ProcessedDeclarationStatement = ts.FunctionDeclaration | ts.ModuleDeclaration | ts.ImportEqualsDeclaration | ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.VariableStatement | ts.ImportDeclaration | ts.ExportDeclaration | ts.ExportAssignment; - function isPreservedDeclarationStatement(node: ts.Node): node is ProcessedDeclarationStatement { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ExportAssignment: - return true; + function getTypeAnnotationFromAllAccessorDeclarations(node: ts.AccessorDeclaration, accessors: ts.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 = ts.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 = ts.createGetSymbolAccessibilityDiagnosticForNode(accessors.secondAccessor); + } + return accessorType; } - type ProcessedComponent = ts.ConstructSignatureDeclaration | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.MethodSignature | ts.CallSignatureDeclaration | ts.IndexSignatureDeclaration | ts.VariableDeclaration | ts.TypeParameterDeclaration | ts.ExpressionWithTypeArguments | ts.TypeReferenceNode | ts.ConditionalTypeNode | ts.FunctionTypeNode | ts.ConstructorTypeNode | ts.ImportTypeNode; - function isProcessedComponent(node: ts.Node): node is ProcessedComponent { - switch (node.kind) { - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.TypeParameter: - case ts.SyntaxKind.ExpressionWithTypeArguments: - case ts.SyntaxKind.TypeReference: - case ts.SyntaxKind.ConditionalType: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ImportType: - return true; - } - return false; + function transformHeritageClauses(nodes: ts.NodeArray | undefined) { + return factory.createNodeArray(ts.filter(ts.map(nodes, clause => factory.updateHeritageClause(clause, ts.visitNodes(factory.createNodeArray(ts.filter(clause.types, t => { + return ts.isEntityNameExpression(t.expression) || (clause.token === ts.SyntaxKind.ExtendsKeyword && t.expression.kind === ts.SyntaxKind.NullKeyword); + })), visitDeclarationSubtree))), clause => clause.types && !!clause.types.length)); + } +} + +function isAlwaysType(node: ts.Node) { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { + return true; + } + return false; +} + +// Elide "public" modifier, as it is the default +function maskModifiers(node: ts.Node, modifierMask?: ts.ModifierFlags, modifierAdditions?: ts.ModifierFlags): ts.Modifier[] | undefined { + return ts.factory.createModifiersFromModifierFlags(maskModifierFlags(node, modifierMask, modifierAdditions)); +} + +function maskModifierFlags(node: ts.Node, modifierMask: ts.ModifierFlags = ts.ModifierFlags.All ^ ts.ModifierFlags.Public, modifierAdditions: ts.ModifierFlags = ts.ModifierFlags.None): ts.ModifierFlags { + let flags = (ts.getEffectiveModifierFlags(node) & modifierMask) | modifierAdditions; + if (flags & ts.ModifierFlags.Default && !(flags & ts.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 ^= ts.ModifierFlags.Export; } + if (flags & ts.ModifierFlags.Default && flags & ts.ModifierFlags.Ambient) { + flags ^= ts.ModifierFlags.Ambient; // `declare` is never required alongside `default` (and would be an error if printed) + } + return flags; +} + +function getTypeAnnotationFromAccessor(accessor: ts.AccessorDeclaration): ts.TypeNode | undefined { + if (accessor) { + return accessor.kind === ts.SyntaxKind.GetAccessor + ? accessor.type // Getter - return type + : accessor.parameters.length > 0 + ? accessor.parameters[0].type // Setter parameter type + : undefined; + } +} + +type CanHaveLiteralInitializer = ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterDeclaration; +function canHaveLiteralInitializer(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + return !ts.hasEffectiveModifier(node, ts.ModifierFlags.Private); + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.VariableDeclaration: + return true; + } + return false; +} + +type ProcessedDeclarationStatement = ts.FunctionDeclaration | ts.ModuleDeclaration | ts.ImportEqualsDeclaration | ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.VariableStatement | ts.ImportDeclaration | ts.ExportDeclaration | ts.ExportAssignment; +function isPreservedDeclarationStatement(node: ts.Node): node is ProcessedDeclarationStatement { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.ExportAssignment: + return true; + } + return false; +} + +type ProcessedComponent = ts.ConstructSignatureDeclaration | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.MethodSignature | ts.CallSignatureDeclaration | ts.IndexSignatureDeclaration | ts.VariableDeclaration | ts.TypeParameterDeclaration | ts.ExpressionWithTypeArguments | ts.TypeReferenceNode | ts.ConditionalTypeNode | ts.FunctionTypeNode | ts.ConstructorTypeNode | ts.ImportTypeNode; +function isProcessedComponent(node: ts.Node): node is ProcessedComponent { + switch (node.kind) { + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.ExpressionWithTypeArguments: + case ts.SyntaxKind.TypeReference: + case ts.SyntaxKind.ConditionalType: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ImportType: + return true; + } + return false; +} } diff --git a/src/compiler/transformers/declarations/diagnostics.ts b/src/compiler/transformers/declarations/diagnostics.ts index 494ab9420f3a5..68fcdeb283964 100644 --- a/src/compiler/transformers/declarations/diagnostics.ts +++ b/src/compiler/transformers/declarations/diagnostics.ts @@ -1,56 +1,154 @@ /* @internal */ namespace ts { - export type GetSymbolAccessibilityDiagnostic = (symbolAccessibilityResult: ts.SymbolAccessibilityResult) => (SymbolAccessibilityDiagnostic | undefined); +export type GetSymbolAccessibilityDiagnostic = (symbolAccessibilityResult: ts.SymbolAccessibilityResult) => (SymbolAccessibilityDiagnostic | undefined); - export interface SymbolAccessibilityDiagnostic { - errorNode: ts.Node; - diagnosticMessage: ts.DiagnosticMessage; - typeName?: ts.DeclarationName | ts.QualifiedName; +export interface SymbolAccessibilityDiagnostic { + errorNode: ts.Node; + diagnosticMessage: ts.DiagnosticMessage; + typeName?: ts.DeclarationName | ts.QualifiedName; +} +export type DeclarationDiagnosticProducing = ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.BindingElement | ts.SetAccessorDeclaration | ts.GetAccessorDeclaration | ts.ConstructSignatureDeclaration | ts.CallSignatureDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.FunctionDeclaration | ts.ParameterDeclaration | ts.TypeParameterDeclaration | ts.ExpressionWithTypeArguments | ts.ImportEqualsDeclaration | ts.TypeAliasDeclaration | ts.ConstructorDeclaration | ts.IndexSignatureDeclaration | ts.PropertyAccessExpression | ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag; +export function canProduceDiagnostics(node: ts.Node): node is DeclarationDiagnosticProducing { + return ts.isVariableDeclaration(node) || + ts.isPropertyDeclaration(node) || + ts.isPropertySignature(node) || + ts.isBindingElement(node) || + ts.isSetAccessor(node) || + ts.isGetAccessor(node) || + ts.isConstructSignatureDeclaration(node) || + ts.isCallSignatureDeclaration(node) || + ts.isMethodDeclaration(node) || + ts.isMethodSignature(node) || + ts.isFunctionDeclaration(node) || + ts.isParameter(node) || + ts.isTypeParameterDeclaration(node) || + ts.isExpressionWithTypeArguments(node) || + ts.isImportEqualsDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isIndexSignatureDeclaration(node) || + ts.isPropertyAccessExpression(node) || + ts.isJSDocTypeAlias(node); +} + +export function createGetSymbolAccessibilityDiagnosticForNodeName(node: DeclarationDiagnosticProducing) { + if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) { + return getAccessorNameVisibilityError; + } + else if (ts.isMethodSignature(node) || ts.isMethodDeclaration(node)) { + return getMethodNameVisibilityError; + } + else { + return createGetSymbolAccessibilityDiagnosticForNode(node); } - export type DeclarationDiagnosticProducing = ts.VariableDeclaration | ts.PropertyDeclaration | ts.PropertySignature | ts.BindingElement | ts.SetAccessorDeclaration | ts.GetAccessorDeclaration | ts.ConstructSignatureDeclaration | ts.CallSignatureDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.FunctionDeclaration | ts.ParameterDeclaration | ts.TypeParameterDeclaration | ts.ExpressionWithTypeArguments | ts.ImportEqualsDeclaration | ts.TypeAliasDeclaration | ts.ConstructorDeclaration | ts.IndexSignatureDeclaration | ts.PropertyAccessExpression | ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocEnumTag; - export function canProduceDiagnostics(node: ts.Node): node is DeclarationDiagnosticProducing { - return ts.isVariableDeclaration(node) || - ts.isPropertyDeclaration(node) || - ts.isPropertySignature(node) || - ts.isBindingElement(node) || - ts.isSetAccessor(node) || - ts.isGetAccessor(node) || - ts.isConstructSignatureDeclaration(node) || - ts.isCallSignatureDeclaration(node) || - ts.isMethodDeclaration(node) || - ts.isMethodSignature(node) || - ts.isFunctionDeclaration(node) || - ts.isParameter(node) || - ts.isTypeParameterDeclaration(node) || - ts.isExpressionWithTypeArguments(node) || - ts.isImportEqualsDeclaration(node) || - ts.isTypeAliasDeclaration(node) || - ts.isConstructorDeclaration(node) || - ts.isIndexSignatureDeclaration(node) || - ts.isPropertyAccessExpression(node) || - ts.isJSDocTypeAlias(node); + function getAccessorNameVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { + const diagnosticMessage = getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + } : undefined; } - export function createGetSymbolAccessibilityDiagnosticForNodeName(node: DeclarationDiagnosticProducing) { - if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) { - return getAccessorNameVisibilityError; + function getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { + if (ts.isStatic(node)) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; } - else if (ts.isMethodSignature(node) || ts.isMethodDeclaration(node)) { - return getMethodNameVisibilityError; + else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_private_name_1; } else { - return createGetSymbolAccessibilityDiagnosticForNode(node); + return symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; + } + } + + function getMethodNameVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage = getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + } : undefined; + } + + function getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { + if (ts.isStatic(node)) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_private_name_1; } - function getAccessorNameVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { - const diagnosticMessage = getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - } : undefined; + else { + return symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Method_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Method_0_of_exported_interface_has_or_is_using_private_name_1; } + } +} - function getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { +export function createGetSymbolAccessibilityDiagnosticForNode(node: DeclarationDiagnosticProducing): GetSymbolAccessibilityDiagnostic { + if (ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isPropertyAccessExpression(node) || ts.isBindingElement(node) || ts.isConstructorDeclaration(node)) { + return getVariableDeclarationTypeVisibilityError; + } + else if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) { + return getAccessorDeclarationTypeVisibilityError; + } + else if (ts.isConstructSignatureDeclaration(node) || ts.isCallSignatureDeclaration(node) || ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isFunctionDeclaration(node) || ts.isIndexSignatureDeclaration(node)) { + return getReturnTypeVisibilityError; + } + else if (ts.isParameter(node)) { + if (ts.isParameterPropertyDeclaration(node, node.parent) && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Private)) { + return getVariableDeclarationTypeVisibilityError; + } + return getParameterDeclarationTypeVisibilityError; + } + else if (ts.isTypeParameterDeclaration(node)) { + return getTypeParameterConstraintVisibilityError; + } + else if (ts.isExpressionWithTypeArguments(node)) { + return getHeritageClauseVisibilityError; + } + else if (ts.isImportEqualsDeclaration(node)) { + return getImportEntityNameVisibilityError; + } + else if (ts.isTypeAliasDeclaration(node) || ts.isJSDocTypeAlias(node)) { + return getTypeAliasDeclarationVisibilityError; + } + else { + return ts.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: ts.SymbolAccessibilityResult) { + if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Exported_variable_0_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 === ts.SyntaxKind.PropertyDeclaration || node.kind === ts.SyntaxKind.PropertyAccessExpression || node.kind === ts.SyntaxKind.PropertySignature || + (node.kind === ts.SyntaxKind.Parameter && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Private))) { + // TODO(jfreeman): Deal with computed properties in error reporting. if (ts.isStatic(node)) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? @@ -58,7 +156,7 @@ namespace ts { ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; } - else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.Parameter) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : @@ -66,398 +164,300 @@ namespace ts { 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 ? ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; } } + } - function getMethodNameVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage = getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - } : undefined; - } + function getVariableDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage = getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + } : undefined; + } - function getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult) { + function getAccessorDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { + let diagnosticMessage: ts.DiagnosticMessage; + if (node.kind === ts.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 (ts.isStatic(node)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Public_method_0_of_exported_class_has_or_is_using_private_name_1; + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_private_name_1; } else { - return symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Method_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Method_0_of_exported_interface_has_or_is_using_private_name_1; - } - } - } - - export function createGetSymbolAccessibilityDiagnosticForNode(node: DeclarationDiagnosticProducing): GetSymbolAccessibilityDiagnostic { - if (ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isPropertyAccessExpression(node) || ts.isBindingElement(node) || ts.isConstructorDeclaration(node)) { - return getVariableDeclarationTypeVisibilityError; - } - else if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) { - return getAccessorDeclarationTypeVisibilityError; - } - else if (ts.isConstructSignatureDeclaration(node) || ts.isCallSignatureDeclaration(node) || ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isFunctionDeclaration(node) || ts.isIndexSignatureDeclaration(node)) { - return getReturnTypeVisibilityError; - } - else if (ts.isParameter(node)) { - if (ts.isParameterPropertyDeclaration(node, node.parent) && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Private)) { - return getVariableDeclarationTypeVisibilityError; + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_private_name_1; } - return getParameterDeclarationTypeVisibilityError; - } - else if (ts.isTypeParameterDeclaration(node)) { - return getTypeParameterConstraintVisibilityError; - } - else if (ts.isExpressionWithTypeArguments(node)) { - return getHeritageClauseVisibilityError; - } - else if (ts.isImportEqualsDeclaration(node)) { - return getImportEntityNameVisibilityError; - } - else if (ts.isTypeAliasDeclaration(node) || ts.isJSDocTypeAlias(node)) { - return getTypeAliasDeclarationVisibilityError; } else { - return ts.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: ts.SymbolAccessibilityResult) { - if (node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.BindingElement) { - return symbolAccessibilityResult.errorModuleName ? + if (ts.isStatic(node)) { + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Exported_variable_0_has_or_is_using_private_name_1; + ts.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 : + ts.Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.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 === ts.SyntaxKind.PropertyDeclaration || node.kind === ts.SyntaxKind.PropertyAccessExpression || node.kind === ts.SyntaxKind.PropertySignature || - (node.kind === ts.SyntaxKind.Parameter && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.Private))) { - // TODO(jfreeman): Deal with computed properties in error reporting. - if (ts.isStatic(node)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.Parameter) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - 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 ? - ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; - } + else { + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_private_name_1; } } + return { + diagnosticMessage, + errorNode: (node as ts.NamedDeclaration).name!, + typeName: (node as ts.NamedDeclaration).name + }; + } - function getVariableDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage = getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - } : undefined; - } - - function getAccessorDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { - let diagnosticMessage: ts.DiagnosticMessage; - if (node.kind === ts.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 (ts.isStatic(node)) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_private_name_1; - } - else { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_private_name_1; - } - } - else { + function getReturnTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { + let diagnosticMessage: ts.DiagnosticMessage; + switch (node.kind) { + case ts.SyntaxKind.ConstructSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + + case ts.SyntaxKind.CallSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + + case ts.SyntaxKind.IndexSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: if (ts.isStatic(node)) { diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.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 : - ts.Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_private_name_1; + ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_private_name_0; } - else { + else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_private_name_1; + ts.Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + ts.Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_private_name_0; } - } - return { - diagnosticMessage, - errorNode: (node as ts.NamedDeclaration).name!, - typeName: (node as ts.NamedDeclaration).name - }; - } - - function getReturnTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { - let diagnosticMessage: ts.DiagnosticMessage; - switch (node.kind) { - case ts.SyntaxKind.ConstructSignature: - // Interfaces cannot have return types that cannot be named - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case ts.SyntaxKind.CallSignature: - // Interfaces cannot have return types that cannot be named - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case ts.SyntaxKind.IndexSignature: + else { // Interfaces cannot have return types that cannot be named diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - if (ts.isStatic(node)) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_private_name_0; - } - else if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - ts.Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : - ts.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 ? - ts.Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_private_name_0; - } - break; - - case ts.SyntaxKind.FunctionDeclaration: - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_private_module_1 : - ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_private_name_0; - break; + ts.Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_private_name_0; + } + break; - default: - return ts.Debug.fail("This is unknown kind for signature: " + node.kind); - } + case ts.SyntaxKind.FunctionDeclaration: + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_private_module_1 : + ts.Diagnostics.Return_type_of_exported_function_has_or_is_using_private_name_0; + break; - return { - diagnosticMessage, - errorNode: (node as ts.NamedDeclaration).name || node - }; + default: + return ts.Debug.fail("This is unknown kind for signature: " + node.kind); } - function getParameterDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage: ts.DiagnosticMessage = getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - } : undefined; - } + return { + diagnosticMessage, + errorNode: (node as ts.NamedDeclaration).name || node + }; + } + + function getParameterDeclarationTypeVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage: ts.DiagnosticMessage = getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + } : undefined; + } - function getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult): ts.DiagnosticMessage { - switch (node.parent.kind) { - case ts.SyntaxKind.Constructor: + function getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: ts.SymbolAccessibilityResult): ts.DiagnosticMessage { + switch (node.parent.kind) { + case ts.SyntaxKind.Constructor: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_private_name_1; + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.ConstructorType: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; + case ts.SyntaxKind.CallSignature: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; + case ts.SyntaxKind.IndexSignature: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + ts.Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_private_name_1; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + if (ts.isStatic(node.parent)) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_private_name_1; - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.ConstructorType: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; - case ts.SyntaxKind.CallSignature: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; - case ts.SyntaxKind.IndexSignature: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - ts.Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_private_name_1; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - if (ts.isStatic(node.parent)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.parent.kind === ts.SyntaxKind.ClassDeclaration) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - ts.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 ? - ts.Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; - } - - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionType: + ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.parent.kind === ts.SyntaxKind.ClassDeclaration) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_private_name_1; - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.GetAccessor: + ts.Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + ts.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 === ts.SymbolAccessibility.CannotBeNamed ? - ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_private_module_2 : - ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_private_name_1; + ts.Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; + } - default: - return ts.Debug.fail(`Unknown parent for parameter: ${(ts as any).SyntaxKind[node.parent.kind]}`); - } + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionType: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_exported_function_has_or_is_using_private_name_1; + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.GetAccessor: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === ts.SymbolAccessibility.CannotBeNamed ? + ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_private_module_2 : + ts.Diagnostics.Parameter_0_of_accessor_has_or_is_using_private_name_1; + + default: + return ts.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: ts.DiagnosticMessage; - switch (node.parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_class_has_or_is_using_private_name_1; - break; - - case ts.SyntaxKind.InterfaceDeclaration: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1; - break; - - case ts.SyntaxKind.MappedType: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_mapped_object_type_is_using_private_name_1; - break; - - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ConstructSignature: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; - break; - - case ts.SyntaxKind.CallSignature: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; - break; - - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - if (ts.isStatic(node.parent)) { - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.parent.kind === ts.SyntaxKind.ClassDeclaration) { - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; - } - else { - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; - } - break; - - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.FunctionDeclaration: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_function_has_or_is_using_private_name_1; - break; - - case ts.SyntaxKind.TypeAliasDeclaration: - diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_type_alias_has_or_is_using_private_name_1; - break; - - default: - return ts.Debug.fail("This is unknown parent for type parameter: " + node.parent.kind); - } + function getTypeParameterConstraintVisibilityError(): SymbolAccessibilityDiagnostic { + // Type parameter constraints are named by user so we should always be able to name it + let diagnosticMessage: ts.DiagnosticMessage; + switch (node.parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_class_has_or_is_using_private_name_1; + break; + + case ts.SyntaxKind.InterfaceDeclaration: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1; + break; + + case ts.SyntaxKind.MappedType: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_mapped_object_type_is_using_private_name_1; + break; + + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ConstructSignature: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; + break; + + case ts.SyntaxKind.CallSignature: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; + break; + + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + if (ts.isStatic(node.parent)) { + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.parent.kind === ts.SyntaxKind.ClassDeclaration) { + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; + } + else { + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; + } + break; - return { - diagnosticMessage, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - }; - } + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.FunctionDeclaration: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_function_has_or_is_using_private_name_1; + break; - function getHeritageClauseVisibilityError(): SymbolAccessibilityDiagnostic { - let diagnosticMessage: ts.DiagnosticMessage; - // Heritage clause is written by user so it can always be named - if (ts.isClassDeclaration(node.parent.parent)) { - // Class or Interface implemented/extended is inaccessible - diagnosticMessage = ts.isHeritageClause(node.parent) && node.parent.token === ts.SyntaxKind.ImplementsKeyword ? - ts.Diagnostics.Implements_clause_of_exported_class_0_has_or_is_using_private_name_1 : - node.parent.parent.name ? ts.Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1 : - ts.Diagnostics.extends_clause_of_exported_class_has_or_is_using_private_name_0; - } - else { - // interface is inaccessible - diagnosticMessage = ts.Diagnostics.extends_clause_of_exported_interface_0_has_or_is_using_private_name_1; - } + case ts.SyntaxKind.TypeAliasDeclaration: + diagnosticMessage = ts.Diagnostics.Type_parameter_0_of_exported_type_alias_has_or_is_using_private_name_1; + break; - return { - diagnosticMessage, - errorNode: node, - typeName: ts.getNameOfDeclaration(node.parent.parent as ts.Declaration) - }; + default: + return ts.Debug.fail("This is unknown parent for type parameter: " + node.parent.kind); } - function getImportEntityNameVisibilityError(): SymbolAccessibilityDiagnostic { - return { - diagnosticMessage: ts.Diagnostics.Import_declaration_0_is_using_private_name_1, - errorNode: node, - typeName: (node as ts.NamedDeclaration).name - }; - } + return { + diagnosticMessage, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + }; + } - function getTypeAliasDeclarationVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { - return { - diagnosticMessage: symbolAccessibilityResult.errorModuleName - ? ts.Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1_from_module_2 - : ts.Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1, - errorNode: ts.isJSDocTypeAlias(node) ? ts.Debug.checkDefined(node.typeExpression) : (node as ts.TypeAliasDeclaration).type, - typeName: ts.isJSDocTypeAlias(node) ? ts.getNameOfDeclaration(node) : (node as ts.TypeAliasDeclaration).name, - }; + function getHeritageClauseVisibilityError(): SymbolAccessibilityDiagnostic { + let diagnosticMessage: ts.DiagnosticMessage; + // Heritage clause is written by user so it can always be named + if (ts.isClassDeclaration(node.parent.parent)) { + // Class or Interface implemented/extended is inaccessible + diagnosticMessage = ts.isHeritageClause(node.parent) && node.parent.token === ts.SyntaxKind.ImplementsKeyword ? + ts.Diagnostics.Implements_clause_of_exported_class_0_has_or_is_using_private_name_1 : + node.parent.parent.name ? ts.Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1 : + ts.Diagnostics.extends_clause_of_exported_class_has_or_is_using_private_name_0; } + else { + // interface is inaccessible + diagnosticMessage = ts.Diagnostics.extends_clause_of_exported_interface_0_has_or_is_using_private_name_1; + } + + return { + diagnosticMessage, + errorNode: node, + typeName: ts.getNameOfDeclaration(node.parent.parent as ts.Declaration) + }; + } + + function getImportEntityNameVisibilityError(): SymbolAccessibilityDiagnostic { + return { + diagnosticMessage: ts.Diagnostics.Import_declaration_0_is_using_private_name_1, + errorNode: node, + typeName: (node as ts.NamedDeclaration).name + }; + } + + function getTypeAliasDeclarationVisibilityError(symbolAccessibilityResult: ts.SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { + return { + diagnosticMessage: symbolAccessibilityResult.errorModuleName + ? ts.Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1_from_module_2 + : ts.Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1, + errorNode: ts.isJSDocTypeAlias(node) ? ts.Debug.checkDefined(node.typeExpression) : (node as ts.TypeAliasDeclaration).type, + typeName: ts.isJSDocTypeAlias(node) ? ts.getNameOfDeclaration(node) : (node as ts.TypeAliasDeclaration).name, + }; } } +} diff --git a/src/compiler/transformers/destructuring.ts b/src/compiler/transformers/destructuring.ts index dfeab55e84d16..f542ab10a43e8 100644 --- a/src/compiler/transformers/destructuring.ts +++ b/src/compiler/transformers/destructuring.ts @@ -1,522 +1,522 @@ /*@internal*/ namespace ts { - interface FlattenContext { - context: ts.TransformationContext; - level: FlattenLevel; - downlevelIteration: boolean; - hoistTempVariables: boolean; - hasTransformedPriorElement?: boolean; // indicates whether we've transformed a prior declaration - emitExpression: (value: ts.Expression) => void; - emitBindingOrAssignment: (target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange, original: ts.Node | undefined) => void; - createArrayBindingOrAssignmentPattern: (elements: ts.BindingOrAssignmentElement[]) => ts.ArrayBindingOrAssignmentPattern; - createObjectBindingOrAssignmentPattern: (elements: ts.BindingOrAssignmentElement[]) => ts.ObjectBindingOrAssignmentPattern; - createArrayBindingOrAssignmentElement: (node: ts.Identifier) => ts.BindingOrAssignmentElement; - visitor?: (node: ts.Node) => ts.VisitResult; - } - - export const enum FlattenLevel { - All, - ObjectRest - } +interface FlattenContext { + context: ts.TransformationContext; + level: FlattenLevel; + downlevelIteration: boolean; + hoistTempVariables: boolean; + hasTransformedPriorElement?: boolean; // indicates whether we've transformed a prior declaration + emitExpression: (value: ts.Expression) => void; + emitBindingOrAssignment: (target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange, original: ts.Node | undefined) => void; + createArrayBindingOrAssignmentPattern: (elements: ts.BindingOrAssignmentElement[]) => ts.ArrayBindingOrAssignmentPattern; + createObjectBindingOrAssignmentPattern: (elements: ts.BindingOrAssignmentElement[]) => ts.ObjectBindingOrAssignmentPattern; + createArrayBindingOrAssignmentElement: (node: ts.Identifier) => ts.BindingOrAssignmentElement; + visitor?: (node: ts.Node) => ts.VisitResult; +} - /** - * 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: ts.VariableDeclaration | ts.DestructuringAssignment, visitor: ((node: ts.Node) => ts.VisitResult) | undefined, context: ts.TransformationContext, level: FlattenLevel, needsValue?: boolean, createAssignmentCallback?: (name: ts.Identifier, value: ts.Expression, location?: ts.TextRange) => ts.Expression): ts.Expression { - let location: ts.TextRange = node; - let value: ts.Expression | undefined; - if (ts.isDestructuringAssignment(node)) { - value = node.right; - while (ts.isEmptyArrayLiteral(node.left) || ts.isEmptyObjectLiteral(node.left)) { - if (ts.isDestructuringAssignment(value)) { - location = node = value; - value = node.right; - } - else { - return ts.visitNode(value, visitor, ts.isExpression); - } - } - } +export const enum FlattenLevel { + All, + ObjectRest +} - let expressions: ts.Expression[] | undefined; - const flattenContext: FlattenContext = { - context, - level, - downlevelIteration: !!context.getCompilerOptions().downlevelIteration, - hoistTempVariables: true, - emitExpression, - emitBindingOrAssignment, - createArrayBindingOrAssignmentPattern: elements => makeArrayAssignmentPattern(context.factory, elements), - createObjectBindingOrAssignmentPattern: elements => makeObjectAssignmentPattern(context.factory, elements), - createArrayBindingOrAssignmentElement: makeAssignmentElement, - visitor - }; - - if (value) { - value = ts.visitNode(value, visitor, ts.isExpression); - if (ts.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); +/** + * 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: ts.VariableDeclaration | ts.DestructuringAssignment, visitor: ((node: ts.Node) => ts.VisitResult) | undefined, context: ts.TransformationContext, level: FlattenLevel, needsValue?: boolean, createAssignmentCallback?: (name: ts.Identifier, value: ts.Expression, location?: ts.TextRange) => ts.Expression): ts.Expression { + let location: ts.TextRange = node; + let value: ts.Expression | undefined; + if (ts.isDestructuringAssignment(node)) { + value = node.right; + while (ts.isEmptyArrayLiteral(node.left) || ts.isEmptyObjectLiteral(node.left)) { + if (ts.isDestructuringAssignment(value)) { + location = node = value; + value = node.right; } - else if (ts.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; + else { + return ts.visitNode(value, visitor, ts.isExpression); } } + } - flattenBindingOrAssignmentElement(flattenContext, node, value, location, /*skipInitializer*/ ts.isDestructuringAssignment(node)); + let expressions: ts.Expression[] | undefined; + const flattenContext: FlattenContext = { + context, + level, + downlevelIteration: !!context.getCompilerOptions().downlevelIteration, + hoistTempVariables: true, + emitExpression, + emitBindingOrAssignment, + createArrayBindingOrAssignmentPattern: elements => makeArrayAssignmentPattern(context.factory, elements), + createObjectBindingOrAssignmentPattern: elements => makeObjectAssignmentPattern(context.factory, elements), + createArrayBindingOrAssignmentElement: makeAssignmentElement, + visitor + }; + + if (value) { + value = ts.visitNode(value, visitor, ts.isExpression); + if (ts.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 (ts.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; + } + } - if (value && needsValue) { - if (!ts.some(expressions)) { - return value; - } + flattenBindingOrAssignmentElement(flattenContext, node, value, location, /*skipInitializer*/ ts.isDestructuringAssignment(node)); - expressions.push(value); + if (value && needsValue) { + if (!ts.some(expressions)) { + return value; } - return context.factory.inlineExpressions(expressions!) || context.factory.createOmittedExpression(); + expressions.push(value); + } - function emitExpression(expression: ts.Expression) { - expressions = ts.append(expressions, expression); - } + return context.factory.inlineExpressions(expressions!) || context.factory.createOmittedExpression(); - function emitBindingOrAssignment(target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange, original: ts.Node) { - ts.Debug.assertNode(target, createAssignmentCallback ? ts.isIdentifier : ts.isExpression); - const expression = createAssignmentCallback - ? createAssignmentCallback(target as ts.Identifier, value, location) - : ts.setTextRange(context.factory.createAssignment(ts.visitNode(target as ts.Expression, visitor, ts.isExpression), value), location); - expression.original = original; - emitExpression(expression); - } + function emitExpression(expression: ts.Expression) { + expressions = ts.append(expressions, expression); } - function bindingOrAssignmentElementAssignsToName(element: ts.BindingOrAssignmentElement, escapedName: ts.__String): boolean { - const target = ts.getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 - if (ts.isBindingOrAssignmentPattern(target)) { - return bindingOrAssignmentPatternAssignsToName(target, escapedName); - } - else if (ts.isIdentifier(target)) { - return target.escapedText === escapedName; - } - return false; + function emitBindingOrAssignment(target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange, original: ts.Node) { + ts.Debug.assertNode(target, createAssignmentCallback ? ts.isIdentifier : ts.isExpression); + const expression = createAssignmentCallback + ? createAssignmentCallback(target as ts.Identifier, value, location) + : ts.setTextRange(context.factory.createAssignment(ts.visitNode(target as ts.Expression, visitor, ts.isExpression), value), location); + expression.original = original; + emitExpression(expression); } +} - function bindingOrAssignmentPatternAssignsToName(pattern: ts.BindingOrAssignmentPattern, escapedName: ts.__String): boolean { - const elements = ts.getElementsOfBindingOrAssignmentPattern(pattern); - for (const element of elements) { - if (bindingOrAssignmentElementAssignsToName(element, escapedName)) { - return true; - } - } - return false; +function bindingOrAssignmentElementAssignsToName(element: ts.BindingOrAssignmentElement, escapedName: ts.__String): boolean { + const target = ts.getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 + if (ts.isBindingOrAssignmentPattern(target)) { + return bindingOrAssignmentPatternAssignsToName(target, escapedName); + } + else if (ts.isIdentifier(target)) { + return target.escapedText === escapedName; } + return false; +} - function bindingOrAssignmentElementContainsNonLiteralComputedName(element: ts.BindingOrAssignmentElement): boolean { - const propertyName = ts.tryGetPropertyNameOfBindingOrAssignmentElement(element); - if (propertyName && ts.isComputedPropertyName(propertyName) && !ts.isLiteralExpression(propertyName.expression)) { +function bindingOrAssignmentPatternAssignsToName(pattern: ts.BindingOrAssignmentPattern, escapedName: ts.__String): boolean { + const elements = ts.getElementsOfBindingOrAssignmentPattern(pattern); + for (const element of elements) { + if (bindingOrAssignmentElementAssignsToName(element, escapedName)) { return true; } - const target = ts.getTargetOfBindingOrAssignmentElement(element); - return !!target && ts.isBindingOrAssignmentPattern(target) && bindingOrAssignmentPatternContainsNonLiteralComputedName(target); } + return false; +} - function bindingOrAssignmentPatternContainsNonLiteralComputedName(pattern: ts.BindingOrAssignmentPattern): boolean { - return !!ts.forEach(ts.getElementsOfBindingOrAssignmentPattern(pattern), bindingOrAssignmentElementContainsNonLiteralComputedName); +function bindingOrAssignmentElementContainsNonLiteralComputedName(element: ts.BindingOrAssignmentElement): boolean { + const propertyName = ts.tryGetPropertyNameOfBindingOrAssignmentElement(element); + if (propertyName && ts.isComputedPropertyName(propertyName) && !ts.isLiteralExpression(propertyName.expression)) { + return true; } + const target = ts.getTargetOfBindingOrAssignmentElement(element); + return !!target && ts.isBindingOrAssignmentPattern(target) && bindingOrAssignmentPatternContainsNonLiteralComputedName(target); +} - /** - * 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: ts.VariableDeclaration | ts.ParameterDeclaration, visitor: (node: ts.Node) => ts.VisitResult, context: ts.TransformationContext, level: FlattenLevel, rval?: ts.Expression, hoistTempVariables = false, skipInitializer?: boolean): ts.VariableDeclaration[] { - let pendingExpressions: ts.Expression[] | undefined; - const pendingDeclarations: { - pendingExpressions?: ts.Expression[]; - name: ts.BindingName; - value: ts.Expression; - location?: ts.TextRange; - original?: ts.Node; - }[] = []; - const declarations: ts.VariableDeclaration[] = []; - const flattenContext: FlattenContext = { - context, - level, - downlevelIteration: !!context.getCompilerOptions().downlevelIteration, - hoistTempVariables, - emitExpression, - emitBindingOrAssignment, - createArrayBindingOrAssignmentPattern: elements => makeArrayBindingPattern(context.factory, elements), - createObjectBindingOrAssignmentPattern: elements => makeObjectBindingPattern(context.factory, elements), - createArrayBindingOrAssignmentElement: name => makeBindingElement(context.factory, name), - visitor - }; - - if (ts.isVariableDeclaration(node)) { - let initializer = ts.getInitializerOfBindingOrAssignmentElement(node); - if (initializer && (ts.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, ts.visitNode(initializer, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, initializer); - node = context.factory.updateVariableDeclaration(node, node.name, /*exclamationToken*/ undefined, /*type*/ undefined, initializer); - } +function bindingOrAssignmentPatternContainsNonLiteralComputedName(pattern: ts.BindingOrAssignmentPattern): boolean { + return !!ts.forEach(ts.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: ts.VariableDeclaration | ts.ParameterDeclaration, visitor: (node: ts.Node) => ts.VisitResult, context: ts.TransformationContext, level: FlattenLevel, rval?: ts.Expression, hoistTempVariables = false, skipInitializer?: boolean): ts.VariableDeclaration[] { + let pendingExpressions: ts.Expression[] | undefined; + const pendingDeclarations: { + pendingExpressions?: ts.Expression[]; + name: ts.BindingName; + value: ts.Expression; + location?: ts.TextRange; + original?: ts.Node; + }[] = []; + const declarations: ts.VariableDeclaration[] = []; + const flattenContext: FlattenContext = { + context, + level, + downlevelIteration: !!context.getCompilerOptions().downlevelIteration, + hoistTempVariables, + emitExpression, + emitBindingOrAssignment, + createArrayBindingOrAssignmentPattern: elements => makeArrayBindingPattern(context.factory, elements), + createObjectBindingOrAssignmentPattern: elements => makeObjectBindingPattern(context.factory, elements), + createArrayBindingOrAssignmentElement: name => makeBindingElement(context.factory, name), + visitor + }; + + if (ts.isVariableDeclaration(node)) { + let initializer = ts.getInitializerOfBindingOrAssignmentElement(node); + if (initializer && (ts.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, ts.visitNode(initializer, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, initializer); + node = context.factory.updateVariableDeclaration(node, node.name, /*exclamationToken*/ undefined, /*type*/ undefined, initializer); } + } - flattenBindingOrAssignmentElement(flattenContext, node, rval, node, skipInitializer); - if (pendingExpressions) { - const temp = context.factory.createTempVariable(/*recordTempVariable*/ undefined); - if (hoistTempVariables) { - const value = context.factory.inlineExpressions(pendingExpressions); - pendingExpressions = undefined; - emitBindingOrAssignment(temp, value, /*location*/ undefined, /*original*/ undefined); - } - else { - context.hoistVariableDeclaration(temp); - const pendingDeclaration = ts.last(pendingDeclarations); - pendingDeclaration.pendingExpressions = ts.append(pendingDeclaration.pendingExpressions, context.factory.createAssignment(temp, pendingDeclaration.value)); - ts.addRange(pendingDeclaration.pendingExpressions, pendingExpressions); - pendingDeclaration.value = temp; - } + flattenBindingOrAssignmentElement(flattenContext, node, rval, node, skipInitializer); + if (pendingExpressions) { + const temp = context.factory.createTempVariable(/*recordTempVariable*/ undefined); + if (hoistTempVariables) { + const value = context.factory.inlineExpressions(pendingExpressions); + pendingExpressions = undefined; + emitBindingOrAssignment(temp, value, /*location*/ undefined, /*original*/ undefined); } - for (const { pendingExpressions, name, value, location, original } of pendingDeclarations) { - const variable = context.factory.createVariableDeclaration(name, - /*exclamationToken*/ undefined, - /*type*/ undefined, pendingExpressions ? context.factory.inlineExpressions(ts.append(pendingExpressions, value)) : value); - variable.original = original; - ts.setTextRange(variable, location); - declarations.push(variable); + else { + context.hoistVariableDeclaration(temp); + const pendingDeclaration = ts.last(pendingDeclarations); + pendingDeclaration.pendingExpressions = ts.append(pendingDeclaration.pendingExpressions, context.factory.createAssignment(temp, pendingDeclaration.value)); + ts.addRange(pendingDeclaration.pendingExpressions, pendingExpressions); + pendingDeclaration.value = temp; } - return declarations; + } + for (const { pendingExpressions, name, value, location, original } of pendingDeclarations) { + const variable = context.factory.createVariableDeclaration(name, + /*exclamationToken*/ undefined, + /*type*/ undefined, pendingExpressions ? context.factory.inlineExpressions(ts.append(pendingExpressions, value)) : value); + variable.original = original; + ts.setTextRange(variable, location); + declarations.push(variable); + } + return declarations; - function emitExpression(value: ts.Expression) { - pendingExpressions = ts.append(pendingExpressions, value); - } + function emitExpression(value: ts.Expression) { + pendingExpressions = ts.append(pendingExpressions, value); + } - function emitBindingOrAssignment(target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange | undefined, original: ts.Node | undefined) { - ts.Debug.assertNode(target, ts.isBindingName); - if (pendingExpressions) { - value = context.factory.inlineExpressions(ts.append(pendingExpressions, value)); - pendingExpressions = undefined; - } - pendingDeclarations.push({ pendingExpressions, name: target, value, location, original }); + function emitBindingOrAssignment(target: ts.BindingOrAssignmentElementTarget, value: ts.Expression, location: ts.TextRange | undefined, original: ts.Node | undefined) { + ts.Debug.assertNode(target, ts.isBindingName); + if (pendingExpressions) { + value = context.factory.inlineExpressions(ts.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: ts.BindingOrAssignmentElement, value: ts.Expression | undefined, location: ts.TextRange, skipInitializer?: boolean) { - const bindingTarget = ts.getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 - if (!skipInitializer) { - const initializer = ts.visitNode(ts.getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, ts.isExpression); - if (initializer) { - // Combine value and initializer - if (value) { - value = createDefaultValueCheck(flattenContext, value, initializer, location); - // If 'value' is not a simple expression, it could contain side-effecting code that should evaluate before an object or array binding pattern. - if (!ts.isSimpleInlineableExpression(initializer) && ts.isBindingOrAssignmentPattern(bindingTarget)) { - value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); - } - } - else { - value = initializer; +/** + * 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: ts.BindingOrAssignmentElement, value: ts.Expression | undefined, location: ts.TextRange, skipInitializer?: boolean) { + const bindingTarget = ts.getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 + if (!skipInitializer) { + const initializer = ts.visitNode(ts.getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, ts.isExpression); + if (initializer) { + // Combine value and initializer + if (value) { + value = createDefaultValueCheck(flattenContext, value, initializer, location); + // If 'value' is not a simple expression, it could contain side-effecting code that should evaluate before an object or array binding pattern. + if (!ts.isSimpleInlineableExpression(initializer) && ts.isBindingOrAssignmentPattern(bindingTarget)) { + value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); } } - else if (!value) { - // Use 'void 0' in absence of value and initializer - value = flattenContext.context.factory.createVoidZero(); + else { + value = initializer; } } - if (ts.isObjectBindingOrAssignmentPattern(bindingTarget)) { - flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); - } - else if (ts.isArrayBindingOrAssignmentPattern(bindingTarget)) { - flattenArrayBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); - } - else { - flattenContext.emitBindingOrAssignment(bindingTarget, value!, location, /*original*/ element); // TODO: GH#18217 + else if (!value) { + // Use 'void 0' in absence of value and initializer + value = flattenContext.context.factory.createVoidZero(); } } + if (ts.isObjectBindingOrAssignmentPattern(bindingTarget)) { + flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); + } + else if (ts.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. - */ - function flattenObjectBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: ts.BindingOrAssignmentElement, pattern: ts.ObjectBindingOrAssignmentPattern, value: ts.Expression, location: ts.TextRange) { - const elements = ts.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 = !ts.isDeclarationBindingElement(parent) || numElements !== 0; - value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); - } - let bindingElements: ts.BindingOrAssignmentElement[] | undefined; - let computedTempVariables: ts.Expression[] | undefined; - for (let i = 0; i < numElements; i++) { - const element = elements[i]; - if (!ts.getRestIndicatorOfBindingOrAssignmentElement(element)) { - const propertyName = ts.getPropertyNameOfBindingOrAssignmentElement(element)!; - if (flattenContext.level >= FlattenLevel.ObjectRest - && !(element.transformFlags & (ts.TransformFlags.ContainsRestOrSpread | ts.TransformFlags.ContainsObjectRestOrSpread)) - && !(ts.getTargetOfBindingOrAssignmentElement(element)!.transformFlags & (ts.TransformFlags.ContainsRestOrSpread | ts.TransformFlags.ContainsObjectRestOrSpread)) - && !ts.isComputedPropertyName(propertyName)) { - bindingElements = ts.append(bindingElements, ts.visitNode(element, flattenContext.visitor)); - } - else { - if (bindingElements) { - flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); - bindingElements = undefined; - } - const rhsValue = createDestructuringPropertyAccess(flattenContext, value, propertyName); - if (ts.isComputedPropertyName(propertyName)) { - computedTempVariables = ts.append(computedTempVariables, (rhsValue as ts.ElementAccessExpression).argumentExpression); - } - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); - } +/** + * 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: ts.BindingOrAssignmentElement, pattern: ts.ObjectBindingOrAssignmentPattern, value: ts.Expression, location: ts.TextRange) { + const elements = ts.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 = !ts.isDeclarationBindingElement(parent) || numElements !== 0; + value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); + } + let bindingElements: ts.BindingOrAssignmentElement[] | undefined; + let computedTempVariables: ts.Expression[] | undefined; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + if (!ts.getRestIndicatorOfBindingOrAssignmentElement(element)) { + const propertyName = ts.getPropertyNameOfBindingOrAssignmentElement(element)!; + if (flattenContext.level >= FlattenLevel.ObjectRest + && !(element.transformFlags & (ts.TransformFlags.ContainsRestOrSpread | ts.TransformFlags.ContainsObjectRestOrSpread)) + && !(ts.getTargetOfBindingOrAssignmentElement(element)!.transformFlags & (ts.TransformFlags.ContainsRestOrSpread | ts.TransformFlags.ContainsObjectRestOrSpread)) + && !ts.isComputedPropertyName(propertyName)) { + bindingElements = ts.append(bindingElements, ts.visitNode(element, flattenContext.visitor)); } - else if (i === numElements - 1) { + else { if (bindingElements) { flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); bindingElements = undefined; } - const rhsValue = flattenContext.context.getEmitHelperFactory().createRestHelper(value, elements, computedTempVariables, pattern); - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, element); + const rhsValue = createDestructuringPropertyAccess(flattenContext, value, propertyName); + if (ts.isComputedPropertyName(propertyName)) { + computedTempVariables = ts.append(computedTempVariables, (rhsValue as ts.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 = flattenContext.context.getEmitHelperFactory().createRestHelper(value, elements, computedTempVariables, pattern); + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, 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. - */ - function flattenArrayBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: ts.BindingOrAssignmentElement, pattern: ts.ArrayBindingOrAssignmentPattern, value: ts.Expression, location: ts.TextRange) { - const elements = ts.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, ts.setTextRange(flattenContext.context.getEmitHelperFactory().createReadHelper(value, numElements > 0 && ts.getRestIndicatorOfBindingOrAssignmentElement(elements[numElements - 1]) - ? undefined - : numElements), location), - /*reuseIdentifierExpressions*/ false, location); - } - else if (numElements !== 1 && (flattenContext.level < FlattenLevel.ObjectRest || numElements === 0) - || ts.every(elements, ts.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 = !ts.isDeclarationBindingElement(parent) || numElements !== 0; - value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); - } - let bindingElements: ts.BindingOrAssignmentElement[] | undefined; - let restContainingElements: [ - ts.Identifier, - ts.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 & ts.TransformFlags.ContainsObjectRestOrSpread || flattenContext.hasTransformedPriorElement && !isSimpleBindingOrAssignmentElement(element)) { - flattenContext.hasTransformedPriorElement = true; - const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined); - if (flattenContext.hoistTempVariables) { - flattenContext.context.hoistVariableDeclaration(temp); - } - - restContainingElements = ts.append(restContainingElements, [temp, element] as [ - ts.Identifier, - ts.BindingOrAssignmentElement - ]); - bindingElements = ts.append(bindingElements, flattenContext.createArrayBindingOrAssignmentElement(temp)); - } - else { - bindingElements = ts.append(bindingElements, 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: ts.BindingOrAssignmentElement, pattern: ts.ArrayBindingOrAssignmentPattern, value: ts.Expression, location: ts.TextRange) { + const elements = ts.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, ts.setTextRange(flattenContext.context.getEmitHelperFactory().createReadHelper(value, numElements > 0 && ts.getRestIndicatorOfBindingOrAssignmentElement(elements[numElements - 1]) + ? undefined + : numElements), location), + /*reuseIdentifierExpressions*/ false, location); + } + else if (numElements !== 1 && (flattenContext.level < FlattenLevel.ObjectRest || numElements === 0) + || ts.every(elements, ts.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 = !ts.isDeclarationBindingElement(parent) || numElements !== 0; + value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); + } + let bindingElements: ts.BindingOrAssignmentElement[] | undefined; + let restContainingElements: [ + ts.Identifier, + ts.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 & ts.TransformFlags.ContainsObjectRestOrSpread || flattenContext.hasTransformedPriorElement && !isSimpleBindingOrAssignmentElement(element)) { + flattenContext.hasTransformedPriorElement = true; + const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined); + if (flattenContext.hoistTempVariables) { + flattenContext.context.hoistVariableDeclaration(temp); } + + restContainingElements = ts.append(restContainingElements, [temp, element] as [ + ts.Identifier, + ts.BindingOrAssignmentElement + ]); + bindingElements = ts.append(bindingElements, flattenContext.createArrayBindingOrAssignmentElement(temp)); } - else if (ts.isOmittedExpression(element)) { - continue; - } - else if (!ts.getRestIndicatorOfBindingOrAssignmentElement(element)) { - const rhsValue = flattenContext.context.factory.createElementAccessExpression(value, i); - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); - } - else if (i === numElements - 1) { - const rhsValue = flattenContext.context.factory.createArraySliceCall(value, i); - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); + else { + bindingElements = ts.append(bindingElements, element); } } - if (bindingElements) { - flattenContext.emitBindingOrAssignment(flattenContext.createArrayBindingOrAssignmentPattern(bindingElements), value, location, pattern); + else if (ts.isOmittedExpression(element)) { + continue; } - if (restContainingElements) { - for (const [id, element] of restContainingElements) { - flattenBindingOrAssignmentElement(flattenContext, element, id, element); - } + else if (!ts.getRestIndicatorOfBindingOrAssignmentElement(element)) { + const rhsValue = flattenContext.context.factory.createElementAccessExpression(value, i); + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); + } + else if (i === numElements - 1) { + const rhsValue = flattenContext.context.factory.createArraySliceCall(value, i); + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); } } - - function isSimpleBindingOrAssignmentElement(element: ts.BindingOrAssignmentElement): boolean { - const target = ts.getTargetOfBindingOrAssignmentElement(element); - if (!target || ts.isOmittedExpression(target)) - return true; - const propertyName = ts.tryGetPropertyNameOfBindingOrAssignmentElement(element); - if (propertyName && !ts.isPropertyNameLiteral(propertyName)) - return false; - const initializer = ts.getInitializerOfBindingOrAssignmentElement(element); - if (initializer && !ts.isSimpleInlineableExpression(initializer)) - return false; - if (ts.isBindingOrAssignmentPattern(target)) - return ts.every(ts.getElementsOfBindingOrAssignmentPattern(target), isSimpleBindingOrAssignmentElement); - return ts.isIdentifier(target); + if (bindingElements) { + flattenContext.emitBindingOrAssignment(flattenContext.createArrayBindingOrAssignmentPattern(bindingElements), value, location, pattern); } - - /** - * 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: ts.Expression, defaultValue: ts.Expression, location: ts.TextRange): ts.Expression { - value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); - return flattenContext.context.factory.createConditionalExpression(flattenContext.context.factory.createTypeCheck(value, "undefined"), /*questionToken*/ undefined, defaultValue, /*colonToken*/ undefined, value); + if (restContainingElements) { + for (const [id, element] of restContainingElements) { + flattenBindingOrAssignmentElement(flattenContext, element, id, element); + } } +} - /** - * 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: ts.Expression, propertyName: ts.PropertyName): ts.LeftHandSideExpression { - if (ts.isComputedPropertyName(propertyName)) { - const argumentExpression = ensureIdentifier(flattenContext, ts.visitNode(propertyName.expression, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, /*location*/ propertyName); - return flattenContext.context.factory.createElementAccessExpression(value, argumentExpression); - } - else if (ts.isStringOrNumericLiteralLike(propertyName)) { - const argumentExpression = ts.factory.cloneNode(propertyName); - return flattenContext.context.factory.createElementAccessExpression(value, argumentExpression); - } - else { - const name = flattenContext.context.factory.createIdentifier(ts.idText(propertyName)); - return flattenContext.context.factory.createPropertyAccessExpression(value, name); - } +function isSimpleBindingOrAssignmentElement(element: ts.BindingOrAssignmentElement): boolean { + const target = ts.getTargetOfBindingOrAssignmentElement(element); + if (!target || ts.isOmittedExpression(target)) + return true; + const propertyName = ts.tryGetPropertyNameOfBindingOrAssignmentElement(element); + if (propertyName && !ts.isPropertyNameLiteral(propertyName)) + return false; + const initializer = ts.getInitializerOfBindingOrAssignmentElement(element); + if (initializer && !ts.isSimpleInlineableExpression(initializer)) + return false; + if (ts.isBindingOrAssignmentPattern(target)) + return ts.every(ts.getElementsOfBindingOrAssignmentPattern(target), isSimpleBindingOrAssignmentElement); + return ts.isIdentifier(target); +} + +/** + * 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: ts.Expression, defaultValue: ts.Expression, location: ts.TextRange): ts.Expression { + value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); + return flattenContext.context.factory.createConditionalExpression(flattenContext.context.factory.createTypeCheck(value, "undefined"), /*questionToken*/ undefined, defaultValue, /*colonToken*/ undefined, 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: ts.Expression, propertyName: ts.PropertyName): ts.LeftHandSideExpression { + if (ts.isComputedPropertyName(propertyName)) { + const argumentExpression = ensureIdentifier(flattenContext, ts.visitNode(propertyName.expression, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, /*location*/ propertyName); + return flattenContext.context.factory.createElementAccessExpression(value, argumentExpression); } + else if (ts.isStringOrNumericLiteralLike(propertyName)) { + const argumentExpression = ts.factory.cloneNode(propertyName); + return flattenContext.context.factory.createElementAccessExpression(value, argumentExpression); + } + else { + const name = flattenContext.context.factory.createIdentifier(ts.idText(propertyName)); + return flattenContext.context.factory.createPropertyAccessExpression(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. - */ - function ensureIdentifier(flattenContext: FlattenContext, value: ts.Expression, reuseIdentifierExpressions: boolean, location: ts.TextRange) { - if (ts.isIdentifier(value) && reuseIdentifierExpressions) { - return value; +/** + * 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: ts.Expression, reuseIdentifierExpressions: boolean, location: ts.TextRange) { + if (ts.isIdentifier(value) && reuseIdentifierExpressions) { + return value; + } + else { + const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined); + if (flattenContext.hoistTempVariables) { + flattenContext.context.hoistVariableDeclaration(temp); + flattenContext.emitExpression(ts.setTextRange(flattenContext.context.factory.createAssignment(temp, value), location)); } else { - const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined); - if (flattenContext.hoistTempVariables) { - flattenContext.context.hoistVariableDeclaration(temp); - flattenContext.emitExpression(ts.setTextRange(flattenContext.context.factory.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(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { - ts.Debug.assertEachNode(elements, ts.isArrayBindingElement); - return factory.createArrayBindingPattern(elements as ts.ArrayBindingElement[]); - } +function makeArrayBindingPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { + ts.Debug.assertEachNode(elements, ts.isArrayBindingElement); + return factory.createArrayBindingPattern(elements as ts.ArrayBindingElement[]); +} - function makeArrayAssignmentPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { - return factory.createArrayLiteralExpression(ts.map(elements, factory.converters.convertToArrayAssignmentElement)); - } +function makeArrayAssignmentPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { + return factory.createArrayLiteralExpression(ts.map(elements, factory.converters.convertToArrayAssignmentElement)); +} - function makeObjectBindingPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { - ts.Debug.assertEachNode(elements, ts.isBindingElement); - return factory.createObjectBindingPattern(elements as ts.BindingElement[]); - } +function makeObjectBindingPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { + ts.Debug.assertEachNode(elements, ts.isBindingElement); + return factory.createObjectBindingPattern(elements as ts.BindingElement[]); +} - function makeObjectAssignmentPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { - return factory.createObjectLiteralExpression(ts.map(elements, factory.converters.convertToObjectAssignmentElement)); - } +function makeObjectAssignmentPattern(factory: ts.NodeFactory, elements: ts.BindingOrAssignmentElement[]) { + return factory.createObjectLiteralExpression(ts.map(elements, factory.converters.convertToObjectAssignmentElement)); +} - function makeBindingElement(factory: ts.NodeFactory, name: ts.Identifier) { - return factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name); - } +function makeBindingElement(factory: ts.NodeFactory, name: ts.Identifier) { + return factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name); +} - function makeAssignmentElement(name: ts.Identifier) { - return name; - } +function makeAssignmentElement(name: ts.Identifier) { + return name; +} } diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index 7a4c00621f6c7..90c8d10a5bf43 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -1,3716 +1,3716 @@ /*@internal*/ namespace ts { - const enum ES2015SubstitutionFlags { - /** Enables substitutions for captured `this` */ - CapturedThis = 1 << 0, - /** Enables substitutions for block-scoped bindings. */ - BlockScopedBindings = 1 << 1 - } +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. - */ - interface LoopOutParameter { - flags: LoopOutParameterFlags; - originalName: ts.Identifier; - outParamName: ts.Identifier; - } - - const enum LoopOutParameterFlags { - Body = 1 << 0, - Initializer = 1 << 1 - } - - const enum CopyDirection { - ToOriginal, - ToOutParameter - } - - const enum Jump { - Break = 1 << 1, - Continue = 1 << 2, - Return = 1 << 3 - } - - 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.ESMap; - /* - * 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
" - const lessThanToken = ts.getTokenAtPosition(sourceFile, pos); - const firstJsxElementOrOpenElement = lessThanToken.parent; - let binaryExpr = firstJsxElementOrOpenElement.parent; - if (!ts.isBinaryExpression(binaryExpr)) { - // In case the start element is a JsxSelfClosingElement, it the end. - // For JsxOpenElement, find one more parent - binaryExpr = binaryExpr.parent; - if (!ts.isBinaryExpression(binaryExpr)) - return undefined; - } - if (!ts.nodeIsMissing(binaryExpr.operatorToken)) +function findNodeToFix(sourceFile: ts.SourceFile, pos: number): ts.BinaryExpression | undefined { + // The error always at 1st token that is "<" in "" + const lessThanToken = ts.getTokenAtPosition(sourceFile, pos); + const firstJsxElementOrOpenElement = lessThanToken.parent; + let binaryExpr = firstJsxElementOrOpenElement.parent; + if (!ts.isBinaryExpression(binaryExpr)) { + // In case the start element is a JsxSelfClosingElement, it the end. + // For JsxOpenElement, find one more parent + binaryExpr = binaryExpr.parent; + if (!ts.isBinaryExpression(binaryExpr)) return undefined; - return binaryExpr; } + if (!ts.nodeIsMissing(binaryExpr.operatorToken)) + return undefined; + return binaryExpr; +} - function doChange(changeTracker: ts.textChanges.ChangeTracker, sf: ts.SourceFile, node: ts.Node) { - const jsx = flattenInvalidBinaryExpr(node); - if (jsx) - changeTracker.replaceNode(sf, node, ts.factory.createJsxFragment(ts.factory.createJsxOpeningFragment(), jsx, ts.factory.createJsxJsxClosingFragment())); - } - // The invalid syntax is constructed as - // InvalidJsxTree :: One of - // JsxElement CommaToken InvalidJsxTree - // JsxElement CommaToken JsxElement - function flattenInvalidBinaryExpr(node: ts.Node): ts.JsxChild[] | undefined { - const children: ts.JsxChild[] = []; - let current = node; - while (true) { - if (ts.isBinaryExpression(current) && ts.nodeIsMissing(current.operatorToken) && current.operatorToken.kind === ts.SyntaxKind.CommaToken) { - children.push(current.left as ts.JsxChild); - if (ts.isJsxChild(current.right)) { - children.push(current.right); - // Indicates the tree has go to the bottom - return children; - } - else if (ts.isBinaryExpression(current.right)) { - current = current.right; - continue; - } - // Unreachable case - else - return undefined; +function doChange(changeTracker: ts.textChanges.ChangeTracker, sf: ts.SourceFile, node: ts.Node) { + const jsx = flattenInvalidBinaryExpr(node); + if (jsx) + changeTracker.replaceNode(sf, node, ts.factory.createJsxFragment(ts.factory.createJsxOpeningFragment(), jsx, ts.factory.createJsxJsxClosingFragment())); +} +// The invalid syntax is constructed as +// InvalidJsxTree :: One of +// JsxElement CommaToken InvalidJsxTree +// JsxElement CommaToken JsxElement +function flattenInvalidBinaryExpr(node: ts.Node): ts.JsxChild[] | undefined { + const children: ts.JsxChild[] = []; + let current = node; + while (true) { + if (ts.isBinaryExpression(current) && ts.nodeIsMissing(current.operatorToken) && current.operatorToken.kind === ts.SyntaxKind.CommaToken) { + children.push(current.left as ts.JsxChild); + if (ts.isJsxChild(current.right)) { + children.push(current.right); + // Indicates the tree has go to the bottom + return children; + } + else if (ts.isBinaryExpression(current.right)) { + current = current.right; + continue; } // Unreachable case else return undefined; } + // Unreachable case + else + return undefined; } } +} diff --git a/src/services/completions.ts b/src/services/completions.ts index 102733b45b9c2..d440072d61c17 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1,4046 +1,4046 @@ /* @internal */ namespace ts.Completions { - // Exported only for tests - export const moduleSpecifierResolutionLimit = 100; - export const moduleSpecifierResolutionCacheAttemptLimit = 1000; - - export type Log = (message: string) => void; - - export type SortText = string & { - __sortText: any; - }; - export const SortText = { - // Presets - LocalDeclarationPriority: "10" as SortText, - LocationPriority: "11" as SortText, - OptionalMember: "12" as SortText, - MemberDeclaredBySpreadAssignment: "13" as SortText, - SuggestedClassMembers: "14" as SortText, - GlobalsOrKeywords: "15" as SortText, - AutoImportSuggestions: "16" as SortText, - ClassMemberSnippets: "17" as SortText, - JavascriptIdentifiers: "18" as SortText, - - // Transformations - Deprecated(sortText: SortText): SortText { - return "z" + sortText as SortText; - }, - - ObjectLiteralProperty(presetSortText: SortText, symbolDisplayName: string): SortText { - return `${presetSortText}\0${symbolDisplayName}\0` as SortText; - }, - - SortBelow(sortText: SortText): SortText { - return sortText + "1" as SortText; - }, - }; - - /** - * Special values for `CompletionInfo['source']` used to disambiguate - * completion items with the same `name`. (Each completion item must - * have a unique name/source combination, because those two fields - * comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. - * - * When the completion item is an auto-import suggestion, the source - * is the module specifier of the suggestion. To avoid collisions, - * the values here should not be a module specifier we would ever - * generate for an auto-import. - */ - export enum CompletionSource { - /** Completions that require `this.` insertion text */ - ThisProperty = "ThisProperty/", - /** Auto-import that comes attached to a class member snippet */ - ClassMemberSnippet = "ClassMemberSnippet/", - /** A type-only import that needs to be promoted in order to be used at the completion location */ - TypeOnlyAlias = "TypeOnlyAlias/", - /** Auto-import that comes attached to an object literal method snippet */ - ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/" - } - - const enum SymbolOriginInfoKind { - ThisType = 1 << 0, - SymbolMember = 1 << 1, - Export = 1 << 2, - Promise = 1 << 3, - Nullable = 1 << 4, - ResolvedExport = 1 << 5, - TypeOnlyAlias = 1 << 6, - ObjectLiteralMethod = 1 << 7, - - SymbolMemberNoExport = SymbolMember, - SymbolMemberExport = SymbolMember | Export - } - - interface SymbolOriginInfo { - kind: SymbolOriginInfoKind; - isDefaultExport?: boolean; - isFromPackageJson?: boolean; - fileName?: string; - } - - interface SymbolOriginInfoExport extends SymbolOriginInfo { - symbolName: string; - moduleSymbol: ts.Symbol; - isDefaultExport: boolean; - exportName: string; - exportMapKey: string; - } - - interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo { - symbolName: string; - moduleSymbol: ts.Symbol; - exportName: string; - moduleSpecifier: string; - } - - interface SymbolOriginInfoTypeOnlyAlias extends SymbolOriginInfo { - declaration: ts.TypeOnlyAliasDeclaration; - } +// Exported only for tests +export const moduleSpecifierResolutionLimit = 100; +export const moduleSpecifierResolutionCacheAttemptLimit = 1000; + +export type Log = (message: string) => void; + +export type SortText = string & { + __sortText: any; +}; +export const SortText = { + // Presets + LocalDeclarationPriority: "10" as SortText, + LocationPriority: "11" as SortText, + OptionalMember: "12" as SortText, + MemberDeclaredBySpreadAssignment: "13" as SortText, + SuggestedClassMembers: "14" as SortText, + GlobalsOrKeywords: "15" as SortText, + AutoImportSuggestions: "16" as SortText, + ClassMemberSnippets: "17" as SortText, + JavascriptIdentifiers: "18" as SortText, + + // Transformations + Deprecated(sortText: SortText): SortText { + return "z" + sortText as SortText; + }, + + ObjectLiteralProperty(presetSortText: SortText, symbolDisplayName: string): SortText { + return `${presetSortText}\0${symbolDisplayName}\0` as SortText; + }, + + SortBelow(sortText: SortText): SortText { + return sortText + "1" as SortText; + }, +}; + +/** + * Special values for `CompletionInfo['source']` used to disambiguate + * completion items with the same `name`. (Each completion item must + * have a unique name/source combination, because those two fields + * comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. + * + * When the completion item is an auto-import suggestion, the source + * is the module specifier of the suggestion. To avoid collisions, + * the values here should not be a module specifier we would ever + * generate for an auto-import. + */ +export enum CompletionSource { + /** Completions that require `this.` insertion text */ + ThisProperty = "ThisProperty/", + /** Auto-import that comes attached to a class member snippet */ + ClassMemberSnippet = "ClassMemberSnippet/", + /** A type-only import that needs to be promoted in order to be used at the completion location */ + TypeOnlyAlias = "TypeOnlyAlias/", + /** Auto-import that comes attached to an object literal method snippet */ + ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/" +} - interface SymbolOriginInfoObjectLiteralMethod extends SymbolOriginInfo { - insertText: string; - labelDetails: ts.CompletionEntryLabelDetails; - isSnippet?: true; - } +const enum SymbolOriginInfoKind { + ThisType = 1 << 0, + SymbolMember = 1 << 1, + Export = 1 << 2, + Promise = 1 << 3, + Nullable = 1 << 4, + ResolvedExport = 1 << 5, + TypeOnlyAlias = 1 << 6, + ObjectLiteralMethod = 1 << 7, + + SymbolMemberNoExport = SymbolMember, + SymbolMemberExport = SymbolMember | Export +} - function originIsThisType(origin: SymbolOriginInfo): boolean { - return !!(origin.kind & SymbolOriginInfoKind.ThisType); - } +interface SymbolOriginInfo { + kind: SymbolOriginInfoKind; + isDefaultExport?: boolean; + isFromPackageJson?: boolean; + fileName?: string; +} - function originIsSymbolMember(origin: SymbolOriginInfo): boolean { - return !!(origin.kind & SymbolOriginInfoKind.SymbolMember); - } +interface SymbolOriginInfoExport extends SymbolOriginInfo { + symbolName: string; + moduleSymbol: ts.Symbol; + isDefaultExport: boolean; + exportName: string; + exportMapKey: string; +} - function originIsExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { - return !!(origin && origin.kind & SymbolOriginInfoKind.Export); - } +interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo { + symbolName: string; + moduleSymbol: ts.Symbol; + exportName: string; + moduleSpecifier: string; +} - function originIsResolvedExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoResolvedExport { - return !!(origin && origin.kind === SymbolOriginInfoKind.ResolvedExport); - } +interface SymbolOriginInfoTypeOnlyAlias extends SymbolOriginInfo { + declaration: ts.TypeOnlyAliasDeclaration; +} - function originIncludesSymbolName(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport | SymbolOriginInfoResolvedExport { - return originIsExport(origin) || originIsResolvedExport(origin); - } +interface SymbolOriginInfoObjectLiteralMethod extends SymbolOriginInfo { + insertText: string; + labelDetails: ts.CompletionEntryLabelDetails; + isSnippet?: true; +} - function originIsPackageJsonImport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { - return (originIsExport(origin) || originIsResolvedExport(origin)) && !!origin.isFromPackageJson; - } +function originIsThisType(origin: SymbolOriginInfo): boolean { + return !!(origin.kind & SymbolOriginInfoKind.ThisType); +} - function originIsPromise(origin: SymbolOriginInfo): boolean { - return !!(origin.kind & SymbolOriginInfoKind.Promise); - } +function originIsSymbolMember(origin: SymbolOriginInfo): boolean { + return !!(origin.kind & SymbolOriginInfoKind.SymbolMember); +} - function originIsNullableMember(origin: SymbolOriginInfo): boolean { - return !!(origin.kind & SymbolOriginInfoKind.Nullable); - } +function originIsExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { + return !!(origin && origin.kind & SymbolOriginInfoKind.Export); +} - function originIsTypeOnlyAlias(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoTypeOnlyAlias { - return !!(origin && origin.kind & SymbolOriginInfoKind.TypeOnlyAlias); - } +function originIsResolvedExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoResolvedExport { + return !!(origin && origin.kind === SymbolOriginInfoKind.ResolvedExport); +} - function originIsObjectLiteralMethod(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoObjectLiteralMethod { - return !!(origin && origin.kind & SymbolOriginInfoKind.ObjectLiteralMethod); - } +function originIncludesSymbolName(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport | SymbolOriginInfoResolvedExport { + return originIsExport(origin) || originIsResolvedExport(origin); +} - interface UniqueNameSet { - add(name: string): void; - has(name: string): boolean; - } +function originIsPackageJsonImport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { + return (originIsExport(origin) || originIsResolvedExport(origin)) && !!origin.isFromPackageJson; +} - /** - * Map from symbol index in `symbols` -> SymbolOriginInfo. - */ - type SymbolOriginInfoMap = Record; +function originIsPromise(origin: SymbolOriginInfo): boolean { + return !!(origin.kind & SymbolOriginInfoKind.Promise); +} - /** Map from symbol id -> SortText. */ - type SymbolSortTextMap = (SortText | undefined)[]; +function originIsNullableMember(origin: SymbolOriginInfo): boolean { + return !!(origin.kind & SymbolOriginInfoKind.Nullable); +} - const enum KeywordCompletionFilters { - None, - All, - ClassElementKeywords, - InterfaceElementKeywords, - ConstructorParameterKeywords, - FunctionLikeBodyKeywords, - TypeAssertionKeywords, - TypeKeywords, - TypeKeyword, - Last = TypeKeyword - } +function originIsTypeOnlyAlias(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoTypeOnlyAlias { + return !!(origin && origin.kind & SymbolOriginInfoKind.TypeOnlyAlias); +} - const enum GlobalsSearch { - Continue, - Success, - Fail - } +function originIsObjectLiteralMethod(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoObjectLiteralMethod { + return !!(origin && origin.kind & SymbolOriginInfoKind.ObjectLiteralMethod); +} - interface ModuleSpecifierResolutioContext { - tryResolve: (exportInfo: readonly ts.SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult; - resolvedAny: () => boolean; - skippedAny: () => boolean; - resolvedBeyondLimit: () => boolean; - } +interface UniqueNameSet { + add(name: string): void; + has(name: string): boolean; +} - type ModuleSpecifierResolutionResult = "skipped" | "failed" | { - exportInfo?: ts.SymbolExportInfo; - moduleSpecifier: string; - }; +/** + * Map from symbol index in `symbols` -> SymbolOriginInfo. + */ +type SymbolOriginInfoMap = Record; + +/** Map from symbol id -> SortText. */ +type SymbolSortTextMap = (SortText | undefined)[]; + +const enum KeywordCompletionFilters { + None, + All, + ClassElementKeywords, + InterfaceElementKeywords, + ConstructorParameterKeywords, + FunctionLikeBodyKeywords, + TypeAssertionKeywords, + TypeKeywords, + TypeKeyword, + Last = TypeKeyword +} - function resolvingModuleSpecifiers(logPrefix: string, host: ts.LanguageServiceHost, program: ts.Program, sourceFile: ts.SourceFile, position: number, preferences: ts.UserPreferences, isForImportStatementCompletion: boolean, isValidTypeOnlyUseSite: boolean, cb: (context: ModuleSpecifierResolutioContext) => TReturn): TReturn { - const start = ts.timestamp(); - const packageJsonImportFilter = ts.createPackageJsonImportFilter(sourceFile, preferences, host); - // Under `--moduleResolution nodenext`, we have to resolve module specifiers up front, because - // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a - // relative path into node_modules), and we want to filter those completions out entirely. - // Import statement completions always need specifier resolution because the module specifier is - // part of their `insertText`, not the `codeActions` creating edits away from the cursor. - const needsFullResolution = isForImportStatementCompletion || ts.moduleResolutionRespectsExports(ts.getEmitModuleResolutionKind(program.getCompilerOptions())); - let skippedAny = false; - let ambientCount = 0; - let resolvedCount = 0; - let resolvedFromCacheCount = 0; - let cacheAttemptCount = 0; - - const result = cb({ - tryResolve, - skippedAny: () => skippedAny, - resolvedAny: () => resolvedCount > 0, - resolvedBeyondLimit: () => resolvedCount > moduleSpecifierResolutionLimit, - }); +const enum GlobalsSearch { + Continue, + Success, + Fail +} - const hitRateMessage = cacheAttemptCount ? ` (${(resolvedFromCacheCount / cacheAttemptCount * 100).toFixed(1)}% hit rate)` : ""; - host.log?.(`${logPrefix}: resolved ${resolvedCount} module specifiers, plus ${ambientCount} ambient and ${resolvedFromCacheCount} from cache${hitRateMessage}`); - host.log?.(`${logPrefix}: response is ${skippedAny ? "incomplete" : "complete"}`); - host.log?.(`${logPrefix}: ${ts.timestamp() - start}`); - return result; +interface ModuleSpecifierResolutioContext { + tryResolve: (exportInfo: readonly ts.SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult; + resolvedAny: () => boolean; + skippedAny: () => boolean; + resolvedBeyondLimit: () => boolean; +} - function tryResolve(exportInfo: readonly ts.SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean): ModuleSpecifierResolutionResult { - if (isFromAmbientModule) { - const result = ts.codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences); - if (result) { - ambientCount++; - } - return result || "failed"; - } - const shouldResolveModuleSpecifier = needsFullResolution || preferences.allowIncompleteCompletions && resolvedCount < moduleSpecifierResolutionLimit; - const shouldGetModuleSpecifierFromCache = !shouldResolveModuleSpecifier && preferences.allowIncompleteCompletions && cacheAttemptCount < moduleSpecifierResolutionCacheAttemptLimit; - const result = (shouldResolveModuleSpecifier || shouldGetModuleSpecifierFromCache) - ? ts.codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences, packageJsonImportFilter, shouldGetModuleSpecifierFromCache) - : undefined; +type ModuleSpecifierResolutionResult = "skipped" | "failed" | { + exportInfo?: ts.SymbolExportInfo; + moduleSpecifier: string; +}; + +function resolvingModuleSpecifiers(logPrefix: string, host: ts.LanguageServiceHost, program: ts.Program, sourceFile: ts.SourceFile, position: number, preferences: ts.UserPreferences, isForImportStatementCompletion: boolean, isValidTypeOnlyUseSite: boolean, cb: (context: ModuleSpecifierResolutioContext) => TReturn): TReturn { + const start = ts.timestamp(); + const packageJsonImportFilter = ts.createPackageJsonImportFilter(sourceFile, preferences, host); + // Under `--moduleResolution nodenext`, we have to resolve module specifiers up front, because + // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a + // relative path into node_modules), and we want to filter those completions out entirely. + // Import statement completions always need specifier resolution because the module specifier is + // part of their `insertText`, not the `codeActions` creating edits away from the cursor. + const needsFullResolution = isForImportStatementCompletion || ts.moduleResolutionRespectsExports(ts.getEmitModuleResolutionKind(program.getCompilerOptions())); + let skippedAny = false; + let ambientCount = 0; + let resolvedCount = 0; + let resolvedFromCacheCount = 0; + let cacheAttemptCount = 0; + + const result = cb({ + tryResolve, + skippedAny: () => skippedAny, + resolvedAny: () => resolvedCount > 0, + resolvedBeyondLimit: () => resolvedCount > moduleSpecifierResolutionLimit, + }); - if (!shouldResolveModuleSpecifier && !shouldGetModuleSpecifierFromCache || shouldGetModuleSpecifierFromCache && !result) { - skippedAny = true; - } + const hitRateMessage = cacheAttemptCount ? ` (${(resolvedFromCacheCount / cacheAttemptCount * 100).toFixed(1)}% hit rate)` : ""; + host.log?.(`${logPrefix}: resolved ${resolvedCount} module specifiers, plus ${ambientCount} ambient and ${resolvedFromCacheCount} from cache${hitRateMessage}`); + host.log?.(`${logPrefix}: response is ${skippedAny ? "incomplete" : "complete"}`); + host.log?.(`${logPrefix}: ${ts.timestamp() - start}`); + return result; - resolvedCount += result?.computedWithoutCacheCount || 0; - resolvedFromCacheCount += exportInfo.length - (result?.computedWithoutCacheCount || 0); - if (shouldGetModuleSpecifierFromCache) { - cacheAttemptCount++; + function tryResolve(exportInfo: readonly ts.SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean): ModuleSpecifierResolutionResult { + if (isFromAmbientModule) { + const result = ts.codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences); + if (result) { + ambientCount++; } + return result || "failed"; + } + const shouldResolveModuleSpecifier = needsFullResolution || preferences.allowIncompleteCompletions && resolvedCount < moduleSpecifierResolutionLimit; + const shouldGetModuleSpecifierFromCache = !shouldResolveModuleSpecifier && preferences.allowIncompleteCompletions && cacheAttemptCount < moduleSpecifierResolutionCacheAttemptLimit; + const result = (shouldResolveModuleSpecifier || shouldGetModuleSpecifierFromCache) + ? ts.codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences, packageJsonImportFilter, shouldGetModuleSpecifierFromCache) + : undefined; - return result || (needsFullResolution ? "failed" : "skipped"); + if (!shouldResolveModuleSpecifier && !shouldGetModuleSpecifierFromCache || shouldGetModuleSpecifierFromCache && !result) { + skippedAny = true; } - } - export function getCompletionsAtPosition(host: ts.LanguageServiceHost, program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, preferences: ts.UserPreferences, triggerCharacter: ts.CompletionsTriggerCharacter | undefined, completionKind: ts.CompletionTriggerKind | undefined, cancellationToken: ts.CancellationToken, formatContext?: ts.formatting.FormatContext): ts.CompletionInfo | undefined { - const { previousToken } = getRelevantTokens(position, sourceFile); - if (triggerCharacter && !ts.isInString(sourceFile, position, previousToken) && !isValidTrigger(sourceFile, triggerCharacter, previousToken, position)) { - return undefined; + resolvedCount += result?.computedWithoutCacheCount || 0; + resolvedFromCacheCount += exportInfo.length - (result?.computedWithoutCacheCount || 0); + if (shouldGetModuleSpecifierFromCache) { + cacheAttemptCount++; } - if (triggerCharacter === " ") { - // `isValidTrigger` ensures we are at `import |` - if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { - return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; - } - return undefined; + return result || (needsFullResolution ? "failed" : "skipped"); + } +} - } +export function getCompletionsAtPosition(host: ts.LanguageServiceHost, program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, preferences: ts.UserPreferences, triggerCharacter: ts.CompletionsTriggerCharacter | undefined, completionKind: ts.CompletionTriggerKind | undefined, cancellationToken: ts.CancellationToken, formatContext?: ts.formatting.FormatContext): ts.CompletionInfo | undefined { + const { previousToken } = getRelevantTokens(position, sourceFile); + if (triggerCharacter && !ts.isInString(sourceFile, position, previousToken) && !isValidTrigger(sourceFile, triggerCharacter, previousToken, position)) { + return undefined; + } - // If the request is a continuation of an earlier `isIncomplete` response, - // we can continue it from the cached previous response. - const compilerOptions = program.getCompilerOptions(); - const incompleteCompletionsCache = preferences.allowIncompleteCompletions ? host.getIncompleteCompletionsCache?.() : undefined; - if (incompleteCompletionsCache && completionKind === ts.CompletionTriggerKind.TriggerForIncompleteCompletions && previousToken && ts.isIdentifier(previousToken)) { - const incompleteContinuation = continuePreviousIncompleteResponse(incompleteCompletionsCache, sourceFile, previousToken, program, host, preferences, cancellationToken); - if (incompleteContinuation) { - return incompleteContinuation; - } - } - else { - incompleteCompletionsCache?.clear(); + if (triggerCharacter === " ") { + // `isValidTrigger` ensures we are at `import |` + if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; } + return undefined; - const stringCompletions = ts.Completions.StringCompletions.getStringLiteralCompletions(sourceFile, position, previousToken, compilerOptions, host, program, log, preferences); - if (stringCompletions) { - return stringCompletions; - } + } - if (previousToken && ts.isBreakOrContinueStatement(previousToken.parent) - && (previousToken.kind === ts.SyntaxKind.BreakKeyword || previousToken.kind === ts.SyntaxKind.ContinueKeyword || previousToken.kind === ts.SyntaxKind.Identifier)) { - return getLabelCompletionAtPosition(previousToken.parent); + // If the request is a continuation of an earlier `isIncomplete` response, + // we can continue it from the cached previous response. + const compilerOptions = program.getCompilerOptions(); + const incompleteCompletionsCache = preferences.allowIncompleteCompletions ? host.getIncompleteCompletionsCache?.() : undefined; + if (incompleteCompletionsCache && completionKind === ts.CompletionTriggerKind.TriggerForIncompleteCompletions && previousToken && ts.isIdentifier(previousToken)) { + const incompleteContinuation = continuePreviousIncompleteResponse(incompleteCompletionsCache, sourceFile, previousToken, program, host, preferences, cancellationToken); + if (incompleteContinuation) { + return incompleteContinuation; } + } + else { + incompleteCompletionsCache?.clear(); + } - const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, preferences, /*detailsEntryId*/ undefined, host, formatContext, cancellationToken); - if (!completionData) { - return undefined; - } + const stringCompletions = ts.Completions.StringCompletions.getStringLiteralCompletions(sourceFile, position, previousToken, compilerOptions, host, program, log, preferences); + if (stringCompletions) { + return stringCompletions; + } - switch (completionData.kind) { - case CompletionDataKind.Data: - const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences, formatContext, position); - if (response?.isIncomplete) { - incompleteCompletionsCache?.set(response); - } - return response; - case CompletionDataKind.JsDocTagName: - // If the current position is a jsDoc tag name, only tag names should be provided for completion - return jsdocCompletionInfo(ts.JsDoc.getJSDocTagNameCompletions()); - case CompletionDataKind.JsDocTag: - // If the current position is a jsDoc tag, only tags should be provided for completion - return jsdocCompletionInfo(ts.JsDoc.getJSDocTagCompletions()); - case CompletionDataKind.JsDocParameterName: - return jsdocCompletionInfo(ts.JsDoc.getJSDocParameterNameCompletions(completionData.tag)); - case CompletionDataKind.Keywords: - return specificKeywordCompletionInfo(completionData.keywordCompletions, completionData.isNewIdentifierLocation); - default: - return ts.Debug.assertNever(completionData); - } + if (previousToken && ts.isBreakOrContinueStatement(previousToken.parent) + && (previousToken.kind === ts.SyntaxKind.BreakKeyword || previousToken.kind === ts.SyntaxKind.ContinueKeyword || previousToken.kind === ts.SyntaxKind.Identifier)) { + return getLabelCompletionAtPosition(previousToken.parent); } - // Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. - // So, it's important that we sort those ties in the order we want them displayed if it matters. We don't - // strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to - // do the work of comparing them so we can sort those ties appropriately; plus, it makes the order returned - // by the language service consistent with what TS Server does and what editors typically do. This also makes - // completions tests make more sense. We used to sort only alphabetically and only in the server layer, but - // this made tests really weird, since most fourslash tests don't use the server. - function compareCompletionEntries(entryInArray: ts.CompletionEntry, entryToInsert: ts.CompletionEntry): ts.Comparison { - let result = ts.compareStringsCaseSensitiveUI(entryInArray.sortText, entryToInsert.sortText); - if (result === ts.Comparison.EqualTo) { - result = ts.compareStringsCaseSensitiveUI(entryInArray.name, entryToInsert.name); - } - if (result === ts.Comparison.EqualTo && entryInArray.data?.moduleSpecifier && entryToInsert.data?.moduleSpecifier) { - // Sort same-named auto-imports by module specifier - result = ts.compareNumberOfDirectorySeparators((entryInArray.data as ts.CompletionEntryDataResolved).moduleSpecifier, (entryToInsert.data as ts.CompletionEntryDataResolved).moduleSpecifier); - } - if (result === ts.Comparison.EqualTo) { - // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. - return ts.Comparison.LessThan; - } - return result; + const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, preferences, /*detailsEntryId*/ undefined, host, formatContext, cancellationToken); + if (!completionData) { + return undefined; } - function completionEntryDataIsResolved(data: ts.CompletionEntryDataAutoImport | undefined): data is ts.CompletionEntryDataResolved { - return !!data?.moduleSpecifier; + switch (completionData.kind) { + case CompletionDataKind.Data: + const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences, formatContext, position); + if (response?.isIncomplete) { + incompleteCompletionsCache?.set(response); + } + return response; + case CompletionDataKind.JsDocTagName: + // If the current position is a jsDoc tag name, only tag names should be provided for completion + return jsdocCompletionInfo(ts.JsDoc.getJSDocTagNameCompletions()); + case CompletionDataKind.JsDocTag: + // If the current position is a jsDoc tag, only tags should be provided for completion + return jsdocCompletionInfo(ts.JsDoc.getJSDocTagCompletions()); + case CompletionDataKind.JsDocParameterName: + return jsdocCompletionInfo(ts.JsDoc.getJSDocParameterNameCompletions(completionData.tag)); + case CompletionDataKind.Keywords: + return specificKeywordCompletionInfo(completionData.keywordCompletions, completionData.isNewIdentifierLocation); + default: + return ts.Debug.assertNever(completionData); } +} - function continuePreviousIncompleteResponse(cache: ts.IncompleteCompletionsCache, file: ts.SourceFile, location: ts.Identifier, program: ts.Program, host: ts.LanguageServiceHost, preferences: ts.UserPreferences, cancellationToken: ts.CancellationToken): ts.CompletionInfo | undefined { - const previousResponse = cache.get(); - if (!previousResponse) - return undefined; +// Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. +// So, it's important that we sort those ties in the order we want them displayed if it matters. We don't +// strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to +// do the work of comparing them so we can sort those ties appropriately; plus, it makes the order returned +// by the language service consistent with what TS Server does and what editors typically do. This also makes +// completions tests make more sense. We used to sort only alphabetically and only in the server layer, but +// this made tests really weird, since most fourslash tests don't use the server. +function compareCompletionEntries(entryInArray: ts.CompletionEntry, entryToInsert: ts.CompletionEntry): ts.Comparison { + let result = ts.compareStringsCaseSensitiveUI(entryInArray.sortText, entryToInsert.sortText); + if (result === ts.Comparison.EqualTo) { + result = ts.compareStringsCaseSensitiveUI(entryInArray.name, entryToInsert.name); + } + if (result === ts.Comparison.EqualTo && entryInArray.data?.moduleSpecifier && entryToInsert.data?.moduleSpecifier) { + // Sort same-named auto-imports by module specifier + result = ts.compareNumberOfDirectorySeparators((entryInArray.data as ts.CompletionEntryDataResolved).moduleSpecifier, (entryToInsert.data as ts.CompletionEntryDataResolved).moduleSpecifier); + } + if (result === ts.Comparison.EqualTo) { + // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. + return ts.Comparison.LessThan; + } + return result; +} - const lowerCaseTokenText = location.text.toLowerCase(); - const exportMap = ts.getExportInfoMap(file, host, program, cancellationToken); - const newEntries = resolvingModuleSpecifiers("continuePreviousIncompleteResponse", host, program, file, location.getStart(), preferences, - /*isForImportStatementCompletion*/ false, ts.isValidTypeOnlyAliasUseSite(location), context => { - const entries = ts.mapDefined(previousResponse.entries, entry => { - if (!entry.hasAction || !entry.source || !entry.data || completionEntryDataIsResolved(entry.data)) { - // Not an auto import or already resolved; keep as is - return entry; - } - if (!charactersFuzzyMatchInString(entry.name, lowerCaseTokenText)) { - // No longer matches typed characters; filter out - return undefined; - } +function completionEntryDataIsResolved(data: ts.CompletionEntryDataAutoImport | undefined): data is ts.CompletionEntryDataResolved { + return !!data?.moduleSpecifier; +} - const { origin } = ts.Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host)); - const info = exportMap.get(file.path, entry.data.exportMapKey); +function continuePreviousIncompleteResponse(cache: ts.IncompleteCompletionsCache, file: ts.SourceFile, location: ts.Identifier, program: ts.Program, host: ts.LanguageServiceHost, preferences: ts.UserPreferences, cancellationToken: ts.CancellationToken): ts.CompletionInfo | undefined { + const previousResponse = cache.get(); + if (!previousResponse) + return undefined; - const result = info && context.tryResolve(info, entry.name, !ts.isExternalModuleNameRelative(ts.stripQuotes(origin.moduleSymbol.name))); - if (result === "skipped") + const lowerCaseTokenText = location.text.toLowerCase(); + const exportMap = ts.getExportInfoMap(file, host, program, cancellationToken); + const newEntries = resolvingModuleSpecifiers("continuePreviousIncompleteResponse", host, program, file, location.getStart(), preferences, + /*isForImportStatementCompletion*/ false, ts.isValidTypeOnlyAliasUseSite(location), context => { + const entries = ts.mapDefined(previousResponse.entries, entry => { + if (!entry.hasAction || !entry.source || !entry.data || completionEntryDataIsResolved(entry.data)) { + // Not an auto import or already resolved; keep as is return entry; - if (!result || result === "failed") { - host.log?.(`Unexpected failure resolving auto import for '${entry.name}' from '${entry.source}'`); - return undefined; - } + } + if (!charactersFuzzyMatchInString(entry.name, lowerCaseTokenText)) { + // No longer matches typed characters; filter out + return undefined; + } - const newOrigin: SymbolOriginInfoResolvedExport = { - ...origin, - kind: SymbolOriginInfoKind.ResolvedExport, - moduleSpecifier: result.moduleSpecifier, - }; - // Mutating for performance... feels sketchy but nobody else uses the cache, - // so why bother allocating a bunch of new objects? - entry.data = originToCompletionEntryData(newOrigin); - entry.source = getSourceFromOrigin(newOrigin); - entry.sourceDisplay = [ts.textPart(newOrigin.moduleSpecifier)]; - return entry; - }); + const { origin } = ts.Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host)); + const info = exportMap.get(file.path, entry.data.exportMapKey); - if (!context.skippedAny()) { - previousResponse.isIncomplete = undefined; + const result = info && context.tryResolve(info, entry.name, !ts.isExternalModuleNameRelative(ts.stripQuotes(origin.moduleSymbol.name))); + if (result === "skipped") + return entry; + if (!result || result === "failed") { + host.log?.(`Unexpected failure resolving auto import for '${entry.name}' from '${entry.source}'`); + return undefined; } - return entries; - }); + const newOrigin: SymbolOriginInfoResolvedExport = { + ...origin, + kind: SymbolOriginInfoKind.ResolvedExport, + moduleSpecifier: result.moduleSpecifier, + }; + // Mutating for performance... feels sketchy but nobody else uses the cache, + // so why bother allocating a bunch of new objects? + entry.data = originToCompletionEntryData(newOrigin); + entry.source = getSourceFromOrigin(newOrigin); + entry.sourceDisplay = [ts.textPart(newOrigin.moduleSpecifier)]; + return entry; + }); - previousResponse.entries = newEntries; - previousResponse.flags = (previousResponse.flags || 0) | ts.CompletionInfoFlags.IsContinuation; - return previousResponse; - } + if (!context.skippedAny()) { + previousResponse.isIncomplete = undefined; + } - function jsdocCompletionInfo(entries: ts.CompletionEntry[]): ts.CompletionInfo { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } + return entries; + }); - function keywordToCompletionEntry(keyword: ts.TokenSyntaxKind) { - return { - name: ts.tokenToString(keyword)!, - kind: ts.ScriptElementKind.keyword, - kindModifiers: ts.ScriptElementKindModifier.none, - sortText: SortText.GlobalsOrKeywords, - }; - } + previousResponse.entries = newEntries; + previousResponse.flags = (previousResponse.flags || 0) | ts.CompletionInfoFlags.IsContinuation; + return previousResponse; +} - function specificKeywordCompletionInfo(entries: readonly ts.CompletionEntry[], isNewIdentifierLocation: boolean): ts.CompletionInfo { - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation, - entries: entries.slice(), - }; - } +function jsdocCompletionInfo(entries: ts.CompletionEntry[]): ts.CompletionInfo { + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; +} - function keywordCompletionData(keywordFilters: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean, isNewIdentifierLocation: boolean): Request { - return { - kind: CompletionDataKind.Keywords, - keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTsOnlyKeywords), - isNewIdentifierLocation, - }; - } +function keywordToCompletionEntry(keyword: ts.TokenSyntaxKind) { + return { + name: ts.tokenToString(keyword)!, + kind: ts.ScriptElementKind.keyword, + kindModifiers: ts.ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords, + }; +} - function keywordFiltersFromSyntaxKind(keywordCompletion: ts.TokenSyntaxKind): KeywordCompletionFilters { - switch (keywordCompletion) { - case ts.SyntaxKind.TypeKeyword: return KeywordCompletionFilters.TypeKeyword; - default: ts.Debug.fail("Unknown mapping from SyntaxKind to KeywordCompletionFilters"); - } - } +function specificKeywordCompletionInfo(entries: readonly ts.CompletionEntry[], isNewIdentifierLocation: boolean): ts.CompletionInfo { + return { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation, + entries: entries.slice(), + }; +} - function getOptionalReplacementSpan(location: ts.Node | undefined) { - // StringLiteralLike locations are handled separately in stringCompletions.ts - return location?.kind === ts.SyntaxKind.Identifier ? ts.createTextSpanFromNode(location) : undefined; - } - function completionInfoFromData(sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, compilerOptions: ts.CompilerOptions, log: Log, completionData: CompletionData, preferences: ts.UserPreferences, formatContext: ts.formatting.FormatContext | undefined, position: number): ts.CompletionInfo | undefined { - const { symbols, contextToken, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer, isTypeOnlyLocation, isJsxIdentifierExpected, isRightOfOpenTag, importCompletionNode, insideJsDocTagTypeExpression, symbolToSortTextMap: symbolToSortTextMap, hasUnresolvedAutoImports, } = completionData; +function keywordCompletionData(keywordFilters: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean, isNewIdentifierLocation: boolean): Request { + return { + kind: CompletionDataKind.Keywords, + keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTsOnlyKeywords), + isNewIdentifierLocation, + }; +} - // Verify if the file is JSX language variant - if (ts.getLanguageVariant(sourceFile.scriptKind) === ts.LanguageVariant.JSX) { - const completionInfo = getJsxClosingTagCompletion(location, sourceFile); - if (completionInfo) { - return completionInfo; - } - } +function keywordFiltersFromSyntaxKind(keywordCompletion: ts.TokenSyntaxKind): KeywordCompletionFilters { + switch (keywordCompletion) { + case ts.SyntaxKind.TypeKeyword: return KeywordCompletionFilters.TypeKeyword; + default: ts.Debug.fail("Unknown mapping from SyntaxKind to KeywordCompletionFilters"); + } +} - const entries = ts.createSortedArray(); +function getOptionalReplacementSpan(location: ts.Node | undefined) { + // StringLiteralLike locations are handled separately in stringCompletions.ts + return location?.kind === ts.SyntaxKind.Identifier ? ts.createTextSpanFromNode(location) : undefined; +} +function completionInfoFromData(sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, compilerOptions: ts.CompilerOptions, log: Log, completionData: CompletionData, preferences: ts.UserPreferences, formatContext: ts.formatting.FormatContext | undefined, position: number): ts.CompletionInfo | undefined { + const { symbols, contextToken, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer, isTypeOnlyLocation, isJsxIdentifierExpected, isRightOfOpenTag, importCompletionNode, insideJsDocTagTypeExpression, symbolToSortTextMap: symbolToSortTextMap, hasUnresolvedAutoImports, } = completionData; - if (isUncheckedFile(sourceFile, compilerOptions)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, - /*replacementToken*/ undefined, contextToken, location, sourceFile, host, program, ts.getEmitScriptTarget(compilerOptions), log, completionKind, preferences, compilerOptions, formatContext, isTypeOnlyLocation, propertyAccessToConvert, isJsxIdentifierExpected, isJsxInitializer, importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap, isJsxIdentifierExpected, isRightOfOpenTag); - getJSCompletionEntries(sourceFile, location.pos, uniqueNames, ts.getEmitScriptTarget(compilerOptions), entries); + // Verify if the file is JSX language variant + if (ts.getLanguageVariant(sourceFile.scriptKind) === ts.LanguageVariant.JSX) { + const completionInfo = getJsxClosingTagCompletion(location, sourceFile); + if (completionInfo) { + return completionInfo; } - else { - if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { - return undefined; - } + } - getCompletionEntriesFromSymbols(symbols, entries, - /*replacementToken*/ undefined, contextToken, location, sourceFile, host, program, ts.getEmitScriptTarget(compilerOptions), log, completionKind, preferences, compilerOptions, formatContext, isTypeOnlyLocation, propertyAccessToConvert, isJsxIdentifierExpected, isJsxInitializer, importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap, isJsxIdentifierExpected, isRightOfOpenTag); - } + const entries = ts.createSortedArray(); - if (keywordFilters !== KeywordCompletionFilters.None) { - const entryNames = new ts.Set(entries.map(e => e.name)); - for (const keywordEntry of getKeywordCompletions(keywordFilters, !insideJsDocTagTypeExpression && ts.isSourceFileJS(sourceFile))) { - if (isTypeOnlyLocation && ts.isTypeKeyword(ts.stringToToken(keywordEntry.name)!) || !entryNames.has(keywordEntry.name)) { - ts.insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true); - } - } + if (isUncheckedFile(sourceFile, compilerOptions)) { + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, + /*replacementToken*/ undefined, contextToken, location, sourceFile, host, program, ts.getEmitScriptTarget(compilerOptions), log, completionKind, preferences, compilerOptions, formatContext, isTypeOnlyLocation, propertyAccessToConvert, isJsxIdentifierExpected, isJsxInitializer, importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap, isJsxIdentifierExpected, isRightOfOpenTag); + getJSCompletionEntries(sourceFile, location.pos, uniqueNames, ts.getEmitScriptTarget(compilerOptions), entries); + } + else { + if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { + return undefined; } + getCompletionEntriesFromSymbols(symbols, entries, + /*replacementToken*/ undefined, contextToken, location, sourceFile, host, program, ts.getEmitScriptTarget(compilerOptions), log, completionKind, preferences, compilerOptions, formatContext, isTypeOnlyLocation, propertyAccessToConvert, isJsxIdentifierExpected, isJsxInitializer, importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap, isJsxIdentifierExpected, isRightOfOpenTag); + } + + if (keywordFilters !== KeywordCompletionFilters.None) { const entryNames = new ts.Set(entries.map(e => e.name)); - for (const keywordEntry of getContextualKeywords(contextToken, position)) { - if (!entryNames.has(keywordEntry.name)) { + for (const keywordEntry of getKeywordCompletions(keywordFilters, !insideJsDocTagTypeExpression && ts.isSourceFileJS(sourceFile))) { + if (isTypeOnlyLocation && ts.isTypeKeyword(ts.stringToToken(keywordEntry.name)!) || !entryNames.has(keywordEntry.name)) { ts.insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true); } } + } - for (const literal of literals) { - ts.insertSorted(entries, createCompletionEntryForLiteral(sourceFile, preferences, literal), compareCompletionEntries, /*allowDuplicates*/ true); + const entryNames = new ts.Set(entries.map(e => e.name)); + for (const keywordEntry of getContextualKeywords(contextToken, position)) { + if (!entryNames.has(keywordEntry.name)) { + ts.insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true); } + } - return { - flags: completionData.flags, - isGlobalCompletion: isInSnippetScope, - isIncomplete: preferences.allowIncompleteCompletions && hasUnresolvedAutoImports ? true : undefined, - isMemberCompletion: isMemberCompletionKind(completionKind), - isNewIdentifierLocation, - optionalReplacementSpan: getOptionalReplacementSpan(location), - entries, - }; + for (const literal of literals) { + ts.insertSorted(entries, createCompletionEntryForLiteral(sourceFile, preferences, literal), compareCompletionEntries, /*allowDuplicates*/ true); } - function isUncheckedFile(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions): boolean { - return ts.isSourceFileJS(sourceFile) && !ts.isCheckJsEnabledForFile(sourceFile, compilerOptions); + return { + flags: completionData.flags, + isGlobalCompletion: isInSnippetScope, + isIncomplete: preferences.allowIncompleteCompletions && hasUnresolvedAutoImports ? true : undefined, + isMemberCompletion: isMemberCompletionKind(completionKind), + isNewIdentifierLocation, + optionalReplacementSpan: getOptionalReplacementSpan(location), + entries, + }; +} + +function isUncheckedFile(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions): boolean { + return ts.isSourceFileJS(sourceFile) && !ts.isCheckJsEnabledForFile(sourceFile, compilerOptions); +} + +function isMemberCompletionKind(kind: CompletionKind): boolean { + switch (kind) { + case CompletionKind.ObjectPropertyDeclaration: + case CompletionKind.MemberLike: + case CompletionKind.PropertyAccess: + return true; + default: + return false; } +} - function isMemberCompletionKind(kind: CompletionKind): boolean { - switch (kind) { - case CompletionKind.ObjectPropertyDeclaration: - case CompletionKind.MemberLike: - case CompletionKind.PropertyAccess: +function getJsxClosingTagCompletion(location: ts.Node | undefined, sourceFile: ts.SourceFile): ts.CompletionInfo | undefined { + // We wanna walk up the tree till we find a JSX closing element + const jsxClosingElement = ts.findAncestor(location, node => { + switch (node.kind) { + case ts.SyntaxKind.JsxClosingElement: return true; - default: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: return false; - } + default: + return "quit"; + } + }) as ts.JsxClosingElement | undefined; + + if (jsxClosingElement) { + // 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". + // And at property access expressions ` ` the completion will + // return full closing tag with an optional replacement span + // For example: + // var x = + // var y = + // the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name + const hasClosingAngleBracket = !!ts.findChildOfKind(jsxClosingElement, ts.SyntaxKind.GreaterThanToken, sourceFile); + const tagName = jsxClosingElement.parent.openingElement.tagName; + const closingTag = tagName.getText(sourceFile); + const fullClosingTag = closingTag + (hasClosingAngleBracket ? "" : ">"); + const replacementSpan = ts.createTextSpanFromNode(jsxClosingElement.tagName); + const entry: ts.CompletionEntry = { + name: fullClosingTag, + kind: ts.ScriptElementKind.classElement, + kindModifiers: undefined, + sortText: SortText.LocationPriority, + }; + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: replacementSpan, entries: [entry] }; } + return; +} - function getJsxClosingTagCompletion(location: ts.Node | undefined, sourceFile: ts.SourceFile): ts.CompletionInfo | undefined { - // We wanna walk up the tree till we find a JSX closing element - const jsxClosingElement = ts.findAncestor(location, node => { - switch (node.kind) { - case ts.SyntaxKind.JsxClosingElement: - return true; - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.GreaterThanToken: - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PropertyAccessExpression: - return false; - default: - return "quit"; - } - }) as ts.JsxClosingElement | undefined; - - if (jsxClosingElement) { - // 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". - // And at property access expressions ` ` the completion will - // return full closing tag with an optional replacement span - // For example: - // var x = - // var y = - // the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name - const hasClosingAngleBracket = !!ts.findChildOfKind(jsxClosingElement, ts.SyntaxKind.GreaterThanToken, sourceFile); - const tagName = jsxClosingElement.parent.openingElement.tagName; - const closingTag = tagName.getText(sourceFile); - const fullClosingTag = closingTag + (hasClosingAngleBracket ? "" : ">"); - const replacementSpan = ts.createTextSpanFromNode(jsxClosingElement.tagName); - const entry: ts.CompletionEntry = { - name: fullClosingTag, - kind: ts.ScriptElementKind.classElement, - kindModifiers: undefined, - sortText: SortText.LocationPriority, - }; - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: replacementSpan, entries: [entry] }; +function getJSCompletionEntries(sourceFile: ts.SourceFile, position: number, uniqueNames: UniqueNameSet, target: ts.ScriptTarget, entries: ts.SortedArray): void { + ts.getNameTable(sourceFile).forEach((pos, name) => { + // Skip identifiers produced only from the current location + if (pos === position) { + return; + } + const realName = ts.unescapeLeadingUnderscores(name); + if (!uniqueNames.has(realName) && ts.isIdentifierText(realName, target)) { + uniqueNames.add(realName); + ts.insertSorted(entries, { + name: realName, + kind: ts.ScriptElementKind.warning, + kindModifiers: "", + sortText: SortText.JavascriptIdentifiers, + isFromUncheckedFile: true + }, compareCompletionEntries); } - return; - } + }); +} - function getJSCompletionEntries(sourceFile: ts.SourceFile, position: number, uniqueNames: UniqueNameSet, target: ts.ScriptTarget, entries: ts.SortedArray): void { - ts.getNameTable(sourceFile).forEach((pos, name) => { - // Skip identifiers produced only from the current location - if (pos === position) { - return; - } - const realName = ts.unescapeLeadingUnderscores(name); - if (!uniqueNames.has(realName) && ts.isIdentifierText(realName, target)) { - uniqueNames.add(realName); - ts.insertSorted(entries, { - name: realName, - kind: ts.ScriptElementKind.warning, - kindModifiers: "", - sortText: SortText.JavascriptIdentifiers, - isFromUncheckedFile: true - }, compareCompletionEntries); - } - }); +function completionNameForLiteral(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, literal: string | number | ts.PseudoBigInt): string { + return typeof literal === "object" ? ts.pseudoBigIntToString(literal) + "n" : + ts.isString(literal) ? ts.quote(sourceFile, preferences, literal) : JSON.stringify(literal); +} +function createCompletionEntryForLiteral(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, literal: string | number | ts.PseudoBigInt): ts.CompletionEntry { + return { name: completionNameForLiteral(sourceFile, preferences, literal), kind: ts.ScriptElementKind.string, kindModifiers: ts.ScriptElementKindModifier.none, sortText: SortText.LocationPriority }; +} +function createCompletionEntry(symbol: ts.Symbol, sortText: SortText, replacementToken: ts.Node | undefined, contextToken: ts.Node | undefined, location: ts.Node, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, name: string, needsConvertPropertyAccess: boolean, origin: SymbolOriginInfo | undefined, recommendedCompletion: ts.Symbol | undefined, propertyAccessToConvert: ts.PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, importCompletionNode: ts.Node | undefined, useSemicolons: boolean, options: ts.CompilerOptions, preferences: ts.UserPreferences, completionKind: CompletionKind, formatContext: ts.formatting.FormatContext | undefined, isJsxIdentifierExpected: boolean | undefined, isRightOfOpenTag: boolean | undefined): ts.CompletionEntry | undefined { + let insertText: string | undefined; + let replacementSpan = ts.getReplacementSpanForContextToken(replacementToken); + let data: ts.CompletionEntryData | undefined; + let isSnippet: true | undefined; + let source = getSourceFromOrigin(origin); + let sourceDisplay; + let hasAction; + let labelDetails; + + const typeChecker = program.getTypeChecker(); + const insertQuestionDot = origin && originIsNullableMember(origin); + const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; + if (origin && originIsThisType(origin)) { + insertText = needsConvertPropertyAccess + ? `this${insertQuestionDot ? "?." : ""}[${quotePropertyName(sourceFile, preferences, name)}]` + : `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(sourceFile, preferences, name)}]` : `[${name}]` : name; + if (insertQuestionDot || propertyAccessToConvert.questionDotToken) { + insertText = `?.${insertText}`; + } + + const dot = ts.findChildOfKind(propertyAccessToConvert, ts.SyntaxKind.DotToken, sourceFile) || + ts.findChildOfKind(propertyAccessToConvert, ts.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 = ts.startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end; + replacementSpan = ts.createTextSpanFromBounds(dot.getStart(sourceFile), end); } - function completionNameForLiteral(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, literal: string | number | ts.PseudoBigInt): string { - return typeof literal === "object" ? ts.pseudoBigIntToString(literal) + "n" : - ts.isString(literal) ? ts.quote(sourceFile, preferences, literal) : JSON.stringify(literal); - } - function createCompletionEntryForLiteral(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, literal: string | number | ts.PseudoBigInt): ts.CompletionEntry { - return { name: completionNameForLiteral(sourceFile, preferences, literal), kind: ts.ScriptElementKind.string, kindModifiers: ts.ScriptElementKindModifier.none, sortText: SortText.LocationPriority }; - } - function createCompletionEntry(symbol: ts.Symbol, sortText: SortText, replacementToken: ts.Node | undefined, contextToken: ts.Node | undefined, location: ts.Node, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, name: string, needsConvertPropertyAccess: boolean, origin: SymbolOriginInfo | undefined, recommendedCompletion: ts.Symbol | undefined, propertyAccessToConvert: ts.PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, importCompletionNode: ts.Node | undefined, useSemicolons: boolean, options: ts.CompilerOptions, preferences: ts.UserPreferences, completionKind: CompletionKind, formatContext: ts.formatting.FormatContext | undefined, isJsxIdentifierExpected: boolean | undefined, isRightOfOpenTag: boolean | undefined): ts.CompletionEntry | undefined { - let insertText: string | undefined; - let replacementSpan = ts.getReplacementSpanForContextToken(replacementToken); - let data: ts.CompletionEntryData | undefined; - let isSnippet: true | undefined; - let source = getSourceFromOrigin(origin); - let sourceDisplay; - let hasAction; - let labelDetails; - - const typeChecker = program.getTypeChecker(); - const insertQuestionDot = origin && originIsNullableMember(origin); - const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; - if (origin && originIsThisType(origin)) { - insertText = needsConvertPropertyAccess - ? `this${insertQuestionDot ? "?." : ""}[${quotePropertyName(sourceFile, preferences, name)}]` - : `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(sourceFile, preferences, name)}]` : `[${name}]` : name; - if (insertQuestionDot || propertyAccessToConvert.questionDotToken) { - insertText = `?.${insertText}`; - } - - const dot = ts.findChildOfKind(propertyAccessToConvert, ts.SyntaxKind.DotToken, sourceFile) || - ts.findChildOfKind(propertyAccessToConvert, ts.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 = ts.startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end; - replacementSpan = ts.createTextSpanFromBounds(dot.getStart(sourceFile), end); + if (isJsxInitializer) { + if (insertText === undefined) + insertText = name; + insertText = `{${insertText}}`; + if (typeof isJsxInitializer !== "boolean") { + replacementSpan = ts.createTextSpanFromNode(isJsxInitializer, sourceFile); } - - if (isJsxInitializer) { - if (insertText === undefined) - insertText = name; - insertText = `{${insertText}}`; - if (typeof isJsxInitializer !== "boolean") { - replacementSpan = ts.createTextSpanFromNode(isJsxInitializer, sourceFile); - } + } + if (origin && originIsPromise(origin) && propertyAccessToConvert) { + if (insertText === undefined) + insertText = name; + const precedingToken = ts.findPrecedingToken(propertyAccessToConvert.pos, sourceFile); + let awaitText = ""; + if (precedingToken && ts.positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { + awaitText = ";"; } - if (origin && originIsPromise(origin) && propertyAccessToConvert) { - if (insertText === undefined) - insertText = name; - const precedingToken = ts.findPrecedingToken(propertyAccessToConvert.pos, sourceFile); - let awaitText = ""; - if (precedingToken && ts.positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { - awaitText = ";"; - } - awaitText += `(await ${propertyAccessToConvert.expression.getText()})`; - insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`; - replacementSpan = ts.createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); - } + awaitText += `(await ${propertyAccessToConvert.expression.getText()})`; + insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`; + replacementSpan = ts.createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); + } - if (originIsResolvedExport(origin)) { - sourceDisplay = [ts.textPart(origin.moduleSpecifier)]; - if (importCompletionNode) { - ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, contextToken, origin, useSemicolons, options, preferences)); - isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined; - } + if (originIsResolvedExport(origin)) { + sourceDisplay = [ts.textPart(origin.moduleSpecifier)]; + if (importCompletionNode) { + ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, contextToken, origin, useSemicolons, options, preferences)); + isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined; } + } - if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) { - hasAction = true; - } + if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) { + hasAction = true; + } - if (preferences.includeCompletionsWithClassMemberSnippets && - preferences.includeCompletionsWithInsertText && - completionKind === CompletionKind.MemberLike && - isClassLikeMemberCompletion(symbol, location)) { - let importAdder; - ({ insertText, isSnippet, importAdder, replacementSpan } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken, formatContext)); - sortText = SortText.ClassMemberSnippets; // sortText has to be lower priority than the sortText for keywords. See #47852. - if (importAdder?.hasFixes()) { - hasAction = true; - source = CompletionSource.ClassMemberSnippet; - } + if (preferences.includeCompletionsWithClassMemberSnippets && + preferences.includeCompletionsWithInsertText && + completionKind === CompletionKind.MemberLike && + isClassLikeMemberCompletion(symbol, location)) { + let importAdder; + ({ insertText, isSnippet, importAdder, replacementSpan } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken, formatContext)); + sortText = SortText.ClassMemberSnippets; // sortText has to be lower priority than the sortText for keywords. See #47852. + if (importAdder?.hasFixes()) { + hasAction = true; + source = CompletionSource.ClassMemberSnippet; } + } - if (origin && originIsObjectLiteralMethod(origin)) { - ({ insertText, isSnippet, labelDetails } = origin); - if (!preferences.useLabelDetailsInCompletionEntries) { - name = name + labelDetails.detail; - labelDetails = undefined; - } - source = CompletionSource.ObjectLiteralMethodSnippet; - sortText = SortText.SortBelow(sortText); + if (origin && originIsObjectLiteralMethod(origin)) { + ({ insertText, isSnippet, labelDetails } = origin); + if (!preferences.useLabelDetailsInCompletionEntries) { + name = name + labelDetails.detail; + labelDetails = undefined; } + source = CompletionSource.ObjectLiteralMethodSnippet; + sortText = SortText.SortBelow(sortText); + } - if (isJsxIdentifierExpected && !isRightOfOpenTag && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") { - let useBraces = preferences.jsxAttributeCompletionStyle === "braces"; - const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location); - - // If is boolean like or undefined, don't return a snippet we want just to return the completion. - if (preferences.jsxAttributeCompletionStyle === "auto" - && !(type.flags & ts.TypeFlags.BooleanLike) - && !(type.flags & ts.TypeFlags.Union && ts.find((type as ts.UnionType).types, type => !!(type.flags & ts.TypeFlags.BooleanLike)))) { - if (type.flags & ts.TypeFlags.StringLike || (type.flags & ts.TypeFlags.Union && ts.every((type as ts.UnionType).types, type => !!(type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Undefined))))) { - // If is string like or undefined use quotes - insertText = `${ts.escapeSnippetText(name)}=${ts.quote(sourceFile, preferences, "$1")}`; - isSnippet = true; - } - else { - // Use braces for everything else - useBraces = true; - } - } + if (isJsxIdentifierExpected && !isRightOfOpenTag && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") { + let useBraces = preferences.jsxAttributeCompletionStyle === "braces"; + const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location); - if (useBraces) { - insertText = `${ts.escapeSnippetText(name)}={$1}`; + // If is boolean like or undefined, don't return a snippet we want just to return the completion. + if (preferences.jsxAttributeCompletionStyle === "auto" + && !(type.flags & ts.TypeFlags.BooleanLike) + && !(type.flags & ts.TypeFlags.Union && ts.find((type as ts.UnionType).types, type => !!(type.flags & ts.TypeFlags.BooleanLike)))) { + if (type.flags & ts.TypeFlags.StringLike || (type.flags & ts.TypeFlags.Union && ts.every((type as ts.UnionType).types, type => !!(type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Undefined))))) { + // If is string like or undefined use quotes + insertText = `${ts.escapeSnippetText(name)}=${ts.quote(sourceFile, preferences, "$1")}`; isSnippet = true; } + else { + // Use braces for everything else + useBraces = true; + } } - if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { - return undefined; + if (useBraces) { + insertText = `${ts.escapeSnippetText(name)}={$1}`; + isSnippet = true; } + } - if (originIsExport(origin) || originIsResolvedExport(origin)) { - data = originToCompletionEntryData(origin); - hasAction = !importCompletionNode; - } - - // 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: ts.SymbolDisplay.getSymbolKind(typeChecker, symbol, location), - kindModifiers: ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), - sortText, - source, - hasAction: hasAction ? true : undefined, - isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined, - insertText, - replacementSpan, - sourceDisplay, - labelDetails, - isSnippet, - isPackageJsonImport: originIsPackageJsonImport(origin) || undefined, - isImportStatementCompletion: !!importCompletionNode || undefined, - data, - }; + if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { + return undefined; } - function isClassLikeMemberCompletion(symbol: ts.Symbol, location: ts.Node): boolean { - // TODO: support JS files. - if (ts.isInJSFile(location)) { - return false; - } + if (originIsExport(origin) || originIsResolvedExport(origin)) { + data = originToCompletionEntryData(origin); + hasAction = !importCompletionNode; + } + + // 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: ts.SymbolDisplay.getSymbolKind(typeChecker, symbol, location), + kindModifiers: ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), + sortText, + source, + hasAction: hasAction ? true : undefined, + isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined, + insertText, + replacementSpan, + sourceDisplay, + labelDetails, + isSnippet, + isPackageJsonImport: originIsPackageJsonImport(origin) || undefined, + isImportStatementCompletion: !!importCompletionNode || undefined, + data, + }; +} - // Completion symbol must be for a class member. - const memberFlags = ts.SymbolFlags.ClassMember - & ts.SymbolFlags.EnumMemberExcludes; - /* In - `class C { - | - }` - `location` is a class-like declaration. - In - `class C { - m| - }` - `location` is an identifier, - `location.parent` is a class element declaration, - and `location.parent.parent` is a class-like declaration. - In - `abstract class C { - abstract - abstract m| - }` - `location` is a syntax list (with modifiers as children), - and `location.parent` is a class-like declaration. - */ - return !!(symbol.flags & memberFlags) && - (ts.isClassLike(location) || - (location.parent && - location.parent.parent && - ts.isClassElement(location.parent) && - location === location.parent.name && - ts.isClassLike(location.parent.parent)) || - (location.parent && - ts.isSyntaxList(location) && - ts.isClassLike(location.parent))); - } - function getEntryForMemberCompletion(host: ts.LanguageServiceHost, program: ts.Program, options: ts.CompilerOptions, preferences: ts.UserPreferences, name: string, symbol: ts.Symbol, location: ts.Node, contextToken: ts.Node | undefined, formatContext: ts.formatting.FormatContext | undefined): { - insertText: string; - isSnippet?: true; - importAdder?: ts.codefix.ImportAdder; - replacementSpan?: ts.TextSpan; - } { - const classLikeDeclaration = ts.findAncestor(location, ts.isClassLike); - if (!classLikeDeclaration) { - return { insertText: name }; - } - - let isSnippet: true | undefined; - let replacementSpan: ts.TextSpan | undefined; - let insertText: string = name; - - const checker = program.getTypeChecker(); - const sourceFile = location.getSourceFile(); - const printer = createSnippetPrinter({ - removeComments: true, - module: options.module, - target: options.target, - omitTrailingSemicolon: false, - newLine: ts.getNewLineKind(ts.getNewLineCharacter(options, ts.maybeBind(host, host.getNewLine))), - }); - const importAdder = ts.codefix.createImportAdder(sourceFile, program, preferences, host); +function isClassLikeMemberCompletion(symbol: ts.Symbol, location: ts.Node): boolean { + // TODO: support JS files. + if (ts.isInJSFile(location)) { + return false; + } - // Create empty body for possible method implementation. - let body; - if (preferences.includeCompletionsWithSnippetText) { - isSnippet = true; - // We are adding a tabstop (i.e. `$0`) in the body of the suggested member, - // if it has one, so that the cursor ends up in the body once the completion is inserted. - // Note: this assumes we won't have more than one body in the completion nodes, which should be the case. - const emptyStmt = ts.factory.createEmptyStatement(); - body = ts.factory.createBlock([emptyStmt], /* multiline */ true); - ts.setSnippetElement(emptyStmt, { kind: ts.SnippetKind.TabStop, order: 0 }); + // Completion symbol must be for a class member. + const memberFlags = ts.SymbolFlags.ClassMember + & ts.SymbolFlags.EnumMemberExcludes; + /* In + `class C { + | + }` + `location` is a class-like declaration. + In + `class C { + m| + }` + `location` is an identifier, + `location.parent` is a class element declaration, + and `location.parent.parent` is a class-like declaration. + In + `abstract class C { + abstract + abstract m| + }` + `location` is a syntax list (with modifiers as children), + and `location.parent` is a class-like declaration. + */ + return !!(symbol.flags & memberFlags) && + (ts.isClassLike(location) || + (location.parent && + location.parent.parent && + ts.isClassElement(location.parent) && + location === location.parent.name && + ts.isClassLike(location.parent.parent)) || + (location.parent && + ts.isSyntaxList(location) && + ts.isClassLike(location.parent))); +} +function getEntryForMemberCompletion(host: ts.LanguageServiceHost, program: ts.Program, options: ts.CompilerOptions, preferences: ts.UserPreferences, name: string, symbol: ts.Symbol, location: ts.Node, contextToken: ts.Node | undefined, formatContext: ts.formatting.FormatContext | undefined): { + insertText: string; + isSnippet?: true; + importAdder?: ts.codefix.ImportAdder; + replacementSpan?: ts.TextSpan; +} { + const classLikeDeclaration = ts.findAncestor(location, ts.isClassLike); + if (!classLikeDeclaration) { + return { insertText: name }; + } + + let isSnippet: true | undefined; + let replacementSpan: ts.TextSpan | undefined; + let insertText: string = name; + + const checker = program.getTypeChecker(); + const sourceFile = location.getSourceFile(); + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + omitTrailingSemicolon: false, + newLine: ts.getNewLineKind(ts.getNewLineCharacter(options, ts.maybeBind(host, host.getNewLine))), + }); + const importAdder = ts.codefix.createImportAdder(sourceFile, program, preferences, host); + + // Create empty body for possible method implementation. + let body; + if (preferences.includeCompletionsWithSnippetText) { + isSnippet = true; + // We are adding a tabstop (i.e. `$0`) in the body of the suggested member, + // if it has one, so that the cursor ends up in the body once the completion is inserted. + // Note: this assumes we won't have more than one body in the completion nodes, which should be the case. + const emptyStmt = ts.factory.createEmptyStatement(); + body = ts.factory.createBlock([emptyStmt], /* multiline */ true); + ts.setSnippetElement(emptyStmt, { kind: ts.SnippetKind.TabStop, order: 0 }); + } + else { + body = ts.factory.createBlock([], /* multiline */ true); + } + + let modifiers = ts.ModifierFlags.None; + // Whether the suggested member should be abstract. + // e.g. in `abstract class C { abstract | }`, we should offer abstract method signatures at position `|`. + const { modifiers: presentModifiers, span: modifiersSpan } = getPresentModifiers(contextToken); + const isAbstract = !!(presentModifiers & ts.ModifierFlags.Abstract); + const completionNodes: ts.Node[] = []; + ts.codefix.addNewNodeForMemberSymbol(symbol, classLikeDeclaration, sourceFile, { program, host }, preferences, importAdder, + // `addNewNodeForMemberSymbol` calls this callback function for each new member node + // it adds for the given member symbol. + // We store these member nodes in the `completionNodes` array. + // Note: there might be: + // - No nodes if `addNewNodeForMemberSymbol` cannot figure out a node for the member; + // - One node; + // - More than one node if the member is overloaded (e.g. a method with overload signatures). + node => { + let requiredModifiers = ts.ModifierFlags.None; + if (isAbstract) { + requiredModifiers |= ts.ModifierFlags.Abstract; + } + if (ts.isClassElement(node) + && checker.getMemberOverrideModifierStatus(classLikeDeclaration, node) === ts.MemberOverrideStatus.NeedsOverride) { + requiredModifiers |= ts.ModifierFlags.Override; + } + + if (!completionNodes.length) { + // Keep track of added missing required modifiers and modifiers already present. + // This is needed when we have overloaded signatures, + // so this callback will be called for multiple nodes/signatures, + // and we need to make sure the modifiers are uniform for all nodes/signatures. + modifiers = node.modifierFlagsCache | requiredModifiers | presentModifiers; + } + node = ts.factory.updateModifiers(node, modifiers); + completionNodes.push(node); + }, body, ts.codefix.PreserveOptionalFlags.Property, isAbstract); + + if (completionNodes.length) { + const format = ts.ListFormat.MultiLine | ts.ListFormat.NoTrailingNewLine; + replacementSpan = modifiersSpan; + // If we have access to formatting settings, we print the nodes using the emitter, + // and then format the printed text. + if (formatContext) { + insertText = printer.printAndFormatSnippetList(format, ts.factory.createNodeArray(completionNodes), sourceFile, formatContext); } - else { - body = ts.factory.createBlock([], /* multiline */ true); - } - - let modifiers = ts.ModifierFlags.None; - // Whether the suggested member should be abstract. - // e.g. in `abstract class C { abstract | }`, we should offer abstract method signatures at position `|`. - const { modifiers: presentModifiers, span: modifiersSpan } = getPresentModifiers(contextToken); - const isAbstract = !!(presentModifiers & ts.ModifierFlags.Abstract); - const completionNodes: ts.Node[] = []; - ts.codefix.addNewNodeForMemberSymbol(symbol, classLikeDeclaration, sourceFile, { program, host }, preferences, importAdder, - // `addNewNodeForMemberSymbol` calls this callback function for each new member node - // it adds for the given member symbol. - // We store these member nodes in the `completionNodes` array. - // Note: there might be: - // - No nodes if `addNewNodeForMemberSymbol` cannot figure out a node for the member; - // - One node; - // - More than one node if the member is overloaded (e.g. a method with overload signatures). - node => { - let requiredModifiers = ts.ModifierFlags.None; - if (isAbstract) { - requiredModifiers |= ts.ModifierFlags.Abstract; - } - if (ts.isClassElement(node) - && checker.getMemberOverrideModifierStatus(classLikeDeclaration, node) === ts.MemberOverrideStatus.NeedsOverride) { - requiredModifiers |= ts.ModifierFlags.Override; - } + else { // Otherwise, just use emitter to print the new nodes. + insertText = printer.printSnippetList(format, ts.factory.createNodeArray(completionNodes), sourceFile); + } + } - if (!completionNodes.length) { - // Keep track of added missing required modifiers and modifiers already present. - // This is needed when we have overloaded signatures, - // so this callback will be called for multiple nodes/signatures, - // and we need to make sure the modifiers are uniform for all nodes/signatures. - modifiers = node.modifierFlagsCache | requiredModifiers | presentModifiers; - } - node = ts.factory.updateModifiers(node, modifiers); - completionNodes.push(node); - }, body, ts.codefix.PreserveOptionalFlags.Property, isAbstract); + return { insertText, isSnippet, importAdder, replacementSpan }; +} - if (completionNodes.length) { - const format = ts.ListFormat.MultiLine | ts.ListFormat.NoTrailingNewLine; - replacementSpan = modifiersSpan; - // If we have access to formatting settings, we print the nodes using the emitter, - // and then format the printed text. - if (formatContext) { - insertText = printer.printAndFormatSnippetList(format, ts.factory.createNodeArray(completionNodes), sourceFile, formatContext); - } - else { // Otherwise, just use emitter to print the new nodes. - insertText = printer.printSnippetList(format, ts.factory.createNodeArray(completionNodes), sourceFile); - } - } +function getPresentModifiers(contextToken: ts.Node | undefined): { + modifiers: ts.ModifierFlags; + span?: ts.TextSpan; +} { + if (!contextToken) { + return { modifiers: ts.ModifierFlags.None }; + } + let modifiers = ts.ModifierFlags.None; + let span; + let contextMod; + /* + Cases supported: + In + `class C { + public abstract | + }` + `contextToken` is ``abstract`` (as an identifier), + `contextToken.parent` is property declaration, + `location` is class declaration ``class C { ... }``. + In + `class C { + protected override m| + }` + `contextToken` is ``override`` (as a keyword), + `contextToken.parent` is property declaration, + `location` is identifier ``m``, + `location.parent` is property declaration ``protected override m``, + `location.parent.parent` is class declaration ``class C { ... }``. + */ + if (contextMod = isModifierLike(contextToken)) { + modifiers |= ts.modifierToFlag(contextMod); + span = ts.createTextSpanFromNode(contextToken); + } + if (ts.isPropertyDeclaration(contextToken.parent)) { + modifiers |= ts.modifiersToFlags(contextToken.parent.modifiers); + span = ts.createTextSpanFromNode(contextToken.parent); + } + return { modifiers, span }; +} - return { insertText, isSnippet, importAdder, replacementSpan }; +function isModifierLike(node: ts.Node): ts.ModifierSyntaxKind | undefined { + if (ts.isModifier(node)) { + return node.kind; } - - function getPresentModifiers(contextToken: ts.Node | undefined): { - modifiers: ts.ModifierFlags; - span?: ts.TextSpan; - } { - if (!contextToken) { - return { modifiers: ts.ModifierFlags.None }; - } - let modifiers = ts.ModifierFlags.None; - let span; - let contextMod; - /* - Cases supported: - In - `class C { - public abstract | - }` - `contextToken` is ``abstract`` (as an identifier), - `contextToken.parent` is property declaration, - `location` is class declaration ``class C { ... }``. - In - `class C { - protected override m| - }` - `contextToken` is ``override`` (as a keyword), - `contextToken.parent` is property declaration, - `location` is identifier ``m``, - `location.parent` is property declaration ``protected override m``, - `location.parent.parent` is class declaration ``class C { ... }``. - */ - if (contextMod = isModifierLike(contextToken)) { - modifiers |= ts.modifierToFlag(contextMod); - span = ts.createTextSpanFromNode(contextToken); - } - if (ts.isPropertyDeclaration(contextToken.parent)) { - modifiers |= ts.modifiersToFlags(contextToken.parent.modifiers); - span = ts.createTextSpanFromNode(contextToken.parent); - } - return { modifiers, span }; + if (ts.isIdentifier(node) && node.originalKeywordKind && ts.isModifierKind(node.originalKeywordKind)) { + return node.originalKeywordKind; } + return undefined; +} - function isModifierLike(node: ts.Node): ts.ModifierSyntaxKind | undefined { - if (ts.isModifier(node)) { - return node.kind; - } - if (ts.isIdentifier(node) && node.originalKeywordKind && ts.isModifierKind(node.originalKeywordKind)) { - return node.originalKeywordKind; - } - return undefined; - } +function getEntryForObjectLiteralMethodCompletion(symbol: ts.Symbol, name: string, enclosingDeclaration: ts.ObjectLiteralExpression, program: ts.Program, host: ts.LanguageServiceHost, options: ts.CompilerOptions, preferences: ts.UserPreferences, formatContext: ts.formatting.FormatContext | undefined): { + insertText: string; + isSnippet?: true; + labelDetails: ts.CompletionEntryLabelDetails; +} | undefined { + const isSnippet = preferences.includeCompletionsWithSnippetText || undefined; + let insertText: string = name; - function getEntryForObjectLiteralMethodCompletion(symbol: ts.Symbol, name: string, enclosingDeclaration: ts.ObjectLiteralExpression, program: ts.Program, host: ts.LanguageServiceHost, options: ts.CompilerOptions, preferences: ts.UserPreferences, formatContext: ts.formatting.FormatContext | undefined): { - insertText: string; - isSnippet?: true; - labelDetails: ts.CompletionEntryLabelDetails; - } | undefined { - const isSnippet = preferences.includeCompletionsWithSnippetText || undefined; - let insertText: string = name; + const sourceFile = enclosingDeclaration.getSourceFile(); - const sourceFile = enclosingDeclaration.getSourceFile(); - - const method = createObjectLiteralMethod(symbol, enclosingDeclaration, sourceFile, program, host, preferences); - if (!method) { - return undefined; - } + const method = createObjectLiteralMethod(symbol, enclosingDeclaration, sourceFile, program, host, preferences); + if (!method) { + return undefined; + } - const printer = createSnippetPrinter({ - removeComments: true, - module: options.module, - target: options.target, - omitTrailingSemicolon: false, - newLine: ts.getNewLineKind(ts.getNewLineCharacter(options, ts.maybeBind(host, host.getNewLine))), - }); - if (formatContext) { - insertText = printer.printAndFormatSnippetList(ts.ListFormat.CommaDelimited | ts.ListFormat.AllowTrailingComma, ts.factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile, formatContext); - } - else { - insertText = printer.printSnippetList(ts.ListFormat.CommaDelimited | ts.ListFormat.AllowTrailingComma, ts.factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile); - } + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + omitTrailingSemicolon: false, + newLine: ts.getNewLineKind(ts.getNewLineCharacter(options, ts.maybeBind(host, host.getNewLine))), + }); + if (formatContext) { + insertText = printer.printAndFormatSnippetList(ts.ListFormat.CommaDelimited | ts.ListFormat.AllowTrailingComma, ts.factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile, formatContext); + } + else { + insertText = printer.printSnippetList(ts.ListFormat.CommaDelimited | ts.ListFormat.AllowTrailingComma, ts.factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile); + } - const signaturePrinter = ts.createPrinter({ - removeComments: true, - module: options.module, - target: options.target, - omitTrailingSemicolon: true, - }); - // The `labelDetails.detail` will be displayed right beside the method name, - // so we drop the name (and modifiers) from the signature. - const methodSignature = ts.factory.createMethodSignature( - /*modifiers*/ undefined, - /*name*/ "", method.questionToken, method.typeParameters, method.parameters, method.type); - const labelDetails = { detail: signaturePrinter.printNode(ts.EmitHint.Unspecified, methodSignature, sourceFile) }; + const signaturePrinter = ts.createPrinter({ + removeComments: true, + module: options.module, + target: options.target, + omitTrailingSemicolon: true, + }); + // The `labelDetails.detail` will be displayed right beside the method name, + // so we drop the name (and modifiers) from the signature. + const methodSignature = ts.factory.createMethodSignature( + /*modifiers*/ undefined, + /*name*/ "", method.questionToken, method.typeParameters, method.parameters, method.type); + const labelDetails = { detail: signaturePrinter.printNode(ts.EmitHint.Unspecified, methodSignature, sourceFile) }; - return { isSnippet, insertText, labelDetails }; + return { isSnippet, insertText, labelDetails }; +} +; +function createObjectLiteralMethod(symbol: ts.Symbol, enclosingDeclaration: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): ts.MethodDeclaration | undefined { + const declarations = symbol.getDeclarations(); + if (!(declarations && declarations.length)) { + return undefined; } - ; - function createObjectLiteralMethod(symbol: ts.Symbol, enclosingDeclaration: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): ts.MethodDeclaration | undefined { - const declarations = symbol.getDeclarations(); - if (!(declarations && declarations.length)) { - return undefined; - } - const checker = program.getTypeChecker(); - const declaration = declarations[0]; - const name = ts.getSynthesizedDeepClone(ts.getNameOfDeclaration(declaration), /*includeTrivia*/ false) as ts.PropertyName; - const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); - const quotePreference = ts.getQuotePreference(sourceFile, preferences); - const builderFlags = quotePreference === ts.QuotePreference.Single ? ts.NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined; - - switch (declaration.kind) { - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.MethodDeclaration: { - let effectiveType = type.flags & ts.TypeFlags.Union && (type as ts.UnionType).types.length < 10 - ? checker.getUnionType((type as ts.UnionType).types, ts.UnionReduction.Subtype) - : type; - if (effectiveType.flags & ts.TypeFlags.Union) { - // Only offer the completion if there's a single function type component. - const functionTypes = ts.filter((effectiveType as ts.UnionType).types, type => checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0); - if (functionTypes.length === 1) { - effectiveType = functionTypes[0]; - } - else { - return undefined; - } - } - const signatures = checker.getSignaturesOfType(effectiveType, ts.SignatureKind.Call); - if (signatures.length !== 1) { - // We don't support overloads in object literals. - return undefined; + const checker = program.getTypeChecker(); + const declaration = declarations[0]; + const name = ts.getSynthesizedDeepClone(ts.getNameOfDeclaration(declaration), /*includeTrivia*/ false) as ts.PropertyName; + const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); + const quotePreference = ts.getQuotePreference(sourceFile, preferences); + const builderFlags = quotePreference === ts.QuotePreference.Single ? ts.NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined; + + switch (declaration.kind) { + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.MethodDeclaration: { + let effectiveType = type.flags & ts.TypeFlags.Union && (type as ts.UnionType).types.length < 10 + ? checker.getUnionType((type as ts.UnionType).types, ts.UnionReduction.Subtype) + : type; + if (effectiveType.flags & ts.TypeFlags.Union) { + // Only offer the completion if there's a single function type component. + const functionTypes = ts.filter((effectiveType as ts.UnionType).types, type => checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0); + if (functionTypes.length === 1) { + effectiveType = functionTypes[0]; } - const typeNode = checker.typeToTypeNode(effectiveType, enclosingDeclaration, builderFlags, ts.codefix.getNoopSymbolTrackerWithResolver({ program, host })); - if (!typeNode || !ts.isFunctionTypeNode(typeNode)) { + else { return undefined; } + } + const signatures = checker.getSignaturesOfType(effectiveType, ts.SignatureKind.Call); + if (signatures.length !== 1) { + // We don't support overloads in object literals. + return undefined; + } + const typeNode = checker.typeToTypeNode(effectiveType, enclosingDeclaration, builderFlags, ts.codefix.getNoopSymbolTrackerWithResolver({ program, host })); + if (!typeNode || !ts.isFunctionTypeNode(typeNode)) { + return undefined; + } - let body; - if (preferences.includeCompletionsWithSnippetText) { - const emptyStmt = ts.factory.createEmptyStatement(); - body = ts.factory.createBlock([emptyStmt], /* multiline */ true); - ts.setSnippetElement(emptyStmt, { kind: ts.SnippetKind.TabStop, order: 0 }); - } - else { - body = ts.factory.createBlock([], /* multiline */ true); - } + let body; + if (preferences.includeCompletionsWithSnippetText) { + const emptyStmt = ts.factory.createEmptyStatement(); + body = ts.factory.createBlock([emptyStmt], /* multiline */ true); + ts.setSnippetElement(emptyStmt, { kind: ts.SnippetKind.TabStop, order: 0 }); + } + else { + body = ts.factory.createBlock([], /* multiline */ true); + } - const parameters = typeNode.parameters.map(typedParam => ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, typedParam.dotDotDotToken, typedParam.name, typedParam.questionToken, - /*type*/ undefined, typedParam.initializer)); - return ts.factory.createMethodDeclaration( + const parameters = typeNode.parameters.map(typedParam => ts.factory.createParameterDeclaration( /*decorators*/ undefined, - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, name, - /*questionToken*/ undefined, - /*typeParameters*/ undefined, parameters, - /*type*/ undefined, body); - } - default: - return undefined; - } + /*modifiers*/ undefined, typedParam.dotDotDotToken, typedParam.name, typedParam.questionToken, + /*type*/ undefined, typedParam.initializer)); + return ts.factory.createMethodDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, name, + /*questionToken*/ undefined, + /*typeParameters*/ undefined, parameters, + /*type*/ undefined, body); + } + default: + return undefined; } +} - function createSnippetPrinter(printerOptions: ts.PrinterOptions) { - let escapes: ts.TextChange[] | undefined; - const baseWriter = ts.textChanges.createWriter(ts.getNewLineCharacter(printerOptions)); - const printer = ts.createPrinter(printerOptions, baseWriter); - const writer: ts.EmitTextWriter = { - ...baseWriter, - write: s => escapingWrite(s, () => baseWriter.write(s)), - nonEscapingWrite: baseWriter.write, - writeLiteral: s => escapingWrite(s, () => baseWriter.writeLiteral(s)), - writeStringLiteral: s => escapingWrite(s, () => baseWriter.writeStringLiteral(s)), - writeSymbol: (s, symbol) => escapingWrite(s, () => baseWriter.writeSymbol(s, symbol)), - writeParameter: s => escapingWrite(s, () => baseWriter.writeParameter(s)), - writeComment: s => escapingWrite(s, () => baseWriter.writeComment(s)), - writeProperty: s => escapingWrite(s, () => baseWriter.writeProperty(s)), - }; - - return { - printSnippetList, - printAndFormatSnippetList, - }; +function createSnippetPrinter(printerOptions: ts.PrinterOptions) { + let escapes: ts.TextChange[] | undefined; + const baseWriter = ts.textChanges.createWriter(ts.getNewLineCharacter(printerOptions)); + const printer = ts.createPrinter(printerOptions, baseWriter); + const writer: ts.EmitTextWriter = { + ...baseWriter, + write: s => escapingWrite(s, () => baseWriter.write(s)), + nonEscapingWrite: baseWriter.write, + writeLiteral: s => escapingWrite(s, () => baseWriter.writeLiteral(s)), + writeStringLiteral: s => escapingWrite(s, () => baseWriter.writeStringLiteral(s)), + writeSymbol: (s, symbol) => escapingWrite(s, () => baseWriter.writeSymbol(s, symbol)), + writeParameter: s => escapingWrite(s, () => baseWriter.writeParameter(s)), + writeComment: s => escapingWrite(s, () => baseWriter.writeComment(s)), + writeProperty: s => escapingWrite(s, () => baseWriter.writeProperty(s)), + }; - // The formatter/scanner will have issues with snippet-escaped text, - // so instead of writing the escaped text directly to the writer, - // generate a set of changes that can be applied to the unescaped text - // to escape it post-formatting. - function escapingWrite(s: string, write: () => void) { - const escaped = ts.escapeSnippetText(s); - if (escaped !== s) { - const start = baseWriter.getTextPos(); - write(); - const end = baseWriter.getTextPos(); - escapes = ts.append(escapes ||= [], { newText: escaped, span: { start, length: end - start } }); - } - else { - write(); - } - } + return { + printSnippetList, + printAndFormatSnippetList, + }; - /* Snippet-escaping version of `printer.printList`. */ - function printSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile | undefined): string { - const unescaped = printUnescapedSnippetList(format, list, sourceFile); - return escapes ? ts.textChanges.applyChanges(unescaped, escapes) : unescaped; + // The formatter/scanner will have issues with snippet-escaped text, + // so instead of writing the escaped text directly to the writer, + // generate a set of changes that can be applied to the unescaped text + // to escape it post-formatting. + function escapingWrite(s: string, write: () => void) { + const escaped = ts.escapeSnippetText(s); + if (escaped !== s) { + const start = baseWriter.getTextPos(); + write(); + const end = baseWriter.getTextPos(); + escapes = ts.append(escapes ||= [], { newText: escaped, span: { start, length: end - start } }); } - function printUnescapedSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile | undefined): string { - escapes = undefined; - writer.clear(); - printer.writeList(format, list, sourceFile, writer); - return writer.getText(); + else { + write(); } + } - function printAndFormatSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile, formatContext: ts.formatting.FormatContext): string { - const syntheticFile = { - text: printUnescapedSnippetList(format, list, sourceFile), - getLineAndCharacterOfPosition(pos: number) { - return ts.getLineAndCharacterOfPosition(this, pos); - }, - }; + /* Snippet-escaping version of `printer.printList`. */ + function printSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile | undefined): string { + const unescaped = printUnescapedSnippetList(format, list, sourceFile); + return escapes ? ts.textChanges.applyChanges(unescaped, escapes) : unescaped; + } + function printUnescapedSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile | undefined): string { + escapes = undefined; + writer.clear(); + printer.writeList(format, list, sourceFile, writer); + return writer.getText(); + } - const formatOptions = ts.getFormatCodeSettingsForWriting(formatContext, sourceFile); - const changes = ts.flatMap(list, node => { - const nodeWithPos = ts.textChanges.assignPositionsToNode(node); - return ts.formatting.formatNodeGivenIndentation(nodeWithPos, syntheticFile, sourceFile.languageVariant, - /* indentation */ 0, - /* delta */ 0, { ...formatContext, options: formatOptions }); - }); + function printAndFormatSnippetList(format: ts.ListFormat, list: ts.NodeArray, sourceFile: ts.SourceFile, formatContext: ts.formatting.FormatContext): string { + const syntheticFile = { + text: printUnescapedSnippetList(format, list, sourceFile), + getLineAndCharacterOfPosition(pos: number) { + return ts.getLineAndCharacterOfPosition(this, pos); + }, + }; - const allChanges = escapes - ? ts.stableSort(ts.concatenate(changes, escapes), (a, b) => ts.compareTextSpans(a.span, b.span)) - : changes; - return ts.textChanges.applyChanges(syntheticFile.text, allChanges); - } + const formatOptions = ts.getFormatCodeSettingsForWriting(formatContext, sourceFile); + const changes = ts.flatMap(list, node => { + const nodeWithPos = ts.textChanges.assignPositionsToNode(node); + return ts.formatting.formatNodeGivenIndentation(nodeWithPos, syntheticFile, sourceFile.languageVariant, + /* indentation */ 0, + /* delta */ 0, { ...formatContext, options: formatOptions }); + }); + + const allChanges = escapes + ? ts.stableSort(ts.concatenate(changes, escapes), (a, b) => ts.compareTextSpans(a.span, b.span)) + : changes; + return ts.textChanges.applyChanges(syntheticFile.text, allChanges); } +} - function originToCompletionEntryData(origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport): ts.CompletionEntryData | undefined { - const ambientModuleName = origin.fileName ? undefined : ts.stripQuotes(origin.moduleSymbol.name); - const isPackageJsonImport = origin.isFromPackageJson ? true : undefined; - if (originIsResolvedExport(origin)) { - const resolvedData: ts.CompletionEntryDataResolved = { - exportName: origin.exportName, - moduleSpecifier: origin.moduleSpecifier, - ambientModuleName, - fileName: origin.fileName, - isPackageJsonImport, - }; - return resolvedData; - } - const unresolvedData: ts.CompletionEntryDataUnresolved = { +function originToCompletionEntryData(origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport): ts.CompletionEntryData | undefined { + const ambientModuleName = origin.fileName ? undefined : ts.stripQuotes(origin.moduleSymbol.name); + const isPackageJsonImport = origin.isFromPackageJson ? true : undefined; + if (originIsResolvedExport(origin)) { + const resolvedData: ts.CompletionEntryDataResolved = { exportName: origin.exportName, - exportMapKey: origin.exportMapKey, + moduleSpecifier: origin.moduleSpecifier, + ambientModuleName, fileName: origin.fileName, - ambientModuleName: origin.fileName ? undefined : ts.stripQuotes(origin.moduleSymbol.name), - isPackageJsonImport: origin.isFromPackageJson ? true : undefined, + isPackageJsonImport, }; - return unresolvedData; - } - - function completionEntryDataToSymbolOriginInfo(data: ts.CompletionEntryData, completionName: string, moduleSymbol: ts.Symbol): SymbolOriginInfoExport | SymbolOriginInfoResolvedExport { - const isDefaultExport = data.exportName === ts.InternalSymbolName.Default; - const isFromPackageJson = !!data.isPackageJsonImport; - if (completionEntryDataIsResolved(data)) { - const resolvedOrigin: SymbolOriginInfoResolvedExport = { - kind: SymbolOriginInfoKind.ResolvedExport, - exportName: data.exportName, - moduleSpecifier: data.moduleSpecifier, - symbolName: completionName, - fileName: data.fileName, - moduleSymbol, - isDefaultExport, - isFromPackageJson, - }; - return resolvedOrigin; - } - const unresolvedOrigin: SymbolOriginInfoExport = { - kind: SymbolOriginInfoKind.Export, + return resolvedData; + } + const unresolvedData: ts.CompletionEntryDataUnresolved = { + exportName: origin.exportName, + exportMapKey: origin.exportMapKey, + fileName: origin.fileName, + ambientModuleName: origin.fileName ? undefined : ts.stripQuotes(origin.moduleSymbol.name), + isPackageJsonImport: origin.isFromPackageJson ? true : undefined, + }; + return unresolvedData; +} + +function completionEntryDataToSymbolOriginInfo(data: ts.CompletionEntryData, completionName: string, moduleSymbol: ts.Symbol): SymbolOriginInfoExport | SymbolOriginInfoResolvedExport { + const isDefaultExport = data.exportName === ts.InternalSymbolName.Default; + const isFromPackageJson = !!data.isPackageJsonImport; + if (completionEntryDataIsResolved(data)) { + const resolvedOrigin: SymbolOriginInfoResolvedExport = { + kind: SymbolOriginInfoKind.ResolvedExport, exportName: data.exportName, - exportMapKey: data.exportMapKey, + moduleSpecifier: data.moduleSpecifier, symbolName: completionName, fileName: data.fileName, moduleSymbol, isDefaultExport, isFromPackageJson, }; - return unresolvedOrigin; + return resolvedOrigin; + } + const unresolvedOrigin: SymbolOriginInfoExport = { + kind: SymbolOriginInfoKind.Export, + exportName: data.exportName, + exportMapKey: data.exportMapKey, + symbolName: completionName, + fileName: data.fileName, + moduleSymbol, + isDefaultExport, + isFromPackageJson, + }; + return unresolvedOrigin; +} + +function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: ts.Node, contextToken: ts.Node | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: ts.CompilerOptions, preferences: ts.UserPreferences) { + const sourceFile = importCompletionNode.getSourceFile(); + const replacementSpan = ts.createTextSpanFromNode(ts.findAncestor(importCompletionNode, ts.or(ts.isImportDeclaration, ts.isImportEqualsDeclaration)) || importCompletionNode, sourceFile); + const quotedModuleSpecifier = ts.quote(sourceFile, preferences, origin.moduleSpecifier); + const exportKind = origin.isDefaultExport ? ts.ExportKind.Default : + origin.exportName === ts.InternalSymbolName.ExportEquals ? ts.ExportKind.ExportEquals : + ts.ExportKind.Named; + const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : ""; + const importKind = ts.codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true); + const isTopLevelTypeOnly = ts.tryCast(importCompletionNode, ts.isImportDeclaration)?.importClause?.isTypeOnly || ts.tryCast(importCompletionNode, ts.isImportEqualsDeclaration)?.isTypeOnly; + const isImportSpecifierTypeOnly = couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken); + const topLevelTypeOnlyText = isTopLevelTypeOnly ? ` ${ts.tokenToString(ts.SyntaxKind.TypeKeyword)} ` : " "; + const importSpecifierTypeOnlyText = isImportSpecifierTypeOnly ? `${ts.tokenToString(ts.SyntaxKind.TypeKeyword)} ` : ""; + const suffix = useSemicolons ? ";" : ""; + switch (importKind) { + case ts.ImportKind.CommonJS: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` }; + case ts.ImportKind.Default: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; + case ts.ImportKind.Namespace: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}* as ${ts.escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` }; + case ts.ImportKind.Named: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}{ ${importSpecifierTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` }; } +} - function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: ts.Node, contextToken: ts.Node | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: ts.CompilerOptions, preferences: ts.UserPreferences) { - const sourceFile = importCompletionNode.getSourceFile(); - const replacementSpan = ts.createTextSpanFromNode(ts.findAncestor(importCompletionNode, ts.or(ts.isImportDeclaration, ts.isImportEqualsDeclaration)) || importCompletionNode, sourceFile); - const quotedModuleSpecifier = ts.quote(sourceFile, preferences, origin.moduleSpecifier); - const exportKind = origin.isDefaultExport ? ts.ExportKind.Default : - origin.exportName === ts.InternalSymbolName.ExportEquals ? ts.ExportKind.ExportEquals : - ts.ExportKind.Named; - const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : ""; - const importKind = ts.codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true); - const isTopLevelTypeOnly = ts.tryCast(importCompletionNode, ts.isImportDeclaration)?.importClause?.isTypeOnly || ts.tryCast(importCompletionNode, ts.isImportEqualsDeclaration)?.isTypeOnly; - const isImportSpecifierTypeOnly = couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken); - const topLevelTypeOnlyText = isTopLevelTypeOnly ? ` ${ts.tokenToString(ts.SyntaxKind.TypeKeyword)} ` : " "; - const importSpecifierTypeOnlyText = isImportSpecifierTypeOnly ? `${ts.tokenToString(ts.SyntaxKind.TypeKeyword)} ` : ""; - const suffix = useSemicolons ? ";" : ""; - switch (importKind) { - case ts.ImportKind.CommonJS: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` }; - case ts.ImportKind.Default: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; - case ts.ImportKind.Namespace: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}* as ${ts.escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` }; - case ts.ImportKind.Named: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}{ ${importSpecifierTypeOnlyText}${ts.escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` }; - } +function quotePropertyName(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, name: string): string { + if (/^\d+$/.test(name)) { + return name; } - function quotePropertyName(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, name: string): string { - if (/^\d+$/.test(name)) { - return name; - } + return ts.quote(sourceFile, preferences, name); +} - return ts.quote(sourceFile, preferences, name); - } +function isRecommendedCompletionMatch(localSymbol: ts.Symbol, recommendedCompletion: ts.Symbol | undefined, checker: ts.TypeChecker): boolean { + return localSymbol === recommendedCompletion || + !!(localSymbol.flags & ts.SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion; +} - function isRecommendedCompletionMatch(localSymbol: ts.Symbol, recommendedCompletion: ts.Symbol | undefined, checker: ts.TypeChecker): boolean { - return localSymbol === recommendedCompletion || - !!(localSymbol.flags & ts.SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion; +function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined { + if (originIsExport(origin)) { + return ts.stripQuotes(origin.moduleSymbol.name); } - - function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined { - if (originIsExport(origin)) { - return ts.stripQuotes(origin.moduleSymbol.name); - } - if (originIsResolvedExport(origin)) { - return origin.moduleSpecifier; - } - if (origin?.kind === SymbolOriginInfoKind.ThisType) { - return CompletionSource.ThisProperty; - } - if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) { - return CompletionSource.TypeOnlyAlias; - } + if (originIsResolvedExport(origin)) { + return origin.moduleSpecifier; + } + if (origin?.kind === SymbolOriginInfoKind.ThisType) { + return CompletionSource.ThisProperty; } + if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) { + return CompletionSource.TypeOnlyAlias; + } +} - export function getCompletionEntriesFromSymbols(symbols: readonly ts.Symbol[], entries: ts.SortedArray, replacementToken: ts.Node | undefined, contextToken: ts.Node | undefined, location: ts.Node, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, target: ts.ScriptTarget, log: Log, kind: CompletionKind, preferences: ts.UserPreferences, compilerOptions: ts.CompilerOptions, formatContext: ts.formatting.FormatContext | undefined, isTypeOnlyLocation?: boolean, propertyAccessToConvert?: ts.PropertyAccessExpression, jsxIdentifierExpected?: boolean, isJsxInitializer?: IsJsxInitializer, importCompletionNode?: ts.Node, recommendedCompletion?: ts.Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, symbolToSortTextMap?: SymbolSortTextMap, isJsxIdentifierExpected?: boolean, isRightOfOpenTag?: boolean): UniqueNameSet { - const start = ts.timestamp(); - const variableDeclaration = getVariableDeclaration(location); - const useSemicolons = ts.probablyUsesSemicolons(sourceFile); - const typeChecker = program.getTypeChecker(); - // Tracks unique names. - // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; - // true otherwise. 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 = new ts.Map(); - for (let i = 0; i < symbols.length; i++) { - const symbol = symbols[i]; - const origin = symbolToOriginInfoMap?.[i]; - const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); - if (!info || (uniques.get(info.name) && (!origin || !originIsObjectLiteralMethod(origin))) || kind === CompletionKind.Global && symbolToSortTextMap && !shouldIncludeSymbol(symbol, symbolToSortTextMap)) { - continue; - } +export function getCompletionEntriesFromSymbols(symbols: readonly ts.Symbol[], entries: ts.SortedArray, replacementToken: ts.Node | undefined, contextToken: ts.Node | undefined, location: ts.Node, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, target: ts.ScriptTarget, log: Log, kind: CompletionKind, preferences: ts.UserPreferences, compilerOptions: ts.CompilerOptions, formatContext: ts.formatting.FormatContext | undefined, isTypeOnlyLocation?: boolean, propertyAccessToConvert?: ts.PropertyAccessExpression, jsxIdentifierExpected?: boolean, isJsxInitializer?: IsJsxInitializer, importCompletionNode?: ts.Node, recommendedCompletion?: ts.Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, symbolToSortTextMap?: SymbolSortTextMap, isJsxIdentifierExpected?: boolean, isRightOfOpenTag?: boolean): UniqueNameSet { + const start = ts.timestamp(); + const variableDeclaration = getVariableDeclaration(location); + const useSemicolons = ts.probablyUsesSemicolons(sourceFile); + const typeChecker = program.getTypeChecker(); + // Tracks unique names. + // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; + // true otherwise. 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 = new ts.Map(); + for (let i = 0; i < symbols.length; i++) { + const symbol = symbols[i]; + const origin = symbolToOriginInfoMap?.[i]; + const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); + if (!info || (uniques.get(info.name) && (!origin || !originIsObjectLiteralMethod(origin))) || kind === CompletionKind.Global && symbolToSortTextMap && !shouldIncludeSymbol(symbol, symbolToSortTextMap)) { + continue; + } + + const { name, needsConvertPropertyAccess } = info; + const originalSortText = symbolToSortTextMap?.[ts.getSymbolId(symbol)] ?? SortText.LocationPriority; + const sortText = (isDeprecated(symbol, typeChecker) ? SortText.Deprecated(originalSortText) : originalSortText); + const entry = createCompletionEntry(symbol, sortText, replacementToken, contextToken, location, sourceFile, host, program, name, needsConvertPropertyAccess, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, importCompletionNode, useSemicolons, compilerOptions, preferences, kind, formatContext, isJsxIdentifierExpected, isRightOfOpenTag); + if (!entry) { + continue; + } + + /** True for locals; false for globals, module exports from other files, `this.` completions. */ + const shouldShadowLaterSymbols = (!origin || originIsTypeOnlyAlias(origin)) && !(symbol.parent === undefined && !ts.some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile())); + uniques.set(name, shouldShadowLaterSymbols); + ts.insertSorted(entries, entry, compareCompletionEntries, /*allowDuplicates*/ true); + } + + log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (ts.timestamp() - start)); + + // Prevent consumers of this map from having to worry about + // the boolean value. Externally, it should be seen as the + // set of all names. + return { + has: name => uniques.has(name), + add: name => uniques.set(name, true), + }; - const { name, needsConvertPropertyAccess } = info; - const originalSortText = symbolToSortTextMap?.[ts.getSymbolId(symbol)] ?? SortText.LocationPriority; - const sortText = (isDeprecated(symbol, typeChecker) ? SortText.Deprecated(originalSortText) : originalSortText); - const entry = createCompletionEntry(symbol, sortText, replacementToken, contextToken, location, sourceFile, host, program, name, needsConvertPropertyAccess, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, importCompletionNode, useSemicolons, compilerOptions, preferences, kind, formatContext, isJsxIdentifierExpected, isRightOfOpenTag); - if (!entry) { - continue; + function shouldIncludeSymbol(symbol: ts.Symbol, symbolToSortTextMap: SymbolSortTextMap): boolean { + let allFlags = symbol.flags; + if (!ts.isSourceFile(location)) { + // export = /**/ here we want to get all meanings, so any symbol is ok + if (ts.isExportAssignment(location.parent)) { + return true; + } + // Filter out variables from their own initializers + // `const a = /* no 'a' here */` + if (variableDeclaration && symbol.valueDeclaration === variableDeclaration) { + return false; } - /** True for locals; false for globals, module exports from other files, `this.` completions. */ - const shouldShadowLaterSymbols = (!origin || originIsTypeOnlyAlias(origin)) && !(symbol.parent === undefined && !ts.some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile())); - uniques.set(name, shouldShadowLaterSymbols); - ts.insertSorted(entries, entry, compareCompletionEntries, /*allowDuplicates*/ true); - } + // External modules can have global export declarations that will be + // available as global keywords in all scopes. But if the external module + // already has an explicit export and user only wants to user explicit + // module imports then the global keywords will be filtered out so auto + // import suggestions will win in the completion + const symbolOrigin = ts.skipAlias(symbol, typeChecker); + // We only want to filter out the global keywords + // Auto Imports are not available for scripts so this conditional is always false + if (!!sourceFile.externalModuleIndicator + && !compilerOptions.allowUmdGlobalAccess + && symbolToSortTextMap[ts.getSymbolId(symbol)] === SortText.GlobalsOrKeywords + && (symbolToSortTextMap[ts.getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions + || symbolToSortTextMap[ts.getSymbolId(symbolOrigin)] === SortText.LocationPriority)) { + return false; + } - log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (ts.timestamp() - start)); + allFlags |= ts.getCombinedLocalAndExportSymbolFlags(symbolOrigin); - // Prevent consumers of this map from having to worry about - // the boolean value. Externally, it should be seen as the - // set of all names. - return { - has: name => uniques.has(name), - add: name => uniques.set(name, true), - }; + // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) + if (ts.isInRightSideOfInternalImportEqualsDeclaration(location)) { + return !!(allFlags & ts.SymbolFlags.Namespace); + } - function shouldIncludeSymbol(symbol: ts.Symbol, symbolToSortTextMap: SymbolSortTextMap): boolean { - let allFlags = symbol.flags; - if (!ts.isSourceFile(location)) { - // export = /**/ here we want to get all meanings, so any symbol is ok - if (ts.isExportAssignment(location.parent)) { - return true; - } - // Filter out variables from their own initializers - // `const a = /* no 'a' here */` - if (variableDeclaration && symbol.valueDeclaration === variableDeclaration) { - return false; - } + if (isTypeOnlyLocation) { + // It's a type, but you can reach it by namespace.type as well + return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); + } + } - // External modules can have global export declarations that will be - // available as global keywords in all scopes. But if the external module - // already has an explicit export and user only wants to user explicit - // module imports then the global keywords will be filtered out so auto - // import suggestions will win in the completion - const symbolOrigin = ts.skipAlias(symbol, typeChecker); - // We only want to filter out the global keywords - // Auto Imports are not available for scripts so this conditional is always false - if (!!sourceFile.externalModuleIndicator - && !compilerOptions.allowUmdGlobalAccess - && symbolToSortTextMap[ts.getSymbolId(symbol)] === SortText.GlobalsOrKeywords - && (symbolToSortTextMap[ts.getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions - || symbolToSortTextMap[ts.getSymbolId(symbolOrigin)] === SortText.LocationPriority)) { - return false; - } + // expressions are value space (which includes the value namespaces) + return !!(allFlags & ts.SymbolFlags.Value); + } +} - allFlags |= ts.getCombinedLocalAndExportSymbolFlags(symbolOrigin); +function getLabelCompletionAtPosition(node: ts.BreakOrContinueStatement): ts.CompletionInfo | undefined { + const entries = getLabelStatementCompletions(node); + if (entries.length) { + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; + } +} - // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) - if (ts.isInRightSideOfInternalImportEqualsDeclaration(location)) { - return !!(allFlags & ts.SymbolFlags.Namespace); - } +function getLabelStatementCompletions(node: ts.Node): ts.CompletionEntry[] { + const entries: ts.CompletionEntry[] = []; + const uniques = new ts.Map(); + let current = node; - if (isTypeOnlyLocation) { - // It's a type, but you can reach it by namespace.type as well - return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); - } + while (current) { + if (ts.isFunctionLike(current)) { + break; + } + if (ts.isLabeledStatement(current)) { + const name = current.label.text; + if (!uniques.has(name)) { + uniques.set(name, true); + entries.push({ + name, + kindModifiers: ts.ScriptElementKindModifier.none, + kind: ts.ScriptElementKind.label, + sortText: SortText.LocationPriority + }); } - - // expressions are value space (which includes the value namespaces) - return !!(allFlags & ts.SymbolFlags.Value); } + current = current.parent; } + return entries; +} - function getLabelCompletionAtPosition(node: ts.BreakOrContinueStatement): ts.CompletionInfo | undefined { - const entries = getLabelStatementCompletions(node); - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; +interface SymbolCompletion { + type: "symbol"; + symbol: ts.Symbol; + location: ts.Node; + origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined; + previousToken: ts.Node | undefined; + contextToken: ts.Node | undefined; + readonly isJsxInitializer: IsJsxInitializer; + readonly isTypeOnlyLocation: boolean; +} +function getSymbolCompletionFromEntryId(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): SymbolCompletion | { + type: "request"; + request: Request; +} | { + type: "literal"; + literal: string | number | ts.PseudoBigInt; +} | { + type: "none"; +} { + if (entryId.data) { + const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host); + if (autoImport) { + const { contextToken, previousToken } = getRelevantTokens(position, sourceFile); + return { + type: "symbol", + symbol: autoImport.symbol, + location: ts.getTouchingPropertyName(sourceFile, position), + previousToken, + contextToken, + isJsxInitializer: false, + isTypeOnlyLocation: false, + origin: autoImport.origin, + }; } } - function getLabelStatementCompletions(node: ts.Node): ts.CompletionEntry[] { - const entries: ts.CompletionEntry[] = []; - const uniques = new ts.Map(); - let current = node; + const compilerOptions = program.getCompilerOptions(); + const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host, /*formatContext*/ undefined); + if (!completionData) { + return { type: "none" }; + } + if (completionData.kind !== CompletionDataKind.Data) { + return { type: "request", request: completionData }; + } - while (current) { - if (ts.isFunctionLike(current)) { - break; - } - if (ts.isLabeledStatement(current)) { - const name = current.label.text; - if (!uniques.has(name)) { - uniques.set(name, true); - entries.push({ - name, - kindModifiers: ts.ScriptElementKindModifier.none, - kind: ts.ScriptElementKind.label, - sortText: SortText.LocationPriority - }); - } - } - current = current.parent; - } - return entries; - } - - interface SymbolCompletion { - type: "symbol"; - symbol: ts.Symbol; - location: ts.Node; - origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined; - previousToken: ts.Node | undefined; - contextToken: ts.Node | undefined; - readonly isJsxInitializer: IsJsxInitializer; - readonly isTypeOnlyLocation: boolean; - } - function getSymbolCompletionFromEntryId(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): SymbolCompletion | { - type: "request"; - request: Request; - } | { - type: "literal"; - literal: string | number | ts.PseudoBigInt; - } | { - type: "none"; - } { - if (entryId.data) { - const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host); - if (autoImport) { - const { contextToken, previousToken } = getRelevantTokens(position, sourceFile); - return { - type: "symbol", - symbol: autoImport.symbol, - location: ts.getTouchingPropertyName(sourceFile, position), - previousToken, - contextToken, - isJsxInitializer: false, - isTypeOnlyLocation: false, - origin: autoImport.origin, - }; + const { symbols, literals, location, completionKind, symbolToOriginInfoMap, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData; + + const literal = ts.find(literals, l => completionNameForLiteral(sourceFile, preferences, l) === 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 ts.firstDefined(symbols, (symbol, index): SymbolCompletion | undefined => { + const origin = symbolToOriginInfoMap[index]; + const info = getCompletionEntryDisplayNameForSymbol(symbol, ts.getEmitScriptTarget(compilerOptions), origin, completionKind, completionData.isJsxIdentifierExpected); + return info && info.name === entryId.name && (entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & ts.SymbolFlags.ClassMember + || entryId.source === CompletionSource.ObjectLiteralMethodSnippet && symbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method) + || getSourceFromOrigin(origin) === entryId.source) + ? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } + : undefined; + }) || { type: "none" }; +} + +export interface CompletionEntryIdentifier { + name: string; + source?: string; + data?: ts.CompletionEntryData; +} +export function getCompletionEntryDetails(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, cancellationToken: ts.CancellationToken): ts.CompletionEntryDetails | undefined { + const typeChecker = program.getTypeChecker(); + const compilerOptions = program.getCompilerOptions(); + const { name, source, data } = entryId; + + const contextToken = ts.findPrecedingToken(position, sourceFile); + if (ts.isInString(sourceFile, position, contextToken)) { + return ts.Completions.StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken, preferences); + } + + // 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 ts.JsDoc.getJSDocTagNameCompletionDetails(name); + case CompletionDataKind.JsDocTag: + return ts.JsDoc.getJSDocTagCompletionDetails(name); + case CompletionDataKind.JsDocParameterName: + return ts.JsDoc.getJSDocParameterNameCompletionDetails(name); + case CompletionDataKind.Keywords: + return ts.some(request.keywordCompletions, c => c.name === name) ? createSimpleDetails(name, ts.ScriptElementKind.keyword, ts.SymbolDisplayPartKind.keyword) : undefined; + default: + return ts.Debug.assertNever(request); } } - - const compilerOptions = program.getCompilerOptions(); - const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host, /*formatContext*/ undefined); - if (!completionData) { - return { type: "none" }; + case "symbol": { + const { symbol, location, contextToken, origin, previousToken } = symbolCompletion; + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source); + return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } - if (completionData.kind !== CompletionDataKind.Data) { - return { type: "request", request: completionData }; + case "literal": { + const { literal } = symbolCompletion; + return createSimpleDetails(completionNameForLiteral(sourceFile, preferences, literal), ts.ScriptElementKind.string, typeof literal === "string" ? ts.SymbolDisplayPartKind.stringLiteral : ts.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, ts.ScriptElementKind.keyword, ts.SymbolDisplayPartKind.keyword) : undefined; + default: + ts.Debug.assertNever(symbolCompletion); + } +} - const { symbols, literals, location, completionKind, symbolToOriginInfoMap, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData; +function createSimpleDetails(name: string, kind: ts.ScriptElementKind, kind2: ts.SymbolDisplayPartKind): ts.CompletionEntryDetails { + return createCompletionDetails(name, ts.ScriptElementKindModifier.none, kind, [ts.displayPart(name, kind2)]); +} - const literal = ts.find(literals, l => completionNameForLiteral(sourceFile, preferences, l) === entryId.name); - if (literal !== undefined) - return { type: "literal", literal }; +export function createCompletionDetailsForSymbol(symbol: ts.Symbol, checker: ts.TypeChecker, sourceFile: ts.SourceFile, location: ts.Node, cancellationToken: ts.CancellationToken, codeActions?: ts.CodeAction[], sourceDisplay?: ts.SymbolDisplayPart[]): ts.CompletionEntryDetails { + const { displayParts, documentation, symbolKind, tags } = checker.runWithCancellationToken(cancellationToken, checker => ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, ts.SemanticMeaning.All)); + return createCompletionDetails(symbol.name, ts.SymbolDisplay.getSymbolModifiers(checker, symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay); +} - // 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 ts.firstDefined(symbols, (symbol, index): SymbolCompletion | undefined => { - const origin = symbolToOriginInfoMap[index]; - const info = getCompletionEntryDisplayNameForSymbol(symbol, ts.getEmitScriptTarget(compilerOptions), origin, completionKind, completionData.isJsxIdentifierExpected); - return info && info.name === entryId.name && (entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & ts.SymbolFlags.ClassMember - || entryId.source === CompletionSource.ObjectLiteralMethodSnippet && symbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method) - || getSourceFromOrigin(origin) === entryId.source) - ? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } - : undefined; - }) || { type: "none" }; - } - - export interface CompletionEntryIdentifier { - name: string; - source?: string; - data?: ts.CompletionEntryData; - } - export function getCompletionEntryDetails(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, cancellationToken: ts.CancellationToken): ts.CompletionEntryDetails | undefined { - const typeChecker = program.getTypeChecker(); - const compilerOptions = program.getCompilerOptions(); - const { name, source, data } = entryId; - - const contextToken = ts.findPrecedingToken(position, sourceFile); - if (ts.isInString(sourceFile, position, contextToken)) { - return ts.Completions.StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken, preferences); - } - - // 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 ts.JsDoc.getJSDocTagNameCompletionDetails(name); - case CompletionDataKind.JsDocTag: - return ts.JsDoc.getJSDocTagCompletionDetails(name); - case CompletionDataKind.JsDocParameterName: - return ts.JsDoc.getJSDocParameterNameCompletionDetails(name); - case CompletionDataKind.Keywords: - return ts.some(request.keywordCompletions, c => c.name === name) ? createSimpleDetails(name, ts.ScriptElementKind.keyword, ts.SymbolDisplayPartKind.keyword) : undefined; - default: - return ts.Debug.assertNever(request); - } - } - case "symbol": { - const { symbol, location, contextToken, origin, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source); - return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 - } - case "literal": { - const { literal } = symbolCompletion; - return createSimpleDetails(completionNameForLiteral(sourceFile, preferences, literal), ts.ScriptElementKind.string, typeof literal === "string" ? ts.SymbolDisplayPartKind.stringLiteral : ts.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, ts.ScriptElementKind.keyword, ts.SymbolDisplayPartKind.keyword) : undefined; - default: - ts.Debug.assertNever(symbolCompletion); +export function createCompletionDetails(name: string, kindModifiers: string, kind: ts.ScriptElementKind, displayParts: ts.SymbolDisplayPart[], documentation?: ts.SymbolDisplayPart[], tags?: ts.JSDocTagInfo[], codeActions?: ts.CodeAction[], source?: ts.SymbolDisplayPart[]): ts.CompletionEntryDetails { + return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source, sourceDisplay: source }; +} + +interface CodeActionsAndSourceDisplay { + readonly codeActions: ts.CodeAction[] | undefined; + readonly sourceDisplay: ts.SymbolDisplayPart[] | undefined; +} +function getCompletionEntryCodeActionsAndSourceDisplay(name: string, location: ts.Node, contextToken: ts.Node | undefined, origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined, symbol: ts.Symbol, program: ts.Program, host: ts.LanguageServiceHost, compilerOptions: ts.CompilerOptions, sourceFile: ts.SourceFile, position: number, previousToken: ts.Node | undefined, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, data: ts.CompletionEntryData | undefined, source: string | undefined): CodeActionsAndSourceDisplay { + if (data?.moduleSpecifier) { + if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) { + // Import statement completion: 'import c|' + return { codeActions: undefined, sourceDisplay: [ts.textPart(data.moduleSpecifier)] }; + } + } + + if (source === CompletionSource.ClassMemberSnippet) { + const { importAdder } = getEntryForMemberCompletion(host, program, compilerOptions, preferences, name, symbol, location, contextToken, formatContext); + if (importAdder) { + const changes = ts.textChanges.ChangeTracker.with({ host, formatContext, preferences }, importAdder.writeFixes); + return { + sourceDisplay: undefined, + codeActions: [{ + changes, + description: ts.diagnosticToString([ts.Diagnostics.Includes_imports_of_types_referenced_by_0, name]), + }], + }; } } - function createSimpleDetails(name: string, kind: ts.ScriptElementKind, kind2: ts.SymbolDisplayPartKind): ts.CompletionEntryDetails { - return createCompletionDetails(name, ts.ScriptElementKindModifier.none, kind, [ts.displayPart(name, kind2)]); + if (originIsTypeOnlyAlias(origin)) { + const codeAction = ts.codefix.getPromoteTypeOnlyCompletionAction(sourceFile, origin.declaration.name, program, host, formatContext, preferences); + ts.Debug.assertIsDefined(codeAction, "Expected to have a code action for promoting type-only alias"); + return { codeActions: [codeAction], sourceDisplay: undefined }; } - export function createCompletionDetailsForSymbol(symbol: ts.Symbol, checker: ts.TypeChecker, sourceFile: ts.SourceFile, location: ts.Node, cancellationToken: ts.CancellationToken, codeActions?: ts.CodeAction[], sourceDisplay?: ts.SymbolDisplayPart[]): ts.CompletionEntryDetails { - const { displayParts, documentation, symbolKind, tags } = checker.runWithCancellationToken(cancellationToken, checker => ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, ts.SemanticMeaning.All)); - return createCompletionDetails(symbol.name, ts.SymbolDisplay.getSymbolModifiers(checker, symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay); + if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) { + return { codeActions: undefined, sourceDisplay: undefined }; } - export function createCompletionDetails(name: string, kindModifiers: string, kind: ts.ScriptElementKind, displayParts: ts.SymbolDisplayPart[], documentation?: ts.SymbolDisplayPart[], tags?: ts.JSDocTagInfo[], codeActions?: ts.CodeAction[], source?: ts.SymbolDisplayPart[]): ts.CompletionEntryDetails { - return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source, sourceDisplay: source }; - } + const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker(); + const { moduleSymbol } = origin; + const targetSymbol = checker.getMergedSymbol(ts.skipAlias(symbol.exportSymbol || symbol, checker)); + const isJsxOpeningTagName = contextToken?.kind === ts.SyntaxKind.LessThanToken && ts.isJsxOpeningLikeElement(contextToken.parent); + const { moduleSpecifier, codeAction } = ts.codefix.getImportCompletionAction(targetSymbol, moduleSymbol, sourceFile, ts.getNameForExportedSymbol(symbol, ts.getEmitScriptTarget(compilerOptions), isJsxOpeningTagName), isJsxOpeningTagName, host, program, formatContext, previousToken && ts.isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position, preferences); + ts.Debug.assert(!data?.moduleSpecifier || moduleSpecifier === data.moduleSpecifier); + return { sourceDisplay: [ts.textPart(moduleSpecifier)], codeActions: [codeAction] }; +} +export function getCompletionEntrySymbol(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): ts.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, + Keywords +} +/** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */ +type IsJsxInitializer = boolean | ts.Identifier; +interface CompletionData { + readonly kind: CompletionDataKind.Data; + readonly symbols: readonly ts.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: ts.PropertyAccessExpression | undefined; + readonly isNewIdentifierLocation: boolean; + readonly location: ts.Node; + readonly keywordFilters: KeywordCompletionFilters; + readonly literals: readonly (string | number | ts.PseudoBigInt)[]; + readonly symbolToOriginInfoMap: SymbolOriginInfoMap; + readonly recommendedCompletion: ts.Symbol | undefined; + readonly previousToken: ts.Node | undefined; + readonly contextToken: ts.Node | undefined; + readonly isJsxInitializer: IsJsxInitializer; + readonly insideJsDocTagTypeExpression: boolean; + readonly symbolToSortTextMap: SymbolSortTextMap; + readonly isTypeOnlyLocation: boolean; + /** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */ + readonly isJsxIdentifierExpected: boolean; + readonly isRightOfOpenTag: boolean; + readonly importCompletionNode?: ts.Node; + readonly hasUnresolvedAutoImports?: boolean; + readonly flags: ts.CompletionInfoFlags; +} +type Request = { + readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag; +} | { + readonly kind: CompletionDataKind.JsDocParameterName; + tag: ts.JSDocParameterTag; +} | { + readonly kind: CompletionDataKind.Keywords; + keywordCompletions: readonly ts.CompletionEntry[]; + isNewIdentifierLocation: boolean; +}; + +export const enum CompletionKind { + ObjectPropertyDeclaration, + Global, + PropertyAccess, + MemberLike, + String, + None +} + +function getRecommendedCompletion(previousToken: ts.Node, contextualType: ts.Type, checker: ts.TypeChecker): ts.Symbol | undefined { + // For a union, return the first one with a recommended completion. + return ts.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 & (ts.SymbolFlags.EnumMember | ts.SymbolFlags.Enum | ts.SymbolFlags.Class) && !ts.isAbstractConstructorSymbol(symbol)) + ? getFirstSymbolInChain(symbol, previousToken, checker) + : undefined; + }); +} - interface CodeActionsAndSourceDisplay { - readonly codeActions: ts.CodeAction[] | undefined; - readonly sourceDisplay: ts.SymbolDisplayPart[] | undefined; +function getContextualType(previousToken: ts.Node, position: number, sourceFile: ts.SourceFile, checker: ts.TypeChecker): ts.Type | undefined { + const { parent } = previousToken; + switch (previousToken.kind) { + case ts.SyntaxKind.Identifier: + return ts.getContextualTypeFromParent(previousToken as ts.Identifier, checker); + case ts.SyntaxKind.EqualsToken: + switch (parent.kind) { + case ts.SyntaxKind.VariableDeclaration: + return checker.getContextualType((parent as ts.VariableDeclaration).initializer!); // TODO: GH#18217 + case ts.SyntaxKind.BinaryExpression: + return checker.getTypeAtLocation((parent as ts.BinaryExpression).left); + case ts.SyntaxKind.JsxAttribute: + return checker.getContextualTypeForJsxAttribute(parent as ts.JsxAttribute); + default: + return undefined; + } + case ts.SyntaxKind.NewKeyword: + return checker.getContextualType(parent as ts.Expression); + case ts.SyntaxKind.CaseKeyword: + const caseClause = ts.tryCast(parent, ts.isCaseClause); + return caseClause ? ts.getSwitchedType(caseClause, checker) : undefined; + case ts.SyntaxKind.OpenBraceToken: + return ts.isJsxExpression(parent) && !ts.isJsxElement(parent.parent) && !ts.isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; + default: + const argInfo = ts.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 === ts.SyntaxKind.CommaToken ? 1 : 0)) : + ts.isEqualityOperatorKind(previousToken.kind) && ts.isBinaryExpression(parent) && ts.isEqualityOperatorKind(parent.operatorToken.kind) ? + // completion at `x ===/**/` should be for the right side + checker.getTypeAtLocation(parent.left) : + checker.getContextualType(previousToken as ts.Expression); } - function getCompletionEntryCodeActionsAndSourceDisplay(name: string, location: ts.Node, contextToken: ts.Node | undefined, origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined, symbol: ts.Symbol, program: ts.Program, host: ts.LanguageServiceHost, compilerOptions: ts.CompilerOptions, sourceFile: ts.SourceFile, position: number, previousToken: ts.Node | undefined, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, data: ts.CompletionEntryData | undefined, source: string | undefined): CodeActionsAndSourceDisplay { - if (data?.moduleSpecifier) { - if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) { - // Import statement completion: 'import c|' - return { codeActions: undefined, sourceDisplay: [ts.textPart(data.moduleSpecifier)] }; +} + +function getFirstSymbolInChain(symbol: ts.Symbol, enclosingDeclaration: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { + const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ ts.SymbolFlags.All, /*useOnlyExternalAliasing*/ false); + if (chain) + return ts.first(chain); + return symbol.parent && (isModuleSymbol(symbol.parent) ? symbol : getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker)); +} + +function isModuleSymbol(symbol: ts.Symbol): boolean { + return !!symbol.declarations?.some(d => d.kind === ts.SyntaxKind.SourceFile); +} +function getCompletionData(program: ts.Program, log: (message: string) => void, sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions, position: number, preferences: ts.UserPreferences, detailsEntryId: CompletionEntryIdentifier | undefined, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext | undefined, cancellationToken?: ts.CancellationToken): CompletionData | Request | undefined { + const typeChecker = program.getTypeChecker(); + const inUncheckedFile = isUncheckedFile(sourceFile, compilerOptions); + let start = ts.timestamp(); + let currentToken = ts.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: " + (ts.timestamp() - start)); + start = ts.timestamp(); + const insideComment = ts.isInComment(sourceFile, position, currentToken); + log("getCompletionData: Is inside comment: " + (ts.timestamp() - start)); + + let insideJsDocTagTypeExpression = false; + let isInSnippetScope = false; + if (insideComment) { + if (ts.hasDocComment(sourceFile, position)) { + if (sourceFile.text.charCodeAt(position - 1) === ts.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 = ts.getLineStartPositionForPosition(position, sourceFile); + if (!/[^\*|\s(/)]/.test(sourceFile.text.substring(lineStart, position))) { + 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 }; + } + const typeExpression = tryGetTypeExpressionFromTag(tag); + if (typeExpression) { + currentToken = ts.getTokenAtPosition(sourceFile, position); + if (!currentToken || + (!ts.isDeclarationName(currentToken) && + (currentToken.parent.kind !== ts.SyntaxKind.JSDocPropertyTag || + (currentToken.parent as ts.JSDocPropertyTag).name !== currentToken))) { + // Use as type location if inside tag's type expression + insideJsDocTagTypeExpression = isCurrentlyEditingNode(typeExpression); + } + } + if (!insideJsDocTagTypeExpression && ts.isJSDocParameterTag(tag) && (ts.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; } + } - if (source === CompletionSource.ClassMemberSnippet) { - const { importAdder } = getEntryForMemberCompletion(host, program, compilerOptions, preferences, name, symbol, location, contextToken, formatContext); - if (importAdder) { - const changes = ts.textChanges.ChangeTracker.with({ host, formatContext, preferences }, importAdder.writeFixes); + start = ts.timestamp(); + // 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 + const isJsOnlyLocation = !insideJsDocTagTypeExpression && ts.isSourceFileJS(sourceFile); + const tokens = getRelevantTokens(position, sourceFile); + const previousToken = tokens.previousToken!; + let contextToken = tokens.contextToken!; + log("getCompletionData: Get previous token: " + (ts.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: ts.PropertyAccessExpression | undefined; + let isRightOfDot = false; + let isRightOfQuestionDot = false; + let isRightOfOpenTag = false; + let isStartingCloseTag = false; + let isJsxInitializer: IsJsxInitializer = false; + let isJsxIdentifierExpected = false; + let importCompletionNode: ts.Node | undefined; + let location = ts.getTouchingPropertyName(sourceFile, position); + let keywordFilters = KeywordCompletionFilters.None; + let isNewIdentifierLocation = false; + let flags = ts.CompletionInfoFlags.None; + + if (contextToken) { + const importStatementCompletion = getImportStatementCompletionInfo(contextToken); + isNewIdentifierLocation = importStatementCompletion.isNewIdentifierLocation; + if (importStatementCompletion.keywordCompletion) { + if (importStatementCompletion.isKeywordOnlyCompletion) { return { - sourceDisplay: undefined, - codeActions: [{ - changes, - description: ts.diagnosticToString([ts.Diagnostics.Includes_imports_of_types_referenced_by_0, name]), - }], + kind: CompletionDataKind.Keywords, + keywordCompletions: [keywordToCompletionEntry(importStatementCompletion.keywordCompletion)], + isNewIdentifierLocation, }; } - } - - if (originIsTypeOnlyAlias(origin)) { - const codeAction = ts.codefix.getPromoteTypeOnlyCompletionAction(sourceFile, origin.declaration.name, program, host, formatContext, preferences); - ts.Debug.assertIsDefined(codeAction, "Expected to have a code action for promoting type-only alias"); - return { codeActions: [codeAction], sourceDisplay: undefined }; - } - - if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) { - return { codeActions: undefined, sourceDisplay: undefined }; - } - - const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker(); - const { moduleSymbol } = origin; - const targetSymbol = checker.getMergedSymbol(ts.skipAlias(symbol.exportSymbol || symbol, checker)); - const isJsxOpeningTagName = contextToken?.kind === ts.SyntaxKind.LessThanToken && ts.isJsxOpeningLikeElement(contextToken.parent); - const { moduleSpecifier, codeAction } = ts.codefix.getImportCompletionAction(targetSymbol, moduleSymbol, sourceFile, ts.getNameForExportedSymbol(symbol, ts.getEmitScriptTarget(compilerOptions), isJsxOpeningTagName), isJsxOpeningTagName, host, program, formatContext, previousToken && ts.isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position, preferences); - ts.Debug.assert(!data?.moduleSpecifier || moduleSpecifier === data.moduleSpecifier); - return { sourceDisplay: [ts.textPart(moduleSpecifier)], codeActions: [codeAction] }; - } - export function getCompletionEntrySymbol(program: ts.Program, log: Log, sourceFile: ts.SourceFile, position: number, entryId: CompletionEntryIdentifier, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): ts.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, - Keywords - } - /** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */ - type IsJsxInitializer = boolean | ts.Identifier; - interface CompletionData { - readonly kind: CompletionDataKind.Data; - readonly symbols: readonly ts.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: ts.PropertyAccessExpression | undefined; - readonly isNewIdentifierLocation: boolean; - readonly location: ts.Node; - readonly keywordFilters: KeywordCompletionFilters; - readonly literals: readonly (string | number | ts.PseudoBigInt)[]; - readonly symbolToOriginInfoMap: SymbolOriginInfoMap; - readonly recommendedCompletion: ts.Symbol | undefined; - readonly previousToken: ts.Node | undefined; - readonly contextToken: ts.Node | undefined; - readonly isJsxInitializer: IsJsxInitializer; - readonly insideJsDocTagTypeExpression: boolean; - readonly symbolToSortTextMap: SymbolSortTextMap; - readonly isTypeOnlyLocation: boolean; - /** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */ - readonly isJsxIdentifierExpected: boolean; - readonly isRightOfOpenTag: boolean; - readonly importCompletionNode?: ts.Node; - readonly hasUnresolvedAutoImports?: boolean; - readonly flags: ts.CompletionInfoFlags; - } - type Request = { - readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag; - } | { - readonly kind: CompletionDataKind.JsDocParameterName; - tag: ts.JSDocParameterTag; - } | { - readonly kind: CompletionDataKind.Keywords; - keywordCompletions: readonly ts.CompletionEntry[]; - isNewIdentifierLocation: boolean; - }; - - export const enum CompletionKind { - ObjectPropertyDeclaration, - Global, - PropertyAccess, - MemberLike, - String, - None - } - - function getRecommendedCompletion(previousToken: ts.Node, contextualType: ts.Type, checker: ts.TypeChecker): ts.Symbol | undefined { - // For a union, return the first one with a recommended completion. - return ts.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 & (ts.SymbolFlags.EnumMember | ts.SymbolFlags.Enum | ts.SymbolFlags.Class) && !ts.isAbstractConstructorSymbol(symbol)) - ? getFirstSymbolInChain(symbol, previousToken, checker) + keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletion.keywordCompletion); + } + if (importStatementCompletion.replacementNode && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` + // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature + // is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients + // to opt in with the `includeCompletionsForImportStatements` user preference. + importCompletionNode = importStatementCompletion.replacementNode; + flags |= ts.CompletionInfoFlags.IsImportStatementCompletion; + } + // Bail out if this is a known invalid completion location + if (!importCompletionNode && isCompletionListBlocker(contextToken)) { + log("Returning an empty list because completion was requested in an invalid position."); + return keywordFilters + ? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierDefinitionLocation()) : undefined; - }); - } + } - function getContextualType(previousToken: ts.Node, position: number, sourceFile: ts.SourceFile, checker: ts.TypeChecker): ts.Type | undefined { - const { parent } = previousToken; - switch (previousToken.kind) { - case ts.SyntaxKind.Identifier: - return ts.getContextualTypeFromParent(previousToken as ts.Identifier, checker); - case ts.SyntaxKind.EqualsToken: - switch (parent.kind) { - case ts.SyntaxKind.VariableDeclaration: - return checker.getContextualType((parent as ts.VariableDeclaration).initializer!); // TODO: GH#18217 - case ts.SyntaxKind.BinaryExpression: - return checker.getTypeAtLocation((parent as ts.BinaryExpression).left); - case ts.SyntaxKind.JsxAttribute: - return checker.getContextualTypeForJsxAttribute(parent as ts.JsxAttribute); - default: + let parent = contextToken.parent; + if (contextToken.kind === ts.SyntaxKind.DotToken || contextToken.kind === ts.SyntaxKind.QuestionDotToken) { + isRightOfDot = contextToken.kind === ts.SyntaxKind.DotToken; + isRightOfQuestionDot = contextToken.kind === ts.SyntaxKind.QuestionDotToken; + switch (parent.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + propertyAccessToConvert = parent as ts.PropertyAccessExpression; + node = propertyAccessToConvert.expression; + const leftmostAccessExpression = ts.getLeftmostAccessExpression(propertyAccessToConvert); + if (ts.nodeIsMissing(leftmostAccessExpression) || + ((ts.isCallExpression(node) || ts.isFunctionLike(node)) && + node.end === contextToken.pos && + node.getChildCount(sourceFile) && + ts.last(node.getChildren(sourceFile)).kind !== ts.SyntaxKind.CloseParenToken)) { + // This is likely dot from incorrectly parsed expression and user is starting to write spread + // eg: Math.min(./**/) + // const x = function (./**/) {} + // ({./**/}) return undefined; - } - case ts.SyntaxKind.NewKeyword: - return checker.getContextualType(parent as ts.Expression); - case ts.SyntaxKind.CaseKeyword: - const caseClause = ts.tryCast(parent, ts.isCaseClause); - return caseClause ? ts.getSwitchedType(caseClause, checker) : undefined; - case ts.SyntaxKind.OpenBraceToken: - return ts.isJsxExpression(parent) && !ts.isJsxElement(parent.parent) && !ts.isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; - default: - const argInfo = ts.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 === ts.SyntaxKind.CommaToken ? 1 : 0)) : - ts.isEqualityOperatorKind(previousToken.kind) && ts.isBinaryExpression(parent) && ts.isEqualityOperatorKind(parent.operatorToken.kind) ? - // completion at `x ===/**/` should be for the right side - checker.getTypeAtLocation(parent.left) : - checker.getContextualType(previousToken as ts.Expression); - } - } - - function getFirstSymbolInChain(symbol: ts.Symbol, enclosingDeclaration: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { - const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ ts.SymbolFlags.All, /*useOnlyExternalAliasing*/ false); - if (chain) - return ts.first(chain); - return symbol.parent && (isModuleSymbol(symbol.parent) ? symbol : getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker)); - } - - function isModuleSymbol(symbol: ts.Symbol): boolean { - return !!symbol.declarations?.some(d => d.kind === ts.SyntaxKind.SourceFile); - } - function getCompletionData(program: ts.Program, log: (message: string) => void, sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions, position: number, preferences: ts.UserPreferences, detailsEntryId: CompletionEntryIdentifier | undefined, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext | undefined, cancellationToken?: ts.CancellationToken): CompletionData | Request | undefined { - const typeChecker = program.getTypeChecker(); - const inUncheckedFile = isUncheckedFile(sourceFile, compilerOptions); - let start = ts.timestamp(); - let currentToken = ts.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: " + (ts.timestamp() - start)); - start = ts.timestamp(); - const insideComment = ts.isInComment(sourceFile, position, currentToken); - log("getCompletionData: Is inside comment: " + (ts.timestamp() - start)); - - let insideJsDocTagTypeExpression = false; - let isInSnippetScope = false; - if (insideComment) { - if (ts.hasDocComment(sourceFile, position)) { - if (sourceFile.text.charCodeAt(position - 1) === ts.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 = ts.getLineStartPositionForPosition(position, sourceFile); - if (!/[^\*|\s(/)]/.test(sourceFile.text.substring(lineStart, position))) { - 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 }; - } - const typeExpression = tryGetTypeExpressionFromTag(tag); - if (typeExpression) { - currentToken = ts.getTokenAtPosition(sourceFile, position); - if (!currentToken || - (!ts.isDeclarationName(currentToken) && - (currentToken.parent.kind !== ts.SyntaxKind.JSDocPropertyTag || - (currentToken.parent as ts.JSDocPropertyTag).name !== currentToken))) { - // Use as type location if inside tag's type expression - insideJsDocTagTypeExpression = isCurrentlyEditingNode(typeExpression); } - } - if (!insideJsDocTagTypeExpression && ts.isJSDocParameterTag(tag) && (ts.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 ts.SyntaxKind.QualifiedName: + node = (parent as ts.QualifiedName).left; + break; + case ts.SyntaxKind.ModuleDeclaration: + node = (parent as ts.ModuleDeclaration).name; + break; + case ts.SyntaxKind.ImportType: + node = parent; + break; + case ts.SyntaxKind.MetaProperty: + node = parent.getFirstToken(sourceFile)!; + ts.Debug.assert(node.kind === ts.SyntaxKind.ImportKeyword || node.kind === ts.SyntaxKind.NewKeyword); + 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 (!importCompletionNode && sourceFile.languageVariant === ts.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 === ts.SyntaxKind.PropertyAccessExpression) { + contextToken = parent; + parent = parent.parent; + } - start = ts.timestamp(); - // 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 - const isJsOnlyLocation = !insideJsDocTagTypeExpression && ts.isSourceFileJS(sourceFile); - const tokens = getRelevantTokens(position, sourceFile); - const previousToken = tokens.previousToken!; - let contextToken = tokens.contextToken!; - log("getCompletionData: Get previous token: " + (ts.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: ts.PropertyAccessExpression | undefined; - let isRightOfDot = false; - let isRightOfQuestionDot = false; - let isRightOfOpenTag = false; - let isStartingCloseTag = false; - let isJsxInitializer: IsJsxInitializer = false; - let isJsxIdentifierExpected = false; - let importCompletionNode: ts.Node | undefined; - let location = ts.getTouchingPropertyName(sourceFile, position); - let keywordFilters = KeywordCompletionFilters.None; - let isNewIdentifierLocation = false; - let flags = ts.CompletionInfoFlags.None; - - if (contextToken) { - const importStatementCompletion = getImportStatementCompletionInfo(contextToken); - isNewIdentifierLocation = importStatementCompletion.isNewIdentifierLocation; - if (importStatementCompletion.keywordCompletion) { - if (importStatementCompletion.isKeywordOnlyCompletion) { - return { - kind: CompletionDataKind.Keywords, - keywordCompletions: [keywordToCompletionEntry(importStatementCompletion.keywordCompletion)], - isNewIdentifierLocation, - }; - } - keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletion.keywordCompletion); - } - if (importStatementCompletion.replacementNode && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { - // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` - // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature - // is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients - // to opt in with the `includeCompletionsForImportStatements` user preference. - importCompletionNode = importStatementCompletion.replacementNode; - flags |= ts.CompletionInfoFlags.IsImportStatementCompletion; - } - // Bail out if this is a known invalid completion location - if (!importCompletionNode && isCompletionListBlocker(contextToken)) { - log("Returning an empty list because completion was requested in an invalid position."); - return keywordFilters - ? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierDefinitionLocation()) - : undefined; - } - - let parent = contextToken.parent; - if (contextToken.kind === ts.SyntaxKind.DotToken || contextToken.kind === ts.SyntaxKind.QuestionDotToken) { - isRightOfDot = contextToken.kind === ts.SyntaxKind.DotToken; - isRightOfQuestionDot = contextToken.kind === ts.SyntaxKind.QuestionDotToken; - switch (parent.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - propertyAccessToConvert = parent as ts.PropertyAccessExpression; - node = propertyAccessToConvert.expression; - const leftmostAccessExpression = ts.getLeftmostAccessExpression(propertyAccessToConvert); - if (ts.nodeIsMissing(leftmostAccessExpression) || - ((ts.isCallExpression(node) || ts.isFunctionLike(node)) && - node.end === contextToken.pos && - node.getChildCount(sourceFile) && - ts.last(node.getChildren(sourceFile)).kind !== ts.SyntaxKind.CloseParenToken)) { - // This is likely dot from incorrectly parsed expression and user is starting to write spread - // eg: Math.min(./**/) - // const x = function (./**/) {} - // ({./**/}) - return undefined; + // Fix location + if (currentToken.parent === location) { + switch (currentToken.kind) { + case ts.SyntaxKind.GreaterThanToken: + if (currentToken.parent.kind === ts.SyntaxKind.JsxElement || currentToken.parent.kind === ts.SyntaxKind.JsxOpeningElement) { + location = currentToken; } break; - case ts.SyntaxKind.QualifiedName: - node = (parent as ts.QualifiedName).left; - break; - case ts.SyntaxKind.ModuleDeclaration: - node = (parent as ts.ModuleDeclaration).name; - break; - case ts.SyntaxKind.ImportType: - node = parent; - break; - case ts.SyntaxKind.MetaProperty: - node = parent.getFirstToken(sourceFile)!; - ts.Debug.assert(node.kind === ts.SyntaxKind.ImportKeyword || node.kind === ts.SyntaxKind.NewKeyword); + + case ts.SyntaxKind.SlashToken: + if (currentToken.parent.kind === ts.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 (!importCompletionNode && sourceFile.languageVariant === ts.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 === ts.SyntaxKind.PropertyAccessExpression) { - contextToken = parent; - parent = parent.parent; - } - - // Fix location - if (currentToken.parent === location) { - switch (currentToken.kind) { - case ts.SyntaxKind.GreaterThanToken: - if (currentToken.parent.kind === ts.SyntaxKind.JsxElement || currentToken.parent.kind === ts.SyntaxKind.JsxOpeningElement) { - location = currentToken; - } - break; - case ts.SyntaxKind.SlashToken: - if (currentToken.parent.kind === ts.SyntaxKind.JsxSelfClosingElement) { - location = currentToken; - } - break; + switch (parent.kind) { + case ts.SyntaxKind.JsxClosingElement: + if (contextToken.kind === ts.SyntaxKind.SlashToken) { + isStartingCloseTag = true; + location = contextToken; } - } + break; - switch (parent.kind) { - case ts.SyntaxKind.JsxClosingElement: - if (contextToken.kind === ts.SyntaxKind.SlashToken) { - isStartingCloseTag = true; - location = contextToken; - } + case ts.SyntaxKind.BinaryExpression: + if (!binaryExpressionMayBeOpenTag(parent as ts.BinaryExpression)) { break; + } + // falls through + + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxElement: + case ts.SyntaxKind.JsxOpeningElement: + isJsxIdentifierExpected = true; + if (contextToken.kind === ts.SyntaxKind.LessThanToken) { + isRightOfOpenTag = true; + location = contextToken; + } + break; - case ts.SyntaxKind.BinaryExpression: - if (!binaryExpressionMayBeOpenTag(parent as ts.BinaryExpression)) { - break; - } - // falls through + case ts.SyntaxKind.JsxExpression: + case ts.SyntaxKind.JsxSpreadAttribute: + // For `
`, `parent` will be `{true}` and `previousToken` will be `}` + if (previousToken.kind === ts.SyntaxKind.CloseBraceToken && currentToken.kind === ts.SyntaxKind.GreaterThanToken) { + isJsxIdentifierExpected = true; + } + break; - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxElement: - case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxAttribute: + // For `
`, `parent` will be JsxAttribute and `previousToken` will be its initializer + if ((parent as ts.JsxAttribute).initializer === previousToken && + previousToken.end < position) { isJsxIdentifierExpected = true; - if (contextToken.kind === ts.SyntaxKind.LessThanToken) { - isRightOfOpenTag = true; - location = contextToken; - } break; - - case ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.JsxSpreadAttribute: - // For `
`, `parent` will be `{true}` and `previousToken` will be `}` - if (previousToken.kind === ts.SyntaxKind.CloseBraceToken && currentToken.kind === ts.SyntaxKind.GreaterThanToken) { + } + switch (previousToken.kind) { + case ts.SyntaxKind.EqualsToken: + isJsxInitializer = true; + break; + case ts.SyntaxKind.Identifier: isJsxIdentifierExpected = true; - } - break; + // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. + if (parent !== previousToken.parent && + !(parent as ts.JsxAttribute).initializer && + ts.findChildOfKind(parent, ts.SyntaxKind.EqualsToken, sourceFile)) { + isJsxInitializer = previousToken as ts.Identifier; + } + } + break; + } + } + } - case ts.SyntaxKind.JsxAttribute: - // For `
`, `parent` will be JsxAttribute and `previousToken` will be its initializer - if ((parent as ts.JsxAttribute).initializer === previousToken && - previousToken.end < position) { - isJsxIdentifierExpected = true; - break; - } - switch (previousToken.kind) { - case ts.SyntaxKind.EqualsToken: - isJsxInitializer = true; - break; - case ts.SyntaxKind.Identifier: - isJsxIdentifierExpected = true; - // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. - if (parent !== previousToken.parent && - !(parent as ts.JsxAttribute).initializer && - ts.findChildOfKind(parent, ts.SyntaxKind.EqualsToken, sourceFile)) { - isJsxInitializer = previousToken as ts.Identifier; - } - } - break; - } - } - } - - const semanticStart = ts.timestamp(); - let completionKind = CompletionKind.None; - let isNonContextualObjectLiteral = false; - let hasUnresolvedAutoImports = false; - // This also gets mutated in nested-functions after the return - let symbols: ts.Symbol[] = []; - const symbolToOriginInfoMap: SymbolOriginInfoMap = []; - const symbolToSortTextMap: SymbolSortTextMap = []; - const seenPropertySymbols = new ts.Map(); - const isTypeOnlyLocation = isTypeOnlyCompletion(); - const getModuleSpecifierResolutionHost = ts.memoizeOne((isFromPackageJson: boolean) => { - return ts.createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); - }); + const semanticStart = ts.timestamp(); + let completionKind = CompletionKind.None; + let isNonContextualObjectLiteral = false; + let hasUnresolvedAutoImports = false; + // This also gets mutated in nested-functions after the return + let symbols: ts.Symbol[] = []; + const symbolToOriginInfoMap: SymbolOriginInfoMap = []; + const symbolToSortTextMap: SymbolSortTextMap = []; + const seenPropertySymbols = new ts.Map(); + const isTypeOnlyLocation = isTypeOnlyCompletion(); + const getModuleSpecifierResolutionHost = ts.memoizeOne((isFromPackageJson: boolean) => { + return ts.createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); + }); - if (isRightOfDot || isRightOfQuestionDot) { - getTypeScriptMemberSymbols(); - } - else if (isRightOfOpenTag) { - symbols = typeChecker.getJsxIntrinsicTagNamesAt(location); - ts.Debug.assertEachIsDefined(symbols, "getJsxIntrinsicTagNames() should all be defined"); - tryGetGlobalSymbols(); - completionKind = CompletionKind.Global; - keywordFilters = KeywordCompletionFilters.None; - } - else if (isStartingCloseTag) { - const tagName = (contextToken.parent.parent as ts.JsxElement).openingElement.tagName; - const tagSymbol = typeChecker.getSymbolAtLocation(tagName); - if (tagSymbol) { - symbols = [tagSymbol]; - } - completionKind = CompletionKind.Global; - keywordFilters = KeywordCompletionFilters.None; + if (isRightOfDot || isRightOfQuestionDot) { + getTypeScriptMemberSymbols(); + } + else if (isRightOfOpenTag) { + symbols = typeChecker.getJsxIntrinsicTagNamesAt(location); + ts.Debug.assertEachIsDefined(symbols, "getJsxIntrinsicTagNames() should all be defined"); + tryGetGlobalSymbols(); + completionKind = CompletionKind.Global; + keywordFilters = KeywordCompletionFilters.None; + } + else if (isStartingCloseTag) { + const tagName = (contextToken.parent.parent as ts.JsxElement).openingElement.tagName; + const tagSymbol = typeChecker.getSymbolAtLocation(tagName); + if (tagSymbol) { + symbols = [tagSymbol]; + } + completionKind = CompletionKind.Global; + 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 keywordFilters + ? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierLocation) + : undefined; } - 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 keywordFilters - ? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierLocation) - : undefined; - } - } - - log("getCompletionData: Semantic work: " + (ts.timestamp() - semanticStart)); - const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); - - const literals = ts.mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() && !(t.flags & ts.TypeFlags.EnumLiteral) ? 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, - contextToken, - isJsxInitializer, - insideJsDocTagTypeExpression, - symbolToSortTextMap, - isTypeOnlyLocation, - isJsxIdentifierExpected, - isRightOfOpenTag, - importCompletionNode, - hasUnresolvedAutoImports, - flags, - }; + } - type JSDocTagWithTypeExpression = ts.JSDocParameterTag | ts.JSDocPropertyTag | ts.JSDocReturnTag | ts.JSDocTypeTag | ts.JSDocTypedefTag | ts.JSDocTemplateTag; - function isTagWithTypeExpression(tag: ts.JSDocTag): tag is JSDocTagWithTypeExpression { - switch (tag.kind) { - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocPropertyTag: - case ts.SyntaxKind.JSDocReturnTag: - case ts.SyntaxKind.JSDocTypeTag: - case ts.SyntaxKind.JSDocTypedefTag: - return true; - case ts.SyntaxKind.JSDocTemplateTag: - return !!(tag as ts.JSDocTemplateTag).constraint; - default: - return false; - } + log("getCompletionData: Semantic work: " + (ts.timestamp() - semanticStart)); + const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); + + const literals = ts.mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() && !(t.flags & ts.TypeFlags.EnumLiteral) ? 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, + contextToken, + isJsxInitializer, + insideJsDocTagTypeExpression, + symbolToSortTextMap, + isTypeOnlyLocation, + isJsxIdentifierExpected, + isRightOfOpenTag, + importCompletionNode, + hasUnresolvedAutoImports, + flags, + }; + + type JSDocTagWithTypeExpression = ts.JSDocParameterTag | ts.JSDocPropertyTag | ts.JSDocReturnTag | ts.JSDocTypeTag | ts.JSDocTypedefTag | ts.JSDocTemplateTag; + function isTagWithTypeExpression(tag: ts.JSDocTag): tag is JSDocTagWithTypeExpression { + switch (tag.kind) { + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocPropertyTag: + case ts.SyntaxKind.JSDocReturnTag: + case ts.SyntaxKind.JSDocTypeTag: + case ts.SyntaxKind.JSDocTypedefTag: + return true; + case ts.SyntaxKind.JSDocTemplateTag: + return !!(tag as ts.JSDocTemplateTag).constraint; + default: + return false; } + } - function tryGetTypeExpressionFromTag(tag: ts.JSDocTag): ts.JSDocTypeExpression | undefined { - if (isTagWithTypeExpression(tag)) { - const typeExpression = ts.isJSDocTemplateTag(tag) ? tag.constraint : tag.typeExpression; - return typeExpression && typeExpression.kind === ts.SyntaxKind.JSDocTypeExpression ? typeExpression : undefined; - } - return undefined; + function tryGetTypeExpressionFromTag(tag: ts.JSDocTag): ts.JSDocTypeExpression | undefined { + if (isTagWithTypeExpression(tag)) { + const typeExpression = ts.isJSDocTemplateTag(tag) ? tag.constraint : tag.typeExpression; + return typeExpression && typeExpression.kind === ts.SyntaxKind.JSDocTypeExpression ? typeExpression : undefined; } + return undefined; + } - 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 = ts.isLiteralImportTypeNode(node); - const isTypeLocation = insideJsDocTagTypeExpression - || (isImportType && !(node as ts.ImportTypeNode).isTypeOf) - || ts.isPartOfTypeNode(node.parent) - || ts.isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker); - const isRhsOfImportDeclaration = ts.isInRightSideOfInternalImportEqualsDeclaration(node); - if (ts.isEntityName(node) || isImportType || ts.isPropertyAccessExpression(node)) { - const isNamespaceName = ts.isModuleDeclaration(node.parent); - if (isNamespaceName) - isNewIdentifierLocation = true; - let symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - symbol = ts.skipAlias(symbol, typeChecker); - if (symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Enum)) { - // Extract module or enum members - const exportedSymbols = typeChecker.getExportsOfModule(symbol); - ts.Debug.assertEachIsDefined(exportedSymbols, "getExportsOfModule() should all be defined"); - const isValidValueAccess = (symbol: ts.Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node as ts.ImportTypeNode : (node.parent as ts.PropertyAccessExpression), symbol.name); - const isValidTypeAccess = (symbol: ts.Symbol) => symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); - const isValidAccess: (symbol: ts.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 & ts.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 = ts.isLiteralImportTypeNode(node); + const isTypeLocation = insideJsDocTagTypeExpression + || (isImportType && !(node as ts.ImportTypeNode).isTypeOf) + || ts.isPartOfTypeNode(node.parent) + || ts.isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker); + const isRhsOfImportDeclaration = ts.isInRightSideOfInternalImportEqualsDeclaration(node); + if (ts.isEntityName(node) || isImportType || ts.isPropertyAccessExpression(node)) { + const isNamespaceName = ts.isModuleDeclaration(node.parent); + if (isNamespaceName) + isNewIdentifierLocation = true; + let symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + symbol = ts.skipAlias(symbol, typeChecker); + if (symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Enum)) { + // Extract module or enum members + const exportedSymbols = typeChecker.getExportsOfModule(symbol); + ts.Debug.assertEachIsDefined(exportedSymbols, "getExportsOfModule() should all be defined"); + const isValidValueAccess = (symbol: ts.Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node as ts.ImportTypeNode : (node.parent as ts.PropertyAccessExpression), symbol.name); + const isValidTypeAccess = (symbol: ts.Symbol) => symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); + const isValidAccess: (symbol: ts.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 & ts.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 !== ts.SyntaxKind.SourceFile && d.kind !== ts.SyntaxKind.ModuleDeclaration && d.kind !== ts.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 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 !== ts.SyntaxKind.SourceFile && d.kind !== ts.SyntaxKind.ModuleDeclaration && d.kind !== ts.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; } } - addTypeProperties(type, !!(node.flags & ts.NodeFlags.AwaitContext), insertQuestionDot); } - - return; + addTypeProperties(type, !!(node.flags & ts.NodeFlags.AwaitContext), insertQuestionDot); } + + return; } } + } - if (!isTypeLocation) { - // GH#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity - // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because - // we will check (and cache) the type of `this` *before* checking the type of the node. - typeChecker.tryGetThisTypeAt(node, /*includeGlobalThis*/ false); + if (!isTypeLocation) { + // GH#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity + // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because + // we will check (and cache) the type of `this` *before* checking the type of the node. + typeChecker.tryGetThisTypeAt(node, /*includeGlobalThis*/ false); - let type = typeChecker.getTypeAtLocation(node).getNonOptionalType(); - let insertQuestionDot = false; - if (type.isNullableType()) { - const canCorrectToQuestionDot = isRightOfDot && - !isRightOfQuestionDot && - preferences.includeAutomaticOptionalChainCompletions !== false; + 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 (canCorrectToQuestionDot || isRightOfQuestionDot) { + type = type.getNonNullableType(); + if (canCorrectToQuestionDot) { + insertQuestionDot = true; } } - addTypeProperties(type, !!(node.flags & ts.NodeFlags.AwaitContext), insertQuestionDot); } + addTypeProperties(type, !!(node.flags & ts.NodeFlags.AwaitContext), insertQuestionDot); } + } - function addTypeProperties(type: ts.Type, insertAwait: boolean, insertQuestionDot: boolean): void { - isNewIdentifierLocation = !!type.getStringIndexType(); - if (isRightOfQuestionDot && ts.some(type.getCallSignatures())) { - isNewIdentifierLocation = true; - } + function addTypeProperties(type: ts.Type, insertAwait: boolean, insertQuestionDot: boolean): void { + isNewIdentifierLocation = !!type.getStringIndexType(); + if (isRightOfQuestionDot && ts.some(type.getCallSignatures())) { + isNewIdentifierLocation = true; + } - const propertyAccess = node.kind === ts.SyntaxKind.ImportType ? node as ts.ImportTypeNode : node.parent as ts.PropertyAccessExpression | ts.QualifiedName; - if (inUncheckedFile) { - // 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(...ts.filter(getPropertiesForCompletion(type, typeChecker), s => typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, s))); - } - else { - for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) { - addPropertySymbol(symbol, /* insertAwait */ false, insertQuestionDot); - } + const propertyAccess = node.kind === ts.SyntaxKind.ImportType ? node as ts.ImportTypeNode : node.parent as ts.PropertyAccessExpression | ts.QualifiedName; + if (inUncheckedFile) { + // 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(...ts.filter(getPropertiesForCompletion(type, typeChecker), s => typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, s))); + } + 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: ts.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 = ts.firstDefined(symbol.declarations, decl => ts.tryCast(ts.getNameOfDeclaration(decl), ts.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 && ts.addToSeen(seenPropertySymbols, ts.getSymbolId(firstAccessibleSymbol))) { - const index = symbols.length; - symbols.push(firstAccessibleSymbol); - const moduleSymbol = firstAccessibleSymbol.parent; - if (!moduleSymbol || - !ts.isExternalModuleSymbol(moduleSymbol) || - typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol) { - symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; - } - else { - const fileName = ts.isExternalModuleNameRelative(ts.stripQuotes(moduleSymbol.name)) ? ts.getSourceFileOfModule(moduleSymbol)?.fileName : undefined; - const { moduleSpecifier } = ts.codefix.getModuleSpecifierForBestExportInfo([{ - exportKind: ts.ExportKind.Named, - moduleFileName: fileName, - isFromPackageJson: false, + function addPropertySymbol(symbol: ts.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 = ts.firstDefined(symbol.declarations, decl => ts.tryCast(ts.getNameOfDeclaration(decl), ts.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 && ts.addToSeen(seenPropertySymbols, ts.getSymbolId(firstAccessibleSymbol))) { + const index = symbols.length; + symbols.push(firstAccessibleSymbol); + const moduleSymbol = firstAccessibleSymbol.parent; + if (!moduleSymbol || + !ts.isExternalModuleSymbol(moduleSymbol) || + typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol) { + symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; + } + else { + const fileName = ts.isExternalModuleNameRelative(ts.stripQuotes(moduleSymbol.name)) ? ts.getSourceFileOfModule(moduleSymbol)?.fileName : undefined; + const { moduleSpecifier } = ts.codefix.getModuleSpecifierForBestExportInfo([{ + exportKind: ts.ExportKind.Named, + moduleFileName: fileName, + isFromPackageJson: false, + moduleSymbol, + symbol: firstAccessibleSymbol, + targetFlags: ts.skipAlias(firstAccessibleSymbol, typeChecker).flags, + }], firstAccessibleSymbol.name, position, ts.isValidTypeOnlyAliasUseSite(location), sourceFile, program, host, preferences) || {}; + + if (moduleSpecifier) { + const origin: SymbolOriginInfoResolvedExport = { + kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), moduleSymbol, - symbol: firstAccessibleSymbol, - targetFlags: ts.skipAlias(firstAccessibleSymbol, typeChecker).flags, - }], firstAccessibleSymbol.name, position, ts.isValidTypeOnlyAliasUseSite(location), sourceFile, program, host, preferences) || {}; - - if (moduleSpecifier) { - const origin: SymbolOriginInfoResolvedExport = { - kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), - moduleSymbol, - isDefaultExport: false, - symbolName: firstAccessibleSymbol.name, - exportName: firstAccessibleSymbol.name, - fileName, - moduleSpecifier, - }; - symbolToOriginInfoMap[index] = origin; - } + isDefaultExport: false, + symbolName: firstAccessibleSymbol.name, + exportName: firstAccessibleSymbol.name, + fileName, + moduleSpecifier, + }; + symbolToOriginInfoMap[index] = origin; } } - else if (preferences.includeCompletionsWithInsertText) { - addSymbolOriginInfo(symbol); - addSymbolSortInfo(symbol); - symbols.push(symbol); - } } - else { + else if (preferences.includeCompletionsWithInsertText) { addSymbolOriginInfo(symbol); addSymbolSortInfo(symbol); symbols.push(symbol); } + } + else { + addSymbolOriginInfo(symbol); + addSymbolSortInfo(symbol); + symbols.push(symbol); + } - function addSymbolSortInfo(symbol: ts.Symbol) { - if (isStaticProperty(symbol)) { - symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.LocalDeclarationPriority; - } + function addSymbolSortInfo(symbol: ts.Symbol) { + if (isStaticProperty(symbol)) { + symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.LocalDeclarationPriority; } + } - function addSymbolOriginInfo(symbol: ts.Symbol) { - if (preferences.includeCompletionsWithInsertText) { - if (insertAwait && ts.addToSeen(seenPropertySymbols, ts.getSymbolId(symbol))) { - symbolToOriginInfoMap[symbols.length] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; - } - else if (insertQuestionDot) { - symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.Nullable }; - } + function addSymbolOriginInfo(symbol: ts.Symbol) { + if (preferences.includeCompletionsWithInsertText) { + if (insertAwait && ts.addToSeen(seenPropertySymbols, ts.getSymbolId(symbol))) { + symbolToOriginInfoMap[symbols.length] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; + } + else if (insertQuestionDot) { + symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.Nullable }; } - } - - function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) { - return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind; } } - /** Given 'a.b.c', returns 'a'. */ - function getLeftMostName(e: ts.Expression): ts.Identifier | undefined { - return ts.isIdentifier(e) ? e : ts.isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined; + function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) { + return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind; } + } - function tryGetGlobalSymbols(): boolean { - const result: GlobalsSearch = tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols() - || tryGetObjectLikeCompletionSymbols() - || tryGetImportCompletionSymbols() - || tryGetImportOrExportClauseCompletionSymbols() - || tryGetLocalNamedExportCompletionSymbols() - || tryGetConstructorCompletion() - || tryGetClassLikeCompletionSymbols() - || tryGetJsxCompletionSymbols() - || (getGlobalCompletions(), GlobalsSearch.Success); - return result === GlobalsSearch.Success; - } + /** Given 'a.b.c', returns 'a'. */ + function getLeftMostName(e: ts.Expression): ts.Identifier | undefined { + return ts.isIdentifier(e) ? e : ts.isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined; + } - 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 tryGetGlobalSymbols(): boolean { + const result: GlobalsSearch = tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols() + || tryGetObjectLikeCompletionSymbols() + || tryGetImportCompletionSymbols() + || tryGetImportOrExportClauseCompletionSymbols() + || tryGetLocalNamedExportCompletionSymbols() + || tryGetConstructorCompletion() + || tryGetClassLikeCompletionSymbols() + || tryGetJsxCompletionSymbols() + || (getGlobalCompletions(), GlobalsSearch.Success); + return result === 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, ts.ContextFlags.Completions); - symbols = ts.concatenate(symbols, filterJsxAttributes(getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer.attributes, typeChecker), jsxContainer.attributes.properties)); - setSortTextToOptionalMember(); - completionKind = CompletionKind.MemberLike; - isNewIdentifierLocation = false; - return 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 tryGetImportCompletionSymbols(): GlobalsSearch { - if (!importCompletionNode) - return GlobalsSearch.Continue; - isNewIdentifierLocation = true; - collectAutoImports(); - 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, ts.ContextFlags.Completions); + symbols = ts.concatenate(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(); - - if (previousToken !== contextToken) { - ts.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 = (isTypeOnlyLocation ? ts.SymbolFlags.None : ts.SymbolFlags.Value) | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias; - const typeOnlyAliasNeedsPromotion = previousToken && !ts.isValidTypeOnlyAliasUseSite(previousToken); - symbols = ts.concatenate(symbols, typeChecker.getSymbolsInScope(scopeNode, symbolMeanings)); - ts.Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); - for (let i = 0; i < symbols.length; i++) { - const symbol = symbols[i]; - if (!typeChecker.isArgumentsSymbol(symbol) && - !ts.some(symbol.declarations, d => d.getSourceFile() === sourceFile)) { - symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.GlobalsOrKeywords; - } - if (typeOnlyAliasNeedsPromotion && !(symbol.flags & ts.SymbolFlags.Value)) { - const typeOnlyAliasDeclaration = symbol.declarations && ts.find(symbol.declarations, ts.isTypeOnlyImportOrExportDeclaration); - if (typeOnlyAliasDeclaration) { - const origin: SymbolOriginInfoTypeOnlyAlias = { kind: SymbolOriginInfoKind.TypeOnlyAlias, declaration: typeOnlyAliasDeclaration }; - symbolToOriginInfoMap[i] = origin; - } + function tryGetImportCompletionSymbols(): GlobalsSearch { + if (!importCompletionNode) + return GlobalsSearch.Continue; + isNewIdentifierLocation = true; + collectAutoImports(); + 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(); + + if (previousToken !== contextToken) { + ts.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 = (isTypeOnlyLocation ? ts.SymbolFlags.None : ts.SymbolFlags.Value) | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias; + const typeOnlyAliasNeedsPromotion = previousToken && !ts.isValidTypeOnlyAliasUseSite(previousToken); + symbols = ts.concatenate(symbols, typeChecker.getSymbolsInScope(scopeNode, symbolMeanings)); + ts.Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); + for (let i = 0; i < symbols.length; i++) { + const symbol = symbols[i]; + if (!typeChecker.isArgumentsSymbol(symbol) && + !ts.some(symbol.declarations, d => d.getSourceFile() === sourceFile)) { + symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.GlobalsOrKeywords; + } + if (typeOnlyAliasNeedsPromotion && !(symbol.flags & ts.SymbolFlags.Value)) { + const typeOnlyAliasDeclaration = symbol.declarations && ts.find(symbol.declarations, ts.isTypeOnlyImportOrExportDeclaration); + if (typeOnlyAliasDeclaration) { + const origin: SymbolOriginInfoTypeOnlyAlias = { kind: SymbolOriginInfoKind.TypeOnlyAlias, declaration: typeOnlyAliasDeclaration }; + symbolToOriginInfoMap[i] = origin; } } + } - // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` - if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== ts.SyntaxKind.SourceFile) { - const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false); - if (thisType && !isProbablyGlobalType(thisType, sourceFile, typeChecker)) { - for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) { - symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.ThisType }; - symbols.push(symbol); - symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.SuggestedClassMembers; - } + // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` + if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== ts.SyntaxKind.SourceFile) { + const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false); + if (thisType && !isProbablyGlobalType(thisType, sourceFile, typeChecker)) { + for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) { + symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.ThisType }; + symbols.push(symbol); + symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.SuggestedClassMembers; } } - collectAutoImports(); - if (isTypeOnlyLocation) { - keywordFilters = contextToken && ts.isAssertionExpression(contextToken.parent) - ? KeywordCompletionFilters.TypeAssertionKeywords - : KeywordCompletionFilters.TypeKeywords; - } } - - function shouldOfferImportCompletions(): boolean { - // If already typing an import statement, provide completions for it. - if (importCompletionNode) - return true; - // If current completion is for non-contextual Object literal shortahands, ignore auto-import symbols - if (isNonContextualObjectLiteral) - return false; - // If not already a module, must have modules enabled. - if (!preferences.includeCompletionsForModuleExports) - return false; - // If already using ES modules, OK to continue using them. - if (sourceFile.externalModuleIndicator || sourceFile.commonJsModuleIndicator) - return true; - // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. - if (ts.compilerOptionsIndicateEsModules(program.getCompilerOptions())) - return true; - // If some file is using ES6 modules, assume that it's OK to add more. - return ts.programContainsModules(program); + collectAutoImports(); + if (isTypeOnlyLocation) { + keywordFilters = contextToken && ts.isAssertionExpression(contextToken.parent) + ? KeywordCompletionFilters.TypeAssertionKeywords + : KeywordCompletionFilters.TypeKeywords; } + } - function isSnippetScope(scopeNode: ts.Node): boolean { - switch (scopeNode.kind) { - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.TemplateExpression: - case ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.Block: - return true; - default: - return ts.isStatement(scopeNode); - } - } - - function isTypeOnlyCompletion(): boolean { - return insideJsDocTagTypeExpression - || !!importCompletionNode && ts.isTypeOnlyImportOrExportDeclaration(location.parent) - || !isContextTokenValueLocation(contextToken) && - (ts.isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) - || ts.isPartOfTypeNode(location) - || isContextTokenTypeLocation(contextToken)); - } - - function isContextTokenValueLocation(contextToken: ts.Node) { - return contextToken && - ((contextToken.kind === ts.SyntaxKind.TypeOfKeyword && - (contextToken.parent.kind === ts.SyntaxKind.TypeQuery || ts.isTypeOfExpression(contextToken.parent))) || - (contextToken.kind === ts.SyntaxKind.AssertsKeyword && contextToken.parent.kind === ts.SyntaxKind.TypePredicate)); - } - - function isContextTokenTypeLocation(contextToken: ts.Node): boolean { - if (contextToken) { - const parentKind = contextToken.parent.kind; - switch (contextToken.kind) { - case ts.SyntaxKind.ColonToken: - return parentKind === ts.SyntaxKind.PropertyDeclaration || - parentKind === ts.SyntaxKind.PropertySignature || - parentKind === ts.SyntaxKind.Parameter || - parentKind === ts.SyntaxKind.VariableDeclaration || - ts.isFunctionLikeKind(parentKind); - case ts.SyntaxKind.EqualsToken: - return parentKind === ts.SyntaxKind.TypeAliasDeclaration; - case ts.SyntaxKind.AsKeyword: - return parentKind === ts.SyntaxKind.AsExpression; - case ts.SyntaxKind.LessThanToken: - return parentKind === ts.SyntaxKind.TypeReference || - parentKind === ts.SyntaxKind.TypeAssertionExpression; - case ts.SyntaxKind.ExtendsKeyword: - return parentKind === ts.SyntaxKind.TypeParameter; - } - } + function shouldOfferImportCompletions(): boolean { + // If already typing an import statement, provide completions for it. + if (importCompletionNode) + return true; + // If current completion is for non-contextual Object literal shortahands, ignore auto-import symbols + if (isNonContextualObjectLiteral) + return false; + // If not already a module, must have modules enabled. + if (!preferences.includeCompletionsForModuleExports) return false; + // If already using ES modules, OK to continue using them. + if (sourceFile.externalModuleIndicator || sourceFile.commonJsModuleIndicator) + return true; + // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. + if (ts.compilerOptionsIndicateEsModules(program.getCompilerOptions())) + return true; + // If some file is using ES6 modules, assume that it's OK to add more. + return ts.programContainsModules(program); + } + + function isSnippetScope(scopeNode: ts.Node): boolean { + switch (scopeNode.kind) { + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.TemplateExpression: + case ts.SyntaxKind.JsxExpression: + case ts.SyntaxKind.Block: + return true; + default: + return ts.isStatement(scopeNode); } + } - /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ - function collectAutoImports() { - if (!shouldOfferImportCompletions()) - return; - ts.Debug.assert(!detailsEntryId?.data, "Should not run 'collectAutoImports' when faster path is available via `data`"); - if (detailsEntryId && !detailsEntryId.source) { - // Asking for completion details for an item that is not an auto-import - return; + function isTypeOnlyCompletion(): boolean { + return insideJsDocTagTypeExpression + || !!importCompletionNode && ts.isTypeOnlyImportOrExportDeclaration(location.parent) + || !isContextTokenValueLocation(contextToken) && + (ts.isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) + || ts.isPartOfTypeNode(location) + || isContextTokenTypeLocation(contextToken)); + } + + function isContextTokenValueLocation(contextToken: ts.Node) { + return contextToken && + ((contextToken.kind === ts.SyntaxKind.TypeOfKeyword && + (contextToken.parent.kind === ts.SyntaxKind.TypeQuery || ts.isTypeOfExpression(contextToken.parent))) || + (contextToken.kind === ts.SyntaxKind.AssertsKeyword && contextToken.parent.kind === ts.SyntaxKind.TypePredicate)); + } + + function isContextTokenTypeLocation(contextToken: ts.Node): boolean { + if (contextToken) { + const parentKind = contextToken.parent.kind; + switch (contextToken.kind) { + case ts.SyntaxKind.ColonToken: + return parentKind === ts.SyntaxKind.PropertyDeclaration || + parentKind === ts.SyntaxKind.PropertySignature || + parentKind === ts.SyntaxKind.Parameter || + parentKind === ts.SyntaxKind.VariableDeclaration || + ts.isFunctionLikeKind(parentKind); + case ts.SyntaxKind.EqualsToken: + return parentKind === ts.SyntaxKind.TypeAliasDeclaration; + case ts.SyntaxKind.AsKeyword: + return parentKind === ts.SyntaxKind.AsExpression; + case ts.SyntaxKind.LessThanToken: + return parentKind === ts.SyntaxKind.TypeReference || + parentKind === ts.SyntaxKind.TypeAssertionExpression; + case ts.SyntaxKind.ExtendsKeyword: + return parentKind === ts.SyntaxKind.TypeParameter; } + } + return false; + } - flags |= ts.CompletionInfoFlags.MayIncludeAutoImports; - // import { type | -> token text should be blank - const isAfterTypeOnlyImportSpecifierModifier = previousToken === contextToken - && importCompletionNode - && couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken); - - const lowerCaseTokenText = isAfterTypeOnlyImportSpecifierModifier ? "" : - previousToken && ts.isIdentifier(previousToken) ? previousToken.text.toLowerCase() : - ""; - - const moduleSpecifierCache = host.getModuleSpecifierCache?.(); - const exportInfo = ts.getExportInfoMap(sourceFile, host, program, cancellationToken); - const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); - const packageJsonFilter = detailsEntryId ? undefined : ts.createPackageJsonImportFilter(sourceFile, preferences, host); - resolvingModuleSpecifiers("collectAutoImports", host, program, sourceFile, position, preferences, !!importCompletionNode, ts.isValidTypeOnlyAliasUseSite(location), context => { - exportInfo.search(sourceFile.path, - /*preferCapitalized*/ isRightOfOpenTag, (symbolName, targetFlags) => { - if (!ts.isIdentifierText(symbolName, ts.getEmitScriptTarget(host.getCompilationSettings()))) - return false; - if (!detailsEntryId && ts.isStringANonContextualKeyword(symbolName)) - return false; - if (!isTypeOnlyLocation && !importCompletionNode && !(targetFlags & ts.SymbolFlags.Value)) - return false; - if (isTypeOnlyLocation && !(targetFlags & (ts.SymbolFlags.Module | ts.SymbolFlags.Type))) - return false; - // Do not try to auto-import something with a lowercase first letter for a JSX tag - const firstChar = symbolName.charCodeAt(0); - if (isRightOfOpenTag && (firstChar < ts.CharacterCodes.A || firstChar > ts.CharacterCodes.Z)) - return false; - if (detailsEntryId) - return true; - return charactersFuzzyMatchInString(symbolName, lowerCaseTokenText); - }, (info, symbolName, isFromAmbientModule, exportMapKey) => { - if (detailsEntryId && !ts.some(info, i => detailsEntryId.source === ts.stripQuotes(i.moduleSymbol.name))) { - return; - } + /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ + function collectAutoImports() { + if (!shouldOfferImportCompletions()) + return; + ts.Debug.assert(!detailsEntryId?.data, "Should not run 'collectAutoImports' when faster path is available via `data`"); + if (detailsEntryId && !detailsEntryId.source) { + // Asking for completion details for an item that is not an auto-import + return; + } + + flags |= ts.CompletionInfoFlags.MayIncludeAutoImports; + // import { type | -> token text should be blank + const isAfterTypeOnlyImportSpecifierModifier = previousToken === contextToken + && importCompletionNode + && couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken); + + const lowerCaseTokenText = isAfterTypeOnlyImportSpecifierModifier ? "" : + previousToken && ts.isIdentifier(previousToken) ? previousToken.text.toLowerCase() : + ""; + + const moduleSpecifierCache = host.getModuleSpecifierCache?.(); + const exportInfo = ts.getExportInfoMap(sourceFile, host, program, cancellationToken); + const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); + const packageJsonFilter = detailsEntryId ? undefined : ts.createPackageJsonImportFilter(sourceFile, preferences, host); + resolvingModuleSpecifiers("collectAutoImports", host, program, sourceFile, position, preferences, !!importCompletionNode, ts.isValidTypeOnlyAliasUseSite(location), context => { + exportInfo.search(sourceFile.path, + /*preferCapitalized*/ isRightOfOpenTag, (symbolName, targetFlags) => { + if (!ts.isIdentifierText(symbolName, ts.getEmitScriptTarget(host.getCompilationSettings()))) + return false; + if (!detailsEntryId && ts.isStringANonContextualKeyword(symbolName)) + return false; + if (!isTypeOnlyLocation && !importCompletionNode && !(targetFlags & ts.SymbolFlags.Value)) + return false; + if (isTypeOnlyLocation && !(targetFlags & (ts.SymbolFlags.Module | ts.SymbolFlags.Type))) + return false; + // Do not try to auto-import something with a lowercase first letter for a JSX tag + const firstChar = symbolName.charCodeAt(0); + if (isRightOfOpenTag && (firstChar < ts.CharacterCodes.A || firstChar > ts.CharacterCodes.Z)) + return false; + if (detailsEntryId) + return true; + return charactersFuzzyMatchInString(symbolName, lowerCaseTokenText); + }, (info, symbolName, isFromAmbientModule, exportMapKey) => { + if (detailsEntryId && !ts.some(info, i => detailsEntryId.source === ts.stripQuotes(i.moduleSymbol.name))) { + return; + } - // Do a relatively cheap check to bail early if all re-exports are non-importable - // due to file location or package.json dependency filtering. For non-node16+ - // module resolution modes, getting past this point guarantees that we'll be - // able to generate a suitable module specifier, so we can safely show a completion, - // even if we defer computing the module specifier. - const firstImportableExportInfo = ts.find(info, isImportableExportInfo); - if (!firstImportableExportInfo) { - return; - } + // Do a relatively cheap check to bail early if all re-exports are non-importable + // due to file location or package.json dependency filtering. For non-node16+ + // module resolution modes, getting past this point guarantees that we'll be + // able to generate a suitable module specifier, so we can safely show a completion, + // even if we defer computing the module specifier. + const firstImportableExportInfo = ts.find(info, isImportableExportInfo); + if (!firstImportableExportInfo) { + return; + } - // In node16+, module specifier resolution can fail due to modules being blocked - // by package.json `exports`. If that happens, don't show a completion item. - // N.B. in this resolution mode we always try to resolve module specifiers here, - // because we have to know now if it's going to fail so we can omit the completion - // from the list. - const result = context.tryResolve(info, symbolName, isFromAmbientModule) || {}; - if (result === "failed") - return; - - // If we skipped resolving module specifiers, our selection of which ExportInfo - // to use here is arbitrary, since the info shown in the completion list derived from - // it should be identical regardless of which one is used. During the subsequent - // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick - // the best one based on the module specifier it produces. - let exportInfo = firstImportableExportInfo, moduleSpecifier; - if (result !== "skipped") { - ({ exportInfo = firstImportableExportInfo, moduleSpecifier } = result); - } + // In node16+, module specifier resolution can fail due to modules being blocked + // by package.json `exports`. If that happens, don't show a completion item. + // N.B. in this resolution mode we always try to resolve module specifiers here, + // because we have to know now if it's going to fail so we can omit the completion + // from the list. + const result = context.tryResolve(info, symbolName, isFromAmbientModule) || {}; + if (result === "failed") + return; - const isDefaultExport = exportInfo.exportKind === ts.ExportKind.Default; - const symbol = isDefaultExport && ts.getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; - - pushAutoImportSymbol(symbol, { - kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, - moduleSpecifier, - symbolName, - exportMapKey, - exportName: exportInfo.exportKind === ts.ExportKind.ExportEquals ? ts.InternalSymbolName.ExportEquals : exportInfo.symbol.name, - fileName: exportInfo.moduleFileName, - isDefaultExport, - moduleSymbol: exportInfo.moduleSymbol, - isFromPackageJson: exportInfo.isFromPackageJson, - }); - }); + // If we skipped resolving module specifiers, our selection of which ExportInfo + // to use here is arbitrary, since the info shown in the completion list derived from + // it should be identical regardless of which one is used. During the subsequent + // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick + // the best one based on the module specifier it produces. + let exportInfo = firstImportableExportInfo, moduleSpecifier; + if (result !== "skipped") { + ({ exportInfo = firstImportableExportInfo, moduleSpecifier } = result); + } - hasUnresolvedAutoImports = context.skippedAny(); - flags |= context.resolvedAny() ? ts.CompletionInfoFlags.ResolvedModuleSpecifiers : 0; - flags |= context.resolvedBeyondLimit() ? ts.CompletionInfoFlags.ResolvedModuleSpecifiersBeyondLimit : 0; + const isDefaultExport = exportInfo.exportKind === ts.ExportKind.Default; + const symbol = isDefaultExport && ts.getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; + + pushAutoImportSymbol(symbol, { + kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, + moduleSpecifier, + symbolName, + exportMapKey, + exportName: exportInfo.exportKind === ts.ExportKind.ExportEquals ? ts.InternalSymbolName.ExportEquals : exportInfo.symbol.name, + fileName: exportInfo.moduleFileName, + isDefaultExport, + moduleSymbol: exportInfo.moduleSymbol, + isFromPackageJson: exportInfo.isFromPackageJson, + }); }); - function isImportableExportInfo(info: ts.SymbolExportInfo) { - const moduleFile = ts.tryCast(info.moduleSymbol.valueDeclaration, ts.isSourceFile); - if (!moduleFile) { - const moduleName = ts.stripQuotes(info.moduleSymbol.name); - if (ts.JsTyping.nodeCoreModules.has(moduleName) && ts.startsWith(moduleName, "node:") !== ts.shouldUseUriStyleNodeCoreModules(sourceFile, program)) { - return false; - } - return packageJsonFilter - ? packageJsonFilter.allowsImportingAmbientModule(info.moduleSymbol, getModuleSpecifierResolutionHost(info.isFromPackageJson)) - : true; + + hasUnresolvedAutoImports = context.skippedAny(); + flags |= context.resolvedAny() ? ts.CompletionInfoFlags.ResolvedModuleSpecifiers : 0; + flags |= context.resolvedBeyondLimit() ? ts.CompletionInfoFlags.ResolvedModuleSpecifiersBeyondLimit : 0; + }); + function isImportableExportInfo(info: ts.SymbolExportInfo) { + const moduleFile = ts.tryCast(info.moduleSymbol.valueDeclaration, ts.isSourceFile); + if (!moduleFile) { + const moduleName = ts.stripQuotes(info.moduleSymbol.name); + if (ts.JsTyping.nodeCoreModules.has(moduleName) && ts.startsWith(moduleName, "node:") !== ts.shouldUseUriStyleNodeCoreModules(sourceFile, program)) { + return false; } - return ts.isImportableFile(info.isFromPackageJson ? packageJsonAutoImportProvider! : program, sourceFile, moduleFile, preferences, packageJsonFilter, getModuleSpecifierResolutionHost(info.isFromPackageJson), moduleSpecifierCache); + return packageJsonFilter + ? packageJsonFilter.allowsImportingAmbientModule(info.moduleSymbol, getModuleSpecifierResolutionHost(info.isFromPackageJson)) + : true; } + return ts.isImportableFile(info.isFromPackageJson ? packageJsonAutoImportProvider! : program, sourceFile, moduleFile, preferences, packageJsonFilter, getModuleSpecifierResolutionHost(info.isFromPackageJson), moduleSpecifierCache); } + } - function pushAutoImportSymbol(symbol: ts.Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { - const symbolId = ts.getSymbolId(symbol); - if (symbolToSortTextMap[symbolId] === SortText.GlobalsOrKeywords) { - // If an auto-importable symbol is available as a global, don't add the auto import - return; - } - symbolToOriginInfoMap[symbols.length] = origin; - symbolToSortTextMap[symbolId] = importCompletionNode ? SortText.LocationPriority : SortText.AutoImportSuggestions; - symbols.push(symbol); + function pushAutoImportSymbol(symbol: ts.Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { + const symbolId = ts.getSymbolId(symbol); + if (symbolToSortTextMap[symbolId] === SortText.GlobalsOrKeywords) { + // If an auto-importable symbol is available as a global, don't add the auto import + return; } + symbolToOriginInfoMap[symbols.length] = origin; + symbolToSortTextMap[symbolId] = importCompletionNode ? SortText.LocationPriority : SortText.AutoImportSuggestions; + symbols.push(symbol); + } - /* Mutates `symbols` and `symbolToOriginInfoMap`. */ - function collectObjectLiteralMethodSymbols(members: ts.Symbol[], enclosingDeclaration: ts.ObjectLiteralExpression): void { - // TODO: support JS files. - if (ts.isInJSFile(location)) { + /* Mutates `symbols` and `symbolToOriginInfoMap`. */ + function collectObjectLiteralMethodSymbols(members: ts.Symbol[], enclosingDeclaration: ts.ObjectLiteralExpression): void { + // TODO: support JS files. + if (ts.isInJSFile(location)) { + return; + } + members.forEach(member => { + if (!isObjectLiteralMethodSymbol(member)) { return; } - members.forEach(member => { - if (!isObjectLiteralMethodSymbol(member)) { - return; - } - const displayName = getCompletionEntryDisplayNameForSymbol(member, ts.getEmitScriptTarget(compilerOptions), - /*origin*/ undefined, CompletionKind.ObjectPropertyDeclaration, - /*jsxIdentifierExpected*/ false); - if (!displayName) { - return; - } - const { name } = displayName; - const entryProps = getEntryForObjectLiteralMethodCompletion(member, name, enclosingDeclaration, program, host, compilerOptions, preferences, formatContext); - if (!entryProps) { - return; - } - const origin: SymbolOriginInfoObjectLiteralMethod = { kind: SymbolOriginInfoKind.ObjectLiteralMethod, ...entryProps }; - flags |= ts.CompletionInfoFlags.MayIncludeMethodSnippets; - symbolToOriginInfoMap[symbols.length] = origin; - symbols.push(member); - }); - } - - function isObjectLiteralMethodSymbol(symbol: ts.Symbol): boolean { - /* - For an object type - `type Foo = { - bar(x: number): void; - foo: (x: string) => string; - }`, - `bar` will have symbol flag `Method`, - `foo` will have symbol flag `Property`. - */ - if (!(symbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method))) { - return false; + const displayName = getCompletionEntryDisplayNameForSymbol(member, ts.getEmitScriptTarget(compilerOptions), + /*origin*/ undefined, CompletionKind.ObjectPropertyDeclaration, + /*jsxIdentifierExpected*/ false); + if (!displayName) { + return; } - return true; + const { name } = displayName; + const entryProps = getEntryForObjectLiteralMethodCompletion(member, name, enclosingDeclaration, program, host, compilerOptions, preferences, formatContext); + if (!entryProps) { + return; + } + const origin: SymbolOriginInfoObjectLiteralMethod = { kind: SymbolOriginInfoKind.ObjectLiteralMethod, ...entryProps }; + flags |= ts.CompletionInfoFlags.MayIncludeMethodSnippets; + symbolToOriginInfoMap[symbols.length] = origin; + symbols.push(member); + }); + } + + function isObjectLiteralMethodSymbol(symbol: ts.Symbol): boolean { + /* + For an object type + `type Foo = { + bar(x: number): void; + foo: (x: string) => string; + }`, + `bar` will have symbol flag `Method`, + `foo` will have symbol flag `Property`. + */ + if (!(symbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method))) { + return false; } + return true; + } - /** - * Finds the first node that "embraces" the position, so that one may - * accurately aggregate locals from the closest containing scope. - */ - function getScopeNode(initialToken: ts.Node | undefined, position: number, sourceFile: ts.SourceFile) { - let scope: ts.Node | undefined = initialToken; - while (scope && !ts.positionBelongsToNode(scope, position, sourceFile)) { - scope = scope.parent; - } - return scope; + /** + * Finds the first node that "embraces" the position, so that one may + * accurately aggregate locals from the closest containing scope. + */ + function getScopeNode(initialToken: ts.Node | undefined, position: number, sourceFile: ts.SourceFile) { + let scope: ts.Node | undefined = initialToken; + while (scope && !ts.positionBelongsToNode(scope, position, sourceFile)) { + scope = scope.parent; } + return scope; + } + + function isCompletionListBlocker(contextToken: ts.Node): boolean { + const start = ts.timestamp(); + const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || + isSolelyIdentifierDefinitionLocation(contextToken) || + isDotOfNumericLiteral(contextToken) || + isInJsxText(contextToken) || + ts.isBigIntLiteral(contextToken); + log("getCompletionsAtPosition: isCompletionListBlocker: " + (ts.timestamp() - start)); + return result; + } - function isCompletionListBlocker(contextToken: ts.Node): boolean { - const start = ts.timestamp(); - const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || - isSolelyIdentifierDefinitionLocation(contextToken) || - isDotOfNumericLiteral(contextToken) || - isInJsxText(contextToken) || - ts.isBigIntLiteral(contextToken); - log("getCompletionsAtPosition: isCompletionListBlocker: " + (ts.timestamp() - start)); - return result; + function isInJsxText(contextToken: ts.Node): boolean { + if (contextToken.kind === ts.SyntaxKind.JsxText) { + return true; } - function isInJsxText(contextToken: ts.Node): boolean { - if (contextToken.kind === ts.SyntaxKind.JsxText) { - return true; + if (contextToken.kind === ts.SyntaxKind.GreaterThanToken && contextToken.parent) { + // /**/ /> + // /**/ > + // - contextToken: GreaterThanToken (before cursor) + // - location: JsxSelfClosingElement or JsxOpeningElement + // - contextToken.parent === location + if (location === contextToken.parent && (location.kind === ts.SyntaxKind.JsxOpeningElement || location.kind === ts.SyntaxKind.JsxSelfClosingElement)) { + return false; } - if (contextToken.kind === ts.SyntaxKind.GreaterThanToken && contextToken.parent) { - // /**/ /> - // /**/ > + if (contextToken.parent.kind === ts.SyntaxKind.JsxOpeningElement) { + //
/**/ // - contextToken: GreaterThanToken (before cursor) - // - location: JsxSelfClosingElement or JsxOpeningElement - // - contextToken.parent === location - if (location === contextToken.parent && (location.kind === ts.SyntaxKind.JsxOpeningElement || location.kind === ts.SyntaxKind.JsxSelfClosingElement)) { - return false; - } - - if (contextToken.parent.kind === ts.SyntaxKind.JsxOpeningElement) { - //
/**/ - // - contextToken: GreaterThanToken (before cursor) - // - location: JSXElement - // - different parents (JSXOpeningElement, JSXElement) - return location.parent.kind !== ts.SyntaxKind.JsxOpeningElement; - } + // - location: JSXElement + // - different parents (JSXOpeningElement, JSXElement) + return location.parent.kind !== ts.SyntaxKind.JsxOpeningElement; + } - if (contextToken.parent.kind === ts.SyntaxKind.JsxClosingElement || contextToken.parent.kind === ts.SyntaxKind.JsxSelfClosingElement) { - return !!contextToken.parent.parent && contextToken.parent.parent.kind === ts.SyntaxKind.JsxElement; - } + if (contextToken.parent.kind === ts.SyntaxKind.JsxClosingElement || contextToken.parent.kind === ts.SyntaxKind.JsxSelfClosingElement) { + return !!contextToken.parent.parent && contextToken.parent.parent.kind === ts.SyntaxKind.JsxElement; } - return false; } + return false; + } - function isNewIdentifierDefinitionLocation(): boolean { - if (contextToken) { - const containingNodeKind = contextToken.parent.kind; - const tokenKind = keywordForNode(contextToken); - // Previous token may have been a keyword that was converted to an identifier. - switch (tokenKind) { - case ts.SyntaxKind.CommaToken: - return containingNodeKind === ts.SyntaxKind.CallExpression // func( a, | - || containingNodeKind === ts.SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ - || containingNodeKind === ts.SyntaxKind.NewExpression // new C(a, | - || containingNodeKind === ts.SyntaxKind.ArrayLiteralExpression // [a, | - || containingNodeKind === ts.SyntaxKind.BinaryExpression // const x = (a, | - || containingNodeKind === ts.SyntaxKind.FunctionType // var x: (s: string, list| - || containingNodeKind === ts.SyntaxKind.ObjectLiteralExpression; // const obj = { x, | - case ts.SyntaxKind.OpenParenToken: - return containingNodeKind === ts.SyntaxKind.CallExpression // func( | - || containingNodeKind === ts.SyntaxKind.Constructor // constructor( | - || containingNodeKind === ts.SyntaxKind.NewExpression // new C(a| - || containingNodeKind === ts.SyntaxKind.ParenthesizedExpression // const x = (a| - || containingNodeKind === ts.SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ - case ts.SyntaxKind.OpenBracketToken: - return containingNodeKind === ts.SyntaxKind.ArrayLiteralExpression // [ | - || containingNodeKind === ts.SyntaxKind.IndexSignature // [ | : string ] - || containingNodeKind === ts.SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ - case ts.SyntaxKind.ModuleKeyword: // module | - case ts.SyntaxKind.NamespaceKeyword: // namespace | - case ts.SyntaxKind.ImportKeyword: // import | - return true; - - case ts.SyntaxKind.DotToken: - return containingNodeKind === ts.SyntaxKind.ModuleDeclaration; // module A.| - case ts.SyntaxKind.OpenBraceToken: - return containingNodeKind === ts.SyntaxKind.ClassDeclaration // class A { | - || containingNodeKind === ts.SyntaxKind.ObjectLiteralExpression; // const obj = { | - case ts.SyntaxKind.EqualsToken: - return containingNodeKind === ts.SyntaxKind.VariableDeclaration // const x = a| - || containingNodeKind === ts.SyntaxKind.BinaryExpression; // x = a| - case ts.SyntaxKind.TemplateHead: - return containingNodeKind === ts.SyntaxKind.TemplateExpression; // `aa ${| - case ts.SyntaxKind.TemplateMiddle: - return containingNodeKind === ts.SyntaxKind.TemplateSpan; // `aa ${10} dd ${| - case ts.SyntaxKind.AsyncKeyword: - return containingNodeKind === ts.SyntaxKind.MethodDeclaration // const obj = { async c|() - || containingNodeKind === ts.SyntaxKind.ShorthandPropertyAssignment; // const obj = { async c| - case ts.SyntaxKind.AsteriskToken: - return containingNodeKind === ts.SyntaxKind.MethodDeclaration; // const obj = { * c| - } - - if (isClassMemberCompletionKeyword(tokenKind)) { + function isNewIdentifierDefinitionLocation(): boolean { + if (contextToken) { + const containingNodeKind = contextToken.parent.kind; + const tokenKind = keywordForNode(contextToken); + // Previous token may have been a keyword that was converted to an identifier. + switch (tokenKind) { + case ts.SyntaxKind.CommaToken: + return containingNodeKind === ts.SyntaxKind.CallExpression // func( a, | + || containingNodeKind === ts.SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ + || containingNodeKind === ts.SyntaxKind.NewExpression // new C(a, | + || containingNodeKind === ts.SyntaxKind.ArrayLiteralExpression // [a, | + || containingNodeKind === ts.SyntaxKind.BinaryExpression // const x = (a, | + || containingNodeKind === ts.SyntaxKind.FunctionType // var x: (s: string, list| + || containingNodeKind === ts.SyntaxKind.ObjectLiteralExpression; // const obj = { x, | + case ts.SyntaxKind.OpenParenToken: + return containingNodeKind === ts.SyntaxKind.CallExpression // func( | + || containingNodeKind === ts.SyntaxKind.Constructor // constructor( | + || containingNodeKind === ts.SyntaxKind.NewExpression // new C(a| + || containingNodeKind === ts.SyntaxKind.ParenthesizedExpression // const x = (a| + || containingNodeKind === ts.SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ + case ts.SyntaxKind.OpenBracketToken: + return containingNodeKind === ts.SyntaxKind.ArrayLiteralExpression // [ | + || containingNodeKind === ts.SyntaxKind.IndexSignature // [ | : string ] + || containingNodeKind === ts.SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ + case ts.SyntaxKind.ModuleKeyword: // module | + case ts.SyntaxKind.NamespaceKeyword: // namespace | + case ts.SyntaxKind.ImportKeyword: // import | return true; - } - } - return false; - } + case ts.SyntaxKind.DotToken: + return containingNodeKind === ts.SyntaxKind.ModuleDeclaration; // module A.| + case ts.SyntaxKind.OpenBraceToken: + return containingNodeKind === ts.SyntaxKind.ClassDeclaration // class A { | + || containingNodeKind === ts.SyntaxKind.ObjectLiteralExpression; // const obj = { | + case ts.SyntaxKind.EqualsToken: + return containingNodeKind === ts.SyntaxKind.VariableDeclaration // const x = a| + || containingNodeKind === ts.SyntaxKind.BinaryExpression; // x = a| + case ts.SyntaxKind.TemplateHead: + return containingNodeKind === ts.SyntaxKind.TemplateExpression; // `aa ${| + case ts.SyntaxKind.TemplateMiddle: + return containingNodeKind === ts.SyntaxKind.TemplateSpan; // `aa ${10} dd ${| + case ts.SyntaxKind.AsyncKeyword: + return containingNodeKind === ts.SyntaxKind.MethodDeclaration // const obj = { async c|() + || containingNodeKind === ts.SyntaxKind.ShorthandPropertyAssignment; // const obj = { async c| + case ts.SyntaxKind.AsteriskToken: + return containingNodeKind === ts.SyntaxKind.MethodDeclaration; // const obj = { * c| + } - function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: ts.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 (ts.isRegularExpressionLiteral(contextToken) || ts.isStringTextContainingNode(contextToken)) && (ts.rangeContainsPositionExclusive(ts.createTextRangeFromSpan(ts.createTextSpanFromNode(contextToken)), position) || - position === contextToken.end && (!!contextToken.isUnterminated || ts.isRegularExpressionLiteral(contextToken))); + if (isClassMemberCompletionKeyword(tokenKind)) { + return true; + } } - function tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols(): GlobalsSearch | undefined { - const typeLiteralNode = tryGetTypeLiteralNode(contextToken); - if (!typeLiteralNode) - return GlobalsSearch.Continue; - const intersectionTypeNode = ts.isIntersectionTypeNode(typeLiteralNode.parent) ? typeLiteralNode.parent : undefined; - const containerTypeNode = intersectionTypeNode || typeLiteralNode; + return false; + } - const containerExpectedType = getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker); - if (!containerExpectedType) - return GlobalsSearch.Continue; + function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: ts.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 (ts.isRegularExpressionLiteral(contextToken) || ts.isStringTextContainingNode(contextToken)) && (ts.rangeContainsPositionExclusive(ts.createTextRangeFromSpan(ts.createTextSpanFromNode(contextToken)), position) || + position === contextToken.end && (!!contextToken.isUnterminated || ts.isRegularExpressionLiteral(contextToken))); + } - const containerActualType = typeChecker.getTypeFromTypeNode(containerTypeNode); + function tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols(): GlobalsSearch | undefined { + const typeLiteralNode = tryGetTypeLiteralNode(contextToken); + if (!typeLiteralNode) + return GlobalsSearch.Continue; + const intersectionTypeNode = ts.isIntersectionTypeNode(typeLiteralNode.parent) ? typeLiteralNode.parent : undefined; + const containerTypeNode = intersectionTypeNode || typeLiteralNode; - const members = getPropertiesForCompletion(containerExpectedType, typeChecker); - const existingMembers = getPropertiesForCompletion(containerActualType, typeChecker); + const containerExpectedType = getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker); + if (!containerExpectedType) + return GlobalsSearch.Continue; - const existingMemberEscapedNames: ts.Set = new ts.Set(); - existingMembers.forEach(s => existingMemberEscapedNames.add(s.escapedName)); + const containerActualType = typeChecker.getTypeFromTypeNode(containerTypeNode); - symbols = ts.concatenate(symbols, ts.filter(members, s => !existingMemberEscapedNames.has(s.escapedName))); + const members = getPropertiesForCompletion(containerExpectedType, typeChecker); + const existingMembers = getPropertiesForCompletion(containerActualType, typeChecker); - completionKind = CompletionKind.ObjectPropertyDeclaration; - isNewIdentifierLocation = true; + const existingMemberEscapedNames: ts.Set = new ts.Set(); + existingMembers.forEach(s => existingMemberEscapedNames.add(s.escapedName)); - return GlobalsSearch.Success; - } + symbols = ts.concatenate(symbols, ts.filter(members, s => !existingMemberEscapedNames.has(s.escapedName))); - /** - * 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 symbolsStartIndex = symbols.length; - const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken); - if (!objectLikeContainer) - return GlobalsSearch.Continue; + completionKind = CompletionKind.ObjectPropertyDeclaration; + isNewIdentifierLocation = true; - // We're looking up possible property names from contextual/inferred/declared type. - completionKind = CompletionKind.ObjectPropertyDeclaration; + return GlobalsSearch.Success; + } - let typeMembers: ts.Symbol[] | undefined; - let existingMembers: readonly ts.Declaration[] | undefined; - if (objectLikeContainer.kind === ts.SyntaxKind.ObjectLiteralExpression) { - const instantiatedType = tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker); + /** + * 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 symbolsStartIndex = symbols.length; + 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: ts.Symbol[] | undefined; + let existingMembers: readonly ts.Declaration[] | undefined; + if (objectLikeContainer.kind === ts.SyntaxKind.ObjectLiteralExpression) { + const instantiatedType = tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker); + + // Check completions for Object property value shorthand + if (instantiatedType === undefined) { + if (objectLikeContainer.flags & ts.NodeFlags.InWithStatement) { + return GlobalsSearch.Fail; + } + isNonContextualObjectLiteral = true; + return GlobalsSearch.Continue; + } + const completionsType = typeChecker.getContextualType(objectLikeContainer, ts.ContextFlags.Completions); + const hasStringIndexType = (completionsType || instantiatedType).getStringIndexType(); + const hasNumberIndextype = (completionsType || instantiatedType).getNumberIndexType(); + isNewIdentifierLocation = !!hasStringIndexType || !!hasNumberIndextype; + typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker); + existingMembers = objectLikeContainer.properties; - // Check completions for Object property value shorthand - if (instantiatedType === undefined) { - if (objectLikeContainer.flags & ts.NodeFlags.InWithStatement) { - return GlobalsSearch.Fail; - } + if (typeMembers.length === 0) { + // Edge case: If NumberIndexType exists + if (!hasNumberIndextype) { isNonContextualObjectLiteral = true; return GlobalsSearch.Continue; } - const completionsType = typeChecker.getContextualType(objectLikeContainer, ts.ContextFlags.Completions); - const hasStringIndexType = (completionsType || instantiatedType).getStringIndexType(); - const hasNumberIndextype = (completionsType || instantiatedType).getNumberIndexType(); - isNewIdentifierLocation = !!hasStringIndexType || !!hasNumberIndextype; - typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker); - existingMembers = objectLikeContainer.properties; - - if (typeMembers.length === 0) { - // Edge case: If NumberIndexType exists - if (!hasNumberIndextype) { - isNonContextualObjectLiteral = true; - return GlobalsSearch.Continue; - } - } } - else { - ts.Debug.assert(objectLikeContainer.kind === ts.SyntaxKind.ObjectBindingPattern); - // We are *only* completing on properties from the type being destructured. - isNewIdentifierLocation = false; - - const rootDeclaration = ts.getRootDeclaration(objectLikeContainer.parent); - if (!ts.isVariableLike(rootDeclaration)) - return ts.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 = ts.hasInitializer(rootDeclaration) || !!ts.getEffectiveTypeAnnotationNode(rootDeclaration) || rootDeclaration.parent.parent.kind === ts.SyntaxKind.ForOfStatement; - if (!canGetType && rootDeclaration.kind === ts.SyntaxKind.Parameter) { - if (ts.isExpression(rootDeclaration.parent)) { - canGetType = !!typeChecker.getContextualType(rootDeclaration.parent as ts.Expression); - } - else if (rootDeclaration.parent.kind === ts.SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === ts.SyntaxKind.SetAccessor) { - canGetType = ts.isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(rootDeclaration.parent.parent as ts.Expression); - } - } - if (canGetType) { - const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); - if (!typeForObject) - return GlobalsSearch.Fail; - typeMembers = typeChecker.getPropertiesOfType(typeForObject).filter(propertySymbol => { - return typeChecker.isPropertyAccessible(objectLikeContainer, /*isSuper*/ false, /*writing*/ false, typeForObject, propertySymbol); - }); - existingMembers = objectLikeContainer.elements; - } - } - - if (typeMembers && typeMembers.length > 0) { - // Add filtered items to the completion list - const filteredMembers = filterObjectMembersList(typeMembers, ts.Debug.checkDefined(existingMembers)); - symbols = ts.concatenate(symbols, filteredMembers); - setSortTextToOptionalMember(); - if (objectLikeContainer.kind === ts.SyntaxKind.ObjectLiteralExpression - && preferences.includeCompletionsWithObjectLiteralMethodSnippets - && preferences.includeCompletionsWithInsertText) { - transformObjectLiteralMembersSortText(symbolsStartIndex); - collectObjectLiteralMethodSymbols(filteredMembers, objectLikeContainer); - } - } - - 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. - */ - function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch { - if (!contextToken) - return GlobalsSearch.Continue; - - // `import { |` or `import { a as 0, | }` or `import { type | }` - const namedImportsOrExports = contextToken.kind === ts.SyntaxKind.OpenBraceToken || contextToken.kind === ts.SyntaxKind.CommaToken ? ts.tryCast(contextToken.parent, ts.isNamedImportsOrExports) : - ts.isTypeKeywordTokenOrIdentifier(contextToken) ? ts.tryCast(contextToken.parent.parent, ts.isNamedImportsOrExports) : undefined; - if (!namedImportsOrExports) - return GlobalsSearch.Continue; - - // We can at least offer `type` at `import { |` - if (!ts.isTypeKeywordTokenOrIdentifier(contextToken)) { - keywordFilters = KeywordCompletionFilters.TypeKeyword; - } - - // try to show exported member for imported/re-exported module - const { moduleSpecifier } = namedImportsOrExports.kind === ts.SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent; - if (!moduleSpecifier) { - isNewIdentifierLocation = true; - return namedImportsOrExports.kind === ts.SyntaxKind.NamedImports ? GlobalsSearch.Fail : GlobalsSearch.Continue; - } - const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier); // TODO: GH#18217 - if (!moduleSpecifierSymbol) { - isNewIdentifierLocation = true; - return GlobalsSearch.Fail; - } - - completionKind = CompletionKind.MemberLike; + else { + ts.Debug.assert(objectLikeContainer.kind === ts.SyntaxKind.ObjectBindingPattern); + // We are *only* completing on properties from the type being destructured. isNewIdentifierLocation = false; - const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol); - const existing = new ts.Set((namedImportsOrExports.elements as ts.NodeArray).filter(n => !isCurrentlyEditingNode(n)).map(n => (n.propertyName || n.name).escapedText)); - const uniques = exports.filter(e => e.escapedName !== ts.InternalSymbolName.Default && !existing.has(e.escapedName)); - symbols = ts.concatenate(symbols, uniques); - if (!uniques.length) { - // If there's nothing else to import, don't offer `type` either - keywordFilters = KeywordCompletionFilters.None; + + const rootDeclaration = ts.getRootDeclaration(objectLikeContainer.parent); + if (!ts.isVariableLike(rootDeclaration)) + return ts.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 = ts.hasInitializer(rootDeclaration) || !!ts.getEffectiveTypeAnnotationNode(rootDeclaration) || rootDeclaration.parent.parent.kind === ts.SyntaxKind.ForOfStatement; + if (!canGetType && rootDeclaration.kind === ts.SyntaxKind.Parameter) { + if (ts.isExpression(rootDeclaration.parent)) { + canGetType = !!typeChecker.getContextualType(rootDeclaration.parent as ts.Expression); + } + else if (rootDeclaration.parent.kind === ts.SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === ts.SyntaxKind.SetAccessor) { + canGetType = ts.isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(rootDeclaration.parent.parent as ts.Expression); + } + } + if (canGetType) { + const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); + if (!typeForObject) + return GlobalsSearch.Fail; + typeMembers = typeChecker.getPropertiesOfType(typeForObject).filter(propertySymbol => { + return typeChecker.isPropertyAccessible(objectLikeContainer, /*isSuper*/ false, /*writing*/ false, typeForObject, propertySymbol); + }); + existingMembers = objectLikeContainer.elements; } - return GlobalsSearch.Success; } - /** - * Adds local declarations for completions in named exports: - * - * export { | }; - * - * Does not check for the absence of a module specifier (`export {} from "./other"`) - * because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, - * preventing this function from running. - */ - function tryGetLocalNamedExportCompletionSymbols(): GlobalsSearch { - const namedExports = contextToken && (contextToken.kind === ts.SyntaxKind.OpenBraceToken || contextToken.kind === ts.SyntaxKind.CommaToken) - ? ts.tryCast(contextToken.parent, ts.isNamedExports) - : undefined; - - if (!namedExports) { - return GlobalsSearch.Continue; + if (typeMembers && typeMembers.length > 0) { + // Add filtered items to the completion list + const filteredMembers = filterObjectMembersList(typeMembers, ts.Debug.checkDefined(existingMembers)); + symbols = ts.concatenate(symbols, filteredMembers); + setSortTextToOptionalMember(); + if (objectLikeContainer.kind === ts.SyntaxKind.ObjectLiteralExpression + && preferences.includeCompletionsWithObjectLiteralMethodSnippets + && preferences.includeCompletionsWithInsertText) { + transformObjectLiteralMembersSortText(symbolsStartIndex); + collectObjectLiteralMethodSymbols(filteredMembers, objectLikeContainer); } - - const localsContainer = ts.findAncestor(namedExports, ts.or(ts.isSourceFile, ts.isModuleDeclaration))!; - completionKind = CompletionKind.None; - isNewIdentifierLocation = false; - localsContainer.locals?.forEach((symbol, name) => { - symbols.push(symbol); - if (localsContainer.symbol?.exports?.has(name)) { - symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.OptionalMember; - } - }); - 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; + return GlobalsSearch.Success; + } - // We're looking up possible property names from parent type. - completionKind = CompletionKind.MemberLike; - // Declaring new property/method/accessor - isNewIdentifierLocation = true; - keywordFilters = contextToken.kind === ts.SyntaxKind.AsteriskToken ? KeywordCompletionFilters.None : - ts.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 (!ts.isClassLike(decl)) - return GlobalsSearch.Success; - const classElement = contextToken.kind === ts.SyntaxKind.SemicolonToken ? contextToken.parent.parent : contextToken.parent; - let classElementModifierFlags = ts.isClassElement(classElement) ? ts.getEffectiveModifierFlags(classElement) : ts.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 === ts.SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) { - switch (contextToken.getText()) { - case "private": - classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Private; - break; - case "static": - classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Static; - break; - case "override": - classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Override; - break; - } - } - if (ts.isClassStaticBlockDeclaration(classElement)) { - classElementModifierFlags |= ts.ModifierFlags.Static; - } + /** + * 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. + */ + function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch { + if (!contextToken) + return GlobalsSearch.Continue; - // No member list for private methods - if (!(classElementModifierFlags & ts.ModifierFlags.Private)) { - // List of property symbols of base type that are not private and already implemented - const baseTypeNodes = ts.isClassLike(decl) && classElementModifierFlags & ts.ModifierFlags.Override ? ts.singleElementArray(ts.getEffectiveBaseTypeNode(decl)) : ts.getAllSuperTypeNodes(decl); - const baseSymbols = ts.flatMap(baseTypeNodes, baseTypeNode => { - const type = typeChecker.getTypeAtLocation(baseTypeNode); - return classElementModifierFlags & ts.ModifierFlags.Static ? - type?.symbol && typeChecker.getPropertiesOfType(typeChecker.getTypeOfSymbolAtLocation(type.symbol, decl)) : - type && typeChecker.getPropertiesOfType(type); - }); - symbols = ts.concatenate(symbols, filterClassMembersList(baseSymbols, decl.members, classElementModifierFlags)); - } + // `import { |` or `import { a as 0, | }` or `import { type | }` + const namedImportsOrExports = contextToken.kind === ts.SyntaxKind.OpenBraceToken || contextToken.kind === ts.SyntaxKind.CommaToken ? ts.tryCast(contextToken.parent, ts.isNamedImportsOrExports) : + ts.isTypeKeywordTokenOrIdentifier(contextToken) ? ts.tryCast(contextToken.parent.parent, ts.isNamedImportsOrExports) : undefined; + if (!namedImportsOrExports) + return GlobalsSearch.Continue; - return GlobalsSearch.Success; + // We can at least offer `type` at `import { |` + if (!ts.isTypeKeywordTokenOrIdentifier(contextToken)) { + keywordFilters = KeywordCompletionFilters.TypeKeyword; } - function isConstructorParameterCompletion(node: ts.Node): boolean { - return !!node.parent && ts.isParameter(node.parent) && ts.isConstructorDeclaration(node.parent.parent) - && (ts.isParameterPropertyModifier(node.kind) || ts.isDeclarationName(node)); + // try to show exported member for imported/re-exported module + const { moduleSpecifier } = namedImportsOrExports.kind === ts.SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent; + if (!moduleSpecifier) { + isNewIdentifierLocation = true; + return namedImportsOrExports.kind === ts.SyntaxKind.NamedImports ? GlobalsSearch.Fail : GlobalsSearch.Continue; } - - /** - * 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: ts.Node): ts.ConstructorDeclaration | undefined { - if (contextToken) { - const parent = contextToken.parent; - switch (contextToken.kind) { - case ts.SyntaxKind.OpenParenToken: - case ts.SyntaxKind.CommaToken: - return ts.isConstructorDeclaration(contextToken.parent) ? contextToken.parent : undefined; - - default: - if (isConstructorParameterCompletion(contextToken)) { - return parent.parent as ts.ConstructorDeclaration; - } - } - } - return undefined; + const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier); // TODO: GH#18217 + if (!moduleSpecifierSymbol) { + isNewIdentifierLocation = true; + return GlobalsSearch.Fail; } - function tryGetFunctionLikeBodyCompletionContainer(contextToken: ts.Node): ts.FunctionLikeDeclaration | undefined { - if (contextToken) { - let prev: ts.Node; - const container = ts.findAncestor(contextToken.parent, (node: ts.Node) => { - if (ts.isClassLike(node)) { - return "quit"; - } - if (ts.isFunctionLikeDeclaration(node) && prev === node.body) { - return true; - } - prev = node; - return false; - }); - return container && container as ts.FunctionLikeDeclaration; - } + completionKind = CompletionKind.MemberLike; + isNewIdentifierLocation = false; + const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol); + const existing = new ts.Set((namedImportsOrExports.elements as ts.NodeArray).filter(n => !isCurrentlyEditingNode(n)).map(n => (n.propertyName || n.name).escapedText)); + const uniques = exports.filter(e => e.escapedName !== ts.InternalSymbolName.Default && !existing.has(e.escapedName)); + symbols = ts.concatenate(symbols, uniques); + if (!uniques.length) { + // If there's nothing else to import, don't offer `type` either + keywordFilters = KeywordCompletionFilters.None; } + return GlobalsSearch.Success; + } - function tryGetContainingJsxElement(contextToken: ts.Node): ts.JsxOpeningLikeElement | undefined { - if (contextToken) { - const parent = contextToken.parent; - switch (contextToken.kind) { - case ts.SyntaxKind.GreaterThanToken: // End of a type argument list - case ts.SyntaxKind.LessThanSlashToken: - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.JsxAttributes: - case ts.SyntaxKind.JsxAttribute: - case ts.SyntaxKind.JsxSpreadAttribute: - if (parent && (parent.kind === ts.SyntaxKind.JsxSelfClosingElement || parent.kind === ts.SyntaxKind.JsxOpeningElement)) { - if (contextToken.kind === ts.SyntaxKind.GreaterThanToken) { - const precedingToken = ts.findPrecedingToken(contextToken.pos, sourceFile, /*startNode*/ undefined); - if (!(parent as ts.JsxOpeningLikeElement).typeArguments || (precedingToken && precedingToken.kind === ts.SyntaxKind.SlashToken)) - break; - } - return parent as ts.JsxOpeningLikeElement; - } - else if (parent.kind === ts.SyntaxKind.JsxAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as ts.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 ts.SyntaxKind.StringLiteral: - if (parent && ((parent.kind === ts.SyntaxKind.JsxAttribute) || (parent.kind === ts.SyntaxKind.JsxSpreadAttribute))) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as ts.JsxOpeningLikeElement; - } - - break; - - case ts.SyntaxKind.CloseBraceToken: - if (parent && - parent.kind === ts.SyntaxKind.JsxExpression && - parent.parent && parent.parent.kind === ts.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 ts.JsxOpeningLikeElement; - } - - if (parent && parent.kind === ts.SyntaxKind.JsxSpreadAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as ts.JsxOpeningLikeElement; - } + /** + * Adds local declarations for completions in named exports: + * + * export { | }; + * + * Does not check for the absence of a module specifier (`export {} from "./other"`) + * because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, + * preventing this function from running. + */ + function tryGetLocalNamedExportCompletionSymbols(): GlobalsSearch { + const namedExports = contextToken && (contextToken.kind === ts.SyntaxKind.OpenBraceToken || contextToken.kind === ts.SyntaxKind.CommaToken) + ? ts.tryCast(contextToken.parent, ts.isNamedExports) + : undefined; - break; - } - } - return undefined; + if (!namedExports) { + return GlobalsSearch.Continue; } - /** - * @returns true if we are certain that the currently edited location must define a new location; false otherwise. - */ - function isSolelyIdentifierDefinitionLocation(contextToken: ts.Node): boolean { - const parent = contextToken.parent; - const containingNodeKind = parent.kind; - switch (contextToken.kind) { - case ts.SyntaxKind.CommaToken: - return containingNodeKind === ts.SyntaxKind.VariableDeclaration || - isVariableDeclarationListButNotTypeArgument(contextToken) || - containingNodeKind === ts.SyntaxKind.VariableStatement || - containingNodeKind === ts.SyntaxKind.EnumDeclaration || // enum a { foo, | - isFunctionLikeButNotConstructor(containingNodeKind) || - containingNodeKind === ts.SyntaxKind.InterfaceDeclaration || // interface A= contextToken.pos); - - case ts.SyntaxKind.DotToken: - return containingNodeKind === ts.SyntaxKind.ArrayBindingPattern; // var [.| - case ts.SyntaxKind.ColonToken: - return containingNodeKind === ts.SyntaxKind.BindingElement; // var {x :html| - case ts.SyntaxKind.OpenBracketToken: - return containingNodeKind === ts.SyntaxKind.ArrayBindingPattern; // var [x| - case ts.SyntaxKind.OpenParenToken: - return containingNodeKind === ts.SyntaxKind.CatchClause || - isFunctionLikeButNotConstructor(containingNodeKind); - - case ts.SyntaxKind.OpenBraceToken: - return containingNodeKind === ts.SyntaxKind.EnumDeclaration; // enum a { | - case ts.SyntaxKind.LessThanToken: - return containingNodeKind === ts.SyntaxKind.ClassDeclaration || // class A< | - containingNodeKind === ts.SyntaxKind.ClassExpression || // var C = class D< | - containingNodeKind === ts.SyntaxKind.InterfaceDeclaration || // interface A< | - containingNodeKind === ts.SyntaxKind.TypeAliasDeclaration || // type List< | - ts.isFunctionLikeKind(containingNodeKind); - case ts.SyntaxKind.StaticKeyword: - return containingNodeKind === ts.SyntaxKind.PropertyDeclaration && !ts.isClassLike(parent.parent); - case ts.SyntaxKind.DotDotDotToken: - return containingNodeKind === ts.SyntaxKind.Parameter || - (!!parent.parent && parent.parent.kind === ts.SyntaxKind.ArrayBindingPattern); // var [...z| - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - return containingNodeKind === ts.SyntaxKind.Parameter && !ts.isConstructorDeclaration(parent.parent); - case ts.SyntaxKind.AsKeyword: - return containingNodeKind === ts.SyntaxKind.ImportSpecifier || - containingNodeKind === ts.SyntaxKind.ExportSpecifier || - containingNodeKind === ts.SyntaxKind.NamespaceImport; - case ts.SyntaxKind.GetKeyword: - case ts.SyntaxKind.SetKeyword: - return !isFromObjectTypeDeclaration(contextToken); - - case ts.SyntaxKind.Identifier: - if (containingNodeKind === ts.SyntaxKind.ImportSpecifier && - contextToken === (parent as ts.ImportSpecifier).name && - (contextToken as ts.Identifier).text === "type") { - // import { type | } - return false; - } - break; - - case ts.SyntaxKind.ClassKeyword: - case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.VarKeyword: - case ts.SyntaxKind.ImportKeyword: - case ts.SyntaxKind.LetKeyword: - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.InferKeyword: - return true; - - case ts.SyntaxKind.TypeKeyword: - // import { type foo| } - return containingNodeKind !== ts.SyntaxKind.ImportSpecifier; - case ts.SyntaxKind.AsteriskToken: - return ts.isFunctionLike(contextToken.parent) && !ts.isMethodDeclaration(contextToken.parent); - } - - // If the previous token is keyword corresponding 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 (!ts.isIdentifier(contextToken) || - ts.isParameterPropertyModifier(keywordForNode(contextToken)) || - isCurrentlyEditingNode(contextToken)) { - return false; - } - } - - // Previous token may have been a keyword that was converted to an identifier. - switch (keywordForNode(contextToken)) { - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.ClassKeyword: - case ts.SyntaxKind.ConstKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.FunctionKeyword: - case ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.LetKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.StaticKeyword: - case ts.SyntaxKind.VarKeyword: - return true; - case ts.SyntaxKind.AsyncKeyword: - return ts.isPropertyDeclaration(contextToken.parent); - } - - // If we are inside a class declaration, and `constructor` is totally not present, - // but we request a completion manually at a whitespace... - const ancestorClassLike = ts.findAncestor(contextToken.parent, ts.isClassLike); - if (ancestorClassLike && contextToken === previousToken && isPreviousPropertyDeclarationTerminated(contextToken, position)) { - return false; // Don't block completions. + const localsContainer = ts.findAncestor(namedExports, ts.or(ts.isSourceFile, ts.isModuleDeclaration))!; + completionKind = CompletionKind.None; + isNewIdentifierLocation = false; + localsContainer.locals?.forEach((symbol, name) => { + symbols.push(symbol); + if (localsContainer.symbol?.exports?.has(name)) { + symbolToSortTextMap[ts.getSymbolId(symbol)] = SortText.OptionalMember; } + }); + return GlobalsSearch.Success; + } - const ancestorPropertyDeclaraion = ts.getAncestor(contextToken.parent, ts.SyntaxKind.PropertyDeclaration); - // If we are inside a class declaration and typing `constructor` after property declaration... - if (ancestorPropertyDeclaraion - && contextToken !== previousToken - && ts.isClassLike(previousToken.parent.parent) - // And the cursor is at the token... - && position <= previousToken.end) { - // If we are sure that the previous property declaration is terminated according to newline or semicolon... - if (isPreviousPropertyDeclarationTerminated(contextToken, previousToken.end)) { - return false; // Don't block completions. - } - else if (contextToken.kind !== ts.SyntaxKind.EqualsToken - // Should not block: `class C { blah = c/**/ }` - // But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }` - && (ts.isInitializedProperty(ancestorPropertyDeclaraion as ts.PropertyDeclaration) - || ts.hasType(ancestorPropertyDeclaraion))) { - return true; - } + /** + * 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 === ts.SyntaxKind.AsteriskToken ? KeywordCompletionFilters.None : + ts.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 (!ts.isClassLike(decl)) + return GlobalsSearch.Success; + const classElement = contextToken.kind === ts.SyntaxKind.SemicolonToken ? contextToken.parent.parent : contextToken.parent; + let classElementModifierFlags = ts.isClassElement(classElement) ? ts.getEffectiveModifierFlags(classElement) : ts.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 === ts.SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) { + switch (contextToken.getText()) { + case "private": + classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Private; + break; + case "static": + classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Static; + break; + case "override": + classElementModifierFlags = classElementModifierFlags | ts.ModifierFlags.Override; + break; } - - return ts.isDeclarationName(contextToken) - && !ts.isShorthandPropertyAssignment(contextToken.parent) - && !ts.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/**/`. - && !(ts.isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end)); } - - function isPreviousPropertyDeclarationTerminated(contextToken: ts.Node, position: number) { - return contextToken.kind !== ts.SyntaxKind.EqualsToken && - (contextToken.kind === ts.SyntaxKind.SemicolonToken - || !ts.positionsAreOnSameLine(contextToken.end, position, sourceFile)); + if (ts.isClassStaticBlockDeclaration(classElement)) { + classElementModifierFlags |= ts.ModifierFlags.Static; } - function isFunctionLikeButNotConstructor(kind: ts.SyntaxKind) { - return ts.isFunctionLikeKind(kind) && kind !== ts.SyntaxKind.Constructor; + // No member list for private methods + if (!(classElementModifierFlags & ts.ModifierFlags.Private)) { + // List of property symbols of base type that are not private and already implemented + const baseTypeNodes = ts.isClassLike(decl) && classElementModifierFlags & ts.ModifierFlags.Override ? ts.singleElementArray(ts.getEffectiveBaseTypeNode(decl)) : ts.getAllSuperTypeNodes(decl); + const baseSymbols = ts.flatMap(baseTypeNodes, baseTypeNode => { + const type = typeChecker.getTypeAtLocation(baseTypeNode); + return classElementModifierFlags & ts.ModifierFlags.Static ? + type?.symbol && typeChecker.getPropertiesOfType(typeChecker.getTypeOfSymbolAtLocation(type.symbol, decl)) : + type && typeChecker.getPropertiesOfType(type); + }); + symbols = ts.concatenate(symbols, filterClassMembersList(baseSymbols, decl.members, classElementModifierFlags)); } - function isDotOfNumericLiteral(contextToken: ts.Node): boolean { - if (contextToken.kind === ts.SyntaxKind.NumericLiteral) { - const text = contextToken.getFullText(); - return text.charAt(text.length - 1) === "."; - } - - return false; - } + return GlobalsSearch.Success; + } - function isVariableDeclarationListButNotTypeArgument(node: ts.Node): boolean { - return node.parent.kind === ts.SyntaxKind.VariableDeclarationList - && !ts.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: ts.Symbol[], existingMembers: readonly ts.Declaration[]): ts.Symbol[] { - if (existingMembers.length === 0) { - return contextualMemberSymbols; - } - - const membersDeclaredBySpreadAssignment = new ts.Set(); - const existingMemberNames = new ts.Set(); - for (const m of existingMembers) { - // Ignore omitted expressions for missing members - if (m.kind !== ts.SyntaxKind.PropertyAssignment && - m.kind !== ts.SyntaxKind.ShorthandPropertyAssignment && - m.kind !== ts.SyntaxKind.BindingElement && - m.kind !== ts.SyntaxKind.MethodDeclaration && - m.kind !== ts.SyntaxKind.GetAccessor && - m.kind !== ts.SyntaxKind.SetAccessor && - m.kind !== ts.SyntaxKind.SpreadAssignment) { - continue; - } + function isConstructorParameterCompletion(node: ts.Node): boolean { + return !!node.parent && ts.isParameter(node.parent) && ts.isConstructorDeclaration(node.parent.parent) + && (ts.isParameterPropertyModifier(node.kind) || ts.isDeclarationName(node)); + } - // If this is the current item we are editing right now, do not filter it out - if (isCurrentlyEditingNode(m)) { - continue; - } + /** + * 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: ts.Node): ts.ConstructorDeclaration | undefined { + if (contextToken) { + const parent = contextToken.parent; + switch (contextToken.kind) { + case ts.SyntaxKind.OpenParenToken: + case ts.SyntaxKind.CommaToken: + return ts.isConstructorDeclaration(contextToken.parent) ? contextToken.parent : undefined; - let existingName: ts.__String | undefined; - if (ts.isSpreadAssignment(m)) { - setMembersDeclaredBySpreadAssignment(m, membersDeclaredBySpreadAssignment); - } - else if (ts.isBindingElement(m) && m.propertyName) { - // include only identifiers in completion list - if (m.propertyName.kind === ts.SyntaxKind.Identifier) { - existingName = m.propertyName.escapedText; + default: + if (isConstructorParameterCompletion(contextToken)) { + return parent.parent as ts.ConstructorDeclaration; } - } - 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 = ts.getNameOfDeclaration(m); - existingName = name && ts.isPropertyNameLiteral(name) ? ts.getEscapedTextOfIdentifierOrLiteral(name) : undefined; - } - - if (existingName !== undefined) { - existingMemberNames.add(existingName); - } - } - - const filteredSymbols = contextualMemberSymbols.filter(m => !existingMemberNames.has(m.escapedName)); - setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - - return filteredSymbols; - } - - function setMembersDeclaredBySpreadAssignment(declaration: ts.SpreadAssignment | ts.JsxSpreadAttribute, membersDeclaredBySpreadAssignment: ts.Set) { - const expression = declaration.expression; - const symbol = typeChecker.getSymbolAtLocation(expression); - const type = symbol && typeChecker.getTypeOfSymbolAtLocation(symbol, expression); - const properties = type && (type as ts.ObjectType).properties; - if (properties) { - properties.forEach(property => { - membersDeclaredBySpreadAssignment.add(property.name); - }); } } + return undefined; + } - // Set SortText to OptionalMember if it is an optional member - function setSortTextToOptionalMember() { - symbols.forEach(m => { - if (m.flags & ts.SymbolFlags.Optional) { - const symbolId = ts.getSymbolId(m); - symbolToSortTextMap[symbolId] = symbolToSortTextMap[symbolId] ?? SortText.OptionalMember; - } - }); - } - - // Set SortText to MemberDeclaredBySpreadAssignment if it is fulfilled by spread assignment - function setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment: ts.Set, contextualMemberSymbols: ts.Symbol[]): void { - if (membersDeclaredBySpreadAssignment.size === 0) { - return; - } - for (const contextualMemberSymbol of contextualMemberSymbols) { - if (membersDeclaredBySpreadAssignment.has(contextualMemberSymbol.name)) { - symbolToSortTextMap[ts.getSymbolId(contextualMemberSymbol)] = SortText.MemberDeclaredBySpreadAssignment; + function tryGetFunctionLikeBodyCompletionContainer(contextToken: ts.Node): ts.FunctionLikeDeclaration | undefined { + if (contextToken) { + let prev: ts.Node; + const container = ts.findAncestor(contextToken.parent, (node: ts.Node) => { + if (ts.isClassLike(node)) { + return "quit"; } - } - } - - function transformObjectLiteralMembersSortText(start: number): void { - for (let i = start; i < symbols.length; i++) { - const symbol = symbols[i]; - const symbolId = ts.getSymbolId(symbol); - const origin = symbolToOriginInfoMap?.[i]; - const target = ts.getEmitScriptTarget(compilerOptions); - const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, CompletionKind.ObjectPropertyDeclaration, - /*jsxIdentifierExpected*/ false); - if (displayName) { - const originalSortText = symbolToSortTextMap[symbolId] ?? SortText.LocationPriority; - const { name } = displayName; - symbolToSortTextMap[symbolId] = SortText.ObjectLiteralProperty(originalSortText, name); + if (ts.isFunctionLikeDeclaration(node) && prev === node.body) { + return true; } - } + prev = node; + return false; + }); + return container && container as ts.FunctionLikeDeclaration; } + } - /** - * 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 ts.Symbol[], existingMembers: readonly ts.ClassElement[], currentClassElementModifierFlags: ts.ModifierFlags): ts.Symbol[] { - const existingMemberNames = new ts.Set(); - for (const m of existingMembers) { - // Ignore omitted expressions for missing members - if (m.kind !== ts.SyntaxKind.PropertyDeclaration && - m.kind !== ts.SyntaxKind.MethodDeclaration && - m.kind !== ts.SyntaxKind.GetAccessor && - m.kind !== ts.SyntaxKind.SetAccessor) { - continue; - } - - // If this is the current item we are editing right now, do not filter it out - if (isCurrentlyEditingNode(m)) { - continue; - } + function tryGetContainingJsxElement(contextToken: ts.Node): ts.JsxOpeningLikeElement | undefined { + if (contextToken) { + const parent = contextToken.parent; + switch (contextToken.kind) { + case ts.SyntaxKind.GreaterThanToken: // End of a type argument list + case ts.SyntaxKind.LessThanSlashToken: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.JsxAttributes: + case ts.SyntaxKind.JsxAttribute: + case ts.SyntaxKind.JsxSpreadAttribute: + if (parent && (parent.kind === ts.SyntaxKind.JsxSelfClosingElement || parent.kind === ts.SyntaxKind.JsxOpeningElement)) { + if (contextToken.kind === ts.SyntaxKind.GreaterThanToken) { + const precedingToken = ts.findPrecedingToken(contextToken.pos, sourceFile, /*startNode*/ undefined); + if (!(parent as ts.JsxOpeningLikeElement).typeArguments || (precedingToken && precedingToken.kind === ts.SyntaxKind.SlashToken)) + break; + } + return parent as ts.JsxOpeningLikeElement; + } + else if (parent.kind === ts.SyntaxKind.JsxAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as ts.JsxOpeningLikeElement; + } + break; - // Dont filter member even if the name matches if it is declared private in the list - if (ts.hasEffectiveModifier(m, ts.ModifierFlags.Private)) { - continue; - } + // 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 ts.SyntaxKind.StringLiteral: + if (parent && ((parent.kind === ts.SyntaxKind.JsxAttribute) || (parent.kind === ts.SyntaxKind.JsxSpreadAttribute))) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as ts.JsxOpeningLikeElement; + } - // do not filter it out if the static presence doesnt match - if (ts.isStatic(m) !== !!(currentClassElementModifierFlags & ts.ModifierFlags.Static)) { - continue; - } + break; - const existingName = ts.getPropertyNameForPropertyNameNode(m.name!); - if (existingName) { - existingMemberNames.add(existingName); - } - } + case ts.SyntaxKind.CloseBraceToken: + if (parent && + parent.kind === ts.SyntaxKind.JsxExpression && + parent.parent && parent.parent.kind === ts.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 ts.JsxOpeningLikeElement; + } - return baseSymbols.filter(propertySymbol => !existingMemberNames.has(propertySymbol.escapedName) && - !!propertySymbol.declarations && - !(ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & ts.ModifierFlags.Private) && - !(propertySymbol.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(propertySymbol.valueDeclaration))); - } - - /** - * 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: ts.Symbol[], attributes: ts.NodeArray): ts.Symbol[] { - const seenNames = new ts.Set(); - const membersDeclaredBySpreadAssignment = new ts.Set(); - 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 (parent && parent.kind === ts.SyntaxKind.JsxSpreadAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as ts.JsxOpeningLikeElement; + } - if (attr.kind === ts.SyntaxKind.JsxAttribute) { - seenNames.add(attr.name.escapedText); - } - else if (ts.isJsxSpreadAttribute(attr)) { - setMembersDeclaredBySpreadAssignment(attr, membersDeclaredBySpreadAssignment); - } + break; } - const filteredSymbols = symbols.filter(a => !seenNames.has(a.escapedName)); - - setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - - return filteredSymbols; - } - - function isCurrentlyEditingNode(node: ts.Node): boolean { - return node.getStart(sourceFile) <= position && position <= node.getEnd(); } + return undefined; } /** - * 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. + * @returns true if we are certain that the currently edited location must define a new location; false otherwise. */ - function tryGetObjectLikeCompletionContainer(contextToken: ts.Node | undefined): ts.ObjectLiteralExpression | ts.ObjectBindingPattern | undefined { - if (contextToken) { - const { parent } = contextToken; - switch (contextToken.kind) { - case ts.SyntaxKind.OpenBraceToken: // const x = { | - case ts.SyntaxKind.CommaToken: // const x = { a: 0, | - if (ts.isObjectLiteralExpression(parent) || ts.isObjectBindingPattern(parent)) { - return parent; - } - break; - case ts.SyntaxKind.AsteriskToken: - return ts.isMethodDeclaration(parent) ? ts.tryCast(parent.parent, ts.isObjectLiteralExpression) : undefined; - case ts.SyntaxKind.Identifier: - return (contextToken as ts.Identifier).text === "async" && ts.isShorthandPropertyAssignment(contextToken.parent) - ? contextToken.parent.parent : undefined; - } - } + function isSolelyIdentifierDefinitionLocation(contextToken: ts.Node): boolean { + const parent = contextToken.parent; + const containingNodeKind = parent.kind; + switch (contextToken.kind) { + case ts.SyntaxKind.CommaToken: + return containingNodeKind === ts.SyntaxKind.VariableDeclaration || + isVariableDeclarationListButNotTypeArgument(contextToken) || + containingNodeKind === ts.SyntaxKind.VariableStatement || + containingNodeKind === ts.SyntaxKind.EnumDeclaration || // enum a { foo, | + isFunctionLikeButNotConstructor(containingNodeKind) || + containingNodeKind === ts.SyntaxKind.InterfaceDeclaration || // interface A= contextToken.pos); + + case ts.SyntaxKind.DotToken: + return containingNodeKind === ts.SyntaxKind.ArrayBindingPattern; // var [.| + case ts.SyntaxKind.ColonToken: + return containingNodeKind === ts.SyntaxKind.BindingElement; // var {x :html| + case ts.SyntaxKind.OpenBracketToken: + return containingNodeKind === ts.SyntaxKind.ArrayBindingPattern; // var [x| + case ts.SyntaxKind.OpenParenToken: + return containingNodeKind === ts.SyntaxKind.CatchClause || + isFunctionLikeButNotConstructor(containingNodeKind); - return undefined; - } + case ts.SyntaxKind.OpenBraceToken: + return containingNodeKind === ts.SyntaxKind.EnumDeclaration; // enum a { | + case ts.SyntaxKind.LessThanToken: + return containingNodeKind === ts.SyntaxKind.ClassDeclaration || // class A< | + containingNodeKind === ts.SyntaxKind.ClassExpression || // var C = class D< | + containingNodeKind === ts.SyntaxKind.InterfaceDeclaration || // interface A< | + containingNodeKind === ts.SyntaxKind.TypeAliasDeclaration || // type List< | + ts.isFunctionLikeKind(containingNodeKind); + case ts.SyntaxKind.StaticKeyword: + return containingNodeKind === ts.SyntaxKind.PropertyDeclaration && !ts.isClassLike(parent.parent); + case ts.SyntaxKind.DotDotDotToken: + return containingNodeKind === ts.SyntaxKind.Parameter || + (!!parent.parent && parent.parent.kind === ts.SyntaxKind.ArrayBindingPattern); // var [...z| + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + return containingNodeKind === ts.SyntaxKind.Parameter && !ts.isConstructorDeclaration(parent.parent); + case ts.SyntaxKind.AsKeyword: + return containingNodeKind === ts.SyntaxKind.ImportSpecifier || + containingNodeKind === ts.SyntaxKind.ExportSpecifier || + containingNodeKind === ts.SyntaxKind.NamespaceImport; + case ts.SyntaxKind.GetKeyword: + case ts.SyntaxKind.SetKeyword: + return !isFromObjectTypeDeclaration(contextToken); - function getRelevantTokens(position: number, sourceFile: ts.SourceFile): { - contextToken: ts.Node; - previousToken: ts.Node; - } | { - contextToken: undefined; - previousToken: undefined; - } { - const previousToken = ts.findPrecedingToken(position, sourceFile); - if (previousToken && position <= previousToken.end && (ts.isMemberName(previousToken) || ts.isKeyword(previousToken.kind))) { - const contextToken = ts.findPrecedingToken(previousToken.getFullStart(), sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217 - return { contextToken, previousToken }; - } - return { contextToken: previousToken as ts.Node, previousToken: previousToken as ts.Node }; - } - - function getAutoImportSymbolFromCompletionEntryData(name: string, data: ts.CompletionEntryData, program: ts.Program, host: ts.LanguageServiceHost): { - symbol: ts.Symbol; - origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport; - } | undefined { - const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; - const checker = containingProgram.getTypeChecker(); - const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : - data.fileName ? checker.getMergedSymbol(ts.Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) : - undefined; - - if (!moduleSymbol) - return undefined; - let symbol = data.exportName === ts.InternalSymbolName.ExportEquals - ? checker.resolveExternalModuleSymbol(moduleSymbol) - : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); - if (!symbol) - return undefined; - const isDefaultExport = data.exportName === ts.InternalSymbolName.Default; - symbol = isDefaultExport && ts.getLocalSymbolForExportDefault(symbol) || symbol; - return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) }; - } - - interface CompletionEntryDisplayNameForSymbol { - readonly name: string; - readonly needsConvertPropertyAccess: boolean; - } - function getCompletionEntryDisplayNameForSymbol(symbol: ts.Symbol, target: ts.ScriptTarget, origin: SymbolOriginInfo | undefined, kind: CompletionKind, jsxIdentifierExpected: boolean): CompletionEntryDisplayNameForSymbol | undefined { - const name = originIncludesSymbolName(origin) ? origin.symbolName : 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 & ts.SymbolFlags.Module && ts.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 "__@" - || ts.isKnownSymbol(symbol)) { - return undefined; - } + case ts.SyntaxKind.Identifier: + if (containingNodeKind === ts.SyntaxKind.ImportSpecifier && + contextToken === (parent as ts.ImportSpecifier).name && + (contextToken as ts.Identifier).text === "type") { + // import { type | } + return false; + } + break; - const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; - if (ts.isIdentifierText(name, target, jsxIdentifierExpected ? ts.LanguageVariant.JSX : ts.LanguageVariant.Standard) || symbol.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(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) === ts.CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; - case CompletionKind.None: - case CompletionKind.String: - return validNameResult; - default: - ts.Debug.assertNever(kind); - } - } + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.FunctionKeyword: + case ts.SyntaxKind.VarKeyword: + case ts.SyntaxKind.ImportKeyword: + case ts.SyntaxKind.LetKeyword: + case ts.SyntaxKind.ConstKeyword: + case ts.SyntaxKind.InferKeyword: + return true; - // A cache of completion entries for keywords, these do not change between sessions - const _keywordCompletions: ts.CompletionEntry[][] = []; - const allKeywordsCompletions: () => readonly ts.CompletionEntry[] = ts.memoize(() => { - const res: ts.CompletionEntry[] = []; - for (let i = ts.SyntaxKind.FirstKeyword; i <= ts.SyntaxKind.LastKeyword; i++) { - res.push({ - name: ts.tokenToString(i)!, - kind: ts.ScriptElementKind.keyword, - kindModifiers: ts.ScriptElementKindModifier.none, - sortText: SortText.GlobalsOrKeywords - }); + case ts.SyntaxKind.TypeKeyword: + // import { type foo| } + return containingNodeKind !== ts.SyntaxKind.ImportSpecifier; + case ts.SyntaxKind.AsteriskToken: + return ts.isFunctionLike(contextToken.parent) && !ts.isMethodDeclaration(contextToken.parent); } - return res; - }); - - function getKeywordCompletions(keywordFilter: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean): readonly ts.CompletionEntry[] { - if (!filterOutTsOnlyKeywords) - return getTypescriptKeywordCompletions(keywordFilter); - const index = keywordFilter + KeywordCompletionFilters.Last + 1; - return _keywordCompletions[index] || - (_keywordCompletions[index] = getTypescriptKeywordCompletions(keywordFilter) - .filter(entry => !isTypeScriptOnlyKeyword(ts.stringToToken(entry.name)!))); - } + // If the previous token is keyword corresponding to class member completion keyword + // there will be completion available here + if (isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken)) { + return false; + } - function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters): readonly ts.CompletionEntry[] { - return _keywordCompletions[keywordFilter] || (_keywordCompletions[keywordFilter] = allKeywordsCompletions().filter(entry => { - const kind = ts.stringToToken(entry.name)!; - switch (keywordFilter) { - case KeywordCompletionFilters.None: - return false; - case KeywordCompletionFilters.All: - return isFunctionLikeBodyKeyword(kind) - || kind === ts.SyntaxKind.DeclareKeyword - || kind === ts.SyntaxKind.ModuleKeyword - || kind === ts.SyntaxKind.TypeKeyword - || kind === ts.SyntaxKind.NamespaceKeyword - || kind === ts.SyntaxKind.AbstractKeyword - || ts.isTypeKeyword(kind) && kind !== ts.SyntaxKind.UndefinedKeyword; - case KeywordCompletionFilters.FunctionLikeBodyKeywords: - return isFunctionLikeBodyKeyword(kind); - case KeywordCompletionFilters.ClassElementKeywords: - return isClassMemberCompletionKeyword(kind); - case KeywordCompletionFilters.InterfaceElementKeywords: - return isInterfaceOrTypeLiteralCompletionKeyword(kind); - case KeywordCompletionFilters.ConstructorParameterKeywords: - return ts.isParameterPropertyModifier(kind); - case KeywordCompletionFilters.TypeAssertionKeywords: - return ts.isTypeKeyword(kind) || kind === ts.SyntaxKind.ConstKeyword; - case KeywordCompletionFilters.TypeKeywords: - return ts.isTypeKeyword(kind); - case KeywordCompletionFilters.TypeKeyword: - return kind === ts.SyntaxKind.TypeKeyword; - default: - return ts.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 (!ts.isIdentifier(contextToken) || + ts.isParameterPropertyModifier(keywordForNode(contextToken)) || + isCurrentlyEditingNode(contextToken)) { + return false; } - })); - } + } - function isTypeScriptOnlyKeyword(kind: ts.SyntaxKind) { - switch (kind) { + // Previous token may have been a keyword that was converted to an identifier. + switch (keywordForNode(contextToken)) { case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.BigIntKeyword: - case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.ConstKeyword: case ts.SyntaxKind.DeclareKeyword: case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.GlobalKeyword: - case ts.SyntaxKind.ImplementsKeyword: - case ts.SyntaxKind.InferKeyword: + case ts.SyntaxKind.FunctionKeyword: case ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.IsKeyword: - case ts.SyntaxKind.KeyOfKeyword: - case ts.SyntaxKind.ModuleKeyword: - case ts.SyntaxKind.NamespaceKeyword: - case ts.SyntaxKind.NeverKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.OverrideKeyword: + case ts.SyntaxKind.LetKeyword: case ts.SyntaxKind.PrivateKeyword: case ts.SyntaxKind.ProtectedKeyword: case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.ReadonlyKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.SymbolKeyword: - case ts.SyntaxKind.TypeKeyword: - case ts.SyntaxKind.UniqueKeyword: - case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.StaticKeyword: + case ts.SyntaxKind.VarKeyword: return true; - default: - return false; + case ts.SyntaxKind.AsyncKeyword: + return ts.isPropertyDeclaration(contextToken.parent); } - } - function isInterfaceOrTypeLiteralCompletionKeyword(kind: ts.SyntaxKind): boolean { - return kind === ts.SyntaxKind.ReadonlyKeyword; - } + // If we are inside a class declaration, and `constructor` is totally not present, + // but we request a completion manually at a whitespace... + const ancestorClassLike = ts.findAncestor(contextToken.parent, ts.isClassLike); + if (ancestorClassLike && contextToken === previousToken && isPreviousPropertyDeclarationTerminated(contextToken, position)) { + return false; // Don't block completions. + } - function isClassMemberCompletionKeyword(kind: ts.SyntaxKind) { - switch (kind) { - case ts.SyntaxKind.AbstractKeyword: - case ts.SyntaxKind.ConstructorKeyword: - case ts.SyntaxKind.GetKeyword: - case ts.SyntaxKind.SetKeyword: - case ts.SyntaxKind.AsyncKeyword: - case ts.SyntaxKind.DeclareKeyword: - case ts.SyntaxKind.OverrideKeyword: + const ancestorPropertyDeclaraion = ts.getAncestor(contextToken.parent, ts.SyntaxKind.PropertyDeclaration); + // If we are inside a class declaration and typing `constructor` after property declaration... + if (ancestorPropertyDeclaraion + && contextToken !== previousToken + && ts.isClassLike(previousToken.parent.parent) + // And the cursor is at the token... + && position <= previousToken.end) { + // If we are sure that the previous property declaration is terminated according to newline or semicolon... + if (isPreviousPropertyDeclarationTerminated(contextToken, previousToken.end)) { + return false; // Don't block completions. + } + else if (contextToken.kind !== ts.SyntaxKind.EqualsToken + // Should not block: `class C { blah = c/**/ }` + // But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }` + && (ts.isInitializedProperty(ancestorPropertyDeclaraion as ts.PropertyDeclaration) + || ts.hasType(ancestorPropertyDeclaraion))) { return true; - default: - return ts.isClassMemberModifier(kind); + } } + + return ts.isDeclarationName(contextToken) + && !ts.isShorthandPropertyAssignment(contextToken.parent) + && !ts.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/**/`. + && !(ts.isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end)); } - function isFunctionLikeBodyKeyword(kind: ts.SyntaxKind) { - return kind === ts.SyntaxKind.AsyncKeyword - || kind === ts.SyntaxKind.AwaitKeyword - || kind === ts.SyntaxKind.AsKeyword - || !ts.isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind); + function isPreviousPropertyDeclarationTerminated(contextToken: ts.Node, position: number) { + return contextToken.kind !== ts.SyntaxKind.EqualsToken && + (contextToken.kind === ts.SyntaxKind.SemicolonToken + || !ts.positionsAreOnSameLine(contextToken.end, position, sourceFile)); } - function keywordForNode(node: ts.Node): ts.SyntaxKind { - return ts.isIdentifier(node) ? node.originalKeywordKind || ts.SyntaxKind.Unknown : node.kind; + function isFunctionLikeButNotConstructor(kind: ts.SyntaxKind) { + return ts.isFunctionLikeKind(kind) && kind !== ts.SyntaxKind.Constructor; } - function getContextualKeywords(contextToken: ts.Node | undefined, position: number): readonly ts.CompletionEntry[] { - const entries = []; - /** - * An `AssertClause` can come after an import declaration: - * import * from "foo" | - * import "foo" | - * or after a re-export declaration that has a module specifier: - * export { foo } from "foo" | - * Source: https://tc39.es/proposal-import-assertions/ - */ - if (contextToken) { - const file = contextToken.getSourceFile(); - const parent = contextToken.parent; - const tokenLine = file.getLineAndCharacterOfPosition(contextToken.end).line; - const currentLine = file.getLineAndCharacterOfPosition(position).line; - if ((ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent) && parent.moduleSpecifier) - && contextToken === parent.moduleSpecifier - && tokenLine === currentLine) { - entries.push({ - name: ts.tokenToString(ts.SyntaxKind.AssertKeyword)!, - kind: ts.ScriptElementKind.keyword, - kindModifiers: ts.ScriptElementKindModifier.none, - sortText: SortText.GlobalsOrKeywords, - }); - } + function isDotOfNumericLiteral(contextToken: ts.Node): boolean { + if (contextToken.kind === ts.SyntaxKind.NumericLiteral) { + const text = contextToken.getFullText(); + return text.charAt(text.length - 1) === "."; } - return entries; + + return false; } - /** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ - function getJsDocTagAtPosition(node: ts.Node, position: number): ts.JSDocTag | undefined { - return ts.findAncestor(node, n => ts.isJSDocTag(n) && ts.rangeContainsPosition(n, position) ? true : - ts.isJSDoc(n) ? "quit" : false) as ts.JSDocTag | undefined; + function isVariableDeclarationListButNotTypeArgument(node: ts.Node): boolean { + return node.parent.kind === ts.SyntaxKind.VariableDeclarationList + && !ts.isPossiblyTypeArgumentPosition(node, sourceFile, typeChecker); } - export function getPropertiesForObjectExpression(contextualType: ts.Type, completionsType: ts.Type | undefined, obj: ts.ObjectLiteralExpression | ts.JsxAttributes, checker: ts.TypeChecker): ts.Symbol[] { - const hasCompletionsType = completionsType && completionsType !== contextualType; - const type = hasCompletionsType && !(completionsType.flags & ts.TypeFlags.AnyOrUnknown) - ? checker.getUnionType([contextualType, completionsType]) - : contextualType; + /** + * 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: ts.Symbol[], existingMembers: readonly ts.Declaration[]): ts.Symbol[] { + if (existingMembers.length === 0) { + return contextualMemberSymbols; + } + + const membersDeclaredBySpreadAssignment = new ts.Set(); + const existingMemberNames = new ts.Set(); + for (const m of existingMembers) { + // Ignore omitted expressions for missing members + if (m.kind !== ts.SyntaxKind.PropertyAssignment && + m.kind !== ts.SyntaxKind.ShorthandPropertyAssignment && + m.kind !== ts.SyntaxKind.BindingElement && + m.kind !== ts.SyntaxKind.MethodDeclaration && + m.kind !== ts.SyntaxKind.GetAccessor && + m.kind !== ts.SyntaxKind.SetAccessor && + m.kind !== ts.SyntaxKind.SpreadAssignment) { + continue; + } - const properties = getApparentProperties(type, obj, checker); - return type.isClass() && containsNonPublicProperties(properties) ? [] : - hasCompletionsType ? ts.filter(properties, hasDeclarationOtherThanSelf) : properties; + // If this is the current item we are editing right now, do not filter it out + if (isCurrentlyEditingNode(m)) { + continue; + } - // 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: ts.Symbol) { - if (!ts.length(member.declarations)) - return true; - return ts.some(member.declarations, decl => decl.parent !== obj); + let existingName: ts.__String | undefined; + if (ts.isSpreadAssignment(m)) { + setMembersDeclaredBySpreadAssignment(m, membersDeclaredBySpreadAssignment); + } + else if (ts.isBindingElement(m) && m.propertyName) { + // include only identifiers in completion list + if (m.propertyName.kind === ts.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 = ts.getNameOfDeclaration(m); + existingName = name && ts.isPropertyNameLiteral(name) ? ts.getEscapedTextOfIdentifierOrLiteral(name) : undefined; + } + + if (existingName !== undefined) { + existingMemberNames.add(existingName); + } } + + const filteredSymbols = contextualMemberSymbols.filter(m => !existingMemberNames.has(m.escapedName)); + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); + + return filteredSymbols; } - function getApparentProperties(type: ts.Type, node: ts.ObjectLiteralExpression | ts.JsxAttributes, checker: ts.TypeChecker) { - if (!type.isUnion()) - return type.getApparentProperties(); - return checker.getAllPossiblePropertiesOfTypes(ts.filter(type.types, memberType => !(memberType.flags & ts.TypeFlags.Primitive - || checker.isArrayLikeType(memberType) - || checker.isTypeInvalidDueToUnionDiscriminant(memberType, node) - || ts.typeHasCallOrConstructSignatures(memberType, checker) - || memberType.isClass() && containsNonPublicProperties(memberType.getApparentProperties())))); + function setMembersDeclaredBySpreadAssignment(declaration: ts.SpreadAssignment | ts.JsxSpreadAttribute, membersDeclaredBySpreadAssignment: ts.Set) { + const expression = declaration.expression; + const symbol = typeChecker.getSymbolAtLocation(expression); + const type = symbol && typeChecker.getTypeOfSymbolAtLocation(symbol, expression); + const properties = type && (type as ts.ObjectType).properties; + if (properties) { + properties.forEach(property => { + membersDeclaredBySpreadAssignment.add(property.name); + }); + } } - function containsNonPublicProperties(props: ts.Symbol[]) { - return ts.some(props, p => !!(ts.getDeclarationModifierFlagsFromSymbol(p) & ts.ModifierFlags.NonPublicAccessibilityModifier)); + // Set SortText to OptionalMember if it is an optional member + function setSortTextToOptionalMember() { + symbols.forEach(m => { + if (m.flags & ts.SymbolFlags.Optional) { + const symbolId = ts.getSymbolId(m); + symbolToSortTextMap[symbolId] = symbolToSortTextMap[symbolId] ?? SortText.OptionalMember; + } + }); } - /** - * Gets all properties on a type, but if that type is a union of several types, - * excludes array-like types or callable/constructable types. - */ - function getPropertiesForCompletion(type: ts.Type, checker: ts.TypeChecker): ts.Symbol[] { - return type.isUnion() - ? ts.Debug.checkEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined") - : ts.Debug.checkEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined"); + // Set SortText to MemberDeclaredBySpreadAssignment if it is fulfilled by spread assignment + function setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment: ts.Set, contextualMemberSymbols: ts.Symbol[]): void { + if (membersDeclaredBySpreadAssignment.size === 0) { + return; + } + for (const contextualMemberSymbol of contextualMemberSymbols) { + if (membersDeclaredBySpreadAssignment.has(contextualMemberSymbol.name)) { + symbolToSortTextMap[ts.getSymbolId(contextualMemberSymbol)] = SortText.MemberDeclaredBySpreadAssignment; + } + } + } + + function transformObjectLiteralMembersSortText(start: number): void { + for (let i = start; i < symbols.length; i++) { + const symbol = symbols[i]; + const symbolId = ts.getSymbolId(symbol); + const origin = symbolToOriginInfoMap?.[i]; + const target = ts.getEmitScriptTarget(compilerOptions); + const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, CompletionKind.ObjectPropertyDeclaration, + /*jsxIdentifierExpected*/ false); + if (displayName) { + const originalSortText = symbolToSortTextMap[symbolId] ?? SortText.LocationPriority; + const { name } = displayName; + symbolToSortTextMap[symbolId] = SortText.ObjectLiteralProperty(originalSortText, name); + } + } } /** - * 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: ts.SourceFile, contextToken: ts.Node | undefined, location: ts.Node, position: number): ts.ObjectTypeDeclaration | undefined { - // class c { method() { } | method2() { } } - switch (location.kind) { - case ts.SyntaxKind.SyntaxList: - return ts.tryCast(location.parent, ts.isObjectTypeDeclaration); - case ts.SyntaxKind.EndOfFileToken: - const cls = ts.tryCast(ts.lastOrUndefined(ts.cast(location.parent, ts.isSourceFile).statements), ts.isObjectTypeDeclaration); - if (cls && !ts.findChildOfKind(cls, ts.SyntaxKind.CloseBraceToken, sourceFile)) { - return cls; - } - break; - case ts.SyntaxKind.Identifier: { - // class c { public prop = c| } - if (ts.isPropertyDeclaration(location.parent) && location.parent.initializer === location) { - return undefined; - } - // class c extends React.Component { a: () => 1\n compon| } - if (isFromObjectTypeDeclaration(location)) { - return ts.findAncestor(location, ts.isObjectTypeDeclaration); - } + function filterClassMembersList(baseSymbols: readonly ts.Symbol[], existingMembers: readonly ts.ClassElement[], currentClassElementModifierFlags: ts.ModifierFlags): ts.Symbol[] { + const existingMemberNames = new ts.Set(); + for (const m of existingMembers) { + // Ignore omitted expressions for missing members + if (m.kind !== ts.SyntaxKind.PropertyDeclaration && + m.kind !== ts.SyntaxKind.MethodDeclaration && + m.kind !== ts.SyntaxKind.GetAccessor && + m.kind !== ts.SyntaxKind.SetAccessor) { + continue; } - } - if (!contextToken) - return undefined; + // If this is the current item we are editing right now, do not filter it out + if (isCurrentlyEditingNode(m)) { + continue; + } - // class C { blah; constructor/**/ } and so on - if (location.kind === ts.SyntaxKind.ConstructorKeyword - // class C { blah \n constructor/**/ } - || (ts.isIdentifier(contextToken) && ts.isPropertyDeclaration(contextToken.parent) && ts.isClassLike(location))) { - return ts.findAncestor(contextToken, ts.isClassLike) as ts.ObjectTypeDeclaration; - } + // Dont filter member even if the name matches if it is declared private in the list + if (ts.hasEffectiveModifier(m, ts.ModifierFlags.Private)) { + continue; + } - switch (contextToken.kind) { - case ts.SyntaxKind.EqualsToken: // class c { public prop = | /* global completions */ } - return undefined; + // do not filter it out if the static presence doesnt match + if (ts.isStatic(m) !== !!(currentClassElementModifierFlags & ts.ModifierFlags.Static)) { + continue; + } - case ts.SyntaxKind.SemicolonToken: // class c {getValue(): number; | } - case ts.SyntaxKind.CloseBraceToken: // class c { method() { } | } - // class c { method() { } b| } - return isFromObjectTypeDeclaration(location) && (location.parent as ts.ClassElement | ts.TypeElement).name === location - ? location.parent.parent as ts.ObjectTypeDeclaration - : ts.tryCast(location, ts.isObjectTypeDeclaration); - case ts.SyntaxKind.OpenBraceToken: // class c { | - case ts.SyntaxKind.CommaToken: // class c {getValue(): number, | } - return ts.tryCast(contextToken.parent, ts.isObjectTypeDeclaration); - default: - if (!isFromObjectTypeDeclaration(contextToken)) { - // class c extends React.Component { a: () => 1\n| } - if (ts.getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== ts.getLineAndCharacterOfPosition(sourceFile, position).line && ts.isObjectTypeDeclaration(location)) { - return location; - } - return undefined; - } - const isValidKeyword = ts.isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword; - return (isValidKeyword(contextToken.kind) || contextToken.kind === ts.SyntaxKind.AsteriskToken || ts.isIdentifier(contextToken) && isValidKeyword(ts.stringToToken(contextToken.text)!)) // TODO: GH#18217 - ? contextToken.parent.parent as ts.ObjectTypeDeclaration : undefined; + const existingName = ts.getPropertyNameForPropertyNameNode(m.name!); + if (existingName) { + existingMemberNames.add(existingName); + } } + + return baseSymbols.filter(propertySymbol => !existingMemberNames.has(propertySymbol.escapedName) && + !!propertySymbol.declarations && + !(ts.getDeclarationModifierFlagsFromSymbol(propertySymbol) & ts.ModifierFlags.Private) && + !(propertySymbol.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(propertySymbol.valueDeclaration))); } - function tryGetTypeLiteralNode(node: ts.Node): ts.TypeLiteralNode | undefined { - if (!node) - return undefined; + /** + * 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: ts.Symbol[], attributes: ts.NodeArray): ts.Symbol[] { + const seenNames = new ts.Set(); + const membersDeclaredBySpreadAssignment = new ts.Set(); + 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 === ts.SyntaxKind.JsxAttribute) { + seenNames.add(attr.name.escapedText); + } + else if (ts.isJsxSpreadAttribute(attr)) { + setMembersDeclaredBySpreadAssignment(attr, membersDeclaredBySpreadAssignment); + } + } + const filteredSymbols = symbols.filter(a => !seenNames.has(a.escapedName)); - const parent = node.parent; + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - switch (node.kind) { - case ts.SyntaxKind.OpenBraceToken: - if (ts.isTypeLiteralNode(parent)) { + return filteredSymbols; + } + + function isCurrentlyEditingNode(node: ts.Node): boolean { + return node.getStart(sourceFile) <= position && position <= node.getEnd(); + } +} + +/** + * 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: ts.Node | undefined): ts.ObjectLiteralExpression | ts.ObjectBindingPattern | undefined { + if (contextToken) { + const { parent } = contextToken; + switch (contextToken.kind) { + case ts.SyntaxKind.OpenBraceToken: // const x = { | + case ts.SyntaxKind.CommaToken: // const x = { a: 0, | + if (ts.isObjectLiteralExpression(parent) || ts.isObjectBindingPattern(parent)) { return parent; } break; - case ts.SyntaxKind.SemicolonToken: - case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.AsteriskToken: + return ts.isMethodDeclaration(parent) ? ts.tryCast(parent.parent, ts.isObjectLiteralExpression) : undefined; case ts.SyntaxKind.Identifier: - if (parent.kind === ts.SyntaxKind.PropertySignature && ts.isTypeLiteralNode(parent.parent)) { - return parent.parent; - } - break; + return (contextToken as ts.Identifier).text === "async" && ts.isShorthandPropertyAssignment(contextToken.parent) + ? contextToken.parent.parent : undefined; } + } + + return undefined; +} +function getRelevantTokens(position: number, sourceFile: ts.SourceFile): { + contextToken: ts.Node; + previousToken: ts.Node; +} | { + contextToken: undefined; + previousToken: undefined; +} { + const previousToken = ts.findPrecedingToken(position, sourceFile); + if (previousToken && position <= previousToken.end && (ts.isMemberName(previousToken) || ts.isKeyword(previousToken.kind))) { + const contextToken = ts.findPrecedingToken(previousToken.getFullStart(), sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217 + return { contextToken, previousToken }; + } + return { contextToken: previousToken as ts.Node, previousToken: previousToken as ts.Node }; +} + +function getAutoImportSymbolFromCompletionEntryData(name: string, data: ts.CompletionEntryData, program: ts.Program, host: ts.LanguageServiceHost): { + symbol: ts.Symbol; + origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport; +} | undefined { + const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; + const checker = containingProgram.getTypeChecker(); + const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : + data.fileName ? checker.getMergedSymbol(ts.Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) : + undefined; + + if (!moduleSymbol) return undefined; - } + let symbol = data.exportName === ts.InternalSymbolName.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); + if (!symbol) + return undefined; + const isDefaultExport = data.exportName === ts.InternalSymbolName.Default; + symbol = isDefaultExport && ts.getLocalSymbolForExportDefault(symbol) || symbol; + return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) }; +} - function getConstraintOfTypeArgumentProperty(node: ts.Node, checker: ts.TypeChecker): ts.Type | undefined { - if (!node) - return undefined; - if (ts.isTypeNode(node) && ts.isTypeReferenceType(node.parent)) { - return checker.getTypeArgumentConstraint(node); - } +interface CompletionEntryDisplayNameForSymbol { + readonly name: string; + readonly needsConvertPropertyAccess: boolean; +} +function getCompletionEntryDisplayNameForSymbol(symbol: ts.Symbol, target: ts.ScriptTarget, origin: SymbolOriginInfo | undefined, kind: CompletionKind, jsxIdentifierExpected: boolean): CompletionEntryDisplayNameForSymbol | undefined { + const name = originIncludesSymbolName(origin) ? origin.symbolName : 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 & ts.SymbolFlags.Module && ts.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 "__@" + || ts.isKnownSymbol(symbol)) { + return undefined; + } - const t = getConstraintOfTypeArgumentProperty(node.parent, checker); - if (!t) + const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; + if (ts.isIdentifierText(name, target, jsxIdentifierExpected ? ts.LanguageVariant.JSX : ts.LanguageVariant.Standard) || symbol.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(symbol.valueDeclaration)) { + return validNameResult; + } + switch (kind) { + case CompletionKind.MemberLike: return undefined; - - switch (node.kind) { - case ts.SyntaxKind.PropertySignature: - return checker.getTypeOfPropertyOfContextualType(t, node.symbol.escapedName); - case ts.SyntaxKind.IntersectionType: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.UnionType: - return t; - } + 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) === ts.CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; + case CompletionKind.None: + case CompletionKind.String: + return validNameResult; + default: + ts.Debug.assertNever(kind); } +} - // TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes - function isFromObjectTypeDeclaration(node: ts.Node): boolean { - return node.parent && ts.isClassOrTypeElement(node.parent) && ts.isObjectTypeDeclaration(node.parent.parent); +// A cache of completion entries for keywords, these do not change between sessions +const _keywordCompletions: ts.CompletionEntry[][] = []; +const allKeywordsCompletions: () => readonly ts.CompletionEntry[] = ts.memoize(() => { + const res: ts.CompletionEntry[] = []; + for (let i = ts.SyntaxKind.FirstKeyword; i <= ts.SyntaxKind.LastKeyword; i++) { + res.push({ + name: ts.tokenToString(i)!, + kind: ts.ScriptElementKind.keyword, + kindModifiers: ts.ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords + }); } + return res; +}); - function isValidTrigger(sourceFile: ts.SourceFile, triggerCharacter: ts.CompletionsTriggerCharacter, contextToken: ts.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 && ts.isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; - case "#": - return !!contextToken && ts.isPrivateIdentifier(contextToken) && !!ts.getContainingClass(contextToken); - case "<": - // Opening JSX tag - return !!contextToken && contextToken.kind === ts.SyntaxKind.LessThanToken && (!ts.isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent)); - case "/": - return !!contextToken && (ts.isStringLiteralLike(contextToken) - ? !!ts.tryGetImportFromModuleSpecifier(contextToken) - : contextToken.kind === ts.SyntaxKind.SlashToken && ts.isJsxClosingElement(contextToken.parent)); - case " ": - return !!contextToken && ts.isImportKeyword(contextToken) && contextToken.parent.kind === ts.SyntaxKind.SourceFile; +function getKeywordCompletions(keywordFilter: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean): readonly ts.CompletionEntry[] { + if (!filterOutTsOnlyKeywords) + return getTypescriptKeywordCompletions(keywordFilter); + + const index = keywordFilter + KeywordCompletionFilters.Last + 1; + return _keywordCompletions[index] || + (_keywordCompletions[index] = getTypescriptKeywordCompletions(keywordFilter) + .filter(entry => !isTypeScriptOnlyKeyword(ts.stringToToken(entry.name)!))); +} + +function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters): readonly ts.CompletionEntry[] { + return _keywordCompletions[keywordFilter] || (_keywordCompletions[keywordFilter] = allKeywordsCompletions().filter(entry => { + const kind = ts.stringToToken(entry.name)!; + switch (keywordFilter) { + case KeywordCompletionFilters.None: + return false; + case KeywordCompletionFilters.All: + return isFunctionLikeBodyKeyword(kind) + || kind === ts.SyntaxKind.DeclareKeyword + || kind === ts.SyntaxKind.ModuleKeyword + || kind === ts.SyntaxKind.TypeKeyword + || kind === ts.SyntaxKind.NamespaceKeyword + || kind === ts.SyntaxKind.AbstractKeyword + || ts.isTypeKeyword(kind) && kind !== ts.SyntaxKind.UndefinedKeyword; + case KeywordCompletionFilters.FunctionLikeBodyKeywords: + return isFunctionLikeBodyKeyword(kind); + case KeywordCompletionFilters.ClassElementKeywords: + return isClassMemberCompletionKeyword(kind); + case KeywordCompletionFilters.InterfaceElementKeywords: + return isInterfaceOrTypeLiteralCompletionKeyword(kind); + case KeywordCompletionFilters.ConstructorParameterKeywords: + return ts.isParameterPropertyModifier(kind); + case KeywordCompletionFilters.TypeAssertionKeywords: + return ts.isTypeKeyword(kind) || kind === ts.SyntaxKind.ConstKeyword; + case KeywordCompletionFilters.TypeKeywords: + return ts.isTypeKeyword(kind); + case KeywordCompletionFilters.TypeKeyword: + return kind === ts.SyntaxKind.TypeKeyword; default: - return ts.Debug.assertNever(triggerCharacter); + return ts.Debug.assertNever(keywordFilter); } - } + })); +} - function binaryExpressionMayBeOpenTag({ left }: ts.BinaryExpression): boolean { - return ts.nodeIsMissing(left); +function isTypeScriptOnlyKeyword(kind: ts.SyntaxKind) { + switch (kind) { + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.AnyKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.GlobalKeyword: + case ts.SyntaxKind.ImplementsKeyword: + case ts.SyntaxKind.InferKeyword: + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.IsKeyword: + case ts.SyntaxKind.KeyOfKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.NeverKeyword: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.OverrideKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.ReadonlyKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.SymbolKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.UniqueKeyword: + case ts.SyntaxKind.UnknownKeyword: + return true; + default: + return false; } +} - /** Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. */ - function isProbablyGlobalType(type: ts.Type, sourceFile: ts.SourceFile, checker: ts.TypeChecker) { - // The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in - // lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists. - const selfSymbol = checker.resolveName("self", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); - if (selfSymbol && checker.getTypeOfSymbolAtLocation(selfSymbol, sourceFile) === type) { - return true; - } - const globalSymbol = checker.resolveName("global", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); - if (globalSymbol && checker.getTypeOfSymbolAtLocation(globalSymbol, sourceFile) === type) { - return true; - } - const globalThisSymbol = checker.resolveName("globalThis", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); - if (globalThisSymbol && checker.getTypeOfSymbolAtLocation(globalThisSymbol, sourceFile) === type) { +function isInterfaceOrTypeLiteralCompletionKeyword(kind: ts.SyntaxKind): boolean { + return kind === ts.SyntaxKind.ReadonlyKeyword; +} + +function isClassMemberCompletionKeyword(kind: ts.SyntaxKind) { + switch (kind) { + case ts.SyntaxKind.AbstractKeyword: + case ts.SyntaxKind.ConstructorKeyword: + case ts.SyntaxKind.GetKeyword: + case ts.SyntaxKind.SetKeyword: + case ts.SyntaxKind.AsyncKeyword: + case ts.SyntaxKind.DeclareKeyword: + case ts.SyntaxKind.OverrideKeyword: return true; - } - return false; + default: + return ts.isClassMemberModifier(kind); } +} - function isStaticProperty(symbol: ts.Symbol) { - return !!(symbol.valueDeclaration && ts.getEffectiveModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Static && ts.isClassLike(symbol.valueDeclaration.parent)); - } +function isFunctionLikeBodyKeyword(kind: ts.SyntaxKind) { + return kind === ts.SyntaxKind.AsyncKeyword + || kind === ts.SyntaxKind.AwaitKeyword + || kind === ts.SyntaxKind.AsKeyword + || !ts.isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind); +} - function tryGetObjectLiteralContextualType(node: ts.ObjectLiteralExpression, typeChecker: ts.TypeChecker) { - const type = typeChecker.getContextualType(node); - if (type) { - return type; - } - const parent = ts.walkUpParenthesizedExpressions(node.parent); - if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && node === parent.left) { - // Object literal is assignment pattern: ({ | } = x) - return typeChecker.getTypeAtLocation(parent); - } - if (ts.isExpression(parent)) { - // f(() => (({ | }))); - return typeChecker.getContextualType(parent); +function keywordForNode(node: ts.Node): ts.SyntaxKind { + return ts.isIdentifier(node) ? node.originalKeywordKind || ts.SyntaxKind.Unknown : node.kind; +} + +function getContextualKeywords(contextToken: ts.Node | undefined, position: number): readonly ts.CompletionEntry[] { + const entries = []; + /** + * An `AssertClause` can come after an import declaration: + * import * from "foo" | + * import "foo" | + * or after a re-export declaration that has a module specifier: + * export { foo } from "foo" | + * Source: https://tc39.es/proposal-import-assertions/ + */ + if (contextToken) { + const file = contextToken.getSourceFile(); + const parent = contextToken.parent; + const tokenLine = file.getLineAndCharacterOfPosition(contextToken.end).line; + const currentLine = file.getLineAndCharacterOfPosition(position).line; + if ((ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent) && parent.moduleSpecifier) + && contextToken === parent.moduleSpecifier + && tokenLine === currentLine) { + entries.push({ + name: ts.tokenToString(ts.SyntaxKind.AssertKeyword)!, + kind: ts.ScriptElementKind.keyword, + kindModifiers: ts.ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords, + }); } - return undefined; } + return entries; +} - interface ImportStatementCompletionInfo { - isKeywordOnlyCompletion: boolean; - keywordCompletion: ts.TokenSyntaxKind | undefined; - isNewIdentifierLocation: boolean; - replacementNode: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ImportSpecifier | ts.Token | undefined; - } - - function getImportStatementCompletionInfo(contextToken: ts.Node): ImportStatementCompletionInfo { - let keywordCompletion: ts.TokenSyntaxKind | undefined; - let isKeywordOnlyCompletion = false; - const candidate = getCandidate(); - return { - isKeywordOnlyCompletion, - keywordCompletion, - isNewIdentifierLocation: !!(candidate || keywordCompletion === ts.SyntaxKind.TypeKeyword), - replacementNode: candidate && ts.rangeIsOnSingleLine(candidate, candidate.getSourceFile()) - ? candidate - : undefined - }; +/** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ +function getJsDocTagAtPosition(node: ts.Node, position: number): ts.JSDocTag | undefined { + return ts.findAncestor(node, n => ts.isJSDocTag(n) && ts.rangeContainsPosition(n, position) ? true : + ts.isJSDoc(n) ? "quit" : false) as ts.JSDocTag | undefined; +} - function getCandidate() { - const parent = contextToken.parent; - if (ts.isImportEqualsDeclaration(parent)) { - keywordCompletion = contextToken.kind === ts.SyntaxKind.TypeKeyword ? undefined : ts.SyntaxKind.TypeKeyword; - return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; +export function getPropertiesForObjectExpression(contextualType: ts.Type, completionsType: ts.Type | undefined, obj: ts.ObjectLiteralExpression | ts.JsxAttributes, checker: ts.TypeChecker): ts.Symbol[] { + const hasCompletionsType = completionsType && completionsType !== contextualType; + const type = hasCompletionsType && !(completionsType.flags & ts.TypeFlags.AnyOrUnknown) + ? checker.getUnionType([contextualType, completionsType]) + : contextualType; + + const properties = getApparentProperties(type, obj, checker); + return type.isClass() && containsNonPublicProperties(properties) ? [] : + hasCompletionsType ? ts.filter(properties, 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: ts.Symbol) { + if (!ts.length(member.declarations)) + return true; + return ts.some(member.declarations, decl => decl.parent !== obj); + } +} + +function getApparentProperties(type: ts.Type, node: ts.ObjectLiteralExpression | ts.JsxAttributes, checker: ts.TypeChecker) { + if (!type.isUnion()) + return type.getApparentProperties(); + return checker.getAllPossiblePropertiesOfTypes(ts.filter(type.types, memberType => !(memberType.flags & ts.TypeFlags.Primitive + || checker.isArrayLikeType(memberType) + || checker.isTypeInvalidDueToUnionDiscriminant(memberType, node) + || ts.typeHasCallOrConstructSignatures(memberType, checker) + || memberType.isClass() && containsNonPublicProperties(memberType.getApparentProperties())))); +} + +function containsNonPublicProperties(props: ts.Symbol[]) { + return ts.some(props, p => !!(ts.getDeclarationModifierFlagsFromSymbol(p) & ts.ModifierFlags.NonPublicAccessibilityModifier)); +} + +/** + * Gets all properties on a type, but if that type is a union of several types, + * excludes array-like types or callable/constructable types. + */ +function getPropertiesForCompletion(type: ts.Type, checker: ts.TypeChecker): ts.Symbol[] { + return type.isUnion() + ? ts.Debug.checkEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined") + : ts.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. + */ +function tryGetObjectTypeDeclarationCompletionContainer(sourceFile: ts.SourceFile, contextToken: ts.Node | undefined, location: ts.Node, position: number): ts.ObjectTypeDeclaration | undefined { + // class c { method() { } | method2() { } } + switch (location.kind) { + case ts.SyntaxKind.SyntaxList: + return ts.tryCast(location.parent, ts.isObjectTypeDeclaration); + case ts.SyntaxKind.EndOfFileToken: + const cls = ts.tryCast(ts.lastOrUndefined(ts.cast(location.parent, ts.isSourceFile).statements), ts.isObjectTypeDeclaration); + if (cls && !ts.findChildOfKind(cls, ts.SyntaxKind.CloseBraceToken, sourceFile)) { + return cls; + } + break; + case ts.SyntaxKind.Identifier: { + // class c { public prop = c| } + if (ts.isPropertyDeclaration(location.parent) && location.parent.initializer === location) { + return undefined; } - if (couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.parent)) { - return parent; + // class c extends React.Component { a: () => 1\n compon| } + if (isFromObjectTypeDeclaration(location)) { + return ts.findAncestor(location, ts.isObjectTypeDeclaration); } - if (ts.isNamedImports(parent) || ts.isNamespaceImport(parent)) { - if (!parent.parent.isTypeOnly && (contextToken.kind === ts.SyntaxKind.OpenBraceToken || - contextToken.kind === ts.SyntaxKind.ImportKeyword || - contextToken.kind === ts.SyntaxKind.CommaToken)) { - keywordCompletion = ts.SyntaxKind.TypeKeyword; - } + } + } - if (canCompleteFromNamedBindings(parent)) { - // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` - if (contextToken.kind === ts.SyntaxKind.CloseBraceToken || contextToken.kind === ts.SyntaxKind.Identifier) { - isKeywordOnlyCompletion = true; - keywordCompletion = ts.SyntaxKind.FromKeyword; - } - else { - return parent.parent.parent; - } + if (!contextToken) + return undefined; + + // class C { blah; constructor/**/ } and so on + if (location.kind === ts.SyntaxKind.ConstructorKeyword + // class C { blah \n constructor/**/ } + || (ts.isIdentifier(contextToken) && ts.isPropertyDeclaration(contextToken.parent) && ts.isClassLike(location))) { + return ts.findAncestor(contextToken, ts.isClassLike) as ts.ObjectTypeDeclaration; + } + + switch (contextToken.kind) { + case ts.SyntaxKind.EqualsToken: // class c { public prop = | /* global completions */ } + return undefined; + + case ts.SyntaxKind.SemicolonToken: // class c {getValue(): number; | } + case ts.SyntaxKind.CloseBraceToken: // class c { method() { } | } + // class c { method() { } b| } + return isFromObjectTypeDeclaration(location) && (location.parent as ts.ClassElement | ts.TypeElement).name === location + ? location.parent.parent as ts.ObjectTypeDeclaration + : ts.tryCast(location, ts.isObjectTypeDeclaration); + case ts.SyntaxKind.OpenBraceToken: // class c { | + case ts.SyntaxKind.CommaToken: // class c {getValue(): number, | } + return ts.tryCast(contextToken.parent, ts.isObjectTypeDeclaration); + default: + if (!isFromObjectTypeDeclaration(contextToken)) { + // class c extends React.Component { a: () => 1\n| } + if (ts.getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== ts.getLineAndCharacterOfPosition(sourceFile, position).line && ts.isObjectTypeDeclaration(location)) { + return location; } return undefined; } - if (ts.isImportKeyword(contextToken) && ts.isSourceFile(parent)) { - // A lone import keyword with nothing following it does not parse as a statement at all - keywordCompletion = ts.SyntaxKind.TypeKeyword; - return contextToken as ts.Token; + const isValidKeyword = ts.isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword; + return (isValidKeyword(contextToken.kind) || contextToken.kind === ts.SyntaxKind.AsteriskToken || ts.isIdentifier(contextToken) && isValidKeyword(ts.stringToToken(contextToken.text)!)) // TODO: GH#18217 + ? contextToken.parent.parent as ts.ObjectTypeDeclaration : undefined; + } +} + +function tryGetTypeLiteralNode(node: ts.Node): ts.TypeLiteralNode | undefined { + if (!node) + return undefined; + + const parent = node.parent; + + switch (node.kind) { + case ts.SyntaxKind.OpenBraceToken: + if (ts.isTypeLiteralNode(parent)) { + return parent; } - if (ts.isImportKeyword(contextToken) && ts.isImportDeclaration(parent)) { - // `import s| from` - keywordCompletion = ts.SyntaxKind.TypeKeyword; - return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined; + break; + case ts.SyntaxKind.SemicolonToken: + case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.Identifier: + if (parent.kind === ts.SyntaxKind.PropertySignature && ts.isTypeLiteralNode(parent.parent)) { + return parent.parent; } - return undefined; - } + break; } - function couldBeTypeOnlyImportSpecifier(importSpecifier: ts.Node, contextToken: ts.Node | undefined): importSpecifier is ts.ImportSpecifier { - return ts.isImportSpecifier(importSpecifier) - && (importSpecifier.isTypeOnly || contextToken === importSpecifier.name && ts.isTypeKeywordTokenOrIdentifier(contextToken)); + return undefined; +} + +function getConstraintOfTypeArgumentProperty(node: ts.Node, checker: ts.TypeChecker): ts.Type | undefined { + if (!node) + return undefined; + if (ts.isTypeNode(node) && ts.isTypeReferenceType(node.parent)) { + return checker.getTypeArgumentConstraint(node); } - function canCompleteFromNamedBindings(namedBindings: ts.NamedImportBindings) { - return isModuleSpecifierMissingOrEmpty(namedBindings.parent.parent.moduleSpecifier) - && (ts.isNamespaceImport(namedBindings) || namedBindings.elements.length < 2) - && !namedBindings.parent.name; + const t = getConstraintOfTypeArgumentProperty(node.parent, checker); + if (!t) + return undefined; + + switch (node.kind) { + case ts.SyntaxKind.PropertySignature: + return checker.getTypeOfPropertyOfContextualType(t, node.symbol.escapedName); + case ts.SyntaxKind.IntersectionType: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.UnionType: + return t; } +} + +// TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes +function isFromObjectTypeDeclaration(node: ts.Node): boolean { + return node.parent && ts.isClassOrTypeElement(node.parent) && ts.isObjectTypeDeclaration(node.parent.parent); +} - function isModuleSpecifierMissingOrEmpty(specifier: ts.ModuleReference | ts.Expression) { - if (ts.nodeIsMissing(specifier)) +function isValidTrigger(sourceFile: ts.SourceFile, triggerCharacter: ts.CompletionsTriggerCharacter, contextToken: ts.Node | undefined, position: number): boolean { + switch (triggerCharacter) { + case ".": + case "@": return true; - return !ts.tryCast(ts.isExternalModuleReference(specifier) ? specifier.expression : specifier, ts.isStringLiteralLike)?.text; + case '"': + case "'": + case "`": + // Only automatically bring up completions if this is an opening quote. + return !!contextToken && ts.isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; + case "#": + return !!contextToken && ts.isPrivateIdentifier(contextToken) && !!ts.getContainingClass(contextToken); + case "<": + // Opening JSX tag + return !!contextToken && contextToken.kind === ts.SyntaxKind.LessThanToken && (!ts.isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent)); + case "/": + return !!contextToken && (ts.isStringLiteralLike(contextToken) + ? !!ts.tryGetImportFromModuleSpecifier(contextToken) + : contextToken.kind === ts.SyntaxKind.SlashToken && ts.isJsxClosingElement(contextToken.parent)); + case " ": + return !!contextToken && ts.isImportKeyword(contextToken) && contextToken.parent.kind === ts.SyntaxKind.SourceFile; + default: + return ts.Debug.assertNever(triggerCharacter); } +} - function getVariableDeclaration(property: ts.Node): ts.VariableDeclaration | undefined { - const variableDeclaration = ts.findAncestor(property, node => ts.isFunctionBlock(node) || isArrowFunctionBody(node) || ts.isBindingPattern(node) - ? "quit" - : ts.isVariableDeclaration(node)); - return variableDeclaration as ts.VariableDeclaration | undefined; - } +function binaryExpressionMayBeOpenTag({ left }: ts.BinaryExpression): boolean { + return ts.nodeIsMissing(left); +} - function isArrowFunctionBody(node: ts.Node) { - return node.parent && ts.isArrowFunction(node.parent) && node.parent.body === node; +/** Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. */ +function isProbablyGlobalType(type: ts.Type, sourceFile: ts.SourceFile, checker: ts.TypeChecker) { + // The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in + // lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists. + const selfSymbol = checker.resolveName("self", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); + if (selfSymbol && checker.getTypeOfSymbolAtLocation(selfSymbol, sourceFile) === type) { + return true; } - ; - - /** True if symbol is a type or a module containing at least one type. */ - function symbolCanBeReferencedAtTypeLocation(symbol: ts.Symbol, checker: ts.TypeChecker, seenModules = new ts.Map()): boolean { - // Since an alias can be merged with a local declaration, we need to test both the alias and its target. - // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. - return nonAliasCanBeReferencedAtTypeLocation(symbol) || nonAliasCanBeReferencedAtTypeLocation(ts.skipAlias(symbol.exportSymbol || symbol, checker)); - function nonAliasCanBeReferencedAtTypeLocation(symbol: ts.Symbol): boolean { - return !!(symbol.flags & ts.SymbolFlags.Type) || checker.isUnknownSymbol(symbol) || - !!(symbol.flags & ts.SymbolFlags.Module) && ts.addToSeen(seenModules, ts.getSymbolId(symbol)) && - checker.getExportsOfModule(symbol).some(e => symbolCanBeReferencedAtTypeLocation(e, checker, seenModules)); - } + const globalSymbol = checker.resolveName("global", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); + if (globalSymbol && checker.getTypeOfSymbolAtLocation(globalSymbol, sourceFile) === type) { + return true; + } + const globalThisSymbol = checker.resolveName("globalThis", /*location*/ undefined, ts.SymbolFlags.Value, /*excludeGlobals*/ false); + if (globalThisSymbol && checker.getTypeOfSymbolAtLocation(globalThisSymbol, sourceFile) === type) { + return true; } + return false; +} + +function isStaticProperty(symbol: ts.Symbol) { + return !!(symbol.valueDeclaration && ts.getEffectiveModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Static && ts.isClassLike(symbol.valueDeclaration.parent)); +} - function isDeprecated(symbol: ts.Symbol, checker: ts.TypeChecker) { - const declarations = ts.skipAlias(symbol, checker).declarations; - return !!ts.length(declarations) && ts.every(declarations, ts.isDeprecatedDeclaration); +function tryGetObjectLiteralContextualType(node: ts.ObjectLiteralExpression, typeChecker: ts.TypeChecker) { + const type = typeChecker.getContextualType(node); + if (type) { + return type; + } + const parent = ts.walkUpParenthesizedExpressions(node.parent); + if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && node === parent.left) { + // Object literal is assignment pattern: ({ | } = x) + return typeChecker.getTypeAtLocation(parent); } + if (ts.isExpression(parent)) { + // f(() => (({ | }))); + return typeChecker.getContextualType(parent); + } + return undefined; +} - /** - * True if the first character of `lowercaseCharacters` is the first character - * of some "word" in `identiferString` (where the string is split into "words" - * by camelCase and snake_case segments), then if the remaining characters of - * `lowercaseCharacters` appear, in order, in the rest of `identifierString`. - * - * True: - * 'state' in 'useState' - * 'sae' in 'useState' - * 'viable' in 'ENVIRONMENT_VARIABLE' - * - * False: - * 'staet' in 'useState' - * 'tate' in 'useState' - * 'ment' in 'ENVIRONMENT_VARIABLE' - */ - function charactersFuzzyMatchInString(identifierString: string, lowercaseCharacters: string): boolean { - if (lowercaseCharacters.length === 0) { - return true; +interface ImportStatementCompletionInfo { + isKeywordOnlyCompletion: boolean; + keywordCompletion: ts.TokenSyntaxKind | undefined; + isNewIdentifierLocation: boolean; + replacementNode: ts.ImportEqualsDeclaration | ts.ImportDeclaration | ts.ImportSpecifier | ts.Token | undefined; +} + +function getImportStatementCompletionInfo(contextToken: ts.Node): ImportStatementCompletionInfo { + let keywordCompletion: ts.TokenSyntaxKind | undefined; + let isKeywordOnlyCompletion = false; + const candidate = getCandidate(); + return { + isKeywordOnlyCompletion, + keywordCompletion, + isNewIdentifierLocation: !!(candidate || keywordCompletion === ts.SyntaxKind.TypeKeyword), + replacementNode: candidate && ts.rangeIsOnSingleLine(candidate, candidate.getSourceFile()) + ? candidate + : undefined + }; + + function getCandidate() { + const parent = contextToken.parent; + if (ts.isImportEqualsDeclaration(parent)) { + keywordCompletion = contextToken.kind === ts.SyntaxKind.TypeKeyword ? undefined : ts.SyntaxKind.TypeKeyword; + return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; + } + if (couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.parent)) { + return parent; } + if (ts.isNamedImports(parent) || ts.isNamespaceImport(parent)) { + if (!parent.parent.isTypeOnly && (contextToken.kind === ts.SyntaxKind.OpenBraceToken || + contextToken.kind === ts.SyntaxKind.ImportKeyword || + contextToken.kind === ts.SyntaxKind.CommaToken)) { + keywordCompletion = ts.SyntaxKind.TypeKeyword; + } - let matchedFirstCharacter = false; - let prevChar: number | undefined; - let characterIndex = 0; - const len = identifierString.length; - for (let strIndex = 0; strIndex < len; strIndex++) { - const strChar = identifierString.charCodeAt(strIndex); - const testChar = lowercaseCharacters.charCodeAt(characterIndex); - if (strChar === testChar || strChar === toUpperCharCode(testChar)) { - matchedFirstCharacter ||= - prevChar === undefined || // Beginning of word - ts.CharacterCodes.a <= prevChar && prevChar <= ts.CharacterCodes.z && ts.CharacterCodes.A <= strChar && strChar <= ts.CharacterCodes.Z || // camelCase transition - prevChar === ts.CharacterCodes._ && strChar !== ts.CharacterCodes._; // snake_case transition - if (matchedFirstCharacter) { - characterIndex++; + if (canCompleteFromNamedBindings(parent)) { + // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` + if (contextToken.kind === ts.SyntaxKind.CloseBraceToken || contextToken.kind === ts.SyntaxKind.Identifier) { + isKeywordOnlyCompletion = true; + keywordCompletion = ts.SyntaxKind.FromKeyword; } - if (characterIndex === lowercaseCharacters.length) { - return true; + else { + return parent.parent.parent; } } - prevChar = strChar; + return undefined; + } + if (ts.isImportKeyword(contextToken) && ts.isSourceFile(parent)) { + // A lone import keyword with nothing following it does not parse as a statement at all + keywordCompletion = ts.SyntaxKind.TypeKeyword; + return contextToken as ts.Token; } + if (ts.isImportKeyword(contextToken) && ts.isImportDeclaration(parent)) { + // `import s| from` + keywordCompletion = ts.SyntaxKind.TypeKeyword; + return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined; + } + return undefined; + } +} - // Did not find all characters - return false; +function couldBeTypeOnlyImportSpecifier(importSpecifier: ts.Node, contextToken: ts.Node | undefined): importSpecifier is ts.ImportSpecifier { + return ts.isImportSpecifier(importSpecifier) + && (importSpecifier.isTypeOnly || contextToken === importSpecifier.name && ts.isTypeKeywordTokenOrIdentifier(contextToken)); +} + +function canCompleteFromNamedBindings(namedBindings: ts.NamedImportBindings) { + return isModuleSpecifierMissingOrEmpty(namedBindings.parent.parent.moduleSpecifier) + && (ts.isNamespaceImport(namedBindings) || namedBindings.elements.length < 2) + && !namedBindings.parent.name; +} + +function isModuleSpecifierMissingOrEmpty(specifier: ts.ModuleReference | ts.Expression) { + if (ts.nodeIsMissing(specifier)) + return true; + return !ts.tryCast(ts.isExternalModuleReference(specifier) ? specifier.expression : specifier, ts.isStringLiteralLike)?.text; +} + +function getVariableDeclaration(property: ts.Node): ts.VariableDeclaration | undefined { + const variableDeclaration = ts.findAncestor(property, node => ts.isFunctionBlock(node) || isArrowFunctionBody(node) || ts.isBindingPattern(node) + ? "quit" + : ts.isVariableDeclaration(node)); + return variableDeclaration as ts.VariableDeclaration | undefined; +} + +function isArrowFunctionBody(node: ts.Node) { + return node.parent && ts.isArrowFunction(node.parent) && node.parent.body === node; +} +; + +/** True if symbol is a type or a module containing at least one type. */ +function symbolCanBeReferencedAtTypeLocation(symbol: ts.Symbol, checker: ts.TypeChecker, seenModules = new ts.Map()): boolean { + // Since an alias can be merged with a local declaration, we need to test both the alias and its target. + // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. + return nonAliasCanBeReferencedAtTypeLocation(symbol) || nonAliasCanBeReferencedAtTypeLocation(ts.skipAlias(symbol.exportSymbol || symbol, checker)); + function nonAliasCanBeReferencedAtTypeLocation(symbol: ts.Symbol): boolean { + return !!(symbol.flags & ts.SymbolFlags.Type) || checker.isUnknownSymbol(symbol) || + !!(symbol.flags & ts.SymbolFlags.Module) && ts.addToSeen(seenModules, ts.getSymbolId(symbol)) && + checker.getExportsOfModule(symbol).some(e => symbolCanBeReferencedAtTypeLocation(e, checker, seenModules)); } +} + +function isDeprecated(symbol: ts.Symbol, checker: ts.TypeChecker) { + const declarations = ts.skipAlias(symbol, checker).declarations; + return !!ts.length(declarations) && ts.every(declarations, ts.isDeprecatedDeclaration); +} - function toUpperCharCode(charCode: number) { - if (ts.CharacterCodes.a <= charCode && charCode <= ts.CharacterCodes.z) { - return charCode - 32; +/** + * True if the first character of `lowercaseCharacters` is the first character + * of some "word" in `identiferString` (where the string is split into "words" + * by camelCase and snake_case segments), then if the remaining characters of + * `lowercaseCharacters` appear, in order, in the rest of `identifierString`. + * + * True: + * 'state' in 'useState' + * 'sae' in 'useState' + * 'viable' in 'ENVIRONMENT_VARIABLE' + * + * False: + * 'staet' in 'useState' + * 'tate' in 'useState' + * 'ment' in 'ENVIRONMENT_VARIABLE' + */ + function charactersFuzzyMatchInString(identifierString: string, lowercaseCharacters: string): boolean { + if (lowercaseCharacters.length === 0) { + return true; + } + + let matchedFirstCharacter = false; + let prevChar: number | undefined; + let characterIndex = 0; + const len = identifierString.length; + for (let strIndex = 0; strIndex < len; strIndex++) { + const strChar = identifierString.charCodeAt(strIndex); + const testChar = lowercaseCharacters.charCodeAt(characterIndex); + if (strChar === testChar || strChar === toUpperCharCode(testChar)) { + matchedFirstCharacter ||= + prevChar === undefined || // Beginning of word + ts.CharacterCodes.a <= prevChar && prevChar <= ts.CharacterCodes.z && ts.CharacterCodes.A <= strChar && strChar <= ts.CharacterCodes.Z || // camelCase transition + prevChar === ts.CharacterCodes._ && strChar !== ts.CharacterCodes._; // snake_case transition + if (matchedFirstCharacter) { + characterIndex++; + } + if (characterIndex === lowercaseCharacters.length) { + return true; + } } - return charCode; + prevChar = strChar; + } + + // Did not find all characters + return false; +} + +function toUpperCharCode(charCode: number) { + if (ts.CharacterCodes.a <= charCode && charCode <= ts.CharacterCodes.z) { + return charCode - 32; } + return charCode; +} } diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 0ab8c29af6ed4..9f900cec1225b 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -1,511 +1,511 @@ namespace ts { - export interface DocumentHighlights { - fileName: string; - highlightSpans: ts.HighlightSpan[]; +export interface DocumentHighlights { + fileName: string; + highlightSpans: ts.HighlightSpan[]; +} + +/* @internal */ +export namespace DocumentHighlights { + export function getDocumentHighlights(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFile: ts.SourceFile, position: number, sourceFilesToSearch: readonly ts.SourceFile[]): DocumentHighlights[] | undefined { + const node = ts.getTouchingPropertyName(sourceFile, position); + if (node.parent && (ts.isJsxOpeningElement(node.parent) && node.parent.tagName === node || ts.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); + } + + function getHighlightSpanForNode(node: ts.Node, sourceFile: ts.SourceFile): ts.HighlightSpan { + return { + fileName: sourceFile.fileName, + textSpan: ts.createTextSpanFromNode(node, sourceFile), + kind: ts.HighlightSpanKind.none + }; } - /* @internal */ - export namespace DocumentHighlights { - export function getDocumentHighlights(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFile: ts.SourceFile, position: number, sourceFilesToSearch: readonly ts.SourceFile[]): DocumentHighlights[] | undefined { - const node = ts.getTouchingPropertyName(sourceFile, position); - if (node.parent && (ts.isJsxOpeningElement(node.parent) && node.parent.tagName === node || ts.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 getSemanticDocumentHighlights(position: number, node: ts.Node, program: ts.Program, cancellationToken: ts.CancellationToken, sourceFilesToSearch: readonly ts.SourceFile[]): DocumentHighlights[] | undefined { + const sourceFilesSet = new ts.Set(sourceFilesToSearch.map(f => f.fileName)); + const referenceEntries = ts.FindAllReferences.getReferenceEntriesForNode(position, node, program, sourceFilesToSearch, cancellationToken, /*options*/ undefined, sourceFilesSet); + if (!referenceEntries) + return undefined; + const map = ts.arrayToMultiMap(referenceEntries.map(ts.FindAllReferences.toHighlightSpan), e => e.fileName, e => e.span); + const getCanonicalFileName = ts.createGetCanonicalFileName(program.useCaseSensitiveFileNames()); + return ts.mapDefined(ts.arrayFrom(map.entries()), ([fileName, highlightSpans]) => { + if (!sourceFilesSet.has(fileName)) { + if (!program.redirectTargetsMap.has(ts.toPath(fileName, program.getCurrentDirectory(), getCanonicalFileName))) { + return undefined; + } + const redirectTarget = program.getSourceFile(fileName); + const redirect = ts.find(sourceFilesToSearch, f => !!f.redirectInfo && f.redirectInfo.redirectTarget === redirectTarget)!; + fileName = redirect.fileName; + ts.Debug.assert(sourceFilesSet.has(fileName)); } + return { fileName, highlightSpans }; + }); + } - return getSemanticDocumentHighlights(position, node, program, cancellationToken, sourceFilesToSearch) || getSyntacticDocumentHighlights(node, sourceFile); + function getSyntacticDocumentHighlights(node: ts.Node, sourceFile: ts.SourceFile): DocumentHighlights[] | undefined { + const highlightSpans = getHighlightSpans(node, sourceFile); + return highlightSpans && [{ fileName: sourceFile.fileName, highlightSpans }]; + } + + function getHighlightSpans(node: ts.Node, sourceFile: ts.SourceFile): ts.HighlightSpan[] | undefined { + switch (node.kind) { + case ts.SyntaxKind.IfKeyword: + case ts.SyntaxKind.ElseKeyword: + return ts.isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; + case ts.SyntaxKind.ReturnKeyword: + return useParent(node.parent, ts.isReturnStatement, getReturnOccurrences); + case ts.SyntaxKind.ThrowKeyword: + return useParent(node.parent, ts.isThrowStatement, getThrowOccurrences); + case ts.SyntaxKind.TryKeyword: + case ts.SyntaxKind.CatchKeyword: + case ts.SyntaxKind.FinallyKeyword: + const tryStatement = node.kind === ts.SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; + return useParent(tryStatement, ts.isTryStatement, getTryCatchFinallyOccurrences); + case ts.SyntaxKind.SwitchKeyword: + return useParent(node.parent, ts.isSwitchStatement, getSwitchCaseDefaultOccurrences); + case ts.SyntaxKind.CaseKeyword: + case ts.SyntaxKind.DefaultKeyword: { + if (ts.isDefaultClause(node.parent) || ts.isCaseClause(node.parent)) { + return useParent(node.parent.parent.parent, ts.isSwitchStatement, getSwitchCaseDefaultOccurrences); + } + return undefined; + } + case ts.SyntaxKind.BreakKeyword: + case ts.SyntaxKind.ContinueKeyword: + return useParent(node.parent, ts.isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); + case ts.SyntaxKind.ForKeyword: + case ts.SyntaxKind.WhileKeyword: + case ts.SyntaxKind.DoKeyword: + return useParent(node.parent, (n): n is ts.IterationStatement => ts.isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); + case ts.SyntaxKind.ConstructorKeyword: + return getFromAllDeclarations(ts.isConstructorDeclaration, [ts.SyntaxKind.ConstructorKeyword]); + case ts.SyntaxKind.GetKeyword: + case ts.SyntaxKind.SetKeyword: + return getFromAllDeclarations(ts.isAccessor, [ts.SyntaxKind.GetKeyword, ts.SyntaxKind.SetKeyword]); + case ts.SyntaxKind.AwaitKeyword: + return useParent(node.parent, ts.isAwaitExpression, getAsyncAndAwaitOccurrences); + case ts.SyntaxKind.AsyncKeyword: + return highlightSpans(getAsyncAndAwaitOccurrences(node)); + case ts.SyntaxKind.YieldKeyword: + return highlightSpans(getYieldOccurrences(node)); + case ts.SyntaxKind.InKeyword: + return undefined; + default: + return ts.isModifierKind(node.kind) && (ts.isDeclaration(node.parent) || ts.isVariableStatement(node.parent)) + ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) + : undefined; } - function getHighlightSpanForNode(node: ts.Node, sourceFile: ts.SourceFile): ts.HighlightSpan { - return { - fileName: sourceFile.fileName, - textSpan: ts.createTextSpanFromNode(node, sourceFile), - kind: ts.HighlightSpanKind.none - }; + function getFromAllDeclarations(nodeTest: (node: ts.Node) => node is T, keywords: readonly ts.SyntaxKind[]): ts.HighlightSpan[] | undefined { + return useParent(node.parent, nodeTest, decl => ts.mapDefined(decl.symbol.declarations, d => nodeTest(d) ? ts.find(d.getChildren(sourceFile), c => ts.contains(keywords, c.kind)) : undefined)); } - function getSemanticDocumentHighlights(position: number, node: ts.Node, program: ts.Program, cancellationToken: ts.CancellationToken, sourceFilesToSearch: readonly ts.SourceFile[]): DocumentHighlights[] | undefined { - const sourceFilesSet = new ts.Set(sourceFilesToSearch.map(f => f.fileName)); - const referenceEntries = ts.FindAllReferences.getReferenceEntriesForNode(position, node, program, sourceFilesToSearch, cancellationToken, /*options*/ undefined, sourceFilesSet); - if (!referenceEntries) - return undefined; - const map = ts.arrayToMultiMap(referenceEntries.map(ts.FindAllReferences.toHighlightSpan), e => e.fileName, e => e.span); - const getCanonicalFileName = ts.createGetCanonicalFileName(program.useCaseSensitiveFileNames()); - return ts.mapDefined(ts.arrayFrom(map.entries()), ([fileName, highlightSpans]) => { - if (!sourceFilesSet.has(fileName)) { - if (!program.redirectTargetsMap.has(ts.toPath(fileName, program.getCurrentDirectory(), getCanonicalFileName))) { - return undefined; - } - const redirectTarget = program.getSourceFile(fileName); - const redirect = ts.find(sourceFilesToSearch, f => !!f.redirectInfo && f.redirectInfo.redirectTarget === redirectTarget)!; - fileName = redirect.fileName; - ts.Debug.assert(sourceFilesSet.has(fileName)); - } - return { fileName, highlightSpans }; - }); + function useParent(node: ts.Node, nodeTest: (node: ts.Node) => node is T, getNodes: (node: T, sourceFile: ts.SourceFile) => readonly ts.Node[] | undefined): ts.HighlightSpan[] | undefined { + return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; } - function getSyntacticDocumentHighlights(node: ts.Node, sourceFile: ts.SourceFile): DocumentHighlights[] | undefined { - const highlightSpans = getHighlightSpans(node, sourceFile); - return highlightSpans && [{ fileName: sourceFile.fileName, highlightSpans }]; + function highlightSpans(nodes: readonly ts.Node[] | undefined): ts.HighlightSpan[] | undefined { + return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); } + } - function getHighlightSpans(node: ts.Node, sourceFile: ts.SourceFile): ts.HighlightSpan[] | undefined { - switch (node.kind) { - case ts.SyntaxKind.IfKeyword: - case ts.SyntaxKind.ElseKeyword: - return ts.isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; - case ts.SyntaxKind.ReturnKeyword: - return useParent(node.parent, ts.isReturnStatement, getReturnOccurrences); - case ts.SyntaxKind.ThrowKeyword: - return useParent(node.parent, ts.isThrowStatement, getThrowOccurrences); - case ts.SyntaxKind.TryKeyword: - case ts.SyntaxKind.CatchKeyword: - case ts.SyntaxKind.FinallyKeyword: - const tryStatement = node.kind === ts.SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; - return useParent(tryStatement, ts.isTryStatement, getTryCatchFinallyOccurrences); - case ts.SyntaxKind.SwitchKeyword: - return useParent(node.parent, ts.isSwitchStatement, getSwitchCaseDefaultOccurrences); - case ts.SyntaxKind.CaseKeyword: - case ts.SyntaxKind.DefaultKeyword: { - if (ts.isDefaultClause(node.parent) || ts.isCaseClause(node.parent)) { - return useParent(node.parent.parent.parent, ts.isSwitchStatement, getSwitchCaseDefaultOccurrences); - } - return undefined; - } - case ts.SyntaxKind.BreakKeyword: - case ts.SyntaxKind.ContinueKeyword: - return useParent(node.parent, ts.isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); - case ts.SyntaxKind.ForKeyword: - case ts.SyntaxKind.WhileKeyword: - case ts.SyntaxKind.DoKeyword: - return useParent(node.parent, (n): n is ts.IterationStatement => ts.isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); - case ts.SyntaxKind.ConstructorKeyword: - return getFromAllDeclarations(ts.isConstructorDeclaration, [ts.SyntaxKind.ConstructorKeyword]); - case ts.SyntaxKind.GetKeyword: - case ts.SyntaxKind.SetKeyword: - return getFromAllDeclarations(ts.isAccessor, [ts.SyntaxKind.GetKeyword, ts.SyntaxKind.SetKeyword]); - case ts.SyntaxKind.AwaitKeyword: - return useParent(node.parent, ts.isAwaitExpression, getAsyncAndAwaitOccurrences); - case ts.SyntaxKind.AsyncKeyword: - return highlightSpans(getAsyncAndAwaitOccurrences(node)); - case ts.SyntaxKind.YieldKeyword: - return highlightSpans(getYieldOccurrences(node)); - case ts.SyntaxKind.InKeyword: - return undefined; - default: - return ts.isModifierKind(node.kind) && (ts.isDeclaration(node.parent) || ts.isVariableStatement(node.parent)) - ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) - : undefined; - } + /** + * Aggregates all throw-statements within this node *without* crossing + * into function boundaries and try-blocks with catch-clauses. + */ + function aggregateOwnedThrowStatements(node: ts.Node): readonly ts.ThrowStatement[] | undefined { + if (ts.isThrowStatement(node)) { + return [node]; + } + else if (ts.isTryStatement(node)) { + // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. + return ts.concatenate(node.catchClause ? aggregateOwnedThrowStatements(node.catchClause) : node.tryBlock && aggregateOwnedThrowStatements(node.tryBlock), node.finallyBlock && aggregateOwnedThrowStatements(node.finallyBlock)); + } + // Do not cross function boundaries. + return ts.isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateOwnedThrowStatements); + } - function getFromAllDeclarations(nodeTest: (node: ts.Node) => node is T, keywords: readonly ts.SyntaxKind[]): ts.HighlightSpan[] | undefined { - return useParent(node.parent, nodeTest, decl => ts.mapDefined(decl.symbol.declarations, d => nodeTest(d) ? ts.find(d.getChildren(sourceFile), c => ts.contains(keywords, c.kind)) : undefined)); - } + /** + * 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: ts.ThrowStatement): ts.Node | undefined { + let child: ts.Node = throwStatement; - function useParent(node: ts.Node, nodeTest: (node: ts.Node) => node is T, getNodes: (node: T, sourceFile: ts.SourceFile) => readonly ts.Node[] | undefined): ts.HighlightSpan[] | undefined { - return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; - } + while (child.parent) { + const parent = child.parent; - function highlightSpans(nodes: readonly ts.Node[] | undefined): ts.HighlightSpan[] | undefined { - return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); + if (ts.isFunctionBlock(parent) || parent.kind === ts.SyntaxKind.SourceFile) { + return parent; } - } - /** - * Aggregates all throw-statements within this node *without* crossing - * into function boundaries and try-blocks with catch-clauses. - */ - function aggregateOwnedThrowStatements(node: ts.Node): readonly ts.ThrowStatement[] | undefined { - if (ts.isThrowStatement(node)) { - return [node]; - } - else if (ts.isTryStatement(node)) { - // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. - return ts.concatenate(node.catchClause ? aggregateOwnedThrowStatements(node.catchClause) : node.tryBlock && aggregateOwnedThrowStatements(node.tryBlock), node.finallyBlock && aggregateOwnedThrowStatements(node.finallyBlock)); + // 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 (ts.isTryStatement(parent) && parent.tryBlock === child && parent.catchClause) { + return child; } - // Do not cross function boundaries. - return ts.isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateOwnedThrowStatements); + + child = parent; } - /** - * 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: ts.ThrowStatement): ts.Node | undefined { - let child: ts.Node = throwStatement; + return undefined; + } - while (child.parent) { - const parent = child.parent; + function aggregateAllBreakAndContinueStatements(node: ts.Node): readonly ts.BreakOrContinueStatement[] | undefined { + return ts.isBreakOrContinueStatement(node) ? [node] : ts.isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateAllBreakAndContinueStatements); + } - if (ts.isFunctionBlock(parent) || parent.kind === ts.SyntaxKind.SourceFile) { - return parent; - } + function flatMapChildren(node: ts.Node, cb: (child: ts.Node) => readonly T[] | T | undefined): readonly T[] { + const result: T[] = []; + node.forEachChild(child => { + const value = cb(child); + if (value !== undefined) { + result.push(...ts.toArray(value)); + } + }); + return result; + } - // 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 (ts.isTryStatement(parent) && parent.tryBlock === child && parent.catchClause) { - return child; - } + function ownsBreakOrContinueStatement(owner: ts.Node, statement: ts.BreakOrContinueStatement): boolean { + const actualOwner = getBreakOrContinueOwner(statement); + return !!actualOwner && actualOwner === owner; + } - child = parent; + function getBreakOrContinueOwner(statement: ts.BreakOrContinueStatement): ts.Node | undefined { + return ts.findAncestor(statement, node => { + switch (node.kind) { + case ts.SyntaxKind.SwitchStatement: + if (statement.kind === ts.SyntaxKind.ContinueStatement) { + return false; + } + // falls through + + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.DoStatement: + return !statement.label || isLabeledBy(node, statement.label.escapedText); + default: + // Don't cross function boundaries. + // TODO: GH#20090 + return ts.isFunctionLike(node) && "quit"; } + }); + } - return undefined; - } - - function aggregateAllBreakAndContinueStatements(node: ts.Node): readonly ts.BreakOrContinueStatement[] | undefined { - return ts.isBreakOrContinueStatement(node) ? [node] : ts.isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateAllBreakAndContinueStatements); - } + function getModifierOccurrences(modifier: ts.Modifier["kind"], declaration: ts.Node): ts.Node[] { + return ts.mapDefined(getNodesToSearchForModifier(declaration, ts.modifierToFlag(modifier)), node => ts.findModifier(node, modifier)); + } - function flatMapChildren(node: ts.Node, cb: (child: ts.Node) => readonly T[] | T | undefined): readonly T[] { - const result: T[] = []; - node.forEachChild(child => { - const value = cb(child); - if (value !== undefined) { - result.push(...ts.toArray(value)); + function getNodesToSearchForModifier(declaration: ts.Node, modifierFlag: ts.ModifierFlags): readonly ts.Node[] | undefined { + // Types of node whose children might have modifiers. + const container = declaration.parent as ts.ModuleBlock | ts.SourceFile | ts.Block | ts.CaseClause | ts.DefaultClause | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.FunctionDeclaration | ts.ObjectTypeDeclaration | ts.ObjectLiteralExpression; + switch (container.kind) { + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.Block: + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.DefaultClause: + // Container is either a class declaration or the declaration is a classDeclaration + if (modifierFlag & ts.ModifierFlags.Abstract && ts.isClassDeclaration(declaration)) { + return [...declaration.members, declaration]; } - }); - return result; - } + else { + return container.statements; + } + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + return [...container.parameters, ...(ts.isClassLike(container.parent) ? container.parent.members : [])]; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.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 & (ts.ModifierFlags.AccessibilityModifier | ts.ModifierFlags.Readonly)) { + const constructor = ts.find(container.members, ts.isConstructorDeclaration); + if (constructor) { + return [...nodes, ...constructor.parameters]; + } + } + else if (modifierFlag & ts.ModifierFlags.Abstract) { + return [...nodes, container]; + } + return nodes; - function ownsBreakOrContinueStatement(owner: ts.Node, statement: ts.BreakOrContinueStatement): boolean { - const actualOwner = getBreakOrContinueOwner(statement); - return !!actualOwner && actualOwner === owner; - } + // Syntactically invalid positions that the parser might produce anyway + case ts.SyntaxKind.ObjectLiteralExpression: + return undefined; - function getBreakOrContinueOwner(statement: ts.BreakOrContinueStatement): ts.Node | undefined { - return ts.findAncestor(statement, node => { - switch (node.kind) { - case ts.SyntaxKind.SwitchStatement: - if (statement.kind === ts.SyntaxKind.ContinueStatement) { - return false; - } - // falls through - - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.DoStatement: - return !statement.label || isLabeledBy(node, statement.label.escapedText); - default: - // Don't cross function boundaries. - // TODO: GH#20090 - return ts.isFunctionLike(node) && "quit"; - } - }); + default: + ts.Debug.assertNever(container, "Invalid container kind."); } + } - function getModifierOccurrences(modifier: ts.Modifier["kind"], declaration: ts.Node): ts.Node[] { - return ts.mapDefined(getNodesToSearchForModifier(declaration, ts.modifierToFlag(modifier)), node => ts.findModifier(node, modifier)); + function pushKeywordIf(keywordList: ts.Push, token: ts.Node | undefined, ...expected: ts.SyntaxKind[]): boolean { + if (token && ts.contains(expected, token.kind)) { + keywordList.push(token); + return true; } - function getNodesToSearchForModifier(declaration: ts.Node, modifierFlag: ts.ModifierFlags): readonly ts.Node[] | undefined { - // Types of node whose children might have modifiers. - const container = declaration.parent as ts.ModuleBlock | ts.SourceFile | ts.Block | ts.CaseClause | ts.DefaultClause | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.FunctionDeclaration | ts.ObjectTypeDeclaration | ts.ObjectLiteralExpression; - switch (container.kind) { - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: - // Container is either a class declaration or the declaration is a classDeclaration - if (modifierFlag & ts.ModifierFlags.Abstract && ts.isClassDeclaration(declaration)) { - return [...declaration.members, declaration]; - } - else { - return container.statements; - } - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - return [...container.parameters, ...(ts.isClassLike(container.parent) ? container.parent.members : [])]; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.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 & (ts.ModifierFlags.AccessibilityModifier | ts.ModifierFlags.Readonly)) { - const constructor = ts.find(container.members, ts.isConstructorDeclaration); - if (constructor) { - return [...nodes, ...constructor.parameters]; - } - } - else if (modifierFlag & ts.ModifierFlags.Abstract) { - return [...nodes, container]; - } - return nodes; + return false; + } - // Syntactically invalid positions that the parser might produce anyway - case ts.SyntaxKind.ObjectLiteralExpression: - return undefined; + function getLoopBreakContinueOccurrences(loopNode: ts.IterationStatement): ts.Node[] { + const keywords: ts.Node[] = []; + if (pushKeywordIf(keywords, loopNode.getFirstToken(), ts.SyntaxKind.ForKeyword, ts.SyntaxKind.WhileKeyword, ts.SyntaxKind.DoKeyword)) { + // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. + if (loopNode.kind === ts.SyntaxKind.DoStatement) { + const loopTokens = loopNode.getChildren(); - default: - ts.Debug.assertNever(container, "Invalid container kind."); + for (let i = loopTokens.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, loopTokens[i], ts.SyntaxKind.WhileKeyword)) { + break; + } + } } } - function pushKeywordIf(keywordList: ts.Push, token: ts.Node | undefined, ...expected: ts.SyntaxKind[]): boolean { - if (token && ts.contains(expected, token.kind)) { - keywordList.push(token); - return true; + ts.forEach(aggregateAllBreakAndContinueStatements(loopNode.statement), statement => { + if (ownsBreakOrContinueStatement(loopNode, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), ts.SyntaxKind.BreakKeyword, ts.SyntaxKind.ContinueKeyword); } + }); - return false; - } + return keywords; + } - function getLoopBreakContinueOccurrences(loopNode: ts.IterationStatement): ts.Node[] { - const keywords: ts.Node[] = []; - if (pushKeywordIf(keywords, loopNode.getFirstToken(), ts.SyntaxKind.ForKeyword, ts.SyntaxKind.WhileKeyword, ts.SyntaxKind.DoKeyword)) { - // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. - if (loopNode.kind === ts.SyntaxKind.DoStatement) { - const loopTokens = loopNode.getChildren(); - - for (let i = loopTokens.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, loopTokens[i], ts.SyntaxKind.WhileKeyword)) { - break; - } - } - } + function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: ts.BreakOrContinueStatement): ts.Node[] | undefined { + const owner = getBreakOrContinueOwner(breakOrContinueStatement); + + if (owner) { + switch (owner.kind) { + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WhileStatement: + return getLoopBreakContinueOccurrences(owner as ts.IterationStatement); + case ts.SyntaxKind.SwitchStatement: + return getSwitchCaseDefaultOccurrences(owner as ts.SwitchStatement); } + } - ts.forEach(aggregateAllBreakAndContinueStatements(loopNode.statement), statement => { - if (ownsBreakOrContinueStatement(loopNode, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), ts.SyntaxKind.BreakKeyword, ts.SyntaxKind.ContinueKeyword); - } - }); + return undefined; + } - return keywords; - } + function getSwitchCaseDefaultOccurrences(switchStatement: ts.SwitchStatement): ts.Node[] { + const keywords: ts.Node[] = []; + pushKeywordIf(keywords, switchStatement.getFirstToken(), ts.SyntaxKind.SwitchKeyword); - function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: ts.BreakOrContinueStatement): ts.Node[] | undefined { - const owner = getBreakOrContinueOwner(breakOrContinueStatement); - - if (owner) { - switch (owner.kind) { - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WhileStatement: - return getLoopBreakContinueOccurrences(owner as ts.IterationStatement); - case ts.SyntaxKind.SwitchStatement: - return getSwitchCaseDefaultOccurrences(owner as ts.SwitchStatement); + // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. + ts.forEach(switchStatement.caseBlock.clauses, clause => { + pushKeywordIf(keywords, clause.getFirstToken(), ts.SyntaxKind.CaseKeyword, ts.SyntaxKind.DefaultKeyword); + ts.forEach(aggregateAllBreakAndContinueStatements(clause), statement => { + if (ownsBreakOrContinueStatement(switchStatement, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), ts.SyntaxKind.BreakKeyword); } - } + }); + }); - return undefined; - } + return keywords; + } - function getSwitchCaseDefaultOccurrences(switchStatement: ts.SwitchStatement): ts.Node[] { - const keywords: ts.Node[] = []; - pushKeywordIf(keywords, switchStatement.getFirstToken(), ts.SyntaxKind.SwitchKeyword); + function getTryCatchFinallyOccurrences(tryStatement: ts.TryStatement, sourceFile: ts.SourceFile): ts.Node[] { + const keywords: ts.Node[] = []; + pushKeywordIf(keywords, tryStatement.getFirstToken(), ts.SyntaxKind.TryKeyword); - // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. - ts.forEach(switchStatement.caseBlock.clauses, clause => { - pushKeywordIf(keywords, clause.getFirstToken(), ts.SyntaxKind.CaseKeyword, ts.SyntaxKind.DefaultKeyword); - ts.forEach(aggregateAllBreakAndContinueStatements(clause), statement => { - if (ownsBreakOrContinueStatement(switchStatement, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), ts.SyntaxKind.BreakKeyword); - } - }); - }); - - return keywords; + if (tryStatement.catchClause) { + pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), ts.SyntaxKind.CatchKeyword); } - function getTryCatchFinallyOccurrences(tryStatement: ts.TryStatement, sourceFile: ts.SourceFile): ts.Node[] { - const keywords: ts.Node[] = []; - pushKeywordIf(keywords, tryStatement.getFirstToken(), ts.SyntaxKind.TryKeyword); + if (tryStatement.finallyBlock) { + const finallyKeyword = ts.findChildOfKind(tryStatement, ts.SyntaxKind.FinallyKeyword, sourceFile)!; + pushKeywordIf(keywords, finallyKeyword, ts.SyntaxKind.FinallyKeyword); + } - if (tryStatement.catchClause) { - pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), ts.SyntaxKind.CatchKeyword); - } + return keywords; + } - if (tryStatement.finallyBlock) { - const finallyKeyword = ts.findChildOfKind(tryStatement, ts.SyntaxKind.FinallyKeyword, sourceFile)!; - pushKeywordIf(keywords, finallyKeyword, ts.SyntaxKind.FinallyKeyword); - } + function getThrowOccurrences(throwStatement: ts.ThrowStatement, sourceFile: ts.SourceFile): ts.Node[] | undefined { + const owner = getThrowStatementOwner(throwStatement); - return keywords; + if (!owner) { + return undefined; } - function getThrowOccurrences(throwStatement: ts.ThrowStatement, sourceFile: ts.SourceFile): ts.Node[] | undefined { - const owner = getThrowStatementOwner(throwStatement); + const keywords: ts.Node[] = []; + ts.forEach(aggregateOwnedThrowStatements(owner), throwStatement => { + keywords.push(ts.findChildOfKind(throwStatement, ts.SyntaxKind.ThrowKeyword, sourceFile)!); + }); - if (!owner) { - return undefined; - } - - const keywords: ts.Node[] = []; - ts.forEach(aggregateOwnedThrowStatements(owner), throwStatement => { - keywords.push(ts.findChildOfKind(throwStatement, ts.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 (ts.isFunctionBlock(owner)) { + ts.forEachReturnStatement(owner as ts.Block, returnStatement => { + keywords.push(ts.findChildOfKind(returnStatement, ts.SyntaxKind.ReturnKeyword, 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 (ts.isFunctionBlock(owner)) { - ts.forEachReturnStatement(owner as ts.Block, returnStatement => { - keywords.push(ts.findChildOfKind(returnStatement, ts.SyntaxKind.ReturnKeyword, sourceFile)!); - }); - } + return keywords; + } - return keywords; + function getReturnOccurrences(returnStatement: ts.ReturnStatement, sourceFile: ts.SourceFile): ts.Node[] | undefined { + const func = ts.getContainingFunction(returnStatement) as ts.FunctionLikeDeclaration; + if (!func) { + return undefined; } - function getReturnOccurrences(returnStatement: ts.ReturnStatement, sourceFile: ts.SourceFile): ts.Node[] | undefined { - const func = ts.getContainingFunction(returnStatement) as ts.FunctionLikeDeclaration; - if (!func) { - return undefined; - } + const keywords: ts.Node[] = []; + ts.forEachReturnStatement(ts.cast(func.body, ts.isBlock), returnStatement => { + keywords.push(ts.findChildOfKind(returnStatement, ts.SyntaxKind.ReturnKeyword, sourceFile)!); + }); - const keywords: ts.Node[] = []; - ts.forEachReturnStatement(ts.cast(func.body, ts.isBlock), returnStatement => { - keywords.push(ts.findChildOfKind(returnStatement, ts.SyntaxKind.ReturnKeyword, sourceFile)!); - }); + // Include 'throw' statements that do not occur within a try block. + ts.forEach(aggregateOwnedThrowStatements(func.body!), throwStatement => { + keywords.push(ts.findChildOfKind(throwStatement, ts.SyntaxKind.ThrowKeyword, sourceFile)!); + }); - // Include 'throw' statements that do not occur within a try block. - ts.forEach(aggregateOwnedThrowStatements(func.body!), throwStatement => { - keywords.push(ts.findChildOfKind(throwStatement, ts.SyntaxKind.ThrowKeyword, sourceFile)!); - }); + return keywords; + } - return keywords; + function getAsyncAndAwaitOccurrences(node: ts.Node): ts.Node[] | undefined { + const func = ts.getContainingFunction(node) as ts.FunctionLikeDeclaration; + if (!func) { + return undefined; } - function getAsyncAndAwaitOccurrences(node: ts.Node): ts.Node[] | undefined { - const func = ts.getContainingFunction(node) as ts.FunctionLikeDeclaration; - if (!func) { - return undefined; - } - - const keywords: ts.Node[] = []; + const keywords: ts.Node[] = []; - if (func.modifiers) { - func.modifiers.forEach(modifier => { - pushKeywordIf(keywords, modifier, ts.SyntaxKind.AsyncKeyword); - }); - } + if (func.modifiers) { + func.modifiers.forEach(modifier => { + pushKeywordIf(keywords, modifier, ts.SyntaxKind.AsyncKeyword); + }); + } - ts.forEachChild(func, child => { - traverseWithoutCrossingFunction(child, node => { - if (ts.isAwaitExpression(node)) { - pushKeywordIf(keywords, node.getFirstToken(), ts.SyntaxKind.AwaitKeyword); - } - }); + ts.forEachChild(func, child => { + traverseWithoutCrossingFunction(child, node => { + if (ts.isAwaitExpression(node)) { + pushKeywordIf(keywords, node.getFirstToken(), ts.SyntaxKind.AwaitKeyword); + } }); + }); - return keywords; - } + return keywords; + } - function getYieldOccurrences(node: ts.Node): ts.Node[] | undefined { - const func = ts.getContainingFunction(node) as ts.FunctionDeclaration; - if (!func) { - return undefined; - } + function getYieldOccurrences(node: ts.Node): ts.Node[] | undefined { + const func = ts.getContainingFunction(node) as ts.FunctionDeclaration; + if (!func) { + return undefined; + } - const keywords: ts.Node[] = []; - ts.forEachChild(func, child => { - traverseWithoutCrossingFunction(child, node => { - if (ts.isYieldExpression(node)) { - pushKeywordIf(keywords, node.getFirstToken(), ts.SyntaxKind.YieldKeyword); - } - }); + const keywords: ts.Node[] = []; + ts.forEachChild(func, child => { + traverseWithoutCrossingFunction(child, node => { + if (ts.isYieldExpression(node)) { + pushKeywordIf(keywords, node.getFirstToken(), ts.SyntaxKind.YieldKeyword); + } }); + }); - return keywords; - } + return keywords; + } - // Do not cross function/class/interface/module/type boundaries. - function traverseWithoutCrossingFunction(node: ts.Node, cb: (node: ts.Node) => void) { - cb(node); - if (!ts.isFunctionLike(node) && !ts.isClassLike(node) && !ts.isInterfaceDeclaration(node) && !ts.isModuleDeclaration(node) && !ts.isTypeAliasDeclaration(node) && !ts.isTypeNode(node)) { - ts.forEachChild(node, child => traverseWithoutCrossingFunction(child, cb)); - } + // Do not cross function/class/interface/module/type boundaries. + function traverseWithoutCrossingFunction(node: ts.Node, cb: (node: ts.Node) => void) { + cb(node); + if (!ts.isFunctionLike(node) && !ts.isClassLike(node) && !ts.isInterfaceDeclaration(node) && !ts.isModuleDeclaration(node) && !ts.isTypeAliasDeclaration(node) && !ts.isTypeNode(node)) { + ts.forEachChild(node, child => traverseWithoutCrossingFunction(child, cb)); } + } - function getIfElseOccurrences(ifStatement: ts.IfStatement, sourceFile: ts.SourceFile): ts.HighlightSpan[] { - const keywords = getIfElseKeywords(ifStatement, sourceFile); - const result: ts.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 === ts.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 (!ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { - shouldCombineElseAndIf = false; - break; - } - } + function getIfElseOccurrences(ifStatement: ts.IfStatement, sourceFile: ts.SourceFile): ts.HighlightSpan[] { + const keywords = getIfElseKeywords(ifStatement, sourceFile); + const result: ts.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 === ts.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; - if (shouldCombineElseAndIf) { - result.push({ - fileName: sourceFile.fileName, - textSpan: ts.createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), - kind: ts.HighlightSpanKind.reference - }); - i++; // skip the next keyword - continue; + // Avoid recalculating getStart() by iterating backwards. + for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { + if (!ts.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: ts.createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), + kind: ts.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: ts.IfStatement, sourceFile: ts.SourceFile): ts.Node[] { - const keywords: ts.Node[] = []; + return result; + } - // Traverse upwards through all parent if-statements linked by their else-branches. - while (ts.isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { - ifStatement = ifStatement.parent; - } + function getIfElseKeywords(ifStatement: ts.IfStatement, sourceFile: ts.SourceFile): ts.Node[] { + const keywords: ts.Node[] = []; - // 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], ts.SyntaxKind.IfKeyword); + // Traverse upwards through all parent if-statements linked by their else-branches. + while (ts.isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { + ifStatement = ifStatement.parent; + } - // 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], ts.SyntaxKind.ElseKeyword)) { - break; - } - } + // 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], ts.SyntaxKind.IfKeyword); - if (!ifStatement.elseStatement || !ts.isIfStatement(ifStatement.elseStatement)) { + // 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], ts.SyntaxKind.ElseKeyword)) { break; } + } - ifStatement = ifStatement.elseStatement; + if (!ifStatement.elseStatement || !ts.isIfStatement(ifStatement.elseStatement)) { + break; } - return keywords; + ifStatement = ifStatement.elseStatement; } - /** - * Whether or not a 'node' is preceded by a label of the given string. - * Note: 'node' cannot be a SourceFile. - */ - function isLabeledBy(node: ts.Node, labelName: ts.__String): boolean { - return !!ts.findAncestor(node.parent, owner => !ts.isLabeledStatement(owner) ? "quit" : owner.label.escapedText === labelName); - } + 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: ts.Node, labelName: ts.__String): boolean { + return !!ts.findAncestor(node.parent, owner => !ts.isLabeledStatement(owner) ? "quit" : owner.label.escapedText === labelName); } } +} diff --git a/src/services/documentRegistry.ts b/src/services/documentRegistry.ts index 49189dbb47cb3..505819259f4b9 100644 --- a/src/services/documentRegistry.ts +++ b/src/services/documentRegistry.ts @@ -1,355 +1,355 @@ namespace 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. - * - * 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 compilationSettingsOrHost 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. A minimal + * resolution cache is needed to fully define a source file's shape when + * the compilation settings include `module: node16`+, so providing a cache host + * object should be preferred. A common host is a language service `ConfiguredProject`. + * @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. */ - 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 compilationSettingsOrHost 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. A minimal - * resolution cache is needed to fully define a source file's shape when - * the compilation settings include `module: node16`+, so providing a cache host - * object should be preferred. A common host is a language service `ConfiguredProject`. - * @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, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; - acquireDocumentWithKey(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.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 compilationSettingsOrHost 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. A minimal - * resolution cache is needed to fully define a source file's shape when - * the compilation settings include `module: node16`+, so providing a cache host - * object should be preferred. A common host is a language service `ConfiguredProject`. - * @param scriptSnapshot Text of the file. - * @param version Current version of the file. - */ - updateDocument(fileName: string, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; - updateDocumentWithKey(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; - getKeyForCompilationSettings(settings: ts.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 - */ - /**@deprecated pass scriptKind for correctness */ - releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions): void; - /** - * 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 - * @param scriptKind The script kind of the file to be released - */ - releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions, scriptKind: ts.ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures - /** - * @deprecated pass scriptKind for correctness */ - releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey): void; - releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey, scriptKind: ts.ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures - - /*@internal*/ - getLanguageServiceRefCounts(path: ts.Path, scriptKind: ts.ScriptKind): [ - string, - number | undefined - ][]; + acquireDocument(fileName: string, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; + acquireDocumentWithKey(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; - reportStats(): string; - } + /** + * 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 compilationSettingsOrHost 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. A minimal + * resolution cache is needed to fully define a source file's shape when + * the compilation settings include `module: node16`+, so providing a cache host + * object should be preferred. A common host is a language service `ConfiguredProject`. + * @param scriptSnapshot Text of the file. + * @param version Current version of the file. + */ + updateDocument(fileName: string, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; + updateDocumentWithKey(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile; + getKeyForCompilationSettings(settings: ts.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 + */ + /**@deprecated pass scriptKind for correctness */ + releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions): void; + /** + * 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 + * @param scriptKind The script kind of the file to be released + */ + releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions, scriptKind: ts.ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures + /** + * @deprecated pass scriptKind for correctness */ + releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey): void; + releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey, scriptKind: ts.ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures /*@internal*/ - export interface ExternalDocumentCache { - setDocument(key: DocumentRegistryBucketKey, path: ts.Path, sourceFile: ts.SourceFile): void; - getDocument(key: DocumentRegistryBucketKey, path: ts.Path): ts.SourceFile | undefined; - } + getLanguageServiceRefCounts(path: ts.Path, scriptKind: ts.ScriptKind): [ + string, + number | undefined + ][]; - export type DocumentRegistryBucketKey = string & { - __bucketKey: any; - }; + reportStats(): string; +} - interface DocumentRegistryEntry { - sourceFile: ts.SourceFile; +/*@internal*/ +export interface ExternalDocumentCache { + setDocument(key: DocumentRegistryBucketKey, path: ts.Path, sourceFile: ts.SourceFile): void; + getDocument(key: DocumentRegistryBucketKey, path: ts.Path): ts.SourceFile | undefined; +} - // 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 type DocumentRegistryBucketKey = string & { + __bucketKey: any; +}; - type BucketEntry = DocumentRegistryEntry | ts.ESMap; - function isDocumentRegistryEntry(entry: BucketEntry): entry is DocumentRegistryEntry { - return !!(entry as DocumentRegistryEntry).sourceFile; - } +interface DocumentRegistryEntry { + sourceFile: ts.SourceFile; - export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry { - return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory); - } + // 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; +} - /*@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 = new ts.Map>(); - const getCanonicalFileName = ts.createGetCanonicalFileName(!!useCaseSensitiveFileNames); - - function reportStats() { - const bucketInfoArray = ts.arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => { - const entries = buckets.get(name)!; - const sourceFiles: { - name: string; - scriptKind: ts.ScriptKind; - refCount: number; - }[] = []; - entries.forEach((entry, name) => { - if (isDocumentRegistryEntry(entry)) { - sourceFiles.push({ - name, - scriptKind: entry.sourceFile.scriptKind, - refCount: entry.languageServiceRefCount - }); - } - else { - entry.forEach((value, scriptKind) => sourceFiles.push({ name, scriptKind, refCount: value.languageServiceRefCount })); - } - }); - sourceFiles.sort((x, y) => y.refCount - x.refCount); - return { - bucket: name, - sourceFiles - }; - }); - return JSON.stringify(bucketInfoArray, undefined, 2); - } +type BucketEntry = DocumentRegistryEntry | ts.ESMap; +function isDocumentRegistryEntry(entry: BucketEntry): entry is DocumentRegistryEntry { + return !!(entry as DocumentRegistryEntry).sourceFile; +} - function getCompilationSettings(settingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost) { - if (typeof settingsOrHost.getCompilationSettings === "function") { - return (settingsOrHost as ts.MinimalResolutionCacheHost).getCompilationSettings(); - } - return settingsOrHost as ts.CompilerOptions; - } +export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry { + return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory); +} - function acquireDocument(fileName: string, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings)); - return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, 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 = new ts.Map>(); + const getCanonicalFileName = ts.createGetCanonicalFileName(!!useCaseSensitiveFileNames); + + function reportStats() { + const bucketInfoArray = ts.arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => { + const entries = buckets.get(name)!; + const sourceFiles: { + name: string; + scriptKind: ts.ScriptKind; + refCount: number; + }[] = []; + entries.forEach((entry, name) => { + if (isDocumentRegistryEntry(entry)) { + sourceFiles.push({ + name, + scriptKind: entry.sourceFile.scriptKind, + refCount: entry.languageServiceRefCount + }); + } + else { + entry.forEach((value, scriptKind) => sourceFiles.push({ name, scriptKind, refCount: value.languageServiceRefCount })); + } + }); + sourceFiles.sort((x, y) => y.refCount - x.refCount); + return { + bucket: name, + sourceFiles + }; + }); + return JSON.stringify(bucketInfoArray, undefined, 2); + } - function acquireDocumentWithKey(fileName: string, path: ts.Path, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { - return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind); + function getCompilationSettings(settingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost) { + if (typeof settingsOrHost.getCompilationSettings === "function") { + return (settingsOrHost as ts.MinimalResolutionCacheHost).getCompilationSettings(); } + return settingsOrHost as ts.CompilerOptions; + } - function updateDocument(fileName: string, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings)); - return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); - } + function acquireDocument(fileName: string, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings)); + return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); + } - function updateDocumentWithKey(fileName: string, path: ts.Path, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { - return acquireOrUpdateDocument(fileName, path, getCompilationSettings(compilationSettings), key, scriptSnapshot, version, /*acquiring*/ false, scriptKind); - } + function acquireDocumentWithKey(fileName: string, path: ts.Path, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { + return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind); + } - function getDocumentRegistryEntry(bucketEntry: BucketEntry, scriptKind: ts.ScriptKind | undefined) { - const entry = isDocumentRegistryEntry(bucketEntry) ? bucketEntry : bucketEntry.get(ts.Debug.checkDefined(scriptKind, "If there are more than one scriptKind's for same document the scriptKind should be provided")); - ts.Debug.assert(scriptKind === undefined || !entry || entry.sourceFile.scriptKind === scriptKind, `Script kind should match provided ScriptKind:${scriptKind} and sourceFile.scriptKind: ${entry?.sourceFile.scriptKind}, !entry: ${!entry}`); - return entry; - } + function updateDocument(fileName: string, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings)); + return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); + } - function acquireOrUpdateDocument(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, acquiring: boolean, scriptKind?: ts.ScriptKind): ts.SourceFile { - scriptKind = ts.ensureScriptKind(fileName, scriptKind); - const compilationSettings = getCompilationSettings(compilationSettingsOrHost); - const host: ts.MinimalResolutionCacheHost | undefined = compilationSettingsOrHost === compilationSettings ? undefined : compilationSettingsOrHost as ts.MinimalResolutionCacheHost; - const scriptTarget = scriptKind === ts.ScriptKind.JSON ? ts.ScriptTarget.JSON : ts.getEmitScriptTarget(compilationSettings); - const sourceFileOptions: ts.CreateSourceFileOptions = { - languageVersion: scriptTarget, - impliedNodeFormat: host && ts.getImpliedNodeFormatForFile(path, host.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), host, compilationSettings), - setExternalModuleIndicator: ts.getSetExternalModuleIndicator(compilationSettings) - }; + function updateDocumentWithKey(fileName: string, path: ts.Path, compilationSettings: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, scriptKind?: ts.ScriptKind): ts.SourceFile { + return acquireOrUpdateDocument(fileName, path, getCompilationSettings(compilationSettings), key, scriptSnapshot, version, /*acquiring*/ false, scriptKind); + } - const oldBucketCount = buckets.size; - const bucket = ts.getOrUpdate(buckets, key, () => new ts.Map()); - if (ts.tracing) { - if (buckets.size > oldBucketCount) { - // It is interesting, but not definitively problematic if a build requires multiple document registry buckets - - // perhaps they are for two projects that don't have any overlap. - // Bonus: these events can help us interpret the more interesting event below. - ts.tracing.instant(ts.tracing.Phase.Session, "createdDocumentRegistryBucket", { configFilePath: compilationSettings.configFilePath, key }); - } + function getDocumentRegistryEntry(bucketEntry: BucketEntry, scriptKind: ts.ScriptKind | undefined) { + const entry = isDocumentRegistryEntry(bucketEntry) ? bucketEntry : bucketEntry.get(ts.Debug.checkDefined(scriptKind, "If there are more than one scriptKind's for same document the scriptKind should be provided")); + ts.Debug.assert(scriptKind === undefined || !entry || entry.sourceFile.scriptKind === scriptKind, `Script kind should match provided ScriptKind:${scriptKind} and sourceFile.scriptKind: ${entry?.sourceFile.scriptKind}, !entry: ${!entry}`); + return entry; + } - // It is fairly suspicious to have one path in two buckets - you'd expect dependencies to have similar configurations. - // If this occurs unexpectedly, the fix is likely to synchronize the project settings. - // Skip .d.ts files to reduce noise (should also cover most of node_modules). - const otherBucketKey = !ts.isDeclarationFileName(path) && - ts.forEachEntry(buckets, (bucket, bucketKey) => bucketKey !== key && bucket.has(path) && bucketKey); - if (otherBucketKey) { - ts.tracing.instant(ts.tracing.Phase.Session, "documentRegistryBucketOverlap", { path, key1: otherBucketKey, key2: key }); - } + function acquireOrUpdateDocument(fileName: string, path: ts.Path, compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: ts.IScriptSnapshot, version: string, acquiring: boolean, scriptKind?: ts.ScriptKind): ts.SourceFile { + scriptKind = ts.ensureScriptKind(fileName, scriptKind); + const compilationSettings = getCompilationSettings(compilationSettingsOrHost); + const host: ts.MinimalResolutionCacheHost | undefined = compilationSettingsOrHost === compilationSettings ? undefined : compilationSettingsOrHost as ts.MinimalResolutionCacheHost; + const scriptTarget = scriptKind === ts.ScriptKind.JSON ? ts.ScriptTarget.JSON : ts.getEmitScriptTarget(compilationSettings); + const sourceFileOptions: ts.CreateSourceFileOptions = { + languageVersion: scriptTarget, + impliedNodeFormat: host && ts.getImpliedNodeFormatForFile(path, host.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), host, compilationSettings), + setExternalModuleIndicator: ts.getSetExternalModuleIndicator(compilationSettings) + }; + + const oldBucketCount = buckets.size; + const bucket = ts.getOrUpdate(buckets, key, () => new ts.Map()); + if (ts.tracing) { + if (buckets.size > oldBucketCount) { + // It is interesting, but not definitively problematic if a build requires multiple document registry buckets - + // perhaps they are for two projects that don't have any overlap. + // Bonus: these events can help us interpret the more interesting event below. + ts.tracing.instant(ts.tracing.Phase.Session, "createdDocumentRegistryBucket", { configFilePath: compilationSettings.configFilePath, key }); } - const bucketEntry = bucket.get(path); - let entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind); - if (!entry && externalCache) { - const sourceFile = externalCache.getDocument(key, path); - if (sourceFile) { - ts.Debug.assert(acquiring); - entry = { - sourceFile, - languageServiceRefCount: 0 - }; - setBucketEntry(); - } + // It is fairly suspicious to have one path in two buckets - you'd expect dependencies to have similar configurations. + // If this occurs unexpectedly, the fix is likely to synchronize the project settings. + // Skip .d.ts files to reduce noise (should also cover most of node_modules). + const otherBucketKey = !ts.isDeclarationFileName(path) && + ts.forEachEntry(buckets, (bucket, bucketKey) => bucketKey !== key && bucket.has(path) && bucketKey); + if (otherBucketKey) { + ts.tracing.instant(ts.tracing.Phase.Session, "documentRegistryBucketOverlap", { path, key1: otherBucketKey, key2: key }); } + } - if (!entry) { - // Have never seen this file with these settings. Create a new source file for it. - const sourceFile = ts.createLanguageServiceSourceFile(fileName, scriptSnapshot, sourceFileOptions, version, /*setNodeParents*/ false, scriptKind); - if (externalCache) { - externalCache.setDocument(key, path, sourceFile); - } + const bucketEntry = bucket.get(path); + let entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind); + if (!entry && externalCache) { + const sourceFile = externalCache.getDocument(key, path); + if (sourceFile) { + ts.Debug.assert(acquiring); entry = { sourceFile, - languageServiceRefCount: 1, + languageServiceRefCount: 0 }; setBucketEntry(); } - 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 = ts.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++; + if (!entry) { + // Have never seen this file with these settings. Create a new source file for it. + const sourceFile = ts.createLanguageServiceSourceFile(fileName, scriptSnapshot, sourceFileOptions, version, /*setNodeParents*/ false, scriptKind); + if (externalCache) { + externalCache.setDocument(key, path, sourceFile); + } + entry = { + sourceFile, + languageServiceRefCount: 1, + }; + setBucketEntry(); + } + 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 = ts.updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version, scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!)); // TODO: GH#18217 + if (externalCache) { + externalCache.setDocument(key, path, entry.sourceFile); } } - ts.Debug.assert(entry.languageServiceRefCount !== 0); - - return entry.sourceFile; - function setBucketEntry() { - if (!bucketEntry) { - bucket.set(path, entry!); - } - else if (isDocumentRegistryEntry(bucketEntry)) { - const scriptKindMap = new ts.Map(); - scriptKindMap.set(bucketEntry.sourceFile.scriptKind, bucketEntry); - scriptKindMap.set(scriptKind!, entry!); - bucket.set(path, scriptKindMap); - } - else { - bucketEntry.set(scriptKind!, entry!); - } + // 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++; } } + ts.Debug.assert(entry.languageServiceRefCount !== 0); - function releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions, scriptKind?: ts.ScriptKind): void { - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(compilationSettings); - return releaseDocumentWithKey(path, key, scriptKind); + return entry.sourceFile; + + function setBucketEntry() { + if (!bucketEntry) { + bucket.set(path, entry!); + } + else if (isDocumentRegistryEntry(bucketEntry)) { + const scriptKindMap = new ts.Map(); + scriptKindMap.set(bucketEntry.sourceFile.scriptKind, bucketEntry); + scriptKindMap.set(scriptKind!, entry!); + bucket.set(path, scriptKindMap); + } + else { + bucketEntry.set(scriptKind!, entry!); + } } + } - function releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey, scriptKind?: ts.ScriptKind): void { - const bucket = ts.Debug.checkDefined(buckets.get(key)); - const bucketEntry = bucket.get(path)!; - const entry = getDocumentRegistryEntry(bucketEntry, scriptKind)!; - entry.languageServiceRefCount--; + function releaseDocument(fileName: string, compilationSettings: ts.CompilerOptions, scriptKind?: ts.ScriptKind): void { + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(compilationSettings); + return releaseDocumentWithKey(path, key, scriptKind); + } - ts.Debug.assert(entry.languageServiceRefCount >= 0); - if (entry.languageServiceRefCount === 0) { - if (isDocumentRegistryEntry(bucketEntry)) { - bucket.delete(path); - } - else { - bucketEntry.delete(scriptKind!); - if (bucketEntry.size === 1) { - bucket.set(path, ts.firstDefinedIterator(bucketEntry.values(), ts.identity)!); - } + function releaseDocumentWithKey(path: ts.Path, key: DocumentRegistryBucketKey, scriptKind?: ts.ScriptKind): void { + const bucket = ts.Debug.checkDefined(buckets.get(key)); + const bucketEntry = bucket.get(path)!; + const entry = getDocumentRegistryEntry(bucketEntry, scriptKind)!; + entry.languageServiceRefCount--; + + ts.Debug.assert(entry.languageServiceRefCount >= 0); + if (entry.languageServiceRefCount === 0) { + if (isDocumentRegistryEntry(bucketEntry)) { + bucket.delete(path); + } + else { + bucketEntry.delete(scriptKind!); + if (bucketEntry.size === 1) { + bucket.set(path, ts.firstDefinedIterator(bucketEntry.values(), ts.identity)!); } } } + } - function getLanguageServiceRefCounts(path: ts.Path, scriptKind: ts.ScriptKind) { - return ts.arrayFrom(buckets.entries(), ([key, bucket]): [ - string, - number | undefined - ] => { - const bucketEntry = bucket.get(path); - const entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind); - return [key, entry && entry.languageServiceRefCount]; - }); - } - - return { - acquireDocument, - acquireDocumentWithKey, - updateDocument, - updateDocumentWithKey, - releaseDocument, - releaseDocumentWithKey, - getLanguageServiceRefCounts, - reportStats, - getKeyForCompilationSettings - }; + function getLanguageServiceRefCounts(path: ts.Path, scriptKind: ts.ScriptKind) { + return ts.arrayFrom(buckets.entries(), ([key, bucket]): [ + string, + number | undefined + ] => { + const bucketEntry = bucket.get(path); + const entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind); + return [key, entry && entry.languageServiceRefCount]; + }); } - function compilerOptionValueToString(value: unknown): string { - if (value === null || typeof value !== "object") { // eslint-disable-line no-null/no-null - return "" + value; - } - if (ts.isArray(value)) { - return `[${ts.map(value, e => compilerOptionValueToString(e))?.join(",")}]`; - } - let str = "{"; - for (const key in value) { - if (ts.hasOwnProperty.call(value, key)) { // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - str += `${key}: ${compilerOptionValueToString((value as any)[key])}`; - } + return { + acquireDocument, + acquireDocumentWithKey, + updateDocument, + updateDocumentWithKey, + releaseDocument, + releaseDocumentWithKey, + getLanguageServiceRefCounts, + reportStats, + getKeyForCompilationSettings + }; +} + +function compilerOptionValueToString(value: unknown): string { + if (value === null || typeof value !== "object") { // eslint-disable-line no-null/no-null + return "" + value; + } + if (ts.isArray(value)) { + return `[${ts.map(value, e => compilerOptionValueToString(e))?.join(",")}]`; + } + let str = "{"; + for (const key in value) { + if (ts.hasOwnProperty.call(value, key)) { // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier + str += `${key}: ${compilerOptionValueToString((value as any)[key])}`; } - return str + "}"; } + return str + "}"; +} - function getKeyForCompilationSettings(settings: ts.CompilerOptions): DocumentRegistryBucketKey { - return ts.sourceFileAffectingCompilerOptions.map(option => compilerOptionValueToString(ts.getCompilerOptionValue(settings, option))).join("|") + (settings.pathsBasePath ? `|${settings.pathsBasePath}` : undefined) as DocumentRegistryBucketKey; - } +function getKeyForCompilationSettings(settings: ts.CompilerOptions): DocumentRegistryBucketKey { + return ts.sourceFileAffectingCompilerOptions.map(option => compilerOptionValueToString(ts.getCompilerOptionValue(settings, option))).join("|") + (settings.pathsBasePath ? `|${settings.pathsBasePath}` : undefined) as DocumentRegistryBucketKey; +} } diff --git a/src/services/exportAsModule.ts b/src/services/exportAsModule.ts index 7179e64d4fc57..2fe5370ae2da7 100644 --- a/src/services/exportAsModule.ts +++ b/src/services/exportAsModule.ts @@ -6,4 +6,4 @@ }; if (typeof module !== "undefined" && module.exports) { module.exports = ts; -} \ No newline at end of file +} diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index afe9d31056f33..56181c4d9b532 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -1,463 +1,463 @@ /*@internal*/ namespace ts { - export const enum ImportKind { - Named, - Default, - Namespace, - CommonJS - } +export const enum ImportKind { + Named, + Default, + Namespace, + CommonJS +} - export const enum ExportKind { - Named, - Default, - ExportEquals, - UMD - } +export const enum ExportKind { + Named, + Default, + ExportEquals, + UMD +} - export interface SymbolExportInfo { - readonly symbol: ts.Symbol; - readonly moduleSymbol: ts.Symbol; - /** Set if `moduleSymbol` is an external module, not an ambient module */ - moduleFileName: string | undefined; - exportKind: ExportKind; - targetFlags: ts.SymbolFlags; - /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ - isFromPackageJson: boolean; - } +export interface SymbolExportInfo { + readonly symbol: ts.Symbol; + readonly moduleSymbol: ts.Symbol; + /** Set if `moduleSymbol` is an external module, not an ambient module */ + moduleFileName: string | undefined; + exportKind: ExportKind; + targetFlags: ts.SymbolFlags; + /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ + isFromPackageJson: boolean; +} - interface CachedSymbolExportInfo { - // Used to rehydrate `symbol` and `moduleSymbol` when transient - id: number; - symbolName: string; - capitalizedSymbolName: string | undefined; - symbolTableKey: ts.__String; - moduleName: string; - moduleFile: ts.SourceFile | undefined; - packageName: string | undefined; - - // SymbolExportInfo, but optional symbols - readonly symbol: ts.Symbol | undefined; - readonly moduleSymbol: ts.Symbol | undefined; - moduleFileName: string | undefined; - exportKind: ExportKind; - targetFlags: ts.SymbolFlags; - isFromPackageJson: boolean; - } +interface CachedSymbolExportInfo { + // Used to rehydrate `symbol` and `moduleSymbol` when transient + id: number; + symbolName: string; + capitalizedSymbolName: string | undefined; + symbolTableKey: ts.__String; + moduleName: string; + moduleFile: ts.SourceFile | undefined; + packageName: string | undefined; + + // SymbolExportInfo, but optional symbols + readonly symbol: ts.Symbol | undefined; + readonly moduleSymbol: ts.Symbol | undefined; + moduleFileName: string | undefined; + exportKind: ExportKind; + targetFlags: ts.SymbolFlags; + isFromPackageJson: boolean; +} - export interface ExportInfoMap { - isUsableByFile(importingFile: ts.Path): boolean; - clear(): void; - add(importingFile: ts.Path, symbol: ts.Symbol, key: ts.__String, moduleSymbol: ts.Symbol, moduleFile: ts.SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: ts.TypeChecker): void; - get(importingFile: ts.Path, key: string): readonly SymbolExportInfo[] | undefined; - search(importingFile: ts.Path, preferCapitalized: boolean, matches: (name: string, targetFlags: ts.SymbolFlags) => boolean, action: (info: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean, key: string) => void): void; - releaseSymbols(): void; - isEmpty(): boolean; - /** @returns Whether the change resulted in the cache being cleared */ - onFileChanged(oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile, typeAcquisitionEnabled: boolean): boolean; - } +export interface ExportInfoMap { + isUsableByFile(importingFile: ts.Path): boolean; + clear(): void; + add(importingFile: ts.Path, symbol: ts.Symbol, key: ts.__String, moduleSymbol: ts.Symbol, moduleFile: ts.SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: ts.TypeChecker): void; + get(importingFile: ts.Path, key: string): readonly SymbolExportInfo[] | undefined; + search(importingFile: ts.Path, preferCapitalized: boolean, matches: (name: string, targetFlags: ts.SymbolFlags) => boolean, action: (info: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean, key: string) => void): void; + releaseSymbols(): void; + isEmpty(): boolean; + /** @returns Whether the change resulted in the cache being cleared */ + onFileChanged(oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile, typeAcquisitionEnabled: boolean): boolean; +} - export interface CacheableExportInfoMapHost { - getCurrentProgram(): ts.Program | undefined; - getPackageJsonAutoImportProvider(): ts.Program | undefined; - getGlobalTypingsCacheLocation(): string | undefined; - } +export interface CacheableExportInfoMapHost { + getCurrentProgram(): ts.Program | undefined; + getPackageJsonAutoImportProvider(): ts.Program | undefined; + getGlobalTypingsCacheLocation(): string | undefined; +} - export function createCacheableExportInfoMap(host: CacheableExportInfoMapHost): ExportInfoMap { - let exportInfoId = 1; - const exportInfo = ts.createMultiMap(); - const symbols = new ts.Map(); - /** - * Key: node_modules package name (no @types). - * Value: path to deepest node_modules folder seen that is - * both visible to `usableByFileName` and contains the package. - * - * Later, we can see if a given SymbolExportInfo is shadowed by - * a another installation of the same package in a deeper - * node_modules folder by seeing if its path starts with the - * value stored here. - */ - const packages = new ts.Map(); - let usableByFileName: ts.Path | undefined; - const cache: ExportInfoMap = { - isUsableByFile: importingFile => importingFile === usableByFileName, - isEmpty: () => !exportInfo.size, - clear: () => { - exportInfo.clear(); - symbols.clear(); - usableByFileName = undefined; - }, - add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, checker) => { - if (importingFile !== usableByFileName) { - cache.clear(); - usableByFileName = importingFile; - } +export function createCacheableExportInfoMap(host: CacheableExportInfoMapHost): ExportInfoMap { + let exportInfoId = 1; + const exportInfo = ts.createMultiMap(); + const symbols = new ts.Map(); + /** + * Key: node_modules package name (no @types). + * Value: path to deepest node_modules folder seen that is + * both visible to `usableByFileName` and contains the package. + * + * Later, we can see if a given SymbolExportInfo is shadowed by + * a another installation of the same package in a deeper + * node_modules folder by seeing if its path starts with the + * value stored here. + */ + const packages = new ts.Map(); + let usableByFileName: ts.Path | undefined; + const cache: ExportInfoMap = { + isUsableByFile: importingFile => importingFile === usableByFileName, + isEmpty: () => !exportInfo.size, + clear: () => { + exportInfo.clear(); + symbols.clear(); + usableByFileName = undefined; + }, + add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, checker) => { + if (importingFile !== usableByFileName) { + cache.clear(); + usableByFileName = importingFile; + } - let packageName; - if (moduleFile) { - const nodeModulesPathParts = ts.getNodeModulePathParts(moduleFile.fileName); - if (nodeModulesPathParts) { - const { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex } = nodeModulesPathParts; - packageName = ts.unmangleScopedPackageName(ts.getPackageNameFromTypesPackageName(moduleFile.fileName.substring(topLevelPackageNameIndex + 1, packageRootIndex))); - if (ts.startsWith(importingFile, moduleFile.path.substring(0, topLevelNodeModulesIndex))) { - const prevDeepestNodeModulesPath = packages.get(packageName); - const nodeModulesPath = moduleFile.fileName.substring(0, topLevelPackageNameIndex + 1); - if (prevDeepestNodeModulesPath) { - const prevDeepestNodeModulesIndex = prevDeepestNodeModulesPath.indexOf(ts.nodeModulesPathPart); - if (topLevelNodeModulesIndex > prevDeepestNodeModulesIndex) { - packages.set(packageName, nodeModulesPath); - } - } - else { + let packageName; + if (moduleFile) { + const nodeModulesPathParts = ts.getNodeModulePathParts(moduleFile.fileName); + if (nodeModulesPathParts) { + const { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex } = nodeModulesPathParts; + packageName = ts.unmangleScopedPackageName(ts.getPackageNameFromTypesPackageName(moduleFile.fileName.substring(topLevelPackageNameIndex + 1, packageRootIndex))); + if (ts.startsWith(importingFile, moduleFile.path.substring(0, topLevelNodeModulesIndex))) { + const prevDeepestNodeModulesPath = packages.get(packageName); + const nodeModulesPath = moduleFile.fileName.substring(0, topLevelPackageNameIndex + 1); + if (prevDeepestNodeModulesPath) { + const prevDeepestNodeModulesIndex = prevDeepestNodeModulesPath.indexOf(ts.nodeModulesPathPart); + if (topLevelNodeModulesIndex > prevDeepestNodeModulesIndex) { packages.set(packageName, nodeModulesPath); } } + else { + packages.set(packageName, nodeModulesPath); + } } } + } - const isDefault = exportKind === ExportKind.Default; - const namedSymbol = isDefault && ts.getLocalSymbolForExportDefault(symbol) || symbol; - // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. - // 2. A re-export merged with an export from a module augmentation can result in `symbol` - // being an external module symbol; the name it is re-exported by will be `symbolTableKey` - // (which comes from the keys of `moduleSymbol.exports`.) - // 3. Otherwise, we have a default/namespace import that can be imported by any name, and - // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to - // get a better name. - const names = exportKind === ExportKind.Named || ts.isExternalModuleSymbol(namedSymbol) - ? ts.unescapeLeadingUnderscores(symbolTableKey) - : ts.getNamesForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined); - - const symbolName = typeof names === "string" ? names : names[0]; - const capitalizedSymbolName = typeof names === "string" ? undefined : names[1]; - - const moduleName = ts.stripQuotes(moduleSymbol.name); - const id = exportInfoId++; - const target = ts.skipAlias(symbol, checker); - const storedSymbol = symbol.flags & ts.SymbolFlags.Transient ? undefined : symbol; - const storedModuleSymbol = moduleSymbol.flags & ts.SymbolFlags.Transient ? undefined : moduleSymbol; - if (!storedSymbol || !storedModuleSymbol) - symbols.set(id, [symbol, moduleSymbol]); - exportInfo.add(key(symbolName, symbol, ts.isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), { - id, - symbolTableKey, - symbolName, - capitalizedSymbolName, - moduleName, - moduleFile, - moduleFileName: moduleFile?.fileName, - packageName, - exportKind, - targetFlags: target.flags, - isFromPackageJson, - symbol: storedSymbol, - moduleSymbol: storedModuleSymbol, - }); - }, - get: (importingFile, key) => { - if (importingFile !== usableByFileName) - return; - const result = exportInfo.get(key); - return result?.map(rehydrateCachedInfo); - }, - search: (importingFile, preferCapitalized, matches, action) => { - if (importingFile !== usableByFileName) - return; - exportInfo.forEach((info, key) => { - const { symbolName, ambientModuleName } = parseKey(key); - const name = preferCapitalized && info[0].capitalizedSymbolName || symbolName; - if (matches(name, info[0].targetFlags)) { - const rehydrated = info.map(rehydrateCachedInfo); - const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName)); - if (filtered.length) { - action(filtered, name, !!ambientModuleName, key); - } + const isDefault = exportKind === ExportKind.Default; + const namedSymbol = isDefault && ts.getLocalSymbolForExportDefault(symbol) || symbol; + // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. + // 2. A re-export merged with an export from a module augmentation can result in `symbol` + // being an external module symbol; the name it is re-exported by will be `symbolTableKey` + // (which comes from the keys of `moduleSymbol.exports`.) + // 3. Otherwise, we have a default/namespace import that can be imported by any name, and + // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to + // get a better name. + const names = exportKind === ExportKind.Named || ts.isExternalModuleSymbol(namedSymbol) + ? ts.unescapeLeadingUnderscores(symbolTableKey) + : ts.getNamesForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined); + + const symbolName = typeof names === "string" ? names : names[0]; + const capitalizedSymbolName = typeof names === "string" ? undefined : names[1]; + + const moduleName = ts.stripQuotes(moduleSymbol.name); + const id = exportInfoId++; + const target = ts.skipAlias(symbol, checker); + const storedSymbol = symbol.flags & ts.SymbolFlags.Transient ? undefined : symbol; + const storedModuleSymbol = moduleSymbol.flags & ts.SymbolFlags.Transient ? undefined : moduleSymbol; + if (!storedSymbol || !storedModuleSymbol) + symbols.set(id, [symbol, moduleSymbol]); + exportInfo.add(key(symbolName, symbol, ts.isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), { + id, + symbolTableKey, + symbolName, + capitalizedSymbolName, + moduleName, + moduleFile, + moduleFileName: moduleFile?.fileName, + packageName, + exportKind, + targetFlags: target.flags, + isFromPackageJson, + symbol: storedSymbol, + moduleSymbol: storedModuleSymbol, + }); + }, + get: (importingFile, key) => { + if (importingFile !== usableByFileName) + return; + const result = exportInfo.get(key); + return result?.map(rehydrateCachedInfo); + }, + search: (importingFile, preferCapitalized, matches, action) => { + if (importingFile !== usableByFileName) + return; + exportInfo.forEach((info, key) => { + const { symbolName, ambientModuleName } = parseKey(key); + const name = preferCapitalized && info[0].capitalizedSymbolName || symbolName; + if (matches(name, info[0].targetFlags)) { + const rehydrated = info.map(rehydrateCachedInfo); + const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName)); + if (filtered.length) { + action(filtered, name, !!ambientModuleName, key); } - }); - }, - releaseSymbols: () => { - symbols.clear(); - }, - onFileChanged: (oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile, typeAcquisitionEnabled: boolean) => { - if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { - // File is purely global; doesn't affect export map - return false; } - if (usableByFileName && usableByFileName !== newSourceFile.path || - // If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node. - // Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list. - typeAcquisitionEnabled && ts.consumesNodeCoreModules(oldSourceFile) !== ts.consumesNodeCoreModules(newSourceFile) || - // Module agumentation and ambient module changes can add or remove exports available to be auto-imported. - // Changes elsewhere in the file can change the *type* of an export in a module augmentation, - // but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache. - !ts.arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || - !ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)) { - cache.clear(); - return true; - } - usableByFileName = newSourceFile.path; + }); + }, + releaseSymbols: () => { + symbols.clear(); + }, + onFileChanged: (oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile, typeAcquisitionEnabled: boolean) => { + if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { + // File is purely global; doesn't affect export map return false; - }, - }; - if (ts.Debug.isDebugging) { - Object.defineProperty(cache, "__cache", { get: () => exportInfo }); - } - return cache; - - function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo { - if (info.symbol && info.moduleSymbol) - return info as SymbolExportInfo; - const { id, exportKind, targetFlags, isFromPackageJson, moduleFileName } = info; - const [cachedSymbol, cachedModuleSymbol] = symbols.get(id) || ts.emptyArray; - if (cachedSymbol && cachedModuleSymbol) { - return { - symbol: cachedSymbol, - moduleSymbol: cachedModuleSymbol, - moduleFileName, - exportKind, - targetFlags, - isFromPackageJson, - }; } - const checker = (isFromPackageJson - ? host.getPackageJsonAutoImportProvider()! - : host.getCurrentProgram()!).getTypeChecker(); - const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || ts.Debug.checkDefined(info.moduleFile - ? checker.getMergedSymbol(info.moduleFile.symbol) - : checker.tryFindAmbientModule(info.moduleName)); - const symbol = info.symbol || cachedSymbol || ts.Debug.checkDefined(exportKind === ExportKind.ExportEquals - ? checker.resolveExternalModuleSymbol(moduleSymbol) - : checker.tryGetMemberInModuleExportsAndProperties(ts.unescapeLeadingUnderscores(info.symbolTableKey), moduleSymbol), `Could not find symbol '${info.symbolName}' by key '${info.symbolTableKey}' in module ${moduleSymbol.name}`); - symbols.set(id, [symbol, moduleSymbol]); + if (usableByFileName && usableByFileName !== newSourceFile.path || + // If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node. + // Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list. + typeAcquisitionEnabled && ts.consumesNodeCoreModules(oldSourceFile) !== ts.consumesNodeCoreModules(newSourceFile) || + // Module agumentation and ambient module changes can add or remove exports available to be auto-imported. + // Changes elsewhere in the file can change the *type* of an export in a module augmentation, + // but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache. + !ts.arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || + !ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)) { + cache.clear(); + return true; + } + usableByFileName = newSourceFile.path; + return false; + }, + }; + if (ts.Debug.isDebugging) { + Object.defineProperty(cache, "__cache", { get: () => exportInfo }); + } + return cache; + + function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo { + if (info.symbol && info.moduleSymbol) + return info as SymbolExportInfo; + const { id, exportKind, targetFlags, isFromPackageJson, moduleFileName } = info; + const [cachedSymbol, cachedModuleSymbol] = symbols.get(id) || ts.emptyArray; + if (cachedSymbol && cachedModuleSymbol) { return { - symbol, - moduleSymbol, + symbol: cachedSymbol, + moduleSymbol: cachedModuleSymbol, moduleFileName, exportKind, targetFlags, isFromPackageJson, }; } + const checker = (isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!).getTypeChecker(); + const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || ts.Debug.checkDefined(info.moduleFile + ? checker.getMergedSymbol(info.moduleFile.symbol) + : checker.tryFindAmbientModule(info.moduleName)); + const symbol = info.symbol || cachedSymbol || ts.Debug.checkDefined(exportKind === ExportKind.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(ts.unescapeLeadingUnderscores(info.symbolTableKey), moduleSymbol), `Could not find symbol '${info.symbolName}' by key '${info.symbolTableKey}' in module ${moduleSymbol.name}`); + symbols.set(id, [symbol, moduleSymbol]); + return { + symbol, + moduleSymbol, + moduleFileName, + exportKind, + targetFlags, + isFromPackageJson, + }; + } - function key(importedName: string, symbol: ts.Symbol, ambientModuleName: string | undefined, checker: ts.TypeChecker): string { - const moduleKey = ambientModuleName || ""; - return `${importedName}|${ts.getSymbolId(ts.skipAlias(symbol, checker))}|${moduleKey}`; - } + function key(importedName: string, symbol: ts.Symbol, ambientModuleName: string | undefined, checker: ts.TypeChecker): string { + const moduleKey = ambientModuleName || ""; + return `${importedName}|${ts.getSymbolId(ts.skipAlias(symbol, checker))}|${moduleKey}`; + } - function parseKey(key: string) { - const symbolName = key.substring(0, key.indexOf("|")); - const moduleKey = key.substring(key.lastIndexOf("|") + 1); - const ambientModuleName = moduleKey === "" ? undefined : moduleKey; - return { symbolName, ambientModuleName }; - } + function parseKey(key: string) { + const symbolName = key.substring(0, key.indexOf("|")); + const moduleKey = key.substring(key.lastIndexOf("|") + 1); + const ambientModuleName = moduleKey === "" ? undefined : moduleKey; + return { symbolName, ambientModuleName }; + } - function fileIsGlobalOnly(file: ts.SourceFile) { - return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames; - } + function fileIsGlobalOnly(file: ts.SourceFile) { + return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames; + } - function ambientModuleDeclarationsAreEqual(oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile) { - if (!ts.arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) { + function ambientModuleDeclarationsAreEqual(oldSourceFile: ts.SourceFile, newSourceFile: ts.SourceFile) { + if (!ts.arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) { + return false; + } + let oldFileStatementIndex = -1; + let newFileStatementIndex = -1; + for (const ambientModuleName of newSourceFile.ambientModuleNames) { + const isMatchingModuleDeclaration = (node: ts.Statement) => ts.isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName; + oldFileStatementIndex = ts.findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1); + newFileStatementIndex = ts.findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1); + if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) { return false; } - let oldFileStatementIndex = -1; - let newFileStatementIndex = -1; - for (const ambientModuleName of newSourceFile.ambientModuleNames) { - const isMatchingModuleDeclaration = (node: ts.Statement) => ts.isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName; - oldFileStatementIndex = ts.findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1); - newFileStatementIndex = ts.findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1); - if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) { - return false; - } - } - return true; } + return true; + } - function isNotShadowedByDeeperNodeModulesPackage(info: SymbolExportInfo, packageName: string | undefined) { - if (!packageName || !info.moduleFileName) - return true; - const typingsCacheLocation = host.getGlobalTypingsCacheLocation(); - if (typingsCacheLocation && ts.startsWith(info.moduleFileName, typingsCacheLocation)) - return true; - const packageDeepestNodeModulesPath = packages.get(packageName); - return !packageDeepestNodeModulesPath || ts.startsWith(info.moduleFileName, packageDeepestNodeModulesPath); - } + function isNotShadowedByDeeperNodeModulesPackage(info: SymbolExportInfo, packageName: string | undefined) { + if (!packageName || !info.moduleFileName) + return true; + const typingsCacheLocation = host.getGlobalTypingsCacheLocation(); + if (typingsCacheLocation && ts.startsWith(info.moduleFileName, typingsCacheLocation)) + return true; + const packageDeepestNodeModulesPath = packages.get(packageName); + return !packageDeepestNodeModulesPath || ts.startsWith(info.moduleFileName, packageDeepestNodeModulesPath); + } +} +export function isImportableFile(program: ts.Program, from: ts.SourceFile, to: ts.SourceFile, preferences: ts.UserPreferences, packageJsonFilter: ts.PackageJsonImportFilter | undefined, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost, moduleSpecifierCache: ts.ModuleSpecifierCache | undefined): boolean { + if (from === to) + return false; + const cachedResult = moduleSpecifierCache?.get(from.path, to.path, preferences, {}); + if (cachedResult?.isBlockedByPackageJsonDependencies !== undefined) { + return !cachedResult.isBlockedByPackageJsonDependencies; } - export function isImportableFile(program: ts.Program, from: ts.SourceFile, to: ts.SourceFile, preferences: ts.UserPreferences, packageJsonFilter: ts.PackageJsonImportFilter | undefined, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost, moduleSpecifierCache: ts.ModuleSpecifierCache | undefined): boolean { - if (from === to) - return false; - const cachedResult = moduleSpecifierCache?.get(from.path, to.path, preferences, {}); - if (cachedResult?.isBlockedByPackageJsonDependencies !== undefined) { - return !cachedResult.isBlockedByPackageJsonDependencies; - } - const getCanonicalFileName = ts.hostGetCanonicalFileName(moduleSpecifierResolutionHost); - const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.(); - const hasImportablePath = !!ts.moduleSpecifiers.forEachFileNameOfModule(from.fileName, to.fileName, moduleSpecifierResolutionHost, - /*preferSymlinks*/ false, toPath => { - const toFile = program.getSourceFile(toPath); - // Determine to import using toPath only if toPath is what we were looking at - // or there doesnt exist the file in the program by the symlink - return (toFile === to || !toFile) && - isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache); - }); + const getCanonicalFileName = ts.hostGetCanonicalFileName(moduleSpecifierResolutionHost); + const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.(); + const hasImportablePath = !!ts.moduleSpecifiers.forEachFileNameOfModule(from.fileName, to.fileName, moduleSpecifierResolutionHost, + /*preferSymlinks*/ false, toPath => { + const toFile = program.getSourceFile(toPath); + // Determine to import using toPath only if toPath is what we were looking at + // or there doesnt exist the file in the program by the symlink + return (toFile === to || !toFile) && + isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache); + }); + + if (packageJsonFilter) { + const isAutoImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost); + moduleSpecifierCache?.setBlockedByPackageJsonDependencies(from.path, to.path, preferences, {}, !isAutoImportable); + return isAutoImportable; + } - if (packageJsonFilter) { - const isAutoImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost); - moduleSpecifierCache?.setBlockedByPackageJsonDependencies(from.path, to.path, preferences, {}, !isAutoImportable); - return isAutoImportable; - } + return hasImportablePath; +} - return hasImportablePath; +/** + * Don't include something from a `node_modules` that isn't actually reachable by a global import. + * A relative import to node_modules is usually a bad idea. + */ +function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: ts.GetCanonicalFileName, globalCachePath?: string): boolean { + // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. + const toNodeModules = ts.forEachAncestorDirectory(toPath, ancestor => ts.getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined); + const toNodeModulesParent = toNodeModules && ts.getDirectoryPath(getCanonicalFileName(toNodeModules)); + return toNodeModulesParent === undefined + || ts.startsWith(getCanonicalFileName(fromPath), toNodeModulesParent) + || (!!globalCachePath && ts.startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent)); +} +export function forEachExternalModuleToImportFrom(program: ts.Program, host: ts.LanguageServiceHost, useAutoImportProvider: boolean, cb: (module: ts.Symbol, moduleFile: ts.SourceFile | undefined, program: ts.Program, isFromPackageJson: boolean) => void) { + forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); + const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); + if (autoImportProvider) { + const start = ts.timestamp(); + forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); + host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${ts.timestamp() - start}`); } +} - /** - * Don't include something from a `node_modules` that isn't actually reachable by a global import. - * A relative import to node_modules is usually a bad idea. - */ - function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: ts.GetCanonicalFileName, globalCachePath?: string): boolean { - // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. - const toNodeModules = ts.forEachAncestorDirectory(toPath, ancestor => ts.getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined); - const toNodeModulesParent = toNodeModules && ts.getDirectoryPath(getCanonicalFileName(toNodeModules)); - return toNodeModulesParent === undefined - || ts.startsWith(getCanonicalFileName(fromPath), toNodeModulesParent) - || (!!globalCachePath && ts.startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent)); - } - export function forEachExternalModuleToImportFrom(program: ts.Program, host: ts.LanguageServiceHost, useAutoImportProvider: boolean, cb: (module: ts.Symbol, moduleFile: ts.SourceFile | undefined, program: ts.Program, isFromPackageJson: boolean) => void) { - forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); - const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); - if (autoImportProvider) { - const start = ts.timestamp(); - forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); - host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${ts.timestamp() - start}`); +function forEachExternalModule(checker: ts.TypeChecker, allSourceFiles: readonly ts.SourceFile[], cb: (module: ts.Symbol, sourceFile: ts.SourceFile | undefined) => void) { + for (const ambient of checker.getAmbientModules()) { + if (!ts.stringContains(ambient.name, "*")) { + cb(ambient, /*sourceFile*/ undefined); } } - - function forEachExternalModule(checker: ts.TypeChecker, allSourceFiles: readonly ts.SourceFile[], cb: (module: ts.Symbol, sourceFile: ts.SourceFile | undefined) => void) { - for (const ambient of checker.getAmbientModules()) { - if (!ts.stringContains(ambient.name, "*")) { - cb(ambient, /*sourceFile*/ undefined); - } - } - for (const sourceFile of allSourceFiles) { - if (ts.isExternalOrCommonJsModule(sourceFile)) { - cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile); - } + for (const sourceFile of allSourceFiles) { + if (ts.isExternalOrCommonJsModule(sourceFile)) { + cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile); } } +} - export function getExportInfoMap(importingFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, cancellationToken: ts.CancellationToken | undefined): ExportInfoMap { - const start = ts.timestamp(); - // Pulling the AutoImportProvider project will trigger its updateGraph if pending, - // which will invalidate the export map cache if things change, so pull it before - // checking the cache. - host.getPackageJsonAutoImportProvider?.(); - const cache = host.getCachedExportInfoMap?.() || createCacheableExportInfoMap({ - getCurrentProgram: () => program, - getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), - getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), - }); - - if (cache.isUsableByFile(importingFile.path)) { - host.log?.("getExportInfoMap: cache hit"); - return cache; - } +export function getExportInfoMap(importingFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, cancellationToken: ts.CancellationToken | undefined): ExportInfoMap { + const start = ts.timestamp(); + // Pulling the AutoImportProvider project will trigger its updateGraph if pending, + // which will invalidate the export map cache if things change, so pull it before + // checking the cache. + host.getPackageJsonAutoImportProvider?.(); + const cache = host.getCachedExportInfoMap?.() || createCacheableExportInfoMap({ + getCurrentProgram: () => program, + getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), + getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), + }); + + if (cache.isUsableByFile(importingFile.path)) { + host.log?.("getExportInfoMap: cache hit"); + return cache; + } - host.log?.("getExportInfoMap: cache miss or empty; calculating new results"); - const compilerOptions = program.getCompilerOptions(); - let moduleCount = 0; - try { - forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { - if (++moduleCount % 100 === 0) - cancellationToken?.throwIfCancellationRequested(); - const seenExports = new ts.Map(); - const checker = program.getTypeChecker(); - const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); - // Note: I think we shouldn't actually see resolved module symbols here, but weird merges - // can cause it to happen: see 'completionsImport_mergedReExport.ts' - if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { - cache.add(importingFile.path, defaultInfo.symbol, defaultInfo.exportKind === ExportKind.Default ? ts.InternalSymbolName.Default : ts.InternalSymbolName.ExportEquals, moduleSymbol, moduleFile, defaultInfo.exportKind, isFromPackageJson, checker); + host.log?.("getExportInfoMap: cache miss or empty; calculating new results"); + const compilerOptions = program.getCompilerOptions(); + let moduleCount = 0; + try { + forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { + if (++moduleCount % 100 === 0) + cancellationToken?.throwIfCancellationRequested(); + const seenExports = new ts.Map(); + const checker = program.getTypeChecker(); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); + // Note: I think we shouldn't actually see resolved module symbols here, but weird merges + // can cause it to happen: see 'completionsImport_mergedReExport.ts' + if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { + cache.add(importingFile.path, defaultInfo.symbol, defaultInfo.exportKind === ExportKind.Default ? ts.InternalSymbolName.Default : ts.InternalSymbolName.ExportEquals, moduleSymbol, moduleFile, defaultInfo.exportKind, isFromPackageJson, checker); + } + checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { + if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && ts.addToSeen(seenExports, key)) { + cache.add(importingFile.path, exported, key, moduleSymbol, moduleFile, ExportKind.Named, isFromPackageJson, checker); } - checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { - if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && ts.addToSeen(seenExports, key)) { - cache.add(importingFile.path, exported, key, moduleSymbol, moduleFile, ExportKind.Named, isFromPackageJson, checker); - } - }); }); - } - catch (err) { - // Ensure cache is reset if operation is cancelled - cache.clear(); - throw err; - } - - host.log?.(`getExportInfoMap: done in ${ts.timestamp() - start} ms`); - return cache; + }); } - - export function getDefaultLikeExportInfo(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions) { - const exported = getDefaultLikeExportWorker(moduleSymbol, checker); - if (!exported) - return undefined; - const { symbol, exportKind } = exported; - const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); - return info && { symbol, exportKind, ...info }; + catch (err) { + // Ensure cache is reset if operation is cancelled + cache.clear(); + throw err; } - function isImportableSymbol(symbol: ts.Symbol, checker: ts.TypeChecker) { - return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !ts.isKnownSymbol(symbol) && !ts.isPrivateIdentifierSymbol(symbol); - } + host.log?.(`getExportInfoMap: done in ${ts.timestamp() - start} ms`); + return cache; +} - function getDefaultLikeExportWorker(moduleSymbol: ts.Symbol, checker: ts.TypeChecker): { - readonly symbol: ts.Symbol; - readonly exportKind: ExportKind; - } | undefined { - const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); - if (exportEquals !== moduleSymbol) - return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; - const defaultExport = checker.tryGetMemberInModuleExports(ts.InternalSymbolName.Default, moduleSymbol); - if (defaultExport) - return { symbol: defaultExport, exportKind: ExportKind.Default }; - } - function getDefaultExportInfoWorker(defaultExport: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions): { - readonly symbolForMeaning: ts.Symbol; - readonly name: string; - } | undefined { - const localSymbol = ts.getLocalSymbolForExportDefault(defaultExport); - if (localSymbol) - return { symbolForMeaning: localSymbol, name: localSymbol.name }; - - const name = getNameForExportDefault(defaultExport); - if (name !== undefined) - return { symbolForMeaning: defaultExport, name }; - if (defaultExport.flags & ts.SymbolFlags.Alias) { - const aliased = checker.getImmediateAliasedSymbol(defaultExport); - if (aliased && aliased.parent) { - // - `aliased` will be undefined if the module is exporting an unresolvable name, - // but we can still offer completions for it. - // - `aliased.parent` will be undefined if the module is exporting `globalThis.something`, - // or another expression that resolves to a global. - return getDefaultExportInfoWorker(aliased, checker, compilerOptions); - } - } +export function getDefaultLikeExportInfo(moduleSymbol: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions) { + const exported = getDefaultLikeExportWorker(moduleSymbol, checker); + if (!exported) + return undefined; + const { symbol, exportKind } = exported; + const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); + return info && { symbol, exportKind, ...info }; +} + +function isImportableSymbol(symbol: ts.Symbol, checker: ts.TypeChecker) { + return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !ts.isKnownSymbol(symbol) && !ts.isPrivateIdentifierSymbol(symbol); +} - if (defaultExport.escapedName !== ts.InternalSymbolName.Default && - defaultExport.escapedName !== ts.InternalSymbolName.ExportEquals) { - return { symbolForMeaning: defaultExport, name: defaultExport.getName() }; +function getDefaultLikeExportWorker(moduleSymbol: ts.Symbol, checker: ts.TypeChecker): { + readonly symbol: ts.Symbol; + readonly exportKind: ExportKind; +} | undefined { + const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) + return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; + const defaultExport = checker.tryGetMemberInModuleExports(ts.InternalSymbolName.Default, moduleSymbol); + if (defaultExport) + return { symbol: defaultExport, exportKind: ExportKind.Default }; +} +function getDefaultExportInfoWorker(defaultExport: ts.Symbol, checker: ts.TypeChecker, compilerOptions: ts.CompilerOptions): { + readonly symbolForMeaning: ts.Symbol; + readonly name: string; +} | undefined { + const localSymbol = ts.getLocalSymbolForExportDefault(defaultExport); + if (localSymbol) + return { symbolForMeaning: localSymbol, name: localSymbol.name }; + + const name = getNameForExportDefault(defaultExport); + if (name !== undefined) + return { symbolForMeaning: defaultExport, name }; + if (defaultExport.flags & ts.SymbolFlags.Alias) { + const aliased = checker.getImmediateAliasedSymbol(defaultExport); + if (aliased && aliased.parent) { + // - `aliased` will be undefined if the module is exporting an unresolvable name, + // but we can still offer completions for it. + // - `aliased.parent` will be undefined if the module is exporting `globalThis.something`, + // or another expression that resolves to a global. + return getDefaultExportInfoWorker(aliased, checker, compilerOptions); } - return { symbolForMeaning: defaultExport, name: ts.getNameForExportedSymbol(defaultExport, compilerOptions.target) }; } - function getNameForExportDefault(symbol: ts.Symbol): string | undefined { - return symbol.declarations && ts.firstDefined(symbol.declarations, declaration => { - if (ts.isExportAssignment(declaration)) { - return ts.tryCast(ts.skipOuterExpressions(declaration.expression), ts.isIdentifier)?.text; - } - else if (ts.isExportSpecifier(declaration)) { - ts.Debug.assert(declaration.name.text === ts.InternalSymbolName.Default, "Expected the specifier to be a default export"); - return declaration.propertyName && declaration.propertyName.text; - } - }); + if (defaultExport.escapedName !== ts.InternalSymbolName.Default && + defaultExport.escapedName !== ts.InternalSymbolName.ExportEquals) { + return { symbolForMeaning: defaultExport, name: defaultExport.getName() }; } + return { symbolForMeaning: defaultExport, name: ts.getNameForExportedSymbol(defaultExport, compilerOptions.target) }; +} + +function getNameForExportDefault(symbol: ts.Symbol): string | undefined { + return symbol.declarations && ts.firstDefined(symbol.declarations, declaration => { + if (ts.isExportAssignment(declaration)) { + return ts.tryCast(ts.skipOuterExpressions(declaration.expression), ts.isIdentifier)?.text; + } + else if (ts.isExportSpecifier(declaration)) { + ts.Debug.assert(declaration.name.text === ts.InternalSymbolName.Default, "Expected the specifier to be a default export"); + return declaration.propertyName && declaration.propertyName.text; + } + }); +} } diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 9fb0b22d8de6f..cd3469448b926 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1,2403 +1,2403 @@ /* @internal */ namespace ts.FindAllReferences { - export interface SymbolAndEntries { - readonly definition: Definition | undefined; - readonly references: readonly Entry[]; +export interface SymbolAndEntries { + readonly definition: Definition | undefined; + readonly references: readonly Entry[]; +} + +export const enum DefinitionKind { + Symbol, + Label, + Keyword, + This, + String, + TripleSlashReference +} +export type Definition = { + readonly type: DefinitionKind.Symbol; + readonly symbol: ts.Symbol; +} | { + readonly type: DefinitionKind.Label; + readonly node: ts.Identifier; +} | { + readonly type: DefinitionKind.Keyword; + readonly node: ts.Node; +} | { + readonly type: DefinitionKind.This; + readonly node: ts.Node; +} | { + readonly type: DefinitionKind.String; + readonly node: ts.StringLiteralLike; +} | { + readonly type: DefinitionKind.TripleSlashReference; + readonly reference: ts.FileReference; + readonly file: ts.SourceFile; +}; +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: ts.Node; + end: ts.Node; +} +export type ContextNode = ts.Node | ContextWithStartAndEndNode; +export interface NodeEntry { + readonly kind: NodeEntryKind; + readonly node: ts.Node; + readonly context?: ContextNode; +} +export interface SpanEntry { + readonly kind: EntryKind.Span; + readonly fileName: string; + readonly textSpan: ts.TextSpan; +} +export function nodeEntry(node: ts.Node, kind: NodeEntryKind = EntryKind.Node): NodeEntry { + return { + kind, + node: (node as ts.NamedDeclaration).name || node, + context: getContextNodeForNodeEntry(node) + }; +} + +export function isContextWithStartAndEndNode(node: ContextNode): node is ContextWithStartAndEndNode { + return node && (node as ts.Node).kind === undefined; +} + +function getContextNodeForNodeEntry(node: ts.Node): ContextNode | undefined { + if (ts.isDeclaration(node)) { + return getContextNode(node); } - export const enum DefinitionKind { - Symbol, - Label, - Keyword, - This, - String, - TripleSlashReference + if (!node.parent) + return undefined; + if (!ts.isDeclaration(node.parent) && !ts.isExportAssignment(node.parent)) { + // Special property assignment in javascript + if (ts.isInJSFile(node)) { + const binaryExpression = ts.isBinaryExpression(node.parent) ? + node.parent : + ts.isAccessExpression(node.parent) && + ts.isBinaryExpression(node.parent.parent) && + node.parent.parent.left === node.parent ? + node.parent.parent : + undefined; + if (binaryExpression && ts.getAssignmentDeclarationKind(binaryExpression) !== ts.AssignmentDeclarationKind.None) { + return getContextNode(binaryExpression); + } + } + + // Jsx Tags + if (ts.isJsxOpeningElement(node.parent) || ts.isJsxClosingElement(node.parent)) { + return node.parent.parent; + } + else if (ts.isJsxSelfClosingElement(node.parent) || + ts.isLabeledStatement(node.parent) || + ts.isBreakOrContinueStatement(node.parent)) { + return node.parent; + } + else if (ts.isStringLiteralLike(node)) { + const validImport = ts.tryGetImportFromModuleSpecifier(node); + if (validImport) { + const declOrStatement = ts.findAncestor(validImport, node => ts.isDeclaration(node) || + ts.isStatement(node) || + ts.isJSDocTag(node))! as ts.NamedDeclaration | ts.Statement | ts.JSDocTag; + return ts.isDeclaration(declOrStatement) ? + getContextNode(declOrStatement) : + declOrStatement; + } + } + + // Handle computed property name + const propertyName = ts.findAncestor(node, ts.isComputedPropertyName); + return propertyName ? + getContextNode(propertyName.parent) : + undefined; } - export type Definition = { - readonly type: DefinitionKind.Symbol; - readonly symbol: ts.Symbol; - } | { - readonly type: DefinitionKind.Label; - readonly node: ts.Identifier; - } | { - readonly type: DefinitionKind.Keyword; - readonly node: ts.Node; - } | { - readonly type: DefinitionKind.This; - readonly node: ts.Node; - } | { - readonly type: DefinitionKind.String; - readonly node: ts.StringLiteralLike; - } | { - readonly type: DefinitionKind.TripleSlashReference; - readonly reference: ts.FileReference; - readonly file: ts.SourceFile; - }; - 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: ts.Node; - end: ts.Node; - } - export type ContextNode = ts.Node | ContextWithStartAndEndNode; - export interface NodeEntry { - readonly kind: NodeEntryKind; - readonly node: ts.Node; - readonly context?: ContextNode; - } - export interface SpanEntry { - readonly kind: EntryKind.Span; - readonly fileName: string; - readonly textSpan: ts.TextSpan; - } - export function nodeEntry(node: ts.Node, kind: NodeEntryKind = EntryKind.Node): NodeEntry { - return { - kind, - node: (node as ts.NamedDeclaration).name || node, - context: getContextNodeForNodeEntry(node) - }; + + if (node.parent.name === node || // node is name of declaration, use parent + ts.isConstructorDeclaration(node.parent) || + ts.isExportAssignment(node.parent) || + // Property name of the import export specifier or binding pattern, use parent + ((ts.isImportOrExportSpecifier(node.parent) || ts.isBindingElement(node.parent)) + && node.parent.propertyName === node) || + // Is default export + (node.kind === ts.SyntaxKind.DefaultKeyword && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.ExportDefault))) { + return getContextNode(node.parent); } - export function isContextWithStartAndEndNode(node: ContextNode): node is ContextWithStartAndEndNode { - return node && (node as ts.Node).kind === undefined; + return undefined; +} + +export function getContextNode(node: ts.NamedDeclaration | ts.BinaryExpression | ts.ForInOrOfStatement | undefined): ContextNode | undefined { + if (!node) + return undefined; + switch (node.kind) { + case ts.SyntaxKind.VariableDeclaration: + return !ts.isVariableDeclarationList(node.parent) || node.parent.declarations.length !== 1 ? + node : + ts.isVariableStatement(node.parent.parent) ? + node.parent.parent : + ts.isForInOrOfStatement(node.parent.parent) ? + getContextNode(node.parent.parent) : + node.parent; + + case ts.SyntaxKind.BindingElement: + return getContextNode(node.parent.parent as ts.NamedDeclaration); + case ts.SyntaxKind.ImportSpecifier: + return node.parent.parent.parent; + + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.NamespaceImport: + return node.parent.parent; + + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceExport: + return node.parent; + + case ts.SyntaxKind.BinaryExpression: + return ts.isExpressionStatement(node.parent) ? + node.parent : + node; + + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.ForInStatement: + return { + start: (node as ts.ForInOrOfStatement).initializer, + end: (node as ts.ForInOrOfStatement).expression + }; + + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + return ts.isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent) ? + getContextNode(ts.findAncestor(node.parent, node => ts.isBinaryExpression(node) || ts.isForInOrOfStatement(node)) as ts.BinaryExpression | ts.ForInOrOfStatement) : + node; + + default: + return node; } +} - function getContextNodeForNodeEntry(node: ts.Node): ContextNode | undefined { - if (ts.isDeclaration(node)) { - return getContextNode(node); - } +export function toContextSpan(textSpan: ts.TextSpan, sourceFile: ts.SourceFile, context?: ContextNode): { + contextSpan: ts.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; +} - if (!node.parent) - return undefined; - if (!ts.isDeclaration(node.parent) && !ts.isExportAssignment(node.parent)) { - // Special property assignment in javascript - if (ts.isInJSFile(node)) { - const binaryExpression = ts.isBinaryExpression(node.parent) ? - node.parent : - ts.isAccessExpression(node.parent) && - ts.isBinaryExpression(node.parent.parent) && - node.parent.parent.left === node.parent ? - node.parent.parent : - undefined; - if (binaryExpression && ts.getAssignmentDeclarationKind(binaryExpression) !== ts.AssignmentDeclarationKind.None) { - return getContextNode(binaryExpression); - } - } +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 +} - // Jsx Tags - if (ts.isJsxOpeningElement(node.parent) || ts.isJsxClosingElement(node.parent)) { - return node.parent.parent; - } - else if (ts.isJsxSelfClosingElement(node.parent) || - ts.isLabeledStatement(node.parent) || - ts.isBreakOrContinueStatement(node.parent)) { - return node.parent; - } - else if (ts.isStringLiteralLike(node)) { - const validImport = ts.tryGetImportFromModuleSpecifier(node); - if (validImport) { - const declOrStatement = ts.findAncestor(validImport, node => ts.isDeclaration(node) || - ts.isStatement(node) || - ts.isJSDocTag(node))! as ts.NamedDeclaration | ts.Statement | ts.JSDocTag; - return ts.isDeclaration(declOrStatement) ? - getContextNode(declOrStatement) : - declOrStatement; - } - } +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; +} - // Handle computed property name - const propertyName = ts.findAncestor(node, ts.isComputedPropertyName); - return propertyName ? - getContextNode(propertyName.parent) : - undefined; - } +export function findReferencedSymbols(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], sourceFile: ts.SourceFile, position: number): ts.ReferencedSymbol[] | undefined { + const node = ts.getTouchingPropertyName(sourceFile, position); + const options = { use: FindReferencesUse.References }; + const referencedSymbols = Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options); + const checker = program.getTypeChecker(); + // Unless the starting node is a declaration (vs e.g. JSDoc), don't attempt to compute isDefinition + const adjustedNode = Core.getAdjustedNode(node, options); + const symbol = isDefinitionForReference(adjustedNode) ? checker.getSymbolAtLocation(adjustedNode) : undefined; + return !referencedSymbols || !referencedSymbols.length ? undefined : ts.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(r => toReferencedSymbolEntry(r, symbol)) + }); +} - if (node.parent.name === node || // node is name of declaration, use parent - ts.isConstructorDeclaration(node.parent) || - ts.isExportAssignment(node.parent) || - // Property name of the import export specifier or binding pattern, use parent - ((ts.isImportOrExportSpecifier(node.parent) || ts.isBindingElement(node.parent)) - && node.parent.propertyName === node) || - // Is default export - (node.kind === ts.SyntaxKind.DefaultKeyword && ts.hasSyntacticModifier(node.parent, ts.ModifierFlags.ExportDefault))) { - return getContextNode(node.parent); +function isDefinitionForReference(node: ts.Node): boolean { + return node.kind === ts.SyntaxKind.DefaultKeyword + || !!ts.getDeclarationFromName(node) + || ts.isLiteralComputedPropertyDeclarationName(node) + || (node.kind === ts.SyntaxKind.ConstructorKeyword && ts.isConstructorDeclaration(node.parent)); +} + +export function getImplementationsAtPosition(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], sourceFile: ts.SourceFile, position: number): ts.ImplementationLocation[] | undefined { + const node = ts.getTouchingPropertyName(sourceFile, position); + let referenceEntries: Entry[] | undefined; + const entries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position); + + if (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression + || node.parent.kind === ts.SyntaxKind.BindingElement + || node.parent.kind === ts.SyntaxKind.ElementAccessExpression + || node.kind === ts.SyntaxKind.SuperKeyword) { + referenceEntries = entries && [...entries]; + } + else { + const queue = entries && [...entries]; + const seenNodes = new ts.Map(); + while (queue && queue.length) { + const entry = queue.shift() as NodeEntry; + if (!ts.addToSeen(seenNodes, ts.getNodeId(entry.node))) { + continue; + } + referenceEntries = ts.append(referenceEntries, entry); + const entries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, entry.node, entry.node.pos); + if (entries) { + queue.push(...entries); + } } + } + const checker = program.getTypeChecker(); + return ts.map(referenceEntries, entry => toImplementationLocation(entry, checker)); +} +function getImplementationReferenceEntries(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], node: ts.Node, position: number): readonly Entry[] | undefined { + if (node.kind === ts.SyntaxKind.SourceFile) { return undefined; } - export function getContextNode(node: ts.NamedDeclaration | ts.BinaryExpression | ts.ForInOrOfStatement | undefined): ContextNode | undefined { - if (!node) - return undefined; - switch (node.kind) { - case ts.SyntaxKind.VariableDeclaration: - return !ts.isVariableDeclarationList(node.parent) || node.parent.declarations.length !== 1 ? - node : - ts.isVariableStatement(node.parent.parent) ? - node.parent.parent : - ts.isForInOrOfStatement(node.parent.parent) ? - getContextNode(node.parent.parent) : - node.parent; - - case ts.SyntaxKind.BindingElement: - return getContextNode(node.parent.parent as ts.NamedDeclaration); - case ts.SyntaxKind.ImportSpecifier: - return node.parent.parent.parent; - - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.NamespaceImport: - return node.parent.parent; - - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceExport: - return node.parent; - - case ts.SyntaxKind.BinaryExpression: - return ts.isExpressionStatement(node.parent) ? - node.parent : - node; - - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForInStatement: + 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 === ts.SyntaxKind.ShorthandPropertyAssignment) { + const result: NodeEntry[] = []; + Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node))); + return result; + } + else if (node.kind === ts.SyntaxKind.SuperKeyword || ts.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: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], node: ts.Node, position: number, options: Options | undefined, convertEntry: ToReferenceOrRenameEntry): T[] | undefined { + return ts.map(flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options)), entry => convertEntry(entry, node, program.getTypeChecker())); +} +export type ToReferenceOrRenameEntry = (entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker) => T; +export function getReferenceEntriesForNode(position: number, node: ts.Node, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(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 && ts.flatMap(referenceSymbols, r => r.references); +} +function definitionToReferencedSymbolDefinitionInfo(def: Definition, checker: ts.TypeChecker, originalNode: ts.Node): ts.ReferencedSymbolDefinitionInfo { + const info = ((): { + sourceFile: ts.SourceFile; + textSpan: ts.TextSpan; + name: string; + kind: ts.ScriptElementKind; + displayParts: ts.SymbolDisplayPart[]; + context?: ts.Node | ContextWithStartAndEndNode; + } => { + 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 && ts.firstOrUndefined(symbol.declarations); + const node = declaration ? (ts.getNameOfDeclaration(declaration) || declaration) : originalNode; return { - start: (node as ts.ForInOrOfStatement).initializer, - end: (node as ts.ForInOrOfStatement).expression + ...getFileAndTextSpanFromNode(node), + name, + kind, + displayParts, + context: getContextNode(declaration) }; + } + case DefinitionKind.Label: { + const { node } = def; + return { ...getFileAndTextSpanFromNode(node), name: node.text, kind: ts.ScriptElementKind.label, displayParts: [ts.displayPart(node.text, ts.SymbolDisplayPartKind.text)] }; + } + case DefinitionKind.Keyword: { + const { node } = def; + const name = ts.tokenToString(node.kind)!; + return { ...getFileAndTextSpanFromNode(node), name, kind: ts.ScriptElementKind.keyword, displayParts: [{ text: name, kind: ts.ScriptElementKind.keyword }] }; + } + case DefinitionKind.This: { + const { node } = def; + const symbol = checker.getSymbolAtLocation(node); + const displayParts = symbol && ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, node.getSourceFile(), ts.getContainerNode(node), node).displayParts || [ts.textPart("this")]; + return { ...getFileAndTextSpanFromNode(node), name: "this", kind: ts.ScriptElementKind.variableElement, displayParts }; + } + case DefinitionKind.String: { + const { node } = def; + return { + ...getFileAndTextSpanFromNode(node), + name: node.text, + kind: ts.ScriptElementKind.variableElement, + displayParts: [ts.displayPart(ts.getTextOfNode(node), ts.SymbolDisplayPartKind.stringLiteral)] + }; + } + case DefinitionKind.TripleSlashReference: { + return { + textSpan: ts.createTextSpanFromRange(def.reference), + sourceFile: def.file, + name: def.reference.fileName, + kind: ts.ScriptElementKind.string, + displayParts: [ts.displayPart(`"${def.reference.fileName}"`, ts.SymbolDisplayPartKind.stringLiteral)] + }; + } + default: + return ts.Debug.assertNever(def); + } + })(); + + const { sourceFile, textSpan, name, kind, displayParts, context } = info; + return { + containerKind: ts.ScriptElementKind.unknown, + containerName: "", + fileName: sourceFile.fileName, + kind, + name, + textSpan, + displayParts, + ...toContextSpan(textSpan, sourceFile, context) + }; +} - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - return ts.isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent) ? - getContextNode(ts.findAncestor(node.parent, node => ts.isBinaryExpression(node) || ts.isForInOrOfStatement(node)) as ts.BinaryExpression | ts.ForInOrOfStatement) : - node; +function getFileAndTextSpanFromNode(node: ts.Node) { + const sourceFile = node.getSourceFile(); + return { + sourceFile, + textSpan: getTextSpan(ts.isComputedPropertyName(node) ? node.expression : node, sourceFile) + }; +} - default: - return node; - } - } +function getDefinitionKindAndDisplayParts(symbol: ts.Symbol, checker: ts.TypeChecker, node: ts.Node): { + displayParts: ts.SymbolDisplayPart[]; + kind: ts.ScriptElementKind; +} { + const meaning = Core.getIntersectingMeaningFromDeclarations(node, symbol); + const enclosingDeclaration = symbol.declarations && ts.firstOrUndefined(symbol.declarations) || node; + const { displayParts, symbolKind } = ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, enclosingDeclaration.getSourceFile(), enclosingDeclaration, enclosingDeclaration, meaning); + return { displayParts, kind: symbolKind }; +} - export function toContextSpan(textSpan: ts.TextSpan, sourceFile: ts.SourceFile, context?: ContextNode): { - contextSpan: ts.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; - } +export function toRenameLocation(entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker, providePrefixAndSuffixText: boolean): ts.RenameLocation { + return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) }; +} - 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 - } +function toReferencedSymbolEntry(entry: Entry, symbol: ts.Symbol | undefined): ts.ReferencedSymbolEntry { + const referenceEntry = toReferenceEntry(entry); + if (!symbol) + return referenceEntry; + return { + ...referenceEntry, + isDefinition: entry.kind !== EntryKind.Span && isDeclarationOfSymbol(entry.node, symbol) + }; +} - 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; +export function toReferenceEntry(entry: Entry): ts.ReferenceEntry { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind === EntryKind.Span) { + return { ...documentSpan, isWriteAccess: false }; } + const { kind, node } = entry; + return { + ...documentSpan, + isWriteAccess: isWriteAccessForReference(node), + isInString: kind === EntryKind.StringLiteral ? true : undefined, + }; +} - export function findReferencedSymbols(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], sourceFile: ts.SourceFile, position: number): ts.ReferencedSymbol[] | undefined { - const node = ts.getTouchingPropertyName(sourceFile, position); - const options = { use: FindReferencesUse.References }; - const referencedSymbols = Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options); - const checker = program.getTypeChecker(); - // Unless the starting node is a declaration (vs e.g. JSDoc), don't attempt to compute isDefinition - const adjustedNode = Core.getAdjustedNode(node, options); - const symbol = isDefinitionForReference(adjustedNode) ? checker.getSymbolAtLocation(adjustedNode) : undefined; - return !referencedSymbols || !referencedSymbols.length ? undefined : ts.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(r => toReferencedSymbolEntry(r, symbol)) - }); +function entryToDocumentSpan(entry: Entry): ts.DocumentSpan { + if (entry.kind === EntryKind.Span) { + return { textSpan: entry.textSpan, fileName: entry.fileName }; } - - function isDefinitionForReference(node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.DefaultKeyword - || !!ts.getDeclarationFromName(node) - || ts.isLiteralComputedPropertyDeclarationName(node) - || (node.kind === ts.SyntaxKind.ConstructorKeyword && ts.isConstructorDeclaration(node.parent)); + else { + const sourceFile = entry.node.getSourceFile(); + const textSpan = getTextSpan(entry.node, sourceFile); + return { + textSpan, + fileName: sourceFile.fileName, + ...toContextSpan(textSpan, sourceFile, entry.context) + }; } +} - export function getImplementationsAtPosition(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], sourceFile: ts.SourceFile, position: number): ts.ImplementationLocation[] | undefined { - const node = ts.getTouchingPropertyName(sourceFile, position); - let referenceEntries: Entry[] | undefined; - const entries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position); - - if (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression - || node.parent.kind === ts.SyntaxKind.BindingElement - || node.parent.kind === ts.SyntaxKind.ElementAccessExpression - || node.kind === ts.SyntaxKind.SuperKeyword) { - referenceEntries = entries && [...entries]; - } - else { - const queue = entries && [...entries]; - const seenNodes = new ts.Map(); - while (queue && queue.length) { - const entry = queue.shift() as NodeEntry; - if (!ts.addToSeen(seenNodes, ts.getNodeId(entry.node))) { - continue; - } - referenceEntries = ts.append(referenceEntries, entry); - const entries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, entry.node, entry.node.pos); - if (entries) { - queue.push(...entries); +interface PrefixAndSuffix { + readonly prefixText?: string; + readonly suffixText?: string; +} +function getPrefixAndSuffixText(entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker): PrefixAndSuffix { + if (entry.kind !== EntryKind.Span && ts.isIdentifier(originalNode)) { + const { node, kind } = entry; + const parent = node.parent; + const name = originalNode.text; + const isShorthandAssignment = ts.isShorthandPropertyAssignment(parent); + if (isShorthandAssignment || (ts.isObjectBindingElementWithoutPropertyName(parent) && parent.name === node && parent.dotDotDotToken === undefined)) { + const prefixColon: PrefixAndSuffix = { prefixText: name + ": " }; + const suffixColon: PrefixAndSuffix = { suffixText: ": " + name }; + if (kind === EntryKind.SearchedLocalFoundProperty) { + return prefixColon; + } + if (kind === EntryKind.SearchedPropertyFoundLocal) { + return 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. + if (isShorthandAssignment) { + const grandParent = parent.parent; + if (ts.isObjectLiteralExpression(grandParent) && + ts.isBinaryExpression(grandParent.parent) && + ts.isModuleExportsAccessExpression(grandParent.parent.left)) { + return prefixColon; } + return suffixColon; + } + else { + return prefixColon; } } - const checker = program.getTypeChecker(); - return ts.map(referenceEntries, entry => toImplementationLocation(entry, checker)); - } - - function getImplementationReferenceEntries(program: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], node: ts.Node, position: number): readonly Entry[] | undefined { - if (node.kind === ts.SyntaxKind.SourceFile) { - return undefined; + else if (ts.isImportSpecifier(parent) && !parent.propertyName) { + // If the original symbol was using this alias, just rename the alias. + const originalSymbol = ts.isExportSpecifier(originalNode.parent) ? checker.getExportSpecifierLocalTargetSymbol(originalNode.parent) : checker.getSymbolAtLocation(originalNode); + return ts.contains(originalSymbol!.declarations, parent) ? { prefixText: name + " as " } : ts.emptyOptions; } - - 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 === ts.SyntaxKind.ShorthandPropertyAssignment) { - const result: NodeEntry[] = []; - Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node))); - return result; - } - else if (node.kind === ts.SyntaxKind.SuperKeyword || ts.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 if (ts.isExportSpecifier(parent) && !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 }; } - 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: ts.Program, cancellationToken: ts.CancellationToken, sourceFiles: readonly ts.SourceFile[], node: ts.Node, position: number, options: Options | undefined, convertEntry: ToReferenceOrRenameEntry): T[] | undefined { - return ts.map(flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options)), entry => convertEntry(entry, node, program.getTypeChecker())); - } - export type ToReferenceOrRenameEntry = (entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker) => T; - export function getReferenceEntriesForNode(position: number, node: ts.Node, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(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 && ts.flatMap(referenceSymbols, r => r.references); - } - function definitionToReferencedSymbolDefinitionInfo(def: Definition, checker: ts.TypeChecker, originalNode: ts.Node): ts.ReferencedSymbolDefinitionInfo { - const info = ((): { - sourceFile: ts.SourceFile; - textSpan: ts.TextSpan; - name: string; - kind: ts.ScriptElementKind; - displayParts: ts.SymbolDisplayPart[]; - context?: ts.Node | ContextWithStartAndEndNode; - } => { - 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 && ts.firstOrUndefined(symbol.declarations); - const node = declaration ? (ts.getNameOfDeclaration(declaration) || declaration) : originalNode; - return { - ...getFileAndTextSpanFromNode(node), - name, - kind, - displayParts, - context: getContextNode(declaration) - }; - } - case DefinitionKind.Label: { - const { node } = def; - return { ...getFileAndTextSpanFromNode(node), name: node.text, kind: ts.ScriptElementKind.label, displayParts: [ts.displayPart(node.text, ts.SymbolDisplayPartKind.text)] }; - } - case DefinitionKind.Keyword: { - const { node } = def; - const name = ts.tokenToString(node.kind)!; - return { ...getFileAndTextSpanFromNode(node), name, kind: ts.ScriptElementKind.keyword, displayParts: [{ text: name, kind: ts.ScriptElementKind.keyword }] }; - } - case DefinitionKind.This: { - const { node } = def; - const symbol = checker.getSymbolAtLocation(node); - const displayParts = symbol && ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, node.getSourceFile(), ts.getContainerNode(node), node).displayParts || [ts.textPart("this")]; - return { ...getFileAndTextSpanFromNode(node), name: "this", kind: ts.ScriptElementKind.variableElement, displayParts }; - } - case DefinitionKind.String: { - const { node } = def; - return { - ...getFileAndTextSpanFromNode(node), - name: node.text, - kind: ts.ScriptElementKind.variableElement, - displayParts: [ts.displayPart(ts.getTextOfNode(node), ts.SymbolDisplayPartKind.stringLiteral)] - }; - } - case DefinitionKind.TripleSlashReference: { - return { - textSpan: ts.createTextSpanFromRange(def.reference), - sourceFile: def.file, - name: def.reference.fileName, - kind: ts.ScriptElementKind.string, - displayParts: [ts.displayPart(`"${def.reference.fileName}"`, ts.SymbolDisplayPartKind.stringLiteral)] - }; - } - default: - return ts.Debug.assertNever(def); - } - })(); - - const { sourceFile, textSpan, name, kind, displayParts, context } = info; - return { - containerKind: ts.ScriptElementKind.unknown, - containerName: "", - fileName: sourceFile.fileName, - kind, - name, - textSpan, - displayParts, - ...toContextSpan(textSpan, sourceFile, context) - }; } - function getFileAndTextSpanFromNode(node: ts.Node) { - const sourceFile = node.getSourceFile(); + return ts.emptyOptions; +} + +function toImplementationLocation(entry: Entry, checker: ts.TypeChecker): ts.ImplementationLocation { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind !== EntryKind.Span) { + const { node } = entry; return { - sourceFile, - textSpan: getTextSpan(ts.isComputedPropertyName(node) ? node.expression : node, sourceFile) + ...documentSpan, + ...implementationKindDisplayParts(node, checker) }; } - - function getDefinitionKindAndDisplayParts(symbol: ts.Symbol, checker: ts.TypeChecker, node: ts.Node): { - displayParts: ts.SymbolDisplayPart[]; - kind: ts.ScriptElementKind; - } { - const meaning = Core.getIntersectingMeaningFromDeclarations(node, symbol); - const enclosingDeclaration = symbol.declarations && ts.firstOrUndefined(symbol.declarations) || node; - const { displayParts, symbolKind } = ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, enclosingDeclaration.getSourceFile(), enclosingDeclaration, enclosingDeclaration, meaning); - return { displayParts, kind: symbolKind }; + else { + return { ...documentSpan, kind: ts.ScriptElementKind.unknown, displayParts: [] }; } +} - export function toRenameLocation(entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker, providePrefixAndSuffixText: boolean): ts.RenameLocation { - return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) }; +function implementationKindDisplayParts(node: ts.Node, checker: ts.TypeChecker): { + kind: ts.ScriptElementKind; + displayParts: ts.SymbolDisplayPart[]; +} { + const symbol = checker.getSymbolAtLocation(ts.isDeclaration(node) && node.name ? node.name : node); + if (symbol) { + return getDefinitionKindAndDisplayParts(symbol, checker, node); } - - function toReferencedSymbolEntry(entry: Entry, symbol: ts.Symbol | undefined): ts.ReferencedSymbolEntry { - const referenceEntry = toReferenceEntry(entry); - if (!symbol) - return referenceEntry; + else if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) { return { - ...referenceEntry, - isDefinition: entry.kind !== EntryKind.Span && isDeclarationOfSymbol(entry.node, symbol) + kind: ts.ScriptElementKind.interfaceElement, + displayParts: [ts.punctuationPart(ts.SyntaxKind.OpenParenToken), ts.textPart("object literal"), ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] }; } + else if (node.kind === ts.SyntaxKind.ClassExpression) { + return { + kind: ts.ScriptElementKind.localClassElement, + displayParts: [ts.punctuationPart(ts.SyntaxKind.OpenParenToken), ts.textPart("anonymous local class"), ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] + }; + } + else { + return { kind: ts.getNodeKind(node), displayParts: [] }; + } +} - export function toReferenceEntry(entry: Entry): ts.ReferenceEntry { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind === EntryKind.Span) { - return { ...documentSpan, isWriteAccess: false }; - } - const { kind, node } = entry; +export function toHighlightSpan(entry: Entry): { + fileName: string; + span: ts.HighlightSpan; +} { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind === EntryKind.Span) { return { - ...documentSpan, - isWriteAccess: isWriteAccessForReference(node), - isInString: kind === EntryKind.StringLiteral ? true : undefined, + fileName: documentSpan.fileName, + span: { + textSpan: documentSpan.textSpan, + kind: ts.HighlightSpanKind.reference + } }; } - function entryToDocumentSpan(entry: Entry): ts.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) - }; - } + const writeAccess = isWriteAccessForReference(entry.node); + const span: ts.HighlightSpan = { + textSpan: documentSpan.textSpan, + kind: writeAccess ? ts.HighlightSpanKind.writtenReference : ts.HighlightSpanKind.reference, + isInString: entry.kind === EntryKind.StringLiteral ? true : undefined, + ...documentSpan.contextSpan && { contextSpan: documentSpan.contextSpan } + }; + return { fileName: documentSpan.fileName, span }; +} + +function getTextSpan(node: ts.Node, sourceFile: ts.SourceFile, endNode?: ts.Node): ts.TextSpan { + let start = node.getStart(sourceFile); + let end = (endNode || node).getEnd(); + if (ts.isStringLiteralLike(node) && (end - start) > 2) { + ts.Debug.assert(endNode === undefined); + start += 1; + end -= 1; } + return ts.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: ts.Node): boolean { + const decl = ts.getDeclarationFromName(node); + return !!decl && declarationIsWriteAccess(decl) || node.kind === ts.SyntaxKind.DefaultKeyword || ts.isWriteAccess(node); +} - interface PrefixAndSuffix { - readonly prefixText?: string; - readonly suffixText?: string; +/** Whether a reference, `node`, is a definition of the `target` symbol */ +function isDeclarationOfSymbol(node: ts.Node, target: ts.Symbol | undefined): boolean { + if (!target) + return false; + const source = ts.getDeclarationFromName(node) || + (node.kind === ts.SyntaxKind.DefaultKeyword ? node.parent + : ts.isLiteralComputedPropertyDeclarationName(node) ? node.parent.parent + : node.kind === ts.SyntaxKind.ConstructorKeyword && ts.isConstructorDeclaration(node.parent) ? node.parent.parent + : undefined); + const commonjsSource = source && ts.isBinaryExpression(source) ? source.left as unknown as ts.Declaration : undefined; + return !!(source && target.declarations?.some(d => d === source || d === commonjsSource)); +} + +/** + * 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: ts.Declaration): boolean { + // Consider anything in an ambient declaration to be a write access since it may be coming from JS. + if (!!(decl.flags & ts.NodeFlags.Ambient)) + return true; + + switch (decl.kind) { + case ts.SyntaxKind.BinaryExpression: + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.ImportClause: // default import + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JsxAttribute: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.NamespaceExportDeclaration: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.NamespaceExport: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.TypeParameter: + return true; + + case ts.SyntaxKind.PropertyAssignment: + // In `({ x: y } = 0);`, `x` is not a write access. (Won't call this function for `y`.) + return !ts.isArrayLiteralOrObjectLiteralDestructuringPattern((decl as ts.PropertyAssignment).parent); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return !!(decl as ts.FunctionDeclaration | ts.FunctionExpression | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration).body; + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.PropertyDeclaration: + return !!(decl as ts.VariableDeclaration | ts.PropertyDeclaration).initializer || ts.isCatchClause(decl.parent); + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.JSDocPropertyTag: + case ts.SyntaxKind.JSDocParameterTag: + return false; + + default: + return ts.Debug.failBadSyntaxKind(decl); } - function getPrefixAndSuffixText(entry: Entry, originalNode: ts.Node, checker: ts.TypeChecker): PrefixAndSuffix { - if (entry.kind !== EntryKind.Span && ts.isIdentifier(originalNode)) { - const { node, kind } = entry; - const parent = node.parent; - const name = originalNode.text; - const isShorthandAssignment = ts.isShorthandPropertyAssignment(parent); - if (isShorthandAssignment || (ts.isObjectBindingElementWithoutPropertyName(parent) && parent.name === node && parent.dotDotDotToken === undefined)) { - const prefixColon: PrefixAndSuffix = { prefixText: name + ": " }; - const suffixColon: PrefixAndSuffix = { suffixText: ": " + name }; - if (kind === EntryKind.SearchedLocalFoundProperty) { - return prefixColon; - } - if (kind === EntryKind.SearchedPropertyFoundLocal) { - return 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. - if (isShorthandAssignment) { - const grandParent = parent.parent; - if (ts.isObjectLiteralExpression(grandParent) && - ts.isBinaryExpression(grandParent.parent) && - ts.isModuleExportsAccessExpression(grandParent.parent.left)) { - return prefixColon; - } - return suffixColon; - } - else { - return prefixColon; - } +/** 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: ts.Node, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(f => f.fileName))): readonly SymbolAndEntries[] | undefined { + node = getAdjustedNode(node, options); + if (ts.isSourceFile(node)) { + const resolvedRef = ts.GoToDefinition.getReferenceAtPosition(node, position, program); + if (!resolvedRef?.file) { + return undefined; } - else if (ts.isImportSpecifier(parent) && !parent.propertyName) { - // If the original symbol was using this alias, just rename the alias. - const originalSymbol = ts.isExportSpecifier(originalNode.parent) ? checker.getExportSpecifierLocalTargetSymbol(originalNode.parent) : checker.getSymbolAtLocation(originalNode); - return ts.contains(originalSymbol!.declarations, parent) ? { prefixText: name + " as " } : ts.emptyOptions; + const moduleSymbol = program.getTypeChecker().getMergedSymbol(resolvedRef.file.symbol); + if (moduleSymbol) { + return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); } - else if (ts.isExportSpecifier(parent) && !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 }; + const fileIncludeReasons = program.getFileIncludeReasons(); + if (!fileIncludeReasons) { + return undefined; } + return [{ + definition: { type: DefinitionKind.TripleSlashReference, reference: resolvedRef.reference, file: node }, + references: getReferencesForNonModule(resolvedRef.file, fileIncludeReasons, program) || ts.emptyArray + }]; } - return ts.emptyOptions; - } - - function toImplementationLocation(entry: Entry, checker: ts.TypeChecker): ts.ImplementationLocation { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind !== EntryKind.Span) { - const { node } = entry; - return { - ...documentSpan, - ...implementationKindDisplayParts(node, checker) - }; - } - else { - return { ...documentSpan, kind: ts.ScriptElementKind.unknown, displayParts: [] }; + if (!options.implementations) { + const special = getReferencedSymbolsSpecial(node, sourceFiles, cancellationToken); + if (special) { + return special; + } } - } - function implementationKindDisplayParts(node: ts.Node, checker: ts.TypeChecker): { - kind: ts.ScriptElementKind; - displayParts: ts.SymbolDisplayPart[]; - } { - const symbol = checker.getSymbolAtLocation(ts.isDeclaration(node) && node.name ? node.name : node); - if (symbol) { - return getDefinitionKindAndDisplayParts(symbol, checker, node); - } - else if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) { - return { - kind: ts.ScriptElementKind.interfaceElement, - displayParts: [ts.punctuationPart(ts.SyntaxKind.OpenParenToken), ts.textPart("object literal"), ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] - }; + const checker = program.getTypeChecker(); + // constructors should use the class symbol, detected by name, if present + const symbol = checker.getSymbolAtLocation(ts.isConstructorDeclaration(node) && node.parent.name || 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. + if (!options.implementations && ts.isStringLiteralLike(node)) { + if (ts.isModuleSpecifierLike(node)) { + const fileIncludeReasons = program.getFileIncludeReasons(); + const referencedFileName = node.getSourceFile().resolvedModules?.get(node.text, ts.getModeForUsageLocation(node.getSourceFile(), node))?.resolvedFileName; + const referencedFile = referencedFileName ? program.getSourceFile(referencedFileName) : undefined; + if (referencedFile) { + return [{ definition: { type: DefinitionKind.String, node }, references: getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || ts.emptyArray }]; + } + // Fall through to string literal references. This is not very likely to return + // anything useful, but I guess it's better than nothing, and there's an existing + // test that expects this to happen (fourslash/cases/untypedModuleImport.ts). + } + return getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken); + } + return undefined; } - else if (node.kind === ts.SyntaxKind.ClassExpression) { - return { - kind: ts.ScriptElementKind.localClassElement, - displayParts: [ts.punctuationPart(ts.SyntaxKind.OpenParenToken), ts.textPart("anonymous local class"), ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] - }; + + if (symbol.escapedName === ts.InternalSymbolName.ExportEquals) { + return getReferencedSymbolsForModule(program, symbol.parent!, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); } - else { - return { kind: ts.getNodeKind(node), displayParts: [] }; + + const moduleReferences = getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); + if (moduleReferences && !(symbol.flags & ts.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: ts.HighlightSpan; - } { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind === EntryKind.Span) { - return { - fileName: documentSpan.fileName, - span: { - textSpan: documentSpan.textSpan, - kind: ts.HighlightSpanKind.reference - } - }; + export function getAdjustedNode(node: ts.Node, options: Options) { + if (options.use === FindReferencesUse.References) { + node = ts.getAdjustedReferenceLocation(node); } - - const writeAccess = isWriteAccessForReference(entry.node); - const span: ts.HighlightSpan = { - textSpan: documentSpan.textSpan, - kind: writeAccess ? ts.HighlightSpanKind.writtenReference : ts.HighlightSpanKind.reference, - isInString: entry.kind === EntryKind.StringLiteral ? true : undefined, - ...documentSpan.contextSpan && { contextSpan: documentSpan.contextSpan } - }; - return { fileName: documentSpan.fileName, span }; + else if (options.use === FindReferencesUse.Rename) { + node = ts.getAdjustedRenameLocation(node); + } + return node; } - function getTextSpan(node: ts.Node, sourceFile: ts.SourceFile, endNode?: ts.Node): ts.TextSpan { - let start = node.getStart(sourceFile); - let end = (endNode || node).getEnd(); - if (ts.isStringLiteralLike(node) && (end - start) > 2) { - ts.Debug.assert(endNode === undefined); - start += 1; - end -= 1; + export function getReferencesForFileName(fileName: string, program: ts.Program, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(f => f.fileName))): readonly Entry[] { + const moduleSymbol = program.getSourceFile(fileName)?.symbol; + if (moduleSymbol) { + return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet)[0]?.references || ts.emptyArray; } - return ts.createTextSpanFromBounds(start, end); + const fileIncludeReasons = program.getFileIncludeReasons(); + const referencedFile = program.getSourceFile(fileName); + return referencedFile && fileIncludeReasons && getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || ts.emptyArray; } - export function getTextSpanOfEntry(entry: Entry) { - return entry.kind === EntryKind.Span ? entry.textSpan : - getTextSpan(entry.node, entry.node.getSourceFile()); + function getReferencesForNonModule(referencedFile: ts.SourceFile, refFileMap: ts.MultiMap, program: ts.Program): readonly SpanEntry[] | undefined { + let entries: SpanEntry[] | undefined; + const references = refFileMap.get(referencedFile.path) || ts.emptyArray; + for (const ref of references) { + if (ts.isReferencedFile(ref)) { + const referencingFile = program.getSourceFileByPath(ref.file)!; + const location = ts.getReferencedFileLocation(program.getSourceFileByPath, ref); + if (ts.isReferenceFileLocation(location)) { + entries = ts.append(entries, { + kind: EntryKind.Span, + fileName: referencingFile.fileName, + textSpan: ts.createTextSpanFromRange(location) + }); + } + } + } + return entries; } - /** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */ - function isWriteAccessForReference(node: ts.Node): boolean { - const decl = ts.getDeclarationFromName(node); - return !!decl && declarationIsWriteAccess(decl) || node.kind === ts.SyntaxKind.DefaultKeyword || ts.isWriteAccess(node); + function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: ts.Node, symbol: ts.Symbol, checker: ts.TypeChecker) { + if (node.parent && ts.isNamespaceExportDeclaration(node.parent)) { + const aliasedSymbol = checker.getAliasedSymbol(symbol); + const targetSymbol = checker.getMergedSymbol(aliasedSymbol); + if (aliasedSymbol !== targetSymbol) { + return targetSymbol; + } + } + return undefined; } - /** Whether a reference, `node`, is a definition of the `target` symbol */ - function isDeclarationOfSymbol(node: ts.Node, target: ts.Symbol | undefined): boolean { - if (!target) - return false; - const source = ts.getDeclarationFromName(node) || - (node.kind === ts.SyntaxKind.DefaultKeyword ? node.parent - : ts.isLiteralComputedPropertyDeclarationName(node) ? node.parent.parent - : node.kind === ts.SyntaxKind.ConstructorKeyword && ts.isConstructorDeclaration(node.parent) ? node.parent.parent - : undefined); - const commonjsSource = source && ts.isBinaryExpression(source) ? source.left as unknown as ts.Declaration : undefined; - return !!(source && target.declarations?.some(d => d === source || d === commonjsSource)); + function getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol: ts.Symbol, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options, sourceFilesSet: ts.ReadonlySet) { + const moduleSourceFile = (symbol.flags & ts.SymbolFlags.Module) && symbol.declarations && ts.find(symbol.declarations, ts.isSourceFile); + if (!moduleSourceFile) + return undefined; + const exportEquals = symbol.exports!.get(ts.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 = ts.skipAlias(exportEquals, checker); + return mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol, /*node*/ undefined, sourceFiles, sourceFilesSet, checker, cancellationToken, options)); } /** - * 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;' + * Merges the references by sorting them (by file index in sourceFiles and their location in it) that point to same definition symbol */ - function declarationIsWriteAccess(decl: ts.Declaration): boolean { - // Consider anything in an ambient declaration to be a write access since it may be coming from JS. - if (!!(decl.flags & ts.NodeFlags.Ambient)) - return true; - - switch (decl.kind) { - case ts.SyntaxKind.BinaryExpression: - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.DefaultKeyword: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.ImportClause: // default import - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JsxAttribute: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.NamespaceExportDeclaration: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.NamespaceExport: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.ShorthandPropertyAssignment: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.TypeParameter: - return true; + function mergeReferences(program: ts.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; + } + const symbol = entry.definition.symbol; + const refIndex = ts.findIndex(result, ref => !!ref.definition && + ref.definition.type === DefinitionKind.Symbol && + ref.definition.symbol === symbol); + if (refIndex === -1) { + result.push(entry); + continue; + } - case ts.SyntaxKind.PropertyAssignment: - // In `({ x: y } = 0);`, `x` is not a write access. (Won't call this function for `y`.) - return !ts.isArrayLiteralOrObjectLiteralDestructuringPattern((decl as ts.PropertyAssignment).parent); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return !!(decl as ts.FunctionDeclaration | ts.FunctionExpression | ts.ConstructorDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration).body; - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.PropertyDeclaration: - return !!(decl as ts.VariableDeclaration | ts.PropertyDeclaration).initializer || ts.isCatchClause(decl.parent); - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.JSDocPropertyTag: - case ts.SyntaxKind.JSDocParameterTag: - return false; + 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 ts.compareValues(entry1File, entry2File); + } - default: - return ts.Debug.failBadSyntaxKind(decl); + const entry1Span = getTextSpanOfEntry(entry1); + const entry2Span = getTextSpanOfEntry(entry2); + return entry1Span.start !== entry2Span.start ? + ts.compareValues(entry1Span.start, entry2Span.start) : + ts.compareValues(entry1Span.length, entry2Span.length); + }) + }; + } } + return result; } - /** 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: ts.Node, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(f => f.fileName))): readonly SymbolAndEntries[] | undefined { - node = getAdjustedNode(node, options); - if (ts.isSourceFile(node)) { - const resolvedRef = ts.GoToDefinition.getReferenceAtPosition(node, position, program); - if (!resolvedRef?.file) { - return undefined; - } - const moduleSymbol = program.getTypeChecker().getMergedSymbol(resolvedRef.file.symbol); - if (moduleSymbol) { - return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); - } - const fileIncludeReasons = program.getFileIncludeReasons(); - if (!fileIncludeReasons) { - return undefined; - } - return [{ - definition: { type: DefinitionKind.TripleSlashReference, reference: resolvedRef.reference, file: node }, - references: getReferencesForNonModule(resolvedRef.file, fileIncludeReasons, program) || ts.emptyArray - }]; - } + function getSourceFileIndexOfEntry(program: ts.Program, entry: Entry) { + const sourceFile = entry.kind === EntryKind.Span ? + program.getSourceFile(entry.fileName)! : + entry.node.getSourceFile(); + return program.getSourceFiles().indexOf(sourceFile); + } - if (!options.implementations) { - const special = getReferencedSymbolsSpecial(node, sourceFiles, cancellationToken); - if (special) { - return special; + function getReferencedSymbolsForModule(program: ts.Program, symbol: ts.Symbol, excludeImportTypeOfExportEquals: boolean, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet): SymbolAndEntries[] { + ts.Debug.assert(!!symbol.valueDeclaration); + const references = ts.mapDefined(ts.FindAllReferences.findModuleReferences(program, sourceFiles, symbol), reference => { + if (reference.kind === "import") { + const parent = reference.literal.parent; + if (ts.isLiteralTypeNode(parent)) { + const importType = ts.cast(parent.parent, ts.isImportTypeNode); + if (excludeImportTypeOfExportEquals && !importType.qualifier) { + return undefined; + } } + // 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: ts.createTextSpanFromRange(reference.ref), + }; } + }); - const checker = program.getTypeChecker(); - // constructors should use the class symbol, detected by name, if present - const symbol = checker.getSymbolAtLocation(ts.isConstructorDeclaration(node) && node.parent.name || 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. - if (!options.implementations && ts.isStringLiteralLike(node)) { - if (ts.isModuleSpecifierLike(node)) { - const fileIncludeReasons = program.getFileIncludeReasons(); - const referencedFileName = node.getSourceFile().resolvedModules?.get(node.text, ts.getModeForUsageLocation(node.getSourceFile(), node))?.resolvedFileName; - const referencedFile = referencedFileName ? program.getSourceFile(referencedFileName) : undefined; - if (referencedFile) { - return [{ definition: { type: DefinitionKind.String, node }, references: getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || ts.emptyArray }]; + if (symbol.declarations) { + for (const decl of symbol.declarations) { + switch (decl.kind) { + case ts.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 ts.SyntaxKind.ModuleDeclaration: + if (sourceFilesSet.has(decl.getSourceFile().fileName)) { + references.push(nodeEntry((decl as ts.ModuleDeclaration).name)); } - // Fall through to string literal references. This is not very likely to return - // anything useful, but I guess it's better than nothing, and there's an existing - // test that expects this to happen (fourslash/cases/untypedModuleImport.ts). - } - return getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken); + break; + default: + // This may be merged with something. + ts.Debug.assert(!!(symbol.flags & ts.SymbolFlags.Transient), "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration."); } - return undefined; - } - - if (symbol.escapedName === ts.InternalSymbolName.ExportEquals) { - return getReferencedSymbolsForModule(program, symbol.parent!, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); } + } - const moduleReferences = getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); - if (moduleReferences && !(symbol.flags & ts.SymbolFlags.Transient)) { - return moduleReferences; + const exported = symbol.exports!.get(ts.InternalSymbolName.ExportEquals); + if (exported?.declarations) { + for (const decl of exported.declarations) { + const sourceFile = decl.getSourceFile(); + if (sourceFilesSet.has(sourceFile.fileName)) { + // At `module.exports = ...`, reference node is `module` + const node = ts.isBinaryExpression(decl) && ts.isPropertyAccessExpression(decl.left) ? decl.left.expression : + ts.isExportAssignment(decl) ? ts.Debug.checkDefined(ts.findChildOfKind(decl, ts.SyntaxKind.ExportKeyword, sourceFile)) : + ts.getNameOfDeclaration(decl) || decl; + references.push(nodeEntry(node)); + } } + } - const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(node, symbol, checker); - const moduleReferencesOfExportTarget = aliasedSymbol && - getReferencedSymbolsForModuleIfDeclaredBySourceFile(aliasedSymbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); + return references.length ? [{ definition: { type: DefinitionKind.Symbol, symbol }, references }] : ts.emptyArray; + } - const references = getReferencedSymbolsForSymbol(symbol, node, sourceFiles, sourceFilesSet, checker, cancellationToken, options); - return mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget); - } + /** As in a `readonly prop: any` or `constructor(readonly prop: any)`, not a `readonly any[]`. */ + function isReadonlyTypeOperator(node: ts.Node): boolean { + return node.kind === ts.SyntaxKind.ReadonlyKeyword + && ts.isTypeOperatorNode(node.parent) + && node.parent.operator === ts.SyntaxKind.ReadonlyKeyword; + } - export function getAdjustedNode(node: ts.Node, options: Options) { - if (options.use === FindReferencesUse.References) { - node = ts.getAdjustedReferenceLocation(node); - } - else if (options.use === FindReferencesUse.Rename) { - node = ts.getAdjustedRenameLocation(node); + /** getReferencedSymbols for special node kinds. */ + function getReferencedSymbolsSpecial(node: ts.Node, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { + if (ts.isTypeKeyword(node.kind)) { + // A void expression (i.e., `void foo()`) is not special, but the `void` type is. + if (node.kind === ts.SyntaxKind.VoidKeyword && ts.isVoidExpression(node.parent)) { + return undefined; } - return node; - } - export function getReferencesForFileName(fileName: string, program: ts.Program, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet = new ts.Set(sourceFiles.map(f => f.fileName))): readonly Entry[] { - const moduleSymbol = program.getSourceFile(fileName)?.symbol; - if (moduleSymbol) { - return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet)[0]?.references || ts.emptyArray; - } - const fileIncludeReasons = program.getFileIncludeReasons(); - const referencedFile = program.getSourceFile(fileName); - return referencedFile && fileIncludeReasons && getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || ts.emptyArray; - } - - function getReferencesForNonModule(referencedFile: ts.SourceFile, refFileMap: ts.MultiMap, program: ts.Program): readonly SpanEntry[] | undefined { - let entries: SpanEntry[] | undefined; - const references = refFileMap.get(referencedFile.path) || ts.emptyArray; - for (const ref of references) { - if (ts.isReferencedFile(ref)) { - const referencingFile = program.getSourceFileByPath(ref.file)!; - const location = ts.getReferencedFileLocation(program.getSourceFileByPath, ref); - if (ts.isReferenceFileLocation(location)) { - entries = ts.append(entries, { - kind: EntryKind.Span, - fileName: referencingFile.fileName, - textSpan: ts.createTextSpanFromRange(location) - }); - } - } + // A modifier readonly (like on a property declaration) is not special; + // a readonly type keyword (like `readonly string[]`) is. + if (node.kind === ts.SyntaxKind.ReadonlyKeyword && !isReadonlyTypeOperator(node)) { + return undefined; } - return entries; + // 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 === ts.SyntaxKind.ReadonlyKeyword ? isReadonlyTypeOperator : undefined); } - - function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: ts.Node, symbol: ts.Symbol, checker: ts.TypeChecker) { - if (node.parent && ts.isNamespaceExportDeclaration(node.parent)) { - const aliasedSymbol = checker.getAliasedSymbol(symbol); - const targetSymbol = checker.getMergedSymbol(aliasedSymbol); - if (aliasedSymbol !== targetSymbol) { - return targetSymbol; - } - } - return undefined; + if (ts.isImportMeta(node.parent) && node.parent.name === node) { + return getAllReferencesForImportMeta(sourceFiles, cancellationToken); } - function getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol: ts.Symbol, program: ts.Program, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken, options: Options, sourceFilesSet: ts.ReadonlySet) { - const moduleSourceFile = (symbol.flags & ts.SymbolFlags.Module) && symbol.declarations && ts.find(symbol.declarations, ts.isSourceFile); - if (!moduleSourceFile) - return undefined; - const exportEquals = symbol.exports!.get(ts.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 = ts.skipAlias(exportEquals, checker); - return mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol, /*node*/ undefined, sourceFiles, sourceFilesSet, checker, cancellationToken, options)); + if (ts.isStaticModifier(node) && ts.isClassStaticBlockDeclaration(node.parent)) { + return [{ definition: { type: DefinitionKind.Keyword, node }, references: [nodeEntry(node)] }]; } - /** - * 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: ts.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; - } - const symbol = entry.definition.symbol; - const refIndex = ts.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 ts.compareValues(entry1File, entry2File); - } + // Labels + if (ts.isJumpStatementTarget(node)) { + const labelDefinition = ts.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 (ts.isLabelOfLabeledStatement(node)) { + // it is a label definition and not a target, search within the parent labeledStatement + return getLabelReferencesInNode(node.parent, node); + } - const entry1Span = getTextSpanOfEntry(entry1); - const entry2Span = getTextSpanOfEntry(entry2); - return entry1Span.start !== entry2Span.start ? - ts.compareValues(entry1Span.start, entry2Span.start) : - ts.compareValues(entry1Span.length, entry2Span.length); - }) - }; - } - } - return result; + if (ts.isThis(node)) { + return getReferencesForThisKeyword(node, sourceFiles, cancellationToken); } - function getSourceFileIndexOfEntry(program: ts.Program, entry: Entry) { - const sourceFile = entry.kind === EntryKind.Span ? - program.getSourceFile(entry.fileName)! : - entry.node.getSourceFile(); - return program.getSourceFiles().indexOf(sourceFile); + if (node.kind === ts.SyntaxKind.SuperKeyword) { + return getReferencesForSuperKeyword(node); } - function getReferencedSymbolsForModule(program: ts.Program, symbol: ts.Symbol, excludeImportTypeOfExportEquals: boolean, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet): SymbolAndEntries[] { - ts.Debug.assert(!!symbol.valueDeclaration); - const references = ts.mapDefined(ts.FindAllReferences.findModuleReferences(program, sourceFiles, symbol), reference => { - if (reference.kind === "import") { - const parent = reference.literal.parent; - if (ts.isLiteralTypeNode(parent)) { - const importType = ts.cast(parent.parent, ts.isImportTypeNode); - if (excludeImportTypeOfExportEquals && !importType.qualifier) { - return undefined; - } - } - // 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: ts.createTextSpanFromRange(reference.ref), - }; - } - }); + return undefined; + } - if (symbol.declarations) { - for (const decl of symbol.declarations) { - switch (decl.kind) { - case ts.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 ts.SyntaxKind.ModuleDeclaration: - if (sourceFilesSet.has(decl.getSourceFile().fileName)) { - references.push(nodeEntry((decl as ts.ModuleDeclaration).name)); - } - break; - default: - // This may be merged with something. - ts.Debug.assert(!!(symbol.flags & ts.SymbolFlags.Transient), "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration."); - } - } - } + /** Core find-all-references algorithm for a normal symbol. */ + function getReferencedSymbolsForSymbol(originalSymbol: ts.Symbol, node: ts.Node | undefined, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken, options: Options): SymbolAndEntries[] { + const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, /*useLocalSymbolForExportSpecifier*/ !isForRenameWithPrefixAndSuffixText(options)) || originalSymbol; - const exported = symbol.exports!.get(ts.InternalSymbolName.ExportEquals); - if (exported?.declarations) { - for (const decl of exported.declarations) { - const sourceFile = decl.getSourceFile(); - if (sourceFilesSet.has(sourceFile.fileName)) { - // At `module.exports = ...`, reference node is `module` - const node = ts.isBinaryExpression(decl) && ts.isPropertyAccessExpression(decl.left) ? decl.left.expression : - ts.isExportAssignment(decl) ? ts.Debug.checkDefined(ts.findChildOfKind(decl, ts.SyntaxKind.ExportKeyword, sourceFile)) : - ts.getNameOfDeclaration(decl) || decl; - references.push(nodeEntry(node)); - } - } - } + // Compute the meaning from the location and the symbol it references + const searchMeaning = node ? getIntersectingMeaningFromDeclarations(node, symbol) : ts.SemanticMeaning.All; + const result: SymbolAndEntries[] = []; + const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result); - return references.length ? [{ definition: { type: DefinitionKind.Symbol, symbol }, references }] : ts.emptyArray; + const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) || !symbol.declarations ? undefined : ts.find(symbol.declarations, ts.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 === ts.SyntaxKind.DefaultKeyword && symbol.escapedName === ts.InternalSymbolName.Default && symbol.parent) { + addReference(node, symbol, state); + searchForImportsOfExport(node, symbol, { exportingModuleSymbol: symbol.parent, exportKind: ts.FindAllReferences.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; + } - /** As in a `readonly prop: any` or `constructor(readonly prop: any)`, not a `readonly any[]`. */ - function isReadonlyTypeOperator(node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.ReadonlyKeyword - && ts.isTypeOperatorNode(node.parent) - && node.parent.operator === ts.SyntaxKind.ReadonlyKeyword; + function getReferencesInContainerOrFiles(symbol: ts.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*/ !(ts.isSourceFile(scope) && !ts.contains(state.sourceFiles, scope))); } + else { + // Global search + for (const sourceFile of state.sourceFiles) { + state.cancellationToken.throwIfCancellationRequested(); + searchForName(sourceFile, search, state); + } + } + } - /** getReferencedSymbols for special node kinds. */ - function getReferencedSymbolsSpecial(node: ts.Node, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { - if (ts.isTypeKeyword(node.kind)) { - // A void expression (i.e., `void foo()`) is not special, but the `void` type is. - if (node.kind === ts.SyntaxKind.VoidKeyword && ts.isVoidExpression(node.parent)) { - return undefined; - } + function getSpecialSearchKind(node: ts.Node): SpecialSearchKind { + switch (node.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructorKeyword: + return SpecialSearchKind.Constructor; + case ts.SyntaxKind.Identifier: + if (ts.isClassLike(node.parent)) { + ts.Debug.assert(node.parent.name === node); + return SpecialSearchKind.Class; + } + // falls through + default: + return SpecialSearchKind.None; + } + } - // A modifier readonly (like on a property declaration) is not special; - // a readonly type keyword (like `readonly string[]`) is. - if (node.kind === ts.SyntaxKind.ReadonlyKeyword && !isReadonlyTypeOperator(node)) { + /** Handle a few special cases relating to export/import specifiers. */ + function skipPastExportOrImportSpecifierOrUnion(symbol: ts.Symbol, node: ts.Node, checker: ts.TypeChecker, useLocalSymbolForExportSpecifier: boolean): ts.Symbol | undefined { + const { parent } = node; + if (ts.isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) { + return getLocalSymbolForExportSpecifier(node as ts.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 ts.firstDefined(symbol.declarations, decl => { + if (!decl.parent) { + // Ignore UMD module and global merge + if (symbol.flags & ts.SymbolFlags.Transient) 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 === ts.SyntaxKind.ReadonlyKeyword ? isReadonlyTypeOperator : undefined); - } - if (ts.isImportMeta(node.parent) && node.parent.name === node) { - return getAllReferencesForImportMeta(sourceFiles, cancellationToken); - } - - if (ts.isStaticModifier(node) && ts.isClassStaticBlockDeclaration(node.parent)) { - return [{ definition: { type: DefinitionKind.Keyword, node }, references: [nodeEntry(node)] }]; + // Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here. + ts.Debug.fail(`Unexpected symbol at ${ts.Debug.formatSyntaxKind(node.kind)}: ${ts.Debug.formatSymbol(symbol)}`); } + return ts.isTypeLiteralNode(decl.parent) && ts.isUnionTypeNode(decl.parent.parent) + ? checker.getPropertyOfType(checker.getTypeFromTypeNode(decl.parent.parent), symbol.name) + : undefined; + }); + } - // Labels - if (ts.isJumpStatementTarget(node)) { - const labelDefinition = ts.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 (ts.isLabelOfLabeledStatement(node)) { - // it is a label definition and not a target, search within the parent labeledStatement - return getLabelReferencesInNode(node.parent, node); - } + /** + * 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?: ts.FindAllReferences.ImportExport; + readonly symbol: ts.Symbol; + readonly text: string; + readonly escapedText: ts.__String; + /** Only set if `options.implementations` is true. These are the symbols checked to get the implementations of a property access. */ + readonly parents: readonly ts.Symbol[] | undefined; + readonly allSearchSymbols: readonly ts.Symbol[]; - if (ts.isThis(node)) { - return getReferencesForThisKeyword(node, sourceFiles, cancellationToken); - } + /** + * 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: ts.Symbol): boolean; + } - if (node.kind === ts.SyntaxKind.SuperKeyword) { - return getReferencesForSuperKeyword(node); - } + const enum SpecialSearchKind { + None, + Constructor, + Class + } + function getNonModuleSymbolOfMergedModuleSymbol(symbol: ts.Symbol) { + if (!(symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Transient))) return undefined; - } - - /** Core find-all-references algorithm for a normal symbol. */ - function getReferencedSymbolsForSymbol(originalSymbol: ts.Symbol, node: ts.Node | undefined, sourceFiles: readonly ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, checker: ts.TypeChecker, cancellationToken: ts.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) : ts.SemanticMeaning.All; - const result: SymbolAndEntries[] = []; - const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result); + const decl = symbol.declarations && ts.find(symbol.declarations, d => !ts.isSourceFile(d) && !ts.isModuleDeclaration(d)); + return decl && decl.symbol; + } - const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) || !symbol.declarations ? undefined : ts.find(symbol.declarations, ts.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 === ts.SyntaxKind.DefaultKeyword && symbol.escapedName === ts.InternalSymbolName.Default && symbol.parent) { - addReference(node, symbol, state); - searchForImportsOfExport(node, symbol, { exportingModuleSymbol: symbol.parent, exportKind: ts.FindAllReferences.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); - } + /** + * Holds all state needed for the finding references. + * Unlike `Search`, there is only one `State`. + */ + class State { + /** Cache for `explicitlyinheritsFrom`. */ + readonly inheritsFromCache = new ts.Map(); - return result; - } + /** + * 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 = ts.nodeSeenTracker(); - function getReferencesInContainerOrFiles(symbol: ts.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*/ !(ts.isSourceFile(scope) && !ts.contains(state.sourceFiles, scope))); - } - else { - // Global search - for (const sourceFile of state.sourceFiles) { - state.cancellationToken.throwIfCancellationRequested(); - searchForName(sourceFile, search, state); - } - } + /** + * 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 = ts.nodeSeenTracker(); + constructor(readonly sourceFiles: readonly ts.SourceFile[], readonly sourceFilesSet: ts.ReadonlySet, readonly specialSearchKind: SpecialSearchKind, readonly checker: ts.TypeChecker, readonly cancellationToken: ts.CancellationToken, readonly searchMeaning: ts.SemanticMeaning, readonly options: Options, private readonly result: ts.Push) { } - - function getSpecialSearchKind(node: ts.Node): SpecialSearchKind { - switch (node.kind) { - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructorKeyword: - return SpecialSearchKind.Constructor; - case ts.SyntaxKind.Identifier: - if (ts.isClassLike(node.parent)) { - ts.Debug.assert(node.parent.name === node); - return SpecialSearchKind.Class; - } - // falls through - default: - return SpecialSearchKind.None; - } + includesSourceFile(sourceFile: ts.SourceFile): boolean { + return this.sourceFilesSet.has(sourceFile.fileName); } - /** Handle a few special cases relating to export/import specifiers. */ - function skipPastExportOrImportSpecifierOrUnion(symbol: ts.Symbol, node: ts.Node, checker: ts.TypeChecker, useLocalSymbolForExportSpecifier: boolean): ts.Symbol | undefined { - const { parent } = node; - if (ts.isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) { - return getLocalSymbolForExportSpecifier(node as ts.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 ts.firstDefined(symbol.declarations, decl => { - if (!decl.parent) { - // Ignore UMD module and global merge - if (symbol.flags & ts.SymbolFlags.Transient) - return undefined; - // Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here. - ts.Debug.fail(`Unexpected symbol at ${ts.Debug.formatSyntaxKind(node.kind)}: ${ts.Debug.formatSymbol(symbol)}`); - } - return ts.isTypeLiteralNode(decl.parent) && ts.isUnionTypeNode(decl.parent.parent) - ? checker.getPropertyOfType(checker.getTypeFromTypeNode(decl.parent.parent), symbol.name) - : undefined; - }); + private importTracker: ts.FindAllReferences.ImportTracker | undefined; + /** Gets every place to look for references of an exported symbols. See `ImportsResult` in `importTracker.ts` for more documentation. */ + getImportSearches(exportSymbol: ts.Symbol, exportInfo: ts.FindAllReferences.ExportInfo): ts.FindAllReferences.ImportsResult { + if (!this.importTracker) + this.importTracker = ts.FindAllReferences.createImportTracker(this.sourceFiles, this.sourceFilesSet, this.checker, this.cancellationToken); + return this.importTracker(exportSymbol, exportInfo, this.options.use === FindReferencesUse.Rename); } - /** - * 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?: ts.FindAllReferences.ImportExport; - readonly symbol: ts.Symbol; - readonly text: string; - readonly escapedText: ts.__String; - /** Only set if `options.implementations` is true. These are the symbols checked to get the implementations of a property access. */ - readonly parents: readonly ts.Symbol[] | undefined; - readonly allSearchSymbols: readonly ts.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: ts.Symbol): boolean; - } - - const enum SpecialSearchKind { - None, - Constructor, - Class - } - - function getNonModuleSymbolOfMergedModuleSymbol(symbol: ts.Symbol) { - if (!(symbol.flags & (ts.SymbolFlags.Module | ts.SymbolFlags.Transient))) - return undefined; - const decl = symbol.declarations && ts.find(symbol.declarations, d => !ts.isSourceFile(d) && !ts.isModuleDeclaration(d)); - return decl && decl.symbol; + /** @param allSearchSymbols set of additional symbols for use by `includes`. */ + createSearch(location: ts.Node | undefined, symbol: ts.Symbol, comingFrom: ts.FindAllReferences.ImportExport | undefined, searchOptions: { + text?: string; + allSearchSymbols?: ts.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 = ts.stripQuotes(ts.symbolName(ts.getLocalSymbolForExportDefault(symbol) || getNonModuleSymbolOfMergedModuleSymbol(symbol) || symbol)), allSearchSymbols = [symbol], } = searchOptions; + const escapedText = ts.escapeLeadingUnderscores(text); + const parents = this.options.implementations && location ? getParentSymbolsOfPropertyAccess(location, symbol, this.checker) : undefined; + return { symbol, comingFrom, text, escapedText, parents, allSearchSymbols, includes: sym => ts.contains(allSearchSymbols, sym) }; } + private readonly symbolIdToReferences: Entry[][] = []; /** - * Holds all state needed for the finding references. - * Unlike `Search`, there is only one `State`. + * 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. */ - class State { - /** Cache for `explicitlyinheritsFrom`. */ - readonly inheritsFromCache = new ts.Map(); - - /** - * 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 = ts.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 = ts.nodeSeenTracker(); - constructor(readonly sourceFiles: readonly ts.SourceFile[], readonly sourceFilesSet: ts.ReadonlySet, readonly specialSearchKind: SpecialSearchKind, readonly checker: ts.TypeChecker, readonly cancellationToken: ts.CancellationToken, readonly searchMeaning: ts.SemanticMeaning, readonly options: Options, private readonly result: ts.Push) { - } - includesSourceFile(sourceFile: ts.SourceFile): boolean { - return this.sourceFilesSet.has(sourceFile.fileName); - } - - private importTracker: ts.FindAllReferences.ImportTracker | undefined; - /** Gets every place to look for references of an exported symbols. See `ImportsResult` in `importTracker.ts` for more documentation. */ - getImportSearches(exportSymbol: ts.Symbol, exportInfo: ts.FindAllReferences.ExportInfo): ts.FindAllReferences.ImportsResult { - if (!this.importTracker) - this.importTracker = ts.FindAllReferences.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: ts.Node | undefined, symbol: ts.Symbol, comingFrom: ts.FindAllReferences.ImportExport | undefined, searchOptions: { - text?: string; - allSearchSymbols?: ts.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 = ts.stripQuotes(ts.symbolName(ts.getLocalSymbolForExportDefault(symbol) || getNonModuleSymbolOfMergedModuleSymbol(symbol) || symbol)), allSearchSymbols = [symbol], } = searchOptions; - const escapedText = ts.escapeLeadingUnderscores(text); - const parents = this.options.implementations && location ? getParentSymbolsOfPropertyAccess(location, symbol, this.checker) : undefined; - return { symbol, comingFrom, text, escapedText, parents, allSearchSymbols, includes: sym => ts.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: ts.Symbol): (node: ts.Node, kind?: NodeEntryKind) => void { - const symbolId = ts.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)); + referenceAdder(searchSymbol: ts.Symbol): (node: ts.Node, kind?: NodeEntryKind) => void { + const symbolId = ts.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: ts.TextSpan): void { - this.result.push({ - definition: undefined, - references: [{ kind: EntryKind.Span, fileName, textSpan }] - }); - } + /** Add a reference with no associated definition. */ + addStringOrCommentReference(fileName: string, textSpan: ts.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.Set[] = []; - /** Returns `true` the first time we search for a symbol in a file and `false` afterwards. */ - markSearchedSymbols(sourceFile: ts.SourceFile, symbols: readonly ts.Symbol[]): boolean { - const sourceId = ts.getNodeId(sourceFile); - const seenSymbols = this.sourceFileToSeenSymbols[sourceId] || (this.sourceFileToSeenSymbols[sourceId] = new ts.Set()); + // Source file ID → symbol ID → Whether the symbol has been searched for in the source file. + private readonly sourceFileToSeenSymbols: ts.Set[] = []; + /** Returns `true` the first time we search for a symbol in a file and `false` afterwards. */ + markSearchedSymbols(sourceFile: ts.SourceFile, symbols: readonly ts.Symbol[]): boolean { + const sourceId = ts.getNodeId(sourceFile); + const seenSymbols = this.sourceFileToSeenSymbols[sourceId] || (this.sourceFileToSeenSymbols[sourceId] = new ts.Set()); - let anyNewSymbols = false; - for (const sym of symbols) { - anyNewSymbols = ts.tryAddToSet(seenSymbols, ts.getSymbolId(sym)) || anyNewSymbols; - } - return anyNewSymbols; + let anyNewSymbols = false; + for (const sym of symbols) { + anyNewSymbols = ts.tryAddToSet(seenSymbols, ts.getSymbolId(sym)) || anyNewSymbols; } + return anyNewSymbols; } + } - /** Search for all imports of a given exported symbol using `State.getImportSearches`. */ - function searchForImportsOfExport(exportLocation: ts.Node, exportSymbol: ts.Symbol, exportInfo: ts.FindAllReferences.ExportInfo, state: State): void { - const { importSearches, singleReferences, indirectUsers } = state.getImportSearches(exportSymbol, exportInfo); + /** Search for all imports of a given exported symbol using `State.getImportSearches`. */ + function searchForImportsOfExport(exportLocation: ts.Node, exportSymbol: ts.Symbol, exportInfo: ts.FindAllReferences.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 `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, ts.FindAllReferences.ImportExport.Export), state); - } + // 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, ts.FindAllReferences.ImportExport.Export), state); + } - if (indirectUsers.length) { - let indirectSearch: Search | undefined; - switch (exportInfo.exportKind) { - case ts.FindAllReferences.ExportKind.Named: - indirectSearch = state.createSearch(exportLocation, exportSymbol, ts.FindAllReferences.ImportExport.Export); - break; - case ts.FindAllReferences.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, ts.FindAllReferences.ImportExport.Export, { text: "default" }); - break; - case ts.FindAllReferences.ExportKind.ExportEquals: - break; - } - if (indirectSearch) { - for (const indirectUser of indirectUsers) { - searchForName(indirectUser, indirectSearch, state); - } + if (indirectUsers.length) { + let indirectSearch: Search | undefined; + switch (exportInfo.exportKind) { + case ts.FindAllReferences.ExportKind.Named: + indirectSearch = state.createSearch(exportLocation, exportSymbol, ts.FindAllReferences.ImportExport.Export); + break; + case ts.FindAllReferences.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, ts.FindAllReferences.ImportExport.Export, { text: "default" }); + break; + case ts.FindAllReferences.ExportKind.ExportEquals: + break; + } + if (indirectSearch) { + for (const indirectUser of indirectUsers) { + searchForName(indirectUser, indirectSearch, state); } } } + } - export function eachExportReference(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined, exportSymbol: ts.Symbol, exportingModuleSymbol: ts.Symbol, exportName: string, isDefaultExport: boolean, cb: (ref: ts.Identifier) => void): void { - const importTracker = ts.FindAllReferences.createImportTracker(sourceFiles, new ts.Set(sourceFiles.map(f => f.fileName)), checker, cancellationToken); - const { importSearches, indirectUsers, singleReferences } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ts.FindAllReferences.ExportKind.Default : ts.FindAllReferences.ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false); - for (const [importLocation] of importSearches) { - cb(importLocation); - } - for (const singleReference of singleReferences) { - if (ts.isIdentifier(singleReference) && ts.isImportTypeNode(singleReference.parent)) { - cb(singleReference); - } + export function eachExportReference(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined, exportSymbol: ts.Symbol, exportingModuleSymbol: ts.Symbol, exportName: string, isDefaultExport: boolean, cb: (ref: ts.Identifier) => void): void { + const importTracker = ts.FindAllReferences.createImportTracker(sourceFiles, new ts.Set(sourceFiles.map(f => f.fileName)), checker, cancellationToken); + const { importSearches, indirectUsers, singleReferences } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ts.FindAllReferences.ExportKind.Default : ts.FindAllReferences.ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false); + for (const [importLocation] of importSearches) { + cb(importLocation); + } + for (const singleReference of singleReferences) { + if (ts.isIdentifier(singleReference) && ts.isImportTypeNode(singleReference.parent)) { + cb(singleReference); } - for (const indirectUser of indirectUsers) { - for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) { - // Import specifiers should be handled by importSearches - const symbol = checker.getSymbolAtLocation(node); - const hasExportAssignmentDeclaration = ts.some(symbol?.declarations, d => ts.tryCast(d, ts.isExportAssignment) ? true : false); - if (ts.isIdentifier(node) && !ts.isImportOrExportSpecifier(node.parent) && (symbol === exportSymbol || hasExportAssignmentDeclaration)) { - cb(node); - } + } + for (const indirectUser of indirectUsers) { + for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) { + // Import specifiers should be handled by importSearches + const symbol = checker.getSymbolAtLocation(node); + const hasExportAssignmentDeclaration = ts.some(symbol?.declarations, d => ts.tryCast(d, ts.isExportAssignment) ? true : false); + if (ts.isIdentifier(node) && !ts.isImportOrExportSpecifier(node.parent) && (symbol === exportSymbol || hasExportAssignmentDeclaration)) { + cb(node); } } } + } - function shouldAddSingleReference(singleRef: ts.Identifier | ts.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 (!ts.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 !(ts.isImportOrExportSpecifier(singleRef.parent) && singleRef.escapedText === ts.InternalSymbolName.Default); - } + function shouldAddSingleReference(singleRef: ts.Identifier | ts.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 (!ts.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 !(ts.isImportOrExportSpecifier(singleRef.parent) && singleRef.escapedText === ts.InternalSymbolName.Default); + } - // Go to the symbol we imported from and find references for it. - function searchForImportedSymbol(symbol: ts.Symbol, state: State): void { - if (!symbol.declarations) - return; + // Go to the symbol we imported from and find references for it. + function searchForImportedSymbol(symbol: ts.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, ts.FindAllReferences.ImportExport.Import), state, state.includesSourceFile(exportingFile)); - } + 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, ts.FindAllReferences.ImportExport.Import), state, state.includesSourceFile(exportingFile)); } + } - /** Search for all occurrences of an identifier in a source file (and filter out the ones that match). */ - function searchForName(sourceFile: ts.SourceFile, search: Search, state: State): void { - if (ts.getNameTable(sourceFile).get(search.escapedText) !== undefined) { - getReferencesInSourceFile(sourceFile, search, state); - } + /** Search for all occurrences of an identifier in a source file (and filter out the ones that match). */ + function searchForName(sourceFile: ts.SourceFile, search: Search, state: State): void { + if (ts.getNameTable(sourceFile).get(search.escapedText) !== undefined) { + getReferencesInSourceFile(sourceFile, search, state); } + } - function getPropertySymbolOfDestructuringAssignment(location: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { - return ts.isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) - ? checker.getPropertySymbolOfDestructuringAssignment(location as ts.Identifier) - : undefined; + function getPropertySymbolOfDestructuringAssignment(location: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { + return ts.isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) + ? checker.getPropertySymbolOfDestructuringAssignment(location as ts.Identifier) + : 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: ts.Symbol): ts.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 === ts.SyntaxKind.FunctionExpression || valueDeclaration.kind === ts.SyntaxKind.ClassExpression)) { + return valueDeclaration; } - /** - * 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: ts.Symbol): ts.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 === ts.SyntaxKind.FunctionExpression || valueDeclaration.kind === ts.SyntaxKind.ClassExpression)) { - return valueDeclaration; - } + if (!declarations) { + return undefined; + } - if (!declarations) { - return undefined; + // If this is private property or method, the scope is the containing class + if (flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method)) { + const privateDeclaration = ts.find(declarations, d => ts.hasEffectiveModifier(d, ts.ModifierFlags.Private) || ts.isPrivateIdentifierClassElementDeclaration(d)); + if (privateDeclaration) { + return ts.getAncestor(privateDeclaration, ts.SyntaxKind.ClassDeclaration); } + // Else this is a public property and could be accessed from anywhere. + return undefined; + } - // If this is private property or method, the scope is the containing class - if (flags & (ts.SymbolFlags.Property | ts.SymbolFlags.Method)) { - const privateDeclaration = ts.find(declarations, d => ts.hasEffectiveModifier(d, ts.ModifierFlags.Private) || ts.isPrivateIdentifierClassElementDeclaration(d)); - if (privateDeclaration) { - return ts.getAncestor(privateDeclaration, ts.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(ts.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 & ts.SymbolFlags.TypeParameter); + if (exposedByParent && !(ts.isExternalModuleSymbol(parent) && !parent.globalExports)) { + 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(ts.isObjectBindingElementWithoutPropertyName)) { + let scope: ts.Node | undefined; + for (const declaration of declarations) { + const container = ts.getContainerNode(declaration); + if (scope && scope !== container) { + // Different declarations have different containers, bail out 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 & ts.SymbolFlags.TypeParameter); - if (exposedByParent && !(ts.isExternalModuleSymbol(parent) && !parent.globalExports)) { + if (!container || container.kind === ts.SyntaxKind.SourceFile && !ts.isExternalOrCommonJsModule(container as ts.SourceFile)) { + // This is a global variable and not an external module, any declaration defined + // within this scope is visible outside the file return undefined; } - let scope: ts.Node | undefined; - for (const declaration of declarations) { - const container = ts.getContainerNode(declaration); - if (scope && scope !== container) { - // Different declarations have different containers, bail out - return undefined; - } - - if (!container || container.kind === ts.SyntaxKind.SourceFile && !ts.isExternalOrCommonJsModule(container as ts.SourceFile)) { - // This is a global variable and not an external module, any declaration defined - // within this scope is visible outside the file - return undefined; - } - - scope = container; - if (ts.isFunctionExpression(scope)) { - let next: ts.Node | undefined; - while (next = ts.getNextJSDocCommentLocation(scope)) { - scope = next; - } + scope = container; + if (ts.isFunctionExpression(scope)) { + let next: ts.Node | undefined; + while (next = ts.getNextJSDocCommentLocation(scope)) { + scope = next; } } - - // 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: ts.Identifier, checker: ts.TypeChecker, sourceFile: ts.SourceFile, searchContainer: ts.Node = sourceFile): boolean { - return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true, searchContainer) || false; - } + // 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 + } - export function eachSymbolReferenceInFile(definition: ts.Identifier, checker: ts.TypeChecker, sourceFile: ts.SourceFile, cb: (token: ts.Identifier) => T, searchContainer: ts.Node = sourceFile): T | undefined { - const symbol = ts.isParameterPropertyDeclaration(definition.parent, definition.parent.parent) - ? ts.first(checker.getSymbolsOfParameterPropertyDeclaration(definition.parent, definition.text)) - : checker.getSymbolAtLocation(definition); - if (!symbol) - return undefined; - for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name, searchContainer)) { - if (!ts.isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) - continue; - const referenceSymbol = checker.getSymbolAtLocation(token)!; - if (referenceSymbol === symbol - || checker.getShorthandAssignmentValueSymbol(token.parent) === symbol - || ts.isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) { - const res = cb(token); - if (res) - return res; - } + /** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */ + export function isSymbolReferencedInFile(definition: ts.Identifier, checker: ts.TypeChecker, sourceFile: ts.SourceFile, searchContainer: ts.Node = sourceFile): boolean { + return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true, searchContainer) || false; + } + + export function eachSymbolReferenceInFile(definition: ts.Identifier, checker: ts.TypeChecker, sourceFile: ts.SourceFile, cb: (token: ts.Identifier) => T, searchContainer: ts.Node = sourceFile): T | undefined { + const symbol = ts.isParameterPropertyDeclaration(definition.parent, definition.parent.parent) + ? ts.first(checker.getSymbolsOfParameterPropertyDeclaration(definition.parent, definition.text)) + : checker.getSymbolAtLocation(definition); + if (!symbol) + return undefined; + for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name, searchContainer)) { + if (!ts.isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) + continue; + const referenceSymbol = checker.getSymbolAtLocation(token)!; + if (referenceSymbol === symbol + || checker.getShorthandAssignmentValueSymbol(token.parent) === symbol + || ts.isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) { + const res = cb(token); + if (res) + return res; } } + } - export function getTopMostDeclarationNamesInFile(declarationName: string, sourceFile: ts.SourceFile): readonly ts.Node[] { - const candidates = ts.filter(getPossibleSymbolReferenceNodes(sourceFile, declarationName), name => !!ts.getDeclarationFromName(name)); - return candidates.reduce((topMost, decl) => { - const depth = getDepth(decl); - if (!ts.some(topMost.declarationNames) || depth === topMost.depth) { - topMost.declarationNames.push(decl); - topMost.depth = depth; - } - else if (depth < topMost.depth) { - topMost.declarationNames = [decl]; - topMost.depth = depth; - } - return topMost; - }, { depth: Infinity, declarationNames: [] as ts.Node[] }).declarationNames; - function getDepth(declaration: ts.Node | undefined) { - let depth = 0; - while (declaration) { - declaration = ts.getContainerNode(declaration); - depth++; - } - return depth; + export function getTopMostDeclarationNamesInFile(declarationName: string, sourceFile: ts.SourceFile): readonly ts.Node[] { + const candidates = ts.filter(getPossibleSymbolReferenceNodes(sourceFile, declarationName), name => !!ts.getDeclarationFromName(name)); + return candidates.reduce((topMost, decl) => { + const depth = getDepth(decl); + if (!ts.some(topMost.declarationNames) || depth === topMost.depth) { + topMost.declarationNames.push(decl); + topMost.depth = depth; } + else if (depth < topMost.depth) { + topMost.declarationNames = [decl]; + topMost.depth = depth; + } + return topMost; + }, { depth: Infinity, declarationNames: [] as ts.Node[] }).declarationNames; + function getDepth(declaration: ts.Node | undefined) { + let depth = 0; + while (declaration) { + declaration = ts.getContainerNode(declaration); + depth++; + } + return depth; } + } - export function someSignatureUsage(signature: ts.SignatureDeclaration, sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cb: (name: ts.Identifier, call?: ts.CallExpression) => boolean): boolean { - if (!signature.name || !ts.isIdentifier(signature.name)) - return false; - const symbol = ts.Debug.checkDefined(checker.getSymbolAtLocation(signature.name)); - - for (const sourceFile of sourceFiles) { - for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { - if (!ts.isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) - continue; - const called = ts.climbPastPropertyAccess(name); - const call = ts.isCallExpression(called.parent) && called.parent.expression === called ? called.parent : undefined; - const referenceSymbol = checker.getSymbolAtLocation(name); - if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) { - if (cb(name, call)) { - return true; - } + export function someSignatureUsage(signature: ts.SignatureDeclaration, sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cb: (name: ts.Identifier, call?: ts.CallExpression) => boolean): boolean { + if (!signature.name || !ts.isIdentifier(signature.name)) + return false; + const symbol = ts.Debug.checkDefined(checker.getSymbolAtLocation(signature.name)); + + for (const sourceFile of sourceFiles) { + for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { + if (!ts.isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) + continue; + const called = ts.climbPastPropertyAccess(name); + const call = ts.isCallExpression(called.parent) && called.parent.expression === called ? called.parent : undefined; + const referenceSymbol = checker.getSymbolAtLocation(name); + if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) { + if (cb(name, call)) { + return true; } } } - return false; } + return false; + } - function getPossibleSymbolReferenceNodes(sourceFile: ts.SourceFile, symbolName: string, container: ts.Node = sourceFile): readonly ts.Node[] { - return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => ts.getTouchingPropertyName(sourceFile, pos)); - } + function getPossibleSymbolReferenceNodes(sourceFile: ts.SourceFile, symbolName: string, container: ts.Node = sourceFile): readonly ts.Node[] { + return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => ts.getTouchingPropertyName(sourceFile, pos)); + } - function getPossibleSymbolReferencePositions(sourceFile: ts.SourceFile, symbolName: string, container: ts.Node = sourceFile): readonly number[] { - const positions: number[] = []; + function getPossibleSymbolReferencePositions(sourceFile: ts.SourceFile, symbolName: string, container: ts.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. + /// 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; - } + // 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; + 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; + 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; + // 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 || !ts.isIdentifierPart(text.charCodeAt(position - 1), ts.ScriptTarget.Latest)) && - (endPosition === sourceLength || !ts.isIdentifierPart(text.charCodeAt(endPosition), ts.ScriptTarget.Latest))) { - // Found a real match. Keep searching. - positions.push(position); - } - position = text.indexOf(symbolName, position + symbolNameLength + 1); + if ((position === 0 || !ts.isIdentifierPart(text.charCodeAt(position - 1), ts.ScriptTarget.Latest)) && + (endPosition === sourceLength || !ts.isIdentifierPart(text.charCodeAt(endPosition), ts.ScriptTarget.Latest))) { + // Found a real match. Keep searching. + positions.push(position); } - - return positions; + position = text.indexOf(symbolName, position + symbolNameLength + 1); } - function getLabelReferencesInNode(container: ts.Node, targetLabel: ts.Identifier): SymbolAndEntries[] { - const sourceFile = container.getSourceFile(); - const labelName = targetLabel.text; - const references = ts.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 || (ts.isJumpStatementTarget(node) && ts.getTargetLabel(node, labelName) === targetLabel) ? nodeEntry(node) : undefined); - return [{ definition: { type: DefinitionKind.Label, node: targetLabel }, references }]; - } + return positions; + } - function isValidReferencePosition(node: ts.Node, searchSymbolName: string): boolean { - // Compare the length so we filter out strict superstrings of the symbol we are looking for - switch (node.kind) { - case ts.SyntaxKind.PrivateIdentifier: - if (ts.isJSDocMemberName(node.parent)) { - return true; - } - // falls through I guess - case ts.SyntaxKind.Identifier: - return (node as ts.PrivateIdentifier | ts.Identifier).text.length === searchSymbolName.length; - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.StringLiteral: { - const str = node as ts.StringLiteralLike; - return (ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(str) || ts.isNameOfModuleDeclaration(node) || ts.isExpressionOfExternalModuleImportEqualsDeclaration(node) || (ts.isCallExpression(node.parent) && ts.isBindableObjectDefinePropertyCall(node.parent) && node.parent.arguments[1] === node)) && - str.text.length === searchSymbolName.length; + function getLabelReferencesInNode(container: ts.Node, targetLabel: ts.Identifier): SymbolAndEntries[] { + const sourceFile = container.getSourceFile(); + const labelName = targetLabel.text; + const references = ts.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 || (ts.isJumpStatementTarget(node) && ts.getTargetLabel(node, labelName) === targetLabel) ? nodeEntry(node) : undefined); + return [{ definition: { type: DefinitionKind.Label, node: targetLabel }, references }]; + } + + function isValidReferencePosition(node: ts.Node, searchSymbolName: string): boolean { + // Compare the length so we filter out strict superstrings of the symbol we are looking for + switch (node.kind) { + case ts.SyntaxKind.PrivateIdentifier: + if (ts.isJSDocMemberName(node.parent)) { + return true; } + // falls through I guess + case ts.SyntaxKind.Identifier: + return (node as ts.PrivateIdentifier | ts.Identifier).text.length === searchSymbolName.length; + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.StringLiteral: { + const str = node as ts.StringLiteralLike; + return (ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(str) || ts.isNameOfModuleDeclaration(node) || ts.isExpressionOfExternalModuleImportEqualsDeclaration(node) || (ts.isCallExpression(node.parent) && ts.isBindableObjectDefinePropertyCall(node.parent) && node.parent.arguments[1] === node)) && + str.text.length === searchSymbolName.length; + } - case ts.SyntaxKind.NumericLiteral: - return ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(node as ts.NumericLiteral) && (node as ts.NumericLiteral).text.length === searchSymbolName.length; - case ts.SyntaxKind.DefaultKeyword: - return "default".length === searchSymbolName.length; + case ts.SyntaxKind.NumericLiteral: + return ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(node as ts.NumericLiteral) && (node as ts.NumericLiteral).text.length === searchSymbolName.length; + case ts.SyntaxKind.DefaultKeyword: + return "default".length === searchSymbolName.length; - default: - return false; - } + default: + return false; } + } - function getAllReferencesForImportMeta(sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { - const references = ts.flatMap(sourceFiles, sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "meta", sourceFile), node => { - const parent = node.parent; - if (ts.isImportMeta(parent)) { - return nodeEntry(parent); - } - }); + function getAllReferencesForImportMeta(sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { + const references = ts.flatMap(sourceFiles, sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "meta", sourceFile), node => { + const parent = node.parent; + if (ts.isImportMeta(parent)) { + return nodeEntry(parent); + } }); - return references.length ? [{ definition: { type: DefinitionKind.Keyword, node: references[0].node }, references }] : undefined; - } + }); + return references.length ? [{ definition: { type: DefinitionKind.Keyword, node: references[0].node }, references }] : undefined; + } - function getAllReferencesForKeyword(sourceFiles: readonly ts.SourceFile[], keywordKind: ts.SyntaxKind, cancellationToken: ts.CancellationToken, filter?: (node: ts.Node) => boolean): SymbolAndEntries[] | undefined { - const references = ts.flatMap(sourceFiles, sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, ts.tokenToString(keywordKind)!, sourceFile), referenceLocation => { - if (referenceLocation.kind === keywordKind && (!filter || filter(referenceLocation))) { - return nodeEntry(referenceLocation); - } - }); + function getAllReferencesForKeyword(sourceFiles: readonly ts.SourceFile[], keywordKind: ts.SyntaxKind, cancellationToken: ts.CancellationToken, filter?: (node: ts.Node) => boolean): SymbolAndEntries[] | undefined { + const references = ts.flatMap(sourceFiles, sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, ts.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: ts.SourceFile, search: Search, state: State, addReferencesHere = true): void { - state.cancellationToken.throwIfCancellationRequested(); - return getReferencesInContainer(sourceFile, sourceFile, search, state, addReferencesHere); - } + }); + return references.length ? [{ definition: { type: DefinitionKind.Keyword, node: references[0].node }, references }] : undefined; + } - /** - * 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: ts.Node, sourceFile: ts.SourceFile, search: Search, state: State, addReferencesHere: boolean): void { - if (!state.markSearchedSymbols(sourceFile, search.allSearchSymbols)) { - return; - } + function getReferencesInSourceFile(sourceFile: ts.SourceFile, search: Search, state: State, addReferencesHere = true): void { + state.cancellationToken.throwIfCancellationRequested(); + return getReferencesInContainer(sourceFile, sourceFile, search, state, addReferencesHere); + } - for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container)) { - getReferencesAtLocation(sourceFile, position, 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: ts.Node, sourceFile: ts.SourceFile, search: Search, state: State, addReferencesHere: boolean): void { + if (!state.markSearchedSymbols(sourceFile, search.allSearchSymbols)) { + return; } - function hasMatchingMeaning(referenceLocation: ts.Node, state: State): boolean { - return !!(ts.getMeaningFromLocation(referenceLocation) & state.searchMeaning); + for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container)) { + getReferencesAtLocation(sourceFile, position, search, state, addReferencesHere); } + } - function getReferencesAtLocation(sourceFile: ts.SourceFile, position: number, search: Search, state: State, addReferencesHere: boolean): void { - const referenceLocation = ts.getTouchingPropertyName(sourceFile, position); + function hasMatchingMeaning(referenceLocation: ts.Node, state: State): boolean { + return !!(ts.getMeaningFromLocation(referenceLocation) & state.searchMeaning); + } - 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 && ts.isInString(sourceFile, position) || state.options.findInComments && ts.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, ts.createTextSpan(position, search.text.length)); - } + function getReferencesAtLocation(sourceFile: ts.SourceFile, position: number, search: Search, state: State, addReferencesHere: boolean): void { + const referenceLocation = ts.getTouchingPropertyName(sourceFile, position); - return; + 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 && ts.isInString(sourceFile, position) || state.options.findInComments && ts.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, ts.createTextSpan(position, search.text.length)); } - if (!hasMatchingMeaning(referenceLocation, state)) - return; + return; + } - let referenceSymbol = state.checker.getSymbolAtLocation(referenceLocation); - if (!referenceSymbol) { - return; - } + if (!hasMatchingMeaning(referenceLocation, state)) + return; - const parent = referenceLocation.parent; - if (ts.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; - } + let referenceSymbol = state.checker.getSymbolAtLocation(referenceLocation); + if (!referenceSymbol) { + return; + } - if (ts.isExportSpecifier(parent)) { - ts.Debug.assert(referenceLocation.kind === ts.SyntaxKind.Identifier); - getReferencesAtExportSpecifier(referenceLocation as ts.Identifier, referenceSymbol, parent, search, state, addReferencesHere); - return; - } + const parent = referenceLocation.parent; + if (ts.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; + } - const relatedSymbol = getRelatedSymbol(search, referenceSymbol, referenceLocation, state); - if (!relatedSymbol) { - getReferenceForShorthandProperty(referenceSymbol, search, state); - return; - } + if (ts.isExportSpecifier(parent)) { + ts.Debug.assert(referenceLocation.kind === ts.SyntaxKind.Identifier); + getReferencesAtExportSpecifier(referenceLocation as ts.Identifier, referenceSymbol, parent, search, state, addReferencesHere); + 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: - ts.Debug.assertNever(state.specialSearchKind); - } + const relatedSymbol = getRelatedSymbol(search, referenceSymbol, referenceLocation, state); + if (!relatedSymbol) { + getReferenceForShorthandProperty(referenceSymbol, search, state); + return; + } - // Use the parent symbol if the location is commonjs require syntax on javascript files only. - if (ts.isInJSFile(referenceLocation) - && referenceLocation.parent.kind === ts.SyntaxKind.BindingElement - && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(referenceLocation.parent)) { - referenceSymbol = referenceLocation.parent.symbol; - // The parent will not have a symbol if it's an ObjectBindingPattern (when destructuring is used). In - // this case, just skip it, since the bound identifiers are not an alias of the import. - if (!referenceSymbol) - 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: + ts.Debug.assertNever(state.specialSearchKind); + } - getImportOrExportReferences(referenceLocation, referenceSymbol, search, state); + // Use the parent symbol if the location is commonjs require syntax on javascript files only. + if (ts.isInJSFile(referenceLocation) + && referenceLocation.parent.kind === ts.SyntaxKind.BindingElement + && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(referenceLocation.parent)) { + referenceSymbol = referenceLocation.parent.symbol; + // The parent will not have a symbol if it's an ObjectBindingPattern (when destructuring is used). In + // this case, just skip it, since the bound identifiers are not an alias of the import. + if (!referenceSymbol) + return; } - function getReferencesAtExportSpecifier(referenceLocation: ts.Identifier, referenceSymbol: ts.Symbol, exportSpecifier: ts.ExportSpecifier, search: Search, state: State, addReferencesHere: boolean, alwaysGetReferences?: boolean): void { - ts.Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled"); + getImportOrExportReferences(referenceLocation, referenceSymbol, search, state); + } - const { parent, propertyName, name } = exportSpecifier; - const exportDeclaration = parent.parent; - const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker); - if (!alwaysGetReferences && !search.includes(localSymbol)) { - return; - } + function getReferencesAtExportSpecifier(referenceLocation: ts.Identifier, referenceSymbol: ts.Symbol, exportSpecifier: ts.ExportSpecifier, search: Search, state: State, addReferencesHere: boolean, alwaysGetReferences?: boolean): void { + ts.Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled"); - 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 === ts.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(); - } + const { parent, propertyName, name } = exportSpecifier; + const exportDeclaration = parent.parent; + const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker); + if (!alwaysGetReferences && !search.includes(localSymbol)) { + return; + } - if (addReferencesHere && state.options.use !== FindReferencesUse.Rename && state.markSeenReExportRHS(name)) { - addReference(name, ts.Debug.checkDefined(exportSpecifier.symbol), state); - } + 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 === ts.InternalSymbolName.Default))) { + addRef(); } - else { - if (state.markSeenReExportRHS(referenceLocation)) { - 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(); } - // For `export { foo as bar }`, rename `foo`, but not `bar`. - if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) { - const isDefaultExport = referenceLocation.originalKeywordKind === ts.SyntaxKind.DefaultKeyword - || exportSpecifier.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword; - const exportKind = isDefaultExport ? ts.FindAllReferences.ExportKind.Default : ts.FindAllReferences.ExportKind.Named; - const exportSymbol = ts.Debug.checkDefined(exportSpecifier.symbol); - const exportInfo = ts.FindAllReferences.getExportInfo(exportSymbol, exportKind, state.checker); - if (exportInfo) { - searchForImportsOfExport(referenceLocation, exportSymbol, exportInfo, state); - } + if (addReferencesHere && state.options.use !== FindReferencesUse.Rename && state.markSeenReExportRHS(name)) { + addReference(name, ts.Debug.checkDefined(exportSpecifier.symbol), state); } - - // At `export { x } from "foo"`, also search for the imported symbol `"foo".x`. - if (search.comingFrom !== ts.FindAllReferences.ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) { - const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); - if (imported) - searchForImportedSymbol(imported, state); + } + else { + if (state.markSeenReExportRHS(referenceLocation)) { + addRef(); } + } - function addRef() { - if (addReferencesHere) - addReference(referenceLocation, localSymbol, state); + // For `export { foo as bar }`, rename `foo`, but not `bar`. + if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) { + const isDefaultExport = referenceLocation.originalKeywordKind === ts.SyntaxKind.DefaultKeyword + || exportSpecifier.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword; + const exportKind = isDefaultExport ? ts.FindAllReferences.ExportKind.Default : ts.FindAllReferences.ExportKind.Named; + const exportSymbol = ts.Debug.checkDefined(exportSpecifier.symbol); + const exportInfo = ts.FindAllReferences.getExportInfo(exportSymbol, exportKind, state.checker); + if (exportInfo) { + searchForImportsOfExport(referenceLocation, exportSymbol, exportInfo, state); } } - function getLocalSymbolForExportSpecifier(referenceLocation: ts.Identifier, referenceSymbol: ts.Symbol, exportSpecifier: ts.ExportSpecifier, checker: ts.TypeChecker): ts.Symbol { - return isExportSpecifierAlias(referenceLocation, exportSpecifier) && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) || referenceSymbol; + // At `export { x } from "foo"`, also search for the imported symbol `"foo".x`. + if (search.comingFrom !== ts.FindAllReferences.ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) { + const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); + if (imported) + searchForImportedSymbol(imported, state); } - function isExportSpecifierAlias(referenceLocation: ts.Identifier, exportSpecifier: ts.ExportSpecifier): boolean { - const { parent, propertyName, name } = exportSpecifier; - ts.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 addRef() { + if (addReferencesHere) + addReference(referenceLocation, localSymbol, state); } + } - function getImportOrExportReferences(referenceLocation: ts.Node, referenceSymbol: ts.Symbol, search: Search, state: State): void { - const importOrExport = ts.FindAllReferences.getImportOrExportSymbol(referenceLocation, referenceSymbol, state.checker, search.comingFrom === ts.FindAllReferences.ImportExport.Export); - if (!importOrExport) - return; - - const { symbol } = importOrExport; + function getLocalSymbolForExportSpecifier(referenceLocation: ts.Identifier, referenceSymbol: ts.Symbol, exportSpecifier: ts.ExportSpecifier, checker: ts.TypeChecker): ts.Symbol { + return isExportSpecifierAlias(referenceLocation, exportSpecifier) && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) || referenceSymbol; + } - if (importOrExport.kind === ts.FindAllReferences.ImportExport.Import) { - if (!(isForRenameWithPrefixAndSuffixText(state.options))) { - searchForImportedSymbol(symbol, state); - } - } - else { - searchForImportsOfExport(referenceLocation, symbol, importOrExport.exportInfo, state); - } + function isExportSpecifierAlias(referenceLocation: ts.Identifier, exportSpecifier: ts.ExportSpecifier): boolean { + const { parent, propertyName, name } = exportSpecifier; + ts.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 getReferenceForShorthandProperty({ flags, valueDeclaration }: ts.Symbol, search: Search, state: State): void { - const shorthandValueSymbol = state.checker.getShorthandAssignmentValueSymbol(valueDeclaration)!; - const name = valueDeclaration && ts.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 & ts.SymbolFlags.Transient) && name && search.includes(shorthandValueSymbol)) { - addReference(name, shorthandValueSymbol, state); + function getImportOrExportReferences(referenceLocation: ts.Node, referenceSymbol: ts.Symbol, search: Search, state: State): void { + const importOrExport = ts.FindAllReferences.getImportOrExportSymbol(referenceLocation, referenceSymbol, state.checker, search.comingFrom === ts.FindAllReferences.ImportExport.Export); + if (!importOrExport) + return; + + const { symbol } = importOrExport; + + if (importOrExport.kind === ts.FindAllReferences.ImportExport.Import) { + if (!(isForRenameWithPrefixAndSuffixText(state.options))) { + searchForImportedSymbol(symbol, state); } } + else { + searchForImportsOfExport(referenceLocation, symbol, importOrExport.exportInfo, state); + } + } + + function getReferenceForShorthandProperty({ flags, valueDeclaration }: ts.Symbol, search: Search, state: State): void { + const shorthandValueSymbol = state.checker.getShorthandAssignmentValueSymbol(valueDeclaration)!; + const name = valueDeclaration && ts.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 & ts.SymbolFlags.Transient) && name && search.includes(shorthandValueSymbol)) { + addReference(name, shorthandValueSymbol, state); + } + } - function addReference(referenceLocation: ts.Node, relatedSymbol: ts.Symbol | RelatedSymbol, state: State): void { - const { kind, symbol } = "kind" in relatedSymbol ? relatedSymbol : { kind: undefined, symbol: relatedSymbol }; // eslint-disable-line no-in-operator + function addReference(referenceLocation: ts.Node, relatedSymbol: ts.Symbol | RelatedSymbol, state: State): void { + const { kind, symbol } = "kind" in relatedSymbol ? relatedSymbol : { kind: undefined, symbol: relatedSymbol }; // eslint-disable-line no-in-operator - // if rename symbol from default export anonymous function, for example `export default function() {}`, we do not need to add reference - if (state.options.use === FindReferencesUse.Rename && referenceLocation.kind === ts.SyntaxKind.DefaultKeyword) { - return; - } + // if rename symbol from default export anonymous function, for example `export default function() {}`, we do not need to add reference + if (state.options.use === FindReferencesUse.Rename && referenceLocation.kind === ts.SyntaxKind.DefaultKeyword) { + return; + } - const addRef = state.referenceAdder(symbol); - if (state.options.implementations) { - addImplementationReferences(referenceLocation, addRef, state); - } - else { - addRef(referenceLocation, kind); - } + 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: ts.Node, sourceFile: ts.SourceFile, search: Search, state: State): void { - if (ts.isNewExpressionTarget(referenceLocation)) { - addReference(referenceLocation, search.symbol, state); - } + /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ + function addConstructorReferences(referenceLocation: ts.Node, sourceFile: ts.SourceFile, search: Search, state: State): void { + if (ts.isNewExpressionTarget(referenceLocation)) { + addReference(referenceLocation, search.symbol, state); + } - const pusher = () => state.referenceAdder(search.symbol); + const pusher = () => state.referenceAdder(search.symbol); - if (ts.isClassLike(referenceLocation.parent)) { - ts.Debug.assert(referenceLocation.kind === ts.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 (ts.isClassLike(referenceLocation.parent)) { + ts.Debug.assert(referenceLocation.kind === ts.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); } } + } - function addClassStaticThisReferences(referenceLocation: ts.Node, search: Search, state: State): void { - addReference(referenceLocation, search.symbol, state); - const classLike = referenceLocation.parent; - if (state.options.use === FindReferencesUse.Rename || !ts.isClassLike(classLike)) - return; - ts.Debug.assert(classLike.name === referenceLocation); - const addRef = state.referenceAdder(search.symbol); - for (const member of classLike.members) { - if (!(ts.isMethodOrAccessor(member) && ts.isStatic(member))) { - continue; - } - if (member.body) { - member.body.forEachChild(function cb(node) { - if (node.kind === ts.SyntaxKind.ThisKeyword) { - addRef(node); - } - else if (!ts.isFunctionLike(node) && !ts.isClassLike(node)) { - node.forEachChild(cb); - } - }); - } + function addClassStaticThisReferences(referenceLocation: ts.Node, search: Search, state: State): void { + addReference(referenceLocation, search.symbol, state); + const classLike = referenceLocation.parent; + if (state.options.use === FindReferencesUse.Rename || !ts.isClassLike(classLike)) + return; + ts.Debug.assert(classLike.name === referenceLocation); + const addRef = state.referenceAdder(search.symbol); + for (const member of classLike.members) { + if (!(ts.isMethodOrAccessor(member) && ts.isStatic(member))) { + continue; + } + if (member.body) { + member.body.forEachChild(function cb(node) { + if (node.kind === ts.SyntaxKind.ThisKeyword) { + addRef(node); + } + else if (!ts.isFunctionLike(node) && !ts.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: ts.Symbol, sourceFile: ts.SourceFile, addNode: (node: ts.Node) => void): void { - const constructorSymbol = getClassConstructorSymbol(classSymbol); - if (constructorSymbol && constructorSymbol.declarations) { - for (const decl of constructorSymbol.declarations) { - const ctrKeyword = ts.findChildOfKind(decl, ts.SyntaxKind.ConstructorKeyword, sourceFile)!; - ts.Debug.assert(decl.kind === ts.SyntaxKind.Constructor && !!ctrKeyword); - addNode(ctrKeyword); + /** + * `classSymbol` is the class where the constructor was defined. + * Reference the constructor and all calls to `new this()`. + */ + function findOwnConstructorReferences(classSymbol: ts.Symbol, sourceFile: ts.SourceFile, addNode: (node: ts.Node) => void): void { + const constructorSymbol = getClassConstructorSymbol(classSymbol); + if (constructorSymbol && constructorSymbol.declarations) { + for (const decl of constructorSymbol.declarations) { + const ctrKeyword = ts.findChildOfKind(decl, ts.SyntaxKind.ConstructorKeyword, sourceFile)!; + ts.Debug.assert(decl.kind === ts.SyntaxKind.Constructor && !!ctrKeyword); + addNode(ctrKeyword); + } + } + + if (classSymbol.exports) { + classSymbol.exports.forEach(member => { + const decl = member.valueDeclaration; + if (decl && decl.kind === ts.SyntaxKind.MethodDeclaration) { + const body = (decl as ts.MethodDeclaration).body; + if (body) { + forEachDescendantOfKind(body, ts.SyntaxKind.ThisKeyword, thisKeyword => { + if (ts.isNewExpressionTarget(thisKeyword)) { + addNode(thisKeyword); + } + }); + } } - } + }); + } + } - if (classSymbol.exports) { - classSymbol.exports.forEach(member => { - const decl = member.valueDeclaration; - if (decl && decl.kind === ts.SyntaxKind.MethodDeclaration) { - const body = (decl as ts.MethodDeclaration).body; - if (body) { - forEachDescendantOfKind(body, ts.SyntaxKind.ThisKeyword, thisKeyword => { - if (ts.isNewExpressionTarget(thisKeyword)) { - addNode(thisKeyword); - } - }); - } + function getClassConstructorSymbol(classSymbol: ts.Symbol): ts.Symbol | undefined { + return classSymbol.members && classSymbol.members.get(ts.InternalSymbolName.Constructor); + } + + /** Find references to `super` in the constructor of an extending class. */ + function findSuperConstructorAccesses(classDeclaration: ts.ClassLikeDeclaration, addNode: (node: ts.Node) => void): void { + const constructor = getClassConstructorSymbol(classDeclaration.symbol); + if (!(constructor && constructor.declarations)) { + return; + } + + for (const decl of constructor.declarations) { + ts.Debug.assert(decl.kind === ts.SyntaxKind.Constructor); + const body = (decl as ts.ConstructorDeclaration).body; + if (body) { + forEachDescendantOfKind(body, ts.SyntaxKind.SuperKeyword, node => { + if (ts.isCallExpressionTarget(node)) { + addNode(node); } }); } } + } - function getClassConstructorSymbol(classSymbol: ts.Symbol): ts.Symbol | undefined { - return classSymbol.members && classSymbol.members.get(ts.InternalSymbolName.Constructor); - } + function hasOwnConstructor(classDeclaration: ts.ClassLikeDeclaration): boolean { + return !!getClassConstructorSymbol(classDeclaration.symbol); + } - /** Find references to `super` in the constructor of an extending class. */ - function findSuperConstructorAccesses(classDeclaration: ts.ClassLikeDeclaration, addNode: (node: ts.Node) => void): void { - const constructor = getClassConstructorSymbol(classDeclaration.symbol); - if (!(constructor && constructor.declarations)) { - return; - } + function findInheritedConstructorReferences(classDeclaration: ts.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); + } - for (const decl of constructor.declarations) { - ts.Debug.assert(decl.kind === ts.SyntaxKind.Constructor); - const body = (decl as ts.ConstructorDeclaration).body; - if (body) { - forEachDescendantOfKind(body, ts.SyntaxKind.SuperKeyword, node => { - if (ts.isCallExpressionTarget(node)) { - addNode(node); - } - }); - } - } + function addImplementationReferences(refNode: ts.Node, addReference: (node: ts.Node) => void, state: State): void { + // Check if we found a function/propertyAssignment/method with an implementation or initializer + if (ts.isDeclarationName(refNode) && isImplementation(refNode.parent)) { + addReference(refNode); + return; } - function hasOwnConstructor(classDeclaration: ts.ClassLikeDeclaration): boolean { - return !!getClassConstructorSymbol(classDeclaration.symbol); + if (refNode.kind !== ts.SyntaxKind.Identifier) { + return; } - function findInheritedConstructorReferences(classDeclaration: ts.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); + if (refNode.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + // Go ahead and dereference the shorthand assignment by going to its definition + getReferenceEntriesForShorthandPropertyAssignment(refNode, state.checker, addReference); } - function addImplementationReferences(refNode: ts.Node, addReference: (node: ts.Node) => void, state: State): void { - // Check if we found a function/propertyAssignment/method with an implementation or initializer - if (ts.isDeclarationName(refNode) && isImplementation(refNode.parent)) { - addReference(refNode); - return; - } - - if (refNode.kind !== ts.SyntaxKind.Identifier) { - return; - } - - if (refNode.parent.kind === ts.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; + } - // 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 = ts.findAncestor(refNode, a => !ts.isQualifiedName(a.parent) && !ts.isTypeNode(a.parent) && !ts.isTypeElement(a.parent))!; + const typeHavingNode = typeNode.parent; + if (ts.hasType(typeHavingNode) && typeHavingNode.type === typeNode && state.markSeenContainingTypeReference(typeHavingNode)) { + if (ts.hasInitializer(typeHavingNode)) { + addIfImplementation(typeHavingNode.initializer!); } - - // 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 = ts.findAncestor(refNode, a => !ts.isQualifiedName(a.parent) && !ts.isTypeNode(a.parent) && !ts.isTypeElement(a.parent))!; - const typeHavingNode = typeNode.parent; - if (ts.hasType(typeHavingNode) && typeHavingNode.type === typeNode && state.markSeenContainingTypeReference(typeHavingNode)) { - if (ts.hasInitializer(typeHavingNode)) { - addIfImplementation(typeHavingNode.initializer!); - } - else if (ts.isFunctionLike(typeHavingNode) && (typeHavingNode as ts.FunctionLikeDeclaration).body) { - const body = (typeHavingNode as ts.FunctionLikeDeclaration).body!; - if (body.kind === ts.SyntaxKind.Block) { - ts.forEachReturnStatement(body as ts.Block, returnStatement => { - if (returnStatement.expression) - addIfImplementation(returnStatement.expression); - }); - } - else { - addIfImplementation(body); - } + else if (ts.isFunctionLike(typeHavingNode) && (typeHavingNode as ts.FunctionLikeDeclaration).body) { + const body = (typeHavingNode as ts.FunctionLikeDeclaration).body!; + if (body.kind === ts.SyntaxKind.Block) { + ts.forEachReturnStatement(body as ts.Block, returnStatement => { + if (returnStatement.expression) + addIfImplementation(returnStatement.expression); + }); } - else if (ts.isAssertionExpression(typeHavingNode)) { - addIfImplementation(typeHavingNode.expression); + else { + addIfImplementation(body); } } - - function addIfImplementation(e: ts.Expression): void { - if (isImplementationExpression(e)) - addReference(e); + else if (ts.isAssertionExpression(typeHavingNode)) { + addIfImplementation(typeHavingNode.expression); } } - function getContainingClassIfInHeritageClause(node: ts.Node): ts.ClassLikeDeclaration | ts.InterfaceDeclaration | undefined { - return ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) ? getContainingClassIfInHeritageClause(node.parent) - : ts.isExpressionWithTypeArguments(node) ? ts.tryCast(node.parent.parent, ts.isClassLike) : undefined; + function addIfImplementation(e: ts.Expression): void { + if (isImplementationExpression(e)) + addReference(e); } + } - /** - * Returns true if this is an expression that can be considered an implementation - */ - function isImplementationExpression(node: ts.Expression): boolean { - switch (node.kind) { - case ts.SyntaxKind.ParenthesizedExpression: - return isImplementationExpression((node as ts.ParenthesizedExpression).expression); - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ArrayLiteralExpression: - return true; - default: - return false; - } - } + function getContainingClassIfInHeritageClause(node: ts.Node): ts.ClassLikeDeclaration | ts.InterfaceDeclaration | undefined { + return ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) ? getContainingClassIfInHeritageClause(node.parent) + : ts.isExpressionWithTypeArguments(node) ? ts.tryCast(node.parent.parent, ts.isClassLike) : undefined; + } - /** - * 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: ts.Symbol, parent: ts.Symbol, cachedResults: ts.ESMap, checker: ts.TypeChecker): boolean { - if (symbol === parent) { + /** + * Returns true if this is an expression that can be considered an implementation + */ + function isImplementationExpression(node: ts.Expression): boolean { + switch (node.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + return isImplementationExpression((node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ArrayLiteralExpression: return true; - } + default: + return false; + } + } - const key = ts.getSymbolId(symbol) + "," + ts.getSymbolId(parent); - const cached = cachedResults.get(key); - if (cached !== undefined) { - return cached; - } + /** + * 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: ts.Symbol, parent: ts.Symbol, cachedResults: ts.ESMap, checker: ts.TypeChecker): boolean { + if (symbol === parent) { + return true; + } - // Set the key so that we don't infinitely recurse - cachedResults.set(key, false); + const key = ts.getSymbolId(symbol) + "," + ts.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 => ts.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; + } - const inherits = !!symbol.declarations && symbol.declarations.some(declaration => ts.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: ts.Node): SymbolAndEntries[] | undefined { + let searchSpaceNode = ts.getSuperContainer(superKeyword, /*stopOnFunctions*/ false); + if (!searchSpaceNode) { + return undefined; } + // Whether 'super' occurs in a static context within a class. + let staticFlag = ts.ModifierFlags.Static; - function getReferencesForSuperKeyword(superKeyword: ts.Node): SymbolAndEntries[] | undefined { - let searchSpaceNode = ts.getSuperContainer(superKeyword, /*stopOnFunctions*/ false); - if (!searchSpaceNode) { + switch (searchSpaceNode.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + staticFlag &= ts.getSyntacticModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + default: return undefined; - } - // Whether 'super' occurs in a static context within a class. - let staticFlag = ts.ModifierFlags.Static; - - switch (searchSpaceNode.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - staticFlag &= ts.getSyntacticModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class - break; - default: - return undefined; - } + } - const sourceFile = searchSpaceNode.getSourceFile(); - const references = ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "super", searchSpaceNode), node => { - if (node.kind !== ts.SyntaxKind.SuperKeyword) { - return; - } + const sourceFile = searchSpaceNode.getSourceFile(); + const references = ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "super", searchSpaceNode), node => { + if (node.kind !== ts.SyntaxKind.SuperKeyword) { + return; + } - const container = ts.getSuperContainer(node, /*stopOnFunctions*/ false); + const container = ts.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 && ts.isStatic(container) === !!staticFlag && container.parent.symbol === searchSpaceNode.symbol ? nodeEntry(node) : undefined; - }); + // 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 && ts.isStatic(container) === !!staticFlag && container.parent.symbol === searchSpaceNode.symbol ? nodeEntry(node) : undefined; + }); - return [{ definition: { type: DefinitionKind.Symbol, symbol: searchSpaceNode.symbol }, references }]; - } + return [{ definition: { type: DefinitionKind.Symbol, symbol: searchSpaceNode.symbol }, references }]; + } - function isParameterName(node: ts.Node) { - return node.kind === ts.SyntaxKind.Identifier && node.parent.kind === ts.SyntaxKind.Parameter && (node.parent as ts.ParameterDeclaration).name === node; - } + function isParameterName(node: ts.Node) { + return node.kind === ts.SyntaxKind.Identifier && node.parent.kind === ts.SyntaxKind.Parameter && (node.parent as ts.ParameterDeclaration).name === node; + } - function getReferencesForThisKeyword(thisOrSuperKeyword: ts.Node, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { - let searchSpaceNode = ts.getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); + function getReferencesForThisKeyword(thisOrSuperKeyword: ts.Node, sourceFiles: readonly ts.SourceFile[], cancellationToken: ts.CancellationToken): SymbolAndEntries[] | undefined { + let searchSpaceNode = ts.getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); - // Whether 'this' occurs in a static context within a class. - let staticFlag = ts.ModifierFlags.Static; + // Whether 'this' occurs in a static context within a class. + let staticFlag = ts.ModifierFlags.Static; - switch (searchSpaceNode.kind) { - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - if (ts.isObjectLiteralMethod(searchSpaceNode)) { - staticFlag &= ts.getSyntacticModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning object literals - break; - } - // falls through - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: + switch (searchSpaceNode.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + if (ts.isObjectLiteralMethod(searchSpaceNode)) { staticFlag &= ts.getSyntacticModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class - break; - case ts.SyntaxKind.SourceFile: - if (ts.isExternalModule(searchSpaceNode as ts.SourceFile) || isParameterName(thisOrSuperKeyword)) { - return undefined; - } - // falls through - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning object literals 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: + } + // falls through + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + staticFlag &= ts.getSyntacticModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + case ts.SyntaxKind.SourceFile: + if (ts.isExternalModule(searchSpaceNode as ts.SourceFile) || isParameterName(thisOrSuperKeyword)) { return undefined; - } + } + // falls through + case ts.SyntaxKind.FunctionDeclaration: + case ts.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 = ts.flatMap(searchSpaceNode.kind === ts.SyntaxKind.SourceFile ? sourceFiles : [searchSpaceNode.getSourceFile()], sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return getPossibleSymbolReferenceNodes(sourceFile, "this", ts.isSourceFile(searchSpaceNode) ? sourceFile : searchSpaceNode).filter(node => { - if (!ts.isThis(node)) { - return false; - } - const container = ts.getThisContainer(node, /* includeArrowFunctions */ false); - switch (searchSpaceNode.kind) { - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - return searchSpaceNode.symbol === container.symbol; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return ts.isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol; - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ObjectLiteralExpression: - // Make sure the container belongs to the same class/object literals - // and has the appropriate static modifier from the original container. - return container.parent && searchSpaceNode.symbol === container.parent.symbol && ts.isStatic(container) === !!staticFlag; - case ts.SyntaxKind.SourceFile: - return container.kind === ts.SyntaxKind.SourceFile && !ts.isExternalModule(container as ts.SourceFile) && !isParameterName(node); - } - }); - }).map(n => nodeEntry(n)); + const references = ts.flatMap(searchSpaceNode.kind === ts.SyntaxKind.SourceFile ? sourceFiles : [searchSpaceNode.getSourceFile()], sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return getPossibleSymbolReferenceNodes(sourceFile, "this", ts.isSourceFile(searchSpaceNode) ? sourceFile : searchSpaceNode).filter(node => { + if (!ts.isThis(node)) { + return false; + } + const container = ts.getThisContainer(node, /* includeArrowFunctions */ false); + switch (searchSpaceNode.kind) { + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + return searchSpaceNode.symbol === container.symbol; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return ts.isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol; + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ObjectLiteralExpression: + // Make sure the container belongs to the same class/object literals + // and has the appropriate static modifier from the original container. + return container.parent && searchSpaceNode.symbol === container.parent.symbol && ts.isStatic(container) === !!staticFlag; + case ts.SyntaxKind.SourceFile: + return container.kind === ts.SyntaxKind.SourceFile && !ts.isExternalModule(container as ts.SourceFile) && !isParameterName(node); + } + }); + }).map(n => nodeEntry(n)); - const thisParameter = ts.firstDefined(references, r => ts.isParameter(r.node.parent) ? r.node : undefined); - return [{ - definition: { type: DefinitionKind.This, node: thisParameter || thisOrSuperKeyword }, - references - }]; - } + const thisParameter = ts.firstDefined(references, r => ts.isParameter(r.node.parent) ? r.node : undefined); + return [{ + definition: { type: DefinitionKind.This, node: thisParameter || thisOrSuperKeyword }, + references + }]; + } - function getReferencesForStringLiteral(node: ts.StringLiteralLike, sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): SymbolAndEntries[] { - const type = ts.getContextualTypeFromParentOrAncestorTypeNode(node, checker); - const references = ts.flatMap(sourceFiles, sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => { - if (ts.isStringLiteralLike(ref) && ref.text === node.text) { - if (type) { - const refType = ts.getContextualTypeFromParentOrAncestorTypeNode(ref, checker); - if (type !== checker.getStringType() && type === refType) { - return nodeEntry(ref, EntryKind.StringLiteral); - } - } - else { - return ts.isNoSubstitutionTemplateLiteral(ref) && !ts.rangeIsOnSingleLine(ref, sourceFile) ? undefined : - nodeEntry(ref, EntryKind.StringLiteral); + function getReferencesForStringLiteral(node: ts.StringLiteralLike, sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): SymbolAndEntries[] { + const type = ts.getContextualTypeFromParentOrAncestorTypeNode(node, checker); + const references = ts.flatMap(sourceFiles, sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return ts.mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => { + if (ts.isStringLiteralLike(ref) && ref.text === node.text) { + if (type) { + const refType = ts.getContextualTypeFromParentOrAncestorTypeNode(ref, checker); + if (type !== checker.getStringType() && type === refType) { + return nodeEntry(ref, EntryKind.StringLiteral); } } - }); + else { + return ts.isNoSubstitutionTemplateLiteral(ref) && !ts.rangeIsOnSingleLine(ref, sourceFile) ? undefined : + nodeEntry(ref, EntryKind.StringLiteral); + } + } }); + }); - return [{ - definition: { type: DefinitionKind.String, node }, - references - }]; - } + 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: ts.Symbol, location: ts.Node, checker: ts.TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): ts.Symbol[] { - const result: ts.Symbol[] = []; - forEachRelatedSymbol(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText), (sym, root, base) => { - // static method/property and instance method/property might have the same name. Only include static or only include instance. - if (base) { - if (isStaticSymbol(symbol) !== isStaticSymbol(base)) { - base = 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: ts.Symbol, location: ts.Node, checker: ts.TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): ts.Symbol[] { + const result: ts.Symbol[] = []; + forEachRelatedSymbol(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText), (sym, root, base) => { + // static method/property and instance method/property might have the same name. Only include static or only include instance. + if (base) { + if (isStaticSymbol(symbol) !== isStaticSymbol(base)) { + base = undefined; } - result.push(base || root || sym); - }, - // when try to find implementation, implementations is true, and not allowed to find base class - /*allowBaseTypes*/() => !implementations); - return result; - } + } + result.push(base || root || sym); + }, + // when try to find implementation, implementations is true, and not allowed to find base class + /*allowBaseTypes*/() => !implementations); + return result; + } + /** + * @param allowBaseTypes return true means it would try to find in base class or interface. + */ + function forEachRelatedSymbol(symbol: ts.Symbol, location: ts.Node, checker: ts.TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean, /** - * @param allowBaseTypes return true means it would try to find in base class or interface. + * @param baseSymbol This symbol means one property/mehtod from base class or interface when it is not null or undefined, */ - function forEachRelatedSymbol(symbol: ts.Symbol, location: ts.Node, checker: ts.TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean, - /** - * @param baseSymbol This symbol means one property/mehtod from base class or interface when it is not null or undefined, - */ - cbSymbol: (symbol: ts.Symbol, rootSymbol?: ts.Symbol, baseSymbol?: ts.Symbol, kind?: NodeEntryKind) => T | undefined, allowBaseTypes: (rootSymbol: ts.Symbol) => boolean): T | undefined { - const containingObjectLiteralElement = ts.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 && ts.firstDefined(ts.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; + cbSymbol: (symbol: ts.Symbol, rootSymbol?: ts.Symbol, baseSymbol?: ts.Symbol, kind?: NodeEntryKind) => T | undefined, allowBaseTypes: (rootSymbol: ts.Symbol) => boolean): T | undefined { + const containingObjectLiteralElement = ts.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); } - const res = fromRoot(symbol); + // 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 && ts.firstDefined(ts.getPropertySymbolsFromContextualType(containingObjectLiteralElement, checker, contextualType, /*unionSymbolOk*/ true), sym => fromRoot(sym, EntryKind.SearchedPropertyFoundLocal)); if (res) return res; - if (symbol.valueDeclaration && ts.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(ts.cast(symbol.valueDeclaration, ts.isParameter), symbol.name); - ts.Debug.assert(paramProps.length === 2 && !!(paramProps[0].flags & ts.SymbolFlags.FunctionScopedVariable) && !!(paramProps[1].flags & ts.SymbolFlags.Property)); // is [parameter, property] - return fromRoot(symbol.flags & ts.SymbolFlags.FunctionScopedVariable ? paramProps[1] : paramProps[0]); - } - - const exportSpecifier = ts.getDeclarationOfKind(symbol, ts.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: ts.Symbol | undefined; - if (onlyIncludeBindingElementAtReferenceLocation) { - bindingElementPropertySymbol = ts.isObjectBindingElementWithoutPropertyName(location.parent) ? ts.getPropertySymbolFromBindingElement(checker, location.parent) : undefined; - } - else { - bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); - } - return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); - } + // 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; - ts.Debug.assert(isForRenamePopulateSearchSymbolSet); - // due to the above assert and the arguments at the uses of this function, - // (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds - const includeOriginalSymbolOfBindingElement = onlyIncludeBindingElementAtReferenceLocation; + const res2 = shorthandValueSymbol && cbSymbol(shorthandValueSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedLocalFoundProperty); + if (res2) + return res2; + } - if (includeOriginalSymbolOfBindingElement) { - const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); - return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); - } + 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; + } - function fromRoot(sym: ts.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 ts.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 & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface) && allowBaseTypes(rootSymbol) - ? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind)) - : undefined)); - } + const res = fromRoot(symbol); + if (res) + return res; + if (symbol.valueDeclaration && ts.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(ts.cast(symbol.valueDeclaration, ts.isParameter), symbol.name); + ts.Debug.assert(paramProps.length === 2 && !!(paramProps[0].flags & ts.SymbolFlags.FunctionScopedVariable) && !!(paramProps[1].flags & ts.SymbolFlags.Property)); // is [parameter, property] + return fromRoot(symbol.flags & ts.SymbolFlags.FunctionScopedVariable ? paramProps[1] : paramProps[0]); + } - function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol | undefined { - const bindingElement = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.BindingElement); - if (bindingElement && ts.isObjectBindingElementWithoutPropertyName(bindingElement)) { - return ts.getPropertySymbolFromBindingElement(checker, bindingElement); - } + const exportSpecifier = ts.getDeclarationOfKind(symbol, ts.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; } } - /** - * 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. - */ - function getPropertySymbolsFromBaseTypes(symbol: ts.Symbol, propertyName: string, checker: ts.TypeChecker, cb: (symbol: ts.Symbol) => T | undefined): T | undefined { - const seen = new ts.Map(); - return recur(symbol); - - function recur(symbol: ts.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 & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) || !ts.addToSeen(seen, ts.getSymbolId(symbol))) - return; - return ts.firstDefined(symbol.declarations, declaration => ts.firstDefined(ts.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 && (ts.firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol)); - })); + // 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: ts.Symbol | undefined; + if (onlyIncludeBindingElementAtReferenceLocation) { + bindingElementPropertySymbol = ts.isObjectBindingElementWithoutPropertyName(location.parent) ? ts.getPropertySymbolFromBindingElement(checker, location.parent) : undefined; } + else { + bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); + } + return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); } - interface RelatedSymbol { - readonly symbol: ts.Symbol; - readonly kind: NodeEntryKind | undefined; + ts.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 isStaticSymbol(symbol: ts.Symbol): boolean { - if (!symbol.valueDeclaration) - return false; - const modifierFlags = ts.getEffectiveModifierFlags(symbol.valueDeclaration); - return !!(modifierFlags & ts.ModifierFlags.Static); - } - - function getRelatedSymbol(search: Search, referenceSymbol: ts.Symbol, referenceLocation: ts.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 => { - // check whether the symbol used to search itself is just the searched one. - if (baseSymbol) { - // static method/property and instance method/property might have the same name. Only check static or only check instance. - if (isStaticSymbol(referenceSymbol) !== isStaticSymbol(baseSymbol)) { - baseSymbol = undefined; - } - } - return 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 && !(ts.getCheckFlags(sym) & ts.CheckFlags.Synthetic) ? rootSymbol : sym, kind } - : undefined; - }, - /*allowBaseTypes*/ rootSymbol => !(search.parents && !search.parents.some(parent => explicitlyInheritsFrom(rootSymbol.parent!, parent, state.inheritsFromCache, checker)))); + function fromRoot(sym: ts.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 ts.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 & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface) && allowBaseTypes(rootSymbol) + ? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind)) + : undefined)); } - /** - * 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: ts.Node, symbol: ts.Symbol): ts.SemanticMeaning { - let meaning = ts.getMeaningFromLocation(node); - const { declarations } = symbol; - if (declarations) { - let lastIterationMeaning: ts.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 = ts.getMeaningFromDeclaration(declaration); - - if (declarationMeaning & meaning) { - meaning |= declarationMeaning; - } - } - } while (meaning !== lastIterationMeaning); + function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol | undefined { + const bindingElement = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.BindingElement); + if (bindingElement && ts.isObjectBindingElementWithoutPropertyName(bindingElement)) { + return ts.getPropertySymbolFromBindingElement(checker, bindingElement); } - return meaning; } + } - function isImplementation(node: ts.Node): boolean { - return !!(node.flags & ts.NodeFlags.Ambient) ? !(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) : - (ts.isVariableLike(node) ? ts.hasInitializer(node) : - ts.isFunctionLikeDeclaration(node) ? !!node.body : - ts.isClassLike(node) || ts.isModuleOrEnumDeclaration(node)); + /** + * 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. + */ + function getPropertySymbolsFromBaseTypes(symbol: ts.Symbol, propertyName: string, checker: ts.TypeChecker, cb: (symbol: ts.Symbol) => T | undefined): T | undefined { + const seen = new ts.Map(); + return recur(symbol); + + function recur(symbol: ts.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 & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) || !ts.addToSeen(seen, ts.getSymbolId(symbol))) + return; + return ts.firstDefined(symbol.declarations, declaration => ts.firstDefined(ts.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 && (ts.firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol)); + })); } + } + + interface RelatedSymbol { + readonly symbol: ts.Symbol; + readonly kind: NodeEntryKind | undefined; + } - export function getReferenceEntriesForShorthandPropertyAssignment(node: ts.Node, checker: ts.TypeChecker, addReference: (node: ts.Node) => void): void { - const refSymbol = checker.getSymbolAtLocation(node)!; - const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(refSymbol.valueDeclaration); + function isStaticSymbol(symbol: ts.Symbol): boolean { + if (!symbol.valueDeclaration) + return false; + const modifierFlags = ts.getEffectiveModifierFlags(symbol.valueDeclaration); + return !!(modifierFlags & ts.ModifierFlags.Static); + } - if (shorthandSymbol) { - for (const declaration of shorthandSymbol.getDeclarations()!) { - if (ts.getMeaningFromDeclaration(declaration) & ts.SemanticMeaning.Value) { - addReference(declaration); + function getRelatedSymbol(search: Search, referenceSymbol: ts.Symbol, referenceLocation: ts.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 => { + // check whether the symbol used to search itself is just the searched one. + if (baseSymbol) { + // static method/property and instance method/property might have the same name. Only check static or only check instance. + if (isStaticSymbol(referenceSymbol) !== isStaticSymbol(baseSymbol)) { + baseSymbol = undefined; } } - } - } + return 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 && !(ts.getCheckFlags(sym) & ts.CheckFlags.Synthetic) ? rootSymbol : sym, kind } + : undefined; + }, + /*allowBaseTypes*/ rootSymbol => !(search.parents && !search.parents.some(parent => explicitlyInheritsFrom(rootSymbol.parent!, parent, state.inheritsFromCache, checker)))); + } - function forEachDescendantOfKind(node: ts.Node, kind: ts.SyntaxKind, action: (node: ts.Node) => void): void { - ts.forEachChild(node, child => { - if (child.kind === kind) { - action(child); + /** + * 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: ts.Node, symbol: ts.Symbol): ts.SemanticMeaning { + let meaning = ts.getMeaningFromLocation(node); + const { declarations } = symbol; + if (declarations) { + let lastIterationMeaning: ts.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 = ts.getMeaningFromDeclaration(declaration); + + if (declarationMeaning & meaning) { + meaning |= declarationMeaning; + } } - forEachDescendantOfKind(child, kind, action); - }); + } while (meaning !== lastIterationMeaning); } + return meaning; + } - /** 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: ts.Node): ts.ClassLikeDeclaration | undefined { - return ts.tryGetClassExtendingExpressionWithTypeArguments(ts.climbPastPropertyAccess(node).parent); - } + function isImplementation(node: ts.Node): boolean { + return !!(node.flags & ts.NodeFlags.Ambient) ? !(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) : + (ts.isVariableLike(node) ? ts.hasInitializer(node) : + ts.isFunctionLikeDeclaration(node) ? !!node.body : + ts.isClassLike(node) || ts.isModuleOrEnumDeclaration(node)); + } - /** - * 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: ts.Node, symbol: ts.Symbol, checker: ts.TypeChecker): readonly ts.Symbol[] | undefined { - const propertyAccessExpression = ts.isRightSideOfPropertyAccess(location) ? location.parent as ts.PropertyAccessExpression : undefined; - const lhsType = propertyAccessExpression && checker.getTypeAtLocation(propertyAccessExpression.expression); - const res = ts.mapDefined(lhsType && (lhsType.isUnionOrIntersection() ? lhsType.types : lhsType.symbol === symbol.parent ? undefined : [lhsType]), t => t.symbol && t.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface) ? t.symbol : undefined); - return res.length === 0 ? undefined : res; - } + export function getReferenceEntriesForShorthandPropertyAssignment(node: ts.Node, checker: ts.TypeChecker, addReference: (node: ts.Node) => void): void { + const refSymbol = checker.getSymbolAtLocation(node)!; + const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(refSymbol.valueDeclaration); - function isForRenameWithPrefixAndSuffixText(options: Options) { - return options.use === FindReferencesUse.Rename && options.providePrefixAndSuffixTextForRename; + if (shorthandSymbol) { + for (const declaration of shorthandSymbol.getDeclarations()!) { + if (ts.getMeaningFromDeclaration(declaration) & ts.SemanticMeaning.Value) { + addReference(declaration); + } + } } } + + function forEachDescendantOfKind(node: ts.Node, kind: ts.SyntaxKind, action: (node: ts.Node) => void): void { + ts.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: ts.Node): ts.ClassLikeDeclaration | undefined { + return ts.tryGetClassExtendingExpressionWithTypeArguments(ts.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: ts.Node, symbol: ts.Symbol, checker: ts.TypeChecker): readonly ts.Symbol[] | undefined { + const propertyAccessExpression = ts.isRightSideOfPropertyAccess(location) ? location.parent as ts.PropertyAccessExpression : undefined; + const lhsType = propertyAccessExpression && checker.getTypeAtLocation(propertyAccessExpression.expression); + const res = ts.mapDefined(lhsType && (lhsType.isUnionOrIntersection() ? lhsType.types : lhsType.symbol === symbol.parent ? undefined : [lhsType]), t => t.symbol && t.symbol.flags & (ts.SymbolFlags.Class | ts.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 2f0f8ed401e91..aa2a28279b277 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -1,1316 +1,1316 @@ /* @internal */ namespace ts.formatting { - export interface FormatContext { - readonly options: ts.FormatCodeSettings; - readonly getRules: ts.formatting.RulesMap; - readonly host: ts.FormattingHost; - } +export interface FormatContext { + readonly options: ts.FormatCodeSettings; + readonly getRules: ts.formatting.RulesMap; + readonly host: ts.FormattingHost; +} - export interface TextRangeWithKind extends ts.TextRange { - kind: T; - } +export interface TextRangeWithKind extends ts.TextRange { + kind: T; +} - export type TextRangeWithTriviaKind = TextRangeWithKind; +export type TextRangeWithTriviaKind = TextRangeWithKind; - export interface TokenInfo { - leadingTrivia: TextRangeWithTriviaKind[] | undefined; - token: TextRangeWithKind; - trailingTrivia: TextRangeWithTriviaKind[] | undefined; - } +export interface TokenInfo { + leadingTrivia: TextRangeWithTriviaKind[] | undefined; + token: TextRangeWithKind; + trailingTrivia: TextRangeWithTriviaKind[] | undefined; +} - export function createTextRangeWithKind(pos: number, end: number, kind: T): TextRangeWithKind { - const textRangeWithKind: TextRangeWithKind = { pos, end, kind }; - if (ts.Debug.isDebugging) { - Object.defineProperty(textRangeWithKind, "__debugKind", { - get: () => ts.Debug.formatSyntaxKind(kind), - }); - } - return textRangeWithKind; +export function createTextRangeWithKind(pos: number, end: number, kind: T): TextRangeWithKind { + const textRangeWithKind: TextRangeWithKind = { pos, end, kind }; + if (ts.Debug.isDebugging) { + Object.defineProperty(textRangeWithKind, "__debugKind", { + get: () => ts.Debug.formatSyntaxKind(kind), + }); } + return textRangeWithKind; +} - const enum Constants { - Unknown = -1 - } +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 +/* + * 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 + */ +interface DynamicIndentation { + getIndentationForToken(tokenLine: number, tokenKind: ts.SyntaxKind, container: ts.Node, suppressDelta: boolean): number; + getIndentationForComment(owningToken: ts.SyntaxKind, tokenIndentation: number, container: ts.Node): number; + /** + * Indentation for open and close tokens of the node if it is block or another node that needs special indentation + * ... { + * ......... + * ....} + * ____ - indentation + * ____ - delta */ - interface DynamicIndentation { - getIndentationForToken(tokenLine: number, tokenKind: ts.SyntaxKind, container: ts.Node, suppressDelta: boolean): number; - getIndentationForComment(owningToken: ts.SyntaxKind, tokenIndentation: number, container: ts.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, parent: ts.Node): void; - } - - export function formatOnEnter(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.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 = ts.getEndLinePosition(line, sourceFile); - while (ts.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 (ts.isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) { - endOfFormatSpan--; - } - const span = { - // get start position for the previous line - pos: ts.getStartPositionOfLine(line - 1, sourceFile), - // end value is exclusive so add 1 to the result - end: endOfFormatSpan + 1 - }; - return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnEnter); - } - - export function formatOnSemicolon(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { - const semicolon = findImmediatelyPrecedingTokenOfKind(position, ts.SyntaxKind.SemicolonToken, sourceFile); - return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnSemicolon); - } - - export function formatOnOpeningCurly(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { - const openingCurly = findImmediatelyPrecedingTokenOfKind(position, ts.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: ts.TextRange = { - pos: ts.getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), - end: position - }; - - return formatSpan(textRange, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnOpeningCurlyBrace); - } + 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, parent: ts.Node): void; +} - export function formatOnClosingCurly(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { - const precedingToken = findImmediatelyPrecedingTokenOfKind(position, ts.SyntaxKind.CloseBraceToken, sourceFile); - return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnClosingCurlyBrace); +export function formatOnEnter(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + const line = sourceFile.getLineAndCharacterOfPosition(position).line; + if (line === 0) { + return []; } - - export function formatDocument(sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { - const span = { - pos: 0, - end: sourceFile.text.length - }; - return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatDocument); + // 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 = ts.getEndLinePosition(line, sourceFile); + while (ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(endOfFormatSpan))) { + endOfFormatSpan--; } - - export function formatSelection(start: number, end: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { - // format from the beginning of the line - const span = { - pos: ts.getLineStartPositionForPosition(start, sourceFile), - end, - }; - return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatSelection); + // 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 (ts.isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) { + endOfFormatSpan--; } + const span = { + // get start position for the previous line + pos: ts.getStartPositionOfLine(line - 1, sourceFile), + // end value is exclusive so add 1 to the result + end: endOfFormatSpan + 1 + }; + return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnEnter); +} - /** - * 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: ts.SyntaxKind, sourceFile: ts.SourceFile): ts.Node | undefined { - const precedingToken = ts.findPrecedingToken(end, sourceFile); +export function formatOnSemicolon(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + const semicolon = findImmediatelyPrecedingTokenOfKind(position, ts.SyntaxKind.SemicolonToken, sourceFile); + return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnSemicolon); +} - return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ? - precedingToken : - undefined; +export function formatOnOpeningCurly(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + const openingCurly = findImmediatelyPrecedingTokenOfKind(position, ts.SyntaxKind.OpenBraceToken, sourceFile); + if (!openingCurly) { + return []; } + const curlyBraceRange = openingCurly.parent; + const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange); /** - * 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 + * 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: * ``` - * let x = 1; - * while (true) { + * class C { + * foo() * } * ``` - * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding - * variable declaration. + * and we wouldn't want to move the closing brace. */ - function findOutermostNodeWithinListLevel(node: ts.Node | undefined) { - let current = node; - while (current && - current.parent && - current.parent.end === node!.end && - !isListElement(current.parent, current)) { - current = current.parent; - } - - 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: ts.Node, node: ts.Node): boolean { - switch (parent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - return ts.rangeContainsRange((parent as ts.InterfaceDeclaration).members, node); - case ts.SyntaxKind.ModuleDeclaration: - const body = (parent as ts.ModuleDeclaration).body; - return !!body && body.kind === ts.SyntaxKind.ModuleBlock && ts.rangeContainsRange(body.statements, node); - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - return ts.rangeContainsRange((parent as ts.Block).statements, node); - case ts.SyntaxKind.CatchClause: - return ts.rangeContainsRange((parent as ts.CatchClause).block.statements, node); - } + const textRange: ts.TextRange = { + pos: ts.getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), + end: position + }; - return false; - } + return formatSpan(textRange, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnOpeningCurlyBrace); +} - /** find node that fully contains given text range */ - function findEnclosingNode(range: ts.TextRange, sourceFile: ts.SourceFile): ts.Node { - return find(sourceFile); +export function formatOnClosingCurly(position: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + const precedingToken = findImmediatelyPrecedingTokenOfKind(position, ts.SyntaxKind.CloseBraceToken, sourceFile); + return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatOnClosingCurlyBrace); +} - function find(n: ts.Node): ts.Node { - const candidate = ts.forEachChild(n, c => ts.startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); - if (candidate) { - const result = find(candidate); - if (result) { - return result; - } - } +export function formatDocument(sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + const span = { + pos: 0, + end: sourceFile.text.length + }; + return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatDocument); +} - return n; - } - } +export function formatSelection(start: number, end: number, sourceFile: ts.SourceFile, formatContext: FormatContext): ts.TextChange[] { + // format from the beginning of the line + const span = { + pos: ts.getLineStartPositionForPosition(start, sourceFile), + end, + }; + return formatSpan(span, sourceFile, formatContext, ts.formatting.FormattingRequestKind.FormatSelection); +} - /** 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 ts.Diagnostic[], originalRange: ts.TextRange): (r: ts.TextRange) => boolean { - if (!errors.length) { - return rangeHasNoErrors; - } +/** + * 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: ts.SyntaxKind, sourceFile: ts.SourceFile): ts.Node | undefined { + const precedingToken = ts.findPrecedingToken(end, sourceFile); - // pick only errors that fall in range - const sorted = errors - .filter(d => ts.rangeOverlapsWithStartEnd(originalRange, d.start!, d.start! + d.length!)) // TODO: GH#18217 - .sort((e1, e2) => e1.start! - e2.start!); + return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ? + precedingToken : + undefined; +} - if (!sorted.length) { - return rangeHasNoErrors; - } +/** + * 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: ts.Node | undefined) { + let current = node; + while (current && + current.parent && + current.parent.end === node!.end && + !isListElement(current.parent, current)) { + current = current.parent; + } - let index = 0; + return current; +} - 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; - } +// 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: ts.Node, node: ts.Node): boolean { + switch (parent.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + return ts.rangeContainsRange((parent as ts.InterfaceDeclaration).members, node); + case ts.SyntaxKind.ModuleDeclaration: + const body = (parent as ts.ModuleDeclaration).body; + return !!body && body.kind === ts.SyntaxKind.ModuleBlock && ts.rangeContainsRange(body.statements, node); + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + return ts.rangeContainsRange((parent as ts.Block).statements, node); + case ts.SyntaxKind.CatchClause: + return ts.rangeContainsRange((parent as ts.CatchClause).block.statements, node); + } - const error = sorted[index]; - if (r.end <= error.start!) { - // specified range ends before the error referred by 'index' - no error in range - return false; - } + return false; +} - if (ts.startEndOverlapsWithStartEnd(r.pos, r.end, error.start!, error.start! + error.length!)) { - // specified range overlaps with error range - return true; - } +/** find node that fully contains given text range */ +function findEnclosingNode(range: ts.TextRange, sourceFile: ts.SourceFile): ts.Node { + return find(sourceFile); - index++; + function find(n: ts.Node): ts.Node { + const candidate = ts.forEachChild(n, c => ts.startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); + if (candidate) { + const result = find(candidate); + if (result) { + return result; } - }; - - function rangeHasNoErrors(): boolean { - return false; } - } - /** - * 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: ts.Node, originalRange: ts.TextRange, sourceFile: ts.SourceFile): number { - const start = enclosingNode.getStart(sourceFile); - if (start === originalRange.pos && enclosingNode.end === originalRange.end) { - return start; - } + return n; + } +} - const precedingToken = ts.findPrecedingToken(originalRange.pos, sourceFile); - if (!precedingToken) { - // no preceding token found - start from the beginning of enclosing node - return enclosingNode.pos; - } +/** 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 ts.Diagnostic[], originalRange: ts.TextRange): (r: ts.TextRange) => boolean { + if (!errors.length) { + return rangeHasNoErrors; + } - // 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; - } + // pick only errors that fall in range + const sorted = errors + .filter(d => ts.rangeOverlapsWithStartEnd(originalRange, d.start!, d.start! + d.length!)) // TODO: GH#18217 + .sort((e1, e2) => e1.start! - e2.start!); - return precedingToken.end; + if (!sorted.length) { + return rangeHasNoErrors; } - /* - * 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. - */ - function getOwnOrInheritedDelta(n: ts.Node, options: ts.FormatCodeSettings, sourceFile: ts.SourceFile): number { - let previousLine = Constants.Unknown; - let child: ts.Node | undefined; - while (n) { - const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line; - if (previousLine !== Constants.Unknown && line !== previousLine) { - break; + 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; } - if (ts.formatting.SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) { - return options.indentSize!; + const error = sorted[index]; + if (r.end <= error.start!) { + // specified range ends before the error referred by 'index' - no error in range + return false; } - previousLine = line; - child = n; - n = n.parent; + if (ts.startEndOverlapsWithStartEnd(r.pos, r.end, error.start!, error.start! + error.length!)) { + // specified range overlaps with error range + return true; + } + + index++; } - return 0; + }; + + function rangeHasNoErrors(): boolean { + return false; + } +} + +/** + * 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: ts.Node, originalRange: ts.TextRange, sourceFile: ts.SourceFile): number { + const start = enclosingNode.getStart(sourceFile); + if (start === originalRange.pos && enclosingNode.end === originalRange.end) { + return start; + } + + const precedingToken = ts.findPrecedingToken(originalRange.pos, sourceFile); + if (!precedingToken) { + // no preceding token found - start from the beginning of enclosing node + return enclosingNode.pos; } - export function formatNodeGivenIndentation(node: ts.Node, sourceFileLike: ts.SourceFileLike, languageVariant: ts.LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): ts.TextChange[] { - const range = { pos: node.pos, end: node.end }; - return ts.formatting.getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(range, node, initialIndentation, delta, scanner, formatContext, ts.formatting.FormattingRequestKind.FormatSelection, _ => false, // assume that node does not have any errors - sourceFileLike)); + // 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; } - function formatNodeLines(node: ts.Node | undefined, sourceFile: ts.SourceFile, formatContext: FormatContext, requestKind: ts.formatting.FormattingRequestKind): ts.TextChange[] { - if (!node) { - return []; + 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. + */ +function getOwnOrInheritedDelta(n: ts.Node, options: ts.FormatCodeSettings, sourceFile: ts.SourceFile): number { + let previousLine = Constants.Unknown; + let child: ts.Node | undefined; + while (n) { + const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line; + if (previousLine !== Constants.Unknown && line !== previousLine) { + break; } - const span = { - pos: ts.getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile), - end: node.end - }; + if (ts.formatting.SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) { + return options.indentSize!; + } - return formatSpan(span, sourceFile, formatContext, requestKind); + previousLine = line; + child = n; + n = n.parent; } + return 0; +} + +export function formatNodeGivenIndentation(node: ts.Node, sourceFileLike: ts.SourceFileLike, languageVariant: ts.LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): ts.TextChange[] { + const range = { pos: node.pos, end: node.end }; + return ts.formatting.getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(range, node, initialIndentation, delta, scanner, formatContext, ts.formatting.FormattingRequestKind.FormatSelection, _ => false, // assume that node does not have any errors + sourceFileLike)); +} - function formatSpan(originalRange: ts.TextRange, sourceFile: ts.SourceFile, formatContext: FormatContext, requestKind: ts.formatting.FormattingRequestKind): ts.TextChange[] { - // find the smallest node that fully wraps the range and compute the initial indentation for the node - const enclosingNode = findEnclosingNode(originalRange, sourceFile); - return ts.formatting.getFormattingScanner(sourceFile.text, sourceFile.languageVariant, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end, scanner => formatSpanWorker(originalRange, enclosingNode, ts.formatting.SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options), getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile), scanner, formatContext, requestKind, prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), sourceFile)); +function formatNodeLines(node: ts.Node | undefined, sourceFile: ts.SourceFile, formatContext: FormatContext, requestKind: ts.formatting.FormattingRequestKind): ts.TextChange[] { + if (!node) { + return []; } - function formatSpanWorker(originalRange: ts.TextRange, enclosingNode: ts.Node, initialIndentation: number, delta: number, formattingScanner: ts.formatting.FormattingScanner, { options, getRules, host }: FormatContext, requestKind: ts.formatting.FormattingRequestKind, rangeContainsError: (r: ts.TextRange) => boolean, sourceFile: ts.SourceFileLike): ts.TextChange[] { - // formatting context is used by rules provider - const formattingContext = new ts.formatting.FormattingContext(sourceFile, requestKind, options); - let previousRange: TextRangeWithKind; - let previousParent: ts.Node; - let previousRangeStartLine: number; + const span = { + pos: ts.getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile), + end: node.end + }; - let lastIndentedLine: number; - let indentationOnLastIndentedLine = Constants.Unknown; + return formatSpan(span, sourceFile, formatContext, requestKind); +} - const edits: ts.TextChange[] = []; +function formatSpan(originalRange: ts.TextRange, sourceFile: ts.SourceFile, formatContext: FormatContext, requestKind: ts.formatting.FormattingRequestKind): ts.TextChange[] { + // find the smallest node that fully wraps the range and compute the initial indentation for the node + const enclosingNode = findEnclosingNode(originalRange, sourceFile); + return ts.formatting.getFormattingScanner(sourceFile.text, sourceFile.languageVariant, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end, scanner => formatSpanWorker(originalRange, enclosingNode, ts.formatting.SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options), getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile), scanner, formatContext, requestKind, prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), sourceFile)); +} +function formatSpanWorker(originalRange: ts.TextRange, enclosingNode: ts.Node, initialIndentation: number, delta: number, formattingScanner: ts.formatting.FormattingScanner, { options, getRules, host }: FormatContext, requestKind: ts.formatting.FormattingRequestKind, rangeContainsError: (r: ts.TextRange) => boolean, sourceFile: ts.SourceFileLike): ts.TextChange[] { - formattingScanner.advance(); + // formatting context is used by rules provider + const formattingContext = new ts.formatting.FormattingContext(sourceFile, requestKind, options); + let previousRange: TextRangeWithKind; + let previousParent: ts.Node; + let previousRangeStartLine: number; - if (formattingScanner.isOnToken()) { - const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; - let undecoratedStartLine = startLine; - if (enclosingNode.decorators) { - undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(ts.getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; - } + let lastIndentedLine: number; + let indentationOnLastIndentedLine = Constants.Unknown; + + const edits: ts.TextChange[] = []; + + formattingScanner.advance(); - processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); + if (formattingScanner.isOnToken()) { + const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; + let undecoratedStartLine = startLine; + if (enclosingNode.decorators) { + undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(ts.getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; } - if (!formattingScanner.isOnToken()) { - const indentation = ts.formatting.SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false) - ? initialIndentation + options.indentSize! - : initialIndentation; - const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); - if (leadingTrivia) { - indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false, item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); - if (options.trimTrailingWhitespace !== false) { - trimTrailingWhitespacesForRemainingRange(leadingTrivia); - } + processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); + } + + if (!formattingScanner.isOnToken()) { + const indentation = ts.formatting.SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false) + ? initialIndentation + options.indentSize! + : initialIndentation; + const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); + if (leadingTrivia) { + indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false, item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); + if (options.trimTrailingWhitespace !== false) { + trimTrailingWhitespacesForRemainingRange(leadingTrivia); } } + } - if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) { - const tokenInfo = formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() : - formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token : - undefined; + if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) { + const tokenInfo = formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() : + formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token : + undefined; - if (tokenInfo) { - const parent = ts.findPrecedingToken(tokenInfo.end, sourceFile, enclosingNode)?.parent || previousParent!; - processPair(tokenInfo, sourceFile.getLineAndCharacterOfPosition(tokenInfo.pos).line, parent, previousRange, previousRangeStartLine!, previousParent!, parent, - /*dynamicIndentation*/ undefined); - } + if (tokenInfo) { + const parent = ts.findPrecedingToken(tokenInfo.end, sourceFile, enclosingNode)?.parent || previousParent!; + processPair(tokenInfo, sourceFile.getLineAndCharacterOfPosition(tokenInfo.pos).line, parent, previousRange, previousRangeStartLine!, previousParent!, parent, + /*dynamicIndentation*/ undefined); } + } - return edits; + return edits; - // local functions + // 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: ts.TextRange, inheritedIndentation: number): number { - if (ts.rangeOverlapsWithStartEnd(range, startPos, endPos) || - ts.rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { + /** 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: ts.TextRange, inheritedIndentation: number): number { + if (ts.rangeOverlapsWithStartEnd(range, startPos, endPos) || + ts.rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { - if (inheritedIndentation !== Constants.Unknown) { - return inheritedIndentation; - } + if (inheritedIndentation !== Constants.Unknown) { + return inheritedIndentation; } - else { - const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; - const startLinePosition = ts.getLineStartPositionForPosition(startPos, sourceFile); - const column = ts.formatting.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 = ts.formatting.SmartIndenter.getBaseIndentation(options); - return baseIndentSize > column ? baseIndentSize : column; - } + } + else { + const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; + const startLinePosition = ts.getLineStartPositionForPosition(startPos, sourceFile); + const column = ts.formatting.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 = ts.formatting.SmartIndenter.getBaseIndentation(options); + return baseIndentSize > column ? baseIndentSize : column; } - - return Constants.Unknown; } - function computeIndentation(node: TextRangeWithKind, startLine: number, inheritedIndentation: number, parent: ts.Node, parentDynamicIndentation: DynamicIndentation, effectiveParentStartLine: number): { - indentation: number; - delta: number; - } { - const delta = ts.formatting.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) - }; + return Constants.Unknown; + } + + function computeIndentation(node: TextRangeWithKind, startLine: number, inheritedIndentation: number, parent: ts.Node, parentDynamicIndentation: DynamicIndentation, effectiveParentStartLine: number): { + indentation: number; + delta: number; + } { + const delta = ts.formatting.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 === ts.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 if (inheritedIndentation === Constants.Unknown) { - if (node.kind === ts.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 if (ts.formatting.SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) || - ts.formatting.SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) || - ts.formatting.SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)) { - return { indentation: parentDynamicIndentation.getIndentation(), delta }; - } - else { - return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; - } + else if (ts.formatting.SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) || + ts.formatting.SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) || + ts.formatting.SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)) { + return { indentation: parentDynamicIndentation.getIndentation(), delta }; } else { - return { indentation: inheritedIndentation, delta }; + return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; } } + else { + return { indentation: inheritedIndentation, delta }; + } + } - function getFirstNonDecoratorTokenOfNode(node: ts.Node) { - if (node.modifiers && node.modifiers.length) { - return node.modifiers[0].kind; - } - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: return ts.SyntaxKind.ClassKeyword; - case ts.SyntaxKind.InterfaceDeclaration: return ts.SyntaxKind.InterfaceKeyword; - case ts.SyntaxKind.FunctionDeclaration: return ts.SyntaxKind.FunctionKeyword; - case ts.SyntaxKind.EnumDeclaration: return ts.SyntaxKind.EnumDeclaration; - case ts.SyntaxKind.GetAccessor: return ts.SyntaxKind.GetKeyword; - case ts.SyntaxKind.SetAccessor: return ts.SyntaxKind.SetKeyword; - case ts.SyntaxKind.MethodDeclaration: - if ((node as ts.MethodDeclaration).asteriskToken) { - return ts.SyntaxKind.AsteriskToken; - } - // falls through - - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.Parameter: - const name = ts.getNameOfDeclaration(node as ts.Declaration); - if (name) { - return name.kind; - } - } + function getFirstNonDecoratorTokenOfNode(node: ts.Node) { + if (node.modifiers && node.modifiers.length) { + return node.modifiers[0].kind; } + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: return ts.SyntaxKind.ClassKeyword; + case ts.SyntaxKind.InterfaceDeclaration: return ts.SyntaxKind.InterfaceKeyword; + case ts.SyntaxKind.FunctionDeclaration: return ts.SyntaxKind.FunctionKeyword; + case ts.SyntaxKind.EnumDeclaration: return ts.SyntaxKind.EnumDeclaration; + case ts.SyntaxKind.GetAccessor: return ts.SyntaxKind.GetKeyword; + case ts.SyntaxKind.SetAccessor: return ts.SyntaxKind.SetKeyword; + case ts.SyntaxKind.MethodDeclaration: + if ((node as ts.MethodDeclaration).asteriskToken) { + return ts.SyntaxKind.AsteriskToken; + } + // falls through - function getDynamicIndentation(node: ts.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 ts.SyntaxKind.CloseBraceToken: - case ts.SyntaxKind.CloseBracketToken: - case ts.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, parent) => { - if (ts.formatting.SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) { - indentation += lineAdded ? options.indentSize! : -options.indentSize!; - delta = ts.formatting.SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; - } + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.Parameter: + const name = ts.getNameOfDeclaration(node as ts.Declaration); + if (name) { + return name.kind; } - }; + } + } - function shouldAddDelta(line: number, kind: ts.SyntaxKind, container: ts.Node): boolean { + function getDynamicIndentation(node: ts.Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { + return { + getIndentationForComment: (kind, tokenIndentation, container) => { switch (kind) { - // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent - case ts.SyntaxKind.OpenBraceToken: + // preceding comment to the token that closes the indentation scope inherits the indentation from the scope + // .. { + // // comment + // } case ts.SyntaxKind.CloseBraceToken: - case ts.SyntaxKind.CloseParenToken: - case ts.SyntaxKind.ElseKeyword: - case ts.SyntaxKind.WhileKeyword: - case ts.SyntaxKind.AtToken: - return false; - case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.GreaterThanToken: - switch (container.kind) { - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxClosingElement: - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.ExpressionWithTypeArguments: - return false; - } - break; - case ts.SyntaxKind.OpenBracketToken: case ts.SyntaxKind.CloseBracketToken: - if (container.kind !== ts.SyntaxKind.MappedType) { - return false; - } - break; + case ts.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, parent) => { + if (ts.formatting.SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) { + indentation += lineAdded ? options.indentSize! : -options.indentSize!; + delta = ts.formatting.SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; } - // 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 ts.formatting.SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; + function shouldAddDelta(line: number, kind: ts.SyntaxKind, container: ts.Node): boolean { + switch (kind) { + // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent + case ts.SyntaxKind.OpenBraceToken: + case ts.SyntaxKind.CloseBraceToken: + case ts.SyntaxKind.CloseParenToken: + case ts.SyntaxKind.ElseKeyword: + case ts.SyntaxKind.WhileKeyword: + case ts.SyntaxKind.AtToken: + return false; + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.GreaterThanToken: + switch (container.kind) { + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxClosingElement: + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.ExpressionWithTypeArguments: + return false; + } + break; + case ts.SyntaxKind.OpenBracketToken: + case ts.SyntaxKind.CloseBracketToken: + if (container.kind !== ts.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 processNode(node: ts.Node, contextNode: ts.Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { - if (!ts.rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { - return; - } + function getDelta(child: TextRangeWithKind) { + // Delta value should be zero when the node explicitly prevents indentation of the child node + return ts.formatting.SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; + } + } - 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 - ts.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() && formattingScanner.getStartPos() < originalRange.end) { - const tokenInfo = formattingScanner.readTokenInfo(node); - if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) { - break; - } - consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); + function processNode(node: ts.Node, contextNode: ts.Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { + if (!ts.rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { + return; + } + + 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 + ts.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() && formattingScanner.getStartPos() < originalRange.end) { + const tokenInfo = formattingScanner.readTokenInfo(node); + if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) { + break; } + consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); + } - function processChildNode(child: ts.Node, inheritedIndentation: number, parent: ts.Node, parentDynamicIndentation: DynamicIndentation, parentStartLine: number, undecoratedParentStartLine: number, isListItem: boolean, isFirstListItem?: boolean): number { - if (ts.nodeIsMissing(child)) { - return inheritedIndentation; - } + function processChildNode(child: ts.Node, inheritedIndentation: number, parent: ts.Node, parentDynamicIndentation: DynamicIndentation, parentStartLine: number, undecoratedParentStartLine: number, isListItem: boolean, isFirstListItem?: boolean): number { + if (ts.nodeIsMissing(child)) { + return inheritedIndentation; + } - const childStartPos = child.getStart(sourceFile); + const childStartPos = child.getStart(sourceFile); - const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; + const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; - let undecoratedChildStartLine = childStartLine; - if (child.decorators) { - undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(ts.getNonDecoratorTokenPosOfNode(child, sourceFile)).line; - } + let undecoratedChildStartLine = childStartLine; + if (child.decorators) { + undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(ts.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 child is a list item - try to get its indentation, only if parent is within the original range. + let childIndentationAmount = Constants.Unknown; - if (isListItem && ts.rangeContainsRange(originalRange, parent)) { - childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); - if (childIndentationAmount !== Constants.Unknown) { - inheritedIndentation = childIndentationAmount; - } + if (isListItem && ts.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 (!ts.rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { - if (child.end < originalRange.pos) { - formattingScanner.skipToEndOf(child); - } - return inheritedIndentation; + // child node is outside the target range - do not dive inside + if (!ts.rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { + if (child.end < originalRange.pos) { + formattingScanner.skipToEndOf(child); } + return inheritedIndentation; + } - if (child.getFullWidth() === 0) { + if (child.getFullWidth() === 0) { + return inheritedIndentation; + } + + while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { + // proceed any parent tokens that are located prior to child.getStart() + const tokenInfo = formattingScanner.readTokenInfo(node); + if (tokenInfo.token.end > originalRange.end) { return inheritedIndentation; } - - while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { - // proceed any parent tokens that are located prior to child.getStart() - const tokenInfo = formattingScanner.readTokenInfo(node); - if (tokenInfo.token.end > originalRange.end) { - return inheritedIndentation; - } - if (tokenInfo.token.end > childStartPos) { - if (tokenInfo.token.pos > childStartPos) { - formattingScanner.skipToStartOf(child); - } - // stop when formatting scanner advances past the beginning of the child - break; + if (tokenInfo.token.end > childStartPos) { + if (tokenInfo.token.pos > childStartPos) { + formattingScanner.skipToStartOf(child); } - - consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); + // stop when formatting scanner advances past the beginning of the child + break; } - if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) { - return inheritedIndentation; - } + consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); + } - if (ts.isToken(child)) { - // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules - const tokenInfo = formattingScanner.readTokenInfo(child); - // JSX text shouldn't affect indenting - if (child.kind !== ts.SyntaxKind.JsxText) { - ts.Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); - consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); - return inheritedIndentation; - } - } + if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) { + return inheritedIndentation; + } - const effectiveParentStartLine = child.kind === ts.SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; - const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); + if (ts.isToken(child)) { + // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules + const tokenInfo = formattingScanner.readTokenInfo(child); + // JSX text shouldn't affect indenting + if (child.kind !== ts.SyntaxKind.JsxText) { + ts.Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); + consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); + return inheritedIndentation; + } + } - processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); + const effectiveParentStartLine = child.kind === ts.SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; + const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); - childContextNode = node; + processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); - if (isFirstListItem && parent.kind === ts.SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { - inheritedIndentation = childIndentation.indentation; - } + childContextNode = node; - return inheritedIndentation; + if (isFirstListItem && parent.kind === ts.SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { + inheritedIndentation = childIndentation.indentation; } - function processChildNodes(nodes: ts.NodeArray, parent: ts.Node, parentStartLine: number, parentDynamicIndentation: DynamicIndentation): void { - ts.Debug.assert(ts.isNodeArray(nodes)); + return inheritedIndentation; + } - const listStartToken = getOpenTokenForList(parent, nodes); + function processChildNodes(nodes: ts.NodeArray, parent: ts.Node, parentStartLine: number, parentDynamicIndentation: DynamicIndentation): void { + ts.Debug.assert(ts.isNodeArray(nodes)); - let listDynamicIndentation = parentDynamicIndentation; - let startLine = parentStartLine; - // node range is outside the target range - do not dive inside - if (!ts.rangeOverlapsWithStartEnd(originalRange, nodes.pos, nodes.end)) { - if (nodes.end < originalRange.pos) { - formattingScanner.skipToEndOf(nodes); - } - return; - } + const listStartToken = getOpenTokenForList(parent, nodes); - if (listStartToken !== ts.SyntaxKind.Unknown) { - // introduce a new indentation scope for lists (including list start and end tokens) - while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { - 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 = ts.getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); - indentationOnListStartToken = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); - } + let listDynamicIndentation = parentDynamicIndentation; + let startLine = parentStartLine; + // node range is outside the target range - do not dive inside + if (!ts.rangeOverlapsWithStartEnd(originalRange, nodes.pos, nodes.end)) { + if (nodes.end < originalRange.pos) { + formattingScanner.skipToEndOf(nodes); + } + return; + } - listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 + if (listStartToken !== ts.SyntaxKind.Unknown) { + // introduce a new indentation scope for lists (including list start and end tokens) + while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { + 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 { - // consume any tokens that precede the list as child elements of 'node' using its indentation scope - consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); + const startLinePosition = ts.getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); + indentationOnListStartToken = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); } - } - } - 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 !== ts.SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { - let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); - if (tokenInfo.token.kind === ts.SyntaxKind.CommaToken && ts.isCallLikeExpression(parent)) { - const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; - if (startLine !== commaTokenLine) { - formattingScanner.advance(); - tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; - } + listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 } - - // 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 && ts.rangeContainsRange(parent, tokenInfo.token)) { - // consume list end token - consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); + else { + // consume any tokens that precede the list as child elements of 'node' using its indentation scope + consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); } } } - function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: ts.Node, dynamicIndentation: DynamicIndentation, container: ts.Node, isListEndToken?: boolean): void { - ts.Debug.assert(ts.rangeContainsRange(parent, currentTokenInfo.token)); - - const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); - let indentToken = false; - - if (currentTokenInfo.leadingTrivia) { - processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); - } + 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); + } - let lineAction = LineAction.None; - const isTokenInRange = ts.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 listEndToken = getCloseTokenForOpenToken(listStartToken); + if (listEndToken !== ts.SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { + let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); + if (tokenInfo.token.kind === ts.SyntaxKind.CommaToken && ts.isCallLikeExpression(parent)) { + const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; + if (startLine !== commaTokenLine) { + formattingScanner.advance(); + tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; } } - if (currentTokenInfo.trailingTrivia) { - processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); + // 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 && ts.rangeContainsRange(parent, tokenInfo.token)) { + // consume list end token + consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); } + } + } - if (indentToken) { - const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ? - dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) : - Constants.Unknown; + function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: ts.Node, dynamicIndentation: DynamicIndentation, container: ts.Node, isListEndToken?: boolean): void { + ts.Debug.assert(ts.rangeContainsRange(parent, currentTokenInfo.token)); - 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)); - } + const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); + let indentToken = false; - // 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); + if (currentTokenInfo.leadingTrivia) { + processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); + } - lastIndentedLine = tokenStart.line; - indentationOnLastIndentedLine = tokenIndentation; + let lineAction = LineAction.None; + const isTokenInRange = ts.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; } } - - formattingScanner.advance(); - - childContextNode = parent; } - } - function indentTriviaItems(trivia: TextRangeWithKind[], commentIndentation: number, indentNextTokenOrTrivia: boolean, indentSingleLine: (item: TextRangeWithKind) => void) { - for (const triviaItem of trivia) { - const triviaInRange = ts.rangeContainsRange(originalRange, triviaItem); - switch (triviaItem.kind) { - case ts.SyntaxKind.MultiLineCommentTrivia: - if (triviaInRange) { - indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); - } - indentNextTokenOrTrivia = false; - break; - case ts.SyntaxKind.SingleLineCommentTrivia: - if (indentNextTokenOrTrivia && triviaInRange) { - indentSingleLine(triviaItem); - } - indentNextTokenOrTrivia = false; - break; - case ts.SyntaxKind.NewLineTrivia: - indentNextTokenOrTrivia = true; - break; - } + if (currentTokenInfo.trailingTrivia) { + processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); } - return indentNextTokenOrTrivia; - } - function processTrivia(trivia: TextRangeWithKind[], parent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation): void { - for (const triviaItem of trivia) { - if (ts.isComment(triviaItem.kind) && ts.rangeContainsRange(originalRange, triviaItem)) { - const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); - processRange(triviaItem, triviaItemStart, parent, contextNode, 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) { + const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container); + indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false)); } - } - } - function processRange(range: TextRangeWithKind, rangeStart: ts.LineAndCharacter, parent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation): LineAction { + // 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); - 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); + lastIndentedLine = tokenStart.line; + indentationOnLastIndentedLine = tokenIndentation; } } - previousRange = range; - previousParent = parent; - previousRangeStartLine = rangeStart.line; + formattingScanner.advance(); - return lineAction; + childContextNode = parent; } + } - function processPair(currentItem: TextRangeWithKind, currentStartLine: number, currentParent: ts.Node, previousItem: TextRangeWithKind, previousStartLine: number, previousParent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation | undefined): LineAction { - - formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); - - const rules = getRules(formattingContext); - - let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== 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. - ts.forEachRight(rules, rule => { - lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); - if (dynamicIndentation) { - 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, contextNode); - } - 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, contextNode); - } - break; - default: - ts.Debug.assert(lineAction === LineAction.None); - } + function indentTriviaItems(trivia: TextRangeWithKind[], commentIndentation: number, indentNextTokenOrTrivia: boolean, indentSingleLine: (item: TextRangeWithKind) => void) { + for (const triviaItem of trivia) { + const triviaInRange = ts.rangeContainsRange(originalRange, triviaItem); + switch (triviaItem.kind) { + case ts.SyntaxKind.MultiLineCommentTrivia: + if (triviaInRange) { + indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !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 = trimTrailingWhitespaces && !(rule.action & ts.formatting.RuleAction.DeleteSpace) && rule.flags !== ts.formatting.RuleFlags.CanDeleteNewLines; - }); - } - else { - trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== ts.SyntaxKind.EndOfFileToken; + indentNextTokenOrTrivia = false; + break; + case ts.SyntaxKind.SingleLineCommentTrivia: + if (indentNextTokenOrTrivia && triviaInRange) { + indentSingleLine(triviaItem); + } + indentNextTokenOrTrivia = false; + break; + case ts.SyntaxKind.NewLineTrivia: + indentNextTokenOrTrivia = true; + break; } + } + return indentNextTokenOrTrivia; + } - 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 processTrivia(trivia: TextRangeWithKind[], parent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation): void { + for (const triviaItem of trivia) { + if (ts.isComment(triviaItem.kind) && ts.rangeContainsRange(originalRange, triviaItem)) { + const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); + processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); } - - return lineAction; } + } + + function processRange(range: TextRangeWithKind, rangeStart: ts.LineAndCharacter, parent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation): 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); + 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 { - const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos); - const startLinePosition = ts.getStartPositionOfLine(tokenStart.line, sourceFile); - if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { - recordReplace(startLinePosition, tokenStart.character, indentationString); - } + lineAction = + processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation); } } - function characterToColumn(startLinePosition: number, characterInLine: number): number { - let column = 0; - for (let i = 0; i < characterInLine; i++) { - if (sourceFile.text.charCodeAt(startLinePosition + i) === ts.CharacterCodes.tab) { - column += options.tabSize! - column % options.tabSize!; - } - else { - column++; + previousRange = range; + previousParent = parent; + previousRangeStartLine = rangeStart.line; + + return lineAction; + } + + function processPair(currentItem: TextRangeWithKind, currentStartLine: number, currentParent: ts.Node, previousItem: TextRangeWithKind, previousStartLine: number, previousParent: ts.Node, contextNode: ts.Node, dynamicIndentation: DynamicIndentation | undefined): LineAction { + + formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); + + const rules = getRules(formattingContext); + + let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== 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. + ts.forEachRight(rules, rule => { + lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); + if (dynamicIndentation) { + 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, contextNode); + } + 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, contextNode); + } + break; + default: + ts.Debug.assert(lineAction === LineAction.None); + } } - } - return column; + + // 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 = trimTrailingWhitespaces && !(rule.action & ts.formatting.RuleAction.DeleteSpace) && rule.flags !== ts.formatting.RuleFlags.CanDeleteNewLines; + }); + } + else { + trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== ts.SyntaxKind.EndOfFileToken; } - function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { - return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); + 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 indentMultilineComment(commentRange: ts.TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true) { - // 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; - } + return lineAction; + } - const parts: ts.TextRange[] = []; - let startPos = commentRange.pos; - for (let line = startLine; line < endLine; line++) { - const endOfLine = ts.getEndLinePosition(line, sourceFile); - parts.push({ pos: startPos, end: endOfLine }); - startPos = ts.getStartPositionOfLine(line + 1, sourceFile); + 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 = ts.getStartPositionOfLine(tokenStart.line, sourceFile); + if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { + recordReplace(startLinePosition, tokenStart.character, indentationString); } + } + } - if (indentFinalLine) { - parts.push({ pos: startPos, end: commentRange.end }); + function characterToColumn(startLinePosition: number, characterInLine: number): number { + let column = 0; + for (let i = 0; i < characterInLine; i++) { + if (sourceFile.text.charCodeAt(startLinePosition + i) === ts.CharacterCodes.tab) { + column += options.tabSize! - column % options.tabSize!; + } + else { + column++; } + } + return column; + } - if (parts.length === 0) - return; - const startLinePos = ts.getStartPositionOfLine(startLine, sourceFile); - const nonWhitespaceColumnInFirstPart = ts.formatting.SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); + function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { + return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); + } - let startIndex = 0; - if (firstLineIsIndented) { - startIndex = 1; - startLine++; + function indentMultilineComment(commentRange: ts.TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true) { + // 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; + } - // shift all parts on the delta size - const delta = indentation - nonWhitespaceColumnInFirstPart.column; - for (let i = startIndex; i < parts.length; i++ , startLine++) { - const startLinePos = ts.getStartPositionOfLine(startLine, sourceFile); - const nonWhitespaceCharacterAndColumn = i === 0 - ? nonWhitespaceColumnInFirstPart - : ts.formatting.SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); - const newIndentation = nonWhitespaceCharacterAndColumn.column + delta; - if (newIndentation > 0) { - const indentationString = getIndentationString(newIndentation, options); - recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString); - } - else { - recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character); - } - } + const parts: ts.TextRange[] = []; + let startPos = commentRange.pos; + for (let line = startLine; line < endLine; line++) { + const endOfLine = ts.getEndLinePosition(line, sourceFile); + parts.push({ pos: startPos, end: endOfLine }); + startPos = ts.getStartPositionOfLine(line + 1, sourceFile); } - function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { - for (let line = line1; line < line2; line++) { - const lineStartPosition = ts.getStartPositionOfLine(line, sourceFile); - const lineEndPosition = ts.getEndLinePosition(line, sourceFile); + if (indentFinalLine) { + parts.push({ pos: startPos, end: commentRange.end }); + } - // do not trim whitespaces in comments or template expression - if (range && (ts.isComment(range.kind) || ts.isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { - continue; - } + if (parts.length === 0) + return; + const startLinePos = ts.getStartPositionOfLine(startLine, sourceFile); + const nonWhitespaceColumnInFirstPart = ts.formatting.SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); - const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); - if (whitespaceStart !== -1) { - ts.Debug.assert(whitespaceStart === lineStartPosition || !ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); - recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); - } - } + let startIndex = 0; + if (firstLineIsIndented) { + startIndex = 1; + startLine++; } - /** - * @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 && ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { - pos--; + // shift all parts on the delta size + const delta = indentation - nonWhitespaceColumnInFirstPart.column; + for (let i = startIndex; i < parts.length; i++ , startLine++) { + const startLinePos = ts.getStartPositionOfLine(startLine, sourceFile); + const nonWhitespaceCharacterAndColumn = i === 0 + ? nonWhitespaceColumnInFirstPart + : ts.formatting.SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); + 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. - * Exclude comments as they had been previously processed. - */ - function trimTrailingWhitespacesForRemainingRange(trivias: TextRangeWithKind[]) { - let startPos = previousRange ? previousRange.end : originalRange.pos; - for (const trivia of trivias) { - if (ts.isComment(trivia.kind)) { - if (startPos < trivia.pos) { - trimTrailingWitespacesForPositions(startPos, trivia.pos - 1, previousRange); - } + function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { + for (let line = line1; line < line2; line++) { + const lineStartPosition = ts.getStartPositionOfLine(line, sourceFile); + const lineEndPosition = ts.getEndLinePosition(line, sourceFile); - startPos = trivia.end + 1; - } + // do not trim whitespaces in comments or template expression + if (range && (ts.isComment(range.kind) || ts.isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { + continue; } - if (startPos < originalRange.end) { - trimTrailingWitespacesForPositions(startPos, originalRange.end, previousRange); + const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); + if (whitespaceStart !== -1) { + ts.Debug.assert(whitespaceStart === lineStartPosition || !ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); + recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); } } + } - function trimTrailingWitespacesForPositions(startPos: number, endPos: number, previousRange: TextRangeWithKind) { - const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(endPos).line; - - trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); + /** + * @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 && ts.isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { + pos--; } - - function recordDelete(start: number, len: number) { - if (len) { - edits.push(ts.createTextChangeFromStartLength(start, len, "")); - } + if (pos !== end) { + return pos + 1; } + return -1; + } - function recordReplace(start: number, len: number, newText: string) { - if (len || newText) { - edits.push(ts.createTextChangeFromStartLength(start, len, newText)); + /** + * Trimming will be done for lines after the previous range. + * Exclude comments as they had been previously processed. + */ + function trimTrailingWhitespacesForRemainingRange(trivias: TextRangeWithKind[]) { + let startPos = previousRange ? previousRange.end : originalRange.pos; + for (const trivia of trivias) { + if (ts.isComment(trivia.kind)) { + if (startPos < trivia.pos) { + trimTrailingWitespacesForPositions(startPos, trivia.pos - 1, previousRange); + } + + startPos = trivia.end + 1; } } - function recordInsert(start: number, text: string) { - if (text) { - edits.push(ts.createTextChangeFromStartLength(start, 0, text)); - } + if (startPos < originalRange.end) { + trimTrailingWitespacesForPositions(startPos, originalRange.end, previousRange); } + } - function applyRuleEdits(rule: ts.formatting.Rule, previousRange: TextRangeWithKind, previousStartLine: number, currentRange: TextRangeWithKind, currentStartLine: number): LineAction { - const onLaterLine = currentStartLine !== previousStartLine; - switch (rule.action) { - case ts.formatting.RuleAction.StopProcessingSpaceActions: - // no action required - return LineAction.None; - case ts.formatting.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 ts.formatting.RuleAction.DeleteToken: - recordDelete(previousRange.pos, previousRange.end - previousRange.pos); - break; - case ts.formatting.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 !== ts.formatting.RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { - return LineAction.None; - } + function trimTrailingWitespacesForPositions(startPos: number, endPos: number, previousRange: TextRangeWithKind) { + const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(endPos).line; - // 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, ts.getNewLineOrDefaultFromHost(host, options)); - return onLaterLine ? LineAction.None : LineAction.LineAdded; - } - break; - case ts.formatting.RuleAction.InsertSpace: - // exit early if we on different lines and rule cannot change number of newlines - if (rule.flags !== ts.formatting.RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { - return LineAction.None; - } + trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); + } - const posDelta = currentRange.pos - previousRange.end; - if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== ts.CharacterCodes.space) { - recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); - return onLaterLine ? LineAction.LineRemoved : LineAction.None; - } - break; - case ts.formatting.RuleAction.InsertTrailingSemicolon: - recordInsert(previousRange.end, ";"); - } - return LineAction.None; + function recordDelete(start: number, len: number) { + if (len) { + edits.push(ts.createTextChangeFromStartLength(start, len, "")); } } - const enum LineAction { - None, - LineAdded, - LineRemoved + function recordReplace(start: number, len: number, newText: string) { + if (len || newText) { + edits.push(ts.createTextChangeFromStartLength(start, len, newText)); + } } - /** - * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. - */ - export function getRangeOfEnclosingComment(sourceFile: ts.SourceFile, position: number, precedingToken?: ts.Node | null, tokenAtPosition = ts.getTokenAtPosition(sourceFile, position)): ts.CommentRange | undefined { - const jsdoc = ts.findAncestor(tokenAtPosition, ts.isJSDoc); - if (jsdoc) - tokenAtPosition = jsdoc.parent; - const tokenStart = tokenAtPosition.getStart(sourceFile); - if (tokenStart <= position && position < tokenAtPosition.getEnd()) { - return undefined; + function recordInsert(start: number, text: string) { + if (text) { + edits.push(ts.createTextChangeFromStartLength(start, 0, text)); } - - // eslint-disable-next-line no-null/no-null - precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? ts.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 && ts.getTrailingCommentRanges(sourceFile.text, precedingToken.end); - const leadingCommentRangesOfNextToken = ts.getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); - const commentRanges = ts.concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); - return commentRanges && ts.find(commentRanges, range => ts.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 === ts.SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); } - function getOpenTokenForList(node: ts.Node, list: readonly ts.Node[]) { - switch (node.kind) { - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.ArrowFunction: - if ((node as ts.FunctionDeclaration).typeParameters === list) { - return ts.SyntaxKind.LessThanToken; - } - else if ((node as ts.FunctionDeclaration).parameters === list) { - return ts.SyntaxKind.OpenParenToken; + function applyRuleEdits(rule: ts.formatting.Rule, previousRange: TextRangeWithKind, previousStartLine: number, currentRange: TextRangeWithKind, currentStartLine: number): LineAction { + const onLaterLine = currentStartLine !== previousStartLine; + switch (rule.action) { + case ts.formatting.RuleAction.StopProcessingSpaceActions: + // no action required + return LineAction.None; + case ts.formatting.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 ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - if ((node as ts.CallExpression).typeArguments === list) { - return ts.SyntaxKind.LessThanToken; + case ts.formatting.RuleAction.DeleteToken: + recordDelete(previousRange.pos, previousRange.end - previousRange.pos); + break; + case ts.formatting.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 !== ts.formatting.RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return LineAction.None; } - else if ((node as ts.CallExpression).arguments === list) { - return ts.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, ts.getNewLineOrDefaultFromHost(host, options)); + return onLaterLine ? LineAction.None : LineAction.LineAdded; } break; - case ts.SyntaxKind.TypeReference: - if ((node as ts.TypeReferenceNode).typeArguments === list) { - return ts.SyntaxKind.LessThanToken; + case ts.formatting.RuleAction.InsertSpace: + // exit early if we on different lines and rule cannot change number of newlines + if (rule.flags !== ts.formatting.RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return LineAction.None; } - break; - case ts.SyntaxKind.TypeLiteral: - return ts.SyntaxKind.OpenBraceToken; - } - return ts.SyntaxKind.Unknown; - } - - function getCloseTokenForOpenToken(kind: ts.SyntaxKind) { - switch (kind) { - case ts.SyntaxKind.OpenParenToken: - return ts.SyntaxKind.CloseParenToken; - case ts.SyntaxKind.LessThanToken: - return ts.SyntaxKind.GreaterThanToken; - case ts.SyntaxKind.OpenBraceToken: - return ts.SyntaxKind.CloseBraceToken; + const posDelta = currentRange.pos - previousRange.end; + if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== ts.CharacterCodes.space) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); + return onLaterLine ? LineAction.LineRemoved : LineAction.None; + } + break; + case ts.formatting.RuleAction.InsertTrailingSemicolon: + recordInsert(previousRange.end, ";"); } - return ts.SyntaxKind.Unknown; + return LineAction.None; } - let internedSizes: { - tabSize: number; - indentSize: number; - }; - let internedTabsIndentation: string[] | undefined; - let internedSpacesIndentation: string[] | undefined; +} - export function getIndentationString(indentation: number, options: ts.EditorSettings): string { - // reset interned strings if FormatCodeOptions were changed - const resetInternedStrings = !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); +const enum LineAction { + None, + LineAdded, + LineRemoved +} - if (resetInternedStrings) { - internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! }; - internedTabsIndentation = internedSpacesIndentation = undefined; - } +/** + * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. + */ +export function getRangeOfEnclosingComment(sourceFile: ts.SourceFile, position: number, precedingToken?: ts.Node | null, tokenAtPosition = ts.getTokenAtPosition(sourceFile, position)): ts.CommentRange | undefined { + const jsdoc = ts.findAncestor(tokenAtPosition, ts.isJSDoc); + if (jsdoc) + tokenAtPosition = jsdoc.parent; + const tokenStart = tokenAtPosition.getStart(sourceFile); + if (tokenStart <= position && position < tokenAtPosition.getEnd()) { + return undefined; + } - if (!options.convertTabsToSpaces) { - const tabs = Math.floor(indentation / options.tabSize!); - const spaces = indentation - tabs * options.tabSize!; + // eslint-disable-next-line no-null/no-null + precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? ts.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 && ts.getTrailingCommentRanges(sourceFile.text, precedingToken.end); + const leadingCommentRangesOfNextToken = ts.getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); + const commentRanges = ts.concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); + return commentRanges && ts.find(commentRanges, range => ts.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 === ts.SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); +} - let tabString: string; - if (!internedTabsIndentation) { - internedTabsIndentation = []; +function getOpenTokenForList(node: ts.Node, list: readonly ts.Node[]) { + switch (node.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.ArrowFunction: + if ((node as ts.FunctionDeclaration).typeParameters === list) { + return ts.SyntaxKind.LessThanToken; } - - if (internedTabsIndentation[tabs] === undefined) { - internedTabsIndentation[tabs] = tabString = ts.repeatString("\t", tabs); + else if ((node as ts.FunctionDeclaration).parameters === list) { + return ts.SyntaxKind.OpenParenToken; } - else { - tabString = internedTabsIndentation[tabs]; + break; + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + if ((node as ts.CallExpression).typeArguments === list) { + return ts.SyntaxKind.LessThanToken; + } + else if ((node as ts.CallExpression).arguments === list) { + return ts.SyntaxKind.OpenParenToken; + } + break; + case ts.SyntaxKind.TypeReference: + if ((node as ts.TypeReferenceNode).typeArguments === list) { + return ts.SyntaxKind.LessThanToken; } + break; + case ts.SyntaxKind.TypeLiteral: + return ts.SyntaxKind.OpenBraceToken; + } + + return ts.SyntaxKind.Unknown; +} - return spaces ? tabString + ts.repeatString(" ", spaces) : tabString; +function getCloseTokenForOpenToken(kind: ts.SyntaxKind) { + switch (kind) { + case ts.SyntaxKind.OpenParenToken: + return ts.SyntaxKind.CloseParenToken; + case ts.SyntaxKind.LessThanToken: + return ts.SyntaxKind.GreaterThanToken; + case ts.SyntaxKind.OpenBraceToken: + return ts.SyntaxKind.CloseBraceToken; + } + return ts.SyntaxKind.Unknown; +} +let internedSizes: { + tabSize: number; + indentSize: number; +}; +let internedTabsIndentation: string[] | undefined; +let internedSpacesIndentation: string[] | undefined; + +export function getIndentationString(indentation: number, options: ts.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 = ts.repeatString("\t", tabs); } else { - let spacesString: string; - const quotient = Math.floor(indentation / options.indentSize!); - const remainder = indentation % options.indentSize!; - if (!internedSpacesIndentation) { - internedSpacesIndentation = []; - } + tabString = internedTabsIndentation[tabs]; + } - if (internedSpacesIndentation[quotient] === undefined) { - spacesString = ts.repeatString(" ", options.indentSize! * quotient); - internedSpacesIndentation[quotient] = spacesString; - } - else { - spacesString = internedSpacesIndentation[quotient]; - } + return spaces ? tabString + ts.repeatString(" ", spaces) : tabString; + } + else { + let spacesString: string; + const quotient = Math.floor(indentation / options.indentSize!); + const remainder = indentation % options.indentSize!; + if (!internedSpacesIndentation) { + internedSpacesIndentation = []; + } - return remainder ? spacesString + ts.repeatString(" ", remainder) : spacesString; + if (internedSpacesIndentation[quotient] === undefined) { + spacesString = ts.repeatString(" ", options.indentSize! * quotient); + internedSpacesIndentation[quotient] = spacesString; + } + else { + spacesString = internedSpacesIndentation[quotient]; } + + return remainder ? spacesString + ts.repeatString(" ", remainder) : spacesString; } } +} diff --git a/src/services/formatting/formattingContext.ts b/src/services/formatting/formattingContext.ts index 01bb9f6694b4e..9e60578aca8bd 100644 --- a/src/services/formatting/formattingContext.ts +++ b/src/services/formatting/formattingContext.ts @@ -1,101 +1,101 @@ /* @internal */ namespace ts.formatting { - export const enum FormattingRequestKind { - FormatDocument, - FormatSelection, - FormatOnEnter, - FormatOnSemicolon, - FormatOnOpeningCurlyBrace, - FormatOnClosingCurlyBrace - } +export const enum FormattingRequestKind { + FormatDocument, + FormatSelection, + FormatOnEnter, + FormatOnSemicolon, + FormatOnOpeningCurlyBrace, + FormatOnClosingCurlyBrace +} - export class FormattingContext { - public currentTokenSpan!: ts.formatting.TextRangeWithKind; - public nextTokenSpan!: ts.formatting.TextRangeWithKind; - public contextNode!: ts.Node; - public currentTokenParent!: ts.Node; - public nextTokenParent!: ts.Node; +export class FormattingContext { + public currentTokenSpan!: ts.formatting.TextRangeWithKind; + public nextTokenSpan!: ts.formatting.TextRangeWithKind; + public contextNode!: ts.Node; + public currentTokenParent!: ts.Node; + public nextTokenParent!: ts.Node; - private contextNodeAllOnSameLine: boolean | undefined; - private nextNodeAllOnSameLine: boolean | undefined; - private tokensAreOnSameLine: boolean | undefined; - private contextNodeBlockIsOnOneLine: boolean | undefined; - private nextNodeBlockIsOnOneLine: boolean | undefined; + private contextNodeAllOnSameLine: boolean | undefined; + private nextNodeAllOnSameLine: boolean | undefined; + private tokensAreOnSameLine: boolean | undefined; + private contextNodeBlockIsOnOneLine: boolean | undefined; + private nextNodeBlockIsOnOneLine: boolean | undefined; - constructor(public readonly sourceFile: ts.SourceFileLike, public formattingRequestKind: FormattingRequestKind, public options: ts.FormatCodeSettings) { - } - public updateContext(currentRange: ts.formatting.TextRangeWithKind, currentTokenParent: ts.Node, nextRange: ts.formatting.TextRangeWithKind, nextTokenParent: ts.Node, commonParent: ts.Node) { - this.currentTokenSpan = ts.Debug.checkDefined(currentRange); - this.currentTokenParent = ts.Debug.checkDefined(currentTokenParent); - this.nextTokenSpan = ts.Debug.checkDefined(nextRange); - this.nextTokenParent = ts.Debug.checkDefined(nextTokenParent); - this.contextNode = ts.Debug.checkDefined(commonParent); - - // drop cached results - this.contextNodeAllOnSameLine = undefined; - this.nextNodeAllOnSameLine = undefined; - this.tokensAreOnSameLine = undefined; - this.contextNodeBlockIsOnOneLine = undefined; - this.nextNodeBlockIsOnOneLine = undefined; + constructor(public readonly sourceFile: ts.SourceFileLike, public formattingRequestKind: FormattingRequestKind, public options: ts.FormatCodeSettings) { + } + public updateContext(currentRange: ts.formatting.TextRangeWithKind, currentTokenParent: ts.Node, nextRange: ts.formatting.TextRangeWithKind, nextTokenParent: ts.Node, commonParent: ts.Node) { + this.currentTokenSpan = ts.Debug.checkDefined(currentRange); + this.currentTokenParent = ts.Debug.checkDefined(currentTokenParent); + this.nextTokenSpan = ts.Debug.checkDefined(nextRange); + this.nextTokenParent = ts.Debug.checkDefined(nextTokenParent); + this.contextNode = ts.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 ContextNodeAllOnSameLine(): boolean { - if (this.contextNodeAllOnSameLine === undefined) { - this.contextNodeAllOnSameLine = this.NodeIsOnOneLine(this.contextNode); - } + return this.contextNodeAllOnSameLine; + } - return this.contextNodeAllOnSameLine; + public NextNodeAllOnSameLine(): boolean { + if (this.nextNodeAllOnSameLine === undefined) { + this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); } - public NextNodeAllOnSameLine(): boolean { - if (this.nextNodeAllOnSameLine === undefined) { - this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); - } + return this.nextNodeAllOnSameLine; + } - 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 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.tokensAreOnSameLine; + public ContextNodeBlockIsOnOneLine() { + if (this.contextNodeBlockIsOnOneLine === undefined) { + this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); } - public ContextNodeBlockIsOnOneLine() { - if (this.contextNodeBlockIsOnOneLine === undefined) { - this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); - } + return this.contextNodeBlockIsOnOneLine; + } - return this.contextNodeBlockIsOnOneLine; + public NextNodeBlockIsOnOneLine() { + if (this.nextNodeBlockIsOnOneLine === undefined) { + this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); } - public NextNodeBlockIsOnOneLine() { - if (this.nextNodeBlockIsOnOneLine === undefined) { - this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); - } + return this.nextNodeBlockIsOnOneLine; + } - return this.nextNodeBlockIsOnOneLine; - } + private NodeIsOnOneLine(node: ts.Node): boolean { + const startLine = this.sourceFile.getLineAndCharacterOfPosition(node.getStart(this.sourceFile)).line; + const endLine = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + return startLine === endLine; + } - private NodeIsOnOneLine(node: ts.Node): boolean { - const startLine = this.sourceFile.getLineAndCharacterOfPosition(node.getStart(this.sourceFile)).line; - const endLine = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + private BlockIsOnOneLine(node: ts.Node): boolean { + const openBrace = ts.findChildOfKind(node, ts.SyntaxKind.OpenBraceToken, this.sourceFile); + const closeBrace = ts.findChildOfKind(node, ts.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: ts.Node): boolean { - const openBrace = ts.findChildOfKind(node, ts.SyntaxKind.OpenBraceToken, this.sourceFile); - const closeBrace = ts.findChildOfKind(node, ts.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 17adacd7e07b1..f779c87da7348 100644 --- a/src/services/formatting/formattingScanner.ts +++ b/src/services/formatting/formattingScanner.ts @@ -1,301 +1,301 @@ /* @internal */ namespace ts.formatting { - const standardScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.Standard); - const jsxScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.JSX); - - export interface FormattingScanner { - advance(): void; - getStartPos(): number; - isOnToken(): boolean; - isOnEOF(): boolean; - readTokenInfo(n: ts.Node): ts.formatting.TokenInfo; - readEOFTokenRange(): ts.formatting.TextRangeWithKind; - getCurrentLeadingTrivia(): ts.formatting.TextRangeWithKind[] | undefined; - lastTrailingTriviaWasNewLine(): boolean; - skipToEndOf(node: ts.Node | ts.NodeArray): void; - skipToStartOf(node: ts.Node): void; - } +const standardScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.Standard); +const jsxScanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false, ts.LanguageVariant.JSX); + +export interface FormattingScanner { + advance(): void; + getStartPos(): number; + isOnToken(): boolean; + isOnEOF(): boolean; + readTokenInfo(n: ts.Node): ts.formatting.TokenInfo; + readEOFTokenRange(): ts.formatting.TextRangeWithKind; + getCurrentLeadingTrivia(): ts.formatting.TextRangeWithKind[] | undefined; + lastTrailingTriviaWasNewLine(): boolean; + skipToEndOf(node: ts.Node | ts.NodeArray): void; + skipToStartOf(node: ts.Node): void; +} - const enum ScanAction { - Scan, - RescanGreaterThanToken, - RescanSlashToken, - RescanTemplateToken, - RescanJsxIdentifier, - RescanJsxText, - RescanJsxAttributeValue - } +const enum ScanAction { + Scan, + RescanGreaterThanToken, + RescanSlashToken, + RescanTemplateToken, + RescanJsxIdentifier, + RescanJsxText, + RescanJsxAttributeValue +} - export function getFormattingScanner(text: string, languageVariant: ts.LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T { - const scanner = languageVariant === ts.LanguageVariant.JSX ? jsxScanner : standardScanner; - - scanner.setText(text); - scanner.setTextPos(startPos); - - let wasNewLine = true; - let leadingTrivia: ts.formatting.TextRangeWithTriviaKind[] | undefined; - let trailingTrivia: ts.formatting.TextRangeWithTriviaKind[] | undefined; - - let savedPos: number; - let lastScanAction: ScanAction | undefined; - let lastTokenInfo: ts.formatting.TokenInfo | undefined; - - const res = cb({ - advance, - readTokenInfo, - readEOFTokenRange, - isOnToken, - isOnEOF, - getCurrentLeadingTrivia: () => leadingTrivia, - lastTrailingTriviaWasNewLine: () => wasNewLine, - skipToEndOf, - skipToStartOf, - getStartPos: () => lastTokenInfo?.token.pos ?? scanner.getTokenPos(), - }); +export function getFormattingScanner(text: string, languageVariant: ts.LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T { + const scanner = languageVariant === ts.LanguageVariant.JSX ? jsxScanner : standardScanner; - lastTokenInfo = undefined; - scanner.setText(undefined); + scanner.setText(text); + scanner.setTextPos(startPos); - return res; + let wasNewLine = true; + let leadingTrivia: ts.formatting.TextRangeWithTriviaKind[] | undefined; + let trailingTrivia: ts.formatting.TextRangeWithTriviaKind[] | undefined; - function advance(): void { - lastTokenInfo = undefined; - const isStarted = scanner.getStartPos() !== startPos; + let savedPos: number; + let lastScanAction: ScanAction | undefined; + let lastTokenInfo: ts.formatting.TokenInfo | undefined; - if (isStarted) { - wasNewLine = !!trailingTrivia && ts.last(trailingTrivia).kind === ts.SyntaxKind.NewLineTrivia; - } - else { - scanner.scan(); - } + const res = cb({ + advance, + readTokenInfo, + readEOFTokenRange, + isOnToken, + isOnEOF, + getCurrentLeadingTrivia: () => leadingTrivia, + lastTrailingTriviaWasNewLine: () => wasNewLine, + skipToEndOf, + skipToStartOf, + getStartPos: () => lastTokenInfo?.token.pos ?? scanner.getTokenPos(), + }); - leadingTrivia = undefined; - trailingTrivia = undefined; + lastTokenInfo = undefined; + scanner.setText(undefined); - let pos = scanner.getStartPos(); + return res; - // Read leading trivia and token - while (pos < endPos) { - const t = scanner.getToken(); - if (!ts.isTrivia(t)) { - break; - } + function advance(): void { + lastTokenInfo = undefined; + const isStarted = scanner.getStartPos() !== startPos; - // consume leading trivia - scanner.scan(); - const item: ts.formatting.TextRangeWithTriviaKind = { - pos, - end: scanner.getStartPos(), - kind: t - }; + if (isStarted) { + wasNewLine = !!trailingTrivia && ts.last(trailingTrivia).kind === ts.SyntaxKind.NewLineTrivia; + } + else { + scanner.scan(); + } - pos = scanner.getStartPos(); + leadingTrivia = undefined; + trailingTrivia = undefined; - leadingTrivia = ts.append(leadingTrivia, item); + let pos = scanner.getStartPos(); + + // Read leading trivia and token + while (pos < endPos) { + const t = scanner.getToken(); + if (!ts.isTrivia(t)) { + break; } - savedPos = scanner.getStartPos(); - } + // consume leading trivia + scanner.scan(); + const item: ts.formatting.TextRangeWithTriviaKind = { + pos, + end: scanner.getStartPos(), + kind: t + }; - function shouldRescanGreaterThanToken(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.GreaterThanEqualsToken: - case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanToken: - return true; - } + pos = scanner.getStartPos(); - return false; + leadingTrivia = ts.append(leadingTrivia, item); } - function shouldRescanJsxIdentifier(node: ts.Node): boolean { - if (node.parent) { - switch (node.parent.kind) { - case ts.SyntaxKind.JsxAttribute: - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxClosingElement: - case ts.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 ts.isKeyword(node.kind) || node.kind === ts.SyntaxKind.Identifier; - } - } + savedPos = scanner.getStartPos(); + } - return false; + function shouldRescanGreaterThanToken(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + return true; } - function shouldRescanJsxText(node: ts.Node): boolean { - return ts.isJsxText(node); - } + return false; + } - function shouldRescanSlashToken(container: ts.Node): boolean { - return container.kind === ts.SyntaxKind.RegularExpressionLiteral; + function shouldRescanJsxIdentifier(node: ts.Node): boolean { + if (node.parent) { + switch (node.parent.kind) { + case ts.SyntaxKind.JsxAttribute: + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxClosingElement: + case ts.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 ts.isKeyword(node.kind) || node.kind === ts.SyntaxKind.Identifier; + } } - function shouldRescanTemplateToken(container: ts.Node): boolean { - return container.kind === ts.SyntaxKind.TemplateMiddle || - container.kind === ts.SyntaxKind.TemplateTail; - } + return false; + } - function shouldRescanJsxAttributeValue(node: ts.Node): boolean { - return node.parent && ts.isJsxAttribute(node.parent) && node.parent.initializer === node; - } + function shouldRescanJsxText(node: ts.Node): boolean { + return ts.isJsxText(node); + } - function startsWithSlashToken(t: ts.SyntaxKind): boolean { - return t === ts.SyntaxKind.SlashToken || t === ts.SyntaxKind.SlashEqualsToken; - } + function shouldRescanSlashToken(container: ts.Node): boolean { + return container.kind === ts.SyntaxKind.RegularExpressionLiteral; + } - function readTokenInfo(n: ts.Node): ts.formatting.TokenInfo { - ts.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 shouldRescanTemplateToken(container: ts.Node): boolean { + return container.kind === ts.SyntaxKind.TemplateMiddle || + container.kind === ts.SyntaxKind.TemplateTail; + } - if (scanner.getStartPos() !== savedPos) { - ts.Debug.assert(lastTokenInfo !== undefined); - // readTokenInfo was called before but scan action differs - rescan text - scanner.setTextPos(savedPos); - scanner.scan(); - } + function shouldRescanJsxAttributeValue(node: ts.Node): boolean { + return node.parent && ts.isJsxAttribute(node.parent) && node.parent.initializer === node; + } - let currentToken = getNextToken(n, expectedScanAction); + function startsWithSlashToken(t: ts.SyntaxKind): boolean { + return t === ts.SyntaxKind.SlashToken || t === ts.SyntaxKind.SlashEqualsToken; + } - const token = ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); + function readTokenInfo(n: ts.Node): ts.formatting.TokenInfo { + ts.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); + } - // consume trailing trivia - if (trailingTrivia) { - trailingTrivia = undefined; - } - while (scanner.getStartPos() < endPos) { - currentToken = scanner.scan(); - if (!ts.isTrivia(currentToken)) { - break; - } - const trivia = ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); + if (scanner.getStartPos() !== savedPos) { + ts.Debug.assert(lastTokenInfo !== undefined); + // readTokenInfo was called before but scan action differs - rescan text + scanner.setTextPos(savedPos); + scanner.scan(); + } - if (!trailingTrivia) { - trailingTrivia = []; - } + let currentToken = getNextToken(n, expectedScanAction); - trailingTrivia.push(trivia); + const token = ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); - if (currentToken === ts.SyntaxKind.NewLineTrivia) { - // move past new line - scanner.scan(); - break; - } + // consume trailing trivia + if (trailingTrivia) { + trailingTrivia = undefined; + } + while (scanner.getStartPos() < endPos) { + currentToken = scanner.scan(); + if (!ts.isTrivia(currentToken)) { + break; } + const trivia = ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); - lastTokenInfo = { leadingTrivia, trailingTrivia, token }; + if (!trailingTrivia) { + trailingTrivia = []; + } - return fixTokenKind(lastTokenInfo, n); - } + trailingTrivia.push(trivia); - function getNextToken(n: ts.Node, expectedScanAction: ScanAction): ts.SyntaxKind { - const token = scanner.getToken(); - lastScanAction = ScanAction.Scan; - switch (expectedScanAction) { - case ScanAction.RescanGreaterThanToken: - if (token === ts.SyntaxKind.GreaterThanToken) { - lastScanAction = ScanAction.RescanGreaterThanToken; - const newToken = scanner.reScanGreaterToken(); - ts.Debug.assert(n.kind === newToken); - return newToken; - } - break; - case ScanAction.RescanSlashToken: - if (startsWithSlashToken(token)) { - lastScanAction = ScanAction.RescanSlashToken; - const newToken = scanner.reScanSlashToken(); - ts.Debug.assert(n.kind === newToken); - return newToken; - } - break; - case ScanAction.RescanTemplateToken: - if (token === ts.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(/* allowMultilineJsxText */ false); - case ScanAction.RescanJsxAttributeValue: - lastScanAction = ScanAction.RescanJsxAttributeValue; - return scanner.reScanJsxAttributeValue(); - case ScanAction.Scan: - break; - default: - ts.Debug.assertNever(expectedScanAction); + if (currentToken === ts.SyntaxKind.NewLineTrivia) { + // move past new line + scanner.scan(); + break; } - return token; } - function readEOFTokenRange(): ts.formatting.TextRangeWithKind { - ts.Debug.assert(isOnEOF()); - return ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), ts.SyntaxKind.EndOfFileToken); - } + lastTokenInfo = { leadingTrivia, trailingTrivia, token }; - function isOnToken(): boolean { - const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); - return current !== ts.SyntaxKind.EndOfFileToken && !ts.isTrivia(current); - } + return fixTokenKind(lastTokenInfo, n); + } - function isOnEOF(): boolean { - const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); - return current === ts.SyntaxKind.EndOfFileToken; + function getNextToken(n: ts.Node, expectedScanAction: ScanAction): ts.SyntaxKind { + const token = scanner.getToken(); + lastScanAction = ScanAction.Scan; + switch (expectedScanAction) { + case ScanAction.RescanGreaterThanToken: + if (token === ts.SyntaxKind.GreaterThanToken) { + lastScanAction = ScanAction.RescanGreaterThanToken; + const newToken = scanner.reScanGreaterToken(); + ts.Debug.assert(n.kind === newToken); + return newToken; + } + break; + case ScanAction.RescanSlashToken: + if (startsWithSlashToken(token)) { + lastScanAction = ScanAction.RescanSlashToken; + const newToken = scanner.reScanSlashToken(); + ts.Debug.assert(n.kind === newToken); + return newToken; + } + break; + case ScanAction.RescanTemplateToken: + if (token === ts.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(/* allowMultilineJsxText */ false); + case ScanAction.RescanJsxAttributeValue: + lastScanAction = ScanAction.RescanJsxAttributeValue; + return scanner.reScanJsxAttributeValue(); + case ScanAction.Scan: + break; + default: + ts.Debug.assertNever(expectedScanAction); } + return token; + } - // 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: ts.formatting.TokenInfo, container: ts.Node): ts.formatting.TokenInfo { - if (ts.isToken(container) && tokenInfo.token.kind !== container.kind) { - tokenInfo.token.kind = container.kind; - } - return tokenInfo; - } + function readEOFTokenRange(): ts.formatting.TextRangeWithKind { + ts.Debug.assert(isOnEOF()); + return ts.formatting.createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), ts.SyntaxKind.EndOfFileToken); + } - function skipToEndOf(node: ts.Node | ts.NodeArray): void { - scanner.setTextPos(node.end); - savedPos = scanner.getStartPos(); - lastScanAction = undefined; - lastTokenInfo = undefined; - wasNewLine = false; - leadingTrivia = undefined; - trailingTrivia = undefined; - } + function isOnToken(): boolean { + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + return current !== ts.SyntaxKind.EndOfFileToken && !ts.isTrivia(current); + } - function skipToStartOf(node: ts.Node): void { - scanner.setTextPos(node.pos); - savedPos = scanner.getStartPos(); - lastScanAction = undefined; - lastTokenInfo = undefined; - wasNewLine = false; - leadingTrivia = undefined; - trailingTrivia = undefined; + function isOnEOF(): boolean { + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + return current === ts.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: ts.formatting.TokenInfo, container: ts.Node): ts.formatting.TokenInfo { + if (ts.isToken(container) && tokenInfo.token.kind !== container.kind) { + tokenInfo.token.kind = container.kind; } + return tokenInfo; + } + + function skipToEndOf(node: ts.Node | ts.NodeArray): void { + scanner.setTextPos(node.end); + savedPos = scanner.getStartPos(); + lastScanAction = undefined; + lastTokenInfo = undefined; + wasNewLine = false; + leadingTrivia = undefined; + trailingTrivia = undefined; } + + function skipToStartOf(node: ts.Node): void { + scanner.setTextPos(node.pos); + 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 f0a7730aff5a8..0d072b8a3d04a 100644 --- a/src/services/formatting/rule.ts +++ b/src/services/formatting/rule.ts @@ -1,37 +1,37 @@ /* @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 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: ts.formatting.FormattingContext) => boolean; - export const anyContext: readonly ContextPredicate[] = ts.emptyArray; +export type ContextPredicate = (context: ts.formatting.FormattingContext) => boolean; +export const anyContext: readonly ContextPredicate[] = ts.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, +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 - } + StopAction = StopProcessingSpaceActions | StopProcessingTokenActions, + ModifySpaceAction = InsertSpace | InsertNewLine | DeleteSpace, + ModifyTokenAction = DeleteToken | InsertTrailingSemicolon +} - export const enum RuleFlags { - None, - CanDeleteNewLines - } +export const enum RuleFlags { + None, + CanDeleteNewLines +} - export interface TokenRange { - readonly tokens: readonly ts.SyntaxKind[]; - readonly isSpecific: boolean; - } +export interface TokenRange { + readonly tokens: readonly ts.SyntaxKind[]; + readonly isSpecific: boolean; +} } diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 60b1785d25bd9..12d25168f024f 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -1,841 +1,841 @@ /* @internal */ namespace ts.formatting { - export interface RuleSpec { - readonly leftTokenRange: ts.formatting.TokenRange; - readonly rightTokenRange: ts.formatting.TokenRange; - readonly rule: ts.formatting.Rule; - } +export interface RuleSpec { + readonly leftTokenRange: ts.formatting.TokenRange; + readonly rightTokenRange: ts.formatting.TokenRange; + readonly rule: ts.formatting.Rule; +} - export function getAllRules(): RuleSpec[] { - const allTokens: ts.SyntaxKind[] = []; - for (let token = ts.SyntaxKind.FirstToken; token <= ts.SyntaxKind.LastToken; token++) { - if (token !== ts.SyntaxKind.EndOfFileToken) { - allTokens.push(token); - } +export function getAllRules(): RuleSpec[] { + const allTokens: ts.SyntaxKind[] = []; + for (let token = ts.SyntaxKind.FirstToken; token <= ts.SyntaxKind.LastToken; token++) { + if (token !== ts.SyntaxKind.EndOfFileToken) { + allTokens.push(token); } - function anyTokenExcept(...tokens: ts.SyntaxKind[]): ts.formatting.TokenRange { - return { tokens: allTokens.filter(t => !tokens.some(t2 => t2 === t)), isSpecific: false }; - } - - const anyToken: ts.formatting.TokenRange = { tokens: allTokens, isSpecific: false }; - const anyTokenIncludingMultilineComments = tokenRangeFrom([...allTokens, ts.SyntaxKind.MultiLineCommentTrivia]); - const anyTokenIncludingEOF = tokenRangeFrom([...allTokens, ts.SyntaxKind.EndOfFileToken]); - const keywords = tokenRangeFromRange(ts.SyntaxKind.FirstKeyword, ts.SyntaxKind.LastKeyword); - const binaryOperators = tokenRangeFromRange(ts.SyntaxKind.FirstBinaryOperator, ts.SyntaxKind.LastBinaryOperator); - const binaryKeywordOperators = [ts.SyntaxKind.InKeyword, ts.SyntaxKind.InstanceOfKeyword, ts.SyntaxKind.OfKeyword, ts.SyntaxKind.AsKeyword, ts.SyntaxKind.IsKeyword]; - const unaryPrefixOperators = [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken, ts.SyntaxKind.TildeToken, ts.SyntaxKind.ExclamationToken]; - const unaryPrefixExpressions = [ - ts.SyntaxKind.NumericLiteral, ts.SyntaxKind.BigIntLiteral, ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, - ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword - ]; - const unaryPreincrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword]; - const unaryPostincrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CloseBracketToken, ts.SyntaxKind.NewKeyword]; - const unaryPredecrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword]; - const unaryPostdecrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CloseBracketToken, ts.SyntaxKind.NewKeyword]; - const comments = [ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia]; - const typeNames = [ts.SyntaxKind.Identifier, ...ts.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([ts.SyntaxKind.Identifier, ts.SyntaxKind.MultiLineCommentTrivia, ts.SyntaxKind.ClassKeyword, ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.ImportKeyword]); - - // Place a space before open brace in a control flow construct - const controlOpenBraceLeftTokenRange = tokenRangeFrom([ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.MultiLineCommentTrivia, ts.SyntaxKind.DoKeyword, ts.SyntaxKind.TryKeyword, ts.SyntaxKind.FinallyKeyword, ts.SyntaxKind.ElseKeyword]); - - // These rules are higher in priority than user-configurable - const highPriorityCommonRules = [ - // Leave comments alone - rule("IgnoreBeforeComment", anyToken, comments, ts.formatting.anyContext, ts.formatting.RuleAction.StopProcessingSpaceActions), - rule("IgnoreAfterLineComment", ts.SyntaxKind.SingleLineCommentTrivia, anyToken, ts.formatting.anyContext, ts.formatting.RuleAction.StopProcessingSpaceActions), - rule("NotSpaceBeforeColon", anyToken, ts.SyntaxKind.ColonToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceAfterColon", ts.SyntaxKind.ColonToken, anyToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeQuestionMark", anyToken, ts.SyntaxKind.QuestionToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), - // insert space after '?' only when it is used in conditional operator - rule("SpaceAfterQuestionMarkInConditionalOperator", ts.SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext, isConditionalOperatorContext], ts.formatting.RuleAction.InsertSpace), - - // in other cases there should be no space between '?' and next token - rule("NoSpaceAfterQuestionMark", ts.SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeDot", anyToken, [ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterDot", [ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBetweenImportParenInImportType", ts.SyntaxKind.ImportKeyword, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isImportTypeContext], ts.formatting.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], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterUnaryPreincrementOperator", ts.SyntaxKind.PlusPlusToken, unaryPreincrementExpressions, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterUnaryPredecrementOperator", ts.SyntaxKind.MinusMinusToken, unaryPredecrementExpressions, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeUnaryPostincrementOperator", unaryPostincrementExpressions, ts.SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeUnaryPostdecrementOperator", unaryPostdecrementExpressions, ts.SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], ts.formatting.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", ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterAddWhenFollowedByUnaryPlus", ts.SyntaxKind.PlusToken, ts.SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterAddWhenFollowedByPreincrement", ts.SyntaxKind.PlusToken, ts.SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterPostdecrementWhenFollowedBySubtract", ts.SyntaxKind.MinusMinusToken, ts.SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterSubtractWhenFollowedByUnaryMinus", ts.SyntaxKind.MinusToken, ts.SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterSubtractWhenFollowedByPredecrement", ts.SyntaxKind.MinusToken, ts.SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterCloseBrace", ts.SyntaxKind.CloseBraceToken, [ts.SyntaxKind.CommaToken, ts.SyntaxKind.SemicolonToken], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - // For functions and control block place } on a new line [multi-line rule] - rule("NewLineBeforeCloseBraceInBlockContext", anyTokenIncludingMultilineComments, ts.SyntaxKind.CloseBraceToken, [isMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine), - - // Space/new line after }. - rule("SpaceAfterCloseBrace", ts.SyntaxKind.CloseBraceToken, anyTokenExcept(ts.SyntaxKind.CloseParenToken), [isNonJsxSameLineTokenContext, isAfterCodeBlockContext], ts.formatting.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", ts.SyntaxKind.CloseBraceToken, ts.SyntaxKind.ElseKeyword, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBetweenCloseBraceAndWhile", ts.SyntaxKind.CloseBraceToken, ts.SyntaxKind.WhileKeyword, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], ts.formatting.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", ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.OpenBracketToken, [isControlDeclContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenFunctionKeywordAndStar", ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken, [isFunctionDeclarationOrFunctionExpressionContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceAfterStarInGeneratorDeclaration", ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.Identifier, [isFunctionDeclarationOrFunctionExpressionContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterFunctionInFuncDecl", ts.SyntaxKind.FunctionKeyword, anyToken, [isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), - // Insert new line after { and before } in multi-line contexts. - rule("NewLineAfterOpenBraceInBlockContext", ts.SyntaxKind.OpenBraceToken, anyToken, [isMultilineBlockContext], ts.formatting.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", [ts.SyntaxKind.GetKeyword, ts.SyntaxKind.SetKeyword], ts.SyntaxKind.Identifier, [isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenYieldKeywordAndStar", ts.SyntaxKind.YieldKeyword, ts.SyntaxKind.AsteriskToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceBetweenYieldOrYieldStarAndOperand", [ts.SyntaxKind.YieldKeyword, ts.SyntaxKind.AsteriskToken], anyToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenReturnAndSemicolon", ts.SyntaxKind.ReturnKeyword, ts.SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceAfterCertainKeywords", [ts.SyntaxKind.VarKeyword, ts.SyntaxKind.ThrowKeyword, ts.SyntaxKind.NewKeyword, ts.SyntaxKind.DeleteKeyword, ts.SyntaxKind.ReturnKeyword, ts.SyntaxKind.TypeOfKeyword, ts.SyntaxKind.AwaitKeyword], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterLetConstInVariableDeclaration", [ts.SyntaxKind.LetKeyword, ts.SyntaxKind.ConstKeyword], anyToken, [isNonJsxSameLineTokenContext, isStartOfVariableDeclarationList], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeOpenParenInFuncCall", anyToken, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isFunctionCallOrNewContext, isPreviousTokenNotComma], ts.formatting.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], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterBinaryKeywordOperator", binaryKeywordOperators, anyToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterVoidOperator", ts.SyntaxKind.VoidKeyword, anyToken, [isNonJsxSameLineTokenContext, isVoidOpContext], ts.formatting.RuleAction.InsertSpace), - - // Async-await - rule("SpaceBetweenAsyncAndOpenParen", ts.SyntaxKind.AsyncKeyword, ts.SyntaxKind.OpenParenToken, [isArrowFunctionContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBetweenAsyncAndFunctionKeyword", ts.SyntaxKind.AsyncKeyword, [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.Identifier], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - - // Template string - rule("NoSpaceBetweenTagAndTemplateString", [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken], [ts.SyntaxKind.NoSubstitutionTemplateLiteral, ts.SyntaxKind.TemplateHead], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // JSX opening elements - rule("SpaceBeforeJsxAttribute", anyToken, ts.SyntaxKind.Identifier, [isNextTokenParentJsxAttribute, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeSlashInJsxOpeningElement", anyToken, ts.SyntaxKind.SlashToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeGreaterThanTokenInJsxOpeningElement", ts.SyntaxKind.SlashToken, ts.SyntaxKind.GreaterThanToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeEqualInJsxAttribute", anyToken, ts.SyntaxKind.EqualsToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterEqualInJsxAttribute", ts.SyntaxKind.EqualsToken, anyToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // TypeScript-specific rules - // Use of module as a function call. e.g.: import m2 = module("m2"); - rule("NoSpaceAfterModuleImport", [ts.SyntaxKind.ModuleKeyword, ts.SyntaxKind.RequireKeyword], ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - // Add a space around certain TypeScript keywords - rule("SpaceAfterCertainTypeScriptKeywords", [ - ts.SyntaxKind.AbstractKeyword, - ts.SyntaxKind.ClassKeyword, - ts.SyntaxKind.DeclareKeyword, - ts.SyntaxKind.DefaultKeyword, - ts.SyntaxKind.EnumKeyword, - ts.SyntaxKind.ExportKeyword, - ts.SyntaxKind.ExtendsKeyword, - ts.SyntaxKind.GetKeyword, - ts.SyntaxKind.ImplementsKeyword, - ts.SyntaxKind.ImportKeyword, - ts.SyntaxKind.InterfaceKeyword, - ts.SyntaxKind.ModuleKeyword, - ts.SyntaxKind.NamespaceKeyword, - ts.SyntaxKind.PrivateKeyword, - ts.SyntaxKind.PublicKeyword, - ts.SyntaxKind.ProtectedKeyword, - ts.SyntaxKind.ReadonlyKeyword, - ts.SyntaxKind.SetKeyword, - ts.SyntaxKind.StaticKeyword, - ts.SyntaxKind.TypeKeyword, - ts.SyntaxKind.FromKeyword, - ts.SyntaxKind.KeyOfKeyword, - ts.SyntaxKind.InferKeyword, - ], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeCertainTypeScriptKeywords", anyToken, [ts.SyntaxKind.ExtendsKeyword, ts.SyntaxKind.ImplementsKeyword, ts.SyntaxKind.FromKeyword], [isNonJsxSameLineTokenContext], ts.formatting.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", ts.SyntaxKind.StringLiteral, ts.SyntaxKind.OpenBraceToken, [isModuleDeclContext], ts.formatting.RuleAction.InsertSpace), - - // Lambda expressions - rule("SpaceBeforeArrow", anyToken, ts.SyntaxKind.EqualsGreaterThanToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterArrow", ts.SyntaxKind.EqualsGreaterThanToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - - // Optional parameters and let args - rule("NoSpaceAfterEllipsis", ts.SyntaxKind.DotDotDotToken, ts.SyntaxKind.Identifier, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterOptionalParameters", ts.SyntaxKind.QuestionToken, [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isNotBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), - - // Remove spaces in empty interface literals. e.g.: x: {} - rule("NoSpaceBetweenEmptyInterfaceBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectTypeContext], ts.formatting.RuleAction.DeleteSpace), - - // generics and type assertions - rule("NoSpaceBeforeOpenAngularBracket", typeNames, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBetweenCloseParenAndAngularBracket", ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenAngularBracket", ts.SyntaxKind.LessThanToken, anyToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseAngularBracket", anyToken, ts.SyntaxKind.GreaterThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterCloseAngularBracket", ts.SyntaxKind.GreaterThanToken, [ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.GreaterThanToken, ts.SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext, isNotFunctionDeclContext /*To prevent an interference with the SpaceBeforeOpenParenInFuncDecl rule*/], ts.formatting.RuleAction.DeleteSpace), - - // decorators - rule("SpaceBeforeAt", [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.Identifier], ts.SyntaxKind.AtToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterAt", ts.SyntaxKind.AtToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - // Insert space after @ in decorator - rule("SpaceAfterDecorator", anyToken, [ - ts.SyntaxKind.AbstractKeyword, - ts.SyntaxKind.Identifier, - ts.SyntaxKind.ExportKeyword, - ts.SyntaxKind.DefaultKeyword, - ts.SyntaxKind.ClassKeyword, - ts.SyntaxKind.StaticKeyword, - ts.SyntaxKind.PublicKeyword, - ts.SyntaxKind.PrivateKeyword, - ts.SyntaxKind.ProtectedKeyword, - ts.SyntaxKind.GetKeyword, - ts.SyntaxKind.SetKeyword, - ts.SyntaxKind.OpenBracketToken, - ts.SyntaxKind.AsteriskToken, - ], [isEndOfDecoratorContextOnSameLine], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeNonNullAssertionOperator", anyToken, ts.SyntaxKind.ExclamationToken, [isNonJsxSameLineTokenContext, isNonNullAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterNewKeywordOnConstructorSignature", ts.SyntaxKind.NewKeyword, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isConstructorSignatureContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceLessThanAndNonJSXTypeAnnotation", ts.SyntaxKind.LessThanToken, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext], ts.formatting.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", ts.SyntaxKind.ConstructorKeyword, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterConstructor", ts.SyntaxKind.ConstructorKeyword, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceAfterComma", ts.SyntaxKind.CommaToken, anyToken, [isOptionEnabled("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNextTokenNotCloseBracket, isNextTokenNotCloseParen], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterComma", ts.SyntaxKind.CommaToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after function keyword for anonymous functions - rule("SpaceAfterAnonymousFunctionKeyword", [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken], ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterAnonymousFunctionKeyword", [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken], ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after keywords in control flow statements - rule("SpaceAfterKeywordInControl", keywords, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterKeywordInControl", keywords, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after opening and before closing nonempty parenthesis - rule("SpaceAfterOpenParen", ts.SyntaxKind.OpenParenToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeCloseParen", anyToken, ts.SyntaxKind.CloseParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBetweenOpenParens", ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenParens", ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenParen", ts.SyntaxKind.OpenParenToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseParen", anyToken, ts.SyntaxKind.CloseParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after opening and before closing nonempty brackets - rule("SpaceAfterOpenBracket", ts.SyntaxKind.OpenBracketToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeCloseBracket", anyToken, ts.SyntaxKind.CloseBracketToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenBrackets", ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenBracket", ts.SyntaxKind.OpenBracketToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBracket", anyToken, ts.SyntaxKind.CloseBracketToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. - rule("SpaceAfterOpenBrace", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeCloseBrace", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenBrace", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBrace", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert a space after opening and before closing empty brace brackets - rule("SpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingEmptyBraces")], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after opening and before closing template string braces - rule("SpaceAfterTemplateHeadAndMiddle", [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle], anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxTextContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("SpaceBeforeTemplateMiddleAndTail", anyToken, [ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail], [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterTemplateHeadAndMiddle", [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle], anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxTextContext], ts.formatting.RuleAction.DeleteSpace, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("NoSpaceBeforeTemplateMiddleAndTail", anyToken, [ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail], [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // No space after { and before } in JSX expression - rule("SpaceAfterOpenBraceInJsxExpression", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceBeforeCloseBraceInJsxExpression", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterOpenBraceInJsxExpression", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBraceInJsxExpression", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space after semicolon in for statement - rule("SpaceAfterSemicolonInFor", ts.SyntaxKind.SemicolonToken, anyToken, [isOptionEnabled("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterSemicolonInFor", ts.SyntaxKind.SemicolonToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], ts.formatting.RuleAction.DeleteSpace), - - // Insert space before and after binary operators - rule("SpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("SpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceBeforeOpenParenInFuncDecl", anyToken, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeOpenParenInFuncDecl", anyToken, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], ts.formatting.RuleAction.DeleteSpace), - - // Open Brace braces after control block - rule("NewLineBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.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, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.RuleFlags.CanDeleteNewLines), - // Open Brace braces after TypeScript module/class/interface - rule("NewLineBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("SpaceAfterTypeAssertion", ts.SyntaxKind.GreaterThanToken, anyToken, [isOptionEnabled("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceAfterTypeAssertion", ts.SyntaxKind.GreaterThanToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceBeforeTypeAnnotation", anyToken, [ts.SyntaxKind.QuestionToken, ts.SyntaxKind.ColonToken], [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], ts.formatting.RuleAction.InsertSpace), - rule("NoSpaceBeforeTypeAnnotation", anyToken, [ts.SyntaxKind.QuestionToken, ts.SyntaxKind.ColonToken], [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoOptionalSemicolon", ts.SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [optionEquals("semicolons", ts.SemicolonPreference.Remove), isSemicolonDeletionContext], ts.formatting.RuleAction.DeleteToken), - rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [optionEquals("semicolons", ts.SemicolonPreference.Insert), isSemicolonInsertionContext], ts.formatting.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, ts.SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("SpaceBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("SpaceBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), - rule("NoSpaceBeforeComma", anyToken, ts.SyntaxKind.CommaToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - - // No space before and after indexer `x[]` - rule("NoSpaceBeforeOpenBracket", anyTokenExcept(ts.SyntaxKind.AsyncKeyword, ts.SyntaxKind.CaseKeyword), ts.SyntaxKind.OpenBracketToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), - rule("NoSpaceAfterCloseBracket", ts.SyntaxKind.CloseBracketToken, anyToken, [isNonJsxSameLineTokenContext, isNotBeforeBlockInFunctionDeclarationContext], ts.formatting.RuleAction.DeleteSpace), - rule("SpaceAfterSemicolon", ts.SyntaxKind.SemicolonToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - - // Remove extra space between for and await - rule("SpaceBetweenForAndAwaitKeyword", ts.SyntaxKind.ForKeyword, ts.SyntaxKind.AwaitKeyword, [isNonJsxSameLineTokenContext], ts.formatting.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", [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.DoKeyword, ts.SyntaxKind.ElseKeyword, ts.SyntaxKind.CaseKeyword], anyToken, [isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNotForContext], ts.formatting.RuleAction.InsertSpace), - // This low-pri rule takes care of "try {", "catch {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. - rule("SpaceAfterTryCatchFinally", [ts.SyntaxKind.TryKeyword, ts.SyntaxKind.CatchKeyword, ts.SyntaxKind.FinallyKeyword], ts.SyntaxKind.OpenBraceToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), - ]; - - return [ - ...highPriorityCommonRules, - ...userConfigurableRules, - ...lowPriorityCommonRules, - ]; } + function anyTokenExcept(...tokens: ts.SyntaxKind[]): ts.formatting.TokenRange { + return { tokens: allTokens.filter(t => !tokens.some(t2 => t2 === t)), isSpecific: false }; + } + + const anyToken: ts.formatting.TokenRange = { tokens: allTokens, isSpecific: false }; + const anyTokenIncludingMultilineComments = tokenRangeFrom([...allTokens, ts.SyntaxKind.MultiLineCommentTrivia]); + const anyTokenIncludingEOF = tokenRangeFrom([...allTokens, ts.SyntaxKind.EndOfFileToken]); + const keywords = tokenRangeFromRange(ts.SyntaxKind.FirstKeyword, ts.SyntaxKind.LastKeyword); + const binaryOperators = tokenRangeFromRange(ts.SyntaxKind.FirstBinaryOperator, ts.SyntaxKind.LastBinaryOperator); + const binaryKeywordOperators = [ts.SyntaxKind.InKeyword, ts.SyntaxKind.InstanceOfKeyword, ts.SyntaxKind.OfKeyword, ts.SyntaxKind.AsKeyword, ts.SyntaxKind.IsKeyword]; + const unaryPrefixOperators = [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken, ts.SyntaxKind.TildeToken, ts.SyntaxKind.ExclamationToken]; + const unaryPrefixExpressions = [ + ts.SyntaxKind.NumericLiteral, ts.SyntaxKind.BigIntLiteral, ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, + ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword + ]; + const unaryPreincrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword]; + const unaryPostincrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CloseBracketToken, ts.SyntaxKind.NewKeyword]; + const unaryPredecrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.ThisKeyword, ts.SyntaxKind.NewKeyword]; + const unaryPostdecrementExpressions = [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CloseBracketToken, ts.SyntaxKind.NewKeyword]; + const comments = [ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia]; + const typeNames = [ts.SyntaxKind.Identifier, ...ts.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([ts.SyntaxKind.Identifier, ts.SyntaxKind.MultiLineCommentTrivia, ts.SyntaxKind.ClassKeyword, ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.ImportKeyword]); + + // Place a space before open brace in a control flow construct + const controlOpenBraceLeftTokenRange = tokenRangeFrom([ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.MultiLineCommentTrivia, ts.SyntaxKind.DoKeyword, ts.SyntaxKind.TryKeyword, ts.SyntaxKind.FinallyKeyword, ts.SyntaxKind.ElseKeyword]); + + // These rules are higher in priority than user-configurable + const highPriorityCommonRules = [ + // Leave comments alone + rule("IgnoreBeforeComment", anyToken, comments, ts.formatting.anyContext, ts.formatting.RuleAction.StopProcessingSpaceActions), + rule("IgnoreAfterLineComment", ts.SyntaxKind.SingleLineCommentTrivia, anyToken, ts.formatting.anyContext, ts.formatting.RuleAction.StopProcessingSpaceActions), + rule("NotSpaceBeforeColon", anyToken, ts.SyntaxKind.ColonToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceAfterColon", ts.SyntaxKind.ColonToken, anyToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeQuestionMark", anyToken, ts.SyntaxKind.QuestionToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), + // insert space after '?' only when it is used in conditional operator + rule("SpaceAfterQuestionMarkInConditionalOperator", ts.SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext, isConditionalOperatorContext], ts.formatting.RuleAction.InsertSpace), + + // in other cases there should be no space between '?' and next token + rule("NoSpaceAfterQuestionMark", ts.SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeDot", anyToken, [ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterDot", [ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBetweenImportParenInImportType", ts.SyntaxKind.ImportKeyword, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isImportTypeContext], ts.formatting.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], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterUnaryPreincrementOperator", ts.SyntaxKind.PlusPlusToken, unaryPreincrementExpressions, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterUnaryPredecrementOperator", ts.SyntaxKind.MinusMinusToken, unaryPredecrementExpressions, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeUnaryPostincrementOperator", unaryPostincrementExpressions, ts.SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeUnaryPostdecrementOperator", unaryPostdecrementExpressions, ts.SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], ts.formatting.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", ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterAddWhenFollowedByUnaryPlus", ts.SyntaxKind.PlusToken, ts.SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterAddWhenFollowedByPreincrement", ts.SyntaxKind.PlusToken, ts.SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterPostdecrementWhenFollowedBySubtract", ts.SyntaxKind.MinusMinusToken, ts.SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterSubtractWhenFollowedByUnaryMinus", ts.SyntaxKind.MinusToken, ts.SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterSubtractWhenFollowedByPredecrement", ts.SyntaxKind.MinusToken, ts.SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterCloseBrace", ts.SyntaxKind.CloseBraceToken, [ts.SyntaxKind.CommaToken, ts.SyntaxKind.SemicolonToken], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + // For functions and control block place } on a new line [multi-line rule] + rule("NewLineBeforeCloseBraceInBlockContext", anyTokenIncludingMultilineComments, ts.SyntaxKind.CloseBraceToken, [isMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine), + + // Space/new line after }. + rule("SpaceAfterCloseBrace", ts.SyntaxKind.CloseBraceToken, anyTokenExcept(ts.SyntaxKind.CloseParenToken), [isNonJsxSameLineTokenContext, isAfterCodeBlockContext], ts.formatting.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", ts.SyntaxKind.CloseBraceToken, ts.SyntaxKind.ElseKeyword, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBetweenCloseBraceAndWhile", ts.SyntaxKind.CloseBraceToken, ts.SyntaxKind.WhileKeyword, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], ts.formatting.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", ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.OpenBracketToken, [isControlDeclContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenFunctionKeywordAndStar", ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken, [isFunctionDeclarationOrFunctionExpressionContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceAfterStarInGeneratorDeclaration", ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.Identifier, [isFunctionDeclarationOrFunctionExpressionContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterFunctionInFuncDecl", ts.SyntaxKind.FunctionKeyword, anyToken, [isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), + // Insert new line after { and before } in multi-line contexts. + rule("NewLineAfterOpenBraceInBlockContext", ts.SyntaxKind.OpenBraceToken, anyToken, [isMultilineBlockContext], ts.formatting.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", [ts.SyntaxKind.GetKeyword, ts.SyntaxKind.SetKeyword], ts.SyntaxKind.Identifier, [isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenYieldKeywordAndStar", ts.SyntaxKind.YieldKeyword, ts.SyntaxKind.AsteriskToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceBetweenYieldOrYieldStarAndOperand", [ts.SyntaxKind.YieldKeyword, ts.SyntaxKind.AsteriskToken], anyToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenReturnAndSemicolon", ts.SyntaxKind.ReturnKeyword, ts.SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceAfterCertainKeywords", [ts.SyntaxKind.VarKeyword, ts.SyntaxKind.ThrowKeyword, ts.SyntaxKind.NewKeyword, ts.SyntaxKind.DeleteKeyword, ts.SyntaxKind.ReturnKeyword, ts.SyntaxKind.TypeOfKeyword, ts.SyntaxKind.AwaitKeyword], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterLetConstInVariableDeclaration", [ts.SyntaxKind.LetKeyword, ts.SyntaxKind.ConstKeyword], anyToken, [isNonJsxSameLineTokenContext, isStartOfVariableDeclarationList], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeOpenParenInFuncCall", anyToken, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isFunctionCallOrNewContext, isPreviousTokenNotComma], ts.formatting.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], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterBinaryKeywordOperator", binaryKeywordOperators, anyToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterVoidOperator", ts.SyntaxKind.VoidKeyword, anyToken, [isNonJsxSameLineTokenContext, isVoidOpContext], ts.formatting.RuleAction.InsertSpace), + + // Async-await + rule("SpaceBetweenAsyncAndOpenParen", ts.SyntaxKind.AsyncKeyword, ts.SyntaxKind.OpenParenToken, [isArrowFunctionContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBetweenAsyncAndFunctionKeyword", ts.SyntaxKind.AsyncKeyword, [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.Identifier], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + + // Template string + rule("NoSpaceBetweenTagAndTemplateString", [ts.SyntaxKind.Identifier, ts.SyntaxKind.CloseParenToken], [ts.SyntaxKind.NoSubstitutionTemplateLiteral, ts.SyntaxKind.TemplateHead], [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // JSX opening elements + rule("SpaceBeforeJsxAttribute", anyToken, ts.SyntaxKind.Identifier, [isNextTokenParentJsxAttribute, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeSlashInJsxOpeningElement", anyToken, ts.SyntaxKind.SlashToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeGreaterThanTokenInJsxOpeningElement", ts.SyntaxKind.SlashToken, ts.SyntaxKind.GreaterThanToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeEqualInJsxAttribute", anyToken, ts.SyntaxKind.EqualsToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterEqualInJsxAttribute", ts.SyntaxKind.EqualsToken, anyToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // TypeScript-specific rules + // Use of module as a function call. e.g.: import m2 = module("m2"); + rule("NoSpaceAfterModuleImport", [ts.SyntaxKind.ModuleKeyword, ts.SyntaxKind.RequireKeyword], ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + // Add a space around certain TypeScript keywords + rule("SpaceAfterCertainTypeScriptKeywords", [ + ts.SyntaxKind.AbstractKeyword, + ts.SyntaxKind.ClassKeyword, + ts.SyntaxKind.DeclareKeyword, + ts.SyntaxKind.DefaultKeyword, + ts.SyntaxKind.EnumKeyword, + ts.SyntaxKind.ExportKeyword, + ts.SyntaxKind.ExtendsKeyword, + ts.SyntaxKind.GetKeyword, + ts.SyntaxKind.ImplementsKeyword, + ts.SyntaxKind.ImportKeyword, + ts.SyntaxKind.InterfaceKeyword, + ts.SyntaxKind.ModuleKeyword, + ts.SyntaxKind.NamespaceKeyword, + ts.SyntaxKind.PrivateKeyword, + ts.SyntaxKind.PublicKeyword, + ts.SyntaxKind.ProtectedKeyword, + ts.SyntaxKind.ReadonlyKeyword, + ts.SyntaxKind.SetKeyword, + ts.SyntaxKind.StaticKeyword, + ts.SyntaxKind.TypeKeyword, + ts.SyntaxKind.FromKeyword, + ts.SyntaxKind.KeyOfKeyword, + ts.SyntaxKind.InferKeyword, + ], anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeCertainTypeScriptKeywords", anyToken, [ts.SyntaxKind.ExtendsKeyword, ts.SyntaxKind.ImplementsKeyword, ts.SyntaxKind.FromKeyword], [isNonJsxSameLineTokenContext], ts.formatting.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", ts.SyntaxKind.StringLiteral, ts.SyntaxKind.OpenBraceToken, [isModuleDeclContext], ts.formatting.RuleAction.InsertSpace), + + // Lambda expressions + rule("SpaceBeforeArrow", anyToken, ts.SyntaxKind.EqualsGreaterThanToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterArrow", ts.SyntaxKind.EqualsGreaterThanToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + + // Optional parameters and let args + rule("NoSpaceAfterEllipsis", ts.SyntaxKind.DotDotDotToken, ts.SyntaxKind.Identifier, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterOptionalParameters", ts.SyntaxKind.QuestionToken, [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isNotBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), + + // Remove spaces in empty interface literals. e.g.: x: {} + rule("NoSpaceBetweenEmptyInterfaceBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectTypeContext], ts.formatting.RuleAction.DeleteSpace), + + // generics and type assertions + rule("NoSpaceBeforeOpenAngularBracket", typeNames, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBetweenCloseParenAndAngularBracket", ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenAngularBracket", ts.SyntaxKind.LessThanToken, anyToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseAngularBracket", anyToken, ts.SyntaxKind.GreaterThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterCloseAngularBracket", ts.SyntaxKind.GreaterThanToken, [ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.GreaterThanToken, ts.SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext, isNotFunctionDeclContext /*To prevent an interference with the SpaceBeforeOpenParenInFuncDecl rule*/], ts.formatting.RuleAction.DeleteSpace), + + // decorators + rule("SpaceBeforeAt", [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.Identifier], ts.SyntaxKind.AtToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterAt", ts.SyntaxKind.AtToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + // Insert space after @ in decorator + rule("SpaceAfterDecorator", anyToken, [ + ts.SyntaxKind.AbstractKeyword, + ts.SyntaxKind.Identifier, + ts.SyntaxKind.ExportKeyword, + ts.SyntaxKind.DefaultKeyword, + ts.SyntaxKind.ClassKeyword, + ts.SyntaxKind.StaticKeyword, + ts.SyntaxKind.PublicKeyword, + ts.SyntaxKind.PrivateKeyword, + ts.SyntaxKind.ProtectedKeyword, + ts.SyntaxKind.GetKeyword, + ts.SyntaxKind.SetKeyword, + ts.SyntaxKind.OpenBracketToken, + ts.SyntaxKind.AsteriskToken, + ], [isEndOfDecoratorContextOnSameLine], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeNonNullAssertionOperator", anyToken, ts.SyntaxKind.ExclamationToken, [isNonJsxSameLineTokenContext, isNonNullAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterNewKeywordOnConstructorSignature", ts.SyntaxKind.NewKeyword, ts.SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isConstructorSignatureContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceLessThanAndNonJSXTypeAnnotation", ts.SyntaxKind.LessThanToken, ts.SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext], ts.formatting.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", ts.SyntaxKind.ConstructorKeyword, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterConstructor", ts.SyntaxKind.ConstructorKeyword, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceAfterComma", ts.SyntaxKind.CommaToken, anyToken, [isOptionEnabled("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNextTokenNotCloseBracket, isNextTokenNotCloseParen], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterComma", ts.SyntaxKind.CommaToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after function keyword for anonymous functions + rule("SpaceAfterAnonymousFunctionKeyword", [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken], ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterAnonymousFunctionKeyword", [ts.SyntaxKind.FunctionKeyword, ts.SyntaxKind.AsteriskToken], ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after keywords in control flow statements + rule("SpaceAfterKeywordInControl", keywords, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterKeywordInControl", keywords, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after opening and before closing nonempty parenthesis + rule("SpaceAfterOpenParen", ts.SyntaxKind.OpenParenToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeCloseParen", anyToken, ts.SyntaxKind.CloseParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBetweenOpenParens", ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenParens", ts.SyntaxKind.OpenParenToken, ts.SyntaxKind.CloseParenToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenParen", ts.SyntaxKind.OpenParenToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseParen", anyToken, ts.SyntaxKind.CloseParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after opening and before closing nonempty brackets + rule("SpaceAfterOpenBracket", ts.SyntaxKind.OpenBracketToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeCloseBracket", anyToken, ts.SyntaxKind.CloseBracketToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenBrackets", ts.SyntaxKind.OpenBracketToken, ts.SyntaxKind.CloseBracketToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenBracket", ts.SyntaxKind.OpenBracketToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBracket", anyToken, ts.SyntaxKind.CloseBracketToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. + rule("SpaceAfterOpenBrace", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeCloseBrace", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenBrace", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBrace", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert a space after opening and before closing empty brace brackets + rule("SpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingEmptyBraces")], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBetweenEmptyBraceBrackets", ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after opening and before closing template string braces + rule("SpaceAfterTemplateHeadAndMiddle", [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle], anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxTextContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("SpaceBeforeTemplateMiddleAndTail", anyToken, [ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail], [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterTemplateHeadAndMiddle", [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle], anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxTextContext], ts.formatting.RuleAction.DeleteSpace, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("NoSpaceBeforeTemplateMiddleAndTail", anyToken, [ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail], [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // No space after { and before } in JSX expression + rule("SpaceAfterOpenBraceInJsxExpression", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceBeforeCloseBraceInJsxExpression", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterOpenBraceInJsxExpression", ts.SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBraceInJsxExpression", anyToken, ts.SyntaxKind.CloseBraceToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space after semicolon in for statement + rule("SpaceAfterSemicolonInFor", ts.SyntaxKind.SemicolonToken, anyToken, [isOptionEnabled("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterSemicolonInFor", ts.SyntaxKind.SemicolonToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], ts.formatting.RuleAction.DeleteSpace), + + // Insert space before and after binary operators + rule("SpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("SpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceBeforeOpenParenInFuncDecl", anyToken, ts.SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeOpenParenInFuncDecl", anyToken, ts.SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], ts.formatting.RuleAction.DeleteSpace), + + // Open Brace braces after control block + rule("NewLineBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.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, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.RuleFlags.CanDeleteNewLines), + // Open Brace braces after TypeScript module/class/interface + rule("NewLineBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isBeforeMultilineBlockContext], ts.formatting.RuleAction.InsertNewLine, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("SpaceAfterTypeAssertion", ts.SyntaxKind.GreaterThanToken, anyToken, [isOptionEnabled("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceAfterTypeAssertion", ts.SyntaxKind.GreaterThanToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceBeforeTypeAnnotation", anyToken, [ts.SyntaxKind.QuestionToken, ts.SyntaxKind.ColonToken], [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], ts.formatting.RuleAction.InsertSpace), + rule("NoSpaceBeforeTypeAnnotation", anyToken, [ts.SyntaxKind.QuestionToken, ts.SyntaxKind.ColonToken], [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoOptionalSemicolon", ts.SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [optionEquals("semicolons", ts.SemicolonPreference.Remove), isSemicolonDeletionContext], ts.formatting.RuleAction.DeleteToken), + rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [optionEquals("semicolons", ts.SemicolonPreference.Insert), isSemicolonInsertionContext], ts.formatting.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, ts.SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("SpaceBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("SpaceBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ts.SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], ts.formatting.RuleAction.InsertSpace, ts.formatting.RuleFlags.CanDeleteNewLines), + rule("NoSpaceBeforeComma", anyToken, ts.SyntaxKind.CommaToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + + // No space before and after indexer `x[]` + rule("NoSpaceBeforeOpenBracket", anyTokenExcept(ts.SyntaxKind.AsyncKeyword, ts.SyntaxKind.CaseKeyword), ts.SyntaxKind.OpenBracketToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.DeleteSpace), + rule("NoSpaceAfterCloseBracket", ts.SyntaxKind.CloseBracketToken, anyToken, [isNonJsxSameLineTokenContext, isNotBeforeBlockInFunctionDeclarationContext], ts.formatting.RuleAction.DeleteSpace), + rule("SpaceAfterSemicolon", ts.SyntaxKind.SemicolonToken, anyToken, [isNonJsxSameLineTokenContext], ts.formatting.RuleAction.InsertSpace), + + // Remove extra space between for and await + rule("SpaceBetweenForAndAwaitKeyword", ts.SyntaxKind.ForKeyword, ts.SyntaxKind.AwaitKeyword, [isNonJsxSameLineTokenContext], ts.formatting.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", [ts.SyntaxKind.CloseParenToken, ts.SyntaxKind.DoKeyword, ts.SyntaxKind.ElseKeyword, ts.SyntaxKind.CaseKeyword], anyToken, [isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNotForContext], ts.formatting.RuleAction.InsertSpace), + // This low-pri rule takes care of "try {", "catch {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. + rule("SpaceAfterTryCatchFinally", [ts.SyntaxKind.TryKeyword, ts.SyntaxKind.CatchKeyword, ts.SyntaxKind.FinallyKeyword], ts.SyntaxKind.OpenBraceToken, [isNonJsxSameLineTokenContext], ts.formatting.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: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange, right: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange, context: readonly ts.formatting.ContextPredicate[], action: ts.formatting.RuleAction, flags: ts.formatting.RuleFlags = ts.formatting.RuleFlags.None): RuleSpec { - return { leftTokenRange: toTokenRange(left), rightTokenRange: toTokenRange(right), rule: { debugName, context, action, flags } }; - } +/** + * 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: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange, right: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange, context: readonly ts.formatting.ContextPredicate[], action: ts.formatting.RuleAction, flags: ts.formatting.RuleFlags = ts.formatting.RuleFlags.None): RuleSpec { + return { leftTokenRange: toTokenRange(left), rightTokenRange: toTokenRange(right), rule: { debugName, context, action, flags } }; +} - function tokenRangeFrom(tokens: readonly ts.SyntaxKind[]): ts.formatting.TokenRange { - return { tokens, isSpecific: true }; - } +function tokenRangeFrom(tokens: readonly ts.SyntaxKind[]): ts.formatting.TokenRange { + return { tokens, isSpecific: true }; +} - function toTokenRange(arg: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange): ts.formatting.TokenRange { - return typeof arg === "number" ? tokenRangeFrom([arg]) : ts.isArray(arg) ? tokenRangeFrom(arg) : arg; - } +function toTokenRange(arg: ts.SyntaxKind | readonly ts.SyntaxKind[] | ts.formatting.TokenRange): ts.formatting.TokenRange { + return typeof arg === "number" ? tokenRangeFrom([arg]) : ts.isArray(arg) ? tokenRangeFrom(arg) : arg; +} - function tokenRangeFromRange(from: ts.SyntaxKind, to: ts.SyntaxKind, except: readonly ts.SyntaxKind[] = []): ts.formatting.TokenRange { - const tokens: ts.SyntaxKind[] = []; - for (let token = from; token <= to; token++) { - if (!ts.contains(except, token)) { - tokens.push(token); - } +function tokenRangeFromRange(from: ts.SyntaxKind, to: ts.SyntaxKind, except: readonly ts.SyntaxKind[] = []): ts.formatting.TokenRange { + const tokens: ts.SyntaxKind[] = []; + for (let token = from; token <= to; token++) { + if (!ts.contains(except, token)) { + tokens.push(token); } - return tokenRangeFrom(tokens); } + return tokenRangeFrom(tokens); +} - /// - /// Contexts - /// +/// +/// Contexts +/// - function optionEquals(optionName: K, optionValue: ts.FormatCodeSettings[K]): (context: ts.formatting.FormattingContext) => boolean { - return (context) => context.options && context.options[optionName] === optionValue; - } +function optionEquals(optionName: K, optionValue: ts.FormatCodeSettings[K]): (context: ts.formatting.FormattingContext) => boolean { + return (context) => context.options && context.options[optionName] === optionValue; +} - function isOptionEnabled(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { - return (context) => context.options && context.options.hasOwnProperty(optionName) && !!context.options[optionName]; - } +function isOptionEnabled(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { + return (context) => context.options && context.options.hasOwnProperty(optionName) && !!context.options[optionName]; +} - function isOptionDisabled(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { - return (context) => context.options && context.options.hasOwnProperty(optionName) && !context.options[optionName]; - } +function isOptionDisabled(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { + return (context) => context.options && context.options.hasOwnProperty(optionName) && !context.options[optionName]; +} - function isOptionDisabledOrUndefined(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName]; - } +function isOptionDisabledOrUndefined(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName]; +} - function isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine(); - } +function isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine(); +} - function isOptionEnabledOrUndefined(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName]; - } +function isOptionEnabledOrUndefined(optionName: keyof ts.FormatCodeSettings): (context: ts.formatting.FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName]; +} - function isForContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ForStatement; - } +function isForContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ForStatement; +} - function isNotForContext(context: ts.formatting.FormattingContext): boolean { - return !isForContext(context); - } +function isNotForContext(context: ts.formatting.FormattingContext): boolean { + return !isForContext(context); +} - function isBinaryOpContext(context: ts.formatting.FormattingContext): boolean { - switch (context.contextNode.kind) { - case ts.SyntaxKind.BinaryExpression: - return (context.contextNode as ts.BinaryExpression).operatorToken.kind !== ts.SyntaxKind.CommaToken; - case ts.SyntaxKind.ConditionalExpression: - case ts.SyntaxKind.ConditionalType: - case ts.SyntaxKind.AsExpression: - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.TypePredicate: - case ts.SyntaxKind.UnionType: - case ts.SyntaxKind.IntersectionType: - return true; +function isBinaryOpContext(context: ts.formatting.FormattingContext): boolean { + switch (context.contextNode.kind) { + case ts.SyntaxKind.BinaryExpression: + return (context.contextNode as ts.BinaryExpression).operatorToken.kind !== ts.SyntaxKind.CommaToken; + case ts.SyntaxKind.ConditionalExpression: + case ts.SyntaxKind.ConditionalType: + case ts.SyntaxKind.AsExpression: + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.TypePredicate: + case ts.SyntaxKind.UnionType: + case ts.SyntaxKind.IntersectionType: + return true; - // equals in binding elements: function foo([[x, y] = [1, 2]]) - case ts.SyntaxKind.BindingElement: - // equals in type X = ... - // falls through - case ts.SyntaxKind.TypeAliasDeclaration: - // equal in import a = module('a'); - // falls through - case ts.SyntaxKind.ImportEqualsDeclaration: - // equal in export = 1 - // falls through - case ts.SyntaxKind.ExportAssignment: - // equal in let a = 0 - // falls through - case ts.SyntaxKind.VariableDeclaration: - // equal in p = 0 - // falls through - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - return context.currentTokenSpan.kind === ts.SyntaxKind.EqualsToken || context.nextTokenSpan.kind === ts.SyntaxKind.EqualsToken; - // "in" keyword in for (let x in []) { } - case ts.SyntaxKind.ForInStatement: - // "in" keyword in [P in keyof T]: T[P] - // falls through - case ts.SyntaxKind.TypeParameter: - return context.currentTokenSpan.kind === ts.SyntaxKind.InKeyword || context.nextTokenSpan.kind === ts.SyntaxKind.InKeyword || context.currentTokenSpan.kind === ts.SyntaxKind.EqualsToken || context.nextTokenSpan.kind === ts.SyntaxKind.EqualsToken; - // Technically, "of" is not a binary operator, but format it the same way as "in" - case ts.SyntaxKind.ForOfStatement: - return context.currentTokenSpan.kind === ts.SyntaxKind.OfKeyword || context.nextTokenSpan.kind === ts.SyntaxKind.OfKeyword; - } - return false; - } + // equals in binding elements: function foo([[x, y] = [1, 2]]) + case ts.SyntaxKind.BindingElement: + // equals in type X = ... + // falls through + case ts.SyntaxKind.TypeAliasDeclaration: + // equal in import a = module('a'); + // falls through + case ts.SyntaxKind.ImportEqualsDeclaration: + // equal in export = 1 + // falls through + case ts.SyntaxKind.ExportAssignment: + // equal in let a = 0 + // falls through + case ts.SyntaxKind.VariableDeclaration: + // equal in p = 0 + // falls through + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + return context.currentTokenSpan.kind === ts.SyntaxKind.EqualsToken || context.nextTokenSpan.kind === ts.SyntaxKind.EqualsToken; + // "in" keyword in for (let x in []) { } + case ts.SyntaxKind.ForInStatement: + // "in" keyword in [P in keyof T]: T[P] + // falls through + case ts.SyntaxKind.TypeParameter: + return context.currentTokenSpan.kind === ts.SyntaxKind.InKeyword || context.nextTokenSpan.kind === ts.SyntaxKind.InKeyword || context.currentTokenSpan.kind === ts.SyntaxKind.EqualsToken || context.nextTokenSpan.kind === ts.SyntaxKind.EqualsToken; + // Technically, "of" is not a binary operator, but format it the same way as "in" + case ts.SyntaxKind.ForOfStatement: + return context.currentTokenSpan.kind === ts.SyntaxKind.OfKeyword || context.nextTokenSpan.kind === ts.SyntaxKind.OfKeyword; + } + return false; +} - function isNotBinaryOpContext(context: ts.formatting.FormattingContext): boolean { - return !isBinaryOpContext(context); - } +function isNotBinaryOpContext(context: ts.formatting.FormattingContext): boolean { + return !isBinaryOpContext(context); +} - function isNotTypeAnnotationContext(context: ts.formatting.FormattingContext): boolean { - return !isTypeAnnotationContext(context); - } +function isNotTypeAnnotationContext(context: ts.formatting.FormattingContext): boolean { + return !isTypeAnnotationContext(context); +} - function isTypeAnnotationContext(context: ts.formatting.FormattingContext): boolean { - const contextKind = context.contextNode.kind; - return contextKind === ts.SyntaxKind.PropertyDeclaration || - contextKind === ts.SyntaxKind.PropertySignature || - contextKind === ts.SyntaxKind.Parameter || - contextKind === ts.SyntaxKind.VariableDeclaration || - ts.isFunctionLikeKind(contextKind); - } - function isConditionalOperatorContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ConditionalExpression || - context.contextNode.kind === ts.SyntaxKind.ConditionalType; - } - function isSameLineTokenOrBeforeBlockContext(context: ts.formatting.FormattingContext): boolean { - return context.TokensAreOnSameLine() || isBeforeBlockContext(context); - } +function isTypeAnnotationContext(context: ts.formatting.FormattingContext): boolean { + const contextKind = context.contextNode.kind; + return contextKind === ts.SyntaxKind.PropertyDeclaration || + contextKind === ts.SyntaxKind.PropertySignature || + contextKind === ts.SyntaxKind.Parameter || + contextKind === ts.SyntaxKind.VariableDeclaration || + ts.isFunctionLikeKind(contextKind); +} +function isConditionalOperatorContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ConditionalExpression || + context.contextNode.kind === ts.SyntaxKind.ConditionalType; +} +function isSameLineTokenOrBeforeBlockContext(context: ts.formatting.FormattingContext): boolean { + return context.TokensAreOnSameLine() || isBeforeBlockContext(context); +} - function isBraceWrappedContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ObjectBindingPattern || - context.contextNode.kind === ts.SyntaxKind.MappedType || - isSingleLineBlockContext(context); - } +function isBraceWrappedContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ObjectBindingPattern || + context.contextNode.kind === ts.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: ts.formatting.FormattingContext): boolean { - return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine()); - } +// This check is done before an open brace in a control construct, a function, or a typescript block declaration +function isBeforeMultilineBlockContext(context: ts.formatting.FormattingContext): boolean { + return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine()); +} - function isMultilineBlockContext(context: ts.formatting.FormattingContext): boolean { - return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); - } +function isMultilineBlockContext(context: ts.formatting.FormattingContext): boolean { + return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); +} - function isSingleLineBlockContext(context: ts.formatting.FormattingContext): boolean { - return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); - } +function isSingleLineBlockContext(context: ts.formatting.FormattingContext): boolean { + return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); +} - function isBlockContext(context: ts.formatting.FormattingContext): boolean { - return nodeIsBlockContext(context.contextNode); - } +function isBlockContext(context: ts.formatting.FormattingContext): boolean { + return nodeIsBlockContext(context.contextNode); +} - function isBeforeBlockContext(context: ts.formatting.FormattingContext): boolean { - return nodeIsBlockContext(context.nextTokenParent); +function isBeforeBlockContext(context: ts.formatting.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: ts.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; } - // IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children - function nodeIsBlockContext(node: ts.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). + switch (node.kind) { + case ts.SyntaxKind.Block: + case ts.SyntaxKind.CaseBlock: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ModuleBlock: return true; - } - - switch (node.kind) { - case ts.SyntaxKind.Block: - case ts.SyntaxKind.CaseBlock: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ModuleBlock: - return true; - } - - return false; } - function isFunctionDeclContext(context: ts.formatting.FormattingContext): boolean { - switch (context.contextNode.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - // case SyntaxKind.MemberFunctionDeclaration: - // falls through - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - // case SyntaxKind.MethodSignature: - // falls through - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ArrowFunction: - // case SyntaxKind.ConstructorDeclaration: - // case SyntaxKind.SimpleArrowFunctionExpression: - // case SyntaxKind.ParenthesizedArrowFunctionExpression: - // falls through - case ts.SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one - return true; - } + return false; +} - return false; +function isFunctionDeclContext(context: ts.formatting.FormattingContext): boolean { + switch (context.contextNode.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + // case SyntaxKind.MemberFunctionDeclaration: + // falls through + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + // case SyntaxKind.MethodSignature: + // falls through + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ArrowFunction: + // case SyntaxKind.ConstructorDeclaration: + // case SyntaxKind.SimpleArrowFunctionExpression: + // case SyntaxKind.ParenthesizedArrowFunctionExpression: + // falls through + case ts.SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one + return true; } - function isNotFunctionDeclContext(context: ts.formatting.FormattingContext): boolean { - return !isFunctionDeclContext(context); - } + return false; +} - function isFunctionDeclarationOrFunctionExpressionContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.FunctionDeclaration || context.contextNode.kind === ts.SyntaxKind.FunctionExpression; - } +function isNotFunctionDeclContext(context: ts.formatting.FormattingContext): boolean { + return !isFunctionDeclContext(context); +} - function isTypeScriptDeclWithBlockContext(context: ts.formatting.FormattingContext): boolean { - return nodeIsTypeScriptDeclWithBlockContext(context.contextNode); - } +function isFunctionDeclarationOrFunctionExpressionContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.FunctionDeclaration || context.contextNode.kind === ts.SyntaxKind.FunctionExpression; +} - function nodeIsTypeScriptDeclWithBlockContext(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.NamedExports: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.NamedImports: - return true; - } +function isTypeScriptDeclWithBlockContext(context: ts.formatting.FormattingContext): boolean { + return nodeIsTypeScriptDeclWithBlockContext(context.contextNode); +} - return false; +function nodeIsTypeScriptDeclWithBlockContext(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.NamedExports: + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.NamedImports: + return true; } - function isAfterCodeBlockContext(context: ts.formatting.FormattingContext): boolean { - switch (context.currentTokenParent.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.CatchClause: - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.SwitchStatement: - return true; - case ts.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 !== ts.SyntaxKind.ArrowFunction && blockParent.kind !== ts.SyntaxKind.FunctionExpression) { - return true; - } - } - } - return false; - } + return false; +} - function isControlDeclContext(context: ts.formatting.FormattingContext): boolean { - switch (context.contextNode.kind) { - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.SwitchStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.TryStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WithStatement: - // TODO - // case SyntaxKind.ElseClause: - // falls through - case ts.SyntaxKind.CatchClause: +function isAfterCodeBlockContext(context: ts.formatting.FormattingContext): boolean { + switch (context.currentTokenParent.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.CatchClause: + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.SwitchStatement: + return true; + case ts.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 !== ts.SyntaxKind.ArrowFunction && blockParent.kind !== ts.SyntaxKind.FunctionExpression) { return true; - - default: - return false; + } } } + return false; +} - function isObjectContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ObjectLiteralExpression; - } - - function isFunctionCallContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.CallExpression; - } +function isControlDeclContext(context: ts.formatting.FormattingContext): boolean { + switch (context.contextNode.kind) { + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.SwitchStatement: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.TryStatement: + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WithStatement: + // TODO + // case SyntaxKind.ElseClause: + // falls through + case ts.SyntaxKind.CatchClause: + return true; - function isNewContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.NewExpression; + default: + return false; } +} - function isFunctionCallOrNewContext(context: ts.formatting.FormattingContext): boolean { - return isFunctionCallContext(context) || isNewContext(context); - } +function isObjectContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ObjectLiteralExpression; +} - function isPreviousTokenNotComma(context: ts.formatting.FormattingContext): boolean { - return context.currentTokenSpan.kind !== ts.SyntaxKind.CommaToken; - } +function isFunctionCallContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.CallExpression; +} - function isNextTokenNotCloseBracket(context: ts.formatting.FormattingContext): boolean { - return context.nextTokenSpan.kind !== ts.SyntaxKind.CloseBracketToken; - } +function isNewContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.NewExpression; +} - function isNextTokenNotCloseParen(context: ts.formatting.FormattingContext): boolean { - return context.nextTokenSpan.kind !== ts.SyntaxKind.CloseParenToken; - } +function isFunctionCallOrNewContext(context: ts.formatting.FormattingContext): boolean { + return isFunctionCallContext(context) || isNewContext(context); +} - function isArrowFunctionContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ArrowFunction; - } +function isPreviousTokenNotComma(context: ts.formatting.FormattingContext): boolean { + return context.currentTokenSpan.kind !== ts.SyntaxKind.CommaToken; +} - function isImportTypeContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ImportType; - } +function isNextTokenNotCloseBracket(context: ts.formatting.FormattingContext): boolean { + return context.nextTokenSpan.kind !== ts.SyntaxKind.CloseBracketToken; +} - function isNonJsxSameLineTokenContext(context: ts.formatting.FormattingContext): boolean { - return context.TokensAreOnSameLine() && context.contextNode.kind !== ts.SyntaxKind.JsxText; - } +function isNextTokenNotCloseParen(context: ts.formatting.FormattingContext): boolean { + return context.nextTokenSpan.kind !== ts.SyntaxKind.CloseParenToken; +} - function isNonJsxTextContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind !== ts.SyntaxKind.JsxText; - } +function isArrowFunctionContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ArrowFunction; +} - function isNonJsxElementOrFragmentContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind !== ts.SyntaxKind.JsxElement && context.contextNode.kind !== ts.SyntaxKind.JsxFragment; - } +function isImportTypeContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ImportType; +} - function isJsxExpressionContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.JsxExpression || context.contextNode.kind === ts.SyntaxKind.JsxSpreadAttribute; - } +function isNonJsxSameLineTokenContext(context: ts.formatting.FormattingContext): boolean { + return context.TokensAreOnSameLine() && context.contextNode.kind !== ts.SyntaxKind.JsxText; +} - function isNextTokenParentJsxAttribute(context: ts.formatting.FormattingContext): boolean { - return context.nextTokenParent.kind === ts.SyntaxKind.JsxAttribute; - } +function isNonJsxTextContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind !== ts.SyntaxKind.JsxText; +} - function isJsxAttributeContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.JsxAttribute; - } +function isNonJsxElementOrFragmentContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind !== ts.SyntaxKind.JsxElement && context.contextNode.kind !== ts.SyntaxKind.JsxFragment; +} - function isJsxSelfClosingElementContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.JsxSelfClosingElement; - } +function isJsxExpressionContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.JsxExpression || context.contextNode.kind === ts.SyntaxKind.JsxSpreadAttribute; +} - function isNotBeforeBlockInFunctionDeclarationContext(context: ts.formatting.FormattingContext): boolean { - return !isFunctionDeclContext(context) && !isBeforeBlockContext(context); - } +function isNextTokenParentJsxAttribute(context: ts.formatting.FormattingContext): boolean { + return context.nextTokenParent.kind === ts.SyntaxKind.JsxAttribute; +} - function isEndOfDecoratorContextOnSameLine(context: ts.formatting.FormattingContext): boolean { - return context.TokensAreOnSameLine() && - !!context.contextNode.decorators && - nodeIsInDecoratorContext(context.currentTokenParent) && - !nodeIsInDecoratorContext(context.nextTokenParent); - } +function isJsxAttributeContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.JsxAttribute; +} - function nodeIsInDecoratorContext(node: ts.Node): boolean { - while (ts.isExpressionNode(node)) { - node = node.parent; - } - return node.kind === ts.SyntaxKind.Decorator; - } +function isJsxSelfClosingElementContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.JsxSelfClosingElement; +} - function isStartOfVariableDeclarationList(context: ts.formatting.FormattingContext): boolean { - return context.currentTokenParent.kind === ts.SyntaxKind.VariableDeclarationList && - context.currentTokenParent.getStart(context.sourceFile) === context.currentTokenSpan.pos; - } +function isNotBeforeBlockInFunctionDeclarationContext(context: ts.formatting.FormattingContext): boolean { + return !isFunctionDeclContext(context) && !isBeforeBlockContext(context); +} - function isNotFormatOnEnter(context: ts.formatting.FormattingContext): boolean { - return context.formattingRequestKind !== ts.formatting.FormattingRequestKind.FormatOnEnter; - } +function isEndOfDecoratorContextOnSameLine(context: ts.formatting.FormattingContext): boolean { + return context.TokensAreOnSameLine() && + !!context.contextNode.decorators && + nodeIsInDecoratorContext(context.currentTokenParent) && + !nodeIsInDecoratorContext(context.nextTokenParent); +} - function isModuleDeclContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ModuleDeclaration; +function nodeIsInDecoratorContext(node: ts.Node): boolean { + while (ts.isExpressionNode(node)) { + node = node.parent; } + return node.kind === ts.SyntaxKind.Decorator; +} - function isObjectTypeContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.TypeLiteral; // && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; - } +function isStartOfVariableDeclarationList(context: ts.formatting.FormattingContext): boolean { + return context.currentTokenParent.kind === ts.SyntaxKind.VariableDeclarationList && + context.currentTokenParent.getStart(context.sourceFile) === context.currentTokenSpan.pos; +} - function isConstructorSignatureContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.ConstructSignature; - } +function isNotFormatOnEnter(context: ts.formatting.FormattingContext): boolean { + return context.formattingRequestKind !== ts.formatting.FormattingRequestKind.FormatOnEnter; +} - function isTypeArgumentOrParameterOrAssertion(token: ts.formatting.TextRangeWithKind, parent: ts.Node): boolean { - if (token.kind !== ts.SyntaxKind.LessThanToken && token.kind !== ts.SyntaxKind.GreaterThanToken) { - return false; - } - switch (parent.kind) { - case ts.SyntaxKind.TypeReference: - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.ExpressionWithTypeArguments: - return true; - default: - return false; +function isModuleDeclContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ModuleDeclaration; +} - } - } +function isObjectTypeContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.TypeLiteral; // && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; +} - function isTypeArgumentOrParameterOrAssertionContext(context: ts.formatting.FormattingContext): boolean { - return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) || - isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent); - } +function isConstructorSignatureContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.ConstructSignature; +} - function isTypeAssertionContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.TypeAssertionExpression; +function isTypeArgumentOrParameterOrAssertion(token: ts.formatting.TextRangeWithKind, parent: ts.Node): boolean { + if (token.kind !== ts.SyntaxKind.LessThanToken && token.kind !== ts.SyntaxKind.GreaterThanToken) { + return false; } + switch (parent.kind) { + case ts.SyntaxKind.TypeReference: + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.ExpressionWithTypeArguments: + return true; + default: + return false; - function isVoidOpContext(context: ts.formatting.FormattingContext): boolean { - return context.currentTokenSpan.kind === ts.SyntaxKind.VoidKeyword && context.currentTokenParent.kind === ts.SyntaxKind.VoidExpression; } +} - function isYieldOrYieldStarWithOperand(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.YieldExpression && (context.contextNode as ts.YieldExpression).expression !== undefined; - } +function isTypeArgumentOrParameterOrAssertionContext(context: ts.formatting.FormattingContext): boolean { + return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) || + isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent); +} - function isNonNullAssertionContext(context: ts.formatting.FormattingContext): boolean { - return context.contextNode.kind === ts.SyntaxKind.NonNullExpression; - } +function isTypeAssertionContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.TypeAssertionExpression; +} - function isNotStatementConditionContext(context: ts.formatting.FormattingContext): boolean { - return !isStatementConditionContext(context); - } +function isVoidOpContext(context: ts.formatting.FormattingContext): boolean { + return context.currentTokenSpan.kind === ts.SyntaxKind.VoidKeyword && context.currentTokenParent.kind === ts.SyntaxKind.VoidExpression; +} - function isStatementConditionContext(context: ts.formatting.FormattingContext): boolean { - switch (context.contextNode.kind) { - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WhileStatement: - return true; +function isYieldOrYieldStarWithOperand(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.YieldExpression && (context.contextNode as ts.YieldExpression).expression !== undefined; +} - default: - return false; - } - } +function isNonNullAssertionContext(context: ts.formatting.FormattingContext): boolean { + return context.contextNode.kind === ts.SyntaxKind.NonNullExpression; +} - function isSemicolonDeletionContext(context: ts.formatting.FormattingContext): boolean { - let nextTokenKind = context.nextTokenSpan.kind; - let nextTokenStart = context.nextTokenSpan.pos; - if (ts.isTrivia(nextTokenKind)) { - const nextRealToken = context.nextTokenParent === context.currentTokenParent - ? ts.findNextToken(context.currentTokenParent, ts.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 isNotStatementConditionContext(context: ts.formatting.FormattingContext): boolean { + return !isStatementConditionContext(context); +} - const startLine = context.sourceFile.getLineAndCharacterOfPosition(context.currentTokenSpan.pos).line; - const endLine = context.sourceFile.getLineAndCharacterOfPosition(nextTokenStart).line; - if (startLine === endLine) { - return nextTokenKind === ts.SyntaxKind.CloseBraceToken - || nextTokenKind === ts.SyntaxKind.EndOfFileToken; - } +function isStatementConditionContext(context: ts.formatting.FormattingContext): boolean { + switch (context.contextNode.kind) { + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WhileStatement: + return true; - if (nextTokenKind === ts.SyntaxKind.SemicolonClassElement || - nextTokenKind === ts.SyntaxKind.SemicolonToken) { + default: return false; - } - - if (context.contextNode.kind === ts.SyntaxKind.InterfaceDeclaration || - context.contextNode.kind === ts.SyntaxKind.TypeAliasDeclaration) { - // Can’t remove semicolon after `foo`; it would parse as a method declaration: - // - // interface I { - // foo; - // (): void - // } - return !ts.isPropertySignature(context.currentTokenParent) - || !!context.currentTokenParent.type - || nextTokenKind !== ts.SyntaxKind.OpenParenToken; - } + } +} - if (ts.isPropertyDeclaration(context.currentTokenParent)) { - return !context.currentTokenParent.initializer; +function isSemicolonDeletionContext(context: ts.formatting.FormattingContext): boolean { + let nextTokenKind = context.nextTokenSpan.kind; + let nextTokenStart = context.nextTokenSpan.pos; + if (ts.isTrivia(nextTokenKind)) { + const nextRealToken = context.nextTokenParent === context.currentTokenParent + ? ts.findNextToken(context.currentTokenParent, ts.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); + } - return context.currentTokenParent.kind !== ts.SyntaxKind.ForStatement - && context.currentTokenParent.kind !== ts.SyntaxKind.EmptyStatement - && context.currentTokenParent.kind !== ts.SyntaxKind.SemicolonClassElement - && nextTokenKind !== ts.SyntaxKind.OpenBracketToken - && nextTokenKind !== ts.SyntaxKind.OpenParenToken - && nextTokenKind !== ts.SyntaxKind.PlusToken - && nextTokenKind !== ts.SyntaxKind.MinusToken - && nextTokenKind !== ts.SyntaxKind.SlashToken - && nextTokenKind !== ts.SyntaxKind.RegularExpressionLiteral - && nextTokenKind !== ts.SyntaxKind.CommaToken - && nextTokenKind !== ts.SyntaxKind.TemplateExpression - && nextTokenKind !== ts.SyntaxKind.TemplateHead - && nextTokenKind !== ts.SyntaxKind.NoSubstitutionTemplateLiteral - && nextTokenKind !== ts.SyntaxKind.DotToken; + const startLine = context.sourceFile.getLineAndCharacterOfPosition(context.currentTokenSpan.pos).line; + const endLine = context.sourceFile.getLineAndCharacterOfPosition(nextTokenStart).line; + if (startLine === endLine) { + return nextTokenKind === ts.SyntaxKind.CloseBraceToken + || nextTokenKind === ts.SyntaxKind.EndOfFileToken; } - function isSemicolonInsertionContext(context: ts.formatting.FormattingContext): boolean { - return ts.positionIsASICandidate(context.currentTokenSpan.end, context.currentTokenParent, context.sourceFile); + + if (nextTokenKind === ts.SyntaxKind.SemicolonClassElement || + nextTokenKind === ts.SyntaxKind.SemicolonToken) { + return false; } + + if (context.contextNode.kind === ts.SyntaxKind.InterfaceDeclaration || + context.contextNode.kind === ts.SyntaxKind.TypeAliasDeclaration) { + // Can’t remove semicolon after `foo`; it would parse as a method declaration: + // + // interface I { + // foo; + // (): void + // } + return !ts.isPropertySignature(context.currentTokenParent) + || !!context.currentTokenParent.type + || nextTokenKind !== ts.SyntaxKind.OpenParenToken; + } + + if (ts.isPropertyDeclaration(context.currentTokenParent)) { + return !context.currentTokenParent.initializer; + } + + return context.currentTokenParent.kind !== ts.SyntaxKind.ForStatement + && context.currentTokenParent.kind !== ts.SyntaxKind.EmptyStatement + && context.currentTokenParent.kind !== ts.SyntaxKind.SemicolonClassElement + && nextTokenKind !== ts.SyntaxKind.OpenBracketToken + && nextTokenKind !== ts.SyntaxKind.OpenParenToken + && nextTokenKind !== ts.SyntaxKind.PlusToken + && nextTokenKind !== ts.SyntaxKind.MinusToken + && nextTokenKind !== ts.SyntaxKind.SlashToken + && nextTokenKind !== ts.SyntaxKind.RegularExpressionLiteral + && nextTokenKind !== ts.SyntaxKind.CommaToken + && nextTokenKind !== ts.SyntaxKind.TemplateExpression + && nextTokenKind !== ts.SyntaxKind.TemplateHead + && nextTokenKind !== ts.SyntaxKind.NoSubstitutionTemplateLiteral + && nextTokenKind !== ts.SyntaxKind.DotToken; +} +function isSemicolonInsertionContext(context: ts.formatting.FormattingContext): boolean { + return ts.positionIsASICandidate(context.currentTokenSpan.end, context.currentTokenParent, context.sourceFile); +} } diff --git a/src/services/formatting/rulesMap.ts b/src/services/formatting/rulesMap.ts index 4540fb19e53d0..b823e844eda84 100644 --- a/src/services/formatting/rulesMap.ts +++ b/src/services/formatting/rulesMap.ts @@ -1,140 +1,140 @@ /* @internal */ namespace ts.formatting { - export function getFormatContext(options: ts.FormatCodeSettings, host: ts.FormattingHost): ts.formatting.FormatContext { - return { options, getRules: getRulesMap(), host }; - } +export function getFormatContext(options: ts.FormatCodeSettings, host: ts.FormattingHost): ts.formatting.FormatContext { + return { options, getRules: getRulesMap(), host }; +} - let rulesMapCache: RulesMap | undefined; +let rulesMapCache: RulesMap | undefined; - function getRulesMap(): RulesMap { - if (rulesMapCache === undefined) { - rulesMapCache = createRulesMap(ts.formatting.getAllRules()); - } - return rulesMapCache; +function getRulesMap(): RulesMap { + if (rulesMapCache === undefined) { + rulesMapCache = createRulesMap(ts.formatting.getAllRules()); } + return rulesMapCache; +} - /** - * For a given rule action, gets a mask of other rule actions that - * cannot be applied at the same position. - */ - function getRuleActionExclusion(ruleAction: ts.formatting.RuleAction): ts.formatting.RuleAction { - let mask: ts.formatting.RuleAction = 0; - if (ruleAction & ts.formatting.RuleAction.StopProcessingSpaceActions) { - mask |= ts.formatting.RuleAction.ModifySpaceAction; - } - if (ruleAction & ts.formatting.RuleAction.StopProcessingTokenActions) { - mask |= ts.formatting.RuleAction.ModifyTokenAction; - } - if (ruleAction & ts.formatting.RuleAction.ModifySpaceAction) { - mask |= ts.formatting.RuleAction.ModifySpaceAction; - } - if (ruleAction & ts.formatting.RuleAction.ModifyTokenAction) { - mask |= ts.formatting.RuleAction.ModifyTokenAction; - } - return mask; +/** + * For a given rule action, gets a mask of other rule actions that + * cannot be applied at the same position. + */ +function getRuleActionExclusion(ruleAction: ts.formatting.RuleAction): ts.formatting.RuleAction { + let mask: ts.formatting.RuleAction = 0; + if (ruleAction & ts.formatting.RuleAction.StopProcessingSpaceActions) { + mask |= ts.formatting.RuleAction.ModifySpaceAction; + } + if (ruleAction & ts.formatting.RuleAction.StopProcessingTokenActions) { + mask |= ts.formatting.RuleAction.ModifyTokenAction; + } + if (ruleAction & ts.formatting.RuleAction.ModifySpaceAction) { + mask |= ts.formatting.RuleAction.ModifySpaceAction; + } + if (ruleAction & ts.formatting.RuleAction.ModifyTokenAction) { + mask |= ts.formatting.RuleAction.ModifyTokenAction; } + return mask; +} - export type RulesMap = (context: ts.formatting.FormattingContext) => readonly ts.formatting.Rule[] | undefined; - function createRulesMap(rules: readonly ts.formatting.RuleSpec[]): RulesMap { - const map = buildMap(rules); - return context => { - const bucket = map[getRuleBucketIndex(context.currentTokenSpan.kind, context.nextTokenSpan.kind)]; - if (bucket) { - const rules: ts.formatting.Rule[] = []; - let ruleActionMask: ts.formatting.RuleAction = 0; - for (const rule of bucket) { - const acceptRuleActions = ~getRuleActionExclusion(ruleActionMask); - if (rule.action & acceptRuleActions && ts.every(rule.context, c => c(context))) { - rules.push(rule); - ruleActionMask |= rule.action; - } - } - if (rules.length) { - return rules; +export type RulesMap = (context: ts.formatting.FormattingContext) => readonly ts.formatting.Rule[] | undefined; +function createRulesMap(rules: readonly ts.formatting.RuleSpec[]): RulesMap { + const map = buildMap(rules); + return context => { + const bucket = map[getRuleBucketIndex(context.currentTokenSpan.kind, context.nextTokenSpan.kind)]; + if (bucket) { + const rules: ts.formatting.Rule[] = []; + let ruleActionMask: ts.formatting.RuleAction = 0; + for (const rule of bucket) { + const acceptRuleActions = ~getRuleActionExclusion(ruleActionMask); + if (rule.action & acceptRuleActions && ts.every(rule.context, c => c(context))) { + rules.push(rule); + ruleActionMask |= rule.action; } } - }; - } + if (rules.length) { + return rules; + } + } + }; +} - function buildMap(rules: readonly ts.formatting.RuleSpec[]): readonly (readonly ts.formatting.Rule[])[] { - // Map from bucket index to array of rules - const map: ts.formatting.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; +function buildMap(rules: readonly ts.formatting.RuleSpec[]): readonly (readonly ts.formatting.Rule[])[] { + // Map from bucket index to array of rules + const map: ts.formatting.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); + 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 map; } + return map; +} - function getRuleBucketIndex(row: number, column: number): number { - ts.Debug.assert(row <= ts.SyntaxKind.LastKeyword && column <= ts.SyntaxKind.LastKeyword, "Must compute formatting context from tokens"); - return (row * mapRowLength) + column; - } +function getRuleBucketIndex(row: number, column: number): number { + ts.Debug.assert(row <= ts.SyntaxKind.LastKeyword && column <= ts.SyntaxKind.LastKeyword, "Must compute formatting context from tokens"); + return (row * mapRowLength) + column; +} - const maskBitSize = 5; - const mask = 0b11111; // MaskBitSize bits - const mapRowLength = ts.SyntaxKind.LastToken + 1; +const maskBitSize = 5; +const mask = 0b11111; // MaskBitSize bits +const mapRowLength = ts.SyntaxKind.LastToken + 1; - enum RulesPosition { - StopRulesSpecific = 0, - StopRulesAny = maskBitSize * 1, - ContextRulesSpecific = maskBitSize * 2, - ContextRulesAny = maskBitSize * 3, - NoContextRulesSpecific = maskBitSize * 4, - NoContextRulesAny = maskBitSize * 5 - } +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: ts.formatting.Rule[], rule: ts.formatting.Rule, specificTokens: boolean, constructionState: number[], rulesBucketIndex: number): void { - const position = rule.action & ts.formatting.RuleAction.StopAction ? - specificTokens ? RulesPosition.StopRulesSpecific : RulesPosition.StopRulesAny : - rule.context !== ts.formatting.anyContext ? - specificTokens ? RulesPosition.ContextRulesSpecific : RulesPosition.ContextRulesAny : - specificTokens ? RulesPosition.NoContextRulesSpecific : RulesPosition.NoContextRulesAny; +// 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: ts.formatting.Rule[], rule: ts.formatting.Rule, specificTokens: boolean, constructionState: number[], rulesBucketIndex: number): void { + const position = rule.action & ts.formatting.RuleAction.StopAction ? + specificTokens ? RulesPosition.StopRulesSpecific : RulesPosition.StopRulesAny : + rule.context !== ts.formatting.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); - } + 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; - } - return index; +function getInsertionIndex(indexBitmap: number, maskPosition: RulesPosition) { + let index = 0; + for (let pos = 0; pos <= maskPosition; pos += maskBitSize) { + index += indexBitmap & mask; + indexBitmap >>= maskBitSize; } + return index; +} - function increaseInsertionIndex(indexBitmap: number, maskPosition: RulesPosition): number { - const value = ((indexBitmap >> maskPosition) & mask) + 1; - ts.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); - } +function increaseInsertionIndex(indexBitmap: number, maskPosition: RulesPosition): number { + const value = ((indexBitmap >> maskPosition) & mask) + 1; + ts.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 6189ae97ba597..7b0a4a501f569 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -1,695 +1,695 @@ /* @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: ts.SourceFile, options: ts.EditorSettings, assumeNewLineBeforeCloseBrace = false): number { - if (position > sourceFile.text.length) { - return getBaseIndentation(options); // past EOF - } +export namespace SmartIndenter { - // no indentation when the indent style is set to none, - // so we can return fast - if (options.indentStyle === ts.IndentStyle.None) { - return 0; - } + const enum Value { + Unknown = -1 + } - const precedingToken = ts.findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*excludeJsdoc*/ true); + /** + * @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: ts.SourceFile, options: ts.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 === ts.IndentStyle.None) { + return 0; + } + + const precedingToken = ts.findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*excludeJsdoc*/ true); + + // eslint-disable-next-line no-null/no-null + const enclosingCommentRange = ts.formatting.getRangeOfEnclosingComment(sourceFile, position, precedingToken || null); + if (enclosingCommentRange && enclosingCommentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + return getCommentIndent(sourceFile, position, options, enclosingCommentRange); + } + + if (!precedingToken) { + return getBaseIndentation(options); + } - // eslint-disable-next-line no-null/no-null - const enclosingCommentRange = ts.formatting.getRangeOfEnclosingComment(sourceFile, position, precedingToken || null); - if (enclosingCommentRange && enclosingCommentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - return getCommentIndent(sourceFile, position, options, enclosingCommentRange); + // no indentation in string \regex\template literals + const precedingTokenIsLiteral = ts.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. + const currentToken = ts.getTokenAtPosition(sourceFile, position); + // For object literals, we want indentation to work just like with blocks. + // If the `{` starts in any position (even in the middle of a line), then + // the following indentation should treat `{` as the start of that line (including leading whitespace). + // ``` + // const a: { x: undefined, y: undefined } = {} // leading 4 whitespaces and { starts in the middle of line + // -> + // const a: { x: undefined, y: undefined } = { + // x: undefined, + // y: undefined, + // } + // --------------------- + // const a: {x : undefined, y: undefined } = + // {} + // -> + // const a: { x: undefined, y: undefined } = + // { // leading 5 whitespaces and { starts at 6 column + // x: undefined, + // y: undefined, + // } + // ``` + const isObjectLiteral = currentToken.kind === ts.SyntaxKind.OpenBraceToken && currentToken.parent.kind === ts.SyntaxKind.ObjectLiteralExpression; + if (options.indentStyle === ts.IndentStyle.Block || isObjectLiteral) { + return getBlockIndent(sourceFile, position, options); + } + + if (precedingToken.kind === ts.SyntaxKind.CommaToken && precedingToken.parent.kind !== ts.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) { - return getBaseIndentation(options); - } + const containerList = getListByPosition(position, precedingToken.parent, sourceFile); + // use list position if the preceding token is before any list items + if (containerList && !ts.rangeContainsRange(containerList, precedingToken)) { + const useTheSameBaseIndentation = [ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ArrowFunction].indexOf(currentToken.parent.kind) !== -1; + const indentSize = useTheSameBaseIndentation ? 0 : options.indentSize!; + return getActualIndentationForListStartLine(containerList, sourceFile, options) + indentSize; // TODO: GH#18217 + } - // no indentation in string \regex\template literals - const precedingTokenIsLiteral = ts.isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind); - if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) { - return 0; - } + return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options); + } - 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. - const currentToken = ts.getTokenAtPosition(sourceFile, position); - // For object literals, we want indentation to work just like with blocks. - // If the `{` starts in any position (even in the middle of a line), then - // the following indentation should treat `{` as the start of that line (including leading whitespace). - // ``` - // const a: { x: undefined, y: undefined } = {} // leading 4 whitespaces and { starts in the middle of line - // -> - // const a: { x: undefined, y: undefined } = { - // x: undefined, - // y: undefined, - // } - // --------------------- - // const a: {x : undefined, y: undefined } = - // {} - // -> - // const a: { x: undefined, y: undefined } = - // { // leading 5 whitespaces and { starts at 6 column - // x: undefined, - // y: undefined, - // } - // ``` - const isObjectLiteral = currentToken.kind === ts.SyntaxKind.OpenBraceToken && currentToken.parent.kind === ts.SyntaxKind.ObjectLiteralExpression; - if (options.indentStyle === ts.IndentStyle.Block || isObjectLiteral) { - return getBlockIndent(sourceFile, position, options); - } + function getCommentIndent(sourceFile: ts.SourceFile, position: number, options: ts.EditorSettings, enclosingCommentRange: ts.CommentRange): number { + const previousLine = ts.getLineAndCharacterOfPosition(sourceFile, position).line - 1; + const commentStartLine = ts.getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line; + ts.Debug.assert(commentStartLine >= 0); - if (precedingToken.kind === ts.SyntaxKind.CommaToken && precedingToken.parent.kind !== ts.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 (previousLine <= commentStartLine) { + return findFirstNonWhitespaceColumn(ts.getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options); + } - const containerList = getListByPosition(position, precedingToken.parent, sourceFile); - // use list position if the preceding token is before any list items - if (containerList && !ts.rangeContainsRange(containerList, precedingToken)) { - const useTheSameBaseIndentation = [ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ArrowFunction].indexOf(currentToken.parent.kind) !== -1; - const indentSize = useTheSameBaseIndentation ? 0 : options.indentSize!; - return getActualIndentationForListStartLine(containerList, sourceFile, options) + indentSize; // TODO: GH#18217 - } + const startPositionOfLine = ts.getStartPositionOfLine(previousLine, sourceFile); + const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options); - return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options); + if (column === 0) { + return column; } - function getCommentIndent(sourceFile: ts.SourceFile, position: number, options: ts.EditorSettings, enclosingCommentRange: ts.CommentRange): number { - const previousLine = ts.getLineAndCharacterOfPosition(sourceFile, position).line - 1; - const commentStartLine = ts.getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line; - ts.Debug.assert(commentStartLine >= 0); + const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character); + return firstNonWhitespaceCharacterCode === ts.CharacterCodes.asterisk ? column - 1 : column; + } - if (previousLine <= commentStartLine) { - return findFirstNonWhitespaceColumn(ts.getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options); + function getBlockIndent(sourceFile: ts.SourceFile, position: number, options: ts.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 (!ts.isWhiteSpaceLike(char)) { + break; } + current--; + } - const startPositionOfLine = ts.getStartPositionOfLine(previousLine, sourceFile); - const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options); + const lineStart = ts.getLineStartPositionForPosition(current, sourceFile); + return findFirstNonWhitespaceColumn(lineStart, current, sourceFile, options); + } - if (column === 0) { - return column; - } + function getSmartIndent(sourceFile: ts.SourceFile, position: number, precedingToken: ts.Node, lineAtPosition: number, assumeNewLineBeforeCloseBrace: boolean, options: ts.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: ts.Node | undefined; + let current = precedingToken; + + while (current) { + if (ts.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); + } - const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character); - return firstNonWhitespaceCharacterCode === ts.CharacterCodes.asterisk ? column - 1 : column; - } + export function getIndentationForNode(n: ts.Node, ignoreActualIndentationRange: ts.TextRange, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); + return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, /*isNextChild*/ false, options); + } - function getBlockIndent(sourceFile: ts.SourceFile, position: number, options: ts.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 (!ts.isWhiteSpaceLike(char)) { - break; - } - current--; + export function getBaseIndentation(options: ts.EditorSettings) { + return options.baseIndentSize || 0; + } + + function getIndentationForNodeWorker(current: ts.Node, currentStart: ts.LineAndCharacter, ignoreActualIndentationRange: ts.TextRange | undefined, indentationDelta: number, sourceFile: ts.SourceFile, isNextChild: boolean, options: ts.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 lineStart = ts.getLineStartPositionForPosition(current, sourceFile); - return findFirstNonWhitespaceColumn(lineStart, current, sourceFile, options); - } - - function getSmartIndent(sourceFile: ts.SourceFile, position: number, precedingToken: ts.Node, lineAtPosition: number, assumeNewLineBeforeCloseBrace: boolean, options: ts.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: ts.Node | undefined; - let current = precedingToken; - - while (current) { - if (ts.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 - } + 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 - // 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); + const firstListChild = getContainingList(current, sourceFile)?.[0]; + // A list indents its children if the children begin on a later line than the list itself: + // + // f1( L0 - List start + // { L1 - First child start: indented, along with all other children + // prop: 0 + // }, + // { + // prop: 1 + // } + // ) + // + // f2({ L0 - List start and first child start: children are not indented. + // prop: 0 Object properties are indented only one level, because the list + // }, { itself contributes nothing. + // prop: 1 L3 - The indentation of the second object literal is best understood by + // }) looking at the relationship between the list and *first* list item. + const listIndentsChild = !!firstListChild && getStartLineAndCharacterForNode(firstListChild, sourceFile).line > containingListOrParentStart.line; + let actualIndentation = getActualIndentationForListItem(current, sourceFile, options, listIndentsChild); if (actualIndentation !== Value.Unknown) { - return actualIndentation; + return actualIndentation + indentationDelta; } - previous = current; - current = current.parent; + // 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; + } } - // no parent was found - return the base indentation of the SourceFile - return getBaseIndentation(options); - } - export function getIndentationForNode(n: ts.Node, ignoreActualIndentationRange: ts.TextRange, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { - const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); - return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, /*isNextChild*/ false, 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!; + } - export function getBaseIndentation(options: ts.EditorSettings) { - return options.baseIndentSize || 0; - } + // 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. - function getIndentationForNodeWorker(current: ts.Node, currentStart: ts.LineAndCharacter, ignoreActualIndentationRange: ts.TextRange | undefined, indentationDelta: number, sourceFile: ts.SourceFile, isNextChild: boolean, options: ts.EditorSettings): number { - let parent = current.parent; + const useTrueStart = isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStart.line, sourceFile); - // 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; - } + current = parent; + parent = current.parent; + currentStart = useTrueStart ? sourceFile.getLineAndCharacterOfPosition(current.getStart(sourceFile)) : containingListOrParentStart; + } - 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 - const firstListChild = getContainingList(current, sourceFile)?.[0]; - // A list indents its children if the children begin on a later line than the list itself: - // - // f1( L0 - List start - // { L1 - First child start: indented, along with all other children - // prop: 0 - // }, - // { - // prop: 1 - // } - // ) - // - // f2({ L0 - List start and first child start: children are not indented. - // prop: 0 Object properties are indented only one level, because the list - // }, { itself contributes nothing. - // prop: 1 L3 - The indentation of the second object literal is best understood by - // }) looking at the relationship between the list and *first* list item. - const listIndentsChild = !!firstListChild && getStartLineAndCharacterForNode(firstListChild, sourceFile).line > containingListOrParentStart.line; - let actualIndentation = getActualIndentationForListItem(current, sourceFile, options, listIndentsChild); - 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; - } - } + return indentationDelta + 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!; - } + function getContainingListOrParentStart(parent: ts.Node, child: ts.Node, sourceFile: ts.SourceFile): ts.LineAndCharacter { + const containingList = getContainingList(child, sourceFile); + const startPos = containingList ? containingList.pos : parent.getStart(sourceFile); + return sourceFile.getLineAndCharacterOfPosition(startPos); + } - // 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. + /* + * Function returns Value.Unknown if indentation cannot be determined + */ + function getActualIndentationForListItemBeforeComma(commaToken: ts.Node, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + const commaItemInfo = ts.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; + } + } - const useTrueStart = isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStart.line, sourceFile); + /* + * Function returns Value.Unknown if actual indentation for node should not be used (i.e because node is nested expression) + */ + function getActualIndentationForNode(current: ts.Node, parent: ts.Node, currentLineAndChar: ts.LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { - current = parent; - parent = current.parent; - currentStart = useTrueStart ? sourceFile.getLineAndCharacterOfPosition(current.getStart(sourceFile)) : containingListOrParentStart; - } + // 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 = (ts.isDeclaration(current) || ts.isStatementButNotDeclaration(current)) && + (parent.kind === ts.SyntaxKind.SourceFile || !parentAndChildShareLine); - return indentationDelta + getBaseIndentation(options); + if (!useActualIndentation) { + return Value.Unknown; } - function getContainingListOrParentStart(parent: ts.Node, child: ts.Node, sourceFile: ts.SourceFile): ts.LineAndCharacter { - const containingList = getContainingList(child, sourceFile); - const startPos = containingList ? containingList.pos : parent.getStart(sourceFile); - return sourceFile.getLineAndCharacterOfPosition(startPos); + return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); + } + + const enum NextTokenKind { + Unknown, + OpenBrace, + CloseBrace + } + + function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: ts.Node, current: ts.Node, lineAtPosition: number, sourceFile: ts.SourceFile): NextTokenKind { + const nextToken = ts.findNextToken(precedingToken, current, sourceFile); + if (!nextToken) { + return NextTokenKind.Unknown; } - /* - * Function returns Value.Unknown if indentation cannot be determined - */ - function getActualIndentationForListItemBeforeComma(commaToken: ts.Node, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { - // previous token is comma that separates items in list - find the previous item and try to derive indentation from it - const commaItemInfo = ts.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; - } + if (nextToken.kind === ts.SyntaxKind.OpenBraceToken) { + // open braces are always indented at the parent level + return NextTokenKind.OpenBrace; } + else if (nextToken.kind === ts.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 { + // $} - /* - * Function returns Value.Unknown if actual indentation for node should not be used (i.e because node is nested expression) - */ - function getActualIndentationForNode(current: ts.Node, parent: ts.Node, currentLineAndChar: ts.LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + const nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; + return lineAtPosition === nextTokenStartLine ? NextTokenKind.CloseBrace : NextTokenKind.Unknown; + } - // 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 = (ts.isDeclaration(current) || ts.isStatementButNotDeclaration(current)) && - (parent.kind === ts.SyntaxKind.SourceFile || !parentAndChildShareLine); + return NextTokenKind.Unknown; + } - if (!useActualIndentation) { - return Value.Unknown; - } + function getStartLineAndCharacterForNode(n: ts.Node, sourceFile: ts.SourceFileLike): ts.LineAndCharacter { + return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); + } - return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); + export function isArgumentAndStartLineOverlapsExpressionBeingCalled(parent: ts.Node, child: ts.Node, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { + if (!(ts.isCallExpression(parent) && ts.contains(parent.arguments, child))) { + return false; } - const enum NextTokenKind { - Unknown, - OpenBrace, - CloseBrace + const expressionOfCallExpressionEnd = parent.expression.getEnd(); + const expressionOfCallExpressionEndLine = ts.getLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd).line; + return expressionOfCallExpressionEndLine === childStartLine; + } + + export function childStartsOnTheSameLineWithElseInIfStatement(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { + if (parent.kind === ts.SyntaxKind.IfStatement && (parent as ts.IfStatement).elseStatement === child) { + const elseKeyword = ts.findChildOfKind(parent, ts.SyntaxKind.ElseKeyword, sourceFile)!; + ts.Debug.assert(elseKeyword !== undefined); + + const elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; + return elseKeywordStartLine === childStartLine; } - function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: ts.Node, current: ts.Node, lineAtPosition: number, sourceFile: ts.SourceFile): NextTokenKind { - const nextToken = ts.findNextToken(precedingToken, current, sourceFile); - if (!nextToken) { - return NextTokenKind.Unknown; - } + return false; + } - if (nextToken.kind === ts.SyntaxKind.OpenBraceToken) { - // open braces are always indented at the parent level - return NextTokenKind.OpenBrace; + // A multiline conditional typically increases the indentation of its whenTrue and whenFalse children: + // + // condition + // ? whenTrue + // : whenFalse; + // + // However, that indentation does not apply if the subexpressions themselves span multiple lines, + // applying their own indentation: + // + // (() => { + // return complexCalculationForCondition(); + // })() ? { + // whenTrue: 'multiline object literal' + // } : ( + // whenFalse('multiline parenthesized expression') + // ); + // + // In these cases, we must discard the indentation increase that would otherwise be applied to the + // whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario, + // we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse + // branch beginning on the line that the whenTrue branch ends. + export function childIsUnindentedBranchOfConditionalExpression(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { + if (ts.isConditionalExpression(parent) && (child === parent.whenTrue || child === parent.whenFalse)) { + const conditionEndLine = ts.getLineAndCharacterOfPosition(sourceFile, parent.condition.end).line; + if (child === parent.whenTrue) { + return childStartLine === conditionEndLine; } - else if (nextToken.kind === ts.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 { + // On the whenFalse side, we have to look at the whenTrue side, because if that one was + // indented, whenFalse must also be indented: + // + // const y = true + // ? 1 : ( L1: whenTrue indented because it's on a new line + // 0 L2: indented two stops, one because whenTrue was indented + // ); and one because of the parentheses spanning multiple lines + const trueStartLine = getStartLineAndCharacterForNode(parent.whenTrue, sourceFile).line; + const trueEndLine = ts.getLineAndCharacterOfPosition(sourceFile, parent.whenTrue.end).line; + return conditionEndLine === trueStartLine && trueEndLine === childStartLine; } - - return NextTokenKind.Unknown; - } - - function getStartLineAndCharacterForNode(n: ts.Node, sourceFile: ts.SourceFileLike): ts.LineAndCharacter { - return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); } + return false; + } - export function isArgumentAndStartLineOverlapsExpressionBeingCalled(parent: ts.Node, child: ts.Node, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { - if (!(ts.isCallExpression(parent) && ts.contains(parent.arguments, child))) { + export function argumentStartsOnSameLineAsPreviousArgument(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { + if (ts.isCallOrNewExpression(parent)) { + if (!parent.arguments) return false; - } - - const expressionOfCallExpressionEnd = parent.expression.getEnd(); - const expressionOfCallExpressionEndLine = ts.getLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd).line; - return expressionOfCallExpressionEndLine === childStartLine; - } + const currentNode = ts.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 - export function childStartsOnTheSameLineWithElseInIfStatement(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { - if (parent.kind === ts.SyntaxKind.IfStatement && (parent as ts.IfStatement).elseStatement === child) { - const elseKeyword = ts.findChildOfKind(parent, ts.SyntaxKind.ElseKeyword, sourceFile)!; - ts.Debug.assert(elseKeyword !== undefined); + const previousNode = parent.arguments[currentIndex - 1]; + const lineOfPreviousNode = ts.getLineAndCharacterOfPosition(sourceFile, previousNode.getEnd()).line; - const elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; - return elseKeywordStartLine === childStartLine; + if (childStartLine === lineOfPreviousNode) { + return true; } - - return false; } - // A multiline conditional typically increases the indentation of its whenTrue and whenFalse children: - // - // condition - // ? whenTrue - // : whenFalse; - // - // However, that indentation does not apply if the subexpressions themselves span multiple lines, - // applying their own indentation: - // - // (() => { - // return complexCalculationForCondition(); - // })() ? { - // whenTrue: 'multiline object literal' - // } : ( - // whenFalse('multiline parenthesized expression') - // ); - // - // In these cases, we must discard the indentation increase that would otherwise be applied to the - // whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario, - // we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse - // branch beginning on the line that the whenTrue branch ends. - export function childIsUnindentedBranchOfConditionalExpression(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { - if (ts.isConditionalExpression(parent) && (child === parent.whenTrue || child === parent.whenFalse)) { - const conditionEndLine = ts.getLineAndCharacterOfPosition(sourceFile, parent.condition.end).line; - if (child === parent.whenTrue) { - return childStartLine === conditionEndLine; - } - else { - // On the whenFalse side, we have to look at the whenTrue side, because if that one was - // indented, whenFalse must also be indented: - // - // const y = true - // ? 1 : ( L1: whenTrue indented because it's on a new line - // 0 L2: indented two stops, one because whenTrue was indented - // ); and one because of the parentheses spanning multiple lines - const trueStartLine = getStartLineAndCharacterForNode(parent.whenTrue, sourceFile).line; - const trueEndLine = ts.getLineAndCharacterOfPosition(sourceFile, parent.whenTrue.end).line; - return conditionEndLine === trueStartLine && trueEndLine === childStartLine; - } - } - return false; - } + return false; + } - export function argumentStartsOnSameLineAsPreviousArgument(parent: ts.Node, child: ts.formatting.TextRangeWithKind, childStartLine: number, sourceFile: ts.SourceFileLike): boolean { - if (ts.isCallOrNewExpression(parent)) { - if (!parent.arguments) - return false; - const currentNode = ts.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 + export function getContainingList(node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { + return node.parent && getListByRange(node.getStart(sourceFile), node.getEnd(), node.parent, sourceFile); + } - const previousNode = parent.arguments[currentIndex - 1]; - const lineOfPreviousNode = ts.getLineAndCharacterOfPosition(sourceFile, previousNode.getEnd()).line; + function getListByPosition(pos: number, node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { + return node && getListByRange(pos, pos, node, sourceFile); + } - if (childStartLine === lineOfPreviousNode) { - return true; - } + function getListByRange(start: number, end: number, node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { + switch (node.kind) { + case ts.SyntaxKind.TypeReference: + return getList((node as ts.TypeReferenceNode).typeArguments); + case ts.SyntaxKind.ObjectLiteralExpression: + return getList((node as ts.ObjectLiteralExpression).properties); + case ts.SyntaxKind.ArrayLiteralExpression: + return getList((node as ts.ArrayLiteralExpression).elements); + case ts.SyntaxKind.TypeLiteral: + return getList((node as ts.TypeLiteralNode).members); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ConstructSignature: + return getList((node as ts.SignatureDeclaration).typeParameters) || getList((node as ts.SignatureDeclaration).parameters); + case ts.SyntaxKind.GetAccessor: + return getList((node as ts.GetAccessorDeclaration).parameters); + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.JSDocTemplateTag: + return getList((node as ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTemplateTag).typeParameters); + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.CallExpression: + return getList((node as ts.CallExpression).typeArguments) || getList((node as ts.CallExpression).arguments); + case ts.SyntaxKind.VariableDeclarationList: + return getList((node as ts.VariableDeclarationList).declarations); + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.NamedExports: + return getList((node as ts.NamedImportsOrExports).elements); + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + return getList((node as ts.ObjectBindingPattern | ts.ArrayBindingPattern).elements); + } + function getList(list: ts.NodeArray | undefined): ts.NodeArray | undefined { + return list && ts.rangeContainsStartEnd(getVisualListRange(node, list, sourceFile), start, end) ? list : undefined; + } + } + function getVisualListRange(node: ts.Node, list: ts.TextRange, sourceFile: ts.SourceFile): ts.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; + } - return false; + function getActualIndentationForListStartLine(list: ts.NodeArray, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + if (!list) { + return Value.Unknown; } + return findColumnForFirstNonWhitespaceCharacterInLine(sourceFile.getLineAndCharacterOfPosition(list.pos), sourceFile, options); + } - export function getContainingList(node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { - return node.parent && getListByRange(node.getStart(sourceFile), node.getEnd(), node.parent, sourceFile); - } - - function getListByPosition(pos: number, node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { - return node && getListByRange(pos, pos, node, sourceFile); - } - - function getListByRange(start: number, end: number, node: ts.Node, sourceFile: ts.SourceFile): ts.NodeArray | undefined { - switch (node.kind) { - case ts.SyntaxKind.TypeReference: - return getList((node as ts.TypeReferenceNode).typeArguments); - case ts.SyntaxKind.ObjectLiteralExpression: - return getList((node as ts.ObjectLiteralExpression).properties); - case ts.SyntaxKind.ArrayLiteralExpression: - return getList((node as ts.ArrayLiteralExpression).elements); - case ts.SyntaxKind.TypeLiteral: - return getList((node as ts.TypeLiteralNode).members); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ConstructSignature: - return getList((node as ts.SignatureDeclaration).typeParameters) || getList((node as ts.SignatureDeclaration).parameters); - case ts.SyntaxKind.GetAccessor: - return getList((node as ts.GetAccessorDeclaration).parameters); - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.JSDocTemplateTag: - return getList((node as ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.JSDocTemplateTag).typeParameters); - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.CallExpression: - return getList((node as ts.CallExpression).typeArguments) || getList((node as ts.CallExpression).arguments); - case ts.SyntaxKind.VariableDeclarationList: - return getList((node as ts.VariableDeclarationList).declarations); - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.NamedExports: - return getList((node as ts.NamedImportsOrExports).elements); - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.ArrayBindingPattern: - return getList((node as ts.ObjectBindingPattern | ts.ArrayBindingPattern).elements); - } - function getList(list: ts.NodeArray | undefined): ts.NodeArray | undefined { - return list && ts.rangeContainsStartEnd(getVisualListRange(node, list, sourceFile), start, end) ? list : undefined; - } + function getActualIndentationForListItem(node: ts.Node, sourceFile: ts.SourceFile, options: ts.EditorSettings, listIndentsChild: boolean): number { + if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList) { + // VariableDeclarationList has no wrapping tokens + return Value.Unknown; } - function getVisualListRange(node: ts.Node, list: ts.TextRange, sourceFile: ts.SourceFile): ts.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) }; + 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 list; + return getActualIndentationForListStartLine(containingList, sourceFile, options) + (listIndentsChild ? options.indentSize! : 0); // TODO: GH#18217 } + return Value.Unknown; + } - function getActualIndentationForListStartLine(list: ts.NodeArray, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { - if (!list) { - return Value.Unknown; - } - return findColumnForFirstNonWhitespaceCharacterInLine(sourceFile.getLineAndCharacterOfPosition(list.pos), sourceFile, options); - } + function deriveActualIndentationFromList(list: readonly ts.Node[], index: number, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + ts.Debug.assert(index >= 0 && index < list.length); + const node = list[index]; - function getActualIndentationForListItem(node: ts.Node, sourceFile: ts.SourceFile, options: ts.EditorSettings, listIndentsChild: boolean): number { - if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList) { - // VariableDeclarationList has no wrapping tokens - return Value.Unknown; + // 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 === ts.SyntaxKind.CommaToken) { + continue; } - 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 + // 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); } - return Value.Unknown; - } - function deriveActualIndentationFromList(list: readonly ts.Node[], index: number, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { - ts.Debug.assert(index >= 0 && index < list.length); - const node = list[index]; + lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); + } + return Value.Unknown; + } - // 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 === ts.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); - } + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: ts.LineAndCharacter, sourceFile: ts.SourceFile, options: ts.EditorSettings): number { + const lineStart = sourceFile.getPositionOfLineAndCharacter(lineAndCharacter.line, 0); + return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options); + } - lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); + /** + * 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: ts.SourceFileLike, options: ts.EditorSettings) { + let character = 0; + let column = 0; + for (let pos = startPos; pos < endPos; pos++) { + const ch = sourceFile.text.charCodeAt(pos); + if (!ts.isWhiteSpaceSingleLine(ch)) { + break; + } + + if (ch === ts.CharacterCodes.tab) { + column += options.tabSize! + (column % options.tabSize!); } - return Value.Unknown; + else { + column++; + } + + character++; } + return { column, character }; + } - function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: ts.LineAndCharacter, sourceFile: ts.SourceFile, options: ts.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: ts.SourceFileLike, options: ts.EditorSettings) { - let character = 0; - let column = 0; - for (let pos = startPos; pos < endPos; pos++) { - const ch = sourceFile.text.charCodeAt(pos); - if (!ts.isWhiteSpaceSingleLine(ch)) { - break; - } + export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: ts.SourceFileLike, options: ts.EditorSettings): number { + return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; + } - if (ch === ts.CharacterCodes.tab) { - column += options.tabSize! + (column % options.tabSize!); + export function nodeWillIndentChild(settings: ts.FormatCodeSettings, parent: ts.formatting.TextRangeWithKind, child: ts.formatting.TextRangeWithKind | undefined, sourceFile: ts.SourceFileLike | undefined, indentByDefault: boolean): boolean { + const childKind = child ? child.kind : ts.SyntaxKind.Unknown; + + switch (parent.kind) { + case ts.SyntaxKind.ExpressionStatement: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.ArrayLiteralExpression: + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.MappedType: + case ts.SyntaxKind.TupleType: + case ts.SyntaxKind.CaseBlock: + case ts.SyntaxKind.DefaultClause: + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.ReturnStatement: + case ts.SyntaxKind.ConditionalExpression: + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.JsxOpeningElement: + case ts.SyntaxKind.JsxOpeningFragment: + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxExpression: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ParenthesizedType: + case ts.SyntaxKind.TaggedTemplateExpression: + case ts.SyntaxKind.AwaitExpression: + case ts.SyntaxKind.NamedExports: + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.PropertyDeclaration: + return true; + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.BinaryExpression: + if (!settings.indentMultiLineObjectLiteralBeginningOnBlankLine && sourceFile && childKind === ts.SyntaxKind.ObjectLiteralExpression) { // TODO: GH#18217 + return rangeIsOnOneLine(sourceFile, child!); } - else { - column++; + if (parent.kind === ts.SyntaxKind.BinaryExpression && sourceFile && child && childKind === ts.SyntaxKind.JsxElement) { + const parentStartLine = sourceFile.getLineAndCharacterOfPosition(ts.skipTrivia(sourceFile.text, parent.pos)).line; + const childStartLine = sourceFile.getLineAndCharacterOfPosition(ts.skipTrivia(sourceFile.text, child.pos)).line; + return parentStartLine !== childStartLine; } - - character++; - } - return { column, character }; - } - - export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: ts.SourceFileLike, options: ts.EditorSettings): number { - return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; - } - - export function nodeWillIndentChild(settings: ts.FormatCodeSettings, parent: ts.formatting.TextRangeWithKind, child: ts.formatting.TextRangeWithKind | undefined, sourceFile: ts.SourceFileLike | undefined, indentByDefault: boolean): boolean { - const childKind = child ? child.kind : ts.SyntaxKind.Unknown; - - switch (parent.kind) { - case ts.SyntaxKind.ExpressionStatement: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ArrayLiteralExpression: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.MappedType: - case ts.SyntaxKind.TupleType: - case ts.SyntaxKind.CaseBlock: - case ts.SyntaxKind.DefaultClause: - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.ReturnStatement: - case ts.SyntaxKind.ConditionalExpression: - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.JsxOpeningElement: - case ts.SyntaxKind.JsxOpeningFragment: - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ParenthesizedType: - case ts.SyntaxKind.TaggedTemplateExpression: - case ts.SyntaxKind.AwaitExpression: - case ts.SyntaxKind.NamedExports: - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.PropertyDeclaration: + if (parent.kind !== ts.SyntaxKind.BinaryExpression) { return true; - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.BinaryExpression: - if (!settings.indentMultiLineObjectLiteralBeginningOnBlankLine && sourceFile && childKind === ts.SyntaxKind.ObjectLiteralExpression) { // TODO: GH#18217 - return rangeIsOnOneLine(sourceFile, child!); - } - if (parent.kind === ts.SyntaxKind.BinaryExpression && sourceFile && child && childKind === ts.SyntaxKind.JsxElement) { - const parentStartLine = sourceFile.getLineAndCharacterOfPosition(ts.skipTrivia(sourceFile.text, parent.pos)).line; - const childStartLine = sourceFile.getLineAndCharacterOfPosition(ts.skipTrivia(sourceFile.text, child.pos)).line; - return parentStartLine !== childStartLine; - } - if (parent.kind !== ts.SyntaxKind.BinaryExpression) { - return true; - } - break; - case ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return childKind !== ts.SyntaxKind.Block; - case ts.SyntaxKind.ArrowFunction: - if (sourceFile && childKind === ts.SyntaxKind.ParenthesizedExpression) { - return rangeIsOnOneLine(sourceFile, child!); - } - return childKind !== ts.SyntaxKind.Block; - case ts.SyntaxKind.ExportDeclaration: - return childKind !== ts.SyntaxKind.NamedExports; - case ts.SyntaxKind.ImportDeclaration: - return childKind !== ts.SyntaxKind.ImportClause || - (!!(child as ts.ImportClause).namedBindings && (child as ts.ImportClause).namedBindings!.kind !== ts.SyntaxKind.NamedImports); - case ts.SyntaxKind.JsxElement: - return childKind !== ts.SyntaxKind.JsxClosingElement; - case ts.SyntaxKind.JsxFragment: - return childKind !== ts.SyntaxKind.JsxClosingFragment; - case ts.SyntaxKind.IntersectionType: - case ts.SyntaxKind.UnionType: - if (childKind === ts.SyntaxKind.TypeLiteral || childKind === ts.SyntaxKind.TupleType) { - return false; - } - break; - } - // No explicit rule for given nodes so the result will follow the default value argument - return indentByDefault; - } - - function isControlFlowEndingStatement(kind: ts.SyntaxKind, parent: ts.formatting.TextRangeWithKind): boolean { - switch (kind) { - case ts.SyntaxKind.ReturnStatement: - case ts.SyntaxKind.ThrowStatement: - case ts.SyntaxKind.ContinueStatement: - case ts.SyntaxKind.BreakStatement: - return parent.kind !== ts.SyntaxKind.Block; - default: + } + break; + case ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return childKind !== ts.SyntaxKind.Block; + case ts.SyntaxKind.ArrowFunction: + if (sourceFile && childKind === ts.SyntaxKind.ParenthesizedExpression) { + return rangeIsOnOneLine(sourceFile, child!); + } + return childKind !== ts.SyntaxKind.Block; + case ts.SyntaxKind.ExportDeclaration: + return childKind !== ts.SyntaxKind.NamedExports; + case ts.SyntaxKind.ImportDeclaration: + return childKind !== ts.SyntaxKind.ImportClause || + (!!(child as ts.ImportClause).namedBindings && (child as ts.ImportClause).namedBindings!.kind !== ts.SyntaxKind.NamedImports); + case ts.SyntaxKind.JsxElement: + return childKind !== ts.SyntaxKind.JsxClosingElement; + case ts.SyntaxKind.JsxFragment: + return childKind !== ts.SyntaxKind.JsxClosingFragment; + case ts.SyntaxKind.IntersectionType: + case ts.SyntaxKind.UnionType: + if (childKind === ts.SyntaxKind.TypeLiteral || childKind === ts.SyntaxKind.TupleType) { return false; - } + } + break; } + // No explicit rule for given nodes so the result will follow the default value argument + return indentByDefault; + } - /** - * 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: ts.FormatCodeSettings, parent: ts.formatting.TextRangeWithKind, child?: ts.Node, sourceFile?: ts.SourceFileLike, isNextChild = false): boolean { - return nodeWillIndentChild(settings, parent, child, sourceFile, /*indentByDefault*/ false) - && !(isNextChild && child && isControlFlowEndingStatement(child.kind, parent)); + function isControlFlowEndingStatement(kind: ts.SyntaxKind, parent: ts.formatting.TextRangeWithKind): boolean { + switch (kind) { + case ts.SyntaxKind.ReturnStatement: + case ts.SyntaxKind.ThrowStatement: + case ts.SyntaxKind.ContinueStatement: + case ts.SyntaxKind.BreakStatement: + return parent.kind !== ts.SyntaxKind.Block; + default: + return false; } + } - function rangeIsOnOneLine(sourceFile: ts.SourceFileLike, range: ts.formatting.TextRangeWithKind) { - const rangeStart = ts.skipTrivia(sourceFile.text, range.pos); - const startLine = sourceFile.getLineAndCharacterOfPosition(rangeStart).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(range.end).line; - return startLine === endLine; - } + /** + * 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: ts.FormatCodeSettings, parent: ts.formatting.TextRangeWithKind, child?: ts.Node, sourceFile?: ts.SourceFileLike, isNextChild = false): boolean { + return nodeWillIndentChild(settings, parent, child, sourceFile, /*indentByDefault*/ false) + && !(isNextChild && child && isControlFlowEndingStatement(child.kind, parent)); + } + + function rangeIsOnOneLine(sourceFile: ts.SourceFileLike, range: ts.formatting.TextRangeWithKind) { + const rangeStart = ts.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 eeae02d58e67f..03a6def75f97d 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,249 +1,249 @@ /* @internal */ namespace ts { - export function getEditsForFileRename(program: ts.Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, sourceMapper: ts.SourceMapper): readonly ts.FileTextChanges[] { - const useCaseSensitiveFileNames = ts.hostUsesCaseSensitiveFileNames(host); - const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper); - const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper); - return ts.textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => { - updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); - updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName); - }); - } +export function getEditsForFileRename(program: ts.Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: ts.LanguageServiceHost, formatContext: ts.formatting.FormatContext, preferences: ts.UserPreferences, sourceMapper: ts.SourceMapper): readonly ts.FileTextChanges[] { + const useCaseSensitiveFileNames = ts.hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper); + const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper); + return ts.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: ts.GetCanonicalFileName, sourceMapper: ts.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 = ts.tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName); - return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix; - } +/** 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: ts.GetCanonicalFileName, sourceMapper: ts.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 = ts.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: ts.GetCanonicalFileName): string { - const rel = ts.getRelativePathFromFile(a0, b0, getCanonicalFileName); - return combinePathsSafe(ts.getDirectoryPath(a1), rel); - } +// 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: ts.GetCanonicalFileName): string { + const rel = ts.getRelativePathFromFile(a0, b0, getCanonicalFileName); + return combinePathsSafe(ts.getDirectoryPath(a1), rel); +} - function updateTsconfigFiles(program: ts.Program, changeTracker: ts.textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void { - const { configFile } = program.getCompilerOptions(); - if (!configFile) - return; - const configDir = ts.getDirectoryPath(configFile.fileName); - const jsonObjectLiteral = ts.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" || !ts.isArrayLiteralExpression(property.initializer)) - return; - const includes = ts.mapDefined(property.initializer.elements, e => ts.isStringLiteral(e) ? e.text : undefined); - if (includes.length === 0) - return; - const matchers = ts.getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); - // If there isn't some include for this, add a new one. - if (ts.getRegexFromPattern(ts.Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && - !ts.getRegexFromPattern(ts.Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { - changeTracker.insertNodeAfter(configFile, ts.last(property.initializer.elements), ts.factory.createStringLiteral(relativePath(newFileOrDirPath))); - } +function updateTsconfigFiles(program: ts.Program, changeTracker: ts.textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void { + const { configFile } = program.getCompilerOptions(); + if (!configFile) + return; + const configDir = ts.getDirectoryPath(configFile.fileName); + const jsonObjectLiteral = ts.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" || !ts.isArrayLiteralExpression(property.initializer)) return; - } - case "compilerOptions": - forEachProperty(property.initializer, (property, propertyName) => { - const option = ts.getOptionFromName(propertyName); - if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) { - updatePaths(property); - } - else if (propertyName === "paths") { - forEachProperty(property.initializer, (pathsProperty) => { - if (!ts.isArrayLiteralExpression(pathsProperty.initializer)) - return; - for (const e of pathsProperty.initializer.elements) { - tryUpdateString(e); - } - }); - } - }); + const includes = ts.mapDefined(property.initializer.elements, e => ts.isStringLiteral(e) ? e.text : undefined); + if (includes.length === 0) return; + const matchers = ts.getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + // If there isn't some include for this, add a new one. + if (ts.getRegexFromPattern(ts.Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && + !ts.getRegexFromPattern(ts.Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { + changeTracker.insertNodeAfter(configFile, ts.last(property.initializer.elements), ts.factory.createStringLiteral(relativePath(newFileOrDirPath))); + } + return; } - }); - - function updatePaths(property: ts.PropertyAssignment): boolean { - const elements = ts.isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; - let foundExactMatch = false; - for (const element of elements) { - foundExactMatch = tryUpdateString(element) || foundExactMatch; - } - return foundExactMatch; + case "compilerOptions": + forEachProperty(property.initializer, (property, propertyName) => { + const option = ts.getOptionFromName(propertyName); + if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) { + updatePaths(property); + } + else if (propertyName === "paths") { + forEachProperty(property.initializer, (pathsProperty) => { + if (!ts.isArrayLiteralExpression(pathsProperty.initializer)) + return; + for (const e of pathsProperty.initializer.elements) { + tryUpdateString(e); + } + }); + } + }); + return; } + }); - function tryUpdateString(element: ts.Expression): boolean { - if (!ts.isStringLiteral(element)) - return false; - const elementFileName = combinePathsSafe(configDir, element.text); + function updatePaths(property: ts.PropertyAssignment): boolean { + const elements = ts.isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; + let foundExactMatch = false; + for (const element of elements) { + foundExactMatch = tryUpdateString(element) || foundExactMatch; + } + return foundExactMatch; + } - const updated = oldToNew(elementFileName); - if (updated !== undefined) { - changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); - return true; - } + function tryUpdateString(element: ts.Expression): boolean { + if (!ts.isStringLiteral(element)) return false; - } + const elementFileName = combinePathsSafe(configDir, element.text); - function relativePath(path: string): string { - return ts.getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); - } - } - function updateImports(program: ts.Program, changeTracker: ts.textChanges.ChangeTracker, oldToNew: PathUpdater, newToOld: PathUpdater, host: ts.LanguageServiceHost, getCanonicalFileName: ts.GetCanonicalFileName): void { - const allFiles = program.getSourceFiles(); - for (const sourceFile of allFiles) { - const newFromOld = oldToNew(sourceFile.fileName); - const newImportFromPath = newFromOld ?? sourceFile.fileName; - const newImportFromDirectory = ts.getDirectoryPath(newImportFromPath); - - const oldFromNew: string | undefined = newToOld(sourceFile.fileName); - const oldImportFromPath: string = oldFromNew || sourceFile.fileName; - const oldImportFromDirectory = ts.getDirectoryPath(oldImportFromPath); - - const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined; - - updateImportsWorker(sourceFile, changeTracker, referenceText => { - if (!ts.pathIsRelative(referenceText)) - return undefined; - const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); - const newAbsolute = oldToNew(oldAbsolute); - return newAbsolute === undefined ? undefined : ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); - }, importLiteral => { - const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); - // No need to update if it's an ambient module^M - if (importedModuleSymbol?.declarations && importedModuleSymbol.declarations.some(d => ts.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(importLiteral, ts.resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ts.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 && ts.pathIsRelative(importLiteral.text))) - ? ts.moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, getCanonicalFileName(newImportFromPath) as ts.Path, toImport.newFileName, ts.createModuleSpecifierResolutionHost(program, host), importLiteral.text) - : undefined; - }); + const updated = oldToNew(elementFileName); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); + return true; } + return false; } - function combineNormal(pathA: string, pathB: string): string { - return ts.normalizePath(ts.combinePaths(pathA, pathB)); + function relativePath(path: string): string { + return ts.getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); } - function combinePathsSafe(pathA: string, pathB: string): string { - return ts.ensurePathIsNonModuleName(combineNormal(pathA, pathB)); +} +function updateImports(program: ts.Program, changeTracker: ts.textChanges.ChangeTracker, oldToNew: PathUpdater, newToOld: PathUpdater, host: ts.LanguageServiceHost, getCanonicalFileName: ts.GetCanonicalFileName): void { + const allFiles = program.getSourceFiles(); + for (const sourceFile of allFiles) { + const newFromOld = oldToNew(sourceFile.fileName); + const newImportFromPath = newFromOld ?? sourceFile.fileName; + const newImportFromDirectory = ts.getDirectoryPath(newImportFromPath); + + const oldFromNew: string | undefined = newToOld(sourceFile.fileName); + const oldImportFromPath: string = oldFromNew || sourceFile.fileName; + const oldImportFromDirectory = ts.getDirectoryPath(oldImportFromPath); + + const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined; + + updateImportsWorker(sourceFile, changeTracker, referenceText => { + if (!ts.pathIsRelative(referenceText)) + return undefined; + const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); + const newAbsolute = oldToNew(oldAbsolute); + return newAbsolute === undefined ? undefined : ts.ensurePathIsNonModuleName(ts.getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); + }, importLiteral => { + const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); + // No need to update if it's an ambient module^M + if (importedModuleSymbol?.declarations && importedModuleSymbol.declarations.some(d => ts.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(importLiteral, ts.resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ts.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 && ts.pathIsRelative(importLiteral.text))) + ? ts.moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, getCanonicalFileName(newImportFromPath) as ts.Path, toImport.newFileName, ts.createModuleSpecifierResolutionHost(program, host), importLiteral.text) + : undefined; + }); } +} - interface ToImport { - readonly newFileName: string; - /** True if the imported file was renamed. */ - readonly updated: boolean; +function combineNormal(pathA: string, pathB: string): string { + return ts.normalizePath(ts.combinePaths(pathA, pathB)); +} +function combinePathsSafe(pathA: string, pathB: string): string { + return ts.ensurePathIsNonModuleName(combineNormal(pathA, pathB)); +} + +interface ToImport { + readonly newFileName: string; + /** True if the imported file was renamed. */ + readonly updated: boolean; +} +function getSourceFileToImport(importedModuleSymbol: ts.Symbol | undefined, importLiteral: ts.StringLiteralLike, importingSourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, oldToNew: PathUpdater): ToImport | undefined { + if (importedModuleSymbol) { + // `find` should succeed because we checked for ambient modules before calling this function. + const oldFileName = ts.find(importedModuleSymbol.declarations!, ts.isSourceFile)!.fileName; + const newFileName = oldToNew(oldFileName); + return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true }; } - function getSourceFileToImport(importedModuleSymbol: ts.Symbol | undefined, importLiteral: ts.StringLiteralLike, importingSourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, oldToNew: PathUpdater): ToImport | undefined { - if (importedModuleSymbol) { - // `find` should succeed because we checked for ambient modules before calling this function. - const oldFileName = ts.find(importedModuleSymbol.declarations!, ts.isSourceFile)!.fileName; - const newFileName = oldToNew(oldFileName); - return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true }; - } - else { - const mode = ts.getModeForUsageLocation(importingSourceFile, importLiteral); - const resolved = host.resolveModuleNames - ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode) - : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode); - return getSourceFileToImportFromResolved(importLiteral, resolved, oldToNew, program.getSourceFiles()); - } + else { + const mode = ts.getModeForUsageLocation(importingSourceFile, importLiteral); + const resolved = host.resolveModuleNames + ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode) + : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode); + return getSourceFileToImportFromResolved(importLiteral, resolved, oldToNew, program.getSourceFiles()); } +} - function getSourceFileToImportFromResolved(importLiteral: ts.StringLiteralLike, resolved: ts.ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly ts.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 getSourceFileToImportFromResolved(importLiteral: ts.StringLiteralLike, resolved: ts.ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly ts.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; - // Then failed lookups that are in the list of sources - const result = ts.forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting) - // Then failed lookups except package.json since we dont want to touch them (only included ts/js files). - // At this point, the confidence level of this fix being correct is too low to change bare specifiers or absolute paths. - || ts.pathIsRelative(importLiteral.text) && ts.forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson); + // First try resolved module + if (resolved.resolvedModule) { + const result = tryChange(resolved.resolvedModule.resolvedFileName); 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 && ts.find(sourceFiles, src => src.fileName === newFileName) - ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined; - } - - function tryChangeWithIgnoringPackageJson(oldFileName: string) { - return !ts.endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined; - } + // Then failed lookups that are in the list of sources + const result = ts.forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting) + // Then failed lookups except package.json since we dont want to touch them (only included ts/js files). + // At this point, the confidence level of this fix being correct is too low to change bare specifiers or absolute paths. + || ts.pathIsRelative(importLiteral.text) && ts.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 && ts.find(sourceFiles, src => src.fileName === newFileName) + ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined; + } - function tryChange(oldFileName: string) { - const newFileName = oldToNew(oldFileName); - return newFileName && { newFileName, updated: true }; - } + function tryChangeWithIgnoringPackageJson(oldFileName: string) { + return !ts.endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined; } - function updateImportsWorker(sourceFile: ts.SourceFile, changeTracker: ts.textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: ts.StringLiteralLike) => string | undefined) { - for (const ref of sourceFile.referencedFiles || ts.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 tryChange(oldFileName: string) { + const newFileName = oldToNew(oldFileName); + return newFileName && { newFileName, updated: true }; + } +} - for (const importStringLiteral of sourceFile.imports) { - const updated = updateImport(importStringLiteral); - if (updated !== undefined && updated !== importStringLiteral.text) - changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); - } +function updateImportsWorker(sourceFile: ts.SourceFile, changeTracker: ts.textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: ts.StringLiteralLike) => string | undefined) { + for (const ref of sourceFile.referencedFiles || ts.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 createStringRange(node: ts.StringLiteralLike, sourceFile: ts.SourceFileLike): ts.TextRange { - return ts.createRange(node.getStart(sourceFile) + 1, node.end - 1); + for (const importStringLiteral of sourceFile.imports) { + const updated = updateImport(importStringLiteral); + if (updated !== undefined && updated !== importStringLiteral.text) + changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); } +} - function forEachProperty(objectLiteral: ts.Expression, cb: (property: ts.PropertyAssignment, propertyName: string) => void) { - if (!ts.isObjectLiteralExpression(objectLiteral)) - return; - for (const property of objectLiteral.properties) { - if (ts.isPropertyAssignment(property) && ts.isStringLiteral(property.name)) { - cb(property, property.name.text); - } +function createStringRange(node: ts.StringLiteralLike, sourceFile: ts.SourceFileLike): ts.TextRange { + return ts.createRange(node.getStart(sourceFile) + 1, node.end - 1); +} + +function forEachProperty(objectLiteral: ts.Expression, cb: (property: ts.PropertyAssignment, propertyName: string) => void) { + if (!ts.isObjectLiteralExpression(objectLiteral)) + return; + for (const property of objectLiteral.properties) { + if (ts.isPropertyAssignment(property) && ts.isStringLiteral(property.name)) { + cb(property, property.name.text); } } } +} diff --git a/src/services/globalThisShim.ts b/src/services/globalThisShim.ts index 28c009422c4a3..24af65aafd735 100644 --- a/src/services/globalThisShim.ts +++ b/src/services/globalThisShim.ts @@ -57,4 +57,4 @@ if (typeof process === "undefined" || process.browser) { //@ts-ignore globalThis.toolsVersion = ts.versionMajorMinor; -} \ No newline at end of file +} diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index 0e4819dfb6fe2..449c66b69927c 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -1,513 +1,513 @@ /* @internal */ namespace ts.GoToDefinition { - export function getDefinitionAtPosition(program: ts.Program, sourceFile: ts.SourceFile, position: number, searchOtherFilesOnly?: boolean, stopAtAlias?: boolean): readonly ts.DefinitionInfo[] | undefined { - const resolvedRef = getReferenceAtPosition(sourceFile, position, program); - const fileReferenceDefinition = resolvedRef && [getDefinitionInfoForFileReference(resolvedRef.reference.fileName, resolvedRef.fileName, resolvedRef.unverified)] || ts.emptyArray; - if (resolvedRef?.file) { - // If `file` is missing, do a symbol-based lookup as well - return fileReferenceDefinition; - } +export function getDefinitionAtPosition(program: ts.Program, sourceFile: ts.SourceFile, position: number, searchOtherFilesOnly?: boolean, stopAtAlias?: boolean): readonly ts.DefinitionInfo[] | undefined { + const resolvedRef = getReferenceAtPosition(sourceFile, position, program); + const fileReferenceDefinition = resolvedRef && [getDefinitionInfoForFileReference(resolvedRef.reference.fileName, resolvedRef.fileName, resolvedRef.unverified)] || ts.emptyArray; + if (resolvedRef?.file) { + // If `file` is missing, do a symbol-based lookup as well + return fileReferenceDefinition; + } - const node = ts.getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } + const node = ts.getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + return undefined; + } - const { parent } = node; - const typeChecker = program.getTypeChecker(); + const { parent } = node; + const typeChecker = program.getTypeChecker(); - if (node.kind === ts.SyntaxKind.OverrideKeyword || (ts.isJSDocOverrideTag(node) && ts.rangeContainsPosition(node.tagName, position))) { - return getDefinitionFromOverriddenMember(typeChecker, node) || ts.emptyArray; - } + if (node.kind === ts.SyntaxKind.OverrideKeyword || (ts.isJSDocOverrideTag(node) && ts.rangeContainsPosition(node.tagName, position))) { + return getDefinitionFromOverriddenMember(typeChecker, node) || ts.emptyArray; + } - // Labels - if (ts.isJumpStatementTarget(node)) { - const label = ts.getTargetLabel(node.parent, node.text); - return label ? [createDefinitionInfoFromName(typeChecker, label, ts.ScriptElementKind.label, node.text, /*containerName*/ undefined!)] : undefined; // TODO: GH#18217 - } + // Labels + if (ts.isJumpStatementTarget(node)) { + const label = ts.getTargetLabel(node.parent, node.text); + return label ? [createDefinitionInfoFromName(typeChecker, label, ts.ScriptElementKind.label, node.text, /*containerName*/ undefined!)] : undefined; // TODO: GH#18217 + } - if (ts.isStaticModifier(node) && ts.isClassStaticBlockDeclaration(node.parent)) { - const classDecl = node.parent.parent; - const { symbol, failedAliasResolution } = getSymbol(classDecl, typeChecker, stopAtAlias); - - const staticBlocks = ts.filter(classDecl.members, ts.isClassStaticBlockDeclaration); - const containerName = symbol ? typeChecker.symbolToString(symbol, classDecl) : ""; - const sourceFile = node.getSourceFile(); - return ts.map(staticBlocks, staticBlock => { - let { pos } = ts.moveRangePastModifiers(staticBlock); - pos = ts.skipTrivia(sourceFile.text, pos); - return createDefinitionInfoFromName(typeChecker, staticBlock, ts.ScriptElementKind.constructorImplementationElement, "static {}", containerName, /*unverified*/ false, failedAliasResolution, { start: pos, length: "static".length }); - }); - } + if (ts.isStaticModifier(node) && ts.isClassStaticBlockDeclaration(node.parent)) { + const classDecl = node.parent.parent; + const { symbol, failedAliasResolution } = getSymbol(classDecl, typeChecker, stopAtAlias); + + const staticBlocks = ts.filter(classDecl.members, ts.isClassStaticBlockDeclaration); + const containerName = symbol ? typeChecker.symbolToString(symbol, classDecl) : ""; + const sourceFile = node.getSourceFile(); + return ts.map(staticBlocks, staticBlock => { + let { pos } = ts.moveRangePastModifiers(staticBlock); + pos = ts.skipTrivia(sourceFile.text, pos); + return createDefinitionInfoFromName(typeChecker, staticBlock, ts.ScriptElementKind.constructorImplementationElement, "static {}", containerName, /*unverified*/ false, failedAliasResolution, { start: pos, length: "static".length }); + }); + } - let { symbol, failedAliasResolution } = getSymbol(node, typeChecker, stopAtAlias); - let fallbackNode = node; - - if (searchOtherFilesOnly && failedAliasResolution) { - // We couldn't resolve the specific import, try on the module specifier. - const importDeclaration = ts.forEach([node, ...symbol?.declarations || ts.emptyArray], n => ts.findAncestor(n, ts.isAnyImportOrBareOrAccessedRequire)); - const moduleSpecifier = importDeclaration && ts.tryGetModuleSpecifierFromDeclaration(importDeclaration); - if (moduleSpecifier) { - ({ symbol, failedAliasResolution } = getSymbol(moduleSpecifier, typeChecker, stopAtAlias)); - fallbackNode = moduleSpecifier; - } - } + let { symbol, failedAliasResolution } = getSymbol(node, typeChecker, stopAtAlias); + let fallbackNode = node; - if (!symbol && ts.isModuleSpecifierLike(fallbackNode)) { - // We couldn't resolve the module specifier as an external module, but it could - // be that module resolution succeeded but the target was not a module. - const ref = sourceFile.resolvedModules?.get(fallbackNode.text, ts.getModeForUsageLocation(sourceFile, fallbackNode)); - if (ref) { - return [{ - name: fallbackNode.text, - fileName: ref.resolvedFileName, - containerName: undefined!, - containerKind: undefined!, - kind: ts.ScriptElementKind.scriptElement, - textSpan: ts.createTextSpan(0, 0), - failedAliasResolution, - isAmbient: ts.isDeclarationFileName(ref.resolvedFileName), - unverified: fallbackNode !== node, - }]; - } + if (searchOtherFilesOnly && failedAliasResolution) { + // We couldn't resolve the specific import, try on the module specifier. + const importDeclaration = ts.forEach([node, ...symbol?.declarations || ts.emptyArray], n => ts.findAncestor(n, ts.isAnyImportOrBareOrAccessedRequire)); + const moduleSpecifier = importDeclaration && ts.tryGetModuleSpecifierFromDeclaration(importDeclaration); + if (moduleSpecifier) { + ({ symbol, failedAliasResolution } = getSymbol(moduleSpecifier, typeChecker, stopAtAlias)); + fallbackNode = moduleSpecifier; } + } - // 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 ts.concatenate(fileReferenceDefinition, getDefinitionInfoForIndexSignatures(node, typeChecker)); + if (!symbol && ts.isModuleSpecifierLike(fallbackNode)) { + // We couldn't resolve the module specifier as an external module, but it could + // be that module resolution succeeded but the target was not a module. + const ref = sourceFile.resolvedModules?.get(fallbackNode.text, ts.getModeForUsageLocation(sourceFile, fallbackNode)); + if (ref) { + return [{ + name: fallbackNode.text, + fileName: ref.resolvedFileName, + containerName: undefined!, + containerKind: undefined!, + kind: ts.ScriptElementKind.scriptElement, + textSpan: ts.createTextSpan(0, 0), + failedAliasResolution, + isAmbient: ts.isDeclarationFileName(ref.resolvedFileName), + unverified: fallbackNode !== node, + }]; } + } - if (searchOtherFilesOnly && ts.every(symbol.declarations, d => d.getSourceFile().fileName === sourceFile.fileName)) - return undefined; + // 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 ts.concatenate(fileReferenceDefinition, 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 && !(ts.isJsxOpeningLikeElement(node.parent) && isConstructorLike(calledDeclaration))) { - const sigInfo = createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration, failedAliasResolution); - // 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))) { - return [sigInfo]; - } - else { - const defs = getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution, calledDeclaration) || ts.emptyArray; - // For a 'super()' call, put the signature first, else put the variable first. - return node.kind === ts.SyntaxKind.SuperKeyword ? [sigInfo, ...defs] : [...defs, sigInfo]; - } - } + if (searchOtherFilesOnly && ts.every(symbol.declarations, d => d.getSourceFile().fileName === sourceFile.fileName)) + return undefined; - // 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 === ts.SyntaxKind.ShorthandPropertyAssignment) { - const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - const definitions = shorthandSymbol?.declarations ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node, /*unverified*/ false, failedAliasResolution)) : ts.emptyArray; - return ts.concatenate(definitions, getDefinitionFromObjectLiteralElement(typeChecker, node) || ts.emptyArray); + 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 && !(ts.isJsxOpeningLikeElement(node.parent) && isConstructorLike(calledDeclaration))) { + const sigInfo = createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration, failedAliasResolution); + // 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))) { + return [sigInfo]; } - - // 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 (ts.isPropertyName(node) && ts.isBindingElement(parent) && ts.isObjectBindingPattern(parent.parent) && - (node === (parent.propertyName || parent.name))) { - const name = ts.getNameFromPropertyName(node); - const type = typeChecker.getTypeAtLocation(parent.parent); - return name === undefined ? ts.emptyArray : ts.flatMap(type.isUnion() ? type.types : [type], t => { - const prop = t.getProperty(name); - return prop && getDefinitionFromSymbol(typeChecker, prop, node); - }); + else { + const defs = getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution, calledDeclaration) || ts.emptyArray; + // For a 'super()' call, put the signature first, else put the variable first. + return node.kind === ts.SyntaxKind.SuperKeyword ? [sigInfo, ...defs] : [...defs, sigInfo]; } - - return ts.concatenate(fileReferenceDefinition, getDefinitionFromObjectLiteralElement(typeChecker, node) || getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution)); } - /** - * 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;`. - * Also true for any assignment RHS. - */ - function symbolMatchesSignature(s: ts.Symbol, calledDeclaration: ts.SignatureDeclaration) { - return s === calledDeclaration.symbol - || s === calledDeclaration.symbol.parent - || ts.isAssignmentExpression(calledDeclaration.parent) - || (!ts.isCallLikeExpression(calledDeclaration.parent) && s === calledDeclaration.parent.symbol); + // 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 === ts.SyntaxKind.ShorthandPropertyAssignment) { + const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + const definitions = shorthandSymbol?.declarations ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node, /*unverified*/ false, failedAliasResolution)) : ts.emptyArray; + return ts.concatenate(definitions, getDefinitionFromObjectLiteralElement(typeChecker, node) || ts.emptyArray); } - // 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 + // 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 // } - // function Foo(arg: Props) {} - // Foo( { pr/*1*/op1: 10, prop2: true }) - function getDefinitionFromObjectLiteralElement(typeChecker: ts.TypeChecker, node: ts.Node) { - const element = ts.getContainingObjectLiteralElement(node); - if (element) { - const contextualType = element && typeChecker.getContextualType(element.parent); - if (contextualType) { - return ts.flatMap(ts.getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false), propertySymbol => getDefinitionFromSymbol(typeChecker, propertySymbol, node)); - } - } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(parent) && ts.isObjectBindingPattern(parent.parent) && + (node === (parent.propertyName || parent.name))) { + const name = ts.getNameFromPropertyName(node); + const type = typeChecker.getTypeAtLocation(parent.parent); + return name === undefined ? ts.emptyArray : ts.flatMap(type.isUnion() ? type.types : [type], t => { + const prop = t.getProperty(name); + return prop && getDefinitionFromSymbol(typeChecker, prop, node); + }); } - function getDefinitionFromOverriddenMember(typeChecker: ts.TypeChecker, node: ts.Node) { - const classElement = ts.findAncestor(node, ts.isClassElement); - if (!(classElement && classElement.name)) - return; - const baseDeclaration = ts.findAncestor(classElement, ts.isClassLike); - if (!baseDeclaration) - return; - const baseTypeNode = ts.getEffectiveBaseTypeNode(baseDeclaration); - const baseType = baseTypeNode ? typeChecker.getTypeAtLocation(baseTypeNode) : undefined; - if (!baseType) - return; - const name = ts.unescapeLeadingUnderscores(ts.getTextOfPropertyName(classElement.name)); - const symbol = ts.hasStaticModifier(classElement) - ? typeChecker.getPropertyOfType(typeChecker.getTypeOfSymbolAtLocation(baseType.symbol, baseDeclaration), name) - : typeChecker.getPropertyOfType(baseType, name); - if (!symbol) - return; - - return getDefinitionFromSymbol(typeChecker, symbol, node); - } + return ts.concatenate(fileReferenceDefinition, getDefinitionFromObjectLiteralElement(typeChecker, node) || getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution)); +} - export function getReferenceAtPosition(sourceFile: ts.SourceFile, position: number, program: ts.Program): { - reference: ts.FileReference; - fileName: string; - unverified: boolean; - file?: ts.SourceFile; - } | undefined { - const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position); - if (referencePath) { - const file = program.getSourceFileFromReference(sourceFile, referencePath); - return file && { reference: referencePath, fileName: file.fileName, file, unverified: false }; - } +/** + * 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;`. + * Also true for any assignment RHS. + */ +function symbolMatchesSignature(s: ts.Symbol, calledDeclaration: ts.SignatureDeclaration) { + return s === calledDeclaration.symbol + || s === calledDeclaration.symbol.parent + || ts.isAssignmentExpression(calledDeclaration.parent) + || (!ts.isCallLikeExpression(calledDeclaration.parent) && s === calledDeclaration.parent.symbol); +} - const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); - if (typeReferenceDirective) { - const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName, typeReferenceDirective.resolutionMode || sourceFile.impliedNodeFormat); - const file = reference && program.getSourceFile(reference.resolvedFileName!); // TODO:GH#18217 - return file && { reference: typeReferenceDirective, fileName: file.fileName, file, unverified: false }; +// 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 }) +function getDefinitionFromObjectLiteralElement(typeChecker: ts.TypeChecker, node: ts.Node) { + const element = ts.getContainingObjectLiteralElement(node); + if (element) { + const contextualType = element && typeChecker.getContextualType(element.parent); + if (contextualType) { + return ts.flatMap(ts.getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false), propertySymbol => getDefinitionFromSymbol(typeChecker, propertySymbol, node)); } + } +} - const libReferenceDirective = findReferenceInPosition(sourceFile.libReferenceDirectives, position); - if (libReferenceDirective) { - const file = program.getLibFileFromReference(libReferenceDirective); - return file && { reference: libReferenceDirective, fileName: file.fileName, file, unverified: false }; - } +function getDefinitionFromOverriddenMember(typeChecker: ts.TypeChecker, node: ts.Node) { + const classElement = ts.findAncestor(node, ts.isClassElement); + if (!(classElement && classElement.name)) + return; + const baseDeclaration = ts.findAncestor(classElement, ts.isClassLike); + if (!baseDeclaration) + return; + const baseTypeNode = ts.getEffectiveBaseTypeNode(baseDeclaration); + const baseType = baseTypeNode ? typeChecker.getTypeAtLocation(baseTypeNode) : undefined; + if (!baseType) + return; + const name = ts.unescapeLeadingUnderscores(ts.getTextOfPropertyName(classElement.name)); + const symbol = ts.hasStaticModifier(classElement) + ? typeChecker.getPropertyOfType(typeChecker.getTypeOfSymbolAtLocation(baseType.symbol, baseDeclaration), name) + : typeChecker.getPropertyOfType(baseType, name); + if (!symbol) + return; + + return getDefinitionFromSymbol(typeChecker, symbol, node); +} - if (sourceFile.resolvedModules?.size()) { - const node = ts.getTouchingToken(sourceFile, position); - if (ts.isModuleSpecifierLike(node) && ts.isExternalModuleNameRelative(node.text) && sourceFile.resolvedModules.has(node.text, ts.getModeForUsageLocation(sourceFile, node))) { - const verifiedFileName = sourceFile.resolvedModules.get(node.text, ts.getModeForUsageLocation(sourceFile, node))?.resolvedFileName; - const fileName = verifiedFileName || ts.resolvePath(ts.getDirectoryPath(sourceFile.fileName), node.text); - return { - file: program.getSourceFile(fileName), - fileName, - reference: { - pos: node.getStart(), - end: node.getEnd(), - fileName: node.text - }, - unverified: !verifiedFileName, - }; - } - } +export function getReferenceAtPosition(sourceFile: ts.SourceFile, position: number, program: ts.Program): { + reference: ts.FileReference; + fileName: string; + unverified: boolean; + file?: ts.SourceFile; +} | undefined { + const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position); + if (referencePath) { + const file = program.getSourceFileFromReference(sourceFile, referencePath); + return file && { reference: referencePath, fileName: file.fileName, file, unverified: false }; + } - return undefined; + const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); + if (typeReferenceDirective) { + const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName, typeReferenceDirective.resolutionMode || sourceFile.impliedNodeFormat); + const file = reference && program.getSourceFile(reference.resolvedFileName!); // TODO:GH#18217 + return file && { reference: typeReferenceDirective, fileName: file.fileName, file, unverified: false }; } - /// Goto type - export function getTypeDefinitionAtPosition(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, position: number): readonly ts.DefinitionInfo[] | undefined { - const node = ts.getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } + const libReferenceDirective = findReferenceInPosition(sourceFile.libReferenceDirectives, position); + if (libReferenceDirective) { + const file = program.getLibFileFromReference(libReferenceDirective); + return file && { reference: libReferenceDirective, fileName: file.fileName, file, unverified: false }; + } - if (ts.isImportMeta(node.parent) && node.parent.name === node) { - return definitionFromType(typeChecker.getTypeAtLocation(node.parent), typeChecker, node.parent, /*failedAliasResolution*/ false); + if (sourceFile.resolvedModules?.size()) { + const node = ts.getTouchingToken(sourceFile, position); + if (ts.isModuleSpecifierLike(node) && ts.isExternalModuleNameRelative(node.text) && sourceFile.resolvedModules.has(node.text, ts.getModeForUsageLocation(sourceFile, node))) { + const verifiedFileName = sourceFile.resolvedModules.get(node.text, ts.getModeForUsageLocation(sourceFile, node))?.resolvedFileName; + const fileName = verifiedFileName || ts.resolvePath(ts.getDirectoryPath(sourceFile.fileName), node.text); + return { + file: program.getSourceFile(fileName), + fileName, + reference: { + pos: node.getStart(), + end: node.getEnd(), + fileName: node.text + }, + unverified: !verifiedFileName, + }; } + } - const { symbol, failedAliasResolution } = getSymbol(node, typeChecker, /*stopAtAlias*/ false); - if (!symbol) - return undefined; + return undefined; +} - const typeAtLocation = typeChecker.getTypeOfSymbolAtLocation(symbol, node); - const returnType = tryGetReturnTypeOfFunction(symbol, typeAtLocation, typeChecker); - const fromReturnType = returnType && definitionFromType(returnType, typeChecker, node, failedAliasResolution); - // If a function returns 'void' or some other type with no definition, just return the function definition. - const typeDefinitions = fromReturnType && fromReturnType.length !== 0 ? fromReturnType : definitionFromType(typeAtLocation, typeChecker, node, failedAliasResolution); - return typeDefinitions.length ? typeDefinitions - : !(symbol.flags & ts.SymbolFlags.Value) && symbol.flags & ts.SymbolFlags.Type ? getDefinitionFromSymbol(typeChecker, ts.skipAlias(symbol, typeChecker), node, failedAliasResolution) - : undefined; +/// Goto type +export function getTypeDefinitionAtPosition(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, position: number): readonly ts.DefinitionInfo[] | undefined { + const node = ts.getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + return undefined; } - function definitionFromType(type: ts.Type, checker: ts.TypeChecker, node: ts.Node, failedAliasResolution: boolean | undefined): readonly ts.DefinitionInfo[] { - return ts.flatMap(type.isUnion() && !(type.flags & ts.TypeFlags.Enum) ? type.types : [type], t => t.symbol && getDefinitionFromSymbol(checker, t.symbol, node, failedAliasResolution)); + if (ts.isImportMeta(node.parent) && node.parent.name === node) { + return definitionFromType(typeChecker.getTypeAtLocation(node.parent), typeChecker, node.parent, /*failedAliasResolution*/ false); } - function tryGetReturnTypeOfFunction(symbol: ts.Symbol, type: ts.Type, checker: ts.TypeChecker): ts.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 && ts.isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer === type.symbol.valueDeclaration as ts.Node) { - const sigs = type.getCallSignatures(); - if (sigs.length === 1) - return checker.getReturnTypeOfSignature(ts.first(sigs)); - } + const { symbol, failedAliasResolution } = getSymbol(node, typeChecker, /*stopAtAlias*/ false); + if (!symbol) return undefined; - } - export function getDefinitionAndBoundSpan(program: ts.Program, sourceFile: ts.SourceFile, position: number): ts.DefinitionInfoAndBoundSpan | undefined { - const definitions = getDefinitionAtPosition(program, sourceFile, position); - - if (!definitions || definitions.length === 0) { - return undefined; - } + const typeAtLocation = typeChecker.getTypeOfSymbolAtLocation(symbol, node); + const returnType = tryGetReturnTypeOfFunction(symbol, typeAtLocation, typeChecker); + const fromReturnType = returnType && definitionFromType(returnType, typeChecker, node, failedAliasResolution); + // If a function returns 'void' or some other type with no definition, just return the function definition. + const typeDefinitions = fromReturnType && fromReturnType.length !== 0 ? fromReturnType : definitionFromType(typeAtLocation, typeChecker, node, failedAliasResolution); + return typeDefinitions.length ? typeDefinitions + : !(symbol.flags & ts.SymbolFlags.Value) && symbol.flags & ts.SymbolFlags.Type ? getDefinitionFromSymbol(typeChecker, ts.skipAlias(symbol, typeChecker), node, failedAliasResolution) + : undefined; +} - // Check if position is on triple slash reference. - const comment = findReferenceInPosition(sourceFile.referencedFiles, position) || - findReferenceInPosition(sourceFile.typeReferenceDirectives, position) || - findReferenceInPosition(sourceFile.libReferenceDirectives, position); +function definitionFromType(type: ts.Type, checker: ts.TypeChecker, node: ts.Node, failedAliasResolution: boolean | undefined): readonly ts.DefinitionInfo[] { + return ts.flatMap(type.isUnion() && !(type.flags & ts.TypeFlags.Enum) ? type.types : [type], t => t.symbol && getDefinitionFromSymbol(checker, t.symbol, node, failedAliasResolution)); +} - if (comment) { - return { definitions, textSpan: ts.createTextSpanFromRange(comment) }; - } +function tryGetReturnTypeOfFunction(symbol: ts.Symbol, type: ts.Type, checker: ts.TypeChecker): ts.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 && ts.isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer === type.symbol.valueDeclaration as ts.Node) { + const sigs = type.getCallSignatures(); + if (sigs.length === 1) + return checker.getReturnTypeOfSignature(ts.first(sigs)); + } + return undefined; +} - const node = ts.getTouchingPropertyName(sourceFile, position); - const textSpan = ts.createTextSpan(node.getStart(), node.getWidth()); +export function getDefinitionAndBoundSpan(program: ts.Program, sourceFile: ts.SourceFile, position: number): ts.DefinitionInfoAndBoundSpan | undefined { + const definitions = getDefinitionAtPosition(program, sourceFile, position); - return { definitions, textSpan }; + if (!definitions || definitions.length === 0) { + return undefined; } - // At 'x.foo', see if the type of 'x' has an index signature, and if so find its declarations. - function getDefinitionInfoForIndexSignatures(node: ts.Node, checker: ts.TypeChecker): ts.DefinitionInfo[] | undefined { - return ts.mapDefined(checker.getIndexInfosAtLocation(node), info => info.declaration && createDefinitionFromSignatureDeclaration(checker, info.declaration)); - } + // Check if position is on triple slash reference. + const comment = findReferenceInPosition(sourceFile.referencedFiles, position) || + findReferenceInPosition(sourceFile.typeReferenceDirectives, position) || + findReferenceInPosition(sourceFile.libReferenceDirectives, position); - function getSymbol(node: ts.Node, checker: ts.TypeChecker, stopAtAlias: boolean | 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. - let failedAliasResolution = false; - if (symbol?.declarations && symbol.flags & ts.SymbolFlags.Alias && !stopAtAlias && shouldSkipAlias(node, symbol.declarations[0])) { - const aliased = checker.getAliasedSymbol(symbol); - if (aliased.declarations) { - return { symbol: aliased }; - } - else { - failedAliasResolution = true; - } - } - return { symbol, failedAliasResolution }; + if (comment) { + return { definitions, textSpan: ts.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: ts.Node, declaration: ts.Node): boolean { - if (node.kind !== ts.SyntaxKind.Identifier) { - return false; - } - if (node.parent === declaration) { - return true; + const node = ts.getTouchingPropertyName(sourceFile, position); + const textSpan = ts.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. +function getDefinitionInfoForIndexSignatures(node: ts.Node, checker: ts.TypeChecker): ts.DefinitionInfo[] | undefined { + return ts.mapDefined(checker.getIndexInfosAtLocation(node), info => info.declaration && createDefinitionFromSignatureDeclaration(checker, info.declaration)); +} + +function getSymbol(node: ts.Node, checker: ts.TypeChecker, stopAtAlias: boolean | 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. + let failedAliasResolution = false; + if (symbol?.declarations && symbol.flags & ts.SymbolFlags.Alias && !stopAtAlias && shouldSkipAlias(node, symbol.declarations[0])) { + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + return { symbol: aliased }; } - if (declaration.kind === ts.SyntaxKind.NamespaceImport) { - return false; + else { + failedAliasResolution = true; } - return true; } + return { symbol, failedAliasResolution }; +} - /** - * ```ts - * function f() {} - * f.foo = 0; - * ``` - * - * Here, `f` has two declarations: the function declaration, and the identifier in the next line. - * The latter is a declaration for `f` because it gives `f` the `SymbolFlags.Namespace` meaning so - * it can contain `foo`. However, that declaration is pretty uninteresting and not intuitively a - * "definition" for `f`. Ideally, the question we'd like to answer is "what SymbolFlags does this - * declaration contribute to the symbol for `f`?" If the answer is just `Namespace` and the - * declaration looks like an assignment, that declaration is in no sense a definition for `f`. - * But that information is totally lost during binding and/or symbol merging, so we need to do - * our best to reconstruct it or use other heuristics. This function (and the logic around its - * calling) covers our tests but feels like a hack, and it would be great if someone could come - * up with a more precise definition of what counts as a definition. - */ - function isExpandoDeclaration(node: ts.Declaration): boolean { - if (!ts.isAssignmentDeclaration(node)) - return false; - const containingAssignment = ts.findAncestor(node, p => { - if (ts.isAssignmentExpression(p)) - return true; - if (!ts.isAssignmentDeclaration(p as ts.Declaration)) - return "quit"; - return false; - }) as ts.AssignmentExpression | undefined; - return !!containingAssignment && ts.getAssignmentDeclarationKind(containingAssignment) === ts.AssignmentDeclarationKind.Property; +// 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: ts.Node, declaration: ts.Node): boolean { + if (node.kind !== ts.SyntaxKind.Identifier) { + return false; } - function getDefinitionFromSymbol(typeChecker: ts.TypeChecker, symbol: ts.Symbol, node: ts.Node, failedAliasResolution?: boolean, excludeDeclaration?: ts.Node): ts.DefinitionInfo[] | undefined { - const filteredDeclarations = ts.filter(symbol.declarations, d => d !== excludeDeclaration); - const withoutExpandos = ts.filter(filteredDeclarations, d => !isExpandoDeclaration(d)); - const results = ts.some(withoutExpandos) ? withoutExpandos : filteredDeclarations; - return getConstructSignatureDefinition() || getCallSignatureDefinition() || ts.map(results, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)); - function getConstructSignatureDefinition(): ts.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 & ts.SymbolFlags.Class && !(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) && (ts.isNewExpressionTarget(node) || node.kind === ts.SyntaxKind.ConstructorKeyword)) { - const cls = ts.find(filteredDeclarations!, ts.isClassLike) || ts.Debug.fail("Expected declaration to have at least one class-like declaration"); - return getSignatureDefinition(cls.members, /*selectConstructors*/ true); - } - } - - function getCallSignatureDefinition(): ts.DefinitionInfo[] | undefined { - return ts.isCallOrNewExpressionTarget(node) || ts.isNameOfFunctionDeclaration(node) - ? getSignatureDefinition(filteredDeclarations, /*selectConstructors*/ false) - : undefined; - } + if (node.parent === declaration) { + return true; + } + if (declaration.kind === ts.SyntaxKind.NamespaceImport) { + return false; + } + return true; +} - function getSignatureDefinition(signatureDeclarations: readonly ts.Declaration[] | undefined, selectConstructors: boolean): ts.DefinitionInfo[] | undefined { - if (!signatureDeclarations) { - return undefined; - } - const declarations = signatureDeclarations.filter(selectConstructors ? ts.isConstructorDeclaration : ts.isFunctionLike); - const declarationsWithBody = declarations.filter(d => !!(d as ts.FunctionLikeDeclaration).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(ts.last(declarations), typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)] - : undefined; +/** + * ```ts + * function f() {} + * f.foo = 0; + * ``` + * + * Here, `f` has two declarations: the function declaration, and the identifier in the next line. + * The latter is a declaration for `f` because it gives `f` the `SymbolFlags.Namespace` meaning so + * it can contain `foo`. However, that declaration is pretty uninteresting and not intuitively a + * "definition" for `f`. Ideally, the question we'd like to answer is "what SymbolFlags does this + * declaration contribute to the symbol for `f`?" If the answer is just `Namespace` and the + * declaration looks like an assignment, that declaration is in no sense a definition for `f`. + * But that information is totally lost during binding and/or symbol merging, so we need to do + * our best to reconstruct it or use other heuristics. This function (and the logic around its + * calling) covers our tests but feels like a hack, and it would be great if someone could come + * up with a more precise definition of what counts as a definition. + */ +function isExpandoDeclaration(node: ts.Declaration): boolean { + if (!ts.isAssignmentDeclaration(node)) + return false; + const containingAssignment = ts.findAncestor(node, p => { + if (ts.isAssignmentExpression(p)) + return true; + if (!ts.isAssignmentDeclaration(p as ts.Declaration)) + return "quit"; + return false; + }) as ts.AssignmentExpression | undefined; + return !!containingAssignment && ts.getAssignmentDeclarationKind(containingAssignment) === ts.AssignmentDeclarationKind.Property; +} +function getDefinitionFromSymbol(typeChecker: ts.TypeChecker, symbol: ts.Symbol, node: ts.Node, failedAliasResolution?: boolean, excludeDeclaration?: ts.Node): ts.DefinitionInfo[] | undefined { + const filteredDeclarations = ts.filter(symbol.declarations, d => d !== excludeDeclaration); + const withoutExpandos = ts.filter(filteredDeclarations, d => !isExpandoDeclaration(d)); + const results = ts.some(withoutExpandos) ? withoutExpandos : filteredDeclarations; + return getConstructSignatureDefinition() || getCallSignatureDefinition() || ts.map(results, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)); + function getConstructSignatureDefinition(): ts.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 & ts.SymbolFlags.Class && !(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) && (ts.isNewExpressionTarget(node) || node.kind === ts.SyntaxKind.ConstructorKeyword)) { + const cls = ts.find(filteredDeclarations!, ts.isClassLike) || ts.Debug.fail("Expected declaration to have at least one class-like declaration"); + return getSignatureDefinition(cls.members, /*selectConstructors*/ true); } } - /** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ - export function createDefinitionInfo(declaration: ts.Declaration, checker: ts.TypeChecker, symbol: ts.Symbol, node: ts.Node, unverified?: boolean, failedAliasResolution?: boolean): ts.DefinitionInfo { - const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol - const symbolKind = ts.SymbolDisplay.getSymbolKind(checker, symbol, node); - const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; - return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName, unverified, failedAliasResolution); + function getCallSignatureDefinition(): ts.DefinitionInfo[] | undefined { + return ts.isCallOrNewExpressionTarget(node) || ts.isNameOfFunctionDeclaration(node) + ? getSignatureDefinition(filteredDeclarations, /*selectConstructors*/ false) + : undefined; } - /** Creates a DefinitionInfo directly from the name of a declaration. */ - function createDefinitionInfoFromName(checker: ts.TypeChecker, declaration: ts.Declaration, symbolKind: ts.ScriptElementKind, symbolName: string, containerName: string, unverified?: boolean, failedAliasResolution?: boolean, textSpan?: ts.TextSpan): ts.DefinitionInfo { - const sourceFile = declaration.getSourceFile(); - if (!textSpan) { - const name = ts.getNameOfDeclaration(declaration) || declaration; - textSpan = ts.createTextSpanFromNode(name, sourceFile); + function getSignatureDefinition(signatureDeclarations: readonly ts.Declaration[] | undefined, selectConstructors: boolean): ts.DefinitionInfo[] | undefined { + if (!signatureDeclarations) { + return undefined; } - return { - fileName: sourceFile.fileName, - textSpan, - kind: symbolKind, - name: symbolName, - containerKind: undefined!, - containerName, - ...ts.FindAllReferences.toContextSpan(textSpan, sourceFile, ts.FindAllReferences.getContextNode(declaration)), - isLocal: !isDefinitionVisible(checker, declaration), - isAmbient: !!(declaration.flags & ts.NodeFlags.Ambient), - unverified, - failedAliasResolution, - }; + const declarations = signatureDeclarations.filter(selectConstructors ? ts.isConstructorDeclaration : ts.isFunctionLike); + const declarationsWithBody = declarations.filter(d => !!(d as ts.FunctionLikeDeclaration).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(ts.last(declarations), typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)] + : undefined; } +} - function isDefinitionVisible(checker: ts.TypeChecker, declaration: ts.Declaration): boolean { - if (checker.isDeclarationVisible(declaration)) - return true; - if (!declaration.parent) - return false; +/** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ +export function createDefinitionInfo(declaration: ts.Declaration, checker: ts.TypeChecker, symbol: ts.Symbol, node: ts.Node, unverified?: boolean, failedAliasResolution?: boolean): ts.DefinitionInfo { + const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol + const symbolKind = ts.SymbolDisplay.getSymbolKind(checker, symbol, node); + const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; + return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName, unverified, failedAliasResolution); +} - // Variable initializers are visible if variable is visible - if (ts.hasInitializer(declaration.parent) && declaration.parent.initializer === declaration) - return isDefinitionVisible(checker, declaration.parent as ts.Declaration); +/** Creates a DefinitionInfo directly from the name of a declaration. */ +function createDefinitionInfoFromName(checker: ts.TypeChecker, declaration: ts.Declaration, symbolKind: ts.ScriptElementKind, symbolName: string, containerName: string, unverified?: boolean, failedAliasResolution?: boolean, textSpan?: ts.TextSpan): ts.DefinitionInfo { + const sourceFile = declaration.getSourceFile(); + if (!textSpan) { + const name = ts.getNameOfDeclaration(declaration) || declaration; + textSpan = ts.createTextSpanFromNode(name, sourceFile); + } + return { + fileName: sourceFile.fileName, + textSpan, + kind: symbolKind, + name: symbolName, + containerKind: undefined!, + containerName, + ...ts.FindAllReferences.toContextSpan(textSpan, sourceFile, ts.FindAllReferences.getContextNode(declaration)), + isLocal: !isDefinitionVisible(checker, declaration), + isAmbient: !!(declaration.flags & ts.NodeFlags.Ambient), + unverified, + failedAliasResolution, + }; +} - // Handle some exceptions here like arrow function, members of class and object literal expression which are technically not visible but we want the definition to be determined by its parent - switch (declaration.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: - // Private/protected properties/methods are not visible - if (ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Private)) - return false; - // Public properties/methods are visible if its parents are visible, so: - // falls through - - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - return isDefinitionVisible(checker, declaration.parent as ts.Declaration); - default: +function isDefinitionVisible(checker: ts.TypeChecker, declaration: ts.Declaration): boolean { + if (checker.isDeclarationVisible(declaration)) + return true; + if (!declaration.parent) + return false; + + // Variable initializers are visible if variable is visible + if (ts.hasInitializer(declaration.parent) && declaration.parent.initializer === declaration) + return isDefinitionVisible(checker, declaration.parent as ts.Declaration); + + // Handle some exceptions here like arrow function, members of class and object literal expression which are technically not visible but we want the definition to be determined by its parent + switch (declaration.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: + // Private/protected properties/methods are not visible + if (ts.hasEffectiveModifier(declaration, ts.ModifierFlags.Private)) return false; - } + // Public properties/methods are visible if its parents are visible, so: + // falls through + + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + return isDefinitionVisible(checker, declaration.parent as ts.Declaration); + default: + return false; } +} - function createDefinitionFromSignatureDeclaration(typeChecker: ts.TypeChecker, decl: ts.SignatureDeclaration, failedAliasResolution?: boolean): ts.DefinitionInfo { - return createDefinitionInfo(decl, typeChecker, decl.symbol, decl, /*unverified*/ false, failedAliasResolution); - } +function createDefinitionFromSignatureDeclaration(typeChecker: ts.TypeChecker, decl: ts.SignatureDeclaration, failedAliasResolution?: boolean): ts.DefinitionInfo { + return createDefinitionInfo(decl, typeChecker, decl.symbol, decl, /*unverified*/ false, failedAliasResolution); +} - export function findReferenceInPosition(refs: readonly ts.FileReference[], pos: number): ts.FileReference | undefined { - return ts.find(refs, ref => ts.textRangeContainsPositionInclusive(ref, pos)); - } +export function findReferenceInPosition(refs: readonly ts.FileReference[], pos: number): ts.FileReference | undefined { + return ts.find(refs, ref => ts.textRangeContainsPositionInclusive(ref, pos)); +} - function getDefinitionInfoForFileReference(name: string, targetFileName: string, unverified: boolean): ts.DefinitionInfo { - return { - fileName: targetFileName, - textSpan: ts.createTextSpanFromBounds(0, 0), - kind: ts.ScriptElementKind.scriptElement, - name, - containerName: undefined!, - containerKind: undefined!, - unverified, - }; - } +function getDefinitionInfoForFileReference(name: string, targetFileName: string, unverified: boolean): ts.DefinitionInfo { + return { + fileName: targetFileName, + textSpan: ts.createTextSpanFromBounds(0, 0), + kind: ts.ScriptElementKind.scriptElement, + name, + containerName: undefined!, + containerKind: undefined!, + unverified, + }; +} - /** Returns a CallLikeExpression where `node` is the target being invoked. */ - function getAncestorCallLikeExpression(node: ts.Node): ts.CallLikeExpression | undefined { - const target = ts.findAncestor(node, n => !ts.isRightSideOfPropertyAccess(n)); - const callLike = target?.parent; - return callLike && ts.isCallLikeExpression(callLike) && ts.getInvokedExpression(callLike) === target ? callLike : undefined; - } +/** Returns a CallLikeExpression where `node` is the target being invoked. */ +function getAncestorCallLikeExpression(node: ts.Node): ts.CallLikeExpression | undefined { + const target = ts.findAncestor(node, n => !ts.isRightSideOfPropertyAccess(n)); + const callLike = target?.parent; + return callLike && ts.isCallLikeExpression(callLike) && ts.getInvokedExpression(callLike) === target ? callLike : undefined; +} - function tryGetSignatureDeclaration(typeChecker: ts.TypeChecker, node: ts.Node): ts.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 ts.tryCast(signature && signature.declaration, (d): d is ts.SignatureDeclaration => ts.isFunctionLike(d) && !ts.isFunctionTypeNode(d)); - } +function tryGetSignatureDeclaration(typeChecker: ts.TypeChecker, node: ts.Node): ts.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 ts.tryCast(signature && signature.declaration, (d): d is ts.SignatureDeclaration => ts.isFunctionLike(d) && !ts.isFunctionTypeNode(d)); +} - function isConstructorLike(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.ConstructSignature: - return true; - default: - return false; - } +function isConstructorLike(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructorType: + case ts.SyntaxKind.ConstructSignature: + return true; + default: + return false; } } +} diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 2ed6b410ee3a5..01ea30fe1685a 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -1,716 +1,716 @@ /* 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 [ - ts.Identifier, - ts.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 (ts.Identifier | ts.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 ts.SourceFile[]; - } - export type ImportTracker = (exportSymbol: ts.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 ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, checker: ts.TypeChecker, cancellationToken: ts.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) }; - }; - } +export interface ImportsResult { + /** For every import of the symbol, the location and local symbol for the import. */ + importSearches: readonly [ + ts.Identifier, + ts.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 (ts.Identifier | ts.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 ts.SourceFile[]; +} +export type ImportTracker = (exportSymbol: ts.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 ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, checker: ts.TypeChecker, cancellationToken: ts.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: ts.Symbol; - exportKind: ExportKind; - } +/** Info about an exported symbol to perform recursive search on. */ +export interface ExportInfo { + exportingModuleSymbol: ts.Symbol; + exportKind: ExportKind; +} - export const enum ExportKind { - Named, - Default, - ExportEquals - } - export const enum ImportExport { - Import, - Export - } - interface AmbientModuleDeclaration extends ts.ModuleDeclaration { - body?: ts.ModuleBlock; - } - type SourceFileLike = ts.SourceFile | AmbientModuleDeclaration; - // Identifier for the case of `const x = require("y")`. - type Importer = ts.AnyImportOrReExport | ts.ValidImportTypeNode | ts.Identifier; - type ImporterOrCallExpression = Importer | ts.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 ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, allDirectImports: ts.ESMap, { exportingModuleSymbol, exportKind }: ExportInfo, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined): { - directImports: Importer[]; - indirectUsers: readonly ts.SourceFile[]; - } { - const markSeenDirectImport = ts.nodeSeenTracker(); - const markSeenIndirectUser = ts.nodeSeenTracker(); - const directImports: Importer[] = []; - const isAvailableThroughGlobal = !!exportingModuleSymbol.globalExports; - const indirectUserDeclarations: SourceFileLike[] | undefined = isAvailableThroughGlobal ? undefined : []; - - handleDirectImports(exportingModuleSymbol); - - return { directImports, indirectUsers: getIndirectUsers() }; - - function getIndirectUsers(): readonly ts.SourceFile[] { - if (isAvailableThroughGlobal) { - // It has `export as namespace`, so anything could potentially use it. - return sourceFiles; - } +export const enum ExportKind { + Named, + Default, + ExportEquals +} +export const enum ImportExport { + Import, + Export +} +interface AmbientModuleDeclaration extends ts.ModuleDeclaration { + body?: ts.ModuleBlock; +} +type SourceFileLike = ts.SourceFile | AmbientModuleDeclaration; +// Identifier for the case of `const x = require("y")`. +type Importer = ts.AnyImportOrReExport | ts.ValidImportTypeNode | ts.Identifier; +type ImporterOrCallExpression = Importer | ts.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 ts.SourceFile[], sourceFilesSet: ts.ReadonlySet, allDirectImports: ts.ESMap, { exportingModuleSymbol, exportKind }: ExportInfo, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined): { + directImports: Importer[]; + indirectUsers: readonly ts.SourceFile[]; +} { + const markSeenDirectImport = ts.nodeSeenTracker(); + const markSeenIndirectUser = ts.nodeSeenTracker(); + const directImports: Importer[] = []; + const isAvailableThroughGlobal = !!exportingModuleSymbol.globalExports; + const indirectUserDeclarations: SourceFileLike[] | undefined = isAvailableThroughGlobal ? undefined : []; + + handleDirectImports(exportingModuleSymbol); + + return { directImports, indirectUsers: getIndirectUsers() }; + + function getIndirectUsers(): readonly ts.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. - if (exportingModuleSymbol.declarations) { - for (const decl of exportingModuleSymbol.declarations) { - if (ts.isExternalModuleAugmentation(decl) && sourceFilesSet.has(decl.getSourceFile().fileName)) { - addIndirectUser(decl); - } + // Module augmentations may use this module's exports without importing it. + if (exportingModuleSymbol.declarations) { + for (const decl of exportingModuleSymbol.declarations) { + if (ts.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(ts.getSourceFileOfNode); } - function handleDirectImports(exportingModuleSymbol: ts.Symbol): void { - const theseDirectImports = getDirectImports(exportingModuleSymbol); - if (theseDirectImports) { - for (const direct of theseDirectImports) { - if (!markSeenDirectImport(direct)) { - continue; - } + // 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(ts.getSourceFileOfNode); + } - if (cancellationToken) - cancellationToken.throwIfCancellationRequested(); + function handleDirectImports(exportingModuleSymbol: ts.Symbol): void { + const theseDirectImports = getDirectImports(exportingModuleSymbol); + if (theseDirectImports) { + for (const direct of theseDirectImports) { + if (!markSeenDirectImport(direct)) { + continue; + } - switch (direct.kind) { - case ts.SyntaxKind.CallExpression: - if (ts.isImportCall(direct)) { - handleImportCall(direct); - break; - } - if (!isAvailableThroughGlobal) { - const parent = direct.parent; - if (exportKind === ExportKind.ExportEquals && parent.kind === ts.SyntaxKind.VariableDeclaration) { - const { name } = parent as ts.VariableDeclaration; - if (name.kind === ts.SyntaxKind.Identifier) { - directImports.push(name); - break; - } + if (cancellationToken) + cancellationToken.throwIfCancellationRequested(); + + switch (direct.kind) { + case ts.SyntaxKind.CallExpression: + if (ts.isImportCall(direct)) { + handleImportCall(direct); + break; + } + if (!isAvailableThroughGlobal) { + const parent = direct.parent; + if (exportKind === ExportKind.ExportEquals && parent.kind === ts.SyntaxKind.VariableDeclaration) { + const { name } = parent as ts.VariableDeclaration; + if (name.kind === ts.SyntaxKind.Identifier) { + directImports.push(name); + break; } } - break; - - case ts.SyntaxKind.Identifier: // for 'const x = require("y"); - break; // TODO: GH#23879 + } + break; - case ts.SyntaxKind.ImportEqualsDeclaration: - handleNamespaceImport(direct, direct.name, ts.hasSyntacticModifier(direct, ts.ModifierFlags.Export), /*alreadyAddedDirect*/ false); - break; + case ts.SyntaxKind.Identifier: // for 'const x = require("y"); + break; // TODO: GH#23879 - case ts.SyntaxKind.ImportDeclaration: - directImports.push(direct); - const namedBindings = direct.importClause && direct.importClause.namedBindings; - if (namedBindings && namedBindings.kind === ts.SyntaxKind.NamespaceImport) { - handleNamespaceImport(direct, namedBindings.name, /*isReExport*/ false, /*alreadyAddedDirect*/ true); - } - else if (!isAvailableThroughGlobal && ts.isDefaultImport(direct)) { - addIndirectUser(getSourceFileLikeForImportDeclaration(direct)); // Add a check for indirect uses to handle synthetic default imports - } - break; + case ts.SyntaxKind.ImportEqualsDeclaration: + handleNamespaceImport(direct, direct.name, ts.hasSyntacticModifier(direct, ts.ModifierFlags.Export), /*alreadyAddedDirect*/ false); + break; - case ts.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 if (direct.exportClause.kind === ts.SyntaxKind.NamespaceExport) { - // `export * as foo from "foo"` add to indirect uses - addIndirectUser(getSourceFileLikeForImportDeclaration(direct), /** addTransitiveDependencies */ true); - } - 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 ts.SyntaxKind.ImportDeclaration: + directImports.push(direct); + const namedBindings = direct.importClause && direct.importClause.namedBindings; + if (namedBindings && namedBindings.kind === ts.SyntaxKind.NamespaceImport) { + handleNamespaceImport(direct, namedBindings.name, /*isReExport*/ false, /*alreadyAddedDirect*/ true); + } + else if (!isAvailableThroughGlobal && ts.isDefaultImport(direct)) { + addIndirectUser(getSourceFileLikeForImportDeclaration(direct)); // Add a check for indirect uses to handle synthetic default imports + } + break; - case ts.SyntaxKind.ImportType: - // Only check for typeof import('xyz') - if (direct.isTypeOf && !direct.qualifier && isExported(direct)) { - addIndirectUser(direct.getSourceFile(), /** addTransitiveDependencies */ true); - } + case ts.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 if (direct.exportClause.kind === ts.SyntaxKind.NamespaceExport) { + // `export * as foo from "foo"` add to indirect uses + addIndirectUser(getSourceFileLikeForImportDeclaration(direct), /** addTransitiveDependencies */ true); + } + else { + // This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports. directImports.push(direct); - break; + } + break; - default: - ts.Debug.failBadSyntaxKind(direct, "Unexpected import kind."); - } + case ts.SyntaxKind.ImportType: + // Only check for typeof import('xyz') + if (direct.isTypeOf && !direct.qualifier && isExported(direct)) { + addIndirectUser(direct.getSourceFile(), /** addTransitiveDependencies */ true); + } + directImports.push(direct); + break; + + default: + ts.Debug.failBadSyntaxKind(direct, "Unexpected import kind."); } } } + } - function handleImportCall(importCall: ts.ImportCall) { - const top = ts.findAncestor(importCall, isAmbientModuleDeclaration) || importCall.getSourceFile(); - addIndirectUser(top, /** addTransitiveDependencies */ !!isExported(importCall, /** stopAtAmbientModule */ true)); - } + function handleImportCall(importCall: ts.ImportCall) { + const top = ts.findAncestor(importCall, isAmbientModuleDeclaration) || importCall.getSourceFile(); + addIndirectUser(top, /** addTransitiveDependencies */ !!isExported(importCall, /** stopAtAmbientModule */ true)); + } - function isExported(node: ts.Node, stopAtAmbientModule = false) { - return ts.findAncestor(node, node => { - if (stopAtAmbientModule && isAmbientModuleDeclaration(node)) - return "quit"; - return ts.some(node.modifiers, mod => mod.kind === ts.SyntaxKind.ExportKeyword); - }); - } + function isExported(node: ts.Node, stopAtAmbientModule = false) { + return ts.findAncestor(node, node => { + if (stopAtAmbientModule && isAmbientModuleDeclaration(node)) + return "quit"; + return ts.some(node.modifiers, mod => mod.kind === ts.SyntaxKind.ExportKeyword); + }); + } - function handleNamespaceImport(importDeclaration: ts.ImportEqualsDeclaration | ts.ImportDeclaration, name: ts.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: ts.ImportEqualsDeclaration | ts.ImportDeclaration, name: ts.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); + ts.Debug.assert(sourceFileLike.kind === ts.SyntaxKind.SourceFile || sourceFileLike.kind === ts.SyntaxKind.ModuleDeclaration); + if (isReExport || findNamespaceReExports(sourceFileLike, name, checker)) { + addIndirectUser(sourceFileLike, /** addTransitiveDependencies */ true); } - else if (!isAvailableThroughGlobal) { - const sourceFileLike = getSourceFileLikeForImportDeclaration(importDeclaration); - ts.Debug.assert(sourceFileLike.kind === ts.SyntaxKind.SourceFile || sourceFileLike.kind === ts.SyntaxKind.ModuleDeclaration); - if (isReExport || findNamespaceReExports(sourceFileLike, name, checker)) { - addIndirectUser(sourceFileLike, /** addTransitiveDependencies */ true); - } - else { - addIndirectUser(sourceFileLike); - } + else { + addIndirectUser(sourceFileLike); } } + } - /** Adds a module and all of its transitive dependencies as possible indirect users. */ - function addIndirectUser(sourceFileLike: SourceFileLike, addTransitiveDependencies = false): void { - ts.Debug.assert(!isAvailableThroughGlobal); - const isNew = markSeenIndirectUser(sourceFileLike); - if (!isNew) - return; - indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 - - if (!addTransitiveDependencies) - return; - const moduleSymbol = checker.getMergedSymbol(sourceFileLike.symbol); - if (!moduleSymbol) - return; - ts.Debug.assert(!!(moduleSymbol.flags & ts.SymbolFlags.Module)); - const directImports = getDirectImports(moduleSymbol); - if (directImports) { - for (const directImport of directImports) { - if (!ts.isImportTypeNode(directImport)) { - addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), /** addTransitiveDependencies */ true); - } + /** Adds a module and all of its transitive dependencies as possible indirect users. */ + function addIndirectUser(sourceFileLike: SourceFileLike, addTransitiveDependencies = false): void { + ts.Debug.assert(!isAvailableThroughGlobal); + const isNew = markSeenIndirectUser(sourceFileLike); + if (!isNew) + return; + indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 + + if (!addTransitiveDependencies) + return; + const moduleSymbol = checker.getMergedSymbol(sourceFileLike.symbol); + if (!moduleSymbol) + return; + ts.Debug.assert(!!(moduleSymbol.flags & ts.SymbolFlags.Module)); + const directImports = getDirectImports(moduleSymbol); + if (directImports) { + for (const directImport of directImports) { + if (!ts.isImportTypeNode(directImport)) { + addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), /** addTransitiveDependencies */ true); } } } + } - function getDirectImports(moduleSymbol: ts.Symbol): ImporterOrCallExpression[] | undefined { - return allDirectImports.get(ts.getSymbolId(moduleSymbol).toString()); - } + function getDirectImports(moduleSymbol: ts.Symbol): ImporterOrCallExpression[] | undefined { + return allDirectImports.get(ts.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. - */ - function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: ts.Symbol, exportKind: ExportKind, checker: ts.TypeChecker, isForRename: boolean): Pick { - const importSearches: [ - ts.Identifier, - ts.Symbol - ][] = []; - const singleReferences: (ts.Identifier | ts.StringLiteral)[] = []; - function addSearch(location: ts.Identifier, symbol: ts.Symbol): void { - importSearches.push([location, symbol]); +/** + * 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. + */ +function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: ts.Symbol, exportKind: ExportKind, checker: ts.TypeChecker, isForRename: boolean): Pick { + const importSearches: [ + ts.Identifier, + ts.Symbol + ][] = []; + const singleReferences: (ts.Identifier | ts.StringLiteral)[] = []; + function addSearch(location: ts.Identifier, symbol: ts.Symbol): void { + importSearches.push([location, symbol]); + } + + if (directImports) { + for (const decl of directImports) { + handleImport(decl); } + } - if (directImports) { - for (const decl of directImports) { - handleImport(decl); + return { importSearches, singleReferences }; + + function handleImport(decl: Importer): void { + if (decl.kind === ts.SyntaxKind.ImportEqualsDeclaration) { + if (isExternalModuleImportEquals(decl)) { + handleNamespaceImportLike(decl.name); } + return; } - return { importSearches, singleReferences }; + if (decl.kind === ts.SyntaxKind.Identifier) { + handleNamespaceImportLike(decl); + return; + } - function handleImport(decl: Importer): void { - if (decl.kind === ts.SyntaxKind.ImportEqualsDeclaration) { - if (isExternalModuleImportEquals(decl)) { - handleNamespaceImportLike(decl.name); + if (decl.kind === ts.SyntaxKind.ImportType) { + if (decl.qualifier) { + const firstIdentifier = ts.getFirstIdentifier(decl.qualifier); + if (firstIdentifier.escapedText === ts.symbolName(exportSymbol)) { + singleReferences.push(firstIdentifier); } - return; } + else if (exportKind === ExportKind.ExportEquals) { + singleReferences.push(decl.argument.literal); + } + return; + } + + // Ignore if there's a grammar error + if (decl.moduleSpecifier!.kind !== ts.SyntaxKind.StringLiteral) { + return; + } - if (decl.kind === ts.SyntaxKind.Identifier) { - handleNamespaceImportLike(decl); - return; + if (decl.kind === ts.SyntaxKind.ExportDeclaration) { + if (decl.exportClause && ts.isNamedExports(decl.exportClause)) { + searchForNamedImport(decl.exportClause); } + return; + } - if (decl.kind === ts.SyntaxKind.ImportType) { - if (decl.qualifier) { - const firstIdentifier = ts.getFirstIdentifier(decl.qualifier); - if (firstIdentifier.escapedText === ts.symbolName(exportSymbol)) { - singleReferences.push(firstIdentifier); + const { name, namedBindings } = decl.importClause || { name: undefined, namedBindings: undefined }; + + if (namedBindings) { + switch (namedBindings.kind) { + case ts.SyntaxKind.NamespaceImport: + handleNamespaceImportLike(namedBindings.name); + break; + case ts.SyntaxKind.NamedImports: + // 'default' might be accessed as a named import `{ default as foo }`. + if (exportKind === ExportKind.Named || exportKind === ExportKind.Default) { + searchForNamedImport(namedBindings); } - } - else if (exportKind === ExportKind.ExportEquals) { - singleReferences.push(decl.argument.literal); - } - return; + break; + default: + ts.Debug.assertNever(namedBindings); } + } - // Ignore if there's a grammar error - if (decl.moduleSpecifier!.kind !== ts.SyntaxKind.StringLiteral) { - return; - } + // `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 === ts.symbolEscapedNameNoDefault(exportSymbol))) { + const defaultImportAlias = checker.getSymbolAtLocation(name)!; + addSearch(name, defaultImportAlias); + } + } - if (decl.kind === ts.SyntaxKind.ExportDeclaration) { - if (decl.exportClause && ts.isNamedExports(decl.exportClause)) { - searchForNamedImport(decl.exportClause); - } - return; - } + /** + * `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: ts.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)!); + } + } - const { name, namedBindings } = decl.importClause || { name: undefined, namedBindings: undefined }; + function searchForNamedImport(namedBindings: ts.NamedImportsOrExports | undefined): void { + if (!namedBindings) { + return; + } - if (namedBindings) { - switch (namedBindings.kind) { - case ts.SyntaxKind.NamespaceImport: - handleNamespaceImportLike(namedBindings.name); - break; - case ts.SyntaxKind.NamedImports: - // 'default' might be accessed as a named import `{ default as foo }`. - if (exportKind === ExportKind.Named || exportKind === ExportKind.Default) { - searchForNamedImport(namedBindings); - } - break; - default: - ts.Debug.assertNever(namedBindings); - } + for (const element of namedBindings.elements) { + const { name, propertyName } = element; + if (!isNameMatch((propertyName || name).escapedText)) { + continue; } - // `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 === ts.symbolEscapedNameNoDefault(exportSymbol))) { - const defaultImportAlias = checker.getSymbolAtLocation(name)!; - addSearch(name, defaultImportAlias); + 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)!); + } } - } - - /** - * `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: ts.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)!); + else { + const localSymbol = element.kind === ts.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); } } + } - function searchForNamedImport(namedBindings: ts.NamedImportsOrExports | undefined): void { - if (!namedBindings) { - return; - } + function isNameMatch(name: ts.__String): boolean { + // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports + return name === exportSymbol.escapedName || exportKind !== ExportKind.Named && name === ts.InternalSymbolName.Default; + } +} - for (const element of namedBindings.elements) { - const { name, propertyName } = element; - if (!isNameMatch((propertyName || name).escapedText)) { - continue; - } +/** 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: ts.Identifier, checker: ts.TypeChecker): boolean { + const namespaceImportSymbol = checker.getSymbolAtLocation(name); + + return !!forEachPossibleImportOrExportStatement(sourceFileLike, statement => { + if (!ts.isExportDeclaration(statement)) + return; + const { exportClause, moduleSpecifier } = statement; + return !moduleSpecifier && exportClause && ts.isNamedExports(exportClause) && + exportClause.elements.some(element => checker.getExportSpecifierLocalTargetSymbol(element) === namespaceImportSymbol); + }); +} - 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)!); - } +export type ModuleReference = + /** "import" also includes require() calls. */ +{ + kind: "import"; + literal: ts.StringLiteralLike; +} + /** or */ + | { + kind: "reference"; + referencingFile: ts.SourceFile; + ref: ts.FileReference; +}; +export function findModuleReferences(program: ts.Program, sourceFiles: readonly ts.SourceFile[], searchModuleSymbol: ts.Symbol): ModuleReference[] { + const refs: ModuleReference[] = []; + const checker = program.getTypeChecker(); + for (const referencingFile of sourceFiles) { + const searchSourceFile = searchModuleSymbol.valueDeclaration; + if (searchSourceFile?.kind === ts.SyntaxKind.SourceFile) { + for (const ref of referencingFile.referencedFiles) { + if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) { + refs.push({ kind: "reference", referencingFile, ref }); } - else { - const localSymbol = element.kind === ts.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); + } + for (const ref of referencingFile.typeReferenceDirectives) { + const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName, ref.resolutionMode || referencingFile.impliedNodeFormat); + if (referenced !== undefined && referenced.resolvedFileName === (searchSourceFile as ts.SourceFile).fileName) { + refs.push({ kind: "reference", referencingFile, ref }); } } } - function isNameMatch(name: ts.__String): boolean { - // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports - return name === exportSymbol.escapedName || exportKind !== ExportKind.Named && name === ts.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: ts.Identifier, checker: ts.TypeChecker): boolean { - const namespaceImportSymbol = checker.getSymbolAtLocation(name); - - return !!forEachPossibleImportOrExportStatement(sourceFileLike, statement => { - if (!ts.isExportDeclaration(statement)) - return; - const { exportClause, moduleSpecifier } = statement; - return !moduleSpecifier && exportClause && ts.isNamedExports(exportClause) && - exportClause.elements.some(element => checker.getExportSpecifierLocalTargetSymbol(element) === namespaceImportSymbol); + forEachImport(referencingFile, (_importDecl, moduleSpecifier) => { + const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); + if (moduleSymbol === searchModuleSymbol) { + refs.push({ kind: "import", literal: moduleSpecifier }); + } }); } + return refs; +} - export type ModuleReference = - /** "import" also includes require() calls. */ - { - kind: "import"; - literal: ts.StringLiteralLike; - } - /** or */ - | { - kind: "reference"; - referencingFile: ts.SourceFile; - ref: ts.FileReference; - }; - export function findModuleReferences(program: ts.Program, sourceFiles: readonly ts.SourceFile[], searchModuleSymbol: ts.Symbol): ModuleReference[] { - const refs: ModuleReference[] = []; - const checker = program.getTypeChecker(); - for (const referencingFile of sourceFiles) { - const searchSourceFile = searchModuleSymbol.valueDeclaration; - if (searchSourceFile?.kind === ts.SyntaxKind.SourceFile) { - for (const ref of referencingFile.referencedFiles) { - if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) { - refs.push({ kind: "reference", referencingFile, ref }); - } - } - for (const ref of referencingFile.typeReferenceDirectives) { - const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName, ref.resolutionMode || referencingFile.impliedNodeFormat); - if (referenced !== undefined && referenced.resolvedFileName === (searchSourceFile as ts.SourceFile).fileName) { - refs.push({ kind: "reference", referencingFile, ref }); - } +/** Returns a map from a module symbol Id to all import statements that directly reference the module. */ +function getDirectImportsMap(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined): ts.ESMap { + const map = new ts.Map(); + + for (const sourceFile of sourceFiles) { + if (cancellationToken) + cancellationToken.throwIfCancellationRequested(); + forEachImport(sourceFile, (importDecl, moduleSpecifier) => { + const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); + if (moduleSymbol) { + const id = ts.getSymbolId(moduleSymbol).toString(); + let imports = map.get(id); + if (!imports) { + map.set(id, imports = []); } + imports.push(importDecl); } + }); + } - forEachImport(referencingFile, (_importDecl, moduleSpecifier) => { - const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); - if (moduleSymbol === searchModuleSymbol) { - refs.push({ kind: "import", literal: moduleSpecifier }); - } - }); + 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: ts.Statement) => T) { + return ts.forEach(sourceFileLike.kind === ts.SyntaxKind.SourceFile ? sourceFileLike.statements : sourceFileLike.body!.statements, statement => // TODO: GH#18217 + action(statement) || (isAmbientModuleDeclaration(statement) && ts.forEach(statement.body && statement.body.statements, action))); +} + +/** Calls `action` for each import, re-export, or require() in a file. */ +function forEachImport(sourceFile: ts.SourceFile, action: (importStatement: ImporterOrCallExpression, imported: ts.StringLiteralLike) => void): void { + if (sourceFile.externalModuleIndicator || sourceFile.imports !== undefined) { + for (const i of sourceFile.imports) { + action(ts.importFromModuleSpecifier(i), i); } - return refs; } - - /** Returns a map from a module symbol Id to all import statements that directly reference the module. */ - function getDirectImportsMap(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken | undefined): ts.ESMap { - const map = new ts.Map(); - - for (const sourceFile of sourceFiles) { - if (cancellationToken) - cancellationToken.throwIfCancellationRequested(); - forEachImport(sourceFile, (importDecl, moduleSpecifier) => { - const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); - if (moduleSymbol) { - const id = ts.getSymbolId(moduleSymbol).toString(); - let imports = map.get(id); - if (!imports) { - map.set(id, imports = []); + else { + forEachPossibleImportOrExportStatement(sourceFile, statement => { + switch (statement.kind) { + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.ImportDeclaration: { + const decl = statement as ts.ImportDeclaration | ts.ExportDeclaration; + if (decl.moduleSpecifier && ts.isStringLiteral(decl.moduleSpecifier)) { + action(decl, decl.moduleSpecifier); } - imports.push(importDecl); + break; } - }); - } - return map; + case ts.SyntaxKind.ImportEqualsDeclaration: { + const decl = statement as ts.ImportEqualsDeclaration; + if (isExternalModuleImportEquals(decl)) { + action(decl, decl.moduleReference.expression); + } + break; + } + } + }); } +} - /** Iterates over all statements at the top level or in module declarations. Returns the first truthy result. */ - function forEachPossibleImportOrExportStatement(sourceFileLike: SourceFileLike, action: (statement: ts.Statement) => T) { - return ts.forEach(sourceFileLike.kind === ts.SyntaxKind.SourceFile ? sourceFileLike.statements : sourceFileLike.body!.statements, statement => // TODO: GH#18217 - action(statement) || (isAmbientModuleDeclaration(statement) && ts.forEach(statement.body && statement.body.statements, action))); - } +export interface ImportedSymbol { + kind: ImportExport.Import; + symbol: ts.Symbol; +} +export interface ExportedSymbol { + kind: ImportExport.Export; + symbol: ts.Symbol; + exportInfo: ExportInfo; +} - /** Calls `action` for each import, re-export, or require() in a file. */ - function forEachImport(sourceFile: ts.SourceFile, action: (importStatement: ImporterOrCallExpression, imported: ts.StringLiteralLike) => void): void { - if (sourceFile.externalModuleIndicator || sourceFile.imports !== undefined) { - for (const i of sourceFile.imports) { - action(ts.importFromModuleSpecifier(i), i); +/** + * 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: ts.Node, symbol: ts.Symbol, checker: ts.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 === ts.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) && ts.isBinaryExpression(grandparent) + ? getSpecialPropertyExport(grandparent, /*useLhsSymbol*/ false) + : undefined; + } + else { + return exportInfo(symbol.exportSymbol, getExportKindForDeclaration(parent)); } } else { - forEachPossibleImportOrExportStatement(sourceFile, statement => { - switch (statement.kind) { - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ImportDeclaration: { - const decl = statement as ts.ImportDeclaration | ts.ExportDeclaration; - if (decl.moduleSpecifier && ts.isStringLiteral(decl.moduleSpecifier)) { - action(decl, decl.moduleSpecifier); - } - break; + const exportNode = getExportNode(parent, node); + if (exportNode && ts.hasSyntacticModifier(exportNode, ts.ModifierFlags.Export)) { + if (ts.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; } - case ts.SyntaxKind.ImportEqualsDeclaration: { - const decl = statement as ts.ImportEqualsDeclaration; - if (isExternalModuleImportEquals(decl)) { - action(decl, decl.moduleReference.expression); - } - break; - } - } - }); - } - } - - export interface ImportedSymbol { - kind: ImportExport.Import; - symbol: ts.Symbol; - } - export interface ExportedSymbol { - kind: ImportExport.Export; - symbol: ts.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: ts.Node, symbol: ts.Symbol, checker: ts.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 === ts.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) && ts.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 && ts.hasSyntacticModifier(exportNode, ts.ModifierFlags.Export)) { - if (ts.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)); - } - } - else if (ts.isNamespaceExport(parent)) { - return exportInfo(symbol, ExportKind.Named); - } - // If we are in `export = a;` or `export default a;`, `parent` is the export assignment. - else if (ts.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 (ts.isExportAssignment(grandparent)) { - return getExportAssignmentExport(grandparent); - } - // Similar for `module.exports =` and `exports.A =`. - else if (ts.isBinaryExpression(parent)) { - return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true); - } - else if (ts.isBinaryExpression(grandparent)) { - return getSpecialPropertyExport(grandparent, /*useLhsSymbol*/ true); - } - else if (ts.isJSDocTypedefTag(parent)) { - return exportInfo(symbol, ExportKind.Named); - } + else if (ts.isNamespaceExport(parent)) { + return exportInfo(symbol, ExportKind.Named); } - - function getExportAssignmentExport(ex: ts.ExportAssignment): ExportedSymbol | undefined { - // Get the symbol for the `export =` node; its parent is the module it's the export of. - if (!ex.symbol.parent) - return undefined; - const exportKind = ex.isExportEquals ? ExportKind.ExportEquals : ExportKind.Default; - return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol: ex.symbol.parent, exportKind } }; + // If we are in `export = a;` or `export default a;`, `parent` is the export assignment. + else if (ts.isExportAssignment(parent)) { + return getExportAssignmentExport(parent); } - - function getSpecialPropertyExport(node: ts.BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined { - let kind: ExportKind; - switch (ts.getAssignmentDeclarationKind(node)) { - case ts.AssignmentDeclarationKind.ExportsProperty: - kind = ExportKind.Named; - break; - case ts.AssignmentDeclarationKind.ModuleExports: - kind = ExportKind.ExportEquals; - break; - default: - return undefined; - } - - const sym = useLhsSymbol ? checker.getSymbolAtLocation(ts.getNameOfAccessExpression(ts.cast(node.left, ts.isAccessExpression))) : symbol; - return sym && exportInfo(sym, kind); + // If we are in `export = class A {};` (or `export = class A {};`) at `A`, `parent.parent` is the export assignment. + else if (ts.isExportAssignment(grandparent)) { + return getExportAssignmentExport(grandparent); + } + // Similar for `module.exports =` and `exports.A =`. + else if (ts.isBinaryExpression(parent)) { + return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true); + } + else if (ts.isBinaryExpression(grandparent)) { + return getSpecialPropertyExport(grandparent, /*useLhsSymbol*/ true); + } + else if (ts.isJSDocTypedefTag(parent)) { + return exportInfo(symbol, ExportKind.Named); } } - 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) + function getExportAssignmentExport(ex: ts.ExportAssignment): ExportedSymbol | undefined { + // Get the symbol for the `export =` node; its parent is the module it's the export of. + if (!ex.symbol.parent) return undefined; + const exportKind = ex.isExportEquals ? ExportKind.ExportEquals : ExportKind.Default; + return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol: ex.symbol.parent, exportKind } }; + } - // 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); + function getSpecialPropertyExport(node: ts.BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined { + let kind: ExportKind; + switch (ts.getAssignmentDeclarationKind(node)) { + case ts.AssignmentDeclarationKind.ExportsProperty: + kind = ExportKind.Named; + break; + case ts.AssignmentDeclarationKind.ModuleExports: + kind = ExportKind.ExportEquals; + break; + default: + return undefined; } - // 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 = ts.symbolEscapedNameNoDefault(importedSymbol); - if (importedName === undefined || importedName === ts.InternalSymbolName.Default || importedName === symbol.escapedName) { - return { kind: ImportExport.Import, symbol: importedSymbol }; - } + const sym = useLhsSymbol ? checker.getSymbolAtLocation(ts.getNameOfAccessExpression(ts.cast(node.left, ts.isAccessExpression))) : symbol; + return sym && exportInfo(sym, kind); } + } - function exportInfo(symbol: ts.Symbol, kind: ExportKind): ExportedSymbol | undefined { - const exportInfo = getExportInfo(symbol, kind, checker); - return exportInfo && { kind: ImportExport.Export, symbol, exportInfo }; + 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); } - // Not meant for use with export specifiers or export assignment. - function getExportKindForDeclaration(node: ts.Node): ExportKind { - return ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) ? ExportKind.Default : ExportKind.Named; + // 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 = ts.symbolEscapedNameNoDefault(importedSymbol); + if (importedName === undefined || importedName === ts.InternalSymbolName.Default || importedName === symbol.escapedName) { + return { kind: ImportExport.Import, symbol: importedSymbol }; } } - function getExportEqualsLocalSymbol(importedSymbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol { - if (importedSymbol.flags & ts.SymbolFlags.Alias) { - return ts.Debug.checkDefined(checker.getImmediateAliasedSymbol(importedSymbol)); - } - - const decl = ts.Debug.checkDefined(importedSymbol.valueDeclaration); - if (ts.isExportAssignment(decl)) { // `export = class {}` - return ts.Debug.checkDefined(decl.expression.symbol); - } - else if (ts.isBinaryExpression(decl)) { // `module.exports = class {}` - return ts.Debug.checkDefined(decl.right.symbol); - } - else if (ts.isSourceFile(decl)) { // json module - return ts.Debug.checkDefined(decl.symbol); - } - return ts.Debug.fail(); + function exportInfo(symbol: ts.Symbol, kind: ExportKind): ExportedSymbol | undefined { + const exportInfo = getExportInfo(symbol, kind, checker); + return exportInfo && { kind: ImportExport.Export, symbol, exportInfo }; } - // 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: ts.Node, node: ts.Node): ts.Node | undefined { - const declaration = ts.isVariableDeclaration(parent) ? parent : ts.isBindingElement(parent) ? ts.walkUpBindingElementsAndPatterns(parent) : undefined; - if (declaration) { - return (parent as ts.VariableDeclaration | ts.BindingElement).name !== node ? undefined : - ts.isCatchClause(declaration.parent) ? undefined : ts.isVariableStatement(declaration.parent.parent) ? declaration.parent.parent : undefined; - } - else { - return parent; - } + // Not meant for use with export specifiers or export assignment. + function getExportKindForDeclaration(node: ts.Node): ExportKind { + return ts.hasSyntacticModifier(node, ts.ModifierFlags.Default) ? ExportKind.Default : ExportKind.Named; } +} - function isNodeImport(node: ts.Node): boolean { - const { parent } = node; - switch (parent.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - return (parent as ts.ImportEqualsDeclaration).name === node && isExternalModuleImportEquals(parent as ts.ImportEqualsDeclaration); - case ts.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 ts.ImportSpecifier).propertyName; - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceImport: - ts.Debug.assert((parent as ts.ImportClause | ts.NamespaceImport).name === node); - return true; - case ts.SyntaxKind.BindingElement: - return ts.isInJSFile(node) && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(parent); - default: - return false; - } +function getExportEqualsLocalSymbol(importedSymbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol { + if (importedSymbol.flags & ts.SymbolFlags.Alias) { + return ts.Debug.checkDefined(checker.getImmediateAliasedSymbol(importedSymbol)); } - export function getExportInfo(exportSymbol: ts.Symbol, exportKind: ExportKind, checker: ts.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 ts.isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined; + const decl = ts.Debug.checkDefined(importedSymbol.valueDeclaration); + if (ts.isExportAssignment(decl)) { // `export = class {}` + return ts.Debug.checkDefined(decl.expression.symbol); + } + else if (ts.isBinaryExpression(decl)) { // `module.exports = class {}` + return ts.Debug.checkDefined(decl.right.symbol); } + else if (ts.isSourceFile(decl)) { // json module + return ts.Debug.checkDefined(decl.symbol); + } + return ts.Debug.fail(); +} - /** If at an export specifier, go to the symbol it refers to. */ - function skipExportSpecifierSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.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 (ts.isExportSpecifier(declaration) && !declaration.propertyName && !declaration.parent.parent.moduleSpecifier) { - return checker.getExportSpecifierLocalTargetSymbol(declaration)!; - } - else if (ts.isPropertyAccessExpression(declaration) && ts.isModuleExportsAccessExpression(declaration.expression) && !ts.isPrivateIdentifier(declaration.name)) { - // Export of form 'module.exports.propName = expr'; - return checker.getSymbolAtLocation(declaration)!; - } - else if (ts.isShorthandPropertyAssignment(declaration) - && ts.isBinaryExpression(declaration.parent.parent) - && ts.getAssignmentDeclarationKind(declaration.parent.parent) === ts.AssignmentDeclarationKind.ModuleExports) { - return checker.getExportSpecifierLocalTargetSymbol(declaration.name)!; - } - } - } - return symbol; +// 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: ts.Node, node: ts.Node): ts.Node | undefined { + const declaration = ts.isVariableDeclaration(parent) ? parent : ts.isBindingElement(parent) ? ts.walkUpBindingElementsAndPatterns(parent) : undefined; + if (declaration) { + return (parent as ts.VariableDeclaration | ts.BindingElement).name !== node ? undefined : + ts.isCatchClause(declaration.parent) ? undefined : ts.isVariableStatement(declaration.parent.parent) ? declaration.parent.parent : undefined; + } + else { + return parent; } +} - function getContainingModuleSymbol(importer: Importer, checker: ts.TypeChecker): ts.Symbol { - return checker.getMergedSymbol(getSourceFileLikeForImportDeclaration(importer).symbol); +function isNodeImport(node: ts.Node): boolean { + const { parent } = node; + switch (parent.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + return (parent as ts.ImportEqualsDeclaration).name === node && isExternalModuleImportEquals(parent as ts.ImportEqualsDeclaration); + case ts.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 ts.ImportSpecifier).propertyName; + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + ts.Debug.assert((parent as ts.ImportClause | ts.NamespaceImport).name === node); + return true; + case ts.SyntaxKind.BindingElement: + return ts.isInJSFile(node) && ts.isVariableDeclarationInitializedToBareOrAccessedRequire(parent); + default: + return false; } +} - function getSourceFileLikeForImportDeclaration(node: ImporterOrCallExpression): SourceFileLike { - if (node.kind === ts.SyntaxKind.CallExpression) { - return node.getSourceFile(); - } +export function getExportInfo(exportSymbol: ts.Symbol, exportKind: ExportKind, checker: ts.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 ts.isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined; +} - const { parent } = node; - if (parent.kind === ts.SyntaxKind.SourceFile) { - return parent as ts.SourceFile; +/** If at an export specifier, go to the symbol it refers to. */ +function skipExportSpecifierSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.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 (ts.isExportSpecifier(declaration) && !declaration.propertyName && !declaration.parent.parent.moduleSpecifier) { + return checker.getExportSpecifierLocalTargetSymbol(declaration)!; + } + else if (ts.isPropertyAccessExpression(declaration) && ts.isModuleExportsAccessExpression(declaration.expression) && !ts.isPrivateIdentifier(declaration.name)) { + // Export of form 'module.exports.propName = expr'; + return checker.getSymbolAtLocation(declaration)!; + } + else if (ts.isShorthandPropertyAssignment(declaration) + && ts.isBinaryExpression(declaration.parent.parent) + && ts.getAssignmentDeclarationKind(declaration.parent.parent) === ts.AssignmentDeclarationKind.ModuleExports) { + return checker.getExportSpecifierLocalTargetSymbol(declaration.name)!; + } } - ts.Debug.assert(parent.kind === ts.SyntaxKind.ModuleBlock); - return ts.cast(parent.parent, isAmbientModuleDeclaration); } + return symbol; +} + +function getContainingModuleSymbol(importer: Importer, checker: ts.TypeChecker): ts.Symbol { + return checker.getMergedSymbol(getSourceFileLikeForImportDeclaration(importer).symbol); +} - function isAmbientModuleDeclaration(node: ts.Node): node is AmbientModuleDeclaration { - return node.kind === ts.SyntaxKind.ModuleDeclaration && (node as ts.ModuleDeclaration).name.kind === ts.SyntaxKind.StringLiteral; +function getSourceFileLikeForImportDeclaration(node: ImporterOrCallExpression): SourceFileLike { + if (node.kind === ts.SyntaxKind.CallExpression) { + return node.getSourceFile(); } - function isExternalModuleImportEquals(eq: ts.ImportEqualsDeclaration): eq is ts.ImportEqualsDeclaration & { - moduleReference: { - expression: ts.StringLiteral; - }; - } { - return eq.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference && eq.moduleReference.expression.kind === ts.SyntaxKind.StringLiteral; + const { parent } = node; + if (parent.kind === ts.SyntaxKind.SourceFile) { + return parent as ts.SourceFile; } + ts.Debug.assert(parent.kind === ts.SyntaxKind.ModuleBlock); + return ts.cast(parent.parent, isAmbientModuleDeclaration); +} + +function isAmbientModuleDeclaration(node: ts.Node): node is AmbientModuleDeclaration { + return node.kind === ts.SyntaxKind.ModuleDeclaration && (node as ts.ModuleDeclaration).name.kind === ts.SyntaxKind.StringLiteral; +} + +function isExternalModuleImportEquals(eq: ts.ImportEqualsDeclaration): eq is ts.ImportEqualsDeclaration & { + moduleReference: { + expression: ts.StringLiteral; + }; +} { + return eq.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference && eq.moduleReference.expression.kind === ts.SyntaxKind.StringLiteral; +} } diff --git a/src/services/inlayHints.ts b/src/services/inlayHints.ts index b5fd1c9747742..da41bc87752b5 100644 --- a/src/services/inlayHints.ts +++ b/src/services/inlayHints.ts @@ -1,322 +1,322 @@ /* @internal */ namespace ts.InlayHints { - const maxHintsLength = 30; +const maxHintsLength = 30; - const leadingParameterNameCommentRegexFactory = (name: string) => { - return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`); - }; +const leadingParameterNameCommentRegexFactory = (name: string) => { + return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`); +}; - function shouldShowParameterNameHints(preferences: ts.UserPreferences) { - return preferences.includeInlayParameterNameHints === "literals" || preferences.includeInlayParameterNameHints === "all"; - } +function shouldShowParameterNameHints(preferences: ts.UserPreferences) { + return preferences.includeInlayParameterNameHints === "literals" || preferences.includeInlayParameterNameHints === "all"; +} - function shouldShowLiteralParameterNameHintsOnly(preferences: ts.UserPreferences) { - return preferences.includeInlayParameterNameHints === "literals"; - } +function shouldShowLiteralParameterNameHintsOnly(preferences: ts.UserPreferences) { + return preferences.includeInlayParameterNameHints === "literals"; +} - export function provideInlayHints(context: ts.InlayHintsContext): ts.InlayHint[] { - const { file, program, span, cancellationToken, preferences } = context; - const sourceFileText = file.text; - const compilerOptions = program.getCompilerOptions(); +export function provideInlayHints(context: ts.InlayHintsContext): ts.InlayHint[] { + const { file, program, span, cancellationToken, preferences } = context; + const sourceFileText = file.text; + const compilerOptions = program.getCompilerOptions(); - const checker = program.getTypeChecker(); - const result: ts.InlayHint[] = []; + const checker = program.getTypeChecker(); + const result: ts.InlayHint[] = []; - visitor(file); - return result; + visitor(file); + return result; - function visitor(node: ts.Node): true | undefined { - if (!node || node.getFullWidth() === 0) { - return; - } + function visitor(node: ts.Node): true | undefined { + if (!node || node.getFullWidth() === 0) { + return; + } - switch (node.kind) { - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.ArrowFunction: - cancellationToken.throwIfCancellationRequested(); - } + switch (node.kind) { + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.ArrowFunction: + cancellationToken.throwIfCancellationRequested(); + } - if (!ts.textSpanIntersectsWith(span, node.pos, node.getFullWidth())) { - return; - } + if (!ts.textSpanIntersectsWith(span, node.pos, node.getFullWidth())) { + return; + } - if (ts.isTypeNode(node)) { - return; - } + if (ts.isTypeNode(node)) { + return; + } - if (preferences.includeInlayVariableTypeHints && ts.isVariableDeclaration(node)) { - visitVariableLikeDeclaration(node); - } - else if (preferences.includeInlayPropertyDeclarationTypeHints && ts.isPropertyDeclaration(node)) { - visitVariableLikeDeclaration(node); - } - else if (preferences.includeInlayEnumMemberValueHints && ts.isEnumMember(node)) { - visitEnumMember(node); - } - else if (shouldShowParameterNameHints(preferences) && (ts.isCallExpression(node) || ts.isNewExpression(node))) { - visitCallOrNewExpression(node); + if (preferences.includeInlayVariableTypeHints && ts.isVariableDeclaration(node)) { + visitVariableLikeDeclaration(node); + } + else if (preferences.includeInlayPropertyDeclarationTypeHints && ts.isPropertyDeclaration(node)) { + visitVariableLikeDeclaration(node); + } + else if (preferences.includeInlayEnumMemberValueHints && ts.isEnumMember(node)) { + visitEnumMember(node); + } + else if (shouldShowParameterNameHints(preferences) && (ts.isCallExpression(node) || ts.isNewExpression(node))) { + visitCallOrNewExpression(node); + } + else { + if (preferences.includeInlayFunctionParameterTypeHints && ts.isFunctionLikeDeclaration(node) && ts.hasContextSensitiveParameters(node)) { + visitFunctionLikeForParameterType(node); } - else { - if (preferences.includeInlayFunctionParameterTypeHints && ts.isFunctionLikeDeclaration(node) && ts.hasContextSensitiveParameters(node)) { - visitFunctionLikeForParameterType(node); - } - if (preferences.includeInlayFunctionLikeReturnTypeHints && isSignatureSupportingReturnAnnotation(node)) { - visitFunctionDeclarationLikeForReturnType(node); - } + if (preferences.includeInlayFunctionLikeReturnTypeHints && isSignatureSupportingReturnAnnotation(node)) { + visitFunctionDeclarationLikeForReturnType(node); } - return ts.forEachChild(node, visitor); } + return ts.forEachChild(node, visitor); + } - function isSignatureSupportingReturnAnnotation(node: ts.Node): node is ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration { - return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node); - } + function isSignatureSupportingReturnAnnotation(node: ts.Node): node is ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration { + return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node); + } - function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) { - result.push({ - text: `${isFirstVariadicArgument ? "..." : ""}${truncation(text, maxHintsLength)}:`, - position, - kind: ts.InlayHintKind.Parameter, - whitespaceAfter: true, - }); - } + function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) { + result.push({ + text: `${isFirstVariadicArgument ? "..." : ""}${truncation(text, maxHintsLength)}:`, + position, + kind: ts.InlayHintKind.Parameter, + whitespaceAfter: true, + }); + } + + function addTypeHints(text: string, position: number) { + result.push({ + text: `: ${truncation(text, maxHintsLength)}`, + position, + kind: ts.InlayHintKind.Type, + whitespaceBefore: true, + }); + } - function addTypeHints(text: string, position: number) { - result.push({ - text: `: ${truncation(text, maxHintsLength)}`, - position, - kind: ts.InlayHintKind.Type, - whitespaceBefore: true, - }); + function addEnumMemberValueHints(text: string, position: number) { + result.push({ + text: `= ${truncation(text, maxHintsLength)}`, + position, + kind: ts.InlayHintKind.Enum, + whitespaceBefore: true, + }); + } + + function visitEnumMember(member: ts.EnumMember) { + if (member.initializer) { + return; } - function addEnumMemberValueHints(text: string, position: number) { - result.push({ - text: `= ${truncation(text, maxHintsLength)}`, - position, - kind: ts.InlayHintKind.Enum, - whitespaceBefore: true, - }); + const enumValue = checker.getConstantValue(member); + if (enumValue !== undefined) { + addEnumMemberValueHints(enumValue.toString(), member.end); } + } - function visitEnumMember(member: ts.EnumMember) { - if (member.initializer) { - return; - } + function isModuleReferenceType(type: ts.Type) { + return type.symbol && (type.symbol.flags & ts.SymbolFlags.Module); + } - const enumValue = checker.getConstantValue(member); - if (enumValue !== undefined) { - addEnumMemberValueHints(enumValue.toString(), member.end); - } + function visitVariableLikeDeclaration(decl: ts.VariableDeclaration | ts.PropertyDeclaration) { + if (!decl.initializer || ts.isBindingPattern(decl.name)) { + return; } - function isModuleReferenceType(type: ts.Type) { - return type.symbol && (type.symbol.flags & ts.SymbolFlags.Module); + const effectiveTypeAnnotation = ts.getEffectiveTypeAnnotationNode(decl); + if (effectiveTypeAnnotation) { + return; } - function visitVariableLikeDeclaration(decl: ts.VariableDeclaration | ts.PropertyDeclaration) { - if (!decl.initializer || ts.isBindingPattern(decl.name)) { - return; - } - - const effectiveTypeAnnotation = ts.getEffectiveTypeAnnotationNode(decl); - if (effectiveTypeAnnotation) { - return; - } + const declarationType = checker.getTypeAtLocation(decl); + if (isModuleReferenceType(declarationType)) { + return; + } - const declarationType = checker.getTypeAtLocation(decl); - if (isModuleReferenceType(declarationType)) { - return; - } + const typeDisplayString = printTypeInSingleLine(declarationType); + if (typeDisplayString) { + addTypeHints(typeDisplayString, decl.name.end); + } + } - const typeDisplayString = printTypeInSingleLine(declarationType); - if (typeDisplayString) { - addTypeHints(typeDisplayString, decl.name.end); - } + function visitCallOrNewExpression(expr: ts.CallExpression | ts.NewExpression) { + const args = expr.arguments; + if (!args || !args.length) { + return; } - function visitCallOrNewExpression(expr: ts.CallExpression | ts.NewExpression) { - const args = expr.arguments; - if (!args || !args.length) { - return; - } + const candidates: ts.Signature[] = []; + const signature = checker.getResolvedSignatureForSignatureHelp(expr, candidates); + if (!signature || !candidates.length) { + return; + } - const candidates: ts.Signature[] = []; - const signature = checker.getResolvedSignatureForSignatureHelp(expr, candidates); - if (!signature || !candidates.length) { - return; + for (let i = 0; i < args.length; ++i) { + const originalArg = args[i]; + const arg = ts.skipParentheses(originalArg); + if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { + continue; } - for (let i = 0; i < args.length; ++i) { - const originalArg = args[i]; - const arg = ts.skipParentheses(originalArg); - if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { + const identifierNameInfo = checker.getParameterIdentifierNameAtPosition(signature, i); + if (identifierNameInfo) { + const [parameterName, isFirstVariadicArgument] = identifierNameInfo; + const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); + if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { continue; } - const identifierNameInfo = checker.getParameterIdentifierNameAtPosition(signature, i); - if (identifierNameInfo) { - const [parameterName, isFirstVariadicArgument] = identifierNameInfo; - const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); - if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { - continue; - } - - const name = ts.unescapeLeadingUnderscores(parameterName); - if (leadingCommentsContainsParameterName(arg, name)) { - continue; - } - - addParameterHints(name, originalArg.getStart(), isFirstVariadicArgument); + const name = ts.unescapeLeadingUnderscores(parameterName); + if (leadingCommentsContainsParameterName(arg, name)) { + continue; } - } - } - function identifierOrAccessExpressionPostfixMatchesParameterName(expr: ts.Expression, parameterName: ts.__String) { - if (ts.isIdentifier(expr)) { - return expr.text === parameterName; - } - if (ts.isPropertyAccessExpression(expr)) { - return expr.name.text === parameterName; + addParameterHints(name, originalArg.getStart(), isFirstVariadicArgument); } - return false; } + } - function leadingCommentsContainsParameterName(node: ts.Node, name: string) { - if (!ts.isIdentifierText(name, compilerOptions.target, ts.getLanguageVariant(file.scriptKind))) { - return false; - } - - const ranges = ts.getLeadingCommentRanges(sourceFileText, node.pos); - if (!ranges?.length) { - return false; - } - - const regex = leadingParameterNameCommentRegexFactory(name); - return ts.some(ranges, range => regex.test(sourceFileText.substring(range.pos, range.end))); + function identifierOrAccessExpressionPostfixMatchesParameterName(expr: ts.Expression, parameterName: ts.__String) { + if (ts.isIdentifier(expr)) { + return expr.text === parameterName; } - - function isHintableLiteral(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.PrefixUnaryExpression: { - const operand = (node as ts.PrefixUnaryExpression).operand; - return ts.isLiteralExpression(operand) || ts.isIdentifier(operand) && ts.isInfinityOrNaNString(operand.escapedText); - } - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TemplateExpression: - return true; - case ts.SyntaxKind.Identifier: { - const name = (node as ts.Identifier).escapedText; - return isUndefined(name) || ts.isInfinityOrNaNString(name); - } - } - return ts.isLiteralExpression(node); + if (ts.isPropertyAccessExpression(expr)) { + return expr.name.text === parameterName; } + return false; + } - function visitFunctionDeclarationLikeForReturnType(decl: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration) { - if (ts.isArrowFunction(decl)) { - if (!ts.findChildOfKind(decl, ts.SyntaxKind.OpenParenToken, file)) { - return; - } - } + function leadingCommentsContainsParameterName(node: ts.Node, name: string) { + if (!ts.isIdentifierText(name, compilerOptions.target, ts.getLanguageVariant(file.scriptKind))) { + return false; + } - const effectiveTypeAnnotation = ts.getEffectiveReturnTypeNode(decl); - if (effectiveTypeAnnotation || !decl.body) { - return; - } + const ranges = ts.getLeadingCommentRanges(sourceFileText, node.pos); + if (!ranges?.length) { + return false; + } - const signature = checker.getSignatureFromDeclaration(decl); - if (!signature) { - return; - } + const regex = leadingParameterNameCommentRegexFactory(name); + return ts.some(ranges, range => regex.test(sourceFileText.substring(range.pos, range.end))); + } - const returnType = checker.getReturnTypeOfSignature(signature); - if (isModuleReferenceType(returnType)) { - return; + function isHintableLiteral(node: ts.Node) { + switch (node.kind) { + case ts.SyntaxKind.PrefixUnaryExpression: { + const operand = (node as ts.PrefixUnaryExpression).operand; + return ts.isLiteralExpression(operand) || ts.isIdentifier(operand) && ts.isInfinityOrNaNString(operand.escapedText); + } + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateExpression: + return true; + case ts.SyntaxKind.Identifier: { + const name = (node as ts.Identifier).escapedText; + return isUndefined(name) || ts.isInfinityOrNaNString(name); } + } + return ts.isLiteralExpression(node); + } - const typeDisplayString = printTypeInSingleLine(returnType); - if (!typeDisplayString) { + function visitFunctionDeclarationLikeForReturnType(decl: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration) { + if (ts.isArrowFunction(decl)) { + if (!ts.findChildOfKind(decl, ts.SyntaxKind.OpenParenToken, file)) { return; } + } - addTypeHints(typeDisplayString, getTypeAnnotationPosition(decl)); + const effectiveTypeAnnotation = ts.getEffectiveReturnTypeNode(decl); + if (effectiveTypeAnnotation || !decl.body) { + return; } - function getTypeAnnotationPosition(decl: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration) { - const closeParenToken = ts.findChildOfKind(decl, ts.SyntaxKind.CloseParenToken, file); - if (closeParenToken) { - return closeParenToken.end; - } - return decl.parameters.end; + const signature = checker.getSignatureFromDeclaration(decl); + if (!signature) { + return; } - function visitFunctionLikeForParameterType(node: ts.FunctionLikeDeclaration) { - const signature = checker.getSignatureFromDeclaration(node); - if (!signature) { - return; - } + const returnType = checker.getReturnTypeOfSignature(signature); + if (isModuleReferenceType(returnType)) { + return; + } - for (let i = 0; i < node.parameters.length && i < signature.parameters.length; ++i) { - const param = node.parameters[i]; - const effectiveTypeAnnotation = ts.getEffectiveTypeAnnotationNode(param); + const typeDisplayString = printTypeInSingleLine(returnType); + if (!typeDisplayString) { + return; + } - if (effectiveTypeAnnotation) { - continue; - } + addTypeHints(typeDisplayString, getTypeAnnotationPosition(decl)); + } - const typeDisplayString = getParameterDeclarationTypeDisplayString(signature.parameters[i]); - if (!typeDisplayString) { - continue; - } + function getTypeAnnotationPosition(decl: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.GetAccessorDeclaration) { + const closeParenToken = ts.findChildOfKind(decl, ts.SyntaxKind.CloseParenToken, file); + if (closeParenToken) { + return closeParenToken.end; + } + return decl.parameters.end; + } - addTypeHints(typeDisplayString, param.questionToken ? param.questionToken.end : param.name.end); - } + function visitFunctionLikeForParameterType(node: ts.FunctionLikeDeclaration) { + const signature = checker.getSignatureFromDeclaration(node); + if (!signature) { + return; } - function getParameterDeclarationTypeDisplayString(symbol: ts.Symbol) { - const valueDeclaration = symbol.valueDeclaration; - if (!valueDeclaration || !ts.isParameter(valueDeclaration)) { - return undefined; + for (let i = 0; i < node.parameters.length && i < signature.parameters.length; ++i) { + const param = node.parameters[i]; + const effectiveTypeAnnotation = ts.getEffectiveTypeAnnotationNode(param); + + if (effectiveTypeAnnotation) { + continue; } - const signatureParamType = checker.getTypeOfSymbolAtLocation(symbol, valueDeclaration); - if (isModuleReferenceType(signatureParamType)) { - return undefined; + const typeDisplayString = getParameterDeclarationTypeDisplayString(signature.parameters[i]); + if (!typeDisplayString) { + continue; } - return printTypeInSingleLine(signatureParamType); + addTypeHints(typeDisplayString, param.questionToken ? param.questionToken.end : param.name.end); } + } - function truncation(text: string, maxLength: number) { - if (text.length > maxLength) { - return text.substr(0, maxLength - "...".length) + "..."; - } - return text; + function getParameterDeclarationTypeDisplayString(symbol: ts.Symbol) { + const valueDeclaration = symbol.valueDeclaration; + if (!valueDeclaration || !ts.isParameter(valueDeclaration)) { + return undefined; } - function printTypeInSingleLine(type: ts.Type) { - const flags = ts.NodeBuilderFlags.IgnoreErrors | ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope; - const options: ts.PrinterOptions = { removeComments: true }; - const printer = ts.createPrinter(options); - return ts.usingSingleLineStringWriter(writer => { - const typeNode = checker.typeToTypeNode(type, /*enclosingDeclaration*/ undefined, flags, writer); - ts.Debug.assertIsDefined(typeNode, "should always get typenode"); - printer.writeNode(ts.EmitHint.Unspecified, typeNode, /*sourceFile*/ file, writer); - }); + const signatureParamType = checker.getTypeOfSymbolAtLocation(symbol, valueDeclaration); + if (isModuleReferenceType(signatureParamType)) { + return undefined; } - function isUndefined(name: ts.__String) { - return name === "undefined"; + return printTypeInSingleLine(signatureParamType); + } + + function truncation(text: string, maxLength: number) { + if (text.length > maxLength) { + return text.substr(0, maxLength - "...".length) + "..."; } + return text; + } + + function printTypeInSingleLine(type: ts.Type) { + const flags = ts.NodeBuilderFlags.IgnoreErrors | ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope; + const options: ts.PrinterOptions = { removeComments: true }; + const printer = ts.createPrinter(options); + return ts.usingSingleLineStringWriter(writer => { + const typeNode = checker.typeToTypeNode(type, /*enclosingDeclaration*/ undefined, flags, writer); + ts.Debug.assertIsDefined(typeNode, "should always get typenode"); + printer.writeNode(ts.EmitHint.Unspecified, typeNode, /*sourceFile*/ file, writer); + }); } + + function isUndefined(name: ts.__String) { + return name === "undefined"; + } +} } diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index cfeae64c8e125..fcaccfc2a8311 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -1,493 +1,493 @@ /* @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", - "link", - "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: ts.CompletionEntry[]; - let jsDocTagCompletionEntries: ts.CompletionEntry[]; - export function getJsDocCommentsFromDeclarations(declarations: readonly ts.Declaration[], checker?: ts.TypeChecker): ts.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 parts: ts.SymbolDisplayPart[][] = []; - ts.forEachUnique(declarations, declaration => { - for (const jsdoc of getCommentHavingNodes(declaration)) { - const inheritDoc = ts.isJSDoc(jsdoc) && jsdoc.tags && ts.find(jsdoc.tags, t => t.kind === ts.SyntaxKind.JSDocTag && (t.tagName.escapedText === "inheritDoc" || t.tagName.escapedText === "inheritdoc")); - // skip comments containing @typedefs since they're not associated with particular declarations - // Exceptions: - // - @typedefs are themselves declarations with associated comments - // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation - if (jsdoc.comment === undefined && !inheritDoc - || ts.isJSDoc(jsdoc) - && declaration.kind !== ts.SyntaxKind.JSDocTypedefTag && declaration.kind !== ts.SyntaxKind.JSDocCallbackTag - && jsdoc.tags - && jsdoc.tags.some(t => t.kind === ts.SyntaxKind.JSDocTypedefTag || t.kind === ts.SyntaxKind.JSDocCallbackTag) - && !jsdoc.tags.some(t => t.kind === ts.SyntaxKind.JSDocParameterTag || t.kind === ts.SyntaxKind.JSDocReturnTag)) { - continue; - } - let newparts = jsdoc.comment ? getDisplayPartsFromComment(jsdoc.comment, checker) : []; - if (inheritDoc && inheritDoc.comment) { - newparts = newparts.concat(getDisplayPartsFromComment(inheritDoc.comment, checker)); - } - if (!ts.contains(parts, newparts, isIdenticalListOfDisplayParts)) { - parts.push(newparts); - } - } - }); - return ts.flatten(ts.intersperse(parts, [ts.lineBreakPart()])); - } - - function isIdenticalListOfDisplayParts(parts1: ts.SymbolDisplayPart[], parts2: ts.SymbolDisplayPart[]) { - return ts.arraysEqual(parts1, parts2, (p1, p2) => p1.kind === p2.kind && p1.text === p2.text); - } - - function getCommentHavingNodes(declaration: ts.Declaration): readonly (ts.JSDoc | ts.JSDocTag)[] { - switch (declaration.kind) { - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocPropertyTag: - return [declaration as ts.JSDocPropertyTag]; - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocTypedefTag: - return [(declaration as ts.JSDocTypedefTag), (declaration as ts.JSDocTypedefTag).parent]; - default: - return ts.getJSDocCommentsAndTags(declaration); - } - } - - export function getJsDocTagsFromDeclarations(declarations?: ts.Declaration[], checker?: ts.TypeChecker): ts.JSDocTagInfo[] { - // Only collect doc comments from duplicate declarations once. - const infos: ts.JSDocTagInfo[] = []; - ts.forEachUnique(declarations, declaration => { - const tags = ts.getJSDocTags(declaration); +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", + "link", + "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: ts.CompletionEntry[]; +let jsDocTagCompletionEntries: ts.CompletionEntry[]; +export function getJsDocCommentsFromDeclarations(declarations: readonly ts.Declaration[], checker?: ts.TypeChecker): ts.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 parts: ts.SymbolDisplayPart[][] = []; + ts.forEachUnique(declarations, declaration => { + for (const jsdoc of getCommentHavingNodes(declaration)) { + const inheritDoc = ts.isJSDoc(jsdoc) && jsdoc.tags && ts.find(jsdoc.tags, t => t.kind === ts.SyntaxKind.JSDocTag && (t.tagName.escapedText === "inheritDoc" || t.tagName.escapedText === "inheritdoc")); // skip comments containing @typedefs since they're not associated with particular declarations // Exceptions: + // - @typedefs are themselves declarations with associated comments // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation - if (tags.some(t => t.kind === ts.SyntaxKind.JSDocTypedefTag || t.kind === ts.SyntaxKind.JSDocCallbackTag) - && !tags.some(t => t.kind === ts.SyntaxKind.JSDocParameterTag || t.kind === ts.SyntaxKind.JSDocReturnTag)) { - return; + if (jsdoc.comment === undefined && !inheritDoc + || ts.isJSDoc(jsdoc) + && declaration.kind !== ts.SyntaxKind.JSDocTypedefTag && declaration.kind !== ts.SyntaxKind.JSDocCallbackTag + && jsdoc.tags + && jsdoc.tags.some(t => t.kind === ts.SyntaxKind.JSDocTypedefTag || t.kind === ts.SyntaxKind.JSDocCallbackTag) + && !jsdoc.tags.some(t => t.kind === ts.SyntaxKind.JSDocParameterTag || t.kind === ts.SyntaxKind.JSDocReturnTag)) { + continue; } - for (const tag of tags) { - infos.push({ name: tag.tagName.text, text: getCommentDisplayParts(tag, checker) }); + let newparts = jsdoc.comment ? getDisplayPartsFromComment(jsdoc.comment, checker) : []; + if (inheritDoc && inheritDoc.comment) { + newparts = newparts.concat(getDisplayPartsFromComment(inheritDoc.comment, checker)); } - }); - return infos; + if (!ts.contains(parts, newparts, isIdenticalListOfDisplayParts)) { + parts.push(newparts); + } + } + }); + return ts.flatten(ts.intersperse(parts, [ts.lineBreakPart()])); +} + +function isIdenticalListOfDisplayParts(parts1: ts.SymbolDisplayPart[], parts2: ts.SymbolDisplayPart[]) { + return ts.arraysEqual(parts1, parts2, (p1, p2) => p1.kind === p2.kind && p1.text === p2.text); +} + +function getCommentHavingNodes(declaration: ts.Declaration): readonly (ts.JSDoc | ts.JSDocTag)[] { + switch (declaration.kind) { + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocPropertyTag: + return [declaration as ts.JSDocPropertyTag]; + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocTypedefTag: + return [(declaration as ts.JSDocTypedefTag), (declaration as ts.JSDocTypedefTag).parent]; + default: + return ts.getJSDocCommentsAndTags(declaration); } +} - function getDisplayPartsFromComment(comment: string | readonly ts.JSDocComment[], checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { - if (typeof comment === "string") { - return [ts.textPart(comment)]; +export function getJsDocTagsFromDeclarations(declarations?: ts.Declaration[], checker?: ts.TypeChecker): ts.JSDocTagInfo[] { + // Only collect doc comments from duplicate declarations once. + const infos: ts.JSDocTagInfo[] = []; + ts.forEachUnique(declarations, declaration => { + const tags = ts.getJSDocTags(declaration); + // skip comments containing @typedefs since they're not associated with particular declarations + // Exceptions: + // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation + if (tags.some(t => t.kind === ts.SyntaxKind.JSDocTypedefTag || t.kind === ts.SyntaxKind.JSDocCallbackTag) + && !tags.some(t => t.kind === ts.SyntaxKind.JSDocParameterTag || t.kind === ts.SyntaxKind.JSDocReturnTag)) { + return; + } + for (const tag of tags) { + infos.push({ name: tag.tagName.text, text: getCommentDisplayParts(tag, checker) }); } - return ts.flatMap(comment, node => node.kind === ts.SyntaxKind.JSDocText ? [ts.textPart(node.text)] : ts.buildLinkParts(node, checker)) as ts.SymbolDisplayPart[]; + }); + return infos; +} + +function getDisplayPartsFromComment(comment: string | readonly ts.JSDocComment[], checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { + if (typeof comment === "string") { + return [ts.textPart(comment)]; } + return ts.flatMap(comment, node => node.kind === ts.SyntaxKind.JSDocText ? [ts.textPart(node.text)] : ts.buildLinkParts(node, checker)) as ts.SymbolDisplayPart[]; +} - function getCommentDisplayParts(tag: ts.JSDocTag, checker?: ts.TypeChecker): ts.SymbolDisplayPart[] | undefined { - const { comment, kind } = tag; - const namePart = getTagNameDisplayPart(kind); - switch (kind) { - case ts.SyntaxKind.JSDocImplementsTag: - return withNode((tag as ts.JSDocImplementsTag).class); - case ts.SyntaxKind.JSDocAugmentsTag: - return withNode((tag as ts.JSDocAugmentsTag).class); - case ts.SyntaxKind.JSDocTemplateTag: - const templateTag = tag as ts.JSDocTemplateTag; - const displayParts: ts.SymbolDisplayPart[] = []; - if (templateTag.constraint) { - displayParts.push(ts.textPart(templateTag.constraint.getText())); +function getCommentDisplayParts(tag: ts.JSDocTag, checker?: ts.TypeChecker): ts.SymbolDisplayPart[] | undefined { + const { comment, kind } = tag; + const namePart = getTagNameDisplayPart(kind); + switch (kind) { + case ts.SyntaxKind.JSDocImplementsTag: + return withNode((tag as ts.JSDocImplementsTag).class); + case ts.SyntaxKind.JSDocAugmentsTag: + return withNode((tag as ts.JSDocAugmentsTag).class); + case ts.SyntaxKind.JSDocTemplateTag: + const templateTag = tag as ts.JSDocTemplateTag; + const displayParts: ts.SymbolDisplayPart[] = []; + if (templateTag.constraint) { + displayParts.push(ts.textPart(templateTag.constraint.getText())); + } + if (ts.length(templateTag.typeParameters)) { + if (ts.length(displayParts)) { + displayParts.push(ts.spacePart()); } - if (ts.length(templateTag.typeParameters)) { - if (ts.length(displayParts)) { - displayParts.push(ts.spacePart()); + const lastTypeParameter = templateTag.typeParameters[templateTag.typeParameters.length - 1]; + ts.forEach(templateTag.typeParameters, tp => { + displayParts.push(namePart(tp.getText())); + if (lastTypeParameter !== tp) { + displayParts.push(...[ts.punctuationPart(ts.SyntaxKind.CommaToken), ts.spacePart()]); } - const lastTypeParameter = templateTag.typeParameters[templateTag.typeParameters.length - 1]; - ts.forEach(templateTag.typeParameters, tp => { - displayParts.push(namePart(tp.getText())); - if (lastTypeParameter !== tp) { - displayParts.push(...[ts.punctuationPart(ts.SyntaxKind.CommaToken), ts.spacePart()]); - } - }); - } - if (comment) { - displayParts.push(...[ts.spacePart(), ...getDisplayPartsFromComment(comment, checker)]); - } - return displayParts; - case ts.SyntaxKind.JSDocTypeTag: - return withNode((tag as ts.JSDocTypeTag).typeExpression); - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocPropertyTag: - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocSeeTag: - const { name } = tag as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | ts.JSDocSeeTag; - return name ? withNode(name) - : comment === undefined ? undefined - : getDisplayPartsFromComment(comment, checker); - default: - return comment === undefined ? undefined : getDisplayPartsFromComment(comment, checker); - } + }); + } + if (comment) { + displayParts.push(...[ts.spacePart(), ...getDisplayPartsFromComment(comment, checker)]); + } + return displayParts; + case ts.SyntaxKind.JSDocTypeTag: + return withNode((tag as ts.JSDocTypeTag).typeExpression); + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocPropertyTag: + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocSeeTag: + const { name } = tag as ts.JSDocTypedefTag | ts.JSDocCallbackTag | ts.JSDocPropertyTag | ts.JSDocParameterTag | ts.JSDocSeeTag; + return name ? withNode(name) + : comment === undefined ? undefined + : getDisplayPartsFromComment(comment, checker); + default: + return comment === undefined ? undefined : getDisplayPartsFromComment(comment, checker); + } - function withNode(node: ts.Node) { - return addComment(node.getText()); - } + function withNode(node: ts.Node) { + return addComment(node.getText()); + } - function addComment(s: string) { - if (comment) { - if (s.match(/^https?$/)) { - return [ts.textPart(s), ...getDisplayPartsFromComment(comment, checker)]; - } - else { - return [namePart(s), ts.spacePart(), ...getDisplayPartsFromComment(comment, checker)]; - } + function addComment(s: string) { + if (comment) { + if (s.match(/^https?$/)) { + return [ts.textPart(s), ...getDisplayPartsFromComment(comment, checker)]; } else { - return [ts.textPart(s)]; + return [namePart(s), ts.spacePart(), ...getDisplayPartsFromComment(comment, checker)]; } } - } - - function getTagNameDisplayPart(kind: ts.SyntaxKind): (text: string) => ts.SymbolDisplayPart { - switch (kind) { - case ts.SyntaxKind.JSDocParameterTag: - return ts.parameterNamePart; - case ts.SyntaxKind.JSDocPropertyTag: - return ts.propertyNamePart; - case ts.SyntaxKind.JSDocTemplateTag: - return ts.typeParameterNamePart; - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: - return ts.typeAliasNamePart; - default: - return ts.textPart; + else { + return [ts.textPart(s)]; } } +} - export function getJSDocTagNameCompletions(): ts.CompletionEntry[] { - return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = ts.map(jsDocTagNames, tagName => { - return { - name: tagName, - kind: ts.ScriptElementKind.keyword, - kindModifiers: "", - sortText: ts.Completions.SortText.LocationPriority, - }; - })); - } - - export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails; - - export function getJSDocTagCompletions(): ts.CompletionEntry[] { - return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = ts.map(jsDocTagNames, tagName => { - return { - name: `@${tagName}`, - kind: ts.ScriptElementKind.keyword, - kindModifiers: "", - sortText: ts.Completions.SortText.LocationPriority - }; - })); +function getTagNameDisplayPart(kind: ts.SyntaxKind): (text: string) => ts.SymbolDisplayPart { + switch (kind) { + case ts.SyntaxKind.JSDocParameterTag: + return ts.parameterNamePart; + case ts.SyntaxKind.JSDocPropertyTag: + return ts.propertyNamePart; + case ts.SyntaxKind.JSDocTemplateTag: + return ts.typeParameterNamePart; + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + return ts.typeAliasNamePart; + default: + return ts.textPart; } +} - export function getJSDocTagCompletionDetails(name: string): ts.CompletionEntryDetails { +export function getJSDocTagNameCompletions(): ts.CompletionEntry[] { + return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = ts.map(jsDocTagNames, tagName => { return { - name, - kind: ts.ScriptElementKind.unknown, + name: tagName, + kind: ts.ScriptElementKind.keyword, kindModifiers: "", - displayParts: [ts.textPart(name)], - documentation: ts.emptyArray, - tags: undefined, - codeActions: undefined, + sortText: ts.Completions.SortText.LocationPriority, }; - } - - export function getJSDocParameterNameCompletions(tag: ts.JSDocParameterTag): ts.CompletionEntry[] { - if (!ts.isIdentifier(tag.name)) { - return ts.emptyArray; - } - const nameThusFar = tag.name.text; - const jsdoc = tag.parent; - const fn = jsdoc.parent; - if (!ts.isFunctionLike(fn)) - return []; - return ts.mapDefined(fn.parameters, param => { - if (!ts.isIdentifier(param.name)) - return undefined; - - const name = param.name.text; - if (jsdoc.tags!.some(t => t !== tag && ts.isJSDocParameterTag(t) && ts.isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217 - || nameThusFar !== undefined && !ts.startsWith(name, nameThusFar)) { - return undefined; - } + })); +} - return { name, kind: ts.ScriptElementKind.parameterElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority }; - }); - } +export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails; - export function getJSDocParameterNameCompletionDetails(name: string): ts.CompletionEntryDetails { +export function getJSDocTagCompletions(): ts.CompletionEntry[] { + return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = ts.map(jsDocTagNames, tagName => { return { - name, - kind: ts.ScriptElementKind.parameterElement, + name: `@${tagName}`, + kind: ts.ScriptElementKind.keyword, kindModifiers: "", - displayParts: [ts.textPart(name)], - documentation: ts.emptyArray, - tags: undefined, - codeActions: undefined, + sortText: ts.Completions.SortText.LocationPriority }; - } + })); +} - /** - * 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: ts.SourceFile, position: number, options?: ts.DocCommentTemplateOptions): ts.TextInsertion | undefined { - const tokenAtPos = ts.getTokenAtPosition(sourceFile, position); - const existingDocComment = ts.findAncestor(tokenAtPos, ts.isJSDoc); - if (existingDocComment && (existingDocComment.comment !== undefined || ts.length(existingDocComment.tags))) { - // Non-empty comment already exists. - return undefined; - } +export function getJSDocTagCompletionDetails(name: string): ts.CompletionEntryDetails { + return { + name, + kind: ts.ScriptElementKind.unknown, + kindModifiers: "", + displayParts: [ts.textPart(name)], + documentation: ts.emptyArray, + tags: undefined, + codeActions: 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) { +export function getJSDocParameterNameCompletions(tag: ts.JSDocParameterTag): ts.CompletionEntry[] { + if (!ts.isIdentifier(tag.name)) { + return ts.emptyArray; + } + const nameThusFar = tag.name.text; + const jsdoc = tag.parent; + const fn = jsdoc.parent; + if (!ts.isFunctionLike(fn)) + return []; + return ts.mapDefined(fn.parameters, param => { + if (!ts.isIdentifier(param.name)) return undefined; - } - const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos, options); - if (!commentOwnerInfo) { + const name = param.name.text; + if (jsdoc.tags!.some(t => t !== tag && ts.isJSDocParameterTag(t) && ts.isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217 + || nameThusFar !== undefined && !ts.startsWith(name, nameThusFar)) { return undefined; } - const { commentOwner, parameters, hasReturn } = commentOwnerInfo; - const commentOwnerJSDoc = ts.hasJSDocNodes(commentOwner) && commentOwner.jsDoc ? ts.lastOrUndefined(commentOwner.jsDoc) : undefined; - if (commentOwner.getStart(sourceFile) < position || commentOwnerJSDoc && commentOwnerJSDoc !== existingDocComment) { - return undefined; - } + return { name, kind: ts.ScriptElementKind.parameterElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority }; + }); +} - const indentationStr = getIndentationStringAtPosition(sourceFile, position); - const isJavaScriptFile = ts.hasJSFileExtension(sourceFile.fileName); - const tags = (parameters ? parameterDocComments(parameters || [], isJavaScriptFile, indentationStr, newLine) : "") + - (hasReturn ? returnsDocComment(indentationStr, newLine) : ""); - - // 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 - // * the '@returns'-tag - // * 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 openComment = "/**"; - const closeComment = " */"; - if (tags) { - const preamble = openComment + newLine + indentationStr + " * "; - const endLine = tokenStart === position ? newLine + indentationStr : ""; - const result = preamble + newLine + tags + indentationStr + closeComment + endLine; - return { newText: result, caretOffset: preamble.length }; - } - return { newText: openComment + closeComment, caretOffset: 3 }; - } +export function getJSDocParameterNameCompletionDetails(name: string): ts.CompletionEntryDetails { + return { + name, + kind: ts.ScriptElementKind.parameterElement, + kindModifiers: "", + displayParts: [ts.textPart(name)], + documentation: ts.emptyArray, + tags: undefined, + codeActions: undefined, + }; +} - function getIndentationStringAtPosition(sourceFile: ts.SourceFile, position: number): string { - const { text } = sourceFile; - const lineStart = ts.getLineStartPositionForPosition(position, sourceFile); - let pos = lineStart; - for (; pos <= position && ts.isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++) - ; - return text.slice(lineStart, pos); +/** + * 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: ts.SourceFile, position: number, options?: ts.DocCommentTemplateOptions): ts.TextInsertion | undefined { + const tokenAtPos = ts.getTokenAtPosition(sourceFile, position); + const existingDocComment = ts.findAncestor(tokenAtPos, ts.isJSDoc); + if (existingDocComment && (existingDocComment.comment !== undefined || ts.length(existingDocComment.tags))) { + // Non-empty comment already exists. + return undefined; } - function parameterDocComments(parameters: readonly ts.ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string { - return parameters.map(({ name, dotDotDotToken }, i) => { - const paramName = name.kind === ts.SyntaxKind.Identifier ? name.text : "param" + i; - const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : ""; - return `${indentationStr} * @param ${type}${paramName}${newLine}`; - }).join(""); + 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 returnsDocComment(indentationStr: string, newLine: string) { - return `${indentationStr} * @returns${newLine}`; + const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos, options); + if (!commentOwnerInfo) { + return undefined; } - interface CommentOwnerInfo { - readonly commentOwner: ts.Node; - readonly parameters?: readonly ts.ParameterDeclaration[]; - readonly hasReturn?: boolean; + const { commentOwner, parameters, hasReturn } = commentOwnerInfo; + const commentOwnerJSDoc = ts.hasJSDocNodes(commentOwner) && commentOwner.jsDoc ? ts.lastOrUndefined(commentOwner.jsDoc) : undefined; + if (commentOwner.getStart(sourceFile) < position || commentOwnerJSDoc && commentOwnerJSDoc !== existingDocComment) { + return undefined; } - function getCommentOwnerInfo(tokenAtPos: ts.Node, options: ts.DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined { - return ts.forEachAncestor(tokenAtPos, n => getCommentOwnerInfoWorker(n, options)); + + const indentationStr = getIndentationStringAtPosition(sourceFile, position); + const isJavaScriptFile = ts.hasJSFileExtension(sourceFile.fileName); + const tags = (parameters ? parameterDocComments(parameters || [], isJavaScriptFile, indentationStr, newLine) : "") + + (hasReturn ? returnsDocComment(indentationStr, newLine) : ""); + + // 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 + // * the '@returns'-tag + // * 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 openComment = "/**"; + const closeComment = " */"; + if (tags) { + const preamble = openComment + newLine + indentationStr + " * "; + const endLine = tokenStart === position ? newLine + indentationStr : ""; + const result = preamble + newLine + tags + indentationStr + closeComment + endLine; + return { newText: result, caretOffset: preamble.length }; } - function getCommentOwnerInfoWorker(commentOwner: ts.Node, options: ts.DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined | "quit" { - switch (commentOwner.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.ArrowFunction: - const host = commentOwner as ts.ArrowFunction | ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.MethodSignature; - return { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) }; - - case ts.SyntaxKind.PropertyAssignment: - return getCommentOwnerInfoWorker((commentOwner as ts.PropertyAssignment).initializer, options); - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.TypeAliasDeclaration: - return { commentOwner }; - - case ts.SyntaxKind.VariableStatement: { - const varStatement = commentOwner as ts.VariableStatement; - const varDeclarations = varStatement.declarationList.declarations; - const host = varDeclarations.length === 1 && varDeclarations[0].initializer - ? getRightHandSideOfAssignment(varDeclarations[0].initializer) - : undefined; - return host - ? { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) } - : { commentOwner }; - } + return { newText: openComment + closeComment, caretOffset: 3 }; +} - case ts.SyntaxKind.SourceFile: - return "quit"; +function getIndentationStringAtPosition(sourceFile: ts.SourceFile, position: number): string { + const { text } = sourceFile; + const lineStart = ts.getLineStartPositionForPosition(position, sourceFile); + let pos = lineStart; + for (; pos <= position && ts.isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++) + ; + return text.slice(lineStart, pos); +} - case ts.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 === ts.SyntaxKind.ModuleDeclaration ? undefined : { commentOwner }; - case ts.SyntaxKind.ExpressionStatement: - return getCommentOwnerInfoWorker((commentOwner as ts.ExpressionStatement).expression, options); - case ts.SyntaxKind.BinaryExpression: { - const be = commentOwner as ts.BinaryExpression; - if (ts.getAssignmentDeclarationKind(be) === ts.AssignmentDeclarationKind.None) { - return "quit"; - } - return ts.isFunctionLike(be.right) - ? { commentOwner, parameters: be.right.parameters, hasReturn: hasReturn(be.right, options) } - : { commentOwner }; +function parameterDocComments(parameters: readonly ts.ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string { + return parameters.map(({ name, dotDotDotToken }, i) => { + const paramName = name.kind === ts.SyntaxKind.Identifier ? name.text : "param" + i; + const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : ""; + return `${indentationStr} * @param ${type}${paramName}${newLine}`; + }).join(""); +} + +function returnsDocComment(indentationStr: string, newLine: string) { + return `${indentationStr} * @returns${newLine}`; +} + +interface CommentOwnerInfo { + readonly commentOwner: ts.Node; + readonly parameters?: readonly ts.ParameterDeclaration[]; + readonly hasReturn?: boolean; +} +function getCommentOwnerInfo(tokenAtPos: ts.Node, options: ts.DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined { + return ts.forEachAncestor(tokenAtPos, n => getCommentOwnerInfoWorker(n, options)); +} +function getCommentOwnerInfoWorker(commentOwner: ts.Node, options: ts.DocCommentTemplateOptions | undefined): CommentOwnerInfo | undefined | "quit" { + switch (commentOwner.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.ArrowFunction: + const host = commentOwner as ts.ArrowFunction | ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.MethodSignature; + return { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) }; + + case ts.SyntaxKind.PropertyAssignment: + return getCommentOwnerInfoWorker((commentOwner as ts.PropertyAssignment).initializer, options); + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.TypeAliasDeclaration: + return { commentOwner }; + + case ts.SyntaxKind.VariableStatement: { + const varStatement = commentOwner as ts.VariableStatement; + const varDeclarations = varStatement.declarationList.declarations; + const host = varDeclarations.length === 1 && varDeclarations[0].initializer + ? getRightHandSideOfAssignment(varDeclarations[0].initializer) + : undefined; + return host + ? { commentOwner, parameters: host.parameters, hasReturn: hasReturn(host, options) } + : { commentOwner }; + } + + case ts.SyntaxKind.SourceFile: + return "quit"; + + case ts.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 === ts.SyntaxKind.ModuleDeclaration ? undefined : { commentOwner }; + case ts.SyntaxKind.ExpressionStatement: + return getCommentOwnerInfoWorker((commentOwner as ts.ExpressionStatement).expression, options); + case ts.SyntaxKind.BinaryExpression: { + const be = commentOwner as ts.BinaryExpression; + if (ts.getAssignmentDeclarationKind(be) === ts.AssignmentDeclarationKind.None) { + return "quit"; } - case ts.SyntaxKind.PropertyDeclaration: - const init = (commentOwner as ts.PropertyDeclaration).initializer; - if (init && (ts.isFunctionExpression(init) || ts.isArrowFunction(init))) { - return { commentOwner, parameters: init.parameters, hasReturn: hasReturn(init, options) }; - } + return ts.isFunctionLike(be.right) + ? { commentOwner, parameters: be.right.parameters, hasReturn: hasReturn(be.right, options) } + : { commentOwner }; } + case ts.SyntaxKind.PropertyDeclaration: + const init = (commentOwner as ts.PropertyDeclaration).initializer; + if (init && (ts.isFunctionExpression(init) || ts.isArrowFunction(init))) { + return { commentOwner, parameters: init.parameters, hasReturn: hasReturn(init, options) }; + } } +} - function hasReturn(node: ts.Node, options: ts.DocCommentTemplateOptions | undefined) { - return !!options?.generateReturnInDocTemplate && - (ts.isArrowFunction(node) && ts.isExpression(node.body) - || ts.isFunctionLikeDeclaration(node) && node.body && ts.isBlock(node.body) && !!ts.forEachReturnStatement(node.body, n => n)); - } +function hasReturn(node: ts.Node, options: ts.DocCommentTemplateOptions | undefined) { + return !!options?.generateReturnInDocTemplate && + (ts.isArrowFunction(node) && ts.isExpression(node.body) + || ts.isFunctionLikeDeclaration(node) && node.body && ts.isBlock(node.body) && !!ts.forEachReturnStatement(node.body, n => n)); +} - function getRightHandSideOfAssignment(rightHandSide: ts.Expression): ts.FunctionExpression | ts.ArrowFunction | ts.ConstructorDeclaration | undefined { - while (rightHandSide.kind === ts.SyntaxKind.ParenthesizedExpression) { - rightHandSide = (rightHandSide as ts.ParenthesizedExpression).expression; - } +function getRightHandSideOfAssignment(rightHandSide: ts.Expression): ts.FunctionExpression | ts.ArrowFunction | ts.ConstructorDeclaration | undefined { + while (rightHandSide.kind === ts.SyntaxKind.ParenthesizedExpression) { + rightHandSide = (rightHandSide as ts.ParenthesizedExpression).expression; + } - switch (rightHandSide.kind) { - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return (rightHandSide as ts.FunctionExpression); - case ts.SyntaxKind.ClassExpression: - return ts.find((rightHandSide as ts.ClassExpression).members, ts.isConstructorDeclaration); - } + switch (rightHandSide.kind) { + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return (rightHandSide as ts.FunctionExpression); + case ts.SyntaxKind.ClassExpression: + return ts.find((rightHandSide as ts.ClassExpression).members, ts.isConstructorDeclaration); } } +} diff --git a/src/services/navigateTo.ts b/src/services/navigateTo.ts index a49bf6e317223..5f7268147d1a6 100644 --- a/src/services/navigateTo.ts +++ b/src/services/navigateTo.ts @@ -1,138 +1,138 @@ /* @internal */ namespace ts.NavigateTo { - interface RawNavigateToItem { - readonly name: string; - readonly fileName: string; - readonly matchKind: ts.PatternMatchKind; - readonly isCaseSensitive: boolean; - readonly declaration: ts.Declaration; - } - - export function getNavigateToItems(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean): ts.NavigateToItem[] { - const patternMatcher = ts.createPatternMatcher(searchValue); - if (!patternMatcher) - return ts.emptyArray; - const rawItems: RawNavigateToItem[] = []; +interface RawNavigateToItem { + readonly name: string; + readonly fileName: string; + readonly matchKind: ts.PatternMatchKind; + readonly isCaseSensitive: boolean; + readonly declaration: ts.Declaration; +} - // Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] - for (const sourceFile of sourceFiles) { - cancellationToken.throwIfCancellationRequested(); +export function getNavigateToItems(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, cancellationToken: ts.CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean): ts.NavigateToItem[] { + const patternMatcher = ts.createPatternMatcher(searchValue); + if (!patternMatcher) + return ts.emptyArray; + const rawItems: RawNavigateToItem[] = []; - if (excludeDtsFiles && sourceFile.isDeclarationFile) { - continue; - } + // Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] + for (const sourceFile of sourceFiles) { + cancellationToken.throwIfCancellationRequested(); - sourceFile.getNamedDeclarations().forEach((declarations, name) => { - getItemsFromNamedDeclaration(patternMatcher, name, declarations, checker, sourceFile.fileName, rawItems); - }); + 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: ts.PatternMatcher, name: string, declarations: readonly ts.Declaration[], checker: ts.TypeChecker, fileName: string, rawItems: ts.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 - } + rawItems.sort(compareNavigateToItems); + return (maxResultCount === undefined ? rawItems : rawItems.slice(0, maxResultCount)).map(createNavigateToItem); +} - for (const declaration of declarations) { - if (!shouldKeepItem(declaration, checker)) - continue; +function getItemsFromNamedDeclaration(patternMatcher: ts.PatternMatcher, name: string, declarations: readonly ts.Declaration[], checker: ts.TypeChecker, fileName: string, rawItems: ts.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 + } - 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 }); + 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: ts.Declaration, checker: ts.TypeChecker): boolean { - switch (declaration.kind) { - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ImportEqualsDeclaration: - const importer = checker.getSymbolAtLocation((declaration as ts.ImportClause | ts.ImportSpecifier | ts.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: ts.Declaration, containers: ts.Push): boolean { - const name = ts.getNameOfDeclaration(declaration); - return !!name && (pushLiteral(name, containers) || name.kind === ts.SyntaxKind.ComputedPropertyName && tryAddComputedPropertyName(name.expression, containers)); +function shouldKeepItem(declaration: ts.Declaration, checker: ts.TypeChecker): boolean { + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ImportEqualsDeclaration: + const importer = checker.getSymbolAtLocation((declaration as ts.ImportClause | ts.ImportSpecifier | ts.ImportEqualsDeclaration).name!)!; // TODO: GH#18217 + const imported = checker.getAliasedSymbol(importer); + return importer.escapedName !== imported.escapedName; + default: + return true; } +} - // Only added the names of computed properties if they're simple dotted expressions, like: - // - // [X.Y.Z]() { } - function tryAddComputedPropertyName(expression: ts.Expression, containers: ts.Push): boolean { - return pushLiteral(expression, containers) - || ts.isPropertyAccessExpression(expression) && (containers.push(expression.name.text), true) && tryAddComputedPropertyName(expression.expression, containers); - } +function tryAddSingleDeclarationName(declaration: ts.Declaration, containers: ts.Push): boolean { + const name = ts.getNameOfDeclaration(declaration); + return !!name && (pushLiteral(name, containers) || name.kind === ts.SyntaxKind.ComputedPropertyName && tryAddComputedPropertyName(name.expression, containers)); +} - function pushLiteral(node: ts.Node, containers: ts.Push): boolean { - return ts.isPropertyNameLiteral(node) && (containers.push(ts.getTextOfIdentifierOrLiteral(node)), true); - } +// Only added the names of computed properties if they're simple dotted expressions, like: +// +// [X.Y.Z]() { } +function tryAddComputedPropertyName(expression: ts.Expression, containers: ts.Push): boolean { + return pushLiteral(expression, containers) + || ts.isPropertyAccessExpression(expression) && (containers.push(expression.name.text), true) && tryAddComputedPropertyName(expression.expression, containers); +} - function getContainers(declaration: ts.Declaration): readonly string[] { - const containers: string[] = []; +function pushLiteral(node: ts.Node, containers: ts.Push): boolean { + return ts.isPropertyNameLiteral(node) && (containers.push(ts.getTextOfIdentifierOrLiteral(node)), true); +} - // First, if we started with a computed property name, then add all but the last - // portion into the container array. - const name = ts.getNameOfDeclaration(declaration); - if (name && name.kind === ts.SyntaxKind.ComputedPropertyName && !tryAddComputedPropertyName(name.expression, containers)) { - return ts.emptyArray; - } - // Don't include the last portion. - containers.shift(); +function getContainers(declaration: ts.Declaration): readonly string[] { + const containers: string[] = []; - // Now, walk up our containers, adding all their names to the container array. - let container = ts.getContainerNode(declaration); + // First, if we started with a computed property name, then add all but the last + // portion into the container array. + const name = ts.getNameOfDeclaration(declaration); + if (name && name.kind === ts.SyntaxKind.ComputedPropertyName && !tryAddComputedPropertyName(name.expression, containers)) { + return ts.emptyArray; + } + // Don't include the last portion. + containers.shift(); - while (container) { - if (!tryAddSingleDeclarationName(container, containers)) { - return ts.emptyArray; - } + // Now, walk up our containers, adding all their names to the container array. + let container = ts.getContainerNode(declaration); - container = ts.getContainerNode(container); + while (container) { + if (!tryAddSingleDeclarationName(container, containers)) { + return ts.emptyArray; } - return containers.reverse(); + container = ts.getContainerNode(container); } - function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem) { - // TODO(cyrusn): get the gamut of comparisons that VS already uses here. - return ts.compareValues(i1.matchKind, i2.matchKind) - || ts.compareStringsCaseSensitiveUI(i1.name, i2.name); - } + return containers.reverse(); +} - function createNavigateToItem(rawItem: RawNavigateToItem): ts.NavigateToItem { - const declaration = rawItem.declaration; - const container = ts.getContainerNode(declaration); - const containerName = container && ts.getNameOfDeclaration(container); - return { - name: rawItem.name, - kind: ts.getNodeKind(declaration), - kindModifiers: ts.getNodeModifiers(declaration), - matchKind: ts.PatternMatchKind[rawItem.matchKind] as keyof typeof ts.PatternMatchKind, - isCaseSensitive: rawItem.isCaseSensitive, - fileName: rawItem.fileName, - textSpan: ts.createTextSpanFromNode(declaration), - // TODO(jfreeman): What should be the containerName when the container has a computed name? - containerName: containerName ? (containerName as ts.Identifier).text : "", - containerKind: containerName ? ts.getNodeKind(container) : ts.ScriptElementKind.unknown, - }; - } +function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem) { + // TODO(cyrusn): get the gamut of comparisons that VS already uses here. + return ts.compareValues(i1.matchKind, i2.matchKind) + || ts.compareStringsCaseSensitiveUI(i1.name, i2.name); +} + +function createNavigateToItem(rawItem: RawNavigateToItem): ts.NavigateToItem { + const declaration = rawItem.declaration; + const container = ts.getContainerNode(declaration); + const containerName = container && ts.getNameOfDeclaration(container); + return { + name: rawItem.name, + kind: ts.getNodeKind(declaration), + kindModifiers: ts.getNodeModifiers(declaration), + matchKind: ts.PatternMatchKind[rawItem.matchKind] as keyof typeof ts.PatternMatchKind, + isCaseSensitive: rawItem.isCaseSensitive, + fileName: rawItem.fileName, + textSpan: ts.createTextSpanFromNode(declaration), + // TODO(jfreeman): What should be the containerName when the container has a computed name? + containerName: containerName ? (containerName as ts.Identifier).text : "", + containerKind: containerName ? ts.getNodeKind(container) : ts.ScriptElementKind.unknown, + }; +} } diff --git a/src/services/navigationBar.ts b/src/services/navigationBar.ts index 781dd5e9d409a..6c4621c3a1468 100644 --- a/src/services/navigationBar.ts +++ b/src/services/navigationBar.ts @@ -1,975 +1,975 @@ /* @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: ts.CancellationToken; - let curSourceFile: ts.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: (ts.ESMap | undefined)[] = []; - let trackedEs5Classes: ts.ESMap | undefined; - - // NavigationBarItem requires an array, but will not mutate it, so just give it this for performance. - let emptyChildItemArray: ts.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: ts.Node; - name: ts.DeclarationName | undefined; - additionalNodes: ts.Node[] | undefined; - parent: NavigationBarNode | undefined; // Present for all but root node - children: NavigationBarNode[] | undefined; - indent: number; // # of parents - } - - export function getNavigationBarItems(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.NavigationBarItem[] { - curCancellationToken = cancellationToken; - curSourceFile = sourceFile; - try { - return ts.map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem); - } - finally { - reset(); - } - } - - export function getNavigationTree(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.NavigationTree { - curCancellationToken = cancellationToken; - curSourceFile = sourceFile; - try { - return convertToTree(rootNavigationBarNode(sourceFile)); - } - 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. + */ +const maxLength = 150; + +// Keep sourceFile handy so we don't have to search for it every time we need to call `getText`. +let curCancellationToken: ts.CancellationToken; +let curSourceFile: ts.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: (ts.ESMap | undefined)[] = []; +let trackedEs5Classes: ts.ESMap | undefined; + +// NavigationBarItem requires an array, but will not mutate it, so just give it this for performance. +let emptyChildItemArray: ts.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: ts.Node; + name: ts.DeclarationName | undefined; + additionalNodes: ts.Node[] | undefined; + parent: NavigationBarNode | undefined; // Present for all but root node + children: NavigationBarNode[] | undefined; + indent: number; // # of parents +} - function reset() { - curSourceFile = undefined!; - curCancellationToken = undefined!; - parentsStack = []; - parent = undefined!; - emptyChildItemArray = []; +export function getNavigationBarItems(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.NavigationBarItem[] { + curCancellationToken = cancellationToken; + curSourceFile = sourceFile; + try { + return ts.map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem); } - - function nodeText(node: ts.Node): string { - return cleanText(node.getText(curSourceFile)); + finally { + reset(); } +} - function navigationBarNodeKind(n: NavigationBarNode): ts.SyntaxKind { - return n.node.kind; +export function getNavigationTree(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.NavigationTree { + curCancellationToken = cancellationToken; + curSourceFile = sourceFile; + try { + return convertToTree(rootNavigationBarNode(sourceFile)); } - - function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void { - if (parent.children) { - parent.children.push(child); - } - else { - parent.children = [child]; - } + finally { + reset(); } +} - function rootNavigationBarNode(sourceFile: ts.SourceFile): NavigationBarNode { - ts.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(); - ts.Debug.assert(!parent && !parentsStack.length); - return root; - } +function reset() { + curSourceFile = undefined!; + curCancellationToken = undefined!; + parentsStack = []; + parent = undefined!; + emptyChildItemArray = []; +} - function addLeafNode(node: ts.Node, name?: ts.DeclarationName): void { - pushChild(parent, emptyNavigationBarNode(node, name)); - } +function nodeText(node: ts.Node): string { + return cleanText(node.getText(curSourceFile)); +} - function emptyNavigationBarNode(node: ts.Node, name?: ts.DeclarationName): NavigationBarNode { - return { - node, - name: name || (ts.isDeclaration(node) || ts.isExpression(node) ? ts.getNameOfDeclaration(node) : undefined), - additionalNodes: undefined, - parent, - children: undefined, - indent: parent.indent + 1 - }; - } +function navigationBarNodeKind(n: NavigationBarNode): ts.SyntaxKind { + return n.node.kind; +} - function addTrackedEs5Class(name: string) { - if (!trackedEs5Classes) { - trackedEs5Classes = new ts.Map(); - } - trackedEs5Classes.set(name, true); +function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void { + if (parent.children) { + parent.children.push(child); } - function endNestedNodes(depth: number): void { - for (let i = 0; i < depth; i++) - endNode(); + else { + parent.children = [child]; } - function startNestedNodes(targetNode: ts.Node, entityName: ts.BindableStaticNameExpression) { - const names: ts.PropertyNameLiteral[] = []; - while (!ts.isPropertyNameLiteral(entityName)) { - const name = ts.getNameOrArgument(entityName); - const nameText = ts.getElementOrPropertyAccessName(entityName); - entityName = entityName.expression; - if (nameText === "prototype" || ts.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; +} + +function rootNavigationBarNode(sourceFile: ts.SourceFile): NavigationBarNode { + ts.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(); + ts.Debug.assert(!parent && !parentsStack.length); + return root; +} - /** - * 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: ts.Node, name?: ts.DeclarationName): void { - const navNode: NavigationBarNode = emptyNavigationBarNode(node, name); - pushChild(parent, navNode); +function addLeafNode(node: ts.Node, name?: ts.DeclarationName): void { + pushChild(parent, emptyNavigationBarNode(node, name)); +} + +function emptyNavigationBarNode(node: ts.Node, name?: ts.DeclarationName): NavigationBarNode { + return { + node, + name: name || (ts.isDeclaration(node) || ts.isExpression(node) ? ts.getNameOfDeclaration(node) : undefined), + additionalNodes: undefined, + parent, + children: undefined, + indent: parent.indent + 1 + }; +} - // Save the old parent - parentsStack.push(parent); - trackedEs5ClassesStack.push(trackedEs5Classes); - trackedEs5Classes = undefined; - parent = navNode; +function addTrackedEs5Class(name: string) { + if (!trackedEs5Classes) { + trackedEs5Classes = new ts.Map(); } + trackedEs5Classes.set(name, true); +} +function endNestedNodes(depth: number): void { + for (let i = 0; i < depth; i++) + endNode(); +} +function startNestedNodes(targetNode: ts.Node, entityName: ts.BindableStaticNameExpression) { + const names: ts.PropertyNameLiteral[] = []; + while (!ts.isPropertyNameLiteral(entityName)) { + const name = ts.getNameOrArgument(entityName); + const nameText = ts.getElementOrPropertyAccessName(entityName); + entityName = entityName.expression; + if (nameText === "prototype" || ts.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; +} - /** 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(); +/** + * 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: ts.Node, name?: ts.DeclarationName): void { + const navNode: NavigationBarNode = emptyNavigationBarNode(node, name); + pushChild(parent, navNode); + + // Save the old parent + parentsStack.push(parent); + trackedEs5ClassesStack.push(trackedEs5Classes); + trackedEs5Classes = undefined; + 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(); +} + +function addNodeWithRecursiveChild(node: ts.Node, child: ts.Node | undefined, name?: ts.DeclarationName): void { + startNode(node, name); + addChildrenRecursively(child); + endNode(); +} - function addNodeWithRecursiveChild(node: ts.Node, child: ts.Node | undefined, name?: ts.DeclarationName): void { - startNode(node, name); - addChildrenRecursively(child); +function addNodeWithRecursiveInitializer(node: ts.VariableDeclaration | ts.PropertyAssignment | ts.BindingElement | ts.PropertyDeclaration): void { + if (node.initializer && isFunctionOrClassExpression(node.initializer)) { + startNode(node); + ts.forEachChild(node.initializer, addChildrenRecursively); endNode(); } - - function addNodeWithRecursiveInitializer(node: ts.VariableDeclaration | ts.PropertyAssignment | ts.BindingElement | ts.PropertyDeclaration): void { - if (node.initializer && isFunctionOrClassExpression(node.initializer)) { - startNode(node); - ts.forEachChild(node.initializer, addChildrenRecursively); - endNode(); - } - else { - addNodeWithRecursiveChild(node, node.initializer); - } + else { + addNodeWithRecursiveChild(node, node.initializer); } +} - /** - * Historically, we've elided dynamic names from the nav tree (including late bound names), - * but included certain "well known" symbol names. While we no longer distinguish those well-known - * symbols from other unique symbols, we do the below to retain those members in the nav tree. - */ - function hasNavigationBarName(node: ts.Declaration) { - return !ts.hasDynamicName(node) || - (node.kind !== ts.SyntaxKind.BinaryExpression && - ts.isPropertyAccessExpression(node.name.expression) && - ts.isIdentifier(node.name.expression.expression) && - ts.idText(node.name.expression.expression) === "Symbol"); - } +/** + * Historically, we've elided dynamic names from the nav tree (including late bound names), + * but included certain "well known" symbol names. While we no longer distinguish those well-known + * symbols from other unique symbols, we do the below to retain those members in the nav tree. + */ +function hasNavigationBarName(node: ts.Declaration) { + return !ts.hasDynamicName(node) || + (node.kind !== ts.SyntaxKind.BinaryExpression && + ts.isPropertyAccessExpression(node.name.expression) && + ts.isIdentifier(node.name.expression.expression) && + ts.idText(node.name.expression.expression) === "Symbol"); +} - /** Look for navigation bar items in node's subtree, adding them to the current `parent`. */ - function addChildrenRecursively(node: ts.Node | undefined): void { - curCancellationToken.throwIfCancellationRequested(); +/** Look for navigation bar items in node's subtree, adding them to the current `parent`. */ +function addChildrenRecursively(node: ts.Node | undefined): void { + curCancellationToken.throwIfCancellationRequested(); - if (!node || ts.isToken(node)) { - return; - } + if (!node || ts.isToken(node)) { + return; + } - switch (node.kind) { - case ts.SyntaxKind.Constructor: - // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it. - const ctr = node as ts.ConstructorDeclaration; - addNodeWithRecursiveChild(ctr, ctr.body); + switch (node.kind) { + case ts.SyntaxKind.Constructor: + // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it. + const ctr = node as ts.ConstructorDeclaration; + addNodeWithRecursiveChild(ctr, ctr.body); - // Parameter properties are children of the class, not the constructor. - for (const param of ctr.parameters) { - if (ts.isParameterPropertyDeclaration(param, ctr)) { - addLeafNode(param); - } + // Parameter properties are children of the class, not the constructor. + for (const param of ctr.parameters) { + if (ts.isParameterPropertyDeclaration(param, ctr)) { + addLeafNode(param); } - break; - - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodSignature: - if (hasNavigationBarName(node as ts.ClassElement | ts.TypeElement)) { - addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); - } - break; + } + break; - case ts.SyntaxKind.PropertyDeclaration: - if (hasNavigationBarName(node as ts.ClassElement)) { - addNodeWithRecursiveInitializer(node as ts.PropertyDeclaration); - } - break; - case ts.SyntaxKind.PropertySignature: - if (hasNavigationBarName(node as ts.TypeElement)) { - addLeafNode(node); - } - break; - - case ts.SyntaxKind.ImportClause: - const importClause = node as ts.ImportClause; - // Handle default import case e.g.: - // import d from "mod"; - if (importClause.name) { - addLeafNode(importClause.name); - } + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodSignature: + if (hasNavigationBarName(node as ts.ClassElement | ts.TypeElement)) { + addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); + } + break; - // 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 === ts.SyntaxKind.NamespaceImport) { - addLeafNode(namedBindings); - } - else { - for (const element of namedBindings.elements) { - addLeafNode(element); - } - } - } - break; - - case ts.SyntaxKind.ShorthandPropertyAssignment: - addNodeWithRecursiveChild(node, (node as ts.ShorthandPropertyAssignment).name); - break; - case ts.SyntaxKind.SpreadAssignment: - const { expression } = node as ts.SpreadAssignment; - // Use the expression as the name of the SpreadAssignment, otherwise show as . - ts.isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node); - break; - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.VariableDeclaration: { - const child = node as ts.VariableDeclaration | ts.PropertyAssignment | ts.BindingElement; - if (ts.isBindingPattern(child.name)) { - addChildrenRecursively(child.name); + case ts.SyntaxKind.PropertyDeclaration: + if (hasNavigationBarName(node as ts.ClassElement)) { + addNodeWithRecursiveInitializer(node as ts.PropertyDeclaration); + } + break; + case ts.SyntaxKind.PropertySignature: + if (hasNavigationBarName(node as ts.TypeElement)) { + addLeafNode(node); + } + break; + + case ts.SyntaxKind.ImportClause: + const importClause = node as ts.ImportClause; + // 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 === ts.SyntaxKind.NamespaceImport) { + addLeafNode(namedBindings); } else { - addNodeWithRecursiveInitializer(child); + for (const element of namedBindings.elements) { + addLeafNode(element); + } } - break; } - case ts.SyntaxKind.FunctionDeclaration: - const nameNode = (node as ts.FunctionLikeDeclaration).name; - // If we see a function declaration track as a possible ES5 class - if (nameNode && ts.isIdentifier(nameNode)) { - addTrackedEs5Class(nameNode.text); + break; + + case ts.SyntaxKind.ShorthandPropertyAssignment: + addNodeWithRecursiveChild(node, (node as ts.ShorthandPropertyAssignment).name); + break; + case ts.SyntaxKind.SpreadAssignment: + const { expression } = node as ts.SpreadAssignment; + // Use the expression as the name of the SpreadAssignment, otherwise show as . + ts.isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node); + break; + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.VariableDeclaration: { + const child = node as ts.VariableDeclaration | ts.PropertyAssignment | ts.BindingElement; + if (ts.isBindingPattern(child.name)) { + addChildrenRecursively(child.name); + } + else { + addNodeWithRecursiveInitializer(child); + } + break; + } + case ts.SyntaxKind.FunctionDeclaration: + const nameNode = (node as ts.FunctionLikeDeclaration).name; + // If we see a function declaration track as a possible ES5 class + if (nameNode && ts.isIdentifier(nameNode)) { + addTrackedEs5Class(nameNode.text); + } + addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); + break; + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); + break; + + case ts.SyntaxKind.EnumDeclaration: + startNode(node); + for (const member of (node as ts.EnumDeclaration).members) { + if (!isComputedProperty(member)) { + addLeafNode(member); } - addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); - break; - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - addNodeWithRecursiveChild(node, (node as ts.FunctionLikeDeclaration).body); - break; + } + endNode(); + break; - case ts.SyntaxKind.EnumDeclaration: - startNode(node); - for (const member of (node as ts.EnumDeclaration).members) { - if (!isComputedProperty(member)) { - addLeafNode(member); - } - } - endNode(); - break; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + startNode(node); + for (const member of (node as ts.InterfaceDeclaration).members) { + addChildrenRecursively(member); + } + endNode(); + break; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + addNodeWithRecursiveChild(node, getInteriorModule(node as ts.ModuleDeclaration).body); + break; + + case ts.SyntaxKind.ExportAssignment: { + const expression = (node as ts.ExportAssignment).expression; + const child = ts.isObjectLiteralExpression(expression) || ts.isCallExpression(expression) ? expression : + ts.isArrowFunction(expression) || ts.isFunctionExpression(expression) ? expression.body : undefined; + if (child) { startNode(node); - for (const member of (node as ts.InterfaceDeclaration).members) { - addChildrenRecursively(member); - } + addChildrenRecursively(child); endNode(); - break; - - case ts.SyntaxKind.ModuleDeclaration: - addNodeWithRecursiveChild(node, getInteriorModule(node as ts.ModuleDeclaration).body); - break; - - case ts.SyntaxKind.ExportAssignment: { - const expression = (node as ts.ExportAssignment).expression; - const child = ts.isObjectLiteralExpression(expression) || ts.isCallExpression(expression) ? expression : - ts.isArrowFunction(expression) || ts.isFunctionExpression(expression) ? expression.body : undefined; - if (child) { - startNode(node); - addChildrenRecursively(child); - endNode(); - } - else { - addLeafNode(node); - } - break; } - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.IndexSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.TypeAliasDeclaration: + else { addLeafNode(node); - break; - - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.BinaryExpression: { - const special = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); - switch (special) { - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.ModuleExports: - addNodeWithRecursiveChild(node, (node as ts.BinaryExpression).right); - return; - case ts.AssignmentDeclarationKind.Prototype: - case ts.AssignmentDeclarationKind.PrototypeProperty: { - const binaryExpression = (node as ts.BinaryExpression); - const assignmentTarget = binaryExpression.left as ts.PropertyAccessExpression; - const prototypeAccess = special === ts.AssignmentDeclarationKind.PrototypeProperty ? - assignmentTarget.expression as ts.PropertyAccessExpression : - assignmentTarget; - - let depth = 0; - let className: ts.PropertyNameLiteral; - // If we see a prototype assignment, start tracking the target as a class - // This is only done for simple classes not nested assignments. - if (ts.isIdentifier(prototypeAccess.expression)) { - addTrackedEs5Class(prototypeAccess.expression.text); - className = prototypeAccess.expression; - } - else { - [depth, className] = startNestedNodes(binaryExpression, prototypeAccess.expression as ts.EntityNameExpression); - } - if (special === ts.AssignmentDeclarationKind.Prototype) { - if (ts.isObjectLiteralExpression(binaryExpression.right)) { - if (binaryExpression.right.properties.length > 0) { - startNode(binaryExpression, className); - ts.forEachChild(binaryExpression.right, addChildrenRecursively); - endNode(); - } + } + break; + } + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.TypeAliasDeclaration: + addLeafNode(node); + break; + + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.BinaryExpression: { + const special = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); + switch (special) { + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.ModuleExports: + addNodeWithRecursiveChild(node, (node as ts.BinaryExpression).right); + return; + case ts.AssignmentDeclarationKind.Prototype: + case ts.AssignmentDeclarationKind.PrototypeProperty: { + const binaryExpression = (node as ts.BinaryExpression); + const assignmentTarget = binaryExpression.left as ts.PropertyAccessExpression; + const prototypeAccess = special === ts.AssignmentDeclarationKind.PrototypeProperty ? + assignmentTarget.expression as ts.PropertyAccessExpression : + assignmentTarget; + + let depth = 0; + let className: ts.PropertyNameLiteral; + // If we see a prototype assignment, start tracking the target as a class + // This is only done for simple classes not nested assignments. + if (ts.isIdentifier(prototypeAccess.expression)) { + addTrackedEs5Class(prototypeAccess.expression.text); + className = prototypeAccess.expression; + } + else { + [depth, className] = startNestedNodes(binaryExpression, prototypeAccess.expression as ts.EntityNameExpression); + } + if (special === ts.AssignmentDeclarationKind.Prototype) { + if (ts.isObjectLiteralExpression(binaryExpression.right)) { + if (binaryExpression.right.properties.length > 0) { + startNode(binaryExpression, className); + ts.forEachChild(binaryExpression.right, addChildrenRecursively); + endNode(); } } - else if (ts.isFunctionExpression(binaryExpression.right) || ts.isArrowFunction(binaryExpression.right)) { - addNodeWithRecursiveChild(node, binaryExpression.right, className); - } - else { - startNode(binaryExpression, className); - addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name); + } + else if (ts.isFunctionExpression(binaryExpression.right) || ts.isArrowFunction(binaryExpression.right)) { + addNodeWithRecursiveChild(node, binaryExpression.right, className); + } + else { + startNode(binaryExpression, className); + addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name); + endNode(); + } + endNestedNodes(depth); + return; + } + case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: + case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: { + const defineCall = node as ts.BindableObjectDefinePropertyCall; + const className = special === ts.AssignmentDeclarationKind.ObjectDefinePropertyValue ? + defineCall.arguments[0] : + (defineCall.arguments[0] as ts.PropertyAccessExpression).expression as ts.EntityNameExpression; + + const memberName = defineCall.arguments[1]; + const [depth, classNameIdentifier] = startNestedNodes(node, className); + startNode(node, classNameIdentifier); + startNode(node, ts.setTextRange(ts.factory.createIdentifier(memberName.text), memberName)); + addChildrenRecursively((node as ts.CallExpression).arguments[2]); endNode(); + endNode(); + endNestedNodes(depth); + return; + } + case ts.AssignmentDeclarationKind.Property: { + const binaryExpression = (node as ts.BinaryExpression); + const assignmentTarget = binaryExpression.left as ts.PropertyAccessExpression | ts.BindableElementAccessExpression; + const targetFunction = assignmentTarget.expression; + if (ts.isIdentifier(targetFunction) && ts.getElementOrPropertyAccessName(assignmentTarget) !== "prototype" && + trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) { + if (ts.isFunctionExpression(binaryExpression.right) || ts.isArrowFunction(binaryExpression.right)) { + addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction); } - endNestedNodes(depth); - return; - } - case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: - case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: { - const defineCall = node as ts.BindableObjectDefinePropertyCall; - const className = special === ts.AssignmentDeclarationKind.ObjectDefinePropertyValue ? - defineCall.arguments[0] : - (defineCall.arguments[0] as ts.PropertyAccessExpression).expression as ts.EntityNameExpression; - - const memberName = defineCall.arguments[1]; - const [depth, classNameIdentifier] = startNestedNodes(node, className); - startNode(node, classNameIdentifier); - startNode(node, ts.setTextRange(ts.factory.createIdentifier(memberName.text), memberName)); - addChildrenRecursively((node as ts.CallExpression).arguments[2]); - endNode(); + else if (ts.isBindableStaticAccessExpression(assignmentTarget)) { + startNode(binaryExpression, targetFunction); + addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, ts.getNameOrArgument(assignmentTarget)); endNode(); - endNestedNodes(depth); - return; - } - case ts.AssignmentDeclarationKind.Property: { - const binaryExpression = (node as ts.BinaryExpression); - const assignmentTarget = binaryExpression.left as ts.PropertyAccessExpression | ts.BindableElementAccessExpression; - const targetFunction = assignmentTarget.expression; - if (ts.isIdentifier(targetFunction) && ts.getElementOrPropertyAccessName(assignmentTarget) !== "prototype" && - trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) { - if (ts.isFunctionExpression(binaryExpression.right) || ts.isArrowFunction(binaryExpression.right)) { - addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction); - } - else if (ts.isBindableStaticAccessExpression(assignmentTarget)) { - startNode(binaryExpression, targetFunction); - addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, ts.getNameOrArgument(assignmentTarget)); - endNode(); - } - return; } - break; + return; } - case ts.AssignmentDeclarationKind.ThisProperty: - case ts.AssignmentDeclarationKind.None: - case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: - break; - default: - ts.Debug.assertNever(special); + break; } + case ts.AssignmentDeclarationKind.ThisProperty: + case ts.AssignmentDeclarationKind.None: + case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: + break; + default: + ts.Debug.assertNever(special); } - // falls through + } + // falls through - default: - if (ts.hasJSDocNodes(node)) { - ts.forEach(node.jsDoc, jsDoc => { - ts.forEach(jsDoc.tags, tag => { - if (ts.isJSDocTypeAlias(tag)) { - addLeafNode(tag); - } - }); + default: + if (ts.hasJSDocNodes(node)) { + ts.forEach(node.jsDoc, jsDoc => { + ts.forEach(jsDoc.tags, tag => { + if (ts.isJSDocTypeAlias(tag)) { + addLeafNode(tag); + } }); - } + }); + } - ts.forEachChild(node, addChildrenRecursively); - } + ts.forEachChild(node, addChildrenRecursively); } +} - /** Merge declarations of the same kind. */ - function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void { - const nameToItems = new ts.Map(); - ts.filterMutate(children, (child, index) => { - const declName = child.name || ts.getNameOfDeclaration(child.node as ts.Declaration); - const name = declName && nodeText(declName); - if (!name) { - // Anonymous items are never merged. - return true; - } +/** Merge declarations of the same kind. */ +function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void { + const nameToItems = new ts.Map(); + ts.filterMutate(children, (child, index) => { + const declName = child.name || ts.getNameOfDeclaration(child.node as ts.Declaration); + 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; - } + 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; - } - else { - const itemWithSameName = itemsWithSameName; + 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 = { - [ts.AssignmentDeclarationKind.Property]: true, - [ts.AssignmentDeclarationKind.PrototypeProperty]: true, - [ts.AssignmentDeclarationKind.ObjectDefinePropertyValue]: true, - [ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true, - [ts.AssignmentDeclarationKind.None]: false, - [ts.AssignmentDeclarationKind.ExportsProperty]: false, - [ts.AssignmentDeclarationKind.ModuleExports]: false, - [ts.AssignmentDeclarationKind.ObjectDefinePropertyExports]: false, - [ts.AssignmentDeclarationKind.Prototype]: true, - [ts.AssignmentDeclarationKind.ThisProperty]: false, - }; - function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined { - function isPossibleConstructor(node: ts.Node) { - return ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node); + itemsWithSameName.push(child); + return true; } - const bAssignmentDeclarationKind = ts.isBinaryExpression(b.node) || ts.isCallExpression(b.node) ? - ts.getAssignmentDeclarationKind(b.node) : - ts.AssignmentDeclarationKind.None; - const aAssignmentDeclarationKind = ts.isBinaryExpression(a.node) || ts.isCallExpression(a.node) ? - ts.getAssignmentDeclarationKind(a.node) : - ts.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 - || (ts.isClassDeclaration(a.node) && isSynthesized(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member - || (ts.isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated) - || (ts.isClassDeclaration(a.node) && isSynthesized(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor - || (ts.isClassDeclaration(b.node) && isPossibleConstructor(a.node) && isSynthesized(a.node)) // ctor & class (generated) + else { + const itemWithSameName = itemsWithSameName; + if (tryMerge(itemWithSameName, child, index, node)) { + return false; + } + nameToItems.set(name, [itemWithSameName, child]); + return true; + } + }); +} +const isEs5ClassMember: Record = { + [ts.AssignmentDeclarationKind.Property]: true, + [ts.AssignmentDeclarationKind.PrototypeProperty]: true, + [ts.AssignmentDeclarationKind.ObjectDefinePropertyValue]: true, + [ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true, + [ts.AssignmentDeclarationKind.None]: false, + [ts.AssignmentDeclarationKind.ExportsProperty]: false, + [ts.AssignmentDeclarationKind.ModuleExports]: false, + [ts.AssignmentDeclarationKind.ObjectDefinePropertyExports]: false, + [ts.AssignmentDeclarationKind.Prototype]: true, + [ts.AssignmentDeclarationKind.ThisProperty]: false, +}; +function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined { + function isPossibleConstructor(node: ts.Node) { + return ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node); + } + const bAssignmentDeclarationKind = ts.isBinaryExpression(b.node) || ts.isCallExpression(b.node) ? + ts.getAssignmentDeclarationKind(b.node) : + ts.AssignmentDeclarationKind.None; + const aAssignmentDeclarationKind = ts.isBinaryExpression(a.node) || ts.isCallExpression(a.node) ? + ts.getAssignmentDeclarationKind(a.node) : + ts.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 + || (ts.isClassDeclaration(a.node) && isSynthesized(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member + || (ts.isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated) + || (ts.isClassDeclaration(a.node) && isSynthesized(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor + || (ts.isClassDeclaration(b.node) && isPossibleConstructor(a.node) && isSynthesized(a.node)) // ctor & class (generated) + ) { + + let lastANode = a.additionalNodes && ts.lastOrUndefined(a.additionalNodes) || a.node; + if ((!ts.isClassDeclaration(a.node) && !ts.isClassDeclaration(b.node)) // If neither outline node is a class + || isPossibleConstructor(a.node) || isPossibleConstructor(b.node) // If either function is a constructor function ) { - - let lastANode = a.additionalNodes && ts.lastOrUndefined(a.additionalNodes) || a.node; - if ((!ts.isClassDeclaration(a.node) && !ts.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 = ts.setTextRange(ts.factory.createConstructorDeclaration(/* 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 ? ts.concatenate([ctor], b.children || [b]) : ts.concatenate(a.children || [{ ...a }], [ctor]); - } - else { - if (a.children || b.children) { - a.children = ts.concatenate(a.children || [{ ...a }], b.children || [b]); - if (a.children) { - mergeChildren(a.children, a); - sortChildren(a.children); - } - } - } - - lastANode = a.node = ts.setTextRange(ts.factory.createClassDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, a.name as ts.Identifier || ts.factory.createIdentifier("__class__"), - /* typeParameters */ undefined, - /* heritageClauses */ undefined, []), a.node); + const ctorFunction = isPossibleConstructor(a.node) ? a.node : + isPossibleConstructor(b.node) ? b.node : + undefined; + + if (ctorFunction !== undefined) { + const ctorNode = ts.setTextRange(ts.factory.createConstructorDeclaration(/* 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 ? ts.concatenate([ctor], b.children || [b]) : ts.concatenate(a.children || [{ ...a }], [ctor]); } else { - a.children = ts.concatenate(a.children, b.children); - if (a.children) { - mergeChildren(a.children, a); + if (a.children || b.children) { + a.children = ts.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) { - ts.setTextRange(lastANode, { pos: lastANode.pos, end: bNode.end }); - } - else { - if (!a.additionalNodes) - a.additionalNodes = []; - a.additionalNodes.push(ts.setTextRange(ts.factory.createClassDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, a.name as ts.Identifier || ts.factory.createIdentifier("__class__"), - /* typeParameters */ undefined, - /* heritageClauses */ undefined, []), b.node)); + lastANode = a.node = ts.setTextRange(ts.factory.createClassDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, a.name as ts.Identifier || ts.factory.createIdentifier("__class__"), + /* typeParameters */ undefined, + /* heritageClauses */ undefined, []), a.node); + } + else { + a.children = ts.concatenate(a.children, b.children); + if (a.children) { + mergeChildren(a.children, a); } - return true; } - return bAssignmentDeclarationKind === ts.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) { + ts.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(ts.setTextRange(ts.factory.createClassDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, a.name as ts.Identifier || ts.factory.createIdentifier("__class__"), + /* typeParameters */ undefined, + /* heritageClauses */ undefined, []), b.node)); } - return false; + return true; } + return bAssignmentDeclarationKind === ts.AssignmentDeclarationKind.None ? false : true; +} - /** a and b have the same name, but they may not be mergeable. */ - function shouldReallyMerge(a: ts.Node, b: ts.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 ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - return ts.isStatic(a) === ts.isStatic(b); - case ts.SyntaxKind.ModuleDeclaration: - return areSameModule(a as ts.ModuleDeclaration, b as ts.ModuleDeclaration) - && getFullyQualifiedModuleName(a as ts.ModuleDeclaration) === getFullyQualifiedModuleName(b as ts.ModuleDeclaration); - default: - return 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; } - - function isSynthesized(node: ts.Node) { - return !!(node.flags & ts.NodeFlags.Synthesized); + if (shouldReallyMerge(a.node, b.node, parent)) { + merge(a, b); + return true; } + return false; +} - // 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: ts.Node, parent: NavigationBarNode): boolean { - const par = ts.isModuleBlock(n.parent) ? n.parent.parent : n.parent; - return par === parent.node || ts.contains(parent.additionalNodes, par); +/** a and b have the same name, but they may not be mergeable. */ +function shouldReallyMerge(a: ts.Node, b: ts.Node, parent: NavigationBarNode): boolean { + if (a.kind !== b.kind || a.parent !== b.parent && !(isOwnChild(a, parent) && isOwnChild(b, parent))) { + return false; } - - // 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: ts.ModuleDeclaration, b: ts.ModuleDeclaration): boolean { - if (!a.body || !b.body) { - return a.body === b.body; - } - return a.body.kind === b.body.kind && (a.body.kind !== ts.SyntaxKind.ModuleDeclaration || areSameModule(a.body as ts.ModuleDeclaration, b.body as ts.ModuleDeclaration)); + switch (a.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + return ts.isStatic(a) === ts.isStatic(b); + case ts.SyntaxKind.ModuleDeclaration: + return areSameModule(a as ts.ModuleDeclaration, b as ts.ModuleDeclaration) + && getFullyQualifiedModuleName(a as ts.ModuleDeclaration) === getFullyQualifiedModuleName(b as ts.ModuleDeclaration); + default: + return true; } +} - /** 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); - } +function isSynthesized(node: ts.Node) { + return !!(node.flags & ts.NodeFlags.Synthesized); +} - target.children = ts.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() {} } };` +function isOwnChild(n: ts.Node, parent: NavigationBarNode): boolean { + const par = ts.isModuleBlock(n.parent) ? n.parent.parent : n.parent; + return par === parent.node || ts.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: ts.ModuleDeclaration, b: ts.ModuleDeclaration): boolean { + if (!a.body || !b.body) { + return a.body === b.body; } + return a.body.kind === b.body.kind && (a.body.kind !== ts.SyntaxKind.ModuleDeclaration || areSameModule(a.body as ts.ModuleDeclaration, b.body as ts.ModuleDeclaration)); +} - /** Recursively ensure that each NavNode's children are in sorted order. */ - function sortChildren(children: NavigationBarNode[]): void { - children.sort(compareChildren); +/** 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); } - function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) { - return ts.compareStringsCaseSensitiveUI(tryGetName(child1.node)!, tryGetName(child2.node)!) // TODO: GH#18217 - || ts.compareValues(navigationBarNodeKind(child1), navigationBarNodeKind(child2)); + target.children = ts.concatenate(target.children, source.children); + if (target.children) { + mergeChildren(target.children, target); + sortChildren(target.children); } +} - /** - * 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: ts.Node): string | undefined { - if (node.kind === ts.SyntaxKind.ModuleDeclaration) { - return getModuleName(node as ts.ModuleDeclaration); - } +/** Recursively ensure that each NavNode's children are in sorted order. */ +function sortChildren(children: NavigationBarNode[]): void { + children.sort(compareChildren); +} - const declName = ts.getNameOfDeclaration(node as ts.Declaration); - if (declName && ts.isPropertyName(declName)) { - const propertyName = ts.getPropertyNameForPropertyNameNode(declName); - return propertyName && ts.unescapeLeadingUnderscores(propertyName); - } - switch (node.kind) { - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.ClassExpression: - return getFunctionOrClassName(node as ts.FunctionExpression | ts.ArrowFunction | ts.ClassExpression); - default: - return undefined; - } +function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) { + return ts.compareStringsCaseSensitiveUI(tryGetName(child1.node)!, tryGetName(child2.node)!) // TODO: GH#18217 + || ts.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: ts.Node): string | undefined { + if (node.kind === ts.SyntaxKind.ModuleDeclaration) { + return getModuleName(node as ts.ModuleDeclaration); + } + + const declName = ts.getNameOfDeclaration(node as ts.Declaration); + if (declName && ts.isPropertyName(declName)) { + const propertyName = ts.getPropertyNameForPropertyNameNode(declName); + return propertyName && ts.unescapeLeadingUnderscores(propertyName); + } + switch (node.kind) { + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.ClassExpression: + return getFunctionOrClassName(node as ts.FunctionExpression | ts.ArrowFunction | ts.ClassExpression); + default: + return undefined; } +} - function getItemName(node: ts.Node, name: ts.Node | undefined): string { - if (node.kind === ts.SyntaxKind.ModuleDeclaration) { - return cleanText(getModuleName(node as ts.ModuleDeclaration)); - } +function getItemName(node: ts.Node, name: ts.Node | undefined): string { + if (node.kind === ts.SyntaxKind.ModuleDeclaration) { + return cleanText(getModuleName(node as ts.ModuleDeclaration)); + } + + if (name) { + const text = ts.isIdentifier(name) ? name.text + : ts.isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]` + : nodeText(name); + if (text.length > 0) { + return cleanText(text); + } + } + + switch (node.kind) { + case ts.SyntaxKind.SourceFile: + const sourceFile = node as ts.SourceFile; + return ts.isExternalModule(sourceFile) + ? `"${ts.escapeString(ts.getBaseFileName(ts.removeFileExtension(ts.normalizePath(sourceFile.fileName))))}"` + : ""; + case ts.SyntaxKind.ExportAssignment: + return ts.isExportAssignment(node) && node.isExportEquals ? ts.InternalSymbolName.ExportEquals : ts.InternalSymbolName.Default; + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + if (ts.getSyntacticModifierFlags(node) & ts.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 readability in the + // navigation bar. + return getFunctionOrClassName(node as ts.ArrowFunction | ts.FunctionExpression | ts.ClassExpression); + case ts.SyntaxKind.Constructor: + return "constructor"; + case ts.SyntaxKind.ConstructSignature: + return "new()"; + case ts.SyntaxKind.CallSignature: + return "()"; + case ts.SyntaxKind.IndexSignature: + return "[]"; + default: + return ""; + } +} - if (name) { - const text = ts.isIdentifier(name) ? name.text - : ts.isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]` - : nodeText(name); - if (text.length > 0) { - return cleanText(text); +/** 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); + } } } + } + 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; + } - switch (node.kind) { + // Some nodes are otherwise important enough to always include in the primary navigation menu. + switch (navigationBarNodeKind(item)) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ModuleDeclaration: case ts.SyntaxKind.SourceFile: - const sourceFile = node as ts.SourceFile; - return ts.isExternalModule(sourceFile) - ? `"${ts.escapeString(ts.getBaseFileName(ts.removeFileExtension(ts.normalizePath(sourceFile.fileName))))}"` - : ""; - case ts.SyntaxKind.ExportAssignment: - return ts.isExportAssignment(node) && node.isExportEquals ? ts.InternalSymbolName.ExportEquals : ts.InternalSymbolName.Default; + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocCallbackTag: + return true; + case ts.SyntaxKind.ArrowFunction: case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - if (ts.getSyntacticModifierFlags(node) & ts.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 readability in the - // navigation bar. - return getFunctionOrClassName(node as ts.ArrowFunction | ts.FunctionExpression | ts.ClassExpression); - case ts.SyntaxKind.Constructor: - return "constructor"; - case ts.SyntaxKind.ConstructSignature: - return "new()"; - case ts.SyntaxKind.CallSignature: - return "()"; - case ts.SyntaxKind.IndexSignature: - return "[]"; - default: - return ""; - } - } + return isTopLevelFunctionDeclaration(item); - /** 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); - } - } - } + default: + 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 as ts.FunctionDeclaration).body) { + return false; } - // Some nodes are otherwise important enough to always include in the primary navigation menu. - switch (navigationBarNodeKind(item)) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ModuleDeclaration: + switch (navigationBarNodeKind(item.parent!)) { + case ts.SyntaxKind.ModuleBlock: case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: return true; - - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - return isTopLevelFunctionDeclaration(item); - default: return false; } - function isTopLevelFunctionDeclaration(item: NavigationBarNode): boolean { - if (!(item.node as ts.FunctionDeclaration).body) { - return false; - } - - switch (navigationBarNodeKind(item.parent!)) { - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - return true; - default: - return false; - } - } } } +} - function convertToTree(n: NavigationBarNode): ts.NavigationTree { - return { - text: getItemName(n.node, n.name), - kind: ts.getNodeKind(n.node), - kindModifiers: getModifiers(n.node), - spans: getSpans(n), - nameSpan: n.name && getNodeSpan(n.name), - childItems: ts.map(n.children, convertToTree) - }; - } +function convertToTree(n: NavigationBarNode): ts.NavigationTree { + return { + text: getItemName(n.node, n.name), + kind: ts.getNodeKind(n.node), + kindModifiers: getModifiers(n.node), + spans: getSpans(n), + nameSpan: n.name && getNodeSpan(n.name), + childItems: ts.map(n.children, convertToTree) + }; +} + +function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): ts.NavigationBarItem { + return { + text: getItemName(n.node, n.name), + kind: ts.getNodeKind(n.node), + kindModifiers: getModifiers(n.node), + spans: getSpans(n), + childItems: ts.map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray, + indent: n.indent, + bolded: false, + grayed: false + }; - function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): ts.NavigationBarItem { + function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): ts.NavigationBarItem { return { text: getItemName(n.node, n.name), kind: ts.getNodeKind(n.node), - kindModifiers: getModifiers(n.node), + kindModifiers: ts.getNodeModifiers(n.node), spans: getSpans(n), - childItems: ts.map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray, - indent: n.indent, + childItems: emptyChildItemArray, + indent: 0, bolded: false, grayed: false }; + } +} - function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): ts.NavigationBarItem { - return { - text: getItemName(n.node, n.name), - kind: ts.getNodeKind(n.node), - kindModifiers: ts.getNodeModifiers(n.node), - spans: getSpans(n), - childItems: emptyChildItemArray, - indent: 0, - bolded: false, - grayed: false - }; +function getSpans(n: NavigationBarNode): ts.TextSpan[] { + const spans = [getNodeSpan(n.node)]; + if (n.additionalNodes) { + for (const node of n.additionalNodes) { + spans.push(getNodeSpan(node)); } } + return spans; +} - function getSpans(n: NavigationBarNode): ts.TextSpan[] { - const spans = [getNodeSpan(n.node)]; - if (n.additionalNodes) { - for (const node of n.additionalNodes) { - spans.push(getNodeSpan(node)); - } - } - return spans; +function getModuleName(moduleDeclaration: ts.ModuleDeclaration): string { + // We want to maintain quotation marks. + if (ts.isAmbientModule(moduleDeclaration)) { + return ts.getTextOfNode(moduleDeclaration.name); } - function getModuleName(moduleDeclaration: ts.ModuleDeclaration): string { - // We want to maintain quotation marks. - if (ts.isAmbientModule(moduleDeclaration)) { - return ts.getTextOfNode(moduleDeclaration.name); - } + return getFullyQualifiedModuleName(moduleDeclaration); +} - return getFullyQualifiedModuleName(moduleDeclaration); +function getFullyQualifiedModuleName(moduleDeclaration: ts.ModuleDeclaration): string { + // Otherwise, we need to aggregate each identifier to build up the qualified name. + const result = [ts.getTextOfIdentifierOrLiteral(moduleDeclaration.name)]; + while (moduleDeclaration.body && moduleDeclaration.body.kind === ts.SyntaxKind.ModuleDeclaration) { + moduleDeclaration = moduleDeclaration.body; + result.push(ts.getTextOfIdentifierOrLiteral(moduleDeclaration.name)); } + return result.join("."); +} - function getFullyQualifiedModuleName(moduleDeclaration: ts.ModuleDeclaration): string { - // Otherwise, we need to aggregate each identifier to build up the qualified name. - const result = [ts.getTextOfIdentifierOrLiteral(moduleDeclaration.name)]; - while (moduleDeclaration.body && moduleDeclaration.body.kind === ts.SyntaxKind.ModuleDeclaration) { - moduleDeclaration = moduleDeclaration.body; - result.push(ts.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: ts.ModuleDeclaration): ts.ModuleDeclaration { + return decl.body && ts.isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl; +} - /** - * 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: ts.ModuleDeclaration): ts.ModuleDeclaration { - return decl.body && ts.isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl; - } +function isComputedProperty(member: ts.EnumMember): boolean { + return !member.name || member.name.kind === ts.SyntaxKind.ComputedPropertyName; +} - function isComputedProperty(member: ts.EnumMember): boolean { - return !member.name || member.name.kind === ts.SyntaxKind.ComputedPropertyName; - } +function getNodeSpan(node: ts.Node): ts.TextSpan { + return node.kind === ts.SyntaxKind.SourceFile ? ts.createTextSpanFromRange(node) : ts.createTextSpanFromNode(node, curSourceFile); +} - function getNodeSpan(node: ts.Node): ts.TextSpan { - return node.kind === ts.SyntaxKind.SourceFile ? ts.createTextSpanFromRange(node) : ts.createTextSpanFromNode(node, curSourceFile); +function getModifiers(node: ts.Node): string { + if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclaration) { + node = node.parent; } + return ts.getNodeModifiers(node); +} - function getModifiers(node: ts.Node): string { - if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclaration) { - node = node.parent; - } - return ts.getNodeModifiers(node); +function getFunctionOrClassName(node: ts.FunctionExpression | ts.FunctionDeclaration | ts.ArrowFunction | ts.ClassLikeDeclaration): string { + const { parent } = node; + if (node.name && ts.getFullWidth(node.name) > 0) { + return cleanText(ts.declarationNameToString(node.name)); } + // See if it is a var initializer. If so, use the var name. + else if (ts.isVariableDeclaration(parent)) { + return cleanText(ts.declarationNameToString(parent.name)); + } + // See if it is of the form " = function(){...}". If so, use the text from the left-hand side. + else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + return nodeText(parent.left).replace(whiteSpaceRegex, ""); + } + // See if it is a property assignment, and if so use the property name + else if (ts.isPropertyAssignment(parent)) { + return nodeText(parent.name); + } + // Default exports are named "default" + else if (ts.getSyntacticModifierFlags(node) & ts.ModifierFlags.Default) { + return "default"; + } + else if (ts.isClassLike(node)) { + return ""; + } + else if (ts.isCallExpression(parent)) { + let name = getCalledExpressionName(parent.expression); + if (name !== undefined) { + name = cleanText(name); - function getFunctionOrClassName(node: ts.FunctionExpression | ts.FunctionDeclaration | ts.ArrowFunction | ts.ClassLikeDeclaration): string { - const { parent } = node; - if (node.name && ts.getFullWidth(node.name) > 0) { - return cleanText(ts.declarationNameToString(node.name)); - } - // See if it is a var initializer. If so, use the var name. - else if (ts.isVariableDeclaration(parent)) { - return cleanText(ts.declarationNameToString(parent.name)); - } - // See if it is of the form " = function(){...}". If so, use the text from the left-hand side. - else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - return nodeText(parent.left).replace(whiteSpaceRegex, ""); - } - // See if it is a property assignment, and if so use the property name - else if (ts.isPropertyAssignment(parent)) { - return nodeText(parent.name); - } - // Default exports are named "default" - else if (ts.getSyntacticModifierFlags(node) & ts.ModifierFlags.Default) { - return "default"; - } - else if (ts.isClassLike(node)) { - return ""; - } - else if (ts.isCallExpression(parent)) { - let name = getCalledExpressionName(parent.expression); - if (name !== undefined) { - name = cleanText(name); - - if (name.length > maxLength) { - return `${name} callback`; - } - - const args = cleanText(ts.mapDefined(parent.arguments, a => ts.isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", ")); - return `${name}(${args}) callback`; + if (name.length > maxLength) { + return `${name} callback`; } + + const args = cleanText(ts.mapDefined(parent.arguments, a => ts.isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", ")); + return `${name}(${args}) callback`; } - return ""; } + return ""; +} - // See also 'tryGetPropertyAccessOrIdentifierToString' - function getCalledExpressionName(expr: ts.Expression): string | undefined { - if (ts.isIdentifier(expr)) { - return expr.text; - } - else if (ts.isPropertyAccessExpression(expr)) { - const left = getCalledExpressionName(expr.expression); - const right = expr.name.text; - return left === undefined ? right : `${left}.${right}`; - } - else { - return undefined; - } +// See also 'tryGetPropertyAccessOrIdentifierToString' +function getCalledExpressionName(expr: ts.Expression): string | undefined { + if (ts.isIdentifier(expr)) { + return expr.text; + } + else if (ts.isPropertyAccessExpression(expr)) { + const left = getCalledExpressionName(expr.expression); + const right = expr.name.text; + return left === undefined ? right : `${left}.${right}`; } + else { + return undefined; + } +} - function isFunctionOrClassExpression(node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression | ts.ClassExpression { - switch (node.kind) { - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ClassExpression: - return true; - default: - return false; - } +function isFunctionOrClassExpression(node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression | ts.ClassExpression { + switch (node.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ClassExpression: + return true; + default: + return false; } +} - 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; +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, ""); - } + // 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 5740b81ec254a..18267dfe02fec 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -1,504 +1,504 @@ /* @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: ts.SourceFile, formatContext: ts.formatting.FormatContext, host: ts.LanguageServiceHost, program: ts.Program, preferences: ts.UserPreferences, skipDestructiveCodeActions?: boolean) { - const changeTracker = ts.textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); - const coalesceAndOrganizeImports = (importGroup: readonly ts.ImportDeclaration[]) => ts.stableSort(coalesceImports(removeUnusedImports(importGroup, sourceFile, program, skipDestructiveCodeActions)), (s1, s2) => compareImportsOrRequireStatements(s1, s2)); - - // All of the old ImportDeclarations in the file, in syntactic order. - const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(ts.isImportDeclaration)); - topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports)); - - // All of the old ExportDeclarations in the file, in syntactic order. - const topLevelExportDecls = sourceFile.statements.filter(ts.isExportDeclaration); - organizeImportsWorker(topLevelExportDecls, coalesceExports); - - for (const ambientModule of sourceFile.statements.filter(ts.isAmbientModule)) { - if (!ambientModule.body) - continue; - const ambientModuleImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(ts.isImportDeclaration)); - ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports)); - - const ambientModuleExportDecls = ambientModule.body.statements.filter(ts.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: ts.SourceFile, formatContext: ts.formatting.FormatContext, host: ts.LanguageServiceHost, program: ts.Program, preferences: ts.UserPreferences, skipDestructiveCodeActions?: boolean) { + const changeTracker = ts.textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); + const coalesceAndOrganizeImports = (importGroup: readonly ts.ImportDeclaration[]) => ts.stableSort(coalesceImports(removeUnusedImports(importGroup, sourceFile, program, skipDestructiveCodeActions)), (s1, s2) => compareImportsOrRequireStatements(s1, s2)); + + // All of the old ImportDeclarations in the file, in syntactic order. + const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(ts.isImportDeclaration)); + topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports)); + + // All of the old ExportDeclarations in the file, in syntactic order. + const topLevelExportDecls = sourceFile.statements.filter(ts.isExportDeclaration); + organizeImportsWorker(topLevelExportDecls, coalesceExports); + + for (const ambientModule of sourceFile.statements.filter(ts.isAmbientModule)) { + if (!ambientModule.body) + continue; + const ambientModuleImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(ts.isImportDeclaration)); + ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports)); + + const ambientModuleExportDecls = ambientModule.body.statements.filter(ts.isExportDeclaration); + organizeImportsWorker(ambientModuleExportDecls, coalesceExports); + } - return changeTracker.getChanges(); + return changeTracker.getChanges(); - function organizeImportsWorker(oldImportDecls: readonly T[], coalesce: (group: readonly T[]) => readonly T[]) { - if (ts.length(oldImportDecls) === 0) { - return; - } + function organizeImportsWorker(oldImportDecls: readonly T[], coalesce: (group: readonly T[]) => readonly T[]) { + if (ts.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. - ts.suppressLeadingTrivia(oldImportDecls[0]); - const oldImportGroups = ts.group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!); - const sortedImportGroups = ts.stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier)); - const newImportDecls = ts.flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier!) - ? coalesce(importGroup) - : importGroup); - - // Delete all nodes if there are no imports. - if (newImportDecls.length === 0) { - // Consider the first node to have trailingTrivia as we want to exclude the - // "header" comment. - changeTracker.deleteNodes(sourceFile, oldImportDecls, { - trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, - }, /*hasTrailingComment*/ true); - } - else { - // Note: Delete the surrounding trivia because it will have been retained in newImportDecls. - const replaceOptions = { - leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, - trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, - suffix: ts.getNewLineOrDefaultFromHost(host, formatContext.options), - }; - changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions); - const hasTrailingComment = changeTracker.nodeHasTrailingComment(sourceFile, oldImportDecls[0], replaceOptions); - changeTracker.deleteNodes(sourceFile, oldImportDecls.slice(1), { - trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, - }, hasTrailingComment); - } + // 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. + ts.suppressLeadingTrivia(oldImportDecls[0]); + const oldImportGroups = ts.group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!); + const sortedImportGroups = ts.stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier)); + const newImportDecls = ts.flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier!) + ? coalesce(importGroup) + : importGroup); + + // Delete all nodes if there are no imports. + if (newImportDecls.length === 0) { + // Consider the first node to have trailingTrivia as we want to exclude the + // "header" comment. + changeTracker.deleteNodes(sourceFile, oldImportDecls, { + trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, + }, /*hasTrailingComment*/ true); + } + else { + // Note: Delete the surrounding trivia because it will have been retained in newImportDecls. + const replaceOptions = { + leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, + trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, + suffix: ts.getNewLineOrDefaultFromHost(host, formatContext.options), + }; + changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions); + const hasTrailingComment = changeTracker.nodeHasTrailingComment(sourceFile, oldImportDecls[0], replaceOptions); + changeTracker.deleteNodes(sourceFile, oldImportDecls.slice(1), { + trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include, + }, hasTrailingComment); } } +} - function groupImportsByNewlineContiguous(sourceFile: ts.SourceFile, importDecls: ts.ImportDeclaration[]): ts.ImportDeclaration[][] { - const scanner = ts.createScanner(sourceFile.languageVersion, /*skipTrivia*/ false, sourceFile.languageVariant); - const groupImports: ts.ImportDeclaration[][] = []; - let groupIndex = 0; - for (const topLevelImportDecl of importDecls) { - if (isNewGroup(sourceFile, topLevelImportDecl, scanner)) { - groupIndex++; - } - - if (!groupImports[groupIndex]) { - groupImports[groupIndex] = []; - } +function groupImportsByNewlineContiguous(sourceFile: ts.SourceFile, importDecls: ts.ImportDeclaration[]): ts.ImportDeclaration[][] { + const scanner = ts.createScanner(sourceFile.languageVersion, /*skipTrivia*/ false, sourceFile.languageVariant); + const groupImports: ts.ImportDeclaration[][] = []; + let groupIndex = 0; + for (const topLevelImportDecl of importDecls) { + if (isNewGroup(sourceFile, topLevelImportDecl, scanner)) { + groupIndex++; + } - groupImports[groupIndex].push(topLevelImportDecl); + if (!groupImports[groupIndex]) { + groupImports[groupIndex] = []; } - return groupImports; + groupImports[groupIndex].push(topLevelImportDecl); } - // a new group is created if an import includes at least two new line - // new line from multi-line comment doesn't count - function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.ImportDeclaration, scanner: ts.Scanner) { - const startPos = topLevelImportDecl.getFullStart(); - const endPos = topLevelImportDecl.getStart(); - scanner.setText(sourceFile.text, startPos, endPos - startPos); + return groupImports; +} - let numberOfNewLines = 0; - while (scanner.getTokenPos() < endPos) { - const tokenKind = scanner.scan(); +// a new group is created if an import includes at least two new line +// new line from multi-line comment doesn't count +function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.ImportDeclaration, scanner: ts.Scanner) { + const startPos = topLevelImportDecl.getFullStart(); + const endPos = topLevelImportDecl.getStart(); + scanner.setText(sourceFile.text, startPos, endPos - startPos); - if (tokenKind === ts.SyntaxKind.NewLineTrivia) { - numberOfNewLines++; + let numberOfNewLines = 0; + while (scanner.getTokenPos() < endPos) { + const tokenKind = scanner.scan(); - if (numberOfNewLines >= 2) { - return true; - } + if (tokenKind === ts.SyntaxKind.NewLineTrivia) { + numberOfNewLines++; + + if (numberOfNewLines >= 2) { + return true; } } - - return false; } - function removeUnusedImports(oldImports: readonly ts.ImportDeclaration[], sourceFile: ts.SourceFile, program: ts.Program, skipDestructiveCodeActions: boolean | undefined) { - // As a precaution, consider unused import detection to be destructive (GH #43051) - if (skipDestructiveCodeActions) { - return oldImports; - } + return false; +} - const typeChecker = program.getTypeChecker(); - const compilerOptions = program.getCompilerOptions(); - const jsxNamespace = typeChecker.getJsxNamespace(sourceFile); - const jsxFragmentFactory = typeChecker.getJsxFragmentFactory(sourceFile); - const jsxElementsPresent = !!(sourceFile.transformFlags & ts.TransformFlags.ContainsJsx); - const usedImports: ts.ImportDeclaration[] = []; +function removeUnusedImports(oldImports: readonly ts.ImportDeclaration[], sourceFile: ts.SourceFile, program: ts.Program, skipDestructiveCodeActions: boolean | undefined) { + // As a precaution, consider unused import detection to be destructive (GH #43051) + if (skipDestructiveCodeActions) { + return oldImports; + } - for (const importDecl of oldImports) { - const { importClause, moduleSpecifier } = importDecl; + const typeChecker = program.getTypeChecker(); + const compilerOptions = program.getCompilerOptions(); + const jsxNamespace = typeChecker.getJsxNamespace(sourceFile); + const jsxFragmentFactory = typeChecker.getJsxFragmentFactory(sourceFile); + const jsxElementsPresent = !!(sourceFile.transformFlags & ts.TransformFlags.ContainsJsx); + const usedImports: ts.ImportDeclaration[] = []; - if (!importClause) { - // Imports without import clauses are assumed to be included for their side effects and are not removed. - usedImports.push(importDecl); - continue; - } + for (const importDecl of oldImports) { + const { importClause, moduleSpecifier } = importDecl; - let { name, namedBindings } = importClause; + if (!importClause) { + // Imports without import clauses are assumed to be included for their side effects and are not removed. + usedImports.push(importDecl); + continue; + } - // Default import - if (name && !isDeclarationUsed(name)) { - name = undefined; - } + let { name, namedBindings } = importClause; - if (namedBindings) { - if (ts.isNamespaceImport(namedBindings)) { - // Namespace import - if (!isDeclarationUsed(namedBindings.name)) { - namedBindings = undefined; - } + // Default import + if (name && !isDeclarationUsed(name)) { + name = undefined; + } + + if (namedBindings) { + if (ts.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 - ? ts.factory.updateNamedImports(namedBindings, newElements) - : undefined; - } + } + else { + // List of named imports + const newElements = namedBindings.elements.filter(e => isDeclarationUsed(e.name)); + if (newElements.length < namedBindings.elements.length) { + namedBindings = newElements.length + ? ts.factory.updateNamedImports(namedBindings, newElements) + : undefined; } } + } - if (name || namedBindings) { - usedImports.push(updateImportDeclarationAndClause(importDecl, name, namedBindings)); + 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(ts.factory.createImportDeclaration(importDecl.decorators, importDecl.modifiers, + /*importClause*/ undefined, moduleSpecifier, + /*assertClause*/ undefined)); } - // 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(ts.factory.createImportDeclaration(importDecl.decorators, importDecl.modifiers, - /*importClause*/ undefined, moduleSpecifier, - /*assertClause*/ undefined)); - } - // 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); - } + // 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); } } + } - return usedImports; + return usedImports; - function isDeclarationUsed(identifier: ts.Identifier) { - // The JSX factory symbol is always used if JSX elements are present - even if they are not allowed. - return jsxElementsPresent && (identifier.text === jsxNamespace || jsxFragmentFactory && identifier.text === jsxFragmentFactory) && ts.jsxModeNeedsExplicitImport(compilerOptions.jsx) || - ts.FindAllReferences.Core.isSymbolReferencedInFile(identifier, typeChecker, sourceFile); - } + function isDeclarationUsed(identifier: ts.Identifier) { + // The JSX factory symbol is always used if JSX elements are present - even if they are not allowed. + return jsxElementsPresent && (identifier.text === jsxNamespace || jsxFragmentFactory && identifier.text === jsxFragmentFactory) && ts.jsxModeNeedsExplicitImport(compilerOptions.jsx) || + ts.FindAllReferences.Core.isSymbolReferencedInFile(identifier, typeChecker, sourceFile); } +} + +function hasModuleDeclarationMatchingSpecifier(sourceFile: ts.SourceFile, moduleSpecifier: ts.Expression) { + const moduleSpecifierText = ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text; + return ts.isString(moduleSpecifierText) && ts.some(sourceFile.moduleAugmentations, moduleName => ts.isStringLiteral(moduleName) + && moduleName.text === moduleSpecifierText); +} + +function getExternalModuleName(specifier: ts.Expression) { + return specifier !== undefined && ts.isStringLiteralLike(specifier) + ? specifier.text + : undefined; +} - function hasModuleDeclarationMatchingSpecifier(sourceFile: ts.SourceFile, moduleSpecifier: ts.Expression) { - const moduleSpecifierText = ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text; - return ts.isString(moduleSpecifierText) && ts.some(sourceFile.moduleAugmentations, moduleName => ts.isStringLiteral(moduleName) - && moduleName.text === moduleSpecifierText); +// Internal for testing +/** + * @param importGroup a list of ImportDeclarations, all with the same module name. + */ +export function coalesceImports(importGroup: readonly ts.ImportDeclaration[]) { + if (importGroup.length === 0) { + return importGroup; } - function getExternalModuleName(specifier: ts.Expression) { - return specifier !== undefined && ts.isStringLiteralLike(specifier) - ? specifier.text - : undefined; + const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup); + + const coalescedImports: ts.ImportDeclaration[] = []; + + if (importWithoutClause) { + coalescedImports.push(importWithoutClause); } - // Internal for testing - /** - * @param importGroup a list of ImportDeclarations, all with the same module name. - */ - export function coalesceImports(importGroup: readonly ts.ImportDeclaration[]) { - if (importGroup.length === 0) { - return importGroup; + 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 sortedNamespaceImports = ts.stableSort(namespaceImports, (i1, i2) => compareIdentifiers((i1.importClause!.namedBindings as ts.NamespaceImport).name, (i2.importClause!.namedBindings as ts.NamespaceImport).name)); // TODO: GH#18217 - const coalescedImports: ts.ImportDeclaration[] = []; + for (const namespaceImport of sortedNamespaceImports) { + // Drop the name, if any + coalescedImports.push(updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217 + } - if (importWithoutClause) { - coalescedImports.push(importWithoutClause); + if (defaultImports.length === 0 && namedImports.length === 0) { + continue; } - 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; + let newDefaultImport: ts.Identifier | undefined; + const newImportSpecifiers: ts.ImportSpecifier[] = []; + if (defaultImports.length === 1) { + newDefaultImport = defaultImports[0].importClause!.name; + } + else { + for (const defaultImport of defaultImports) { + newImportSpecifiers.push(ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217 } + } + + newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); + + const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers); + const importDecl = defaultImports.length > 0 + ? defaultImports[0] + : namedImports[0]; + + const newNamedImports = sortedImportSpecifiers.length === 0 + ? newDefaultImport + ? undefined + : ts.factory.createNamedImports(ts.emptyArray) + : namedImports.length === 0 + ? ts.factory.createNamedImports(sortedImportSpecifiers) + : ts.factory.updateNamedImports(namedImports[0].importClause!.namedBindings as ts.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)); + } + } - const sortedNamespaceImports = ts.stableSort(namespaceImports, (i1, i2) => compareIdentifiers((i1.importClause!.namedBindings as ts.NamespaceImport).name, (i2.importClause!.namedBindings as ts.NamespaceImport).name)); // TODO: GH#18217 + return coalescedImports; - 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; - } +interface ImportGroup { + defaultImports: ts.ImportDeclaration[]; + namespaceImports: ts.ImportDeclaration[]; + namedImports: ts.ImportDeclaration[]; +} - let newDefaultImport: ts.Identifier | undefined; - const newImportSpecifiers: ts.ImportSpecifier[] = []; - if (defaultImports.length === 1) { - newDefaultImport = defaultImports[0].importClause!.name; - } - else { - for (const defaultImport of defaultImports) { - newImportSpecifiers.push(ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217 - } - } +/* + * 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`. + */ +function getCategorizedImports(importGroup: readonly ts.ImportDeclaration[]) { + let importWithoutClause: ts.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); + } - newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); - - const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers); - const importDecl = defaultImports.length > 0 - ? defaultImports[0] - : namedImports[0]; - - const newNamedImports = sortedImportSpecifiers.length === 0 - ? newDefaultImport - ? undefined - : ts.factory.createNamedImports(ts.emptyArray) - : namedImports.length === 0 - ? ts.factory.createNamedImports(sortedImportSpecifiers) - : ts.factory.updateNamedImports(namedImports[0].importClause!.namedBindings as ts.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)); + if (namedBindings) { + if (ts.isNamespaceImport(namedBindings)) { + group.namespaceImports.push(importDeclaration); } else { - coalescedImports.push(updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports)); + group.namedImports.push(importDeclaration); } } + } - return coalescedImports; + return { + importWithoutClause, + typeOnlyImports, + regularImports, + }; +} +// Internal for testing +/** + * @param exportGroup a list of ExportDeclarations, all with the same module name. + */ +export function coalesceExports(exportGroup: readonly ts.ExportDeclaration[]) { + if (exportGroup.length === 0) { + return exportGroup; } - interface ImportGroup { - defaultImports: ts.ImportDeclaration[]; - namespaceImports: ts.ImportDeclaration[]; - namedImports: ts.ImportDeclaration[]; + const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); + + const coalescedExports: ts.ExportDeclaration[] = []; + + if (exportWithoutClause) { + coalescedExports.push(exportWithoutClause); } + for (const exportGroup of [namedExports, typeOnlyExports]) { + if (exportGroup.length === 0) { + continue; + } + const newExportSpecifiers: ts.ExportSpecifier[] = []; + newExportSpecifiers.push(...ts.flatMap(exportGroup, i => i.exportClause && ts.isNamedExports(i.exportClause) ? i.exportClause.elements : ts.emptyArray)); + + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); + + const exportDecl = exportGroup[0]; + coalescedExports.push(ts.factory.updateExportDeclaration(exportDecl, exportDecl.decorators, exportDecl.modifiers, exportDecl.isTypeOnly, exportDecl.exportClause && (ts.isNamedExports(exportDecl.exportClause) ? + ts.factory.updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) : + ts.factory.updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name)), exportDecl.moduleSpecifier, exportDecl.assertClause)); + } + + 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 ts.ImportDeclaration[]) { - let importWithoutClause: ts.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 ts.ExportDeclaration[]) { + let exportWithoutClause: ts.ExportDeclaration | undefined; + const namedExports: ts.ExportDeclaration[] = []; + const typeOnlyExports: ts.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 (ts.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 ts.ExportDeclaration[]) { - if (exportGroup.length === 0) { - return exportGroup; - } - - const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); - - const coalescedExports: ts.ExportDeclaration[] = []; - - if (exportWithoutClause) { - coalescedExports.push(exportWithoutClause); - } - - for (const exportGroup of [namedExports, typeOnlyExports]) { - if (exportGroup.length === 0) { - continue; - } - const newExportSpecifiers: ts.ExportSpecifier[] = []; - newExportSpecifiers.push(...ts.flatMap(exportGroup, i => i.exportClause && ts.isNamedExports(i.exportClause) ? i.exportClause.elements : ts.emptyArray)); - - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); - - const exportDecl = exportGroup[0]; - coalescedExports.push(ts.factory.updateExportDeclaration(exportDecl, exportDecl.decorators, exportDecl.modifiers, exportDecl.isTypeOnly, exportDecl.exportClause && (ts.isNamedExports(exportDecl.exportClause) ? - ts.factory.updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) : - ts.factory.updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name)), exportDecl.moduleSpecifier, exportDecl.assertClause)); - } - - 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 ts.ExportDeclaration[]) { - let exportWithoutClause: ts.ExportDeclaration | undefined; - const namedExports: ts.ExportDeclaration[] = []; - const typeOnlyExports: ts.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: ts.ImportDeclaration, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined) { - return ts.factory.updateImportDeclaration(importDeclaration, importDeclaration.decorators, importDeclaration.modifiers, ts.factory.updateImportClause(importDeclaration.importClause!, importDeclaration.importClause!.isTypeOnly, name, namedBindings), // TODO: GH#18217 - importDeclaration.moduleSpecifier, importDeclaration.assertClause); - } - function sortSpecifiers(specifiers: readonly T[]) { - return ts.stableSort(specifiers, compareImportOrExportSpecifiers); - } - export function compareImportOrExportSpecifiers(s1: T, s2: T) { - return ts.compareBooleans(s1.isTypeOnly, s2.isTypeOnly) - || compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) - || compareIdentifiers(s1.name, s2.name); - } +function updateImportDeclarationAndClause(importDeclaration: ts.ImportDeclaration, name: ts.Identifier | undefined, namedBindings: ts.NamedImportBindings | undefined) { + return ts.factory.updateImportDeclaration(importDeclaration, importDeclaration.decorators, importDeclaration.modifiers, ts.factory.updateImportClause(importDeclaration.importClause!, importDeclaration.importClause!.isTypeOnly, name, namedBindings), // TODO: GH#18217 + importDeclaration.moduleSpecifier, importDeclaration.assertClause); +} +function sortSpecifiers(specifiers: readonly T[]) { + return ts.stableSort(specifiers, compareImportOrExportSpecifiers); +} +export function compareImportOrExportSpecifiers(s1: T, s2: T) { + return ts.compareBooleans(s1.isTypeOnly, s2.isTypeOnly) + || compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) + || compareIdentifiers(s1.name, s2.name); +} - /* internal */ // Exported for testing - export function compareModuleSpecifiers(m1: ts.Expression | undefined, m2: ts.Expression | undefined) { - const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); - const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); - return ts.compareBooleans(name1 === undefined, name2 === undefined) || - ts.compareBooleans(ts.isExternalModuleNameRelative(name1!), ts.isExternalModuleNameRelative(name2!)) || - ts.compareStringsCaseInsensitive(name1!, name2!); - } +/* internal */ // Exported for testing +export function compareModuleSpecifiers(m1: ts.Expression | undefined, m2: ts.Expression | undefined) { + const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); + const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); + return ts.compareBooleans(name1 === undefined, name2 === undefined) || + ts.compareBooleans(ts.isExternalModuleNameRelative(name1!), ts.isExternalModuleNameRelative(name2!)) || + ts.compareStringsCaseInsensitive(name1!, name2!); +} - function compareIdentifiers(s1: ts.Identifier, s2: ts.Identifier) { - return ts.compareStringsCaseInsensitive(s1.text, s2.text); - } +function compareIdentifiers(s1: ts.Identifier, s2: ts.Identifier) { + return ts.compareStringsCaseInsensitive(s1.text, s2.text); +} - function getModuleSpecifierExpression(declaration: ts.AnyImportOrRequireStatement): ts.Expression | undefined { - switch (declaration.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - return ts.tryCast(declaration.moduleReference, ts.isExternalModuleReference)?.expression; - case ts.SyntaxKind.ImportDeclaration: - return declaration.moduleSpecifier; - case ts.SyntaxKind.VariableStatement: - return declaration.declarationList.declarations[0].initializer.arguments[0]; - } +function getModuleSpecifierExpression(declaration: ts.AnyImportOrRequireStatement): ts.Expression | undefined { + switch (declaration.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + return ts.tryCast(declaration.moduleReference, ts.isExternalModuleReference)?.expression; + case ts.SyntaxKind.ImportDeclaration: + return declaration.moduleSpecifier; + case ts.SyntaxKind.VariableStatement: + return declaration.declarationList.declarations[0].initializer.arguments[0]; } +} - export function importsAreSorted(imports: readonly ts.AnyImportOrRequireStatement[]): imports is ts.SortedReadonlyArray { - return ts.arrayIsSorted(imports, compareImportsOrRequireStatements); - } +export function importsAreSorted(imports: readonly ts.AnyImportOrRequireStatement[]): imports is ts.SortedReadonlyArray { + return ts.arrayIsSorted(imports, compareImportsOrRequireStatements); +} - export function importSpecifiersAreSorted(imports: readonly ts.ImportSpecifier[]): imports is ts.SortedReadonlyArray { - return ts.arrayIsSorted(imports, compareImportOrExportSpecifiers); - } +export function importSpecifiersAreSorted(imports: readonly ts.ImportSpecifier[]): imports is ts.SortedReadonlyArray { + return ts.arrayIsSorted(imports, compareImportOrExportSpecifiers); +} - export function getImportDeclarationInsertionIndex(sortedImports: ts.SortedReadonlyArray, newImport: ts.AnyImportOrRequireStatement) { - const index = ts.binarySearch(sortedImports, newImport, ts.identity, compareImportsOrRequireStatements); - return index < 0 ? ~index : index; - } +export function getImportDeclarationInsertionIndex(sortedImports: ts.SortedReadonlyArray, newImport: ts.AnyImportOrRequireStatement) { + const index = ts.binarySearch(sortedImports, newImport, ts.identity, compareImportsOrRequireStatements); + return index < 0 ? ~index : index; +} - export function getImportSpecifierInsertionIndex(sortedImports: ts.SortedReadonlyArray, newImport: ts.ImportSpecifier) { - const index = ts.binarySearch(sortedImports, newImport, ts.identity, compareImportOrExportSpecifiers); - return index < 0 ? ~index : index; - } +export function getImportSpecifierInsertionIndex(sortedImports: ts.SortedReadonlyArray, newImport: ts.ImportSpecifier) { + const index = ts.binarySearch(sortedImports, newImport, ts.identity, compareImportOrExportSpecifiers); + return index < 0 ? ~index : index; +} - export function compareImportsOrRequireStatements(s1: ts.AnyImportOrRequireStatement, s2: ts.AnyImportOrRequireStatement) { - return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2); - } +export function compareImportsOrRequireStatements(s1: ts.AnyImportOrRequireStatement, s2: ts.AnyImportOrRequireStatement) { + return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2); +} - function compareImportKind(s1: ts.AnyImportOrRequireStatement, s2: ts.AnyImportOrRequireStatement) { - return ts.compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); - } +function compareImportKind(s1: ts.AnyImportOrRequireStatement, s2: ts.AnyImportOrRequireStatement) { + return ts.compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); +} - // 1. Side-effect imports - // 2. Type-only imports - // 3. Namespace imports - // 4. Default imports - // 5. Named imports - // 6. ImportEqualsDeclarations - // 7. Require variable statements - function getImportKindOrder(s1: ts.AnyImportOrRequireStatement) { - switch (s1.kind) { - case ts.SyntaxKind.ImportDeclaration: - if (!s1.importClause) - return 0; - if (s1.importClause.isTypeOnly) - return 1; - if (s1.importClause.namedBindings?.kind === ts.SyntaxKind.NamespaceImport) - return 2; - if (s1.importClause.name) - return 3; - return 4; - case ts.SyntaxKind.ImportEqualsDeclaration: - return 5; - case ts.SyntaxKind.VariableStatement: - return 6; - } +// 1. Side-effect imports +// 2. Type-only imports +// 3. Namespace imports +// 4. Default imports +// 5. Named imports +// 6. ImportEqualsDeclarations +// 7. Require variable statements +function getImportKindOrder(s1: ts.AnyImportOrRequireStatement) { + switch (s1.kind) { + case ts.SyntaxKind.ImportDeclaration: + if (!s1.importClause) + return 0; + if (s1.importClause.isTypeOnly) + return 1; + if (s1.importClause.namedBindings?.kind === ts.SyntaxKind.NamespaceImport) + return 2; + if (s1.importClause.name) + return 3; + return 4; + case ts.SyntaxKind.ImportEqualsDeclaration: + return 5; + case ts.SyntaxKind.VariableStatement: + return 6; } +} - function getNewImportSpecifiers(namedImports: ts.ImportDeclaration[]) { - return ts.flatMap(namedImports, namedImport => ts.map(tryGetNamedBindingElements(namedImport), importSpecifier => importSpecifier.name && importSpecifier.propertyName && importSpecifier.name.escapedText === importSpecifier.propertyName.escapedText - ? ts.factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name) - : importSpecifier)); - } +function getNewImportSpecifiers(namedImports: ts.ImportDeclaration[]) { + return ts.flatMap(namedImports, namedImport => ts.map(tryGetNamedBindingElements(namedImport), importSpecifier => importSpecifier.name && importSpecifier.propertyName && importSpecifier.name.escapedText === importSpecifier.propertyName.escapedText + ? ts.factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name) + : importSpecifier)); +} - function tryGetNamedBindingElements(namedImport: ts.ImportDeclaration) { - return namedImport.importClause?.namedBindings && ts.isNamedImports(namedImport.importClause.namedBindings) - ? namedImport.importClause.namedBindings.elements - : undefined; - } +function tryGetNamedBindingElements(namedImport: ts.ImportDeclaration) { + return namedImport.importClause?.namedBindings && ts.isNamedImports(namedImport.importClause.namedBindings) + ? namedImport.importClause.namedBindings.elements + : undefined; +} } diff --git a/src/services/outliningElementsCollector.ts b/src/services/outliningElementsCollector.ts index b0c468947fe20..b2051148e0906 100644 --- a/src/services/outliningElementsCollector.ts +++ b/src/services/outliningElementsCollector.ts @@ -1,350 +1,350 @@ /* @internal */ namespace ts.OutliningElementsCollector { - export function collectElements(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.OutliningSpan[] { - const res: ts.OutliningSpan[] = []; - addNodeOutliningSpans(sourceFile, cancellationToken, res); - addRegionOutliningSpans(sourceFile, res); - return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); - } +export function collectElements(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken): ts.OutliningSpan[] { + const res: ts.OutliningSpan[] = []; + addNodeOutliningSpans(sourceFile, cancellationToken, res); + addRegionOutliningSpans(sourceFile, res); + return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); +} - function addNodeOutliningSpans(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.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 && !ts.isAnyImportSyntax(statements[current])) { - visitNonImportNode(statements[current]); - current++; - } - if (current === n) - break; - const firstImport = current; - while (current < n && ts.isAnyImportSyntax(statements[current])) { - addOutliningForLeadingCommentsForNode(statements[current], sourceFile, cancellationToken, out); - current++; - } - const lastImport = current - 1; - if (lastImport !== firstImport) { - out.push(createOutliningSpanFromBounds(ts.findChildOfKind(statements[firstImport], ts.SyntaxKind.ImportKeyword, sourceFile)!.getStart(sourceFile), statements[lastImport].getEnd(), ts.OutliningSpanKind.Imports)); - } +function addNodeOutliningSpans(sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.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 && !ts.isAnyImportSyntax(statements[current])) { + visitNonImportNode(statements[current]); + current++; } + if (current === n) + break; + const firstImport = current; + while (current < n && ts.isAnyImportSyntax(statements[current])) { + addOutliningForLeadingCommentsForNode(statements[current], sourceFile, cancellationToken, out); + current++; + } + const lastImport = current - 1; + if (lastImport !== firstImport) { + out.push(createOutliningSpanFromBounds(ts.findChildOfKind(statements[firstImport], ts.SyntaxKind.ImportKeyword, sourceFile)!.getStart(sourceFile), statements[lastImport].getEnd(), ts.OutliningSpanKind.Imports)); + } + } - function visitNonImportNode(n: ts.Node) { - if (depthRemaining === 0) - return; - cancellationToken.throwIfCancellationRequested(); + function visitNonImportNode(n: ts.Node) { + if (depthRemaining === 0) + return; + cancellationToken.throwIfCancellationRequested(); - if (ts.isDeclaration(n) || ts.isVariableStatement(n) || ts.isReturnStatement(n) || ts.isCallOrNewExpression(n) || n.kind === ts.SyntaxKind.EndOfFileToken) { - addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); - } + if (ts.isDeclaration(n) || ts.isVariableStatement(n) || ts.isReturnStatement(n) || ts.isCallOrNewExpression(n) || n.kind === ts.SyntaxKind.EndOfFileToken) { + addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); + } - if (ts.isFunctionLike(n) && ts.isBinaryExpression(n.parent) && ts.isPropertyAccessExpression(n.parent.left)) { - addOutliningForLeadingCommentsForNode(n.parent.left, sourceFile, cancellationToken, out); - } + if (ts.isFunctionLike(n) && ts.isBinaryExpression(n.parent) && ts.isPropertyAccessExpression(n.parent.left)) { + addOutliningForLeadingCommentsForNode(n.parent.left, sourceFile, cancellationToken, out); + } - if (ts.isBlock(n) || ts.isModuleBlock(n)) { - addOutliningForLeadingCommentsForPos(n.statements.end, sourceFile, cancellationToken, out); - } + if (ts.isBlock(n) || ts.isModuleBlock(n)) { + addOutliningForLeadingCommentsForPos(n.statements.end, sourceFile, cancellationToken, out); + } - if (ts.isClassLike(n) || ts.isInterfaceDeclaration(n)) { - addOutliningForLeadingCommentsForPos(n.members.end, sourceFile, cancellationToken, out); - } + if (ts.isClassLike(n) || ts.isInterfaceDeclaration(n)) { + addOutliningForLeadingCommentsForPos(n.members.end, sourceFile, cancellationToken, out); + } - const span = getOutliningSpanForNode(n, sourceFile); - if (span) - out.push(span); + const span = getOutliningSpanForNode(n, sourceFile); + if (span) + out.push(span); + depthRemaining--; + if (ts.isCallExpression(n)) { + depthRemaining++; + visitNonImportNode(n.expression); depthRemaining--; - if (ts.isCallExpression(n)) { - depthRemaining++; - visitNonImportNode(n.expression); - depthRemaining--; - n.arguments.forEach(visitNonImportNode); - n.typeArguments?.forEach(visitNonImportNode); - } - else if (ts.isIfStatement(n) && n.elseStatement && ts.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 (ts.isIfStatement(n) && n.elseStatement && ts.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); } + depthRemaining++; } +} - function addRegionOutliningSpans(sourceFile: ts.SourceFile, out: ts.Push): void { - const regions: ts.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 || ts.isInComment(sourceFile, currentLineStart)) { - continue; - } +function addRegionOutliningSpans(sourceFile: ts.SourceFile, out: ts.Push): void { + const regions: ts.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 || ts.isInComment(sourceFile, currentLineStart)) { + continue; + } - if (!result[1]) { - const span = ts.createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd); - regions.push(createOutliningSpan(span, ts.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); - } + if (!result[1]) { + const span = ts.createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd); + regions.push(createOutliningSpan(span, ts.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); } } } +} - const regionDelimiterRegExp = /^#(end)?region(?:\s+(.*))?(?:\r)?$/; - function isRegionDelimiter(lineText: string) { - // We trim the leading whitespace and // without the regex since the - // multiple potential whitespace matches can make for some gnarly backtracking behavior - lineText = ts.trimStringStart(lineText); - if (!ts.startsWith(lineText, "\/\/")) { - return null; // eslint-disable-line no-null/no-null - } - lineText = ts.trimString(lineText.slice(2)); - return regionDelimiterRegExp.exec(lineText); +const regionDelimiterRegExp = /^#(end)?region(?:\s+(.*))?(?:\r)?$/; +function isRegionDelimiter(lineText: string) { + // We trim the leading whitespace and // without the regex since the + // multiple potential whitespace matches can make for some gnarly backtracking behavior + lineText = ts.trimStringStart(lineText); + if (!ts.startsWith(lineText, "\/\/")) { + return null; // eslint-disable-line no-null/no-null } + lineText = ts.trimString(lineText.slice(2)); + return regionDelimiterRegExp.exec(lineText); +} - function addOutliningForLeadingCommentsForPos(pos: number, sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.Push): void { - const comments = ts.getLeadingCommentRanges(sourceFile.text, pos); - 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 ts.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 ts.SyntaxKind.MultiLineCommentTrivia: +function addOutliningForLeadingCommentsForPos(pos: number, sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.Push): void { + const comments = ts.getLeadingCommentRanges(sourceFile.text, pos); + 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 ts.SyntaxKind.SingleLineCommentTrivia: + // never fold region delimiters into single-line comment regions + const commentText = sourceText.slice(pos, end); + if (isRegionDelimiter(commentText)) { combineAndAddMultipleSingleLineComments(); - out.push(createOutliningSpanFromBounds(pos, end, ts.OutliningSpanKind.Comment)); singleLineCommentCount = 0; break; - default: - ts.Debug.assertNever(kind); - } - } - combineAndAddMultipleSingleLineComments(); + } - function combineAndAddMultipleSingleLineComments(): void { - // Only outline spans of two or more consecutive single line comments - if (singleLineCommentCount > 1) { - out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, ts.OutliningSpanKind.Comment)); - } + // 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 ts.SyntaxKind.MultiLineCommentTrivia: + combineAndAddMultipleSingleLineComments(); + out.push(createOutliningSpanFromBounds(pos, end, ts.OutliningSpanKind.Comment)); + singleLineCommentCount = 0; + break; + default: + ts.Debug.assertNever(kind); } } + combineAndAddMultipleSingleLineComments(); - function addOutliningForLeadingCommentsForNode(n: ts.Node, sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.Push): void { - if (ts.isJsxText(n)) - return; - addOutliningForLeadingCommentsForPos(n.pos, sourceFile, cancellationToken, out); + function combineAndAddMultipleSingleLineComments(): void { + // Only outline spans of two or more consecutive single line comments + if (singleLineCommentCount > 1) { + out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, ts.OutliningSpanKind.Comment)); + } } +} - function createOutliningSpanFromBounds(pos: number, end: number, kind: ts.OutliningSpanKind): ts.OutliningSpan { - return createOutliningSpan(ts.createTextSpanFromBounds(pos, end), kind); - } +function addOutliningForLeadingCommentsForNode(n: ts.Node, sourceFile: ts.SourceFile, cancellationToken: ts.CancellationToken, out: ts.Push): void { + if (ts.isJsxText(n)) + return; + addOutliningForLeadingCommentsForPos(n.pos, sourceFile, cancellationToken, out); +} - function getOutliningSpanForNode(n: ts.Node, sourceFile: ts.SourceFile): ts.OutliningSpan | undefined { - switch (n.kind) { - case ts.SyntaxKind.Block: - if (ts.isFunctionLike(n.parent)) { - return functionSpan(n.parent, n as ts.Block, sourceFile); - } - // 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 ts.SyntaxKind.DoStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.IfStatement: - case ts.SyntaxKind.WhileStatement: - case ts.SyntaxKind.WithStatement: - case ts.SyntaxKind.CatchClause: - return spanForNode(n.parent); - case ts.SyntaxKind.TryStatement: - // Could be the try-block, or the finally-block. - const tryStatement = n.parent as ts.TryStatement; - if (tryStatement.tryBlock === n) { - return spanForNode(n.parent); - } - else if (tryStatement.finallyBlock === n) { - const node = ts.findChildOfKind(tryStatement, ts.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(ts.createTextSpanFromNode(n, sourceFile), ts.OutliningSpanKind.Code); - } - case ts.SyntaxKind.ModuleBlock: - return spanForNode(n.parent); - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.CaseBlock: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.ObjectBindingPattern: - return spanForNode(n); - case ts.SyntaxKind.TupleType: - return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !ts.isTupleTypeNode(n.parent), ts.SyntaxKind.OpenBracketToken); - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: - return spanForNodeArray((n as ts.CaseClause | ts.DefaultClause).statements); - case ts.SyntaxKind.ObjectLiteralExpression: - return spanForObjectOrArrayLiteral(n); - case ts.SyntaxKind.ArrayLiteralExpression: - return spanForObjectOrArrayLiteral(n, ts.SyntaxKind.OpenBracketToken); - case ts.SyntaxKind.JsxElement: - return spanForJSXElement(n as ts.JsxElement); - case ts.SyntaxKind.JsxFragment: - return spanForJSXFragment(n as ts.JsxFragment); - case ts.SyntaxKind.JsxSelfClosingElement: - case ts.SyntaxKind.JsxOpeningElement: - return spanForJSXAttributes((n as ts.JsxOpeningLikeElement).attributes); - case ts.SyntaxKind.TemplateExpression: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return spanForTemplateLiteral(n as ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral); - case ts.SyntaxKind.ArrayBindingPattern: - return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !ts.isBindingElement(n.parent), ts.SyntaxKind.OpenBracketToken); - case ts.SyntaxKind.ArrowFunction: - return spanForArrowFunction(n as ts.ArrowFunction); - case ts.SyntaxKind.CallExpression: - return spanForCallExpression(n as ts.CallExpression); - case ts.SyntaxKind.ParenthesizedExpression: - return spanForParenthesizedExpression(n as ts.ParenthesizedExpression); - } - function spanForCallExpression(node: ts.CallExpression): ts.OutliningSpan | undefined { - if (!node.arguments.length) { - return undefined; +function createOutliningSpanFromBounds(pos: number, end: number, kind: ts.OutliningSpanKind): ts.OutliningSpan { + return createOutliningSpan(ts.createTextSpanFromBounds(pos, end), kind); +} + +function getOutliningSpanForNode(n: ts.Node, sourceFile: ts.SourceFile): ts.OutliningSpan | undefined { + switch (n.kind) { + case ts.SyntaxKind.Block: + if (ts.isFunctionLike(n.parent)) { + return functionSpan(n.parent, n as ts.Block, sourceFile); } - const openToken = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); - const closeToken = ts.findChildOfKind(node, ts.SyntaxKind.CloseParenToken, sourceFile); - if (!openToken || !closeToken || ts.positionsAreOnSameLine(openToken.pos, closeToken.pos, sourceFile)) { - 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 ts.SyntaxKind.DoStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.IfStatement: + case ts.SyntaxKind.WhileStatement: + case ts.SyntaxKind.WithStatement: + case ts.SyntaxKind.CatchClause: + return spanForNode(n.parent); + case ts.SyntaxKind.TryStatement: + // Could be the try-block, or the finally-block. + const tryStatement = n.parent as ts.TryStatement; + if (tryStatement.tryBlock === n) { + return spanForNode(n.parent); + } + else if (tryStatement.finallyBlock === n) { + const node = ts.findChildOfKind(tryStatement, ts.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(ts.createTextSpanFromNode(n, sourceFile), ts.OutliningSpanKind.Code); } - - return spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ false, /*useFullStart*/ true); + case ts.SyntaxKind.ModuleBlock: + return spanForNode(n.parent); + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.CaseBlock: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.ObjectBindingPattern: + return spanForNode(n); + case ts.SyntaxKind.TupleType: + return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !ts.isTupleTypeNode(n.parent), ts.SyntaxKind.OpenBracketToken); + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.DefaultClause: + return spanForNodeArray((n as ts.CaseClause | ts.DefaultClause).statements); + case ts.SyntaxKind.ObjectLiteralExpression: + return spanForObjectOrArrayLiteral(n); + case ts.SyntaxKind.ArrayLiteralExpression: + return spanForObjectOrArrayLiteral(n, ts.SyntaxKind.OpenBracketToken); + case ts.SyntaxKind.JsxElement: + return spanForJSXElement(n as ts.JsxElement); + case ts.SyntaxKind.JsxFragment: + return spanForJSXFragment(n as ts.JsxFragment); + case ts.SyntaxKind.JsxSelfClosingElement: + case ts.SyntaxKind.JsxOpeningElement: + return spanForJSXAttributes((n as ts.JsxOpeningLikeElement).attributes); + case ts.SyntaxKind.TemplateExpression: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return spanForTemplateLiteral(n as ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral); + case ts.SyntaxKind.ArrayBindingPattern: + return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !ts.isBindingElement(n.parent), ts.SyntaxKind.OpenBracketToken); + case ts.SyntaxKind.ArrowFunction: + return spanForArrowFunction(n as ts.ArrowFunction); + case ts.SyntaxKind.CallExpression: + return spanForCallExpression(n as ts.CallExpression); + case ts.SyntaxKind.ParenthesizedExpression: + return spanForParenthesizedExpression(n as ts.ParenthesizedExpression); + } + function spanForCallExpression(node: ts.CallExpression): ts.OutliningSpan | undefined { + if (!node.arguments.length) { + return undefined; } - - function spanForArrowFunction(node: ts.ArrowFunction): ts.OutliningSpan | undefined { - if (ts.isBlock(node.body) || ts.isParenthesizedExpression(node.body) || ts.positionsAreOnSameLine(node.body.getFullStart(), node.body.getEnd(), sourceFile)) { - return undefined; - } - const textSpan = ts.createTextSpanFromBounds(node.body.getFullStart(), node.body.getEnd()); - return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(node)); + const openToken = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); + const closeToken = ts.findChildOfKind(node, ts.SyntaxKind.CloseParenToken, sourceFile); + if (!openToken || !closeToken || ts.positionsAreOnSameLine(openToken.pos, closeToken.pos, sourceFile)) { + return undefined; } - function spanForJSXElement(node: ts.JsxElement): ts.OutliningSpan | undefined { - const textSpan = ts.createTextSpanFromBounds(node.openingElement.getStart(sourceFile), node.closingElement.getEnd()); - const tagName = node.openingElement.tagName.getText(sourceFile); - const bannerText = "<" + tagName + ">..."; - return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); - } + return spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ false, /*useFullStart*/ true); + } - function spanForJSXFragment(node: ts.JsxFragment): ts.OutliningSpan | undefined { - const textSpan = ts.createTextSpanFromBounds(node.openingFragment.getStart(sourceFile), node.closingFragment.getEnd()); - const bannerText = "<>..."; - return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + function spanForArrowFunction(node: ts.ArrowFunction): ts.OutliningSpan | undefined { + if (ts.isBlock(node.body) || ts.isParenthesizedExpression(node.body) || ts.positionsAreOnSameLine(node.body.getFullStart(), node.body.getEnd(), sourceFile)) { + return undefined; } + const textSpan = ts.createTextSpanFromBounds(node.body.getFullStart(), node.body.getEnd()); + return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(node)); + } - function spanForJSXAttributes(node: ts.JsxAttributes): ts.OutliningSpan | undefined { - if (node.properties.length === 0) { - return undefined; - } - - return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), ts.OutliningSpanKind.Code); - } + function spanForJSXElement(node: ts.JsxElement): ts.OutliningSpan | undefined { + const textSpan = ts.createTextSpanFromBounds(node.openingElement.getStart(sourceFile), node.closingElement.getEnd()); + const tagName = node.openingElement.tagName.getText(sourceFile); + const bannerText = "<" + tagName + ">..."; + return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + } - function spanForTemplateLiteral(node: ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral) { - if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral && node.text.length === 0) { - return undefined; - } - return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), ts.OutliningSpanKind.Code); - } + function spanForJSXFragment(node: ts.JsxFragment): ts.OutliningSpan | undefined { + const textSpan = ts.createTextSpanFromBounds(node.openingFragment.getStart(sourceFile), node.closingFragment.getEnd()); + const bannerText = "<>..."; + return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + } - function spanForObjectOrArrayLiteral(node: ts.Node, open: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenBracketToken = ts.SyntaxKind.OpenBraceToken): ts.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*/ !ts.isArrayLiteralExpression(node.parent) && !ts.isCallExpression(node.parent), open); + function spanForJSXAttributes(node: ts.JsxAttributes): ts.OutliningSpan | undefined { + if (node.properties.length === 0) { + return undefined; } - function spanForNode(hintSpanNode: ts.Node, autoCollapse = false, useFullStart = true, open: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenBracketToken = ts.SyntaxKind.OpenBraceToken, close: ts.SyntaxKind = open === ts.SyntaxKind.OpenBraceToken ? ts.SyntaxKind.CloseBraceToken : ts.SyntaxKind.CloseBracketToken): ts.OutliningSpan | undefined { - const openToken = ts.findChildOfKind(n, open, sourceFile); - const closeToken = ts.findChildOfKind(n, close, sourceFile); - return openToken && closeToken && spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, autoCollapse, useFullStart); - } + return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), ts.OutliningSpanKind.Code); + } - function spanForNodeArray(nodeArray: ts.NodeArray): ts.OutliningSpan | undefined { - return nodeArray.length ? createOutliningSpan(ts.createTextSpanFromRange(nodeArray), ts.OutliningSpanKind.Code) : undefined; + function spanForTemplateLiteral(node: ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral) { + if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral && node.text.length === 0) { + return undefined; } + return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), ts.OutliningSpanKind.Code); + } - function spanForParenthesizedExpression(node: ts.ParenthesizedExpression): ts.OutliningSpan | undefined { - if (ts.positionsAreOnSameLine(node.getStart(), node.getEnd(), sourceFile)) - return undefined; - const textSpan = ts.createTextSpanFromBounds(node.getStart(), node.getEnd()); - return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(node)); - } + function spanForObjectOrArrayLiteral(node: ts.Node, open: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenBracketToken = ts.SyntaxKind.OpenBraceToken): ts.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*/ !ts.isArrayLiteralExpression(node.parent) && !ts.isCallExpression(node.parent), open); } - function functionSpan(node: ts.SignatureDeclaration, body: ts.Block, sourceFile: ts.SourceFile): ts.OutliningSpan | undefined { - const openToken = tryGetFunctionOpenToken(node, body, sourceFile); - const closeToken = ts.findChildOfKind(body, ts.SyntaxKind.CloseBraceToken, sourceFile); - return openToken && closeToken && spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ node.kind !== ts.SyntaxKind.ArrowFunction); + function spanForNode(hintSpanNode: ts.Node, autoCollapse = false, useFullStart = true, open: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenBracketToken = ts.SyntaxKind.OpenBraceToken, close: ts.SyntaxKind = open === ts.SyntaxKind.OpenBraceToken ? ts.SyntaxKind.CloseBraceToken : ts.SyntaxKind.CloseBracketToken): ts.OutliningSpan | undefined { + const openToken = ts.findChildOfKind(n, open, sourceFile); + const closeToken = ts.findChildOfKind(n, close, sourceFile); + return openToken && closeToken && spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, autoCollapse, useFullStart); } - function spanBetweenTokens(openToken: ts.Node, closeToken: ts.Node, hintSpanNode: ts.Node, sourceFile: ts.SourceFile, autoCollapse = false, useFullStart = true): ts.OutliningSpan { - const textSpan = ts.createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd()); - return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse); + function spanForNodeArray(nodeArray: ts.NodeArray): ts.OutliningSpan | undefined { + return nodeArray.length ? createOutliningSpan(ts.createTextSpanFromRange(nodeArray), ts.OutliningSpanKind.Code) : undefined; } - function createOutliningSpan(textSpan: ts.TextSpan, kind: ts.OutliningSpanKind, hintSpan: ts.TextSpan = textSpan, autoCollapse = false, bannerText = "..."): ts.OutliningSpan { - return { textSpan, kind, hintSpan, bannerText, autoCollapse }; + function spanForParenthesizedExpression(node: ts.ParenthesizedExpression): ts.OutliningSpan | undefined { + if (ts.positionsAreOnSameLine(node.getStart(), node.getEnd(), sourceFile)) + return undefined; + const textSpan = ts.createTextSpanFromBounds(node.getStart(), node.getEnd()); + return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(node)); } +} - function tryGetFunctionOpenToken(node: ts.SignatureDeclaration, body: ts.Block, sourceFile: ts.SourceFile): ts.Node | undefined { - if (ts.isNodeArrayMultiLine(node.parameters, sourceFile)) { - const openParenToken = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); - if (openParenToken) { - return openParenToken; - } +function functionSpan(node: ts.SignatureDeclaration, body: ts.Block, sourceFile: ts.SourceFile): ts.OutliningSpan | undefined { + const openToken = tryGetFunctionOpenToken(node, body, sourceFile); + const closeToken = ts.findChildOfKind(body, ts.SyntaxKind.CloseBraceToken, sourceFile); + return openToken && closeToken && spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ node.kind !== ts.SyntaxKind.ArrowFunction); +} + +function spanBetweenTokens(openToken: ts.Node, closeToken: ts.Node, hintSpanNode: ts.Node, sourceFile: ts.SourceFile, autoCollapse = false, useFullStart = true): ts.OutliningSpan { + const textSpan = ts.createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd()); + return createOutliningSpan(textSpan, ts.OutliningSpanKind.Code, ts.createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse); +} + +function createOutliningSpan(textSpan: ts.TextSpan, kind: ts.OutliningSpanKind, hintSpan: ts.TextSpan = textSpan, autoCollapse = false, bannerText = "..."): ts.OutliningSpan { + return { textSpan, kind, hintSpan, bannerText, autoCollapse }; +} + +function tryGetFunctionOpenToken(node: ts.SignatureDeclaration, body: ts.Block, sourceFile: ts.SourceFile): ts.Node | undefined { + if (ts.isNodeArrayMultiLine(node.parameters, sourceFile)) { + const openParenToken = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); + if (openParenToken) { + return openParenToken; } - return ts.findChildOfKind(body, ts.SyntaxKind.OpenBraceToken, sourceFile); } + return ts.findChildOfKind(body, ts.SyntaxKind.OpenBraceToken, sourceFile); +} } diff --git a/src/services/patternMatcher.ts b/src/services/patternMatcher.ts index 8b8e282cb2465..5b23df1cb9879 100644 --- a/src/services/patternMatcher.ts +++ b/src/services/patternMatcher.ts @@ -1,594 +1,594 @@ /* @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; - } +// Note(cyrusn): this enum is ordered from strongest match type to weakest match type. +export enum PatternMatchKind { + exact, + prefix, + substring, + camelCase +} - // 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; - } +// 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; +} - // 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[]; - } +// 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; +} - // 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: ts.TextSpan[]; - } +// 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[]; +} - function createPatternMatch(kind: PatternMatchKind, isCaseSensitive: boolean): PatternMatch { - return { - kind, - isCaseSensitive - }; - } +// 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: ts.TextSpan[]; +} - 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 = new ts.Map(); +function createPatternMatch(kind: PatternMatchKind, isCaseSensitive: boolean): PatternMatch { + return { + kind, + isCaseSensitive + }; +} - 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; +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 = new ts.Map(); + + 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, ts.last(dotSeparatedSegments), stringToWordSpans), + patternContainsDots: dotSeparatedSegments.length > 1 + }; +} - return { - getFullMatch: (containers, candidate) => getFullMatch(containers, candidate, dotSeparatedSegments, stringToWordSpans), - getMatchForLastSegmentOfPattern: candidate => matchSegment(candidate, ts.last(dotSeparatedSegments), stringToWordSpans), - patternContainsDots: dotSeparatedSegments.length > 1 - }; +function getFullMatch(candidateContainers: readonly string[], candidate: string, dotSeparatedSegments: readonly Segment[], stringToWordSpans: ts.ESMap): 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, ts.last(dotSeparatedSegments), stringToWordSpans); + if (!candidateMatch) { + return undefined; } - function getFullMatch(candidateContainers: readonly string[], candidate: string, dotSeparatedSegments: readonly Segment[], stringToWordSpans: ts.ESMap): 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, ts.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; - } + // -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; + 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: ts.ESMap): ts.TextSpan[] { - let spans = stringToWordSpans.get(word); - if (!spans) { - stringToWordSpans.set(word, spans = breakIntoWordSpans(word)); - } - return spans; +function getWordSpans(word: string, stringToWordSpans: ts.ESMap): ts.TextSpan[] { + let spans = stringToWordSpans.get(word); + if (!spans) { + stringToWordSpans.set(word, spans = breakIntoWordSpans(word)); } + return spans; +} - function matchTextChunk(candidate: string, chunk: TextChunk, stringToWordSpans: ts.ESMap): 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:*/ ts.startsWith(candidate, chunk.text)); - } +function matchTextChunk(candidate: string, chunk: TextChunk, stringToWordSpans: ts.ESMap): 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:*/ ts.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)); - } - } - // 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); + 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)); } } - 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); - } - } + // 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); } } - - function matchSegment(candidate: string, segment: Segment, stringToWordSpans: ts.ESMap): 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 !== ts.CharacterCodes.space && ch !== ts.CharacterCodes.asterisk)) { - const match = matchTextChunk(candidate, segment.totalTextChunk, stringToWordSpans); - if (match) - return match; + 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); } - - // 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)); + // 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); + } } - return bestMatch; } +} - function betterMatch(a: PatternMatch | undefined, b: PatternMatch | undefined): PatternMatch | undefined { - return ts.min(a, b, compareMatches); - } - function compareMatches(a: PatternMatch | undefined, b: PatternMatch | undefined): ts.Comparison { - return a === undefined ? ts.Comparison.GreaterThan : b === undefined ? ts.Comparison.LessThan - : ts.compareValues(a.kind, b.kind) || ts.compareBooleans(!a.isCaseSensitive, !b.isCaseSensitive); - } +function matchSegment(candidate: string, segment: Segment, stringToWordSpans: ts.ESMap): 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 !== ts.CharacterCodes.space && ch !== ts.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; +} - function partStartsWith(candidate: string, candidateSpan: ts.TextSpan, pattern: string, ignoreCase: boolean, patternSpan: ts.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 betterMatch(a: PatternMatch | undefined, b: PatternMatch | undefined): PatternMatch | undefined { + return ts.min(a, b, compareMatches); +} +function compareMatches(a: PatternMatch | undefined, b: PatternMatch | undefined): ts.Comparison { + return a === undefined ? ts.Comparison.GreaterThan : b === undefined ? ts.Comparison.LessThan + : ts.compareValues(a.kind, b.kind) || ts.compareBooleans(!a.isCaseSensitive, !b.isCaseSensitive); +} - function equalChars(ch1: number, ch2: number, ignoreCase: boolean): boolean { - return ignoreCase ? toLowerCase(ch1) === toLowerCase(ch2) : ch1 === ch2; - } +function partStartsWith(candidate: string, candidateSpan: ts.TextSpan, pattern: string, ignoreCase: boolean, patternSpan: ts.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 tryCamelCaseMatch(candidate: string, candidateParts: ts.TextSpan[], chunk: TextChunk, ignoreCase: boolean): boolean { - const chunkCharacterSpans = chunk.characterSpans; +function equalChars(ch1: number, ch2: number, ignoreCase: boolean): boolean { + return ignoreCase ? toLowerCase(ch1) === toLowerCase(ch2) : ch1 === ch2; +} - // 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. +function tryCamelCaseMatch(candidate: string, candidateParts: ts.TextSpan[], chunk: TextChunk, ignoreCase: boolean): boolean { + const chunkCharacterSpans = chunk.characterSpans; - let currentCandidate = 0; - let currentChunkSpan = 0; - let firstMatch: number | undefined; - let contiguous: boolean | undefined; + // 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. - 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 currentCandidate = 0; + let currentChunkSpan = 0; + let firstMatch: number | undefined; + let contiguous: boolean | undefined; - 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; - } - } + 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; + } - 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; + if (!partStartsWith(candidate, candidatePart, chunk.text, ignoreCase, chunkCharacterSpan)) { + break; + } - firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; + gotOneMatchThisCandidate = true; - // 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; + firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; - candidatePart = ts.createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); - } + // 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; - // 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; - } + candidatePart = ts.createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); + } - // Move onto the next candidate. - currentCandidate++; + // 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; } - } - function createSegment(text: string): Segment { - return { - totalTextChunk: createTextChunk(text), - subWordTextChunks: breakPatternIntoTextChunks(text) - }; + // Move onto the next candidate. + currentCandidate++; } +} - function isUpperCaseLetter(ch: number) { - // Fast check for the ascii range. - if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z) { - return true; - } - - if (ch < ts.CharacterCodes.maxAsciiCharacter || !ts.isUnicodeIdentifierStart(ch, ts.ScriptTarget.Latest)) { - return false; - } +function createSegment(text: string): Segment { + return { + totalTextChunk: createTextChunk(text), + subWordTextChunks: breakPatternIntoTextChunks(text) + }; +} - // 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 isUpperCaseLetter(ch: number) { + // Fast check for the ascii range. + if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z) { + return true; } - function isLowerCaseLetter(ch: number) { - // Fast check for the ascii range. - if (ch >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z) { - return true; - } + if (ch < ts.CharacterCodes.maxAsciiCharacter || !ts.isUnicodeIdentifierStart(ch, ts.ScriptTarget.Latest)) { + return false; + } - if (ch < ts.CharacterCodes.maxAsciiCharacter || !ts.isUnicodeIdentifierStart(ch, ts.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 >= ts.CharacterCodes.a && ch <= ts.CharacterCodes.z) { + return true; + } - // 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(); + if (ch < ts.CharacterCodes.maxAsciiCharacter || !ts.isUnicodeIdentifierStart(ch, ts.ScriptTarget.Latest)) { + return false; } - // 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; - } + // 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(); +} - function toLowerCase(ch: number): number { - // Fast convert for the ascii range. - if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z) { - return ts.CharacterCodes.a + (ch - ts.CharacterCodes.A); +// 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; } + } - if (ch < ts.CharacterCodes.maxAsciiCharacter) { - return ch; - } + return -1; +} - // TODO: find a way to compute this for any unicode characters in a - // non-allocating manner. - return String.fromCharCode(ch).toLowerCase().charCodeAt(0); +function toLowerCase(ch: number): number { + // Fast convert for the ascii range. + if (ch >= ts.CharacterCodes.A && ch <= ts.CharacterCodes.Z) { + return ts.CharacterCodes.a + (ch - ts.CharacterCodes.A); } - function isDigit(ch: number) { - // TODO(cyrusn): Find a way to support this for unicode digits. - return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9; + if (ch < ts.CharacterCodes.maxAsciiCharacter) { + return ch; } - function isWordChar(ch: number) { - return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === ts.CharacterCodes._ || ch === ts.CharacterCodes.$; - } + // TODO: find a way to compute this for any unicode characters in a + // non-allocating manner. + return String.fromCharCode(ch).toLowerCase().charCodeAt(0); +} + +function isDigit(ch: number) { + // TODO(cyrusn): Find a way to support this for unicode digits. + return ch >= ts.CharacterCodes._0 && ch <= ts.CharacterCodes._9; +} - function breakPatternIntoTextChunks(pattern: string): TextChunk[] { - const result: TextChunk[] = []; - let wordStart = 0; - let wordLength = 0; +function isWordChar(ch: number) { + return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === ts.CharacterCodes._ || ch === ts.CharacterCodes.$; +} - 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; - } +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 (wordLength > 0) { - result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + else { + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + wordLength = 0; + } } - - return result; } - function createTextChunk(text: string): TextChunk { - const textLowerCase = text.toLowerCase(); - return { - text, - textLowerCase, - isLowerCase: text === textLowerCase, - characterSpans: breakIntoCharacterSpans(text) - }; + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); } - export function breakIntoCharacterSpans(identifier: string): ts.TextSpan[] { - return breakIntoSpans(identifier, /*word:*/ false); - } + return result; +} - export function breakIntoWordSpans(identifier: string): ts.TextSpan[] { - return breakIntoSpans(identifier, /*word:*/ true); - } +function createTextChunk(text: string): TextChunk { + const textLowerCase = text.toLowerCase(); + return { + text, + textLowerCase, + isLowerCase: text === textLowerCase, + characterSpans: breakIntoCharacterSpans(text) + }; +} - function breakIntoSpans(identifier: string, word: boolean): ts.TextSpan[] { - const result: ts.TextSpan[] = []; +export function breakIntoCharacterSpans(identifier: string): ts.TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ false); +} - let wordStart = 0; - for (let i = 1; i < identifier.length; i++) { - const lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); - const currentIsDigit = isDigit(identifier.charCodeAt(i)); +export function breakIntoWordSpans(identifier: string): ts.TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ true); +} - const hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); - const hasTransitionFromUpperToLower = word && transitionFromUpperToLower(identifier, i, wordStart); +function breakIntoSpans(identifier: string, word: boolean): ts.TextSpan[] { + const result: ts.TextSpan[] = []; - if (charIsPunctuation(identifier.charCodeAt(i - 1)) || - charIsPunctuation(identifier.charCodeAt(i)) || - lastIsDigit !== currentIsDigit || - hasTransitionFromLowerToUpper || - hasTransitionFromUpperToLower) { + let wordStart = 0; + for (let i = 1; i < identifier.length; i++) { + const lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); + const currentIsDigit = isDigit(identifier.charCodeAt(i)); - if (!isAllPunctuation(identifier, wordStart, i)) { - result.push(ts.createTextSpan(wordStart, i - wordStart)); - } + const hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); + const hasTransitionFromUpperToLower = word && transitionFromUpperToLower(identifier, i, wordStart); - wordStart = i; + if (charIsPunctuation(identifier.charCodeAt(i - 1)) || + charIsPunctuation(identifier.charCodeAt(i)) || + lastIsDigit !== currentIsDigit || + hasTransitionFromLowerToUpper || + hasTransitionFromUpperToLower) { + + if (!isAllPunctuation(identifier, wordStart, i)) { + result.push(ts.createTextSpan(wordStart, i - wordStart)); } - } - if (!isAllPunctuation(identifier, wordStart, identifier.length)) { - result.push(ts.createTextSpan(wordStart, identifier.length - wordStart)); + wordStart = i; } + } - return result; + if (!isAllPunctuation(identifier, wordStart, identifier.length)) { + result.push(ts.createTextSpan(wordStart, identifier.length - wordStart)); } - function charIsPunctuation(ch: number) { - switch (ch) { - case ts.CharacterCodes.exclamation: - case ts.CharacterCodes.doubleQuote: - case ts.CharacterCodes.hash: - case ts.CharacterCodes.percent: - case ts.CharacterCodes.ampersand: - case ts.CharacterCodes.singleQuote: - case ts.CharacterCodes.openParen: - case ts.CharacterCodes.closeParen: - case ts.CharacterCodes.asterisk: - case ts.CharacterCodes.comma: - case ts.CharacterCodes.minus: - case ts.CharacterCodes.dot: - case ts.CharacterCodes.slash: - case ts.CharacterCodes.colon: - case ts.CharacterCodes.semicolon: - case ts.CharacterCodes.question: - case ts.CharacterCodes.at: - case ts.CharacterCodes.openBracket: - case ts.CharacterCodes.backslash: - case ts.CharacterCodes.closeBracket: - case ts.CharacterCodes._: - case ts.CharacterCodes.openBrace: - case ts.CharacterCodes.closeBrace: - return true; - } + return result; +} - return false; +function charIsPunctuation(ch: number) { + switch (ch) { + case ts.CharacterCodes.exclamation: + case ts.CharacterCodes.doubleQuote: + case ts.CharacterCodes.hash: + case ts.CharacterCodes.percent: + case ts.CharacterCodes.ampersand: + case ts.CharacterCodes.singleQuote: + case ts.CharacterCodes.openParen: + case ts.CharacterCodes.closeParen: + case ts.CharacterCodes.asterisk: + case ts.CharacterCodes.comma: + case ts.CharacterCodes.minus: + case ts.CharacterCodes.dot: + case ts.CharacterCodes.slash: + case ts.CharacterCodes.colon: + case ts.CharacterCodes.semicolon: + case ts.CharacterCodes.question: + case ts.CharacterCodes.at: + case ts.CharacterCodes.openBracket: + case ts.CharacterCodes.backslash: + case ts.CharacterCodes.closeBracket: + case ts.CharacterCodes._: + case ts.CharacterCodes.openBrace: + case ts.CharacterCodes.closeBrace: + return true; } - function isAllPunctuation(identifier: string, start: number, end: number): boolean { - return every(identifier, ch => charIsPunctuation(ch) && ch !== ts.CharacterCodes._, start, end); - } + return false; +} - 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); - } +function isAllPunctuation(identifier: string, start: number, end: number): boolean { + return every(identifier, ch => charIsPunctuation(ch) && ch !== ts.CharacterCodes._, start, end); +} - function transitionFromLowerToUpper(identifier: string, word: boolean, index: number): boolean { - const lastIsUpper = isUpperCaseLetter(identifier.charCodeAt(index - 1)); - const currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); +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); +} - // 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); - } +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); +} - function everyInRange(start: number, end: number, pred: (n: number) => boolean): boolean { - for (let i = start; i < end; i++) { - if (!pred(i)) { - return false; - } +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; } + 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)); - } +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 b7b87c43fa423..86d7284cd2af6 100644 --- a/src/services/preProcess.ts +++ b/src/services/preProcess.ts @@ -1,211 +1,151 @@ namespace ts { - export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): ts.PreProcessedFileInfo { - const pragmaContext: ts.PragmaContext = { - languageVersion: ts.ScriptTarget.ES5, - pragmas: undefined, - checkJsDirective: undefined, - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - amdDependencies: [], - hasNoDefaultLib: undefined, - moduleName: undefined - }; - const importedFiles: ts.FileReference[] = []; - let ambientExternalModules: { - ref: ts.FileReference; - depth: number; - }[] | undefined; - let lastToken: ts.SyntaxKind; - let currentToken: ts.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 = ts.scanner.scan(); - if (currentToken === ts.SyntaxKind.OpenBraceToken) { - braceNesting++; - } - else if (currentToken === ts.SyntaxKind.CloseBraceToken) { - braceNesting--; - } - return currentToken; +export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): ts.PreProcessedFileInfo { + const pragmaContext: ts.PragmaContext = { + languageVersion: ts.ScriptTarget.ES5, + pragmas: undefined, + checkJsDirective: undefined, + referencedFiles: [], + typeReferenceDirectives: [], + libReferenceDirectives: [], + amdDependencies: [], + hasNoDefaultLib: undefined, + moduleName: undefined + }; + const importedFiles: ts.FileReference[] = []; + let ambientExternalModules: { + ref: ts.FileReference; + depth: number; + }[] | undefined; + let lastToken: ts.SyntaxKind; + let currentToken: ts.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 = ts.scanner.scan(); + if (currentToken === ts.SyntaxKind.OpenBraceToken) { + braceNesting++; } - - function getFileReference() { - const fileName = ts.scanner.getTokenValue(); - const pos = ts.scanner.getTokenPos(); - return { fileName, pos, end: pos + fileName.length }; + else if (currentToken === ts.SyntaxKind.CloseBraceToken) { + braceNesting--; } + return currentToken; + } - function recordAmbientExternalModule(): void { - if (!ambientExternalModules) { - ambientExternalModules = []; - } - ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); + function getFileReference() { + const fileName = ts.scanner.getTokenValue(); + const pos = ts.scanner.getTokenPos(); + return { fileName, pos, end: pos + fileName.length }; + } + + function recordAmbientExternalModule(): void { + if (!ambientExternalModules) { + ambientExternalModules = []; } + ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); + } - function recordModuleName() { - importedFiles.push(getFileReference()); + function recordModuleName() { + importedFiles.push(getFileReference()); - markAsExternalModuleIfTopLevel(); - } + markAsExternalModuleIfTopLevel(); + } - function markAsExternalModuleIfTopLevel() { - if (braceNesting === 0) { - externalModule = true; - } + function markAsExternalModuleIfTopLevel() { + if (braceNesting === 0) { + externalModule = true; } + } - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeDeclare(): boolean { - let token = ts.scanner.getToken(); - if (token === ts.SyntaxKind.DeclareKeyword) { - // declare module "mod" + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeDeclare(): boolean { + let token = ts.scanner.getToken(); + if (token === ts.SyntaxKind.DeclareKeyword) { + // declare module "mod" + token = nextToken(); + if (token === ts.SyntaxKind.ModuleKeyword) { token = nextToken(); - if (token === ts.SyntaxKind.ModuleKeyword) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral) { - recordAmbientExternalModule(); - } + if (token === ts.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 === ts.SyntaxKind.DotToken) { return false; } - - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeImport(): boolean { - if (lastToken === ts.SyntaxKind.DotToken) { - return false; - } - let token = ts.scanner.getToken(); - if (token === ts.SyntaxKind.ImportKeyword) { + let token = ts.scanner.getToken(); + if (token === ts.SyntaxKind.ImportKeyword) { + token = nextToken(); + if (token === ts.SyntaxKind.OpenParenToken) { token = nextToken(); - if (token === ts.SyntaxKind.OpenParenToken) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - // import("mod"); - recordModuleName(); - return true; - } - } - else if (token === ts.SyntaxKind.StringLiteral) { - // import "mod"; + if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + // import("mod"); recordModuleName(); return true; } - else { - if (token === ts.SyntaxKind.TypeKeyword) { - const skipTypeKeyword = ts.scanner.lookAhead(() => { - const token = ts.scanner.scan(); - return token !== ts.SyntaxKind.FromKeyword && (token === ts.SyntaxKind.AsteriskToken || - token === ts.SyntaxKind.OpenBraceToken || - token === ts.SyntaxKind.Identifier || - ts.isKeyword(token)); - }); - if (skipTypeKeyword) { - token = nextToken(); - } + } + else if (token === ts.SyntaxKind.StringLiteral) { + // import "mod"; + recordModuleName(); + return true; + } + else { + if (token === ts.SyntaxKind.TypeKeyword) { + const skipTypeKeyword = ts.scanner.lookAhead(() => { + const token = ts.scanner.scan(); + return token !== ts.SyntaxKind.FromKeyword && (token === ts.SyntaxKind.AsteriskToken || + token === ts.SyntaxKind.OpenBraceToken || + token === ts.SyntaxKind.Identifier || + ts.isKeyword(token)); + }); + if (skipTypeKeyword) { + token = nextToken(); } + } - if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { + if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { + token = nextToken(); + if (token === ts.SyntaxKind.FromKeyword) { token = nextToken(); - if (token === ts.SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral) { - // import d from "mod"; - recordModuleName(); - return true; - } - } - else if (token === ts.SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } - } - else if (token === ts.SyntaxKind.CommaToken) { - // consume comma and keep going - token = nextToken(); - } - else { - // unknown syntax + if (token === ts.SyntaxKind.StringLiteral) { + // import d from "mod"; + recordModuleName(); return true; } } - - if (token === ts.SyntaxKind.OpenBraceToken) { - token = nextToken(); - // consume "{ a as B, c, d as D}" clauses - // make sure that it stops on EOF - while (token !== ts.SyntaxKind.CloseBraceToken && token !== ts.SyntaxKind.EndOfFileToken) { - token = nextToken(); - } - - if (token === ts.SyntaxKind.CloseBraceToken) { - token = nextToken(); - if (token === ts.SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral) { - // import {a as A} from "mod"; - // import d, {a, b as B} from "mod" - recordModuleName(); - } - } + else if (token === ts.SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; } } - else if (token === ts.SyntaxKind.AsteriskToken) { + else if (token === ts.SyntaxKind.CommaToken) { + // consume comma and keep going token = nextToken(); - if (token === ts.SyntaxKind.AsKeyword) { - token = nextToken(); - if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { - token = nextToken(); - if (token === ts.SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral) { - // import * as NS from "mod" - // import d, * as NS from "mod" - recordModuleName(); - } - } - } - } } - } - - return true; - } - - return false; - } - - function tryConsumeExport(): boolean { - let token = ts.scanner.getToken(); - if (token === ts.SyntaxKind.ExportKeyword) { - markAsExternalModuleIfTopLevel(); - token = nextToken(); - if (token === ts.SyntaxKind.TypeKeyword) { - const skipTypeKeyword = ts.scanner.lookAhead(() => { - const token = ts.scanner.scan(); - return token === ts.SyntaxKind.AsteriskToken || - token === ts.SyntaxKind.OpenBraceToken; - }); - if (skipTypeKeyword) { - token = nextToken(); + else { + // unknown syntax + return true; } } + if (token === ts.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 !== ts.SyntaxKind.CloseBraceToken && token !== ts.SyntaxKind.EndOfFileToken) { token = nextToken(); } @@ -215,221 +155,281 @@ namespace ts { if (token === ts.SyntaxKind.FromKeyword) { token = nextToken(); if (token === ts.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(); } } } } else if (token === ts.SyntaxKind.AsteriskToken) { + token = nextToken(); + if (token === ts.SyntaxKind.AsKeyword) { + token = nextToken(); + if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { + token = nextToken(); + if (token === ts.SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === ts.SyntaxKind.StringLiteral) { + // import * as NS from "mod" + // import d, * as NS from "mod" + recordModuleName(); + } + } + } + } + } + } + + return true; + } + + return false; + } + + function tryConsumeExport(): boolean { + let token = ts.scanner.getToken(); + if (token === ts.SyntaxKind.ExportKeyword) { + markAsExternalModuleIfTopLevel(); + token = nextToken(); + if (token === ts.SyntaxKind.TypeKeyword) { + const skipTypeKeyword = ts.scanner.lookAhead(() => { + const token = ts.scanner.scan(); + return token === ts.SyntaxKind.AsteriskToken || + token === ts.SyntaxKind.OpenBraceToken; + }); + if (skipTypeKeyword) { + token = nextToken(); + } + } + if (token === ts.SyntaxKind.OpenBraceToken) { + token = nextToken(); + // consume "{ a as B, c, d as D}" clauses + // make sure it stops on EOF + while (token !== ts.SyntaxKind.CloseBraceToken && token !== ts.SyntaxKind.EndOfFileToken) { + token = nextToken(); + } + + if (token === ts.SyntaxKind.CloseBraceToken) { token = nextToken(); if (token === ts.SyntaxKind.FromKeyword) { token = nextToken(); if (token === ts.SyntaxKind.StringLiteral) { - // export * from "mod" + // export {a as A} from "mod"; + // export {a, b as B} from "mod" recordModuleName(); } } } - else if (token === ts.SyntaxKind.ImportKeyword) { + } + else if (token === ts.SyntaxKind.AsteriskToken) { + token = nextToken(); + if (token === ts.SyntaxKind.FromKeyword) { token = nextToken(); - if (token === ts.SyntaxKind.TypeKeyword) { - const skipTypeKeyword = ts.scanner.lookAhead(() => { - const token = ts.scanner.scan(); - return token === ts.SyntaxKind.Identifier || - ts.isKeyword(token); - }); - if (skipTypeKeyword) { - token = nextToken(); - } + if (token === ts.SyntaxKind.StringLiteral) { + // export * from "mod" + recordModuleName(); } - if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { + } + } + else if (token === ts.SyntaxKind.ImportKeyword) { + token = nextToken(); + if (token === ts.SyntaxKind.TypeKeyword) { + const skipTypeKeyword = ts.scanner.lookAhead(() => { + const token = ts.scanner.scan(); + return token === ts.SyntaxKind.Identifier || + ts.isKeyword(token); + }); + if (skipTypeKeyword) { token = nextToken(); - if (token === ts.SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } + } + } + if (token === ts.SyntaxKind.Identifier || ts.isKeyword(token)) { + token = nextToken(); + if (token === ts.SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; } } } - - return true; } - return false; + return true; } - function tryConsumeRequireCall(skipCurrentToken: boolean, allowTemplateLiterals = false): boolean { - let token = skipCurrentToken ? nextToken() : ts.scanner.getToken(); - if (token === ts.SyntaxKind.RequireKeyword) { + return false; + } + + function tryConsumeRequireCall(skipCurrentToken: boolean, allowTemplateLiterals = false): boolean { + let token = skipCurrentToken ? nextToken() : ts.scanner.getToken(); + if (token === ts.SyntaxKind.RequireKeyword) { + token = nextToken(); + if (token === ts.SyntaxKind.OpenParenToken) { token = nextToken(); - if (token === ts.SyntaxKind.OpenParenToken) { - token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral || - allowTemplateLiterals && token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - // require("mod"); - recordModuleName(); - } + if (token === ts.SyntaxKind.StringLiteral || + allowTemplateLiterals && token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + // require("mod"); + recordModuleName(); } - return true; } - return false; + return true; } + return false; + } - function tryConsumeDefine(): boolean { - let token = ts.scanner.getToken(); - if (token === ts.SyntaxKind.Identifier && ts.scanner.getTokenValue() === "define") { - token = nextToken(); - if (token !== ts.SyntaxKind.OpenParenToken) { - return true; - } + function tryConsumeDefine(): boolean { + let token = ts.scanner.getToken(); + if (token === ts.SyntaxKind.Identifier && ts.scanner.getTokenValue() === "define") { + token = nextToken(); + if (token !== ts.SyntaxKind.OpenParenToken) { + return true; + } + token = nextToken(); + if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + // looks like define ("modname", ... - skip string literal and comma token = nextToken(); - if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - // looks like define ("modname", ... - skip string literal and comma + if (token === ts.SyntaxKind.CommaToken) { token = nextToken(); - if (token === ts.SyntaxKind.CommaToken) { - token = nextToken(); - } - else { - // unexpected token - return true; - } } - - // should be start of dependency list - if (token !== ts.SyntaxKind.OpenBracketToken) { + else { + // unexpected token return true; } + } - // skip open bracket - token = nextToken(); - // scan until ']' or EOF - while (token !== ts.SyntaxKind.CloseBracketToken && token !== ts.SyntaxKind.EndOfFileToken) { - // record string literals as module names - if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - recordModuleName(); - } + // should be start of dependency list + if (token !== ts.SyntaxKind.OpenBracketToken) { + return true; + } - token = nextToken(); + // skip open bracket + token = nextToken(); + // scan until ']' or EOF + while (token !== ts.SyntaxKind.CloseBracketToken && token !== ts.SyntaxKind.EndOfFileToken) { + // record string literals as module names + if (token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + recordModuleName(); } - return true; + token = nextToken(); } - return false; + return true; + } + return false; + } - function processImports(): void { - ts.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 (ts.scanner.getToken() === ts.SyntaxKind.EndOfFileToken) { - break; - } + function processImports(): void { + ts.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 (ts.scanner.getToken() === ts.SyntaxKind.EndOfFileToken) { + break; + } - if (ts.scanner.getToken() === ts.SyntaxKind.TemplateHead) { - const stack = [ts.scanner.getToken()]; - let token = ts.scanner.scan(); - loop: while (ts.length(stack)) { - switch (token) { - case ts.SyntaxKind.EndOfFileToken: - break loop; - case ts.SyntaxKind.ImportKeyword: - tryConsumeImport(); - break; - case ts.SyntaxKind.TemplateHead: + if (ts.scanner.getToken() === ts.SyntaxKind.TemplateHead) { + const stack = [ts.scanner.getToken()]; + let token = ts.scanner.scan(); + loop: while (ts.length(stack)) { + switch (token) { + case ts.SyntaxKind.EndOfFileToken: + break loop; + case ts.SyntaxKind.ImportKeyword: + tryConsumeImport(); + break; + case ts.SyntaxKind.TemplateHead: + stack.push(token); + break; + case ts.SyntaxKind.OpenBraceToken: + if (ts.length(stack)) { stack.push(token); - break; - case ts.SyntaxKind.OpenBraceToken: - if (ts.length(stack)) { - stack.push(token); - } - break; - case ts.SyntaxKind.CloseBraceToken: - if (ts.length(stack)) { - if (ts.lastOrUndefined(stack) === ts.SyntaxKind.TemplateHead) { - if (ts.scanner.reScanTemplateToken(/* isTaggedTemplate */ false) === ts.SyntaxKind.TemplateTail) { - stack.pop(); - } - } - else { + } + break; + case ts.SyntaxKind.CloseBraceToken: + if (ts.length(stack)) { + if (ts.lastOrUndefined(stack) === ts.SyntaxKind.TemplateHead) { + if (ts.scanner.reScanTemplateToken(/* isTaggedTemplate */ false) === ts.SyntaxKind.TemplateTail) { stack.pop(); } } - break; - } - token = ts.scanner.scan(); + else { + stack.pop(); + } + } + break; } - nextToken(); - } - - // check if at least one of alternative have moved scanner forward - if (tryConsumeDeclare() || - tryConsumeImport() || - tryConsumeExport() || - (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false, /*allowTemplateLiterals*/ true) || - tryConsumeDefine()))) { - continue; - } - else { - nextToken(); + token = ts.scanner.scan(); } + nextToken(); } - ts.scanner.setText(undefined); + // check if at least one of alternative have moved scanner forward + if (tryConsumeDeclare() || + tryConsumeImport() || + tryConsumeExport() || + (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false, /*allowTemplateLiterals*/ true) || + tryConsumeDefine()))) { + continue; + } + else { + nextToken(); + } } - if (readImportFiles) { - processImports(); - } - ts.processCommentPragmas(pragmaContext, sourceText); - ts.processPragmasIntoFields(pragmaContext, ts.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); - } + ts.scanner.setText(undefined); + } + + if (readImportFiles) { + processImports(); + } + ts.processCommentPragmas(pragmaContext, sourceText); + ts.processPragmasIntoFields(pragmaContext, ts.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 fcd091ca4d62a..9fbe433cee2f6 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -1,22 +1,22 @@ /* @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 = new ts.Map(); +// A map with the refactor code as key, the refactor itself as value +// e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want +const refactors = new ts.Map(); - /** @param name An unique code associated with each refactor. Does not have to be human-readable. */ - export function registerRefactor(name: string, refactor: ts.Refactor) { - refactors.set(name, refactor); - } +/** @param name An unique code associated with each refactor. Does not have to be human-readable. */ +export function registerRefactor(name: string, refactor: ts.Refactor) { + refactors.set(name, refactor); +} - export function getApplicableRefactors(context: ts.RefactorContext): ts.ApplicableRefactorInfo[] { - return ts.arrayFrom(ts.flatMapIterator(refactors.values(), refactor => context.cancellationToken && context.cancellationToken.isCancellationRequested() || - !refactor.kinds?.some(kind => ts.refactor.refactorKindBeginsWith(kind, context.kind)) ? undefined : - refactor.getAvailableActions(context))); - } +export function getApplicableRefactors(context: ts.RefactorContext): ts.ApplicableRefactorInfo[] { + return ts.arrayFrom(ts.flatMapIterator(refactors.values(), refactor => context.cancellationToken && context.cancellationToken.isCancellationRequested() || + !refactor.kinds?.some(kind => ts.refactor.refactorKindBeginsWith(kind, context.kind)) ? undefined : + refactor.getAvailableActions(context))); +} - export function getEditsForRefactor(context: ts.RefactorContext, refactorName: string, actionName: string): ts.RefactorEditInfo | undefined { - const refactor = refactors.get(refactorName); - return refactor && refactor.getEditsForAction(context, actionName); - } +export function getEditsForRefactor(context: ts.RefactorContext, refactorName: string, actionName: string): ts.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 e21f3676f80bc..b789af5fc6423 100644 --- a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts +++ b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts @@ -1,121 +1,121 @@ /* @internal */ namespace ts.refactor.addOrRemoveBracesToArrowFunction { - const refactorName = "Add or remove braces in an arrow function"; - const refactorDescription = ts.Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; - - const addBracesAction = { - name: "Add braces to arrow function", - description: ts.Diagnostics.Add_braces_to_arrow_function.message, - kind: "refactor.rewrite.arrow.braces.add", - }; - const removeBracesAction = { - name: "Remove braces from arrow function", - description: ts.Diagnostics.Remove_braces_from_arrow_function.message, - kind: "refactor.rewrite.arrow.braces.remove" - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [removeBracesAction.kind], - getEditsForAction: getRefactorEditsToRemoveFunctionBraces, - getAvailableActions: getRefactorActionsToRemoveFunctionBraces - }); - - interface FunctionBracesInfo { - func: ts.ArrowFunction; - expression: ts.Expression | undefined; - returnStatement?: ts.ReturnStatement; - addBraces: boolean; - } - - function getRefactorActionsToRemoveFunctionBraces(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const { file, startPosition, triggerReason } = context; - const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - return [{ - name: refactorName, - description: refactorDescription, - actions: [ - info.addBraces ? addBracesAction : removeBracesAction - ] - }]; - } - - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ - name: refactorName, - description: refactorDescription, - actions: [ - { ...addBracesAction, notApplicableReason: info.error }, - { ...removeBracesAction, notApplicableReason: info.error }, - ] - }]; - } +const refactorName = "Add or remove braces in an arrow function"; +const refactorDescription = ts.Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; + +const addBracesAction = { + name: "Add braces to arrow function", + description: ts.Diagnostics.Add_braces_to_arrow_function.message, + kind: "refactor.rewrite.arrow.braces.add", +}; +const removeBracesAction = { + name: "Remove braces from arrow function", + description: ts.Diagnostics.Remove_braces_from_arrow_function.message, + kind: "refactor.rewrite.arrow.braces.remove" +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [removeBracesAction.kind], + getEditsForAction: getRefactorEditsToRemoveFunctionBraces, + getAvailableActions: getRefactorActionsToRemoveFunctionBraces +}); + +interface FunctionBracesInfo { + func: ts.ArrowFunction; + expression: ts.Expression | undefined; + returnStatement?: ts.ReturnStatement; + addBraces: boolean; +} +function getRefactorActionsToRemoveFunctionBraces(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const { file, startPosition, triggerReason } = context; + const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked"); + if (!info) return ts.emptyArray; + if (!ts.refactor.isRefactorErrorInfo(info)) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [ + info.addBraces ? addBracesAction : removeBracesAction + ] + }]; } - function getRefactorEditsToRemoveFunctionBraces(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - const { file, startPosition } = context; - const info = getConvertibleArrowFunctionAtPosition(file, startPosition); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [ + { ...addBracesAction, notApplicableReason: info.error }, + { ...removeBracesAction, notApplicableReason: info.error }, + ] + }]; + } - const { expression, returnStatement, func } = info; + return ts.emptyArray; +} - let body: ts.ConciseBody; +function getRefactorEditsToRemoveFunctionBraces(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + const { file, startPosition } = context; + const info = getConvertibleArrowFunctionAtPosition(file, startPosition); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); - if (actionName === addBracesAction.name) { - const returnStatement = ts.factory.createReturnStatement(expression); - body = ts.factory.createBlock([returnStatement], /* multiLine */ true); - ts.copyLeadingComments(expression!, returnStatement, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true); - } - else if (actionName === removeBracesAction.name && returnStatement) { - const actualExpression = expression || ts.factory.createVoidZero(); - body = ts.needsParentheses(actualExpression) ? ts.factory.createParenthesizedExpression(actualExpression) : actualExpression; - ts.copyTrailingAsLeadingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - ts.copyLeadingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - ts.copyTrailingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - } - else { - ts.Debug.fail("invalid action"); - } + const { expression, returnStatement, func } = info; - const edits = ts.textChanges.ChangeTracker.with(context, t => { - t.replaceNode(file, func.body, body); - }); + let body: ts.ConciseBody; - return { renameFilename: undefined, renameLocation: undefined, edits }; + if (actionName === addBracesAction.name) { + const returnStatement = ts.factory.createReturnStatement(expression); + body = ts.factory.createBlock([returnStatement], /* multiLine */ true); + ts.copyLeadingComments(expression!, returnStatement, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true); + } + else if (actionName === removeBracesAction.name && returnStatement) { + const actualExpression = expression || ts.factory.createVoidZero(); + body = ts.needsParentheses(actualExpression) ? ts.factory.createParenthesizedExpression(actualExpression) : actualExpression; + ts.copyTrailingAsLeadingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + ts.copyLeadingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + ts.copyTrailingComments(returnStatement, body, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + } + else { + ts.Debug.fail("invalid action"); } - function getConvertibleArrowFunctionAtPosition(file: ts.SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | ts.refactor.RefactorErrorInfo | undefined { - const node = ts.getTokenAtPosition(file, startPosition); - const func = ts.getContainingFunction(node); + const edits = ts.textChanges.ChangeTracker.with(context, t => { + t.replaceNode(file, func.body, body); + }); - if (!func) { - return { - error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_a_containing_arrow_function) - }; - } + return { renameFilename: undefined, renameLocation: undefined, edits }; +} - if (!ts.isArrowFunction(func)) { - return { - error: ts.getLocaleSpecificMessage(ts.Diagnostics.Containing_function_is_not_an_arrow_function) - }; - } +function getConvertibleArrowFunctionAtPosition(file: ts.SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | ts.refactor.RefactorErrorInfo | undefined { + const node = ts.getTokenAtPosition(file, startPosition); + const func = ts.getContainingFunction(node); - if ((!ts.rangeContainsRange(func, node) || ts.rangeContainsRange(func.body, node) && !considerFunctionBodies)) { - return undefined; - } + if (!func) { + return { + error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_a_containing_arrow_function) + }; + } - if (ts.refactor.refactorKindBeginsWith(addBracesAction.kind, kind) && ts.isExpression(func.body)) { - return { func, addBraces: true, expression: func.body }; - } - else if (ts.refactor.refactorKindBeginsWith(removeBracesAction.kind, kind) && ts.isBlock(func.body) && func.body.statements.length === 1) { - const firstStatement = ts.first(func.body.statements); - if (ts.isReturnStatement(firstStatement)) { - return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement }; - } - } + if (!ts.isArrowFunction(func)) { + return { + error: ts.getLocaleSpecificMessage(ts.Diagnostics.Containing_function_is_not_an_arrow_function) + }; + } + + if ((!ts.rangeContainsRange(func, node) || ts.rangeContainsRange(func.body, node) && !considerFunctionBodies)) { return undefined; } + + if (ts.refactor.refactorKindBeginsWith(addBracesAction.kind, kind) && ts.isExpression(func.body)) { + return { func, addBraces: true, expression: func.body }; + } + else if (ts.refactor.refactorKindBeginsWith(removeBracesAction.kind, kind) && ts.isBlock(func.body) && func.body.statements.length === 1) { + const firstStatement = ts.first(func.body.statements); + if (ts.isReturnStatement(firstStatement)) { + return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement }; + } + } + return undefined; +} } diff --git a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts index 5779d4b8694a0..c1be94e4a5a7c 100644 --- a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts +++ b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts @@ -1,262 +1,262 @@ /* @internal */ namespace ts.refactor.convertArrowFunctionOrFunctionExpression { - const refactorName = "Convert arrow function or function expression"; - const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_arrow_function_or_function_expression); - - const toAnonymousFunctionAction = { - name: "Convert to anonymous function", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_anonymous_function), - kind: "refactor.rewrite.function.anonymous", - }; - const toNamedFunctionAction = { - name: "Convert to named function", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_named_function), - kind: "refactor.rewrite.function.named", - }; - const toArrowFunctionAction = { - name: "Convert to arrow function", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_arrow_function), - kind: "refactor.rewrite.function.arrow", - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [ - toAnonymousFunctionAction.kind, - toNamedFunctionAction.kind, - toArrowFunctionAction.kind - ], - getEditsForAction: getRefactorEditsToConvertFunctionExpressions, - getAvailableActions: getRefactorActionsToConvertFunctionExpressions - }); +const refactorName = "Convert arrow function or function expression"; +const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_arrow_function_or_function_expression); + +const toAnonymousFunctionAction = { + name: "Convert to anonymous function", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_anonymous_function), + kind: "refactor.rewrite.function.anonymous", +}; +const toNamedFunctionAction = { + name: "Convert to named function", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_named_function), + kind: "refactor.rewrite.function.named", +}; +const toArrowFunctionAction = { + name: "Convert to arrow function", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_arrow_function), + kind: "refactor.rewrite.function.arrow", +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [ + toAnonymousFunctionAction.kind, + toNamedFunctionAction.kind, + toArrowFunctionAction.kind + ], + getEditsForAction: getRefactorEditsToConvertFunctionExpressions, + getAvailableActions: getRefactorActionsToConvertFunctionExpressions +}); + +interface FunctionInfo { + readonly selectedVariableDeclaration: boolean; + readonly func: ts.FunctionExpression | ts.ArrowFunction; +} - interface FunctionInfo { - readonly selectedVariableDeclaration: boolean; - readonly func: ts.FunctionExpression | ts.ArrowFunction; - } +interface VariableInfo { + readonly variableDeclaration: ts.VariableDeclaration; + readonly variableDeclarationList: ts.VariableDeclarationList; + readonly statement: ts.VariableStatement; + readonly name: ts.Identifier; +} - interface VariableInfo { - readonly variableDeclaration: ts.VariableDeclaration; - readonly variableDeclarationList: ts.VariableDeclarationList; - readonly statement: ts.VariableStatement; - readonly name: ts.Identifier; +function getRefactorActionsToConvertFunctionExpressions(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const { file, startPosition, program, kind } = context; + const info = getFunctionInfo(file, startPosition, program); + + if (!info) + return ts.emptyArray; + const { selectedVariableDeclaration, func } = info; + const possibleActions: ts.RefactorActionInfo[] = []; + const errors: ts.RefactorActionInfo[] = []; + if (ts.refactor.refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) { + const error = selectedVariableDeclaration || (ts.isArrowFunction(func) && ts.isVariableDeclaration(func.parent)) ? + undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_named_function); + if (error) { + errors.push({ ...toNamedFunctionAction, notApplicableReason: error }); + } + else { + possibleActions.push(toNamedFunctionAction); + } } - function getRefactorActionsToConvertFunctionExpressions(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const { file, startPosition, program, kind } = context; - const info = getFunctionInfo(file, startPosition, program); - - if (!info) - return ts.emptyArray; - const { selectedVariableDeclaration, func } = info; - const possibleActions: ts.RefactorActionInfo[] = []; - const errors: ts.RefactorActionInfo[] = []; - if (ts.refactor.refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) { - const error = selectedVariableDeclaration || (ts.isArrowFunction(func) && ts.isVariableDeclaration(func.parent)) ? - undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_named_function); - if (error) { - errors.push({ ...toNamedFunctionAction, notApplicableReason: error }); - } - else { - possibleActions.push(toNamedFunctionAction); - } + if (ts.refactor.refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) { + const error = !selectedVariableDeclaration && ts.isArrowFunction(func) ? + undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_anonymous_function); + if (error) { + errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error }); } - - if (ts.refactor.refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) { - const error = !selectedVariableDeclaration && ts.isArrowFunction(func) ? - undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_anonymous_function); - if (error) { - errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error }); - } - else { - possibleActions.push(toAnonymousFunctionAction); - } + else { + possibleActions.push(toAnonymousFunctionAction); } + } - if (ts.refactor.refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) { - const error = ts.isFunctionExpression(func) ? undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_arrow_function); - if (error) { - errors.push({ ...toArrowFunctionAction, notApplicableReason: error }); - } - else { - possibleActions.push(toArrowFunctionAction); - } + if (ts.refactor.refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) { + const error = ts.isFunctionExpression(func) ? undefined : ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_convert_to_arrow_function); + if (error) { + errors.push({ ...toArrowFunctionAction, notApplicableReason: error }); + } + else { + possibleActions.push(toArrowFunctionAction); } - - return [{ - name: refactorName, - description: refactorDescription, - actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ? - errors : possibleActions - }]; } - function getRefactorEditsToConvertFunctionExpressions(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - const { file, startPosition, program } = context; - const info = getFunctionInfo(file, startPosition, program); + return [{ + name: refactorName, + description: refactorDescription, + actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ? + errors : possibleActions + }]; +} - if (!info) - return undefined; - const { func } = info; - const edits: ts.FileTextChanges[] = []; - - switch (actionName) { - case toAnonymousFunctionAction.name: - edits.push(...getEditInfoForConvertToAnonymousFunction(context, func)); - break; - - case toNamedFunctionAction.name: - const variableInfo = getVariableInfo(func); - if (!variableInfo) - return undefined; - - edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo)); - break; - - case toArrowFunctionAction.name: - if (!ts.isFunctionExpression(func)) - return undefined; - edits.push(...getEditInfoForConvertToArrowFunction(context, func)); - break; - - default: - return ts.Debug.fail("invalid action"); - } +function getRefactorEditsToConvertFunctionExpressions(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + const { file, startPosition, program } = context; + const info = getFunctionInfo(file, startPosition, program); - return { renameFilename: undefined, renameLocation: undefined, edits }; - } + if (!info) + return undefined; + const { func } = info; + const edits: ts.FileTextChanges[] = []; - function containingThis(node: ts.Node): boolean { - let containsThis = false; - node.forEachChild(function checkThis(child) { + switch (actionName) { + case toAnonymousFunctionAction.name: + edits.push(...getEditInfoForConvertToAnonymousFunction(context, func)); + break; - if (ts.isThis(child)) { - containsThis = true; - return; - } + case toNamedFunctionAction.name: + const variableInfo = getVariableInfo(func); + if (!variableInfo) + return undefined; - if (!ts.isClassLike(child) && !ts.isFunctionDeclaration(child) && !ts.isFunctionExpression(child)) { - ts.forEachChild(child, checkThis); - } - }); + edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo)); + break; - return containsThis; + case toArrowFunctionAction.name: + if (!ts.isFunctionExpression(func)) + return undefined; + edits.push(...getEditInfoForConvertToArrowFunction(context, func)); + break; + + default: + return ts.Debug.fail("invalid action"); } - function getFunctionInfo(file: ts.SourceFile, startPosition: number, program: ts.Program): FunctionInfo | undefined { - const token = ts.getTokenAtPosition(file, startPosition); - const typeChecker = program.getTypeChecker(); - const func = tryGetFunctionFromVariableDeclaration(file, typeChecker, token.parent); - if (func && !containingThis(func.body) && !typeChecker.containsArgumentsReference(func)) { - return { selectedVariableDeclaration: true, func }; + return { renameFilename: undefined, renameLocation: undefined, edits }; +} + +function containingThis(node: ts.Node): boolean { + let containsThis = false; + node.forEachChild(function checkThis(child) { + + if (ts.isThis(child)) { + containsThis = true; + return; } - const maybeFunc = ts.getContainingFunction(token); - if (maybeFunc && - (ts.isFunctionExpression(maybeFunc) || ts.isArrowFunction(maybeFunc)) && - !ts.rangeContainsRange(maybeFunc.body, token) && - !containingThis(maybeFunc.body) && - !typeChecker.containsArgumentsReference(maybeFunc)) { - if (ts.isFunctionExpression(maybeFunc) && isFunctionReferencedInFile(file, typeChecker, maybeFunc)) - return undefined; - return { selectedVariableDeclaration: false, func: maybeFunc }; + if (!ts.isClassLike(child) && !ts.isFunctionDeclaration(child) && !ts.isFunctionExpression(child)) { + ts.forEachChild(child, checkThis); } + }); - return undefined; - } + return containsThis; +} - function isSingleVariableDeclaration(parent: ts.Node): parent is ts.VariableDeclarationList { - return ts.isVariableDeclaration(parent) || (ts.isVariableDeclarationList(parent) && parent.declarations.length === 1); +function getFunctionInfo(file: ts.SourceFile, startPosition: number, program: ts.Program): FunctionInfo | undefined { + const token = ts.getTokenAtPosition(file, startPosition); + const typeChecker = program.getTypeChecker(); + const func = tryGetFunctionFromVariableDeclaration(file, typeChecker, token.parent); + if (func && !containingThis(func.body) && !typeChecker.containsArgumentsReference(func)) { + return { selectedVariableDeclaration: true, func }; } - function tryGetFunctionFromVariableDeclaration(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, parent: ts.Node): ts.ArrowFunction | ts.FunctionExpression | undefined { - if (!isSingleVariableDeclaration(parent)) { + const maybeFunc = ts.getContainingFunction(token); + if (maybeFunc && + (ts.isFunctionExpression(maybeFunc) || ts.isArrowFunction(maybeFunc)) && + !ts.rangeContainsRange(maybeFunc.body, token) && + !containingThis(maybeFunc.body) && + !typeChecker.containsArgumentsReference(maybeFunc)) { + if (ts.isFunctionExpression(maybeFunc) && isFunctionReferencedInFile(file, typeChecker, maybeFunc)) return undefined; - } - const variableDeclaration = ts.isVariableDeclaration(parent) ? parent : ts.first(parent.declarations); - const initializer = variableDeclaration.initializer; - if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) && !isFunctionReferencedInFile(sourceFile, typeChecker, initializer))) { - return initializer; - } - return undefined; - } - - function convertToBlock(body: ts.ConciseBody): ts.Block { - if (ts.isExpression(body)) { - const returnStatement = ts.factory.createReturnStatement(body); - const file = body.getSourceFile(); - ts.suppressLeadingAndTrailingTrivia(returnStatement); - ts.copyTrailingAsLeadingComments(body, returnStatement, file, /* commentKind */ undefined, /* hasTrailingNewLine */ true); - return ts.factory.createBlock([returnStatement], /* multiLine */ true); - } - else { - return body; - } + return { selectedVariableDeclaration: false, func: maybeFunc }; } - function getVariableInfo(func: ts.FunctionExpression | ts.ArrowFunction): VariableInfo | undefined { - const variableDeclaration = func.parent; - if (!ts.isVariableDeclaration(variableDeclaration) || !ts.isVariableDeclarationInVariableStatement(variableDeclaration)) - return undefined; + return undefined; +} - const variableDeclarationList = variableDeclaration.parent; - const statement = variableDeclarationList.parent; - if (!ts.isVariableDeclarationList(variableDeclarationList) || !ts.isVariableStatement(statement) || !ts.isIdentifier(variableDeclaration.name)) - return undefined; +function isSingleVariableDeclaration(parent: ts.Node): parent is ts.VariableDeclarationList { + return ts.isVariableDeclaration(parent) || (ts.isVariableDeclarationList(parent) && parent.declarations.length === 1); +} - return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name }; +function tryGetFunctionFromVariableDeclaration(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, parent: ts.Node): ts.ArrowFunction | ts.FunctionExpression | undefined { + if (!isSingleVariableDeclaration(parent)) { + return undefined; + } + const variableDeclaration = ts.isVariableDeclaration(parent) ? parent : ts.first(parent.declarations); + const initializer = variableDeclaration.initializer; + if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) && !isFunctionReferencedInFile(sourceFile, typeChecker, initializer))) { + return initializer; } + return undefined; +} - function getEditInfoForConvertToAnonymousFunction(context: ts.RefactorContext, func: ts.FunctionExpression | ts.ArrowFunction): ts.FileTextChanges[] { - const { file } = context; - const body = convertToBlock(func.body); - const newNode = ts.factory.createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body); - return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); +function convertToBlock(body: ts.ConciseBody): ts.Block { + if (ts.isExpression(body)) { + const returnStatement = ts.factory.createReturnStatement(body); + const file = body.getSourceFile(); + ts.suppressLeadingAndTrailingTrivia(returnStatement); + ts.copyTrailingAsLeadingComments(body, returnStatement, file, /* commentKind */ undefined, /* hasTrailingNewLine */ true); + return ts.factory.createBlock([returnStatement], /* multiLine */ true); + } + else { + return body; } +} - function getEditInfoForConvertToNamedFunction(context: ts.RefactorContext, func: ts.FunctionExpression | ts.ArrowFunction, variableInfo: VariableInfo): ts.FileTextChanges[] { - const { file } = context; - const body = convertToBlock(func.body); +function getVariableInfo(func: ts.FunctionExpression | ts.ArrowFunction): VariableInfo | undefined { + const variableDeclaration = func.parent; + if (!ts.isVariableDeclaration(variableDeclaration) || !ts.isVariableDeclarationInVariableStatement(variableDeclaration)) + return undefined; - const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo; - ts.suppressLeadingTrivia(statement); - const modifiersFlags = (ts.getCombinedModifierFlags(variableDeclaration) & ts.ModifierFlags.Export) | ts.getEffectiveModifierFlags(func); - const modifiers = ts.factory.createModifiersFromModifierFlags(modifiersFlags); - const newNode = ts.factory.createFunctionDeclaration(func.decorators, ts.length(modifiers) ? modifiers : undefined, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body); + const variableDeclarationList = variableDeclaration.parent; + const statement = variableDeclarationList.parent; + if (!ts.isVariableDeclarationList(variableDeclarationList) || !ts.isVariableStatement(statement) || !ts.isIdentifier(variableDeclaration.name)) + return undefined; - if (variableDeclarationList.declarations.length === 1) { - return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode)); - } - else { - return ts.textChanges.ChangeTracker.with(context, t => { - t.delete(file, variableDeclaration); - t.insertNodeAfter(file, statement, newNode); - }); - } - } + return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name }; +} - function getEditInfoForConvertToArrowFunction(context: ts.RefactorContext, func: ts.FunctionExpression): ts.FileTextChanges[] { - const { file } = context; - const statements = func.body.statements; - const head = statements[0]; - let body: ts.ConciseBody; +function getEditInfoForConvertToAnonymousFunction(context: ts.RefactorContext, func: ts.FunctionExpression | ts.ArrowFunction): ts.FileTextChanges[] { + const { file } = context; + const body = convertToBlock(func.body); + const newNode = ts.factory.createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body); + return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); +} - if (canBeConvertedToExpression(func.body, head)) { - body = head.expression!; - ts.suppressLeadingAndTrailingTrivia(body); - ts.copyComments(head, body); - } - else { - body = func.body; - } +function getEditInfoForConvertToNamedFunction(context: ts.RefactorContext, func: ts.FunctionExpression | ts.ArrowFunction, variableInfo: VariableInfo): ts.FileTextChanges[] { + const { file } = context; + const body = convertToBlock(func.body); - const newNode = ts.factory.createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body); - return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); - } + const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo; + ts.suppressLeadingTrivia(statement); + const modifiersFlags = (ts.getCombinedModifierFlags(variableDeclaration) & ts.ModifierFlags.Export) | ts.getEffectiveModifierFlags(func); + const modifiers = ts.factory.createModifiersFromModifierFlags(modifiersFlags); + const newNode = ts.factory.createFunctionDeclaration(func.decorators, ts.length(modifiers) ? modifiers : undefined, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body); - function canBeConvertedToExpression(body: ts.Block, head: ts.Statement): head is ts.ReturnStatement { - return body.statements.length === 1 && ((ts.isReturnStatement(head) && !!head.expression)); + if (variableDeclarationList.declarations.length === 1) { + return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode)); + } + else { + return ts.textChanges.ChangeTracker.with(context, t => { + t.delete(file, variableDeclaration); + t.insertNodeAfter(file, statement, newNode); + }); } +} - function isFunctionReferencedInFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, node: ts.FunctionExpression): boolean { - return !!node.name && ts.FindAllReferences.Core.isSymbolReferencedInFile(node.name, typeChecker, sourceFile); +function getEditInfoForConvertToArrowFunction(context: ts.RefactorContext, func: ts.FunctionExpression): ts.FileTextChanges[] { + const { file } = context; + const statements = func.body.statements; + const head = statements[0]; + let body: ts.ConciseBody; + + if (canBeConvertedToExpression(func.body, head)) { + body = head.expression!; + ts.suppressLeadingAndTrailingTrivia(body); + ts.copyComments(head, body); + } + else { + body = func.body; } + + const newNode = ts.factory.createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body); + return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); +} + +function canBeConvertedToExpression(body: ts.Block, head: ts.Statement): head is ts.ReturnStatement { + return body.statements.length === 1 && ((ts.isReturnStatement(head) && !!head.expression)); +} + +function isFunctionReferencedInFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, node: ts.FunctionExpression): boolean { + return !!node.name && ts.FindAllReferences.Core.isSymbolReferencedInFile(node.name, typeChecker, sourceFile); +} } diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index a5705a1a45b4b..1d461063fc0fb 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -1,264 +1,264 @@ /* @internal */ namespace ts.refactor { - const refactorName = "Convert export"; +const refactorName = "Convert export"; - const defaultToNamedAction = { - name: "Convert default export to named export", - description: ts.Diagnostics.Convert_default_export_to_named_export.message, - kind: "refactor.rewrite.export.named" - }; - const namedToDefaultAction = { - name: "Convert named export to default export", - description: ts.Diagnostics.Convert_named_export_to_default_export.message, - kind: "refactor.rewrite.export.default" - }; +const defaultToNamedAction = { + name: "Convert default export to named export", + description: ts.Diagnostics.Convert_default_export_to_named_export.message, + kind: "refactor.rewrite.export.named" +}; +const namedToDefaultAction = { + name: "Convert named export to default export", + description: ts.Diagnostics.Convert_named_export_to_default_export.message, + kind: "refactor.rewrite.export.default" +}; - ts.refactor.registerRefactor(refactorName, { - kinds: [ - defaultToNamedAction.kind, - namedToDefaultAction.kind - ], - getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndDefaultExports(context): readonly ts.ApplicableRefactorInfo[] { - const info = getInfo(context, context.triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction; - return [{ name: refactorName, description: action.description, actions: [action] }]; - } +ts.refactor.registerRefactor(refactorName, { + kinds: [ + defaultToNamedAction.kind, + namedToDefaultAction.kind + ], + getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndDefaultExports(context): readonly ts.ApplicableRefactorInfo[] { + const info = getInfo(context, context.triggerReason === "invoked"); + if (!info) + return ts.emptyArray; + if (!ts.refactor.isRefactorErrorInfo(info)) { + const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction; + return [{ name: refactorName, description: action.description, actions: [action] }]; + } - if (context.preferences.provideRefactorNotApplicableReason) { - return [ - { name: refactorName, description: ts.Diagnostics.Convert_default_export_to_named_export.message, actions: [ - { ...defaultToNamedAction, notApplicableReason: info.error }, - { ...namedToDefaultAction, notApplicableReason: info.error }, - ]} - ]; - } + if (context.preferences.provideRefactorNotApplicableReason) { + return [ + { name: refactorName, description: ts.Diagnostics.Convert_default_export_to_named_export.message, actions: [ + { ...defaultToNamedAction, notApplicableReason: info.error }, + { ...namedToDefaultAction, notApplicableReason: info.error }, + ]} + ]; + } - return ts.emptyArray; - }, - getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndDefaultExports(context, actionName): ts.RefactorEditInfo { - ts.Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name"); - const info = getInfo(context); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); - const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, t, context.cancellationToken)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - }, - }); + return ts.emptyArray; + }, + getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndDefaultExports(context, actionName): ts.RefactorEditInfo { + ts.Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name"); + const info = getInfo(context); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, 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 = ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration | ts.NamespaceDeclaration | ts.TypeAliasDeclaration | ts.VariableStatement | ts.ExportAssignment; - interface ExportInfo { - readonly exportNode: ExportToConvert; - readonly exportName: ts.Identifier; // This is exportNode.name except for VariableStatement_s. - readonly wasDefault: boolean; - readonly exportingModuleSymbol: ts.Symbol; +// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. +type ExportToConvert = ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration | ts.NamespaceDeclaration | ts.TypeAliasDeclaration | ts.VariableStatement | ts.ExportAssignment; +interface ExportInfo { + readonly exportNode: ExportToConvert; + readonly exportName: ts.Identifier; // This is exportNode.name except for VariableStatement_s. + readonly wasDefault: boolean; + readonly exportingModuleSymbol: ts.Symbol; +} +; +function getInfo(context: ts.RefactorContext, considerPartialSpans = true): ExportInfo | ts.refactor.RefactorErrorInfo | undefined { + const { file, program } = context; + const span = ts.getRefactorContextSpan(context); + const token = ts.getTokenAtPosition(file, span.start); + const exportNode = !!(token.parent && ts.getSyntacticModifierFlags(token.parent) & ts.ModifierFlags.Export) && considerPartialSpans ? token.parent : ts.getParentNodeInSpan(token, file, span); + if (!exportNode || (!ts.isSourceFile(exportNode.parent) && !(ts.isModuleBlock(exportNode.parent) && ts.isAmbientModule(exportNode.parent.parent)))) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_export_statement) }; } - ; - function getInfo(context: ts.RefactorContext, considerPartialSpans = true): ExportInfo | ts.refactor.RefactorErrorInfo | undefined { - const { file, program } = context; - const span = ts.getRefactorContextSpan(context); - const token = ts.getTokenAtPosition(file, span.start); - const exportNode = !!(token.parent && ts.getSyntacticModifierFlags(token.parent) & ts.ModifierFlags.Export) && considerPartialSpans ? token.parent : ts.getParentNodeInSpan(token, file, span); - if (!exportNode || (!ts.isSourceFile(exportNode.parent) && !(ts.isModuleBlock(exportNode.parent) && ts.isAmbientModule(exportNode.parent.parent)))) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_export_statement) }; + const exportingModuleSymbol = ts.isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; + const flags = ts.getSyntacticModifierFlags(exportNode) || ((ts.isExportAssignment(exportNode) && !exportNode.isExportEquals) ? ts.ModifierFlags.ExportDefault : ts.ModifierFlags.None); + const wasDefault = !!(flags & ts.ModifierFlags.Default); + // If source file already has a default export, don't offer refactor. + if (!(flags & ts.ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(ts.InternalSymbolName.Default)) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.This_file_already_has_a_default_export) }; + } + + const checker = program.getTypeChecker(); + const noSymbolError = (id: ts.Node) => (ts.isIdentifier(id) && checker.getSymbolAtLocation(id)) ? undefined + : { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_named_export) }; + + switch (exportNode.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.ModuleDeclaration: { + const node = exportNode as ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.NamespaceDeclaration; + if (!node.name) + return undefined; + return noSymbolError(node.name) + || { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol }; } - const exportingModuleSymbol = ts.isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; - const flags = ts.getSyntacticModifierFlags(exportNode) || ((ts.isExportAssignment(exportNode) && !exportNode.isExportEquals) ? ts.ModifierFlags.ExportDefault : ts.ModifierFlags.None); - const wasDefault = !!(flags & ts.ModifierFlags.Default); - // If source file already has a default export, don't offer refactor. - if (!(flags & ts.ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(ts.InternalSymbolName.Default)) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.This_file_already_has_a_default_export) }; + case ts.SyntaxKind.VariableStatement: { + const vs = exportNode as ts.VariableStatement; + // Must be `export const x = something;`. + if (!(vs.declarationList.flags & ts.NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { + return undefined; + } + const decl = ts.first(vs.declarationList.declarations); + if (!decl.initializer) + return undefined; + ts.Debug.assert(!wasDefault, "Can't have a default flag here"); + return noSymbolError(decl.name) + || { exportNode: vs, exportName: decl.name as ts.Identifier, wasDefault, exportingModuleSymbol }; } + case ts.SyntaxKind.ExportAssignment: { + const node = exportNode as ts.ExportAssignment; + if (node.isExportEquals) + return undefined; + return noSymbolError(node.expression) + || { exportNode: node, exportName: node.expression as ts.Identifier, wasDefault, exportingModuleSymbol }; + } + default: + return undefined; + } +} - const checker = program.getTypeChecker(); - const noSymbolError = (id: ts.Node) => (ts.isIdentifier(id) && checker.getSymbolAtLocation(id)) ? undefined - : { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_named_export) }; +function doChange(exportingSourceFile: ts.SourceFile, program: ts.Program, info: ExportInfo, changes: ts.textChanges.ChangeTracker, cancellationToken: ts.CancellationToken | undefined): void { + changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); + changeImports(program, info, changes, cancellationToken); +} +function changeExport(exportingSourceFile: ts.SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: ts.textChanges.ChangeTracker, checker: ts.TypeChecker): void { + if (wasDefault) { + if (ts.isExportAssignment(exportNode) && !exportNode.isExportEquals) { + const exp = exportNode.expression as ts.Identifier; + const spec = makeExportSpecifier(exp.text, exp.text); + changes.replaceNode(exportingSourceFile, exportNode, ts.factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, ts.factory.createNamedExports([spec]))); + } + else { + changes.delete(exportingSourceFile, ts.Debug.checkDefined(ts.findModifier(exportNode, ts.SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); + } + } + else { + const exportKeyword = ts.Debug.checkDefined(ts.findModifier(exportNode, ts.SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); switch (exportNode.kind) { case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.InterfaceDeclaration: + changes.insertNodeAfter(exportingSourceFile, exportKeyword, ts.factory.createToken(ts.SyntaxKind.DefaultKeyword)); + break; + case ts.SyntaxKind.VariableStatement: + // If 'x' isn't used in this file and doesn't have type definition, `export const x = 0;` --> `export default 0;` + const decl = ts.first(exportNode.declarationList.declarations); + if (!ts.FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile) && !decl.type) { + // We checked in `getInfo` that an initializer exists. + changes.replaceNode(exportingSourceFile, exportNode, ts.factory.createExportDefault(ts.Debug.checkDefined(decl.initializer, "Initializer was previously known to be present"))); + break; + } + // falls through case ts.SyntaxKind.EnumDeclaration: case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ModuleDeclaration: { - const node = exportNode as ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.NamespaceDeclaration; - if (!node.name) - return undefined; - return noSymbolError(node.name) - || { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol }; - } - case ts.SyntaxKind.VariableStatement: { - const vs = exportNode as ts.VariableStatement; - // Must be `export const x = something;`. - if (!(vs.declarationList.flags & ts.NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { - return undefined; - } - const decl = ts.first(vs.declarationList.declarations); - if (!decl.initializer) - return undefined; - ts.Debug.assert(!wasDefault, "Can't have a default flag here"); - return noSymbolError(decl.name) - || { exportNode: vs, exportName: decl.name as ts.Identifier, wasDefault, exportingModuleSymbol }; - } - case ts.SyntaxKind.ExportAssignment: { - const node = exportNode as ts.ExportAssignment; - if (node.isExportEquals) - return undefined; - return noSymbolError(node.expression) - || { exportNode: node, exportName: node.expression as ts.Identifier, wasDefault, exportingModuleSymbol }; - } + case ts.SyntaxKind.ModuleDeclaration: + // `export type T = number;` -> `type T = number; export default T;` + changes.deleteModifier(exportingSourceFile, exportKeyword); + changes.insertNodeAfter(exportingSourceFile, exportNode, ts.factory.createExportDefault(ts.factory.createIdentifier(exportName.text))); + break; default: - return undefined; + ts.Debug.fail(`Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); } } +} - function doChange(exportingSourceFile: ts.SourceFile, program: ts.Program, info: ExportInfo, changes: ts.textChanges.ChangeTracker, cancellationToken: ts.CancellationToken | undefined): void { - changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); - changeImports(program, info, changes, cancellationToken); - } - - function changeExport(exportingSourceFile: ts.SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: ts.textChanges.ChangeTracker, checker: ts.TypeChecker): void { +function changeImports(program: ts.Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: ts.textChanges.ChangeTracker, cancellationToken: ts.CancellationToken | undefined): void { + const checker = program.getTypeChecker(); + const exportSymbol = ts.Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); + ts.FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { + const importingSourceFile = ref.getSourceFile(); if (wasDefault) { - if (ts.isExportAssignment(exportNode) && !exportNode.isExportEquals) { - const exp = exportNode.expression as ts.Identifier; - const spec = makeExportSpecifier(exp.text, exp.text); - changes.replaceNode(exportingSourceFile, exportNode, ts.factory.createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, ts.factory.createNamedExports([spec]))); - } - else { - changes.delete(exportingSourceFile, ts.Debug.checkDefined(ts.findModifier(exportNode, ts.SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); - } + changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); } else { - const exportKeyword = ts.Debug.checkDefined(ts.findModifier(exportNode, ts.SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); - switch (exportNode.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - changes.insertNodeAfter(exportingSourceFile, exportKeyword, ts.factory.createToken(ts.SyntaxKind.DefaultKeyword)); - break; - case ts.SyntaxKind.VariableStatement: - // If 'x' isn't used in this file and doesn't have type definition, `export const x = 0;` --> `export default 0;` - const decl = ts.first(exportNode.declarationList.declarations); - if (!ts.FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile) && !decl.type) { - // We checked in `getInfo` that an initializer exists. - changes.replaceNode(exportingSourceFile, exportNode, ts.factory.createExportDefault(ts.Debug.checkDefined(decl.initializer, "Initializer was previously known to be present"))); - break; - } - // falls through - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - // `export type T = number;` -> `type T = number; export default T;` - changes.deleteModifier(exportingSourceFile, exportKeyword); - changes.insertNodeAfter(exportingSourceFile, exportNode, ts.factory.createExportDefault(ts.factory.createIdentifier(exportName.text))); - break; - default: - ts.Debug.fail(`Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); - } + changeNamedToDefaultImport(importingSourceFile, ref, changes); } - } + }); +} - function changeImports(program: ts.Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: ts.textChanges.ChangeTracker, cancellationToken: ts.CancellationToken | undefined): void { - const checker = program.getTypeChecker(); - const exportSymbol = ts.Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); - ts.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); +function changeDefaultToNamedImport(importingSourceFile: ts.SourceFile, ref: ts.Identifier, changes: ts.textChanges.ChangeTracker, exportName: string): void { + const { parent } = ref; + switch (parent.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + // `a.default` --> `a.foo` + changes.replaceNode(importingSourceFile, ref, ts.factory.createIdentifier(exportName)); + break; + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: { + const spec = parent as ts.ImportSpecifier | ts.ExportSpecifier; + // `default as foo` --> `foo`, `default as bar` --> `foo as bar` + changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); + break; + } + case ts.SyntaxKind.ImportClause: { + const clause = parent as ts.ImportClause; + ts.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, ts.factory.createNamedImports([spec])); } - else { - changeNamedToDefaultImport(importingSourceFile, ref, changes); + else if (namedBindings.kind === ts.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 = ts.isStringLiteral(clause.parent.moduleSpecifier) ? ts.quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : ts.QuotePreference.Double; + const newImport = ts.makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); + changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); } - }); - } - - function changeDefaultToNamedImport(importingSourceFile: ts.SourceFile, ref: ts.Identifier, changes: ts.textChanges.ChangeTracker, exportName: string): void { - const { parent } = ref; - switch (parent.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - // `a.default` --> `a.foo` - changes.replaceNode(importingSourceFile, ref, ts.factory.createIdentifier(exportName)); - break; - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: { - const spec = parent as ts.ImportSpecifier | ts.ExportSpecifier; - // `default as foo` --> `foo`, `default as bar` --> `foo as bar` - changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); - break; - } - case ts.SyntaxKind.ImportClause: { - const clause = parent as ts.ImportClause; - ts.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, ts.factory.createNamedImports([spec])); - } - else if (namedBindings.kind === ts.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 = ts.isStringLiteral(clause.parent.moduleSpecifier) ? ts.quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : ts.QuotePreference.Double; - const newImport = ts.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); } - case ts.SyntaxKind.ImportType: - const importTypeNode = parent as ts.ImportTypeNode; - changes.replaceNode(importingSourceFile, parent, ts.factory.createImportTypeNode(importTypeNode.argument, ts.factory.createIdentifier(exportName), importTypeNode.typeArguments, importTypeNode.isTypeOf)); - break; - default: - ts.Debug.failBadSyntaxKind(parent); + break; } + case ts.SyntaxKind.ImportType: + const importTypeNode = parent as ts.ImportTypeNode; + changes.replaceNode(importingSourceFile, parent, ts.factory.createImportTypeNode(importTypeNode.argument, ts.factory.createIdentifier(exportName), importTypeNode.typeArguments, importTypeNode.isTypeOf)); + break; + default: + ts.Debug.failBadSyntaxKind(parent); } +} - function changeNamedToDefaultImport(importingSourceFile: ts.SourceFile, ref: ts.Identifier, changes: ts.textChanges.ChangeTracker): void { - const parent = ref.parent as ts.PropertyAccessExpression | ts.ImportSpecifier | ts.ExportSpecifier; - switch (parent.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - // `a.foo` --> `a.default` - changes.replaceNode(importingSourceFile, ref, ts.factory.createIdentifier("default")); - break; - case ts.SyntaxKind.ImportSpecifier: { - // `import { foo } from "./a";` --> `import foo from "./a";` - // `import { foo as bar } from "./a";` --> `import bar from "./a";` - const defaultImport = ts.factory.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; +function changeNamedToDefaultImport(importingSourceFile: ts.SourceFile, ref: ts.Identifier, changes: ts.textChanges.ChangeTracker): void { + const parent = ref.parent as ts.PropertyAccessExpression | ts.ImportSpecifier | ts.ExportSpecifier; + switch (parent.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + // `a.foo` --> `a.default` + changes.replaceNode(importingSourceFile, ref, ts.factory.createIdentifier("default")); + break; + case ts.SyntaxKind.ImportSpecifier: { + // `import { foo } from "./a";` --> `import foo from "./a";` + // `import { foo as bar } from "./a";` --> `import bar from "./a";` + const defaultImport = ts.factory.createIdentifier(parent.name.text); + if (parent.parent.elements.length === 1) { + changes.replaceNode(importingSourceFile, parent.parent, defaultImport); } - case ts.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: - ts.Debug.assertNever(parent, `Unexpected parent kind ${(parent as ts.Node).kind}`); + break; } - + case ts.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: + ts.Debug.assertNever(parent, `Unexpected parent kind ${(parent as ts.Node).kind}`); } - function makeImportSpecifier(propertyName: string, name: string): ts.ImportSpecifier { - return ts.factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name)); - } +} - function makeExportSpecifier(propertyName: string, name: string): ts.ExportSpecifier { - return ts.factory.createExportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name)); - } +function makeImportSpecifier(propertyName: string, name: string): ts.ImportSpecifier { + return ts.factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name)); +} + +function makeExportSpecifier(propertyName: string, name: string): ts.ExportSpecifier { + return ts.factory.createExportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name)); +} } diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index 782b7be17d6b3..b546b5b73c5df 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -1,237 +1,237 @@ /* @internal */ namespace ts.refactor { - const refactorName = "Convert import"; - - const actions = { - [ts.ImportKind.Named]: { - name: "Convert namespace import to named imports", - description: ts.Diagnostics.Convert_namespace_import_to_named_imports.message, - kind: "refactor.rewrite.import.named", - }, - [ts.ImportKind.Namespace]: { - name: "Convert named imports to namespace import", - description: ts.Diagnostics.Convert_named_imports_to_namespace_import.message, - kind: "refactor.rewrite.import.namespace", - }, - [ts.ImportKind.Default]: { - name: "Convert named imports to default import", - description: ts.Diagnostics.Convert_named_imports_to_default_import.message, - kind: "refactor.rewrite.import.default", - }, - }; - - ts.refactor.registerRefactor(refactorName, { - kinds: ts.getOwnValues(actions).map(a => a.kind), - getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ts.ApplicableRefactorInfo[] { - const info = getImportConversionInfo(context, context.triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - const action = actions[info.convertTo]; - return [{ name: refactorName, description: action.description, actions: [action] }]; - } - - if (context.preferences.provideRefactorNotApplicableReason) { - return ts.getOwnValues(actions).map(action => ({ - name: refactorName, - description: action.description, - actions: [{ ...action, notApplicableReason: info.error }] - })); - } - +const refactorName = "Convert import"; + +const actions = { + [ts.ImportKind.Named]: { + name: "Convert namespace import to named imports", + description: ts.Diagnostics.Convert_namespace_import_to_named_imports.message, + kind: "refactor.rewrite.import.named", + }, + [ts.ImportKind.Namespace]: { + name: "Convert named imports to namespace import", + description: ts.Diagnostics.Convert_named_imports_to_namespace_import.message, + kind: "refactor.rewrite.import.namespace", + }, + [ts.ImportKind.Default]: { + name: "Convert named imports to default import", + description: ts.Diagnostics.Convert_named_imports_to_default_import.message, + kind: "refactor.rewrite.import.default", + }, +}; + +ts.refactor.registerRefactor(refactorName, { + kinds: ts.getOwnValues(actions).map(a => a.kind), + getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ts.ApplicableRefactorInfo[] { + const info = getImportConversionInfo(context, context.triggerReason === "invoked"); + if (!info) return ts.emptyArray; - }, - getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): ts.RefactorEditInfo { - ts.Debug.assert(ts.some(ts.getOwnValues(actions), action => action.name === actionName), "Unexpected action name"); - const info = getImportConversionInfo(context); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); - const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - } - }); - - // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. - type ImportConversionInfo = { - convertTo: ts.ImportKind.Default; - import: ts.NamedImports; - } | { - convertTo: ts.ImportKind.Namespace; - import: ts.NamedImports; - } | { - convertTo: ts.ImportKind.Named; - import: ts.NamespaceImport; - }; - function getImportConversionInfo(context: ts.RefactorContext, considerPartialSpans = true): ImportConversionInfo | ts.refactor.RefactorErrorInfo | undefined { - const { file } = context; - const span = ts.getRefactorContextSpan(context); - const token = ts.getTokenAtPosition(file, span.start); - const importDecl = considerPartialSpans ? ts.findAncestor(token, ts.isImportDeclaration) : ts.getParentNodeInSpan(token, file, span); - if (!importDecl || !ts.isImportDeclaration(importDecl)) - return { error: "Selection is not an import declaration." }; - - const end = span.start + span.length; - const nextToken = ts.findNextToken(importDecl, importDecl.parent, file); - if (nextToken && end > nextToken.getStart()) - return undefined; - - const { importClause } = importDecl; - if (!importClause) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_import_clause) }; + if (!ts.refactor.isRefactorErrorInfo(info)) { + const action = actions[info.convertTo]; + return [{ name: refactorName, description: action.description, actions: [action] }]; } - if (!importClause.namedBindings) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_namespace_import_or_named_imports) }; + if (context.preferences.provideRefactorNotApplicableReason) { + return ts.getOwnValues(actions).map(action => ({ + name: refactorName, + description: action.description, + actions: [{ ...action, notApplicableReason: info.error }] + })); } - if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { - return { convertTo: ts.ImportKind.Named, import: importClause.namedBindings }; - } - const shouldUseDefault = getShouldUseDefault(context.program, importClause); - - return shouldUseDefault - ? { convertTo: ts.ImportKind.Default, import: importClause.namedBindings } - : { convertTo: ts.ImportKind.Namespace, import: importClause.namedBindings }; + return ts.emptyArray; + }, + getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): ts.RefactorEditInfo { + ts.Debug.assert(ts.some(ts.getOwnValues(actions), action => action.name === actionName), "Unexpected action name"); + const info = getImportConversionInfo(context); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } +}); + +// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. +type ImportConversionInfo = { + convertTo: ts.ImportKind.Default; + import: ts.NamedImports; +} | { + convertTo: ts.ImportKind.Namespace; + import: ts.NamedImports; +} | { + convertTo: ts.ImportKind.Named; + import: ts.NamespaceImport; +}; +function getImportConversionInfo(context: ts.RefactorContext, considerPartialSpans = true): ImportConversionInfo | ts.refactor.RefactorErrorInfo | undefined { + const { file } = context; + const span = ts.getRefactorContextSpan(context); + const token = ts.getTokenAtPosition(file, span.start); + const importDecl = considerPartialSpans ? ts.findAncestor(token, ts.isImportDeclaration) : ts.getParentNodeInSpan(token, file, span); + if (!importDecl || !ts.isImportDeclaration(importDecl)) + return { error: "Selection is not an import declaration." }; + + const end = span.start + span.length; + const nextToken = ts.findNextToken(importDecl, importDecl.parent, file); + if (nextToken && end > nextToken.getStart()) + return undefined; + + const { importClause } = importDecl; + if (!importClause) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_import_clause) }; } - function getShouldUseDefault(program: ts.Program, importClause: ts.ImportClause) { - return ts.getAllowSyntheticDefaultImports(program.getCompilerOptions()) - && isExportEqualsModule(importClause.parent.moduleSpecifier, program.getTypeChecker()); + if (!importClause.namedBindings) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_namespace_import_or_named_imports) }; } - function doChange(sourceFile: ts.SourceFile, program: ts.Program, changes: ts.textChanges.ChangeTracker, info: ImportConversionInfo): void { - const checker = program.getTypeChecker(); - if (info.convertTo === ts.ImportKind.Named) { - doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, ts.getAllowSyntheticDefaultImports(program.getCompilerOptions())); - } - else { - doChangeNamedToNamespaceOrDefault(sourceFile, program, changes, info.import, info.convertTo === ts.ImportKind.Default); - } + if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { + return { convertTo: ts.ImportKind.Named, import: importClause.namedBindings }; } + const shouldUseDefault = getShouldUseDefault(context.program, importClause); - function doChangeNamespaceToNamed(sourceFile: ts.SourceFile, checker: ts.TypeChecker, changes: ts.textChanges.ChangeTracker, toConvert: ts.NamespaceImport, allowSyntheticDefaultImports: boolean): void { - let usedAsNamespaceOrDefault = false; + return shouldUseDefault + ? { convertTo: ts.ImportKind.Default, import: importClause.namedBindings } + : { convertTo: ts.ImportKind.Namespace, import: importClause.namedBindings }; +} - const nodesToReplace: (ts.PropertyAccessExpression | ts.QualifiedName)[] = []; - const conflictingNames = new ts.Map(); - ts.FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { - if (!ts.isPropertyAccessOrQualifiedName(id.parent)) { - usedAsNamespaceOrDefault = true; - } - else { - const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text; - if (checker.resolveName(exportName, id, ts.SymbolFlags.All, /*excludeGlobals*/ true)) { - conflictingNames.set(exportName, true); - } - ts.Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id"); - nodesToReplace.push(id.parent); - } - }); +function getShouldUseDefault(program: ts.Program, importClause: ts.ImportClause) { + return ts.getAllowSyntheticDefaultImports(program.getCompilerOptions()) + && isExportEqualsModule(importClause.parent.moduleSpecifier, program.getTypeChecker()); +} - // We may need to change `mod.x` to `_x` to avoid a name conflict. - const exportNameToImportName = new ts.Map(); +function doChange(sourceFile: ts.SourceFile, program: ts.Program, changes: ts.textChanges.ChangeTracker, info: ImportConversionInfo): void { + const checker = program.getTypeChecker(); + if (info.convertTo === ts.ImportKind.Named) { + doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, ts.getAllowSyntheticDefaultImports(program.getCompilerOptions())); + } + else { + doChangeNamedToNamespaceOrDefault(sourceFile, program, changes, info.import, info.convertTo === ts.ImportKind.Default); + } +} - for (const propertyAccessOrQualifiedName of nodesToReplace) { - const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text; - let importName = exportNameToImportName.get(exportName); - if (importName === undefined) { - exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? ts.getUniqueName(exportName, sourceFile) : exportName); +function doChangeNamespaceToNamed(sourceFile: ts.SourceFile, checker: ts.TypeChecker, changes: ts.textChanges.ChangeTracker, toConvert: ts.NamespaceImport, allowSyntheticDefaultImports: boolean): void { + let usedAsNamespaceOrDefault = false; + + const nodesToReplace: (ts.PropertyAccessExpression | ts.QualifiedName)[] = []; + const conflictingNames = new ts.Map(); + ts.FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { + if (!ts.isPropertyAccessOrQualifiedName(id.parent)) { + usedAsNamespaceOrDefault = true; + } + else { + const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text; + if (checker.resolveName(exportName, id, ts.SymbolFlags.All, /*excludeGlobals*/ true)) { + conflictingNames.set(exportName, true); } - changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, ts.factory.createIdentifier(importName)); + ts.Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id"); + nodesToReplace.push(id.parent); } + }); - const importSpecifiers: ts.ImportSpecifier[] = []; - exportNameToImportName.forEach((name, propertyName) => { - importSpecifiers.push(ts.factory.createImportSpecifier(/*isTypeOnly*/ false, name === propertyName ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name))); - }); + // We may need to change `mod.x` to `_x` to avoid a name conflict. + const exportNameToImportName = new ts.Map(); - 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 ? ts.factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); + for (const propertyAccessOrQualifiedName of nodesToReplace) { + const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text; + let importName = exportNameToImportName.get(exportName); + if (importName === undefined) { + exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? ts.getUniqueName(exportName, sourceFile) : exportName); } + changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, ts.factory.createIdentifier(importName)); } - function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: ts.PropertyAccessExpression | ts.QualifiedName) { - return ts.isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right; - } + const importSpecifiers: ts.ImportSpecifier[] = []; + exportNameToImportName.forEach((name, propertyName) => { + importSpecifiers.push(ts.factory.createImportSpecifier(/*isTypeOnly*/ false, name === propertyName ? undefined : ts.factory.createIdentifier(propertyName), ts.factory.createIdentifier(name))); + }); - function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: ts.PropertyAccessExpression | ts.QualifiedName) { - return ts.isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left; + 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 ? ts.factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); + } +} - export function doChangeNamedToNamespaceOrDefault(sourceFile: ts.SourceFile, program: ts.Program, changes: ts.textChanges.ChangeTracker, toConvert: ts.NamedImports, shouldUseDefault = getShouldUseDefault(program, toConvert.parent)): void { - const checker = program.getTypeChecker(); - const importDecl = toConvert.parent.parent; - const { moduleSpecifier } = importDecl; +function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: ts.PropertyAccessExpression | ts.QualifiedName) { + return ts.isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right; +} - const toConvertSymbols: ts.Set = new ts.Set(); - toConvert.elements.forEach(namedImport => { - const symbol = checker.getSymbolAtLocation(namedImport.name); - if (symbol) { - toConvertSymbols.add(symbol); - } - }); - const preferredName = moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ? ts.codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ts.ScriptTarget.ESNext) : "module"; - function hasNamespaceNameConflict(namedImport: ts.ImportSpecifier): boolean { - // We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict. - // A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope. - // We are going to use the namespace name in the scopes the named imports being refactored are referenced, - // so we look for conflicts by looking at every reference to those named imports. - return !!ts.FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => { - const symbol = checker.resolveName(preferredName, id, ts.SymbolFlags.All, /*excludeGlobals*/ true); - if (symbol) { // There already is a symbol with the same name as the preferred namespace name. - if (toConvertSymbols.has(symbol)) { // `preferredName` resolves to a symbol for one of the named import references we are going to transform into namespace import references... - return ts.isExportSpecifier(id.parent); // ...but if this reference is an export specifier, it will not be transformed, so it is a conflict; otherwise, it will be renamed and is not a conflict. - } - return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict. - } - return false; // There is no symbol with the same name as the preferred namespace name, so no conflict. - }); - } - const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict); - const namespaceImportName = namespaceNameConflicts ? ts.getUniqueName(preferredName, sourceFile) : preferredName; - - // Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics. - // More specifically, those are named imports that appear in named exports in the original code, e.g. `a` in `import { a } from "m"; export { a }`. - const neededNamedImports: ts.Set = new ts.Set(); - - for (const element of toConvert.elements) { - const propertyName = (element.propertyName || element.name).text; - ts.FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { - const access = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(namespaceImportName), propertyName); - if (ts.isShorthandPropertyAssignment(id.parent)) { - changes.replaceNode(sourceFile, id.parent, ts.factory.createPropertyAssignment(id.text, access)); - } - else if (ts.isExportSpecifier(id.parent)) { - neededNamedImports.add(element); - } - else { - changes.replaceNode(sourceFile, id, access); - } - }); - } +function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: ts.PropertyAccessExpression | ts.QualifiedName) { + return ts.isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left; +} + +export function doChangeNamedToNamespaceOrDefault(sourceFile: ts.SourceFile, program: ts.Program, changes: ts.textChanges.ChangeTracker, toConvert: ts.NamedImports, shouldUseDefault = getShouldUseDefault(program, toConvert.parent)): void { + const checker = program.getTypeChecker(); + const importDecl = toConvert.parent.parent; + const { moduleSpecifier } = importDecl; - changes.replaceNode(sourceFile, toConvert, shouldUseDefault - ? ts.factory.createIdentifier(namespaceImportName) - : ts.factory.createNamespaceImport(ts.factory.createIdentifier(namespaceImportName))); - if (neededNamedImports.size) { - const newNamedImports: ts.ImportSpecifier[] = ts.arrayFrom(neededNamedImports.values()).map(element => ts.factory.createImportSpecifier(element.isTypeOnly, element.propertyName && ts.factory.createIdentifier(element.propertyName.text), ts.factory.createIdentifier(element.name.text))); - changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports)); + const toConvertSymbols: ts.Set = new ts.Set(); + toConvert.elements.forEach(namedImport => { + const symbol = checker.getSymbolAtLocation(namedImport.name); + if (symbol) { + toConvertSymbols.add(symbol); } + }); + const preferredName = moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ? ts.codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ts.ScriptTarget.ESNext) : "module"; + function hasNamespaceNameConflict(namedImport: ts.ImportSpecifier): boolean { + // We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict. + // A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope. + // We are going to use the namespace name in the scopes the named imports being refactored are referenced, + // so we look for conflicts by looking at every reference to those named imports. + return !!ts.FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => { + const symbol = checker.resolveName(preferredName, id, ts.SymbolFlags.All, /*excludeGlobals*/ true); + if (symbol) { // There already is a symbol with the same name as the preferred namespace name. + if (toConvertSymbols.has(symbol)) { // `preferredName` resolves to a symbol for one of the named import references we are going to transform into namespace import references... + return ts.isExportSpecifier(id.parent); // ...but if this reference is an export specifier, it will not be transformed, so it is a conflict; otherwise, it will be renamed and is not a conflict. + } + return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict. + } + return false; // There is no symbol with the same name as the preferred namespace name, so no conflict. + }); } - - function isExportEqualsModule(moduleSpecifier: ts.Expression, checker: ts.TypeChecker) { - const externalModule = checker.resolveExternalModuleName(moduleSpecifier); - if (!externalModule) - return false; - const exportEquals = checker.resolveExternalModuleSymbol(externalModule); - return externalModule !== exportEquals; + const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict); + const namespaceImportName = namespaceNameConflicts ? ts.getUniqueName(preferredName, sourceFile) : preferredName; + + // Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics. + // More specifically, those are named imports that appear in named exports in the original code, e.g. `a` in `import { a } from "m"; export { a }`. + const neededNamedImports: ts.Set = new ts.Set(); + + for (const element of toConvert.elements) { + const propertyName = (element.propertyName || element.name).text; + ts.FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { + const access = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(namespaceImportName), propertyName); + if (ts.isShorthandPropertyAssignment(id.parent)) { + changes.replaceNode(sourceFile, id.parent, ts.factory.createPropertyAssignment(id.text, access)); + } + else if (ts.isExportSpecifier(id.parent)) { + neededNamedImports.add(element); + } + else { + changes.replaceNode(sourceFile, id, access); + } + }); } - function updateImport(old: ts.ImportDeclaration, defaultImportName: ts.Identifier | undefined, elements: readonly ts.ImportSpecifier[] | undefined): ts.ImportDeclaration { - return ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? ts.factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined); + changes.replaceNode(sourceFile, toConvert, shouldUseDefault + ? ts.factory.createIdentifier(namespaceImportName) + : ts.factory.createNamespaceImport(ts.factory.createIdentifier(namespaceImportName))); + if (neededNamedImports.size) { + const newNamedImports: ts.ImportSpecifier[] = ts.arrayFrom(neededNamedImports.values()).map(element => ts.factory.createImportSpecifier(element.isTypeOnly, element.propertyName && ts.factory.createIdentifier(element.propertyName.text), ts.factory.createIdentifier(element.name.text))); + changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports)); } } + +function isExportEqualsModule(moduleSpecifier: ts.Expression, checker: ts.TypeChecker) { + const externalModule = checker.resolveExternalModuleName(moduleSpecifier); + if (!externalModule) + return false; + const exportEquals = checker.resolveExternalModuleSymbol(externalModule); + return externalModule !== exportEquals; +} + +function updateImport(old: ts.ImportDeclaration, defaultImportName: ts.Identifier | undefined, elements: readonly ts.ImportSpecifier[] | undefined): ts.ImportDeclaration { + return ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? ts.factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined); +} +} diff --git a/src/services/refactors/convertOverloadListToSingleSignature.ts b/src/services/refactors/convertOverloadListToSingleSignature.ts index f1ba62b54f598..6fb337deddf1f 100644 --- a/src/services/refactors/convertOverloadListToSingleSignature.ts +++ b/src/services/refactors/convertOverloadListToSingleSignature.ts @@ -1,173 +1,173 @@ /* @internal */ namespace ts.refactor.addOrRemoveBracesToArrowFunction { - const refactorName = "Convert overload list to single signature"; - const refactorDescription = ts.Diagnostics.Convert_overload_list_to_single_signature.message; +const refactorName = "Convert overload list to single signature"; +const refactorDescription = ts.Diagnostics.Convert_overload_list_to_single_signature.message; - const functionOverloadAction = { - name: refactorName, - description: refactorDescription, - kind: "refactor.rewrite.function.overloadList", - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [functionOverloadAction.kind], - getEditsForAction: getRefactorEditsToConvertOverloadsToOneSignature, - getAvailableActions: getRefactorActionsToConvertOverloadsToOneSignature - }); +const functionOverloadAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.function.overloadList", +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [functionOverloadAction.kind], + getEditsForAction: getRefactorEditsToConvertOverloadsToOneSignature, + getAvailableActions: getRefactorActionsToConvertOverloadsToOneSignature +}); - function getRefactorActionsToConvertOverloadsToOneSignature(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const { file, startPosition, program } = context; - const info = getConvertableOverloadListAtPosition(file, startPosition, program); - if (!info) - return ts.emptyArray; +function getRefactorActionsToConvertOverloadsToOneSignature(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const { file, startPosition, program } = context; + const info = getConvertableOverloadListAtPosition(file, startPosition, program); + if (!info) + return ts.emptyArray; - return [{ - name: refactorName, - description: refactorDescription, - actions: [functionOverloadAction] - }]; - } + return [{ + name: refactorName, + description: refactorDescription, + actions: [functionOverloadAction] + }]; +} - function getRefactorEditsToConvertOverloadsToOneSignature(context: ts.RefactorContext): ts.RefactorEditInfo | undefined { - const { file, startPosition, program } = context; - const signatureDecls = getConvertableOverloadListAtPosition(file, startPosition, program); - if (!signatureDecls) - return undefined; +function getRefactorEditsToConvertOverloadsToOneSignature(context: ts.RefactorContext): ts.RefactorEditInfo | undefined { + const { file, startPosition, program } = context; + const signatureDecls = getConvertableOverloadListAtPosition(file, startPosition, program); + if (!signatureDecls) + return undefined; - const checker = program.getTypeChecker(); + const checker = program.getTypeChecker(); - const lastDeclaration = signatureDecls[signatureDecls.length - 1]; - let updated = lastDeclaration; - switch (lastDeclaration.kind) { - case ts.SyntaxKind.MethodSignature: { - updated = ts.factory.updateMethodSignature(lastDeclaration, lastDeclaration.modifiers, lastDeclaration.name, lastDeclaration.questionToken, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); - break; - } - case ts.SyntaxKind.MethodDeclaration: { - updated = ts.factory.updateMethodDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, lastDeclaration.asteriskToken, lastDeclaration.name, lastDeclaration.questionToken, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type, lastDeclaration.body); - break; - } - case ts.SyntaxKind.CallSignature: { - updated = ts.factory.updateCallSignature(lastDeclaration, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); - break; - } - case ts.SyntaxKind.Constructor: { - updated = ts.factory.updateConstructorDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.body); - break; - } - case ts.SyntaxKind.ConstructSignature: { - updated = ts.factory.updateConstructSignature(lastDeclaration, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); - break; - } - case ts.SyntaxKind.FunctionDeclaration: { - updated = ts.factory.updateFunctionDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, lastDeclaration.asteriskToken, lastDeclaration.name, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type, lastDeclaration.body); - break; - } - default: return ts.Debug.failBadSyntaxKind(lastDeclaration, "Unhandled signature kind in overload list conversion refactoring"); + const lastDeclaration = signatureDecls[signatureDecls.length - 1]; + let updated = lastDeclaration; + switch (lastDeclaration.kind) { + case ts.SyntaxKind.MethodSignature: { + updated = ts.factory.updateMethodSignature(lastDeclaration, lastDeclaration.modifiers, lastDeclaration.name, lastDeclaration.questionToken, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); + break; } - - if (updated === lastDeclaration) { - return; // No edits to apply, do nothing + case ts.SyntaxKind.MethodDeclaration: { + updated = ts.factory.updateMethodDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, lastDeclaration.asteriskToken, lastDeclaration.name, lastDeclaration.questionToken, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type, lastDeclaration.body); + break; + } + case ts.SyntaxKind.CallSignature: { + updated = ts.factory.updateCallSignature(lastDeclaration, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); + break; + } + case ts.SyntaxKind.Constructor: { + updated = ts.factory.updateConstructorDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.body); + break; + } + case ts.SyntaxKind.ConstructSignature: { + updated = ts.factory.updateConstructSignature(lastDeclaration, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type); + break; } + case ts.SyntaxKind.FunctionDeclaration: { + updated = ts.factory.updateFunctionDeclaration(lastDeclaration, lastDeclaration.decorators, lastDeclaration.modifiers, lastDeclaration.asteriskToken, lastDeclaration.name, lastDeclaration.typeParameters, getNewParametersForCombinedSignature(signatureDecls), lastDeclaration.type, lastDeclaration.body); + break; + } + default: return ts.Debug.failBadSyntaxKind(lastDeclaration, "Unhandled signature kind in overload list conversion refactoring"); + } - const edits = ts.textChanges.ChangeTracker.with(context, t => { - t.replaceNodeRange(file, signatureDecls[0], signatureDecls[signatureDecls.length - 1], updated); - }); + if (updated === lastDeclaration) { + return; // No edits to apply, do nothing + } - return { renameFilename: undefined, renameLocation: undefined, edits }; + const edits = ts.textChanges.ChangeTracker.with(context, t => { + t.replaceNodeRange(file, signatureDecls[0], signatureDecls[signatureDecls.length - 1], updated); + }); - function getNewParametersForCombinedSignature(signatureDeclarations: (ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration)[]): ts.NodeArray { - const lastSig = signatureDeclarations[signatureDeclarations.length - 1]; - if (ts.isFunctionLikeDeclaration(lastSig) && lastSig.body) { - // Trim away implementation signature arguments (they should already be compatible with overloads, but are likely less precise to guarantee compatability with the overloads) - signatureDeclarations = signatureDeclarations.slice(0, signatureDeclarations.length - 1); - } - return ts.factory.createNodeArray([ - ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), "args", - /*questionToken*/ undefined, ts.factory.createUnionTypeNode(ts.map(signatureDeclarations, convertSignatureParametersToTuple))) - ]); - } + return { renameFilename: undefined, renameLocation: undefined, edits }; - function convertSignatureParametersToTuple(decl: ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration): ts.TupleTypeNode { - const members = ts.map(decl.parameters, convertParameterToNamedTupleMember); - return ts.setEmitFlags(ts.factory.createTupleTypeNode(members), ts.some(members, m => !!ts.length(ts.getSyntheticLeadingComments(m))) ? ts.EmitFlags.None : ts.EmitFlags.SingleLine); + function getNewParametersForCombinedSignature(signatureDeclarations: (ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration)[]): ts.NodeArray { + const lastSig = signatureDeclarations[signatureDeclarations.length - 1]; + if (ts.isFunctionLikeDeclaration(lastSig) && lastSig.body) { + // Trim away implementation signature arguments (they should already be compatible with overloads, but are likely less precise to guarantee compatability with the overloads) + signatureDeclarations = signatureDeclarations.slice(0, signatureDeclarations.length - 1); } - function convertParameterToNamedTupleMember(p: ts.ParameterDeclaration): ts.NamedTupleMember { - ts.Debug.assert(ts.isIdentifier(p.name)); // This is checked during refactoring applicability checking - const result = ts.setTextRange(ts.factory.createNamedTupleMember(p.dotDotDotToken, p.name, p.questionToken, p.type || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), p); - const parameterDocComment = p.symbol && p.symbol.getDocumentationComment(checker); - if (parameterDocComment) { - const newComment = ts.displayPartsToString(parameterDocComment); - if (newComment.length) { - ts.setSyntheticLeadingComments(result, [{ - text: `* + return ts.factory.createNodeArray([ + ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), "args", + /*questionToken*/ undefined, ts.factory.createUnionTypeNode(ts.map(signatureDeclarations, convertSignatureParametersToTuple))) + ]); + } + + function convertSignatureParametersToTuple(decl: ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration): ts.TupleTypeNode { + const members = ts.map(decl.parameters, convertParameterToNamedTupleMember); + return ts.setEmitFlags(ts.factory.createTupleTypeNode(members), ts.some(members, m => !!ts.length(ts.getSyntheticLeadingComments(m))) ? ts.EmitFlags.None : ts.EmitFlags.SingleLine); + } + function convertParameterToNamedTupleMember(p: ts.ParameterDeclaration): ts.NamedTupleMember { + ts.Debug.assert(ts.isIdentifier(p.name)); // This is checked during refactoring applicability checking + const result = ts.setTextRange(ts.factory.createNamedTupleMember(p.dotDotDotToken, p.name, p.questionToken, p.type || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), p); + const parameterDocComment = p.symbol && p.symbol.getDocumentationComment(checker); + if (parameterDocComment) { + const newComment = ts.displayPartsToString(parameterDocComment); + if (newComment.length) { + ts.setSyntheticLeadingComments(result, [{ + text: `* ${newComment.split("\n").map(c => ` * ${c}`).join("\n")} `, - kind: ts.SyntaxKind.MultiLineCommentTrivia, - pos: -1, - end: -1, - hasTrailingNewLine: true, - hasLeadingNewline: true, - }]); - } + kind: ts.SyntaxKind.MultiLineCommentTrivia, + pos: -1, + end: -1, + hasTrailingNewLine: true, + hasLeadingNewline: true, + }]); } - return result; } - + return result; } - function isConvertableSignatureDeclaration(d: ts.Node): d is ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration { - switch (d.kind) { - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.FunctionDeclaration: - return true; - } - return false; - } +} - function getConvertableOverloadListAtPosition(file: ts.SourceFile, startPosition: number, program: ts.Program) { - const node = ts.getTokenAtPosition(file, startPosition); - const containingDecl = ts.findAncestor(node, isConvertableSignatureDeclaration); - if (!containingDecl) { - return; - } - const checker = program.getTypeChecker(); - const signatureSymbol = containingDecl.symbol; - if (!signatureSymbol) { - return; - } - const decls = signatureSymbol.declarations; - if (ts.length(decls) <= 1) { - return; - } - if (!ts.every(decls, d => ts.getSourceFileOfNode(d) === file)) { - return; - } - if (!isConvertableSignatureDeclaration(decls![0])) { - return; - } - const kindOne = decls![0].kind; - if (!ts.every(decls, d => d.kind === kindOne)) { - return; - } - const signatureDecls = decls as (ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration)[]; - if (ts.some(signatureDecls, d => !!d.typeParameters || ts.some(d.parameters, p => !!p.decorators || !!p.modifiers || !ts.isIdentifier(p.name)))) { - return; - } - const signatures = ts.mapDefined(signatureDecls, d => checker.getSignatureFromDeclaration(d)); - if (ts.length(signatures) !== ts.length(decls)) { - return; - } - const returnOne = checker.getReturnTypeOfSignature(signatures[0]); - if (!ts.every(signatures, s => checker.getReturnTypeOfSignature(s) === returnOne)) { - return; - } +function isConvertableSignatureDeclaration(d: ts.Node): d is ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration { + switch (d.kind) { + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.FunctionDeclaration: + return true; + } + return false; +} - return signatureDecls; +function getConvertableOverloadListAtPosition(file: ts.SourceFile, startPosition: number, program: ts.Program) { + const node = ts.getTokenAtPosition(file, startPosition); + const containingDecl = ts.findAncestor(node, isConvertableSignatureDeclaration); + if (!containingDecl) { + return; + } + const checker = program.getTypeChecker(); + const signatureSymbol = containingDecl.symbol; + if (!signatureSymbol) { + return; + } + const decls = signatureSymbol.declarations; + if (ts.length(decls) <= 1) { + return; + } + if (!ts.every(decls, d => ts.getSourceFileOfNode(d) === file)) { + return; + } + if (!isConvertableSignatureDeclaration(decls![0])) { + return; } + const kindOne = decls![0].kind; + if (!ts.every(decls, d => d.kind === kindOne)) { + return; + } + const signatureDecls = decls as (ts.MethodSignature | ts.MethodDeclaration | ts.CallSignatureDeclaration | ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionDeclaration)[]; + if (ts.some(signatureDecls, d => !!d.typeParameters || ts.some(d.parameters, p => !!p.decorators || !!p.modifiers || !ts.isIdentifier(p.name)))) { + return; + } + const signatures = ts.mapDefined(signatureDecls, d => checker.getSignatureFromDeclaration(d)); + if (ts.length(signatures) !== ts.length(decls)) { + return; + } + const returnOne = checker.getReturnTypeOfSignature(signatures[0]); + if (!ts.every(signatures, s => checker.getReturnTypeOfSignature(s) === returnOne)) { + return; + } + + return signatureDecls; +} } diff --git a/src/services/refactors/convertParamsToDestructuredObject.ts b/src/services/refactors/convertParamsToDestructuredObject.ts index 8e7db35baacca..ec0ec58ee3097 100644 --- a/src/services/refactors/convertParamsToDestructuredObject.ts +++ b/src/services/refactors/convertParamsToDestructuredObject.ts @@ -1,642 +1,642 @@ /* @internal */ namespace ts.refactor.convertParamsToDestructuredObject { - const refactorName = "Convert parameters to destructured object"; - const minimumParameterLength = 1; - const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_parameters_to_destructured_object); - - const toDestructuredAction = { +const refactorName = "Convert parameters to destructured object"; +const minimumParameterLength = 1; +const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_parameters_to_destructured_object); + +const toDestructuredAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.parameters.toDestructured" +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [toDestructuredAction.kind], + getEditsForAction: getRefactorEditsToConvertParametersToDestructuredObject, + getAvailableActions: getRefactorActionsToConvertParametersToDestructuredObject +}); + +function getRefactorActionsToConvertParametersToDestructuredObject(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const { file, startPosition } = context; + const isJSFile = ts.isSourceFileJS(file); + if (isJSFile) + return ts.emptyArray; // TODO: GH#30113 + const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker()); + if (!functionDeclaration) + return ts.emptyArray; + + return [{ name: refactorName, description: refactorDescription, - kind: "refactor.rewrite.parameters.toDestructured" - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [toDestructuredAction.kind], - getEditsForAction: getRefactorEditsToConvertParametersToDestructuredObject, - getAvailableActions: getRefactorActionsToConvertParametersToDestructuredObject - }); - - function getRefactorActionsToConvertParametersToDestructuredObject(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const { file, startPosition } = context; - const isJSFile = ts.isSourceFileJS(file); - if (isJSFile) - return ts.emptyArray; // TODO: GH#30113 - const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker()); - if (!functionDeclaration) - return ts.emptyArray; - - return [{ - name: refactorName, - description: refactorDescription, - actions: [toDestructuredAction] - }]; - } + actions: [toDestructuredAction] + }]; +} - function getRefactorEditsToConvertParametersToDestructuredObject(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - ts.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 = ts.textChanges.ChangeTracker.with(context, t => doChange(file, program, host, t, functionDeclaration, groupedReferences)); - return { renameFilename: undefined, renameLocation: undefined, edits }; - } +function getRefactorEditsToConvertParametersToDestructuredObject(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + ts.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; - return { edits: [] }; // TODO: GH#30113 + const groupedReferences = getGroupedReferences(functionDeclaration, program, cancellationToken); + if (groupedReferences.valid) { + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(file, program, host, t, functionDeclaration, groupedReferences)); + return { renameFilename: undefined, renameLocation: undefined, edits }; } - function doChange(sourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, changes: ts.textChanges.ChangeTracker, functionDeclaration: ValidFunctionDeclaration, groupedReferences: GroupedReferences): void { - const signature = groupedReferences.signature; - const newFunctionDeclarationParams = ts.map(createNewParameters(functionDeclaration, program, host), param => ts.getSynthesizedDeepClone(param)); + return { edits: [] }; // TODO: GH#30113 +} - if (signature) { - const newSignatureParams = ts.map(createNewParameters(signature, program, host), param => ts.getSynthesizedDeepClone(param)); - replaceParameters(signature, newSignatureParams); - } - replaceParameters(functionDeclaration, newFunctionDeclarationParams); +function doChange(sourceFile: ts.SourceFile, program: ts.Program, host: ts.LanguageServiceHost, changes: ts.textChanges.ChangeTracker, functionDeclaration: ValidFunctionDeclaration, groupedReferences: GroupedReferences): void { + const signature = groupedReferences.signature; + const newFunctionDeclarationParams = ts.map(createNewParameters(functionDeclaration, program, host), param => ts.getSynthesizedDeepClone(param)); - const functionCalls = ts.sortAndDeduplicate(groupedReferences.functionCalls, /*comparer*/ (a, b) => ts.compareValues(a.pos, b.pos)); - for (const call of functionCalls) { - if (call.arguments && call.arguments.length) { - const newArgument = ts.getSynthesizedDeepClone(createNewArgument(functionDeclaration, call.arguments), /*includeTrivia*/ true); - changes.replaceNodeRange(ts.getSourceFileOfNode(call), ts.first(call.arguments), ts.last(call.arguments), newArgument, { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.IncludeAll, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include }); - } - } - function replaceParameters(declarationOrSignature: ValidFunctionDeclaration | ValidMethodSignature, parameterDeclarations: ts.ParameterDeclaration[]) { - changes.replaceNodeRangeWithNodes(sourceFile, ts.first(declarationOrSignature.parameters), ts.last(declarationOrSignature.parameters), parameterDeclarations, { - joiner: ", ", - // indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter - indentation: 0, - leadingTriviaOption: ts.textChanges.LeadingTriviaOption.IncludeAll, - trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include - }); + if (signature) { + const newSignatureParams = ts.map(createNewParameters(signature, program, host), param => ts.getSynthesizedDeepClone(param)); + replaceParameters(signature, newSignatureParams); + } + replaceParameters(functionDeclaration, newFunctionDeclarationParams); + + const functionCalls = ts.sortAndDeduplicate(groupedReferences.functionCalls, /*comparer*/ (a, b) => ts.compareValues(a.pos, b.pos)); + for (const call of functionCalls) { + if (call.arguments && call.arguments.length) { + const newArgument = ts.getSynthesizedDeepClone(createNewArgument(functionDeclaration, call.arguments), /*includeTrivia*/ true); + changes.replaceNodeRange(ts.getSourceFileOfNode(call), ts.first(call.arguments), ts.last(call.arguments), newArgument, { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.IncludeAll, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include }); } } + function replaceParameters(declarationOrSignature: ValidFunctionDeclaration | ValidMethodSignature, parameterDeclarations: ts.ParameterDeclaration[]) { + changes.replaceNodeRangeWithNodes(sourceFile, ts.first(declarationOrSignature.parameters), ts.last(declarationOrSignature.parameters), parameterDeclarations, { + joiner: ", ", + // indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter + indentation: 0, + leadingTriviaOption: ts.textChanges.LeadingTriviaOption.IncludeAll, + trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Include + }); + } +} - function getGroupedReferences(functionDeclaration: ValidFunctionDeclaration, program: ts.Program, cancellationToken: ts.CancellationToken): GroupedReferences { - const functionNames = getFunctionNames(functionDeclaration); - const classNames = ts.isConstructorDeclaration(functionDeclaration) ? getClassNames(functionDeclaration) : []; - const names = ts.deduplicate([...functionNames, ...classNames], ts.equateValues); - const checker = program.getTypeChecker(); +function getGroupedReferences(functionDeclaration: ValidFunctionDeclaration, program: ts.Program, cancellationToken: ts.CancellationToken): GroupedReferences { + const functionNames = getFunctionNames(functionDeclaration); + const classNames = ts.isConstructorDeclaration(functionDeclaration) ? getClassNames(functionDeclaration) : []; + const names = ts.deduplicate([...functionNames, ...classNames], ts.equateValues); + const checker = program.getTypeChecker(); - const references = ts.flatMap(names, /*mapfn*/ /*mapfn*/ name => ts.FindAllReferences.getReferenceEntriesForNode(-1, name, program, program.getSourceFiles(), cancellationToken)); - const groupedReferences = groupReferences(references); + const references = ts.flatMap(names, /*mapfn*/ /*mapfn*/ name => ts.FindAllReferences.getReferenceEntriesForNode(-1, name, program, program.getSourceFiles(), cancellationToken)); + const groupedReferences = groupReferences(references); - if (!ts.every(groupedReferences.declarations, /*callback*/ /*callback*/ decl => ts.contains(names, decl))) { - groupedReferences.valid = false; - } + if (!ts.every(groupedReferences.declarations, /*callback*/ /*callback*/ decl => ts.contains(names, decl))) { + groupedReferences.valid = false; + } - return groupedReferences; + return groupedReferences; + + function groupReferences(referenceEntries: readonly ts.FindAllReferences.Entry[]): GroupedReferences { + const classReferences: ClassReferences = { accessExpressions: [], typeUsages: [] }; + const groupedReferences: GroupedReferences = { functionCalls: [], declarations: [], classReferences, valid: true }; + const functionSymbols = ts.map(functionNames, getSymbolTargetAtLocation); + const classSymbols = ts.map(classNames, getSymbolTargetAtLocation); + const isConstructor = ts.isConstructorDeclaration(functionDeclaration); + const contextualSymbols = ts.map(functionNames, name => getSymbolForContextualType(name, checker)); + + for (const entry of referenceEntries) { + if (entry.kind === ts.FindAllReferences.EntryKind.Span) { + groupedReferences.valid = false; + continue; + } - function groupReferences(referenceEntries: readonly ts.FindAllReferences.Entry[]): GroupedReferences { - const classReferences: ClassReferences = { accessExpressions: [], typeUsages: [] }; - const groupedReferences: GroupedReferences = { functionCalls: [], declarations: [], classReferences, valid: true }; - const functionSymbols = ts.map(functionNames, getSymbolTargetAtLocation); - const classSymbols = ts.map(classNames, getSymbolTargetAtLocation); - const isConstructor = ts.isConstructorDeclaration(functionDeclaration); - const contextualSymbols = ts.map(functionNames, name => getSymbolForContextualType(name, checker)); - - for (const entry of referenceEntries) { - if (entry.kind === ts.FindAllReferences.EntryKind.Span) { - groupedReferences.valid = false; + /* Declarations in object literals may be implementations of method signatures which have a different symbol from the declaration + For example: + interface IFoo { m(a: number): void } + const foo: IFoo = { m(a: number): void {} } + In these cases we get the symbol for the signature from the contextual type. + */ + if (ts.contains(contextualSymbols, getSymbolTargetAtLocation(entry.node))) { + if (isValidMethodSignature(entry.node.parent)) { + groupedReferences.signature = entry.node.parent; + continue; + } + const call = entryToFunctionCall(entry); + if (call) { + groupedReferences.functionCalls.push(call); continue; } + } - /* Declarations in object literals may be implementations of method signatures which have a different symbol from the declaration - For example: - interface IFoo { m(a: number): void } - const foo: IFoo = { m(a: number): void {} } - In these cases we get the symbol for the signature from the contextual type. - */ - if (ts.contains(contextualSymbols, getSymbolTargetAtLocation(entry.node))) { - if (isValidMethodSignature(entry.node.parent)) { - groupedReferences.signature = entry.node.parent; - continue; - } - const call = entryToFunctionCall(entry); - if (call) { - groupedReferences.functionCalls.push(call); - continue; - } + const contextualSymbol = getSymbolForContextualType(entry.node, checker); + if (contextualSymbol && ts.contains(contextualSymbols, contextualSymbol)) { + const decl = entryToDeclaration(entry); + if (decl) { + groupedReferences.declarations.push(decl); + continue; } + } - const contextualSymbol = getSymbolForContextualType(entry.node, checker); - if (contextualSymbol && ts.contains(contextualSymbols, contextualSymbol)) { - const decl = entryToDeclaration(entry); - if (decl) { - groupedReferences.declarations.push(decl); - 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 (ts.contains(functionSymbols, getSymbolTargetAtLocation(entry.node)) || ts.isNewExpressionTarget(entry.node)) { + const importOrExportReference = entryToImportOrExport(entry); + if (importOrExportReference) { + continue; + } + const decl = entryToDeclaration(entry); + if (decl) { + groupedReferences.declarations.push(decl); + 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 (ts.contains(functionSymbols, getSymbolTargetAtLocation(entry.node)) || ts.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; + } + } + // if the refactored function is a constructor, we must also check if the references to its class are valid + if (isConstructor && ts.contains(classSymbols, getSymbolTargetAtLocation(entry.node))) { + const importOrExportReference = entryToImportOrExport(entry); + if (importOrExportReference) { + 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 && ts.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; + } - const accessExpression = entryToAccessExpression(entry); - if (accessExpression) { - classReferences.accessExpressions.push(accessExpression); + // 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 (ts.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 (ts.isClassDeclaration(functionDeclaration.parent)) { - const type = entryToType(entry); - if (type) { - classReferences.typeUsages.push(type); - continue; - } - } } - groupedReferences.valid = false; } - - return groupedReferences; + groupedReferences.valid = false; } - function getSymbolTargetAtLocation(node: ts.Node) { - const symbol = checker.getSymbolAtLocation(node); - return symbol && ts.getSymbolTarget(symbol, checker); - } + return groupedReferences; } - /** - * Gets the symbol for the contextual type of the node if it is not a union or intersection. - */ - function getSymbolForContextualType(node: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { - const element = ts.getContainingObjectLiteralElement(node); - if (element) { - const contextualType = checker.getContextualTypeForObjectLiteralElement(element as ts.ObjectLiteralElementLike); - const symbol = contextualType?.getSymbol(); - if (symbol && !(ts.getCheckFlags(symbol) & ts.CheckFlags.Synthetic)) { - return symbol; - } + function getSymbolTargetAtLocation(node: ts.Node) { + const symbol = checker.getSymbolAtLocation(node); + return symbol && ts.getSymbolTarget(symbol, checker); + } +} + +/** + * Gets the symbol for the contextual type of the node if it is not a union or intersection. + */ +function getSymbolForContextualType(node: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { + const element = ts.getContainingObjectLiteralElement(node); + if (element) { + const contextualType = checker.getContextualTypeForObjectLiteralElement(element as ts.ObjectLiteralElementLike); + const symbol = contextualType?.getSymbol(); + if (symbol && !(ts.getCheckFlags(symbol) & ts.CheckFlags.Synthetic)) { + return symbol; } } +} - function entryToImportOrExport(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { - const node = entry.node; +function entryToImportOrExport(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { + const node = entry.node; - if (ts.isImportSpecifier(node.parent) - || ts.isImportClause(node.parent) - || ts.isImportEqualsDeclaration(node.parent) - || ts.isNamespaceImport(node.parent)) { - return node; - } + if (ts.isImportSpecifier(node.parent) + || ts.isImportClause(node.parent) + || ts.isImportEqualsDeclaration(node.parent) + || ts.isNamespaceImport(node.parent)) { + return node; + } - if (ts.isExportSpecifier(node.parent) || ts.isExportAssignment(node.parent)) { - return node; - } - return undefined; + if (ts.isExportSpecifier(node.parent) || ts.isExportAssignment(node.parent)) { + return node; } + return undefined; +} - function entryToDeclaration(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { - if (ts.isDeclaration(entry.node.parent)) { - return entry.node; - } - return undefined; +function entryToDeclaration(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { + if (ts.isDeclaration(entry.node.parent)) { + return entry.node; } + return undefined; +} - function entryToFunctionCall(entry: ts.FindAllReferences.NodeEntry): ts.CallExpression | ts.NewExpression | undefined { - if (entry.node.parent) { - const functionReference = entry.node; - const parent = functionReference.parent; - switch (parent.kind) { - // foo(...) or super(...) or new Foo(...) - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - const callOrNewExpression = ts.tryCast(parent, ts.isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === functionReference) { +function entryToFunctionCall(entry: ts.FindAllReferences.NodeEntry): ts.CallExpression | ts.NewExpression | undefined { + if (entry.node.parent) { + const functionReference = entry.node; + const parent = functionReference.parent; + switch (parent.kind) { + // foo(...) or super(...) or new Foo(...) + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + const callOrNewExpression = ts.tryCast(parent, ts.isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === functionReference) { + return callOrNewExpression; + } + break; + // x.foo(...) + case ts.SyntaxKind.PropertyAccessExpression: + const propertyAccessExpression = ts.tryCast(parent, ts.isPropertyAccessExpression); + if (propertyAccessExpression && propertyAccessExpression.parent && propertyAccessExpression.name === functionReference) { + const callOrNewExpression = ts.tryCast(propertyAccessExpression.parent, ts.isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === propertyAccessExpression) { return callOrNewExpression; } - break; - // x.foo(...) - case ts.SyntaxKind.PropertyAccessExpression: - const propertyAccessExpression = ts.tryCast(parent, ts.isPropertyAccessExpression); - if (propertyAccessExpression && propertyAccessExpression.parent && propertyAccessExpression.name === functionReference) { - const callOrNewExpression = ts.tryCast(propertyAccessExpression.parent, ts.isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === propertyAccessExpression) { - return callOrNewExpression; - } - } - break; - // x["foo"](...) - case ts.SyntaxKind.ElementAccessExpression: - const elementAccessExpression = ts.tryCast(parent, ts.isElementAccessExpression); - if (elementAccessExpression && elementAccessExpression.parent && elementAccessExpression.argumentExpression === functionReference) { - const callOrNewExpression = ts.tryCast(elementAccessExpression.parent, ts.isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === elementAccessExpression) { - return callOrNewExpression; - } + } + break; + // x["foo"](...) + case ts.SyntaxKind.ElementAccessExpression: + const elementAccessExpression = ts.tryCast(parent, ts.isElementAccessExpression); + if (elementAccessExpression && elementAccessExpression.parent && elementAccessExpression.argumentExpression === functionReference) { + const callOrNewExpression = ts.tryCast(elementAccessExpression.parent, ts.isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === elementAccessExpression) { + return callOrNewExpression; } - break; - } + } + break; } - return undefined; } + return undefined; +} - function entryToAccessExpression(entry: ts.FindAllReferences.NodeEntry): ts.ElementAccessExpression | ts.PropertyAccessExpression | undefined { - if (entry.node.parent) { - const reference = entry.node; - const parent = reference.parent; - switch (parent.kind) { - // `C.foo` - case ts.SyntaxKind.PropertyAccessExpression: - const propertyAccessExpression = ts.tryCast(parent, ts.isPropertyAccessExpression); - if (propertyAccessExpression && propertyAccessExpression.expression === reference) { - return propertyAccessExpression; - } - break; - // `C["foo"]` - case ts.SyntaxKind.ElementAccessExpression: - const elementAccessExpression = ts.tryCast(parent, ts.isElementAccessExpression); - if (elementAccessExpression && elementAccessExpression.expression === reference) { - return elementAccessExpression; - } - break; - } +function entryToAccessExpression(entry: ts.FindAllReferences.NodeEntry): ts.ElementAccessExpression | ts.PropertyAccessExpression | undefined { + if (entry.node.parent) { + const reference = entry.node; + const parent = reference.parent; + switch (parent.kind) { + // `C.foo` + case ts.SyntaxKind.PropertyAccessExpression: + const propertyAccessExpression = ts.tryCast(parent, ts.isPropertyAccessExpression); + if (propertyAccessExpression && propertyAccessExpression.expression === reference) { + return propertyAccessExpression; + } + break; + // `C["foo"]` + case ts.SyntaxKind.ElementAccessExpression: + const elementAccessExpression = ts.tryCast(parent, ts.isElementAccessExpression); + if (elementAccessExpression && elementAccessExpression.expression === reference) { + return elementAccessExpression; + } + break; } - return undefined; } + return undefined; +} - function entryToType(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { - const reference = entry.node; - if (ts.getMeaningFromLocation(reference) === ts.SemanticMeaning.Type || ts.isExpressionWithTypeArgumentsInClassExtendsClause(reference.parent)) { - return reference; - } - return undefined; +function entryToType(entry: ts.FindAllReferences.NodeEntry): ts.Node | undefined { + const reference = entry.node; + if (ts.getMeaningFromLocation(reference) === ts.SemanticMeaning.Type || ts.isExpressionWithTypeArgumentsInClassExtendsClause(reference.parent)) { + return reference; } + return undefined; +} - function getFunctionDeclarationAtPosition(file: ts.SourceFile, startPosition: number, checker: ts.TypeChecker): ValidFunctionDeclaration | undefined { - const node = ts.getTouchingToken(file, startPosition); - const functionDeclaration = ts.getContainingFunctionDeclaration(node); +function getFunctionDeclarationAtPosition(file: ts.SourceFile, startPosition: number, checker: ts.TypeChecker): ValidFunctionDeclaration | undefined { + const node = ts.getTouchingToken(file, startPosition); + const functionDeclaration = ts.getContainingFunctionDeclaration(node); - // don't offer refactor on top-level JSDoc - if (isTopLevelJSDoc(node)) - return undefined; + // don't offer refactor on top-level JSDoc + if (isTopLevelJSDoc(node)) + return undefined; - if (functionDeclaration - && isValidFunctionDeclaration(functionDeclaration, checker) - && ts.rangeContainsRange(functionDeclaration, node) - && !(functionDeclaration.body && ts.rangeContainsRange(functionDeclaration.body, node))) - return functionDeclaration; + if (functionDeclaration + && isValidFunctionDeclaration(functionDeclaration, checker) + && ts.rangeContainsRange(functionDeclaration, node) + && !(functionDeclaration.body && ts.rangeContainsRange(functionDeclaration.body, node))) + return functionDeclaration; - return undefined; - } + return undefined; +} - function isTopLevelJSDoc(node: ts.Node): boolean { - const containingJSDoc = ts.findAncestor(node, ts.isJSDocNode); - if (containingJSDoc) { - const containingNonJSDoc = ts.findAncestor(containingJSDoc, n => !ts.isJSDocNode(n)); - return !!containingNonJSDoc && ts.isFunctionLikeDeclaration(containingNonJSDoc); - } - return false; +function isTopLevelJSDoc(node: ts.Node): boolean { + const containingJSDoc = ts.findAncestor(node, ts.isJSDocNode); + if (containingJSDoc) { + const containingNonJSDoc = ts.findAncestor(containingJSDoc, n => !ts.isJSDocNode(n)); + return !!containingNonJSDoc && ts.isFunctionLikeDeclaration(containingNonJSDoc); } + return false; +} - function isValidMethodSignature(node: ts.Node): node is ValidMethodSignature { - return ts.isMethodSignature(node) && (ts.isInterfaceDeclaration(node.parent) || ts.isTypeLiteralNode(node.parent)); - } +function isValidMethodSignature(node: ts.Node): node is ValidMethodSignature { + return ts.isMethodSignature(node) && (ts.isInterfaceDeclaration(node.parent) || ts.isTypeLiteralNode(node.parent)); +} - function isValidFunctionDeclaration(functionDeclaration: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): functionDeclaration is ValidFunctionDeclaration { - if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) - return false; - switch (functionDeclaration.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker); - case ts.SyntaxKind.MethodDeclaration: - if (ts.isObjectLiteralExpression(functionDeclaration.parent)) { - const contextualSymbol = getSymbolForContextualType(functionDeclaration.name, checker); - // don't offer the refactor when there are multiple signatures since we won't know which ones the user wants to change - return contextualSymbol?.declarations?.length === 1 && isSingleImplementation(functionDeclaration, checker); - } - return isSingleImplementation(functionDeclaration, checker); - case ts.SyntaxKind.Constructor: - if (ts.isClassDeclaration(functionDeclaration.parent)) { - return hasNameOrDefault(functionDeclaration.parent) && isSingleImplementation(functionDeclaration, checker); - } - else { - return isValidVariableDeclaration(functionDeclaration.parent.parent) - && isSingleImplementation(functionDeclaration, checker); - } - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return isValidVariableDeclaration(functionDeclaration.parent); - } +function isValidFunctionDeclaration(functionDeclaration: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): functionDeclaration is ValidFunctionDeclaration { + if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) return false; + switch (functionDeclaration.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker); + case ts.SyntaxKind.MethodDeclaration: + if (ts.isObjectLiteralExpression(functionDeclaration.parent)) { + const contextualSymbol = getSymbolForContextualType(functionDeclaration.name, checker); + // don't offer the refactor when there are multiple signatures since we won't know which ones the user wants to change + return contextualSymbol?.declarations?.length === 1 && isSingleImplementation(functionDeclaration, checker); + } + return isSingleImplementation(functionDeclaration, checker); + case ts.SyntaxKind.Constructor: + if (ts.isClassDeclaration(functionDeclaration.parent)) { + return hasNameOrDefault(functionDeclaration.parent) && isSingleImplementation(functionDeclaration, checker); + } + else { + return isValidVariableDeclaration(functionDeclaration.parent.parent) + && isSingleImplementation(functionDeclaration, checker); + } + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return isValidVariableDeclaration(functionDeclaration.parent); } + return false; +} - function isSingleImplementation(functionDeclaration: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): boolean { - return !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration); - } +function isSingleImplementation(functionDeclaration: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): boolean { + return !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration); +} - function hasNameOrDefault(functionOrClassDeclaration: ts.FunctionDeclaration | ts.ClassDeclaration): boolean { - if (!functionOrClassDeclaration.name) { - const defaultKeyword = ts.findModifier(functionOrClassDeclaration, ts.SyntaxKind.DefaultKeyword); - return !!defaultKeyword; - } - return true; +function hasNameOrDefault(functionOrClassDeclaration: ts.FunctionDeclaration | ts.ClassDeclaration): boolean { + if (!functionOrClassDeclaration.name) { + const defaultKeyword = ts.findModifier(functionOrClassDeclaration, ts.SyntaxKind.DefaultKeyword); + return !!defaultKeyword; } + return true; +} - function isValidParameterNodeArray(parameters: ts.NodeArray, checker: ts.TypeChecker): parameters is ValidParameterNodeArray { - return getRefactorableParametersLength(parameters) >= minimumParameterLength - && ts.every(parameters, /*callback*/ /*callback*/ paramDecl => isValidParameterDeclaration(paramDecl, checker)); - } +function isValidParameterNodeArray(parameters: ts.NodeArray, checker: ts.TypeChecker): parameters is ValidParameterNodeArray { + return getRefactorableParametersLength(parameters) >= minimumParameterLength + && ts.every(parameters, /*callback*/ /*callback*/ paramDecl => isValidParameterDeclaration(paramDecl, checker)); +} - function isValidParameterDeclaration(parameterDeclaration: ts.ParameterDeclaration, checker: ts.TypeChecker): parameterDeclaration is ValidParameterDeclaration { - if (ts.isRestParameter(parameterDeclaration)) { - const type = checker.getTypeAtLocation(parameterDeclaration); - if (!checker.isArrayType(type) && !checker.isTupleType(type)) - return false; - } - return !parameterDeclaration.modifiers && !parameterDeclaration.decorators && ts.isIdentifier(parameterDeclaration.name); +function isValidParameterDeclaration(parameterDeclaration: ts.ParameterDeclaration, checker: ts.TypeChecker): parameterDeclaration is ValidParameterDeclaration { + if (ts.isRestParameter(parameterDeclaration)) { + const type = checker.getTypeAtLocation(parameterDeclaration); + if (!checker.isArrayType(type) && !checker.isTupleType(type)) + return false; } + return !parameterDeclaration.modifiers && !parameterDeclaration.decorators && ts.isIdentifier(parameterDeclaration.name); +} - function isValidVariableDeclaration(node: ts.Node): node is ValidVariableDeclaration { - return ts.isVariableDeclaration(node) && ts.isVarConst(node) && ts.isIdentifier(node.name) && !node.type; // TODO: GH#30113 - } +function isValidVariableDeclaration(node: ts.Node): node is ValidVariableDeclaration { + return ts.isVariableDeclaration(node) && ts.isVarConst(node) && ts.isIdentifier(node.name) && !node.type; // TODO: GH#30113 +} - function hasThisParameter(parameters: ts.NodeArray): boolean { - return parameters.length > 0 && ts.isThis(parameters[0].name); +function hasThisParameter(parameters: ts.NodeArray): boolean { + return parameters.length > 0 && ts.isThis(parameters[0].name); +} + +function getRefactorableParametersLength(parameters: ts.NodeArray): number { + if (hasThisParameter(parameters)) { + return parameters.length - 1; } + return parameters.length; +} - function getRefactorableParametersLength(parameters: ts.NodeArray): number { - if (hasThisParameter(parameters)) { - return parameters.length - 1; - } - return parameters.length; +function getRefactorableParameters(parameters: ts.NodeArray): ts.NodeArray { + if (hasThisParameter(parameters)) { + parameters = ts.factory.createNodeArray(parameters.slice(1), parameters.hasTrailingComma); } + return parameters; +} - function getRefactorableParameters(parameters: ts.NodeArray): ts.NodeArray { - if (hasThisParameter(parameters)) { - parameters = ts.factory.createNodeArray(parameters.slice(1), parameters.hasTrailingComma); - } - return parameters; +function createPropertyOrShorthandAssignment(name: string, initializer: ts.Expression): ts.PropertyAssignment | ts.ShorthandPropertyAssignment { + if (ts.isIdentifier(initializer) && ts.getTextOfIdentifierOrLiteral(initializer) === name) { + return ts.factory.createShorthandPropertyAssignment(name); } + return ts.factory.createPropertyAssignment(name, initializer); +} - function createPropertyOrShorthandAssignment(name: string, initializer: ts.Expression): ts.PropertyAssignment | ts.ShorthandPropertyAssignment { - if (ts.isIdentifier(initializer) && ts.getTextOfIdentifierOrLiteral(initializer) === name) { - return ts.factory.createShorthandPropertyAssignment(name); - } - return ts.factory.createPropertyAssignment(name, initializer); +function createNewArgument(functionDeclaration: ValidFunctionDeclaration, functionArguments: ts.NodeArray): ts.ObjectLiteralExpression { + const parameters = getRefactorableParameters(functionDeclaration.parameters); + const hasRestParameter = ts.isRestParameter(ts.last(parameters)); + const nonRestArguments = hasRestParameter ? functionArguments.slice(0, parameters.length - 1) : functionArguments; + const properties = ts.map(nonRestArguments, (arg, i) => { + const parameterName = getParameterName(parameters[i]); + const property = createPropertyOrShorthandAssignment(parameterName, arg); + + ts.suppressLeadingAndTrailingTrivia(property.name); + if (ts.isPropertyAssignment(property)) + ts.suppressLeadingAndTrailingTrivia(property.initializer); + ts.copyComments(arg, property); + return property; + }); + + if (hasRestParameter && functionArguments.length >= parameters.length) { + const restArguments = functionArguments.slice(parameters.length - 1); + const restProperty = ts.factory.createPropertyAssignment(getParameterName(ts.last(parameters)), ts.factory.createArrayLiteralExpression(restArguments)); + properties.push(restProperty); } - function createNewArgument(functionDeclaration: ValidFunctionDeclaration, functionArguments: ts.NodeArray): ts.ObjectLiteralExpression { - const parameters = getRefactorableParameters(functionDeclaration.parameters); - const hasRestParameter = ts.isRestParameter(ts.last(parameters)); - const nonRestArguments = hasRestParameter ? functionArguments.slice(0, parameters.length - 1) : functionArguments; - const properties = ts.map(nonRestArguments, (arg, i) => { - const parameterName = getParameterName(parameters[i]); - const property = createPropertyOrShorthandAssignment(parameterName, arg); - - ts.suppressLeadingAndTrailingTrivia(property.name); - if (ts.isPropertyAssignment(property)) - ts.suppressLeadingAndTrailingTrivia(property.initializer); - ts.copyComments(arg, property); - return property; - }); - - if (hasRestParameter && functionArguments.length >= parameters.length) { - const restArguments = functionArguments.slice(parameters.length - 1); - const restProperty = ts.factory.createPropertyAssignment(getParameterName(ts.last(parameters)), ts.factory.createArrayLiteralExpression(restArguments)); - properties.push(restProperty); - } + const objectLiteral = ts.factory.createObjectLiteralExpression(properties, /*multiLine*/ false); + return objectLiteral; +} - const objectLiteral = ts.factory.createObjectLiteralExpression(properties, /*multiLine*/ false); - return objectLiteral; +function createNewParameters(functionDeclaration: ValidFunctionDeclaration | ValidMethodSignature, program: ts.Program, host: ts.LanguageServiceHost): ts.NodeArray { + const checker = program.getTypeChecker(); + const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters); + const bindingElements = ts.map(refactorableParameters, createBindingElementFromParameterDeclaration); + const objectParameterName = ts.factory.createObjectBindingPattern(bindingElements); + const objectParameterType = createParameterTypeNode(refactorableParameters); + + let objectInitializer: ts.Expression | undefined; + // If every parameter in the original function was optional, add an empty object initializer to the new object parameter + if (ts.every(refactorableParameters, isOptionalParameter)) { + objectInitializer = ts.factory.createObjectLiteralExpression(); } - function createNewParameters(functionDeclaration: ValidFunctionDeclaration | ValidMethodSignature, program: ts.Program, host: ts.LanguageServiceHost): ts.NodeArray { - const checker = program.getTypeChecker(); - const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters); - const bindingElements = ts.map(refactorableParameters, createBindingElementFromParameterDeclaration); - const objectParameterName = ts.factory.createObjectBindingPattern(bindingElements); - const objectParameterType = createParameterTypeNode(refactorableParameters); - - let objectInitializer: ts.Expression | undefined; - // If every parameter in the original function was optional, add an empty object initializer to the new object parameter - if (ts.every(refactorableParameters, isOptionalParameter)) { - objectInitializer = ts.factory.createObjectLiteralExpression(); - } + const objectParameter = ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, objectParameterName, + /*questionToken*/ undefined, objectParameterType, objectInitializer); - const objectParameter = ts.factory.createParameterDeclaration( + if (hasThisParameter(functionDeclaration.parameters)) { + const thisParameter = functionDeclaration.parameters[0]; + const newThisParameter = ts.factory.createParameterDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, objectParameterName, - /*questionToken*/ undefined, objectParameterType, objectInitializer); - - if (hasThisParameter(functionDeclaration.parameters)) { - const thisParameter = functionDeclaration.parameters[0]; - const newThisParameter = ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, thisParameter.name, - /*questionToken*/ undefined, thisParameter.type); - ts.suppressLeadingAndTrailingTrivia(newThisParameter.name); - ts.copyComments(thisParameter.name, newThisParameter.name); - if (thisParameter.type) { - ts.suppressLeadingAndTrailingTrivia(newThisParameter.type!); - ts.copyComments(thisParameter.type, newThisParameter.type!); - } - - return ts.factory.createNodeArray([newThisParameter, objectParameter]); - } - return ts.factory.createNodeArray([objectParameter]); - function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): ts.BindingElement { - const element = ts.factory.createBindingElement( - /*dotDotDotToken*/ undefined, - /*propertyName*/ undefined, getParameterName(parameterDeclaration), ts.isRestParameter(parameterDeclaration) && isOptionalParameter(parameterDeclaration) ? ts.factory.createArrayLiteralExpression() : parameterDeclaration.initializer); - ts.suppressLeadingAndTrailingTrivia(element); - if (parameterDeclaration.initializer && element.initializer) { - ts.copyComments(parameterDeclaration.initializer, element.initializer); - } - return element; + /*dotDotDotToken*/ undefined, thisParameter.name, + /*questionToken*/ undefined, thisParameter.type); + ts.suppressLeadingAndTrailingTrivia(newThisParameter.name); + ts.copyComments(thisParameter.name, newThisParameter.name); + if (thisParameter.type) { + ts.suppressLeadingAndTrailingTrivia(newThisParameter.type!); + ts.copyComments(thisParameter.type, newThisParameter.type!); } - function createParameterTypeNode(parameters: ts.NodeArray): ts.TypeLiteralNode { - const members = ts.map(parameters, createPropertySignatureFromParameterDeclaration); - const typeNode = ts.addEmitFlags(ts.factory.createTypeLiteralNode(members), ts.EmitFlags.SingleLine); - return typeNode; + return ts.factory.createNodeArray([newThisParameter, objectParameter]); + } + return ts.factory.createNodeArray([objectParameter]); + function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): ts.BindingElement { + const element = ts.factory.createBindingElement( + /*dotDotDotToken*/ undefined, + /*propertyName*/ undefined, getParameterName(parameterDeclaration), ts.isRestParameter(parameterDeclaration) && isOptionalParameter(parameterDeclaration) ? ts.factory.createArrayLiteralExpression() : parameterDeclaration.initializer); + ts.suppressLeadingAndTrailingTrivia(element); + if (parameterDeclaration.initializer && element.initializer) { + ts.copyComments(parameterDeclaration.initializer, element.initializer); } + return element; + } - function createPropertySignatureFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): ts.PropertySignature { - let parameterType = parameterDeclaration.type; - if (!parameterType && (parameterDeclaration.initializer || ts.isRestParameter(parameterDeclaration))) { - parameterType = getTypeNode(parameterDeclaration); - } - - const propertySignature = ts.factory.createPropertySignature( - /*modifiers*/ undefined, getParameterName(parameterDeclaration), isOptionalParameter(parameterDeclaration) ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : parameterDeclaration.questionToken, parameterType); - ts.suppressLeadingAndTrailingTrivia(propertySignature); - ts.copyComments(parameterDeclaration.name, propertySignature.name); - if (parameterDeclaration.type && propertySignature.type) { - ts.copyComments(parameterDeclaration.type, propertySignature.type); - } - - return propertySignature; - } + function createParameterTypeNode(parameters: ts.NodeArray): ts.TypeLiteralNode { + const members = ts.map(parameters, createPropertySignatureFromParameterDeclaration); + const typeNode = ts.addEmitFlags(ts.factory.createTypeLiteralNode(members), ts.EmitFlags.SingleLine); + return typeNode; + } - function getTypeNode(node: ts.Node): ts.TypeNode | undefined { - const type = checker.getTypeAtLocation(node); - return ts.getTypeNodeIfAccessible(type, node, program, host); + function createPropertySignatureFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): ts.PropertySignature { + let parameterType = parameterDeclaration.type; + if (!parameterType && (parameterDeclaration.initializer || ts.isRestParameter(parameterDeclaration))) { + parameterType = getTypeNode(parameterDeclaration); } - function isOptionalParameter(parameterDeclaration: ValidParameterDeclaration): boolean { - if (ts.isRestParameter(parameterDeclaration)) { - const type = checker.getTypeAtLocation(parameterDeclaration); - return !checker.isTupleType(type); - } - return checker.isOptionalParameter(parameterDeclaration); + const propertySignature = ts.factory.createPropertySignature( + /*modifiers*/ undefined, getParameterName(parameterDeclaration), isOptionalParameter(parameterDeclaration) ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : parameterDeclaration.questionToken, parameterType); + ts.suppressLeadingAndTrailingTrivia(propertySignature); + ts.copyComments(parameterDeclaration.name, propertySignature.name); + if (parameterDeclaration.type && propertySignature.type) { + ts.copyComments(parameterDeclaration.type, propertySignature.type); } - } - function getParameterName(paramDeclaration: ValidParameterDeclaration) { - return ts.getTextOfIdentifierOrLiteral(paramDeclaration.name); + return propertySignature; } - function getClassNames(constructorDeclaration: ValidConstructor): (ts.Identifier | ts.Modifier)[] { - switch (constructorDeclaration.parent.kind) { - case ts.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 = ts.Debug.checkDefined(ts.findModifier(classDeclaration, ts.SyntaxKind.DefaultKeyword), "Nameless class declaration should be a default export"); - return [defaultModifier]; - case ts.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 getTypeNode(node: ts.Node): ts.TypeNode | undefined { + const type = checker.getTypeAtLocation(node); + return ts.getTypeNodeIfAccessible(type, node, program, host); } - function getFunctionNames(functionDeclaration: ValidFunctionDeclaration): ts.Node[] { - switch (functionDeclaration.kind) { - case ts.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 = ts.Debug.checkDefined(ts.findModifier(functionDeclaration, ts.SyntaxKind.DefaultKeyword), "Nameless function declaration should be a default export"); - return [defaultModifier]; - case ts.SyntaxKind.MethodDeclaration: - return [functionDeclaration.name]; - case ts.SyntaxKind.Constructor: - const ctrKeyword = ts.Debug.checkDefined(ts.findChildOfKind(functionDeclaration, ts.SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile()), "Constructor declaration should have constructor keyword"); - if (functionDeclaration.parent.kind === ts.SyntaxKind.ClassExpression) { - const variableDeclaration = functionDeclaration.parent.parent; - return [variableDeclaration.name, ctrKeyword]; - } - return [ctrKeyword]; - case ts.SyntaxKind.ArrowFunction: - return [functionDeclaration.parent.name]; - case ts.SyntaxKind.FunctionExpression: - if (functionDeclaration.name) - return [functionDeclaration.name, functionDeclaration.parent.name]; - return [functionDeclaration.parent.name]; - default: - return ts.Debug.assertNever(functionDeclaration, `Unexpected function declaration kind ${(functionDeclaration as ValidFunctionDeclaration).kind}`); + function isOptionalParameter(parameterDeclaration: ValidParameterDeclaration): boolean { + if (ts.isRestParameter(parameterDeclaration)) { + const type = checker.getTypeAtLocation(parameterDeclaration); + return !checker.isTupleType(type); } + return checker.isOptionalParameter(parameterDeclaration); } +} - type ValidParameterNodeArray = ts.NodeArray; - interface ValidVariableDeclaration extends ts.VariableDeclaration { - name: ts.Identifier; - type: undefined; - } +function getParameterName(paramDeclaration: ValidParameterDeclaration) { + return ts.getTextOfIdentifierOrLiteral(paramDeclaration.name); +} - interface ValidConstructor extends ts.ConstructorDeclaration { - parent: ts.ClassDeclaration | (ts.ClassExpression & { - parent: ValidVariableDeclaration; - }); - parameters: ts.NodeArray; - body: ts.FunctionBody; +function getClassNames(constructorDeclaration: ValidConstructor): (ts.Identifier | ts.Modifier)[] { + switch (constructorDeclaration.parent.kind) { + case ts.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 = ts.Debug.checkDefined(ts.findModifier(classDeclaration, ts.SyntaxKind.DefaultKeyword), "Nameless class declaration should be a default export"); + return [defaultModifier]; + case ts.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 ValidFunction extends ts.FunctionDeclaration { - parameters: ts.NodeArray; - body: ts.FunctionBody; +function getFunctionNames(functionDeclaration: ValidFunctionDeclaration): ts.Node[] { + switch (functionDeclaration.kind) { + case ts.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 = ts.Debug.checkDefined(ts.findModifier(functionDeclaration, ts.SyntaxKind.DefaultKeyword), "Nameless function declaration should be a default export"); + return [defaultModifier]; + case ts.SyntaxKind.MethodDeclaration: + return [functionDeclaration.name]; + case ts.SyntaxKind.Constructor: + const ctrKeyword = ts.Debug.checkDefined(ts.findChildOfKind(functionDeclaration, ts.SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile()), "Constructor declaration should have constructor keyword"); + if (functionDeclaration.parent.kind === ts.SyntaxKind.ClassExpression) { + const variableDeclaration = functionDeclaration.parent.parent; + return [variableDeclaration.name, ctrKeyword]; + } + return [ctrKeyword]; + case ts.SyntaxKind.ArrowFunction: + return [functionDeclaration.parent.name]; + case ts.SyntaxKind.FunctionExpression: + if (functionDeclaration.name) + return [functionDeclaration.name, functionDeclaration.parent.name]; + return [functionDeclaration.parent.name]; + default: + return ts.Debug.assertNever(functionDeclaration, `Unexpected function declaration kind ${(functionDeclaration as ValidFunctionDeclaration).kind}`); } +} - interface ValidMethod extends ts.MethodDeclaration { - parameters: ts.NodeArray; - body: ts.FunctionBody; - } +type ValidParameterNodeArray = ts.NodeArray; +interface ValidVariableDeclaration extends ts.VariableDeclaration { + name: ts.Identifier; + type: undefined; +} - interface ValidFunctionExpression extends ts.FunctionExpression { +interface ValidConstructor extends ts.ConstructorDeclaration { + parent: ts.ClassDeclaration | (ts.ClassExpression & { parent: ValidVariableDeclaration; - parameters: ts.NodeArray; - } + }); + parameters: ts.NodeArray; + body: ts.FunctionBody; +} - interface ValidArrowFunction extends ts.ArrowFunction { - parent: ValidVariableDeclaration; - parameters: ts.NodeArray; - } +interface ValidFunction extends ts.FunctionDeclaration { + parameters: ts.NodeArray; + body: ts.FunctionBody; +} - interface ValidMethodSignature extends ts.MethodSignature { - parameters: ts.NodeArray; - } +interface ValidMethod extends ts.MethodDeclaration { + parameters: ts.NodeArray; + body: ts.FunctionBody; +} - type ValidFunctionDeclaration = ValidConstructor | ValidFunction | ValidMethod | ValidArrowFunction | ValidFunctionExpression; +interface ValidFunctionExpression extends ts.FunctionExpression { + parent: ValidVariableDeclaration; + parameters: ts.NodeArray; +} - interface ValidParameterDeclaration extends ts.ParameterDeclaration { - name: ts.Identifier; - modifiers: undefined; - decorators: undefined; - } +interface ValidArrowFunction extends ts.ArrowFunction { + parent: ValidVariableDeclaration; + parameters: ts.NodeArray; +} - interface GroupedReferences { - functionCalls: (ts.CallExpression | ts.NewExpression)[]; - declarations: ts.Node[]; - signature?: ValidMethodSignature; - classReferences?: ClassReferences; - valid: boolean; - } - interface ClassReferences { - accessExpressions: ts.Node[]; - typeUsages: ts.Node[]; - } +interface ValidMethodSignature extends ts.MethodSignature { + parameters: ts.NodeArray; +} + +type ValidFunctionDeclaration = ValidConstructor | ValidFunction | ValidMethod | ValidArrowFunction | ValidFunctionExpression; + +interface ValidParameterDeclaration extends ts.ParameterDeclaration { + name: ts.Identifier; + modifiers: undefined; + decorators: undefined; +} + +interface GroupedReferences { + functionCalls: (ts.CallExpression | ts.NewExpression)[]; + declarations: ts.Node[]; + signature?: ValidMethodSignature; + classReferences?: ClassReferences; + valid: boolean; +} +interface ClassReferences { + accessExpressions: ts.Node[]; + typeUsages: ts.Node[]; +} } diff --git a/src/services/refactors/convertStringOrTemplateLiteral.ts b/src/services/refactors/convertStringOrTemplateLiteral.ts index d4c7bbaec65ee..f187be4299668 100644 --- a/src/services/refactors/convertStringOrTemplateLiteral.ts +++ b/src/services/refactors/convertStringOrTemplateLiteral.ts @@ -1,258 +1,258 @@ /* @internal */ namespace ts.refactor.convertStringOrTemplateLiteral { - const refactorName = "Convert to template string"; - const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_template_string); +const refactorName = "Convert to template string"; +const refactorDescription = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_template_string); - const convertStringAction = { - name: refactorName, - description: refactorDescription, - kind: "refactor.rewrite.string" - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [convertStringAction.kind], - getEditsForAction: getRefactorEditsToConvertToTemplateString, - getAvailableActions: getRefactorActionsToConvertToTemplateString - }); +const convertStringAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.string" +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [convertStringAction.kind], + getEditsForAction: getRefactorEditsToConvertToTemplateString, + getAvailableActions: getRefactorActionsToConvertToTemplateString +}); - function getRefactorActionsToConvertToTemplateString(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const { file, startPosition } = context; - const node = getNodeOrParentOfParentheses(file, startPosition); - const maybeBinary = getParentBinaryExpression(node); - const refactorInfo: ts.ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; - if (ts.isBinaryExpression(maybeBinary) && treeToArray(maybeBinary).isValidConcatenation) { - refactorInfo.actions.push(convertStringAction); - return [refactorInfo]; - } - else if (context.preferences.provideRefactorNotApplicableReason) { - refactorInfo.actions.push({ ...convertStringAction, - notApplicableReason: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_string_concatenation) - }); - return [refactorInfo]; - } - return ts.emptyArray; +function getRefactorActionsToConvertToTemplateString(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const { file, startPosition } = context; + const node = getNodeOrParentOfParentheses(file, startPosition); + const maybeBinary = getParentBinaryExpression(node); + const refactorInfo: ts.ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; + if (ts.isBinaryExpression(maybeBinary) && treeToArray(maybeBinary).isValidConcatenation) { + refactorInfo.actions.push(convertStringAction); + return [refactorInfo]; + } + else if (context.preferences.provideRefactorNotApplicableReason) { + refactorInfo.actions.push({ ...convertStringAction, + notApplicableReason: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_string_concatenation) + }); + return [refactorInfo]; } + return ts.emptyArray; +} - function getNodeOrParentOfParentheses(file: ts.SourceFile, startPosition: number) { - const node = ts.getTokenAtPosition(file, startPosition); - const nestedBinary = getParentBinaryExpression(node); - const isNonStringBinary = !treeToArray(nestedBinary).isValidConcatenation; +function getNodeOrParentOfParentheses(file: ts.SourceFile, startPosition: number) { + const node = ts.getTokenAtPosition(file, startPosition); + const nestedBinary = getParentBinaryExpression(node); + const isNonStringBinary = !treeToArray(nestedBinary).isValidConcatenation; - if (isNonStringBinary && - ts.isParenthesizedExpression(nestedBinary.parent) && - ts.isBinaryExpression(nestedBinary.parent.parent)) { - return nestedBinary.parent.parent; - } - return node; + if (isNonStringBinary && + ts.isParenthesizedExpression(nestedBinary.parent) && + ts.isBinaryExpression(nestedBinary.parent.parent)) { + return nestedBinary.parent.parent; } + return node; +} - function getRefactorEditsToConvertToTemplateString(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - const { file, startPosition } = context; - const node = getNodeOrParentOfParentheses(file, startPosition); +function getRefactorEditsToConvertToTemplateString(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + const { file, startPosition } = context; + const node = getNodeOrParentOfParentheses(file, startPosition); - switch (actionName) { - case refactorDescription: - return { edits: getEditsForToTemplateLiteral(context, node) }; - default: - return ts.Debug.fail("invalid action"); - } + switch (actionName) { + case refactorDescription: + return { edits: getEditsForToTemplateLiteral(context, node) }; + default: + return ts.Debug.fail("invalid action"); } +} - function getEditsForToTemplateLiteral(context: ts.RefactorContext, node: ts.Node) { - const maybeBinary = getParentBinaryExpression(node); - const file = context.file; +function getEditsForToTemplateLiteral(context: ts.RefactorContext, node: ts.Node) { + const maybeBinary = getParentBinaryExpression(node); + const file = context.file; - const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file); - const trailingCommentRanges = ts.getTrailingCommentRanges(file.text, maybeBinary.end); + const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file); + const trailingCommentRanges = ts.getTrailingCommentRanges(file.text, maybeBinary.end); - if (trailingCommentRanges) { - const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1]; - const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.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 ts.textChanges.ChangeTracker.with(context, t => { - t.deleteRange(file, trailingRange); - t.replaceNode(file, maybeBinary, templateLiteral); - }); - } - else { - return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral)); - } + // since suppressTrailingTrivia(maybeBinary) does not work, the trailing comment is removed manually + // otherwise it would have the trailing comment twice + return ts.textChanges.ChangeTracker.with(context, t => { + t.deleteRange(file, trailingRange); + t.replaceNode(file, maybeBinary, templateLiteral); + }); } - - function isNotEqualsOperator(node: ts.BinaryExpression) { - return node.operatorToken.kind !== ts.SyntaxKind.EqualsToken; + else { + return ts.textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral)); } +} - function getParentBinaryExpression(expr: ts.Node) { - const container = ts.findAncestor(expr.parent, n => { - switch (n.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.ElementAccessExpression: - return false; - case ts.SyntaxKind.TemplateExpression: - case ts.SyntaxKind.BinaryExpression: - return !(ts.isBinaryExpression(n.parent) && isNotEqualsOperator(n.parent)); - default: - return "quit"; - } - }); - - return (container || expr) as ts.Expression; - } - function treeToArray(current: ts.Expression) { - const loop = (current: ts.Node): { - nodes: ts.Expression[]; - operators: ts.Token[]; - hasString: boolean; - validOperators: boolean; - } => { - if (!ts.isBinaryExpression(current)) { - return { nodes: [current as ts.Expression], operators: [], validOperators: true, - hasString: ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current) }; - } - const { nodes, operators, hasString: leftHasString, validOperators: leftOperatorValid } = loop(current.left); +function isNotEqualsOperator(node: ts.BinaryExpression) { + return node.operatorToken.kind !== ts.SyntaxKind.EqualsToken; +} - if (!(leftHasString || ts.isStringLiteral(current.right) || ts.isTemplateExpression(current.right))) { - return { nodes: [current], operators: [], hasString: false, validOperators: true }; - } +function getParentBinaryExpression(expr: ts.Node) { + const container = ts.findAncestor(expr.parent, n => { + switch (n.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + return false; + case ts.SyntaxKind.TemplateExpression: + case ts.SyntaxKind.BinaryExpression: + return !(ts.isBinaryExpression(n.parent) && isNotEqualsOperator(n.parent)); + default: + return "quit"; + } + }); - const currentOperatorValid = current.operatorToken.kind === ts.SyntaxKind.PlusToken; - const validOperators = leftOperatorValid && currentOperatorValid; + return (container || expr) as ts.Expression; +} +function treeToArray(current: ts.Expression) { + const loop = (current: ts.Node): { + nodes: ts.Expression[]; + operators: ts.Token[]; + hasString: boolean; + validOperators: boolean; + } => { + if (!ts.isBinaryExpression(current)) { + return { nodes: [current as ts.Expression], operators: [], validOperators: true, + hasString: ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current) }; + } + const { nodes, operators, hasString: leftHasString, validOperators: leftOperatorValid } = loop(current.left); - nodes.push(current.right); - operators.push(current.operatorToken); + if (!(leftHasString || ts.isStringLiteral(current.right) || ts.isTemplateExpression(current.right))) { + return { nodes: [current], operators: [], hasString: false, validOperators: true }; + } - return { nodes, operators, hasString: true, validOperators }; - }; - const { nodes, operators, validOperators, hasString } = loop(current); - return { nodes, operators, isValidConcatenation: validOperators && hasString }; - } + const currentOperatorValid = current.operatorToken.kind === ts.SyntaxKind.PlusToken; + const validOperators = leftOperatorValid && currentOperatorValid; - // to copy comments following the operator - // "foo" + /* comment */ "bar" - const copyTrailingOperatorComments = (operators: ts.Token[], file: ts.SourceFile) => (index: number, targetNode: ts.Node) => { - if (index < operators.length) { - ts.copyTrailingComments(operators[index], targetNode, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - } - }; + nodes.push(current.right); + operators.push(current.operatorToken); - // to copy comments following the string - // "foo" /* comment */ + "bar" /* comment */ + "bar2" - const copyCommentFromMultiNode = (nodes: readonly ts.Expression[], file: ts.SourceFile, copyOperatorComments: (index: number, targetNode: ts.Node) => void) => (indexes: number[], targetNode: ts.Node) => { - while (indexes.length > 0) { - const index = indexes.shift()!; - ts.copyTrailingComments(nodes[index], targetNode, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - copyOperatorComments(index, targetNode); - } + return { nodes, operators, hasString: true, validOperators }; }; + const { nodes, operators, validOperators, hasString } = loop(current); + return { nodes, operators, isValidConcatenation: validOperators && hasString }; +} - function escapeRawStringForTemplate(s: string) { - // Escaping for $s in strings that are to be used in template strings - // Naive implementation: replace \x by itself and otherwise $ and ` by \$ and \`. - // But to complicate it a bit, this should work for raw strings too. - return s.replace(/\\.|[$`]/g, m => m[0] === "\\" ? m : "\\" + m); - // Finally, a less-backslash-happy version can work too, doing only ${ instead of all $s: - // s.replace(/\\.|\${|`/g, m => m[0] === "\\" ? m : "\\" + m); - // but `\$${foo}` is likely more clear than the more-confusing-but-still-working `$${foo}`. +// to copy comments following the operator +// "foo" + /* comment */ "bar" +const copyTrailingOperatorComments = (operators: ts.Token[], file: ts.SourceFile) => (index: number, targetNode: ts.Node) => { + if (index < operators.length) { + ts.copyTrailingComments(operators[index], targetNode, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); } +}; - function getRawTextOfTemplate(node: ts.TemplateHead | ts.TemplateMiddle | ts.TemplateTail) { - // in these cases the right side is ${ - const rightShaving = ts.isTemplateHead(node) || ts.isTemplateMiddle(node) ? -2 : -1; - return ts.getTextOfNode(node).slice(1, rightShaving); +// to copy comments following the string +// "foo" /* comment */ + "bar" /* comment */ + "bar2" +const copyCommentFromMultiNode = (nodes: readonly ts.Expression[], file: ts.SourceFile, copyOperatorComments: (index: number, targetNode: ts.Node) => void) => (indexes: number[], targetNode: ts.Node) => { + while (indexes.length > 0) { + const index = indexes.shift()!; + ts.copyTrailingComments(nodes[index], targetNode, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + copyOperatorComments(index, targetNode); } - function concatConsecutiveString(index: number, nodes: readonly ts.Expression[]): [ - nextIndex: number, - text: string, - rawText: string, - usedIndexes: number[] - ] { - const indexes = []; - let text = "", rawText = ""; - while (index < nodes.length) { - const node = nodes[index]; - if (ts.isStringLiteralLike(node)) { // includes isNoSubstitutionTemplateLiteral(node) - text += node.text; - rawText += escapeRawStringForTemplate(ts.getTextOfNode(node).slice(1, -1)); - indexes.push(index); - index++; - } - else if (ts.isTemplateExpression(node)) { - text += node.head.text; - rawText += getRawTextOfTemplate(node.head); - break; - } - else { - break; - } +}; + +function escapeRawStringForTemplate(s: string) { + // Escaping for $s in strings that are to be used in template strings + // Naive implementation: replace \x by itself and otherwise $ and ` by \$ and \`. + // But to complicate it a bit, this should work for raw strings too. + return s.replace(/\\.|[$`]/g, m => m[0] === "\\" ? m : "\\" + m); + // Finally, a less-backslash-happy version can work too, doing only ${ instead of all $s: + // s.replace(/\\.|\${|`/g, m => m[0] === "\\" ? m : "\\" + m); + // but `\$${foo}` is likely more clear than the more-confusing-but-still-working `$${foo}`. +} + +function getRawTextOfTemplate(node: ts.TemplateHead | ts.TemplateMiddle | ts.TemplateTail) { + // in these cases the right side is ${ + const rightShaving = ts.isTemplateHead(node) || ts.isTemplateMiddle(node) ? -2 : -1; + return ts.getTextOfNode(node).slice(1, rightShaving); +} +function concatConsecutiveString(index: number, nodes: readonly ts.Expression[]): [ + nextIndex: number, + text: string, + rawText: string, + usedIndexes: number[] +] { + const indexes = []; + let text = "", rawText = ""; + while (index < nodes.length) { + const node = nodes[index]; + if (ts.isStringLiteralLike(node)) { // includes isNoSubstitutionTemplateLiteral(node) + text += node.text; + rawText += escapeRawStringForTemplate(ts.getTextOfNode(node).slice(1, -1)); + indexes.push(index); + index++; + } + else if (ts.isTemplateExpression(node)) { + text += node.head.text; + rawText += getRawTextOfTemplate(node.head); + break; + } + else { + break; } - return [index, text, rawText, indexes]; } + return [index, text, rawText, indexes]; +} - function nodesToTemplate({ nodes, operators }: { - nodes: readonly ts.Expression[]; - operators: ts.Token[]; - }, file: ts.SourceFile) { - const copyOperatorComments = copyTrailingOperatorComments(operators, file); - const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); - const [begin, headText, rawHeadText, headIndexes] = concatConsecutiveString(0, nodes); +function nodesToTemplate({ nodes, operators }: { + nodes: readonly ts.Expression[]; + operators: ts.Token[]; +}, file: ts.SourceFile) { + const copyOperatorComments = copyTrailingOperatorComments(operators, file); + const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); + const [begin, headText, rawHeadText, headIndexes] = concatConsecutiveString(0, nodes); - if (begin === nodes.length) { - const noSubstitutionTemplateLiteral = ts.factory.createNoSubstitutionTemplateLiteral(headText, rawHeadText); - copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); - return noSubstitutionTemplateLiteral; - } + if (begin === nodes.length) { + const noSubstitutionTemplateLiteral = ts.factory.createNoSubstitutionTemplateLiteral(headText, rawHeadText); + copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); + return noSubstitutionTemplateLiteral; + } - const templateSpans: ts.TemplateSpan[] = []; - const templateHead = ts.factory.createTemplateHead(headText, rawHeadText); - copyCommentFromStringLiterals(headIndexes, templateHead); + const templateSpans: ts.TemplateSpan[] = []; + const templateHead = ts.factory.createTemplateHead(headText, rawHeadText); + copyCommentFromStringLiterals(headIndexes, templateHead); - for (let i = begin; i < nodes.length; i++) { - const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); - copyOperatorComments(i, currentNode); + for (let i = begin; i < nodes.length; i++) { + const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); + copyOperatorComments(i, currentNode); - const [newIndex, subsequentText, rawSubsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); - i = newIndex - 1; - const isLast = i === nodes.length - 1; + const [newIndex, subsequentText, rawSubsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); + i = newIndex - 1; + const isLast = i === nodes.length - 1; - if (ts.isTemplateExpression(currentNode)) { - const spans = ts.map(currentNode.templateSpans, (span, index) => { - copyExpressionComments(span); - const isLastSpan = index === currentNode.templateSpans.length - 1; - const text = span.literal.text + (isLastSpan ? subsequentText : ""); - const rawText = getRawTextOfTemplate(span.literal) + (isLastSpan ? rawSubsequentText : ""); - return ts.factory.createTemplateSpan(span.expression, isLast && isLastSpan - ? ts.factory.createTemplateTail(text, rawText) - : ts.factory.createTemplateMiddle(text, rawText)); - }); - templateSpans.push(...spans); - } - else { - const templatePart = isLast - ? ts.factory.createTemplateTail(subsequentText, rawSubsequentText) - : ts.factory.createTemplateMiddle(subsequentText, rawSubsequentText); - copyCommentFromStringLiterals(stringIndexes, templatePart); - templateSpans.push(ts.factory.createTemplateSpan(currentNode, templatePart)); - } + if (ts.isTemplateExpression(currentNode)) { + const spans = ts.map(currentNode.templateSpans, (span, index) => { + copyExpressionComments(span); + const isLastSpan = index === currentNode.templateSpans.length - 1; + const text = span.literal.text + (isLastSpan ? subsequentText : ""); + const rawText = getRawTextOfTemplate(span.literal) + (isLastSpan ? rawSubsequentText : ""); + return ts.factory.createTemplateSpan(span.expression, isLast && isLastSpan + ? ts.factory.createTemplateTail(text, rawText) + : ts.factory.createTemplateMiddle(text, rawText)); + }); + templateSpans.push(...spans); + } + else { + const templatePart = isLast + ? ts.factory.createTemplateTail(subsequentText, rawSubsequentText) + : ts.factory.createTemplateMiddle(subsequentText, rawSubsequentText); + copyCommentFromStringLiterals(stringIndexes, templatePart); + templateSpans.push(ts.factory.createTemplateSpan(currentNode, templatePart)); } - - return ts.factory.createTemplateExpression(templateHead, templateSpans); } - // to copy comments following the opening & closing parentheses - // "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" - function copyExpressionComments(node: ts.ParenthesizedExpression | ts.TemplateSpan) { - const file = node.getSourceFile(); - ts.copyTrailingComments(node, node.expression, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - ts.copyTrailingAsLeadingComments(node.expression, node.expression, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - } + return ts.factory.createTemplateExpression(templateHead, templateSpans); +} - function getExpressionFromParenthesesOrExpression(node: ts.Expression) { - if (ts.isParenthesizedExpression(node)) { - copyExpressionComments(node); - node = node.expression; - } - return node; +// to copy comments following the opening & closing parentheses +// "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" +function copyExpressionComments(node: ts.ParenthesizedExpression | ts.TemplateSpan) { + const file = node.getSourceFile(); + ts.copyTrailingComments(node, node.expression, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + ts.copyTrailingAsLeadingComments(node.expression, node.expression, file, ts.SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); +} + +function getExpressionFromParenthesesOrExpression(node: ts.Expression) { + if (ts.isParenthesizedExpression(node)) { + copyExpressionComments(node); + node = node.expression; } + return node; +} } diff --git a/src/services/refactors/convertToOptionalChainExpression.ts b/src/services/refactors/convertToOptionalChainExpression.ts index 1ca419ef96e4f..2b85941a92663 100644 --- a/src/services/refactors/convertToOptionalChainExpression.ts +++ b/src/services/refactors/convertToOptionalChainExpression.ts @@ -1,303 +1,303 @@ /* @internal */ namespace ts.refactor.convertToOptionalChainExpression { - const refactorName = "Convert to optional chain expression"; - const convertToOptionalChainExpressionMessage = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_optional_chain_expression); - - const toOptionalChainAction = { - name: refactorName, - description: convertToOptionalChainExpressionMessage, - kind: "refactor.rewrite.expression.optionalChain", - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [toOptionalChainAction.kind], - getEditsForAction: getRefactorEditsToConvertToOptionalChain, - getAvailableActions: getRefactorActionsToConvertToOptionalChain, - }); - - function getRefactorActionsToConvertToOptionalChain(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const info = getInfo(context, context.triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - return [{ - name: refactorName, - description: convertToOptionalChainExpressionMessage, - actions: [toOptionalChainAction], - }]; - } - - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ - name: refactorName, - description: convertToOptionalChainExpressionMessage, - actions: [{ ...toOptionalChainAction, notApplicableReason: info.error }], - }]; - } +const refactorName = "Convert to optional chain expression"; +const convertToOptionalChainExpressionMessage = ts.getLocaleSpecificMessage(ts.Diagnostics.Convert_to_optional_chain_expression); + +const toOptionalChainAction = { + name: refactorName, + description: convertToOptionalChainExpressionMessage, + kind: "refactor.rewrite.expression.optionalChain", +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [toOptionalChainAction.kind], + getEditsForAction: getRefactorEditsToConvertToOptionalChain, + getAvailableActions: getRefactorActionsToConvertToOptionalChain, +}); + +function getRefactorActionsToConvertToOptionalChain(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const info = getInfo(context, context.triggerReason === "invoked"); + if (!info) return ts.emptyArray; + if (!ts.refactor.isRefactorErrorInfo(info)) { + return [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage, + actions: [toOptionalChainAction], + }]; } - function getRefactorEditsToConvertToOptionalChain(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - const info = getInfo(context); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); - const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program.getTypeChecker(), t, info, actionName)); - return { edits, renameFilename: undefined, renameLocation: undefined }; + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage, + actions: [{ ...toOptionalChainAction, notApplicableReason: info.error }], + }]; } + return ts.emptyArray; +} - type Occurrence = ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.Identifier; +function getRefactorEditsToConvertToOptionalChain(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + const info = getInfo(context); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program.getTypeChecker(), t, info, actionName)); + return { edits, renameFilename: undefined, renameLocation: undefined }; +} - interface OptionalChainInfo { - finalExpression: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression; - occurrences: Occurrence[]; - expression: ValidExpression; - } - ; +type Occurrence = ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.Identifier; - type ValidExpressionOrStatement = ValidExpression | ValidStatement; +interface OptionalChainInfo { + finalExpression: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression; + occurrences: Occurrence[]; + expression: ValidExpression; +} +; - /** - * Types for which a "Convert to optional chain refactor" are offered. - */ - type ValidExpression = ts.BinaryExpression | ts.ConditionalExpression; +type ValidExpressionOrStatement = ValidExpression | ValidStatement; - /** - * Types of statements which are likely to include a valid expression for extraction. - */ - type ValidStatement = ts.ExpressionStatement | ts.ReturnStatement | ts.VariableStatement; - function isValidExpression(node: ts.Node): node is ValidExpression { - return ts.isBinaryExpression(node) || ts.isConditionalExpression(node); - } +/** + * Types for which a "Convert to optional chain refactor" are offered. + */ +type ValidExpression = ts.BinaryExpression | ts.ConditionalExpression; - function isValidStatement(node: ts.Node): node is ValidStatement { - return ts.isExpressionStatement(node) || ts.isReturnStatement(node) || ts.isVariableStatement(node); - } +/** + * Types of statements which are likely to include a valid expression for extraction. + */ +type ValidStatement = ts.ExpressionStatement | ts.ReturnStatement | ts.VariableStatement; +function isValidExpression(node: ts.Node): node is ValidExpression { + return ts.isBinaryExpression(node) || ts.isConditionalExpression(node); +} - function isValidExpressionOrStatement(node: ts.Node): node is ValidExpressionOrStatement { - return isValidExpression(node) || isValidStatement(node); - } +function isValidStatement(node: ts.Node): node is ValidStatement { + return ts.isExpressionStatement(node) || ts.isReturnStatement(node) || ts.isVariableStatement(node); +} - function getInfo(context: ts.RefactorContext, considerEmptySpans = true): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { - const { file, program } = context; - const span = ts.getRefactorContextSpan(context); +function isValidExpressionOrStatement(node: ts.Node): node is ValidExpressionOrStatement { + return isValidExpression(node) || isValidStatement(node); +} - const forEmptySpan = span.length === 0; - if (forEmptySpan && !considerEmptySpans) - return undefined; +function getInfo(context: ts.RefactorContext, considerEmptySpans = true): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { + const { file, program } = context; + const span = ts.getRefactorContextSpan(context); - // selecting fo[|o && foo.ba|]r should be valid, so adjust span to fit start and end tokens - const startToken = ts.getTokenAtPosition(file, span.start); - const endToken = ts.findTokenOnLeftOfPosition(file, span.start + span.length); - const adjustedSpan = ts.createTextSpanFromBounds(startToken.pos, endToken && endToken.end >= startToken.pos ? endToken.getEnd() : startToken.getEnd()); + const forEmptySpan = span.length === 0; + if (forEmptySpan && !considerEmptySpans) + return undefined; - const parent = forEmptySpan ? getValidParentNodeOfEmptySpan(startToken) : getValidParentNodeContainingSpan(startToken, adjustedSpan); - const expression = parent && isValidExpressionOrStatement(parent) ? getExpression(parent) : undefined; - if (!expression) - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; + // selecting fo[|o && foo.ba|]r should be valid, so adjust span to fit start and end tokens + const startToken = ts.getTokenAtPosition(file, span.start); + const endToken = ts.findTokenOnLeftOfPosition(file, span.start + span.length); + const adjustedSpan = ts.createTextSpanFromBounds(startToken.pos, endToken && endToken.end >= startToken.pos ? endToken.getEnd() : startToken.getEnd()); - const checker = program.getTypeChecker(); - return ts.isConditionalExpression(expression) ? getConditionalInfo(expression, checker) : getBinaryInfo(expression); - } + const parent = forEmptySpan ? getValidParentNodeOfEmptySpan(startToken) : getValidParentNodeContainingSpan(startToken, adjustedSpan); + const expression = parent && isValidExpressionOrStatement(parent) ? getExpression(parent) : undefined; + if (!expression) + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; - function getConditionalInfo(expression: ts.ConditionalExpression, checker: ts.TypeChecker): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { - const condition = expression.condition; - const finalExpression = getFinalExpressionInChain(expression.whenTrue); + const checker = program.getTypeChecker(); + return ts.isConditionalExpression(expression) ? getConditionalInfo(expression, checker) : getBinaryInfo(expression); +} - if (!finalExpression || checker.isNullableType(checker.getTypeAtLocation(finalExpression))) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; - } +function getConditionalInfo(expression: ts.ConditionalExpression, checker: ts.TypeChecker): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { + const condition = expression.condition; + const finalExpression = getFinalExpressionInChain(expression.whenTrue); - if ((ts.isPropertyAccessExpression(condition) || ts.isIdentifier(condition)) - && getMatchingStart(condition, finalExpression.expression)) { - return { finalExpression, occurrences: [condition], expression }; - } - else if (ts.isBinaryExpression(condition)) { - const occurrences = getOccurrencesInExpression(finalExpression.expression, condition); - return occurrences ? { finalExpression, occurrences, expression } : - { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_matching_access_expressions) }; - } + if (!finalExpression || checker.isNullableType(checker.getTypeAtLocation(finalExpression))) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; } - function getBinaryInfo(expression: ts.BinaryExpression): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { - if (expression.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_logical_AND_access_chains) }; - } - ; - const finalExpression = getFinalExpressionInChain(expression.right); - - if (!finalExpression) - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; - - const occurrences = getOccurrencesInExpression(finalExpression.expression, expression.left); + if ((ts.isPropertyAccessExpression(condition) || ts.isIdentifier(condition)) + && getMatchingStart(condition, finalExpression.expression)) { + return { finalExpression, occurrences: [condition], expression }; + } + else if (ts.isBinaryExpression(condition)) { + const occurrences = getOccurrencesInExpression(finalExpression.expression, condition); return occurrences ? { finalExpression, occurrences, expression } : { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_matching_access_expressions) }; } +} - /** - * Gets a list of property accesses that appear in matchTo and occur in sequence in expression. - */ - function getOccurrencesInExpression(matchTo: ts.Expression, expression: ts.Expression): Occurrence[] | undefined { - const occurrences: Occurrence[] = []; - while (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { - const match = getMatchingStart(ts.skipParentheses(matchTo), ts.skipParentheses(expression.right)); - if (!match) { - break; - } - occurrences.push(match); - matchTo = match; - expression = expression.left; - } - const finalMatch = getMatchingStart(matchTo, expression); - if (finalMatch) { - occurrences.push(finalMatch); - } - return occurrences.length > 0 ? occurrences: undefined; +function getBinaryInfo(expression: ts.BinaryExpression): OptionalChainInfo | ts.refactor.RefactorErrorInfo | undefined { + if (expression.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Can_only_convert_logical_AND_access_chains) }; } + ; + const finalExpression = getFinalExpressionInChain(expression.right); - /** - * Returns subchain if chain begins with subchain syntactically. - */ - function getMatchingStart(chain: ts.Expression, subchain: ts.Expression): ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.Identifier | undefined { - if (!ts.isIdentifier(subchain) && !ts.isPropertyAccessExpression(subchain) && !ts.isElementAccessExpression(subchain)) { - return undefined; - } - return chainStartsWith(chain, subchain) ? subchain : undefined; - } + if (!finalExpression) + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_convertible_access_expression) }; - /** - * Returns true if chain begins with subchain syntactically. - */ - function chainStartsWith(chain: ts.Node, subchain: ts.Node): boolean { - // skip until we find a matching identifier. - while (ts.isCallExpression(chain) || ts.isPropertyAccessExpression(chain) || ts.isElementAccessExpression(chain)) { - if (getTextOfChainNode(chain) === getTextOfChainNode(subchain)) - break; - chain = chain.expression; - } - // check that the chains match at each access. Call chains in subchain are not valid. - while ((ts.isPropertyAccessExpression(chain) && ts.isPropertyAccessExpression(subchain)) || - (ts.isElementAccessExpression(chain) && ts.isElementAccessExpression(subchain))) { - if (getTextOfChainNode(chain) !== getTextOfChainNode(subchain)) - return false; - chain = chain.expression; - subchain = subchain.expression; + const occurrences = getOccurrencesInExpression(finalExpression.expression, expression.left); + return occurrences ? { finalExpression, occurrences, expression } : + { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_find_matching_access_expressions) }; +} + +/** + * Gets a list of property accesses that appear in matchTo and occur in sequence in expression. + */ +function getOccurrencesInExpression(matchTo: ts.Expression, expression: ts.Expression): Occurrence[] | undefined { + const occurrences: Occurrence[] = []; + while (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + const match = getMatchingStart(ts.skipParentheses(matchTo), ts.skipParentheses(expression.right)); + if (!match) { + break; } - // check if we have reached a final identifier. - return ts.isIdentifier(chain) && ts.isIdentifier(subchain) && chain.getText() === subchain.getText(); + occurrences.push(match); + matchTo = match; + expression = expression.left; } + const finalMatch = getMatchingStart(matchTo, expression); + if (finalMatch) { + occurrences.push(finalMatch); + } + return occurrences.length > 0 ? occurrences: undefined; +} - function getTextOfChainNode(node: ts.Node): string | undefined { - if (ts.isIdentifier(node) || ts.isStringOrNumericLiteralLike(node)) { - return node.getText(); - } - if (ts.isPropertyAccessExpression(node)) { - return getTextOfChainNode(node.name); - } - if (ts.isElementAccessExpression(node)) { - return getTextOfChainNode(node.argumentExpression); - } +/** + * Returns subchain if chain begins with subchain syntactically. + */ +function getMatchingStart(chain: ts.Expression, subchain: ts.Expression): ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.Identifier | undefined { + if (!ts.isIdentifier(subchain) && !ts.isPropertyAccessExpression(subchain) && !ts.isElementAccessExpression(subchain)) { return undefined; } + return chainStartsWith(chain, subchain) ? subchain : undefined; +} - /** - * Find the least ancestor of the input node that is a valid type for extraction and contains the input span. - */ - function getValidParentNodeContainingSpan(node: ts.Node, span: ts.TextSpan): ValidExpressionOrStatement | undefined { - while (node.parent) { - if (isValidExpressionOrStatement(node) && span.length !== 0 && node.end >= span.start + span.length) { - return node; - } - node = node.parent; - } - return undefined; +/** + * Returns true if chain begins with subchain syntactically. + */ +function chainStartsWith(chain: ts.Node, subchain: ts.Node): boolean { + // skip until we find a matching identifier. + while (ts.isCallExpression(chain) || ts.isPropertyAccessExpression(chain) || ts.isElementAccessExpression(chain)) { + if (getTextOfChainNode(chain) === getTextOfChainNode(subchain)) + break; + chain = chain.expression; + } + // check that the chains match at each access. Call chains in subchain are not valid. + while ((ts.isPropertyAccessExpression(chain) && ts.isPropertyAccessExpression(subchain)) || + (ts.isElementAccessExpression(chain) && ts.isElementAccessExpression(subchain))) { + if (getTextOfChainNode(chain) !== getTextOfChainNode(subchain)) + return false; + chain = chain.expression; + subchain = subchain.expression; } + // check if we have reached a final identifier. + return ts.isIdentifier(chain) && ts.isIdentifier(subchain) && chain.getText() === subchain.getText(); +} - /** - * Finds an ancestor of the input node that is a valid type for extraction, skipping subexpressions. - */ - function getValidParentNodeOfEmptySpan(node: ts.Node): ValidExpressionOrStatement | undefined { - while (node.parent) { - if (isValidExpressionOrStatement(node) && !isValidExpressionOrStatement(node.parent)) { - return node; - } - node = node.parent; - } - return undefined; +function getTextOfChainNode(node: ts.Node): string | undefined { + if (ts.isIdentifier(node) || ts.isStringOrNumericLiteralLike(node)) { + return node.getText(); + } + if (ts.isPropertyAccessExpression(node)) { + return getTextOfChainNode(node.name); } + if (ts.isElementAccessExpression(node)) { + return getTextOfChainNode(node.argumentExpression); + } + return undefined; +} - /** - * Gets an expression of valid extraction type from a valid statement or expression. - */ - function getExpression(node: ValidExpressionOrStatement): ValidExpression | undefined { - if (isValidExpression(node)) { +/** + * Find the least ancestor of the input node that is a valid type for extraction and contains the input span. + */ +function getValidParentNodeContainingSpan(node: ts.Node, span: ts.TextSpan): ValidExpressionOrStatement | undefined { + while (node.parent) { + if (isValidExpressionOrStatement(node) && span.length !== 0 && node.end >= span.start + span.length) { return node; } - if (ts.isVariableStatement(node)) { - const variable = ts.getSingleVariableOfVariableStatement(node); - const initializer = variable?.initializer; - return initializer && isValidExpression(initializer) ? initializer : undefined; - } - return node.expression && isValidExpression(node.expression) ? node.expression : undefined; + node = node.parent; } + return undefined; +} - /** - * Gets a property access expression which may be nested inside of a binary expression. The final - * expression in an && chain will occur as the right child of the parent binary expression, unless - * it is followed by a different binary operator. - * @param node the right child of a binary expression or a call expression. - */ - function getFinalExpressionInChain(node: ts.Expression): ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression | undefined { - // foo && |foo.bar === 1|; - here the right child of the && binary expression is another binary expression. - // the rightmost member of the && chain should be the leftmost child of that expression. - node = ts.skipParentheses(node); - if (ts.isBinaryExpression(node)) { - return getFinalExpressionInChain(node.left); - } - // foo && |foo.bar()()| - nested calls are treated like further accesses. - else if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isCallExpression(node)) && !ts.isOptionalChain(node)) { +/** + * Finds an ancestor of the input node that is a valid type for extraction, skipping subexpressions. + */ +function getValidParentNodeOfEmptySpan(node: ts.Node): ValidExpressionOrStatement | undefined { + while (node.parent) { + if (isValidExpressionOrStatement(node) && !isValidExpressionOrStatement(node.parent)) { return node; } - return undefined; + node = node.parent; + } + return undefined; +} + +/** + * Gets an expression of valid extraction type from a valid statement or expression. + */ +function getExpression(node: ValidExpressionOrStatement): ValidExpression | undefined { + if (isValidExpression(node)) { + return node; + } + if (ts.isVariableStatement(node)) { + const variable = ts.getSingleVariableOfVariableStatement(node); + const initializer = variable?.initializer; + return initializer && isValidExpression(initializer) ? initializer : undefined; + } + return node.expression && isValidExpression(node.expression) ? node.expression : undefined; +} + +/** + * Gets a property access expression which may be nested inside of a binary expression. The final + * expression in an && chain will occur as the right child of the parent binary expression, unless + * it is followed by a different binary operator. + * @param node the right child of a binary expression or a call expression. + */ +function getFinalExpressionInChain(node: ts.Expression): ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression | undefined { + // foo && |foo.bar === 1|; - here the right child of the && binary expression is another binary expression. + // the rightmost member of the && chain should be the leftmost child of that expression. + node = ts.skipParentheses(node); + if (ts.isBinaryExpression(node)) { + return getFinalExpressionInChain(node.left); + } + // foo && |foo.bar()()| - nested calls are treated like further accesses. + else if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isCallExpression(node)) && !ts.isOptionalChain(node)) { + return node; } + return undefined; +} - /** - * Creates an access chain from toConvert with '?.' accesses at expressions appearing in occurrences. - */ - function convertOccurrences(checker: ts.TypeChecker, toConvert: ts.Expression, occurrences: Occurrence[]): ts.Expression { - if (ts.isPropertyAccessExpression(toConvert) || ts.isElementAccessExpression(toConvert) || ts.isCallExpression(toConvert)) { - const chain = convertOccurrences(checker, toConvert.expression, occurrences); - const lastOccurrence = occurrences.length > 0 ? occurrences[occurrences.length - 1] : undefined; - const isOccurrence = lastOccurrence?.getText() === toConvert.expression.getText(); - if (isOccurrence) - occurrences.pop(); - if (ts.isCallExpression(toConvert)) { - return isOccurrence ? - ts.factory.createCallChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.typeArguments, toConvert.arguments) : - ts.factory.createCallChain(chain, toConvert.questionDotToken, toConvert.typeArguments, toConvert.arguments); - } - else if (ts.isPropertyAccessExpression(toConvert)) { - return isOccurrence ? - ts.factory.createPropertyAccessChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.name) : - ts.factory.createPropertyAccessChain(chain, toConvert.questionDotToken, toConvert.name); - } - else if (ts.isElementAccessExpression(toConvert)) { - return isOccurrence ? - ts.factory.createElementAccessChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.argumentExpression) : - ts.factory.createElementAccessChain(chain, toConvert.questionDotToken, toConvert.argumentExpression); - } +/** + * Creates an access chain from toConvert with '?.' accesses at expressions appearing in occurrences. + */ +function convertOccurrences(checker: ts.TypeChecker, toConvert: ts.Expression, occurrences: Occurrence[]): ts.Expression { + if (ts.isPropertyAccessExpression(toConvert) || ts.isElementAccessExpression(toConvert) || ts.isCallExpression(toConvert)) { + const chain = convertOccurrences(checker, toConvert.expression, occurrences); + const lastOccurrence = occurrences.length > 0 ? occurrences[occurrences.length - 1] : undefined; + const isOccurrence = lastOccurrence?.getText() === toConvert.expression.getText(); + if (isOccurrence) + occurrences.pop(); + if (ts.isCallExpression(toConvert)) { + return isOccurrence ? + ts.factory.createCallChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.typeArguments, toConvert.arguments) : + ts.factory.createCallChain(chain, toConvert.questionDotToken, toConvert.typeArguments, toConvert.arguments); + } + else if (ts.isPropertyAccessExpression(toConvert)) { + return isOccurrence ? + ts.factory.createPropertyAccessChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.name) : + ts.factory.createPropertyAccessChain(chain, toConvert.questionDotToken, toConvert.name); + } + else if (ts.isElementAccessExpression(toConvert)) { + return isOccurrence ? + ts.factory.createElementAccessChain(chain, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), toConvert.argumentExpression) : + ts.factory.createElementAccessChain(chain, toConvert.questionDotToken, toConvert.argumentExpression); } - return toConvert; } + return toConvert; +} - function doChange(sourceFile: ts.SourceFile, checker: ts.TypeChecker, changes: ts.textChanges.ChangeTracker, info: OptionalChainInfo, _actionName: string): void { - const { finalExpression, occurrences, expression } = info; - const firstOccurrence = occurrences[occurrences.length - 1]; - const convertedChain = convertOccurrences(checker, finalExpression, occurrences); - if (convertedChain && (ts.isPropertyAccessExpression(convertedChain) || ts.isElementAccessExpression(convertedChain) || ts.isCallExpression(convertedChain))) { - if (ts.isBinaryExpression(expression)) { - changes.replaceNodeRange(sourceFile, firstOccurrence, finalExpression, convertedChain); - } - else if (ts.isConditionalExpression(expression)) { - changes.replaceNode(sourceFile, expression, ts.factory.createBinaryExpression(convertedChain, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), expression.whenFalse)); - } +function doChange(sourceFile: ts.SourceFile, checker: ts.TypeChecker, changes: ts.textChanges.ChangeTracker, info: OptionalChainInfo, _actionName: string): void { + const { finalExpression, occurrences, expression } = info; + const firstOccurrence = occurrences[occurrences.length - 1]; + const convertedChain = convertOccurrences(checker, finalExpression, occurrences); + if (convertedChain && (ts.isPropertyAccessExpression(convertedChain) || ts.isElementAccessExpression(convertedChain) || ts.isCallExpression(convertedChain))) { + if (ts.isBinaryExpression(expression)) { + changes.replaceNodeRange(sourceFile, firstOccurrence, finalExpression, convertedChain); + } + else if (ts.isConditionalExpression(expression)) { + changes.replaceNode(sourceFile, expression, ts.factory.createBinaryExpression(convertedChain, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), expression.whenFalse)); } } } +} diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 604ec8e9d4bff..1675d277610cb 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1,1962 +1,1962 @@ /* @internal */ namespace ts.refactor.extractSymbol { - const refactorName = "Extract Symbol"; - - const extractConstantAction = { - name: "Extract Constant", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), - kind: "refactor.extract.constant", - }; - const extractFunctionAction = { - name: "Extract Function", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), - kind: "refactor.extract.function", - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [ - extractConstantAction.kind, - extractFunctionAction.kind - ], - getEditsForAction: getRefactorEditsToExtractSymbol, - getAvailableActions: getRefactorActionsToExtractSymbol, - }); - - /** - * Compute the associated code actions - * Exported for tests. - */ - export function getRefactorActionsToExtractSymbol(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const requestedRefactor = context.kind; - const rangeToExtract = getRangeToExtract(context.file, ts.getRefactorContextSpan(context), context.triggerReason === "invoked"); - const targetRange = rangeToExtract.targetRange; - - if (targetRange === undefined) { - if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) { - return ts.emptyArray; - } - - const errors = []; - if (ts.refactor.refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { - errors.push({ - name: refactorName, - description: extractFunctionAction.description, - actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }] - }); - } - if (ts.refactor.refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { - errors.push({ - name: refactorName, - description: extractConstantAction.description, - actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }] - }); - } - return errors; +const refactorName = "Extract Symbol"; + +const extractConstantAction = { + name: "Extract Constant", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), + kind: "refactor.extract.constant", +}; +const extractFunctionAction = { + name: "Extract Function", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), + kind: "refactor.extract.function", +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [ + extractConstantAction.kind, + extractFunctionAction.kind + ], + getEditsForAction: getRefactorEditsToExtractSymbol, + getAvailableActions: getRefactorActionsToExtractSymbol, +}); + +/** + * Compute the associated code actions + * Exported for tests. + */ +export function getRefactorActionsToExtractSymbol(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const requestedRefactor = context.kind; + const rangeToExtract = getRangeToExtract(context.file, ts.getRefactorContextSpan(context), context.triggerReason === "invoked"); + const targetRange = rangeToExtract.targetRange; + + if (targetRange === undefined) { + if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) { + return ts.emptyArray; } - const extractions = getPossibleExtractions(targetRange, context); - if (extractions === undefined) { - // No extractions possible - return ts.emptyArray; + const errors = []; + if (ts.refactor.refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { + errors.push({ + name: refactorName, + description: extractFunctionAction.description, + actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }] + }); + } + if (ts.refactor.refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { + errors.push({ + name: refactorName, + description: extractConstantAction.description, + actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }] + }); } - const functionActions: ts.RefactorActionInfo[] = []; - const usedFunctionNames = new ts.Map(); - let innermostErrorFunctionAction: ts.RefactorActionInfo | undefined; - const constantActions: ts.RefactorActionInfo[] = []; - const usedConstantNames = new ts.Map(); - let innermostErrorConstantAction: ts.RefactorActionInfo | undefined; + return errors; + } - let i = 0; - for (const { functionExtraction, constantExtraction } of extractions) { - const description = functionExtraction.description; - if (ts.refactor.refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { - 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 - if (!usedFunctionNames.has(description)) { - usedFunctionNames.set(description, true); - functionActions.push({ - description, - name: `function_scope_${i}`, - kind: extractFunctionAction.kind - }); - } - } - else if (!innermostErrorFunctionAction) { - innermostErrorFunctionAction = { + const extractions = getPossibleExtractions(targetRange, context); + if (extractions === undefined) { + // No extractions possible + return ts.emptyArray; + } + const functionActions: ts.RefactorActionInfo[] = []; + const usedFunctionNames = new ts.Map(); + let innermostErrorFunctionAction: ts.RefactorActionInfo | undefined; + const constantActions: ts.RefactorActionInfo[] = []; + const usedConstantNames = new ts.Map(); + let innermostErrorConstantAction: ts.RefactorActionInfo | undefined; + + let i = 0; + for (const { functionExtraction, constantExtraction } of extractions) { + const description = functionExtraction.description; + if (ts.refactor.refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { + 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 + if (!usedFunctionNames.has(description)) { + usedFunctionNames.set(description, true); + functionActions.push({ description, name: `function_scope_${i}`, - notApplicableReason: getStringError(functionExtraction.errors), kind: extractFunctionAction.kind - }; + }); } } - - if (ts.refactor.refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { - 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}`, - kind: extractConstantAction.kind - }); - } - } - else if (!innermostErrorConstantAction) { - innermostErrorConstantAction = { + else if (!innermostErrorFunctionAction) { + innermostErrorFunctionAction = { + description, + name: `function_scope_${i}`, + notApplicableReason: getStringError(functionExtraction.errors), + kind: extractFunctionAction.kind + }; + } + } + + if (ts.refactor.refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { + 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}`, - notApplicableReason: getStringError(constantExtraction.errors), kind: extractConstantAction.kind - }; + }); } } - - // *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++; + else if (!innermostErrorConstantAction) { + innermostErrorConstantAction = { + description, + name: `constant_scope_${i}`, + notApplicableReason: getStringError(constantExtraction.errors), + kind: extractConstantAction.kind + }; + } } - const infos: ts.ApplicableRefactorInfo[] = []; + // *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++; + } - if (functionActions.length) { - infos.push({ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), - actions: functionActions, - }); - } - else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) { - infos.push({ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), - actions: [ innermostErrorFunctionAction ] - }); - } + const infos: ts.ApplicableRefactorInfo[] = []; - if (constantActions.length) { - infos.push({ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), - actions: constantActions - }); - } - else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorConstantAction) { - infos.push({ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), - actions: [ innermostErrorConstantAction ] - }); + if (functionActions.length) { + infos.push({ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), + actions: functionActions, + }); + } + else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) { + infos.push({ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_function), + actions: [ innermostErrorFunctionAction ] + }); + } + + if (constantActions.length) { + infos.push({ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), + actions: constantActions + }); + } + else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorConstantAction) { + infos.push({ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_constant), + actions: [ innermostErrorConstantAction ] + }); + } + + return infos.length ? infos : ts.emptyArray; + function getStringError(errors: readonly ts.Diagnostic[]) { + let error = errors[0].messageText; + if (typeof error !== "string") { + error = error.messageText; } + return error; + } +} - return infos.length ? infos : ts.emptyArray; - function getStringError(errors: readonly ts.Diagnostic[]) { - let error = errors[0].messageText; - if (typeof error !== "string") { - error = error.messageText; - } - return error; - } - } - - /* Exported for tests */ - export function getRefactorEditsToExtractSymbol(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { - const rangeToExtract = getRangeToExtract(context.file, ts.getRefactorContextSpan(context)); - const targetRange = rangeToExtract.targetRange!; // TODO:GH#18217 - - const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); - if (parsedFunctionIndexMatch) { - const index = +parsedFunctionIndexMatch[1]; - ts.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]; - ts.Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index"); - return getConstantExtractionAtIndex(targetRange, context, index); - } - - ts.Debug.fail("Unrecognized action name"); - } - - // Move these into diagnostic messages if they become user-facing - export namespace Messages { - function createMessage(message: string): ts.DiagnosticMessage { - return { message, code: 0, category: ts.DiagnosticCategory.Message, key: message }; - } - export const cannotExtractRange: ts.DiagnosticMessage = createMessage("Cannot extract range."); - export const cannotExtractImport: ts.DiagnosticMessage = createMessage("Cannot extract import statement."); - export const cannotExtractSuper: ts.DiagnosticMessage = createMessage("Cannot extract super call."); - export const cannotExtractJSDoc: ts.DiagnosticMessage = createMessage("Cannot extract JSDoc."); - export const cannotExtractEmpty: ts.DiagnosticMessage = createMessage("Cannot extract empty range."); - export const expressionExpected: ts.DiagnosticMessage = createMessage("expression expected."); - export const uselessConstantType: ts.DiagnosticMessage = createMessage("No reason to extract constant of type."); - export const statementOrExpressionExpected: ts.DiagnosticMessage = createMessage("Statement or expression expected."); - export const cannotExtractRangeContainingConditionalBreakOrContinueStatements: ts.DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements."); - export const cannotExtractRangeContainingConditionalReturnStatement: ts.DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement."); - export const cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange: ts.DiagnosticMessage = createMessage("Cannot extract range containing labeled break or continue with target outside of the range."); - export const cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: ts.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 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"); - export const cannotExtractFunctionsContainingThisToMethod = createMessage("Cannot extract functions containing this to method"); - } - - enum RangeFacts { - None = 0, - HasReturn = 1 << 0, - IsGenerator = 1 << 1, - IsAsyncFunction = 1 << 2, - UsesThis = 1 << 3, - UsesThisInFunction = 1 << 4, - /** - * The range is in a function which needs the 'static' modifier in a class - */ - InStaticRegion = 1 << 5 +/* Exported for tests */ +export function getRefactorEditsToExtractSymbol(context: ts.RefactorContext, actionName: string): ts.RefactorEditInfo | undefined { + const rangeToExtract = getRangeToExtract(context.file, ts.getRefactorContextSpan(context)); + const targetRange = rangeToExtract.targetRange!; // TODO:GH#18217 + + const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); + if (parsedFunctionIndexMatch) { + const index = +parsedFunctionIndexMatch[1]; + ts.Debug.assert(isFinite(index), "Expected to parse a finite number from the function scope index"); + return getFunctionExtractionAtIndex(targetRange, context, index); } - /** - * Represents an expression or a list of statements that should be extracted with some extra information - */ - interface TargetRange { - readonly range: ts.Expression | ts.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: ts.Symbol[]; - /** - * If `this` is referring to a function instead of class, we need to retrieve its type. - */ - readonly thisNode: ts.Node | undefined; + const parsedConstantIndexMatch = /^constant_scope_(\d+)$/.exec(actionName); + if (parsedConstantIndexMatch) { + const index = +parsedConstantIndexMatch[1]; + ts.Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index"); + return getConstantExtractionAtIndex(targetRange, context, index); + } + + ts.Debug.fail("Unrecognized action name"); +} + +// Move these into diagnostic messages if they become user-facing +export namespace Messages { + function createMessage(message: string): ts.DiagnosticMessage { + return { message, code: 0, category: ts.DiagnosticCategory.Message, key: message }; } + export const cannotExtractRange: ts.DiagnosticMessage = createMessage("Cannot extract range."); + export const cannotExtractImport: ts.DiagnosticMessage = createMessage("Cannot extract import statement."); + export const cannotExtractSuper: ts.DiagnosticMessage = createMessage("Cannot extract super call."); + export const cannotExtractJSDoc: ts.DiagnosticMessage = createMessage("Cannot extract JSDoc."); + export const cannotExtractEmpty: ts.DiagnosticMessage = createMessage("Cannot extract empty range."); + export const expressionExpected: ts.DiagnosticMessage = createMessage("expression expected."); + export const uselessConstantType: ts.DiagnosticMessage = createMessage("No reason to extract constant of type."); + export const statementOrExpressionExpected: ts.DiagnosticMessage = createMessage("Statement or expression expected."); + export const cannotExtractRangeContainingConditionalBreakOrContinueStatements: ts.DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements."); + export const cannotExtractRangeContainingConditionalReturnStatement: ts.DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement."); + export const cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange: ts.DiagnosticMessage = createMessage("Cannot extract range containing labeled break or continue with target outside of the range."); + export const cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: ts.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 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"); + export const cannotExtractFunctionsContainingThisToMethod = createMessage("Cannot extract functions containing this to method"); +} +enum RangeFacts { + None = 0, + HasReturn = 1 << 0, + IsGenerator = 1 << 1, + IsAsyncFunction = 1 << 2, + UsesThis = 1 << 3, + UsesThisInFunction = 1 << 4, /** - * 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 ts.Diagnostic[]; - } | { - readonly targetRange: TargetRange; - readonly errors?: never; - }; - - /* - * Scopes that can store newly extracted method - */ - type Scope = ts.FunctionLikeDeclaration | ts.SourceFile | ts.ModuleBlock | ts.ClassLikeDeclaration; + InStaticRegion = 1 << 5 +} +/** + * Represents an expression or a list of statements that should be extracted with some extra information + */ +interface TargetRange { + readonly range: ts.Expression | ts.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 errors are shown to - * users if they have the provideRefactorNotApplicableReason option set. + * 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: ts.SourceFile, span: ts.TextSpan, invoked = true): RangeToExtract { - const { length } = span; - if (length === 0 && !invoked) { - return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] }; - } - const cursorRequest = length === 0 && invoked; + readonly declarations: ts.Symbol[]; + /** + * If `this` is referring to a function instead of class, we need to retrieve its type. + */ + readonly thisNode: ts.Node | undefined; +} + +/** + * Result of 'getRangeToExtract' operation: contains either a range or a list of errors + */ +type RangeToExtract = { + readonly targetRange?: never; + readonly errors: readonly ts.Diagnostic[]; +} | { + readonly targetRange: TargetRange; + readonly errors?: never; +}; + +/* + * Scopes that can store newly extracted method + */ +type Scope = ts.FunctionLikeDeclaration | ts.SourceFile | ts.ModuleBlock | ts.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 errors are shown to + * users if they have the provideRefactorNotApplicableReason option set. + */ +// exported only for tests +export function getRangeToExtract(sourceFile: ts.SourceFile, span: ts.TextSpan, invoked = true): RangeToExtract { + const { length } = span; + if (length === 0 && !invoked) { + return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] }; + } + const cursorRequest = length === 0 && invoked; - const startToken = ts.findFirstNonJsxWhitespaceToken(sourceFile, span.start); - const endToken = ts.findTokenOnLeftOfPosition(sourceFile, ts.textSpanEnd(span)); - /* If the refactoring command is invoked through a keyboard action it's safe to assume that the user is actively looking for - refactoring actions at the span location. As they may not know the exact range that will trigger a refactoring, we expand the - searched span to cover a real node range making it more likely that something useful will show up. */ - const adjustedSpan = startToken && endToken && invoked ? getAdjustedSpanFromNodes(startToken, endToken, sourceFile) : span; + const startToken = ts.findFirstNonJsxWhitespaceToken(sourceFile, span.start); + const endToken = ts.findTokenOnLeftOfPosition(sourceFile, ts.textSpanEnd(span)); + /* If the refactoring command is invoked through a keyboard action it's safe to assume that the user is actively looking for + refactoring actions at the span location. As they may not know the exact range that will trigger a refactoring, we expand the + searched span to cover a real node range making it more likely that something useful will show up. */ + const adjustedSpan = startToken && endToken && invoked ? getAdjustedSpanFromNodes(startToken, endToken, sourceFile) : span; - // 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 = cursorRequest ? getExtractableParent(startToken) : ts.getParentNodeInSpan(startToken, sourceFile, adjustedSpan); + // 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 = cursorRequest ? getExtractableParent(startToken) : ts.getParentNodeInSpan(startToken, sourceFile, adjustedSpan); - // Do the same for the ending position - const end = cursorRequest ? start : ts.getParentNodeInSpan(endToken, sourceFile, adjustedSpan); - const declarations: ts.Symbol[] = []; + // Do the same for the ending position + const end = cursorRequest ? start : ts.getParentNodeInSpan(endToken, sourceFile, adjustedSpan); + const declarations: ts.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; + // 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; - let thisNode: ts.Node | undefined; + let thisNode: ts.Node | undefined; - if (!start || !end) { - // cannot find either start or end node - return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; - } + if (!start || !end) { + // cannot find either start or end node + return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } - if (start.flags & ts.NodeFlags.JSDoc) { - return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] }; - } + if (start.flags & ts.NodeFlags.JSDoc) { + return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] }; + } + + if (start.parent !== end.parent) { + // start and end nodes belong to different subtrees + return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } - if (start.parent !== end.parent) { - // start and end nodes belong to different subtrees + 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: [ts.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: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; - } - const statements: ts.Statement[] = []; - for (const statement of start.parent.statements) { - if (statement === start || statements.length) { - const errors = checkNode(statement); - if (errors) { - return { errors }; - } - statements.push(statement); - } - if (statement === end) { - break; + const statements: ts.Statement[] = []; + for (const statement of start.parent.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: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + if (statement === end) { + break; } - - return { targetRange: { range: statements, facts: rangeFacts, declarations, thisNode } }; } - if (ts.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: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; } - // We have a single node (start) - const node = refineNode(start); + return { targetRange: { range: statements, facts: rangeFacts, declarations, thisNode } }; + } - const errors = checkRootNode(node) || checkNode(node); - if (errors) { - return { errors }; - } - return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations, thisNode } }; // TODO: GH#18217 + if (ts.isReturnStatement(start) && !start.expression) { + // Makes no sense to extract an expression-less return statement. + return { errors: [ts.createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } + + // We have a single node (start) + const node = refineNode(start); - /** - * Attempt to refine the extraction node (generally, by shrinking it) to produce better results. - * @param node The unrefined extraction node. - */ - function refineNode(node: ts.Node): ts.Node { - if (ts.isReturnStatement(node)) { - if (node.expression) { - return node.expression; + const errors = checkRootNode(node) || checkNode(node); + if (errors) { + return { errors }; + } + return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations, thisNode } }; // 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: ts.Node): ts.Node { + if (ts.isReturnStatement(node)) { + if (node.expression) { + return node.expression; + } + } + else if (ts.isVariableStatement(node) || ts.isVariableDeclarationList(node)) { + const declarations = ts.isVariableStatement(node) ? node.declarationList.declarations : node.declarations; + let numInitializers = 0; + let lastInitializer: ts.Expression | undefined; + for (const declaration of declarations) { + if (declaration.initializer) { + numInitializers++; + lastInitializer = declaration.initializer; } } - else if (ts.isVariableStatement(node) || ts.isVariableDeclarationList(node)) { - const declarations = ts.isVariableStatement(node) ? node.declarationList.declarations : node.declarations; - let numInitializers = 0; - let lastInitializer: ts.Expression | undefined; - for (const declaration of declarations) { - if (declaration.initializer) { - numInitializers++; - lastInitializer = declaration.initializer; - } - } - if (numInitializers === 1) { - return lastInitializer!; - } - // No special handling if there are multiple initializers. + if (numInitializers === 1) { + return lastInitializer!; } - else if (ts.isVariableDeclaration(node)) { - if (node.initializer) { - return node.initializer; - } + // No special handling if there are multiple initializers. + } + else if (ts.isVariableDeclaration(node)) { + if (node.initializer) { + return node.initializer; } - return node; } + return node; + } - function checkRootNode(node: ts.Node): ts.Diagnostic[] | undefined { - if (ts.isIdentifier(ts.isExpressionStatement(node) ? node.expression : node)) { - return [ts.createDiagnosticForNode(node, Messages.cannotExtractIdentifier)]; - } - return undefined; + function checkRootNode(node: ts.Node): ts.Diagnostic[] | undefined { + if (ts.isIdentifier(ts.isExpressionStatement(node) ? node.expression : node)) { + return [ts.createDiagnosticForNode(node, Messages.cannotExtractIdentifier)]; } + return undefined; + } - function checkForStaticContext(nodeToCheck: ts.Node, containingClass: ts.Node) { - let current: ts.Node = nodeToCheck; - while (current !== containingClass) { - if (current.kind === ts.SyntaxKind.PropertyDeclaration) { - if (ts.isStatic(current)) { - rangeFacts |= RangeFacts.InStaticRegion; - } - break; + function checkForStaticContext(nodeToCheck: ts.Node, containingClass: ts.Node) { + let current: ts.Node = nodeToCheck; + while (current !== containingClass) { + if (current.kind === ts.SyntaxKind.PropertyDeclaration) { + if (ts.isStatic(current)) { + rangeFacts |= RangeFacts.InStaticRegion; } - else if (current.kind === ts.SyntaxKind.Parameter) { - const ctorOrMethod = ts.getContainingFunction(current)!; - if (ctorOrMethod.kind === ts.SyntaxKind.Constructor) { - rangeFacts |= RangeFacts.InStaticRegion; - } - break; + break; + } + else if (current.kind === ts.SyntaxKind.Parameter) { + const ctorOrMethod = ts.getContainingFunction(current)!; + if (ctorOrMethod.kind === ts.SyntaxKind.Constructor) { + rangeFacts |= RangeFacts.InStaticRegion; } - else if (current.kind === ts.SyntaxKind.MethodDeclaration) { - if (ts.isStatic(current)) { - rangeFacts |= RangeFacts.InStaticRegion; - } + break; + } + else if (current.kind === ts.SyntaxKind.MethodDeclaration) { + if (ts.isStatic(current)) { + rangeFacts |= RangeFacts.InStaticRegion; } - current = current.parent; } + current = current.parent; } + } - // Verifies whether we can actually extract this node or not. - function checkNode(nodeToCheck: ts.Node): ts.Diagnostic[] | undefined { - const enum PermittedJumps { - None = 0, - Break = 1 << 0, - Continue = 1 << 1, - Return = 1 << 2 - } + // Verifies whether we can actually extract this node or not. + function checkNode(nodeToCheck: ts.Node): ts.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. - ts.Debug.assert(nodeToCheck.pos <= nodeToCheck.end, "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (1)"); + // We believe it's true because the node is from the (unmodified) tree. + ts.Debug.assert(nodeToCheck.pos <= nodeToCheck.end, "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (1)"); - // For understanding how skipTrivia functioned: - ts.Debug.assert(!ts.positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); - if (!ts.isStatement(nodeToCheck) && !(ts.isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck)) && !isStringLiteralJsxAttribute(nodeToCheck)) { - return [ts.createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; - } + // For understanding how skipTrivia functioned: + ts.Debug.assert(!ts.positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); + if (!ts.isStatement(nodeToCheck) && !(ts.isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck)) && !isStringLiteralJsxAttribute(nodeToCheck)) { + return [ts.createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; + } - if (nodeToCheck.flags & ts.NodeFlags.Ambient) { - return [ts.createDiagnosticForNode(nodeToCheck, Messages.cannotExtractAmbientBlock)]; - } + if (nodeToCheck.flags & ts.NodeFlags.Ambient) { + return [ts.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 = ts.getContainingClass(nodeToCheck); - if (containingClass) { - checkForStaticContext(nodeToCheck, containingClass); - } + // 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 = ts.getContainingClass(nodeToCheck); + if (containingClass) { + checkForStaticContext(nodeToCheck, containingClass); + } - let errors: ts.Diagnostic[] | undefined; - let permittedJumps = PermittedJumps.Return; - let seenLabels: ts.__String[]; + let errors: ts.Diagnostic[] | undefined; + let permittedJumps = PermittedJumps.Return; + let seenLabels: ts.__String[]; - visit(nodeToCheck); + visit(nodeToCheck); - if (rangeFacts & RangeFacts.UsesThis) { - const container = ts.getThisContainer(nodeToCheck, /** includeArrowFunctions */ false); - if (container.kind === ts.SyntaxKind.FunctionDeclaration || - (container.kind === ts.SyntaxKind.MethodDeclaration && container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) || - container.kind === ts.SyntaxKind.FunctionExpression) { - rangeFacts |= RangeFacts.UsesThisInFunction; - } + if (rangeFacts & RangeFacts.UsesThis) { + const container = ts.getThisContainer(nodeToCheck, /** includeArrowFunctions */ false); + if (container.kind === ts.SyntaxKind.FunctionDeclaration || + (container.kind === ts.SyntaxKind.MethodDeclaration && container.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) || + container.kind === ts.SyntaxKind.FunctionExpression) { + rangeFacts |= RangeFacts.UsesThisInFunction; } + } - return errors; - - function visit(node: ts.Node) { - if (errors) { - // already found an error - can stop now - return true; - } + return errors; - if (ts.isDeclaration(node)) { - const declaringNode = (node.kind === ts.SyntaxKind.VariableDeclaration) ? node.parent.parent : node; - if (ts.hasSyntacticModifier(declaringNode, ts.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 ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); - return true; - } - declarations.push(node.symbol); - } + function visit(node: ts.Node) { + if (errors) { + // already found an error - can stop now + return true; + } - // Some things can't be extracted in certain situations - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractImport)); - return true; - case ts.SyntaxKind.ExportAssignment: - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); - return true; - case ts.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 === ts.SyntaxKind.CallExpression) { - // Super constructor call - const containingClass = ts.getContainingClass(node); - if (containingClass === undefined || containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) { - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractSuper)); - return true; - } - } - else { - rangeFacts |= RangeFacts.UsesThis; - thisNode = node; - } - break; - case ts.SyntaxKind.ArrowFunction: - // check if arrow function uses this - ts.forEachChild(node, function check(n) { - if (ts.isThis(n)) { - rangeFacts |= RangeFacts.UsesThis; - thisNode = node; - } - else if (ts.isClassLike(n) || (ts.isFunctionLike(n) && !ts.isArrowFunction(n))) { - return false; - } - else { - ts.forEachChild(n, check); - } - }); - // falls through - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - if (ts.isSourceFile(node.parent) && node.parent.externalModuleIndicator === undefined) { - // You cannot extract global declarations - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.functionWillNotBeVisibleInTheNewScope)); - } - // falls through - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - // do not dive into functions or classes - return false; + if (ts.isDeclaration(node)) { + const declaringNode = (node.kind === ts.SyntaxKind.VariableDeclaration) ? node.parent.parent : node; + if (ts.hasSyntacticModifier(declaringNode, ts.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 ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); + return true; } + declarations.push(node.symbol); + } - const savedPermittedJumps = permittedJumps; - switch (node.kind) { - case ts.SyntaxKind.IfStatement: - permittedJumps = PermittedJumps.None; - break; - case ts.SyntaxKind.TryStatement: - // forbid all jumps inside try blocks - permittedJumps = PermittedJumps.None; - break; - case ts.SyntaxKind.Block: - if (node.parent && node.parent.kind === ts.SyntaxKind.TryStatement && (node.parent as ts.TryStatement).finallyBlock === node) { - // allow unconditional returns from finally blocks - permittedJumps = PermittedJumps.Return; - } - break; - case ts.SyntaxKind.DefaultClause: - case ts.SyntaxKind.CaseClause: - // allow unlabeled break inside case clauses - permittedJumps |= PermittedJumps.Break; - break; - default: - if (ts.isIterationStatement(node, /*lookInLabeledStatements*/ false)) { - // allow unlabeled break/continue inside loops - permittedJumps |= PermittedJumps.Break | PermittedJumps.Continue; + // Some things can't be extracted in certain situations + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractImport)); + return true; + case ts.SyntaxKind.ExportAssignment: + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); + return true; + case ts.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 === ts.SyntaxKind.CallExpression) { + // Super constructor call + const containingClass = ts.getContainingClass(node); + if (containingClass === undefined || containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) { + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractSuper)); + return true; } - break; - } - - switch (node.kind) { - case ts.SyntaxKind.ThisType: - case ts.SyntaxKind.ThisKeyword: + } + else { rangeFacts |= RangeFacts.UsesThis; thisNode = node; - break; - case ts.SyntaxKind.LabeledStatement: { - const label = (node as ts.LabeledStatement).label; - (seenLabels || (seenLabels = [])).push(label.escapedText); - ts.forEachChild(node, visit); - seenLabels.pop(); - break; } - case ts.SyntaxKind.BreakStatement: - case ts.SyntaxKind.ContinueStatement: { - const label = (node as ts.BreakStatement | ts.ContinueStatement).label; - if (label) { - if (!ts.contains(seenLabels, label.escapedText)) { - // attempts to jump to label that is not in range to be extracted - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange)); - } + break; + case ts.SyntaxKind.ArrowFunction: + // check if arrow function uses this + ts.forEachChild(node, function check(n) { + if (ts.isThis(n)) { + rangeFacts |= RangeFacts.UsesThis; + thisNode = node; + } + else if (ts.isClassLike(n) || (ts.isFunctionLike(n) && !ts.isArrowFunction(n))) { + return false; } else { - if (!(permittedJumps & (node.kind === ts.SyntaxKind.BreakStatement ? PermittedJumps.Break : PermittedJumps.Continue))) { - // attempt to break or continue in a forbidden context - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements)); - } + ts.forEachChild(n, check); } - break; + }); + // falls through + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + if (ts.isSourceFile(node.parent) && node.parent.externalModuleIndicator === undefined) { + // You cannot extract global declarations + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.functionWillNotBeVisibleInTheNewScope)); + } + // falls through + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + // do not dive into functions or classes + return false; + } + + const savedPermittedJumps = permittedJumps; + switch (node.kind) { + case ts.SyntaxKind.IfStatement: + permittedJumps = PermittedJumps.None; + break; + case ts.SyntaxKind.TryStatement: + // forbid all jumps inside try blocks + permittedJumps = PermittedJumps.None; + break; + case ts.SyntaxKind.Block: + if (node.parent && node.parent.kind === ts.SyntaxKind.TryStatement && (node.parent as ts.TryStatement).finallyBlock === node) { + // allow unconditional returns from finally blocks + permittedJumps = PermittedJumps.Return; } - case ts.SyntaxKind.AwaitExpression: - rangeFacts |= RangeFacts.IsAsyncFunction; - break; - case ts.SyntaxKind.YieldExpression: - rangeFacts |= RangeFacts.IsGenerator; - break; - case ts.SyntaxKind.ReturnStatement: - if (permittedJumps & PermittedJumps.Return) { - rangeFacts |= RangeFacts.HasReturn; + break; + case ts.SyntaxKind.DefaultClause: + case ts.SyntaxKind.CaseClause: + // allow unlabeled break inside case clauses + permittedJumps |= PermittedJumps.Break; + break; + default: + if (ts.isIterationStatement(node, /*lookInLabeledStatements*/ false)) { + // allow unlabeled break/continue inside loops + permittedJumps |= PermittedJumps.Break | PermittedJumps.Continue; + } + break; + } + + switch (node.kind) { + case ts.SyntaxKind.ThisType: + case ts.SyntaxKind.ThisKeyword: + rangeFacts |= RangeFacts.UsesThis; + thisNode = node; + break; + case ts.SyntaxKind.LabeledStatement: { + const label = (node as ts.LabeledStatement).label; + (seenLabels || (seenLabels = [])).push(label.escapedText); + ts.forEachChild(node, visit); + seenLabels.pop(); + break; + } + case ts.SyntaxKind.BreakStatement: + case ts.SyntaxKind.ContinueStatement: { + const label = (node as ts.BreakStatement | ts.ContinueStatement).label; + if (label) { + if (!ts.contains(seenLabels, label.escapedText)) { + // attempts to jump to label that is not in range to be extracted + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange)); } - else { - (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalReturnStatement)); + } + else { + if (!(permittedJumps & (node.kind === ts.SyntaxKind.BreakStatement ? PermittedJumps.Break : PermittedJumps.Continue))) { + // attempt to break or continue in a forbidden context + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements)); } - break; - default: - ts.forEachChild(node, visit); - break; + } + break; } - - permittedJumps = savedPermittedJumps; + case ts.SyntaxKind.AwaitExpression: + rangeFacts |= RangeFacts.IsAsyncFunction; + break; + case ts.SyntaxKind.YieldExpression: + rangeFacts |= RangeFacts.IsGenerator; + break; + case ts.SyntaxKind.ReturnStatement: + if (permittedJumps & PermittedJumps.Return) { + rangeFacts |= RangeFacts.HasReturn; + } + else { + (errors ||= []).push(ts.createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalReturnStatement)); + } + break; + default: + ts.forEachChild(node, visit); + break; } - } - } - /** - * Includes the final semicolon so that the span covers statements in cases where it would otherwise - * only cover the declaration list. - */ - function getAdjustedSpanFromNodes(startNode: ts.Node, endNode: ts.Node, sourceFile: ts.SourceFile): ts.TextSpan { - const start = startNode.getStart(sourceFile); - let end = endNode.getEnd(); - if (sourceFile.text.charCodeAt(end) === ts.CharacterCodes.semicolon) { - end++; + permittedJumps = savedPermittedJumps; } - return { start, length: end - start }; } +} - function getStatementOrExpressionRange(node: ts.Node): ts.Statement[] | ts.Expression | undefined { - if (ts.isStatement(node)) { - return [node]; - } - if (ts.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 ts.isExpressionStatement(node.parent) ? [node.parent] : node as ts.Expression; - } - if (isStringLiteralJsxAttribute(node)) { - return node; - } - return undefined; +/** + * Includes the final semicolon so that the span covers statements in cases where it would otherwise + * only cover the declaration list. + */ +function getAdjustedSpanFromNodes(startNode: ts.Node, endNode: ts.Node, sourceFile: ts.SourceFile): ts.TextSpan { + const start = startNode.getStart(sourceFile); + let end = endNode.getEnd(); + if (sourceFile.text.charCodeAt(end) === ts.CharacterCodes.semicolon) { + end++; } + return { start, length: end - start }; +} - function isScope(node: ts.Node): node is Scope { - return ts.isArrowFunction(node) ? ts.isFunctionBody(node.body) : - ts.isFunctionLikeDeclaration(node) || ts.isSourceFile(node) || ts.isModuleBlock(node) || ts.isClassLike(node); +function getStatementOrExpressionRange(node: ts.Node): ts.Statement[] | ts.Expression | undefined { + if (ts.isStatement(node)) { + return [node]; + } + if (ts.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 ts.isExpressionStatement(node.parent) ? [node.parent] : node as ts.Expression; + } + if (isStringLiteralJsxAttribute(node)) { + return node; } + return undefined; +} - /** - * 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: ts.Node = isReadonlyArray(range.range) ? ts.first(range.range) : range.range; - if (range.facts & RangeFacts.UsesThis && !(range.facts & RangeFacts.UsesThisInFunction)) { - // 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 = ts.getContainingClass(current); - if (containingClass) { - const containingFunction = ts.findAncestor(current, ts.isFunctionLikeDeclaration); - return containingFunction - ? [containingFunction, containingClass] - : [containingClass]; - } +function isScope(node: ts.Node): node is Scope { + return ts.isArrowFunction(node) ? ts.isFunctionBody(node.body) : + ts.isFunctionLikeDeclaration(node) || ts.isSourceFile(node) || ts.isModuleBlock(node) || ts.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. + */ +function collectEnclosingScopes(range: TargetRange): Scope[] { + let current: ts.Node = isReadonlyArray(range.range) ? ts.first(range.range) : range.range; + if (range.facts & RangeFacts.UsesThis && !(range.facts & RangeFacts.UsesThisInFunction)) { + // 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 = ts.getContainingClass(current); + if (containingClass) { + const containingFunction = ts.findAncestor(current, ts.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 === ts.SyntaxKind.Parameter) { - // Skip all the way to the outer scope of the function that declared this parameter - current = ts.findAncestor(current, parent => ts.isFunctionLikeDeclaration(parent))!.parent; - } + 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 === ts.SyntaxKind.Parameter) { + // Skip all the way to the outer scope of the function that declared this parameter + current = ts.findAncestor(current, parent => ts.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 === ts.SyntaxKind.SourceFile) { - return scopes; - } + // 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 === ts.SyntaxKind.SourceFile) { + return scopes; } } } +} - function getFunctionExtractionAtIndex(targetRange: TargetRange, context: ts.RefactorContext, requestedChangesIndex: number): ts.RefactorEditInfo { - const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); - ts.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: ts.RefactorContext, requestedChangesIndex: number): ts.RefactorEditInfo { - const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); - ts.Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); - ts.Debug.assert(exposedVariableDeclarations.length === 0, "Extract constant accepted a range containing a variable declaration?"); - context.cancellationToken!.throwIfCancellationRequested(); - const expression = ts.isExpression(target) - ? target - : (target.statements[0] as ts.ExpressionStatement).expression; - return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context); - } +function getFunctionExtractionAtIndex(targetRange: TargetRange, context: ts.RefactorContext, requestedChangesIndex: number): ts.RefactorEditInfo { + const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); + ts.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); +} - interface Extraction { - readonly description: string; - readonly errors: readonly ts.Diagnostic[]; - } +function getConstantExtractionAtIndex(targetRange: TargetRange, context: ts.RefactorContext, requestedChangesIndex: number): ts.RefactorEditInfo { + const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); + ts.Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); + ts.Debug.assert(exposedVariableDeclarations.length === 0, "Extract constant accepted a range containing a variable declaration?"); + context.cancellationToken!.throwIfCancellationRequested(); + const expression = ts.isExpression(target) + ? target + : (target.statements[0] as ts.ExpressionStatement).expression; + return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context); +} - interface ScopeExtractions { - readonly functionExtraction: Extraction; - readonly constantExtraction: Extraction; - } +interface Extraction { + readonly description: string; + readonly errors: readonly ts.Diagnostic[]; +} - /** - * 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: ts.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 = ts.isFunctionLikeDeclaration(scope) - ? getDescriptionForFunctionLikeDeclaration(scope) - : ts.isClassLike(scope) - ? getDescriptionForClassLikeDeclaration(scope) - : getDescriptionForModuleLikeDeclaration(scope); - - let functionDescription: string; - let constantDescription: string; - if (scopeDescription === SpecialScope.Global) { - functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "global"]); - constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "global"]); - } - else if (scopeDescription === SpecialScope.Module) { - functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "module"]); - constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "module"]); - } - else { - functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1), [functionDescriptionPart, scopeDescription]); - constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1), [constantDescriptionPart, scopeDescription]); - } +interface ScopeExtractions { + readonly functionExtraction: Extraction; + readonly constantExtraction: Extraction; +} - // Customize the phrasing for the innermost scope to increase clarity. - if (i === 0 && !ts.isClassLike(scope)) { - constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_enclosing_scope), [constantDescriptionPart]); - } +/** + * 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: ts.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 = ts.isFunctionLikeDeclaration(scope) + ? getDescriptionForFunctionLikeDeclaration(scope) + : ts.isClassLike(scope) + ? getDescriptionForClassLikeDeclaration(scope) + : getDescriptionForModuleLikeDeclaration(scope); - return { - functionExtraction: { - description: functionDescription, - errors: functionErrorsPerScope[i], - }, - constantExtraction: { - description: constantDescription, - errors: constantErrorsPerScope[i], - }, - }; - }); - return extractions; - } + let functionDescription: string; + let constantDescription: string; + if (scopeDescription === SpecialScope.Global) { + functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "global"]); + constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "global"]); + } + else if (scopeDescription === SpecialScope.Module) { + functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "module"]); + constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "module"]); + } + else { + functionDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1), [functionDescriptionPart, scopeDescription]); + constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_1), [constantDescriptionPart, scopeDescription]); + } - function getPossibleExtractionsWorker(targetRange: TargetRange, context: ts.RefactorContext): { - readonly scopes: Scope[]; - readonly readsAndWrites: ReadsAndWrites; - } { - const { file: sourceFile } = context; + // Customize the phrasing for the innermost scope to increase clarity. + if (i === 0 && !ts.isClassLike(scope)) { + constantDescription = ts.formatStringFromArgs(ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_0_in_enclosing_scope), [constantDescriptionPart]); + } - const scopes = collectEnclosingScopes(targetRange); - const enclosingTextRange = getEnclosingTextRange(targetRange, sourceFile); - const readsAndWrites = collectReadsAndWrites(targetRange, scopes, enclosingTextRange, sourceFile, context.program.getTypeChecker(), context.cancellationToken!); - return { scopes, readsAndWrites }; - } + return { + functionExtraction: { + description: functionDescription, + errors: functionErrorsPerScope[i], + }, + constantExtraction: { + description: constantDescription, + errors: constantErrorsPerScope[i], + }, + }; + }); + return extractions; +} - function getDescriptionForFunctionInScope(scope: Scope): string { - return ts.isFunctionLikeDeclaration(scope) - ? "inner function" - : ts.isClassLike(scope) - ? "method" - : "function"; - } - function getDescriptionForConstantInScope(scope: Scope): string { - return ts.isClassLike(scope) - ? "readonly field" - : "constant"; - } - function getDescriptionForFunctionLikeDeclaration(scope: ts.FunctionLikeDeclaration): string { - switch (scope.kind) { - case ts.SyntaxKind.Constructor: - return "constructor"; - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - return scope.name - ? `function '${scope.name.text}'` - : ts.ANONYMOUS; - case ts.SyntaxKind.ArrowFunction: - return "arrow function"; - case ts.SyntaxKind.MethodDeclaration: - return `method '${scope.name.getText()}'`; - case ts.SyntaxKind.GetAccessor: - return `'get ${scope.name.getText()}'`; - case ts.SyntaxKind.SetAccessor: - return `'set ${scope.name.getText()}'`; - default: - throw ts.Debug.assertNever(scope, `Unexpected scope kind ${(scope as ts.FunctionLikeDeclaration).kind}`); - } - } - function getDescriptionForClassLikeDeclaration(scope: ts.ClassLikeDeclaration): string { - return scope.kind === ts.SyntaxKind.ClassDeclaration - ? scope.name ? `class '${scope.name.text}'` : "anonymous class declaration" - : scope.name ? `class expression '${scope.name.text}'` : "anonymous class expression"; - } - function getDescriptionForModuleLikeDeclaration(scope: ts.SourceFile | ts.ModuleBlock): string | SpecialScope { - return scope.kind === ts.SyntaxKind.ModuleBlock - ? `namespace '${scope.parent.name.getText()}'` - : scope.externalModuleIndicator ? SpecialScope.Module : SpecialScope.Global; - } - - const enum SpecialScope { - Module, - Global - } +function getPossibleExtractionsWorker(targetRange: TargetRange, context: ts.RefactorContext): { + readonly scopes: Scope[]; + readonly readsAndWrites: ReadsAndWrites; +} { + const { file: sourceFile } = context; - /** - * 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: ts.Statement | ts.Expression | ts.Block, scope: Scope, { usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages, exposedVariableDeclarations: readonly ts.VariableDeclaration[], range: TargetRange, context: ts.RefactorContext): ts.RefactorEditInfo { - - const checker = context.program.getTypeChecker(); - const scriptTarget = ts.getEmitScriptTarget(context.program.getCompilerOptions()); - const importAdder = ts.codefix.createImportAdder(context.file, context.program, context.preferences, context.host); - - // Make a unique name for the extracted function - const file = scope.getSourceFile(); - const functionNameText = ts.getUniqueName(ts.isClassLike(scope) ? "newMethod" : "newFunction", file); - const isJS = ts.isInJSFile(scope); - const functionName = ts.factory.createIdentifier(functionNameText); - let returnType: ts.TypeNode | undefined; - const parameters: ts.ParameterDeclaration[] = []; - const callArguments: ts.Identifier[] = []; - let writes: UsageEntry[] | undefined; - usagesInScope.forEach((usage, name) => { - let typeNode: ts.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 = ts.codefix.typeToAutoImportableTypeNode(checker, importAdder, type, scope, scriptTarget, ts.NodeBuilderFlags.NoTruncation); - } + const scopes = collectEnclosingScopes(targetRange); + const enclosingTextRange = getEnclosingTextRange(targetRange, sourceFile); + const readsAndWrites = collectReadsAndWrites(targetRange, scopes, enclosingTextRange, sourceFile, context.program.getTypeChecker(), context.cancellationToken!); + return { scopes, readsAndWrites }; +} - const paramDecl = ts.factory.createParameterDeclaration( - /*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(ts.factory.createIdentifier(name)); - }); +function getDescriptionForFunctionInScope(scope: Scope): string { + return ts.isFunctionLikeDeclaration(scope) + ? "inner function" + : ts.isClassLike(scope) + ? "method" + : "function"; +} +function getDescriptionForConstantInScope(scope: Scope): string { + return ts.isClassLike(scope) + ? "readonly field" + : "constant"; +} +function getDescriptionForFunctionLikeDeclaration(scope: ts.FunctionLikeDeclaration): string { + switch (scope.kind) { + case ts.SyntaxKind.Constructor: + return "constructor"; + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + return scope.name + ? `function '${scope.name.text}'` + : ts.ANONYMOUS; + case ts.SyntaxKind.ArrowFunction: + return "arrow function"; + case ts.SyntaxKind.MethodDeclaration: + return `method '${scope.name.getText()}'`; + case ts.SyntaxKind.GetAccessor: + return `'get ${scope.name.getText()}'`; + case ts.SyntaxKind.SetAccessor: + return `'set ${scope.name.getText()}'`; + default: + throw ts.Debug.assertNever(scope, `Unexpected scope kind ${(scope as ts.FunctionLikeDeclaration).kind}`); + } +} +function getDescriptionForClassLikeDeclaration(scope: ts.ClassLikeDeclaration): string { + return scope.kind === ts.SyntaxKind.ClassDeclaration + ? scope.name ? `class '${scope.name.text}'` : "anonymous class declaration" + : scope.name ? `class expression '${scope.name.text}'` : "anonymous class expression"; +} +function getDescriptionForModuleLikeDeclaration(scope: ts.SourceFile | ts.ModuleBlock): string | SpecialScope { + return scope.kind === ts.SyntaxKind.ModuleBlock + ? `namespace '${scope.parent.name.getText()}'` + : scope.externalModuleIndicator ? SpecialScope.Module : SpecialScope.Global; +} - const typeParametersAndDeclarations = ts.arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) })); - const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder); +const enum SpecialScope { + Module, + Global +} - const typeParameters: readonly ts.TypeParameterDeclaration[] | undefined = sortedTypeParametersAndDeclarations.length === 0 - ? undefined - : sortedTypeParametersAndDeclarations.map(t => t.declaration as ts.TypeParameterDeclaration); +/** + * 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: ts.Statement | ts.Expression | ts.Block, scope: Scope, { usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages, exposedVariableDeclarations: readonly ts.VariableDeclaration[], range: TargetRange, context: ts.RefactorContext): ts.RefactorEditInfo { + + const checker = context.program.getTypeChecker(); + const scriptTarget = ts.getEmitScriptTarget(context.program.getCompilerOptions()); + const importAdder = ts.codefix.createImportAdder(context.file, context.program, context.preferences, context.host); + + // Make a unique name for the extracted function + const file = scope.getSourceFile(); + const functionNameText = ts.getUniqueName(ts.isClassLike(scope) ? "newMethod" : "newFunction", file); + const isJS = ts.isInJSFile(scope); + const functionName = ts.factory.createIdentifier(functionNameText); + let returnType: ts.TypeNode | undefined; + const parameters: ts.ParameterDeclaration[] = []; + const callArguments: ts.Identifier[] = []; + let writes: UsageEntry[] | undefined; + usagesInScope.forEach((usage, name) => { + let typeNode: ts.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 = ts.codefix.typeToAutoImportableTypeNode(checker, importAdder, type, scope, scriptTarget, ts.NodeBuilderFlags.NoTruncation); + } + + const paramDecl = ts.factory.createParameterDeclaration( + /*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(ts.factory.createIdentifier(name)); + }); - // 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 ts.TypeNode[] | undefined = typeParameters !== undefined - ? typeParameters.map(decl => ts.factory.createTypeReferenceNode(decl.name, /*typeArguments*/ undefined)) - : undefined; + const typeParametersAndDeclarations = ts.arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) })); + const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder); - // Provide explicit return types for contextually-typed functions - // to avoid problems when there are literal types present - if (ts.isExpression(node) && !isJS) { - const contextualType = checker.getContextualType(node); - returnType = checker.typeToTypeNode(contextualType!, scope, ts.NodeBuilderFlags.NoTruncation); // TODO: GH#18217 - } + const typeParameters: readonly ts.TypeParameterDeclaration[] | undefined = sortedTypeParametersAndDeclarations.length === 0 + ? undefined + : sortedTypeParametersAndDeclarations.map(t => t.declaration as ts.TypeParameterDeclaration); - const { body, returnValueProperty } = transformFunctionBody(node, exposedVariableDeclarations, writes, substitutions, !!(range.facts & RangeFacts.HasReturn)); - ts.suppressLeadingAndTrailingTrivia(body); - let newFunction: ts.MethodDeclaration | ts.FunctionDeclaration; + // 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 ts.TypeNode[] | undefined = typeParameters !== undefined + ? typeParameters.map(decl => ts.factory.createTypeReferenceNode(decl.name, /*typeArguments*/ undefined)) + : undefined; - const callThis = !!(range.facts & RangeFacts.UsesThisInFunction); + // Provide explicit return types for contextually-typed functions + // to avoid problems when there are literal types present + if (ts.isExpression(node) && !isJS) { + const contextualType = checker.getContextualType(node); + returnType = checker.typeToTypeNode(contextualType!, scope, ts.NodeBuilderFlags.NoTruncation); // TODO: GH#18217 + } - if (ts.isClassLike(scope)) { - // always create private method in TypeScript files - const modifiers: ts.Modifier[] = isJS ? [] : [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)]; - if (range.facts & RangeFacts.InStaticRegion) { - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); - } - if (range.facts & RangeFacts.IsAsyncFunction) { - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)); - } - newFunction = ts.factory.createMethodDeclaration( - /*decorators*/ undefined, modifiers.length ? modifiers : undefined, range.facts & RangeFacts.IsGenerator ? ts.factory.createToken(ts.SyntaxKind.AsteriskToken) : undefined, functionName, - /*questionToken*/ undefined, typeParameters, parameters, returnType, body); - } - else { - if (callThis) { - parameters.unshift(ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - /*name*/ "this", - /*questionToken*/ undefined, checker.typeToTypeNode(checker.getTypeAtLocation(range.thisNode!), scope, ts.NodeBuilderFlags.NoTruncation), - /*initializer*/ undefined)); - } - newFunction = ts.factory.createFunctionDeclaration( - /*decorators*/ undefined, range.facts & RangeFacts.IsAsyncFunction ? [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)] : undefined, range.facts & RangeFacts.IsGenerator ? ts.factory.createToken(ts.SyntaxKind.AsteriskToken) : undefined, functionName, typeParameters, parameters, returnType, body); - } - const changeTracker = ts.textChanges.ChangeTracker.fromContext(context); - const minInsertionPos = (isReadonlyArray(range.range) ? ts.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); - } - importAdder.writeFixes(changeTracker); + const { body, returnValueProperty } = transformFunctionBody(node, exposedVariableDeclarations, writes, substitutions, !!(range.facts & RangeFacts.HasReturn)); + ts.suppressLeadingAndTrailingTrivia(body); + let newFunction: ts.MethodDeclaration | ts.FunctionDeclaration; - const newNodes: ts.Node[] = []; - // replace range with function call - const called = getCalledExpression(scope, range, functionNameText); + const callThis = !!(range.facts & RangeFacts.UsesThisInFunction); - if (callThis) { - callArguments.unshift(ts.factory.createIdentifier("this")); - } - let call: ts.Expression = ts.factory.createCallExpression(callThis ? ts.factory.createPropertyAccessExpression(called, "call") : called, callTypeArguments, // Note that no attempt is made to take advantage of type argument inference - callArguments); - if (range.facts & RangeFacts.IsGenerator) { - call = ts.factory.createYieldExpression(ts.factory.createToken(ts.SyntaxKind.AsteriskToken), call); + if (ts.isClassLike(scope)) { + // always create private method in TypeScript files + const modifiers: ts.Modifier[] = isJS ? [] : [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)]; + if (range.facts & RangeFacts.InStaticRegion) { + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); } if (range.facts & RangeFacts.IsAsyncFunction) { - call = ts.factory.createAwaitExpression(call); - } - if (isInJSXContent(node)) { - call = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, call); + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)); } + newFunction = ts.factory.createMethodDeclaration( + /*decorators*/ undefined, modifiers.length ? modifiers : undefined, range.facts & RangeFacts.IsGenerator ? ts.factory.createToken(ts.SyntaxKind.AsteriskToken) : undefined, functionName, + /*questionToken*/ undefined, typeParameters, parameters, returnType, body); + } + else { + if (callThis) { + parameters.unshift(ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + /*name*/ "this", + /*questionToken*/ undefined, checker.typeToTypeNode(checker.getTypeAtLocation(range.thisNode!), scope, ts.NodeBuilderFlags.NoTruncation), + /*initializer*/ undefined)); + } + newFunction = ts.factory.createFunctionDeclaration( + /*decorators*/ undefined, range.facts & RangeFacts.IsAsyncFunction ? [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)] : undefined, range.facts & RangeFacts.IsGenerator ? ts.factory.createToken(ts.SyntaxKind.AsteriskToken) : undefined, functionName, typeParameters, parameters, returnType, body); + } + const changeTracker = ts.textChanges.ChangeTracker.fromContext(context); + const minInsertionPos = (isReadonlyArray(range.range) ? ts.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); + } + importAdder.writeFixes(changeTracker); - if (exposedVariableDeclarations.length && !writes) { - // No need to mix declarations and writes. + const newNodes: ts.Node[] = []; + // replace range with function call + const called = getCalledExpression(scope, range, functionNameText); - // How could any variables be exposed if there's a return statement? - ts.Debug.assert(!returnValueProperty, "Expected no returnValueProperty"); - ts.Debug.assert(!(range.facts & RangeFacts.HasReturn), "Expected RangeFacts.HasReturn flag to be unset"); + if (callThis) { + callArguments.unshift(ts.factory.createIdentifier("this")); + } + let call: ts.Expression = ts.factory.createCallExpression(callThis ? ts.factory.createPropertyAccessExpression(called, "call") : called, callTypeArguments, // Note that no attempt is made to take advantage of type argument inference + callArguments); + if (range.facts & RangeFacts.IsGenerator) { + call = ts.factory.createYieldExpression(ts.factory.createToken(ts.SyntaxKind.AsteriskToken), call); + } + if (range.facts & RangeFacts.IsAsyncFunction) { + call = ts.factory.createAwaitExpression(call); + } + if (isInJSXContent(node)) { + call = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, call); + } - if (exposedVariableDeclarations.length === 1) { - // Declaring exactly one variable: let x = newFunction(); - const variableDeclaration = exposedVariableDeclarations[0]; - newNodes.push(ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.getSynthesizedDeepClone(variableDeclaration.name), /*exclamationToken*/ undefined, /*type*/ ts.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: ts.BindingElement[] = []; - const typeElements: ts.TypeElement[] = []; - let commonNodeFlags = exposedVariableDeclarations[0].parent.flags; - let sawExplicitType = false; - for (const variableDeclaration of exposedVariableDeclarations) { - bindingElements.push(ts.factory.createBindingElement( - /*dotDotDotToken*/ undefined, - /*propertyName*/ undefined, - /*name*/ ts.getSynthesizedDeepClone(variableDeclaration.name))); - - // Being returned through an object literal will have widened the type. - const variableType: ts.TypeNode | undefined = checker.typeToTypeNode(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(variableDeclaration)), scope, ts.NodeBuilderFlags.NoTruncation); - typeElements.push(ts.factory.createPropertySignature( - /*modifiers*/ undefined, - /*name*/ variableDeclaration.symbol.name, - /*questionToken*/ undefined, - /*type*/ variableType)); - sawExplicitType = sawExplicitType || variableDeclaration.type !== undefined; - commonNodeFlags = commonNodeFlags & variableDeclaration.parent.flags; - } + if (exposedVariableDeclarations.length && !writes) { + // No need to mix declarations and writes. - const typeLiteral: ts.TypeLiteralNode | undefined = sawExplicitType ? ts.factory.createTypeLiteralNode(typeElements) : undefined; - if (typeLiteral) { - ts.setEmitFlags(typeLiteral, ts.EmitFlags.SingleLine); - } + // How could any variables be exposed if there's a return statement? + ts.Debug.assert(!returnValueProperty, "Expected no returnValueProperty"); + ts.Debug.assert(!(range.facts & RangeFacts.HasReturn), "Expected RangeFacts.HasReturn flag to be unset"); - newNodes.push(ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createObjectBindingPattern(bindingElements), - /*exclamationToken*/ undefined, - /*type*/ typeLiteral, - /*initializer*/ call)], commonNodeFlags))); - } + if (exposedVariableDeclarations.length === 1) { + // Declaring exactly one variable: let x = newFunction(); + const variableDeclaration = exposedVariableDeclarations[0]; + newNodes.push(ts.factory.createVariableStatement( + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.getSynthesizedDeepClone(variableDeclaration.name), /*exclamationToken*/ undefined, /*type*/ ts.getSynthesizedDeepClone(variableDeclaration.type), /*initializer*/ call)], // TODO (acasey): test binding patterns + variableDeclaration.parent.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: ts.NodeFlags = variableDeclaration.parent.flags; - if (flags & ts.NodeFlags.Const) { - flags = (flags & ~ts.NodeFlags.Const) | ts.NodeFlags.Let; - } - - newNodes.push(ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(variableDeclaration.symbol.name, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(variableDeclaration.type))], flags))); + else { + // Declaring multiple variables / return properties: + // let {x, y} = newFunction(); + const bindingElements: ts.BindingElement[] = []; + const typeElements: ts.TypeElement[] = []; + let commonNodeFlags = exposedVariableDeclarations[0].parent.flags; + let sawExplicitType = false; + for (const variableDeclaration of exposedVariableDeclarations) { + bindingElements.push(ts.factory.createBindingElement( + /*dotDotDotToken*/ undefined, + /*propertyName*/ undefined, + /*name*/ ts.getSynthesizedDeepClone(variableDeclaration.name))); + + // Being returned through an object literal will have widened the type. + const variableType: ts.TypeNode | undefined = checker.typeToTypeNode(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(variableDeclaration)), scope, ts.NodeBuilderFlags.NoTruncation); + typeElements.push(ts.factory.createPropertySignature( + /*modifiers*/ undefined, + /*name*/ variableDeclaration.symbol.name, + /*questionToken*/ undefined, + /*type*/ variableType)); + sawExplicitType = sawExplicitType || variableDeclaration.type !== undefined; + commonNodeFlags = commonNodeFlags & variableDeclaration.parent.flags; + } + + const typeLiteral: ts.TypeLiteralNode | undefined = sawExplicitType ? ts.factory.createTypeLiteralNode(typeElements) : undefined; + if (typeLiteral) { + ts.setEmitFlags(typeLiteral, ts.EmitFlags.SingleLine); + } + + newNodes.push(ts.factory.createVariableStatement( + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createObjectBindingPattern(bindingElements), + /*exclamationToken*/ undefined, + /*type*/ typeLiteral, + /*initializer*/ call)], commonNodeFlags))); + } + } + 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: ts.NodeFlags = variableDeclaration.parent.flags; + if (flags & ts.NodeFlags.Const) { + flags = (flags & ~ts.NodeFlags.Const) | ts.NodeFlags.Let; } - } - if (returnValueProperty) { - // has both writes and return, need to create variable declaration to hold return value; newNodes.push(ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(returnValueProperty, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(returnType))], ts.NodeFlags.Let))); + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(variableDeclaration.symbol.name, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(variableDeclaration.type))], flags))); } + } - const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); - if (returnValueProperty) { - assignments.unshift(ts.factory.createShorthandPropertyAssignment(returnValueProperty)); - } + if (returnValueProperty) { + // has both writes and return, need to create variable declaration to hold return value; + newNodes.push(ts.factory.createVariableStatement( + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(returnValueProperty, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(returnType))], ts.NodeFlags.Let))); + } - // propagate writes back - if (assignments.length === 1) { - // We would only have introduced a return value property if there had been - // other assignments to make. - ts.Debug.assert(!returnValueProperty, "Shouldn't have returnValueProperty here"); - newNodes.push(ts.factory.createExpressionStatement(ts.factory.createAssignment(assignments[0].name, call))); + const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); + if (returnValueProperty) { + assignments.unshift(ts.factory.createShorthandPropertyAssignment(returnValueProperty)); + } - if (range.facts & RangeFacts.HasReturn) { - newNodes.push(ts.factory.createReturnStatement()); - } - } - else { - // emit e.g. - // { a, b, __return } = newFunction(a, b); - // return __return; - newNodes.push(ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createObjectLiteralExpression(assignments), call))); - if (returnValueProperty) { - newNodes.push(ts.factory.createReturnStatement(ts.factory.createIdentifier(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. + ts.Debug.assert(!returnValueProperty, "Shouldn't have returnValueProperty here"); + newNodes.push(ts.factory.createExpressionStatement(ts.factory.createAssignment(assignments[0].name, call))); + + if (range.facts & RangeFacts.HasReturn) { + newNodes.push(ts.factory.createReturnStatement()); } } else { - if (range.facts & RangeFacts.HasReturn) { - newNodes.push(ts.factory.createReturnStatement(call)); - } - else if (isReadonlyArray(range.range)) { - newNodes.push(ts.factory.createExpressionStatement(call)); - } - else { - newNodes.push(call); + // emit e.g. + // { a, b, __return } = newFunction(a, b); + // return __return; + newNodes.push(ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createObjectLiteralExpression(assignments), call))); + if (returnValueProperty) { + newNodes.push(ts.factory.createReturnStatement(ts.factory.createIdentifier(returnValueProperty))); } } - - if (isReadonlyArray(range.range)) { - changeTracker.replaceNodeRangeWithNodes(context.file, ts.first(range.range), ts.last(range.range), newNodes); + } + else { + if (range.facts & RangeFacts.HasReturn) { + newNodes.push(ts.factory.createReturnStatement(call)); + } + else if (isReadonlyArray(range.range)) { + newNodes.push(ts.factory.createExpressionStatement(call)); } else { - changeTracker.replaceNodeWithNodes(context.file, range.range, newNodes); + newNodes.push(call); } + } + + if (isReadonlyArray(range.range)) { + changeTracker.replaceNodeRangeWithNodes(context.file, ts.first(range.range), ts.last(range.range), newNodes); + } + else { + changeTracker.replaceNodeWithNodes(context.file, range.range, newNodes); + } - const edits = changeTracker.getChanges(); - const renameRange = isReadonlyArray(range.range) ? ts.first(range.range) : range.range; + const edits = changeTracker.getChanges(); + const renameRange = isReadonlyArray(range.range) ? ts.first(range.range) : range.range; - const renameFilename = renameRange.getSourceFile().fileName; - const renameLocation = ts.getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false); - return { renameFilename, renameLocation, edits }; + const renameFilename = renameRange.getSourceFile().fileName; + const renameLocation = ts.getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false); + return { renameFilename, renameLocation, edits }; - function getTypeDeepCloneUnionUndefined(typeNode: ts.TypeNode | undefined): ts.TypeNode | undefined { - if (typeNode === undefined) { - return undefined; - } + function getTypeDeepCloneUnionUndefined(typeNode: ts.TypeNode | undefined): ts.TypeNode | undefined { + if (typeNode === undefined) { + return undefined; + } - const clone = ts.getSynthesizedDeepClone(typeNode); - let withoutParens = clone; - while (ts.isParenthesizedTypeNode(withoutParens)) { - withoutParens = withoutParens.type; - } - return ts.isUnionTypeNode(withoutParens) && ts.find(withoutParens.types, t => t.kind === ts.SyntaxKind.UndefinedKeyword) - ? clone - : ts.factory.createUnionTypeNode([clone, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)]); + const clone = ts.getSynthesizedDeepClone(typeNode); + let withoutParens = clone; + while (ts.isParenthesizedTypeNode(withoutParens)) { + withoutParens = withoutParens.type; + } + return ts.isUnionTypeNode(withoutParens) && ts.find(withoutParens.types, t => t.kind === ts.SyntaxKind.UndefinedKeyword) + ? clone + : ts.factory.createUnionTypeNode([clone, ts.factory.createKeywordTypeNode(ts.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 + */ +function extractConstantInScope(node: ts.Expression, scope: Scope, { substitutions }: ScopeUsages, rangeFacts: RangeFacts, context: ts.RefactorContext): ts.RefactorEditInfo { + + const checker = context.program.getTypeChecker(); + + // Make a unique name for the extracted variable + const file = scope.getSourceFile(); + const localNameText = ts.isPropertyAccessExpression(node) && !ts.isClassLike(scope) && !checker.resolveName(node.name.text, node, ts.SymbolFlags.Value, /*excludeGlobals*/ false) && !ts.isPrivateIdentifier(node.name) && !ts.isKeyword(node.name.originalKeywordKind!) + ? node.name.text + : ts.getUniqueName(ts.isClassLike(scope) ? "newProperty" : "newLocal", file); + const isJS = ts.isInJSFile(scope); + + let variableType = isJS || !checker.isContextSensitive(node) + ? undefined + : checker.typeToTypeNode(checker.getContextualType(node)!, scope, ts.NodeBuilderFlags.NoTruncation); // TODO: GH#18217 + let initializer = transformConstantInitializer(ts.skipParentheses(node), substitutions); + + ({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer)); + + ts.suppressLeadingAndTrailingTrivia(initializer); + const changeTracker = ts.textChanges.ChangeTracker.fromContext(context); + if (ts.isClassLike(scope)) { + ts.Debug.assert(!isJS, "Cannot extract to a JS class"); // See CannotExtractToJSClass + const modifiers: ts.Modifier[] = []; + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)); + if (rangeFacts & RangeFacts.InStaticRegion) { + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); + } + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); + const newVariable = ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, modifiers, localNameText, + /*questionToken*/ undefined, variableType, initializer); + let localReference: ts.Expression = ts.factory.createPropertyAccessExpression(rangeFacts & RangeFacts.InStaticRegion + ? ts.factory.createIdentifier(scope.name!.getText()) // TODO: GH#18217 + : ts.factory.createThis(), ts.factory.createIdentifier(localNameText)); + + if (isInJSXContent(node)) { + localReference = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); } - } - /** - * 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: ts.Expression, scope: Scope, { substitutions }: ScopeUsages, rangeFacts: RangeFacts, context: ts.RefactorContext): ts.RefactorEditInfo { - - const checker = context.program.getTypeChecker(); - - // Make a unique name for the extracted variable - const file = scope.getSourceFile(); - const localNameText = ts.isPropertyAccessExpression(node) && !ts.isClassLike(scope) && !checker.resolveName(node.name.text, node, ts.SymbolFlags.Value, /*excludeGlobals*/ false) && !ts.isPrivateIdentifier(node.name) && !ts.isKeyword(node.name.originalKeywordKind!) - ? node.name.text - : ts.getUniqueName(ts.isClassLike(scope) ? "newProperty" : "newLocal", file); - const isJS = ts.isInJSFile(scope); - - let variableType = isJS || !checker.isContextSensitive(node) - ? undefined - : checker.typeToTypeNode(checker.getContextualType(node)!, scope, ts.NodeBuilderFlags.NoTruncation); // TODO: GH#18217 - let initializer = transformConstantInitializer(ts.skipParentheses(node), substitutions); - - ({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer)); - - ts.suppressLeadingAndTrailingTrivia(initializer); - const changeTracker = ts.textChanges.ChangeTracker.fromContext(context); - if (ts.isClassLike(scope)) { - ts.Debug.assert(!isJS, "Cannot extract to a JS class"); // See CannotExtractToJSClass - const modifiers: ts.Modifier[] = []; - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)); - if (rangeFacts & RangeFacts.InStaticRegion) { - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); - } - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); - const newVariable = ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, modifiers, localNameText, - /*questionToken*/ undefined, variableType, initializer); - let localReference: ts.Expression = ts.factory.createPropertyAccessExpression(rangeFacts & RangeFacts.InStaticRegion - ? ts.factory.createIdentifier(scope.name!.getText()) // TODO: GH#18217 - : ts.factory.createThis(), ts.factory.createIdentifier(localNameText)); - - if (isInJSXContent(node)) { - localReference = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); - } + // 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 = ts.factory.createVariableDeclaration(localNameText, /*exclamationToken*/ undefined, 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 = ts.factory.createIdentifier(localNameText); changeTracker.replaceNode(context.file, node, localReference); } + else if (node.parent.kind === ts.SyntaxKind.ExpressionStatement && scope === ts.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 = ts.factory.createVariableStatement( + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([newVariableDeclaration], ts.NodeFlags.Const)); + changeTracker.replaceNode(context.file, node.parent, newVariableStatement); + } else { - const newVariableDeclaration = ts.factory.createVariableDeclaration(localNameText, /*exclamationToken*/ undefined, 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 = ts.factory.createIdentifier(localNameText); - changeTracker.replaceNode(context.file, node, localReference); - } - else if (node.parent.kind === ts.SyntaxKind.ExpressionStatement && scope === ts.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 = ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([newVariableDeclaration], ts.NodeFlags.Const)); - changeTracker.replaceNode(context.file, node.parent, newVariableStatement); + const newVariableStatement = ts.factory.createVariableStatement( + /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([newVariableDeclaration], ts.NodeFlags.Const)); + + // Declare + const nodeToInsertBefore = getNodeToInsertConstantBefore(node, scope); + if (nodeToInsertBefore.pos === 0) { + changeTracker.insertNodeAtTopOfFile(context.file, newVariableStatement, /*blankLineBetween*/ false); } else { - const newVariableStatement = ts.factory.createVariableStatement( - /*modifiers*/ undefined, ts.factory.createVariableDeclarationList([newVariableDeclaration], ts.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); - } + changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariableStatement, /*blankLineBetween*/ false); + } - // Consume - if (node.parent.kind === ts.SyntaxKind.ExpressionStatement) { - // If the parent is an expression statement, delete it. - changeTracker.delete(context.file, node.parent); - } - else { - let localReference: ts.Expression = ts.factory.createIdentifier(localNameText); - // When extract to a new variable in JSX content, need to wrap a {} out of the new variable - // or it will become a plain text - if (isInJSXContent(node)) { - localReference = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); - } - changeTracker.replaceNode(context.file, node, localReference); + // Consume + if (node.parent.kind === ts.SyntaxKind.ExpressionStatement) { + // If the parent is an expression statement, delete it. + changeTracker.delete(context.file, node.parent); + } + else { + let localReference: ts.Expression = ts.factory.createIdentifier(localNameText); + // When extract to a new variable in JSX content, need to wrap a {} out of the new variable + // or it will become a plain text + if (isInJSXContent(node)) { + localReference = ts.factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); } + changeTracker.replaceNode(context.file, node, localReference); } } + } - const edits = changeTracker.getChanges(); - - const renameFilename = node.getSourceFile().fileName; - const renameLocation = ts.getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true); - return { renameFilename, renameLocation, edits }; - - function transformFunctionInitializerAndType(variableType: ts.TypeNode | undefined, initializer: ts.Expression): { - variableType: ts.TypeNode | undefined; - initializer: ts.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 (!ts.isFunctionExpression(initializer) && !ts.isArrowFunction(initializer) || !!initializer.typeParameters) - return { variableType, initializer }; - const functionType = checker.getTypeAtLocation(node); - const functionSignature = ts.singleOrUndefined(checker.getSignaturesOfType(functionType, ts.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: ts.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(ts.factory.updateParameterDeclaration(p, p.decorators, p.modifiers, p.dotDotDotToken, p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, ts.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 (ts.isArrowFunction(initializer)) { - initializer = ts.factory.updateArrowFunction(initializer, node.modifiers, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, ts.NodeBuilderFlags.NoTruncation), initializer.equalsGreaterThanToken, initializer.body); + const edits = changeTracker.getChanges(); + + const renameFilename = node.getSourceFile().fileName; + const renameLocation = ts.getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true); + return { renameFilename, renameLocation, edits }; + + function transformFunctionInitializerAndType(variableType: ts.TypeNode | undefined, initializer: ts.Expression): { + variableType: ts.TypeNode | undefined; + initializer: ts.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 (!ts.isFunctionExpression(initializer) && !ts.isArrowFunction(initializer) || !!initializer.typeParameters) + return { variableType, initializer }; + const functionType = checker.getTypeAtLocation(node); + const functionSignature = ts.singleOrUndefined(checker.getSignaturesOfType(functionType, ts.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: ts.ParameterDeclaration[] = []; + let hasAny = false; + for (const p of initializer.parameters) { + if (p.type) { + parameters.push(p); } else { - if (functionSignature && !!functionSignature.thisParameter) { - const firstParameter = ts.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 || (ts.isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) { - const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node); - parameters.splice(0, 0, ts.factory.createParameterDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, "this", - /* questionToken */ undefined, checker.typeToTypeNode(thisType, scope, ts.NodeBuilderFlags.NoTruncation))); - } - } - initializer = ts.factory.updateFunctionExpression(initializer, node.modifiers, initializer.asteriskToken, initializer.name, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, ts.NodeBuilderFlags.NoTruncation), initializer.body); + const paramType = checker.getTypeAtLocation(p); + if (paramType === checker.getAnyType()) + hasAny = true; + parameters.push(ts.factory.updateParameterDeclaration(p, p.decorators, p.modifiers, p.dotDotDotToken, p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, ts.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 (ts.isArrowFunction(initializer)) { + initializer = ts.factory.updateArrowFunction(initializer, node.modifiers, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, ts.NodeBuilderFlags.NoTruncation), initializer.equalsGreaterThanToken, initializer.body); + } + else { + if (functionSignature && !!functionSignature.thisParameter) { + const firstParameter = ts.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 || (ts.isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) { + const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node); + parameters.splice(0, 0, ts.factory.createParameterDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, "this", + /* questionToken */ undefined, checker.typeToTypeNode(thisType, scope, ts.NodeBuilderFlags.NoTruncation))); + } + } + initializer = ts.factory.updateFunctionExpression(initializer, node.modifiers, initializer.asteriskToken, initializer.name, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, ts.NodeBuilderFlags.NoTruncation), initializer.body); } + return { variableType, initializer }; } +} - function getContainingVariableDeclarationIfInList(node: ts.Node, scope: Scope) { - let prevNode; - while (node !== undefined && node !== scope) { - if (ts.isVariableDeclaration(node) && - node.initializer === prevNode && - ts.isVariableDeclarationList(node.parent) && - node.parent.declarations.length > 1) { - - return node; - } +function getContainingVariableDeclarationIfInList(node: ts.Node, scope: Scope) { + let prevNode; + while (node !== undefined && node !== scope) { + if (ts.isVariableDeclaration(node) && + node.initializer === prevNode && + ts.isVariableDeclarationList(node.parent) && + node.parent.declarations.length > 1) { - prevNode = node; - node = node.parent; + return node; } + + prevNode = node; + node = node.parent; } +} - function getFirstDeclaration(type: ts.Type): ts.Declaration | undefined { - let firstDeclaration; +function getFirstDeclaration(type: ts.Type): ts.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; - } + const symbol = type.symbol; + if (symbol && symbol.declarations) { + for (const declaration of symbol.declarations) { + if (firstDeclaration === undefined || declaration.pos < firstDeclaration.pos) { + firstDeclaration = declaration; } } - - return firstDeclaration; } - function compareTypesByDeclarationOrder({ type: type1, declaration: declaration1 }: { - type: ts.Type; - declaration?: ts.Declaration; - }, { type: type2, declaration: declaration2 }: { - type: ts.Type; - declaration?: ts.Declaration; - }) { - return ts.compareProperties(declaration1, declaration2, "pos", ts.compareValues) - || ts.compareStringsCaseSensitive(type1.symbol ? type1.symbol.getName() : "", type2.symbol ? type2.symbol.getName() : "") - || ts.compareValues(type1.id, type2.id); + return firstDeclaration; +} + +function compareTypesByDeclarationOrder({ type: type1, declaration: declaration1 }: { + type: ts.Type; + declaration?: ts.Declaration; +}, { type: type2, declaration: declaration2 }: { + type: ts.Type; + declaration?: ts.Declaration; +}) { + return ts.compareProperties(declaration1, declaration2, "pos", ts.compareValues) + || ts.compareStringsCaseSensitive(type1.symbol ? type1.symbol.getName() : "", type2.symbol ? type2.symbol.getName() : "") + || ts.compareValues(type1.id, type2.id); +} +function getCalledExpression(scope: ts.Node, range: TargetRange, functionNameText: string): ts.Expression { + const functionReference = ts.factory.createIdentifier(functionNameText); + if (ts.isClassLike(scope)) { + const lhs = range.facts & RangeFacts.InStaticRegion ? ts.factory.createIdentifier(scope.name!.text) : ts.factory.createThis(); // TODO: GH#18217 + return ts.factory.createPropertyAccessExpression(lhs, functionReference); } - function getCalledExpression(scope: ts.Node, range: TargetRange, functionNameText: string): ts.Expression { - const functionReference = ts.factory.createIdentifier(functionNameText); - if (ts.isClassLike(scope)) { - const lhs = range.facts & RangeFacts.InStaticRegion ? ts.factory.createIdentifier(scope.name!.text) : ts.factory.createThis(); // TODO: GH#18217 - return ts.factory.createPropertyAccessExpression(lhs, functionReference); - } - else { - return functionReference; - } + else { + return functionReference; } +} - function transformFunctionBody(body: ts.Node, exposedVariableDeclarations: readonly ts.VariableDeclaration[], writes: readonly UsageEntry[] | undefined, substitutions: ts.ReadonlyESMap, hasReturn: boolean): { - body: ts.Block; - returnValueProperty: string | undefined; - } { - const hasWritesOrVariableDeclarations = writes !== undefined || exposedVariableDeclarations.length > 0; - if (ts.isBlock(body) && !hasWritesOrVariableDeclarations && substitutions.size === 0) { - // already block, no declarations or writes to propagate back, no substitutions - can use node as is - return { body: ts.factory.createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; - } - let returnValueProperty: string | undefined; - let ignoreReturns = false; - const statements = ts.factory.createNodeArray(ts.isBlock(body) ? body.statements.slice(0) : [ts.isStatement(body) ? body : ts.factory.createReturnStatement(ts.skipParentheses(body as ts.Expression))]); - // 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 = ts.visitNodes(statements, visitor).slice(); - if (hasWritesOrVariableDeclarations && !hasReturn && ts.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(ts.factory.createReturnStatement(assignments[0].name)); - } - else { - rewrittenStatements.push(ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(assignments))); - } +function transformFunctionBody(body: ts.Node, exposedVariableDeclarations: readonly ts.VariableDeclaration[], writes: readonly UsageEntry[] | undefined, substitutions: ts.ReadonlyESMap, hasReturn: boolean): { + body: ts.Block; + returnValueProperty: string | undefined; +} { + const hasWritesOrVariableDeclarations = writes !== undefined || exposedVariableDeclarations.length > 0; + if (ts.isBlock(body) && !hasWritesOrVariableDeclarations && substitutions.size === 0) { + // already block, no declarations or writes to propagate back, no substitutions - can use node as is + return { body: ts.factory.createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; + } + let returnValueProperty: string | undefined; + let ignoreReturns = false; + const statements = ts.factory.createNodeArray(ts.isBlock(body) ? body.statements.slice(0) : [ts.isStatement(body) ? body : ts.factory.createReturnStatement(ts.skipParentheses(body as ts.Expression))]); + // 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 = ts.visitNodes(statements, visitor).slice(); + if (hasWritesOrVariableDeclarations && !hasReturn && ts.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(ts.factory.createReturnStatement(assignments[0].name)); + } + else { + rewrittenStatements.push(ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(assignments))); } - return { body: ts.factory.createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty }; - } - else { - return { body: ts.factory.createBlock(statements, /*multiLine*/ true), returnValueProperty: undefined }; } + return { body: ts.factory.createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty }; + } + else { + return { body: ts.factory.createBlock(statements, /*multiLine*/ true), returnValueProperty: undefined }; + } - function visitor(node: ts.Node): ts.VisitResult { - if (!ignoreReturns && ts.isReturnStatement(node) && hasWritesOrVariableDeclarations) { - const assignments: ts.ObjectLiteralElementLike[] = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); - if (node.expression) { - if (!returnValueProperty) { - returnValueProperty = "__return"; - } - assignments.unshift(ts.factory.createPropertyAssignment(returnValueProperty, ts.visitNode(node.expression, visitor))); - } - if (assignments.length === 1) { - return ts.factory.createReturnStatement(assignments[0].name as ts.Expression); - } - else { - return ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(assignments)); + function visitor(node: ts.Node): ts.VisitResult { + if (!ignoreReturns && ts.isReturnStatement(node) && hasWritesOrVariableDeclarations) { + const assignments: ts.ObjectLiteralElementLike[] = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); + if (node.expression) { + if (!returnValueProperty) { + returnValueProperty = "__return"; } + assignments.unshift(ts.factory.createPropertyAssignment(returnValueProperty, ts.visitNode(node.expression, visitor))); + } + if (assignments.length === 1) { + return ts.factory.createReturnStatement(assignments[0].name as ts.Expression); } else { - const oldIgnoreReturns = ignoreReturns; - ignoreReturns = ignoreReturns || ts.isFunctionLikeDeclaration(node) || ts.isClassLike(node); - const substitution = substitutions.get(ts.getNodeId(node).toString()); - const result = substitution ? ts.getSynthesizedDeepClone(substitution) : ts.visitEachChild(node, visitor, ts.nullTransformationContext); - ignoreReturns = oldIgnoreReturns; - return result; + return ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(assignments)); } } + else { + const oldIgnoreReturns = ignoreReturns; + ignoreReturns = ignoreReturns || ts.isFunctionLikeDeclaration(node) || ts.isClassLike(node); + const substitution = substitutions.get(ts.getNodeId(node).toString()); + const result = substitution ? ts.getSynthesizedDeepClone(substitution) : ts.visitEachChild(node, visitor, ts.nullTransformationContext); + ignoreReturns = oldIgnoreReturns; + return result; + } } +} - function transformConstantInitializer(initializer: ts.Expression, substitutions: ts.ReadonlyESMap): ts.Expression { - return substitutions.size - ? visitor(initializer) as ts.Expression - : initializer; +function transformConstantInitializer(initializer: ts.Expression, substitutions: ts.ReadonlyESMap): ts.Expression { + return substitutions.size + ? visitor(initializer) as ts.Expression + : initializer; - function visitor(node: ts.Node): ts.VisitResult { - const substitution = substitutions.get(ts.getNodeId(node).toString()); - return substitution ? ts.getSynthesizedDeepClone(substitution) : ts.visitEachChild(node, visitor, ts.nullTransformationContext); - } + function visitor(node: ts.Node): ts.VisitResult { + const substitution = substitutions.get(ts.getNodeId(node).toString()); + return substitution ? ts.getSynthesizedDeepClone(substitution) : ts.visitEachChild(node, visitor, ts.nullTransformationContext); } +} - function getStatementsOrClassElements(scope: Scope): readonly ts.Statement[] | readonly ts.ClassElement[] { - if (ts.isFunctionLikeDeclaration(scope)) { - const body = scope.body!; // TODO: GH#18217 - if (ts.isBlock(body)) { - return body.statements; - } +function getStatementsOrClassElements(scope: Scope): readonly ts.Statement[] | readonly ts.ClassElement[] { + if (ts.isFunctionLikeDeclaration(scope)) { + const body = scope.body!; // TODO: GH#18217 + if (ts.isBlock(body)) { + return body.statements; } - else if (ts.isModuleBlock(scope) || ts.isSourceFile(scope)) { - return scope.statements; - } - else if (ts.isClassLike(scope)) { - return scope.members; + } + else if (ts.isModuleBlock(scope) || ts.isSourceFile(scope)) { + return scope.statements; + } + else if (ts.isClassLike(scope)) { + return scope.members; + } + else { + ts.assertType(scope); + } + + return ts.emptyArray; +} + +/** + * If `scope` contains a function after `minPos`, then return the first such function. + * Otherwise, return `undefined`. + */ +function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): ts.Statement | ts.ClassElement | undefined { + return ts.find(getStatementsOrClassElements(scope), child => child.pos >= minPos && ts.isFunctionLikeDeclaration(child) && !ts.isConstructorDeclaration(child)); +} + +function getNodeToInsertPropertyBefore(maxPos: number, scope: ts.ClassLikeDeclaration): ts.ClassElement { + const members = scope.members; + ts.Debug.assert(members.length > 0, "Found no members"); // There must be at least one child, since we extracted from one. + let prevMember: ts.ClassElement | undefined; + let allProperties = true; + for (const member of members) { + if (member.pos > maxPos) { + return prevMember || members[0]; } - else { - ts.assertType(scope); + if (allProperties && !ts.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; + } - return ts.emptyArray; + if (prevMember === undefined) + return ts.Debug.fail(); // If the loop didn't return, then it did set prevMember. + return prevMember; +} + +function getNodeToInsertConstantBefore(node: ts.Node, scope: Scope): ts.Statement { + ts.Debug.assert(!ts.isClassLike(scope)); + + let prevScope: Scope | undefined; + for (let curr = node; curr !== scope; curr = curr.parent) { + if (isScope(curr)) { + prevScope = curr; + } } - /** - * If `scope` contains a function after `minPos`, then return the first such function. - * Otherwise, return `undefined`. - */ - function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): ts.Statement | ts.ClassElement | undefined { - return ts.find(getStatementsOrClassElements(scope), child => child.pos >= minPos && ts.isFunctionLikeDeclaration(child) && !ts.isConstructorDeclaration(child)); - } - - function getNodeToInsertPropertyBefore(maxPos: number, scope: ts.ClassLikeDeclaration): ts.ClassElement { - const members = scope.members; - ts.Debug.assert(members.length > 0, "Found no members"); // There must be at least one child, since we extracted from one. - let prevMember: ts.ClassElement | undefined; - let allProperties = true; - for (const member of members) { - if (member.pos > maxPos) { - return prevMember || members[0]; - } - if (allProperties && !ts.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; + for (let curr = (prevScope || node).parent; ; curr = curr.parent) { + if (isBlockLike(curr)) { + let prevStatement: ts.Statement | undefined; + for (const statement of curr.statements) { + if (statement.pos > node.pos) { + break; } + prevStatement = statement; + } - allProperties = false; + if (!prevStatement && ts.isCaseClause(curr)) { + // We must have been in the expression of the case clause. + ts.Debug.assert(ts.isSwitchStatement(curr.parent.parent), "Grandparent isn't a switch statement"); + return curr.parent.parent; } - prevMember = member; + + // There must be at least one statement since we started in one. + return ts.Debug.checkDefined(prevStatement, "prevStatement failed to get set"); } - if (prevMember === undefined) - return ts.Debug.fail(); // If the loop didn't return, then it did set prevMember. - return prevMember; + ts.Debug.assert(curr !== scope, "Didn't encounter a block-like before encountering scope"); } +} - function getNodeToInsertConstantBefore(node: ts.Node, scope: Scope): ts.Statement { - ts.Debug.assert(!ts.isClassLike(scope)); +function getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations: readonly ts.VariableDeclaration[], writes: readonly UsageEntry[] | undefined): ts.ShorthandPropertyAssignment[] { + const variableAssignments = ts.map(exposedVariableDeclarations, v => ts.factory.createShorthandPropertyAssignment(v.symbol.name)); + const writeAssignments = ts.map(writes, w => ts.factory.createShorthandPropertyAssignment(w.symbol.name)); - let prevScope: Scope | undefined; - for (let curr = node; curr !== scope; curr = curr.parent) { - if (isScope(curr)) { - prevScope = curr; - } - } + // TODO: GH#18217 `variableAssignments` not possibly undefined! + return variableAssignments === undefined + ? writeAssignments! + : writeAssignments === undefined + ? variableAssignments + : variableAssignments.concat(writeAssignments); +} - for (let curr = (prevScope || node).parent; ; curr = curr.parent) { - if (isBlockLike(curr)) { - let prevStatement: ts.Statement | undefined; - for (const statement of curr.statements) { - if (statement.pos > node.pos) { - break; - } - prevStatement = statement; - } +function isReadonlyArray(v: any): v is readonly any[] { + return ts.isArray(v); +} - if (!prevStatement && ts.isCaseClause(curr)) { - // We must have been in the expression of the case clause. - ts.Debug.assert(ts.isSwitchStatement(curr.parent.parent), "Grandparent isn't a switch statement"); - return curr.parent.parent; - } +/** + * 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: ts.SourceFile): ts.TextRange { + return isReadonlyArray(targetRange.range) + ? { pos: ts.first(targetRange.range).getStart(sourceFile), end: ts.last(targetRange.range).getEnd() } + : targetRange.range; +} - // There must be at least one statement since we started in one. - return ts.Debug.checkDefined(prevStatement, "prevStatement failed to get set"); - } +const enum Usage { + // value should be passed to extracted method + Read = 1, + // value should be passed to extracted method and propagated back + Write = 2 +} - ts.Debug.assert(curr !== scope, "Didn't encounter a block-like before encountering scope"); - } - } +interface UsageEntry { + readonly usage: Usage; + readonly symbol: ts.Symbol; + readonly node: ts.Node; +} + +interface ScopeUsages { + readonly usages: ts.ESMap; + readonly typeParameterUsages: ts.ESMap; // Key is type ID + readonly substitutions: ts.ESMap; +} - function getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations: readonly ts.VariableDeclaration[], writes: readonly UsageEntry[] | undefined): ts.ShorthandPropertyAssignment[] { - const variableAssignments = ts.map(exposedVariableDeclarations, v => ts.factory.createShorthandPropertyAssignment(v.symbol.name)); - const writeAssignments = ts.map(writes, w => ts.factory.createShorthandPropertyAssignment(w.symbol.name)); +interface ReadsAndWrites { + readonly target: ts.Expression | ts.Block; + readonly usagesPerScope: readonly ScopeUsages[]; + readonly functionErrorsPerScope: readonly (readonly ts.Diagnostic[])[]; + readonly constantErrorsPerScope: readonly (readonly ts.Diagnostic[])[]; + readonly exposedVariableDeclarations: readonly ts.VariableDeclaration[]; +} +function collectReadsAndWrites(targetRange: TargetRange, scopes: Scope[], enclosingTextRange: ts.TextRange, sourceFile: ts.SourceFile, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): ReadsAndWrites { + const allTypeParameterUsages = new ts.Map(); // Key is type ID + const usagesPerScope: ScopeUsages[] = []; + const substitutionsPerScope: ts.ESMap[] = []; + const functionErrorsPerScope: ts.Diagnostic[][] = []; + const constantErrorsPerScope: ts.Diagnostic[][] = []; + const visibleDeclarationsInExtractedRange: ts.NamedDeclaration[] = []; + const exposedVariableSymbolSet = new ts.Map(); // Key is symbol ID + const exposedVariableDeclarations: ts.VariableDeclaration[] = []; + let firstExposedNonVariableDeclaration: ts.NamedDeclaration | undefined; + + const expression = !isReadonlyArray(targetRange.range) + ? targetRange.range + : targetRange.range.length === 1 && ts.isExpressionStatement(targetRange.range[0]) + ? targetRange.range[0].expression + : undefined; - // TODO: GH#18217 `variableAssignments` not possibly undefined! - return variableAssignments === undefined - ? writeAssignments! - : writeAssignments === undefined - ? variableAssignments - : variableAssignments.concat(writeAssignments); + let expressionDiagnostic: ts.Diagnostic | undefined; + if (expression === undefined) { + const statements = targetRange.range as readonly ts.Statement[]; + const start = ts.first(statements).getStart(); + const end = ts.last(statements).end; + expressionDiagnostic = ts.createFileDiagnostic(sourceFile, start, end - start, Messages.expressionExpected); } - - function isReadonlyArray(v: any): v is readonly any[] { - return ts.isArray(v); + else if (checker.getTypeAtLocation(expression).flags & (ts.TypeFlags.Void | ts.TypeFlags.Never)) { + expressionDiagnostic = ts.createDiagnosticForNode(expression, Messages.uselessConstantType); } - /** - * 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: ts.SourceFile): ts.TextRange { - return isReadonlyArray(targetRange.range) - ? { pos: ts.first(targetRange.range).getStart(sourceFile), end: ts.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: ts.Symbol; - readonly node: ts.Node; - } - - interface ScopeUsages { - readonly usages: ts.ESMap; - readonly typeParameterUsages: ts.ESMap; // Key is type ID - readonly substitutions: ts.ESMap; - } - - interface ReadsAndWrites { - readonly target: ts.Expression | ts.Block; - readonly usagesPerScope: readonly ScopeUsages[]; - readonly functionErrorsPerScope: readonly (readonly ts.Diagnostic[])[]; - readonly constantErrorsPerScope: readonly (readonly ts.Diagnostic[])[]; - readonly exposedVariableDeclarations: readonly ts.VariableDeclaration[]; - } - function collectReadsAndWrites(targetRange: TargetRange, scopes: Scope[], enclosingTextRange: ts.TextRange, sourceFile: ts.SourceFile, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): ReadsAndWrites { - const allTypeParameterUsages = new ts.Map(); // Key is type ID - const usagesPerScope: ScopeUsages[] = []; - const substitutionsPerScope: ts.ESMap[] = []; - const functionErrorsPerScope: ts.Diagnostic[][] = []; - const constantErrorsPerScope: ts.Diagnostic[][] = []; - const visibleDeclarationsInExtractedRange: ts.NamedDeclaration[] = []; - const exposedVariableSymbolSet = new ts.Map(); // Key is symbol ID - const exposedVariableDeclarations: ts.VariableDeclaration[] = []; - let firstExposedNonVariableDeclaration: ts.NamedDeclaration | undefined; - - const expression = !isReadonlyArray(targetRange.range) - ? targetRange.range - : targetRange.range.length === 1 && ts.isExpressionStatement(targetRange.range[0]) - ? targetRange.range[0].expression - : undefined; - - let expressionDiagnostic: ts.Diagnostic | undefined; - if (expression === undefined) { - const statements = targetRange.range as readonly ts.Statement[]; - const start = ts.first(statements).getStart(); - const end = ts.last(statements).end; - expressionDiagnostic = ts.createFileDiagnostic(sourceFile, start, end - start, Messages.expressionExpected); - } - else if (checker.getTypeAtLocation(expression).flags & (ts.TypeFlags.Void | ts.TypeFlags.Never)) { - expressionDiagnostic = ts.createDiagnosticForNode(expression, Messages.uselessConstantType); - } - - // initialize results - for (const scope of scopes) { - usagesPerScope.push({ usages: new ts.Map(), typeParameterUsages: new ts.Map(), substitutions: new ts.Map() }); - substitutionsPerScope.push(new ts.Map()); - - functionErrorsPerScope.push([]); - - const constantErrors = []; - if (expressionDiagnostic) { - constantErrors.push(expressionDiagnostic); - } - if (ts.isClassLike(scope) && ts.isInJSFile(scope)) { - constantErrors.push(ts.createDiagnosticForNode(scope, Messages.cannotExtractToJSClass)); - } - if (ts.isArrowFunction(scope) && !ts.isBlock(scope.body)) { - // TODO (https://github.com/Microsoft/TypeScript/issues/18924): allow this - constantErrors.push(ts.createDiagnosticForNode(scope, Messages.cannotExtractToExpressionArrowFunction)); - } - constantErrorsPerScope.push(constantErrors); + // initialize results + for (const scope of scopes) { + usagesPerScope.push({ usages: new ts.Map(), typeParameterUsages: new ts.Map(), substitutions: new ts.Map() }); + substitutionsPerScope.push(new ts.Map()); + + functionErrorsPerScope.push([]); + + const constantErrors = []; + if (expressionDiagnostic) { + constantErrors.push(expressionDiagnostic); + } + if (ts.isClassLike(scope) && ts.isInJSFile(scope)) { + constantErrors.push(ts.createDiagnosticForNode(scope, Messages.cannotExtractToJSClass)); } + if (ts.isArrowFunction(scope) && !ts.isBlock(scope.body)) { + // TODO (https://github.com/Microsoft/TypeScript/issues/18924): allow this + constantErrors.push(ts.createDiagnosticForNode(scope, Messages.cannotExtractToExpressionArrowFunction)); + } + constantErrorsPerScope.push(constantErrors); + } - const seenUsages = new ts.Map(); - const target = isReadonlyArray(targetRange.range) ? ts.factory.createBlock(targetRange.range) : targetRange.range; - const unmodifiedNode = isReadonlyArray(targetRange.range) ? ts.first(targetRange.range) : targetRange.range; - const inGenericContext = isInGenericContext(unmodifiedNode); + const seenUsages = new ts.Map(); + const target = isReadonlyArray(targetRange.range) ? ts.factory.createBlock(targetRange.range) : targetRange.range; + const unmodifiedNode = isReadonlyArray(targetRange.range) ? ts.first(targetRange.range) : targetRange.range; + const inGenericContext = isInGenericContext(unmodifiedNode); - collectUsages(target); + 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) && !ts.isJsxAttribute(targetRange.range)) { - const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 - recordTypeParameterUsages(contextualType); - } + // 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) && !ts.isJsxAttribute(targetRange.range)) { + const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 + recordTypeParameterUsages(contextualType); + } - if (allTypeParameterUsages.size > 0) { - const seenTypeParameterUsages = new ts.Map(); // Key is type ID + if (allTypeParameterUsages.size > 0) { + const seenTypeParameterUsages = new ts.Map(); // Key is type ID - let i = 0; - for (let curr: ts.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); - }); + let i = 0; + for (let curr: ts.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++; - } + i++; + } - // Note that we add the current node's type parameters *after* updating the corresponding scope. - if (ts.isDeclarationWithTypeParameters(curr)) { - for (const typeParameterDecl of ts.getEffectiveTypeParameterDeclarations(curr)) { - const typeParameter = checker.getTypeAtLocation(typeParameterDecl) as ts.TypeParameter; - if (allTypeParameterUsages.has(typeParameter.id.toString())) { - seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter); - } + // Note that we add the current node's type parameters *after* updating the corresponding scope. + if (ts.isDeclarationWithTypeParameters(curr)) { + for (const typeParameterDecl of ts.getEffectiveTypeParameterDeclarations(curr)) { + const typeParameter = checker.getTypeAtLocation(typeParameterDecl) as ts.TypeParameter; + if (allTypeParameterUsages.has(typeParameter.id.toString())) { + seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter); } } } - - // 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. - ts.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 = ts.isBlockScope(scopes[0], scopes[0].parent) - ? scopes[0] - : ts.getEnclosingBlockScopeContainer(scopes[0]); - ts.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(ts.createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes)); - } + // 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. + ts.Debug.assert(i === scopes.length, "Should have iterated all scopes"); + } - if (targetRange.facts & RangeFacts.UsesThisInFunction && ts.isClassLike(scopes[i])) { - functionErrorsPerScope[i].push(ts.createDiagnosticForNode(targetRange.thisNode!, Messages.cannotExtractFunctionsContainingThisToMethod)); - } + // 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 = ts.isBlockScope(scopes[0], scopes[0].parent) + ? scopes[0] + : ts.getEnclosingBlockScopeContainer(scopes[0]); + ts.forEachChild(containingLexicalScopeOfExtraction, checkForUsedDeclarations); + } - let hasWrite = false; - let readonlyClassPropertyWrite: ts.Declaration | undefined; - usagesPerScope[i].usages.forEach(value => { - if (value.usage === Usage.Write) { - hasWrite = true; - if (value.symbol.flags & ts.SymbolFlags.ClassMember && - value.symbol.valueDeclaration && - ts.hasEffectiveModifier(value.symbol.valueDeclaration, ts.ModifierFlags.Readonly)) { - readonlyClassPropertyWrite = value.symbol.valueDeclaration; - } + 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(ts.createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes)); + } + + if (targetRange.facts & RangeFacts.UsesThisInFunction && ts.isClassLike(scopes[i])) { + functionErrorsPerScope[i].push(ts.createDiagnosticForNode(targetRange.thisNode!, Messages.cannotExtractFunctionsContainingThisToMethod)); + } + + let hasWrite = false; + let readonlyClassPropertyWrite: ts.Declaration | undefined; + usagesPerScope[i].usages.forEach(value => { + if (value.usage === Usage.Write) { + hasWrite = true; + if (value.symbol.flags & ts.SymbolFlags.ClassMember && + value.symbol.valueDeclaration && + ts.hasEffectiveModifier(value.symbol.valueDeclaration, ts.ModifierFlags.Readonly)) { + readonlyClassPropertyWrite = value.symbol.valueDeclaration; } - }); + } + }); - // If an expression was extracted, then there shouldn't have been any variable declarations. - ts.Debug.assert(isReadonlyArray(targetRange.range) || exposedVariableDeclarations.length === 0, "No variable declarations expected if something was extracted"); + // If an expression was extracted, then there shouldn't have been any variable declarations. + ts.Debug.assert(isReadonlyArray(targetRange.range) || exposedVariableDeclarations.length === 0, "No variable declarations expected if something was extracted"); - if (hasWrite && !isReadonlyArray(targetRange.range)) { - const diag = ts.createDiagnosticForNode(targetRange.range, Messages.cannotWriteInExpression); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } - else if (readonlyClassPropertyWrite && i > 0) { - const diag = ts.createDiagnosticForNode(readonlyClassPropertyWrite, Messages.cannotExtractReadonlyPropertyInitializerOutsideConstructor); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } - else if (firstExposedNonVariableDeclaration) { - const diag = ts.createDiagnosticForNode(firstExposedNonVariableDeclaration, Messages.cannotExtractExportedEntity); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } + if (hasWrite && !isReadonlyArray(targetRange.range)) { + const diag = ts.createDiagnosticForNode(targetRange.range, Messages.cannotWriteInExpression); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); + } + else if (readonlyClassPropertyWrite && i > 0) { + const diag = ts.createDiagnosticForNode(readonlyClassPropertyWrite, Messages.cannotExtractReadonlyPropertyInitializerOutsideConstructor); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); } + else if (firstExposedNonVariableDeclaration) { + const diag = ts.createDiagnosticForNode(firstExposedNonVariableDeclaration, Messages.cannotExtractExportedEntity); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); + } + } - return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope, exposedVariableDeclarations }; + return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope, exposedVariableDeclarations }; - function isInGenericContext(node: ts.Node) { - return !!ts.findAncestor(node, n => ts.isDeclarationWithTypeParameters(n) && ts.getEffectiveTypeParameterDeclarations(n).length !== 0); - } + function isInGenericContext(node: ts.Node) { + return !!ts.findAncestor(node, n => ts.isDeclarationWithTypeParameters(n) && ts.getEffectiveTypeParameterDeclarations(n).length !== 0); + } - function recordTypeParameterUsages(type: ts.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); + function recordTypeParameterUsages(type: ts.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); - } + for (const visitedType of visitedTypes) { + if (visitedType.isTypeParameter()) { + allTypeParameterUsages.set(visitedType.id.toString(), visitedType); } } + } - function collectUsages(node: ts.Node, valueUsage = Usage.Read) { - if (inGenericContext) { - const type = checker.getTypeAtLocation(node); - recordTypeParameterUsages(type); - } + function collectUsages(node: ts.Node, valueUsage = Usage.Read) { + if (inGenericContext) { + const type = checker.getTypeAtLocation(node); + recordTypeParameterUsages(type); + } - if (ts.isDeclaration(node) && node.symbol) { - visibleDeclarationsInExtractedRange.push(node); - } + if (ts.isDeclaration(node) && node.symbol) { + visibleDeclarationsInExtractedRange.push(node); + } - if (ts.isAssignmentExpression(node)) { - // use 'write' as default usage for values - collectUsages(node.left, Usage.Write); - collectUsages(node.right); - } - else if (ts.isUnaryExpressionWithWrite(node)) { - collectUsages(node.operand, Usage.Write); - } - else if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { - // use 'write' as default usage for values - ts.forEachChild(node, collectUsages); + if (ts.isAssignmentExpression(node)) { + // use 'write' as default usage for values + collectUsages(node.left, Usage.Write); + collectUsages(node.right); + } + else if (ts.isUnaryExpressionWithWrite(node)) { + collectUsages(node.operand, Usage.Write); + } + else if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { + // use 'write' as default usage for values + ts.forEachChild(node, collectUsages); + } + else if (ts.isIdentifier(node)) { + if (!node.parent) { + return; } - else if (ts.isIdentifier(node)) { - if (!node.parent) { - return; - } - if (ts.isQualifiedName(node.parent) && node !== node.parent.left) { - return; - } - if (ts.isPropertyAccessExpression(node.parent) && node !== node.parent.expression) { - return; - } - recordUsage(node, valueUsage, /*isTypeNode*/ ts.isPartOfTypeNode(node)); + if (ts.isQualifiedName(node.parent) && node !== node.parent.left) { + return; } - else { - ts.forEachChild(node, collectUsages); + if (ts.isPropertyAccessExpression(node.parent) && node !== node.parent.expression) { + return; } + recordUsage(node, valueUsage, /*isTypeNode*/ ts.isPartOfTypeNode(node)); } + else { + ts.forEachChild(node, collectUsages); + } + } - function recordUsage(n: ts.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(ts.getNodeId(n).toString(), substitution); - } + function recordUsage(n: ts.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(ts.getNodeId(n).toString(), substitution); } } } + } - function recordUsagebySymbol(identifier: ts.Identifier, usage: Usage, isTypeName: boolean) { - const symbol = getSymbolReferencedByIdentifier(identifier); - if (!symbol) { - // cannot find symbol - do nothing - return undefined; - } - const symbolId = ts.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; - } + function recordUsagebySymbol(identifier: ts.Identifier, usage: Usage, isTypeName: boolean) { + const symbol = getSymbolReferencedByIdentifier(identifier); + if (!symbol) { + // cannot find symbol - do nothing + return undefined; + } + const symbolId = ts.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 }); - } + 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 && ts.find(decls, d => d.getSourceFile() === sourceFile); - if (!declInFile) { - return undefined; } - if (ts.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 && ts.find(decls, d => d.getSourceFile() === sourceFile); + if (!declInFile) { + return undefined; + } + if (ts.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 = ts.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 = ts.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 & ts.SymbolFlags.TypeParameter)) { - const diag = ts.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 & ts.SymbolFlags.TypeParameter)) { + const diag = ts.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; } + return symbolId; + } - function checkForUsedDeclarations(node: ts.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 ts.Statement) >= 0)) { - return; - } - - // Otherwise check and recurse. - const sym = ts.isIdentifier(node) - ? getSymbolReferencedByIdentifier(node) - : checker.getSymbolAtLocation(node); - if (sym) { - const decl = ts.find(visibleDeclarationsInExtractedRange, d => d.symbol === sym); - if (decl) { - if (ts.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; + function checkForUsedDeclarations(node: ts.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 ts.Statement) >= 0)) { + return; + } + + // Otherwise check and recurse. + const sym = ts.isIdentifier(node) + ? getSymbolReferencedByIdentifier(node) + : checker.getSymbolAtLocation(node); + if (sym) { + const decl = ts.find(visibleDeclarationsInExtractedRange, d => d.symbol === sym); + if (decl) { + if (ts.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; + } } - - ts.forEachChild(node, checkForUsedDeclarations); - } - - /** - * Return the symbol referenced by an identifier (even if it declares a different symbol). - */ - function getSymbolReferencedByIdentifier(identifier: ts.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 && ts.isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier - ? checker.getShorthandAssignmentValueSymbol(identifier.parent) - : checker.getSymbolAtLocation(identifier); - } - - function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: ts.Symbol | undefined, scopeDecl: ts.Node, isTypeNode: boolean): ts.PropertyAccessExpression | ts.EntityName | undefined { - if (!symbol) { - return undefined; - } - const decls = symbol.getDeclarations(); - if (decls && decls.some(d => d.parent === scopeDecl)) { - return ts.factory.createIdentifier(symbol.name); - } - const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); - if (prefix === undefined) { - return undefined; - } - return isTypeNode - ? ts.factory.createQualifiedName(prefix as ts.EntityName, ts.factory.createIdentifier(symbol.name)) - : ts.factory.createPropertyAccessExpression(prefix as ts.Expression, symbol.name); } - } - function getExtractableParent(node: ts.Node | undefined): ts.Node | undefined { - return ts.findAncestor(node, node => node.parent && isExtractableExpression(node) && !ts.isBinaryExpression(node.parent)); + ts.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: ts.Node): boolean { - const { parent } = node; - switch (parent.kind) { - case ts.SyntaxKind.EnumMember: - return false; - } - - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - return parent.kind !== ts.SyntaxKind.ImportDeclaration && - parent.kind !== ts.SyntaxKind.ImportSpecifier; - case ts.SyntaxKind.SpreadElement: - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.BindingElement: - return false; - - case ts.SyntaxKind.Identifier: - return parent.kind !== ts.SyntaxKind.BindingElement && - parent.kind !== ts.SyntaxKind.ImportSpecifier && - parent.kind !== ts.SyntaxKind.ExportSpecifier; - } - return true; - } - - function isBlockLike(node: ts.Node): node is ts.BlockLike { - switch (node.kind) { - case ts.SyntaxKind.Block: - case ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.CaseClause: - return true; - default: - return false; + function getSymbolReferencedByIdentifier(identifier: ts.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 && ts.isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier + ? checker.getShorthandAssignmentValueSymbol(identifier.parent) + : checker.getSymbolAtLocation(identifier); + } + + function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: ts.Symbol | undefined, scopeDecl: ts.Node, isTypeNode: boolean): ts.PropertyAccessExpression | ts.EntityName | undefined { + if (!symbol) { + return undefined; + } + const decls = symbol.getDeclarations(); + if (decls && decls.some(d => d.parent === scopeDecl)) { + return ts.factory.createIdentifier(symbol.name); + } + const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); + if (prefix === undefined) { + return undefined; } + return isTypeNode + ? ts.factory.createQualifiedName(prefix as ts.EntityName, ts.factory.createIdentifier(symbol.name)) + : ts.factory.createPropertyAccessExpression(prefix as ts.Expression, symbol.name); + } +} + +function getExtractableParent(node: ts.Node | undefined): ts.Node | undefined { + return ts.findAncestor(node, node => node.parent && isExtractableExpression(node) && !ts.isBinaryExpression(node.parent)); +} + +/** + * 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 + */ +function isExtractableExpression(node: ts.Node): boolean { + const { parent } = node; + switch (parent.kind) { + case ts.SyntaxKind.EnumMember: + return false; } - function isInJSXContent(node: ts.Node) { - return isStringLiteralJsxAttribute(node) || - (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent)); + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + return parent.kind !== ts.SyntaxKind.ImportDeclaration && + parent.kind !== ts.SyntaxKind.ImportSpecifier; + case ts.SyntaxKind.SpreadElement: + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.BindingElement: + return false; + + case ts.SyntaxKind.Identifier: + return parent.kind !== ts.SyntaxKind.BindingElement && + parent.kind !== ts.SyntaxKind.ImportSpecifier && + parent.kind !== ts.SyntaxKind.ExportSpecifier; } + return true; +} - function isStringLiteralJsxAttribute(node: ts.Node): node is ts.StringLiteral { - return ts.isStringLiteral(node) && node.parent && ts.isJsxAttribute(node.parent); +function isBlockLike(node: ts.Node): node is ts.BlockLike { + switch (node.kind) { + case ts.SyntaxKind.Block: + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.CaseClause: + return true; + default: + return false; } } + +function isInJSXContent(node: ts.Node) { + return isStringLiteralJsxAttribute(node) || + (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent)); +} + +function isStringLiteralJsxAttribute(node: ts.Node): node is ts.StringLiteral { + return ts.isStringLiteral(node) && node.parent && ts.isJsxAttribute(node.parent); +} +} diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index c94bea810d3ac..c5803033af68e 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -1,245 +1,245 @@ /* @internal */ namespace ts.refactor { - const refactorName = "Extract type"; - - const extractToTypeAliasAction = { - name: "Extract to type alias", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_type_alias), - kind: "refactor.extract.type", - }; - const extractToInterfaceAction = { - name: "Extract to interface", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_interface), - kind: "refactor.extract.interface", - }; - const extractToTypeDefAction = { - name: "Extract to typedef", - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_typedef), - kind: "refactor.extract.typedef" - }; - - ts.refactor.registerRefactor(refactorName, { - kinds: [ - extractToTypeAliasAction.kind, - extractToInterfaceAction.kind, - extractToTypeDefAction.kind - ], - getAvailableActions: function getRefactorActionsToExtractType(context): readonly ts.ApplicableRefactorInfo[] { - const info = getRangeToExtract(context, context.triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - return [{ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_type), - actions: info.isJS ? - [extractToTypeDefAction] : ts.append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction) - }]; - } - - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ - name: refactorName, - description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_type), - actions: [ - { ...extractToTypeDefAction, notApplicableReason: info.error }, - { ...extractToTypeAliasAction, notApplicableReason: info.error }, - { ...extractToInterfaceAction, notApplicableReason: info.error }, - ] - }]; - } - +const refactorName = "Extract type"; + +const extractToTypeAliasAction = { + name: "Extract to type alias", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_type_alias), + kind: "refactor.extract.type", +}; +const extractToInterfaceAction = { + name: "Extract to interface", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_interface), + kind: "refactor.extract.interface", +}; +const extractToTypeDefAction = { + name: "Extract to typedef", + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_to_typedef), + kind: "refactor.extract.typedef" +}; + +ts.refactor.registerRefactor(refactorName, { + kinds: [ + extractToTypeAliasAction.kind, + extractToInterfaceAction.kind, + extractToTypeDefAction.kind + ], + getAvailableActions: function getRefactorActionsToExtractType(context): readonly ts.ApplicableRefactorInfo[] { + const info = getRangeToExtract(context, context.triggerReason === "invoked"); + if (!info) return ts.emptyArray; - }, - getEditsForAction: function getRefactorEditsToExtractType(context, actionName): ts.RefactorEditInfo { - const { file, } = context; - const info = getRangeToExtract(context); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected to find a range to extract"); - const name = ts.getUniqueName("NewType", file); - const edits = ts.textChanges.ChangeTracker.with(context, changes => { - switch (actionName) { - case extractToTypeAliasAction.name: - ts.Debug.assert(!info.isJS, "Invalid actionName/JS combo"); - return doTypeAliasChange(changes, file, name, info); - case extractToTypeDefAction.name: - ts.Debug.assert(info.isJS, "Invalid actionName/JS combo"); - return doTypedefChange(changes, file, name, info); - case extractToInterfaceAction.name: - ts.Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo"); - return doInterfaceChange(changes, file, name, info as InterfaceInfo); - default: - ts.Debug.fail("Unexpected action name"); - } - }); + if (!ts.refactor.isRefactorErrorInfo(info)) { + return [{ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_type), + actions: info.isJS ? + [extractToTypeDefAction] : ts.append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction) + }]; + } - const renameFilename = file.fileName; - const renameLocation = ts.getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false); - return { edits, renameFilename, renameLocation }; + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: ts.getLocaleSpecificMessage(ts.Diagnostics.Extract_type), + actions: [ + { ...extractToTypeDefAction, notApplicableReason: info.error }, + { ...extractToTypeAliasAction, notApplicableReason: info.error }, + { ...extractToInterfaceAction, notApplicableReason: info.error }, + ] + }]; } - }); - interface TypeAliasInfo { - isJS: boolean; - selection: ts.TypeNode; - firstStatement: ts.Statement; - typeParameters: readonly ts.TypeParameterDeclaration[]; - typeElements?: readonly ts.TypeElement[]; - } + return ts.emptyArray; + }, + getEditsForAction: function getRefactorEditsToExtractType(context, actionName): ts.RefactorEditInfo { + const { file, } = context; + const info = getRangeToExtract(context); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected to find a range to extract"); + const name = ts.getUniqueName("NewType", file); + const edits = ts.textChanges.ChangeTracker.with(context, changes => { + switch (actionName) { + case extractToTypeAliasAction.name: + ts.Debug.assert(!info.isJS, "Invalid actionName/JS combo"); + return doTypeAliasChange(changes, file, name, info); + case extractToTypeDefAction.name: + ts.Debug.assert(info.isJS, "Invalid actionName/JS combo"); + return doTypedefChange(changes, file, name, info); + case extractToInterfaceAction.name: + ts.Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo"); + return doInterfaceChange(changes, file, name, info as InterfaceInfo); + default: + ts.Debug.fail("Unexpected action name"); + } + }); - interface InterfaceInfo { - isJS: boolean; - selection: ts.TypeNode; - firstStatement: ts.Statement; - typeParameters: readonly ts.TypeParameterDeclaration[]; - typeElements: readonly ts.TypeElement[]; + const renameFilename = file.fileName; + const renameLocation = ts.getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false); + return { edits, renameFilename, renameLocation }; } +}); + +interface TypeAliasInfo { + isJS: boolean; + selection: ts.TypeNode; + firstStatement: ts.Statement; + typeParameters: readonly ts.TypeParameterDeclaration[]; + typeElements?: readonly ts.TypeElement[]; +} - type ExtractInfo = TypeAliasInfo | InterfaceInfo; +interface InterfaceInfo { + isJS: boolean; + selection: ts.TypeNode; + firstStatement: ts.Statement; + typeParameters: readonly ts.TypeParameterDeclaration[]; + typeElements: readonly ts.TypeElement[]; +} - function getRangeToExtract(context: ts.RefactorContext, considerEmptySpans = true): ExtractInfo | ts.refactor.RefactorErrorInfo | undefined { - const { file, startPosition } = context; - const isJS = ts.isSourceFileJS(file); - const current = ts.getTokenAtPosition(file, startPosition); - const range = ts.createTextRangeFromSpan(ts.getRefactorContextSpan(context)); - const cursorRequest = range.pos === range.end && considerEmptySpans; +type ExtractInfo = TypeAliasInfo | InterfaceInfo; - const selection = ts.findAncestor(current, (node => node.parent && ts.isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) && - (cursorRequest || ts.nodeOverlapsWithStartEnd(current, file, range.pos, range.end)))); - if (!selection || !ts.isTypeNode(selection)) - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Selection_is_not_a_valid_type_node) }; +function getRangeToExtract(context: ts.RefactorContext, considerEmptySpans = true): ExtractInfo | ts.refactor.RefactorErrorInfo | undefined { + const { file, startPosition } = context; + const isJS = ts.isSourceFileJS(file); + const current = ts.getTokenAtPosition(file, startPosition); + const range = ts.createTextRangeFromSpan(ts.getRefactorContextSpan(context)); + const cursorRequest = range.pos === range.end && considerEmptySpans; - const checker = context.program.getTypeChecker(); - const firstStatement = ts.Debug.checkDefined(ts.findAncestor(selection, ts.isStatement), "Should find a statement"); - const typeParameters = collectTypeParameters(checker, selection, firstStatement, file); - if (!typeParameters) - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.No_type_could_be_extracted_from_this_type_node) }; + const selection = ts.findAncestor(current, (node => node.parent && ts.isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) && + (cursorRequest || ts.nodeOverlapsWithStartEnd(current, file, range.pos, range.end)))); + if (!selection || !ts.isTypeNode(selection)) + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Selection_is_not_a_valid_type_node) }; - const typeElements = flattenTypeLiteralNodeReference(checker, selection); - return { isJS, selection, firstStatement, typeParameters, typeElements }; - } + const checker = context.program.getTypeChecker(); + const firstStatement = ts.Debug.checkDefined(ts.findAncestor(selection, ts.isStatement), "Should find a statement"); + const typeParameters = collectTypeParameters(checker, selection, firstStatement, file); + if (!typeParameters) + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.No_type_could_be_extracted_from_this_type_node) }; - function flattenTypeLiteralNodeReference(checker: ts.TypeChecker, node: ts.TypeNode | undefined): readonly ts.TypeElement[] | undefined { - if (!node) - return undefined; - if (ts.isIntersectionTypeNode(node)) { - const result: ts.TypeElement[] = []; - const seen = new ts.Map(); - for (const type of node.types) { - const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type); - if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && ts.addToSeen(seen, ts.getNameFromPropertyName(type.name) as string))) { - return undefined; - } + const typeElements = flattenTypeLiteralNodeReference(checker, selection); + return { isJS, selection, firstStatement, typeParameters, typeElements }; +} - ts.addRange(result, flattenedTypeMembers); +function flattenTypeLiteralNodeReference(checker: ts.TypeChecker, node: ts.TypeNode | undefined): readonly ts.TypeElement[] | undefined { + if (!node) + return undefined; + if (ts.isIntersectionTypeNode(node)) { + const result: ts.TypeElement[] = []; + const seen = new ts.Map(); + for (const type of node.types) { + const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type); + if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && ts.addToSeen(seen, ts.getNameFromPropertyName(type.name) as string))) { + return undefined; } - return result; - } - else if (ts.isParenthesizedTypeNode(node)) { - return flattenTypeLiteralNodeReference(checker, node.type); - } - else if (ts.isTypeLiteralNode(node)) { - return node.members; + + ts.addRange(result, flattenedTypeMembers); } - return undefined; + return result; } - - function rangeContainsSkipTrivia(r1: ts.TextRange, node: ts.Node, file: ts.SourceFile): boolean { - return ts.rangeContainsStartEnd(r1, ts.skipTrivia(file.text, node.pos), node.end); + else if (ts.isParenthesizedTypeNode(node)) { + return flattenTypeLiteralNodeReference(checker, node.type); + } + else if (ts.isTypeLiteralNode(node)) { + return node.members; } + return undefined; +} + +function rangeContainsSkipTrivia(r1: ts.TextRange, node: ts.Node, file: ts.SourceFile): boolean { + return ts.rangeContainsStartEnd(r1, ts.skipTrivia(file.text, node.pos), node.end); +} - function collectTypeParameters(checker: ts.TypeChecker, selection: ts.TypeNode, statement: ts.Statement, file: ts.SourceFile): ts.TypeParameterDeclaration[] | undefined { - const result: ts.TypeParameterDeclaration[] = []; - return visitor(selection) ? undefined : result; - - function visitor(node: ts.Node): true | undefined { - if (ts.isTypeReferenceNode(node)) { - if (ts.isIdentifier(node.typeName)) { - const typeName = node.typeName; - const symbol = checker.resolveName(typeName.text, typeName, ts.SymbolFlags.TypeParameter, /* excludeGlobals */ true); - for (const decl of symbol?.declarations || ts.emptyArray) { - if (ts.isTypeParameterDeclaration(decl) && decl.getSourceFile() === file) { - // skip extraction if the type node is in the range of the type parameter declaration. - // function foo(): void; - if (decl.name.escapedText === typeName.escapedText && rangeContainsSkipTrivia(decl, selection, file)) { - return true; - } - - if (rangeContainsSkipTrivia(statement, decl, file) && !rangeContainsSkipTrivia(selection, decl, file)) { - ts.pushIfUnique(result, decl); - break; - } +function collectTypeParameters(checker: ts.TypeChecker, selection: ts.TypeNode, statement: ts.Statement, file: ts.SourceFile): ts.TypeParameterDeclaration[] | undefined { + const result: ts.TypeParameterDeclaration[] = []; + return visitor(selection) ? undefined : result; + + function visitor(node: ts.Node): true | undefined { + if (ts.isTypeReferenceNode(node)) { + if (ts.isIdentifier(node.typeName)) { + const typeName = node.typeName; + const symbol = checker.resolveName(typeName.text, typeName, ts.SymbolFlags.TypeParameter, /* excludeGlobals */ true); + for (const decl of symbol?.declarations || ts.emptyArray) { + if (ts.isTypeParameterDeclaration(decl) && decl.getSourceFile() === file) { + // skip extraction if the type node is in the range of the type parameter declaration. + // function foo(): void; + if (decl.name.escapedText === typeName.escapedText && rangeContainsSkipTrivia(decl, selection, file)) { + return true; + } + + if (rangeContainsSkipTrivia(statement, decl, file) && !rangeContainsSkipTrivia(selection, decl, file)) { + ts.pushIfUnique(result, decl); + break; } } } } - else if (ts.isInferTypeNode(node)) { - const conditionalTypeNode = ts.findAncestor(node, n => ts.isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file)); - if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) { - return true; - } + } + else if (ts.isInferTypeNode(node)) { + const conditionalTypeNode = ts.findAncestor(node, n => ts.isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file)); + if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) { + return true; + } + } + else if ((ts.isTypePredicateNode(node) || ts.isThisTypeNode(node))) { + const functionLikeNode = ts.findAncestor(node.parent, ts.isFunctionLike); + if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) { + return true; } - else if ((ts.isTypePredicateNode(node) || ts.isThisTypeNode(node))) { - const functionLikeNode = ts.findAncestor(node.parent, ts.isFunctionLike); - if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) { + } + else if (ts.isTypeQueryNode(node)) { + if (ts.isIdentifier(node.exprName)) { + const symbol = checker.resolveName(node.exprName.text, node.exprName, ts.SymbolFlags.Value, /* excludeGlobals */ false); + if (symbol?.valueDeclaration && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) { return true; } } - else if (ts.isTypeQueryNode(node)) { - if (ts.isIdentifier(node.exprName)) { - const symbol = checker.resolveName(node.exprName.text, node.exprName, ts.SymbolFlags.Value, /* excludeGlobals */ false); - if (symbol?.valueDeclaration && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) { - return true; - } - } - else { - if (ts.isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) { - return true; - } + else { + if (ts.isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) { + return true; } } + } - if (file && ts.isTupleTypeNode(node) && (ts.getLineAndCharacterOfPosition(file, node.pos).line === ts.getLineAndCharacterOfPosition(file, node.end).line)) { - ts.setEmitFlags(node, ts.EmitFlags.SingleLine); - } - - return ts.forEachChild(node, visitor); + if (file && ts.isTupleTypeNode(node) && (ts.getLineAndCharacterOfPosition(file, node.pos).line === ts.getLineAndCharacterOfPosition(file, node.end).line)) { + ts.setEmitFlags(node, ts.EmitFlags.SingleLine); } + + return ts.forEachChild(node, visitor); } +} - function doTypeAliasChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: TypeAliasInfo) { - const { firstStatement, selection, typeParameters } = info; +function doTypeAliasChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: TypeAliasInfo) { + const { firstStatement, selection, typeParameters } = info; - const newTypeNode = ts.factory.createTypeAliasDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, name, typeParameters.map(id => ts.factory.updateTypeParameterDeclaration(id, id.modifiers, id.name, id.constraint, /* defaultType */ undefined)), selection); - changes.insertNodeBefore(file, firstStatement, ts.ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); - changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.ExcludeWhitespace }); - } - function doInterfaceChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: InterfaceInfo) { - const { firstStatement, selection, typeParameters, typeElements } = info; - - const newTypeNode = ts.factory.createInterfaceDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, name, typeParameters, - /* heritageClauses */ undefined, typeElements); - ts.setTextRange(newTypeNode, typeElements[0]?.parent); - changes.insertNodeBefore(file, firstStatement, ts.ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); - changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.ExcludeWhitespace }); - } - function doTypedefChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: ExtractInfo) { - const { firstStatement, selection, typeParameters } = info; - - ts.setEmitFlags(selection, ts.EmitFlags.NoComments | ts.EmitFlags.NoNestedComments); - const node = ts.factory.createJSDocTypedefTag(ts.factory.createIdentifier("typedef"), ts.factory.createJSDocTypeExpression(selection), ts.factory.createIdentifier(name)); - const templates: ts.JSDocTemplateTag[] = []; - ts.forEach(typeParameters, typeParameter => { - const constraint = ts.getEffectiveConstraintOfTypeParameter(typeParameter); - const parameter = ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, typeParameter.name); - const template = ts.factory.createJSDocTemplateTag(ts.factory.createIdentifier("template"), constraint && ts.cast(constraint, ts.isJSDocTypeExpression), [parameter]); - templates.push(template); - }); + const newTypeNode = ts.factory.createTypeAliasDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, name, typeParameters.map(id => ts.factory.updateTypeParameterDeclaration(id, id.modifiers, id.name, id.constraint, /* defaultType */ undefined)), selection); + changes.insertNodeBefore(file, firstStatement, ts.ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); + changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.ExcludeWhitespace }); +} +function doInterfaceChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: InterfaceInfo) { + const { firstStatement, selection, typeParameters, typeElements } = info; + + const newTypeNode = ts.factory.createInterfaceDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, name, typeParameters, + /* heritageClauses */ undefined, typeElements); + ts.setTextRange(newTypeNode, typeElements[0]?.parent); + changes.insertNodeBefore(file, firstStatement, ts.ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); + changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.ExcludeWhitespace }); +} +function doTypedefChange(changes: ts.textChanges.ChangeTracker, file: ts.SourceFile, name: string, info: ExtractInfo) { + const { firstStatement, selection, typeParameters } = info; + + ts.setEmitFlags(selection, ts.EmitFlags.NoComments | ts.EmitFlags.NoNestedComments); + const node = ts.factory.createJSDocTypedefTag(ts.factory.createIdentifier("typedef"), ts.factory.createJSDocTypeExpression(selection), ts.factory.createIdentifier(name)); + const templates: ts.JSDocTemplateTag[] = []; + ts.forEach(typeParameters, typeParameter => { + const constraint = ts.getEffectiveConstraintOfTypeParameter(typeParameter); + const parameter = ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, typeParameter.name); + const template = ts.factory.createJSDocTemplateTag(ts.factory.createIdentifier("template"), constraint && ts.cast(constraint, ts.isJSDocTypeExpression), [parameter]); + templates.push(template); + }); - changes.insertNodeBefore(file, firstStatement, ts.factory.createJSDocComment(/* comment */ undefined, ts.factory.createNodeArray(ts.concatenate(templates, [node]))), /* blankLineBetween */ true); - changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); - } + changes.insertNodeBefore(file, firstStatement, ts.factory.createJSDocComment(/* comment */ undefined, ts.factory.createNodeArray(ts.concatenate(templates, [node]))), /* blankLineBetween */ true); + changes.replaceNode(file, selection, ts.factory.createTypeReferenceNode(name, typeParameters.map(id => ts.factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); +} } diff --git a/src/services/refactors/generateGetAccessorAndSetAccessor.ts b/src/services/refactors/generateGetAccessorAndSetAccessor.ts index 7c4d4a838cf16..55193a1f97167 100644 --- a/src/services/refactors/generateGetAccessorAndSetAccessor.ts +++ b/src/services/refactors/generateGetAccessorAndSetAccessor.ts @@ -1,54 +1,54 @@ /* @internal */ namespace ts.refactor.generateGetAccessorAndSetAccessor { - const actionName = "Generate 'get' and 'set' accessors"; - const actionDescription = ts.Diagnostics.Generate_get_and_set_accessors.message; +const actionName = "Generate 'get' and 'set' accessors"; +const actionDescription = ts.Diagnostics.Generate_get_and_set_accessors.message; - const generateGetSetAction = { - name: actionName, - description: actionDescription, - kind: "refactor.rewrite.property.generateAccessors", - }; - ts.refactor.registerRefactor(actionName, { - kinds: [generateGetSetAction.kind], - getEditsForAction: function getRefactorActionsToGenerateGetAndSetAccessors(context, actionName) { - if (!context.endPosition) - return undefined; - const info = ts.codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition); - ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); - const edits = ts.codefix.generateAccessorFromProperty(context.file, context.program, context.startPosition, context.endPosition, context, actionName); - if (!edits) - return undefined; +const generateGetSetAction = { + name: actionName, + description: actionDescription, + kind: "refactor.rewrite.property.generateAccessors", +}; +ts.refactor.registerRefactor(actionName, { + kinds: [generateGetSetAction.kind], + getEditsForAction: function getRefactorActionsToGenerateGetAndSetAccessors(context, actionName) { + if (!context.endPosition) + return undefined; + const info = ts.codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition); + ts.Debug.assert(info && !ts.refactor.isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = ts.codefix.generateAccessorFromProperty(context.file, context.program, context.startPosition, context.endPosition, context, actionName); + if (!edits) + return undefined; - const renameFilename = context.file.fileName; - const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName; - const renameLocationOffset = ts.isIdentifier(nameNeedRename) ? 0 : -1; - const renameLocation = renameLocationOffset + ts.getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ ts.isParameter(info.declaration)); - - return { renameFilename, renameLocation, edits }; - }, - getAvailableActions(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - if (!context.endPosition) - return ts.emptyArray; - const info = ts.codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition, context.triggerReason === "invoked"); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - return [{ - name: actionName, - description: actionDescription, - actions: [generateGetSetAction], - }]; - } - - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ - name: actionName, - description: actionDescription, - actions: [{ ...generateGetSetAction, notApplicableReason: info.error }], - }]; - } + const renameFilename = context.file.fileName; + const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName; + const renameLocationOffset = ts.isIdentifier(nameNeedRename) ? 0 : -1; + const renameLocation = renameLocationOffset + ts.getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ ts.isParameter(info.declaration)); + return { renameFilename, renameLocation, edits }; + }, + getAvailableActions(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + if (!context.endPosition) + return ts.emptyArray; + const info = ts.codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition, context.triggerReason === "invoked"); + if (!info) return ts.emptyArray; + if (!ts.refactor.isRefactorErrorInfo(info)) { + return [{ + name: actionName, + description: actionDescription, + actions: [generateGetSetAction], + }]; } - }); + + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: actionName, + description: actionDescription, + actions: [{ ...generateGetSetAction, notApplicableReason: info.error }], + }]; + } + + return ts.emptyArray; + } +}); } diff --git a/src/services/refactors/helpers.ts b/src/services/refactors/helpers.ts index 009257fc95bd3..d044d7b710123 100644 --- a/src/services/refactors/helpers.ts +++ b/src/services/refactors/helpers.ts @@ -1,27 +1,27 @@ /* @internal */ namespace ts.refactor { - /** - * Returned by refactor functions when some error message needs to be surfaced to users. - */ - export interface RefactorErrorInfo { - error: string; - } - ; +/** + * Returned by refactor functions when some error message needs to be surfaced to users. + */ +export interface RefactorErrorInfo { + error: string; +} +; - /** - * Checks if some refactor info has refactor error info. - */ - export function isRefactorErrorInfo(info: unknown): info is RefactorErrorInfo { - return (info as RefactorErrorInfo).error !== undefined; - } +/** + * Checks if some refactor info has refactor error info. + */ +export function isRefactorErrorInfo(info: unknown): info is RefactorErrorInfo { + return (info as RefactorErrorInfo).error !== undefined; +} - /** - * Checks if string "known" begins with string "requested". - * Used to match requested kinds with a known kind. - */ - export function refactorKindBeginsWith(known: string, requested: string | undefined): boolean { - if (!requested) - return true; - return known.substr(0, requested.length) === requested; - } +/** + * Checks if string "known" begins with string "requested". + * Used to match requested kinds with a known kind. + */ +export function refactorKindBeginsWith(known: string, requested: string | undefined): boolean { + if (!requested) + return true; + return known.substr(0, requested.length) === requested; +} } diff --git a/src/services/refactors/inferFunctionReturnType.ts b/src/services/refactors/inferFunctionReturnType.ts index 7095bd377f70b..bc68142fd2781 100644 --- a/src/services/refactors/inferFunctionReturnType.ts +++ b/src/services/refactors/inferFunctionReturnType.ts @@ -1,113 +1,113 @@ /* @internal */ namespace ts.refactor.inferFunctionReturnType { - const refactorName = "Infer function return type"; - const refactorDescription = ts.Diagnostics.Infer_function_return_type.message; +const refactorName = "Infer function return type"; +const refactorDescription = ts.Diagnostics.Infer_function_return_type.message; - const inferReturnTypeAction = { - name: refactorName, - description: refactorDescription, - kind: "refactor.rewrite.function.returnType" - }; - ts.refactor.registerRefactor(refactorName, { - kinds: [inferReturnTypeAction.kind], - getEditsForAction: getRefactorEditsToInferReturnType, - getAvailableActions: getRefactorActionsToInferReturnType - }); +const inferReturnTypeAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.function.returnType" +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [inferReturnTypeAction.kind], + getEditsForAction: getRefactorEditsToInferReturnType, + getAvailableActions: getRefactorActionsToInferReturnType +}); - function getRefactorEditsToInferReturnType(context: ts.RefactorContext): ts.RefactorEditInfo | undefined { - const info = getInfo(context); - if (info && !ts.refactor.isRefactorErrorInfo(info)) { - const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, t, info.declaration, info.returnTypeNode)); - return { renameFilename: undefined, renameLocation: undefined, edits }; - } - return undefined; +function getRefactorEditsToInferReturnType(context: ts.RefactorContext): ts.RefactorEditInfo | undefined { + const info = getInfo(context); + if (info && !ts.refactor.isRefactorErrorInfo(info)) { + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, t, info.declaration, info.returnTypeNode)); + return { renameFilename: undefined, renameLocation: undefined, edits }; } + return undefined; +} - function getRefactorActionsToInferReturnType(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { - const info = getInfo(context); - if (!info) - return ts.emptyArray; - if (!ts.refactor.isRefactorErrorInfo(info)) { - return [{ - name: refactorName, - description: refactorDescription, - actions: [inferReturnTypeAction] - }]; - } - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ - name: refactorName, - description: refactorDescription, - actions: [{ ...inferReturnTypeAction, notApplicableReason: info.error }] - }]; - } +function getRefactorActionsToInferReturnType(context: ts.RefactorContext): readonly ts.ApplicableRefactorInfo[] { + const info = getInfo(context); + if (!info) return ts.emptyArray; + if (!ts.refactor.isRefactorErrorInfo(info)) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [inferReturnTypeAction] + }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [{ ...inferReturnTypeAction, notApplicableReason: info.error }] + }]; } + return ts.emptyArray; +} - type ConvertibleDeclaration = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration; +type ConvertibleDeclaration = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration; - interface FunctionInfo { - declaration: ConvertibleDeclaration; - returnTypeNode: ts.TypeNode; - } +interface FunctionInfo { + declaration: ConvertibleDeclaration; + returnTypeNode: ts.TypeNode; +} - function doChange(sourceFile: ts.SourceFile, changes: ts.textChanges.ChangeTracker, declaration: ConvertibleDeclaration, typeNode: ts.TypeNode) { - const closeParen = ts.findChildOfKind(declaration, ts.SyntaxKind.CloseParenToken, sourceFile); - const needParens = ts.isArrowFunction(declaration) && closeParen === undefined; - const endNode = needParens ? ts.first(declaration.parameters) : closeParen; - if (endNode) { - if (needParens) { - changes.insertNodeBefore(sourceFile, endNode, ts.factory.createToken(ts.SyntaxKind.OpenParenToken)); - changes.insertNodeAfter(sourceFile, endNode, ts.factory.createToken(ts.SyntaxKind.CloseParenToken)); - } - changes.insertNodeAt(sourceFile, endNode.end, typeNode, { prefix: ": " }); +function doChange(sourceFile: ts.SourceFile, changes: ts.textChanges.ChangeTracker, declaration: ConvertibleDeclaration, typeNode: ts.TypeNode) { + const closeParen = ts.findChildOfKind(declaration, ts.SyntaxKind.CloseParenToken, sourceFile); + const needParens = ts.isArrowFunction(declaration) && closeParen === undefined; + const endNode = needParens ? ts.first(declaration.parameters) : closeParen; + if (endNode) { + if (needParens) { + changes.insertNodeBefore(sourceFile, endNode, ts.factory.createToken(ts.SyntaxKind.OpenParenToken)); + changes.insertNodeAfter(sourceFile, endNode, ts.factory.createToken(ts.SyntaxKind.CloseParenToken)); } + changes.insertNodeAt(sourceFile, endNode.end, typeNode, { prefix: ": " }); } +} - function getInfo(context: ts.RefactorContext): FunctionInfo | ts.refactor.RefactorErrorInfo | undefined { - if (ts.isInJSFile(context.file) || !ts.refactor.refactorKindBeginsWith(inferReturnTypeAction.kind, context.kind)) - return; - const token = ts.getTokenAtPosition(context.file, context.startPosition); - const declaration = ts.findAncestor(token, n => ts.isBlock(n) || n.parent && ts.isArrowFunction(n.parent) && (n.kind === ts.SyntaxKind.EqualsGreaterThanToken || n.parent.body === n) ? "quit" : - isConvertibleDeclaration(n)) as ConvertibleDeclaration | undefined; - if (!declaration || !declaration.body || declaration.type) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Return_type_must_be_inferred_from_a_function) }; - } +function getInfo(context: ts.RefactorContext): FunctionInfo | ts.refactor.RefactorErrorInfo | undefined { + if (ts.isInJSFile(context.file) || !ts.refactor.refactorKindBeginsWith(inferReturnTypeAction.kind, context.kind)) + return; + const token = ts.getTokenAtPosition(context.file, context.startPosition); + const declaration = ts.findAncestor(token, n => ts.isBlock(n) || n.parent && ts.isArrowFunction(n.parent) && (n.kind === ts.SyntaxKind.EqualsGreaterThanToken || n.parent.body === n) ? "quit" : + isConvertibleDeclaration(n)) as ConvertibleDeclaration | undefined; + if (!declaration || !declaration.body || declaration.type) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Return_type_must_be_inferred_from_a_function) }; + } - const typeChecker = context.program.getTypeChecker(); - const returnType = tryGetReturnType(typeChecker, declaration); - if (!returnType) { - return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_determine_function_return_type) }; - } + const typeChecker = context.program.getTypeChecker(); + const returnType = tryGetReturnType(typeChecker, declaration); + if (!returnType) { + return { error: ts.getLocaleSpecificMessage(ts.Diagnostics.Could_not_determine_function_return_type) }; + } - const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, ts.NodeBuilderFlags.NoTruncation); - if (returnTypeNode) { - return { declaration, returnTypeNode }; - } + const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, ts.NodeBuilderFlags.NoTruncation); + if (returnTypeNode) { + return { declaration, returnTypeNode }; } +} - function isConvertibleDeclaration(node: ts.Node): node is ConvertibleDeclaration { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.MethodDeclaration: - return true; - default: - return false; - } +function isConvertibleDeclaration(node: ts.Node): node is ConvertibleDeclaration { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.MethodDeclaration: + return true; + default: + return false; } +} - function tryGetReturnType(typeChecker: ts.TypeChecker, node: ConvertibleDeclaration): ts.Type | undefined { - if (typeChecker.isImplementationOfOverload(node)) { - const signatures = typeChecker.getTypeAtLocation(node).getCallSignatures(); - if (signatures.length > 1) { - return typeChecker.getUnionType(ts.mapDefined(signatures, s => s.getReturnType())); - } - } - const signature = typeChecker.getSignatureFromDeclaration(node); - if (signature) { - return typeChecker.getReturnTypeOfSignature(signature); +function tryGetReturnType(typeChecker: ts.TypeChecker, node: ConvertibleDeclaration): ts.Type | undefined { + if (typeChecker.isImplementationOfOverload(node)) { + const signatures = typeChecker.getTypeAtLocation(node).getCallSignatures(); + if (signatures.length > 1) { + return typeChecker.getUnionType(ts.mapDefined(signatures, s => s.getReturnType())); } } + const signature = typeChecker.getSignatureFromDeclaration(node); + if (signature) { + return typeChecker.getReturnTypeOfSignature(signature); + } +} } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 9315693067484..29ebc5a769c06 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,827 +1,827 @@ /* @internal */ namespace ts.refactor { - const refactorName = "Move to a new file"; - const description = ts.getLocaleSpecificMessage(ts.Diagnostics.Move_to_a_new_file); - - const moveToNewFileAction = { - name: refactorName, - description, - kind: "refactor.move.newFile", +const refactorName = "Move to a new file"; +const description = ts.getLocaleSpecificMessage(ts.Diagnostics.Move_to_a_new_file); + +const moveToNewFileAction = { + name: refactorName, + description, + kind: "refactor.move.newFile", +}; +ts.refactor.registerRefactor(refactorName, { + kinds: [moveToNewFileAction.kind], + getAvailableActions: function getRefactorActionsToMoveToNewFile(context): readonly ts.ApplicableRefactorInfo[] { + const statements = getStatementsToMove(context); + if (context.preferences.allowTextChangesInNewFiles && statements) { + return [{ name: refactorName, description, actions: [moveToNewFileAction] }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ name: refactorName, description, actions: [{ ...moveToNewFileAction, notApplicableReason: ts.getLocaleSpecificMessage(ts.Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] + }]; + } + return ts.emptyArray; + }, + getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): ts.RefactorEditInfo { + ts.Debug.assert(actionName === refactorName, "Wrong refactor invoked"); + const statements = ts.Debug.checkDefined(getStatementsToMove(context)); + const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } +}); + +interface RangeToMove { + readonly toMove: readonly ts.Statement[]; + readonly afterLast: ts.Statement | undefined; +} +function getRangeToMove(context: ts.RefactorContext): RangeToMove | undefined { + const { file } = context; + const range = ts.createTextRangeFromSpan(ts.getRefactorContextSpan(context)); + const { statements } = file; + + const startNodeIndex = ts.findIndex(statements, s => s.end > range.pos); + if (startNodeIndex === -1) + return undefined; + + const startStatement = statements[startNodeIndex]; + if (ts.isNamedDeclaration(startStatement) && startStatement.name && ts.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 = ts.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], }; - ts.refactor.registerRefactor(refactorName, { - kinds: [moveToNewFileAction.kind], - getAvailableActions: function getRefactorActionsToMoveToNewFile(context): readonly ts.ApplicableRefactorInfo[] { - const statements = getStatementsToMove(context); - if (context.preferences.allowTextChangesInNewFiles && statements) { - return [{ name: refactorName, description, actions: [moveToNewFileAction] }]; - } - if (context.preferences.provideRefactorNotApplicableReason) { - return [{ name: refactorName, description, actions: [{ ...moveToNewFileAction, notApplicableReason: ts.getLocaleSpecificMessage(ts.Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] - }]; - } - return ts.emptyArray; - }, - getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): ts.RefactorEditInfo { - ts.Debug.assert(actionName === refactorName, "Wrong refactor invoked"); - const statements = ts.Debug.checkDefined(getStatementsToMove(context)); - const edits = ts.textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - } - }); - - interface RangeToMove { - readonly toMove: readonly ts.Statement[]; - readonly afterLast: ts.Statement | undefined; - } - function getRangeToMove(context: ts.RefactorContext): RangeToMove | undefined { - const { file } = context; - const range = ts.createTextRangeFromSpan(ts.getRefactorContextSpan(context)); - const { statements } = file; +} - const startNodeIndex = ts.findIndex(statements, s => s.end > range.pos); - if (startNodeIndex === -1) - return undefined; +function doChange(oldFile: ts.SourceFile, program: ts.Program, toMove: ToMove, changes: ts.textChanges.ChangeTracker, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): void { + const checker = program.getTypeChecker(); + const usage = getUsageInfo(oldFile, toMove.all, checker); - const startStatement = statements[startNodeIndex]; - if (ts.isNamedDeclaration(startStatement) && startStatement.name && ts.rangeContainsRange(startStatement.name, range)) { - return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] }; - } + const currentDirectory = ts.getDirectoryPath(oldFile.fileName); + const extension = ts.extensionFromPath(oldFile.fileName); + const newModuleName = makeUniqueModuleName(getNewModuleName(usage.movedSymbols), extension, currentDirectory, host); + const newFileNameWithExtension = newModuleName + extension; - // 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 = ts.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; + // If previous file was global, this is easy. + changes.createNewFile(oldFile, ts.combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences)); + addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, ts.hostGetCanonicalFileName(host)); +} - return { - toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex), - afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex], - }; - } +interface StatementRange { + readonly first: ts.Statement; + readonly afterLast: ts.Statement | undefined; +} +interface ToMove { + readonly all: readonly ts.Statement[]; + readonly ranges: readonly StatementRange[]; +} - function doChange(oldFile: ts.SourceFile, program: ts.Program, toMove: ToMove, changes: ts.textChanges.ChangeTracker, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): void { - const checker = program.getTypeChecker(); - const usage = getUsageInfo(oldFile, toMove.all, checker); +function getStatementsToMove(context: ts.RefactorContext): ToMove | undefined { + const rangeToMove = getRangeToMove(context); + if (rangeToMove === undefined) + return undefined; + const all: ts.Statement[] = []; + const ranges: StatementRange[] = []; + const { toMove, afterLast } = rangeToMove; + ts.getRangesWhere(toMove, isAllowedStatementToMove, (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 }; +} - const currentDirectory = ts.getDirectoryPath(oldFile.fileName); - const extension = ts.extensionFromPath(oldFile.fileName); - const newModuleName = makeUniqueModuleName(getNewModuleName(usage.movedSymbols), extension, currentDirectory, host); - const newFileNameWithExtension = newModuleName + extension; +function isAllowedStatementToMove(statement: ts.Statement): boolean { + // Filters imports and prologue directives 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. + // Prologue directives will be copied to the new file and should be left in the old file. + return !isPureImport(statement) && !ts.isPrologueDirective(statement); + ; +} - // If previous file was global, this is easy. - changes.createNewFile(oldFile, ts.combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences)); - addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, ts.hostGetCanonicalFileName(host)); +function isPureImport(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + return true; + case ts.SyntaxKind.ImportEqualsDeclaration: + return !ts.hasSyntacticModifier(node, ts.ModifierFlags.Export); + case ts.SyntaxKind.VariableStatement: + return (node as ts.VariableStatement).declarationList.declarations.every(d => !!d.initializer && ts.isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); + default: + return false; } +} - interface StatementRange { - readonly first: ts.Statement; - readonly afterLast: ts.Statement | undefined; +function addNewFileToTsconfig(program: ts.Program, changes: ts.textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: ts.GetCanonicalFileName): void { + const cfg = program.getCompilerOptions().configFile; + if (!cfg) + return; + const newFileAbsolutePath = ts.normalizePath(ts.combinePaths(oldFileName, "..", newFileNameWithExtension)); + const newFilePath = ts.getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); + const cfgObject = cfg.statements[0] && ts.tryCast(cfg.statements[0].expression, ts.isObjectLiteralExpression); + const filesProp = cfgObject && ts.find(cfgObject.properties, (prop): prop is ts.PropertyAssignment => ts.isPropertyAssignment(prop) && ts.isStringLiteral(prop.name) && prop.name.text === "files"); + if (filesProp && ts.isArrayLiteralExpression(filesProp.initializer)) { + changes.insertNodeInListAfter(cfg, ts.last(filesProp.initializer.elements), ts.factory.createStringLiteral(newFilePath), filesProp.initializer.elements); } - interface ToMove { - readonly all: readonly ts.Statement[]; - readonly ranges: readonly StatementRange[]; - } - - function getStatementsToMove(context: ts.RefactorContext): ToMove | undefined { - const rangeToMove = getRangeToMove(context); - if (rangeToMove === undefined) - return undefined; - const all: ts.Statement[] = []; - const ranges: StatementRange[] = []; - const { toMove, afterLast } = rangeToMove; - ts.getRangesWhere(toMove, isAllowedStatementToMove, (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 isAllowedStatementToMove(statement: ts.Statement): boolean { - // Filters imports and prologue directives 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. - // Prologue directives will be copied to the new file and should be left in the old file. - return !isPureImport(statement) && !ts.isPrologueDirective(statement); - ; - } - - function isPureImport(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - return true; - case ts.SyntaxKind.ImportEqualsDeclaration: - return !ts.hasSyntacticModifier(node, ts.ModifierFlags.Export); - case ts.SyntaxKind.VariableStatement: - return (node as ts.VariableStatement).declarationList.declarations.every(d => !!d.initializer && ts.isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); - default: - return false; - } +} +function getNewStatementsAndRemoveFromOldFile(oldFile: ts.SourceFile, usage: UsageInfo, changes: ts.textChanges.ChangeTracker, toMove: ToMove, program: ts.Program, newModuleName: string, preferences: ts.UserPreferences) { + const checker = program.getTypeChecker(); + const prologueDirectives = ts.takeWhile(oldFile.statements, ts.isPrologueDirective); + if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) { + deleteMovedStatements(oldFile, toMove.ranges, changes); + return [...prologueDirectives, ...toMove.all]; } - function addNewFileToTsconfig(program: ts.Program, changes: ts.textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: ts.GetCanonicalFileName): void { - const cfg = program.getCompilerOptions().configFile; - if (!cfg) - return; - const newFileAbsolutePath = ts.normalizePath(ts.combinePaths(oldFileName, "..", newFileNameWithExtension)); - const newFilePath = ts.getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); - const cfgObject = cfg.statements[0] && ts.tryCast(cfg.statements[0].expression, ts.isObjectLiteralExpression); - const filesProp = cfgObject && ts.find(cfgObject.properties, (prop): prop is ts.PropertyAssignment => ts.isPropertyAssignment(prop) && ts.isStringLiteral(prop.name) && prop.name.text === "files"); - if (filesProp && ts.isArrayLiteralExpression(filesProp.initializer)) { - changes.insertNodeInListAfter(cfg, ts.last(filesProp.initializer.elements), ts.factory.createStringLiteral(newFilePath), filesProp.initializer.elements); - } + const useEsModuleSyntax = !!oldFile.externalModuleIndicator; + const quotePreference = ts.getQuotePreference(oldFile, preferences); + const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEsModuleSyntax, quotePreference); + if (importsFromNewFile) { + ts.insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true); } - function getNewStatementsAndRemoveFromOldFile(oldFile: ts.SourceFile, usage: UsageInfo, changes: ts.textChanges.ChangeTracker, toMove: ToMove, program: ts.Program, newModuleName: string, preferences: ts.UserPreferences) { - const checker = program.getTypeChecker(); - const prologueDirectives = ts.takeWhile(oldFile.statements, ts.isPrologueDirective); - if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) { - deleteMovedStatements(oldFile, toMove.ranges, changes); - return [...prologueDirectives, ...toMove.all]; - } - - const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = ts.getQuotePreference(oldFile, preferences); - const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEsModuleSyntax, quotePreference); - if (importsFromNewFile) { - ts.insertImports(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); - - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEsModuleSyntax, quotePreference); - const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); - if (imports.length && body.length) { - return [ - ...prologueDirectives, - ...imports, - ts.SyntaxKind.NewLineTrivia as const, - ...body - ]; - } + deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); + deleteMovedStatements(oldFile, toMove.ranges, changes); + updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEsModuleSyntax, quotePreference); + const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); + if (imports.length && body.length) { return [ ...prologueDirectives, ...imports, - ...body, + ts.SyntaxKind.NewLineTrivia as const, + ...body ]; } - function deleteMovedStatements(sourceFile: ts.SourceFile, moved: readonly StatementRange[], changes: ts.textChanges.ChangeTracker) { - for (const { first, afterLast } of moved) { - changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); - } - } + return [ + ...prologueDirectives, + ...imports, + ...body, + ]; +} - function deleteUnusedOldImports(oldFile: ts.SourceFile, toMove: readonly ts.Statement[], changes: ts.textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: ts.TypeChecker) { - for (const statement of oldFile.statements) { - if (ts.contains(toMove, statement)) - continue; - forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); - } +function deleteMovedStatements(sourceFile: ts.SourceFile, moved: readonly StatementRange[], changes: ts.textChanges.ChangeTracker) { + for (const { first, afterLast } of moved) { + changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); } +} - function updateImportsInOtherFiles(changes: ts.textChanges.ChangeTracker, program: ts.Program, oldFile: ts.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: ts.Identifier): boolean => { - const symbol = ts.isBindingElement(name.parent) - ? ts.getPropertySymbolFromBindingElement(checker, name.parent as ts.ObjectBindingElementWithoutPropertyName) - : ts.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 = ts.combinePaths(ts.getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); - const newImportDeclaration = filterImport(importNode, ts.factory.createStringLiteral(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 deleteUnusedOldImports(oldFile: ts.SourceFile, toMove: readonly ts.Statement[], changes: ts.textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: ts.TypeChecker) { + for (const statement of oldFile.statements) { + if (ts.contains(toMove, statement)) + continue; + forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); } +} - function getNamespaceLikeImport(node: SupportedImport): ts.Identifier | undefined { - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport ? - node.importClause.namedBindings.name : undefined; - case ts.SyntaxKind.ImportEqualsDeclaration: - return node.name; - case ts.SyntaxKind.VariableDeclaration: - return ts.tryCast(node.name, ts.isIdentifier); - default: - return ts.Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); +function updateImportsInOtherFiles(changes: ts.textChanges.ChangeTracker, program: ts.Program, oldFile: ts.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: ts.Identifier): boolean => { + const symbol = ts.isBindingElement(name.parent) + ? ts.getPropertySymbolFromBindingElement(checker, name.parent as ts.ObjectBindingElementWithoutPropertyName) + : ts.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 = ts.combinePaths(ts.getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); + const newImportDeclaration = filterImport(importNode, ts.factory.createStringLiteral(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 updateNamespaceLikeImport(changes: ts.textChanges.ChangeTracker, sourceFile: ts.SourceFile, checker: ts.TypeChecker, movedSymbols: ReadonlySymbolSet, newModuleName: string, newModuleSpecifier: string, oldImportId: ts.Identifier, oldImportNode: SupportedImport): void { - const preferredNewNamespaceName = ts.codefix.moduleSpecifierToValidIdentifier(newModuleName, ts.ScriptTarget.ESNext); - let needUniqueName = false; - const toChange: ts.Identifier[] = []; - ts.FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { - if (!ts.isPropertyAccessExpression(ref.parent)) - return; - needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, ts.SymbolFlags.All, /*excludeGlobals*/ true); - if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { - toChange.push(ref); - } - }); +} - if (toChange.length) { - const newNamespaceName = needUniqueName ? ts.getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; - for (const ref of toChange) { - changes.replaceNode(sourceFile, ref, ts.factory.createIdentifier(newNamespaceName)); - } - changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier)); - } +function getNamespaceLikeImport(node: SupportedImport): ts.Identifier | undefined { + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport ? + node.importClause.namedBindings.name : undefined; + case ts.SyntaxKind.ImportEqualsDeclaration: + return node.name; + case ts.SyntaxKind.VariableDeclaration: + return ts.tryCast(node.name, ts.isIdentifier); + default: + return ts.Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); } +} +function updateNamespaceLikeImport(changes: ts.textChanges.ChangeTracker, sourceFile: ts.SourceFile, checker: ts.TypeChecker, movedSymbols: ReadonlySymbolSet, newModuleName: string, newModuleSpecifier: string, oldImportId: ts.Identifier, oldImportNode: SupportedImport): void { + const preferredNewNamespaceName = ts.codefix.moduleSpecifierToValidIdentifier(newModuleName, ts.ScriptTarget.ESNext); + let needUniqueName = false; + const toChange: ts.Identifier[] = []; + ts.FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { + if (!ts.isPropertyAccessExpression(ref.parent)) + return; + needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, ts.SymbolFlags.All, /*excludeGlobals*/ true); + if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { + toChange.push(ref); + } + }); - function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): ts.Node { - const newNamespaceId = ts.factory.createIdentifier(newNamespaceName); - const newModuleString = ts.factory.createStringLiteral(newModuleSpecifier); - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - return ts.factory.createImportDeclaration( - /*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, ts.factory.createNamespaceImport(newNamespaceId)), newModuleString, - /*assertClause*/ undefined); - case ts.SyntaxKind.ImportEqualsDeclaration: - return ts.factory.createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, ts.factory.createExternalModuleReference(newModuleString)); - case ts.SyntaxKind.VariableDeclaration: - return ts.factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString)); - default: - return ts.Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); + if (toChange.length) { + const newNamespaceName = needUniqueName ? ts.getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; + for (const ref of toChange) { + changes.replaceNode(sourceFile, ref, ts.factory.createIdentifier(newNamespaceName)); } + changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier)); } +} - function moduleSpecifierFromImport(i: SupportedImport): ts.StringLiteralLike { - return (i.kind === ts.SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === ts.SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression - : i.initializer.arguments[0]); +function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): ts.Node { + const newNamespaceId = ts.factory.createIdentifier(newNamespaceName); + const newModuleString = ts.factory.createStringLiteral(newModuleSpecifier); + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + return ts.factory.createImportDeclaration( + /*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, ts.factory.createNamespaceImport(newNamespaceId)), newModuleString, + /*assertClause*/ undefined); + case ts.SyntaxKind.ImportEqualsDeclaration: + return ts.factory.createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, ts.factory.createExternalModuleReference(newModuleString)); + case ts.SyntaxKind.VariableDeclaration: + return ts.factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString)); + default: + return ts.Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); } +} - function forEachImportInStatement(statement: ts.Statement, cb: (importNode: SupportedImport) => void): void { - if (ts.isImportDeclaration(statement)) { - if (ts.isStringLiteral(statement.moduleSpecifier)) - cb(statement as SupportedImport); - } - else if (ts.isImportEqualsDeclaration(statement)) { - if (ts.isExternalModuleReference(statement.moduleReference) && ts.isStringLiteralLike(statement.moduleReference.expression)) { - cb(statement as SupportedImport); - } - } - else if (ts.isVariableStatement(statement)) { - for (const decl of statement.declarationList.declarations) { - if (decl.initializer && ts.isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { - cb(decl as SupportedImport); - } - } +function moduleSpecifierFromImport(i: SupportedImport): ts.StringLiteralLike { + return (i.kind === ts.SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === ts.SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression + : i.initializer.arguments[0]); +} + +function forEachImportInStatement(statement: ts.Statement, cb: (importNode: SupportedImport) => void): void { + if (ts.isImportDeclaration(statement)) { + if (ts.isStringLiteral(statement.moduleSpecifier)) + cb(statement as SupportedImport); + } + else if (ts.isImportEqualsDeclaration(statement)) { + if (ts.isExternalModuleReference(statement.moduleReference) && ts.isStringLiteralLike(statement.moduleReference.expression)) { + cb(statement as SupportedImport); } } - - type SupportedImport = (ts.ImportDeclaration & { - moduleSpecifier: ts.StringLiteralLike; - }) | (ts.ImportEqualsDeclaration & { - moduleReference: ts.ExternalModuleReference & { - expression: ts.StringLiteralLike; - }; - }) | (ts.VariableDeclaration & { - initializer: ts.RequireOrImportCall; - }); - type SupportedImportStatement = ts.ImportDeclaration | ts.ImportEqualsDeclaration | ts.VariableStatement; - function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: ts.QuotePreference): ts.AnyImportOrRequireStatement | undefined { - let defaultImport: ts.Identifier | undefined; - const imports: string[] = []; - newFileNeedExport.forEach(symbol => { - if (symbol.escapedName === ts.InternalSymbolName.Default) { - defaultImport = ts.factory.createIdentifier(ts.symbolNameNoDefault(symbol)!); // TODO: GH#18217 + else if (ts.isVariableStatement(statement)) { + for (const decl of statement.declarationList.declarations) { + if (decl.initializer && ts.isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { + cb(decl as SupportedImport); } - else { - imports.push(symbol.name); - } - }); - return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); + } } +} - function makeImportOrRequire(defaultImport: ts.Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: ts.QuotePreference): ts.AnyImportOrRequireStatement | undefined { - path = ts.ensurePathIsNonModuleName(path); - if (useEs6Imports) { - const specifiers = imports.map(i => ts.factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, ts.factory.createIdentifier(i))); - return ts.makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); +type SupportedImport = (ts.ImportDeclaration & { + moduleSpecifier: ts.StringLiteralLike; +}) | (ts.ImportEqualsDeclaration & { + moduleReference: ts.ExternalModuleReference & { + expression: ts.StringLiteralLike; + }; +}) | (ts.VariableDeclaration & { + initializer: ts.RequireOrImportCall; +}); +type SupportedImportStatement = ts.ImportDeclaration | ts.ImportEqualsDeclaration | ts.VariableStatement; +function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: ts.QuotePreference): ts.AnyImportOrRequireStatement | undefined { + let defaultImport: ts.Identifier | undefined; + const imports: string[] = []; + newFileNeedExport.forEach(symbol => { + if (symbol.escapedName === ts.InternalSymbolName.Default) { + defaultImport = ts.factory.createIdentifier(ts.symbolNameNoDefault(symbol)!); // TODO: GH#18217 } else { - ts.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 => ts.factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); - return bindingElements.length - ? makeVariableStatement(ts.factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(ts.factory.createStringLiteral(path))) as ts.RequireVariableStatement - : undefined; + imports.push(symbol.name); } - } + }); + return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); +} - function makeVariableStatement(name: ts.BindingName, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined, flags: ts.NodeFlags = ts.NodeFlags.Const) { - return ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); +function makeImportOrRequire(defaultImport: ts.Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: ts.QuotePreference): ts.AnyImportOrRequireStatement | undefined { + path = ts.ensurePathIsNonModuleName(path); + if (useEs6Imports) { + const specifiers = imports.map(i => ts.factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, ts.factory.createIdentifier(i))); + return ts.makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); } - - function createRequireCall(moduleSpecifier: ts.StringLiteralLike): ts.CallExpression { - return ts.factory.createCallExpression(ts.factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); + else { + ts.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 => ts.factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); + return bindingElements.length + ? makeVariableStatement(ts.factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(ts.factory.createStringLiteral(path))) as ts.RequireVariableStatement + : undefined; } +} + +function makeVariableStatement(name: ts.BindingName, type: ts.TypeNode | undefined, initializer: ts.Expression | undefined, flags: ts.NodeFlags = ts.NodeFlags.Const) { + return ts.factory.createVariableStatement(/*modifiers*/ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); +} + +function createRequireCall(moduleSpecifier: ts.StringLiteralLike): ts.CallExpression { + return ts.factory.createCallExpression(ts.factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); +} + +function addExports(sourceFile: ts.SourceFile, toMove: readonly ts.Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly ts.Statement[] { + return ts.flatMap(toMove, statement => { + if (isTopLevelDeclarationStatement(statement) && + !isExported(sourceFile, statement, useEs6Exports) && + forEachTopLevelDeclaration(statement, d => needExport.has(ts.Debug.checkDefined(d.symbol)))) { + const exports = addExport(statement, useEs6Exports); + if (exports) + return exports; + } + return statement; + }); +} - function addExports(sourceFile: ts.SourceFile, toMove: readonly ts.Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly ts.Statement[] { - return ts.flatMap(toMove, statement => { - if (isTopLevelDeclarationStatement(statement) && - !isExported(sourceFile, statement, useEs6Exports) && - forEachTopLevelDeclaration(statement, d => needExport.has(ts.Debug.checkDefined(d.symbol)))) { - const exports = addExport(statement, useEs6Exports); - if (exports) - return exports; +function deleteUnusedImports(sourceFile: ts.SourceFile, importDecl: SupportedImport, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean): void { + switch (importDecl.kind) { + case ts.SyntaxKind.ImportDeclaration: + deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); + break; + case ts.SyntaxKind.ImportEqualsDeclaration: + if (isUnused(importDecl.name)) { + changes.delete(sourceFile, importDecl); } - return statement; - }); + break; + case ts.SyntaxKind.VariableDeclaration: + deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); + break; + default: + ts.Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); } - - function deleteUnusedImports(sourceFile: ts.SourceFile, importDecl: SupportedImport, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean): void { - switch (importDecl.kind) { - case ts.SyntaxKind.ImportDeclaration: - deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); - break; - case ts.SyntaxKind.ImportEqualsDeclaration: - if (isUnused(importDecl.name)) { - changes.delete(sourceFile, importDecl); +} +function deleteUnusedImportsInDeclaration(sourceFile: ts.SourceFile, importDecl: ts.ImportDeclaration, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean): void { + if (!importDecl.importClause) + return; + const { name, namedBindings } = importDecl.importClause; + const defaultUnused = !name || isUnused(name); + const namedBindingsUnused = !namedBindings || + (namedBindings.kind === ts.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, ts.factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined)); + } + else if (namedBindings.kind === ts.SyntaxKind.NamedImports) { + for (const element of namedBindings.elements) { + if (isUnused(element.name)) + changes.delete(sourceFile, element); } - break; - case ts.SyntaxKind.VariableDeclaration: - deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); - break; - default: - ts.Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); + } } } - function deleteUnusedImportsInDeclaration(sourceFile: ts.SourceFile, importDecl: ts.ImportDeclaration, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean): void { - if (!importDecl.importClause) - return; - const { name, namedBindings } = importDecl.importClause; - const defaultUnused = !name || isUnused(name); - const namedBindingsUnused = !namedBindings || - (namedBindings.kind === ts.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) { +} +function deleteUnusedImportsInVariableDeclaration(sourceFile: ts.SourceFile, varDecl: ts.VariableDeclaration, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean) { + const { name } = varDecl; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + if (isUnused(name)) { changes.delete(sourceFile, name); } - if (namedBindings) { - if (namedBindingsUnused) { - changes.replaceNode(sourceFile, importDecl.importClause, ts.factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined)); - } - else if (namedBindings.kind === ts.SyntaxKind.NamedImports) { - for (const element of namedBindings.elements) { - if (isUnused(element.name)) - changes.delete(sourceFile, element); + break; + case ts.SyntaxKind.ArrayBindingPattern: + break; + case ts.SyntaxKind.ObjectBindingPattern: + if (name.elements.every(e => ts.isIdentifier(e.name) && isUnused(e.name))) { + changes.delete(sourceFile, ts.isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); + } + else { + for (const element of name.elements) { + if (ts.isIdentifier(element.name) && isUnused(element.name)) { + changes.delete(sourceFile, element.name); } } } - } + break; } - function deleteUnusedImportsInVariableDeclaration(sourceFile: ts.SourceFile, varDecl: ts.VariableDeclaration, changes: ts.textChanges.ChangeTracker, isUnused: (name: ts.Identifier) => boolean) { - const { name } = varDecl; - switch (name.kind) { - case ts.SyntaxKind.Identifier: - if (isUnused(name)) { - changes.delete(sourceFile, name); - } - break; - case ts.SyntaxKind.ArrayBindingPattern: - break; - case ts.SyntaxKind.ObjectBindingPattern: - if (name.elements.every(e => ts.isIdentifier(e.name) && isUnused(e.name))) { - changes.delete(sourceFile, ts.isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); - } - else { - for (const element of name.elements) { - if (ts.isIdentifier(element.name) && isUnused(element.name)) { - changes.delete(sourceFile, element.name); - } - } - } - break; - } +} + +function getNewFileImportsAndAddExportInOldFile(oldFile: ts.SourceFile, importsToCopy: ReadonlySymbolSet, newFileImportsFromOldFile: ReadonlySymbolSet, changes: ts.textChanges.ChangeTracker, checker: ts.TypeChecker, useEsModuleSyntax: boolean, quotePreference: ts.QuotePreference): readonly SupportedImportStatement[] { + const copiedOldImports: SupportedImportStatement[] = []; + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + ts.append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); } - function getNewFileImportsAndAddExportInOldFile(oldFile: ts.SourceFile, importsToCopy: ReadonlySymbolSet, newFileImportsFromOldFile: ReadonlySymbolSet, changes: ts.textChanges.ChangeTracker, checker: ts.TypeChecker, useEsModuleSyntax: boolean, quotePreference: ts.QuotePreference): readonly SupportedImportStatement[] { - const copiedOldImports: SupportedImportStatement[] = []; - for (const oldStatement of oldFile.statements) { - forEachImportInStatement(oldStatement, i => { - ts.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: ts.Identifier | undefined; + const oldFileNamedImports: string[] = []; + const markSeenTop = ts.nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. + newFileImportsFromOldFile.forEach(symbol => { + if (!symbol.declarations) { + return; } + for (const decl of symbol.declarations) { + if (!isTopLevelDeclaration(decl)) + continue; + const name = nameOfTopLevelDeclaration(decl); + if (!name) + continue; - // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. - let oldFileDefault: ts.Identifier | undefined; - const oldFileNamedImports: string[] = []; - const markSeenTop = ts.nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. - newFileImportsFromOldFile.forEach(symbol => { - if (!symbol.declarations) { - return; + const top = getTopLevelDeclarationStatement(decl); + if (markSeenTop(top)) { + addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); } - 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, name, changes, useEsModuleSyntax); - } - if (ts.hasSyntacticModifier(decl, ts.ModifierFlags.Default)) { - oldFileDefault = name; - } - else { - oldFileNamedImports.push(name.text); - } + if (ts.hasSyntacticModifier(decl, ts.ModifierFlags.Default)) { + oldFileDefault = name; } - }); + else { + oldFileNamedImports.push(name.text); + } + } + }); - ts.append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, ts.removeFileExtension(ts.getBaseFileName(oldFile.fileName)), useEsModuleSyntax, quotePreference)); - return copiedOldImports; - } + ts.append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, ts.removeFileExtension(ts.getBaseFileName(oldFile.fileName)), useEsModuleSyntax, quotePreference)); + return copiedOldImports; +} - function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: ts.LanguageServiceHost): string { - let newModuleName = moduleName; - for (let i = 1; ; i++) { - const name = ts.combinePaths(inDirectory, newModuleName + extension); - if (!host.fileExists(name)) - return newModuleName; - newModuleName = `${moduleName}.${i}`; - } +function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: ts.LanguageServiceHost): string { + let newModuleName = moduleName; + for (let i = 1; ; i++) { + const name = ts.combinePaths(inDirectory, newModuleName + extension); + if (!host.fileExists(name)) + return newModuleName; + newModuleName = `${moduleName}.${i}`; } +} - function getNewModuleName(movedSymbols: ReadonlySymbolSet): string { - return movedSymbols.forEachEntry(ts.symbolNameNoDefault) || "newFile"; - } +function getNewModuleName(movedSymbols: ReadonlySymbolSet): string { + return movedSymbols.forEachEntry(ts.symbolNameNoDefault) || "newFile"; +} - interface UsageInfo { - // Symbols whose declarations are moved from the old file to the new file. - readonly movedSymbols: ReadonlySymbolSet; +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; + // 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: ts.SourceFile, toMove: readonly ts.Statement[], checker: ts.TypeChecker): UsageInfo { - const movedSymbols = new SymbolSet(); - const oldImportsNeededByNewFile = new SymbolSet(); - const newFileImportsFromOldFile = new SymbolSet(); + readonly oldImportsNeededByNewFile: ReadonlySymbolSet; + // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. + readonly unusedImportsFromOldFile: ReadonlySymbolSet; +} +function getUsageInfo(oldFile: ts.SourceFile, toMove: readonly ts.Statement[], checker: ts.TypeChecker): UsageInfo { + const movedSymbols = new SymbolSet(); + const oldImportsNeededByNewFile = new SymbolSet(); + const newFileImportsFromOldFile = new SymbolSet(); - const containsJsx = ts.find(toMove, statement => !!(statement.transformFlags & ts.TransformFlags.ContainsJsx)); - const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); - if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) - oldImportsNeededByNewFile.add(jsxNamespaceSymbol); - } + const containsJsx = ts.find(toMove, statement => !!(statement.transformFlags & ts.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(ts.Debug.checkDefined(ts.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); - } + for (const statement of toMove) { + forEachTopLevelDeclaration(statement, decl => { + movedSymbols.add(ts.Debug.checkDefined(ts.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 unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); - const oldFileImportsFromNewFile = new SymbolSet(); - for (const statement of oldFile.statements) { - if (ts.contains(toMove, statement)) - continue; + const oldFileImportsFromNewFile = new SymbolSet(); + for (const statement of oldFile.statements) { + if (ts.contains(toMove, statement)) + continue; - // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. - if (jsxNamespaceSymbol && !!(statement.transformFlags & ts.TransformFlags.ContainsJsx)) { - unusedImportsFromOldFile.delete(jsxNamespaceSymbol); - } + // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. + if (jsxNamespaceSymbol && !!(statement.transformFlags & ts.TransformFlags.ContainsJsx)) { + unusedImportsFromOldFile.delete(jsxNamespaceSymbol); + } - forEachReference(statement, checker, symbol => { - if (movedSymbols.has(symbol)) - oldFileImportsFromNewFile.add(symbol); - unusedImportsFromOldFile.delete(symbol); - }); + forEachReference(statement, checker, symbol => { + if (movedSymbols.has(symbol)) + oldFileImportsFromNewFile.add(symbol); + unusedImportsFromOldFile.delete(symbol); + }); + } + + return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + + function getJsxNamespaceSymbol(containsJsx: ts.Node | undefined) { + if (containsJsx === undefined) { + return undefined; } - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + const jsxNamespace = checker.getJsxNamespace(containsJsx); - function getJsxNamespaceSymbol(containsJsx: ts.Node | undefined) { - if (containsJsx === undefined) { - return undefined; - } + // 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, ts.SymbolFlags.Namespace, /*excludeGlobals*/ true); + return !!jsxNamespaceSymbol && ts.some(jsxNamespaceSymbol.declarations, isInImport) + ? jsxNamespaceSymbol + : undefined; + } +} - const jsxNamespace = checker.getJsxNamespace(containsJsx); +// Below should all be utilities + +function isInImport(decl: ts.Declaration) { + switch (decl.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + return true; + case ts.SyntaxKind.VariableDeclaration: + return isVariableDeclarationInImport(decl as ts.VariableDeclaration); + case ts.SyntaxKind.BindingElement: + return ts.isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); + default: + return false; + } +} +function isVariableDeclarationInImport(decl: ts.VariableDeclaration) { + return ts.isSourceFile(decl.parent.parent.parent) && + !!decl.initializer && ts.isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); +} - // 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, ts.SymbolFlags.Namespace, /*excludeGlobals*/ true); - return !!jsxNamespaceSymbol && ts.some(jsxNamespaceSymbol.declarations, isInImport) - ? jsxNamespaceSymbol +function filterImport(i: SupportedImport, moduleSpecifier: ts.StringLiteralLike, keep: (name: ts.Identifier) => boolean): SupportedImportStatement | undefined { + switch (i.kind) { + case ts.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 + ? ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, defaultImport, namedBindings), moduleSpecifier, /*assertClause*/ undefined) : undefined; } - } - - // Below should all be utilities - - function isInImport(decl: ts.Declaration) { - switch (decl.kind) { - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceImport: - return true; - case ts.SyntaxKind.VariableDeclaration: - return isVariableDeclarationInImport(decl as ts.VariableDeclaration); - case ts.SyntaxKind.BindingElement: - return ts.isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); - default: - return false; + case ts.SyntaxKind.ImportEqualsDeclaration: + return keep(i.name) ? i : undefined; + case ts.SyntaxKind.VariableDeclaration: { + const name = filterBindingName(i.name, keep); + return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; } + default: + return ts.Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); } - function isVariableDeclarationInImport(decl: ts.VariableDeclaration) { - return ts.isSourceFile(decl.parent.parent.parent) && - !!decl.initializer && ts.isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); - } - - function filterImport(i: SupportedImport, moduleSpecifier: ts.StringLiteralLike, keep: (name: ts.Identifier) => boolean): SupportedImportStatement | undefined { - switch (i.kind) { - case ts.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 - ? ts.factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createImportClause(/*isTypeOnly*/ false, defaultImport, namedBindings), moduleSpecifier, /*assertClause*/ undefined) - : undefined; - } - case ts.SyntaxKind.ImportEqualsDeclaration: - return keep(i.name) ? i : undefined; - case ts.SyntaxKind.VariableDeclaration: { - const name = filterBindingName(i.name, keep); - return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; - } - default: - return ts.Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); +} +function filterNamedBindings(namedBindings: ts.NamedImportBindings, keep: (name: ts.Identifier) => boolean): ts.NamedImportBindings | undefined { + if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) { + return keep(namedBindings.name) ? namedBindings : undefined; + } + else { + const newElements = namedBindings.elements.filter(e => keep(e.name)); + return newElements.length ? ts.factory.createNamedImports(newElements) : undefined; + } +} +function filterBindingName(name: ts.BindingName, keep: (name: ts.Identifier) => boolean): ts.BindingName | undefined { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return keep(name) ? name : undefined; + case ts.SyntaxKind.ArrayBindingPattern: + return name; + case ts.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 || !ts.isIdentifier(prop.name) || keep(prop.name)); + return newElements.length ? ts.factory.createObjectBindingPattern(newElements) : undefined; } } - function filterNamedBindings(namedBindings: ts.NamedImportBindings, keep: (name: ts.Identifier) => boolean): ts.NamedImportBindings | undefined { - if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) { - return keep(namedBindings.name) ? namedBindings : undefined; +} + +function forEachReference(node: ts.Node, checker: ts.TypeChecker, onReference: (s: ts.Symbol) => void) { + node.forEachChild(function cb(node) { + if (ts.isIdentifier(node) && !ts.isDeclarationName(node)) { + const sym = checker.getSymbolAtLocation(node); + if (sym) + onReference(sym); } else { - const newElements = namedBindings.elements.filter(e => keep(e.name)); - return newElements.length ? ts.factory.createNamedImports(newElements) : undefined; + node.forEachChild(cb); } + }); +} + +interface ReadonlySymbolSet { + has(symbol: ts.Symbol): boolean; + forEach(cb: (symbol: ts.Symbol) => void): void; + forEachEntry(cb: (symbol: ts.Symbol) => T | undefined): T | undefined; +} +class SymbolSet implements ReadonlySymbolSet { + private map = new ts.Map(); + add(symbol: ts.Symbol): void { + this.map.set(String(ts.getSymbolId(symbol)), symbol); } - function filterBindingName(name: ts.BindingName, keep: (name: ts.Identifier) => boolean): ts.BindingName | undefined { - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return keep(name) ? name : undefined; - case ts.SyntaxKind.ArrayBindingPattern: - return name; - case ts.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 || !ts.isIdentifier(prop.name) || keep(prop.name)); - return newElements.length ? ts.factory.createObjectBindingPattern(newElements) : undefined; - } - } + has(symbol: ts.Symbol): boolean { + return this.map.has(String(ts.getSymbolId(symbol))); } - - function forEachReference(node: ts.Node, checker: ts.TypeChecker, onReference: (s: ts.Symbol) => void) { - node.forEachChild(function cb(node) { - if (ts.isIdentifier(node) && !ts.isDeclarationName(node)) { - const sym = checker.getSymbolAtLocation(node); - if (sym) - onReference(sym); - } - else { - node.forEachChild(cb); - } - }); + delete(symbol: ts.Symbol): void { + this.map.delete(String(ts.getSymbolId(symbol))); } - - interface ReadonlySymbolSet { - has(symbol: ts.Symbol): boolean; - forEach(cb: (symbol: ts.Symbol) => void): void; - forEachEntry(cb: (symbol: ts.Symbol) => T | undefined): T | undefined; + forEach(cb: (symbol: ts.Symbol) => void): void { + this.map.forEach(cb); } - class SymbolSet implements ReadonlySymbolSet { - private map = new ts.Map(); - add(symbol: ts.Symbol): void { - this.map.set(String(ts.getSymbolId(symbol)), symbol); - } - has(symbol: ts.Symbol): boolean { - return this.map.has(String(ts.getSymbolId(symbol))); - } - delete(symbol: ts.Symbol): void { - this.map.delete(String(ts.getSymbolId(symbol))); - } - forEach(cb: (symbol: ts.Symbol) => void): void { - this.map.forEach(cb); - } - forEachEntry(cb: (symbol: ts.Symbol) => T | undefined): T | undefined { - return ts.forEachEntry(this.map, cb); - } - clone(): SymbolSet { - const clone = new SymbolSet(); - ts.copyEntries(this.map, clone.map); - return clone; - } + forEachEntry(cb: (symbol: ts.Symbol) => T | undefined): T | undefined { + return ts.forEachEntry(this.map, cb); + } + clone(): SymbolSet { + const clone = new SymbolSet(); + ts.copyEntries(this.map, clone.map); + return clone; } +} - type TopLevelExpressionStatement = ts.ExpressionStatement & { - expression: ts.BinaryExpression & { - left: ts.PropertyAccessExpression; - }; - }; // 'exports.x = ...' - type NonVariableTopLevelDeclaration = ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.ModuleDeclaration | TopLevelExpressionStatement | ts.ImportEqualsDeclaration; - type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | ts.VariableStatement; - interface TopLevelVariableDeclaration extends ts.VariableDeclaration { - parent: ts.VariableDeclarationList & { - parent: ts.VariableStatement; - }; - } - type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | ts.BindingElement; - function isTopLevelDeclaration(node: ts.Node): node is TopLevelDeclaration { - return isNonVariableTopLevelDeclaration(node) && ts.isSourceFile(node.parent) || ts.isVariableDeclaration(node) && ts.isSourceFile(node.parent.parent.parent); - } - function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): ts.Node { - return ts.isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; - } - function isTopLevelDeclarationStatement(node: ts.Node): node is TopLevelDeclarationStatement { - ts.Debug.assert(ts.isSourceFile(node.parent), "Node parent should be a SourceFile"); - return isNonVariableTopLevelDeclaration(node) || ts.isVariableStatement(node); - } - function isNonVariableTopLevelDeclaration(node: ts.Node): node is NonVariableTopLevelDeclaration { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - return true; - default: - return false; - } +type TopLevelExpressionStatement = ts.ExpressionStatement & { + expression: ts.BinaryExpression & { + left: ts.PropertyAccessExpression; + }; +}; // 'exports.x = ...' +type NonVariableTopLevelDeclaration = ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.ModuleDeclaration | TopLevelExpressionStatement | ts.ImportEqualsDeclaration; +type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | ts.VariableStatement; +interface TopLevelVariableDeclaration extends ts.VariableDeclaration { + parent: ts.VariableDeclarationList & { + parent: ts.VariableStatement; + }; +} +type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | ts.BindingElement; +function isTopLevelDeclaration(node: ts.Node): node is TopLevelDeclaration { + return isNonVariableTopLevelDeclaration(node) && ts.isSourceFile(node.parent) || ts.isVariableDeclaration(node) && ts.isSourceFile(node.parent.parent.parent); +} +function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): ts.Node { + return ts.isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; +} +function isTopLevelDeclarationStatement(node: ts.Node): node is TopLevelDeclarationStatement { + ts.Debug.assert(ts.isSourceFile(node.parent), "Node parent should be a SourceFile"); + return isNonVariableTopLevelDeclaration(node) || ts.isVariableStatement(node); +} +function isNonVariableTopLevelDeclaration(node: ts.Node): node is NonVariableTopLevelDeclaration { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + default: + return false; } +} - function forEachTopLevelDeclaration(statement: ts.Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (statement.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - return cb(statement as ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.ModuleDeclaration | ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.ImportEqualsDeclaration); - case ts.SyntaxKind.VariableStatement: - return ts.firstDefined((statement as ts.VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); - case ts.SyntaxKind.ExpressionStatement: { - const { expression } = statement as ts.ExpressionStatement; - return ts.isBinaryExpression(expression) && ts.getAssignmentDeclarationKind(expression) === ts.AssignmentDeclarationKind.ExportsProperty - ? cb(statement as TopLevelExpressionStatement) - : undefined; - } +function forEachTopLevelDeclaration(statement: ts.Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (statement.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + return cb(statement as ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.ModuleDeclaration | ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.ImportEqualsDeclaration); + case ts.SyntaxKind.VariableStatement: + return ts.firstDefined((statement as ts.VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); + case ts.SyntaxKind.ExpressionStatement: { + const { expression } = statement as ts.ExpressionStatement; + return ts.isBinaryExpression(expression) && ts.getAssignmentDeclarationKind(expression) === ts.AssignmentDeclarationKind.ExportsProperty + ? cb(statement as TopLevelExpressionStatement) + : undefined; } } - function forEachTopLevelDeclarationInBindingName(name: ts.BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return cb(ts.cast(name.parent, (x): x is TopLevelVariableDeclaration | ts.BindingElement => ts.isVariableDeclaration(x) || ts.isBindingElement(x))); - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ObjectBindingPattern: - return ts.firstDefined(name.elements, em => ts.isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); - default: - return ts.Debug.assertNever(name, `Unexpected name kind ${(name as ts.BindingName).kind}`); - } +} +function forEachTopLevelDeclarationInBindingName(name: ts.BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return cb(ts.cast(name.parent, (x): x is TopLevelVariableDeclaration | ts.BindingElement => ts.isVariableDeclaration(x) || ts.isBindingElement(x))); + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ObjectBindingPattern: + return ts.firstDefined(name.elements, em => ts.isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); + default: + return ts.Debug.assertNever(name, `Unexpected name kind ${(name as ts.BindingName).kind}`); } +} - function nameOfTopLevelDeclaration(d: TopLevelDeclaration): ts.Identifier | undefined { - return ts.isExpressionStatement(d) ? ts.tryCast(d.expression.left.name, ts.isIdentifier) : ts.tryCast(d.name, ts.isIdentifier); +function nameOfTopLevelDeclaration(d: TopLevelDeclaration): ts.Identifier | undefined { + return ts.isExpressionStatement(d) ? ts.tryCast(d.expression.left.name, ts.isIdentifier) : ts.tryCast(d.name, ts.isIdentifier); +} + +function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { + switch (d.kind) { + case ts.SyntaxKind.VariableDeclaration: + return d.parent.parent; + case ts.SyntaxKind.BindingElement: + return getTopLevelDeclarationStatement(ts.cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | ts.BindingElement => ts.isVariableDeclaration(p) || ts.isBindingElement(p))); + default: + return d; } +} - function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { - switch (d.kind) { - case ts.SyntaxKind.VariableDeclaration: - return d.parent.parent; - case ts.SyntaxKind.BindingElement: - return getTopLevelDeclarationStatement(ts.cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | ts.BindingElement => ts.isVariableDeclaration(p) || ts.isBindingElement(p))); - default: - return d; - } +function addExportToChanges(sourceFile: ts.SourceFile, decl: TopLevelDeclarationStatement, name: ts.Identifier, changes: ts.textChanges.ChangeTracker, useEs6Exports: boolean): void { + if (isExported(sourceFile, decl, useEs6Exports, name)) + return; + if (useEs6Exports) { + if (!ts.isExpressionStatement(decl)) + changes.insertExportModifier(sourceFile, decl); } + else { + const names = getNamesToExportInCommonJS(decl); + if (names.length !== 0) + changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); + } +} - function addExportToChanges(sourceFile: ts.SourceFile, decl: TopLevelDeclarationStatement, name: ts.Identifier, changes: ts.textChanges.ChangeTracker, useEs6Exports: boolean): void { - if (isExported(sourceFile, decl, useEs6Exports, name)) - return; - if (useEs6Exports) { - if (!ts.isExpressionStatement(decl)) - changes.insertExportModifier(sourceFile, decl); - } - else { - const names = getNamesToExportInCommonJS(decl); - if (names.length !== 0) - changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); - } +function isExported(sourceFile: ts.SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: ts.Identifier): boolean { + if (useEs6Exports) { + return !ts.isExpressionStatement(decl) && ts.hasSyntacticModifier(decl, ts.ModifierFlags.Export) || !!(name && sourceFile.symbol.exports?.has(name.escapedText)); } + return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(ts.escapeLeadingUnderscores(name))); +} - function isExported(sourceFile: ts.SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: ts.Identifier): boolean { - if (useEs6Exports) { - return !ts.isExpressionStatement(decl) && ts.hasSyntacticModifier(decl, ts.ModifierFlags.Export) || !!(name && sourceFile.symbol.exports?.has(name.escapedText)); - } - return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(ts.escapeLeadingUnderscores(name))); - } - - function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly ts.Statement[] | undefined { - return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); - } - function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { - const modifiers = ts.concatenate([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], d.modifiers); - switch (d.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return ts.factory.updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); - case ts.SyntaxKind.ClassDeclaration: - return ts.factory.updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); - case ts.SyntaxKind.VariableStatement: - return ts.factory.updateVariableStatement(d, modifiers, d.declarationList); - case ts.SyntaxKind.ModuleDeclaration: - return ts.factory.updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body); - case ts.SyntaxKind.EnumDeclaration: - return ts.factory.updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members); - case ts.SyntaxKind.TypeAliasDeclaration: - return ts.factory.updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type); - case ts.SyntaxKind.InterfaceDeclaration: - return ts.factory.updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); - case ts.SyntaxKind.ImportEqualsDeclaration: - return ts.factory.updateImportEqualsDeclaration(d, d.decorators, modifiers, d.isTypeOnly, d.name, d.moduleReference); - case ts.SyntaxKind.ExpressionStatement: - return ts.Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return ts.Debug.assertNever(d, `Unexpected declaration kind ${(d as ts.DeclarationStatement).kind}`); - } +function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly ts.Statement[] | undefined { + return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); +} +function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { + const modifiers = ts.concatenate([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], d.modifiers); + switch (d.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return ts.factory.updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); + case ts.SyntaxKind.ClassDeclaration: + return ts.factory.updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case ts.SyntaxKind.VariableStatement: + return ts.factory.updateVariableStatement(d, modifiers, d.declarationList); + case ts.SyntaxKind.ModuleDeclaration: + return ts.factory.updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body); + case ts.SyntaxKind.EnumDeclaration: + return ts.factory.updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members); + case ts.SyntaxKind.TypeAliasDeclaration: + return ts.factory.updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type); + case ts.SyntaxKind.InterfaceDeclaration: + return ts.factory.updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case ts.SyntaxKind.ImportEqualsDeclaration: + return ts.factory.updateImportEqualsDeclaration(d, d.decorators, modifiers, d.isTypeOnly, d.name, d.moduleReference); + case ts.SyntaxKind.ExpressionStatement: + return ts.Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + return ts.Debug.assertNever(d, `Unexpected declaration kind ${(d as ts.DeclarationStatement).kind}`); } - function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly ts.Statement[] | undefined { - return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; - } - function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { - switch (decl.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - return [decl.name!.text]; // TODO: GH#18217 - case ts.SyntaxKind.VariableStatement: - return ts.mapDefined(decl.declarationList.declarations, d => ts.isIdentifier(d.name) ? d.name.text : undefined); - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - return ts.emptyArray; - case ts.SyntaxKind.ExpressionStatement: - return ts.Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return ts.Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); - } +} +function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly ts.Statement[] | undefined { + return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; +} +function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { + switch (decl.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassDeclaration: + return [decl.name!.text]; // TODO: GH#18217 + case ts.SyntaxKind.VariableStatement: + return ts.mapDefined(decl.declarationList.declarations, d => ts.isIdentifier(d.name) ? d.name.text : undefined); + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + return ts.emptyArray; + case ts.SyntaxKind.ExpressionStatement: + return ts.Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + return ts.Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); } +} - /** Creates `exports.x = x;` */ - function createExportAssignment(name: string): ts.Statement { - return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("exports"), ts.factory.createIdentifier(name)), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier(name))); - } +/** Creates `exports.x = x;` */ +function createExportAssignment(name: string): ts.Statement { + return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("exports"), ts.factory.createIdentifier(name)), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier(name))); +} } diff --git a/src/services/rename.ts b/src/services/rename.ts index 9b2583eebcc3e..649afe786751f 100644 --- a/src/services/rename.ts +++ b/src/services/rename.ts @@ -1,128 +1,128 @@ /* @internal */ namespace ts.Rename { - export function getRenameInfo(program: ts.Program, sourceFile: ts.SourceFile, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { - const node = ts.getAdjustedRenameLocation(ts.getTouchingPropertyName(sourceFile, position)); - if (nodeIsEligibleForRename(node)) { - const renameInfo = getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, program, options); - if (renameInfo) { - return renameInfo; - } +export function getRenameInfo(program: ts.Program, sourceFile: ts.SourceFile, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + const node = ts.getAdjustedRenameLocation(ts.getTouchingPropertyName(sourceFile, position)); + if (nodeIsEligibleForRename(node)) { + const renameInfo = getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, program, options); + if (renameInfo) { + return renameInfo; } - return getRenameInfoError(ts.Diagnostics.You_cannot_rename_this_element); } + return getRenameInfoError(ts.Diagnostics.You_cannot_rename_this_element); +} - function getRenameInfoForNode(node: ts.Node, typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, program: ts.Program, options?: ts.RenameInfoOptions): ts.RenameInfo | undefined { - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol) { - if (ts.isStringLiteralLike(node)) { - const type = ts.getContextualTypeFromParentOrAncestorTypeNode(node, typeChecker); - if (type && ((type.flags & ts.TypeFlags.StringLiteral) || ((type.flags & ts.TypeFlags.Union) && ts.every((type as ts.UnionType).types, type => !!(type.flags & ts.TypeFlags.StringLiteral))))) { - return getRenameInfoSuccess(node.text, node.text, ts.ScriptElementKind.string, "", node, sourceFile); - } - } - else if (ts.isLabelName(node)) { - const name = ts.getTextOfNode(node); - return getRenameInfoSuccess(name, name, ts.ScriptElementKind.label, ts.ScriptElementKindModifier.none, node, sourceFile); +function getRenameInfoForNode(node: ts.Node, typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, program: ts.Program, options?: ts.RenameInfoOptions): ts.RenameInfo | undefined { + const symbol = typeChecker.getSymbolAtLocation(node); + if (!symbol) { + if (ts.isStringLiteralLike(node)) { + const type = ts.getContextualTypeFromParentOrAncestorTypeNode(node, typeChecker); + if (type && ((type.flags & ts.TypeFlags.StringLiteral) || ((type.flags & ts.TypeFlags.Union) && ts.every((type as ts.UnionType).types, type => !!(type.flags & ts.TypeFlags.StringLiteral))))) { + return getRenameInfoSuccess(node.text, node.text, ts.ScriptElementKind.string, "", node, sourceFile); } - return undefined; } - // 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(declaration => isDefinedInLibraryFile(program, declaration))) { - return getRenameInfoError(ts.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 (ts.isIdentifier(node) && node.originalKeywordKind === ts.SyntaxKind.DefaultKeyword && symbol.parent && symbol.parent.flags & ts.SymbolFlags.Module) { - return undefined; + else if (ts.isLabelName(node)) { + const name = ts.getTextOfNode(node); + return getRenameInfoSuccess(name, name, ts.ScriptElementKind.label, ts.ScriptElementKindModifier.none, node, sourceFile); } + return undefined; + } + // Only allow a symbol to be renamed if it actually has at least one declaration. + const { declarations } = symbol; + if (!declarations || declarations.length === 0) + return; - if (ts.isStringLiteralLike(node) && ts.tryGetImportFromModuleSpecifier(node)) { - return options && options.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; - } + // Disallow rename for elements that are defined in the standard TypeScript library. + if (declarations.some(declaration => isDefinedInLibraryFile(program, declaration))) { + return getRenameInfoError(ts.Diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library); + } - const kind = ts.SymbolDisplay.getSymbolKind(typeChecker, symbol, node); - const specifierName = (ts.isImportOrExportSpecifierName(node) || ts.isStringOrNumericLiteralLike(node) && node.parent.kind === ts.SyntaxKind.ComputedPropertyName) - ? ts.stripQuotes(ts.getTextOfIdentifierOrLiteral(node)) - : undefined; - const displayName = specifierName || typeChecker.symbolToString(symbol); - const fullDisplayName = specifierName || typeChecker.getFullyQualifiedName(symbol); - return getRenameInfoSuccess(displayName, fullDisplayName, kind, ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), node, sourceFile); + // Cannot rename `default` as in `import { default as foo } from "./someModule"; + if (ts.isIdentifier(node) && node.originalKeywordKind === ts.SyntaxKind.DefaultKeyword && symbol.parent && symbol.parent.flags & ts.SymbolFlags.Module) { + return undefined; } - function isDefinedInLibraryFile(program: ts.Program, declaration: ts.Node) { - const sourceFile = declaration.getSourceFile(); - return program.isSourceFileDefaultLibrary(sourceFile) && ts.fileExtensionIs(sourceFile.fileName, ts.Extension.Dts); + if (ts.isStringLiteralLike(node) && ts.tryGetImportFromModuleSpecifier(node)) { + return options && options.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; } - function getRenameInfoForModule(node: ts.StringLiteralLike, sourceFile: ts.SourceFile, moduleSymbol: ts.Symbol): ts.RenameInfo | undefined { - if (!ts.isExternalModuleNameRelative(node.text)) { - return getRenameInfoError(ts.Diagnostics.You_cannot_rename_a_module_via_a_global_import); - } + const kind = ts.SymbolDisplay.getSymbolKind(typeChecker, symbol, node); + const specifierName = (ts.isImportOrExportSpecifierName(node) || ts.isStringOrNumericLiteralLike(node) && node.parent.kind === ts.SyntaxKind.ComputedPropertyName) + ? ts.stripQuotes(ts.getTextOfIdentifierOrLiteral(node)) + : undefined; + const displayName = specifierName || typeChecker.symbolToString(symbol); + const fullDisplayName = specifierName || typeChecker.getFullyQualifiedName(symbol); + return getRenameInfoSuccess(displayName, fullDisplayName, kind, ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), node, sourceFile); +} - const moduleSourceFile = moduleSymbol.declarations && ts.find(moduleSymbol.declarations, ts.isSourceFile); - if (!moduleSourceFile) - return undefined; - const withoutIndex = ts.endsWith(node.text, "/index") || ts.endsWith(node.text, "/index.js") ? undefined : ts.tryRemoveSuffix(ts.removeFileExtension(moduleSourceFile.fileName), "/index"); - const name = withoutIndex === undefined ? moduleSourceFile.fileName : withoutIndex; - const kind = withoutIndex === undefined ? ts.ScriptElementKind.moduleElement : ts.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 = ts.createTextSpan(node.getStart(sourceFile) + 1 + indexAfterLastSlash, node.text.length - indexAfterLastSlash); - return { - canRename: true, - fileToRename: name, - kind, - displayName: name, - fullDisplayName: name, - kindModifiers: ts.ScriptElementKindModifier.none, - triggerSpan, - }; - } +function isDefinedInLibraryFile(program: ts.Program, declaration: ts.Node) { + const sourceFile = declaration.getSourceFile(); + return program.isSourceFileDefaultLibrary(sourceFile) && ts.fileExtensionIs(sourceFile.fileName, ts.Extension.Dts); +} - function getRenameInfoSuccess(displayName: string, fullDisplayName: string, kind: ts.ScriptElementKind, kindModifiers: string, node: ts.Node, sourceFile: ts.SourceFile): ts.RenameInfoSuccess { - return { - canRename: true, - fileToRename: undefined, - kind, - displayName, - fullDisplayName, - kindModifiers, - triggerSpan: createTriggerSpanForNode(node, sourceFile) - }; +function getRenameInfoForModule(node: ts.StringLiteralLike, sourceFile: ts.SourceFile, moduleSymbol: ts.Symbol): ts.RenameInfo | undefined { + if (!ts.isExternalModuleNameRelative(node.text)) { + return getRenameInfoError(ts.Diagnostics.You_cannot_rename_a_module_via_a_global_import); } - function getRenameInfoError(diagnostic: ts.DiagnosticMessage): ts.RenameInfoFailure { - return { canRename: false, localizedErrorMessage: ts.getLocaleSpecificMessage(diagnostic) }; - } + const moduleSourceFile = moduleSymbol.declarations && ts.find(moduleSymbol.declarations, ts.isSourceFile); + if (!moduleSourceFile) + return undefined; + const withoutIndex = ts.endsWith(node.text, "/index") || ts.endsWith(node.text, "/index.js") ? undefined : ts.tryRemoveSuffix(ts.removeFileExtension(moduleSourceFile.fileName), "/index"); + const name = withoutIndex === undefined ? moduleSourceFile.fileName : withoutIndex; + const kind = withoutIndex === undefined ? ts.ScriptElementKind.moduleElement : ts.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 = ts.createTextSpan(node.getStart(sourceFile) + 1 + indexAfterLastSlash, node.text.length - indexAfterLastSlash); + return { + canRename: true, + fileToRename: name, + kind, + displayName: name, + fullDisplayName: name, + kindModifiers: ts.ScriptElementKindModifier.none, + triggerSpan, + }; +} - function createTriggerSpanForNode(node: ts.Node, sourceFile: ts.SourceFile) { - let start = node.getStart(sourceFile); - let width = node.getWidth(sourceFile); - if (ts.isStringLiteralLike(node)) { - // Exclude the quotes - start += 1; - width -= 2; - } - return ts.createTextSpan(start, width); +function getRenameInfoSuccess(displayName: string, fullDisplayName: string, kind: ts.ScriptElementKind, kindModifiers: string, node: ts.Node, sourceFile: ts.SourceFile): ts.RenameInfoSuccess { + return { + canRename: true, + fileToRename: undefined, + kind, + displayName, + fullDisplayName, + kindModifiers, + triggerSpan: createTriggerSpanForNode(node, sourceFile) + }; +} + +function getRenameInfoError(diagnostic: ts.DiagnosticMessage): ts.RenameInfoFailure { + return { canRename: false, localizedErrorMessage: ts.getLocaleSpecificMessage(diagnostic) }; +} + +function createTriggerSpanForNode(node: ts.Node, sourceFile: ts.SourceFile) { + let start = node.getStart(sourceFile); + let width = node.getWidth(sourceFile); + if (ts.isStringLiteralLike(node)) { + // Exclude the quotes + start += 1; + width -= 2; } + return ts.createTextSpan(start, width); +} - export function nodeIsEligibleForRename(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PrivateIdentifier: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.ThisKeyword: - return true; - case ts.SyntaxKind.NumericLiteral: - return ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(node as ts.NumericLiteral); - default: - return false; - } +export function nodeIsEligibleForRename(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PrivateIdentifier: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.ThisKeyword: + return true; + case ts.SyntaxKind.NumericLiteral: + return ts.isLiteralNameOfPropertyDeclarationOrIndexAccess(node as ts.NumericLiteral); + default: + return false; } } +} diff --git a/src/services/services.ts b/src/services/services.ts index d6750aaf9bb24..eb117d45544d6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,2825 +1,2825 @@ namespace ts { - /** The version of the language service API */ - export const servicesVersion = "0.8"; - - function createNode(kind: TKind, pos: number, end: number, parent: ts.Node): NodeObject | TokenObject | IdentifierObject | PrivateIdentifierObject { - const node = ts.isNodeKind(kind) ? new NodeObject(kind, pos, end) : - kind === ts.SyntaxKind.Identifier ? new IdentifierObject(ts.SyntaxKind.Identifier, pos, end) : - kind === ts.SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(ts.SyntaxKind.PrivateIdentifier, pos, end) : - new TokenObject(kind, pos, end); - node.parent = parent; - node.flags = parent.flags & ts.NodeFlags.ContextFlags; - return node; - } - - class NodeObject implements ts.Node { - public kind: ts.SyntaxKind; - public pos: number; - public end: number; - public flags: ts.NodeFlags; - public modifierFlagsCache: ts.ModifierFlags; - public transformFlags: ts.TransformFlags; - public parent: ts.Node; - public symbol!: ts.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?: ts.JSDoc[]; - public original?: ts.Node; - private _children: ts.Node[] | undefined; - constructor(kind: ts.SyntaxKind, pos: number, end: number) { - this.pos = pos; - this.end = end; - this.flags = ts.NodeFlags.None; - this.modifierFlagsCache = ts.ModifierFlags.None; - this.transformFlags = ts.TransformFlags.None; - this.parent = undefined!; - this.kind = kind; - } +/** The version of the language service API */ +export const servicesVersion = "0.8"; + +function createNode(kind: TKind, pos: number, end: number, parent: ts.Node): NodeObject | TokenObject | IdentifierObject | PrivateIdentifierObject { + const node = ts.isNodeKind(kind) ? new NodeObject(kind, pos, end) : + kind === ts.SyntaxKind.Identifier ? new IdentifierObject(ts.SyntaxKind.Identifier, pos, end) : + kind === ts.SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(ts.SyntaxKind.PrivateIdentifier, pos, end) : + new TokenObject(kind, pos, end); + node.parent = parent; + node.flags = parent.flags & ts.NodeFlags.ContextFlags; + return node; +} - private assertHasRealPosition(message?: string) { - // eslint-disable-next-line debug-assert - ts.Debug.assert(!ts.positionIsSynthesized(this.pos) && !ts.positionIsSynthesized(this.end), message || "Node must have a real position for this operation"); - } +class NodeObject implements ts.Node { + public kind: ts.SyntaxKind; + public pos: number; + public end: number; + public flags: ts.NodeFlags; + public modifierFlagsCache: ts.ModifierFlags; + public transformFlags: ts.TransformFlags; + public parent: ts.Node; + public symbol!: ts.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?: ts.JSDoc[]; + public original?: ts.Node; + private _children: ts.Node[] | undefined; + constructor(kind: ts.SyntaxKind, pos: number, end: number) { + this.pos = pos; + this.end = end; + this.flags = ts.NodeFlags.None; + this.modifierFlagsCache = ts.ModifierFlags.None; + this.transformFlags = ts.TransformFlags.None; + this.parent = undefined!; + this.kind = kind; + } - public getSourceFile(): ts.SourceFile { - return ts.getSourceFileOfNode(this); - } + private assertHasRealPosition(message?: string) { + // eslint-disable-next-line debug-assert + ts.Debug.assert(!ts.positionIsSynthesized(this.pos) && !ts.positionIsSynthesized(this.end), message || "Node must have a real position for this operation"); + } - public getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number { - this.assertHasRealPosition(); - return ts.getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } + public getSourceFile(): ts.SourceFile { + return ts.getSourceFileOfNode(this); + } - public getFullStart(): number { - this.assertHasRealPosition(); - return this.pos; - } + public getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number { + this.assertHasRealPosition(); + return ts.getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } - public getEnd(): number { - this.assertHasRealPosition(); - return this.end; - } + public getFullStart(): number { + this.assertHasRealPosition(); + return this.pos; + } - public getWidth(sourceFile?: ts.SourceFile): number { - this.assertHasRealPosition(); - return this.getEnd() - this.getStart(sourceFile); - } + public getEnd(): number { + this.assertHasRealPosition(); + return this.end; + } - public getFullWidth(): number { - this.assertHasRealPosition(); - return this.end - this.pos; - } + public getWidth(sourceFile?: ts.SourceFile): number { + this.assertHasRealPosition(); + return this.getEnd() - this.getStart(sourceFile); + } - public getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { - this.assertHasRealPosition(); - return this.getStart(sourceFile) - this.pos; - } + public getFullWidth(): number { + this.assertHasRealPosition(); + return this.end - this.pos; + } - public getFullText(sourceFile?: ts.SourceFile): string { - this.assertHasRealPosition(); - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } + public getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { + this.assertHasRealPosition(); + return this.getStart(sourceFile) - this.pos; + } - public getText(sourceFile?: ts.SourceFile): string { - this.assertHasRealPosition(); - if (!sourceFile) { - sourceFile = this.getSourceFile(); - } - return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); - } + public getFullText(sourceFile?: ts.SourceFile): string { + this.assertHasRealPosition(); + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } - public getChildCount(sourceFile?: ts.SourceFile): number { - return this.getChildren(sourceFile).length; + public getText(sourceFile?: ts.SourceFile): string { + this.assertHasRealPosition(); + if (!sourceFile) { + sourceFile = this.getSourceFile(); } + return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); + } - public getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { - return this.getChildren(sourceFile)[index]; - } + public getChildCount(sourceFile?: ts.SourceFile): number { + return this.getChildren(sourceFile).length; + } - public getChildren(sourceFile?: ts.SourceFileLike): ts.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 getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { + return this.getChildren(sourceFile)[index]; + } - public getFirstToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined { - this.assertHasRealPosition(); - const children = this.getChildren(sourceFile); - if (!children.length) { - return undefined; - } + public getChildren(sourceFile?: ts.SourceFileLike): ts.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)); + } - const child = ts.find(children, kid => kid.kind < ts.SyntaxKind.FirstJSDocNode || kid.kind > ts.SyntaxKind.LastJSDocNode)!; - return child.kind < ts.SyntaxKind.FirstNode ? - child : - child.getFirstToken(sourceFile); + public getFirstToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined { + this.assertHasRealPosition(); + const children = this.getChildren(sourceFile); + if (!children.length) { + return undefined; } - public getLastToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined { - this.assertHasRealPosition(); - const children = this.getChildren(sourceFile); + const child = ts.find(children, kid => kid.kind < ts.SyntaxKind.FirstJSDocNode || kid.kind > ts.SyntaxKind.LastJSDocNode)!; + return child.kind < ts.SyntaxKind.FirstNode ? + child : + child.getFirstToken(sourceFile); + } - const child = ts.lastOrUndefined(children); - if (!child) { - return undefined; - } + public getLastToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined { + this.assertHasRealPosition(); + const children = this.getChildren(sourceFile); - return child.kind < ts.SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); + const child = ts.lastOrUndefined(children); + if (!child) { + return undefined; } - public forEachChild(cbNode: (node: ts.Node) => T, cbNodeArray?: (nodes: ts.NodeArray) => T): T | undefined { - return ts.forEachChild(this, cbNode, cbNodeArray); - } + return child.kind < ts.SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); } - function createChildren(node: ts.Node, sourceFile: ts.SourceFileLike | undefined): ts.Node[] { - if (!ts.isNodeKind(node.kind)) { - return ts.emptyArray; - } + public forEachChild(cbNode: (node: ts.Node) => T, cbNodeArray?: (nodes: ts.NodeArray) => T): T | undefined { + return ts.forEachChild(this, cbNode, cbNodeArray); + } +} - const children: ts.Node[] = []; - if (ts.isJSDocCommentContainingNode(node)) { - /** Don't add trivia for "tokens" since this is in a comment. */ - node.forEachChild(child => { - children.push(child); - }); - return children; - } +function createChildren(node: ts.Node, sourceFile: ts.SourceFileLike | undefined): ts.Node[] { + if (!ts.isNodeKind(node.kind)) { + return ts.emptyArray; + } - ts.scanner.setText((sourceFile || node.getSourceFile()).text); - let pos = node.pos; - const processNode = (child: ts.Node) => { - addSyntheticNodes(children, pos, child.pos, node); + const children: ts.Node[] = []; + if (ts.isJSDocCommentContainingNode(node)) { + /** Don't add trivia for "tokens" since this is in a comment. */ + node.forEachChild(child => { children.push(child); - pos = child.end; - }; - const processNodes = (nodes: ts.NodeArray) => { - addSyntheticNodes(children, pos, nodes.pos, node); - children.push(createSyntaxList(nodes, node)); - pos = nodes.end; - }; - // jsDocComments need to be the first children - ts.forEach((node as ts.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); - ts.scanner.setText(undefined); + }); return children; } - function addSyntheticNodes(nodes: ts.Push, pos: number, end: number, parent: ts.Node): void { - ts.scanner.setTextPos(pos); - while (pos < end) { - const token = ts.scanner.scan(); - const textPos = ts.scanner.getTextPos(); - if (textPos <= end) { - if (token === ts.SyntaxKind.Identifier) { - ts.Debug.fail(`Did not expect ${ts.Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`); - } - nodes.push(createNode(token, pos, textPos, parent)); - } - pos = textPos; - if (token === ts.SyntaxKind.EndOfFileToken) { - break; + ts.scanner.setText((sourceFile || node.getSourceFile()).text); + let pos = node.pos; + const processNode = (child: ts.Node) => { + addSyntheticNodes(children, pos, child.pos, node); + children.push(child); + pos = child.end; + }; + const processNodes = (nodes: ts.NodeArray) => { + addSyntheticNodes(children, pos, nodes.pos, node); + children.push(createSyntaxList(nodes, node)); + pos = nodes.end; + }; + // jsDocComments need to be the first children + ts.forEach((node as ts.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); + ts.scanner.setText(undefined); + return children; +} + +function addSyntheticNodes(nodes: ts.Push, pos: number, end: number, parent: ts.Node): void { + ts.scanner.setTextPos(pos); + while (pos < end) { + const token = ts.scanner.scan(); + const textPos = ts.scanner.getTextPos(); + if (textPos <= end) { + if (token === ts.SyntaxKind.Identifier) { + ts.Debug.fail(`Did not expect ${ts.Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`); } + nodes.push(createNode(token, pos, textPos, parent)); } - } - - function createSyntaxList(nodes: ts.NodeArray, parent: ts.Node): ts.Node { - const list = createNode(ts.SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as ts.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; + pos = textPos; + if (token === ts.SyntaxKind.EndOfFileToken) { + break; } - addSyntheticNodes(list._children, pos, nodes.end, parent); - return list; } +} - class TokenOrIdentifierObject implements ts.Node { - public kind!: ts.SyntaxKind; - public pos: number; - public end: number; - public flags: ts.NodeFlags; - public modifierFlagsCache: ts.ModifierFlags; - public transformFlags: ts.TransformFlags; - public parent: ts.Node; - public symbol!: ts.Symbol; - public jsDocComments?: ts.JSDoc[]; +function createSyntaxList(nodes: ts.NodeArray, parent: ts.Node): ts.Node { + const list = createNode(ts.SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as ts.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; +} - constructor(pos: number, end: number) { - // Set properties in same order as NodeObject - this.pos = pos; - this.end = end; - this.flags = ts.NodeFlags.None; - this.modifierFlagsCache = ts.ModifierFlags.None; - this.transformFlags = ts.TransformFlags.None; - this.parent = undefined!; - } +class TokenOrIdentifierObject implements ts.Node { + public kind!: ts.SyntaxKind; + public pos: number; + public end: number; + public flags: ts.NodeFlags; + public modifierFlagsCache: ts.ModifierFlags; + public transformFlags: ts.TransformFlags; + public parent: ts.Node; + public symbol!: ts.Symbol; + public jsDocComments?: ts.JSDoc[]; + + constructor(pos: number, end: number) { + // Set properties in same order as NodeObject + this.pos = pos; + this.end = end; + this.flags = ts.NodeFlags.None; + this.modifierFlagsCache = ts.ModifierFlags.None; + this.transformFlags = ts.TransformFlags.None; + this.parent = undefined!; + } - public getSourceFile(): ts.SourceFile { - return ts.getSourceFileOfNode(this); - } + public getSourceFile(): ts.SourceFile { + return ts.getSourceFileOfNode(this); + } - public getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number { - return ts.getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } + public getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number { + return ts.getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } - public getFullStart(): number { - return this.pos; - } + public getFullStart(): number { + return this.pos; + } - public getEnd(): number { - return this.end; - } + public getEnd(): number { + return this.end; + } - public getWidth(sourceFile?: ts.SourceFile): number { - return this.getEnd() - this.getStart(sourceFile); - } + public getWidth(sourceFile?: ts.SourceFile): number { + return this.getEnd() - this.getStart(sourceFile); + } - public getFullWidth(): number { - return this.end - this.pos; - } + public getFullWidth(): number { + return this.end - this.pos; + } - public getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { - return this.getStart(sourceFile) - this.pos; - } + public getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { + return this.getStart(sourceFile) - this.pos; + } - public getFullText(sourceFile?: ts.SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } + public getFullText(sourceFile?: ts.SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } - public getText(sourceFile?: ts.SourceFile): string { - if (!sourceFile) { - sourceFile = this.getSourceFile(); - } - return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); + public getText(sourceFile?: ts.SourceFile): string { + if (!sourceFile) { + sourceFile = this.getSourceFile(); } + return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); + } - public getChildCount(): number { - return this.getChildren().length; - } + public getChildCount(): number { + return this.getChildren().length; + } - public getChildAt(index: number): ts.Node { - return this.getChildren()[index]; - } + public getChildAt(index: number): ts.Node { + return this.getChildren()[index]; + } - public getChildren(): ts.Node[] { - return this.kind === ts.SyntaxKind.EndOfFileToken ? (this as ts.EndOfFileToken).jsDoc || ts.emptyArray : ts.emptyArray; - } + public getChildren(): ts.Node[] { + return this.kind === ts.SyntaxKind.EndOfFileToken ? (this as ts.EndOfFileToken).jsDoc || ts.emptyArray : ts.emptyArray; + } - public getFirstToken(): ts.Node | undefined { - return undefined; - } + public getFirstToken(): ts.Node | undefined { + return undefined; + } - public getLastToken(): ts.Node | undefined { - return undefined; - } + public getLastToken(): ts.Node | undefined { + return undefined; + } - public forEachChild(): T | undefined { - return undefined; - } + public forEachChild(): T | undefined { + return undefined; } +} - class SymbolObject implements ts.Symbol { - flags: ts.SymbolFlags; - escapedName: ts.__String; - declarations!: ts.Declaration[]; - valueDeclaration!: ts.Declaration; +class SymbolObject implements ts.Symbol { + flags: ts.SymbolFlags; + escapedName: ts.__String; + declarations!: ts.Declaration[]; + valueDeclaration!: ts.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?: ts.SymbolDisplayPart[]; + tags?: ts.JSDocTagInfo[]; // same + contextualGetAccessorDocumentationComment?: ts.SymbolDisplayPart[]; + contextualSetAccessorDocumentationComment?: ts.SymbolDisplayPart[]; + contextualGetAccessorTags?: ts.JSDocTagInfo[]; + contextualSetAccessorTags?: ts.JSDocTagInfo[]; + constructor(flags: ts.SymbolFlags, name: ts.__String) { + this.flags = flags; + this.escapedName = name; + } - // 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?: ts.SymbolDisplayPart[]; - tags?: ts.JSDocTagInfo[]; // same - contextualGetAccessorDocumentationComment?: ts.SymbolDisplayPart[]; - contextualSetAccessorDocumentationComment?: ts.SymbolDisplayPart[]; - contextualGetAccessorTags?: ts.JSDocTagInfo[]; - contextualSetAccessorTags?: ts.JSDocTagInfo[]; - constructor(flags: ts.SymbolFlags, name: ts.__String) { - this.flags = flags; - this.escapedName = name; - } + getFlags(): ts.SymbolFlags { + return this.flags; + } - getFlags(): ts.SymbolFlags { - return this.flags; - } + get name(): string { + return ts.symbolName(this); + } - get name(): string { - return ts.symbolName(this); - } + getEscapedName(): ts.__String { + return this.escapedName; + } - getEscapedName(): ts.__String { - return this.escapedName; - } + getName(): string { + return this.name; + } - getName(): string { - return this.name; - } + getDeclarations(): ts.Declaration[] | undefined { + return this.declarations; + } - getDeclarations(): ts.Declaration[] | undefined { - return this.declarations; + getDocumentationComment(checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { + if (!this.documentationComment) { + this.documentationComment = ts.emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs + if (!this.declarations && (this as ts.Symbol as ts.TransientSymbol).target && ((this as ts.Symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration) { + const labelDecl = ((this as ts.Symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration!; + this.documentationComment = getDocumentationComment([labelDecl], checker); + } + else { + this.documentationComment = getDocumentationComment(this.declarations, checker); + } } + return this.documentationComment; + } - getDocumentationComment(checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { - if (!this.documentationComment) { - this.documentationComment = ts.emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs - if (!this.declarations && (this as ts.Symbol as ts.TransientSymbol).target && ((this as ts.Symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration) { - const labelDecl = ((this as ts.Symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration!; - this.documentationComment = getDocumentationComment([labelDecl], checker); + getContextualDocumentationComment(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { + switch (context?.kind) { + case ts.SyntaxKind.GetAccessor: + if (!this.contextualGetAccessorDocumentationComment) { + this.contextualGetAccessorDocumentationComment = getDocumentationComment(ts.filter(this.declarations, ts.isGetAccessor), checker); } - else { - this.documentationComment = getDocumentationComment(this.declarations, checker); + return this.contextualGetAccessorDocumentationComment; + case ts.SyntaxKind.SetAccessor: + if (!this.contextualSetAccessorDocumentationComment) { + this.contextualSetAccessorDocumentationComment = getDocumentationComment(ts.filter(this.declarations, ts.isSetAccessor), checker); } - } - return this.documentationComment; + return this.contextualSetAccessorDocumentationComment; + default: + return this.getDocumentationComment(checker); } + } - getContextualDocumentationComment(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { - switch (context?.kind) { - case ts.SyntaxKind.GetAccessor: - if (!this.contextualGetAccessorDocumentationComment) { - this.contextualGetAccessorDocumentationComment = getDocumentationComment(ts.filter(this.declarations, ts.isGetAccessor), checker); - } - return this.contextualGetAccessorDocumentationComment; - case ts.SyntaxKind.SetAccessor: - if (!this.contextualSetAccessorDocumentationComment) { - this.contextualSetAccessorDocumentationComment = getDocumentationComment(ts.filter(this.declarations, ts.isSetAccessor), checker); - } - return this.contextualSetAccessorDocumentationComment; - default: - return this.getDocumentationComment(checker); - } + getJsDocTags(checker?: ts.TypeChecker): ts.JSDocTagInfo[] { + if (this.tags === undefined) { + this.tags = getJsDocTagsOfDeclarations(this.declarations, checker); } - getJsDocTags(checker?: ts.TypeChecker): ts.JSDocTagInfo[] { - if (this.tags === undefined) { - this.tags = getJsDocTagsOfDeclarations(this.declarations, checker); - } - - return this.tags; - } + return this.tags; + } - getContextualJsDocTags(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): ts.JSDocTagInfo[] { - switch (context?.kind) { - case ts.SyntaxKind.GetAccessor: - if (!this.contextualGetAccessorTags) { - this.contextualGetAccessorTags = getJsDocTagsOfDeclarations(ts.filter(this.declarations, ts.isGetAccessor), checker); - } - return this.contextualGetAccessorTags; - case ts.SyntaxKind.SetAccessor: - if (!this.contextualSetAccessorTags) { - this.contextualSetAccessorTags = getJsDocTagsOfDeclarations(ts.filter(this.declarations, ts.isSetAccessor), checker); - } - return this.contextualSetAccessorTags; - default: - return this.getJsDocTags(checker); - } + getContextualJsDocTags(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): ts.JSDocTagInfo[] { + switch (context?.kind) { + case ts.SyntaxKind.GetAccessor: + if (!this.contextualGetAccessorTags) { + this.contextualGetAccessorTags = getJsDocTagsOfDeclarations(ts.filter(this.declarations, ts.isGetAccessor), checker); + } + return this.contextualGetAccessorTags; + case ts.SyntaxKind.SetAccessor: + if (!this.contextualSetAccessorTags) { + this.contextualSetAccessorTags = getJsDocTagsOfDeclarations(ts.filter(this.declarations, ts.isSetAccessor), checker); + } + return this.contextualSetAccessorTags; + default: + return this.getJsDocTags(checker); } } +} - class TokenObject extends TokenOrIdentifierObject implements ts.Token { - public kind: TKind; +class TokenObject extends TokenOrIdentifierObject implements ts.Token { + public kind: TKind; - constructor(kind: TKind, pos: number, end: number) { - super(pos, end); - this.kind = kind; - } + constructor(kind: TKind, pos: number, end: number) { + super(pos, end); + this.kind = kind; } +} - class IdentifierObject extends TokenOrIdentifierObject implements ts.Identifier { - public kind: ts.SyntaxKind.Identifier = ts.SyntaxKind.Identifier; - public escapedText!: ts.__String; - public autoGenerateFlags!: ts.GeneratedIdentifierFlags; - _primaryExpressionBrand: any; - _memberExpressionBrand: any; - _leftHandSideExpressionBrand: any; - _updateExpressionBrand: any; - _unaryExpressionBrand: any; - _expressionBrand: any; - _declarationBrand: any; - /*@internal*/ typeArguments!: ts.NodeArray; - constructor(_kind: ts.SyntaxKind.Identifier, pos: number, end: number) { - super(pos, end); - } +class IdentifierObject extends TokenOrIdentifierObject implements ts.Identifier { + public kind: ts.SyntaxKind.Identifier = ts.SyntaxKind.Identifier; + public escapedText!: ts.__String; + public autoGenerateFlags!: ts.GeneratedIdentifierFlags; + _primaryExpressionBrand: any; + _memberExpressionBrand: any; + _leftHandSideExpressionBrand: any; + _updateExpressionBrand: any; + _unaryExpressionBrand: any; + _expressionBrand: any; + _declarationBrand: any; + /*@internal*/ typeArguments!: ts.NodeArray; + constructor(_kind: ts.SyntaxKind.Identifier, pos: number, end: number) { + super(pos, end); + } - get text(): string { - return ts.idText(this); - } + get text(): string { + return ts.idText(this); + } +} +IdentifierObject.prototype.kind = ts.SyntaxKind.Identifier; +class PrivateIdentifierObject extends TokenOrIdentifierObject implements ts.PrivateIdentifier { + public kind!: ts.SyntaxKind.PrivateIdentifier; + public escapedText!: ts.__String; + public symbol!: ts.Symbol; + _primaryExpressionBrand: any; + _memberExpressionBrand: any; + _leftHandSideExpressionBrand: any; + _updateExpressionBrand: any; + _unaryExpressionBrand: any; + _expressionBrand: any; + constructor(_kind: ts.SyntaxKind.PrivateIdentifier, pos: number, end: number) { + super(pos, end); } - IdentifierObject.prototype.kind = ts.SyntaxKind.Identifier; - class PrivateIdentifierObject extends TokenOrIdentifierObject implements ts.PrivateIdentifier { - public kind!: ts.SyntaxKind.PrivateIdentifier; - public escapedText!: ts.__String; - public symbol!: ts.Symbol; - _primaryExpressionBrand: any; - _memberExpressionBrand: any; - _leftHandSideExpressionBrand: any; - _updateExpressionBrand: any; - _unaryExpressionBrand: any; - _expressionBrand: any; - constructor(_kind: ts.SyntaxKind.PrivateIdentifier, pos: number, end: number) { - super(pos, end); - } - get text(): string { - return ts.idText(this); - } + get text(): string { + return ts.idText(this); + } +} +PrivateIdentifierObject.prototype.kind = ts.SyntaxKind.PrivateIdentifier; +class TypeObject implements ts.Type { + checker: ts.TypeChecker; + flags: ts.TypeFlags; + objectFlags?: ts.ObjectFlags; + id!: number; + symbol!: ts.Symbol; + constructor(checker: ts.TypeChecker, flags: ts.TypeFlags) { + this.checker = checker; + this.flags = flags; + } + getFlags(): ts.TypeFlags { + return this.flags; + } + getSymbol(): ts.Symbol | undefined { + return this.symbol; + } + getProperties(): ts.Symbol[] { + return this.checker.getPropertiesOfType(this); + } + getProperty(propertyName: string): ts.Symbol | undefined { + return this.checker.getPropertyOfType(this, propertyName); + } + getApparentProperties(): ts.Symbol[] { + return this.checker.getAugmentedPropertiesOfType(this); + } + getCallSignatures(): readonly ts.Signature[] { + return this.checker.getSignaturesOfType(this, ts.SignatureKind.Call); + } + getConstructSignatures(): readonly ts.Signature[] { + return this.checker.getSignaturesOfType(this, ts.SignatureKind.Construct); + } + getStringIndexType(): ts.Type | undefined { + return this.checker.getIndexTypeOfType(this, ts.IndexKind.String); + } + getNumberIndexType(): ts.Type | undefined { + return this.checker.getIndexTypeOfType(this, ts.IndexKind.Number); + } + getBaseTypes(): ts.BaseType[] | undefined { + return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined; + } + isNullableType(): boolean { + return this.checker.isNullableType(this); + } + getNonNullableType(): ts.Type { + return this.checker.getNonNullableType(this); + } + getNonOptionalType(): ts.Type { + return this.checker.getNonOptionalType(this); + } + getConstraint(): ts.Type | undefined { + return this.checker.getBaseConstraintOfType(this); + } + getDefault(): ts.Type | undefined { + return this.checker.getDefaultFromTypeParameter(this); } - PrivateIdentifierObject.prototype.kind = ts.SyntaxKind.PrivateIdentifier; - class TypeObject implements ts.Type { - checker: ts.TypeChecker; - flags: ts.TypeFlags; - objectFlags?: ts.ObjectFlags; - id!: number; - symbol!: ts.Symbol; - constructor(checker: ts.TypeChecker, flags: ts.TypeFlags) { - this.checker = checker; - this.flags = flags; - } - getFlags(): ts.TypeFlags { - return this.flags; - } - getSymbol(): ts.Symbol | undefined { - return this.symbol; - } - getProperties(): ts.Symbol[] { - return this.checker.getPropertiesOfType(this); - } - getProperty(propertyName: string): ts.Symbol | undefined { - return this.checker.getPropertyOfType(this, propertyName); - } - getApparentProperties(): ts.Symbol[] { - return this.checker.getAugmentedPropertiesOfType(this); - } - getCallSignatures(): readonly ts.Signature[] { - return this.checker.getSignaturesOfType(this, ts.SignatureKind.Call); - } - getConstructSignatures(): readonly ts.Signature[] { - return this.checker.getSignaturesOfType(this, ts.SignatureKind.Construct); - } - getStringIndexType(): ts.Type | undefined { - return this.checker.getIndexTypeOfType(this, ts.IndexKind.String); - } - getNumberIndexType(): ts.Type | undefined { - return this.checker.getIndexTypeOfType(this, ts.IndexKind.Number); - } - getBaseTypes(): ts.BaseType[] | undefined { - return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined; - } - isNullableType(): boolean { - return this.checker.isNullableType(this); - } - getNonNullableType(): ts.Type { - return this.checker.getNonNullableType(this); - } - getNonOptionalType(): ts.Type { - return this.checker.getNonOptionalType(this); - } - getConstraint(): ts.Type | undefined { - return this.checker.getBaseConstraintOfType(this); - } - getDefault(): ts.Type | undefined { - return this.checker.getDefaultFromTypeParameter(this); - } - isUnion(): this is ts.UnionType { - return !!(this.flags & ts.TypeFlags.Union); - } - isIntersection(): this is ts.IntersectionType { - return !!(this.flags & ts.TypeFlags.Intersection); - } - isUnionOrIntersection(): this is ts.UnionOrIntersectionType { - return !!(this.flags & ts.TypeFlags.UnionOrIntersection); - } - isLiteral(): this is ts.LiteralType { - return !!(this.flags & ts.TypeFlags.StringOrNumberLiteral); - } - isStringLiteral(): this is ts.StringLiteralType { - return !!(this.flags & ts.TypeFlags.StringLiteral); - } - isNumberLiteral(): this is ts.NumberLiteralType { - return !!(this.flags & ts.TypeFlags.NumberLiteral); - } - isTypeParameter(): this is ts.TypeParameter { - return !!(this.flags & ts.TypeFlags.TypeParameter); - } - isClassOrInterface(): this is ts.InterfaceType { - return !!(ts.getObjectFlags(this) & ts.ObjectFlags.ClassOrInterface); - } - isClass(): this is ts.InterfaceType { - return !!(ts.getObjectFlags(this) & ts.ObjectFlags.Class); - } - isIndexType(): this is ts.IndexType { - return !!(this.flags & ts.TypeFlags.Index); - } - /** - * This polyfills `referenceType.typeArguments` for API consumers - */ - get typeArguments() { - if (ts.getObjectFlags(this) & ts.ObjectFlags.Reference) { - return this.checker.getTypeArguments(this as ts.Type as ts.TypeReference); - } - return undefined; + isUnion(): this is ts.UnionType { + return !!(this.flags & ts.TypeFlags.Union); + } + isIntersection(): this is ts.IntersectionType { + return !!(this.flags & ts.TypeFlags.Intersection); + } + isUnionOrIntersection(): this is ts.UnionOrIntersectionType { + return !!(this.flags & ts.TypeFlags.UnionOrIntersection); + } + isLiteral(): this is ts.LiteralType { + return !!(this.flags & ts.TypeFlags.StringOrNumberLiteral); + } + isStringLiteral(): this is ts.StringLiteralType { + return !!(this.flags & ts.TypeFlags.StringLiteral); + } + isNumberLiteral(): this is ts.NumberLiteralType { + return !!(this.flags & ts.TypeFlags.NumberLiteral); + } + isTypeParameter(): this is ts.TypeParameter { + return !!(this.flags & ts.TypeFlags.TypeParameter); + } + isClassOrInterface(): this is ts.InterfaceType { + return !!(ts.getObjectFlags(this) & ts.ObjectFlags.ClassOrInterface); + } + isClass(): this is ts.InterfaceType { + return !!(ts.getObjectFlags(this) & ts.ObjectFlags.Class); + } + isIndexType(): this is ts.IndexType { + return !!(this.flags & ts.TypeFlags.Index); + } + /** + * This polyfills `referenceType.typeArguments` for API consumers + */ + get typeArguments() { + if (ts.getObjectFlags(this) & ts.ObjectFlags.Reference) { + return this.checker.getTypeArguments(this as ts.Type as ts.TypeReference); } + return undefined; } +} - class SignatureObject implements ts.Signature { - flags: ts.SignatureFlags; - checker: ts.TypeChecker; - declaration!: ts.SignatureDeclaration; - typeParameters?: ts.TypeParameter[]; - parameters!: ts.Symbol[]; - thisParameter!: ts.Symbol; - resolvedReturnType!: ts.Type; - resolvedTypePredicate: ts.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?: ts.SymbolDisplayPart[]; - jsDocTags?: ts.JSDocTagInfo[]; // same - constructor(checker: ts.TypeChecker, flags: ts.SignatureFlags) { - this.checker = checker; - this.flags = flags; - } +class SignatureObject implements ts.Signature { + flags: ts.SignatureFlags; + checker: ts.TypeChecker; + declaration!: ts.SignatureDeclaration; + typeParameters?: ts.TypeParameter[]; + parameters!: ts.Symbol[]; + thisParameter!: ts.Symbol; + resolvedReturnType!: ts.Type; + resolvedTypePredicate: ts.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?: ts.SymbolDisplayPart[]; + jsDocTags?: ts.JSDocTagInfo[]; // same + constructor(checker: ts.TypeChecker, flags: ts.SignatureFlags) { + this.checker = checker; + this.flags = flags; + } - getDeclaration(): ts.SignatureDeclaration { - return this.declaration; - } - getTypeParameters(): ts.TypeParameter[] | undefined { - return this.typeParameters; - } - getParameters(): ts.Symbol[] { - return this.parameters; - } - getReturnType(): ts.Type { - return this.checker.getReturnTypeOfSignature(this); - } - getTypeParameterAtPosition(pos: number): ts.Type { - const type = this.checker.getParameterType(this, pos); - if (type.isIndexType() && ts.isThisTypeParameter(type.type)) { - const constraint = type.type.getConstraint(); - if (constraint) { - return this.checker.getIndexType(constraint); - } + getDeclaration(): ts.SignatureDeclaration { + return this.declaration; + } + getTypeParameters(): ts.TypeParameter[] | undefined { + return this.typeParameters; + } + getParameters(): ts.Symbol[] { + return this.parameters; + } + getReturnType(): ts.Type { + return this.checker.getReturnTypeOfSignature(this); + } + getTypeParameterAtPosition(pos: number): ts.Type { + const type = this.checker.getParameterType(this, pos); + if (type.isIndexType() && ts.isThisTypeParameter(type.type)) { + const constraint = type.type.getConstraint(); + if (constraint) { + return this.checker.getIndexType(constraint); } - return type; } + return type; + } - getDocumentationComment(): ts.SymbolDisplayPart[] { - return this.documentationComment || (this.documentationComment = getDocumentationComment(ts.singleElementArray(this.declaration), this.checker)); - } + getDocumentationComment(): ts.SymbolDisplayPart[] { + return this.documentationComment || (this.documentationComment = getDocumentationComment(ts.singleElementArray(this.declaration), this.checker)); + } - getJsDocTags(): ts.JSDocTagInfo[] { - return this.jsDocTags || (this.jsDocTags = getJsDocTagsOfDeclarations(ts.singleElementArray(this.declaration), this.checker)); - } + getJsDocTags(): ts.JSDocTagInfo[] { + return this.jsDocTags || (this.jsDocTags = getJsDocTagsOfDeclarations(ts.singleElementArray(this.declaration), this.checker)); } +} - /** - * 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: ts.Node) { - return ts.getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc" || tag.tagName.text === "inheritdoc"); - } - function getJsDocTagsOfDeclarations(declarations: ts.Declaration[] | undefined, checker: ts.TypeChecker | undefined): ts.JSDocTagInfo[] { - if (!declarations) - return ts.emptyArray; - let tags = ts.JsDoc.getJsDocTagsFromDeclarations(declarations, checker); - if (checker && (tags.length === 0 || declarations.some(hasJSDocInheritDocTag))) { - const seenSymbols = new ts.Set(); - for (const declaration of declarations) { - const inheritedTags = findBaseOfDeclaration(checker, declaration, symbol => { - if (!seenSymbols.has(symbol)) { - seenSymbols.add(symbol); - if (declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) { - return symbol.getContextualJsDocTags(declaration, checker); - } - return symbol.declarations?.length === 1 ? symbol.getJsDocTags() : undefined; +/** + * 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: ts.Node) { + return ts.getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc" || tag.tagName.text === "inheritdoc"); +} +function getJsDocTagsOfDeclarations(declarations: ts.Declaration[] | undefined, checker: ts.TypeChecker | undefined): ts.JSDocTagInfo[] { + if (!declarations) + return ts.emptyArray; + let tags = ts.JsDoc.getJsDocTagsFromDeclarations(declarations, checker); + if (checker && (tags.length === 0 || declarations.some(hasJSDocInheritDocTag))) { + const seenSymbols = new ts.Set(); + for (const declaration of declarations) { + const inheritedTags = findBaseOfDeclaration(checker, declaration, symbol => { + if (!seenSymbols.has(symbol)) { + seenSymbols.add(symbol); + if (declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) { + return symbol.getContextualJsDocTags(declaration, checker); } - }); - if (inheritedTags) { - tags = [...inheritedTags, ...tags]; + return symbol.declarations?.length === 1 ? symbol.getJsDocTags() : undefined; } + }); + if (inheritedTags) { + tags = [...inheritedTags, ...tags]; } } - return tags; - } - - function getDocumentationComment(declarations: readonly ts.Declaration[] | undefined, checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { - if (!declarations) - return ts.emptyArray; - let doc = ts.JsDoc.getJsDocCommentsFromDeclarations(declarations, checker); - if (checker && (doc.length === 0 || declarations.some(hasJSDocInheritDocTag))) { - const seenSymbols = new ts.Set(); - for (const declaration of declarations) { - const inheritedDocs = findBaseOfDeclaration(checker, declaration, symbol => { - if (!seenSymbols.has(symbol)) { - seenSymbols.add(symbol); - if (declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) { - return symbol.getContextualDocumentationComment(declaration, checker); - } - return symbol.getDocumentationComment(checker); + } + return tags; +} + +function getDocumentationComment(declarations: readonly ts.Declaration[] | undefined, checker: ts.TypeChecker | undefined): ts.SymbolDisplayPart[] { + if (!declarations) + return ts.emptyArray; + let doc = ts.JsDoc.getJsDocCommentsFromDeclarations(declarations, checker); + if (checker && (doc.length === 0 || declarations.some(hasJSDocInheritDocTag))) { + const seenSymbols = new ts.Set(); + for (const declaration of declarations) { + const inheritedDocs = findBaseOfDeclaration(checker, declaration, symbol => { + if (!seenSymbols.has(symbol)) { + seenSymbols.add(symbol); + if (declaration.kind === ts.SyntaxKind.GetAccessor || declaration.kind === ts.SyntaxKind.SetAccessor) { + return symbol.getContextualDocumentationComment(declaration, checker); } - }); - // TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs - if (inheritedDocs) - doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(ts.lineBreakPart(), doc); - } + return symbol.getDocumentationComment(checker); + } + }); + // TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs + if (inheritedDocs) + doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(ts.lineBreakPart(), doc); } - return doc; } + return doc; +} - function findBaseOfDeclaration(checker: ts.TypeChecker, declaration: ts.Declaration, cb: (symbol: ts.Symbol) => T[] | undefined): T[] | undefined { - const classOrInterfaceDeclaration = declaration.parent?.kind === ts.SyntaxKind.Constructor ? declaration.parent.parent : declaration.parent; - if (!classOrInterfaceDeclaration) - return; - const isStaticMember = ts.hasStaticModifier(declaration); - return ts.firstDefined(ts.getAllSuperTypeNodes(classOrInterfaceDeclaration), superTypeNode => { - const baseType = checker.getTypeAtLocation(superTypeNode); - const symbol = isStaticMember - ? ts.find(checker.getExportsOfModule(baseType.symbol), s => s.escapedName === declaration.symbol.name) - : checker.getPropertyOfType(baseType, declaration.symbol.name); - return symbol ? cb(symbol) : undefined; - }); +function findBaseOfDeclaration(checker: ts.TypeChecker, declaration: ts.Declaration, cb: (symbol: ts.Symbol) => T[] | undefined): T[] | undefined { + const classOrInterfaceDeclaration = declaration.parent?.kind === ts.SyntaxKind.Constructor ? declaration.parent.parent : declaration.parent; + if (!classOrInterfaceDeclaration) + return; + const isStaticMember = ts.hasStaticModifier(declaration); + return ts.firstDefined(ts.getAllSuperTypeNodes(classOrInterfaceDeclaration), superTypeNode => { + const baseType = checker.getTypeAtLocation(superTypeNode); + const symbol = isStaticMember + ? ts.find(checker.getExportsOfModule(baseType.symbol), s => s.escapedName === declaration.symbol.name) + : checker.getPropertyOfType(baseType, declaration.symbol.name); + return symbol ? cb(symbol) : undefined; + }); +} + +class SourceFileObject extends NodeObject implements ts.SourceFile { + public kind: ts.SyntaxKind.SourceFile = ts.SyntaxKind.SourceFile; + public _declarationBrand: any; + public fileName!: string; + public path!: ts.Path; + public resolvedPath!: ts.Path; + public originalFileName!: string; + public text!: string; + public scriptSnapshot!: ts.IScriptSnapshot; + public lineMap!: readonly number[]; + + public statements!: ts.NodeArray; + public endOfFileToken!: ts.Token; + public amdDependencies!: { + name: string; + path: string; + }[]; + public moduleName!: string; + public referencedFiles!: ts.FileReference[]; + public typeReferenceDirectives!: ts.FileReference[]; + public libReferenceDirectives!: ts.FileReference[]; + public syntacticDiagnostics!: ts.DiagnosticWithLocation[]; + public parseDiagnostics!: ts.DiagnosticWithLocation[]; + public bindDiagnostics!: ts.DiagnosticWithLocation[]; + public bindSuggestionDiagnostics?: ts.DiagnosticWithLocation[]; + + public isDeclarationFile!: boolean; + public isDefaultLib!: boolean; + public hasNoDefaultLib!: boolean; + public externalModuleIndicator!: ts.Node; // The first node that causes this file to be an external module + public commonJsModuleIndicator!: ts.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!: ts.ScriptKind; + public languageVersion!: ts.ScriptTarget; + public languageVariant!: ts.LanguageVariant; + public identifiers!: ts.ESMap; + public nameTable: ts.UnderscoreEscapedMap | undefined; + public resolvedModules: ts.ModeAwareCache | undefined; + public resolvedTypeReferenceDirectiveNames!: ts.ModeAwareCache; + public imports!: readonly ts.StringLiteralLike[]; + public moduleAugmentations!: ts.StringLiteral[]; + private namedDeclarations: ts.ESMap | undefined; + public ambientModuleNames!: string[]; + public checkJsDirective: ts.CheckJsDirective | undefined; + public errorExpectations: ts.TextRange[] | undefined; + public possiblyContainDynamicImport?: boolean; + public pragmas!: ts.PragmaMap; + public localJsxFactory: ts.EntityName | undefined; + public localJsxNamespace: ts.__String | undefined; + constructor(kind: ts.SyntaxKind, pos: number, end: number) { + super(kind, pos, end); } - class SourceFileObject extends NodeObject implements ts.SourceFile { - public kind: ts.SyntaxKind.SourceFile = ts.SyntaxKind.SourceFile; - public _declarationBrand: any; - public fileName!: string; - public path!: ts.Path; - public resolvedPath!: ts.Path; - public originalFileName!: string; - public text!: string; - public scriptSnapshot!: ts.IScriptSnapshot; - public lineMap!: readonly number[]; - - public statements!: ts.NodeArray; - public endOfFileToken!: ts.Token; - public amdDependencies!: { - name: string; - path: string; - }[]; - public moduleName!: string; - public referencedFiles!: ts.FileReference[]; - public typeReferenceDirectives!: ts.FileReference[]; - public libReferenceDirectives!: ts.FileReference[]; - public syntacticDiagnostics!: ts.DiagnosticWithLocation[]; - public parseDiagnostics!: ts.DiagnosticWithLocation[]; - public bindDiagnostics!: ts.DiagnosticWithLocation[]; - public bindSuggestionDiagnostics?: ts.DiagnosticWithLocation[]; - - public isDeclarationFile!: boolean; - public isDefaultLib!: boolean; - public hasNoDefaultLib!: boolean; - public externalModuleIndicator!: ts.Node; // The first node that causes this file to be an external module - public commonJsModuleIndicator!: ts.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!: ts.ScriptKind; - public languageVersion!: ts.ScriptTarget; - public languageVariant!: ts.LanguageVariant; - public identifiers!: ts.ESMap; - public nameTable: ts.UnderscoreEscapedMap | undefined; - public resolvedModules: ts.ModeAwareCache | undefined; - public resolvedTypeReferenceDirectiveNames!: ts.ModeAwareCache; - public imports!: readonly ts.StringLiteralLike[]; - public moduleAugmentations!: ts.StringLiteral[]; - private namedDeclarations: ts.ESMap | undefined; - public ambientModuleNames!: string[]; - public checkJsDirective: ts.CheckJsDirective | undefined; - public errorExpectations: ts.TextRange[] | undefined; - public possiblyContainDynamicImport?: boolean; - public pragmas!: ts.PragmaMap; - public localJsxFactory: ts.EntityName | undefined; - public localJsxNamespace: ts.__String | undefined; - constructor(kind: ts.SyntaxKind, pos: number, end: number) { - super(kind, pos, end); - } - - public update(newText: string, textChangeRange: ts.TextChangeRange): ts.SourceFile { - return ts.updateSourceFile(this, newText, textChangeRange); - } - - public getLineAndCharacterOfPosition(position: number): ts.LineAndCharacter { - return ts.getLineAndCharacterOfPosition(this, position); - } - - public getLineStarts(): readonly number[] { - return ts.getLineStarts(this); - } - - public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number { - return ts.computePositionOfLineAndCharacter(ts.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; - } + public update(newText: string, textChangeRange: ts.TextChangeRange): ts.SourceFile { + return ts.updateSourceFile(this, newText, textChangeRange); + } - 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 getLineAndCharacterOfPosition(position: number): ts.LineAndCharacter { + return ts.getLineAndCharacterOfPosition(this, position); + } + + public getLineStarts(): readonly number[] { + return ts.getLineStarts(this); + } + + public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number { + return ts.computePositionOfLineAndCharacter(ts.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; } - public getNamedDeclarations(): ts.ESMap { - if (!this.namedDeclarations) { - this.namedDeclarations = this.computeNamedDeclarations(); - } + 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; + } - return this.namedDeclarations; + public getNamedDeclarations(): ts.ESMap { + if (!this.namedDeclarations) { + this.namedDeclarations = this.computeNamedDeclarations(); } - private computeNamedDeclarations(): ts.ESMap { - const result = ts.createMultiMap(); + return this.namedDeclarations; + } + + private computeNamedDeclarations(): ts.ESMap { + const result = ts.createMultiMap(); - this.forEachChild(visit); + this.forEachChild(visit); - return result; + return result; - function addDeclaration(declaration: ts.Declaration) { - const name = getDeclarationName(declaration); - if (name) { - result.add(name, declaration); - } + function addDeclaration(declaration: ts.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; + function getDeclarations(name: string) { + let declarations = result.get(name); + if (!declarations) { + result.set(name, declarations = []); } + return declarations; + } - function getDeclarationName(declaration: ts.Declaration) { - const name = ts.getNonAssignedNameOfDeclaration(declaration); - return name && (ts.isComputedPropertyName(name) && ts.isPropertyAccessExpression(name.expression) ? name.expression.name.text - : ts.isPropertyName(name) ? ts.getNameFromPropertyName(name) : undefined); - } + function getDeclarationName(declaration: ts.Declaration) { + const name = ts.getNonAssignedNameOfDeclaration(declaration); + return name && (ts.isComputedPropertyName(name) && ts.isPropertyAccessExpression(name.expression) ? name.expression.name.text + : ts.isPropertyName(name) ? ts.getNameFromPropertyName(name) : undefined); + } - function visit(node: ts.Node): void { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - const functionDeclaration = node as ts.FunctionLikeDeclaration; - const declarationName = getDeclarationName(functionDeclaration); - - if (declarationName) { - const declarations = getDeclarations(declarationName); - const lastDeclaration = ts.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 as ts.FunctionLikeDeclaration).body) { - declarations[declarations.length - 1] = functionDeclaration; - } - } - else { - declarations.push(functionDeclaration); + function visit(node: ts.Node): void { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + const functionDeclaration = node as ts.FunctionLikeDeclaration; + const declarationName = getDeclarationName(functionDeclaration); + + if (declarationName) { + const declarations = getDeclarations(declarationName); + const lastDeclaration = ts.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 as ts.FunctionLikeDeclaration).body) { + declarations[declarations.length - 1] = functionDeclaration; } } - ts.forEachChild(node, visit); + else { + declarations.push(functionDeclaration); + } + } + ts.forEachChild(node, visit); + break; + + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.TypeLiteral: + addDeclaration(node as ts.Declaration); + ts.forEachChild(node, visit); + break; + + case ts.SyntaxKind.Parameter: + // Only consider parameter properties + if (!ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier)) { break; + } + // falls through - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.TypeLiteral: - addDeclaration(node as ts.Declaration); - ts.forEachChild(node, visit); + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.BindingElement: { + const decl = node as ts.VariableDeclaration; + if (ts.isBindingPattern(decl.name)) { + ts.forEachChild(decl.name, visit); break; + } + if (decl.initializer) { + visit(decl.initializer); + } + } + // falls through + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + addDeclaration(node as ts.Declaration); + break; - case ts.SyntaxKind.Parameter: - // Only consider parameter properties - if (!ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier)) { - break; + case ts.SyntaxKind.ExportDeclaration: + // Handle named exports case e.g.: + // export {a, b as B} from "mod"; + const exportDeclaration = node as ts.ExportDeclaration; + if (exportDeclaration.exportClause) { + if (ts.isNamedExports(exportDeclaration.exportClause)) { + ts.forEach(exportDeclaration.exportClause.elements, visit); } - // falls through - - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.BindingElement: { - const decl = node as ts.VariableDeclaration; - if (ts.isBindingPattern(decl.name)) { - ts.forEachChild(decl.name, visit); - break; - } - if (decl.initializer) { - visit(decl.initializer); + else { + visit(exportDeclaration.exportClause.name); } } - // falls through - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - addDeclaration(node as ts.Declaration); - break; + break; - case ts.SyntaxKind.ExportDeclaration: - // Handle named exports case e.g.: - // export {a, b as B} from "mod"; - const exportDeclaration = node as ts.ExportDeclaration; - if (exportDeclaration.exportClause) { - if (ts.isNamedExports(exportDeclaration.exportClause)) { - ts.forEach(exportDeclaration.exportClause.elements, visit); - } - else { - visit(exportDeclaration.exportClause.name); - } + case ts.SyntaxKind.ImportDeclaration: + const importClause = (node as ts.ImportDeclaration).importClause; + if (importClause) { + // Handle default import case e.g.: + // import d from "mod"; + if (importClause.name) { + addDeclaration(importClause.name); } - break; - case ts.SyntaxKind.ImportDeclaration: - const importClause = (node as ts.ImportDeclaration).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 === ts.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 === ts.SyntaxKind.NamespaceImport) { - addDeclaration(importClause.namedBindings); - } - else { - ts.forEach(importClause.namedBindings.elements, visit); - } + else { + ts.forEach(importClause.namedBindings.elements, visit); } } - break; + } + break; - case ts.SyntaxKind.BinaryExpression: - if (ts.getAssignmentDeclarationKind(node as ts.BinaryExpression) !== ts.AssignmentDeclarationKind.None) { - addDeclaration(node as ts.BinaryExpression); - } - // falls through + case ts.SyntaxKind.BinaryExpression: + if (ts.getAssignmentDeclarationKind(node as ts.BinaryExpression) !== ts.AssignmentDeclarationKind.None) { + addDeclaration(node as ts.BinaryExpression); + } + // falls through - default: - ts.forEachChild(node, visit); - } + default: + ts.forEachChild(node, visit); } } } +} - class SourceMapSourceObject implements ts.SourceMapSource { - lineMap!: number[]; - constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { } +class SourceMapSourceObject implements ts.SourceMapSource { + lineMap!: number[]; + constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { } - public getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter { - return ts.getLineAndCharacterOfPosition(this, pos); - } + public getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter { + return ts.getLineAndCharacterOfPosition(this, pos); } +} - function getServicesObjectAllocator(): ts.ObjectAllocator { - return { - getNodeConstructor: () => NodeObject, - getTokenConstructor: () => TokenObject, - - getIdentifierConstructor: () => IdentifierObject, - getPrivateIdentifierConstructor: () => PrivateIdentifierObject, - getSourceFileConstructor: () => SourceFileObject, - getSymbolConstructor: () => SymbolObject, - getTypeConstructor: () => TypeObject, - getSignatureConstructor: () => SignatureObject, - getSourceMapSourceConstructor: () => SourceMapSourceObject, - }; - } +function getServicesObjectAllocator(): ts.ObjectAllocator { + return { + getNodeConstructor: () => NodeObject, + getTokenConstructor: () => TokenObject, + + getIdentifierConstructor: () => IdentifierObject, + getPrivateIdentifierConstructor: () => PrivateIdentifierObject, + getSourceFileConstructor: () => SourceFileObject, + getSymbolConstructor: () => SymbolObject, + getTypeConstructor: () => TypeObject, + getSignatureConstructor: () => SignatureObject, + getSourceMapSourceConstructor: () => SourceMapSourceObject, + }; +} - /// Language Service +/// Language Service - // Information about a specific host file. - interface HostFileInformation { - hostFileName: string; - version: string; - scriptSnapshot: ts.IScriptSnapshot; - scriptKind: ts.ScriptKind; - } +// Information about a specific host file. +interface HostFileInformation { + hostFileName: string; + version: string; + scriptSnapshot: ts.IScriptSnapshot; + scriptKind: ts.ScriptKind; +} - /* @internal */ - export interface DisplayPartsSymbolWriter extends ts.EmitTextWriter { - displayParts(): ts.SymbolDisplayPart[]; - } +/* @internal */ +export interface DisplayPartsSymbolWriter extends ts.EmitTextWriter { + displayParts(): ts.SymbolDisplayPart[]; +} - /* @internal */ - export function toEditorSettings(options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.FormatCodeSettings; - export function toEditorSettings(options: ts.EditorOptions | ts.EditorSettings): ts.EditorSettings; - export function toEditorSettings(optionsAsMap: ts.MapLike): ts.MapLike { - let allPropertiesAreCamelCased = true; - for (const key in optionsAsMap) { - if (ts.hasProperty(optionsAsMap, key) && !isCamelCase(key)) { - allPropertiesAreCamelCased = false; - break; - } - } - if (allPropertiesAreCamelCased) { - return optionsAsMap; - } - const settings: ts.MapLike = {}; - for (const key in optionsAsMap) { - if (ts.hasProperty(optionsAsMap, key)) { - const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1); - settings[newKey] = optionsAsMap[key]; - } +/* @internal */ +export function toEditorSettings(options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.FormatCodeSettings; +export function toEditorSettings(options: ts.EditorOptions | ts.EditorSettings): ts.EditorSettings; +export function toEditorSettings(optionsAsMap: ts.MapLike): ts.MapLike { + let allPropertiesAreCamelCased = true; + for (const key in optionsAsMap) { + if (ts.hasProperty(optionsAsMap, key) && !isCamelCase(key)) { + allPropertiesAreCamelCased = false; + break; } - return settings; } - - function isCamelCase(s: string) { - return !s.length || s.charAt(0) === s.charAt(0).toLowerCase(); + if (allPropertiesAreCamelCased) { + return optionsAsMap; } - - export function displayPartsToString(displayParts: ts.SymbolDisplayPart[] | undefined) { - if (displayParts) { - return ts.map(displayParts, displayPart => displayPart.text).join(""); + const settings: ts.MapLike = {}; + for (const key in optionsAsMap) { + if (ts.hasProperty(optionsAsMap, key)) { + const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1); + settings[newKey] = optionsAsMap[key]; } - - return ""; } + return settings; +} - export function getDefaultCompilerOptions(): ts.CompilerOptions { - // Always default to "ScriptTarget.ES5" for the language service - return { - target: ts.ScriptTarget.ES5, - jsx: ts.JsxEmit.Preserve - }; - } +function isCamelCase(s: string) { + return !s.length || s.charAt(0) === s.charAt(0).toLowerCase(); +} - export function getSupportedCodeFixes() { - return ts.codefix.getSupportedErrorCodes(); +export function displayPartsToString(displayParts: ts.SymbolDisplayPart[] | undefined) { + if (displayParts) { + return ts.map(displayParts, displayPart => displayPart.text).join(""); } - // Either it will be file name if host doesnt have file or it will be the host's file information - type CachedHostFileInformation = HostFileInformation | string; + return ""; +} - // 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.ESMap; - private currentDirectory: string; +export function getDefaultCompilerOptions(): ts.CompilerOptions { + // Always default to "ScriptTarget.ES5" for the language service + return { + target: ts.ScriptTarget.ES5, + jsx: ts.JsxEmit.Preserve + }; +} - constructor(private host: ts.LanguageServiceHost, getCanonicalFileName: ts.GetCanonicalFileName) { - // script id => script index - this.currentDirectory = host.getCurrentDirectory(); - this.fileNameToEntry = new ts.Map(); +export function getSupportedCodeFixes() { + return ts.codefix.getSupportedErrorCodes(); +} - // Initialize the list with the root file names - const rootFileNames = host.getScriptFileNames(); - ts.tracing?.push(ts.tracing.Phase.Session, "initializeHostCache", { count: rootFileNames.length }); - for (const fileName of rootFileNames) { - this.createEntry(fileName, ts.toPath(fileName, this.currentDirectory, getCanonicalFileName)); - } - ts.tracing?.pop(); - } - - private createEntry(fileName: string, path: ts.Path) { - let entry: CachedHostFileInformation; - const scriptSnapshot = this.host.getScriptSnapshot(fileName); - if (scriptSnapshot) { - entry = { - hostFileName: fileName, - version: this.host.getScriptVersion(fileName), - scriptSnapshot, - scriptKind: ts.getScriptKind(fileName, this.host) - }; - } - else { - entry = fileName; - } +// Either it will be file name if host doesnt have file or it will be the host's file information +type CachedHostFileInformation = HostFileInformation | string; - this.fileNameToEntry.set(path, entry); - return entry; - } +// 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.ESMap; + private currentDirectory: string; - public getEntryByPath(path: ts.Path): CachedHostFileInformation | undefined { - return this.fileNameToEntry.get(path); - } + constructor(private host: ts.LanguageServiceHost, getCanonicalFileName: ts.GetCanonicalFileName) { + // script id => script index + this.currentDirectory = host.getCurrentDirectory(); + this.fileNameToEntry = new ts.Map(); - public getHostFileInformation(path: ts.Path): HostFileInformation | undefined { - const entry = this.fileNameToEntry.get(path); - return !ts.isString(entry) ? entry : undefined; + // Initialize the list with the root file names + const rootFileNames = host.getScriptFileNames(); + ts.tracing?.push(ts.tracing.Phase.Session, "initializeHostCache", { count: rootFileNames.length }); + for (const fileName of rootFileNames) { + this.createEntry(fileName, ts.toPath(fileName, this.currentDirectory, getCanonicalFileName)); } + ts.tracing?.pop(); + } - public getOrCreateEntryByPath(fileName: string, path: ts.Path): HostFileInformation { - const info = this.getEntryByPath(path) || this.createEntry(fileName, path); - return ts.isString(info) ? undefined! : info; // TODO: GH#18217 + private createEntry(fileName: string, path: ts.Path) { + let entry: CachedHostFileInformation; + const scriptSnapshot = this.host.getScriptSnapshot(fileName); + if (scriptSnapshot) { + entry = { + hostFileName: fileName, + version: this.host.getScriptVersion(fileName), + scriptSnapshot, + scriptKind: ts.getScriptKind(fileName, this.host) + }; } - - public getRootFileNames(): string[] { - const names: string[] = []; - this.fileNameToEntry.forEach(entry => { - if (ts.isString(entry)) { - names.push(entry); - } - else { - names.push(entry.hostFileName); - } - }); - return names; + else { + entry = fileName; } - public getScriptSnapshot(path: ts.Path): ts.IScriptSnapshot { - const file = this.getHostFileInformation(path); - return (file && file.scriptSnapshot)!; // TODO: GH#18217 - } + this.fileNameToEntry.set(path, entry); + return entry; } - 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: ts.IScriptSnapshot | undefined; - private currentSourceFile: ts.SourceFile | undefined; - constructor(private host: ts.LanguageServiceHost) { - } + public getEntryByPath(path: ts.Path): CachedHostFileInformation | undefined { + return this.fileNameToEntry.get(path); + } - public getCurrentSourceFile(fileName: string): ts.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 + "'."); - } + public getHostFileInformation(path: ts.Path): HostFileInformation | undefined { + const entry = this.fileNameToEntry.get(path); + return !ts.isString(entry) ? entry : undefined; + } - const scriptKind = ts.getScriptKind(fileName, this.host); - const version = this.host.getScriptVersion(fileName); - let sourceFile: ts.SourceFile | undefined; + public getOrCreateEntryByPath(fileName: string, path: ts.Path): HostFileInformation { + const info = this.getEntryByPath(path) || this.createEntry(fileName, path); + return ts.isString(info) ? undefined! : info; // TODO: GH#18217 + } - if (this.currentFileName !== fileName) { - // This is a new file, just parse it - const options: ts.CreateSourceFileOptions = { - languageVersion: ts.ScriptTarget.Latest, - impliedNodeFormat: ts.getImpliedNodeFormatForFile(ts.toPath(fileName, this.host.getCurrentDirectory(), this.host.getCompilerHost?.()?.getCanonicalFileName || ts.hostGetCanonicalFileName(this.host)), this.host.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), this.host, this.host.getCompilationSettings()), - setExternalModuleIndicator: ts.getSetExternalModuleIndicator(this.host.getCompilationSettings()) - }; - sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, options, version, /*setNodeParents*/ true, scriptKind); + public getRootFileNames(): string[] { + const names: string[] = []; + this.fileNameToEntry.forEach(entry => { + if (ts.isString(entry)) { + names.push(entry); } - 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; + else { + names.push(entry.hostFileName); } - - return this.currentSourceFile!; - } + }); + return names; } - function setSourceFileFields(sourceFile: ts.SourceFile, scriptSnapshot: ts.IScriptSnapshot, version: string) { - sourceFile.version = version; - sourceFile.scriptSnapshot = scriptSnapshot; + public getScriptSnapshot(path: ts.Path): ts.IScriptSnapshot { + const file = this.getHostFileInformation(path); + return (file && file.scriptSnapshot)!; // TODO: GH#18217 } +} - export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: ts.IScriptSnapshot, scriptTargetOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, version: string, setNodeParents: boolean, scriptKind?: ts.ScriptKind): ts.SourceFile { - const sourceFile = ts.createSourceFile(fileName, ts.getSnapshotText(scriptSnapshot), scriptTargetOrOptions, 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: ts.IScriptSnapshot | undefined; + private currentSourceFile: ts.SourceFile | undefined; + constructor(private host: ts.LanguageServiceHost) { } - export function updateLanguageServiceSourceFile(sourceFile: ts.SourceFile, scriptSnapshot: ts.IScriptSnapshot, version: string, textChangeRange: ts.TextChangeRange | undefined, aggressiveChecks?: boolean): ts.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; + public getCurrentSourceFile(fileName: string): ts.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 + "'."); + } - // 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) - : ""; + const scriptKind = ts.getScriptKind(fileName, this.host); + const version = this.host.getScriptVersion(fileName); + let sourceFile: ts.SourceFile | undefined; - // grab the fragment from the end of the span till the end of the original text - const suffix = ts.textSpanEnd(textChangeRange.span) !== sourceFile.text.length - ? sourceFile.text.substr(ts.textSpanEnd(textChangeRange.span)) - : ""; + if (this.currentFileName !== fileName) { + // This is a new file, just parse it + const options: ts.CreateSourceFileOptions = { + languageVersion: ts.ScriptTarget.Latest, + impliedNodeFormat: ts.getImpliedNodeFormatForFile(ts.toPath(fileName, this.host.getCurrentDirectory(), this.host.getCompilerHost?.()?.getCanonicalFileName || ts.hostGetCanonicalFileName(this.host)), this.host.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), this.host, this.host.getCompilationSettings()), + setExternalModuleIndicator: ts.getSetExternalModuleIndicator(this.host.getCompilationSettings()) + }; + sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, options, 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 (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); - } + if (sourceFile) { + // All done, ensure state is up to date + this.currentFileVersion = version; + this.currentFileName = fileName; + this.currentFileScriptSnapshot = scriptSnapshot; + this.currentSourceFile = sourceFile; + } - const newSourceFile = ts.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; + return this.currentSourceFile!; + } +} - // dispose all resources held by old script snapshot - if (sourceFile !== newSourceFile && sourceFile.scriptSnapshot) { - if (sourceFile.scriptSnapshot.dispose) { - sourceFile.scriptSnapshot.dispose(); - } +function setSourceFileFields(sourceFile: ts.SourceFile, scriptSnapshot: ts.IScriptSnapshot, version: string) { + sourceFile.version = version; + sourceFile.scriptSnapshot = scriptSnapshot; +} + +export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: ts.IScriptSnapshot, scriptTargetOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, version: string, setNodeParents: boolean, scriptKind?: ts.ScriptKind): ts.SourceFile { + const sourceFile = ts.createSourceFile(fileName, ts.getSnapshotText(scriptSnapshot), scriptTargetOrOptions, setNodeParents, scriptKind); + setSourceFileFields(sourceFile, scriptSnapshot, version); + return sourceFile; +} + +export function updateLanguageServiceSourceFile(sourceFile: ts.SourceFile, scriptSnapshot: ts.IScriptSnapshot, version: string, textChangeRange: ts.TextChangeRange | undefined, aggressiveChecks?: boolean): ts.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) + : ""; - sourceFile.scriptSnapshot = undefined; + // grab the fragment from the end of the span till the end of the original text + const suffix = ts.textSpanEnd(textChangeRange.span) !== sourceFile.text.length + ? sourceFile.text.substr(ts.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 = ts.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; } - } - const options: ts.CreateSourceFileOptions = { - languageVersion: sourceFile.languageVersion, - impliedNodeFormat: sourceFile.impliedNodeFormat, - setExternalModuleIndicator: sourceFile.setExternalModuleIndicator, - }; - // Otherwise, just create a new source file. - return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, options, version, /*setNodeParents*/ true, sourceFile.scriptKind); + return newSourceFile; + } } - const NoopCancellationToken: ts.CancellationToken = { - isCancellationRequested: ts.returnFalse, - throwIfCancellationRequested: ts.noop, + const options: ts.CreateSourceFileOptions = { + languageVersion: sourceFile.languageVersion, + impliedNodeFormat: sourceFile.impliedNodeFormat, + setExternalModuleIndicator: sourceFile.setExternalModuleIndicator, }; + // Otherwise, just create a new source file. + return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, options, version, /*setNodeParents*/ true, sourceFile.scriptKind); +} - class CancellationTokenObject implements ts.CancellationToken { - constructor(private cancellationToken: ts.HostCancellationToken) { - } - - public isCancellationRequested(): boolean { - return this.cancellationToken.isCancellationRequested(); - } +const NoopCancellationToken: ts.CancellationToken = { + isCancellationRequested: ts.returnFalse, + throwIfCancellationRequested: ts.noop, +}; - public throwIfCancellationRequested(): void { - if (this.isCancellationRequested()) { - ts.tracing?.instant(ts.tracing.Phase.Session, "cancellationThrown", { kind: "CancellationTokenObject" }); - throw new ts.OperationCanceledException(); - } - } +class CancellationTokenObject implements ts.CancellationToken { + constructor(private cancellationToken: ts.HostCancellationToken) { } - /* @internal */ - /** A cancellation that throttles calls to the host */ - export class ThrottledCancellationToken implements ts.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; + public isCancellationRequested(): boolean { + return this.cancellationToken.isCancellationRequested(); + } - constructor(private hostCancellationToken: ts.HostCancellationToken, private readonly throttleWaitMilliseconds = 20) { + public throwIfCancellationRequested(): void { + if (this.isCancellationRequested()) { + ts.tracing?.instant(ts.tracing.Phase.Session, "cancellationThrown", { kind: "CancellationTokenObject" }); + throw new ts.OperationCanceledException(); } + } +} - public isCancellationRequested(): boolean { - const time = ts.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; - } +/* @internal */ +/** A cancellation that throttles calls to the host */ +export class ThrottledCancellationToken implements ts.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; - public throwIfCancellationRequested(): void { - if (this.isCancellationRequested()) { - ts.tracing?.instant(ts.tracing.Phase.Session, "cancellationThrown", { kind: "ThrottledCancellationToken" }); - throw new ts.OperationCanceledException(); - } - } + constructor(private hostCancellationToken: ts.HostCancellationToken, private readonly throttleWaitMilliseconds = 20) { } - const invalidOperationsInPartialSemanticMode: readonly (keyof ts.LanguageService)[] = [ - "getSemanticDiagnostics", - "getSuggestionDiagnostics", - "getCompilerOptionsDiagnostics", - "getSemanticClassifications", - "getEncodedSemanticClassifications", - "getCodeFixesAtPosition", - "getCombinedCodeFix", - "applyCodeActionCommand", - "organizeImports", - "getEditsForFileRename", - "getEmitOutput", - "getApplicableRefactors", - "getEditsForRefactor", - "prepareCallHierarchy", - "provideCallHierarchyIncomingCalls", - "provideCallHierarchyOutgoingCalls", - "provideInlayHints" - ]; - - const invalidOperationsInSyntacticMode: readonly (keyof ts.LanguageService)[] = [ - ...invalidOperationsInPartialSemanticMode, - "getCompletionsAtPosition", - "getCompletionEntryDetails", - "getCompletionEntrySymbol", - "getSignatureHelpItems", - "getQuickInfoAtPosition", - "getDefinitionAtPosition", - "getDefinitionAndBoundSpan", - "getImplementationAtPosition", - "getTypeDefinitionAtPosition", - "getReferencesAtPosition", - "findReferences", - "getOccurrencesAtPosition", - "getDocumentHighlights", - "getNavigateToItems", - "getRenameInfo", - "findRenameLocations", - "getApplicableRefactors", - ]; - export function createLanguageService(host: ts.LanguageServiceHost, documentRegistry: ts.DocumentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()), syntaxOnlyOrLanguageServiceMode?: boolean | ts.LanguageServiceMode): ts.LanguageService { - let languageServiceMode: ts.LanguageServiceMode; - if (syntaxOnlyOrLanguageServiceMode === undefined) { - languageServiceMode = ts.LanguageServiceMode.Semantic; - } - else if (typeof syntaxOnlyOrLanguageServiceMode === "boolean") { - // languageServiceMode = SyntaxOnly - languageServiceMode = syntaxOnlyOrLanguageServiceMode ? ts.LanguageServiceMode.Syntactic : ts.LanguageServiceMode.Semantic; - } - else { - languageServiceMode = syntaxOnlyOrLanguageServiceMode; + public isCancellationRequested(): boolean { + const time = ts.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(); } - const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); - let program: ts.Program; - let lastProjectVersion: string; - let lastTypesRootVersion = 0; - - const cancellationToken = host.getCancellationToken - ? new CancellationTokenObject(host.getCancellationToken()) - : NoopCancellationToken; + return false; + } - const currentDirectory = host.getCurrentDirectory(); + public throwIfCancellationRequested(): void { + if (this.isCancellationRequested()) { + ts.tracing?.instant(ts.tracing.Phase.Session, "cancellationThrown", { kind: "ThrottledCancellationToken" }); + throw new ts.OperationCanceledException(); + } + } +} - // Checks if the localized messages json is set, and if not, query the host for it - ts.maybeSetLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages?.bind(host)); +const invalidOperationsInPartialSemanticMode: readonly (keyof ts.LanguageService)[] = [ + "getSemanticDiagnostics", + "getSuggestionDiagnostics", + "getCompilerOptionsDiagnostics", + "getSemanticClassifications", + "getEncodedSemanticClassifications", + "getCodeFixesAtPosition", + "getCombinedCodeFix", + "applyCodeActionCommand", + "organizeImports", + "getEditsForFileRename", + "getEmitOutput", + "getApplicableRefactors", + "getEditsForRefactor", + "prepareCallHierarchy", + "provideCallHierarchyIncomingCalls", + "provideCallHierarchyOutgoingCalls", + "provideInlayHints" +]; + +const invalidOperationsInSyntacticMode: readonly (keyof ts.LanguageService)[] = [ + ...invalidOperationsInPartialSemanticMode, + "getCompletionsAtPosition", + "getCompletionEntryDetails", + "getCompletionEntrySymbol", + "getSignatureHelpItems", + "getQuickInfoAtPosition", + "getDefinitionAtPosition", + "getDefinitionAndBoundSpan", + "getImplementationAtPosition", + "getTypeDefinitionAtPosition", + "getReferencesAtPosition", + "findReferences", + "getOccurrencesAtPosition", + "getDocumentHighlights", + "getNavigateToItems", + "getRenameInfo", + "findRenameLocations", + "getApplicableRefactors", +]; +export function createLanguageService(host: ts.LanguageServiceHost, documentRegistry: ts.DocumentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()), syntaxOnlyOrLanguageServiceMode?: boolean | ts.LanguageServiceMode): ts.LanguageService { + let languageServiceMode: ts.LanguageServiceMode; + if (syntaxOnlyOrLanguageServiceMode === undefined) { + languageServiceMode = ts.LanguageServiceMode.Semantic; + } + else if (typeof syntaxOnlyOrLanguageServiceMode === "boolean") { + // languageServiceMode = SyntaxOnly + languageServiceMode = syntaxOnlyOrLanguageServiceMode ? ts.LanguageServiceMode.Syntactic : ts.LanguageServiceMode.Semantic; + } + else { + languageServiceMode = syntaxOnlyOrLanguageServiceMode; + } - function log(message: string) { - if (host.log) { - host.log(message); - } - } + const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); + let program: ts.Program; + let lastProjectVersion: string; + let lastTypesRootVersion = 0; - const useCaseSensitiveFileNames = ts.hostUsesCaseSensitiveFileNames(host); - const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - const sourceMapper = ts.getSourceMapper({ - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getCurrentDirectory: () => currentDirectory, - getProgram, - fileExists: ts.maybeBind(host, host.fileExists), - readFile: ts.maybeBind(host, host.readFile), - getDocumentPositionMapper: ts.maybeBind(host, host.getDocumentPositionMapper), - getSourceFileLike: ts.maybeBind(host, host.getSourceFileLike), - log - }); + const cancellationToken = host.getCancellationToken + ? new CancellationTokenObject(host.getCancellationToken()) + : NoopCancellationToken; - function getValidSourceFile(fileName: string): ts.SourceFile { - const sourceFile = program.getSourceFile(fileName); - if (!sourceFile) { - const error: Error & ts.PossibleProgramFileInfo = new Error(`Could not find source file: '${fileName}'.`); + const currentDirectory = host.getCurrentDirectory(); - // 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); + // Checks if the localized messages json is set, and if not, query the host for it + ts.maybeSetLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages?.bind(host)); - throw error; - } - return sourceFile; + function log(message: string) { + if (host.log) { + host.log(message); } + } - function synchronizeHostData(): void { - ts.Debug.assert(languageServiceMode !== ts.LanguageServiceMode.Syntactic); - // perform fast check if host supports it - if (host.getProjectVersion) { - const hostProjectVersion = host.getProjectVersion(); - if (hostProjectVersion) { - if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames?.()) { - return; - } + const useCaseSensitiveFileNames = ts.hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + const sourceMapper = ts.getSourceMapper({ + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getCurrentDirectory: () => currentDirectory, + getProgram, + fileExists: ts.maybeBind(host, host.fileExists), + readFile: ts.maybeBind(host, host.readFile), + getDocumentPositionMapper: ts.maybeBind(host, host.getDocumentPositionMapper), + getSourceFileLike: ts.maybeBind(host, host.getSourceFileLike), + log + }); + + function getValidSourceFile(fileName: string): ts.SourceFile { + const sourceFile = program.getSourceFile(fileName); + if (!sourceFile) { + const error: Error & ts.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; + } - lastProjectVersion = hostProjectVersion; + function synchronizeHostData(): void { + ts.Debug.assert(languageServiceMode !== ts.LanguageServiceMode.Syntactic); + // perform fast check if host supports it + if (host.getProjectVersion) { + const hostProjectVersion = host.getProjectVersion(); + if (hostProjectVersion) { + if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames?.()) { + return; } - } - - 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 newSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); - const hasInvalidatedResolution: ts.HasInvalidatedResolution = host.hasInvalidatedResolution || ts.returnFalse; - const hasChangedAutomaticTypeDirectiveNames = ts.maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames); - const projectReferences = host.getProjectReferences?.(); - let parsedCommandLines: ts.ESMap | undefined; - const parseConfigHost: ts.ParseConfigFileHost = { - useCaseSensitiveFileNames, - fileExists, - readFile, - readDirectory, - trace: ts.maybeBind(host, host.trace), - getCurrentDirectory: () => currentDirectory, - onUnRecoverableConfigFileDiagnostic: ts.noop, - }; - // If the program is already up-to-date, we can reuse it - if (ts.isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) { - return; + lastProjectVersion = hostProjectVersion; } + } - // 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. - - // Now create a new compiler - const compilerHost: ts.CompilerHost = { - getSourceFile: getOrCreateSourceFile, - getSourceFileByPath: getOrCreateSourceFileByPath, - getCancellationToken: () => cancellationToken, - getCanonicalFileName, - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getNewLine: () => ts.getNewLineCharacter(newSettings, () => ts.getNewLineOrDefaultFromHost(host)), - getDefaultLibFileName: options => host.getDefaultLibFileName(options), - writeFile: ts.noop, - getCurrentDirectory: () => currentDirectory, - fileExists, - readFile, - getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache), - realpath: ts.maybeBind(host, host.realpath), - directoryExists: directoryName => { - return ts.directoryProbablyExists(directoryName, host); - }, - getDirectories: path => { - return host.getDirectories ? host.getDirectories(path) : []; - }, - readDirectory, - onReleaseOldSourceFile, - onReleaseParsedCommandLine, - hasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames, - trace: parseConfigHost.trace, - resolveModuleNames: ts.maybeBind(host, host.resolveModuleNames), - getModuleResolutionCache: ts.maybeBind(host, host.getModuleResolutionCache), - resolveTypeReferenceDirectives: ts.maybeBind(host, host.resolveTypeReferenceDirectives), - useSourceOfProjectReferenceRedirect: ts.maybeBind(host, host.useSourceOfProjectReferenceRedirect), - getParsedCommandLine, - }; - host.setCompilerHost?.(compilerHost); - - const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const options: ts.CreateProgramOptions = { - rootNames: rootFileNames, - options: newSettings, - host: compilerHost, - oldProgram: program, - projectReferences - }; - program = ts.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; - parsedCommandLines = 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(); + 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 newSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); + const hasInvalidatedResolution: ts.HasInvalidatedResolution = host.hasInvalidatedResolution || ts.returnFalse; + const hasChangedAutomaticTypeDirectiveNames = ts.maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames); + const projectReferences = host.getProjectReferences?.(); + let parsedCommandLines: ts.ESMap | undefined; + const parseConfigHost: ts.ParseConfigFileHost = { + useCaseSensitiveFileNames, + fileExists, + readFile, + readDirectory, + trace: ts.maybeBind(host, host.trace), + getCurrentDirectory: () => currentDirectory, + onUnRecoverableConfigFileDiagnostic: ts.noop, + }; - // Make sure all the nodes in the program are both bound, and have their parent - // pointers set property. - program.getTypeChecker(); + // If the program is already up-to-date, we can reuse it + if (ts.isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) { return; + } - function getParsedCommandLine(fileName: string): ts.ParsedCommandLine | undefined { - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const existing = parsedCommandLines?.get(path); - if (existing !== undefined) - return existing || undefined; - - const result = host.getParsedCommandLine ? - host.getParsedCommandLine(fileName) : - getParsedCommandLineOfConfigFileUsingSourceFile(fileName); - (parsedCommandLines ||= new ts.Map()).set(path, result || false); - return result; - } - - function getParsedCommandLineOfConfigFileUsingSourceFile(configFileName: string): ts.ParsedCommandLine | undefined { - const result = getOrCreateSourceFile(configFileName, ts.ScriptTarget.JSON) as ts.JsonSourceFile | undefined; - if (!result) - return undefined; - result.path = ts.toPath(configFileName, currentDirectory, getCanonicalFileName); - result.resolvedPath = result.path; - result.originalFileName = result.fileName; - return ts.parseJsonSourceFileConfigFileContent(result, parseConfigHost, ts.getNormalizedAbsolutePath(ts.getDirectoryPath(configFileName), currentDirectory), - /*optionsToExtend*/ undefined, ts.getNormalizedAbsolutePath(configFileName, currentDirectory)); - } - function onReleaseParsedCommandLine(configFileName: string, oldResolvedRef: ts.ResolvedProjectReference | undefined, oldOptions: ts.CompilerOptions) { - if (host.getParsedCommandLine) { - host.onReleaseParsedCommandLine?.(configFileName, oldResolvedRef, oldOptions); - } - else if (oldResolvedRef) { - onReleaseOldSourceFile(oldResolvedRef.sourceFile, oldOptions); - } - } + // 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. - function fileExists(fileName: string): boolean { - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - return entry ? - !ts.isString(entry) : - (!!host.fileExists && host.fileExists(fileName)); - } + // Now create a new compiler + const compilerHost: ts.CompilerHost = { + getSourceFile: getOrCreateSourceFile, + getSourceFileByPath: getOrCreateSourceFileByPath, + getCancellationToken: () => cancellationToken, + getCanonicalFileName, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getNewLine: () => ts.getNewLineCharacter(newSettings, () => ts.getNewLineOrDefaultFromHost(host)), + getDefaultLibFileName: options => host.getDefaultLibFileName(options), + writeFile: ts.noop, + getCurrentDirectory: () => currentDirectory, + fileExists, + readFile, + getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache), + realpath: ts.maybeBind(host, host.realpath), + directoryExists: directoryName => { + return ts.directoryProbablyExists(directoryName, host); + }, + getDirectories: path => { + return host.getDirectories ? host.getDirectories(path) : []; + }, + readDirectory, + onReleaseOldSourceFile, + onReleaseParsedCommandLine, + hasInvalidatedResolution, + hasChangedAutomaticTypeDirectiveNames, + trace: parseConfigHost.trace, + resolveModuleNames: ts.maybeBind(host, host.resolveModuleNames), + getModuleResolutionCache: ts.maybeBind(host, host.getModuleResolutionCache), + resolveTypeReferenceDirectives: ts.maybeBind(host, host.resolveTypeReferenceDirectives), + useSourceOfProjectReferenceRedirect: ts.maybeBind(host, host.useSourceOfProjectReferenceRedirect), + getParsedCommandLine, + }; + host.setCompilerHost?.(compilerHost); + + const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); + const options: ts.CreateProgramOptions = { + rootNames: rootFileNames, + options: newSettings, + host: compilerHost, + oldProgram: program, + projectReferences + }; + program = ts.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; + parsedCommandLines = 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 getParsedCommandLine(fileName: string): ts.ParsedCommandLine | undefined { + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const existing = parsedCommandLines?.get(path); + if (existing !== undefined) + return existing || undefined; + + const result = host.getParsedCommandLine ? + host.getParsedCommandLine(fileName) : + getParsedCommandLineOfConfigFileUsingSourceFile(fileName); + (parsedCommandLines ||= new ts.Map()).set(path, result || false); + return result; + } - function readFile(fileName: string) { - // stub missing host functionality - const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - if (entry) { - return ts.isString(entry) ? undefined : ts.getSnapshotText(entry.scriptSnapshot); - } - return host.readFile && host.readFile(fileName); + function getParsedCommandLineOfConfigFileUsingSourceFile(configFileName: string): ts.ParsedCommandLine | undefined { + const result = getOrCreateSourceFile(configFileName, ts.ScriptTarget.JSON) as ts.JsonSourceFile | undefined; + if (!result) + return undefined; + result.path = ts.toPath(configFileName, currentDirectory, getCanonicalFileName); + result.resolvedPath = result.path; + result.originalFileName = result.fileName; + return ts.parseJsonSourceFileConfigFileContent(result, parseConfigHost, ts.getNormalizedAbsolutePath(ts.getDirectoryPath(configFileName), currentDirectory), + /*optionsToExtend*/ undefined, ts.getNormalizedAbsolutePath(configFileName, currentDirectory)); + } + function onReleaseParsedCommandLine(configFileName: string, oldResolvedRef: ts.ResolvedProjectReference | undefined, oldOptions: ts.CompilerOptions) { + if (host.getParsedCommandLine) { + host.onReleaseParsedCommandLine?.(configFileName, oldResolvedRef, oldOptions); } - - function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) { - ts.Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); - return host.readDirectory!(path, extensions, exclude, include, depth); + else if (oldResolvedRef) { + onReleaseOldSourceFile(oldResolvedRef.sourceFile, oldOptions); } + } - // Release any files we have acquired in the old program but are - // not part of the new program. - function onReleaseOldSourceFile(oldSourceFile: ts.SourceFile, oldOptions: ts.CompilerOptions) { - const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); - documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey, oldSourceFile.scriptKind); - } + function fileExists(fileName: string): boolean { + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache && hostCache.getEntryByPath(path); + return entry ? + !ts.isString(entry) : + (!!host.fileExists && host.fileExists(fileName)); + } - function getOrCreateSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined { - return getOrCreateSourceFileByPath(fileName, ts.toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + function readFile(fileName: string) { + // stub missing host functionality + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache && hostCache.getEntryByPath(path); + if (entry) { + return ts.isString(entry) ? undefined : ts.getSnapshotText(entry.scriptSnapshot); } + return host.readFile && host.readFile(fileName); + } - function getOrCreateSourceFileByPath(fileName: string, path: ts.Path, _languageVersion: ts.ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined { - ts.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" - if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) { - return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); - } - else { - // Release old source file and fall through to aquire new file with new script kind - documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()), oldSourceFile.scriptKind); - } - } + function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) { + ts.Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); + return host.readDirectory!(path, extensions, exclude, include, depth); + } - // We didn't already have the file. Fall through and acquire it from the registry. - } + // Release any files we have acquired in the old program but are + // not part of the new program. + function onReleaseOldSourceFile(oldSourceFile: ts.SourceFile, oldOptions: ts.CompilerOptions) { + const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); + documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey, oldSourceFile.scriptKind); + } - // Could not find this file in the old program, create a new SourceFile for it. - return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); - } + function getOrCreateSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined { + return getOrCreateSourceFileByPath(fileName, ts.toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); } - // TODO: GH#18217 frequently asserted as defined - function getProgram(): ts.Program | undefined { - if (languageServiceMode === ts.LanguageServiceMode.Syntactic) { - ts.Debug.assert(program === undefined); + function getOrCreateSourceFileByPath(fileName: string, path: ts.Path, _languageVersion: ts.ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined { + ts.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; } - synchronizeHostData(); + // 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" + if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) { + return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); + } + else { + // Release old source file and fall through to aquire new file with new script kind + documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()), oldSourceFile.scriptKind); + } + } - return program; - } + // We didn't already have the file. Fall through and acquire it from the registry. + } - function getAutoImportProvider(): ts.Program | undefined { - return host.getPackageJsonAutoImportProvider?.(); + // Could not find this file in the old program, create a new SourceFile for it. + return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } + } - function cleanupSemanticCache(): void { - program = undefined!; // TODO: GH#18217 + // TODO: GH#18217 frequently asserted as defined + function getProgram(): ts.Program | undefined { + if (languageServiceMode === ts.LanguageServiceMode.Syntactic) { + ts.Debug.assert(program === undefined); + return undefined; } - function dispose(): void { - if (program) { - // Use paths to ensure we are using correct key and paths as document registry could be created with different current directory than host - const key = documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()); - ts.forEach(program.getSourceFiles(), f => documentRegistry.releaseDocumentWithKey(f.resolvedPath, key, f.scriptKind)); - program = undefined!; // TODO: GH#18217 - } - host = undefined!; - } + synchronizeHostData(); + + return program; + } + + function getAutoImportProvider(): ts.Program | undefined { + return host.getPackageJsonAutoImportProvider?.(); + } - /// Diagnostics - function getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - synchronizeHostData(); + function cleanupSemanticCache(): void { + program = undefined!; // TODO: GH#18217 + } - return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice(); + function dispose(): void { + if (program) { + // Use paths to ensure we are using correct key and paths as document registry could be created with different current directory than host + const key = documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()); + ts.forEach(program.getSourceFiles(), f => documentRegistry.releaseDocumentWithKey(f.resolvedPath, key, f.scriptKind)); + program = undefined!; // TODO: GH#18217 } + host = undefined!; + } - /** - * 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): ts.Diagnostic[] { - synchronizeHostData(); + /// Diagnostics + function getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + synchronizeHostData(); - const targetSourceFile = getValidSourceFile(fileName); + return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice(); + } - // 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. + /** + * 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): ts.Diagnostic[] { + synchronizeHostData(); - const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken); - if (!ts.getEmitDeclarations(program.getCompilerOptions())) { - return semanticDiagnostics.slice(); - } + const targetSourceFile = getValidSourceFile(fileName); - // 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]; - } + // 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. - function getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - synchronizeHostData(); - return ts.computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken); + const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken); + if (!ts.getEmitDeclarations(program.getCompilerOptions())) { + return semanticDiagnostics.slice(); } - function getCompilerOptionsDiagnostics() { - synchronizeHostData(); - return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; - } + // 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 getCompletionsAtPosition(fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions = ts.emptyOptions, formattingSettings?: ts.FormatCodeSettings): ts.CompletionInfo | undefined { - // Convert from deprecated options names to new names - const fullPreferences: ts.UserPreferences = { - ...ts.identity(options), - includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports, - includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions, - }; - synchronizeHostData(); - return ts.Completions.getCompletionsAtPosition(host, program, log, getValidSourceFile(fileName), position, fullPreferences, options.triggerCharacter, options.triggerKind, cancellationToken, formattingSettings && ts.formatting.getFormatContext(formattingSettings, host)); - } + function getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + synchronizeHostData(); + return ts.computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken); + } - function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: ts.FormatCodeSettings | undefined, source: string | undefined, preferences: ts.UserPreferences = ts.emptyOptions, data?: ts.CompletionEntryData): ts.CompletionEntryDetails | undefined { - synchronizeHostData(); - return ts.Completions.getCompletionEntryDetails(program, log, getValidSourceFile(fileName), position, { name, source, data }, host, (formattingOptions && ts.formatting.getFormatContext(formattingOptions, host))!, // TODO: GH#18217 - preferences, cancellationToken); - } + function getCompilerOptionsDiagnostics() { + synchronizeHostData(); + return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; + } - function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: ts.UserPreferences = ts.emptyOptions): ts.Symbol | undefined { - synchronizeHostData(); - return ts.Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences); - } + function getCompletionsAtPosition(fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions = ts.emptyOptions, formattingSettings?: ts.FormatCodeSettings): ts.CompletionInfo | undefined { + // Convert from deprecated options names to new names + const fullPreferences: ts.UserPreferences = { + ...ts.identity(options), + includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports, + includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions, + }; + synchronizeHostData(); + return ts.Completions.getCompletionsAtPosition(host, program, log, getValidSourceFile(fileName), position, fullPreferences, options.triggerCharacter, options.triggerKind, cancellationToken, formattingSettings && ts.formatting.getFormatContext(formattingSettings, host)); + } - function getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo | undefined { - synchronizeHostData(); + function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: ts.FormatCodeSettings | undefined, source: string | undefined, preferences: ts.UserPreferences = ts.emptyOptions, data?: ts.CompletionEntryData): ts.CompletionEntryDetails | undefined { + synchronizeHostData(); + return ts.Completions.getCompletionEntryDetails(program, log, getValidSourceFile(fileName), position, { name, source, data }, host, (formattingOptions && ts.formatting.getFormatContext(formattingOptions, host))!, // TODO: GH#18217 + preferences, cancellationToken); + } - const sourceFile = getValidSourceFile(fileName); - const node = ts.getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - // Avoid giving quickInfo for the sourceFile as a whole. - return undefined; - } + function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: ts.UserPreferences = ts.emptyOptions): ts.Symbol | undefined { + synchronizeHostData(); + return ts.Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences); + } - 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: ts.ScriptElementKind.unknown, - kindModifiers: ts.ScriptElementKindModifier.none, - textSpan: ts.createTextSpanFromNode(nodeForQuickInfo, sourceFile), - displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => ts.typeToDisplayParts(typeChecker, type, ts.getContainerNode(nodeForQuickInfo))), - documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined, - tags: type.symbol ? type.symbol.getJsDocTags(typeChecker) : undefined - }; - } + function getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo | undefined { + synchronizeHostData(); - const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, ts.getContainerNode(nodeForQuickInfo), nodeForQuickInfo)); - return { - kind: symbolKind, - kindModifiers: ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), - textSpan: ts.createTextSpanFromNode(nodeForQuickInfo, sourceFile), - displayParts, - documentation, - tags, - }; + const sourceFile = getValidSourceFile(fileName); + const node = ts.getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + // Avoid giving quickInfo for the sourceFile as a whole. + return undefined; } - function getNodeForQuickInfo(node: ts.Node): ts.Node { - if (ts.isNewExpression(node.parent) && node.pos === node.parent.pos) { - return node.parent.expression; - } - if (ts.isNamedTupleMember(node.parent) && node.pos === node.parent.pos) { - return node.parent; - } - if (ts.isImportMeta(node.parent) && node.parent.name === node) { - return node.parent; - } - return node; - } + const typeChecker = program.getTypeChecker(); + const nodeForQuickInfo = getNodeForQuickInfo(node); + const symbol = getSymbolAtLocationForQuickInfo(nodeForQuickInfo, typeChecker); - function shouldGetType(sourceFile: ts.SourceFile, node: ts.Node, position: number): boolean { - switch (node.kind) { - case ts.SyntaxKind.Identifier: - return !ts.isLabelName(node) && !ts.isTagName(node) && !ts.isConstTypeReference(node.parent); - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.QualifiedName: - // Don't return quickInfo if inside the comment in `a/**/.b` - return !ts.isInComment(sourceFile, position); - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.ThisType: - case ts.SyntaxKind.SuperKeyword: - case ts.SyntaxKind.NamedTupleMember: - return true; - case ts.SyntaxKind.MetaProperty: - return ts.isImportMeta(node); - default: - return false; - } + if (!symbol || typeChecker.isUnknownSymbol(symbol)) { + const type = shouldGetType(sourceFile, nodeForQuickInfo, position) ? typeChecker.getTypeAtLocation(nodeForQuickInfo) : undefined; + return type && { + kind: ts.ScriptElementKind.unknown, + kindModifiers: ts.ScriptElementKindModifier.none, + textSpan: ts.createTextSpanFromNode(nodeForQuickInfo, sourceFile), + displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => ts.typeToDisplayParts(typeChecker, type, ts.getContainerNode(nodeForQuickInfo))), + documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined, + tags: type.symbol ? type.symbol.getJsDocTags(typeChecker) : undefined + }; } - /// Goto definition - function getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly?: boolean, stopAtAlias?: boolean): readonly ts.DefinitionInfo[] | undefined { - synchronizeHostData(); - return ts.GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position, searchOtherFilesOnly, stopAtAlias); - } + const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => ts.SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, ts.getContainerNode(nodeForQuickInfo), nodeForQuickInfo)); + return { + kind: symbolKind, + kindModifiers: ts.SymbolDisplay.getSymbolModifiers(typeChecker, symbol), + textSpan: ts.createTextSpanFromNode(nodeForQuickInfo, sourceFile), + displayParts, + documentation, + tags, + }; + } - function getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan | undefined { - synchronizeHostData(); - return ts.GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position); + function getNodeForQuickInfo(node: ts.Node): ts.Node { + if (ts.isNewExpression(node.parent) && node.pos === node.parent.pos) { + return node.parent.expression; } - - function getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[] | undefined { - synchronizeHostData(); - return ts.GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position); + if (ts.isNamedTupleMember(node.parent) && node.pos === node.parent.pos) { + return node.parent; } - - /// Goto implementation - - function getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] | undefined { - synchronizeHostData(); - return ts.FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); + if (ts.isImportMeta(node.parent) && node.parent.name === node) { + return node.parent; } + return node; + } - /// References and Occurrences - function getOccurrencesAtPosition(fileName: string, position: number): readonly ts.ReferenceEntry[] | undefined { - return ts.flatMap(getDocumentHighlights(fileName, position, [fileName]), entry => entry.highlightSpans.map(highlightSpan => ({ - fileName: entry.fileName, - textSpan: highlightSpan.textSpan, - isWriteAccess: highlightSpan.kind === ts.HighlightSpanKind.writtenReference, - ...highlightSpan.isInString && { isInString: true }, - ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } - }))); + function shouldGetType(sourceFile: ts.SourceFile, node: ts.Node, position: number): boolean { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return !ts.isLabelName(node) && !ts.isTagName(node) && !ts.isConstTypeReference(node.parent); + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.QualifiedName: + // Don't return quickInfo if inside the comment in `a/**/.b` + return !ts.isInComment(sourceFile, position); + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.ThisType: + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.NamedTupleMember: + return true; + case ts.SyntaxKind.MetaProperty: + return ts.isImportMeta(node); + default: + return false; } + } - function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): ts.DocumentHighlights[] | undefined { - const normalizedFileName = ts.normalizePath(fileName); - ts.Debug.assert(filesToSearch.some(f => ts.normalizePath(f) === normalizedFileName)); - synchronizeHostData(); - const sourceFilesToSearch = ts.mapDefined(filesToSearch, fileName => program.getSourceFile(fileName)); - const sourceFile = getValidSourceFile(fileName); - return ts.DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch); - } + /// Goto definition + function getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly?: boolean, stopAtAlias?: boolean): readonly ts.DefinitionInfo[] | undefined { + synchronizeHostData(); + return ts.GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position, searchOtherFilesOnly, stopAtAlias); + } - function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] | undefined { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const node = ts.getAdjustedRenameLocation(ts.getTouchingPropertyName(sourceFile, position)); - if (!ts.Rename.nodeIsEligibleForRename(node)) - return undefined; - if (ts.isIdentifier(node) && (ts.isJsxOpeningElement(node.parent) || ts.isJsxClosingElement(node.parent)) && ts.isIntrinsicJsxName(node.escapedText)) { - const { openingElement, closingElement } = node.parent.parent; - return [openingElement, closingElement].map((node): ts.RenameLocation => { - const textSpan = ts.createTextSpanFromNode(node.tagName, sourceFile); - return { - fileName: sourceFile.fileName, - textSpan, - ...ts.FindAllReferences.toContextSpan(textSpan, sourceFile, node.parent) - }; - }); - } - else { - return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, use: ts.FindAllReferences.FindReferencesUse.Rename }, (entry, originalNode, checker) => ts.FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false)); - } - } + function getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan | undefined { + synchronizeHostData(); + return ts.GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position); + } - function getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] | undefined { - synchronizeHostData(); - return getReferencesWorker(ts.getTouchingPropertyName(getValidSourceFile(fileName), position), position, { use: ts.FindAllReferences.FindReferencesUse.References }, ts.FindAllReferences.toReferenceEntry); - } + function getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[] | undefined { + synchronizeHostData(); + return ts.GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position); + } - function getReferencesWorker(node: ts.Node, position: number, options: ts.FindAllReferences.Options, cb: ts.FindAllReferences.ToReferenceOrRenameEntry): T[] | undefined { - synchronizeHostData(); + /// Goto implementation - // Exclude default library when renaming as commonly user don't want to change that file. - const sourceFiles = options && options.use === ts.FindAllReferences.FindReferencesUse.Rename - ? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile)) - : program.getSourceFiles(); + function getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] | undefined { + synchronizeHostData(); + return ts.FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); + } - return ts.FindAllReferences.findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb); - } + /// References and Occurrences + function getOccurrencesAtPosition(fileName: string, position: number): readonly ts.ReferenceEntry[] | undefined { + return ts.flatMap(getDocumentHighlights(fileName, position, [fileName]), entry => entry.highlightSpans.map(highlightSpan => ({ + fileName: entry.fileName, + textSpan: highlightSpan.textSpan, + isWriteAccess: highlightSpan.kind === ts.HighlightSpanKind.writtenReference, + ...highlightSpan.isInString && { isInString: true }, + ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } + }))); + } - function findReferences(fileName: string, position: number): ts.ReferencedSymbol[] | undefined { - synchronizeHostData(); - return ts.FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); - } + function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): ts.DocumentHighlights[] | undefined { + const normalizedFileName = ts.normalizePath(fileName); + ts.Debug.assert(filesToSearch.some(f => ts.normalizePath(f) === normalizedFileName)); + synchronizeHostData(); + const sourceFilesToSearch = ts.mapDefined(filesToSearch, fileName => program.getSourceFile(fileName)); + const sourceFile = getValidSourceFile(fileName); + return ts.DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch); + } - function getFileReferences(fileName: string): ts.ReferenceEntry[] { - synchronizeHostData(); - return ts.FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(ts.FindAllReferences.toReferenceEntry); + function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] | undefined { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const node = ts.getAdjustedRenameLocation(ts.getTouchingPropertyName(sourceFile, position)); + if (!ts.Rename.nodeIsEligibleForRename(node)) + return undefined; + if (ts.isIdentifier(node) && (ts.isJsxOpeningElement(node.parent) || ts.isJsxClosingElement(node.parent)) && ts.isIntrinsicJsxName(node.escapedText)) { + const { openingElement, closingElement } = node.parent.parent; + return [openingElement, closingElement].map((node): ts.RenameLocation => { + const textSpan = ts.createTextSpanFromNode(node.tagName, sourceFile); + return { + fileName: sourceFile.fileName, + textSpan, + ...ts.FindAllReferences.toContextSpan(textSpan, sourceFile, node.parent) + }; + }); } - - function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): ts.NavigateToItem[] { - synchronizeHostData(); - const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles(); - return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); + else { + return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, use: ts.FindAllReferences.FindReferencesUse.Rename }, (entry, originalNode, checker) => ts.FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false)); } + } - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) { - synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - return ts.getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit); - } + function getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] | undefined { + synchronizeHostData(); + return getReferencesWorker(ts.getTouchingPropertyName(getValidSourceFile(fileName), position), position, { use: ts.FindAllReferences.FindReferencesUse.References }, ts.FindAllReferences.toReferenceEntry); + } - // Signature help - /** - * This is a semantic operation. - */ - function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: ts.SignatureHelpItemsOptions = ts.emptyOptions): ts.SignatureHelpItems | undefined { - synchronizeHostData(); + function getReferencesWorker(node: ts.Node, position: number, options: ts.FindAllReferences.Options, cb: ts.FindAllReferences.ToReferenceOrRenameEntry): T[] | undefined { + synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); + // Exclude default library when renaming as commonly user don't want to change that file. + const sourceFiles = options && options.use === ts.FindAllReferences.FindReferencesUse.Rename + ? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile)) + : program.getSourceFiles(); - return ts.SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); - } + return ts.FindAllReferences.findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb); + } - /// Syntactic features - function getNonBoundSourceFile(fileName: string): ts.SourceFile { - return syntaxTreeCache.getCurrentSourceFile(fileName); - } + function findReferences(fileName: string, position: number): ts.ReferencedSymbol[] | undefined { + synchronizeHostData(); + return ts.FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); + } - function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): ts.TextSpan | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + function getFileReferences(fileName: string): ts.ReferenceEntry[] { + synchronizeHostData(); + return ts.FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(ts.FindAllReferences.toReferenceEntry); + } - // Get node at the location - const node = ts.getTouchingPropertyName(sourceFile, startPos); + function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): ts.NavigateToItem[] { + synchronizeHostData(); + const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles(); + return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); + } - if (node === sourceFile) { - return undefined; - } + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) { + synchronizeHostData(); - switch (node.kind) { - case ts.SyntaxKind.PropertyAccessExpression: - case ts.SyntaxKind.QualifiedName: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.SuperKeyword: - case ts.SyntaxKind.ThisKeyword: - case ts.SyntaxKind.ThisType: - case ts.SyntaxKind.Identifier: - break; + const sourceFile = getValidSourceFile(fileName); + const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); + return ts.getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit); + } - // Cant create the text span - default: - return undefined; - } + // Signature help + /** + * This is a semantic operation. + */ + function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: ts.SignatureHelpItemsOptions = ts.emptyOptions): ts.SignatureHelpItems | undefined { + synchronizeHostData(); - let nodeForStartPos = node; - while (true) { - if (ts.isRightSideOfPropertyAccess(nodeForStartPos) || ts.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 (ts.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 === ts.SyntaxKind.ModuleDeclaration && - (nodeForStartPos.parent.parent as ts.ModuleDeclaration).body === nodeForStartPos.parent) { - // Use parent module declarations name for start pos - nodeForStartPos = (nodeForStartPos.parent.parent as ts.ModuleDeclaration).name; - } - else { - // We have to use this name for start pos - break; - } - } - else { - // Is not a member expression so we have found the node for start pos - break; - } - } + const sourceFile = getValidSourceFile(fileName); - return ts.createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd()); - } + return ts.SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); + } - function getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan | undefined { - // doesn't use compiler - no need to synchronize with host - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + /// Syntactic features + function getNonBoundSourceFile(fileName: string): ts.SourceFile { + return syntaxTreeCache.getCurrentSourceFile(fileName); + } - return ts.BreakpointResolver.spanInSourceFileAtLocation(sourceFile, position); - } + function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): ts.TextSpan | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - function getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { - return ts.NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); - } + // Get node at the location + const node = ts.getTouchingPropertyName(sourceFile, startPos); - function getNavigationTree(fileName: string): ts.NavigationTree { - return ts.NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); + if (node === sourceFile) { + return undefined; } - function getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[]; - function getSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[] { - synchronizeHostData(); + switch (node.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.QualifiedName: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.ThisType: + case ts.SyntaxKind.Identifier: + break; - const responseFormat = format || ts.SemanticClassificationFormat.Original; - if (responseFormat === ts.SemanticClassificationFormat.TwentyTwenty) { - return ts.classifier.v2020.getSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span); - } - else { - return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); - } + // Cant create the text span + default: + return undefined; } - function getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.Classifications { - synchronizeHostData(); - - const responseFormat = format || ts.SemanticClassificationFormat.Original; - if (responseFormat === ts.SemanticClassificationFormat.Original) { - return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); + let nodeForStartPos = node; + while (true) { + if (ts.isRightSideOfPropertyAccess(nodeForStartPos) || ts.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 (ts.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 === ts.SyntaxKind.ModuleDeclaration && + (nodeForStartPos.parent.parent as ts.ModuleDeclaration).body === nodeForStartPos.parent) { + // Use parent module declarations name for start pos + nodeForStartPos = (nodeForStartPos.parent.parent as ts.ModuleDeclaration).name; + } + else { + // We have to use this name for start pos + break; + } } else { - return ts.classifier.v2020.getEncodedSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span); + // Is not a member expression so we have found the node for start pos + break; } } - function getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - // doesn't use compiler - no need to synchronize with host - return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); - } + return ts.createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd()); + } - function getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - // doesn't use compiler - no need to synchronize with host - return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); - } + function getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan | undefined { + // doesn't use compiler - no need to synchronize with host + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - function getOutliningSpans(fileName: string): ts.OutliningSpan[] { - // doesn't use compiler - no need to synchronize with host - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return ts.OutliningElementsCollector.collectElements(sourceFile, cancellationToken); - } - const braceMatching = new ts.Map(ts.getEntries({ - [ts.SyntaxKind.OpenBraceToken]: ts.SyntaxKind.CloseBraceToken, - [ts.SyntaxKind.OpenParenToken]: ts.SyntaxKind.CloseParenToken, - [ts.SyntaxKind.OpenBracketToken]: ts.SyntaxKind.CloseBracketToken, - [ts.SyntaxKind.GreaterThanToken]: ts.SyntaxKind.LessThanToken, - })); - braceMatching.forEach((value, key) => braceMatching.set(value.toString(), Number(key) as ts.SyntaxKind)); - function getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const token = ts.getTouchingToken(sourceFile, position); - const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined; - const match = matchKind && ts.findChildOfKind(token.parent, matchKind, sourceFile); - // We want to order the braces when we return the result. - return match ? [ts.createTextSpanFromNode(token, sourceFile), ts.createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : ts.emptyArray; - } + return ts.BreakpointResolver.spanInSourceFileAtLocation(sourceFile, position); + } - function getIndentationAtPosition(fileName: string, position: number, editorOptions: ts.EditorOptions | ts.EditorSettings) { - let start = ts.timestamp(); - const settings = toEditorSettings(editorOptions); - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - log("getIndentationAtPosition: getCurrentSourceFile: " + (ts.timestamp() - start)); - start = ts.timestamp(); - const result = ts.formatting.SmartIndenter.getIndentation(position, sourceFile, settings); - log("getIndentationAtPosition: computeIndentation : " + (ts.timestamp() - start)); + function getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { + return ts.NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); + } - return result; - } + function getNavigationTree(fileName: string): ts.NavigationTree { + return ts.NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); + } - function getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return ts.formatting.formatSelection(start, end, sourceFile, ts.formatting.getFormatContext(toEditorSettings(options), host)); - } + function getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[]; + function getSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[] { + synchronizeHostData(); - function getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { - return ts.formatting.formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), ts.formatting.getFormatContext(toEditorSettings(options), host)); + const responseFormat = format || ts.SemanticClassificationFormat.Original; + if (responseFormat === ts.SemanticClassificationFormat.TwentyTwenty) { + return ts.classifier.v2020.getSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span); + } + else { + return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); } + } - function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const formatContext = ts.formatting.getFormatContext(toEditorSettings(options), host); - if (!ts.isInComment(sourceFile, position)) { - switch (key) { - case "{": - return ts.formatting.formatOnOpeningCurly(position, sourceFile, formatContext); - case "}": - return ts.formatting.formatOnClosingCurly(position, sourceFile, formatContext); - case ";": - return ts.formatting.formatOnSemicolon(position, sourceFile, formatContext); - case "\n": - return ts.formatting.formatOnEnter(position, sourceFile, formatContext); - } - } + function getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.Classifications { + synchronizeHostData(); - return []; + const responseFormat = format || ts.SemanticClassificationFormat.Original; + if (responseFormat === ts.SemanticClassificationFormat.Original) { + return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); } - - function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.CodeFixAction[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const span = ts.createTextSpanFromBounds(start, end); - const formatContext = ts.formatting.getFormatContext(formatOptions, host); - return ts.flatMap(ts.deduplicate(errorCodes, ts.equateValues, ts.compareValues), errorCode => { - cancellationToken.throwIfCancellationRequested(); - return ts.codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences }); - }); + else { + return ts.classifier.v2020.getEncodedSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span); } + } - function getCombinedCodeFix(scope: ts.CombinedCodeFixScope, fixId: {}, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): ts.CombinedCodeActions { - synchronizeHostData(); - ts.Debug.assert(scope.type === "file"); - const sourceFile = getValidSourceFile(scope.fileName); - const formatContext = ts.formatting.getFormatContext(formatOptions, host); - return ts.codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); - } - - function organizeImports(args: ts.OrganizeImportsArgs, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.FileTextChanges[] { - synchronizeHostData(); - ts.Debug.assert(args.type === "file"); - const sourceFile = getValidSourceFile(args.fileName); - const formatContext = ts.formatting.getFormatContext(formatOptions, host); - return ts.OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences, args.skipDestructiveCodeActions); - } - function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.FileTextChanges[] { - return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, ts.formatting.getFormatContext(formatOptions, host), preferences, sourceMapper); - } - function applyCodeActionCommand(action: ts.CodeActionCommand, formatSettings?: ts.FormatCodeSettings): Promise; - function applyCodeActionCommand(action: ts.CodeActionCommand[], formatSettings?: ts.FormatCodeSettings): Promise; - function applyCodeActionCommand(action: ts.CodeActionCommand | ts.CodeActionCommand[], formatSettings?: ts.FormatCodeSettings): Promise; - function applyCodeActionCommand(fileName: ts.Path, action: ts.CodeActionCommand): Promise; - function applyCodeActionCommand(fileName: ts.Path, action: ts.CodeActionCommand[]): Promise; - function applyCodeActionCommand(fileName: ts.Path | ts.CodeActionCommand | ts.CodeActionCommand[], actionOrFormatSettingsOrUndefined?: ts.CodeActionCommand | ts.CodeActionCommand[] | ts.FormatCodeSettings): Promise { - const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as ts.CodeActionCommand | ts.CodeActionCommand[] : fileName as ts.CodeActionCommand[]; - return ts.isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action); - } - function applySingleCodeActionCommand(action: ts.CodeActionCommand): Promise { - const getPath = (path: string): ts.Path => ts.toPath(path, currentDirectory, getCanonicalFileName); - ts.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, options?: ts.DocCommentTemplateOptions): ts.TextInsertion | undefined { - return ts.JsDoc.getDocCommentTemplateAtPosition(ts.getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position, options); - } - - 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 === ts.CharacterCodes.lessThan) { - return false; - } + function getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + // doesn't use compiler - no need to synchronize with host + return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); + } - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + function getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + // doesn't use compiler - no need to synchronize with host + return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); + } - // Check if in a context where we don't want to perform any insertion - if (ts.isInString(sourceFile, position)) { - return false; - } + function getOutliningSpans(fileName: string): ts.OutliningSpan[] { + // doesn't use compiler - no need to synchronize with host + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + return ts.OutliningElementsCollector.collectElements(sourceFile, cancellationToken); + } + const braceMatching = new ts.Map(ts.getEntries({ + [ts.SyntaxKind.OpenBraceToken]: ts.SyntaxKind.CloseBraceToken, + [ts.SyntaxKind.OpenParenToken]: ts.SyntaxKind.CloseParenToken, + [ts.SyntaxKind.OpenBracketToken]: ts.SyntaxKind.CloseBracketToken, + [ts.SyntaxKind.GreaterThanToken]: ts.SyntaxKind.LessThanToken, + })); + braceMatching.forEach((value, key) => braceMatching.set(value.toString(), Number(key) as ts.SyntaxKind)); + function getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const token = ts.getTouchingToken(sourceFile, position); + const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined; + const match = matchKind && ts.findChildOfKind(token.parent, matchKind, sourceFile); + // We want to order the braces when we return the result. + return match ? [ts.createTextSpanFromNode(token, sourceFile), ts.createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : ts.emptyArray; + } - if (ts.isInsideJsxElementOrAttribute(sourceFile, position)) { - return openingBrace === ts.CharacterCodes.openBrace; - } + function getIndentationAtPosition(fileName: string, position: number, editorOptions: ts.EditorOptions | ts.EditorSettings) { + let start = ts.timestamp(); + const settings = toEditorSettings(editorOptions); + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + log("getIndentationAtPosition: getCurrentSourceFile: " + (ts.timestamp() - start)); + start = ts.timestamp(); + const result = ts.formatting.SmartIndenter.getIndentation(position, sourceFile, settings); + log("getIndentationAtPosition: computeIndentation : " + (ts.timestamp() - start)); - if (ts.isInTemplateString(sourceFile, position)) { - return false; - } + return result; + } - switch (openingBrace) { - case ts.CharacterCodes.singleQuote: - case ts.CharacterCodes.doubleQuote: - case ts.CharacterCodes.backtick: - return !ts.isInComment(sourceFile, position); - } + function getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + return ts.formatting.formatSelection(start, end, sourceFile, ts.formatting.getFormatContext(toEditorSettings(options), host)); + } - return true; - } + function getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { + return ts.formatting.formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), ts.formatting.getFormatContext(toEditorSettings(options), host)); + } - function getJsxClosingTagAtPosition(fileName: string, position: number): ts.JsxClosingTagInfo | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const token = ts.findPrecedingToken(position, sourceFile); - if (!token) - return undefined; - const element = token.kind === ts.SyntaxKind.GreaterThanToken && ts.isJsxOpeningElement(token.parent) ? token.parent.parent - : ts.isJsxText(token) && ts.isJsxElement(token.parent) ? token.parent : undefined; - if (element && isUnclosedTag(element)) { - return { newText: `` }; + function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const formatContext = ts.formatting.getFormatContext(toEditorSettings(options), host); + if (!ts.isInComment(sourceFile, position)) { + switch (key) { + case "{": + return ts.formatting.formatOnOpeningCurly(position, sourceFile, formatContext); + case "}": + return ts.formatting.formatOnClosingCurly(position, sourceFile, formatContext); + case ";": + return ts.formatting.formatOnSemicolon(position, sourceFile, formatContext); + case "\n": + return ts.formatting.formatOnEnter(position, sourceFile, formatContext); } - const fragment = token.kind === ts.SyntaxKind.GreaterThanToken && ts.isJsxOpeningFragment(token.parent) ? token.parent.parent - : ts.isJsxText(token) && ts.isJsxFragment(token.parent) ? token.parent : undefined; - if (fragment && isUnclosedFragment(fragment)) { - return { newText: "" }; - } - } - - function getLinesForRange(sourceFile: ts.SourceFile, textRange: ts.TextRange) { - return { - lineStarts: sourceFile.getLineStarts(), - firstLine: sourceFile.getLineAndCharacterOfPosition(textRange.pos).line, - lastLine: sourceFile.getLineAndCharacterOfPosition(textRange.end).line - }; } - function toggleLineComment(fileName: string, textRange: ts.TextRange, insertComment?: boolean): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const textChanges: ts.TextChange[] = []; - const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); - - let isCommenting = insertComment || false; - let leftMostPosition = Number.MAX_VALUE; - const lineTextStarts = new ts.Map(); - const firstNonWhitespaceCharacterRegex = new RegExp(/\S/); - const isJsx = ts.isInsideJsxElement(sourceFile, lineStarts[firstLine]); - const openComment = isJsx ? "{/*" : "//"; + return []; + } - // Check each line before any text changes. - for (let i = firstLine; i <= lastLine; i++) { - const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.CodeFixAction[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = ts.createTextSpanFromBounds(start, end); + const formatContext = ts.formatting.getFormatContext(formatOptions, host); + return ts.flatMap(ts.deduplicate(errorCodes, ts.equateValues, ts.compareValues), errorCode => { + cancellationToken.throwIfCancellationRequested(); + return ts.codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences }); + }); + } - // Find the start of text and the left-most character. No-op on empty lines. - const regExec = firstNonWhitespaceCharacterRegex.exec(lineText); - if (regExec) { - leftMostPosition = Math.min(leftMostPosition, regExec.index); - lineTextStarts.set(i.toString(), regExec.index); + function getCombinedCodeFix(scope: ts.CombinedCodeFixScope, fixId: {}, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): ts.CombinedCodeActions { + synchronizeHostData(); + ts.Debug.assert(scope.type === "file"); + const sourceFile = getValidSourceFile(scope.fileName); + const formatContext = ts.formatting.getFormatContext(formatOptions, host); + return ts.codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); + } - if (lineText.substr(regExec.index, openComment.length) !== openComment) { - isCommenting = insertComment === undefined || insertComment; - } - } - } + function organizeImports(args: ts.OrganizeImportsArgs, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.FileTextChanges[] { + synchronizeHostData(); + ts.Debug.assert(args.type === "file"); + const sourceFile = getValidSourceFile(args.fileName); + const formatContext = ts.formatting.getFormatContext(formatOptions, host); + return ts.OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences, args.skipDestructiveCodeActions); + } + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences = ts.emptyOptions): readonly ts.FileTextChanges[] { + return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, ts.formatting.getFormatContext(formatOptions, host), preferences, sourceMapper); + } + function applyCodeActionCommand(action: ts.CodeActionCommand, formatSettings?: ts.FormatCodeSettings): Promise; + function applyCodeActionCommand(action: ts.CodeActionCommand[], formatSettings?: ts.FormatCodeSettings): Promise; + function applyCodeActionCommand(action: ts.CodeActionCommand | ts.CodeActionCommand[], formatSettings?: ts.FormatCodeSettings): Promise; + function applyCodeActionCommand(fileName: ts.Path, action: ts.CodeActionCommand): Promise; + function applyCodeActionCommand(fileName: ts.Path, action: ts.CodeActionCommand[]): Promise; + function applyCodeActionCommand(fileName: ts.Path | ts.CodeActionCommand | ts.CodeActionCommand[], actionOrFormatSettingsOrUndefined?: ts.CodeActionCommand | ts.CodeActionCommand[] | ts.FormatCodeSettings): Promise { + const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as ts.CodeActionCommand | ts.CodeActionCommand[] : fileName as ts.CodeActionCommand[]; + return ts.isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action); + } + function applySingleCodeActionCommand(action: ts.CodeActionCommand): Promise { + const getPath = (path: string): ts.Path => ts.toPath(path, currentDirectory, getCanonicalFileName); + ts.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`"); + } - // Push all text changes. - for (let i = firstLine; i <= lastLine; i++) { - // If the range is multiline and ends on a beginning of a line, don't comment/uncomment. - if (firstLine !== lastLine && lineStarts[i] === textRange.end) { - continue; - } + function getDocCommentTemplateAtPosition(fileName: string, position: number, options?: ts.DocCommentTemplateOptions): ts.TextInsertion | undefined { + return ts.JsDoc.getDocCommentTemplateAtPosition(ts.getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position, options); + } - const lineTextStart = lineTextStarts.get(i.toString()); + 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 === ts.CharacterCodes.lessThan) { + return false; + } - // If the line is not an empty line; otherwise no-op. - if (lineTextStart !== undefined) { - if (isJsx) { - textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }, isCommenting, isJsx)); - } - else if (isCommenting) { - textChanges.push({ - newText: openComment, - span: { - length: 0, - start: lineStarts[i] + leftMostPosition - } - }); - } - else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { - textChanges.push({ - newText: "", - span: { - length: openComment.length, - start: lineStarts[i] + lineTextStart - } - }); - } - } - } + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return textChanges; + // Check if in a context where we don't want to perform any insertion + if (ts.isInString(sourceFile, position)) { + return false; } - function toggleMultilineComment(fileName: string, textRange: ts.TextRange, insertComment?: boolean, isInsideJsx?: boolean): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const textChanges: ts.TextChange[] = []; - const { text } = sourceFile; - - let hasComment = false; - let isCommenting = insertComment || false; - const positions = [] as number[] as ts.SortedArray; + if (ts.isInsideJsxElementOrAttribute(sourceFile, position)) { + return openingBrace === ts.CharacterCodes.openBrace; + } - let { pos } = textRange; - const isJsx = isInsideJsx !== undefined ? isInsideJsx : ts.isInsideJsxElement(sourceFile, pos); + if (ts.isInTemplateString(sourceFile, position)) { + return false; + } - const openMultiline = isJsx ? "{/*" : "/*"; - const closeMultiline = isJsx ? "*/}" : "*/"; - const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*"; - const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/"; + switch (openingBrace) { + case ts.CharacterCodes.singleQuote: + case ts.CharacterCodes.doubleQuote: + case ts.CharacterCodes.backtick: + return !ts.isInComment(sourceFile, position); + } - // Get all comment positions - while (pos <= textRange.end) { - // Start of comment is considered inside comment. - const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0; - const commentRange = ts.isInComment(sourceFile, pos + offset); + return true; + } - // If position is in a comment add it to the positions array. - if (commentRange) { - // Comment range doesn't include the brace character. Increase it to include them. - if (isJsx) { - commentRange.pos--; - commentRange.end++; - } + function getJsxClosingTagAtPosition(fileName: string, position: number): ts.JsxClosingTagInfo | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const token = ts.findPrecedingToken(position, sourceFile); + if (!token) + return undefined; + const element = token.kind === ts.SyntaxKind.GreaterThanToken && ts.isJsxOpeningElement(token.parent) ? token.parent.parent + : ts.isJsxText(token) && ts.isJsxElement(token.parent) ? token.parent : undefined; + if (element && isUnclosedTag(element)) { + return { newText: `` }; + } + const fragment = token.kind === ts.SyntaxKind.GreaterThanToken && ts.isJsxOpeningFragment(token.parent) ? token.parent.parent + : ts.isJsxText(token) && ts.isJsxFragment(token.parent) ? token.parent : undefined; + if (fragment && isUnclosedFragment(fragment)) { + return { newText: "" }; + } + } - positions.push(commentRange.pos); - if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - positions.push(commentRange.end); - } + function getLinesForRange(sourceFile: ts.SourceFile, textRange: ts.TextRange) { + return { + lineStarts: sourceFile.getLineStarts(), + firstLine: sourceFile.getLineAndCharacterOfPosition(textRange.pos).line, + lastLine: sourceFile.getLineAndCharacterOfPosition(textRange.end).line + }; + } - hasComment = true; - pos = commentRange.end + 1; + function toggleLineComment(fileName: string, textRange: ts.TextRange, insertComment?: boolean): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: ts.TextChange[] = []; + const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); + + let isCommenting = insertComment || false; + let leftMostPosition = Number.MAX_VALUE; + const lineTextStarts = new ts.Map(); + const firstNonWhitespaceCharacterRegex = new RegExp(/\S/); + const isJsx = ts.isInsideJsxElement(sourceFile, lineStarts[firstLine]); + const openComment = isJsx ? "{/*" : "//"; + + // Check each line before any text changes. + for (let i = firstLine; i <= lastLine; i++) { + const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); + + // Find the start of text and the left-most character. No-op on empty lines. + const regExec = firstNonWhitespaceCharacterRegex.exec(lineText); + if (regExec) { + leftMostPosition = Math.min(leftMostPosition, regExec.index); + lineTextStarts.set(i.toString(), regExec.index); + + if (lineText.substr(regExec.index, openComment.length) !== openComment) { + isCommenting = insertComment === undefined || insertComment; } - else { // If it's not in a comment range, then we need to comment the uncommented portions. - const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + } + } - isCommenting = insertComment !== undefined - ? insertComment - : isCommenting || !ts.isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); // If isCommenting is already true we don't need to check whitespace again. - pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; - } + // Push all text changes. + for (let i = firstLine; i <= lastLine; i++) { + // If the range is multiline and ends on a beginning of a line, don't comment/uncomment. + if (firstLine !== lastLine && lineStarts[i] === textRange.end) { + continue; } - // If it didn't found a comment and isCommenting is false means is only empty space. - // We want to insert comment in this scenario. - if (isCommenting || !hasComment) { - if (ts.isInComment(sourceFile, textRange.pos)?.kind !== ts.SyntaxKind.SingleLineCommentTrivia) { - ts.insertSorted(positions, textRange.pos, ts.compareValues); - } - ts.insertSorted(positions, textRange.end, ts.compareValues); + const lineTextStart = lineTextStarts.get(i.toString()); - // Insert open comment if the first position is not a comment already. - const firstPos = positions[0]; - if (text.substr(firstPos, openMultiline.length) !== openMultiline) { + // If the line is not an empty line; otherwise no-op. + if (lineTextStart !== undefined) { + if (isJsx) { + textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }, isCommenting, isJsx)); + } + else if (isCommenting) { textChanges.push({ - newText: openMultiline, + newText: openComment, span: { length: 0, - start: firstPos + start: lineStarts[i] + leftMostPosition + } + }); + } + else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { + textChanges.push({ + newText: "", + span: { + length: openComment.length, + start: lineStarts[i] + lineTextStart } }); } + } + } - // Insert open and close comment to all positions between first and last. Exclusive. - for (let i = 1; i < positions.length - 1; i++) { - if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) { - textChanges.push({ - newText: closeMultiline, - span: { - length: 0, - start: positions[i] - } - }); - } + return textChanges; + } - if (text.substr(positions[i], openMultiline.length) !== openMultiline) { - textChanges.push({ - newText: openMultiline, - span: { - length: 0, - start: positions[i] - } - }); - } + function toggleMultilineComment(fileName: string, textRange: ts.TextRange, insertComment?: boolean, isInsideJsx?: boolean): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: ts.TextChange[] = []; + const { text } = sourceFile; + + let hasComment = false; + let isCommenting = insertComment || false; + const positions = [] as number[] as ts.SortedArray; + + let { pos } = textRange; + const isJsx = isInsideJsx !== undefined ? isInsideJsx : ts.isInsideJsxElement(sourceFile, pos); + + const openMultiline = isJsx ? "{/*" : "/*"; + const closeMultiline = isJsx ? "*/}" : "*/"; + const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*"; + const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/"; + + // Get all comment positions + while (pos <= textRange.end) { + // Start of comment is considered inside comment. + const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0; + const commentRange = ts.isInComment(sourceFile, pos + offset); + + // If position is in a comment add it to the positions array. + if (commentRange) { + // Comment range doesn't include the brace character. Increase it to include them. + if (isJsx) { + commentRange.pos--; + commentRange.end++; } - // Insert open comment if the last position is not a comment already. - if (textChanges.length % 2 !== 0) { + positions.push(commentRange.pos); + if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + positions.push(commentRange.end); + } + + hasComment = true; + pos = commentRange.end + 1; + } + else { // If it's not in a comment range, then we need to comment the uncommented portions. + const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + + isCommenting = insertComment !== undefined + ? insertComment + : isCommenting || !ts.isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); // If isCommenting is already true we don't need to check whitespace again. + pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; + } + } + + // If it didn't found a comment and isCommenting is false means is only empty space. + // We want to insert comment in this scenario. + if (isCommenting || !hasComment) { + if (ts.isInComment(sourceFile, textRange.pos)?.kind !== ts.SyntaxKind.SingleLineCommentTrivia) { + ts.insertSorted(positions, textRange.pos, ts.compareValues); + } + ts.insertSorted(positions, textRange.end, ts.compareValues); + + // Insert open comment if the first position is not a comment already. + const firstPos = positions[0]; + if (text.substr(firstPos, openMultiline.length) !== openMultiline) { + textChanges.push({ + newText: openMultiline, + span: { + length: 0, + start: firstPos + } + }); + } + + // Insert open and close comment to all positions between first and last. Exclusive. + for (let i = 1; i < positions.length - 1; i++) { + if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) { textChanges.push({ newText: closeMultiline, span: { length: 0, - start: positions[positions.length - 1] + start: positions[i] } }); } - } - else { - // If is not commenting then remove all comments found. - for (const pos of positions) { - const from = pos - closeMultiline.length > 0 ? pos - closeMultiline.length : 0; - const offset = text.substr(from, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + + if (text.substr(positions[i], openMultiline.length) !== openMultiline) { textChanges.push({ - newText: "", + newText: openMultiline, span: { - length: openMultiline.length, - start: pos - offset + length: 0, + start: positions[i] } }); } } - return textChanges; + // Insert open comment if the last position is not a comment already. + if (textChanges.length % 2 !== 0) { + textChanges.push({ + newText: closeMultiline, + span: { + length: 0, + start: positions[positions.length - 1] + } + }); + } } - - function commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const { firstLine, lastLine } = getLinesForRange(sourceFile, textRange); - - // If there is a selection that is on the same line, add multiline. - return firstLine === lastLine && textRange.pos !== textRange.end - ? toggleMultilineComment(fileName, textRange, /*insertComment*/ true) - : toggleLineComment(fileName, textRange, /*insertComment*/ true); + else { + // If is not commenting then remove all comments found. + for (const pos of positions) { + const from = pos - closeMultiline.length > 0 ? pos - closeMultiline.length : 0; + const offset = text.substr(from, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + textChanges.push({ + newText: "", + span: { + length: openMultiline.length, + start: pos - offset + } + }); + } } - function uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const textChanges: ts.TextChange[] = []; - const { pos } = textRange; - let { end } = textRange; - - // If cursor is not a selection we need to increase the end position - // to include the start of the comment. - if (pos === end) { - end += ts.isInsideJsxElement(sourceFile, pos) ? 2 : 1; - } + return textChanges; + } - for (let i = pos; i <= end; i++) { - const commentRange = ts.isInComment(sourceFile, i); - if (commentRange) { - switch (commentRange.kind) { - case ts.SyntaxKind.SingleLineCommentTrivia: - textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); - break; - case ts.SyntaxKind.MultiLineCommentTrivia: - textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); - } + function commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const { firstLine, lastLine } = getLinesForRange(sourceFile, textRange); - i = commentRange.end + 1; - } - } + // If there is a selection that is on the same line, add multiline. + return firstLine === lastLine && textRange.pos !== textRange.end + ? toggleMultilineComment(fileName, textRange, /*insertComment*/ true) + : toggleLineComment(fileName, textRange, /*insertComment*/ true); + } - return textChanges; - } + function uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: ts.TextChange[] = []; + const { pos } = textRange; + let { end } = textRange; - function isUnclosedTag({ openingElement, closingElement, parent }: ts.JsxElement): boolean { - return !ts.tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || - ts.isJsxElement(parent) && ts.tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); + // If cursor is not a selection we need to increase the end position + // to include the start of the comment. + if (pos === end) { + end += ts.isInsideJsxElement(sourceFile, pos) ? 2 : 1; } - function isUnclosedFragment({ closingFragment, parent }: ts.JsxFragment): boolean { - return !!(closingFragment.flags & ts.NodeFlags.ThisNodeHasError) || (ts.isJsxFragment(parent) && isUnclosedFragment(parent)); - } + for (let i = pos; i <= end; i++) { + const commentRange = ts.isInComment(sourceFile, i); + if (commentRange) { + switch (commentRange.kind) { + case ts.SyntaxKind.SingleLineCommentTrivia: + textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); + break; + case ts.SyntaxKind.MultiLineCommentTrivia: + textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); + } - function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const range = ts.formatting.getRangeOfEnclosingComment(sourceFile, position); - return range && (!onlyMultiLine || range.kind === ts.SyntaxKind.MultiLineCommentTrivia) ? ts.createTextSpanFromRange(range) : undefined; + i = commentRange.end + 1; + } } - function getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.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: ts.TodoComment[] = []; + return textChanges; + } - // 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(); + function isUnclosedTag({ openingElement, closingElement, parent }: ts.JsxElement): boolean { + return !ts.tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || + ts.isJsxElement(parent) && ts.tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); + } - let matchArray: RegExpExecArray | null; - while (matchArray = regExp.exec(fileContents)) { - cancellationToken.throwIfCancellationRequested(); + function isUnclosedFragment({ closingFragment, parent }: ts.JsxFragment): boolean { + return !!(closingFragment.flags & ts.NodeFlags.ThisNodeHasError) || (ts.isJsxFragment(parent) && isUnclosedFragment(parent)); + } - // 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; - ts.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 (!ts.isInComment(sourceFile, matchPosition)) { - continue; - } + function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const range = ts.formatting.getRangeOfEnclosingComment(sourceFile, position); + return range && (!onlyMultiLine || range.kind === ts.SyntaxKind.MultiLineCommentTrivia) ? ts.createTextSpanFromRange(range) : undefined; + } - let descriptor: ts.TodoCommentDescriptor | undefined; - for (let i = 0; i < descriptors.length; i++) { - if (matchArray[i + firstDescriptorCaptureIndex]) { - descriptor = descriptors[i]; - } - } - if (descriptor === undefined) - return ts.Debug.fail(); + function getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.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(); - // 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 sourceFile = getValidSourceFile(fileName); - const message = matchArray[2]; - result.push({ descriptor, message, position: matchPosition }); - } - } + cancellationToken.throwIfCancellationRequested(); - return result; + const fileContents = sourceFile.text; + const result: ts.TodoComment[] = []; - function escapeRegExp(str: string): string { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - } + // 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(); - 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. + let matchArray: RegExpExecArray | null; + while (matchArray = regExp.exec(fileContents)) { + cancellationToken.throwIfCancellationRequested(); - // TODO comments can appear in one of the following forms: - // - // 1) // TODO or /////////// TODO - // - // 2) /* TODO or /********** TODO + // If we got a match, here is what the match array will look like. Say the source text is: // - // 3) /* - // * TODO - // */ + // " // hack 1" // - // 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: + // The result array with the regexp: will be: // - // (?:(TODO\(jason\))|(HACK)) + // ["// hack 1", "// ", "hack 1", undefined, "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 = "(?:" + ts.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. + // 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' 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"); - } + // i.e. 'undefined' in position 3 above means TODO(jason) didn't match. + // "hack" in position 4 means HACK did match. + const firstDescriptorCaptureIndex = 3; + ts.Debug.assert(matchArray.length === descriptors.length + firstDescriptorCaptureIndex); - function isLetterOrDigit(char: number): boolean { - return (char >= ts.CharacterCodes.a && char <= ts.CharacterCodes.z) || - (char >= ts.CharacterCodes.A && char <= ts.CharacterCodes.Z) || - (char >= ts.CharacterCodes._0 && char <= ts.CharacterCodes._9); - } + 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 (!ts.isInComment(sourceFile, matchPosition)) { + continue; + } + + let descriptor: ts.TodoCommentDescriptor | undefined; + for (let i = 0; i < descriptors.length; i++) { + if (matchArray[i + firstDescriptorCaptureIndex]) { + descriptor = descriptors[i]; + } + } + if (descriptor === undefined) + return ts.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; + } - function isNodeModulesFile(path: string): boolean { - return ts.stringContains(path, "/node_modules/"); + const message = matchArray[2]; + result.push({ descriptor, message, position: matchPosition }); } } - function getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { - synchronizeHostData(); - return ts.Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); - } - - function getRefactorContext(file: ts.SourceFile, positionOrRange: number | ts.TextRange, preferences: ts.UserPreferences, formatOptions?: ts.FormatCodeSettings, triggerReason?: ts.RefactorTriggerReason, kind?: string): ts.RefactorContext { - const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; - return { - file, - startPosition, - endPosition, - program: getProgram()!, - host, - formatContext: ts.formatting.getFormatContext(formatOptions!, host), - cancellationToken, - preferences, - triggerReason, - kind - }; - } + return result; - function getInlayHintsContext(file: ts.SourceFile, span: ts.TextSpan, preferences: ts.UserPreferences): ts.InlayHintsContext { - return { - file, - program: getProgram()!, - host, - span, - preferences, - cancellationToken, - }; + function escapeRegExp(str: string): string { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } - function getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { - return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + 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 = "(?:" + ts.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 getApplicableRefactors(fileName: string, positionOrRange: number | ts.TextRange, preferences: ts.UserPreferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind: string): ts.ApplicableRefactorInfo[] { - synchronizeHostData(); - const file = getValidSourceFile(fileName); - return ts.refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, ts.emptyOptions, triggerReason, kind)); + function isLetterOrDigit(char: number): boolean { + return (char >= ts.CharacterCodes.a && char <= ts.CharacterCodes.z) || + (char >= ts.CharacterCodes.A && char <= ts.CharacterCodes.Z) || + (char >= ts.CharacterCodes._0 && char <= ts.CharacterCodes._9); } - function getEditsForRefactor(fileName: string, formatOptions: ts.FormatCodeSettings, positionOrRange: number | ts.TextRange, refactorName: string, actionName: string, preferences: ts.UserPreferences = ts.emptyOptions): ts.RefactorEditInfo | undefined { - synchronizeHostData(); - const file = getValidSourceFile(fileName); - return ts.refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); + + function isNodeModulesFile(path: string): boolean { + return ts.stringContains(path, "/node_modules/"); } + } - function toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter { - // Go to Definition supports returning a zero-length span at position 0 for - // non-existent files. We need to special-case the conversion of position 0 - // to avoid a crash trying to get the text for that file, since this function - // otherwise assumes that 'fileName' is the name of a file that exists. - if (position === 0) { - return { line: 0, character: 0 }; - } - return sourceMapper.toLineColumnOffset(fileName, position); - } - - function prepareCallHierarchy(fileName: string, position: number): ts.CallHierarchyItem | ts.CallHierarchyItem[] | undefined { - synchronizeHostData(); - const declarations = ts.CallHierarchy.resolveCallHierarchyDeclaration(program, ts.getTouchingPropertyName(getValidSourceFile(fileName), position)); - return declarations && ts.mapOneOrMany(declarations, declaration => ts.CallHierarchy.createCallHierarchyItem(program, declaration)); - } - - function provideCallHierarchyIncomingCalls(fileName: string, position: number): ts.CallHierarchyIncomingCall[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const declaration = ts.firstOrOnly(ts.CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : ts.getTouchingPropertyName(sourceFile, position))); - return declaration ? ts.CallHierarchy.getIncomingCalls(program, declaration, cancellationToken) : []; - } - - function provideCallHierarchyOutgoingCalls(fileName: string, position: number): ts.CallHierarchyOutgoingCall[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const declaration = ts.firstOrOnly(ts.CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : ts.getTouchingPropertyName(sourceFile, position))); - return declaration ? ts.CallHierarchy.getOutgoingCalls(program, declaration) : []; - } - - function provideInlayHints(fileName: string, span: ts.TextSpan, preferences: ts.UserPreferences = ts.emptyOptions): ts.InlayHint[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - return ts.InlayHints.provideInlayHints(getInlayHintsContext(sourceFile, span, preferences)); - } - - const ls: ts.LanguageService = { - dispose, - cleanupSemanticCache, - getSyntacticDiagnostics, - getSemanticDiagnostics, - getSuggestionDiagnostics, - getCompilerOptionsDiagnostics, - getSyntacticClassifications, - getSemanticClassifications, - getEncodedSyntacticClassifications, - getEncodedSemanticClassifications, - getCompletionsAtPosition, - getCompletionEntryDetails, - getCompletionEntrySymbol, - getSignatureHelpItems, - getQuickInfoAtPosition, - getDefinitionAtPosition, - getDefinitionAndBoundSpan, - getImplementationAtPosition, - getTypeDefinitionAtPosition, - getReferencesAtPosition, - findReferences, - getFileReferences, - 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, - getAutoImportProvider, - getApplicableRefactors, - getEditsForRefactor, - toLineColumnOffset, - getSourceMapper: () => sourceMapper, - clearSourceMapperCache: () => sourceMapper.clearCache(), - prepareCallHierarchy, - provideCallHierarchyIncomingCalls, - provideCallHierarchyOutgoingCalls, - toggleLineComment, - toggleMultilineComment, - commentSelection, - uncommentSelection, - provideInlayHints, + function getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + synchronizeHostData(); + return ts.Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); + } + + function getRefactorContext(file: ts.SourceFile, positionOrRange: number | ts.TextRange, preferences: ts.UserPreferences, formatOptions?: ts.FormatCodeSettings, triggerReason?: ts.RefactorTriggerReason, kind?: string): ts.RefactorContext { + const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; + return { + file, + startPosition, + endPosition, + program: getProgram()!, + host, + formatContext: ts.formatting.getFormatContext(formatOptions!, host), + cancellationToken, + preferences, + triggerReason, + kind }; + } - switch (languageServiceMode) { - case ts.LanguageServiceMode.Semantic: - break; - case ts.LanguageServiceMode.PartialSemantic: - invalidOperationsInPartialSemanticMode.forEach(key => ls[key] = () => { - throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.PartialSemantic`); - }); - break; - case ts.LanguageServiceMode.Syntactic: - invalidOperationsInSyntacticMode.forEach(key => ls[key] = () => { - throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.Syntactic`); - }); - break; - default: - ts.Debug.assertNever(languageServiceMode); - } - return ls; + function getInlayHintsContext(file: ts.SourceFile, span: ts.TextSpan, preferences: ts.UserPreferences): ts.InlayHintsContext { + return { + file, + program: getProgram()!, + host, + span, + preferences, + cancellationToken, + }; } - /* @internal */ - /** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */ - export function getNameTable(sourceFile: ts.SourceFile): ts.UnderscoreEscapedMap { - if (!sourceFile.nameTable) { - initializeNameTable(sourceFile); - } + function getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { + return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } - return sourceFile.nameTable!; // TODO: GH#18217 + function getApplicableRefactors(fileName: string, positionOrRange: number | ts.TextRange, preferences: ts.UserPreferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind: string): ts.ApplicableRefactorInfo[] { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + return ts.refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, ts.emptyOptions, triggerReason, kind)); + } + function getEditsForRefactor(fileName: string, formatOptions: ts.FormatCodeSettings, positionOrRange: number | ts.TextRange, refactorName: string, actionName: string, preferences: ts.UserPreferences = ts.emptyOptions): ts.RefactorEditInfo | undefined { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + return ts.refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); } - function initializeNameTable(sourceFile: ts.SourceFile): void { - const nameTable = sourceFile.nameTable = new ts.Map(); - sourceFile.forEachChild(function walk(node) { - if (ts.isIdentifier(node) && !ts.isTagName(node) && node.escapedText || ts.isStringOrNumericLiteralLike(node) && literalIsName(node)) { - const text = ts.getEscapedTextOfIdentifierOrLiteral(node); - nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); - } - else if (ts.isPrivateIdentifier(node)) { - const text = node.escapedText; - nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); - } + function toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter { + // Go to Definition supports returning a zero-length span at position 0 for + // non-existent files. We need to special-case the conversion of position 0 + // to avoid a crash trying to get the text for that file, since this function + // otherwise assumes that 'fileName' is the name of a file that exists. + if (position === 0) { + return { line: 0, character: 0 }; + } + return sourceMapper.toLineColumnOffset(fileName, position); + } - ts.forEachChild(node, walk); - if (ts.hasJSDocNodes(node)) { - for (const jsDoc of node.jsDoc!) { - ts.forEachChild(jsDoc, walk); - } - } - }); + function prepareCallHierarchy(fileName: string, position: number): ts.CallHierarchyItem | ts.CallHierarchyItem[] | undefined { + synchronizeHostData(); + const declarations = ts.CallHierarchy.resolveCallHierarchyDeclaration(program, ts.getTouchingPropertyName(getValidSourceFile(fileName), position)); + return declarations && ts.mapOneOrMany(declarations, declaration => ts.CallHierarchy.createCallHierarchyItem(program, declaration)); } - /** - * 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: ts.StringLiteralLike | ts.NumericLiteral): boolean { - return ts.isDeclarationName(node) || - node.parent.kind === ts.SyntaxKind.ExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - ts.isLiteralComputedPropertyDeclarationName(node); + function provideCallHierarchyIncomingCalls(fileName: string, position: number): ts.CallHierarchyIncomingCall[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const declaration = ts.firstOrOnly(ts.CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : ts.getTouchingPropertyName(sourceFile, position))); + return declaration ? ts.CallHierarchy.getIncomingCalls(program, declaration, cancellationToken) : []; } - /** - * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } - */ - /* @internal */ - export function getContainingObjectLiteralElement(node: ts.Node): ObjectLiteralElementWithName | undefined { - const element = getContainingObjectLiteralElementWorker(node); - return element && (ts.isObjectLiteralExpression(element.parent) || ts.isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined; + function provideCallHierarchyOutgoingCalls(fileName: string, position: number): ts.CallHierarchyOutgoingCall[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const declaration = ts.firstOrOnly(ts.CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : ts.getTouchingPropertyName(sourceFile, position))); + return declaration ? ts.CallHierarchy.getOutgoingCalls(program, declaration) : []; } - function getContainingObjectLiteralElementWorker(node: ts.Node): ts.ObjectLiteralElement | undefined { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.NumericLiteral: - if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { - return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; - } - // falls through - case ts.SyntaxKind.Identifier: - return ts.isObjectLiteralElement(node.parent) && - (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && - node.parent.name === node ? node.parent : undefined; - } - return undefined; + function provideInlayHints(fileName: string, span: ts.TextSpan, preferences: ts.UserPreferences = ts.emptyOptions): ts.InlayHint[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + return ts.InlayHints.provideInlayHints(getInlayHintsContext(sourceFile, span, preferences)); } - /* @internal */ - export type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { - name: ts.PropertyName; - parent: ts.ObjectLiteralExpression | ts.JsxAttributes; + const ls: ts.LanguageService = { + dispose, + cleanupSemanticCache, + getSyntacticDiagnostics, + getSemanticDiagnostics, + getSuggestionDiagnostics, + getCompilerOptionsDiagnostics, + getSyntacticClassifications, + getSemanticClassifications, + getEncodedSyntacticClassifications, + getEncodedSemanticClassifications, + getCompletionsAtPosition, + getCompletionEntryDetails, + getCompletionEntrySymbol, + getSignatureHelpItems, + getQuickInfoAtPosition, + getDefinitionAtPosition, + getDefinitionAndBoundSpan, + getImplementationAtPosition, + getTypeDefinitionAtPosition, + getReferencesAtPosition, + findReferences, + getFileReferences, + 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, + getAutoImportProvider, + getApplicableRefactors, + getEditsForRefactor, + toLineColumnOffset, + getSourceMapper: () => sourceMapper, + clearSourceMapperCache: () => sourceMapper.clearCache(), + prepareCallHierarchy, + provideCallHierarchyIncomingCalls, + provideCallHierarchyOutgoingCalls, + toggleLineComment, + toggleMultilineComment, + commentSelection, + uncommentSelection, + provideInlayHints, }; - function getSymbolAtLocationForQuickInfo(node: ts.Node, checker: ts.TypeChecker): ts.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 ts.first(properties); - } - } - return checker.getSymbolAtLocation(node); + + switch (languageServiceMode) { + case ts.LanguageServiceMode.Semantic: + break; + case ts.LanguageServiceMode.PartialSemantic: + invalidOperationsInPartialSemanticMode.forEach(key => ls[key] = () => { + throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.PartialSemantic`); + }); + break; + case ts.LanguageServiceMode.Syntactic: + invalidOperationsInSyntacticMode.forEach(key => ls[key] = () => { + throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.Syntactic`); + }); + break; + default: + ts.Debug.assertNever(languageServiceMode); + } + return ls; +} + +/* @internal */ +/** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */ +export function getNameTable(sourceFile: ts.SourceFile): ts.UnderscoreEscapedMap { + if (!sourceFile.nameTable) { + initializeNameTable(sourceFile); } - /** Gets all symbols for one property. Does not get symbols for every property. */ - /* @internal */ - export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: ts.TypeChecker, contextualType: ts.Type, unionSymbolOk: boolean): readonly ts.Symbol[] { - const name = ts.getNameFromPropertyName(node.name); - if (!name) - return ts.emptyArray; - if (!contextualType.isUnion()) { - const symbol = contextualType.getProperty(name); - return symbol ? [symbol] : ts.emptyArray; + return sourceFile.nameTable!; // TODO: GH#18217 +} + +function initializeNameTable(sourceFile: ts.SourceFile): void { + const nameTable = sourceFile.nameTable = new ts.Map(); + sourceFile.forEachChild(function walk(node) { + if (ts.isIdentifier(node) && !ts.isTagName(node) && node.escapedText || ts.isStringOrNumericLiteralLike(node) && literalIsName(node)) { + const text = ts.getEscapedTextOfIdentifierOrLiteral(node); + nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); + } + else if (ts.isPrivateIdentifier(node)) { + const text = node.escapedText; + nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); } - const discriminatedPropertySymbols = ts.mapDefined(contextualType.types, t => (ts.isObjectLiteralExpression(node.parent) || ts.isJsxAttributes(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]; + ts.forEachChild(node, walk); + if (ts.hasJSDocNodes(node)) { + for (const jsDoc of node.jsDoc!) { + ts.forEachChild(jsDoc, walk); + } } - if (discriminatedPropertySymbols.length === 0) { - // Bad discriminant -- do again without discriminating - return ts.mapDefined(contextualType.types, t => t.getProperty(name)); + }); +} + +/** + * 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: ts.StringLiteralLike | ts.NumericLiteral): boolean { + return ts.isDeclarationName(node) || + node.parent.kind === ts.SyntaxKind.ExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + ts.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: ts.Node): ObjectLiteralElementWithName | undefined { + const element = getContainingObjectLiteralElementWorker(node); + return element && (ts.isObjectLiteralExpression(element.parent) || ts.isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined; +} +function getContainingObjectLiteralElementWorker(node: ts.Node): ts.ObjectLiteralElement | undefined { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.NumericLiteral: + if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + + case ts.SyntaxKind.Identifier: + return ts.isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; +} + +/* @internal */ +export type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { + name: ts.PropertyName; + parent: ts.ObjectLiteralExpression | ts.JsxAttributes; +}; +function getSymbolAtLocationForQuickInfo(node: ts.Node, checker: ts.TypeChecker): ts.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 ts.first(properties); } - return discriminatedPropertySymbols; } + return checker.getSymbolAtLocation(node); +} - function isArgumentOfElementAccessExpression(node: ts.Node) { - return node && - node.parent && - node.parent.kind === ts.SyntaxKind.ElementAccessExpression && - (node.parent as ts.ElementAccessExpression).argumentExpression === node; +/** Gets all symbols for one property. Does not get symbols for every property. */ +/* @internal */ +export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: ts.TypeChecker, contextualType: ts.Type, unionSymbolOk: boolean): readonly ts.Symbol[] { + const name = ts.getNameFromPropertyName(node.name); + if (!name) + return ts.emptyArray; + if (!contextualType.isUnion()) { + const symbol = contextualType.getProperty(name); + return symbol ? [symbol] : ts.emptyArray; } - /// getDefaultLibraryFilePath - declare const __dirname: string; + const discriminatedPropertySymbols = ts.mapDefined(contextualType.types, t => (ts.isObjectLiteralExpression(node.parent) || ts.isJsxAttributes(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 ts.mapDefined(contextualType.types, t => t.getProperty(name)); + } + return discriminatedPropertySymbols; +} - /** - * 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: ts.CompilerOptions): string { - // Check __dirname is defined and that we are on a node.js system. - if (typeof __dirname !== "undefined") { - return ts.combinePaths(__dirname, ts.getDefaultLibFileName(options)); - } +function isArgumentOfElementAccessExpression(node: ts.Node) { + return node && + node.parent && + node.parent.kind === ts.SyntaxKind.ElementAccessExpression && + (node.parent as ts.ElementAccessExpression).argumentExpression === node; +} - throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. "); +/// 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: ts.CompilerOptions): string { + // Check __dirname is defined and that we are on a node.js system. + if (typeof __dirname !== "undefined") { + return ts.combinePaths(__dirname, ts.getDefaultLibFileName(options)); } - ts.setObjectAllocator(getServicesObjectAllocator()); + throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. "); +} + +ts.setObjectAllocator(getServicesObjectAllocator()); } diff --git a/src/services/shims.ts b/src/services/shims.ts index 858bce7415abd..1e8659f737c2c 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -25,1166 +25,1166 @@ let debugObjectHost: { /* @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: ts.ESMap; // The map of package names to their cached typing locations and installed versions - typeAcquisition: ts.TypeAcquisition; // Used to customize the type acquisition process - compilerOptions: ts.CompilerOptions; // Used as a source for typing inference - unresolvedImports: readonly string[]; // List of unresolved module ids from imports - typesRegistry: ts.ReadonlyESMap>; // 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): ts.ScriptKind; - getScriptVersion(fileName: string): string; - getScriptSnapshot(fileName: string): ScriptSnapshotShim; - getLocalizedDiagnosticMessages(): string; - getCancellationToken(): ts.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.ESMap; // The map of package names to their cached typing locations and installed versions + typeAcquisition: ts.TypeAcquisition; // Used to customize the type acquisition process + compilerOptions: ts.CompilerOptions; // Used as a source for typing inference + unresolvedImports: readonly string[]; // List of unresolved module ids from imports + typesRegistry: ts.ReadonlyESMap>; // The map of available typings in npm to maps of TS versions to their latest supported versions +} - /** 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: ts.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, format?: ts.SemanticClassificationFormat): string; - getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string; - getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: ts.SemanticClassificationFormat): string; - getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined, formattingSettings: ts.FormatCodeSettings | undefined): string; - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string /*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | 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: ts.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?: ts.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; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[] - */ - getFileReferences(fileName: string): 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 }[] }[] - * - * @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, options?: ts.DocCommentTemplateOptions): 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; - provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences | undefined): string; - getEmitOutput(fileName: string): string; - getEmitOutputObject(fileName: string): ts.EmitOutput; - toggleLineComment(fileName: string, textChange: ts.TextRange): string; - toggleMultilineComment(fileName: string, textChange: ts.TextRange): string; - commentSelection(fileName: string, textChange: ts.TextRange): string; - uncommentSelection(fileName: string, textChange: ts.TextRange): string; - } - - export interface ClassifierShim extends Shim { - getEncodedLexicalClassifications(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent?: boolean): string; - getClassificationsForLine(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent?: boolean): string; - } - - export interface CoreServicesShim extends Shim { - getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string; - getPreProcessedFileInfo(fileName: string, sourceText: ts.IScriptSnapshot): string; - getTSConfigFileInfo(fileName: string, sourceText: ts.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); - } - } +export interface ScriptSnapshotShim { + /** Gets a portion of the script snapshot specified by [start, end). */ + getText(start: number, end: number): string; - class ScriptSnapshotShimAdapter implements ts.IScriptSnapshot { - constructor(private scriptSnapshotShim: ScriptSnapshotShim) { - } + /** Gets the length of this script snapshot. */ + getLength(): number; - public getText(start: number, end: number): string { - return this.scriptSnapshotShim.getText(start, end); - } + /** + * 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; - public getLength(): number { - return this.scriptSnapshotShim.getLength(); - } + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} - public getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange | undefined { - const oldSnapshotShim = oldSnapshot as ScriptSnapshotShimAdapter; - 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 */ +export interface Logger { + log(s: string): void; + trace(s: string): void; + error(s: string): void; +} - const decoded: { - span: { - start: number; - length: number; - }; - newLength: number; - } = JSON.parse(encoded!); // TODO: GH#18217 - return ts.createTextChangeRange(ts.createTextSpan(decoded.span.start, decoded.span.length), decoded.newLength); - } +/** 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): ts.ScriptKind; + getScriptVersion(fileName: string): string; + getScriptSnapshot(fileName: string): ScriptSnapshotShim; + getLocalizedDiagnosticMessages(): string; + getCancellationToken(): ts.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 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 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; +} + +/** 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: ts.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, format?: ts.SemanticClassificationFormat): string; + getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string; + getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: ts.SemanticClassificationFormat): string; + getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined, formattingSettings: ts.FormatCodeSettings | undefined): string; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string /*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | 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: ts.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?: ts.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; + + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[] + */ + getFileReferences(fileName: string): 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 }[] }[] + * + * @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, options?: ts.DocCommentTemplateOptions): 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; + provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences | undefined): string; + getEmitOutput(fileName: string): string; + getEmitOutputObject(fileName: string): ts.EmitOutput; + toggleLineComment(fileName: string, textChange: ts.TextRange): string; + toggleMultilineComment(fileName: string, textChange: ts.TextRange): string; + commentSelection(fileName: string, textChange: ts.TextRange): string; + uncommentSelection(fileName: string, textChange: ts.TextRange): string; +} + +export interface ClassifierShim extends Shim { + getEncodedLexicalClassifications(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent?: boolean): string; + getClassificationsForLine(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent?: boolean): string; +} + +export interface CoreServicesShim extends Shim { + getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string; + getPreProcessedFileInfo(fileName: string, sourceText: ts.IScriptSnapshot): string; + getTSConfigFileInfo(fileName: string, sourceText: ts.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); } +} - export class LanguageServiceShimHostAdapter implements ts.LanguageServiceHost { - private loggingEnabled = false; - private tracingEnabled = false; - - public resolveModuleNames: ((moduleName: string[], containingFile: string) => (ts.ResolvedModuleFull | undefined)[]) | undefined; - public resolveTypeReferenceDirectives: ((typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string) => (ts.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)) as ts.MapLike; // TODO: GH#18217 - return ts.map(moduleNames, name => { - const result = ts.getProperty(resolutionsInFile, name); - return result ? { resolvedFileName: result, extension: ts.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)) as ts.MapLike; // TODO: GH#18217 - return ts.map(typeDirectiveNames as (string | ts.FileReference)[], name => ts.getProperty(typeDirectivesForFile, ts.isString(name) ? name : name.fileName.toLowerCase())); - }; - } +class ScriptSnapshotShimAdapter implements ts.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: ts.IScriptSnapshot): ts.TextChangeRange | undefined { + const oldSnapshotShim = oldSnapshot as ScriptSnapshotShimAdapter; + 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 ts.createTextChangeRange(ts.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 log(s: string): void { - if (this.loggingEnabled) { - this.shimHost.log(s); - } +export class LanguageServiceShimHostAdapter implements ts.LanguageServiceHost { + private loggingEnabled = false; + private tracingEnabled = false; + + public resolveModuleNames: ((moduleName: string[], containingFile: string) => (ts.ResolvedModuleFull | undefined)[]) | undefined; + public resolveTypeReferenceDirectives: ((typeDirectiveNames: string[] | readonly ts.FileReference[], containingFile: string) => (ts.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)) as ts.MapLike; // TODO: GH#18217 + return ts.map(moduleNames, name => { + const result = ts.getProperty(resolutionsInFile, name); + return result ? { resolvedFileName: result, extension: ts.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)) as ts.MapLike; // TODO: GH#18217 + return ts.map(typeDirectiveNames as (string | ts.FileReference)[], name => ts.getProperty(typeDirectivesForFile, ts.isString(name) ? name : name.fileName.toLowerCase())); + }; + } + } - public trace(s: string): void { - if (this.tracingEnabled) { - this.shimHost.trace(s); - } + public log(s: string): void { + if (this.loggingEnabled) { + this.shimHost.log(s); } + } - public error(s: string): void { - this.shimHost.error(s); + public trace(s: string): void { + if (this.tracingEnabled) { + this.shimHost.trace(s); } + } - public getProjectVersion(): string { - if (!this.shimHost.getProjectVersion) { - // shimmed host does not support getProjectVersion - return undefined!; // TODO: GH#18217 - } + public error(s: string): void { + this.shimHost.error(s); + } - return this.shimHost.getProjectVersion(); + public getProjectVersion(): string { + if (!this.shimHost.getProjectVersion) { + // shimmed host does not support getProjectVersion + return undefined!; // TODO: GH#18217 } - public getTypeRootsVersion(): number { - if (!this.shimHost.getTypeRootsVersion) { - return 0; - } - return this.shimHost.getTypeRootsVersion(); - } + return this.shimHost.getProjectVersion(); + } - public useCaseSensitiveFileNames(): boolean { - return this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; + public getTypeRootsVersion(): number { + if (!this.shimHost.getTypeRootsVersion) { + return 0; } + return this.shimHost.getTypeRootsVersion(); + } - public getCompilationSettings(): ts.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) as ts.CompilerOptions; - // permit language service to handle all files (filtering should be performed on the host side) - compilerOptions.allowNonTsExtensions = true; - return compilerOptions; - } + public useCaseSensitiveFileNames(): boolean { + return this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; + } - public getScriptFileNames(): string[] { - const encoded = this.shimHost.getScriptFileNames(); - return JSON.parse(encoded); + public getCompilationSettings(): ts.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) as ts.CompilerOptions; + // permit language service to handle all files (filtering should be performed on the host side) + compilerOptions.allowNonTsExtensions = true; + return compilerOptions; + } - public getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { - const scriptSnapshot = this.shimHost.getScriptSnapshot(fileName); - return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot); - } + public getScriptFileNames(): string[] { + const encoded = this.shimHost.getScriptFileNames(); + return JSON.parse(encoded); + } - public getScriptKind(fileName: string): ts.ScriptKind { - if ("getScriptKind" in this.shimHost) { - return this.shimHost.getScriptKind!(fileName); // TODO: GH#18217 - } - else { - return ts.ScriptKind.Unknown; - } - } + public getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { + const scriptSnapshot = this.shimHost.getScriptSnapshot(fileName); + return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot); + } - public getScriptVersion(fileName: string): string { - return this.shimHost.getScriptVersion(fileName); + public getScriptKind(fileName: string): ts.ScriptKind { + if ("getScriptKind" in this.shimHost) { + return this.shimHost.getScriptKind!(fileName); // TODO: GH#18217 + } + else { + return ts.ScriptKind.Unknown; } + } - public getLocalizedDiagnosticMessages() { - /* eslint-disable no-null/no-null */ - const diagnosticMessagesJson = this.shimHost.getLocalizedDiagnosticMessages(); - if (diagnosticMessagesJson === null || diagnosticMessagesJson === "") { - return null; - } + public getScriptVersion(fileName: string): string { + return this.shimHost.getScriptVersion(fileName); + } - 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 getLocalizedDiagnosticMessages() { + /* eslint-disable no-null/no-null */ + const diagnosticMessagesJson = this.shimHost.getLocalizedDiagnosticMessages(); + if (diagnosticMessagesJson === null || diagnosticMessagesJson === "") { + return null; } - public getCancellationToken(): ts.HostCancellationToken { - const hostCancellationToken = this.shimHost.getCancellationToken(); - return new ts.ThrottledCancellationToken(hostCancellationToken); + try { + return JSON.parse(diagnosticMessagesJson); } - - public getCurrentDirectory(): string { - return this.shimHost.getCurrentDirectory(); + catch (e) { + this.log(e.description || "diagnosticMessages.generated.json has invalid JSON format"); + return null; } + /* eslint-enable no-null/no-null */ + } - public getDirectories(path: string): string[] { - return JSON.parse(this.shimHost.getDirectories(path)); - } + public getCancellationToken(): ts.HostCancellationToken { + const hostCancellationToken = this.shimHost.getCancellationToken(); + return new ts.ThrottledCancellationToken(hostCancellationToken); + } - public getDefaultLibFileName(options: ts.CompilerOptions): string { - return this.shimHost.getDefaultLibFileName(JSON.stringify(options)); - } + public getCurrentDirectory(): string { + return this.shimHost.getCurrentDirectory(); + } - public readDirectory(path: string, extensions?: readonly string[], exclude?: string[], include?: string[], depth?: number): string[] { - const pattern = ts.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 getDirectories(path: string): string[] { + return JSON.parse(this.shimHost.getDirectories(path)); + } - public readFile(path: string, encoding?: string): string | undefined { - return this.shimHost.readFile(path, encoding); - } + public getDefaultLibFileName(options: ts.CompilerOptions): string { + return this.shimHost.getDefaultLibFileName(JSON.stringify(options)); + } - public fileExists(path: string): boolean { - return this.shimHost.fileExists(path); - } + public readDirectory(path: string, extensions?: readonly string[], exclude?: string[], include?: string[], depth?: number): string[] { + const pattern = ts.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)); } - export class CoreServicesShimHostAdapter implements ts.ParseConfigHost, ts.ModuleResolutionHost, ts.JsTyping.TypingResolutionHost { + public readFile(path: string, encoding?: string): string | undefined { + return this.shimHost.readFile(path, encoding); + } - public directoryExists: (directoryName: string) => boolean; - public realpath: (path: string) => string; - public useCaseSensitiveFileNames: boolean; + public fileExists(path: string): boolean { + return this.shimHost.fileExists(path); + } +} - 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 - } - } +export class CoreServicesShimHostAdapter implements ts.ParseConfigHost, ts.ModuleResolutionHost, ts.JsTyping.TypingResolutionHost { - public readDirectory(rootDir: string, extensions: readonly string[], exclude: readonly string[], include: readonly string[], depth?: number): string[] { - const pattern = ts.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 directoryExists: (directoryName: string) => boolean; + public realpath: (path: string) => string; + public useCaseSensitiveFileNames: boolean; - public fileExists(fileName: string): boolean { - return this.shimHost.fileExists(fileName); + 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 readFile(fileName: string): string | undefined { - return this.shimHost.readFile(fileName); + else { + this.directoryExists = undefined!; // TODO: GH#18217 } - - public getDirectories(path: string): string[] { - return JSON.parse(this.shimHost.getDirectories(path)); + if ("realpath" in this.shimHost) { + this.realpath = path => this.shimHost.realpath!(path); // TODO: GH#18217 + } + else { + this.realpath = undefined!; // TODO: GH#18217 } } - function simpleForwardCall(logger: Logger, actionDescription: string, action: () => unknown, logPerformance: boolean): unknown { - let start: number | undefined; - if (logPerformance) { - logger.log(actionDescription); - start = ts.timestamp(); - } + public readDirectory(rootDir: string, extensions: readonly string[], exclude: readonly string[], include: readonly string[], depth?: number): string[] { + const pattern = ts.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)); + } - const result = action(); - - if (logPerformance) { - const end = ts.timestamp(); - logger.log(`${actionDescription} completed in ${end - start!} msec`); - if (ts.isString(result)) { - let str = result; - if (str.length > 128) { - str = str.substring(0, 128) + "..."; - } - logger.log(` result.length=${str.length}, result='${JSON.stringify(str)}'`); - } - } + public fileExists(fileName: string): boolean { + return this.shimHost.fileExists(fileName); + } - return result; + public readFile(fileName: string): string | undefined { + return this.shimHost.readFile(fileName); } - function forwardJSONCall(logger: Logger, actionDescription: string, action: () => {} | null | undefined, logPerformance: boolean): string { - return forwardCall(logger, actionDescription, /*returnJson*/ true, action, logPerformance) as string; + public getDirectories(path: string): string[] { + return JSON.parse(this.shimHost.getDirectories(path)); } +} - 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 ts.OperationCanceledException) { - return JSON.stringify({ canceled: true }); +function simpleForwardCall(logger: Logger, actionDescription: string, action: () => unknown, logPerformance: boolean): unknown { + let start: number | undefined; + if (logPerformance) { + logger.log(actionDescription); + start = ts.timestamp(); + } + + const result = action(); + + if (logPerformance) { + const end = ts.timestamp(); + logger.log(`${actionDescription} completed in ${end - start!} msec`); + if (ts.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)}'`); } } + return result; +} - class ShimBase implements Shim { - constructor(private factory: ShimFactory) { - factory.registerShim(this); - } - public dispose(_dummy: {}): void { - this.factory.unregisterShim(this); +function forwardJSONCall(logger: Logger, actionDescription: string, action: () => {} | null | undefined, logPerformance: boolean): string { + return forwardCall(logger, actionDescription, /*returnJson*/ true, action, logPerformance) as string; +} + +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 ts.OperationCanceledException) { + return JSON.stringify({ canceled: true }); } + logInternalError(logger, err); + err.description = actionDescription; + return JSON.stringify({ error: err }); } +} - export interface RealizedDiagnostic { - message: string; - start: number; - length: number; - category: string; - code: number; - reportsUnnecessary?: {}; - reportsDeprecated?: {}; + +class ShimBase implements Shim { + constructor(private factory: ShimFactory) { + factory.registerShim(this); } - export function realizeDiagnostics(diagnostics: readonly ts.Diagnostic[], newLine: string): RealizedDiagnostic[] { - return diagnostics.map(d => realizeDiagnostic(d, newLine)); + public dispose(_dummy: {}): void { + this.factory.unregisterShim(this); } +} - function realizeDiagnostic(diagnostic: ts.Diagnostic, newLine: string): RealizedDiagnostic { - return { - message: ts.flattenDiagnosticMessageText(diagnostic.messageText, newLine), - start: diagnostic.start!, - length: diagnostic.length!, - category: ts.diagnosticCategoryName(diagnostic), - code: diagnostic.code, - reportsUnnecessary: diagnostic.reportsUnnecessary, - reportsDeprecated: diagnostic.reportsDeprecated - }; +export interface RealizedDiagnostic { + message: string; + start: number; + length: number; + category: string; + code: number; + reportsUnnecessary?: {}; + reportsDeprecated?: {}; +} +export function realizeDiagnostics(diagnostics: readonly ts.Diagnostic[], newLine: string): RealizedDiagnostic[] { + return diagnostics.map(d => realizeDiagnostic(d, newLine)); +} + +function realizeDiagnostic(diagnostic: ts.Diagnostic, newLine: string): RealizedDiagnostic { + return { + message: ts.flattenDiagnosticMessageText(diagnostic.messageText, newLine), + start: diagnostic.start!, + length: diagnostic.length!, + category: ts.diagnosticCategoryName(diagnostic), + code: diagnostic.code, + reportsUnnecessary: diagnostic.reportsUnnecessary, + reportsDeprecated: diagnostic.reportsDeprecated + }; +} + +class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim { + private logger: Logger; + private logPerformance = false; + + constructor(factory: ShimFactory, private host: LanguageServiceShimHost, public languageService: ts.LanguageService) { + super(factory); + this.logger = this.host; } - class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim { - private logger: Logger; - private logPerformance = false; + public forwardJSONCall(actionDescription: string, action: () => {} | null | undefined): string { + return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); + } - constructor(factory: ShimFactory, private host: LanguageServiceShimHost, public languageService: ts.LanguageService) { - super(factory); - this.logger = this.host; - } + /// 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 - public forwardJSONCall(actionDescription: string, action: () => {} | null | undefined): string { - return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); + // force a GC + if (debugObjectHost && debugObjectHost.CollectGarbage) { + debugObjectHost.CollectGarbage(); + this.logger.log("CollectGarbage()"); } - /// 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 - this.logger = null!; // eslint-disable-line no-null/no-null + super.dispose(dummy); + } - super.dispose(dummy); - } + /// REFRESH - /// 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 + ); + } - /** - * 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 + }); + } - public cleanupSemanticCache(): void { - this.forwardJSONCall("cleanupSemanticCache()", () => { - this.languageService.cleanupSemanticCache(); - return null; // eslint-disable-line no-null/no-null - }); - } + private realizeDiagnostics(diagnostics: readonly ts.Diagnostic[]): { + message: string; + start: number; + length: number; + category: string; + }[] { + const newLine = ts.getNewLineOrDefaultFromHost(this.host); + return realizeDiagnostics(diagnostics, newLine); + } - private realizeDiagnostics(diagnostics: readonly ts.Diagnostic[]): { - message: string; - start: number; - length: number; - category: string; - }[] { - const newLine = ts.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, ts.createTextSpan(start, length))); + } - public getSyntacticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall(`getSyntacticClassifications('${fileName}', ${start}, ${length})`, () => this.languageService.getSyntacticClassifications(fileName, ts.createTextSpan(start, length))); - } + public getSemanticClassifications(fileName: string, start: number, length: number): string { + return this.forwardJSONCall(`getSemanticClassifications('${fileName}', ${start}, ${length})`, () => this.languageService.getSemanticClassifications(fileName, ts.createTextSpan(start, length))); + } - public getSemanticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall(`getSemanticClassifications('${fileName}', ${start}, ${length})`, () => this.languageService.getSemanticClassifications(fileName, ts.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, ts.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, ts.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, ts.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, ts.createTextSpan(start, length)))); - } + public getSyntacticDiagnostics(fileName: string): string { + return this.forwardJSONCall(`getSyntacticDiagnostics('${fileName}')`, () => { + const diagnostics = this.languageService.getSyntacticDiagnostics(fileName); + return this.realizeDiagnostics(diagnostics); + }); + } - 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 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 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); + }); + } - public getCompilerOptionsDiagnostics(): string { - return this.forwardJSONCall("getCompilerOptionsDiagnostics()", () => { - const diagnostics = this.languageService.getCompilerOptionsDiagnostics(); - return this.realizeDiagnostics(diagnostics); - }); - } + /// QUICKINFO - /// 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)); + } - /** - * 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 - /// 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)); + } - /** - * 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)); + } - /** - * 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 - /// SIGNATUREHELP + public getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): string { + return this.forwardJSONCall(`getSignatureHelpItems('${fileName}', ${position})`, () => this.languageService.getSignatureHelpItems(fileName, position, options)); + } - public getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): string { - return this.forwardJSONCall(`getSignatureHelpItems('${fileName}', ${position})`, () => this.languageService.getSignatureHelpItems(fileName, position, options)); - } + /// GOTO DEFINITION - /// 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 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)); + } - /** - * 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 - /// 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)); + } - /** - * 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 - /// 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)); + } - /** - * 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?: ts.RenameInfoOptions): string { + return this.forwardJSONCall(`getRenameInfo('${fileName}', ${position})`, () => this.languageService.getRenameInfo(fileName, position, options)); + } - public getRenameInfo(fileName: string, position: number, options?: ts.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 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)); + } - 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)); + } - /// 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 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)); + } - 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: ts.EditorOptions = JSON.parse(options); + return this.languageService.getIndentationAtPosition(fileName, position, localOptions); + }); + } - /// GET SMART INDENT - public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string { - return this.forwardJSONCall(`getIndentationAtPosition('${fileName}', ${position})`, () => { - const localOptions: ts.EditorOptions = JSON.parse(options); - return this.languageService.getIndentationAtPosition(fileName, position, localOptions); - }); - } + /// GET REFERENCES - /// GET REFERENCES + public getReferencesAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getReferencesAtPosition('${fileName}', ${position})`, () => this.languageService.getReferencesAtPosition(fileName, position)); + } - 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 findReferences(fileName: string, position: number): string { - return this.forwardJSONCall(`findReferences('${fileName}', ${position})`, () => this.languageService.findReferences(fileName, position)); - } + public getFileReferences(fileName: string) { + return this.forwardJSONCall(`getFileReferences('${fileName})`, () => this.languageService.getFileReferences(fileName)); + } - public getFileReferences(fileName: string) { - return this.forwardJSONCall(`getFileReferences('${fileName})`, () => this.languageService.getFileReferences(fileName)); - } + public getOccurrencesAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getOccurrencesAtPosition('${fileName}', ${position})`, () => this.languageService.getOccurrencesAtPosition(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 = ts.toFileNameLowerCase(ts.normalizeSlashes(fileName)); + return ts.filter(results, r => ts.toFileNameLowerCase(ts.normalizeSlashes(r.fileName)) === normalizedName); + }); + } - 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 = ts.toFileNameLowerCase(ts.normalizeSlashes(fileName)); - return ts.filter(results, r => ts.toFileNameLowerCase(ts.normalizeSlashes(r.fileName)) === normalizedName); - }); - } + /// COMPLETION LISTS - /// 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: ts.GetCompletionsAtPositionOptions | undefined, formattingSettings: ts.FormatCodeSettings | undefined) { + return this.forwardJSONCall(`getCompletionsAtPosition('${fileName}', ${position}, ${preferences}, ${formattingSettings})`, () => this.languageService.getCompletionsAtPosition(fileName, position, preferences, formattingSettings)); + } - /** - * 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: ts.GetCompletionsAtPositionOptions | undefined, formattingSettings: ts.FormatCodeSettings | undefined) { - return this.forwardJSONCall(`getCompletionsAtPosition('${fileName}', ${position}, ${preferences}, ${formattingSettings})`, () => this.languageService.getCompletionsAtPosition(fileName, position, preferences, formattingSettings)); - } + /** 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: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined) { + return this.forwardJSONCall(`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, () => { + const localOptions: ts.FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); + return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences, data); + }); + } - /** 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: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined) { - return this.forwardJSONCall(`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, () => { - const localOptions: ts.FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); - return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences, data); + public getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsForRange('${fileName}', ${start}, ${end})`, () => { + const localOptions: ts.FormatCodeOptions = JSON.parse(options); + return this.languageService.getFormattingEditsForRange(fileName, start, end, localOptions); }); - } - - public getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string { - return this.forwardJSONCall(`getFormattingEditsForRange('${fileName}', ${start}, ${end})`, () => { - const localOptions: ts.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: ts.FormatCodeOptions = JSON.parse(options); - return this.languageService.getFormattingEditsForDocument(fileName, localOptions); - }); - } + public getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsForDocument('${fileName}')`, () => { + const localOptions: ts.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: ts.FormatCodeOptions = JSON.parse(options); - return this.languageService.getFormattingEditsAfterKeystroke(fileName, position, key, localOptions); - }); - } + public getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsAfterKeystroke('${fileName}', ${position}, '${key}')`, () => { + const localOptions: ts.FormatCodeOptions = JSON.parse(options); + return this.languageService.getFormattingEditsAfterKeystroke(fileName, position, key, localOptions); + }); + } - public getDocCommentTemplateAtPosition(fileName: string, position: number, options?: ts.DocCommentTemplateOptions): string { - return this.forwardJSONCall(`getDocCommentTemplateAtPosition('${fileName}', ${position})`, () => this.languageService.getDocCommentTemplateAtPosition(fileName, position, options)); - } + public getDocCommentTemplateAtPosition(fileName: string, position: number, options?: ts.DocCommentTemplateOptions): string { + return this.forwardJSONCall(`getDocCommentTemplateAtPosition('${fileName}', ${position})`, () => this.languageService.getDocCommentTemplateAtPosition(fileName, position, options)); + } - /// NAVIGATE TO + /// 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)); - } + /** 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 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 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 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))); - } + public getTodoComments(fileName: string, descriptors: string): string { + return this.forwardJSONCall(`getTodoComments('${fileName}')`, () => this.languageService.getTodoComments(fileName, JSON.parse(descriptors))); + } - /// CALL HIERARCHY + /// CALL HIERARCHY - public prepareCallHierarchy(fileName: string, position: number): string { - return this.forwardJSONCall(`prepareCallHierarchy('${fileName}', ${position})`, () => this.languageService.prepareCallHierarchy(fileName, position)); - } + 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 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)); - } + public provideCallHierarchyOutgoingCalls(fileName: string, position: number): string { + return this.forwardJSONCall(`provideCallHierarchyOutgoingCalls('${fileName}', ${position})`, () => this.languageService.provideCallHierarchyOutgoingCalls(fileName, position)); + } - public provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences | undefined): string { - return this.forwardJSONCall(`provideInlayHints('${fileName}', '${JSON.stringify(span)}', ${JSON.stringify(preference)})`, () => this.languageService.provideInlayHints(fileName, span, preference)); - } + public provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences | undefined): string { + return this.forwardJSONCall(`provideInlayHints('${fileName}', '${JSON.stringify(span)}', ${JSON.stringify(preference)})`, () => this.languageService.provideInlayHints(fileName, span, preference)); + } - /// Emit - public getEmitOutput(fileName: string): string { - return this.forwardJSONCall(`getEmitOutput('${fileName}')`, () => { - const { diagnostics, ...rest } = this.languageService.getEmitOutput(fileName); - return { ...rest, diagnostics: this.realizeDiagnostics(diagnostics) }; - }); - } - public getEmitOutputObject(fileName: string): ts.EmitOutput { - return forwardCall(this.logger, `getEmitOutput('${fileName}')`, - /*returnJson*/ false, () => this.languageService.getEmitOutput(fileName), this.logPerformance) as ts.EmitOutput; - } + /// Emit + public getEmitOutput(fileName: string): string { + return this.forwardJSONCall(`getEmitOutput('${fileName}')`, () => { + const { diagnostics, ...rest } = this.languageService.getEmitOutput(fileName); + return { ...rest, diagnostics: this.realizeDiagnostics(diagnostics) }; + }); + } + public getEmitOutputObject(fileName: string): ts.EmitOutput { + return forwardCall(this.logger, `getEmitOutput('${fileName}')`, + /*returnJson*/ false, () => this.languageService.getEmitOutput(fileName), this.logPerformance) as ts.EmitOutput; + } - public toggleLineComment(fileName: string, textRange: ts.TextRange): string { - return this.forwardJSONCall(`toggleLineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleLineComment(fileName, textRange)); - } + public toggleLineComment(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall(`toggleLineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleLineComment(fileName, textRange)); + } - public toggleMultilineComment(fileName: string, textRange: ts.TextRange): string { - return this.forwardJSONCall(`toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleMultilineComment(fileName, textRange)); - } + public toggleMultilineComment(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall(`toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleMultilineComment(fileName, textRange)); + } - public commentSelection(fileName: string, textRange: ts.TextRange): string { - return this.forwardJSONCall(`commentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.commentSelection(fileName, textRange)); - } + public commentSelection(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall(`commentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.commentSelection(fileName, textRange)); + } - public uncommentSelection(fileName: string, textRange: ts.TextRange): string { - return this.forwardJSONCall(`uncommentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.uncommentSelection(fileName, textRange)); - } + public uncommentSelection(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall(`uncommentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.uncommentSelection(fileName, textRange)); } +} + +function convertClassifications(classifications: ts.Classifications): { + spans: string; + endOfLineState: ts.EndOfLineState; +} { + return { spans: classifications.spans.join(","), endOfLineState: classifications.endOfLineState }; +} + +class ClassifierShimObject extends ShimBase implements ClassifierShim { + public classifier: ts.Classifier; + private logPerformance = false; - function convertClassifications(classifications: ts.Classifications): { - spans: string; - endOfLineState: ts.EndOfLineState; - } { - return { spans: classifications.spans.join(","), endOfLineState: classifications.endOfLineState }; + constructor(factory: ShimFactory, private logger: Logger) { + super(factory); + this.classifier = ts.createClassifier(); } - class ClassifierShimObject extends ShimBase implements ClassifierShim { - public classifier: ts.Classifier; - private logPerformance = false; + public getEncodedLexicalClassifications(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent = false): string { + return forwardJSONCall(this.logger, "getEncodedLexicalClassifications", () => convertClassifications(this.classifier.getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent)), this.logPerformance); + } - constructor(factory: ShimFactory, private logger: Logger) { - super(factory); - this.classifier = ts.createClassifier(); + /// COLORIZATION + public getClassificationsForLine(text: string, lexState: ts.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 getEncodedLexicalClassifications(text: string, lexState: ts.EndOfLineState, syntacticClassifierAbsent = false): string { - return forwardJSONCall(this.logger, "getEncodedLexicalClassifications", () => convertClassifications(this.classifier.getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent)), this.logPerformance); - } +class CoreServicesShimObject extends ShimBase implements CoreServicesShim { + private logPerformance = false; + private safeList: ts.JsTyping.SafeList | undefined; - /// COLORIZATION - public getClassificationsForLine(text: string, lexState: ts.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; - } + constructor(factory: ShimFactory, public readonly logger: Logger, private readonly host: CoreServicesShimHostAdapter) { + super(factory); } - class CoreServicesShimObject extends ShimBase implements CoreServicesShim { - private logPerformance = false; - private safeList: ts.JsTyping.SafeList | undefined; + private forwardJSONCall(actionDescription: string, action: () => {}): string { + return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); + } - constructor(factory: ShimFactory, public readonly logger: Logger, private readonly host: CoreServicesShimHostAdapter) { - super(factory); - } + public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => { + const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; + const result = ts.resolveModuleName(moduleName, ts.normalizeSlashes(fileName), compilerOptions, this.host); + let resolvedFileName = result.resolvedModule ? result.resolvedModule.resolvedFileName : undefined; + if (result.resolvedModule && result.resolvedModule.extension !== ts.Extension.Ts && result.resolvedModule.extension !== ts.Extension.Tsx && result.resolvedModule.extension !== ts.Extension.Dts) { + resolvedFileName = undefined; + } - private forwardJSONCall(actionDescription: string, action: () => {}): string { - return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); - } + return { + resolvedFileName, + failedLookupLocations: result.failedLookupLocations + }; + }); + } - public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string { - return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => { - const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; - const result = ts.resolveModuleName(moduleName, ts.normalizeSlashes(fileName), compilerOptions, this.host); - let resolvedFileName = result.resolvedModule ? result.resolvedModule.resolvedFileName : undefined; - if (result.resolvedModule && result.resolvedModule.extension !== ts.Extension.Ts && result.resolvedModule.extension !== ts.Extension.Tsx && result.resolvedModule.extension !== ts.Extension.Dts) { - resolvedFileName = undefined; - } + public resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => { + const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; + const result = ts.resolveTypeReferenceDirective(typeReferenceDirective, ts.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: ts.IScriptSnapshot): string { + return this.forwardJSONCall(`getPreProcessedFileInfo('${fileName}')`, () => { + // for now treat files as JavaScript + const result = ts.preProcessFile(ts.getSnapshotText(sourceTextSnapshot), /* readImportFiles */ true, /* detectJavaScriptImports */ true); return { - resolvedFileName, - failedLookupLocations: result.failedLookupLocations + 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 resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilerOptionsJson: string): string { - return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => { - const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; - const result = ts.resolveTypeReferenceDirective(typeReferenceDirective, ts.normalizeSlashes(fileName), compilerOptions, this.host); - return { - resolvedFileName: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.resolvedFileName : undefined, - primary: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.primary : true, - failedLookupLocations: result.failedLookupLocations - }; - }); - } + public getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string { + return this.forwardJSONCall(`getAutomaticTypeDirectiveNames('${compilerOptionsJson}')`, () => { + const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; + return ts.getAutomaticTypeDirectiveNames(compilerOptions, this.host); + }); + } - public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: ts.IScriptSnapshot): string { - return this.forwardJSONCall(`getPreProcessedFileInfo('${fileName}')`, () => { - // for now treat files as JavaScript - const result = ts.preProcessFile(ts.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) - }; - }); + private convertFileReferences(refs: ts.FileReference[]): ShimsFileReference[] | undefined { + if (!refs) { + return undefined; } - - public getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string { - return this.forwardJSONCall(`getAutomaticTypeDirectiveNames('${compilerOptionsJson}')`, () => { - const compilerOptions = JSON.parse(compilerOptionsJson) as ts.CompilerOptions; - return ts.getAutomaticTypeDirectiveNames(compilerOptions, this.host); + const result: ShimsFileReference[] = []; + for (const ref of refs) { + result.push({ + path: ts.normalizeSlashes(ref.fileName), + position: ref.pos, + length: ref.end - ref.pos }); } + return result; + } - private convertFileReferences(refs: ts.FileReference[]): ShimsFileReference[] | undefined { - if (!refs) { - return undefined; - } - const result: ShimsFileReference[] = []; - for (const ref of refs) { - result.push({ - path: ts.normalizeSlashes(ref.fileName), - position: ref.pos, - length: ref.end - ref.pos - }); - } - return result; - } + public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: ts.IScriptSnapshot): string { + return this.forwardJSONCall(`getTSConfigFileInfo('${fileName}')`, () => { + const result = ts.parseJsonText(fileName, ts.getSnapshotText(sourceTextSnapshot)); + const normalizedFileName = ts.normalizeSlashes(fileName); + const configFile = ts.parseJsonSourceFileConfigFileContent(result, this.host, ts.getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName); - public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: ts.IScriptSnapshot): string { - return this.forwardJSONCall(`getTSConfigFileInfo('${fileName}')`, () => { - const result = ts.parseJsonText(fileName, ts.getSnapshotText(sourceTextSnapshot)); - const normalizedFileName = ts.normalizeSlashes(fileName); - const configFile = ts.parseJsonSourceFileConfigFileContent(result, this.host, ts.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") - }; - }); - } + 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()", () => ts.getDefaultCompilerOptions()); - } + public getDefaultCompilationSettings(): string { + return this.forwardJSONCall("getDefaultCompilationSettings()", () => ts.getDefaultCompilerOptions()); + } - public discoverTypings(discoverTypingsJson: string): string { - const getCanonicalFileName = ts.createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false); - return this.forwardJSONCall("discoverTypings()", () => { - const info = JSON.parse(discoverTypingsJson) as DiscoverTypingsInfo; - if (this.safeList === undefined) { - this.safeList = ts.JsTyping.loadSafeList(this.host, ts.toPath(info.safeListPath, info.safeListPath, getCanonicalFileName)); - } - return ts.JsTyping.discoverTypings(this.host, msg => this.logger.log(msg), info.fileNames, ts.toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName), this.safeList, info.packageNameToTypingLocation, info.typeAcquisition, info.unresolvedImports, info.typesRegistry); - }); - } + public discoverTypings(discoverTypingsJson: string): string { + const getCanonicalFileName = ts.createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false); + return this.forwardJSONCall("discoverTypings()", () => { + const info = JSON.parse(discoverTypingsJson) as DiscoverTypingsInfo; + if (this.safeList === undefined) { + this.safeList = ts.JsTyping.loadSafeList(this.host, ts.toPath(info.safeListPath, info.safeListPath, getCanonicalFileName)); + } + return ts.JsTyping.discoverTypings(this.host, msg => this.logger.log(msg), info.fileNames, ts.toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName), this.safeList, info.packageNameToTypingLocation, info.typeAcquisition, info.unresolvedImports, info.typesRegistry); + }); } +} - export class TypeScriptServicesFactory implements ShimFactory { - private _shims: Shim[] = []; - private documentRegistry: ts.DocumentRegistry | undefined; +export class TypeScriptServicesFactory implements ShimFactory { + private _shims: Shim[] = []; + private documentRegistry: ts.DocumentRegistry | undefined; - /* - * Returns script API version. - */ - public getServicesVersion(): string { - return ts.servicesVersion; - } + /* + * Returns script API version. + */ + public getServicesVersion(): string { + return ts.servicesVersion; + } - public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim { - try { - if (this.documentRegistry === undefined) { - this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()); - } - const hostAdapter = new LanguageServiceShimHostAdapter(host); - const languageService = ts.createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnly*/ false); - return new LanguageServiceShimObject(this, host, languageService); - } - catch (err) { - logInternalError(host, err); - throw err; + public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim { + try { + if (this.documentRegistry === undefined) { + this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()); } + const hostAdapter = new LanguageServiceShimHostAdapter(host); + const languageService = ts.createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnly*/ false); + return new LanguageServiceShimObject(this, host, languageService); } - - public createClassifierShim(logger: Logger): ClassifierShim { - try { - return new ClassifierShimObject(this, logger); - } - catch (err) { - logInternalError(logger, err); - throw err; - } + catch (err) { + logInternalError(host, err); + throw err; } + } - public createCoreServicesShim(host: CoreServicesShimHost): CoreServicesShim { - try { - const adapter = new CoreServicesShimHostAdapter(host); - return new CoreServicesShimObject(this, host as Logger, adapter); - } - catch (err) { - logInternalError(host as Logger, err); - throw err; - } + public createClassifierShim(logger: Logger): ClassifierShim { + try { + return new ClassifierShimObject(this, logger); } - - public close(): void { - // Forget all the registered shims - ts.clear(this._shims); - this.documentRegistry = undefined; + catch (err) { + logInternalError(logger, err); + throw err; } + } - public registerShim(shim: Shim): void { - this._shims.push(shim); + public createCoreServicesShim(host: CoreServicesShimHost): CoreServicesShim { + try { + const adapter = new CoreServicesShimHostAdapter(host); + return new CoreServicesShimObject(this, host as Logger, adapter); } + catch (err) { + logInternalError(host as Logger, 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 + ts.clear(this._shims); + this.documentRegistry = undefined; + } - throw new Error("Invalid operation"); + 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"); } } +} /* eslint-enable no-in-operator */ diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index a3bd2c22634c9..36219e8a537ac 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -1,682 +1,682 @@ /* @internal */ namespace ts.SignatureHelp { - const enum InvocationKind { - Call, - TypeArgs, - Contextual - } - interface CallInvocation { - readonly kind: InvocationKind.Call; - readonly node: ts.CallLikeExpression; - } - interface TypeArgsInvocation { - readonly kind: InvocationKind.TypeArgs; - readonly called: ts.Identifier; - } - interface ContextualInvocation { - readonly kind: InvocationKind.Contextual; - readonly signature: ts.Signature; - readonly node: ts.Node; // Just for enclosingDeclaration for printing types - readonly symbol: ts.Symbol; - } - type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation; - - interface ArgumentListInfo { - readonly isTypeParameterList: boolean; - readonly invocation: Invocation; - readonly argumentsSpan: ts.TextSpan; - readonly argumentIndex: number; - /** argumentCount is the *apparent* number of arguments. */ - readonly argumentCount: number; - } - - export function getSignatureHelpItems(program: ts.Program, sourceFile: ts.SourceFile, position: number, triggerReason: ts.SignatureHelpTriggerReason | undefined, cancellationToken: ts.CancellationToken): ts.SignatureHelpItems | undefined { - const typeChecker = program.getTypeChecker(); +const enum InvocationKind { + Call, + TypeArgs, + Contextual +} +interface CallInvocation { + readonly kind: InvocationKind.Call; + readonly node: ts.CallLikeExpression; +} +interface TypeArgsInvocation { + readonly kind: InvocationKind.TypeArgs; + readonly called: ts.Identifier; +} +interface ContextualInvocation { + readonly kind: InvocationKind.Contextual; + readonly signature: ts.Signature; + readonly node: ts.Node; // Just for enclosingDeclaration for printing types + readonly symbol: ts.Symbol; +} +type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation; + +interface ArgumentListInfo { + readonly isTypeParameterList: boolean; + readonly invocation: Invocation; + readonly argumentsSpan: ts.TextSpan; + readonly argumentIndex: number; + /** argumentCount is the *apparent* number of arguments. */ + readonly argumentCount: number; +} - // Decide whether to show signature help - const startingToken = ts.findTokenOnLeftOfPosition(sourceFile, position); - if (!startingToken) { - // We are at the beginning of the file - return undefined; - } +export function getSignatureHelpItems(program: ts.Program, sourceFile: ts.SourceFile, position: number, triggerReason: ts.SignatureHelpTriggerReason | undefined, cancellationToken: ts.CancellationToken): ts.SignatureHelpItems | undefined { + const typeChecker = program.getTypeChecker(); - // Only need to be careful if the user typed a character and signature help wasn't showing. - const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped"; + // Decide whether to show signature help + const startingToken = ts.findTokenOnLeftOfPosition(sourceFile, position); + if (!startingToken) { + // We are at the beginning of the file + return undefined; + } - // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. - if (onlyUseSyntacticOwners && (ts.isInString(sourceFile, position, startingToken) || ts.isInComment(sourceFile, position))) { - 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"; - const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked"; - const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked); - if (!argumentInfo) - return undefined; + // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. + if (onlyUseSyntacticOwners && (ts.isInString(sourceFile, position, startingToken) || ts.isInComment(sourceFile, position))) { + return undefined; + } - cancellationToken.throwIfCancellationRequested(); + const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked"; + const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked); + if (!argumentInfo) + return undefined; - // Extra syntactic and semantic filtering of signature help - const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); - cancellationToken.throwIfCancellationRequested(); + 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 ts.isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; - } + // Extra syntactic and semantic filtering of signature help + const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); + cancellationToken.throwIfCancellationRequested(); - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => candidateInfo.kind === CandidateOrTypeKind.Candidate - ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) - : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); + 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 ts.isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; } - const enum CandidateOrTypeKind { - Candidate, - Type - } - interface CandidateInfo { - readonly kind: CandidateOrTypeKind.Candidate; - readonly candidates: readonly ts.Signature[]; - readonly resolvedSignature: ts.Signature; - } - interface TypeInfo { - readonly kind: CandidateOrTypeKind.Type; - readonly symbol: ts.Symbol; - } + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => candidateInfo.kind === CandidateOrTypeKind.Candidate + ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) + : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); +} - function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: ts.TypeChecker, sourceFile: ts.SourceFile, startingToken: ts.Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined { - switch (invocation.kind) { - case InvocationKind.Call: { - if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) { - return undefined; - } - const candidates: ts.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, ts.isIdentifier(called) ? called.parent : called)) { - return undefined; - } - const candidates = ts.getPossibleGenericSignatures(called, argumentCount, checker); - if (candidates.length !== 0) - return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: ts.first(candidates) }; +const enum CandidateOrTypeKind { + Candidate, + Type +} +interface CandidateInfo { + readonly kind: CandidateOrTypeKind.Candidate; + readonly candidates: readonly ts.Signature[]; + readonly resolvedSignature: ts.Signature; +} +interface TypeInfo { + readonly kind: CandidateOrTypeKind.Type; + readonly symbol: ts.Symbol; +} - const symbol = checker.getSymbolAtLocation(called); - return symbol && { kind: CandidateOrTypeKind.Type, symbol }; +function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: ts.TypeChecker, sourceFile: ts.SourceFile, startingToken: ts.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 ts.Debug.assertNever(invocation); + const candidates: ts.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: ts.Node, node: ts.CallLikeExpression, sourceFile: ts.SourceFile): boolean { - if (!ts.isCallOrNewExpression(node)) - return false; - const invocationChildren = node.getChildren(sourceFile); - switch (startingToken.kind) { - case ts.SyntaxKind.OpenParenToken: - return ts.contains(invocationChildren, startingToken); - case ts.SyntaxKind.CommaToken: { - const containingList = ts.findContainingList(startingToken); - return !!containingList && ts.contains(invocationChildren, containingList); + case InvocationKind.TypeArgs: { + const { called } = invocation; + if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, ts.isIdentifier(called) ? called.parent : called)) { + return undefined; } - case ts.SyntaxKind.LessThanToken: - return containsPrecedingToken(startingToken, sourceFile, node.expression); - default: - return false; + const candidates = ts.getPossibleGenericSignatures(called, argumentCount, checker); + if (candidates.length !== 0) + return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: ts.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 ts.Debug.assertNever(invocation); } +} - function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: ts.Program, cancellationToken: ts.CancellationToken): ts.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 = ts.isPropertyAccessExpression(expression) ? expression.name.text : undefined; - const typeChecker = program.getTypeChecker(); - return name === undefined ? undefined : ts.firstDefined(program.getSourceFiles(), sourceFile => ts.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, - /*useFullPrefix*/ true)); - } - })); +function isSyntacticOwner(startingToken: ts.Node, node: ts.CallLikeExpression, sourceFile: ts.SourceFile): boolean { + if (!ts.isCallOrNewExpression(node)) + return false; + const invocationChildren = node.getChildren(sourceFile); + switch (startingToken.kind) { + case ts.SyntaxKind.OpenParenToken: + return ts.contains(invocationChildren, startingToken); + case ts.SyntaxKind.CommaToken: { + const containingList = ts.findContainingList(startingToken); + return !!containingList && ts.contains(invocationChildren, containingList); + } + case ts.SyntaxKind.LessThanToken: + return containsPrecedingToken(startingToken, sourceFile, node.expression); + default: + return false; } +} - function containsPrecedingToken(startingToken: ts.Node, sourceFile: ts.SourceFile, container: ts.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: ts.Node | undefined = startingToken.parent; - while (currentParent) { - const precedingToken = ts.findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true); - if (precedingToken) { - return ts.rangeContainsRange(container, precedingToken); +function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: ts.Program, cancellationToken: ts.CancellationToken): ts.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 = ts.isPropertyAccessExpression(expression) ? expression.name.text : undefined; + const typeChecker = program.getTypeChecker(); + return name === undefined ? undefined : ts.firstDefined(program.getSourceFiles(), sourceFile => ts.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, + /*useFullPrefix*/ true)); } - currentParent = currentParent.parent; + })); +} + +function containsPrecedingToken(startingToken: ts.Node, sourceFile: ts.SourceFile, container: ts.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: ts.Node | undefined = startingToken.parent; + while (currentParent) { + const precedingToken = ts.findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true); + if (precedingToken) { + return ts.rangeContainsRange(container, precedingToken); } - return ts.Debug.fail("Could not find preceding token"); + currentParent = currentParent.parent; } + return ts.Debug.fail("Could not find preceding token"); +} + +export interface ArgumentInfoForCompletions { + readonly invocation: ts.CallLikeExpression; + readonly argumentIndex: number; + readonly argumentCount: number; +} +export function getArgumentInfoForCompletions(node: ts.Node, position: number, sourceFile: ts.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 }; +} + +function getArgumentOrParameterListInfo(node: ts.Node, position: number, sourceFile: ts.SourceFile): { + readonly list: ts.Node; + readonly argumentIndex: number; + readonly argumentCount: number; + readonly argumentsSpan: ts.TextSpan; +} | undefined { + const info = getArgumentOrParameterListAndIndex(node, sourceFile); + if (!info) + return undefined; + const { list, argumentIndex } = info; - export interface ArgumentInfoForCompletions { - readonly invocation: ts.CallLikeExpression; - readonly argumentIndex: number; - readonly argumentCount: number; + const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ ts.isInString(sourceFile, position, node)); + if (argumentIndex !== 0) { + ts.Debug.assertLessThan(argumentIndex, argumentCount); } - export function getArgumentInfoForCompletions(node: ts.Node, position: number, sourceFile: ts.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 }; + const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); + return { list, argumentIndex, argumentCount, argumentsSpan }; +} +function getArgumentOrParameterListAndIndex(node: ts.Node, sourceFile: ts.SourceFile): { + readonly list: ts.Node; + readonly argumentIndex: number; +} | undefined { + if (node.kind === ts.SyntaxKind.LessThanToken || node.kind === ts.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 = ts.findContainingList(node); + return list && { list, argumentIndex: getArgumentIndex(list, node) }; } +} - function getArgumentOrParameterListInfo(node: ts.Node, position: number, sourceFile: ts.SourceFile): { - readonly list: ts.Node; - readonly argumentIndex: number; - readonly argumentCount: number; - readonly argumentsSpan: ts.TextSpan; - } | undefined { - const info = getArgumentOrParameterListAndIndex(node, sourceFile); +/** + * 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: ts.Node, position: number, sourceFile: ts.SourceFile): ArgumentListInfo | undefined { + const { parent } = node; + if (ts.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, position, sourceFile); if (!info) return undefined; - const { list, argumentIndex } = info; - - const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ ts.isInString(sourceFile, position, node)); - if (argumentIndex !== 0) { - ts.Debug.assertLessThan(argumentIndex, argumentCount); + 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 (ts.isNoSubstitutionTemplateLiteral(node) && ts.isTaggedTemplateExpression(parent)) { + // Check if we're actually inside the template; + // otherwise we'll fall out and return undefined. + if (ts.isInsideTemplateLiteral(node, position, sourceFile)) { + return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); } - const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); - return { list, argumentIndex, argumentCount, argumentsSpan }; + return undefined; } - function getArgumentOrParameterListAndIndex(node: ts.Node, sourceFile: ts.SourceFile): { - readonly list: ts.Node; - readonly argumentIndex: number; - } | undefined { - if (node.kind === ts.SyntaxKind.LessThanToken || node.kind === ts.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 = ts.findContainingList(node); - return list && { list, argumentIndex: getArgumentIndex(list, node) }; - } + else if (ts.isTemplateHead(node) && parent.parent.kind === ts.SyntaxKind.TaggedTemplateExpression) { + const templateExpression = parent as ts.TemplateExpression; + const tagExpression = templateExpression.parent as ts.TaggedTemplateExpression; + ts.Debug.assert(templateExpression.kind === ts.SyntaxKind.TemplateExpression); + const argumentIndex = ts.isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1; + + return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); } + else if (ts.isTemplateSpan(parent) && ts.isTaggedTemplateExpression(parent.parent.parent)) { + const templateSpan = parent; + const tagExpression = parent.parent.parent; - /** - * 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: ts.Node, position: number, sourceFile: ts.SourceFile): ArgumentListInfo | undefined { - const { parent } = node; - if (ts.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, position, 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 (ts.isNoSubstitutionTemplateLiteral(node) && ts.isTaggedTemplateExpression(parent)) { - // Check if we're actually inside the template; - // otherwise we'll fall out and return undefined. - if (ts.isInsideTemplateLiteral(node, position, sourceFile)) { - return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); - } + // If we're just after a template tail, don't show signature help. + if (ts.isTemplateTail(node) && !ts.isInsideTemplateLiteral(node, position, sourceFile)) { return undefined; } - else if (ts.isTemplateHead(node) && parent.parent.kind === ts.SyntaxKind.TaggedTemplateExpression) { - const templateExpression = parent as ts.TemplateExpression; - const tagExpression = templateExpression.parent as ts.TaggedTemplateExpression; - ts.Debug.assert(templateExpression.kind === ts.SyntaxKind.TemplateExpression); - const argumentIndex = ts.isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1; - return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); - } - else if (ts.isTemplateSpan(parent) && ts.isTaggedTemplateExpression(parent.parent.parent)) { - const templateSpan = parent; - const tagExpression = parent.parent.parent; + const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan); + const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position, sourceFile); - // If we're just after a template tail, don't show signature help. - if (ts.isTemplateTail(node) && !ts.isInsideTemplateLiteral(node, position, sourceFile)) { - return undefined; - } - - const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan); - const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position, sourceFile); - - return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); - } - else if (ts.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 { ... } - // ts.isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s - : s; - } +// 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: ts.Symbol): ts.Symbol { + return s.name === ts.InternalSymbolName.Type + ? ts.firstDefined(s.declarations, d => ts.isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s + : s; +} - function getArgumentIndex(argumentsList: ts.Node, node: ts.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 !== ts.SyntaxKind.CommaToken) { - argumentIndex++; - } +function getArgumentIndex(argumentsList: ts.Node, node: ts.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; } - - return argumentIndex; - } - - function getArgumentCount(argumentsList: ts.Node, ignoreTrailingComma: boolean) { - // 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 = ts.countWhere(listChildren, arg => arg.kind !== ts.SyntaxKind.CommaToken); - if (!ignoreTrailingComma && listChildren.length > 0 && ts.last(listChildren).kind === ts.SyntaxKind.CommaToken) { - argumentCount++; + if (child.kind !== ts.SyntaxKind.CommaToken) { + argumentIndex++; } - return argumentCount; } - // spanIndex is either the index for a given template span. - // This does not give appropriate results for a NoSubstitutionTemplateLiteral - function getArgumentIndexForTemplatePiece(spanIndex: number, node: ts.Node, position: number, sourceFile: ts.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 */ - ts.Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); - if (ts.isTemplateLiteralToken(node)) { - if (ts.isInsideTemplateLiteral(node, position, sourceFile)) { - return 0; - } - return spanIndex + 2; - } - return spanIndex + 1; - } + return argumentIndex; +} + +function getArgumentCount(argumentsList: ts.Node, ignoreTrailingComma: boolean) { + // 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 = ts.countWhere(listChildren, arg => arg.kind !== ts.SyntaxKind.CommaToken); + if (!ignoreTrailingComma && listChildren.length > 0 && ts.last(listChildren).kind === ts.SyntaxKind.CommaToken) { + argumentCount++; + } + return argumentCount; +} - function getArgumentListInfoForTemplate(tagExpression: ts.TaggedTemplateExpression, argumentIndex: number, sourceFile: ts.SourceFile): ArgumentListInfo { - // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. - const argumentCount = ts.isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; - if (argumentIndex !== 0) { - ts.Debug.assertLessThan(argumentIndex, argumentCount); +// spanIndex is either the index for a given template span. +// This does not give appropriate results for a NoSubstitutionTemplateLiteral +function getArgumentIndexForTemplatePiece(spanIndex: number, node: ts.Node, position: number, sourceFile: ts.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 */ + ts.Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); + if (ts.isTemplateLiteralToken(node)) { + if (ts.isInsideTemplateLiteral(node, position, sourceFile)) { + return 0; } - return { - isTypeParameterList: false, - invocation: { kind: InvocationKind.Call, node: tagExpression }, - argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), - argumentIndex, - argumentCount - }; + return spanIndex + 2; } + return spanIndex + 1; +} - function getApplicableSpanForArguments(argumentsList: ts.Node, sourceFile: ts.SourceFile): ts.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 = ts.skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); - return ts.createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); - } +function getArgumentListInfoForTemplate(tagExpression: ts.TaggedTemplateExpression, argumentIndex: number, sourceFile: ts.SourceFile): ArgumentListInfo { + // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. + const argumentCount = ts.isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; + if (argumentIndex !== 0) { + ts.Debug.assertLessThan(argumentIndex, argumentCount); + } + return { + isTypeParameterList: false, + invocation: { kind: InvocationKind.Call, node: tagExpression }, + argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), + argumentIndex, + argumentCount + }; +} - function getApplicableSpanForTaggedTemplate(taggedTemplate: ts.TaggedTemplateExpression, sourceFile: ts.SourceFile): ts.TextSpan { - const template = taggedTemplate.template; - const applicableSpanStart = template.getStart(); - let applicableSpanEnd = template.getEnd(); +function getApplicableSpanForArguments(argumentsList: ts.Node, sourceFile: ts.SourceFile): ts.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 = ts.skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); + return ts.createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); +} - // 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 === ts.SyntaxKind.TemplateExpression) { - const lastSpan = ts.last(template.templateSpans); - if (lastSpan.literal.getFullWidth() === 0) { - applicableSpanEnd = ts.skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); - } +function getApplicableSpanForTaggedTemplate(taggedTemplate: ts.TaggedTemplateExpression, sourceFile: ts.SourceFile): ts.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 === ts.SyntaxKind.TemplateExpression) { + const lastSpan = ts.last(template.templateSpans); + if (lastSpan.literal.getFullWidth() === 0) { + applicableSpanEnd = ts.skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); } - - return ts.createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); } - function getContainingArgumentInfo(node: ts.Node, position: number, sourceFile: ts.SourceFile, checker: ts.TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined { - for (let n = node; !ts.isSourceFile(n) && (isManuallyInvoked || !ts.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. - ts.Debug.assert(ts.rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${ts.Debug.formatSyntaxKind(n.kind)}, parent: ${ts.Debug.formatSyntaxKind(n.parent.kind)}`); - const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker); - if (argumentInfo) { - return argumentInfo; - } - } - return undefined; - } + return ts.createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); +} - function getChildListThatStartsWithOpenerToken(parent: ts.Node, openerToken: ts.Node, sourceFile: ts.SourceFile): ts.Node { - const children = parent.getChildren(sourceFile); - const indexOfOpenerToken = children.indexOf(openerToken); - ts.Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); - return children[indexOfOpenerToken + 1]; +function getContainingArgumentInfo(node: ts.Node, position: number, sourceFile: ts.SourceFile, checker: ts.TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined { + for (let n = node; !ts.isSourceFile(n) && (isManuallyInvoked || !ts.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. + ts.Debug.assert(ts.rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${ts.Debug.formatSyntaxKind(n.kind)}, parent: ${ts.Debug.formatSyntaxKind(n.parent.kind)}`); + const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker); + if (argumentInfo) { + return argumentInfo; + } } + return undefined; +} - function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): ts.Expression { - return invocation.kind === InvocationKind.Call ? ts.getInvokedExpression(invocation.node) : invocation.called; - } +function getChildListThatStartsWithOpenerToken(parent: ts.Node, openerToken: ts.Node, sourceFile: ts.SourceFile): ts.Node { + const children = parent.getChildren(sourceFile); + const indexOfOpenerToken = children.indexOf(openerToken); + ts.Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); + return children[indexOfOpenerToken + 1]; +} - function getEnclosingDeclarationFromInvocation(invocation: Invocation): ts.Node { - return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; - } +function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): ts.Expression { + return invocation.kind === InvocationKind.Call ? ts.getInvokedExpression(invocation.node) : invocation.called; +} - const signatureHelpNodeBuilderFlags = ts.NodeBuilderFlags.OmitParameterModifiers | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; - function createSignatureHelpItems(candidates: readonly ts.Signature[], resolvedSignature: ts.Signature, { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, useFullPrefix?: boolean): ts.SignatureHelpItems { - const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); - const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : (typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)) || useFullPrefix && resolvedSignature.declaration?.symbol); - const callTargetDisplayParts = callTargetSymbol ? ts.symbolToDisplayParts(typeChecker, callTargetSymbol, useFullPrefix ? sourceFile : undefined, /*meaning*/ undefined) : ts.emptyArray; - const items = ts.map(candidates, candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); +function getEnclosingDeclarationFromInvocation(invocation: Invocation): ts.Node { + return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; +} - if (argumentIndex !== 0) { - ts.Debug.assertLessThan(argumentIndex, argumentCount); - } - let selectedItemIndex = 0; - let itemsSeen = 0; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (candidates[i] === resolvedSignature) { - selectedItemIndex = itemsSeen; - if (item.length > 1) { - // check to see if any items in the list better match than the first one, as the checker isn't filtering the nested lists - // (those come from tuple parameter expansion) - let count = 0; - for (const i of item) { - if (i.isVariadic || i.parameters.length >= argumentCount) { - selectedItemIndex = itemsSeen + count; - break; - } - count++; +const signatureHelpNodeBuilderFlags = ts.NodeBuilderFlags.OmitParameterModifiers | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; +function createSignatureHelpItems(candidates: readonly ts.Signature[], resolvedSignature: ts.Signature, { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, useFullPrefix?: boolean): ts.SignatureHelpItems { + const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); + const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : (typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)) || useFullPrefix && resolvedSignature.declaration?.symbol); + const callTargetDisplayParts = callTargetSymbol ? ts.symbolToDisplayParts(typeChecker, callTargetSymbol, useFullPrefix ? sourceFile : undefined, /*meaning*/ undefined) : ts.emptyArray; + const items = ts.map(candidates, candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); + + if (argumentIndex !== 0) { + ts.Debug.assertLessThan(argumentIndex, argumentCount); + } + let selectedItemIndex = 0; + let itemsSeen = 0; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (candidates[i] === resolvedSignature) { + selectedItemIndex = itemsSeen; + if (item.length > 1) { + // check to see if any items in the list better match than the first one, as the checker isn't filtering the nested lists + // (those come from tuple parameter expansion) + let count = 0; + for (const i of item) { + if (i.isVariadic || i.parameters.length >= argumentCount) { + selectedItemIndex = itemsSeen + count; + break; } + count++; } } - itemsSeen += item.length; } + itemsSeen += item.length; + } - ts.Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. - const help = { items: ts.flatMapToMutable(items, ts.identity), applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; - const selected = help.items[selectedItemIndex]; - if (selected.isVariadic) { - const firstRest = ts.findIndex(selected.parameters, p => !!p.isRest); - if (-1 < firstRest && firstRest < selected.parameters.length - 1) { - // We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL - help.argumentIndex = selected.parameters.length; - } - else { - help.argumentIndex = Math.min(help.argumentIndex, selected.parameters.length - 1); - } + ts.Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. + const help = { items: ts.flatMapToMutable(items, ts.identity), applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; + const selected = help.items[selectedItemIndex]; + if (selected.isVariadic) { + const firstRest = ts.findIndex(selected.parameters, p => !!p.isRest); + if (-1 < firstRest && firstRest < selected.parameters.length - 1) { + // We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL + help.argumentIndex = selected.parameters.length; + } + else { + help.argumentIndex = Math.min(help.argumentIndex, selected.parameters.length - 1); } - return help; } + return help; +} - function createTypeHelpItems(symbol: ts.Symbol, { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: ts.SourceFile, checker: ts.TypeChecker): ts.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 createTypeHelpItems(symbol: ts.Symbol, { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: ts.SourceFile, checker: ts.TypeChecker): ts.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: ts.Symbol, typeParameters: readonly ts.TypeParameter[], checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): ts.SignatureHelpItem { - const typeSymbolDisplay = ts.symbolToDisplayParts(checker, symbol); - const printer = ts.createPrinter({ removeComments: true }); - const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); +function getTypeHelpItem(symbol: ts.Symbol, typeParameters: readonly ts.TypeParameter[], checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): ts.SignatureHelpItem { + const typeSymbolDisplay = ts.symbolToDisplayParts(checker, symbol); + const printer = ts.createPrinter({ removeComments: true }); + const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); - const documentation = symbol.getDocumentationComment(checker); - const tags = symbol.getJsDocTags(checker); - const prefixDisplayParts = [...typeSymbolDisplay, ts.punctuationPart(ts.SyntaxKind.LessThanToken)]; - return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [ts.punctuationPart(ts.SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; - } + const documentation = symbol.getDocumentationComment(checker); + const tags = symbol.getJsDocTags(checker); + const prefixDisplayParts = [...typeSymbolDisplay, ts.punctuationPart(ts.SyntaxKind.LessThanToken)]; + return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [ts.punctuationPart(ts.SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; +} - const separatorDisplayParts: ts.SymbolDisplayPart[] = [ts.punctuationPart(ts.SyntaxKind.CommaToken), ts.spacePart()]; - function getSignatureHelpItem(candidateSignature: ts.Signature, callTargetDisplayParts: readonly ts.SymbolDisplayPart[], isTypeParameterList: boolean, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): ts.SignatureHelpItem[] { - const infos = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); - return ts.map(infos, ({ isVariadic, parameters, prefix, suffix }) => { - 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 }; - }); - } +const separatorDisplayParts: ts.SymbolDisplayPart[] = [ts.punctuationPart(ts.SyntaxKind.CommaToken), ts.spacePart()]; +function getSignatureHelpItem(candidateSignature: ts.Signature, callTargetDisplayParts: readonly ts.SymbolDisplayPart[], isTypeParameterList: boolean, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): ts.SignatureHelpItem[] { + const infos = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); + return ts.map(infos, ({ isVariadic, parameters, prefix, suffix }) => { + 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: ts.Signature, enclosingDeclaration: ts.Node, checker: ts.TypeChecker): readonly ts.SymbolDisplayPart[] { - return ts.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); - } - }); - } +function returnTypeToDisplayParts(candidateSignature: ts.Signature, enclosingDeclaration: ts.Node, checker: ts.TypeChecker): readonly ts.SymbolDisplayPart[] { + return ts.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: ts.SignatureHelpParameter[]; - readonly prefix: readonly ts.SymbolDisplayPart[]; - readonly suffix: readonly ts.SymbolDisplayPart[]; - } - function itemInfoForTypeParameters(candidateSignature: ts.Signature, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): SignatureHelpItemInfo[] { - const typeParameters = (candidateSignature.target || candidateSignature).typeParameters; - const printer = ts.createPrinter({ removeComments: true }); - const parameters = (typeParameters || ts.emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); - const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; - - return checker.getExpandedParameters(candidateSignature).map(paramList => { - const params = ts.factory.createNodeArray([...thisParameter, ...ts.map(paramList, param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); - const parameterParts = ts.mapToDisplayParts(writer => { - printer.writeList(ts.ListFormat.CallExpressionArguments, params, sourceFile, writer); - }); - return { isVariadic: false, parameters, prefix: [ts.punctuationPart(ts.SyntaxKind.LessThanToken)], suffix: [ts.punctuationPart(ts.SyntaxKind.GreaterThanToken), ...parameterParts] }; +interface SignatureHelpItemInfo { + readonly isVariadic: boolean; + readonly parameters: ts.SignatureHelpParameter[]; + readonly prefix: readonly ts.SymbolDisplayPart[]; + readonly suffix: readonly ts.SymbolDisplayPart[]; +} +function itemInfoForTypeParameters(candidateSignature: ts.Signature, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): SignatureHelpItemInfo[] { + const typeParameters = (candidateSignature.target || candidateSignature).typeParameters; + const printer = ts.createPrinter({ removeComments: true }); + const parameters = (typeParameters || ts.emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); + const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; + + return checker.getExpandedParameters(candidateSignature).map(paramList => { + const params = ts.factory.createNodeArray([...thisParameter, ...ts.map(paramList, param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); + const parameterParts = ts.mapToDisplayParts(writer => { + printer.writeList(ts.ListFormat.CallExpressionArguments, params, sourceFile, writer); }); - } + return { isVariadic: false, parameters, prefix: [ts.punctuationPart(ts.SyntaxKind.LessThanToken)], suffix: [ts.punctuationPart(ts.SyntaxKind.GreaterThanToken), ...parameterParts] }; + }); +} - function itemInfoForParameters(candidateSignature: ts.Signature, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): SignatureHelpItemInfo[] { - const printer = ts.createPrinter({ removeComments: true }); - const typeParameterParts = ts.mapToDisplayParts(writer => { - if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { - const args = ts.factory.createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)); - printer.writeList(ts.ListFormat.TypeParameters, args, sourceFile, writer); - } - }); - const lists = checker.getExpandedParameters(candidateSignature); - const isVariadic: (parameterList: readonly ts.Symbol[]) => boolean = !checker.hasEffectiveRestParameter(candidateSignature) ? _ => false - : lists.length === 1 ? _ => true - : pList => !!(pList.length && (pList[pList.length - 1] as ts.TransientSymbol).checkFlags & ts.CheckFlags.RestParameter); - return lists.map(parameterList => ({ - isVariadic: isVariadic(parameterList), - parameters: parameterList.map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)), - prefix: [...typeParameterParts, ts.punctuationPart(ts.SyntaxKind.OpenParenToken)], - suffix: [ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] - })); - } +function itemInfoForParameters(candidateSignature: ts.Signature, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile): SignatureHelpItemInfo[] { + const printer = ts.createPrinter({ removeComments: true }); + const typeParameterParts = ts.mapToDisplayParts(writer => { + if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { + const args = ts.factory.createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)); + printer.writeList(ts.ListFormat.TypeParameters, args, sourceFile, writer); + } + }); + const lists = checker.getExpandedParameters(candidateSignature); + const isVariadic: (parameterList: readonly ts.Symbol[]) => boolean = !checker.hasEffectiveRestParameter(candidateSignature) ? _ => false + : lists.length === 1 ? _ => true + : pList => !!(pList.length && (pList[pList.length - 1] as ts.TransientSymbol).checkFlags & ts.CheckFlags.RestParameter); + return lists.map(parameterList => ({ + isVariadic: isVariadic(parameterList), + parameters: parameterList.map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)), + prefix: [...typeParameterParts, ts.punctuationPart(ts.SyntaxKind.OpenParenToken)], + suffix: [ts.punctuationPart(ts.SyntaxKind.CloseParenToken)] + })); +} - function createSignatureHelpParameterForParameter(parameter: ts.Symbol, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile, printer: ts.Printer): ts.SignatureHelpParameter { - const displayParts = ts.mapToDisplayParts(writer => { - const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; - printer.writeNode(ts.EmitHint.Unspecified, param, sourceFile, writer); - }); - const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ts.ParameterDeclaration); - const isRest = !!((parameter as ts.TransientSymbol).checkFlags & ts.CheckFlags.RestParameter); - return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest }; - } +function createSignatureHelpParameterForParameter(parameter: ts.Symbol, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile, printer: ts.Printer): ts.SignatureHelpParameter { + const displayParts = ts.mapToDisplayParts(writer => { + const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; + printer.writeNode(ts.EmitHint.Unspecified, param, sourceFile, writer); + }); + const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ts.ParameterDeclaration); + const isRest = !!((parameter as ts.TransientSymbol).checkFlags & ts.CheckFlags.RestParameter); + return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest }; +} - function createSignatureHelpParameterForTypeParameter(typeParameter: ts.TypeParameter, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile, printer: ts.Printer): ts.SignatureHelpParameter { - const displayParts = ts.mapToDisplayParts(writer => { - const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; - printer.writeNode(ts.EmitHint.Unspecified, param, sourceFile, writer); - }); - return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false }; - } +function createSignatureHelpParameterForTypeParameter(typeParameter: ts.TypeParameter, checker: ts.TypeChecker, enclosingDeclaration: ts.Node, sourceFile: ts.SourceFile, printer: ts.Printer): ts.SignatureHelpParameter { + const displayParts = ts.mapToDisplayParts(writer => { + const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; + printer.writeNode(ts.EmitHint.Unspecified, param, sourceFile, writer); + }); + return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false }; +} } diff --git a/src/services/smartSelection.ts b/src/services/smartSelection.ts index 185f8e622e5fb..009047b795d97 100644 --- a/src/services/smartSelection.ts +++ b/src/services/smartSelection.ts @@ -1,294 +1,294 @@ /* @internal */ namespace ts.SmartSelectionRange { - export function getSmartSelectionRange(pos: number, sourceFile: ts.SourceFile): ts.SelectionRange { - let selectionRange: ts.SelectionRange = { - textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) - }; +export function getSmartSelectionRange(pos: number, sourceFile: ts.SourceFile): ts.SelectionRange { + let selectionRange: ts.SelectionRange = { + textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; - let parentNode: ts.Node = sourceFile; - outer: while (true) { - const children = getSelectionChildren(parentNode); - if (!children.length) - break; - for (let i = 0; i < children.length; i++) { - const prevNode: ts.Node | undefined = children[i - 1]; - const node: ts.Node = children[i]; - const nextNode: ts.Node | undefined = children[i + 1]; - if (ts.getTokenPosOfNode(node, sourceFile, /*includeJsDoc*/ true) > pos) { - break outer; - } - - const comment = ts.singleOrUndefined(ts.getTrailingCommentRanges(sourceFile.text, node.end)); - if (comment && comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { - pushSelectionCommentRange(comment.pos, comment.end); - } - - 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 (ts.isBlock(node) - || ts.isTemplateSpan(node) || ts.isTemplateHead(node) || ts.isTemplateTail(node) - || prevNode && ts.isTemplateHead(prevNode) - || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) - || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) - || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1 - || ts.isJSDocTypeExpression(node) || ts.isJSDocSignature(node) || ts.isJSDocTypeLiteral(node)) { - parentNode = node; - break; - } - - // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. - if (ts.isTemplateSpan(parentNode) && nextNode && ts.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 = ts.isSyntaxList(node) && isListOpener(prevNode) && isListCloser(nextNode) - && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); - const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); - const end = isBetweenMultiLineBookends ? nextNode.getStart() : getEndPos(sourceFile, node); - - if (ts.hasJSDocNodes(node) && node.jsDoc?.length) { - pushSelectionRange(ts.first(node.jsDoc).getStart(), end); - } - - pushSelectionRange(start, end); + let parentNode: ts.Node = sourceFile; + outer: while (true) { + const children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (let i = 0; i < children.length; i++) { + const prevNode: ts.Node | undefined = children[i - 1]; + const node: ts.Node = children[i]; + const nextNode: ts.Node | undefined = children[i + 1]; + if (ts.getTokenPosOfNode(node, sourceFile, /*includeJsDoc*/ true) > pos) { + break outer; + } - // String literals should have a stop both inside and outside their quotes. - if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { - pushSelectionRange(start + 1, end - 1); - } + const comment = ts.singleOrUndefined(ts.getTrailingCommentRanges(sourceFile.text, node.end)); + if (comment && comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { + pushSelectionCommentRange(comment.pos, comment.end); + } + 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 (ts.isBlock(node) + || ts.isTemplateSpan(node) || ts.isTemplateHead(node) || ts.isTemplateTail(node) + || prevNode && ts.isTemplateHead(prevNode) + || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) + || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) + || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1 + || ts.isJSDocTypeExpression(node) || ts.isJSDocSignature(node) || ts.isJSDocTypeLiteral(node)) { 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 (ts.isTemplateSpan(parentNode) && nextNode && ts.isTemplateMiddleOrTemplateTail(nextNode)) { + const start = node.getFullStart() - "${".length; + const end = nextNode.getStart() + "}".length; + pushSelectionRange(start, end); } - } - } - return 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 = ts.isSyntaxList(node) && isListOpener(prevNode) && isListCloser(nextNode) + && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBookends ? nextNode.getStart() : getEndPos(sourceFile, node); - function pushSelectionRange(start: number, end: number): void { - // Skip empty ranges - if (start !== end) { - const textSpan = ts.createTextSpanFromBounds(start, end); - if (!selectionRange || ( - // Skip ranges that are identical to the parent - !ts.textSpansEqual(textSpan, selectionRange.textSpan) && - // Skip ranges that don’t contain the original position - ts.textSpanIntersectsWithPosition(textSpan, pos))) { - selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + if (ts.hasJSDocNodes(node) && node.jsDoc?.length) { + pushSelectionRange(ts.first(node.jsDoc).getStart(), end); } - } - } - function pushSelectionCommentRange(start: number, end: number): void { - pushSelectionRange(start, end); + pushSelectionRange(start, end); + + // String literals should have a stop both inside and outside their quotes. + if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + + parentNode = node; + break; + } - let pos = start; - while (sourceFile.text.charCodeAt(pos) === ts.CharacterCodes.slash) { - pos++; + // 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; } - pushSelectionRange(pos, end); } } - /** - * 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: ts.SourceFile, pos: number, node: ts.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. - ts.Debug.assert(node.pos <= pos); - if (pos < node.end) { - return true; - } - const nodeEnd = node.getEnd(); - if (nodeEnd === pos) { - return ts.getTouchingPropertyName(sourceFile, pos).pos < node.end; + return selectionRange; + + function pushSelectionRange(start: number, end: number): void { + // Skip empty ranges + if (start !== end) { + const textSpan = ts.createTextSpanFromBounds(start, end); + if (!selectionRange || ( + // Skip ranges that are identical to the parent + !ts.textSpansEqual(textSpan, selectionRange.textSpan) && + // Skip ranges that don’t contain the original position + ts.textSpanIntersectsWithPosition(textSpan, pos))) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + } } - return false; } - const isImport = ts.or(ts.isImportDeclaration, ts.isImportEqualsDeclaration); + function pushSelectionCommentRange(start: number, end: number): void { + pushSelectionRange(start, end); - /** - * 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: ts.Node): readonly ts.Node[] { - // Group top-level imports - if (ts.isSourceFile(node)) { - return groupChildren(node.getChildAt(0).getChildren(), isImport); + let pos = start; + while (sourceFile.text.charCodeAt(pos) === ts.CharacterCodes.slash) { + pos++; } + pushSelectionRange(pos, end); + } +} - // 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 (ts.isMappedTypeNode(node)) { - const [openBraceToken, ...children] = node.getChildren(); - const closeBraceToken = ts.Debug.checkDefined(children.pop()); - ts.Debug.assertEqual(openBraceToken.kind, ts.SyntaxKind.OpenBraceToken); - ts.Debug.assertEqual(closeBraceToken.kind, ts.SyntaxKind.CloseBraceToken); - // Group `-/+readonly` and `-/+?` - const groupedWithPlusMinusTokens = groupChildren(children, child => child === node.readonlyToken || child.kind === ts.SyntaxKind.ReadonlyKeyword || - child === node.questionToken || child.kind === ts.SyntaxKind.QuestionToken); - // Group type parameter with surrounding brackets - const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => kind === ts.SyntaxKind.OpenBracketToken || - kind === ts.SyntaxKind.TypeParameter || - kind === ts.SyntaxKind.CloseBracketToken); - return [ - openBraceToken, - // Pivot on `:` - createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === ts.SyntaxKind.ColonToken)), - closeBraceToken, - ]; - } +/** + * 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: ts.SourceFile, pos: number, node: ts.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. + ts.Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + const nodeEnd = node.getEnd(); + if (nodeEnd === pos) { + return ts.getTouchingPropertyName(sourceFile, pos).pos < node.end; + } + return false; +} - // Group modifiers and property name, then pivot on `:`. - if (ts.isPropertySignature(node)) { - const children = groupChildren(node.getChildren(), child => child === node.name || ts.contains(node.modifiers, child)); - return splitChildren(children, ({ kind }) => kind === ts.SyntaxKind.ColonToken); - } +const isImport = ts.or(ts.isImportDeclaration, ts.isImportEqualsDeclaration); - // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. - if (ts.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 === ts.SyntaxKind.EqualsToken); - } +/** + * 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: ts.Node): readonly ts.Node[] { + // Group top-level imports + if (ts.isSourceFile(node)) { + return groupChildren(node.getChildAt(0).getChildren(), isImport); + } - // Pivot on '=' - if (ts.isBindingElement(node)) { - return splitChildren(node.getChildren(), ({ kind }) => kind === ts.SyntaxKind.EqualsToken); - } + // 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 (ts.isMappedTypeNode(node)) { + const [openBraceToken, ...children] = node.getChildren(); + const closeBraceToken = ts.Debug.checkDefined(children.pop()); + ts.Debug.assertEqual(openBraceToken.kind, ts.SyntaxKind.OpenBraceToken); + ts.Debug.assertEqual(closeBraceToken.kind, ts.SyntaxKind.CloseBraceToken); + // Group `-/+readonly` and `-/+?` + const groupedWithPlusMinusTokens = groupChildren(children, child => child === node.readonlyToken || child.kind === ts.SyntaxKind.ReadonlyKeyword || + child === node.questionToken || child.kind === ts.SyntaxKind.QuestionToken); + // Group type parameter with surrounding brackets + const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => kind === ts.SyntaxKind.OpenBracketToken || + kind === ts.SyntaxKind.TypeParameter || + kind === ts.SyntaxKind.CloseBracketToken); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === ts.SyntaxKind.ColonToken)), + closeBraceToken, + ]; + } - return node.getChildren(); + // Group modifiers and property name, then pivot on `:`. + if (ts.isPropertySignature(node)) { + const children = groupChildren(node.getChildren(), child => child === node.name || ts.contains(node.modifiers, child)); + return splitChildren(children, ({ kind }) => kind === ts.SyntaxKind.ColonToken); } - /** - * Groups sibling nodes together into their own SyntaxList if they - * a) are adjacent, AND b) match a predicate function. - */ - function groupChildren(children: ts.Node[], groupOn: (child: ts.Node) => boolean): ts.Node[] { - const result: ts.Node[] = []; - let group: ts.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)); - } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (ts.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 === ts.SyntaxKind.EqualsToken); + } - return result; + // Pivot on '=' + if (ts.isBindingElement(node)) { + return splitChildren(node.getChildren(), ({ kind }) => kind === ts.SyntaxKind.EqualsToken); } - /** - * 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: ts.Node[], pivotOn: (child: ts.Node) => boolean, separateTrailingSemicolon = true): ts.Node[] { - if (children.length < 2) { - return children; + return node.getChildren(); +} + +/** + * Groups sibling nodes together into their own SyntaxList if they + * a) are adjacent, AND b) match a predicate function. + */ +function groupChildren(children: ts.Node[], groupOn: (child: ts.Node) => boolean): ts.Node[] { + const result: ts.Node[] = []; + let group: ts.Node[] | undefined; + for (const child of children) { + if (groupOn(child)) { + group = group || []; + group.push(child); } - const splitTokenIndex = ts.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 = ts.last(children); - const separateLastToken = separateTrailingSemicolon && lastToken.kind === ts.SyntaxKind.SemicolonToken; - const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); - const result = ts.compact([ - leftChildren.length ? createSyntaxList(leftChildren) : undefined, - splitToken, - rightChildren.length ? createSyntaxList(rightChildren) : undefined, - ]); - return separateLastToken ? result.concat(lastToken) : result; } - - function createSyntaxList(children: ts.Node[]): ts.SyntaxList { - ts.Debug.assertGreaterThanOrEqual(children.length, 1); - return ts.setTextRangePosEnd(ts.parseNodeFactory.createSyntaxList(children), children[0].pos, ts.last(children).end); + if (group) { + result.push(createSyntaxList(group)); } - function isListOpener(token: ts.Node | undefined): token is ts.Node { - const kind = token && token.kind; - return kind === ts.SyntaxKind.OpenBraceToken - || kind === ts.SyntaxKind.OpenBracketToken - || kind === ts.SyntaxKind.OpenParenToken - || kind === ts.SyntaxKind.JsxOpeningElement; - } + return result; +} - function isListCloser(token: ts.Node | undefined): token is ts.Node { - const kind = token && token.kind; - return kind === ts.SyntaxKind.CloseBraceToken - || kind === ts.SyntaxKind.CloseBracketToken - || kind === ts.SyntaxKind.CloseParenToken - || kind === ts.SyntaxKind.JsxClosingElement; +/** + * 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: ts.Node[], pivotOn: (child: ts.Node) => boolean, separateTrailingSemicolon = true): ts.Node[] { + if (children.length < 2) { + return children; } + const splitTokenIndex = ts.findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; + } + const leftChildren = children.slice(0, splitTokenIndex); + const splitToken = children[splitTokenIndex]; + const lastToken = ts.last(children); + const separateLastToken = separateTrailingSemicolon && lastToken.kind === ts.SyntaxKind.SemicolonToken; + const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + const result = ts.compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; +} - function getEndPos(sourceFile: ts.SourceFile, node: ts.Node): number { - switch (node.kind) { - case ts.SyntaxKind.JSDocParameterTag: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocPropertyTag: - case ts.SyntaxKind.JSDocTypedefTag: - case ts.SyntaxKind.JSDocThisTag: - return sourceFile.getLineEndOfPosition(node.getStart()); - default: - return node.getEnd(); - } +function createSyntaxList(children: ts.Node[]): ts.SyntaxList { + ts.Debug.assertGreaterThanOrEqual(children.length, 1); + return ts.setTextRangePosEnd(ts.parseNodeFactory.createSyntaxList(children), children[0].pos, ts.last(children).end); +} + +function isListOpener(token: ts.Node | undefined): token is ts.Node { + const kind = token && token.kind; + return kind === ts.SyntaxKind.OpenBraceToken + || kind === ts.SyntaxKind.OpenBracketToken + || kind === ts.SyntaxKind.OpenParenToken + || kind === ts.SyntaxKind.JsxOpeningElement; +} + +function isListCloser(token: ts.Node | undefined): token is ts.Node { + const kind = token && token.kind; + return kind === ts.SyntaxKind.CloseBraceToken + || kind === ts.SyntaxKind.CloseBracketToken + || kind === ts.SyntaxKind.CloseParenToken + || kind === ts.SyntaxKind.JsxClosingElement; +} + +function getEndPos(sourceFile: ts.SourceFile, node: ts.Node): number { + switch (node.kind) { + case ts.SyntaxKind.JSDocParameterTag: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocPropertyTag: + case ts.SyntaxKind.JSDocTypedefTag: + case ts.SyntaxKind.JSDocThisTag: + return sourceFile.getLineEndOfPosition(node.getStart()); + default: + return node.getEnd(); } } +} diff --git a/src/services/sourcemaps.ts b/src/services/sourcemaps.ts index 1a77ee0e344d5..24a4a1418f276 100644 --- a/src/services/sourcemaps.ts +++ b/src/services/sourcemaps.ts @@ -1,195 +1,195 @@ /* @internal */ namespace ts { - const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; +const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; - export interface SourceMapper { - toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter; - tryGetSourcePosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined; - tryGetGeneratedPosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined; - clearCache(): void; - } +export interface SourceMapper { + toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter; + tryGetSourcePosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined; + tryGetGeneratedPosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined; + clearCache(): void; +} - export interface SourceMapperHost { - useCaseSensitiveFileNames(): boolean; - getCurrentDirectory(): string; - getProgram(): ts.Program | undefined; - fileExists?(path: string): boolean; - readFile?(path: string, encoding?: string): string | undefined; - getSourceFileLike?(fileName: string): ts.SourceFileLike | undefined; - getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): ts.DocumentPositionMapper | undefined; - log(s: string): void; - } +export interface SourceMapperHost { + useCaseSensitiveFileNames(): boolean; + getCurrentDirectory(): string; + getProgram(): ts.Program | undefined; + fileExists?(path: string): boolean; + readFile?(path: string, encoding?: string): string | undefined; + getSourceFileLike?(fileName: string): ts.SourceFileLike | undefined; + getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): ts.DocumentPositionMapper | undefined; + log(s: string): void; +} - export function getSourceMapper(host: SourceMapperHost): SourceMapper { - const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - const currentDirectory = host.getCurrentDirectory(); - const sourceFileLike = new ts.Map(); - const documentPositionMappers = new ts.Map(); - return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; +export function getSourceMapper(host: SourceMapperHost): SourceMapper { + const getCanonicalFileName = ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + const currentDirectory = host.getCurrentDirectory(); + const sourceFileLike = new ts.Map(); + const documentPositionMappers = new ts.Map(); + return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } + 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: ts.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, ts.getLineInfo(file.text, ts.getLineStarts(file)), f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined); - } - documentPositionMappers.set(path, mapper || ts.identitySourceMapConsumer); - return mapper || ts.identitySourceMapConsumer; + function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) { + const path = toPath(generatedFileName); + const value = documentPositionMappers.get(path); + if (value) + return value; + let mapper: ts.DocumentPositionMapper | undefined; + if (host.getDocumentPositionMapper) { + mapper = host.getDocumentPositionMapper(generatedFileName, sourceFileName); } - function tryGetSourcePosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined { - if (!ts.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; + else if (host.readFile) { + const file = getSourceFileLike(generatedFileName); + mapper = file && ts.getDocumentPositionMapper({ getSourceFileLike, getCanonicalFileName, log: s => host.log(s) }, generatedFileName, ts.getLineInfo(file.text, ts.getLineStarts(file)), f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined); } + documentPositionMappers.set(path, mapper || ts.identitySourceMapConsumer); + return mapper || ts.identitySourceMapConsumer; + } + function tryGetSourcePosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined { + if (!ts.isDeclarationFileName(info.fileName)) + return undefined; - function tryGetGeneratedPosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined { - if (ts.isDeclarationFileName(info.fileName)) - return undefined; - - const sourceFile = getSourceFile(info.fileName); - if (!sourceFile) - return undefined; + const file = getSourceFile(info.fileName); + if (!file) + 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 newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); + return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; + } - const options = program.getCompilerOptions(); - const outPath = ts.outFile(options); + function tryGetGeneratedPosition(info: ts.DocumentPosition): ts.DocumentPosition | undefined { + if (ts.isDeclarationFileName(info.fileName)) + return undefined; - const declarationPath = outPath ? - ts.removeFileExtension(outPath) + ts.Extension.Dts : - ts.getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); - if (declarationPath === undefined) - return undefined; + const sourceFile = getSourceFile(info.fileName); + if (!sourceFile) + return undefined; - const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); - return newLoc === info ? undefined : newLoc; + 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 getSourceFile(fileName: string) { - const program = host.getProgram(); - if (!program) - return undefined; + const options = program.getCompilerOptions(); + const outPath = ts.outFile(options); - 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; - } + const declarationPath = outPath ? + ts.removeFileExtension(outPath) + ts.Extension.Dts : + ts.getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); + if (declarationPath === undefined) + return undefined; - function getOrCreateSourceFileLike(fileName: string): ts.SourceFileLike | undefined { - const path = toPath(fileName); - const fileFromCache = sourceFileLike.get(path); - if (fileFromCache !== undefined) - return fileFromCache ? fileFromCache : undefined; + const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); + return newLoc === info ? undefined : newLoc; + } - if (!host.readFile || host.fileExists && !host.fileExists(path)) { - sourceFileLike.set(path, false); - return undefined; - } + function getSourceFile(fileName: string) { + const program = host.getProgram(); + if (!program) + 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; - } + 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; + } - // 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 getOrCreateSourceFileLike(fileName: string): ts.SourceFileLike | undefined { + const path = toPath(fileName); + const fileFromCache = sourceFileLike.get(path); + if (fileFromCache !== undefined) + return fileFromCache ? fileFromCache : undefined; - function toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter { - const file = getSourceFileLike(fileName)!; // TODO: GH#18217 - return file.getLineAndCharacterOfPosition(position); + if (!host.readFile || host.fileExists && !host.fileExists(path)) { + sourceFileLike.set(path, false); + return undefined; } - function clearCache(): void { - sourceFileLike.clear(); - documentPositionMappers.clear(); - } + // 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): ts.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 | ts.DocumentPositionMapper | false; - export function getDocumentPositionMapper(host: ts.DocumentPositionMapperHost, generatedFileName: string, generatedFileLineInfo: ts.LineInfo, readMapFile: ReadMapFile) { - let mapFileName = ts.tryGetSourceMappingURL(generatedFileLineInfo); - if (mapFileName) { - const match = base64UrlRegExp.exec(mapFileName); - if (match) { - if (match[1]) { - const base64Object = match[1]; - return convertDocumentToSourceMapper(host, ts.base64decode(ts.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 + */ +export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | ts.DocumentPositionMapper | false; +export function getDocumentPositionMapper(host: ts.DocumentPositionMapperHost, generatedFileName: string, generatedFileLineInfo: ts.LineInfo, readMapFile: ReadMapFile) { + let mapFileName = ts.tryGetSourceMappingURL(generatedFileLineInfo); + if (mapFileName) { + const match = base64UrlRegExp.exec(mapFileName); + if (match) { + if (match[1]) { + const base64Object = match[1]; + return convertDocumentToSourceMapper(host, ts.base64decode(ts.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 && ts.getNormalizedAbsolutePath(mapFileName, ts.getDirectoryPath(generatedFileName)); + for (const location of possibleMapLocations) { + const mapFileName = ts.getNormalizedAbsolutePath(location, ts.getDirectoryPath(generatedFileName)); + const mapFileContents = readMapFile(mapFileName, originalMapFileName); + if (ts.isString(mapFileContents)) { + return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); } - possibleMapLocations.push(generatedFileName + ".map"); - const originalMapFileName = mapFileName && ts.getNormalizedAbsolutePath(mapFileName, ts.getDirectoryPath(generatedFileName)); - for (const location of possibleMapLocations) { - const mapFileName = ts.getNormalizedAbsolutePath(location, ts.getDirectoryPath(generatedFileName)); - const mapFileContents = readMapFile(mapFileName, originalMapFileName); - if (ts.isString(mapFileContents)) { - return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); - } - if (mapFileContents !== undefined) { - return mapFileContents || undefined; - } + if (mapFileContents !== undefined) { + return mapFileContents || undefined; } - return undefined; } + return undefined; +} - function convertDocumentToSourceMapper(host: ts.DocumentPositionMapperHost, contents: string, mapFileName: string) { - const map = ts.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(ts.isString)) - return undefined; - return ts.createDocumentPositionMapper(host, map, mapFileName); +function convertDocumentToSourceMapper(host: ts.DocumentPositionMapperHost, contents: string, mapFileName: string) { + const map = ts.tryParseRawSourceMap(contents); + if (!map || !map.sources || !map.file || !map.mappings) { + // obviously invalid map + return undefined; } - function createSourceFileLike(text: string, lineMap?: ts.SourceFileLike["lineMap"]): ts.SourceFileLike { - return { - text, - lineMap, - getLineAndCharacterOfPosition(pos: number) { - return ts.computeLineAndCharacterOfPosition(ts.getLineStarts(this), pos); - } - }; - } + // Dont support sourcemaps that contain inlined sources + if (map.sourcesContent && map.sourcesContent.some(ts.isString)) + return undefined; + return ts.createDocumentPositionMapper(host, map, mapFileName); +} + +function createSourceFileLike(text: string, lineMap?: ts.SourceFileLike["lineMap"]): ts.SourceFileLike { + return { + text, + lineMap, + getLineAndCharacterOfPosition(pos: number) { + return ts.computeLineAndCharacterOfPosition(ts.getLineStarts(this), pos); + } + }; +} } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index e99ceddc3a56d..15e03bf63852c 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -1,848 +1,848 @@ /* @internal */ namespace ts.Completions.StringCompletions { - export function getStringLiteralCompletions(sourceFile: ts.SourceFile, position: number, contextToken: ts.Node | undefined, options: ts.CompilerOptions, host: ts.LanguageServiceHost, program: ts.Program, log: ts.Completions.Log, preferences: ts.UserPreferences): ts.CompletionInfo | undefined { - if (ts.isInReferenceComment(sourceFile, position)) { - const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host); - return entries && convertPathCompletions(entries); - } - if (ts.isInString(sourceFile, position, contextToken)) { - if (!contextToken || !ts.isStringLiteralLike(contextToken)) - return undefined; - const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program.getTypeChecker(), options, host, preferences); - return convertStringLiteralCompletions(entries, contextToken, sourceFile, host, program, log, options, preferences); - } +export function getStringLiteralCompletions(sourceFile: ts.SourceFile, position: number, contextToken: ts.Node | undefined, options: ts.CompilerOptions, host: ts.LanguageServiceHost, program: ts.Program, log: ts.Completions.Log, preferences: ts.UserPreferences): ts.CompletionInfo | undefined { + if (ts.isInReferenceComment(sourceFile, position)) { + const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host); + return entries && convertPathCompletions(entries); } - - function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: ts.StringLiteralLike, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, log: ts.Completions.Log, options: ts.CompilerOptions, preferences: ts.UserPreferences): ts.CompletionInfo | undefined { - if (completion === undefined) { + if (ts.isInString(sourceFile, position, contextToken)) { + if (!contextToken || !ts.isStringLiteralLike(contextToken)) return undefined; - } - - const optionalReplacementSpan = ts.createTextSpanFromStringLiteralLikeContent(contextToken); - switch (completion.kind) { - case StringLiteralCompletionKind.Paths: - return convertPathCompletions(completion.paths); - case StringLiteralCompletionKind.Properties: { - const entries = ts.createSortedArray(); - ts.Completions.getCompletionEntriesFromSymbols(completion.symbols, entries, contextToken, contextToken, sourceFile, sourceFile, host, program, ts.ScriptTarget.ESNext, log, ts.Completions.CompletionKind.String, preferences, options, - /*formatContext*/ undefined); // Target will not be used, so arbitrary - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries }; - } - case StringLiteralCompletionKind.Types: { - const entries = completion.types.map(type => ({ - name: type.value, - kindModifiers: ts.ScriptElementKindModifier.none, - kind: ts.ScriptElementKind.string, - sortText: ts.Completions.SortText.LocationPriority, - replacementSpan: ts.getReplacementSpanForContextToken(contextToken) - })); - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries }; - } - default: - return ts.Debug.assertNever(completion); - } + const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program.getTypeChecker(), options, host, preferences); + return convertStringLiteralCompletions(entries, contextToken, sourceFile, host, program, log, options, preferences); } +} - export function getStringLiteralCompletionDetails(name: string, sourceFile: ts.SourceFile, position: number, contextToken: ts.Node | undefined, checker: ts.TypeChecker, options: ts.CompilerOptions, host: ts.LanguageServiceHost, cancellationToken: ts.CancellationToken, preferences: ts.UserPreferences) { - if (!contextToken || !ts.isStringLiteralLike(contextToken)) - return undefined; - const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host, preferences); - return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken); +function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: ts.StringLiteralLike, sourceFile: ts.SourceFile, host: ts.LanguageServiceHost, program: ts.Program, log: ts.Completions.Log, options: ts.CompilerOptions, preferences: ts.UserPreferences): ts.CompletionInfo | undefined { + if (completion === undefined) { + return undefined; + } + + const optionalReplacementSpan = ts.createTextSpanFromStringLiteralLikeContent(contextToken); + switch (completion.kind) { + case StringLiteralCompletionKind.Paths: + return convertPathCompletions(completion.paths); + case StringLiteralCompletionKind.Properties: { + const entries = ts.createSortedArray(); + ts.Completions.getCompletionEntriesFromSymbols(completion.symbols, entries, contextToken, contextToken, sourceFile, sourceFile, host, program, ts.ScriptTarget.ESNext, log, ts.Completions.CompletionKind.String, preferences, options, + /*formatContext*/ undefined); // Target will not be used, so arbitrary + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries }; + } + case StringLiteralCompletionKind.Types: { + const entries = completion.types.map(type => ({ + name: type.value, + kindModifiers: ts.ScriptElementKindModifier.none, + kind: ts.ScriptElementKind.string, + sortText: ts.Completions.SortText.LocationPriority, + replacementSpan: ts.getReplacementSpanForContextToken(contextToken) + })); + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries }; + } + default: + return ts.Debug.assertNever(completion); } +} - function stringLiteralCompletionDetails(name: string, location: ts.Node, completion: StringLiteralCompletion, sourceFile: ts.SourceFile, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): ts.CompletionEntryDetails | undefined { - switch (completion.kind) { - case StringLiteralCompletionKind.Paths: { - const match = ts.find(completion.paths, p => p.name === name); - return match && ts.Completions.createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [ts.textPart(name)]); - } - case StringLiteralCompletionKind.Properties: { - const match = ts.find(completion.symbols, s => s.name === name); - return match && ts.Completions.createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken); - } - case StringLiteralCompletionKind.Types: - return ts.find(completion.types, t => t.value === name) ? ts.Completions.createCompletionDetails(name, ts.ScriptElementKindModifier.none, ts.ScriptElementKind.typeElement, [ts.textPart(name)]) : undefined; - default: - return ts.Debug.assertNever(completion); +export function getStringLiteralCompletionDetails(name: string, sourceFile: ts.SourceFile, position: number, contextToken: ts.Node | undefined, checker: ts.TypeChecker, options: ts.CompilerOptions, host: ts.LanguageServiceHost, cancellationToken: ts.CancellationToken, preferences: ts.UserPreferences) { + if (!contextToken || !ts.isStringLiteralLike(contextToken)) + return undefined; + const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host, preferences); + return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken); +} + +function stringLiteralCompletionDetails(name: string, location: ts.Node, completion: StringLiteralCompletion, sourceFile: ts.SourceFile, checker: ts.TypeChecker, cancellationToken: ts.CancellationToken): ts.CompletionEntryDetails | undefined { + switch (completion.kind) { + case StringLiteralCompletionKind.Paths: { + const match = ts.find(completion.paths, p => p.name === name); + return match && ts.Completions.createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [ts.textPart(name)]); + } + case StringLiteralCompletionKind.Properties: { + const match = ts.find(completion.symbols, s => s.name === name); + return match && ts.Completions.createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken); } + case StringLiteralCompletionKind.Types: + return ts.find(completion.types, t => t.value === name) ? ts.Completions.createCompletionDetails(name, ts.ScriptElementKindModifier.none, ts.ScriptElementKind.typeElement, [ts.textPart(name)]) : undefined; + default: + return ts.Debug.assertNever(completion); } +} - function convertPathCompletions(pathCompletions: readonly PathCompletion[]): ts.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 }): ts.CompletionEntry => ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: ts.Completions.SortText.LocationPriority, replacementSpan: span })); - return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries }; - } - function kindModifiersFromExtension(extension: ts.Extension | undefined): ts.ScriptElementKindModifier { - switch (extension) { - case ts.Extension.Dts: return ts.ScriptElementKindModifier.dtsModifier; - case ts.Extension.Js: return ts.ScriptElementKindModifier.jsModifier; - case ts.Extension.Json: return ts.ScriptElementKindModifier.jsonModifier; - case ts.Extension.Jsx: return ts.ScriptElementKindModifier.jsxModifier; - case ts.Extension.Ts: return ts.ScriptElementKindModifier.tsModifier; - case ts.Extension.Tsx: return ts.ScriptElementKindModifier.tsxModifier; - case ts.Extension.Dmts: return ts.ScriptElementKindModifier.dmtsModifier; - case ts.Extension.Mjs: return ts.ScriptElementKindModifier.mjsModifier; - case ts.Extension.Mts: return ts.ScriptElementKindModifier.mtsModifier; - case ts.Extension.Dcts: return ts.ScriptElementKindModifier.dctsModifier; - case ts.Extension.Cjs: return ts.ScriptElementKindModifier.cjsModifier; - case ts.Extension.Cts: return ts.ScriptElementKindModifier.ctsModifier; - case ts.Extension.TsBuildInfo: return ts.Debug.fail(`Extension ${ts.Extension.TsBuildInfo} is unsupported.`); - case undefined: return ts.ScriptElementKindModifier.none; - default: - return ts.Debug.assertNever(extension); - } +function convertPathCompletions(pathCompletions: readonly PathCompletion[]): ts.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 }): ts.CompletionEntry => ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: ts.Completions.SortText.LocationPriority, replacementSpan: span })); + return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries }; +} +function kindModifiersFromExtension(extension: ts.Extension | undefined): ts.ScriptElementKindModifier { + switch (extension) { + case ts.Extension.Dts: return ts.ScriptElementKindModifier.dtsModifier; + case ts.Extension.Js: return ts.ScriptElementKindModifier.jsModifier; + case ts.Extension.Json: return ts.ScriptElementKindModifier.jsonModifier; + case ts.Extension.Jsx: return ts.ScriptElementKindModifier.jsxModifier; + case ts.Extension.Ts: return ts.ScriptElementKindModifier.tsModifier; + case ts.Extension.Tsx: return ts.ScriptElementKindModifier.tsxModifier; + case ts.Extension.Dmts: return ts.ScriptElementKindModifier.dmtsModifier; + case ts.Extension.Mjs: return ts.ScriptElementKindModifier.mjsModifier; + case ts.Extension.Mts: return ts.ScriptElementKindModifier.mtsModifier; + case ts.Extension.Dcts: return ts.ScriptElementKindModifier.dctsModifier; + case ts.Extension.Cjs: return ts.ScriptElementKindModifier.cjsModifier; + case ts.Extension.Cts: return ts.ScriptElementKindModifier.ctsModifier; + case ts.Extension.TsBuildInfo: return ts.Debug.fail(`Extension ${ts.Extension.TsBuildInfo} is unsupported.`); + case undefined: return ts.ScriptElementKindModifier.none; + default: + return ts.Debug.assertNever(extension); } +} - const enum StringLiteralCompletionKind { - Paths, - Properties, - Types - } - interface StringLiteralCompletionsFromProperties { - readonly kind: StringLiteralCompletionKind.Properties; - readonly symbols: readonly ts.Symbol[]; - readonly hasIndexSignature: boolean; - } - interface StringLiteralCompletionsFromTypes { - readonly kind: StringLiteralCompletionKind.Types; - readonly types: readonly ts.StringLiteralType[]; - readonly isNewIdentifier: boolean; - } - type StringLiteralCompletion = { - readonly kind: StringLiteralCompletionKind.Paths; - readonly paths: readonly PathCompletion[]; - } | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes; - function getStringLiteralCompletionEntries(sourceFile: ts.SourceFile, node: ts.StringLiteralLike, position: number, typeChecker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): StringLiteralCompletion | undefined { - const parent = walkUpParentheses(node.parent); - switch (parent.kind) { - case ts.SyntaxKind.LiteralType: { - const grandParent = walkUpParentheses(parent.parent); - switch (grandParent.kind) { - case ts.SyntaxKind.TypeReference: { - const typeReference = grandParent as ts.TypeReferenceNode; - const typeArgument = ts.findAncestor(parent, n => n.parent === typeReference) as ts.LiteralTypeNode; - if (typeArgument) { - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false }; - } - return undefined; +const enum StringLiteralCompletionKind { + Paths, + Properties, + Types +} +interface StringLiteralCompletionsFromProperties { + readonly kind: StringLiteralCompletionKind.Properties; + readonly symbols: readonly ts.Symbol[]; + readonly hasIndexSignature: boolean; +} +interface StringLiteralCompletionsFromTypes { + readonly kind: StringLiteralCompletionKind.Types; + readonly types: readonly ts.StringLiteralType[]; + readonly isNewIdentifier: boolean; +} +type StringLiteralCompletion = { + readonly kind: StringLiteralCompletionKind.Paths; + readonly paths: readonly PathCompletion[]; +} | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes; +function getStringLiteralCompletionEntries(sourceFile: ts.SourceFile, node: ts.StringLiteralLike, position: number, typeChecker: ts.TypeChecker, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, preferences: ts.UserPreferences): StringLiteralCompletion | undefined { + const parent = walkUpParentheses(node.parent); + switch (parent.kind) { + case ts.SyntaxKind.LiteralType: { + const grandParent = walkUpParentheses(parent.parent); + switch (grandParent.kind) { + case ts.SyntaxKind.TypeReference: { + const typeReference = grandParent as ts.TypeReferenceNode; + const typeArgument = ts.findAncestor(parent, n => n.parent === typeReference) as ts.LiteralTypeNode; + if (typeArgument) { + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false }; } - case ts.SyntaxKind.IndexedAccessType: - // Get all apparent property names - // i.e. interface Foo { - // foo: string; - // bar: string; - // } - // let x: Foo["/*completion position*/"] - const { indexType, objectType } = grandParent as ts.IndexedAccessTypeNode; - if (!ts.rangeContainsPosition(indexType, position)) { - return undefined; - } - return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType)); - case ts.SyntaxKind.ImportType: - return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker, preferences) }; - case ts.SyntaxKind.UnionType: { - if (!ts.isTypeReferenceNode(grandParent.parent)) { - return undefined; - } - const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(grandParent as ts.UnionTypeNode, parent as ts.LiteralTypeNode); - const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(grandParent as ts.UnionTypeNode)).filter(t => !ts.contains(alreadyUsedTypes, t.value)); - return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false }; + return undefined; + } + case ts.SyntaxKind.IndexedAccessType: + // Get all apparent property names + // i.e. interface Foo { + // foo: string; + // bar: string; + // } + // let x: Foo["/*completion position*/"] + const { indexType, objectType } = grandParent as ts.IndexedAccessTypeNode; + if (!ts.rangeContainsPosition(indexType, position)) { + return undefined; } - default: + return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType)); + case ts.SyntaxKind.ImportType: + return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker, preferences) }; + case ts.SyntaxKind.UnionType: { + if (!ts.isTypeReferenceNode(grandParent.parent)) { return undefined; + } + const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(grandParent as ts.UnionTypeNode, parent as ts.LiteralTypeNode); + const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(grandParent as ts.UnionTypeNode)).filter(t => !ts.contains(alreadyUsedTypes, t.value)); + return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false }; } + default: + return undefined; } - case ts.SyntaxKind.PropertyAssignment: - if (ts.isObjectLiteralExpression(parent.parent) && (parent as ts.PropertyAssignment).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 stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent); - } - return fromContextualType(); - - case ts.SyntaxKind.ElementAccessExpression: { - const { expression, argumentExpression } = parent as ts.ElementAccessExpression; - if (node === ts.skipParentheses(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 ts.SyntaxKind.PropertyAssignment: + if (ts.isObjectLiteralExpression(parent.parent) && (parent as ts.PropertyAssignment).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 stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent); } - - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.NewExpression: - case ts.SyntaxKind.JsxAttribute: - if (!isRequireCallArgument(node) && !ts.isImportCall(parent)) { - const argumentInfo = ts.SignatureHelp.getArgumentInfoForCompletions(parent.kind === ts.SyntaxKind.JsxAttribute ? parent.parent : 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.invocation, node, argumentInfo, typeChecker) : fromContextualType(); - } - // falls through (is `require("")` or `require(""` or `import("")`) - - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ExportDeclaration: - case ts.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, preferences) }; - - default: - return fromContextualType(); + return fromContextualType(); + + case ts.SyntaxKind.ElementAccessExpression: { + const { expression, argumentExpression } = parent as ts.ElementAccessExpression; + if (node === ts.skipParentheses(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; } - 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(ts.getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; - } - } + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + case ts.SyntaxKind.JsxAttribute: + if (!isRequireCallArgument(node) && !ts.isImportCall(parent)) { + const argumentInfo = ts.SignatureHelp.getArgumentInfoForCompletions(parent.kind === ts.SyntaxKind.JsxAttribute ? parent.parent : 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.invocation, node, argumentInfo, typeChecker) : fromContextualType(); + } + // falls through (is `require("")` or `require(""` or `import("")`) - function walkUpParentheses(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.ParenthesizedType: - return ts.walkUpParenthesizedTypes(node); - case ts.SyntaxKind.ParenthesizedExpression: - return ts.walkUpParenthesizedExpressions(node); - default: - return node; - } - } + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportDeclaration: + case ts.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, preferences) }; - function getAlreadyUsedTypesInStringLiteralUnion(union: ts.UnionTypeNode, current: ts.LiteralTypeNode): readonly string[] { - return ts.mapDefined(union.types, type => type !== current && ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal) ? type.literal.text : undefined); - } - - function getStringLiteralCompletionsFromSignature(call: ts.CallLikeExpression, arg: ts.StringLiteralLike, argumentInfo: ts.SignatureHelp.ArgumentInfoForCompletions, checker: ts.TypeChecker): StringLiteralCompletionsFromTypes { - let isNewIdentifier = false; - const uniques = new ts.Map(); - const candidates: ts.Signature[] = []; - const editingArgument = ts.isJsxOpeningLikeElement(call) ? ts.Debug.checkDefined(ts.findAncestor(arg.parent, ts.isJsxAttribute)) : arg; - checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates); - const types = ts.flatMap(candidates, candidate => { - if (!ts.signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) - return; - let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex); - if (ts.isJsxOpeningLikeElement(call)) { - const propType = checker.getTypeOfPropertyOfType(type, (editingArgument as ts.JsxAttribute).name.text); - if (propType) { - type = propType; - } - } - isNewIdentifier = isNewIdentifier || !!(type.flags & ts.TypeFlags.String); - return getStringLiteralTypes(type, uniques); - }); + default: + return fromContextualType(); + } - return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; + 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(ts.getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; } +} - function stringLiteralCompletionsFromProperties(type: ts.Type | undefined): StringLiteralCompletionsFromProperties | undefined { - return type && { - kind: StringLiteralCompletionKind.Properties, - symbols: ts.filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))), - hasIndexSignature: ts.hasIndexSignature(type) - }; +function walkUpParentheses(node: ts.Node) { + switch (node.kind) { + case ts.SyntaxKind.ParenthesizedType: + return ts.walkUpParenthesizedTypes(node); + case ts.SyntaxKind.ParenthesizedExpression: + return ts.walkUpParenthesizedExpressions(node); + default: + return node; } +} - function stringLiteralCompletionsForObjectLiteral(checker: ts.TypeChecker, objectLiteralExpression: ts.ObjectLiteralExpression): StringLiteralCompletionsFromProperties | undefined { - const contextualType = checker.getContextualType(objectLiteralExpression); - if (!contextualType) - return undefined; - const completionsType = checker.getContextualType(objectLiteralExpression, ts.ContextFlags.Completions); - const symbols = ts.Completions.getPropertiesForObjectExpression(contextualType, completionsType, objectLiteralExpression, checker); +function getAlreadyUsedTypesInStringLiteralUnion(union: ts.UnionTypeNode, current: ts.LiteralTypeNode): readonly string[] { + return ts.mapDefined(union.types, type => type !== current && ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal) ? type.literal.text : undefined); +} - return { - kind: StringLiteralCompletionKind.Properties, - symbols, - hasIndexSignature: ts.hasIndexSignature(contextualType) - }; - } +function getStringLiteralCompletionsFromSignature(call: ts.CallLikeExpression, arg: ts.StringLiteralLike, argumentInfo: ts.SignatureHelp.ArgumentInfoForCompletions, checker: ts.TypeChecker): StringLiteralCompletionsFromTypes { + let isNewIdentifier = false; + const uniques = new ts.Map(); + const candidates: ts.Signature[] = []; + const editingArgument = ts.isJsxOpeningLikeElement(call) ? ts.Debug.checkDefined(ts.findAncestor(arg.parent, ts.isJsxAttribute)) : arg; + checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates); + const types = ts.flatMap(candidates, candidate => { + if (!ts.signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) + return; + let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex); + if (ts.isJsxOpeningLikeElement(call)) { + const propType = checker.getTypeOfPropertyOfType(type, (editingArgument as ts.JsxAttribute).name.text); + if (propType) { + type = propType; + } + } + isNewIdentifier = isNewIdentifier || !!(type.flags & ts.TypeFlags.String); + return getStringLiteralTypes(type, uniques); + }); - function getStringLiteralTypes(type: ts.Type | undefined, uniques = new ts.Map()): readonly ts.StringLiteralType[] { - if (!type) - return ts.emptyArray; - type = ts.skipConstraint(type); - return type.isUnion() ? ts.flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : - type.isStringLiteral() && !(type.flags & ts.TypeFlags.EnumLiteral) && ts.addToSeen(uniques, type.value) ? [type] : ts.emptyArray; - } + return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; +} - interface NameAndKind { - readonly name: string; - readonly kind: ts.ScriptElementKind.scriptElement | ts.ScriptElementKind.directory | ts.ScriptElementKind.externalModuleName; - readonly extension: ts.Extension | undefined; - } - interface PathCompletion extends NameAndKind { - readonly span: ts.TextSpan | undefined; - } +function stringLiteralCompletionsFromProperties(type: ts.Type | undefined): StringLiteralCompletionsFromProperties | undefined { + return type && { + kind: StringLiteralCompletionKind.Properties, + symbols: ts.filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && ts.isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))), + hasIndexSignature: ts.hasIndexSignature(type) + }; +} - function nameAndKind(name: string, kind: NameAndKind["kind"], extension: ts.Extension | undefined): NameAndKind { - return { name, kind, extension }; - } - function directoryResult(name: string): NameAndKind { - return nameAndKind(name, ts.ScriptElementKind.directory, /*extension*/ undefined); - } +function stringLiteralCompletionsForObjectLiteral(checker: ts.TypeChecker, objectLiteralExpression: ts.ObjectLiteralExpression): StringLiteralCompletionsFromProperties | undefined { + const contextualType = checker.getContextualType(objectLiteralExpression); + if (!contextualType) + return undefined; + const completionsType = checker.getContextualType(objectLiteralExpression, ts.ContextFlags.Completions); + const symbols = ts.Completions.getPropertiesForObjectExpression(contextualType, completionsType, objectLiteralExpression, checker); + + return { + kind: StringLiteralCompletionKind.Properties, + symbols, + hasIndexSignature: ts.hasIndexSignature(contextualType) + }; +} - function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] { - const span = getDirectoryFragmentTextSpan(text, textStart); - const wholeSpan = text.length === 0 ? undefined : ts.createTextSpan(textStart, text.length); - return names.map(({ name, kind, extension }): PathCompletion => Math.max(name.indexOf(ts.directorySeparator), name.indexOf(ts.altDirectorySeparator)) !== -1 ? { name, kind, extension, span: wholeSpan } : { name, kind, extension, span }); - } +function getStringLiteralTypes(type: ts.Type | undefined, uniques = new ts.Map()): readonly ts.StringLiteralType[] { + if (!type) + return ts.emptyArray; + type = ts.skipConstraint(type); + return type.isUnion() ? ts.flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : + type.isStringLiteral() && !(type.flags & ts.TypeFlags.EnumLiteral) && ts.addToSeen(uniques, type.value) ? [type] : ts.emptyArray; +} - function getStringLiteralCompletionsFromModuleNames(sourceFile: ts.SourceFile, node: ts.LiteralExpression, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker, preferences: ts.UserPreferences): readonly PathCompletion[] { - return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker, preferences)); - } +interface NameAndKind { + readonly name: string; + readonly kind: ts.ScriptElementKind.scriptElement | ts.ScriptElementKind.directory | ts.ScriptElementKind.externalModuleName; + readonly extension: ts.Extension | undefined; +} +interface PathCompletion extends NameAndKind { + readonly span: ts.TextSpan | undefined; +} - function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: ts.SourceFile, node: ts.LiteralExpression, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker, preferences: ts.UserPreferences): readonly NameAndKind[] { - const literalValue = ts.normalizeSlashes(node.text); +function nameAndKind(name: string, kind: NameAndKind["kind"], extension: ts.Extension | undefined): NameAndKind { + return { name, kind, extension }; +} +function directoryResult(name: string): NameAndKind { + return nameAndKind(name, ts.ScriptElementKind.directory, /*extension*/ undefined); +} - const scriptPath = sourceFile.path; - const scriptDirectory = ts.getDirectoryPath(scriptPath); - return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (ts.isRootedDiskPath(literalValue) || ts.isUrl(literalValue)) - ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath, getIncludeExtensionOption()) - : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker); +function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] { + const span = getDirectoryFragmentTextSpan(text, textStart); + const wholeSpan = text.length === 0 ? undefined : ts.createTextSpan(textStart, text.length); + return names.map(({ name, kind, extension }): PathCompletion => Math.max(name.indexOf(ts.directorySeparator), name.indexOf(ts.altDirectorySeparator)) !== -1 ? { name, kind, extension, span: wholeSpan } : { name, kind, extension, span }); +} - function getIncludeExtensionOption() { - const mode = ts.isStringLiteralLike(node) ? ts.getModeForUsageLocation(sourceFile, node) : undefined; - return preferences.importModuleSpecifierEnding === "js" || mode === ts.ModuleKind.ESNext ? IncludeExtensionsOption.ModuleSpecifierCompletion : IncludeExtensionsOption.Exclude; - } +function getStringLiteralCompletionsFromModuleNames(sourceFile: ts.SourceFile, node: ts.LiteralExpression, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker, preferences: ts.UserPreferences): readonly PathCompletion[] { + return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker, preferences)); +} + +function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: ts.SourceFile, node: ts.LiteralExpression, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker, preferences: ts.UserPreferences): readonly NameAndKind[] { + const literalValue = ts.normalizeSlashes(node.text); + + const scriptPath = sourceFile.path; + const scriptDirectory = ts.getDirectoryPath(scriptPath); + return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (ts.isRootedDiskPath(literalValue) || ts.isUrl(literalValue)) + ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath, getIncludeExtensionOption()) + : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker); + + function getIncludeExtensionOption() { + const mode = ts.isStringLiteralLike(node) ? ts.getModeForUsageLocation(sourceFile, node) : undefined; + return preferences.importModuleSpecifierEnding === "js" || mode === ts.ModuleKind.ESNext ? IncludeExtensionsOption.ModuleSpecifierCompletion : IncludeExtensionsOption.Exclude; } +} - interface ExtensionOptions { - readonly extensions: readonly ts.Extension[]; - readonly includeExtensionsOption: IncludeExtensionsOption; +interface ExtensionOptions { + readonly extensions: readonly ts.Extension[]; + readonly includeExtensionsOption: IncludeExtensionsOption; +} +function getExtensionOptions(compilerOptions: ts.CompilerOptions, includeExtensionsOption = IncludeExtensionsOption.Exclude): ExtensionOptions { + return { extensions: ts.flatten(getSupportedExtensionsForModuleResolution(compilerOptions)), includeExtensionsOption }; +} +function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, scriptPath: ts.Path, includeExtensions: IncludeExtensionsOption) { + const extensionOptions = getExtensionOptions(compilerOptions, includeExtensions); + if (compilerOptions.rootDirs) { + return getCompletionEntriesForDirectoryFragmentWithRootDirs(compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath); } - function getExtensionOptions(compilerOptions: ts.CompilerOptions, includeExtensionsOption = IncludeExtensionsOption.Exclude): ExtensionOptions { - return { extensions: ts.flatten(getSupportedExtensionsForModuleResolution(compilerOptions)), includeExtensionsOption }; - } - function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, scriptPath: ts.Path, includeExtensions: IncludeExtensionsOption) { - const extensionOptions = getExtensionOptions(compilerOptions, includeExtensions); - if (compilerOptions.rootDirs) { - return getCompletionEntriesForDirectoryFragmentWithRootDirs(compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath); - } - else { - return getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath); - } + else { + return getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath); } +} - function isEmitResolutionKindUsingNodeModules(compilerOptions: ts.CompilerOptions): boolean { - return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeJs || - ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || - ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext; - } +function isEmitResolutionKindUsingNodeModules(compilerOptions: ts.CompilerOptions): boolean { + return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeJs || + ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || + ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext; +} - function isEmitModuleResolutionRespectingExportMaps(compilerOptions: ts.CompilerOptions) { - return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || - ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext; - } +function isEmitModuleResolutionRespectingExportMaps(compilerOptions: ts.CompilerOptions) { + return ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.Node16 || + ts.getEmitModuleResolutionKind(compilerOptions) === ts.ModuleResolutionKind.NodeNext; +} - function getSupportedExtensionsForModuleResolution(compilerOptions: ts.CompilerOptions): readonly ts.Extension[][] { - const extensions = ts.getSupportedExtensions(compilerOptions); - return isEmitResolutionKindUsingNodeModules(compilerOptions) ? - ts.getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) : - extensions; - } +function getSupportedExtensionsForModuleResolution(compilerOptions: ts.CompilerOptions): readonly ts.Extension[][] { + const extensions = ts.getSupportedExtensions(compilerOptions); + return isEmitResolutionKindUsingNodeModules(compilerOptions) ? + ts.getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) : + 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 - */ - 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 => ts.normalizePath(ts.isRootedDiskPath(rootDirectory) ? rootDirectory : ts.combinePaths(basePath, rootDirectory))); +/** + * 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 => ts.normalizePath(ts.isRootedDiskPath(rootDirectory) ? rootDirectory : ts.combinePaths(basePath, rootDirectory))); - // Determine the path to the directory containing the script relative to the root directory it is contained within - const relativeDirectory = ts.firstDefined(rootDirs, rootDirectory => ts.containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!; // TODO: GH#18217 + // Determine the path to the directory containing the script relative to the root directory it is contained within + const relativeDirectory = ts.firstDefined(rootDirs, rootDirectory => ts.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 ts.deduplicate([...rootDirs.map(rootDirectory => ts.combinePaths(rootDirectory, relativeDirectory)), scriptDirectory], ts.equateStringsCaseSensitive, ts.compareStringsCaseSensitive); - } + // Now find a path for each potential directory that is to be merged with the one containing the script + return ts.deduplicate([...rootDirs.map(rootDirectory => ts.combinePaths(rootDirectory, relativeDirectory)), scriptDirectory], ts.equateStringsCaseSensitive, ts.compareStringsCaseSensitive); +} - function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: ts.CompilerOptions, host: ts.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 ts.flatMap(baseDirectories, baseDirectory => getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude)); - } +function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: ts.CompilerOptions, host: ts.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 ts.flatMap(baseDirectories, baseDirectory => getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude)); +} - const enum IncludeExtensionsOption { - Exclude, - Include, - ModuleSpecifierCompletion +const enum IncludeExtensionsOption { + Exclude, + Include, + ModuleSpecifierCompletion +} +/** + * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename. + */ +function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, { extensions, includeExtensionsOption }: ExtensionOptions, host: ts.LanguageServiceHost, exclude?: string, result: NameAndKind[] = []): NameAndKind[] { + if (fragment === undefined) { + fragment = ""; } + + fragment = ts.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, includeExtensionsOption }: ExtensionOptions, host: ts.LanguageServiceHost, exclude?: string, result: NameAndKind[] = []): NameAndKind[] { - if (fragment === undefined) { - fragment = ""; - } + if (!ts.hasTrailingDirectorySeparator(fragment)) { + fragment = ts.getDirectoryPath(fragment); + } - fragment = ts.normalizeSlashes(fragment); + if (fragment === "") { + fragment = "." + ts.directorySeparator; + } - /** - * Remove the basename from the path. Note that we don't use the basename to filter completions; - * the client is responsible for refining completions. - */ - if (!ts.hasTrailingDirectorySeparator(fragment)) { - fragment = ts.getDirectoryPath(fragment); - } + fragment = ts.ensureTrailingDirectorySeparator(fragment); - if (fragment === "") { - fragment = "." + ts.directorySeparator; - } + // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths + const absolutePath = ts.resolvePath(scriptPath, fragment); + const baseDirectory = ts.hasTrailingDirectorySeparator(absolutePath) ? absolutePath : ts.getDirectoryPath(absolutePath); - fragment = ts.ensureTrailingDirectorySeparator(fragment); - - // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths - const absolutePath = ts.resolvePath(scriptPath, fragment); - const baseDirectory = ts.hasTrailingDirectorySeparator(absolutePath) ? absolutePath : ts.getDirectoryPath(absolutePath); - - const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); - if (!ts.tryDirectoryExists(host, baseDirectory)) - return result; - - // Enumerate the available files if possible - const files = ts.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 includeExtensionsOption === includeExtensionsOption.Exclude) so we use a set-like data structure. Eg: - * - * both foo.ts and foo.tsx become foo - */ - const foundFiles = new ts.Map(); // maps file to its extension - for (let filePath of files) { - filePath = ts.normalizePath(filePath); - if (exclude && ts.comparePaths(filePath, exclude, scriptPath, ignoreCase) === ts.Comparison.EqualTo) { - continue; - } + const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); + if (!ts.tryDirectoryExists(host, baseDirectory)) + return result; - let foundFileName: string; - const outputExtension = ts.moduleSpecifiers.tryGetJSExtensionForFile(filePath, host.getCompilationSettings()); - if (includeExtensionsOption === IncludeExtensionsOption.Exclude && !ts.fileExtensionIsOneOf(filePath, [ts.Extension.Json, ts.Extension.Mts, ts.Extension.Cts, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Mjs, ts.Extension.Cjs])) { - foundFileName = ts.removeFileExtension(ts.getBaseFileName(filePath)); - foundFiles.set(foundFileName, ts.tryGetExtensionFromPath(filePath)); - } - else if ((ts.fileExtensionIsOneOf(filePath, [ts.Extension.Mts, ts.Extension.Cts, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Mjs, ts.Extension.Cjs]) || includeExtensionsOption === IncludeExtensionsOption.ModuleSpecifierCompletion) && outputExtension) { - foundFileName = ts.changeExtension(ts.getBaseFileName(filePath), outputExtension); - foundFiles.set(foundFileName, outputExtension); - } - else { - foundFileName = ts.getBaseFileName(filePath); - foundFiles.set(foundFileName, ts.tryGetExtensionFromPath(filePath)); - } + // Enumerate the available files if possible + const files = ts.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 includeExtensionsOption === includeExtensionsOption.Exclude) so we use a set-like data structure. Eg: + * + * both foo.ts and foo.tsx become foo + */ + const foundFiles = new ts.Map(); // maps file to its extension + for (let filePath of files) { + filePath = ts.normalizePath(filePath); + if (exclude && ts.comparePaths(filePath, exclude, scriptPath, ignoreCase) === ts.Comparison.EqualTo) { + continue; } - foundFiles.forEach((ext, foundFile) => { - result.push(nameAndKind(foundFile, ts.ScriptElementKind.scriptElement, ext)); - }); + let foundFileName: string; + const outputExtension = ts.moduleSpecifiers.tryGetJSExtensionForFile(filePath, host.getCompilationSettings()); + if (includeExtensionsOption === IncludeExtensionsOption.Exclude && !ts.fileExtensionIsOneOf(filePath, [ts.Extension.Json, ts.Extension.Mts, ts.Extension.Cts, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Mjs, ts.Extension.Cjs])) { + foundFileName = ts.removeFileExtension(ts.getBaseFileName(filePath)); + foundFiles.set(foundFileName, ts.tryGetExtensionFromPath(filePath)); + } + else if ((ts.fileExtensionIsOneOf(filePath, [ts.Extension.Mts, ts.Extension.Cts, ts.Extension.Dmts, ts.Extension.Dcts, ts.Extension.Mjs, ts.Extension.Cjs]) || includeExtensionsOption === IncludeExtensionsOption.ModuleSpecifierCompletion) && outputExtension) { + foundFileName = ts.changeExtension(ts.getBaseFileName(filePath), outputExtension); + foundFiles.set(foundFileName, outputExtension); + } + else { + foundFileName = ts.getBaseFileName(filePath); + foundFiles.set(foundFileName, ts.tryGetExtensionFromPath(filePath)); + } } - // If possible, get folder completion as well - const directories = ts.tryGetDirectories(host, baseDirectory); + foundFiles.forEach((ext, foundFile) => { + result.push(nameAndKind(foundFile, ts.ScriptElementKind.scriptElement, ext)); + }); + } - if (directories) { - for (const directory of directories) { - const directoryName = ts.getBaseFileName(ts.normalizePath(directory)); - if (directoryName !== "@types") { - result.push(directoryResult(directoryName)); - } + // If possible, get folder completion as well + const directories = ts.tryGetDirectories(host, baseDirectory); + + if (directories) { + for (const directory of directories) { + const directoryName = ts.getBaseFileName(ts.normalizePath(directory)); + if (directoryName !== "@types") { + result.push(directoryResult(directoryName)); } } + } - // check for a version redirect - const packageJsonPath = ts.findPackageJson(baseDirectory, host); - if (packageJsonPath) { - const packageJson = ts.readJson(packageJsonPath, host as { - readFile: (filename: string) => string | undefined; - }); - const typesVersions = (packageJson as any).typesVersions; - if (typeof typesVersions === "object") { - const versionResult = ts.getPackageJsonTypesVersionsPaths(typesVersions); - const versionPaths = versionResult && versionResult.paths; - const rest = absolutePath.slice(ts.ensureTrailingDirectorySeparator(baseDirectory).length); - if (versionPaths) { - addCompletionEntriesFromPaths(result, rest, baseDirectory, extensions, versionPaths, host); - } + // check for a version redirect + const packageJsonPath = ts.findPackageJson(baseDirectory, host); + if (packageJsonPath) { + const packageJson = ts.readJson(packageJsonPath, host as { + readFile: (filename: string) => string | undefined; + }); + const typesVersions = (packageJson as any).typesVersions; + if (typeof typesVersions === "object") { + const versionResult = ts.getPackageJsonTypesVersionsPaths(typesVersions); + const versionPaths = versionResult && versionResult.paths; + const rest = absolutePath.slice(ts.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: ts.MapLike, host: ts.LanguageServiceHost) { - for (const path in paths) { - if (!ts.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; +} + +function addCompletionEntriesFromPaths(result: NameAndKind[], fragment: string, baseDirectory: string, fileExtensions: readonly string[], paths: ts.MapLike, host: ts.LanguageServiceHost) { + for (const path in paths) { + if (!ts.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: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker): readonly NameAndKind[] { - const { baseUrl, paths } = compilerOptions; - - const result: NameAndKind[] = []; - - const extensionOptions = getExtensionOptions(compilerOptions); - if (baseUrl) { - const projectDir = compilerOptions.project || host.getCurrentDirectory(); - const absolute = ts.normalizePath(ts.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 + */ +function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost, typeChecker: ts.TypeChecker): readonly NameAndKind[] { + const { baseUrl, paths } = compilerOptions; - const fragmentDirectory = getFragmentDirectory(fragment); - for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { - result.push(nameAndKind(ambientName, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); + const result: NameAndKind[] = []; + + const extensionOptions = getExtensionOptions(compilerOptions); + if (baseUrl) { + const projectDir = compilerOptions.project || host.getCurrentDirectory(); + const absolute = ts.normalizePath(ts.combinePaths(projectDir, baseUrl)); + getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result); + if (paths) { + addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions.extensions, paths, host); } + } - getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result); + const fragmentDirectory = getFragmentDirectory(fragment); + for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { + result.push(nameAndKind(ambientName, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); + } - if (isEmitResolutionKindUsingNodeModules(compilerOptions)) { - // 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, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); - } + getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result); + + if (isEmitResolutionKindUsingNodeModules(compilerOptions)) { + // 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, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); } } - if (!foundGlobal) { - let ancestorLookup: (directory: string) => void | undefined = ancestor => { - const nodeModules = ts.combinePaths(ancestor, "node_modules"); - if (ts.tryDirectoryExists(host, nodeModules)) { - getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result); + } + if (!foundGlobal) { + let ancestorLookup: (directory: string) => void | undefined = ancestor => { + const nodeModules = ts.combinePaths(ancestor, "node_modules"); + if (ts.tryDirectoryExists(host, nodeModules)) { + getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result); + } + }; + if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) { + const nodeModulesDirectoryLookup = ancestorLookup; + ancestorLookup = ancestor => { + const components = ts.getPathComponents(fragment); + components.shift(); // shift off empty root + let packagePath = components.shift(); + if (!packagePath) { + return nodeModulesDirectoryLookup(ancestor); } - }; - if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) { - const nodeModulesDirectoryLookup = ancestorLookup; - ancestorLookup = ancestor => { - const components = ts.getPathComponents(fragment); - components.shift(); // shift off empty root - let packagePath = components.shift(); - if (!packagePath) { + if (ts.startsWith(packagePath, "@")) { + const subName = components.shift(); + if (!subName) { return nodeModulesDirectoryLookup(ancestor); } - if (ts.startsWith(packagePath, "@")) { - const subName = components.shift(); - if (!subName) { - return nodeModulesDirectoryLookup(ancestor); + packagePath = ts.combinePaths(packagePath, subName); + } + const packageFile = ts.combinePaths(ancestor, "node_modules", packagePath, "package.json"); + if (ts.tryFileExists(host, packageFile)) { + const packageJson = ts.readJson(packageFile, host as { + readFile: (filename: string) => string | undefined; + }); + const exports = (packageJson as any).exports; + if (exports) { + if (typeof exports !== "object" || exports === null) { // eslint-disable-line no-null/no-null + return; // null exports or entrypoint only, no sub-modules available } - packagePath = ts.combinePaths(packagePath, subName); - } - const packageFile = ts.combinePaths(ancestor, "node_modules", packagePath, "package.json"); - if (ts.tryFileExists(host, packageFile)) { - const packageJson = ts.readJson(packageFile, host as { - readFile: (filename: string) => string | undefined; + const keys = ts.getOwnKeys(exports); + const fragmentSubpath = components.join("/"); + const processedKeys = ts.mapDefined(keys, k => { + if (k === ".") + return undefined; + if (!ts.startsWith(k, "./")) + return undefined; + const subpath = k.substring(2); + if (!ts.startsWith(subpath, fragmentSubpath)) + return undefined; + // subpath is a valid export (barring conditions, which we don't currently check here) + if (!ts.stringContains(subpath, "*")) { + return subpath; + } + // pattern export - only return everything up to the `*`, so the user can autocomplete, then + // keep filling in the pattern (we could speculatively return a list of options by hitting disk, + // but conditions will make that somewhat awkward, as each condition may have a different set of possible + // options for the `*`. + return subpath.slice(0, subpath.indexOf("*")); }); - const exports = (packageJson as any).exports; - if (exports) { - if (typeof exports !== "object" || exports === null) { // eslint-disable-line no-null/no-null - return; // null exports or entrypoint only, no sub-modules available + ts.forEach(processedKeys, k => { + if (k) { + result.push(nameAndKind(k, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); } - const keys = ts.getOwnKeys(exports); - const fragmentSubpath = components.join("/"); - const processedKeys = ts.mapDefined(keys, k => { - if (k === ".") - return undefined; - if (!ts.startsWith(k, "./")) - return undefined; - const subpath = k.substring(2); - if (!ts.startsWith(subpath, fragmentSubpath)) - return undefined; - // subpath is a valid export (barring conditions, which we don't currently check here) - if (!ts.stringContains(subpath, "*")) { - return subpath; - } - // pattern export - only return everything up to the `*`, so the user can autocomplete, then - // keep filling in the pattern (we could speculatively return a list of options by hitting disk, - // but conditions will make that somewhat awkward, as each condition may have a different set of possible - // options for the `*`. - return subpath.slice(0, subpath.indexOf("*")); - }); - ts.forEach(processedKeys, k => { - if (k) { - result.push(nameAndKind(k, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); - } - }); - return; - } + }); + return; } - return nodeModulesDirectoryLookup(ancestor); - }; - } - ts.forEachAncestorDirectory(scriptPath, ancestorLookup); + } + return nodeModulesDirectoryLookup(ancestor); + }; } + ts.forEachAncestorDirectory(scriptPath, ancestorLookup); } - - return result; } - function getFragmentDirectory(fragment: string): string | undefined { - return containsSlash(fragment) ? ts.hasTrailingDirectorySeparator(fragment) ? fragment : ts.getDirectoryPath(fragment) : undefined; + return result; +} + +function getFragmentDirectory(fragment: string): string | undefined { + return containsSlash(fragment) ? ts.hasTrailingDirectorySeparator(fragment) ? fragment : ts.getDirectoryPath(fragment) : undefined; +} + +function getCompletionsForPathMapping(path: string, patterns: readonly string[], fragment: string, baseUrl: string, fileExtensions: readonly string[], host: ts.LanguageServiceHost): readonly NameAndKind[] { + if (!ts.endsWith(path, "*")) { + // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. + return !ts.stringContains(path, "*") ? justPathMappingName(path) : ts.emptyArray; } - function getCompletionsForPathMapping(path: string, patterns: readonly string[], fragment: string, baseUrl: string, fileExtensions: readonly string[], host: ts.LanguageServiceHost): readonly NameAndKind[] { - if (!ts.endsWith(path, "*")) { - // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. - return !ts.stringContains(path, "*") ? justPathMappingName(path) : ts.emptyArray; - } + const pathPrefix = path.slice(0, path.length - 1); + const remainingFragment = ts.tryRemovePrefix(fragment, pathPrefix); + return remainingFragment === undefined ? justPathMappingName(pathPrefix) : ts.flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); + + function justPathMappingName(name: string): readonly NameAndKind[] { + return ts.startsWith(name, fragment) ? [directoryResult(name)] : ts.emptyArray; + } +} - const pathPrefix = path.slice(0, path.length - 1); - const remainingFragment = ts.tryRemovePrefix(fragment, pathPrefix); - return remainingFragment === undefined ? justPathMappingName(pathPrefix) : ts.flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); +function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: readonly string[], host: ts.LanguageServiceHost): readonly NameAndKind[] | undefined { + if (!host.readDirectory) { + return undefined; + } - function justPathMappingName(name: string): readonly NameAndKind[] { - return ts.startsWith(name, fragment) ? [directoryResult(name)] : ts.emptyArray; - } + const parsed = ts.tryParsePattern(pattern); + if (parsed === undefined || ts.isString(parsed)) { + return undefined; } - function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: readonly string[], host: ts.LanguageServiceHost): readonly NameAndKind[] | undefined { - if (!host.readDirectory) { - 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 = ts.resolvePath(parsed.prefix); + const normalizedPrefixDirectory = ts.hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : ts.getDirectoryPath(normalizedPrefix); + const normalizedPrefixBase = ts.hasTrailingDirectorySeparator(parsed.prefix) ? "" : ts.getBaseFileName(normalizedPrefix); - const parsed = ts.tryParsePattern(pattern); - if (parsed === undefined || ts.isString(parsed)) { - return undefined; - } + const fragmentHasPath = containsSlash(fragment); + const fragmentDirectory = fragmentHasPath ? ts.hasTrailingDirectorySeparator(fragment) ? fragment : ts.getDirectoryPath(fragment) : 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 = ts.resolvePath(parsed.prefix); - const normalizedPrefixDirectory = ts.hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : ts.getDirectoryPath(normalizedPrefix); - const normalizedPrefixBase = ts.hasTrailingDirectorySeparator(parsed.prefix) ? "" : ts.getBaseFileName(normalizedPrefix); - - const fragmentHasPath = containsSlash(fragment); - const fragmentDirectory = fragmentHasPath ? ts.hasTrailingDirectorySeparator(fragment) ? fragment : ts.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 ? ts.combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; - const normalizedSuffix = ts.normalizePath(parsed.suffix); - // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". - const baseDirectory = ts.normalizePath(ts.combinePaths(baseUrl, expandedPrefixDirectory)); - const completePrefix = fragmentHasPath ? baseDirectory : ts.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 = ts.mapDefined(ts.tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => { - const extension = ts.tryGetExtensionFromPath(match); - const name = trimPrefixAndSuffix(match); - return name === undefined ? undefined : nameAndKind(ts.removeFileExtension(name), ts.ScriptElementKind.scriptElement, extension); - }); - const directories = ts.mapDefined(ts.tryGetDirectories(host, baseDirectory).map(d => ts.combinePaths(baseDirectory, d)), dir => { - const name = trimPrefixAndSuffix(dir); - return name === undefined ? undefined : directoryResult(name); - }); - return [...matches, ...directories]; + // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call + const expandedPrefixDirectory = fragmentHasPath ? ts.combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; + const normalizedSuffix = ts.normalizePath(parsed.suffix); + // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". + const baseDirectory = ts.normalizePath(ts.combinePaths(baseUrl, expandedPrefixDirectory)); + const completePrefix = fragmentHasPath ? baseDirectory : ts.ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; - function trimPrefixAndSuffix(path: string): string | undefined { - const inner = withoutStartAndEnd(ts.normalizePath(path), completePrefix, normalizedSuffix); - return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner); - } - } + // 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 ? "**/*" : "./*"; - function withoutStartAndEnd(s: string, start: string, end: string): string | undefined { - return ts.startsWith(s, start) && ts.endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined; - } + const matches = ts.mapDefined(ts.tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => { + const extension = ts.tryGetExtensionFromPath(match); + const name = trimPrefixAndSuffix(match); + return name === undefined ? undefined : nameAndKind(ts.removeFileExtension(name), ts.ScriptElementKind.scriptElement, extension); + }); + const directories = ts.mapDefined(ts.tryGetDirectories(host, baseDirectory).map(d => ts.combinePaths(baseDirectory, d)), dir => { + const name = trimPrefixAndSuffix(dir); + return name === undefined ? undefined : directoryResult(name); + }); + return [...matches, ...directories]; - function removeLeadingDirectorySeparator(path: string): string { - return path[0] === ts.directorySeparator ? path.slice(1) : path; + function trimPrefixAndSuffix(path: string): string | undefined { + const inner = withoutStartAndEnd(ts.normalizePath(path), completePrefix, normalizedSuffix); + return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner); } +} - function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: ts.TypeChecker): readonly string[] { - // Get modules that the type checker picked up - const ambientModules = checker.getAmbientModules().map(sym => ts.stripQuotes(sym.name)); - const nonRelativeModuleNames = ambientModules.filter(moduleName => ts.startsWith(moduleName, fragment)); +function withoutStartAndEnd(s: string, start: string, end: string): string | undefined { + return ts.startsWith(s, start) && ts.endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined; +} - // 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 = ts.ensureTrailingDirectorySeparator(fragmentDirectory); - return nonRelativeModuleNames.map(nonRelativeModuleName => ts.removePrefix(nonRelativeModuleName, moduleNameWithSeparator)); - } - return nonRelativeModuleNames; - } +function removeLeadingDirectorySeparator(path: string): string { + return path[0] === ts.directorySeparator ? path.slice(1) : path; +} - function getTripleSlashReferenceCompletion(sourceFile: ts.SourceFile, position: number, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost): readonly PathCompletion[] | undefined { - const token = ts.getTokenAtPosition(sourceFile, position); - const commentRanges = ts.getLeadingCommentRanges(sourceFile.text, token.pos); - const range = commentRanges && ts.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; - } +function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: ts.TypeChecker): readonly string[] { + // Get modules that the type checker picked up + const ambientModules = checker.getAmbientModules().map(sym => ts.stripQuotes(sym.name)); + const nonRelativeModuleNames = ambientModules.filter(moduleName => ts.startsWith(moduleName, fragment)); - const [, prefix, kind, toComplete] = match; - const scriptPath = ts.getDirectoryPath(sourceFile.path); - const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, IncludeExtensionsOption.Include), host, sourceFile.path) - : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions)) - : ts.Debug.fail(); - return addReplacementSpans(toComplete, range.pos + prefix.length, names); + // 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 = ts.ensureTrailingDirectorySeparator(fragmentDirectory); + return nonRelativeModuleNames.map(nonRelativeModuleName => ts.removePrefix(nonRelativeModuleName, moduleNameWithSeparator)); } + return nonRelativeModuleNames; +} - function getCompletionEntriesFromTypings(host: ts.LanguageServiceHost, options: ts.CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result: NameAndKind[] = []): readonly NameAndKind[] { - // Check for typings specified in compiler options - const seen = new ts.Map(); - const typeRoots = ts.tryAndIgnoreErrors(() => ts.getEffectiveTypeRoots(options, host)) || ts.emptyArray; +function getTripleSlashReferenceCompletion(sourceFile: ts.SourceFile, position: number, compilerOptions: ts.CompilerOptions, host: ts.LanguageServiceHost): readonly PathCompletion[] | undefined { + const token = ts.getTokenAtPosition(sourceFile, position); + const commentRanges = ts.getLeadingCommentRanges(sourceFile.text, token.pos); + const range = commentRanges && ts.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 = ts.getDirectoryPath(sourceFile.path); + const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, IncludeExtensionsOption.Include), host, sourceFile.path) + : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions)) + : ts.Debug.fail(); + return addReplacementSpans(toComplete, range.pos + prefix.length, names); +} - for (const root of typeRoots) { - getCompletionEntriesFromDirectories(root); - } +function getCompletionEntriesFromTypings(host: ts.LanguageServiceHost, options: ts.CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result: NameAndKind[] = []): readonly NameAndKind[] { + // Check for typings specified in compiler options + const seen = new ts.Map(); + const typeRoots = ts.tryAndIgnoreErrors(() => ts.getEffectiveTypeRoots(options, host)) || ts.emptyArray; - // Also get all @types typings installed in visible node_modules directories - for (const packageJson of ts.findPackageJsons(scriptPath, host)) { - const typesDir = ts.combinePaths(ts.getDirectoryPath(packageJson), "node_modules/@types"); - getCompletionEntriesFromDirectories(typesDir); - } + for (const root of typeRoots) { + getCompletionEntriesFromDirectories(root); + } - return result; + // Also get all @types typings installed in visible node_modules directories + for (const packageJson of ts.findPackageJsons(scriptPath, host)) { + const typesDir = ts.combinePaths(ts.getDirectoryPath(packageJson), "node_modules/@types"); + getCompletionEntriesFromDirectories(typesDir); + } - function getCompletionEntriesFromDirectories(directory: string): void { - if (!ts.tryDirectoryExists(host, directory)) - return; - for (const typeDirectoryName of ts.tryGetDirectories(host, directory)) { - const packageName = ts.unmangleScopedPackageName(typeDirectoryName); - if (options.types && !ts.contains(options.types, packageName)) - continue; - - if (fragmentDirectory === undefined) { - if (!seen.has(packageName)) { - result.push(nameAndKind(packageName, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); - seen.set(packageName, true); - } + return result; + + function getCompletionEntriesFromDirectories(directory: string): void { + if (!ts.tryDirectoryExists(host, directory)) + return; + for (const typeDirectoryName of ts.tryGetDirectories(host, directory)) { + const packageName = ts.unmangleScopedPackageName(typeDirectoryName); + if (options.types && !ts.contains(options.types, packageName)) + continue; + + if (fragmentDirectory === undefined) { + if (!seen.has(packageName)) { + result.push(nameAndKind(packageName, ts.ScriptElementKind.externalModuleName, /*extension*/ undefined)); + seen.set(packageName, true); } - else { - const baseDirectory = ts.combinePaths(directory, typeDirectoryName); - const remainingFragment = ts.tryRemoveDirectoryPrefix(fragmentDirectory, packageName, ts.hostGetCanonicalFileName(host)); - if (remainingFragment !== undefined) { - getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result); - } + } + else { + const baseDirectory = ts.combinePaths(directory, typeDirectoryName); + const remainingFragment = ts.tryRemoveDirectoryPrefix(fragmentDirectory, packageName, ts.hostGetCanonicalFileName(host)); + if (remainingFragment !== undefined) { + getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result); } } } } +} - function enumerateNodeModulesVisibleToScript(host: ts.LanguageServiceHost, scriptPath: string): readonly string[] { - if (!host.readFile || !host.fileExists) - return ts.emptyArray; - - const result: string[] = []; - for (const packageJson of ts.findPackageJsons(scriptPath, host)) { - const contents = ts.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) && !ts.startsWith(dep, "@types/")) { - result.push(dep); - } +function enumerateNodeModulesVisibleToScript(host: ts.LanguageServiceHost, scriptPath: string): readonly string[] { + if (!host.readFile || !host.fileExists) + return ts.emptyArray; + + const result: string[] = []; + for (const packageJson of ts.findPackageJsons(scriptPath, host)) { + const contents = ts.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) && !ts.startsWith(dep, "@types/")) { + result.push(dep); } } } - return result; } + return result; +} - // Replace everything after the last directory separator that appears - function getDirectoryFragmentTextSpan(text: string, textStart: number): ts.TextSpan | undefined { - const index = Math.max(text.lastIndexOf(ts.directorySeparator), text.lastIndexOf(ts.altDirectorySeparator)); - const offset = index !== -1 ? index + 1 : 0; - // If the range is an identifier, span is unnecessary. - const length = text.length - offset; - return length === 0 || ts.isIdentifierText(text.substr(offset, length), ts.ScriptTarget.ESNext) ? undefined : ts.createTextSpan(textStart + offset, length); - } +// Replace everything after the last directory separator that appears +function getDirectoryFragmentTextSpan(text: string, textStart: number): ts.TextSpan | undefined { + const index = Math.max(text.lastIndexOf(ts.directorySeparator), text.lastIndexOf(ts.altDirectorySeparator)); + const offset = index !== -1 ? index + 1 : 0; + // If the range is an identifier, span is unnecessary. + const length = text.length - offset; + return length === 0 || ts.isIdentifierText(text.substr(offset, length), ts.ScriptTarget.ESNext) ? undefined : ts.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) === ts.CharacterCodes.dot) { - const slashIndex = path.length >= 3 && path.charCodeAt(1) === ts.CharacterCodes.dot ? 2 : 1; - const slashCharCode = path.charCodeAt(slashIndex); - return slashCharCode === ts.CharacterCodes.slash || slashCharCode === ts.CharacterCodes.backslash; - } - return false; +// 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) === ts.CharacterCodes.dot) { + const slashIndex = path.length >= 3 && path.charCodeAt(1) === ts.CharacterCodes.dot ? 2 : 1; + const slashCharCode = path.charCodeAt(slashIndex); + return slashCharCode === ts.CharacterCodes.slash || slashCharCode === ts.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: ts.SourceFile, program: ts.Program, cancellationToken: ts.CancellationToken): ts.DiagnosticWithLocation[] { - program.getSemanticDiagnostics(sourceFile, cancellationToken); - const diags: ts.DiagnosticWithLocation[] = []; - const checker = program.getTypeChecker(); - const isCommonJSFile = sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS || ts.fileExtensionIsOneOf(sourceFile.fileName, [ts.Extension.Cts, ts.Extension.Cjs]); - - if (!isCommonJSFile && - sourceFile.commonJsModuleIndicator && - (ts.programContainsEsModules(program) || ts.compilerOptionsIndicateEsModules(program.getCompilerOptions())) && - containsTopLevelCommonjs(sourceFile)) { - diags.push(ts.createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), ts.Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module)); - } - - const isJsFile = ts.isSourceFileJS(sourceFile); - - visitedNestedConvertibleFunctions.clear(); - check(sourceFile); - - if (ts.getAllowSyntheticDefaultImports(program.getCompilerOptions())) { - for (const moduleSpecifier of sourceFile.imports) { - const importNode = ts.importFromModuleSpecifier(moduleSpecifier); - const name = importNameForConvertToDefaultImport(importNode); - if (!name) - continue; - const module = ts.getResolvedModule(sourceFile, moduleSpecifier.text, ts.getModeForUsageLocation(sourceFile, moduleSpecifier)); - const resolvedFile = module && program.getSourceFile(module.resolvedFileName); - if (resolvedFile && resolvedFile.externalModuleIndicator && resolvedFile.externalModuleIndicator !== true && ts.isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) { - diags.push(ts.createDiagnosticForNode(name, ts.Diagnostics.Import_may_be_converted_to_a_default_import)); - } +const visitedNestedConvertibleFunctions = new ts.Map(); +export function computeSuggestionDiagnostics(sourceFile: ts.SourceFile, program: ts.Program, cancellationToken: ts.CancellationToken): ts.DiagnosticWithLocation[] { + program.getSemanticDiagnostics(sourceFile, cancellationToken); + const diags: ts.DiagnosticWithLocation[] = []; + const checker = program.getTypeChecker(); + const isCommonJSFile = sourceFile.impliedNodeFormat === ts.ModuleKind.CommonJS || ts.fileExtensionIsOneOf(sourceFile.fileName, [ts.Extension.Cts, ts.Extension.Cjs]); + + if (!isCommonJSFile && + sourceFile.commonJsModuleIndicator && + (ts.programContainsEsModules(program) || ts.compilerOptionsIndicateEsModules(program.getCompilerOptions())) && + containsTopLevelCommonjs(sourceFile)) { + diags.push(ts.createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), ts.Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module)); + } + + const isJsFile = ts.isSourceFileJS(sourceFile); + + visitedNestedConvertibleFunctions.clear(); + check(sourceFile); + + if (ts.getAllowSyntheticDefaultImports(program.getCompilerOptions())) { + for (const moduleSpecifier of sourceFile.imports) { + const importNode = ts.importFromModuleSpecifier(moduleSpecifier); + const name = importNameForConvertToDefaultImport(importNode); + if (!name) + continue; + const module = ts.getResolvedModule(sourceFile, moduleSpecifier.text, ts.getModeForUsageLocation(sourceFile, moduleSpecifier)); + const resolvedFile = module && program.getSourceFile(module.resolvedFileName); + if (resolvedFile && resolvedFile.externalModuleIndicator && resolvedFile.externalModuleIndicator !== true && ts.isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) { + diags.push(ts.createDiagnosticForNode(name, ts.Diagnostics.Import_may_be_converted_to_a_default_import)); } } + } - ts.addRange(diags, sourceFile.bindSuggestionDiagnostics); - ts.addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken)); - return diags.sort((d1, d2) => d1.start - d2.start); + ts.addRange(diags, sourceFile.bindSuggestionDiagnostics); + ts.addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken)); + return diags.sort((d1, d2) => d1.start - d2.start); - function check(node: ts.Node) { - if (isJsFile) { - if (canBeConvertedToClass(node, checker)) { - diags.push(ts.createDiagnosticForNode(ts.isVariableDeclaration(node.parent) ? node.parent.name : node, ts.Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); - } + function check(node: ts.Node) { + if (isJsFile) { + if (canBeConvertedToClass(node, checker)) { + diags.push(ts.createDiagnosticForNode(ts.isVariableDeclaration(node.parent) ? node.parent.name : node, ts.Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); } - else { - if (ts.isVariableStatement(node) && - node.parent === sourceFile && - node.declarationList.flags & ts.NodeFlags.Const && - node.declarationList.declarations.length === 1) { - const init = node.declarationList.declarations[0].initializer; - if (init && ts.isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { - diags.push(ts.createDiagnosticForNode(init, ts.Diagnostics.require_call_may_be_converted_to_an_import)); - } - } - - if (ts.codefix.parameterShouldGetTypeFromJSDoc(node)) { - diags.push(ts.createDiagnosticForNode(node.name || node, ts.Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); + } + else { + if (ts.isVariableStatement(node) && + node.parent === sourceFile && + node.declarationList.flags & ts.NodeFlags.Const && + node.declarationList.declarations.length === 1) { + const init = node.declarationList.declarations[0].initializer; + if (init && ts.isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { + diags.push(ts.createDiagnosticForNode(init, ts.Diagnostics.require_call_may_be_converted_to_an_import)); } } - if (canBeConvertedToAsync(node)) { - addConvertToAsyncFunctionDiagnostics(node, checker, diags); + if (ts.codefix.parameterShouldGetTypeFromJSDoc(node)) { + diags.push(ts.createDiagnosticForNode(node.name || node, ts.Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); } - node.forEachChild(check); } - } - // convertToEsModule only works on top-level, so don't trigger it if commonjs code only appears in nested scopes. - function containsTopLevelCommonjs(sourceFile: ts.SourceFile): boolean { - return sourceFile.statements.some(statement => { - switch (statement.kind) { - case ts.SyntaxKind.VariableStatement: - return (statement as ts.VariableStatement).declarationList.declarations.some(decl => !!decl.initializer && ts.isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true)); - case ts.SyntaxKind.ExpressionStatement: { - const { expression } = statement as ts.ExpressionStatement; - if (!ts.isBinaryExpression(expression)) - return ts.isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true); - const kind = ts.getAssignmentDeclarationKind(expression); - return kind === ts.AssignmentDeclarationKind.ExportsProperty || kind === ts.AssignmentDeclarationKind.ModuleExports; - } - default: - return false; - } - }); - } - - function propertyAccessLeftHandSide(node: ts.Expression): ts.Expression { - return ts.isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node; + if (canBeConvertedToAsync(node)) { + addConvertToAsyncFunctionDiagnostics(node, checker, diags); + } + node.forEachChild(check); } +} - function importNameForConvertToDefaultImport(node: ts.AnyValidImportOrReExport): ts.Identifier | undefined { - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - const { importClause, moduleSpecifier } = node; - return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport && ts.isStringLiteral(moduleSpecifier) - ? importClause.namedBindings.name - : undefined; - case ts.SyntaxKind.ImportEqualsDeclaration: - return node.name; +// convertToEsModule only works on top-level, so don't trigger it if commonjs code only appears in nested scopes. +function containsTopLevelCommonjs(sourceFile: ts.SourceFile): boolean { + return sourceFile.statements.some(statement => { + switch (statement.kind) { + case ts.SyntaxKind.VariableStatement: + return (statement as ts.VariableStatement).declarationList.declarations.some(decl => !!decl.initializer && ts.isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true)); + case ts.SyntaxKind.ExpressionStatement: { + const { expression } = statement as ts.ExpressionStatement; + if (!ts.isBinaryExpression(expression)) + return ts.isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true); + const kind = ts.getAssignmentDeclarationKind(expression); + return kind === ts.AssignmentDeclarationKind.ExportsProperty || kind === ts.AssignmentDeclarationKind.ModuleExports; + } default: - return undefined; + return false; } - } + }); +} - function addConvertToAsyncFunctionDiagnostics(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker, diags: ts.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(ts.createDiagnosticForNode(!node.name && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name) ? node.parent.name : node, ts.Diagnostics.This_may_be_converted_to_an_async_function)); - } - } +function propertyAccessLeftHandSide(node: ts.Expression): ts.Expression { + return ts.isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node; +} - function isConvertibleFunction(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker) { - return !ts.isAsyncFunction(node) && - node.body && - ts.isBlock(node.body) && - hasReturnStatementWithPromiseHandler(node.body, checker) && - returnsPromise(node, checker); +function importNameForConvertToDefaultImport(node: ts.AnyValidImportOrReExport): ts.Identifier | undefined { + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + const { importClause, moduleSpecifier } = node; + return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport && ts.isStringLiteral(moduleSpecifier) + ? importClause.namedBindings.name + : undefined; + case ts.SyntaxKind.ImportEqualsDeclaration: + return node.name; + default: + return undefined; } +} - export function returnsPromise(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): boolean { - const signature = checker.getSignatureFromDeclaration(node); - const returnType = signature ? checker.getReturnTypeOfSignature(signature) : undefined; - return !!returnType && !!checker.getPromisedTypeOfPromise(returnType); +function addConvertToAsyncFunctionDiagnostics(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker, diags: ts.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(ts.createDiagnosticForNode(!node.name && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name) ? node.parent.name : node, ts.Diagnostics.This_may_be_converted_to_an_async_function)); } +} - function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: ts.Node): ts.Node { - return ts.isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; - } +function isConvertibleFunction(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker) { + return !ts.isAsyncFunction(node) && + node.body && + ts.isBlock(node.body) && + hasReturnStatementWithPromiseHandler(node.body, checker) && + returnsPromise(node, checker); +} - function hasReturnStatementWithPromiseHandler(body: ts.Block, checker: ts.TypeChecker): boolean { - return !!ts.forEachReturnStatement(body, statement => isReturnStatementWithFixablePromiseHandler(statement, checker)); - } +export function returnsPromise(node: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): boolean { + const signature = checker.getSignatureFromDeclaration(node); + const returnType = signature ? checker.getReturnTypeOfSignature(signature) : undefined; + return !!returnType && !!checker.getPromisedTypeOfPromise(returnType); +} - export function isReturnStatementWithFixablePromiseHandler(node: ts.Node, checker: ts.TypeChecker): node is ts.ReturnStatement & { - expression: ts.CallExpression; - } { - return ts.isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression, checker); - } +function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: ts.Node): ts.Node { + return ts.isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; +} - // Should be kept up to date with transformExpression in convertToAsyncFunction.ts - export function isFixablePromiseHandler(node: ts.Node, checker: ts.TypeChecker): boolean { - // ensure outermost call exists and is a promise handler - if (!isPromiseHandler(node) || !hasSupportedNumberOfArguments(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { - return false; - } +function hasReturnStatementWithPromiseHandler(body: ts.Block, checker: ts.TypeChecker): boolean { + return !!ts.forEachReturnStatement(body, statement => isReturnStatementWithFixablePromiseHandler(statement, checker)); +} - // ensure all chained calls are valid - let currentNode = node.expression.expression; - while (isPromiseHandler(currentNode) || ts.isPropertyAccessExpression(currentNode)) { - if (ts.isCallExpression(currentNode)) { - if (!hasSupportedNumberOfArguments(currentNode) || !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { - return false; - } - currentNode = currentNode.expression.expression; - } - else { - currentNode = currentNode.expression; +export function isReturnStatementWithFixablePromiseHandler(node: ts.Node, checker: ts.TypeChecker): node is ts.ReturnStatement & { + expression: ts.CallExpression; +} { + return ts.isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression, checker); +} + +// Should be kept up to date with transformExpression in convertToAsyncFunction.ts +export function isFixablePromiseHandler(node: ts.Node, checker: ts.TypeChecker): boolean { + // ensure outermost call exists and is a promise handler + if (!isPromiseHandler(node) || !hasSupportedNumberOfArguments(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { + return false; + } + + // ensure all chained calls are valid + let currentNode = node.expression.expression; + while (isPromiseHandler(currentNode) || ts.isPropertyAccessExpression(currentNode)) { + if (ts.isCallExpression(currentNode)) { + if (!hasSupportedNumberOfArguments(currentNode) || !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { + return false; } + currentNode = currentNode.expression.expression; + } + else { + currentNode = currentNode.expression; } - return true; } + return true; +} - function isPromiseHandler(node: ts.Node): node is ts.CallExpression & { - readonly expression: ts.PropertyAccessExpression; - } { - return ts.isCallExpression(node) && (ts.hasPropertyAccessExpressionWithName(node, "then") || - ts.hasPropertyAccessExpressionWithName(node, "catch") || - ts.hasPropertyAccessExpressionWithName(node, "finally")); - } +function isPromiseHandler(node: ts.Node): node is ts.CallExpression & { + readonly expression: ts.PropertyAccessExpression; +} { + return ts.isCallExpression(node) && (ts.hasPropertyAccessExpressionWithName(node, "then") || + ts.hasPropertyAccessExpressionWithName(node, "catch") || + ts.hasPropertyAccessExpressionWithName(node, "finally")); +} - function hasSupportedNumberOfArguments(node: ts.CallExpression & { - readonly expression: ts.PropertyAccessExpression; - }) { - const name = node.expression.name.text; - const maxArguments = name === "then" ? 2 : name === "catch" ? 1 : name === "finally" ? 1 : 0; - if (node.arguments.length > maxArguments) - return false; - if (node.arguments.length < maxArguments) - return true; - return maxArguments === 1 || ts.some(node.arguments, arg => { - return arg.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(arg) && arg.text === "undefined"; - }); - } +function hasSupportedNumberOfArguments(node: ts.CallExpression & { + readonly expression: ts.PropertyAccessExpression; +}) { + const name = node.expression.name.text; + const maxArguments = name === "then" ? 2 : name === "catch" ? 1 : name === "finally" ? 1 : 0; + if (node.arguments.length > maxArguments) + return false; + if (node.arguments.length < maxArguments) + return true; + return maxArguments === 1 || ts.some(node.arguments, arg => { + return arg.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(arg) && arg.text === "undefined"; + }); +} - // should be kept up to date with getTransformationBody in convertToAsyncFunction.ts - function isFixablePromiseArgument(arg: ts.Expression, checker: ts.TypeChecker): boolean { - switch (arg.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - const functionFlags = ts.getFunctionFlags(arg as ts.FunctionDeclaration | ts.FunctionExpression); - if (functionFlags & ts.FunctionFlags.Generator) { - return false; - } - // falls through - case ts.SyntaxKind.ArrowFunction: - visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as ts.FunctionLikeDeclaration), true); - // falls through - case ts.SyntaxKind.NullKeyword: - return true; - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PropertyAccessExpression: { - const symbol = checker.getSymbolAtLocation(arg); - if (!symbol) { - return false; - } - return checker.isUndefinedSymbol(symbol) || - ts.some(ts.skipAlias(symbol, checker).declarations, d => ts.isFunctionLike(d) || ts.hasInitializer(d) && !!d.initializer && ts.isFunctionLike(d.initializer)); +// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts +function isFixablePromiseArgument(arg: ts.Expression, checker: ts.TypeChecker): boolean { + switch (arg.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + const functionFlags = ts.getFunctionFlags(arg as ts.FunctionDeclaration | ts.FunctionExpression); + if (functionFlags & ts.FunctionFlags.Generator) { + return false; } - default: + // falls through + case ts.SyntaxKind.ArrowFunction: + visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as ts.FunctionLikeDeclaration), true); + // falls through + case ts.SyntaxKind.NullKeyword: + return true; + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: { + const symbol = checker.getSymbolAtLocation(arg); + if (!symbol) { return false; + } + return checker.isUndefinedSymbol(symbol) || + ts.some(ts.skipAlias(symbol, checker).declarations, d => ts.isFunctionLike(d) || ts.hasInitializer(d) && !!d.initializer && ts.isFunctionLike(d.initializer)); } + default: + return false; } +} - function getKeyFromNode(exp: ts.FunctionLikeDeclaration) { - return `${exp.pos.toString()}:${exp.end.toString()}`; - } - - function canBeConvertedToClass(node: ts.Node, checker: ts.TypeChecker): boolean { - if (node.kind === ts.SyntaxKind.FunctionExpression) { - if (ts.isVariableDeclaration(node.parent) && node.symbol.members?.size) { - return true; - } +function getKeyFromNode(exp: ts.FunctionLikeDeclaration) { + return `${exp.pos.toString()}:${exp.end.toString()}`; +} - const symbol = checker.getSymbolOfExpando(node, /*allowDeclaration*/ false); - return !!(symbol && (symbol.exports?.size || symbol.members?.size)); +function canBeConvertedToClass(node: ts.Node, checker: ts.TypeChecker): boolean { + if (node.kind === ts.SyntaxKind.FunctionExpression) { + if (ts.isVariableDeclaration(node.parent) && node.symbol.members?.size) { + return true; } - if (node.kind === ts.SyntaxKind.FunctionDeclaration) { - return !!node.symbol.members?.size; - } + const symbol = checker.getSymbolOfExpando(node, /*allowDeclaration*/ false); + return !!(symbol && (symbol.exports?.size || symbol.members?.size)); + } - return false; + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + return !!node.symbol.members?.size; } - export function canBeConvertedToAsync(node: ts.Node): node is ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - return true; - default: - return false; - } + return false; +} + +export function canBeConvertedToAsync(node: ts.Node): node is ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return true; + default: + return false; } } +} diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index 69ad5ee664b08..1fd1080c5a574 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -1,237 +1,266 @@ /* @internal */ namespace ts.SymbolDisplay { - const symbolDisplayNodeBuilderFlags = ts.NodeBuilderFlags.OmitParameterModifiers | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; - - // TODO(drosen): use contextual SemanticMeaning. - export function getSymbolKind(typeChecker: ts.TypeChecker, symbol: ts.Symbol, location: ts.Node): ts.ScriptElementKind { - const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location); - if (result !== ts.ScriptElementKind.unknown) { - return result; - } - - const flags = ts.getCombinedLocalAndExportSymbolFlags(symbol); - if (flags & ts.SymbolFlags.Class) { - return ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ClassExpression) ? - ts.ScriptElementKind.localClassElement : ts.ScriptElementKind.classElement; - } - if (flags & ts.SymbolFlags.Enum) - return ts.ScriptElementKind.enumElement; - if (flags & ts.SymbolFlags.TypeAlias) - return ts.ScriptElementKind.typeElement; - if (flags & ts.SymbolFlags.Interface) - return ts.ScriptElementKind.interfaceElement; - if (flags & ts.SymbolFlags.TypeParameter) - return ts.ScriptElementKind.typeParameterElement; - if (flags & ts.SymbolFlags.EnumMember) - return ts.ScriptElementKind.enumMemberElement; - if (flags & ts.SymbolFlags.Alias) - return ts.ScriptElementKind.alias; - if (flags & ts.SymbolFlags.Module) - return ts.ScriptElementKind.moduleElement; +const symbolDisplayNodeBuilderFlags = ts.NodeBuilderFlags.OmitParameterModifiers | ts.NodeBuilderFlags.IgnoreErrors | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; +// TODO(drosen): use contextual SemanticMeaning. +export function getSymbolKind(typeChecker: ts.TypeChecker, symbol: ts.Symbol, location: ts.Node): ts.ScriptElementKind { + const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location); + if (result !== ts.ScriptElementKind.unknown) { return result; } - function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: ts.TypeChecker, symbol: ts.Symbol, location: ts.Node): ts.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 - && ts.first(roots).flags & ts.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 ts.ScriptElementKind.memberFunctionElement; - } + const flags = ts.getCombinedLocalAndExportSymbolFlags(symbol); + if (flags & ts.SymbolFlags.Class) { + return ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ClassExpression) ? + ts.ScriptElementKind.localClassElement : ts.ScriptElementKind.classElement; + } + if (flags & ts.SymbolFlags.Enum) + return ts.ScriptElementKind.enumElement; + if (flags & ts.SymbolFlags.TypeAlias) + return ts.ScriptElementKind.typeElement; + if (flags & ts.SymbolFlags.Interface) + return ts.ScriptElementKind.interfaceElement; + if (flags & ts.SymbolFlags.TypeParameter) + return ts.ScriptElementKind.typeParameterElement; + if (flags & ts.SymbolFlags.EnumMember) + return ts.ScriptElementKind.enumMemberElement; + if (flags & ts.SymbolFlags.Alias) + return ts.ScriptElementKind.alias; + if (flags & ts.SymbolFlags.Module) + return ts.ScriptElementKind.moduleElement; + + return result; +} + +function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: ts.TypeChecker, symbol: ts.Symbol, location: ts.Node): ts.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 + && ts.first(roots).flags & ts.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 ts.ScriptElementKind.memberFunctionElement; + } - if (typeChecker.isUndefinedSymbol(symbol)) { - return ts.ScriptElementKind.variableElement; + if (typeChecker.isUndefinedSymbol(symbol)) { + return ts.ScriptElementKind.variableElement; + } + if (typeChecker.isArgumentsSymbol(symbol)) { + return ts.ScriptElementKind.localVariableElement; + } + if (location.kind === ts.SyntaxKind.ThisKeyword && ts.isExpression(location) || ts.isThisInTypeQuery(location)) { + return ts.ScriptElementKind.parameterElement; + } + const flags = ts.getCombinedLocalAndExportSymbolFlags(symbol); + if (flags & ts.SymbolFlags.Variable) { + if (ts.isFirstDeclarationOfSymbolParameter(symbol)) { + return ts.ScriptElementKind.parameterElement; } - if (typeChecker.isArgumentsSymbol(symbol)) { - return ts.ScriptElementKind.localVariableElement; + else if (symbol.valueDeclaration && ts.isVarConst(symbol.valueDeclaration as ts.VariableDeclaration)) { + return ts.ScriptElementKind.constElement; } - if (location.kind === ts.SyntaxKind.ThisKeyword && ts.isExpression(location) || ts.isThisInTypeQuery(location)) { - return ts.ScriptElementKind.parameterElement; + else if (ts.forEach(symbol.declarations, ts.isLet)) { + return ts.ScriptElementKind.letElement; } - const flags = ts.getCombinedLocalAndExportSymbolFlags(symbol); - if (flags & ts.SymbolFlags.Variable) { - if (ts.isFirstDeclarationOfSymbolParameter(symbol)) { - return ts.ScriptElementKind.parameterElement; - } - else if (symbol.valueDeclaration && ts.isVarConst(symbol.valueDeclaration as ts.VariableDeclaration)) { - return ts.ScriptElementKind.constElement; - } - else if (ts.forEach(symbol.declarations, ts.isLet)) { - return ts.ScriptElementKind.letElement; - } - return isLocalVariableOrFunction(symbol) ? ts.ScriptElementKind.localVariableElement : ts.ScriptElementKind.variableElement; - } - if (flags & ts.SymbolFlags.Function) - return isLocalVariableOrFunction(symbol) ? ts.ScriptElementKind.localFunctionElement : ts.ScriptElementKind.functionElement; - // FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag. - // So, even when the location is just on the declaration of setter, this function returns getter. - if (flags & ts.SymbolFlags.GetAccessor) - return ts.ScriptElementKind.memberGetAccessorElement; - if (flags & ts.SymbolFlags.SetAccessor) - return ts.ScriptElementKind.memberSetAccessorElement; - if (flags & ts.SymbolFlags.Method) - return ts.ScriptElementKind.memberFunctionElement; - if (flags & ts.SymbolFlags.Constructor) - return ts.ScriptElementKind.constructorImplementationElement; - if (flags & ts.SymbolFlags.Property) { - if (flags & ts.SymbolFlags.Transient && (symbol as ts.TransientSymbol).checkFlags & ts.CheckFlags.Synthetic) { - // If union property is result of union of non method (property/accessors/variables), it is labeled as property - const unionPropertyKind = ts.forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { - const rootSymbolFlags = rootSymbol.getFlags(); - if (rootSymbolFlags & (ts.SymbolFlags.PropertyOrAccessor | ts.SymbolFlags.Variable)) { - return ts.ScriptElementKind.memberVariableElement; - } - }); - 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 ts.ScriptElementKind.memberFunctionElement; - } + return isLocalVariableOrFunction(symbol) ? ts.ScriptElementKind.localVariableElement : ts.ScriptElementKind.variableElement; + } + if (flags & ts.SymbolFlags.Function) + return isLocalVariableOrFunction(symbol) ? ts.ScriptElementKind.localFunctionElement : ts.ScriptElementKind.functionElement; + // FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag. + // So, even when the location is just on the declaration of setter, this function returns getter. + if (flags & ts.SymbolFlags.GetAccessor) + return ts.ScriptElementKind.memberGetAccessorElement; + if (flags & ts.SymbolFlags.SetAccessor) + return ts.ScriptElementKind.memberSetAccessorElement; + if (flags & ts.SymbolFlags.Method) + return ts.ScriptElementKind.memberFunctionElement; + if (flags & ts.SymbolFlags.Constructor) + return ts.ScriptElementKind.constructorImplementationElement; + if (flags & ts.SymbolFlags.Property) { + if (flags & ts.SymbolFlags.Transient && (symbol as ts.TransientSymbol).checkFlags & ts.CheckFlags.Synthetic) { + // If union property is result of union of non method (property/accessors/variables), it is labeled as property + const unionPropertyKind = ts.forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { + const rootSymbolFlags = rootSymbol.getFlags(); + if (rootSymbolFlags & (ts.SymbolFlags.PropertyOrAccessor | ts.SymbolFlags.Variable)) { return ts.ScriptElementKind.memberVariableElement; } - return unionPropertyKind; + }); + 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 ts.ScriptElementKind.memberFunctionElement; + } + return ts.ScriptElementKind.memberVariableElement; } - - return ts.ScriptElementKind.memberVariableElement; + return unionPropertyKind; } - return ts.ScriptElementKind.unknown; + return ts.ScriptElementKind.memberVariableElement; } - function getNormalizedSymbolModifiers(symbol: ts.Symbol) { - if (symbol.declarations && symbol.declarations.length) { - const [declaration, ...declarations] = symbol.declarations; - // omit deprecated flag if some declarations are not deprecated - const excludeFlags = ts.length(declarations) && ts.isDeprecatedDeclaration(declaration) && ts.some(declarations, d => !ts.isDeprecatedDeclaration(d)) - ? ts.ModifierFlags.Deprecated - : ts.ModifierFlags.None; - const modifiers = ts.getNodeModifiers(declaration, excludeFlags); - if (modifiers) { - return modifiers.split(","); - } + return ts.ScriptElementKind.unknown; +} + +function getNormalizedSymbolModifiers(symbol: ts.Symbol) { + if (symbol.declarations && symbol.declarations.length) { + const [declaration, ...declarations] = symbol.declarations; + // omit deprecated flag if some declarations are not deprecated + const excludeFlags = ts.length(declarations) && ts.isDeprecatedDeclaration(declaration) && ts.some(declarations, d => !ts.isDeprecatedDeclaration(d)) + ? ts.ModifierFlags.Deprecated + : ts.ModifierFlags.None; + const modifiers = ts.getNodeModifiers(declaration, excludeFlags); + if (modifiers) { + return modifiers.split(","); } - return []; } + return []; +} - export function getSymbolModifiers(typeChecker: ts.TypeChecker, symbol: ts.Symbol): string { - if (!symbol) { - return ts.ScriptElementKindModifier.none; - } +export function getSymbolModifiers(typeChecker: ts.TypeChecker, symbol: ts.Symbol): string { + if (!symbol) { + return ts.ScriptElementKindModifier.none; + } - const modifiers = new ts.Set(getNormalizedSymbolModifiers(symbol)); - if (symbol.flags & ts.SymbolFlags.Alias) { - const resolvedSymbol = typeChecker.getAliasedSymbol(symbol); - if (resolvedSymbol !== symbol) { - ts.forEach(getNormalizedSymbolModifiers(resolvedSymbol), modifier => { - modifiers.add(modifier); - }); - } + const modifiers = new ts.Set(getNormalizedSymbolModifiers(symbol)); + if (symbol.flags & ts.SymbolFlags.Alias) { + const resolvedSymbol = typeChecker.getAliasedSymbol(symbol); + if (resolvedSymbol !== symbol) { + ts.forEach(getNormalizedSymbolModifiers(resolvedSymbol), modifier => { + modifiers.add(modifier); + }); } - if (symbol.flags & ts.SymbolFlags.Optional) { - modifiers.add(ts.ScriptElementKindModifier.optionalModifier); - } - return modifiers.size > 0 ? ts.arrayFrom(modifiers.values()).join(",") : ts.ScriptElementKindModifier.none; - } - - interface SymbolDisplayPartsDocumentationAndSymbolKind { - displayParts: ts.SymbolDisplayPart[]; - documentation: ts.SymbolDisplayPart[]; - symbolKind: ts.ScriptElementKind; - tags: ts.JSDocTagInfo[] | undefined; - } - - // TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location - export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: ts.TypeChecker, symbol: ts.Symbol, sourceFile: ts.SourceFile, enclosingDeclaration: ts.Node | undefined, location: ts.Node, semanticMeaning = ts.getMeaningFromLocation(location), alias?: ts.Symbol): SymbolDisplayPartsDocumentationAndSymbolKind { - const displayParts: ts.SymbolDisplayPart[] = []; - let documentation: ts.SymbolDisplayPart[] = []; - let tags: ts.JSDocTagInfo[] = []; - const symbolFlags = ts.getCombinedLocalAndExportSymbolFlags(symbol); - let symbolKind = semanticMeaning & ts.SemanticMeaning.Value ? getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) : ts.ScriptElementKind.unknown; - let hasAddedSymbolInfo = false; - const isThisExpression = location.kind === ts.SyntaxKind.ThisKeyword && ts.isInExpressionContext(location) || ts.isThisInTypeQuery(location); - let type: ts.Type | undefined; - let printer: ts.Printer; - let documentationFromAlias: ts.SymbolDisplayPart[] | undefined; - let tagsFromAlias: ts.JSDocTagInfo[] | undefined; - let hasMultipleSignatures = false; - - if (location.kind === ts.SyntaxKind.ThisKeyword && !isThisExpression) { - return { displayParts: [ts.keywordPart(ts.SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ts.ScriptElementKind.primitiveType, tags: undefined }; - } - - // Class at constructor site need to be shown as constructor apart from property,method, vars - if (symbolKind !== ts.ScriptElementKind.unknown || symbolFlags & ts.SymbolFlags.Class || symbolFlags & ts.SymbolFlags.Alias) { - // If symbol is accessor, they are allowed only if location is at declaration identifier of the accessor - if (symbolKind === ts.ScriptElementKind.memberGetAccessorElement || symbolKind === ts.ScriptElementKind.memberSetAccessorElement) { - const declaration = ts.find(symbol.declarations as ((ts.GetAccessorDeclaration | ts.SetAccessorDeclaration)[]), declaration => declaration.name === location); - if (declaration) { - switch(declaration.kind){ - case ts.SyntaxKind.GetAccessor: - symbolKind = ts.ScriptElementKind.memberGetAccessorElement; - break; - case ts.SyntaxKind.SetAccessor: - symbolKind = ts.ScriptElementKind.memberSetAccessorElement; - break; - default: - ts.Debug.assertNever(declaration); - } - } - else { - symbolKind = ts.ScriptElementKind.memberVariableElement; - } - } + } + if (symbol.flags & ts.SymbolFlags.Optional) { + modifiers.add(ts.ScriptElementKindModifier.optionalModifier); + } + return modifiers.size > 0 ? ts.arrayFrom(modifiers.values()).join(",") : ts.ScriptElementKindModifier.none; +} - let signature: ts.Signature | undefined; - type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol, location); +interface SymbolDisplayPartsDocumentationAndSymbolKind { + displayParts: ts.SymbolDisplayPart[]; + documentation: ts.SymbolDisplayPart[]; + symbolKind: ts.ScriptElementKind; + tags: ts.JSDocTagInfo[] | undefined; +} - if (location.parent && location.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { - const right = (location.parent as ts.PropertyAccessExpression).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; - } - } +// TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location +export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: ts.TypeChecker, symbol: ts.Symbol, sourceFile: ts.SourceFile, enclosingDeclaration: ts.Node | undefined, location: ts.Node, semanticMeaning = ts.getMeaningFromLocation(location), alias?: ts.Symbol): SymbolDisplayPartsDocumentationAndSymbolKind { + const displayParts: ts.SymbolDisplayPart[] = []; + let documentation: ts.SymbolDisplayPart[] = []; + let tags: ts.JSDocTagInfo[] = []; + const symbolFlags = ts.getCombinedLocalAndExportSymbolFlags(symbol); + let symbolKind = semanticMeaning & ts.SemanticMeaning.Value ? getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) : ts.ScriptElementKind.unknown; + let hasAddedSymbolInfo = false; + const isThisExpression = location.kind === ts.SyntaxKind.ThisKeyword && ts.isInExpressionContext(location) || ts.isThisInTypeQuery(location); + let type: ts.Type | undefined; + let printer: ts.Printer; + let documentationFromAlias: ts.SymbolDisplayPart[] | undefined; + let tagsFromAlias: ts.JSDocTagInfo[] | undefined; + let hasMultipleSignatures = false; + + if (location.kind === ts.SyntaxKind.ThisKeyword && !isThisExpression) { + return { displayParts: [ts.keywordPart(ts.SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ts.ScriptElementKind.primitiveType, tags: undefined }; + } - // try get the call/construct signature from the type if it matches - let callExpressionLike: ts.CallExpression | ts.NewExpression | ts.JsxOpeningLikeElement | ts.TaggedTemplateExpression | undefined; - if (ts.isCallOrNewExpression(location)) { - callExpressionLike = location; + // Class at constructor site need to be shown as constructor apart from property,method, vars + if (symbolKind !== ts.ScriptElementKind.unknown || symbolFlags & ts.SymbolFlags.Class || symbolFlags & ts.SymbolFlags.Alias) { + // If symbol is accessor, they are allowed only if location is at declaration identifier of the accessor + if (symbolKind === ts.ScriptElementKind.memberGetAccessorElement || symbolKind === ts.ScriptElementKind.memberSetAccessorElement) { + const declaration = ts.find(symbol.declarations as ((ts.GetAccessorDeclaration | ts.SetAccessorDeclaration)[]), declaration => declaration.name === location); + if (declaration) { + switch(declaration.kind){ + case ts.SyntaxKind.GetAccessor: + symbolKind = ts.ScriptElementKind.memberGetAccessorElement; + break; + case ts.SyntaxKind.SetAccessor: + symbolKind = ts.ScriptElementKind.memberSetAccessorElement; + break; + default: + ts.Debug.assertNever(declaration); + } } - else if (ts.isCallExpressionTarget(location) || ts.isNewExpressionTarget(location)) { - callExpressionLike = location.parent as ts.CallExpression | ts.NewExpression; + else { + symbolKind = ts.ScriptElementKind.memberVariableElement; } - else if (location.parent && (ts.isJsxOpeningLikeElement(location.parent) || ts.isTaggedTemplateExpression(location.parent)) && ts.isFunctionLike(symbol.valueDeclaration)) { - callExpressionLike = location.parent; + } + + let signature: ts.Signature | undefined; + type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol, location); + + if (location.parent && location.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + const right = (location.parent as ts.PropertyAccessExpression).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: ts.CallExpression | ts.NewExpression | ts.JsxOpeningLikeElement | ts.TaggedTemplateExpression | undefined; + if (ts.isCallOrNewExpression(location)) { + callExpressionLike = location; + } + else if (ts.isCallExpressionTarget(location) || ts.isNewExpressionTarget(location)) { + callExpressionLike = location.parent as ts.CallExpression | ts.NewExpression; + } + else if (location.parent && (ts.isJsxOpeningLikeElement(location.parent) || ts.isTaggedTemplateExpression(location.parent)) && ts.isFunctionLike(symbol.valueDeclaration)) { + callExpressionLike = location.parent; + } - if (callExpressionLike) { - signature = typeChecker.getResolvedSignature(callExpressionLike); // TODO: GH#18217 + if (callExpressionLike) { + signature = typeChecker.getResolvedSignature(callExpressionLike); // TODO: GH#18217 - const useConstructSignatures = callExpressionLike.kind === ts.SyntaxKind.NewExpression || (ts.isCallExpression(callExpressionLike) && callExpressionLike.expression.kind === ts.SyntaxKind.SuperKeyword); + const useConstructSignatures = callExpressionLike.kind === ts.SyntaxKind.NewExpression || (ts.isCallExpression(callExpressionLike) && callExpressionLike.expression.kind === ts.SyntaxKind.SuperKeyword); - const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); + const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); - if (signature && !ts.contains(allSignatures, signature.target) && !ts.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 && !ts.contains(allSignatures, signature.target) && !ts.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 & ts.SymbolFlags.Class)) { - // Constructor - symbolKind = ts.ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + if (signature) { + if (useConstructSignatures && (symbolFlags & ts.SymbolFlags.Class)) { + // Constructor + symbolKind = ts.ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + } + else if (symbolFlags & ts.SymbolFlags.Alias) { + symbolKind = ts.ScriptElementKind.alias; + pushSymbolKind(symbolKind); + displayParts.push(ts.spacePart()); + if (useConstructSignatures) { + if (signature.flags & ts.SignatureFlags.Abstract) { + displayParts.push(ts.keywordPart(ts.SyntaxKind.AbstractKeyword)); + displayParts.push(ts.spacePart()); + } + displayParts.push(ts.keywordPart(ts.SyntaxKind.NewKeyword)); + displayParts.push(ts.spacePart()); } - else if (symbolFlags & ts.SymbolFlags.Alias) { - symbolKind = ts.ScriptElementKind.alias; - pushSymbolKind(symbolKind); + addFullSymbolName(symbol); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + + switch (symbolKind) { + case ts.ScriptElementKind.jsxAttribute: + case ts.ScriptElementKind.memberVariableElement: + case ts.ScriptElementKind.variableElement: + case ts.ScriptElementKind.constElement: + case ts.ScriptElementKind.letElement: + case ts.ScriptElementKind.parameterElement: + case ts.ScriptElementKind.localVariableElement: + // If it is call or construct signature of lambda's write type name + displayParts.push(ts.punctuationPart(ts.SyntaxKind.ColonToken)); displayParts.push(ts.spacePart()); + if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous) && type.symbol) { + ts.addRange(displayParts, ts.symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, ts.SymbolFormatFlags.AllowAnyNodeKind | ts.SymbolFormatFlags.WriteTypeParametersOrArguments)); + displayParts.push(ts.lineBreakPart()); + } if (useConstructSignatures) { if (signature.flags & ts.SignatureFlags.Abstract) { displayParts.push(ts.keywordPart(ts.SyntaxKind.AbstractKeyword)); @@ -240,498 +269,469 @@ namespace ts.SymbolDisplay { displayParts.push(ts.keywordPart(ts.SyntaxKind.NewKeyword)); displayParts.push(ts.spacePart()); } - addFullSymbolName(symbol); - } - else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); - } - - switch (symbolKind) { - case ts.ScriptElementKind.jsxAttribute: - case ts.ScriptElementKind.memberVariableElement: - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.constElement: - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.parameterElement: - case ts.ScriptElementKind.localVariableElement: - // If it is call or construct signature of lambda's write type name - displayParts.push(ts.punctuationPart(ts.SyntaxKind.ColonToken)); - displayParts.push(ts.spacePart()); - if (!(ts.getObjectFlags(type) & ts.ObjectFlags.Anonymous) && type.symbol) { - ts.addRange(displayParts, ts.symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, ts.SymbolFormatFlags.AllowAnyNodeKind | ts.SymbolFormatFlags.WriteTypeParametersOrArguments)); - displayParts.push(ts.lineBreakPart()); - } - if (useConstructSignatures) { - if (signature.flags & ts.SignatureFlags.Abstract) { - displayParts.push(ts.keywordPart(ts.SyntaxKind.AbstractKeyword)); - displayParts.push(ts.spacePart()); - } - displayParts.push(ts.keywordPart(ts.SyntaxKind.NewKeyword)); - displayParts.push(ts.spacePart()); - } - addSignatureDisplayParts(signature, allSignatures, ts.TypeFormatFlags.WriteArrowStyleSignature); - break; - - default: - // Just signature - addSignatureDisplayParts(signature, allSignatures); - } - hasAddedSymbolInfo = true; - hasMultipleSignatures = allSignatures.length > 1; - } - } - else if ((ts.isNameOfFunctionDeclaration(location) && !(symbolFlags & ts.SymbolFlags.Accessor)) || // name of function declaration - (location.kind === ts.SyntaxKind.ConstructorKeyword && location.parent.kind === ts.SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration - // get the signature from the declaration and write it - const functionDeclaration = location.parent as ts.SignatureDeclaration; - // Use function declaration to write the signatures only if the symbol corresponding to this declaration - const locationIsSymbolDeclaration = symbol.declarations && ts.find(symbol.declarations, declaration => declaration === (location.kind === ts.SyntaxKind.ConstructorKeyword ? functionDeclaration.parent : functionDeclaration)); - - if (locationIsSymbolDeclaration) { - const allSignatures = functionDeclaration.kind === ts.SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures(); - if (!typeChecker.isImplementationOfOverload(functionDeclaration)) { - signature = typeChecker.getSignatureFromDeclaration(functionDeclaration); // TODO: GH#18217 - } - else { - signature = allSignatures[0]; - } + addSignatureDisplayParts(signature, allSignatures, ts.TypeFormatFlags.WriteArrowStyleSignature); + break; - if (functionDeclaration.kind === ts.SyntaxKind.Constructor) { - // show (constructor) Type(...) signature - symbolKind = ts.ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); - } - else { - // (function/method) symbol(..signature) - addPrefixForAnyFunctionOrVar(functionDeclaration.kind === ts.SyntaxKind.CallSignature && - !(type.symbol.flags & ts.SymbolFlags.TypeLiteral || type.symbol.flags & ts.SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); - } - if (signature) { + default: + // Just signature addSignatureDisplayParts(signature, allSignatures); - } - hasAddedSymbolInfo = true; - hasMultipleSignatures = allSignatures.length > 1; } + hasAddedSymbolInfo = true; + hasMultipleSignatures = allSignatures.length > 1; } } - if (symbolFlags & ts.SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { - addAliasPrefixIfNecessary(); - if (ts.getDeclarationOfKind(symbol, ts.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(ts.ScriptElementKind.localClassElement); - } - else { - // Class declaration has name which is not local. - displayParts.push(ts.keywordPart(ts.SyntaxKind.ClassKeyword)); - } - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if ((symbolFlags & ts.SymbolFlags.Interface) && (semanticMeaning & ts.SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(ts.keywordPart(ts.SyntaxKind.InterfaceKeyword)); - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if ((symbolFlags & ts.SymbolFlags.TypeAlias) && (semanticMeaning & ts.SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(ts.keywordPart(ts.SyntaxKind.TypeKeyword)); - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - displayParts.push(ts.spacePart()); - displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); - displayParts.push(ts.spacePart()); - ts.addRange(displayParts, ts.typeToDisplayParts(typeChecker, ts.isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, ts.TypeFormatFlags.InTypeAlias)); - } - if (symbolFlags & ts.SymbolFlags.Enum) { - prefixNextMeaning(); - if (ts.some(symbol.declarations, d => ts.isEnumDeclaration(d) && ts.isEnumConst(d))) { - displayParts.push(ts.keywordPart(ts.SyntaxKind.ConstKeyword)); - displayParts.push(ts.spacePart()); + else if ((ts.isNameOfFunctionDeclaration(location) && !(symbolFlags & ts.SymbolFlags.Accessor)) || // name of function declaration + (location.kind === ts.SyntaxKind.ConstructorKeyword && location.parent.kind === ts.SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration + // get the signature from the declaration and write it + const functionDeclaration = location.parent as ts.SignatureDeclaration; + // Use function declaration to write the signatures only if the symbol corresponding to this declaration + const locationIsSymbolDeclaration = symbol.declarations && ts.find(symbol.declarations, declaration => declaration === (location.kind === ts.SyntaxKind.ConstructorKeyword ? functionDeclaration.parent : functionDeclaration)); + + if (locationIsSymbolDeclaration) { + const allSignatures = functionDeclaration.kind === ts.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 === ts.SyntaxKind.Constructor) { + // show (constructor) Type(...) signature + symbolKind = ts.ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + } + else { + // (function/method) symbol(..signature) + addPrefixForAnyFunctionOrVar(functionDeclaration.kind === ts.SyntaxKind.CallSignature && + !(type.symbol.flags & ts.SymbolFlags.TypeLiteral || type.symbol.flags & ts.SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); + } + if (signature) { + addSignatureDisplayParts(signature, allSignatures); + } + hasAddedSymbolInfo = true; + hasMultipleSignatures = allSignatures.length > 1; } - displayParts.push(ts.keywordPart(ts.SyntaxKind.EnumKeyword)); - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); } - if (symbolFlags & ts.SymbolFlags.Module && !isThisExpression) { - prefixNextMeaning(); - const declaration = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ModuleDeclaration); - const isNamespace = declaration && declaration.name && declaration.name.kind === ts.SyntaxKind.Identifier; - displayParts.push(ts.keywordPart(isNamespace ? ts.SyntaxKind.NamespaceKeyword : ts.SyntaxKind.ModuleKeyword)); + } + if (symbolFlags & ts.SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { + addAliasPrefixIfNecessary(); + if (ts.getDeclarationOfKind(symbol, ts.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(ts.ScriptElementKind.localClassElement); + } + else { + // Class declaration has name which is not local. + displayParts.push(ts.keywordPart(ts.SyntaxKind.ClassKeyword)); + } + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if ((symbolFlags & ts.SymbolFlags.Interface) && (semanticMeaning & ts.SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(ts.keywordPart(ts.SyntaxKind.InterfaceKeyword)); + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if ((symbolFlags & ts.SymbolFlags.TypeAlias) && (semanticMeaning & ts.SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(ts.keywordPart(ts.SyntaxKind.TypeKeyword)); + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + displayParts.push(ts.spacePart()); + displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); + displayParts.push(ts.spacePart()); + ts.addRange(displayParts, ts.typeToDisplayParts(typeChecker, ts.isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, ts.TypeFormatFlags.InTypeAlias)); + } + if (symbolFlags & ts.SymbolFlags.Enum) { + prefixNextMeaning(); + if (ts.some(symbol.declarations, d => ts.isEnumDeclaration(d) && ts.isEnumConst(d))) { + displayParts.push(ts.keywordPart(ts.SyntaxKind.ConstKeyword)); displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); } - if ((symbolFlags & ts.SymbolFlags.TypeParameter) && (semanticMeaning & ts.SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); - displayParts.push(ts.textPart("type parameter")); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); - displayParts.push(ts.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 = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.TypeParameter); - if (decl === undefined) - return ts.Debug.fail(); - const declaration = decl.parent; - - if (declaration) { - if (ts.isFunctionLikeKind(declaration.kind)) { - addInPrefix(); - const signature = typeChecker.getSignatureFromDeclaration(declaration as ts.SignatureDeclaration)!; // TODO: GH#18217 - if (declaration.kind === ts.SyntaxKind.ConstructSignature) { - displayParts.push(ts.keywordPart(ts.SyntaxKind.NewKeyword)); - displayParts.push(ts.spacePart()); - } - else if (declaration.kind !== ts.SyntaxKind.CallSignature && (declaration as ts.SignatureDeclaration).name) { - addFullSymbolName(declaration.symbol); - } - ts.addRange(displayParts, ts.signatureToDisplayParts(typeChecker, signature, sourceFile, ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)); - } - else if (declaration.kind === ts.SyntaxKind.TypeAliasDeclaration) { - // Type alias type parameter - // For example - // type list = T[]; // Both T will go through same code path - addInPrefix(); - displayParts.push(ts.keywordPart(ts.SyntaxKind.TypeKeyword)); + displayParts.push(ts.keywordPart(ts.SyntaxKind.EnumKeyword)); + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + } + if (symbolFlags & ts.SymbolFlags.Module && !isThisExpression) { + prefixNextMeaning(); + const declaration = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.ModuleDeclaration); + const isNamespace = declaration && declaration.name && declaration.name.kind === ts.SyntaxKind.Identifier; + displayParts.push(ts.keywordPart(isNamespace ? ts.SyntaxKind.NamespaceKeyword : ts.SyntaxKind.ModuleKeyword)); + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + } + if ((symbolFlags & ts.SymbolFlags.TypeParameter) && (semanticMeaning & ts.SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); + displayParts.push(ts.textPart("type parameter")); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); + displayParts.push(ts.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 = ts.getDeclarationOfKind(symbol, ts.SyntaxKind.TypeParameter); + if (decl === undefined) + return ts.Debug.fail(); + const declaration = decl.parent; + + if (declaration) { + if (ts.isFunctionLikeKind(declaration.kind)) { + addInPrefix(); + const signature = typeChecker.getSignatureFromDeclaration(declaration as ts.SignatureDeclaration)!; // TODO: GH#18217 + if (declaration.kind === ts.SyntaxKind.ConstructSignature) { + displayParts.push(ts.keywordPart(ts.SyntaxKind.NewKeyword)); displayParts.push(ts.spacePart()); + } + else if (declaration.kind !== ts.SyntaxKind.CallSignature && (declaration as ts.SignatureDeclaration).name) { addFullSymbolName(declaration.symbol); - writeTypeParametersOfSymbol(declaration.symbol, sourceFile); } + ts.addRange(displayParts, ts.signatureToDisplayParts(typeChecker, signature, sourceFile, ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)); } - } - } - if (symbolFlags & ts.SymbolFlags.EnumMember) { - symbolKind = ts.ScriptElementKind.enumMemberElement; - addPrefixForAnyFunctionOrVar(symbol, "enum member"); - const declaration = symbol.declarations?.[0]; - if (declaration?.kind === ts.SyntaxKind.EnumMember) { - const constantValue = typeChecker.getConstantValue(declaration as ts.EnumMember); - if (constantValue !== undefined) { - displayParts.push(ts.spacePart()); - displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); + else if (declaration.kind === ts.SyntaxKind.TypeAliasDeclaration) { + // Type alias type parameter + // For example + // type list = T[]; // Both T will go through same code path + addInPrefix(); + displayParts.push(ts.keywordPart(ts.SyntaxKind.TypeKeyword)); displayParts.push(ts.spacePart()); - displayParts.push(ts.displayPart(ts.getTextOfConstantValue(constantValue), typeof constantValue === "number" ? ts.SymbolDisplayPartKind.numericLiteral : ts.SymbolDisplayPartKind.stringLiteral)); + addFullSymbolName(declaration.symbol); + writeTypeParametersOfSymbol(declaration.symbol, sourceFile); } } } - // don't use symbolFlags since getAliasedSymbol requires the flag on the symbol itself - if (symbol.flags & ts.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 = ts.getNameOfDeclaration(resolvedNode); - if (declarationName) { - const isExternalModuleDeclaration = ts.isModuleWithStringLiteralName(resolvedNode) && - ts.hasSyntacticModifier(resolvedNode, ts.ModifierFlags.Ambient); - const shouldUseAliasName = symbol.name !== "default" && !isExternalModuleDeclaration; - const resolvedInfo = getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, resolvedSymbol, ts.getSourceFileOfNode(resolvedNode), resolvedNode, declarationName, semanticMeaning, shouldUseAliasName ? symbol : resolvedSymbol); - displayParts.push(...resolvedInfo.displayParts); - displayParts.push(ts.lineBreakPart()); - documentationFromAlias = resolvedInfo.documentation; - tagsFromAlias = resolvedInfo.tags; - } - else { - documentationFromAlias = resolvedSymbol.getContextualDocumentationComment(resolvedNode, typeChecker); - tagsFromAlias = resolvedSymbol.getJsDocTags(typeChecker); - } + } + if (symbolFlags & ts.SymbolFlags.EnumMember) { + symbolKind = ts.ScriptElementKind.enumMemberElement; + addPrefixForAnyFunctionOrVar(symbol, "enum member"); + const declaration = symbol.declarations?.[0]; + if (declaration?.kind === ts.SyntaxKind.EnumMember) { + const constantValue = typeChecker.getConstantValue(declaration as ts.EnumMember); + if (constantValue !== undefined) { + displayParts.push(ts.spacePart()); + displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); + displayParts.push(ts.spacePart()); + displayParts.push(ts.displayPart(ts.getTextOfConstantValue(constantValue), typeof constantValue === "number" ? ts.SymbolDisplayPartKind.numericLiteral : ts.SymbolDisplayPartKind.stringLiteral)); + } + } + } + // don't use symbolFlags since getAliasedSymbol requires the flag on the symbol itself + if (symbol.flags & ts.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 = ts.getNameOfDeclaration(resolvedNode); + if (declarationName) { + const isExternalModuleDeclaration = ts.isModuleWithStringLiteralName(resolvedNode) && + ts.hasSyntacticModifier(resolvedNode, ts.ModifierFlags.Ambient); + const shouldUseAliasName = symbol.name !== "default" && !isExternalModuleDeclaration; + const resolvedInfo = getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, resolvedSymbol, ts.getSourceFileOfNode(resolvedNode), resolvedNode, declarationName, semanticMeaning, shouldUseAliasName ? symbol : resolvedSymbol); + displayParts.push(...resolvedInfo.displayParts); + displayParts.push(ts.lineBreakPart()); + documentationFromAlias = resolvedInfo.documentation; + tagsFromAlias = resolvedInfo.tags; + } + else { + documentationFromAlias = resolvedSymbol.getContextualDocumentationComment(resolvedNode, typeChecker); + tagsFromAlias = resolvedSymbol.getJsDocTags(typeChecker); } } + } - if (symbol.declarations) { - switch (symbol.declarations[0].kind) { - case ts.SyntaxKind.NamespaceExportDeclaration: - displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); - displayParts.push(ts.spacePart()); - displayParts.push(ts.keywordPart(ts.SyntaxKind.NamespaceKeyword)); - break; - case ts.SyntaxKind.ExportAssignment: - displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); - displayParts.push(ts.spacePart()); - displayParts.push(ts.keywordPart((symbol.declarations[0] as ts.ExportAssignment).isExportEquals ? ts.SyntaxKind.EqualsToken : ts.SyntaxKind.DefaultKeyword)); - break; - case ts.SyntaxKind.ExportSpecifier: - displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); - break; - default: - displayParts.push(ts.keywordPart(ts.SyntaxKind.ImportKeyword)); - } + if (symbol.declarations) { + switch (symbol.declarations[0].kind) { + case ts.SyntaxKind.NamespaceExportDeclaration: + displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); + displayParts.push(ts.spacePart()); + displayParts.push(ts.keywordPart(ts.SyntaxKind.NamespaceKeyword)); + break; + case ts.SyntaxKind.ExportAssignment: + displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); + displayParts.push(ts.spacePart()); + displayParts.push(ts.keywordPart((symbol.declarations[0] as ts.ExportAssignment).isExportEquals ? ts.SyntaxKind.EqualsToken : ts.SyntaxKind.DefaultKeyword)); + break; + case ts.SyntaxKind.ExportSpecifier: + displayParts.push(ts.keywordPart(ts.SyntaxKind.ExportKeyword)); + break; + default: + displayParts.push(ts.keywordPart(ts.SyntaxKind.ImportKeyword)); } - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); - ts.forEach(symbol.declarations, declaration => { - if (declaration.kind === ts.SyntaxKind.ImportEqualsDeclaration) { - const importEqualsDeclaration = declaration as ts.ImportEqualsDeclaration; - if (ts.isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { + } + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); + ts.forEach(symbol.declarations, declaration => { + if (declaration.kind === ts.SyntaxKind.ImportEqualsDeclaration) { + const importEqualsDeclaration = declaration as ts.ImportEqualsDeclaration; + if (ts.isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { + displayParts.push(ts.spacePart()); + displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); + displayParts.push(ts.spacePart()); + displayParts.push(ts.keywordPart(ts.SyntaxKind.RequireKeyword)); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); + displayParts.push(ts.displayPart(ts.getTextOfNode(ts.getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), ts.SymbolDisplayPartKind.stringLiteral)); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); + } + else { + const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); + if (internalAliasSymbol) { displayParts.push(ts.spacePart()); displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); displayParts.push(ts.spacePart()); - displayParts.push(ts.keywordPart(ts.SyntaxKind.RequireKeyword)); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); - displayParts.push(ts.displayPart(ts.getTextOfNode(ts.getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), ts.SymbolDisplayPartKind.stringLiteral)); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); + addFullSymbolName(internalAliasSymbol, enclosingDeclaration); } - else { - const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); - if (internalAliasSymbol) { - displayParts.push(ts.spacePart()); - displayParts.push(ts.operatorPart(ts.SyntaxKind.EqualsToken)); - displayParts.push(ts.spacePart()); - addFullSymbolName(internalAliasSymbol, enclosingDeclaration); - } - } - return true; } - }); - } - if (!hasAddedSymbolInfo) { - if (symbolKind !== ts.ScriptElementKind.unknown) { - if (type) { - if (isThisExpression) { - prefixNextMeaning(); - displayParts.push(ts.keywordPart(ts.SyntaxKind.ThisKeyword)); + return true; + } + }); + } + if (!hasAddedSymbolInfo) { + if (symbolKind !== ts.ScriptElementKind.unknown) { + if (type) { + if (isThisExpression) { + prefixNextMeaning(); + displayParts.push(ts.keywordPart(ts.SyntaxKind.ThisKeyword)); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + + // For properties, variables and local vars: show the type + if (symbolKind === ts.ScriptElementKind.memberVariableElement || + symbolKind === ts.ScriptElementKind.memberGetAccessorElement || + symbolKind === ts.ScriptElementKind.memberSetAccessorElement || + symbolKind === ts.ScriptElementKind.jsxAttribute || + symbolFlags & ts.SymbolFlags.Variable || + symbolKind === ts.ScriptElementKind.localVariableElement || + isThisExpression) { + displayParts.push(ts.punctuationPart(ts.SyntaxKind.ColonToken)); + displayParts.push(ts.spacePart()); + // If the type is type parameter, format it specially + if (type.symbol && type.symbol.flags & ts.SymbolFlags.TypeParameter) { + const typeParameterParts = ts.mapToDisplayParts(writer => { + const param = typeChecker.typeParameterToDeclaration(type as ts.TypeParameter, enclosingDeclaration, symbolDisplayNodeBuilderFlags)!; + getPrinter().writeNode(ts.EmitHint.Unspecified, param, ts.getSourceFileOfNode(ts.getParseTreeNode(enclosingDeclaration)), writer); + }); + ts.addRange(displayParts, typeParameterParts); } else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); + ts.addRange(displayParts, ts.typeToDisplayParts(typeChecker, type, enclosingDeclaration)); } - - // For properties, variables and local vars: show the type - if (symbolKind === ts.ScriptElementKind.memberVariableElement || - symbolKind === ts.ScriptElementKind.memberGetAccessorElement || - symbolKind === ts.ScriptElementKind.memberSetAccessorElement || - symbolKind === ts.ScriptElementKind.jsxAttribute || - symbolFlags & ts.SymbolFlags.Variable || - symbolKind === ts.ScriptElementKind.localVariableElement || - isThisExpression) { - displayParts.push(ts.punctuationPart(ts.SyntaxKind.ColonToken)); + if ((symbol as ts.TransientSymbol).target && ((symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration) { + const labelDecl = ((symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration!; + ts.Debug.assertNode(labelDecl.name, ts.isIdentifier); displayParts.push(ts.spacePart()); - // If the type is type parameter, format it specially - if (type.symbol && type.symbol.flags & ts.SymbolFlags.TypeParameter) { - const typeParameterParts = ts.mapToDisplayParts(writer => { - const param = typeChecker.typeParameterToDeclaration(type as ts.TypeParameter, enclosingDeclaration, symbolDisplayNodeBuilderFlags)!; - getPrinter().writeNode(ts.EmitHint.Unspecified, param, ts.getSourceFileOfNode(ts.getParseTreeNode(enclosingDeclaration)), writer); - }); - ts.addRange(displayParts, typeParameterParts); - } - else { - ts.addRange(displayParts, ts.typeToDisplayParts(typeChecker, type, enclosingDeclaration)); - } - if ((symbol as ts.TransientSymbol).target && ((symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration) { - const labelDecl = ((symbol as ts.TransientSymbol).target as ts.TransientSymbol).tupleLabelDeclaration!; - ts.Debug.assertNode(labelDecl.name, ts.isIdentifier); - displayParts.push(ts.spacePart()); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); - displayParts.push(ts.textPart(ts.idText(labelDecl.name))); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); - } + displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); + displayParts.push(ts.textPart(ts.idText(labelDecl.name))); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); } - else if (symbolFlags & ts.SymbolFlags.Function || - symbolFlags & ts.SymbolFlags.Method || - symbolFlags & ts.SymbolFlags.Constructor || - symbolFlags & ts.SymbolFlags.Signature || - symbolFlags & ts.SymbolFlags.Accessor || - symbolKind === ts.ScriptElementKind.memberFunctionElement) { - const allSignatures = type.getNonNullableType().getCallSignatures(); - if (allSignatures.length) { - addSignatureDisplayParts(allSignatures[0], allSignatures); - hasMultipleSignatures = allSignatures.length > 1; - } + } + else if (symbolFlags & ts.SymbolFlags.Function || + symbolFlags & ts.SymbolFlags.Method || + symbolFlags & ts.SymbolFlags.Constructor || + symbolFlags & ts.SymbolFlags.Signature || + symbolFlags & ts.SymbolFlags.Accessor || + symbolKind === ts.ScriptElementKind.memberFunctionElement) { + const allSignatures = type.getNonNullableType().getCallSignatures(); + if (allSignatures.length) { + addSignatureDisplayParts(allSignatures[0], allSignatures); + hasMultipleSignatures = allSignatures.length > 1; } } } - else { - symbolKind = getSymbolKind(typeChecker, symbol, location); - } } - - if (documentation.length === 0 && !hasMultipleSignatures) { - documentation = symbol.getContextualDocumentationComment(enclosingDeclaration, typeChecker); + else { + symbolKind = getSymbolKind(typeChecker, symbol, location); } + } - if (documentation.length === 0 && symbolFlags & ts.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 && symbol.declarations && ts.forEach(symbol.parent.declarations, declaration => declaration.kind === ts.SyntaxKind.SourceFile)) { - for (const declaration of symbol.declarations) { - if (!declaration.parent || declaration.parent.kind !== ts.SyntaxKind.BinaryExpression) { - continue; - } + if (documentation.length === 0 && !hasMultipleSignatures) { + documentation = symbol.getContextualDocumentationComment(enclosingDeclaration, typeChecker); + } - const rhsSymbol = typeChecker.getSymbolAtLocation((declaration.parent as ts.BinaryExpression).right); - if (!rhsSymbol) { - continue; - } + if (documentation.length === 0 && symbolFlags & ts.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 && symbol.declarations && ts.forEach(symbol.parent.declarations, declaration => declaration.kind === ts.SyntaxKind.SourceFile)) { + for (const declaration of symbol.declarations) { + if (!declaration.parent || declaration.parent.kind !== ts.SyntaxKind.BinaryExpression) { + continue; + } - documentation = rhsSymbol.getDocumentationComment(typeChecker); - tags = rhsSymbol.getJsDocTags(typeChecker); - if (documentation.length > 0) { - break; - } + const rhsSymbol = typeChecker.getSymbolAtLocation((declaration.parent as ts.BinaryExpression).right); + if (!rhsSymbol) { + continue; } - } - } - if (documentation.length === 0 && ts.isIdentifier(location) && symbol.valueDeclaration && ts.isBindingElement(symbol.valueDeclaration)) { - const declaration = symbol.valueDeclaration; - const parent = declaration.parent; - if (ts.isIdentifier(declaration.name) && ts.isObjectBindingPattern(parent)) { - const name = ts.getTextOfIdentifierOrLiteral(declaration.name); - const objectType = typeChecker.getTypeAtLocation(parent); - documentation = ts.firstDefined(objectType.isUnion() ? objectType.types : [objectType], t => { - const prop = t.getProperty(name); - return prop ? prop.getDocumentationComment(typeChecker) : undefined; - }) || ts.emptyArray; + documentation = rhsSymbol.getDocumentationComment(typeChecker); + tags = rhsSymbol.getJsDocTags(typeChecker); + if (documentation.length > 0) { + break; + } } } + } - if (tags.length === 0 && !hasMultipleSignatures) { - tags = symbol.getContextualJsDocTags(enclosingDeclaration, typeChecker); + if (documentation.length === 0 && ts.isIdentifier(location) && symbol.valueDeclaration && ts.isBindingElement(symbol.valueDeclaration)) { + const declaration = symbol.valueDeclaration; + const parent = declaration.parent; + if (ts.isIdentifier(declaration.name) && ts.isObjectBindingPattern(parent)) { + const name = ts.getTextOfIdentifierOrLiteral(declaration.name); + const objectType = typeChecker.getTypeAtLocation(parent); + documentation = ts.firstDefined(objectType.isUnion() ? objectType.types : [objectType], t => { + const prop = t.getProperty(name); + return prop ? prop.getDocumentationComment(typeChecker) : undefined; + }) || ts.emptyArray; } + } - if (documentation.length === 0 && documentationFromAlias) { - documentation = documentationFromAlias; - } + if (tags.length === 0 && !hasMultipleSignatures) { + tags = symbol.getContextualJsDocTags(enclosingDeclaration, typeChecker); + } - if (tags.length === 0 && tagsFromAlias) { - tags = tagsFromAlias; - } + if (documentation.length === 0 && documentationFromAlias) { + documentation = documentationFromAlias; + } - return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags }; + if (tags.length === 0 && tagsFromAlias) { + tags = tagsFromAlias; + } - function getPrinter() { - if (!printer) { - printer = ts.createPrinter({ removeComments: true }); - } - return printer; - } + return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags }; - function prefixNextMeaning() { - if (displayParts.length) { - displayParts.push(ts.lineBreakPart()); - } - addAliasPrefixIfNecessary(); + function getPrinter() { + if (!printer) { + printer = ts.createPrinter({ removeComments: true }); } + return printer; + } - function addAliasPrefixIfNecessary() { - if (alias) { - pushSymbolKind(ts.ScriptElementKind.alias); - displayParts.push(ts.spacePart()); - } + function prefixNextMeaning() { + if (displayParts.length) { + displayParts.push(ts.lineBreakPart()); } + addAliasPrefixIfNecessary(); + } - function addInPrefix() { - displayParts.push(ts.spacePart()); - displayParts.push(ts.keywordPart(ts.SyntaxKind.InKeyword)); + function addAliasPrefixIfNecessary() { + if (alias) { + pushSymbolKind(ts.ScriptElementKind.alias); displayParts.push(ts.spacePart()); } + } - function addFullSymbolName(symbolToDisplay: ts.Symbol, enclosingDeclaration?: ts.Node) { - if (alias && symbolToDisplay === symbol) { - symbolToDisplay = alias; - } - const fullSymbolDisplayParts = ts.symbolToDisplayParts(typeChecker, symbolToDisplay, enclosingDeclaration || sourceFile, /*meaning*/ undefined, ts.SymbolFormatFlags.WriteTypeParametersOrArguments | ts.SymbolFormatFlags.UseOnlyExternalAliasing | ts.SymbolFormatFlags.AllowAnyNodeKind); - ts.addRange(displayParts, fullSymbolDisplayParts); - if (symbol.flags & ts.SymbolFlags.Optional) { - displayParts.push(ts.punctuationPart(ts.SyntaxKind.QuestionToken)); - } - } + function addInPrefix() { + displayParts.push(ts.spacePart()); + displayParts.push(ts.keywordPart(ts.SyntaxKind.InKeyword)); + displayParts.push(ts.spacePart()); + } - function addPrefixForAnyFunctionOrVar(symbol: ts.Symbol, symbolKind: string) { - prefixNextMeaning(); - if (symbolKind) { - pushSymbolKind(symbolKind); - if (symbol && !ts.some(symbol.declarations, d => ts.isArrowFunction(d) || (ts.isFunctionExpression(d) || ts.isClassExpression(d)) && !d.name)) { - displayParts.push(ts.spacePart()); - addFullSymbolName(symbol); - } - } + function addFullSymbolName(symbolToDisplay: ts.Symbol, enclosingDeclaration?: ts.Node) { + if (alias && symbolToDisplay === symbol) { + symbolToDisplay = alias; } + const fullSymbolDisplayParts = ts.symbolToDisplayParts(typeChecker, symbolToDisplay, enclosingDeclaration || sourceFile, /*meaning*/ undefined, ts.SymbolFormatFlags.WriteTypeParametersOrArguments | ts.SymbolFormatFlags.UseOnlyExternalAliasing | ts.SymbolFormatFlags.AllowAnyNodeKind); + ts.addRange(displayParts, fullSymbolDisplayParts); + if (symbol.flags & ts.SymbolFlags.Optional) { + displayParts.push(ts.punctuationPart(ts.SyntaxKind.QuestionToken)); + } + } - function pushSymbolKind(symbolKind: string) { - switch (symbolKind) { - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.functionElement: - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.constElement: - case ts.ScriptElementKind.constructorImplementationElement: - displayParts.push(ts.textOrKeywordPart(symbolKind)); - return; - default: - displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); - displayParts.push(ts.textOrKeywordPart(symbolKind)); - displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); - return; + function addPrefixForAnyFunctionOrVar(symbol: ts.Symbol, symbolKind: string) { + prefixNextMeaning(); + if (symbolKind) { + pushSymbolKind(symbolKind); + if (symbol && !ts.some(symbol.declarations, d => ts.isArrowFunction(d) || (ts.isFunctionExpression(d) || ts.isClassExpression(d)) && !d.name)) { + displayParts.push(ts.spacePart()); + addFullSymbolName(symbol); } } + } - function addSignatureDisplayParts(signature: ts.Signature, allSignatures: readonly ts.Signature[], flags = ts.TypeFormatFlags.None) { - ts.addRange(displayParts, ts.signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)); - if (allSignatures.length > 1) { - displayParts.push(ts.spacePart()); + function pushSymbolKind(symbolKind: string) { + switch (symbolKind) { + case ts.ScriptElementKind.variableElement: + case ts.ScriptElementKind.functionElement: + case ts.ScriptElementKind.letElement: + case ts.ScriptElementKind.constElement: + case ts.ScriptElementKind.constructorImplementationElement: + displayParts.push(ts.textOrKeywordPart(symbolKind)); + return; + default: displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); - displayParts.push(ts.operatorPart(ts.SyntaxKind.PlusToken)); - displayParts.push(ts.displayPart((allSignatures.length - 1).toString(), ts.SymbolDisplayPartKind.numericLiteral)); - displayParts.push(ts.spacePart()); - displayParts.push(ts.textPart(allSignatures.length === 2 ? "overload" : "overloads")); + displayParts.push(ts.textOrKeywordPart(symbolKind)); displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); - } - documentation = signature.getDocumentationComment(typeChecker); - tags = signature.getJsDocTags(); + return; + } + } - if (allSignatures.length > 1 && documentation.length === 0 && tags.length === 0) { - documentation = allSignatures[0].getDocumentationComment(typeChecker); - tags = allSignatures[0].getJsDocTags(); - } + function addSignatureDisplayParts(signature: ts.Signature, allSignatures: readonly ts.Signature[], flags = ts.TypeFormatFlags.None) { + ts.addRange(displayParts, ts.signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)); + if (allSignatures.length > 1) { + displayParts.push(ts.spacePart()); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.OpenParenToken)); + displayParts.push(ts.operatorPart(ts.SyntaxKind.PlusToken)); + displayParts.push(ts.displayPart((allSignatures.length - 1).toString(), ts.SymbolDisplayPartKind.numericLiteral)); + displayParts.push(ts.spacePart()); + displayParts.push(ts.textPart(allSignatures.length === 2 ? "overload" : "overloads")); + displayParts.push(ts.punctuationPart(ts.SyntaxKind.CloseParenToken)); } + documentation = signature.getDocumentationComment(typeChecker); + tags = signature.getJsDocTags(); - function writeTypeParametersOfSymbol(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined) { - const typeParameterParts = ts.mapToDisplayParts(writer => { - const params = typeChecker.symbolToTypeParameterDeclarations(symbol, enclosingDeclaration, symbolDisplayNodeBuilderFlags); - getPrinter().writeList(ts.ListFormat.TypeParameters, params, ts.getSourceFileOfNode(ts.getParseTreeNode(enclosingDeclaration)), writer); - }); - ts.addRange(displayParts, typeParameterParts); + if (allSignatures.length > 1 && documentation.length === 0 && tags.length === 0) { + documentation = allSignatures[0].getDocumentationComment(typeChecker); + tags = allSignatures[0].getJsDocTags(); } } - function isLocalVariableOrFunction(symbol: ts.Symbol) { - if (symbol.parent) { - return false; // This is exported symbol + function writeTypeParametersOfSymbol(symbol: ts.Symbol, enclosingDeclaration: ts.Node | undefined) { + const typeParameterParts = ts.mapToDisplayParts(writer => { + const params = typeChecker.symbolToTypeParameterDeclarations(symbol, enclosingDeclaration, symbolDisplayNodeBuilderFlags); + getPrinter().writeList(ts.ListFormat.TypeParameters, params, ts.getSourceFileOfNode(ts.getParseTreeNode(enclosingDeclaration)), writer); + }); + ts.addRange(displayParts, typeParameterParts); + } +} + +function isLocalVariableOrFunction(symbol: ts.Symbol) { + if (symbol.parent) { + return false; // This is exported symbol + } + + return ts.forEach(symbol.declarations, declaration => { + // Function expressions are local + if (declaration.kind === ts.SyntaxKind.FunctionExpression) { + return true; } - return ts.forEach(symbol.declarations, declaration => { - // Function expressions are local - if (declaration.kind === ts.SyntaxKind.FunctionExpression) { - return true; - } + if (declaration.kind !== ts.SyntaxKind.VariableDeclaration && declaration.kind !== ts.SyntaxKind.FunctionDeclaration) { + return false; + } - if (declaration.kind !== ts.SyntaxKind.VariableDeclaration && declaration.kind !== ts.SyntaxKind.FunctionDeclaration) { + // If the parent is not sourceFile or module block it is local variable + for (let parent = declaration.parent; !ts.isFunctionBlock(parent); parent = parent.parent) { + // Reached source file or module block + if (parent.kind === ts.SyntaxKind.SourceFile || parent.kind === ts.SyntaxKind.ModuleBlock) { return false; } + } - // If the parent is not sourceFile or module block it is local variable - for (let parent = declaration.parent; !ts.isFunctionBlock(parent); parent = parent.parent) { - // Reached source file or module block - if (parent.kind === ts.SyntaxKind.SourceFile || parent.kind === ts.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 d3e54cd62f134..64facd14772dc 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1,1608 +1,1608 @@ /* @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. + */ +function getPos(n: ts.TextRange): number { + const result = (n as any).__pos; + ts.Debug.assert(typeof result === "number"); + return result; +} + +function setPos(n: ts.TextRange, pos: number): void { + ts.Debug.assert(typeof pos === "number"); + (n as any).__pos = pos; +} + +function getEnd(n: ts.TextRange): number { + const result = (n as any).__end; + ts.Debug.assert(typeof result === "number"); + return result; +} + +function setEnd(n: ts.TextRange, end: number): void { + ts.Debug.assert(typeof end === "number"); + (n as any).__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, + /** + * Include attached JSDoc comments + */ + JSDoc, + /** + * Only delete trivia on the same line as getStart(). + * Used to avoid deleting leading comments + */ + StartLine +} + +export enum TrailingTriviaOption { + /** Exclude all trailing trivia (use getEnd()) */ + Exclude, + /** Doesn't include whitespace, but does strip comments */ + ExcludeWhitespace, + /** Include trailing trivia */ + Include +} + +function skipWhitespacesAndLineBreaks(text: string, start: number) { + return ts.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 (ts.isWhiteSpaceSingleLine(ch)) { + i++; + continue; + } + return ch === ts.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`. + */ +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; /** - * 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. + * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind */ - function getPos(n: ts.TextRange): number { - const result = (n as any).__pos; - ts.Debug.assert(typeof result === "number"); - return result; + delta?: number; +} + +export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions { + readonly joiner?: string; +} + +enum ChangeKind { + Remove, + ReplaceWithSingleNode, + ReplaceWithMultipleNodes, + Text +} + +type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText; + +interface BaseChange { + readonly sourceFile: ts.SourceFile; + readonly range: ts.TextRange; +} +export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions { +} + +interface ReplaceWithSingleNode extends BaseChange { + readonly kind: ChangeKind.ReplaceWithSingleNode; + readonly node: ts.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 ts.Node[]; + readonly options?: ReplaceWithMultipleNodesOptions; +} + +interface ChangeText extends BaseChange { + readonly kind: ChangeKind.Text; + readonly text: string; +} + +function getAdjustedRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, options: ConfigurableStartEnd): ts.TextRange { + return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; +} + +function getAdjustedStartPosition(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableStartEnd, hasTrailingComment = false) { + const { leadingTriviaOption } = options; + if (leadingTriviaOption === LeadingTriviaOption.Exclude) { + return node.getStart(sourceFile); + } + if (leadingTriviaOption === LeadingTriviaOption.StartLine) { + const startPos = node.getStart(sourceFile); + const pos = ts.getLineStartPositionForPosition(startPos, sourceFile); + return ts.rangeContainsPosition(node, pos) ? pos : startPos; + } + if (leadingTriviaOption === LeadingTriviaOption.JSDoc) { + const JSDocComments = ts.getJSDocCommentRanges(node, sourceFile.text); + if (JSDocComments?.length) { + return ts.getLineStartPositionForPosition(JSDocComments[0].pos, sourceFile); + } + } + const fullStart = node.getFullStart(); + const start = node.getStart(sourceFile); + if (fullStart === start) { + return start; + } + const fullStartLine = ts.getLineStartPositionForPosition(fullStart, sourceFile); + const startLine = ts.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; + } + + // if node has a trailing comments, use comment end position as the text has already been included. + if (hasTrailingComment) { + // Check first for leading comments as if the node is the first import, we want to exclude the trivia; + // otherwise we get the trailing comments. + const comment = ts.getLeadingCommentRanges(sourceFile.text, fullStart)?.[0] || ts.getTrailingCommentRanges(sourceFile.text, fullStart)?.[0]; + if (comment) { + return ts.skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true); + } + } + + // 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 = ts.getStartPositionOfLine(ts.getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile); + // skip whitespaces/newlines + adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition); + return ts.getStartPositionOfLine(ts.getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile); +} + +/** Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; */ +function getEndPositionOfMultilineTrailingComment(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableEnd): number | undefined { + const { end } = node; + const { trailingTriviaOption } = options; + if (trailingTriviaOption === TrailingTriviaOption.Include) { + // If the trailing comment is a multiline comment that extends to the next lines, + // return the end of the comment and track it for the next nodes to adjust. + const comments = ts.getTrailingCommentRanges(sourceFile.text, end); + if (comments) { + const nodeEndLine = ts.getLineOfLocalPosition(sourceFile, node.end); + for (const comment of comments) { + // Single line can break the loop as trivia will only be this line. + // Comments on subsequest lines are also ignored. + if (comment.kind === ts.SyntaxKind.SingleLineCommentTrivia || ts.getLineOfLocalPosition(sourceFile, comment.pos) > nodeEndLine) { + break; + } + + // Get the end line of the comment and compare against the end line of the node. + // If the comment end line position and the multiline comment extends to multiple lines, + // then is safe to return the end position. + const commentEndLine = ts.getLineOfLocalPosition(sourceFile, comment.end); + if (commentEndLine > nodeEndLine) { + return ts.skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true); + } + } + } } - function setPos(n: ts.TextRange, pos: number): void { - ts.Debug.assert(typeof pos === "number"); - (n as any).__pos = pos; + return undefined; +} + +function getAdjustedEndPosition(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableEnd): number { + const { end } = node; + const { trailingTriviaOption } = options; + if (trailingTriviaOption === TrailingTriviaOption.Exclude) { + return end; + } + if (trailingTriviaOption === TrailingTriviaOption.ExcludeWhitespace) { + const comments = ts.concatenate(ts.getTrailingCommentRanges(sourceFile.text, end), ts.getLeadingCommentRanges(sourceFile.text, end)); + const realEnd = comments?.[comments.length - 1]?.end; + if (realEnd) { + return realEnd; + } + return end; } - function getEnd(n: ts.TextRange): number { - const result = (n as any).__end; - ts.Debug.assert(typeof result === "number"); - return result; + const multilineEndPosition = getEndPositionOfMultilineTrailingComment(sourceFile, node, options); + if (multilineEndPosition) { + return multilineEndPosition; } - function setEnd(n: ts.TextRange, end: number): void { - ts.Debug.assert(typeof end === "number"); - (n as any).__end = end; + const newEnd = ts.skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true); + return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || ts.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 + */ +function isSeparator(node: ts.Node, candidate: ts.Node | undefined): candidate is ts.Token { + return !!candidate && !!node.parent && (candidate.kind === ts.SyntaxKind.CommaToken || (candidate.kind === ts.SyntaxKind.SemicolonToken && node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression)); +} + +export interface TextChangesContext { + host: ts.LanguageServiceHost; + formatContext: ts.formatting.FormatContext; + preferences: ts.UserPreferences; +} +export type TypeAnnotatable = ts.SignatureDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature; +export type ThisTypeAnnotatable = ts.FunctionDeclaration | ts.FunctionExpression; +export function isThisTypeAnnotatable(containingFunction: ts.SignatureDeclaration): containingFunction is ThisTypeAnnotatable { + return ts.isFunctionExpression(containingFunction) || ts.isFunctionDeclaration(containingFunction); +} + +export class ChangeTracker { + private readonly changes: Change[] = []; + private readonly newFiles: { + readonly oldFile: ts.SourceFile | undefined; + readonly fileName: string; + readonly statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[]; + }[] = []; + private readonly classesWithNodesInsertedAtStart = new ts.Map(); // Set implemented as Map + private readonly deletedNodes: { + readonly sourceFile: ts.SourceFile; + readonly node: ts.Node | ts.NodeArray; + }[] = []; + + public static fromContext(context: TextChangesContext): ChangeTracker { + return new ChangeTracker(ts.getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); } - export interface ConfigurableStart { - leadingTriviaOption?: LeadingTriviaOption; + public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): ts.FileTextChanges[] { + const tracker = ChangeTracker.fromContext(context); + cb(tracker); + return tracker.getChanges(); } - export interface ConfigurableEnd { - trailingTriviaOption?: TrailingTriviaOption; + + /** Public for tests only. Other callers should use `ChangeTracker.with`. */ + constructor(private readonly newLineCharacter: string, private readonly formatContext: ts.formatting.FormatContext) { } + public pushRaw(sourceFile: ts.SourceFile, change: ts.FileTextChanges) { + ts.Debug.assertEqual(sourceFile.fileName, change.fileName); + for (const c of change.textChanges) { + this.changes.push({ + kind: ChangeKind.Text, + sourceFile, + text: c.newText, + range: ts.createTextRangeFromSpan(c.span), + }); + } } - 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, - /** - * Include attached JSDoc comments - */ - JSDoc, - /** - * Only delete trivia on the same line as getStart(). - * Used to avoid deleting leading comments - */ - StartLine + public deleteRange(sourceFile: ts.SourceFile, range: ts.TextRange): void { + this.changes.push({ kind: ChangeKind.Remove, sourceFile, range }); } - export enum TrailingTriviaOption { - /** Exclude all trailing trivia (use getEnd()) */ - Exclude, - /** Doesn't include whitespace, but does strip comments */ - ExcludeWhitespace, - /** Include trailing trivia */ - Include + delete(sourceFile: ts.SourceFile, node: ts.Node | ts.NodeArray): void { + this.deletedNodes.push({ sourceFile, node }); } - function skipWhitespacesAndLineBreaks(text: string, start: number) { - return ts.skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); + /** Stop! Consider using `delete` instead, which has logic for deleting nodes from delimited lists. */ + public deleteNode(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { + this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options)); } - function hasCommentsBeforeLineBreak(text: string, start: number) { - let i = start; - while (i < text.length) { - const ch = text.charCodeAt(i); - if (ts.isWhiteSpaceSingleLine(ch)) { - i++; - continue; - } - return ch === ts.CharacterCodes.slash; + public deleteNodes(sourceFile: ts.SourceFile, nodes: readonly ts.Node[], options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }, hasTrailingComment: boolean): void { + // When deleting multiple nodes we need to track if the end position is including multiline trailing comments. + for (const node of nodes) { + const pos = getAdjustedStartPosition(sourceFile, node, options, hasTrailingComment); + const end = getAdjustedEndPosition(sourceFile, node, options); + + this.deleteRange(sourceFile, { pos, end }); + + hasTrailingComment = !!getEndPositionOfMultilineTrailingComment(sourceFile, node, options); } - 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`. - */ - export interface ConfigurableStartEnd extends ConfigurableStart, ConfigurableEnd { + public deleteModifier(sourceFile: ts.SourceFile, modifier: ts.Modifier): void { + this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: ts.skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) }); } - const useNonAdjustedPositions: ConfigurableStartEnd = { - leadingTriviaOption: LeadingTriviaOption.Exclude, - trailingTriviaOption: TrailingTriviaOption.Exclude, - }; + public deleteNodeRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.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 }); + } - 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; + public deleteNodeRangeExcludingEnd(sourceFile: ts.SourceFile, startNode: ts.Node, afterEndNode: ts.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 }); } - export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions { - readonly joiner?: string; + public replaceRange(sourceFile: ts.SourceFile, range: ts.TextRange, newNode: ts.Node, options: InsertNodeOptions = {}): void { + this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode }); } - enum ChangeKind { - Remove, - ReplaceWithSingleNode, - ReplaceWithMultipleNodes, - Text + public replaceNode(sourceFile: ts.SourceFile, oldNode: ts.Node, newNode: ts.Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options); } - type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText; + public replaceNodeRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, newNode: ts.Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options); + } - interface BaseChange { - readonly sourceFile: ts.SourceFile; - readonly range: ts.TextRange; + private replaceRangeWithNodes(sourceFile: ts.SourceFile, range: ts.TextRange, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void { + this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes }); } - export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions { + + public replaceNodeWithNodes(sourceFile: ts.SourceFile, oldNode: ts.Node, newNodes: readonly ts.Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options); } - interface ReplaceWithSingleNode extends BaseChange { - readonly kind: ChangeKind.ReplaceWithSingleNode; - readonly node: ts.Node; - readonly options?: InsertNodeOptions; + public replaceNodeWithText(sourceFile: ts.SourceFile, oldNode: ts.Node, text: string): void { + this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text); } - interface RemoveNode extends BaseChange { - readonly kind: ChangeKind.Remove; - readonly node?: never; - readonly options?: never; + public replaceNodeRangeWithNodes(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void { + this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options); } - interface ReplaceWithMultipleNodes extends BaseChange { - readonly kind: ChangeKind.ReplaceWithMultipleNodes; - readonly nodes: readonly ts.Node[]; - readonly options?: ReplaceWithMultipleNodesOptions; + public nodeHasTrailingComment(sourceFile: ts.SourceFile, oldNode: ts.Node, configurableEnd: ConfigurableEnd = useNonAdjustedPositions): boolean { + return !!getEndPositionOfMultilineTrailingComment(sourceFile, oldNode, configurableEnd); } - interface ChangeText extends BaseChange { - readonly kind: ChangeKind.Text; - readonly text: string; + private nextCommaToken(sourceFile: ts.SourceFile, node: ts.Node): ts.Node | undefined { + const next = ts.findNextToken(node, node.parent, sourceFile); + return next && next.kind === ts.SyntaxKind.CommaToken ? next : undefined; } - function getAdjustedRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, options: ConfigurableStartEnd): ts.TextRange { - return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; + public replacePropertyAssignment(sourceFile: ts.SourceFile, oldNode: ts.PropertyAssignment, newNode: ts.PropertyAssignment): void { + const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter); + this.replaceNode(sourceFile, oldNode, newNode, { suffix }); } - function getAdjustedStartPosition(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableStartEnd, hasTrailingComment = false) { - const { leadingTriviaOption } = options; - if (leadingTriviaOption === LeadingTriviaOption.Exclude) { - return node.getStart(sourceFile); - } - if (leadingTriviaOption === LeadingTriviaOption.StartLine) { - const startPos = node.getStart(sourceFile); - const pos = ts.getLineStartPositionForPosition(startPos, sourceFile); - return ts.rangeContainsPosition(node, pos) ? pos : startPos; - } - if (leadingTriviaOption === LeadingTriviaOption.JSDoc) { - const JSDocComments = ts.getJSDocCommentRanges(node, sourceFile.text); - if (JSDocComments?.length) { - return ts.getLineStartPositionForPosition(JSDocComments[0].pos, sourceFile); - } - } - const fullStart = node.getFullStart(); - const start = node.getStart(sourceFile); - if (fullStart === start) { - return start; - } - const fullStartLine = ts.getLineStartPositionForPosition(fullStart, sourceFile); - const startLine = ts.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; - } - - // if node has a trailing comments, use comment end position as the text has already been included. - if (hasTrailingComment) { - // Check first for leading comments as if the node is the first import, we want to exclude the trivia; - // otherwise we get the trailing comments. - const comment = ts.getLeadingCommentRanges(sourceFile.text, fullStart)?.[0] || ts.getTrailingCommentRanges(sourceFile.text, fullStart)?.[0]; - if (comment) { - return ts.skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true); - } - } + public insertNodeAt(sourceFile: ts.SourceFile, pos: number, newNode: ts.Node, options: InsertNodeOptions = {}): void { + this.replaceRange(sourceFile, ts.createRange(pos), newNode, options); + } - // 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 = ts.getStartPositionOfLine(ts.getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile); - // skip whitespaces/newlines - adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition); - return ts.getStartPositionOfLine(ts.getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile); - } - - /** Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; */ - function getEndPositionOfMultilineTrailingComment(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableEnd): number | undefined { - const { end } = node; - const { trailingTriviaOption } = options; - if (trailingTriviaOption === TrailingTriviaOption.Include) { - // If the trailing comment is a multiline comment that extends to the next lines, - // return the end of the comment and track it for the next nodes to adjust. - const comments = ts.getTrailingCommentRanges(sourceFile.text, end); - if (comments) { - const nodeEndLine = ts.getLineOfLocalPosition(sourceFile, node.end); - for (const comment of comments) { - // Single line can break the loop as trivia will only be this line. - // Comments on subsequest lines are also ignored. - if (comment.kind === ts.SyntaxKind.SingleLineCommentTrivia || ts.getLineOfLocalPosition(sourceFile, comment.pos) > nodeEndLine) { - break; - } - - // Get the end line of the comment and compare against the end line of the node. - // If the comment end line position and the multiline comment extends to multiple lines, - // then is safe to return the end position. - const commentEndLine = ts.getLineOfLocalPosition(sourceFile, comment.end); - if (commentEndLine > nodeEndLine) { - return ts.skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true); - } - } - } - } + private insertNodesAt(sourceFile: ts.SourceFile, pos: number, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions = {}): void { + this.replaceRangeWithNodes(sourceFile, ts.createRange(pos), newNodes, options); + } - return undefined; + public insertNodeAtTopOfFile(sourceFile: ts.SourceFile, newNode: ts.Statement, blankLineBetween: boolean): void { + this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween); + } + + public insertNodesAtTopOfFile(sourceFile: ts.SourceFile, newNodes: readonly ts.Statement[], blankLineBetween: boolean): void { + this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween); } - function getAdjustedEndPosition(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableEnd): number { - const { end } = node; - const { trailingTriviaOption } = options; - if (trailingTriviaOption === TrailingTriviaOption.Exclude) { - return end; + private insertAtTopOfFile(sourceFile: ts.SourceFile, insert: ts.Statement | readonly ts.Statement[], blankLineBetween: boolean): void { + const pos = getInsertionPositionAtSourceFileTop(sourceFile); + const options = { + prefix: pos === 0 ? undefined : this.newLineCharacter, + suffix: (ts.isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), + }; + if (ts.isArray(insert)) { + this.insertNodesAt(sourceFile, pos, insert, options); } - if (trailingTriviaOption === TrailingTriviaOption.ExcludeWhitespace) { - const comments = ts.concatenate(ts.getTrailingCommentRanges(sourceFile.text, end), ts.getLeadingCommentRanges(sourceFile.text, end)); - const realEnd = comments?.[comments.length - 1]?.end; - if (realEnd) { - return realEnd; - } - return end; + else { + this.insertNodeAt(sourceFile, pos, insert, options); } + } - const multilineEndPosition = getEndPositionOfMultilineTrailingComment(sourceFile, node, options); - if (multilineEndPosition) { - return multilineEndPosition; + public insertFirstParameter(sourceFile: ts.SourceFile, parameters: ts.NodeArray, newParam: ts.ParameterDeclaration): void { + const p0 = ts.firstOrUndefined(parameters); + if (p0) { + this.insertNodeBefore(sourceFile, p0, newParam); + } + else { + this.insertNodeAt(sourceFile, parameters.pos, newParam); } + } - const newEnd = ts.skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true); - return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || ts.isLineBreak(sourceFile.text.charCodeAt(newEnd - 1))) - ? newEnd - : end; + public insertNodeBefore(sourceFile: ts.SourceFile, before: ts.Node, newNode: ts.Node, blankLineBetween = false, options: ConfigurableStartEnd = {}): void { + this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, options), newNode, this.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween)); } - /** - * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element - */ - function isSeparator(node: ts.Node, candidate: ts.Node | undefined): candidate is ts.Token { - return !!candidate && !!node.parent && (candidate.kind === ts.SyntaxKind.CommaToken || (candidate.kind === ts.SyntaxKind.SemicolonToken && node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression)); - } - - export interface TextChangesContext { - host: ts.LanguageServiceHost; - formatContext: ts.formatting.FormatContext; - preferences: ts.UserPreferences; - } - export type TypeAnnotatable = ts.SignatureDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration | ts.PropertySignature; - export type ThisTypeAnnotatable = ts.FunctionDeclaration | ts.FunctionExpression; - export function isThisTypeAnnotatable(containingFunction: ts.SignatureDeclaration): containingFunction is ThisTypeAnnotatable { - return ts.isFunctionExpression(containingFunction) || ts.isFunctionDeclaration(containingFunction); - } - - export class ChangeTracker { - private readonly changes: Change[] = []; - private readonly newFiles: { - readonly oldFile: ts.SourceFile | undefined; - readonly fileName: string; - readonly statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[]; - }[] = []; - private readonly classesWithNodesInsertedAtStart = new ts.Map(); // Set implemented as Map - private readonly deletedNodes: { - readonly sourceFile: ts.SourceFile; - readonly node: ts.Node | ts.NodeArray; - }[] = []; - - public static fromContext(context: TextChangesContext): ChangeTracker { - return new ChangeTracker(ts.getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); - } - - public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): ts.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: ts.formatting.FormatContext) { } - public pushRaw(sourceFile: ts.SourceFile, change: ts.FileTextChanges) { - ts.Debug.assertEqual(sourceFile.fileName, change.fileName); - for (const c of change.textChanges) { - this.changes.push({ - kind: ChangeKind.Text, - sourceFile, - text: c.newText, - range: ts.createTextRangeFromSpan(c.span), + public insertModifierAt(sourceFile: ts.SourceFile, pos: number, modifier: ts.SyntaxKind, options: InsertNodeOptions = {}): void { + this.insertNodeAt(sourceFile, pos, ts.factory.createToken(modifier), options); + } + + public insertModifierBefore(sourceFile: ts.SourceFile, modifier: ts.SyntaxKind, before: ts.Node): void { + return this.insertModifierAt(sourceFile, before.getStart(sourceFile), modifier, { suffix: " " }); + } + + public insertCommentBeforeLine(sourceFile: ts.SourceFile, lineNumber: number, position: number, commentText: string): void { + const lineStartPosition = ts.getStartPositionOfLine(lineNumber, sourceFile); + const startPosition = ts.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 = ts.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: ts.SourceFile, node: ts.HasJSDoc, tag: ts.JSDoc): void { + const fnStart = node.getStart(sourceFile); + if (node.jsDoc) { + for (const jsdoc of node.jsDoc) { + this.deleteRange(sourceFile, { + pos: ts.getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile), + end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {}) }); } } + const startPosition = ts.getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1); + const indent = sourceFile.text.slice(startPosition, fnStart); + this.insertNodeAt(sourceFile, fnStart, tag, { suffix: this.newLineCharacter + indent }); + } - public deleteRange(sourceFile: ts.SourceFile, range: ts.TextRange): void { - this.changes.push({ kind: ChangeKind.Remove, sourceFile, range }); - } - - delete(sourceFile: ts.SourceFile, node: ts.Node | ts.NodeArray): void { - this.deletedNodes.push({ sourceFile, node }); - } + private createJSDocText(sourceFile: ts.SourceFile, node: ts.HasJSDoc) { + const comments = ts.flatMap(node.jsDoc, jsDoc => ts.isString(jsDoc.comment) ? ts.factory.createJSDocText(jsDoc.comment) : jsDoc.comment) as ts.JSDocComment[]; + const jsDoc = ts.singleOrUndefined(node.jsDoc); + return jsDoc && ts.positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && ts.length(comments) === 0 ? undefined : + ts.factory.createNodeArray(ts.intersperse(comments, ts.factory.createJSDocText("\n"))); + } + public replaceJSDocComment(sourceFile: ts.SourceFile, node: ts.HasJSDoc, tags: readonly ts.JSDocTag[]) { + this.insertJsdocCommentBefore(sourceFile, updateJSDocHost(node), ts.factory.createJSDocComment(this.createJSDocText(sourceFile, node), ts.factory.createNodeArray(tags))); + } + public addJSDocTags(sourceFile: ts.SourceFile, parent: ts.HasJSDoc, newTags: readonly ts.JSDocTag[]): void { + const oldTags = ts.flatMapToMutable(parent.jsDoc, j => j.tags); + const unmergedNewTags = newTags.filter(newTag => !oldTags.some((tag, i) => { + const merged = tryMergeJsdocTags(tag, newTag); + if (merged) + oldTags[i] = merged; + return !!merged; + })); + this.replaceJSDocComment(sourceFile, parent, [...oldTags, ...unmergedNewTags]); + } - /** Stop! Consider using `delete` instead, which has logic for deleting nodes from delimited lists. */ - public deleteNode(sourceFile: ts.SourceFile, node: ts.Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { - this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options)); - } + public filterJSDocTags(sourceFile: ts.SourceFile, parent: ts.HasJSDoc, predicate: (tag: ts.JSDocTag) => boolean): void { + this.replaceJSDocComment(sourceFile, parent, ts.filter(ts.flatMapToMutable(parent.jsDoc, j => j.tags), predicate)); + } - public deleteNodes(sourceFile: ts.SourceFile, nodes: readonly ts.Node[], options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }, hasTrailingComment: boolean): void { - // When deleting multiple nodes we need to track if the end position is including multiline trailing comments. - for (const node of nodes) { - const pos = getAdjustedStartPosition(sourceFile, node, options, hasTrailingComment); - const end = getAdjustedEndPosition(sourceFile, node, options); + public replaceRangeWithText(sourceFile: ts.SourceFile, range: ts.TextRange, text: string): void { + this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text }); + } - this.deleteRange(sourceFile, { pos, end }); + public insertText(sourceFile: ts.SourceFile, pos: number, text: string): void { + this.replaceRangeWithText(sourceFile, ts.createRange(pos), text); + } - hasTrailingComment = !!getEndPositionOfMultilineTrailingComment(sourceFile, node, options); + /** 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: ts.SourceFile, node: TypeAnnotatable, type: ts.TypeNode): boolean { + let endNode: ts.Node | undefined; + if (ts.isFunctionLike(node)) { + endNode = ts.findChildOfKind(node, ts.SyntaxKind.CloseParenToken, sourceFile); + if (!endNode) { + if (!ts.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 = ts.first(node.parameters); } } - - public deleteModifier(sourceFile: ts.SourceFile, modifier: ts.Modifier): void { - this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: ts.skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) }); + else { + endNode = (node.kind === ts.SyntaxKind.VariableDeclaration ? node.exclamationToken : node.questionToken) ?? node.name; } - public deleteNodeRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.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 }); - } + this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " }); + return true; + } - public deleteNodeRangeExcludingEnd(sourceFile: ts.SourceFile, startNode: ts.Node, afterEndNode: ts.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 tryInsertThisTypeAnnotation(sourceFile: ts.SourceFile, node: ThisTypeAnnotatable, type: ts.TypeNode): void { + const start = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1; + const suffix = node.parameters.length ? ", " : ""; - public replaceRange(sourceFile: ts.SourceFile, range: ts.TextRange, newNode: ts.Node, options: InsertNodeOptions = {}): void { - this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode }); - } + this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix }); + } - public replaceNode(sourceFile: ts.SourceFile, oldNode: ts.Node, newNode: ts.Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options); - } + public insertTypeParameters(sourceFile: ts.SourceFile, node: ts.SignatureDeclaration, typeParameters: readonly ts.TypeParameterDeclaration[]): void { + // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter + const start = (ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile) || ts.first(node.parameters)).getStart(sourceFile); + this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">", joiner: ", " }); + } - public replaceNodeRange(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, newNode: ts.Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options); + private getOptionsForInsertNodeBefore(before: ts.Node, inserted: ts.Node, blankLineBetween: boolean): InsertNodeOptions { + if (ts.isStatement(before) || ts.isClassElement(before)) { + return { suffix: blankLineBetween ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter }; } - - private replaceRangeWithNodes(sourceFile: ts.SourceFile, range: ts.TextRange, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void { - this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes }); + else if (ts.isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2; + return { suffix: ", " }; } - - public replaceNodeWithNodes(sourceFile: ts.SourceFile, oldNode: ts.Node, newNodes: readonly ts.Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options); + else if (ts.isParameter(before)) { + return ts.isParameter(inserted) ? { suffix: ", " } : {}; } - - public replaceNodeWithText(sourceFile: ts.SourceFile, oldNode: ts.Node, text: string): void { - this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text); + else if (ts.isStringLiteral(before) && ts.isImportDeclaration(before.parent) || ts.isNamedImports(before)) { + return { suffix: ", " }; } - - public replaceNodeRangeWithNodes(sourceFile: ts.SourceFile, startNode: ts.Node, endNode: ts.Node, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void { - this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options); + else if (ts.isImportSpecifier(before)) { + return { suffix: "," + (blankLineBetween ? this.newLineCharacter : " ") }; } + return ts.Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it + } - public nodeHasTrailingComment(sourceFile: ts.SourceFile, oldNode: ts.Node, configurableEnd: ConfigurableEnd = useNonAdjustedPositions): boolean { - return !!getEndPositionOfMultilineTrailingComment(sourceFile, oldNode, configurableEnd); + public insertNodeAtConstructorStart(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { + const firstStatement = ts.firstOrUndefined(ctr.body!.statements); + if (!firstStatement || !ctr.body!.multiLine) { + this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]); } - - private nextCommaToken(sourceFile: ts.SourceFile, node: ts.Node): ts.Node | undefined { - const next = ts.findNextToken(node, node.parent, sourceFile); - return next && next.kind === ts.SyntaxKind.CommaToken ? next : undefined; + else { + this.insertNodeBefore(sourceFile, firstStatement, newStatement); } + } - public replacePropertyAssignment(sourceFile: ts.SourceFile, oldNode: ts.PropertyAssignment, newNode: ts.PropertyAssignment): void { - const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter); - this.replaceNode(sourceFile, oldNode, newNode, { suffix }); + public insertNodeAtConstructorStartAfterSuperCall(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { + const superCallStatement = ts.find(ctr.body!.statements, stmt => ts.isExpressionStatement(stmt) && ts.isSuperCall(stmt.expression)); + if (!superCallStatement || !ctr.body!.multiLine) { + this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); } - - public insertNodeAt(sourceFile: ts.SourceFile, pos: number, newNode: ts.Node, options: InsertNodeOptions = {}): void { - this.replaceRange(sourceFile, ts.createRange(pos), newNode, options); + else { + this.insertNodeAfter(sourceFile, superCallStatement, newStatement); } + } - private insertNodesAt(sourceFile: ts.SourceFile, pos: number, newNodes: readonly ts.Node[], options: ReplaceWithMultipleNodesOptions = {}): void { - this.replaceRangeWithNodes(sourceFile, ts.createRange(pos), newNodes, options); + public insertNodeAtConstructorEnd(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { + const lastStatement = ts.lastOrUndefined(ctr.body!.statements); + if (!lastStatement || !ctr.body!.multiLine) { + this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); } - - public insertNodeAtTopOfFile(sourceFile: ts.SourceFile, newNode: ts.Statement, blankLineBetween: boolean): void { - this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween); + else { + this.insertNodeAfter(sourceFile, lastStatement, newStatement); } + } - public insertNodesAtTopOfFile(sourceFile: ts.SourceFile, newNodes: readonly ts.Statement[], blankLineBetween: boolean): void { - this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween); - } + private replaceConstructorBody(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, statements: readonly ts.Statement[]): void { + this.replaceNode(sourceFile, ctr.body!, ts.factory.createBlock(statements, /*multiLine*/ true)); + } - private insertAtTopOfFile(sourceFile: ts.SourceFile, insert: ts.Statement | readonly ts.Statement[], blankLineBetween: boolean): void { - const pos = getInsertionPositionAtSourceFileTop(sourceFile); - const options = { - prefix: pos === 0 ? undefined : this.newLineCharacter, - suffix: (ts.isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), - }; - if (ts.isArray(insert)) { - this.insertNodesAt(sourceFile, pos, insert, options); - } - else { - this.insertNodeAt(sourceFile, pos, insert, options); - } - } + public insertNodeAtEndOfScope(sourceFile: ts.SourceFile, scope: ts.Node, newNode: ts.Node): void { + const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {}); + this.insertNodeAt(sourceFile, pos, newNode, { + prefix: ts.isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter, + suffix: this.newLineCharacter + }); + } - public insertFirstParameter(sourceFile: ts.SourceFile, parameters: ts.NodeArray, newParam: ts.ParameterDeclaration): void { - const p0 = ts.firstOrUndefined(parameters); - if (p0) { - this.insertNodeBefore(sourceFile, p0, newParam); - } - else { - this.insertNodeAt(sourceFile, parameters.pos, newParam); - } - } + public insertMemberAtStart(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode, newElement: ts.ClassElement | ts.PropertySignature | ts.MethodSignature): void { + this.insertNodeAtStartWorker(sourceFile, node, newElement); + } - public insertNodeBefore(sourceFile: ts.SourceFile, before: ts.Node, newNode: ts.Node, blankLineBetween = false, options: ConfigurableStartEnd = {}): void { - this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, options), newNode, this.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween)); - } + public insertNodeAtObjectStart(sourceFile: ts.SourceFile, obj: ts.ObjectLiteralExpression, newElement: ts.ObjectLiteralElementLike): void { + this.insertNodeAtStartWorker(sourceFile, obj, newElement); + } - public insertModifierAt(sourceFile: ts.SourceFile, pos: number, modifier: ts.SyntaxKind, options: InsertNodeOptions = {}): void { - this.insertNodeAt(sourceFile, pos, ts.factory.createToken(modifier), options); - } + private insertNodeAtStartWorker(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode, newElement: ts.ClassElement | ts.ObjectLiteralElementLike | ts.PropertySignature | ts.MethodSignature): void { + const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node); + this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation)); + } - public insertModifierBefore(sourceFile: ts.SourceFile, modifier: ts.SyntaxKind, before: ts.Node): void { - return this.insertModifierAt(sourceFile, before.getStart(sourceFile), modifier, { suffix: " " }); - } + /** + * 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: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode) { + let indentation: number | undefined; + let lastRange: ts.TextRange = node; + for (const member of getMembersOrProperties(node)) { + if (ts.rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) { + // each indented member must be on a new line + return undefined; + } + const memberStart = member.getStart(sourceFile); + const memberIndentation = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(ts.getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options); + if (indentation === undefined) { + indentation = memberIndentation; + } + else if (memberIndentation !== indentation) { + // indentation of multiple members is not consistent + return undefined; + } + lastRange = member; + } + return indentation; + } - public insertCommentBeforeLine(sourceFile: ts.SourceFile, lineNumber: number, position: number, commentText: string): void { - const lineStartPosition = ts.getStartPositionOfLine(lineNumber, sourceFile); - const startPosition = ts.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 = ts.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); - } + private computeIndentationForNewMember(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode) { + const nodeStart = node.getStart(sourceFile); + return ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(ts.getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options) + + (this.formatContext.options.indentSize ?? 4); + } - public insertJsdocCommentBefore(sourceFile: ts.SourceFile, node: ts.HasJSDoc, tag: ts.JSDoc): void { - const fnStart = node.getStart(sourceFile); - if (node.jsDoc) { - for (const jsdoc of node.jsDoc) { - this.deleteRange(sourceFile, { - pos: ts.getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile), - end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {}) - }); - } - } - const startPosition = ts.getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1); - const indent = sourceFile.text.slice(startPosition, fnStart); - this.insertNodeAt(sourceFile, fnStart, tag, { suffix: this.newLineCharacter + indent }); - } + private getInsertNodeAtStartInsertOptions(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode, 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(node); + const isEmpty = members.length === 0; + const isFirstInsertion = ts.addToSeen(this.classesWithNodesInsertedAtStart, ts.getNodeId(node), { node, sourceFile }); + const insertTrailingComma = ts.isObjectLiteralExpression(node) && (!ts.isJsonSourceFile(sourceFile) || !isEmpty); + const insertLeadingComma = ts.isObjectLiteralExpression(node) && ts.isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; + return { + indentation, + prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter, + suffix: insertTrailingComma ? "," : "" + }; + } - private createJSDocText(sourceFile: ts.SourceFile, node: ts.HasJSDoc) { - const comments = ts.flatMap(node.jsDoc, jsDoc => ts.isString(jsDoc.comment) ? ts.factory.createJSDocText(jsDoc.comment) : jsDoc.comment) as ts.JSDocComment[]; - const jsDoc = ts.singleOrUndefined(node.jsDoc); - return jsDoc && ts.positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && ts.length(comments) === 0 ? undefined : - ts.factory.createNodeArray(ts.intersperse(comments, ts.factory.createJSDocText("\n"))); - } - public replaceJSDocComment(sourceFile: ts.SourceFile, node: ts.HasJSDoc, tags: readonly ts.JSDocTag[]) { - this.insertJsdocCommentBefore(sourceFile, updateJSDocHost(node), ts.factory.createJSDocComment(this.createJSDocText(sourceFile, node), ts.factory.createNodeArray(tags))); - } - public addJSDocTags(sourceFile: ts.SourceFile, parent: ts.HasJSDoc, newTags: readonly ts.JSDocTag[]): void { - const oldTags = ts.flatMapToMutable(parent.jsDoc, j => j.tags); - const unmergedNewTags = newTags.filter(newTag => !oldTags.some((tag, i) => { - const merged = tryMergeJsdocTags(tag, newTag); - if (merged) - oldTags[i] = merged; - return !!merged; - })); - this.replaceJSDocComment(sourceFile, parent, [...oldTags, ...unmergedNewTags]); - } + public insertNodeAfterComma(sourceFile: ts.SourceFile, after: ts.Node, newNode: ts.Node): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, this.nextCommaToken(sourceFile, after) || after, newNode); + this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); + } - public filterJSDocTags(sourceFile: ts.SourceFile, parent: ts.HasJSDoc, predicate: (tag: ts.JSDocTag) => boolean): void { - this.replaceJSDocComment(sourceFile, parent, ts.filter(ts.flatMapToMutable(parent.jsDoc, j => j.tags), predicate)); - } + public insertNodeAfter(sourceFile: ts.SourceFile, after: ts.Node, newNode: ts.Node): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode); + this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); + } - public replaceRangeWithText(sourceFile: ts.SourceFile, range: ts.TextRange, text: string): void { - this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text }); - } + public insertNodeAtEndOfList(sourceFile: ts.SourceFile, list: ts.NodeArray, newNode: ts.Node): void { + this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " }); + } - public insertText(sourceFile: ts.SourceFile, pos: number, text: string): void { - this.replaceRangeWithText(sourceFile, ts.createRange(pos), text); - } + public insertNodesAfter(sourceFile: ts.SourceFile, after: ts.Node, newNodes: readonly ts.Node[]): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, ts.first(newNodes)); + this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); + } - /** 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: ts.SourceFile, node: TypeAnnotatable, type: ts.TypeNode): boolean { - let endNode: ts.Node | undefined; - if (ts.isFunctionLike(node)) { - endNode = ts.findChildOfKind(node, ts.SyntaxKind.CloseParenToken, sourceFile); - if (!endNode) { - if (!ts.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 = ts.first(node.parameters); - } - } - else { - endNode = (node.kind === ts.SyntaxKind.VariableDeclaration ? node.exclamationToken : node.questionToken) ?? node.name; + private insertNodeAfterWorker(sourceFile: ts.SourceFile, after: ts.Node, newNode: ts.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) !== ts.CharacterCodes.semicolon) { + this.replaceRange(sourceFile, ts.createRange(after.end), ts.factory.createToken(ts.SyntaxKind.SemicolonToken)); } - - this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " }); - return true; } + const endPosition = getAdjustedEndPosition(sourceFile, after, {}); + return endPosition; + } - public tryInsertThisTypeAnnotation(sourceFile: ts.SourceFile, node: ThisTypeAnnotatable, type: ts.TypeNode): void { - const start = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1; - const suffix = node.parameters.length ? ", " : ""; + private getInsertNodeAfterOptions(sourceFile: ts.SourceFile, after: ts.Node): InsertNodeOptions { + const options = this.getInsertNodeAfterOptionsWorker(after); + return { + ...options, + prefix: after.end === sourceFile.end && ts.isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix, + }; + } - this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix }); - } + private getInsertNodeAfterOptionsWorker(node: ts.Node): InsertNodeOptions { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + return { prefix: this.newLineCharacter, suffix: this.newLineCharacter }; - public insertTypeParameters(sourceFile: ts.SourceFile, node: ts.SignatureDeclaration, typeParameters: readonly ts.TypeParameterDeclaration[]): void { - // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter - const start = (ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile) || ts.first(node.parameters)).getStart(sourceFile); - this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">", joiner: ", " }); - } + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.Identifier: + return { prefix: ", " }; - private getOptionsForInsertNodeBefore(before: ts.Node, inserted: ts.Node, blankLineBetween: boolean): InsertNodeOptions { - if (ts.isStatement(before) || ts.isClassElement(before)) { - return { suffix: blankLineBetween ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter }; - } - else if (ts.isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2; - return { suffix: ", " }; - } - else if (ts.isParameter(before)) { - return ts.isParameter(inserted) ? { suffix: ", " } : {}; - } - else if (ts.isStringLiteral(before) && ts.isImportDeclaration(before.parent) || ts.isNamedImports(before)) { - return { suffix: ", " }; - } - else if (ts.isImportSpecifier(before)) { - return { suffix: "," + (blankLineBetween ? this.newLineCharacter : " ") }; - } - return ts.Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it - } + case ts.SyntaxKind.PropertyAssignment: + return { suffix: "," + this.newLineCharacter }; - public insertNodeAtConstructorStart(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { - const firstStatement = ts.firstOrUndefined(ctr.body!.statements); - if (!firstStatement || !ctr.body!.multiLine) { - this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]); - } - else { - this.insertNodeBefore(sourceFile, firstStatement, newStatement); - } + case ts.SyntaxKind.ExportKeyword: + return { prefix: " " }; + + case ts.SyntaxKind.Parameter: + return {}; + + default: + ts.Debug.assert(ts.isStatement(node) || ts.isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it + return { suffix: this.newLineCharacter }; } + } - public insertNodeAtConstructorStartAfterSuperCall(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { - const superCallStatement = ts.find(ctr.body!.statements, stmt => ts.isExpressionStatement(stmt) && ts.isSuperCall(stmt.expression)); - if (!superCallStatement || !ctr.body!.multiLine) { - this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); + public insertName(sourceFile: ts.SourceFile, node: ts.FunctionExpression | ts.ClassExpression | ts.ArrowFunction, name: string): void { + ts.Debug.assert(!node.name); + if (node.kind === ts.SyntaxKind.ArrowFunction) { + const arrow = ts.findChildOfKind(node, ts.SyntaxKind.EqualsGreaterThanToken, sourceFile)!; + const lparen = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); + if (lparen) { + // `() => {}` --> `function f() {}` + this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [ts.factory.createToken(ts.SyntaxKind.FunctionKeyword), ts.factory.createIdentifier(name)], { joiner: " " }); + deleteNode(this, sourceFile, arrow); } else { - this.insertNodeAfter(sourceFile, superCallStatement, newStatement); + // `x => {}` -> `function f(x) {}` + this.insertText(sourceFile, ts.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, ts.factory.createToken(ts.SyntaxKind.CloseParenToken)); } - } - public insertNodeAtConstructorEnd(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, newStatement: ts.Statement): void { - const lastStatement = ts.lastOrUndefined(ctr.body!.statements); - if (!lastStatement || !ctr.body!.multiLine) { - this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); - } - else { - this.insertNodeAfter(sourceFile, lastStatement, newStatement); + if (node.body.kind !== ts.SyntaxKind.Block) { + // `() => 0` => `function f() { return 0; }` + this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [ts.factory.createToken(ts.SyntaxKind.OpenBraceToken), ts.factory.createToken(ts.SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " }); + this.insertNodesAt(sourceFile, node.body.end, [ts.factory.createToken(ts.SyntaxKind.SemicolonToken), ts.factory.createToken(ts.SyntaxKind.CloseBraceToken)], { joiner: " " }); } } - - private replaceConstructorBody(sourceFile: ts.SourceFile, ctr: ts.ConstructorDeclaration, statements: readonly ts.Statement[]): void { - this.replaceNode(sourceFile, ctr.body!, ts.factory.createBlock(statements, /*multiLine*/ true)); + else { + const pos = ts.findChildOfKind(node, node.kind === ts.SyntaxKind.FunctionExpression ? ts.SyntaxKind.FunctionKeyword : ts.SyntaxKind.ClassKeyword, sourceFile)!.end; + this.insertNodeAt(sourceFile, pos, ts.factory.createIdentifier(name), { prefix: " " }); } + } - public insertNodeAtEndOfScope(sourceFile: ts.SourceFile, scope: ts.Node, newNode: ts.Node): void { - const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {}); - this.insertNodeAt(sourceFile, pos, newNode, { - prefix: ts.isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter, - suffix: this.newLineCharacter - }); - } + public insertExportModifier(sourceFile: ts.SourceFile, node: ts.DeclarationStatement | ts.VariableStatement): void { + this.insertText(sourceFile, node.getStart(sourceFile), "export "); + } - public insertMemberAtStart(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode, newElement: ts.ClassElement | ts.PropertySignature | ts.MethodSignature): void { - this.insertNodeAtStartWorker(sourceFile, node, newElement); + public insertImportSpecifierAtIndex(sourceFile: ts.SourceFile, importSpecifier: ts.ImportSpecifier, namedImports: ts.NamedImports, index: number) { + const prevSpecifier = namedImports.elements[index - 1]; + if (prevSpecifier) { + this.insertNodeInListAfter(sourceFile, prevSpecifier, importSpecifier); } - - public insertNodeAtObjectStart(sourceFile: ts.SourceFile, obj: ts.ObjectLiteralExpression, newElement: ts.ObjectLiteralElementLike): void { - this.insertNodeAtStartWorker(sourceFile, obj, newElement); + else { + this.insertNodeBefore(sourceFile, namedImports.elements[0], importSpecifier, !ts.positionsAreOnSameLine(namedImports.elements[0].getStart(), namedImports.parent.parent.getStart(), sourceFile)); } + } - private insertNodeAtStartWorker(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode, newElement: ts.ClassElement | ts.ObjectLiteralElementLike | ts.PropertySignature | ts.MethodSignature): void { - const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node); - this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation)); + /** + * 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: ts.SourceFile, after: ts.Node, newNode: ts.Node, containingList = ts.formatting.SmartIndenter.getContainingList(after, sourceFile)): void { + if (!containingList) { + ts.Debug.fail("node is not a list element"); + 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: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode) { - let indentation: number | undefined; - let lastRange: ts.TextRange = node; - for (const member of getMembersOrProperties(node)) { - if (ts.rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) { - // each indented member must be on a new line - return undefined; - } - const memberStart = member.getStart(sourceFile); - const memberIndentation = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(ts.getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options); - if (indentation === undefined) { - indentation = memberIndentation; - } - else if (memberIndentation !== indentation) { - // indentation of multiple members is not consistent - return undefined; + const index = ts.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 = ts.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 next element start as start and end position in final change + // - build text of change by formatting the text of node + 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, + const nextNode = containingList[index + 1]; + const startPos = skipWhitespacesAndLineBreaks(sourceFile.text, nextNode.getFullStart()); + + // write separator and leading trivia of the next element as suffix + const suffix = `${ts.tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, startPos)}`; + this.insertNodesAt(sourceFile, startPos, [newNode], { suffix }); + } + } + else { + const afterStart = after.getStart(sourceFile); + const afterStartLinePosition = ts.getLineStartPositionForPosition(afterStart, sourceFile); + let separator: ts.SyntaxKind.CommaToken | ts.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 = ts.SyntaxKind.CommaToken; + } + else { + // element has more than one element, pick separator from the list + const tokenBeforeInsertPosition = ts.findPrecedingToken(after.pos, sourceFile); + separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : ts.SyntaxKind.CommaToken; + // determine if list is multiline by checking lines of after element and element that precedes it. + const afterMinusOneStartLinePosition = ts.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, ts.createRange(end), ts.factory.createToken(separator)); + // use the same indentation as 'after' item + const indentation = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options); + // insert element before the line break on the line that contains 'after' element + let insertPos = ts.skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false); + // find position before "\n" or "\r\n" + while (insertPos !== end && ts.isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) { + insertPos--; } - lastRange = member; + this.replaceRange(sourceFile, ts.createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter }); + } + else { + this.replaceRange(sourceFile, ts.createRange(end), newNode, { prefix: `${ts.tokenToString(separator)} ` }); } - return indentation; } + } - private computeIndentationForNewMember(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode) { - const nodeStart = node.getStart(sourceFile); - return ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(ts.getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options) - + (this.formatContext.options.indentSize ?? 4); - } + public parenthesizeExpression(sourceFile: ts.SourceFile, expression: ts.Expression) { + this.replaceRange(sourceFile, ts.rangeOfNode(expression), ts.factory.createParenthesizedExpression(expression)); + } - private getInsertNodeAtStartInsertOptions(sourceFile: ts.SourceFile, node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode, 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`. + 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 = ts.positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile); + if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) { + // For `class C { }` remove the whitespace inside the braces. + this.deleteRange(sourceFile, ts.createRange(openBraceEnd, closeBraceEnd - 1)); + } + if (isSingleLine) { + this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter); + } + } + }); + } - const members = getMembersOrProperties(node); - const isEmpty = members.length === 0; - const isFirstInsertion = ts.addToSeen(this.classesWithNodesInsertedAtStart, ts.getNodeId(node), { node, sourceFile }); - const insertTrailingComma = ts.isObjectLiteralExpression(node) && (!ts.isJsonSourceFile(sourceFile) || !isEmpty); - const insertLeadingComma = ts.isObjectLiteralExpression(node) && ts.isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; - return { - indentation, - prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter, - suffix: insertTrailingComma ? "," : "" - }; + private finishDeleteDeclarations(): void { + const deletedNodesInLists = new ts.Set(); // 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 && ts.rangeContainsRangeExclusive(d.node, node))) { + if (ts.isArray(node)) { + this.deleteRange(sourceFile, ts.rangeOfTypeParameters(sourceFile, node)); + } + else { + deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node); + } + } } - public insertNodeAfterComma(sourceFile: ts.SourceFile, after: ts.Node, newNode: ts.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: ts.SourceFile, after: ts.Node, newNode: ts.Node): void { - const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode); - this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); - } - - public insertNodeAtEndOfList(sourceFile: ts.SourceFile, list: ts.NodeArray, newNode: ts.Node): void { - this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " }); - } - - public insertNodesAfter(sourceFile: ts.SourceFile, after: ts.Node, newNodes: readonly ts.Node[]): void { - const endPosition = this.insertNodeAfterWorker(sourceFile, after, ts.first(newNodes)); - this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); - } - - private insertNodeAfterWorker(sourceFile: ts.SourceFile, after: ts.Node, newNode: ts.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) !== ts.CharacterCodes.semicolon) { - this.replaceRange(sourceFile, ts.createRange(after.end), ts.factory.createToken(ts.SyntaxKind.SemicolonToken)); - } + deletedNodesInLists.forEach(node => { + const sourceFile = node.getSourceFile(); + const list = ts.formatting.SmartIndenter.getContainingList(node, sourceFile)!; + if (node !== ts.last(list)) + return; + const lastNonDeletedIndex = ts.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 endPosition = getAdjustedEndPosition(sourceFile, after, {}); - return endPosition; - } + }); + } - private getInsertNodeAfterOptions(sourceFile: ts.SourceFile, after: ts.Node): InsertNodeOptions { - const options = this.getInsertNodeAfterOptionsWorker(after); - return { - ...options, - prefix: after.end === sourceFile.end && ts.isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix, - }; - } + /** + * 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): ts.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; + } - private getInsertNodeAfterOptionsWorker(node: ts.Node): InsertNodeOptions { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - return { prefix: this.newLineCharacter, suffix: this.newLineCharacter }; + public createNewFile(oldFile: ts.SourceFile | undefined, fileName: string, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[]): void { + this.newFiles.push({ oldFile, fileName, statements }); + } +} - case ts.SyntaxKind.VariableDeclaration: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.Identifier: - return { prefix: ", " }; +function updateJSDocHost(parent: ts.HasJSDoc): ts.HasJSDoc { + if (parent.kind !== ts.SyntaxKind.ArrowFunction) { + return parent; + } + const jsDocNode = parent.parent.kind === ts.SyntaxKind.PropertyDeclaration ? + parent.parent as ts.HasJSDoc : + parent.parent.parent as ts.HasJSDoc; + jsDocNode.jsDoc = parent.jsDoc; + jsDocNode.jsDocCache = parent.jsDocCache; + return jsDocNode; +} - case ts.SyntaxKind.PropertyAssignment: - return { suffix: "," + this.newLineCharacter }; +function tryMergeJsdocTags(oldTag: ts.JSDocTag, newTag: ts.JSDocTag): ts.JSDocTag | undefined { + if (oldTag.kind !== newTag.kind) { + return undefined; + } + switch (oldTag.kind) { + case ts.SyntaxKind.JSDocParameterTag: { + const oldParam = oldTag as ts.JSDocParameterTag; + const newParam = newTag as ts.JSDocParameterTag; + return ts.isIdentifier(oldParam.name) && ts.isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText + ? ts.factory.createJSDocParameterTag(/*tagName*/ undefined, newParam.name, /*isBracketed*/ false, newParam.typeExpression, newParam.isNameFirst, oldParam.comment) + : undefined; + } + case ts.SyntaxKind.JSDocReturnTag: + return ts.factory.createJSDocReturnTag(/*tagName*/ undefined, (newTag as ts.JSDocReturnTag).typeExpression, oldTag.comment); + case ts.SyntaxKind.JSDocTypeTag: + return ts.factory.createJSDocTypeTag(/*tagName*/ undefined, (newTag as ts.JSDocTypeTag).typeExpression, oldTag.comment); + } +} - case ts.SyntaxKind.ExportKeyword: - return { prefix: " " }; +// find first non-whitespace position in the leading trivia of the node +function startPositionToDeleteNodeInList(sourceFile: ts.SourceFile, node: ts.Node): number { + return ts.skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); +} +function getClassOrObjectBraceEnds(cls: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression, sourceFile: ts.SourceFile): [ + number | undefined, + number | undefined +] { + const open = ts.findChildOfKind(cls, ts.SyntaxKind.OpenBraceToken, sourceFile); + const close = ts.findChildOfKind(cls, ts.SyntaxKind.CloseBraceToken, sourceFile); + return [open?.end, close?.end]; +} +function getMembersOrProperties(node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode): ts.NodeArray { + return ts.isObjectLiteralExpression(node) ? node.properties : node.members; +} - case ts.SyntaxKind.Parameter: - return {}; +export type ValidateNonFormattedText = (node: ts.Node, text: string) => void; +export function getNewFileText(statements: readonly ts.Statement[], scriptKind: ts.ScriptKind, newLineCharacter: string, formatContext: ts.formatting.FormatContext): string { + return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext); +} - default: - ts.Debug.assert(ts.isStatement(node) || ts.isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it - return { suffix: this.newLineCharacter }; +namespace changesToText { + export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: ts.formatting.FormatContext, validate: ValidateNonFormattedText | undefined): ts.FileTextChanges[] { + return ts.mapDefined(ts.group(changes, c => c.sourceFile.path), 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 = ts.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++) { + ts.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)}`); } - } - public insertName(sourceFile: ts.SourceFile, node: ts.FunctionExpression | ts.ClassExpression | ts.ArrowFunction, name: string): void { - ts.Debug.assert(!node.name); - if (node.kind === ts.SyntaxKind.ArrowFunction) { - const arrow = ts.findChildOfKind(node, ts.SyntaxKind.EqualsGreaterThanToken, sourceFile)!; - const lparen = ts.findChildOfKind(node, ts.SyntaxKind.OpenParenToken, sourceFile); - if (lparen) { - // `() => {}` --> `function f() {}` - this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [ts.factory.createToken(ts.SyntaxKind.FunctionKeyword), ts.factory.createIdentifier(name)], { joiner: " " }); - deleteNode(this, sourceFile, arrow); - } - else { - // `x => {}` -> `function f(x) {}` - this.insertText(sourceFile, ts.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, ts.factory.createToken(ts.SyntaxKind.CloseParenToken)); - } + const textChanges = ts.mapDefined(normalized, c => { + const span = ts.createTextSpanFromRange(c.range); + const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate); - if (node.body.kind !== ts.SyntaxKind.Block) { - // `() => 0` => `function f() { return 0; }` - this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [ts.factory.createToken(ts.SyntaxKind.OpenBraceToken), ts.factory.createToken(ts.SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " }); - this.insertNodesAt(sourceFile, node.body.end, [ts.factory.createToken(ts.SyntaxKind.SemicolonToken), ts.factory.createToken(ts.SyntaxKind.CloseBraceToken)], { joiner: " " }); + // Filter out redundant changes. + if (span.length === newText.length && ts.stringContainsAt(sourceFile.text, newText, span.start)) { + return undefined; } - } - else { - const pos = ts.findChildOfKind(node, node.kind === ts.SyntaxKind.FunctionExpression ? ts.SyntaxKind.FunctionKeyword : ts.SyntaxKind.ClassKeyword, sourceFile)!.end; - this.insertNodeAt(sourceFile, pos, ts.factory.createIdentifier(name), { prefix: " " }); - } - } - - public insertExportModifier(sourceFile: ts.SourceFile, node: ts.DeclarationStatement | ts.VariableStatement): void { - this.insertText(sourceFile, node.getStart(sourceFile), "export "); - } - public insertImportSpecifierAtIndex(sourceFile: ts.SourceFile, importSpecifier: ts.ImportSpecifier, namedImports: ts.NamedImports, index: number) { - const prevSpecifier = namedImports.elements[index - 1]; - if (prevSpecifier) { - this.insertNodeInListAfter(sourceFile, prevSpecifier, importSpecifier); - } - else { - this.insertNodeBefore(sourceFile, namedImports.elements[0], importSpecifier, !ts.positionsAreOnSameLine(namedImports.elements[0].getStart(), namedImports.parent.parent.getStart(), sourceFile)); - } - } - - /** - * 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: ts.SourceFile, after: ts.Node, newNode: ts.Node, containingList = ts.formatting.SmartIndenter.getContainingList(after, sourceFile)): void { - if (!containingList) { - ts.Debug.fail("node is not a list element"); - return; - } - const index = ts.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 = ts.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 next element start as start and end position in final change - // - build text of change by formatting the text of node + 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, - const nextNode = containingList[index + 1]; - const startPos = skipWhitespacesAndLineBreaks(sourceFile.text, nextNode.getFullStart()); - - // write separator and leading trivia of the next element as suffix - const suffix = `${ts.tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, startPos)}`; - this.insertNodesAt(sourceFile, startPos, [newNode], { suffix }); - } - } - else { - const afterStart = after.getStart(sourceFile); - const afterStartLinePosition = ts.getLineStartPositionForPosition(afterStart, sourceFile); - let separator: ts.SyntaxKind.CommaToken | ts.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 = ts.SyntaxKind.CommaToken; - } - else { - // element has more than one element, pick separator from the list - const tokenBeforeInsertPosition = ts.findPrecedingToken(after.pos, sourceFile); - separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : ts.SyntaxKind.CommaToken; - // determine if list is multiline by checking lines of after element and element that precedes it. - const afterMinusOneStartLinePosition = ts.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, ts.createRange(end), ts.factory.createToken(separator)); - // use the same indentation as 'after' item - const indentation = ts.formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options); - // insert element before the line break on the line that contains 'after' element - let insertPos = ts.skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false); - // find position before "\n" or "\r\n" - while (insertPos !== end && ts.isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) { - insertPos--; - } - this.replaceRange(sourceFile, ts.createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter }); - } - else { - this.replaceRange(sourceFile, ts.createRange(end), newNode, { prefix: `${ts.tokenToString(separator)} ` }); - } - } - } + return ts.createTextChange(span, newText); + }); - public parenthesizeExpression(sourceFile: ts.SourceFile, expression: ts.Expression) { - this.replaceRange(sourceFile, ts.rangeOfNode(expression), ts.factory.createParenthesizedExpression(expression)); - } + return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined; + }); + } - 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 = ts.positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile); - if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) { - // For `class C { }` remove the whitespace inside the braces. - this.deleteRange(sourceFile, ts.createRange(openBraceEnd, closeBraceEnd - 1)); - } - if (isSingleLine) { - this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter); - } - } - }); - } + export function newFileChanges(oldFile: ts.SourceFile | undefined, fileName: string, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: ts.formatting.FormatContext): ts.FileTextChanges { + const text = newFileChangesWorker(oldFile, ts.getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); + return { fileName, textChanges: [ts.createTextChange(ts.createTextSpan(0, 0), text)], isNewFile: true }; + } - private finishDeleteDeclarations(): void { - const deletedNodesInLists = new ts.Set(); // 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 && ts.rangeContainsRangeExclusive(d.node, node))) { - if (ts.isArray(node)) { - this.deleteRange(sourceFile, ts.rangeOfTypeParameters(sourceFile, node)); - } - else { - deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node); - } - } - } + export function newFileChangesWorker(oldFile: ts.SourceFile | undefined, scriptKind: ts.ScriptKind, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: ts.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 => s === ts.SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); + const sourceFile = ts.createSourceFile("any file name", nonFormattedText, ts.ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); + const changes = ts.formatting.formatDocument(sourceFile, formatContext); + return applyChanges(nonFormattedText, changes) + newLineCharacter; + } - deletedNodesInLists.forEach(node => { - const sourceFile = node.getSourceFile(); - const list = ts.formatting.SmartIndenter.getContainingList(node, sourceFile)!; - if (node !== ts.last(list)) - return; - const lastNonDeletedIndex = ts.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]) }); - } - }); + function computeNewText(change: Change, sourceFile: ts.SourceFile, newLineCharacter: string, formatContext: ts.formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { + if (change.kind === ChangeKind.Remove) { + return ""; } - - /** - * 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): ts.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; + if (change.kind === ChangeKind.Text) { + return change.text; } - public createNewFile(oldFile: ts.SourceFile | undefined, fileName: string, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[]): void { - this.newFiles.push({ oldFile, fileName, statements }); - } + const { options = {}, range: { pos } } = change; + const format = (n: ts.Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate); + const text = change.kind === ChangeKind.ReplaceWithMultipleNodes + ? change.nodes.map(n => ts.removeSuffix(format(n), newLineCharacter)).join(change.options?.joiner || newLineCharacter) + : format(change.node); + // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line + const noIndent = (options.indentation !== undefined || ts.getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, ""); + return (options.prefix || "") + noIndent + + ((!options.suffix || ts.endsWith(noIndent, options.suffix)) + ? "" : options.suffix); } - function updateJSDocHost(parent: ts.HasJSDoc): ts.HasJSDoc { - if (parent.kind !== ts.SyntaxKind.ArrowFunction) { - return parent; + /** Note: this may mutate `nodeIn`. */ + function getFormattedTextOfNode(nodeIn: ts.Node, sourceFile: ts.SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: ts.formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { + const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter); + if (validate) + validate(node, text); + const formatOptions = ts.getFormatCodeSettingsForWriting(formatContext, sourceFile); + const initialIndentation = indentation !== undefined + ? indentation + : ts.formatting.SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || ts.getLineStartPositionForPosition(pos, sourceFile) === pos); + if (delta === undefined) { + delta = ts.formatting.SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0; } - const jsDocNode = parent.parent.kind === ts.SyntaxKind.PropertyDeclaration ? - parent.parent as ts.HasJSDoc : - parent.parent.parent as ts.HasJSDoc; - jsDocNode.jsDoc = parent.jsDoc; - jsDocNode.jsDocCache = parent.jsDocCache; - return jsDocNode; - } - function tryMergeJsdocTags(oldTag: ts.JSDocTag, newTag: ts.JSDocTag): ts.JSDocTag | undefined { - if (oldTag.kind !== newTag.kind) { - return undefined; - } - switch (oldTag.kind) { - case ts.SyntaxKind.JSDocParameterTag: { - const oldParam = oldTag as ts.JSDocParameterTag; - const newParam = newTag as ts.JSDocParameterTag; - return ts.isIdentifier(oldParam.name) && ts.isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText - ? ts.factory.createJSDocParameterTag(/*tagName*/ undefined, newParam.name, /*isBracketed*/ false, newParam.typeExpression, newParam.isNameFirst, oldParam.comment) - : undefined; + const file: ts.SourceFileLike = { + text, + getLineAndCharacterOfPosition(pos) { + return ts.getLineAndCharacterOfPosition(this, pos); } - case ts.SyntaxKind.JSDocReturnTag: - return ts.factory.createJSDocReturnTag(/*tagName*/ undefined, (newTag as ts.JSDocReturnTag).typeExpression, oldTag.comment); - case ts.SyntaxKind.JSDocTypeTag: - return ts.factory.createJSDocTypeTag(/*tagName*/ undefined, (newTag as ts.JSDocTypeTag).typeExpression, oldTag.comment); - } + }; + const changes = ts.formatting.formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions }); + return applyChanges(text, changes); } - // find first non-whitespace position in the leading trivia of the node - function startPositionToDeleteNodeInList(sourceFile: ts.SourceFile, node: ts.Node): number { - return ts.skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); - } - function getClassOrObjectBraceEnds(cls: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression, sourceFile: ts.SourceFile): [ - number | undefined, - number | undefined - ] { - const open = ts.findChildOfKind(cls, ts.SyntaxKind.OpenBraceToken, sourceFile); - const close = ts.findChildOfKind(cls, ts.SyntaxKind.CloseBraceToken, sourceFile); - return [open?.end, close?.end]; - } - function getMembersOrProperties(node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration | ts.ObjectLiteralExpression | ts.TypeLiteralNode): ts.NodeArray { - return ts.isObjectLiteralExpression(node) ? node.properties : node.members; + /** Note: output node may be mutated input node. */ + export function getNonformattedText(node: ts.Node, sourceFile: ts.SourceFile | undefined, newLineCharacter: string): { + text: string; + node: ts.Node; + } { + const writer = createWriter(newLineCharacter); + const newLine = ts.getNewLineKind(newLineCharacter); + ts.createPrinter({ + newLine, + neverAsciiEscape: true, + preserveSourceNewlines: true, + terminateUnterminatedLiterals: true + }, writer).writeNode(ts.EmitHint.Unspecified, node, sourceFile, writer); + return { text: writer.getText(), node: assignPositionsToNode(node) }; } +} - export type ValidateNonFormattedText = (node: ts.Node, text: string) => void; - export function getNewFileText(statements: readonly ts.Statement[], scriptKind: ts.ScriptKind, newLineCharacter: string, formatContext: ts.formatting.FormatContext): string { - return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext); +export function applyChanges(text: string, changes: readonly ts.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(ts.textSpanEnd(span))}`; } + return text; +} - namespace changesToText { - export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: ts.formatting.FormatContext, validate: ValidateNonFormattedText | undefined): ts.FileTextChanges[] { - return ts.mapDefined(ts.group(changes, c => c.sourceFile.path), 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 = ts.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++) { - ts.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)}`); - } +function isTrivia(s: string) { + return ts.skipTrivia(s, 0) === s.length; +} - const textChanges = ts.mapDefined(normalized, c => { - const span = ts.createTextSpanFromRange(c.range); - const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate); +// A transformation context that won't perform parenthesization, as some parenthesization rules +// are more aggressive than is strictly necessary. +const textChangesTransformationContext: ts.TransformationContext = { + ...ts.nullTransformationContext, + factory: ts.createNodeFactory(ts.nullTransformationContext.factory.flags | ts.NodeFactoryFlags.NoParenthesizerRules, ts.nullTransformationContext.factory.baseFactory), +}; + +export function assignPositionsToNode(node: ts.Node): ts.Node { + const visited = ts.visitEachChild(node, assignPositionsToNode, textChangesTransformationContext, assignPositionsToNodeArray, assignPositionsToNode); + // create proxy node for non synthesized nodes + const newNode = ts.nodeIsSynthesized(visited) ? visited : Object.create(visited) as ts.Node; + ts.setTextRangePosEnd(newNode, getPos(node), getEnd(node)); + return newNode; +} - // Filter out redundant changes. - if (span.length === newText.length && ts.stringContainsAt(sourceFile.text, newText, span.start)) { - return undefined; - } +function assignPositionsToNodeArray(nodes: ts.NodeArray, visitor: ts.Visitor, test?: (node: ts.Node) => boolean, start?: number, count?: number) { + const visited = ts.visitNodes(nodes, visitor, test, start, count); + if (!visited) { + return visited; + } + // clone nodearray if necessary + const nodeArray = visited === nodes ? ts.factory.createNodeArray(visited.slice(0)) : visited; + ts.setTextRangePosEnd(nodeArray, getPos(nodes), getEnd(nodes)); + return nodeArray; +} - return ts.createTextChange(span, newText); - }); +interface TextChangesWriter extends ts.EmitTextWriter, ts.PrintHandlers { +} - return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined; - }); - } +export function createWriter(newLine: string): TextChangesWriter { + let lastNonTriviaPosition = 0; - export function newFileChanges(oldFile: ts.SourceFile | undefined, fileName: string, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: ts.formatting.FormatContext): ts.FileTextChanges { - const text = newFileChangesWorker(oldFile, ts.getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); - return { fileName, textChanges: [ts.createTextChange(ts.createTextSpan(0, 0), text)], isNewFile: true }; + const writer = ts.createTextWriter(newLine); + const onBeforeEmitNode: ts.PrintHandlers["onBeforeEmitNode"] = node => { + if (node) { + setPos(node, lastNonTriviaPosition); } - - export function newFileChangesWorker(oldFile: ts.SourceFile | undefined, scriptKind: ts.ScriptKind, statements: readonly (ts.Statement | ts.SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: ts.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 => s === ts.SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); - const sourceFile = ts.createSourceFile("any file name", nonFormattedText, ts.ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); - const changes = ts.formatting.formatDocument(sourceFile, formatContext); - return applyChanges(nonFormattedText, changes) + newLineCharacter; + }; + const onAfterEmitNode: ts.PrintHandlers["onAfterEmitNode"] = node => { + if (node) { + setEnd(node, lastNonTriviaPosition); } - - function computeNewText(change: Change, sourceFile: ts.SourceFile, newLineCharacter: string, formatContext: ts.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: ts.Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate); - const text = change.kind === ChangeKind.ReplaceWithMultipleNodes - ? change.nodes.map(n => ts.removeSuffix(format(n), newLineCharacter)).join(change.options?.joiner || newLineCharacter) - : format(change.node); - // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line - const noIndent = (options.indentation !== undefined || ts.getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, ""); - return (options.prefix || "") + noIndent - + ((!options.suffix || ts.endsWith(noIndent, options.suffix)) - ? "" : options.suffix); - } - - /** Note: this may mutate `nodeIn`. */ - function getFormattedTextOfNode(nodeIn: ts.Node, sourceFile: ts.SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: ts.formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { - const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter); - if (validate) - validate(node, text); - const formatOptions = ts.getFormatCodeSettingsForWriting(formatContext, sourceFile); - const initialIndentation = indentation !== undefined - ? indentation - : ts.formatting.SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || ts.getLineStartPositionForPosition(pos, sourceFile) === pos); - if (delta === undefined) { - delta = ts.formatting.SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0; - } - - const file: ts.SourceFileLike = { - text, - getLineAndCharacterOfPosition(pos) { - return ts.getLineAndCharacterOfPosition(this, pos); - } - }; - const changes = ts.formatting.formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions }); - return applyChanges(text, changes); + }; + const onBeforeEmitNodeArray: ts.PrintHandlers["onBeforeEmitNodeArray"] = nodes => { + if (nodes) { + setPos(nodes, lastNonTriviaPosition); } - - /** Note: output node may be mutated input node. */ - export function getNonformattedText(node: ts.Node, sourceFile: ts.SourceFile | undefined, newLineCharacter: string): { - text: string; - node: ts.Node; - } { - const writer = createWriter(newLineCharacter); - const newLine = ts.getNewLineKind(newLineCharacter); - ts.createPrinter({ - newLine, - neverAsciiEscape: true, - preserveSourceNewlines: true, - terminateUnterminatedLiterals: true - }, writer).writeNode(ts.EmitHint.Unspecified, node, sourceFile, writer); - return { text: writer.getText(), node: assignPositionsToNode(node) }; + }; + const onAfterEmitNodeArray: ts.PrintHandlers["onAfterEmitNodeArray"] = nodes => { + if (nodes) { + setEnd(nodes, lastNonTriviaPosition); } - } + }; + const onBeforeEmitToken: ts.PrintHandlers["onBeforeEmitToken"] = node => { + if (node) { + setPos(node, lastNonTriviaPosition); + } + }; + const onAfterEmitToken: ts.PrintHandlers["onAfterEmitToken"] = node => { + if (node) { + setEnd(node, lastNonTriviaPosition); + } + }; - export function applyChanges(text: string, changes: readonly ts.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(ts.textSpanEnd(span))}`; + function setLastNonTriviaPosition(s: string, force: boolean) { + if (force || !isTrivia(s)) { + lastNonTriviaPosition = writer.getTextPos(); + let i = 0; + while (ts.isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) { + i++; + } + // trim trailing whitespaces + lastNonTriviaPosition -= i; } - return text; } - function isTrivia(s: string) { - return ts.skipTrivia(s, 0) === s.length; + 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: ts.Symbol): void { + writer.writeSymbol(s, sym); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeLine(force?: boolean): void { + writer.writeLine(force); + } + 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; } - // A transformation context that won't perform parenthesization, as some parenthesization rules - // are more aggressive than is strictly necessary. - const textChangesTransformationContext: ts.TransformationContext = { - ...ts.nullTransformationContext, - factory: ts.createNodeFactory(ts.nullTransformationContext.factory.flags | ts.NodeFactoryFlags.NoParenthesizerRules, ts.nullTransformationContext.factory.baseFactory), + return { + onBeforeEmitNode, + onAfterEmitNode, + 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 }; +} - export function assignPositionsToNode(node: ts.Node): ts.Node { - const visited = ts.visitEachChild(node, assignPositionsToNode, textChangesTransformationContext, assignPositionsToNodeArray, assignPositionsToNode); - // create proxy node for non synthesized nodes - const newNode = ts.nodeIsSynthesized(visited) ? visited : Object.create(visited) as ts.Node; - ts.setTextRangePosEnd(newNode, getPos(node), getEnd(node)); - return newNode; - } - - function assignPositionsToNodeArray(nodes: ts.NodeArray, visitor: ts.Visitor, test?: (node: ts.Node) => boolean, start?: number, count?: number) { - const visited = ts.visitNodes(nodes, visitor, test, start, count); - if (!visited) { - return visited; +function getInsertionPositionAtSourceFileTop(sourceFile: ts.SourceFile): number { + let lastPrologue: ts.PrologueDirective | undefined; + for (const node of sourceFile.statements) { + if (ts.isPrologueDirective(node)) { + lastPrologue = node; + } + else { + break; } - // clone nodearray if necessary - const nodeArray = visited === nodes ? ts.factory.createNodeArray(visited.slice(0)) : visited; - ts.setTextRangePosEnd(nodeArray, getPos(nodes), getEnd(nodes)); - return nodeArray; } - interface TextChangesWriter extends ts.EmitTextWriter, ts.PrintHandlers { + let position = 0; + const text = sourceFile.text; + if (lastPrologue) { + position = lastPrologue.end; + advancePastLineBreak(); + return position; } - export function createWriter(newLine: string): TextChangesWriter { - let lastNonTriviaPosition = 0; + const shebang = ts.getShebang(text); + if (shebang !== undefined) { + position = shebang.length; + advancePastLineBreak(); + } - const writer = ts.createTextWriter(newLine); - const onBeforeEmitNode: ts.PrintHandlers["onBeforeEmitNode"] = node => { - if (node) { - setPos(node, lastNonTriviaPosition); - } - }; - const onAfterEmitNode: ts.PrintHandlers["onAfterEmitNode"] = node => { - if (node) { - setEnd(node, lastNonTriviaPosition); - } - }; - const onBeforeEmitNodeArray: ts.PrintHandlers["onBeforeEmitNodeArray"] = nodes => { - if (nodes) { - setPos(nodes, lastNonTriviaPosition); - } - }; - const onAfterEmitNodeArray: ts.PrintHandlers["onAfterEmitNodeArray"] = nodes => { - if (nodes) { - setEnd(nodes, lastNonTriviaPosition); - } - }; - const onBeforeEmitToken: ts.PrintHandlers["onBeforeEmitToken"] = node => { - if (node) { - setPos(node, lastNonTriviaPosition); - } - }; - const onAfterEmitToken: ts.PrintHandlers["onAfterEmitToken"] = node => { - if (node) { - setEnd(node, lastNonTriviaPosition); - } - }; + const ranges = ts.getLeadingCommentRanges(text, position); + if (!ranges) + return position; - function setLastNonTriviaPosition(s: string, force: boolean) { - if (force || !isTrivia(s)) { - lastNonTriviaPosition = writer.getTextPos(); - let i = 0; - while (ts.isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) { - i++; - } - // trim trailing whitespaces - lastNonTriviaPosition -= i; + // Find the first attached comment to the first node and add before it + let lastComment: { + range: ts.CommentRange; + pinnedOrTripleSlash: boolean; + } | undefined; + let firstNodeLine: number | undefined; + for (const range of ranges) { + if (range.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + if (ts.isPinnedComment(text, range.pos)) { + lastComment = { range, pinnedOrTripleSlash: true }; + continue; } } - - 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: ts.Symbol): void { - writer.writeSymbol(s, sym); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeLine(force?: boolean): void { - writer.writeLine(force); - } - 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; + else if (ts.isRecognizedTripleSlashComment(text, range.pos, range.end)) { + lastComment = { range, pinnedOrTripleSlash: true }; + continue; } - return { - onBeforeEmitNode, - onAfterEmitNode, - 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 - }; - } + if (lastComment) { + // Always insert after pinned or triple slash comments + if (lastComment.pinnedOrTripleSlash) + break; - function getInsertionPositionAtSourceFileTop(sourceFile: ts.SourceFile): number { - let lastPrologue: ts.PrologueDirective | undefined; - for (const node of sourceFile.statements) { - if (ts.isPrologueDirective(node)) { - lastPrologue = node; - } - else { + // 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; - } } - let position = 0; - const text = sourceFile.text; - if (lastPrologue) { - position = lastPrologue.end; - advancePastLineBreak(); - return position; + 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 }; + } - const shebang = ts.getShebang(text); - if (shebang !== undefined) { - position = shebang.length; - advancePastLineBreak(); - } + if (lastComment) { + position = lastComment.range.end; + advancePastLineBreak(); + } + return position; - const ranges = ts.getLeadingCommentRanges(text, position); - if (!ranges) - return position; + function advancePastLineBreak() { + if (position < text.length) { + const charCode = text.charCodeAt(position); + if (ts.isLineBreak(charCode)) { + position++; - // Find the first attached comment to the first node and add before it - let lastComment: { - range: ts.CommentRange; - pinnedOrTripleSlash: boolean; - } | undefined; - let firstNodeLine: number | undefined; - for (const range of ranges) { - if (range.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - if (ts.isPinnedComment(text, range.pos)) { - lastComment = { range, pinnedOrTripleSlash: true }; - continue; + if (position < text.length && charCode === ts.CharacterCodes.carriageReturn && text.charCodeAt(position) === ts.CharacterCodes.lineFeed) { + position++; } } - else if (ts.isRecognizedTripleSlashComment(text, range.pos, range.end)) { - 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 }; } + } +} - if (lastComment) { - position = lastComment.range.end; - advancePastLineBreak(); - } - return position; +export function isValidLocationToAddComment(sourceFile: ts.SourceFile, position: number) { + return !ts.isInComment(sourceFile, position) && !ts.isInString(sourceFile, position) && !ts.isInTemplateString(sourceFile, position) && !ts.isInJSXText(sourceFile, position); +} - function advancePastLineBreak() { - if (position < text.length) { - const charCode = text.charCodeAt(position); - if (ts.isLineBreak(charCode)) { - position++; +function needSemicolonBetween(a: ts.Node, b: ts.Node): boolean { + return (ts.isPropertySignature(a) || ts.isPropertyDeclaration(a)) && ts.isClassOrTypeElement(b) && b.name!.kind === ts.SyntaxKind.ComputedPropertyName + || ts.isStatementButNotDeclaration(a) && ts.isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[` +} - if (position < text.length && charCode === ts.CharacterCodes.carriageReturn && text.charCodeAt(position) === ts.CharacterCodes.lineFeed) { - position++; - } +namespace deleteDeclaration { + export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.Node): void { + switch (node.kind) { + case ts.SyntaxKind.Parameter: { + const oldFunction = node.parent; + if (ts.isArrowFunction(oldFunction) && + oldFunction.parameters.length === 1 && + !ts.findChildOfKind(oldFunction, ts.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; } - } - } - export function isValidLocationToAddComment(sourceFile: ts.SourceFile, position: number) { - return !ts.isInComment(sourceFile, position) && !ts.isInString(sourceFile, position) && !ts.isInTemplateString(sourceFile, position) && !ts.isInJSXText(sourceFile, position); - } - - function needSemicolonBetween(a: ts.Node, b: ts.Node): boolean { - return (ts.isPropertySignature(a) || ts.isPropertyDeclaration(a)) && ts.isClassOrTypeElement(b) && b.name!.kind === ts.SyntaxKind.ComputedPropertyName - || ts.isStatementButNotDeclaration(a) && ts.isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[` - } + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ImportEqualsDeclaration: + const isFirstImport = sourceFile.imports.length && node === ts.first(sourceFile.imports).parent || node === ts.find(sourceFile.statements, ts.isAnyImportSyntax); + // For first import, leave header comment in place, otherwise only delete JSDoc comments + deleteNode(changes, sourceFile, node, { + leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : ts.hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine, + }); + break; - namespace deleteDeclaration { - export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.Node): void { - switch (node.kind) { - case ts.SyntaxKind.Parameter: { - const oldFunction = node.parent; - if (ts.isArrowFunction(oldFunction) && - oldFunction.parameters.length === 1 && - !ts.findChildOfKind(oldFunction, ts.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; + case ts.SyntaxKind.BindingElement: + const pattern = (node as ts.BindingElement).parent; + const preserveComma = pattern.kind === ts.SyntaxKind.ArrayBindingPattern && node !== ts.last(pattern.elements); + if (preserveComma) { + deleteNode(changes, sourceFile, node); } + else { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + } + break; - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ImportEqualsDeclaration: - const isFirstImport = sourceFile.imports.length && node === ts.first(sourceFile.imports).parent || node === ts.find(sourceFile.statements, ts.isAnyImportSyntax); - // For first import, leave header comment in place, otherwise only delete JSDoc comments - deleteNode(changes, sourceFile, node, { - leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : ts.hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine, - }); - break; - - case ts.SyntaxKind.BindingElement: - const pattern = (node as ts.BindingElement).parent; - const preserveComma = pattern.kind === ts.SyntaxKind.ArrayBindingPattern && node !== ts.last(pattern.elements); - if (preserveComma) { - deleteNode(changes, sourceFile, node); - } - else { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - break; + case ts.SyntaxKind.VariableDeclaration: + deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as ts.VariableDeclaration); + break; - case ts.SyntaxKind.VariableDeclaration: - deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as ts.VariableDeclaration); - break; + case ts.SyntaxKind.TypeParameter: + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + break; - case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.ImportSpecifier: + const namedImports = (node as ts.ImportSpecifier).parent; + if (namedImports.elements.length === 1) { + deleteImportBinding(changes, sourceFile, namedImports); + } + else { deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - break; - - case ts.SyntaxKind.ImportSpecifier: - const namedImports = (node as ts.ImportSpecifier).parent; - if (namedImports.elements.length === 1) { - deleteImportBinding(changes, sourceFile, namedImports); - } - else { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - break; - - case ts.SyntaxKind.NamespaceImport: - deleteImportBinding(changes, sourceFile, node as ts.NamespaceImport); - break; + } + break; - case ts.SyntaxKind.SemicolonToken: - deleteNode(changes, sourceFile, node, { trailingTriviaOption: TrailingTriviaOption.Exclude }); - break; + case ts.SyntaxKind.NamespaceImport: + deleteImportBinding(changes, sourceFile, node as ts.NamespaceImport); + break; - case ts.SyntaxKind.FunctionKeyword: - deleteNode(changes, sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.Exclude }); - break; + case ts.SyntaxKind.SemicolonToken: + deleteNode(changes, sourceFile, node, { trailingTriviaOption: TrailingTriviaOption.Exclude }); + break; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - deleteNode(changes, sourceFile, node, { leadingTriviaOption: ts.hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine }); - break; + case ts.SyntaxKind.FunctionKeyword: + deleteNode(changes, sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.Exclude }); + break; - default: - if (!node.parent) { - // a misbehaving client can reach here with the SourceFile node - deleteNode(changes, sourceFile, node); - } - else if (ts.isImportClause(node.parent) && node.parent.name === node) { - deleteDefaultImport(changes, sourceFile, node.parent); - } - else if (ts.isCallExpression(node.parent) && ts.contains(node.parent.arguments, node)) { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - else { - deleteNode(changes, sourceFile, node); - } - } - } + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + deleteNode(changes, sourceFile, node, { leadingTriviaOption: ts.hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine }); + break; - function deleteDefaultImport(changes: ChangeTracker, sourceFile: ts.SourceFile, importClause: ts.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 = ts.getTokenAtPosition(sourceFile, importClause.name!.end); - if (nextToken && nextToken.kind === ts.SyntaxKind.CommaToken) { - // shift first non-whitespace position after comma to the start position of the node - const end = ts.skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true); - changes.deleteRange(sourceFile, { pos: start, end }); + default: + if (!node.parent) { + // a misbehaving client can reach here with the SourceFile node + deleteNode(changes, sourceFile, node); + } + else if (ts.isImportClause(node.parent) && node.parent.name === node) { + deleteDefaultImport(changes, sourceFile, node.parent); + } + else if (ts.isCallExpression(node.parent) && ts.contains(node.parent.arguments, node)) { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); } else { - deleteNode(changes, sourceFile, importClause.name!); + deleteNode(changes, sourceFile, node); } - } } + } - function deleteImportBinding(changes: ChangeTracker, sourceFile: ts.SourceFile, node: ts.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 = ts.Debug.checkDefined(ts.getTokenAtPosition(sourceFile, node.pos - 1)); - changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end }); + function deleteDefaultImport(changes: ChangeTracker, sourceFile: ts.SourceFile, importClause: ts.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 = ts.getTokenAtPosition(sourceFile, importClause.name!.end); + if (nextToken && nextToken.kind === ts.SyntaxKind.CommaToken) { + // shift first non-whitespace position after comma to the start position of the node + const end = ts.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 = ts.getAncestor(node, ts.SyntaxKind.ImportDeclaration)!; - deleteNode(changes, sourceFile, importDecl); + deleteNode(changes, sourceFile, importClause.name!); } } + } - function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.VariableDeclaration): void { - const { parent } = node; + function deleteImportBinding(changes: ChangeTracker, sourceFile: ts.SourceFile, node: ts.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 = ts.Debug.checkDefined(ts.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 = ts.getAncestor(node, ts.SyntaxKind.ImportDeclaration)!; + deleteNode(changes, sourceFile, importDecl); + } + } - if (parent.kind === ts.SyntaxKind.CatchClause) { - // TODO: There's currently no unused diagnostic for this, could be a suggestion - changes.deleteNodeRange(sourceFile, ts.findChildOfKind(parent, ts.SyntaxKind.OpenParenToken, sourceFile)!, ts.findChildOfKind(parent, ts.SyntaxKind.CloseParenToken, sourceFile)!); - return; - } + function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.VariableDeclaration): void { + const { parent } = node; - if (parent.declarations.length !== 1) { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - return; - } + if (parent.kind === ts.SyntaxKind.CatchClause) { + // TODO: There's currently no unused diagnostic for this, could be a suggestion + changes.deleteNodeRange(sourceFile, ts.findChildOfKind(parent, ts.SyntaxKind.OpenParenToken, sourceFile)!, ts.findChildOfKind(parent, ts.SyntaxKind.CloseParenToken, sourceFile)!); + return; + } - const gp = parent.parent; - switch (gp.kind) { - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.ForInStatement: - changes.replaceNode(sourceFile, node, ts.factory.createObjectLiteralExpression()); - break; + if (parent.declarations.length !== 1) { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + return; + } - case ts.SyntaxKind.ForStatement: - deleteNode(changes, sourceFile, parent); - break; + const gp = parent.parent; + switch (gp.kind) { + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.ForInStatement: + changes.replaceNode(sourceFile, node, ts.factory.createObjectLiteralExpression()); + break; - case ts.SyntaxKind.VariableStatement: - deleteNode(changes, sourceFile, gp, { leadingTriviaOption: ts.hasJSDocNodes(gp) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine }); - break; + case ts.SyntaxKind.ForStatement: + deleteNode(changes, sourceFile, parent); + break; - default: - ts.Debug.assertNever(gp); - } - } - } + case ts.SyntaxKind.VariableStatement: + deleteNode(changes, sourceFile, gp, { leadingTriviaOption: ts.hasJSDocNodes(gp) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine }); + break; - /** 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: ts.SourceFile, node: ts.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 }); + default: + ts.Debug.assertNever(gp); + } } +} - function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.Node): void { - const containingList = ts.Debug.checkDefined(ts.formatting.SmartIndenter.getContainingList(node, sourceFile)); - const index = ts.indexOfNode(containingList, node); - ts.Debug.assert(index !== -1); - if (containingList.length === 1) { - deleteNode(changes, sourceFile, node); - return; - } +/** 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: ts.SourceFile, node: ts.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 }); +} - // 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`. - ts.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]), - }); +function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: ts.Set, sourceFile: ts.SourceFile, node: ts.Node): void { + const containingList = ts.Debug.checkDefined(ts.formatting.SmartIndenter.getContainingList(node, sourceFile)); + const index = ts.indexOfNode(containingList, node); + ts.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`. + ts.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 8d51cb5d804fc..be8c18096334f 100644 --- a/src/services/transform.ts +++ b/src/services/transform.ts @@ -1,16 +1,16 @@ 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: ts.TransformerFactory[], compilerOptions?: ts.CompilerOptions) { - const diagnostics: ts.DiagnosticWithLocation[] = []; - compilerOptions = ts.fixupCompilerOptions(compilerOptions!, diagnostics); // TODO: GH#18217 - const nodes = ts.isArray(source) ? source : [source]; - const result = ts.transformNodes(/*resolver*/ undefined, /*emitHost*/ undefined, ts.factory, compilerOptions, nodes, transformers, /*allowDtsFiles*/ true); - result.diagnostics = ts.concatenate(result.diagnostics, diagnostics); - return result; - } -} \ No newline at end of file +/** + * 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: ts.TransformerFactory[], compilerOptions?: ts.CompilerOptions) { + const diagnostics: ts.DiagnosticWithLocation[] = []; + compilerOptions = ts.fixupCompilerOptions(compilerOptions!, diagnostics); // TODO: GH#18217 + const nodes = ts.isArray(source) ? source : [source]; + const result = ts.transformNodes(/*resolver*/ undefined, /*emitHost*/ undefined, ts.factory, compilerOptions, nodes, transformers, /*allowDtsFiles*/ true); + result.diagnostics = ts.concatenate(result.diagnostics, diagnostics); + return result; +} +} diff --git a/src/services/transpile.ts b/src/services/transpile.ts index 542b1c67b3a88..e1751d78e6288 100644 --- a/src/services/transpile.ts +++ b/src/services/transpile.ts @@ -1,148 +1,148 @@ namespace ts { - export interface TranspileOptions { - compilerOptions?: ts.CompilerOptions; - fileName?: string; - reportDiagnostics?: boolean; - moduleName?: string; - renamedDependencies?: ts.MapLike; - transformers?: ts.CustomTransformers; - } +export interface TranspileOptions { + compilerOptions?: ts.CompilerOptions; + fileName?: string; + reportDiagnostics?: boolean; + moduleName?: string; + renamedDependencies?: ts.MapLike; + transformers?: ts.CustomTransformers; +} - export interface TranspileOutput { - outputText: string; - diagnostics?: ts.Diagnostic[]; - sourceMapText?: string; - } +export interface TranspileOutput { + outputText: string; + diagnostics?: ts.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: ts.Diagnostic[] = []; - const options: ts.CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {}; - - // mix in default options - const defaultOptions = ts.getDefaultCompilerOptions(); - for (const key in defaultOptions) { - if (ts.hasProperty(defaultOptions, key) && options[key] === undefined) { - options[key] = defaultOptions[key]; - } +/* + * 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: ts.Diagnostic[] = []; + const options: ts.CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {}; + + // mix in default options + const defaultOptions = ts.getDefaultCompilerOptions(); + for (const key in defaultOptions) { + if (ts.hasProperty(defaultOptions, key) && options[key] === undefined) { + options[key] = defaultOptions[key]; } + } - for (const option of ts.transpileOptionValueCompilerOptions) { - options[option.name] = option.transpileOptionValue; - } + for (const option of ts.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; - - const newLine = ts.getNewLineCharacter(options); - // Create a compilerHost object to allow the compiler to read and write files - const compilerHost: ts.CompilerHost = { - getSourceFile: (fileName) => fileName === ts.normalizePath(inputFileName) ? sourceFile : undefined, - writeFile: (name, text) => { - if (ts.fileExtensionIs(name, ".map")) { - ts.Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name); - sourceMapText = text; - } - else { - ts.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: () => [] - }; - - // if jsx is specified then treat file as .tsx - const inputFileName = transpileOptions.fileName || (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx ? "module.tsx" : "module.ts"); - const sourceFile = ts.createSourceFile(inputFileName, input, { - languageVersion: ts.getEmitScriptTarget(options), - impliedNodeFormat: ts.getImpliedNodeFormatForFile(ts.toPath(inputFileName, "", compilerHost.getCanonicalFileName), /*cache*/ undefined, compilerHost, options), - setExternalModuleIndicator: ts.getSetExternalModuleIndicator(options) - }); - if (transpileOptions.moduleName) { - sourceFile.moduleName = transpileOptions.moduleName; - } + // 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; - if (transpileOptions.renamedDependencies) { - sourceFile.renamedDependencies = new ts.Map(ts.getEntries(transpileOptions.renamedDependencies)); - } + // Filename can be non-ts file. + options.allowNonTsExtensions = true; - // Output - let outputText: string | undefined; - let sourceMapText: string | undefined; + const newLine = ts.getNewLineCharacter(options); + // Create a compilerHost object to allow the compiler to read and write files + const compilerHost: ts.CompilerHost = { + getSourceFile: (fileName) => fileName === ts.normalizePath(inputFileName) ? sourceFile : undefined, + writeFile: (name, text) => { + if (ts.fileExtensionIs(name, ".map")) { + ts.Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name); + sourceMapText = text; + } + else { + ts.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: () => [] + }; + + // if jsx is specified then treat file as .tsx + const inputFileName = transpileOptions.fileName || (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx ? "module.tsx" : "module.ts"); + const sourceFile = ts.createSourceFile(inputFileName, input, { + languageVersion: ts.getEmitScriptTarget(options), + impliedNodeFormat: ts.getImpliedNodeFormatForFile(ts.toPath(inputFileName, "", compilerHost.getCanonicalFileName), /*cache*/ undefined, compilerHost, options), + setExternalModuleIndicator: ts.getSetExternalModuleIndicator(options) + }); + if (transpileOptions.moduleName) { + sourceFile.moduleName = transpileOptions.moduleName; + } - const program = ts.createProgram([inputFileName], options, compilerHost); + if (transpileOptions.renamedDependencies) { + sourceFile.renamedDependencies = new ts.Map(ts.getEntries(transpileOptions.renamedDependencies)); + } - if (transpileOptions.reportDiagnostics) { - ts.addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); - ts.addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); - } - // Emit - program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); + // Output + let outputText: string | undefined; + let sourceMapText: string | undefined; - if (outputText === undefined) - return ts.Debug.fail("Output generation failed"); + const program = ts.createProgram([inputFileName], options, compilerHost); - return { outputText, diagnostics, sourceMapText }; + if (transpileOptions.reportDiagnostics) { + ts.addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); + ts.addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); } + // Emit + program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); - /* - * 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?: ts.CompilerOptions, fileName?: string, diagnostics?: ts.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 - ts.addRange(diagnostics, output.diagnostics); - return output.outputText; - } + if (outputText === undefined) + return ts.Debug.fail("Output generation failed"); - let commandLineOptionsStringToEnum: ts.CommandLineOptionOfCustomType[]; + return { outputText, diagnostics, sourceMapText }; +} - /** JS users may pass in string values for enum compiler options (such as ModuleKind), so convert. */ - /*@internal*/ - export function fixupCompilerOptions(options: ts.CompilerOptions, diagnostics: ts.Diagnostic[]): ts.CompilerOptions { - // Lazily create this value to fix module loading errors. - commandLineOptionsStringToEnum = commandLineOptionsStringToEnum || - ts.filter(ts.optionDeclarations, o => typeof o.type === "object" && !ts.forEachEntry(o.type, v => typeof v !== "number")) as ts.CommandLineOptionOfCustomType[]; - options = ts.cloneCompilerOptions(options); +/* + * 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?: ts.CompilerOptions, fileName?: string, diagnostics?: ts.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 + ts.addRange(diagnostics, output.diagnostics); + return output.outputText; +} - for (const opt of commandLineOptionsStringToEnum) { - if (!ts.hasProperty(options, opt.name)) { - continue; - } +let commandLineOptionsStringToEnum: ts.CommandLineOptionOfCustomType[]; - const value = options[opt.name]; - // Value should be a key of opt.type - if (ts.isString(value)) { - // If value is not a string, this will fail - options[opt.name] = ts.parseCustomTypeOption(opt, value, diagnostics); - } - else { - if (!ts.forEachEntry(opt.type, v => v === value)) { - // Supplied value isn't a valid enum value. - diagnostics.push(ts.createCompilerDiagnosticForInvalidCustomType(opt)); - } - } +/** JS users may pass in string values for enum compiler options (such as ModuleKind), so convert. */ +/*@internal*/ +export function fixupCompilerOptions(options: ts.CompilerOptions, diagnostics: ts.Diagnostic[]): ts.CompilerOptions { + // Lazily create this value to fix module loading errors. + commandLineOptionsStringToEnum = commandLineOptionsStringToEnum || + ts.filter(ts.optionDeclarations, o => typeof o.type === "object" && !ts.forEachEntry(o.type, v => typeof v !== "number")) as ts.CommandLineOptionOfCustomType[]; + options = ts.cloneCompilerOptions(options); + + for (const opt of commandLineOptionsStringToEnum) { + if (!ts.hasProperty(options, opt.name)) { + continue; } - return options; + const value = options[opt.name]; + // Value should be a key of opt.type + if (ts.isString(value)) { + // If value is not a string, this will fail + options[opt.name] = ts.parseCustomTypeOption(opt, value, diagnostics); + } + else { + if (!ts.forEachEntry(opt.type, v => v === value)) { + // Supplied value isn't a valid enum value. + diagnostics.push(ts.createCompilerDiagnosticForInvalidCustomType(opt)); + } + } } + + return options; +} } diff --git a/src/services/types.ts b/src/services/types.ts index f029b9639f327..29cec10000e7c 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,1633 +1,1633 @@ namespace ts { - export interface 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?: ts.SourceFileLike): ts.Node[]; // eslint-disable-line @typescript-eslint/unified-signatures - getStart(sourceFile?: ts.SourceFile, includeJsDocComment?: boolean): number; - /* @internal */ - getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number; // eslint-disable-line @typescript-eslint/unified-signatures - getFullStart(): number; - getEnd(): number; - getWidth(sourceFile?: ts.SourceFileLike): number; - getFullWidth(): number; - 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?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures - getLastToken(sourceFile?: ts.SourceFile): ts.Node | undefined; - /* @internal */ - getLastToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures - // See ts.forEachChild for documentation. - forEachChild(cbNode: (node: ts.Node) => T | undefined, cbNodeArray?: (nodes: ts.NodeArray) => T | undefined): T | undefined; - } +export interface 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?: ts.SourceFileLike): ts.Node[]; // eslint-disable-line @typescript-eslint/unified-signatures + getStart(sourceFile?: ts.SourceFile, includeJsDocComment?: boolean): number; + /* @internal */ + getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number; // eslint-disable-line @typescript-eslint/unified-signatures + getFullStart(): number; + getEnd(): number; + getWidth(sourceFile?: ts.SourceFileLike): number; + getFullWidth(): number; + 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?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures + getLastToken(sourceFile?: ts.SourceFile): ts.Node | undefined; + /* @internal */ + getLastToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures + // See ts.forEachChild for documentation. + forEachChild(cbNode: (node: ts.Node) => T | undefined, cbNodeArray?: (nodes: ts.NodeArray) => T | undefined): T | undefined; +} - export interface Identifier { - readonly text: string; - } +export interface Identifier { + readonly text: string; +} - export interface PrivateIdentifier { - readonly text: string; - } +export interface PrivateIdentifier { + readonly text: string; +} - export interface Symbol { - readonly name: string; - getFlags(): ts.SymbolFlags; - getEscapedName(): ts.__String; - getName(): string; - getDeclarations(): ts.Declaration[] | undefined; - getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; - /* @internal */ - getContextualDocumentationComment(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): SymbolDisplayPart[]; - getJsDocTags(checker?: ts.TypeChecker): JSDocTagInfo[]; - /* @internal */ - getContextualJsDocTags(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): JSDocTagInfo[]; - } +export interface Symbol { + readonly name: string; + getFlags(): ts.SymbolFlags; + getEscapedName(): ts.__String; + getName(): string; + getDeclarations(): ts.Declaration[] | undefined; + getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; + /* @internal */ + getContextualDocumentationComment(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): SymbolDisplayPart[]; + getJsDocTags(checker?: ts.TypeChecker): JSDocTagInfo[]; + /* @internal */ + getContextualJsDocTags(context: ts.Node | undefined, checker: ts.TypeChecker | undefined): JSDocTagInfo[]; +} - export interface 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(): 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; - isIndexType(): this is ts.IndexType; - } +export interface 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(): 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; + isIndexType(): this is ts.IndexType; +} - export interface TypeReference { - typeArguments?: readonly ts.Type[]; - } +export interface TypeReference { + typeArguments?: readonly ts.Type[]; +} - export interface Signature { - getDeclaration(): ts.SignatureDeclaration; - getTypeParameters(): ts.TypeParameter[] | undefined; - getParameters(): ts.Symbol[]; - getTypeParameterAtPosition(pos: number): ts.Type; - getReturnType(): ts.Type; - getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; - getJsDocTags(): JSDocTagInfo[]; - } +export interface Signature { + getDeclaration(): ts.SignatureDeclaration; + getTypeParameters(): ts.TypeParameter[] | undefined; + getParameters(): ts.Symbol[]; + getTypeParameterAtPosition(pos: number): ts.Type; + getReturnType(): ts.Type; + getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; + getJsDocTags(): JSDocTagInfo[]; +} - export interface SourceFile { - /* @internal */ version: string; - /* @internal */ scriptSnapshot: IScriptSnapshot | undefined; - /* @internal */ nameTable: ts.UnderscoreEscapedMap | undefined; - /* @internal */ getNamedDeclarations(): ts.ESMap; - getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; - getLineEndOfPosition(pos: number): number; - getLineStarts(): readonly number[]; - getPositionOfLineAndCharacter(line: number, character: number): number; - update(newText: string, textChangeRange: ts.TextChangeRange): ts.SourceFile; - /* @internal */ sourceMapper?: ts.DocumentPositionMapper; - } +export interface SourceFile { + /* @internal */ version: string; + /* @internal */ scriptSnapshot: IScriptSnapshot | undefined; + /* @internal */ nameTable: ts.UnderscoreEscapedMap | undefined; + /* @internal */ getNamedDeclarations(): ts.ESMap; + getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; + getLineEndOfPosition(pos: number): number; + getLineStarts(): readonly number[]; + getPositionOfLineAndCharacter(line: number, character: number): number; + update(newText: string, textChangeRange: ts.TextChangeRange): ts.SourceFile; + /* @internal */ sourceMapper?: ts.DocumentPositionMapper; +} - export interface SourceFileLike { - getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; - } +export interface SourceFileLike { + getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; +} - export interface SourceMapSource { - getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; - } +export interface SourceMapSource { + 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/naming-convention +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/naming-convention - 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): ts.TextChangeRange | undefined; - - /** Releases all resources held by this script snapshot */ - dispose?(): void; - } + getChangeRange(oldSnapshot: IScriptSnapshot): ts.TextChangeRange | undefined; - export namespace ScriptSnapshot { - class StringScriptSnapshot implements IScriptSnapshot { + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} - constructor(private text: string) { - } +export namespace ScriptSnapshot { + class StringScriptSnapshot implements IScriptSnapshot { - public getText(start: number, end: number): string { - return start === 0 && end === this.text.length - ? this.text - : this.text.substring(start, end); - } + constructor(private text: string) { + } - public getLength(): number { - return this.text.length; - } + public getText(start: number, end: number): string { + return start === 0 && end === this.text.length + ? this.text + : this.text.substring(start, end); + } - public getChangeRange(): ts.TextChangeRange | undefined { - // Text-based snapshots do not support incremental parsing. Return undefined - // to signal that to the caller. - return undefined; - } + public getLength(): number { + return this.text.length; } - export function fromString(text: string): IScriptSnapshot { - return new StringScriptSnapshot(text); + public getChangeRange(): ts.TextChangeRange | undefined { + // Text-based snapshots do not support incremental parsing. Return undefined + // to signal that to the caller. + return undefined; } } - export interface PreProcessedFileInfo { - referencedFiles: ts.FileReference[]; - typeReferenceDirectives: ts.FileReference[]; - libReferenceDirectives: ts.FileReference[]; - importedFiles: ts.FileReference[]; - ambientExternalModules?: string[]; - isLibFile: boolean; + export function fromString(text: string): IScriptSnapshot { + return new StringScriptSnapshot(text); } +} - export interface HostCancellationToken { - isCancellationRequested(): boolean; - } +export interface PreProcessedFileInfo { + referencedFiles: ts.FileReference[]; + typeReferenceDirectives: ts.FileReference[]; + libReferenceDirectives: ts.FileReference[]; + importedFiles: ts.FileReference[]; + ambientExternalModules?: string[]; + isLibFile: boolean; +} - export interface InstallPackageOptions { - fileName: ts.Path; - packageName: string; - } +export interface HostCancellationToken { + isCancellationRequested(): boolean; +} - /* @internal */ - export const enum PackageJsonDependencyGroup { - Dependencies = 1 << 0, - DevDependencies = 1 << 1, - PeerDependencies = 1 << 2, - OptionalDependencies = 1 << 3, - All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies - } +export interface InstallPackageOptions { + fileName: ts.Path; + packageName: string; +} - /* @internal */ - export interface PackageJsonInfo { - fileName: string; - parseable: boolean; - dependencies?: ts.ESMap; - devDependencies?: ts.ESMap; - peerDependencies?: ts.ESMap; - optionalDependencies?: ts.ESMap; - get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined; - has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean; - } +/* @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 FormattingHost { - getNewLine?(): string; - } +/* @internal */ +export interface PackageJsonInfo { + fileName: string; + parseable: boolean; + dependencies?: ts.ESMap; + devDependencies?: ts.ESMap; + peerDependencies?: ts.ESMap; + optionalDependencies?: ts.ESMap; + get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined; + has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean; +} - /* @internal */ - export const enum PackageJsonAutoImportPreference { - Off, - On, - Auto - } +/* @internal */ +export interface FormattingHost { + getNewLine?(): string; +} - export interface PerformanceEvent { - kind: "UpdateGraph" | "CreatePackageJsonAutoImportProvider"; - durationMs: number; - } +/* @internal */ +export const enum PackageJsonAutoImportPreference { + Off, + On, + Auto +} - export enum LanguageServiceMode { - Semantic, - PartialSemantic, - Syntactic - } +export interface PerformanceEvent { + kind: "UpdateGraph" | "CreatePackageJsonAutoImportProvider"; + durationMs: number; +} - export interface IncompleteCompletionsCache { - get(): CompletionInfo | undefined; - set(response: CompletionInfo): void; - clear(): void; - } +export enum LanguageServiceMode { + Semantic, + PartialSemantic, + Syntactic +} - // - // Public interface of the host of a language service instance. - // - export interface LanguageServiceHost extends ts.GetEffectiveTypeRootsHost, ts.MinimalResolutionCacheHost { - getCompilationSettings(): ts.CompilerOptions; - getNewLine?(): string; - getProjectVersion?(): string; - getScriptFileNames(): string[]; - getScriptKind?(fileName: string): ts.ScriptKind; - getScriptVersion(fileName: string): string; - getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; - getProjectReferences?(): readonly ts.ProjectReference[] | undefined; - getLocalizedDiagnosticMessages?(): any; - getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; - getDefaultLibFileName(options: ts.CompilerOptions): string; - log?(s: string): void; - trace?(s: string): void; - error?(s: string): void; - useCaseSensitiveFileNames?(): boolean; - - /* - * 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[]; - realpath?(path: string): string; - - /* - * Unlike `realpath and `readDirectory`, `readFile` and `fileExists` are now _required_ - * to properly acquire and setup source files under module: node16+ modes. - */ - readFile(path: string, encoding?: string): string | undefined; - 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: ts.ResolvedProjectReference | undefined, options: ts.CompilerOptions, containingSourceFile?: ts.SourceFile): (ts.ResolvedModule | undefined)[]; - getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined; - resolveTypeReferenceDirectives?(typeDirectiveNames: string[] | ts.FileReference[], containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, options: ts.CompilerOptions, containingFileMode?: ts.SourceFile["impliedNodeFormat"] | undefined): (ts.ResolvedTypeReferenceDirective | undefined)[]; - /* @internal */ hasInvalidatedResolution?: ts.HasInvalidatedResolution; - /* @internal */ hasChangedAutomaticTypeDirectiveNames?: ts.HasChangedAutomaticTypeDirectiveNames; - /* @internal */ getGlobalTypingsCacheLocation?(): string | undefined; - /* @internal */ getSymlinkCache?(files?: readonly ts.SourceFile[]): ts.SymlinkCache; - /* Lets the Program from a AutoImportProviderProject use its host project's ModuleResolutionCache */ - /* @internal */ getModuleResolutionCache?(): ts.ModuleResolutionCache | 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?(): ts.CustomTransformers | undefined; - - isKnownTypesPackageName?(name: string): boolean; - installPackage?(options: InstallPackageOptions): Promise; - writeFile?(fileName: string, content: string): void; - - /* @internal */ getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): ts.DocumentPositionMapper | undefined; - /* @internal */ getSourceFileLike?(fileName: string): ts.SourceFileLike | undefined; - /* @internal */ getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[]; - /* @internal */ getNearestAncestorDirectoryWithPackageJson?(fileName: string): string | undefined; - /* @internal */ getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[]; - /* @internal */ getCachedExportInfoMap?(): ts.ExportInfoMap; - /* @internal */ getModuleSpecifierCache?(): ts.ModuleSpecifierCache; - /* @internal */ setCompilerHost?(host: ts.CompilerHost): void; - /* @internal */ useSourceOfProjectReferenceRedirect?(): boolean; - /* @internal */ getPackageJsonAutoImportProvider?(): ts.Program | undefined; - /* @internal */ sendPerformanceEvent?(kind: PerformanceEvent["kind"], durationMs: number): void; - getParsedCommandLine?(fileName: string): ts.ParsedCommandLine | undefined; - /* @internal */ onReleaseParsedCommandLine?(configFileName: string, oldResolvedRef: ts.ResolvedProjectReference | undefined, optionOptions: ts.CompilerOptions): void; - /* @internal */ getIncompleteCompletionsCache?(): IncompleteCompletionsCache; - } +export interface IncompleteCompletionsCache { + get(): CompletionInfo | undefined; + set(response: CompletionInfo): void; + clear(): void; +} - /* @internal */ - export const emptyOptions = {}; +// +// Public interface of the host of a language service instance. +// +export interface LanguageServiceHost extends ts.GetEffectiveTypeRootsHost, ts.MinimalResolutionCacheHost { + getCompilationSettings(): ts.CompilerOptions; + getNewLine?(): string; + getProjectVersion?(): string; + getScriptFileNames(): string[]; + getScriptKind?(fileName: string): ts.ScriptKind; + getScriptVersion(fileName: string): string; + getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; + getProjectReferences?(): readonly ts.ProjectReference[] | undefined; + getLocalizedDiagnosticMessages?(): any; + getCancellationToken?(): HostCancellationToken; + getCurrentDirectory(): string; + getDefaultLibFileName(options: ts.CompilerOptions): string; + log?(s: string): void; + trace?(s: string): void; + error?(s: string): void; + useCaseSensitiveFileNames?(): boolean; + + /* + * 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[]; + realpath?(path: string): string; - export type WithMetadata = T & { - metadata?: unknown; - }; + /* + * Unlike `realpath and `readDirectory`, `readFile` and `fileExists` are now _required_ + * to properly acquire and setup source files under module: node16+ modes. + */ + readFile(path: string, encoding?: string): string | undefined; + fileExists(path: string): boolean; - export const enum SemanticClassificationFormat { - Original = "original", - TwentyTwenty = "2020" - } + /* + * 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: ts.ResolvedProjectReference | undefined, options: ts.CompilerOptions, containingSourceFile?: ts.SourceFile): (ts.ResolvedModule | undefined)[]; + getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string, resolutionMode?: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext): ts.ResolvedModuleWithFailedLookupLocations | undefined; + resolveTypeReferenceDirectives?(typeDirectiveNames: string[] | ts.FileReference[], containingFile: string, redirectedReference: ts.ResolvedProjectReference | undefined, options: ts.CompilerOptions, containingFileMode?: ts.SourceFile["impliedNodeFormat"] | undefined): (ts.ResolvedTypeReferenceDirective | undefined)[]; + /* @internal */ hasInvalidatedResolution?: ts.HasInvalidatedResolution; + /* @internal */ hasChangedAutomaticTypeDirectiveNames?: ts.HasChangedAutomaticTypeDirectiveNames; + /* @internal */ getGlobalTypingsCacheLocation?(): string | undefined; + /* @internal */ getSymlinkCache?(files?: readonly ts.SourceFile[]): ts.SymlinkCache; + /* Lets the Program from a AutoImportProviderProject use its host project's ModuleResolutionCache */ + /* @internal */ getModuleResolutionCache?(): ts.ModuleResolutionCache | 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[]; - // - // Public services of a language service instance associated - // with a language service host instance - // - export interface LanguageService { - /** This is used as a part of restarting the language service. */ - cleanupSemanticCache(): void; - - /** - * Gets errors indicating invalid syntax in a file. - * - * In English, "this cdeo have, erorrs" is syntactically invalid because it has typos, - * grammatical errors, and misplaced punctuation. Likewise, examples of syntax - * errors in TypeScript are missing parentheses in an `if` statement, mismatched - * curly braces, and using a reserved keyword as a variable name. - * - * These diagnostics are inexpensive to compute and don't require knowledge of - * other files. Note that a non-empty result increases the likelihood of false positives - * from `getSemanticDiagnostics`. - * - * While these represent the majority of syntax-related diagnostics, there are some - * that require the type system, which will be present in `getSemanticDiagnostics`. - * - * @param fileName A path to the file you want syntactic diagnostics for - */ - getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[]; - - /** - * Gets warnings or errors indicating type system issues in a given file. - * Requesting semantic diagnostics may start up the type system and - * run deferred work, so the first call may take longer than subsequent calls. - * - * Unlike the other get*Diagnostics functions, these diagnostics can potentially not - * include a reference to a source file. Specifically, the first time this is called, - * it will return global diagnostics with no associated location. - * - * To contrast the differences between semantic and syntactic diagnostics, consider the - * sentence: "The sun is green." is syntactically correct; those are real English words with - * correct sentence structure. However, it is semantically invalid, because it is not true. - * - * @param fileName A path to the file you want semantic diagnostics for - */ - getSemanticDiagnostics(fileName: string): ts.Diagnostic[]; - - /** - * Gets suggestion diagnostics for a specific file. These diagnostics tend to - * proactively suggest refactors, as opposed to diagnostics that indicate - * potentially incorrect runtime behavior. - * - * @param fileName A path to the file you want semantic diagnostics for - */ - getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[]; - - // TODO: Rename this to getProgramDiagnostics to better indicate that these are any - // diagnostics present for the program level, and not just 'options' diagnostics. - - /** - * Gets global diagnostics related to the program configuration and compiler options. - */ - getCompilerOptionsDiagnostics(): ts.Diagnostic[]; - - /** @deprecated Use getEncodedSyntacticClassifications instead. */ - getSyntacticClassifications(fileName: string, span: ts.TextSpan): ClassifiedSpan[]; - getSyntacticClassifications(fileName: string, span: ts.TextSpan, format: SemanticClassificationFormat): ClassifiedSpan[] | ClassifiedSpan2020[]; - - /** @deprecated Use getEncodedSemanticClassifications instead. */ - getSemanticClassifications(fileName: string, span: ts.TextSpan): ClassifiedSpan[]; - getSemanticClassifications(fileName: string, span: ts.TextSpan, format: SemanticClassificationFormat): ClassifiedSpan[] | ClassifiedSpan2020[]; - - /** Encoded as triples of [start, length, ClassificationType]. */ - getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): Classifications; - - /** - * Gets semantic highlights information for a particular file. Has two formats, an older - * version used by VS and a format used by VS Code. - * - * @param fileName The path to the file - * @param position A text span to return results within - * @param format Which format to use, defaults to "original" - * @returns a number array encoded as triples of [start, length, ClassificationType, ...]. - */ - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: SemanticClassificationFormat): Classifications; - - /** - * Gets completion entries at a particular position in a file. - * - * @param fileName The path to the file - * @param position A zero-based index of the character where you want the entries - * @param options An object describing how the request was triggered and what kinds - * of code actions can be returned with the completions. - * @param formattingSettings settings needed for calling formatting functions. - */ - getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined, formattingSettings?: FormatCodeSettings): WithMetadata | undefined; - - /** - * Gets the extended details for a completion entry retrieved from `getCompletionsAtPosition`. - * - * @param fileName The path to the file - * @param position A zero based index of the character where you want the entries - * @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition` - * @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility - * @param source `source` property from the completion entry - * @param preferences User settings, can be undefined for backwards compatibility - * @param data `data` property from the completion entry - */ - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined; - getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): ts.Symbol | undefined; - - /** - * Gets semantic information about the identifier at a particular position in a - * file. Quick info is what you typically see when you hover in an editor. - * - * @param fileName The path to the file - * @param position A zero-based index of the character where you want the quick info - */ - getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; - - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan | undefined; - getBreakpointStatementAtPosition(fileName: string, position: number): ts.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; - - /*@internal*/ - // eslint-disable-next-line @typescript-eslint/unified-signatures - getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly: false, stopAtAlias: boolean): readonly DefinitionInfo[] | undefined; - /*@internal*/ - // eslint-disable-next-line @typescript-eslint/unified-signatures - getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly: boolean, stopAtAlias: false): readonly DefinitionInfo[] | undefined; - 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[]): ts.DocumentHighlights[] | undefined; - getFileReferences(fileName: string): ReferenceEntry[]; - - /** @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[]; - - provideInlayHints(fileName: string, span: ts.TextSpan, preferences: ts.UserPreferences | undefined): InlayHint[]; - - getOutliningSpans(fileName: string): OutliningSpan[]; - getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; - getBraceMatchingAtPosition(fileName: string, position: number): ts.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, options?: DocCommentTemplateOptions): 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): ts.TextSpan | undefined; - toLineColumnOffset?(fileName: string, position: number): ts.LineAndCharacter; - /** @internal */ - getSourceMapper(): ts.SourceMapper; - /** @internal */ - clearSourceMapperCache(): void; - - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: ts.UserPreferences): readonly CodeFixAction[]; - getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: ts.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 | ts.TextRange, preferences: ts.UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | ts.TextRange, refactorName: string, actionName: string, preferences: ts.UserPreferences | undefined): RefactorEditInfo | undefined; - organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly FileTextChanges[]; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly FileTextChanges[]; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): ts.EmitOutput; - getProgram(): ts.Program | undefined; - /* @internal */ getNonBoundSourceFile(fileName: string): ts.SourceFile; - /* @internal */ getAutoImportProvider(): ts.Program | undefined; - toggleLineComment(fileName: string, textRange: ts.TextRange): TextChange[]; - toggleMultilineComment(fileName: string, textRange: ts.TextRange): TextChange[]; - commentSelection(fileName: string, textRange: ts.TextRange): TextChange[]; - uncommentSelection(fileName: string, textRange: ts.TextRange): TextChange[]; - - dispose(): void; - } + /** + * Gets a set of custom transformers to use during emit. + */ + getCustomTransformers?(): ts.CustomTransformers | undefined; + + isKnownTypesPackageName?(name: string): boolean; + installPackage?(options: InstallPackageOptions): Promise; + writeFile?(fileName: string, content: string): void; + + /* @internal */ getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): ts.DocumentPositionMapper | undefined; + /* @internal */ getSourceFileLike?(fileName: string): ts.SourceFileLike | undefined; + /* @internal */ getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[]; + /* @internal */ getNearestAncestorDirectoryWithPackageJson?(fileName: string): string | undefined; + /* @internal */ getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[]; + /* @internal */ getCachedExportInfoMap?(): ts.ExportInfoMap; + /* @internal */ getModuleSpecifierCache?(): ts.ModuleSpecifierCache; + /* @internal */ setCompilerHost?(host: ts.CompilerHost): void; + /* @internal */ useSourceOfProjectReferenceRedirect?(): boolean; + /* @internal */ getPackageJsonAutoImportProvider?(): ts.Program | undefined; + /* @internal */ sendPerformanceEvent?(kind: PerformanceEvent["kind"], durationMs: number): void; + getParsedCommandLine?(fileName: string): ts.ParsedCommandLine | undefined; + /* @internal */ onReleaseParsedCommandLine?(configFileName: string, oldResolvedRef: ts.ResolvedProjectReference | undefined, optionOptions: ts.CompilerOptions): void; + /* @internal */ getIncompleteCompletionsCache?(): IncompleteCompletionsCache; +} - export interface JsxClosingTagInfo { - readonly newText: string; - } +/* @internal */ +export const emptyOptions = {}; - export interface CombinedCodeFixScope { - type: "file"; - fileName: string; - } +export type WithMetadata = T & { + metadata?: unknown; +}; - export interface OrganizeImportsArgs extends CombinedCodeFixScope { - skipDestructiveCodeActions?: boolean; - } +export const enum SemanticClassificationFormat { + Original = "original", + TwentyTwenty = "2020" +} - export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; +// +// Public services of a language service instance associated +// with a language service host instance +// +export interface LanguageService { + /** This is used as a part of restarting the language service. */ + cleanupSemanticCache(): void; - export const enum CompletionTriggerKind { - /** Completion was triggered by typing an identifier, manual invocation (e.g Ctrl+Space) or via API. */ - Invoked = 1, + /** + * Gets errors indicating invalid syntax in a file. + * + * In English, "this cdeo have, erorrs" is syntactically invalid because it has typos, + * grammatical errors, and misplaced punctuation. Likewise, examples of syntax + * errors in TypeScript are missing parentheses in an `if` statement, mismatched + * curly braces, and using a reserved keyword as a variable name. + * + * These diagnostics are inexpensive to compute and don't require knowledge of + * other files. Note that a non-empty result increases the likelihood of false positives + * from `getSemanticDiagnostics`. + * + * While these represent the majority of syntax-related diagnostics, there are some + * that require the type system, which will be present in `getSemanticDiagnostics`. + * + * @param fileName A path to the file you want syntactic diagnostics for + */ + getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[]; - /** Completion was triggered by a trigger character. */ - TriggerCharacter = 2, + /** + * Gets warnings or errors indicating type system issues in a given file. + * Requesting semantic diagnostics may start up the type system and + * run deferred work, so the first call may take longer than subsequent calls. + * + * Unlike the other get*Diagnostics functions, these diagnostics can potentially not + * include a reference to a source file. Specifically, the first time this is called, + * it will return global diagnostics with no associated location. + * + * To contrast the differences between semantic and syntactic diagnostics, consider the + * sentence: "The sun is green." is syntactically correct; those are real English words with + * correct sentence structure. However, it is semantically invalid, because it is not true. + * + * @param fileName A path to the file you want semantic diagnostics for + */ + getSemanticDiagnostics(fileName: string): ts.Diagnostic[]; - /** Completion was re-triggered as the current completion list is incomplete. */ - TriggerForIncompleteCompletions = 3 - } + /** + * Gets suggestion diagnostics for a specific file. These diagnostics tend to + * proactively suggest refactors, as opposed to diagnostics that indicate + * potentially incorrect runtime behavior. + * + * @param fileName A path to the file you want semantic diagnostics for + */ + getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[]; - export interface GetCompletionsAtPositionOptions extends ts.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; - triggerKind?: CompletionTriggerKind; - /** @deprecated Use includeCompletionsForModuleExports */ - includeExternalModuleExports?: boolean; - /** @deprecated Use includeCompletionsWithInsertText */ - includeInsertTextCompletions?: boolean; - } + // TODO: Rename this to getProgramDiagnostics to better indicate that these are any + // diagnostics present for the program level, and not just 'options' diagnostics. + + /** + * Gets global diagnostics related to the program configuration and compiler options. + */ + getCompilerOptionsDiagnostics(): ts.Diagnostic[]; - export type SignatureHelpTriggerCharacter = "," | "(" | "<"; - export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + /** @deprecated Use getEncodedSyntacticClassifications instead. */ + getSyntacticClassifications(fileName: string, span: ts.TextSpan): ClassifiedSpan[]; + getSyntacticClassifications(fileName: string, span: ts.TextSpan, format: SemanticClassificationFormat): ClassifiedSpan[] | ClassifiedSpan2020[]; - export interface SignatureHelpItemsOptions { - triggerReason?: SignatureHelpTriggerReason; - } + /** @deprecated Use getEncodedSemanticClassifications instead. */ + getSemanticClassifications(fileName: string, span: ts.TextSpan): ClassifiedSpan[]; + getSemanticClassifications(fileName: string, span: ts.TextSpan, format: SemanticClassificationFormat): ClassifiedSpan[] | ClassifiedSpan2020[]; - export type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; + /** Encoded as triples of [start, length, ClassificationType]. */ + getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): Classifications; /** - * Signals that the user manually requested signature help. - * The language service will unconditionally attempt to provide a result. + * Gets semantic highlights information for a particular file. Has two formats, an older + * version used by VS and a format used by VS Code. + * + * @param fileName The path to the file + * @param position A text span to return results within + * @param format Which format to use, defaults to "original" + * @returns a number array encoded as triples of [start, length, ClassificationType, ...]. */ - export interface SignatureHelpInvokedReason { - kind: "invoked"; - triggerCharacter?: undefined; - } + getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: SemanticClassificationFormat): Classifications; /** - * 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. + * Gets completion entries at a particular position in a file. + * + * @param fileName The path to the file + * @param position A zero-based index of the character where you want the entries + * @param options An object describing how the request was triggered and what kinds + * of code actions can be returned with the completions. + * @param formattingSettings settings needed for calling formatting functions. */ - export interface SignatureHelpCharacterTypedReason { - kind: "characterTyped"; - /** - * Character that was responsible for triggering signature help. - */ - triggerCharacter: SignatureHelpTriggerCharacter; - } + getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined, formattingSettings?: FormatCodeSettings): WithMetadata | undefined; /** - * 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. + * Gets the extended details for a completion entry retrieved from `getCompletionsAtPosition`. + * + * @param fileName The path to the file + * @param position A zero based index of the character where you want the entries + * @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition` + * @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility + * @param source `source` property from the completion entry + * @param preferences User settings, can be undefined for backwards compatibility + * @param data `data` property from the completion entry */ - export interface SignatureHelpRetriggeredReason { - kind: "retrigger"; - /** - * Character that was responsible for triggering signature help. - */ - triggerCharacter?: SignatureHelpRetriggerCharacter; - } + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined; + getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): ts.Symbol | undefined; - export interface ApplyCodeActionCommandResult { - successMessage: string; - } + /** + * Gets semantic information about the identifier at a particular position in a + * file. Quick info is what you typically see when you hover in an editor. + * + * @param fileName The path to the file + * @param position A zero-based index of the character where you want the quick info + */ + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; - export interface Classifications { - spans: number[]; - endOfLineState: EndOfLineState; - } + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan | undefined; + getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan | undefined; - export interface ClassifiedSpan { - textSpan: ts.TextSpan; - classificationType: ClassificationTypeNames; - } + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; - export interface ClassifiedSpan2020 { - textSpan: ts.TextSpan; - classificationType: number; - } + 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; + + /*@internal*/ + // eslint-disable-next-line @typescript-eslint/unified-signatures + getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly: false, stopAtAlias: boolean): readonly DefinitionInfo[] | undefined; + /*@internal*/ + // eslint-disable-next-line @typescript-eslint/unified-signatures + getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly: boolean, stopAtAlias: false): readonly DefinitionInfo[] | undefined; + 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[]): ts.DocumentHighlights[] | undefined; + getFileReferences(fileName: string): ReferenceEntry[]; + + /** @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[]; + + provideInlayHints(fileName: string, span: ts.TextSpan, preferences: ts.UserPreferences | undefined): InlayHint[]; + + getOutliningSpans(fileName: string): OutliningSpan[]; + getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; + getBraceMatchingAtPosition(fileName: string, position: number): ts.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, options?: DocCommentTemplateOptions): TextInsertion | undefined; + + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; /** - * 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`. + * 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 NavigationBarItem { - text: string; - kind: ScriptElementKind; - kindModifiers: string; - spans: ts.TextSpan[]; - childItems: NavigationBarItem[]; - indent: number; - bolded: boolean; - grayed: boolean; - } + getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined; + + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan | undefined; + toLineColumnOffset?(fileName: string, position: number): ts.LineAndCharacter; + /** @internal */ + getSourceMapper(): ts.SourceMapper; + /** @internal */ + clearSourceMapperCache(): void; + + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: ts.UserPreferences): readonly CodeFixAction[]; + getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: ts.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 | ts.TextRange, preferences: ts.UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | ts.TextRange, refactorName: string, actionName: string, preferences: ts.UserPreferences | undefined): RefactorEditInfo | undefined; + organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly FileTextChanges[]; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly FileTextChanges[]; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): ts.EmitOutput; + getProgram(): ts.Program | undefined; + /* @internal */ getNonBoundSourceFile(fileName: string): ts.SourceFile; + /* @internal */ getAutoImportProvider(): ts.Program | undefined; + toggleLineComment(fileName: string, textRange: ts.TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRange: ts.TextRange): TextChange[]; + commentSelection(fileName: string, textRange: ts.TextRange): TextChange[]; + uncommentSelection(fileName: string, textRange: ts.TextRange): TextChange[]; + + dispose(): void; +} +export interface JsxClosingTagInfo { + readonly newText: string; +} + +export interface CombinedCodeFixScope { + type: "file"; + fileName: string; +} + +export interface OrganizeImportsArgs extends CombinedCodeFixScope { + skipDestructiveCodeActions?: boolean; +} + +export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; + +export const enum CompletionTriggerKind { + /** Completion was triggered by typing an identifier, manual invocation (e.g Ctrl+Space) or via API. */ + Invoked = 1, + + /** Completion was triggered by a trigger character. */ + TriggerCharacter = 2, + + /** Completion was re-triggered as the current completion list is incomplete. */ + TriggerForIncompleteCompletions = 3 +} + +export interface GetCompletionsAtPositionOptions extends ts.UserPreferences { /** - * Node in a tree of nested declarations in a file. - * The top node is always a script or module node. + * 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 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: ts.TextSpan[]; - nameSpan: ts.TextSpan | undefined; - /** Present if non-empty */ - childItems?: NavigationTree[]; - } + triggerCharacter?: CompletionsTriggerCharacter; + triggerKind?: CompletionTriggerKind; + /** @deprecated Use includeCompletionsForModuleExports */ + includeExternalModuleExports?: boolean; + /** @deprecated Use includeCompletionsWithInsertText */ + includeInsertTextCompletions?: boolean; +} - export interface CallHierarchyItem { - name: string; - kind: ScriptElementKind; - kindModifiers?: string; - file: string; - span: ts.TextSpan; - selectionSpan: ts.TextSpan; - containerName?: string; - } +export type SignatureHelpTriggerCharacter = "," | "(" | "<"; +export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; - export interface CallHierarchyIncomingCall { - from: CallHierarchyItem; - fromSpans: ts.TextSpan[]; - } +export interface SignatureHelpItemsOptions { + triggerReason?: SignatureHelpTriggerReason; +} - export interface CallHierarchyOutgoingCall { - to: CallHierarchyItem; - fromSpans: ts.TextSpan[]; - } +export type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; - export const enum InlayHintKind { - Type = "Type", - Parameter = "Parameter", - Enum = "Enum" - } +/** + * 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; +} - export interface InlayHint { - text: string; - position: number; - kind: InlayHintKind; - whitespaceBefore?: boolean; - whitespaceAfter?: boolean; - } +/** + * 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"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; +} - export interface TodoCommentDescriptor { - text: string; - priority: number; - } +/** + * 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"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; +} - export interface TodoComment { - descriptor: TodoCommentDescriptor; - message: string; - position: number; - } +export interface ApplyCodeActionCommandResult { + successMessage: string; +} - export interface TextChange { - span: ts.TextSpan; - newText: string; - } +export interface Classifications { + spans: number[]; + endOfLineState: EndOfLineState; +} - export interface FileTextChanges { - fileName: string; - textChanges: readonly TextChange[]; - isNewFile?: boolean; - } +export interface ClassifiedSpan { + textSpan: ts.TextSpan; + classificationType: ClassificationTypeNames; +} - 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 ClassifiedSpan2020 { + textSpan: ts.TextSpan; + classificationType: number; +} - 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; - } +/** + * 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: ts.TextSpan[]; + childItems: NavigationBarItem[]; + indent: number; + bolded: boolean; + grayed: boolean; +} - export interface CombinedCodeActions { - changes: readonly FileTextChanges[]; - commands?: readonly CodeActionCommand[]; - } +/** + * 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; + /** + * Spans of the nodes that generated this declaration. + * There will be more than one if this is the result of merging. + */ + spans: ts.TextSpan[]; + nameSpan: ts.TextSpan | undefined; + /** Present if non-empty */ + childItems?: NavigationTree[]; +} + +export interface CallHierarchyItem { + name: string; + kind: ScriptElementKind; + kindModifiers?: string; + file: string; + span: ts.TextSpan; + selectionSpan: ts.TextSpan; + containerName?: string; +} - // 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 CallHierarchyIncomingCall { + from: CallHierarchyItem; + fromSpans: ts.TextSpan[]; +} - export interface InstallPackageAction { - /* @internal */ readonly type: "install package"; - /* @internal */ readonly file: string; - /* @internal */ readonly packageName: string; - } +export interface CallHierarchyOutgoingCall { + to: CallHierarchyItem; + fromSpans: ts.TextSpan[]; +} + +export const enum InlayHintKind { + Type = "Type", + Parameter = "Parameter", + Enum = "Enum" +} + +export interface InlayHint { + text: string; + position: number; + kind: InlayHintKind; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} +export interface TodoCommentDescriptor { + text: string; + priority: number; +} + +export interface TodoComment { + descriptor: TodoCommentDescriptor; + message: string; + position: number; +} + +export interface TextChange { + span: ts.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 one or more available refactoring actions, grouped under a parent refactoring. + * 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 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[]; - } + commands?: CodeActionCommand[]; +} +export interface CodeFixAction extends CodeAction { + /** Short name to identify the fix, for use by telemetry. */ + fixName: 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. + * 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 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 message to show to the user if the refactoring cannot be applied in - * the current context. - */ - notApplicableReason?: string; - - /** - * The hierarchical dotted name of the refactor action. - */ - kind?: string; - } + 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 { /** - * A set of edits to make in response to a refactor action, plus an optional - * location where renaming should be invoked from + * The programmatic name of the refactoring */ - export interface RefactorEditInfo { - edits: FileTextChanges[]; - renameFilename?: string ; - renameLocation?: number; - commands?: CodeActionCommand[]; - } + 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; - export type RefactorTriggerReason = "implicit" | "invoked"; + actions: RefactorActionInfo[]; +} - export interface TextInsertion { - newText: string; - /** The position in newText the caret should point to after the insertion. */ - caretOffset: number; - } +/** + * 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; - export interface DocumentSpan { - textSpan: ts.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?: ts.TextSpan; - originalFileName?: string; - - /** - * If DocumentSpan.textSpan is the span for name of the declaration, - * then this is the span for relevant declaration - */ - contextSpan?: ts.TextSpan; - originalContextSpan?: ts.TextSpan; - } + /** + * 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; - export interface RenameLocation extends DocumentSpan { - readonly prefixText?: string; - readonly suffixText?: string; - } + /** + * A message to show to the user if the refactoring cannot be applied in + * the current context. + */ + notApplicableReason?: string; - export interface ReferenceEntry extends DocumentSpan { - isWriteAccess: boolean; - isInString?: true; - } + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; +} - export interface ImplementationLocation extends DocumentSpan { - kind: ScriptElementKind; - displayParts: SymbolDisplayPart[]; - } +/** + * 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 const enum HighlightSpanKind { - none = "none", - definition = "definition", - reference = "reference", - writtenReference = "writtenReference" - } +export type RefactorTriggerReason = "implicit" | "invoked"; - export interface HighlightSpan { - fileName?: string; - isInString?: true; - textSpan: ts.TextSpan; - contextSpan?: ts.TextSpan; - kind: HighlightSpanKind; - } +export interface TextInsertion { + newText: string; + /** The position in newText the caret should point to after the insertion. */ + caretOffset: number; +} - export interface NavigateToItem { - name: string; - kind: ScriptElementKind; - kindModifiers: string; - matchKind: "exact" | "prefix" | "substring" | "camelCase"; - isCaseSensitive: boolean; - fileName: string; - textSpan: ts.TextSpan; - containerName: string; - containerKind: ScriptElementKind; - } +export interface DocumentSpan { + textSpan: ts.TextSpan; + fileName: string; - export enum IndentStyle { - None = 0, - Block = 1, - Smart = 2 - } + /** + * 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?: ts.TextSpan; + originalFileName?: string; - export enum SemicolonPreference { - Ignore = "ignore", - Insert = "insert", - Remove = "remove" - } + /** + * If DocumentSpan.textSpan is the span for name of the declaration, + * then this is the span for relevant declaration + */ + contextSpan?: ts.TextSpan; + originalContextSpan?: ts.TextSpan; +} - /* @deprecated - consider using EditorSettings instead */ - export interface EditorOptions { - BaseIndentSize?: number; - IndentSize: number; - TabSize: number; - NewLineCharacter: string; - ConvertTabsToSpaces: boolean; - IndentStyle: IndentStyle; - } +export interface RenameLocation extends DocumentSpan { + readonly prefixText?: string; + readonly suffixText?: string; +} - // TODO: GH#18217 These are frequently asserted as defined - export interface EditorSettings { - baseIndentSize?: number; - indentSize?: number; - tabSize?: number; - newLineCharacter?: string; - convertTabsToSpaces?: boolean; - indentStyle?: IndentStyle; - trimTrailingWhitespace?: boolean; - } +export interface ReferenceEntry extends DocumentSpan { + isWriteAccess: boolean; + isInString?: true; +} - /* @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 ImplementationLocation extends DocumentSpan { + kind: ScriptElementKind; + displayParts: SymbolDisplayPart[]; +} - 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 insertSpaceAfterOpeningAndBeforeClosingEmptyBraces?: 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 const enum HighlightSpanKind { + none = "none", + definition = "definition", + reference = "reference", + writtenReference = "writtenReference" +} - 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, - trimTrailingWhitespace: true - }; - } +export interface HighlightSpan { + fileName?: string; + isInString?: true; + textSpan: ts.TextSpan; + contextSpan?: ts.TextSpan; + kind: HighlightSpanKind; +} - /* @internal */ - export const testFormatSettings = getDefaultFormatCodeSettings("\n"); - - export interface DefinitionInfo extends DocumentSpan { - kind: ScriptElementKind; - name: string; - containerKind: ScriptElementKind; - containerName: string; - unverified?: boolean; - /* @internal */ isLocal?: boolean; - /* @internal */ isAmbient?: boolean; - /* @internal */ failedAliasResolution?: boolean; - } +export interface NavigateToItem { + name: string; + kind: ScriptElementKind; + kindModifiers: string; + matchKind: "exact" | "prefix" | "substring" | "camelCase"; + isCaseSensitive: boolean; + fileName: string; + textSpan: ts.TextSpan; + containerName: string; + containerKind: ScriptElementKind; +} - export interface DefinitionInfoAndBoundSpan { - definitions?: readonly DefinitionInfo[]; - textSpan: ts.TextSpan; - } +export enum IndentStyle { + None = 0, + Block = 1, + Smart = 2 +} - export interface ReferencedSymbolDefinitionInfo extends DefinitionInfo { - displayParts: SymbolDisplayPart[]; - } +export enum SemicolonPreference { + Ignore = "ignore", + Insert = "insert", + Remove = "remove" +} - export interface ReferencedSymbol { - definition: ReferencedSymbolDefinitionInfo; - references: ReferencedSymbolEntry[]; - } +/* @deprecated - consider using EditorSettings instead */ +export interface EditorOptions { + BaseIndentSize?: number; + IndentSize: number; + TabSize: number; + NewLineCharacter: string; + ConvertTabsToSpaces: boolean; + IndentStyle: IndentStyle; +} - export interface ReferencedSymbolEntry extends ReferenceEntry { - isDefinition?: boolean; - } +// TODO: GH#18217 These are frequently asserted as defined +export interface EditorSettings { + baseIndentSize?: number; + indentSize?: number; + tabSize?: number; + newLineCharacter?: string; + convertTabsToSpaces?: boolean; + indentStyle?: IndentStyle; + trimTrailingWhitespace?: boolean; +} - 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, - link, - linkName, - linkText - } +/* @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 SymbolDisplayPart { - text: string; - kind: string; - } +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 insertSpaceAfterOpeningAndBeforeClosingEmptyBraces?: 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 interface JSDocLinkDisplayPart extends SymbolDisplayPart { - target: DocumentSpan; - } +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, + trimTrailingWhitespace: true + }; +} - export interface JSDocTagInfo { - name: string; - text?: SymbolDisplayPart[]; - } +/* @internal */ +export const testFormatSettings = getDefaultFormatCodeSettings("\n"); + +export interface DefinitionInfo extends DocumentSpan { + kind: ScriptElementKind; + name: string; + containerKind: ScriptElementKind; + containerName: string; + unverified?: boolean; + /* @internal */ isLocal?: boolean; + /* @internal */ isAmbient?: boolean; + /* @internal */ failedAliasResolution?: boolean; +} - export interface QuickInfo { - kind: ScriptElementKind; - kindModifiers: string; - textSpan: ts.TextSpan; - displayParts?: SymbolDisplayPart[]; - documentation?: SymbolDisplayPart[]; - tags?: JSDocTagInfo[]; - } +export interface DefinitionInfoAndBoundSpan { + definitions?: readonly DefinitionInfo[]; + textSpan: ts.TextSpan; +} - 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: ts.TextSpan; - } - export interface RenameInfoFailure { - canRename: false; - localizedErrorMessage: string; - } +export interface ReferencedSymbolDefinitionInfo extends DefinitionInfo { + displayParts: SymbolDisplayPart[]; +} - export interface RenameInfoOptions { - readonly allowRenameOfImportPath?: boolean; - } +export interface ReferencedSymbol { + definition: ReferencedSymbolDefinitionInfo; + references: ReferencedSymbolEntry[]; +} - export interface DocCommentTemplateOptions { - readonly generateReturnInDocTemplate?: boolean; - } +export interface ReferencedSymbolEntry extends ReferenceEntry { + isDefinition?: boolean; +} - export interface SignatureHelpParameter { - name: string; - documentation: SymbolDisplayPart[]; - displayParts: SymbolDisplayPart[]; - isOptional: boolean; - isRest?: boolean; - } +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, + link, + linkName, + linkText +} - export interface SelectionRange { - textSpan: ts.TextSpan; - parent?: SelectionRange; - } +export interface SymbolDisplayPart { + text: string; + kind: string; +} + +export interface JSDocLinkDisplayPart extends SymbolDisplayPart { + target: DocumentSpan; +} + +export interface JSDocTagInfo { + name: string; + text?: SymbolDisplayPart[]; +} +export interface QuickInfo { + kind: ScriptElementKind; + kindModifiers: string; + textSpan: ts.TextSpan; + displayParts?: SymbolDisplayPart[]; + documentation?: SymbolDisplayPart[]; + tags?: JSDocTagInfo[]; +} + +export type RenameInfo = RenameInfoSuccess | RenameInfoFailure; +export interface RenameInfoSuccess { + canRename: true; /** - * 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?'. + * File or directory to rename. + * If set, `getEditsForFileRename` should be called instead of `findRenameLocations`. */ - export interface SignatureHelpItem { - isVariadic: boolean; - prefixDisplayParts: SymbolDisplayPart[]; - suffixDisplayParts: SymbolDisplayPart[]; - separatorDisplayParts: SymbolDisplayPart[]; - parameters: SignatureHelpParameter[]; - documentation: SymbolDisplayPart[]; - tags: JSDocTagInfo[]; - } + fileToRename?: string; + displayName: string; + fullDisplayName: string; + kind: ScriptElementKind; + kindModifiers: string; + triggerSpan: ts.TextSpan; +} +export interface RenameInfoFailure { + canRename: false; + localizedErrorMessage: string; +} + +export interface RenameInfoOptions { + readonly allowRenameOfImportPath?: boolean; +} + +export interface DocCommentTemplateOptions { + readonly generateReturnInDocTemplate?: boolean; +} + +export interface SignatureHelpParameter { + name: string; + documentation: SymbolDisplayPart[]; + displayParts: SymbolDisplayPart[]; + isOptional: boolean; + isRest?: boolean; +} + +export interface SelectionRange { + textSpan: ts.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: ts.TextSpan; + selectedItemIndex: number; + argumentIndex: number; + argumentCount: number; +} + +// Do not change existing values, as they exist in telemetry. +export const enum CompletionInfoFlags { + None = 0, + MayIncludeAutoImports = 1 << 0, + IsImportStatementCompletion = 1 << 1, + IsContinuation = 1 << 2, + ResolvedModuleSpecifiers = 1 << 3, + ResolvedModuleSpecifiersBeyondLimit = 1 << 4, + MayIncludeMethodSnippets = 1 << 5 +} +export interface CompletionInfo { + /** For performance telemetry. */ + flags?: CompletionInfoFlags; + /** 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; /** - * Represents a set of signature help items, and the preferred item that should be selected. + * In the absence of `CompletionEntry["replacementSpan"], the editor may choose whether to use + * this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span + * must be used to commit that completion entry. */ - export interface SignatureHelpItems { - items: SignatureHelpItem[]; - applicableSpan: ts.TextSpan; - selectedItemIndex: number; - argumentIndex: number; - argumentCount: number; - } - - // Do not change existing values, as they exist in telemetry. - export const enum CompletionInfoFlags { - None = 0, - MayIncludeAutoImports = 1 << 0, - IsImportStatementCompletion = 1 << 1, - IsContinuation = 1 << 2, - ResolvedModuleSpecifiers = 1 << 3, - ResolvedModuleSpecifiersBeyondLimit = 1 << 4, - MayIncludeMethodSnippets = 1 << 5 - } + optionalReplacementSpan?: ts.TextSpan; + /** + * true when the current location also allows for a new identifier + */ + isNewIdentifierLocation: boolean; + /** + * Indicates to client to continue requesting completions on subsequent keystrokes. + */ + isIncomplete?: true; + entries: CompletionEntry[]; +} - export interface CompletionInfo { - /** For performance telemetry. */ - flags?: CompletionInfoFlags; - /** 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; - /** - * In the absence of `CompletionEntry["replacementSpan"], the editor may choose whether to use - * this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span - * must be used to commit that completion entry. - */ - optionalReplacementSpan?: ts.TextSpan; - /** - * true when the current location also allows for a new identifier - */ - isNewIdentifierLocation: boolean; - /** - * Indicates to client to continue requesting completions on subsequent keystrokes. - */ - isIncomplete?: true; - entries: CompletionEntry[]; - } +export interface CompletionEntryDataAutoImport { + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + exportName: string; + moduleSpecifier?: string; + /** The file name declaring the export's module symbol, if it was an external module */ + fileName?: string; + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + ambientModuleName?: string; + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport?: true; +} - export interface CompletionEntryDataAutoImport { - /** - * The name of the property or export in the module's symbol table. Differs from the completion name - * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. - */ - exportName: string; - moduleSpecifier?: string; - /** The file name declaring the export's module symbol, if it was an external module */ - fileName?: string; - /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ - ambientModuleName?: string; - /** True if the export was found in the package.json AutoImportProvider */ - isPackageJsonImport?: true; - } +export interface CompletionEntryDataUnresolved extends CompletionEntryDataAutoImport { + /** The key in the `ExportMapCache` where the completion entry's `SymbolExportInfo[]` is found */ + exportMapKey: string; +} - export interface CompletionEntryDataUnresolved extends CompletionEntryDataAutoImport { - /** The key in the `ExportMapCache` where the completion entry's `SymbolExportInfo[]` is found */ - exportMapKey: string; - } +export interface CompletionEntryDataResolved extends CompletionEntryDataAutoImport { + moduleSpecifier: string; +} - export interface CompletionEntryDataResolved extends CompletionEntryDataAutoImport { - moduleSpecifier: string; - } +export type CompletionEntryData = CompletionEntryDataUnresolved | CompletionEntryDataResolved; - export type CompletionEntryData = CompletionEntryDataUnresolved | CompletionEntryDataResolved; - - // see comments in protocol.ts - export interface CompletionEntry { - name: string; - kind: ScriptElementKind; - kindModifiers?: string; // see ScriptElementKindModifier, comma separated - sortText: string; - insertText?: string; - isSnippet?: true; - /** - * 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?: ts.TextSpan; - hasAction?: true; - source?: string; - sourceDisplay?: SymbolDisplayPart[]; - labelDetails?: CompletionEntryLabelDetails; - isRecommended?: true; - isFromUncheckedFile?: true; - isPackageJsonImport?: true; - isImportStatementCompletion?: true; - /** - * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, - * that allows TS Server to look up the symbol represented by the completion item, disambiguating - * items with the same name. Currently only defined for auto-import completions, but the type is - * `unknown` in the protocol, so it can be changed as needed to support other kinds of completions. - * The presence of this property should generally not be used to assume that this completion entry - * is an auto-import. - */ - data?: CompletionEntryData; - } +// see comments in protocol.ts +export interface CompletionEntry { + name: string; + kind: ScriptElementKind; + kindModifiers?: string; // see ScriptElementKindModifier, comma separated + sortText: string; + insertText?: string; + isSnippet?: true; + /** + * 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?: ts.TextSpan; + hasAction?: true; + source?: string; + sourceDisplay?: SymbolDisplayPart[]; + labelDetails?: CompletionEntryLabelDetails; + isRecommended?: true; + isFromUncheckedFile?: true; + isPackageJsonImport?: true; + isImportStatementCompletion?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. Currently only defined for auto-import completions, but the type is + * `unknown` in the protocol, so it can be changed as needed to support other kinds of completions. + * The presence of this property should generally not be used to assume that this completion entry + * is an auto-import. + */ + data?: CompletionEntryData; +} - export interface CompletionEntryLabelDetails { - detail?: string; - description?: string; - } +export interface CompletionEntryLabelDetails { + detail?: string; + description?: string; +} - export interface CompletionEntryDetails { - name: string; - kind: ScriptElementKind; - kindModifiers: string; // see ScriptElementKindModifier, comma separated - displayParts: SymbolDisplayPart[]; - documentation?: SymbolDisplayPart[]; - tags?: JSDocTagInfo[]; - codeActions?: CodeAction[]; - /** @deprecated Use `sourceDisplay` instead. */ - source?: SymbolDisplayPart[]; - sourceDisplay?: SymbolDisplayPart[]; - } +export interface CompletionEntryDetails { + name: string; + kind: ScriptElementKind; + kindModifiers: string; // see ScriptElementKindModifier, comma separated + displayParts: SymbolDisplayPart[]; + documentation?: SymbolDisplayPart[]; + tags?: JSDocTagInfo[]; + codeActions?: CodeAction[]; + /** @deprecated Use `sourceDisplay` instead. */ + source?: SymbolDisplayPart[]; + sourceDisplay?: SymbolDisplayPart[]; +} - export interface OutliningSpan { - /** The span of the document to actually collapse. */ - textSpan: ts.TextSpan; +export interface OutliningSpan { + /** The span of the document to actually collapse. */ + textSpan: ts.TextSpan; - /** The span of the document to display when the user hovers over the collapsed span. */ - hintSpan: ts.TextSpan; + /** The span of the document to display when the user hovers over the collapsed span. */ + hintSpan: ts.TextSpan; - /** The text to display in the editor for the collapsed region. */ - bannerText: string; + /** 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; + /** + * 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; - } + /** + * Classification of the contents of the span + */ + kind: OutliningSpanKind; +} - export const enum OutliningSpanKind { - /** Single or multi-line comments */ - Comment = "comment", +export const enum OutliningSpanKind { + /** Single or multi-line comments */ + Comment = "comment", - /** Sections marked by '// #region' and '// #endregion' comments */ - Region = "region", + /** Sections marked by '// #region' and '// #endregion' comments */ + Region = "region", - /** Declarations and expressions */ - Code = "code", + /** Declarations and expressions */ + Code = "code", - /** Contiguous blocks of import declarations */ - Imports = "imports" - } + /** Contiguous blocks of import declarations */ + Imports = "imports" +} - export const enum OutputFileType { - JavaScript, - SourceMap, - Declaration - } +export const enum OutputFileType { + JavaScript, + SourceMap, + Declaration +} - export const enum EndOfLineState { - None, - InMultiLineCommentTrivia, - InSingleQuoteStringLiteral, - InDoubleQuoteStringLiteral, - InTemplateHeadOrNoSubstitutionTemplate, - InTemplateMiddleOrTail, - InTemplateSubstitutionPosition - } +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 enum TokenClass { + Punctuation, + Keyword, + Operator, + Comment, + Whitespace, + Identifier, + NumberLiteral, + BigIntLiteral, + StringLiteral, + RegExpLiteral +} - export interface ClassificationResult { - finalLexState: EndOfLineState; - entries: ClassificationInfo[]; - } +export interface ClassificationResult { + finalLexState: EndOfLineState; + entries: ClassificationInfo[]; +} - export interface ClassificationInfo { - length: number; - classification: TokenClass; - } +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 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", +export const enum ScriptElementKind { + unknown = "", + warning = "warning", - /** predefined type (void) or keyword (class) */ - keyword = "keyword", + /** predefined type (void) or keyword (class) */ + keyword = "keyword", - /** top level script node */ - scriptElement = "script", + /** top level script node */ + scriptElement = "script", - /** module foo {} */ - moduleElement = "module", + /** module foo {} */ + moduleElement = "module", - /** class X {} */ - classElement = "class", + /** class X {} */ + classElement = "class", - /** var x = class X {} */ - localClassElement = "local class", + /** var x = class X {} */ + localClassElement = "local class", - /** interface Y {} */ - interfaceElement = "interface", + /** interface Y {} */ + interfaceElement = "interface", - /** type T = ... */ - typeElement = "type", + /** type T = ... */ + typeElement = "type", - /** enum E */ - enumElement = "enum", - enumMemberElement = "enum member", + /** enum E */ + enumElement = "enum", + enumMemberElement = "enum member", - /** - * Inside module and script only - * const v = .. - */ - variableElement = "var", + /** + * Inside module and script only + * const v = .. + */ + variableElement = "var", - /** Inside function */ - localVariableElement = "local var", + /** Inside function */ + localVariableElement = "local var", - /** - * Inside module and script only - * function f() { } - */ - functionElement = "function", + /** + * Inside module and script only + * function f() { } + */ + functionElement = "function", - /** Inside function */ - localFunctionElement = "local function", + /** Inside function */ + localFunctionElement = "local function", - /** class X { [public|private]* foo() {} } */ - memberFunctionElement = "method", + /** class X { [public|private]* foo() {} } */ + memberFunctionElement = "method", - /** class X { [public|private]* [get|set] foo:number; } */ - memberGetAccessorElement = "getter", - memberSetAccessorElement = "setter", + /** 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 { [public|private]* foo:number; } + * interface Y { foo:number; } + */ + memberVariableElement = "property", - /** - * class X { constructor() { } } - * class X { static { } } - */ - constructorImplementationElement = "constructor", + /** + * class X { constructor() { } } + * class X { static { } } + */ + constructorImplementationElement = "constructor", - /** interface Y { ():number; } */ - callSignatureElement = "call", + /** interface Y { ():number; } */ + callSignatureElement = "call", - /** interface Y { []:number; } */ - indexSignatureElement = "index", + /** interface Y { []:number; } */ + indexSignatureElement = "index", - /** interface Y { new():Y; } */ - constructSignatureElement = "construct", + /** interface Y { new():Y; } */ + constructSignatureElement = "construct", - /** function foo(*Y*: string) */ - parameterElement = "parameter", + /** function foo(*Y*: string) */ + parameterElement = "parameter", - typeParameterElement = "type parameter", + typeParameterElement = "type parameter", - primitiveType = "primitive type", + primitiveType = "primitive type", - label = "label", + label = "label", - alias = "alias", + alias = "alias", - constElement = "const", + constElement = "const", - letElement = "let", + letElement = "let", - directory = "directory", + directory = "directory", - externalModuleName = "external module name", + externalModuleName = "external module name", - /** - * - * @deprecated - */ - jsxAttribute = "JSX attribute", + /** + * + * @deprecated + */ + jsxAttribute = "JSX attribute", - /** String literal */ - string = "string", + /** String literal */ + string = "string", - /** Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" */ - link = "link", + /** Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" */ + link = "link", - /** Jsdoc @link: in `{@link C link text}`, the entity name "C" */ - linkName = "link name", + /** Jsdoc @link: in `{@link C link text}`, the entity name "C" */ + linkName = "link name", - /** Jsdoc @link: in `{@link C link text}`, the link text "link text" */ - linkText = "link text" - } + /** Jsdoc @link: in `{@link C link text}`, the link text "link text" */ + linkText = "link text" +} - export const enum ScriptElementKindModifier { - none = "", - publicMemberModifier = "public", - privateMemberModifier = "private", - protectedMemberModifier = "protected", - exportedModifier = "export", - ambientModifier = "declare", - staticModifier = "static", - abstractModifier = "abstract", - optionalModifier = "optional", - - deprecatedModifier = "deprecated", - - dtsModifier = ".d.ts", - tsModifier = ".ts", - tsxModifier = ".tsx", - jsModifier = ".js", - jsxModifier = ".jsx", - jsonModifier = ".json", - dmtsModifier = ".d.mts", - mtsModifier = ".mts", - mjsModifier = ".mjs", - dctsModifier = ".d.cts", - ctsModifier = ".cts", - cjsModifier = ".cjs" - } +export const enum ScriptElementKindModifier { + none = "", + publicMemberModifier = "public", + privateMemberModifier = "private", + protectedMemberModifier = "protected", + exportedModifier = "export", + ambientModifier = "declare", + staticModifier = "static", + abstractModifier = "abstract", + optionalModifier = "optional", + + deprecatedModifier = "deprecated", + + dtsModifier = ".d.ts", + tsModifier = ".ts", + tsxModifier = ".tsx", + jsModifier = ".js", + jsxModifier = ".jsx", + jsonModifier = ".json", + dmtsModifier = ".d.mts", + mtsModifier = ".mts", + mjsModifier = ".mjs", + dctsModifier = ".d.cts", + ctsModifier = ".cts", + cjsModifier = ".cjs" +} - 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 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 - } +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 CodeFixRegistration { + errorCodes: readonly number[]; + getCodeActions(context: CodeFixContext): CodeFixAction[] | undefined; + fixIds?: readonly string[]; + getAllCodeActions?(context: CodeFixAllContext): CombinedCodeActions; +} - /** @internal */ - export interface CodeFixContextBase extends ts.textChanges.TextChangesContext { - sourceFile: ts.SourceFile; - program: ts.Program; - cancellationToken: ts.CancellationToken; - preferences: ts.UserPreferences; - } +/** @internal */ +export interface CodeFixContextBase extends ts.textChanges.TextChangesContext { + sourceFile: ts.SourceFile; + program: ts.Program; + cancellationToken: ts.CancellationToken; + preferences: ts.UserPreferences; +} - /** @internal */ - export interface CodeFixAllContext extends CodeFixContextBase { - fixId: {}; - } +/** @internal */ +export interface CodeFixAllContext extends CodeFixContextBase { + fixId: {}; +} - /** @internal */ - export interface CodeFixContext extends CodeFixContextBase { - errorCode: number; - span: ts.TextSpan; - } +/** @internal */ +export interface CodeFixContext extends CodeFixContextBase { + errorCode: number; + span: ts.TextSpan; +} - /** @internal */ - export interface Refactor { - /** List of action kinds a refactor can provide. - * Used to skip unnecessary calculation when specific refactors are requested. */ - kinds?: string[]; +/** @internal */ +export interface Refactor { + /** List of action kinds a refactor can provide. + * Used to skip unnecessary calculation when specific refactors are requested. */ + kinds?: string[]; - /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; + /** Compute the associated code actions */ + getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; - /** Compute (quickly) which actions are available here */ - getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; - } + /** Compute (quickly) which actions are available here */ + getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; +} - /** @internal */ - export interface RefactorContext extends ts.textChanges.TextChangesContext { - file: ts.SourceFile; - startPosition: number; - endPosition?: number; - program: ts.Program; - cancellationToken?: ts.CancellationToken; - preferences: ts.UserPreferences; - triggerReason?: RefactorTriggerReason; - kind?: string; - } +/** @internal */ +export interface RefactorContext extends ts.textChanges.TextChangesContext { + file: ts.SourceFile; + startPosition: number; + endPosition?: number; + program: ts.Program; + cancellationToken?: ts.CancellationToken; + preferences: ts.UserPreferences; + triggerReason?: RefactorTriggerReason; + kind?: string; +} - export interface InlayHintsContext { - file: ts.SourceFile; - program: ts.Program; - cancellationToken: ts.CancellationToken; - host: LanguageServiceHost; - span: ts.TextSpan; - preferences: ts.UserPreferences; - } +export interface InlayHintsContext { + file: ts.SourceFile; + program: ts.Program; + cancellationToken: ts.CancellationToken; + host: LanguageServiceHost; + span: ts.TextSpan; + preferences: ts.UserPreferences; +} } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 688d643de8dc5..6e5b2be616fd3 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -10,3381 +10,3381 @@ 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: ts.Scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true); - - export const enum SemanticMeaning { - None = 0x0, - Value = 0x1, - Type = 0x2, - Namespace = 0x4, - All = Value | Type | Namespace - } +// These utilities are common to multiple language service features. +//#region +export const scanner: ts.Scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true); + +export const enum SemanticMeaning { + None = 0x0, + Value = 0x1, + Type = 0x2, + Namespace = 0x4, + All = Value | Type | Namespace +} - export function getMeaningFromDeclaration(node: ts.Node): SemanticMeaning { - switch (node.kind) { - case ts.SyntaxKind.VariableDeclaration: - return ts.isInJSFile(node) && ts.getJSDocEnumTag(node) ? SemanticMeaning.All : SemanticMeaning.Value; - case ts.SyntaxKind.Parameter: - case ts.SyntaxKind.BindingElement: - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.ShorthandPropertyAssignment: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.CatchClause: - case ts.SyntaxKind.JsxAttribute: - return SemanticMeaning.Value; +export function getMeaningFromDeclaration(node: ts.Node): SemanticMeaning { + switch (node.kind) { + case ts.SyntaxKind.VariableDeclaration: + return ts.isInJSFile(node) && ts.getJSDocEnumTag(node) ? SemanticMeaning.All : SemanticMeaning.Value; + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.CatchClause: + case ts.SyntaxKind.JsxAttribute: + return SemanticMeaning.Value; - case ts.SyntaxKind.TypeParameter: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.TypeLiteral: - return SemanticMeaning.Type; - - case ts.SyntaxKind.JSDocTypedefTag: - // If it has no name node, it shares the name with the value declaration below it. - return (node as ts.JSDocTypedefTag).name === undefined ? SemanticMeaning.Value | SemanticMeaning.Type : SemanticMeaning.Type; - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.ClassDeclaration: - return SemanticMeaning.Value | SemanticMeaning.Type; + case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.TypeLiteral: + return SemanticMeaning.Type; - case ts.SyntaxKind.ModuleDeclaration: - if (ts.isAmbientModule(node as ts.ModuleDeclaration)) { - return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - else if (ts.getModuleInstanceState(node as ts.ModuleDeclaration) === ts.ModuleInstanceState.Instantiated) { - return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - else { - return SemanticMeaning.Namespace; - } + case ts.SyntaxKind.JSDocTypedefTag: + // If it has no name node, it shares the name with the value declaration below it. + return (node as ts.JSDocTypedefTag).name === undefined ? SemanticMeaning.Value | SemanticMeaning.Type : SemanticMeaning.Type; + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.ClassDeclaration: + return SemanticMeaning.Value | SemanticMeaning.Type; - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportDeclaration: - case ts.SyntaxKind.ExportAssignment: - case ts.SyntaxKind.ExportDeclaration: - return SemanticMeaning.All; - - // An external module can be a Value - case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.ModuleDeclaration: + if (ts.isAmbientModule(node as ts.ModuleDeclaration)) { return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - - return SemanticMeaning.All; - } + } + else if (ts.getModuleInstanceState(node as ts.ModuleDeclaration) === ts.ModuleInstanceState.Instantiated) { + return SemanticMeaning.Namespace | SemanticMeaning.Value; + } + else { + return SemanticMeaning.Namespace; + } - export function getMeaningFromLocation(node: ts.Node): SemanticMeaning { - node = getAdjustedReferenceLocation(node); - const parent = node.parent; - if (node.kind === ts.SyntaxKind.SourceFile) { - return SemanticMeaning.Value; - } - else if (ts.isExportAssignment(parent) - || ts.isExportSpecifier(parent) - || ts.isExternalModuleReference(parent) - || ts.isImportSpecifier(parent) - || ts.isImportClause(parent) - || ts.isImportEqualsDeclaration(parent) && node === parent.name) { - return SemanticMeaning.All; - } - else if (isInRightSideOfInternalImportEqualsDeclaration(node)) { - return getMeaningFromRightHandSideOfImportEquals(node as ts.Identifier); - } - else if (ts.isDeclarationName(node)) { - return getMeaningFromDeclaration(parent); - } - else if (ts.isEntityName(node) && ts.findAncestor(node, ts.or(ts.isJSDocNameReference, ts.isJSDocLinkLike, ts.isJSDocMemberName))) { + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportAssignment: + case ts.SyntaxKind.ExportDeclaration: return SemanticMeaning.All; - } - else if (isTypeReference(node)) { - return SemanticMeaning.Type; - } - else if (isNamespaceReference(node)) { - return SemanticMeaning.Namespace; - } - else if (ts.isTypeParameterDeclaration(parent)) { - ts.Debug.assert(ts.isJSDocTemplateTag(parent.parent)); // Else would be handled by isDeclarationName - return SemanticMeaning.Type; - } - else if (ts.isLiteralTypeNode(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: ts.Node): SemanticMeaning { - // import a = |b|; // Namespace - // import a = |b.c|; // Value, type, namespace - // import a = |b.c|.d; // Namespace - const name = node.kind === ts.SyntaxKind.QualifiedName ? node : ts.isQualifiedName(node.parent) && node.parent.right === node ? node.parent : undefined; - return name && name.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration ? SemanticMeaning.All : SemanticMeaning.Namespace; + // An external module can be a Value + case ts.SyntaxKind.SourceFile: + return SemanticMeaning.Namespace | SemanticMeaning.Value; } - export function isInRightSideOfInternalImportEqualsDeclaration(node: ts.Node) { - while (node.parent.kind === ts.SyntaxKind.QualifiedName) { - node = node.parent; - } - return ts.isInternalModuleImportEqualsDeclaration(node.parent) && node.parent.moduleReference === node; - } + return SemanticMeaning.All; +} - function isNamespaceReference(node: ts.Node): boolean { - return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node); +export function getMeaningFromLocation(node: ts.Node): SemanticMeaning { + node = getAdjustedReferenceLocation(node); + const parent = node.parent; + if (node.kind === ts.SyntaxKind.SourceFile) { + return SemanticMeaning.Value; + } + else if (ts.isExportAssignment(parent) + || ts.isExportSpecifier(parent) + || ts.isExternalModuleReference(parent) + || ts.isImportSpecifier(parent) + || ts.isImportClause(parent) + || ts.isImportEqualsDeclaration(parent) && node === parent.name) { + return SemanticMeaning.All; } + else if (isInRightSideOfInternalImportEqualsDeclaration(node)) { + return getMeaningFromRightHandSideOfImportEquals(node as ts.Identifier); + } + else if (ts.isDeclarationName(node)) { + return getMeaningFromDeclaration(parent); + } + else if (ts.isEntityName(node) && ts.findAncestor(node, ts.or(ts.isJSDocNameReference, ts.isJSDocLinkLike, ts.isJSDocMemberName))) { + return SemanticMeaning.All; + } + else if (isTypeReference(node)) { + return SemanticMeaning.Type; + } + else if (isNamespaceReference(node)) { + return SemanticMeaning.Namespace; + } + else if (ts.isTypeParameterDeclaration(parent)) { + ts.Debug.assert(ts.isJSDocTemplateTag(parent.parent)); // Else would be handled by isDeclarationName + return SemanticMeaning.Type; + } + else if (ts.isLiteralTypeNode(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 isQualifiedNameNamespaceReference(node: ts.Node): boolean { - let root = node; - let isLastClause = true; - if (root.parent.kind === ts.SyntaxKind.QualifiedName) { - while (root.parent && root.parent.kind === ts.SyntaxKind.QualifiedName) { - root = root.parent; - } - - isLastClause = (root as ts.QualifiedName).right === node; - } +function getMeaningFromRightHandSideOfImportEquals(node: ts.Node): SemanticMeaning { + // import a = |b|; // Namespace + // import a = |b.c|; // Value, type, namespace + // import a = |b.c|.d; // Namespace + const name = node.kind === ts.SyntaxKind.QualifiedName ? node : ts.isQualifiedName(node.parent) && node.parent.right === node ? node.parent : undefined; + return name && name.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration ? SemanticMeaning.All : SemanticMeaning.Namespace; +} - return root.parent.kind === ts.SyntaxKind.TypeReference && !isLastClause; +export function isInRightSideOfInternalImportEqualsDeclaration(node: ts.Node) { + while (node.parent.kind === ts.SyntaxKind.QualifiedName) { + node = node.parent; } + return ts.isInternalModuleImportEqualsDeclaration(node.parent) && node.parent.moduleReference === node; +} - function isPropertyAccessNamespaceReference(node: ts.Node): boolean { - let root = node; - let isLastClause = true; - if (root.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { - while (root.parent && root.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { - root = root.parent; - } - - isLastClause = (root as ts.PropertyAccessExpression).name === node; - } +function isNamespaceReference(node: ts.Node): boolean { + return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node); +} - if (!isLastClause && root.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && root.parent.parent.kind === ts.SyntaxKind.HeritageClause) { - const decl = root.parent.parent.parent; - return (decl.kind === ts.SyntaxKind.ClassDeclaration && (root.parent.parent as ts.HeritageClause).token === ts.SyntaxKind.ImplementsKeyword) || - (decl.kind === ts.SyntaxKind.InterfaceDeclaration && (root.parent.parent as ts.HeritageClause).token === ts.SyntaxKind.ExtendsKeyword); +function isQualifiedNameNamespaceReference(node: ts.Node): boolean { + let root = node; + let isLastClause = true; + if (root.parent.kind === ts.SyntaxKind.QualifiedName) { + while (root.parent && root.parent.kind === ts.SyntaxKind.QualifiedName) { + root = root.parent; } - return false; + isLastClause = (root as ts.QualifiedName).right === node; } - function isTypeReference(node: ts.Node): boolean { - if (ts.isRightSideOfQualifiedNameOrPropertyAccess(node)) { - node = node.parent; - } - - switch (node.kind) { - case ts.SyntaxKind.ThisKeyword: - return !ts.isExpressionNode(node); - case ts.SyntaxKind.ThisType: - return true; - } + return root.parent.kind === ts.SyntaxKind.TypeReference && !isLastClause; +} - switch (node.parent.kind) { - case ts.SyntaxKind.TypeReference: - return true; - case ts.SyntaxKind.ImportType: - return !(node.parent as ts.ImportTypeNode).isTypeOf; - case ts.SyntaxKind.ExpressionWithTypeArguments: - return ts.isPartOfTypeNode(node.parent); +function isPropertyAccessNamespaceReference(node: ts.Node): boolean { + let root = node; + let isLastClause = true; + if (root.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + while (root.parent && root.parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + root = root.parent; } - return false; + isLastClause = (root as ts.PropertyAccessExpression).name === node; } - export function isCallExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isCallExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); + if (!isLastClause && root.parent.kind === ts.SyntaxKind.ExpressionWithTypeArguments && root.parent.parent.kind === ts.SyntaxKind.HeritageClause) { + const decl = root.parent.parent.parent; + return (decl.kind === ts.SyntaxKind.ClassDeclaration && (root.parent.parent as ts.HeritageClause).token === ts.SyntaxKind.ImplementsKeyword) || + (decl.kind === ts.SyntaxKind.InterfaceDeclaration && (root.parent.parent as ts.HeritageClause).token === ts.SyntaxKind.ExtendsKeyword); } - export function isNewExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); - } + return false; +} - export function isCallOrNewExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isCallOrNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +function isTypeReference(node: ts.Node): boolean { + if (ts.isRightSideOfQualifiedNameOrPropertyAccess(node)) { + node = node.parent; } - export function isTaggedTemplateTag(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isTaggedTemplateExpression, selectTagOfTaggedTemplateExpression, includeElementAccess, skipPastOuterExpressions); + switch (node.kind) { + case ts.SyntaxKind.ThisKeyword: + return !ts.isExpressionNode(node); + case ts.SyntaxKind.ThisType: + return true; } - export function isDecoratorTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isDecorator, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); + switch (node.parent.kind) { + case ts.SyntaxKind.TypeReference: + return true; + case ts.SyntaxKind.ImportType: + return !(node.parent as ts.ImportTypeNode).isTypeOf; + case ts.SyntaxKind.ExpressionWithTypeArguments: + return ts.isPartOfTypeNode(node.parent); } - export function isJsxOpeningLikeElementTagName(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, ts.isJsxOpeningLikeElement, selectTagNameOfJsxOpeningLikeElement, includeElementAccess, skipPastOuterExpressions); - } + return false; +} - function selectExpressionOfCallOrNewExpressionOrDecorator(node: ts.CallExpression | ts.NewExpression | ts.Decorator) { - return node.expression; - } +export function isCallExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isCallExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} - function selectTagOfTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { - return node.tag; - } +export function isNewExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} - function selectTagNameOfJsxOpeningLikeElement(node: ts.JsxOpeningLikeElement) { - return node.tagName; +export function isCallOrNewExpressionTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isCallOrNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} + +export function isTaggedTemplateTag(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isTaggedTemplateExpression, selectTagOfTaggedTemplateExpression, includeElementAccess, skipPastOuterExpressions); +} + +export function isDecoratorTarget(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isDecorator, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} + +export function isJsxOpeningLikeElementTagName(node: ts.Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, ts.isJsxOpeningLikeElement, selectTagNameOfJsxOpeningLikeElement, includeElementAccess, skipPastOuterExpressions); +} + +function selectExpressionOfCallOrNewExpressionOrDecorator(node: ts.CallExpression | ts.NewExpression | ts.Decorator) { + return node.expression; +} + +function selectTagOfTaggedTemplateExpression(node: ts.TaggedTemplateExpression) { + return node.tag; +} + +function selectTagNameOfJsxOpeningLikeElement(node: ts.JsxOpeningLikeElement) { + return node.tagName; +} + +function isCalleeWorker(node: ts.Node, pred: (node: ts.Node) => node is T, calleeSelector: (node: T) => ts.Expression, includeElementAccess: boolean, skipPastOuterExpressions: boolean) { + let target = includeElementAccess ? climbPastPropertyOrElementAccess(node) : climbPastPropertyAccess(node); + if (skipPastOuterExpressions) { + target = ts.skipOuterExpressions(target); } + return !!target && !!target.parent && pred(target.parent) && calleeSelector(target.parent) === target; +} + +export function climbPastPropertyAccess(node: ts.Node) { + return isRightSideOfPropertyAccess(node) ? node.parent : node; +} - function isCalleeWorker(node: ts.Node, pred: (node: ts.Node) => node is T, calleeSelector: (node: T) => ts.Expression, includeElementAccess: boolean, skipPastOuterExpressions: boolean) { - let target = includeElementAccess ? climbPastPropertyOrElementAccess(node) : climbPastPropertyAccess(node); - if (skipPastOuterExpressions) { - target = ts.skipOuterExpressions(target); +export function climbPastPropertyOrElementAccess(node: ts.Node) { + return isRightSideOfPropertyAccess(node) || isArgumentExpressionOfElementAccess(node) ? node.parent : node; +} + +export function getTargetLabel(referenceNode: ts.Node, labelName: string): ts.Identifier | undefined { + while (referenceNode) { + if (referenceNode.kind === ts.SyntaxKind.LabeledStatement && (referenceNode as ts.LabeledStatement).label.escapedText === labelName) { + return (referenceNode as ts.LabeledStatement).label; } - return !!target && !!target.parent && pred(target.parent) && calleeSelector(target.parent) === target; + referenceNode = referenceNode.parent; } + return undefined; +} - export function climbPastPropertyAccess(node: ts.Node) { - return isRightSideOfPropertyAccess(node) ? node.parent : node; +export function hasPropertyAccessExpressionWithName(node: ts.CallExpression, funcName: string): boolean { + if (!ts.isPropertyAccessExpression(node.expression)) { + return false; } - export function climbPastPropertyOrElementAccess(node: ts.Node) { - return isRightSideOfPropertyAccess(node) || isArgumentExpressionOfElementAccess(node) ? node.parent : node; - } + return node.expression.name.text === funcName; +} - export function getTargetLabel(referenceNode: ts.Node, labelName: string): ts.Identifier | undefined { - while (referenceNode) { - if (referenceNode.kind === ts.SyntaxKind.LabeledStatement && (referenceNode as ts.LabeledStatement).label.escapedText === labelName) { - return (referenceNode as ts.LabeledStatement).label; - } - referenceNode = referenceNode.parent; - } - return undefined; - } +export function isJumpStatementTarget(node: ts.Node): node is ts.Identifier & { + parent: ts.BreakOrContinueStatement; +} { + return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isBreakOrContinueStatement)?.label === node; +} - export function hasPropertyAccessExpressionWithName(node: ts.CallExpression, funcName: string): boolean { - if (!ts.isPropertyAccessExpression(node.expression)) { - return false; - } +export function isLabelOfLabeledStatement(node: ts.Node): node is ts.Identifier { + return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isLabeledStatement)?.label === node; +} - return node.expression.name.text === funcName; - } +export function isLabelName(node: ts.Node): boolean { + return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node); +} - export function isJumpStatementTarget(node: ts.Node): node is ts.Identifier & { - parent: ts.BreakOrContinueStatement; - } { - return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isBreakOrContinueStatement)?.label === node; - } +export function isTagName(node: ts.Node): boolean { + return ts.tryCast(node.parent, ts.isJSDocTag)?.tagName === node; +} - export function isLabelOfLabeledStatement(node: ts.Node): node is ts.Identifier { - return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isLabeledStatement)?.label === node; - } +export function isRightSideOfQualifiedName(node: ts.Node) { + return ts.tryCast(node.parent, ts.isQualifiedName)?.right === node; +} - export function isLabelName(node: ts.Node): boolean { - return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node); - } +export function isRightSideOfPropertyAccess(node: ts.Node) { + return ts.tryCast(node.parent, ts.isPropertyAccessExpression)?.name === node; +} - export function isTagName(node: ts.Node): boolean { - return ts.tryCast(node.parent, ts.isJSDocTag)?.tagName === node; - } +export function isArgumentExpressionOfElementAccess(node: ts.Node) { + return ts.tryCast(node.parent, ts.isElementAccessExpression)?.argumentExpression === node; +} - export function isRightSideOfQualifiedName(node: ts.Node) { - return ts.tryCast(node.parent, ts.isQualifiedName)?.right === node; - } +export function isNameOfModuleDeclaration(node: ts.Node) { + return ts.tryCast(node.parent, ts.isModuleDeclaration)?.name === node; +} - export function isRightSideOfPropertyAccess(node: ts.Node) { - return ts.tryCast(node.parent, ts.isPropertyAccessExpression)?.name === node; - } +export function isNameOfFunctionDeclaration(node: ts.Node): boolean { + return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isFunctionLike)?.name === node; +} - export function isArgumentExpressionOfElementAccess(node: ts.Node) { - return ts.tryCast(node.parent, ts.isElementAccessExpression)?.argumentExpression === node; +export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: ts.StringLiteral | ts.NumericLiteral | ts.NoSubstitutionTemplateLiteral): boolean { + switch (node.parent.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.PropertyAssignment: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.ModuleDeclaration: + return ts.getNameOfDeclaration(node.parent as ts.Declaration) === node; + case ts.SyntaxKind.ElementAccessExpression: + return (node.parent as ts.ElementAccessExpression).argumentExpression === node; + case ts.SyntaxKind.ComputedPropertyName: + return true; + case ts.SyntaxKind.LiteralType: + return node.parent.parent.kind === ts.SyntaxKind.IndexedAccessType; + default: + return false; } +} - export function isNameOfModuleDeclaration(node: ts.Node) { - return ts.tryCast(node.parent, ts.isModuleDeclaration)?.name === node; - } +export function isExpressionOfExternalModuleImportEqualsDeclaration(node: ts.Node) { + return ts.isExternalModuleImportEqualsDeclaration(node.parent.parent) && + ts.getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node; +} - export function isNameOfFunctionDeclaration(node: ts.Node): boolean { - return ts.isIdentifier(node) && ts.tryCast(node.parent, ts.isFunctionLike)?.name === node; +export function getContainerNode(node: ts.Node): ts.Declaration | undefined { + if (ts.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; } - export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: ts.StringLiteral | ts.NumericLiteral | ts.NoSubstitutionTemplateLiteral): boolean { - switch (node.parent.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.PropertyAssignment: - case ts.SyntaxKind.EnumMember: + while (true) { + node = node.parent; + if (!node) { + return undefined; + } + switch (node.kind) { + case ts.SyntaxKind.SourceFile: case ts.SyntaxKind.MethodDeclaration: case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: case ts.SyntaxKind.ModuleDeclaration: - return ts.getNameOfDeclaration(node.parent as ts.Declaration) === node; - case ts.SyntaxKind.ElementAccessExpression: - return (node.parent as ts.ElementAccessExpression).argumentExpression === node; - case ts.SyntaxKind.ComputedPropertyName: - return true; - case ts.SyntaxKind.LiteralType: - return node.parent.parent.kind === ts.SyntaxKind.IndexedAccessType; - default: - return false; + return node as ts.Declaration; } } - - export function isExpressionOfExternalModuleImportEqualsDeclaration(node: ts.Node) { - return ts.isExternalModuleImportEqualsDeclaration(node.parent.parent) && - ts.getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node; - } - - export function getContainerNode(node: ts.Node): ts.Declaration | undefined { - if (ts.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 ts.SyntaxKind.SourceFile: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - return node as ts.Declaration; +} +export function getNodeKind(node: ts.Node): ts.ScriptElementKind { + switch (node.kind) { + case ts.SyntaxKind.SourceFile: + return ts.isExternalModule(node as ts.SourceFile) ? ts.ScriptElementKind.moduleElement : ts.ScriptElementKind.scriptElement; + case ts.SyntaxKind.ModuleDeclaration: + return ts.ScriptElementKind.moduleElement; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return ts.ScriptElementKind.classElement; + case ts.SyntaxKind.InterfaceDeclaration: return ts.ScriptElementKind.interfaceElement; + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.JSDocCallbackTag: + case ts.SyntaxKind.JSDocTypedefTag: + return ts.ScriptElementKind.typeElement; + case ts.SyntaxKind.EnumDeclaration: return ts.ScriptElementKind.enumElement; + case ts.SyntaxKind.VariableDeclaration: + return getKindOfVariableDeclaration(node as ts.VariableDeclaration); + case ts.SyntaxKind.BindingElement: + return getKindOfVariableDeclaration(ts.getRootDeclaration(node) as ts.VariableDeclaration); + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + return ts.ScriptElementKind.functionElement; + case ts.SyntaxKind.GetAccessor: return ts.ScriptElementKind.memberGetAccessorElement; + case ts.SyntaxKind.SetAccessor: return ts.ScriptElementKind.memberSetAccessorElement; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return ts.ScriptElementKind.memberFunctionElement; + case ts.SyntaxKind.PropertyAssignment: + const { initializer } = node as ts.PropertyAssignment; + return ts.isFunctionLike(initializer) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.SpreadAssignment: + return ts.ScriptElementKind.memberVariableElement; + case ts.SyntaxKind.IndexSignature: return ts.ScriptElementKind.indexSignatureElement; + case ts.SyntaxKind.ConstructSignature: return ts.ScriptElementKind.constructSignatureElement; + case ts.SyntaxKind.CallSignature: return ts.ScriptElementKind.callSignatureElement; + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + return ts.ScriptElementKind.constructorImplementationElement; + case ts.SyntaxKind.TypeParameter: return ts.ScriptElementKind.typeParameterElement; + case ts.SyntaxKind.EnumMember: return ts.ScriptElementKind.enumMemberElement; + case ts.SyntaxKind.Parameter: return ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier) ? ts.ScriptElementKind.memberVariableElement : ts.ScriptElementKind.parameterElement; + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.ExportSpecifier: + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.NamespaceExport: + return ts.ScriptElementKind.alias; + case ts.SyntaxKind.BinaryExpression: + const kind = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); + const { right } = node as ts.BinaryExpression; + switch (kind) { + case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: + case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: + case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: + case ts.AssignmentDeclarationKind.None: + return ts.ScriptElementKind.unknown; + case ts.AssignmentDeclarationKind.ExportsProperty: + case ts.AssignmentDeclarationKind.ModuleExports: + const rightKind = getNodeKind(right); + return rightKind === ts.ScriptElementKind.unknown ? ts.ScriptElementKind.constElement : rightKind; + case ts.AssignmentDeclarationKind.PrototypeProperty: + return ts.isFunctionExpression(right) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; + case ts.AssignmentDeclarationKind.ThisProperty: + return ts.ScriptElementKind.memberVariableElement; // property + case ts.AssignmentDeclarationKind.Property: + // static method / property + return ts.isFunctionExpression(right) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; + case ts.AssignmentDeclarationKind.Prototype: + return ts.ScriptElementKind.localClassElement; + default: { + ts.assertType(kind); + return ts.ScriptElementKind.unknown; + } } - } + case ts.SyntaxKind.Identifier: + return ts.isImportClause(node.parent) ? ts.ScriptElementKind.alias : ts.ScriptElementKind.unknown; + case ts.SyntaxKind.ExportAssignment: + const scriptKind = getNodeKind((node as ts.ExportAssignment).expression); + // If the expression didn't come back with something (like it does for an identifiers) + return scriptKind === ts.ScriptElementKind.unknown ? ts.ScriptElementKind.constElement : scriptKind; + default: + return ts.ScriptElementKind.unknown; } - export function getNodeKind(node: ts.Node): ts.ScriptElementKind { - switch (node.kind) { - case ts.SyntaxKind.SourceFile: - return ts.isExternalModule(node as ts.SourceFile) ? ts.ScriptElementKind.moduleElement : ts.ScriptElementKind.scriptElement; - case ts.SyntaxKind.ModuleDeclaration: - return ts.ScriptElementKind.moduleElement; - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return ts.ScriptElementKind.classElement; - case ts.SyntaxKind.InterfaceDeclaration: return ts.ScriptElementKind.interfaceElement; - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.JSDocCallbackTag: - case ts.SyntaxKind.JSDocTypedefTag: - return ts.ScriptElementKind.typeElement; - case ts.SyntaxKind.EnumDeclaration: return ts.ScriptElementKind.enumElement; - case ts.SyntaxKind.VariableDeclaration: - return getKindOfVariableDeclaration(node as ts.VariableDeclaration); - case ts.SyntaxKind.BindingElement: - return getKindOfVariableDeclaration(ts.getRootDeclaration(node) as ts.VariableDeclaration); - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - return ts.ScriptElementKind.functionElement; - case ts.SyntaxKind.GetAccessor: return ts.ScriptElementKind.memberGetAccessorElement; - case ts.SyntaxKind.SetAccessor: return ts.ScriptElementKind.memberSetAccessorElement; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return ts.ScriptElementKind.memberFunctionElement; - case ts.SyntaxKind.PropertyAssignment: - const { initializer } = node as ts.PropertyAssignment; - return ts.isFunctionLike(initializer) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.ShorthandPropertyAssignment: - case ts.SyntaxKind.SpreadAssignment: - return ts.ScriptElementKind.memberVariableElement; - case ts.SyntaxKind.IndexSignature: return ts.ScriptElementKind.indexSignatureElement; - case ts.SyntaxKind.ConstructSignature: return ts.ScriptElementKind.constructSignatureElement; - case ts.SyntaxKind.CallSignature: return ts.ScriptElementKind.callSignatureElement; - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - return ts.ScriptElementKind.constructorImplementationElement; - case ts.SyntaxKind.TypeParameter: return ts.ScriptElementKind.typeParameterElement; - case ts.SyntaxKind.EnumMember: return ts.ScriptElementKind.enumMemberElement; - case ts.SyntaxKind.Parameter: return ts.hasSyntacticModifier(node, ts.ModifierFlags.ParameterPropertyModifier) ? ts.ScriptElementKind.memberVariableElement : ts.ScriptElementKind.parameterElement; - case ts.SyntaxKind.ImportEqualsDeclaration: - case ts.SyntaxKind.ImportSpecifier: - case ts.SyntaxKind.ExportSpecifier: - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.NamespaceExport: - return ts.ScriptElementKind.alias; - case ts.SyntaxKind.BinaryExpression: - const kind = ts.getAssignmentDeclarationKind(node as ts.BinaryExpression); - const { right } = node as ts.BinaryExpression; - switch (kind) { - case ts.AssignmentDeclarationKind.ObjectDefinePropertyValue: - case ts.AssignmentDeclarationKind.ObjectDefinePropertyExports: - case ts.AssignmentDeclarationKind.ObjectDefinePrototypeProperty: - case ts.AssignmentDeclarationKind.None: - return ts.ScriptElementKind.unknown; - case ts.AssignmentDeclarationKind.ExportsProperty: - case ts.AssignmentDeclarationKind.ModuleExports: - const rightKind = getNodeKind(right); - return rightKind === ts.ScriptElementKind.unknown ? ts.ScriptElementKind.constElement : rightKind; - case ts.AssignmentDeclarationKind.PrototypeProperty: - return ts.isFunctionExpression(right) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; - case ts.AssignmentDeclarationKind.ThisProperty: - return ts.ScriptElementKind.memberVariableElement; // property - case ts.AssignmentDeclarationKind.Property: - // static method / property - return ts.isFunctionExpression(right) ? ts.ScriptElementKind.memberFunctionElement : ts.ScriptElementKind.memberVariableElement; - case ts.AssignmentDeclarationKind.Prototype: - return ts.ScriptElementKind.localClassElement; - default: { - ts.assertType(kind); - return ts.ScriptElementKind.unknown; - } - } - case ts.SyntaxKind.Identifier: - return ts.isImportClause(node.parent) ? ts.ScriptElementKind.alias : ts.ScriptElementKind.unknown; - case ts.SyntaxKind.ExportAssignment: - const scriptKind = getNodeKind((node as ts.ExportAssignment).expression); - // If the expression didn't come back with something (like it does for an identifiers) - return scriptKind === ts.ScriptElementKind.unknown ? ts.ScriptElementKind.constElement : scriptKind; - default: - return ts.ScriptElementKind.unknown; - } - function getKindOfVariableDeclaration(v: ts.VariableDeclaration): ts.ScriptElementKind { - return ts.isVarConst(v) - ? ts.ScriptElementKind.constElement - : ts.isLet(v) - ? ts.ScriptElementKind.letElement - : ts.ScriptElementKind.variableElement; - } + function getKindOfVariableDeclaration(v: ts.VariableDeclaration): ts.ScriptElementKind { + return ts.isVarConst(v) + ? ts.ScriptElementKind.constElement + : ts.isLet(v) + ? ts.ScriptElementKind.letElement + : ts.ScriptElementKind.variableElement; } +} - export function isThis(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.ThisKeyword: - // case SyntaxKind.ThisType: TODO: GH#9267 - return true; - case ts.SyntaxKind.Identifier: - // 'this' as a parameter - return ts.identifierIsThisKeyword(node as ts.Identifier) && node.parent.kind === ts.SyntaxKind.Parameter; - default: - return false; - } +export function isThis(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ThisKeyword: + // case SyntaxKind.ThisType: TODO: GH#9267 + return true; + case ts.SyntaxKind.Identifier: + // 'this' as a parameter + return ts.identifierIsThisKeyword(node as ts.Identifier) && node.parent.kind === ts.SyntaxKind.Parameter; + default: + return false; } +} - // Matches the beginning of a triple slash directive - const tripleSlashDirectivePrefixRegex = /^\/\/\/\s*= range.end; - } +export function startEndContainsRange(start: number, end: number, range: ts.TextRange): boolean { + return start <= range.pos && end >= range.end; +} - export function rangeContainsStartEnd(range: ts.TextRange, start: number, end: number): boolean { - return range.pos <= start && range.end >= end; - } +export function rangeContainsStartEnd(range: ts.TextRange, start: number, end: number): boolean { + return range.pos <= start && range.end >= end; +} - export function rangeOverlapsWithStartEnd(r1: ts.TextRange, start: number, end: number) { - return startEndOverlapsWithStartEnd(r1.pos, r1.end, start, end); - } +export function rangeOverlapsWithStartEnd(r1: ts.TextRange, start: number, end: number) { + return startEndOverlapsWithStartEnd(r1.pos, r1.end, start, end); +} - export function nodeOverlapsWithStartEnd(node: ts.Node, sourceFile: ts.SourceFile, start: number, end: number) { - return startEndOverlapsWithStartEnd(node.getStart(sourceFile), node.end, start, end); - } +export function nodeOverlapsWithStartEnd(node: ts.Node, sourceFile: ts.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; - } +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: ts.Node, position: number, sourceFile: ts.SourceFile): boolean { - ts.Debug.assert(candidate.pos <= position); - return position < candidate.end || !isCompletedNode(candidate, sourceFile); +/** + * Assumes `candidate.start <= position` holds. + */ +export function positionBelongsToNode(candidate: ts.Node, position: number, sourceFile: ts.SourceFile): boolean { + ts.Debug.assert(candidate.pos <= position); + return position < candidate.end || !isCompletedNode(candidate, sourceFile); +} + +function isCompletedNode(n: ts.Node | undefined, sourceFile: ts.SourceFile): boolean { + if (n === undefined || ts.nodeIsMissing(n)) { + return false; } - function isCompletedNode(n: ts.Node | undefined, sourceFile: ts.SourceFile): boolean { - if (n === undefined || ts.nodeIsMissing(n)) { + switch (n.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.Block: + case ts.SyntaxKind.ModuleBlock: + case ts.SyntaxKind.CaseBlock: + case ts.SyntaxKind.NamedImports: + case ts.SyntaxKind.NamedExports: + return nodeEndsWith(n, ts.SyntaxKind.CloseBraceToken, sourceFile); + case ts.SyntaxKind.CatchClause: + return isCompletedNode((n as ts.CatchClause).block, sourceFile); + case ts.SyntaxKind.NewExpression: + if (!(n as ts.NewExpression).arguments) { + return true; + } + // falls through + + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.ParenthesizedExpression: + case ts.SyntaxKind.ParenthesizedType: + return nodeEndsWith(n, ts.SyntaxKind.CloseParenToken, sourceFile); + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + return isCompletedNode((n as ts.SignatureDeclaration).type, sourceFile); + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ArrowFunction: + if ((n as ts.FunctionLikeDeclaration).body) { + return isCompletedNode((n as ts.FunctionLikeDeclaration).body, sourceFile); + } + if ((n as ts.FunctionLikeDeclaration).type) { + return isCompletedNode((n as ts.FunctionLikeDeclaration).type, sourceFile); + } + + // Even though type parameters can be unclosed, we can get away with + // having at least a closing paren. + return hasChildOfKind(n, ts.SyntaxKind.CloseParenToken, sourceFile); + case ts.SyntaxKind.ModuleDeclaration: + return !!(n as ts.ModuleDeclaration).body && isCompletedNode((n as ts.ModuleDeclaration).body, sourceFile); + case ts.SyntaxKind.IfStatement: + if ((n as ts.IfStatement).elseStatement) { + return isCompletedNode((n as ts.IfStatement).elseStatement, sourceFile); + } + return isCompletedNode((n as ts.IfStatement).thenStatement, sourceFile); + case ts.SyntaxKind.ExpressionStatement: + return isCompletedNode((n as ts.ExpressionStatement).expression, sourceFile) || + hasChildOfKind(n, ts.SyntaxKind.SemicolonToken, sourceFile); + case ts.SyntaxKind.ArrayLiteralExpression: + case ts.SyntaxKind.ArrayBindingPattern: + case ts.SyntaxKind.ElementAccessExpression: + case ts.SyntaxKind.ComputedPropertyName: + case ts.SyntaxKind.TupleType: + return nodeEndsWith(n, ts.SyntaxKind.CloseBracketToken, sourceFile); + case ts.SyntaxKind.IndexSignature: + if ((n as ts.IndexSignatureDeclaration).type) { + return isCompletedNode((n as ts.IndexSignatureDeclaration).type, sourceFile); + } + return hasChildOfKind(n, ts.SyntaxKind.CloseBracketToken, sourceFile); + case ts.SyntaxKind.CaseClause: + case ts.SyntaxKind.DefaultClause: + // there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed return false; + + case ts.SyntaxKind.ForStatement: + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: + case ts.SyntaxKind.WhileStatement: + return isCompletedNode((n as ts.IterationStatement).statement, sourceFile); + case ts.SyntaxKind.DoStatement: + // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; + return hasChildOfKind(n, ts.SyntaxKind.WhileKeyword, sourceFile) + ? nodeEndsWith(n, ts.SyntaxKind.CloseParenToken, sourceFile) + : isCompletedNode((n as ts.DoStatement).statement, sourceFile); + case ts.SyntaxKind.TypeQuery: + return isCompletedNode((n as ts.TypeQueryNode).exprName, sourceFile); + case ts.SyntaxKind.TypeOfExpression: + case ts.SyntaxKind.DeleteExpression: + case ts.SyntaxKind.VoidExpression: + case ts.SyntaxKind.YieldExpression: + case ts.SyntaxKind.SpreadElement: + const unaryWordExpression = n as (ts.TypeOfExpression | ts.DeleteExpression | ts.VoidExpression | ts.YieldExpression | ts.SpreadElement); + return isCompletedNode(unaryWordExpression.expression, sourceFile); + + case ts.SyntaxKind.TaggedTemplateExpression: + return isCompletedNode((n as ts.TaggedTemplateExpression).template, sourceFile); + case ts.SyntaxKind.TemplateExpression: + const lastSpan = ts.lastOrUndefined((n as ts.TemplateExpression).templateSpans); + return isCompletedNode(lastSpan, sourceFile); + case ts.SyntaxKind.TemplateSpan: + return ts.nodeIsPresent((n as ts.TemplateSpan).literal); + case ts.SyntaxKind.ExportDeclaration: + case ts.SyntaxKind.ImportDeclaration: + return ts.nodeIsPresent((n as ts.ExportDeclaration | ts.ImportDeclaration).moduleSpecifier); + case ts.SyntaxKind.PrefixUnaryExpression: + return isCompletedNode((n as ts.PrefixUnaryExpression).operand, sourceFile); + case ts.SyntaxKind.BinaryExpression: + return isCompletedNode((n as ts.BinaryExpression).right, sourceFile); + case ts.SyntaxKind.ConditionalExpression: + return isCompletedNode((n as ts.ConditionalExpression).whenFalse, sourceFile); + + default: + return true; + } +} + +/* + * Checks if node ends with 'expectedLastToken'. + * If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'. + */ +function nodeEndsWith(n: ts.Node, expectedLastToken: ts.SyntaxKind, sourceFile: ts.SourceFile): boolean { + const children = n.getChildren(sourceFile); + if (children.length) { + const lastChild = ts.last(children); + if (lastChild.kind === expectedLastToken) { + return true; + } + else if (lastChild.kind === ts.SyntaxKind.SemicolonToken && children.length !== 1) { + return children[children.length - 2].kind === expectedLastToken; } + } + return false; +} - switch (n.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ObjectBindingPattern: - case ts.SyntaxKind.TypeLiteral: - case ts.SyntaxKind.Block: - case ts.SyntaxKind.ModuleBlock: - case ts.SyntaxKind.CaseBlock: - case ts.SyntaxKind.NamedImports: - case ts.SyntaxKind.NamedExports: - return nodeEndsWith(n, ts.SyntaxKind.CloseBraceToken, sourceFile); - case ts.SyntaxKind.CatchClause: - return isCompletedNode((n as ts.CatchClause).block, sourceFile); - case ts.SyntaxKind.NewExpression: - if (!(n as ts.NewExpression).arguments) { - return true; - } - // falls through +export function findListItemInfo(node: ts.Node): ListItemInfo | undefined { + const list = findContainingList(node); - case ts.SyntaxKind.CallExpression: - case ts.SyntaxKind.ParenthesizedExpression: - case ts.SyntaxKind.ParenthesizedType: - return nodeEndsWith(n, ts.SyntaxKind.CloseParenToken, sourceFile); - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - return isCompletedNode((n as ts.SignatureDeclaration).type, sourceFile); - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ArrowFunction: - if ((n as ts.FunctionLikeDeclaration).body) { - return isCompletedNode((n as ts.FunctionLikeDeclaration).body, sourceFile); - } - if ((n as ts.FunctionLikeDeclaration).type) { - return isCompletedNode((n as ts.FunctionLikeDeclaration).type, sourceFile); - } + // 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; + } - // Even though type parameters can be unclosed, we can get away with - // having at least a closing paren. - return hasChildOfKind(n, ts.SyntaxKind.CloseParenToken, sourceFile); - case ts.SyntaxKind.ModuleDeclaration: - return !!(n as ts.ModuleDeclaration).body && isCompletedNode((n as ts.ModuleDeclaration).body, sourceFile); - case ts.SyntaxKind.IfStatement: - if ((n as ts.IfStatement).elseStatement) { - return isCompletedNode((n as ts.IfStatement).elseStatement, sourceFile); - } - return isCompletedNode((n as ts.IfStatement).thenStatement, sourceFile); - case ts.SyntaxKind.ExpressionStatement: - return isCompletedNode((n as ts.ExpressionStatement).expression, sourceFile) || - hasChildOfKind(n, ts.SyntaxKind.SemicolonToken, sourceFile); - case ts.SyntaxKind.ArrayLiteralExpression: - case ts.SyntaxKind.ArrayBindingPattern: - case ts.SyntaxKind.ElementAccessExpression: - case ts.SyntaxKind.ComputedPropertyName: - case ts.SyntaxKind.TupleType: - return nodeEndsWith(n, ts.SyntaxKind.CloseBracketToken, sourceFile); - case ts.SyntaxKind.IndexSignature: - if ((n as ts.IndexSignatureDeclaration).type) { - return isCompletedNode((n as ts.IndexSignatureDeclaration).type, sourceFile); - } - return hasChildOfKind(n, ts.SyntaxKind.CloseBracketToken, sourceFile); - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: - // there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed - return false; + const children = list.getChildren(); + const listItemIndex = ts.indexOfNode(children, node); - case ts.SyntaxKind.ForStatement: - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: - case ts.SyntaxKind.WhileStatement: - return isCompletedNode((n as ts.IterationStatement).statement, sourceFile); - case ts.SyntaxKind.DoStatement: - // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; - return hasChildOfKind(n, ts.SyntaxKind.WhileKeyword, sourceFile) - ? nodeEndsWith(n, ts.SyntaxKind.CloseParenToken, sourceFile) - : isCompletedNode((n as ts.DoStatement).statement, sourceFile); - case ts.SyntaxKind.TypeQuery: - return isCompletedNode((n as ts.TypeQueryNode).exprName, sourceFile); - case ts.SyntaxKind.TypeOfExpression: - case ts.SyntaxKind.DeleteExpression: - case ts.SyntaxKind.VoidExpression: - case ts.SyntaxKind.YieldExpression: - case ts.SyntaxKind.SpreadElement: - const unaryWordExpression = n as (ts.TypeOfExpression | ts.DeleteExpression | ts.VoidExpression | ts.YieldExpression | ts.SpreadElement); - return isCompletedNode(unaryWordExpression.expression, sourceFile); - - case ts.SyntaxKind.TaggedTemplateExpression: - return isCompletedNode((n as ts.TaggedTemplateExpression).template, sourceFile); - case ts.SyntaxKind.TemplateExpression: - const lastSpan = ts.lastOrUndefined((n as ts.TemplateExpression).templateSpans); - return isCompletedNode(lastSpan, sourceFile); - case ts.SyntaxKind.TemplateSpan: - return ts.nodeIsPresent((n as ts.TemplateSpan).literal); - case ts.SyntaxKind.ExportDeclaration: - case ts.SyntaxKind.ImportDeclaration: - return ts.nodeIsPresent((n as ts.ExportDeclaration | ts.ImportDeclaration).moduleSpecifier); - case ts.SyntaxKind.PrefixUnaryExpression: - return isCompletedNode((n as ts.PrefixUnaryExpression).operand, sourceFile); - case ts.SyntaxKind.BinaryExpression: - return isCompletedNode((n as ts.BinaryExpression).right, sourceFile); - case ts.SyntaxKind.ConditionalExpression: - return isCompletedNode((n as ts.ConditionalExpression).whenFalse, sourceFile); + return { + listItemIndex, + list + }; +} - default: - return true; - } - } +export function hasChildOfKind(n: ts.Node, kind: ts.SyntaxKind, sourceFile: ts.SourceFile): boolean { + return !!findChildOfKind(n, kind, sourceFile); +} - /* - * 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: ts.Node, expectedLastToken: ts.SyntaxKind, sourceFile: ts.SourceFile): boolean { - const children = n.getChildren(sourceFile); - if (children.length) { - const lastChild = ts.last(children); - if (lastChild.kind === expectedLastToken) { - return true; - } - else if (lastChild.kind === ts.SyntaxKind.SemicolonToken && children.length !== 1) { - return children[children.length - 2].kind === expectedLastToken; - } - } - return false; - } +export function findChildOfKind(n: ts.Node, kind: T["kind"], sourceFile: ts.SourceFileLike): T | undefined { + return ts.find(n.getChildren(sourceFile), (c): c is T => c.kind === kind); +} - export function findListItemInfo(node: ts.Node): ListItemInfo | undefined { - const list = findContainingList(node); +export function findContainingList(node: ts.Node): ts.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 = ts.find(node.parent.getChildren(), (c): c is ts.SyntaxList => ts.isSyntaxList(c) && rangeContainsRange(c, node)); + // Either we didn't find an appropriate list, or the list must contain us. + ts.Debug.assert(!syntaxList || ts.contains(syntaxList.getChildren(), node)); + return syntaxList; +} - // 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; - } +function isDefaultModifier(node: ts.Node) { + return node.kind === ts.SyntaxKind.DefaultKeyword; +} - const children = list.getChildren(); - const listItemIndex = ts.indexOfNode(children, node); +function isClassKeyword(node: ts.Node) { + return node.kind === ts.SyntaxKind.ClassKeyword; +} - return { - listItemIndex, - list - }; - } +function isFunctionKeyword(node: ts.Node) { + return node.kind === ts.SyntaxKind.FunctionKeyword; +} - export function hasChildOfKind(n: ts.Node, kind: ts.SyntaxKind, sourceFile: ts.SourceFile): boolean { - return !!findChildOfKind(n, kind, sourceFile); +function getAdjustedLocationForClass(node: ts.ClassDeclaration | ts.ClassExpression) { + if (ts.isNamedDeclaration(node)) { + return node.name; } - - export function findChildOfKind(n: ts.Node, kind: T["kind"], sourceFile: ts.SourceFileLike): T | undefined { - return ts.find(n.getChildren(sourceFile), (c): c is T => c.kind === kind); + if (ts.isClassDeclaration(node)) { + // for class and function declarations, use the `default` modifier + // when the declaration is unnamed. + const defaultModifier = node.modifiers && ts.find(node.modifiers, isDefaultModifier); + if (defaultModifier) + return defaultModifier; } - - export function findContainingList(node: ts.Node): ts.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 = ts.find(node.parent.getChildren(), (c): c is ts.SyntaxList => ts.isSyntaxList(c) && rangeContainsRange(c, node)); - // Either we didn't find an appropriate list, or the list must contain us. - ts.Debug.assert(!syntaxList || ts.contains(syntaxList.getChildren(), node)); - return syntaxList; + if (ts.isClassExpression(node)) { + // for class expressions, use the `class` keyword when the class is unnamed + const classKeyword = ts.find(node.getChildren(), isClassKeyword); + if (classKeyword) + return classKeyword; } +} - function isDefaultModifier(node: ts.Node) { - return node.kind === ts.SyntaxKind.DefaultKeyword; +function getAdjustedLocationForFunction(node: ts.FunctionDeclaration | ts.FunctionExpression) { + if (ts.isNamedDeclaration(node)) { + return node.name; } - - function isClassKeyword(node: ts.Node) { - return node.kind === ts.SyntaxKind.ClassKeyword; + if (ts.isFunctionDeclaration(node)) { + // for class and function declarations, use the `default` modifier + // when the declaration is unnamed. + const defaultModifier = ts.find(node.modifiers!, isDefaultModifier); + if (defaultModifier) + return defaultModifier; } - - function isFunctionKeyword(node: ts.Node) { - return node.kind === ts.SyntaxKind.FunctionKeyword; + if (ts.isFunctionExpression(node)) { + // for function expressions, use the `function` keyword when the function is unnamed + const functionKeyword = ts.find(node.getChildren(), isFunctionKeyword); + if (functionKeyword) + return functionKeyword; } +} - function getAdjustedLocationForClass(node: ts.ClassDeclaration | ts.ClassExpression) { - if (ts.isNamedDeclaration(node)) { - return node.name; - } - if (ts.isClassDeclaration(node)) { - // for class and function declarations, use the `default` modifier - // when the declaration is unnamed. - const defaultModifier = node.modifiers && ts.find(node.modifiers, isDefaultModifier); - if (defaultModifier) - return defaultModifier; +function getAncestorTypeNode(node: ts.Node) { + let lastTypeNode: ts.TypeNode | undefined; + ts.findAncestor(node, a => { + if (ts.isTypeNode(a)) { + lastTypeNode = a; } - if (ts.isClassExpression(node)) { - // for class expressions, use the `class` keyword when the class is unnamed - const classKeyword = ts.find(node.getChildren(), isClassKeyword); - if (classKeyword) - return classKeyword; - } - } + return !ts.isQualifiedName(a.parent) && !ts.isTypeNode(a.parent) && !ts.isTypeElement(a.parent); + }); + return lastTypeNode; +} - function getAdjustedLocationForFunction(node: ts.FunctionDeclaration | ts.FunctionExpression) { - if (ts.isNamedDeclaration(node)) { - return node.name; - } - if (ts.isFunctionDeclaration(node)) { - // for class and function declarations, use the `default` modifier - // when the declaration is unnamed. - const defaultModifier = ts.find(node.modifiers!, isDefaultModifier); - if (defaultModifier) - return defaultModifier; - } - if (ts.isFunctionExpression(node)) { - // for function expressions, use the `function` keyword when the function is unnamed - const functionKeyword = ts.find(node.getChildren(), isFunctionKeyword); - if (functionKeyword) - return functionKeyword; +export function getContextualTypeFromParentOrAncestorTypeNode(node: ts.Expression, checker: ts.TypeChecker): ts.Type | undefined { + const contextualType = getContextualTypeFromParent(node, checker); + if (contextualType) + return contextualType; + + const ancestorTypeNode = getAncestorTypeNode(node); + return ancestorTypeNode && checker.getTypeAtLocation(ancestorTypeNode); +} + +function getAdjustedLocationForDeclaration(node: ts.Node, forRename: boolean) { + if (!forRename) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + return getAdjustedLocationForClass(node as ts.ClassDeclaration | ts.ClassExpression); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + return getAdjustedLocationForFunction(node as ts.FunctionDeclaration | ts.FunctionExpression); + case ts.SyntaxKind.Constructor: + return node; } } - - function getAncestorTypeNode(node: ts.Node) { - let lastTypeNode: ts.TypeNode | undefined; - ts.findAncestor(node, a => { - if (ts.isTypeNode(a)) { - lastTypeNode = a; - } - return !ts.isQualifiedName(a.parent) && !ts.isTypeNode(a.parent) && !ts.isTypeElement(a.parent); - }); - return lastTypeNode; + if (ts.isNamedDeclaration(node)) { + return node.name; } +} - export function getContextualTypeFromParentOrAncestorTypeNode(node: ts.Expression, checker: ts.TypeChecker): ts.Type | undefined { - const contextualType = getContextualTypeFromParent(node, checker); - if (contextualType) - return contextualType; +function getAdjustedLocationForImportDeclaration(node: ts.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; + } - const ancestorTypeNode = getAncestorTypeNode(node); - return ancestorTypeNode && checker.getTypeAtLocation(ancestorTypeNode); - } + // /**/import [|name|] from ...; + // import /**/type [|name|] from ...; + if (node.importClause.name) { + return node.importClause.name; + } - function getAdjustedLocationForDeclaration(node: ts.Node, forRename: boolean) { - if (!forRename) { - switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ClassExpression: - return getAdjustedLocationForClass(node as ts.ClassDeclaration | ts.ClassExpression); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - return getAdjustedLocationForFunction(node as ts.FunctionDeclaration | ts.FunctionExpression); - case ts.SyntaxKind.Constructor: - return node; + // /**/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 (ts.isNamedImports(node.importClause.namedBindings)) { + // do nothing if there is more than one binding + const onlyBinding = ts.singleOrUndefined(node.importClause.namedBindings.elements); + if (!onlyBinding) { + return; + } + return onlyBinding.name; + } + else if (ts.isNamespaceImport(node.importClause.namedBindings)) { + return node.importClause.namedBindings.name; } } - if (ts.isNamedDeclaration(node)) { - return node.name; - } } + if (!forRename) { + // /**/import "[|module|]"; + // /**/import ... from "[|module|]"; + // import /**/type ... from "[|module|]"; + return node.moduleSpecifier; + } +} - function getAdjustedLocationForImportDeclaration(node: ts.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 +function getAdjustedLocationForExportDeclaration(node: ts.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 (ts.isNamedExports(node.exportClause)) { + // do nothing if there is more than one binding + const onlyBinding = ts.singleOrUndefined(node.exportClause.elements); + if (!onlyBinding) { 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 (ts.isNamedImports(node.importClause.namedBindings)) { - // do nothing if there is more than one binding - const onlyBinding = ts.singleOrUndefined(node.importClause.namedBindings.elements); - if (!onlyBinding) { - return; - } - return onlyBinding.name; - } - else if (ts.isNamespaceImport(node.importClause.namedBindings)) { - return node.importClause.namedBindings.name; - } - } + return node.exportClause.elements[0].name; } - if (!forRename) { - // /**/import "[|module|]"; - // /**/import ... from "[|module|]"; - // import /**/type ... from "[|module|]"; - return node.moduleSpecifier; + else if (ts.isNamespaceExport(node.exportClause)) { + return node.exportClause.name; } } + if (!forRename) { + // /**/export * from "[|module|]"; + // export /**/type * from "[|module|]"; + return node.moduleSpecifier; + } +} - function getAdjustedLocationForExportDeclaration(node: ts.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 (ts.isNamedExports(node.exportClause)) { - // do nothing if there is more than one binding - const onlyBinding = ts.singleOrUndefined(node.exportClause.elements); - if (!onlyBinding) { - return; - } - return node.exportClause.elements[0].name; - } - else if (ts.isNamespaceExport(node.exportClause)) { - return node.exportClause.name; +function getAdjustedLocationForHeritageClause(node: ts.HeritageClause) { + // /**/extends [|name|] + // /**/implements [|name|] + if (node.types.length === 1) { + return node.types[0].expression; + } + + // /**/extends name1, name2 ... + // /**/implements name1, name2 ... +} + +function getAdjustedLocation(node: ts.Node, forRename: boolean): ts.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 (ts.isModifier(node) && (forRename || node.kind !== ts.SyntaxKind.DefaultKeyword) ? ts.contains(parent.modifiers, node) : + node.kind === ts.SyntaxKind.ClassKeyword ? ts.isClassDeclaration(parent) || ts.isClassExpression(node) : + node.kind === ts.SyntaxKind.FunctionKeyword ? ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(node) : + node.kind === ts.SyntaxKind.InterfaceKeyword ? ts.isInterfaceDeclaration(parent) : + node.kind === ts.SyntaxKind.EnumKeyword ? ts.isEnumDeclaration(parent) : + node.kind === ts.SyntaxKind.TypeKeyword ? ts.isTypeAliasDeclaration(parent) : + node.kind === ts.SyntaxKind.NamespaceKeyword || node.kind === ts.SyntaxKind.ModuleKeyword ? ts.isModuleDeclaration(parent) : + node.kind === ts.SyntaxKind.ImportKeyword ? ts.isImportEqualsDeclaration(parent) : + node.kind === ts.SyntaxKind.GetKeyword ? ts.isGetAccessorDeclaration(parent) : + node.kind === ts.SyntaxKind.SetKeyword && ts.isSetAccessorDeclaration(parent)) { + const location = getAdjustedLocationForDeclaration(parent, forRename); + if (location) { + return location; + } + } + // /**/ [|name|] ... + if ((node.kind === ts.SyntaxKind.VarKeyword || node.kind === ts.SyntaxKind.ConstKeyword || node.kind === ts.SyntaxKind.LetKeyword) && + ts.isVariableDeclarationList(parent) && parent.declarations.length === 1) { + const decl = parent.declarations[0]; + if (ts.isIdentifier(decl.name)) { + return decl.name; + } + } + if (node.kind === ts.SyntaxKind.TypeKeyword) { + // import /**/type [|name|] from ...; + // import /**/type { [|name|] } from ...; + // import /**/type { propertyName as [|name|] } from ...; + // import /**/type ... from "[|module|]"; + if (ts.isImportClause(parent) && parent.isTypeOnly) { + const location = getAdjustedLocationForImportDeclaration(parent.parent, forRename); + if (location) { + return location; } } - if (!forRename) { - // /**/export * from "[|module|]"; - // export /**/type * from "[|module|]"; - return node.moduleSpecifier; - } - } - - function getAdjustedLocationForHeritageClause(node: ts.HeritageClause) { - // /**/extends [|name|] - // /**/implements [|name|] - if (node.types.length === 1) { - return node.types[0].expression; - } - - // /**/extends name1, name2 ... - // /**/implements name1, name2 ... - } - - function getAdjustedLocation(node: ts.Node, forRename: boolean): ts.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 (ts.isModifier(node) && (forRename || node.kind !== ts.SyntaxKind.DefaultKeyword) ? ts.contains(parent.modifiers, node) : - node.kind === ts.SyntaxKind.ClassKeyword ? ts.isClassDeclaration(parent) || ts.isClassExpression(node) : - node.kind === ts.SyntaxKind.FunctionKeyword ? ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(node) : - node.kind === ts.SyntaxKind.InterfaceKeyword ? ts.isInterfaceDeclaration(parent) : - node.kind === ts.SyntaxKind.EnumKeyword ? ts.isEnumDeclaration(parent) : - node.kind === ts.SyntaxKind.TypeKeyword ? ts.isTypeAliasDeclaration(parent) : - node.kind === ts.SyntaxKind.NamespaceKeyword || node.kind === ts.SyntaxKind.ModuleKeyword ? ts.isModuleDeclaration(parent) : - node.kind === ts.SyntaxKind.ImportKeyword ? ts.isImportEqualsDeclaration(parent) : - node.kind === ts.SyntaxKind.GetKeyword ? ts.isGetAccessorDeclaration(parent) : - node.kind === ts.SyntaxKind.SetKeyword && ts.isSetAccessorDeclaration(parent)) { - const location = getAdjustedLocationForDeclaration(parent, forRename); + // export /**/type { [|name|] } from ...; + // export /**/type { propertyName as [|name|] } from ...; + // export /**/type * from "[|module|]"; + // export /**/type * as ... from "[|module|]"; + if (ts.isExportDeclaration(parent) && parent.isTypeOnly) { + const location = getAdjustedLocationForExportDeclaration(parent, forRename); if (location) { return location; } } - // /**/ [|name|] ... - if ((node.kind === ts.SyntaxKind.VarKeyword || node.kind === ts.SyntaxKind.ConstKeyword || node.kind === ts.SyntaxKind.LetKeyword) && - ts.isVariableDeclarationList(parent) && parent.declarations.length === 1) { - const decl = parent.declarations[0]; - if (ts.isIdentifier(decl.name)) { - return decl.name; - } + } + // import { propertyName /**/as [|name|] } ... + // import * /**/as [|name|] ... + // export { propertyName /**/as [|name|] } ... + // export * /**/as [|name|] ... + if (node.kind === ts.SyntaxKind.AsKeyword) { + if (ts.isImportSpecifier(parent) && parent.propertyName || + ts.isExportSpecifier(parent) && parent.propertyName || + ts.isNamespaceImport(parent) || + ts.isNamespaceExport(parent)) { + return parent.name; } - if (node.kind === ts.SyntaxKind.TypeKeyword) { - // import /**/type [|name|] from ...; - // import /**/type { [|name|] } from ...; - // import /**/type { propertyName as [|name|] } from ...; - // import /**/type ... from "[|module|]"; - if (ts.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 (ts.isExportDeclaration(parent) && parent.isTypeOnly) { - const location = getAdjustedLocationForExportDeclaration(parent, forRename); - if (location) { - return location; - } - } + if (ts.isExportDeclaration(parent) && parent.exportClause && ts.isNamespaceExport(parent.exportClause)) { + return parent.exportClause.name; } - // import { propertyName /**/as [|name|] } ... - // import * /**/as [|name|] ... - // export { propertyName /**/as [|name|] } ... - // export * /**/as [|name|] ... - if (node.kind === ts.SyntaxKind.AsKeyword) { - if (ts.isImportSpecifier(parent) && parent.propertyName || - ts.isExportSpecifier(parent) && parent.propertyName || - ts.isNamespaceImport(parent) || - ts.isNamespaceExport(parent)) { - return parent.name; - } - if (ts.isExportDeclaration(parent) && parent.exportClause && ts.isNamespaceExport(parent.exportClause)) { - return parent.exportClause.name; - } + } + // /**/import [|name|] from ...; + // /**/import { [|name|] } from ...; + // /**/import { propertyName as [|name|] } from ...; + // /**/import ... from "[|module|]"; + // /**/import "[|module|]"; + if (node.kind === ts.SyntaxKind.ImportKeyword && ts.isImportDeclaration(parent)) { + const location = getAdjustedLocationForImportDeclaration(parent, forRename); + if (location) { + return location; } - // /**/import [|name|] from ...; - // /**/import { [|name|] } from ...; - // /**/import { propertyName as [|name|] } from ...; - // /**/import ... from "[|module|]"; - // /**/import "[|module|]"; - if (node.kind === ts.SyntaxKind.ImportKeyword && ts.isImportDeclaration(parent)) { - const location = getAdjustedLocationForImportDeclaration(parent, forRename); + } + if (node.kind === ts.SyntaxKind.ExportKeyword) { + // /**/export { [|name|] } ...; + // /**/export { propertyName as [|name|] } ...; + // /**/export * from "[|module|]"; + // /**/export * as ... from "[|module|]"; + if (ts.isExportDeclaration(parent)) { + const location = getAdjustedLocationForExportDeclaration(parent, forRename); if (location) { return location; } } - if (node.kind === ts.SyntaxKind.ExportKeyword) { - // /**/export { [|name|] } ...; - // /**/export { propertyName as [|name|] } ...; - // /**/export * from "[|module|]"; - // /**/export * as ... from "[|module|]"; - if (ts.isExportDeclaration(parent)) { - const location = getAdjustedLocationForExportDeclaration(parent, forRename); - if (location) { - return location; - } - } - // NOTE: We don't adjust the location of the `default` keyword as that is handled specially by `getSymbolAtLocation`. - // /**/export default [|name|]; - // /**/export = [|name|]; - if (ts.isExportAssignment(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 (ts.isExportAssignment(parent)) { + return ts.skipOuterExpressions(parent.expression); + } + } + // import name = /**/require("[|module|]"); + if (node.kind === ts.SyntaxKind.RequireKeyword && ts.isExternalModuleReference(parent)) { + return parent.expression; + } + // import ... /**/from "[|module|]"; + // export ... /**/from "[|module|]"; + if (node.kind === ts.SyntaxKind.FromKeyword && (ts.isImportDeclaration(parent) || ts.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 === ts.SyntaxKind.ExtendsKeyword || node.kind === ts.SyntaxKind.ImplementsKeyword) && ts.isHeritageClause(parent) && parent.token === node.kind) { + const location = getAdjustedLocationForHeritageClause(parent); + if (location) { + return location; + } + } + if (node.kind === ts.SyntaxKind.ExtendsKeyword) { + // ... ... + if (ts.isTypeParameterDeclaration(parent) && parent.constraint && ts.isTypeReferenceNode(parent.constraint)) { + return parent.constraint.typeName; + } + // ... T /**/extends [|U|] ? ... + if (ts.isConditionalTypeNode(parent) && ts.isTypeReferenceNode(parent.extendsType)) { + return parent.extendsType.typeName; + } + } + // ... T extends /**/infer [|U|] ? ... + if (node.kind === ts.SyntaxKind.InferKeyword && ts.isInferTypeNode(parent)) { + return parent.typeParameter.name; + } + // { [ [|K|] /**/in keyof T]: ... } + if (node.kind === ts.SyntaxKind.InKeyword && ts.isTypeParameterDeclaration(parent) && ts.isMappedTypeNode(parent.parent)) { + return parent.name; + } + // /**/keyof [|T|] + if (node.kind === ts.SyntaxKind.KeyOfKeyword && ts.isTypeOperatorNode(parent) && parent.operator === ts.SyntaxKind.KeyOfKeyword && + ts.isTypeReferenceNode(parent.type)) { + return parent.type.typeName; + } + // /**/readonly [|name|][] + if (node.kind === ts.SyntaxKind.ReadonlyKeyword && ts.isTypeOperatorNode(parent) && parent.operator === ts.SyntaxKind.ReadonlyKeyword && + ts.isArrayTypeNode(parent.type) && ts.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 === ts.SyntaxKind.NewKeyword && ts.isNewExpression(parent) || + node.kind === ts.SyntaxKind.VoidKeyword && ts.isVoidExpression(parent) || + node.kind === ts.SyntaxKind.TypeOfKeyword && ts.isTypeOfExpression(parent) || + node.kind === ts.SyntaxKind.AwaitKeyword && ts.isAwaitExpression(parent) || + node.kind === ts.SyntaxKind.YieldKeyword && ts.isYieldExpression(parent) || + node.kind === ts.SyntaxKind.DeleteKeyword && ts.isDeleteExpression(parent)) { + if (parent.expression) { return ts.skipOuterExpressions(parent.expression); } } - // import name = /**/require("[|module|]"); - if (node.kind === ts.SyntaxKind.RequireKeyword && ts.isExternalModuleReference(parent)) { - return parent.expression; - } - // import ... /**/from "[|module|]"; - // export ... /**/from "[|module|]"; - if (node.kind === ts.SyntaxKind.FromKeyword && (ts.isImportDeclaration(parent) || ts.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 === ts.SyntaxKind.ExtendsKeyword || node.kind === ts.SyntaxKind.ImplementsKeyword) && ts.isHeritageClause(parent) && parent.token === node.kind) { - const location = getAdjustedLocationForHeritageClause(parent); - if (location) { - return location; - } - } - if (node.kind === ts.SyntaxKind.ExtendsKeyword) { - // ... ... - if (ts.isTypeParameterDeclaration(parent) && parent.constraint && ts.isTypeReferenceNode(parent.constraint)) { - return parent.constraint.typeName; - } - // ... T /**/extends [|U|] ? ... - if (ts.isConditionalTypeNode(parent) && ts.isTypeReferenceNode(parent.extendsType)) { - return parent.extendsType.typeName; - } - } - // ... T extends /**/infer [|U|] ? ... - if (node.kind === ts.SyntaxKind.InferKeyword && ts.isInferTypeNode(parent)) { - return parent.typeParameter.name; + // left /**/in [|name|] + // left /**/instanceof [|name|] + if ((node.kind === ts.SyntaxKind.InKeyword || node.kind === ts.SyntaxKind.InstanceOfKeyword) && ts.isBinaryExpression(parent) && parent.operatorToken === node) { + return ts.skipOuterExpressions(parent.right); } - // { [ [|K|] /**/in keyof T]: ... } - if (node.kind === ts.SyntaxKind.InKeyword && ts.isTypeParameterDeclaration(parent) && ts.isMappedTypeNode(parent.parent)) { - return parent.name; - } - // /**/keyof [|T|] - if (node.kind === ts.SyntaxKind.KeyOfKeyword && ts.isTypeOperatorNode(parent) && parent.operator === ts.SyntaxKind.KeyOfKeyword && - ts.isTypeReferenceNode(parent.type)) { + // left /**/as [|name|] + if (node.kind === ts.SyntaxKind.AsKeyword && ts.isAsExpression(parent) && ts.isTypeReferenceNode(parent.type)) { return parent.type.typeName; } - // /**/readonly [|name|][] - if (node.kind === ts.SyntaxKind.ReadonlyKeyword && ts.isTypeOperatorNode(parent) && parent.operator === ts.SyntaxKind.ReadonlyKeyword && - ts.isArrayTypeNode(parent.type) && ts.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 === ts.SyntaxKind.NewKeyword && ts.isNewExpression(parent) || - node.kind === ts.SyntaxKind.VoidKeyword && ts.isVoidExpression(parent) || - node.kind === ts.SyntaxKind.TypeOfKeyword && ts.isTypeOfExpression(parent) || - node.kind === ts.SyntaxKind.AwaitKeyword && ts.isAwaitExpression(parent) || - node.kind === ts.SyntaxKind.YieldKeyword && ts.isYieldExpression(parent) || - node.kind === ts.SyntaxKind.DeleteKeyword && ts.isDeleteExpression(parent)) { - if (parent.expression) { - return ts.skipOuterExpressions(parent.expression); - } - } - // left /**/in [|name|] - // left /**/instanceof [|name|] - if ((node.kind === ts.SyntaxKind.InKeyword || node.kind === ts.SyntaxKind.InstanceOfKeyword) && ts.isBinaryExpression(parent) && parent.operatorToken === node) { - return ts.skipOuterExpressions(parent.right); - } - // left /**/as [|name|] - if (node.kind === ts.SyntaxKind.AsKeyword && ts.isAsExpression(parent) && ts.isTypeReferenceNode(parent.type)) { - return parent.type.typeName; - } - // for (... /**/in [|name|]) - // for (... /**/of [|name|]) - if (node.kind === ts.SyntaxKind.InKeyword && ts.isForInStatement(parent) || - node.kind === ts.SyntaxKind.OfKeyword && ts.isForOfStatement(parent)) { - return ts.skipOuterExpressions(parent.expression); - } + // for (... /**/in [|name|]) + // for (... /**/of [|name|]) + if (node.kind === ts.SyntaxKind.InKeyword && ts.isForInStatement(parent) || + node.kind === ts.SyntaxKind.OfKeyword && ts.isForOfStatement(parent)) { + return ts.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: ts.Node): ts.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: ts.Node): ts.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: ts.SourceFile, position: number): ts.Node { - return getTouchingToken(sourceFile, position, n => ts.isPropertyNameLiteral(n) || ts.isKeyword(n.kind) || ts.isPrivateIdentifier(n)); } + return node; +} - /** - * Returns the token if position is in [start, end). - * If position === end, returns the preceding token if includeItemAtEndPosition(previousToken) === true - */ - export function getTouchingToken(sourceFile: ts.SourceFile, position: number, includePrecedingTokenAtEndPosition?: (n: ts.Node) => boolean): ts.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: ts.SourceFile, position: number): ts.Node { - return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined, /*includeEndPosition*/ false); - } - - /** Get the token whose text contains the position */ - function getTokenAtPositionWorker(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: ts.Node) => boolean) | undefined, includeEndPosition: boolean): ts.Node { - let current: ts.Node = sourceFile; - let foundToken: ts.Node | undefined; - outer: while (true) { - // find the child that contains 'position' - - const children = current.getChildren(sourceFile); - const i = ts.binarySearchKey(children, position, (_, i) => i, (middle, _) => { - // This last callback is more of a selector than a comparator - - // `EqualTo` causes the `middle` result to be returned - // `GreaterThan` causes recursion on the left of the middle - // `LessThan` causes recursion on the right of the middle - - // Let's say you have 3 nodes, spanning positons - // pos: 1, end: 3 - // pos: 3, end: 3 - // pos: 3, end: 5 - // and you're looking for the token at positon 3 - all 3 of these nodes are overlapping with position 3. - // In fact, there's a _good argument_ that node 2 shouldn't even be allowed to exist - depending on if - // the start or end of the ranges are considered inclusive, it's either wholly subsumed by the first or the last node. - // Unfortunately, such nodes do exist. :( - See fourslash/completionsImport_tsx.tsx - empty jsx attributes create - // a zero-length node. - // What also you may not expect is that which node we return depends on the includePrecedingTokenAtEndPosition flag. - // Specifically, if includePrecedingTokenAtEndPosition is set, we return the 1-3 node, while if it's unset, we - // return the 3-5 node. (The zero length node is never correct.) This is because the includePrecedingTokenAtEndPosition - // flag causes us to return the first node whose end position matches the position and which produces and acceptable token - // kind. Meanwhile, if includePrecedingTokenAtEndPosition is unset, we look for the first node whose start is <= the - // position and whose end is greater than the position. - - - const start = allowPositionInLeadingTrivia ? children[middle].getFullStart() : children[middle].getStart(sourceFile, /*includeJsDoc*/ true); - if (start > position) { - return ts.Comparison.GreaterThan; - } +/** + * Adjusts the location used for "find references" and "go to definition" when the cursor was not + * on a property name. + */ +export function getAdjustedReferenceLocation(node: ts.Node): ts.Node { + return getAdjustedLocation(node, /*forRename*/ false); +} - // first element whose start position is before the input and whose end position is after or equal to the input - if (nodeContainsPosition(children[middle])) { - if (children[middle - 1]) { - // we want the _first_ element that contains the position, so left-recur if the prior node also contains the position - if (nodeContainsPosition(children[middle - 1])) { - return ts.Comparison.GreaterThan; - } - } - return ts.Comparison.EqualTo; - } +/** + * Adjusts the location used for "rename" when the cursor was not on a property name. + */ +export function getAdjustedRenameLocation(node: ts.Node): ts.Node { + return getAdjustedLocation(node, /*forRename*/ true); +} - // this complex condition makes us left-recur around a zero-length node when includePrecedingTokenAtEndPosition is set, rather than right-recur on it - if (includePrecedingTokenAtEndPosition && start === position && children[middle - 1] && children[middle - 1].getEnd() === position && nodeContainsPosition(children[middle - 1])) { - return ts.Comparison.GreaterThan; - } - return ts.Comparison.LessThan; - }); +/** + * 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: ts.SourceFile, position: number): ts.Node { + return getTouchingToken(sourceFile, position, n => ts.isPropertyNameLiteral(n) || ts.isKeyword(n.kind) || ts.isPrivateIdentifier(n)); +} - if (foundToken) { - return foundToken; - } - if (i >= 0 && children[i]) { - current = children[i]; - continue outer; - } +/** + * Returns the token if position is in [start, end). + * If position === end, returns the preceding token if includeItemAtEndPosition(previousToken) === true + */ +export function getTouchingToken(sourceFile: ts.SourceFile, position: number, includePrecedingTokenAtEndPosition?: (n: ts.Node) => boolean): ts.Node { + return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, includePrecedingTokenAtEndPosition, /*includeEndPosition*/ false); +} - return current; - } +/** Returns a token if position is in [start-of-leading-trivia, end) */ +export function getTokenAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { + return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined, /*includeEndPosition*/ false); +} - function nodeContainsPosition(node: ts.Node) { - const start = allowPositionInLeadingTrivia ? node.getFullStart() : node.getStart(sourceFile, /*includeJsDoc*/ true); +/** Get the token whose text contains the position */ +function getTokenAtPositionWorker(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: ts.Node) => boolean) | undefined, includeEndPosition: boolean): ts.Node { + let current: ts.Node = sourceFile; + let foundToken: ts.Node | undefined; + outer: while (true) { + // find the child that contains 'position' + + const children = current.getChildren(sourceFile); + const i = ts.binarySearchKey(children, position, (_, i) => i, (middle, _) => { + // This last callback is more of a selector than a comparator - + // `EqualTo` causes the `middle` result to be returned + // `GreaterThan` causes recursion on the left of the middle + // `LessThan` causes recursion on the right of the middle + + // Let's say you have 3 nodes, spanning positons + // pos: 1, end: 3 + // pos: 3, end: 3 + // pos: 3, end: 5 + // and you're looking for the token at positon 3 - all 3 of these nodes are overlapping with position 3. + // In fact, there's a _good argument_ that node 2 shouldn't even be allowed to exist - depending on if + // the start or end of the ranges are considered inclusive, it's either wholly subsumed by the first or the last node. + // Unfortunately, such nodes do exist. :( - See fourslash/completionsImport_tsx.tsx - empty jsx attributes create + // a zero-length node. + // What also you may not expect is that which node we return depends on the includePrecedingTokenAtEndPosition flag. + // Specifically, if includePrecedingTokenAtEndPosition is set, we return the 1-3 node, while if it's unset, we + // return the 3-5 node. (The zero length node is never correct.) This is because the includePrecedingTokenAtEndPosition + // flag causes us to return the first node whose end position matches the position and which produces and acceptable token + // kind. Meanwhile, if includePrecedingTokenAtEndPosition is unset, we look for the first node whose start is <= the + // position and whose end is greater than the position. + + + const start = allowPositionInLeadingTrivia ? children[middle].getFullStart() : children[middle].getStart(sourceFile, /*includeJsDoc*/ true); if (start > position) { - // If this child begins after position, then all subsequent children will as well. - return false; - } - const end = node.getEnd(); - if (position < end || (position === end && (node.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { - return true; + return ts.Comparison.GreaterThan; } - else if (includePrecedingTokenAtEndPosition && end === position) { - const previousToken = findPrecedingToken(position, sourceFile, node); - if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) { - foundToken = previousToken; - return true; + + // first element whose start position is before the input and whose end position is after or equal to the input + if (nodeContainsPosition(children[middle])) { + if (children[middle - 1]) { + // we want the _first_ element that contains the position, so left-recur if the prior node also contains the position + if (nodeContainsPosition(children[middle - 1])) { + return ts.Comparison.GreaterThan; + } } + return ts.Comparison.EqualTo; } - return false; - } - } - /** - * Returns the first token where position is in [start, end), - * excluding `JsxText` tokens containing only whitespace. - */ - export function findFirstNonJsxWhitespaceToken(sourceFile: ts.SourceFile, position: number): ts.Node | undefined { - let tokenAtPosition = getTokenAtPosition(sourceFile, position); - while (isWhiteSpaceOnlyJsxText(tokenAtPosition)) { - const nextToken = findNextToken(tokenAtPosition, tokenAtPosition.parent, sourceFile); - if (!nextToken) - return; - tokenAtPosition = nextToken; - } - return tokenAtPosition; - } + // this complex condition makes us left-recur around a zero-length node when includePrecedingTokenAtEndPosition is set, rather than right-recur on it + if (includePrecedingTokenAtEndPosition && start === position && children[middle - 1] && children[middle - 1].getEnd() === position && nodeContainsPosition(children[middle - 1])) { + return ts.Comparison.GreaterThan; + } + return ts.Comparison.LessThan; + }); - /** - * 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: ts.SourceFile, position: number): ts.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 (ts.isToken(tokenAtPosition) && position > tokenAtPosition.getStart(file) && position < tokenAtPosition.getEnd()) { - return tokenAtPosition; + if (foundToken) { + return foundToken; + } + if (i >= 0 && children[i]) { + current = children[i]; + continue outer; } - return findPrecedingToken(position, file); + return current; } - export function findNextToken(previousToken: ts.Node, parent: ts.Node, sourceFile: ts.SourceFileLike): ts.Node | undefined { - return find(parent); - - function find(n: ts.Node): ts.Node | undefined { - if (ts.isToken(n) && n.pos === previousToken.end) { - // this is token that starts at the end of previous token - return it - return n; + function nodeContainsPosition(node: ts.Node) { + const start = allowPositionInLeadingTrivia ? node.getFullStart() : node.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + return false; + } + const end = node.getEnd(); + if (position < end || (position === end && (node.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + return true; + } + else if (includePrecedingTokenAtEndPosition && end === position) { + const previousToken = findPrecedingToken(position, sourceFile, node); + if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) { + foundToken = previousToken; + return true; } - return ts.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; - }); } + return false; } +} - /** - * Finds the rightmost token satisfying `token.end <= position`, - * excluding `JsxText` tokens containing only whitespace. - */ - export function findPrecedingToken(position: number, sourceFile: ts.SourceFileLike, startNode: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined; - export function findPrecedingToken(position: number, sourceFile: ts.SourceFile, startNode?: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined; - export function findPrecedingToken(position: number, sourceFile: ts.SourceFileLike, startNode?: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined { - const result = find((startNode || sourceFile) as ts.Node); - ts.Debug.assert(!(result && isWhiteSpaceOnlyJsxText(result))); - return result; - - function find(n: ts.Node): ts.Node | undefined { - if (isNonWhitespaceToken(n) && n.kind !== ts.SyntaxKind.EndOfFileToken) { - return n; - } +/** + * Returns the first token where position is in [start, end), + * excluding `JsxText` tokens containing only whitespace. + */ +export function findFirstNonJsxWhitespaceToken(sourceFile: ts.SourceFile, position: number): ts.Node | undefined { + let tokenAtPosition = getTokenAtPosition(sourceFile, position); + while (isWhiteSpaceOnlyJsxText(tokenAtPosition)) { + const nextToken = findNextToken(tokenAtPosition, tokenAtPosition.parent, sourceFile); + if (!nextToken) + return; + tokenAtPosition = nextToken; + } + return tokenAtPosition; +} - const children = n.getChildren(sourceFile); - const i = ts.binarySearchKey(children, position, (_, i) => i, (middle, _) => { - // This last callback is more of a selector than a comparator - - // `EqualTo` causes the `middle` result to be returned - // `GreaterThan` causes recursion on the left of the middle - // `LessThan` causes recursion on the right of the middle - if (position < children[middle].end) { - // first element whose end position is greater than the input position - if (!children[middle - 1] || position >= children[middle - 1].end) { - return ts.Comparison.EqualTo; - } - return ts.Comparison.GreaterThan; - } - return ts.Comparison.LessThan; - }); - if (i >= 0 && children[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, n.kind); - return candidate && findRightmostToken(candidate, sourceFile); - } - else { - // candidate should be in this node - return find(child); - } - } - } +/** + * 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: ts.SourceFile, position: number): ts.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 (ts.isToken(tokenAtPosition) && position > tokenAtPosition.getStart(file) && position < tokenAtPosition.getEnd()) { + return tokenAtPosition; + } - ts.Debug.assert(startNode !== undefined || n.kind === ts.SyntaxKind.SourceFile || n.kind === ts.SyntaxKind.EndOfFileToken || ts.isJSDocCommentContainingNode(n)); + return findPrecedingToken(position, file); +} - // 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, n.kind); - return candidate && findRightmostToken(candidate, sourceFile); - } - } +export function findNextToken(previousToken: ts.Node, parent: ts.Node, sourceFile: ts.SourceFileLike): ts.Node | undefined { + return find(parent); - function isNonWhitespaceToken(n: ts.Node): boolean { - return ts.isToken(n) && !isWhiteSpaceOnlyJsxText(n); + function find(n: ts.Node): ts.Node | undefined { + if (ts.isToken(n) && n.pos === previousToken.end) { + // this is token that starts at the end of previous token - return it + return n; + } + return ts.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; + }); } +} - function findRightmostToken(n: ts.Node, sourceFile: ts.SourceFileLike): ts.Node | undefined { - if (isNonWhitespaceToken(n)) { +/** + * Finds the rightmost token satisfying `token.end <= position`, + * excluding `JsxText` tokens containing only whitespace. + */ +export function findPrecedingToken(position: number, sourceFile: ts.SourceFileLike, startNode: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined; +export function findPrecedingToken(position: number, sourceFile: ts.SourceFile, startNode?: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined; +export function findPrecedingToken(position: number, sourceFile: ts.SourceFileLike, startNode?: ts.Node, excludeJsdoc?: boolean): ts.Node | undefined { + const result = find((startNode || sourceFile) as ts.Node); + ts.Debug.assert(!(result && isWhiteSpaceOnlyJsxText(result))); + return result; + + function find(n: ts.Node): ts.Node | undefined { + if (isNonWhitespaceToken(n) && n.kind !== ts.SyntaxKind.EndOfFileToken) { return n; } const children = n.getChildren(sourceFile); - if (children.length === 0) { - return n; + const i = ts.binarySearchKey(children, position, (_, i) => i, (middle, _) => { + // This last callback is more of a selector than a comparator - + // `EqualTo` causes the `middle` result to be returned + // `GreaterThan` causes recursion on the left of the middle + // `LessThan` causes recursion on the right of the middle + if (position < children[middle].end) { + // first element whose end position is greater than the input position + if (!children[middle - 1] || position >= children[middle - 1].end) { + return ts.Comparison.EqualTo; + } + return ts.Comparison.GreaterThan; + } + return ts.Comparison.LessThan; + }); + if (i >= 0 && children[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, n.kind); + return candidate && findRightmostToken(candidate, sourceFile); + } + else { + // candidate should be in this node + return find(child); + } + } } + ts.Debug.assert(startNode !== undefined || n.kind === ts.SyntaxKind.SourceFile || n.kind === ts.SyntaxKind.EndOfFileToken || ts.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, n.kind); return candidate && findRightmostToken(candidate, sourceFile); } +} - /** - * Finds the rightmost child to the left of `children[exclusiveStartPosition]` which is a non-all-whitespace token or has constituent tokens. - */ - function findRightmostChildNodeWithTokens(children: ts.Node[], exclusiveStartPosition: number, sourceFile: ts.SourceFileLike, parentKind: ts.SyntaxKind): ts.Node | undefined { - for (let i = exclusiveStartPosition - 1; i >= 0; i--) { - const child = children[i]; +function isNonWhitespaceToken(n: ts.Node): boolean { + return ts.isToken(n) && !isWhiteSpaceOnlyJsxText(n); +} - if (isWhiteSpaceOnlyJsxText(child)) { - if (i === 0 && (parentKind === ts.SyntaxKind.JsxText || parentKind === ts.SyntaxKind.JsxSelfClosingElement)) { - ts.Debug.fail("`JsxText` tokens should not be the first child of `JsxElement | JsxSelfClosingElement`"); - } - } - else if (nodeHasTokens(children[i], sourceFile)) { - return children[i]; - } - } +function findRightmostToken(n: ts.Node, sourceFile: ts.SourceFileLike): ts.Node | undefined { + if (isNonWhitespaceToken(n)) { + return n; } - export function isInString(sourceFile: ts.SourceFile, position: number, previousToken = findPrecedingToken(position, sourceFile)): boolean { - if (previousToken && ts.isStringTextContainingNode(previousToken)) { - const start = previousToken.getStart(sourceFile); - const end = previousToken.getEnd(); + const children = n.getChildren(sourceFile); + if (children.length === 0) { + return n; + } - // 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; - } + const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile, n.kind); + return candidate && findRightmostToken(candidate, sourceFile); +} + +/** + * Finds the rightmost child to the left of `children[exclusiveStartPosition]` which is a non-all-whitespace token or has constituent tokens. + */ +function findRightmostChildNodeWithTokens(children: ts.Node[], exclusiveStartPosition: number, sourceFile: ts.SourceFileLike, parentKind: ts.SyntaxKind): ts.Node | undefined { + for (let i = exclusiveStartPosition - 1; i >= 0; i--) { + const child = children[i]; - if (position === end) { - return !!(previousToken as ts.LiteralExpression).isUnterminated; + if (isWhiteSpaceOnlyJsxText(child)) { + if (i === 0 && (parentKind === ts.SyntaxKind.JsxText || parentKind === ts.SyntaxKind.JsxSelfClosingElement)) { + ts.Debug.fail("`JsxText` tokens should not be the first child of `JsxElement | JsxSelfClosingElement`"); } } - - return false; - } - - /** - * returns true if the position is in between the open and close elements of an JSX expression. - */ - export function isInsideJsxElementOrAttribute(sourceFile: ts.SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - - if (!token) { - return false; + else if (nodeHasTokens(children[i], sourceFile)) { + return children[i]; } + } +} - if (token.kind === ts.SyntaxKind.JsxText) { - return true; - } +export function isInString(sourceFile: ts.SourceFile, position: number, previousToken = findPrecedingToken(position, sourceFile)): boolean { + if (previousToken && ts.isStringTextContainingNode(previousToken)) { + const start = previousToken.getStart(sourceFile); + const end = previousToken.getEnd(); - //
Hello |
- if (token.kind === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxText) { + // 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 === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxExpression) { - return true; + if (position === end) { + return !!(previousToken as ts.LiteralExpression).isUnterminated; } + } - //
{ - // | - // } < /div> - if (token && token.kind === ts.SyntaxKind.CloseBraceToken && token.parent.kind === ts.SyntaxKind.JsxExpression) { - return true; - } + return false; +} - //
|
- if (token.kind === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxClosingElement) { - return true; - } +/** + * returns true if the position is in between the open and close elements of an JSX expression. + */ +export function isInsideJsxElementOrAttribute(sourceFile: ts.SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + if (!token) { return false; } - function isWhiteSpaceOnlyJsxText(node: ts.Node): boolean { - return ts.isJsxText(node) && node.containsOnlyTriviaWhiteSpaces; + if (token.kind === ts.SyntaxKind.JsxText) { + return true; } - export function isInTemplateString(sourceFile: ts.SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - return ts.isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile); + //
Hello |
+ if (token.kind === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxText) { + return true; } - export function isInJSXText(sourceFile: ts.SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - if (ts.isJsxText(token)) { - return true; - } - if (token.kind === ts.SyntaxKind.OpenBraceToken && ts.isJsxExpression(token.parent) && ts.isJsxElement(token.parent.parent)) { - return true; - } - if (token.kind === ts.SyntaxKind.LessThanToken && ts.isJsxOpeningLikeElement(token.parent) && ts.isJsxElement(token.parent.parent)) { - return true; - } - return false; + //
{ |
or
+ if (token.kind === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxExpression) { + return true; } - export function isInsideJsxElement(sourceFile: ts.SourceFile, position: number): boolean { - function isInsideJsxElementTraversal(node: ts.Node): boolean { - while (node) { - if (node.kind >= ts.SyntaxKind.JsxSelfClosingElement && node.kind <= ts.SyntaxKind.JsxExpression - || node.kind === ts.SyntaxKind.JsxText - || node.kind === ts.SyntaxKind.LessThanToken - || node.kind === ts.SyntaxKind.GreaterThanToken - || node.kind === ts.SyntaxKind.Identifier - || node.kind === ts.SyntaxKind.CloseBraceToken - || node.kind === ts.SyntaxKind.OpenBraceToken - || node.kind === ts.SyntaxKind.SlashToken) { - node = node.parent; - } - else if (node.kind === ts.SyntaxKind.JsxElement) { - if (position > node.getStart(sourceFile)) - return true; + //
{ + // | + // } < /div> + if (token && token.kind === ts.SyntaxKind.CloseBraceToken && token.parent.kind === ts.SyntaxKind.JsxExpression) { + return true; + } - node = node.parent; - } - else { - return false; - } - } + //
|
+ if (token.kind === ts.SyntaxKind.LessThanToken && token.parent.kind === ts.SyntaxKind.JsxClosingElement) { + return true; + } - return false; - } + return false; +} - return isInsideJsxElementTraversal(getTokenAtPosition(sourceFile, position)); - } +function isWhiteSpaceOnlyJsxText(node: ts.Node): boolean { + return ts.isJsxText(node) && node.containsOnlyTriviaWhiteSpaces; +} - export function findPrecedingMatchingToken(token: ts.Node, matchingTokenKind: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenParenToken | ts.SyntaxKind.OpenBracketToken, sourceFile: ts.SourceFile) { - const closeTokenText = ts.tokenToString(token.kind)!; - const matchingTokenText = ts.tokenToString(matchingTokenKind)!; - const tokenFullStart = token.getFullStart(); - // Text-scan based fast path - can be bamboozled by comments and other trivia, but often provides - // a good, fast approximation without too much extra work in the cases where it fails. - const bestGuessIndex = sourceFile.text.lastIndexOf(matchingTokenText, tokenFullStart); - if (bestGuessIndex === -1) { - return undefined; // if the token text doesn't appear in the file, there can't be a match - super fast bail - } - // we can only use the textual result directly if we didn't have to count any close tokens within the range - if (sourceFile.text.lastIndexOf(closeTokenText, tokenFullStart - 1) < bestGuessIndex) { - const nodeAtGuess = findPrecedingToken(bestGuessIndex + 1, sourceFile); - if (nodeAtGuess && nodeAtGuess.kind === matchingTokenKind) { - return nodeAtGuess; - } - } - const tokenKind = token.kind; - let remainingMatchingTokens = 0; - while (true) { - const preceding = findPrecedingToken(token.getFullStart(), sourceFile); - if (!preceding) { - return undefined; - } - token = preceding; +export function isInTemplateString(sourceFile: ts.SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + return ts.isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile); +} - if (token.kind === matchingTokenKind) { - if (remainingMatchingTokens === 0) { - return token; - } +export function isInJSXText(sourceFile: ts.SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + if (ts.isJsxText(token)) { + return true; + } + if (token.kind === ts.SyntaxKind.OpenBraceToken && ts.isJsxExpression(token.parent) && ts.isJsxElement(token.parent.parent)) { + return true; + } + if (token.kind === ts.SyntaxKind.LessThanToken && ts.isJsxOpeningLikeElement(token.parent) && ts.isJsxElement(token.parent.parent)) { + return true; + } + return false; +} + +export function isInsideJsxElement(sourceFile: ts.SourceFile, position: number): boolean { + function isInsideJsxElementTraversal(node: ts.Node): boolean { + while (node) { + if (node.kind >= ts.SyntaxKind.JsxSelfClosingElement && node.kind <= ts.SyntaxKind.JsxExpression + || node.kind === ts.SyntaxKind.JsxText + || node.kind === ts.SyntaxKind.LessThanToken + || node.kind === ts.SyntaxKind.GreaterThanToken + || node.kind === ts.SyntaxKind.Identifier + || node.kind === ts.SyntaxKind.CloseBraceToken + || node.kind === ts.SyntaxKind.OpenBraceToken + || node.kind === ts.SyntaxKind.SlashToken) { + node = node.parent; + } + else if (node.kind === ts.SyntaxKind.JsxElement) { + if (position > node.getStart(sourceFile)) + return true; - remainingMatchingTokens--; + node = node.parent; } - else if (token.kind === tokenKind) { - remainingMatchingTokens++; + else { + return false; } } - } - export function removeOptionality(type: ts.Type, isOptionalExpression: boolean, isOptionalChain: boolean) { - return isOptionalExpression ? type.getNonNullableType() : - isOptionalChain ? type.getNonOptionalType() : - type; + return false; } - export function isPossiblyTypeArgumentPosition(token: ts.Node, sourceFile: ts.SourceFile, checker: ts.TypeChecker): boolean { - const info = getPossibleTypeArgumentsInfo(token, sourceFile); - return info !== undefined && (ts.isPartOfTypeNode(info.called) || - getPossibleGenericSignatures(info.called, info.nTypeArguments, checker).length !== 0 || - isPossiblyTypeArgumentPosition(info.called, sourceFile, checker)); - } + return isInsideJsxElementTraversal(getTokenAtPosition(sourceFile, position)); +} - export function getPossibleGenericSignatures(called: ts.Expression, typeArgumentCount: number, checker: ts.TypeChecker): readonly ts.Signature[] { - let type = checker.getTypeAtLocation(called); - if (ts.isOptionalChain(called.parent)) { - type = removeOptionality(type, ts.isOptionalChainRoot(called.parent), /*isOptionalChain*/ true); +export function findPrecedingMatchingToken(token: ts.Node, matchingTokenKind: ts.SyntaxKind.OpenBraceToken | ts.SyntaxKind.OpenParenToken | ts.SyntaxKind.OpenBracketToken, sourceFile: ts.SourceFile) { + const closeTokenText = ts.tokenToString(token.kind)!; + const matchingTokenText = ts.tokenToString(matchingTokenKind)!; + const tokenFullStart = token.getFullStart(); + // Text-scan based fast path - can be bamboozled by comments and other trivia, but often provides + // a good, fast approximation without too much extra work in the cases where it fails. + const bestGuessIndex = sourceFile.text.lastIndexOf(matchingTokenText, tokenFullStart); + if (bestGuessIndex === -1) { + return undefined; // if the token text doesn't appear in the file, there can't be a match - super fast bail + } + // we can only use the textual result directly if we didn't have to count any close tokens within the range + if (sourceFile.text.lastIndexOf(closeTokenText, tokenFullStart - 1) < bestGuessIndex) { + const nodeAtGuess = findPrecedingToken(bestGuessIndex + 1, sourceFile); + if (nodeAtGuess && nodeAtGuess.kind === matchingTokenKind) { + return nodeAtGuess; + } + } + const tokenKind = token.kind; + let remainingMatchingTokens = 0; + while (true) { + const preceding = findPrecedingToken(token.getFullStart(), sourceFile); + if (!preceding) { + return undefined; } + token = preceding; - const signatures = ts.isNewExpression(called.parent) ? type.getConstructSignatures() : type.getCallSignatures(); - return signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= typeArgumentCount); - } + if (token.kind === matchingTokenKind) { + if (remainingMatchingTokens === 0) { + return token; + } - export interface PossibleTypeArgumentInfo { - readonly called: ts.Identifier; - readonly nTypeArguments: number; + remainingMatchingTokens--; + } + else if (token.kind === tokenKind) { + remainingMatchingTokens++; + } } +} + +export function removeOptionality(type: ts.Type, isOptionalExpression: boolean, isOptionalChain: boolean) { + return isOptionalExpression ? type.getNonNullableType() : + isOptionalChain ? type.getNonOptionalType() : + type; +} + +export function isPossiblyTypeArgumentPosition(token: ts.Node, sourceFile: ts.SourceFile, checker: ts.TypeChecker): boolean { + const info = getPossibleTypeArgumentsInfo(token, sourceFile); + return info !== undefined && (ts.isPartOfTypeNode(info.called) || + getPossibleGenericSignatures(info.called, info.nTypeArguments, checker).length !== 0 || + isPossiblyTypeArgumentPosition(info.called, sourceFile, checker)); +} - export interface PossibleProgramFileInfo { - ProgramFiles?: string[]; +export function getPossibleGenericSignatures(called: ts.Expression, typeArgumentCount: number, checker: ts.TypeChecker): readonly ts.Signature[] { + let type = checker.getTypeAtLocation(called); + if (ts.isOptionalChain(called.parent)) { + type = removeOptionality(type, ts.isOptionalChainRoot(called.parent), /*isOptionalChain*/ true); } - // Get info for an expression like `f <` that may be the start of type arguments. - export function getPossibleTypeArgumentsInfo(tokenIn: ts.Node | undefined, sourceFile: ts.SourceFile): PossibleTypeArgumentInfo | undefined { - // This is a rare case, but one that saves on a _lot_ of work if true - if the source file has _no_ `<` character, - // then there obviously can't be any type arguments - no expensive brace-matching backwards scanning required + const signatures = ts.isNewExpression(called.parent) ? type.getConstructSignatures() : type.getCallSignatures(); + return signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= typeArgumentCount); +} - if (sourceFile.text.lastIndexOf("<", tokenIn ? tokenIn.pos : sourceFile.text.length) === -1) { - return undefined; - } +export interface PossibleTypeArgumentInfo { + readonly called: ts.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: ts.Node | undefined, sourceFile: ts.SourceFile): PossibleTypeArgumentInfo | undefined { + // This is a rare case, but one that saves on a _lot_ of work if true - if the source file has _no_ `<` character, + // then there obviously can't be any type arguments - no expensive brace-matching backwards scanning required - let token: ts.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 ts.SyntaxKind.LessThanToken: - // Found the beginning of the generic argument expression + if (sourceFile.text.lastIndexOf("<", tokenIn ? tokenIn.pos : sourceFile.text.length) === -1) { + return undefined; + } + + let token: ts.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 ts.SyntaxKind.LessThanToken: + // Found the beginning of the generic argument expression + token = findPrecedingToken(token.getFullStart(), sourceFile); + if (token && token.kind === ts.SyntaxKind.QuestionDotToken) { token = findPrecedingToken(token.getFullStart(), sourceFile); - if (token && token.kind === ts.SyntaxKind.QuestionDotToken) { - token = findPrecedingToken(token.getFullStart(), sourceFile); - } - if (!token || !ts.isIdentifier(token)) - return undefined; - if (!remainingLessThanTokens) { - return ts.isDeclarationName(token) ? undefined : { called: token, nTypeArguments }; - } - remainingLessThanTokens--; - break; + } + if (!token || !ts.isIdentifier(token)) + return undefined; + if (!remainingLessThanTokens) { + return ts.isDeclarationName(token) ? undefined : { called: token, nTypeArguments }; + } + remainingLessThanTokens--; + break; - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - remainingLessThanTokens = + 3; - break; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + remainingLessThanTokens = + 3; + break; - case ts.SyntaxKind.GreaterThanGreaterThanToken: - remainingLessThanTokens = + 2; - break; + case ts.SyntaxKind.GreaterThanGreaterThanToken: + remainingLessThanTokens = + 2; + break; - case ts.SyntaxKind.GreaterThanToken: - remainingLessThanTokens++; - break; + case ts.SyntaxKind.GreaterThanToken: + remainingLessThanTokens++; + break; - case ts.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, ts.SyntaxKind.OpenBraceToken, sourceFile); - if (!token) - return undefined; - break; + case ts.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, ts.SyntaxKind.OpenBraceToken, sourceFile); + if (!token) + return undefined; + break; - case ts.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, ts.SyntaxKind.OpenParenToken, sourceFile); - if (!token) - return undefined; - break; + case ts.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, ts.SyntaxKind.OpenParenToken, sourceFile); + if (!token) + return undefined; + break; - case ts.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, ts.SyntaxKind.OpenBracketToken, sourceFile); - if (!token) - return undefined; - break; + case ts.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, ts.SyntaxKind.OpenBracketToken, sourceFile); + if (!token) + return undefined; + break; - // Valid tokens in a type name. Skip. - case ts.SyntaxKind.CommaToken: - nTypeArguments++; - break; + // Valid tokens in a type name. Skip. + case ts.SyntaxKind.CommaToken: + nTypeArguments++; + break; - case ts.SyntaxKind.EqualsGreaterThanToken: - // falls through - - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - case ts.SyntaxKind.BigIntLiteral: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - // falls through - - case ts.SyntaxKind.TypeOfKeyword: - case ts.SyntaxKind.ExtendsKeyword: - case ts.SyntaxKind.KeyOfKeyword: - case ts.SyntaxKind.DotToken: - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.QuestionToken: - case ts.SyntaxKind.ColonToken: - break; + case ts.SyntaxKind.EqualsGreaterThanToken: + // falls through - default: - if (ts.isTypeNode(token)) { - break; - } + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + // falls through - // Invalid token in type - return undefined; - } + case ts.SyntaxKind.TypeOfKeyword: + case ts.SyntaxKind.ExtendsKeyword: + case ts.SyntaxKind.KeyOfKeyword: + case ts.SyntaxKind.DotToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.QuestionToken: + case ts.SyntaxKind.ColonToken: + break; - token = findPrecedingToken(token.getFullStart(), sourceFile); + default: + if (ts.isTypeNode(token)) { + break; + } + + // Invalid token in type + return undefined; } - return undefined; + token = findPrecedingToken(token.getFullStart(), sourceFile); } - /** - * 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: ts.SourceFile, position: number, tokenAtPosition?: ts.Node): ts.CommentRange | undefined { - return ts.formatting.getRangeOfEnclosingComment(sourceFile, position, /*precedingToken*/ undefined, tokenAtPosition); - } - - export function hasDocComment(sourceFile: ts.SourceFile, position: number): boolean { - const token = getTokenAtPosition(sourceFile, position); - return !!ts.findAncestor(token, ts.isJSDoc); - } - - function nodeHasTokens(n: ts.Node, sourceFile: ts.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 === ts.SyntaxKind.EndOfFileToken ? !!(n as ts.EndOfFileToken).jsDoc : n.getWidth(sourceFile) !== 0; - } - - export function getNodeModifiers(node: ts.Node, excludeFlags = ts.ModifierFlags.None): string { - const result: string[] = []; - const flags = ts.isDeclaration(node) - ? ts.getCombinedNodeFlagsAlwaysIncludeJSDoc(node) & ~excludeFlags - : ts.ModifierFlags.None; - if (flags & ts.ModifierFlags.Private) - result.push(ts.ScriptElementKindModifier.privateMemberModifier); - if (flags & ts.ModifierFlags.Protected) - result.push(ts.ScriptElementKindModifier.protectedMemberModifier); - if (flags & ts.ModifierFlags.Public) - result.push(ts.ScriptElementKindModifier.publicMemberModifier); - if (flags & ts.ModifierFlags.Static || ts.isClassStaticBlockDeclaration(node)) - result.push(ts.ScriptElementKindModifier.staticModifier); - if (flags & ts.ModifierFlags.Abstract) - result.push(ts.ScriptElementKindModifier.abstractModifier); - if (flags & ts.ModifierFlags.Export) - result.push(ts.ScriptElementKindModifier.exportedModifier); - if (flags & ts.ModifierFlags.Deprecated) - result.push(ts.ScriptElementKindModifier.deprecatedModifier); - if (node.flags & ts.NodeFlags.Ambient) - result.push(ts.ScriptElementKindModifier.ambientModifier); - if (node.kind === ts.SyntaxKind.ExportAssignment) - result.push(ts.ScriptElementKindModifier.exportedModifier); - return result.length > 0 ? result.join(",") : ts.ScriptElementKindModifier.none; - } - export function getTypeArgumentOrTypeParameterList(node: ts.Node): ts.NodeArray | undefined { - if (node.kind === ts.SyntaxKind.TypeReference || node.kind === ts.SyntaxKind.CallExpression) { - return (node as ts.CallExpression).typeArguments; - } - if (ts.isFunctionLike(node) || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.InterfaceDeclaration) { - return (node as ts.FunctionLikeDeclaration).typeParameters; - } + return undefined; +} - 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: ts.SourceFile, position: number, tokenAtPosition?: ts.Node): ts.CommentRange | undefined { + return ts.formatting.getRangeOfEnclosingComment(sourceFile, position, /*precedingToken*/ undefined, tokenAtPosition); +} + +export function hasDocComment(sourceFile: ts.SourceFile, position: number): boolean { + const token = getTokenAtPosition(sourceFile, position); + return !!ts.findAncestor(token, ts.isJSDoc); +} + +function nodeHasTokens(n: ts.Node, sourceFile: ts.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 === ts.SyntaxKind.EndOfFileToken ? !!(n as ts.EndOfFileToken).jsDoc : n.getWidth(sourceFile) !== 0; +} + +export function getNodeModifiers(node: ts.Node, excludeFlags = ts.ModifierFlags.None): string { + const result: string[] = []; + const flags = ts.isDeclaration(node) + ? ts.getCombinedNodeFlagsAlwaysIncludeJSDoc(node) & ~excludeFlags + : ts.ModifierFlags.None; + if (flags & ts.ModifierFlags.Private) + result.push(ts.ScriptElementKindModifier.privateMemberModifier); + if (flags & ts.ModifierFlags.Protected) + result.push(ts.ScriptElementKindModifier.protectedMemberModifier); + if (flags & ts.ModifierFlags.Public) + result.push(ts.ScriptElementKindModifier.publicMemberModifier); + if (flags & ts.ModifierFlags.Static || ts.isClassStaticBlockDeclaration(node)) + result.push(ts.ScriptElementKindModifier.staticModifier); + if (flags & ts.ModifierFlags.Abstract) + result.push(ts.ScriptElementKindModifier.abstractModifier); + if (flags & ts.ModifierFlags.Export) + result.push(ts.ScriptElementKindModifier.exportedModifier); + if (flags & ts.ModifierFlags.Deprecated) + result.push(ts.ScriptElementKindModifier.deprecatedModifier); + if (node.flags & ts.NodeFlags.Ambient) + result.push(ts.ScriptElementKindModifier.ambientModifier); + if (node.kind === ts.SyntaxKind.ExportAssignment) + result.push(ts.ScriptElementKindModifier.exportedModifier); + return result.length > 0 ? result.join(",") : ts.ScriptElementKindModifier.none; +} +export function getTypeArgumentOrTypeParameterList(node: ts.Node): ts.NodeArray | undefined { + if (node.kind === ts.SyntaxKind.TypeReference || node.kind === ts.SyntaxKind.CallExpression) { + return (node as ts.CallExpression).typeArguments; } + if (ts.isFunctionLike(node) || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.InterfaceDeclaration) { + return (node as ts.FunctionLikeDeclaration).typeParameters; + } + + return undefined; +} + +export function isComment(kind: ts.SyntaxKind): boolean { + return kind === ts.SyntaxKind.SingleLineCommentTrivia || kind === ts.SyntaxKind.MultiLineCommentTrivia; +} - export function isComment(kind: ts.SyntaxKind): boolean { - return kind === ts.SyntaxKind.SingleLineCommentTrivia || kind === ts.SyntaxKind.MultiLineCommentTrivia; +export function isStringOrRegularExpressionOrTemplateLiteral(kind: ts.SyntaxKind): boolean { + if (kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.RegularExpressionLiteral + || ts.isTemplateLiteralKind(kind)) { + return true; } + return false; +} + +export function isPunctuation(kind: ts.SyntaxKind): boolean { + return ts.SyntaxKind.FirstPunctuation <= kind && kind <= ts.SyntaxKind.LastPunctuation; +} + +export function isInsideTemplateLiteral(node: ts.TemplateLiteralToken, position: number, sourceFile: ts.SourceFile): boolean { + return ts.isTemplateLiteralKind(node.kind) + && (node.getStart(sourceFile) < position && position < node.end) || (!!node.isUnterminated && position === node.end); +} - export function isStringOrRegularExpressionOrTemplateLiteral(kind: ts.SyntaxKind): boolean { - if (kind === ts.SyntaxKind.StringLiteral - || kind === ts.SyntaxKind.RegularExpressionLiteral - || ts.isTemplateLiteralKind(kind)) { +export function isAccessibilityModifier(kind: ts.SyntaxKind) { + switch (kind) { + case ts.SyntaxKind.PublicKeyword: + case ts.SyntaxKind.PrivateKeyword: + case ts.SyntaxKind.ProtectedKeyword: return true; - } - return false; } - export function isPunctuation(kind: ts.SyntaxKind): boolean { - return ts.SyntaxKind.FirstPunctuation <= kind && kind <= ts.SyntaxKind.LastPunctuation; - } + return false; +} - export function isInsideTemplateLiteral(node: ts.TemplateLiteralToken, position: number, sourceFile: ts.SourceFile): boolean { - return ts.isTemplateLiteralKind(node.kind) - && (node.getStart(sourceFile) < position && position < node.end) || (!!node.isUnterminated && position === node.end); - } +export function cloneCompilerOptions(options: ts.CompilerOptions): ts.CompilerOptions { + const result = ts.clone(options); + ts.setConfigFileInOptions(result, options && options.configFile); + return result; +} - export function isAccessibilityModifier(kind: ts.SyntaxKind) { - switch (kind) { - case ts.SyntaxKind.PublicKeyword: - case ts.SyntaxKind.PrivateKeyword: - case ts.SyntaxKind.ProtectedKeyword: - return true; +export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: ts.Node) { + if (node.kind === ts.SyntaxKind.ArrayLiteralExpression || + node.kind === ts.SyntaxKind.ObjectLiteralExpression) { + // [a,b,c] from: + // [a, b, c] = someExpression; + if (node.parent.kind === ts.SyntaxKind.BinaryExpression && + (node.parent as ts.BinaryExpression).left === node && + (node.parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { + return true; } - return false; - } + // [a, b, c] from: + // for([a, b, c] of expression) + if (node.parent.kind === ts.SyntaxKind.ForOfStatement && + (node.parent as ts.ForOfStatement).initializer === node) { + return true; + } - export function cloneCompilerOptions(options: ts.CompilerOptions): ts.CompilerOptions { - const result = ts.clone(options); - ts.setConfigFileInOptions(result, options && options.configFile); - return result; + // [a, b, c] of + // [x, [a, b, c] ] = someExpression + // or + // {x, a: {a, b, c} } = someExpression + if (isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent.kind === ts.SyntaxKind.PropertyAssignment ? node.parent.parent : node.parent)) { + return true; + } } - export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: ts.Node) { - if (node.kind === ts.SyntaxKind.ArrayLiteralExpression || - node.kind === ts.SyntaxKind.ObjectLiteralExpression) { - // [a,b,c] from: - // [a, b, c] = someExpression; - if (node.parent.kind === ts.SyntaxKind.BinaryExpression && - (node.parent as ts.BinaryExpression).left === node && - (node.parent as ts.BinaryExpression).operatorToken.kind === ts.SyntaxKind.EqualsToken) { - return true; - } + return false; +} - // [a, b, c] from: - // for([a, b, c] of expression) - if (node.parent.kind === ts.SyntaxKind.ForOfStatement && - (node.parent as ts.ForOfStatement).initializer === node) { - return true; - } +export function isInReferenceComment(sourceFile: ts.SourceFile, position: number): boolean { + return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ true); +} - // [a, b, c] of - // [x, [a, b, c] ] = someExpression - // or - // {x, a: {a, b, c} } = someExpression - if (isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent.kind === ts.SyntaxKind.PropertyAssignment ? node.parent.parent : node.parent)) { - return true; - } - } +export function isInNonReferenceComment(sourceFile: ts.SourceFile, position: number): boolean { + return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ false); +} - return false; - } +function isInReferenceCommentWorker(sourceFile: ts.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 isInReferenceComment(sourceFile: ts.SourceFile, position: number): boolean { - return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ true); - } +export function getReplacementSpanForContextToken(contextToken: ts.Node | undefined) { + if (!contextToken) + return undefined; - export function isInNonReferenceComment(sourceFile: ts.SourceFile, position: number): boolean { - return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ false); + switch (contextToken.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return createTextSpanFromStringLiteralLikeContent(contextToken as ts.StringLiteralLike); + default: + return createTextSpanFromNode(contextToken); } +} - function isInReferenceCommentWorker(sourceFile: ts.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: ts.Node, sourceFile?: ts.SourceFile, endNode?: ts.Node): ts.TextSpan { + return ts.createTextSpanFromBounds(node.getStart(sourceFile), (endNode || node).getEnd()); +} - export function getReplacementSpanForContextToken(contextToken: ts.Node | undefined) { - if (!contextToken) - return undefined; +export function createTextSpanFromStringLiteralLikeContent(node: ts.StringLiteralLike) { + if (node.isUnterminated) + return undefined; + return ts.createTextSpanFromBounds(node.getStart() + 1, node.getEnd() - 1); +} - switch (contextToken.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return createTextSpanFromStringLiteralLikeContent(contextToken as ts.StringLiteralLike); - default: - return createTextSpanFromNode(contextToken); - } - } +export function createTextRangeFromNode(node: ts.Node, sourceFile: ts.SourceFile): ts.TextRange { + return ts.createRange(node.getStart(sourceFile), node.end); +} - export function createTextSpanFromNode(node: ts.Node, sourceFile?: ts.SourceFile, endNode?: ts.Node): ts.TextSpan { - return ts.createTextSpanFromBounds(node.getStart(sourceFile), (endNode || node).getEnd()); - } +export function createTextSpanFromRange(range: ts.TextRange): ts.TextSpan { + return ts.createTextSpanFromBounds(range.pos, range.end); +} - export function createTextSpanFromStringLiteralLikeContent(node: ts.StringLiteralLike) { - if (node.isUnterminated) - return undefined; - return ts.createTextSpanFromBounds(node.getStart() + 1, node.getEnd() - 1); - } +export function createTextRangeFromSpan(span: ts.TextSpan): ts.TextRange { + return ts.createRange(span.start, span.start + span.length); +} - export function createTextRangeFromNode(node: ts.Node, sourceFile: ts.SourceFile): ts.TextRange { - return ts.createRange(node.getStart(sourceFile), node.end); - } +export function createTextChangeFromStartLength(start: number, length: number, newText: string): ts.TextChange { + return createTextChange(ts.createTextSpan(start, length), newText); +} - export function createTextSpanFromRange(range: ts.TextRange): ts.TextSpan { - return ts.createTextSpanFromBounds(range.pos, range.end); - } +export function createTextChange(span: ts.TextSpan, newText: string): ts.TextChange { + return { span, newText }; +} - export function createTextRangeFromSpan(span: ts.TextSpan): ts.TextRange { - return ts.createRange(span.start, span.start + span.length); - } +export const typeKeywords: readonly ts.SyntaxKind[] = [ + ts.SyntaxKind.AnyKeyword, + ts.SyntaxKind.AssertsKeyword, + ts.SyntaxKind.BigIntKeyword, + ts.SyntaxKind.BooleanKeyword, + ts.SyntaxKind.FalseKeyword, + ts.SyntaxKind.InferKeyword, + ts.SyntaxKind.KeyOfKeyword, + ts.SyntaxKind.NeverKeyword, + ts.SyntaxKind.NullKeyword, + ts.SyntaxKind.NumberKeyword, + ts.SyntaxKind.ObjectKeyword, + ts.SyntaxKind.ReadonlyKeyword, + ts.SyntaxKind.StringKeyword, + ts.SyntaxKind.SymbolKeyword, + ts.SyntaxKind.TrueKeyword, + ts.SyntaxKind.VoidKeyword, + ts.SyntaxKind.UndefinedKeyword, + ts.SyntaxKind.UniqueKeyword, + ts.SyntaxKind.UnknownKeyword, +]; + +export function isTypeKeyword(kind: ts.SyntaxKind): boolean { + return ts.contains(typeKeywords, kind); +} - export function createTextChangeFromStartLength(start: number, length: number, newText: string): ts.TextChange { - return createTextChange(ts.createTextSpan(start, length), newText); - } +export function isTypeKeywordToken(node: ts.Node): node is ts.Token { + return node.kind === ts.SyntaxKind.TypeKeyword; +} - export function createTextChange(span: ts.TextSpan, newText: string): ts.TextChange { - return { span, newText }; - } +export function isTypeKeywordTokenOrIdentifier(node: ts.Node) { + return isTypeKeywordToken(node) || ts.isIdentifier(node) && node.text === "type"; +} - export const typeKeywords: readonly ts.SyntaxKind[] = [ - ts.SyntaxKind.AnyKeyword, - ts.SyntaxKind.AssertsKeyword, - ts.SyntaxKind.BigIntKeyword, - ts.SyntaxKind.BooleanKeyword, - ts.SyntaxKind.FalseKeyword, - ts.SyntaxKind.InferKeyword, - ts.SyntaxKind.KeyOfKeyword, - ts.SyntaxKind.NeverKeyword, - ts.SyntaxKind.NullKeyword, - ts.SyntaxKind.NumberKeyword, - ts.SyntaxKind.ObjectKeyword, - ts.SyntaxKind.ReadonlyKeyword, - ts.SyntaxKind.StringKeyword, - ts.SyntaxKind.SymbolKeyword, - ts.SyntaxKind.TrueKeyword, - ts.SyntaxKind.VoidKeyword, - ts.SyntaxKind.UndefinedKeyword, - ts.SyntaxKind.UniqueKeyword, - ts.SyntaxKind.UnknownKeyword, - ]; +/** True if the symbol is for an external module, as opposed to a namespace. */ +export function isExternalModuleSymbol(moduleSymbol: ts.Symbol): boolean { + return !!(moduleSymbol.flags & ts.SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === ts.CharacterCodes.doubleQuote; +} - export function isTypeKeyword(kind: ts.SyntaxKind): boolean { - return ts.contains(typeKeywords, kind); - } +/** 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 = ts.getNodeId(node); + return !seen[id] && (seen[id] = true); + }; +} - export function isTypeKeywordToken(node: ts.Node): node is ts.Token { - return node.kind === ts.SyntaxKind.TypeKeyword; +export function getSnapshotText(snap: ts.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: ts.Type): ts.Type { + return type.isTypeParameter() ? type.getConstraint() || type : type; +} + +export function getNameFromPropertyName(name: ts.PropertyName): string | undefined { + return name.kind === ts.SyntaxKind.ComputedPropertyName + // treat computed property names where expression is string/numeric literal as just string/numeric literal + ? ts.isStringOrNumericLiteralLike(name.expression) ? name.expression.text : undefined + : ts.isPrivateIdentifier(name) ? ts.idText(name) : ts.getTextOfIdentifierOrLiteral(name); +} + +export function programContainsModules(program: ts.Program): boolean { + return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!(s.externalModuleIndicator || s.commonJsModuleIndicator)); +} +export function programContainsEsModules(program: ts.Program): boolean { + return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator); +} +export function compilerOptionsIndicateEsModules(compilerOptions: ts.CompilerOptions): boolean { + return !!compilerOptions.module || ts.getEmitScriptTarget(compilerOptions) >= ts.ScriptTarget.ES2015 || !!compilerOptions.noEmit; +} + +export function createModuleSpecifierResolutionHost(program: ts.Program, host: ts.LanguageServiceHost): ts.ModuleSpecifierResolutionHost { + // Mix in `getSymlinkCache` from Program when host doesn't have it + // in order for non-Project hosts to have a symlinks cache. + return { + fileExists: fileName => program.fileExists(fileName), + getCurrentDirectory: () => host.getCurrentDirectory(), + readFile: ts.maybeBind(host, host.readFile), + useCaseSensitiveFileNames: ts.maybeBind(host, host.useCaseSensitiveFileNames), + getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache) || program.getSymlinkCache, + getModuleSpecifierCache: ts.maybeBind(host, host.getModuleSpecifierCache), + getPackageJsonInfoCache: () => program.getModuleResolutionCache()?.getPackageJsonInfoCache(), + getGlobalTypingsCacheLocation: ts.maybeBind(host, host.getGlobalTypingsCacheLocation), + redirectTargetsMap: program.redirectTargetsMap, + getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName), + isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName), + getNearestAncestorDirectoryWithPackageJson: ts.maybeBind(host, host.getNearestAncestorDirectoryWithPackageJson), + getFileIncludeReasons: () => program.getFileIncludeReasons(), + }; +} + +export function getModuleSpecifierResolverHost(program: ts.Program, host: ts.LanguageServiceHost): ts.SymbolTracker["moduleResolverHost"] { + return { + ...createModuleSpecifierResolutionHost(program, host), + getCommonSourceDirectory: () => program.getCommonSourceDirectory(), + }; +} + +export function moduleResolutionRespectsExports(moduleResolution: ts.ModuleResolutionKind): boolean { + return moduleResolution >= ts.ModuleResolutionKind.Node16 && moduleResolution <= ts.ModuleResolutionKind.NodeNext; +} + +export function moduleResolutionUsesNodeModules(moduleResolution: ts.ModuleResolutionKind): boolean { + return moduleResolution === ts.ModuleResolutionKind.NodeJs || moduleResolution >= ts.ModuleResolutionKind.Node16 && moduleResolution <= ts.ModuleResolutionKind.NodeNext; +} + +export function makeImportIfNecessary(defaultImport: ts.Identifier | undefined, namedImports: readonly ts.ImportSpecifier[] | undefined, moduleSpecifier: string, quotePreference: QuotePreference): ts.ImportDeclaration | undefined { + return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, quotePreference) : undefined; +} + +export function makeImport(defaultImport: ts.Identifier | undefined, namedImports: readonly ts.ImportSpecifier[] | undefined, moduleSpecifier: string | ts.Expression, quotePreference: QuotePreference, isTypeOnly?: boolean): ts.ImportDeclaration { + return ts.factory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, defaultImport || namedImports + ? ts.factory.createImportClause(!!isTypeOnly, defaultImport, namedImports && namedImports.length ? ts.factory.createNamedImports(namedImports) : undefined) + : undefined, typeof moduleSpecifier === "string" ? makeStringLiteral(moduleSpecifier, quotePreference) : moduleSpecifier, + /*assertClause*/ undefined); +} - export function isTypeKeywordTokenOrIdentifier(node: ts.Node) { - return isTypeKeywordToken(node) || ts.isIdentifier(node) && node.text === "type"; - } +export function makeStringLiteral(text: string, quotePreference: QuotePreference): ts.StringLiteral { + return ts.factory.createStringLiteral(text, quotePreference === QuotePreference.Single); +} +export const enum QuotePreference { + Single, + Double +} - /** True if the symbol is for an external module, as opposed to a namespace. */ - export function isExternalModuleSymbol(moduleSymbol: ts.Symbol): boolean { - return !!(moduleSymbol.flags & ts.SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === ts.CharacterCodes.doubleQuote; - } +export function quotePreferenceFromString(str: ts.StringLiteral, sourceFile: ts.SourceFile): QuotePreference { + return ts.isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single; +} - /** 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 = ts.getNodeId(node); - return !seen[id] && (seen[id] = true); - }; +export function getQuotePreference(sourceFile: ts.SourceFile, preferences: ts.UserPreferences): QuotePreference { + if (preferences.quotePreference && preferences.quotePreference !== "auto") { + return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; } - - export function getSnapshotText(snap: ts.IScriptSnapshot): string { - return snap.getText(0, snap.getLength()); + else { + // ignore synthetic import added when importHelpers: true + const firstModuleSpecifier = sourceFile.imports && + ts.find(sourceFile.imports, n => ts.isStringLiteral(n) && !ts.nodeIsSynthesized(n.parent)) as ts.StringLiteral; + return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double; } +} - export function repeatString(str: string, count: number): string { - let result = ""; - for (let i = 0; i < count; i++) { - result += str; - } - return result; +export function getQuoteFromPreference(qp: QuotePreference): string { + switch (qp) { + case QuotePreference.Single: return "'"; + case QuotePreference.Double: return '"'; + default: return ts.Debug.assertNever(qp); } +} - export function skipConstraint(type: ts.Type): ts.Type { - return type.isTypeParameter() ? type.getConstraint() || type : type; - } +export function symbolNameNoDefault(symbol: ts.Symbol): string | undefined { + const escaped = symbolEscapedNameNoDefault(symbol); + return escaped === undefined ? undefined : ts.unescapeLeadingUnderscores(escaped); +} - export function getNameFromPropertyName(name: ts.PropertyName): string | undefined { - return name.kind === ts.SyntaxKind.ComputedPropertyName - // treat computed property names where expression is string/numeric literal as just string/numeric literal - ? ts.isStringOrNumericLiteralLike(name.expression) ? name.expression.text : undefined - : ts.isPrivateIdentifier(name) ? ts.idText(name) : ts.getTextOfIdentifierOrLiteral(name); +export function symbolEscapedNameNoDefault(symbol: ts.Symbol): ts.__String | undefined { + if (symbol.escapedName !== ts.InternalSymbolName.Default) { + return symbol.escapedName; } - export function programContainsModules(program: ts.Program): boolean { - return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!(s.externalModuleIndicator || s.commonJsModuleIndicator)); - } - export function programContainsEsModules(program: ts.Program): boolean { - return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator); - } - export function compilerOptionsIndicateEsModules(compilerOptions: ts.CompilerOptions): boolean { - return !!compilerOptions.module || ts.getEmitScriptTarget(compilerOptions) >= ts.ScriptTarget.ES2015 || !!compilerOptions.noEmit; - } + return ts.firstDefined(symbol.declarations, decl => { + const name = ts.getNameOfDeclaration(decl); + return name && name.kind === ts.SyntaxKind.Identifier ? name.escapedText : undefined; + }); +} - export function createModuleSpecifierResolutionHost(program: ts.Program, host: ts.LanguageServiceHost): ts.ModuleSpecifierResolutionHost { - // Mix in `getSymlinkCache` from Program when host doesn't have it - // in order for non-Project hosts to have a symlinks cache. - return { - fileExists: fileName => program.fileExists(fileName), - getCurrentDirectory: () => host.getCurrentDirectory(), - readFile: ts.maybeBind(host, host.readFile), - useCaseSensitiveFileNames: ts.maybeBind(host, host.useCaseSensitiveFileNames), - getSymlinkCache: ts.maybeBind(host, host.getSymlinkCache) || program.getSymlinkCache, - getModuleSpecifierCache: ts.maybeBind(host, host.getModuleSpecifierCache), - getPackageJsonInfoCache: () => program.getModuleResolutionCache()?.getPackageJsonInfoCache(), - getGlobalTypingsCacheLocation: ts.maybeBind(host, host.getGlobalTypingsCacheLocation), - redirectTargetsMap: program.redirectTargetsMap, - getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName), - isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName), - getNearestAncestorDirectoryWithPackageJson: ts.maybeBind(host, host.getNearestAncestorDirectoryWithPackageJson), - getFileIncludeReasons: () => program.getFileIncludeReasons(), - }; - } +export function isModuleSpecifierLike(node: ts.Node): node is ts.StringLiteralLike { + return ts.isStringLiteralLike(node) && (ts.isExternalModuleReference(node.parent) || + ts.isImportDeclaration(node.parent) || + ts.isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ false) && node.parent.arguments[0] === node || + ts.isImportCall(node.parent) && node.parent.arguments[0] === node); +} +export type ObjectBindingElementWithoutPropertyName = ts.BindingElement & { + name: ts.Identifier; +}; +export function isObjectBindingElementWithoutPropertyName(bindingElement: ts.Node): bindingElement is ObjectBindingElementWithoutPropertyName { + return ts.isBindingElement(bindingElement) && + ts.isObjectBindingPattern(bindingElement.parent) && + ts.isIdentifier(bindingElement.name) && + !bindingElement.propertyName; +} - export function getModuleSpecifierResolverHost(program: ts.Program, host: ts.LanguageServiceHost): ts.SymbolTracker["moduleResolverHost"] { - return { - ...createModuleSpecifierResolutionHost(program, host), - getCommonSourceDirectory: () => program.getCommonSourceDirectory(), - }; - } +export function getPropertySymbolFromBindingElement(checker: ts.TypeChecker, bindingElement: ObjectBindingElementWithoutPropertyName): ts.Symbol | undefined { + const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent); + return typeOfPattern && checker.getPropertyOfType(typeOfPattern, bindingElement.name.text); +} - export function moduleResolutionRespectsExports(moduleResolution: ts.ModuleResolutionKind): boolean { - return moduleResolution >= ts.ModuleResolutionKind.Node16 && moduleResolution <= ts.ModuleResolutionKind.NodeNext; - } +export function getParentNodeInSpan(node: ts.Node | undefined, file: ts.SourceFile, span: ts.TextSpan): ts.Node | undefined { + if (!node) + return undefined; - export function moduleResolutionUsesNodeModules(moduleResolution: ts.ModuleResolutionKind): boolean { - return moduleResolution === ts.ModuleResolutionKind.NodeJs || moduleResolution >= ts.ModuleResolutionKind.Node16 && moduleResolution <= ts.ModuleResolutionKind.NodeNext; - } + while (node.parent) { + if (ts.isSourceFile(node.parent) || !spanContainsNode(span, node.parent, file)) { + return node; + } - export function makeImportIfNecessary(defaultImport: ts.Identifier | undefined, namedImports: readonly ts.ImportSpecifier[] | undefined, moduleSpecifier: string, quotePreference: QuotePreference): ts.ImportDeclaration | undefined { - return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, quotePreference) : undefined; + node = node.parent; } +} - export function makeImport(defaultImport: ts.Identifier | undefined, namedImports: readonly ts.ImportSpecifier[] | undefined, moduleSpecifier: string | ts.Expression, quotePreference: QuotePreference, isTypeOnly?: boolean): ts.ImportDeclaration { - return ts.factory.createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, defaultImport || namedImports - ? ts.factory.createImportClause(!!isTypeOnly, defaultImport, namedImports && namedImports.length ? ts.factory.createNamedImports(namedImports) : undefined) - : undefined, typeof moduleSpecifier === "string" ? makeStringLiteral(moduleSpecifier, quotePreference) : moduleSpecifier, - /*assertClause*/ undefined); - } +function spanContainsNode(span: ts.TextSpan, node: ts.Node, file: ts.SourceFile): boolean { + return ts.textSpanContainsPosition(span, node.getStart(file)) && + node.getEnd() <= ts.textSpanEnd(span); +} - export function makeStringLiteral(text: string, quotePreference: QuotePreference): ts.StringLiteral { - return ts.factory.createStringLiteral(text, quotePreference === QuotePreference.Single); - } - export const enum QuotePreference { - Single, - Double - } +export function findModifier(node: ts.Node, kind: ts.Modifier["kind"]): ts.Modifier | undefined { + return node.modifiers && ts.find(node.modifiers, m => m.kind === kind); +} - export function quotePreferenceFromString(str: ts.StringLiteral, sourceFile: ts.SourceFile): QuotePreference { - return ts.isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single; +export function insertImports(changes: ts.textChanges.ChangeTracker, sourceFile: ts.SourceFile, imports: ts.AnyImportOrRequireStatement | readonly ts.AnyImportOrRequireStatement[], blankLineBetween: boolean): void { + const decl = ts.isArray(imports) ? imports[0] : imports; + const importKindPredicate: (node: ts.Node) => node is ts.AnyImportOrRequireStatement = decl.kind === ts.SyntaxKind.VariableStatement ? ts.isRequireVariableStatement : ts.isAnyImportSyntax; + const existingImportStatements = ts.filter(sourceFile.statements, importKindPredicate); + const sortedNewImports = ts.isArray(imports) ? ts.stableSort(imports, ts.OrganizeImports.compareImportsOrRequireStatements) : [imports]; + if (!existingImportStatements.length) { + changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); + } + else if (existingImportStatements && ts.OrganizeImports.importsAreSorted(existingImportStatements)) { + for (const newImport of sortedNewImports) { + const insertionIndex = ts.OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport); + if (insertionIndex === 0) { + // If the first import is top-of-file, insert after the leading comment which is likely the header. + const options = existingImportStatements[0] === sourceFile.statements[0] ? + { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude } : {}; + changes.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); + } + else { + const prevImport = existingImportStatements[insertionIndex - 1]; + changes.insertNodeAfter(sourceFile, prevImport, newImport); + } + } } - - export function getQuotePreference(sourceFile: ts.SourceFile, preferences: ts.UserPreferences): QuotePreference { - if (preferences.quotePreference && preferences.quotePreference !== "auto") { - return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; + else { + const lastExistingImport = ts.lastOrUndefined(existingImportStatements); + if (lastExistingImport) { + changes.insertNodesAfter(sourceFile, lastExistingImport, sortedNewImports); } else { - // ignore synthetic import added when importHelpers: true - const firstModuleSpecifier = sourceFile.imports && - ts.find(sourceFile.imports, n => ts.isStringLiteral(n) && !ts.nodeIsSynthesized(n.parent)) as ts.StringLiteral; - return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double; + changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } } +} - export function getQuoteFromPreference(qp: QuotePreference): string { - switch (qp) { - case QuotePreference.Single: return "'"; - case QuotePreference.Double: return '"'; - default: return ts.Debug.assertNever(qp); - } - } +export function getTypeKeywordOfTypeOnlyImport(importClause: ts.ImportClause, sourceFile: ts.SourceFile): ts.Token { + ts.Debug.assert(importClause.isTypeOnly); + return ts.cast(importClause.getChildAt(0, sourceFile), isTypeKeywordToken); +} - export function symbolNameNoDefault(symbol: ts.Symbol): string | undefined { - const escaped = symbolEscapedNameNoDefault(symbol); - return escaped === undefined ? undefined : ts.unescapeLeadingUnderscores(escaped); - } +export function textSpansEqual(a: ts.TextSpan | undefined, b: ts.TextSpan | undefined): boolean { + return !!a && !!b && a.start === b.start && a.length === b.length; +} +export function documentSpansEqual(a: ts.DocumentSpan, b: ts.DocumentSpan): boolean { + return a.fileName === b.fileName && textSpansEqual(a.textSpan, b.textSpan); +} - export function symbolEscapedNameNoDefault(symbol: ts.Symbol): ts.__String | undefined { - if (symbol.escapedName !== ts.InternalSymbolName.Default) { - return symbol.escapedName; +/** + * 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; + } + } } - - return ts.firstDefined(symbol.declarations, decl => { - const name = ts.getNameOfDeclaration(decl); - return name && name.kind === ts.SyntaxKind.Identifier ? name.escapedText : undefined; - }); } + return undefined; +} - export function isModuleSpecifierLike(node: ts.Node): node is ts.StringLiteralLike { - return ts.isStringLiteralLike(node) && (ts.isExternalModuleReference(node.parent) || - ts.isImportDeclaration(node.parent) || - ts.isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ false) && node.parent.arguments[0] === node || - ts.isImportCall(node.parent) && node.parent.arguments[0] === node); - } - export type ObjectBindingElementWithoutPropertyName = ts.BindingElement & { - name: ts.Identifier; - }; - export function isObjectBindingElementWithoutPropertyName(bindingElement: ts.Node): bindingElement is ObjectBindingElementWithoutPropertyName { - return ts.isBindingElement(bindingElement) && - ts.isObjectBindingPattern(bindingElement.parent) && - ts.isIdentifier(bindingElement.name) && - !bindingElement.propertyName; +export function isTextWhiteSpaceLike(text: string, startPos: number, endPos: number): boolean { + for (let i = startPos; i < endPos; i++) { + if (!ts.isWhiteSpaceLike(text.charCodeAt(i))) { + return false; + } } - export function getPropertySymbolFromBindingElement(checker: ts.TypeChecker, bindingElement: ObjectBindingElementWithoutPropertyName): ts.Symbol | undefined { - const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent); - return typeOfPattern && checker.getPropertyOfType(typeOfPattern, bindingElement.name.text); - } + return true; +} - export function getParentNodeInSpan(node: ts.Node | undefined, file: ts.SourceFile, span: ts.TextSpan): ts.Node | undefined { - if (!node) - return undefined; +// #endregion - while (node.parent) { - if (ts.isSourceFile(node.parent) || !spanContainsNode(span, node.parent, file)) { - return node; - } +// Display-part writer helpers +// #region +export function isFirstDeclarationOfSymbolParameter(symbol: ts.Symbol) { + const declaration = symbol.declarations ? ts.firstOrUndefined(symbol.declarations) : undefined; + return !!ts.findAncestor(declaration, n => ts.isParameter(n) ? true : ts.isBindingElement(n) || ts.isObjectBindingPattern(n) || ts.isArrayBindingPattern(n) ? false : "quit"); +} + +const displayPartWriter = getDisplayPartWriter(); +function getDisplayPartWriter(): ts.DisplayPartsSymbolWriter { + const absoluteMaximumLength = ts.defaultMaximumTruncationLength * 10; // A hard cutoff to avoid overloading the messaging channel in worst-case scenarios + let displayParts: ts.SymbolDisplayPart[]; + let lineStart: boolean; + let indent: number; + let length: number; + + resetWriter(); + const unknownWrite = (text: string) => writeKind(text, ts.SymbolDisplayPartKind.text); + return { + displayParts: () => { + const finalText = displayParts.length && displayParts[displayParts.length - 1].text; + if (length > absoluteMaximumLength && finalText && finalText !== "...") { + if (!ts.isWhiteSpaceLike(finalText.charCodeAt(finalText.length - 1))) { + displayParts.push(displayPart(" ", ts.SymbolDisplayPartKind.space)); + } + displayParts.push(displayPart("...", ts.SymbolDisplayPartKind.punctuation)); + } + return displayParts; + }, + writeKeyword: text => writeKind(text, ts.SymbolDisplayPartKind.keyword), + writeOperator: text => writeKind(text, ts.SymbolDisplayPartKind.operator), + writePunctuation: text => writeKind(text, ts.SymbolDisplayPartKind.punctuation), + writeTrailingSemicolon: text => writeKind(text, ts.SymbolDisplayPartKind.punctuation), + writeSpace: text => writeKind(text, ts.SymbolDisplayPartKind.space), + writeStringLiteral: text => writeKind(text, ts.SymbolDisplayPartKind.stringLiteral), + writeParameter: text => writeKind(text, ts.SymbolDisplayPartKind.parameterName), + writeProperty: text => writeKind(text, ts.SymbolDisplayPartKind.propertyName), + writeLiteral: text => writeKind(text, ts.SymbolDisplayPartKind.stringLiteral), + writeSymbol, + writeLine, + write: unknownWrite, + writeComment: unknownWrite, + getText: () => "", + getTextPos: () => 0, + getColumn: () => 0, + getLine: () => 0, + isAtStartOfLine: () => false, + hasTrailingWhitespace: () => false, + hasTrailingComment: () => false, + rawWrite: ts.notImplemented, + getIndent: () => indent, + increaseIndent: () => { indent++; }, + decreaseIndent: () => { indent--; }, + clear: resetWriter, + trackSymbol: () => false, + reportInaccessibleThisError: ts.noop, + reportInaccessibleUniqueSymbolError: ts.noop, + reportPrivateInBaseOfClassExpression: ts.noop, + }; - node = node.parent; + function writeIndent() { + if (length > absoluteMaximumLength) + return; + if (lineStart) { + const indentString = ts.getIndentString(indent); + if (indentString) { + length += indentString.length; + displayParts.push(displayPart(indentString, ts.SymbolDisplayPartKind.space)); + } + lineStart = false; } } - function spanContainsNode(span: ts.TextSpan, node: ts.Node, file: ts.SourceFile): boolean { - return ts.textSpanContainsPosition(span, node.getStart(file)) && - node.getEnd() <= ts.textSpanEnd(span); + function writeKind(text: string, kind: ts.SymbolDisplayPartKind) { + if (length > absoluteMaximumLength) + return; + writeIndent(); + length += text.length; + displayParts.push(displayPart(text, kind)); } - export function findModifier(node: ts.Node, kind: ts.Modifier["kind"]): ts.Modifier | undefined { - return node.modifiers && ts.find(node.modifiers, m => m.kind === kind); + function writeSymbol(text: string, symbol: ts.Symbol) { + if (length > absoluteMaximumLength) + return; + writeIndent(); + length += text.length; + displayParts.push(symbolPart(text, symbol)); } - export function insertImports(changes: ts.textChanges.ChangeTracker, sourceFile: ts.SourceFile, imports: ts.AnyImportOrRequireStatement | readonly ts.AnyImportOrRequireStatement[], blankLineBetween: boolean): void { - const decl = ts.isArray(imports) ? imports[0] : imports; - const importKindPredicate: (node: ts.Node) => node is ts.AnyImportOrRequireStatement = decl.kind === ts.SyntaxKind.VariableStatement ? ts.isRequireVariableStatement : ts.isAnyImportSyntax; - const existingImportStatements = ts.filter(sourceFile.statements, importKindPredicate); - const sortedNewImports = ts.isArray(imports) ? ts.stableSort(imports, ts.OrganizeImports.compareImportsOrRequireStatements) : [imports]; - if (!existingImportStatements.length) { - changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); - } - else if (existingImportStatements && ts.OrganizeImports.importsAreSorted(existingImportStatements)) { - for (const newImport of sortedNewImports) { - const insertionIndex = ts.OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport); - if (insertionIndex === 0) { - // If the first import is top-of-file, insert after the leading comment which is likely the header. - const options = existingImportStatements[0] === sourceFile.statements[0] ? - { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude } : {}; - changes.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); - } - else { - const prevImport = existingImportStatements[insertionIndex - 1]; - changes.insertNodeAfter(sourceFile, prevImport, newImport); - } - } - } - else { - const lastExistingImport = ts.lastOrUndefined(existingImportStatements); - if (lastExistingImport) { - changes.insertNodesAfter(sourceFile, lastExistingImport, sortedNewImports); - } - else { - changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); - } - } + function writeLine() { + if (length > absoluteMaximumLength) + return; + length += 1; + displayParts.push(lineBreakPart()); + lineStart = true; } - export function getTypeKeywordOfTypeOnlyImport(importClause: ts.ImportClause, sourceFile: ts.SourceFile): ts.Token { - ts.Debug.assert(importClause.isTypeOnly); - return ts.cast(importClause.getChildAt(0, sourceFile), isTypeKeywordToken); + function resetWriter() { + displayParts = []; + lineStart = true; + indent = 0; + length = 0; } +} - export function textSpansEqual(a: ts.TextSpan | undefined, b: ts.TextSpan | undefined): boolean { - return !!a && !!b && a.start === b.start && a.length === b.length; - } - export function documentSpansEqual(a: ts.DocumentSpan, b: ts.DocumentSpan): boolean { - return a.fileName === b.fileName && textSpansEqual(a.textSpan, b.textSpan); +export function symbolPart(text: string, symbol: ts.Symbol) { + return displayPart(text, displayPartKind(symbol)); + + function displayPartKind(symbol: ts.Symbol): ts.SymbolDisplayPartKind { + const flags = symbol.flags; + + if (flags & ts.SymbolFlags.Variable) { + return isFirstDeclarationOfSymbolParameter(symbol) ? ts.SymbolDisplayPartKind.parameterName : ts.SymbolDisplayPartKind.localName; + } + if (flags & ts.SymbolFlags.Property) + return ts.SymbolDisplayPartKind.propertyName; + if (flags & ts.SymbolFlags.GetAccessor) + return ts.SymbolDisplayPartKind.propertyName; + if (flags & ts.SymbolFlags.SetAccessor) + return ts.SymbolDisplayPartKind.propertyName; + if (flags & ts.SymbolFlags.EnumMember) + return ts.SymbolDisplayPartKind.enumMemberName; + if (flags & ts.SymbolFlags.Function) + return ts.SymbolDisplayPartKind.functionName; + if (flags & ts.SymbolFlags.Class) + return ts.SymbolDisplayPartKind.className; + if (flags & ts.SymbolFlags.Interface) + return ts.SymbolDisplayPartKind.interfaceName; + if (flags & ts.SymbolFlags.Enum) + return ts.SymbolDisplayPartKind.enumName; + if (flags & ts.SymbolFlags.Module) + return ts.SymbolDisplayPartKind.moduleName; + if (flags & ts.SymbolFlags.Method) + return ts.SymbolDisplayPartKind.methodName; + if (flags & ts.SymbolFlags.TypeParameter) + return ts.SymbolDisplayPartKind.typeParameterName; + if (flags & ts.SymbolFlags.TypeAlias) + return ts.SymbolDisplayPartKind.aliasName; + if (flags & ts.SymbolFlags.Alias) + return ts.SymbolDisplayPartKind.aliasName; + return ts.SymbolDisplayPartKind.text; } +} +export function displayPart(text: string, kind: ts.SymbolDisplayPartKind): ts.SymbolDisplayPart { + return { text, kind: ts.SymbolDisplayPartKind[kind] }; +} - /** - * 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; - } - } - } - } - return undefined; - } +export function spacePart() { + return displayPart(" ", ts.SymbolDisplayPartKind.space); +} - export function isTextWhiteSpaceLike(text: string, startPos: number, endPos: number): boolean { - for (let i = startPos; i < endPos; i++) { - if (!ts.isWhiteSpaceLike(text.charCodeAt(i))) { - return false; - } - } +export function keywordPart(kind: ts.SyntaxKind) { + return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.keyword); +} - return true; - } +export function punctuationPart(kind: ts.SyntaxKind) { + return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.punctuation); +} - // #endregion +export function operatorPart(kind: ts.SyntaxKind) { + return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.operator); +} - // Display-part writer helpers - // #region - export function isFirstDeclarationOfSymbolParameter(symbol: ts.Symbol) { - const declaration = symbol.declarations ? ts.firstOrUndefined(symbol.declarations) : undefined; - return !!ts.findAncestor(declaration, n => ts.isParameter(n) ? true : ts.isBindingElement(n) || ts.isObjectBindingPattern(n) || ts.isArrayBindingPattern(n) ? false : "quit"); - } +export function parameterNamePart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.parameterName); +} - const displayPartWriter = getDisplayPartWriter(); - function getDisplayPartWriter(): ts.DisplayPartsSymbolWriter { - const absoluteMaximumLength = ts.defaultMaximumTruncationLength * 10; // A hard cutoff to avoid overloading the messaging channel in worst-case scenarios - let displayParts: ts.SymbolDisplayPart[]; - let lineStart: boolean; - let indent: number; - let length: number; +export function propertyNamePart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.propertyName); +} - resetWriter(); - const unknownWrite = (text: string) => writeKind(text, ts.SymbolDisplayPartKind.text); - return { - displayParts: () => { - const finalText = displayParts.length && displayParts[displayParts.length - 1].text; - if (length > absoluteMaximumLength && finalText && finalText !== "...") { - if (!ts.isWhiteSpaceLike(finalText.charCodeAt(finalText.length - 1))) { - displayParts.push(displayPart(" ", ts.SymbolDisplayPartKind.space)); - } - displayParts.push(displayPart("...", ts.SymbolDisplayPartKind.punctuation)); - } - return displayParts; - }, - writeKeyword: text => writeKind(text, ts.SymbolDisplayPartKind.keyword), - writeOperator: text => writeKind(text, ts.SymbolDisplayPartKind.operator), - writePunctuation: text => writeKind(text, ts.SymbolDisplayPartKind.punctuation), - writeTrailingSemicolon: text => writeKind(text, ts.SymbolDisplayPartKind.punctuation), - writeSpace: text => writeKind(text, ts.SymbolDisplayPartKind.space), - writeStringLiteral: text => writeKind(text, ts.SymbolDisplayPartKind.stringLiteral), - writeParameter: text => writeKind(text, ts.SymbolDisplayPartKind.parameterName), - writeProperty: text => writeKind(text, ts.SymbolDisplayPartKind.propertyName), - writeLiteral: text => writeKind(text, ts.SymbolDisplayPartKind.stringLiteral), - writeSymbol, - writeLine, - write: unknownWrite, - writeComment: unknownWrite, - getText: () => "", - getTextPos: () => 0, - getColumn: () => 0, - getLine: () => 0, - isAtStartOfLine: () => false, - hasTrailingWhitespace: () => false, - hasTrailingComment: () => false, - rawWrite: ts.notImplemented, - getIndent: () => indent, - increaseIndent: () => { indent++; }, - decreaseIndent: () => { indent--; }, - clear: resetWriter, - trackSymbol: () => false, - reportInaccessibleThisError: ts.noop, - reportInaccessibleUniqueSymbolError: ts.noop, - reportPrivateInBaseOfClassExpression: ts.noop, - }; - - function writeIndent() { - if (length > absoluteMaximumLength) - return; - if (lineStart) { - const indentString = ts.getIndentString(indent); - if (indentString) { - length += indentString.length; - displayParts.push(displayPart(indentString, ts.SymbolDisplayPartKind.space)); - } - lineStart = false; - } - } +export function textOrKeywordPart(text: string) { + const kind = ts.stringToToken(text); + return kind === undefined + ? textPart(text) + : keywordPart(kind); +} - function writeKind(text: string, kind: ts.SymbolDisplayPartKind) { - if (length > absoluteMaximumLength) - return; - writeIndent(); - length += text.length; - displayParts.push(displayPart(text, kind)); - } +export function textPart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.text); +} - function writeSymbol(text: string, symbol: ts.Symbol) { - if (length > absoluteMaximumLength) - return; - writeIndent(); - length += text.length; - displayParts.push(symbolPart(text, symbol)); - } +export function typeAliasNamePart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.aliasName); +} - function writeLine() { - if (length > absoluteMaximumLength) - return; - length += 1; - displayParts.push(lineBreakPart()); - lineStart = true; - } +export function typeParameterNamePart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.typeParameterName); +} - function resetWriter() { - displayParts = []; - lineStart = true; - indent = 0; - length = 0; - } - } +export function linkTextPart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.linkText); +} - export function symbolPart(text: string, symbol: ts.Symbol) { - return displayPart(text, displayPartKind(symbol)); +export function linkNamePart(text: string, target: ts.Declaration): ts.JSDocLinkDisplayPart { + return { + text, + kind: ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.linkName], + target: { + fileName: ts.getSourceFileOfNode(target).fileName, + textSpan: createTextSpanFromNode(target), + }, + }; +} - function displayPartKind(symbol: ts.Symbol): ts.SymbolDisplayPartKind { - const flags = symbol.flags; +export function linkPart(text: string) { + return displayPart(text, ts.SymbolDisplayPartKind.link); +} - if (flags & ts.SymbolFlags.Variable) { - return isFirstDeclarationOfSymbolParameter(symbol) ? ts.SymbolDisplayPartKind.parameterName : ts.SymbolDisplayPartKind.localName; - } - if (flags & ts.SymbolFlags.Property) - return ts.SymbolDisplayPartKind.propertyName; - if (flags & ts.SymbolFlags.GetAccessor) - return ts.SymbolDisplayPartKind.propertyName; - if (flags & ts.SymbolFlags.SetAccessor) - return ts.SymbolDisplayPartKind.propertyName; - if (flags & ts.SymbolFlags.EnumMember) - return ts.SymbolDisplayPartKind.enumMemberName; - if (flags & ts.SymbolFlags.Function) - return ts.SymbolDisplayPartKind.functionName; - if (flags & ts.SymbolFlags.Class) - return ts.SymbolDisplayPartKind.className; - if (flags & ts.SymbolFlags.Interface) - return ts.SymbolDisplayPartKind.interfaceName; - if (flags & ts.SymbolFlags.Enum) - return ts.SymbolDisplayPartKind.enumName; - if (flags & ts.SymbolFlags.Module) - return ts.SymbolDisplayPartKind.moduleName; - if (flags & ts.SymbolFlags.Method) - return ts.SymbolDisplayPartKind.methodName; - if (flags & ts.SymbolFlags.TypeParameter) - return ts.SymbolDisplayPartKind.typeParameterName; - if (flags & ts.SymbolFlags.TypeAlias) - return ts.SymbolDisplayPartKind.aliasName; - if (flags & ts.SymbolFlags.Alias) - return ts.SymbolDisplayPartKind.aliasName; - return ts.SymbolDisplayPartKind.text; +export function buildLinkParts(link: ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain, checker?: ts.TypeChecker): ts.SymbolDisplayPart[] { + const prefix = ts.isJSDocLink(link) ? "link" + : ts.isJSDocLinkCode(link) ? "linkcode" + : "linkplain"; + const parts = [linkPart(`{@${prefix} `)]; + if (!link.name) { + if (link.text) { + parts.push(linkTextPart(link.text)); + } + } + else { + const symbol = checker?.getSymbolAtLocation(link.name); + const suffix = findLinkNameEnd(link.text); + const name = ts.getTextOfNode(link.name) + link.text.slice(0, suffix); + const text = skipSeparatorFromLinkText(link.text.slice(suffix)); + const decl = symbol?.valueDeclaration || symbol?.declarations?.[0]; + if (decl) { + parts.push(linkNamePart(name, decl)); + if (text) + parts.push(linkTextPart(text)); + } + else { + parts.push(linkTextPart(name + (suffix || text.indexOf("://") === 0 ? "" : " ") + text)); } } - export function displayPart(text: string, kind: ts.SymbolDisplayPartKind): ts.SymbolDisplayPart { - return { text, kind: ts.SymbolDisplayPartKind[kind] }; - } - - export function spacePart() { - return displayPart(" ", ts.SymbolDisplayPartKind.space); - } + parts.push(linkPart("}")); + return parts; +} - export function keywordPart(kind: ts.SyntaxKind) { - return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.keyword); +function skipSeparatorFromLinkText(text: string) { + let pos = 0; + if (text.charCodeAt(pos++) === ts.CharacterCodes.bar) { + while (pos < text.length && text.charCodeAt(pos) === ts.CharacterCodes.space) + pos++; + return text.slice(pos); } + return text; +} - export function punctuationPart(kind: ts.SyntaxKind) { - return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.punctuation); - } +function findLinkNameEnd(text: string) { + if (text.indexOf("()") === 0) + return 2; + if (text[0] !== "<") + return 0; + let brackets = 0; + let i = 0; + while (i < text.length) { + if (text[i] === "<") + brackets++; + if (text[i] === ">") + brackets--; + i++; + if (!brackets) + return i; + } + return 0; +} - export function operatorPart(kind: ts.SyntaxKind) { - return displayPart(ts.tokenToString(kind)!, ts.SymbolDisplayPartKind.operator); - } +const carriageReturnLineFeed = "\r\n"; +/** + * The default is CRLF. + */ +export function getNewLineOrDefaultFromHost(host: ts.FormattingHost, formatSettings?: ts.FormatCodeSettings) { + return formatSettings?.newLineCharacter || + host.getNewLine?.() || + carriageReturnLineFeed; +} - export function parameterNamePart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.parameterName); - } +export function lineBreakPart() { + return displayPart("\n", ts.SymbolDisplayPartKind.lineBreak); +} - export function propertyNamePart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.propertyName); +export function mapToDisplayParts(writeDisplayParts: (writer: ts.DisplayPartsSymbolWriter) => void): ts.SymbolDisplayPart[] { + try { + writeDisplayParts(displayPartWriter); + return displayPartWriter.displayParts(); } - - export function textOrKeywordPart(text: string) { - const kind = ts.stringToToken(text); - return kind === undefined - ? textPart(text) - : keywordPart(kind); + finally { + displayPartWriter.clear(); } +} - export function textPart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.text); - } +export function typeToDisplayParts(typechecker: ts.TypeChecker, type: ts.Type, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.None): ts.SymbolDisplayPart[] { + return mapToDisplayParts(writer => { + typechecker.writeType(type, enclosingDeclaration, flags | ts.TypeFormatFlags.MultilineObjectLiterals | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer); + }); +} - export function typeAliasNamePart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.aliasName); - } +export function symbolToDisplayParts(typeChecker: ts.TypeChecker, symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags, flags: ts.SymbolFormatFlags = ts.SymbolFormatFlags.None): ts.SymbolDisplayPart[] { + return mapToDisplayParts(writer => { + typeChecker.writeSymbol(symbol, enclosingDeclaration, meaning, flags | ts.SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope, writer); + }); +} - export function typeParameterNamePart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.typeParameterName); - } +export function signatureToDisplayParts(typechecker: ts.TypeChecker, signature: ts.Signature, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.None): ts.SymbolDisplayPart[] { + flags |= ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.MultilineObjectLiterals | ts.TypeFormatFlags.WriteTypeArgumentsOfSignature | ts.TypeFormatFlags.OmitParameterModifiers; + return mapToDisplayParts(writer => { + typechecker.writeSignature(signature, enclosingDeclaration, flags, /*signatureKind*/ undefined, writer); + }); +} - export function linkTextPart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.linkText); - } +export function nodeToDisplayParts(node: ts.Node, enclosingDeclaration: ts.Node): ts.SymbolDisplayPart[] { + const file = enclosingDeclaration.getSourceFile(); + return mapToDisplayParts(writer => { + const printer = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true }); + printer.writeNode(ts.EmitHint.Unspecified, node, file, writer); + }); +} - export function linkNamePart(text: string, target: ts.Declaration): ts.JSDocLinkDisplayPart { - return { - text, - kind: ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.linkName], - target: { - fileName: ts.getSourceFileOfNode(target).fileName, - textSpan: createTextSpanFromNode(target), - }, - }; - } +export function isImportOrExportSpecifierName(location: ts.Node): location is ts.Identifier { + return !!location.parent && ts.isImportOrExportSpecifier(location.parent) && location.parent.propertyName === location; +} - export function linkPart(text: string) { - return displayPart(text, ts.SymbolDisplayPartKind.link); - } +export function getScriptKind(fileName: string, host: ts.LanguageServiceHost): ts.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 ts.ensureScriptKind(fileName, host.getScriptKind && host.getScriptKind(fileName)); +} - export function buildLinkParts(link: ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain, checker?: ts.TypeChecker): ts.SymbolDisplayPart[] { - const prefix = ts.isJSDocLink(link) ? "link" - : ts.isJSDocLinkCode(link) ? "linkcode" - : "linkplain"; - const parts = [linkPart(`{@${prefix} `)]; - if (!link.name) { - if (link.text) { - parts.push(linkTextPart(link.text)); - } +export function getSymbolTarget(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol { + let next: ts.Symbol = symbol; + while (isAliasSymbol(next) || (isTransientSymbol(next) && next.target)) { + if (isTransientSymbol(next) && next.target) { + next = next.target; } else { - const symbol = checker?.getSymbolAtLocation(link.name); - const suffix = findLinkNameEnd(link.text); - const name = ts.getTextOfNode(link.name) + link.text.slice(0, suffix); - const text = skipSeparatorFromLinkText(link.text.slice(suffix)); - const decl = symbol?.valueDeclaration || symbol?.declarations?.[0]; - if (decl) { - parts.push(linkNamePart(name, decl)); - if (text) - parts.push(linkTextPart(text)); - } - else { - parts.push(linkTextPart(name + (suffix || text.indexOf("://") === 0 ? "" : " ") + text)); - } + next = ts.skipAlias(next, checker); } - parts.push(linkPart("}")); - return parts; } + return next; +} - function skipSeparatorFromLinkText(text: string) { - let pos = 0; - if (text.charCodeAt(pos++) === ts.CharacterCodes.bar) { - while (pos < text.length && text.charCodeAt(pos) === ts.CharacterCodes.space) - pos++; - return text.slice(pos); - } - return text; - } +function isTransientSymbol(symbol: ts.Symbol): symbol is ts.TransientSymbol { + return (symbol.flags & ts.SymbolFlags.Transient) !== 0; +} - function findLinkNameEnd(text: string) { - if (text.indexOf("()") === 0) - return 2; - if (text[0] !== "<") - return 0; - let brackets = 0; - let i = 0; - while (i < text.length) { - if (text[i] === "<") - brackets++; - if (text[i] === ">") - brackets--; - i++; - if (!brackets) - return i; - } - return 0; - } +function isAliasSymbol(symbol: ts.Symbol): boolean { + return (symbol.flags & ts.SymbolFlags.Alias) !== 0; +} - const carriageReturnLineFeed = "\r\n"; - /** - * The default is CRLF. - */ - export function getNewLineOrDefaultFromHost(host: ts.FormattingHost, formatSettings?: ts.FormatCodeSettings) { - return formatSettings?.newLineCharacter || - host.getNewLine?.() || - carriageReturnLineFeed; - } +export function getUniqueSymbolId(symbol: ts.Symbol, checker: ts.TypeChecker) { + return ts.getSymbolId(ts.skipAlias(symbol, checker)); +} - export function lineBreakPart() { - return displayPart("\n", ts.SymbolDisplayPartKind.lineBreak); +export function getFirstNonSpaceCharacterPosition(text: string, position: number) { + while (ts.isWhiteSpaceLike(text.charCodeAt(position))) { + position += 1; } + return position; +} - export function mapToDisplayParts(writeDisplayParts: (writer: ts.DisplayPartsSymbolWriter) => void): ts.SymbolDisplayPart[] { - try { - writeDisplayParts(displayPartWriter); - return displayPartWriter.displayParts(); - } - finally { - displayPartWriter.clear(); - } +export function getPrecedingNonSpaceCharacterPosition(text: string, position: number) { + while (position > -1 && ts.isWhiteSpaceSingleLine(text.charCodeAt(position))) { + position -= 1; } + return position + 1; +} - export function typeToDisplayParts(typechecker: ts.TypeChecker, type: ts.Type, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.None): ts.SymbolDisplayPart[] { - return mapToDisplayParts(writer => { - typechecker.writeType(type, enclosingDeclaration, flags | ts.TypeFormatFlags.MultilineObjectLiterals | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer); - }); - } +/** + * 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 symbolToDisplayParts(typeChecker: ts.TypeChecker, symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags, flags: ts.SymbolFormatFlags = ts.SymbolFormatFlags.None): ts.SymbolDisplayPart[] { - return mapToDisplayParts(writer => { - typeChecker.writeSymbol(symbol, enclosingDeclaration, meaning, flags | ts.SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope, writer); - }); +export function getSynthesizedDeepCloneWithReplacements(node: T, includeTrivia: boolean, replaceNode: (node: ts.Node) => ts.Node | undefined): T { + let clone = replaceNode(node); + if (clone) { + ts.setOriginalNode(clone, node); } - - export function signatureToDisplayParts(typechecker: ts.TypeChecker, signature: ts.Signature, enclosingDeclaration?: ts.Node, flags: ts.TypeFormatFlags = ts.TypeFormatFlags.None): ts.SymbolDisplayPart[] { - flags |= ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.MultilineObjectLiterals | ts.TypeFormatFlags.WriteTypeArgumentsOfSignature | ts.TypeFormatFlags.OmitParameterModifiers; - return mapToDisplayParts(writer => { - typechecker.writeSignature(signature, enclosingDeclaration, flags, /*signatureKind*/ undefined, writer); - }); + else { + clone = getSynthesizedDeepCloneWorker(node as NonNullable, replaceNode); } - export function nodeToDisplayParts(node: ts.Node, enclosingDeclaration: ts.Node): ts.SymbolDisplayPart[] { - const file = enclosingDeclaration.getSourceFile(); - return mapToDisplayParts(writer => { - const printer = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true }); - printer.writeNode(ts.EmitHint.Unspecified, node, file, writer); - }); - } + if (clone && !includeTrivia) + suppressLeadingAndTrailingTrivia(clone); + return clone as T; +} - export function isImportOrExportSpecifierName(location: ts.Node): location is ts.Identifier { - return !!location.parent && ts.isImportOrExportSpecifier(location.parent) && location.parent.propertyName === location; - } +function getSynthesizedDeepCloneWorker(node: T, replaceNode?: (node: ts.Node) => ts.Node | undefined): T { + const nodeClone: (n: T) => T = replaceNode + ? n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode) + : getSynthesizedDeepClone; + const nodesClone: (ns: ts.NodeArray) => ts.NodeArray = replaceNode + ? ns => ns && getSynthesizedDeepClonesWithReplacements(ns, /*includeTrivia*/ true, replaceNode) + : ns => ns && getSynthesizedDeepClones(ns); + const visited = ts.visitEachChild(node, nodeClone, ts.nullTransformationContext, nodesClone, nodeClone); + + if (visited === node) { + // This only happens for leaf nodes - internal nodes always see their children change. + const clone = ts.isStringLiteral(node) ? ts.setOriginalNode(ts.factory.createStringLiteralFromNode(node), node) as ts.Node as T : + ts.isNumericLiteral(node) ? ts.setOriginalNode(ts.factory.createNumericLiteral(node.text, node.numericLiteralFlags), node) as ts.Node as T : + ts.factory.cloneNode(node); + return ts.setTextRange(clone, node); + } + + // PERF: As an optimization, rather than calling factory.cloneNode, we'll update + // the new node created by visitEachChild with the extra changes factory.cloneNode + // would have made. + (visited as ts.Mutable).parent = undefined!; + return visited; +} - export function getScriptKind(fileName: string, host: ts.LanguageServiceHost): ts.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 ts.ensureScriptKind(fileName, host.getScriptKind && host.getScriptKind(fileName)); - } +export function getSynthesizedDeepClones(nodes: ts.NodeArray, includeTrivia?: boolean): ts.NodeArray; +export function getSynthesizedDeepClones(nodes: ts.NodeArray | undefined, includeTrivia?: boolean): ts.NodeArray | undefined; +export function getSynthesizedDeepClones(nodes: ts.NodeArray | undefined, includeTrivia = true): ts.NodeArray | undefined { + return nodes && ts.factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); +} +export function getSynthesizedDeepClonesWithReplacements(nodes: ts.NodeArray, includeTrivia: boolean, replaceNode: (node: ts.Node) => ts.Node | undefined): ts.NodeArray { + return ts.factory.createNodeArray(nodes.map(n => getSynthesizedDeepCloneWithReplacements(n, includeTrivia, replaceNode)), nodes.hasTrailingComma); +} - export function getSymbolTarget(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol { - let next: ts.Symbol = symbol; - while (isAliasSymbol(next) || (isTransientSymbol(next) && next.target)) { - if (isTransientSymbol(next) && next.target) { - next = next.target; - } - else { - next = ts.skipAlias(next, checker); - } - } - return next; - } +/** + * Sets EmitFlags to suppress leading and trailing trivia on the node. + */ +export function suppressLeadingAndTrailingTrivia(node: ts.Node) { + suppressLeadingTrivia(node); + suppressTrailingTrivia(node); +} - function isTransientSymbol(symbol: ts.Symbol): symbol is ts.TransientSymbol { - return (symbol.flags & ts.SymbolFlags.Transient) !== 0; - } +/** + * Sets EmitFlags to suppress leading trivia on the node. + */ +export function suppressLeadingTrivia(node: ts.Node) { + addEmitFlagsRecursively(node, ts.EmitFlags.NoLeadingComments, getFirstChild); +} - function isAliasSymbol(symbol: ts.Symbol): boolean { - return (symbol.flags & ts.SymbolFlags.Alias) !== 0; - } +/** + * Sets EmitFlags to suppress trailing trivia on the node. + */ +export function suppressTrailingTrivia(node: ts.Node) { + addEmitFlagsRecursively(node, ts.EmitFlags.NoTrailingComments, ts.getLastChild); +} - export function getUniqueSymbolId(symbol: ts.Symbol, checker: ts.TypeChecker) { - return ts.getSymbolId(ts.skipAlias(symbol, checker)); +export function copyComments(sourceNode: ts.Node, targetNode: ts.Node) { + const sourceFile = sourceNode.getSourceFile(); + const text = sourceFile.text; + if (hasLeadingLineBreak(sourceNode, text)) { + copyLeadingComments(sourceNode, targetNode, sourceFile); } - - export function getFirstNonSpaceCharacterPosition(text: string, position: number) { - while (ts.isWhiteSpaceLike(text.charCodeAt(position))) { - position += 1; - } - return position; + else { + copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile); } + copyTrailingComments(sourceNode, targetNode, sourceFile); +} - export function getPrecedingNonSpaceCharacterPosition(text: string, position: number) { - while (position > -1 && ts.isWhiteSpaceSingleLine(text.charCodeAt(position))) { - position -= 1; - } - return position + 1; +function hasLeadingLineBreak(node: ts.Node, text: string) { + const start = node.getFullStart(); + const end = node.getStart(); + for (let i = start; i < end; i++) { + if (text.charCodeAt(i) === ts.CharacterCodes.lineFeed) + return true; } + return false; +} - /** - * 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; - } +function addEmitFlagsRecursively(node: ts.Node, flag: ts.EmitFlags, getChild: (n: ts.Node) => ts.Node | undefined) { + ts.addEmitFlags(node, flag); + const child = getChild(node); + if (child) + addEmitFlagsRecursively(child, flag, getChild); +} - export function getSynthesizedDeepCloneWithReplacements(node: T, includeTrivia: boolean, replaceNode: (node: ts.Node) => ts.Node | undefined): T { - let clone = replaceNode(node); - if (clone) { - ts.setOriginalNode(clone, node); - } - else { - clone = getSynthesizedDeepCloneWorker(node as NonNullable, replaceNode); - } +function getFirstChild(node: ts.Node): ts.Node | undefined { + return node.forEachChild(child => child); +} - if (clone && !includeTrivia) - suppressLeadingAndTrailingTrivia(clone); - return clone as T; +export function getUniqueName(baseName: string, sourceFile: ts.SourceFile): string { + let nameText = baseName; + for (let i = 1; !ts.isFileLevelUniqueName(sourceFile, nameText); i++) { + nameText = `${baseName}_${i}`; } + return nameText; +} - function getSynthesizedDeepCloneWorker(node: T, replaceNode?: (node: ts.Node) => ts.Node | undefined): T { - const nodeClone: (n: T) => T = replaceNode - ? n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode) - : getSynthesizedDeepClone; - const nodesClone: (ns: ts.NodeArray) => ts.NodeArray = replaceNode - ? ns => ns && getSynthesizedDeepClonesWithReplacements(ns, /*includeTrivia*/ true, replaceNode) - : ns => ns && getSynthesizedDeepClones(ns); - const visited = ts.visitEachChild(node, nodeClone, ts.nullTransformationContext, nodesClone, nodeClone); - - if (visited === node) { - // This only happens for leaf nodes - internal nodes always see their children change. - const clone = ts.isStringLiteral(node) ? ts.setOriginalNode(ts.factory.createStringLiteralFromNode(node), node) as ts.Node as T : - ts.isNumericLiteral(node) ? ts.setOriginalNode(ts.factory.createNumericLiteral(node.text, node.numericLiteralFlags), node) as ts.Node as T : - ts.factory.cloneNode(node); - return ts.setTextRange(clone, node); +/** + * @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 ts.FileTextChanges[], renameFilename: string, name: string, preferLastLocation: boolean): number { + let delta = 0; + let lastPos = -1; + for (const { fileName, textChanges } of edits) { + ts.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; } - - // PERF: As an optimization, rather than calling factory.cloneNode, we'll update - // the new node created by visitEachChild with the extra changes factory.cloneNode - // would have made. - (visited as ts.Mutable).parent = undefined!; - return visited; } - export function getSynthesizedDeepClones(nodes: ts.NodeArray, includeTrivia?: boolean): ts.NodeArray; - export function getSynthesizedDeepClones(nodes: ts.NodeArray | undefined, includeTrivia?: boolean): ts.NodeArray | undefined; - export function getSynthesizedDeepClones(nodes: ts.NodeArray | undefined, includeTrivia = true): ts.NodeArray | undefined { - return nodes && ts.factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); - } - export function getSynthesizedDeepClonesWithReplacements(nodes: ts.NodeArray, includeTrivia: boolean, replaceNode: (node: ts.Node) => ts.Node | undefined): ts.NodeArray { - return ts.factory.createNodeArray(nodes.map(n => getSynthesizedDeepCloneWithReplacements(n, includeTrivia, replaceNode)), nodes.hasTrailingComma); - } + // If the declaration comes first, return the position of the last occurrence. + ts.Debug.assert(preferLastLocation); + ts.Debug.assert(lastPos >= 0); + return lastPos; +} - /** - * Sets EmitFlags to suppress leading and trailing trivia on the node. - */ - export function suppressLeadingAndTrailingTrivia(node: ts.Node) { - suppressLeadingTrivia(node); - suppressTrailingTrivia(node); - } +export function copyLeadingComments(sourceNode: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { + ts.forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.addSyntheticLeadingComment)); +} - /** - * Sets EmitFlags to suppress leading trivia on the node. - */ - export function suppressLeadingTrivia(node: ts.Node) { - addEmitFlagsRecursively(node, ts.EmitFlags.NoLeadingComments, getFirstChild); - } - /** - * Sets EmitFlags to suppress trailing trivia on the node. - */ - export function suppressTrailingTrivia(node: ts.Node) { - addEmitFlagsRecursively(node, ts.EmitFlags.NoTrailingComments, ts.getLastChild); - } +export function copyTrailingComments(sourceNode: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { + ts.forEachTrailingCommentRange(sourceFile.text, sourceNode.end, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.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: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { + ts.forEachTrailingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.addSyntheticLeadingComment)); +} - export function copyComments(sourceNode: ts.Node, targetNode: ts.Node) { - const sourceFile = sourceNode.getSourceFile(); - const text = sourceFile.text; - if (hasLeadingLineBreak(sourceNode, text)) { - copyLeadingComments(sourceNode, targetNode, sourceFile); +function getAddCommentsFunction(targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind: ts.CommentKind | undefined, hasTrailingNewLine: boolean | undefined, cb: (node: ts.Node, kind: ts.CommentKind, text: string, hasTrailingNewLine?: boolean) => void) { + return (pos: number, end: number, kind: ts.CommentKind, htnl: boolean) => { + if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + // Remove leading /* + pos += 2; + // Remove trailing */ + end -= 2; } else { - copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile); + // Remove leading // + pos += 2; } - copyTrailingComments(sourceNode, targetNode, sourceFile); - } + cb(targetNode, commentKind || kind, sourceFile.text.slice(pos, end), hasTrailingNewLine !== undefined ? hasTrailingNewLine : htnl); + }; +} - function hasLeadingLineBreak(node: ts.Node, text: string) { - const start = node.getFullStart(); - const end = node.getStart(); - for (let i = start; i < end; i++) { - if (text.charCodeAt(i) === ts.CharacterCodes.lineFeed) - return true; - } - return false; - } +function indexInTextChange(change: string, name: string): number { + if (ts.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; +} - function addEmitFlagsRecursively(node: ts.Node, flag: ts.EmitFlags, getChild: (n: ts.Node) => ts.Node | undefined) { - ts.addEmitFlags(node, flag); - const child = getChild(node); - if (child) - addEmitFlagsRecursively(child, flag, getChild); - } +/* @internal */ +export function needsParentheses(expression: ts.Expression): boolean { + return ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.CommaToken + || ts.isObjectLiteralExpression(expression) + || ts.isAsExpression(expression) && ts.isObjectLiteralExpression(expression.expression); +} - function getFirstChild(node: ts.Node): ts.Node | undefined { - return node.forEachChild(child => child); +export function getContextualTypeFromParent(node: ts.Expression, checker: ts.TypeChecker): ts.Type | undefined { + const { parent } = node; + switch (parent.kind) { + case ts.SyntaxKind.NewExpression: + return checker.getContextualType(parent as ts.NewExpression); + case ts.SyntaxKind.BinaryExpression: { + const { left, operatorToken, right } = parent as ts.BinaryExpression; + return isEqualityOperatorKind(operatorToken.kind) + ? checker.getTypeAtLocation(node === right ? left : right) + : checker.getContextualType(node); + } + case ts.SyntaxKind.CaseClause: + return (parent as ts.CaseClause).expression === node ? getSwitchedType(parent as ts.CaseClause, checker) : undefined; + default: + return checker.getContextualType(node); } +} - export function getUniqueName(baseName: string, sourceFile: ts.SourceFile): string { - let nameText = baseName; - for (let i = 1; !ts.isFileLevelUniqueName(sourceFile, nameText); i++) { - nameText = `${baseName}_${i}`; - } - return nameText; - } +export function quote(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, text: string): string { + // Editors can pass in undefined or empty string - we want to infer the preference in those cases. + const quotePreference = getQuotePreference(sourceFile, preferences); + const quoted = JSON.stringify(text); + return quotePreference === QuotePreference.Single ? `'${ts.stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted; +} - /** - * @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 ts.FileTextChanges[], renameFilename: string, name: string, preferLastLocation: boolean): number { - let delta = 0; - let lastPos = -1; - for (const { fileName, textChanges } of edits) { - ts.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; - } - } +export function isEqualityOperatorKind(kind: ts.SyntaxKind): kind is ts.EqualityOperator { + switch (kind) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return true; + default: + return false; + } +} - // If the declaration comes first, return the position of the last occurrence. - ts.Debug.assert(preferLastLocation); - ts.Debug.assert(lastPos >= 0); - return lastPos; +export function isStringLiteralOrTemplate(node: ts.Node): node is ts.StringLiteralLike | ts.TemplateExpression | ts.TaggedTemplateExpression { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateExpression: + case ts.SyntaxKind.TaggedTemplateExpression: + return true; + default: + return false; } +} - export function copyLeadingComments(sourceNode: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { - ts.forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.addSyntheticLeadingComment)); - } +export function hasIndexSignature(type: ts.Type): boolean { + return !!type.getStringIndexType() || !!type.getNumberIndexType(); +} +export function getSwitchedType(caseClause: ts.CaseClause, checker: ts.TypeChecker): ts.Type | undefined { + return checker.getTypeAtLocation(caseClause.parent.parent.expression); +} - export function copyTrailingComments(sourceNode: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { - ts.forEachTrailingCommentRange(sourceFile.text, sourceNode.end, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.addSyntheticTrailingComment)); - } +export const ANONYMOUS = "anonymous function"; + +export function getTypeNodeIfAccessible(type: ts.Type, enclosingScope: ts.Node, program: ts.Program, host: ts.LanguageServiceHost): ts.TypeNode | undefined { + const checker = program.getTypeChecker(); + let typeIsAccessible = true; + const notAccessible = () => typeIsAccessible = false; + const res = checker.typeToTypeNode(type, enclosingScope, ts.NodeBuilderFlags.NoTruncation, { + trackSymbol: (symbol, declaration, meaning) => { + typeIsAccessible = typeIsAccessible && checker.isSymbolAccessible(symbol, declaration, meaning, /*shouldComputeAliasToMarkVisible*/ false).accessibility === ts.SymbolAccessibility.Accessible; + return !typeIsAccessible; + }, + reportInaccessibleThisError: notAccessible, + reportPrivateInBaseOfClassExpression: notAccessible, + reportInaccessibleUniqueSymbolError: notAccessible, + moduleResolverHost: getModuleSpecifierResolverHost(program, host) + }); + return typeIsAccessible ? res : undefined; +} - /** - * 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: ts.Node, targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind?: ts.CommentKind, hasTrailingNewLine?: boolean) { - ts.forEachTrailingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, ts.addSyntheticLeadingComment)); +function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: ts.SyntaxKind) { + return kind === ts.SyntaxKind.CallSignature + || kind === ts.SyntaxKind.ConstructSignature + || kind === ts.SyntaxKind.IndexSignature + || kind === ts.SyntaxKind.PropertySignature + || kind === ts.SyntaxKind.MethodSignature; +} +function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: ts.SyntaxKind) { + return kind === ts.SyntaxKind.FunctionDeclaration + || kind === ts.SyntaxKind.Constructor + || kind === ts.SyntaxKind.MethodDeclaration + || kind === ts.SyntaxKind.GetAccessor + || kind === ts.SyntaxKind.SetAccessor; +} +function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: ts.SyntaxKind) { + return kind === ts.SyntaxKind.ModuleDeclaration; +} +export function syntaxRequiresTrailingSemicolonOrASI(kind: ts.SyntaxKind) { + return kind === ts.SyntaxKind.VariableStatement + || kind === ts.SyntaxKind.ExpressionStatement + || kind === ts.SyntaxKind.DoStatement + || kind === ts.SyntaxKind.ContinueStatement + || kind === ts.SyntaxKind.BreakStatement + || kind === ts.SyntaxKind.ReturnStatement + || kind === ts.SyntaxKind.ThrowStatement + || kind === ts.SyntaxKind.DebuggerStatement + || kind === ts.SyntaxKind.PropertyDeclaration + || kind === ts.SyntaxKind.TypeAliasDeclaration + || kind === ts.SyntaxKind.ImportDeclaration + || kind === ts.SyntaxKind.ImportEqualsDeclaration + || kind === ts.SyntaxKind.ExportDeclaration + || kind === ts.SyntaxKind.NamespaceExportDeclaration + || kind === ts.SyntaxKind.ExportAssignment; +} +export const syntaxMayBeASICandidate = ts.or(syntaxRequiresTrailingCommaOrSemicolonOrASI, syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI, syntaxRequiresTrailingModuleBlockOrSemicolonOrASI, syntaxRequiresTrailingSemicolonOrASI); +function nodeIsASICandidate(node: ts.Node, sourceFile: ts.SourceFileLike): boolean { + const lastToken = node.getLastToken(sourceFile); + if (lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken) { + return false; } - function getAddCommentsFunction(targetNode: ts.Node, sourceFile: ts.SourceFile, commentKind: ts.CommentKind | undefined, hasTrailingNewLine: boolean | undefined, cb: (node: ts.Node, kind: ts.CommentKind, text: string, hasTrailingNewLine?: boolean) => void) { - return (pos: number, end: number, kind: ts.CommentKind, htnl: boolean) => { - if (kind === ts.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 (ts.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 needsParentheses(expression: ts.Expression): boolean { - return ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.CommaToken - || ts.isObjectLiteralExpression(expression) - || ts.isAsExpression(expression) && ts.isObjectLiteralExpression(expression.expression); - } - - export function getContextualTypeFromParent(node: ts.Expression, checker: ts.TypeChecker): ts.Type | undefined { - const { parent } = node; - switch (parent.kind) { - case ts.SyntaxKind.NewExpression: - return checker.getContextualType(parent as ts.NewExpression); - case ts.SyntaxKind.BinaryExpression: { - const { left, operatorToken, right } = parent as ts.BinaryExpression; - return isEqualityOperatorKind(operatorToken.kind) - ? checker.getTypeAtLocation(node === right ? left : right) - : checker.getContextualType(node); - } - case ts.SyntaxKind.CaseClause: - return (parent as ts.CaseClause).expression === node ? getSwitchedType(parent as ts.CaseClause, checker) : undefined; - default: - return checker.getContextualType(node); + if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { + if (lastToken && lastToken.kind === ts.SyntaxKind.CommaToken) { + return false; } } - - export function quote(sourceFile: ts.SourceFile, preferences: ts.UserPreferences, text: string): string { - // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - const quotePreference = getQuotePreference(sourceFile, preferences); - const quoted = JSON.stringify(text); - return quotePreference === QuotePreference.Single ? `'${ts.stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted; - } - - export function isEqualityOperatorKind(kind: ts.SyntaxKind): kind is ts.EqualityOperator { - switch (kind) { - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - return true; - default: - return false; + else if (syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.kind)) { + const lastChild = ts.last(node.getChildren(sourceFile)); + if (lastChild && ts.isModuleBlock(lastChild)) { + return false; } } - - export function isStringLiteralOrTemplate(node: ts.Node): node is ts.StringLiteralLike | ts.TemplateExpression | ts.TaggedTemplateExpression { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - case ts.SyntaxKind.TemplateExpression: - case ts.SyntaxKind.TaggedTemplateExpression: - return true; - default: - return false; + else if (syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.kind)) { + const lastChild = ts.last(node.getChildren(sourceFile)); + if (lastChild && ts.isFunctionBlock(lastChild)) { + return false; } } + else if (!syntaxRequiresTrailingSemicolonOrASI(node.kind)) { + return false; + } - export function hasIndexSignature(type: ts.Type): boolean { - return !!type.getStringIndexType() || !!type.getNumberIndexType(); + // See comment in parser’s `parseDoStatement` + if (node.kind === ts.SyntaxKind.DoStatement) { + return true; } - export function getSwitchedType(caseClause: ts.CaseClause, checker: ts.TypeChecker): ts.Type | undefined { - return checker.getTypeAtLocation(caseClause.parent.parent.expression); + const topNode = ts.findAncestor(node, ancestor => !ancestor.parent)!; + const nextToken = findNextToken(node, topNode, sourceFile); + if (!nextToken || nextToken.kind === ts.SyntaxKind.CloseBraceToken) { + return true; } - export const ANONYMOUS = "anonymous function"; + const startLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(nextToken.getStart(sourceFile)).line; + return startLine !== endLine; +} - export function getTypeNodeIfAccessible(type: ts.Type, enclosingScope: ts.Node, program: ts.Program, host: ts.LanguageServiceHost): ts.TypeNode | undefined { - const checker = program.getTypeChecker(); - let typeIsAccessible = true; - const notAccessible = () => typeIsAccessible = false; - const res = checker.typeToTypeNode(type, enclosingScope, ts.NodeBuilderFlags.NoTruncation, { - trackSymbol: (symbol, declaration, meaning) => { - typeIsAccessible = typeIsAccessible && checker.isSymbolAccessible(symbol, declaration, meaning, /*shouldComputeAliasToMarkVisible*/ false).accessibility === ts.SymbolAccessibility.Accessible; - return !typeIsAccessible; - }, - reportInaccessibleThisError: notAccessible, - reportPrivateInBaseOfClassExpression: notAccessible, - reportInaccessibleUniqueSymbolError: notAccessible, - moduleResolverHost: getModuleSpecifierResolverHost(program, host) - }); - return typeIsAccessible ? res : undefined; - } - - function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: ts.SyntaxKind) { - return kind === ts.SyntaxKind.CallSignature - || kind === ts.SyntaxKind.ConstructSignature - || kind === ts.SyntaxKind.IndexSignature - || kind === ts.SyntaxKind.PropertySignature - || kind === ts.SyntaxKind.MethodSignature; - } - function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: ts.SyntaxKind) { - return kind === ts.SyntaxKind.FunctionDeclaration - || kind === ts.SyntaxKind.Constructor - || kind === ts.SyntaxKind.MethodDeclaration - || kind === ts.SyntaxKind.GetAccessor - || kind === ts.SyntaxKind.SetAccessor; - } - function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: ts.SyntaxKind) { - return kind === ts.SyntaxKind.ModuleDeclaration; - } - export function syntaxRequiresTrailingSemicolonOrASI(kind: ts.SyntaxKind) { - return kind === ts.SyntaxKind.VariableStatement - || kind === ts.SyntaxKind.ExpressionStatement - || kind === ts.SyntaxKind.DoStatement - || kind === ts.SyntaxKind.ContinueStatement - || kind === ts.SyntaxKind.BreakStatement - || kind === ts.SyntaxKind.ReturnStatement - || kind === ts.SyntaxKind.ThrowStatement - || kind === ts.SyntaxKind.DebuggerStatement - || kind === ts.SyntaxKind.PropertyDeclaration - || kind === ts.SyntaxKind.TypeAliasDeclaration - || kind === ts.SyntaxKind.ImportDeclaration - || kind === ts.SyntaxKind.ImportEqualsDeclaration - || kind === ts.SyntaxKind.ExportDeclaration - || kind === ts.SyntaxKind.NamespaceExportDeclaration - || kind === ts.SyntaxKind.ExportAssignment; - } - export const syntaxMayBeASICandidate = ts.or(syntaxRequiresTrailingCommaOrSemicolonOrASI, syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI, syntaxRequiresTrailingModuleBlockOrSemicolonOrASI, syntaxRequiresTrailingSemicolonOrASI); - function nodeIsASICandidate(node: ts.Node, sourceFile: ts.SourceFileLike): boolean { - const lastToken = node.getLastToken(sourceFile); - if (lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken) { - return false; +export function positionIsASICandidate(pos: number, context: ts.Node, sourceFile: ts.SourceFileLike): boolean { + const contextAncestor = ts.findAncestor(context, ancestor => { + if (ancestor.end !== pos) { + return "quit"; } + return syntaxMayBeASICandidate(ancestor.kind); + }); - if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { - if (lastToken && lastToken.kind === ts.SyntaxKind.CommaToken) { - return false; + return !!contextAncestor && nodeIsASICandidate(contextAncestor, sourceFile); +} + +export function probablyUsesSemicolons(sourceFile: ts.SourceFile): boolean { + let withSemicolon = 0; + let withoutSemicolon = 0; + const nStatementsToObserve = 5; + ts.forEachChild(sourceFile, function visit(node): boolean | undefined { + if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) { + const lastToken = node.getLastToken(sourceFile); + if (lastToken?.kind === ts.SyntaxKind.SemicolonToken) { + withSemicolon++; } - } - else if (syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.kind)) { - const lastChild = ts.last(node.getChildren(sourceFile)); - if (lastChild && ts.isModuleBlock(lastChild)) { - return false; + else { + withoutSemicolon++; } } - else if (syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.kind)) { - const lastChild = ts.last(node.getChildren(sourceFile)); - if (lastChild && ts.isFunctionBlock(lastChild)) { - return false; + else if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { + const lastToken = node.getLastToken(sourceFile); + if (lastToken?.kind === ts.SyntaxKind.SemicolonToken) { + withSemicolon++; + } + else if (lastToken && lastToken.kind !== ts.SyntaxKind.CommaToken) { + const lastTokenLine = ts.getLineAndCharacterOfPosition(sourceFile, lastToken.getStart(sourceFile)).line; + const nextTokenLine = ts.getLineAndCharacterOfPosition(sourceFile, ts.getSpanOfTokenAtPosition(sourceFile, lastToken.end).start).line; + // Avoid counting missing semicolon in single-line objects: + // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` + if (lastTokenLine !== nextTokenLine) { + withoutSemicolon++; + } } - } - else if (!syntaxRequiresTrailingSemicolonOrASI(node.kind)) { - return false; - } - - // See comment in parser’s `parseDoStatement` - if (node.kind === ts.SyntaxKind.DoStatement) { - return true; } - const topNode = ts.findAncestor(node, ancestor => !ancestor.parent)!; - const nextToken = findNextToken(node, topNode, sourceFile); - if (!nextToken || nextToken.kind === ts.SyntaxKind.CloseBraceToken) { + if (withSemicolon + withoutSemicolon >= nStatementsToObserve) { return true; } - const startLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(nextToken.getStart(sourceFile)).line; - return startLine !== endLine; - } - - export function positionIsASICandidate(pos: number, context: ts.Node, sourceFile: ts.SourceFileLike): boolean { - const contextAncestor = ts.findAncestor(context, ancestor => { - if (ancestor.end !== pos) { - return "quit"; - } - return syntaxMayBeASICandidate(ancestor.kind); - }); + return ts.forEachChild(node, visit); + }); - return !!contextAncestor && nodeIsASICandidate(contextAncestor, sourceFile); + // 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 probablyUsesSemicolons(sourceFile: ts.SourceFile): boolean { - let withSemicolon = 0; - let withoutSemicolon = 0; - const nStatementsToObserve = 5; - ts.forEachChild(sourceFile, function visit(node): boolean | undefined { - if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) { - const lastToken = node.getLastToken(sourceFile); - if (lastToken?.kind === ts.SyntaxKind.SemicolonToken) { - withSemicolon++; - } - else { - withoutSemicolon++; - } - } - else if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { - const lastToken = node.getLastToken(sourceFile); - if (lastToken?.kind === ts.SyntaxKind.SemicolonToken) { - withSemicolon++; - } - else if (lastToken && lastToken.kind !== ts.SyntaxKind.CommaToken) { - const lastTokenLine = ts.getLineAndCharacterOfPosition(sourceFile, lastToken.getStart(sourceFile)).line; - const nextTokenLine = ts.getLineAndCharacterOfPosition(sourceFile, ts.getSpanOfTokenAtPosition(sourceFile, lastToken.end).start).line; - // Avoid counting missing semicolon in single-line objects: - // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` - if (lastTokenLine !== nextTokenLine) { - withoutSemicolon++; - } - } - } - - if (withSemicolon + withoutSemicolon >= nStatementsToObserve) { - return true; - } + // If even 2/5 places have a semicolon, the user probably wants semicolons + return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve; +} - return ts.forEachChild(node, visit); - }); +export function tryGetDirectories(host: Pick, directoryName: string): string[] { + return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; +} - // 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 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) || ts.emptyArray; +} - // If even 2/5 places have a semicolon, the user probably wants semicolons - return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve; - } +export function tryFileExists(host: Pick, path: string): boolean { + return tryIOAndConsumeErrors(host, host.fileExists, path); +} - export function tryGetDirectories(host: Pick, directoryName: string): string[] { - return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; - } +export function tryDirectoryExists(host: ts.LanguageServiceHost, path: string): boolean { + return tryAndIgnoreErrors(() => ts.directoryProbablyExists(path, host)) || false; +} - 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) || ts.emptyArray; +export function tryAndIgnoreErrors(cb: () => T): T | undefined { + try { + return cb(); } - - export function tryFileExists(host: Pick, path: string): boolean { - return tryIOAndConsumeErrors(host, host.fileExists, path); + catch { + return undefined; } +} - export function tryDirectoryExists(host: ts.LanguageServiceHost, path: string): boolean { - return tryAndIgnoreErrors(() => ts.directoryProbablyExists(path, host)) || false; - } +export function tryIOAndConsumeErrors(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { + return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); +} - export function tryAndIgnoreErrors(cb: () => T): T | undefined { - try { - return cb(); +export function findPackageJsons(startDirectory: string, host: Pick, stopDirectory?: string): string[] { + const paths: string[] = []; + ts.forEachAncestorDirectory(startDirectory, ancestor => { + if (ancestor === stopDirectory) { + return true; } - catch { - return undefined; + const currentConfigPath = ts.combinePaths(ancestor, "package.json"); + if (tryFileExists(host, currentConfigPath)) { + paths.push(currentConfigPath); } - } + }); + return paths; +} - export function tryIOAndConsumeErrors(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { - return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); - } +export function findPackageJson(directory: string, host: ts.LanguageServiceHost): string | undefined { + let packageJson: string | undefined; + ts.forEachAncestorDirectory(directory, ancestor => { + if (ancestor === "node_modules") + return true; + packageJson = ts.findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json"); + if (packageJson) { + return true; // break out + } + }); + return packageJson; +} - export function findPackageJsons(startDirectory: string, host: Pick, stopDirectory?: string): string[] { - const paths: string[] = []; - ts.forEachAncestorDirectory(startDirectory, ancestor => { - if (ancestor === stopDirectory) { - return true; - } - const currentConfigPath = ts.combinePaths(ancestor, "package.json"); - if (tryFileExists(host, currentConfigPath)) { - paths.push(currentConfigPath); - } - }); - return paths; +export function getPackageJsonsVisibleToFile(fileName: string, host: ts.LanguageServiceHost): readonly ts.PackageJsonInfo[] { + if (!host.fileExists) { + return []; } - export function findPackageJson(directory: string, host: ts.LanguageServiceHost): string | undefined { - let packageJson: string | undefined; - ts.forEachAncestorDirectory(directory, ancestor => { - if (ancestor === "node_modules") - return true; - packageJson = ts.findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json"); - if (packageJson) { - return true; // break out + const packageJsons: ts.PackageJsonInfo[] = []; + ts.forEachAncestorDirectory(ts.getDirectoryPath(fileName), ancestor => { + const packageJsonFileName = ts.combinePaths(ancestor, "package.json"); + if (host.fileExists(packageJsonFileName)) { + const info = createPackageJsonInfo(packageJsonFileName, host); + if (info) { + packageJsons.push(info); } - }); - return packageJson; - } - - export function getPackageJsonsVisibleToFile(fileName: string, host: ts.LanguageServiceHost): readonly ts.PackageJsonInfo[] { - if (!host.fileExists) { - return []; } + }); - const packageJsons: ts.PackageJsonInfo[] = []; - ts.forEachAncestorDirectory(ts.getDirectoryPath(fileName), ancestor => { - const packageJsonFileName = ts.combinePaths(ancestor, "package.json"); - if (host.fileExists(packageJsonFileName)) { - const info = createPackageJsonInfo(packageJsonFileName, host); - if (info) { - packageJsons.push(info); - } - } - }); + return packageJsons; +} - return packageJsons; +export function createPackageJsonInfo(fileName: string, host: { + readFile?(fileName: string): string | undefined; +}): ts.PackageJsonInfo | undefined { + if (!host.readFile) { + return undefined; } - export function createPackageJsonInfo(fileName: string, host: { - readFile?(fileName: string): string | undefined; - }): ts.PackageJsonInfo | undefined { - if (!host.readFile) { - return undefined; - } - - type PackageJsonRaw = Record | undefined>; - const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] as const; - const stringContent = host.readFile(fileName) || ""; - const content = tryParseJson(stringContent) as PackageJsonRaw | undefined; - const info: Pick = {}; - if (content) { - for (const key of dependencyKeys) { - const dependencies = content[key]; - if (!dependencies) { - continue; - } - const dependencyMap = new ts.Map(); - for (const packageName in dependencies) { - dependencyMap.set(packageName, dependencies[packageName]); - } - info[key] = dependencyMap; - } - } + type PackageJsonRaw = Record | undefined>; + const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] as const; + const stringContent = host.readFile(fileName) || ""; + const content = tryParseJson(stringContent) as PackageJsonRaw | undefined; + const info: Pick = {}; + if (content) { + for (const key of dependencyKeys) { + const dependencies = content[key]; + if (!dependencies) { + continue; + } + const dependencyMap = new ts.Map(); + for (const packageName in dependencies) { + dependencyMap.set(packageName, dependencies[packageName]); + } + info[key] = dependencyMap; + } + } + + const dependencyGroups = [ + [ts.PackageJsonDependencyGroup.Dependencies, info.dependencies], + [ts.PackageJsonDependencyGroup.DevDependencies, info.devDependencies], + [ts.PackageJsonDependencyGroup.OptionalDependencies, info.optionalDependencies], + [ts.PackageJsonDependencyGroup.PeerDependencies, info.peerDependencies], + ] as const; + + return { + ...info, + parseable: !!content, + fileName, + get, + has(dependencyName, inGroups) { + return !!get(dependencyName, inGroups); + }, + }; - const dependencyGroups = [ - [ts.PackageJsonDependencyGroup.Dependencies, info.dependencies], - [ts.PackageJsonDependencyGroup.DevDependencies, info.devDependencies], - [ts.PackageJsonDependencyGroup.OptionalDependencies, info.optionalDependencies], - [ts.PackageJsonDependencyGroup.PeerDependencies, info.peerDependencies], - ] as const; - - return { - ...info, - parseable: !!content, - fileName, - get, - has(dependencyName, inGroups) { - return !!get(dependencyName, inGroups); - }, - }; - - function get(dependencyName: string, inGroups = ts.PackageJsonDependencyGroup.All) { - for (const [group, deps] of dependencyGroups) { - if (deps && (inGroups & group)) { - const dep = deps.get(dependencyName); - if (dep !== undefined) { - return dep; - } + function get(dependencyName: string, inGroups = ts.PackageJsonDependencyGroup.All) { + for (const [group, deps] of dependencyGroups) { + if (deps && (inGroups & group)) { + const dep = deps.get(dependencyName); + if (dep !== undefined) { + return dep; } } } } +} - export interface PackageJsonImportFilter { - allowsImportingAmbientModule: (moduleSymbol: ts.Symbol, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost) => boolean; - allowsImportingSourceFile: (sourceFile: ts.SourceFile, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost) => boolean; - /** - * Use for a specific module specifier that has already been resolved. - * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve - * the best module specifier for a given module _and_ determine if it’s importable. - */ - allowsImportingSpecifier: (moduleSpecifier: string) => boolean; - } +export interface PackageJsonImportFilter { + allowsImportingAmbientModule: (moduleSymbol: ts.Symbol, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost) => boolean; + allowsImportingSourceFile: (sourceFile: ts.SourceFile, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost) => boolean; + /** + * Use for a specific module specifier that has already been resolved. + * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve + * the best module specifier for a given module _and_ determine if it’s importable. + */ + allowsImportingSpecifier: (moduleSpecifier: string) => boolean; +} - export function createPackageJsonImportFilter(fromFile: ts.SourceFile, preferences: ts.UserPreferences, host: ts.LanguageServiceHost): PackageJsonImportFilter { - const packageJsons = ((host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host)).filter(p => p.parseable); +export function createPackageJsonImportFilter(fromFile: ts.SourceFile, preferences: ts.UserPreferences, host: ts.LanguageServiceHost): PackageJsonImportFilter { + const packageJsons = ((host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host)).filter(p => p.parseable); - let usesNodeCoreModules: boolean | undefined; - return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier }; + let usesNodeCoreModules: boolean | undefined; + return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier }; - function moduleSpecifierIsCoveredByPackageJson(specifier: string) { - const packageName = getNodeModuleRootSpecifier(specifier); - for (const packageJson of packageJsons) { - if (packageJson.has(packageName) || packageJson.has(ts.getTypesPackageName(packageName))) { - return true; - } + function moduleSpecifierIsCoveredByPackageJson(specifier: string) { + const packageName = getNodeModuleRootSpecifier(specifier); + for (const packageJson of packageJsons) { + if (packageJson.has(packageName) || packageJson.has(ts.getTypesPackageName(packageName))) { + return true; } - return false; + } + return false; + } + + function allowsImportingAmbientModule(moduleSymbol: ts.Symbol, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): boolean { + if (!packageJsons.length || !moduleSymbol.valueDeclaration) { + return true; } - function allowsImportingAmbientModule(moduleSymbol: ts.Symbol, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): boolean { - if (!packageJsons.length || !moduleSymbol.valueDeclaration) { - return true; - } + const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile(); + const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, moduleSpecifierResolutionHost); + if (typeof declaringNodeModuleName === "undefined") { + return true; + } - const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile(); - const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, moduleSpecifierResolutionHost); - if (typeof declaringNodeModuleName === "undefined") { - return true; - } + const declaredModuleSpecifier = ts.stripQuotes(moduleSymbol.getName()); + if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) { + return true; + } - const declaredModuleSpecifier = ts.stripQuotes(moduleSymbol.getName()); - if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) { - return true; - } + return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName) + || moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier); + } - return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName) - || moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier); + function allowsImportingSourceFile(sourceFile: ts.SourceFile, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): boolean { + if (!packageJsons.length) { + return true; } - function allowsImportingSourceFile(sourceFile: ts.SourceFile, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): boolean { - if (!packageJsons.length) { - return true; - } + const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, moduleSpecifierResolutionHost); + if (!moduleSpecifier) { + return true; + } - const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, moduleSpecifierResolutionHost); - if (!moduleSpecifier) { - return true; - } + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); + } - return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); + function allowsImportingSpecifier(moduleSpecifier: string) { + if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) { + return true; } + if (ts.pathIsRelative(moduleSpecifier) || ts.isRootedDiskPath(moduleSpecifier)) { + return true; + } + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); + } - function allowsImportingSpecifier(moduleSpecifier: string) { - if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) { - return true; + function isAllowedCoreNodeModulesImport(moduleSpecifier: string) { + // If we’re in JavaScript, it can be difficult to tell whether the user wants to import + // from Node core modules or not. We can start by seeing if the user is actually using + // any node core modules, as opposed to simply having @types/node accidentally as a + // dependency of a dependency. + if (ts.isSourceFileJS(fromFile) && ts.JsTyping.nodeCoreModules.has(moduleSpecifier)) { + if (usesNodeCoreModules === undefined) { + usesNodeCoreModules = consumesNodeCoreModules(fromFile); } - if (ts.pathIsRelative(moduleSpecifier) || ts.isRootedDiskPath(moduleSpecifier)) { + if (usesNodeCoreModules) { return true; } - return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); } + return false; + } - function isAllowedCoreNodeModulesImport(moduleSpecifier: string) { - // If we’re in JavaScript, it can be difficult to tell whether the user wants to import - // from Node core modules or not. We can start by seeing if the user is actually using - // any node core modules, as opposed to simply having @types/node accidentally as a - // dependency of a dependency. - if (ts.isSourceFileJS(fromFile) && ts.JsTyping.nodeCoreModules.has(moduleSpecifier)) { - if (usesNodeCoreModules === undefined) { - usesNodeCoreModules = consumesNodeCoreModules(fromFile); - } - if (usesNodeCoreModules) { - return true; - } - } - return false; + function getNodeModulesPackageNameFromFileName(importedFileName: string, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): string | undefined { + if (!ts.stringContains(importedFileName, "node_modules")) { + return undefined; } + const specifier = ts.moduleSpecifiers.getNodeModulesPackageName(host.getCompilationSettings(), fromFile, importedFileName, moduleSpecifierResolutionHost, preferences); - function getNodeModulesPackageNameFromFileName(importedFileName: string, moduleSpecifierResolutionHost: ts.ModuleSpecifierResolutionHost): string | undefined { - if (!ts.stringContains(importedFileName, "node_modules")) { - return undefined; - } - const specifier = ts.moduleSpecifiers.getNodeModulesPackageName(host.getCompilationSettings(), fromFile, importedFileName, moduleSpecifierResolutionHost, preferences); - - if (!specifier) { - return undefined; - } - // Paths here are not node_modules, so we don’t care about them; - // returning anything will trigger a lookup in package.json. - if (!ts.pathIsRelative(specifier) && !ts.isRootedDiskPath(specifier)) { - return getNodeModuleRootSpecifier(specifier); - } + if (!specifier) { + return undefined; } - - function getNodeModuleRootSpecifier(fullSpecifier: string): string { - const components = ts.getPathComponents(ts.getPackageNameFromTypesPackageName(fullSpecifier)).slice(1); - // Scoped packages - if (ts.startsWith(components[0], "@")) { - return `${components[0]}/${components[1]}`; - } - return components[0]; + // Paths here are not node_modules, so we don’t care about them; + // returning anything will trigger a lookup in package.json. + if (!ts.pathIsRelative(specifier) && !ts.isRootedDiskPath(specifier)) { + return getNodeModuleRootSpecifier(specifier); } } - function tryParseJson(text: string) { - try { - return JSON.parse(text); - } - catch { - return undefined; + function getNodeModuleRootSpecifier(fullSpecifier: string): string { + const components = ts.getPathComponents(ts.getPackageNameFromTypesPackageName(fullSpecifier)).slice(1); + // Scoped packages + if (ts.startsWith(components[0], "@")) { + return `${components[0]}/${components[1]}`; } + return components[0]; } +} - export function consumesNodeCoreModules(sourceFile: ts.SourceFile): boolean { - return ts.some(sourceFile.imports, ({ text }) => ts.JsTyping.nodeCoreModules.has(text)); +function tryParseJson(text: string) { + try { + return JSON.parse(text); } - - export function isInsideNodeModules(fileOrDirectory: string): boolean { - return ts.contains(ts.getPathComponents(fileOrDirectory), "node_modules"); + catch { + return undefined; } +} + +export function consumesNodeCoreModules(sourceFile: ts.SourceFile): boolean { + return ts.some(sourceFile.imports, ({ text }) => ts.JsTyping.nodeCoreModules.has(text)); +} + +export function isInsideNodeModules(fileOrDirectory: string): boolean { + return ts.contains(ts.getPathComponents(fileOrDirectory), "node_modules"); +} + +export function isDiagnosticWithLocation(diagnostic: ts.Diagnostic): diagnostic is ts.DiagnosticWithLocation { + return diagnostic.file !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined; +} - export function isDiagnosticWithLocation(diagnostic: ts.Diagnostic): diagnostic is ts.DiagnosticWithLocation { - return diagnostic.file !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined; +export function findDiagnosticForNode(node: ts.Node, sortedFileDiagnostics: readonly ts.Diagnostic[]): ts.DiagnosticWithLocation | undefined { + const span: Partial = createTextSpanFromNode(node); + const index = ts.binarySearchKey(sortedFileDiagnostics, span, ts.identity, ts.compareTextSpans); + if (index >= 0) { + const diagnostic = sortedFileDiagnostics[index]; + ts.Debug.assertEqual(diagnostic.file, node.getSourceFile(), "Diagnostics proided to 'findDiagnosticForNode' must be from a single SourceFile"); + return ts.cast(diagnostic, isDiagnosticWithLocation); } +} - export function findDiagnosticForNode(node: ts.Node, sortedFileDiagnostics: readonly ts.Diagnostic[]): ts.DiagnosticWithLocation | undefined { - const span: Partial = createTextSpanFromNode(node); - const index = ts.binarySearchKey(sortedFileDiagnostics, span, ts.identity, ts.compareTextSpans); - if (index >= 0) { - const diagnostic = sortedFileDiagnostics[index]; - ts.Debug.assertEqual(diagnostic.file, node.getSourceFile(), "Diagnostics proided to 'findDiagnosticForNode' must be from a single SourceFile"); - return ts.cast(diagnostic, isDiagnosticWithLocation); - } +export function getDiagnosticsWithinSpan(span: ts.TextSpan, sortedFileDiagnostics: readonly ts.Diagnostic[]): readonly ts.DiagnosticWithLocation[] { + let index = ts.binarySearchKey(sortedFileDiagnostics, span.start, diag => diag.start, ts.compareValues); + if (index < 0) { + index = ~index; + } + while (sortedFileDiagnostics[index - 1]?.start === span.start) { + index--; } - export function getDiagnosticsWithinSpan(span: ts.TextSpan, sortedFileDiagnostics: readonly ts.Diagnostic[]): readonly ts.DiagnosticWithLocation[] { - let index = ts.binarySearchKey(sortedFileDiagnostics, span.start, diag => diag.start, ts.compareValues); - if (index < 0) { - index = ~index; + const result: ts.DiagnosticWithLocation[] = []; + const end = ts.textSpanEnd(span); + while (true) { + const diagnostic = ts.tryCast(sortedFileDiagnostics[index], isDiagnosticWithLocation); + if (!diagnostic || diagnostic.start > end) { + break; } - while (sortedFileDiagnostics[index - 1]?.start === span.start) { - index--; - } - - const result: ts.DiagnosticWithLocation[] = []; - const end = ts.textSpanEnd(span); - while (true) { - const diagnostic = ts.tryCast(sortedFileDiagnostics[index], isDiagnosticWithLocation); - if (!diagnostic || diagnostic.start > end) { - break; - } - if (ts.textSpanContainsTextSpan(span, diagnostic)) { - result.push(diagnostic); - } - index++; + if (ts.textSpanContainsTextSpan(span, diagnostic)) { + result.push(diagnostic); } - - return result; + index++; } - /* @internal */ - export function getRefactorContextSpan({ startPosition, endPosition }: ts.RefactorContext): ts.TextSpan { - return ts.createTextSpanFromBounds(startPosition, endPosition === undefined ? startPosition : endPosition); - } + return result; +} - /* @internal */ - export function getFixableErrorSpanExpression(sourceFile: ts.SourceFile, span: ts.TextSpan): ts.Expression | undefined { - const token = getTokenAtPosition(sourceFile, span.start); - // Checker has already done work to determine that await might be possible, and has attached - // related info to the node, so start by finding the expression that exactly matches up - // with the diagnostic range. - const expression = ts.findAncestor(token, node => { - if (node.getStart(sourceFile) < span.start || node.getEnd() > ts.textSpanEnd(span)) { - return "quit"; - } - return ts.isExpression(node) && textSpansEqual(span, createTextSpanFromNode(node, sourceFile)); - }) as ts.Expression | undefined; +/* @internal */ +export function getRefactorContextSpan({ startPosition, endPosition }: ts.RefactorContext): ts.TextSpan { + return ts.createTextSpanFromBounds(startPosition, endPosition === undefined ? startPosition : endPosition); +} - return expression; - } +/* @internal */ +export function getFixableErrorSpanExpression(sourceFile: ts.SourceFile, span: ts.TextSpan): ts.Expression | undefined { + const token = getTokenAtPosition(sourceFile, span.start); + // Checker has already done work to determine that await might be possible, and has attached + // related info to the node, so start by finding the expression that exactly matches up + // with the diagnostic range. + const expression = ts.findAncestor(token, node => { + if (node.getStart(sourceFile) < span.start || node.getEnd() > ts.textSpanEnd(span)) { + return "quit"; + } + return ts.isExpression(node) && textSpansEqual(span, createTextSpanFromNode(node, sourceFile)); + }) as ts.Expression | undefined; + + return expression; +} - /** - * 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[] = ts.identity): U | U[] | undefined { - return valueOrArray ? ts.isArray(valueOrArray) ? resultSelector(ts.map(valueOrArray, f)) : f(valueOrArray, 0) : undefined; - } +/** + * 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[] = ts.identity): U | U[] | undefined { + return valueOrArray ? ts.isArray(valueOrArray) ? resultSelector(ts.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 ts.isArray(valueOrArray) ? ts.first(valueOrArray) : valueOrArray; - } +/** + * 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 ts.isArray(valueOrArray) ? ts.first(valueOrArray) : valueOrArray; +} - export function getNamesForExportedSymbol(symbol: ts.Symbol, scriptTarget: ts.ScriptTarget | undefined): string | [ - lowercase: string, - capitalized: string - ] { - if (needsNameFromDeclaration(symbol)) { - const fromDeclaration = getDefaultLikeExportNameFromDeclaration(symbol); - if (fromDeclaration) - return fromDeclaration; - const fileNameCase = ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ false); - const capitalized = ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ true); - if (fileNameCase === capitalized) - return fileNameCase; - return [fileNameCase, capitalized]; - } - return symbol.name; +export function getNamesForExportedSymbol(symbol: ts.Symbol, scriptTarget: ts.ScriptTarget | undefined): string | [ + lowercase: string, + capitalized: string +] { + if (needsNameFromDeclaration(symbol)) { + const fromDeclaration = getDefaultLikeExportNameFromDeclaration(symbol); + if (fromDeclaration) + return fromDeclaration; + const fileNameCase = ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ false); + const capitalized = ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ true); + if (fileNameCase === capitalized) + return fileNameCase; + return [fileNameCase, capitalized]; + } + return symbol.name; +} + +export function getNameForExportedSymbol(symbol: ts.Symbol, scriptTarget: ts.ScriptTarget | undefined, preferCapitalized?: boolean) { + if (needsNameFromDeclaration(symbol)) { + // Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase. + return getDefaultLikeExportNameFromDeclaration(symbol) + || ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized); } + return symbol.name; - export function getNameForExportedSymbol(symbol: ts.Symbol, scriptTarget: ts.ScriptTarget | undefined, preferCapitalized?: boolean) { - if (needsNameFromDeclaration(symbol)) { - // Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase. - return getDefaultLikeExportNameFromDeclaration(symbol) - || ts.codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized); - } - return symbol.name; +} - } +function needsNameFromDeclaration(symbol: ts.Symbol) { + return !(symbol.flags & ts.SymbolFlags.Transient) && (symbol.escapedName === ts.InternalSymbolName.ExportEquals || symbol.escapedName === ts.InternalSymbolName.Default); +} - function needsNameFromDeclaration(symbol: ts.Symbol) { - return !(symbol.flags & ts.SymbolFlags.Transient) && (symbol.escapedName === ts.InternalSymbolName.ExportEquals || symbol.escapedName === ts.InternalSymbolName.Default); - } +function getDefaultLikeExportNameFromDeclaration(symbol: ts.Symbol) { + return ts.firstDefined(symbol.declarations, d => ts.isExportAssignment(d) ? ts.tryCast(ts.skipOuterExpressions(d.expression), ts.isIdentifier)?.text : undefined); +} - function getDefaultLikeExportNameFromDeclaration(symbol: ts.Symbol) { - return ts.firstDefined(symbol.declarations, d => ts.isExportAssignment(d) ? ts.tryCast(ts.skipOuterExpressions(d.expression), ts.isIdentifier)?.text : undefined); - } +function getSymbolParentOrFail(symbol: ts.Symbol) { + return ts.Debug.checkDefined(symbol.parent, `Symbol parent was undefined. Flags: ${ts.Debug.formatSymbolFlags(symbol.flags)}. ` + + `Declarations: ${symbol.declarations?.map(d => { + const kind = ts.Debug.formatSyntaxKind(d.kind); + const inJS = ts.isInJSFile(d); + const { expression } = d as any; + return (inJS ? "[JS]" : "") + kind + (expression ? ` (expression: ${ts.Debug.formatSyntaxKind(expression.kind)})` : ""); + }).join(", ")}.`); +} - function getSymbolParentOrFail(symbol: ts.Symbol) { - return ts.Debug.checkDefined(symbol.parent, `Symbol parent was undefined. Flags: ${ts.Debug.formatSymbolFlags(symbol.flags)}. ` + - `Declarations: ${symbol.declarations?.map(d => { - const kind = ts.Debug.formatSyntaxKind(d.kind); - const inJS = ts.isInJSFile(d); - const { expression } = d as any; - return (inJS ? "[JS]" : "") + kind + (expression ? ` (expression: ${ts.Debug.formatSyntaxKind(expression.kind)})` : ""); - }).join(", ")}.`); +/** + * Useful to check whether a string contains another string at a specific index + * without allocating another string or traversing the entire contents of the outer string. + * + * This function is useful in place of either of the following: + * + * ```ts + * // Allocates + * haystack.substr(startIndex, needle.length) === needle + * + * // Full traversal + * haystack.indexOf(needle, startIndex) === startIndex + * ``` + * + * @param haystack The string that potentially contains `needle`. + * @param needle The string whose content might sit within `haystack`. + * @param startIndex The index within `haystack` to start searching for `needle`. + */ +export function stringContainsAt(haystack: string, needle: string, startIndex: number) { + const needleLength = needle.length; + if (needleLength + startIndex > haystack.length) { + return false; } - - /** - * Useful to check whether a string contains another string at a specific index - * without allocating another string or traversing the entire contents of the outer string. - * - * This function is useful in place of either of the following: - * - * ```ts - * // Allocates - * haystack.substr(startIndex, needle.length) === needle - * - * // Full traversal - * haystack.indexOf(needle, startIndex) === startIndex - * ``` - * - * @param haystack The string that potentially contains `needle`. - * @param needle The string whose content might sit within `haystack`. - * @param startIndex The index within `haystack` to start searching for `needle`. - */ - export function stringContainsAt(haystack: string, needle: string, startIndex: number) { - const needleLength = needle.length; - if (needleLength + startIndex > haystack.length) { + for (let i = 0; i < needleLength; i++) { + if (needle.charCodeAt(i) !== haystack.charCodeAt(i + startIndex)) return false; - } - for (let i = 0; i < needleLength; i++) { - if (needle.charCodeAt(i) !== haystack.charCodeAt(i + startIndex)) - return false; - } - return true; } + return true; +} - export function startsWithUnderscore(name: string): boolean { - return name.charCodeAt(0) === ts.CharacterCodes._; - } +export function startsWithUnderscore(name: string): boolean { + return name.charCodeAt(0) === ts.CharacterCodes._; +} - export function isGlobalDeclaration(declaration: ts.Declaration) { - return !isNonGlobalDeclaration(declaration); - } +export function isGlobalDeclaration(declaration: ts.Declaration) { + return !isNonGlobalDeclaration(declaration); +} - export function isNonGlobalDeclaration(declaration: ts.Declaration) { - const sourceFile = declaration.getSourceFile(); - // If the file is not a module, the declaration is global - if (!sourceFile.externalModuleIndicator && !sourceFile.commonJsModuleIndicator) { - return false; - } - // If the file is a module written in TypeScript, it still might be in a `declare global` augmentation - return ts.isInJSFile(declaration) || !ts.findAncestor(declaration, ts.isGlobalScopeAugmentation); +export function isNonGlobalDeclaration(declaration: ts.Declaration) { + const sourceFile = declaration.getSourceFile(); + // If the file is not a module, the declaration is global + if (!sourceFile.externalModuleIndicator && !sourceFile.commonJsModuleIndicator) { + return false; } + // If the file is a module written in TypeScript, it still might be in a `declare global` augmentation + return ts.isInJSFile(declaration) || !ts.findAncestor(declaration, ts.isGlobalScopeAugmentation); +} - export function isDeprecatedDeclaration(decl: ts.Declaration) { - return !!(ts.getCombinedNodeFlagsAlwaysIncludeJSDoc(decl) & ts.ModifierFlags.Deprecated); - } +export function isDeprecatedDeclaration(decl: ts.Declaration) { + return !!(ts.getCombinedNodeFlagsAlwaysIncludeJSDoc(decl) & ts.ModifierFlags.Deprecated); +} - export function shouldUseUriStyleNodeCoreModules(file: ts.SourceFile, program: ts.Program): boolean { - const decisionFromFile = ts.firstDefined(file.imports, node => { - if (ts.JsTyping.nodeCoreModules.has(node.text)) { - return ts.startsWith(node.text, "node:"); - } - }); - return decisionFromFile ?? program.usesUriStyleNodeCoreModules; - } +export function shouldUseUriStyleNodeCoreModules(file: ts.SourceFile, program: ts.Program): boolean { + const decisionFromFile = ts.firstDefined(file.imports, node => { + if (ts.JsTyping.nodeCoreModules.has(node.text)) { + return ts.startsWith(node.text, "node:"); + } + }); + return decisionFromFile ?? program.usesUriStyleNodeCoreModules; +} - export function getNewLineKind(newLineCharacter: string): ts.NewLineKind { - return newLineCharacter === "\n" ? ts.NewLineKind.LineFeed : ts.NewLineKind.CarriageReturnLineFeed; - } +export function getNewLineKind(newLineCharacter: string): ts.NewLineKind { + return newLineCharacter === "\n" ? ts.NewLineKind.LineFeed : ts.NewLineKind.CarriageReturnLineFeed; +} - export type DiagnosticAndArguments = ts.DiagnosticMessage | [ - ts.DiagnosticMessage, - string - ] | [ - ts.DiagnosticMessage, - string, - string - ]; - export function diagnosticToString(diag: DiagnosticAndArguments): string { - return ts.isArray(diag) - ? ts.formatStringFromArgs(ts.getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[]) - : ts.getLocaleSpecificMessage(diag); - } +export type DiagnosticAndArguments = ts.DiagnosticMessage | [ + ts.DiagnosticMessage, + string +] | [ + ts.DiagnosticMessage, + string, + string +]; +export function diagnosticToString(diag: DiagnosticAndArguments): string { + return ts.isArray(diag) + ? ts.formatStringFromArgs(ts.getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[]) + : ts.getLocaleSpecificMessage(diag); +} - /** - * Get format code settings for a code writing context (e.g. when formatting text changes or completions code). - */ - export function getFormatCodeSettingsForWriting({ options }: ts.formatting.FormatContext, sourceFile: ts.SourceFile): ts.FormatCodeSettings { - const shouldAutoDetectSemicolonPreference = !options.semicolons || options.semicolons === ts.SemicolonPreference.Ignore; - const shouldRemoveSemicolons = options.semicolons === ts.SemicolonPreference.Remove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile); - return { - ...options, - semicolons: shouldRemoveSemicolons ? ts.SemicolonPreference.Remove : ts.SemicolonPreference.Ignore, - }; - } +/** + * Get format code settings for a code writing context (e.g. when formatting text changes or completions code). + */ +export function getFormatCodeSettingsForWriting({ options }: ts.formatting.FormatContext, sourceFile: ts.SourceFile): ts.FormatCodeSettings { + const shouldAutoDetectSemicolonPreference = !options.semicolons || options.semicolons === ts.SemicolonPreference.Ignore; + const shouldRemoveSemicolons = options.semicolons === ts.SemicolonPreference.Remove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile); + return { + ...options, + semicolons: shouldRemoveSemicolons ? ts.SemicolonPreference.Remove : ts.SemicolonPreference.Ignore, + }; +} - export function jsxModeNeedsExplicitImport(jsx: ts.JsxEmit | undefined) { - return jsx === ts.JsxEmit.React || jsx === ts.JsxEmit.ReactNative; - } +export function jsxModeNeedsExplicitImport(jsx: ts.JsxEmit | undefined) { + return jsx === ts.JsxEmit.React || jsx === ts.JsxEmit.ReactNative; +} - // #endregion +// #endregion } diff --git a/src/shims/collectionShims.ts b/src/shims/collectionShims.ts index 05f80f6479ed6..2b6cacf46c2be 100644 --- a/src/shims/collectionShims.ts +++ b/src/shims/collectionShims.ts @@ -1,304 +1,304 @@ /* @internal */ namespace ts { - type GetIteratorCallback = | ReadonlyMapShim | undefined>(iterable: I) => IteratorShim ? [ - K, - V - ] : I extends ReadonlySetShim ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; - type IteratorResultShim = { - value: T; - done?: false; - } | { - value: void; - done: true; - }; - - interface IteratorShim { - next(): IteratorResultShim; - } - - interface ReadonlyMapShim { - readonly size: number; - get(key: K): V | undefined; - has(key: K): boolean; - keys(): IteratorShim; - values(): IteratorShim; - entries(): IteratorShim<[ - K, - V - ]>; - forEach(action: (value: V, key: K) => void): void; - } +type GetIteratorCallback = | ReadonlyMapShim | undefined>(iterable: I) => IteratorShim ? [ + K, + V +] : I extends ReadonlySetShim ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; +type IteratorResultShim = { + value: T; + done?: false; +} | { + value: void; + done: true; +}; - interface MapShim extends ReadonlyMapShim { - set(key: K, value: V): this; - delete(key: K): boolean; - clear(): void; - } +interface IteratorShim { + next(): IteratorResultShim; +} - type MapShimConstructor = new (iterable?: readonly (readonly [ +interface ReadonlyMapShim { + readonly size: number; + get(key: K): V | undefined; + has(key: K): boolean; + keys(): IteratorShim; + values(): IteratorShim; + entries(): IteratorShim<[ K, V - ])[] | ReadonlyMapShim) => MapShim; + ]>; + forEach(action: (value: V, key: K) => void): void; +} - interface ReadonlySetShim { - readonly size: number; - has(value: T): boolean; - keys(): IteratorShim; - values(): IteratorShim; - entries(): IteratorShim<[ - T, - T - ]>; - forEach(action: (value: T, key: T) => void): void; - } +interface MapShim extends ReadonlyMapShim { + set(key: K, value: V): this; + delete(key: K): boolean; + clear(): void; +} - interface SetShim extends ReadonlySetShim { - add(value: T): this; - delete(value: T): boolean; - clear(): void; - } +type MapShimConstructor = new (iterable?: readonly (readonly [ + K, + V +])[] | ReadonlyMapShim) => MapShim; - type SetShimConstructor = new (iterable?: readonly T[] | ReadonlySetShim) => SetShim; +interface ReadonlySetShim { + readonly size: number; + has(value: T): boolean; + keys(): IteratorShim; + values(): IteratorShim; + entries(): IteratorShim<[ + T, + T + ]>; + forEach(action: (value: T, key: T) => void): void; +} - interface MapData { - size: number; - readonly head: MapEntry; - tail: MapEntry; - } +interface SetShim extends ReadonlySetShim { + add(value: T): this; + delete(value: T): boolean; + clear(): void; +} - interface MapEntry { - readonly key?: K; - value?: V; - /** - * Specifies the next entry in the linked list. - */ - next?: MapEntry; - /** - * Specifies the previous entry in the linked list. - * Must be set when the entry is part of a Map/Set. - * When 'undefined', iterators should skip the next entry. - * This will be set to 'undefined' when an entry is deleted. - * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. - */ - prev?: MapEntry; - } +type SetShimConstructor = new (iterable?: readonly T[] | ReadonlySetShim) => SetShim; - interface IteratorData { - current?: MapEntry; - selector: (key: K, value: V) => U; - } +interface MapData { + size: number; + readonly head: MapEntry; + tail: MapEntry; +} - function createMapData(): MapData { - const sentinel: MapEntry = {}; - sentinel.prev = sentinel; - return { head: sentinel, tail: sentinel, size: 0 }; - } +interface MapEntry { + readonly key?: K; + value?: V; + /** + * Specifies the next entry in the linked list. + */ + next?: MapEntry; + /** + * Specifies the previous entry in the linked list. + * Must be set when the entry is part of a Map/Set. + * When 'undefined', iterators should skip the next entry. + * This will be set to 'undefined' when an entry is deleted. + * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. + */ + prev?: MapEntry; +} - function createMapEntry(key: K, value: V): MapEntry { - return { key, value, next: undefined, prev: undefined }; - } +interface IteratorData { + current?: MapEntry; + selector: (key: K, value: V) => U; +} - function sameValueZero(x: unknown, y: unknown) { - // Treats -0 === 0 and NaN === NaN - return x === y || x !== x && y !== y; - } +function createMapData(): MapData { + const sentinel: MapEntry = {}; + sentinel.prev = sentinel; + return { head: sentinel, tail: sentinel, size: 0 }; +} - function getPrev(entry: MapEntry) { - const prev = entry.prev; - // Entries without a 'prev' have been removed from the map. - // An entry whose 'prev' points to itself is the head of the list and is invalid here. - if (!prev || prev === entry) - throw new Error("Illegal state"); - return prev; - } +function createMapEntry(key: K, value: V): MapEntry { + return { key, value, next: undefined, prev: undefined }; +} - function getNext(entry: MapEntry | undefined) { - while (entry) { - // Entries without a 'prev' have been removed from the map. Their 'next' - // pointer should point to the previous entry prior to deletion and - // that entry should be skipped to resume iteration. - const skipNext = !entry.prev; - entry = entry.next; - if (skipNext) { - continue; - } - return entry; - } - } +function sameValueZero(x: unknown, y: unknown) { + // Treats -0 === 0 and NaN === NaN + return x === y || x !== x && y !== y; +} - function getEntry(data: MapData, key: K): MapEntry | undefined { - // We walk backwards from 'tail' to prioritize recently added entries. - // We skip 'head' because it is an empty entry used to track iteration start. - for (let entry = data.tail; entry !== data.head; entry = getPrev(entry)) { - if (sameValueZero(entry.key, key)) { - return entry; - } +function getPrev(entry: MapEntry) { + const prev = entry.prev; + // Entries without a 'prev' have been removed from the map. + // An entry whose 'prev' points to itself is the head of the list and is invalid here. + if (!prev || prev === entry) + throw new Error("Illegal state"); + return prev; +} + +function getNext(entry: MapEntry | undefined) { + while (entry) { + // Entries without a 'prev' have been removed from the map. Their 'next' + // pointer should point to the previous entry prior to deletion and + // that entry should be skipped to resume iteration. + const skipNext = !entry.prev; + entry = entry.next; + if (skipNext) { + continue; } + return entry; } +} - function addOrUpdateEntry(data: MapData, key: K, value: V): MapEntry | undefined { - const existing = getEntry(data, key); - if (existing) { - existing.value = value; - return; +function getEntry(data: MapData, key: K): MapEntry | undefined { + // We walk backwards from 'tail' to prioritize recently added entries. + // We skip 'head' because it is an empty entry used to track iteration start. + for (let entry = data.tail; entry !== data.head; entry = getPrev(entry)) { + if (sameValueZero(entry.key, key)) { + return entry; } + } +} - const entry = createMapEntry(key, value); - entry.prev = data.tail; - data.tail.next = entry; - data.tail = entry; - data.size++; - return entry; +function addOrUpdateEntry(data: MapData, key: K, value: V): MapEntry | undefined { + const existing = getEntry(data, key); + if (existing) { + existing.value = value; + return; } - function deleteEntry(data: MapData, key: K): MapEntry | undefined { - // We walk backwards from 'tail' to prioritize recently added entries. - // We skip 'head' because it is an empty entry used to track iteration start. - for (let entry = data.tail; entry !== data.head; entry = getPrev(entry)) { - // all entries in the map should have a 'prev' pointer. - if (entry.prev === undefined) - throw new Error("Illegal state"); - if (sameValueZero(entry.key, key)) { - if (entry.next) { - entry.next.prev = entry.prev; - } - else { - // an entry in the map without a 'next' pointer must be the 'tail'. - if (data.tail !== entry) - throw new Error("Illegal state"); - data.tail = entry.prev; - } + const entry = createMapEntry(key, value); + entry.prev = data.tail; + data.tail.next = entry; + data.tail = entry; + data.size++; + return entry; +} - entry.prev.next = entry.next; - entry.next = entry.prev; - entry.prev = undefined; - data.size--; - return entry; +function deleteEntry(data: MapData, key: K): MapEntry | undefined { + // We walk backwards from 'tail' to prioritize recently added entries. + // We skip 'head' because it is an empty entry used to track iteration start. + for (let entry = data.tail; entry !== data.head; entry = getPrev(entry)) { + // all entries in the map should have a 'prev' pointer. + if (entry.prev === undefined) + throw new Error("Illegal state"); + if (sameValueZero(entry.key, key)) { + if (entry.next) { + entry.next.prev = entry.prev; + } + else { + // an entry in the map without a 'next' pointer must be the 'tail'. + if (data.tail !== entry) + throw new Error("Illegal state"); + data.tail = entry.prev; } + + entry.prev.next = entry.next; + entry.next = entry.prev; + entry.prev = undefined; + data.size--; + return entry; } } +} - function clearEntries(data: MapData) { - let node = data.tail; - while (node !== data.head) { - const prev = getPrev(node); - node.next = data.head; - node.prev = undefined; - node = prev; - } - data.head.next = undefined; - data.tail = data.head; - data.size = 0; +function clearEntries(data: MapData) { + let node = data.tail; + while (node !== data.head) { + const prev = getPrev(node); + node.next = data.head; + node.prev = undefined; + node = prev; } + data.head.next = undefined; + data.tail = data.head; + data.size = 0; +} - function forEachEntry(data: MapData, action: (value: V, key: K) => void) { - let entry: MapEntry | undefined = data.head; - while (entry) { - entry = getNext(entry); - if (entry) { - action(entry.value!, entry.key!); - } +function forEachEntry(data: MapData, action: (value: V, key: K) => void) { + let entry: MapEntry | undefined = data.head; + while (entry) { + entry = getNext(entry); + if (entry) { + action(entry.value!, entry.key!); } } +} - function forEachIteration(iterator: IteratorShim | undefined, action: (value: any) => void) { - if (iterator) { - for (let step = iterator.next(); !step.done; step = iterator.next()) { - action(step.value); - } +function forEachIteration(iterator: IteratorShim | undefined, action: (value: any) => void) { + if (iterator) { + for (let step = iterator.next(); !step.done; step = iterator.next()) { + action(step.value); } } +} - function createIteratorData(data: MapData, selector: (key: K, value: V) => U): IteratorData { - return { current: data.head, selector }; - } +function createIteratorData(data: MapData, selector: (key: K, value: V) => U): IteratorData { + return { current: data.head, selector }; +} - function iteratorNext(data: IteratorData): IteratorResultShim { - // Navigate to the next entry. - data.current = getNext(data.current); - if (data.current) { - return { value: data.selector(data.current.key!, data.current.value!), done: false }; - } - else { - return { value: undefined as never, done: true }; - } +function iteratorNext(data: IteratorData): IteratorResultShim { + // Navigate to the next entry. + data.current = getNext(data.current); + if (data.current) { + return { value: data.selector(data.current.key!, data.current.value!), done: false }; } + else { + return { value: undefined as never, done: true }; + } +} - /* @internal */ - export namespace ShimCollections { - export function createMapShim(getIterator: GetIteratorCallback): MapShimConstructor { - class MapIterator { + private _data: IteratorData; + constructor(data: MapData, selector: (key: K, value: V) => U) { + this._data = createIteratorData(data, selector); + } + next() { return iteratorNext(this._data); } + } + return class Map implements MapShim { + private _mapData = createMapData(); + constructor(iterable?: readonly (readonly [ K, V - ])> { - private _data: IteratorData; - constructor(data: MapData, selector: (key: K, value: V) => U) { - this._data = createIteratorData(data, selector); - } - next() { return iteratorNext(this._data); } + ])[] | ReadonlyMapShim) { + forEachIteration(getIterator(iterable), ([key, value]) => this.set(key, value)); } - return class Map implements MapShim { - private _mapData = createMapData(); - constructor(iterable?: readonly (readonly [ - K, - V - ])[] | ReadonlyMapShim) { - forEachIteration(getIterator(iterable), ([key, value]) => this.set(key, value)); - } - get size() { return this._mapData.size; } - get(key: K): V | undefined { return getEntry(this._mapData, key)?.value; } - set(key: K, value: V): this { return addOrUpdateEntry(this._mapData, key, value), this; } - has(key: K): boolean { return !!getEntry(this._mapData, key); } - delete(key: K): boolean { return !!deleteEntry(this._mapData, key); } - clear(): void { clearEntries(this._mapData); } - keys(): IteratorShim { return new MapIterator(this._mapData, (key, _value) => key); } - values(): IteratorShim { return new MapIterator(this._mapData, (_key, value) => value); } - entries(): IteratorShim<[ - K, - V - ]> { return new MapIterator(this._mapData, (key, value) => [key, value]); } - forEach(action: (value: V, key: K) => void): void { forEachEntry(this._mapData, action); } - }; - } - - export function createSetShim(getIterator: GetIteratorCallback): SetShimConstructor { - class SetIterator { return new MapIterator(this._mapData, (key, _value) => key); } + values(): IteratorShim { return new MapIterator(this._mapData, (_key, value) => value); } + entries(): IteratorShim<[ K, V - ])> { - private _data: IteratorData; - constructor(data: MapData, selector: (key: K, value: V) => U) { - this._data = createIteratorData(data, selector); - } - next() { return iteratorNext(this._data); } + ]> { return new MapIterator(this._mapData, (key, value) => [key, value]); } + forEach(action: (value: V, key: K) => void): void { forEachEntry(this._mapData, action); } + }; + } + + export function createSetShim(getIterator: GetIteratorCallback): SetShimConstructor { + class SetIterator { + private _data: IteratorData; + constructor(data: MapData, selector: (key: K, value: V) => U) { + this._data = createIteratorData(data, selector); } - return class Set implements SetShim { - private _mapData = createMapData(); - constructor(iterable?: readonly T[] | ReadonlySetShim) { - forEachIteration(getIterator(iterable), value => this.add(value)); - } - get size() { return this._mapData.size; } - add(value: T): this { return addOrUpdateEntry(this._mapData, value, value), this; } - has(value: T): boolean { return !!getEntry(this._mapData, value); } - delete(value: T): boolean { return !!deleteEntry(this._mapData, value); } - clear(): void { clearEntries(this._mapData); } - keys(): IteratorShim { return new SetIterator(this._mapData, (key, _value) => key); } - values(): IteratorShim { return new SetIterator(this._mapData, (_key, value) => value); } - entries(): IteratorShim<[ - T, - T - ]> { return new SetIterator(this._mapData, (key, value) => [key, value]); } - forEach(action: (value: T, key: T) => void): void { forEachEntry(this._mapData, action); } - }; + next() { return iteratorNext(this._data); } } + return class Set implements SetShim { + private _mapData = createMapData(); + constructor(iterable?: readonly T[] | ReadonlySetShim) { + forEachIteration(getIterator(iterable), value => this.add(value)); + } + get size() { return this._mapData.size; } + add(value: T): this { return addOrUpdateEntry(this._mapData, value, value), this; } + has(value: T): boolean { return !!getEntry(this._mapData, value); } + delete(value: T): boolean { return !!deleteEntry(this._mapData, value); } + clear(): void { clearEntries(this._mapData); } + keys(): IteratorShim { return new SetIterator(this._mapData, (key, _value) => key); } + values(): IteratorShim { return new SetIterator(this._mapData, (_key, value) => value); } + entries(): IteratorShim<[ + T, + T + ]> { return new SetIterator(this._mapData, (key, value) => [key, value]); } + forEach(action: (value: T, key: T) => void): void { forEachEntry(this._mapData, action); } + }; } } +} diff --git a/src/testRunner/compilerRef.ts b/src/testRunner/compilerRef.ts index b76e4e72fbb88..245705db5ddac 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 +namespace compiler {} diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index 3f2c0c964130d..63fbc92683139 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -1,311 +1,311 @@ namespace Harness { - export const enum CompilerTestType { - Conformance, - Regressions, - Test262 - } +export const enum CompilerTestType { + Conformance, + Regressions, + Test262 +} - interface CompilerFileBasedTest extends Harness.FileBasedTest { - readonly content?: string; - } +interface CompilerFileBasedTest extends Harness.FileBasedTest { + readonly content?: string; +} - export class CompilerBaselineRunner extends Harness.RunnerBase { - private basePath = "tests/cases"; - private testSuiteName: Harness.TestRunnerKind; - private emit: boolean; +export class CompilerBaselineRunner extends Harness.RunnerBase { + private basePath = "tests/cases"; + private testSuiteName: Harness.TestRunnerKind; + private emit: boolean; - public options: string | undefined; + 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; + 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"; } + else { + this.testSuiteName = "compiler"; // default to this for historical reasons + } + this.basePath += "/" + this.testSuiteName; + } - public initializeTests() { - describe(this.testSuiteName + " tests", () => { - describe("Setup compiler for compiler baselines", () => { - this.parseOptions(); - }); + public kind() { + return this.testSuiteName; + } - // 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 : Harness.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); - }); + 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(); }); - } - public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { - if (test && ts.some(test.configurations)) { - test.configurations.forEach(configuration => { - describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${Harness.getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { - this.runSuite(fileName, test, configuration); - }); - }); - } - else { - describe(`${this.testSuiteName} tests for ${fileName}`, () => { - this.runSuite(fileName, test); - }); - } - } + // 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 : Harness.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); + }); + }); + } - private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: Harness.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 = Harness.TestCaseParser.makeUnitsFromTest(test.content, test.file, rootDir); - } - compilerTest = new CompilerTest(fileName, payload, configuration); + public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { + if (test && ts.some(test.configurations)) { + test.configurations.forEach(configuration => { + describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${Harness.getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { + this.runSuite(fileName, test, 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}`, () => (this.emit && compilerTest.verifyJavaScriptOutput())); - it(`Correct Sourcemap output for ${fileName}`, () => compilerTest.verifySourceMapOutput()); - it(`Correct type/symbol baselines for ${fileName}`, () => compilerTest.verifyTypesAndSymbols()); - after(() => { - compilerTest = undefined!; + } + else { + describe(`${this.testSuiteName} tests for ${fileName}`, () => { + this.runSuite(fileName, test); }); } + } - 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?: Harness.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 = Harness.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}`, () => (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", - "moduleResolution", - "moduleDetection", - "target", - "jsx", - "removeComments", - "importHelpers", - "importHelpers", - "downlevelIteration", - "isolatedModules", - "strict", - "noImplicitAny", - "strictNullChecks", - "strictFunctionTypes", - "strictBindCallApply", - "strictPropertyInitialization", - "noImplicitThis", - "alwaysStrict", - "allowSyntheticDefaultImports", - "esModuleInterop", - "emitDecoratorMetadata", - "skipDefaultLibCheck", - "preserveConstEnums", - "skipLibCheck", - "exactOptionalPropertyTypes", - "useUnknownInCatchVariables" - ]; - private fileName: string; - private justName: string; - private configuredName: string; - private lastUnit: Harness.TestCaseParser.TestUnitData; - private harnessSettings: Harness.TestCaseParser.CompilerSettings; - private hasNonDtsFiles: boolean; - private result: compiler.CompilationResult; - private options: ts.CompilerOptions; - private tsConfigFiles: Harness.Compiler.TestFile[]; - // equivalent to the files that will be passed on the command line - private toBeCompiled: Harness.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: Harness.Compiler.TestFile[]; - constructor(fileName: string, testCaseContent?: Harness.TestCaseParser.TestCaseContent, configurationOverrides?: Harness.TestCaseParser.CompilerSettings) { - this.fileName = fileName; - this.justName = vpath.basename(fileName); - this.configuredName = this.justName; - if (configurationOverrides) { - let configuredName = ""; - const keys = Object - .keys(configurationOverrides) - .sort(); - for (const key of keys) { - if (configuredName) { - configuredName += ","; - } - configuredName += `${key.toLowerCase()}=${configurationOverrides[key].toLowerCase()}`; - } +class CompilerTest { + private static varyBy: readonly string[] = [ + "module", + "moduleResolution", + "moduleDetection", + "target", + "jsx", + "removeComments", + "importHelpers", + "importHelpers", + "downlevelIteration", + "isolatedModules", + "strict", + "noImplicitAny", + "strictNullChecks", + "strictFunctionTypes", + "strictBindCallApply", + "strictPropertyInitialization", + "noImplicitThis", + "alwaysStrict", + "allowSyntheticDefaultImports", + "esModuleInterop", + "emitDecoratorMetadata", + "skipDefaultLibCheck", + "preserveConstEnums", + "skipLibCheck", + "exactOptionalPropertyTypes", + "useUnknownInCatchVariables" + ]; + private fileName: string; + private justName: string; + private configuredName: string; + private lastUnit: Harness.TestCaseParser.TestUnitData; + private harnessSettings: Harness.TestCaseParser.CompilerSettings; + private hasNonDtsFiles: boolean; + private result: compiler.CompilationResult; + private options: ts.CompilerOptions; + private tsConfigFiles: Harness.Compiler.TestFile[]; + // equivalent to the files that will be passed on the command line + private toBeCompiled: Harness.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: Harness.Compiler.TestFile[]; + constructor(fileName: string, testCaseContent?: Harness.TestCaseParser.TestCaseContent, configurationOverrides?: Harness.TestCaseParser.CompilerSettings) { + this.fileName = fileName; + this.justName = vpath.basename(fileName); + this.configuredName = this.justName; + if (configurationOverrides) { + let configuredName = ""; + const keys = Object + .keys(configurationOverrides) + .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}`; + configuredName += ","; } + configuredName += `${key.toLowerCase()}=${configurationOverrides[key].toLowerCase()}`; } - - const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; - - if (testCaseContent === undefined) { - testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(Harness.IO.readFile(fileName)!, fileName, rootDir); + if (configuredName) { + const extname = vpath.extname(this.justName); + const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true); + this.configuredName = `${basename}(${configuredName})${extname}`; } + } - if (configurationOverrides) { - testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; - } + const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; - 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`); + if (testCaseContent === undefined) { + testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(Harness.IO.readFile(fileName)!, fileName, rootDir); + } - 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); - } - } + if (configurationOverrides) { + testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; + } - 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); - }); - } + 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`); - if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { - tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); - tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; + 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); } - - this.result = Harness.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 = Harness.IO.readFile(file)!; - const settings = Harness.TestCaseParser.extractCompilerSettings(content); - const configurations = Harness.getFileBasedTestConfigurations(settings, CompilerTest.varyBy); - return { file, configurations, content }; + 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)); + } + }); } - - public verifyDiagnostics() { - // check errors - Harness.Compiler.doErrorBaseline(this.configuredName, this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), this.result.diagnostics, !!this.options.pretty); + else { + this.toBeCompiled = units.map(unit => { + return this.createHarnessTestFile(unit, rootDir); + }); } - public verifyModuleResolution() { - if (this.options.traceResolution) { - Harness.Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".trace.json"), JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4)); - } + if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { + tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); + tsConfigOptions.configFile!.fileName = 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; - Harness.Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".sourcemap.txt"), baseline); - } - } + this.result = Harness.Compiler.compileFiles(this.toBeCompiled, this.otherFiles, this.harnessSettings, + /*options*/ tsConfigOptions, + /*currentDirectory*/ this.harnessSettings.currentDirectory, testCaseContent.symlinks); - public verifyJavaScriptOutput() { - if (this.hasNonDtsFiles) { - Harness.Compiler.doJsEmitBaseline(this.configuredName, this.fileName, this.options, this.result, this.tsConfigFiles, this.toBeCompiled, this.otherFiles, this.harnessSettings); - } + this.options = this.result.options; + } + + public static getConfigurations(file: string): CompilerFileBasedTest { + // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts + const content = Harness.IO.readFile(file)!; + const settings = Harness.TestCaseParser.extractCompilerSettings(content); + const configurations = Harness.getFileBasedTestConfigurations(settings, CompilerTest.varyBy); + return { file, configurations, content }; + } + + public verifyDiagnostics() { + // check errors + Harness.Compiler.doErrorBaseline(this.configuredName, this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), this.result.diagnostics, !!this.options.pretty); + } + + public verifyModuleResolution() { + if (this.options.traceResolution) { + Harness.Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".trace.json"), JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4)); } + } - public verifySourceMapOutput() { - Harness.Compiler.doSourcemapBaseline(this.configuredName, this.options, this.result, this.harnessSettings); + 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; + Harness.Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".sourcemap.txt"), baseline); } + } - public verifyTypesAndSymbols() { - if (this.fileName.indexOf("APISample") >= 0) { - return; - } + public verifyJavaScriptOutput() { + if (this.hasNonDtsFiles) { + Harness.Compiler.doJsEmitBaseline(this.configuredName, this.fileName, this.options, this.result, this.tsConfigFiles, this.toBeCompiled, this.otherFiles, this.harnessSettings); + } + } - const noTypesAndSymbols = this.harnessSettings.noTypesAndSymbols && - this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; - if (noTypesAndSymbols) { - return; - } + public verifySourceMapOutput() { + Harness.Compiler.doSourcemapBaseline(this.configuredName, this.options, this.result, this.harnessSettings); + } - Harness.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)); + public verifyTypesAndSymbols() { + if (this.fileName.indexOf("APISample") >= 0) { + return; } - private makeUnitName(name: string, root: string) { - const path = ts.toPath(name, root, ts.identity); - const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity); - return pathStart ? path.replace(pathStart, "/") : path; + const noTypesAndSymbols = this.harnessSettings.noTypesAndSymbols && + this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; + if (noTypesAndSymbols) { + return; } - private createHarnessTestFile(lastUnit: Harness.TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Harness.Compiler.TestFile { - return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; - } + Harness.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)); } + + private makeUnitName(name: string, root: string) { + const path = ts.toPath(name, root, ts.identity); + const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity); + return pathStart ? path.replace(pathStart, "/") : path; + } + + private createHarnessTestFile(lastUnit: Harness.TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Harness.Compiler.TestFile { + return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; + } +} } diff --git a/src/testRunner/documentsRef.ts b/src/testRunner/documentsRef.ts index d3d92746b4ed7..c1f869b7bd190 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 +namespace documents {} diff --git a/src/testRunner/evaluatorRef.ts b/src/testRunner/evaluatorRef.ts index cc81ec2402db5..a034813f9699f 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 +namespace evaluator {} diff --git a/src/testRunner/externalCompileRunner.ts b/src/testRunner/externalCompileRunner.ts index 8c7dc16e69140..743a2e134e53f 100644 --- a/src/testRunner/externalCompileRunner.ts +++ b/src/testRunner/externalCompileRunner.ts @@ -1,356 +1,356 @@ 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; - } +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; + branch?: string; + path?: string; +} - interface UserConfig { - types: string[]; - cloneUrl: string; - branch?: string; - path?: string; +abstract class ExternalCompileRunnerBase extends Harness.RunnerBase { + abstract testDir: string; + abstract report(result: ExecResult, cwd: string): string | null; + enumerateTestFiles() { + return Harness.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.Suite) { + this.timeout(600000); // 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.Suite) { + this.timeout(timeout); + const cp: typeof import("child_process") = require("child_process"); - abstract class ExternalCompileRunnerBase extends Harness.RunnerBase { - abstract testDir: string; - abstract report(result: ExecResult, cwd: string): string | null; - enumerateTestFiles() { - return Harness.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.Suite) { - this.timeout(600000); // 10 minutes - for (const test of testList) { - cls.runTest(typeof test === "string" ? test : test.file); + it("should build successfully", () => { + let cwd = path.join(Harness.IO.getWorkspaceRoot(), cls.testDir, directoryName); + const originalCwd = cwd; + const stdio = Harness.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", "-b", config.branch || "master", config.cloneUrl, path.join(submoduleDir, ".git")], { cwd }); + } + else { + exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "checkout", config.branch || "master"], { cwd: submoduleDir }); + 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 = 600000; // 10 minutes - describe(directoryName, function (this: Mocha.Suite) { - this.timeout(timeout); - const cp: typeof import("child_process") = require("child_process"); - - it("should build successfully", () => { - let cwd = path.join(Harness.IO.getWorkspaceRoot(), cls.testDir, directoryName); - const originalCwd = cwd; - const stdio = Harness.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", "-b", config.branch || "master", config.cloneUrl, path.join(submoduleDir, ".git")], { cwd }); - } - else { - exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "checkout", config.branch || "master"], { cwd: submoduleDir }); - 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; + const npmVersionText = exec("npm", ["--version"], { cwd, stdio: "pipe" })?.trim(); + const npmVersion = npmVersionText ? ts.Version.tryParse(npmVersionText.trim()) : undefined; + const isV7OrLater = !!npmVersion && npmVersion.major >= 7; + 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")); } - const npmVersionText = exec("npm", ["--version"], { cwd, stdio: "pipe" })?.trim(); - const npmVersion = npmVersionText ? ts.Version.tryParse(npmVersionText.trim()) : undefined; - const isV7OrLater = !!npmVersion && npmVersion.major >= 7; - 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", ...(isV7OrLater ? ["--legacy-peer-deps"] : [])], { 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(Harness.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", ...(isV7OrLater ? ["--legacy-peer-deps"] : [])], { 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", ...(isV7OrLater ? ["--legacy-peer-deps"] : [])], { 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(Harness.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", ...(isV7OrLater ? ["--legacy-peer-deps"] : [])], { 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"); - Harness.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; - stdio?: import("child_process").StdioOptions; - }): string | undefined { - const res = cp.spawnSync(Harness.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()}`); - } - return options.stdio === "pipe" ? res.stdout.toString("utf8") : undefined; + } + args.push("--noEmit"); + Harness.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; + stdio?: import("child_process").StdioOptions; + }): string | undefined { + const res = cp.spawnSync(Harness.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()}`); } - }); + return options.stdio === "pipe" ? res.stdout.toString("utf8") : undefined; + } }); - } + }); } +} - export class UserCodeRunner extends ExternalCompileRunnerBase { - readonly testDir = "tests/cases/user/"; - kind(): Harness.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(): Harness.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(): Harness.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.Suite) { - this.timeout(cls.timeout); // 20 minutes - before(() => { - cls.exec("docker", ["build", ".", "-t", "typescript/typescript"], { cwd: Harness.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(Harness.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"); - Harness.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(): Harness.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.Suite) { + this.timeout(cls.timeout); // 20 minutes + before(() => { + cls.exec("docker", ["build", ".", "-t", "typescript/typescript"], { cwd: Harness.IO.getWorkspaceRoot() }); // cached because workspace is hashed to determine cacheability }); - } - - 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 = Harness.isWorker ? "pipe" : "inherit"; - const res = cp.spawnSync(Harness.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(Harness.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"); + Harness.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 = Harness.isWorker ? "pipe" : "inherit"; + const res = cp.spawnSync(Harness.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 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 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 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 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 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"); - } +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, ""); - } +/** + * 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 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"); - } +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(Harness.IO.getWorkspaceRoot().replace(/\\/g, "\\\\"), "g"); - return result - .replace(/import\(".*?\/tests\/cases\/user\//g, `import("/`) - .replace(/Module '".*?\/tests\/cases\/user\//g, `Module '"/`) - .replace(workspaceRegexp, "../../.."); - } +/** + * 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(Harness.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 ts.flatten(splitBy(result.split("\n"), s => /^\S+/.test(s)).sort(compareErrorStrings)).join("\n"); - } +function sortErrors(result: string) { + return ts.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[]) { - 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 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")); +} - export class DefinitelyTypedRunner extends ExternalCompileRunnerBase { - readonly testDir = "../DefinitelyTyped/types/"; - workingDirectory = this.testDir; - kind(): Harness.TestRunnerKind { - return "dt"; - } - report(result: ExecResult, cwd: string) { - const stdout = removeExpectedErrors(result.stdout.toString(), cwd); - const stderr = result.stderr.toString(); +export class DefinitelyTypedRunner extends ExternalCompileRunnerBase { + readonly testDir = "../DefinitelyTyped/types/"; + workingDirectory = this.testDir; + kind(): Harness.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} + // 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 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]; } - 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/fakesRef.ts b/src/testRunner/fakesRef.ts index b19d4cc8c80ed..4743acab2db42 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 +namespace fakes {} diff --git a/src/testRunner/fourslashRef.ts b/src/testRunner/fourslashRef.ts index 23a50810a46af..6eae77e2bd25f 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 +namespace FourSlash {} diff --git a/src/testRunner/fourslashRunner.ts b/src/testRunner/fourslashRunner.ts index 20432f5c42dee..3f5db6a7fa145 100644 --- a/src/testRunner/fourslashRunner.ts +++ b/src/testRunner/fourslashRunner.ts @@ -1,73 +1,73 @@ namespace Harness { - export class FourSlashRunner extends Harness.RunnerBase { - protected basePath: string; - protected testSuiteName: Harness.TestRunnerKind; +export class FourSlashRunner extends Harness.RunnerBase { + protected basePath: string; + protected testSuiteName: Harness.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); - } + 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); } + } - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false }); - } + public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false }); + } - public kind() { - return this.testSuiteName; - } + public kind() { + return this.testSuiteName; + } - public initializeTests() { - if (this.tests.length === 0) { - this.tests = Harness.IO.enumerateTestFiles(this); - } + public initializeTests() { + if (this.tests.length === 0) { + this.tests = Harness.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(/^.*[\\\/]/, ""); + 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); + // Convert to relative path + const testIndex = fn.indexOf("tests/"); + if (testIndex >= 0) + fn = fn.substr(testIndex); - if (justName !== "fourslash.ts") { - it(this.testSuiteName + " test " + justName + " runs correctly", () => { - FourSlash.runFourSlashTest(this.basePath, this.testType, fn); - }); - } - }); + if (justName !== "fourslash.ts") { + it(this.testSuiteName + " test " + justName + " runs correctly", () => { + FourSlash.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: FourSlash.FourSlashTestType) { + super(testType); + this.basePath += "/generated/"; } } +} diff --git a/src/testRunner/parallel/host.ts b/src/testRunner/parallel/host.ts index 66b815d9b90ac..ac22335fb56cc 100644 --- a/src/testRunner/parallel/host.ts +++ b/src/testRunner/parallel/host.ts @@ -1,642 +1,642 @@ 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(Utils.findUpFile("scripts/failed-tests.js")) as typeof import("../../../scripts/failed-tests"); - - const perfdataFileNameFragment = ".parallelperf"; - const perfData = readSavedPerfData(Harness.configOption); - const newTasks: Harness.Parallel.Task[] = []; - let tasks: Harness.Parallel.Task[] = []; - let unknownValue: string | undefined; - let totalCost = 0; - - class RemoteSuite extends Mocha.Suite { - suiteMap = new ts.Map(); - 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); - } +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(Utils.findUpFile("scripts/failed-tests.js")) as typeof import("../../../scripts/failed-tests"); + + const perfdataFileNameFragment = ".parallelperf"; + const perfData = readSavedPerfData(Harness.configOption); + const newTasks: Harness.Parallel.Task[] = []; + let tasks: Harness.Parallel.Task[] = []; + let unknownValue: string | undefined; + let totalCost = 0; + + class RemoteSuite extends Mocha.Suite { + suiteMap = new ts.Map(); + constructor(title: string) { + super(title); + this.pending = false; + this.delayed = false; } - - class RemoteTest extends Mocha.Test { - info: Harness.Parallel.ErrorInfo | Harness.Parallel.TestInfo; - constructor(info: Harness.Parallel.ErrorInfo | Harness.Parallel.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: Harness.Parallel.ErrorInfo | Harness.Parallel.TestInfo; + constructor(info: Harness.Parallel.ErrorInfo | Harness.Parallel.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 ProgressBar { - lastN?: number; - title?: string; - progressColor?: string; - text?: string; - } + interface Worker { + process: import("child_process").ChildProcess; + accumulatedOutput: string; + currentTasks?: { + file: string; + }[]; + timer?: any; + } - 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 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; + } + enable() { + if (!this._enabled) { + process.stdout.write(os.EOL); + this._enabled = true; } - enable() { - if (!this._enabled) { - process.stdout.write(os.EOL); - this._enabled = true; - } + } + disable() { + if (this._enabled) { + process.stdout.write(os.EOL); + this._enabled = false; } - disable() { - if (this._enabled) { - process.stdout.write(os.EOL); - this._enabled = false; - } + } + 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; } - 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); + progressBar.lastN = n; + progressBar.title = title; + progressBar.progressColor = color; - if (title) { - progress += this._color(titleColor || "progress", " " + title); - } + 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 (progressBar.text !== progress) { - progressBar.text = progress; - this._render(index); - } + if (title) { + progress += this._color(titleColor || "progress", " " + title); } - 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); - } - else { - readline.moveCursor(process.stdout, -process.stdout.columns, +1); - } + if (progressBar.text !== progress) { + progressBar.text = progress; + this._render(index); + } + } + private _render(index: number) { + if (!this._enabled || !isatty) { + return; + } - lineCount++; + 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); } - 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 = Harness.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 perfdataFileName(target?: string) { + return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; + } - function hashName(runner: Harness.TestRunnerKind | "unittest", test: string) { - return `tsrunner-${runner}://${test}`; + function readSavedPerfData(target?: string): { + [testHash: string]: number; + } | undefined { + const perfDataContents = Harness.IO.readFile(perfdataFileName(target)); + if (perfDataContents) { + return JSON.parse(perfDataContents); } + return undefined; + } - 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 Harness.runners) { - for (const test of runner.getTestFiles()) { - const file = typeof test === "string" ? test : test.file; - let size: number; - if (!perfData) { + function hashName(runner: Harness.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 Harness.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 = Harness.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 = Harness.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 = Harness.workerCount; - const packfraction = 0.9; - const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test - const batchSize = (totalCost / Harness.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 ${Harness.workerCount} threads...`); - - const totalFiles = tasks.length; - let passingFiles = 0; - let failingFiles = 0; - let errorResults: Harness.Parallel.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 < Harness.workerCount; i++) { - // TODO: Just send the config over the IPC channel or in the command line arguments - const config: Harness.TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: Harness.runUnitTests, stackTraceLimit: Harness.stackTraceLimit, timeout: Harness.globalTimeout }; // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - const configPath = ts.combinePaths(Harness.taskConfigsFolder, `task-config${i}.json`); - Harness.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: Harness.Parallel.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 = Harness.workerCount; + const packfraction = 0.9; + const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test + const batchSize = (totalCost / Harness.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 ${Harness.workerCount} threads...`); + + const totalFiles = tasks.length; + let passingFiles = 0; + let failingFiles = 0; + let errorResults: Harness.Parallel.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 < Harness.workerCount; i++) { + // TODO: Just send the config over the IPC channel or in the command line arguments + const config: Harness.TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: Harness.runUnitTests, stackTraceLimit: Harness.stackTraceLimit, timeout: Harness.globalTimeout }; // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier + const configPath = ts.combinePaths(Harness.taskConfigsFolder, `task-config${i}.json`); + Harness.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: Harness.Parallel.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: Harness.Parallel.ParallelClientMessage) => { - switch (data.type) { - case "error": { - console.error(`Test worker encountered 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: Harness.Parallel.ParallelClientMessage) => { + switch (data.type) { + case "error": { + console.error(`Test worker encountered 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; + else { + // eslint-disable-next-line no-restricted-globals + worker.timer = setTimeout(killChild, data.payload.duration, data.payload); + } + break; + } + 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); + 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 === Harness.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 Harness.Parallel.ParallelHostMessage); // TODO: GH#18217 - } - else { - worker.process.send({ type: "batch", payload: taskList } as Harness.Parallel.ParallelHostMessage); // TODO: GH#18217 + if (data.type === "result") { + if (tasks.length === 0) { + // No more tasks to distribute + worker.process.send({ type: "close" }); + closedWorkers++; + if (closedWorkers === Harness.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 Harness.Parallel.ParallelHostMessage); // TODO: GH#18217 + } + else { + worker.process.send({ type: "batch", payload: taskList } as Harness.Parallel.ParallelHostMessage); // TODO: GH#18217 } } } - }); - workers.push(worker); - } + } + }); + 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: Harness.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 <= Harness.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; - } - 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; - } - const task = tasks.pop()!; - batches[i].push(task); - scheduledTotal += task.size; + // 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: Harness.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 <= Harness.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; } - for (let j = 0; j < batchCount; j++) { - if (!doneBatching[j]) { - continue batcher; - } + if (doneBatching[i]) { + continue; } - 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 }); + if (!batches[i]) { + batches[i] = []; + } + const total = batches[i].reduce((p, c) => p + c.size, 0); + if (total >= batchSize) { + doneBatching[i] = true; + continue; } - 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 }); + const task = tasks.pop()!; + batches[i].push(task); + scheduledTotal += task.size; + } + for (let j = 0; j < batchCount; j++) { + if (!doneBatching[j]) { + continue batcher; } } + break; + } + const prefix = `Batched into ${batchCount} groups`; + if (unknownValue) { + console.log(`${prefix}. Unprofiled tests including ${unknownValue} will be run first.`); } else { - for (let i = 0; i < Harness.workerCount; i++) { - const task = tasks.pop()!; - workers[i].currentTasks = [task]; - workers[i].process.send({ type: "test", payload: task }); + 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 }); + } + 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 }); } } + } + else { + for (let i = 0; i < Harness.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; + progressBars.enable(); + updateProgress(0); + let duration: number; + let endDate: Date; - const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; - const summaryDuration = "(" + ms(duration) + ")"; - const savedUseColors = Base.useColors; - Base.useColors = !Harness.noColors; + function completeBar() { + const isPartitionFail = failingFiles !== 0; + const summaryColor = isPartitionFail ? "fail" : "green"; + const summarySymbol = isPartitionFail ? Base.symbols.err : Base.symbols.ok; - const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); - Base.useColors = savedUseColors; + const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; + const summaryDuration = "(" + ms(duration) + ")"; + const savedUseColors = Base.useColors; + Base.useColors = !Harness.noColors; - updateProgress(1, summary); - } + const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); + Base.useColors = savedUseColors; - function updateProgress(percentComplete: number, title?: string, titleColor?: string) { - let progressColor = "pending"; - if (failingFiles) { - progressColor = "fail"; - } + updateProgress(1, summary); + } - progressBars.update(0, percentComplete, progressColor, title, titleColor); + function updateProgress(percentComplete: number, title?: string, titleColor?: string) { + let progressColor = "pending"; + if (failingFiles) { + progressColor = "fail"; } - 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) { } - } - }); - } + progressBars.update(0, percentComplete, progressColor, title, titleColor); + } - function rebuildSuite(failures: Harness.Parallel.ErrorInfo[], passes: Harness.Parallel.TestInfo[]) { - const root = new RemoteSuite(""); - for (const result of [...failures, ...passes] as (Harness.Parallel.ErrorInfo | Harness.Parallel.TestInfo)[]) { - getSuite(root, result.name.slice(0, -1)).addTest(new RemoteTest(result)); + 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) { } } - 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: Harness.Parallel.ErrorInfo) { - const error = new Error(result.error); - error.stack = result.stack; - return error; + function rebuildSuite(failures: Harness.Parallel.ErrorInfo[], passes: Harness.Parallel.TestInfo[]) { + const root = new RemoteSuite(""); + for (const result of [...failures, ...passes] as (Harness.Parallel.ErrorInfo | Harness.Parallel.TestInfo)[]) { + getSuite(root, result.name.slice(0, -1)).addTest(new RemoteTest(result)); } - - 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); + 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 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); - } - runner.emit("test end", test); - } + function rebuildError(result: Harness.Parallel.ErrorInfo) { + const error = new Error(result.error); + error.stack = result.stack; + return error; + } - 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`); + function replaySuite(runner: Mocha.Runner, suite: RemoteSuite) { + runner.emit("suite", suite); + for (const test of suite.tests) { + replayTest(runner, test as RemoteTest); } - else { - failedTestReporter = new FailedTestReporter(replayRunner, { - reporterOptions: { - file: path.resolve(".failed-tests"), - keepFailed: Harness.keepFailed // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - } - }); + for (const child of suite.suites) { + replaySuite(runner, child as RemoteSuite); } + runner.emit("suite end", suite); + } - const savedUseColors = Base.useColors; - if (Harness.noColors) - Base.useColors = false; - replayRunner.started = true; - replayRunner.emit("start"); - replaySuite(replayRunner, rebuildSuite(errorResults, passingResults)); - replayRunner.emit("end"); - consoleReporter.epilogue(); - if (Harness.noColors) - Base.useColors = savedUseColors; - - // eslint-disable-next-line no-null/no-null - Harness.IO.writeFile(perfdataFileName(Harness.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)); + 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`); + } + else { + failedTestReporter = new FailedTestReporter(replayRunner, { + reporterOptions: { + file: path.resolve(".failed-tests"), + keepFailed: Harness.keepFailed // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier + } + }); } - return s.length > size ? s.substr(0, size) : s; - } + const savedUseColors = Base.useColors; + if (Harness.noColors) + Base.useColors = false; + replayRunner.started = true; + replayRunner.emit("start"); + replaySuite(replayRunner, rebuildSuite(errorResults, passingResults)); + replayRunner.emit("end"); + consoleReporter.epilogue(); + if (Harness.noColors) + Base.useColors = savedUseColors; - function minMax(value: number, min: number, max: number) { - if (value < min) - return min; - if (value > max) - return max; - return value; - } + // eslint-disable-next-line no-null/no-null + Harness.IO.writeFile(perfdataFileName(Harness.configOption), JSON.stringify(newPerfData, null, 4)); - function shimDiscoveryInterface(context: Mocha.MochaGlobals) { - Harness.Parallel.shimNoopTestInterface(context); - const perfData = readSavedPerfData(Harness.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; + 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 (Harness.runUnitTests) { - shimDiscoveryInterface(global); + function fill(ch: string, size: number) { + let s = ""; + while (s.length < size) { + s += ch; } - else { - Harness.Parallel.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) { + Harness.Parallel.shimNoopTestInterface(context); + const perfData = readSavedPerfData(Harness.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 (Harness.runUnitTests) { + shimDiscoveryInterface(global); + } + else { + Harness.Parallel.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 e9c669324d662..c141f66be822f 100644 --- a/src/testRunner/parallel/shared.ts +++ b/src/testRunner/parallel/shared.ts @@ -1,92 +1,92 @@ namespace Harness.Parallel { - export interface RunnerTask { - runner: Harness.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[]; +export interface RunnerTask { + runner: Harness.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) => { }) 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) => { }) 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 = ts.noop; + global.after = ts.noop; + global.beforeEach = ts.noop; + global.afterEach = ts.noop; + global.describe = global.context = ((_: any, __: any) => { }) 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) => { }) as Mocha.TestFunction; + global.it.skip = global.xit = global.xspecify = ts.noop as Mocha.PendingTestFunction; + global.it.only = ts.noop as Mocha.ExclusiveTestFunction; +} } diff --git a/src/testRunner/parallel/worker.ts b/src/testRunner/parallel/worker.ts index 217236eb4f480..61fb1a366072c 100644 --- a/src/testRunner/parallel/worker.ts +++ b/src/testRunner/parallel/worker.ts @@ -1,324 +1,324 @@ namespace Harness.Parallel.Worker { - export function start() { - function hookUncaughtExceptions() { - if (!exceptionsHooked) { - process.on("uncaughtException", handleUncaughtException); - process.on("unhandledRejection", handleUncaughtException); - exceptionsHooked = true; - } +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; - } + function unhookUncaughtExceptions() { + if (exceptionsHooked) { + process.removeListener("uncaughtException", handleUncaughtException); + process.removeListener("unhandledRejection", handleUncaughtException); + exceptionsHooked = false; } + } - let exceptionsHooked = false; - hookUncaughtExceptions(); + let exceptionsHooked = false; + hookUncaughtExceptions(); - // Capitalization is aligned with the global `Mocha` namespace for typespace/namespace references. - const Mocha = require("mocha") as typeof import("mocha"); + // 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; + /** + * 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; + } - /** - * 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.timeout() > 0) { - sendMessage({ type: "timeout", payload: { duration: this.timeout() || 1e9 } }); - this.timer = true; - } - } - clearTimeout() { - if (this.timer) { - sendMessage({ type: "timeout", payload: { duration: "reset" } }); - this.timer = 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.timeout() > 0) { + sendMessage({ type: "timeout", payload: { duration: this.timeout() || 1e9 } }); + this.timer = true; } - } 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; + } + clearTimeout() { + if (this.timer) { + sendMessage({ type: "timeout", payload: { duration: "reset" } }); + this.timer = false; } - } as T; - } + } + } 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; + /** + * 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.Hook` subclass to support parallel test execution in a worker. - */ - class Hook extends mixin(Mocha.Hook, Timeout) { + /** + * 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.Test` subclass to support parallel test execution in a worker. - */ - class Test extends mixin(Mocha.Test, Timeout, Clone) { - } + /** + * A `Mocha.Hook` subclass to support parallel test execution in a worker. + */ + class Hook extends mixin(Mocha.Hook, Timeout) { + } - /** - * 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; - } + /** + * A `Mocha.Test` subclass to support parallel test execution in a worker. + */ + class Test extends mixin(Mocha.Test, Timeout, Clone) { + } - 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; + /** + * 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; } - /** - * Run the tests in the requested task. - */ - function runTests(task: Harness.Parallel.Task, fn: (payload: Harness.Parallel.TaskResult) => void) { - if (task.runner === "unittest") { - return executeUnitTests(task, fn); - } - else { - return runFileTests(task, fn); + 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; } + } - function executeUnitTests(task: Harness.Parallel.UnitTestTask, fn: (payload: Harness.Parallel.TaskResult) => void) { - if (!unitTestSuiteMap && unitTestSuite.suites.length) { - unitTestSuiteMap = new ts.Map(); - for (const suite of unitTestSuite.suites) { - unitTestSuiteMap.set(suite.title, suite); - } - } - if (!unitTestTestMap && unitTestSuite.tests.length) { - unitTestTestMap = new ts.Map(); - for (const test of unitTestSuite.tests) { - unitTestTestMap.set(test.title, test); - } - } + /** + * Run the tests in the requested task. + */ + function runTests(task: Harness.Parallel.Task, fn: (payload: Harness.Parallel.TaskResult) => void) { + if (task.runner === "unittest") { + return executeUnitTests(task, fn); + } + else { + return runFileTests(task, fn); + } + } - if (!unitTestSuiteMap && !unitTestTestMap) { - throw new Error(`Asked to run unit test ${task.file}, but no unit tests were discovered!`); + function executeUnitTests(task: Harness.Parallel.UnitTestTask, fn: (payload: Harness.Parallel.TaskResult) => void) { + if (!unitTestSuiteMap && unitTestSuite.suites.length) { + unitTestSuiteMap = new ts.Map(); + for (const suite of unitTestSuite.suites) { + unitTestSuiteMap.set(suite.title, suite); } - - 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!`); + } + if (!unitTestTestMap && unitTestSuite.tests.length) { + unitTestTestMap = new ts.Map(); + for (const test of unitTestSuite.tests) { + unitTestTestMap.set(test.title, test); } + } - const root = new Suite("", new Mocha.Context()); - root.timeout(Harness.globalTimeout || 40000); - 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; - } + if (!unitTestSuiteMap && !unitTestTestMap) { + throw new Error(`Asked to run unit test ${task.file}, but no unit tests were discovered!`); + } - runSuite(task, suite!, payload => { - suite!.parent = unitTestSuite; - Object.setPrototypeOf(suite!.ctx, unitTestSuite.ctx); - fn(payload); - }); + 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 runFileTests(task: Harness.Parallel.RunnerTask, fn: (result: Harness.Parallel.TaskResult) => void) { - let instance = runners.get(task.runner); - if (!instance) - runners.set(task.runner, instance = Harness.createRunner(task.runner)); - instance.tests = [task.file]; + const root = new Suite("", new Mocha.Context()); + root.timeout(Harness.globalTimeout || 40000); + 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; + } - const suite = new Suite("", new Mocha.Context()); - suite.timeout(Harness.globalTimeout || 40000); + runSuite(task, suite!, payload => { + suite!.parent = unitTestSuite; + Object.setPrototypeOf(suite!.ctx, unitTestSuite.ctx); + fn(payload); + }); + } - shimTestInterface(suite, global); - instance.initializeTests(); + function runFileTests(task: Harness.Parallel.RunnerTask, fn: (result: Harness.Parallel.TaskResult) => void) { + let instance = runners.get(task.runner); + if (!instance) + runners.set(task.runner, instance = Harness.createRunner(task.runner)); + instance.tests = [task.file]; - runSuite(task, suite, fn); - } + const suite = new Suite("", new Mocha.Context()); + suite.timeout(Harness.globalTimeout || 40000); - function runSuite(task: Harness.Parallel.Task, suite: Mocha.Suite, fn: (result: Harness.Parallel.TaskResult) => void) { - const errors: Harness.Parallel.ErrorInfo[] = []; - const passes: Harness.Parallel.TestInfo[] = []; - const start = +new Date(); - const runner = new Mocha.Runner(suite, { delay: false }); - - runner - .on("start", () => { - unhookUncaughtExceptions(); // turn off global uncaught handling - }) - .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", () => { - hookUncaughtExceptions(); - runner.dispose(); - }) - .run(() => { - fn({ task, errors, passes, passing: passes.length, duration: +new Date() - start }); - }); - } + shimTestInterface(suite, global); + instance.initializeTests(); - /** - * Validates a message received from the host is well-formed. - */ - function validateHostMessage(message: Harness.Parallel.ParallelHostMessage) { - switch (message.type) { - case "test": return validateTest(message.payload); - case "batch": return validateBatch(message.payload); - case "close": return true; - default: return false; - } - } + runSuite(task, suite, fn); + } - /** - * Validates a test task is well formed. - */ - function validateTest(task: Harness.Parallel.Task) { - return !!task && !!task.runner && !!task.file; - } + function runSuite(task: Harness.Parallel.Task, suite: Mocha.Suite, fn: (result: Harness.Parallel.TaskResult) => void) { + const errors: Harness.Parallel.ErrorInfo[] = []; + const passes: Harness.Parallel.TestInfo[] = []; + const start = +new Date(); + const runner = new Mocha.Runner(suite, { delay: false }); + + runner + .on("start", () => { + unhookUncaughtExceptions(); // turn off global uncaught handling + }) + .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", () => { + hookUncaughtExceptions(); + runner.dispose(); + }) + .run(() => { + fn({ task, errors, passes, passing: passes.length, duration: +new Date() - start }); + }); + } - /** - * Validates a batch of test tasks are well formed. - */ - function validateBatch(tasks: Harness.Parallel.Task[]) { - return !!tasks && Array.isArray(tasks) && tasks.length > 0 && tasks.every(validateTest); + /** + * Validates a message received from the host is well-formed. + */ + function validateHostMessage(message: Harness.Parallel.ParallelHostMessage) { + switch (message.type) { + case "test": return validateTest(message.payload); + case "batch": return validateBatch(message.payload); + case "close": return true; + default: return false; } + } - function processHostMessage(message: Harness.Parallel.ParallelHostMessage) { - if (!validateHostMessage(message)) { - console.log("Invalid message:", message); - return; - } + /** + * Validates a test task is well formed. + */ + function validateTest(task: Harness.Parallel.Task) { + return !!task && !!task.runner && !!task.file; + } - switch (message.type) { - case "test": return processTest(message.payload, /*last*/ true); - case "batch": return processBatch(message.payload); - case "close": return process.exit(0); - } - } + /** + * Validates a batch of test tasks are well formed. + */ + function validateBatch(tasks: Harness.Parallel.Task[]) { + return !!tasks && Array.isArray(tasks) && tasks.length > 0 && tasks.every(validateTest); + } - function processTest(task: Harness.Parallel.Task, last: boolean, fn?: () => void) { - runTests(task, payload => { - sendMessage(last ? { type: "result", payload } : { type: "progress", payload }); - if (fn) - fn(); - }); + function processHostMessage(message: Harness.Parallel.ParallelHostMessage) { + if (!validateHostMessage(message)) { + console.log("Invalid message:", message); + return; } - function processBatch(tasks: Harness.Parallel.Task[], fn?: () => void) { - const next = () => { - const task = tasks.shift(); - if (task) - return processTest(task, tasks.length === 0, next); - if (fn) - fn(); - }; - next(); + switch (message.type) { + case "test": return processTest(message.payload, /*last*/ true); + case "batch": return processBatch(message.payload); + case "close": return process.exit(0); } + } - function handleUncaughtException(err: any) { - const error = err instanceof Error ? err : new Error("" + err); - sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } }); - } + function processTest(task: Harness.Parallel.Task, last: boolean, fn?: () => void) { + runTests(task, payload => { + sendMessage(last ? { type: "result", payload } : { type: "progress", payload }); + if (fn) + fn(); + }); + } - function sendMessage(message: Harness.Parallel.ParallelClientMessage) { - process.send!(message); - } + function processBatch(tasks: Harness.Parallel.Task[], fn?: () => void) { + const next = () => { + const task = tasks.shift(); + if (task) + return processTest(task, tasks.length === 0, next); + if (fn) + fn(); + }; + next(); + } - // A cache of test harness Runner instances. - const runners = new ts.Map(); + function handleUncaughtException(err: any) { + const error = err instanceof Error ? err : new Error("" + err); + sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } }); + } - // The root suite for all unit tests. - let unitTestSuite: Suite; - let unitTestSuiteMap: ts.ESMap; - // (Unit) Tests directly within the root suite - let unitTestTestMap: ts.ESMap; + function sendMessage(message: Harness.Parallel.ParallelClientMessage) { + process.send!(message); + } - if (Harness.runUnitTests) { - unitTestSuite = new Suite("", new Mocha.Context()); - unitTestSuite.timeout(Harness.globalTimeout || 40000); - shimTestInterface(unitTestSuite, global); - } - else { - // ensure unit tests do not get run - Harness.Parallel.shimNoopTestInterface(global); - } + // A cache of test harness Runner instances. + const runners = new ts.Map(); - process.on("message", processHostMessage); + // The root suite for all unit tests. + let unitTestSuite: Suite; + let unitTestSuiteMap: ts.ESMap; + // (Unit) Tests directly within the root suite + let unitTestTestMap: ts.ESMap; + + if (Harness.runUnitTests) { + unitTestSuite = new Suite("", new Mocha.Context()); + unitTestSuite.timeout(Harness.globalTimeout || 40000); + shimTestInterface(unitTestSuite, global); + } + else { + // ensure unit tests do not get run + Harness.Parallel.shimNoopTestInterface(global); } + + process.on("message", processHostMessage); +} } diff --git a/src/testRunner/playbackRef.ts b/src/testRunner/playbackRef.ts index 8fcb9965e0923..91e14ed52a582 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 +namespace Playback {} diff --git a/src/testRunner/projectsRunner.ts b/src/testRunner/projectsRunner.ts index 2893e0cb54c82..ecf412de49a3f 100644 --- a/src/testRunner/projectsRunner.ts +++ b/src/testRunner/projectsRunner.ts @@ -1,458 +1,458 @@ 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 - } +// 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 CompileProjectFilesResult { - configFileSourceFiles: readonly ts.SourceFile[]; - moduleKind: ts.ModuleKind; - program?: ts.Program; - compilerOptions?: ts.CompilerOptions; - errors: readonly ts.Diagnostic[]; - sourceMapData?: readonly ts.SourceMapEmitResult[]; - } +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 BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { - outputFiles?: readonly documents.TextDocument[]; - } +interface CompileProjectFilesResult { + configFileSourceFiles: readonly ts.SourceFile[]; + moduleKind: ts.ModuleKind; + program?: ts.Program; + compilerOptions?: ts.CompilerOptions; + errors: readonly ts.Diagnostic[]; + sourceMapData?: readonly ts.SourceMapEmitResult[]; +} - export class ProjectRunner extends Harness.RunnerBase { - public enumerateTestFiles() { - const all = this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true }); - if (Harness.shards === 1) { - return all; - } - return all.filter((_val, idx) => idx % Harness.shards === (Harness.shardId - 1)); - } +interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { + outputFiles?: readonly documents.TextDocument[]; +} - public kind(): Harness.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; } + return all.filter((_val, idx) => idx % Harness.shards === (Harness.shardId - 1)); + } - 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); - } - }); - } + public kind(): Harness.TestRunnerKind { + return "project"; + } - 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; - }); - }); + 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); } - } + }); } - 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; + 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; + }); + }); } + } +} - public get parseConfigHost(): fakes.ParseConfigHost { - return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase)); - } +class ProjectCompilerHost extends fakes.CompilerHost { + private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; + private _projectParseConfigHost: ProjectParseConfigHost | undefined; - public getDefaultLibFileName(_options: ts.CompilerOptions) { - return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts"); - } + 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 ProjectParseConfigHost extends fakes.ParseConfigHost { - private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; - - constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) { - super(sys); - this._testCase = testCase; - } + public get parseConfigHost(): fakes.ParseConfigHost { + return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._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 = vpath.resolve(vfs.srcFolder, this._testCase.projectRoot); - return result.map(item => vpath.relative(projectRoot, vpath.resolve(projectRoot, item), this.vfs.ignoreCase)); - } + public getDefaultLibFileName(_options: ts.CompilerOptions) { + return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts"); } +} - interface ProjectTestConfiguration { - name: string; - payload: ProjectTestPayload; +class ProjectParseConfigHost extends fakes.ParseConfigHost { + private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; + + constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) { + super(sys); + this._testCase = testCase; } - interface ProjectTestPayload { - testCase: ProjectRunnerTestCase & ts.CompilerOptions; - moduleKind: ts.ModuleKind; - vfs: vfs.FileSystem; + 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)); } +} - 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)); - } +interface ProjectTestConfiguration { + name: string; + payload: ProjectTestPayload; +} - 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]; - } +interface ProjectTestPayload { + testCase: ProjectRunnerTestCase & ts.CompilerOptions; + moduleKind: ts.ModuleKind; + vfs: vfs.FileSystem; +} - 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, - }; +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"); } - - private get vfs() { - return this.sys.vfs; + else if (!inputFiles || inputFiles.length === 0) { + configFileName = ts.findConfigFile("", path => this.sys.fileExists(path)); } - public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] { - let testCase: ProjectRunnerTestCase & ts.CompilerOptions; + 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]; + } - let testFileText: string | undefined; - try { - testFileText = Harness.IO.readFile(testCaseFileName); - } - catch (e) { - assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message); - } + 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, + }; + } - try { - testCase = JSON.parse(testFileText!) as ProjectRunnerTestCase & ts.CompilerOptions; - } - catch (e) { - throw assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); - } + private get vfs() { + return this.sys.vfs; + } - 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(); + public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] { + let testCase: ProjectRunnerTestCase & ts.CompilerOptions; - return [ - { name: `@module: commonjs`, payload: { testCase, moduleKind: ts.ModuleKind.CommonJS, vfs: fs } }, - { name: `@module: amd`, payload: { testCase, moduleKind: ts.ModuleKind.AMD, vfs: fs } } - ]; + let testFileText: string | undefined; + try { + testFileText = Harness.IO.readFile(testCaseFileName); + } + catch (e) { + assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message); } - 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); + try { + testCase = JSON.parse(testFileText!) as ProjectRunnerTestCase & ts.CompilerOptions; + } + catch (e) { + throw assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); } - public verifyDiagnostics() { - if (this.compilerResult.errors.length) { - Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".errors.txt", getErrorsBaseline(this.compilerResult)); - } + 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)); } + } - 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); + 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++; } - } - if (errs.length) { - throw Error(errs.join("\n ")); + 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); } } - } - 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))); - // }); - // } + if (errs.length) { + throw Error(errs.join("\n ")); + } } + } - 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)); - } + 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)); } } + } - // Project baselines verified go in project/testCaseName/moduleKind/ - private getBaselineFolder(moduleKind: ts.ModuleKind) { - return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; - } + // Project baselines verified go in project/testCaseName/moduleKind/ + private getBaselineFolder(moduleKind: ts.ModuleKind) { + return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; + } - 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; - } + 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; } } - - return url; } - private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: readonly ts.SourceFile[], getInputFiles: () => readonly string[], compilerHost: ts.CompilerHost, compilerOptions: ts.CompilerOptions): CompileProjectFilesResult { + return url; + } - const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost); - const errors = ts.getPreEmitDiagnostics(program); + private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: readonly ts.SourceFile[], getInputFiles: () => readonly string[], compilerHost: ts.CompilerHost, compilerOptions: ts.CompilerOptions): CompileProjectFilesResult { - const { sourceMaps: sourceMapData, diagnostics: emitDiagnostics } = program.emit(); + const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost); + const errors = ts.getPreEmitDiagnostics(program); - // 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) - }; - } + 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) + }; } + } + + return { + configFileSourceFiles, + moduleKind, + program, + errors: ts.concatenate(errors, emitDiagnostics), + sourceMapData + }; + } - return { - configFileSourceFiles, - moduleKind, - program, - errors: ts.concatenate(errors, emitDiagnostics), - sourceMapData - }; + private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) { + if (!compilerResult.program) { + return; } - 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); } - - 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 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); } - 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 + ts.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 = 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); } - }); + } + }); - const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { - documents: allInputFiles, - cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot) - }); + 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!); + // 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); - } + function findOutputDtsFile(fileName: string) { + return ts.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 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 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); } } - - 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 - }; + 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); +} - // 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] as any; - if (!ts.isString(optType)) { - const key = value.toLowerCase(); - const optTypeValue = optType.get(key); - if (optTypeValue) { - value = optTypeValue; - } +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] as any; + if (!ts.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 d2a9888b09b28..ed14dfde4c65d 100644 --- a/src/testRunner/runner.ts +++ b/src/testRunner/runner.ts @@ -1,286 +1,286 @@ namespace Harness { - /* eslint-disable prefer-const */ - export let runners: Harness.RunnerBase[] = []; - export let iterations = 1; - /* eslint-enable prefer-const */ +/* eslint-disable prefer-const */ +export let runners: Harness.RunnerBase[] = []; +export let iterations = 1; +/* eslint-enable prefer-const */ - function runTests(runners: Harness.RunnerBase[]) { - for (let i = iterations; i > 0; i--) { - const seen = new Map(); - const dupes: [ - string, - string - ][] = []; - for (const runner of runners) { - if (runner instanceof Harness.CompilerBaselineRunner || runner instanceof Harness.FourSlashRunner) { - for (const sf of runner.enumerateTestFiles()) { - const full = typeof sf === "string" ? sf : sf.file; - const base = vpath.basename(full).toLowerCase(); - // allow existing dupes in fourslash/shims and fourslash/server - if (seen.has(base) && !/fourslash\/(shim|server)/.test(full)) { - dupes.push([seen.get(base)!, full]); - } - else { - seen.set(base, full); - } +function runTests(runners: Harness.RunnerBase[]) { + for (let i = iterations; i > 0; i--) { + const seen = new Map(); + const dupes: [ + string, + string + ][] = []; + for (const runner of runners) { + if (runner instanceof Harness.CompilerBaselineRunner || runner instanceof Harness.FourSlashRunner) { + for (const sf of runner.enumerateTestFiles()) { + const full = typeof sf === "string" ? sf : sf.file; + const base = vpath.basename(full).toLowerCase(); + // allow existing dupes in fourslash/shims and fourslash/server + if (seen.has(base) && !/fourslash\/(shim|server)/.test(full)) { + dupes.push([seen.get(base)!, full]); + } + else { + seen.set(base, full); } } - runner.initializeTests(); } - if (dupes.length) { - throw new Error(`${dupes.length} Tests with duplicate baseline names: + runner.initializeTests(); + } + if (dupes.length) { + throw new Error(`${dupes.length} Tests with duplicate baseline names: ${JSON.stringify(dupes, undefined, 2)}`); - } } } +} - 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 = 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, ""); +} - export function createRunner(kind: Harness.TestRunnerKind): Harness.RunnerBase { - switch (kind) { - case "conformance": - return new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance); - case "compiler": - return new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions); - case "fourslash": - return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native); - case "fourslash-shims": - return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims); - case "fourslash-shims-pp": - return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess); - case "fourslash-server": - return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server); - case "project": - return new project.ProjectRunner(); - case "rwc": - return new RWC.RWCRunner(); - case "test262": - return new Harness.Test262BaselineRunner(); - case "user": - return new Harness.UserCodeRunner(); - case "dt": - return new Harness.DefinitelyTypedRunner(); - case "docker": - return new Harness.DockerfileRunner(); - } - return ts.Debug.fail(`Unknown runner kind ${kind}`); +export function createRunner(kind: Harness.TestRunnerKind): Harness.RunnerBase { + switch (kind) { + case "conformance": + return new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance); + case "compiler": + return new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions); + case "fourslash": + return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native); + case "fourslash-shims": + return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims); + case "fourslash-shims-pp": + return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess); + case "fourslash-server": + return new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server); + case "project": + return new project.ProjectRunner(); + case "rwc": + return new RWC.RWCRunner(); + case "test262": + return new Harness.Test262BaselineRunner(); + case "user": + return new Harness.UserCodeRunner(); + case "dt": + return new Harness.DefinitelyTypedRunner(); + case "docker": + return new Harness.DockerfileRunner(); } + 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 +// 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 mytestconfigFileName = "mytest.config"; +const testconfigFileName = "test.config"; - const customConfig = tryGetConfig(Harness.IO.args()); - const testConfigContent = customConfig && Harness.IO.fileExists(customConfig) - ? Harness.IO.readFile(customConfig)! - : Harness.IO.fileExists(mytestconfigFileName) - ? Harness.IO.readFile(mytestconfigFileName)! - : Harness.IO.fileExists(testconfigFileName) ? Harness.IO.readFile(testconfigFileName)! : ""; +const customConfig = tryGetConfig(Harness.IO.args()); +const testConfigContent = customConfig && Harness.IO.fileExists(customConfig) + ? Harness.IO.readFile(customConfig)! + : Harness.IO.fileExists(mytestconfigFileName) + ? Harness.IO.readFile(mytestconfigFileName)! + : Harness.IO.fileExists(testconfigFileName) ? Harness.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 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 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: Harness.TestRunnerKind; - files: string[]; - } +export interface TaskSet { + runner: Harness.TestRunnerKind; + files: string[]; +} - export let configOption: string; - export let globalTimeout: number; - function handleTestConfig() { - if (testConfigContent !== "") { - const testConfig = JSON.parse(testConfigContent) as TestConfig; - if (testConfig.light) { - Harness.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) { - Harness.setShardId(testConfig.shardId); - } - if (testConfig.shards) { - Harness.setShards(testConfig.shards); - } +export let configOption: string; +export let globalTimeout: number; +function handleTestConfig() { + if (testConfigContent !== "") { + const testConfig = JSON.parse(testConfigContent) as TestConfig; + if (testConfig.light) { + Harness.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) { + Harness.setShardId(testConfig.shardId); + } + if (testConfig.shards) { + Harness.setShards(testConfig.shards); + } - if (testConfig.stackTraceLimit === "full") { - (Error as any).stackTraceLimit = Infinity; - stackTraceLimit = testConfig.stackTraceLimit; - } - else if ((+testConfig.stackTraceLimit! | 0) > 0) { - (Error as any).stackTraceLimit = +testConfig.stackTraceLimit! | 0; - stackTraceLimit = +testConfig.stackTraceLimit! | 0; - } - if (testConfig.listenForWork) { - return true; - } + if (testConfig.stackTraceLimit === "full") { + (Error as any).stackTraceLimit = Infinity; + stackTraceLimit = testConfig.stackTraceLimit; + } + else if ((+testConfig.stackTraceLimit! | 0) > 0) { + (Error as any).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; - } + if (!configOption) { + configOption = option; + } + else { + configOption += "+" + option; + } - switch (option) { - case "compiler": - runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); - runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions)); - break; - case "conformance": - runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); - break; - case "project": - runners.push(new project.ProjectRunner()); - break; - case "fourslash": - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native)); - break; - case "fourslash-shims": - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims)); - break; - case "fourslash-shims-pp": - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); - break; - case "fourslash-server": - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server)); - break; - case "fourslash-generated": - runners.push(new Harness.GeneratedFourslashRunner(FourSlash.FourSlashTestType.Native)); - break; - case "rwc": - runners.push(new RWC.RWCRunner()); - break; - case "test262": - runners.push(new Harness.Test262BaselineRunner()); - break; - case "user": - runners.push(new Harness.UserCodeRunner()); - break; - case "dt": - runners.push(new Harness.DefinitelyTypedRunner()); - break; - case "docker": - runners.push(new Harness.DockerfileRunner()); - break; - } + switch (option) { + case "compiler": + runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); + runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions)); + break; + case "conformance": + runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); + break; + case "project": + runners.push(new project.ProjectRunner()); + break; + case "fourslash": + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native)); + break; + case "fourslash-shims": + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims)); + break; + case "fourslash-shims-pp": + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); + break; + case "fourslash-server": + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server)); + break; + case "fourslash-generated": + runners.push(new Harness.GeneratedFourslashRunner(FourSlash.FourSlashTestType.Native)); + break; + case "rwc": + runners.push(new RWC.RWCRunner()); + break; + case "test262": + runners.push(new Harness.Test262BaselineRunner()); + break; + case "user": + runners.push(new Harness.UserCodeRunner()); + break; + case "dt": + runners.push(new Harness.DefinitelyTypedRunner()); + break; + case "docker": + runners.push(new Harness.DockerfileRunner()); + break; } } } + } - if (runners.length === 0) { - // compiler - runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); - runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions)); + if (runners.length === 0) { + // compiler + runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Conformance)); + runners.push(new Harness.CompilerBaselineRunner(Harness.CompilerTestType.Regressions)); - runners.push(new project.ProjectRunner()); + runners.push(new project.ProjectRunner()); - // language services - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native)); - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims)); - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); - runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server)); - // runners.push(new GeneratedFourslashRunner()); + // language services + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Native)); + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Shims)); + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); + runners.push(new Harness.FourSlashRunner(FourSlash.FourSlashTestType.Server)); + // runners.push(new GeneratedFourslashRunner()); - // CRON-only tests - if (process.env.TRAVIS_EVENT_TYPE === "cron") { - runners.push(new Harness.UserCodeRunner()); - runners.push(new Harness.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 + // CRON-only tests + if (process.env.TRAVIS_EVENT_TYPE === "cron") { + runners.push(new Harness.UserCodeRunner()); + runners.push(new Harness.DockerfileRunner()); } - return false; } + 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() { - ts.Debug.loggingHost = { - log(_level, s) { - console.log(s || ""); - } - }; - - if (ts.Debug.isDebugging) { - ts.Debug.enableDebugInfo(); +function beginTests() { + ts.Debug.loggingHost = { + log(_level, s) { + console.log(s || ""); } + }; + + 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)); + // 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); + runTests(runners); - if (!runUnitTests) { - // patch `describe` to skip unit tests - (global as any).describe = ts.noop; - } + if (!runUnitTests) { + // patch `describe` to skip unit tests + (global as any).describe = ts.noop; } +} - export let isWorker: boolean; - function startTestEnvironment() { - isWorker = handleTestConfig(); - if (isWorker) { - return Harness.Parallel.Worker.start(); - } - else if (taskConfigsFolder && workerCount && workerCount > 1) { - return Harness.Parallel.Host.start(); - } - beginTests(); +export let isWorker: boolean; +function startTestEnvironment() { + isWorker = handleTestConfig(); + if (isWorker) { + return Harness.Parallel.Worker.start(); + } + else if (taskConfigsFolder && workerCount && workerCount > 1) { + return Harness.Parallel.Host.start(); } + beginTests(); +} - startTestEnvironment(); +startTestEnvironment(); } diff --git a/src/testRunner/rwcRunner.ts b/src/testRunner/rwcRunner.ts index bebde6c03bad3..0b565affa5467 100644 --- a/src/testRunner/rwcRunner.ts +++ b/src/testRunner/rwcRunner.ts @@ -1,230 +1,230 @@ // 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; +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); + const wrappedIO = Playback.wrapIO(oldIO); + wrappedIO.startReplayFromData(ioLog); + Harness.setHarnessIO(wrappedIO); - try { - fn(oldIO); - } - finally { - wrappedIO.endReplay(); - Harness.setHarnessIO(oldIO); - } + try { + fn(oldIO); } + finally { + wrappedIO.endReplay(); + Harness.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: 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; + }); - it("can compile", function (this: Mocha.Context) { - this.timeout(800000); // 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); - } + it("can compile", function (this: Mocha.Context) { + this.timeout(800000); // Allow long timeouts for RWC compilations + let opts!: ts.ParsedCommandLine; - // Deduplicate files so they are only printed once in baselines (they are deduplicated within the compiler already) - const uniqueNames = new ts.Map(); - 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)); - } - } + 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); - // 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)); - } - } - }); + // 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); + } - if (useCustomLibraryFile) { - // do not use lib since we already read it in above - opts.options.lib = undefined; - opts.options.noLib = true; + // Deduplicate files so they are only printed once in baselines (they are deduplicated within the compiler already) + const uniqueNames = new ts.Map(); + 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)); + } } - 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)!; + // 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)); } - catch (e) { - content = Harness.IO.readFile(fileName)!; + 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)); } - return { unitName, content }; } }); + if (useCustomLibraryFile) { + // do not use lib since we already read it in above + opts.options.lib = undefined; + opts.options.noLib = true; + } - it("has the expected emitted code", function (this: Mocha.Context) { - this.timeout(100000); // Allow longer timeouts for RWC js verification - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - return Harness.Compiler.iterateOutputs(compilerResult.js.values()); - }, baselineOpts, [".js", ".jsx"]); - }); + 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)!; + } + catch (e) { + content = Harness.IO.readFile(fileName)!; + } + return { unitName, content }; + } + }); - it("has the expected declaration file content", () => { - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - if (!compilerResult.dts.size) { - return null; // eslint-disable-line no-null/no-null - } - return Harness.Compiler.iterateOutputs(compilerResult.dts.values()); - }, baselineOpts, [".d.ts"]); - }); + it("has the expected emitted code", function (this: Mocha.Context) { + this.timeout(100000); // 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 source maps", () => { - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - if (!compilerResult.maps.size) { - return null; // eslint-disable-line no-null/no-null - } + it("has the expected declaration file content", () => { + Harness.Baseline.runMultifileBaseline(baseName, "", () => { + if (!compilerResult.dts.size) { + return null; // eslint-disable-line no-null/no-null + } - return Harness.Compiler.iterateOutputs(compilerResult.maps.values()); - }, baselineOpts, [".map"]); - }); + 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 + } - it("has the expected errors", () => { - Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => { + return Harness.Compiler.iterateOutputs(compilerResult.maps.values()); + }, baselineOpts, [".map"]); + }); + + it("has the expected errors", () => { + Harness.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 => !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 }); + }, 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 } - // 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 }); - }, 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); - } - }); + 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 Harness.RunnerBase { - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return Harness.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"; - } + 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); - } + /** 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); - } + private runTest(jsonFileName: string) { + runRWCTest(jsonFileName); } } +} diff --git a/src/testRunner/test262Runner.ts b/src/testRunner/test262Runner.ts index 9e3da396df9eb..5a246bcc48427 100644 --- a/src/testRunner/test262Runner.ts +++ b/src/testRunner/test262Runner.ts @@ -1,107 +1,107 @@ namespace Harness { - // In harness baselines, null is different than undefined. See `generateActual` in `harness.ts`. - export class Test262BaselineRunner extends Harness.RunnerBase { - private static readonly basePath = "internal/cases/test262"; - private static readonly helpersFilePath = "tests/cases/test262-harness/helpers.d.ts"; - private static readonly helperFile: Harness.Compiler.TestFile = { - unitName: Test262BaselineRunner.helpersFilePath, - content: Harness.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: Harness.Baseline.BaselineOptions = { - Subfolder: "test262", - Baselinefolder: "internal/baselines" - }; +// In harness baselines, null is different than undefined. See `generateActual` in `harness.ts`. +export class Test262BaselineRunner extends Harness.RunnerBase { + private static readonly basePath = "internal/cases/test262"; + private static readonly helpersFilePath = "tests/cases/test262-harness/helpers.d.ts"; + private static readonly helperFile: Harness.Compiler.TestFile = { + unitName: Test262BaselineRunner.helpersFilePath, + content: Harness.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: Harness.Baseline.BaselineOptions = { + Subfolder: "test262", + Baselinefolder: "internal/baselines" + }; - private static getTestFilePath(filename: string): string { - return Test262BaselineRunner.basePath + "/" + filename; - } + 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: Harness.Compiler.TestFile[]; - }; + 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: Harness.Compiler.TestFile[]; + }; - before(() => { - const content = Harness.IO.readFile(filePath)!; - const testFilename = ts.removeFileExtension(filePath).replace(/\//g, "_") + ".test"; - const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, testFilename); - const inputFiles: Harness.Compiler.TestFile[] = testCaseContent.testUnitData.map(unit => { - const unitName = Test262BaselineRunner.getTestFilePath(unit.name); - return { unitName, content: unit.content }; - }); + before(() => { + const content = Harness.IO.readFile(filePath)!; + const testFilename = ts.removeFileExtension(filePath).replace(/\//g, "_") + ".test"; + const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, testFilename); + const inputFiles: Harness.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 - }; + // Emit the results + testState = { + filename: testFilename, + inputFiles, + compilerResult: undefined!, // TODO: GH#18217 + }; - testState.compilerResult = Harness.Compiler.compileFiles([Test262BaselineRunner.helperFile].concat(inputFiles), - /*otherFiles*/ [], - /* harnessOptions */ undefined, Test262BaselineRunner.options, - /* currentDirectory */ undefined); - }); + testState.compilerResult = Harness.Compiler.compileFiles([Test262BaselineRunner.helperFile].concat(inputFiles), + /*otherFiles*/ [], + /* harnessOptions */ undefined, Test262BaselineRunner.options, + /* currentDirectory */ undefined); + }); - after(() => { - testState = undefined!; - }); + after(() => { + testState = undefined!; + }); - it("has the expected emitted code", () => { - const files = Array.from(testState.compilerResult.js.values()).filter(f => f.file !== Test262BaselineRunner.helpersFilePath); - Harness.Baseline.runBaseline(testState.filename + ".output.js", Harness.Compiler.collateOutputs(files), Test262BaselineRunner.baselineOptions); - }); + it("has the expected emitted code", () => { + const files = Array.from(testState.compilerResult.js.values()).filter(f => f.file !== Test262BaselineRunner.helpersFilePath); + Harness.Baseline.runBaseline(testState.filename + ".output.js", Harness.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 : Harness.Compiler.getErrorBaseline(testState.inputFiles, errors); - Harness.Baseline.runBaseline(testState.filename + ".errors.txt", baseline, 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 : Harness.Compiler.getErrorBaseline(testState.inputFiles, errors); + Harness.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("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))!; - Harness.Baseline.runBaseline(testState.filename + ".AST.txt", Utils.sourceFileToJSON(sourceFile), Test262BaselineRunner.baselineOptions); - }); + it("has the expected AST", () => { + const sourceFile = testState.compilerResult.program!.getSourceFile(Test262BaselineRunner.getTestFilePath(testState.filename))!; + Harness.Baseline.runBaseline(testState.filename + ".AST.txt", Utils.sourceFileToJSON(sourceFile), Test262BaselineRunner.baselineOptions); }); - } + }); + } - public kind(): Harness.TestRunnerKind { - return "test262"; - } + public kind(): Harness.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 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)); - } + 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)); } } -} \ No newline at end of file +} +} diff --git a/src/testRunner/unittests/asserts.ts b/src/testRunner/unittests/asserts.ts index 3efb51485465e..f84762d89bbc5 100644 --- a/src/testRunner/unittests/asserts.ts +++ b/src/testRunner/unittests/asserts.ts @@ -1,12 +1,12 @@ namespace ts { - describe("unittests:: assert", () => { - it("deepEqual", () => { - assert.throws(() => assert.deepEqual(ts.factory.createNodeArray([ts.factory.createIdentifier("A")]), ts.factory.createNodeArray([ts.factory.createIdentifier("B")]))); - assert.throws(() => assert.deepEqual(ts.factory.createNodeArray([], /*hasTrailingComma*/ true), ts.factory.createNodeArray([], /*hasTrailingComma*/ false))); - assert.deepEqual(ts.factory.createNodeArray([ts.factory.createIdentifier("A")], /*hasTrailingComma*/ true), ts.factory.createNodeArray([ts.factory.createIdentifier("A")], /*hasTrailingComma*/ true)); - }); - it("assertNever on string has correct error", () => { - assert.throws(() => ts.Debug.assertNever("hi" as never), "Debug Failure. Illegal value: \"hi\""); - }); +describe("unittests:: assert", () => { + it("deepEqual", () => { + assert.throws(() => assert.deepEqual(ts.factory.createNodeArray([ts.factory.createIdentifier("A")]), ts.factory.createNodeArray([ts.factory.createIdentifier("B")]))); + assert.throws(() => assert.deepEqual(ts.factory.createNodeArray([], /*hasTrailingComma*/ true), ts.factory.createNodeArray([], /*hasTrailingComma*/ false))); + assert.deepEqual(ts.factory.createNodeArray([ts.factory.createIdentifier("A")], /*hasTrailingComma*/ true), ts.factory.createNodeArray([ts.factory.createIdentifier("A")], /*hasTrailingComma*/ true)); }); + it("assertNever on string has correct error", () => { + assert.throws(() => ts.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 19ce31cc98912..c2563f4e511b2 100644 --- a/src/testRunner/unittests/base64.ts +++ b/src/testRunner/unittests/base64.ts @@ -1,22 +1,22 @@ 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(ts.base64decode({}, ts.convertToBase64(test)), test); - } - }); +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(ts.base64decode({}, ts.convertToBase64(test)), test); + } }); }); +}); } diff --git a/src/testRunner/unittests/builder.ts b/src/testRunner/unittests/builder.ts index db5f0bba4168c..1165c83d37535 100644 --- a/src/testRunner/unittests/builder.ts +++ b/src/testRunner/unittests/builder.ts @@ -1,130 +1,130 @@ namespace ts { - describe("unittests:: builder", () => { - it("emits dependent files", () => { - const files: ts.NamedSourceText[] = [ - { name: "/a.ts", text: ts.SourceText.New("", 'import { b } from "./b";', "") }, - { name: "/b.ts", text: ts.SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, - { name: "/c.ts", text: ts.SourceText.New("", "", "export const c = 0;") }, - ]; - - let program = ts.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: ts.NamedSourceText[] = [ - { name: "/a.ts", text: ts.SourceText.New("", "", "namespace A { export const x = 0; }") }, - { name: "/b.ts", text: ts.SourceText.New("", "", "namespace B { export const x = 0; }") }, - ]; - - let program = ts.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: ts.NamedSourceText[] = [ - { name: "/a.ts", text: ts.SourceText.New("", 'import { b } from "./b";', "") }, - { name: "/b.ts", text: ts.SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, - { name: "/c.ts", text: ts.SourceText.New("", "", "export const c = 0;") }, - { name: "/d.ts", text: ts.SourceText.New("", "", "export const dd = 0;") }, - { name: "/e.ts", text: ts.SourceText.New("", "", "export const ee = 0;") }, - ]; - - let program = ts.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"]); - }); +describe("unittests:: builder", () => { + it("emits dependent files", () => { + const files: ts.NamedSourceText[] = [ + { name: "/a.ts", text: ts.SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: ts.SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: ts.SourceText.New("", "", "export const c = 0;") }, + ]; + + let program = ts.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: ts.NamedSourceText[] = [ + { name: "/a.ts", text: ts.SourceText.New("", "", "namespace A { export const x = 0; }") }, + { name: "/b.ts", text: ts.SourceText.New("", "", "namespace B { export const x = 0; }") }, + ]; + + let program = ts.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"]); }); - function makeAssertChanges(getProgram: () => ts.Program): (fileNames: readonly string[]) => void { - const host: ts.BuilderProgramHost = { useCaseSensitiveFileNames: ts.returnTrue }; - let builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined; - return fileNames => { - const program = getProgram(); - builderProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); - const outputFileNames: string[] = []; - // eslint-disable-next-line no-empty - while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) { + it("keeps the file in affected files if cancellation token throws during the operation", () => { + const files: ts.NamedSourceText[] = [ + { name: "/a.ts", text: ts.SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: ts.SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: ts.SourceText.New("", "", "export const c = 0;") }, + { name: "/d.ts", text: ts.SourceText.New("", "", "export const dd = 0;") }, + { name: "/e.ts", text: ts.SourceText.New("", "", "export const ee = 0;") }, + ]; + + let program = ts.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: () => ts.Program): (fileNames: readonly string[]) => void { + const host: ts.BuilderProgramHost = { useCaseSensitiveFileNames: ts.returnTrue }; + let builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined; + return fileNames => { + const program = getProgram(); + builderProgram = ts.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: () => ts.Program): (fileNames: readonly string[], cancelAfterEmitLength?: number) => void { + const host: ts.BuilderProgramHost = { useCaseSensitiveFileNames: ts.returnTrue }; + let builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined; + let cancel = false; + const cancellationToken: ts.CancellationToken = { + isCancellationRequested: () => cancel, + throwIfCancellationRequested: () => { + if (cancel) { + throw new ts.OperationCanceledException(); } - assert.deepEqual(outputFileNames, fileNames); - }; - } - - function makeAssertChangesWithCancellationToken(getProgram: () => ts.Program): (fileNames: readonly string[], cancelAfterEmitLength?: number) => void { - const host: ts.BuilderProgramHost = { useCaseSensitiveFileNames: ts.returnTrue }; - let builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined; - let cancel = false; - const cancellationToken: ts.CancellationToken = { - isCancellationRequested: () => cancel, - throwIfCancellationRequested: () => { - if (cancel) { - throw new ts.OperationCanceledException(); + }, + }; + return (fileNames, cancelAfterEmitLength?: number) => { + cancel = false; + let operationWasCancelled = false; + const program = getProgram(); + builderProgram = ts.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 = ts.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 ts.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: ts.ProgramWithSourceTexts, fileName: string, fileContent: string): ts.ProgramWithSourceTexts { - return ts.updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { - ts.updateProgramText(files, fileName, fileContent); - }); - } + } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken)); + } + catch (e) { + assert.isFalse(operationWasCancelled); + assert(e instanceof ts.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: ts.ProgramWithSourceTexts, fileName: string, fileContent: string): ts.ProgramWithSourceTexts { + return ts.updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { + ts.updateProgramText(files, fileName, fileContent); + }); +} } diff --git a/src/testRunner/unittests/comments.ts b/src/testRunner/unittests/comments.ts index 783845487ff06..8246ff05f9c44 100644 --- a/src/testRunner/unittests/comments.ts +++ b/src/testRunner/unittests/comments.ts @@ -1,32 +1,32 @@ namespace ts { - describe("comment parsing", () => { - const withShebang = `#! node +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 = ts.getLeadingCommentRanges(withShebang, 0); - assert.isDefined(result); - assert.strictEqual(result!.length, 2); - }); + it("skips shebang", () => { + const result = ts.getLeadingCommentRanges(withShebang, 0); + assert.isDefined(result); + assert.strictEqual(result!.length, 2); + }); - it("treats all comments at start of file as leading comments", () => { - const result = ts.getLeadingCommentRanges(noShebang, 0); - assert.isDefined(result); - assert.strictEqual(result!.length, 2); - }); + it("treats all comments at start of file as leading comments", () => { + const result = ts.getLeadingCommentRanges(noShebang, 0); + assert.isDefined(result); + assert.strictEqual(result!.length, 2); + }); - it("returns leading comments if position is not 0", () => { - const result = ts.getLeadingCommentRanges(withTrailing, 1); - assert.isDefined(result); - assert.strictEqual(result!.length, 1); - assert.strictEqual(result![0].kind, ts.SyntaxKind.SingleLineCommentTrivia); - }); + it("returns leading comments if position is not 0", () => { + const result = ts.getLeadingCommentRanges(withTrailing, 1); + assert.isDefined(result); + assert.strictEqual(result!.length, 1); + assert.strictEqual(result![0].kind, ts.SyntaxKind.SingleLineCommentTrivia); }); +}); } diff --git a/src/testRunner/unittests/compilerCore.ts b/src/testRunner/unittests/compilerCore.ts index 8c3fe30258a71..99995322c49bf 100644 --- a/src/testRunner/unittests/compilerCore.ts +++ b/src/testRunner/unittests/compilerCore.ts @@ -1,195 +1,195 @@ namespace ts { - describe("unittests:: compilerCore", () => { - describe("equalOwnProperties", () => { - it("correctly equates objects", () => { - assert.isTrue(ts.equalOwnProperties({}, {})); - assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: 1 })); - assert.isTrue(ts.equalOwnProperties({ a: 1, b: 2 }, { b: 2, a: 1 })); - }); - it("correctly identifies unmatched objects", () => { - assert.isFalse(ts.equalOwnProperties({}, { a: 1 }), "missing left property"); - assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}), "missing right property"); - assert.isFalse(ts.equalOwnProperties({ a: 1 }, { a: 2 }), "differing property"); - }); - it("correctly identifies undefined vs hasOwnProperty", () => { - assert.isFalse(ts.equalOwnProperties({}, { a: undefined }), "missing left property"); - assert.isFalse(ts.equalOwnProperties({ a: undefined }, {}), "missing right property"); - }); - it("truthiness", () => { - const trythyTest = (l: any, r: any) => !!l === !!r; - assert.isFalse(ts.equalOwnProperties({}, { a: 1 }, trythyTest), "missing left truthy property"); - assert.isFalse(ts.equalOwnProperties({}, { a: 0 }, trythyTest), "missing left falsey property"); - assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}, trythyTest), "missing right truthy property"); - assert.isFalse(ts.equalOwnProperties({ a: 0 }, {}, trythyTest), "missing right falsey property"); - assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: "foo" }, trythyTest), "valid equality"); - }); - it("all equal", () => { - assert.isFalse(ts.equalOwnProperties({}, { a: 1 }, () => true), "missing left property"); - assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}, () => true), "missing right property"); - assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality"); - }); +describe("unittests:: compilerCore", () => { + describe("equalOwnProperties", () => { + it("correctly equates objects", () => { + assert.isTrue(ts.equalOwnProperties({}, {})); + assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: 1 })); + assert.isTrue(ts.equalOwnProperties({ a: 1, b: 2 }, { b: 2, a: 1 })); }); - describe("customSet", () => { - it("mutation", () => { - const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); - assert.equal(set.size, 0); + it("correctly identifies unmatched objects", () => { + assert.isFalse(ts.equalOwnProperties({}, { a: 1 }), "missing left property"); + assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}), "missing right property"); + assert.isFalse(ts.equalOwnProperties({ a: 1 }, { a: 2 }), "differing property"); + }); + it("correctly identifies undefined vs hasOwnProperty", () => { + assert.isFalse(ts.equalOwnProperties({}, { a: undefined }), "missing left property"); + assert.isFalse(ts.equalOwnProperties({ a: undefined }, {}), "missing right property"); + }); + it("truthiness", () => { + const trythyTest = (l: any, r: any) => !!l === !!r; + assert.isFalse(ts.equalOwnProperties({}, { a: 1 }, trythyTest), "missing left truthy property"); + assert.isFalse(ts.equalOwnProperties({}, { a: 0 }, trythyTest), "missing left falsey property"); + assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}, trythyTest), "missing right truthy property"); + assert.isFalse(ts.equalOwnProperties({ a: 0 }, {}, trythyTest), "missing right falsey property"); + assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: "foo" }, trythyTest), "valid equality"); + }); + it("all equal", () => { + assert.isFalse(ts.equalOwnProperties({}, { a: 1 }, () => true), "missing left property"); + assert.isFalse(ts.equalOwnProperties({ a: 1 }, {}, () => true), "missing right property"); + assert.isTrue(ts.equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality"); + }); + }); + describe("customSet", () => { + it("mutation", () => { + const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); + assert.equal(set.size, 0); - const newSet = set.add(0); - assert.strictEqual(newSet, set); - assert.equal(set.size, 1); + const newSet = set.add(0); + assert.strictEqual(newSet, set); + assert.equal(set.size, 1); - set.add(1); - assert.equal(set.size, 2); + set.add(1); + assert.equal(set.size, 2); - set.add(2); // Collision with 0 - assert.equal(set.size, 3); + set.add(2); // Collision with 0 + assert.equal(set.size, 3); - set.add(3); // Collision with 1 - assert.equal(set.size, 4); + set.add(3); // Collision with 1 + assert.equal(set.size, 4); - set.add(4); // Already present as 0 - assert.equal(set.size, 4); + set.add(4); // Already present as 0 + assert.equal(set.size, 4); - set.add(5); // Already present as 1 - assert.equal(set.size, 4); - - assert.isTrue(set.has(6)); - assert.isTrue(set.has(7)); - - assert.isTrue(set.delete(8)); - assert.equal(set.size, 3); - assert.isFalse(set.has(8)); - assert.isFalse(set.delete(8)); + set.add(5); // Already present as 1 + assert.equal(set.size, 4); - assert.isTrue(set.delete(9)); - assert.equal(set.size, 2); + assert.isTrue(set.has(6)); + assert.isTrue(set.has(7)); - assert.isTrue(set.delete(10)); - assert.equal(set.size, 1); + assert.isTrue(set.delete(8)); + assert.equal(set.size, 3); + assert.isFalse(set.has(8)); + assert.isFalse(set.delete(8)); - assert.isTrue(set.delete(11)); - assert.equal(set.size, 0); - }); - it("resizing", () => { - const set = ts.createSet(x => x % 2, (x, y) => x === y); - const elementCount = 100; + assert.isTrue(set.delete(9)); + assert.equal(set.size, 2); - for (let i = 0; i < elementCount; i++) { - assert.isFalse(set.has(i)); - set.add(i); - assert.isTrue(set.has(i)); - assert.equal(set.size, i + 1); - } + assert.isTrue(set.delete(10)); + assert.equal(set.size, 1); - for (let i = 0; i < elementCount; i++) { - assert.isTrue(set.has(i)); - set.delete(i); - assert.isFalse(set.has(i)); - assert.equal(set.size, elementCount - (i + 1)); - } - }); - it("clear", () => { - const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); - for (let j = 0; j < 2; j++) { - for (let i = 0; i < 100; i++) { - set.add(i); - } - assert.equal(set.size, 4); - - set.clear(); - assert.equal(set.size, 0); - assert.isFalse(set.has(0)); - } - }); - it("forEach", () => { - const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); + assert.isTrue(set.delete(11)); + assert.equal(set.size, 0); + }); + it("resizing", () => { + const set = ts.createSet(x => x % 2, (x, y) => x === y); + const elementCount = 100; + + for (let i = 0; i < elementCount; i++) { + assert.isFalse(set.has(i)); + set.add(i); + assert.isTrue(set.has(i)); + assert.equal(set.size, i + 1); + } + + for (let i = 0; i < elementCount; i++) { + assert.isTrue(set.has(i)); + set.delete(i); + assert.isFalse(set.has(i)); + assert.equal(set.size, elementCount - (i + 1)); + } + }); + it("clear", () => { + const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); + for (let j = 0; j < 2; j++) { for (let i = 0; i < 100; i++) { set.add(i); } + assert.equal(set.size, 4); - const values: number[] = []; - const keys: number[] = []; - set.forEach((value, key) => { - values.push(value); - keys.push(key); - }); - - assert.equal(values.length, 4); - - values.sort(); - keys.sort(); - - // NB: first equal value wins (i.e. not [96, 97, 98, 99]) - const expected = [0, 1, 2, 3]; - assert.deepEqual(values, expected); - assert.deepEqual(keys, expected); - }); - it("iteration", () => { - const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); - for (let i = 0; i < 4; i++) { - set.add(i); - } - - const expected = [0, 1, 2, 3]; - let actual: number[]; - - actual = ts.arrayFrom(set.keys()); - actual.sort(); - assert.deepEqual(actual, expected); - - actual = ts.arrayFrom(set.values()); - actual.sort(); - assert.deepEqual(actual, expected); - - const actualTuple = ts.arrayFrom(set.entries()); - assert.isFalse(actualTuple.some(([v, k]) => v !== k)); - actual = actualTuple.map(([v, _]) => v); - actual.sort(); - assert.deepEqual(actual, expected); + set.clear(); + assert.equal(set.size, 0); + assert.isFalse(set.has(0)); + } + }); + it("forEach", () => { + const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); + for (let i = 0; i < 100; i++) { + set.add(i); + } + + const values: number[] = []; + const keys: number[] = []; + set.forEach((value, key) => { + values.push(value); + keys.push(key); }); - it("string hash code", () => { - interface Thing { - x: number; - y: string; - } - - const set = ts.createSet(t => t.y, (t, u) => t.x === u.x && t.y === u.y); - - const thing1: Thing = { - x: 1, - y: "a", - }; - - const thing2: Thing = { - x: 2, - y: "b", - }; - const thing3: Thing = { - x: 3, - y: "a", // Collides with thing1 - }; + assert.equal(values.length, 4); - set.add(thing1); - set.add(thing2); - set.add(thing3); + values.sort(); + keys.sort(); - assert.equal(set.size, 3); - - assert.isTrue(set.has(thing1)); - assert.isTrue(set.has(thing2)); - assert.isTrue(set.has(thing3)); - - assert.isFalse(set.has({ - x: 4, - y: "a", // Collides with thing1 - })); - - assert.isFalse(set.has({ - x: 5, - y: "c", // No collision - })); - }); + // NB: first equal value wins (i.e. not [96, 97, 98, 99]) + const expected = [0, 1, 2, 3]; + assert.deepEqual(values, expected); + assert.deepEqual(keys, expected); + }); + it("iteration", () => { + const set = ts.createSet(x => x % 2, (x, y) => (x % 4) === (y % 4)); + for (let i = 0; i < 4; i++) { + set.add(i); + } + + const expected = [0, 1, 2, 3]; + let actual: number[]; + + actual = ts.arrayFrom(set.keys()); + actual.sort(); + assert.deepEqual(actual, expected); + + actual = ts.arrayFrom(set.values()); + actual.sort(); + assert.deepEqual(actual, expected); + + const actualTuple = ts.arrayFrom(set.entries()); + assert.isFalse(actualTuple.some(([v, k]) => v !== k)); + actual = actualTuple.map(([v, _]) => v); + actual.sort(); + assert.deepEqual(actual, expected); + }); + it("string hash code", () => { + interface Thing { + x: number; + y: string; + } + + const set = ts.createSet(t => t.y, (t, u) => t.x === u.x && t.y === u.y); + + const thing1: Thing = { + x: 1, + y: "a", + }; + + const thing2: Thing = { + x: 2, + y: "b", + }; + + const thing3: Thing = { + x: 3, + y: "a", // Collides with thing1 + }; + + set.add(thing1); + set.add(thing2); + set.add(thing3); + + assert.equal(set.size, 3); + + assert.isTrue(set.has(thing1)); + assert.isTrue(set.has(thing2)); + assert.isTrue(set.has(thing3)); + + assert.isFalse(set.has({ + x: 4, + y: "a", // Collides with thing1 + })); + + assert.isFalse(set.has({ + x: 5, + y: "c", // No collision + })); }); }); +}); } diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 29ddc070c815d..95504ebe30197 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,470 +1,439 @@ namespace ts { - describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { - - function assertParseResult(commandLine: string[], expectedParsedCommandLine: ts.ParsedCommandLine, workerDiagnostic?: () => ts.ParseCommandLineWorkerDiagnostics) { - const parsed = ts.parseCommandLineWorker(workerDiagnostic?.() || ts.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); - // Allow matching a prefix of the error message - if (typeof expectedError.messageText === "string" && expectedError.messageText.includes("[...]")) { - const prefix = expectedError.messageText.split("[...]")[0]; - assert(expectedError.messageText.startsWith(prefix)); - } - else { - assert.equal(parsedError.messageText, expectedError.messageText); - } +describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { + + function assertParseResult(commandLine: string[], expectedParsedCommandLine: ts.ParsedCommandLine, workerDiagnostic?: () => ts.ParseCommandLineWorkerDiagnostics) { + const parsed = ts.parseCommandLineWorker(workerDiagnostic?.() || ts.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); + // Allow matching a prefix of the error message + if (typeof expectedError.messageText === "string" && expectedError.messageText.includes("[...]")) { + const prefix = expectedError.messageText.split("[...]")[0]; + assert(expectedError.messageText.startsWith(prefix)); } - - 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); + else { + 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 'may only be used with --build' flags", () => { - const buildFlags = ["--clean", "--dry", "--force", "--verbose"]; + it("Handles 'may only be used with --build' flags", () => { + const buildFlags = ["--clean", "--dry", "--force", "--verbose"]; + + assertParseResult(buildFlags, { + errors: buildFlags.map(buildFlag => ({ + messageText: `Compiler option '${buildFlag}' may only be used with '--build'.`, + category: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build.category, + code: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build.code, + file: undefined, + start: undefined, + length: undefined + })), + fileNames: [], + options: {} + }); + }); - assertParseResult(buildFlags, { - errors: buildFlags.map(buildFlag => ({ - messageText: `Compiler option '${buildFlag}' may only be used with '--build'.`, - category: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build.category, - code: ts.Diagnostics.Compiler_option_0_may_only_be_used_with_build.code, + it("Handles 'did you mean?' for misspelt flags", () => { + // --declarations --allowTS + assertParseResult(["--declarations", "--allowTS"], { + errors: [ + { + messageText: "Unknown compiler option '--declarations'. Did you mean 'declaration'?", + category: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, file: undefined, start: undefined, length: undefined - })), - fileNames: [], - options: {} - }); + }, + { + messageText: "Unknown compiler option '--allowTS'. Did you mean 'allowJs'?", + category: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: ts.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: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, - code: ts.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: ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, - code: ts.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 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' [...]", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - 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"] - } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--jsx' option must be: 'preserve', 'react-native', 'react', 'react-jsx', 'react-jsxdev'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - 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' [...]", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.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', 'react-jsx', 'react-jsxdev'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { jsx: undefined } + }); + }); - 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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - it("Parse empty options of --module ", () => { - // 0.ts -- - assertParseResult(["0.ts", "--module"], { - errors: [{ - messageText: "Compiler option 'module' expects an argument.", - category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.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', 'es2022', 'esnext', 'node16', 'nodenext'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { module: undefined } - }); - }); + 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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + it("Parse empty options of --newLine ", () => { + // 0.ts --newLine + assertParseResult(["0.ts", "--newLine"], { + errors: [{ + messageText: "Compiler option 'newLine' expects an argument.", + category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { newLine: undefined } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - it("Parse empty options of --target ", () => { - // 0.ts --target - assertParseResult(["0.ts", "--target"], { - errors: [{ - messageText: "Compiler option 'target' expects an argument.", - category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.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', 'es2021', 'es2022', 'esnext'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { newLine: undefined } + }); + }); - 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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - it("Parse empty options of --moduleResolution ", () => { - // 0.ts --moduleResolution - assertParseResult(["0.ts", "--moduleResolution"], { - errors: [{ - messageText: "Compiler option 'moduleResolution' expects an argument.", - category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic', 'node16', 'nodenext'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'es2021', 'es2022', 'esnext'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { moduleResolution: undefined } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { target: undefined } + }); + }); - it("Parse empty options of --lib ", () => { - // 0.ts --lib - assertParseResult(["0.ts", "--lib"], { - errors: [{ - messageText: "Compiler option 'lib' expects an argument.", - category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { - lib: [] - } - }); - }); + it("Parse empty options of --moduleResolution ", () => { + // 0.ts --moduleResolution + assertParseResult(["0.ts", "--moduleResolution"], { + errors: [{ + messageText: "Compiler option 'moduleResolution' expects an argument.", + category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - 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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, - code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { - lib: [] - } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic', 'node16', 'nodenext'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - 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 - } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { moduleResolution: undefined } + }); + }); - 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' [...].", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.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 empty options of --lib ", () => { + // 0.ts --lib + assertParseResult(["0.ts", "--lib"], { + errors: [{ + messageText: "Compiler option 'lib' expects an argument.", + category: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - 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', [...]", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.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"] - } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { + lib: [] + } + }); + }); - 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: ts.ScriptTarget.ES5, - } - }); - }); + 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: ts.Diagnostics.Compiler_option_0_expects_an_argument.category, + code: ts.Diagnostics.Compiler_option_0_expects_an_argument.code, - 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: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES5, - lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], - } - }); - }); + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { + lib: [] + } + }); + }); - 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: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES5, - lib: ["lib.es2015.core.d.ts", "lib.es2015.symbol.wellknown.d.ts"], - } - }); - }); + 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 explicit boolean flag value", () => { - assertParseResult(["--strictNullChecks", "false", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: { - strictNullChecks: false, - } - }); - }); + 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' [...].", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.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 non boolean argument after boolean flag", () => { - assertParseResult(["--noImplicitAny", "t", "0.ts"], { - errors: [], - fileNames: ["t", "0.ts"], - options: { - noImplicitAny: true, - } - }); - }); + 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', [...]", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.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 implicit boolean flag value", () => { - assertParseResult(["--strictNullChecks"], { - errors: [], - fileNames: [], - options: { - strictNullChecks: true, - } - }); - }); + 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: ts.ScriptTarget.ES5, + } + }); + }); - it("parse --incremental", () => { - // --lib es6 0.ts - assertParseResult(["--incremental", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: { incremental: true } - }); - }); + 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: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5, + lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], + } + }); + }); - it("parse --tsBuildInfoFile", () => { - // --lib es6 0.ts - assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: { tsBuildInfoFile: "build.tsbuildinfo" } - }); - }); + 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: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5, + lib: ["lib.es2015.core.d.ts", "lib.es2015.symbol.wellknown.d.ts"], + } + }); + }); - describe("parses command line null for tsconfig only option", () => { - interface VerifyNull { - optionName: string; - nonNullValue?: string; - workerDiagnostic?: () => ts.ParseCommandLineWorkerDiagnostics; - diagnosticMessage: ts.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); - }); + it("Parse explicit boolean flag value", () => { + assertParseResult(["--strictNullChecks", "false", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { + strictNullChecks: false, + } + }); + }); - if (nonNullValue) { - it("errors if non null value is passed", () => { - assertParseResult([`--${optionName}`, nonNullValue, "0.ts"], { - errors: [{ - messageText: ts.formatStringFromArgs(diagnosticMessage.message, [optionName]), - category: diagnosticMessage.category, - code: diagnosticMessage.code, - file: undefined, - start: undefined, - length: undefined - }], - fileNames: ["0.ts"], - options: {} - }, workerDiagnostic); - }); + it("Parse non boolean argument after boolean flag", () => { + assertParseResult(["--noImplicitAny", "t", "0.ts"], { + errors: [], + fileNames: ["t", "0.ts"], + options: { + noImplicitAny: true, } + }); + }); - it("errors if its followed by another option", () => { - assertParseResult(["0.ts", "--strictNullChecks", `--${optionName}`], { - errors: [{ - messageText: ts.formatStringFromArgs(diagnosticMessage.message, [optionName]), - category: diagnosticMessage.category, - code: diagnosticMessage.code, - file: undefined, - start: undefined, - length: undefined - }], - fileNames: ["0.ts"], - options: { strictNullChecks: true } - }, workerDiagnostic); - }); + it("Parse implicit boolean flag value", () => { + assertParseResult(["--strictNullChecks"], { + errors: [], + fileNames: [], + options: { + strictNullChecks: true, + } + }); + }); - it("errors if its last option", () => { - assertParseResult(["0.ts", `--${optionName}`], { + 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"], { + errors: [], + fileNames: ["0.ts"], + options: { tsBuildInfoFile: "build.tsbuildinfo" } + }); + }); + + describe("parses command line null for tsconfig only option", () => { + interface VerifyNull { + optionName: string; + nonNullValue?: string; + workerDiagnostic?: () => ts.ParseCommandLineWorkerDiagnostics; + diagnosticMessage: ts.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: ts.formatStringFromArgs(diagnosticMessage.message, [optionName]), category: diagnosticMessage.category, @@ -479,512 +448,543 @@ namespace ts { }); } - interface VerifyNullNonIncludedOption { - type: () => "string" | "number" | ts.ESMap; - nonNullValue?: string; - } - function verifyNullNonIncludedOption({ type, nonNullValue }: VerifyNullNonIncludedOption) { - verifyNull({ - optionName: "optionName", - nonNullValue, - diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, - workerDiagnostic: () => { - const optionDeclarations: ts.CommandLineOption[] = [ - ...ts.compilerOptionsDidYouMeanDiagnostics.optionDeclarations, - { - name: "optionName", - type: type(), - isTSConfigOnly: true, - category: ts.Diagnostics.Backwards_Compatibility, - description: ts.Diagnostics.Enable_project_compilation, - defaultValueDescription: undefined, - } - ]; - return { - ...ts.compilerOptionsDidYouMeanDiagnostics, - optionDeclarations, - getOptionsNameMap: () => ts.createOptionNameMap(optionDeclarations) - }; - } - }); - } - - describe("option of type boolean", () => { - it("allows setting it to false", () => { - assertParseResult(["--composite", "false", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: { composite: false } - }); - - }); - verifyNull({ - optionName: "composite", - nonNullValue: "true", - diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line - }); - }); - - describe("option of type object", () => { - verifyNull({ - optionName: "paths", - diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line - }); - }); - - describe("option of type list", () => { - verifyNull({ - optionName: "rootDirs", - nonNullValue: "abc,xyz", - diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line - }); - }); - - describe("option of type string", () => { - verifyNullNonIncludedOption({ - type: () => "string", - nonNullValue: "hello" - }); - }); - - describe("option of type number", () => { - verifyNullNonIncludedOption({ - type: () => "number", - nonNullValue: "10" - }); - }); - - describe("option of type Map", () => { - verifyNullNonIncludedOption({ - type: () => new ts.Map(ts.getEntries({ - node: ts.ModuleResolutionKind.NodeJs, - classic: ts.ModuleResolutionKind.Classic, - })), - nonNullValue: "node" - }); + it("errors if its followed by another option", () => { + assertParseResult(["0.ts", "--strictNullChecks", `--${optionName}`], { + errors: [{ + messageText: ts.formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: { strictNullChecks: true } + }, workerDiagnostic); }); - }); - - 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: [], + it("errors if its last option", () => { + assertParseResult(["0.ts", `--${optionName}`], { + errors: [{ + messageText: ts.formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], fileNames: ["0.ts"], - options: {}, - watchOptions: { watchFile: ts.WatchFileKind.UseFsEvents } - }); + options: {} + }, workerDiagnostic); }); + } - it("parse --watchDirectory", () => { - assertParseResult(["--watchDirectory", "FixedPollingInterval", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } - }); + interface VerifyNullNonIncludedOption { + type: () => "string" | "number" | ts.ESMap; + nonNullValue?: string; + } + function verifyNullNonIncludedOption({ type, nonNullValue }: VerifyNullNonIncludedOption) { + verifyNull({ + optionName: "optionName", + nonNullValue, + diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, + workerDiagnostic: () => { + const optionDeclarations: ts.CommandLineOption[] = [ + ...ts.compilerOptionsDidYouMeanDiagnostics.optionDeclarations, + { + name: "optionName", + type: type(), + isTSConfigOnly: true, + category: ts.Diagnostics.Backwards_Compatibility, + description: ts.Diagnostics.Enable_project_compilation, + defaultValueDescription: undefined, + } + ]; + return { + ...ts.compilerOptionsDidYouMeanDiagnostics, + optionDeclarations, + getOptionsNameMap: () => ts.createOptionNameMap(optionDeclarations) + }; + } }); + } - it("parse --fallbackPolling", () => { - assertParseResult(["--fallbackPolling", "PriorityInterval", "0.ts"], { + describe("option of type boolean", () => { + it("allows setting it to false", () => { + assertParseResult(["--composite", "false", "0.ts"], { errors: [], fileNames: ["0.ts"], - options: {}, - watchOptions: { fallbackPolling: ts.PollingWatchKind.PriorityInterval } - }); + options: { composite: false } }); - it("parse --synchronousWatchDirectory", () => { - assertParseResult(["--synchronousWatchDirectory", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { synchronousWatchDirectory: true } - }); }); + verifyNull({ + optionName: "composite", + nonNullValue: "true", + diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line + }); + }); - it("errors on missing argument to --fallbackPolling", () => { - assertParseResult(["0.ts", "--fallbackPolling"], { - errors: [ - { - messageText: "Watch option 'fallbackPolling' requires a value of type string.", - category: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, - code: ts.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', 'fixedchunksize'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined - } - ], - fileNames: ["0.ts"], - options: {}, - watchOptions: { fallbackPolling: undefined } - }); + describe("option of type object", () => { + verifyNull({ + optionName: "paths", + diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line }); + }); - it("parse --excludeDirectories", () => { - assertParseResult(["--excludeDirectories", "**/temp", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { excludeDirectories: ["**/temp"] } - }); + describe("option of type list", () => { + verifyNull({ + optionName: "rootDirs", + nonNullValue: "abc,xyz", + diagnosticMessage: ts.Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line }); + }); - it("errors on invalid excludeDirectories", () => { - assertParseResult(["--excludeDirectories", "**/../*", "0.ts"], { - errors: [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: undefined, - start: undefined, - length: undefined - } - ], - fileNames: ["0.ts"], - options: {}, - watchOptions: { excludeDirectories: [] } - }); + describe("option of type string", () => { + verifyNullNonIncludedOption({ + type: () => "string", + nonNullValue: "hello" }); + }); - it("parse --excludeFiles", () => { - assertParseResult(["--excludeFiles", "**/temp/*.ts", "0.ts"], { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { excludeFiles: ["**/temp/*.ts"] } - }); + describe("option of type number", () => { + verifyNullNonIncludedOption({ + type: () => "number", + nonNullValue: "10" }); + }); - it("errors on invalid excludeFiles", () => { - assertParseResult(["--excludeFiles", "**/../*", "0.ts"], { - errors: [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: undefined, - start: undefined, - length: undefined - } - ], - fileNames: ["0.ts"], - options: {}, - watchOptions: { excludeFiles: [] } - }); + describe("option of type Map", () => { + verifyNullNonIncludedOption({ + type: () => new ts.Map(ts.getEntries({ + node: ts.ModuleResolutionKind.NodeJs, + classic: ts.ModuleResolutionKind.Classic, + })), + nonNullValue: "node" }); }); }); - describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { - function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ts.ParsedBuildCommand) { - const parsed = ts.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("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 } + }); + }); - 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([], { + describe("Watch options", () => { + it("parse --watchFile", () => { + assertParseResult(["--watchFile", "UseFsEvents", "0.ts"], { errors: [], - projects: ["."], - buildOptions: {}, - watchOptions: undefined + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchFile: ts.WatchFileKind.UseFsEvents } }); }); - it("Parse multiple options", () => { - // --lib es5,es2015.symbol.wellknown 0.ts - assertParseResult(["--verbose", "--force", "tests"], { + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "0.ts"], { 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: ts.Diagnostics.Unknown_build_option_0.category, - code: ts.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: "Compiler option '--listFilesOnly' may not be used with '--build'.", - category: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.category, - code: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.code, - file: undefined, - start: undefined, - length: undefined, - }], - projects: ["."], - buildOptions: {}, - watchOptions: undefined, + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } }); }); - it("Parse multiple flags with input projects at the end", () => { - // --lib es5,es2015.symbol.wellknown --target es5 0.ts - assertParseResult(["--force", "--verbose", "src", "tests"], { + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "0.ts"], { errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: ts.PollingWatchKind.PriorityInterval } }); }); - 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"], { + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "0.ts"], { errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, + fileNames: ["0.ts"], + options: {}, + watchOptions: { synchronousWatchDirectory: true } + }); + }); + + it("errors on missing argument to --fallbackPolling", () => { + assertParseResult(["0.ts", "--fallbackPolling"], { + errors: [ + { + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: ts.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', 'fixedchunksize'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: 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"], { + it("parse --excludeDirectories", () => { + assertParseResult(["--excludeDirectories", "**/temp", "0.ts"], { errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, + fileNames: ["0.ts"], + options: {}, + watchOptions: { excludeDirectories: ["**/temp"] } + }); + }); + + it("errors on invalid excludeDirectories", () => { + assertParseResult(["--excludeDirectories", "**/../*", "0.ts"], { + errors: [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: ["0.ts"], + options: {}, + watchOptions: { excludeDirectories: [] } }); }); - it("parse build with --incremental", () => { - // --lib es6 0.ts - assertParseResult(["--incremental", "tests"], { + it("parse --excludeFiles", () => { + assertParseResult(["--excludeFiles", "**/temp/*.ts", "0.ts"], { errors: [], - projects: ["tests"], - buildOptions: { incremental: true }, - watchOptions: undefined, + fileNames: ["0.ts"], + options: {}, + watchOptions: { excludeFiles: ["**/temp/*.ts"] } + }); + }); + + it("errors on invalid excludeFiles", () => { + assertParseResult(["--excludeFiles", "**/../*", "0.ts"], { + errors: [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: ["0.ts"], + options: {}, + watchOptions: { excludeFiles: [] } }); }); + }); +}); + +describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { + function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ts.ParsedBuildCommand) { + const parsed = ts.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 with --locale en-us", () => { - // --lib es6 0.ts - assertParseResult(["--locale", "en-us", "src"], { - errors: [], - projects: ["src"], - buildOptions: { locale: "en-us" }, - 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 build with --tsBuildInfoFile", () => { - // --lib es6 0.ts - assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "tests"], { - errors: [{ - messageText: "Compiler option '--tsBuildInfoFile' may not be used with '--build'.", - category: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.category, - code: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.code, - file: undefined, - start: undefined, - length: undefined - }], - projects: ["build.tsbuildinfo", "tests"], - 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("reports other common 'may not be used with --build' flags", () => { - const buildFlags = ["--declaration", "--strict"]; + it("Parse option with invalid option ", () => { + // --lib es5,invalidOption 0.ts + assertParseResult(["--verbose", "--invalidOption"], { + errors: [{ + messageText: "Unknown build option '--invalidOption'.", + category: ts.Diagnostics.Unknown_build_option_0.category, + code: ts.Diagnostics.Unknown_build_option_0.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: undefined + }); + }); - assertParseResult(buildFlags, { - errors: buildFlags.map(buildFlag => ({ - messageText: `Compiler option '${buildFlag}' may not be used with '--build'.`, + it("parse build with listFilesOnly ", () => { + // --lib es6 0.ts + assertParseResult(["--listFilesOnly"], { + errors: [{ + messageText: "Compiler option '--listFilesOnly' may not be used with '--build'.", category: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.category, code: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.code, file: undefined, start: undefined, - length: undefined - })), - buildOptions: {}, + length: undefined, + }], projects: ["."], + buildOptions: {}, watchOptions: undefined, }); - }); - - describe("Combining options that make no sense together", () => { - function verifyInvalidCombination(flag1: keyof ts.BuildOptions, flag2: keyof ts.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: ts.Diagnostics.Options_0_and_1_cannot_be_combined.category, - code: ts.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: ts.WatchFileKind.UseFsEvents } - }); + 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 --watchDirectory", () => { - assertParseResult(["--watchDirectory", "FixedPollingInterval", "--verbose"], { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } - }); + 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 --fallbackPolling", () => { - assertParseResult(["--fallbackPolling", "PriorityInterval", "--verbose"], { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { fallbackPolling: ts.PollingWatchKind.PriorityInterval } - }); + 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 --synchronousWatchDirectory", () => { - assertParseResult(["--synchronousWatchDirectory", "--verbose"], { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { synchronousWatchDirectory: true } - }); + it("parse build with --incremental", () => { + // --lib es6 0.ts + assertParseResult(["--incremental", "tests"], { + errors: [], + projects: ["tests"], + buildOptions: { incremental: true }, + watchOptions: undefined, }); + }); - it("errors on missing argument", () => { - assertParseResult(["--verbose", "--fallbackPolling"], { - errors: [ - { - messageText: "Watch option 'fallbackPolling' requires a value of type string.", - category: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, - code: ts.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', 'fixedchunksize'.", - category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined - } - ], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { fallbackPolling: 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("errors on invalid excludeDirectories", () => { - assertParseResult(["--excludeDirectories", "**/../*"], { - errors: [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: undefined, - start: undefined, - length: undefined - } - ], - projects: ["."], - buildOptions: {}, - watchOptions: { excludeDirectories: [] } - }); + it("parse build with --tsBuildInfoFile", () => { + // --lib es6 0.ts + assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "tests"], { + errors: [{ + messageText: "Compiler option '--tsBuildInfoFile' may not be used with '--build'.", + category: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.category, + code: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.code, + file: undefined, + start: undefined, + length: undefined + }], + projects: ["build.tsbuildinfo", "tests"], + buildOptions: {}, + watchOptions: undefined, }); + }); - it("parse --excludeFiles", () => { - assertParseResult(["--excludeFiles", "**/temp/*.ts"], { - errors: [], - projects: ["."], - buildOptions: {}, - watchOptions: { excludeFiles: ["**/temp/*.ts"] } - }); - }); + it("reports other common 'may not be used with --build' flags", () => { + const buildFlags = ["--declaration", "--strict"]; + + assertParseResult(buildFlags, { + errors: buildFlags.map(buildFlag => ({ + messageText: `Compiler option '${buildFlag}' may not be used with '--build'.`, + category: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.category, + code: ts.Diagnostics.Compiler_option_0_may_not_be_used_with_build.code, + file: undefined, + start: undefined, + length: undefined + })), + buildOptions: {}, + projects: ["."], + watchOptions: undefined, + }); + }); - it("errors on invalid excludeFiles", () => { - assertParseResult(["--excludeFiles", "**/../*"], { - errors: [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: undefined, - start: undefined, - length: undefined - } - ], + describe("Combining options that make no sense together", () => { + function verifyInvalidCombination(flag1: keyof ts.BuildOptions, flag2: keyof ts.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: ts.Diagnostics.Options_0_and_1_cannot_be_combined.category, + code: ts.Diagnostics.Options_0_and_1_cannot_be_combined.code, + file: undefined, + start: undefined, + length: undefined, + }], projects: ["."], - buildOptions: {}, - watchOptions: { excludeFiles: [] } + 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: ts.WatchFileKind.UseFsEvents } + }); + }); + + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } + }); + }); + + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: ts.PollingWatchKind.PriorityInterval } + }); + }); + + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { synchronousWatchDirectory: true } + }); + }); + + it("errors on missing argument", () => { + assertParseResult(["--verbose", "--fallbackPolling"], { + errors: [ + { + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: ts.Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: ts.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', 'fixedchunksize'.", + category: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: undefined } + }); + }); + + it("errors on invalid excludeDirectories", () => { + assertParseResult(["--excludeDirectories", "**/../*"], { + errors: [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined + } + ], + projects: ["."], + buildOptions: {}, + watchOptions: { excludeDirectories: [] } + }); + }); + + it("parse --excludeFiles", () => { + assertParseResult(["--excludeFiles", "**/temp/*.ts"], { + errors: [], + projects: ["."], + buildOptions: {}, + watchOptions: { excludeFiles: ["**/temp/*.ts"] } + }); + }); + + it("errors on invalid excludeFiles", () => { + assertParseResult(["--excludeFiles", "**/../*"], { + errors: [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined + } + ], + projects: ["."], + buildOptions: {}, + watchOptions: { excludeFiles: [] } + }); }); }); +}); } diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index d42f839a0ebe5..cb0b9b933cd80 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -1,364 +1,364 @@ 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, - 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": "" - } +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, + 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 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, "/")); +const caseSensitiveBasePath = "/dev/"; +const caseSensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ false, caseSensitiveBasePath, "/")); - function verifyDiagnostics(actual: ts.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, ts.DiagnosticCategory.Error, "Category mismatch"); // Should always be error - assert.equal(ts.flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText); - } +function verifyDiagnostics(actual: ts.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, ts.DiagnosticCategory.Error, "Category mismatch"); // Should always be error + assert.equal(ts.flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText); } +} - describe("unittests:: config:: configurationExtension", () => { - ts.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 } = ts.readConfigFile(entry, name => host.readFile(name)); - assert(config && !error, ts.flattenDiagnosticMessageText(error && error.messageText, "\n")); - return ts.parseJsonConfigFileContent(config, host, basePath, {}, entry); - } +describe("unittests:: config:: configurationExtension", () => { + ts.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 } = ts.readConfigFile(entry, name => host.readFile(name)); + assert(config && !error, ts.flattenDiagnosticMessageText(error && error.messageText, "\n")); + return ts.parseJsonConfigFileContent(config, host, basePath, {}, entry); + } - function getParseCommandLineJsonSourceFile(entry: string) { - const jsonSourceFile = ts.readJsonConfigFile(entry, name => host.readFile(name)); - assert(jsonSourceFile.endOfFileToken && !jsonSourceFile.parseDiagnostics.length, ts.flattenDiagnosticMessageText(jsonSourceFile.parseDiagnostics[0] && jsonSourceFile.parseDiagnostics[0].messageText, "\n")); - return { - jsonSourceFile, - parsed: ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, basePath, {}, entry) - }; - } + function getParseCommandLineJsonSourceFile(entry: string) { + const jsonSourceFile = ts.readJsonConfigFile(entry, name => host.readFile(name)); + assert(jsonSourceFile.endOfFileToken && !jsonSourceFile.parseDiagnostics.length, ts.flattenDiagnosticMessageText(jsonSourceFile.parseDiagnostics[0] && jsonSourceFile.parseDiagnostics[0].messageText, "\n")); + return { + jsonSourceFile, + parsed: ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, basePath, {}, entry) + }; + } - function testSuccess(name: string, entry: string, expected: ts.CompilerOptions, expectedFiles: string[]) { - expected.configFilePath = entry; - it(name, () => { - const parsed = getParseCommandLine(entry); - assert(!parsed.errors.length, ts.flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); - assert.deepEqual(parsed.options, expected); - assert.deepEqual(parsed.fileNames, expectedFiles); - }); + function testSuccess(name: string, entry: string, expected: ts.CompilerOptions, expectedFiles: string[]) { + expected.configFilePath = entry; + it(name, () => { + const parsed = getParseCommandLine(entry); + assert(!parsed.errors.length, ts.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, ts.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); - }); - } + it(name + " with jsonSourceFile", () => { + const { parsed, jsonSourceFile } = getParseCommandLineJsonSourceFile(entry); + assert(!parsed.errors.length, ts.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); - }); + 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); - }); - } + 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, - }, [ - ts.combinePaths(basePath, "main.ts"), - ts.combinePaths(basePath, "supplemental.ts"), - ]); + describe(testName, () => { + testSuccess("can resolve an extension with a base extension", "tsconfig.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + }, [ + ts.combinePaths(basePath, "main.ts"), + 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, - }, [ - ts.combinePaths(basePath, "main.ts"), - 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, + }, [ + ts.combinePaths(basePath, "main.ts"), + ts.combinePaths(basePath, "supplemental.ts"), + ]); - testFailure("can report errors on circular imports", "circular.json", [ - { - code: 18000, - messageText: `Circularity detected while resolving configuration: ${[ts.combinePaths(basePath, "circular.json"), ts.combinePaths(basePath, "circular2.json"), ts.combinePaths(basePath, "circular.json")].join(" -> ")}` - } - ]); + testFailure("can report errors on circular imports", "circular.json", [ + { + code: 18000, + messageText: `Circularity detected while resolving configuration: ${[ts.combinePaths(basePath, "circular.json"), ts.combinePaths(basePath, "circular2.json"), ts.combinePaths(basePath, "circular.json")].join(" -> ")}` + } + ]); - testFailure("can report missing configurations", "missing.json", [{ - code: 6053, - messageText: `File './missing2' not found.` - }]); + testFailure("can report missing configurations", "missing.json", [{ + code: 6053, + messageText: `File './missing2' not found.` + }]); - testFailure("can report errors in extended configs", "failure.json", [{ - code: 6114, - messageText: `Unknown option 'excludes'. Did you mean 'exclude'?` - }]); + 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", [{ - code: 5024, - messageText: `Compiler option 'extends' requires a value of type string.` - }]); + 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 - }, [ - ts.combinePaths(basePath, "supplemental.ts") - ]); + 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 + }, [ + ts.combinePaths(basePath, "supplemental.ts") + ]); - testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - module: ts.ModuleKind.System - }, [ - ts.combinePaths(basePath, "main.ts") - ]); + testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + module: ts.ModuleKind.System + }, [ + ts.combinePaths(basePath, "main.ts") + ]); - testSuccess("can overwrite top-level files using extended []", "configs/fifth.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - module: ts.ModuleKind.System - }, [ - ts.combinePaths(basePath, "tests/utils.ts") - ]); + testSuccess("can overwrite top-level files using extended []", "configs/fifth.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + module: ts.ModuleKind.System + }, [ + ts.combinePaths(basePath, "tests/utils.ts") + ]); - describe("finding extended configs from node_modules", () => { - testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); - }); + describe("finding extended configs from node_modules", () => { + testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + }); - it("adds extendedSourceFiles only once", () => { - const sourceFile = ts.readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); - const dir = ts.combinePaths(basePath, "configs"); - const expected = [ - ts.combinePaths(dir, "third.json"), - ts.combinePaths(dir, "second.json"), - ts.combinePaths(dir, "base.json"), - ]; - ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); - assert.deepEqual(sourceFile.extendedSourceFiles, expected); - ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); - assert.deepEqual(sourceFile.extendedSourceFiles, expected); - }); + it("adds extendedSourceFiles only once", () => { + const sourceFile = ts.readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); + const dir = ts.combinePaths(basePath, "configs"); + const expected = [ + ts.combinePaths(dir, "third.json"), + ts.combinePaths(dir, "second.json"), + ts.combinePaths(dir, "base.json"), + ]; + ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); + assert.deepEqual(sourceFile.extendedSourceFiles, expected); + ts.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 8befec359ddc5..9d069c284da77 100644 --- a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts +++ b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts @@ -1,574 +1,574 @@ namespace ts { - describe("unittests:: config:: convertCompilerOptionsFromJson", () => { - const formatDiagnosticHost: ts.FormatDiagnosticsHost = { - getCurrentDirectory: () => "/apath/", - getCanonicalFileName: ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), - getNewLine: () => "\n" - }; - - interface ExpectedResultWithParsingSuccess { - compilerOptions: ts.CompilerOptions; - errors: readonly ts.Diagnostic[]; +describe("unittests:: config:: convertCompilerOptionsFromJson", () => { + const formatDiagnosticHost: ts.FormatDiagnosticsHost = { + getCurrentDirectory: () => "/apath/", + getCanonicalFileName: ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), + getNewLine: () => "\n" + }; + + interface ExpectedResultWithParsingSuccess { + compilerOptions: ts.CompilerOptions; + errors: readonly ts.Diagnostic[]; + } + + interface ExpectedResultWithParsingFailure { + compilerOptions: ts.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 } = ts.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 = ts.parseJsonText(configFileName, fileText); + assert(!!result.endOfFileToken); + assert.equal(!!result.parseDiagnostics.length, isExpectedResultWithParsingFailure(expectedResult)); + const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); + const { options: actualCompilerOptions, errors: actualParseErrors } = ts.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 !== ts.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: ts.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 } = ts.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 verifyErrors(actualErrors: ts.Diagnostic[], expectedErrors: readonly ts.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.isDefined(actualError.start); + assert(actualError.length); + } } - function assertCompilerOptionsWithJsonText(fileText: string, configFileName: string, expectedResult: ExpectedResult) { - const result = ts.parseJsonText(configFileName, fileText); - assert(!!result.endOfFileToken); - assert.equal(!!result.parseDiagnostics.length, isExpectedResultWithParsingFailure(expectedResult)); - const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); - const { options: actualCompilerOptions, errors: actualParseErrors } = ts.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 !== ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code), expectedResult.errors); + function getDiagnosticString(diagnostic: ts.Diagnostic) { + if (ignoreLocation) { + const { file, ...rest } = diagnostic; + diagnostic = { file: undefined, ...rest }; } + return ts.formatDiagnostic(diagnostic, formatDiagnosticHost); } + } - function verifyErrors(actualErrors: ts.Diagnostic[], expectedErrors: readonly ts.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.isDefined(actualError.start); - assert(actualError.length); + // 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: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] + }, + errors: [] + }); - function getDiagnosticString(diagnostic: ts.Diagnostic) { - if (ignoreLocation) { - const { file, ...rest } = diagnostic; - diagnostic = { file: undefined, ...rest }; + }); + 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"] } - return ts.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: ts.ModuleKind.CommonJS, - target: ts.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: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - allowJs: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] - }, - errors: [] - }); + }, "tsconfig.json", { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.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: ts.ModuleKind.CommonJS, - target: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ModuleKind.CommonJS, + target: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.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', 'es2020', 'es2022', 'esnext'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.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', 'es2020', 'es2022', 'esnext'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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', 'esnext.bigint', 'esnext.string', 'esnext.promise'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ModuleKind.CommonJS, + target: ts.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', 'esnext.bigint', 'esnext.string', 'esnext.promise'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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', 'esnext.string', 'esnext.promise'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ModuleKind.CommonJS, + target: ts.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', 'esnext.string', 'esnext.promise'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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', 'esnext.string', 'esnext.promise'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ModuleKind.CommonJS, + target: ts.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', 'esnext.string', 'esnext.promise'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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', 'esnext.string', 'esnext.promise'.", - code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: ts.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: ts.ModuleKind.CommonJS, + target: ts.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', 'esnext.string', 'esnext.promise'.", + code: ts.Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: [] + }, + errors: [] + }); - }); - it("Convert empty string option of moduleSuffixes to compiler-options ", () => { - assertCompilerOptions({ - compilerOptions: { - moduleSuffixes: [".ios", ""] - } - }, "tsconfig.json", { - compilerOptions: { - moduleSuffixes: [".ios", ""] - }, - errors: [] - }); + }); + it("Convert empty string option of moduleSuffixes to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + moduleSuffixes: [".ios", ""] + } + }, "tsconfig.json", { + compilerOptions: { + moduleSuffixes: [".ios", ""] + }, + errors: [] + }); - }); - it("Convert empty string option of moduleSuffixes to compiler-options ", () => { - assertCompilerOptions({ - compilerOptions: { - moduleSuffixes: [""] - } - }, "tsconfig.json", { - compilerOptions: { - moduleSuffixes: [""] - }, - errors: [] - }); + }); + it("Convert empty string option of moduleSuffixes to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + moduleSuffixes: [""] + } + }, "tsconfig.json", { + compilerOptions: { + moduleSuffixes: [""] + }, + errors: [] + }); - }); - it("Convert trailing-whitespace string option of moduleSuffixes to compiler-options ", () => { - assertCompilerOptions({ - compilerOptions: { - moduleSuffixes: [" "] - } - }, "tsconfig.json", { - compilerOptions: { - moduleSuffixes: [" "] - }, - errors: [] - }); + }); + it("Convert trailing-whitespace string option of moduleSuffixes to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + moduleSuffixes: [" "] + } + }, "tsconfig.json", { + compilerOptions: { + moduleSuffixes: [" "] + }, + errors: [] + }); - }); - it("Convert empty option of moduleSuffixes to compiler-options ", () => { - assertCompilerOptions({ - compilerOptions: { - moduleSuffixes: [] - } - }, "tsconfig.json", { - compilerOptions: { - moduleSuffixes: [] - }, - errors: [] - }); + }); + it("Convert empty option of moduleSuffixes to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + moduleSuffixes: [] + } + }, "tsconfig.json", { + compilerOptions: { + moduleSuffixes: [] + }, + 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: ts.Diagnostics.Unknown_compiler_option_0.code, - category: ts.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: ts.Diagnostics.Unknown_compiler_option_0.code, + category: ts.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: ts.ModuleKind.CommonJS, - target: ts.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: ts.ModuleKind.CommonJS, + target: ts.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: ts.ModuleKind.CommonJS, - target: ts.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: ts.ModuleKind.CommonJS, + target: ts.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: ts.Diagnostics.Unknown_compiler_option_0.code, - category: ts.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: ts.Diagnostics.Unknown_compiler_option_0.code, + category: ts.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", @@ -588,121 +588,121 @@ namespace ts { } } `, "tsconfig.json", { - compilerOptions: { - target: undefined, - module: ts.ModuleKind.ESNext, - experimentalDecorators: true, - }, - hasParseErrors: true - }); + compilerOptions: { + target: undefined, + module: ts.ModuleKind.ESNext, + experimentalDecorators: true, + }, + hasParseErrors: true }); + }); - it("Convert a tsconfig file with stray trailing characters", () => { - assertCompilerOptionsWithJsonText(`{ + it("Convert a tsconfig file with stray trailing characters", () => { + assertCompilerOptionsWithJsonText(`{ "compilerOptions": { "target": "esnext" } } blah`, "tsconfig.json", { - compilerOptions: { - target: ts.ScriptTarget.ESNext - }, - hasParseErrors: true, - errors: [{ - ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, - messageText: "The root value of a 'tsconfig.json' file must be an object.", - file: undefined, - start: 0, - length: 0 - }] - }); + compilerOptions: { + target: ts.ScriptTarget.ESNext + }, + hasParseErrors: true, + errors: [{ + ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, + messageText: "The root value of a 'tsconfig.json' file must be an object.", + file: undefined, + start: 0, + length: 0 + }] }); + }); - it("Convert a tsconfig file with stray leading characters", () => { - assertCompilerOptionsWithJsonText(`blah { + it("Convert a tsconfig file with stray leading characters", () => { + assertCompilerOptionsWithJsonText(`blah { "compilerOptions": { "target": "esnext" } }`, "tsconfig.json", { - compilerOptions: { - target: ts.ScriptTarget.ESNext - }, - hasParseErrors: true, - errors: [{ - ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, - messageText: "The root value of a 'tsconfig.json' file must be an object.", - file: undefined, - start: 0, - length: 0 - }] - }); + compilerOptions: { + target: ts.ScriptTarget.ESNext + }, + hasParseErrors: true, + errors: [{ + ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, + messageText: "The root value of a 'tsconfig.json' file must be an object.", + file: undefined, + start: 0, + length: 0 + }] }); + }); - it("Convert a tsconfig file as an array", () => { - assertCompilerOptionsWithJsonText(`[{ + it("Convert a tsconfig file as an array", () => { + assertCompilerOptionsWithJsonText(`[{ "compilerOptions": { "target": "esnext" } }]`, "tsconfig.json", { - compilerOptions: { - target: ts.ScriptTarget.ESNext - }, - errors: [{ - ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, - messageText: "The root value of a 'tsconfig.json' file must be an object.", - file: undefined, - start: 0, - length: 0 - }] - }); + compilerOptions: { + target: ts.ScriptTarget.ESNext + }, + errors: [{ + ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, + messageText: "The root value of a 'tsconfig.json' file must be an object.", + file: undefined, + start: 0, + length: 0 + }] }); + }); - it("raises an error if you've set a compiler flag in the root without including 'compilerOptions'", () => { - assertCompilerOptionsWithJsonText(`{ + it("raises an error if you've set a compiler flag in the root without including 'compilerOptions'", () => { + assertCompilerOptionsWithJsonText(`{ "module": "esnext", }`, "tsconfig.json", { - compilerOptions: {}, - errors: [{ - ...ts.Diagnostics._0_should_be_set_inside_the_compilerOptions_object_of_the_config_json_file, - messageText: "'module' should be set inside the 'compilerOptions' object of the config json file.", - file: undefined, - start: 0, - length: 0 - }] - }); + compilerOptions: {}, + errors: [{ + ...ts.Diagnostics._0_should_be_set_inside_the_compilerOptions_object_of_the_config_json_file, + messageText: "'module' should be set inside the 'compilerOptions' object of the config json file.", + file: undefined, + start: 0, + length: 0 + }] }); + }); - it("does not raise an error if you've set a compiler flag in the root when you have included 'compilerOptions'", () => { - assertCompilerOptionsWithJsonText(`{ + it("does not raise an error if you've set a compiler flag in the root when you have included 'compilerOptions'", () => { + assertCompilerOptionsWithJsonText(`{ "target": "esnext", "compilerOptions": { "module": "esnext" } }`, "tsconfig.json", { - compilerOptions: { - module: ts.ModuleKind.ESNext - }, - errors: [] - }); + compilerOptions: { + module: ts.ModuleKind.ESNext + }, + errors: [] }); + }); - it("Don't crash when root expression is not object at all", () => { - assertCompilerOptionsWithJsonText(`42`, "tsconfig.json", { - compilerOptions: {}, - errors: [{ - ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, - messageText: "The root value of a 'tsconfig.json' file must be an object.", - file: undefined, - start: 0, - length: 0 - }] - }); + it("Don't crash when root expression is not object at all", () => { + assertCompilerOptionsWithJsonText(`42`, "tsconfig.json", { + compilerOptions: {}, + errors: [{ + ...ts.Diagnostics.The_root_value_of_a_0_file_must_be_an_object, + messageText: "The root value of a 'tsconfig.json' file must be an object.", + file: undefined, + start: 0, + length: 0 + }] }); + }); - it("Allow trailing comments", () => { - assertCompilerOptionsWithJsonText(`{} // no options`, "tsconfig.json", { - compilerOptions: {}, - errors: [] - }); + it("Allow trailing comments", () => { + assertCompilerOptionsWithJsonText(`{} // no options`, "tsconfig.json", { + compilerOptions: {}, + errors: [] }); }); +}); } diff --git a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index e12fa1e69201c..9e70d757ea1e7 100644 --- a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -1,207 +1,207 @@ namespace ts { - interface ExpectedResult { - typeAcquisition: ts.TypeAcquisition; - errors: ts.Diagnostic[]; +interface ExpectedResult { + typeAcquisition: ts.TypeAcquisition; + errors: ts.Diagnostic[]; +} +describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { + function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { + assertTypeAcquisitionWithJson(json, configFileName, expectedResult); + assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); } - describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { - function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { - assertTypeAcquisitionWithJson(json, configFileName, expectedResult); - assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); - } - function verifyAcquisition(actualTypeAcquisition: ts.TypeAcquisition | undefined, expectedResult: ExpectedResult) { - const parsedTypeAcquisition = JSON.stringify(actualTypeAcquisition); - const expectedTypeAcquisition = JSON.stringify(expectedResult.typeAcquisition); - assert.equal(parsedTypeAcquisition, expectedTypeAcquisition); - } + function verifyAcquisition(actualTypeAcquisition: ts.TypeAcquisition | undefined, expectedResult: ExpectedResult) { + const parsedTypeAcquisition = JSON.stringify(actualTypeAcquisition); + const expectedTypeAcquisition = JSON.stringify(expectedResult.typeAcquisition); + assert.equal(parsedTypeAcquisition, expectedTypeAcquisition); + } - function verifyErrors(actualErrors: ts.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 verifyErrors(actualErrors: ts.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 } = ts.convertTypeAcquisitionFromJson(jsonOptions, "/apath/", configFileName); - verifyAcquisition(actualTypeAcquisition, expectedResult); - verifyErrors(actualErrors, expectedResult); - } + function assertTypeAcquisitionWithJson(json: any, configFileName: string, expectedResult: ExpectedResult) { + const jsonOptions = json.typeAcquisition || json.typingOptions; + const { options: actualTypeAcquisition, errors: actualErrors } = ts.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 = ts.parseJsonText(configFileName, fileText); - assert(!result.parseDiagnostics.length); - assert(!!result.endOfFileToken); - const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); - const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = ts.parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); - verifyAcquisition(actualTypeAcquisition, expectedResult); + function assertTypeAcquisitionWithJsonNode(json: any, configFileName: string, expectedResult: ExpectedResult) { + const fileText = JSON.stringify(json); + const result = ts.parseJsonText(configFileName, fileText); + assert(!result.parseDiagnostics.length); + assert(!!result.endOfFileToken); + const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); + const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = ts.parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); + verifyAcquisition(actualTypeAcquisition, expectedResult); - const actualErrors = ts.filter(actualParseErrors, error => error.code !== ts.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); - } + const actualErrors = ts.filter(actualParseErrors, error => error.code !== ts.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: [] as ts.Diagnostic[] - }); + // 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: [] as ts.Diagnostic[] }); + }); - 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: [] as ts.Diagnostic[] - }); - }); + 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: [] as ts.Diagnostic[] + }); + }); - it("Convert incorrect format tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({ - typeAcquisition: { - enableAutoDiscovy: true, + it("Convert incorrect format tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enableAutoDiscovy: true, + } + }, "tsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: [ + { + category: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, + file: undefined, + start: 0, + length: 0, + messageText: undefined!, // TODO: GH#18217 } - }, "tsconfig.json", { - typeAcquisition: { - enable: false, - include: [], - exclude: [] - }, - errors: [ - { - category: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, - code: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, - file: undefined, - start: 0, - length: 0, - messageText: undefined!, // TODO: GH#18217 - } - ] - }); - }); + ] + }); + }); - it("Convert default tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({}, "tsconfig.json", { - typeAcquisition: { - enable: false, - include: [], - exclude: [] - }, - errors: [] as ts.Diagnostic[] - }); - }); + it("Convert default tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({}, "tsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: [] as ts.Diagnostic[] + }); + }); - it("Convert tsconfig.json with only enable property to typeAcquisition ", () => { - assertTypeAcquisition({ - typeAcquisition: { - enable: true - } - }, "tsconfig.json", { - typeAcquisition: { - enable: true, - include: [], - exclude: [] - }, - errors: [] as ts.Diagnostic[] - }); - }); + it("Convert tsconfig.json with only enable property to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: true + } + }, "tsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: [] as ts.Diagnostic[] + }); + }); - // 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: [] as ts.Diagnostic[] - }); - }); + // 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: [] as ts.Diagnostic[] + }); + }); - it("Convert default jsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({}, "jsconfig.json", { - typeAcquisition: { - enable: true, - include: [], - exclude: [] - }, - errors: [] as ts.Diagnostic[] - }); - }); + it("Convert default jsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({}, "jsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: [] as ts.Diagnostic[] + }); + }); - it("Convert incorrect format jsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({ - typeAcquisition: { - enableAutoDiscovy: true, + it("Convert incorrect format jsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enableAutoDiscovy: true, + } + }, "jsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: [ + { + category: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, + file: undefined, + start: 0, + length: 0, + messageText: undefined!, // TODO: GH#18217 } - }, "jsconfig.json", { - typeAcquisition: { - enable: true, - include: [], - exclude: [] - }, - errors: [ - { - category: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, - code: ts.Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, - file: undefined, - start: 0, - length: 0, - messageText: undefined!, // TODO: GH#18217 - } - ] - }); - }); + ] + }); + }); - it("Convert jsconfig.json with only enable property to typeAcquisition ", () => { - assertTypeAcquisition({ - typeAcquisition: { - enable: false - } - }, "jsconfig.json", { - typeAcquisition: { - enable: false, - include: [], - exclude: [] - }, - errors: [] as ts.Diagnostic[] - }); - }); + it("Convert jsconfig.json with only enable property to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: false + } + }, "jsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: [] as ts.Diagnostic[] + }); }); +}); } diff --git a/src/testRunner/unittests/config/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts index 21427969ffcc1..f702be89ccf55 100644 --- a/src/testRunner/unittests/config/initializeTSConfig.ts +++ b/src/testRunner/unittests/config/initializeTSConfig.ts @@ -1,33 +1,33 @@ namespace ts { - describe("unittests:: config:: initTSConfig", () => { - function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { - describe(name, () => { - const commandLine = ts.parseCommandLine(commandLinesArgs); - const initResult = ts.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); - }); +describe("unittests:: config:: initTSConfig", () => { + function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { + describe(name, () => { + const commandLine = ts.parseCommandLine(commandLinesArgs); + const initResult = ts.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); }); - } + }); + } - initTSConfigCorrectly("Default initialized TSConfig", ["--init"]); + initTSConfigCorrectly("Default initialized TSConfig", ["--init"]); - initTSConfigCorrectly("Initialized TSConfig with files options", ["--init", "file0.st", "file1.ts", "file2.ts"]); + 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 boolean value compiler options", ["--init", "--noUnusedLocals"]); - initTSConfigCorrectly("Initialized TSConfig with enum value compiler options", ["--init", "--target", "es5", "--jsx", "react"]); + 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", ["--init", "--types", "jquery,mocha"]); - initTSConfigCorrectly("Initialized TSConfig with list compiler options with enum value", ["--init", "--lib", "es5,es2015.core"]); + 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", ["--init", "--someNonExistOption"]); - initTSConfigCorrectly("Initialized TSConfig with incorrect compiler option value", ["--init", "--lib", "nonExistLib,es5,es2015.promise"]); + 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("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 296a9f2ac7944..0c8639130bf76 100644 --- a/src/testRunner/unittests/config/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -1,481 +1,669 @@ 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: { - "c:/dev/a.ts": "", - "c:/dev/a.d.ts": "", - "c:/dev/a.js": "", - "c:/dev/b.ts": "", - "c:/dev/b.js": "", - "c:/dev/c.d.ts": "", - "c:/dev/z/a.ts": "", - "c:/dev/z/abz.ts": "", - "c:/dev/z/aba.ts": "", - "c:/dev/z/b.ts": "", - "c:/dev/z/bbz.ts": "", - "c:/dev/z/bba.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": "", - "c:/dev/js/a.js": "", - "c:/dev/js/b.js": "", - "c:/dev/js/d.min.js": "", - "c:/dev/js/ab.min.js": "", - "c:/ext/ext.ts": "", - "c:/ext/b/a..b.ts": "", - }})); +const caseInsensitiveBasePath = "c:/dev/"; +const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json"; +const caseInsensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + "c:/dev/a.ts": "", + "c:/dev/a.d.ts": "", + "c:/dev/a.js": "", + "c:/dev/b.ts": "", + "c:/dev/b.js": "", + "c:/dev/c.d.ts": "", + "c:/dev/z/a.ts": "", + "c:/dev/z/abz.ts": "", + "c:/dev/z/aba.ts": "", + "c:/dev/z/b.ts": "", + "c:/dev/z/bbz.ts": "", + "c:/dev/z/bba.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": "", + "c:/dev/js/a.js": "", + "c:/dev/js/b.js": "", + "c:/dev/js/d.min.js": "", + "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: { - "/dev/a.ts": "", - "/dev/a.d.ts": "", - "/dev/a.js": "", - "/dev/b.ts": "", - "/dev/b.js": "", - "/dev/A.ts": "", - "/dev/B.ts": "", - "/dev/c.d.ts": "", - "/dev/z/a.ts": "", - "/dev/z/abz.ts": "", - "/dev/z/aba.ts": "", - "/dev/z/b.ts": "", - "/dev/z/bbz.ts": "", - "/dev/z/bba.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": "", - "/dev/js/a.js": "", - "/dev/js/b.js": "", - }})); +const caseSensitiveBasePath = "/dev/"; +const caseSensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { + "/dev/a.ts": "", + "/dev/a.d.ts": "", + "/dev/a.js": "", + "/dev/b.ts": "", + "/dev/b.js": "", + "/dev/A.ts": "", + "/dev/B.ts": "", + "/dev/c.d.ts": "", + "/dev/z/a.ts": "", + "/dev/z/abz.ts": "", + "/dev/z/aba.ts": "", + "/dev/z/b.ts": "", + "/dev/z/bbz.ts": "", + "/dev/z/bba.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": "", + "/dev/js/a.js": "", + "/dev/js/b.js": "", +}})); - const caseInsensitiveMixedExtensionHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { - "c:/dev/a.ts": "", - "c:/dev/a.d.ts": "", - "c:/dev/a.js": "", - "c:/dev/b.tsx": "", - "c:/dev/b.d.ts": "", - "c:/dev/b.jsx": "", - "c:/dev/c.tsx": "", - "c:/dev/c.js": "", - "c:/dev/d.js": "", - "c:/dev/e.jsx": "", - "c:/dev/f.other": "", - }})); +const caseInsensitiveMixedExtensionHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + "c:/dev/a.ts": "", + "c:/dev/a.d.ts": "", + "c:/dev/a.js": "", + "c:/dev/b.tsx": "", + "c:/dev/b.d.ts": "", + "c:/dev/b.jsx": "", + "c:/dev/c.tsx": "", + "c:/dev/c.js": "", + "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: { - "c:/dev/a.ts": "", - "c:/dev/a.d.ts": "", - "c:/dev/a.js": "", - "c:/dev/b.ts": "", - "c:/dev/x/a.ts": "", - "c:/dev/node_modules/a.ts": "", - "c:/dev/bower_components/a.ts": "", - "c:/dev/jspm_packages/a.ts": "", - }})); +const caseInsensitiveCommonFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + "c:/dev/a.ts": "", + "c:/dev/a.d.ts": "", + "c:/dev/a.js": "", + "c:/dev/b.ts": "", + "c:/dev/x/a.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: { - "c:/dev/x/d.ts": "", - "c:/dev/x/y/d.ts": "", - "c:/dev/x/y/.e.ts": "", - "c:/dev/x/.y/a.ts": "", - "c:/dev/.z/.b.ts": "", - "c:/dev/.z/c.ts": "", - "c:/dev/w/.u/e.ts": "", - "c:/dev/g.min.js/.g/g.ts": "", - }})); +const caseInsensitiveDottedFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + "c:/dev/x/d.ts": "", + "c:/dev/x/y/d.ts": "", + "c:/dev/x/y/.e.ts": "", + "c:/dev/x/.y/a.ts": "", + "c:/dev/.z/.b.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: { - "c:/dev/xylophone.ts": "", - "c:/dev/Yosemite.ts": "", - "c:/dev/zebra.ts": "", - }})); +const caseInsensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.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: { - "/dev/xylophone.ts": "", - "/dev/Yosemite.ts": "", - "/dev/zebra.ts": "", - }})); +const caseSensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { + "/dev/xylophone.ts": "", + "/dev/Yosemite.ts": "", + "/dev/zebra.ts": "", +}})); - function assertParsed(actual: ts.ParsedCommandLine, expected: ts.ParsedCommandLine): void { - assert.deepEqual(actual.fileNames, expected.fileNames); - assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); - assert.deepEqual(actual.errors, expected.errors); - } +function assertParsed(actual: ts.ParsedCommandLine, expected: ts.ParsedCommandLine): void { + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + assert.deepEqual(actual.errors, expected.errors); +} - function validateMatches(expected: ts.ParsedCommandLine, json: any, host: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[]) { - { - const jsonText = JSON.stringify(json); - const result = ts.parseJsonText(caseInsensitiveTsconfigPath, jsonText); - const actual = ts.parseJsonSourceFileConfigFileContent(result, host, basePath, existingOptions, configFileName, resolutionStack); - for (const error of expected.errors) { - if (error.file) { - error.file = result; - } +function validateMatches(expected: ts.ParsedCommandLine, json: any, host: ts.ParseConfigHost, basePath: string, existingOptions?: ts.CompilerOptions, configFileName?: string, resolutionStack?: ts.Path[]) { + { + const jsonText = JSON.stringify(json); + const result = ts.parseJsonText(caseInsensitiveTsconfigPath, jsonText); + const actual = ts.parseJsonSourceFileConfigFileContent(result, host, basePath, existingOptions, configFileName, resolutionStack); + for (const error of expected.errors) { + if (error.file) { + error.file = result; } - assertParsed(actual, expected); - } - { - const actual = ts.parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack); - expected.errors = expected.errors.map((error): ts.Diagnostic => ({ - category: error.category, - code: error.code, - file: undefined, - length: undefined, - messageText: error.messageText, - start: undefined, - reportsUnnecessary: undefined, - reportsDeprecated: undefined - })); - assertParsed(actual, expected); } + assertParsed(actual, expected); } - - function createDiagnosticForConfigFile(json: any, start: number, length: number, diagnosticMessage: ts.DiagnosticMessage, arg0: string) { - const text = JSON.stringify(json); - const file = { - fileName: caseInsensitiveTsconfigPath, - kind: ts.SyntaxKind.SourceFile, - text - } as ts.SourceFile; - return ts.createFileDiagnostic(file, start, length, diagnosticMessage, arg0); + { + const actual = ts.parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack); + expected.errors = expected.errors.map((error): ts.Diagnostic => ({ + category: error.category, + code: error.code, + file: undefined, + length: undefined, + messageText: error.messageText, + start: undefined, + reportsUnnecessary: undefined, + reportsDeprecated: undefined + })); + assertParsed(actual, expected); } +} + +function createDiagnosticForConfigFile(json: any, start: number, length: number, diagnosticMessage: ts.DiagnosticMessage, arg0: string) { + const text = JSON.stringify(json); + const file = { + fileName: caseInsensitiveTsconfigPath, + kind: ts.SyntaxKind.SourceFile, + text + } as ts.SourceFile; + return ts.createFileDiagnostic(file, start, length, diagnosticMessage, arg0); +} + +describe("unittests:: config:: matchFiles", () => { + it("with defaults", () => { + const json = {}; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/x/a.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + }); + + describe("with literal file list", () => { + it("without exclusions", () => { + const json = { + files: [ + "a.ts", + "b.ts" + ] + }; + const expected: ts.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: ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + }); - describe("unittests:: config:: matchFiles", () => { - it("with defaults", () => { - const json = {}; + describe("with literal include list", () => { + it("without exclusions", () => { + const json = { + include: [ + "a.ts", + "b.ts" + ] + }; + const expected: ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [ + ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [ + ts.createCompilerDiagnostic(ts.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: ts.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: ts.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: ts.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: ts.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: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ "c:/dev/a.ts", "c:/dev/b.ts", - "c:/dev/x/a.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: ts.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: ts.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: ts.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": ts.WatchDirectoryFlags.None, + "c:/dev/x": ts.WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("same named declarations are excluded", () => { + const json = { + include: [ + "*.ts" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("`*` matches only ts files", () => { + const json = { + include: [ + "*" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("`?` matches only a single character", () => { + const json = { + include: [ + "x/?.ts" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/a.ts", + "c:/dev/x/b.ts" + ], + wildcardDirectories: { + "c:/dev/x": ts.WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with recursive directory", () => { + const json = { + include: [ + "**/a.ts" + ] + }; + const expected: ts.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": ts.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: ts.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: ts.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: ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/y/a.ts", + "c:/dev/x/a.ts", + "c:/dev/z/a.ts" + ], + wildcardDirectories: { + "c:/dev/x": ts.WatchDirectoryFlags.Recursive, + "c:/dev/z": ts.WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - - describe("with literal include list", () => { - it("without exclusions", () => { - const json = { - include: [ - "a.ts", - "b.ts" - ] - }; - const expected: ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - ts.createCompilerDiagnostic(ts.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: ts.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: ts.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: ts.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: ts.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: ts.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: ts.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: ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "/dev/A.ts" + ], + wildcardDirectories: { + "/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); }); - - describe("with wildcard include list", () => { - it("is sorted in include order, then in alphabetical order", () => { - const json = { - include: [ - "z/*.ts", - "x/*.ts" - ] - }; - const expected: ts.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": ts.WatchDirectoryFlags.None, - "c:/dev/x": ts.WatchDirectoryFlags.None - }, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("same named declarations are excluded", () => { + it("with missing files are excluded", () => { + const json = { + include: [ + "*/z.ts" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [ + ts.createCompilerDiagnostic(ts.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": ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("exclude folders", () => { + const json = { + include: [ + "**/*" + ], + exclude: [ + "z", + "x" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + describe("with common package folders", () => { + it("and no exclusions", () => { const json = { include: [ - "*.ts" + "**/a.ts" ] }; const expected: ts.ParsedCommandLine = { @@ -483,1050 +671,862 @@ 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": ts.WatchDirectoryFlags.None + "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("`*` matches only ts files", () => { + it("and exclusions", () => { const json = { include: [ - "*" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/c.d.ts" + "**/?.ts" ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.None - }, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("`?` matches only a single character", () => { - const json = { - include: [ - "x/?.ts" + exclude: [ + "a.ts" ] }; const expected: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/x/a.ts", - "c:/dev/x/b.ts" + "c:/dev/b.ts", + "c:/dev/x/a.ts" ], wildcardDirectories: { - "c:/dev/x": ts.WatchDirectoryFlags.None + "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("with recursive directory", () => { + it("and empty exclude", () => { const json = { include: [ "**/a.ts" - ] + ], + exclude: [] as string[] }; const expected: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/x/y/a.ts", - "c:/dev/z/a.ts" + "c:/dev/x/a.ts" ], wildcardDirectories: { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("with multiple recursive directories", () => { + it("and explicit recursive include", () => { const json = { include: [ - "x/y/**/a.ts", - "x/**/a.ts", - "z/**/a.ts" + "**/a.ts", + "**/node_modules/a.ts" ] }; const expected: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/x/y/a.ts", + "c:/dev/a.ts", "c:/dev/x/a.ts", - "c:/dev/z/a.ts" - ], - wildcardDirectories: { - "c:/dev/x": ts.WatchDirectoryFlags.Recursive, - "c:/dev/z": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("case sensitive", () => { - const json = { - include: [ - "**/A.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "/dev/A.ts" - ], - wildcardDirectories: { - "/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); - }); - it("with missing files are excluded", () => { - const json = { - include: [ - "*/z.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [ - ts.createCompilerDiagnostic(ts.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/node_modules/a.ts" ], - fileNames: [], wildcardDirectories: { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("always include literal files", () => { + it("and wildcard include", () => { const json = { - files: [ - "a.ts" - ], include: [ - "*/z.ts" - ], - exclude: [ - "**/a.ts" + "*/a.ts" ] }; const expected: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/a.ts" + "c:/dev/x/a.ts" ], wildcardDirectories: { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("exclude folders", () => { + it("and explicit wildcard include", () => { const json = { include: [ - "**/*" - ], - exclude: [ - "z", - "x" + "*/a.ts", + "node_modules/a.ts" ] }; const expected: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/c.d.ts" + "c:/dev/x/a.ts", + "c:/dev/node_modules/a.ts" ], wildcardDirectories: { "c:/dev": ts.WatchDirectoryFlags.Recursive - } + }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - describe("with common package folders", () => { - it("and no exclusions", () => { - const json = { - include: [ - "**/a.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and exclusions", () => { - const json = { - include: [ - "**/?.ts" - ], - exclude: [ - "a.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/b.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and empty exclude", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [] as string[] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and explicit recursive include", () => { - const json = { - include: [ - "**/a.ts", - "**/node_modules/a.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/node_modules/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and wildcard include", () => { - const json = { - include: [ - "*/a.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and explicit wildcard include", () => { - const json = { - include: [ - "*/a.ts", - "node_modules/a.ts" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/a.ts", - "c:/dev/node_modules/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("exclude .js files when allowJs=false", () => { + }); + it("exclude .js files when allowJs=false", () => { + const json = { + compilerOptions: { + allowJs: false + }, + include: [ + "js/*" + ] + }; + const expected: ts.ParsedCommandLine = { + options: { + allowJs: false + }, + errors: [ + ts.createCompilerDiagnostic(ts.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": ts.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: ts.ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/a.js", + "c:/dev/js/b.js" + ], + wildcardDirectories: { + "c:/dev/js": ts.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: ts.ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/ab.min.js", + "c:/dev/js/d.min.js" + ], + wildcardDirectories: { + "c:/dev/js": ts.WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("include paths outside of the project", () => { + const json = { + include: [ + "*", + "c:/ext/*" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts", + "c:/ext/ext.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None, + "c:/ext": ts.WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("include paths outside of the project using relative paths", () => { + const json = { + include: [ + "*", + "../ext/*" + ], + exclude: [ + "**" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/ext/ext.ts" + ], + wildcardDirectories: { + "c:/ext": ts.WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("exclude paths outside of the project using relative paths", () => { + const json = { + include: [ + "c:/**/*" + ], + exclude: [ + "../**" + ] + }; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [ + ts.createCompilerDiagnostic(ts.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: ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/ext/ext.ts", + ], + wildcardDirectories: { + "c:/ext": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with jsx=none, allowJs=false", () => { + const json = { + compilerOptions: { + allowJs: false + } + }; + const expected: ts.ParsedCommandLine = { + options: { + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=preserve, allowJs=false", () => { + const json = { + compilerOptions: { + jsx: "preserve", + allowJs: false + } + }; + const expected: ts.ParsedCommandLine = { + options: { + jsx: ts.JsxEmit.Preserve, + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=react-native, allowJs=false", () => { + const json = { + compilerOptions: { + jsx: "react-native", + allowJs: false + } + }; + const expected: ts.ParsedCommandLine = { + options: { + jsx: ts.JsxEmit.ReactNative, + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=none, allowJs=true", () => { + const json = { + compilerOptions: { + allowJs: true + } + }; + const expected: ts.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": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=preserve, allowJs=true", () => { + const json = { + compilerOptions: { + jsx: "preserve", + allowJs: true + } + }; + const expected: ts.ParsedCommandLine = { + options: { + jsx: ts.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": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=react-native, allowJs=true", () => { + const json = { + compilerOptions: { + jsx: "react-native", + allowJs: true + } + }; + const expected: ts.ParsedCommandLine = { + options: { + jsx: ts.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": ts.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: ts.ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/d.min.js" + ], + wildcardDirectories: { + "c:/dev/js": ts.WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + + describe("with trailing recursive directory", () => { + it("in includes", () => { const json = { - compilerOptions: { - allowJs: false - }, include: [ - "js/*" + "**" ] }; const expected: ts.ParsedCommandLine = { - options: { - allowJs: false - }, + options: {}, errors: [ + createDiagnosticForConfigFile(json, 12, 4, ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), ts.createCompilerDiagnostic(ts.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": ts.WatchDirectoryFlags.None - } + wildcardDirectories: {} }; validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("include .js files when allowJs=true", () => { + it("in excludes", () => { const json = { - compilerOptions: { - allowJs: true - }, include: [ - "js/*" - ] - }; - const expected: ts.ParsedCommandLine = { - options: { - allowJs: true - }, - errors: [], - fileNames: [ - "c:/dev/js/a.js", - "c:/dev/js/b.js" + "**/*" ], - wildcardDirectories: { - "c:/dev/js": ts.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" + exclude: [ + "**" ] }; const expected: ts.ParsedCommandLine = { - options: { - allowJs: true - }, - errors: [], - fileNames: [ - "c:/dev/js/ab.min.js", - "c:/dev/js/d.min.js" + options: {}, + errors: [ + ts.createCompilerDiagnostic(ts.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: { - "c:/dev/js": ts.WatchDirectoryFlags.None - } + fileNames: [], + wildcardDirectories: {} }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("include paths outside of the project", () => { + }); + describe("with multiple recursive directory patterns", () => { + it("in includes", () => { const json = { include: [ - "*", - "c:/ext/*" + "**/x/**/*" ] }; const expected: ts.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": ts.WatchDirectoryFlags.None, - "c:/ext": ts.WatchDirectoryFlags.None + "c:/dev": ts.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: ts.ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/ext/ext.ts" + "c:/dev/a.ts", + "c:/dev/z/a.ts" ], wildcardDirectories: { - "c:/ext": ts.WatchDirectoryFlags.None + "c:/dev": ts.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: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.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, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), + ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/ext/b/a..b.ts" + errors: [ + createDiagnosticForConfigFile(json, 12, 11, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), + ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/ext/ext.ts", - ], - wildcardDirectories: { - "c:/ext": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with jsx=none, allowJs=false", () => { - const json = { - compilerOptions: { - allowJs: false - } - }; - const expected: ts.ParsedCommandLine = { - options: { - allowJs: false - }, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", + errors: [ + createDiagnosticForConfigFile(json, 34, 7, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..") ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=preserve, allowJs=false", () => { - const json = { - compilerOptions: { - jsx: "preserve", - allowJs: false - } - }; - const expected: ts.ParsedCommandLine = { - options: { - jsx: ts.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": ts.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: ts.ParsedCommandLine = { - options: { - jsx: ts.JsxEmit.ReactNative, - allowJs: false - }, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", + include: [ + "**/a.ts" ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=none, allowJs=true", () => { - const json = { - compilerOptions: { - allowJs: true - } + exclude: [ + "**/y/.." + ] }; const expected: ts.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, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..") ], - wildcardDirectories: { - "c:/dev": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=preserve, allowJs=true", () => { - const json = { - compilerOptions: { - jsx: "preserve", - allowJs: true - } - }; - const expected: ts.ParsedCommandLine = { - options: { - jsx: ts.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": ts.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: ts.ParsedCommandLine = { - options: { - jsx: ts.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": ts.WatchDirectoryFlags.Recursive + "c:/dev/z": ts.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: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/d.ts", + "c:/dev/x/y/d.ts", + ], + wildcardDirectories: { + "c:/dev/x": ts.WatchDirectoryFlags.Recursive, + "c:/dev/w": ts.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: ts.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": ts.WatchDirectoryFlags.None - } + wildcardDirectories: {} }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - - describe("with trailing recursive directory", () => { - it("in includes", () => { - const json = { - include: [ - "**" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 4, ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), - ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - ts.createCompilerDiagnostic(ts.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: ts.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": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - it("in excludes", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [ - "**/x/**" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/z/a.ts" - ], - wildcardDirectories: { - "c:/dev": ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 9, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), - ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 11, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), - ts.createCompilerDiagnostic(ts.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: ts.ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 34, 7, ts.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": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - - it("in excludes after a subdirectory", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [ - "**/y/.." - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 34, 9, ts.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": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - }); - - describe("with implicit globbification", () => { - it("Expands 'z' to 'z/**/*'", () => { - const json = { - include: ["z"] - }; - const expected: ts.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": ts.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: ts.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": ts.WatchDirectoryFlags.Recursive, - "c:/dev/w": ts.WatchDirectoryFlags.Recursive + "c:/dev": ts.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: ts.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: ts.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": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - }); - it("with recursive wildcards that match nothing", () => { - const json = { - include: [ - "x/**/.y/*", - ".z/**/.*" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/.y/a.ts", - "c:/dev/.z/.b.ts" - ], - wildcardDirectories: { - "c:/dev/.z": ts.WatchDirectoryFlags.Recursive, - "c:/dev/x": ts.WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - }); - it("with wildcard excludes that implicitly exclude dotted files", () => { - const json = { - include: [ - "**/.*/*" - ], - exclude: [ - "**/*" - ] - }; - const expected: ts.ParsedCommandLine = { - options: {}, - errors: [ - ts.createCompilerDiagnostic(ts.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: ts.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": ts.WatchDirectoryFlags.Recursive + "c:/dev/.z": ts.WatchDirectoryFlags.Recursive, + "c:/dev/x": ts.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: ts.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: [ + ts.createCompilerDiagnostic(ts.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": ts.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): ts.ParsedCommandLine { - return { - options: {}, - errors: [], - fileNames: [ - `${basePath}Yosemite.ts`, - `${basePath}xylophone.ts`, - `${basePath}zebra.ts` - ], - wildcardDirectories: { - [basePath.slice(0, basePath.length - 1)]: ts.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: ts.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": ts.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: ts.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": ts.WatchDirectoryFlags.Recursive + "/dev": ts.WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + }); + }); + + it("can include files in the same order on multiple platforms", () => { + function getExpected(basePath: string): ts.ParsedCommandLine { + return { + options: {}, + errors: [], + fileNames: [ + `${basePath}Yosemite.ts`, + `${basePath}xylophone.ts`, + `${basePath}zebra.ts` + ], + wildcardDirectories: { + [basePath.slice(0, basePath.length - 1)]: ts.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 vfs.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 fakes.ParseConfigHost(fs); + const json = {}; + const expected: ts.ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/index.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, host, caseInsensitiveBasePath); }); +}); } diff --git a/src/testRunner/unittests/config/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts index 41389fb5d8c0a..acdd7bff25e25 100644 --- a/src/testRunner/unittests/config/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -1,356 +1,356 @@ namespace ts { - interface TestProjectSpecification { - configFileName?: string; - references?: readonly (string | ts.ProjectReference)[]; - files: { - [fileName: string]: string; - }; - outputFiles?: { - [fileName: string]: string; - }; - config?: object; - options?: Partial; - } - interface TestSpecification { - [path: string]: TestProjectSpecification; - } +interface TestProjectSpecification { + configFileName?: string; + references?: readonly (string | ts.ProjectReference)[]; + files: { + [fileName: string]: string; + }; + outputFiles?: { + [fileName: string]: string; + }; + config?: object; + options?: Partial; +} +interface TestSpecification { + [path: string]: TestProjectSpecification; +} - function assertHasError(message: string, errors: readonly ts.Diagnostic[], diag: ts.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 assertHasError(message: string, errors: readonly ts.Diagnostic[], diag: ts.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 assertNoErrors(message: string, errors: readonly ts.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 assertNoErrors(message: string, errors: readonly ts.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 combineAllPaths(...paths: string[]) { - let result = paths[0]; - for (let i = 1; i < paths.length; i++) { - result = ts.combinePaths(result, paths[i]); - } - return result; +function combineAllPaths(...paths: string[]) { + let result = paths[0]; + for (let i = 1; i < paths.length; i++) { + result = ts.combinePaths(result, paths[i]); } + return result; +} - const emptyModule = "export { };"; +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"); - } +/** + * 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: ts.Program, host: fakes.CompilerHost) => void) { - const files = new ts.Map(); - 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]); +function testProjectReferences(spec: TestSpecification, entryPointConfigFileName: string, checkResult: (prog: ts.Program, host: fakes.CompilerHost) => void) { + const files = new ts.Map(); + 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 vfs.FileSystem(false, { files: { "/lib.d.ts": ts.TestFSWithWatch.libFile.content } }); - files.forEach((v, k) => { - vfsys.mkdirpSync(ts.getDirectoryPath(k)); - vfsys.writeFileSync(k, v); - }); - const host = new fakes.CompilerHost(new fakes.System(vfsys)); + const vfsys = new vfs.FileSystem(false, { files: { "/lib.d.ts": ts.TestFSWithWatch.libFile.content } }); + files.forEach((v, k) => { + vfsys.mkdirpSync(ts.getDirectoryPath(k)); + vfsys.writeFileSync(k, v); + }); + const host = new fakes.CompilerHost(new fakes.System(vfsys)); - const { config, error } = ts.readConfigFile(entryPointConfigFileName, name => host.readFile(name)); + const { config, error } = ts.readConfigFile(entryPointConfigFileName, name => host.readFile(name)); - // We shouldn't have any errors about invalid tsconfig files in these tests - assert(config && !error, ts.flattenDiagnosticMessageText(error && error.messageText, "\n")); - const file = ts.parseJsonConfigFileContent(config, ts.parseConfigHostFromCompilerHostLike(host), ts.getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName); - file.options.configFilePath = entryPointConfigFileName; - const prog = ts.createProgram({ - rootNames: file.fileNames, - options: file.options, - host, - projectReferences: file.projectReferences - }); - checkResult(prog, host); - } + // We shouldn't have any errors about invalid tsconfig files in these tests + assert(config && !error, ts.flattenDiagnosticMessageText(error && error.messageText, "\n")); + const file = ts.parseJsonConfigFileContent(config, ts.parseConfigHostFromCompilerHostLike(host), ts.getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName); + file.options.configFilePath = entryPointConfigFileName; + const prog = ts.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"] - } - }; - testProjectReferences(spec, "/primary/tsconfig.json", prog => { - assert.isTrue(!!prog, "Program should exist"); - assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics()); - }); +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"] + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", prog => { + assert.isTrue(!!prog, "Program should exist"); + assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics()); }); }); +}); - /** - * 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 - } +/** + * 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", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about the wrong decl setting", errs, ts.Diagnostics.Composite_projects_may_not_disable_declaration_emit); - }); + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about the wrong decl setting", errs, ts.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"] - } + 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, "/reference/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about 'composite' not being set", errs, ts.Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true); - }); + }, + "/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, ts.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, ts.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, ts.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, ts.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, ts.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, ts.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, ts.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, ts.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, ts.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(), ts.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(), ts.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(), ts.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(), ts.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(), ts.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(), ts.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, ts.Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files); - assertHasError("Issues an error about the fileList", semanticDiagnostics, ts.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, ts.Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files); + assertHasError("Issues an error about the fileList", semanticDiagnostics, ts.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 ac831468fd88b..ee0fd062e802f 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -1,193 +1,193 @@ 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 = ts.combinePaths(cwd, "tsconfig.json"); - const configContents = configJson ? JSON.stringify(configJson) : undefined; - const configParseHost: ts.ParseConfigFileHost = { - fileExists: path => ts.comparePaths(ts.getNormalizedAbsolutePath(path, cwd), configPath) === ts.Comparison.EqualTo ? true : false, - getCurrentDirectory() { return cwd; }, - useCaseSensitiveFileNames: true, - onUnRecoverableConfigFileDiagnostic: d => { - throw new Error(ts.flattenDiagnosticMessageText(d.messageText, "\n")); - }, - readDirectory() { return []; }, - readFile: path => ts.comparePaths(ts.getNormalizedAbsolutePath(path, cwd), configPath) === ts.Comparison.EqualTo ? configContents : undefined, - }; - let commandLine = ts.parseCommandLine(commandLinesArgs); - if (commandLine.options.project) { - const result = ts.getParsedCommandLineOfConfigFile(commandLine.options.project, commandLine.options, configParseHost); - if (result) { - commandLine = result; - } +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 = ts.combinePaths(cwd, "tsconfig.json"); + const configContents = configJson ? JSON.stringify(configJson) : undefined; + const configParseHost: ts.ParseConfigFileHost = { + fileExists: path => ts.comparePaths(ts.getNormalizedAbsolutePath(path, cwd), configPath) === ts.Comparison.EqualTo ? true : false, + getCurrentDirectory() { return cwd; }, + useCaseSensitiveFileNames: true, + onUnRecoverableConfigFileDiagnostic: d => { + throw new Error(ts.flattenDiagnosticMessageText(d.messageText, "\n")); + }, + readDirectory() { return []; }, + readFile: path => ts.comparePaths(ts.getNormalizedAbsolutePath(path, cwd), configPath) === ts.Comparison.EqualTo ? configContents : undefined, + }; + let commandLine = ts.parseCommandLine(commandLinesArgs); + if (commandLine.options.project) { + const result = ts.getParsedCommandLineOfConfigFile(commandLine.options.project, commandLine.options, configParseHost); + if (result) { + commandLine = result; } - const initResult = ts.convertToTSConfig(commandLine, configPath, configParseHost); + } + const initResult = ts.convertToTSConfig(commandLine, configPath, configParseHost); - // eslint-disable-next-line no-null/no-null - Harness.Baseline.runBaseline(outputFileName, JSON.stringify(initResult, null, 4) + "\n"); - }); + // eslint-disable-next-line no-null/no-null + Harness.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("Default initialized TSConfig", ["--showConfig"]); - showTSConfigCorrectly("Show TSConfig with boolean value compiler options", ["--showConfig", "--noUnusedLocals"]); + showTSConfigCorrectly("Show TSConfig with files options", ["--showConfig", "file0.ts", "file1.ts", "file2.ts"]); - showTSConfigCorrectly("Show TSConfig with enum value compiler options", ["--showConfig", "--target", "es5", "--jsx", "react"]); + showTSConfigCorrectly("Show TSConfig with boolean value compiler options", ["--showConfig", "--noUnusedLocals"]); - showTSConfigCorrectly("Show TSConfig with list compiler options", ["--showConfig", "--types", "jquery,mocha"]); + showTSConfigCorrectly("Show TSConfig with enum value compiler options", ["--showConfig", "--target", "es5", "--jsx", "react"]); - showTSConfigCorrectly("Show TSConfig with list compiler options with enum value", ["--showConfig", "--lib", "es5,es2015.core"]); + showTSConfigCorrectly("Show TSConfig with list compiler options", ["--showConfig", "--types", "jquery,mocha"]); - showTSConfigCorrectly("Show TSConfig with incorrect compiler option", ["--showConfig", "--someNonExistOption"]); + showTSConfigCorrectly("Show TSConfig with list compiler options with enum value", ["--showConfig", "--lib", "es5,es2015.core"]); - showTSConfigCorrectly("Show TSConfig with incorrect compiler option value", ["--showConfig", "--lib", "nonExistLib,es5,es2015.promise"]); + showTSConfigCorrectly("Show TSConfig with incorrect compiler option", ["--showConfig", "--someNonExistOption"]); - showTSConfigCorrectly("Show TSConfig with advanced options", ["--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); + showTSConfigCorrectly("Show TSConfig with incorrect compiler option value", ["--showConfig", "--lib", "nonExistLib,es5,es2015.promise"]); - 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" } - ], - }); + showTSConfigCorrectly("Show TSConfig with advanced options", ["--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); - // 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 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" } + ], + }); - showTSConfigCorrectly("Show TSConfig with watch options", ["-p", "tsconfig.json"], { - watchOptions: { - watchFile: "DynamicPriorityPolling" + // 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 ts.optionDeclarations) { - baselineOption(option, /*isCompilerOptions*/ true); - } + experimentalDecorators: true, + emitDecoratorMetadata: true, + resolveJsonModule: true + }, + include: [ + "./src/**/*" + ] + }); - for (const option of ts.optionsForWatch) { - baselineOption(option, /*isCompilerOptions*/ false); - } + showTSConfigCorrectly("Show TSConfig with watch options", ["-p", "tsconfig.json"], { + watchOptions: { + watchFile: "DynamicPriorityPolling" + }, + include: [ + "./src/**/*" + ] + }); - function baselineOption(option: ts.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; + // Bulk validation of all option declarations + for (const option of ts.optionDeclarations) { + baselineOption(option, /*isCompilerOptions*/ true); + } + + for (const option of ts.optionsForWatch) { + baselineOption(option, /*isCompilerOptions*/ false); + } + + function baselineOption(option: ts.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 ts.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 ts.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 1e8e3d33b4c61..1ececdafde28f 100644 --- a/src/testRunner/unittests/config/tsconfigParsing.ts +++ b/src/testRunner/unittests/config/tsconfigParsing.ts @@ -1,105 +1,105 @@ namespace ts { - describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { - function assertParseResult(jsonText: string, expectedConfigObject: { - config?: any; - error?: ts.Diagnostic[]; - }) { +describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { + function assertParseResult(jsonText: string, expectedConfigObject: { + config?: any; + error?: ts.Diagnostic[]; + }) { + const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); + assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); + } + + function assertParseErrorWithExcludesKeyword(jsonText: string) { + { const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); + const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); } - - function assertParseErrorWithExcludesKeyword(jsonText: string) { - { - const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "tests/cases/unittests"); - assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && - parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); - } - { - const parsed = ts.parseJsonText("/apath/tsconfig.json", jsonText); - const parsedCommand = ts.parseJsonSourceFileConfigFileContent(parsed, ts.sys, "tests/cases/unittests"); - assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && - parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); - } + { + const parsed = ts.parseJsonText("/apath/tsconfig.json", jsonText); + const parsedCommand = ts.parseJsonSourceFileConfigFileContent(parsed, ts.sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); } - - function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { - const parsed = ts.parseConfigFileTextToJson(configFileName, jsonText); - const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); - const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); - return ts.parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName); + } + + function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = ts.parseConfigFileTextToJson(configFileName, jsonText); + const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); + const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); + return ts.parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName); + } + + function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = ts.parseJsonText(configFileName, jsonText); + const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); + const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); + return ts.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(ts.arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); } - - function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { - const parsed = ts.parseJsonText(configFileName, jsonText); - const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); - const host: ts.ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); - return ts.parseJsonSourceFileConfigFileContent(parsed, host, basePath, /*existingOptions*/ undefined, configFileName); + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(ts.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(ts.arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); - } - { - const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); - assert.isTrue(ts.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)}`); + } + { + 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 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 for file with comments only", () => { + assertParseResult("// Comment", { config: {} }); + assertParseResult("/* Comment*/", { config: {} }); + }); - it("returns empty config when config is empty object", () => { - assertParseResult("{}", { config: {} }); - }); + it("returns empty config when config is empty object", () => { + assertParseResult("{}", { config: {} }); + }); - it("returns config object without comments", () => { - assertParseResult(`{ // Excluded files + 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 */ @@ -107,67 +107,67 @@ 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 = ts.parseConfigFileTextToJson("/apath/tsconfig.json", "invalid"); - assert.deepEqual(parsed.config, {}); - const expected = ts.createCompilerDiagnostic(ts.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 = ts.parseConfigFileTextToJson("/apath/tsconfig.json", "invalid"); + assert.deepEqual(parsed.config, {}); + const expected = ts.createCompilerDiagnostic(ts.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"] } } - }); + config: { compilerOptions: { lib: ["es5"] } } + }); - assertParseResult(`{ + 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"] }, @@ -175,64 +175,64 @@ 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("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("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 = `{ + 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"]; + 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); - }); + 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("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 = `{ + it("parse and re-emit tsconfig.json file with diagnostics", () => { + const content = `{ "compilerOptions": { "allowJs": true // Some comments @@ -240,120 +240,120 @@ namespace ts { } "files": ["file1.ts"] }`; - const result = ts.parseJsonText("config.json", content); - const diagnostics = result.parseDiagnostics; - const configJsonObject = ts.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 = ts.parseJsonText("config.json", content); + const diagnostics = result.parseDiagnostics; + const configJsonObject = ts.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"], ts.Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], 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 = `{ + 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"], ts.Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], 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 = `{ + 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"], ts.Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); + assertParseFileDiagnosticsExclusion(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.The_files_list_in_config_file_0_is_empty.code); + }); - it("generates errors for directory with no .ts files", () => { - const content = `{ + it("generates errors for directory with no .ts files", () => { + const content = `{ }`; - assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.js"], ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.js"], 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 empty directory", () => { - const content = `{ + it("generates errors for empty directory", () => { + const content = `{ "compilerOptions": { "allowJs": true } }`; - assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", [], ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", [], 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 empty include", () => { - const content = `{ + it("generates errors for empty include", () => { + const content = `{ "include": [] }`; - assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], 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 = `{ + it("generates errors for includes with outDir", () => { + const content = `{ "compilerOptions": { "outDir": "./" }, "include": ["**/*"] }`; - assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], 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 = `{ + 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); - }); - - it("generates errors when files is not string", () => { - assertParseFileDiagnostics(JSON.stringify({ - files: [{ - compilerOptions: { - experimentalDecorators: true, - allowJs: true - } - }] - }), "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code, - /*noLocation*/ true); - }); - - it("generates errors when include is not string", () => { - assertParseFileDiagnostics(JSON.stringify({ - include: [ - ["./**/*.ts"] - ] - }), "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code, - /*noLocation*/ true); - }); - - it("parses wildcard directories even when parent directories have dots", () => { - const parsed = ts.parseConfigFileTextToJson("/foo.bar/tsconfig.json", JSON.stringify({ - include: ["src"] - })); - - const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "/foo.bar"); - assert.deepEqual(parsedCommand.wildcardDirectories, { "/foo.bar/src": ts.WatchDirectoryFlags.Recursive }); - }); + const parsed = getParsedCommandJsonNode(jsonText, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"]); + assert.isTrue(parsed.errors.length >= 0); + }); + + it("generates errors when files is not string", () => { + assertParseFileDiagnostics(JSON.stringify({ + files: [{ + compilerOptions: { + experimentalDecorators: true, + allowJs: true + } + }] + }), "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code, + /*noLocation*/ true); + }); + + it("generates errors when include is not string", () => { + assertParseFileDiagnostics(JSON.stringify({ + include: [ + ["./**/*.ts"] + ] + }), "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], ts.Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code, + /*noLocation*/ true); + }); + + it("parses wildcard directories even when parent directories have dots", () => { + const parsed = ts.parseConfigFileTextToJson("/foo.bar/tsconfig.json", JSON.stringify({ + include: ["src"] + })); + + const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "/foo.bar"); + assert.deepEqual(parsedCommand.wildcardDirectories, { "/foo.bar/src": ts.WatchDirectoryFlags.Recursive }); }); +}); } diff --git a/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts index 3fa639b6fca10..7cbede14da9e2 100644 --- a/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts +++ b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts @@ -1,214 +1,214 @@ 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?: ts.WatchOptions) { - return ts.parseJsonConfigFileContent(json, createParseConfigHost(additionalFiles), "/", - /*existingOptions*/ undefined, "tsconfig.json", - /*resolutionStack*/ undefined, - /*extraFileExtensions*/ undefined, - /*extendedConfigCache*/ undefined, existingWatchOptions); - } - function getParsedCommandJsonNode(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: ts.WatchOptions) { - const parsed = ts.parseJsonText("tsconfig.json", JSON.stringify(json)); - return ts.parseJsonSourceFileConfigFileContent(parsed, createParseConfigHost(additionalFiles), "/", - /*existingOptions*/ undefined, "tsconfig.json", - /*resolutionStack*/ undefined, - /*extraFileExtensions*/ undefined, - /*extendedConfigCache*/ undefined, existingWatchOptions); - } +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?: ts.WatchOptions) { + return ts.parseJsonConfigFileContent(json, createParseConfigHost(additionalFiles), "/", + /*existingOptions*/ undefined, "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, existingWatchOptions); + } + function getParsedCommandJsonNode(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: ts.WatchOptions) { + const parsed = ts.parseJsonText("tsconfig.json", JSON.stringify(json)); + return ts.parseJsonSourceFileConfigFileContent(parsed, createParseConfigHost(additionalFiles), "/", + /*existingOptions*/ undefined, "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, existingWatchOptions); + } - interface VerifyWatchOptions { - json: object; - expectedOptions: ts.WatchOptions | undefined; - additionalFiles?: vfs.FileSet; - existingWatchOptions?: ts.WatchOptions | undefined; - expectedErrors?: (sourceFile?: ts.SourceFile) => ts.Diagnostic[]; - } + interface VerifyWatchOptions { + json: object; + expectedOptions: ts.WatchOptions | undefined; + additionalFiles?: vfs.FileSet; + existingWatchOptions?: ts.WatchOptions | undefined; + expectedErrors?: (sourceFile?: ts.SourceFile) => ts.Diagnostic[]; + } - function verifyWatchOptions(scenario: () => VerifyWatchOptions[]) { - it("with json api", () => { - for (const { json, expectedOptions, additionalFiles, existingWatchOptions, expectedErrors } of scenario()) { - const parsed = getParsedCommandJson(json, additionalFiles, existingWatchOptions); - assert.deepEqual(parsed.watchOptions, expectedOptions, `With ${JSON.stringify(json)}`); - if (ts.length(parsed.errors)) { - assert.deepEqual(parsed.errors, expectedErrors?.()); - } - else { - assert.equal(0, ts.length(expectedErrors?.()), `Expected no errors`); - } + function verifyWatchOptions(scenario: () => VerifyWatchOptions[]) { + it("with json api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions, expectedErrors } of scenario()) { + const parsed = getParsedCommandJson(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions, `With ${JSON.stringify(json)}`); + if (ts.length(parsed.errors)) { + assert.deepEqual(parsed.errors, expectedErrors?.()); } - }); - - it("with json source file api", () => { - for (const { json, expectedOptions, additionalFiles, existingWatchOptions, expectedErrors } of scenario()) { - const parsed = getParsedCommandJsonNode(json, additionalFiles, existingWatchOptions); - assert.deepEqual(parsed.watchOptions, expectedOptions); - if (ts.length(parsed.errors)) { - assert.deepEqual(parsed.errors, expectedErrors?.(parsed.options.configFile)); - } - else { - assert.equal(0, ts.length(expectedErrors?.(parsed.options.configFile)), `Expected no errors`); - } + else { + assert.equal(0, ts.length(expectedErrors?.()), `Expected no errors`); } - }); - } - - describe("no watchOptions specified option", () => { - verifyWatchOptions(() => [{ - json: {}, - expectedOptions: undefined - }]); + } }); - describe("empty watchOptions specified option", () => { - verifyWatchOptions(() => [{ - json: { watchOptions: {} }, - expectedOptions: undefined - }]); + it("with json source file api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions, expectedErrors } of scenario()) { + const parsed = getParsedCommandJsonNode(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions); + if (ts.length(parsed.errors)) { + assert.deepEqual(parsed.errors, expectedErrors?.(parsed.options.configFile)); + } + else { + assert.equal(0, ts.length(expectedErrors?.(parsed.options.configFile)), `Expected no errors`); + } + } }); + } - describe("extending config file", () => { - describe("when extending config file without watchOptions", () => { - verifyWatchOptions(() => [ - { - json: { - extends: "./base.json", - watchOptions: { watchFile: "UseFsEvents" } - }, - expectedOptions: { watchFile: ts.WatchFileKind.UseFsEvents }, - additionalFiles: { "/base.json": "{}" } - }, - { - json: { extends: "./base.json", }, - expectedOptions: undefined, - additionalFiles: { "/base.json": "{}" } - } - ]); - }); + describe("no watchOptions specified option", () => { + verifyWatchOptions(() => [{ + json: {}, + expectedOptions: undefined + }]); + }); - describe("when extending config file with watchOptions", () => { - verifyWatchOptions(() => [ - { - json: { - extends: "./base.json", - watchOptions: { - watchFile: "UseFsEvents", - } - }, - expectedOptions: { - watchFile: ts.WatchFileKind.UseFsEvents, - watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval - }, - additionalFiles: { - "/base.json": JSON.stringify({ - watchOptions: { - watchFile: "UseFsEventsOnParentDirectory", - watchDirectory: "FixedPollingInterval" - } - }) - } - }, - { - json: { - extends: "./base.json", - }, - expectedOptions: { - watchFile: ts.WatchFileKind.UseFsEventsOnParentDirectory, - watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval - }, - additionalFiles: { - "/base.json": JSON.stringify({ - watchOptions: { - watchFile: "UseFsEventsOnParentDirectory", - watchDirectory: "FixedPollingInterval" - } - }) - } - } - ]); - }); - }); + describe("empty watchOptions specified option", () => { + verifyWatchOptions(() => [{ + json: { watchOptions: {} }, + expectedOptions: undefined + }]); + }); - describe("different options", () => { + describe("extending config file", () => { + describe("when extending config file without watchOptions", () => { verifyWatchOptions(() => [ { - json: { watchOptions: { watchFile: "UseFsEvents" } }, - expectedOptions: { watchFile: ts.WatchFileKind.UseFsEvents } - }, - { - json: { watchOptions: { watchDirectory: "UseFsEvents" } }, - expectedOptions: { watchDirectory: ts.WatchDirectoryKind.UseFsEvents } - }, - { - json: { watchOptions: { fallbackPolling: "DynamicPriority" } }, - expectedOptions: { fallbackPolling: ts.PollingWatchKind.DynamicPriority } - }, - { - json: { watchOptions: { synchronousWatchDirectory: true } }, - expectedOptions: { synchronousWatchDirectory: true } - }, - { - json: { watchOptions: { excludeDirectories: ["**/temp"] } }, - expectedOptions: { excludeDirectories: ["/**/temp"] } - }, - { - json: { watchOptions: { excludeFiles: ["**/temp/*.ts"] } }, - expectedOptions: { excludeFiles: ["/**/temp/*.ts"] } - }, - { - json: { watchOptions: { excludeDirectories: ["**/../*"] } }, - expectedOptions: { excludeDirectories: [] }, - expectedErrors: sourceFile => [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: sourceFile, - start: sourceFile && sourceFile.text.indexOf(`"**/../*"`), - length: sourceFile && `"**/../*"`.length, - reportsDeprecated: undefined, - reportsUnnecessary: undefined - } - ] + json: { + extends: "./base.json", + watchOptions: { watchFile: "UseFsEvents" } + }, + expectedOptions: { watchFile: ts.WatchFileKind.UseFsEvents }, + additionalFiles: { "/base.json": "{}" } }, { - json: { watchOptions: { excludeFiles: ["**/../*"] } }, - expectedOptions: { excludeFiles: [] }, - expectedErrors: sourceFile => [ - { - messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, - category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, - code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, - file: sourceFile, - start: sourceFile && sourceFile.text.indexOf(`"**/../*"`), - length: sourceFile && `"**/../*"`.length, - reportsDeprecated: undefined, - reportsUnnecessary: undefined - } - ] - }, + 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: ts.WatchFileKind.UseFsEvents, watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval }, - existingWatchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } + json: { + extends: "./base.json", + watchOptions: { + watchFile: "UseFsEvents", + } + }, + expectedOptions: { + watchFile: ts.WatchFileKind.UseFsEvents, + watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } }, { - json: {}, - expectedOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval }, - existingWatchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } - }, + json: { + extends: "./base.json", + }, + expectedOptions: { + watchFile: ts.WatchFileKind.UseFsEventsOnParentDirectory, + watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } + } ]); }); }); + + describe("different options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: ts.WatchFileKind.UseFsEvents } + }, + { + json: { watchOptions: { watchDirectory: "UseFsEvents" } }, + expectedOptions: { watchDirectory: ts.WatchDirectoryKind.UseFsEvents } + }, + { + json: { watchOptions: { fallbackPolling: "DynamicPriority" } }, + expectedOptions: { fallbackPolling: ts.PollingWatchKind.DynamicPriority } + }, + { + json: { watchOptions: { synchronousWatchDirectory: true } }, + expectedOptions: { synchronousWatchDirectory: true } + }, + { + json: { watchOptions: { excludeDirectories: ["**/temp"] } }, + expectedOptions: { excludeDirectories: ["/**/temp"] } + }, + { + json: { watchOptions: { excludeFiles: ["**/temp/*.ts"] } }, + expectedOptions: { excludeFiles: ["/**/temp/*.ts"] } + }, + { + json: { watchOptions: { excludeDirectories: ["**/../*"] } }, + expectedOptions: { excludeDirectories: [] }, + expectedErrors: sourceFile => [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: sourceFile, + start: sourceFile && sourceFile.text.indexOf(`"**/../*"`), + length: sourceFile && `"**/../*"`.length, + reportsDeprecated: undefined, + reportsUnnecessary: undefined + } + ] + }, + { + json: { watchOptions: { excludeFiles: ["**/../*"] } }, + expectedOptions: { excludeFiles: [] }, + expectedErrors: sourceFile => [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: sourceFile, + start: sourceFile && sourceFile.text.indexOf(`"**/../*"`), + length: sourceFile && `"**/../*"`.length, + reportsDeprecated: undefined, + reportsUnnecessary: undefined + } + ] + }, + ]); + }); + + describe("watch options extending passed in watch options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: ts.WatchFileKind.UseFsEvents, watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } + }, + { + json: {}, + expectedOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: ts.WatchDirectoryKind.FixedPollingInterval } + }, + ]); + }); +}); } diff --git a/src/testRunner/unittests/convertToBase64.ts b/src/testRunner/unittests/convertToBase64.ts index dcc4c056df855..09f2ffb8abe7b 100644 --- a/src/testRunner/unittests/convertToBase64.ts +++ b/src/testRunner/unittests/convertToBase64.ts @@ -1,33 +1,33 @@ namespace ts { - describe("unittests:: convertToBase64", () => { - function runTest(input: string): void { - const actual = ts.convertToBase64(input); - const expected = ts.sys.base64encode!(input); - assert.equal(actual, expected, "Encoded string using convertToBase64 does not match buffer.toString('base64')"); - } +describe("unittests:: convertToBase64", () => { + function runTest(input: string): void { + const actual = ts.convertToBase64(input); + const expected = ts.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{|}~"); - }); + if (Buffer) { + it("Converts ASCII charaters correctly", () => { + runTest(" !\"#$ %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); + }); - it("Converts escape sequences correctly", () => { - runTest("\t\n\r\\\"\'\u0062"); - }); + it("Converts escape sequences correctly", () => { + runTest("\t\n\r\\\"\'\u0062"); + }); - it("Converts simple unicode characters correctly", () => { - runTest("ΠΣ ٵپ औठ ⺐⺠"); - }); + it("Converts simple unicode characters correctly", () => { + runTest("ΠΣ ٵپ औठ ⺐⺠"); + }); - it("Converts simple code snippet 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 b4bed7270bc19..39651c959ab42 100644 --- a/src/testRunner/unittests/createMapShim.ts +++ b/src/testRunner/unittests/createMapShim.ts @@ -1,328 +1,328 @@ namespace ts { - describe("unittests:: createMapShim", () => { - - const stringKeys = [ - "1", - "3", - "2", - "4", - "0", - "999", - "A", - "B", - "C", - "Z", - "X", - "X1", - "X2", - "Y" - ]; - - const mixedKeys = [ - true, - 3, - { toString() { return "2"; } }, - "4", - false, - null, - undefined, - "B", - { toString() { return "C"; } }, - "Z", - "X", - { toString() { return "X1"; } }, - "X2", - "Y" - ]; - - function testMapIterationAddedValues(keys: K[], map: ts.ESMap, useForEach: boolean): string { - let resultString = ""; - - map.set(keys[0], "1"); - map.set(keys[1], "3"); - map.set(keys[2], "2"); - map.set(keys[3], "4"); - - let addedThree = false; - const doForEach = (value: string, key: K) => { - resultString += `${key}:${value};`; - - // Add a new key ("0") - the map should provide this - // one in the next iteration. - if (key === keys[0]) { - map.set(keys[0], "X1"); - map.set(keys[4], "X0"); - map.set(keys[3], "X4"); - } - else if (key === keys[1]) { - if (!addedThree) { - addedThree = true; - - // Remove and re-add key "3"; the map should - // visit it after "0". - map.delete(keys[1]); - map.set(keys[1], "Y3"); - - // Change the value of "2"; the map should provide - // it when visiting the key. - map.set(keys[2], "Y2"); - } - else { - // Check that an entry added when we visit the - // currently last entry will still be visited. - map.set(keys[5], "999"); - } - } - else if (key === keys[5]) { - // Ensure that clear() behaves correctly same as removing all keys. - map.set(keys[6], "A"); - map.set(keys[7], "B"); - map.set(keys[8], "C"); - } - else if (key === keys[6]) { - map.clear(); - map.set(keys[9], "Z"); +describe("unittests:: createMapShim", () => { + + const stringKeys = [ + "1", + "3", + "2", + "4", + "0", + "999", + "A", + "B", + "C", + "Z", + "X", + "X1", + "X2", + "Y" + ]; + + const mixedKeys = [ + true, + 3, + { toString() { return "2"; } }, + "4", + false, + null, + undefined, + "B", + { toString() { return "C"; } }, + "Z", + "X", + { toString() { return "X1"; } }, + "X2", + "Y" + ]; + + function testMapIterationAddedValues(keys: K[], map: ts.ESMap, useForEach: boolean): string { + let resultString = ""; + + map.set(keys[0], "1"); + map.set(keys[1], "3"); + map.set(keys[2], "2"); + map.set(keys[3], "4"); + + let addedThree = false; + const doForEach = (value: string, key: K) => { + resultString += `${key}:${value};`; + + // Add a new key ("0") - the map should provide this + // one in the next iteration. + if (key === keys[0]) { + map.set(keys[0], "X1"); + map.set(keys[4], "X0"); + map.set(keys[3], "X4"); + } + else if (key === keys[1]) { + if (!addedThree) { + addedThree = true; + + // Remove and re-add key "3"; the map should + // visit it after "0". + map.delete(keys[1]); + map.set(keys[1], "Y3"); + + // Change the value of "2"; the map should provide + // it when visiting the key. + map.set(keys[2], "Y2"); } - else if (key === keys[9]) { - // Check that the map behaves correctly when two items are - // added and removed immediately. - map.set(keys[10], "X"); - map.set(keys[11], "X1"); - map.set(keys[12], "X2"); - map.delete(keys[11]); - map.delete(keys[12]); - map.set(keys[13], "Y"); + else { + // Check that an entry added when we visit the + // currently last entry will still be visited. + map.set(keys[5], "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 === keys[5]) { + // Ensure that clear() behaves correctly same as removing all keys. + map.set(keys[6], "A"); + map.set(keys[7], "B"); + map.set(keys[8], "C"); + } + else if (key === keys[6]) { + map.clear(); + map.set(keys[9], "Z"); + } + else if (key === keys[9]) { + // Check that the map behaves correctly when two items are + // added and removed immediately. + map.set(keys[10], "X"); + map.set(keys[11], "X1"); + map.set(keys[12], "X2"); + map.delete(keys[11]); + map.delete(keys[12]); + map.set(keys[13], "Y"); } + }; - return resultString; + if (useForEach) { + map.forEach(doForEach); } + else { + // Use an iterator. + const iterator = map.entries(); + while (true) { + const iterResult = iterator.next(); + if (iterResult.done) { + break; + } - let MapShim!: ts.MapConstructor; - beforeEach(() => { - function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ - K, - V - ] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; - function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { - // override `ts.getIterator` with a version that allows us to iterate over a `MapShim` in an environment with a native `Map`. - if (iterable instanceof MapShim) - return iterable.entries(); - return ts.getIterator(iterable); + const [key, value] = iterResult.value; + doForEach(value, key); } + } + + return resultString; + } + + let MapShim!: ts.MapConstructor; + beforeEach(() => { + function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ + K, + V + ] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; + function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { + // override `ts.getIterator` with a version that allows us to iterate over a `MapShim` in an environment with a native `Map`. + if (iterable instanceof MapShim) + return iterable.entries(); + return ts.getIterator(iterable); + } + + MapShim = ts.ShimCollections.createMapShim(getIterator); + }); + afterEach(() => { + MapShim = undefined!; + }); + + it("iterates values in insertion order and handles changes with string keys", () => { + 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 = new ts.Map(); + const nativeMapForEachResult = testMapIterationAddedValues(stringKeys, nativeMap, /* useForEach */ true); + assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); + + nativeMap = new ts.Map(); + const nativeMapIteratorResult = testMapIterationAddedValues(stringKeys, nativeMap, /* useForEach */ false); + assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); + + // Then, test the map shim. + let localShimMap = new MapShim(); + const shimMapForEachResult = testMapIterationAddedValues(stringKeys, localShimMap, /* useForEach */ true); + assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); + + localShimMap = new MapShim(); + const shimMapIteratorResult = testMapIterationAddedValues(stringKeys, localShimMap, /* useForEach */ false); + assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); + }); + + it("iterates values in insertion order and handles changes with mixed-type keys", () => { + const expectedResult = "true:1;3:3;2:Y2;4:X4;false:X0;3:Y3;null:999;undefined:A;Z:Z;X:X;Y:Y;"; + + // First, ensure the test actually has the same behavior as a native Map. + let nativeMap = new ts.Map(); + const nativeMapForEachResult = testMapIterationAddedValues(mixedKeys, nativeMap, /* useForEach */ true); + assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); + + nativeMap = new ts.Map(); + const nativeMapIteratorResult = testMapIterationAddedValues(mixedKeys, nativeMap, /* useForEach */ false); + assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); + + // Then, test the map shim. + let localShimMap = new MapShim(); + const shimMapForEachResult = testMapIterationAddedValues(mixedKeys, localShimMap, /* useForEach */ true); + assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); + + localShimMap = new MapShim(); + const shimMapIteratorResult = testMapIterationAddedValues(mixedKeys, localShimMap, /* useForEach */ false); + assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); + }); + + it("create from Array", () => { + const map = new MapShim([["a", "b"]]); + assert.equal(map.size, 1); + assert.isTrue(map.has("a")); + assert.equal(map.get("a"), "b"); + }); + + it("create from Map", () => { + const map1 = new MapShim([["a", "b"]]); + const map2 = new MapShim(map1); + assert.equal(map1.size, 1); + assert.equal(map2.size, 1); + assert.isTrue(map2.has("a")); + assert.equal(map2.get("a"), "b"); + }); + + it("set when not present", () => { + const map = new MapShim(); + const result = map.set("a", "b"); + assert.equal(map.size, 1); + assert.strictEqual(result, map); + assert.isTrue(map.has("a")); + assert.equal(map.get("a"), "b"); + }); + + it("set when present", () => { + const map = new MapShim(); + map.set("a", "z"); + const result = map.set("a", "b"); + assert.equal(map.size, 1); + assert.strictEqual(result, map); + assert.isTrue(map.has("a")); + assert.equal(map.get("a"), "b"); + }); + + it("has when not present", () => { + const map = new MapShim(); + assert.isFalse(map.has("a")); + }); + + it("has when present", () => { + const map = new MapShim(); + map.set("a", "b"); + assert.isTrue(map.has("a")); + }); + + it("get when not present", () => { + const map = new MapShim(); + assert.isUndefined(map.get("a")); + }); + + it("get when present", () => { + const map = new MapShim(); + map.set("a", "b"); + assert.equal(map.get("a"), "b"); + }); + + it("delete when not present", () => { + const map = new MapShim(); + assert.isFalse(map.delete("a")); + }); + + it("delete when present", () => { + const map = new MapShim(); + map.set("a", "b"); + assert.isTrue(map.delete("a")); + }); + + it("delete twice when present", () => { + const map = new MapShim(); + map.set("a", "b"); + assert.isTrue(map.delete("a")); + assert.isFalse(map.delete("a")); + }); + + it("remove only item and iterate", () => { + const map = new MapShim(); + map.set("a", "b"); + map.delete("a"); + const actual = ts.arrayFrom(map.keys()); + assert.deepEqual(actual, []); + }); + + it("remove first item and iterate", () => { + const map = new MapShim(); + map.set("a", "b"); + map.set("c", "d"); + map.delete("a"); + assert.deepEqual(ts.arrayFrom(map.keys()), ["c"]); + assert.deepEqual(ts.arrayFrom(map.values()), ["d"]); + assert.deepEqual(ts.arrayFrom(map.entries()), [["c", "d"]]); + }); + + it("remove last item and iterate", () => { + const map = new MapShim(); + map.set("a", "b"); + map.set("c", "d"); + map.delete("c"); + assert.deepEqual(ts.arrayFrom(map.keys()), ["a"]); + assert.deepEqual(ts.arrayFrom(map.values()), ["b"]); + assert.deepEqual(ts.arrayFrom(map.entries()), [["a", "b"]]); + }); + + it("remove middle item and iterate", () => { + const map = new MapShim(); + map.set("a", "b"); + map.set("c", "d"); + map.set("e", "f"); + map.delete("c"); + assert.deepEqual(ts.arrayFrom(map.keys()), ["a", "e"]); + assert.deepEqual(ts.arrayFrom(map.values()), ["b", "f"]); + assert.deepEqual(ts.arrayFrom(map.entries()), [["a", "b"], ["e", "f"]]); + }); + + it("keys", () => { + const map = new MapShim(); + map.set("c", "d"); + map.set("a", "b"); + assert.deepEqual(ts.arrayFrom(map.keys()), ["c", "a"]); + }); + + it("values", () => { + const map = new MapShim(); + map.set("c", "d"); + map.set("a", "b"); + assert.deepEqual(ts.arrayFrom(map.values()), ["d", "b"]); + }); + + it("entries", () => { + const map = new MapShim(); + map.set("c", "d"); + map.set("a", "b"); + assert.deepEqual(ts.arrayFrom(map.entries()), [["c", "d"], ["a", "b"]]); + }); - MapShim = ts.ShimCollections.createMapShim(getIterator); - }); - afterEach(() => { - MapShim = undefined!; - }); - - it("iterates values in insertion order and handles changes with string keys", () => { - 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 = new ts.Map(); - const nativeMapForEachResult = testMapIterationAddedValues(stringKeys, nativeMap, /* useForEach */ true); - assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); - - nativeMap = new ts.Map(); - const nativeMapIteratorResult = testMapIterationAddedValues(stringKeys, nativeMap, /* useForEach */ false); - assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); - - // Then, test the map shim. - let localShimMap = new MapShim(); - const shimMapForEachResult = testMapIterationAddedValues(stringKeys, localShimMap, /* useForEach */ true); - assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); - - localShimMap = new MapShim(); - const shimMapIteratorResult = testMapIterationAddedValues(stringKeys, localShimMap, /* useForEach */ false); - assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); - }); - - it("iterates values in insertion order and handles changes with mixed-type keys", () => { - const expectedResult = "true:1;3:3;2:Y2;4:X4;false:X0;3:Y3;null:999;undefined:A;Z:Z;X:X;Y:Y;"; - - // First, ensure the test actually has the same behavior as a native Map. - let nativeMap = new ts.Map(); - const nativeMapForEachResult = testMapIterationAddedValues(mixedKeys, nativeMap, /* useForEach */ true); - assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); - - nativeMap = new ts.Map(); - const nativeMapIteratorResult = testMapIterationAddedValues(mixedKeys, nativeMap, /* useForEach */ false); - assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); - - // Then, test the map shim. - let localShimMap = new MapShim(); - const shimMapForEachResult = testMapIterationAddedValues(mixedKeys, localShimMap, /* useForEach */ true); - assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); - - localShimMap = new MapShim(); - const shimMapIteratorResult = testMapIterationAddedValues(mixedKeys, localShimMap, /* useForEach */ false); - assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); - }); - - it("create from Array", () => { - const map = new MapShim([["a", "b"]]); - assert.equal(map.size, 1); - assert.isTrue(map.has("a")); - assert.equal(map.get("a"), "b"); - }); - - it("create from Map", () => { - const map1 = new MapShim([["a", "b"]]); - const map2 = new MapShim(map1); - assert.equal(map1.size, 1); - assert.equal(map2.size, 1); - assert.isTrue(map2.has("a")); - assert.equal(map2.get("a"), "b"); - }); - - it("set when not present", () => { - const map = new MapShim(); - const result = map.set("a", "b"); - assert.equal(map.size, 1); - assert.strictEqual(result, map); - assert.isTrue(map.has("a")); - assert.equal(map.get("a"), "b"); - }); - - it("set when present", () => { - const map = new MapShim(); - map.set("a", "z"); - const result = map.set("a", "b"); - assert.equal(map.size, 1); - assert.strictEqual(result, map); - assert.isTrue(map.has("a")); - assert.equal(map.get("a"), "b"); - }); - - it("has when not present", () => { - const map = new MapShim(); - assert.isFalse(map.has("a")); - }); - - it("has when present", () => { - const map = new MapShim(); - map.set("a", "b"); - assert.isTrue(map.has("a")); - }); - - it("get when not present", () => { - const map = new MapShim(); - assert.isUndefined(map.get("a")); - }); - - it("get when present", () => { - const map = new MapShim(); - map.set("a", "b"); - assert.equal(map.get("a"), "b"); - }); - - it("delete when not present", () => { - const map = new MapShim(); - assert.isFalse(map.delete("a")); - }); - - it("delete when present", () => { - const map = new MapShim(); - map.set("a", "b"); - assert.isTrue(map.delete("a")); - }); - - it("delete twice when present", () => { - const map = new MapShim(); - map.set("a", "b"); - assert.isTrue(map.delete("a")); - assert.isFalse(map.delete("a")); - }); - - it("remove only item and iterate", () => { - const map = new MapShim(); - map.set("a", "b"); - map.delete("a"); - const actual = ts.arrayFrom(map.keys()); - assert.deepEqual(actual, []); - }); - - it("remove first item and iterate", () => { - const map = new MapShim(); - map.set("a", "b"); - map.set("c", "d"); - map.delete("a"); - assert.deepEqual(ts.arrayFrom(map.keys()), ["c"]); - assert.deepEqual(ts.arrayFrom(map.values()), ["d"]); - assert.deepEqual(ts.arrayFrom(map.entries()), [["c", "d"]]); - }); - - it("remove last item and iterate", () => { - const map = new MapShim(); - map.set("a", "b"); - map.set("c", "d"); - map.delete("c"); - assert.deepEqual(ts.arrayFrom(map.keys()), ["a"]); - assert.deepEqual(ts.arrayFrom(map.values()), ["b"]); - assert.deepEqual(ts.arrayFrom(map.entries()), [["a", "b"]]); - }); - - it("remove middle item and iterate", () => { - const map = new MapShim(); - map.set("a", "b"); - map.set("c", "d"); - map.set("e", "f"); - map.delete("c"); - assert.deepEqual(ts.arrayFrom(map.keys()), ["a", "e"]); - assert.deepEqual(ts.arrayFrom(map.values()), ["b", "f"]); - assert.deepEqual(ts.arrayFrom(map.entries()), [["a", "b"], ["e", "f"]]); - }); - - it("keys", () => { - const map = new MapShim(); - map.set("c", "d"); - map.set("a", "b"); - assert.deepEqual(ts.arrayFrom(map.keys()), ["c", "a"]); - }); - - it("values", () => { - const map = new MapShim(); - map.set("c", "d"); - map.set("a", "b"); - assert.deepEqual(ts.arrayFrom(map.values()), ["d", "b"]); - }); - - it("entries", () => { - const map = new MapShim(); - map.set("c", "d"); - map.set("a", "b"); - assert.deepEqual(ts.arrayFrom(map.entries()), [["c", "d"], ["a", "b"]]); - }); - - it("forEach", () => { - const map = new MapShim(); - map.set("c", "d"); - map.set("a", "b"); - const actual: [ - string, - string - ][] = []; - map.forEach((value, key) => actual.push([key, value])); - assert.deepEqual(actual, [["c", "d"], ["a", "b"]]); - }); + it("forEach", () => { + const map = new MapShim(); + map.set("c", "d"); + map.set("a", "b"); + const actual: [ + string, + string + ][] = []; + map.forEach((value, key) => actual.push([key, value])); + assert.deepEqual(actual, [["c", "d"], ["a", "b"]]); }); +}); } diff --git a/src/testRunner/unittests/createSetShim.ts b/src/testRunner/unittests/createSetShim.ts index 6b6b6f79ca524..dde01e8bf5132 100644 --- a/src/testRunner/unittests/createSetShim.ts +++ b/src/testRunner/unittests/createSetShim.ts @@ -1,311 +1,311 @@ namespace ts { - describe("unittests:: createSetShim", () => { - const stringKeys = [ - "1", - "3", - "2", - "4", - "0", - "999", - "A", - "B", - "C", - "Z", - "X", - "X1", - "X2", - "Y" - ]; - - const mixedKeys = [ - true, - 3, - { toString() { return "2"; } }, - "4", - false, - null, - undefined, - "B", - { toString() { return "C"; } }, - "Z", - "X", - { toString() { return "X1"; } }, - "X2", - "Y" - ]; - - function testSetIterationAddedValues(keys: K[], set: ts.Set, useForEach: boolean): string { - let resultString = ""; - - set.add(keys[0]); - set.add(keys[1]); - set.add(keys[2]); - set.add(keys[3]); - - let addedThree = false; - const doForEach = (key: K) => { - resultString += `${key};`; - - // Add a new key ("0") - the set should provide this - // one in the next iteration. - if (key === keys[0]) { - set.add(keys[0]); - set.add(keys[4]); - set.add(keys[3]); - } - else if (key === keys[1]) { - if (!addedThree) { - addedThree = true; - - // Remove and re-add key "3"; the set should - // visit it after "0". - set.delete(keys[1]); - set.add(keys[1]); - - // Change the value of "2"; the set should provide - // it when visiting the key. - set.add(keys[2]); - } - else { - // Check that an entry added when we visit the - // currently last entry will still be visited. - set.add(keys[5]); - } - } - else if (key === keys[5]) { - // Ensure that clear() behaves correctly same as removing all keys. - set.add(keys[6]); - set.add(keys[7]); - set.add(keys[8]); - } - else if (key === keys[6]) { - set.clear(); - set.add(keys[9]); +describe("unittests:: createSetShim", () => { + const stringKeys = [ + "1", + "3", + "2", + "4", + "0", + "999", + "A", + "B", + "C", + "Z", + "X", + "X1", + "X2", + "Y" + ]; + + const mixedKeys = [ + true, + 3, + { toString() { return "2"; } }, + "4", + false, + null, + undefined, + "B", + { toString() { return "C"; } }, + "Z", + "X", + { toString() { return "X1"; } }, + "X2", + "Y" + ]; + + function testSetIterationAddedValues(keys: K[], set: ts.Set, useForEach: boolean): string { + let resultString = ""; + + set.add(keys[0]); + set.add(keys[1]); + set.add(keys[2]); + set.add(keys[3]); + + let addedThree = false; + const doForEach = (key: K) => { + resultString += `${key};`; + + // Add a new key ("0") - the set should provide this + // one in the next iteration. + if (key === keys[0]) { + set.add(keys[0]); + set.add(keys[4]); + set.add(keys[3]); + } + else if (key === keys[1]) { + if (!addedThree) { + addedThree = true; + + // Remove and re-add key "3"; the set should + // visit it after "0". + set.delete(keys[1]); + set.add(keys[1]); + + // Change the value of "2"; the set should provide + // it when visiting the key. + set.add(keys[2]); } - else if (key === keys[9]) { - // Check that the set behaves correctly when two items are - // added and removed immediately. - set.add(keys[10]); - set.add(keys[11]); - set.add(keys[12]); - set.delete(keys[11]); - set.delete(keys[12]); - set.add(keys[13]); + else { + // Check that an entry added when we visit the + // currently last entry will still be visited. + set.add(keys[5]); } - }; - - if (useForEach) { - set.forEach(doForEach); } - else { - // Use an iterator. - const iterator = set.values(); - while (true) { - const iterResult = iterator.next(); - if (iterResult.done) { - break; - } - - doForEach(iterResult.value); - } + else if (key === keys[5]) { + // Ensure that clear() behaves correctly same as removing all keys. + set.add(keys[6]); + set.add(keys[7]); + set.add(keys[8]); + } + else if (key === keys[6]) { + set.clear(); + set.add(keys[9]); + } + else if (key === keys[9]) { + // Check that the set behaves correctly when two items are + // added and removed immediately. + set.add(keys[10]); + set.add(keys[11]); + set.add(keys[12]); + set.delete(keys[11]); + set.delete(keys[12]); + set.add(keys[13]); } + }; - return resultString; + if (useForEach) { + set.forEach(doForEach); } + else { + // Use an iterator. + const iterator = set.values(); + while (true) { + const iterResult = iterator.next(); + if (iterResult.done) { + break; + } - let SetShim!: ts.SetConstructor; - beforeEach(() => { - function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ - K, - V - ] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; - function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { - // override `ts.getIterator` with a version that allows us to iterate over a `SetShim` in an environment with a native `Set`. - if (iterable instanceof SetShim) - return iterable.values(); - return ts.getIterator(iterable); + doForEach(iterResult.value); } + } + + return resultString; + } + + let SetShim!: ts.SetConstructor; + beforeEach(() => { + function getIterator | ts.ReadonlyESMap | undefined>(iterable: I): ts.Iterator ? [ + K, + V + ] : I extends ts.ReadonlySet ? T : I extends readonly (infer T)[] ? T : I extends undefined ? undefined : never>; + function getIterator(iterable: readonly any[] | ts.ReadonlySet | ts.ReadonlyESMap | undefined): ts.Iterator | undefined { + // override `ts.getIterator` with a version that allows us to iterate over a `SetShim` in an environment with a native `Set`. + if (iterable instanceof SetShim) + return iterable.values(); + return ts.getIterator(iterable); + } + + SetShim = ts.ShimCollections.createSetShim(getIterator); + }); + afterEach(() => { + SetShim = undefined!; + }); + + it("iterates values in insertion order and handles changes with string keys", () => { + const expectedResult = "1;3;2;4;0;3;999;A;Z;X;Y;"; + + // First, ensure the test actually has the same behavior as a native Set. + let nativeSet = new ts.Set(); + const nativeSetForEachResult = testSetIterationAddedValues(stringKeys, nativeSet, /* useForEach */ true); + assert.equal(nativeSetForEachResult, expectedResult, "nativeSet-forEach"); + + nativeSet = new ts.Set(); + const nativeSetIteratorResult = testSetIterationAddedValues(stringKeys, nativeSet, /* useForEach */ false); + assert.equal(nativeSetIteratorResult, expectedResult, "nativeSet-iterator"); + + // Then, test the set shim. + let localShimSet = new SetShim(); + const shimSetForEachResult = testSetIterationAddedValues(stringKeys, localShimSet, /* useForEach */ true); + assert.equal(shimSetForEachResult, expectedResult, "shimSet-forEach"); + + localShimSet = new SetShim(); + const shimSetIteratorResult = testSetIterationAddedValues(stringKeys, localShimSet, /* useForEach */ false); + assert.equal(shimSetIteratorResult, expectedResult, "shimSet-iterator"); + }); + + it("iterates values in insertion order and handles changes with mixed-type keys", () => { + const expectedResult = "true;3;2;4;false;3;null;undefined;Z;X;Y;"; + + // First, ensure the test actually has the same behavior as a native Set. + let nativeSet = new ts.Set(); + const nativeSetForEachResult = testSetIterationAddedValues(mixedKeys, nativeSet, /* useForEach */ true); + assert.equal(nativeSetForEachResult, expectedResult, "nativeSet-forEach"); + + nativeSet = new ts.Set(); + const nativeSetIteratorResult = testSetIterationAddedValues(mixedKeys, nativeSet, /* useForEach */ false); + assert.equal(nativeSetIteratorResult, expectedResult, "nativeSet-iterator"); + + // Then, test the set shim. + let localshimSet = new SetShim(); + const shimSetForEachResult = testSetIterationAddedValues(mixedKeys, localshimSet, /* useForEach */ true); + assert.equal(shimSetForEachResult, expectedResult, "shimSet-forEach"); + + localshimSet = new SetShim(); + const shimSetIteratorResult = testSetIterationAddedValues(mixedKeys, localshimSet, /* useForEach */ false); + assert.equal(shimSetIteratorResult, expectedResult, "shimSet-iterator"); + }); + + it("create from Array", () => { + const set = new SetShim(["a"]); + assert.equal(set.size, 1); + assert.isTrue(set.has("a")); + }); + + it("create from set", () => { + const set1 = new SetShim(["a"]); + const set2 = new SetShim(set1); + assert.equal(set1.size, 1); + assert.equal(set2.size, 1); + assert.isTrue(set2.has("a")); + }); + + it("add when not present", () => { + const set = new SetShim(); + const result = set.add("a"); + assert.equal(set.size, 1); + assert.strictEqual(result, set); + assert.isTrue(set.has("a")); + }); + + it("add when present", () => { + const set = new SetShim(); + set.add("a"); + const result = set.add("a"); + assert.equal(set.size, 1); + assert.strictEqual(result, set); + assert.isTrue(set.has("a")); + }); + + it("has when not present", () => { + const set = new SetShim(); + assert.isFalse(set.has("a")); + }); + + it("has when present", () => { + const set = new SetShim(); + set.add("a"); + assert.isTrue(set.has("a")); + }); + + it("delete when not present", () => { + const set = new SetShim(); + assert.isFalse(set.delete("a")); + }); + + it("delete when present", () => { + const set = new SetShim(); + set.add("a"); + assert.isTrue(set.delete("a")); + }); + + it("delete twice when present", () => { + const set = new SetShim(); + set.add("a"); + assert.isTrue(set.delete("a")); + assert.isFalse(set.delete("a")); + }); + + it("remove only item and iterate", () => { + const set = new SetShim(); + set.add("a"); + set.delete("a"); + const actual = ts.arrayFrom(set.keys()); + assert.deepEqual(actual, []); + }); + + it("remove first item and iterate", () => { + const set = new SetShim(); + set.add("a"); + set.add("c"); + set.delete("a"); + assert.deepEqual(ts.arrayFrom(set.keys()), ["c"]); + assert.deepEqual(ts.arrayFrom(set.values()), ["c"]); + assert.deepEqual(ts.arrayFrom(set.entries()), [["c", "c"]]); + }); + + it("remove last item and iterate", () => { + const set = new SetShim(); + set.add("a"); + set.add("c"); + set.delete("c"); + assert.deepEqual(ts.arrayFrom(set.keys()), ["a"]); + assert.deepEqual(ts.arrayFrom(set.values()), ["a"]); + assert.deepEqual(ts.arrayFrom(set.entries()), [["a", "a"]]); + }); + + it("remove middle item and iterate", () => { + const set = new SetShim(); + set.add("a"); + set.add("c"); + set.add("e"); + set.delete("c"); + assert.deepEqual(ts.arrayFrom(set.keys()), ["a", "e"]); + assert.deepEqual(ts.arrayFrom(set.values()), ["a", "e"]); + assert.deepEqual(ts.arrayFrom(set.entries()), [["a", "a"], ["e", "e"]]); + }); + + it("keys", () => { + const set = new SetShim(); + set.add("c"); + set.add("a"); + assert.deepEqual(ts.arrayFrom(set.keys()), ["c", "a"]); + }); + + it("values", () => { + const set = new SetShim(); + set.add("c"); + set.add("a"); + assert.deepEqual(ts.arrayFrom(set.values()), ["c", "a"]); + }); + + it("entries", () => { + const set = new SetShim(); + set.add("c"); + set.add("a"); + assert.deepEqual(ts.arrayFrom(set.entries()), [["c", "c"], ["a", "a"]]); + }); - SetShim = ts.ShimCollections.createSetShim(getIterator); - }); - afterEach(() => { - SetShim = undefined!; - }); - - it("iterates values in insertion order and handles changes with string keys", () => { - const expectedResult = "1;3;2;4;0;3;999;A;Z;X;Y;"; - - // First, ensure the test actually has the same behavior as a native Set. - let nativeSet = new ts.Set(); - const nativeSetForEachResult = testSetIterationAddedValues(stringKeys, nativeSet, /* useForEach */ true); - assert.equal(nativeSetForEachResult, expectedResult, "nativeSet-forEach"); - - nativeSet = new ts.Set(); - const nativeSetIteratorResult = testSetIterationAddedValues(stringKeys, nativeSet, /* useForEach */ false); - assert.equal(nativeSetIteratorResult, expectedResult, "nativeSet-iterator"); - - // Then, test the set shim. - let localShimSet = new SetShim(); - const shimSetForEachResult = testSetIterationAddedValues(stringKeys, localShimSet, /* useForEach */ true); - assert.equal(shimSetForEachResult, expectedResult, "shimSet-forEach"); - - localShimSet = new SetShim(); - const shimSetIteratorResult = testSetIterationAddedValues(stringKeys, localShimSet, /* useForEach */ false); - assert.equal(shimSetIteratorResult, expectedResult, "shimSet-iterator"); - }); - - it("iterates values in insertion order and handles changes with mixed-type keys", () => { - const expectedResult = "true;3;2;4;false;3;null;undefined;Z;X;Y;"; - - // First, ensure the test actually has the same behavior as a native Set. - let nativeSet = new ts.Set(); - const nativeSetForEachResult = testSetIterationAddedValues(mixedKeys, nativeSet, /* useForEach */ true); - assert.equal(nativeSetForEachResult, expectedResult, "nativeSet-forEach"); - - nativeSet = new ts.Set(); - const nativeSetIteratorResult = testSetIterationAddedValues(mixedKeys, nativeSet, /* useForEach */ false); - assert.equal(nativeSetIteratorResult, expectedResult, "nativeSet-iterator"); - - // Then, test the set shim. - let localshimSet = new SetShim(); - const shimSetForEachResult = testSetIterationAddedValues(mixedKeys, localshimSet, /* useForEach */ true); - assert.equal(shimSetForEachResult, expectedResult, "shimSet-forEach"); - - localshimSet = new SetShim(); - const shimSetIteratorResult = testSetIterationAddedValues(mixedKeys, localshimSet, /* useForEach */ false); - assert.equal(shimSetIteratorResult, expectedResult, "shimSet-iterator"); - }); - - it("create from Array", () => { - const set = new SetShim(["a"]); - assert.equal(set.size, 1); - assert.isTrue(set.has("a")); - }); - - it("create from set", () => { - const set1 = new SetShim(["a"]); - const set2 = new SetShim(set1); - assert.equal(set1.size, 1); - assert.equal(set2.size, 1); - assert.isTrue(set2.has("a")); - }); - - it("add when not present", () => { - const set = new SetShim(); - const result = set.add("a"); - assert.equal(set.size, 1); - assert.strictEqual(result, set); - assert.isTrue(set.has("a")); - }); - - it("add when present", () => { - const set = new SetShim(); - set.add("a"); - const result = set.add("a"); - assert.equal(set.size, 1); - assert.strictEqual(result, set); - assert.isTrue(set.has("a")); - }); - - it("has when not present", () => { - const set = new SetShim(); - assert.isFalse(set.has("a")); - }); - - it("has when present", () => { - const set = new SetShim(); - set.add("a"); - assert.isTrue(set.has("a")); - }); - - it("delete when not present", () => { - const set = new SetShim(); - assert.isFalse(set.delete("a")); - }); - - it("delete when present", () => { - const set = new SetShim(); - set.add("a"); - assert.isTrue(set.delete("a")); - }); - - it("delete twice when present", () => { - const set = new SetShim(); - set.add("a"); - assert.isTrue(set.delete("a")); - assert.isFalse(set.delete("a")); - }); - - it("remove only item and iterate", () => { - const set = new SetShim(); - set.add("a"); - set.delete("a"); - const actual = ts.arrayFrom(set.keys()); - assert.deepEqual(actual, []); - }); - - it("remove first item and iterate", () => { - const set = new SetShim(); - set.add("a"); - set.add("c"); - set.delete("a"); - assert.deepEqual(ts.arrayFrom(set.keys()), ["c"]); - assert.deepEqual(ts.arrayFrom(set.values()), ["c"]); - assert.deepEqual(ts.arrayFrom(set.entries()), [["c", "c"]]); - }); - - it("remove last item and iterate", () => { - const set = new SetShim(); - set.add("a"); - set.add("c"); - set.delete("c"); - assert.deepEqual(ts.arrayFrom(set.keys()), ["a"]); - assert.deepEqual(ts.arrayFrom(set.values()), ["a"]); - assert.deepEqual(ts.arrayFrom(set.entries()), [["a", "a"]]); - }); - - it("remove middle item and iterate", () => { - const set = new SetShim(); - set.add("a"); - set.add("c"); - set.add("e"); - set.delete("c"); - assert.deepEqual(ts.arrayFrom(set.keys()), ["a", "e"]); - assert.deepEqual(ts.arrayFrom(set.values()), ["a", "e"]); - assert.deepEqual(ts.arrayFrom(set.entries()), [["a", "a"], ["e", "e"]]); - }); - - it("keys", () => { - const set = new SetShim(); - set.add("c"); - set.add("a"); - assert.deepEqual(ts.arrayFrom(set.keys()), ["c", "a"]); - }); - - it("values", () => { - const set = new SetShim(); - set.add("c"); - set.add("a"); - assert.deepEqual(ts.arrayFrom(set.values()), ["c", "a"]); - }); - - it("entries", () => { - const set = new SetShim(); - set.add("c"); - set.add("a"); - assert.deepEqual(ts.arrayFrom(set.entries()), [["c", "c"], ["a", "a"]]); - }); - - it("forEach", () => { - const set = new SetShim(); - set.add("c"); - set.add("a"); - const actual: [ - string, - string - ][] = []; - set.forEach((value, key) => actual.push([key, value])); - assert.deepEqual(actual, [["c", "c"], ["a", "a"]]); - }); + it("forEach", () => { + const set = new SetShim(); + set.add("c"); + set.add("a"); + const actual: [ + string, + string + ][] = []; + set.forEach((value, key) => actual.push([key, value])); + assert.deepEqual(actual, [["c", "c"], ["a", "a"]]); }); +}); } diff --git a/src/testRunner/unittests/customTransforms.ts b/src/testRunner/unittests/customTransforms.ts index 88701ef078855..a9cbcf0cd9abc 100644 --- a/src/testRunner/unittests/customTransforms.ts +++ b/src/testRunner/unittests/customTransforms.ts @@ -1,164 +1,164 @@ namespace ts { - describe("unittests:: customTransforms", () => { - function emitsCorrectly(name: string, sources: { - file: string; - text: string; - }[], customTransformers: ts.CustomTransformers, options: ts.CompilerOptions = {}) { - it(name, () => { - const roots = sources.map(source => ts.createSourceFile(source.file, source.text, ts.ScriptTarget.ES2015)); - const fileMap = ts.arrayToMap(roots, file => file.fileName); - const outputs = new ts.Map(); - const host: ts.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), - }; +describe("unittests:: customTransforms", () => { + function emitsCorrectly(name: string, sources: { + file: string; + text: string; + }[], customTransformers: ts.CustomTransformers, options: ts.CompilerOptions = {}) { + it(name, () => { + const roots = sources.map(source => ts.createSourceFile(source.file, source.text, ts.ScriptTarget.ES2015)); + const fileMap = ts.arrayToMap(roots, file => file.fileName); + const outputs = new ts.Map(); + const host: ts.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 = ts.createProgram(ts.arrayFrom(fileMap.keys()), { newLine: ts.NewLineKind.LineFeed, ...options }, host); - program.emit(/*targetSourceFile*/ undefined, host.writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, customTransformers); - let content = ""; - for (const [file, text] of ts.arrayFrom(outputs.entries())) { - if (content) - content += "\n\n"; - content += `// [${file}]\n`; - content += text; - } - Harness.Baseline.runBaseline(`customTransforms/${name}.js`, content); - }); - } + const program = ts.createProgram(ts.arrayFrom(fileMap.keys()), { newLine: ts.NewLineKind.LineFeed, ...options }, host); + program.emit(/*targetSourceFile*/ undefined, host.writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, customTransformers); + let content = ""; + for (const [file, text] of ts.arrayFrom(outputs.entries())) { + if (content) + content += "\n\n"; + content += `// [${file}]\n`; + content += text; + } + Harness.Baseline.runBaseline(`customTransforms/${name}.js`, content); + }); + } - const sources = [{ - file: "source.ts", - text: ` + const sources = [{ + file: "source.ts", + text: ` function f1() { } class c() { } enum e { } // leading function f2() { } // trailing ` - }]; + }]; - const before: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return visitFunction(node as ts.FunctionDeclaration); - default: - return ts.visitEachChild(node, visit, context); - } - } - function visitFunction(node: ts.FunctionDeclaration) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "@before", /*hasTrailingNewLine*/ true); - return node; + const before: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return visitFunction(node as ts.FunctionDeclaration); + default: + return ts.visitEachChild(node, visit, context); } - }; + } + function visitFunction(node: ts.FunctionDeclaration) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "@before", /*hasTrailingNewLine*/ true); + return node; + } + }; - const after: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.VariableStatement: - return visitVariableStatement(node as ts.VariableStatement); - default: - return ts.visitEachChild(node, visit, context); - } + const after: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + return visitVariableStatement(node as ts.VariableStatement); + default: + return ts.visitEachChild(node, visit, context); } - function visitVariableStatement(node: ts.VariableStatement) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, "@after"); - return node; - } - }; + } + function visitVariableStatement(node: ts.VariableStatement) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, "@after"); + return node; + } + }; - emitsCorrectly("before", sources, { before: [before] }); - emitsCorrectly("after", sources, { after: [after] }); - emitsCorrectly("both", sources, { before: [before], after: [after] }); + emitsCorrectly("before", sources, { before: [before] }); + emitsCorrectly("after", sources, { after: [after] }); + emitsCorrectly("both", sources, { before: [before], after: [after] }); - emitsCorrectly("before+decorators", [{ - file: "source.ts", - text: ` + emitsCorrectly("before+decorators", [{ + file: "source.ts", + text: ` declare const dec: any; class B {} @dec export class C { constructor(b: B) { } } 'change' ` - }], {before: [ - context => node => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { - if (ts.isStringLiteral(node) && node.text === "change") - return ts.factory.createStringLiteral("changed"); - return ts.visitEachChild(node, visitor, context); - }) - ]}, { - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.ES2015, - emitDecoratorMetadata: true, - experimentalDecorators: true - }); + }], {before: [ + context => node => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { + if (ts.isStringLiteral(node) && node.text === "change") + return ts.factory.createStringLiteral("changed"); + return ts.visitEachChild(node, visitor, context); + }) + ]}, { + target: ts.ScriptTarget.ES5, + module: ts.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 + 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 => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { - if (ts.isStringLiteral(node) && node.text === "change") { - const text = "'changed'"; - const lineMap = ts.computeLineStarts(text); - ts.setSourceMapRange(node, { - pos: 0, end: text.length, source: { - text, - fileName: "another.html", - lineMap, - getLineAndCharacterOfPosition: pos => ts.computeLineAndCharacterOfPosition(lineMap, pos) - } - }); - return node; - } - return ts.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: ts.Transformer = node => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { - if (ts.isIdentifier(node) && node.text === "original") { - const newNode = ts.factory.createIdentifier("changed"); - ts.setSourceMapRange(newNode, { - pos: 0, - end: 7, - // Do not provide a custom skipTrivia function for `source`. - source: ts.createSourceMapSource("another.html", "changed;") - }); - return newNode; + }, + ], { + before: [ + context => node => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { + if (ts.isStringLiteral(node) && node.text === "change") { + const text = "'changed'"; + const lineMap = ts.computeLineStarts(text); + ts.setSourceMapRange(node, { + pos: 0, end: text.length, source: { + text, + fileName: "another.html", + lineMap, + getLineAndCharacterOfPosition: pos => ts.computeLineAndCharacterOfPosition(lineMap, pos) } - return ts.visitEachChild(node, visitor, context); }); - return { - transformSourceFile, - transformBundle: node => ts.factory.createBundle(ts.map(node.sourceFiles, transformSourceFile), node.prepends), - }; + return node; } - ] - }, { sourceMap: true, outFile: "source.js" }); + return ts.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: ts.Transformer = node => ts.visitNode(node, function visitor(node: ts.Node): ts.Node { + if (ts.isIdentifier(node) && node.text === "original") { + const newNode = ts.factory.createIdentifier("changed"); + ts.setSourceMapRange(newNode, { + pos: 0, + end: 7, + // Do not provide a custom skipTrivia function for `source`. + source: ts.createSourceMapSource("another.html", "changed;") + }); + return newNode; + } + return ts.visitEachChild(node, visitor, context); + }); + return { + transformSourceFile, + transformBundle: node => ts.factory.createBundle(ts.map(node.sourceFiles, transformSourceFile), node.prepends), + }; + } + ] + }, { sourceMap: true, outFile: "source.js" }); - }); +}); } diff --git a/src/testRunner/unittests/debugDeprecation.ts b/src/testRunner/unittests/debugDeprecation.ts index 80b0b732b5423..216460493f4b5 100644 --- a/src/testRunner/unittests/debugDeprecation.ts +++ b/src/testRunner/unittests/debugDeprecation.ts @@ -1,97 +1,97 @@ namespace ts { - describe("unittests:: debugDeprecation", () => { - let loggingHost: ts.LoggingHost | undefined; - beforeEach(() => { - loggingHost = ts.Debug.loggingHost; - }); - afterEach(() => { - ts.Debug.loggingHost = loggingHost; - loggingHost = undefined; - }); - describe("deprecateFunction", () => { - it("silent deprecation", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - warnAfter: "3.9", - typeScriptVersion: "3.8" - }); - let logWritten = false; - ts.Debug.loggingHost = { - log() { - logWritten = true; - } - }; - deprecation(); - assert.isFalse(logWritten); +describe("unittests:: debugDeprecation", () => { + let loggingHost: ts.LoggingHost | undefined; + beforeEach(() => { + loggingHost = ts.Debug.loggingHost; + }); + afterEach(() => { + ts.Debug.loggingHost = loggingHost; + loggingHost = undefined; + }); + describe("deprecateFunction", () => { + it("silent deprecation", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + warnAfter: "3.9", + typeScriptVersion: "3.8" }); - it("warning deprecation with warnAfter", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - warnAfter: "3.9", - typeScriptVersion: "3.9" - }); - let logWritten = false; - ts.Debug.loggingHost = { - log() { - logWritten = true; - } - }; - deprecation(); - assert.isTrue(logWritten); + let logWritten = false; + ts.Debug.loggingHost = { + log() { + logWritten = true; + } + }; + deprecation(); + assert.isFalse(logWritten); + }); + it("warning deprecation with warnAfter", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + warnAfter: "3.9", + typeScriptVersion: "3.9" }); - it("warning deprecation without warnAfter", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - typeScriptVersion: "3.9" - }); - let logWritten = false; - ts.Debug.loggingHost = { - log() { - logWritten = true; - } - }; - deprecation(); - assert.isTrue(logWritten); + let logWritten = false; + ts.Debug.loggingHost = { + log() { + logWritten = true; + } + }; + deprecation(); + assert.isTrue(logWritten); + }); + it("warning deprecation without warnAfter", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + typeScriptVersion: "3.9" }); - it("warning deprecation writes once", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - typeScriptVersion: "3.9" - }); - let logWrites = 0; - ts.Debug.loggingHost = { - log() { - logWrites++; - } - }; - deprecation(); - deprecation(); - assert.equal(logWrites, 1); + let logWritten = false; + ts.Debug.loggingHost = { + log() { + logWritten = true; + } + }; + deprecation(); + assert.isTrue(logWritten); + }); + it("warning deprecation writes once", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + typeScriptVersion: "3.9" }); - it("error deprecation with errorAfter", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - warnAfter: "3.8", - errorAfter: "3.9", - typeScriptVersion: "3.9" - }); - let logWritten = false; - ts.Debug.loggingHost = { - log() { - logWritten = true; - } - }; - expect(deprecation).throws(); - assert.isFalse(logWritten); + let logWrites = 0; + ts.Debug.loggingHost = { + log() { + logWrites++; + } + }; + deprecation(); + deprecation(); + assert.equal(logWrites, 1); + }); + it("error deprecation with errorAfter", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + warnAfter: "3.8", + errorAfter: "3.9", + typeScriptVersion: "3.9" }); - it("error deprecation with error", () => { - const deprecation = ts.Debug.deprecate(ts.noop, { - error: true, - }); - let logWritten = false; - ts.Debug.loggingHost = { - log() { - logWritten = true; - } - }; - expect(deprecation).throws(); - assert.isFalse(logWritten); + let logWritten = false; + ts.Debug.loggingHost = { + log() { + logWritten = true; + } + }; + expect(deprecation).throws(); + assert.isFalse(logWritten); + }); + it("error deprecation with error", () => { + const deprecation = ts.Debug.deprecate(ts.noop, { + error: true, }); + let logWritten = false; + ts.Debug.loggingHost = { + log() { + logWritten = true; + } + }; + expect(deprecation).throws(); + assert.isFalse(logWritten); }); }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/evaluation/externalModules.ts b/src/testRunner/unittests/evaluation/externalModules.ts index 18bb0fa043f80..8febc2a6704a4 100644 --- a/src/testRunner/unittests/evaluation/externalModules.ts +++ b/src/testRunner/unittests/evaluation/externalModules.ts @@ -81,4 +81,4 @@ describe("unittests:: evaluation:: externalModules", () => { assert.equal(result.output[2], true); // `f.call(obj, obj)`. Behavior of `.call` (or `.apply`, etc.) should not be affected. assert.equal(result.output[3], true); // `other.f(other)`. `this` is still namespace because it is left of `.`. }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/forOf.ts b/src/testRunner/unittests/evaluation/forOf.ts index 9efdf16d351cd..3df4c762da2b9 100644 --- a/src/testRunner/unittests/evaluation/forOf.ts +++ b/src/testRunner/unittests/evaluation/forOf.ts @@ -116,4 +116,4 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { result.main(); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/superInStaticInitializer.ts b/src/testRunner/unittests/evaluation/superInStaticInitializer.ts index da1dc5fe888d9..73e591e78e553 100644 --- a/src/testRunner/unittests/evaluation/superInStaticInitializer.ts +++ b/src/testRunner/unittests/evaluation/superInStaticInitializer.ts @@ -188,4 +188,4 @@ describe("unittests:: evaluation:: superInStaticInitializer", () => { const [Derived] = result.main(); assert.strictEqual(Derived.y, 2); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/updateExpressionInModule.ts b/src/testRunner/unittests/evaluation/updateExpressionInModule.ts index 443a1fb71a11b..8cbe776ab69c5 100644 --- a/src/testRunner/unittests/evaluation/updateExpressionInModule.ts +++ b/src/testRunner/unittests/evaluation/updateExpressionInModule.ts @@ -122,4 +122,4 @@ describe("unittests:: evaluation:: updateExpressionInModule", () => { assert.equal(result.a, BigInt(2)); assert.equal(result.b, BigInt(1)); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/factory.ts b/src/testRunner/unittests/factory.ts index f942c09e231e5..e9d96650e7a64 100644 --- a/src/testRunner/unittests/factory.ts +++ b/src/testRunner/unittests/factory.ts @@ -1,76 +1,76 @@ namespace ts { - describe("unittests:: FactoryAPI", () => { - function assertSyntaxKind(node: ts.Node, expected: ts.SyntaxKind) { - assert.strictEqual(node.kind, expected, `Actual: ${ts.Debug.formatSyntaxKind(node.kind)} Expected: ${ts.Debug.formatSyntaxKind(expected)}`); - } - describe("factory.createExportAssignment", () => { - it("parenthesizes default export if necessary", () => { - function checkExpression(expression: ts.Expression) { - const node = ts.factory.createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isExportEquals*/ false, expression); - assertSyntaxKind(node.expression, ts.SyntaxKind.ParenthesizedExpression); - } - - const clazz = ts.factory.createClassExpression(/*decorators*/ undefined, /*modifiers*/ undefined, "C", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - ts.factory.createPropertyDeclaration(/*decorators*/ undefined, [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], "prop", /*questionOrExclamationToken*/ undefined, /*type*/ undefined, ts.factory.createStringLiteral("1")), - ]); - checkExpression(clazz); - checkExpression(ts.factory.createPropertyAccessExpression(clazz, "prop")); - const func = ts.factory.createFunctionExpression(/*modifiers*/ undefined, /*asteriskToken*/ undefined, "fn", /*typeParameters*/ undefined, /*parameters*/ undefined, /*type*/ undefined, ts.factory.createBlock([])); - checkExpression(func); - checkExpression(ts.factory.createCallExpression(func, /*typeArguments*/ undefined, /*argumentsArray*/ undefined)); - checkExpression(ts.factory.createTaggedTemplateExpression(func, /*typeArguments*/ undefined, ts.factory.createNoSubstitutionTemplateLiteral(""))); - checkExpression(ts.factory.createBinaryExpression(ts.factory.createStringLiteral("a"), ts.SyntaxKind.CommaToken, ts.factory.createStringLiteral("b"))); - checkExpression(ts.factory.createCommaListExpression([ts.factory.createStringLiteral("a"), ts.factory.createStringLiteral("b")])); - }); - }); - - describe("factory.createArrowFunction", () => { - it("parenthesizes concise body if necessary", () => { - function checkBody(body: ts.ConciseBody) { - const node = ts.factory.createArrowFunction( - /*modifiers*/ undefined, - /*typeParameters*/ undefined, [], - /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, body); - assertSyntaxKind(node.body, ts.SyntaxKind.ParenthesizedExpression); - } +describe("unittests:: FactoryAPI", () => { + function assertSyntaxKind(node: ts.Node, expected: ts.SyntaxKind) { + assert.strictEqual(node.kind, expected, `Actual: ${ts.Debug.formatSyntaxKind(node.kind)} Expected: ${ts.Debug.formatSyntaxKind(expected)}`); + } + describe("factory.createExportAssignment", () => { + it("parenthesizes default export if necessary", () => { + function checkExpression(expression: ts.Expression) { + const node = ts.factory.createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isExportEquals*/ false, expression); + assertSyntaxKind(node.expression, ts.SyntaxKind.ParenthesizedExpression); + } - checkBody(ts.factory.createObjectLiteralExpression()); - checkBody(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop")); - checkBody(ts.factory.createAsExpression(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop"), ts.factory.createTypeReferenceNode("T", /*typeArguments*/ undefined))); - checkBody(ts.factory.createNonNullExpression(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop"))); - checkBody(ts.factory.createCommaListExpression([ts.factory.createStringLiteral("a"), ts.factory.createStringLiteral("b")])); - checkBody(ts.factory.createBinaryExpression(ts.factory.createStringLiteral("a"), ts.SyntaxKind.CommaToken, ts.factory.createStringLiteral("b"))); - }); + const clazz = ts.factory.createClassExpression(/*decorators*/ undefined, /*modifiers*/ undefined, "C", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + ts.factory.createPropertyDeclaration(/*decorators*/ undefined, [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], "prop", /*questionOrExclamationToken*/ undefined, /*type*/ undefined, ts.factory.createStringLiteral("1")), + ]); + checkExpression(clazz); + checkExpression(ts.factory.createPropertyAccessExpression(clazz, "prop")); + const func = ts.factory.createFunctionExpression(/*modifiers*/ undefined, /*asteriskToken*/ undefined, "fn", /*typeParameters*/ undefined, /*parameters*/ undefined, /*type*/ undefined, ts.factory.createBlock([])); + checkExpression(func); + checkExpression(ts.factory.createCallExpression(func, /*typeArguments*/ undefined, /*argumentsArray*/ undefined)); + checkExpression(ts.factory.createTaggedTemplateExpression(func, /*typeArguments*/ undefined, ts.factory.createNoSubstitutionTemplateLiteral(""))); + checkExpression(ts.factory.createBinaryExpression(ts.factory.createStringLiteral("a"), ts.SyntaxKind.CommaToken, ts.factory.createStringLiteral("b"))); + checkExpression(ts.factory.createCommaListExpression([ts.factory.createStringLiteral("a"), ts.factory.createStringLiteral("b")])); }); + }); - describe("createBinaryExpression", () => { - it("parenthesizes arrow function in RHS if necessary", () => { - const lhs = ts.factory.createIdentifier("foo"); - const rhs = ts.factory.createArrowFunction( + describe("factory.createArrowFunction", () => { + it("parenthesizes concise body if necessary", () => { + function checkBody(body: ts.ConciseBody) { + const node = ts.factory.createArrowFunction( /*modifiers*/ undefined, /*typeParameters*/ undefined, [], /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, ts.factory.createBlock([])); - function checkRhs(operator: ts.BinaryOperator, expectParens: boolean) { - const node = ts.factory.createBinaryExpression(lhs, operator, rhs); - assertSyntaxKind(node.right, expectParens ? ts.SyntaxKind.ParenthesizedExpression : ts.SyntaxKind.ArrowFunction); - } + /*equalsGreaterThanToken*/ undefined, body); + assertSyntaxKind(node.body, ts.SyntaxKind.ParenthesizedExpression); + } + + checkBody(ts.factory.createObjectLiteralExpression()); + checkBody(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop")); + checkBody(ts.factory.createAsExpression(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop"), ts.factory.createTypeReferenceNode("T", /*typeArguments*/ undefined))); + checkBody(ts.factory.createNonNullExpression(ts.factory.createPropertyAccessExpression(ts.factory.createObjectLiteralExpression(), "prop"))); + checkBody(ts.factory.createCommaListExpression([ts.factory.createStringLiteral("a"), ts.factory.createStringLiteral("b")])); + checkBody(ts.factory.createBinaryExpression(ts.factory.createStringLiteral("a"), ts.SyntaxKind.CommaToken, ts.factory.createStringLiteral("b"))); + }); + }); + + describe("createBinaryExpression", () => { + it("parenthesizes arrow function in RHS if necessary", () => { + const lhs = ts.factory.createIdentifier("foo"); + const rhs = ts.factory.createArrowFunction( + /*modifiers*/ undefined, + /*typeParameters*/ undefined, [], + /*type*/ undefined, + /*equalsGreaterThanToken*/ undefined, ts.factory.createBlock([])); + function checkRhs(operator: ts.BinaryOperator, expectParens: boolean) { + const node = ts.factory.createBinaryExpression(lhs, operator, rhs); + assertSyntaxKind(node.right, expectParens ? ts.SyntaxKind.ParenthesizedExpression : ts.SyntaxKind.ArrowFunction); + } - checkRhs(ts.SyntaxKind.CommaToken, /*expectParens*/ false); - checkRhs(ts.SyntaxKind.EqualsToken, /*expectParens*/ false); - checkRhs(ts.SyntaxKind.PlusEqualsToken, /*expectParens*/ false); - checkRhs(ts.SyntaxKind.BarBarToken, /*expectParens*/ true); - checkRhs(ts.SyntaxKind.AmpersandAmpersandToken, /*expectParens*/ true); - checkRhs(ts.SyntaxKind.QuestionQuestionToken, /*expectParens*/ true); - checkRhs(ts.SyntaxKind.EqualsEqualsToken, /*expectParens*/ true); - checkRhs(ts.SyntaxKind.BarBarEqualsToken, /*expectParens*/ false); - checkRhs(ts.SyntaxKind.AmpersandAmpersandEqualsToken, /*expectParens*/ false); - checkRhs(ts.SyntaxKind.QuestionQuestionEqualsToken, /*expectParens*/ false); - }); + checkRhs(ts.SyntaxKind.CommaToken, /*expectParens*/ false); + checkRhs(ts.SyntaxKind.EqualsToken, /*expectParens*/ false); + checkRhs(ts.SyntaxKind.PlusEqualsToken, /*expectParens*/ false); + checkRhs(ts.SyntaxKind.BarBarToken, /*expectParens*/ true); + checkRhs(ts.SyntaxKind.AmpersandAmpersandToken, /*expectParens*/ true); + checkRhs(ts.SyntaxKind.QuestionQuestionToken, /*expectParens*/ true); + checkRhs(ts.SyntaxKind.EqualsEqualsToken, /*expectParens*/ true); + checkRhs(ts.SyntaxKind.BarBarEqualsToken, /*expectParens*/ false); + checkRhs(ts.SyntaxKind.AmpersandAmpersandEqualsToken, /*expectParens*/ false); + checkRhs(ts.SyntaxKind.QuestionQuestionEqualsToken, /*expectParens*/ false); }); }); +}); } diff --git a/src/testRunner/unittests/incrementalParser.ts b/src/testRunner/unittests/incrementalParser.ts index e4b907580160e..15ee7749c254e 100644 --- a/src/testRunner/unittests/incrementalParser.ts +++ b/src/testRunner/unittests/incrementalParser.ts @@ -1,849 +1,849 @@ namespace ts { - function withChange(text: ts.IScriptSnapshot, start: number, length: number, newText: string): { - text: ts.IScriptSnapshot; - textChangeRange: ts.TextChangeRange; - } { - const contents = ts.getSnapshotText(text); - const newContents = contents.substr(0, start) + newText + contents.substring(start + length); - - return { text: ts.ScriptSnapshot.fromString(newContents), textChangeRange: ts.createTextChangeRange(ts.createTextSpan(start, length), newText.length) }; - } +function withChange(text: ts.IScriptSnapshot, start: number, length: number, newText: string): { + text: ts.IScriptSnapshot; + textChangeRange: ts.TextChangeRange; +} { + const contents = ts.getSnapshotText(text); + const newContents = contents.substr(0, start) + newText + contents.substring(start + length); + + return { text: ts.ScriptSnapshot.fromString(newContents), textChangeRange: ts.createTextChangeRange(ts.createTextSpan(start, length), newText.length) }; +} - function withInsert(text: ts.IScriptSnapshot, start: number, newText: string): { - text: ts.IScriptSnapshot; - textChangeRange: ts.TextChangeRange; - } { - return withChange(text, start, 0, newText); - } +function withInsert(text: ts.IScriptSnapshot, start: number, newText: string): { + text: ts.IScriptSnapshot; + textChangeRange: ts.TextChangeRange; +} { + return withChange(text, start, 0, newText); +} - function withDelete(text: ts.IScriptSnapshot, start: number, length: number): { - text: ts.IScriptSnapshot; - textChangeRange: ts.TextChangeRange; - } { - return withChange(text, start, length, ""); - } +function withDelete(text: ts.IScriptSnapshot, start: number, length: number): { + text: ts.IScriptSnapshot; + textChangeRange: ts.TextChangeRange; +} { + return withChange(text, start, length, ""); +} - function createTree(text: ts.IScriptSnapshot, version: string) { - return ts.createLanguageServiceSourceFile(/*fileName:*/ "", text, ts.ScriptTarget.Latest, version, /*setNodeParents:*/ true); - } +function createTree(text: ts.IScriptSnapshot, version: string) { + return ts.createLanguageServiceSourceFile(/*fileName:*/ "", text, ts.ScriptTarget.Latest, version, /*setNodeParents:*/ true); +} - function assertSameDiagnostics(file1: ts.SourceFile, file2: ts.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 assertSameDiagnostics(file1: ts.SourceFile, file2: ts.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: ts.IScriptSnapshot, newText: ts.IScriptSnapshot, textChangeRange: ts.TextChangeRange, expectedReusedElements: number, oldTree?: ts.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); +// 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: ts.IScriptSnapshot, newText: ts.IScriptSnapshot, textChangeRange: ts.TextChangeRange, expectedReusedElements: number, oldTree?: ts.SourceFile) { + oldTree = oldTree || createTree(oldText, /*version:*/ "."); + Utils.assertInvariants(oldTree, /*parent:*/ undefined); - // Create a tree for the new text, in an incremental fashion. - const incrementalNewTree = ts.updateLanguageServiceSourceFile(oldTree, newText, oldTree.version + ".", textChangeRange); - Utils.assertInvariants(incrementalNewTree, /*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); - // We should get the same tree when doign a full or incremental parse. - Utils.assertStructuralEquals(newTree, incrementalNewTree); + // Create a tree for the new text, in an incremental fashion. + const incrementalNewTree = ts.updateLanguageServiceSourceFile(oldTree, newText, oldTree.version + ".", textChangeRange); + Utils.assertInvariants(incrementalNewTree, /*parent:*/ undefined); - // We should also get the exact same set of diagnostics. - assertSameDiagnostics(newTree, incrementalNewTree); + // We should get the same tree when doign a full or incremental parse. + Utils.assertStructuralEquals(newTree, incrementalNewTree); - // There should be no reused nodes between two trees that are fully parsed. - assert.isTrue(reusedElements(oldTree, newTree) === 0); + // We should also get the exact same set of diagnostics. + assertSameDiagnostics(newTree, incrementalNewTree); - assert.equal(newTree.fileName, incrementalNewTree.fileName, "newTree.fileName !== incrementalNewTree.fileName"); - assert.equal(newTree.text, incrementalNewTree.text, "newTree.text !== incrementalNewTree.text"); + // There should be no reused nodes between two trees that are fully parsed. + assert.isTrue(reusedElements(oldTree, newTree) === 0); - if (expectedReusedElements !== -1) { - const actualReusedCount = reusedElements(oldTree, incrementalNewTree); - assert.equal(actualReusedCount, expectedReusedElements, actualReusedCount + " !== " + expectedReusedElements); - } + assert.equal(newTree.fileName, incrementalNewTree.fileName, "newTree.fileName !== incrementalNewTree.fileName"); + assert.equal(newTree.text, incrementalNewTree.text, "newTree.text !== incrementalNewTree.text"); - return { oldTree, newTree, incrementalNewTree }; + if (expectedReusedElements !== -1) { + const actualReusedCount = reusedElements(oldTree, incrementalNewTree); + assert.equal(actualReusedCount, expectedReusedElements, actualReusedCount + " !== " + expectedReusedElements); } - function reusedElements(oldNode: ts.SourceFile, newNode: ts.SourceFile): number { - const allOldElements = collectElements(oldNode); - const allNewElements = collectElements(newNode); + return { oldTree, newTree, incrementalNewTree }; +} + +function reusedElements(oldNode: ts.SourceFile, newNode: ts.SourceFile): number { + const allOldElements = collectElements(oldNode); + const allNewElements = collectElements(newNode); - return ts.filter(allOldElements, v => ts.contains(allNewElements, v)).length; - } + return ts.filter(allOldElements, v => ts.contains(allNewElements, v)).length; +} - function collectElements(node: ts.Node) { - const result: ts.Node[] = []; - visit(node); - return result; +function collectElements(node: ts.Node) { + const result: ts.Node[] = []; + visit(node); + return result; - function visit(node: ts.Node) { - result.push(node); - ts.forEachChild(node, visit); - } + function visit(node: ts.Node) { + result.push(node); + ts.forEachChild(node, visit); } +} + +function deleteCode(source: string, index: number, toDelete: string) { + const repeat = toDelete.length; + let oldTree = createTree(ts.ScriptSnapshot.fromString(source), /*version:*/ "."); + for (let i = 0; i < repeat; i++) { + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); + const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; - function deleteCode(source: string, index: number, toDelete: string) { - const repeat = toDelete.length; - let oldTree = createTree(ts.ScriptSnapshot.fromString(source), /*version:*/ "."); - for (let i = 0; i < repeat; i++) { - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); - const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; - - source = ts.getSnapshotText(newTextAndChange.text); - oldTree = newTree; - } + source = ts.getSnapshotText(newTextAndChange.text); + oldTree = newTree; } +} - function insertCode(source: string, index: number, toInsert: string) { - const repeat = toInsert.length; - let oldTree = createTree(ts.ScriptSnapshot.fromString(source), /*version:*/ "."); - for (let i = 0; i < repeat; i++) { - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + i, toInsert.charAt(i)); - const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; - - source = ts.getSnapshotText(newTextAndChange.text); - oldTree = newTree; - } +function insertCode(source: string, index: number, toInsert: string) { + const repeat = toInsert.length; + let oldTree = createTree(ts.ScriptSnapshot.fromString(source), /*version:*/ "."); + for (let i = 0; i < repeat; i++) { + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + i, toInsert.charAt(i)); + const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; + + source = ts.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 = ts.ScriptSnapshot.fromString(source); - const semicolonIndex = source.indexOf(";"); - const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); +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 = ts.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" + - "}"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, "+ 1".length); + const index = source.indexOf("+ 1"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, "+ 1".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); + }); - it("Regular expression 1", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1;} public foo3() { } }"; + it("Regular expression 1", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1;} public foo3() { } }"; - const semicolonIndex = source.indexOf(";}"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + const semicolonIndex = source.indexOf(";}"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Regular expression 2", () => { - const source = "class C { public foo1() { ; } public foo2() { return 1/;} public foo3() { } }"; + it("Regular expression 2", () => { + const source = "class C { public foo1() { ; } public foo2() { return 1/;} public foo3() { } }"; - const semicolonIndex = source.indexOf(";"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + const semicolonIndex = source.indexOf(";"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); - it("Comment 1", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + it("Comment 1", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - const semicolonIndex = source.indexOf(";"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + const semicolonIndex = source.indexOf(";"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Comment 2", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + it("Comment 2", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "//"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "//"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Comment 3", () => { - const source = "//class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + it("Comment 3", () => { + const source = "//class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, 2); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, 2); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Comment 4", () => { - const source = "class C { public foo1() { /; } public foo2() { */ return 1; } public foo3() { } }"; + it("Comment 4", () => { + const source = "class C { public foo1() { /; } public foo2() { */ return 1; } public foo3() { } }"; - const index = source.indexOf(";"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "*"); + const index = source.indexOf(";"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, "*"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + 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" + - "}"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); + const semicolonIndex = source.indexOf(";"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "?"); + const index = source.indexOf(": string"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, "?"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 2, "+"); + const index = source.indexOf("<<"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 2, "+"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); + }); - it("Strict mode 1", () => { - const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; + it("Strict mode 1", () => { + const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "'strict';\r\n"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "'strict';\r\n"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); - it("Strict mode 2", () => { - const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; + it("Strict mode 2", () => { + const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "'use strict';\r\n"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "'use strict';\r\n"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); - it("Strict mode 3", () => { - const source = "'strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; + it("Strict mode 3", () => { + const source = "'strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; - const index = source.indexOf("f"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); + const index = source.indexOf("f"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); - it("Strict mode 4", () => { - const source = "'use strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; + it("Strict mode 4", () => { + const source = "'use strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; - const index = source.indexOf("f"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); + const index = source.indexOf("f"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + 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"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 6, "strict"); + const index = source.indexOf("b"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 6, "strict"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); - }); + 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"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 6, "blahhh"); + const index = source.indexOf("s"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 6, "blahhh"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); - }); + 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"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); + const index = source.indexOf("f"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); + }); - it("Parenthesized expression to arrow function 1", () => { - const source = "var v = (a, b, c, d, e)"; + it("Parenthesized expression to arrow function 1", () => { + const source = "var v = (a, b, c, d, e)"; - const index = source.indexOf("a"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ":"); + const index = source.indexOf("a"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ":"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Parenthesized expression to arrow function 2", () => { - const source = "var v = (a, b) = c"; + it("Parenthesized expression to arrow function 2", () => { + const source = "var v = (a, b) = c"; - const index = source.indexOf("= c") + 1; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, ">"); + const index = source.indexOf("= c") + 1; + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, ">"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Arrow function to parenthesized expression 1", () => { - const source = "var v = (a:, b, c, d, e)"; + it("Arrow function to parenthesized expression 1", () => { + const source = "var v = (a:, b, c, d, e)"; - const index = source.indexOf(":"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); + const index = source.indexOf(":"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Arrow function to parenthesized expression 2", () => { - const source = "var v = (a, b) => c"; + it("Arrow function to parenthesized expression 2", () => { + const source = "var v = (a, b) => c"; - const index = source.indexOf(">"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); + const index = source.indexOf(">"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Speculative generic lookahead 1", () => { - const source = "var v = Fe"; + it("Speculative generic lookahead 1", () => { + const source = "var v = Fe"; - const index = source.indexOf("b"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); + const index = source.indexOf("b"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); - it("Speculative generic lookahead 2", () => { - const source = "var v = Fe"; + it("Speculative generic lookahead 2", () => { + const source = "var v = Fe"; - const index = source.indexOf("b"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); + const index = source.indexOf("b"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); - it("Speculative generic lookahead 3", () => { - const source = "var v = Fe"; + it("Speculative generic lookahead 3", () => { + const source = "var v = Fe"; - const index = source.indexOf("b"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); + const index = source.indexOf("b"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); - it("Speculative generic lookahead 4", () => { - const source = "var v = Fe"; + it("Speculative generic lookahead 4", () => { + const source = "var v = Fe"; - const index = source.indexOf("b"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); + const index = source.indexOf("b"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); - it("Assertion to arrow function", () => { - const source = "var v = (a);"; + it("Assertion to arrow function", () => { + const source = "var v = (a);"; - const index = source.indexOf(";"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, " => 1"); + const index = source.indexOf(";"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, " => 1"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Arrow function to assertion", () => { - const source = "var v = (a) => 1;"; + it("Arrow function to assertion", () => { + const source = "var v = (a) => 1;"; - const index = source.indexOf(" =>"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, " => 1".length); + const index = source.indexOf(" =>"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, " => 1".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Contextual shift to shift-equals", () => { - const source = "var v = 1 >> = 2"; + it("Contextual shift to shift-equals", () => { + const source = "var v = 1 >> = 2"; - const index = source.indexOf(">> ="); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index + 2, 1); + const index = source.indexOf(">> ="); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index + 2, 1); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Contextual shift-equals to shift", () => { - const source = "var v = 1 >>= 2"; + it("Contextual shift-equals to shift", () => { + const source = "var v = 1 >>= 2"; - const index = source.indexOf(">>="); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 2, " "); + const index = source.indexOf(">>="); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 2, " "); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Contextual shift to generic invocation", () => { - const source = "var v = T>>(2)"; + it("Contextual shift to generic invocation", () => { + const source = "var v = T>>(2)"; - const index = source.indexOf("T"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "Foo { - const source = "var v = Foo>(2)"; + it("Test generic invocation to contextual shift", () => { + const source = "var v = Foo>(2)"; - const index = source.indexOf("Foo { - const source = "var v = T>>=2;"; + it("Contextual shift to generic type and initializer", () => { + const source = "var v = T>>=2;"; - const index = source.indexOf("="); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, "= ".length, ": Foo { - const source = "var v : Foo>=2;"; + it("Generic type and initializer to contextual shift", () => { + const source = "var v : Foo>=2;"; - const index = source.indexOf(":"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, ": Foo { - const source = "var v = new Dictionary0"; + it("Arithmetic operator to type argument list", () => { + const source = "var v = new Dictionary0"; - const index = source.indexOf("0"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 1, "()"); + const index = source.indexOf("0"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 1, "()"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Type argument list to arithmetic operator", () => { - const source = "var v = new Dictionary()"; + it("Type argument list to arithmetic operator", () => { + const source = "var v = new Dictionary()"; - const index = source.indexOf("()"); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 2); + const index = source.indexOf("()"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 2); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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}"; + 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 = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("foo"); - const newTextAndChange = withInsert(oldText, index, "*"); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("foo"); + const newTextAndChange = withInsert(oldText, index, "*"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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}"; + 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 = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("*"); - const newTextAndChange = withDelete(oldText, index, "*".length); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("*"); + const newTextAndChange = withDelete(oldText, index, "*".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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"; + 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 = ts.ScriptSnapshot.fromString(source); - const index = source.lastIndexOf(";"); - const newTextAndChange = withDelete(oldText, index, 1); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.lastIndexOf(";"); + const newTextAndChange = withDelete(oldText, index, 1); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); + }); - it("Edit after empty type parameter list", () => { - const source = "class Dictionary<> { }\r\nvar y;\r\n"; + it("Edit after empty type parameter list", () => { + const source = "class Dictionary<> { }\r\nvar y;\r\n"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.length; - const newTextAndChange = withInsert(oldText, index, "var x;"); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.length; + const newTextAndChange = withInsert(oldText, index, "var x;"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 2); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 2); + }); - it("Delete parameter after comment", () => { - const source = "function fn(/* comment! */ a: number, c) { }"; + it("Delete parameter after comment", () => { + const source = "function fn(/* comment! */ a: number, c) { }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("a:"); - const newTextAndChange = withDelete(oldText, index, "a: number,".length); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("a:"); + const newTextAndChange = withDelete(oldText, index, "a: number,".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Modifier added to accessor", () => { - const source = "class C {\ + it("Modifier added to accessor", () => { + const source = "class C {\ set Bar(bar:string) {}\ }\ var o2 = { set Foo(val:number) { } };"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("set"); - const newTextAndChange = withInsert(oldText, index, "public "); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("set"); + const newTextAndChange = withInsert(oldText, index, "public "); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); + }); - it("Insert parameter ahead of parameter", () => { - const source = "alert(100);\ + it("Insert parameter ahead of parameter", () => { + const source = "alert(100);\ \ class OverloadedMonster {\ constructor();\ constructor(name) { }\ }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("100"); - const newTextAndChange = withInsert(oldText, index, "'1', "); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("100"); + const newTextAndChange = withInsert(oldText, index, "'1', "); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); + }); - it("Insert declare modifier before module", () => { - const source = "module mAmbient {\ + it("Insert declare modifier before module", () => { + const source = "module mAmbient {\ module m3 { }\ }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = 0; - const newTextAndChange = withInsert(oldText, index, "declare "); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = 0; + const newTextAndChange = withInsert(oldText, index, "declare "); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Insert function above arrow function with comment", () => { - const source = "\ + it("Insert function above arrow function with comment", () => { + const source = "\ () =>\ // do something\ 0;"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = 0; - const newTextAndChange = withInsert(oldText, index, "function Foo() { }"); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = 0; + const newTextAndChange = withInsert(oldText, index, "function Foo() { }"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Finish incomplete regular expression", () => { - const source = "while (true) /3; return;"; + it("Finish incomplete regular expression", () => { + const source = "while (true) /3; return;"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.length - 1; - const newTextAndChange = withInsert(oldText, index, "/"); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.length - 1; + const newTextAndChange = withInsert(oldText, index, "/"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Regular expression to divide operation", () => { - const source = "return;\r\nwhile (true) /3/g;"; + it("Regular expression to divide operation", () => { + const source = "return;\r\nwhile (true) /3/g;"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("while"); - const newTextAndChange = withDelete(oldText, index, "while ".length); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("while"); + const newTextAndChange = withDelete(oldText, index, "while ".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Divide operation to regular expression", () => { - const source = "return;\r\n(true) /3/g;"; + it("Divide operation to regular expression", () => { + const source = "return;\r\n(true) /3/g;"; - const oldText = ts.ScriptSnapshot.fromString(source); - const index = source.indexOf("("); - const newTextAndChange = withInsert(oldText, index, "while "); + const oldText = ts.ScriptSnapshot.fromString(source); + const index = source.indexOf("("); + const newTextAndChange = withInsert(oldText, index, "while "); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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 /*"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, ""); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, ""); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); - }); + 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 }"; + it("Class to interface", () => { + const source = "class A { public M1() { } public M2() { } public M3() { } p1 = 0; p2 = 0; p3 = 0 }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); - it("Interface to class", () => { - const source = "interface A { M1?(); M2?(); M3?(); p1?; p2?; p3? }"; + it("Interface to class", () => { + const source = "interface A { M1?(); M2?(); M3?(); p1?; p2?; p3? }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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() { }"; + it("Surrounding function declarations with block", () => { + const source = "declare function F1() { } export function F2() { } declare export function F3() { }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "{"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "{"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + 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() { }"; + it("Removing block around function declarations", () => { + const source = "{ declare function F1() { } export function F2() { } declare export function F3() { }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, "{".length); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, "{".length); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); + 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() { } }"; + it("Moving methods from class to object literal", () => { + const source = "class C { public A() { } public B() { } public C() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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() { } }"; + it("Moving methods from object literal to class", () => { + const source = "var v = { public A() { } public B() { } public C() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + 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() { } }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + 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() { } }"; + it("Do not move constructors from class to object-literal.", () => { + const source = "class C { public constructor() { } public constructor() { } public constructor() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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() { } }"; + it("Do not move methods called \"constructor\" from object literal to class", () => { + const source = "var v = { public constructor() { } public constructor() { } public constructor() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "class".length, "interface"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "class".length, "interface"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); + 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 }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "interface".length, "class"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "interface".length, "class"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); + 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() { } }"; + it("Moving accessors from class to object literal", () => { + const source = "class C { public get A() { } public get B() { } public get C() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); + 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() { } }"; + it("Moving accessors from object literal to class", () => { + const source = "var v = { public get A() { } public get B() { } public get C() { } }"; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + 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() { } }"; + 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 = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); - it("Reuse transformFlags of subtree during bind", () => { - const source = `class Greeter { constructor(element: HTMLElement) { } }`; - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 15, 0, "\n"); - const { oldTree, incrementalNewTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - ts.bindSourceFile(oldTree, {}); - ts.bindSourceFile(incrementalNewTree, {}); - assert.equal(oldTree.transformFlags, incrementalNewTree.transformFlags); - }); + it("Reuse transformFlags of subtree during bind", () => { + const source = `class Greeter { constructor(element: HTMLElement) { } }`; + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 15, 0, "\n"); + const { oldTree, incrementalNewTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + ts.bindSourceFile(oldTree, {}); + ts.bindSourceFile(incrementalNewTree, {}); + assert.equal(oldTree.transformFlags, incrementalNewTree.transformFlags); + }); - // Simulated typing tests. + // Simulated typing tests. - it("Type extends clause 1", () => { - const source = "interface IFoo { }\r\ninterface Array extends IFoo { }"; + it("Type extends clause 1", () => { + const source = "interface IFoo { }\r\ninterface Array extends IFoo { }"; - const index = source.indexOf("extends"); - deleteCode(source, index, "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"); - }); + 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"); + }); - for (const tsIgnoreComment of [ - "// @ts-ignore", - "/* @ts-ignore */", - "/*\n @ts-ignore */" - ]) { - describe(`${tsIgnoreComment} comment directives`, () => { - const textWithIgnoreComment = `const x = 10; + for (const tsIgnoreComment of [ + "// @ts-ignore", + "/* @ts-ignore */", + "/*\n @ts-ignore */" + ]) { + describe(`${tsIgnoreComment} comment directives`, () => { + const textWithIgnoreComment = `const x = 10; function foo() { ${tsIgnoreComment} let y: string = x; @@ -862,107 +862,107 @@ module m3 { }\ foo(); bar(); bar3();`; - verifyScenario("when deleting ts-ignore comment", verifyDelete); - verifyScenario("when inserting ts-ignore comment", verifyInsert); - verifyScenario("when changing ts-ignore comment to blah", verifyChangeToBlah); - verifyScenario("when changing blah comment to ts-ignore", verifyChangeBackToDirective); - verifyScenario("when deleting blah comment", verifyDeletingBlah); - verifyScenario("when changing text that adds another comment", verifyChangeDirectiveType); - verifyScenario("when changing text that keeps the comment but adds more nodes", verifyReuseChange); - - function verifyCommentDirectives(oldText: ts.IScriptSnapshot, newTextAndChange: { - text: ts.IScriptSnapshot; - textChangeRange: ts.TextChangeRange; - }) { - const { incrementalNewTree, newTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - assert.deepEqual(incrementalNewTree.commentDirectives, newTree.commentDirectives); - } - - function verifyScenario(scenario: string, verifyChange: (atIndex: number, singleIgnore?: true) => void) { - it(`${scenario} - 0`, () => { - verifyChange(0); - }); - it(`${scenario} - 1`, () => { - verifyChange(1); - }); - it(`${scenario} - 2`, () => { - verifyChange(2); - }); - it(`${scenario} - with single ts-ignore`, () => { - verifyChange(0, /*singleIgnore*/ true); - }); - } - - function getIndexOfTsIgnoreComment(atIndex: number) { - let index = 0; - for (let i = 0; i <= atIndex; i++) { - index = textWithIgnoreComment.indexOf(tsIgnoreComment, index); - } - return index; - } - - function textWithIgnoreCommentFrom(text: string, singleIgnore: true | undefined) { - if (!singleIgnore) - return text; - const splits = text.split(tsIgnoreComment); - if (splits.length > 2) { - const tail = splits[splits.length - 2] + splits[splits.length - 1]; - splits.length = splits.length - 2; - return splits.join(tsIgnoreComment) + tail; - } - else { - return splits.join(tsIgnoreComment); - } - } - - function verifyDelete(atIndex: number, singleIgnore?: true) { - const index = getIndexOfTsIgnoreComment(atIndex); - const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); - const newTextAndChange = withDelete(oldText, index, tsIgnoreComment.length); - verifyCommentDirectives(oldText, newTextAndChange); - } - - function verifyInsert(atIndex: number, singleIgnore?: true) { - const index = getIndexOfTsIgnoreComment(atIndex); - const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + textWithIgnoreComment.slice(index + tsIgnoreComment.length), singleIgnore); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, tsIgnoreComment); - verifyCommentDirectives(oldText, newTextAndChange); + verifyScenario("when deleting ts-ignore comment", verifyDelete); + verifyScenario("when inserting ts-ignore comment", verifyInsert); + verifyScenario("when changing ts-ignore comment to blah", verifyChangeToBlah); + verifyScenario("when changing blah comment to ts-ignore", verifyChangeBackToDirective); + verifyScenario("when deleting blah comment", verifyDeletingBlah); + verifyScenario("when changing text that adds another comment", verifyChangeDirectiveType); + verifyScenario("when changing text that keeps the comment but adds more nodes", verifyReuseChange); + + function verifyCommentDirectives(oldText: ts.IScriptSnapshot, newTextAndChange: { + text: ts.IScriptSnapshot; + textChangeRange: ts.TextChangeRange; + }) { + const { incrementalNewTree, newTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + assert.deepEqual(incrementalNewTree.commentDirectives, newTree.commentDirectives); + } + + function verifyScenario(scenario: string, verifyChange: (atIndex: number, singleIgnore?: true) => void) { + it(`${scenario} - 0`, () => { + verifyChange(0); + }); + it(`${scenario} - 1`, () => { + verifyChange(1); + }); + it(`${scenario} - 2`, () => { + verifyChange(2); + }); + it(`${scenario} - with single ts-ignore`, () => { + verifyChange(0, /*singleIgnore*/ true); + }); + } + + function getIndexOfTsIgnoreComment(atIndex: number) { + let index = 0; + for (let i = 0; i <= atIndex; i++) { + index = textWithIgnoreComment.indexOf(tsIgnoreComment, index); } - - function verifyChangeToBlah(atIndex: number, singleIgnore?: true) { - const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@"); - const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); - const newTextAndChange = withChange(oldText, index, 1, "blah "); - verifyCommentDirectives(oldText, newTextAndChange); - } - - function verifyChangeBackToDirective(atIndex: number, singleIgnore?: true) { - const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@"); - const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, "blah ".length, "@"); - verifyCommentDirectives(oldText, newTextAndChange); + return index; + } + + function textWithIgnoreCommentFrom(text: string, singleIgnore: true | undefined) { + if (!singleIgnore) + return text; + const splits = text.split(tsIgnoreComment); + if (splits.length > 2) { + const tail = splits[splits.length - 2] + splits[splits.length - 1]; + splits.length = splits.length - 2; + return splits.join(tsIgnoreComment) + tail; } - - function verifyDeletingBlah(atIndex: number, singleIgnore?: true) { - const tsIgnoreIndex = getIndexOfTsIgnoreComment(atIndex); - const index = tsIgnoreIndex + tsIgnoreComment.indexOf("@"); - const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore); - const oldText = ts.ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, tsIgnoreIndex, tsIgnoreComment.length + "blah".length); - verifyCommentDirectives(oldText, newTextAndChange); - } - - function verifyChangeDirectiveType(atIndex: number, singleIgnore?: true) { - const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("ignore"); - const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); - const newTextAndChange = withChange(oldText, index, "ignore".length, "expect-error"); - verifyCommentDirectives(oldText, newTextAndChange); + else { + return splits.join(tsIgnoreComment); } - - function verifyReuseChange(atIndex: number, singleIgnore?: true) { - const source = `const x = 10; + } + + function verifyDelete(atIndex: number, singleIgnore?: true) { + const index = getIndexOfTsIgnoreComment(atIndex); + const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); + const newTextAndChange = withDelete(oldText, index, tsIgnoreComment.length); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyInsert(atIndex: number, singleIgnore?: true) { + const index = getIndexOfTsIgnoreComment(atIndex); + const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + textWithIgnoreComment.slice(index + tsIgnoreComment.length), singleIgnore); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, tsIgnoreComment); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyChangeToBlah(atIndex: number, singleIgnore?: true) { + const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@"); + const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); + const newTextAndChange = withChange(oldText, index, 1, "blah "); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyChangeBackToDirective(atIndex: number, singleIgnore?: true) { + const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@"); + const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, "blah ".length, "@"); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyDeletingBlah(atIndex: number, singleIgnore?: true) { + const tsIgnoreIndex = getIndexOfTsIgnoreComment(atIndex); + const index = tsIgnoreIndex + tsIgnoreComment.indexOf("@"); + const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore); + const oldText = ts.ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, tsIgnoreIndex, tsIgnoreComment.length + "blah".length); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyChangeDirectiveType(atIndex: number, singleIgnore?: true) { + const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("ignore"); + const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore)); + const newTextAndChange = withChange(oldText, index, "ignore".length, "expect-error"); + verifyCommentDirectives(oldText, newTextAndChange); + } + + function verifyReuseChange(atIndex: number, singleIgnore?: true) { + const source = `const x = 10; function foo1() { const x1 = 10; ${tsIgnoreComment} @@ -987,16 +987,16 @@ module m3 { }\ foo1(); foo2(); foo3();`; - const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(source, singleIgnore)); - const start = source.indexOf(`const x${atIndex + 1}`); - const letStr = `let y${atIndex + 1}: string = x;`; - const end = source.indexOf(letStr) + letStr.length; - const oldSubStr = source.slice(start, end); - const newText = oldSubStr.replace(letStr, `let yn : string = x;`); - const newTextAndChange = withChange(oldText, start, end - start, newText); - verifyCommentDirectives(oldText, newTextAndChange); - } - }); - } - }); + const oldText = ts.ScriptSnapshot.fromString(textWithIgnoreCommentFrom(source, singleIgnore)); + const start = source.indexOf(`const x${atIndex + 1}`); + const letStr = `let y${atIndex + 1}: string = x;`; + const end = source.indexOf(letStr) + letStr.length; + const oldSubStr = source.slice(start, end); + const newText = oldSubStr.replace(letStr, `let yn : string = x;`); + const newTextAndChange = withChange(oldText, start, end - start, newText); + verifyCommentDirectives(oldText, newTextAndChange); + } + }); + } +}); } diff --git a/src/testRunner/unittests/jsDocParsing.ts b/src/testRunner/unittests/jsDocParsing.ts index 18f91443e6d42..2d65c288e47a5 100644 --- a/src/testRunner/unittests/jsDocParsing.ts +++ b/src/testRunner/unittests/jsDocParsing.ts @@ -1,278 +1,278 @@ namespace ts { - describe("unittests:: JSDocParsing", () => { - describe("TypeExpressions", () => { - function parsesCorrectly(name: string, content: string) { - it(name, () => { - const typeAndDiagnostics = ts.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 = ts.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("unittests:: JSDocParsing", () => { + describe("TypeExpressions", () => { + function parsesCorrectly(name: string, content: string) { + it(name, () => { + const typeAndDiagnostics = ts.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)); }); + } - 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 = ts.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 = ts.parseIsolatedJSDocComment(content)!; - if (!comment) { - ts.Debug.fail("Comment failed to parse entirely"); - } - if (comment.diagnostics.length > 0) { - ts.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 = ts.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 = ts.parseIsolatedJSDocComment(content)!; + if (!comment) { + ts.Debug.fail("Comment failed to parse entirely"); + } + if (comment.diagnostics.length > 0) { + ts.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 = ts.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("asteriskAfterPreamble", "/** * @type {number} */"); - parsesCorrectly("typeTag", `/** + 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 } * Inside {@link link text} thing * @param foo See also {@link A.Reference} @@ -291,7 +291,7 @@ oh.no * }, because of the intermediate asterisks. * @author Alfa Romero See my home page: {@link https://example.com} */`); - parsesCorrectly("authorTag", `/** + parsesCorrectly("authorTag", `/** * @author John Doe * @author John Doe unexpected comment * @author 108 <108@actionbutton.net> Video Games Forever @@ -311,56 +311,56 @@ oh.no * want to keep commenting down here, I dunno. */`); - parsesCorrectly("consecutive newline tokens", `/** + parsesCorrectly("consecutive newline tokens", `/** * @example * Some\n\n * text\r\n * with newlines. */`); - parsesCorrectly("Chained tags, no leading whitespace", `/**@a @b @c@d*/`); - parsesCorrectly("Initial star is not a tag", `/***@a*/`); - parsesCorrectly("Initial star space is not a tag", `/*** @a*/`); - parsesCorrectly("Initial email address is not a tag", `/**bill@example.com*/`); - parsesCorrectly("no space before @ is not a new tag", `/** + parsesCorrectly("Chained tags, no leading whitespace", `/**@a @b @c@d*/`); + parsesCorrectly("Initial star is not a tag", `/***@a*/`); + parsesCorrectly("Initial star space is not a tag", `/*** @a*/`); + parsesCorrectly("Initial email address is not a tag", `/**bill@example.com*/`); + parsesCorrectly("no space before @ is not a new tag", `/** * @param this (@is@) * @param fine its@fine @zerowidth *@singlestar **@doublestar */`); - parsesCorrectly("@@ does not start a new tag", `/** + parsesCorrectly("@@ does not start a new tag", `/** * @param this is (@@fine@@and) is one comment */`); - }); }); - describe("getFirstToken", () => { - it("gets jsdoc", () => { - const root = ts.createSourceFile("foo.ts", "/** comment */var a = true;", ts.ScriptTarget.ES5, /*setParentNodes*/ true); - assert.isDefined(root); - assert.equal(root.kind, ts.SyntaxKind.SourceFile); - const first = root.getFirstToken(); - assert.isDefined(first); - assert.equal(first!.kind, ts.SyntaxKind.VarKeyword); - }); + }); + describe("getFirstToken", () => { + it("gets jsdoc", () => { + const root = ts.createSourceFile("foo.ts", "/** comment */var a = true;", ts.ScriptTarget.ES5, /*setParentNodes*/ true); + assert.isDefined(root); + assert.equal(root.kind, ts.SyntaxKind.SourceFile); + const first = root.getFirstToken(); + assert.isDefined(first); + assert.equal(first!.kind, ts.SyntaxKind.VarKeyword); }); - describe("getLastToken", () => { - it("gets jsdoc", () => { - const root = ts.createSourceFile("foo.ts", "var a = true;/** comment */", ts.ScriptTarget.ES5, /*setParentNodes*/ true); - assert.isDefined(root); - const last = root.getLastToken(); - assert.isDefined(last); - assert.equal(last!.kind, ts.SyntaxKind.EndOfFileToken); - }); + }); + describe("getLastToken", () => { + it("gets jsdoc", () => { + const root = ts.createSourceFile("foo.ts", "var a = true;/** comment */", ts.ScriptTarget.ES5, /*setParentNodes*/ true); + assert.isDefined(root); + const last = root.getLastToken(); + assert.isDefined(last); + assert.equal(last!.kind, ts.SyntaxKind.EndOfFileToken); }); - describe("getStart", () => { - it("runs when node with JSDoc but no parent pointers", () => { - const root = ts.createSourceFile("foo.ts", "/** */var a = true;", ts.ScriptTarget.ES5, /*setParentNodes*/ false); - root.statements[0].getStart(root, /*includeJsdocComment*/ true); - }); + }); + describe("getStart", () => { + it("runs when node with JSDoc but no parent pointers", () => { + const root = ts.createSourceFile("foo.ts", "/** */var a = true;", ts.ScriptTarget.ES5, /*setParentNodes*/ false); + root.statements[0].getStart(root, /*includeJsdocComment*/ true); }); - describe("parseIsolatedJSDocComment", () => { - it("doesn't create a 1-element array with missing type parameter in jsDoc", () => { - const doc = ts.parseIsolatedJSDocComment("/**\n @template\n*/"); - assert.equal((doc?.jsDoc.tags?.[0] as ts.JSDocTemplateTag).typeParameters.length, 0); - }); + }); + describe("parseIsolatedJSDocComment", () => { + it("doesn't create a 1-element array with missing type parameter in jsDoc", () => { + const doc = ts.parseIsolatedJSDocComment("/**\n @template\n*/"); + assert.equal((doc?.jsDoc.tags?.[0] as ts.JSDocTemplateTag).typeParameters.length, 0); }); }); +}); } diff --git a/src/testRunner/unittests/jsonParserRecovery.ts b/src/testRunner/unittests/jsonParserRecovery.ts index ba3c8e2ea41d5..0b0d17e5910bf 100644 --- a/src/testRunner/unittests/jsonParserRecovery.ts +++ b/src/testRunner/unittests/jsonParserRecovery.ts @@ -1,24 +1,24 @@ namespace ts { - describe("unittests:: jsonParserRecovery", () => { - function parsesToValidSourceFileWithErrors(name: string, text: string) { - it(name, () => { - const file = ts.parseJsonText(name, text); - assert(file.parseDiagnostics.length, "Should have parse errors"); - Harness.Baseline.runBaseline(`jsonParserRecovery/${name.replace(/[^a-z0-9_-]/ig, "_")}.errors.txt`, Harness.Compiler.getErrorBaseline([{ - content: text, - unitName: name - }], file.parseDiagnostics)); +describe("unittests:: jsonParserRecovery", () => { + function parsesToValidSourceFileWithErrors(name: string, text: string) { + it(name, () => { + const file = ts.parseJsonText(name, text); + assert(file.parseDiagnostics.length, "Should have parse errors"); + Harness.Baseline.runBaseline(`jsonParserRecovery/${name.replace(/[^a-z0-9_-]/ig, "_")}.errors.txt`, Harness.Compiler.getErrorBaseline([{ + content: text, + unitName: name + }], file.parseDiagnostics)); - // Will throw if parse tree does not cover full input text - file.getChildren(); - }); - } + // Will throw if parse tree does not cover full input text + file.getChildren(); + }); + } - parsesToValidSourceFileWithErrors("trailing identifier", "{} blah"); - parsesToValidSourceFileWithErrors("TypeScript code", "interface Foo {} blah"); - parsesToValidSourceFileWithErrors("Two comma-separated objects", "{}, {}"); - parsesToValidSourceFileWithErrors("Two objects", "{} {}"); - parsesToValidSourceFileWithErrors("JSX", ` + parsesToValidSourceFileWithErrors("trailing identifier", "{} blah"); + parsesToValidSourceFileWithErrors("TypeScript code", "interface Foo {} blah"); + parsesToValidSourceFileWithErrors("Two comma-separated objects", "{}, {}"); + parsesToValidSourceFileWithErrors("Two objects", "{} {}"); + parsesToValidSourceFileWithErrors("JSX", ` interface Test {} const Header = () => ( @@ -33,5 +33,5 @@ namespace ts {
)`); - }); +}); } diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index aad8ffdfd870a..0ab1d27686e27 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -1,1324 +1,1324 @@ namespace ts { - export function checkResolvedModule(actual: ts.ResolvedModuleFull | undefined, expected: ts.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"); +export function checkResolvedModule(actual: ts.ResolvedModuleFull | undefined, expected: ts.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: ts.ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ts.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): ts.ResolvedModuleFull { - return { resolvedFileName, extension: ts.extensionFromPath(resolvedFileName), isExternalLibraryImport }; - } + 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; +} - interface File { - name: string; - content?: string; - symlinks?: string[]; - } +export function checkResolvedModuleWithFailedLookupLocations(actual: ts.ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ts.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}`); +} - function createModuleResolutionHost(hasDirectoryExists: boolean, ...files: File[]): ts.ModuleResolutionHost { - const map = new ts.Map(); - for (const file of files) { - map.set(file.name, file); - if (file.symlinks) { - for (const symlink of file.symlinks) { - map.set(symlink, file); - } +export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport = false): ts.ResolvedModuleFull { + return { resolvedFileName, extension: ts.extensionFromPath(resolvedFileName), isExternalLibraryImport }; +} + +interface File { + name: string; + content?: string; + symlinks?: string[]; +} + +function createModuleResolutionHost(hasDirectoryExists: boolean, ...files: File[]): ts.ModuleResolutionHost { + const map = new ts.Map(); + 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 = new ts.Map(); - for (const f of files) { - let name = ts.getDirectoryPath(f.name); - while (true) { - directories.set(name, name); - const baseName = ts.getDirectoryPath(name); - if (baseName === name) { - break; - } - name = baseName; + if (hasDirectoryExists) { + const directories = new ts.Map(); + for (const f of files) { + let name = ts.getDirectoryPath(f.name); + while (true) { + directories.set(name, name); + const baseName = ts.getDirectoryPath(name); + if (baseName === name) { + break; } + name = baseName; } - return { - readFile, - realpath, - directoryExists: path => directories.has(path), - fileExists: path => { - assert.isTrue(directories.has(ts.getDirectoryPath(path)), `'fileExists' '${path}' request in non-existing directory`); - return map.has(path); - }, - useCaseSensitiveFileNames: true - }; - } - else { - return { readFile, realpath, fileExists: path => map.has(path), useCaseSensitiveFileNames: true }; - } - 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; } + return { + readFile, + realpath, + directoryExists: path => directories.has(path), + fileExists: path => { + assert.isTrue(directories.has(ts.getDirectoryPath(path)), `'fileExists' '${path}' request in non-existing directory`); + return map.has(path); + }, + useCaseSensitiveFileNames: true + }; } + else { + return { readFile, realpath, fileExists: path => map.has(path), useCaseSensitiveFileNames: true }; + } + 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", () => { - // node module resolution does _not_ implicitly append these extensions to an extensionless path (though will still attempt to load them if explicitly) - const nonImplicitExtensions = [ts.Extension.Mts, ts.Extension.Dmts, ts.Extension.Mjs, ts.Extension.Cts, ts.Extension.Dcts, ts.Extension.Cjs]; - const autoExtensions = ts.filter(ts.supportedTSExtensionsFlat, e => nonImplicitExtensions.indexOf(e) === -1); - function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { - for (const ext of autoExtensions) { - test(ext, /*hasDirectoryExists*/ false); - test(ext, /*hasDirectoryExists*/ true); - } +describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => { + // node module resolution does _not_ implicitly append these extensions to an extensionless path (though will still attempt to load them if explicitly) + const nonImplicitExtensions = [ts.Extension.Mts, ts.Extension.Dmts, ts.Extension.Mjs, ts.Extension.Cts, ts.Extension.Dcts, ts.Extension.Cjs]; + const autoExtensions = ts.filter(ts.supportedTSExtensionsFlat, e => nonImplicitExtensions.indexOf(e) === -1); + function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { + for (const ext of autoExtensions) { + 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 = ts.nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); - - const failedLookupLocations: string[] = []; - const dir = ts.getDirectoryPath(containingFileName); - for (const e of autoExtensions) { - if (e === ext) { - break; - } - else { - failedLookupLocations.push(ts.normalizePath(ts.getRootLength(moduleName) === 0 ? ts.combinePaths(dir, moduleName) : moduleName) + e); - } + function test(ext: string, hasDirectoryExists: boolean) { + const containingFile = { name: containingFileName }; + const moduleFile = { name: moduleFileNameNoExt + ext }; + const resolution = ts.nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); + + const failedLookupLocations: string[] = []; + const dir = ts.getDirectoryPath(containingFileName); + for (const e of autoExtensions) { + if (e === ext) { + break; } + else { + failedLookupLocations.push(ts.normalizePath(ts.getRootLength(moduleName) === 0 ? ts.combinePaths(dir, moduleName) : moduleName) + e); + } + } - assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); + assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); - } } + } - 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/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 '../' 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 '/' 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"); - }); + 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 = ts.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, ts.supportedTSExtensions[0].length); - } + 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 = ts.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, ts.supportedTSExtensions[0].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"); - }); + 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 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" }; + 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 indexPath = "/node_modules/b/index.d.ts"; + const indexFile = { name: indexPath }; - const resolution = ts.nodeModuleNameResolver("b", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile, indexFile)); + const resolution = ts.nodeModuleNameResolver("b", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile, indexFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(indexPath, /*isExternalLibraryImport*/ true)); - } + 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); + 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 = ts.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", + ]); + } + }); +}); + +describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { + it("computes correct commonPrefix for moduleName cache", () => { + const resolutionCache = ts.createModuleResolutionCache("/", (f) => f); + let cache = resolutionCache.getOrCreateCacheForModuleName("a", /*mode*/ undefined); + cache.set("/sub", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/sub/node_modules/a/index.ts", + isExternalLibraryImport: true, + extension: ts.Extension.Ts, + }, + failedLookupLocations: [], + resolutionDiagnostics: [], }); - 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 = ts.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("/sub")); + assert.isUndefined(cache.get("/")); + + cache = resolutionCache.getOrCreateCacheForModuleName("b", /*mode*/ undefined); + cache.set("/sub/dir/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/sub/directory/node_modules/b/index.ts", + isExternalLibraryImport: true, + extension: ts.Extension.Ts, + }, + failedLookupLocations: [], + resolutionDiagnostics: [], }); - }); - - describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { - it("computes correct commonPrefix for moduleName cache", () => { - const resolutionCache = ts.createModuleResolutionCache("/", (f) => f); - let cache = resolutionCache.getOrCreateCacheForModuleName("a", /*mode*/ undefined); - cache.set("/sub", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/sub/node_modules/a/index.ts", - isExternalLibraryImport: true, - extension: ts.Extension.Ts, - }, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - assert.isDefined(cache.get("/sub")); - assert.isUndefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("b", /*mode*/ undefined); - cache.set("/sub/dir/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/sub/directory/node_modules/b/index.ts", - isExternalLibraryImport: true, - extension: ts.Extension.Ts, - }, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - 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", /*mode*/ undefined); - cache.set("/foo/bar", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/bar/node_modules/c/index.ts", - isExternalLibraryImport: true, - extension: ts.Extension.Ts, - }, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - assert.isDefined(cache.get("/foo/bar")); - assert.isDefined(cache.get("/foo")); - assert.isDefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("d", /*mode*/ undefined); - cache.set("/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/foo/index.ts", - isExternalLibraryImport: true, - extension: ts.Extension.Ts, - }, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - assert.isDefined(cache.get("/foo")); - assert.isUndefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("e", /*mode*/ undefined); - cache.set("c:/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "d:/bar/node_modules/e/index.ts", - isExternalLibraryImport: true, - extension: ts.Extension.Ts, - }, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - assert.isDefined(cache.get("c:/foo")); - assert.isDefined(cache.get("c:/")); - assert.isUndefined(cache.get("d:/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("f", /*mode*/ undefined); - cache.set("/foo/bar/baz", { - resolvedModule: undefined, - failedLookupLocations: [], - resolutionDiagnostics: [], - }); - assert.isDefined(cache.get("/foo/bar/baz")); - assert.isDefined(cache.get("/foo/bar")); - assert.isDefined(cache.get("/foo")); - assert.isDefined(cache.get("/")); + 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", /*mode*/ undefined); + cache.set("/foo/bar", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/bar/node_modules/c/index.ts", + isExternalLibraryImport: true, + extension: ts.Extension.Ts, + }, + failedLookupLocations: [], + resolutionDiagnostics: [], }); + assert.isDefined(cache.get("/foo/bar")); + assert.isDefined(cache.get("/foo")); + assert.isDefined(cache.get("/")); + + cache = resolutionCache.getOrCreateCacheForModuleName("d", /*mode*/ undefined); + cache.set("/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/foo/index.ts", + isExternalLibraryImport: true, + extension: ts.Extension.Ts, + }, + failedLookupLocations: [], + resolutionDiagnostics: [], + }); + assert.isDefined(cache.get("/foo")); + assert.isUndefined(cache.get("/")); + + cache = resolutionCache.getOrCreateCacheForModuleName("e", /*mode*/ undefined); + cache.set("c:/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "d:/bar/node_modules/e/index.ts", + isExternalLibraryImport: true, + extension: ts.Extension.Ts, + }, + failedLookupLocations: [], + resolutionDiagnostics: [], + }); + assert.isDefined(cache.get("c:/foo")); + assert.isDefined(cache.get("c:/")); + assert.isUndefined(cache.get("d:/")); + + cache = resolutionCache.getOrCreateCacheForModuleName("f", /*mode*/ undefined); + cache.set("/foo/bar/baz", { + resolvedModule: undefined, + failedLookupLocations: [], + resolutionDiagnostics: [], + }); + 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 = ts.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", + it("load module as file - ts files not loaded", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); - "/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", + function test(hasDirectoryExists: boolean) { + const containingFile = { name: "/a/b/c/d/e.ts" }; + const moduleFile = { name: "/a/b/node_modules/foo.ts" }; + const resolution = ts.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/@types/foo/package.json", - "/a/b/c/d/node_modules/@types/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/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/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/d/node_modules/@types/foo/index.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/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/@types/foo/package.json", - "/a/b/c/node_modules/@types/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/index.d.ts", - "/a/b/node_modules/foo/package.json", - ]); - } - }); + "/a/b/c/node_modules/@types/foo/package.json", + "/a/b/c/node_modules/@types/foo.d.ts", - it("load module as file", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); + "/a/b/c/node_modules/@types/foo/index.d.ts", + "/a/b/node_modules/foo/package.json", + ]); + } + }); - 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 = ts.nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true)); - } - }); + it("load module as file", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); - it("load module as directory", () => { - 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 = ts.nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ 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 = ts.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", + it("load module as directory", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); - "/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", + 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 = ts.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/@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/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/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/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/d/node_modules/@types/foo/index.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/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/@types/foo/package.json", - "/a/node_modules/b/c/node_modules/@types/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/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/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/c/node_modules/@types/foo/index.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/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/@types/foo/package.json", - "/a/node_modules/b/node_modules/@types/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/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/foo/package.json", - "/a/node_modules/foo.ts", - "/a/node_modules/foo.tsx", - "/a/node_modules/foo.d.ts", + "/a/node_modules/b/node_modules/@types/foo/index.d.ts", - "/a/node_modules/foo/index.ts", - "/a/node_modules/foo/index.tsx" - ]); - } - }); + "/a/node_modules/foo/package.json", + "/a/node_modules/foo.ts", + "/a/node_modules/foo.tsx", + "/a/node_modules/foo.d.ts", - 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 = ts.nodeModuleNameResolver("linked", "/app/app.ts", { preserveSymlinks }, host); - const resolvedFileName = preserveSymlinks ? symlinkFileName : realFileName; - checkResolvedModule(resolution.resolvedModule, createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/ true)); - }); + "/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: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.NodeJs }; - const cache = ts.createModuleResolutionCache("/", (f) => f); - let resolution = ts.resolveModuleName("a", "/sub/dir/foo.ts", compilerOptions, host, cache); - checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - - resolution = ts.resolveModuleName("a", "/sub/foo.ts", compilerOptions, host, cache); - checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - - resolution = ts.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 = ts.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 = ts.createModuleResolutionCache("/", (f) => f); - const compilerOptions: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.NodeJs }; - checkResolution(ts.resolveModuleName("linked", "/app/src/app.ts", compilerOptions, host, cache)); - checkResolution(ts.resolveModuleName("linked", "/app/lib/main.ts", compilerOptions, host, cache)); - function checkResolution(resolution: ts.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"}' }); - }); - - describe("unittests:: moduleResolution:: Relative imports", () => { - function test(files: ts.ESMap, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { - const options: ts.CompilerOptions = { module: ts.ModuleKind.CommonJS }; - const host: ts.CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { - const path = ts.normalizePath(ts.combinePaths(currentDirectory, fileName)); - const file = files.get(path); - return file ? ts.createSourceFile(fileName, file, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: ts.notImplemented, - getCurrentDirectory: () => currentDirectory, - getDirectories: () => [], - getCanonicalFileName: fileName => fileName.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - fileExists: fileName => { - const path = ts.normalizePath(ts.combinePaths(currentDirectory, fileName)); - return files.has(path); - }, - readFile: ts.notImplemented, - }; + const compilerOptions: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.NodeJs }; + const cache = ts.createModuleResolutionCache("/", (f) => f); + let resolution = ts.resolveModuleName("a", "/sub/dir/foo.ts", compilerOptions, host, cache); + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - const program = ts.createProgram(rootFiles, options, host); + resolution = ts.resolveModuleName("a", "/sub/foo.ts", compilerOptions, host, cache); + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - 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))}`); + resolution = ts.resolveModuleName("a", "/foo.ts", compilerOptions, host, cache); + assert.isUndefined(resolution.resolvedModule, "lookup in parent directory doesn't hit the cache"); + }); - // 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 = ts.createModuleResolutionCache("/", (f) => f); + const compilerOptions: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.NodeJs }; + checkResolution(ts.resolveModuleName("linked", "/app/src/app.ts", compilerOptions, host, cache)); + checkResolution(ts.resolveModuleName("linked", "/app/lib/main.ts", compilerOptions, host, cache)); + function checkResolution(resolution: ts.ResolvedModuleWithFailedLookupLocations) { + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/linked/index.d.ts", /*isExternalLibraryImport*/ true)); + assert.strictEqual(resolution.resolvedModule!.originalPath, "/app/node_modules/linked/index.d.ts"); + } + }); +}); + +describe("unittests:: moduleResolution:: Relative imports", () => { + function test(files: ts.ESMap, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { + const options: ts.CompilerOptions = { module: ts.ModuleKind.CommonJS }; + const host: ts.CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { + const path = ts.normalizePath(ts.combinePaths(currentDirectory, fileName)); + const file = files.get(path); + return file ? ts.createSourceFile(fileName, file, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: ts.notImplemented, + getCurrentDirectory: () => currentDirectory, + getDirectories: () => [], + getCanonicalFileName: fileName => fileName.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + fileExists: fileName => { + const path = ts.normalizePath(ts.combinePaths(currentDirectory, fileName)); + return files.has(path); + }, + readFile: ts.notImplemented, + }; + + const program = ts.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("should find all modules", () => { - const files = new ts.Map(ts.getEntries({ - "/a/b/c/first/shared.ts": ` + it("should find all modules", () => { + const files = new ts.Map(ts.getEntries({ + "/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 = new ts.Map(ts.getEntries({ - "/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 file referenced via absolute and relative names", () => { - const files = new ts.Map(ts.getEntries({ - "/a/b/c.ts": `/// `, - "/a/b/b.ts": "var x" - })); - test(files, "/a/b", ["c.ts", "/a/b/b.ts"], 2, []); - }); + it("should find modules in node_modules", () => { + const files = new ts.Map(ts.getEntries({ + "/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, []); }); - describe("unittests:: moduleResolution:: Files with different casing with forceConsistentCasingInFileNames", () => { - let library: ts.SourceFile; - function test(files: ts.ESMap, options: ts.CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], expectedDiagnostics: (program: ts.Program) => readonly ts.Diagnostic[]): void { - const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - if (!useCaseSensitiveFileNames) { - const oldFiles = files; - files = new ts.Map(); - oldFiles.forEach((file, fileName) => { - files.set(getCanonicalFileName(fileName), file); - }); - } + it("should find file referenced via absolute and relative names", () => { + const files = new ts.Map(ts.getEntries({ + "/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: ts.SourceFile; + function test(files: ts.ESMap, options: ts.CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], expectedDiagnostics: (program: ts.Program) => readonly ts.Diagnostic[]): void { + const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); + if (!useCaseSensitiveFileNames) { + const oldFiles = files; + files = new ts.Map(); + oldFiles.forEach((file, fileName) => { + files.set(getCanonicalFileName(fileName), file); + }); + } - const host: ts.CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { - if (fileName === "lib.d.ts") { - if (!library) { - library = ts.createSourceFile("lib.d.ts", "", ts.ScriptTarget.ES5); - } - return library; + const host: ts.CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { + if (fileName === "lib.d.ts") { + if (!library) { + library = ts.createSourceFile("lib.d.ts", "", ts.ScriptTarget.ES5); } - const path = getCanonicalFileName(ts.normalizePath(ts.combinePaths(currentDirectory, fileName))); - const file = files.get(path); - return file ? ts.createSourceFile(fileName, file, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: ts.notImplemented, - getCurrentDirectory: () => currentDirectory, - getDirectories: () => [], - getCanonicalFileName, - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - fileExists: fileName => { - const path = getCanonicalFileName(ts.normalizePath(ts.combinePaths(currentDirectory, fileName))); - return files.has(path); - }, - readFile: ts.notImplemented, - }; - const program = ts.createProgram(rootFiles, options, host); - const diagnostics = ts.sortAndDeduplicateDiagnostics([...program.getSemanticDiagnostics(), ...program.getOptionsDiagnostics()]); - assert.deepEqual(diagnostics, ts.sortAndDeduplicateDiagnostics(expectedDiagnostics(program))); - } + return library; + } + const path = getCanonicalFileName(ts.normalizePath(ts.combinePaths(currentDirectory, fileName))); + const file = files.get(path); + return file ? ts.createSourceFile(fileName, file, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: ts.notImplemented, + getCurrentDirectory: () => currentDirectory, + getDirectories: () => [], + getCanonicalFileName, + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + fileExists: fileName => { + const path = getCanonicalFileName(ts.normalizePath(ts.combinePaths(currentDirectory, fileName))); + return files.has(path); + }, + readFile: ts.notImplemented, + }; + const program = ts.createProgram(rootFiles, options, host); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...program.getSemanticDiagnostics(), ...program.getOptionsDiagnostics()]); + assert.deepEqual(diagnostics, ts.sortAndDeduplicateDiagnostics(expectedDiagnostics(program))); + } - it("should succeed when the same file is referenced using absolute and relative names", () => { - const files = new ts.Map(ts.getEntries({ - "/a/b/c.ts": `/// `, - "/a/b/d.ts": "var x" - })); - test(files, { module: ts.ModuleKind.AMD }, "/a/b", - /*useCaseSensitiveFileNames*/ false, ["c.ts", "/a/b/d.ts"], () => ts.emptyArray); - }); + it("should succeed when the same file is referenced using absolute and relative names", () => { + const files = new ts.Map(ts.getEntries({ + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" + })); + test(files, { module: ts.ModuleKind.AMD }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "/a/b/d.ts"], () => ts.emptyArray); + }); - it("should fail when two files used in program differ only in casing (tripleslash references)", () => { - const files = new ts.Map(ts.getEntries({ - "/a/b/c.ts": `/// `, - "/a/b/d.ts": "var x" - })); - test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", - /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `/// `.indexOf(`D.ts`), "D.ts".length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["D.ts", "d.ts"], [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Referenced_via_0_from_file_1, ["D.ts", "c.ts"]), - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) - ]) - ])), - relatedInformation: undefined, - }]); - }); + it("should fail when two files used in program differ only in casing (tripleslash references)", () => { + const files = new ts.Map(ts.getEntries({ + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" + })); + test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `/// `.indexOf(`D.ts`), "D.ts".length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["D.ts", "d.ts"], [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Referenced_via_0_from_file_1, ["D.ts", "c.ts"]), + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) + ]) + ])), + relatedInformation: undefined, + }]); + }); - it("should fail when two files used in program differ only in casing (imports)", () => { - const files = new ts.Map(ts.getEntries({ - "/a/b/c.ts": `import {x} from "D"`, - "/a/b/d.ts": "export var x" - })); - test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", - /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["/a/b/D.ts", "d.ts"], [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"D"`, "c.ts"]), - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) - ]) - ])), - relatedInformation: undefined, - }]); - }); + it("should fail when two files used in program differ only in casing (imports)", () => { + const files = new ts.Map(ts.getEntries({ + "/a/b/c.ts": `import {x} from "D"`, + "/a/b/d.ts": "export var x" + })); + test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["/a/b/D.ts", "d.ts"], [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"D"`, "c.ts"]), + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) + ]) + ])), + relatedInformation: undefined, + }]); + }); - it("should fail when two files used in program differ only in casing (imports, relative module names)", () => { - const files = new ts.Map(ts.getEntries({ - "moduleA.ts": `import {x} from "./ModuleB"`, - "moduleB.ts": "export var x" - })); - test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", - /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts"], program => [{ - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import {x} from "./ModuleB"`.indexOf(`"./ModuleB"`), `"./ModuleB"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["ModuleB.ts", "moduleB.ts"], [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleB"`, "moduleA.ts"]), - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) - ]) - ])), - relatedInformation: undefined - }]); - }); + it("should fail when two files used in program differ only in casing (imports, relative module names)", () => { + const files = new ts.Map(ts.getEntries({ + "moduleA.ts": `import {x} from "./ModuleB"`, + "moduleB.ts": "export var x" + })); + test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", + /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts"], program => [{ + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import {x} from "./ModuleB"`.indexOf(`"./ModuleB"`), `"./ModuleB"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["ModuleB.ts", "moduleB.ts"], [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleB"`, "moduleA.ts"]), + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) + ]) + ])), + relatedInformation: undefined + }]); + }); - it("should fail when two files exist on disk that differs only in casing", () => { - const files = new ts.Map(ts.getEntries({ - "/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: ts.ModuleKind.AMD }, "/a/b", - /*useCaseSensitiveFileNames*/ true, ["c.ts", "d.ts"], program => [{ - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["/a/b/D.ts", "d.ts"], [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"D"`, "c.ts"]), - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) - ]) - ])), - relatedInformation: undefined - }]); - }); + it("should fail when two files exist on disk that differs only in casing", () => { + const files = new ts.Map(ts.getEntries({ + "/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: ts.ModuleKind.AMD }, "/a/b", + /*useCaseSensitiveFileNames*/ true, ["c.ts", "d.ts"], program => [{ + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["/a/b/D.ts", "d.ts"], [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"D"`, "c.ts"]), + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation) + ]) + ])), + relatedInformation: undefined + }]); + }); - it("should fail when module name in 'require' calls has inconsistent casing", () => { - const files = new ts.Map(ts.getEntries({ - "moduleA.ts": `import a = require("./ModuleC")`, - "moduleB.ts": `import a = require("./moduleC")`, - "moduleC.ts": "export var x" - })); - test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", - /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts", "moduleC.ts"], program => { - const importInA = { - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), `"./ModuleC"`.length, ts.Diagnostics.File_is_included_via_import_here), - reportsUnnecessary: undefined, - reportsDeprecated: undefined - }; - const importInB = { - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, ts.Diagnostics.File_is_included_via_import_here), - reportsUnnecessary: undefined, - reportsDeprecated: undefined - }; - const importHereInA = ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleC"`, "moduleA.ts"]); - const importHereInB = ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./moduleC"`, "moduleB.ts"]); - const details = [ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [importHereInA, importHereInB, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation)])]; - return [ - { - ...ts.tscWatch.getDiagnosticOfFileFrom(importInA.file, importInA.start, importInA.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["ModuleC.ts", "moduleC.ts"], details)), - relatedInformation: [importInB] - }, - { - ...ts.tscWatch.getDiagnosticOfFileFrom(importInB.file, importInB.start, importInB.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, ["moduleC.ts", "ModuleC.ts"], details)), - relatedInformation: [importInA] - } - ]; - }); + it("should fail when module name in 'require' calls has inconsistent casing", () => { + const files = new ts.Map(ts.getEntries({ + "moduleA.ts": `import a = require("./ModuleC")`, + "moduleB.ts": `import a = require("./moduleC")`, + "moduleC.ts": "export var x" + })); + test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", + /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts", "moduleC.ts"], program => { + const importInA = { + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), `"./ModuleC"`.length, ts.Diagnostics.File_is_included_via_import_here), + reportsUnnecessary: undefined, + reportsDeprecated: undefined + }; + const importInB = { + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, ts.Diagnostics.File_is_included_via_import_here), + reportsUnnecessary: undefined, + reportsDeprecated: undefined + }; + const importHereInA = ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleC"`, "moduleA.ts"]); + const importHereInB = ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./moduleC"`, "moduleB.ts"]); + const details = [ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [importHereInA, importHereInB, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Root_file_specified_for_compilation)])]; + return [ + { + ...ts.tscWatch.getDiagnosticOfFileFrom(importInA.file, importInA.start, importInA.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, ["ModuleC.ts", "moduleC.ts"], details)), + relatedInformation: [importInB] + }, + { + ...ts.tscWatch.getDiagnosticOfFileFrom(importInB.file, importInB.start, importInB.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, ["moduleC.ts", "ModuleC.ts"], details)), + relatedInformation: [importInA] + } + ]; + }); - }); - it("should fail when module names in 'require' calls has inconsistent casing and current directory has uppercase chars", () => { - const files = new ts.Map(ts.getEntries({ - "/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": ` + }); + it("should fail when module names in 'require' calls has inconsistent casing and current directory has uppercase chars", () => { + const files = new ts.Map(ts.getEntries({ + "/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: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", - /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], program => [{ - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, ["/a/B/c/moduleC.ts", "/a/B/c/ModuleC.ts"], [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleC"`, "/a/B/c/moduleA.ts"]), - ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./moduleC"`, "/a/B/c/moduleB.ts"]) - ]) - ])), - relatedInformation: [ - { - ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), `"./ModuleC"`.length, ts.Diagnostics.File_is_included_via_import_here), - reportsUnnecessary: undefined, - reportsDeprecated: undefined - } - ] - }]); - }); - it("should not fail when module names in 'require' calls has consistent casing and current directory has uppercase chars", () => { - const files = new ts.Map(ts.getEntries({ - "/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: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", + /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], program => [{ + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, ["/a/B/c/moduleC.ts", "/a/B/c/ModuleC.ts"], [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.The_file_is_in_the_program_because_Colon, ts.emptyArray, [ + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./ModuleC"`, "/a/B/c/moduleA.ts"]), + ts.tscWatch.getDiagnosticMessageChain(ts.Diagnostics.Imported_via_0_from_file_1, [`"./moduleC"`, "/a/B/c/moduleB.ts"]) + ]) + ])), + relatedInformation: [ + { + ...ts.tscWatch.getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), `"./ModuleC"`.length, ts.Diagnostics.File_is_included_via_import_here), + reportsUnnecessary: undefined, + reportsDeprecated: undefined + } + ] + }]); + }); + it("should not fail when module names in 'require' calls has consistent casing and current directory has uppercase chars", () => { + const files = new ts.Map(ts.getEntries({ + "/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: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", - /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], () => ts.emptyArray); - }); - - it("should succeed when the two files in program differ only in drive letter in their names", () => { - const files = new ts.Map(ts.getEntries({ - "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: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "d:/someFolder", - /*useCaseSensitiveFileNames*/ false, ["d:/someFolder/moduleA.ts", "d:/someFolder/moduleB.ts"], () => ts.emptyArray); - }); + })); + test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", + /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], () => 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 [ts.ModuleResolutionKind.NodeJs, ts.ModuleResolutionKind.Classic]) { - const options: ts.CompilerOptions = { moduleResolution, baseUrl: "/root" }; - { - const result = ts.resolveModuleName("folder2/file2", file1.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file2.name), []); - } - { - const result = ts.resolveModuleName("./file3", file2.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file3.name), []); - } - { - const result = ts.resolveModuleName("/root/folder1/file1", file2.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file1.name), []); - } + it("should succeed when the two files in program differ only in drive letter in their names", () => { + const files = new ts.Map(ts.getEntries({ + "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: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "d:/someFolder", + /*useCaseSensitiveFileNames*/ false, ["d:/someFolder/moduleA.ts", "d:/someFolder/moduleB.ts"], () => 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 [ts.ModuleResolutionKind.NodeJs, ts.ModuleResolutionKind.Classic]) { + const options: ts.CompilerOptions = { moduleResolution, baseUrl: "/root" }; + { + const result = ts.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: ts.CompilerOptions = { moduleResolution: ts.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 = ts.resolveModuleName(name, caller.name, options, host); - checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name, isExternalLibraryImport)); + { + const result = ts.resolveModuleName("./file3", file2.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file3.name), []); + } + { + const result = ts.resolveModuleName("/root/folder1/file1", file2.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file1.name), []); } } - }); - - it("classic + baseUrl", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); + } + // add failure tests + }); - 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 + 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: ts.CompilerOptions = { moduleResolution: ts.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 = ts.resolveModuleName(name, caller.name, options, host); + checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name, isExternalLibraryImport)); + } + } + }); - const options: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.Classic, baseUrl: "/root/x", jsx: ts.JsxEmit.React }; - const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2); + it("classic + baseUrl", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); - check("m1", main, m1); - check("m2", main, m2); + 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 - function check(name: string, caller: File, expected: File) { - const result = ts.resolveModuleName(name, caller.name, options, host); - checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name)); - } - } - }); + const options: ts.CompilerOptions = { moduleResolution: ts.ModuleResolutionKind.Classic, baseUrl: "/root/x", jsx: ts.JsxEmit.React }; + const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2); - 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: ts.CompilerOptions = { - moduleResolution: ts.ModuleResolutionKind.NodeJs, - baseUrl: "/root", - jsx: ts.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 = ts.resolveModuleName(name, main.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name, isExternalLibraryImport), expectedFailedLookups); - } - } - }); + check("m1", main, m1); + check("m2", main, m2); - 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: ts.CompilerOptions = { - moduleResolution: ts.ModuleResolutionKind.Classic, - baseUrl: "/root", - jsx: ts.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 = ts.resolveModuleName(name, main.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); - } + function check(name: string, caller: File, expected: File) { + const result = ts.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/naming-convention - 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: ts.CompilerOptions = { - moduleResolution: ts.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: ts.CompilerOptions = { + moduleResolution: ts.ModuleResolutionKind.NodeJs, + baseUrl: "/root", + jsx: ts.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 = ts.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", + // 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 = ts.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: ts.CompilerOptions = { - moduleResolution: ts.ModuleResolutionKind.Classic, - jsx: ts.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: ts.CompilerOptions = { + moduleResolution: ts.ModuleResolutionKind.Classic, + baseUrl: "/root", + jsx: ts.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 = ts.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", + // 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 = ts.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: ts.CompilerOptions = { - moduleResolution: ts.ModuleResolutionKind.NodeJs, - baseUrl: "/root", - paths: { - "libs/guid": [ "src/libs/guid" ] - } - }; - const result = ts.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/naming-convention + 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: ts.CompilerOptions = { + moduleResolution: ts.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", + // 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 = ts.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: ts.ModuleResolutionHost = { - readFile: ts.notImplemented, - fileExists: ts.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: ts.CompilerOptions = { + moduleResolution: ts.ModuleResolutionKind.Classic, + jsx: ts.JsxEmit.React, + rootDirs: [ + "/root", + "/root/generated/" + ] }; - - const result = ts.resolveModuleName("someName", "/a/b/c/d", { moduleResolution: ts.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", + // 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 = ts.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 = ts.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: ts.CompilerOptions = { + moduleResolution: ts.ModuleResolutionKind.NodeJs, + baseUrl: "/root", + paths: { + "libs/guid": [ "src/libs/guid" ] + } + }; + const result = ts.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", + ]); } + }); +}); + +describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => { + it("No 'fileExists' calls if containing directory is missing", () => { + const host: ts.ModuleResolutionHost = { + readFile: ts.notImplemented, + fileExists: ts.notImplemented, + directoryExists: _ => false + }; + + const result = ts.resolveModuleName("someName", "/a/b/c/d", { moduleResolution: ts.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 = ts.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); - } + 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); - } - { - 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 = ts.map(files, f => f.name); - const sourceFiles = ts.arrayToMap(ts.map(files, f => ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015)), f => f.fileName); - const compilerHost: ts.CompilerHost = { - fileExists: fileName => sourceFiles.has(fileName), - getSourceFile: fileName => sourceFiles.get(fileName), - getDefaultLibFileName: () => "lib.d.ts", - writeFile: ts.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 = ts.createProgram(names, {}, compilerHost); - const diagnostics1 = program1.getOptionsDiagnostics(); - assert.equal(diagnostics1.length, 1, "expected one diagnostic"); - - const program2 = ts.createProgram(names, {}, compilerHost, program1); - assert.isTrue(program2.structureIsReused === ts.StructureIsReused.Completely); - const diagnostics2 = program2.getOptionsDiagnostics(); - assert.equal(diagnostics2.length, 1, "expected one diagnostic"); - assert.deepEqual(diagnostics1[0].messageText, diagnostics2[0].messageText, "expected one diagnostic"); - }); + 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 = ts.map(files, f => f.name); + const sourceFiles = ts.arrayToMap(ts.map(files, f => ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015)), f => f.fileName); + const compilerHost: ts.CompilerHost = { + fileExists: fileName => sourceFiles.has(fileName), + getSourceFile: fileName => sourceFiles.get(fileName), + getDefaultLibFileName: () => "lib.d.ts", + writeFile: ts.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 = ts.createProgram(names, {}, compilerHost); + const diagnostics1 = program1.getOptionsDiagnostics(); + assert.equal(diagnostics1.length, 1, "expected one diagnostic"); + + const program2 = ts.createProgram(names, {}, compilerHost, program1); + assert.isTrue(program2.structureIsReused === ts.StructureIsReused.Completely); + const diagnostics2 = program2.getOptionsDiagnostics(); + assert.equal(diagnostics2.length, 1, "expected one diagnostic"); + assert.deepEqual(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: ` + 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 } } @@ -1326,28 +1326,28 @@ import b = require("./moduleB"); import { Stat } from "fs"; export function foo(): Stat; }` - }; - const file = ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015); - const compilerHost: ts.CompilerHost = { - fileExists: fileName => fileName === file.fileName, - getSourceFile: fileName => fileName === file.fileName ? file : undefined, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: ts.notImplemented, - getCurrentDirectory: () => "/", - getDirectories: () => [], - getCanonicalFileName: f => f.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - readFile: fileName => fileName === file.fileName ? file.text : undefined, - resolveModuleNames: ts.notImplemented, - }; - ts.createProgram([f.name], {}, compilerHost); - }); + }; + const file = ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015); + const compilerHost: ts.CompilerHost = { + fileExists: fileName => fileName === file.fileName, + getSourceFile: fileName => fileName === file.fileName ? file : undefined, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: ts.notImplemented, + getCurrentDirectory: () => "/", + getDirectories: () => [], + getCanonicalFileName: f => f.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + readFile: fileName => fileName === file.fileName ? file.text : undefined, + resolveModuleNames: ts.notImplemented, + }; + ts.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: ` + 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 } } @@ -1355,35 +1355,35 @@ import b = require("./moduleB"); import { Stat } from "fs"; export function foo(): Stat; }` - }; - const file = ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015); - const compilerHost: ts.CompilerHost = { - fileExists: fileName => fileName === file.fileName, - getSourceFile: fileName => fileName === file.fileName ? file : undefined, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: ts.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 - } - }; - ts.createProgram([f.name], {}, compilerHost); + }; + const file = ts.createSourceFile(f.name, f.content, ts.ScriptTarget.ES2015); + const compilerHost: ts.CompilerHost = { + fileExists: fileName => fileName === file.fileName, + getSourceFile: fileName => fileName === file.fileName ? file : undefined, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: ts.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 + } + }; + ts.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 6f17c2cf73b9b..d07df2a35a2e3 100644 --- a/src/testRunner/unittests/parsePseudoBigInt.ts +++ b/src/testRunner/unittests/parsePseudoBigInt.ts @@ -1,57 +1,57 @@ 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(ts.parsePseudoBigInt("0".repeat(leadingZeros) + testNumber + "n"), String(testNumber)); - } +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(ts.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(ts.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(ts.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(ts.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(ts.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(ts.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(ts.parsePseudoBigInt(prefix + hexCase), String(testNumber)); } } } - }); - it("can parse large literals", () => { - assert.equal(ts.parsePseudoBigInt("123456789012345678901234567890n"), "123456789012345678901234567890"); - assert.equal(ts.parsePseudoBigInt("0b1100011101110100100001111111101101100001101110011111000001110111001001110001111110000101011010010n"), "123456789012345678901234567890"); - assert.equal(ts.parsePseudoBigInt("0o143564417755415637016711617605322n"), "123456789012345678901234567890"); - assert.equal(ts.parsePseudoBigInt("0x18ee90ff6c373e0ee4e3f0ad2n"), "123456789012345678901234567890"); - }); + } + }); + it("can parse large literals", () => { + assert.equal(ts.parsePseudoBigInt("123456789012345678901234567890n"), "123456789012345678901234567890"); + assert.equal(ts.parsePseudoBigInt("0b1100011101110100100001111111101101100001101110011111000001110111001001110001111110000101011010010n"), "123456789012345678901234567890"); + assert.equal(ts.parsePseudoBigInt("0o143564417755415637016711617605322n"), "123456789012345678901234567890"); + assert.equal(ts.parsePseudoBigInt("0x18ee90ff6c373e0ee4e3f0ad2n"), "123456789012345678901234567890"); }); }); +}); } diff --git a/src/testRunner/unittests/printer.ts b/src/testRunner/unittests/printer.ts index ddf62ce0467de..af2379dd2e934 100644 --- a/src/testRunner/unittests/printer.ts +++ b/src/testRunner/unittests/printer.ts @@ -1,20 +1,20 @@ namespace ts { - describe("unittests:: PrinterAPI", () => { - function makePrintsCorrectly(prefix: string) { - return function printsCorrectly(name: string, options: ts.PrinterOptions, printCallback: (printer: ts.Printer) => string) { - it(name, () => { - Harness.Baseline.runBaseline(`printerApi/${prefix}.${name}.js`, printCallback(ts.createPrinter({ newLine: ts.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: ts.SourceFile; - before(() => { - sourceFile = ts.createSourceFile("source.ts", ` +describe("unittests:: PrinterAPI", () => { + function makePrintsCorrectly(prefix: string) { + return function printsCorrectly(name: string, options: ts.PrinterOptions, printCallback: (printer: ts.Printer) => string) { + it(name, () => { + Harness.Baseline.runBaseline(`printerApi/${prefix}.${name}.js`, printCallback(ts.createPrinter({ newLine: ts.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: ts.SourceFile; + before(() => { + sourceFile = ts.createSourceFile("source.ts", ` interface A { // comment1 readonly prop?: T; @@ -49,169 +49,169 @@ namespace ts { // comment10 function functionWithDefaultArgValue(argument: string = "defaultValue"): void { } `, ts.ScriptTarget.ES2015); - }); - printsCorrectly("default", {}, printer => printer.printFile(sourceFile)); - printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile)); }); + 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(ts.createSourceFile("source.ts", "let greeting = `Hi ${name}, how are you?`;", ts.ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/14948 + // eslint-disable-next-line no-template-curly-in-string + printsCorrectly("templateLiteral", {}, printer => printer.printFile(ts.createSourceFile("source.ts", "let greeting = `Hi ${name}, how are you?`;", ts.ScriptTarget.ES2017))); - // https://github.com/microsoft/TypeScript/issues/18071 - printsCorrectly("regularExpressionLiteral", {}, printer => printer.printFile(ts.createSourceFile("source.ts", "let regex = /abc/;", ts.ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/18071 + printsCorrectly("regularExpressionLiteral", {}, printer => printer.printFile(ts.createSourceFile("source.ts", "let regex = /abc/;", ts.ScriptTarget.ES2017))); - // https://github.com/microsoft/TypeScript/issues/22239 - printsCorrectly("importStatementRemoveComments", { removeComments: true }, printer => printer.printFile(ts.createSourceFile("source.ts", "import {foo} from 'foo';", ts.ScriptTarget.ESNext))); - printsCorrectly("classHeritageClauses", {}, printer => printer.printFile(ts.createSourceFile("source.ts", `class A extends B implements C implements D {}`, ts.ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/22239 + printsCorrectly("importStatementRemoveComments", { removeComments: true }, printer => printer.printFile(ts.createSourceFile("source.ts", "import {foo} from 'foo';", ts.ScriptTarget.ESNext))); + printsCorrectly("classHeritageClauses", {}, printer => printer.printFile(ts.createSourceFile("source.ts", `class A extends B implements C implements D {}`, ts.ScriptTarget.ES2017))); - // https://github.com/microsoft/TypeScript/issues/35093 - printsCorrectly("definiteAssignmentAssertions", {}, printer => printer.printFile(ts.createSourceFile("source.ts", `class A { + // https://github.com/microsoft/TypeScript/issues/35093 + printsCorrectly("definiteAssignmentAssertions", {}, printer => printer.printFile(ts.createSourceFile("source.ts", `class A { prop!: string; } let x!: string;`, ts.ScriptTarget.ES2017))); - // https://github.com/microsoft/TypeScript/issues/35054 - printsCorrectly("jsx attribute escaping", {}, printer => { - return printer.printFile(ts.createSourceFile("source.ts", String.raw ``, ts.ScriptTarget.ESNext, - /*setParentNodes*/ undefined, ts.ScriptKind.TSX)); - }); + // https://github.com/microsoft/TypeScript/issues/35054 + printsCorrectly("jsx attribute escaping", {}, printer => { + return printer.printFile(ts.createSourceFile("source.ts", String.raw ``, ts.ScriptTarget.ESNext, + /*setParentNodes*/ undefined, ts.ScriptKind.TSX)); }); + }); - describe("No duplicate ref directives when emiting .d.ts->.d.ts", () => { - it("without statements", () => { - const host = new fakes.CompilerHost(new vfs.FileSystem(true, { - files: { - "/test.d.ts": `/// \n/// { - const host = new fakes.CompilerHost(new vfs.FileSystem(true, { - files: { - "/test.d.ts": `/// \n/// .d.ts", () => { + it("without statements", () => { + const host = new fakes.CompilerHost(new vfs.FileSystem(true, { + files: { + "/test.d.ts": `/// \n/// { + const host = new fakes.CompilerHost(new vfs.FileSystem(true, { + files: { + "/test.d.ts": `/// \n/// { - const printsCorrectly = makePrintsCorrectly("printsBundleCorrectly"); - let bundle: ts.Bundle; - before(() => { - bundle = ts.factory.createBundle([ - ts.createSourceFile("a.ts", ` + describe("printBundle", () => { + const printsCorrectly = makePrintsCorrectly("printsBundleCorrectly"); + let bundle: ts.Bundle; + before(() => { + bundle = ts.factory.createBundle([ + ts.createSourceFile("a.ts", ` /*! [a.ts] */ // comment0 const a = 1; `, ts.ScriptTarget.ES2015), - ts.createSourceFile("b.ts", ` + ts.createSourceFile("b.ts", ` /*! [b.ts] */ // comment1 const b = 2; `, ts.ScriptTarget.ES2015) - ]); - }); - printsCorrectly("default", {}, printer => printer.printBundle(bundle)); - printsCorrectly("removeComments", { removeComments: true }, printer => printer.printBundle(bundle)); + ]); }); + 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(ts.EmitHint.Unspecified, ts.factory.createClassDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ ts.factory.createIdentifier("C"), - /*typeParameters*/ undefined, - /*heritageClauses*/ undefined, [ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, ts.factory.createNodeArray([ts.factory.createToken(ts.SyntaxKind.PublicKeyword)]), ts.factory.createIdentifier("prop"), - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined)]), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - printsCorrectly("namespaceExportDeclaration", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNamespaceExportDeclaration("B"), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - printsCorrectly("newExpressionWithPropertyAccessOnCallExpression", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNewExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier("f"), /*typeArguments*/ undefined, /*argumentsArray*/ undefined), "x"), - /*typeArguments*/ undefined, - /*argumentsArray*/ undefined), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext))); - printsCorrectly("newExpressionOnConditionalExpression", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNewExpression(ts.factory.createConditionalExpression(ts.factory.createIdentifier("x"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createIdentifier("y"), ts.factory.createToken(ts.SyntaxKind.ColonToken), ts.factory.createIdentifier("z")), - /*typeArguments*/ undefined, - /*argumentsArray*/ undefined), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext))); - printsCorrectly("emptyGlobalAugmentation", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], ts.factory.createIdentifier("global"), ts.factory.createModuleBlock(ts.emptyArray), ts.NodeFlags.GlobalAugmentation), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - printsCorrectly("emptyGlobalAugmentationWithNoDeclareKeyword", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createIdentifier("global"), ts.factory.createModuleBlock(ts.emptyArray), ts.NodeFlags.GlobalAugmentation), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - - // https://github.com/Microsoft/TypeScript/issues/15971 - printsCorrectly("classWithOptionalMethodAndProperty", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createClassDeclaration( - /*decorators*/ undefined, - /*modifiers*/ [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - /*name*/ ts.factory.createIdentifier("X"), - /*typeParameters*/ undefined, - /*heritageClauses*/ undefined, [ - ts.factory.createMethodDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ ts.factory.createIdentifier("method"), - /*questionToken*/ ts.factory.createToken(ts.SyntaxKind.QuestionToken), - /*typeParameters*/ undefined, [], - /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), - /*body*/ undefined), - ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ ts.factory.createIdentifier("property"), - /*questionToken*/ ts.factory.createToken(ts.SyntaxKind.QuestionToken), - /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - /*initializer*/ undefined), - ]), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - - // https://github.com/Microsoft/TypeScript/issues/15651 - printsCorrectly("functionTypes", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.setEmitFlags(ts.factory.createTupleTypeNode([ - ts.factory.createFunctionTypeNode( - /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ts.factory.createFunctionTypeNode([ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, "T")], [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ts.factory.createFunctionTypeNode( - /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ts.factory.createFunctionTypeNode( - /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"), ts.factory.createToken(ts.SyntaxKind.QuestionToken))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ts.factory.createFunctionTypeNode( - /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"), - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ts.factory.createFunctionTypeNode( - /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, ts.factory.createObjectBindingPattern([]))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - ]), ts.EmitFlags.SingleLine), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); - }); + describe("printNode", () => { + const printsCorrectly = makePrintsCorrectly("printsNodeCorrectly"); + printsCorrectly("class", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ ts.factory.createIdentifier("C"), + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, [ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, ts.factory.createNodeArray([ts.factory.createToken(ts.SyntaxKind.PublicKeyword)]), ts.factory.createIdentifier("prop"), + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)]), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); + printsCorrectly("namespaceExportDeclaration", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNamespaceExportDeclaration("B"), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); + printsCorrectly("newExpressionWithPropertyAccessOnCallExpression", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNewExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier("f"), /*typeArguments*/ undefined, /*argumentsArray*/ undefined), "x"), + /*typeArguments*/ undefined, + /*argumentsArray*/ undefined), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext))); + printsCorrectly("newExpressionOnConditionalExpression", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createNewExpression(ts.factory.createConditionalExpression(ts.factory.createIdentifier("x"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createIdentifier("y"), ts.factory.createToken(ts.SyntaxKind.ColonToken), ts.factory.createIdentifier("z")), + /*typeArguments*/ undefined, + /*argumentsArray*/ undefined), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext))); + printsCorrectly("emptyGlobalAugmentation", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], ts.factory.createIdentifier("global"), ts.factory.createModuleBlock(ts.emptyArray), ts.NodeFlags.GlobalAugmentation), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); + printsCorrectly("emptyGlobalAugmentationWithNoDeclareKeyword", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createIdentifier("global"), ts.factory.createModuleBlock(ts.emptyArray), ts.NodeFlags.GlobalAugmentation), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); + + // https://github.com/Microsoft/TypeScript/issues/15971 + printsCorrectly("classWithOptionalMethodAndProperty", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.factory.createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], + /*name*/ ts.factory.createIdentifier("X"), + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, [ + ts.factory.createMethodDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ ts.factory.createIdentifier("method"), + /*questionToken*/ ts.factory.createToken(ts.SyntaxKind.QuestionToken), + /*typeParameters*/ undefined, [], + /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), + /*body*/ undefined), + ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ ts.factory.createIdentifier("property"), + /*questionToken*/ ts.factory.createToken(ts.SyntaxKind.QuestionToken), + /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + /*initializer*/ undefined), + ]), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); + + // https://github.com/Microsoft/TypeScript/issues/15651 + printsCorrectly("functionTypes", {}, printer => printer.printNode(ts.EmitHint.Unspecified, ts.setEmitFlags(ts.factory.createTupleTypeNode([ + ts.factory.createFunctionTypeNode( + /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ts.factory.createFunctionTypeNode([ts.factory.createTypeParameterDeclaration(/*modifiers*/ undefined, "T")], [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ts.factory.createFunctionTypeNode( + /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), ts.factory.createIdentifier("args"))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ts.factory.createFunctionTypeNode( + /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"), ts.factory.createToken(ts.SyntaxKind.QuestionToken))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ts.factory.createFunctionTypeNode( + /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, ts.factory.createIdentifier("args"), + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ts.factory.createFunctionTypeNode( + /*typeArguments*/ undefined, [ts.factory.createParameterDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, ts.factory.createObjectBindingPattern([]))], ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ]), ts.EmitFlags.SingleLine), ts.createSourceFile("source.ts", "", ts.ScriptTarget.ES2015))); }); +}); } diff --git a/src/testRunner/unittests/programApi.ts b/src/testRunner/unittests/programApi.ts index 3a851070a9035..965b93aae9f00 100644 --- a/src/testRunner/unittests/programApi.ts +++ b/src/testRunner/unittests/programApi.ts @@ -1,218 +1,218 @@ namespace ts { - function verifyMissingFilePaths(missingPaths: readonly ts.Path[], expected: readonly string[]) { - assert.isDefined(missingPaths); - const map = new ts.Set(expected); - for (const missing of missingPaths) { - const value = map.has(missing); - assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); - map.delete(missing); - } - const notFound = ts.arrayFrom(ts.mapDefinedIterator(map.keys(), k => map.has(k) ? k : undefined)); - assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); +function verifyMissingFilePaths(missingPaths: readonly ts.Path[], expected: readonly string[]) { + assert.isDefined(missingPaths); + const map = new ts.Set(expected); + for (const missing of missingPaths) { + const value = map.has(missing); + assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); + map.delete(missing); } + const notFound = ts.arrayFrom(ts.mapDefinedIterator(map.keys(), k => map.has(k) ? k : undefined)); + assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); +} - describe("unittests:: programApi:: Program.getMissingFilePaths", () => { +describe("unittests:: programApi:: Program.getMissingFilePaths", () => { - const options: ts.CompilerOptions = { - noLib: true, - }; + const options: ts.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: ts.NewLineKind.LineFeed }); + + it("handles no missing root files", () => { + const program = ts.createProgram([emptyFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, []); + }); + + it("handles missing root file", () => { + const program = ts.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 = ts.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 = ts.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 = ts.createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); + }); + + it("normalizes file paths", () => { + const program0 = ts.createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost); + const program1 = ts.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 = ts.createProgram([referenceFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, [ + // From absolute reference + "d:/imaginary/nonexistent1.ts", + + // From relative reference + "d:/pretend/nonexistent2.ts", - 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: ts.NewLineKind.LineFeed }); - - it("handles no missing root files", () => { - const program = ts.createProgram([emptyFileRelativePath], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, []); - }); - - it("handles missing root file", () => { - const program = ts.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 = ts.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 = ts.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 = ts.createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); - }); - - it("normalizes file paths", () => { - const program0 = ts.createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost); - const program1 = ts.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 = ts.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 = ` + // 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: ts.CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget, _onError?: (message: string) => void) => { - return fileName === "test.ts" ? ts.createSourceFile(fileName, testSource, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "", - writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, - getCurrentDirectory: () => ts.sys.getCurrentDirectory(), - getCanonicalFileName: fileName => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), - getNewLine: () => ts.sys.newLine, - useCaseSensitiveFileNames: () => ts.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 = ts.createProgram(["test.ts"], { module: ts.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()?.length || 0) === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); - }); + const host: ts.CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget, _onError?: (message: string) => void) => { + return fileName === "test.ts" ? ts.createSourceFile(fileName, testSource, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "", + writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, + getCurrentDirectory: () => ts.sys.getCurrentDirectory(), + getCanonicalFileName: fileName => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), + getNewLine: () => ts.sys.newLine, + useCaseSensitiveFileNames: () => ts.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 = ts.createProgram(["test.ts"], { module: ts.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()?.length || 0) === 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 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 = ts.createProgram(["/a.ts"], ts.emptyOptions, new fakes.CompilerHost(fs, { newLine: ts.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 = ts.createProgram(["/a.ts"], ts.emptyOptions, new fakes.CompilerHost(fs, { newLine: ts.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 = ts.createProgram(["/a.ts"], ts.emptyOptions, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - assertIsExternal(program, [a, fooIndex], f => f !== a); - }); - - function assertIsExternal(program: ts.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 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 = ts.createProgram(["/a.ts"], ts.emptyOptions, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); + assertIsExternal(program, [a, fooIndex], f => f !== a); }); - 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"}'); + function assertIsExternal(program: ts.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}`); + } + } +}); + +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 = ts.createProgram(["/main.ts"], { resolveJsonModule: true }, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); + const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, pkg], cwd: "/" }); + const program = ts.createProgram(["/main.ts"], { resolveJsonModule: true }, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - const json = program.getSourceFile("/package.json")!; - assert.equal(json.scriptKind, ts.ScriptKind.JSON); - assert.isNumber(json.nodeCount); - assert.isNumber(json.identifierCount); + const json = program.getSourceFile("/package.json")!; + assert.equal(json.scriptKind, ts.ScriptKind.JSON); + assert.isNumber(json.nodeCount); + assert.isNumber(json.identifierCount); - assert.isNotNaN(program.getNodeCount()); - assert.isNotNaN(program.getIdentifierCount()); - }); + assert.isNotNaN(program.getNodeCount()); + assert.isNotNaN(program.getIdentifierCount()); }); +}); + +describe("unittests:: programApi:: Program.getTypeChecker / 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 = ts.createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); + const typeChecker = program.getTypeChecker(); + const sourceFile = program.getSourceFile("main.ts")!; + typeChecker.getTypeAtLocation(((sourceFile.statements[0] as ts.ExpressionStatement).expression as ts.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 = ts.createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - describe("unittests:: programApi:: Program.getTypeChecker / 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 = ts.createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - const typeChecker = program.getTypeChecker(); - const sourceFile = program.getSourceFile("main.ts")!; - typeChecker.getTypeAtLocation(((sourceFile.statements[0] as ts.ExpressionStatement).expression as ts.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 = ts.createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - - const sourceFile = program.getSourceFile("main.ts")!; - const typeChecker = program.getTypeChecker(); - typeChecker.getSymbolAtLocation((sourceFile.statements[0] as ts.ImportDeclaration).moduleSpecifier); - assert.isEmpty(program.getSemanticDiagnostics()); - }); + const sourceFile = program.getSourceFile("main.ts")!; + const typeChecker = program.getTypeChecker(); + typeChecker.getSymbolAtLocation((sourceFile.statements[0] as ts.ImportDeclaration).moduleSpecifier); + assert.isEmpty(program.getSemanticDiagnostics()); }); +}); - describe("unittests:: programApi:: CompilerOptions relative paths", () => { - it("resolves relative paths by getCurrentDirectory", () => { - const main = new documents.TextDocument("/main.ts", "import \"module\";"); - const mod = new documents.TextDocument("/lib/module.ts", "declare const foo: any;"); +describe("unittests:: programApi:: CompilerOptions relative paths", () => { + it("resolves relative paths by getCurrentDirectory", () => { + const main = new documents.TextDocument("/main.ts", "import \"module\";"); + const mod = new documents.TextDocument("/lib/module.ts", "declare const foo: any;"); - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" }); - const program = ts.createProgram(["./main.ts"], { - paths: { "*": ["./lib/*"] } - }, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); + const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" }); + const program = ts.createProgram(["./main.ts"], { + paths: { "*": ["./lib/*"] } + }, new fakes.CompilerHost(fs, { newLine: ts.NewLineKind.LineFeed })); - assert.isEmpty(program.getConfigFileParsingDiagnostics()); - assert.isEmpty(program.getGlobalDiagnostics()); - assert.isEmpty(program.getSemanticDiagnostics()); - }); + assert.isEmpty(program.getConfigFileParsingDiagnostics()); + assert.isEmpty(program.getGlobalDiagnostics()); + assert.isEmpty(program.getSemanticDiagnostics()); }); +}); } diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 54b8b25134c8f..5670eafe50cce 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -1,671 +1,633 @@ namespace ts { - const enum ChangedPart { - references = 1 << 0, - importsAndExports = 1 << 1, - program = 1 << 2 - } +const enum ChangedPart { + references = 1 << 0, + importsAndExports = 1 << 1, + program = 1 << 2 +} - const newLine = "\r\n"; +const newLine = "\r\n"; - interface SourceFileWithText extends ts.SourceFile { - sourceText?: SourceText; - } - - export interface NamedSourceText { - name: string; - text: SourceText; - } +interface SourceFileWithText extends ts.SourceFile { + sourceText?: SourceText; +} - export interface ProgramWithSourceTexts extends ts.Program { - sourceTexts?: readonly NamedSourceText[]; - host: TestCompilerHost; - } +export interface NamedSourceText { + name: string; + text: SourceText; +} - interface TestCompilerHost extends ts.CompilerHost { - getTrace(): string[]; - } +export interface ProgramWithSourceTexts extends ts.Program { + sourceTexts?: readonly NamedSourceText[]; + host: TestCompilerHost; +} - export class SourceText implements ts.IScriptSnapshot { - private fullText: string | undefined; +interface TestCompilerHost extends ts.CompilerHost { + getTrace(): string[]; +} - constructor(private references: string, private importsAndExports: string, private program: string, private changedPart: ChangedPart = 0, private version = 0) { - } +export class SourceText implements ts.IScriptSnapshot { + private fullText: string | undefined; - static New(references: string, importsAndExports: string, program: string): SourceText { - ts.Debug.assert(references !== undefined); - ts.Debug.assert(importsAndExports !== undefined); - ts.Debug.assert(program !== undefined); - return new SourceText(references + newLine, importsAndExports + newLine, program || ""); - } + constructor(private references: string, private importsAndExports: string, private program: string, private changedPart: ChangedPart = 0, private version = 0) { + } - public getVersion(): number { - return this.version; - } + static New(references: string, importsAndExports: string, program: string): SourceText { + ts.Debug.assert(references !== undefined); + ts.Debug.assert(importsAndExports !== undefined); + ts.Debug.assert(program !== undefined); + return new SourceText(references + newLine, importsAndExports + newLine, program || ""); + } - public updateReferences(newReferences: string): SourceText { - ts.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 { - ts.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 { - ts.Debug.assert(newProgram !== undefined); - return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); - } + public getVersion(): number { + return this.version; + } - public getFullText() { - return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); - } + public updateReferences(newReferences: string): SourceText { + ts.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 { + ts.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 { + ts.Debug.assert(newProgram !== undefined); + return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); + } - public getText(start: number, end: number): string { - return this.getFullText().substring(start, end); - } + public getFullText() { + return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); + } - getLength(): number { - return this.getFullText().length; - } + public getText(start: number, end: number): string { + return this.getFullText().substring(start, end); + } - getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { - const oldText = oldSnapshot as SourceText; - let oldSpan: ts.TextSpan; - let newLength: number; - switch (oldText.changedPart ^ this.changedPart) { - case ChangedPart.references: - oldSpan = ts.createTextSpan(0, oldText.references.length); - newLength = this.references.length; - break; - case ChangedPart.importsAndExports: - oldSpan = ts.createTextSpan(oldText.references.length, oldText.importsAndExports.length); - newLength = this.importsAndExports.length; - break; - case ChangedPart.program: - oldSpan = ts.createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); - newLength = this.program.length; - break; - default: - return ts.Debug.fail("Unexpected change"); - } + getLength(): number { + return this.getFullText().length; + } - return ts.createTextChangeRange(oldSpan, newLength); + getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { + const oldText = oldSnapshot as SourceText; + let oldSpan: ts.TextSpan; + let newLength: number; + switch (oldText.changedPart ^ this.changedPart) { + case ChangedPart.references: + oldSpan = ts.createTextSpan(0, oldText.references.length); + newLength = this.references.length; + break; + case ChangedPart.importsAndExports: + oldSpan = ts.createTextSpan(oldText.references.length, oldText.importsAndExports.length); + newLength = this.importsAndExports.length; + break; + case ChangedPart.program: + oldSpan = ts.createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); + newLength = this.program.length; + break; + default: + return ts.Debug.fail("Unexpected change"); } - } - function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ts.ScriptTarget) { - const file = ts.createSourceFile(fileName, sourceText.getFullText(), target) as SourceFileWithText; - file.sourceText = sourceText; - file.version = "" + sourceText.getVersion(); - return file; + return ts.createTextChangeRange(oldSpan, newLength); } +} - export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ts.ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) { - const files = ts.arrayToMap(texts, t => t.name, t => { - if (oldProgram) { - let oldFile = oldProgram.getSourceFile(t.name) as SourceFileWithText; - if (oldFile && oldFile.redirectInfo) { - oldFile = oldFile.redirectInfo.unredirected; - } - if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) { - return oldFile; - } +function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ts.ScriptTarget) { + const file = ts.createSourceFile(fileName, sourceText.getFullText(), target) as SourceFileWithText; + file.sourceText = sourceText; + file.version = "" + sourceText.getVersion(); + return file; +} + +export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ts.ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) { + const files = ts.arrayToMap(texts, t => t.name, t => { + if (oldProgram) { + let oldFile = oldProgram.getSourceFile(t.name) as SourceFileWithText; + 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 = ts.sys && ts.sys.useCaseSensitiveFileNames; - const getCanonicalFileName = ts.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: ts.notImplemented, - getCurrentDirectory: () => "", - getDirectories: () => [], - getCanonicalFileName, - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getNewLine: () => ts.sys ? ts.sys.newLine : newLine, - fileExists: fileName => files.has(fileName), - readFile: fileName => { - const file = files.get(fileName); - return file && file.text; - }, - }; - if (useGetSourceFileByPath) { - const filesByPath = ts.mapEntries(files, (fileName, file) => [ts.toPath(fileName, "", getCanonicalFileName), file]); - result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path); } - return result; + return createSourceFileWithText(t.name, t.text, target); + }); + const useCaseSensitiveFileNames = ts.sys && ts.sys.useCaseSensitiveFileNames; + const getCanonicalFileName = ts.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: ts.notImplemented, + getCurrentDirectory: () => "", + getDirectories: () => [], + getCanonicalFileName, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getNewLine: () => ts.sys ? ts.sys.newLine : newLine, + fileExists: fileName => files.has(fileName), + readFile: fileName => { + const file = files.get(fileName); + return file && file.text; + }, + }; + if (useGetSourceFileByPath) { + const filesByPath = ts.mapEntries(files, (fileName, file) => [ts.toPath(fileName, "", getCanonicalFileName), file]); + result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path); } + return result; +} - export function newProgram(texts: NamedSourceText[], rootNames: string[], options: ts.CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts { - const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath); - const program = ts.createProgram(rootNames, options, host) as ProgramWithSourceTexts; - program.sourceTexts = texts; - program.host = host; - return program; - } +export function newProgram(texts: NamedSourceText[], rootNames: string[], options: ts.CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts { + const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath); + const program = ts.createProgram(rootNames, options, host) as ProgramWithSourceTexts; + program.sourceTexts = texts; + program.host = host; + return program; +} - export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: ts.CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) { - if (!newTexts) { - newTexts = oldProgram.sourceTexts!.slice(0); - } - updater(newTexts); - const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath); - const program = ts.createProgram(rootNames, options, host, oldProgram) as ProgramWithSourceTexts; - program.sourceTexts = newTexts; - program.host = host; - return program; +export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: ts.CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) { + if (!newTexts) { + newTexts = oldProgram.sourceTexts!.slice(0); } + updater(newTexts); + const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath); + const program = ts.createProgram(rootNames, options, host, oldProgram) as ProgramWithSourceTexts; + program.sourceTexts = newTexts; + program.host = host; + return program; +} - export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) { - const file = ts.find(files, f => f.name === fileName)!; - file.text = file.text.updateProgram(newProgramText); - } +export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) { + const file = ts.find(files, f => f.name === fileName)!; + file.text = file.text.updateProgram(newProgramText); +} - function checkResolvedTypeDirective(actual: ts.ResolvedTypeReferenceDirective, expected: ts.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 checkResolvedTypeDirective(actual: ts.ResolvedTypeReferenceDirective, expected: ts.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: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined, getCache: (f: ts.SourceFile) => ts.ModeAwareCache | 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(mapEqualToCache(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`); - } +function checkCache(caption: string, program: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined, getCache: (f: ts.SourceFile) => ts.ModeAwareCache | 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`); } - - /** True if the maps have the same keys and values. */ - function mapEqualToCache(left: ts.ESMap, right: ts.ModeAwareCache, valuesAreEqual?: (left: T, right: T) => boolean): boolean { - if (left as any === right) - return true; // given the type mismatch (the tests never pass a cache), this'll never be true - if (!left || !right) - return false; - const someInLeftHasNoMatch = ts.forEachEntry(left, (leftValue, leftKey) => { - if (!right.has(leftKey, /*mode*/ undefined)) - return true; - const rightValue = right.get(leftKey, /*mode*/ undefined)!; - return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue); - }); - if (someInLeftHasNoMatch) - return false; - let someInRightHasNoMatch = false; - right.forEach((_, rightKey) => someInRightHasNoMatch = someInRightHasNoMatch || !left.has(rightKey)); - return !someInRightHasNoMatch; + else { + assert.isTrue(cache !== undefined, `expected ${caption} to be set`); + assert.isTrue(mapEqualToCache(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`); } +} - function checkResolvedModulesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined): void { - checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, ts.checkResolvedModule); - } +/** True if the maps have the same keys and values. */ +function mapEqualToCache(left: ts.ESMap, right: ts.ModeAwareCache, valuesAreEqual?: (left: T, right: T) => boolean): boolean { + if (left as any === right) + return true; // given the type mismatch (the tests never pass a cache), this'll never be true + if (!left || !right) + return false; + const someInLeftHasNoMatch = ts.forEachEntry(left, (leftValue, leftKey) => { + if (!right.has(leftKey, /*mode*/ undefined)) + return true; + const rightValue = right.get(leftKey, /*mode*/ undefined)!; + return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue); + }); + if (someInLeftHasNoMatch) + return false; + let someInRightHasNoMatch = false; + right.forEach((_, rightKey) => someInRightHasNoMatch = someInRightHasNoMatch || !left.has(rightKey)); + return !someInRightHasNoMatch; +} - function checkResolvedTypeDirectivesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined): void { - checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); - } +function checkResolvedModulesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined): void { + checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, ts.checkResolvedModule); +} - describe("unittests:: Reuse program structure:: General", () => { - const target = ts.ScriptTarget.Latest; - const files: NamedSourceText[] = [ - { - name: "a.ts", text: SourceText.New(` +function checkResolvedTypeDirectivesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap | undefined): void { + checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); +} + +describe("unittests:: Reuse program structure:: General", () => { + const target = ts.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(program2.structureIsReused, ts.StructureIsReused.Completely); - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - const program2Diagnostics = program2.getSemanticDiagnostics(program2.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"); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + const program2Diagnostics = program2.getSemanticDiagnostics(program2.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"); - }); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - const program2Diagnostics = program2.getSemanticDiagnostics(program2.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"); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("a.ts")); + assert.equal(program1Diagnostics.length, program2Diagnostics.length); + }); - it("successful if change affects a single module of a package", () => { - const files = [ - { name: "/a.ts", text: SourceText.New("", "import {b} from 'b'", "var a = b;") }, - { name: "/node_modules/b/index.d.ts", text: SourceText.New("", "export * from './internal';", "") }, - { name: "/node_modules/b/internal.d.ts", text: SourceText.New("", "", "export const b = 1;") }, - { name: "/node_modules/b/package.json", text: SourceText.New("", "", JSON.stringify({ name: "b", version: "1.2.3" })) }, - ]; + it("successful if change affects a single module of a package", () => { + const files = [ + { name: "/a.ts", text: SourceText.New("", "import {b} from 'b'", "var a = b;") }, + { name: "/node_modules/b/index.d.ts", text: SourceText.New("", "export * from './internal';", "") }, + { name: "/node_modules/b/internal.d.ts", text: SourceText.New("", "", "export const b = 1;") }, + { name: "/node_modules/b/package.json", text: SourceText.New("", "", JSON.stringify({ name: "b", version: "1.2.3" })) }, + ]; - const options: ts.CompilerOptions = { target, moduleResolution: ts.ModuleResolutionKind.NodeJs }; - const program1 = newProgram(files, ["/a.ts"], options); - const program2 = updateProgram(program1, ["/a.ts"], options, files => { - files[2].text = files[2].text.updateProgram("export const b = 2;"); - }); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("a.ts")); - assert.equal(program1Diagnostics.length, program2Diagnostics.length); + const options: ts.CompilerOptions = { target, moduleResolution: ts.ModuleResolutionKind.NodeJs }; + const program1 = newProgram(files, ["/a.ts"], options); + const program2 = updateProgram(program1, ["/a.ts"], options, files => { + files[2].text = files[2].text.updateProgram("export const b = 2;"); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("a.ts")); + assert.equal(program1Diagnostics.length, program2Diagnostics.length); + }); - it("fails if change affects tripleslash references", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - const program2 = updateProgram(program1, ["a.ts"], { target }, files => { - const newReferences = `/// + it("fails if change affects tripleslash references", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + const program2 = updateProgram(program1, ["a.ts"], { target }, files => { + const newReferences = `/// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + files[0].text = files[0].text.updateReferences(newReferences); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + }); - it("fails if change affects type references", () => { - const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); - const program2 = updateProgram(program1, ["a.ts"], { types: ["b"] }, ts.noop); - assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); - }); + it("fails if change affects type references", () => { + const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); + const program2 = updateProgram(program1, ["a.ts"], { types: ["b"] }, ts.noop); + assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + }); - it("succeeds if change doesn't affect type references", () => { - const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); - const program2 = updateProgram(program1, ["a.ts"], { types: ["a"] }, ts.noop); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - }); + it("succeeds if change doesn't affect type references", () => { + const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); + const program2 = updateProgram(program1, ["a.ts"], { types: ["a"] }, ts.noop); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + }); - it("fails if change affects imports", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - const program2 = updateProgram(program1, ["a.ts"], { target }, files => { - files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); - }); - assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + it("fails if change affects imports", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + const program2 = updateProgram(program1, ["a.ts"], { target }, files => { + files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + }); - it("fails if change affects type directives", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - const program2 = updateProgram(program1, ["a.ts"], { target }, files => { - const newReferences = ` + it("fails if change affects type directives", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + const program2 = updateProgram(program1, ["a.ts"], { target }, files => { + const newReferences = ` /// /// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + files[0].text = files[0].text.updateReferences(newReferences); }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.SafeModules); + }); - it("fails if module kind changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS }); - const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.AMD }, ts.noop); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); - }); + it("fails if module kind changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS }); + const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.AMD }, ts.noop); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); + }); - it("succeeds if rootdir changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, rootDir: "/a/b" }); - const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, rootDir: "/a/c" }, ts.noop); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - }); + it("succeeds if rootdir changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, rootDir: "/a/b" }); + const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, rootDir: "/a/c" }, ts.noop); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + }); - it("fails if config path changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); - const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, ts.noop); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); - }); + it("fails if config path changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); + const program2 = updateProgram(program1, ["a.ts"], { target, module: ts.ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, ts.noop); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); + }); - it("succeeds if missing files remain missing", () => { - const options: ts.CompilerOptions = { target, noLib: true }; + it("succeeds if missing files remain missing", () => { + const options: ts.CompilerOptions = { target, noLib: true }; - const program1 = newProgram(files, ["a.ts"], options); - assert.notDeepEqual(ts.emptyArray, program1.getMissingFilePaths()); - const program2 = updateProgram(program1, ["a.ts"], options, ts.noop); - assert.deepEqual(program1.getMissingFilePaths(), program2.getMissingFilePaths()); + const program1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(ts.emptyArray, program1.getMissingFilePaths()); + const program2 = updateProgram(program1, ["a.ts"], options, ts.noop); + assert.deepEqual(program1.getMissingFilePaths(), program2.getMissingFilePaths()); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); + }); - it("fails if missing file is created", () => { - const options: ts.CompilerOptions = { target, noLib: true }; + it("fails if missing file is created", () => { + const options: ts.CompilerOptions = { target, noLib: true }; - const program1 = newProgram(files, ["a.ts"], options); - assert.notDeepEqual(ts.emptyArray, program1.getMissingFilePaths()); + const program1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(ts.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, ts.noop, newTexts); - assert.lengthOf(program2.getMissingFilePaths(), 0); + const newTexts: NamedSourceText[] = files.concat([{ name: "non-existing-file.ts", text: SourceText.New("", "", `var x = 1`) }]); + const program2 = updateProgram(program1, ["a.ts"], options, ts.noop, newTexts); + assert.lengthOf(program2.getMissingFilePaths(), 0); - assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); - }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Not); + }); - it("resolution cache follows imports", () => { - (Error as any).stackTraceLimit = Infinity; + it("resolution cache follows imports", () => { + (Error as any).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: ts.CompilerOptions = { target }; + 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: ts.CompilerOptions = { target }; - const program1 = newProgram(files, ["a.ts"], options); - checkResolvedModulesCache(program1, "a.ts", new ts.Map(ts.getEntries({ b: ts.createResolvedModule("b.ts") }))); - checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); + const program1 = newProgram(files, ["a.ts"], options); + checkResolvedModulesCache(program1, "a.ts", new ts.Map(ts.getEntries({ b: ts.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(program2.structureIsReused, ts.StructureIsReused.Completely); + const program2 = updateProgram(program1, ["a.ts"], options, files => { + files[0].text = files[0].text.updateProgram("var x = 2"); + }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - // content of resolution cache should not change - checkResolvedModulesCache(program1, "a.ts", new ts.Map(ts.getEntries({ b: ts.createResolvedModule("b.ts") }))); - checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); + // content of resolution cache should not change + checkResolvedModulesCache(program1, "a.ts", new ts.Map(ts.getEntries({ b: ts.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(program3.structureIsReused, ts.StructureIsReused.SafeModules); - checkResolvedModulesCache(program3, "a.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(program3.structureIsReused, ts.StructureIsReused.SafeModules); + checkResolvedModulesCache(program3, "a.ts", /*expectedContent*/ undefined); - const program4 = updateProgram(program3, ["a.ts"], options, files => { - const newImports = `import x from 'b' + 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(program4.structureIsReused, ts.StructureIsReused.SafeModules); - checkResolvedModulesCache(program4, "a.ts", new ts.Map(ts.getEntries({ b: ts.createResolvedModule("b.ts"), c: undefined }))); + files[0].text = files[0].text.updateImportsAndExports(newImports); }); + assert.equal(program4.structureIsReused, ts.StructureIsReused.SafeModules); + checkResolvedModulesCache(program4, "a.ts", new ts.Map(ts.getEntries({ b: ts.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: ts.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", /*mode*/ undefined), "'a' is not an unresolved module after re-use"); + 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: ts.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", /*mode*/ undefined), "'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: ts.CompilerOptions = { target, typeRoots: ["/types"] }; - const program1 = ts.createProgram(["/a.ts"], options, host); - let sourceFile = program1.getSourceFile("/a.ts")!; - assert.isDefined(sourceFile, "'/a.ts' is included in the program"); - sourceFile = ts.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 = ts.createProgram(["/a.ts"], options, updateHost, program1); - assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a", /*mode*/ undefined), "'a' is not an unresolved module after re-use"); - assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are not altered"); - }); + 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: ts.CompilerOptions = { target, typeRoots: ["/types"] }; + const program1 = ts.createProgram(["/a.ts"], options, host); + let sourceFile = program1.getSourceFile("/a.ts")!; + assert.isDefined(sourceFile, "'/a.ts' is included in the program"); + sourceFile = ts.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 = ts.createProgram(["/a.ts"], options, updateHost, program1); + assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a", /*mode*/ undefined), "'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: ts.CompilerOptions = { target, typeRoots: ["/types"] }; + 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: ts.CompilerOptions = { target, typeRoots: ["/types"] }; - const program1 = newProgram(files, ["/a.ts"], options); - checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }))); - checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); + const program1 = newProgram(files, ["/a.ts"], options); + checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ 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(program2.structureIsReused, ts.StructureIsReused.Completely); + const program2 = updateProgram(program1, ["/a.ts"], options, files => { + files[0].text = files[0].text.updateProgram("var x = 2"); + }); + assert.equal(program2.structureIsReused, ts.StructureIsReused.Completely); - // content of resolution cache should not change - checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }))); - checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); + // content of resolution cache should not change + checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ 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(""); - }); + // 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(program3.structureIsReused, ts.StructureIsReused.SafeModules); - checkResolvedTypeDirectivesCache(program3, "/a.ts", /*expectedContent*/ undefined); + assert.equal(program3.structureIsReused, ts.StructureIsReused.SafeModules); + checkResolvedTypeDirectivesCache(program3, "/a.ts", /*expectedContent*/ undefined); - const program4 = updateProgram(program3, ["/a.ts"], options, files => { - const newReferences = `/// + const program4 = updateProgram(program3, ["/a.ts"], options, files => { + const newReferences = `/// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program4.structureIsReused, ts.StructureIsReused.SafeModules); - checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }))); + files[0].text = files[0].text.updateReferences(newReferences); }); + assert.equal(program4.structureIsReused, ts.StructureIsReused.SafeModules); + checkResolvedTypeDirectivesCache(program1, "/a.ts", new ts.Map(ts.getEntries({ 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: ts.CompilerOptions = { target: ts.ScriptTarget.ES2015, traceResolution: true, moduleResolution: ts.ModuleResolutionKind.NodeJs }; - const rootFiles = [file1Ts, file2Ts]; - const filesAfterNpmInstall = [file1Ts, file2Ts, indexDTS]; + 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: ts.CompilerOptions = { target: ts.ScriptTarget.ES2015, traceResolution: true, moduleResolution: ts.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 according to earlier cached lookups.", + "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 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 according to earlier cached lookups.", - "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.`); + } + }); - 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: ts.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"); - 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: ts.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"); + 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;") }, - { - 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: "f2.ts", - text: SourceText.New(`/// ${newLine}/// `, `import { B } from './b2';${newLine}import { BB } from './f1';`, "(new BB).x; (new BB).y;") - }, - ]; - - const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2015, traceResolution: true, moduleResolution: ts.ModuleResolutionKind.Classic }; - const program1 = newProgram(files, files.map(f => f.name), options); - let expectedErrors = 0; + 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(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 }; - }); - + name: "f1.ts", + text: SourceText.New(`/// ${newLine}/// ${newLine}/// `, `import { B } from './b1';${newLine}export let BB = B;`, "declare module './b1' { interface B { y: string; } }") + }, { - const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("f2.ts")); - assert.lengthOf(program2Diagnostics, expectedErrors, `removing no-default-lib shouldn't affect any types used.`); + name: "f2.ts", + text: SourceText.New(`/// ${newLine}/// `, `import { B } from './b2';${newLine}import { BB } from './f1';`, "(new BB).x; (new BB).y;") + }, + ]; - assert.deepEqual(program2.host.getTrace(), [ + const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2015, traceResolution: true, moduleResolution: ts.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.", @@ -680,473 +642,511 @@ namespace ts { "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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", - "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'." - ], "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 }; - }); + "======== 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 program3Diagnostics = program3.getSemanticDiagnostics(program3.getSourceFile("f2.ts")); - assert.lengthOf(program3Diagnostics, expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`); + 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 }; + }); - 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", - "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'." - ], "program3: reuse module resolutions in f2 since it is unchanged"); - } + { + 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", + "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'." + ], "program2: 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 program3 = updateProgram(program2, program2.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.`); + { + const program3Diagnostics = program3.getSemanticDiagnostics(program3.getSourceFile("f2.ts")); + assert.lengthOf(program3Diagnostics, expectedErrors, `typerefs2 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", - "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", - ], "program_4: reuse module resolutions in f2 since it is unchanged"); - } + 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", + "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'." + ], "program3: 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`); + const program4 = updateProgram(program3, program3.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); - 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 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", + "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", + ], "program_4: reuse module resolutions in f2 since it is unchanged"); + } - const program6 = updateProgram(program5, program5.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateProgram(""); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; - }); + 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 program6Diagnostics = program6.getSemanticDiagnostics(program6.getSourceFile("f2.ts")); - assert.lengthOf(program6Diagnostics, expectedErrors, `import of BB in f1 fails.`); + { + 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(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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", - "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", - ], "program_6: reuse module resolutions in f2 since it is unchanged"); - } + 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 program7 = updateProgram(program6, program6.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateImportsAndExports(""); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; - }); + const program6 = updateProgram(program5, program5.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateProgram(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); - { - 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.`); + { + 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", + "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", + ], "program_6: reuse module resolutions in f2 since it is unchanged"); + } - 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", - "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", - ], "program_7 should 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 }; }); - 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: ts.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); - } + { + 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' from 'f2.ts' of old program, it was successfully resolved to 'b2.ts'.", + "Reusing resolution of module './f1' from 'f2.ts' of old program, it was successfully resolved to 'f1.ts'.", + ], "program_7 should reuse module resolutions in f2 since it is unchanged"); + } + }); - function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void, useGetSourceFileByPath: boolean): ProgramWithSourceTexts { - return updateProgram(program, [root], compilerOptions, updater, /*newTexts*/ undefined, useGetSourceFileByPath); - } + 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: ts.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)"), + }, + ]; - 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(program2.structureIsReused, ts.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(program2.structureIsReused, ts.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(program2.structureIsReused, ts.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(program2.structureIsReused, ts.StructureIsReused.Not); - assert.deepEqual(program2.getSemanticDiagnostics(), []); - }); - } + 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(program2.structureIsReused, ts.StructureIsReused.Completely); + assert.lengthOf(program2.getSemanticDiagnostics(), 0); + }); - describe("when host implements getSourceFile", () => { - verifyRedirects(/*useGetSourceFileByPath*/ false); + 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(program2.structureIsReused, ts.StructureIsReused.Not); + assert.lengthOf(program2.getSemanticDiagnostics(), 1); }); - describe("when host implements getSourceFileByPath", () => { - verifyRedirects(/*useGetSourceFileByPath*/ true); + + 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(program2.structureIsReused, ts.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(program2.structureIsReused, ts.StructureIsReused.Not); + assert.deepEqual(program2.getSemanticDiagnostics(), []); }); + } + + describe("when host implements getSourceFile", () => { + verifyRedirects(/*useGetSourceFileByPath*/ false); + }); + describe("when host implements getSourceFileByPath", () => { + verifyRedirects(/*useGetSourceFileByPath*/ true); }); }); +}); - describe("unittests:: Reuse program structure:: host is optional", () => { - it("should work if host is not provided", () => { - ts.createProgram([], {}); - }); +describe("unittests:: Reuse program structure:: host is optional", () => { + it("should work if host is not provided", () => { + ts.createProgram([], {}); }); +}); - type File = ts.TestFSWithWatch.File; - import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; - import libFile = ts.TestFSWithWatch.libFile; +type File = ts.TestFSWithWatch.File; +import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; +import libFile = ts.TestFSWithWatch.libFile; - describe("unittests:: Reuse program structure:: isProgramUptoDate", () => { - function getWhetherProgramIsUptoDate(program: ts.Program, newRootFileNames: string[], newOptions: ts.CompilerOptions) { - return ts.isProgramUptoDate(program, newRootFileNames, newOptions, path => program.getSourceFileByPath(path)!.version, ts.returnFalse, ts.returnFalse, - /*hasChangedAutomaticTypeDirectiveNames*/ undefined, ts.returnUndefined, - /*projectReferences*/ undefined); - } +describe("unittests:: Reuse program structure:: isProgramUptoDate", () => { + function getWhetherProgramIsUptoDate(program: ts.Program, newRootFileNames: string[], newOptions: ts.CompilerOptions) { + return ts.isProgramUptoDate(program, newRootFileNames, newOptions, path => program.getSourceFileByPath(path)!.version, ts.returnFalse, ts.returnFalse, + /*hasChangedAutomaticTypeDirectiveNames*/ undefined, ts.returnUndefined, + /*projectReferences*/ undefined); + } - function duplicate(options: ts.CompilerOptions): ts.CompilerOptions; - function duplicate(fileNames: string[]): string[]; - function duplicate(filesOrOptions: ts.CompilerOptions | string[]) { - return JSON.parse(JSON.stringify(filesOrOptions)); + function duplicate(options: ts.CompilerOptions): ts.CompilerOptions; + function duplicate(fileNames: string[]): string[]; + function duplicate(filesOrOptions: ts.CompilerOptions | string[]) { + return JSON.parse(JSON.stringify(filesOrOptions)); + } + + describe("should return true when there is no change in compiler options and", () => { + function verifyProgramIsUptoDate(program: ts.Program, newRootFileNames: string[], newOptions: ts.CompilerOptions) { + const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); + assert.isTrue(actual); } - describe("should return true when there is no change in compiler options and", () => { - function verifyProgramIsUptoDate(program: ts.Program, newRootFileNames: string[], newOptions: ts.CompilerOptions) { - const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); - assert.isTrue(actual); - } + function verifyProgramWithoutConfigFile(system: ts.System, rootFiles: string[], options: ts.CompilerOptions) { + const program = ts.createWatchProgram(ts.createWatchCompilerHostOfFilesAndCompilerOptions({ + rootFiles, + options, + watchOptions: undefined, + system + })).getCurrentProgram().getProgram(); + verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); + } - function verifyProgramWithoutConfigFile(system: ts.System, rootFiles: string[], options: ts.CompilerOptions) { - const program = ts.createWatchProgram(ts.createWatchCompilerHostOfFilesAndCompilerOptions({ - rootFiles, - options, - watchOptions: undefined, - system - })).getCurrentProgram().getProgram(); - verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); - } + function verifyProgramWithConfigFile(system: ts.System, configFileName: string) { + const program = ts.createWatchProgram(ts.createWatchCompilerHostOfConfigFile({ + configFileName, + system + })).getCurrentProgram().getProgram(); + const { fileNames, options } = ts.parseConfigFileWithSystem(configFileName, {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, ts.notImplemented)!; // TODO: GH#18217 + verifyProgramIsUptoDate(program, fileNames, options); + } - function verifyProgramWithConfigFile(system: ts.System, configFileName: string) { - const program = ts.createWatchProgram(ts.createWatchCompilerHostOfConfigFile({ - configFileName, - system - })).getCurrentProgram().getProgram(); - const { fileNames, options } = ts.parseConfigFileWithSystem(configFileName, {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, ts.notImplemented)!; // TODO: GH#18217 - verifyProgramIsUptoDate(program, fileNames, options); - } + function verifyProgram(files: File[], rootFiles: string[], options: ts.CompilerOptions, configFile: string) { + const system = createTestSystem(files); + verifyProgramWithoutConfigFile(system, rootFiles, options); + verifyProgramWithConfigFile(system, configFile); + } - function verifyProgram(files: File[], rootFiles: string[], options: ts.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 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: ts.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 {}" + }; - it("has lib specified in the options", () => { - const compilerOptions: ts.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); - }); + verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); + }); - it("has paths specified in the options", () => { - const compilerOptions: ts.CompilerOptions = { - baseUrl: ".", - paths: { - "*": [ - "packages/mail/data/*", - "packages/styles/*", - "*" - ] - } - }; - const app: File = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ + it("has paths specified in the options", () => { + const compilerOptions: ts.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); - }); + }; + 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: ts.CompilerOptions = { - baseUrl: ".", - paths: { - "*": [ - "packages/mail/data/*", - "packages/styles/*", - "*" - ] - } - }; - const app: File = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ + it("has include paths specified in tsconfig file", () => { + const compilerOptions: ts.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 = ts.createWatchProgram(ts.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); + }); + 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 = ts.createWatchProgram(ts.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: ts.Program, newRootFileNames: string[], newOptions: ts.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 = ts.createWatchProgram(ts.createWatchCompilerHostOfFilesAndCompilerOptions({ + rootFiles, + options, + watchOptions: undefined, + system + })).getCurrentProgram().getProgram(); + verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options)); }); - describe("should return false when there is no change in compiler options but", () => { - function verifyProgramIsNotUptoDate(program: ts.Program, newRootFileNames: string[], newOptions: ts.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 = ts.createWatchProgram(ts.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 = ts.createWatchProgram(ts.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 = ts.createWatchProgram(ts.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 d974ff0d2a77a..8202134838337 100644 --- a/src/testRunner/unittests/semver.ts +++ b/src/testRunner/unittests/semver.ts @@ -1,257 +1,257 @@ namespace ts { - import theory = Utils.theory; - describe("unittests:: semver", () => { - describe("VersionRange", () => { - function assertVersionRange(version: string, good: string[], bad: string[]): () => void { - return () => { - const range = ts.VersionRange.tryParse(version)!; - assert(range); - for (const g of good) { - assert.isTrue(range.test(g), g); - } - for (const b of bad) { - assert.isFalse(range.test(b), b); - } - }; - } - it("< works", assertVersionRange("<3.8.0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); - it("<= works", assertVersionRange("<=3.8.0", ["3.6", "3.7", "3.8"], ["3.9", "4.0"])); - it("> works", assertVersionRange(">3.8.0", ["3.9", "4.0"], ["3.6", "3.7", "3.8"])); - it(">= works", assertVersionRange(">=3.8.0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); +import theory = Utils.theory; +describe("unittests:: semver", () => { + describe("VersionRange", () => { + function assertVersionRange(version: string, good: string[], bad: string[]): () => void { + return () => { + const range = ts.VersionRange.tryParse(version)!; + assert(range); + for (const g of good) { + assert.isTrue(range.test(g), g); + } + for (const b of bad) { + assert.isFalse(range.test(b), b); + } + }; + } + it("< works", assertVersionRange("<3.8.0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); + it("<= works", assertVersionRange("<=3.8.0", ["3.6", "3.7", "3.8"], ["3.9", "4.0"])); + it("> works", assertVersionRange(">3.8.0", ["3.9", "4.0"], ["3.6", "3.7", "3.8"])); + it(">= works", assertVersionRange(">=3.8.0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); - it("< works with prerelease", assertVersionRange("<3.8.0-0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); - it("<= works with prerelease", assertVersionRange("<=3.8.0-0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); - it("> works with prerelease", assertVersionRange(">3.8.0-0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); - it(">= works with prerelease", assertVersionRange(">=3.8.0-0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); - }); - describe("Version", () => { - function assertVersion(version: ts.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 || ts.emptyArray); - assert.deepEqual(version.build, build || ts.emptyArray); - } - describe("new", () => { - it("text", () => { - assertVersion(new ts.Version("1.2.3-pre.4+build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); - }); - it("parts", () => { - assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); - assertVersion(new ts.Version(1, 2, 3), [1, 2, 3]); - assertVersion(new ts.Version(1, 2), [1, 2, 0]); - assertVersion(new ts.Version(1), [1, 0, 0]); - }); + it("< works with prerelease", assertVersionRange("<3.8.0-0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); + it("<= works with prerelease", assertVersionRange("<=3.8.0-0", ["3.6", "3.7"], ["3.8", "3.9", "4.0"])); + it("> works with prerelease", assertVersionRange(">3.8.0-0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); + it(">= works with prerelease", assertVersionRange(">=3.8.0-0", ["3.8", "3.9", "4.0"], ["3.6", "3.7"])); + }); + describe("Version", () => { + function assertVersion(version: ts.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 || ts.emptyArray); + assert.deepEqual(version.build, build || ts.emptyArray); + } + describe("new", () => { + it("text", () => { + assertVersion(new ts.Version("1.2.3-pre.4+build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); }); - it("toString", () => { - assert.strictEqual(new ts.Version(1, 2, 3, "pre.4", "build.5").toString(), "1.2.3-pre.4+build.5"); - assert.strictEqual(new ts.Version(1, 2, 3, "pre.4").toString(), "1.2.3-pre.4"); - assert.strictEqual(new ts.Version(1, 2, 3, /*prerelease*/ undefined, "build.5").toString(), "1.2.3+build.5"); - assert.strictEqual(new ts.Version(1, 2, 3).toString(), "1.2.3"); - assert.strictEqual(new ts.Version(1, 2).toString(), "1.2.0"); - assert.strictEqual(new ts.Version(1).toString(), "1.0.0"); + it("parts", () => { + assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); + assertVersion(new ts.Version(1, 2, 3), [1, 2, 3]); + assertVersion(new ts.Version(1, 2), [1, 2, 0]); + assertVersion(new ts.Version(1), [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 ts.Version("1.0.0").compareTo(new ts.Version("2.0.0")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.1.0")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.0.1")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("2.0.0").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.1.0").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.1").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.0.0")), ts.Comparison.EqualTo); + }); + it("toString", () => { + assert.strictEqual(new ts.Version(1, 2, 3, "pre.4", "build.5").toString(), "1.2.3-pre.4+build.5"); + assert.strictEqual(new ts.Version(1, 2, 3, "pre.4").toString(), "1.2.3-pre.4"); + assert.strictEqual(new ts.Version(1, 2, 3, /*prerelease*/ undefined, "build.5").toString(), "1.2.3+build.5"); + assert.strictEqual(new ts.Version(1, 2, 3).toString(), "1.2.3"); + assert.strictEqual(new ts.Version(1, 2).toString(), "1.2.0"); + assert.strictEqual(new ts.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 ts.Version("1.0.0").compareTo(new ts.Version("2.0.0")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.1.0")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.0.1")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("2.0.0").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.1.0").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.1").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0").compareTo(new ts.Version("1.0.0")), ts.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 ts.Version("1.0.0").compareTo(new ts.Version("1.0.0-pre")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.1-pre").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-pre").compareTo(new ts.Version("1.0.0")), ts.Comparison.LessThan); + // 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 ts.Version("1.0.0").compareTo(new ts.Version("1.0.0-pre")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.1-pre").compareTo(new ts.Version("1.0.0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-pre").compareTo(new ts.Version("1.0.0")), ts.Comparison.LessThan); - // https://semver.org/#spec-item-11 - // > identifiers consisting of only digits are compared numerically - assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-1")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-1").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-2").compareTo(new ts.Version("1.0.0-10")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-10").compareTo(new ts.Version("1.0.0-2")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.EqualTo); + // https://semver.org/#spec-item-11 + // > identifiers consisting of only digits are compared numerically + assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-1")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-1").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-2").compareTo(new ts.Version("1.0.0-10")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-10").compareTo(new ts.Version("1.0.0-2")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.EqualTo); - // https://semver.org/#spec-item-11 - // > identifiers with letters or hyphens are compared lexically in ASCII sort order. - assert.strictEqual(new ts.Version("1.0.0-a").compareTo(new ts.Version("1.0.0-b")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-a-2").compareTo(new ts.Version("1.0.0-a-10")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-b").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-a").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.EqualTo); - assert.strictEqual(new ts.Version("1.0.0-A").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.LessThan); + // https://semver.org/#spec-item-11 + // > identifiers with letters or hyphens are compared lexically in ASCII sort order. + assert.strictEqual(new ts.Version("1.0.0-a").compareTo(new ts.Version("1.0.0-b")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-a-2").compareTo(new ts.Version("1.0.0-a-10")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-b").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-a").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.EqualTo); + assert.strictEqual(new ts.Version("1.0.0-A").compareTo(new ts.Version("1.0.0-a")), ts.Comparison.LessThan); - // https://semver.org/#spec-item-11 - // > Numeric identifiers always have lower precedence than non-numeric identifiers. - assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-alpha")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.EqualTo); - assert.strictEqual(new ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-alpha")), ts.Comparison.EqualTo); + // https://semver.org/#spec-item-11 + // > Numeric identifiers always have lower precedence than non-numeric identifiers. + assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-alpha")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-0").compareTo(new ts.Version("1.0.0-0")), ts.Comparison.EqualTo); + assert.strictEqual(new ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-alpha")), ts.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 ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-alpha.0")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-alpha.0").compareTo(new ts.Version("1.0.0-alpha")), ts.Comparison.GreaterThan); + // 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 ts.Version("1.0.0-alpha").compareTo(new ts.Version("1.0.0-alpha.0")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-alpha.0").compareTo(new ts.Version("1.0.0-alpha")), ts.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 ts.Version("1.0.0-a.0.b.1").compareTo(new ts.Version("1.0.0-a.0.b.2")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-a.0.b.1").compareTo(new ts.Version("1.0.0-b.0.a.1")), ts.Comparison.LessThan); - assert.strictEqual(new ts.Version("1.0.0-a.0.b.2").compareTo(new ts.Version("1.0.0-a.0.b.1")), ts.Comparison.GreaterThan); - assert.strictEqual(new ts.Version("1.0.0-b.0.a.1").compareTo(new ts.Version("1.0.0-a.0.b.1")), ts.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 ts.Version("1.0.0-a.0.b.1").compareTo(new ts.Version("1.0.0-a.0.b.2")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-a.0.b.1").compareTo(new ts.Version("1.0.0-b.0.a.1")), ts.Comparison.LessThan); + assert.strictEqual(new ts.Version("1.0.0-a.0.b.2").compareTo(new ts.Version("1.0.0-a.0.b.1")), ts.Comparison.GreaterThan); + assert.strictEqual(new ts.Version("1.0.0-b.0.a.1").compareTo(new ts.Version("1.0.0-a.0.b.1")), ts.Comparison.GreaterThan); - // https://semver.org/#spec-item-11 - // > Build metadata does not figure into precedence - assert.strictEqual(new ts.Version("1.0.0+build").compareTo(new ts.Version("1.0.0")), ts.Comparison.EqualTo); - }); - it("increment", () => { - assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5").increment("major"), [2, 0, 0]); - assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5").increment("minor"), [1, 3, 0]); - assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5").increment("patch"), [1, 2, 4]); - }); + // https://semver.org/#spec-item-11 + // > Build metadata does not figure into precedence + assert.strictEqual(new ts.Version("1.0.0+build").compareTo(new ts.Version("1.0.0")), ts.Comparison.EqualTo); }); - describe("VersionRange", () => { - function assertRange(rangeText: string, versionText: string, inRange = true) { - const range = new ts.VersionRange(rangeText); - const version = new ts.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("increment", () => { + assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5").increment("major"), [2, 0, 0]); + assertVersion(new ts.Version(1, 2, 3, "pre.4", "build.5").increment("minor"), [1, 3, 0]); + assertVersion(new ts.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 ts.VersionRange(rangeText); + const version = new ts.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 d01b5cd3e155d..bb9d2fa92a425 100644 --- a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -1,90 +1,90 @@ namespace ts { - describe("unittests:: services:: cancellableLanguageServiceOperations", () => { - const file = ` +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"), ts.emptyOptions)!, r => assert.exists(r.items[0])); - }); + 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"), ts.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 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 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: ts.FormatCodeSettings = { - indentSize: 4, - tabSize: 4, - newLineCharacter: "\n", - convertTabsToSpaces: true, - indentStyle: ts.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, /*source*/ undefined, {}, /*data*/ undefined)!, r => assert.exists(r.displayParts)); - }); + it("can cancel completion entry details mid-request", () => { + const options: ts.FormatCodeSettings = { + indentSize: 4, + tabSize: 4, + newLineCharacter: "\n", + convertTabsToSpaces: true, + indentStyle: ts.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, /*source*/ undefined, {}, /*data*/ 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 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: ts.LanguageService) => T, validator: (arg: T) => void, fileName?: string, fileContent?: string, options?: ts.CompilerOptions) { - let checks = 0; - const token: ts.HostCancellationToken = { - isCancellationRequested() { - checks++; - const result = checks >= cancelAfter; - if (result) { - checks = -Infinity; // Cancel just once, then disable cancellation, effectively - } - return result; +function verifyOperationCancelledAfter(content: string, cancelAfter: number, operation: (service: ts.LanguageService) => T, validator: (arg: T) => void, fileName?: string, fileContent?: string, options?: ts.CompilerOptions) { + let checks = 0; + const token: ts.HostCancellationToken = { + isCancellationRequested() { + checks++; + const result = checks >= cancelAfter; + if (result) { + checks = -Infinity; // Cancel just once, then disable cancellation, effectively } - }; - 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; + return result; } - assert.exists(caught, "Expected operation to be cancelled, but was not"); - assert.instanceOf(caught, ts.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, ts.OperationCanceledException); +} } diff --git a/src/testRunner/unittests/services/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts index 2e545a7094df4..be0a8fccd1182 100644 --- a/src/testRunner/unittests/services/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/services/convertToAsyncFunction.ts @@ -1,7 +1,7 @@ namespace ts { - const libFile: ts.TestFSWithWatch.File = { - path: "/a/lib/lib.d.ts", - content: `/// +const libFile: ts.TestFSWithWatch.File = { + path: "/a/lib/lib.d.ts", + content: `/// interface Boolean {} interface Function {} interface IArguments {} @@ -260,202 +260,202 @@ declare var Promise: PromiseConstructor; interface RegExp {} interface String { charAt: any; } interface Array {}` - }; +}; - const moduleFile: ts.TestFSWithWatch.File = { - path: "/module.ts", - content: `export function fn(res: any): any { +const moduleFile: ts.TestFSWithWatch.File = { + path: "/module.ts", + content: `export function fn(res: any): any { return res; }` - }; - - type WithSkipAndOnly = ((...args: T) => void) & { - skip: (...args: T) => void; - only: (...args: T) => void; - }; - - function createTestWrapper(fn: (it: Mocha.PendingTestFunction, ...args: T) => void): WithSkipAndOnly { - wrapped.skip = (...args: T) => fn(it.skip, ...args); - wrapped.only = (...args: T) => fn(it.only, ...args); - return wrapped; - function wrapped(...args: T) { - return fn(it, ...args); - } +}; + +type WithSkipAndOnly = ((...args: T) => void) & { + skip: (...args: T) => void; + only: (...args: T) => void; +}; + +function createTestWrapper(fn: (it: Mocha.PendingTestFunction, ...args: T) => void): WithSkipAndOnly { + wrapped.skip = (...args: T) => fn(it.skip, ...args); + wrapped.only = (...args: T) => fn(it.only, ...args); + return wrapped; + function wrapped(...args: T) { + return fn(it, ...args); } +} - const enum ConvertToAsyncTestFlags { - None, - IncludeLib = 1 << 0, - IncludeModule = 1 << 1, - ExpectSuggestionDiagnostic = 1 << 2, - ExpectNoSuggestionDiagnostic = 1 << 3, - ExpectAction = 1 << 4, - ExpectNoAction = 1 << 5, - - ExpectSuccess = ExpectSuggestionDiagnostic | ExpectAction, - ExpectFailed = ExpectNoSuggestionDiagnostic | ExpectNoAction +const enum ConvertToAsyncTestFlags { + None, + IncludeLib = 1 << 0, + IncludeModule = 1 << 1, + ExpectSuggestionDiagnostic = 1 << 2, + ExpectNoSuggestionDiagnostic = 1 << 3, + ExpectAction = 1 << 4, + ExpectNoAction = 1 << 5, + + ExpectSuccess = ExpectSuggestionDiagnostic | ExpectAction, + ExpectFailed = ExpectNoSuggestionDiagnostic | ExpectNoAction +} + +function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, flags: ConvertToAsyncTestFlags) { + const includeLib = !!(flags & ConvertToAsyncTestFlags.IncludeLib); + const includeModule = !!(flags & ConvertToAsyncTestFlags.IncludeModule); + const expectSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic); + const expectNoSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic); + const expectAction = !!(flags & ConvertToAsyncTestFlags.ExpectAction); + const expectNoAction = !!(flags & ConvertToAsyncTestFlags.ExpectNoAction); + const expectFailure = expectNoSuggestionDiagnostic || expectNoAction; + ts.Debug.assert(!(expectSuggestionDiagnostic && expectNoSuggestionDiagnostic), "Cannot combine both 'ExpectSuggestionDiagnostic' and 'ExpectNoSuggestionDiagnostic'"); + ts.Debug.assert(!(expectAction && expectNoAction), "Cannot combine both 'ExpectAction' and 'ExpectNoAction'"); + const t = ts.extractTest(text); + const selectionRange = t.ranges.get("selection")!; + if (!selectionRange) { + throw new Error(`Test ${caption} does not specify selection range`); } - function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, flags: ConvertToAsyncTestFlags) { - const includeLib = !!(flags & ConvertToAsyncTestFlags.IncludeLib); - const includeModule = !!(flags & ConvertToAsyncTestFlags.IncludeModule); - const expectSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic); - const expectNoSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic); - const expectAction = !!(flags & ConvertToAsyncTestFlags.ExpectAction); - const expectNoAction = !!(flags & ConvertToAsyncTestFlags.ExpectNoAction); - const expectFailure = expectNoSuggestionDiagnostic || expectNoAction; - ts.Debug.assert(!(expectSuggestionDiagnostic && expectNoSuggestionDiagnostic), "Cannot combine both 'ExpectSuggestionDiagnostic' and 'ExpectNoSuggestionDiagnostic'"); - ts.Debug.assert(!(expectAction && expectNoAction), "Cannot combine both 'ExpectAction' and 'ExpectNoAction'"); - const t = ts.extractTest(text); - const selectionRange = t.ranges.get("selection")!; - if (!selectionRange) { - throw new Error(`Test ${caption} does not specify selection range`); + const extensions = expectFailure ? [ts.Extension.Ts] : [ts.Extension.Ts, ts.Extension.Js]; + extensions.forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); + function runBaseline(extension: ts.Extension) { + const path = "/a" + extension; + const languageService = makeLanguageService({ path, content: t.source }, includeLib, includeModule); + const program = languageService.getProgram()!; + + if (hasSyntacticDiagnostics(program)) { + // Don't bother generating JS baselines for inputs that aren't valid JS. + assert.equal(ts.Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); + return; } - const extensions = expectFailure ? [ts.Extension.Ts] : [ts.Extension.Ts, ts.Extension.Js]; - extensions.forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); - function runBaseline(extension: ts.Extension) { - const path = "/a" + extension; - const languageService = makeLanguageService({ path, content: t.source }, includeLib, includeModule); - const program = languageService.getProgram()!; - - if (hasSyntacticDiagnostics(program)) { - // Don't bother generating JS baselines for inputs that aren't valid JS. - assert.equal(ts.Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); - return; - } - - const f = { - path, - content: t.source - }; - - const sourceFile = program.getSourceFile(path)!; - const context: ts.CodeFixContext = { - errorCode: 80006, - span: { start: selectionRange.pos, length: selectionRange.end - selectionRange.pos }, - sourceFile, - program, - cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, - preferences: ts.emptyOptions, - host: ts.notImplementedHost, - formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, ts.notImplementedHost) - }; - - const diagnostics = languageService.getSuggestionDiagnostics(f.path); - const diagnostic = ts.find(diagnostics, diagnostic => diagnostic.messageText === ts.Diagnostics.This_may_be_converted_to_an_async_function.message && - diagnostic.start === context.span.start && diagnostic.length === context.span.length); - const actions = ts.codefix.getFixes(context); - const action = ts.find(actions, action => action.description === ts.Diagnostics.Convert_to_async_function.message); - - let outputText: string | null; - if (action?.changes.length) { - 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 = ts.textChanges.applyChanges(sourceFile.text, changes[0].textChanges); - data.push(newText); - - const diagProgram = makeLanguageService({ path, content: newText }, includeLib, includeModule).getProgram()!; - assert.isFalse(hasSyntacticDiagnostics(diagProgram)); - outputText = data.join(ts.newLineCharacter); - } - else { - // eslint-disable-next-line no-null/no-null - outputText = null; - } - - Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, outputText); - - if (expectNoSuggestionDiagnostic) { - assert.isUndefined(diagnostic, "Expected code fix to not provide a suggestion diagnostic"); - } - else if (expectSuggestionDiagnostic) { - assert.exists(diagnostic, "Expected code fix to provide a suggestion diagnostic"); - } - - if (expectNoAction) { - assert.isNotTrue(!!action?.changes.length, "Expected code fix to not provide an action"); - assert.isNotTrue(typeof outputText === "string", "Expected code fix to not apply changes"); - } - else if (expectAction) { - assert.isTrue(!!action?.changes.length, "Expected code fix to provide an action"); - assert.isTrue(typeof outputText === "string", "Expected code fix to apply changes"); - } + const f = { + path, + content: t.source + }; + + const sourceFile = program.getSourceFile(path)!; + const context: ts.CodeFixContext = { + errorCode: 80006, + span: { start: selectionRange.pos, length: selectionRange.end - selectionRange.pos }, + sourceFile, + program, + cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, + preferences: ts.emptyOptions, + host: ts.notImplementedHost, + formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, ts.notImplementedHost) + }; + + const diagnostics = languageService.getSuggestionDiagnostics(f.path); + const diagnostic = ts.find(diagnostics, diagnostic => diagnostic.messageText === ts.Diagnostics.This_may_be_converted_to_an_async_function.message && + diagnostic.start === context.span.start && diagnostic.length === context.span.length); + const actions = ts.codefix.getFixes(context); + const action = ts.find(actions, action => action.description === ts.Diagnostics.Convert_to_async_function.message); + + let outputText: string | null; + if (action?.changes.length) { + 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 = ts.textChanges.applyChanges(sourceFile.text, changes[0].textChanges); + data.push(newText); + + const diagProgram = makeLanguageService({ path, content: newText }, includeLib, includeModule).getProgram()!; + assert.isFalse(hasSyntacticDiagnostics(diagProgram)); + outputText = data.join(ts.newLineCharacter); } + else { + // eslint-disable-next-line no-null/no-null + outputText = null; + } + + Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, outputText); - function makeLanguageService(file: ts.TestFSWithWatch.File, includeLib?: boolean, includeModule?: boolean) { - const files = [file]; - if (includeLib) { - files.push(libFile); // libFile is expensive to parse repeatedly - only test when required - } - if (includeModule) { - files.push(moduleFile); - } - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file.path); - return ts.first(projectService.inferredProjects).getLanguageService(); + if (expectNoSuggestionDiagnostic) { + assert.isUndefined(diagnostic, "Expected code fix to not provide a suggestion diagnostic"); + } + else if (expectSuggestionDiagnostic) { + assert.exists(diagnostic, "Expected code fix to provide a suggestion diagnostic"); } - function hasSyntacticDiagnostics(program: ts.Program) { - const diags = program.getSyntacticDiagnostics(); - return ts.length(diags) > 0; + if (expectNoAction) { + assert.isNotTrue(!!action?.changes.length, "Expected code fix to not provide an action"); + assert.isNotTrue(typeof outputText === "string", "Expected code fix to not apply changes"); + } + else if (expectAction) { + assert.isTrue(!!action?.changes.length, "Expected code fix to provide an action"); + assert.isTrue(typeof outputText === "string", "Expected code fix to apply changes"); } } - const _testConvertToAsyncFunction = createTestWrapper((it, caption: string, text: string) => { - testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuccess); - }); + function makeLanguageService(file: ts.TestFSWithWatch.File, includeLib?: boolean, includeModule?: boolean) { + const files = [file]; + if (includeLib) { + files.push(libFile); // libFile is expensive to parse repeatedly - only test when required + } + if (includeModule) { + files.push(moduleFile); + } + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file.path); + return ts.first(projectService.inferredProjects).getLanguageService(); + } - const _testConvertToAsyncFunctionFailed = createTestWrapper((it, caption: string, text: string) => { - testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectFailed); - }); + function hasSyntacticDiagnostics(program: ts.Program) { + const diags = program.getSyntacticDiagnostics(); + return ts.length(diags) > 0; + } +} - const _testConvertToAsyncFunctionFailedSuggestion = createTestWrapper((it, caption: string, text: string) => { - testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectAction); - }); +const _testConvertToAsyncFunction = createTestWrapper((it, caption: string, text: string) => { + testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuccess); +}); - const _testConvertToAsyncFunctionFailedAction = createTestWrapper((it, caption: string, text: string) => { - testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectNoAction); - }); +const _testConvertToAsyncFunctionFailed = createTestWrapper((it, caption: string, text: string) => { + testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectFailed); +}); - const _testConvertToAsyncFunctionWithModule = createTestWrapper((it, caption: string, text: string) => { - testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.IncludeModule | ConvertToAsyncTestFlags.ExpectSuccess); - }); +const _testConvertToAsyncFunctionFailedSuggestion = createTestWrapper((it, caption: string, text: string) => { + testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectAction); +}); - describe("unittests:: services:: convertToAsyncFunction", () => { - _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` +const _testConvertToAsyncFunctionFailedAction = createTestWrapper((it, caption: string, text: string) => { + testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectNoAction); +}); + +const _testConvertToAsyncFunctionWithModule = createTestWrapper((it, caption: string, text: string) => { + testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.IncludeModule | ConvertToAsyncTestFlags.ExpectSuccess); +}); + +describe("unittests:: services:: convertToAsyncFunction", () => { + _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. */ @@ -464,23 +464,23 @@ function [#|f|](): Promise{ // 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); }); }`); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchAndRej", ` + _testConvertToAsyncFunctionFailed("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) }); }`); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchAndRejRef", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchAndRejRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res, rej).catch(catch_err) } @@ -493,7 +493,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) } @@ -504,23 +504,23 @@ 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); } @@ -528,17 +528,17 @@ 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); } @@ -548,7 +548,7 @@ 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); } @@ -559,54 +559,54 @@ 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{ } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Rej", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Rej", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => { console.log(result); }, rejection => { console.log("rejected:", rejection); }); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_RejRef", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_RejRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res, rej); } @@ -617,13 +617,13 @@ function rej(err){ console.log(err); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_RejNoBrackets", ` + _testConvertToAsyncFunctionFailed("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); } @@ -632,7 +632,7 @@ function res(result){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRef1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRef1", ` class Foo { public [#|method|](): Promise { return fetch('a').then(this.foo); @@ -644,7 +644,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRef2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRef2", ` class Foo { public [#|method|](): Promise { return fetch('a').then(this.foo); @@ -654,7 +654,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRef3", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRef3", ` const res = (result) => { return result.ok; } @@ -663,14 +663,14 @@ function [#|f|](): Promise { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef1", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef1", ` const res = 1; function [#|f|]() { return fetch('https://typescriptlang.org').then(res); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef2", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef2", ` class Foo { private foo = 1; public [#|method|](): Promise { @@ -679,14 +679,14 @@ class Foo { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef3", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef3", ` const res = undefined; function [#|f|]() { return fetch('https://typescriptlang.org').then(res); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef4", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef4", ` class Foo { private foo = undefined; public [#|method|](): Promise { @@ -695,7 +695,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res); } @@ -704,7 +704,7 @@ function res(result){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal1", ` class Foo { public [#|method|](): Promise { return fetch('a').then(this.foo); @@ -716,29 +716,29 @@ class Foo { } `); - _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'); @@ -748,7 +748,7 @@ function [#|innerPromise|](): Promise { }); } `); - _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'); @@ -758,7 +758,7 @@ function [#|innerPromise|](): Promise { } `); - _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); @@ -768,7 +768,7 @@ function [#|innerPromise|](): Promise { } `); - _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'); @@ -778,7 +778,7 @@ function [#|innerPromise|](): Promise { } `); - _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); @@ -788,7 +788,7 @@ function [#|innerPromise|](): Promise { } `); - _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]); @@ -798,20 +798,20 @@ function [#|innerPromise|](): Promise { } `); - _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)); @@ -823,7 +823,7 @@ 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; @@ -833,7 +833,7 @@ function err (rej) { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn05", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn05", ` function [#|f|]() { var blob = fetch("https://typescriptlang.org").then(res => console.log(res)); blob.then(x => x); @@ -841,14 +841,14 @@ function [#|f|]() { } `); - _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"); @@ -858,7 +858,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn08", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn08", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org"); if (!blob.ok){ @@ -869,7 +869,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn09", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn09", ` function [#|f|]() { let blob3; let blob = fetch("https://typescriptlang.org"); @@ -882,7 +882,7 @@ function [#|f|]() { `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn10", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn10", ` function [#|f|]() { let blob3; let blob = fetch("https://typescriptlang.org"); @@ -895,7 +895,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn11", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn11", ` function [#|f|]() { let blob; return blob; @@ -904,7 +904,7 @@ function [#|f|]() { - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Param1", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Param1", ` function [#|f|]() { return my_print(fetch("https://typescriptlang.org").then(res => console.log(res))); } @@ -917,7 +917,7 @@ function my_print (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)); } @@ -931,7 +931,7 @@ 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) { @@ -943,7 +943,7 @@ function [#|f|](): Promise { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns2", ` function [#|f|](): Promise { let x = fetch("https://microsoft.com").then(res => console.log("Microsoft:", res)); if (x.ok) { @@ -957,7 +957,7 @@ function [#|f|](): Promise { `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_SeperateLines", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_SeperateLines", ` function [#|f|](): Promise { var blob = fetch("https://typescriptlang.org") blob.then(resp => { @@ -972,7 +972,7 @@ function [#|f|](): Promise { `); - _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'); @@ -981,7 +981,7 @@ function [#|f|](): Promise { }); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseSimple", ` + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseSimple", ` function [#|f|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(blob => blob.byteOffset); @@ -990,7 +990,7 @@ function [#|f|](): Promise { }); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen1", ` function [#|f|]() { return Promise.resolve().then(function () { return Promise.all([fetch("https://typescriptlang.org"), fetch("https://microsoft.com"), Promise.resolve().then(function () { @@ -1000,7 +1000,7 @@ function [#|f|]() { } `); - _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 () { @@ -1010,7 +1010,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen3", ` + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen3", ` function [#|f|]() { return Promise.resolve().then(() => Promise.all([fetch("https://typescriptlang.org"), fetch("https://microsoft.com"), Promise.resolve().then(function () { @@ -1019,7 +1019,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen4", ` + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen4", ` function [#|f|]() { return Promise.resolve().then(() => Promise.all([fetch("https://typescriptlang.org"), fetch("https://microsoft.com"), Promise.resolve().then(function () { @@ -1027,7 +1027,7 @@ function [#|f|]() { })]).then(res => res.toString())); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_Scope1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Scope1", ` function [#|f|]() { var var1: Response, var2; return fetch('https://typescriptlang.org').then( _ => @@ -1044,7 +1044,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_Conditionals", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Conditionals", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => { if (res.ok) { @@ -1062,7 +1062,7 @@ function [#|f|](){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThen", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThen", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1076,7 +1076,7 @@ function rej(reject){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1090,7 +1090,7 @@ function rej(reject): number { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01NoAnnotations", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1105,7 +1105,7 @@ function rej(reject){ `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => 0).catch(rej => 1).then(res); } @@ -1115,7 +1115,7 @@ function res(result): number { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02NoAnnotations", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => 0).catch(rej => 1).then(res); } @@ -1125,7 +1125,7 @@ function res(result){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes01", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes01", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1139,7 +1139,7 @@ function rej(reject){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1153,7 +1153,7 @@ function rej(reject): Response{ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02NoAnnotations", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1168,7 +1168,7 @@ function rej(reject){ `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes03", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes03", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1182,7 +1182,7 @@ function rej(reject){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes04", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes04", ` interface a { name: string; age: number; @@ -1206,7 +1206,7 @@ function rej(reject): a{ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_ParameterNameCollision", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ParameterNameCollision", ` async function foo(x: T): Promise { return x; } @@ -1216,39 +1216,39 @@ function [#|bar|](x: T): Promise { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_Return1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Return1", ` function [#|f|](p: Promise) { return p.catch((error: Error) => { return Promise.reject(error); }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_Return2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Return2", ` function [#|f|](p: Promise) { return p.catch((error: Error) => Promise.reject(error)); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_Return3", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Return3", ` function [#|f|](p: Promise) { return p.catch(function (error: Error) { return Promise.reject(error); }); }`); - _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(); } @@ -1262,14 +1262,14 @@ function rej(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); @@ -1277,7 +1277,7 @@ function [#|f|](){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_Conditional2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Conditional2", ` function [#|f|](){ var res = 100; if (res > 50) { @@ -1293,7 +1293,7 @@ function res_func(result){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_Scope3", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Scope3", ` function [#|f|]() { var obj; return fetch("https://typescriptlang.org").then(function (res) { @@ -1306,7 +1306,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NestedFunctionWrongLocation", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NestedFunctionWrongLocation", ` function [#|f|]() { function fn2(){ function fn3(){ @@ -1318,7 +1318,7 @@ function [#|f|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_NestedFunctionRightLocation", ` + _testConvertToAsyncFunction("convertToAsyncFunction_NestedFunctionRightLocation", ` function f() { function fn2(){ function [#|fn3|](){ @@ -1330,56 +1330,56 @@ function f() { } `); - _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); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_ResRejNoArgsArrow", ` + _testConvertToAsyncFunctionFailed("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); } @@ -1388,7 +1388,7 @@ function res({ status, trailer }){ } `); - _testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", ` + _testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", ` function [#|f|]() { const result = 'https://typescriptlang.org'; return fetch(result).then(res); @@ -1398,19 +1398,19 @@ function res({ status, trailer }){ } `); - _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")); } @@ -1419,31 +1419,31 @@ function res(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)) @@ -1451,13 +1451,13 @@ function [#|f|]() { } `); - _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_noArgs1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_noArgs1", ` function delay(millis: number): Promise { throw "no" } @@ -1471,7 +1471,7 @@ function [#|main2|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_noArgs2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_noArgs2", ` function delay(millis: number): Promise { throw "no" } @@ -1485,20 +1485,20 @@ function [#|main2|]() { } `); - _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'); }) } `); - _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethod", ` + _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethod", ` function decorator() { return (target: any, key: any, descriptor: PropertyDescriptor) => descriptor; } @@ -1510,7 +1510,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithSingleLineComment", ` + _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithSingleLineComment", ` function decorator() { return (target: any, key: any, descriptor: PropertyDescriptor) => descriptor; } @@ -1523,7 +1523,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithMultipleLineComment", ` + _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithMultipleLineComment", ` function decorator() { return (target: any, key: any, descriptor: PropertyDescriptor) => descriptor; } @@ -1538,7 +1538,7 @@ class Foo { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithModifier", ` + _testConvertToAsyncFunction("convertToAsyncFunction_decoratedMethodWithModifier", ` function decorator() { return (target: any, key: any, descriptor: PropertyDescriptor) => descriptor; } @@ -1550,7 +1550,7 @@ class Foo { } `); - _testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", ` + _testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", ` function foo() { return fetch('a').then([#|() => {|] return fetch('b').then(() => 'c'); @@ -1558,7 +1558,7 @@ function foo() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument1", ` type APIResponse = { success: true, data: T } | { success: false }; function wrapResponse(response: T): APIResponse { @@ -1570,7 +1570,7 @@ function [#|get|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument2", ` type APIResponse = { success: true, data: T } | { success: false }; function wrapResponse(response: T): APIResponse { @@ -1582,7 +1582,7 @@ function [#|get|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument3", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenTypeArgument3", ` type APIResponse = { success: true, data: T } | { success: false }; function wrapResponse(response: T): APIResponse { @@ -1597,7 +1597,7 @@ function [#|get|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_catchTypeArgument1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_catchTypeArgument1", ` type APIResponse = { success: true, data: T } | { success: false }; function [#|get|]() { @@ -1607,12 +1607,12 @@ function [#|get|]() { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_threeArguments", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_threeArguments", ` function [#|f|]() { return Promise.resolve().then(undefined, undefined, () => 1); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_callbackArgument", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackArgument", ` function foo(props: any): void { return props; } @@ -1625,26 +1625,26 @@ function [#|f|]() { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_emptyCatch1", ` + _testConvertToAsyncFunction("convertToAsyncFunction_emptyCatch1", ` function [#|f|]() { return Promise.resolve().catch(); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_emptyCatch2", ` + _testConvertToAsyncFunction("convertToAsyncFunction_emptyCatch2", ` function [#|f|]() { return Promise.resolve(0).then(x => x).catch(); } `); - _testConvertToAsyncFunctionWithModule("convertToAsyncFunction_importedFunction", ` + _testConvertToAsyncFunctionWithModule("convertToAsyncFunction_importedFunction", ` import { fn } from "./module"; function [#|f|]() { return Promise.resolve(0).then(fn); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInFunctionsWithNonFixableReturnStatements1", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInFunctionsWithNonFixableReturnStatements1", ` function f(x: number): Promise; function f(): void; function [#|f|](x?: number): Promise | void { @@ -1653,7 +1653,7 @@ function [#|f|](x?: number): Promise | void { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInFunctionsWithNonFixableReturnStatements2", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInFunctionsWithNonFixableReturnStatements2", ` function f(x: number): Promise; function f(): number; function [#|f|](x?: number): Promise | number { @@ -1662,7 +1662,7 @@ function [#|f|](x?: number): Promise | number { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInGetters", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionInGetters", ` class Foo { get [#|m|](): Promise { return Promise.resolve(1).then(n => n); @@ -1670,7 +1670,7 @@ class Foo { } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionForGeneratorCallbacks", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction__NoSuggestionForGeneratorCallbacks", ` function [#|foo|](p: Promise) { return p.then(function* (strings) { for (const s of strings) { @@ -1680,52 +1680,52 @@ function [#|foo|](p: Promise) { } `); - _testConvertToAsyncFunction("convertToAsyncFunction_thenNoArguments", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenNoArguments", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_catchNoArguments", ` + _testConvertToAsyncFunction("convertToAsyncFunction_catchNoArguments", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().catch(); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_chainedThenCatchThen", ` + _testConvertToAsyncFunction("convertToAsyncFunction_chainedThenCatchThen", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(x => Promise.resolve(x + 1)).catch(() => 1).then(y => y + 2); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_finally", ` + _testConvertToAsyncFunction("convertToAsyncFunction_finally", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().finally(() => console.log("done")); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_finallyNoArguments", ` + _testConvertToAsyncFunction("convertToAsyncFunction_finallyNoArguments", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().finally(); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_finallyNull", ` + _testConvertToAsyncFunction("convertToAsyncFunction_finallyNull", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().finally(null); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_finallyUndefined", ` + _testConvertToAsyncFunction("convertToAsyncFunction_finallyUndefined", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().finally(undefined); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_thenFinally", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenFinally", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(x => x + 1).finally(() => console.log("done")); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_thenFinallyThen", ` + _testConvertToAsyncFunction("convertToAsyncFunction_thenFinallyThen", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(x => Promise.resolve(x + 1)).finally(() => console.log("done")).then(y => y + 2); }`); - _testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_returnInBranch", ` + _testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_returnInBranch", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(() => { @@ -1738,7 +1738,7 @@ function [#|f|](): Promise { }); } `); - _testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_partialReturnInBranch", ` + _testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_partialReturnInBranch", ` declare function foo(): Promise; function [#|f|](): Promise { return foo().then(() => { @@ -1751,5 +1751,5 @@ function [#|f|](): Promise { }); } `); - }); +}); } diff --git a/src/testRunner/unittests/services/extract/constants.ts b/src/testRunner/unittests/services/extract/constants.ts index fe32437e00a23..c9220dc7714c9 100644 --- a/src/testRunner/unittests/services/extract/constants.ts +++ b/src/testRunner/unittests/services/extract/constants.ts @@ -1,48 +1,48 @@ namespace ts { - describe("unittests:: services:: extract:: extractConstants", () => { - testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); - testExtractConstant("extractConstant_Namespace", `namespace N { +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_ExpressionStatement", `[#|"hello";|]`); + testExtractConstant("extractConstant_ExpressionStatementExpression", `[#|"hello"|];`); - testExtractConstant("extractConstant_ExpressionStatementInNestedScope", ` + 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() { } @@ -52,7 +52,7 @@ function F() { } }`); - testExtractConstant("extractConstant_ClassInsertionPosition2", `class C { + testExtractConstant("extractConstant_ClassInsertionPosition2", `class C { a = 1; M1() { } b = 2; @@ -62,7 +62,7 @@ function F() { } }`); - testExtractConstant("extractConstant_ClassInsertionPosition3", `class C { + testExtractConstant("extractConstant_ClassInsertionPosition3", `class C { M1() { } a = 1; b = 2; @@ -72,30 +72,30 @@ function F() { } }`); - 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|];`); + 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_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, + // 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|]; @@ -103,14 +103,14 @@ for (let i = 0; i < 10; i++) { } `); - 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++) { @@ -119,13 +119,13 @@ 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|]; @@ -133,7 +133,7 @@ function F() { } `); - testExtractConstant("extractConstant_StatementInsertionPosition5", ` + testExtractConstant("extractConstant_StatementInsertionPosition5", ` function F0() { function F1() { function F2(x = [#|2 + 1|]) { @@ -142,13 +142,13 @@ 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() { @@ -159,25 +159,25 @@ 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 */ /// @@ -187,21 +187,21 @@ 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_ArrowFunction_Expression", `const f = () => [#|2 + 1|];`); - testExtractConstant("extractConstant_PreserveTrivia", ` + testExtractConstant("extractConstant_PreserveTrivia", ` // a var q = /*b*/ //c /*d*/ [#|1 /*e*/ //f @@ -209,15 +209,15 @@ var q = /*b*/ //c /*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()|]; @@ -225,7 +225,7 @@ class C { m2() { return 1; } }`); - testExtractConstant("extractConstant_This_Method", ` + testExtractConstant("extractConstant_This_Method", ` class C { m1() { [#|this.m2()|]; @@ -233,7 +233,7 @@ class C { m2() { return 1; } }`); - testExtractConstant("extractConstant_This_Property", ` + testExtractConstant("extractConstant_This_Property", ` namespace N { // Force this test to be TS-only class C { x = 1; @@ -241,38 +241,38 @@ namespace N { // Force this test to be TS-only } }`); - // 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; } `); - testExtractConstant("extractConstant_PropertyName", `[#|x.y|].z();`); - testExtractConstant("extractConstant_PropertyName_ExistingName", `let y; + testExtractConstant("extractConstant_PropertyName", `[#|x.y|].z();`); + testExtractConstant("extractConstant_PropertyName_ExistingName", `let y; [#|x.y|].z();`); - testExtractConstant("extractConstant_PropertyName_Keyword", `[#|x.if|].z();`); - testExtractConstant("extractConstant_PropertyName_PrivateIdentifierKeyword", `[#|this.#if|].z();`); - }); + testExtractConstant("extractConstant_PropertyName_Keyword", `[#|x.if|].z();`); + testExtractConstant("extractConstant_PropertyName_PrivateIdentifierKeyword", `[#|this.#if|].z();`); +}); - function testExtractConstant(caption: string, text: string) { - ts.testExtractSymbol(caption, text, "extractConstant", ts.Diagnostics.Extract_constant); - } +function testExtractConstant(caption: string, text: string) { + ts.testExtractSymbol(caption, text, "extractConstant", ts.Diagnostics.Extract_constant); +} - function testExtractConstantFailed(caption: string, text: string) { - ts.testExtractSymbolFailed(caption, text, ts.Diagnostics.Extract_constant); - } +function testExtractConstantFailed(caption: string, text: string) { + ts.testExtractSymbolFailed(caption, text, ts.Diagnostics.Extract_constant); +} } diff --git a/src/testRunner/unittests/services/extract/functions.ts b/src/testRunner/unittests/services/extract/functions.ts index 6ec3ca43158e7..d5d6402a80763 100644 --- a/src/testRunner/unittests/services/extract/functions.ts +++ b/src/testRunner/unittests/services/extract/functions.ts @@ -1,6 +1,6 @@ namespace ts { - describe("unittests:: services:: extract:: extractFunctions", () => { - testExtractFunction("extractFunction1", `namespace A { +describe("unittests:: services:: extract:: extractFunctions", () => { + testExtractFunction("extractFunction1", `namespace A { let x = 1; function foo() { } @@ -15,7 +15,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction2", `namespace A { + testExtractFunction("extractFunction2", `namespace A { let x = 1; function foo() { } @@ -28,7 +28,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction3", `namespace A { + testExtractFunction("extractFunction3", `namespace A { function foo() { } namespace B { @@ -40,7 +40,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction4", `namespace A { + testExtractFunction("extractFunction4", `namespace A { function foo() { } namespace B { @@ -54,7 +54,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction5", `namespace A { + testExtractFunction("extractFunction5", `namespace A { let x = 1; export function foo() { } @@ -69,7 +69,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction6", `namespace A { + testExtractFunction("extractFunction6", `namespace A { let x = 1; export function foo() { } @@ -84,7 +84,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction7", `namespace A { + testExtractFunction("extractFunction7", `namespace A { let x = 1; export namespace C { export function foo() { @@ -101,7 +101,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction9", `namespace A { + testExtractFunction("extractFunction9", `namespace A { export interface I { x: number }; namespace B { function a() { @@ -110,7 +110,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction10", `namespace A { + testExtractFunction("extractFunction10", `namespace A { export interface I { x: number }; class C { a() { @@ -120,7 +120,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction11", `namespace A { + testExtractFunction("extractFunction11", `namespace A { let y = 1; class C { a() { @@ -132,7 +132,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction12", `namespace A { + testExtractFunction("extractFunction12", `namespace A { let y = 1; class C { b() {} @@ -146,12 +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) { @@ -166,93 +166,93 @@ 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;|] @@ -260,8 +260,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;|] @@ -269,8 +269,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; @@ -287,12 +287,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; @@ -303,8 +303,8 @@ function parsePrimaryExpression(): any { }|] } }`); - // Return in nested class - testExtractFunction("extractFunction32", `namespace N { + // Return in nested class + testExtractFunction("extractFunction32", `namespace N { export const value = 1; @@ -316,80 +316,80 @@ 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() { }|] }`); - // Arrow function - testExtractFunction("extractFunction34", `const F = () => { + // Arrow function + testExtractFunction("extractFunction34", `const 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; @@ -397,7 +397,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_NoType", ` function f() { let a = 1; [#|let x = 1; @@ -405,7 +405,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_Type", ` function f() { let a = 1; [#|let x: number = 1; @@ -413,9 +413,9 @@ function f() { 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; @@ -423,7 +423,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType2", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType2", ` function f() { let a = 1; [#|let x: "a" | 'b' = 'a'; @@ -431,9 +431,9 @@ function f() { 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; @@ -441,7 +441,7 @@ function f() { 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'; @@ -449,7 +449,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_NoType", ` function f() { let a = 1; [#|const x = 1; @@ -457,7 +457,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_Type", ` function f() { let a = 1; [#|const x: number = 1; @@ -465,7 +465,7 @@ function f() { a; x; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed1", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed1", ` function f() { let a = 1; [#|const x = 1; @@ -474,7 +474,7 @@ function f() { a; x; y; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed2", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed2", ` function f() { let a = 1; [#|var x = 1; @@ -483,7 +483,7 @@ function f() { a; x; y; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed3", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed3", ` function f() { let a = 1; [#|let x: number = 1; @@ -492,7 +492,7 @@ function f() { a; x; y; }`); - testExtractFunction("extractFunction_VariableDeclaration_Writes_UnionUndefined", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_UnionUndefined", ` function f() { let a = 1; [#|let x: number | undefined = 1; @@ -502,13 +502,13 @@ function f() { 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 @@ -516,20 +516,20 @@ var q = /*b*/ //c /*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) { - ts.testExtractSymbol(caption, text, "extractFunction", ts.Diagnostics.Extract_function, includeLib); - } +function testExtractFunction(caption: string, text: string, includeLib?: boolean) { + ts.testExtractSymbol(caption, text, "extractFunction", ts.Diagnostics.Extract_function, includeLib); +} } diff --git a/src/testRunner/unittests/services/extract/helpers.ts b/src/testRunner/unittests/services/extract/helpers.ts index e43145f1d68c7..6478af38bc554 100644 --- a/src/testRunner/unittests/services/extract/helpers.ts +++ b/src/testRunner/unittests/services/extract/helpers.ts @@ -1,180 +1,180 @@ namespace ts { - interface Range { - pos: number; - end: number; - name: string; - } +interface Range { + pos: number; + end: number; + name: string; +} - interface Test { - source: string; - ranges: ts.ESMap; - } +interface Test { + source: string; + ranges: ts.ESMap; +} - export function extractTest(source: string): Test { - const activeRanges: Range[] = []; - let text = ""; - let lastPos = 0; - let pos = 0; - const ranges = new ts.Map(); +export function extractTest(source: string): Test { + const activeRanges: Range[] = []; + let text = ""; + let lastPos = 0; + let pos = 0; + const ranges = new ts.Map(); - while (pos < source.length) { - if (source.charCodeAt(pos) === ts.CharacterCodes.openBracket && - (source.charCodeAt(pos + 1) === ts.CharacterCodes.hash || source.charCodeAt(pos + 1) === ts.CharacterCodes.$)) { - const saved = pos; - pos += 2; - const s = pos; - consumeIdentifier(); - const e = pos; - if (source.charCodeAt(pos) === ts.CharacterCodes.bar) { - pos++; - text += source.substring(lastPos, saved); - const name = s === e - ? source.charCodeAt(saved + 1) === ts.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) === ts.CharacterCodes.bar && source.charCodeAt(pos + 1) === ts.CharacterCodes.closeBracket) { - text += source.substring(lastPos, pos); - activeRanges[activeRanges.length - 1].end = text.length; - const range = activeRanges.pop()!; - if (ts.hasProperty(ranges, range.name)) { - throw new Error(`Duplicate name of range ${range.name}`); - } - ranges.set(range.name, range); - pos += 2; + while (pos < source.length) { + if (source.charCodeAt(pos) === ts.CharacterCodes.openBracket && + (source.charCodeAt(pos + 1) === ts.CharacterCodes.hash || source.charCodeAt(pos + 1) === ts.CharacterCodes.$)) { + const saved = pos; + pos += 2; + const s = pos; + consumeIdentifier(); + const e = pos; + if (source.charCodeAt(pos) === ts.CharacterCodes.bar) { + pos++; + text += source.substring(lastPos, saved); + const name = s === e + ? source.charCodeAt(saved + 1) === ts.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 (ts.isIdentifierPart(source.charCodeAt(pos), ts.ScriptTarget.Latest)) { - pos++; + else if (source.charCodeAt(pos) === ts.CharacterCodes.bar && source.charCodeAt(pos + 1) === ts.CharacterCodes.closeBracket) { + text += source.substring(lastPos, pos); + activeRanges[activeRanges.length - 1].end = text.length; + const range = activeRanges.pop()!; + if (ts.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++; } + text += source.substring(lastPos, pos); - export const newLineCharacter = "\n"; - - export const notImplementedHost: ts.LanguageServiceHost = { - getCompilationSettings: ts.notImplemented, - getScriptFileNames: ts.notImplemented, - getScriptVersion: ts.notImplemented, - getScriptSnapshot: ts.notImplemented, - getDefaultLibFileName: ts.notImplemented, - getCurrentDirectory: ts.notImplemented, - readFile: ts.notImplemented, - fileExists: ts.notImplemented - }; - - export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: ts.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`); + function consumeIdentifier() { + while (ts.isIdentifierPart(source.charCodeAt(pos), ts.ScriptTarget.Latest)) { + pos++; } + } + return { source: text, ranges }; +} - [ts.Extension.Ts, ts.Extension.Js].forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); - function runBaseline(extension: ts.Extension) { - const path = "/a" + extension; - const { program } = makeProgram({ path, content: t.source }, includeLib); +export const newLineCharacter = "\n"; - if (hasSyntacticDiagnostics(program)) { - // Don't bother generating JS baselines for inputs that aren't valid JS. - assert.equal(ts.Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); - return; - } +export const notImplementedHost: ts.LanguageServiceHost = { + getCompilationSettings: ts.notImplemented, + getScriptFileNames: ts.notImplemented, + getScriptVersion: ts.notImplemented, + getScriptSnapshot: ts.notImplemented, + getDefaultLibFileName: ts.notImplemented, + getCurrentDirectory: ts.notImplemented, + readFile: ts.notImplemented, + fileExists: ts.notImplemented +}; - const sourceFile = program.getSourceFile(path)!; - const context: ts.RefactorContext = { - cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, - program, - file: sourceFile, - startPosition: selectionRange.pos, - endPosition: selectionRange.end, - host: notImplementedHost, - formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, notImplementedHost), - preferences: ts.emptyOptions, - }; - const rangeToExtract = ts.refactor.extractSymbol.getRangeToExtract(sourceFile, ts.createTextSpanFromRange(selectionRange)); - assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = ts.refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); - const actions = ts.find(infos, info => info.description === description.message)!.actions; +export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: ts.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`); + } - const data: string[] = []; - data.push(`// ==ORIGINAL==`); - data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); - for (const action of actions) { - const { renameLocation, edits } = ts.refactor.extractSymbol.getRefactorEditsToExtractSymbol(context, action.name)!; - assert.lengthOf(edits, 1); - data.push(`// ==SCOPE::${action.description}==`); - const newText = ts.textChanges.applyChanges(sourceFile.text, edits[0].textChanges); - const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); - data.push(newTextWithRename); + [ts.Extension.Ts, ts.Extension.Js].forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); + function runBaseline(extension: ts.Extension) { + const path = "/a" + extension; + const { program } = makeProgram({ path, content: t.source }, includeLib); - const { program: diagProgram } = makeProgram({ path, content: newText }, includeLib); - assert.isFalse(hasSyntacticDiagnostics(diagProgram)); - } - Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); + if (hasSyntacticDiagnostics(program)) { + // Don't bother generating JS baselines for inputs that aren't valid JS. + assert.equal(ts.Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); + return; } - function makeProgram(f: { - path: string; - content: string; - }, includeLib?: boolean) { - const host = ts.projectSystem.createServerHost(includeLib ? [f, ts.projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; - const autoImportProvider = projectService.inferredProjects[0].getLanguageService().getAutoImportProvider(); - return { program, autoImportProvider }; - } + const sourceFile = program.getSourceFile(path)!; + const context: ts.RefactorContext = { + cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, + program, + file: sourceFile, + startPosition: selectionRange.pos, + endPosition: selectionRange.end, + host: notImplementedHost, + formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, notImplementedHost), + preferences: ts.emptyOptions, + }; + const rangeToExtract = ts.refactor.extractSymbol.getRangeToExtract(sourceFile, ts.createTextSpanFromRange(selectionRange)); + assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = ts.refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); + const actions = ts.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 } = ts.refactor.extractSymbol.getRefactorEditsToExtractSymbol(context, action.name)!; + assert.lengthOf(edits, 1); + data.push(`// ==SCOPE::${action.description}==`); + const newText = ts.textChanges.applyChanges(sourceFile.text, edits[0].textChanges); + const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); + data.push(newTextWithRename); - function hasSyntacticDiagnostics(program: ts.Program) { - const diags = program.getSyntacticDiagnostics(); - return ts.length(diags) > 0; + const { program: diagProgram } = makeProgram({ path, content: newText }, includeLib); + assert.isFalse(hasSyntacticDiagnostics(diagProgram)); } + Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); } - export function testExtractSymbolFailed(caption: string, text: string, description: ts.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 = ts.projectSystem.createServerHost([f, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; - const sourceFile = program.getSourceFile(f.path)!; - const context: ts.RefactorContext = { - cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, - program, - file: sourceFile, - startPosition: selectionRange.pos, - endPosition: selectionRange.end, - host: notImplementedHost, - formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, notImplementedHost), - preferences: ts.emptyOptions, - }; - const rangeToExtract = ts.refactor.extractSymbol.getRangeToExtract(sourceFile, ts.createTextSpanFromRange(selectionRange)); - assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = ts.refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); - assert.isUndefined(ts.find(infos, info => info.description === description.message)); - }); + function makeProgram(f: { + path: string; + content: string; + }, includeLib?: boolean) { + const host = ts.projectSystem.createServerHost(includeLib ? [f, ts.projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; + const autoImportProvider = projectService.inferredProjects[0].getLanguageService().getAutoImportProvider(); + return { program, autoImportProvider }; + } + + function hasSyntacticDiagnostics(program: ts.Program) { + const diags = program.getSyntacticDiagnostics(); + return ts.length(diags) > 0; } } + +export function testExtractSymbolFailed(caption: string, text: string, description: ts.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 = ts.projectSystem.createServerHost([f, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; + const sourceFile = program.getSourceFile(f.path)!; + const context: ts.RefactorContext = { + cancellationToken: { throwIfCancellationRequested: ts.noop, isCancellationRequested: ts.returnFalse }, + program, + file: sourceFile, + startPosition: selectionRange.pos, + endPosition: selectionRange.end, + host: notImplementedHost, + formatContext: ts.formatting.getFormatContext(ts.testFormatSettings, notImplementedHost), + preferences: ts.emptyOptions, + }; + const rangeToExtract = ts.refactor.extractSymbol.getRangeToExtract(sourceFile, ts.createTextSpanFromRange(selectionRange)); + assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = ts.refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); + assert.isUndefined(ts.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 257e9e8b34b6c..4a645a0eff92e 100644 --- a/src/testRunner/unittests/services/extract/ranges.ts +++ b/src/testRunner/unittests/services/extract/ranges.ts @@ -1,113 +1,113 @@ namespace ts { - function testExtractRangeFailed(caption: string, s: string, expectedErrors: string[]) { - return it(caption, () => { - const t = ts.extractTest(s); - const file = ts.createSourceFile("a.ts", t.source, ts.ScriptTarget.Latest, /*setParentNodes*/ true); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - throw new Error(`Test ${s} does not specify selection range`); - } - const result = ts.refactor.extractSymbol.getRangeToExtract(file, ts.createTextSpanFromRange(selectionRange), /*userRequested*/ false); - assert(result.targetRange === undefined, "failure expected"); - const sortedErrors = result.errors.map(e => e.messageText as string).sort(); - assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); - }); - } +function testExtractRangeFailed(caption: string, s: string, expectedErrors: string[]) { + return it(caption, () => { + const t = ts.extractTest(s); + const file = ts.createSourceFile("a.ts", t.source, ts.ScriptTarget.Latest, /*setParentNodes*/ true); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + throw new Error(`Test ${s} does not specify selection range`); + } + const result = ts.refactor.extractSymbol.getRangeToExtract(file, ts.createTextSpanFromRange(selectionRange), /*userRequested*/ false); + assert(result.targetRange === undefined, "failure expected"); + const sortedErrors = result.errors.map(e => e.messageText as string).sort(); + assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); + }); +} - function testExtractRange(caption: string, s: string) { - return it(caption, () => { - const t = ts.extractTest(s); - const f = ts.createSourceFile("a.ts", t.source, ts.ScriptTarget.Latest, /*setParentNodes*/ true); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - throw new Error(`Test ${s} does not specify selection range`); - } - const result = ts.refactor.extractSymbol.getRangeToExtract(f, ts.createTextSpanFromRange(selectionRange)); - const expectedRange = t.ranges.get("extracted"); - if (expectedRange) { - let pos: number, end: number; - const targetRange = result.targetRange!; - if (ts.isArray(targetRange.range)) { - pos = targetRange.range[0].getStart(f); - end = ts.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"); +function testExtractRange(caption: string, s: string) { + return it(caption, () => { + const t = ts.extractTest(s); + const f = ts.createSourceFile("a.ts", t.source, ts.ScriptTarget.Latest, /*setParentNodes*/ true); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + throw new Error(`Test ${s} does not specify selection range`); + } + const result = ts.refactor.extractSymbol.getRangeToExtract(f, ts.createTextSpanFromRange(selectionRange)); + const expectedRange = t.ranges.get("extracted"); + if (expectedRange) { + let pos: number, end: number; + const targetRange = result.targetRange!; + if (ts.isArray(targetRange.range)) { + pos = targetRange.range[0].getStart(f); + end = ts.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", () => { - describe("get extract range from selection", () => { - testExtractRange("extractRange1", ` +describe("unittests:: services:: extract:: extractRanges", () => { + describe("get extract range from selection", () => { + testExtractRange("extractRange1", ` [#| [$|var x = 1; var y = 2;|]|] `); - testExtractRange("extractRange2", ` + testExtractRange("extractRange2", ` [$|[#|var x = 1; var y = 2|];|] `); - testExtractRange("extractRange3", ` + testExtractRange("extractRange3", ` [#|var x = [$|1|]|]; var y = 2; `); - testExtractRange("extractRange4", ` + testExtractRange("extractRange4", ` var x = [$|10[#|00|]|]; `); - testExtractRange("extractRange5", ` + testExtractRange("extractRange5", ` [$|va[#|r foo = 1; var y = 200|]0;|] `); - testExtractRange("extractRange6", ` + testExtractRange("extractRange6", ` var x = [$|fo[#|o.bar.baz()|]|]; `); - testExtractRange("extractRange7", ` + testExtractRange("extractRange7", ` if ([#|[#extracted|a && b && c && d|]|]) { } `); - testExtractRange("extractRange8", ` + testExtractRange("extractRange8", ` if [#|(a && b && c && d|]) { } `); - testExtractRange("extractRange9", ` + testExtractRange("extractRange9", ` if ([$|a[#|a && b && c && d|]d|]) { } `); - testExtractRange("extractRange10", ` + testExtractRange("extractRange10", ` if (a && b && c && d) { [#| [$|var x = 1; console.log(x);|] |] } `); - testExtractRange("extractRange11", ` + testExtractRange("extractRange11", ` [#| if (a) { return 100; } |] `); - testExtractRange("extractRange12", ` + testExtractRange("extractRange12", ` function foo() { [#| [$|if (a) { } return 100|] |] } `); - testExtractRange("extractRange13", ` + testExtractRange("extractRange13", ` [#| [$|l1: if (x) { break l1; }|]|] `); - testExtractRange("extractRange14", ` + testExtractRange("extractRange14", ` [#| [$|l2: { @@ -116,21 +116,21 @@ namespace ts { break l2; }|]|] `); - testExtractRange("extractRange15", ` + testExtractRange("extractRange15", ` while (true) { [#| if(x) { } break; |] } `); - testExtractRange("extractRange16", ` + testExtractRange("extractRange16", ` while (true) { [#| if(x) { } continue; |] } `); - testExtractRange("extractRange17", ` + testExtractRange("extractRange17", ` l3: { [#| @@ -139,7 +139,7 @@ namespace ts { break l3; |] } `); - testExtractRange("extractRange18", ` + testExtractRange("extractRange18", ` function f() { while (true) { [#| @@ -149,7 +149,7 @@ namespace ts { } } `); - testExtractRange("extractRange19", ` + testExtractRange("extractRange19", ` function f() { while (true) { [#| @@ -160,13 +160,13 @@ namespace ts { } } `); - testExtractRange("extractRange20", ` + testExtractRange("extractRange20", ` function f() { return [#| [$|1 + 2|] |]+ 3; } } `); - testExtractRange("extractRange21", ` + testExtractRange("extractRange21", ` function f(x: number) { [#|[$|try { x++; @@ -177,25 +177,25 @@ namespace ts { } `); - // Variable statements - testExtractRange("extractRange22", `[#|let x = [$|1|];|]`); - testExtractRange("extractRange23", `[#|let x = [$|1|], y;|]`); - testExtractRange("extractRange24", `[#|[$|let x = 1, y = 1;|]|]`); + // Variable statements + testExtractRange("extractRange22", `[#|let x = [$|1|];|]`); + testExtractRange("extractRange23", `[#|let x = [$|1|], y;|]`); + testExtractRange("extractRange24", `[#|[$|let x = 1, y = 1;|]|]`); - // Variable declarations - testExtractRange("extractRange25", `let [#|x = [$|1|]|];`); - testExtractRange("extractRange26", `let [#|x = [$|1|]|], y = 2;`); - testExtractRange("extractRange27", `let x = 1, [#|y = [$|2|]|];`); + // Variable declarations + testExtractRange("extractRange25", `let [#|x = [$|1|]|];`); + testExtractRange("extractRange26", `let [#|x = [$|1|]|], y = 2;`); + testExtractRange("extractRange27", `let x = 1, [#|y = [$|2|]|];`); - // Return statements - testExtractRange("extractRange28", `[#|return [$|1|];|]`); + // Return statements + testExtractRange("extractRange28", `[#|return [$|1|];|]`); - // For statements - testExtractRange("extractRange29", `for ([#|var i = [$|1|]|]; i < 2; i++) {}`); - testExtractRange("extractRange30", `for (var i = [#|[$|1|]|]; i < 2; i++) {}`); - }); + // For statements + testExtractRange("extractRange29", `for ([#|var i = [$|1|]|]; i < 2; i++) {}`); + testExtractRange("extractRange30", `for (var i = [#|[$|1|]|]; i < 2; i++) {}`); + }); - testExtractRangeFailed("extractRangeFailed1", ` + testExtractRangeFailed("extractRangeFailed1", ` namespace A { function f() { [#| @@ -207,7 +207,7 @@ function f() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - testExtractRangeFailed("extractRangeFailed2", ` + testExtractRangeFailed("extractRangeFailed2", ` namespace A { function f() { while (true) { @@ -221,7 +221,7 @@ function f() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - testExtractRangeFailed("extractRangeFailed3", ` + testExtractRangeFailed("extractRangeFailed3", ` namespace A { function f() { while (true) { @@ -235,7 +235,7 @@ function f() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - testExtractRangeFailed("extractRangeFailed4", ` + testExtractRangeFailed("extractRangeFailed4", ` namespace A { function f() { l1: { @@ -249,7 +249,7 @@ function f() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange.message]); - testExtractRangeFailed("extractRangeFailed5", ` + testExtractRangeFailed("extractRangeFailed5", ` namespace A { function f() { [#| @@ -265,7 +265,7 @@ function f2() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - testExtractRangeFailed("extractRangeFailed6", ` + testExtractRangeFailed("extractRangeFailed6", ` namespace A { function f() { [#| @@ -281,7 +281,7 @@ function f2() { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - testExtractRangeFailed("extractRangeFailed7", ` + testExtractRangeFailed("extractRangeFailed7", ` function test(x: number) { while (x) { x--; @@ -289,7 +289,7 @@ while (x) { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - testExtractRangeFailed("extractRangeFailed8", ` + testExtractRangeFailed("extractRangeFailed8", ` function test(x: number) { switch (x) { case 1: @@ -297,14 +297,14 @@ switch (x) { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - testExtractRangeFailed("extractRangeFailed9", `var x = ([#||]1 + 2);`, [ts.refactor.extractSymbol.Messages.cannotExtractEmpty.message]); - testExtractRangeFailed("extractRangeFailed10", ` + testExtractRangeFailed("extractRangeFailed9", `var x = ([#||]1 + 2);`, [ts.refactor.extractSymbol.Messages.cannotExtractEmpty.message]); + testExtractRangeFailed("extractRangeFailed10", ` function f() { return 1 + [#|2 + 3|]; } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - testExtractRangeFailed("extractRangeFailed11", ` + testExtractRangeFailed("extractRangeFailed11", ` function f(x: number) { while (true) { [#|try { @@ -316,38 +316,38 @@ switch (x) { } } `, [ts.refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - testExtractRangeFailed("extractRangeFailed12", `let [#|x|];`, [ts.refactor.extractSymbol.Messages.statementOrExpressionExpected.message]); - testExtractRangeFailed("extractRangeFailed13", `[#|return;|]`, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - testExtractRangeFailed("extractRangeFailed14", ` + testExtractRangeFailed("extractRangeFailed12", `let [#|x|];`, [ts.refactor.extractSymbol.Messages.statementOrExpressionExpected.message]); + testExtractRangeFailed("extractRangeFailed13", `[#|return;|]`, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed14", ` switch(1) { case [#|1: break;|] } `, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - testExtractRangeFailed("extractRangeFailed15", ` + testExtractRangeFailed("extractRangeFailed15", ` switch(1) { case [#|1: break|]; } `, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - // Documentation only - it would be nice if the result were [$|1|] - testExtractRangeFailed("extractRangeFailed16", ` + // Documentation only - it would be nice if the result were [$|1|] + testExtractRangeFailed("extractRangeFailed16", ` switch(1) { [#|case 1|]: break; } `, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - // Documentation only - it would be nice if the result were [$|1|] - testExtractRangeFailed("extractRangeFailed17", ` + // Documentation only - it would be nice if the result were [$|1|] + testExtractRangeFailed("extractRangeFailed17", ` switch(1) { [#|case 1:|] break; } `, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - testExtractRangeFailed("extractRangeFailed18", `[#|{ 1;|] }`, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); - testExtractRangeFailed("extractRangeFailed19", `[#|/** @type {number} */|] const foo = 1;`, [ts.refactor.extractSymbol.Messages.cannotExtractJSDoc.message]); - testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [ts.refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); - }); + testExtractRangeFailed("extractRangeFailed18", `[#|{ 1;|] }`, [ts.refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed19", `[#|/** @type {number} */|] const foo = 1;`, [ts.refactor.extractSymbol.Messages.cannotExtractJSDoc.message]); + testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [ts.refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); +}); } diff --git a/src/testRunner/unittests/services/extract/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts index c17fc44c63d7c..3f81d74a362c8 100644 --- a/src/testRunner/unittests/services/extract/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -1,45 +1,45 @@ namespace ts { - describe("unittests:: services:: extract:: Symbol Walker", () => { - function test(description: string, source: string, verifier: (file: ts.SourceFile, checker: ts.TypeChecker) => void) { - it(description, () => { - const result = Harness.Compiler.compileFiles([{ - unitName: "main.ts", - content: source - }], [], {}, {}, "/"); - const file = result.program!.getSourceFile("main.ts")!; - const checker = result.program!.getTypeChecker(); - verifier(file, checker); - }); - } +describe("unittests:: services:: extract:: Symbol Walker", () => { + function test(description: string, source: string, verifier: (file: ts.SourceFile, checker: ts.TypeChecker) => void) { + it(description, () => { + const result = Harness.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", ` + 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 = ts.forEach(symbol.declarations, d => { - return ts.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 = ts.forEach(symbol.declarations, d => { + return ts.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 762d5292dc842..d391fd343ee87 100644 --- a/src/testRunner/unittests/services/hostNewLineSupport.ts +++ b/src/testRunner/unittests/services/hostNewLineSupport.ts @@ -1,76 +1,76 @@ namespace ts { - describe("unittests:: services:: hostNewLineSupport", () => { - function testLSWithFiles(settings: ts.CompilerOptions, files: Harness.Compiler.TestFile[]) { - function snapFor(path: string): ts.IScriptSnapshot | undefined { - if (path === "lib.d.ts") { - return ts.ScriptSnapshot.fromString(""); - } - const result = ts.find(files, f => f.unitName === path); - return result && ts.ScriptSnapshot.fromString(result.content); +describe("unittests:: services:: hostNewLineSupport", () => { + function testLSWithFiles(settings: ts.CompilerOptions, files: Harness.Compiler.TestFile[]) { + function snapFor(path: string): ts.IScriptSnapshot | undefined { + if (path === "lib.d.ts") { + return ts.ScriptSnapshot.fromString(""); } - const lshost: ts.LanguageServiceHost = { - getCompilationSettings: () => settings, - getScriptFileNames: () => ts.map(files, f => f.unitName), - getScriptVersion: () => "1", - getScriptSnapshot: name => snapFor(name), - getDefaultLibFileName: () => "lib.d.ts", - getCurrentDirectory: () => "", - readFile: name => { - const snap = snapFor(name); - if (!snap) - return undefined; - return snap.getText(0, snap.getLength()); - }, - fileExists: name => !!snapFor(name), - }; - return ts.createLanguageService(lshost); + const result = ts.find(files, f => f.unitName === path); + return result && ts.ScriptSnapshot.fromString(result.content); } + const lshost: ts.LanguageServiceHost = { + getCompilationSettings: () => settings, + getScriptFileNames: () => ts.map(files, f => f.unitName), + getScriptVersion: () => "1", + getScriptSnapshot: name => snapFor(name), + getDefaultLibFileName: () => "lib.d.ts", + getCurrentDirectory: () => "", + readFile: name => { + const snap = snapFor(name); + if (!snap) + return undefined; + return snap.getText(0, snap.getLength()); + }, + fileExists: name => !!snapFor(name), + }; + return ts.createLanguageService(lshost); + } - function verifyNewLines(content: string, options: ts.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 === ts.NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); - assert(!result.outputFiles[0].text.match(options.newLine === ts.NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); - } + function verifyNewLines(content: string, options: ts.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 === ts.NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); + assert(!result.outputFiles[0].text.match(options.newLine === ts.NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); + } - function verifyBothNewLines(content: string) { - verifyNewLines(content, { newLine: ts.NewLineKind.CarriageReturnLineFeed }); - verifyNewLines(content, { newLine: ts.NewLineKind.LineFeed }); - } + function verifyBothNewLines(content: string) { + verifyNewLines(content, { newLine: ts.NewLineKind.CarriageReturnLineFeed }); + verifyNewLines(content, { newLine: ts.NewLineKind.LineFeed }); + } - function verifyOutliningSpanNewLines(content: string, options: ts.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 === ts.NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); - assert(!textAfterSpanCollapse.match(options.newLine === ts.NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); - } + function verifyOutliningSpanNewLines(content: string, options: ts.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 === ts.NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); + assert(!textAfterSpanCollapse.match(options.newLine === ts.NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); + } - it("should exist and respect provided compiler options", () => { - verifyBothNewLines(` + 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: ts.NewLineKind.CarriageReturnLineFeed }); - }); + 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: ts.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: ts.NewLineKind.LineFeed }); - }); + 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: ts.NewLineKind.LineFeed }); }); +}); } diff --git a/src/testRunner/unittests/services/languageService.ts b/src/testRunner/unittests/services/languageService.ts index 7f5d7b79d5747..df32c32891f56 100644 --- a/src/testRunner/unittests/services/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -1,11 +1,11 @@ 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"; +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"; @@ -14,233 +14,233 @@ 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 ts.ScriptSnapshot.fromString(""); - } - return ts.ScriptSnapshot.fromString(files[fileName] || ""); - }, - getCurrentDirectory: () => ".", - getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); - }, - fileExists: name => !!files[name], - readFile: name => files[name] - }); - } - // 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 + 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 ts.ScriptSnapshot.fromString(""); + } + return ts.ScriptSnapshot.fromString(files[fileName] || ""); + }, + getCurrentDirectory: () => ".", + getDefaultLibFileName(options) { + return ts.getDefaultLibFilePath(options); + }, + fileExists: name => !!files[name], + readFile: name => files[name] }); + } + // 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, - diagnostics: ts.emptyArray, - outputFiles: ts.emptyArray, - exportedModulesFromDeclarationEmit: undefined - }); - assert.deepEqual(languageService.getEmitOutput("foo.ts", - /*emitOnlyDtsFiles*/ true, - /*forceDtsEmit*/ true), { - emitSkipped: false, - diagnostics: ts.emptyArray, - outputFiles: [{ - name: "foo.d.ts", - text: "export {};\r\n", - writeByteOrderMark: false - }], - exportedModulesFromDeclarationEmit: undefined + it("getEmitOutput on language service has way to force dts emit", () => { + const languageService = createLanguageService(); + assert.deepEqual(languageService.getEmitOutput("foo.ts", + /*emitOnlyDtsFiles*/ true), { + emitSkipped: true, + diagnostics: ts.emptyArray, + outputFiles: ts.emptyArray, + exportedModulesFromDeclarationEmit: undefined }); + assert.deepEqual(languageService.getEmitOutput("foo.ts", + /*emitOnlyDtsFiles*/ true, + /*forceDtsEmit*/ true), { + emitSkipped: false, + diagnostics: ts.emptyArray, + 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 = new ts.Map(); - 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: ts.projectSystem.libFile.content }); - const host: ts.LanguageServiceHost = { - useCaseSensitiveFileNames: ts.returnTrue, - getCompilationSettings: ts.getDefaultCompilerOptions, - fileExists: path => files.has(path), - readFile: path => files.get(path)?.text, - getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, - getScriptFileNames: () => ["/project/root.ts"], - getScriptVersion: path => files.get(path)?.version || "", - getScriptSnapshot: path => { - const text = files.get(path)?.text; - return text ? ts.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); + }); + describe("detects program upto date correctly", () => { + function verifyProgramUptoDate(useProjectVersion: boolean) { + let projectVersion = "1"; + const files = new ts.Map(); + 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: ts.projectSystem.libFile.content }); + const host: ts.LanguageServiceHost = { + useCaseSensitiveFileNames: ts.returnTrue, + getCompilationSettings: ts.getDefaultCompilerOptions, + fileExists: path => files.has(path), + readFile: path => files.get(path)?.text, + getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, + getScriptFileNames: () => ["/project/root.ts"], + getScriptVersion: path => files.get(path)?.version || "", + getScriptSnapshot: path => { + const text = files.get(path)?.text; + return text ? ts.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 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); + // 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: ts.Program) { - assert.deepEqual(program.getSourceFiles().map(f => f.fileName), ["/lib/lib.d.ts", "/project/other.ts", "/project/root.ts"]); - } + function verifyProgramFiles(program: ts.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); }); + }); - describe("detects program upto date when new file is added to the referenced project", () => { - function setup(useSourceOfProjectReferenceRedirect: (() => boolean) | undefined) { - const config1: ts.TestFSWithWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - exclude: ["temp"] - }) - }; - const class1: ts.TestFSWithWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, - content: `class class1 {}` - }; - const class1Dts: ts.TestFSWithWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, - content: `declare class class1 {}` - }; - const config2: ts.TestFSWithWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - references: [ - { path: "../project1" } - ] - }) - }; - const class2: ts.TestFSWithWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, - content: `class class2 {}` - }; - const system = ts.projectSystem.createServerHost([config1, class1, class1Dts, config2, class2, ts.projectSystem.libFile]); - const result = ts.getParsedCommandLineOfConfigFile(`${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, /*optionsToExtend*/ undefined, { - useCaseSensitiveFileNames: true, - fileExists: path => system.fileExists(path), - readFile: path => system.readFile(path), - getCurrentDirectory: () => system.getCurrentDirectory(), - readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), - onUnRecoverableConfigFileDiagnostic: ts.noop, - })!; - const host: ts.LanguageServiceHost = { - useCaseSensitiveFileNames: ts.returnTrue, - useSourceOfProjectReferenceRedirect, - getCompilationSettings: () => result.options, - fileExists: path => system.fileExists(path), - readFile: path => system.readFile(path), - getScriptFileNames: () => result.fileNames, - getScriptVersion: path => { - const text = system.readFile(path); - return text !== undefined ? system.createHash(path) : ""; + describe("detects program upto date when new file is added to the referenced project", () => { + function setup(useSourceOfProjectReferenceRedirect: (() => boolean) | undefined) { + const config1: ts.TestFSWithWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true }, - getScriptSnapshot: path => { - const text = system.readFile(path); - return text ? ts.ScriptSnapshot.fromString(text) : undefined; + exclude: ["temp"] + }) + }; + const class1: ts.TestFSWithWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, + content: `class class1 {}` + }; + const class1Dts: ts.TestFSWithWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, + content: `declare class class1 {}` + }; + const config2: ts.TestFSWithWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true }, - readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), - getCurrentDirectory: () => system.getCurrentDirectory(), - getDefaultLibFileName: () => ts.projectSystem.libFile.path, - getProjectReferences: () => result.projectReferences, - }; - const ls = ts.createLanguageService(host); - return { system, ls, class1, class1Dts, class2 }; - } - it("detects program upto date when new file is added to the referenced project", () => { - const { ls, system, class1, class2 } = setup(ts.returnTrue); - assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1.path, class2.path]); - // Add new file to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - system.writeFile(class3, `class class3 {}`); - const program = ls.getProgram()!; - assert.deepEqual(program.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1.path, class3, class2.path]); - // Add excluded file to referenced project - system.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - assert.strictEqual(ls.getProgram(), program); - // Add output from new class to referenced project - system.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`); - assert.strictEqual(ls.getProgram(), program); - }); + references: [ + { path: "../project1" } + ] + }) + }; + const class2: ts.TestFSWithWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, + content: `class class2 {}` + }; + const system = ts.projectSystem.createServerHost([config1, class1, class1Dts, config2, class2, ts.projectSystem.libFile]); + const result = ts.getParsedCommandLineOfConfigFile(`${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, /*optionsToExtend*/ undefined, { + useCaseSensitiveFileNames: true, + fileExists: path => system.fileExists(path), + readFile: path => system.readFile(path), + getCurrentDirectory: () => system.getCurrentDirectory(), + readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), + onUnRecoverableConfigFileDiagnostic: ts.noop, + })!; + const host: ts.LanguageServiceHost = { + useCaseSensitiveFileNames: ts.returnTrue, + useSourceOfProjectReferenceRedirect, + getCompilationSettings: () => result.options, + fileExists: path => system.fileExists(path), + readFile: path => system.readFile(path), + getScriptFileNames: () => result.fileNames, + getScriptVersion: path => { + const text = system.readFile(path); + return text !== undefined ? system.createHash(path) : ""; + }, + getScriptSnapshot: path => { + const text = system.readFile(path); + return text ? ts.ScriptSnapshot.fromString(text) : undefined; + }, + readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), + getCurrentDirectory: () => system.getCurrentDirectory(), + getDefaultLibFileName: () => ts.projectSystem.libFile.path, + getProjectReferences: () => result.projectReferences, + }; + const ls = ts.createLanguageService(host); + return { system, ls, class1, class1Dts, class2 }; + } + it("detects program upto date when new file is added to the referenced project", () => { + const { ls, system, class1, class2 } = setup(ts.returnTrue); + assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1.path, class2.path]); + // Add new file to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + system.writeFile(class3, `class class3 {}`); + const program = ls.getProgram()!; + assert.deepEqual(program.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1.path, class3, class2.path]); + // Add excluded file to referenced project + system.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + assert.strictEqual(ls.getProgram(), program); + // Add output from new class to referenced project + system.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`); + assert.strictEqual(ls.getProgram(), program); + }); - it("detects program upto date when new file is added to the referenced project without useSourceOfProjectReferenceRedirect", () => { - const { ls, system, class1Dts, class2 } = setup(/*useSourceOfProjectReferenceRedirect*/ undefined); - const program1 = ls.getProgram()!; - assert.deepEqual(program1.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); - // Add new file to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - system.writeFile(class3, `class class3 {}`); - assert.notStrictEqual(ls.getProgram(), program1); - assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); - // Add class3 output - const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; - system.writeFile(class3Dts, `declare class class3 {}`); - const program2 = ls.getProgram()!; - assert.deepEqual(program2.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path]); - // Add excluded file to referenced project - system.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - assert.strictEqual(ls.getProgram(), program2); - // Delete output from new class to referenced project - system.deleteFile(class3Dts); - assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); - // Write output again - system.writeFile(class3Dts, `declare class class3 {}`); - assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path]); - }); + it("detects program upto date when new file is added to the referenced project without useSourceOfProjectReferenceRedirect", () => { + const { ls, system, class1Dts, class2 } = setup(/*useSourceOfProjectReferenceRedirect*/ undefined); + const program1 = ls.getProgram()!; + assert.deepEqual(program1.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); + // Add new file to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + system.writeFile(class3, `class class3 {}`); + assert.notStrictEqual(ls.getProgram(), program1); + assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); + // Add class3 output + const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; + system.writeFile(class3Dts, `declare class class3 {}`); + const program2 = ls.getProgram()!; + assert.deepEqual(program2.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path]); + // Add excluded file to referenced project + system.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + assert.strictEqual(ls.getProgram(), program2); + // Delete output from new class to referenced project + system.deleteFile(class3Dts); + assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class2.path]); + // Write output again + system.writeFile(class3Dts, `declare class class3 {}`); + assert.deepEqual(ls.getProgram()!.getSourceFiles().map(f => f.fileName), [ts.projectSystem.libFile.path, class1Dts.path, class3Dts, class2.path]); }); }); +}); } diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 172f07e9c7771..c34ec6e5e6125 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1,286 +1,286 @@ 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";`); - }); +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 non-relative", () => { - assertSortsBefore(`import y from "lib";`, `import x from "./lib";`); - }); + it("Sort - relative vs relative", () => { + assertSortsBefore(`import y from "./lib1";`, `import x from "./lib2";`); + }); - it("Sort - case-insensitive", () => { - assertSortsBefore(`import y from "a";`, `import x from "Z";`); - assertSortsBefore(`import y from "A";`, `import x from "z";`); - }); + it("Sort - relative vs non-relative", () => { + assertSortsBefore(`import y from "lib";`, `import x from "./lib";`); + }); - function assertSortsBefore(importString1: string, importString2: string) { - const [{moduleSpecifier: moduleSpecifier1}, {moduleSpecifier: moduleSpecifier2}] = parseImports(importString1, importString2); - assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), ts.Comparison.LessThan); - assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), ts.Comparison.GreaterThan); - } + it("Sort - case-insensitive", () => { + assertSortsBefore(`import y from "a";`, `import x from "Z";`); + assertSortsBefore(`import y from "A";`, `import x from "z";`); }); - describe("Coalesce imports", () => { - it("No imports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceImports([])); - }); + function assertSortsBefore(importString1: string, importString2: string) { + const [{moduleSpecifier: moduleSpecifier1}, {moduleSpecifier: moduleSpecifier2}] = parseImports(importString1, importString2); + assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), ts.Comparison.LessThan); + assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), ts.Comparison.GreaterThan); + } + }); - it("Sort specifiers - case-insensitive", () => { - const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { a as n, B, default as M, y, Z as O } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + describe("Coalesce imports", () => { + it("No imports", () => { + assert.isEmpty(ts.OrganizeImports.coalesceImports([])); + }); - it("Combine side-effect-only imports", () => { - const sortedImports = parseImports(`import "lib";`, `import "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Sort specifiers - case-insensitive", () => { + const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); + const actualCoalescedImports = ts.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 namespace imports", () => { - const sortedImports = parseImports(`import * as x from "lib";`, `import * as y from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine side-effect-only imports", () => { + const sortedImports = parseImports(`import "lib";`, `import "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine default imports", () => { - const sortedImports = parseImports(`import x from "lib";`, `import y from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine namespace imports", () => { + const sortedImports = parseImports(`import * as x from "lib";`, `import * as y from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine property imports", () => { - const sortedImports = parseImports(`import { x } from "lib";`, `import { y as z } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine default imports", () => { + const sortedImports = parseImports(`import x from "lib";`, `import y from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { default as x, default as y } 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 = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine property imports", () => { + const sortedImports = parseImports(`import { x } from "lib";`, `import { y as z } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine side-effect-only import with default import", () => { - const sortedImports = parseImports(`import "lib";`, `import x from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine side-effect-only import with namespace import", () => { + const sortedImports = parseImports(`import "lib";`, `import * as x from "lib";`); + const actualCoalescedImports = ts.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 = ts.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 = ts.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 = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import y, * as x from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Combine side-effect-only import with property import", () => { + const sortedImports = parseImports(`import "lib";`, `import { x } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine namespace import with property import", () => { - const sortedImports = parseImports(`import * as x from "lib";`, `import { y } from "lib";`); - const actualCoalescedImports = ts.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 = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import y, * as x from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine default import with property import", () => { - const sortedImports = parseImports(`import x from "lib";`, `import { y } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import x, { y } 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 = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + 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 = ts.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); - }); + it("Combine default import with property import", () => { + const sortedImports = parseImports(`import x from "lib";`, `import { y } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import x, { y } 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 = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - 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 = ts.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); + }); - 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 = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { z } from "lib";`, `import type { x, y } 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 = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + 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 = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = actualCoalescedImports; - 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 = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { z } from "lib";`, `import type { x, y } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); - describe("Coalesce exports", () => { - it("No exports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceExports([])); - }); + 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 = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = actualCoalescedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + }); - it("Sort specifiers - case-insensitive", () => { - const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { a as n, B, default as M, y, Z as O } from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + describe("Coalesce exports", () => { + it("No exports", () => { + assert.isEmpty(ts.OrganizeImports.coalesceExports([])); + }); - it("Sort specifiers - type-only", () => { - const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Sort specifiers - case-insensitive", () => { + const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); + const actualCoalescedExports = ts.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 = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export * from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Sort specifiers - type-only", () => { + const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); - it("Combine property exports", () => { - const sortedExports = parseExports(`export { x };`, `export { y as z };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { x, y as z };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Combine namespace re-exports", () => { + const sortedExports = parseExports(`export * from "lib";`, `export * from "lib";`); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export * from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); - it("Combine property re-exports", () => { - const sortedExports = parseExports(`export { x } from "lib";`, `export { y as z } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Combine property exports", () => { + const sortedExports = parseExports(`export { x };`, `export { y as z };`); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { x, y as z };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); - it("Combine namespace re-export with property re-export", () => { - const sortedExports = parseExports(`export * from "lib";`, `export { y } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = sortedExports; - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Combine property re-exports", () => { + const sortedExports = parseExports(`export { x } from "lib";`, `export { y as z } from "lib";`); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); + 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 = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { w as q, x, y as w, z as default };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Combine namespace re-export with property re-export", () => { + const sortedExports = parseExports(`export * from "lib";`, `export { y } from "lib";`); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = sortedExports; + 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 = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export * from "lib";`, `export { x as a, y, z as b } from "lib";`); - 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 = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { w as q, x, y as w, z as default };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); - it("Keep type-only exports separate", () => { - const sortedExports = parseExports(`export { x };`, `export type { y };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = sortedExports; - 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 = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export * from "lib";`, `export { x as a, y, z as b } from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); - it("Combine type-only exports", () => { - const sortedExports = parseExports(`export type { x };`, `export type { y };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export type { x, y };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Keep type-only exports separate", () => { + const sortedExports = parseExports(`export { x };`, `export type { y };`); + const actualCoalescedExports = ts.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 = ts.OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export type { x, y };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + }); - describe("Baselines", () => { - const libFile = { - path: "/lib.ts", - content: ` + 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() { }", - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); - }); + // 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 }, ts.testFormatSettings, ts.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 }, ts.testFormatSettings, ts.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 }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); - it("doesn't return any changes when the text would be identical", () => { - const testFile = { - path: "/a.ts", - content: `import { f } from 'foo';\nf();` - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); - }); + it("doesn't return any changes when the text would be identical", () => { + const testFile = { + path: "/a.ts", + content: `import { f } from 'foo';\nf();` + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); - testOrganizeImports("Renamed_used", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("Renamed_used", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1 as EffOne, F2 as EffTwo } from "lib"; EffOne(); `, - }, libFile); + }, libFile); - testOrganizeImports("Simple", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("Simple", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; @@ -290,25 +290,25 @@ D(); F1(); F2(); `, - }, libFile); + }, libFile); - testOrganizeImports("Unused_Some", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("Unused_Some", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; D(); `, - }, libFile); + }, libFile); - describe("skipDestructiveCodeActions=true", () => { - testOrganizeImports("Syntax_Error_Body_skipDestructiveCodeActions", - /*skipDestructiveCodeActions*/ true, { - path: "/test.ts", - content: ` + describe("skipDestructiveCodeActions=true", () => { + testOrganizeImports("Syntax_Error_Body_skipDestructiveCodeActions", + /*skipDestructiveCodeActions*/ true, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; @@ -316,13 +316,13 @@ import D from "lib"; class class class; D; `, - }, libFile); - }); + }, libFile); + }); - testOrganizeImports("Syntax_Error_Imports_skipDestructiveCodeActions", - /*skipDestructiveCodeActions*/ true, { - path: "/test.ts", - content: ` + testOrganizeImports("Syntax_Error_Imports_skipDestructiveCodeActions", + /*skipDestructiveCodeActions*/ true, { + path: "/test.ts", + content: ` import { F1, F2 class class class; } from "lib"; import * as NS from "lib"; class class class; @@ -330,13 +330,13 @@ import D from "lib"; D; `, - }, libFile); + }, libFile); - describe("skipDestructiveCodeActions=false", () => { - testOrganizeImports("Syntax_Error_Body", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + describe("skipDestructiveCodeActions=false", () => { + testOrganizeImports("Syntax_Error_Body", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; @@ -344,12 +344,12 @@ import D from "lib"; class class class; D; `, - }, libFile); + }, libFile); - testOrganizeImports("Syntax_Error_Imports", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("Syntax_Error_Imports", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 class class class; } from "lib"; import * as NS from "lib"; class class class; @@ -357,45 +357,45 @@ import D from "lib"; D; `, - }, libFile); - }); - - it("doesn't return any changes when the text would be identical", () => { - const testFile = { - path: "/a.ts", - content: `import { f } from 'foo';\nf();` - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); + }, libFile); }); - testOrganizeImports("Unused_All", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + it("doesn't return any changes when the text would be identical", () => { + const testFile = { + path: "/a.ts", + content: `import { f } from 'foo';\nf();` + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); + + testOrganizeImports("Unused_All", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; `, - }, libFile); + }, libFile); - it("Unused_Empty", () => { - const testFile = { - path: "/test.ts", - content: ` + it("Unused_Empty", () => { + const testFile = { + path: "/test.ts", + content: ` import { } from "lib"; `, - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); - }); + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); - testOrganizeImports("Unused_false_positive_module_augmentation", - /*skipDestructiveCodeActions*/ false, { - path: "/test.d.ts", - content: ` + testOrganizeImports("Unused_false_positive_module_augmentation", + /*skipDestructiveCodeActions*/ false, { + path: "/test.d.ts", + content: ` import foo from 'foo'; import { Caseless } from 'caseless'; @@ -405,12 +405,12 @@ declare module 'caseless' { test(name: KeyType): boolean; } }` - }); + }); - testOrganizeImports("Unused_preserve_imports_for_module_augmentation_in_non_declaration_file", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("Unused_preserve_imports_for_module_augmentation_in_non_declaration_file", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import foo from 'foo'; import { Caseless } from 'caseless'; @@ -420,38 +420,38 @@ declare module 'caseless' { test(name: KeyType): boolean; } }` - }); + }); - it("Unused_false_positive_shorthand_assignment", () => { - const testFile = { - path: "/test.ts", - content: ` + it("Unused_false_positive_shorthand_assignment", () => { + const testFile = { + path: "/test.ts", + content: ` import { x } from "a"; const o = { x }; ` - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); - }); + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); - it("Unused_false_positive_export_shorthand", () => { - const testFile = { - path: "/test.ts", - content: ` + it("Unused_false_positive_export_shorthand", () => { + const testFile = { + path: "/test.ts", + content: ` import { x } from "a"; export { x }; ` - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); - assert.isEmpty(changes); - }); + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, ts.testFormatSettings, ts.emptyOptions); + assert.isEmpty(changes); + }); - testOrganizeImports("MoveToTop", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("MoveToTop", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; F1(); F2(); @@ -460,13 +460,13 @@ NS.F1(); import D from "lib"; D(); `, - }, libFile); + }, libFile); - /* eslint-disable no-template-curly-in-string */ - testOrganizeImports("MoveToTop_Invalid", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + /* eslint-disable no-template-curly-in-string */ + testOrganizeImports("MoveToTop_Invalid", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; F1(); F2(); @@ -477,95 +477,95 @@ import a from ${"`${'lib'}`"}; import D from "lib"; D(); `, - }, libFile); - /* eslint-enable no-template-curly-in-string */ + }, libFile); + /* eslint-enable no-template-curly-in-string */ - testOrganizeImports("TypeOnly", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("TypeOnly", + /*skipDestructiveCodeActions*/ false, { + 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", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("CoalesceMultipleModules", + /*skipDestructiveCodeActions*/ false, { + 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;" }); + }, { path: "/lib1.ts", content: "export const b = 1, d = 2;" }, { path: "/lib2.ts", content: "export const a = 3, c = 4;" }); - testOrganizeImports("CoalesceTrivia", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("CoalesceTrivia", + /*skipDestructiveCodeActions*/ false, { + 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); + }, libFile); - testOrganizeImports("SortTrivia", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("SortTrivia", + /*skipDestructiveCodeActions*/ false, { + 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: "" }); + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); - testOrganizeImports("UnusedTrivia1", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("UnusedTrivia1", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` /*A*/import /*B*/ { /*C*/ F1 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I `, - }, libFile); + }, libFile); - testOrganizeImports("UnusedTrivia2", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("UnusedTrivia2", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` /*A*/import /*B*/ { /*C*/ F1 /*D*/, /*E*/ F2 /*F*/ } /*G*/ from /*H*/ "lib" /*I*/;/*J*/ //K F1(); `, - }, libFile); + }, libFile); - testOrganizeImports("UnusedHeaderComment", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("UnusedHeaderComment", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` // Header import { F1 } from "lib"; `, - }, libFile); + }, libFile); - testOrganizeImports("SortHeaderComment", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("SortHeaderComment", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` // Header import "lib2"; import "lib1"; `, - }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); - testOrganizeImports("AmbientModule", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("AmbientModule", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` declare module "mod" { import { F1 } from "lib"; import * as NS from "lib"; @@ -574,12 +574,12 @@ declare module "mod" { function F(f1: {} = F1, f2: {} = F2) {} } `, - }, libFile); + }, libFile); - testOrganizeImports("TopLevelAndAmbientModule", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("TopLevelAndAmbientModule", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import D from "lib"; declare module "mod" { @@ -595,141 +595,141 @@ import "lib"; D(); `, - }, libFile); + }, libFile); - testOrganizeImports("JsxFactoryUsedJsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.jsx", - content: ` + testOrganizeImports("JsxFactoryUsedJsx", + /*skipDestructiveCodeActions*/ false, { + path: "/test.jsx", + content: ` import { React, Other } from "react";
; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxFactoryUsedJs", - /*skipDestructiveCodeActions*/ false, { - path: "/test.js", - content: ` + testOrganizeImports("JsxFactoryUsedJs", + /*skipDestructiveCodeActions*/ false, { + path: "/test.js", + content: ` import { React, Other } from "react";
; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxFactoryUsedTsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.tsx", - content: ` + testOrganizeImports("JsxFactoryUsedTsx", + /*skipDestructiveCodeActions*/ false, { + path: "/test.tsx", + content: ` import { React, Other } from "react";
; `, - }, reactLibFile); + }, reactLibFile); - // TS files are not JSX contexts, so the parser does not treat - // `
` as a JSX element. - testOrganizeImports("JsxFactoryUsedTs", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + // TS files are not JSX contexts, so the parser does not treat + // `
` as a JSX element. + testOrganizeImports("JsxFactoryUsedTs", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { React, Other } from "react";
; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxFactoryUnusedJsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.jsx", - content: ` + testOrganizeImports("JsxFactoryUnusedJsx", + /*skipDestructiveCodeActions*/ false, { + path: "/test.jsx", + content: ` import { React, Other } from "react"; `, - }, reactLibFile); + }, 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", - /*skipDestructiveCodeActions*/ false, { - path: "/test.js", - content: ` + // 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", + /*skipDestructiveCodeActions*/ false, { + path: "/test.js", + content: ` import { React, Other } from "react"; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxFactoryUnusedTsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.tsx", - content: ` + testOrganizeImports("JsxFactoryUnusedTsx", + /*skipDestructiveCodeActions*/ false, { + path: "/test.tsx", + content: ` import { React, Other } from "react"; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxFactoryUnusedTs", - /*skipDestructiveCodeActions*/ false, { - path: "/test.ts", - content: ` + testOrganizeImports("JsxFactoryUnusedTs", + /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: ` import { React, Other } from "react"; `, - }, reactLibFile); + }, reactLibFile); - testOrganizeImports("JsxPragmaTsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.tsx", - content: `/** @jsx jsx */ + testOrganizeImports("JsxPragmaTsx", + /*skipDestructiveCodeActions*/ false, { + 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 { } } ` - }); + }); - testOrganizeImports("JsxFragmentPragmaTsx", - /*skipDestructiveCodeActions*/ false, { - path: "/test.tsx", - content: `/** @jsx h */ + testOrganizeImports("JsxFragmentPragmaTsx", + /*skipDestructiveCodeActions*/ false, { + path: "/test.tsx", + content: `/** @jsx h */ /** @jsxFrag frag */ import { h, frag } from "@foo/core"; const elem = <>
Foo
; `, - }, { - path: "/@foo/core/index.d.ts", - content: `export function h(): void; + }, { + path: "/@foo/core/index.d.ts", + content: `export function h(): void; export function frag(): void; ` - }); + }); - describe("Exports", () => { + describe("Exports", () => { - testOrganizeExports("MoveToTop", { - path: "/test.ts", - content: ` + testOrganizeExports("MoveToTop", { + path: "/test.ts", + content: ` export { F1, F2 } from "lib"; 1; export * from "lib"; 2; `, - }, libFile); + }, libFile); - /* eslint-disable no-template-curly-in-string */ - testOrganizeExports("MoveToTop_Invalid", { - path: "/test.ts", - content: ` + /* eslint-disable no-template-curly-in-string */ + testOrganizeExports("MoveToTop_Invalid", { + path: "/test.ts", + content: ` export { F1, F2 } from "lib"; 1; export * from "lib"; @@ -739,12 +739,12 @@ export { a } from ${"`${'lib'}`"}; export { D } from "lib"; 3; `, - }, libFile); - /* eslint-enable no-template-curly-in-string */ + }, libFile); + /* eslint-enable no-template-curly-in-string */ - testOrganizeExports("MoveToTop_WithImportsFirst", { - path: "/test.ts", - content: ` + testOrganizeExports("MoveToTop_WithImportsFirst", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; 1; export { F1, F2 } from "lib"; @@ -755,10 +755,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"; @@ -769,51 +769,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" { @@ -825,142 +825,142 @@ declare module "mod" { export { E } from "lib"; export * from "lib"; `, - }, libFile); - }); + }, libFile); + }); - function testOrganizeExports(testName: string, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { - testOrganizeImports(`${testName}.exports`, /*skipDestructiveCodeActions*/ true, testFile, ...otherFiles); - } - - function testOrganizeImports(testName: string, skipDestructiveCodeActions: boolean, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { - it(testName, () => runBaseline(`organizeImports/${testName}.ts`, skipDestructiveCodeActions, testFile, ...otherFiles)); - } - - function runBaseline(baselinePath: string, skipDestructiveCodeActions: boolean, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { - const { path: testPath, content: testContent } = testFile; - const languageService = makeLanguageService(testFile, ...otherFiles); - const changes = languageService.organizeImports({ skipDestructiveCodeActions, type: "file", fileName: testPath }, ts.testFormatSettings, ts.emptyOptions); - assert.equal(changes.length, 1); - assert.equal(changes[0].fileName, testPath); - - const newText = ts.textChanges.applyChanges(testContent, changes[0].textChanges); - Harness.Baseline.runBaseline(baselinePath, [ - "// ==ORIGINAL==", - testContent, - "// ==ORGANIZED==", - newText, - ].join(ts.newLineCharacter)); - } - - function makeLanguageService(...files: ts.TestFSWithWatch.File[]) { - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); - projectService.setCompilerOptionsForInferredProjects({ jsx: files.some(f => f.path.endsWith("x")) ? ts.JsxEmit.React : ts.JsxEmit.None }); - files.forEach(f => projectService.openClientFile(f.path)); - return projectService.inferredProjects[0].getLanguageService(); - } - }); - - function parseImports(...importStrings: string[]): readonly ts.ImportDeclaration[] { - const sourceFile = ts.createSourceFile("a.ts", importStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); - const imports = ts.filter(sourceFile.statements, ts.isImportDeclaration); - assert.equal(imports.length, importStrings.length); - return imports; + function testOrganizeExports(testName: string, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { + testOrganizeImports(`${testName}.exports`, /*skipDestructiveCodeActions*/ true, testFile, ...otherFiles); } - function parseExports(...exportStrings: string[]): readonly ts.ExportDeclaration[] { - const sourceFile = ts.createSourceFile("a.ts", exportStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); - const exports = ts.filter(sourceFile.statements, ts.isExportDeclaration); - assert.equal(exports.length, exportStrings.length); - return exports; + function testOrganizeImports(testName: string, skipDestructiveCodeActions: boolean, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { + it(testName, () => runBaseline(`organizeImports/${testName}.ts`, skipDestructiveCodeActions, testFile, ...otherFiles)); } - function assertEqual(node1?: ts.Node, node2?: ts.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 ts.SyntaxKind.ImportDeclaration: - const decl1 = node1 as ts.ImportDeclaration; - const decl2 = node2 as ts.ImportDeclaration; - assertEqual(decl1.importClause, decl2.importClause); - assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier); - break; - case ts.SyntaxKind.ImportClause: - const clause1 = node1 as ts.ImportClause; - const clause2 = node2 as ts.ImportClause; - assertEqual(clause1.name, clause2.name); - assertEqual(clause1.namedBindings, clause2.namedBindings); - break; - case ts.SyntaxKind.NamespaceImport: - const nsi1 = node1 as ts.NamespaceImport; - const nsi2 = node2 as ts.NamespaceImport; - assertEqual(nsi1.name, nsi2.name); - break; - case ts.SyntaxKind.NamedImports: - const ni1 = node1 as ts.NamedImports; - const ni2 = node2 as ts.NamedImports; - assertListEqual(ni1.elements, ni2.elements); - break; - case ts.SyntaxKind.ImportSpecifier: - const is1 = node1 as ts.ImportSpecifier; - const is2 = node2 as ts.ImportSpecifier; - assertEqual(is1.name, is2.name); - assertEqual(is1.propertyName, is2.propertyName); - break; - case ts.SyntaxKind.ExportDeclaration: - const ed1 = node1 as ts.ExportDeclaration; - const ed2 = node2 as ts.ExportDeclaration; - assertEqual(ed1.exportClause, ed2.exportClause); - assertEqual(ed1.moduleSpecifier, ed2.moduleSpecifier); - break; - case ts.SyntaxKind.NamedExports: - const ne1 = node1 as ts.NamedExports; - const ne2 = node2 as ts.NamedExports; - assertListEqual(ne1.elements, ne2.elements); - break; - case ts.SyntaxKind.ExportSpecifier: - const es1 = node1 as ts.ExportSpecifier; - const es2 = node2 as ts.ExportSpecifier; - assertEqual(es1.name, es2.name); - assertEqual(es1.propertyName, es2.propertyName); - break; - case ts.SyntaxKind.Identifier: - const id1 = node1 as ts.Identifier; - const id2 = node2 as ts.Identifier; - assert.equal(id1.text, id2.text); - break; - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - const sl1 = node1 as ts.LiteralLikeNode; - const sl2 = node2 as ts.LiteralLikeNode; - assert.equal(sl1.text, sl2.text); - break; - default: - assert.equal(node1.getText(), node2.getText()); - break; - } + function runBaseline(baselinePath: string, skipDestructiveCodeActions: boolean, testFile: ts.TestFSWithWatch.File, ...otherFiles: ts.TestFSWithWatch.File[]) { + const { path: testPath, content: testContent } = testFile; + const languageService = makeLanguageService(testFile, ...otherFiles); + const changes = languageService.organizeImports({ skipDestructiveCodeActions, type: "file", fileName: testPath }, ts.testFormatSettings, ts.emptyOptions); + assert.equal(changes.length, 1); + assert.equal(changes[0].fileName, testPath); + + const newText = ts.textChanges.applyChanges(testContent, changes[0].textChanges); + Harness.Baseline.runBaseline(baselinePath, [ + "// ==ORIGINAL==", + testContent, + "// ==ORGANIZED==", + newText, + ].join(ts.newLineCharacter)); } - function assertListEqual(list1: readonly ts.Node[], list2: readonly ts.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: ts.TestFSWithWatch.File[]) { + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); + projectService.setCompilerOptionsForInferredProjects({ jsx: files.some(f => f.path.endsWith("x")) ? ts.JsxEmit.React : ts.JsxEmit.None }); + files.forEach(f => projectService.openClientFile(f.path)); + return projectService.inferredProjects[0].getLanguageService(); } }); + + function parseImports(...importStrings: string[]): readonly ts.ImportDeclaration[] { + const sourceFile = ts.createSourceFile("a.ts", importStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); + const imports = ts.filter(sourceFile.statements, ts.isImportDeclaration); + assert.equal(imports.length, importStrings.length); + return imports; + } + + function parseExports(...exportStrings: string[]): readonly ts.ExportDeclaration[] { + const sourceFile = ts.createSourceFile("a.ts", exportStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); + const exports = ts.filter(sourceFile.statements, ts.isExportDeclaration); + assert.equal(exports.length, exportStrings.length); + return exports; + } + + function assertEqual(node1?: ts.Node, node2?: ts.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 ts.SyntaxKind.ImportDeclaration: + const decl1 = node1 as ts.ImportDeclaration; + const decl2 = node2 as ts.ImportDeclaration; + assertEqual(decl1.importClause, decl2.importClause); + assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier); + break; + case ts.SyntaxKind.ImportClause: + const clause1 = node1 as ts.ImportClause; + const clause2 = node2 as ts.ImportClause; + assertEqual(clause1.name, clause2.name); + assertEqual(clause1.namedBindings, clause2.namedBindings); + break; + case ts.SyntaxKind.NamespaceImport: + const nsi1 = node1 as ts.NamespaceImport; + const nsi2 = node2 as ts.NamespaceImport; + assertEqual(nsi1.name, nsi2.name); + break; + case ts.SyntaxKind.NamedImports: + const ni1 = node1 as ts.NamedImports; + const ni2 = node2 as ts.NamedImports; + assertListEqual(ni1.elements, ni2.elements); + break; + case ts.SyntaxKind.ImportSpecifier: + const is1 = node1 as ts.ImportSpecifier; + const is2 = node2 as ts.ImportSpecifier; + assertEqual(is1.name, is2.name); + assertEqual(is1.propertyName, is2.propertyName); + break; + case ts.SyntaxKind.ExportDeclaration: + const ed1 = node1 as ts.ExportDeclaration; + const ed2 = node2 as ts.ExportDeclaration; + assertEqual(ed1.exportClause, ed2.exportClause); + assertEqual(ed1.moduleSpecifier, ed2.moduleSpecifier); + break; + case ts.SyntaxKind.NamedExports: + const ne1 = node1 as ts.NamedExports; + const ne2 = node2 as ts.NamedExports; + assertListEqual(ne1.elements, ne2.elements); + break; + case ts.SyntaxKind.ExportSpecifier: + const es1 = node1 as ts.ExportSpecifier; + const es2 = node2 as ts.ExportSpecifier; + assertEqual(es1.name, es2.name); + assertEqual(es1.propertyName, es2.propertyName); + break; + case ts.SyntaxKind.Identifier: + const id1 = node1 as ts.Identifier; + const id2 = node2 as ts.Identifier; + assert.equal(id1.text, id2.text); + break; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + const sl1 = node1 as ts.LiteralLikeNode; + const sl2 = node2 as ts.LiteralLikeNode; + assert.equal(sl1.text, sl2.text); + break; + default: + assert.equal(node1.getText(), node2.getText()); + break; + } + } + + function assertListEqual(list1: readonly ts.Node[], list2: readonly ts.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/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts index a369cf9d69c96..d8486048ea3ab 100644 --- a/src/testRunner/unittests/services/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -1,64 +1,64 @@ // Some tests have trailing whitespace namespace ts { - describe("unittests:: services:: textChanges", () => { - function findChild(name: string, n: ts.Node) { - return find(n)!; +describe("unittests:: services:: textChanges", () => { + function findChild(name: string, n: ts.Node) { + return find(n)!; - function find(node: ts.Node): ts.Node | undefined { - if (ts.isDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.escapedText === name) { - return node; - } - else { - return ts.forEachChild(node, find); - } + function find(node: ts.Node): ts.Node | undefined { + if (ts.isDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.escapedText === name) { + return node; + } + else { + return ts.forEachChild(node, find); } } + } - const printerOptions = { newLine: ts.NewLineKind.LineFeed }; - const newLineCharacter = ts.getNewLineCharacter(printerOptions); - function getRuleProvider(placeOpenBraceOnNewLineForFunctions: boolean): ts.formatting.FormatContext { - return ts.formatting.getFormatContext(placeOpenBraceOnNewLineForFunctions ? { ...ts.testFormatSettings, placeOpenBraceOnNewLineForFunctions: true } : ts.testFormatSettings, ts.notImplementedHost); - } + const printerOptions = { newLine: ts.NewLineKind.LineFeed }; + const newLineCharacter = ts.getNewLineCharacter(printerOptions); + function getRuleProvider(placeOpenBraceOnNewLineForFunctions: boolean): ts.formatting.FormatContext { + return ts.formatting.getFormatContext(placeOpenBraceOnNewLineForFunctions ? { ...ts.testFormatSettings, placeOpenBraceOnNewLineForFunctions: true } : ts.testFormatSettings, ts.notImplementedHost); + } - // 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: ts.Node, text: string): void { - const nodeList = flattenNodes(node); - const sourceFile = ts.createSourceFile("f.ts", text, ts.ScriptTarget.ES2015); - const parsedNodeList = flattenNodes(sourceFile.statements[0]); - ts.zipWith(nodeList, parsedNodeList, (left, right) => { - ts.Debug.assert(left.pos === right.pos); - ts.Debug.assert(left.end === right.end); - }); + // 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: ts.Node, text: string): void { + const nodeList = flattenNodes(node); + const sourceFile = ts.createSourceFile("f.ts", text, ts.ScriptTarget.ES2015); + const parsedNodeList = flattenNodes(sourceFile.statements[0]); + ts.zipWith(nodeList, parsedNodeList, (left, right) => { + ts.Debug.assert(left.pos === right.pos); + ts.Debug.assert(left.end === right.end); + }); - function flattenNodes(n: ts.Node) { - const data: (ts.Node | ts.NodeArray)[] = []; - walk(n); - return data; + function flattenNodes(n: ts.Node) { + const data: (ts.Node | ts.NodeArray)[] = []; + walk(n); + return data; - function walk(n: ts.Node | ts.NodeArray): void { - data.push(n); - return ts.isArray(n) ? ts.forEach(n, walk) : ts.forEachChild(n, walk, walk); - } + function walk(n: ts.Node | ts.NodeArray): void { + data.push(n); + return ts.isArray(n) ? ts.forEach(n, walk) : ts.forEachChild(n, walk, walk); } } + } - function runSingleFileTest(caption: string, placeOpenBraceOnNewLineForFunctions: boolean, text: string, validateNodes: boolean, testBlock: (sourceFile: ts.SourceFile, changeTracker: ts.textChanges.ChangeTracker) => void) { - it(caption, () => { - const sourceFile = ts.createSourceFile("source.ts", text, ts.ScriptTarget.ES2015, /*setParentNodes*/ true); - const rulesProvider = getRuleProvider(placeOpenBraceOnNewLineForFunctions); - const changeTracker = new ts.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 = ts.textChanges.applyChanges(sourceFile.text, changes[0].textChanges); - Harness.Baseline.runBaseline(`textChanges/${caption}.js`, `===ORIGINAL===${newLineCharacter}${text}${newLineCharacter}===MODIFIED===${newLineCharacter}${modified}`); - }); - } + function runSingleFileTest(caption: string, placeOpenBraceOnNewLineForFunctions: boolean, text: string, validateNodes: boolean, testBlock: (sourceFile: ts.SourceFile, changeTracker: ts.textChanges.ChangeTracker) => void) { + it(caption, () => { + const sourceFile = ts.createSourceFile("source.ts", text, ts.ScriptTarget.ES2015, /*setParentNodes*/ true); + const rulesProvider = getRuleProvider(placeOpenBraceOnNewLineForFunctions); + const changeTracker = new ts.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 = ts.textChanges.applyChanges(sourceFile.text, changes[0].textChanges); + Harness.Baseline.runBaseline(`textChanges/${caption}.js`, `===ORIGINAL===${newLineCharacter}${text}${newLineCharacter}===MODIFIED===${newLineCharacter}${modified}`); + }); + } - { - const text = ` + { + const text = ` namespace M { namespace M2 @@ -79,28 +79,28 @@ namespace M } } }`; - runSingleFileTest("extractMethodLike", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - const statements = (findChild("foo", sourceFile) as ts.FunctionDeclaration).body!.statements.slice(1); - const newFunction = ts.factory.createFunctionDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ "bar", - /*typeParameters*/ undefined, ts.emptyArray, - /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /*body */ ts.factory.createBlock(statements)); + runSingleFileTest("extractMethodLike", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + const statements = (findChild("foo", sourceFile) as ts.FunctionDeclaration).body!.statements.slice(1); + const newFunction = ts.factory.createFunctionDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ "bar", + /*typeParameters*/ undefined, ts.emptyArray, + /*type*/ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + /*body */ ts.factory.createBlock(statements)); - changeTracker.insertNodeBefore(sourceFile, /*before*/findChild("M2", sourceFile), newFunction); + changeTracker.insertNodeBefore(sourceFile, /*before*/findChild("M2", sourceFile), newFunction); - // replace statements with return statement - const newStatement = ts.factory.createReturnStatement(ts.factory.createCallExpression( - /*expression*/ newFunction.name!, - /*typeArguments*/ undefined, ts.emptyArray)); - changeTracker.replaceNodeRange(sourceFile, statements[0], ts.last(statements), newStatement, { suffix: newLineCharacter }); - }); - } - { - const text = ` + // replace statements with return statement + const newStatement = ts.factory.createReturnStatement(ts.factory.createCallExpression( + /*expression*/ newFunction.name!, + /*typeArguments*/ undefined, ts.emptyArray)); + changeTracker.replaceNodeRange(sourceFile, statements[0], ts.last(statements), newStatement, { suffix: newLineCharacter }); + }); + } + { + const text = ` function foo() { return 1; } @@ -109,19 +109,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: ts.SourceFile): ts.VariableStatement { - return ts.cast(findVariableDeclarationContaining(name, sourceFile).parent.parent, ts.isVariableStatement); - } - function findVariableDeclarationContaining(name: string, sourceFile: ts.SourceFile): ts.VariableDeclaration { - return ts.cast(findChild(name, sourceFile), ts.isVariableDeclaration); - } - const { deleteNode } = ts.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: ts.SourceFile): ts.VariableStatement { + return ts.cast(findVariableDeclarationContaining(name, sourceFile).parent.parent, ts.isVariableStatement); + } + function findVariableDeclarationContaining(name: string, sourceFile: ts.SourceFile): ts.VariableDeclaration { + return ts.cast(findChild(name, sourceFile), ts.isVariableDeclaration); + } + const { deleteNode } = ts.textChanges; + { + const text = ` var x = 1; // some comment - 1 /** * comment 2 @@ -129,24 +129,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: ts.textChanges.LeadingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNode3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNode4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.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: ts.textChanges.LeadingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNode3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNode4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.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 @@ -155,41 +155,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: ts.textChanges.LeadingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); - }); - } - function createTestVariableDeclaration(name: string) { - return ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment("p1", ts.factory.createNumericLiteral(1))], /*multiline*/ true)); - } - function createTestClass() { - return ts.factory.createClassDeclaration( - /*decorators*/ undefined, [ - ts.factory.createToken(ts.SyntaxKind.PublicKeyword) - ], "class1", - /*typeParameters*/ undefined, [ - ts.factory.createHeritageClause(ts.SyntaxKind.ImplementsKeyword, [ - ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier("interface1"), /*typeArguments*/ undefined) - ]) - ], [ - ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, "property1", - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.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: ts.textChanges.LeadingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + } + function createTestVariableDeclaration(name: string) { + return ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment("p1", ts.factory.createNumericLiteral(1))], /*multiline*/ true)); + } + function createTestClass() { + return ts.factory.createClassDeclaration( + /*decorators*/ undefined, [ + ts.factory.createToken(ts.SyntaxKind.PublicKeyword) + ], "class1", + /*typeParameters*/ undefined, [ + ts.factory.createHeritageClause(ts.SyntaxKind.ImplementsKeyword, [ + ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier("interface1"), /*typeArguments*/ undefined) + ]) + ], [ + ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, "property1", + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + /*initializer*/ undefined) + ]); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -197,31 +197,31 @@ 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("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("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 @@ -229,24 +229,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: ts.textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); - }); - runSingleFileTest("replaceNode3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); - }); - runSingleFileTest("replaceNode4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("replaceNode5", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("x", sourceFile), createTestClass(), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.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: ts.textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); + }); + runSingleFileTest("replaceNode3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNode4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("replaceNode5", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("x", sourceFile), createTestClass(), { leadingTriviaOption: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -254,21 +254,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: ts.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: ts.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: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.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: ts.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: ts.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: ts.textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: ts.textChanges.TrailingTriviaOption.Exclude }); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -276,15 +276,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 @@ -294,104 +294,104 @@ 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()); - }); - } + 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: ts.SourceFile): ts.ConstructorDeclaration { - const classDecl = sourceFile.statements[0] as ts.ClassDeclaration; - return ts.find(classDecl.members, (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && !!m.body)!; - } - function createTestSuperCall() { - const superCall = ts.factory.createCallExpression(ts.factory.createSuper(), - /*typeArguments*/ undefined, ts.emptyArray); - return ts.factory.createExpressionStatement(superCall); - } + function findConstructor(sourceFile: ts.SourceFile): ts.ConstructorDeclaration { + const classDecl = sourceFile.statements[0] as ts.ClassDeclaration; + return ts.find(classDecl.members, (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && !!m.body)!; + } + function createTestSuperCall() { + const superCall = ts.factory.createCallExpression(ts.factory.createSuper(), + /*typeArguments*/ undefined, ts.emptyArray); + return ts.factory.createExpressionStatement(superCall); + } - { - const text1 = ` + { + 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 @@ -399,348 +399,348 @@ 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), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(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), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + } + { + const text = ` const x = 1;`; - runSingleFileTest("insertNodeInListAfter5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + } + { + const text = ` const x = 1, y = 2;`; - runSingleFileTest("insertNodeInListAfter6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(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), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter8", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), ts.factory.createVariableDeclaration("z", /*exclamationToken*/ undefined, /*type*/ undefined, ts.factory.createNumericLiteral(1))); + }); + } + { + const text = ` import { x } from "bar"`; - runSingleFileTest("insertNodeInListAfter10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.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), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x } from "bar"`; - runSingleFileTest("insertNodeInListAfter12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.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), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, ts.factory.createIdentifier("b"), ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter16", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter16", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter17", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter17", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter18", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + runSingleFileTest("insertNodeInListAfter18", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); + }); + } + { + const runTest = (name: string, text: string) => runSingleFileTest(name, /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + for (const specifier of ["x3", "x4", "x5"]) { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier("a"))); - }); - } - { - const runTest = (name: string, text: string) => runSingleFileTest(name, /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - for (const specifier of ["x3", "x4", "x5"]) { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x2", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier(specifier))); - } - }); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x2", sourceFile), ts.factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, ts.factory.createIdentifier(specifier))); + } + }); - const crlfText = "import {\r\nx1,\r\nx2\r\n} from \"bar\";"; - runTest("insertNodeInListAfter19", crlfText); + const crlfText = "import {\r\nx1,\r\nx2\r\n} from \"bar\";"; + runTest("insertNodeInListAfter19", crlfText); - const lfText = "import {\nx1,\nx2\n} from \"bar\";"; - runTest("insertNodeInListAfter20", lfText); - } - { - const text = ` + const lfText = "import {\nx1,\nx2\n} from \"bar\";"; + runTest("insertNodeInListAfter20", lfText); + } + { + 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( - // eslint-disable-next-line boolean-trivia - ts.factory.createPropertyDeclaration(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( + // eslint-disable-next-line boolean-trivia + ts.factory.createPropertyDeclaration(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) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), ts.factory.createPropertyDeclaration(undefined, undefined, "a", undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), undefined)); - }); - } - { - const text = ` + runSingleFileTest("insertNodeAfterInClass1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), ts.factory.createPropertyDeclaration(undefined, undefined, "a", undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), undefined)); + }); + } + { + const text = ` class A { x; } `; - runSingleFileTest("insertNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), ts.factory.createPropertyDeclaration(undefined, undefined, "a", undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), undefined)); - }); - } - { - const text = ` + runSingleFileTest("insertNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + // eslint-disable-next-line boolean-trivia + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), ts.factory.createPropertyDeclaration(undefined, undefined, "a", undefined, ts.factory.createKeywordTypeNode(ts.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 = ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.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 = ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.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 = ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.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 = ts.factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), - /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = ts.factory.createPropertyDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, ts.factory.createComputedPropertyName(ts.factory.createNumericLiteral(1)), + /*questionToken*/ undefined, ts.factory.createKeywordTypeNode(ts.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 = ts.factory.createExpressionStatement(ts.factory.createParenthesizedExpression(ts.factory.createNumericLiteral(1))); - changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), newNode); - }); - } - }); + runSingleFileTest("insertNodeInStatementListAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = ts.factory.createExpressionStatement(ts.factory.createParenthesizedExpression(ts.factory.createNumericLiteral(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 df8c89edc4ba7..6332ee1404913 100644 --- a/src/testRunner/unittests/services/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -1,477 +1,477 @@ namespace ts { - describe("unittests:: services:: Transpile", () => { - - interface TranspileTestSettings { - options?: ts.TranspileOptions; - noSetFileName?: boolean; - } - - function transpilesCorrectly(name: string, input: string, testSettings: TranspileTestSettings) { - describe(name, () => { - let transpileResult: ts.TranspileOutput; - let oldTranspileResult: string; - let oldTranspileDiagnostics: ts.Diagnostic[]; - const transpileOptions: ts.TranspileOptions = testSettings.options || {}; - if (!transpileOptions.compilerOptions) { - transpileOptions.compilerOptions = { }; - } - if (transpileOptions.compilerOptions.target === undefined) { - transpileOptions.compilerOptions.target = ts.ScriptTarget.ES3; - } +describe("unittests:: services:: Transpile", () => { + + interface TranspileTestSettings { + options?: ts.TranspileOptions; + noSetFileName?: boolean; + } + + function transpilesCorrectly(name: string, input: string, testSettings: TranspileTestSettings) { + describe(name, () => { + let transpileResult: ts.TranspileOutput; + let oldTranspileResult: string; + let oldTranspileDiagnostics: ts.Diagnostic[]; + const transpileOptions: ts.TranspileOptions = testSettings.options || {}; + if (!transpileOptions.compilerOptions) { + transpileOptions.compilerOptions = { }; + } + if (transpileOptions.compilerOptions.target === undefined) { + transpileOptions.compilerOptions.target = ts.ScriptTarget.ES3; + } - if (transpileOptions.compilerOptions.newLine === undefined) { - // use \r\n as default new line - transpileOptions.compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed; - } + if (transpileOptions.compilerOptions.newLine === undefined) { + // use \r\n as default new line + transpileOptions.compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed; + } - transpileOptions.compilerOptions.sourceMap = true; + transpileOptions.compilerOptions.sourceMap = true; - let unitName = transpileOptions.fileName; - if (!unitName) { - unitName = transpileOptions.compilerOptions.jsx ? "file.tsx" : "file.ts"; - if (!testSettings.noSetFileName) { - transpileOptions.fileName = unitName; - } + 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 ? ts.Extension.Tsx : ts.Extension.Ts); - const toBeCompiled = [{ - unitName, - content: input - }]; - const canUseOldTranspile = !transpileOptions.renamedDependencies; - - before(() => { - transpileResult = ts.transpileModule(input, transpileOptions); - - if (canUseOldTranspile) { - oldTranspileDiagnostics = []; - oldTranspileResult = ts.transpile(input, transpileOptions.compilerOptions, transpileOptions.fileName, oldTranspileDiagnostics, transpileOptions.moduleName); - } - }); + transpileOptions.reportDiagnostics = true; - after(() => { - transpileResult = undefined!; - oldTranspileResult = undefined!; - oldTranspileDiagnostics = undefined!; - }); + const justName = "transpile/" + name.replace(/[^a-z0-9\-. ]/ig, "") + (transpileOptions.compilerOptions.jsx ? ts.Extension.Tsx : ts.Extension.Ts); + const toBeCompiled = [{ + unitName, + content: input + }]; + const canUseOldTranspile = !transpileOptions.renamedDependencies; - /* 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!)); - }); + before(() => { + transpileResult = ts.transpileModule(input, transpileOptions); 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)); - }); + oldTranspileDiagnostics = []; + oldTranspileResult = ts.transpile(input, transpileOptions.compilerOptions, transpileOptions.fileName, oldTranspileDiagnostics, transpileOptions.moduleName); } - /* eslint-enable no-null/no-null */ + }); - it("Correct output for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ts.Extension.Js), transpileResult.outputText); - }); + after(() => { + transpileResult = undefined!; + oldTranspileResult = undefined!; + oldTranspileDiagnostics = undefined!; + }); - if (canUseOldTranspile) { - it("Correct output (old transpile) for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.js"), oldTranspileResult); - }); - } + /* 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!)); }); - } - transpilesCorrectly("Generates no diagnostics with valid inputs", `var x = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + 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 */ - transpilesCorrectly("Generates no diagnostics for missing file references", `/// -var x = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + it("Correct output for " + justName, () => { + Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ts.Extension.Js), transpileResult.outputText); + }); - transpilesCorrectly("Generates no diagnostics for missing module imports", `import {a} from "module2";`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + if (canUseOldTranspile) { + it("Correct output (old transpile) for " + justName, () => { + Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.js"), oldTranspileResult); + }); + } }); + } - transpilesCorrectly("Generates expected syntactic diagnostics", `a b`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + transpilesCorrectly("Generates no diagnostics with valid inputs", `var x = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Does not generate semantic diagnostics", `var x: string = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + transpilesCorrectly("Generates no diagnostics for missing file references", `/// +var x = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Generates module output", `var x = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.AMD } } - }); + transpilesCorrectly("Generates no diagnostics for missing module imports", `import {a} from "module2";`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Uses correct newLine character", `var x = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS, newLine: ts.NewLineKind.LineFeed } } - }); + transpilesCorrectly("Generates expected syntactic diagnostics", `a b`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Sets module name", "var x = 1;", { - options: { compilerOptions: { module: ts.ModuleKind.System, newLine: ts.NewLineKind.LineFeed }, moduleName: "NamedModule" } - }); + transpilesCorrectly("Does not generate semantic diagnostics", `var x: string = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("No extra errors for file without extension", `"use strict";\r\nvar x = 0;`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS }, fileName: "file" } - }); + transpilesCorrectly("Generates module output", `var x = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.AMD } } + }); - transpilesCorrectly("Rename dependencies - System", `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ts.ModuleKind.System, newLine: ts.NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } - }); + transpilesCorrectly("Uses correct newLine character", `var x = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS, newLine: ts.NewLineKind.LineFeed } } + }); - transpilesCorrectly("Rename dependencies - AMD", `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ts.ModuleKind.AMD, newLine: ts.NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } - }); + transpilesCorrectly("Sets module name", "var x = 1;", { + options: { compilerOptions: { module: ts.ModuleKind.System, newLine: ts.NewLineKind.LineFeed }, moduleName: "NamedModule" } + }); - transpilesCorrectly("Rename dependencies - UMD", `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ts.ModuleKind.UMD, newLine: ts.NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } - }); + transpilesCorrectly("No extra errors for file without extension", `"use strict";\r\nvar x = 0;`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS }, fileName: "file" } + }); - 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: ts.ModuleKind.CommonJS, - newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true, - emitDecoratorMetadata: true, - experimentalDecorators: true, - target: ts.ScriptTarget.ES5, - } + transpilesCorrectly("Rename dependencies - System", `import {foo} from "SomeName";\n` + + `declare function use(a: any);\n` + + `use(foo);`, { + options: { compilerOptions: { module: ts.ModuleKind.System, newLine: ts.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: ts.ModuleKind.AMD, newLine: ts.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: ts.ModuleKind.UMD, newLine: ts.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: ts.ModuleKind.CommonJS, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + emitDecoratorMetadata: true, + experimentalDecorators: true, + target: ts.ScriptTarget.ES5, } - }); - - 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: ts.JsxEmit.React, newLine: ts.NewLineKind.LineFeed } } - }); + transpilesCorrectly("Supports backslashes in file name", "var x", { + options: { fileName: "a\\b.ts" } + }); - transpilesCorrectly("transpile .js files", "const a = 10;", { - options: { compilerOptions: { newLine: ts.NewLineKind.LineFeed, module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("transpile file as 'tsx' if 'jsx' is specified", `var x =
`, { + options: { compilerOptions: { jsx: ts.JsxEmit.React, newLine: ts.NewLineKind.LineFeed } } + }); - transpilesCorrectly("Supports urls in file name", "var x", { - options: { fileName: "http://somewhere/directory//directory2/file.ts" } - }); + transpilesCorrectly("transpile .js files", "const a = 10;", { + options: { compilerOptions: { newLine: ts.NewLineKind.LineFeed, module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Accepts string as enum values for compile-options", "export const x = 0", { - options: { - compilerOptions: { - module: "es6" as any as ts.ModuleKind, - // Capitalization and spaces ignored - target: " Es6 " as any as ts.ScriptTarget - } + 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" as any as ts.ModuleKind, + // Capitalization and spaces ignored + target: " Es6 " as any as ts.ScriptTarget } - }); + } + }); - transpilesCorrectly("Report an error when compiler-options module-kind is out-of-range", "", { - options: { compilerOptions: { module: 123 as any as ts.ModuleKind } } - }); + transpilesCorrectly("Report an error when compiler-options module-kind is out-of-range", "", { + options: { compilerOptions: { module: 123 as any as ts.ModuleKind } } + }); - transpilesCorrectly("Report an error when compiler-options target-script is out-of-range", "", { - options: { compilerOptions: { module: 123 as any as ts.ModuleKind } } - }); + transpilesCorrectly("Report an error when compiler-options target-script is out-of-range", "", { + options: { compilerOptions: { module: 123 as any as ts.ModuleKind } } + }); - transpilesCorrectly("Support options with lib values", "const a = 10;", { - options: { compilerOptions: { lib: ["es6", "dom"], module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Support options with lib values", "const a = 10;", { + options: { compilerOptions: { lib: ["es6", "dom"], module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Support options with types values", "const a = 10;", { - options: { compilerOptions: { types: ["jquery", "typescript"], module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Support options with types values", "const a = 10;", { + options: { compilerOptions: { types: ["jquery", "typescript"], module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'allowJs'", "x;", { - options: { compilerOptions: { allowJs: true }, 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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 'locale'", "x;", { + options: { compilerOptions: { locale: "en-us" }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'module'", "x;", { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Supports setting 'module'", "x;", { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'moduleResolution'", "x;", { - options: { compilerOptions: { moduleResolution: ts.ModuleResolutionKind.NodeJs }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Supports setting 'moduleResolution'", "x;", { + options: { compilerOptions: { moduleResolution: ts.ModuleResolutionKind.NodeJs }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'newLine'", "x;", { - options: { compilerOptions: { newLine: ts.NewLineKind.CarriageReturnLineFeed }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Supports setting 'newLine'", "x;", { + options: { compilerOptions: { newLine: ts.NewLineKind.CarriageReturnLineFeed }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'noEmit'", "x;", { - options: { compilerOptions: { noEmit: true }, 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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 'jsxFactory'", "x;", { + options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'jsxFragmentFactory'", "x;", { - options: { compilerOptions: { jsxFactory: "x", jsxFragmentFactory: "frag" }, fileName: "input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Supports setting 'jsxFragmentFactory'", "x;", { + options: { compilerOptions: { jsxFactory: "x", jsxFragmentFactory: "frag" }, fileName: "input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'removeComments'", "x;", { - options: { compilerOptions: { removeComments: true }, 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: "./rootDir/input.js", reportDiagnostics: true } - }); + transpilesCorrectly("Supports setting 'rootDir'", "x;", { + options: { compilerOptions: { rootDir: "./rootDir" }, fileName: "./rootDir/input.js", reportDiagnostics: true } + }); - transpilesCorrectly("Supports setting 'rootDirs'", "x;", { - options: { compilerOptions: { rootDirs: ["./a", "./b"] }, 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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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 '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("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: ts.ScriptTarget.ES5, - module: ts.ModuleKind.CommonJS, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - emitDecoratorMetadata: true, - experimentalDecorators: true, - isolatedModules: 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: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.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: ts.ScriptTarget.ES5, - module: ts.ModuleKind.System, - moduleResolution: ts.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: ts.ScriptTarget.ES5, + module: ts.ModuleKind.System, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + emitDecoratorMetadata: true, + experimentalDecorators: true, + isolatedModules: true, } - }); + } + }); - transpilesCorrectly("Supports readonly keyword for arrays", "let x: readonly string[];", { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + transpilesCorrectly("Supports readonly keyword for arrays", "let x: readonly string[];", { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Supports 'as const' arrays", `([] as const).forEach(k => console.log(k));`, { - options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } - }); + transpilesCorrectly("Supports 'as const' arrays", `([] as const).forEach(k => console.log(k));`, { + options: { compilerOptions: { module: ts.ModuleKind.CommonJS } } + }); - transpilesCorrectly("Infer correct file extension", `const fn = (a: T) => a`, { - noSetFileName: true - }); + transpilesCorrectly("Infer correct file extension", `const fn = (a: T) => a`, { + noSetFileName: true + }); - transpilesCorrectly("Export star as ns conflict does not crash", ` + 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 ecd6a2f3d8ed5..936bde0b490d4 100644 --- a/src/testRunner/unittests/transform.ts +++ b/src/testRunner/unittests/transform.ts @@ -1,433 +1,433 @@ namespace ts { - describe("unittests:: TransformAPI", () => { - function replaceUndefinedWithVoid0(context: ts.TransformationContext) { - const previousOnSubstituteNode = context.onSubstituteNode; - context.enableSubstitution(ts.SyntaxKind.Identifier); - context.onSubstituteNode = (hint, node) => { - node = previousOnSubstituteNode(hint, node); - if (hint === ts.EmitHint.Expression && ts.isIdentifier(node) && node.escapedText === "undefined") { - node = ts.factory.createPartiallyEmittedExpression(ts.addSyntheticTrailingComment(ts.setTextRange(ts.factory.createVoidZero(), node), ts.SyntaxKind.MultiLineCommentTrivia, "undefined")); - } - return node; - }; - return (file: ts.SourceFile) => file; - } - function replaceNumberWith2(context: ts.TransformationContext) { - function visitor(node: ts.Node): ts.Node { - if (ts.isNumericLiteral(node)) { - return ts.factory.createNumericLiteral("2"); - } - return ts.visitEachChild(node, visitor, context); +describe("unittests:: TransformAPI", () => { + function replaceUndefinedWithVoid0(context: ts.TransformationContext) { + const previousOnSubstituteNode = context.onSubstituteNode; + context.enableSubstitution(ts.SyntaxKind.Identifier); + context.onSubstituteNode = (hint, node) => { + node = previousOnSubstituteNode(hint, node); + if (hint === ts.EmitHint.Expression && ts.isIdentifier(node) && node.escapedText === "undefined") { + node = ts.factory.createPartiallyEmittedExpression(ts.addSyntheticTrailingComment(ts.setTextRange(ts.factory.createVoidZero(), node), ts.SyntaxKind.MultiLineCommentTrivia, "undefined")); } - return (file: ts.SourceFile) => ts.visitNode(file, visitor); - } - - function replaceIdentifiersNamedOldNameWithNewName(context: ts.TransformationContext) { - const previousOnSubstituteNode = context.onSubstituteNode; - context.enableSubstitution(ts.SyntaxKind.Identifier); - context.onSubstituteNode = (hint, node) => { - node = previousOnSubstituteNode(hint, node); - if (ts.isIdentifier(node) && node.escapedText === "oldName") { - node = ts.setTextRange(ts.factory.createIdentifier("newName"), node); - } - return node; - }; - return (file: ts.SourceFile) => file; - } - - function replaceIdentifiersNamedOldNameWithNewName2(context: ts.TransformationContext) { - const visitor: ts.Visitor = (node) => { - if (ts.isIdentifier(node) && node.text === "oldName") { - return ts.factory.createIdentifier("newName"); - } - return ts.visitEachChild(node, visitor, context); - }; - return (node: ts.SourceFile) => ts.visitNode(node, visitor); - } - function createTaggedTemplateLiteral(): ts.Transformer { - return sourceFile => ts.factory.updateSourceFile(sourceFile, [ - ts.factory.createExpressionStatement(ts.factory.createTaggedTemplateExpression(ts.factory.createIdentifier("$tpl"), - /*typeArguments*/ undefined, ts.factory.createNoSubstitutionTemplateLiteral("foo", "foo"))) - ]); + return node; + }; + return (file: ts.SourceFile) => file; + } + function replaceNumberWith2(context: ts.TransformationContext) { + function visitor(node: ts.Node): ts.Node { + if (ts.isNumericLiteral(node)) { + return ts.factory.createNumericLiteral("2"); + } + return ts.visitEachChild(node, visitor, context); } + return (file: ts.SourceFile) => ts.visitNode(file, visitor); + } + + function replaceIdentifiersNamedOldNameWithNewName(context: ts.TransformationContext) { + const previousOnSubstituteNode = context.onSubstituteNode; + context.enableSubstitution(ts.SyntaxKind.Identifier); + context.onSubstituteNode = (hint, node) => { + node = previousOnSubstituteNode(hint, node); + if (ts.isIdentifier(node) && node.escapedText === "oldName") { + node = ts.setTextRange(ts.factory.createIdentifier("newName"), node); + } + return node; + }; + return (file: ts.SourceFile) => file; + } + + function replaceIdentifiersNamedOldNameWithNewName2(context: ts.TransformationContext) { + const visitor: ts.Visitor = (node) => { + if (ts.isIdentifier(node) && node.text === "oldName") { + return ts.factory.createIdentifier("newName"); + } + return ts.visitEachChild(node, visitor, context); + }; + return (node: ts.SourceFile) => ts.visitNode(node, visitor); + } + function createTaggedTemplateLiteral(): ts.Transformer { + return sourceFile => ts.factory.updateSourceFile(sourceFile, [ + ts.factory.createExpressionStatement(ts.factory.createTaggedTemplateExpression(ts.factory.createIdentifier("$tpl"), + /*typeArguments*/ undefined, ts.factory.createNoSubstitutionTemplateLiteral("foo", "foo"))) + ]); + } + + function transformSourceFile(sourceText: string, transformers: ts.TransformerFactory[]) { + const transformed = ts.transform(ts.createSourceFile("source.ts", sourceText, ts.ScriptTarget.ES2015), transformers); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.CarriageReturnLineFeed }, { + onEmitNode: transformed.emitNodeWithNotification, + substituteNode: transformed.substituteNode + }); + const result = printer.printBundle(ts.factory.createBundle(transformed.transformed)); + transformed.dispose(); + return result; + } + + function testBaseline(testName: string, test: () => string) { + it(testName, () => { + Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, test()); + }); + } - function transformSourceFile(sourceText: string, transformers: ts.TransformerFactory[]) { - const transformed = ts.transform(ts.createSourceFile("source.ts", sourceText, ts.ScriptTarget.ES2015), transformers); - const printer = ts.createPrinter({ newLine: ts.NewLineKind.CarriageReturnLineFeed }, { - onEmitNode: transformed.emitNodeWithNotification, - substituteNode: transformed.substituteNode + function testBaselineAndEvaluate(testName: string, test: () => string, onEvaluate: (exports: any) => void) { + describe(testName, () => { + let sourceText!: string; + before(() => { + sourceText = test(); }); - const result = printer.printBundle(ts.factory.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", () => { + Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, sourceText); + }); + it("evaluate", () => { + onEvaluate(evaluator.evaluateJavaScript(sourceText)); }); - } - - testBaseline("substitution", () => { - return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]); }); + } - testBaseline("types", () => { - return transformSourceFile(`let a: () => void`, [ - context => file => ts.visitNode(file, function visitor(node: ts.Node): ts.VisitResult { - return ts.visitEachChild(node, visitor, context); - }) - ]); - }); + testBaseline("substitution", () => { + return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]); + }); - testBaseline("transformDefiniteAssignmentAssertions", () => { - return transformSourceFile(`let a!: () => void`, [ - context => file => ts.visitNode(file, function visitor(node: ts.Node): ts.VisitResult { - if (node.kind === ts.SyntaxKind.VoidKeyword) { - return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); - } - return ts.visitEachChild(node, visitor, context); - }) - ]); - }); + testBaseline("types", () => { + return transformSourceFile(`let a: () => void`, [ + context => file => ts.visitNode(file, function visitor(node: ts.Node): ts.VisitResult { + return ts.visitEachChild(node, visitor, context); + }) + ]); + }); - testBaseline("fromTranspileModule", () => { - return ts.transpileModule(`var oldName = undefined;`, { - transformers: { - before: [replaceUndefinedWithVoid0], - after: [replaceIdentifiersNamedOldNameWithNewName] - }, - compilerOptions: { - newLine: ts.NewLineKind.CarriageReturnLineFeed + testBaseline("transformDefiniteAssignmentAssertions", () => { + return transformSourceFile(`let a!: () => void`, [ + context => file => ts.visitNode(file, function visitor(node: ts.Node): ts.VisitResult { + if (node.kind === ts.SyntaxKind.VoidKeyword) { + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); } - }).outputText; - }); + return ts.visitEachChild(node, visitor, context); + }) + ]); + }); - testBaseline("transformTaggedTemplateLiteral", () => { - return ts.transpileModule("", { - transformers: { - before: [createTaggedTemplateLiteral], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed - } - }).outputText; - }); + testBaseline("fromTranspileModule", () => { + return ts.transpileModule(`var oldName = undefined;`, { + transformers: { + before: [replaceUndefinedWithVoid0], + after: [replaceIdentifiersNamedOldNameWithNewName] + }, + compilerOptions: { + newLine: ts.NewLineKind.CarriageReturnLineFeed + } + }).outputText; + }); - testBaseline("issue27854", () => { - return ts.transpileModule(`oldName<{ a: string; }>\` ... \`;`, { - transformers: { - before: [replaceIdentifiersNamedOldNameWithNewName2] - }, - compilerOptions: { - newLine: ts.NewLineKind.CarriageReturnLineFeed, - target: ts.ScriptTarget.Latest - } - }).outputText; - }); + testBaseline("transformTaggedTemplateLiteral", () => { + return ts.transpileModule("", { + transformers: { + before: [createTaggedTemplateLiteral], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed + } + }).outputText; + }); - testBaseline("issue44068", () => { - return transformSourceFile(` + testBaseline("issue27854", () => { + return ts.transpileModule(`oldName<{ a: string; }>\` ... \`;`, { + transformers: { + before: [replaceIdentifiersNamedOldNameWithNewName2] + }, + compilerOptions: { + newLine: ts.NewLineKind.CarriageReturnLineFeed, + target: ts.ScriptTarget.Latest + } + }).outputText; + }); + + testBaseline("issue44068", () => { + return transformSourceFile(` const FirstVar = null; const SecondVar = null; `, [ - context => file => { - const firstVarName = (file.statements[0] as ts.VariableStatement) - .declarationList.declarations[0].name as ts.Identifier; - const secondVarName = (file.statements[0] as ts.VariableStatement) - .declarationList.declarations[0].name as ts.Identifier; - - return context.factory.updateSourceFile(file, file.statements.concat([ - context.factory.createExpressionStatement(context.factory.createArrayLiteralExpression([firstVarName, secondVarName])), - ])); - } - ]); - }); + context => file => { + const firstVarName = (file.statements[0] as ts.VariableStatement) + .declarationList.declarations[0].name as ts.Identifier; + const secondVarName = (file.statements[0] as ts.VariableStatement) + .declarationList.declarations[0].name as ts.Identifier; + + return context.factory.updateSourceFile(file, file.statements.concat([ + context.factory.createExpressionStatement(context.factory.createArrayLiteralExpression([firstVarName, secondVarName])), + ])); + } + ]); + }); - testBaseline("rewrittenNamespace", () => { - return ts.transpileModule(`namespace Reflect { const x = 1; }`, { - transformers: { - before: [forceNamespaceRewrite], - }, - compilerOptions: { - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + testBaseline("rewrittenNamespace", () => { + return ts.transpileModule(`namespace Reflect { const x = 1; }`, { + transformers: { + before: [forceNamespaceRewrite], + }, + compilerOptions: { + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - testBaseline("rewrittenNamespaceFollowingClass", () => { - return ts.transpileModule(` + testBaseline("rewrittenNamespaceFollowingClass", () => { + return ts.transpileModule(` class C { foo = 10; static bar = 20 } namespace C { export let x = 10; } `, { - transformers: { - before: [forceNamespaceRewrite], - }, - compilerOptions: { - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - useDefineForClassFields: false, - } - }).outputText; - }); + transformers: { + before: [forceNamespaceRewrite], + }, + compilerOptions: { + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + useDefineForClassFields: false, + } + }).outputText; + }); - testBaseline("transformTypesInExportDefault", () => { - return ts.transpileModule(` + testBaseline("transformTypesInExportDefault", () => { + return ts.transpileModule(` export default (foo: string) => { return 1; } `, { - transformers: { - before: [replaceNumberWith2], - }, - compilerOptions: { - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [replaceNumberWith2], + }, + compilerOptions: { + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + + testBaseline("synthesizedClassAndNamespaceCombination", () => { + return ts.transpileModule("", { + transformers: { + before: [replaceWithClassAndNamespace], + }, + compilerOptions: { + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + + function replaceWithClassAndNamespace() { + return (sourceFile: ts.SourceFile) => { + // TODO(rbuckton): Does this need to be parented? + const result = ts.factory.updateSourceFile(sourceFile, ts.factory.createNodeArray([ + ts.factory.createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ undefined!), + ts.factory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createIdentifier("Foo"), ts.factory.createModuleBlock([ts.factory.createEmptyStatement()])) + ])); + return result; + }; + } + }); - testBaseline("synthesizedClassAndNamespaceCombination", () => { - return ts.transpileModule("", { - transformers: { - before: [replaceWithClassAndNamespace], - }, - compilerOptions: { - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.CarriageReturnLineFeed, + function forceNamespaceRewrite(context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return visitNode(sourceFile); + + function visitNode(node: T): T { + if (node.kind === ts.SyntaxKind.ModuleBlock) { + const block = node as T & ts.ModuleBlock; + const statements = ts.factory.createNodeArray([...block.statements]); + return ts.factory.updateModuleBlock(block, statements) as typeof block; } - }).outputText; - - function replaceWithClassAndNamespace() { - return (sourceFile: ts.SourceFile) => { - // TODO(rbuckton): Does this need to be parented? - const result = ts.factory.updateSourceFile(sourceFile, ts.factory.createNodeArray([ - ts.factory.createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ undefined!), - ts.factory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, ts.factory.createIdentifier("Foo"), ts.factory.createModuleBlock([ts.factory.createEmptyStatement()])) - ])); - return result; - }; + return ts.visitEachChild(node, visitNode, context); } - }); + }; + } + + testBaseline("transformAwayExportStar", () => { + return ts.transpileModule("export * from './helper';", { + transformers: { + before: [expandExportStar], + }, + compilerOptions: { + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; - function forceNamespaceRewrite(context: ts.TransformationContext) { + function expandExportStar(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile): ts.SourceFile => { return visitNode(sourceFile); function visitNode(node: T): T { - if (node.kind === ts.SyntaxKind.ModuleBlock) { - const block = node as T & ts.ModuleBlock; - const statements = ts.factory.createNodeArray([...block.statements]); - return ts.factory.updateModuleBlock(block, statements) as typeof block; + if (node.kind === ts.SyntaxKind.ExportDeclaration) { + const ed = node as ts.Node as ts.ExportDeclaration; + const exports = [{ name: "x" }]; + const exportSpecifiers = exports.map(e => ts.factory.createExportSpecifier(/*isTypeOnly*/ false, e.name, e.name)); + const exportClause = ts.factory.createNamedExports(exportSpecifiers); + const newEd = ts.factory.updateExportDeclaration(ed, ed.decorators, ed.modifiers, ed.isTypeOnly, exportClause, ed.moduleSpecifier, ed.assertClause); + return newEd as ts.Node as T; } return ts.visitEachChild(node, visitNode, context); } }; } + }); - testBaseline("transformAwayExportStar", () => { - return ts.transpileModule("export * from './helper';", { - transformers: { - before: [expandExportStar], - }, - compilerOptions: { - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function expandExportStar(context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return visitNode(sourceFile); - - function visitNode(node: T): T { - if (node.kind === ts.SyntaxKind.ExportDeclaration) { - const ed = node as ts.Node as ts.ExportDeclaration; - const exports = [{ name: "x" }]; - const exportSpecifiers = exports.map(e => ts.factory.createExportSpecifier(/*isTypeOnly*/ false, e.name, e.name)); - const exportClause = ts.factory.createNamedExports(exportSpecifiers); - const newEd = ts.factory.updateExportDeclaration(ed, ed.decorators, ed.modifiers, ed.isTypeOnly, exportClause, ed.moduleSpecifier, ed.assertClause); - return newEd as ts.Node as T; - } - return ts.visitEachChild(node, visitNode, context); - } - }; + // https://github.com/Microsoft/TypeScript/issues/19618 + testBaseline("transformAddImportStar", () => { + return ts.transpileModule("", { + transformers: { + before: [transformAddImportStar], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.System, + newLine: ts.NewLineKind.CarriageReturnLineFeed, } - }); + }).outputText; - // https://github.com/Microsoft/TypeScript/issues/19618 - testBaseline("transformAddImportStar", () => { - return ts.transpileModule("", { - transformers: { - before: [transformAddImportStar], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.System, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function transformAddImportStar(_context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: ts.SourceFile) { - // produce `import * as i0 from './comp'; - const importStar = ts.factory.createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*importClause*/ ts.factory.createImportClause( - /*isTypeOnly*/ false, - /*name*/ undefined, ts.factory.createNamespaceImport(ts.factory.createIdentifier("i0"))), - /*moduleSpecifier*/ ts.factory.createStringLiteral("./comp1"), - /*assertClause*/ undefined); - return ts.factory.updateSourceFile(sf, [importStar]); - } + function transformAddImportStar(_context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return visitNode(sourceFile); + }; + function visitNode(sf: ts.SourceFile) { + // produce `import * as i0 from './comp'; + const importStar = ts.factory.createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*importClause*/ ts.factory.createImportClause( + /*isTypeOnly*/ false, + /*name*/ undefined, ts.factory.createNamespaceImport(ts.factory.createIdentifier("i0"))), + /*moduleSpecifier*/ ts.factory.createStringLiteral("./comp1"), + /*assertClause*/ undefined); + return ts.factory.updateSourceFile(sf, [importStar]); } - }); + } + }); - // https://github.com/Microsoft/TypeScript/issues/17384 - testBaseline("transformAddDecoratedNode", () => { - return ts.transpileModule("", { - transformers: { - before: [transformAddDecoratedNode], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; + // https://github.com/Microsoft/TypeScript/issues/17384 + testBaseline("transformAddDecoratedNode", () => { + return ts.transpileModule("", { + transformers: { + before: [transformAddDecoratedNode], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; - function transformAddDecoratedNode(_context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: ts.SourceFile) { - // produce `class Foo { @Bar baz() {} }`; - const classDecl = ts.factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - ts.factory.createMethodDeclaration([ts.factory.createDecorator(ts.factory.createIdentifier("Bar"))], [], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, ts.factory.createBlock([])) - ]); - return ts.factory.updateSourceFile(sf, [classDecl]); - } + function transformAddDecoratedNode(_context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return visitNode(sourceFile); + }; + function visitNode(sf: ts.SourceFile) { + // produce `class Foo { @Bar baz() {} }`; + const classDecl = ts.factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + ts.factory.createMethodDeclaration([ts.factory.createDecorator(ts.factory.createIdentifier("Bar"))], [], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, ts.factory.createBlock([])) + ]); + return ts.factory.updateSourceFile(sf, [classDecl]); } - }); + } + }); - testBaseline("transformDeclarationFile", () => { - return baselineDeclarationTransform(`var oldName = undefined;`, { - transformers: { - afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] - }, - compilerOptions: { - newLine: ts.NewLineKind.CarriageReturnLineFeed, - declaration: true - } - }); + testBaseline("transformDeclarationFile", () => { + return baselineDeclarationTransform(`var oldName = undefined;`, { + transformers: { + afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] + }, + compilerOptions: { + newLine: ts.NewLineKind.CarriageReturnLineFeed, + declaration: true + } }); + }); - // https://github.com/microsoft/TypeScript/issues/33295 - testBaseline("transformParameterProperty", () => { - return ts.transpileModule("", { - transformers: { - before: [transformAddParameterProperty], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function transformAddParameterProperty(_context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: ts.SourceFile) { - // produce `class Foo { constructor(@Dec private x) {} }`; - // The decorator is required to trigger ts.ts transformations. - const classDecl = ts.factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - ts.factory.createConstructorDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, [ - ts.factory.createParameterDeclaration(/*decorators*/ [ts.factory.createDecorator(ts.factory.createIdentifier("Dec"))], /*modifiers*/ [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x") - ], ts.factory.createBlock([])) - ]); - return ts.factory.updateSourceFile(sf, [classDecl]); - } + // https://github.com/microsoft/TypeScript/issues/33295 + testBaseline("transformParameterProperty", () => { + return ts.transpileModule("", { + transformers: { + before: [transformAddParameterProperty], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed, } - }); + }).outputText; - function baselineDeclarationTransform(text: string, opts: ts.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 = ts.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 transformAddParameterProperty(_context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return visitNode(sourceFile); + }; + function visitNode(sf: ts.SourceFile) { + // produce `class Foo { constructor(@Dec private x) {} }`; + // The decorator is required to trigger ts.ts transformations. + const classDecl = ts.factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + ts.factory.createConstructorDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, [ + ts.factory.createParameterDeclaration(/*decorators*/ [ts.factory.createDecorator(ts.factory.createIdentifier("Dec"))], /*modifiers*/ [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x") + ], ts.factory.createBlock([])) + ]); + return ts.factory.updateSourceFile(sf, [classDecl]); + } } + }); - function addSyntheticComment(nodeFilter: (node: ts.Node) => boolean) { - return (context: ts.TransformationContext) => { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); - }; - function rootTransform(node: T): ts.VisitResult { - if (nodeFilter(node)) { - ts.setEmitFlags(node, ts.EmitFlags.NoLeadingComments); - ts.setSyntheticLeadingComments(node, [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); - } - return ts.visitEachChild(node, rootTransform, context); - } + function baselineDeclarationTransform(text: string, opts: ts.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 = ts.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: ts.Node) => boolean) { + return (context: ts.TransformationContext) => { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); }; - } + function rootTransform(node: T): ts.VisitResult { + if (nodeFilter(node)) { + ts.setEmitFlags(node, ts.EmitFlags.NoLeadingComments); + ts.setSyntheticLeadingComments(node, [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); + } + return ts.visitEachChild(node, rootTransform, context); + } + }; + } - // https://github.com/Microsoft/TypeScript/issues/24096 - testBaseline("transformAddCommentToArrowReturnValue", () => { - return ts.transpileModule(`const foo = () => + // https://github.com/Microsoft/TypeScript/issues/24096 + testBaseline("transformAddCommentToArrowReturnValue", () => { + return ts.transpileModule(`const foo = () => void 0 `, { - transformers: { - before: [addSyntheticComment(ts.isVoidExpression)], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addSyntheticComment(ts.isVoidExpression)], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToExportedVar", () => { - return ts.transpileModule(`export const exportedDirectly = 1; + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToExportedVar", () => { + return ts.transpileModule(`export const exportedDirectly = 1; const exportedSeparately = 2; export {exportedSeparately}; `, { - transformers: { - before: [addSyntheticComment(ts.isVariableStatement)], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addSyntheticComment(ts.isVariableStatement)], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToImport", () => { - return ts.transpileModule(` + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToImport", () => { + return ts.transpileModule(` // Previous comment on import. import {Value} from 'somewhere'; import * as X from 'somewhere'; @@ -436,19 +436,19 @@ export { /* specifier comment */ X, Y} from 'somewhere'; export * from 'somewhere'; export {Value}; `, { - transformers: { - before: [addSyntheticComment(n => ts.isImportDeclaration(n) || ts.isExportDeclaration(n) || ts.isImportSpecifier(n) || ts.isExportSpecifier(n))], - }, - compilerOptions: { - target: ts.ScriptTarget.ES5, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addSyntheticComment(n => ts.isImportDeclaration(n) || ts.isExportDeclaration(n) || ts.isImportSpecifier(n) || ts.isExportSpecifier(n))], + }, + compilerOptions: { + target: ts.ScriptTarget.ES5, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToProperties", () => { - return ts.transpileModule(` + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToProperties", () => { + return ts.transpileModule(` // class comment. class Clazz { // original comment 1. @@ -459,18 +459,18 @@ class Clazz { constructor(readonly field = 1) {} } `, { - transformers: { - before: [addSyntheticComment(n => ts.isPropertyDeclaration(n) || ts.isParameterPropertyDeclaration(n, n.parent) || ts.isClassDeclaration(n) || ts.isConstructorDeclaration(n))], - }, - compilerOptions: { - target: ts.ScriptTarget.ES2015, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addSyntheticComment(n => ts.isPropertyDeclaration(n) || ts.isParameterPropertyDeclaration(n, n.parent) || ts.isClassDeclaration(n) || ts.isConstructorDeclaration(n))], + }, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - testBaseline("transformAddCommentToNamespace", () => { - return ts.transpileModule(` + testBaseline("transformAddCommentToNamespace", () => { + return ts.transpileModule(` // namespace comment. namespace Foo { export const x = 1; @@ -480,149 +480,149 @@ namespace Foo { export const y = 1; } `, { - transformers: { - before: [addSyntheticComment(n => ts.isModuleDeclaration(n))], - }, - compilerOptions: { - target: ts.ScriptTarget.ES2015, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addSyntheticComment(n => ts.isModuleDeclaration(n))], + }, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - testBaseline("transformUpdateModuleMember", () => { - return ts.transpileModule(` + testBaseline("transformUpdateModuleMember", () => { + return ts.transpileModule(` module MyModule { const myVariable = 1; function foo(param: string) {} } `, { - transformers: { - before: [renameVariable], - }, - compilerOptions: { - target: ts.ScriptTarget.ES2015, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function renameVariable(context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); - }; - function rootTransform(node: T): ts.Node { - if (ts.isVariableDeclaration(node)) { - return ts.factory.updateVariableDeclaration(node, ts.factory.createIdentifier("newName"), /*exclamationToken*/ undefined, /*type*/ undefined, node.initializer); - } - return ts.visitEachChild(node, rootTransform, context); - } + transformers: { + before: [renameVariable], + }, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + newLine: ts.NewLineKind.CarriageReturnLineFeed, } - }); + }).outputText; - // https://github.com/Microsoft/TypeScript/issues/24709 - testBaseline("issue24709", () => { - const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true); - const transformed = ts.transform(ts.createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ts.ScriptTarget.ES3), [transformSourceFile]); - const transformedSourceFile = transformed.transformed[0]; - transformed.dispose(); - const host = new fakes.CompilerHost(fs); - host.getSourceFile = () => transformedSourceFile; - const program = ts.createProgram(["source.ts"], { - target: ts.ScriptTarget.ES3, - module: ts.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: ts.TransformationContext) { - const visitor: ts.Visitor = (node) => { - if (ts.isMethodDeclaration(node)) { - return ts.factory.updateMethodDeclaration(node, node.decorators, node.modifiers, node.asteriskToken, ts.factory.createIdentifier("foobar"), node.questionToken, node.typeParameters, node.parameters, node.type, node.body); - } - return ts.visitEachChild(node, visitor, context); + function renameVariable(context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); }; - return (node: ts.SourceFile) => ts.visitNode(node, visitor); - } - - }); - - testBaselineAndEvaluate("templateSpans", () => { - return ts.transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { - compilerOptions: { - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - }, - transformers: { - before: [transformSourceFile] + function rootTransform(node: T): ts.Node { + if (ts.isVariableDeclaration(node)) { + return ts.factory.updateVariableDeclaration(node, ts.factory.createIdentifier("newName"), /*exclamationToken*/ undefined, /*type*/ undefined, node.initializer); + } + return ts.visitEachChild(node, rootTransform, context); } - }).outputText; + } + }); - function transformSourceFile(context: ts.TransformationContext): ts.Transformer { - function visitor(node: ts.Node): ts.VisitResult { - if (ts.isNoSubstitutionTemplateLiteral(node)) { - return ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText); - } - else { - return ts.visitEachChild(node, visitor, context); - } + // https://github.com/Microsoft/TypeScript/issues/24709 + testBaseline("issue24709", () => { + const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true); + const transformed = ts.transform(ts.createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ts.ScriptTarget.ES3), [transformSourceFile]); + const transformedSourceFile = transformed.transformed[0]; + transformed.dispose(); + const host = new fakes.CompilerHost(fs); + host.getSourceFile = () => transformedSourceFile; + const program = ts.createProgram(["source.ts"], { + target: ts.ScriptTarget.ES3, + module: ts.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: ts.TransformationContext) { + const visitor: ts.Visitor = (node) => { + if (ts.isMethodDeclaration(node)) { + return ts.factory.updateMethodDeclaration(node, node.decorators, node.modifiers, node.asteriskToken, ts.factory.createIdentifier("foobar"), node.questionToken, node.typeParameters, node.parameters, node.type, node.body); } - return sourceFile => ts.visitNode(sourceFile, visitor, ts.isSourceFile); + return ts.visitEachChild(node, visitor, context); + }; + return (node: ts.SourceFile) => ts.visitNode(node, visitor); + } + + }); + + testBaselineAndEvaluate("templateSpans", () => { + return ts.transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { + compilerOptions: { + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + }, + transformers: { + before: [transformSourceFile] } - }, exports => { - assert.equal(exports.stringLength, 5); - }); + }).outputText; - function addStaticFieldWithComment(context: ts.TransformationContext) { - return (sourceFile: ts.SourceFile): ts.SourceFile => { - return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); - }; - function rootTransform(node: T): ts.Node { - if (ts.isClassLike(node)) { - const newMembers = [ts.factory.createPropertyDeclaration(/* decorators */ undefined, [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], "newField", /* questionOrExclamationToken */ undefined, /* type */ undefined, ts.factory.createStringLiteral("x"))]; - ts.setSyntheticLeadingComments(newMembers[0], [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); - return ts.isClassDeclaration(node) ? - ts.factory.updateClassDeclaration(node, node.decorators, - /* modifierFlags */ undefined, node.name, node.typeParameters, node.heritageClauses, newMembers) : - ts.factory.updateClassExpression(node, node.decorators, - /* modifierFlags */ undefined, node.name, node.typeParameters, node.heritageClauses, newMembers); + function transformSourceFile(context: ts.TransformationContext): ts.Transformer { + function visitor(node: ts.Node): ts.VisitResult { + if (ts.isNoSubstitutionTemplateLiteral(node)) { + return ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText); } - return ts.visitEachChild(node, rootTransform, context); + else { + return ts.visitEachChild(node, visitor, context); + } + } + return sourceFile => ts.visitNode(sourceFile, visitor, ts.isSourceFile); + } + }, exports => { + assert.equal(exports.stringLength, 5); + }); + + function addStaticFieldWithComment(context: ts.TransformationContext) { + return (sourceFile: ts.SourceFile): ts.SourceFile => { + return ts.visitNode(sourceFile, rootTransform, ts.isSourceFile); + }; + function rootTransform(node: T): ts.Node { + if (ts.isClassLike(node)) { + const newMembers = [ts.factory.createPropertyDeclaration(/* decorators */ undefined, [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], "newField", /* questionOrExclamationToken */ undefined, /* type */ undefined, ts.factory.createStringLiteral("x"))]; + ts.setSyntheticLeadingComments(newMembers[0], [{ kind: ts.SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); + return ts.isClassDeclaration(node) ? + ts.factory.updateClassDeclaration(node, node.decorators, + /* modifierFlags */ undefined, node.name, node.typeParameters, node.heritageClauses, newMembers) : + ts.factory.updateClassExpression(node, node.decorators, + /* modifierFlags */ undefined, node.name, node.typeParameters, node.heritageClauses, newMembers); } + return ts.visitEachChild(node, rootTransform, context); } + } - testBaseline("transformSyntheticCommentOnStaticFieldInClassDeclaration", () => { - return ts.transpileModule(` + testBaseline("transformSyntheticCommentOnStaticFieldInClassDeclaration", () => { + return ts.transpileModule(` declare const Decorator: any; @Decorator class MyClass { } `, { - transformers: { - before: [addStaticFieldWithComment], - }, - compilerOptions: { - target: ts.ScriptTarget.ES2015, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); + transformers: { + before: [addStaticFieldWithComment], + }, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); - testBaseline("transformSyntheticCommentOnStaticFieldInClassExpression", () => { - return ts.transpileModule(` + testBaseline("transformSyntheticCommentOnStaticFieldInClassExpression", () => { + return ts.transpileModule(` const MyClass = class { }; `, { - transformers: { - before: [addStaticFieldWithComment], - }, - compilerOptions: { - target: ts.ScriptTarget.ES2015, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - + transformers: { + before: [addStaticFieldWithComment], + }, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + newLine: ts.NewLineKind.CarriageReturnLineFeed, + } + }).outputText; }); + +}); } diff --git a/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts b/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts index 51c88a274af10..c51fd527baee6 100644 --- a/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts +++ b/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts @@ -1,108 +1,108 @@ namespace ts { - describe("unittests:: tsbuild:: outFile:: on amd modules with --out", () => { - let outFileFs: vfs.FileSystem; - before(() => { - outFileFs = ts.loadProjectFromDisk("tests/projects/amdModulesWithOut"); - }); - after(() => { - outFileFs = undefined!; - }); +describe("unittests:: tsbuild:: outFile:: on amd modules with --out", () => { + let outFileFs: vfs.FileSystem; + before(() => { + outFileFs = ts.loadProjectFromDisk("tests/projects/amdModulesWithOut"); + }); + after(() => { + outFileFs = undefined!; + }); - interface VerifyOutFileScenarioInput { - subScenario: string; - modifyFs?: (fs: vfs.FileSystem) => void; - modifyAgainFs?: (fs: vfs.FileSystem) => void; - } + interface VerifyOutFileScenarioInput { + subScenario: string; + modifyFs?: (fs: vfs.FileSystem) => void; + modifyAgainFs?: (fs: vfs.FileSystem) => void; + } - function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs }: VerifyOutFileScenarioInput) { - ts.verifyTscWithEdits({ - scenario: "amdModulesWithOut", - subScenario, - fs: () => outFileFs, - commandLineArgs: ["--b", "/src/app", "--verbose"], - baselineSourceMap: true, - modifyFs, - edits: [ - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.appendText(fs, "/src/lib/file1.ts", "console.log(x);") - }, - ...(modifyAgainFs ? [{ - subScenario: "incremental-headers-change-without-dts-changes", - modifyFs: modifyAgainFs - }] : ts.emptyArray), - ] - }); - } + function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs }: VerifyOutFileScenarioInput) { + ts.verifyTscWithEdits({ + scenario: "amdModulesWithOut", + subScenario, + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/app", "--verbose"], + baselineSourceMap: true, + modifyFs, + edits: [ + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.appendText(fs, "/src/lib/file1.ts", "console.log(x);") + }, + ...(modifyAgainFs ? [{ + subScenario: "incremental-headers-change-without-dts-changes", + modifyFs: modifyAgainFs + }] : ts.emptyArray), + ] + }); + } - describe("Prepend output with .tsbuildinfo", () => { - verifyOutFileScenario({ - subScenario: "modules and globals mixed in amd", - }); + describe("Prepend output with .tsbuildinfo", () => { + verifyOutFileScenario({ + subScenario: "modules and globals mixed in amd", + }); - // Prologues - describe("Prologues", () => { - verifyOutFileScenario({ - subScenario: "multiple prologues in all projects", - modifyFs: fs => { - ts.enableStrict(fs, "/src/lib/tsconfig.json"); - ts.addTestPrologue(fs, "/src/lib/file0.ts", `"myPrologue"`); - ts.addTestPrologue(fs, "/src/lib/file2.ts", `"myPrologueFile"`); - ts.addTestPrologue(fs, "/src/lib/global.ts", `"myPrologue3"`); - ts.enableStrict(fs, "/src/app/tsconfig.json"); - ts.addTestPrologue(fs, "/src/app/file3.ts", `"myPrologue"`); - ts.addTestPrologue(fs, "/src/app/file4.ts", `"myPrologue2";`); - }, - modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/lib/file1.ts", `"myPrologue5"`) - }); + // Prologues + describe("Prologues", () => { + verifyOutFileScenario({ + subScenario: "multiple prologues in all projects", + modifyFs: fs => { + ts.enableStrict(fs, "/src/lib/tsconfig.json"); + ts.addTestPrologue(fs, "/src/lib/file0.ts", `"myPrologue"`); + ts.addTestPrologue(fs, "/src/lib/file2.ts", `"myPrologueFile"`); + ts.addTestPrologue(fs, "/src/lib/global.ts", `"myPrologue3"`); + ts.enableStrict(fs, "/src/app/tsconfig.json"); + ts.addTestPrologue(fs, "/src/app/file3.ts", `"myPrologue"`); + ts.addTestPrologue(fs, "/src/app/file4.ts", `"myPrologue2";`); + }, + modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/lib/file1.ts", `"myPrologue5"`) }); + }); - // Shebang - describe("Shebang", () => { - // changes declaration because its emitted in .d.ts file - verifyOutFileScenario({ - subScenario: "shebang in all projects", - modifyFs: fs => { - ts.addShebang(fs, "lib", "file0"); - ts.addShebang(fs, "lib", "file1"); - ts.addShebang(fs, "app", "file3"); - }, - }); + // Shebang + describe("Shebang", () => { + // changes declaration because its emitted in .d.ts file + verifyOutFileScenario({ + subScenario: "shebang in all projects", + modifyFs: fs => { + ts.addShebang(fs, "lib", "file0"); + ts.addShebang(fs, "lib", "file1"); + ts.addShebang(fs, "app", "file3"); + }, }); + }); - // emitHelpers - describe("emitHelpers", () => { - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in all projects", - modifyFs: fs => { - ts.addSpread(fs, "lib", "file0"); - ts.addRest(fs, "lib", "file1"); - ts.addRest(fs, "app", "file3"); - ts.addSpread(fs, "app", "file4"); - }, - modifyAgainFs: fs => ts.removeRest(fs, "lib", "file1") - }); + // emitHelpers + describe("emitHelpers", () => { + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in all projects", + modifyFs: fs => { + ts.addSpread(fs, "lib", "file0"); + ts.addRest(fs, "lib", "file1"); + ts.addRest(fs, "app", "file3"); + ts.addSpread(fs, "app", "file4"); + }, + modifyAgainFs: fs => ts.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 => { - ts.addTripleSlashRef(fs, "lib", "file0"); - ts.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 => { + ts.addTripleSlashRef(fs, "lib", "file0"); + ts.addTripleSlashRef(fs, "app", "file4"); + } }); + }); - describe("stripInternal", () => { - function stripInternalScenario(fs: vfs.FileSystem) { - const internal = "/*@internal*/"; - ts.replaceText(fs, "/src/app/tsconfig.json", `"composite": true,`, `"composite": true, + describe("stripInternal", () => { + function stripInternalScenario(fs: vfs.FileSystem) { + const internal = "/*@internal*/"; + ts.replaceText(fs, "/src/app/tsconfig.json", `"composite": true,`, `"composite": true, "stripInternal": true,`); - ts.replaceText(fs, "/src/lib/file0.ts", "const", `${internal} const`); - ts.appendText(fs, "/src/lib/file1.ts", ` + ts.replaceText(fs, "/src/lib/file0.ts", "const", `${internal} const`); + ts.appendText(fs, "/src/lib/file1.ts", ` export class normalC { ${internal} constructor() { } ${internal} prop: string; @@ -128,33 +128,33 @@ ${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 => ts.replaceText(fs, "/src/lib/file1.ts", `export const`, `/*@internal*/ export const`), - }); + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "stripInternal", + modifyFs: stripInternalScenario, + modifyAgainFs: fs => ts.replaceText(fs, "/src/lib/file1.ts", `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 - ts.replaceText(fs, "/src/lib/tsconfig.json", `"outFile": "module.js"`, `"outFile": "../module.js", "rootDir": "../"`); - // Change reference to file1 module to resolve to lib/file1 - ts.replaceText(fs, "/src/app/file3.ts", "file1", "lib/file1"); - } + describe("when the module resolution finds original source file", () => { + function modifyFs(fs: vfs.FileSystem) { + // Make lib to output to parent dir + ts.replaceText(fs, "/src/lib/tsconfig.json", `"outFile": "module.js"`, `"outFile": "../module.js", "rootDir": "../"`); + // Change reference to file1 module to resolve to lib/file1 + ts.replaceText(fs, "/src/app/file3.ts", "file1", "lib/file1"); + } - ts.verifyTsc({ - scenario: "amdModulesWithOut", - subScenario: "when the module resolution finds original source file", - fs: () => outFileFs, - commandLineArgs: ["-b", "/src/app", "--verbose"], - modifyFs, - baselineSourceMap: true, - }); + ts.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/clean.ts b/src/testRunner/unittests/tsbuild/clean.ts index 809cf34122345..4df494df5b2c3 100644 --- a/src/testRunner/unittests/tsbuild/clean.ts +++ b/src/testRunner/unittests/tsbuild/clean.ts @@ -1,16 +1,16 @@ namespace ts { - describe("unittests:: tsbuild - clean", () => { - ts.verifyTsc({ - scenario: "clean", - subScenario: `file name and output name clashing`, - commandLineArgs: ["--b", "/src/tsconfig.json", "-clean"], - fs: () => ts.loadProjectFromFiles({ - "/src/index.js": "", - "/src/bar.ts": "", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { allowJs: true }, - }), +describe("unittests:: tsbuild - clean", () => { + ts.verifyTsc({ + scenario: "clean", + subScenario: `file name and output name clashing`, + commandLineArgs: ["--b", "/src/tsconfig.json", "-clean"], + fs: () => ts.loadProjectFromFiles({ + "/src/index.js": "", + "/src/bar.ts": "", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { allowJs: true }, }), - }); + }), }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuild/configFileErrors.ts b/src/testRunner/unittests/tsbuild/configFileErrors.ts index 60e1c1c5c815d..5455502acbd3d 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", () => { - ts.verifyTsc({ - scenario: "configFileErrors", - subScenario: "when tsconfig extends the missing file", - fs: () => ts.loadProjectFromDisk("tests/projects/missingExtendedConfig"), - commandLineArgs: ["--b", "/src/tsconfig.json"], - }); +describe("unittests:: tsbuild:: configFileErrors:: when tsconfig extends the missing file", () => { + ts.verifyTsc({ + scenario: "configFileErrors", + subScenario: "when tsconfig extends the missing file", + fs: () => ts.loadProjectFromDisk("tests/projects/missingExtendedConfig"), + commandLineArgs: ["--b", "/src/tsconfig.json"], }); +}); - describe("unittests:: tsbuild:: configFileErrors:: reports syntax errors in config file", () => { - ts.verifyTscWithEdits({ - scenario: "configFileErrors", - subScenario: "reports syntax errors in config file", - fs: () => ts.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", () => { + ts.verifyTscWithEdits({ + scenario: "configFileErrors", + subScenario: "reports syntax errors in config file", + fs: () => ts.loadProjectFromFiles({ + "/src/a.ts": "export function foo() { }", + "/src/b.ts": "export function bar() { }", + "/src/tsconfig.json": Utils.dedent` { "compilerOptions": { "composite": true, @@ -25,27 +25,27 @@ namespace ts { "b.ts" ] }` - }), - commandLineArgs: ["--b", "/src/tsconfig.json"], - edits: [ - { - modifyFs: fs => ts.replaceText(fs, "/src/tsconfig.json", ",", `, + }), + commandLineArgs: ["--b", "/src/tsconfig.json"], + edits: [ + { + modifyFs: fs => ts.replaceText(fs, "/src/tsconfig.json", ",", `, "declaration": true,`), - subScenario: "reports syntax errors after change to config file" - }, - { - modifyFs: fs => ts.appendText(fs, "/src/a.ts", "export function fooBar() { }"), - subScenario: "reports syntax errors after change to ts file" - }, - ts.noChangeRun, - { - 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" + }, + { + modifyFs: fs => ts.appendText(fs, "/src/a.ts", "export function fooBar() { }"), + subScenario: "reports syntax errors after change to ts file" + }, + ts.noChangeRun, + { + 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/configFileExtends.ts b/src/testRunner/unittests/tsbuild/configFileExtends.ts index 3a1431df6ddd4..955a6c5b1fc1e 100644 --- a/src/testRunner/unittests/tsbuild/configFileExtends.ts +++ b/src/testRunner/unittests/tsbuild/configFileExtends.ts @@ -1,52 +1,52 @@ namespace ts { - describe("unittests:: tsbuild:: configFileExtends:: when tsconfig extends another config", () => { - function getConfigExtendsWithIncludeFs() { - return ts.loadProjectFromFiles({ - "/src/tsconfig.json": JSON.stringify({ - references: [ - { path: "./shared/tsconfig.json" }, - { path: "./webpack/tsconfig.json" } - ], - files: [] - }), - "/src/shared/tsconfig-base.json": JSON.stringify({ - include: ["./typings-base/"] - }), - "/src/shared/typings-base/globals.d.ts": `type Unrestricted = any;`, - "/src/shared/tsconfig.json": JSON.stringify({ - extends: "./tsconfig-base.json", - compilerOptions: { - composite: true, - outDir: "../target-tsc-build/", - rootDir: ".." - }, - files: ["./index.ts"] - }), - "/src/shared/index.ts": `export const a: Unrestricted = 1;`, - "/src/webpack/tsconfig.json": JSON.stringify({ - extends: "../shared/tsconfig-base.json", - compilerOptions: { - composite: true, - outDir: "../target-tsc-build/", - rootDir: ".." - }, - files: ["./index.ts"], - references: [{ path: "../shared/tsconfig.json" }] - }), - "/src/webpack/index.ts": `export const b: Unrestricted = 1;`, - }); - } - ts.verifyTsc({ - scenario: "configFileExtends", - subScenario: "when building solution with projects extends config with include", - fs: getConfigExtendsWithIncludeFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--v", "--listFiles"], - }); - ts.verifyTsc({ - scenario: "configFileExtends", - subScenario: "when building project uses reference and both extend config with include", - fs: getConfigExtendsWithIncludeFs, - commandLineArgs: ["--b", "/src/webpack/tsconfig.json", "--v", "--listFiles"], +describe("unittests:: tsbuild:: configFileExtends:: when tsconfig extends another config", () => { + function getConfigExtendsWithIncludeFs() { + return ts.loadProjectFromFiles({ + "/src/tsconfig.json": JSON.stringify({ + references: [ + { path: "./shared/tsconfig.json" }, + { path: "./webpack/tsconfig.json" } + ], + files: [] + }), + "/src/shared/tsconfig-base.json": JSON.stringify({ + include: ["./typings-base/"] + }), + "/src/shared/typings-base/globals.d.ts": `type Unrestricted = any;`, + "/src/shared/tsconfig.json": JSON.stringify({ + extends: "./tsconfig-base.json", + compilerOptions: { + composite: true, + outDir: "../target-tsc-build/", + rootDir: ".." + }, + files: ["./index.ts"] + }), + "/src/shared/index.ts": `export const a: Unrestricted = 1;`, + "/src/webpack/tsconfig.json": JSON.stringify({ + extends: "../shared/tsconfig-base.json", + compilerOptions: { + composite: true, + outDir: "../target-tsc-build/", + rootDir: ".." + }, + files: ["./index.ts"], + references: [{ path: "../shared/tsconfig.json" }] + }), + "/src/webpack/index.ts": `export const b: Unrestricted = 1;`, }); + } + ts.verifyTsc({ + scenario: "configFileExtends", + subScenario: "when building solution with projects extends config with include", + fs: getConfigExtendsWithIncludeFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--v", "--listFiles"], + }); + ts.verifyTsc({ + scenario: "configFileExtends", + subScenario: "when building project uses reference and both extend config with include", + fs: getConfigExtendsWithIncludeFs, + commandLineArgs: ["--b", "/src/webpack/tsconfig.json", "--v", "--listFiles"], }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts b/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts index 762e87974ff77..7973b80dccd54 100644 --- a/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts +++ b/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts @@ -1,11 +1,11 @@ namespace ts { - describe("unittests:: tsbuild:: when containerOnly project is referenced", () => { - ts.verifyTscWithEdits({ - scenario: "containerOnlyReferenced", - subScenario: "verify that subsequent builds after initial build doesnt build anything", - fs: () => ts.loadProjectFromDisk("tests/projects/containerOnlyReferenced"), - commandLineArgs: ["--b", "/src", "--verbose"], - edits: ts.noChangeOnlyRuns - }); +describe("unittests:: tsbuild:: when containerOnly project is referenced", () => { + ts.verifyTscWithEdits({ + scenario: "containerOnlyReferenced", + subScenario: "verify that subsequent builds after initial build doesnt build anything", + fs: () => ts.loadProjectFromDisk("tests/projects/containerOnlyReferenced"), + commandLineArgs: ["--b", "/src", "--verbose"], + edits: ts.noChangeOnlyRuns }); +}); } diff --git a/src/testRunner/unittests/tsbuild/declarationEmit.ts b/src/testRunner/unittests/tsbuild/declarationEmit.ts index e6f4c2a4a5450..34d4ffbb440f6 100644 --- a/src/testRunner/unittests/tsbuild/declarationEmit.ts +++ b/src/testRunner/unittests/tsbuild/declarationEmit.ts @@ -1,39 +1,39 @@ namespace ts { - describe("unittests:: tsbuild:: declarationEmit", () => { - function getFiles(): vfs.FileSet { - return { - "/src/solution/tsconfig.base.json": JSON.stringify({ - compilerOptions: { - rootDir: "./", - outDir: "lib" - } - }), - "/src/solution/tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true }, - references: [{ path: "./src" }], - include: [] - }), - "/src/solution/src/tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true }, - references: [{ path: "./subProject" }, { path: "./subProject2" }], - include: [] - }), - "/src/solution/src/subProject/tsconfig.json": JSON.stringify({ - extends: "../../tsconfig.base.json", - compilerOptions: { composite: true }, - references: [{ path: "../common" }], - include: ["./index.ts"] - }), - "/src/solution/src/subProject/index.ts": Utils.dedent` +describe("unittests:: tsbuild:: declarationEmit", () => { + function getFiles(): vfs.FileSet { + return { + "/src/solution/tsconfig.base.json": JSON.stringify({ + compilerOptions: { + rootDir: "./", + outDir: "lib" + } + }), + "/src/solution/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true }, + references: [{ path: "./src" }], + include: [] + }), + "/src/solution/src/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true }, + references: [{ path: "./subProject" }, { path: "./subProject2" }], + include: [] + }), + "/src/solution/src/subProject/tsconfig.json": JSON.stringify({ + extends: "../../tsconfig.base.json", + compilerOptions: { composite: true }, + references: [{ path: "../common" }], + include: ["./index.ts"] + }), + "/src/solution/src/subProject/index.ts": Utils.dedent` import { Nominal } from '../common/nominal'; export type MyNominal = Nominal;`, - "/src/solution/src/subProject2/tsconfig.json": JSON.stringify({ - extends: "../../tsconfig.base.json", - compilerOptions: { composite: true }, - references: [{ path: "../subProject" }], - include: ["./index.ts"] - }), - "/src/solution/src/subProject2/index.ts": Utils.dedent` + "/src/solution/src/subProject2/tsconfig.json": JSON.stringify({ + extends: "../../tsconfig.base.json", + compilerOptions: { composite: true }, + references: [{ path: "../subProject" }], + include: ["./index.ts"] + }), + "/src/solution/src/subProject2/index.ts": Utils.dedent` import { MyNominal } from '../subProject/index'; const variable = { key: 'value' as MyNominal, @@ -41,78 +41,78 @@ const variable = { export function getVar(): keyof typeof variable { return 'key'; }`, - "/src/solution/src/common/tsconfig.json": JSON.stringify({ - extends: "../../tsconfig.base.json", - compilerOptions: { composite: true }, - include: ["./nominal.ts"] - }), - "/src/solution/src/common/nominal.ts": Utils.dedent` + "/src/solution/src/common/tsconfig.json": JSON.stringify({ + extends: "../../tsconfig.base.json", + compilerOptions: { composite: true }, + include: ["./nominal.ts"] + }), + "/src/solution/src/common/nominal.ts": Utils.dedent` /// export declare type Nominal = MyNominal;`, - "/src/solution/src/common/types.d.ts": Utils.dedent` + "/src/solution/src/common/types.d.ts": Utils.dedent` declare type MyNominal = T & { specialKey: Name; };`, - }; - } - ts.verifyTsc({ - scenario: "declarationEmit", - subScenario: "when declaration file is referenced through triple slash", - fs: () => ts.loadProjectFromFiles(getFiles()), - commandLineArgs: ["--b", "/src/solution/tsconfig.json", "--verbose"] - }); + }; + } + ts.verifyTsc({ + scenario: "declarationEmit", + subScenario: "when declaration file is referenced through triple slash", + fs: () => ts.loadProjectFromFiles(getFiles()), + commandLineArgs: ["--b", "/src/solution/tsconfig.json", "--verbose"] + }); - ts.verifyTsc({ - scenario: "declarationEmit", - subScenario: "when declaration file is referenced through triple slash but uses no references", - fs: () => ts.loadProjectFromFiles({ - ...getFiles(), - "/src/solution/tsconfig.json": JSON.stringify({ - extends: "./tsconfig.base.json", - compilerOptions: { composite: true }, - include: ["./src/**/*.ts"] - }), + ts.verifyTsc({ + scenario: "declarationEmit", + subScenario: "when declaration file is referenced through triple slash but uses no references", + fs: () => ts.loadProjectFromFiles({ + ...getFiles(), + "/src/solution/tsconfig.json": JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { composite: true }, + include: ["./src/**/*.ts"] }), - commandLineArgs: ["--b", "/src/solution/tsconfig.json", "--verbose"] - }); + }), + commandLineArgs: ["--b", "/src/solution/tsconfig.json", "--verbose"] + }); - ts.verifyTsc({ - scenario: "declarationEmit", - subScenario: "when declaration file used inferred type from referenced project", - fs: () => ts.loadProjectFromFiles({ - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - composite: true, - baseUrl: ".", - paths: { "@fluentui/*": ["packages/*/src"] } - } - }), - "/src/packages/pkg1/src/index.ts": Utils.dedent` + ts.verifyTsc({ + scenario: "declarationEmit", + subScenario: "when declaration file used inferred type from referenced project", + fs: () => ts.loadProjectFromFiles({ + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + composite: true, + baseUrl: ".", + paths: { "@fluentui/*": ["packages/*/src"] } + } + }), + "/src/packages/pkg1/src/index.ts": Utils.dedent` export interface IThing { a: string; } export interface IThings { thing1: IThing; }`, - "/src/packages/pkg1/tsconfig.json": JSON.stringify({ - extends: "../../tsconfig", - compilerOptions: { outDir: "lib" }, - include: ["src"] - }), - "/src/packages/pkg2/src/index.ts": Utils.dedent` + "/src/packages/pkg1/tsconfig.json": JSON.stringify({ + extends: "../../tsconfig", + compilerOptions: { outDir: "lib" }, + include: ["src"] + }), + "/src/packages/pkg2/src/index.ts": Utils.dedent` import { IThings } from '@fluentui/pkg1'; export function fn4() { const a: IThings = { thing1: { a: 'b' } }; return a.thing1; }`, - "/src/packages/pkg2/tsconfig.json": JSON.stringify({ - extends: "../../tsconfig", - compilerOptions: { outDir: "lib" }, - include: ["src"], - references: [{ path: "../pkg1" }] - }), + "/src/packages/pkg2/tsconfig.json": JSON.stringify({ + extends: "../../tsconfig", + compilerOptions: { outDir: "lib" }, + include: ["src"], + references: [{ path: "../pkg1" }] }), - commandLineArgs: ["--b", "/src/packages/pkg2/tsconfig.json", "--verbose"] - }); + }), + commandLineArgs: ["--b", "/src/packages/pkg2/tsconfig.json", "--verbose"] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/demo.ts b/src/testRunner/unittests/tsbuild/demo.ts index 63bdc8efae770..8c80f8e57db19 100644 --- a/src/testRunner/unittests/tsbuild/demo.ts +++ b/src/testRunner/unittests/tsbuild/demo.ts @@ -1,40 +1,40 @@ namespace ts { - describe("unittests:: tsbuild:: on demo project", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/demo"); - }); +describe("unittests:: tsbuild:: on demo project", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/demo"); + }); - after(() => { - projFs = undefined!; // Release the contents - }); + after(() => { + projFs = undefined!; // Release the contents + }); - ts.verifyTsc({ - scenario: "demo", - subScenario: "in master branch with everything setup correctly and reports no error", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"] - }); + ts.verifyTsc({ + scenario: "demo", + subScenario: "in master branch with everything setup correctly and reports no error", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"] + }); - ts.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 => ts.replaceText(fs, "/src/core/tsconfig.json", "}", `}, + ts.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 => ts.replaceText(fs, "/src/core/tsconfig.json", "}", `}, "references": [ { "path": "../zoo" } ]`) - }); - ts.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 => ts.prependText(fs, "/src/core/utilities.ts", `import * as A from '../animals'; + }); + ts.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 => ts.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 091c40719f7b2..1cb2d6d34ea52 100644 --- a/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts +++ b/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts @@ -1,52 +1,52 @@ namespace ts { - describe("unittests:: tsbuild:: on project with emitDeclarationOnly set to true", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/emitDeclarationOnly"); - }); - after(() => { - projFs = undefined!; - }); - - function verifyEmitDeclarationOnly(disableMap?: true) { - ts.verifyTscWithEdits({ - subScenario: `only dts output in circular import project with emitDeclarationOnly${disableMap ? "" : " and declarationMap"}`, - fs: () => projFs, - scenario: "emitDeclarationOnly", - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: disableMap ? - (fs => ts.replaceText(fs, "/src/tsconfig.json", `"declarationMap": true,`, "")) : - undefined, - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), - }], - }); - } - verifyEmitDeclarationOnly(); - verifyEmitDeclarationOnly(/*disableMap*/ true); +describe("unittests:: tsbuild:: on project with emitDeclarationOnly set to true", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/emitDeclarationOnly"); + }); + after(() => { + projFs = undefined!; + }); + function verifyEmitDeclarationOnly(disableMap?: true) { ts.verifyTscWithEdits({ - 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"); - ts.replaceText(fs, "/src/src/a.ts", `import { B } from "./b";`, `export class B { prop = "hello"; }`); - }, - edits: [ - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.replaceText(fs, "/src/src/a.ts", "export interface A {", `class C { } -export interface A {`), - - }, - { - subScenario: "incremental-declaration-changes", + modifyFs: disableMap ? + (fs => ts.replaceText(fs, "/src/tsconfig.json", `"declarationMap": true,`, "")) : + undefined, + edits: [{ + subScenario: "incremental-declaration-changes", modifyFs: fs => ts.replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), - }, - ], + }], }); + } + verifyEmitDeclarationOnly(); + verifyEmitDeclarationOnly(/*disableMap*/ true); + + ts.verifyTscWithEdits({ + 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"); + ts.replaceText(fs, "/src/src/a.ts", `import { B } from "./b";`, `export class B { prop = "hello"; }`); + }, + edits: [ + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.replaceText(fs, "/src/src/a.ts", "export interface A {", `class C { } +export interface A {`), + + }, + { + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), + }, + ], }); +}); } diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index fe829e0558945..ccb1a8d553433 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -1,25 +1,25 @@ namespace ts { - describe("unittests:: tsbuild - empty files option in tsconfig", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/empty-files"); - }); - after(() => { - projFs = undefined!; - }); +describe("unittests:: tsbuild - empty files option in tsconfig", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/empty-files"); + }); + after(() => { + projFs = undefined!; + }); - ts.verifyTsc({ - scenario: "emptyFiles", - subScenario: "has empty files diagnostic when files is empty and no references are provided", - fs: () => projFs, - commandLineArgs: ["--b", "/src/no-references"], - }); + ts.verifyTsc({ + scenario: "emptyFiles", + subScenario: "has empty files diagnostic when files is empty and no references are provided", + fs: () => projFs, + commandLineArgs: ["--b", "/src/no-references"], + }); - ts.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"], - }); + ts.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 fd200a3ca5788..d60fa6c1276f0 100644 --- a/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts +++ b/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts @@ -1,11 +1,11 @@ namespace ts { - // https://github.com/microsoft/TypeScript/issues/33849 - describe("unittests:: tsbuild:: exitCodeOnBogusFile:: test exit code", () => { - ts.verifyTsc({ - scenario: "exitCodeOnBogusFile", - subScenario: `test exit code`, - fs: () => ts.loadProjectFromFiles({}), - commandLineArgs: ["-b", "bogus.json"] - }); +// https://github.com/microsoft/TypeScript/issues/33849 +describe("unittests:: tsbuild:: exitCodeOnBogusFile:: test exit code", () => { + ts.verifyTsc({ + scenario: "exitCodeOnBogusFile", + subScenario: `test exit code`, + fs: () => ts.loadProjectFromFiles({}), + commandLineArgs: ["-b", "bogus.json"] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts index 678947281b0d3..5c7f4b4050118 100644 --- a/src/testRunner/unittests/tsbuild/graphOrdering.ts +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -1,100 +1,100 @@ 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"] - ]; +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); - }); + 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; - }); + 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 - 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 - 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("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); - }); + 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 = ts.createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false }); - const buildOrder = builder.getBuildOrder(); - assert.equal(ts.isCircularBuildOrder(buildOrder), !!circular); - const buildQueue = ts.getBuildOrderFromAnyBuildOrder(buildOrder); - assert.deepEqual(buildQueue, expectedBuildSet.map(getProjectFileName)); + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[], circular?: true) { + const builder = ts.createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false }); + const buildOrder = builder.getBuildOrder(); + assert.equal(ts.isCircularBuildOrder(buildOrder), !!circular); + const buildQueue = ts.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}`); - } + 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 ts.ResolvedConfigFileName; - } + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ts.ResolvedConfigFileName; + } - 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; + 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; + } +}); } diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index c270fbd1ae433..4c2b76674d50f 100644 --- a/src/testRunner/unittests/tsbuild/helpers.ts +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -1,89 +1,89 @@ namespace ts { - export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic { - return { message }; - } +export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic { + return { message }; +} - export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { - return [ts.Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; - } +export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { + return [ts.Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; +} - export function changeCompilerVersion(host: fakes.SolutionBuilderHost) { - const originalReadFile = host.readFile; - host.readFile = path => { - const value = originalReadFile.call(host, path); - if (!value || !ts.isBuildInfoFile(path)) - return value; - const buildInfo = ts.getBuildInfo(value); - buildInfo.version = fakes.version; - return ts.getBuildInfoText(buildInfo); - }; - } +export function changeCompilerVersion(host: fakes.SolutionBuilderHost) { + const originalReadFile = host.readFile; + host.readFile = path => { + const value = originalReadFile.call(host, path); + if (!value || !ts.isBuildInfoFile(path)) + return value; + const buildInfo = ts.getBuildInfo(value); + buildInfo.version = fakes.version; + return ts.getBuildInfoText(buildInfo); + }; +} - 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"); +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`); } - - 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"); + 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"); +} - 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}`); +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"); +} - 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); +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}`); +} - export function lastIndexOf(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.lastIndexOf(searchStr); +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); +} - export function expectedLocationIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { - return { - file, - start: indexOf(fs, file, searchStr), - length: searchStr.length - }; +export function lastIndexOf(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.lastIndexOf(searchStr); +} - export function expectedLocationLastIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { - return { - file, - start: lastIndexOf(fs, file, searchStr), - length: searchStr.length - }; - } +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 const libContent = `${ts.TestFSWithWatch.libFile.content} +export const libContent = `${ts.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; @@ -94,537 +94,537 @@ interface Symbol { } `; - /** - * 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; - } +/** + * 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; - } +/** + * 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(); - } +function addLibAndMakeReadonly(fs: vfs.FileSystem, libContentToAppend?: string) { + fs.mkdirSync("/lib"); + fs.writeFileSync("/lib/lib.d.ts", libContentToAppend ? `${libContent}${libContentToAppend}` : libContent); + fs.makeReadonly(); +} - export function verifyOutputsPresent(fs: vfs.FileSystem, outputs: readonly string[]) { - for (const output of outputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } +export function verifyOutputsPresent(fs: vfs.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: vfs.FileSystem, outputs: readonly string[]) { + for (const output of outputs) { + assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); } +} - export function generateSourceMapBaselineFiles(sys: ts.System & { - writtenFiles: ts.ReadonlyCollection; - }) { - const mapFileNames = ts.mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined); - while (true) { - const result = mapFileNames.next(); - if (result.done) - break; - const mapFile = result.value; - const text = Harness.SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile); - sys.writeFile(`${mapFile}.baseline.txt`, text); - } +export function generateSourceMapBaselineFiles(sys: ts.System & { + writtenFiles: ts.ReadonlyCollection; +}) { + const mapFileNames = ts.mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined); + while (true) { + const result = mapFileNames.next(); + if (result.done) + break; + const mapFile = result.value; + const text = Harness.SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile); + sys.writeFile(`${mapFile}.baseline.txt`, text); } +} - function generateBundleFileSectionInfo(sys: ts.System, originalReadCall: ts.System["readFile"], baselineRecorder: Harness.Compiler.WriterAggregator, bundleFileInfo: ts.BundleFileInfo | undefined, outFile: string | undefined) { - if (!ts.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 : ts.emptyArray) { - baselineRecorder.WriteLine("----------------------------------------------------------------------"); - writeSectionHeader(section); - if (section.kind !== ts.BundleFileSectionKind.Prepend) { - writeTextOfSection(section.pos, section.end); - } - else if (section.texts.length > 0) { - ts.Debug.assert(section.pos === ts.first(section.texts).pos); - ts.Debug.assert(section.end === ts.last(section.texts).end); - for (const text of section.texts) { - baselineRecorder.WriteLine(">>--------------------------------------------------------------------"); - writeSectionHeader(text); - writeTextOfSection(text.pos, text.end); - } - } - else { - ts.Debug.assert(section.pos === section.end); - } +function generateBundleFileSectionInfo(sys: ts.System, originalReadCall: ts.System["readFile"], baselineRecorder: Harness.Compiler.WriterAggregator, bundleFileInfo: ts.BundleFileInfo | undefined, outFile: string | undefined) { + if (!ts.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 : ts.emptyArray) { + baselineRecorder.WriteLine("----------------------------------------------------------------------"); + writeSectionHeader(section); + if (section.kind !== ts.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) { + ts.Debug.assert(section.pos === ts.first(section.texts).pos); + ts.Debug.assert(section.end === ts.last(section.texts).end); + for (const text of section.texts) { + baselineRecorder.WriteLine(">>--------------------------------------------------------------------"); + writeSectionHeader(text); + writeTextOfSection(text.pos, text.end); } } + else { + ts.Debug.assert(section.pos === section.end); + } + } + baselineRecorder.WriteLine("======================================================================"); - function writeSectionHeader(section: ts.BundleFileSection) { - baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === ts.BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`); + function writeTextOfSection(pos: number, end: number) { + const textLines = content.substring(pos, end).split(/\r?\n/); + for (const line of textLines) { + baselineRecorder.WriteLine(line); } } - type ReadableProgramBuildInfoDiagnostic = string | [ - string, - readonly ts.ReusableDiagnostic[] - ]; - type ReadableProgramBuilderInfoFilePendingEmit = [ - string, - "DtsOnly" | "Full" - ]; - interface ReadableProgramBuildInfo { - fileNames: readonly string[]; - fileNamesList: readonly (readonly string[])[] | undefined; - fileInfos: ts.MapLike; - options: ts.CompilerOptions | undefined; - referencedMap?: ts.MapLike; - exportedModulesMap?: ts.MapLike; - semanticDiagnosticsPerFile?: readonly ReadableProgramBuildInfoDiagnostic[]; - affectedFilesPendingEmit?: readonly ReadableProgramBuilderInfoFilePendingEmit[]; + function writeSectionHeader(section: ts.BundleFileSection) { + baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === ts.BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`); } - type ReadableBuildInfo = Omit & { - program: ReadableProgramBuildInfo | undefined; - size: number; - }; - function generateBuildInfoProgramBaseline(sys: ts.System, buildInfoPath: string, buildInfo: ts.BuildInfo) { - const fileInfos: ReadableProgramBuildInfo["fileInfos"] = {}; - buildInfo.program?.fileInfos.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ts.ProgramBuildInfoFileId)] = ts.toBuilderStateFileInfo(fileInfo)); - const fileNamesList = buildInfo.program?.fileIdsList?.map(fileIdsListId => fileIdsListId.map(toFileName)); - const program: ReadableProgramBuildInfo | undefined = buildInfo.program && { - fileNames: buildInfo.program.fileNames, - fileNamesList, - fileInfos, - options: buildInfo.program.options, - referencedMap: toMapOfReferencedSet(buildInfo.program.referencedMap), - exportedModulesMap: toMapOfReferencedSet(buildInfo.program.exportedModulesMap), - semanticDiagnosticsPerFile: buildInfo.program.semanticDiagnosticsPerFile?.map(d => ts.isNumber(d) ? - toFileName(d) : - [toFileName(d[0]), d[1]]), - affectedFilesPendingEmit: buildInfo.program.affectedFilesPendingEmit?.map(([fileId, emitKind]) => [ - toFileName(fileId), - emitKind === ts.BuilderFileEmit.DtsOnly ? "DtsOnly" : - emitKind === ts.BuilderFileEmit.Full ? "Full" : - ts.Debug.assertNever(emitKind) - ]), - }; - const version = buildInfo.version === ts.version ? fakes.version : buildInfo.version; - const result: ReadableBuildInfo = { - bundle: buildInfo.bundle, - program, - version, - size: ts.getBuildInfoText({ ...buildInfo, version }).length, - }; - // For now its just JSON.stringify - sys.writeFile(`${buildInfoPath}.readable.baseline.txt`, JSON.stringify(result, /*replacer*/ undefined, 2)); - - function toFileName(fileId: ts.ProgramBuildInfoFileId) { - return buildInfo.program!.fileNames[fileId - 1]; - } +} - function toFileNames(fileIdsListId: ts.ProgramBuildInfoFileIdListId) { - return fileNamesList![fileIdsListId - 1]; - } +type ReadableProgramBuildInfoDiagnostic = string | [ + string, + readonly ts.ReusableDiagnostic[] +]; +type ReadableProgramBuilderInfoFilePendingEmit = [ + string, + "DtsOnly" | "Full" +]; +interface ReadableProgramBuildInfo { + fileNames: readonly string[]; + fileNamesList: readonly (readonly string[])[] | undefined; + fileInfos: ts.MapLike; + options: ts.CompilerOptions | undefined; + referencedMap?: ts.MapLike; + exportedModulesMap?: ts.MapLike; + semanticDiagnosticsPerFile?: readonly ReadableProgramBuildInfoDiagnostic[]; + affectedFilesPendingEmit?: readonly ReadableProgramBuilderInfoFilePendingEmit[]; +} +type ReadableBuildInfo = Omit & { + program: ReadableProgramBuildInfo | undefined; + size: number; +}; +function generateBuildInfoProgramBaseline(sys: ts.System, buildInfoPath: string, buildInfo: ts.BuildInfo) { + const fileInfos: ReadableProgramBuildInfo["fileInfos"] = {}; + buildInfo.program?.fileInfos.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ts.ProgramBuildInfoFileId)] = ts.toBuilderStateFileInfo(fileInfo)); + const fileNamesList = buildInfo.program?.fileIdsList?.map(fileIdsListId => fileIdsListId.map(toFileName)); + const program: ReadableProgramBuildInfo | undefined = buildInfo.program && { + fileNames: buildInfo.program.fileNames, + fileNamesList, + fileInfos, + options: buildInfo.program.options, + referencedMap: toMapOfReferencedSet(buildInfo.program.referencedMap), + exportedModulesMap: toMapOfReferencedSet(buildInfo.program.exportedModulesMap), + semanticDiagnosticsPerFile: buildInfo.program.semanticDiagnosticsPerFile?.map(d => ts.isNumber(d) ? + toFileName(d) : + [toFileName(d[0]), d[1]]), + affectedFilesPendingEmit: buildInfo.program.affectedFilesPendingEmit?.map(([fileId, emitKind]) => [ + toFileName(fileId), + emitKind === ts.BuilderFileEmit.DtsOnly ? "DtsOnly" : + emitKind === ts.BuilderFileEmit.Full ? "Full" : + ts.Debug.assertNever(emitKind) + ]), + }; + const version = buildInfo.version === ts.version ? fakes.version : buildInfo.version; + const result: ReadableBuildInfo = { + bundle: buildInfo.bundle, + program, + version, + size: ts.getBuildInfoText({ ...buildInfo, version }).length, + }; + // For now its just JSON.stringify + sys.writeFile(`${buildInfoPath}.readable.baseline.txt`, JSON.stringify(result, /*replacer*/ undefined, 2)); - function toMapOfReferencedSet(referenceMap: ts.ProgramBuildInfoReferencedMap | undefined): ts.MapLike | undefined { - if (!referenceMap) - return undefined; - const result: ts.MapLike = {}; - for (const [fileNamesKey, fileNamesListKey] of referenceMap) { - result[toFileName(fileNamesKey)] = toFileNames(fileNamesListKey); - } - return result; - } + function toFileName(fileId: ts.ProgramBuildInfoFileId) { + return buildInfo.program!.fileNames[fileId - 1]; } - export function toPathWithSystem(sys: ts.System, fileName: string): ts.Path { - return ts.toPath(fileName, sys.getCurrentDirectory(), ts.createGetCanonicalFileName(sys.useCaseSensitiveFileNames)); + function toFileNames(fileIdsListId: ts.ProgramBuildInfoFileIdListId) { + return fileNamesList![fileIdsListId - 1]; } - export function baselineBuildInfo(options: ts.CompilerOptions, sys: ts.TscCompileSystem | ts.tscWatch.WatchedSystem, originalReadCall?: ts.System["readFile"]) { - const buildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(options); - if (!buildInfoPath || !sys.writtenFiles!.has(toPathWithSystem(sys, buildInfoPath))) - return; - if (!sys.fileExists(buildInfoPath)) - return; - const buildInfo = ts.getBuildInfo((originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!); - generateBuildInfoProgramBaseline(sys, buildInfoPath, buildInfo); - if (!ts.outFile(options)) - return; - const { jsFilePath, declarationFilePath } = ts.getOutputPathsForBundle(options, /*forceDts*/ false); - const bundle = buildInfo.bundle; - if (!bundle || (!ts.length(bundle.js && bundle.js.sections) && !ts.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); - } - interface VerifyTscEditDiscrepanciesInput { - index: number; - scenario: ts.TestTscCompile["scenario"]; - subScenario: ts.TestTscCompile["subScenario"]; - baselines: string[] | undefined; - commandLineArgs: ts.TestTscCompile["commandLineArgs"]; - modifyFs: ts.TestTscCompile["modifyFs"]; - editFs: TestTscEdit["modifyFs"]; - baseFs: vfs.FileSystem; - newSys: ts.TscCompileSystem; - discrepancyExplanation: TestTscEdit["discrepancyExplanation"]; + function toMapOfReferencedSet(referenceMap: ts.ProgramBuildInfoReferencedMap | undefined): ts.MapLike | undefined { + if (!referenceMap) + return undefined; + const result: ts.MapLike = {}; + for (const [fileNamesKey, fileNamesListKey] of referenceMap) { + result[toFileName(fileNamesKey)] = toFileNames(fileNamesListKey); + } + return result; } - function verifyTscEditDiscrepancies({ index, scenario, subScenario, commandLineArgs, discrepancyExplanation, baselines, modifyFs, editFs, baseFs, newSys }: VerifyTscEditDiscrepanciesInput): string[] | undefined { - const sys = ts.testTscCompile({ - scenario, - subScenario, - fs: () => baseFs.makeReadonly(), - commandLineArgs, - modifyFs: fs => { - if (modifyFs) - modifyFs(fs); - editFs(fs); - }, - disableUseFileVersionAsSignature: true, - }); - let headerAdded = false; - for (const outputFile of ts.arrayFrom(sys.writtenFiles.keys())) { - const cleanBuildText = sys.readFile(outputFile); - const incrementalBuildText = newSys.readFile(outputFile); - if (ts.isBuildInfoFile(outputFile)) { - // Check only presence and absence and not text as we will do that for readable baseline - if (!sys.fileExists(`${outputFile}.readable.baseline.txt`)) - addBaseline(`Readable baseline not present in clean build:: File:: ${outputFile}`); - if (!newSys.fileExists(`${outputFile}.readable.baseline.txt`)) - addBaseline(`Readable baseline not present in incremental build:: File:: ${outputFile}`); - verifyPresenceAbsence(incrementalBuildText, cleanBuildText, `Incremental and clean tsbuildinfo file presence differs:: File:: ${outputFile}`); - } - else if (!ts.fileExtensionIs(outputFile, ".tsbuildinfo.readable.baseline.txt")) { - verifyTextEqual(incrementalBuildText, cleanBuildText, `File: ${outputFile}`); - } - else if (incrementalBuildText !== cleanBuildText) { - // Verify build info without affectedFilesPendingEmit - const { buildInfo: incrementalBuildInfo, readableBuildInfo: incrementalReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(incrementalBuildText); - const { buildInfo: cleanBuildInfo, readableBuildInfo: cleanReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(cleanBuildText); - verifyTextEqual(incrementalBuildInfo, cleanBuildInfo, `TsBuild info text without affectedFilesPendingEmit:: ${outputFile}::`); - // Verify file info sigantures - verifyMapLike(incrementalReadableBuildInfo?.program?.fileInfos, cleanReadableBuildInfo?.program?.fileInfos, (key, incrementalFileInfo, cleanFileInfo) => { - if (incrementalFileInfo.signature !== cleanFileInfo.signature && incrementalFileInfo.signature !== incrementalFileInfo.version) { - return [ - `Incremental signature is neither dts signature nor file version for File:: ${key}`, - `Incremental:: ${JSON.stringify(incrementalFileInfo, /*replacer*/ undefined, 2)}`, - `Clean:: ${JSON.stringify(cleanFileInfo, /*replacer*/ undefined, 2)}` - ]; - } - }, `FileInfos:: File:: ${outputFile}`); - // Verify exportedModulesMap - verifyMapLike(incrementalReadableBuildInfo?.program?.exportedModulesMap, cleanReadableBuildInfo?.program?.exportedModulesMap, (key, incrementalReferenceSet, cleanReferenceSet) => { - if (!ts.arrayIsEqualTo(incrementalReferenceSet, cleanReferenceSet) && !ts.arrayIsEqualTo(incrementalReferenceSet, incrementalReadableBuildInfo!.program!.referencedMap![key])) { - return [ - `Incremental Reference set is neither from dts nor files reference map for File:: ${key}::`, - `Incremental:: ${JSON.stringify(incrementalReferenceSet, /*replacer*/ undefined, 2)}`, - `Clean:: ${JSON.stringify(cleanReferenceSet, /*replacer*/ undefined, 2)}`, - `IncrementalReferenceMap:: ${JSON.stringify(incrementalReadableBuildInfo!.program!.referencedMap![key], /*replacer*/ undefined, 2)}`, - `CleanReferenceMap:: ${JSON.stringify(cleanReadableBuildInfo!.program!.referencedMap![key], /*replacer*/ undefined, 2)}`, - ]; - } - }, `exportedModulesMap:: File:: ${outputFile}`); - // Verify that incrementally pending affected file emit are in clean build since clean build can contain more files compared to incremental depending of noEmitOnError option - if (incrementalReadableBuildInfo?.program?.affectedFilesPendingEmit) { - if (cleanReadableBuildInfo?.program?.affectedFilesPendingEmit === undefined) { - addBaseline(`Incremental build contains affectedFilesPendingEmit, clean build does not have it: ${outputFile}::`, `Incremental buildInfoText:: ${incrementalBuildText}`, `Clean buildInfoText:: ${cleanBuildText}`); +} + +export function toPathWithSystem(sys: ts.System, fileName: string): ts.Path { + return ts.toPath(fileName, sys.getCurrentDirectory(), ts.createGetCanonicalFileName(sys.useCaseSensitiveFileNames)); +} +export function baselineBuildInfo(options: ts.CompilerOptions, sys: ts.TscCompileSystem | ts.tscWatch.WatchedSystem, originalReadCall?: ts.System["readFile"]) { + const buildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(options); + if (!buildInfoPath || !sys.writtenFiles!.has(toPathWithSystem(sys, buildInfoPath))) + return; + if (!sys.fileExists(buildInfoPath)) + return; + const buildInfo = ts.getBuildInfo((originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!); + generateBuildInfoProgramBaseline(sys, buildInfoPath, buildInfo); + + if (!ts.outFile(options)) + return; + const { jsFilePath, declarationFilePath } = ts.getOutputPathsForBundle(options, /*forceDts*/ false); + const bundle = buildInfo.bundle; + if (!bundle || (!ts.length(bundle.js && bundle.js.sections) && !ts.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); +} +interface VerifyTscEditDiscrepanciesInput { + index: number; + scenario: ts.TestTscCompile["scenario"]; + subScenario: ts.TestTscCompile["subScenario"]; + baselines: string[] | undefined; + commandLineArgs: ts.TestTscCompile["commandLineArgs"]; + modifyFs: ts.TestTscCompile["modifyFs"]; + editFs: TestTscEdit["modifyFs"]; + baseFs: vfs.FileSystem; + newSys: ts.TscCompileSystem; + discrepancyExplanation: TestTscEdit["discrepancyExplanation"]; +} +function verifyTscEditDiscrepancies({ index, scenario, subScenario, commandLineArgs, discrepancyExplanation, baselines, modifyFs, editFs, baseFs, newSys }: VerifyTscEditDiscrepanciesInput): string[] | undefined { + const sys = ts.testTscCompile({ + scenario, + subScenario, + fs: () => baseFs.makeReadonly(), + commandLineArgs, + modifyFs: fs => { + if (modifyFs) + modifyFs(fs); + editFs(fs); + }, + disableUseFileVersionAsSignature: true, + }); + let headerAdded = false; + for (const outputFile of ts.arrayFrom(sys.writtenFiles.keys())) { + const cleanBuildText = sys.readFile(outputFile); + const incrementalBuildText = newSys.readFile(outputFile); + if (ts.isBuildInfoFile(outputFile)) { + // Check only presence and absence and not text as we will do that for readable baseline + if (!sys.fileExists(`${outputFile}.readable.baseline.txt`)) + addBaseline(`Readable baseline not present in clean build:: File:: ${outputFile}`); + if (!newSys.fileExists(`${outputFile}.readable.baseline.txt`)) + addBaseline(`Readable baseline not present in incremental build:: File:: ${outputFile}`); + verifyPresenceAbsence(incrementalBuildText, cleanBuildText, `Incremental and clean tsbuildinfo file presence differs:: File:: ${outputFile}`); + } + else if (!ts.fileExtensionIs(outputFile, ".tsbuildinfo.readable.baseline.txt")) { + verifyTextEqual(incrementalBuildText, cleanBuildText, `File: ${outputFile}`); + } + else if (incrementalBuildText !== cleanBuildText) { + // Verify build info without affectedFilesPendingEmit + const { buildInfo: incrementalBuildInfo, readableBuildInfo: incrementalReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(incrementalBuildText); + const { buildInfo: cleanBuildInfo, readableBuildInfo: cleanReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(cleanBuildText); + verifyTextEqual(incrementalBuildInfo, cleanBuildInfo, `TsBuild info text without affectedFilesPendingEmit:: ${outputFile}::`); + // Verify file info sigantures + verifyMapLike(incrementalReadableBuildInfo?.program?.fileInfos, cleanReadableBuildInfo?.program?.fileInfos, (key, incrementalFileInfo, cleanFileInfo) => { + if (incrementalFileInfo.signature !== cleanFileInfo.signature && incrementalFileInfo.signature !== incrementalFileInfo.version) { + return [ + `Incremental signature is neither dts signature nor file version for File:: ${key}`, + `Incremental:: ${JSON.stringify(incrementalFileInfo, /*replacer*/ undefined, 2)}`, + `Clean:: ${JSON.stringify(cleanFileInfo, /*replacer*/ undefined, 2)}` + ]; } - let expectedIndex = 0; - incrementalReadableBuildInfo.program.affectedFilesPendingEmit.forEach(([actualFile]) => { - expectedIndex = ts.findIndex(cleanReadableBuildInfo!.program!.affectedFilesPendingEmit!, ([expectedFile]) => actualFile === expectedFile, expectedIndex); - if (expectedIndex === -1) { - addBaseline(`Incremental build contains ${actualFile} file as pending emit, clean build does not have it: ${outputFile}::`, `Incremental buildInfoText:: ${incrementalBuildText}`, `Clean buildInfoText:: ${cleanBuildText}`); - } - expectedIndex++; - }); + }, `FileInfos:: File:: ${outputFile}`); + // Verify exportedModulesMap + verifyMapLike(incrementalReadableBuildInfo?.program?.exportedModulesMap, cleanReadableBuildInfo?.program?.exportedModulesMap, (key, incrementalReferenceSet, cleanReferenceSet) => { + if (!ts.arrayIsEqualTo(incrementalReferenceSet, cleanReferenceSet) && !ts.arrayIsEqualTo(incrementalReferenceSet, incrementalReadableBuildInfo!.program!.referencedMap![key])) { + return [ + `Incremental Reference set is neither from dts nor files reference map for File:: ${key}::`, + `Incremental:: ${JSON.stringify(incrementalReferenceSet, /*replacer*/ undefined, 2)}`, + `Clean:: ${JSON.stringify(cleanReferenceSet, /*replacer*/ undefined, 2)}`, + `IncrementalReferenceMap:: ${JSON.stringify(incrementalReadableBuildInfo!.program!.referencedMap![key], /*replacer*/ undefined, 2)}`, + `CleanReferenceMap:: ${JSON.stringify(cleanReadableBuildInfo!.program!.referencedMap![key], /*replacer*/ undefined, 2)}`, + ]; + } + }, `exportedModulesMap:: File:: ${outputFile}`); + // Verify that incrementally pending affected file emit are in clean build since clean build can contain more files compared to incremental depending of noEmitOnError option + if (incrementalReadableBuildInfo?.program?.affectedFilesPendingEmit) { + if (cleanReadableBuildInfo?.program?.affectedFilesPendingEmit === undefined) { + addBaseline(`Incremental build contains affectedFilesPendingEmit, clean build does not have it: ${outputFile}::`, `Incremental buildInfoText:: ${incrementalBuildText}`, `Clean buildInfoText:: ${cleanBuildText}`); } + let expectedIndex = 0; + incrementalReadableBuildInfo.program.affectedFilesPendingEmit.forEach(([actualFile]) => { + expectedIndex = ts.findIndex(cleanReadableBuildInfo!.program!.affectedFilesPendingEmit!, ([expectedFile]) => actualFile === expectedFile, expectedIndex); + if (expectedIndex === -1) { + addBaseline(`Incremental build contains ${actualFile} file as pending emit, clean build does not have it: ${outputFile}::`, `Incremental buildInfoText:: ${incrementalBuildText}`, `Clean buildInfoText:: ${cleanBuildText}`); + } + expectedIndex++; + }); } } - if (!headerAdded && discrepancyExplanation) - addBaseline("*** Supplied discrepancy explanation but didnt file any difference"); - return baselines; + } + if (!headerAdded && discrepancyExplanation) + addBaseline("*** Supplied discrepancy explanation but didnt file any difference"); + return baselines; - function verifyTextEqual(incrementalText: string | undefined, cleanText: string | undefined, message: string) { - if (incrementalText !== cleanText) - writeNotEqual(incrementalText, cleanText, message); - } + function verifyTextEqual(incrementalText: string | undefined, cleanText: string | undefined, message: string) { + if (incrementalText !== cleanText) + writeNotEqual(incrementalText, cleanText, message); + } - function verifyMapLike(incremental: ts.MapLike | undefined, clean: ts.MapLike | undefined, verifyValue: (key: string, incrementalValue: T, cleanValue: T) => string[] | undefined, message: string) { - verifyPresenceAbsence(incremental, clean, `Incremental and clean do not match:: ${message}`); - if (!incremental || !clean) - return; - const incrementalMap = new ts.Map(ts.getEntries(incremental)); - const cleanMap = new ts.Map(ts.getEntries(clean)); - if (incrementalMap.size !== cleanMap.size) { - addBaseline(`Incremental and clean size of maps do not match:: ${message}`, `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`); - return; - } - cleanMap.forEach((cleanValue, key) => { - const incrementalValue = incrementalMap.get(key); - if (!incrementalValue) { - addBaseline(`Incremental does not contain ${key} which is present in clean:: ${message}`, `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`); - } - else { - const result = verifyValue(key, incrementalMap.get(key)!, cleanValue); - if (result) - addBaseline(...result); - } - }); + function verifyMapLike(incremental: ts.MapLike | undefined, clean: ts.MapLike | undefined, verifyValue: (key: string, incrementalValue: T, cleanValue: T) => string[] | undefined, message: string) { + verifyPresenceAbsence(incremental, clean, `Incremental and clean do not match:: ${message}`); + if (!incremental || !clean) + return; + const incrementalMap = new ts.Map(ts.getEntries(incremental)); + const cleanMap = new ts.Map(ts.getEntries(clean)); + if (incrementalMap.size !== cleanMap.size) { + addBaseline(`Incremental and clean size of maps do not match:: ${message}`, `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`); + return; } - - function verifyPresenceAbsence(actual: T | undefined, expected: T | undefined, message: string) { - if (expected === undefined) { - if (actual === undefined) - return; + cleanMap.forEach((cleanValue, key) => { + const incrementalValue = incrementalMap.get(key); + if (!incrementalValue) { + addBaseline(`Incremental does not contain ${key} which is present in clean:: ${message}`, `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`); } else { - if (actual !== undefined) - return; + const result = verifyValue(key, incrementalMap.get(key)!, cleanValue); + if (result) + addBaseline(...result); } - writeNotEqual(actual, expected, message); - } + }); + } - function writeNotEqual(actual: T | undefined, expected: T | undefined, message: string) { - addBaseline(message, "CleanBuild:", ts.isString(expected) ? expected : JSON.stringify(expected), "IncrementalBuild:", ts.isString(actual) ? actual : JSON.stringify(actual)); + function verifyPresenceAbsence(actual: T | undefined, expected: T | undefined, message: string) { + if (expected === undefined) { + if (actual === undefined) + return; + } + else { + if (actual !== undefined) + return; } + writeNotEqual(actual, expected, message); + } - function addBaseline(...text: string[]) { - if (!baselines || !headerAdded) { - (baselines ||= []).push(`${index}:: ${subScenario}`, ...(discrepancyExplanation?.()|| ["*** Needs explanation"])); - headerAdded = true; - } - baselines.push(...text); + function writeNotEqual(actual: T | undefined, expected: T | undefined, message: string) { + addBaseline(message, "CleanBuild:", ts.isString(expected) ? expected : JSON.stringify(expected), "IncrementalBuild:", ts.isString(actual) ? actual : JSON.stringify(actual)); + } + + function addBaseline(...text: string[]) { + if (!baselines || !headerAdded) { + (baselines ||= []).push(`${index}:: ${subScenario}`, ...(discrepancyExplanation?.()|| ["*** Needs explanation"])); + headerAdded = true; } + baselines.push(...text); } +} - function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): { - buildInfo: string | undefined; - readableBuildInfo?: ReadableBuildInfo; - } { - if (!text) - return { buildInfo: text }; - const readableBuildInfo = JSON.parse(text) as ReadableBuildInfo; - let sanitizedFileInfos: ts.MapLike | undefined; - if (readableBuildInfo.program?.fileInfos) { - sanitizedFileInfos = {}; - for (const id in readableBuildInfo.program.fileInfos) { - if (ts.hasProperty(readableBuildInfo.program.fileInfos, id)) { - sanitizedFileInfos[id] = { ...readableBuildInfo.program.fileInfos[id], signature: undefined }; - } +function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): { + buildInfo: string | undefined; + readableBuildInfo?: ReadableBuildInfo; +} { + if (!text) + return { buildInfo: text }; + const readableBuildInfo = JSON.parse(text) as ReadableBuildInfo; + let sanitizedFileInfos: ts.MapLike | undefined; + if (readableBuildInfo.program?.fileInfos) { + sanitizedFileInfos = {}; + for (const id in readableBuildInfo.program.fileInfos) { + if (ts.hasProperty(readableBuildInfo.program.fileInfos, id)) { + sanitizedFileInfos[id] = { ...readableBuildInfo.program.fileInfos[id], signature: undefined }; } } - return { - buildInfo: JSON.stringify({ - ...readableBuildInfo, - program: readableBuildInfo.program && { - ...readableBuildInfo.program, - fileNames: undefined, - fileNamesList: undefined, - fileInfos: sanitizedFileInfos, - // Ignore noEmit since that shouldnt be reason to emit the tsbuild info and presence of it in the buildinfo file does not matter - options: { ...readableBuildInfo.program.options, noEmit: undefined }, - exportedModulesMap: undefined, - affectedFilesPendingEmit: undefined, - }, - size: undefined, // Size doesnt need to be equal - }, /*replacer*/ undefined, 2), - readableBuildInfo, - }; } + return { + buildInfo: JSON.stringify({ + ...readableBuildInfo, + program: readableBuildInfo.program && { + ...readableBuildInfo.program, + fileNames: undefined, + fileNamesList: undefined, + fileInfos: sanitizedFileInfos, + // Ignore noEmit since that shouldnt be reason to emit the tsbuild info and presence of it in the buildinfo file does not matter + options: { ...readableBuildInfo.program.options, noEmit: undefined }, + exportedModulesMap: undefined, + affectedFilesPendingEmit: undefined, + }, + size: undefined, // Size doesnt need to be equal + }, /*replacer*/ undefined, 2), + readableBuildInfo, + }; +} - export enum CleanBuildDescrepancy { - CleanFileTextDifferent, - CleanFilePresent - } +export enum CleanBuildDescrepancy { + CleanFileTextDifferent, + CleanFilePresent +} - export interface TestTscEdit { - modifyFs: (fs: vfs.FileSystem) => void; - subScenario: string; - commandLineArgs?: readonly string[]; - /** An array of lines to be printed in order when a discrepancy is detected */ - discrepancyExplanation?: () => readonly string[]; - } +export interface TestTscEdit { + modifyFs: (fs: vfs.FileSystem) => void; + subScenario: string; + commandLineArgs?: readonly string[]; + /** An array of lines to be printed in order when a discrepancy is detected */ + discrepancyExplanation?: () => readonly string[]; +} - export interface VerifyTscWithEditsInput extends ts.TestTscCompile { - edits: TestTscEdit[]; - } +export interface VerifyTscWithEditsInput extends ts.TestTscCompile { + edits: TestTscEdit[]; +} - /** - * Verify non watch tsc invokcation after each edit - */ - export function verifyTscWithEdits({ subScenario, fs, scenario, commandLineArgs, baselineSourceMap, modifyFs, baselineReadFileCalls, baselinePrograms, edits }: VerifyTscWithEditsInput) { - describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario} serializedEdits`, () => { - let sys: ts.TscCompileSystem; - let baseFs: vfs.FileSystem; - let editsSys: ts.TscCompileSystem[]; - before(() => { - ts.Debug.assert(!!edits.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`); - baseFs = fs().makeReadonly(); - sys = ts.testTscCompile({ +/** + * Verify non watch tsc invokcation after each edit + */ +export function verifyTscWithEdits({ subScenario, fs, scenario, commandLineArgs, baselineSourceMap, modifyFs, baselineReadFileCalls, baselinePrograms, edits }: VerifyTscWithEditsInput) { + describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario} serializedEdits`, () => { + let sys: ts.TscCompileSystem; + let baseFs: vfs.FileSystem; + let editsSys: ts.TscCompileSystem[]; + before(() => { + ts.Debug.assert(!!edits.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`); + baseFs = fs().makeReadonly(); + sys = ts.testTscCompile({ + scenario, + subScenario, + fs: () => baseFs, + commandLineArgs, + modifyFs, + baselineSourceMap, + baselineReadFileCalls, + baselinePrograms + }); + edits.forEach(({ modifyFs, subScenario: editScenario, commandLineArgs: editCommandLineArgs }, index) => { + (editsSys || (editsSys = [])).push(ts.testTscCompile({ scenario, - subScenario, - fs: () => baseFs, - commandLineArgs, + subScenario: editScenario || subScenario, + diffWithInitial: true, + fs: () => index === 0 ? sys.vfs : editsSys[index - 1].vfs, + commandLineArgs: editCommandLineArgs || commandLineArgs, modifyFs, baselineSourceMap, baselineReadFileCalls, baselinePrograms + })); + }); + }); + after(() => { + baseFs = undefined!; + sys = undefined!; + editsSys = undefined!; + }); + ts.verifyTscBaseline(() => ({ + baseLine: () => { + const { file, text } = sys.baseLine(); + const texts: string[] = [text]; + editsSys.forEach((sys, index) => { + const incrementalScenario = edits[index]; + texts.push(""); + texts.push(`Change:: ${incrementalScenario.subScenario}`); + texts.push(sys.baseLine().text); }); - edits.forEach(({ modifyFs, subScenario: editScenario, commandLineArgs: editCommandLineArgs }, index) => { - (editsSys || (editsSys = [])).push(ts.testTscCompile({ - scenario, - subScenario: editScenario || subScenario, - diffWithInitial: true, - fs: () => index === 0 ? sys.vfs : editsSys[index - 1].vfs, - commandLineArgs: editCommandLineArgs || commandLineArgs, - modifyFs, - baselineSourceMap, - baselineReadFileCalls, - baselinePrograms - })); + return { file, text: texts.join("\r\n") }; + } + })); + it("tsc invocation after edit and clean build correctness", () => { + let baselines: string[] | undefined; + for (let index = 0; index < edits.length; index++) { + baselines = verifyTscEditDiscrepancies({ + index, + scenario, + subScenario: edits[index].subScenario, + baselines, + baseFs, + newSys: editsSys[index], + commandLineArgs: edits[index].commandLineArgs || commandLineArgs, + discrepancyExplanation: edits[index].discrepancyExplanation, + editFs: fs => { + for (let i = 0; i <= index; i++) { + edits[i].modifyFs(fs); + } + }, + modifyFs }); - }); - after(() => { - baseFs = undefined!; - sys = undefined!; - editsSys = undefined!; - }); - ts.verifyTscBaseline(() => ({ - baseLine: () => { - const { file, text } = sys.baseLine(); - const texts: string[] = [text]; - editsSys.forEach((sys, index) => { - const incrementalScenario = edits[index]; - texts.push(""); - texts.push(`Change:: ${incrementalScenario.subScenario}`); - texts.push(sys.baseLine().text); - }); - return { file, text: texts.join("\r\n") }; - } - })); - it("tsc invocation after edit and clean build correctness", () => { - let baselines: string[] | undefined; - for (let index = 0; index < edits.length; index++) { - baselines = verifyTscEditDiscrepancies({ - index, - scenario, - subScenario: edits[index].subScenario, - baselines, - baseFs, - newSys: editsSys[index], - commandLineArgs: edits[index].commandLineArgs || commandLineArgs, - discrepancyExplanation: edits[index].discrepancyExplanation, - editFs: fs => { - for (let i = 0; i <= index; i++) { - edits[i].modifyFs(fs); - } - }, - modifyFs - }); - } - Harness.Baseline.runBaseline(`${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}-discrepancies.js`, baselines ? baselines.join("\r\n") : null // eslint-disable-line no-null/no-null - ); - }); + } + Harness.Baseline.runBaseline(`${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}-discrepancies.js`, baselines ? baselines.join("\r\n") : null // eslint-disable-line no-null/no-null + ); }); - } + }); +} - export function enableStrict(fs: vfs.FileSystem, path: string) { - replaceText(fs, path, `"strict": false`, `"strict": true`); - } +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 addTestPrologue(fs: vfs.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: vfs.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() { }`; - } +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 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 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 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 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} +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 ${project}${file}Spread(...b: number[]) { } const ${project}${file}_ar = [20, 30]; ${project}${file}Spread(10, ...${project}${file}_ar);`); - 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 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 addTripleSlashRef(fs: vfs.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 bb897438dfe75..1c191779a4c04 100644 --- a/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts +++ b/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts @@ -1,89 +1,89 @@ namespace ts { - describe("unittests:: tsbuild:: inferredTypeFromTransitiveModule::", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/inferredTypeFromTransitiveModule"); - }); - after(() => { - projFs = undefined!; - }); +describe("unittests:: tsbuild:: inferredTypeFromTransitiveModule::", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/inferredTypeFromTransitiveModule"); + }); + after(() => { + projFs = undefined!; + }); - ts.verifyTscWithEdits({ - scenario: "inferredTypeFromTransitiveModule", - subScenario: "inferred type from transitive module", - fs: () => projFs, - commandLineArgs: ["--b", "/src", "--verbose"], - edits: [ - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParam, - }, - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParamBack, - }, - ], - }); + ts.verifyTscWithEdits({ + scenario: "inferredTypeFromTransitiveModule", + subScenario: "inferred type from transitive module", + fs: () => projFs, + commandLineArgs: ["--b", "/src", "--verbose"], + edits: [ + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParam, + }, + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParamBack, + }, + ], + }); - ts.verifyTscWithEdits({ - subScenario: "inferred type from transitive module with isolatedModules", - fs: () => projFs, - scenario: "inferredTypeFromTransitiveModule", - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: changeToIsolatedModules, - edits: [ - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParam - }, - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParamBack, - }, - ] - }); + ts.verifyTscWithEdits({ + subScenario: "inferred type from transitive module with isolatedModules", + fs: () => projFs, + scenario: "inferredTypeFromTransitiveModule", + commandLineArgs: ["--b", "/src", "--verbose"], + modifyFs: changeToIsolatedModules, + edits: [ + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParam + }, + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParamBack, + }, + ] + }); - ts.verifyTscWithEdits({ - scenario: "inferredTypeFromTransitiveModule", - subScenario: "reports errors in files affected by change in signature with isolatedModules", - fs: () => projFs, - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: fs => { - changeToIsolatedModules(fs); - ts.appendText(fs, "/src/lazyIndex.ts", ` + ts.verifyTscWithEdits({ + scenario: "inferredTypeFromTransitiveModule", + subScenario: "reports errors in files affected by change in signature with isolatedModules", + fs: () => projFs, + commandLineArgs: ["--b", "/src", "--verbose"], + modifyFs: fs => { + changeToIsolatedModules(fs); + ts.appendText(fs, "/src/lazyIndex.ts", ` import { default as bar } from './bar'; bar("hello");`); + }, + edits: [ + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParam + }, + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParamBack, + }, + { + subScenario: "incremental-declaration-changes", + modifyFs: changeBarParam + }, + { + subScenario: "Fix Error", + modifyFs: fs => ts.replaceText(fs, "/src/lazyIndex.ts", `bar("hello")`, "bar()") }, - edits: [ - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParam - }, - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParamBack, - }, - { - subScenario: "incremental-declaration-changes", - modifyFs: changeBarParam - }, - { - subScenario: "Fix Error", - modifyFs: fs => ts.replaceText(fs, "/src/lazyIndex.ts", `bar("hello")`, "bar()") - }, - ] - }); + ] }); +}); - function changeToIsolatedModules(fs: vfs.FileSystem) { - ts.replaceText(fs, "/src/tsconfig.json", `"incremental": true`, `"incremental": true, "isolatedModules": true`); - } +function changeToIsolatedModules(fs: vfs.FileSystem) { + ts.replaceText(fs, "/src/tsconfig.json", `"incremental": true`, `"incremental": true, "isolatedModules": true`); +} - function changeBarParam(fs: vfs.FileSystem) { - ts.replaceText(fs, "/src/bar.ts", "param: string", ""); - } +function changeBarParam(fs: vfs.FileSystem) { + ts.replaceText(fs, "/src/bar.ts", "param: string", ""); +} - function changeBarParamBack(fs: vfs.FileSystem) { - ts.replaceText(fs, "/src/bar.ts", "foobar()", "foobar(param: string)"); - } +function changeBarParamBack(fs: vfs.FileSystem) { + ts.replaceText(fs, "/src/bar.ts", "foobar()", "foobar(param: string)"); +} } diff --git a/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts b/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts index 9b5c626ad8b07..a022d54f8f2be 100644 --- a/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts +++ b/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts @@ -1,17 +1,17 @@ namespace ts { - describe("unittests:: tsbuild:: javascriptProjectEmit::", () => { - ts.verifyTsc({ - scenario: "javascriptProjectEmit", - subScenario: `loads js-based projects and emits them correctly`, - fs: () => ts.loadProjectFromFiles({ - "/src/common/nominal.js": Utils.dedent` +describe("unittests:: tsbuild:: javascriptProjectEmit::", () => { + ts.verifyTsc({ + scenario: "javascriptProjectEmit", + subScenario: `loads js-based projects and emits them correctly`, + fs: () => ts.loadProjectFromFiles({ + "/src/common/nominal.js": Utils.dedent` /** * @template T, Name * @typedef {T & {[Symbol.species]: Name}} Nominal */ module.exports = {}; `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -19,14 +19,14 @@ namespace ts { }, "include": ["nominal.js"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": Utils.dedent` import { Nominal } from '../common/nominal'; /** * @typedef {Nominal} MyNominal */ `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -37,7 +37,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": Utils.dedent` import { MyNominal } from '../sub-project/index'; const variable = { @@ -51,7 +51,7 @@ namespace ts { return 'key'; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -62,7 +62,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": Utils.dedent` { "compilerOptions": { "composite": true @@ -73,7 +73,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": Utils.dedent` { "compilerOptions": { "skipLibCheck": true, @@ -84,21 +84,21 @@ namespace ts { "declaration": true } }`, - }, ts.symbolLibContent), - commandLineArgs: ["-b", "/src"] - }); + }, ts.symbolLibContent), + commandLineArgs: ["-b", "/src"] + }); - ts.verifyTscWithEdits({ - scenario: "javascriptProjectEmit", - subScenario: `modifies outfile js projects and concatenates them correctly`, - fs: () => ts.loadProjectFromFiles({ - "/src/common/nominal.js": Utils.dedent` + ts.verifyTscWithEdits({ + scenario: "javascriptProjectEmit", + subScenario: `modifies outfile js projects and concatenates them correctly`, + fs: () => ts.loadProjectFromFiles({ + "/src/common/nominal.js": Utils.dedent` /** * @template T, Name * @typedef {T & {[Symbol.species]: Name}} Nominal */ `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -108,13 +108,13 @@ namespace ts { }, "include": ["nominal.js"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": Utils.dedent` /** * @typedef {Nominal} MyNominal */ const c = /** @type {*} */(null); `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -127,7 +127,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": Utils.dedent` const variable = { key: /** @type {MyNominal} */('value'), }; @@ -139,7 +139,7 @@ namespace ts { return 'key'; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -152,7 +152,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": Utils.dedent` { "compilerOptions": { "composite": true, @@ -164,7 +164,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": Utils.dedent` { "compilerOptions": { "skipLibCheck": true, @@ -174,27 +174,27 @@ namespace ts { "declaration": true } }`, - }, ts.symbolLibContent), - commandLineArgs: ["-b", "/src"], - edits: [{ - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.replaceText(fs, "/src/sub-project/index.js", "null", "undefined") - }] - }); + }, ts.symbolLibContent), + commandLineArgs: ["-b", "/src"], + edits: [{ + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.replaceText(fs, "/src/sub-project/index.js", "null", "undefined") + }] + }); - ts.verifyTsc({ - scenario: "javascriptProjectEmit", - subScenario: `loads js-based projects with non-moved json files and emits them correctly`, - fs: () => ts.loadProjectFromFiles({ - "/src/common/obj.json": Utils.dedent` + ts.verifyTsc({ + scenario: "javascriptProjectEmit", + subScenario: `loads js-based projects with non-moved json files and emits them correctly`, + fs: () => ts.loadProjectFromFiles({ + "/src/common/obj.json": Utils.dedent` { "val": 42 }`, - "/src/common/index.ts": Utils.dedent` + "/src/common/index.ts": Utils.dedent` import x = require("./obj.json"); export = x; `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -203,12 +203,12 @@ namespace ts { }, "include": ["index.ts", "obj.json"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": Utils.dedent` import mod from '../common'; export const m = mod; `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -219,7 +219,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": Utils.dedent` import { m } from '../sub-project/index'; const variable = { @@ -230,7 +230,7 @@ namespace ts { return variable; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": Utils.dedent` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -241,7 +241,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": Utils.dedent` { "compilerOptions": { "composite": true @@ -252,7 +252,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": Utils.dedent` { "compilerOptions": { "skipLibCheck": true, @@ -265,8 +265,8 @@ namespace ts { "declaration": true } }`, - }, ts.symbolLibContent), - commandLineArgs: ["-b", "/src"] - }); + }, ts.symbolLibContent), + commandLineArgs: ["-b", "/src"] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts b/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts index 85a1b344c8831..f689ce1cf000b 100644 --- a/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts +++ b/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts @@ -1,20 +1,20 @@ namespace ts { - describe("unittests:: tsbuild:: lateBoundSymbol:: interface is merged and contains late bound member", () => { - ts.verifyTscWithEdits({ - subScenario: "interface is merged and contains late bound member", - fs: () => ts.loadProjectFromDisk("tests/projects/lateBoundSymbol"), - scenario: "lateBoundSymbol", - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], - edits: [ - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.replaceText(fs, "/src/src/main.ts", "const x = 10;", ""), - }, - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.appendText(fs, "/src/src/main.ts", "const x = 10;"), - }, - ] - }); +describe("unittests:: tsbuild:: lateBoundSymbol:: interface is merged and contains late bound member", () => { + ts.verifyTscWithEdits({ + subScenario: "interface is merged and contains late bound member", + fs: () => ts.loadProjectFromDisk("tests/projects/lateBoundSymbol"), + scenario: "lateBoundSymbol", + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], + edits: [ + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.replaceText(fs, "/src/src/main.ts", "const x = 10;", ""), + }, + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.appendText(fs, "/src/src/main.ts", "const x = 10;"), + }, + ] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/moduleResolution.ts b/src/testRunner/unittests/tsbuild/moduleResolution.ts index 320e77163e1fd..15b38f41b7f8d 100644 --- a/src/testRunner/unittests/tsbuild/moduleResolution.ts +++ b/src/testRunner/unittests/tsbuild/moduleResolution.ts @@ -1,325 +1,325 @@ namespace ts.tscWatch { - describe("unittests:: tsbuild:: moduleResolution:: handles the modules and options from referenced project correctly", () => { - function sys(optionsToExtend?: ts.CompilerOptions) { - return ts.tscWatch.createWatchedSystem([ - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, - content: Utils.dedent` +describe("unittests:: tsbuild:: moduleResolution:: handles the modules and options from referenced project correctly", () => { + function sys(optionsToExtend?: ts.CompilerOptions) { + return ts.tscWatch.createWatchedSystem([ + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, + content: Utils.dedent` import type { TheNum } from 'pkg2' export const theNum: TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { outDir: "build", ...optionsToExtend }, - references: [{ path: "../pkg2" }] - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.ts`, - content: `export type TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, - content: `export type { TheNum } from 'const';` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "build", - baseUrl: ".", - ...optionsToExtend - } - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, - content: JSON.stringify({ - name: "pkg2", - version: "1.0.0", - main: "build/index.js", - }) - }, - { - path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, - symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, - }, - ts.tscWatch.libFile - ], { currentDirectory: ts.tscWatch.projectRoot }); - } + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { outDir: "build", ...optionsToExtend }, + references: [{ path: "../pkg2" }] + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.ts`, + content: `export type TheNum = 42;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, + content: `export type { TheNum } from 'const';` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "build", + baseUrl: ".", + ...optionsToExtend + } + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, + content: JSON.stringify({ + name: "pkg2", + version: "1.0.0", + main: "build/index.js", + }) + }, + { + path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, + symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, + }, + ts.tscWatch.libFile + ], { currentDirectory: ts.tscWatch.projectRoot }); + } - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolution", - subScenario: `resolves specifier in output declaration file from referenced project correctly`, - sys, - commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], - changes: ts.emptyArray - }); + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolution", + subScenario: `resolves specifier in output declaration file from referenced project correctly`, + sys, + commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolution", - subScenario: `resolves specifier in output declaration file from referenced project correctly with preserveSymlinks`, - sys: () => sys({ preserveSymlinks: true }), - commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], - changes: ts.emptyArray - }); + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolution", + subScenario: `resolves specifier in output declaration file from referenced project correctly with preserveSymlinks`, + sys: () => sys({ preserveSymlinks: true }), + commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolution", - subScenario: `resolves specifier in output declaration file from referenced project correctly with cts and mts extensions`, - sys: () => ts.tscWatch.createWatchedSystem([ - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, - content: JSON.stringify({ - name: "pkg1", - version: "1.0.0", - main: "build/index.js", - type: "module" - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, - content: Utils.dedent` + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolution", + subScenario: `resolves specifier in output declaration file from referenced project correctly with cts and mts extensions`, + sys: () => ts.tscWatch.createWatchedSystem([ + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, + content: JSON.stringify({ + name: "pkg1", + version: "1.0.0", + main: "build/index.js", + type: "module" + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, + content: Utils.dedent` import type { TheNum } from 'pkg2' export const theNum: TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - outDir: "build", - module: "node16", - }, - references: [{ path: "../pkg2" }] - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.cts`, - content: `export type TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, - content: `export type { TheNum } from './const.cjs';` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "build", - module: "node16", - } - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, - content: JSON.stringify({ - name: "pkg2", - version: "1.0.0", - main: "build/index.js", - type: "module" - }) - }, - { - path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, - symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, - }, - { ...ts.tscWatch.libFile, path: `/a/lib/lib.es2022.full.d.ts` } - ], { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["-b", "packages/pkg1", "-w", "--verbose", "--traceResolution"], - changes: [ - { - caption: "reports import errors after change to package file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"module"`, `"commonjs"`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "removes those errors when a package file is changed back", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"commonjs"`, `"module"`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "reports import errors after change to package file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"module"`, `"commonjs"`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "removes those errors when a package file is changed to cjs extensions", - change: sys => { - ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `"build/index.js"`, `"build/index.cjs"`); - sys.renameFile(`${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, `${ts.tscWatch.projectRoot}/packages/pkg2/index.cts`); + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "build", + module: "node16", }, - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + references: [{ path: "../pkg2" }] + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.cts`, + content: `export type TheNum = 42;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, + content: `export type { TheNum } from './const.cjs';` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "build", + module: "node16", + } + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, + content: JSON.stringify({ + name: "pkg2", + version: "1.0.0", + main: "build/index.js", + type: "module" + }) + }, + { + path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, + symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, + }, + { ...ts.tscWatch.libFile, path: `/a/lib/lib.es2022.full.d.ts` } + ], { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["-b", "packages/pkg1", "-w", "--verbose", "--traceResolution"], + changes: [ + { + caption: "reports import errors after change to package file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"module"`, `"commonjs"`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "removes those errors when a package file is changed back", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"commonjs"`, `"module"`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "reports import errors after change to package file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, `"module"`, `"commonjs"`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "removes those errors when a package file is changed to cjs extensions", + change: sys => { + ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `"build/index.js"`, `"build/index.cjs"`); + sys.renameFile(`${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, `${ts.tscWatch.projectRoot}/packages/pkg2/index.cts`); + }, + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - ts.verifyTsc({ - scenario: "moduleResolution", - subScenario: `type reference resolution uses correct options for different resolution options referenced project`, - fs: () => ts.loadProjectFromFiles({ - "/src/packages/pkg1_index.ts": `export const theNum: TheNum = "type1";`, - "/src/packages/pkg1.tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true, typeRoots: ["./typeroot1"] }, - files: ["./pkg1_index.ts"] - }), - "/src/packages/typeroot1/sometype/index.d.ts": Utils.dedent`declare type TheNum = "type1";`, - "/src/packages/pkg2_index.ts": `export const theNum: TheNum2 = "type2";`, - "/src/packages/pkg2.tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true, typeRoots: ["./typeroot2"] }, - files: ["./pkg2_index.ts"] - }), - "/src/packages/typeroot2/sometype/index.d.ts": Utils.dedent`declare type TheNum2 = "type2";`, + ts.verifyTsc({ + scenario: "moduleResolution", + subScenario: `type reference resolution uses correct options for different resolution options referenced project`, + fs: () => ts.loadProjectFromFiles({ + "/src/packages/pkg1_index.ts": `export const theNum: TheNum = "type1";`, + "/src/packages/pkg1.tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, typeRoots: ["./typeroot1"] }, + files: ["./pkg1_index.ts"] + }), + "/src/packages/typeroot1/sometype/index.d.ts": Utils.dedent`declare type TheNum = "type1";`, + "/src/packages/pkg2_index.ts": `export const theNum: TheNum2 = "type2";`, + "/src/packages/pkg2.tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, typeRoots: ["./typeroot2"] }, + files: ["./pkg2_index.ts"] }), - commandLineArgs: ["-b", "/src/packages/pkg1.tsconfig.json", "/src/packages/pkg2.tsconfig.json", "--verbose", "--traceResolution"], - }); + "/src/packages/typeroot2/sometype/index.d.ts": Utils.dedent`declare type TheNum2 = "type2";`, + }), + commandLineArgs: ["-b", "/src/packages/pkg1.tsconfig.json", "/src/packages/pkg2.tsconfig.json", "--verbose", "--traceResolution"], + }); - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolution", - subScenario: `watches for changes to package-json main fields`, - sys: () => ts.tscWatch.createWatchedSystem([ - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, - content: JSON.stringify({ - name: "pkg1", - version: "1.0.0", - main: "build/index.js", - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, - content: Utils.dedent` + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolution", + subScenario: `watches for changes to package-json main fields`, + sys: () => ts.tscWatch.createWatchedSystem([ + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, + content: JSON.stringify({ + name: "pkg1", + version: "1.0.0", + main: "build/index.js", + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, + content: Utils.dedent` import type { TheNum } from 'pkg2' export const theNum: TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - outDir: "build", - }, - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/const.d.ts`, - content: `export type TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/index.d.ts`, - content: `export type { TheNum } from './const.js';` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/other.d.ts`, - content: `export type TheStr = string;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, - content: JSON.stringify({ - name: "pkg2", - version: "1.0.0", - main: "build/index.js", - }) - }, - { - path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, - symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, - }, - ts.tscWatch.libFile - ], { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["--project", "./packages/pkg1/tsconfig.json", "-w", "--traceResolution"], - changes: [ - { - caption: "reports import errors after change to package file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `index.js`, `other.js`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "removes those errors when a package file is changed back", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `other.js`, `index.js`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "build", + }, + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/const.d.ts`, + content: `export type TheNum = 42;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/index.d.ts`, + content: `export type { TheNum } from './const.js';` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/build/other.d.ts`, + content: `export type TheStr = string;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, + content: JSON.stringify({ + name: "pkg2", + version: "1.0.0", + main: "build/index.js", + }) + }, + { + path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, + symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, + }, + ts.tscWatch.libFile + ], { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["--project", "./packages/pkg1/tsconfig.json", "-w", "--traceResolution"], + changes: [ + { + caption: "reports import errors after change to package file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `index.js`, `other.js`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "removes those errors when a package file is changed back", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `other.js`, `index.js`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolution", - subScenario: `build mode watches for changes to package-json main fields`, - sys: () => ts.tscWatch.createWatchedSystem([ - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, - content: JSON.stringify({ - name: "pkg1", - version: "1.0.0", - main: "build/index.js", - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, - content: Utils.dedent` + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolution", + subScenario: `build mode watches for changes to package-json main fields`, + sys: () => ts.tscWatch.createWatchedSystem([ + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/package.json`, + content: JSON.stringify({ + name: "pkg1", + version: "1.0.0", + main: "build/index.js", + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/index.ts`, + content: Utils.dedent` import type { TheNum } from 'pkg2' export const theNum: TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - outDir: "build", - }, - references: [{ path: "../pkg2" }] - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "build", - baseUrl: ".", - } - }) - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.ts`, - content: `export type TheNum = 42;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, - content: `export type { TheNum } from './const.js';` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/other.ts`, - content: `export type TheStr = string;` - }, - { - path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, - content: JSON.stringify({ - name: "pkg2", - version: "1.0.0", - main: "build/index.js", - }) - }, - { - path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, - symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, - }, - ts.tscWatch.libFile - ], { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["-b", "packages/pkg1", "--verbose", "-w", "--traceResolution"], - changes: [ - { - caption: "reports import errors after change to package file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `index.js`, `other.js`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "removes those errors when a package file is changed back", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `other.js`, `index.js`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "build", + }, + references: [{ path: "../pkg2" }] + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "build", + baseUrl: ".", + } + }) + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/const.ts`, + content: `export type TheNum = 42;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/index.ts`, + content: `export type { TheNum } from './const.js';` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/other.ts`, + content: `export type TheStr = string;` + }, + { + path: `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, + content: JSON.stringify({ + name: "pkg2", + version: "1.0.0", + main: "build/index.js", + }) + }, + { + path: `${ts.tscWatch.projectRoot}/node_modules/pkg2`, + symLink: `${ts.tscWatch.projectRoot}/packages/pkg2`, + }, + ts.tscWatch.libFile + ], { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["-b", "packages/pkg1", "--verbose", "-w", "--traceResolution"], + changes: [ + { + caption: "reports import errors after change to package file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `index.js`, `other.js`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "removes those errors when a package file is changed back", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/packages/pkg2/package.json`, `other.js`, `index.js`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts b/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts index e11f48140032a..9069187a5d48b 100644 --- a/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts +++ b/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts @@ -1,16 +1,16 @@ namespace ts { - // https://github.com/microsoft/TypeScript/issues/31696 - describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers to referenced projects resolve correctly", () => { - ts.verifyTsc({ - scenario: "moduleSpecifiers", - subScenario: `synthesized module specifiers resolve correctly`, - fs: () => ts.loadProjectFromFiles({ - "/src/solution/common/nominal.ts": Utils.dedent` +// https://github.com/microsoft/TypeScript/issues/31696 +describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers to referenced projects resolve correctly", () => { + ts.verifyTsc({ + scenario: "moduleSpecifiers", + subScenario: `synthesized module specifiers resolve correctly`, + fs: () => ts.loadProjectFromFiles({ + "/src/solution/common/nominal.ts": Utils.dedent` export declare type Nominal = T & { [Symbol.species]: Name; }; `, - "/src/solution/common/tsconfig.json": Utils.dedent` + "/src/solution/common/tsconfig.json": Utils.dedent` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -18,12 +18,12 @@ namespace ts { }, "include": ["nominal.ts"] }`, - "/src/solution/sub-project/index.ts": Utils.dedent` + "/src/solution/sub-project/index.ts": Utils.dedent` import { Nominal } from '../common/nominal'; export type MyNominal = Nominal; `, - "/src/solution/sub-project/tsconfig.json": Utils.dedent` + "/src/solution/sub-project/tsconfig.json": Utils.dedent` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -34,7 +34,7 @@ namespace ts { ], "include": ["./index.ts"] }`, - "/src/solution/sub-project-2/index.ts": Utils.dedent` + "/src/solution/sub-project-2/index.ts": Utils.dedent` import { MyNominal } from '../sub-project/index'; const variable = { @@ -45,7 +45,7 @@ namespace ts { return 'key'; } `, - "/src/solution/sub-project-2/tsconfig.json": Utils.dedent` + "/src/solution/sub-project-2/tsconfig.json": Utils.dedent` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -56,7 +56,7 @@ namespace ts { ], "include": ["./index.ts"] }`, - "/src/solution/tsconfig.json": Utils.dedent` + "/src/solution/tsconfig.json": Utils.dedent` { "compilerOptions": { "composite": true @@ -67,7 +67,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": Utils.dedent` { "compilerOptions": { "skipLibCheck": true, @@ -75,7 +75,7 @@ namespace ts { "outDir": "lib", } }`, - "/src/tsconfig.json": Utils.dedent`{ + "/src/tsconfig.json": Utils.dedent`{ "compilerOptions": { "composite": true }, @@ -84,35 +84,35 @@ namespace ts { ], "include": [] }` - }, ts.symbolLibContent), - commandLineArgs: ["-b", "/src", "--verbose"] - }); + }, ts.symbolLibContent), + commandLineArgs: ["-b", "/src", "--verbose"] }); +}); - // https://github.com/microsoft/TypeScript/issues/44434 but with `module: node16`, some `exports` maps blocking direct access, and no `baseUrl` - describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers across referenced projects resolve correctly", () => { - ts.verifyTsc({ - scenario: "moduleSpecifiers", - subScenario: `synthesized module specifiers across projects resolve correctly`, - fs: () => ts.loadProjectFromFiles({ - "/src/src-types/index.ts": Utils.dedent` +// https://github.com/microsoft/TypeScript/issues/44434 but with `module: node16`, some `exports` maps blocking direct access, and no `baseUrl` +describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers across referenced projects resolve correctly", () => { + ts.verifyTsc({ + scenario: "moduleSpecifiers", + subScenario: `synthesized module specifiers across projects resolve correctly`, + fs: () => ts.loadProjectFromFiles({ + "/src/src-types/index.ts": Utils.dedent` export * from './dogconfig.js';`, - "/src/src-types/dogconfig.ts": Utils.dedent` + "/src/src-types/dogconfig.ts": Utils.dedent` export interface DogConfig { name: string; }`, - "/src/src-dogs/index.ts": Utils.dedent` + "/src/src-dogs/index.ts": Utils.dedent` export * from 'src-types'; export * from './lassie/lassiedog.js'; `, - "/src/src-dogs/dogconfig.ts": Utils.dedent` + "/src/src-dogs/dogconfig.ts": Utils.dedent` import { DogConfig } from 'src-types'; export const DOG_CONFIG: DogConfig = { name: 'Default dog', }; `, - "/src/src-dogs/dog.ts": Utils.dedent` + "/src/src-dogs/dog.ts": Utils.dedent` import { DogConfig } from 'src-types'; import { DOG_CONFIG } from './dogconfig.js'; @@ -123,7 +123,7 @@ namespace ts { } } `, - "/src/src-dogs/lassie/lassiedog.ts": Utils.dedent` + "/src/src-dogs/lassie/lassiedog.ts": Utils.dedent` import { Dog } from '../dog.js'; import { LASSIE_CONFIG } from './lassieconfig.js'; @@ -131,29 +131,29 @@ namespace ts { protected static getDogConfig = () => LASSIE_CONFIG; } `, - "/src/src-dogs/lassie/lassieconfig.ts": Utils.dedent` + "/src/src-dogs/lassie/lassieconfig.ts": Utils.dedent` import { DogConfig } from 'src-types'; export const LASSIE_CONFIG: DogConfig = { name: 'Lassie' }; `, - "/src/tsconfig-base.json": Utils.dedent` + "/src/tsconfig-base.json": Utils.dedent` { "compilerOptions": { "declaration": true, "module": "node16" } }`, - "/src/src-types/package.json": Utils.dedent` + "/src/src-types/package.json": Utils.dedent` { "type": "module", "exports": "./index.js" }`, - "/src/src-dogs/package.json": Utils.dedent` + "/src/src-dogs/package.json": Utils.dedent` { "type": "module", "exports": "./index.js" }`, - "/src/src-types/tsconfig.json": Utils.dedent` + "/src/src-types/tsconfig.json": Utils.dedent` { "extends": "../tsconfig-base.json", "compilerOptions": { @@ -163,7 +163,7 @@ namespace ts { "**/*" ] }`, - "/src/src-dogs/tsconfig.json": Utils.dedent` + "/src/src-dogs/tsconfig.json": Utils.dedent` { "extends": "../tsconfig-base.json", "compilerOptions": { @@ -176,13 +176,13 @@ namespace ts { "**/*" ] }`, - }, ""), - modifyFs: fs => { - fs.writeFileSync("/lib/lib.es2022.full.d.ts", ts.tscWatch.libFile.content); - fs.symlinkSync("/src", "/src/src-types/node_modules"); - fs.symlinkSync("/src", "/src/src-dogs/node_modules"); - }, - commandLineArgs: ["-b", "src/src-types", "src/src-dogs", "--verbose"] - }); + }, ""), + modifyFs: fs => { + fs.writeFileSync("/lib/lib.es2022.full.d.ts", ts.tscWatch.libFile.content); + fs.symlinkSync("/src", "/src/src-types/node_modules"); + fs.symlinkSync("/src", "/src/src-dogs/node_modules"); + }, + commandLineArgs: ["-b", "src/src-types", "src/src-dogs", "--verbose"] }); +}); } diff --git a/src/testRunner/unittests/tsbuild/noEmitOnError.ts b/src/testRunner/unittests/tsbuild/noEmitOnError.ts index 4943b2e6928ca..9fbf626e2e8ba 100644 --- a/src/testRunner/unittests/tsbuild/noEmitOnError.ts +++ b/src/testRunner/unittests/tsbuild/noEmitOnError.ts @@ -1,88 +1,88 @@ namespace ts { - describe("unittests:: tsbuild - with noEmitOnError", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/noEmitOnError"); - }); - after(() => { - projFs = undefined!; - }); +describe("unittests:: tsbuild - with noEmitOnError", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/noEmitOnError"); + }); + after(() => { + projFs = undefined!; + }); - ts.verifyTscWithEdits({ - scenario: "noEmitOnError", - subScenario: "syntax errors", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json"], - edits: [ - ts.noChangeRun, - { - subScenario: "Fix error", - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + ts.verifyTscWithEdits({ + scenario: "noEmitOnError", + subScenario: "syntax errors", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json"], + edits: [ + ts.noChangeRun, + { + subScenario: "Fix error", + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`, "utf-8"), - }, - ts.noChangeRun, - ], - baselinePrograms: true, - }); + }, + ts.noChangeRun, + ], + baselinePrograms: true, + }); - ts.verifyTscWithEdits({ - scenario: "noEmitOnError", - subScenario: "syntax errors with incremental", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--incremental"], - edits: [ - ts.noChangeRun, - { - subScenario: "Fix error", - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + ts.verifyTscWithEdits({ + scenario: "noEmitOnError", + subScenario: "syntax errors with incremental", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--incremental"], + edits: [ + ts.noChangeRun, + { + subScenario: "Fix error", + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`, "utf-8"), - discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation, - }, - ts.noChangeWithExportsDiscrepancyRun, - ], - baselinePrograms: true, - }); + discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation, + }, + ts.noChangeWithExportsDiscrepancyRun, + ], + baselinePrograms: true, + }); - ts.verifyTscWithEdits({ - scenario: "noEmitOnError", - subScenario: "semantic errors", - fs: () => projFs, - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + ts.verifyTscWithEdits({ + scenario: "noEmitOnError", + subScenario: "semantic errors", + fs: () => projFs, + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = 10;`, "utf-8"), - commandLineArgs: ["--b", "/src/tsconfig.json"], - edits: [ - ts.noChangeRun, - { - subScenario: "Fix error", - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + commandLineArgs: ["--b", "/src/tsconfig.json"], + edits: [ + ts.noChangeRun, + { + subScenario: "Fix error", + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = "hello";`, "utf-8"), - }, - ts.noChangeRun, - ], - baselinePrograms: true, - }); + }, + ts.noChangeRun, + ], + baselinePrograms: true, + }); - ts.verifyTscWithEdits({ - scenario: "noEmitOnError", - subScenario: "semantic errors with incremental", - fs: () => projFs, - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + ts.verifyTscWithEdits({ + scenario: "noEmitOnError", + subScenario: "semantic errors with incremental", + fs: () => projFs, + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = 10;`, "utf-8"), - commandLineArgs: ["--b", "/src/tsconfig.json", "--incremental"], - edits: [ - ts.noChangeWithExportsDiscrepancyRun, - { - subScenario: "Fix error", - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + commandLineArgs: ["--b", "/src/tsconfig.json", "--incremental"], + edits: [ + ts.noChangeWithExportsDiscrepancyRun, + { + subScenario: "Fix error", + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = "hello";`, "utf-8"), - }, - ts.noChangeRun, - ], - baselinePrograms: true, - }); + }, + ts.noChangeRun, + ], + baselinePrograms: true, }); +}); } diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts index 23f3ce47149fd..3ab161b66cecb 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -1,371 +1,371 @@ namespace ts { - describe("unittests:: tsbuild:: outFile::", () => { - let outFileFs: vfs.FileSystem; - let outFileWithBuildFs: vfs.FileSystem; - before(() => { - outFileFs = ts.loadProjectFromDisk("tests/projects/outfile-concat"); - }); - after(() => { - outFileFs = undefined!; - outFileWithBuildFs = undefined!; - }); +describe("unittests:: tsbuild:: outFile::", () => { + let outFileFs: vfs.FileSystem; + let outFileWithBuildFs: vfs.FileSystem; + before(() => { + outFileFs = ts.loadProjectFromDisk("tests/projects/outfile-concat"); + }); + after(() => { + outFileFs = undefined!; + outFileWithBuildFs = undefined!; + }); - interface VerifyOutFileScenarioInput { - subScenario: string; - modifyFs?: (fs: vfs.FileSystem) => void; - modifyAgainFs?: (fs: vfs.FileSystem) => void; - ignoreDtsChanged?: true; - ignoreDtsUnchanged?: true; - baselineOnly?: true; - additionalCommandLineArgs?: string[]; - } + interface VerifyOutFileScenarioInput { + subScenario: string; + modifyFs?: (fs: vfs.FileSystem) => void; + modifyAgainFs?: (fs: vfs.FileSystem) => void; + ignoreDtsChanged?: true; + ignoreDtsUnchanged?: true; + baselineOnly?: true; + additionalCommandLineArgs?: string[]; + } - function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs, ignoreDtsChanged, ignoreDtsUnchanged, baselineOnly, additionalCommandLineArgs, }: VerifyOutFileScenarioInput) { - const edits: ts.TestTscEdit[] = []; - if (!ignoreDtsChanged) { - edits.push({ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/first/first_PART1.ts", "Hello", "Hola"), - }); - } - if (!ignoreDtsUnchanged) { - edits.push({ - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), - }); - } - if (modifyAgainFs) { - edits.push({ - subScenario: "incremental-headers-change-without-dts-changes", - modifyFs: modifyAgainFs - }); - } - const input: ts.VerifyTscWithEditsInput = { - subScenario, - fs: () => outFileFs, - scenario: "outfile-concat", - commandLineArgs: ["--b", "/src/third", "--verbose", ...(additionalCommandLineArgs || [])], - baselineSourceMap: true, - modifyFs, - baselineReadFileCalls: !baselineOnly, - edits, - }; - return edits.length ? - ts.verifyTscWithEdits(input) : - ts.verifyTsc(input); + function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs, ignoreDtsChanged, ignoreDtsUnchanged, baselineOnly, additionalCommandLineArgs, }: VerifyOutFileScenarioInput) { + const edits: ts.TestTscEdit[] = []; + if (!ignoreDtsChanged) { + edits.push({ + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.replaceText(fs, "/src/first/first_PART1.ts", "Hello", "Hola"), + }); + } + if (!ignoreDtsUnchanged) { + edits.push({ + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), + }); } + if (modifyAgainFs) { + edits.push({ + subScenario: "incremental-headers-change-without-dts-changes", + modifyFs: modifyAgainFs + }); + } + const input: ts.VerifyTscWithEditsInput = { + subScenario, + fs: () => outFileFs, + scenario: "outfile-concat", + commandLineArgs: ["--b", "/src/third", "--verbose", ...(additionalCommandLineArgs || [])], + baselineSourceMap: true, + modifyFs, + baselineReadFileCalls: !baselineOnly, + edits, + }; + return edits.length ? + ts.verifyTscWithEdits(input) : + ts.verifyTsc(input); + } - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "baseline sectioned sourcemaps", - }); + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "baseline sectioned sourcemaps", + }); - verifyOutFileScenario({ - subScenario: "explainFiles", - additionalCommandLineArgs: ["--explainFiles"], - baselineOnly: true - }); + verifyOutFileScenario({ + subScenario: "explainFiles", + additionalCommandLineArgs: ["--explainFiles"], + baselineOnly: true + }); - // Verify baseline with build info + dts unChanged - verifyOutFileScenario({ - subScenario: "when final project is not composite but uses project references", - modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify baseline with build info + dts unChanged + verifyOutFileScenario({ + subScenario: "when final project is not composite but uses project references", + modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), + ignoreDtsChanged: true, + baselineOnly: true + }); - // Verify baseline with build info - verifyOutFileScenario({ - subScenario: "when final project is not composite but incremental", - modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"incremental": true,`), - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); + // Verify baseline with build info + verifyOutFileScenario({ + subScenario: "when final project is not composite but incremental", + modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"incremental": true,`), + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); - // Verify baseline with build info - verifyOutFileScenario({ - subScenario: "when final project specifies tsBuildInfoFile", - modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"composite": true, + // Verify baseline with build info + verifyOutFileScenario({ + subScenario: "when final project specifies tsBuildInfoFile", + modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"composite": true, "tsBuildInfoFile": "./thirdjs/output/third.tsbuildinfo",`), - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); - - function getOutFileFsAfterBuild() { - if (outFileWithBuildFs) - return outFileWithBuildFs; - const fs = outFileFs.shadow(); - const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }); - const host = ts.createSolutionBuilderHostForBaseline(sys as ts.TscCompileSystem); - const builder = ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true }); - builder.build(); - fs.makeReadonly(); - return outFileWithBuildFs = fs; - } - - ts.verifyTscWithEdits({ - scenario: "outFile", - subScenario: "clean projects", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--b", "/src/third", "--clean"], - edits: ts.noChangeOnlyRuns - }); + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); - ts.verifyTsc({ - scenario: "outFile", - subScenario: "verify buildInfo absence results in new build", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => fs.unlinkSync("/src/first/bin/first-output.tsbuildinfo"), - }); + function getOutFileFsAfterBuild() { + if (outFileWithBuildFs) + return outFileWithBuildFs; + const fs = outFileFs.shadow(); + const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }); + const host = ts.createSolutionBuilderHostForBaseline(sys as ts.TscCompileSystem); + const builder = ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true }); + builder.build(); + fs.makeReadonly(); + return outFileWithBuildFs = fs; + } - ts.verifyTsc({ - scenario: "outFile", - subScenario: "tsbuildinfo is not generated when incremental is set to false", - fs: () => outFileFs, - commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), - }); + ts.verifyTscWithEdits({ + scenario: "outFile", + subScenario: "clean projects", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--b", "/src/third", "--clean"], + edits: ts.noChangeOnlyRuns + }); - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "outFile", - subScenario: "rebuilds completely when version in tsbuildinfo doesnt match ts version", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--b", "/src/third", "--verbose"], - compile: sys => { - // Buildinfo will have version which does not match with current ts version - const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); - const builder = ts.createSolutionBuilder(buildHost, ["/src/third"], { verbose: true }); - sys.exit(builder.build()); - } - }); + ts.verifyTsc({ + scenario: "outFile", + subScenario: "verify buildInfo absence results in new build", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => fs.unlinkSync("/src/first/bin/first-output.tsbuildinfo"), + }); - ts.verifyTscWithEdits({ - scenario: "outFile", - subScenario: "rebuilds completely when command line incremental flag changes between non dts changes", - fs: () => outFileFs, - // Make non composite third project - modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), - // Build with command line incremental - commandLineArgs: ["--b", "/src/third", "--i", "--verbose"], - edits: [ - { - subScenario: "Make non incremental build with change in file that doesnt affect dts", - modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), - commandLineArgs: ["--b", "/src/third", "--verbose"], - }, - { - subScenario: "Make incremental build with change in file that doesnt affect dts", - modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), - commandLineArgs: ["--b", "/src/third", "--verbose", "--incremental"], - } - ] - }); + ts.verifyTsc({ + scenario: "outFile", + subScenario: "tsbuildinfo is not generated when incremental is set to false", + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), + }); - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "outFile", - subScenario: "builds till project specified", - fs: () => outFileFs, - commandLineArgs: ["--build", "/src/second/tsconfig.json"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); - sys.exit(builder.build("/src/second/tsconfig.json")); - } - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "outFile", + subScenario: "rebuilds completely when version in tsbuildinfo doesnt match ts version", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--b", "/src/third", "--verbose"], + compile: sys => { + // Buildinfo will have version which does not match with current ts version + const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); + const builder = ts.createSolutionBuilder(buildHost, ["/src/third"], { verbose: true }); + sys.exit(builder.build()); + } + }); - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "outFile", - subScenario: "cleans till project specified", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--build", "--clean", "/src/second/tsconfig.json"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], { verbose: true }); - sys.exit(builder.clean("/src/second/tsconfig.json")); + ts.verifyTscWithEdits({ + scenario: "outFile", + subScenario: "rebuilds completely when command line incremental flag changes between non dts changes", + fs: () => outFileFs, + // Make non composite third project + modifyFs: fs => ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""), + // Build with command line incremental + commandLineArgs: ["--b", "/src/third", "--i", "--verbose"], + edits: [ + { + subScenario: "Make non incremental build with change in file that doesnt affect dts", + modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), + commandLineArgs: ["--b", "/src/third", "--verbose"], + }, + { + subScenario: "Make incremental build with change in file that doesnt affect dts", + modifyFs: fs => ts.appendText(fs, "/src/first/first_PART1.ts", "console.log(s);"), + commandLineArgs: ["--b", "/src/third", "--verbose", "--incremental"], } - }); + ] + }); - describe("Prepend output with .tsbuildinfo", () => { - // Prologues - describe("Prologues", () => { - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "strict in all projects", - modifyFs: fs => { - ts.enableStrict(fs, "/src/first/tsconfig.json"); - ts.enableStrict(fs, "/src/second/tsconfig.json"); - ts.enableStrict(fs, "/src/third/tsconfig.json"); - }, - modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue"`) - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "outFile", + subScenario: "builds till project specified", + fs: () => outFileFs, + commandLineArgs: ["--build", "/src/second/tsconfig.json"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); + sys.exit(builder.build("/src/second/tsconfig.json")); + } + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "strict in one dependency", - modifyFs: fs => ts.enableStrict(fs, "/src/second/tsconfig.json"), - modifyAgainFs: fs => ts.addTestPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`), - ignoreDtsChanged: true, - baselineOnly: true - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "outFile", + subScenario: "cleans till project specified", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--build", "--clean", "/src/second/tsconfig.json"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], { verbose: true }); + sys.exit(builder.clean("/src/second/tsconfig.json")); + } + }); - // Verify initial + incremental edits - sourcemap verification - verifyOutFileScenario({ - subScenario: "multiple prologues in all projects", - modifyFs: fs => { - ts.enableStrict(fs, "/src/first/tsconfig.json"); - ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue"`); - ts.enableStrict(fs, "/src/second/tsconfig.json"); - ts.addTestPrologue(fs, "/src/second/second_part1.ts", `"myPrologue"`); - ts.addTestPrologue(fs, "/src/second/second_part2.ts", `"myPrologue2";`); - ts.enableStrict(fs, "/src/third/tsconfig.json"); - ts.addTestPrologue(fs, "/src/third/third_part1.ts", `"myPrologue";`); - ts.addTestPrologue(fs, "/src/third/third_part1.ts", `"myPrologue3";`); - }, - modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue5"`) - }); + describe("Prepend output with .tsbuildinfo", () => { + // Prologues + describe("Prologues", () => { + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "strict in all projects", + modifyFs: fs => { + ts.enableStrict(fs, "/src/first/tsconfig.json"); + ts.enableStrict(fs, "/src/second/tsconfig.json"); + ts.enableStrict(fs, "/src/third/tsconfig.json"); + }, + modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue"`) + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple prologues in different projects", - modifyFs: fs => { - ts.enableStrict(fs, "/src/first/tsconfig.json"); - ts.addTestPrologue(fs, "/src/second/second_part1.ts", `"myPrologue"`); - ts.addTestPrologue(fs, "/src/second/second_part2.ts", `"myPrologue2";`); - ts.enableStrict(fs, "/src/third/tsconfig.json"); - }, - modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue5"`), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "strict in one dependency", + modifyFs: fs => ts.enableStrict(fs, "/src/second/tsconfig.json"), + modifyAgainFs: fs => ts.addTestPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`), + ignoreDtsChanged: true, + baselineOnly: true }); - // Shebang - describe("Shebang", () => { - // changes declaration because its emitted in .d.ts file - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "shebang in all projects", - modifyFs: fs => { - ts.addShebang(fs, "first", "first_PART1"); - ts.addShebang(fs, "first", "first_part2"); - ts.addShebang(fs, "second", "second_part1"); - ts.addShebang(fs, "third", "third_part1"); - }, - }); + // Verify initial + incremental edits - sourcemap verification + verifyOutFileScenario({ + subScenario: "multiple prologues in all projects", + modifyFs: fs => { + ts.enableStrict(fs, "/src/first/tsconfig.json"); + ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue"`); + ts.enableStrict(fs, "/src/second/tsconfig.json"); + ts.addTestPrologue(fs, "/src/second/second_part1.ts", `"myPrologue"`); + ts.addTestPrologue(fs, "/src/second/second_part2.ts", `"myPrologue2";`); + ts.enableStrict(fs, "/src/third/tsconfig.json"); + ts.addTestPrologue(fs, "/src/third/third_part1.ts", `"myPrologue";`); + ts.addTestPrologue(fs, "/src/third/third_part1.ts", `"myPrologue3";`); + }, + modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue5"`) + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "shebang in only one dependency project", - modifyFs: fs => ts.addShebang(fs, "second", "second_part1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple prologues in different projects", + modifyFs: fs => { + ts.enableStrict(fs, "/src/first/tsconfig.json"); + ts.addTestPrologue(fs, "/src/second/second_part1.ts", `"myPrologue"`); + ts.addTestPrologue(fs, "/src/second/second_part2.ts", `"myPrologue2";`); + ts.enableStrict(fs, "/src/third/tsconfig.json"); + }, + modifyAgainFs: fs => ts.addTestPrologue(fs, "/src/first/first_PART1.ts", `"myPrologue5"`), + ignoreDtsChanged: true, + baselineOnly: true }); + }); - // emitHelpers - describe("emitHelpers", () => { - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "emitHelpers in all projects", - modifyFs: fs => { - ts.addRest(fs, "first", "first_PART1"); - ts.addRest(fs, "second", "second_part1"); - ts.addRest(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => ts.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 => { + ts.addShebang(fs, "first", "first_PART1"); + ts.addShebang(fs, "first", "first_part2"); + ts.addShebang(fs, "second", "second_part1"); + ts.addShebang(fs, "third", "third_part1"); + }, + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "emitHelpers in only one dependency project", - modifyFs: fs => { - ts.addStubFoo(fs, "first", "first_PART1"); - ts.addRest(fs, "second", "second_part1"); - }, - modifyAgainFs: fs => ts.changeStubToRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "shebang in only one dependency project", + modifyFs: fs => ts.addShebang(fs, "second", "second_part1"), + ignoreDtsChanged: true, + baselineOnly: true + }); + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in all projects", - modifyFs: fs => { - ts.addRest(fs, "first", "first_PART1"); - ts.addSpread(fs, "first", "first_part3"); - ts.addRest(fs, "second", "second_part1"); - ts.addSpread(fs, "second", "second_part2"); - ts.addRest(fs, "third", "third_part1"); - ts.addSpread(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => ts.removeRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // emitHelpers + describe("emitHelpers", () => { + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "emitHelpers in all projects", + modifyFs: fs => { + ts.addRest(fs, "first", "first_PART1"); + ts.addRest(fs, "second", "second_part1"); + ts.addRest(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => ts.removeRest(fs, "first", "first_PART1") + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in different projects", - modifyFs: fs => { - ts.addRest(fs, "first", "first_PART1"); - ts.addSpread(fs, "second", "second_part1"); - ts.addRest(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => ts.removeRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "emitHelpers in only one dependency project", + modifyFs: fs => { + ts.addStubFoo(fs, "first", "first_PART1"); + ts.addRest(fs, "second", "second_part1"); + }, + modifyAgainFs: fs => ts.changeStubToRest(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 => { - ts.addTripleSlashRef(fs, "first", "first_part2"); - ts.addTripleSlashRef(fs, "second", "second_part1"); - ts.addTripleSlashRef(fs, "third", "third_part1"); - } - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in all projects", + modifyFs: fs => { + ts.addRest(fs, "first", "first_PART1"); + ts.addSpread(fs, "first", "first_part3"); + ts.addRest(fs, "second", "second_part1"); + ts.addSpread(fs, "second", "second_part2"); + ts.addRest(fs, "third", "third_part1"); + ts.addSpread(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => ts.removeRest(fs, "first", "first_PART1"), + ignoreDtsChanged: true, + baselineOnly: true + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "triple slash refs in one project", - modifyFs: fs => ts.addTripleSlashRef(fs, "second", "second_part1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in different projects", + modifyFs: fs => { + ts.addRest(fs, "first", "first_PART1"); + ts.addSpread(fs, "second", "second_part1"); + ts.addRest(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => ts.removeRest(fs, "first", "first_PART1"), + ignoreDtsChanged: true, + baselineOnly: true }); + }); - describe("stripInternal", () => { - function disableRemoveComments(fs: vfs.FileSystem, file: string) { - ts.replaceText(fs, file, `"removeComments": true`, `"removeComments": false`); + // 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 => { + ts.addTripleSlashRef(fs, "first", "first_part2"); + ts.addTripleSlashRef(fs, "second", "second_part1"); + ts.addTripleSlashRef(fs, "third", "third_part1"); } + }); - function diableRemoveCommentsInAll(fs: vfs.FileSystem) { - disableRemoveComments(fs, "/src/first/tsconfig.json"); - disableRemoveComments(fs, "/src/second/tsconfig.json"); - disableRemoveComments(fs, "/src/third/tsconfig.json"); - } + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "triple slash refs in one project", + modifyFs: fs => ts.addTripleSlashRef(fs, "second", "second_part1"), + ignoreDtsChanged: true, + baselineOnly: true + }); + }); - function stripInternalOfThird(fs: vfs.FileSystem) { - ts.replaceText(fs, "/src/third/tsconfig.json", `"declaration": true,`, `"declaration": true, + describe("stripInternal", () => { + function disableRemoveComments(fs: vfs.FileSystem, file: string) { + ts.replaceText(fs, file, `"removeComments": true`, `"removeComments": false`); + } + + function diableRemoveCommentsInAll(fs: vfs.FileSystem) { + disableRemoveComments(fs, "/src/first/tsconfig.json"); + disableRemoveComments(fs, "/src/second/tsconfig.json"); + disableRemoveComments(fs, "/src/third/tsconfig.json"); + } + + function stripInternalOfThird(fs: vfs.FileSystem) { + ts.replaceText(fs, "/src/third/tsconfig.json", `"declaration": true,`, `"declaration": true, "stripInternal": true,`); - } + } - function stripInternalScenario(fs: vfs.FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { - const internal: string = jsDocStyle ? `/**@internal*/` : `/*@internal*/`; - if (removeCommentsDisabled) { - diableRemoveCommentsInAll(fs); - } - stripInternalOfThird(fs); - ts.replaceText(fs, "/src/first/first_PART1.ts", "interface", `${internal} interface`); - ts.appendText(fs, "/src/second/second_part1.ts", ` + function stripInternalScenario(fs: vfs.FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { + const internal: string = jsDocStyle ? `/**@internal*/` : `/*@internal*/`; + if (removeCommentsDisabled) { + diableRemoveCommentsInAll(fs); + } + stripInternalOfThird(fs); + ts.replaceText(fs, "/src/first/first_PART1.ts", "interface", `${internal} interface`); + ts.appendText(fs, "/src/second/second_part1.ts", ` class normalC { ${internal} constructor() { } ${internal} prop: string; @@ -391,19 +391,64 @@ ${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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@internal*/ interface`, "interface"), + }); + + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "stripInternal with comments emit enabled", + modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ true), + modifyAgainFs: fs => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/**@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: vfs.FileSystem) { + ts.replaceText(fs, "/src/second/tsconfig.json", "[", `[ + { "path": "../first", "prepend": true }`); + ts.replaceText(fs, "/src/third/tsconfig.json", `{ "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", - modifyFs: stripInternalScenario, + subScenario: "stripInternal when one-two-three are prepended in order", + modifyFs: stripInternalWithDependentOrder, modifyAgainFs: fs => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@internal*/ interface`, "interface"), ignoreDtsChanged: true, baselineOnly: true @@ -411,8 +456,8 @@ ${internal} enum internalEnum { a, b, c }`); // 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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/**@internal*/ interface`, "interface"), ignoreDtsChanged: true, baselineOnly: true @@ -420,64 +465,19 @@ ${internal} enum internalEnum { a, b, c }`); // 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) { - ts.replaceText(fs, "/src/second/tsconfig.json", "[", `[ - { "path": "../first", "prepend": true }`); - ts.replaceText(fs, "/src/third/tsconfig.json", `{ "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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/*@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 => ts.replaceText(fs, "/src/first/first_PART1.ts", `/**@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); - ts.prependText(fs, "/src/first/first_PART1.ts", `namespace ts { + // only baseline + verifyOutFileScenario({ + subScenario: "stripInternal baseline when internal is inside another internal", + modifyFs: fs => { + stripInternalOfThird(fs); + ts.prependText(fs, "/src/first/first_PART1.ts", `namespace ts { /* @internal */ /** * Subset of properties from SourceFile that are used in multiple utility functions @@ -505,18 +505,18 @@ ${internal} enum internalEnum { a, b, c }`); someProp: string; } }`); - }, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); - // only baseline - verifyOutFileScenario({ - subScenario: "stripInternal when few members of enum are internal", - modifyFs: fs => { - stripInternalOfThird(fs); - ts.prependText(fs, "/src/first/first_PART1.ts", `enum TokenFlags { + // only baseline + verifyOutFileScenario({ + subScenario: "stripInternal when few members of enum are internal", + modifyFs: fs => { + stripInternalOfThird(fs); + ts.prependText(fs, "/src/first/first_PART1.ts", `enum TokenFlags { None = 0, /* @internal */ PrecedingLineBreak = 1 << 0, @@ -539,96 +539,96 @@ ${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("/src/first/first_PART1.ts", "/* @internal */ const A = 1;"); - fs.writeFileSync("/src/third/third_part1.ts", "const B = 2;"); - fs.writeFileSync("/src/first/tsconfig.json", JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: true, - skipDefaultLibCheck: true, - sourceMap: true, - outFile: "./bin/first-output.js" - }, - files: ["/src/first/first_PART1.ts"] - })); - fs.writeFileSync("/src/third/tsconfig.json", 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: ["/src/third/third_part1.ts"] - })); - } - }); + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true }); - describe("empty source files", () => { - function makeThirdEmptySourceFile(fs: vfs.FileSystem) { - fs.writeFileSync("/src/third/third_part1.ts", "", "utf8"); + verifyOutFileScenario({ + subScenario: "stripInternal when prepend is completely internal", + baselineOnly: true, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + modifyFs: fs => { + fs.writeFileSync("/src/first/first_PART1.ts", "/* @internal */ const A = 1;"); + fs.writeFileSync("/src/third/third_part1.ts", "const B = 2;"); + fs.writeFileSync("/src/first/tsconfig.json", JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + skipDefaultLibCheck: true, + sourceMap: true, + outFile: "./bin/first-output.js" + }, + files: ["/src/first/first_PART1.ts"] + })); + fs.writeFileSync("/src/third/tsconfig.json", 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: ["/src/third/third_part1.ts"] + })); } + }); + }); - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "when source files are empty in the own file", - modifyFs: makeThirdEmptySourceFile, - ignoreDtsChanged: true, - baselineOnly: true - }); + describe("empty source files", () => { + function makeThirdEmptySourceFile(fs: vfs.FileSystem) { + fs.writeFileSync("/src/third/third_part1.ts", "", "utf8"); + } - // only baseline - verifyOutFileScenario({ - subScenario: "declarationMap and sourceMap disabled", - modifyFs: fs => { - makeThirdEmptySourceFile(fs); - ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""); - ts.replaceText(fs, "/src/third/tsconfig.json", `"sourceMap": true,`, ""); - ts.replaceText(fs, "/src/third/tsconfig.json", `"declarationMap": true,`, ""); - }, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "when source files are empty in the own file", + modifyFs: makeThirdEmptySourceFile, + ignoreDtsChanged: true, + baselineOnly: true }); - }); - ts.verifyTsc({ - scenario: "outFile", - subScenario: "non module projects without prepend", - fs: () => outFileFs, - commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => { - // No prepend - ts.replaceText(fs, "/src/third/tsconfig.json", `{ "path": "../first", "prepend": true }`, `{ "path": "../first" }`); - ts.replaceText(fs, "/src/third/tsconfig.json", `{ "path": "../second", "prepend": true }`, `{ "path": "../second" }`); - - // Non Modules - ts.replaceText(fs, "/src/first/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); - ts.replaceText(fs, "/src/second/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); - ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); - - // Own file emit - ts.replaceText(fs, "/src/first/tsconfig.json", `"outFile": "./bin/first-output.js",`, ""); - ts.replaceText(fs, "/src/second/tsconfig.json", `"outFile": "../2/second-output.js",`, ""); - ts.replaceText(fs, "/src/third/tsconfig.json", `"outFile": "./thirdjs/output/third-output.js",`, ""); - }, + // only baseline + verifyOutFileScenario({ + subScenario: "declarationMap and sourceMap disabled", + modifyFs: fs => { + makeThirdEmptySourceFile(fs); + ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, ""); + ts.replaceText(fs, "/src/third/tsconfig.json", `"sourceMap": true,`, ""); + ts.replaceText(fs, "/src/third/tsconfig.json", `"declarationMap": true,`, ""); + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); }); }); + + ts.verifyTsc({ + scenario: "outFile", + subScenario: "non module projects without prepend", + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => { + // No prepend + ts.replaceText(fs, "/src/third/tsconfig.json", `{ "path": "../first", "prepend": true }`, `{ "path": "../first" }`); + ts.replaceText(fs, "/src/third/tsconfig.json", `{ "path": "../second", "prepend": true }`, `{ "path": "../second" }`); + + // Non Modules + ts.replaceText(fs, "/src/first/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); + ts.replaceText(fs, "/src/second/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); + ts.replaceText(fs, "/src/third/tsconfig.json", `"composite": true,`, `"composite": true, "module": "none",`); + + // Own file emit + ts.replaceText(fs, "/src/first/tsconfig.json", `"outFile": "./bin/first-output.js",`, ""); + ts.replaceText(fs, "/src/second/tsconfig.json", `"outFile": "../2/second-output.js",`, ""); + ts.replaceText(fs, "/src/third/tsconfig.json", `"outFile": "./thirdjs/output/third-output.js",`, ""); + }, + }); +}); } diff --git a/src/testRunner/unittests/tsbuild/outputPaths.ts b/src/testRunner/unittests/tsbuild/outputPaths.ts index 881179bb06262..5b81360a90ec2 100644 --- a/src/testRunner/unittests/tsbuild/outputPaths.ts +++ b/src/testRunner/unittests/tsbuild/outputPaths.ts @@ -1,100 +1,100 @@ namespace ts { - describe("unittests:: tsbuild - output file paths", () => { - const noChangeProject: ts.TestTscEdit = { - modifyFs: ts.noop, - subScenario: "Normal build without change, that does not block emit on error to show files that get emitted", - commandLineArgs: ["-p", "/src/tsconfig.json"], - }; - const edits: ts.TestTscEdit[] = [ - ts.noChangeRun, - noChangeProject, - ]; +describe("unittests:: tsbuild - output file paths", () => { + const noChangeProject: ts.TestTscEdit = { + modifyFs: ts.noop, + subScenario: "Normal build without change, that does not block emit on error to show files that get emitted", + commandLineArgs: ["-p", "/src/tsconfig.json"], + }; + const edits: ts.TestTscEdit[] = [ + ts.noChangeRun, + noChangeProject, + ]; - function verify(input: Pick, expectedOuptutNames: readonly string[]) { - ts.verifyTscWithEdits({ - scenario: "outputPaths", - commandLineArgs: ["--b", "/src/tsconfig.json", "-v"], - ...input - }); + function verify(input: Pick, expectedOuptutNames: readonly string[]) { + ts.verifyTscWithEdits({ + scenario: "outputPaths", + commandLineArgs: ["--b", "/src/tsconfig.json", "-v"], + ...input + }); - it("verify getOutputFileNames", () => { - const sys = new fakes.System(input.fs().makeReadonly(), { executingFilePath: "/lib/tsc" }) as ts.TscCompileSystem; - ; - assert.deepEqual(ts.getOutputFileNames(ts.parseConfigFileWithSystem("/src/tsconfig.json", {}, /*extendedConfigCache*/ undefined, {}, sys, ts.noop)!, "/src/src/index.ts", - /*ignoreCase*/ false), expectedOuptutNames); - }); - } + it("verify getOutputFileNames", () => { + const sys = new fakes.System(input.fs().makeReadonly(), { executingFilePath: "/lib/tsc" }) as ts.TscCompileSystem; + ; + assert.deepEqual(ts.getOutputFileNames(ts.parseConfigFileWithSystem("/src/tsconfig.json", {}, /*extendedConfigCache*/ undefined, {}, sys, ts.noop)!, "/src/src/index.ts", + /*ignoreCase*/ false), expectedOuptutNames); + }); + } - verify({ - subScenario: "when rootDir is not specified", - fs: () => ts.loadProjectFromFiles({ - "/src/src/index.ts": "export const x = 10;", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "dist" - } - }) - }), - edits, - }, ["/src/dist/index.js"]); + verify({ + subScenario: "when rootDir is not specified", + fs: () => ts.loadProjectFromFiles({ + "/src/src/index.ts": "export const x = 10;", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "dist" + } + }) + }), + edits, + }, ["/src/dist/index.js"]); - verify({ - subScenario: "when rootDir is not specified and is composite", - fs: () => ts.loadProjectFromFiles({ - "/src/src/index.ts": "export const x = 10;", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "dist", - composite: true - } - }) - }), - edits, - }, ["/src/dist/src/index.js", "/src/dist/src/index.d.ts"]); + verify({ + subScenario: "when rootDir is not specified and is composite", + fs: () => ts.loadProjectFromFiles({ + "/src/src/index.ts": "export const x = 10;", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "dist", + composite: true + } + }) + }), + edits, + }, ["/src/dist/src/index.js", "/src/dist/src/index.d.ts"]); - verify({ - subScenario: "when rootDir is specified", - fs: () => ts.loadProjectFromFiles({ - "/src/src/index.ts": "export const x = 10;", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "dist", - rootDir: "src" - } - }) - }), - edits, - }, ["/src/dist/index.js"]); + verify({ + subScenario: "when rootDir is specified", + fs: () => ts.loadProjectFromFiles({ + "/src/src/index.ts": "export const x = 10;", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "dist", + rootDir: "src" + } + }) + }), + edits, + }, ["/src/dist/index.js"]); - verify({ - subScenario: "when rootDir is specified but not all files belong to rootDir", - fs: () => ts.loadProjectFromFiles({ - "/src/src/index.ts": "export const x = 10;", - "/src/types/type.ts": "export type t = string;", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "dist", - rootDir: "src" - } - }) - }), - edits, - }, ["/src/dist/index.js"]); + verify({ + subScenario: "when rootDir is specified but not all files belong to rootDir", + fs: () => ts.loadProjectFromFiles({ + "/src/src/index.ts": "export const x = 10;", + "/src/types/type.ts": "export type t = string;", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "dist", + rootDir: "src" + } + }) + }), + edits, + }, ["/src/dist/index.js"]); - verify({ - subScenario: "when rootDir is specified but not all files belong to rootDir and is composite", - fs: () => ts.loadProjectFromFiles({ - "/src/src/index.ts": "export const x = 10;", - "/src/types/type.ts": "export type t = string;", - "/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "dist", - rootDir: "src", - composite: true - } - }) - }), - edits, - }, ["/src/dist/index.js", "/src/dist/index.d.ts"]); - }); + verify({ + subScenario: "when rootDir is specified but not all files belong to rootDir and is composite", + fs: () => ts.loadProjectFromFiles({ + "/src/src/index.ts": "export const x = 10;", + "/src/types/type.ts": "export type t = string;", + "/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "dist", + rootDir: "src", + composite: true + } + }) + }), + edits, + }, ["/src/dist/index.js", "/src/dist/index.d.ts"]); +}); } diff --git a/src/testRunner/unittests/tsbuild/publicApi.ts b/src/testRunner/unittests/tsbuild/publicApi.ts index 4feb5d0d6e219..663b49d35ebcf 100644 --- a/src/testRunner/unittests/tsbuild/publicApi.ts +++ b/src/testRunner/unittests/tsbuild/publicApi.ts @@ -1,117 +1,117 @@ namespace ts { - describe("unittests:: tsbuild:: Public API with custom transformers when passed to build", () => { - let sys: ts.TscCompileSystem; - before(() => { - const inputFs = ts.loadProjectFromFiles({ - "/src/tsconfig.json": JSON.stringify({ - references: [ - { path: "./shared/tsconfig.json" }, - { path: "./webpack/tsconfig.json" } - ], - files: [] - }), - "/src/shared/tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true }, - }), - "/src/shared/index.ts": `export function f1() { } +describe("unittests:: tsbuild:: Public API with custom transformers when passed to build", () => { + let sys: ts.TscCompileSystem; + before(() => { + const inputFs = ts.loadProjectFromFiles({ + "/src/tsconfig.json": JSON.stringify({ + references: [ + { path: "./shared/tsconfig.json" }, + { path: "./webpack/tsconfig.json" } + ], + files: [] + }), + "/src/shared/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true }, + }), + "/src/shared/index.ts": `export function f1() { } export class c { } export enum e { } // leading export function f2() { } // trailing`, - "/src/webpack/tsconfig.json": JSON.stringify({ - compilerOptions: { - composite: true, - }, - references: [{ path: "../shared/tsconfig.json" }] - }), - "/src/webpack/index.ts": `export function f2() { } + "/src/webpack/tsconfig.json": JSON.stringify({ + compilerOptions: { + composite: true, + }, + references: [{ path: "../shared/tsconfig.json" }] + }), + "/src/webpack/index.ts": `export function f2() { } export class c2 { } export enum e2 { } // leading export function f22() { } // trailing`, - }).makeReadonly(); - const fs = inputFs.shadow(); + }).makeReadonly(); + const fs = inputFs.shadow(); - // Create system - sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }) as ts.TscCompileSystem; - fakes.patchHostForBuildInfoReadWrite(sys); - const commandLineArgs = ["--b", "/src/tsconfig.json"]; - sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); - sys.exit = exitCode => sys.exitCode = exitCode; - const writtenFiles = sys.writtenFiles = new ts.Set(); - const originalWriteFile = sys.writeFile; - sys.writeFile = (fileName, content, writeByteOrderMark) => { - const path = ts.toPathWithSystem(sys, fileName); - assert.isFalse(writtenFiles.has(path)); - writtenFiles.add(path); - return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); - }; - const { cb, getPrograms } = ts.commandLineCallbacks(sys, /*originalReadCall*/ undefined); - const buildHost = ts.createSolutionBuilderHost(sys, - /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true), (errorCount, filesInError) => sys.write(ts.getErrorSummaryText(errorCount, filesInError, sys.newLine, sys))); - buildHost.afterProgramEmitAndDiagnostics = cb; - buildHost.afterEmitBundle = cb; - const builder = ts.createSolutionBuilder(buildHost, [commandLineArgs[1]], { verbose: true }); - const exitStatus = builder.build(/*project*/ undefined, /*cancellationToken*/ undefined, /*writeFile*/ undefined, getCustomTransformers); - sys.exit(exitStatus); - sys.write(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}\n`); - const baseline: string[] = []; - ts.tscWatch.baselinePrograms(baseline, getPrograms, ts.emptyArray, /*baselineDependencies*/ false); - sys.write(baseline.join("\n")); - fs.makeReadonly(); - sys.baseLine = () => { - const baseFsPatch = inputFs.diff(/*base*/ undefined, { baseIsNotShadowRoot: true }); - const patch = fs.diff(inputFs, { includeChangedFileWithSameContent: true }); - return { - file: `tsbuild/publicAPI/build-with-custom-transformers.js`, - text: `Input:: + // Create system + sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }) as ts.TscCompileSystem; + fakes.patchHostForBuildInfoReadWrite(sys); + const commandLineArgs = ["--b", "/src/tsconfig.json"]; + sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); + sys.exit = exitCode => sys.exitCode = exitCode; + const writtenFiles = sys.writtenFiles = new ts.Set(); + const originalWriteFile = sys.writeFile; + sys.writeFile = (fileName, content, writeByteOrderMark) => { + const path = ts.toPathWithSystem(sys, fileName); + assert.isFalse(writtenFiles.has(path)); + writtenFiles.add(path); + return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); + }; + const { cb, getPrograms } = ts.commandLineCallbacks(sys, /*originalReadCall*/ undefined); + const buildHost = ts.createSolutionBuilderHost(sys, + /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true), (errorCount, filesInError) => sys.write(ts.getErrorSummaryText(errorCount, filesInError, sys.newLine, sys))); + buildHost.afterProgramEmitAndDiagnostics = cb; + buildHost.afterEmitBundle = cb; + const builder = ts.createSolutionBuilder(buildHost, [commandLineArgs[1]], { verbose: true }); + const exitStatus = builder.build(/*project*/ undefined, /*cancellationToken*/ undefined, /*writeFile*/ undefined, getCustomTransformers); + sys.exit(exitStatus); + sys.write(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}\n`); + const baseline: string[] = []; + ts.tscWatch.baselinePrograms(baseline, getPrograms, ts.emptyArray, /*baselineDependencies*/ false); + sys.write(baseline.join("\n")); + fs.makeReadonly(); + sys.baseLine = () => { + const baseFsPatch = inputFs.diff(/*base*/ undefined, { baseIsNotShadowRoot: true }); + const patch = fs.diff(inputFs, { includeChangedFileWithSameContent: true }); + return { + file: `tsbuild/publicAPI/build-with-custom-transformers.js`, + text: `Input:: ${baseFsPatch ? vfs.formatPatch(baseFsPatch) : ""} Output:: ${sys.output.join("")} ${patch ? vfs.formatPatch(patch) : ""}` - }; }; + }; - function getCustomTransformers(project: string): ts.CustomTransformers { - const before: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return visitFunction(node as ts.FunctionDeclaration); - default: - return ts.visitEachChild(node, visit, context); - } - } - function visitFunction(node: ts.FunctionDeclaration) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `@before${project}`, /*hasTrailingNewLine*/ true); - return node; + function getCustomTransformers(project: string): ts.CustomTransformers { + const before: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return visitFunction(node as ts.FunctionDeclaration); + default: + return ts.visitEachChild(node, visit, context); } - }; + } + function visitFunction(node: ts.FunctionDeclaration) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `@before${project}`, /*hasTrailingNewLine*/ true); + return node; + } + }; - const after: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.VariableStatement: - return visitVariableStatement(node as ts.VariableStatement); - default: - return ts.visitEachChild(node, visit, context); - } + const after: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + return visitVariableStatement(node as ts.VariableStatement); + default: + return ts.visitEachChild(node, visit, context); } - function visitVariableStatement(node: ts.VariableStatement) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, `@after${project}`); - return node; - } - }; - return { before: [before], after: [after] }; - } - }); - after(() => { - sys = undefined!; - }); - ts.verifyTscBaseline(() => sys); + } + function visitVariableStatement(node: ts.VariableStatement) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, `@after${project}`); + return node; + } + }; + return { before: [before], after: [after] }; + } + }); + after(() => { + sys = undefined!; }); + ts.verifyTscBaseline(() => sys); +}); } diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts index 6504a43073b68..85c6e5d564291 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -1,61 +1,61 @@ namespace ts { - describe("unittests:: tsbuild:: with rootDir of project reference in parentDirectory", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); - }); +describe("unittests:: tsbuild:: with rootDir of project reference in parentDirectory", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); + }); - after(() => { - projFs = undefined!; // Release the contents - }); + after(() => { + projFs = undefined!; // Release the contents + }); - ts.verifyTsc({ - scenario: "projectReferenceWithRootDirInParent", - subScenario: "builds correctly", - fs: () => projFs, - commandLineArgs: ["--b", "/src/src/main", "/src/src/other"], - }); + ts.verifyTsc({ + scenario: "projectReferenceWithRootDirInParent", + subScenario: "builds correctly", + fs: () => projFs, + commandLineArgs: ["--b", "/src/src/main", "/src/src/other"], + }); - ts.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 => ts.replaceText(fs, "/src/tsconfig.base.json", `"rootDir": "./src/",`, ""), - }); + ts.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 => ts.replaceText(fs, "/src/tsconfig.base.json", `"rootDir": "./src/",`, ""), + }); - ts.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/" }, - })); - }, - }); + ts.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/" }, + })); + }, + }); - ts.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/" }, - })); - }, - }); + ts.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 edd6eba1e27e7..55cc53fd815de 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -1,82 +1,82 @@ namespace ts { - describe("unittests:: tsbuild:: with resolveJsonModule option on project resolveJsonModuleAndComposite", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); - }); +describe("unittests:: tsbuild:: with resolveJsonModule option on project resolveJsonModuleAndComposite", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); + }); - after(() => { - projFs = undefined!; // Release the contents - }); + after(() => { + projFs = undefined!; // Release the contents + }); - ts.verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include only", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withInclude.json", "--v", "--explainFiles"], - }); + ts.verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include only", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withInclude.json", "--v", "--explainFiles"], + }); - ts.verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include of json along with other include", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json", "--v", "--explainFiles"], - }); + ts.verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include of json along with other include", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json", "--v", "--explainFiles"], + }); - ts.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", "--v", "--explainFiles"], - 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" + ts.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", "--v", "--explainFiles"], + 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`); - }, - }); + }, + }); - ts.verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "files containing json file", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withFiles.json", "--v", "--explainFiles"], - }); + ts.verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "files containing json file", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withFiles.json", "--v", "--explainFiles"], + }); - ts.verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include and files", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withIncludeAndFiles.json", "--v", "--explainFiles"], - }); + ts.verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include and files", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withIncludeAndFiles.json", "--v", "--explainFiles"], + }); - ts.verifyTscWithEdits({ - scenario: "resolveJsonModule", - subScenario: "sourcemap", - fs: () => projFs, - commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose", "--explainFiles"], - modifyFs: fs => ts.replaceText(fs, "src/tsconfig_withFiles.json", `"composite": true,`, `"composite": true, "sourceMap": true,`), - edits: ts.noChangeOnlyRuns - }); + ts.verifyTscWithEdits({ + scenario: "resolveJsonModule", + subScenario: "sourcemap", + fs: () => projFs, + commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose", "--explainFiles"], + modifyFs: fs => ts.replaceText(fs, "src/tsconfig_withFiles.json", `"composite": true,`, `"composite": true, "sourceMap": true,`), + edits: ts.noChangeOnlyRuns + }); - ts.verifyTscWithEdits({ - scenario: "resolveJsonModule", - subScenario: "without outDir", - fs: () => projFs, - commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], - modifyFs: fs => ts.replaceText(fs, "src/tsconfig_withFiles.json", `"outDir": "dist",`, ""), - edits: ts.noChangeOnlyRuns - }); + ts.verifyTscWithEdits({ + scenario: "resolveJsonModule", + subScenario: "without outDir", + fs: () => projFs, + commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], + modifyFs: fs => ts.replaceText(fs, "src/tsconfig_withFiles.json", `"outDir": "dist",`, ""), + edits: ts.noChangeOnlyRuns }); +}); - describe("unittests:: tsbuild:: with resolveJsonModule option on project importJsonFromProjectReference", () => { - ts.verifyTscWithEdits({ - scenario: "resolveJsonModule", - subScenario: "importing json module from project reference", - fs: () => ts.loadProjectFromDisk("tests/projects/importJsonFromProjectReference"), - commandLineArgs: ["--b", "src/tsconfig.json", "--verbose", "--explainFiles"], - edits: ts.noChangeOnlyRuns - }); +describe("unittests:: tsbuild:: with resolveJsonModule option on project importJsonFromProjectReference", () => { + ts.verifyTscWithEdits({ + scenario: "resolveJsonModule", + subScenario: "importing json module from project reference", + fs: () => ts.loadProjectFromDisk("tests/projects/importJsonFromProjectReference"), + commandLineArgs: ["--b", "src/tsconfig.json", "--verbose", "--explainFiles"], + edits: ts.noChangeOnlyRuns }); +}); } diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 82951bc5237bd..79fdec51d5c10 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -1,448 +1,448 @@ namespace ts { - describe("unittests:: tsbuild:: on 'sample1' project", () => { - let projFs: vfs.FileSystem; - let projFsWithBuild: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/sample1"); +describe("unittests:: tsbuild:: on 'sample1' project", () => { + let projFs: vfs.FileSystem; + let projFsWithBuild: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/sample1"); + }); + + after(() => { + projFs = undefined!; // Release the contents + projFsWithBuild = undefined!; + }); + + function getTsBuildProjectFile(project: string, file: string): ts.tscWatch.File { + return { + path: ts.TestFSWithWatch.getTsBuildProjectFilePath(project, file), + content: projFs.readFileSync(`/src/${project}/${file}`, "utf8")! + }; + } + + function getSampleFsAfterBuild() { + if (projFsWithBuild) + return projFsWithBuild; + const fs = projFs.shadow(); + const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }); + const host = ts.createSolutionBuilderHostForBaseline(sys as ts.TscCompileSystem); + const builder = ts.createSolutionBuilder(host, ["/src/tests"], {}); + builder.build(); + fs.makeReadonly(); + return projFsWithBuild = fs; + } + + describe("sanity check of clean build of 'sample1' project", () => { + ts.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" }] + })), }); - after(() => { - projFs = undefined!; // Release the contents - projFsWithBuild = undefined!; + ts.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" }] + })), }); - function getTsBuildProjectFile(project: string, file: string): ts.tscWatch.File { - return { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath(project, file), - content: projFs.readFileSync(`/src/${project}/${file}`, "utf8")! - }; - } + ts.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 => ts.replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), + }); + }); - function getSampleFsAfterBuild() { - if (projFsWithBuild) - return projFsWithBuild; - const fs = projFs.shadow(); - const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }); - const host = ts.createSolutionBuilderHostForBaseline(sys as ts.TscCompileSystem); - const builder = ts.createSolutionBuilder(host, ["/src/tests"], {}); - builder.build(); - fs.makeReadonly(); - return projFsWithBuild = fs; - } + describe("dry builds", () => { + ts.verifyTsc({ + scenario: "sample1", + subScenario: "does not write any files in a dry build", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--dry"], + }); + }); - describe("sanity check of clean build of 'sample1' project", () => { - ts.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" }] - })), - }); - - ts.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" }] - })), - }); - - ts.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 => ts.replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), - }); + describe("clean builds", () => { + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "removes all files it built", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--clean"], + edits: ts.noChangeOnlyRuns }); - describe("dry builds", () => { - ts.verifyTsc({ - scenario: "sample1", - subScenario: "does not write any files in a dry build", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--dry"], - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "cleans till project specified", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/logic", "--clean"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); + sys.exit(builder.clean("/src/logic")); + } }); - describe("clean builds", () => { - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "removes all files it built", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--clean"], - edits: ts.noChangeOnlyRuns - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "cleans till project specified", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/logic", "--clean"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); - sys.exit(builder.clean("/src/logic")); - } - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "cleaning project in not build order doesnt throw error", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/logic2", "--clean"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); - sys.exit(builder.clean("/src/logic2")); - } - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "cleaning project in not build order doesnt throw error", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/logic2", "--clean"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); + sys.exit(builder.clean("/src/logic2")); + } }); + }); - describe("force builds", () => { - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "always builds under with force option", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--force"], - edits: ts.noChangeOnlyRuns - }); + describe("force builds", () => { + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "always builds under with force option", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--force"], + edits: ts.noChangeOnlyRuns }); + }); - describe("can detect when and what to rebuild", () => { - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "can detect when and what to rebuild", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - edits: [ - // Update a file in the leaf node (tests), only it should rebuild the last one - { - subScenario: "Only builds the leaf node project", - 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", - modifyFs: fs => ts.replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), - }, - { - subScenario: "rebuilds when tsconfig changes", - modifyFs: fs => ts.replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), - }, - ] - }); - - ts.verifyTsc({ - scenario: "sample1", - subScenario: "indicates that it would skip builds during a dry build", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--dry"], - }); - - ts.verifyTsc({ - scenario: "sample1", - subScenario: "rebuilds from start if force option is set", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "rebuilds completely when version in tsbuildinfo doesnt match ts version", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - compile: sys => { - // Buildinfo will have version which does not match with current ts version - const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); - const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); - sys.exit(builder.build()); - } - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", - fs: () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, ts.createAbstractBuilder); - const builder = ts.createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.build(); - fs.makeReadonly(); - return fs; + describe("can detect when and what to rebuild", () => { + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "can detect when and what to rebuild", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + edits: [ + // Update a file in the leaf node (tests), only it should rebuild the last one + { + subScenario: "Only builds the leaf node project", + modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"), }, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - compile: sys => { - // Buildinfo will have version which does not match with current ts version - const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); - const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); - sys.exit(builder.build()); + // Update a file in the parent (without affecting types), should get fast downstream builds + { + subScenario: "Detects type-only changes in upstream projects", + modifyFs: fs => ts.replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), }, - }); - - ts.verifyTscWithEdits({ - 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" } })); - ts.replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); + { + subScenario: "rebuilds when tsconfig changes", + modifyFs: fs => ts.replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), }, - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })) - }] - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "builds till project specified", - fs: () => projFs, - commandLineArgs: ["--build", "/src/logic/tsconfig.json"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], {}); - sys.exit(builder.build("/src/logic/tsconfig.json")); - } - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "building project in not build order doesnt throw error", - fs: () => projFs, - commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], {}); - sys.exit(builder.build("/src/logic2/tsconfig.json")); - } - }); - - it("building using getNextInvalidatedProject", () => { - const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); - const coreIndex = getTsBuildProjectFile("core", "index.ts"); - const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); - const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); - const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); - const logicIndex = getTsBuildProjectFile("logic", "index.ts"); - const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); - const testsIndex = getTsBuildProjectFile("tests", "index.ts"); - const baseline: string[] = []; - let oldSnap: ReturnType | undefined; - const system = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(ts.tscWatch.createWatchedSystem([ - coreConfig, coreIndex, coreDecl, coreAnotherModule, - logicConfig, logicIndex, - testsConfig, testsIndex, - ts.tscWatch.libFile - ]))); - const host = ts.createSolutionBuilderHostForBaseline(system); - const builder = ts.createSolutionBuilder(host, [testsConfig.path], {}); - baseline.push("Input::"); - baselineState(); - verifyBuildNextResult(); // core - verifyBuildNextResult(); // logic - verifyBuildNextResult();// tests - verifyBuildNextResult(); // All Done - Harness.Baseline.runBaseline(`tsbuild/sample1/building-using-getNextInvalidatedProject.js`, baseline.join("\r\n")); - - function verifyBuildNextResult() { - const project = builder.getNextInvalidatedProject(); - const result = project && project.done(); - baseline.push(`Project Result:: ${JSON.stringify({ project: project?.project, result })}`); - baselineState(); - } - - function baselineState() { - system.serializeOutput(baseline); - system.diff(baseline, oldSnap); - system.writtenFiles.clear(); - oldSnap = system.snap(); - } - }); - - ts.verifyTscCompileLike(ts.testTscCompileLike, { - scenario: "sample1", - subScenario: "building using buildReferencedProject", - fs: () => projFs, - commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], - compile: sys => { - const buildHost = ts.createSolutionBuilderHostForBaseline(sys); - const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); - sys.exit(builder.buildReferences("/src/tests")); - } - }); + ] + }); + + ts.verifyTsc({ + scenario: "sample1", + subScenario: "indicates that it would skip builds during a dry build", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--dry"], + }); + + ts.verifyTsc({ + scenario: "sample1", + subScenario: "rebuilds from start if force option is set", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], }); - describe("downstream-blocked compilations", () => { - ts.verifyTsc({ - scenario: "sample1", - subScenario: "does not build downstream projects if upstream projects have errors", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - modifyFs: fs => ts.replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`) - }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "rebuilds completely when version in tsbuildinfo doesnt match ts version", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + compile: sys => { + // Buildinfo will have version which does not match with current ts version + const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); + const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); + sys.exit(builder.build()); + } }); - describe("project invalidation", () => { - it("invalidates projects correctly", () => { - const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); - const coreIndex = getTsBuildProjectFile("core", "index.ts"); - const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); - const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); - const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); - const logicIndex = getTsBuildProjectFile("logic", "index.ts"); - const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); - const testsIndex = getTsBuildProjectFile("tests", "index.ts"); - const baseline: string[] = []; - let oldSnap: ReturnType | undefined; - const system = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(ts.tscWatch.createWatchedSystem([ - coreConfig, coreIndex, coreDecl, coreAnotherModule, - logicConfig, logicIndex, - testsConfig, testsIndex, - ts.tscWatch.libFile - ]))); - const host = ts.createSolutionBuilderHostForBaseline(system); - const builder = ts.createSolutionBuilder(host, [testsConfig.path], { dry: false, force: false, verbose: false }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", + fs: () => { + const fs = projFs.shadow(); + const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, ts.createAbstractBuilder); + const builder = ts.createSolutionBuilder(host, ["/src/tests"], { verbose: true }); builder.build(); - baselineState("Build of project"); + fs.makeReadonly(); + return fs; + }, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + compile: sys => { + // Buildinfo will have version which does not match with current ts version + const buildHost = ts.createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); + const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); + sys.exit(builder.build()); + }, + }); - // Update a timestamp in the middle project - system.appendFile(logicIndex.path, "function foo() {}"); + ts.verifyTscWithEdits({ + 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" } })); + ts.replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); + }, + edits: [{ + subScenario: "incremental-declaration-changes", + modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })) + }] + }); - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfProject(logicConfig.path); - baseline.push(`Project should still be upto date: ${ts.UpToDateStatusType[status.type]}`); - verifyInvalidation("non Dts change to logic"); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "builds till project specified", + fs: () => projFs, + commandLineArgs: ["--build", "/src/logic/tsconfig.json"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], {}); + sys.exit(builder.build("/src/logic/tsconfig.json")); + } + }); + + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "building project in not build order doesnt throw error", + fs: () => projFs, + commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], {}); + sys.exit(builder.build("/src/logic2/tsconfig.json")); + } + }); + + it("building using getNextInvalidatedProject", () => { + const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); + const coreIndex = getTsBuildProjectFile("core", "index.ts"); + const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); + const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); + const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); + const logicIndex = getTsBuildProjectFile("logic", "index.ts"); + const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); + const testsIndex = getTsBuildProjectFile("tests", "index.ts"); + const baseline: string[] = []; + let oldSnap: ReturnType | undefined; + const system = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(ts.tscWatch.createWatchedSystem([ + coreConfig, coreIndex, coreDecl, coreAnotherModule, + logicConfig, logicIndex, + testsConfig, testsIndex, + ts.tscWatch.libFile + ]))); + const host = ts.createSolutionBuilderHostForBaseline(system); + const builder = ts.createSolutionBuilder(host, [testsConfig.path], {}); + baseline.push("Input::"); + baselineState(); + verifyBuildNextResult(); // core + verifyBuildNextResult(); // logic + verifyBuildNextResult();// tests + verifyBuildNextResult(); // All Done + Harness.Baseline.runBaseline(`tsbuild/sample1/building-using-getNextInvalidatedProject.js`, baseline.join("\r\n")); + + function verifyBuildNextResult() { + const project = builder.getNextInvalidatedProject(); + const result = project && project.done(); + baseline.push(`Project Result:: ${JSON.stringify({ project: project?.project, result })}`); + baselineState(); + } + + function baselineState() { + system.serializeOutput(baseline); + system.diff(baseline, oldSnap); + system.writtenFiles.clear(); + oldSnap = system.snap(); + } + }); + ts.verifyTscCompileLike(ts.testTscCompileLike, { + scenario: "sample1", + subScenario: "building using buildReferencedProject", + fs: () => projFs, + commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], + compile: sys => { + const buildHost = ts.createSolutionBuilderHostForBaseline(sys); + const builder = ts.createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); + sys.exit(builder.buildReferences("/src/tests")); + } + }); + }); + + describe("downstream-blocked compilations", () => { + ts.verifyTsc({ + scenario: "sample1", + subScenario: "does not build downstream projects if upstream projects have errors", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + modifyFs: fs => ts.replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`) + }); + }); + + describe("project invalidation", () => { + it("invalidates projects correctly", () => { + const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); + const coreIndex = getTsBuildProjectFile("core", "index.ts"); + const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); + const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); + const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); + const logicIndex = getTsBuildProjectFile("logic", "index.ts"); + const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); + const testsIndex = getTsBuildProjectFile("tests", "index.ts"); + const baseline: string[] = []; + let oldSnap: ReturnType | undefined; + const system = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(ts.tscWatch.createWatchedSystem([ + coreConfig, coreIndex, coreDecl, coreAnotherModule, + logicConfig, logicIndex, + testsConfig, testsIndex, + ts.tscWatch.libFile + ]))); + const host = ts.createSolutionBuilderHostForBaseline(system); + const builder = ts.createSolutionBuilder(host, [testsConfig.path], { dry: false, force: false, verbose: false }); + builder.build(); + baselineState("Build of project"); + + // Update a timestamp in the middle project + system.appendFile(logicIndex.path, "function foo() {}"); + + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfProject(logicConfig.path); + baseline.push(`Project should still be upto date: ${ts.UpToDateStatusType[status.type]}`); + verifyInvalidation("non Dts change to logic"); + + // Rebuild this project + system.appendFile(logicIndex.path, `export class cNew {}`); + verifyInvalidation("Dts change to Logic"); + Harness.Baseline.runBaseline(`tsbuild/sample1/invalidates-projects-correctly.js`, baseline.join("\r\n")); + + function verifyInvalidation(heading: string) { // Rebuild this project - system.appendFile(logicIndex.path, `export class cNew {}`); - verifyInvalidation("Dts change to Logic"); - Harness.Baseline.runBaseline(`tsbuild/sample1/invalidates-projects-correctly.js`, baseline.join("\r\n")); - - function verifyInvalidation(heading: string) { - // Rebuild this project - builder.invalidateProject(logicConfig.path as ts.ResolvedConfigFilePath); - builder.buildNextInvalidatedProject(); - baselineState(`${heading}:: After rebuilding logicConfig`); - - // Build downstream projects should update 'tests', but not 'core' - builder.buildNextInvalidatedProject(); - baselineState(`${heading}:: After building next project`); - } - - function baselineState(heading: string) { - baseline.push(heading); - system.serializeOutput(baseline); - system.diff(baseline, oldSnap); - system.writtenFiles.clear(); - oldSnap = system.snap(); - } - }); + builder.invalidateProject(logicConfig.path as ts.ResolvedConfigFilePath); + builder.buildNextInvalidatedProject(); + baselineState(`${heading}:: After rebuilding logicConfig`); + + // Build downstream projects should update 'tests', but not 'core' + builder.buildNextInvalidatedProject(); + baselineState(`${heading}:: After building next project`); + } + + function baselineState(heading: string) { + baseline.push(heading); + system.serializeOutput(baseline); + system.diff(baseline, oldSnap); + system.writtenFiles.clear(); + oldSnap = system.snap(); + } }); + }); - const coreChanges: ts.TestTscEdit[] = [ - { - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.appendText(fs, "/src/core/index.ts", ` + const coreChanges: ts.TestTscEdit[] = [ + { + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.appendText(fs, "/src/core/index.ts", ` export class someClass { }`), - }, - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.appendText(fs, "/src/core/index.ts", ` + }, + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.appendText(fs, "/src/core/index.ts", ` class someClass2 { }`), - } - ]; - - describe("lists files", () => { - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "listFiles", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--listFiles"], - edits: coreChanges - }); - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "listEmittedFiles", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], - edits: coreChanges - }); - ts.verifyTscWithEdits({ - scenario: "sample1", - subScenario: "explainFiles", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--explainFiles", "--v"], - edits: coreChanges - }); + } + ]; + + describe("lists files", () => { + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "listFiles", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--listFiles"], + edits: coreChanges }); + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "listEmittedFiles", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], + edits: coreChanges + }); + ts.verifyTscWithEdits({ + scenario: "sample1", + subScenario: "explainFiles", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--explainFiles", "--v"], + edits: coreChanges + }); + }); - describe("emit output", () => { - ts.verifyTscWithEdits({ - subScenario: "sample", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/tests", "--verbose"], - baselineSourceMap: true, - baselineReadFileCalls: true, - edits: [ - ...coreChanges, - { - subScenario: "when logic config changes declaration dir", - modifyFs: fs => ts.replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, + describe("emit output", () => { + ts.verifyTscWithEdits({ + subScenario: "sample", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/tests", "--verbose"], + baselineSourceMap: true, + baselineReadFileCalls: true, + edits: [ + ...coreChanges, + { + subScenario: "when logic config changes declaration dir", + modifyFs: fs => ts.replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, "declarationDir": "decls",`), - }, - ts.noChangeRun, - ], - }); - - ts.verifyTsc({ - scenario: "sample1", - subScenario: "when logic specifies tsBuildInfoFile", - fs: () => projFs, - modifyFs: fs => ts.replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, + }, + ts.noChangeRun, + ], + }); + + ts.verifyTsc({ + scenario: "sample1", + subScenario: "when logic specifies tsBuildInfoFile", + fs: () => projFs, + modifyFs: fs => ts.replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, "tsBuildInfoFile": "ownFile.tsbuildinfo",`), - commandLineArgs: ["--b", "/src/tests", "--verbose"], - baselineSourceMap: true, - baselineReadFileCalls: true - }); - - ts.verifyTscWithEdits({ - 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 + }); + + ts.verifyTscWithEdits({ + 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 } }`), - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`), - }], - }); - - ts.verifyTscWithEdits({ - subScenario: "when target option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => { - fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// + edits: [{ + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`), + }], + }); + + ts.verifyTscWithEdits({ + 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", ts.libContent); - fs.writeFileSync("/lib/lib.d.ts", `/// + fs.writeFileSync("/lib/lib.esnext.d.ts", 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, @@ -450,36 +450,36 @@ class someClass2 { }`), "target": "esnext", } }`); - }, - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"), - }], - }); - - ts.verifyTscWithEdits({ - subScenario: "when module option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ + }, + edits: [{ + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"), + }], + }); + + ts.verifyTscWithEdits({ + 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" } }`), - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`), - }], - }); - - ts.verifyTscWithEdits({ - subScenario: "when esModuleInterop option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/tests", "--verbose"], - modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{ + edits: [{ + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`), + }], + }); + + ts.verifyTscWithEdits({ + 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" } @@ -493,11 +493,11 @@ class someClass2 { }`), "esModuleInterop": false } }`), - edits: [{ - subScenario: "incremental-declaration-changes", - modifyFs: fs => ts.replaceText(fs, "/src/tests/tsconfig.json", `"esModuleInterop": false`, `"esModuleInterop": true`), - }], - }); + edits: [{ + subScenario: "incremental-declaration-changes", + modifyFs: fs => ts.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 e428506d96df6..cc4f9055d078b 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -1,47 +1,47 @@ namespace ts { - describe("unittests:: tsbuild:: when project reference is referenced transitively", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/transitiveReferences"); - }); - after(() => { - projFs = undefined!; // Release the contents - }); +describe("unittests:: tsbuild:: when project reference is referenced transitively", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.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'; + function modifyFsBTsToNonRelativeImport(fs: vfs.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" }] - })); - } + fs.writeFileSync("/src/tsconfig.b.json", JSON.stringify({ + compilerOptions: { + composite: true, + moduleResolution + }, + files: ["b.ts"], + references: [{ path: "tsconfig.a.json" }] + })); + } - ts.verifyTsc({ - scenario: "transitiveReferences", - subScenario: "builds correctly", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], - }); + ts.verifyTsc({ + scenario: "transitiveReferences", + subScenario: "builds correctly", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], + }); - ts.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"), - }); + ts.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"), + }); - ts.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"), - }); + ts.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/tsbuildWatch/configFileErrors.ts b/src/testRunner/unittests/tsbuildWatch/configFileErrors.ts index 57e76de142f92..34688e4d4c0dc 100644 --- a/src/testRunner/unittests/tsbuildWatch/configFileErrors.ts +++ b/src/testRunner/unittests/tsbuildWatch/configFileErrors.ts @@ -1,18 +1,18 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: configFileErrors:: reports syntax errors in config file", () => { - function build(sys: ts.tscWatch.WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // build the project - sys.checkTimeoutQueueLength(0); - } - ts.tscWatch.verifyTscWatch({ - scenario: "configFileErrors", - subScenario: "reports syntax errors in config file", - sys: () => ts.tscWatch.createWatchedSystem([ - { path: `${ts.tscWatch.projectRoot}/a.ts`, content: "export function foo() { }" }, - { path: `${ts.tscWatch.projectRoot}/b.ts`, content: "export function bar() { }" }, - { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: Utils.dedent` +describe("unittests:: tsbuildWatch:: watchMode:: configFileErrors:: reports syntax errors in config file", () => { + function build(sys: ts.tscWatch.WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // build the project + sys.checkTimeoutQueueLength(0); + } + ts.tscWatch.verifyTscWatch({ + scenario: "configFileErrors", + subScenario: "reports syntax errors in config file", + sys: () => ts.tscWatch.createWatchedSystem([ + { path: `${ts.tscWatch.projectRoot}/a.ts`, content: "export function foo() { }" }, + { path: `${ts.tscWatch.projectRoot}/b.ts`, content: "export function bar() { }" }, + { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: Utils.dedent` { "compilerOptions": { "composite": true, @@ -22,36 +22,36 @@ namespace ts.tscWatch { "b.ts" ] }` - }, - ts.tscWatch.libFile - ], { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["--b", "-w"], - changes: [ - { - caption: "reports syntax errors after change to config file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/tsconfig.json`, ",", `, - "declaration": true,`), - timeouts: build, - }, - { - caption: "reports syntax errors after change to ts file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/a.ts`, "foo", "fooBar"), - timeouts: build, }, - { - caption: "reports error when there is no change to tsconfig file", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/tsconfig.json`, "", ""), - timeouts: build, - }, - { - caption: "builds after fixing config file errors", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - files: ["a.ts", "b.ts"] - })), - timeouts: build, - } - ] - }); + ts.tscWatch.libFile + ], { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["--b", "-w"], + changes: [ + { + caption: "reports syntax errors after change to config file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/tsconfig.json`, ",", `, + "declaration": true,`), + timeouts: build, + }, + { + caption: "reports syntax errors after change to ts file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/a.ts`, "foo", "fooBar"), + timeouts: build, + }, + { + caption: "reports error when there is no change to tsconfig file", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/tsconfig.json`, "", ""), + timeouts: build, + }, + { + caption: "builds after fixing config file errors", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + files: ["a.ts", "b.ts"] + })), + timeouts: build, + } + ] }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/demo.ts b/src/testRunner/unittests/tsbuildWatch/demo.ts index f358ae171d7ea..2cb924f6e6483 100644 --- a/src/testRunner/unittests/tsbuildWatch/demo.ts +++ b/src/testRunner/unittests/tsbuildWatch/demo.ts @@ -1,87 +1,87 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: with demo project", () => { - const projectLocation = `${ts.TestFSWithWatch.tsbuildProjectsLocation}/demo`; - let coreFiles: ts.tscWatch.File[]; - let animalFiles: ts.tscWatch.File[]; - let zooFiles: ts.tscWatch.File[]; - let solutionFile: ts.tscWatch.File; - let baseConfig: ts.tscWatch.File; - let allFiles: ts.tscWatch.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: ts.tscWatch.libFile.path, content: ts.libContent }]; - }); +describe("unittests:: tsbuildWatch:: watchMode:: with demo project", () => { + const projectLocation = `${ts.TestFSWithWatch.tsbuildProjectsLocation}/demo`; + let coreFiles: ts.tscWatch.File[]; + let animalFiles: ts.tscWatch.File[]; + let zooFiles: ts.tscWatch.File[]; + let solutionFile: ts.tscWatch.File; + let baseConfig: ts.tscWatch.File; + let allFiles: ts.tscWatch.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: ts.tscWatch.libFile.path, content: ts.libContent }]; + }); - after(() => { - coreFiles = undefined!; - animalFiles = undefined!; - zooFiles = undefined!; - solutionFile = undefined!; - baseConfig = undefined!; - allFiles = undefined!; - }); + after(() => { + coreFiles = undefined!; + animalFiles = undefined!; + zooFiles = undefined!; + solutionFile = undefined!; + baseConfig = undefined!; + allFiles = undefined!; + }); - ts.tscWatch.verifyTscWatch({ - scenario: "demo", - subScenario: "updates with circular reference", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => { - const sys = ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectLocation }); - sys.writeFile(coreFiles[0].path, coreFiles[0].content.replace("}", `}, + ts.tscWatch.verifyTscWatch({ + scenario: "demo", + subScenario: "updates with circular reference", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => { + const sys = ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectLocation }); + sys.writeFile(coreFiles[0].path, coreFiles[0].content.replace("}", `}, "references": [ { "path": "../zoo" } ]`)); - return sys; - }, - changes: [ - { - caption: "Fix error", - change: sys => sys.writeFile(coreFiles[0].path, coreFiles[0].content), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // build core - sys.checkTimeoutQueueLengthAndRun(1); // build animals - sys.checkTimeoutQueueLengthAndRun(1); // build zoo - sys.checkTimeoutQueueLengthAndRun(1); // build solution - sys.checkTimeoutQueueLength(0); - }, - } - ] - }); + return sys; + }, + changes: [ + { + caption: "Fix error", + change: sys => sys.writeFile(coreFiles[0].path, coreFiles[0].content), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // build core + sys.checkTimeoutQueueLengthAndRun(1); // build animals + sys.checkTimeoutQueueLengthAndRun(1); // build zoo + sys.checkTimeoutQueueLengthAndRun(1); // build solution + sys.checkTimeoutQueueLength(0); + }, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "demo", - subScenario: "updates with bad reference", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => { - const sys = ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectLocation }); - sys.writeFile(coreFiles[1].path, `import * as A from '../animals'; + ts.tscWatch.verifyTscWatch({ + scenario: "demo", + subScenario: "updates with bad reference", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => { + const sys = ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectLocation }); + sys.writeFile(coreFiles[1].path, `import * as A from '../animals'; ${coreFiles[1].content}`); - return sys; - }, - changes: [ - { - caption: "Prepend a line", - change: sys => sys.writeFile(coreFiles[1].path, ` + return sys; + }, + changes: [ + { + caption: "Prepend a line", + change: sys => sys.writeFile(coreFiles[1].path, ` import * as A from '../animals'; ${coreFiles[1].content}`), - // build core - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - } - ] - }); + // build core + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + } + ] + }); - function subProjectFiles(subProject: string, fileNames: readonly string[]): ts.tscWatch.File[] { - return fileNames.map(file => projectFile(`${subProject}/${file}`)); - } + function subProjectFiles(subProject: string, fileNames: readonly string[]): ts.tscWatch.File[] { + return fileNames.map(file => projectFile(`${subProject}/${file}`)); + } - function projectFile(fileName: string): ts.tscWatch.File { - return ts.TestFSWithWatch.getTsBuildProjectFile("demo", fileName); - } - }); -} \ No newline at end of file + function projectFile(fileName: string): ts.tscWatch.File { + return ts.TestFSWithWatch.getTsBuildProjectFile("demo", fileName); + } +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/moduleResolution.ts b/src/testRunner/unittests/tsbuildWatch/moduleResolution.ts index 69c317ba8fdca..2e861fcb19de5 100644 --- a/src/testRunner/unittests/tsbuildWatch/moduleResolution.ts +++ b/src/testRunner/unittests/tsbuildWatch/moduleResolution.ts @@ -1,53 +1,53 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: module resolution different in referenced project", () => { - ts.tscWatch.verifyTscWatch({ - scenario: "moduleResolutionCache", - subScenario: "handles the cache correctly when two projects use different module resolution settings", - sys: () => ts.tscWatch.createWatchedSystem([ - { path: `${ts.tscWatch.projectRoot}/project1/index.ts`, content: `import { foo } from "file";` }, - { path: `${ts.tscWatch.projectRoot}/project1/node_modules/file/index.d.ts`, content: "export const foo = 10;" }, - { - path: `${ts.tscWatch.projectRoot}/project1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, types: ["foo", "bar"] }, - files: ["index.ts"] - }) - }, - { path: `${ts.tscWatch.projectRoot}/project2/index.ts`, content: `import { foo } from "file";` }, - { path: `${ts.tscWatch.projectRoot}/project2/file.d.ts`, content: "export const foo = 10;" }, - { - path: `${ts.tscWatch.projectRoot}/project2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, types: ["foo"], moduleResolution: "classic" }, - files: ["index.ts"] - }) - }, - { path: `${ts.tscWatch.projectRoot}/node_modules/@types/foo/index.d.ts`, content: "export const foo = 10;" }, - { path: `${ts.tscWatch.projectRoot}/node_modules/@types/bar/index.d.ts`, content: "export const bar = 10;" }, - { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - files: [], - references: [ - { path: "./project1" }, - { path: "./project2" } - ] - }) - }, - ts.tscWatch.libFile - ], { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["--b", "-w", "-v"], - changes: [ +describe("unittests:: tsbuildWatch:: watchMode:: module resolution different in referenced project", () => { + ts.tscWatch.verifyTscWatch({ + scenario: "moduleResolutionCache", + subScenario: "handles the cache correctly when two projects use different module resolution settings", + sys: () => ts.tscWatch.createWatchedSystem([ + { path: `${ts.tscWatch.projectRoot}/project1/index.ts`, content: `import { foo } from "file";` }, + { path: `${ts.tscWatch.projectRoot}/project1/node_modules/file/index.d.ts`, content: "export const foo = 10;" }, { - caption: "Append text", - change: sys => sys.appendFile(`${ts.tscWatch.projectRoot}/project1/index.ts`, "const bar = 10;"), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // build project1 - sys.checkTimeoutQueueLengthAndRun(1); // Solution - sys.checkTimeoutQueueLength(0); - } + path: `${ts.tscWatch.projectRoot}/project1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, types: ["foo", "bar"] }, + files: ["index.ts"] + }) }, - ] - }); + { path: `${ts.tscWatch.projectRoot}/project2/index.ts`, content: `import { foo } from "file";` }, + { path: `${ts.tscWatch.projectRoot}/project2/file.d.ts`, content: "export const foo = 10;" }, + { + path: `${ts.tscWatch.projectRoot}/project2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, types: ["foo"], moduleResolution: "classic" }, + files: ["index.ts"] + }) + }, + { path: `${ts.tscWatch.projectRoot}/node_modules/@types/foo/index.d.ts`, content: "export const foo = 10;" }, + { path: `${ts.tscWatch.projectRoot}/node_modules/@types/bar/index.d.ts`, content: "export const bar = 10;" }, + { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + files: [], + references: [ + { path: "./project1" }, + { path: "./project2" } + ] + }) + }, + ts.tscWatch.libFile + ], { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["--b", "-w", "-v"], + changes: [ + { + caption: "Append text", + change: sys => sys.appendFile(`${ts.tscWatch.projectRoot}/project1/index.ts`, "const bar = 10;"), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // build project1 + sys.checkTimeoutQueueLengthAndRun(1); // Solution + sys.checkTimeoutQueueLength(0); + } + }, + ] }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/noEmit.ts b/src/testRunner/unittests/tsbuildWatch/noEmit.ts index aed4b288db20a..6385069cce8a3 100644 --- a/src/testRunner/unittests/tsbuildWatch/noEmit.ts +++ b/src/testRunner/unittests/tsbuildWatch/noEmit.ts @@ -1,31 +1,31 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: with noEmit", () => { - ts.tscWatch.verifyTscWatch({ - scenario: "noEmit", - subScenario: "does not go in loop when watching when no files are emitted", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => ts.tscWatch.createWatchedSystem([ - ts.tscWatch.libFile, - { path: `${ts.tscWatch.projectRoot}/a.js`, content: "" }, - { path: `${ts.tscWatch.projectRoot}/b.ts`, content: "" }, - { path: `${ts.tscWatch.projectRoot}/tsconfig.json`, content: JSON.stringify({ compilerOptions: { allowJs: true, noEmit: true } }) }, - { path: ts.tscWatch.libFile.path, content: ts.libContent } - ], { currentDirectory: ts.tscWatch.projectRoot }), - changes: [ - { - caption: "No change", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.js`, sys.readFile(`${ts.tscWatch.projectRoot}/a.js`)!), - // build project - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - }, - { - caption: "change", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.js`, "const x = 10;"), - // build project - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - }, - ], - baselineIncremental: true - }); +describe("unittests:: tsbuildWatch:: watchMode:: with noEmit", () => { + ts.tscWatch.verifyTscWatch({ + scenario: "noEmit", + subScenario: "does not go in loop when watching when no files are emitted", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => ts.tscWatch.createWatchedSystem([ + ts.tscWatch.libFile, + { path: `${ts.tscWatch.projectRoot}/a.js`, content: "" }, + { path: `${ts.tscWatch.projectRoot}/b.ts`, content: "" }, + { path: `${ts.tscWatch.projectRoot}/tsconfig.json`, content: JSON.stringify({ compilerOptions: { allowJs: true, noEmit: true } }) }, + { path: ts.tscWatch.libFile.path, content: ts.libContent } + ], { currentDirectory: ts.tscWatch.projectRoot }), + changes: [ + { + caption: "No change", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.js`, sys.readFile(`${ts.tscWatch.projectRoot}/a.js`)!), + // build project + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + }, + { + caption: "change", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.js`, "const x = 10;"), + // build project + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + }, + ], + baselineIncremental: true }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/noEmitOnError.ts b/src/testRunner/unittests/tsbuildWatch/noEmitOnError.ts index 6330a1f89d53a..d6bea71651ba0 100644 --- a/src/testRunner/unittests/tsbuildWatch/noEmitOnError.ts +++ b/src/testRunner/unittests/tsbuildWatch/noEmitOnError.ts @@ -1,43 +1,43 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: with noEmitOnError", () => { - function change(caption: string, content: string): ts.tscWatch.TscWatchCompileChange { - return { - caption, - change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, content), - // build project - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - }; - } - - const noChange: ts.tscWatch.TscWatchCompileChange = { - caption: "No change", - change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`)!), +describe("unittests:: tsbuildWatch:: watchMode:: with noEmitOnError", () => { + function change(caption: string, content: string): ts.tscWatch.TscWatchCompileChange { + return { + caption, + change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, content), // build project timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, }; - ts.tscWatch.verifyTscWatch({ - scenario: "noEmitOnError", - subScenario: "does not emit any files on error", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => ts.tscWatch.createWatchedSystem([ - ...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"] - .map(f => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), - { path: ts.tscWatch.libFile.path, content: ts.libContent } - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError` }), - changes: [ - noChange, - change("Fix Syntax error", `import { A } from "../shared/types/db"; + } + + const noChange: ts.tscWatch.TscWatchCompileChange = { + caption: "No change", + change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`)!), + // build project + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + }; + ts.tscWatch.verifyTscWatch({ + scenario: "noEmitOnError", + subScenario: "does not emit any files on error", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => ts.tscWatch.createWatchedSystem([ + ...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"] + .map(f => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), + { path: ts.tscWatch.libFile.path, content: ts.libContent } + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError` }), + changes: [ + noChange, + change("Fix Syntax error", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`), - change("Semantic Error", `import { A } from "../shared/types/db"; + change("Semantic Error", `import { A } from "../shared/types/db"; const a: string = 10;`), - noChange, - change("Fix Semantic Error", `import { A } from "../shared/types/db"; + noChange, + change("Fix Semantic Error", `import { A } from "../shared/types/db"; const a: string = "hello";`), - noChange, - ], - baselineIncremental: true - }); + noChange, + ], + baselineIncremental: true }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/programUpdates.ts b/src/testRunner/unittests/tsbuildWatch/programUpdates.ts index 7608ac8a29843..cd3bc76fe574f 100644 --- a/src/testRunner/unittests/tsbuildWatch/programUpdates.ts +++ b/src/testRunner/unittests/tsbuildWatch/programUpdates.ts @@ -1,279 +1,279 @@ namespace ts.tscWatch { - import projectsLocation = ts.TestFSWithWatch.tsbuildProjectsLocation; - describe("unittests:: tsbuildWatch:: watchMode:: program updates", () => { - const enum SubProject { - core = "core", - logic = "logic", - tests = "tests", - ui = "ui" +import projectsLocation = ts.TestFSWithWatch.tsbuildProjectsLocation; +describe("unittests:: tsbuildWatch:: watchMode:: program updates", () => { + const enum SubProject { + core = "core", + logic = "logic", + tests = "tests", + ui = "ui" + } + type ReadonlyFile = Readonly; + /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ + type SubProjectFiles = [ + tsconfig: ReadonlyFile, + index: ReadonlyFile + ] | [ + tsconfig: ReadonlyFile, + index: ReadonlyFile, + anotherModule: ReadonlyFile, + someDecl: ReadonlyFile + ]; + function projectFilePath(subProject: SubProject, baseFileName: string) { + return `${ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", subProject)}/${baseFileName.toLowerCase()}`; + } + + function projectFile(subProject: SubProject, baseFileName: string): ts.tscWatch.File { + return ts.TestFSWithWatch.getTsBuildProjectFile("sample1", `${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]; } - type ReadonlyFile = Readonly; - /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ - type SubProjectFiles = [ - tsconfig: ReadonlyFile, - index: ReadonlyFile - ] | [ - tsconfig: ReadonlyFile, - index: ReadonlyFile, - anotherModule: ReadonlyFile, - someDecl: ReadonlyFile - ]; - function projectFilePath(subProject: SubProject, baseFileName: string) { - return `${ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", subProject)}/${baseFileName.toLowerCase()}`; - } - - function projectFile(subProject: SubProject, baseFileName: string): ts.tscWatch.File { - return ts.TestFSWithWatch.getTsBuildProjectFile("sample1", `${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 changeFile(fileName: string | (() => string), content: string | (() => string), caption: string): ts.tscWatch.TscWatchCompileChange { - return { - caption, - change: sys => sys.writeFile(ts.isString(fileName) ? fileName : fileName(), ts.isString(content) ? content : content()), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds core - }; - } - - function changeCore(content: () => string, caption: string) { - return changeFile(() => core[1].path, content, caption); - } - - let core: SubProjectFiles; - let logic: SubProjectFiles; - let tests: SubProjectFiles; - let ui: SubProjectFiles; - let allFiles: readonly ts.tscWatch.File[]; + const anotherModule = projectFile(SubProject.core, "anotherModule.ts"); + const someDecl = projectFile(SubProject.core, "some_decl.ts"); + return [tsconfig, index, anotherModule, someDecl]; + } + + function changeFile(fileName: string | (() => string), content: string | (() => string), caption: string): ts.tscWatch.TscWatchCompileChange { + return { + caption, + change: sys => sys.writeFile(ts.isString(fileName) ? fileName : fileName(), ts.isString(content) ? content : content()), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds core + }; + } + + function changeCore(content: () => string, caption: string) { + return changeFile(() => core[1].path, content, caption); + } + + let core: SubProjectFiles; + let logic: SubProjectFiles; + let tests: SubProjectFiles; + let ui: SubProjectFiles; + let allFiles: readonly ts.tscWatch.File[]; + + before(() => { + core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); + logic = subProjectFiles(SubProject.logic); + tests = subProjectFiles(SubProject.tests); + ui = subProjectFiles(SubProject.ui); + allFiles = [ts.tscWatch.libFile, ...core, ...logic, ...tests, ...ui]; + }); - before(() => { - core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); - logic = subProjectFiles(SubProject.logic); - tests = subProjectFiles(SubProject.tests); - ui = subProjectFiles(SubProject.ui); - allFiles = [ts.tscWatch.libFile, ...core, ...logic, ...tests, ...ui]; - }); + after(() => { + core = undefined!; + logic = undefined!; + tests = undefined!; + ui = undefined!; + allFiles = undefined!; + }); - after(() => { - core = undefined!; - logic = undefined!; - tests = undefined!; - ui = undefined!; - allFiles = undefined!; - }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "creates solution in watch mode", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], + sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ + it("verify building references watches only those projects", () => { + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation })); + const host = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, [`sample1/${SubProject.tests}`], { watch: true }); + solutionBuilder.buildReferences(`sample1/${SubProject.tests}`); + ts.tscWatch.runWatchBaseline({ scenario: "programUpdates", - subScenario: "creates solution in watch mode", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], - sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: ts.emptyArray + subScenario: "verify building references watches only those projects", + commandLineArgs: ["--b", "--w"], + sys, + baseline, + oldSnap, + getPrograms, + changes: ts.emptyArray, + watchOrSolution: solutionBuilder }); + }); - it("verify building references watches only those projects", () => { - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation })); - const host = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); - const solutionBuilder = ts.createSolutionBuilderWithWatch(host, [`sample1/${SubProject.tests}`], { watch: true }); - solutionBuilder.buildReferences(`sample1/${SubProject.tests}`); - ts.tscWatch.runWatchBaseline({ - scenario: "programUpdates", - subScenario: "verify building references watches only those projects", - commandLineArgs: ["--b", "--w"], - sys, - baseline, - oldSnap, - getPrograms, - changes: ts.emptyArray, - watchOrSolution: solutionBuilder - }); - }); + const buildTests: ts.tscWatch.TscWatchCompileChange = { + caption: "Build Tests", + change: ts.noop, + // Build tests + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + }; - const buildTests: ts.tscWatch.TscWatchCompileChange = { - caption: "Build Tests", - change: ts.noop, - // Build tests - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + describe("validates the changes and watched files", () => { + const newFileWithoutExtension = "newFile"; + const newFile: ts.tscWatch.File = { + path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), + content: `export const newFileConst = 30;` }; - describe("validates the changes and watched files", () => { - const newFileWithoutExtension = "newFile"; - const newFile: ts.tscWatch.File = { - path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), - content: `export const newFileConst = 30;` + function verifyProjectChanges(subScenario: string, allFilesGetter: () => readonly ts.tscWatch.File[]) { + const buildLogicOrUpdateTimeStamps: ts.tscWatch.TscWatchCompileChange = { + caption: "Build logic or update time stamps", + change: ts.noop, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds logic or updates timestamps }; - function verifyProjectChanges(subScenario: string, allFilesGetter: () => readonly ts.tscWatch.File[]) { - const buildLogicOrUpdateTimeStamps: ts.tscWatch.TscWatchCompileChange = { - caption: "Build logic or update time stamps", - change: ts.noop, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds logic or updates timestamps - }; - - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: `${subScenario}/change builds changes and reports found errors message`, - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], - sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), - changes: [ - changeCore(() => `${core[1].content} + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: `${subScenario}/change builds changes and reports found errors message`, + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], + sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + changeCore(() => `${core[1].content} export class someClass { }`, "Make change to core"), - buildLogicOrUpdateTimeStamps, - buildTests, - // Another change requeues and builds it - changeCore(() => core[1].content, "Revert core file"), - buildLogicOrUpdateTimeStamps, - buildTests, - { - caption: "Make two changes", - change: sys => { - const change1 = `${core[1].content} + buildLogicOrUpdateTimeStamps, + buildTests, + // Another change requeues and builds it + changeCore(() => core[1].content, "Revert core file"), + buildLogicOrUpdateTimeStamps, + buildTests, + { + caption: "Make two changes", + change: 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 { }`); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds core }, - buildLogicOrUpdateTimeStamps, - buildTests, - ] - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds core + }, + buildLogicOrUpdateTimeStamps, + buildTests, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: `${subScenario}/non local change does not start build of referencing projects`, - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], - sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), - changes: [ - changeCore(() => `${core[1].content} + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: `${subScenario}/non local change does not start build of referencing projects`, + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], + sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + changeCore(() => `${core[1].content} function foo() { }`, "Make local change to core"), - buildLogicOrUpdateTimeStamps, - buildTests - ] - }); + buildLogicOrUpdateTimeStamps, + buildTests + ] + }); - function changeNewFile(newFileContent: string) { - return changeFile(newFile.path, newFileContent, "Change to new File and build core"); - } - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: `${subScenario}/builds when new file is added, and its subsequent updates`, - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], - sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), - changes: [ - changeNewFile(newFile.content), - buildLogicOrUpdateTimeStamps, - buildTests, - changeNewFile(`${newFile.content} -export class someClass2 { }`), - buildLogicOrUpdateTimeStamps, - buildTests - ] - }); + function changeNewFile(newFileContent: string) { + return changeFile(newFile.path, newFileContent, "Change to new File and build core"); } - - describe("with simple project reference graph", () => { - verifyProjectChanges("with simple project reference graph", () => allFiles); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: `${subScenario}/builds when new file is added, and its subsequent updates`, + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], + sys: () => ts.tscWatch.createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + changeNewFile(newFile.content), + buildLogicOrUpdateTimeStamps, + buildTests, + changeNewFile(`${newFile.content} +export class someClass2 { }`), + buildLogicOrUpdateTimeStamps, + buildTests + ] }); + } - describe("with circular project reference", () => { - verifyProjectChanges("with circular project reference", () => { - const [coreTsconfig, ...otherCoreFiles] = core; - const circularCoreConfig: ts.tscWatch.File = { - path: coreTsconfig.path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - references: [{ path: "../tests", circular: true }] - }) - }; - return [ts.tscWatch.libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; - }); + describe("with simple project reference graph", () => { + verifyProjectChanges("with simple project reference graph", () => allFiles); }); + describe("with circular project reference", () => { + verifyProjectChanges("with circular project reference", () => { + const [coreTsconfig, ...otherCoreFiles] = core; + const circularCoreConfig: ts.tscWatch.File = { + path: coreTsconfig.path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + references: [{ path: "../tests", circular: true }] + }) + }; + return [ts.tscWatch.libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; }); + }); + + }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "watches config files that are not present", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ...core, logic[1], ...tests], { currentDirectory: projectsLocation }), + changes: [ + { + caption: "Write logic tsconfig and build logic", + change: sys => sys.writeFile(logic[0].path, logic[0].content), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds logic + }, + buildTests + ] + }); + + describe("when referenced using prepend, builds referencing project even for non local change", () => { + let coreIndex: ts.tscWatch.File; + before(() => { + coreIndex = { + path: core[1].path, + content: `function foo() { return 10; }` + }; + }); + after(() => { + coreIndex = undefined!; + }); + const buildLogic: ts.tscWatch.TscWatchCompileChange = { + caption: "Build logic", + change: ts.noop, + // Builds logic + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + }; ts.tscWatch.verifyTscWatch({ scenario: "programUpdates", - subScenario: "watches config files that are not present", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ...core, logic[1], ...tests], { currentDirectory: projectsLocation }), - changes: [ - { - caption: "Write logic tsconfig and build logic", - change: sys => sys.writeFile(logic[0].path, logic[0].content), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Builds logic - }, - buildTests - ] - }); - - describe("when referenced using prepend, builds referencing project even for non local change", () => { - let coreIndex: ts.tscWatch.File; - before(() => { - coreIndex = { - path: core[1].path, - content: `function foo() { return 10; }` + subScenario: "when referenced using prepend builds referencing project even for non local change", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.logic}`], + sys: () => { + const coreTsConfig: ts.tscWatch.File = { + path: core[0].path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true, outFile: "index.js" } + }) }; - }); - after(() => { - coreIndex = undefined!; - }); - const buildLogic: ts.tscWatch.TscWatchCompileChange = { - caption: "Build logic", - change: ts.noop, - // Builds logic - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - }; - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "when referenced using prepend builds referencing project even for non local change", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.logic}`], - sys: () => { - const coreTsConfig: ts.tscWatch.File = { - path: core[0].path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true, outFile: "index.js" } - }) - }; - const logicTsConfig: ts.tscWatch.File = { - path: logic[0].path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true, outFile: "index.js" }, - references: [{ path: "../core", prepend: true }] - }) - }; - const logicIndex: ts.tscWatch.File = { - path: logic[1].path, - content: `function bar() { return foo() + 1 };` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, coreTsConfig, coreIndex, logicTsConfig, logicIndex], { currentDirectory: projectsLocation }); - }, - changes: [ - changeCore(() => `${coreIndex.content} + const logicTsConfig: ts.tscWatch.File = { + path: logic[0].path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true, outFile: "index.js" }, + references: [{ path: "../core", prepend: true }] + }) + }; + const logicIndex: ts.tscWatch.File = { + path: logic[1].path, + content: `function bar() { return foo() + 1 };` + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, coreTsConfig, coreIndex, logicTsConfig, logicIndex], { currentDirectory: projectsLocation }); + }, + changes: [ + changeCore(() => `${coreIndex.content} function myFunc() { return 10; }`, "Make non local change and build core"), - buildLogic, - changeCore(() => `${coreIndex.content} + buildLogic, + changeCore(() => `${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}/sample1/Library`; - const libraryTs: ts.tscWatch.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}/sample1/Library`; + const libraryTs: ts.tscWatch.File = { + path: `${subProjectLibrary}/library.ts`, + content: ` interface SomeObject { message: string; @@ -285,441 +285,441 @@ export function createSomeObject(): SomeObject message: "new Object" }; }` - }; - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "when referenced project change introduces error in the down stream project and then fixes it", - commandLineArgs: ["-b", "-w", "App"], - sys: () => { - const libraryTsconfig: ts.tscWatch.File = { - path: `${subProjectLibrary}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true } }) - }; - const subProjectApp = `${projectsLocation}/sample1/App`; - const appTs: ts.tscWatch.File = { - path: `${subProjectApp}/app.ts`, - content: `import { createSomeObject } from "../Library/library"; + }; + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "when referenced project change introduces error in the down stream project and then fixes it", + commandLineArgs: ["-b", "-w", "App"], + sys: () => { + const libraryTsconfig: ts.tscWatch.File = { + path: `${subProjectLibrary}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true } }) + }; + const subProjectApp = `${projectsLocation}/sample1/App`; + const appTs: ts.tscWatch.File = { + path: `${subProjectApp}/app.ts`, + content: `import { createSomeObject } from "../Library/library"; createSomeObject().message;` - }; - const appTsconfig: ts.tscWatch.File = { - path: `${subProjectApp}/tsconfig.json`, - content: JSON.stringify({ references: [{ path: "../Library" }] }) - }; + }; + const appTsconfig: ts.tscWatch.File = { + path: `${subProjectApp}/tsconfig.json`, + content: JSON.stringify({ references: [{ path: "../Library" }] }) + }; - const files = [ts.tscWatch.libFile, libraryTs, libraryTsconfig, appTs, appTsconfig]; - return ts.tscWatch.createWatchedSystem(files, { currentDirectory: `${projectsLocation}/sample1` }); + const files = [ts.tscWatch.libFile, libraryTs, libraryTsconfig, appTs, appTsconfig]; + return ts.tscWatch.createWatchedSystem(files, { currentDirectory: `${projectsLocation}/sample1` }); + }, + changes: [ + { + caption: "Introduce error", + // Change message in library to message2 + change: sys => sys.writeFile(libraryTs.path, libraryTs.content.replace(/message/g, "message2")), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // Build library + sys.checkTimeoutQueueLengthAndRun(1); // Build App + }, + }, + { + caption: "Fix error", + // Revert library changes + change: sys => sys.writeFile(libraryTs.path, libraryTs.content), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // Build library + sys.checkTimeoutQueueLengthAndRun(1); // Build App + }, }, + ] + }); + + }); + + describe("reports errors in all projects on incremental compile", () => { + function verifyIncrementalErrors(subScenario: string, buildOptions: readonly string[]) { + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: `reportErrors/${subScenario}`, + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`, ...buildOptions], + sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), changes: [ { - caption: "Introduce error", - // Change message in library to message2 - change: sys => sys.writeFile(libraryTs.path, libraryTs.content.replace(/message/g, "message2")), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // Build library - sys.checkTimeoutQueueLengthAndRun(1); // Build App - }, + caption: "change logic", + change: sys => sys.writeFile(logic[1].path, `${logic[1].content} +let y: string = 10;`), + // Builds logic + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, }, { - caption: "Fix error", - // Revert library changes - change: sys => sys.writeFile(libraryTs.path, libraryTs.content), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // Build library - sys.checkTimeoutQueueLengthAndRun(1); // Build App - }, - }, + caption: "change core", + change: sys => sys.writeFile(core[1].path, `${core[1].content} +let x: string = 10;`), + // Builds core + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + } ] }); - - }); - - describe("reports errors in all projects on incremental compile", () => { - function verifyIncrementalErrors(subScenario: string, buildOptions: readonly string[]) { - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: `reportErrors/${subScenario}`, - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`, ...buildOptions], - sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: [ - { - caption: "change logic", - change: sys => sys.writeFile(logic[1].path, `${logic[1].content} -let y: string = 10;`), - // Builds logic - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - }, - { - caption: "change core", - change: sys => sys.writeFile(core[1].path, `${core[1].content} -let x: string = 10;`), - // Builds core - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - } - ] - }); - } - verifyIncrementalErrors("when preserveWatchOutput is not used", ts.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: ts.tscWatch.File = { - path: `${subProjectLocation}/fileWithError.ts`, - content: `export var myClassWithError = class { + } + verifyIncrementalErrors("when preserveWatchOutput is not used", ts.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: ts.tscWatch.File = { + path: `${subProjectLocation}/fileWithError.ts`, + content: `export var myClassWithError = class { tags() { } private p = 12 };` - }; - const fileWithFixedError: ts.tscWatch.File = { - path: fileWithError.path, - content: fileWithError.content.replace("private p = 12", "") - }; - const fileWithoutError: ts.tscWatch.File = { - path: `${subProjectLocation}/fileWithoutError.ts`, - content: `export class myClass { }` - }; - const tsconfig: ts.tscWatch.File = { - path: `${subProjectLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true } }) - }; + }; + const fileWithFixedError: ts.tscWatch.File = { + path: fileWithError.path, + content: fileWithError.content.replace("private p = 12", "") + }; + const fileWithoutError: ts.tscWatch.File = { + path: `${subProjectLocation}/fileWithoutError.ts`, + content: `export class myClass { }` + }; + const tsconfig: ts.tscWatch.File = { + path: `${subProjectLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true } }) + }; - function incrementalBuild(sys: ts.tscWatch.WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // Build the app - sys.checkTimeoutQueueLength(0); - } + function incrementalBuild(sys: ts.tscWatch.WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // Build the app + sys.checkTimeoutQueueLength(0); + } - const fixError: ts.tscWatch.TscWatchCompileChange = { - caption: "Fix error in fileWithError", - // Fix error - change: sys => sys.writeFile(fileWithError.path, fileWithFixedError.content), - timeouts: incrementalBuild - }; + const fixError: ts.tscWatch.TscWatchCompileChange = { + caption: "Fix error in fileWithError", + // Fix error + change: sys => sys.writeFile(fileWithError.path, fileWithFixedError.content), + timeouts: incrementalBuild + }; + + const changeFileWithoutError: ts.tscWatch.TscWatchCompileChange = { + caption: "Change fileWithoutError", + change: sys => sys.writeFile(fileWithoutError.path, fileWithoutError.content.replace(/myClass/g, "myClass2")), + timeouts: incrementalBuild + }; + + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "reportErrors/declarationEmitErrors/when fixing error files all files are emitted", + commandLineArgs: ["-b", "-w", subProject], + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + changes: [ + fixError + ] + }); - const changeFileWithoutError: ts.tscWatch.TscWatchCompileChange = { - caption: "Change fileWithoutError", - change: sys => sys.writeFile(fileWithoutError.path, fileWithoutError.content.replace(/myClass/g, "myClass2")), - timeouts: incrementalBuild + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "reportErrors/declarationEmitErrors/when file with no error changes", + commandLineArgs: ["-b", "-w", subProject], + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + changes: [ + changeFileWithoutError + ] + }); + + describe("when reporting errors on introducing error", () => { + const introduceError: ts.tscWatch.TscWatchCompileChange = { + caption: "Introduce error", + change: sys => sys.writeFile(fileWithError.path, fileWithError.content), + timeouts: incrementalBuild, }; ts.tscWatch.verifyTscWatch({ scenario: "programUpdates", - 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: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), changes: [ + introduceError, fixError ] }); ts.tscWatch.verifyTscWatch({ scenario: "programUpdates", - subScenario: "reportErrors/declarationEmitErrors/when file with no error changes", + subScenario: "reportErrors/declarationEmitErrors/introduceError/when file with no error changes", commandLineArgs: ["-b", "-w", subProject], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), changes: [ + introduceError, changeFileWithoutError ] }); - - describe("when reporting errors on introducing error", () => { - const introduceError: ts.tscWatch.TscWatchCompileChange = { - caption: "Introduce error", - change: sys => sys.writeFile(fileWithError.path, fileWithError.content), - timeouts: incrementalBuild, - }; - - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "reportErrors/declarationEmitErrors/introduceError/when fixing errors only changed file is emitted", - commandLineArgs: ["-b", "-w", subProject], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), - changes: [ - introduceError, - fixError - ] - }); - - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "reportErrors/declarationEmitErrors/introduceError/when file with no error changes", - commandLineArgs: ["-b", "-w", subProject], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), - changes: [ - introduceError, - changeFileWithoutError - ] - }); - }); }); }); + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "incremental updates in verbose mode", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`, "-verbose"], - sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: [ - { - caption: "Make non dts change", - change: sys => sys.writeFile(logic[1].path, `${logic[1].content} + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "incremental updates in verbose mode", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.tests}`, "-verbose"], + sys: () => ts.tscWatch.createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), + changes: [ + { + caption: "Make non dts change", + change: sys => sys.writeFile(logic[1].path, `${logic[1].content} function someFn() { }`), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // build logic - sys.checkTimeoutQueueLengthAndRun(1); // build tests - }, + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // build logic + sys.checkTimeoutQueueLengthAndRun(1); // build tests }, - { - caption: "Make dts change", - change: sys => sys.writeFile(logic[1].path, `${logic[1].content} + }, + { + caption: "Make dts change", + change: sys => sys.writeFile(logic[1].path, `${logic[1].content} export function someFn() { }`), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // build logic - sys.checkTimeoutQueueLengthAndRun(1); // build tests - }, - } - ], - }); + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // build logic + sys.checkTimeoutQueueLengthAndRun(1); // build tests + }, + } + ], + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "works when noUnusedParameters changes to false", - commandLineArgs: ["-b", "-w"], - sys: () => { - const index: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/index.ts`, - content: `const fn = (a: string, b: string) => b;` - }; - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - noUnusedParameters: true - } - }) - }; - return ts.tscWatch.createWatchedSystem([index, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "works when noUnusedParameters changes to false", + commandLineArgs: ["-b", "-w"], + sys: () => { + const index: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/index.ts`, + content: `const fn = (a: string, b: string) => b;` + }; + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + noUnusedParameters: true + } + }) + }; + return ts.tscWatch.createWatchedSystem([index, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Change tsconfig to set noUnusedParameters to false", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ + compilerOptions: { + noUnusedParameters: false + } + })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Change tsconfig to set noUnusedParameters to false", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ - compilerOptions: { - noUnusedParameters: false - } - })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "should not trigger recompilation because of program emit", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.core}`, "-verbose"], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ...core], { currentDirectory: projectsLocation }), - changes: [ - ts.tscWatch.noopChange, - { - caption: "Add new file", - change: sys => sys.writeFile(`sample1/${SubProject.core}/file3.ts`, `export const y = 10;`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - ts.tscWatch.noopChange, - ] - }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "should not trigger recompilation because of program emit", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.core}`, "-verbose"], + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ...core], { currentDirectory: projectsLocation }), + changes: [ + ts.tscWatch.noopChange, + { + caption: "Add new file", + change: sys => sys.writeFile(`sample1/${SubProject.core}/file3.ts`, `export const y = 10;`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + ts.tscWatch.noopChange, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "should not trigger recompilation because of program emit with outDir specified", - commandLineArgs: ["-b", "-w", `sample1/${SubProject.core}`, "-verbose"], - sys: () => { - const [coreConfig, ...rest] = core; - const newCoreConfig: ts.tscWatch.File = { path: coreConfig.path, content: JSON.stringify({ compilerOptions: { composite: true, outDir: "outDir" } }) }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, newCoreConfig, ...rest], { currentDirectory: projectsLocation }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "should not trigger recompilation because of program emit with outDir specified", + commandLineArgs: ["-b", "-w", `sample1/${SubProject.core}`, "-verbose"], + sys: () => { + const [coreConfig, ...rest] = core; + const newCoreConfig: ts.tscWatch.File = { path: coreConfig.path, content: JSON.stringify({ compilerOptions: { composite: true, outDir: "outDir" } }) }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, newCoreConfig, ...rest], { currentDirectory: projectsLocation }); + }, + changes: [ + ts.tscWatch.noopChange, + { + caption: "Add new file", + change: sys => sys.writeFile(`sample1/${SubProject.core}/file3.ts`, `export const y = 10;`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun }, - changes: [ - ts.tscWatch.noopChange, - { - caption: "Add new file", - change: sys => sys.writeFile(`sample1/${SubProject.core}/file3.ts`, `export const y = 10;`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - ts.tscWatch.noopChange - ] - }); + ts.tscWatch.noopChange + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "works with extended source files", - commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json"], - sys: () => { - const alphaExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/alpha.tsconfig.json", - content: "{}" - }; - const project1Config: ts.tscWatch.File = { - path: "/a/b/project1.tsconfig.json", - content: JSON.stringify({ - extends: "./alpha.tsconfig.json", - compilerOptions: { - composite: true, - }, - files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] - }) - }; - const bravoExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/bravo.tsconfig.json", - content: JSON.stringify({ - extends: "./alpha.tsconfig.json" - }) - }; - const otherFile: ts.tscWatch.File = { - path: "/a/b/other.ts", - content: "let z = 0;", - }; - const project2Config: ts.tscWatch.File = { - path: "/a/b/project2.tsconfig.json", - content: JSON.stringify({ - extends: "./bravo.tsconfig.json", - compilerOptions: { - composite: true, - }, - files: [otherFile.path] - }) - }; - return ts.tscWatch.createWatchedSystem([ - ts.tscWatch.libFile, - alphaExtendedConfigFile, project1Config, - ts.tscWatch.commonFile1, - ts.tscWatch.commonFile2, - bravoExtendedConfigFile, project2Config, otherFile - ], { currentDirectory: "/a/b" }); + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "works with extended source files", + commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json"], + sys: () => { + const alphaExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/alpha.tsconfig.json", + content: "{}" + }; + const project1Config: ts.tscWatch.File = { + path: "/a/b/project1.tsconfig.json", + content: JSON.stringify({ + extends: "./alpha.tsconfig.json", + compilerOptions: { + composite: true, + }, + files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] + }) + }; + const bravoExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/bravo.tsconfig.json", + content: JSON.stringify({ + extends: "./alpha.tsconfig.json" + }) + }; + const otherFile: ts.tscWatch.File = { + path: "/a/b/other.ts", + content: "let z = 0;", + }; + const project2Config: ts.tscWatch.File = { + path: "/a/b/project2.tsconfig.json", + content: JSON.stringify({ + extends: "./bravo.tsconfig.json", + compilerOptions: { + composite: true, + }, + files: [otherFile.path] + }) + }; + return ts.tscWatch.createWatchedSystem([ + ts.tscWatch.libFile, + alphaExtendedConfigFile, project1Config, + ts.tscWatch.commonFile1, + ts.tscWatch.commonFile2, + bravoExtendedConfigFile, project2Config, otherFile + ], { currentDirectory: "/a/b" }); + }, + changes: [ + { + caption: "Modify alpha config", + change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", JSON.stringify({ + compilerOptions: { strict: true } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun // Build project1 }, - changes: [ - { - caption: "Modify alpha config", - change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", JSON.stringify({ - compilerOptions: { strict: true } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun // Build project1 - }, - { - caption: "Build project 2", - change: ts.noop, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 - }, - { - caption: "change bravo config", - change: sys => sys.writeFile("/a/b/bravo.tsconfig.json", JSON.stringify({ - extends: "./alpha.tsconfig.json", - compilerOptions: { strict: false } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 - }, - { - caption: "project 2 extends alpha", - change: sys => sys.writeFile("/a/b/project2.tsconfig.json", JSON.stringify({ - extends: "./alpha.tsconfig.json", - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 - }, - { - caption: "update aplha config", - change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", "{}"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // build project1 - }, - { - caption: "Build project 2", - change: ts.noop, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 - }, - ] - }); + { + caption: "Build project 2", + change: ts.noop, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 + }, + { + caption: "change bravo config", + change: sys => sys.writeFile("/a/b/bravo.tsconfig.json", JSON.stringify({ + extends: "./alpha.tsconfig.json", + compilerOptions: { strict: false } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 + }, + { + caption: "project 2 extends alpha", + change: sys => sys.writeFile("/a/b/project2.tsconfig.json", JSON.stringify({ + extends: "./alpha.tsconfig.json", + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 + }, + { + caption: "update aplha config", + change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", "{}"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // build project1 + }, + { + caption: "Build project 2", + change: ts.noop, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "programUpdates", - subScenario: "works correctly when project with extended config is removed", - commandLineArgs: ["-b", "-w", "-v"], - sys: () => { - const alphaExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/alpha.tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - strict: true - } - }) - }; - const project1Config: ts.tscWatch.File = { - path: "/a/b/project1.tsconfig.json", - content: JSON.stringify({ - extends: "./alpha.tsconfig.json", - compilerOptions: { - composite: true, + ts.tscWatch.verifyTscWatch({ + scenario: "programUpdates", + subScenario: "works correctly when project with extended config is removed", + commandLineArgs: ["-b", "-w", "-v"], + sys: () => { + const alphaExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/alpha.tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + strict: true + } + }) + }; + const project1Config: ts.tscWatch.File = { + path: "/a/b/project1.tsconfig.json", + content: JSON.stringify({ + extends: "./alpha.tsconfig.json", + compilerOptions: { + composite: true, + }, + files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] + }) + }; + const bravoExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/bravo.tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + strict: true + } + }) + }; + const otherFile: ts.tscWatch.File = { + path: "/a/b/other.ts", + content: "let z = 0;", + }; + const project2Config: ts.tscWatch.File = { + path: "/a/b/project2.tsconfig.json", + content: JSON.stringify({ + extends: "./bravo.tsconfig.json", + compilerOptions: { + composite: true, + }, + files: [otherFile.path] + }) + }; + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + references: [ + { + path: "./project1.tsconfig.json", }, - files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] - }) - }; - const bravoExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/bravo.tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - strict: true - } - }) - }; - const otherFile: ts.tscWatch.File = { - path: "/a/b/other.ts", - content: "let z = 0;", - }; - const project2Config: ts.tscWatch.File = { - path: "/a/b/project2.tsconfig.json", - content: JSON.stringify({ - extends: "./bravo.tsconfig.json", - compilerOptions: { - composite: true, + { + path: "./project2.tsconfig.json", }, - files: [otherFile.path] - }) - }; - const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - references: [ - { - path: "./project1.tsconfig.json", - }, - { - path: "./project2.tsconfig.json", - }, - ], - files: [], - }) - }; - return ts.tscWatch.createWatchedSystem([ - ts.tscWatch.libFile, - configFile, - alphaExtendedConfigFile, project1Config, - ts.tscWatch.commonFile1, - ts.tscWatch.commonFile2, - bravoExtendedConfigFile, project2Config, otherFile - ], { currentDirectory: "/a/b" }); - }, - changes: [ - { - caption: "Remove project2 from base config", - change: sys => sys.modifyFile("/a/b/tsconfig.json", JSON.stringify({ - references: [ - { - path: "./project1.tsconfig.json", - }, - ], - files: [], - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, - } - ] - }); + ], + files: [], + }) + }; + return ts.tscWatch.createWatchedSystem([ + ts.tscWatch.libFile, + configFile, + alphaExtendedConfigFile, project1Config, + ts.tscWatch.commonFile1, + ts.tscWatch.commonFile2, + bravoExtendedConfigFile, project2Config, otherFile + ], { currentDirectory: "/a/b" }); + }, + changes: [ + { + caption: "Remove project2 from base config", + change: sys => sys.modifyFile("/a/b/tsconfig.json", JSON.stringify({ + references: [ + { + path: "./project1.tsconfig.json", + }, + ], + files: [], + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, + } + ] }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/publicApi.ts b/src/testRunner/unittests/tsbuildWatch/publicApi.ts index ef2f34fdfd478..b02f4651f7e8e 100644 --- a/src/testRunner/unittests/tsbuildWatch/publicApi.ts +++ b/src/testRunner/unittests/tsbuildWatch/publicApi.ts @@ -1,106 +1,106 @@ namespace ts.tscWatch { - it("unittests:: tsbuildWatch:: watchMode:: Public API with custom transformers", () => { - const solution: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - references: [ - { path: "./shared/tsconfig.json" }, - { path: "./webpack/tsconfig.json" } - ], - files: [] - }) - }; - const sharedConfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/shared/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true }, - }) - }; - const sharedIndex: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/shared/index.ts`, - content: `export function f1() { } +it("unittests:: tsbuildWatch:: watchMode:: Public API with custom transformers", () => { + const solution: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + references: [ + { path: "./shared/tsconfig.json" }, + { path: "./webpack/tsconfig.json" } + ], + files: [] + }) + }; + const sharedConfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/shared/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true }, + }) + }; + const sharedIndex: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/shared/index.ts`, + content: `export function f1() { } export class c { } export enum e { } // leading export function f2() { } // trailing` - }; - const webpackConfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/webpack/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, }, - references: [{ path: "../shared/tsconfig.json" }] - }) - }; - const webpackIndex: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/webpack/index.ts`, - content: `export function f2() { } + }; + const webpackConfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/webpack/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, }, + references: [{ path: "../shared/tsconfig.json" }] + }) + }; + const webpackIndex: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/webpack/index.ts`, + content: `export function f2() { } export class c2 { } export enum e2 { } // leading export function f22() { } // trailing` - }; - const commandLineArgs = ["--b", "--w"]; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, solution, sharedConfig, sharedIndex, webpackConfig, webpackIndex], { currentDirectory: ts.tscWatch.projectRoot })); - const buildHost = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); - buildHost.getCustomTransformers = getCustomTransformers; - const builder = ts.createSolutionBuilderWithWatch(buildHost, [solution.path], { verbose: true }); - builder.build(); - ts.tscWatch.runWatchBaseline({ - scenario: "publicApi", - subScenario: "with custom transformers", - commandLineArgs, - sys, - baseline, - oldSnap, - getPrograms, - changes: [ - { - caption: "change to shared", - change: sys => sys.prependFile(sharedIndex.path, "export function fooBar() {}"), - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // Shared - sys.checkTimeoutQueueLengthAndRun(1); // webpack - sys.checkTimeoutQueueLengthAndRun(1); // solution - sys.checkTimeoutQueueLength(0); - } + }; + const commandLineArgs = ["--b", "--w"]; + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, solution, sharedConfig, sharedIndex, webpackConfig, webpackIndex], { currentDirectory: ts.tscWatch.projectRoot })); + const buildHost = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); + buildHost.getCustomTransformers = getCustomTransformers; + const builder = ts.createSolutionBuilderWithWatch(buildHost, [solution.path], { verbose: true }); + builder.build(); + ts.tscWatch.runWatchBaseline({ + scenario: "publicApi", + subScenario: "with custom transformers", + commandLineArgs, + sys, + baseline, + oldSnap, + getPrograms, + changes: [ + { + caption: "change to shared", + change: sys => sys.prependFile(sharedIndex.path, "export function fooBar() {}"), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // Shared + sys.checkTimeoutQueueLengthAndRun(1); // webpack + sys.checkTimeoutQueueLengthAndRun(1); // solution + sys.checkTimeoutQueueLength(0); } - ], - watchOrSolution: builder - }); + } + ], + watchOrSolution: builder + }); - function getCustomTransformers(project: string): ts.CustomTransformers { - const before: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.FunctionDeclaration: - return visitFunction(node as ts.FunctionDeclaration); - default: - return ts.visitEachChild(node, visit, context); - } - } - function visitFunction(node: ts.FunctionDeclaration) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `@before${project}`, /*hasTrailingNewLine*/ true); - return node; + function getCustomTransformers(project: string): ts.CustomTransformers { + const before: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + return visitFunction(node as ts.FunctionDeclaration); + default: + return ts.visitEachChild(node, visit, context); } - }; + } + function visitFunction(node: ts.FunctionDeclaration) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `@before${project}`, /*hasTrailingNewLine*/ true); + return node; + } + }; - const after: ts.TransformerFactory = context => { - return file => ts.visitEachChild(file, visit, context); - function visit(node: ts.Node): ts.VisitResult { - switch (node.kind) { - case ts.SyntaxKind.VariableStatement: - return visitVariableStatement(node as ts.VariableStatement); - default: - return ts.visitEachChild(node, visit, context); - } + const after: ts.TransformerFactory = context => { + return file => ts.visitEachChild(file, visit, context); + function visit(node: ts.Node): ts.VisitResult { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + return visitVariableStatement(node as ts.VariableStatement); + default: + return ts.visitEachChild(node, visit, context); } - function visitVariableStatement(node: ts.VariableStatement) { - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, `@after${project}`); - return node; - } - }; - return { before: [before], after: [after] }; - } - }); -} \ No newline at end of file + } + function visitVariableStatement(node: ts.VariableStatement) { + ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, `@after${project}`); + return node; + } + }; + return { before: [before], after: [after] }; + } +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/reexport.ts b/src/testRunner/unittests/tsbuildWatch/reexport.ts index 89e80bc03ae3f..1d8cefa4e7362 100644 --- a/src/testRunner/unittests/tsbuildWatch/reexport.ts +++ b/src/testRunner/unittests/tsbuildWatch/reexport.ts @@ -1,36 +1,36 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchMode:: with reexport when referenced project reexports definitions from another file", () => { - function build(sys: ts.tscWatch.WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // build src/pure - sys.checkTimeoutQueueLengthAndRun(1); // build src/main - sys.checkTimeoutQueueLengthAndRun(1); // build src - sys.checkTimeoutQueueLength(0); - } - ts.tscWatch.verifyTscWatch({ - scenario: "reexport", - subScenario: "Reports errors correctly", - commandLineArgs: ["-b", "-w", "-verbose", "src"], - sys: () => ts.tscWatch.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 => ts.TestFSWithWatch.getTsBuildProjectFile("reexport", f)), - { path: ts.tscWatch.libFile.path, content: ts.libContent } - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport` }), - changes: [ - { - caption: "Introduce error", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport/src/pure/session.ts`, "// ", ""), - timeouts: build, - }, - { - caption: "Fix error", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport/src/pure/session.ts`, "bar: ", "// bar: "), - timeouts: build - } - ] - }); +describe("unittests:: tsbuildWatch:: watchMode:: with reexport when referenced project reexports definitions from another file", () => { + function build(sys: ts.tscWatch.WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // build src/pure + sys.checkTimeoutQueueLengthAndRun(1); // build src/main + sys.checkTimeoutQueueLengthAndRun(1); // build src + sys.checkTimeoutQueueLength(0); + } + ts.tscWatch.verifyTscWatch({ + scenario: "reexport", + subScenario: "Reports errors correctly", + commandLineArgs: ["-b", "-w", "-verbose", "src"], + sys: () => ts.tscWatch.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 => ts.TestFSWithWatch.getTsBuildProjectFile("reexport", f)), + { path: ts.tscWatch.libFile.path, content: ts.libContent } + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport` }), + changes: [ + { + caption: "Introduce error", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport/src/pure/session.ts`, "// ", ""), + timeouts: build, + }, + { + caption: "Fix error", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.TestFSWithWatch.tsbuildProjectsLocation}/reexport/src/pure/session.ts`, "bar: ", "// bar: "), + timeouts: build + } + ] }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsbuildWatch/watchEnvironment.ts b/src/testRunner/unittests/tsbuildWatch/watchEnvironment.ts index 3b051054740ab..4f2fbb59058c0 100644 --- a/src/testRunner/unittests/tsbuildWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tsbuildWatch/watchEnvironment.ts @@ -1,116 +1,116 @@ namespace ts.tscWatch { - describe("unittests:: tsbuildWatch:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { - describe("when watchFile can create multiple watchers per file", () => { - verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false); - }); +describe("unittests:: tsbuildWatch:: 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, ts.arrayToMap(["TSC_WATCHFILE"], ts.identity, () => ts.TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)); - }); + describe("when watchFile is single watcher per file", () => { + verifyWatchFileOnMultipleProjects( + /*singleWatchPerFile*/ true, ts.arrayToMap(["TSC_WATCHFILE"], ts.identity, () => ts.TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)); + }); - function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: ts.ESMap) { - it("watchFile on same file multiple times because file is part of multiple projects", () => { - const project = `${ts.TestFSWithWatch.tsbuildProjectsLocation}/myproject`; - let maxPkgs = 4; - const configPath = `${project}/tsconfig.json`; - const typing: ts.tscWatch.File = { - path: `${project}/typings/xterm.d.ts`, - content: "export const typing = 10;" - }; + function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: ts.ESMap) { + it("watchFile on same file multiple times because file is part of multiple projects", () => { + const project = `${ts.TestFSWithWatch.tsbuildProjectsLocation}/myproject`; + let maxPkgs = 4; + const configPath = `${project}/tsconfig.json`; + const typing: ts.tscWatch.File = { + path: `${project}/typings/xterm.d.ts`, + content: "export const typing = 10;" + }; - const allPkgFiles = pkgs(pkgFiles); - const system = ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables }); - writePkgReferences(system); - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(system); - const host = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); - const solutionBuilder = ts.createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); - solutionBuilder.build(); - ts.tscWatch.runWatchBaseline({ - scenario: "watchEnvironment", - subScenario: `same file in multiple projects${singleWatchPerFile ? " with single watcher per file" : ""}`, - commandLineArgs: ["--b", "--w"], - sys, - baseline, - oldSnap, - getPrograms, - changes: [ - { - caption: "modify typing file", - change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`), - timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) + const allPkgFiles = pkgs(pkgFiles); + const system = ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables }); + writePkgReferences(system); + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(system); + const host = ts.tscWatch.createSolutionBuilderWithWatchHostForBaseline(sys, cb); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); + solutionBuilder.build(); + ts.tscWatch.runWatchBaseline({ + scenario: "watchEnvironment", + subScenario: `same file in multiple projects${singleWatchPerFile ? " with single watcher per file" : ""}`, + commandLineArgs: ["--b", "--w"], + sys, + baseline, + oldSnap, + getPrograms, + changes: [ + { + caption: "modify typing file", + change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`), + timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) + }, + { + // Make change + caption: "change pkg references", + change: sys => { + maxPkgs--; + writePkgReferences(sys); }, - { - // Make change - caption: "change pkg references", - change: sys => { - maxPkgs--; - writePkgReferences(sys); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "modify typing file", + change: sys => sys.writeFile(typing.path, typing.content), + timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) + }, + { + // Make change to remove all watches + caption: "change pkg references to remove all watches", + change: sys => { + maxPkgs = 0; + writePkgReferences(sys); }, - { - caption: "modify typing file", - change: sys => sys.writeFile(typing.path, typing.content), - timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) - }, - { - // Make change to remove all watches - caption: "change pkg references to remove all watches", - change: sys => { - maxPkgs = 0; - writePkgReferences(sys); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "modify typing file", - change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`), - timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) - }, - ], - watchOrSolution: solutionBuilder - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "modify typing file", + change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`), + timeouts: sys => pkgs(() => sys.checkTimeoutQueueLengthAndRun(1)) + }, + ], + watchOrSolution: solutionBuilder + }); - function flatArray(arr: T[][]): readonly T[] { - return ts.flatMap(arr, ts.identity); + function flatArray(arr: T[][]): readonly T[] { + return ts.flatMap(arr, ts.identity); + } + function pkgs(cb: (index: number) => T): T[] { + const result: T[] = []; + for (let index = 0; index < maxPkgs; index++) { + result.push(cb(index)); } - 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): ts.tscWatch.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" + ] + }) } - return result; - } - function createPkgReference(index: number) { - return { path: `./pkg${index}` }; - } - function pkgFiles(index: number): ts.tscWatch.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: ts.TestFSWithWatch.TestServerHost) { - system.writeFile(configPath, JSON.stringify({ - files: [], - include: [], - references: pkgs(createPkgReference) - })); - } - }); - } - }); + ]; + } + function writePkgReferences(system: ts.TestFSWithWatch.TestServerHost) { + system.writeFile(configPath, JSON.stringify({ + files: [], + include: [], + references: pkgs(createPkgReference) + })); + } + }); + } +}); } diff --git a/src/testRunner/unittests/tsc/composite.ts b/src/testRunner/unittests/tsc/composite.ts index 7b20b3764a67d..d1b304d0e2d25 100644 --- a/src/testRunner/unittests/tsc/composite.ts +++ b/src/testRunner/unittests/tsc/composite.ts @@ -1,11 +1,11 @@ namespace ts { - describe("unittests:: tsc:: composite::", () => { - ts.verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false on command line", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` +describe("unittests:: tsc:: composite::", () => { + ts.verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "es5", @@ -16,16 +16,16 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "false", "--p", "src/project"], - }); + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); - ts.verifyTsc({ - scenario: "composite", - subScenario: "when setting composite null on command line", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + ts.verifyTsc({ + scenario: "composite", + subScenario: "when setting composite null on command line", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "es5", @@ -36,16 +36,16 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "null", "--p", "src/project"], - }); + }), + commandLineArgs: ["--composite", "null", "--p", "src/project"], + }); - ts.verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false on command line but has tsbuild info in config", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + ts.verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line but has tsbuild info in config", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "es5", @@ -57,16 +57,16 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "false", "--p", "src/project"], - }); + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); - ts.verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false and tsbuildinfo as null on command line but has tsbuild info in config", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + ts.verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false and tsbuildinfo as null on command line but has tsbuild info in config", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "es5", @@ -78,8 +78,8 @@ 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 1b51326798146..fc5ae5db8153a 100644 --- a/src/testRunner/unittests/tsc/declarationEmit.ts +++ b/src/testRunner/unittests/tsc/declarationEmit.ts @@ -1,63 +1,63 @@ namespace ts { - describe("unittests:: tsc:: declarationEmit::", () => { - interface VerifyDeclarationEmitInput { - subScenario: string; - files: ts.TestFSWithWatch.FileOrFolderOrSymLink[]; - rootProject: string; - changeCaseFileTestPath: (path: string) => boolean; - } +describe("unittests:: tsc:: declarationEmit::", () => { + interface VerifyDeclarationEmitInput { + subScenario: string; + files: ts.TestFSWithWatch.FileOrFolderOrSymLink[]; + rootProject: string; + changeCaseFileTestPath: (path: string) => boolean; + } - function changeCaseFile(file: ts.TestFSWithWatch.FileOrFolderOrSymLink, testPath: (path: string) => boolean, replacePath: (path: string) => string): ts.TestFSWithWatch.FileOrFolderOrSymLink { - return !ts.TestFSWithWatch.isSymLink(file) || !testPath(file.symLink) ? - testPath(file.path) ? { ...file, path: replacePath(file.path) } : file : - { path: testPath(file.path) ? replacePath(file.path) : file.path, symLink: replacePath(file.symLink) }; - } + function changeCaseFile(file: ts.TestFSWithWatch.FileOrFolderOrSymLink, testPath: (path: string) => boolean, replacePath: (path: string) => string): ts.TestFSWithWatch.FileOrFolderOrSymLink { + return !ts.TestFSWithWatch.isSymLink(file) || !testPath(file.symLink) ? + testPath(file.path) ? { ...file, path: replacePath(file.path) } : file : + { path: testPath(file.path) ? replacePath(file.path) : file.path, symLink: replacePath(file.symLink) }; + } - function verifyDeclarationEmit({ subScenario, files, rootProject, changeCaseFileTestPath }: VerifyDeclarationEmitInput) { - describe(subScenario, () => { - ts.tscWatch.verifyTscWatch({ - scenario: "declarationEmit", - subScenario, - sys: () => ts.tscWatch.createWatchedSystem(files, { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["-p", rootProject, "--explainFiles"], - changes: ts.emptyArray - }); + function verifyDeclarationEmit({ subScenario, files, rootProject, changeCaseFileTestPath }: VerifyDeclarationEmitInput) { + describe(subScenario, () => { + ts.tscWatch.verifyTscWatch({ + scenario: "declarationEmit", + subScenario, + sys: () => ts.tscWatch.createWatchedSystem(files, { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["-p", rootProject, "--explainFiles"], + changes: ts.emptyArray }); + }); - const caseChangeScenario = `${subScenario} moduleCaseChange`; - describe(caseChangeScenario, () => { - ts.tscWatch.verifyTscWatch({ - scenario: "declarationEmit", - subScenario: caseChangeScenario, - sys: () => ts.tscWatch.createWatchedSystem(files.map(f => changeCaseFile(f, changeCaseFileTestPath, str => str.replace("myproject", "myProject"))), { currentDirectory: ts.tscWatch.projectRoot }), - commandLineArgs: ["-p", rootProject, "--explainFiles"], - changes: ts.emptyArray - }); + const caseChangeScenario = `${subScenario} moduleCaseChange`; + describe(caseChangeScenario, () => { + ts.tscWatch.verifyTscWatch({ + scenario: "declarationEmit", + subScenario: caseChangeScenario, + sys: () => ts.tscWatch.createWatchedSystem(files.map(f => changeCaseFile(f, changeCaseFileTestPath, str => str.replace("myproject", "myProject"))), { currentDirectory: ts.tscWatch.projectRoot }), + commandLineArgs: ["-p", rootProject, "--explainFiles"], + changes: ts.emptyArray }); - } + }); + } - describe("with symlinks in sibling folders and common package referenced from both folders", () => { - function pluginOneConfig() { - return JSON.stringify({ - compilerOptions: { - target: "es5", - declaration: true, - traceResolution: true - }, - }); - } - function pluginOneIndex() { - return `import pluginTwo from "plugin-two"; // include this to add reference to symlink`; - } - function pluginOneAction() { - return Utils.dedent` + describe("with symlinks in sibling folders and common package referenced from both folders", () => { + function pluginOneConfig() { + return JSON.stringify({ + compilerOptions: { + target: "es5", + declaration: true, + traceResolution: true + }, + }); + } + function pluginOneIndex() { + return `import pluginTwo from "plugin-two"; // include this to add reference to symlink`; + } + function pluginOneAction() { + return Utils.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 };`; - } - function pluginTwoDts() { - return Utils.dedent` + } + function pluginTwoDts() { + return Utils.dedent` declare const _default: { features: { featureOne: { @@ -79,15 +79,15 @@ namespace ts { }; }; export default _default;`; - } - function fsaPackageJson() { - return JSON.stringify({ - name: "typescript-fsa", - version: "3.0.0-beta-2" - }); - } - function fsaIndex() { - return Utils.dedent` + } + function fsaPackageJson() { + return JSON.stringify({ + name: "typescript-fsa", + version: "3.0.0-beta-2" + }); + } + function fsaIndex() { + return Utils.dedent` export interface Action { type: string; payload: Payload; @@ -101,69 +101,69 @@ namespace ts { } export declare function actionCreatorFactory(prefix?: string | null): ActionCreatorFactory; export default actionCreatorFactory;`; - } - - verifyDeclarationEmit({ - subScenario: "when same version is referenced through source and another symlinked package", - rootProject: "plugin-one", - files: [ - { path: `${ts.tscWatch.projectRoot}/plugin-two/index.d.ts`, content: pluginTwoDts() }, - { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, - { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/tsconfig.json`, content: pluginOneConfig() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/index.ts`, content: pluginOneIndex() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/action.ts`, content: pluginOneAction() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/plugin-two`, symLink: `${ts.tscWatch.projectRoot}/plugin-two` }, - ts.tscWatch.libFile - ], - changeCaseFileTestPath: str => ts.stringContains(str, "/plugin-two"), - }); + } - verifyDeclarationEmit({ - subScenario: "when same version is referenced through source and another symlinked package with indirect link", - rootProject: "plugin-one", - files: [ - { - path: `${ts.tscWatch.projectRoot}/plugin-two/package.json`, - content: JSON.stringify({ - name: "plugin-two", - version: "0.1.3", - main: "dist/commonjs/index.js" - }) - }, - { path: `${ts.tscWatch.projectRoot}/plugin-two/dist/commonjs/index.d.ts`, content: pluginTwoDts() }, - { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, - { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/tsconfig.json`, content: pluginOneConfig() }, - { - path: `${ts.tscWatch.projectRoot}/plugin-one/index.ts`, - content: `${pluginOneIndex()} -${pluginOneAction()}` - }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, - { path: `/temp/yarn/data/link/plugin-two`, symLink: `${ts.tscWatch.projectRoot}/plugin-two` }, - { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/plugin-two`, symLink: `/temp/yarn/data/link/plugin-two` }, - ts.tscWatch.libFile - ], - changeCaseFileTestPath: str => ts.stringContains(str, "/plugin-two"), - }); + verifyDeclarationEmit({ + subScenario: "when same version is referenced through source and another symlinked package", + rootProject: "plugin-one", + files: [ + { path: `${ts.tscWatch.projectRoot}/plugin-two/index.d.ts`, content: pluginTwoDts() }, + { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, + { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/tsconfig.json`, content: pluginOneConfig() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/index.ts`, content: pluginOneIndex() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/action.ts`, content: pluginOneAction() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/plugin-two`, symLink: `${ts.tscWatch.projectRoot}/plugin-two` }, + ts.tscWatch.libFile + ], + changeCaseFileTestPath: str => ts.stringContains(str, "/plugin-two"), }); verifyDeclarationEmit({ - subScenario: "when pkg references sibling package through indirect symlink", - rootProject: "pkg3", + subScenario: "when same version is referenced through source and another symlinked package with indirect link", + rootProject: "plugin-one", files: [ { - path: `${ts.tscWatch.projectRoot}/pkg1/dist/index.d.ts`, - content: Utils.dedent` - export * from './types';` + path: `${ts.tscWatch.projectRoot}/plugin-two/package.json`, + content: JSON.stringify({ + name: "plugin-two", + version: "0.1.3", + main: "dist/commonjs/index.js" + }) }, + { path: `${ts.tscWatch.projectRoot}/plugin-two/dist/commonjs/index.d.ts`, content: pluginTwoDts() }, + { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, + { path: `${ts.tscWatch.projectRoot}/plugin-two/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/tsconfig.json`, content: pluginOneConfig() }, { - path: `${ts.tscWatch.projectRoot}/pkg1/dist/types.d.ts`, - content: Utils.dedent` + path: `${ts.tscWatch.projectRoot}/plugin-one/index.ts`, + content: `${pluginOneIndex()} +${pluginOneAction()}` + }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/package.json`, content: fsaPackageJson() }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/typescript-fsa/index.d.ts`, content: fsaIndex() }, + { path: `/temp/yarn/data/link/plugin-two`, symLink: `${ts.tscWatch.projectRoot}/plugin-two` }, + { path: `${ts.tscWatch.projectRoot}/plugin-one/node_modules/plugin-two`, symLink: `/temp/yarn/data/link/plugin-two` }, + ts.tscWatch.libFile + ], + changeCaseFileTestPath: str => ts.stringContains(str, "/plugin-two"), + }); + }); + + verifyDeclarationEmit({ + subScenario: "when pkg references sibling package through indirect symlink", + rootProject: "pkg3", + files: [ + { + path: `${ts.tscWatch.projectRoot}/pkg1/dist/index.d.ts`, + content: Utils.dedent` + export * from './types';` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg1/dist/types.d.ts`, + content: Utils.dedent` export declare type A = { id: string; }; @@ -177,71 +177,71 @@ ${pluginOneAction()}` toString(): string; static create(key: string): MetadataAccessor; }` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg1/package.json`, - content: JSON.stringify({ - name: "@raymondfeng/pkg1", - version: "1.0.0", - main: "dist/index.js", - typings: "dist/index.d.ts" - }) - }, - { - path: `${ts.tscWatch.projectRoot}/pkg2/dist/index.d.ts`, - content: Utils.dedent` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg1/package.json`, + content: JSON.stringify({ + name: "@raymondfeng/pkg1", + version: "1.0.0", + main: "dist/index.js", + typings: "dist/index.d.ts" + }) + }, + { + path: `${ts.tscWatch.projectRoot}/pkg2/dist/index.d.ts`, + content: Utils.dedent` export * from './types';` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg2/dist/types.d.ts`, - content: Utils.dedent` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg2/dist/types.d.ts`, + content: Utils.dedent` export {MetadataAccessor} from '@raymondfeng/pkg1';` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg2/package.json`, - content: JSON.stringify({ - name: "@raymondfeng/pkg2", - version: "1.0.0", - main: "dist/index.js", - typings: "dist/index.d.ts" - }) - }, - { - path: `${ts.tscWatch.projectRoot}/pkg3/src/index.ts`, - content: Utils.dedent` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg2/package.json`, + content: JSON.stringify({ + name: "@raymondfeng/pkg2", + version: "1.0.0", + main: "dist/index.js", + typings: "dist/index.d.ts" + }) + }, + { + path: `${ts.tscWatch.projectRoot}/pkg3/src/index.ts`, + content: Utils.dedent` export * from './keys';` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg3/src/keys.ts`, - content: Utils.dedent` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg3/src/keys.ts`, + content: Utils.dedent` import {MetadataAccessor} from "@raymondfeng/pkg2"; export const ADMIN = MetadataAccessor.create('1');` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg3/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - outDir: "dist", - rootDir: "src", - target: "es5", - module: "commonjs", - strict: true, - esModuleInterop: true, - declaration: true - } - }) - }, - { - path: `${ts.tscWatch.projectRoot}/pkg2/node_modules/@raymondfeng/pkg1`, - symLink: `${ts.tscWatch.projectRoot}/pkg1` - }, - { - path: `${ts.tscWatch.projectRoot}/pkg3/node_modules/@raymondfeng/pkg2`, - symLink: `${ts.tscWatch.projectRoot}/pkg2` - }, - ts.tscWatch.libFile - ], - changeCaseFileTestPath: str => ts.stringContains(str, "/pkg1"), - }); + }, + { + path: `${ts.tscWatch.projectRoot}/pkg3/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "dist", + rootDir: "src", + target: "es5", + module: "commonjs", + strict: true, + esModuleInterop: true, + declaration: true + } + }) + }, + { + path: `${ts.tscWatch.projectRoot}/pkg2/node_modules/@raymondfeng/pkg1`, + symLink: `${ts.tscWatch.projectRoot}/pkg1` + }, + { + path: `${ts.tscWatch.projectRoot}/pkg3/node_modules/@raymondfeng/pkg2`, + symLink: `${ts.tscWatch.projectRoot}/pkg2` + }, + ts.tscWatch.libFile + ], + changeCaseFileTestPath: str => ts.stringContains(str, "/pkg1"), }); +}); } diff --git a/src/testRunner/unittests/tsc/helpers.ts b/src/testRunner/unittests/tsc/helpers.ts index 972e232747ea6..9805b5263eb47 100644 --- a/src/testRunner/unittests/tsc/helpers.ts +++ b/src/testRunner/unittests/tsc/helpers.ts @@ -1,237 +1,237 @@ namespace ts { - export type TscCompileSystem = fakes.System & { - writtenFiles: ts.Set; - baseLine(): { - file: string; - text: string; - }; - disableUseFileVersionAsSignature?: boolean; - storeFilesChangingSignatureDuringEmit?: boolean; - }; - - export const noChangeRun: ts.TestTscEdit = { - subScenario: "no-change-run", - modifyFs: ts.noop - }; - export const noChangeWithExportsDiscrepancyRun: ts.TestTscEdit = { - ...noChangeRun, - discrepancyExplanation: () => [ - "Incremental build did not emit and has .ts as signature so exports has all imported modules/referenced files", - "Clean build always uses d.ts for signature for testing thus does not contain non exported modules/referenced files that arent needed" - ] +export type TscCompileSystem = fakes.System & { + writtenFiles: ts.Set; + baseLine(): { + file: string; + text: string; }; - export const noChangeOnlyRuns = [noChangeRun]; - export const noChangeWithExportsDiscrepancyOnlyRuns = [noChangeWithExportsDiscrepancyRun]; - - export interface TestTscCompile extends TestTscCompileLikeBase { - baselineSourceMap?: boolean; - baselineReadFileCalls?: boolean; - baselinePrograms?: boolean; - baselineDependencies?: boolean; - } - - export type CommandLineProgram = [ - ts.Program, - ts.BuilderProgram? - ]; - export interface CommandLineCallbacks { - cb: ts.ExecuteCommandLineCallbacks; - getPrograms: () => readonly CommandLineProgram[]; - } + disableUseFileVersionAsSignature?: boolean; + storeFilesChangingSignatureDuringEmit?: boolean; +}; + +export const noChangeRun: ts.TestTscEdit = { + subScenario: "no-change-run", + modifyFs: ts.noop +}; +export const noChangeWithExportsDiscrepancyRun: ts.TestTscEdit = { + ...noChangeRun, + discrepancyExplanation: () => [ + "Incremental build did not emit and has .ts as signature so exports has all imported modules/referenced files", + "Clean build always uses d.ts for signature for testing thus does not contain non exported modules/referenced files that arent needed" + ] +}; +export const noChangeOnlyRuns = [noChangeRun]; +export const noChangeWithExportsDiscrepancyOnlyRuns = [noChangeWithExportsDiscrepancyRun]; + +export interface TestTscCompile extends TestTscCompileLikeBase { + baselineSourceMap?: boolean; + baselineReadFileCalls?: boolean; + baselinePrograms?: boolean; + baselineDependencies?: boolean; +} - function isAnyProgram(program: ts.Program | ts.BuilderProgram | ts.ParsedCommandLine): program is ts.Program | ts.BuilderProgram { - return !!(program as ts.Program | ts.BuilderProgram).getCompilerOptions; - } - export function commandLineCallbacks(sys: TscCompileSystem | ts.tscWatch.WatchedSystem, originalReadCall?: ts.System["readFile"]): CommandLineCallbacks { - let programs: CommandLineProgram[] | undefined; +export type CommandLineProgram = [ + ts.Program, + ts.BuilderProgram? +]; +export interface CommandLineCallbacks { + cb: ts.ExecuteCommandLineCallbacks; + getPrograms: () => readonly CommandLineProgram[]; +} - return { - cb: program => { - if (isAnyProgram(program)) { - ts.baselineBuildInfo(program.getCompilerOptions(), sys, originalReadCall); - (programs || (programs = [])).push(ts.isBuilderProgram(program) ? - [program.getProgram(), program] : - [program]); - } - else { - ts.baselineBuildInfo(program.options, sys, originalReadCall); - } - }, - getPrograms: () => { - const result = programs || ts.emptyArray; - programs = undefined; - return result; +function isAnyProgram(program: ts.Program | ts.BuilderProgram | ts.ParsedCommandLine): program is ts.Program | ts.BuilderProgram { + return !!(program as ts.Program | ts.BuilderProgram).getCompilerOptions; +} +export function commandLineCallbacks(sys: TscCompileSystem | ts.tscWatch.WatchedSystem, originalReadCall?: ts.System["readFile"]): CommandLineCallbacks { + let programs: CommandLineProgram[] | undefined; + + return { + cb: program => { + if (isAnyProgram(program)) { + ts.baselineBuildInfo(program.getCompilerOptions(), sys, originalReadCall); + (programs || (programs = [])).push(ts.isBuilderProgram(program) ? + [program.getProgram(), program] : + [program]); } - }; - } - export interface TestTscCompileLikeBase extends VerifyTscCompileLike { - diffWithInitial?: boolean; - modifyFs?: (fs: vfs.FileSystem) => void; - disableUseFileVersionAsSignature?: boolean; - environmentVariables?: Record; - } + else { + ts.baselineBuildInfo(program.options, sys, originalReadCall); + } + }, + getPrograms: () => { + const result = programs || ts.emptyArray; + programs = undefined; + return result; + } + }; +} +export interface TestTscCompileLikeBase extends VerifyTscCompileLike { + diffWithInitial?: boolean; + modifyFs?: (fs: vfs.FileSystem) => void; + disableUseFileVersionAsSignature?: boolean; + environmentVariables?: Record; +} - export interface TestTscCompileLike extends TestTscCompileLikeBase { - compile: (sys: TscCompileSystem) => void; - additionalBaseline?: (sys: TscCompileSystem) => void; - } - /** - * Initialize FS, run compile function and save baseline - */ - export function testTscCompileLike(input: TestTscCompileLike) { - const initialFs = input.fs(); - const inputFs = initialFs.shadow(); - const { scenario, subScenario, diffWithInitial, commandLineArgs, modifyFs, environmentVariables, compile: worker, additionalBaseline, } = input; - if (modifyFs) - modifyFs(inputFs); - inputFs.makeReadonly(); - const fs = inputFs.shadow(); - - // Create system - const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc", env: environmentVariables }) as TscCompileSystem; - if (input.disableUseFileVersionAsSignature) - sys.disableUseFileVersionAsSignature = true; - sys.storeFilesChangingSignatureDuringEmit = true; - sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); - sys.exit = exitCode => sys.exitCode = exitCode; - worker(sys); - sys.write(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}\n`); - additionalBaseline?.(sys); - fs.makeReadonly(); - sys.baseLine = () => { - const baseFsPatch = diffWithInitial ? - inputFs.diff(initialFs, { includeChangedFileWithSameContent: true }) : - inputFs.diff(/*base*/ undefined, { baseIsNotShadowRoot: true }); - const patch = fs.diff(inputFs, { includeChangedFileWithSameContent: true }); - return { - file: `${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}.js`, - text: `Input:: +export interface TestTscCompileLike extends TestTscCompileLikeBase { + compile: (sys: TscCompileSystem) => void; + additionalBaseline?: (sys: TscCompileSystem) => void; +} +/** + * Initialize FS, run compile function and save baseline + */ +export function testTscCompileLike(input: TestTscCompileLike) { + const initialFs = input.fs(); + const inputFs = initialFs.shadow(); + const { scenario, subScenario, diffWithInitial, commandLineArgs, modifyFs, environmentVariables, compile: worker, additionalBaseline, } = input; + if (modifyFs) + modifyFs(inputFs); + inputFs.makeReadonly(); + const fs = inputFs.shadow(); + + // Create system + const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc", env: environmentVariables }) as TscCompileSystem; + if (input.disableUseFileVersionAsSignature) + sys.disableUseFileVersionAsSignature = true; + sys.storeFilesChangingSignatureDuringEmit = true; + sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); + sys.exit = exitCode => sys.exitCode = exitCode; + worker(sys); + sys.write(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}\n`); + additionalBaseline?.(sys); + fs.makeReadonly(); + sys.baseLine = () => { + const baseFsPatch = diffWithInitial ? + inputFs.diff(initialFs, { includeChangedFileWithSameContent: true }) : + inputFs.diff(/*base*/ undefined, { baseIsNotShadowRoot: true }); + const patch = fs.diff(inputFs, { includeChangedFileWithSameContent: true }); + return { + file: `${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}.js`, + text: `Input:: ${baseFsPatch ? vfs.formatPatch(baseFsPatch) : ""} Output:: ${sys.output.join("")} ${patch ? vfs.formatPatch(patch) : ""}` - }; }; - return sys; + }; + return sys; +} + +function makeSystemReadyForBaseline(sys: TscCompileSystem, versionToWrite?: string) { + if (versionToWrite) { + fakes.patchHostForBuildInfoWrite(sys, versionToWrite); + } + else { + fakes.patchHostForBuildInfoReadWrite(sys); } + const writtenFiles = sys.writtenFiles = new ts.Set(); + const originalWriteFile = sys.writeFile; + sys.writeFile = (fileName, content, writeByteOrderMark) => { + const path = ts.toPathWithSystem(sys, fileName); + // When buildinfo is same for two projects, + // it gives error and doesnt write buildinfo but because buildInfo is written for one project, + // readable baseline will be written two times for those two projects with same contents and is ok + ts.Debug.assert(!writtenFiles.has(path) || ts.endsWith(path, "baseline.txt")); + writtenFiles.add(path); + return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); + }; +} - function makeSystemReadyForBaseline(sys: TscCompileSystem, versionToWrite?: string) { - if (versionToWrite) { - fakes.patchHostForBuildInfoWrite(sys, versionToWrite); - } - else { - fakes.patchHostForBuildInfoReadWrite(sys); - } - const writtenFiles = sys.writtenFiles = new ts.Set(); - const originalWriteFile = sys.writeFile; - sys.writeFile = (fileName, content, writeByteOrderMark) => { - const path = ts.toPathWithSystem(sys, fileName); - // When buildinfo is same for two projects, - // it gives error and doesnt write buildinfo but because buildInfo is written for one project, - // readable baseline will be written two times for those two projects with same contents and is ok - ts.Debug.assert(!writtenFiles.has(path) || ts.endsWith(path, "baseline.txt")); - writtenFiles.add(path); - return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); +export function createSolutionBuilderHostForBaseline(sys: TscCompileSystem | ts.tscWatch.WatchedSystem, versionToWrite?: string, originalRead?: (TscCompileSystem | ts.tscWatch.WatchedSystem)["readFile"]) { + if (sys instanceof fakes.System) + makeSystemReadyForBaseline(sys, versionToWrite); + const { cb } = commandLineCallbacks(sys, originalRead); + const host = ts.createSolutionBuilderHost(sys, + /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true)); + host.afterProgramEmitAndDiagnostics = cb; + host.afterEmitBundle = cb; + return host; +} + +/** + * Initialize Fs, execute command line and save baseline + */ +export function testTscCompile(input: TestTscCompile) { + let actualReadFileMap: ts.MapLike | undefined; + let getPrograms: CommandLineCallbacks["getPrograms"] | undefined; + return testTscCompileLike({ + ...input, + compile: commandLineCompile, + additionalBaseline + }); + + function commandLineCompile(sys: TscCompileSystem) { + makeSystemReadyForBaseline(sys); + actualReadFileMap = {}; + const originalReadFile = sys.readFile; + sys.readFile = path => { + // Dont record libs + if (path.startsWith("/src/")) { + actualReadFileMap![path] = (ts.getProperty(actualReadFileMap!, path) || 0) + 1; + } + return originalReadFile.call(sys, path); }; - } - export function createSolutionBuilderHostForBaseline(sys: TscCompileSystem | ts.tscWatch.WatchedSystem, versionToWrite?: string, originalRead?: (TscCompileSystem | ts.tscWatch.WatchedSystem)["readFile"]) { - if (sys instanceof fakes.System) - makeSystemReadyForBaseline(sys, versionToWrite); - const { cb } = commandLineCallbacks(sys, originalRead); - const host = ts.createSolutionBuilderHost(sys, - /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true)); - host.afterProgramEmitAndDiagnostics = cb; - host.afterEmitBundle = cb; - return host; + const result = commandLineCallbacks(sys, originalReadFile); + ts.executeCommandLine(sys, result.cb, input.commandLineArgs); + sys.readFile = originalReadFile; + getPrograms = result.getPrograms; } - /** - * Initialize Fs, execute command line and save baseline - */ - export function testTscCompile(input: TestTscCompile) { - let actualReadFileMap: ts.MapLike | undefined; - let getPrograms: CommandLineCallbacks["getPrograms"] | undefined; - return testTscCompileLike({ - ...input, - compile: commandLineCompile, - additionalBaseline - }); - - function commandLineCompile(sys: TscCompileSystem) { - makeSystemReadyForBaseline(sys); - actualReadFileMap = {}; - const originalReadFile = sys.readFile; - sys.readFile = path => { - // Dont record libs - if (path.startsWith("/src/")) { - actualReadFileMap![path] = (ts.getProperty(actualReadFileMap!, path) || 0) + 1; - } - return originalReadFile.call(sys, path); - }; - - const result = commandLineCallbacks(sys, originalReadFile); - ts.executeCommandLine(sys, result.cb, input.commandLineArgs); - sys.readFile = originalReadFile; - getPrograms = result.getPrograms; + function additionalBaseline(sys: TscCompileSystem) { + const { baselineSourceMap, baselineReadFileCalls, baselinePrograms, baselineDependencies } = input; + if (baselinePrograms) { + const baseline: string[] = []; + ts.tscWatch.baselinePrograms(baseline, getPrograms!, ts.emptyArray, baselineDependencies); + sys.write(baseline.join("\n")); } - - function additionalBaseline(sys: TscCompileSystem) { - const { baselineSourceMap, baselineReadFileCalls, baselinePrograms, baselineDependencies } = input; - if (baselinePrograms) { - const baseline: string[] = []; - ts.tscWatch.baselinePrograms(baseline, getPrograms!, ts.emptyArray, baselineDependencies); - sys.write(baseline.join("\n")); - } - if (baselineReadFileCalls) { - sys.write(`readFiles:: ${JSON.stringify(actualReadFileMap, /*replacer*/ undefined, " ")} `); - } - if (baselineSourceMap) - ts.generateSourceMapBaselineFiles(sys); - actualReadFileMap = undefined; - getPrograms = undefined; + if (baselineReadFileCalls) { + sys.write(`readFiles:: ${JSON.stringify(actualReadFileMap, /*replacer*/ undefined, " ")} `); } + if (baselineSourceMap) + ts.generateSourceMapBaselineFiles(sys); + actualReadFileMap = undefined; + getPrograms = undefined; } +} - export function verifyTscBaseline(sys: () => { - baseLine: TscCompileSystem["baseLine"]; - }) { - it(`Generates files matching the baseline`, () => { - const { file, text } = sys().baseLine(); - Harness.Baseline.runBaseline(file, text); - }); - } - export interface VerifyTscCompileLike { - scenario: string; - subScenario: string; - commandLineArgs: readonly string[]; - fs: () => vfs.FileSystem; - } +export function verifyTscBaseline(sys: () => { + baseLine: TscCompileSystem["baseLine"]; +}) { + it(`Generates files matching the baseline`, () => { + const { file, text } = sys().baseLine(); + Harness.Baseline.runBaseline(file, text); + }); +} +export interface VerifyTscCompileLike { + scenario: string; + subScenario: string; + commandLineArgs: readonly string[]; + fs: () => vfs.FileSystem; +} - /** - * Verify by baselining after initializing FS and custom compile - */ - export function verifyTscCompileLike(verifier: (input: T) => { - baseLine: TscCompileSystem["baseLine"]; - }, input: T) { - describe(`tsc ${input.commandLineArgs.join(" ")} ${input.scenario}:: ${input.subScenario}`, () => { - describe(input.scenario, () => { - describe(input.subScenario, () => { - verifyTscBaseline(() => verifier({ - ...input, - fs: () => input.fs().makeReadonly() - })); - }); +/** + * Verify by baselining after initializing FS and custom compile + */ +export function verifyTscCompileLike(verifier: (input: T) => { + baseLine: TscCompileSystem["baseLine"]; +}, input: T) { + describe(`tsc ${input.commandLineArgs.join(" ")} ${input.scenario}:: ${input.subScenario}`, () => { + describe(input.scenario, () => { + describe(input.subScenario, () => { + verifyTscBaseline(() => verifier({ + ...input, + fs: () => input.fs().makeReadonly() + })); }); }); - } + }); +} - /** - * Verify by baselining after initializing FS and command line compile - */ - export function verifyTsc(input: TestTscCompile) { - verifyTscCompileLike(testTscCompile, input); - } +/** + * Verify by baselining after initializing FS and command line compile + */ + export function verifyTsc(input: TestTscCompile) { + verifyTscCompileLike(testTscCompile, input); +} } diff --git a/src/testRunner/unittests/tsc/incremental.ts b/src/testRunner/unittests/tsc/incremental.ts index a9bd18f6cab66..098ddf4274ad0 100644 --- a/src/testRunner/unittests/tsc/incremental.ts +++ b/src/testRunner/unittests/tsc/incremental.ts @@ -1,11 +1,11 @@ namespace ts { - describe("unittests:: tsc:: incremental::", () => { - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "when passing filename for buildinfo on commandline", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` +describe("unittests:: tsc:: incremental::", () => { + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "when passing filename for buildinfo on commandline", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "es5", @@ -15,52 +15,52 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--incremental", "--p", "src/project", "--tsBuildInfoFile", "src/project/.tsbuildinfo", "--explainFiles"], - edits: ts.noChangeOnlyRuns - }); + }), + commandLineArgs: ["--incremental", "--p", "src/project", "--tsBuildInfoFile", "src/project/.tsbuildinfo", "--explainFiles"], + edits: ts.noChangeOnlyRuns + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "when passing rootDir from commandline", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "when passing rootDir from commandline", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "incremental": true, "outDir": "dist", }, }`, - }), - commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], - edits: ts.noChangeOnlyRuns - }); + }), + commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], + edits: ts.noChangeOnlyRuns + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "with only dts files", - fs: () => ts.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"], - edits: [ - ts.noChangeRun, - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fs => ts.appendText(fs, "/src/project/src/main.d.ts", "export const xy = 100;") - } - ] - }); + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "with only dts files", + fs: () => ts.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"], + edits: [ + ts.noChangeRun, + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fs => ts.appendText(fs, "/src/project/src/main.d.ts", "export const xy = 100;") + } + ] + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "when passing rootDir is in the tsconfig", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "when passing rootDir is in the tsconfig", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "incremental": true, @@ -68,225 +68,225 @@ namespace ts { "rootDir": "./" }, }`, - }), - commandLineArgs: ["--p", "src/project"], - edits: ts.noChangeOnlyRuns + }), + commandLineArgs: ["--p", "src/project"], + edits: ts.noChangeOnlyRuns + }); + + describe("with noEmitOnError", () => { + let projFs: vfs.FileSystem; + before(() => { + projFs = ts.loadProjectFromDisk("tests/projects/noEmitOnError"); + }); + after(() => { + projFs = undefined!; }); - describe("with noEmitOnError", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = ts.loadProjectFromDisk("tests/projects/noEmitOnError"); - }); - after(() => { - projFs = undefined!; + function verifyNoEmitOnError(subScenario: string, fixModifyFs: ts.TestTscEdit["modifyFs"], modifyFs?: ts.TestTscEdit["modifyFs"]) { + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario, + fs: () => projFs, + commandLineArgs: ["--incremental", "-p", "src"], + modifyFs, + edits: [ + ts.noChangeWithExportsDiscrepancyRun, + { + subScenario: "incremental-declaration-doesnt-change", + modifyFs: fixModifyFs + }, + ts.noChangeRun, + ], + baselinePrograms: true }); - - function verifyNoEmitOnError(subScenario: string, fixModifyFs: ts.TestTscEdit["modifyFs"], modifyFs?: ts.TestTscEdit["modifyFs"]) { - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario, - fs: () => projFs, - commandLineArgs: ["--incremental", "-p", "src"], - modifyFs, - edits: [ - ts.noChangeWithExportsDiscrepancyRun, - { - subScenario: "incremental-declaration-doesnt-change", - modifyFs: fixModifyFs - }, - ts.noChangeRun, - ], - baselinePrograms: true - }); - } - verifyNoEmitOnError("with noEmitOnError syntax errors", fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + } + verifyNoEmitOnError("with noEmitOnError syntax errors", fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`, "utf-8")); - verifyNoEmitOnError("with noEmitOnError semantic errors", fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + verifyNoEmitOnError("with noEmitOnError semantic errors", fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = "hello";`, "utf-8"), fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a: string = 10;`, "utf-8")); - }); + }); - describe("when noEmit changes between compilation", () => { - verifyNoEmitChanges({ incremental: true }); - verifyNoEmitChanges({ incremental: true, declaration: true }); - verifyNoEmitChanges({ composite: true }); + describe("when noEmit changes between compilation", () => { + verifyNoEmitChanges({ incremental: true }); + verifyNoEmitChanges({ incremental: true, declaration: true }); + verifyNoEmitChanges({ composite: true }); - function verifyNoEmitChanges(compilerOptions: ts.CompilerOptions) { - const discrepancyIfNoDtsEmit = ts.getEmitDeclarations(compilerOptions) ? - undefined : - ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation; - const noChangeRunWithNoEmit: ts.TestTscEdit = { - ...ts.noChangeRun, - subScenario: "No Change run with noEmit", - commandLineArgs: ["--p", "src/project", "--noEmit"], - discrepancyExplanation: discrepancyIfNoDtsEmit, - }; - const noChangeRunWithEmit: ts.TestTscEdit = { - ...ts.noChangeRun, - subScenario: "No Change run with emit", - commandLineArgs: ["--p", "src/project"], - discrepancyExplanation: discrepancyIfNoDtsEmit, - }; - let optionsString = ""; - for (const key in compilerOptions) { - if (ts.hasProperty(compilerOptions, key)) { - optionsString += ` ${key}`; - } + function verifyNoEmitChanges(compilerOptions: ts.CompilerOptions) { + const discrepancyIfNoDtsEmit = ts.getEmitDeclarations(compilerOptions) ? + undefined : + ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation; + const noChangeRunWithNoEmit: ts.TestTscEdit = { + ...ts.noChangeRun, + subScenario: "No Change run with noEmit", + commandLineArgs: ["--p", "src/project", "--noEmit"], + discrepancyExplanation: discrepancyIfNoDtsEmit, + }; + const noChangeRunWithEmit: ts.TestTscEdit = { + ...ts.noChangeRun, + subScenario: "No Change run with emit", + commandLineArgs: ["--p", "src/project"], + discrepancyExplanation: discrepancyIfNoDtsEmit, + }; + let optionsString = ""; + for (const key in compilerOptions) { + if (ts.hasProperty(compilerOptions, key)) { + optionsString += ` ${key}`; } + } - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: `noEmit changes${optionsString}`, - commandLineArgs: ["--p", "src/project"], - fs, - edits: [ - noChangeRunWithNoEmit, - noChangeRunWithNoEmit, - { - subScenario: "Introduce error but still noEmit", - commandLineArgs: ["--p", "src/project", "--noEmit"], - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), - discrepancyExplanation: ts.getEmitDeclarations(compilerOptions) ? ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation : undefined, - }, - { - subScenario: "Fix error and emit", - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), - discrepancyExplanation: discrepancyIfNoDtsEmit - }, - noChangeRunWithEmit, - noChangeRunWithNoEmit, - noChangeRunWithNoEmit, - noChangeRunWithEmit, - { - subScenario: "Introduce error and emit", - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), - discrepancyExplanation: discrepancyIfNoDtsEmit - }, - noChangeRunWithEmit, - noChangeRunWithNoEmit, - noChangeRunWithNoEmit, - noChangeRunWithEmit, - { - subScenario: "Fix error and no emit", - commandLineArgs: ["--p", "src/project", "--noEmit"], - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), - discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation, - }, - noChangeRunWithEmit, - noChangeRunWithNoEmit, - noChangeRunWithNoEmit, - noChangeRunWithEmit, - ], - }); + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: `noEmit changes${optionsString}`, + commandLineArgs: ["--p", "src/project"], + fs, + edits: [ + noChangeRunWithNoEmit, + noChangeRunWithNoEmit, + { + subScenario: "Introduce error but still noEmit", + commandLineArgs: ["--p", "src/project", "--noEmit"], + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), + discrepancyExplanation: ts.getEmitDeclarations(compilerOptions) ? ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation : undefined, + }, + { + subScenario: "Fix error and emit", + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), + discrepancyExplanation: discrepancyIfNoDtsEmit + }, + noChangeRunWithEmit, + noChangeRunWithNoEmit, + noChangeRunWithNoEmit, + noChangeRunWithEmit, + { + subScenario: "Introduce error and emit", + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), + discrepancyExplanation: discrepancyIfNoDtsEmit + }, + noChangeRunWithEmit, + noChangeRunWithNoEmit, + noChangeRunWithNoEmit, + noChangeRunWithEmit, + { + subScenario: "Fix error and no emit", + commandLineArgs: ["--p", "src/project", "--noEmit"], + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), + discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation, + }, + noChangeRunWithEmit, + noChangeRunWithNoEmit, + noChangeRunWithNoEmit, + noChangeRunWithEmit, + ], + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: `noEmit changes with initial noEmit${optionsString}`, - commandLineArgs: ["--p", "src/project", "--noEmit"], - fs, - edits: [ - noChangeRunWithEmit, - { - subScenario: "Introduce error with emit", - commandLineArgs: ["--p", "src/project"], - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), - }, - { - subScenario: "Fix error and no emit", - modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), - discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation - }, - noChangeRunWithEmit, - ], - }); + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: `noEmit changes with initial noEmit${optionsString}`, + commandLineArgs: ["--p", "src/project", "--noEmit"], + fs, + edits: [ + noChangeRunWithEmit, + { + subScenario: "Introduce error with emit", + commandLineArgs: ["--p", "src/project"], + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), + }, + { + subScenario: "Fix error and no emit", + modifyFs: fs => ts.replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), + discrepancyExplanation: ts.noChangeWithExportsDiscrepancyRun.discrepancyExplanation + }, + noChangeRunWithEmit, + ], + }); - function fs() { - return ts.loadProjectFromFiles({ - "/src/project/src/class.ts": Utils.dedent` + function fs() { + return ts.loadProjectFromFiles({ + "/src/project/src/class.ts": Utils.dedent` export class classC { prop = 1; }`, - "/src/project/src/indirectClass.ts": Utils.dedent` + "/src/project/src/indirectClass.ts": Utils.dedent` import { classC } from './class'; export class indirectClass { classC = new classC(); }`, - "/src/project/src/directUse.ts": Utils.dedent` + "/src/project/src/directUse.ts": Utils.dedent` import { indirectClass } from './indirectClass'; new indirectClass().classC.prop;`, - "/src/project/src/indirectUse.ts": Utils.dedent` + "/src/project/src/indirectUse.ts": Utils.dedent` import { indirectClass } from './indirectClass'; new indirectClass().classC.prop;`, - "/src/project/src/noChangeFile.ts": Utils.dedent` + "/src/project/src/noChangeFile.ts": Utils.dedent` export function writeLog(s: string) { }`, - "/src/project/src/noChangeFileWithEmitSpecificError.ts": Utils.dedent` + "/src/project/src/noChangeFileWithEmitSpecificError.ts": Utils.dedent` function someFunc(arguments: boolean, ...rest: any[]) { }`, - "/src/project/tsconfig.json": JSON.stringify({ compilerOptions }), - }); - } + "/src/project/tsconfig.json": JSON.stringify({ compilerOptions }), + }); } - }); + } + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: `when global file is added, the signatures are updated`, - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": Utils.dedent` + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: `when global file is added, the signatures are updated`, + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": Utils.dedent` /// /// function main() { } `, - "/src/project/src/anotherFileWithSameReferenes.ts": Utils.dedent` + "/src/project/src/anotherFileWithSameReferenes.ts": Utils.dedent` /// /// function anotherFileWithSameReferenes() { } `, - "/src/project/src/filePresent.ts": `function something() { return 10; }`, - "/src/project/tsconfig.json": JSON.stringify({ - compilerOptions: { composite: true, }, - include: ["src/**/*.ts"] - }), + "/src/project/src/filePresent.ts": `function something() { return 10; }`, + "/src/project/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, }, + include: ["src/**/*.ts"] }), - commandLineArgs: ["--p", "src/project"], - edits: [ - ts.noChangeRun, - { - subScenario: "Modify main file", - modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), - }, - { - subScenario: "Modify main file again", - modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), - }, - { - subScenario: "Add new file and update main file", - modifyFs: fs => { - fs.writeFileSync(`/src/project/src/newFile.ts`, "function foo() { return 20; }"); - ts.prependText(fs, `/src/project/src/main.ts`, `/// + }), + commandLineArgs: ["--p", "src/project"], + edits: [ + ts.noChangeRun, + { + subScenario: "Modify main file", + modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), + }, + { + subScenario: "Modify main file again", + modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), + }, + { + subScenario: "Add new file and update main file", + modifyFs: fs => { + fs.writeFileSync(`/src/project/src/newFile.ts`, "function foo() { return 20; }"); + ts.prependText(fs, `/src/project/src/main.ts`, `/// `); - ts.appendText(fs, `/src/project/src/main.ts`, `foo();`); - }, - }, - { - subScenario: "Write file that could not be resolved", - modifyFs: fs => fs.writeFileSync(`/src/project/src/fileNotFound.ts`, "function something2() { return 20; }"), + ts.appendText(fs, `/src/project/src/main.ts`, `foo();`); }, - { - subScenario: "Modify main file", - modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), - }, - ], - baselinePrograms: true, - }); + }, + { + subScenario: "Write file that could not be resolved", + modifyFs: fs => fs.writeFileSync(`/src/project/src/fileNotFound.ts`, "function something2() { return 20; }"), + }, + { + subScenario: "Modify main file", + modifyFs: fs => ts.appendText(fs, `/src/project/src/main.ts`, `something();`), + }, + ], + baselinePrograms: true, + }); - describe("when synthesized imports are added to files", () => { - function getJsxLibraryContent() { - return ` + describe("when synthesized imports are added to files", () => { + function getJsxLibraryContent() { + return ` export {}; declare global { namespace JSX { @@ -298,125 +298,125 @@ declare global { } } }`; - } - - ts.verifyTsc({ - scenario: "react-jsx-emit-mode", - subScenario: "with no backing types found doesn't crash", - fs: () => ts.loadProjectFromFiles({ - "/src/project/node_modules/react/jsx-runtime.js": "export {}", - "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), - "/src/project/src/index.tsx": `export const App = () =>
;`, - "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) - }), - commandLineArgs: ["--p", "src/project"] - }); + } - ts.verifyTsc({ - scenario: "react-jsx-emit-mode", - subScenario: "with no backing types found doesn't crash under --strict", - fs: () => ts.loadProjectFromFiles({ - "/src/project/node_modules/react/jsx-runtime.js": "export {}", - "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), - "/src/project/src/index.tsx": `export const App = () =>
;`, - "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) - }), - commandLineArgs: ["--p", "src/project", "--strict"] - }); + ts.verifyTsc({ + scenario: "react-jsx-emit-mode", + subScenario: "with no backing types found doesn't crash", + fs: () => ts.loadProjectFromFiles({ + "/src/project/node_modules/react/jsx-runtime.js": "export {}", + "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), + "/src/project/src/index.tsx": `export const App = () =>
;`, + "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) + }), + commandLineArgs: ["--p", "src/project"] }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "when new file is added to the referenced project", - commandLineArgs: ["-i", "-p", `src/projects/project2`], + ts.verifyTsc({ + scenario: "react-jsx-emit-mode", + subScenario: "with no backing types found doesn't crash under --strict", fs: () => ts.loadProjectFromFiles({ - "/src/projects/project1/tsconfig.json": JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - exclude: ["temp"] - }), - "/src/projects/project1/class1.ts": `class class1 {}`, - "/src/projects/project1/class1.d.ts": `declare class class1 {}`, - "/src/projects/project2/tsconfig.json": JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - references: [ - { path: "../project1" } - ] - }), - "/src/projects/project2/class2.ts": `class class2 {}`, + "/src/project/node_modules/react/jsx-runtime.js": "export {}", + "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), + "/src/project/src/index.tsx": `export const App = () =>
;`, + "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) }), - edits: [ - { - subScenario: "Add class3 to project1 and build it", - modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.ts", `class class3 {}`, "utf-8"), - discrepancyExplanation: () => [ - "Ts buildinfo will not be updated in incremental build so it will have semantic diagnostics cached from previous build", - "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", - ], - }, - { - subScenario: "Add output of class3", - modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), - }, - { - subScenario: "Add excluded file to project1", - modifyFs: fs => { - fs.mkdirSync("/src/projects/project1/temp"); - fs.writeFileSync("/src/projects/project1/temp/file.d.ts", `declare class file {}`, "utf-8"); - }, + commandLineArgs: ["--p", "src/project", "--strict"] + }); + }); + + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "when new file is added to the referenced project", + commandLineArgs: ["-i", "-p", `src/projects/project2`], + fs: () => ts.loadProjectFromFiles({ + "/src/projects/project1/tsconfig.json": JSON.stringify({ + compilerOptions: { + module: "none", + composite: true }, - { - subScenario: "Delete output for class3", - modifyFs: fs => fs.unlinkSync("/src/projects/project1/class3.d.ts"), - discrepancyExplanation: () => [ - "Ts buildinfo will be updated but will retain lib file errors from previous build and not others because they are emitted because of change which results in clearing their semantic diagnostics cache", - "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", - ], + exclude: ["temp"] + }), + "/src/projects/project1/class1.ts": `class class1 {}`, + "/src/projects/project1/class1.d.ts": `declare class class1 {}`, + "/src/projects/project2/tsconfig.json": JSON.stringify({ + compilerOptions: { + module: "none", + composite: true }, - { - subScenario: "Create output for class3", - modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), + references: [ + { path: "../project1" } + ] + }), + "/src/projects/project2/class2.ts": `class class2 {}`, + }), + edits: [ + { + subScenario: "Add class3 to project1 and build it", + modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.ts", `class class3 {}`, "utf-8"), + discrepancyExplanation: () => [ + "Ts buildinfo will not be updated in incremental build so it will have semantic diagnostics cached from previous build", + "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", + ], + }, + { + subScenario: "Add output of class3", + modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), + }, + { + subScenario: "Add excluded file to project1", + modifyFs: fs => { + fs.mkdirSync("/src/projects/project1/temp"); + fs.writeFileSync("/src/projects/project1/temp/file.d.ts", `declare class file {}`, "utf-8"); }, - ] - }); + }, + { + subScenario: "Delete output for class3", + modifyFs: fs => fs.unlinkSync("/src/projects/project1/class3.d.ts"), + discrepancyExplanation: () => [ + "Ts buildinfo will be updated but will retain lib file errors from previous build and not others because they are emitted because of change which results in clearing their semantic diagnostics cache", + "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", + ], + }, + { + subScenario: "Create output for class3", + modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), + }, + ] + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "when project has strict true", - commandLineArgs: ["-noEmit", "-p", `src/project`], - fs: () => ts.loadProjectFromFiles({ - "/src/project/tsconfig.json": JSON.stringify({ - compilerOptions: { - incremental: true, - strict: true, - }, - }), - "/src/project/class1.ts": `export class class1 {}`, + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "when project has strict true", + commandLineArgs: ["-noEmit", "-p", `src/project`], + fs: () => ts.loadProjectFromFiles({ + "/src/project/tsconfig.json": JSON.stringify({ + compilerOptions: { + incremental: true, + strict: true, + }, }), - edits: ts.noChangeOnlyRuns, - baselinePrograms: true - }); + "/src/project/class1.ts": `export class class1 {}`, + }), + edits: ts.noChangeOnlyRuns, + baselinePrograms: true + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "serializing error chains", - commandLineArgs: ["-p", `src/project`], - fs: () => ts.loadProjectFromFiles({ - "/src/project/tsconfig.json": JSON.stringify({ - compilerOptions: { - incremental: true, - strict: true, - jsx: "react", - module: "esnext", - }, - }), - "/src/project/index.tsx": Utils.dedent` + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "serializing error chains", + commandLineArgs: ["-p", `src/project`], + fs: () => ts.loadProjectFromFiles({ + "/src/project/tsconfig.json": JSON.stringify({ + compilerOptions: { + incremental: true, + strict: true, + jsx: "react", + module: "esnext", + }, + }), + "/src/project/index.tsx": Utils.dedent` declare namespace JSX { interface ElementChildrenAttribute { children: {}; } interface IntrinsicElements { div: {} } @@ -430,15 +430,15 @@ declare global {
)` - }, `\ninterface ReadonlyArray { readonly length: number }`), - edits: ts.noChangeOnlyRuns, - }); + }, `\ninterface ReadonlyArray { readonly length: number }`), + edits: ts.noChangeOnlyRuns, + }); - ts.verifyTsc({ - scenario: "incremental", - subScenario: "ts file with no-default-lib that augments the global scope", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": Utils.dedent` + ts.verifyTsc({ + scenario: "incremental", + subScenario: "ts file with no-default-lib that augments the global scope", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": Utils.dedent` /// /// @@ -449,7 +449,7 @@ declare global { export {}; `, - "/src/project/tsconfig.json": Utils.dedent` + "/src/project/tsconfig.json": Utils.dedent` { "compilerOptions": { "target": "ESNext", @@ -458,46 +458,46 @@ declare global { "outDir": "dist", }, }`, - }), - commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], - modifyFs: (fs) => { - fs.writeFileSync("/lib/lib.esnext.d.ts", ts.libContent); - } - }); + }), + commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], + modifyFs: (fs) => { + fs.writeFileSync("/lib/lib.esnext.d.ts", ts.libContent); + } + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "change to type that gets used as global through export in another file", - commandLineArgs: ["-p", `src/project`], - fs: () => ts.loadProjectFromFiles({ - "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), - "/src/project/class1.ts": `const a: MagicNumber = 1; + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "change to type that gets used as global through export in another file", + commandLineArgs: ["-p", `src/project`], + fs: () => ts.loadProjectFromFiles({ + "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), + "/src/project/class1.ts": `const a: MagicNumber = 1; console.log(a);`, - "/src/project/constants.ts": "export default 1;", - "/src/project/types.d.ts": `type MagicNumber = typeof import('./constants').default`, - }), - edits: [{ - subScenario: "Modify imports used in global file", - modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), - }], - }); + "/src/project/constants.ts": "export default 1;", + "/src/project/types.d.ts": `type MagicNumber = typeof import('./constants').default`, + }), + edits: [{ + subScenario: "Modify imports used in global file", + modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), + }], + }); - ts.verifyTscWithEdits({ - scenario: "incremental", - subScenario: "change to type that gets used as global through export in another file through indirect import", - commandLineArgs: ["-p", `src/project`], - fs: () => ts.loadProjectFromFiles({ - "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), - "/src/project/class1.ts": `const a: MagicNumber = 1; + ts.verifyTscWithEdits({ + scenario: "incremental", + subScenario: "change to type that gets used as global through export in another file through indirect import", + commandLineArgs: ["-p", `src/project`], + fs: () => ts.loadProjectFromFiles({ + "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), + "/src/project/class1.ts": `const a: MagicNumber = 1; console.log(a);`, - "/src/project/constants.ts": "export default 1;", - "/src/project/reexport.ts": `export { default as ConstantNumber } from "./constants"`, - "/src/project/types.d.ts": `type MagicNumber = typeof import('./reexport').ConstantNumber`, - }), - edits: [{ - subScenario: "Modify imports used in global file", - modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), - }], - }); + "/src/project/constants.ts": "export default 1;", + "/src/project/reexport.ts": `export { default as ConstantNumber } from "./constants"`, + "/src/project/types.d.ts": `type MagicNumber = typeof import('./reexport').ConstantNumber`, + }), + edits: [{ + subScenario: "Modify imports used in global file", + modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), + }], }); +}); } diff --git a/src/testRunner/unittests/tsc/listFilesOnly.ts b/src/testRunner/unittests/tsc/listFilesOnly.ts index 90bb5907105b9..df8d3310b024e 100644 --- a/src/testRunner/unittests/tsc/listFilesOnly.ts +++ b/src/testRunner/unittests/tsc/listFilesOnly.ts @@ -1,23 +1,23 @@ namespace ts { - describe("unittests:: tsc:: listFilesOnly::", () => { - ts.verifyTsc({ - scenario: "listFilesOnly", - subScenario: "combined with watch", - fs: () => ts.loadProjectFromFiles({ - "/src/test.ts": Utils.dedent` +describe("unittests:: tsc:: listFilesOnly::", () => { + ts.verifyTsc({ + scenario: "listFilesOnly", + subScenario: "combined with watch", + fs: () => ts.loadProjectFromFiles({ + "/src/test.ts": Utils.dedent` export const x = 1;`, - }), - commandLineArgs: ["/src/test.ts", "--watch", "--listFilesOnly"] - }); + }), + commandLineArgs: ["/src/test.ts", "--watch", "--listFilesOnly"] + }); - ts.verifyTsc({ - scenario: "listFilesOnly", - subScenario: "loose file", - fs: () => ts.loadProjectFromFiles({ - "/src/test.ts": Utils.dedent` + ts.verifyTsc({ + scenario: "listFilesOnly", + subScenario: "loose file", + fs: () => ts.loadProjectFromFiles({ + "/src/test.ts": Utils.dedent` export const x = 1;`, - }), - commandLineArgs: ["/src/test.ts", "--listFilesOnly"] - }); + }), + commandLineArgs: ["/src/test.ts", "--listFilesOnly"] }); +}); } diff --git a/src/testRunner/unittests/tsc/projectReferences.ts b/src/testRunner/unittests/tsc/projectReferences.ts index 7c4cd855282d5..7c2c84e36fb40 100644 --- a/src/testRunner/unittests/tsc/projectReferences.ts +++ b/src/testRunner/unittests/tsc/projectReferences.ts @@ -1,42 +1,42 @@ namespace ts { - describe("unittests:: tsc:: projectReferences::", () => { - ts.verifyTsc({ - scenario: "projectReferences", - subScenario: "when project contains invalid project reference", - fs: () => ts.loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": JSON.stringify({ - compilerOptions: { - module: "amd", - outFile: "theApp.js" - }, - references: [ - { path: "../Util/Dates" } - ] - }), +describe("unittests:: tsc:: projectReferences::", () => { + ts.verifyTsc({ + scenario: "projectReferences", + subScenario: "when project contains invalid project reference", + fs: () => ts.loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": JSON.stringify({ + compilerOptions: { + module: "amd", + outFile: "theApp.js" + }, + references: [ + { path: "../Util/Dates" } + ] }), - commandLineArgs: ["--p", "src/project"], - }); + }), + commandLineArgs: ["--p", "src/project"], + }); - ts.verifyTsc({ - scenario: "projectReferences", - subScenario: "when project references composite project with noEmit", - fs: () => ts.loadProjectFromFiles({ - "/src/utils/index.ts": "export const x = 10;", - "/src/utils/tsconfig.json": JSON.stringify({ - compilerOptions: { - composite: true, - noEmit: true, - } - }), - "/src/project/index.ts": `import { x } from "../utils";`, - "/src/project/tsconfig.json": JSON.stringify({ - references: [ - { path: "../utils" } - ] - }), + ts.verifyTsc({ + scenario: "projectReferences", + subScenario: "when project references composite project with noEmit", + fs: () => ts.loadProjectFromFiles({ + "/src/utils/index.ts": "export const x = 10;", + "/src/utils/tsconfig.json": JSON.stringify({ + compilerOptions: { + composite: true, + noEmit: true, + } + }), + "/src/project/index.ts": `import { x } from "../utils";`, + "/src/project/tsconfig.json": JSON.stringify({ + references: [ + { path: "../utils" } + ] }), - commandLineArgs: ["--p", "src/project"] - }); + }), + commandLineArgs: ["--p", "src/project"] }); +}); } diff --git a/src/testRunner/unittests/tsc/redirect.ts b/src/testRunner/unittests/tsc/redirect.ts index 67a549b8dd765..676f47613716c 100644 --- a/src/testRunner/unittests/tsc/redirect.ts +++ b/src/testRunner/unittests/tsc/redirect.ts @@ -1,34 +1,34 @@ namespace ts { - describe("unittests:: tsc:: redirect::", () => { - ts.verifyTsc({ - scenario: "redirect", - subScenario: "when redirecting ts file", - fs: () => ts.loadProjectFromFiles({ - "/src/project/tsconfig.json": JSON.stringify({ - compilerOptions: { - outDir: "out" - }, - include: [ - "copy1/node_modules/target/*", - "copy2/node_modules/target/*", - ] - }), - "/src/project/copy1/node_modules/target/index.ts": "export const a = 1;", - "/src/project/copy1/node_modules/target/import.ts": `import {} from "./";`, - "/src/project/copy1/node_modules/target/package.json": JSON.stringify({ - name: "target", - version: "1.0.0", - main: "index.js", - }), - "/src/project/copy2/node_modules/target/index.ts": "export const a = 1;", - "/src/project/copy2/node_modules/target/import.ts": `import {} from "./";`, - "/src/project/copy2/node_modules/target/package.json": JSON.stringify({ - name: "target", - version: "1.0.0", - main: "index.js", - }), +describe("unittests:: tsc:: redirect::", () => { + ts.verifyTsc({ + scenario: "redirect", + subScenario: "when redirecting ts file", + fs: () => ts.loadProjectFromFiles({ + "/src/project/tsconfig.json": JSON.stringify({ + compilerOptions: { + outDir: "out" + }, + include: [ + "copy1/node_modules/target/*", + "copy2/node_modules/target/*", + ] }), - commandLineArgs: ["-p", "src/project"], - }); + "/src/project/copy1/node_modules/target/index.ts": "export const a = 1;", + "/src/project/copy1/node_modules/target/import.ts": `import {} from "./";`, + "/src/project/copy1/node_modules/target/package.json": JSON.stringify({ + name: "target", + version: "1.0.0", + main: "index.js", + }), + "/src/project/copy2/node_modules/target/index.ts": "export const a = 1;", + "/src/project/copy2/node_modules/target/import.ts": `import {} from "./";`, + "/src/project/copy2/node_modules/target/package.json": JSON.stringify({ + name: "target", + version: "1.0.0", + main: "index.js", + }), + }), + commandLineArgs: ["-p", "src/project"], }); +}); } diff --git a/src/testRunner/unittests/tsc/runWithoutArgs.ts b/src/testRunner/unittests/tsc/runWithoutArgs.ts index baa5b30a76cc8..462282affa42a 100644 --- a/src/testRunner/unittests/tsc/runWithoutArgs.ts +++ b/src/testRunner/unittests/tsc/runWithoutArgs.ts @@ -1,27 +1,27 @@ namespace ts { - describe("unittests:: tsc:: runWithoutArgs::", () => { - ts.verifyTsc({ - scenario: "runWithoutArgs", - subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped", - fs: () => ts.loadProjectFromFiles({}), - commandLineArgs: [], - environmentVariables: { TS_TEST_TERMINAL_WIDTH: "120" } - }); - - ts.verifyTsc({ - scenario: "runWithoutArgs", - subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped when host can't provide terminal width", - fs: () => ts.loadProjectFromFiles({}), - commandLineArgs: [], - }); +describe("unittests:: tsc:: runWithoutArgs::", () => { + ts.verifyTsc({ + scenario: "runWithoutArgs", + subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped", + fs: () => ts.loadProjectFromFiles({}), + commandLineArgs: [], + environmentVariables: { TS_TEST_TERMINAL_WIDTH: "120" } + }); - ts.verifyTsc({ - scenario: "runWithoutArgs", - subScenario: "does not add color when NO_COLOR is set", - fs: () => ts.loadProjectFromFiles({}), - commandLineArgs: [], - environmentVariables: { NO_COLOR: "true" } - }); + ts.verifyTsc({ + scenario: "runWithoutArgs", + subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped when host can't provide terminal width", + fs: () => ts.loadProjectFromFiles({}), + commandLineArgs: [], + }); + ts.verifyTsc({ + scenario: "runWithoutArgs", + subScenario: "does not add color when NO_COLOR is set", + fs: () => ts.loadProjectFromFiles({}), + commandLineArgs: [], + environmentVariables: { NO_COLOR: "true" } }); + +}); } diff --git a/src/testRunner/unittests/tscWatch/consoleClearing.ts b/src/testRunner/unittests/tscWatch/consoleClearing.ts index 26fbd7ec2ec3f..33b55e480ac5c 100644 --- a/src/testRunner/unittests/tscWatch/consoleClearing.ts +++ b/src/testRunner/unittests/tscWatch/consoleClearing.ts @@ -1,66 +1,66 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: console clearing", () => { - const scenario = "consoleClearing"; - const file: ts.tscWatch.File = { - path: "/f.ts", - content: "" - }; +describe("unittests:: tsc-watch:: console clearing", () => { + const scenario = "consoleClearing"; + const file: ts.tscWatch.File = { + path: "/f.ts", + content: "" + }; - const makeChangeToFile: ts.tscWatch.TscWatchCompileChange[] = [{ - caption: "Comment added to file f", - change: sys => sys.modifyFile(file.path, "//"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }]; + const makeChangeToFile: ts.tscWatch.TscWatchCompileChange[] = [{ + caption: "Comment added to file f", + change: sys => sys.modifyFile(file.path, "//"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }]; - function checkConsoleClearingUsingCommandLineOptions(subScenario: string, commandLineOptions?: string[]) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario, - commandLineArgs: ["--w", file.path, ...commandLineOptions || ts.emptyArray], - sys: () => ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile]), - changes: makeChangeToFile, - }); - } + function checkConsoleClearingUsingCommandLineOptions(subScenario: string, commandLineOptions?: string[]) { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario, + commandLineArgs: ["--w", file.path, ...commandLineOptions || ts.emptyArray], + sys: () => ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile]), + changes: makeChangeToFile, + }); + } - checkConsoleClearingUsingCommandLineOptions("without --diagnostics or --extendedDiagnostics"); - checkConsoleClearingUsingCommandLineOptions("with --diagnostics", ["--diagnostics"]); - checkConsoleClearingUsingCommandLineOptions("with --extendedDiagnostics", ["--extendedDiagnostics"]); - checkConsoleClearingUsingCommandLineOptions("with --preserveWatchOutput", ["--preserveWatchOutput"]); + 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: ts.CompilerOptions = { - preserveWatchOutput: true - }; - const configFile: ts.tscWatch.File = { - path: "/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - const files = [file, configFile, ts.tscWatch.libFile]; - it("using createWatchOfConfigFile ", () => { - const baseline = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files)); - const watch = ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - system: baseline.sys, - cb: baseline.cb, - configFileName: configFile.path, - })); - // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed - ts.tscWatch.runWatchBaseline({ - scenario, - subScenario: "when preserveWatchOutput is true in config file/createWatchOfConfigFile", - commandLineArgs: ["--w", "-p", configFile.path], - ...baseline, - getPrograms: () => [[watch.getCurrentProgram().getProgram(), watch.getCurrentProgram()]], - changes: makeChangeToFile, - watchOrSolution: watch - }); - }); - ts.tscWatch.verifyTscWatch({ + describe("when preserveWatchOutput is true in config file", () => { + const compilerOptions: ts.CompilerOptions = { + preserveWatchOutput: true + }; + const configFile: ts.tscWatch.File = { + path: "/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const files = [file, configFile, ts.tscWatch.libFile]; + it("using createWatchOfConfigFile ", () => { + const baseline = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files)); + const watch = ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + system: baseline.sys, + cb: baseline.cb, + configFileName: configFile.path, + })); + // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed + ts.tscWatch.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: () => ts.tscWatch.createWatchedSystem(files), + ...baseline, + getPrograms: () => [[watch.getCurrentProgram().getProgram(), watch.getCurrentProgram()]], changes: makeChangeToFile, + watchOrSolution: watch }); }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when preserveWatchOutput is true in config file/when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", + commandLineArgs: ["--w", "-p", configFile.path], + sys: () => ts.tscWatch.createWatchedSystem(files), + changes: makeChangeToFile, + }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/emit.ts b/src/testRunner/unittests/tscWatch/emit.ts index 016fce2c6e1d5..9c8c0bbb6ef6a 100644 --- a/src/testRunner/unittests/tscWatch/emit.ts +++ b/src/testRunner/unittests/tscWatch/emit.ts @@ -1,515 +1,515 @@ namespace ts.tscWatch { - const scenario = "emit"; - describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { - function verifyOutAndOutFileSetting(subScenario: string, out?: string, outFile?: string) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `emit with outFile or out setting/${subScenario}`, - commandLineArgs: ["--w", "-p", "/a/tsconfig.json"], - sys: () => { - const config: ts.tscWatch.File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { out, outFile } }) - }; - const f1: ts.tscWatch.File = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2: ts.tscWatch.File = { - path: "/a/b.ts", - content: "let y = 1" - }; - return ts.tscWatch.createWatchedSystem([f1, f2, config, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Make change in the file", - change: sys => sys.writeFile("/a/a.ts", "let x = 11"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks - }, - { - caption: "Make change in the file again", - change: sys => sys.writeFile("/a/a.ts", "let xy = 11"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks - } - ] - }); - } - 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) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `emit with outFile or out setting/${subScenario}`, - commandLineArgs: ["--w", "-p", "/a/b/project/tsconfig.json"], - sys: () => { - const file1: ts.tscWatch.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: ts.tscWatch.File = { - path: "/a/b/dependencies/file2.d.ts", - content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" - }; - const file3: ts.tscWatch.File = { - path: "/a/b/project/src/main.ts", - content: "namespace Main { export function fooBar() {} }" - }; - const file4: ts.tscWatch.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: ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, file3, file4, ts.tscWatch.libFile, configFile]); - }, - changes: ts.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?: () => ts.tscWatch.File[]; - /** initial list of files to emit if not the default list */ - firstReloadFileList?: string[]; - changes: ts.tscWatch.TscWatchCompileChange[]; - } - function verifyTscWatchEmit({ subScenario, configObj, getAdditionalFileOrFolder, firstReloadFileList, changes }: VerifyTscWatchEmit) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `emit for configured projects/${subScenario}`, - commandLineArgs: ["--w", "-p", configFilePath], - sys: () => { - const moduleFile1: ts.tscWatch.File = { - path: moduleFile1Path, - content: "export function Foo() { };", - }; - - const file1Consumer1: ts.tscWatch.File = { - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }; - - const file1Consumer2: ts.tscWatch.File = { - path: file1Consumer2Path, - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }; - - const moduleFile2: ts.tscWatch.File = { - path: moduleFile2Path, - content: `export var Foo4 = 10;`, - }; - - const globalFile3: ts.tscWatch.File = { - path: globalFilePath, - content: `interface GlobalFoo { age: number }` - }; - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: JSON.stringify(configObj || {}) - }; - const additionalFiles = getAdditionalFileOrFolder?.() || ts.emptyArray; - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.tscWatch.libFile, ...additionalFiles]; - return ts.tscWatch.createWatchedSystem(firstReloadFileList ? - ts.map(firstReloadFileList, fileName => ts.find(files, file => file.path === fileName)!) : - files); - }, - changes - }); - } - - function modifyModuleFile1Shape(sys: ts.tscWatch.WatchedSystem) { - sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { };`); - } - const changeModuleFile1Shape: ts.tscWatch.TscWatchCompileChange = { - caption: "Change the content of moduleFile1 to `export var T: number;export function Foo() { };`", - change: modifyModuleFile1Shape, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }; - - 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, - { - caption: "Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`", - change: sys => sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { console.log('hi'); };`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with the reference map changes", - changes: [ - { - caption: "Change file1Consumer1 content to `export let y = Foo();`", - change: sys => sys.writeFile(file1Consumer1Path, `export let y = Foo();`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - changeModuleFile1Shape, - { - caption: "Add the import statements back to file1Consumer1", - change: sys => sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`", - change: sys => sys.writeFile(moduleFile1Path, `export let y = Foo();`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "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() { };` - change: sys => { - sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); - modifyModuleFile1Shape(sys); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with deleted files", - changes: [ - { - caption: "change moduleFile1 shape and delete file1Consumer2", - change: sys => { - modifyModuleFile1Shape(sys); - sys.deleteFile(file1Consumer2Path); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with newly created files", - changes: [ - { - caption: "change moduleFile1 shape and create file1Consumer3", - change: sys => { - sys.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); - modifyModuleFile1Shape(sys); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect changes in non-root files", - configObj: { files: [file1Consumer1Path] }, - changes: [ - changeModuleFile1Shape, - { - caption: "change file1 internal, and verify only file1 is affected", - change: sys => sys.appendFile(moduleFile1Path, "var T1: number;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should return all files if a global file changed shape", - changes: [ - { - caption: "change shape of global file", - change: sys => sys.appendFile(globalFilePath, "var T2: string;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - 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: [ - { - caption: "change file1Consumer1", - change: sys => sys.appendFile(file1Consumer1Path, "export var T: number;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - changeModuleFile1Shape, - { - caption: "change file1Consumer1 and moduleFile1", - change: sys => { - sys.appendFile(file1Consumer1Path, "export var T2: number;"); - sys.writeFile(moduleFile1Path, `export var T2: number;export function Foo() { };`); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - 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: [ts.tscWatch.libFile.path, "/a/b/file1.ts", "/a/b/file2.ts", configFilePath], - changes: [ - { - caption: "change file1", - change: sys => sys.appendFile("/a/b/file1.ts", "export var t3 = 10;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect removed code file", - getAdditionalFileOrFolder: () => [{ - path: "/a/b/referenceFile1.ts", - content: `/// -export var x = Foo();` - }], - firstReloadFileList: [ts.tscWatch.libFile.path, "/a/b/referenceFile1.ts", moduleFile1Path, configFilePath], - changes: [ - { - caption: "delete moduleFile1", - change: sys => sys.deleteFile(moduleFile1Path), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect non existing code file", - getAdditionalFileOrFolder: () => [{ - path: "/a/b/referenceFile1.ts", - content: `/// -export var x = Foo();` - }], - firstReloadFileList: [ts.tscWatch.libFile.path, "/a/b/referenceFile1.ts", configFilePath], - changes: [ - { - caption: "edit refereceFile1", - change: sys => sys.appendFile("/a/b/referenceFile1.ts", "export var yy = Foo();"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "create moduleFile2", - change: sys => sys.writeFile(moduleFile2Path, "export var Foo4 = 10;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - }); - - describe("unittests:: tsc-watch:: emit file content", () => { - function verifyNewLine(subScenario: string, newLine: string) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `emit file content/${subScenario}`, - commandLineArgs: ["--w", "/a/app.ts"], - sys: () => ts.tscWatch.createWatchedSystem([ - { - path: "/a/app.ts", - content: ["var x = 1;", "var y = 2;"].join(newLine) - }, - ts.tscWatch.libFile - ], { newLine }), - changes: [ - { - caption: "Append a line", - change: sys => sys.appendFile("/a/app.ts", newLine + "var z = 3;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ], - }); - } - verifyNewLine("handles new lines lineFeed", "\n"); - verifyNewLine("handles new lines carriageReturn lineFeed", "\r\n"); - +const scenario = "emit"; +describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { + function verifyOutAndOutFileSetting(subScenario: string, out?: string, outFile?: string) { ts.tscWatch.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 config: ts.tscWatch.File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { out, outFile } }) }; - - 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 f1: ts.tscWatch.File = { + path: "/a/a.ts", + content: "let x = 1" }; - - const configFile = { - path: "/a/b/tsconfig.json", - content: "{}" + const f2: ts.tscWatch.File = { + path: "/a/b.ts", + content: "let y = 1" }; - return ts.tscWatch.createWatchedSystem([file1, file2, file3, configFile, ts.tscWatch.libFile]); + return ts.tscWatch.createWatchedSystem([f1, f2, config, ts.tscWatch.libFile]); }, changes: [ { - caption: "Append content to f1", - change: sys => sys.appendFile("/a/b/f1.ts", "export function foo2() { return 2; }"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + caption: "Make change in the file", + change: sys => sys.writeFile("/a/a.ts", "let x = 11"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks }, { - caption: "Again Append content to f1", - change: sys => sys.appendFile("/a/b/f1.ts", "export function fooN() { return 2; }"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + caption: "Make change in the file again", + change: sys => sys.writeFile("/a/a.ts", "let xy = 11"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks } - ], + ] }); + } + 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) { ts.tscWatch.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: ts.tscWatch.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: ts.tscWatch.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: ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Append content to file3", - change: sys => sys.appendFile("/user/someone/projects/myproject/file3.ts", "function foo2() { return 2; }"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ], - }); - - ts.tscWatch.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: ts.tscWatch.File = { - path: `${projectLocation}/app/file.ts`, - content: "var a = 10;" + const file4: ts.tscWatch.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: ts.tscWatch.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, ts.tscWatch.libFile]; - return ts.tscWatch.createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + return ts.tscWatch.createWatchedSystem([file1, file2, file3, file4, ts.tscWatch.libFile, configFile]); }, - changes: [ - { - caption: "file is deleted and then created to modify content", - change: sys => sys.appendFile("/home/username/project/app/file.ts", "\nvar b = 10;", { invokeFileDeleteCreateAsPartInsteadOfChange: true }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] + changes: ts.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 with when module emit is specified as node", () => { +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?: () => ts.tscWatch.File[]; + /** initial list of files to emit if not the default list */ + firstReloadFileList?: string[]; + changes: ts.tscWatch.TscWatchCompileChange[]; + } + function verifyTscWatchEmit({ subScenario, configObj, getAdditionalFileOrFolder, firstReloadFileList, changes }: VerifyTscWatchEmit) { ts.tscWatch.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: ts.tscWatch.File = { - path: "/a/rootFolder/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - module: "none", - allowJs: true, - outDir: "Static/scripts/" - }, - include: [ - "Scripts/**/*" - ], - }) + const moduleFile1: ts.tscWatch.File = { + path: moduleFile1Path, + content: "export function Foo() { };", }; - const file1: ts.tscWatch.File = { - path: "/a/rootFolder/project/Scripts/TypeScript.ts", - content: "var z = 10;" + + const file1Consumer1: ts.tscWatch.File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, }; - const file2: ts.tscWatch.File = { - path: "/a/rootFolder/project/Scripts/Javascript.js", - content: "var zz = 10;" + + const file1Consumer2: ts.tscWatch.File = { + path: file1Consumer2Path, + content: `import {Foo} from "./moduleFile1"; let z = 10;`, }; - return ts.tscWatch.createWatchedSystem([configFile, file1, file2, ts.tscWatch.libFile]); + + const moduleFile2: ts.tscWatch.File = { + path: moduleFile2Path, + content: `export var Foo4 = 10;`, + }; + + const globalFile3: ts.tscWatch.File = { + path: globalFilePath, + content: `interface GlobalFoo { age: number }` + }; + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: JSON.stringify(configObj || {}) + }; + const additionalFiles = getAdditionalFileOrFolder?.() || ts.emptyArray; + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.tscWatch.libFile, ...additionalFiles]; + return ts.tscWatch.createWatchedSystem(firstReloadFileList ? + ts.map(firstReloadFileList, fileName => ts.find(files, file => file.path === fileName)!) : + files); + }, + changes + }); + } + + function modifyModuleFile1Shape(sys: ts.tscWatch.WatchedSystem) { + sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { };`); + } + const changeModuleFile1Shape: ts.tscWatch.TscWatchCompileChange = { + caption: "Change the content of moduleFile1 to `export var T: number;export function Foo() { };`", + change: modifyModuleFile1Shape, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }; + + 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, + { + caption: "Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`", + change: sys => sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { console.log('hi'); };`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should be up-to-date with the reference map changes", + changes: [ + { + caption: "Change file1Consumer1 content to `export let y = Foo();`", + change: sys => sys.writeFile(file1Consumer1Path, `export let y = Foo();`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + changeModuleFile1Shape, + { + caption: "Add the import statements back to file1Consumer1", + change: sys => sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`", + change: sys => sys.writeFile(moduleFile1Path, `export let y = Foo();`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "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() { };` + change: sys => { + sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); + modifyModuleFile1Shape(sys); + }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should be up-to-date with deleted files", + changes: [ + { + caption: "change moduleFile1 shape and delete file1Consumer2", + change: sys => { + modifyModuleFile1Shape(sys); + sys.deleteFile(file1Consumer2Path); + }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should be up-to-date with newly created files", + changes: [ + { + caption: "change moduleFile1 shape and create file1Consumer3", + change: sys => { + sys.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + modifyModuleFile1Shape(sys); + }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should detect changes in non-root files", + configObj: { files: [file1Consumer1Path] }, + changes: [ + changeModuleFile1Shape, + { + caption: "change file1 internal, and verify only file1 is affected", + change: sys => sys.appendFile(moduleFile1Path, "var T1: number;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should return all files if a global file changed shape", + changes: [ + { + caption: "change shape of global file", + change: sys => sys.appendFile(globalFilePath, "var T2: string;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + 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: [ + { + caption: "change file1Consumer1", + change: sys => sys.appendFile(file1Consumer1Path, "export var T: number;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + changeModuleFile1Shape, + { + caption: "change file1Consumer1 and moduleFile1", + change: sys => { + sys.appendFile(file1Consumer1Path, "export var T2: number;"); + sys.writeFile(moduleFile1Path, `export var T2: number;export function Foo() { };`); + }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + 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: [ts.tscWatch.libFile.path, "/a/b/file1.ts", "/a/b/file2.ts", configFilePath], + changes: [ + { + caption: "change file1", + change: sys => sys.appendFile("/a/b/file1.ts", "export var t3 = 10;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should detect removed code file", + getAdditionalFileOrFolder: () => [{ + path: "/a/b/referenceFile1.ts", + content: `/// +export var x = Foo();` + }], + firstReloadFileList: [ts.tscWatch.libFile.path, "/a/b/referenceFile1.ts", moduleFile1Path, configFilePath], + changes: [ + { + caption: "delete moduleFile1", + change: sys => sys.deleteFile(moduleFile1Path), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); + + verifyTscWatchEmit({ + subScenario: "should detect non existing code file", + getAdditionalFileOrFolder: () => [{ + path: "/a/b/referenceFile1.ts", + content: `/// +export var x = Foo();` + }], + firstReloadFileList: [ts.tscWatch.libFile.path, "/a/b/referenceFile1.ts", configFilePath], + changes: [ + { + caption: "edit refereceFile1", + change: sys => sys.appendFile("/a/b/referenceFile1.ts", "export var yy = Foo();"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "create moduleFile2", + change: sys => sys.writeFile(moduleFile2Path, "export var Foo4 = 10;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); +}); + +describe("unittests:: tsc-watch:: emit file content", () => { + function verifyNewLine(subScenario: string, newLine: string) { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `emit file content/${subScenario}`, + commandLineArgs: ["--w", "/a/app.ts"], + sys: () => ts.tscWatch.createWatchedSystem([ + { + path: "/a/app.ts", + content: ["var x = 1;", "var y = 2;"].join(newLine) + }, + ts.tscWatch.libFile + ], { newLine }), changes: [ { - caption: "Modify typescript file", - change: sys => sys.modifyFile("/a/rootFolder/project/Scripts/TypeScript.ts", "var zz30 = 100;", { invokeDirectoryWatcherInsteadOfFileChanged: true }), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + caption: "Append a line", + change: sys => sys.appendFile("/a/app.ts", newLine + "var z = 3;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, } ], }); + } + verifyNewLine("handles new lines lineFeed", "\n"); + verifyNewLine("handles new lines carriageReturn lineFeed", "\r\n"); + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, file3, configFile, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Append content to f1", + change: sys => sys.appendFile("/a/b/f1.ts", "export function foo2() { return 2; }"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Again Append content to f1", + change: sys => sys.appendFile("/a/b/f1.ts", "export function fooN() { return 2; }"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ], + }); + + ts.tscWatch.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: ts.tscWatch.File = { + path: `${currentDirectory}/file1.ts`, + content: "export const enum E1 { V = 1 }" + }; + const file2: ts.tscWatch.File = { + path: `${currentDirectory}/file2.ts`, + content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` + }; + const file3: ts.tscWatch.File = { + path: `${currentDirectory}/file3.ts`, + content: `import { E2 } from "./file2"; const v: E2 = E2.V;` + }; + return ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Append content to file3", + change: sys => sys.appendFile("/user/someone/projects/myproject/file3.ts", "function foo2() { return 2; }"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ], + }); + + ts.tscWatch.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: ts.tscWatch.File = { + path: `${projectLocation}/app/file.ts`, + content: "var a = 10;" + }; + const configFile: ts.tscWatch.File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + include: [ + "app/**/*.ts" + ] + }) + }; + const files = [file, configFile, ts.tscWatch.libFile]; + return ts.tscWatch.createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + }, + changes: [ + { + caption: "file is deleted and then created to modify content", + change: sys => sys.appendFile("/home/username/project/app/file.ts", "\nvar b = 10;", { invokeFileDeleteCreateAsPartInsteadOfChange: true }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); +}); + +describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => { + ts.tscWatch.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: ts.tscWatch.File = { + path: "/a/rootFolder/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + module: "none", + allowJs: true, + outDir: "Static/scripts/" + }, + include: [ + "Scripts/**/*" + ], + }) + }; + const file1: ts.tscWatch.File = { + path: "/a/rootFolder/project/Scripts/TypeScript.ts", + content: "var z = 10;" + }; + const file2: ts.tscWatch.File = { + path: "/a/rootFolder/project/Scripts/Javascript.js", + content: "var zz = 10;" + }; + return ts.tscWatch.createWatchedSystem([configFile, file1, file2, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Modify typescript file", + change: sys => sys.modifyFile("/a/rootFolder/project/Scripts/TypeScript.ts", "var zz30 = 100;", { invokeDirectoryWatcherInsteadOfFileChanged: true }), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], }); +}); } diff --git a/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts index 52b6ab0524ca7..ce5c1cbf996a3 100644 --- a/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts +++ b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts @@ -1,159 +1,159 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: `{}` - }; - interface VerifyEmitAndErrorUpdatesWorker extends VerifyEmitAndErrorUpdates { - configFile: () => ts.tscWatch.File; - } - function verifyEmitAndErrorUpdatesWorker({ subScenario, files, currentDirectory, lib, configFile, changes, baselineIncremental }: VerifyEmitAndErrorUpdatesWorker) { - ts.tscWatch.verifyTscWatch({ - scenario: "emitAndErrorUpdates", - subScenario, - commandLineArgs: ["--w"], - sys: () => ts.tscWatch.createWatchedSystem([...files(), configFile(), lib?.() || ts.tscWatch.libFile], { currentDirectory: currentDirectory || ts.tscWatch.projectRoot }), - changes, - baselineIncremental - }); - ts.tscWatch.verifyTscWatch({ - scenario: "emitAndErrorUpdates", - subScenario: `incremental/${subScenario}`, - commandLineArgs: ["--w", "--i"], - sys: () => ts.tscWatch.createWatchedSystem([...files(), configFile(), lib?.() || ts.tscWatch.libFile], { currentDirectory: currentDirectory || ts.tscWatch.projectRoot }), - changes, - baselineIncremental - }); - } +describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: `{}` + }; + interface VerifyEmitAndErrorUpdatesWorker extends VerifyEmitAndErrorUpdates { + configFile: () => ts.tscWatch.File; + } + function verifyEmitAndErrorUpdatesWorker({ subScenario, files, currentDirectory, lib, configFile, changes, baselineIncremental }: VerifyEmitAndErrorUpdatesWorker) { + ts.tscWatch.verifyTscWatch({ + scenario: "emitAndErrorUpdates", + subScenario, + commandLineArgs: ["--w"], + sys: () => ts.tscWatch.createWatchedSystem([...files(), configFile(), lib?.() || ts.tscWatch.libFile], { currentDirectory: currentDirectory || ts.tscWatch.projectRoot }), + changes, + baselineIncremental + }); + ts.tscWatch.verifyTscWatch({ + scenario: "emitAndErrorUpdates", + subScenario: `incremental/${subScenario}`, + commandLineArgs: ["--w", "--i"], + sys: () => ts.tscWatch.createWatchedSystem([...files(), configFile(), lib?.() || ts.tscWatch.libFile], { currentDirectory: currentDirectory || ts.tscWatch.projectRoot }), + changes, + baselineIncremental + }); + } - function changeCompilerOptions(input: VerifyEmitAndErrorUpdates, additionalOptions: ts.CompilerOptions): ts.tscWatch.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) }; - } + function changeCompilerOptions(input: VerifyEmitAndErrorUpdates, additionalOptions: ts.CompilerOptions): ts.tscWatch.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: () => ts.tscWatch.File[]; - currentDirectory?: string; - lib?: () => ts.tscWatch.File; - changes: ts.tscWatch.TscWatchCompileChange[]; - configFile?: () => ts.tscWatch.File; - baselineIncremental?: boolean; - } - function verifyEmitAndErrorUpdates(input: VerifyEmitAndErrorUpdates) { - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `default/${input.subScenario}`, - configFile: () => input.configFile?.() || config - }); + interface VerifyEmitAndErrorUpdates { + subScenario: string; + files: () => ts.tscWatch.File[]; + currentDirectory?: string; + lib?: () => ts.tscWatch.File; + changes: ts.tscWatch.TscWatchCompileChange[]; + configFile?: () => ts.tscWatch.File; + baselineIncremental?: boolean; + } + 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: `defaultAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { declaration: true }) + }); - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `isolatedModules/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { isolatedModules: 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: `isolatedModulesAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { isolatedModules: true, declaration: true }) + }); - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `assumeChangesOnlyAffectDirectDependencies/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: 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 }) - }); - } + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `assumeChangesOnlyAffectDirectDependenciesAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true, declaration: true }) + }); + } - describe("deep import changes", () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import {B} from './b'; + describe("deep import changes", () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.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: ts.tscWatch.File, cFile: ts.tscWatch.File) { - verifyEmitAndErrorUpdates({ - subScenario: `deepImportChanges/${subScenario}`, - files: () => [aFile, bFile, cFile], - changes: [ - { - caption: "Rename property d to d2 of class C to initialize signatures", - change: sys => sys.writeFile(cFile.path, cFile.content.replace("d", "d2")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property d2 to d of class C to revert back to original text", - change: sys => sys.writeFile(cFile.path, cFile.content.replace("d2", "d")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property d to d2 of class C", - change: sys => sys.writeFile(cFile.path, cFile.content.replace("d", "d2")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); - } - describe("updates errors when deep import file changes", () => { - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `import {C} from './c'; + function verifyDeepImportChange(subScenario: string, bFile: ts.tscWatch.File, cFile: ts.tscWatch.File) { + verifyEmitAndErrorUpdates({ + subScenario: `deepImportChanges/${subScenario}`, + files: () => [aFile, bFile, cFile], + changes: [ + { + caption: "Rename property d to d2 of class C to initialize signatures", + change: sys => sys.writeFile(cFile.path, cFile.content.replace("d", "d2")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property d2 to d of class C to revert back to original text", + change: sys => sys.writeFile(cFile.path, cFile.content.replace("d2", "d")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property d to d2 of class C", + change: sys => sys.writeFile(cFile.path, cFile.content.replace("d", "d2")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], + }); + } + describe("updates errors when deep import file changes", () => { + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `import {C} from './c'; export class B { c = new C(); }` - }; - const cFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/c.ts`, - content: `export class C + }; + const cFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/c.ts`, + content: `export class C { d = 1; }` - }; - verifyDeepImportChange("errors for .ts change", bFile, cFile); - }); - describe("updates errors when deep import through declaration file changes", () => { - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.d.ts`, - content: `import {C} from './c'; + }; + verifyDeepImportChange("errors for .ts change", bFile, cFile); + }); + describe("updates errors when deep import through declaration file changes", () => { + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.d.ts`, + content: `import {C} from './c'; export class B { c: C; }` - }; - const cFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/c.d.ts`, - content: `export class C + }; + const cFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/c.d.ts`, + content: `export class C { d: number; }` - }; - verifyDeepImportChange("errors for .d.ts change", bFile, cFile); - }); + }; + verifyDeepImportChange("errors for .d.ts change", bFile, cFile); }); + }); - describe("updates errors in file not exporting a deep multilevel import that changes", () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `export interface Point { + describe("updates errors in file not exporting a deep multilevel import that changes", () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `export interface Point { name: string; c: Coords; } @@ -161,16 +161,16 @@ export interface Coords { x2: number; y: number; }` - }; - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `import { Point } from "./a"; + }; + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `import { Point } from "./a"; export interface PointWrapper extends Point { }` - }; - const cFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/c.ts`, - content: `import { PointWrapper } from "./b"; + }; + const cFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/c.ts`, + content: `import { PointWrapper } from "./b"; export function getPoint(): PointWrapper { return { name: "test", @@ -180,62 +180,62 @@ export function getPoint(): PointWrapper { } } };` - }; - const dFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/d.ts`, - content: `import { getPoint } from "./c"; + }; + const dFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/d.ts`, + content: `import { getPoint } from "./c"; getPoint().c.x;` - }; - const eFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/e.ts`, - content: `import "./d";` - }; - verifyEmitAndErrorUpdates({ - subScenario: "file not exporting a deep multilevel import that changes", - files: () => [aFile, bFile, cFile, dFile, eFile], - changes: [ - { - caption: "Rename property x2 to x of interface Coords to initialize signatures", - change: sys => sys.writeFile(aFile.path, aFile.content.replace("x2", "x")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property x to x2 of interface Coords to revert back to original text", - change: sys => sys.writeFile(aFile.path, aFile.content.replace("x: number", "x2: number")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property x2 to x of interface Coords", - change: sys => sys.writeFile(aFile.path, aFile.content.replace("x2", "x")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + }; + const eFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/e.ts`, + content: `import "./d";` + }; + verifyEmitAndErrorUpdates({ + subScenario: "file not exporting a deep multilevel import that changes", + files: () => [aFile, bFile, cFile, dFile, eFile], + changes: [ + { + caption: "Rename property x2 to x of interface Coords to initialize signatures", + change: sys => sys.writeFile(aFile.path, aFile.content.replace("x2", "x")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property x to x2 of interface Coords to revert back to original text", + change: sys => sys.writeFile(aFile.path, aFile.content.replace("x: number", "x2: number")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property x2 to x of interface Coords", + change: sys => sys.writeFile(aFile.path, aFile.content.replace("x2", "x")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] }); - describe("updates errors when file transitively exported file changes", () => { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts"], - compilerOptions: { baseUrl: "." } - }) - }; - const app: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/app.ts`, - content: `import { Data } from "lib2/public"; + }); + describe("updates errors when file transitively exported file changes", () => { + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts"], + compilerOptions: { baseUrl: "." } + }) + }; + const app: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/app.ts`, + content: `import { Data } from "lib2/public"; export class App { public constructor() { new Data().test(); } }` - }; - const lib2Public: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib2/public.ts`, - content: `export * from "./data";` - }; - const lib2Data: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; + }; + const lib2Public: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib2/public.ts`, + content: `export * from "./data";` + }; + const lib2Data: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib2/data.ts`, + content: `import { ITest } from "lib1/public"; export class Data { public test() { const result: ITest = { @@ -244,53 +244,53 @@ export class Data { return result; } }` - }; - const lib1Public: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib1/public.ts`, - content: `export * from "./tools/public";` - }; - const lib1ToolsPublic: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib1/tools/public.ts`, - content: `export * from "./tools.interface";` - }; - const lib1ToolsInterface: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib1/tools/tools.interface.ts`, - content: `export interface ITest { + }; + const lib1Public: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib1/public.ts`, + content: `export * from "./tools/public";` + }; + const lib1ToolsPublic: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib1/tools/public.ts`, + content: `export * from "./tools.interface";` + }; + const lib1ToolsInterface: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib1/tools/tools.interface.ts`, + content: `export interface ITest { title: string; }` - }; + }; - function verifyTransitiveExports(subScenario: string, files: readonly ts.tscWatch.File[]) { - verifyEmitAndErrorUpdates({ - subScenario: `transitive exports/${subScenario}`, - files: () => [lib1ToolsInterface, lib1ToolsPublic, app, lib2Public, lib1Public, ...files], - configFile: () => config, - changes: [ - { - caption: "Rename property title to title2 of interface ITest to initialize signatures", - change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property title2 to title of interface ITest to revert back to original text", - change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title2", "title")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rename property title to title2 of interface ITest", - change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); - } - describe("when there are no circular import and exports", () => { - verifyTransitiveExports("no circular import/export", [lib2Data]); + function verifyTransitiveExports(subScenario: string, files: readonly ts.tscWatch.File[]) { + verifyEmitAndErrorUpdates({ + subScenario: `transitive exports/${subScenario}`, + files: () => [lib1ToolsInterface, lib1ToolsPublic, app, lib2Public, lib1Public, ...files], + configFile: () => config, + changes: [ + { + caption: "Rename property title to title2 of interface ITest to initialize signatures", + change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property title2 to title of interface ITest to revert back to original text", + change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title2", "title")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rename property title to title2 of interface ITest", + change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] }); - describe("when there are circular import and exports", () => { - const lib2Data: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; + } + describe("when there are no circular import and exports", () => { + verifyTransitiveExports("no circular import/export", [lib2Data]); + }); + describe("when there are circular import and exports", () => { + const lib2Data: ts.tscWatch.File = { + path: `${ts.tscWatch.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 = { @@ -299,55 +299,55 @@ export class Data { return result; } }` - }; - const lib2Data2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib2/data2.ts`, - content: `import { Data } from "./data"; + }; + const lib2Data2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib2/data2.ts`, + content: `import { Data } from "./data"; export class Data2 { public dat?: Data; }` - }; - verifyTransitiveExports("yes circular import/exports", [lib2Data, lib2Data2]); - }); + }; + verifyTransitiveExports("yes circular import/exports", [lib2Data, lib2Data2]); }); + }); - describe("with noEmitOnError", () => { - function change(caption: string, content: string): ts.tscWatch.TscWatchCompileChange { - return { - caption, - change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, content), - // build project - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }; - } - const noChange: ts.tscWatch.TscWatchCompileChange = { - caption: "No change", - change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`)!), + describe("with noEmitOnError", () => { + function change(caption: string, content: string): ts.tscWatch.TscWatchCompileChange { + return { + caption, + change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, content), // build project - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun }; - verifyEmitAndErrorUpdates({ - subScenario: "with noEmitOnError", - currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`, - files: () => ["shared/types/db.ts", "src/main.ts", "src/other.ts"] - .map(f => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), - lib: () => ({ path: ts.tscWatch.libFile.path, content: ts.libContent }), - configFile: () => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", "tsconfig.json"), - changes: [ - noChange, - change("Fix Syntax error", `import { A } from "../shared/types/db"; + } + const noChange: ts.tscWatch.TscWatchCompileChange = { + caption: "No change", + change: sys => sys.writeFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`)!), + // build project + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }; + verifyEmitAndErrorUpdates({ + subScenario: "with noEmitOnError", + currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`, + files: () => ["shared/types/db.ts", "src/main.ts", "src/other.ts"] + .map(f => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), + lib: () => ({ path: ts.tscWatch.libFile.path, content: ts.libContent }), + configFile: () => ts.TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", "tsconfig.json"), + changes: [ + noChange, + change("Fix Syntax error", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`), - change("Semantic Error", `import { A } from "../shared/types/db"; + change("Semantic Error", `import { A } from "../shared/types/db"; const a: string = 10;`), - noChange, - change("Fix Semantic Error", `import { A } from "../shared/types/db"; + noChange, + change("Fix Semantic Error", `import { A } from "../shared/types/db"; const a: string = "hello";`), - noChange, - ], - baselineIncremental: true - }); + noChange, + ], + baselineIncremental: true }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts index 69ddd03e47db8..06767652ef91b 100644 --- a/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts +++ b/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts @@ -1,97 +1,97 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: forceConsistentCasingInFileNames", () => { - const loggerFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/logger.ts`, - content: `export class logger { }` - }; - const anotherFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/another.ts`, - content: `import { logger } from "./logger"; new logger();` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; - - function verifyConsistentFileNames({ subScenario, changes }: { - subScenario: string; - changes: ts.tscWatch.TscWatchCompileChange[]; - }) { - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario, - commandLineArgs: ["--w", "--p", tsconfig.path], - sys: () => ts.tscWatch.createWatchedSystem([loggerFile, anotherFile, tsconfig, ts.tscWatch.libFile, tsconfig]), - changes - }); - } +describe("unittests:: tsc-watch:: forceConsistentCasingInFileNames", () => { + const loggerFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/logger.ts`, + content: `export class logger { }` + }; + const anotherFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/another.ts`, + content: `import { logger } from "./logger"; new logger();` + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; - verifyConsistentFileNames({ - subScenario: "when changing module name with different casing", - changes: [ - { - caption: "Change module name from logger to Logger", - change: sys => sys.writeFile(anotherFile.path, anotherFile.content.replace("./logger", "./Logger")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] + function verifyConsistentFileNames({ subScenario, changes }: { + subScenario: string; + changes: ts.tscWatch.TscWatchCompileChange[]; + }) { + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario, + commandLineArgs: ["--w", "--p", tsconfig.path], + sys: () => ts.tscWatch.createWatchedSystem([loggerFile, anotherFile, tsconfig, ts.tscWatch.libFile, tsconfig]), + changes }); + } - verifyConsistentFileNames({ - subScenario: "when renaming file with different casing", - changes: [ - { - caption: "Change name of file from logger to Logger", - change: sys => sys.renameFile(loggerFile.path, `${ts.tscWatch.projectRoot}/Logger.ts`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + verifyConsistentFileNames({ + subScenario: "when changing module name with different casing", + changes: [ + { + caption: "Change module name from logger to Logger", + change: sys => sys.writeFile(anotherFile.path, anotherFile.content.replace("./logger", "./Logger")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario: "when relative information file location changes", - commandLineArgs: ["--w", "--p", ".", "--explainFiles"], - sys: () => { - const moduleA: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/moduleA.ts`, - content: `import a = require("./ModuleC")` - }; - const moduleB: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/moduleB.ts`, - content: `import a = require("./moduleC")` - }; - const moduleC: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/moduleC.ts`, - content: `export const x = 10;` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) - }; - return ts.tscWatch.createWatchedSystem([moduleA, moduleB, moduleC, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "Prepend a line to moduleA", - change: sys => sys.prependFile(`${ts.tscWatch.projectRoot}/moduleA.ts`, `// some comment + verifyConsistentFileNames({ + subScenario: "when renaming file with different casing", + changes: [ + { + caption: "Change name of file from logger to Logger", + change: sys => sys.renameFile(loggerFile.path, `${ts.tscWatch.projectRoot}/Logger.ts`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario: "when relative information file location changes", + commandLineArgs: ["--w", "--p", ".", "--explainFiles"], + sys: () => { + const moduleA: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/moduleA.ts`, + content: `import a = require("./ModuleC")` + }; + const moduleB: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/moduleB.ts`, + content: `import a = require("./moduleC")` + }; + const moduleC: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/moduleC.ts`, + content: `export const x = 10;` + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) + }; + return ts.tscWatch.createWatchedSystem([moduleA, moduleB, moduleC, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Prepend a line to moduleA", + change: sys => sys.prependFile(`${ts.tscWatch.projectRoot}/moduleA.ts`, `// some comment `), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], + }); - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario: "jsxImportSource option changed", - commandLineArgs: ["--w", "--p", ".", "--explainFiles"], - sys: () => ts.tscWatch.createWatchedSystem([ - ts.tscWatch.libFile, - { - path: `${ts.tscWatch.projectRoot}/node_modules/react/Jsx-runtime/index.d.ts`, - content: `export namespace JSX { + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario: "jsxImportSource option changed", + commandLineArgs: ["--w", "--p", ".", "--explainFiles"], + sys: () => ts.tscWatch.createWatchedSystem([ + ts.tscWatch.libFile, + { + path: `${ts.tscWatch.projectRoot}/node_modules/react/Jsx-runtime/index.d.ts`, + content: `export namespace JSX { interface Element {} interface IntrinsicElements { div: { @@ -103,167 +103,167 @@ export function jsx(...args: any[]): void; export function jsxs(...args: any[]): void; export const Fragment: unique symbol; `, - }, - { - path: `${ts.tscWatch.projectRoot}/node_modules/react/package.json`, - content: JSON.stringify({ name: "react", version: "0.0.1" }) - }, - { - path: `${ts.tscWatch.projectRoot}/index.tsx`, - content: `export const App = () =>
;` - }, - { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { jsx: "react-jsx", jsxImportSource: "react", forceConsistentCasingInFileNames: true }, - files: ["node_modules/react/jsx-Runtime/index.d.ts", "index.tsx"] // NB: casing does not match disk - }) - } - ], { currentDirectory: ts.tscWatch.projectRoot }), - changes: ts.emptyArray, - }); + }, + { + path: `${ts.tscWatch.projectRoot}/node_modules/react/package.json`, + content: JSON.stringify({ name: "react", version: "0.0.1" }) + }, + { + path: `${ts.tscWatch.projectRoot}/index.tsx`, + content: `export const App = () =>
;` + }, + { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { jsx: "react-jsx", jsxImportSource: "react", forceConsistentCasingInFileNames: true }, + files: ["node_modules/react/jsx-Runtime/index.d.ts", "index.tsx"] // NB: casing does not match disk + }) + } + ], { currentDirectory: ts.tscWatch.projectRoot }), + changes: ts.emptyArray, + }); - function verifyWindowsStyleRoot(subScenario: string, windowsStyleRoot: string, projectRootRelative: string) { - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario, - commandLineArgs: ["--w", "--p", `${windowsStyleRoot}/${projectRootRelative}`, "--explainFiles"], - sys: () => { - const moduleA: ts.tscWatch.File = { - path: `${windowsStyleRoot}/${projectRootRelative}/a.ts`, - content: ` + function verifyWindowsStyleRoot(subScenario: string, windowsStyleRoot: string, projectRootRelative: string) { + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario, + commandLineArgs: ["--w", "--p", `${windowsStyleRoot}/${projectRootRelative}`, "--explainFiles"], + sys: () => { + const moduleA: ts.tscWatch.File = { + path: `${windowsStyleRoot}/${projectRootRelative}/a.ts`, + content: ` export const a = 1; export const b = 2; ` - }; - const moduleB: ts.tscWatch.File = { - path: `${windowsStyleRoot}/${projectRootRelative}/b.ts`, - content: ` + }; + const moduleB: ts.tscWatch.File = { + path: `${windowsStyleRoot}/${projectRootRelative}/b.ts`, + content: ` import { a } from "${windowsStyleRoot.toLocaleUpperCase()}/${projectRootRelative}/a" import { b } from "${windowsStyleRoot.toLocaleLowerCase()}/${projectRootRelative}/a" a;b; ` - }; - const tsconfig: ts.tscWatch.File = { - path: `${windowsStyleRoot}/${projectRootRelative}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) - }; - return ts.tscWatch.createWatchedSystem([moduleA, moduleB, ts.tscWatch.libFile, tsconfig], { windowsStyleRoot, useCaseSensitiveFileNames: false }); - }, - changes: [ - { - caption: "Prepend a line to moduleA", - change: sys => sys.prependFile(`${windowsStyleRoot}/${projectRootRelative}/a.ts`, `// some comment + }; + const tsconfig: ts.tscWatch.File = { + path: `${windowsStyleRoot}/${projectRootRelative}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) + }; + return ts.tscWatch.createWatchedSystem([moduleA, moduleB, ts.tscWatch.libFile, tsconfig], { windowsStyleRoot, useCaseSensitiveFileNames: false }); + }, + changes: [ + { + caption: "Prepend a line to moduleA", + change: sys => sys.prependFile(`${windowsStyleRoot}/${projectRootRelative}/a.ts`, `// some comment `), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); - } + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], + }); + } - verifyWindowsStyleRoot("when Windows-style drive root is lowercase", "c:/", "project"); - verifyWindowsStyleRoot("when Windows-style drive root is uppercase", "C:/", "project"); + verifyWindowsStyleRoot("when Windows-style drive root is lowercase", "c:/", "project"); + verifyWindowsStyleRoot("when Windows-style drive root is uppercase", "C:/", "project"); - function verifyFileSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario, - commandLineArgs: ["--w", "--p", ".", "--explainFiles"], - sys: () => { - const moduleA: ts.tscWatch.File = { + function verifyFileSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario, + commandLineArgs: ["--w", "--p", ".", "--explainFiles"], + sys: () => { + const moduleA: ts.tscWatch.File = { - path: diskPath, - content: ` + path: diskPath, + content: ` export const a = 1; export const b = 2; ` - }; - const symlinkA: ts.tscWatch.SymLink = { - path: `${ts.tscWatch.projectRoot}/link.ts`, - symLink: targetPath, - }; - const moduleB: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: ` + }; + const symlinkA: ts.tscWatch.SymLink = { + path: `${ts.tscWatch.projectRoot}/link.ts`, + symLink: targetPath, + }; + const moduleB: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: ` import { a } from "${importedPath}"; import { b } from "./link"; a;b; ` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) - }; - return ts.tscWatch.createWatchedSystem([moduleA, symlinkA, moduleB, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "Prepend a line to moduleA", - change: sys => sys.prependFile(diskPath, `// some comment + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) + }; + return ts.tscWatch.createWatchedSystem([moduleA, symlinkA, moduleB, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Prepend a line to moduleA", + change: sys => sys.prependFile(diskPath, `// some comment `), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); - } + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], + }); + } - verifyFileSymlink("when both file symlink target and import match disk", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/XY.ts`, `./XY`); - verifyFileSymlink("when file symlink target matches disk but import does not", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./XY`); - verifyFileSymlink("when import matches disk but file symlink target does not", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/XY.ts`, `./Xy`); - verifyFileSymlink("when import and file symlink target agree but do not match disk", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./Xy`); - verifyFileSymlink("when import, file symlink target, and disk are all different", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./yX`); + verifyFileSymlink("when both file symlink target and import match disk", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/XY.ts`, `./XY`); + verifyFileSymlink("when file symlink target matches disk but import does not", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./XY`); + verifyFileSymlink("when import matches disk but file symlink target does not", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/XY.ts`, `./Xy`); + verifyFileSymlink("when import and file symlink target agree but do not match disk", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./Xy`); + verifyFileSymlink("when import, file symlink target, and disk are all different", `${ts.tscWatch.projectRoot}/XY.ts`, `${ts.tscWatch.projectRoot}/Xy.ts`, `./yX`); - function verifyDirSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { - ts.tscWatch.verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario, - commandLineArgs: ["--w", "--p", ".", "--explainFiles"], - sys: () => { - const moduleA: ts.tscWatch.File = { + function verifyDirSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { + ts.tscWatch.verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario, + commandLineArgs: ["--w", "--p", ".", "--explainFiles"], + sys: () => { + const moduleA: ts.tscWatch.File = { - path: `${diskPath}/a.ts`, - content: ` + path: `${diskPath}/a.ts`, + content: ` export const a = 1; export const b = 2; ` - }; - const symlinkA: ts.tscWatch.SymLink = { - path: `${ts.tscWatch.projectRoot}/link`, - symLink: targetPath, - }; - const moduleB: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: ` + }; + const symlinkA: ts.tscWatch.SymLink = { + path: `${ts.tscWatch.projectRoot}/link`, + symLink: targetPath, + }; + const moduleB: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: ` import { a } from "${importedPath}/a"; import { b } from "./link/a"; a;b; ` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - // Use outFile because otherwise the real and linked files will have the same output path - content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true, outFile: "out.js", module: "system" } }) - }; - return ts.tscWatch.createWatchedSystem([moduleA, symlinkA, moduleB, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "Prepend a line to moduleA", - change: sys => sys.prependFile(`${diskPath}/a.ts`, `// some comment + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + // Use outFile because otherwise the real and linked files will have the same output path + content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true, outFile: "out.js", module: "system" } }) + }; + return ts.tscWatch.createWatchedSystem([moduleA, symlinkA, moduleB, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Prepend a line to moduleA", + change: sys => sys.prependFile(`${diskPath}/a.ts`, `// some comment `), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); - } + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], + }); + } - verifyDirSymlink("when both directory symlink target and import match disk", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/XY`, `./XY`); - verifyDirSymlink("when directory symlink target matches disk but import does not", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./XY`); - verifyDirSymlink("when import matches disk but directory symlink target does not", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/XY`, `./Xy`); - verifyDirSymlink("when import and directory symlink target agree but do not match disk", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./Xy`); - verifyDirSymlink("when import, directory symlink target, and disk are all different", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./yX`); - }); + verifyDirSymlink("when both directory symlink target and import match disk", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/XY`, `./XY`); + verifyDirSymlink("when directory symlink target matches disk but import does not", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./XY`); + verifyDirSymlink("when import matches disk but directory symlink target does not", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/XY`, `./Xy`); + verifyDirSymlink("when import and directory symlink target agree but do not match disk", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./Xy`); + verifyDirSymlink("when import, directory symlink target, and disk are all different", `${ts.tscWatch.projectRoot}/XY`, `${ts.tscWatch.projectRoot}/Xy`, `./yX`); +}); } diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index d90f0dbf450ab..0ff91da4397eb 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -1,418 +1,418 @@ namespace ts.tscWatch { - export const projects = `/user/username/projects`; - export const projectRoot = `${projects}/myproject`; - export import WatchedSystem = ts.TestFSWithWatch.TestServerHost; - export type File = ts.TestFSWithWatch.File; - export type SymLink = ts.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: ts.Program, expectedFiles: readonly string[]) { - checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); - } - - export function getDiagnosticMessageChain(message: ts.DiagnosticMessage, args?: (string | number)[], next?: ts.DiagnosticMessageChain[]): ts.DiagnosticMessageChain { - let text = ts.getLocaleSpecificMessage(message); - if (args?.length) { - text = ts.formatStringFromArgs(text, args); - } - return { - messageText: text, - category: message.category, - code: message.code, - next - }; - } +export const projects = `/user/username/projects`; +export const projectRoot = `${projects}/myproject`; +export import WatchedSystem = ts.TestFSWithWatch.TestServerHost; +export type File = ts.TestFSWithWatch.File; +export type SymLink = ts.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: ts.Program, expectedFiles: readonly string[]) { + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); +} - function isDiagnosticMessageChain(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain): message is ts.DiagnosticMessageChain { - return !!(message as ts.DiagnosticMessageChain).messageText; +export function getDiagnosticMessageChain(message: ts.DiagnosticMessage, args?: (string | number)[], next?: ts.DiagnosticMessageChain[]): ts.DiagnosticMessageChain { + let text = ts.getLocaleSpecificMessage(message); + if (args?.length) { + text = ts.formatStringFromArgs(text, args); } + return { + messageText: text, + category: message.category, + code: message.code, + next + }; +} - export function getDiagnosticOfFileFrom(file: ts.SourceFile | undefined, start: number | undefined, length: number | undefined, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { - return { - file, - start, - length, - - messageText: isDiagnosticMessageChain(message) ? - message : - getDiagnosticMessageChain(message, args).messageText, - category: message.category, - code: message.code, - }; - } +function isDiagnosticMessageChain(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain): message is ts.DiagnosticMessageChain { + return !!(message as ts.DiagnosticMessageChain).messageText; +} - export function getDiagnosticWithoutFile(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { - return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); - } +export function getDiagnosticOfFileFrom(file: ts.SourceFile | undefined, start: number | undefined, length: number | undefined, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { + return { + file, + start, + length, + + messageText: isDiagnosticMessageChain(message) ? + message : + getDiagnosticMessageChain(message, args).messageText, + category: message.category, + code: message.code, + }; +} - export function getDiagnosticOfFile(file: ts.SourceFile, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { - return getDiagnosticOfFileFrom(file, start, length, message, ...args); - } +export function getDiagnosticWithoutFile(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { + return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); +} - export function getDiagnosticOfFileFromProgram(program: ts.Program, filePath: string, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { - return getDiagnosticOfFileFrom(program.getSourceFileByPath(ts.toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), start, length, message, ...args); - } +export function getDiagnosticOfFile(file: ts.SourceFile, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { + return getDiagnosticOfFileFrom(file, start, length, message, ...args); +} - export function getUnknownCompilerOption(program: ts.Program, configFile: File, option: string) { - const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0, option); - } +export function getDiagnosticOfFileFromProgram(program: ts.Program, filePath: string, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic { + return getDiagnosticOfFileFrom(program.getSourceFileByPath(ts.toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), start, length, message, ...args); +} - export function getUnknownDidYouMeanCompilerOption(program: ts.Program, configFile: File, option: string, didYouMean: string) { - const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); - } +export function getUnknownCompilerOption(program: ts.Program, configFile: File, option: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0, option); +} - export function getDiagnosticModuleNotFoundOfFile(program: ts.Program, file: File, moduleName: string) { - const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName); - } +export function getUnknownDidYouMeanCompilerOption(program: ts.Program, configFile: File, option: string, didYouMean: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); +} - export function runQueuedTimeoutCallbacks(sys: WatchedSystem) { - sys.runQueuedTimeoutCallbacks(); - } +export function getDiagnosticModuleNotFoundOfFile(program: ts.Program, file: File, moduleName: string) { + const quotedModuleName = `"${moduleName}"`; + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName); +} - export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); - } +export function runQueuedTimeoutCallbacks(sys: WatchedSystem) { + sys.runQueuedTimeoutCallbacks(); +} - export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); - sys.checkTimeoutQueueLength(0); - } +export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); +} - export type WatchOrSolution = void | ts.SolutionBuilder | ts.WatchOfConfigFile | ts.WatchOfFilesAndCompilerOptions; - export interface TscWatchCompileChange { - caption: string; - change: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void; - timeouts: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, programs: readonly ts.CommandLineProgram[], watchOrSolution: WatchOrSolution) => void; - } - export interface TscWatchCheckOptions { - baselineSourceMap?: boolean; - baselineDependencies?: boolean; - } - export interface TscWatchCompileBase extends TscWatchCheckOptions { - scenario: string; - subScenario: string; - commandLineArgs: readonly string[]; - changes: readonly TscWatchCompileChange[]; - } - export interface TscWatchCompile extends TscWatchCompileBase { - sys: () => WatchedSystem; - } +export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); + sys.checkTimeoutQueueLength(0); +} - export const noopChange: TscWatchCompileChange = { - caption: "No change", - change: ts.noop, - timeouts: sys => sys.checkTimeoutQueueLength(0), - }; +export type WatchOrSolution = void | ts.SolutionBuilder | ts.WatchOfConfigFile | ts.WatchOfFilesAndCompilerOptions; +export interface TscWatchCompileChange { + caption: string; + change: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void; + timeouts: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, programs: readonly ts.CommandLineProgram[], watchOrSolution: WatchOrSolution) => void; +} +export interface TscWatchCheckOptions { + baselineSourceMap?: boolean; + baselineDependencies?: boolean; +} +export interface TscWatchCompileBase extends TscWatchCheckOptions { + scenario: string; + subScenario: string; + commandLineArgs: readonly string[]; + changes: readonly TscWatchCompileChange[]; +} +export interface TscWatchCompile extends TscWatchCompileBase { + sys: () => WatchedSystem; +} - export type SystemSnap = ReturnType; - function tscWatchCompile(input: TscWatchCompile) { - it("tsc-watch:: Generates files matching the baseline", () => { - const { sys, baseline, oldSnap } = createBaseline(input.sys()); - const { scenario, subScenario, commandLineArgs, changes, baselineSourceMap, baselineDependencies } = input; - if (!isWatch(commandLineArgs)) - sys.exit = exitCode => sys.exitCode = exitCode; - const { cb, getPrograms } = ts.commandLineCallbacks(sys); - const watchOrSolution = ts.executeCommandLine(sys, cb, commandLineArgs); - runWatchBaseline({ - scenario, - subScenario, - commandLineArgs, - sys, - baseline, - oldSnap, - getPrograms, - baselineSourceMap, - baselineDependencies, - changes, - watchOrSolution - }); +export const noopChange: TscWatchCompileChange = { + caption: "No change", + change: ts.noop, + timeouts: sys => sys.checkTimeoutQueueLength(0), +}; + +export type SystemSnap = ReturnType; +function tscWatchCompile(input: TscWatchCompile) { + it("tsc-watch:: Generates files matching the baseline", () => { + const { sys, baseline, oldSnap } = createBaseline(input.sys()); + const { scenario, subScenario, commandLineArgs, changes, baselineSourceMap, baselineDependencies } = input; + if (!isWatch(commandLineArgs)) + sys.exit = exitCode => sys.exitCode = exitCode; + const { cb, getPrograms } = ts.commandLineCallbacks(sys); + const watchOrSolution = ts.executeCommandLine(sys, cb, commandLineArgs); + runWatchBaseline({ + scenario, + subScenario, + commandLineArgs, + sys, + baseline, + oldSnap, + getPrograms, + baselineSourceMap, + baselineDependencies, + changes, + watchOrSolution }); - } + }); +} - export interface BaselineBase { - baseline: string[]; - sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles; - oldSnap: SystemSnap; - } +export interface BaselineBase { + baseline: string[]; + sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles; + oldSnap: SystemSnap; +} - export interface Baseline extends BaselineBase, ts.CommandLineCallbacks { - } +export interface Baseline extends BaselineBase, ts.CommandLineCallbacks { +} - export function createBaseline(system: WatchedSystem, modifySystem?: (sys: WatchedSystem, originalRead: WatchedSystem["readFile"]) => void): Baseline { - const originalRead = system.readFile; - const initialSys = fakes.patchHostForBuildInfoReadWrite(system); - modifySystem?.(initialSys, originalRead); - const sys = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(initialSys); - const baseline: string[] = []; - baseline.push("Input::"); - sys.diff(baseline); - const { cb, getPrograms } = ts.commandLineCallbacks(sys); - return { sys, baseline, oldSnap: sys.snap(), cb, getPrograms }; - } +export function createBaseline(system: WatchedSystem, modifySystem?: (sys: WatchedSystem, originalRead: WatchedSystem["readFile"]) => void): Baseline { + const originalRead = system.readFile; + const initialSys = fakes.patchHostForBuildInfoReadWrite(system); + modifySystem?.(initialSys, originalRead); + const sys = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(initialSys); + const baseline: string[] = []; + baseline.push("Input::"); + sys.diff(baseline); + const { cb, getPrograms } = ts.commandLineCallbacks(sys); + return { sys, baseline, oldSnap: sys.snap(), cb, getPrograms }; +} - export function createSolutionBuilderWithWatchHostForBaseline(sys: WatchedSystem, cb: ts.ExecuteCommandLineCallbacks) { - const host = ts.createSolutionBuilderWithWatchHost(sys, - /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true), ts.createWatchStatusReporter(sys, /*pretty*/ true)); - host.afterProgramEmitAndDiagnostics = cb; - host.afterEmitBundle = cb; - return host; - } +export function createSolutionBuilderWithWatchHostForBaseline(sys: WatchedSystem, cb: ts.ExecuteCommandLineCallbacks) { + const host = ts.createSolutionBuilderWithWatchHost(sys, + /*createProgram*/ undefined, ts.createDiagnosticReporter(sys, /*pretty*/ true), ts.createBuilderStatusReporter(sys, /*pretty*/ true), ts.createWatchStatusReporter(sys, /*pretty*/ true)); + host.afterProgramEmitAndDiagnostics = cb; + host.afterEmitBundle = cb; + return host; +} - interface CreateWatchCompilerHostOfConfigFileForBaseline extends ts.CreateWatchCompilerHostOfConfigFileInput { - system: WatchedSystem; - cb: ts.ExecuteCommandLineCallbacks; - } +interface CreateWatchCompilerHostOfConfigFileForBaseline extends ts.CreateWatchCompilerHostOfConfigFileInput { + system: WatchedSystem; + cb: ts.ExecuteCommandLineCallbacks; +} - export function createWatchCompilerHostOfConfigFileForBaseline(input: CreateWatchCompilerHostOfConfigFileForBaseline) { - const host = ts.createWatchCompilerHostOfConfigFile({ - ...input, - reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true), - reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true), - }); - updateWatchHostForBaseline(host, input.cb); - return host; - } +export function createWatchCompilerHostOfConfigFileForBaseline(input: CreateWatchCompilerHostOfConfigFileForBaseline) { + const host = ts.createWatchCompilerHostOfConfigFile({ + ...input, + reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true), + reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true), + }); + updateWatchHostForBaseline(host, input.cb); + return host; +} - interface CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline extends ts.CreateWatchCompilerHostOfFilesAndCompilerOptionsInput { - system: WatchedSystem; - cb: ts.ExecuteCommandLineCallbacks; - } - export function createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline(input: CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline) { - const host = ts.createWatchCompilerHostOfFilesAndCompilerOptions({ - ...input, - reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true), - reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true), - }); - updateWatchHostForBaseline(host, input.cb); - return host; - } +interface CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline extends ts.CreateWatchCompilerHostOfFilesAndCompilerOptionsInput { + system: WatchedSystem; + cb: ts.ExecuteCommandLineCallbacks; +} +export function createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline(input: CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline) { + const host = ts.createWatchCompilerHostOfFilesAndCompilerOptions({ + ...input, + reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true), + reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true), + }); + updateWatchHostForBaseline(host, input.cb); + return host; +} - function updateWatchHostForBaseline(host: ts.WatchCompilerHost, cb: ts.ExecuteCommandLineCallbacks) { - const emitFilesAndReportErrors = host.afterProgramCreate!; - host.afterProgramCreate = builderProgram => { - emitFilesAndReportErrors.call(host, builderProgram); - cb(builderProgram); - }; - return host; - } +function updateWatchHostForBaseline(host: ts.WatchCompilerHost, cb: ts.ExecuteCommandLineCallbacks) { + const emitFilesAndReportErrors = host.afterProgramCreate!; + host.afterProgramCreate = builderProgram => { + emitFilesAndReportErrors.call(host, builderProgram); + cb(builderProgram); + }; + return host; +} - export function applyChange(sys: BaselineBase["sys"], baseline: BaselineBase["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) { - const oldSnap = sys.snap(); - baseline.push(`Change::${caption ? " " + caption : ""}`, ""); - change(sys); - baseline.push("Input::"); - sys.diff(baseline, oldSnap); - return sys.snap(); - } +export function applyChange(sys: BaselineBase["sys"], baseline: BaselineBase["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) { + const oldSnap = sys.snap(); + baseline.push(`Change::${caption ? " " + caption : ""}`, ""); + change(sys); + baseline.push("Input::"); + sys.diff(baseline, oldSnap); + return sys.snap(); +} - export interface RunWatchBaseline extends BaselineBase, TscWatchCompileBase { - sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles; - getPrograms: () => readonly ts.CommandLineProgram[]; - watchOrSolution: WatchOrSolution; - } - export function runWatchBaseline({ scenario, subScenario, commandLineArgs, getPrograms, sys, baseline, oldSnap, baselineSourceMap, baselineDependencies, changes, watchOrSolution }: RunWatchBaseline) { - baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); - let programs = watchBaseline({ +export interface RunWatchBaseline extends BaselineBase, TscWatchCompileBase { + sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles; + getPrograms: () => readonly ts.CommandLineProgram[]; + watchOrSolution: WatchOrSolution; +} +export function runWatchBaseline({ scenario, subScenario, commandLineArgs, getPrograms, sys, baseline, oldSnap, baselineSourceMap, baselineDependencies, changes, watchOrSolution }: RunWatchBaseline) { + baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); + let programs = watchBaseline({ + baseline, + getPrograms, + oldPrograms: ts.emptyArray, + sys, + oldSnap, + baselineSourceMap, + baselineDependencies, + }); + + for (const { caption, change, timeouts } of changes) { + oldSnap = applyChange(sys, baseline, change, caption); + timeouts(sys, programs, watchOrSolution); + programs = watchBaseline({ baseline, getPrograms, - oldPrograms: ts.emptyArray, + oldPrograms: programs, sys, oldSnap, baselineSourceMap, baselineDependencies, }); - - for (const { caption, change, timeouts } of changes) { - oldSnap = applyChange(sys, baseline, change, caption); - timeouts(sys, programs, watchOrSolution); - programs = watchBaseline({ - baseline, - getPrograms, - oldPrograms: programs, - sys, - oldSnap, - baselineSourceMap, - baselineDependencies, - }); - } - Harness.Baseline.runBaseline(`${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}${isWatch(commandLineArgs) ? "Watch" : ""}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); } + Harness.Baseline.runBaseline(`${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}${isWatch(commandLineArgs) ? "Watch" : ""}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); +} - function isWatch(commandLineArgs: readonly string[]) { - return ts.forEach(commandLineArgs, arg => { - if (arg.charCodeAt(0) !== ts.CharacterCodes.minus) - return false; - const option = arg.slice(arg.charCodeAt(1) === ts.CharacterCodes.minus ? 2 : 1).toLowerCase(); - return option === "watch" || option === "w"; - }); - } +function isWatch(commandLineArgs: readonly string[]) { + return ts.forEach(commandLineArgs, arg => { + if (arg.charCodeAt(0) !== ts.CharacterCodes.minus) + return false; + const option = arg.slice(arg.charCodeAt(1) === ts.CharacterCodes.minus ? 2 : 1).toLowerCase(); + return option === "watch" || option === "w"; + }); +} - export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions { - oldPrograms: readonly (ts.CommandLineProgram | undefined)[]; - getPrograms: () => readonly ts.CommandLineProgram[]; - } - export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) { - if (baselineSourceMap) - ts.generateSourceMapBaselineFiles(sys); - sys.serializeOutput(baseline); - const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies); - sys.serializeWatches(baseline); - baseline.push(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}`, ""); - sys.diff(baseline, oldSnap); - sys.writtenFiles.forEach((value, key) => { - assert.equal(value, 1, `Expected to write file ${key} only once`); - }); - sys.writtenFiles.clear(); - return programs; +export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions { + oldPrograms: readonly (ts.CommandLineProgram | undefined)[]; + getPrograms: () => readonly ts.CommandLineProgram[]; +} +export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) { + if (baselineSourceMap) + ts.generateSourceMapBaselineFiles(sys); + sys.serializeOutput(baseline); + const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies); + sys.serializeWatches(baseline); + baseline.push(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}`, ""); + sys.diff(baseline, oldSnap); + sys.writtenFiles.forEach((value, key) => { + assert.equal(value, 1, `Expected to write file ${key} only once`); + }); + sys.writtenFiles.clear(); + return programs; +} + +export function baselinePrograms(baseline: string[], getPrograms: () => readonly ts.CommandLineProgram[], oldPrograms: readonly (ts.CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) { + const programs = getPrograms(); + for (let i = 0; i < programs.length; i++) { + baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies); } + return programs; +} - export function baselinePrograms(baseline: string[], getPrograms: () => readonly ts.CommandLineProgram[], oldPrograms: readonly (ts.CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) { - const programs = getPrograms(); - for (let i = 0; i < programs.length; i++) { - baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies); +function baselineProgram(baseline: string[], [program, builderProgram]: ts.CommandLineProgram, oldProgram: ts.CommandLineProgram | undefined, baselineDependencies: boolean | undefined) { + if (program !== oldProgram?.[0]) { + const options = program.getCompilerOptions(); + baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); + baseline.push(`Program options: ${JSON.stringify(options)}`); + baseline.push(`Program structureReused: ${(ts as any).StructureIsReused[program.structureIsReused]}`); + baseline.push("Program files::"); + for (const file of program.getSourceFiles()) { + baseline.push(file.fileName); } - return programs; } + else { + baseline.push(`Program: Same as old program`); + } + baseline.push(""); - function baselineProgram(baseline: string[], [program, builderProgram]: ts.CommandLineProgram, oldProgram: ts.CommandLineProgram | undefined, baselineDependencies: boolean | undefined) { - if (program !== oldProgram?.[0]) { - const options = program.getCompilerOptions(); - baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); - baseline.push(`Program options: ${JSON.stringify(options)}`); - baseline.push(`Program structureReused: ${(ts as any).StructureIsReused[program.structureIsReused]}`); - baseline.push("Program files::"); + if (!builderProgram) + return; + if (builderProgram !== oldProgram?.[1]) { + const state = builderProgram.getState(); + const internalState = state as unknown as ts.BuilderProgramState; + if (state.semanticDiagnosticsPerFile?.size) { + baseline.push("Semantic diagnostics in builder refreshed for::"); for (const file of program.getSourceFiles()) { - baseline.push(file.fileName); + if (!state.semanticDiagnosticsFromOldState || !state.semanticDiagnosticsFromOldState.has(file.resolvedPath)) { + baseline.push(file.fileName); + } } } else { - baseline.push(`Program: Same as old program`); + baseline.push("No cached semantic diagnostics in the builder::"); } - baseline.push(""); - - if (!builderProgram) - return; - if (builderProgram !== oldProgram?.[1]) { - const state = builderProgram.getState(); - const internalState = state as unknown as ts.BuilderProgramState; - 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 (internalState) { + baseline.push(""); + if (internalState.hasCalledUpdateShapeSignature?.size) { + baseline.push("Shape signatures in builder refreshed for::"); + internalState.hasCalledUpdateShapeSignature.forEach((path: ts.Path) => { + const info = state.fileInfos.get(path); + if (info?.version === info?.signature || !info?.signature) { + baseline.push(path + " (used version)"); } - } + else if (internalState.filesChangingSignature?.has(path)) { + baseline.push(path + " (computed .d.ts during emit)"); + } + else { + baseline.push(path + " (computed .d.ts)"); + } + }); } else { - baseline.push("No cached semantic diagnostics in the builder::"); + baseline.push("No shapes updated in the builder::"); } - if (internalState) { - baseline.push(""); - if (internalState.hasCalledUpdateShapeSignature?.size) { - baseline.push("Shape signatures in builder refreshed for::"); - internalState.hasCalledUpdateShapeSignature.forEach((path: ts.Path) => { - const info = state.fileInfos.get(path); - if (info?.version === info?.signature || !info?.signature) { - baseline.push(path + " (used version)"); - } - else if (internalState.filesChangingSignature?.has(path)) { - baseline.push(path + " (computed .d.ts during emit)"); - } - else { - baseline.push(path + " (computed .d.ts)"); - } - }); - } - else { - baseline.push("No shapes updated in the builder::"); - } - } - baseline.push(""); - if (!baselineDependencies) - return; - baseline.push("Dependencies for::"); - for (const file of builderProgram.getSourceFiles()) { - baseline.push(`${file.fileName}:`); - for (const depenedency of builderProgram.getAllDependencies(file)) { - baseline.push(` ${depenedency}`); - } - } - } - else { - baseline.push(`BuilderProgram: Same as old builder program`); } baseline.push(""); + if (!baselineDependencies) + return; + baseline.push("Dependencies for::"); + for (const file of builderProgram.getSourceFiles()) { + baseline.push(`${file.fileName}:`); + for (const depenedency of builderProgram.getAllDependencies(file)) { + baseline.push(` ${depenedency}`); + } + } } - - export interface VerifyTscWatch extends TscWatchCompile { - baselineIncremental?: boolean; + else { + baseline.push(`BuilderProgram: Same as old builder program`); } - export function verifyTscWatch(input: VerifyTscWatch) { - describe(input.scenario, () => { - describe(input.subScenario, () => { - tscWatchCompile(input); - }); - if (input.baselineIncremental) { - describe(`${input.subScenario} with incremental`, () => { - tscWatchCompile({ - ...input, - subScenario: `${input.subScenario} with incremental`, - commandLineArgs: [...input.commandLineArgs, "--incremental"], - }); - }); - } + baseline.push(""); +} + +export interface VerifyTscWatch extends TscWatchCompile { + baselineIncremental?: boolean; +} +export function verifyTscWatch(input: VerifyTscWatch) { + describe(input.scenario, () => { + describe(input.subScenario, () => { + tscWatchCompile(input); }); - } + if (input.baselineIncremental) { + describe(`${input.subScenario} with incremental`, () => { + tscWatchCompile({ + ...input, + subScenario: `${input.subScenario} with incremental`, + commandLineArgs: [...input.commandLineArgs, "--incremental"], + }); + }); + } + }); +} - export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { - const content = ts.Debug.checkDefined(sys.readFile(file)); - sys.writeFile(file, content.replace(searchValue, replaceValue)); - } +export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { + const content = ts.Debug.checkDefined(sys.readFile(file)); + sys.writeFile(file, content.replace(searchValue, replaceValue)); +} - export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], originalRead?: WatchedSystem["readFile"]) { - const host = ts.createSolutionBuilderHostForBaseline(system, /*versionToWrite*/ undefined, originalRead); - return ts.createSolutionBuilder(host, rootNames, {}); - } +export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], originalRead?: WatchedSystem["readFile"]) { + const host = ts.createSolutionBuilderHostForBaseline(system, /*versionToWrite*/ undefined, originalRead); + return ts.createSolutionBuilder(host, rootNames, {}); +} - export function ensureErrorFreeBuild(host: WatchedSystem, rootNames: readonly string[]) { - // ts build should succeed - solutionBuildWithBaseline(host, rootNames); - assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); - } +export function ensureErrorFreeBuild(host: WatchedSystem, rootNames: readonly string[]) { + // ts build should succeed + solutionBuildWithBaseline(host, rootNames); + assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); +} - export function solutionBuildWithBaseline(sys: WatchedSystem, solutionRoots: readonly string[], originalRead?: WatchedSystem["readFile"]) { - const originalReadFile = sys.readFile; - const originalWrite = sys.write; - const originalWriteFile = sys.writeFile; - const solutionBuilder = createSolutionBuilder(ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(sys)), solutionRoots, originalRead); - solutionBuilder.build(); - sys.readFile = originalReadFile; - sys.write = originalWrite; - sys.writeFile = originalWriteFile; - return sys; - } +export function solutionBuildWithBaseline(sys: WatchedSystem, solutionRoots: readonly string[], originalRead?: WatchedSystem["readFile"]) { + const originalReadFile = sys.readFile; + const originalWrite = sys.write; + const originalWriteFile = sys.writeFile; + const solutionBuilder = createSolutionBuilder(ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(fakes.patchHostForBuildInfoReadWrite(sys)), solutionRoots, originalRead); + solutionBuilder.build(); + sys.readFile = originalReadFile; + sys.write = originalWrite; + sys.writeFile = originalWriteFile; + return sys; +} - export function createSystemWithSolutionBuild(solutionRoots: readonly string[], files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], params?: ts.TestFSWithWatch.TestServerHostCreationParameters) { - return solutionBuildWithBaseline(createWatchedSystem(files, params), solutionRoots); - } +export function createSystemWithSolutionBuild(solutionRoots: readonly string[], files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], params?: ts.TestFSWithWatch.TestServerHostCreationParameters) { + return solutionBuildWithBaseline(createWatchedSystem(files, params), solutionRoots); +} } diff --git a/src/testRunner/unittests/tscWatch/incremental.ts b/src/testRunner/unittests/tscWatch/incremental.ts index 2caad5ec8737e..fe2eefa6b89ac 100644 --- a/src/testRunner/unittests/tscWatch/incremental.ts +++ b/src/testRunner/unittests/tscWatch/incremental.ts @@ -1,283 +1,283 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: emit file --incremental", () => { - const project = "/users/username/projects/project"; +describe("unittests:: tsc-watch:: emit file --incremental", () => { + const project = "/users/username/projects/project"; - const configFile: ts.tscWatch.File = { - path: `${project}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { incremental: true } }) - }; + const configFile: ts.tscWatch.File = { + path: `${project}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { incremental: true } }) + }; - interface VerifyIncrementalWatchEmitInput { - subScenario: string; - files: () => readonly ts.tscWatch.File[]; - optionsToExtend?: readonly string[]; - modifyFs?: (host: ts.tscWatch.WatchedSystem) => void; - } - function verifyIncrementalWatchEmit(input: VerifyIncrementalWatchEmitInput) { - describe(input.subScenario, () => { - it("with tsc --w", () => { - verifyIncrementalWatchEmitWorker(input, /*incremental*/ false); - }); - it("with tsc", () => { - verifyIncrementalWatchEmitWorker(input, /*incremental*/ true); - }); + interface VerifyIncrementalWatchEmitInput { + subScenario: string; + files: () => readonly ts.tscWatch.File[]; + optionsToExtend?: readonly string[]; + modifyFs?: (host: ts.tscWatch.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, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files(), { currentDirectory: project })); + if (incremental) + sys.exit = exitCode => sys.exitCode = exitCode; + const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || ts.emptyArray)]; + baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`); + let oldPrograms: readonly ts.CommandLineProgram[] = ts.emptyArray; + build(oldSnap); - function verifyIncrementalWatchEmitWorker({ subScenario, files, optionsToExtend, modifyFs }: VerifyIncrementalWatchEmitInput, incremental: boolean) { - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files(), { currentDirectory: project })); - if (incremental) - sys.exit = exitCode => sys.exitCode = exitCode; - const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || ts.emptyArray)]; - baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`); - let oldPrograms: readonly ts.CommandLineProgram[] = ts.emptyArray; + if (modifyFs) { + const oldSnap = ts.tscWatch.applyChange(sys, baseline, modifyFs); build(oldSnap); + } - if (modifyFs) { - const oldSnap = ts.tscWatch.applyChange(sys, baseline, modifyFs); - build(oldSnap); - } + Harness.Baseline.runBaseline(`${ts.isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n")); + function build(oldSnap: ts.tscWatch.SystemSnap) { + const closer = ts.executeCommandLine(sys, cb, argsToPass); + oldPrograms = ts.tscWatch.watchBaseline({ + baseline, + getPrograms, + oldPrograms, + sys, + oldSnap + }); + if (closer) + closer.close(); + } + } - Harness.Baseline.runBaseline(`${ts.isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n")); - function build(oldSnap: ts.tscWatch.SystemSnap) { - const closer = ts.executeCommandLine(sys, cb, argsToPass); - oldPrograms = ts.tscWatch.watchBaseline({ - baseline, - getPrograms, - oldPrograms, - sys, - oldSnap + describe("non module compilation", () => { + const file1: ts.tscWatch.File = { + path: `${project}/file1.ts`, + content: "const x = 10;" + }; + const file2: ts.tscWatch.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: () => [ts.tscWatch.libFile, file1, file2, configFile], + optionsToExtend, + subScenario: `own file emit without errors/${subScenario}`, + modifyFs: host => host.writeFile(file2.path, modifiedFile2Content), }); - if (closer) - closer.close(); } - } + verify("without commandline options"); + verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]); + }); - describe("non module compilation", () => { - const file1: ts.tscWatch.File = { - path: `${project}/file1.ts`, - content: "const x = 10;" - }; - const file2: ts.tscWatch.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: () => [ts.tscWatch.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: () => [ts.tscWatch.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: () => [ts.tscWatch.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: () => [ts.tscWatch.libFile, file1, file2, { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } }) + }], + subScenario: "with --out", + }); + }); - verifyIncrementalWatchEmit({ - files: () => [ts.tscWatch.libFile, file1, file2, { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } }) - }], - subScenario: "with --out", - }); + describe("module compilation", () => { + const file1: ts.tscWatch.File = { + path: `${project}/file1.ts`, + content: "export const x = 10;" + }; + const file2: ts.tscWatch.File = { + path: `${project}/file2.ts`, + content: "export const y = 20;" + }; + const config: ts.tscWatch.File = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) + }; + + verifyIncrementalWatchEmit({ + files: () => [ts.tscWatch.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("module compilation", () => { - const file1: ts.tscWatch.File = { - path: `${project}/file1.ts`, - content: "export const x = 10;" - }; - const file2: ts.tscWatch.File = { - path: `${project}/file2.ts`, - content: "export const y = 20;" - }; - const config: ts.tscWatch.File = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) + describe("own file emit with errors", () => { + const fileModified: ts.tscWatch.File = { + path: file2.path, + content: `export const y: string = 20;` }; verifyIncrementalWatchEmit({ - files: () => [ts.tscWatch.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: () => [ts.tscWatch.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: ts.tscWatch.File = { - path: file2.path, - content: `export const y: string = 20;` - }; - - verifyIncrementalWatchEmit({ - files: () => [ts.tscWatch.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 = ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, fileModified, config], { currentDirectory: project }); + const reportDiagnostic = ts.createDiagnosticReporter(system); + const parsedConfig = ts.parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!; + ts.performIncrementalCompilation({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + projectReferences: parsedConfig.projectReferences, + configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig), + reportDiagnostic, + system }); - it("verify that state is read correctly", () => { - const system = ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, fileModified, config], { currentDirectory: project }); - const reportDiagnostic = ts.createDiagnosticReporter(system); - const parsedConfig = ts.parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!; - ts.performIncrementalCompilation({ - rootNames: parsedConfig.fileNames, - options: parsedConfig.options, - projectReferences: parsedConfig.projectReferences, - configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig), - reportDiagnostic, - system - }); - - const command = ts.parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, ts.noop)!; - const builderProgram = ts.createIncrementalProgram({ - rootNames: command.fileNames, - options: command.options, - projectReferences: command.projectReferences, - configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(command), - host: ts.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(ts.tscWatch.libFile.path as ts.Path), { - version: system.createHash(ts.tscWatch.libFile.content), - signature: system.createHash(ts.tscWatch.libFile.content), - affectsGlobalScope: true, - impliedFormat: undefined, - }); - assert.deepEqual(state.fileInfos.get(file1.path as ts.Path), { - version: system.createHash(file1.content), - signature: system.createHash(file1.content), - affectsGlobalScope: undefined, - impliedFormat: undefined, - }); - assert.deepEqual(state.fileInfos.get(file2.path as ts.Path), { - version: system.createHash(fileModified.content), - signature: system.createHash(fileModified.content), - affectsGlobalScope: undefined, - impliedFormat: undefined, - }); + const command = ts.parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, ts.noop)!; + const builderProgram = ts.createIncrementalProgram({ + rootNames: command.fileNames, + options: command.options, + projectReferences: command.projectReferences, + configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(command), + host: ts.createIncrementalCompilerHost(command.options, system) + }); - assert.deepEqual(state.compilerOptions, { - incremental: true, - module: ts.ModuleKind.AMD, - configFilePath: config.path - }); + const state = builderProgram.getState(); + assert.equal(state.changedFilesSet!.size, 0, "changes"); - assert.equal(ts.arrayFrom(state.referencedMap!.keys()).length, 0); - assert.equal(ts.arrayFrom(state.exportedModulesMap!.keys()).length, 0); + assert.equal(state.fileInfos.size, 3, "FileInfo size"); + assert.deepEqual(state.fileInfos.get(ts.tscWatch.libFile.path as ts.Path), { + version: system.createHash(ts.tscWatch.libFile.content), + signature: system.createHash(ts.tscWatch.libFile.content), + affectsGlobalScope: true, + impliedFormat: undefined, + }); + assert.deepEqual(state.fileInfos.get(file1.path as ts.Path), { + version: system.createHash(file1.content), + signature: system.createHash(file1.content), + affectsGlobalScope: undefined, + impliedFormat: undefined, + }); + assert.deepEqual(state.fileInfos.get(file2.path as ts.Path), { + version: system.createHash(fileModified.content), + signature: system.createHash(fileModified.content), + affectsGlobalScope: undefined, + impliedFormat: undefined, + }); - assert.equal(state.semanticDiagnosticsPerFile!.size, 3); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(ts.tscWatch.libFile.path as ts.Path), ts.emptyArray); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path as ts.Path), ts.emptyArray); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path as ts.Path), [{ - file: state.program!.getSourceFileByPath(file2.path as ts.Path)!, - start: 13, - length: 1, - code: ts.Diagnostics.Type_0_is_not_assignable_to_type_1.code, - category: ts.Diagnostics.Type_0_is_not_assignable_to_type_1.category, - messageText: "Type 'number' is not assignable to type 'string'.", - relatedInformation: undefined, - reportsUnnecessary: undefined, - reportsDeprecated: undefined, - source: undefined, - skippedOn: undefined, - }]); + assert.deepEqual(state.compilerOptions, { + incremental: true, + module: ts.ModuleKind.AMD, + configFilePath: config.path }); - }); - verifyIncrementalWatchEmit({ - files: () => [ts.tscWatch.libFile, file1, file2, { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } }) - }], - subScenario: "module compilation/with --out", + assert.equal(ts.arrayFrom(state.referencedMap!.keys()).length, 0); + assert.equal(ts.arrayFrom(state.exportedModulesMap!.keys()).length, 0); + + assert.equal(state.semanticDiagnosticsPerFile!.size, 3); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(ts.tscWatch.libFile.path as ts.Path), ts.emptyArray); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path as ts.Path), ts.emptyArray); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path as ts.Path), [{ + file: state.program!.getSourceFileByPath(file2.path as ts.Path)!, + start: 13, + length: 1, + code: ts.Diagnostics.Type_0_is_not_assignable_to_type_1.code, + category: ts.Diagnostics.Type_0_is_not_assignable_to_type_1.category, + messageText: "Type 'number' is not assignable to type 'string'.", + relatedInformation: undefined, + reportsUnnecessary: undefined, + reportsDeprecated: undefined, + source: undefined, + skippedOn: undefined, + }]); }); }); verifyIncrementalWatchEmit({ - files: () => { - const config: ts.tscWatch.File = { - path: configFile.path, - content: JSON.stringify({ - compilerOptions: { - incremental: true, - target: "es5", - module: "commonjs", - declaration: true, - emitDeclarationOnly: true - } - }) - }; - const aTs: ts.tscWatch.File = { - path: `${project}/a.ts`, - content: `import { B } from "./b"; + files: () => [ts.tscWatch.libFile, file1, file2, { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } }) + }], + subScenario: "module compilation/with --out", + }); + }); + + verifyIncrementalWatchEmit({ + files: () => { + const config: ts.tscWatch.File = { + path: configFile.path, + content: JSON.stringify({ + compilerOptions: { + incremental: true, + target: "es5", + module: "commonjs", + declaration: true, + emitDeclarationOnly: true + } + }) + }; + const aTs: ts.tscWatch.File = { + path: `${project}/a.ts`, + content: `import { B } from "./b"; export interface A { b: B; } ` - }; - const bTs: ts.tscWatch.File = { - path: `${project}/b.ts`, - content: `import { C } from "./c"; + }; + const bTs: ts.tscWatch.File = { + path: `${project}/b.ts`, + content: `import { C } from "./c"; export interface B { b: C; } ` - }; - const cTs: ts.tscWatch.File = { - path: `${project}/c.ts`, - content: `import { A } from "./a"; + }; + const cTs: ts.tscWatch.File = { + path: `${project}/c.ts`, + content: `import { A } from "./a"; export interface C { a: A; } ` - }; - const indexTs: ts.tscWatch.File = { - path: `${project}/index.ts`, - content: `export { A } from "./a"; + }; + const indexTs: ts.tscWatch.File = { + path: `${project}/index.ts`, + content: `export { A } from "./a"; export { B } from "./b"; export { C } from "./c"; ` - }; - return [ts.tscWatch.libFile, aTs, bTs, cTs, indexTs, config]; - }, - subScenario: "incremental with circular references", - modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b"; + }; + return [ts.tscWatch.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; } `) - }); + }); - verifyIncrementalWatchEmit({ - subScenario: "when file with ambient global declaration file is deleted", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/globals.d.ts`, content: `declare namespace Config { const value: string;} ` }, - { path: `${project}/index.ts`, content: `console.log(Config.value);` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, } }) } - ], - modifyFs: host => host.deleteFile(`${project}/globals.d.ts`) - }); + verifyIncrementalWatchEmit({ + subScenario: "when file with ambient global declaration file is deleted", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/globals.d.ts`, content: `declare namespace Config { const value: string;} ` }, + { path: `${project}/index.ts`, content: `console.log(Config.value);` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, } }) } + ], + modifyFs: host => host.deleteFile(`${project}/globals.d.ts`) + }); - describe("with option jsxImportSource", () => { - const jsxImportSourceOptions = { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" }; - const jsxLibraryContent = `export namespace JSX { + describe("with option jsxImportSource", () => { + const jsxImportSourceOptions = { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" }; + const jsxLibraryContent = `export namespace JSX { interface Element {} interface IntrinsicElements { div: { @@ -290,83 +290,83 @@ export function jsxs(...args: any[]): void; export const Fragment: unique symbol; `; - verifyIncrementalWatchEmit({ - subScenario: "jsxImportSource option changed", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, - { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, - { path: `${project}/node_modules/preact/jsx-runtime/index.d.ts`, content: jsxLibraryContent.replace("propA", "propB") }, - { path: `${project}/node_modules/preact/package.json`, content: JSON.stringify({ name: "preact", version: "0.0.1" }) }, - { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } - ], - modifyFs: host => host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { ...jsxImportSourceOptions, jsxImportSource: "preact" } })), - optionsToExtend: ["--explainFiles"] - }); + verifyIncrementalWatchEmit({ + subScenario: "jsxImportSource option changed", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, + { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, + { path: `${project}/node_modules/preact/jsx-runtime/index.d.ts`, content: jsxLibraryContent.replace("propA", "propB") }, + { path: `${project}/node_modules/preact/package.json`, content: JSON.stringify({ name: "preact", version: "0.0.1" }) }, + { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } + ], + modifyFs: host => host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { ...jsxImportSourceOptions, jsxImportSource: "preact" } })), + optionsToExtend: ["--explainFiles"] + }); - verifyIncrementalWatchEmit({ - subScenario: "jsxImportSource backing types added", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } - ], - modifyFs: host => { - host.createDirectory(`${project}/node_modules`); - host.createDirectory(`${project}/node_modules/react`); - host.createDirectory(`${project}/node_modules/react/jsx-runtime`); - host.writeFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`, jsxLibraryContent); - host.writeFile(`${project}/node_modules/react/package.json`, JSON.stringify({ name: "react", version: "0.0.1" })); - } - }); + verifyIncrementalWatchEmit({ + subScenario: "jsxImportSource backing types added", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } + ], + modifyFs: host => { + host.createDirectory(`${project}/node_modules`); + host.createDirectory(`${project}/node_modules/react`); + host.createDirectory(`${project}/node_modules/react/jsx-runtime`); + host.writeFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`, jsxLibraryContent); + host.writeFile(`${project}/node_modules/react/package.json`, JSON.stringify({ name: "react", version: "0.0.1" })); + } + }); - verifyIncrementalWatchEmit({ - subScenario: "jsxImportSource backing types removed", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, - { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, - { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } - ], - modifyFs: host => { - host.deleteFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`); - host.deleteFile(`${project}/node_modules/react/package.json`); - } - }); + verifyIncrementalWatchEmit({ + subScenario: "jsxImportSource backing types removed", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent }, + { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) }, + { path: `${project}/index.tsx`, content: `export const App = () =>
;` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) } + ], + modifyFs: host => { + host.deleteFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`); + host.deleteFile(`${project}/node_modules/react/package.json`); + } + }); - verifyIncrementalWatchEmit({ - subScenario: "importHelpers backing types removed", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/node_modules/tslib/index.d.ts`, content: "export function __assign(...args: any[]): any;" }, - { path: `${project}/node_modules/tslib/package.json`, content: JSON.stringify({ name: "tslib", version: "0.0.1" }) }, - { path: `${project}/index.tsx`, content: `export const x = {...{}};` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: { importHelpers: true } }) } - ], - modifyFs: host => { - host.deleteFile(`${project}/node_modules/tslib/index.d.ts`); - host.deleteFile(`${project}/node_modules/tslib/package.json`); - } - }); + verifyIncrementalWatchEmit({ + subScenario: "importHelpers backing types removed", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/node_modules/tslib/index.d.ts`, content: "export function __assign(...args: any[]): any;" }, + { path: `${project}/node_modules/tslib/package.json`, content: JSON.stringify({ name: "tslib", version: "0.0.1" }) }, + { path: `${project}/index.tsx`, content: `export const x = {...{}};` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: { importHelpers: true } }) } + ], + modifyFs: host => { + host.deleteFile(`${project}/node_modules/tslib/index.d.ts`); + host.deleteFile(`${project}/node_modules/tslib/package.json`); + } }); + }); - describe("editing module augmentation", () => { - verifyIncrementalWatchEmit({ - subScenario: "editing module augmentation", - files: () => [ - { path: ts.tscWatch.libFile.path, content: ts.libContent }, - { path: `${project}/node_modules/classnames/index.d.ts`, content: `export interface Result {} export default function classNames(): Result;` }, - { path: `${project}/src/types/classnames.d.ts`, content: `export {}; declare module "classnames" { interface Result { foo } }` }, - { path: `${project}/src/index.ts`, content: `import classNames from "classnames"; classNames().foo;` }, - { path: configFile.path, content: JSON.stringify({ compilerOptions: { module: "commonjs", incremental: true } }) }, - ], - modifyFs: host => { - // delete 'foo' - host.writeFile(`${project}/src/types/classnames.d.ts`, `export {}; declare module "classnames" { interface Result {} }`); - }, - }); + describe("editing module augmentation", () => { + verifyIncrementalWatchEmit({ + subScenario: "editing module augmentation", + files: () => [ + { path: ts.tscWatch.libFile.path, content: ts.libContent }, + { path: `${project}/node_modules/classnames/index.d.ts`, content: `export interface Result {} export default function classNames(): Result;` }, + { path: `${project}/src/types/classnames.d.ts`, content: `export {}; declare module "classnames" { interface Result { foo } }` }, + { path: `${project}/src/index.ts`, content: `import classNames from "classnames"; classNames().foo;` }, + { path: configFile.path, content: JSON.stringify({ compilerOptions: { module: "commonjs", incremental: true } }) }, + ], + modifyFs: host => { + // delete 'foo' + host.writeFile(`${project}/src/types/classnames.d.ts`, `export {}; declare module "classnames" { interface Result {} }`); + }, }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/nodeNextWatch.ts b/src/testRunner/unittests/tscWatch/nodeNextWatch.ts index 9a119d483f739..df5f8e8e304ce 100644 --- a/src/testRunner/unittests/tscWatch/nodeNextWatch.ts +++ b/src/testRunner/unittests/tscWatch/nodeNextWatch.ts @@ -1,54 +1,54 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: nodeNextWatch:: emit when module emit is specified as nodenext", () => { - ts.tscWatch.verifyTscWatch({ - scenario: "nodenext watch emit", - subScenario: "esm-mode file is edited", - commandLineArgs: ["--w", "--p", "/project/tsconfig.json"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: "/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - strict: true, - target: "es2020", - module: "nodenext", - moduleResolution: "nodenext", - outDir: "../dist" - } - }) - }; - const packageFile: ts.tscWatch.File = { - path: "/project/package.json", - content: JSON.stringify({ - name: "some-proj", - version: "1.0.0", - description: "", - type: "module", - main: "index.js", - }) - }; - const file1: ts.tscWatch.File = { - path: "/project/src/index.ts", - content: Utils.dedent` +describe("unittests:: tsc-watch:: nodeNextWatch:: emit when module emit is specified as nodenext", () => { + ts.tscWatch.verifyTscWatch({ + scenario: "nodenext watch emit", + subScenario: "esm-mode file is edited", + commandLineArgs: ["--w", "--p", "/project/tsconfig.json"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + strict: true, + target: "es2020", + module: "nodenext", + moduleResolution: "nodenext", + outDir: "../dist" + } + }) + }; + const packageFile: ts.tscWatch.File = { + path: "/project/package.json", + content: JSON.stringify({ + name: "some-proj", + version: "1.0.0", + description: "", + type: "module", + main: "index.js", + }) + }; + const file1: ts.tscWatch.File = { + path: "/project/src/index.ts", + content: Utils.dedent` import * as Thing from "thing"; Thing.fn();` - }; - const declFile: ts.tscWatch.File = { - path: "/project/src/deps.d.ts", - content: `declare module "thing";` - }; - return ts.tscWatch.createWatchedSystem([configFile, file1, declFile, packageFile, { ...ts.tscWatch.libFile, path: "/a/lib/lib.es2020.full.d.ts" }]); - }, - changes: [ - { - caption: "Modify typescript file", - change: sys => sys.modifyFile("/project/src/index.ts", Utils.dedent ` + }; + const declFile: ts.tscWatch.File = { + path: "/project/src/deps.d.ts", + content: `declare module "thing";` + }; + return ts.tscWatch.createWatchedSystem([configFile, file1, declFile, packageFile, { ...ts.tscWatch.libFile, path: "/a/lib/lib.es2020.full.d.ts" }]); + }, + changes: [ + { + caption: "Modify typescript file", + change: sys => sys.modifyFile("/project/src/index.ts", Utils.dedent ` import * as Thing from "thing"; Thing.fn();`, {}), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ], - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ], }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index a8e9d65317169..b5a50408ae044 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -1,1090 +1,1090 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: program updates", () => { - const scenario = "programUpdates"; - const configFilePath = "/a/b/tsconfig.json"; - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{}` - }; - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "create watch without config file", - commandLineArgs: ["-w", "/a/b/c/app.ts"], - sys: () => { - const appFile: ts.tscWatch.File = { - path: "/a/b/c/app.ts", - content: ` +describe("unittests:: tsc-watch:: program updates", () => { + const scenario = "programUpdates"; + const configFilePath = "/a/b/tsconfig.json"; + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{}` + }; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "create watch without config file", + commandLineArgs: ["-w", "/a/b/c/app.ts"], + sys: () => { + const appFile: ts.tscWatch.File = { + path: "/a/b/c/app.ts", + content: ` import {f} from "./module" console.log(f) ` - }; + }; - const moduleFile: ts.tscWatch.File = { - path: "/a/b/c/module.d.ts", - content: `export let x: number` - }; - return ts.tscWatch.createWatchedSystem([appFile, moduleFile, ts.tscWatch.libFile]); - }, - changes: ts.emptyArray - }); + const moduleFile: ts.tscWatch.File = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + return ts.tscWatch.createWatchedSystem([appFile, moduleFile, ts.tscWatch.libFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f1, ts.tscWatch.libFile, config], { useCaseSensitiveFileNames: false }); - }, - changes: ts.emptyArray - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f1, ts.tscWatch.libFile, config], { useCaseSensitiveFileNames: false }); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "create configured project without file list", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: ` + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "create configured project without file list", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: ` { "compilerOptions": {}, "exclude": [ "e" ] }` - }; - const file1: ts.tscWatch.File = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: ts.tscWatch.File = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: ts.tscWatch.File = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - return ts.tscWatch.createWatchedSystem([configFile, ts.tscWatch.libFile, file1, file2, file3]); - }, - changes: ts.emptyArray - }); + }; + const file1: ts.tscWatch.File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: ts.tscWatch.File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: ts.tscWatch.File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + return ts.tscWatch.createWatchedSystem([configFile, ts.tscWatch.libFile, file1, file2, file3]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "add new files to a configured program without file list", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.libFile, configFile]), - changes: [ - { - caption: "Create commonFile2", - change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "add new files to a configured program without file list", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.libFile, configFile]), + changes: [ + { + caption: "Create commonFile2", + change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "should ignore non-existing files specified in the config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "should ignore non-existing files specified in the config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": [ "commonFile1.ts", "commonFile3.ts" ] }` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, ts.tscWatch.libFile, configFile]); - }, - changes: ts.emptyArray - }); + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, ts.tscWatch.libFile, configFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "handle recreated files correctly", - commandLineArgs: ["-w", "-p", configFilePath, "--explainFiles"], - sys: () => { - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "handle recreated files correctly", + commandLineArgs: ["-w", "-p", configFilePath, "--explainFiles"], + sys: () => { + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); + }, + changes: [ + { + caption: "change file to ensure signatures are updated", + change: sys => sys.appendFile(ts.tscWatch.commonFile2.path, ";let xy = 10;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "change file to ensure signatures are updated", - change: sys => sys.appendFile(ts.tscWatch.commonFile2.path, ";let xy = 10;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "delete file2", - change: sys => sys.deleteFile(ts.tscWatch.commonFile2.path), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "recreate file2", - change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + { + caption: "delete file2", + change: sys => sys.deleteFile(ts.tscWatch.commonFile2.path), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "recreate file2", + change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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: ts.tscWatch.File = { - path: ts.tscWatch.commonFile1.path, - content: `/// + ts.tscWatch.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: ts.tscWatch.File = { + path: ts.tscWatch.commonFile1.path, + content: `/// let x = y` - }; - return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "create file2", - change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + }; + return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "create file2", + change: sys => sys.writeFile(ts.tscWatch.commonFile2.path, ts.tscWatch.commonFile2.content), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "should reflect change in config file", - commandLineArgs: ["-w", "-p", configFilePath, "--explainFiles"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "should reflect change in config file", + commandLineArgs: ["-w", "-p", configFilePath, "--explainFiles"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": ["${ts.tscWatch.commonFile1.path}", "${ts.tscWatch.commonFile2.path}"] }` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); + }, + changes: [ + { + caption: "change file to ensure signatures are updated", + change: sys => sys.appendFile(ts.tscWatch.commonFile2.path, ";let xy = 10;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "change file to ensure signatures are updated", - change: sys => sys.appendFile(ts.tscWatch.commonFile2.path, ";let xy = 10;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Change config", - change: sys => sys.writeFile(configFilePath, `{ + { + caption: "Change config", + change: sys => sys.writeFile(configFilePath, `{ "compilerOptions": {}, "files": ["${ts.tscWatch.commonFile1.path}"] }`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "works correctly when config file is changed but its content havent", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "works correctly when config file is changed but its content havent", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": ["${ts.tscWatch.commonFile1.path}", "${ts.tscWatch.commonFile2.path}"] }` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); - }, - changes: [ - { - caption: "Modify config without changing content", - change: sys => sys.modifyFile(configFilePath, `{ + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]); + }, + changes: [ + { + caption: "Modify config without changing content", + change: sys => sys.modifyFile(configFilePath, `{ "compilerOptions": {}, "files": ["${ts.tscWatch.commonFile1.path}", "${ts.tscWatch.commonFile2.path}"] }`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "Updates diagnostics when '--noUnusedLabels' changes", - commandLineArgs: ["-w", "-p", "/tsconfig.json"], - sys: () => { - const aTs: ts.tscWatch.File = { - path: "/a.ts", - content: "label: while (1) {}" - }; - const tsconfig: ts.tscWatch.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { allowUnusedLabels: true } - }) - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, aTs, tsconfig]); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "Updates diagnostics when '--noUnusedLabels' changes", + commandLineArgs: ["-w", "-p", "/tsconfig.json"], + sys: () => { + const aTs: ts.tscWatch.File = { + path: "/a.ts", + content: "label: while (1) {}" + }; + const tsconfig: ts.tscWatch.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { allowUnusedLabels: true } + }) + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, aTs, tsconfig]); + }, + changes: [ + { + caption: "Disable allowUnsusedLabels", + change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { allowUnusedLabels: false } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun }, - changes: [ - { - caption: "Disable allowUnsusedLabels", - change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { allowUnusedLabels: false } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - { - caption: "Enable allowUnsusedLabels", - change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { allowUnusedLabels: true } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + { + caption: "Enable allowUnsusedLabels", + change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { allowUnusedLabels: true } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates diagnostics and emit for decorators", - commandLineArgs: ["-w"], - sys: () => { - const aTs: ts.tscWatch.File = { - path: "/a.ts", - content: `import {B} from './b' + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates diagnostics and emit for decorators", + commandLineArgs: ["-w"], + sys: () => { + const aTs: ts.tscWatch.File = { + path: "/a.ts", + content: `import {B} from './b' @((_) => {}) export class A { constructor(p: B) {} }`, - }; - const bTs: ts.tscWatch.File = { - path: "/b.ts", - content: `export class B {}`, - }; - const tsconfig: ts.tscWatch.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error" } - }) - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, aTs, bTs, tsconfig]); - }, - changes: [ - { - caption: "Enable experimentalDecorators", - change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }; + const bTs: ts.tscWatch.File = { + path: "/b.ts", + content: `export class B {}`, + }; + const tsconfig: ts.tscWatch.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error" } + }) + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, aTs, bTs, tsconfig]); + }, + changes: [ + { + caption: "Enable experimentalDecorators", + change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Enable emitDecoratorMetadata", - change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true, emitDecoratorMetadata: true } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + }, + { + caption: "Enable emitDecoratorMetadata", + change: sys => sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true, emitDecoratorMetadata: true } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "files explicitly excluded in config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "files explicitly excluded in config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "exclude": ["/a/c"] }` - }; - const excludedFile1: ts.tscWatch.File = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, excludedFile1, configFile]); - }, - changes: ts.emptyArray - }); + }; + const excludedFile1: ts.tscWatch.File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, excludedFile1, configFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "should properly handle module resolution changes in config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1: ts.tscWatch.File = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: ts.tscWatch.File = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: ts.tscWatch.File = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "should properly handle module resolution changes in config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1: ts.tscWatch.File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: ts.tscWatch.File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: ts.tscWatch.File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["${file1.path}"] }` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, nodeModuleFile, classicModuleFile, configFile]); - }, - changes: [ - { - caption: "Change module resolution to classic", - change: sys => sys.writeFile(configFile.path, `{ + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, nodeModuleFile, classicModuleFile, configFile]); + }, + changes: [ + { + caption: "Change module resolution to classic", + change: sys => sys.writeFile(configFile.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "files": ["/a/b/file1.ts"] }`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "should tolerate config file errors and still try to build a project", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: `{ + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "should tolerate config file errors and still try to build a project", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: `{ "compilerOptions": { "module": "none", "allowAnything": true }, "someOtherProperty": {} }` - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, ts.tscWatch.libFile, configFile]); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "changes in files are reflected in project structure", - commandLineArgs: ["-w", "/a/b/f1.ts", "--explainFiles"], - 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Modify f2 to include f3", - // now inferred project should inclule file3 - change: sys => sys.modifyFile("/a/b/f2.ts", `export * from "../c/f3"`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "deleted files affect project structure", - commandLineArgs: ["-w", "/a/b/f1.ts", "--noImplicitAny"], - 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Delete f2", - change: sys => sys.deleteFile("/a/b/f2.ts"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, ts.tscWatch.libFile, configFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "deleted files affect project structure-2", - commandLineArgs: ["-w", "/a/b/f1.ts", "/a/c/f3.ts", "--noImplicitAny"], - 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Delete f2", - change: sys => sys.deleteFile("/a/b/f2.ts"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "changes in files are reflected in project structure", + commandLineArgs: ["-w", "/a/b/f1.ts", "--explainFiles"], + 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Modify f2 to include f3", + // now inferred project should inclule file3 + change: sys => sys.modifyFile("/a/b/f2.ts", `export * from "../c/f3"`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile, configFile]); - }, - changes: ts.emptyArray - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "deleted files affect project structure", + commandLineArgs: ["-w", "/a/b/f1.ts", "--noImplicitAny"], + 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Delete f2", + change: sys => sys.deleteFile("/a/b/f2.ts"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "change module to none", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export {}\ndeclare global {}" - }; - return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile]); - }, - changes: [{ - caption: "change `module` to 'none'", - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - change: sys => { - sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { module: "none" } })); - } - }] - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "deleted files affect project structure-2", + commandLineArgs: ["-w", "/a/b/f1.ts", "/a/c/f3.ts", "--noImplicitAny"], + 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Delete f2", + change: sys => sys.deleteFile("/a/b/f2.ts"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - it("two watch programs are not affected by each other", () => { + ts.tscWatch.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 * from "../c/f2"; - export * from "../d/f3";` + 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;" - }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, file2, file3])); - const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles: [file2.path, file3.path], - system: sys, - options: { allowNonTsExtensions: true }, - cb, - watchOptions: undefined - }); - ts.createWatchProgram(host); - baseline.push(`${sys.getExecutingFilePath()} --w ${file2.path} ${file3.path}`); - ts.tscWatch.watchBaseline({ - baseline, - getPrograms, - oldPrograms: ts.emptyArray, - sys, - oldSnap, - }); - - const { cb: cb2, getPrograms: getPrograms2 } = ts.commandLineCallbacks(sys); - const oldSnap2 = sys.snap(); - baseline.push("createing separate watcher"); - ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles:[file1.path], - system: sys, - options: { allowNonTsExtensions: true }, - cb: cb2, - watchOptions: undefined - })); - ts.tscWatch.watchBaseline({ - baseline, - getPrograms: getPrograms2, - oldPrograms: ts.emptyArray, - sys, - oldSnap: oldSnap2, - }); - - sys.checkTimeoutQueueLength(0); - baseline.push(`First program is not updated:: ${getPrograms() === ts.emptyArray}`); - baseline.push(`Second program is not updated:: ${getPrograms2() === ts.emptyArray}`); - Harness.Baseline.runBaseline(`tscWatch/${scenario}/two-watch-programs-are-not-affected-by-each-other.js`, baseline.join("\r\n")); - }); + 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 ts.tscWatch.createWatchedSystem([file1, file2, file3, ts.tscWatch.libFile, configFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Write f2", - change: sys => sys.writeFile("/a/b/f2.ts", "let y = 1"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "change module to none", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export {}\ndeclare global {}" + }; + return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile]); + }, + changes: [{ + caption: "change `module` to 'none'", + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + change: sys => { + sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { module: "none" } })); + } + }] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Modify config to make f2 as root too", - change: sys => sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] + it("two watch programs are not affected by each other", () => { + 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 { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, file2, file3])); + const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles: [file2.path, file3.path], + system: sys, + options: { allowNonTsExtensions: true }, + cb, + watchOptions: undefined }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "can correctly update configured project when set of root files has changed through include", - commandLineArgs: ["-w", "-p", "."], - sys: () => { - const file1 = { - path: `${ts.tscWatch.projectRoot}/Project/file1.ts`, - content: "export const x = 10;" - }; - const configFile = { - path: `${ts.tscWatch.projectRoot}/Project/tsconfig.json`, - content: JSON.stringify({ include: [".", "./**/*.json"] }) - }; - return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile], { currentDirectory: `${ts.tscWatch.projectRoot}/Project` }); - }, - changes: [ - { - caption: "Write file2", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/Project/file2.ts`, "export const y = 10;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - } - ] + ts.createWatchProgram(host); + baseline.push(`${sys.getExecutingFilePath()} --w ${file2.path} ${file3.path}`); + ts.tscWatch.watchBaseline({ + baseline, + getPrograms, + oldPrograms: ts.emptyArray, + sys, + oldSnap, }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Modify config to set outFile option", - change: sys => sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] + const { cb: cb2, getPrograms: getPrograms2 } = ts.commandLineCallbacks(sys); + const oldSnap2 = sys.snap(); + baseline.push("createing separate watcher"); + ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles:[file1.path], + system: sys, + options: { allowNonTsExtensions: true }, + cb: cb2, + watchOptions: undefined + })); + ts.tscWatch.watchBaseline({ + baseline, + getPrograms: getPrograms2, + oldPrograms: ts.emptyArray, + sys, + oldSnap: oldSnap2, }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "file in files 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 = 1" - }; - const configFile = { - path: configFilePath, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - return ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Delete f2", - change: sys => sys.deleteFile("/a/b/f2.ts"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + sys.checkTimeoutQueueLength(0); + baseline.push(`First program is not updated:: ${getPrograms() === ts.emptyArray}`); + baseline.push(`Second program is not updated:: ${getPrograms2() === ts.emptyArray}`); + Harness.Baseline.runBaseline(`tscWatch/${scenario}/two-watch-programs-are-not-affected-by-each-other.js`, baseline.join("\r\n")); + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Delete config file", - change: sys => sys.deleteFile(configFilePath), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Write f2", + change: sys => sys.writeFile("/a/b/f2.ts", "let y = 1"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, corruptedConfig]); - }, - changes: ts.emptyArray - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Modify config to make f2 as root too", + change: sys => sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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: `${ts.tscWatch.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 ts.tscWatch.createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - }, - changes: [ - { - caption: "Change the lib in config", - change: sys => sys.writeFile("/src/tsconfig.json", JSON.stringify({ - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - } - ] - }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "can correctly update configured project when set of root files has changed through include", + commandLineArgs: ["-w", "-p", "."], + sys: () => { + const file1 = { + path: `${ts.tscWatch.projectRoot}/Project/file1.ts`, + content: "export const x = 10;" + }; + const configFile = { + path: `${ts.tscWatch.projectRoot}/Project/tsconfig.json`, + content: JSON.stringify({ include: [".", "./**/*.json"] }) + }; + return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, configFile], { currentDirectory: `${ts.tscWatch.projectRoot}/Project` }); + }, + changes: [ + { + caption: "Write file2", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/Project/file2.ts`, "export const y = 10;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + } + ] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f, config, ts.tscWatch.libFile]); - }, - changes: ts.emptyArray - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Modify config to set outFile option", + change: sys => sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - function runQueuedTimeoutCallbacksTwice(sys: ts.tscWatch.WatchedSystem) { - sys.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions - sys.runQueuedTimeoutCallbacks(); // Actual update - } + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "file in files 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 = 1" + }; + const configFile = { + path: configFilePath, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + return ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Delete f2", + change: sys => sys.deleteFile("/a/b/f2.ts"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - const changeModuleFileToModuleFile1: ts.tscWatch.TscWatchCompileChange = { - caption: "Rename moduleFile to moduleFile1", - change: sys => { - sys.renameFile("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); - sys.deleteFile("/a/b/moduleFile.js"); - }, - timeouts: runQueuedTimeoutCallbacksTwice - }; - const changeModuleFile1ToModuleFile: ts.tscWatch.TscWatchCompileChange = { - caption: "Rename moduleFile1 back to moduleFile", - change: sys => sys.renameFile("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"), - timeouts: runQueuedTimeoutCallbacksTwice, - }; + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Delete config file", + change: sys => sys.deleteFile(configFilePath), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([moduleFile, file1, ts.tscWatch.libFile]); - }, - changes: [ - changeModuleFileToModuleFile1, - changeModuleFile1ToModuleFile - ] - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile, corruptedConfig]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([moduleFile, file1, configFile, ts.tscWatch.libFile]); - }, - changes: [ - changeModuleFileToModuleFile1, - changeModuleFile1ToModuleFile - ] - }); + ts.tscWatch.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: `${ts.tscWatch.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 ts.tscWatch.createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + }, + changes: [ + { + caption: "Change the lib in config", + change: sys => sys.writeFile("/src/tsconfig.json", JSON.stringify({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + } + ] + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f1, config, node, cwd, ts.tscWatch.libFile], { currentDirectory: cwd.path }); - }, - changes: ts.emptyArray - }); + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f, config, ts.tscWatch.libFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Create module file", - change: sys => sys.writeFile("/a/b/moduleFile.ts", "export function bar() { }"), - timeouts: runQueuedTimeoutCallbacksTwice, - } - ] - }); + function runQueuedTimeoutCallbacksTwice(sys: ts.tscWatch.WatchedSystem) { + sys.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + sys.runQueuedTimeoutCallbacks(); // Actual update + } - ts.tscWatch.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: `{ + const changeModuleFileToModuleFile1: ts.tscWatch.TscWatchCompileChange = { + caption: "Rename moduleFile to moduleFile1", + change: sys => { + sys.renameFile("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); + sys.deleteFile("/a/b/moduleFile.js"); + }, + timeouts: runQueuedTimeoutCallbacksTwice + }; + const changeModuleFile1ToModuleFile: ts.tscWatch.TscWatchCompileChange = { + caption: "Rename moduleFile1 back to moduleFile", + change: sys => sys.renameFile("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"), + timeouts: runQueuedTimeoutCallbacksTwice, + }; + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([moduleFile, file1, ts.tscWatch.libFile]); + }, + changes: [ + changeModuleFileToModuleFile1, + changeModuleFile1ToModuleFile + ] + }); + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([moduleFile, file1, configFile, ts.tscWatch.libFile]); + }, + changes: [ + changeModuleFileToModuleFile1, + changeModuleFile1ToModuleFile + ] + }); + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f1, config, node, cwd, ts.tscWatch.libFile], { currentDirectory: cwd.path }); + }, + changes: ts.emptyArray + }); + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Create module file", + change: sys => sys.writeFile("/a/b/moduleFile.ts", "export function bar() { }"), + timeouts: runQueuedTimeoutCallbacksTwice, + } + ] + }); + + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); - }, - changes: ts.emptyArray - }); + }; + return ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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: `{ + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); - }, - changes: ts.emptyArray - }); + }; + return ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); + }, + changes: ts.emptyArray + }); - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "change config file to add error", - change: sys => sys.writeFile(configFilePath, `{ + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file, configFile, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "change config file to add error", + change: sys => sys.writeFile(configFilePath, `{ "compilerOptions": { "haha": 123 } }`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "change config file to remove error", - change: sys => sys.writeFile(configFilePath, `{ + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "change config file to remove error", + change: sys => sys.writeFile(configFilePath, `{ "compilerOptions": { } }`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.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: `{ + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([file1, configFile, ts.tscWatch.libFile]); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.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 ts.tscWatch.createWatchedSystem([f, config, t1, t2, ts.tscWatch.libFile], { currentDirectory: ts.getDirectoryPath(f.path) }); - }, - changes: ts.emptyArray - }); + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + return ts.tscWatch.createWatchedSystem([file1, configFile, ts.tscWatch.libFile]); + }, + changes: ts.emptyArray + }); - it("should support files without extensions", () => { + ts.tscWatch.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 { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([f, ts.tscWatch.libFile])); - const watch = ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles: [f.path], - system: sys, - options: { allowNonTsExtensions: true }, - cb, - watchOptions: undefined - })); - ts.tscWatch.runWatchBaseline({ - scenario, - subScenario: "should support files without extensions", - commandLineArgs: ["--w", f.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: ts.emptyArray, - watchOrSolution: watch - }); - }); + 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 ts.tscWatch.createWatchedSystem([f, config, t1, t2, ts.tscWatch.libFile], { currentDirectory: ts.getDirectoryPath(f.path) }); + }, + changes: ts.emptyArray + }); - ts.tscWatch.verifyTscWatch({ + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([f, ts.tscWatch.libFile])); + const watch = ts.createWatchProgram(ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles: [f.path], + system: sys, + options: { allowNonTsExtensions: true }, + cb, + watchOptions: undefined + })); + ts.tscWatch.runWatchBaseline({ 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: ` + subScenario: "should support files without extensions", + commandLineArgs: ["--w", f.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: ts.emptyArray, + watchOrSolution: watch + }); + }); + + ts.tscWatch.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 @@ -1093,208 +1093,208 @@ declare const eval: any` "mapRoot": "./" } }` - }; - return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Remove the comment from config file", - change: sys => sys.writeFile(configFilePath, ` + }; + return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Remove the comment from config file", + change: sys => sys.writeFile(configFilePath, ` { "compilerOptions": { "inlineSourceMap": true, "mapRoot": "./" } }`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - describe("should not trigger recompilation because of program emit", () => { - function verifyWithOptions(subScenario: string, options: ts.CompilerOptions) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `should not trigger recompilation because of program emit/${subScenario}`, - commandLineArgs: ["-w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], - sys: () => { - const file1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/file1.ts`, - content: "export const c = 30;" - }; - const file2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/file2.ts`, - content: `import {c} from "file1"; export const d = 30;` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: ts.generateTSConfig(options, ts.emptyArray, "\n") - }; - return ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); + describe("should not trigger recompilation because of program emit", () => { + function verifyWithOptions(subScenario: string, options: ts.CompilerOptions) { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `should not trigger recompilation because of program emit/${subScenario}`, + commandLineArgs: ["-w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], + sys: () => { + const file1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/file1.ts`, + content: "export const c = 30;" + }; + const file2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/file2.ts`, + content: `import {c} from "file1"; export const d = 30;` + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: ts.generateTSConfig(options, ts.emptyArray, "\n") + }; + return ts.tscWatch.createWatchedSystem([file1, file2, ts.tscWatch.libFile, tsconfig], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + ts.tscWatch.noopChange, + { + caption: "Add new file", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/src/file3.ts`, `export const y = 10;`), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), // To update program and failed lookups }, - changes: [ - ts.tscWatch.noopChange, - { - caption: "Add new file", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/src/file3.ts`, `export const y = 10;`), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), // To update program and failed lookups - }, - ts.tscWatch.noopChange, - ] - }); - } + ts.tscWatch.noopChange, + ] + }); + } - verifyWithOptions("without outDir or outFile is specified", { module: ts.ModuleKind.AMD }); - verifyWithOptions("with outFile", { module: ts.ModuleKind.AMD, outFile: "build/outFile.js" }); - verifyWithOptions("when outDir is specified", { module: ts.ModuleKind.AMD, outDir: "build" }); - verifyWithOptions("without outDir or outFile is specified with declaration enabled", { module: ts.ModuleKind.AMD, declaration: true }); - verifyWithOptions("when outDir and declarationDir is specified", { module: ts.ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" }); - verifyWithOptions("declarationDir is specified", { module: ts.ModuleKind.AMD, declaration: true, declarationDir: "decls" }); - }); - ts.tscWatch.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: ts.tscWatch.File = { - path: "/a/b/file.ts", - content: `function one() {} + verifyWithOptions("without outDir or outFile is specified", { module: ts.ModuleKind.AMD }); + verifyWithOptions("with outFile", { module: ts.ModuleKind.AMD, outFile: "build/outFile.js" }); + verifyWithOptions("when outDir is specified", { module: ts.ModuleKind.AMD, outDir: "build" }); + verifyWithOptions("without outDir or outFile is specified with declaration enabled", { module: ts.ModuleKind.AMD, declaration: true }); + verifyWithOptions("when outDir and declarationDir is specified", { module: ts.ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" }); + verifyWithOptions("declarationDir is specified", { module: ts.ModuleKind.AMD, declaration: true, declarationDir: "decls" }); + }); + ts.tscWatch.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: ts.tscWatch.File = { + path: "/a/b/file.ts", + content: `function one() {} function two() { return function three() { one(); } }` - }; - return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile]); - }, - changes: [ - { - caption: "Change file to module", - change: sys => sys.writeFile("/a/b/file.ts", `function one() {} + }; + return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Change file to module", + change: sys => sys.writeFile("/a/b/file.ts", `function one() {} export function two() { return function three() { one(); } }`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - - } - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - ts.tscWatch.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: ts.tscWatch.File = { - path: `${projectLocation}/src/file1.ts`, - content: "var a = 10;" - }; - const configFile: ts.tscWatch.File = { - path: `${projectLocation}/tsconfig.json`, - content: "{}" - }; - return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, configFile]); - }, - changes: [ - { - caption: "Rename file1 to file2", - change: sys => sys.renameFile("/home/username/project/src/file1.ts", "/home/username/project/src/file2.ts"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + } + ] + }); - function changeParameterTypeOfBFile(parameterName: string, toType: string): ts.tscWatch.TscWatchCompileChange { - return { - caption: `Changed ${parameterName} type to ${toType}`, - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/b.ts`, new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + ts.tscWatch.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: ts.tscWatch.File = { + path: `${projectLocation}/src/file1.ts`, + content: "var a = 10;" }; - } + const configFile: ts.tscWatch.File = { + path: `${projectLocation}/tsconfig.json`, + content: "{}" + }; + return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, configFile]); + }, + changes: [ + { + caption: "Rename file1 to file2", + change: sys => sys.renameFile("/home/username/project/src/file1.ts", "/home/username/project/src/file2.ts"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors correctly when declaration emit is disabled in compiler options", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import test from './b'; + function changeParameterTypeOfBFile(parameterName: string, toType: string): ts.tscWatch.TscWatchCompileChange { + return { + caption: `Changed ${parameterName} type to ${toType}`, + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/b.ts`, new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }; + } + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors correctly when declaration emit is disabled in compiler options", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `import test from './b'; test(4, 5);` - }; - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `function test(x: number, y: number) { + }; + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `function test(x: number, y: number) { return x + y / 5; } export default test;` - }; - const tsconfigFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "commonjs", - noEmit: true, - strict: true, - } - }) - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, ts.tscWatch.libFile, tsconfigFile], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - changeParameterTypeOfBFile("x", "string"), - changeParameterTypeOfBFile("x", "number"), - changeParameterTypeOfBFile("y", "string"), - changeParameterTypeOfBFile("y", "number"), - ] - }); + }; + const tsconfigFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + noEmit: true, + strict: true, + } + }) + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, ts.tscWatch.libFile, tsconfigFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + changeParameterTypeOfBFile("x", "string"), + changeParameterTypeOfBFile("x", "number"), + changeParameterTypeOfBFile("y", "string"), + changeParameterTypeOfBFile("y", "number"), + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors when strictNullChecks changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `declare function foo(): null | { hello: any }; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors when strictNullChecks changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `declare function foo(): null | { hello: any }; foo().hello` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Enable strict null checks", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strictNullChecks: true } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Enable strict null checks", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strictNullChecks: true } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Set always strict false", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Disable strict", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: {} })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + { + caption: "Set always strict false", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Disable strict", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: {} })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors when noErrorTruncation changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `declare var v: { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors when noErrorTruncation changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.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; @@ -1304,590 +1304,590 @@ foo().hello` reallyLongPropertyName7: string | number | boolean | object | symbol | bigint; }; v === 'foo';` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Enable noErrorTruncation", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { noErrorTruncation: true } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Enable noErrorTruncation", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { noErrorTruncation: true } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates diagnostics and emit when useDefineForClassFields changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `/a.ts`, - content: `class C { get prop() { return 1; } } + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates diagnostics and emit when useDefineForClassFields changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `/a.ts`, + content: `class C { get prop() { return 1; } } class D extends C { prop = 1; }` - }; - const config: ts.tscWatch.File = { - path: `/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { target: "es6" } }) - }; - return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile]); + }; + const config: ts.tscWatch.File = { + path: `/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { target: "es6" } }) + }; + return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "Enable useDefineForClassFields", + change: sys => sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { target: "es6", useDefineForClassFields: true } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Enable useDefineForClassFields", - change: sys => sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { target: "es6", useDefineForClassFields: true } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors and emit when importsNotUsedAsValues changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `export class C {}` - }; - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `import {C} from './a'; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors and emit when importsNotUsedAsValues changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `export class C {}` + }; + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `import {C} from './a'; export function f(p: C) { return p; }` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: 'Set to "remove"', + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "remove" } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: 'Set to "remove"', - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "remove" } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: 'Set to "error"', - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "error" } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: 'Set to "preserve"', - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "preserve" } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + { + caption: 'Set to "error"', + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "error" } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: 'Set to "preserve"', + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "preserve" } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors when forceConsistentCasingInFileNames changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `/a.ts`, - content: `export class C {}` - }; - const bFile: ts.tscWatch.File = { - path: `/b.ts`, - content: `import {C} from './a'; import * as A from './A';` - }; - const config: ts.tscWatch.File = { - path: `/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, config, ts.tscWatch.libFile], { useCaseSensitiveFileNames: false }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors when forceConsistentCasingInFileNames changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `/a.ts`, + content: `export class C {}` + }; + const bFile: ts.tscWatch.File = { + path: `/b.ts`, + content: `import {C} from './a'; import * as A from './A';` + }; + const config: ts.tscWatch.File = { + path: `/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, config, ts.tscWatch.libFile], { useCaseSensitiveFileNames: false }); + }, + changes: [ + { + caption: "Enable forceConsistentCasingInFileNames", + change: sys => sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Enable forceConsistentCasingInFileNames", - change: sys => sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates moduleResolution when resolveJsonModule changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import * as data from './data.json'` - }; - const jsonFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/data.json`, - content: `{ "foo": 1 }` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { moduleResolution: "node" } }) - }; - return ts.tscWatch.createWatchedSystem([aFile, jsonFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates moduleResolution when resolveJsonModule changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `import * as data from './data.json'` + }; + const jsonFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/data.json`, + content: `{ "foo": 1 }` + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { moduleResolution: "node" } }) + }; + return ts.tscWatch.createWatchedSystem([aFile, jsonFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Enable resolveJsonModule", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { moduleResolution: "node", resolveJsonModule: true } })), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Enable resolveJsonModule", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { moduleResolution: "node", resolveJsonModule: true } })), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates errors when ambient modules of program changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `declare module 'a' { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates errors when ambient modules of program changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `declare module 'a' { type foo = number; }` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "Create b.ts with same content", - // Create bts with same file contents - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/b.ts`, `declare module 'a' { + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + return ts.tscWatch.createWatchedSystem([aFile, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Create b.ts with same content", + // Create bts with same file contents + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/b.ts`, `declare module 'a' { type foo = number; }`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Delete b.ts", - change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/b.ts`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Delete b.ts", + change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/b.ts`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - describe("updates errors in lib file", () => { - const field = "fullscreen"; - const fieldWithoutReadonly = `interface Document { + describe("updates errors in lib file", () => { + const field = "fullscreen"; + const fieldWithoutReadonly = `interface Document { ${field}: boolean; }`; - const libFileWithDocument: ts.tscWatch.File = { - path: ts.tscWatch.libFile.path, - content: `${ts.tscWatch.libFile.content} + const libFileWithDocument: ts.tscWatch.File = { + path: ts.tscWatch.libFile.path, + content: `${ts.tscWatch.libFile.content} interface Document { readonly ${field}: boolean; }` - }; + }; - function verifyLibFileErrorsWith(subScenario: string, aFile: ts.tscWatch.File) { - function verifyLibErrors(subScenario: string, commandLineOptions: readonly string[]) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `updates errors in lib file/${subScenario}`, - commandLineArgs: ["-w", aFile.path, ...commandLineOptions], - sys: () => ts.tscWatch.createWatchedSystem([aFile, libFileWithDocument], { currentDirectory: ts.tscWatch.projectRoot }), - changes: [ - { - caption: "Remove document declaration from file", - change: sys => sys.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: "Rever the file to contain document declaration", - change: sys => sys.writeFile(aFile.path, aFile.content), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); - } - - verifyLibErrors(`${subScenario}/with default options`, ts.emptyArray); - verifyLibErrors(`${subScenario}/with skipLibCheck`, ["--skipLibCheck"]); - verifyLibErrors(`${subScenario}/with skipDefaultLibCheck`, ["--skipDefaultLibCheck"]); + function verifyLibFileErrorsWith(subScenario: string, aFile: ts.tscWatch.File) { + function verifyLibErrors(subScenario: string, commandLineOptions: readonly string[]) { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `updates errors in lib file/${subScenario}`, + commandLineArgs: ["-w", aFile.path, ...commandLineOptions], + sys: () => ts.tscWatch.createWatchedSystem([aFile, libFileWithDocument], { currentDirectory: ts.tscWatch.projectRoot }), + changes: [ + { + caption: "Remove document declaration from file", + change: sys => sys.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: "Rever the file to contain document declaration", + change: sys => sys.writeFile(aFile.path, aFile.content), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); } - describe("when non module file changes", () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `${fieldWithoutReadonly} + verifyLibErrors(`${subScenario}/with default options`, ts.emptyArray); + verifyLibErrors(`${subScenario}/with skipLibCheck`, ["--skipLibCheck"]); + verifyLibErrors(`${subScenario}/with skipDefaultLibCheck`, ["--skipDefaultLibCheck"]); + } + + describe("when non module file changes", () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `${fieldWithoutReadonly} var y: number;` - }; - verifyLibFileErrorsWith("when non module file changes", aFile); - }); + }; + verifyLibFileErrorsWith("when non module file changes", aFile); + }); - describe("when module file with global definitions changes", () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `export {} + describe("when module file with global definitions changes", () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.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(compilerOptions: ts.CompilerOptions): ts.tscWatch.TscWatchCompileChange { - const configFileContent = JSON.stringify({ compilerOptions }); - return { - caption: `Changing config to ${configFileContent}`, - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, configFileContent), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }; - } + function changeWhenLibCheckChanges(compilerOptions: ts.CompilerOptions): ts.tscWatch.TscWatchCompileChange { + const configFileContent = JSON.stringify({ compilerOptions }); + return { + caption: `Changing config to ${configFileContent}`, + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, configFileContent), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }; + } - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "when skipLibCheck and skipDefaultLibCheck changes", - commandLineArgs: ["-w"], - sys: () => { - const field = "fullscreen"; - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `interface Document { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when skipLibCheck and skipDefaultLibCheck changes", + commandLineArgs: ["-w"], + sys: () => { + const field = "fullscreen"; + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `interface Document { ${field}: boolean; }` - }; - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.d.ts`, - content: `interface Document { + }; + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.d.ts`, + content: `interface Document { ${field}: boolean; }` - }; - const libFileWithDocument: ts.tscWatch.File = { - path: ts.tscWatch.libFile.path, - content: `${ts.tscWatch.libFile.content} + }; + const libFileWithDocument: ts.tscWatch.File = { + path: ts.tscWatch.libFile.path, + content: `${ts.tscWatch.libFile.content} interface Document { readonly ${field}: boolean; }` - }; - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, libFileWithDocument], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - changeWhenLibCheckChanges({ skipLibCheck: true }), - changeWhenLibCheckChanges({ skipDefaultLibCheck: true }), - changeWhenLibCheckChanges({}), - changeWhenLibCheckChanges({ skipDefaultLibCheck: true }), - changeWhenLibCheckChanges({ skipLibCheck: true }), - changeWhenLibCheckChanges({}), - ] - }); + }; + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, libFileWithDocument], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + changeWhenLibCheckChanges({ skipLibCheck: true }), + changeWhenLibCheckChanges({ skipDefaultLibCheck: true }), + changeWhenLibCheckChanges({}), + changeWhenLibCheckChanges({ skipDefaultLibCheck: true }), + changeWhenLibCheckChanges({ skipLibCheck: true }), + changeWhenLibCheckChanges({}), + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "reports errors correctly with isolatedModules", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `export const a: string = "";` - }; - const bFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `import { a } from "./a"; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "reports errors correctly with isolatedModules", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `export const a: string = "";` + }; + const bFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `import { a } from "./a"; const b: string = a;` - }; - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - isolatedModules: true - } - }) - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }; + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + isolatedModules: true + } + }) + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Change shape of a", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.ts`, `export const a: number = 1`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Change shape of a", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.ts`, `export const a: number = 1`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "reports errors correctly with file not in rootDir", - commandLineArgs: ["-w"], - sys: () => { - const aFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import { x } from "../b";` - }; - const bFile: ts.tscWatch.File = { - path: `/user/username/projects/b.ts`, - content: `export const x = 10;` - }; - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - rootDir: ".", - outDir: "lib" - } - }) - }; - return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "Make changes to file a", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.ts`, ` + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "reports errors correctly with file not in rootDir", + commandLineArgs: ["-w"], + sys: () => { + const aFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `import { x } from "../b";` + }; + const bFile: ts.tscWatch.File = { + path: `/user/username/projects/b.ts`, + content: `export const x = 10;` + }; + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + rootDir: ".", + outDir: "lib" + } + }) + }; + return ts.tscWatch.createWatchedSystem([aFile, bFile, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Make changes to file a", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/a.ts`, ` import { x } from "../b";`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "updates emit on jsx option change", - commandLineArgs: ["-w"], - sys: () => { - const index: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/index.tsx`, - content: `declare var React: any;\nconst d =
;` - }; - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - jsx: "preserve" - } - }) - }; - return ts.tscWatch.createWatchedSystem([index, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "updates emit on jsx option change", + commandLineArgs: ["-w"], + sys: () => { + const index: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/index.tsx`, + content: `declare var React: any;\nconst d =
;` + }; + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + jsx: "preserve" + } + }) + }; + return ts.tscWatch.createWatchedSystem([index, configFile, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Update 'jsx' to 'react'", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, '{ "compilerOptions": { "jsx": "react" } }'), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, }, - changes: [ - { - caption: "Update 'jsx' to 'react'", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/tsconfig.json`, '{ "compilerOptions": { "jsx": "react" } }'), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "extended source files are watched", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const firstExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/first.tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - strict: true - } - }) - }; - const secondExtendedConfigFile: ts.tscWatch.File = { - path: "/a/b/second.tsconfig.json", - content: JSON.stringify({ - extends: "./first.tsconfig.json" - }) - }; - const configFile: ts.tscWatch.File = { - path: configFilePath, - content: JSON.stringify({ - compilerOptions: {}, - files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] - }) - }; - return ts.tscWatch.createWatchedSystem([ - ts.tscWatch.libFile, - ts.tscWatch.commonFile1, - ts.tscWatch.commonFile2, - configFile, firstExtendedConfigFile, secondExtendedConfigFile - ]); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "extended source files are watched", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const firstExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/first.tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + strict: true + } + }) + }; + const secondExtendedConfigFile: ts.tscWatch.File = { + path: "/a/b/second.tsconfig.json", + content: JSON.stringify({ + extends: "./first.tsconfig.json" + }) + }; + const configFile: ts.tscWatch.File = { + path: configFilePath, + content: JSON.stringify({ + compilerOptions: {}, + files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] + }) + }; + return ts.tscWatch.createWatchedSystem([ + ts.tscWatch.libFile, + ts.tscWatch.commonFile1, + ts.tscWatch.commonFile2, + configFile, firstExtendedConfigFile, secondExtendedConfigFile + ]); + }, + changes: [ + { + caption: "Change config to extend another config", + change: sys => sys.modifyFile(configFilePath, JSON.stringify({ + extends: "./second.tsconfig.json", + compilerOptions: {}, + files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "Change config to extend another config", - change: sys => sys.modifyFile(configFilePath, JSON.stringify({ - extends: "./second.tsconfig.json", - compilerOptions: {}, - files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Change first extended config", - change: sys => sys.modifyFile("/a/b/first.tsconfig.json", JSON.stringify({ - compilerOptions: { - strict: false, - } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Change second extended config", - change: sys => sys.modifyFile("/a/b/second.tsconfig.json", JSON.stringify({ - extends: "./first.tsconfig.json", - compilerOptions: { - strictNullChecks: true, - } - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Change config to stop extending another config", - change: sys => sys.modifyFile(configFilePath, JSON.stringify({ - compilerOptions: {}, - files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] - })), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - ] - }); + { + caption: "Change first extended config", + change: sys => sys.modifyFile("/a/b/first.tsconfig.json", JSON.stringify({ + compilerOptions: { + strict: false, + } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Change second extended config", + change: sys => sys.modifyFile("/a/b/second.tsconfig.json", JSON.stringify({ + extends: "./first.tsconfig.json", + compilerOptions: { + strictNullChecks: true, + } + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Change config to stop extending another config", + change: sys => sys.modifyFile(configFilePath, JSON.stringify({ + compilerOptions: {}, + files: [ts.tscWatch.commonFile1.path, ts.tscWatch.commonFile2.path] + })), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "when creating new file in symlinked folder", - commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"], - sys: () => { - const module1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/client/folder1/module1.ts`, - content: `export class Module1Class { }` - }; - const module2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/folder2/module2.ts`, - content: `import * as M from "folder1/module1";` - }; - const symlink: ts.tscWatch.SymLink = { - path: `${ts.tscWatch.projectRoot}/client/linktofolder2`, - symLink: `${ts.tscWatch.projectRoot}/folder2`, - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - baseUrl: "client", - paths: { "*": ["*"] }, - }, - include: ["client/**/*", "folder2"] - }) - }; - return ts.tscWatch.createWatchedSystem([module1, module2, symlink, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when creating new file in symlinked folder", + commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"], + sys: () => { + const module1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/client/folder1/module1.ts`, + content: `export class Module1Class { }` + }; + const module2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/folder2/module2.ts`, + content: `import * as M from "folder1/module1";` + }; + const symlink: ts.tscWatch.SymLink = { + path: `${ts.tscWatch.projectRoot}/client/linktofolder2`, + symLink: `${ts.tscWatch.projectRoot}/folder2`, + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "client", + paths: { "*": ["*"] }, + }, + include: ["client/**/*", "folder2"] + }) + }; + return ts.tscWatch.createWatchedSystem([module1, module2, symlink, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Add module3 to folder2", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/client/linktofolder2/module3.ts`, `import * as M from "folder1/module1";`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "Add module3 to folder2", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/client/linktofolder2/module3.ts`, `import * as M from "folder1/module1";`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - ] - }); + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "when new file is added to the referenced project", - commandLineArgs: ["-w", "-p", `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, "--extendedDiagnostics"], - sys: () => { - const config1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - exclude: ["temp"] - }) - }; - const class1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, - content: `class class1 {}` - }; - // Built file - const class1Dt: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, - content: `declare class class1 {}` - }; - const config2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - references: [ - { path: "../project1" } - ] - }) - }; - const class2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, - content: `class class2 {}` - }; - return ts.tscWatch.createWatchedSystem([config1, class1, config2, class2, ts.tscWatch.libFile, class1Dt]); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when new file is added to the referenced project", + commandLineArgs: ["-w", "-p", `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, "--extendedDiagnostics"], + sys: () => { + const config1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true + }, + exclude: ["temp"] + }) + }; + const class1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, + content: `class class1 {}` + }; + // Built file + const class1Dt: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, + content: `declare class class1 {}` + }; + const config2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true + }, + references: [ + { path: "../project1" } + ] + }) + }; + const class2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, + content: `class class2 {}` + }; + return ts.tscWatch.createWatchedSystem([config1, class1, config2, class2, ts.tscWatch.libFile, class1Dt]); + }, + changes: [ + { + caption: "Add class3 to project1", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "Add class3 to project1", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add output of class3", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add excluded file to project1", - change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), - timeouts: sys => sys.checkTimeoutQueueLength(0), - }, - { - caption: "Delete output of class3", - change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add output of class3", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - ] - }); + { + caption: "Add output of class3", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add excluded file to project1", + change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), + timeouts: sys => sys.checkTimeoutQueueLength(0), + }, + { + caption: "Delete output of class3", + change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add output of class3", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "when creating extensionless file", - commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"], - sys: () => { - const module1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/index.ts`, - content: `` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: `{}` - }; - return ts.tscWatch.createWatchedSystem([module1, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when creating extensionless file", + commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"], + sys: () => { + const module1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/index.ts`, + content: `` + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: `{}` + }; + return ts.tscWatch.createWatchedSystem([module1, config, ts.tscWatch.libFile], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "Create foo in project root", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/foo`, ``), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, }, - changes: [ - { - caption: "Create foo in project root", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/foo`, ``), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - ] - }); + ] }); +}); } diff --git a/src/testRunner/unittests/tscWatch/projectsWithReferences.ts b/src/testRunner/unittests/tscWatch/projectsWithReferences.ts index 6dd44cd87af83..3645d20bb24e4 100644 --- a/src/testRunner/unittests/tscWatch/projectsWithReferences.ts +++ b/src/testRunner/unittests/tscWatch/projectsWithReferences.ts @@ -1,382 +1,382 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: projects with references: invoking when references are already built", () => { - ts.tscWatch.verifyTscWatch({ - scenario: "projectsWithReferences", - subScenario: "on sample project", - sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tests"], [ - ts.tscWatch.libFile, - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/tsconfig.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/index.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/anotherModule.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/some_decl.d.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "logic/tsconfig.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "logic/index.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "tests/tsconfig.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "tests/index.ts"), - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/sample1` }), - commandLineArgs: ["-w", "-p", "tests"], - changes: [ - { - caption: "local edit in logic ts, and build logic", - change: sys => { - sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/index.ts"), `function foo() { }`); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); - solutionBuilder.build(); - }, - // 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 - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun +describe("unittests:: tsc-watch:: projects with references: invoking when references are already built", () => { + ts.tscWatch.verifyTscWatch({ + scenario: "projectsWithReferences", + subScenario: "on sample project", + sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tests"], [ + ts.tscWatch.libFile, + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/tsconfig.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/index.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/anotherModule.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "core/some_decl.d.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "logic/tsconfig.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "logic/index.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "tests/tsconfig.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("sample1", "tests/index.ts"), + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/sample1` }), + commandLineArgs: ["-w", "-p", "tests"], + changes: [ + { + caption: "local edit in logic ts, and build logic", + change: sys => { + sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/index.ts"), `function foo() { }`); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); + solutionBuilder.build(); }, - { - caption: "non local edit in logic ts, and build logic", - change: sys => { - sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/index.ts"), `export function gfoo() { }`); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); - solutionBuilder.build(); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + // 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 + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "non local edit in logic ts, and build logic", + change: sys => { + sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/index.ts"), `export function gfoo() { }`); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); + solutionBuilder.build(); }, - { - caption: "change in project reference config file builds correctly", - change: sys => { - sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/tsconfig.json"), JSON.stringify({ - compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, - references: [{ path: "../core" }] - })); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); - solutionBuilder.build(); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "change in project reference config file builds correctly", + change: sys => { + sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("sample1", "logic/tsconfig.json"), JSON.stringify({ + compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, + references: [{ path: "../core" }] + })); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["logic"]); + solutionBuilder.build(); }, - ], - baselineDependencies: true - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + ], + baselineDependencies: true + }); - function changeCompilerOpitonsPaths(sys: ts.tscWatch.WatchedSystem, config: string, newPaths: object) { - const configJson = JSON.parse(sys.readFile(config)!); - configJson.compilerOptions.paths = newPaths; - sys.writeFile(config, JSON.stringify(configJson)); - } + function changeCompilerOpitonsPaths(sys: ts.tscWatch.WatchedSystem, config: string, newPaths: object) { + const configJson = JSON.parse(sys.readFile(config)!); + configJson.compilerOptions.paths = newPaths; + sys.writeFile(config, JSON.stringify(configJson)); + } - ts.tscWatch.verifyTscWatch({ - scenario: "projectsWithReferences", - subScenario: "on transitive references", - sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tsconfig.c.json"], [ - ts.tscWatch.libFile, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.b.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.c.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "a.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "b.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "c.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), - commandLineArgs: ["-w", "-p", "tsconfig.c.json"], - changes: [ - { - caption: "non local edit b ts, and build b", - change: sys => { - sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b.ts"), `export function gfoo() { }`); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["tsconfig.b.json"]); - solutionBuilder.build(); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + ts.tscWatch.verifyTscWatch({ + scenario: "projectsWithReferences", + subScenario: "on transitive references", + sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tsconfig.c.json"], [ + ts.tscWatch.libFile, + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.b.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.c.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "a.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "b.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "c.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), + commandLineArgs: ["-w", "-p", "tsconfig.c.json"], + changes: [ + { + caption: "non local edit b ts, and build b", + change: sys => { + sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b.ts"), `export function gfoo() { }`); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["tsconfig.b.json"]); + solutionBuilder.build(); }, - { - caption: "edit on config file", - change: sys => { - sys.ensureFileOrFolder({ - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), - content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! - }); - changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.c.json"), { "@ref/*": ["./nrefs/*"] }); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit on config file", + change: sys => { + sys.ensureFileOrFolder({ + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), + content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! + }); + changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.c.json"), { "@ref/*": ["./nrefs/*"] }); }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.c.json"), { "@ref/*": ["./refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit in referenced config file", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), { "@ref/*": ["./nrefs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert referenced config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), { "@ref/*": ["./refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "deleting referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json")), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert deleting referenced config file", + change: sys => sys.ensureFileOrFolder(ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.b.json")), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "deleting transitively referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.a.json")), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert deleting transitively referenced config file", + change: sys => sys.ensureFileOrFolder(ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json")), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + ], + baselineDependencies: true, + }); + + ts.tscWatch.verifyTscWatch({ + scenario: "projectsWithReferences", + subScenario: "when referenced project uses different module resolution", + sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tsconfig.c.json"], [ + ts.tscWatch.libFile, + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json"), { - caption: "Revert config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.c.json"), { "@ref/*": ["./refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), + content: JSON.stringify({ + compilerOptions: { composite: true, moduleResolution: "classic" }, + files: ["b.ts"], + references: [{ path: "tsconfig.a.json" }] + }) }, + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.c.json"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "a.ts"), { - caption: "edit in referenced config file", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), { "@ref/*": ["./nrefs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b.ts"), + content: `import {A} from "a";export const b = new A();` }, + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "c.ts"), + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), + commandLineArgs: ["-w", "-p", "tsconfig.c.json"], + changes: ts.emptyArray, + baselineDependencies: true, + }); + + ts.tscWatch.verifyTscWatch({ + scenario: "projectsWithReferences", + subScenario: "on transitive references in different folders", + sys: () => ts.tscWatch.createSystemWithSolutionBuild(["c"], [ + ts.tscWatch.libFile, { - caption: "Revert referenced config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), { "@ref/*": ["./refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), + content: JSON.stringify({ + compilerOptions: { composite: true }, + files: ["index.ts"] + }), }, { - caption: "deleting referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json")), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), + content: JSON.stringify({ + compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, + files: ["index.ts"], + references: [{ path: `../a` }] + }), }, { - caption: "Revert deleting referenced config file", - change: sys => sys.ensureFileOrFolder(ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.b.json")), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), + content: JSON.stringify({ + compilerOptions: { baseUrl: "./", paths: { "@ref/*": ["../refs/*"] } }, + files: ["index.ts"], + references: [{ path: `../b` }] + }), }, { - caption: "deleting transitively referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.a.json")), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/index.ts"), + content: `export class A {}`, }, { - caption: "Revert deleting transitively referenced config file", - change: sys => sys.ensureFileOrFolder(ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json")), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - ], - baselineDependencies: true, - }); - - ts.tscWatch.verifyTscWatch({ - scenario: "projectsWithReferences", - subScenario: "when referenced project uses different module resolution", - sys: () => ts.tscWatch.createSystemWithSolutionBuild(["tsconfig.c.json"], [ - ts.tscWatch.libFile, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.a.json"), - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "tsconfig.b.json"), - content: JSON.stringify({ - compilerOptions: { composite: true, moduleResolution: "classic" }, - files: ["b.ts"], - references: [{ path: "tsconfig.a.json" }] - }) - }, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "tsconfig.c.json"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "a.ts"), - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b.ts"), - content: `import {A} from "a";export const b = new A();` - }, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "c.ts"), - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), - commandLineArgs: ["-w", "-p", "tsconfig.c.json"], - changes: ts.emptyArray, - baselineDependencies: true, - }); - - ts.tscWatch.verifyTscWatch({ - scenario: "projectsWithReferences", - subScenario: "on transitive references in different folders", - sys: () => ts.tscWatch.createSystemWithSolutionBuild(["c"], [ - ts.tscWatch.libFile, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), - content: JSON.stringify({ - compilerOptions: { composite: true }, - files: ["index.ts"] - }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), - content: JSON.stringify({ - compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, - files: ["index.ts"], - references: [{ path: `../a` }] - }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), - content: JSON.stringify({ - compilerOptions: { baseUrl: "./", paths: { "@ref/*": ["../refs/*"] } }, - files: ["index.ts"], - references: [{ path: `../b` }] - }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/index.ts"), - content: `export class A {}`, - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), - content: `import {A} from '@ref/a'; + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), + content: `import {A} from '@ref/a'; export const b = new A();`, - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/index.ts"), - content: `import {b} from '../b'; + }, + { + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/index.ts"), + content: `import {b} from '../b'; import {X} from "@ref/a"; b; X;`, - }, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), - commandLineArgs: ["-w", "-p", "c"], - changes: [ - { - caption: "non local edit b ts, and build b", - change: sys => { - sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), `export function gfoo() { }`); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["b"]); - solutionBuilder.build(); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun }, - { - caption: "edit on config file", - change: sys => { - sys.ensureFileOrFolder({ - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), - content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! - }); - changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), + commandLineArgs: ["-w", "-p", "c"], + changes: [ + { + caption: "non local edit b ts, and build b", + change: sys => { + sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), `export function gfoo() { }`); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["b"]); + solutionBuilder.build(); }, - { - caption: "Revert config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit on config file", + change: sys => { + sys.ensureFileOrFolder({ + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), + content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! + }); + changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }); }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit in referenced config file", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert referenced config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "deleting referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json")), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "Revert deleting referenced config file", + change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), JSON.stringify({ + compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, + files: ["index.ts"], + references: [{ path: `../a` }] + })), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "deleting transitively referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json")), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "Revert deleting transitively referenced config file", + change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), JSON.stringify({ + compilerOptions: { composite: true }, + files: ["index.ts"] + })), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + ], + baselineDependencies: true, + }); + + ts.tscWatch.verifyTscWatch({ + scenario: "projectsWithReferences", + subScenario: "on transitive references in different folders with no files clause", + sys: () => ts.tscWatch.createSystemWithSolutionBuild(["c"], [ + ts.tscWatch.libFile, { - caption: "edit in referenced config file", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), + content: JSON.stringify({ compilerOptions: { composite: true } }), }, { - caption: "Revert referenced config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), + content: JSON.stringify({ + compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, + references: [{ path: `../a` }] + }), }, { - caption: "deleting referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json")), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), + content: JSON.stringify({ + compilerOptions: { baseUrl: "./", paths: { "@ref/*": ["../refs/*"] } }, + references: [{ path: `../b` }] + }), }, { - caption: "Revert deleting referenced config file", - change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), JSON.stringify({ - compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, - files: ["index.ts"], - references: [{ path: `../a` }] - })), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/index.ts"), + content: `export class A {}`, }, { - caption: "deleting transitively referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json")), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), + content: `import {A} from '@ref/a'; +export const b = new A();`, }, { - caption: "Revert deleting transitively referenced config file", - change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), JSON.stringify({ - compilerOptions: { composite: true }, - files: ["index.ts"] - })), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) - }, - ], - baselineDependencies: true, - }); - - ts.tscWatch.verifyTscWatch({ - scenario: "projectsWithReferences", - subScenario: "on transitive references in different folders with no files clause", - sys: () => ts.tscWatch.createSystemWithSolutionBuild(["c"], [ - ts.tscWatch.libFile, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), - content: JSON.stringify({ compilerOptions: { composite: true } }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), - content: JSON.stringify({ - compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, - references: [{ path: `../a` }] - }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), - content: JSON.stringify({ - compilerOptions: { baseUrl: "./", paths: { "@ref/*": ["../refs/*"] } }, - references: [{ path: `../b` }] - }), - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/index.ts"), - content: `export class A {}`, - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), - content: `import {A} from '@ref/a'; -export const b = new A();`, - }, - { - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/index.ts"), - content: `import {b} from '../b'; + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/index.ts"), + content: `import {b} from '../b'; import {X} from "@ref/a"; b; X;`, - }, - ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), - ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), - commandLineArgs: ["-w", "-p", "c"], - changes: [ - { - caption: "non local edit b ts, and build b", - change: sys => { - sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), `export function gfoo() { }`); - const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["b"]); - solutionBuilder.build(); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun }, - { - caption: "edit on config file", - change: sys => { - sys.ensureFileOrFolder({ - path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), - content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! - }); - changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - { - caption: "Revert config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - { - caption: "edit in referenced config file", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + ts.TestFSWithWatch.getTsBuildProjectFile("transitiveReferences", "refs/a.d.ts"), + ], { currentDirectory: `${ts.TestFSWithWatch.tsbuildProjectsLocation}/transitiveReferences` }), + commandLineArgs: ["-w", "-p", "c"], + changes: [ + { + caption: "non local edit b ts, and build b", + change: sys => { + sys.appendFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/index.ts"), `export function gfoo() { }`); + const solutionBuilder = ts.tscWatch.createSolutionBuilder(sys, ["b"]); + solutionBuilder.build(); }, - { - caption: "Revert referenced config file edit", - change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../refs/*"] }), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun - }, - { - caption: "deleting referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json")), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) - }, - { - caption: "Revert deleting referenced config file", - change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), JSON.stringify({ - compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, - references: [{ path: `../a` }] - })), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) - }, - { - caption: "deleting transitively referenced config file", - change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json")), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) - }, - { - caption: "Revert deleting transitively referenced config file", - change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), JSON.stringify({ compilerOptions: { composite: true } })), - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit on config file", + change: sys => { + sys.ensureFileOrFolder({ + path: ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "nrefs/a.d.ts"), + content: sys.readFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "refs/a.d.ts"))! + }); + changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }); }, - ], - baselineDependencies: true, - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "c/tsconfig.json"), { "@ref/*": ["../refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "edit in referenced config file", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../nrefs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "Revert referenced config file edit", + change: sys => changeCompilerOpitonsPaths(sys, ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), { "@ref/*": ["../refs/*"] }), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun + }, + { + caption: "deleting referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json")), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "Revert deleting referenced config file", + change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "b/tsconfig.json"), JSON.stringify({ + compilerOptions: { composite: true, baseUrl: "./", paths: { "@ref/*": ["../*"] } }, + references: [{ path: `../a` }] + })), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "deleting transitively referenced config file", + change: sys => sys.deleteFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json")), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + { + caption: "Revert deleting transitively referenced config file", + change: sys => sys.writeFile(ts.TestFSWithWatch.getTsBuildProjectFilePath("transitiveReferences", "a/tsconfig.json"), JSON.stringify({ compilerOptions: { composite: true } })), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2) + }, + ], + baselineDependencies: true, }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index 613e11a835e26..ff338fd818ef7 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -1,566 +1,566 @@ namespace ts.tscWatch { - describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { - const scenario = "resolutionCache"; - it("caching works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; +describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { + const scenario = "resolutionCache"; + it("caching works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, imported, ts.tscWatch.libFile])); - const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles: [root.path], - system: sys, - options: { module: ts.ModuleKind.AMD }, - cb, - watchOptions: undefined - }); - const originalFileExists = host.fileExists; - const watch = ts.createWatchProgram(host); - let fileExistsIsCalled = false; - ts.tscWatch.runWatchBaseline({ - scenario: "resolutionCache", - subScenario: "caching works", - commandLineArgs: ["--w", root.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [ - { - caption: "Adding text doesnt re-resole the imports", - change: sys => { - // patch fileExists to make sure that disk is not touched - host.fileExists = ts.notImplemented; - sys.writeFile(root.path, `import {x} from "f1" + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, imported, ts.tscWatch.libFile])); + const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles: [root.path], + system: sys, + options: { module: ts.ModuleKind.AMD }, + cb, + watchOptions: undefined + }); + const originalFileExists = host.fileExists; + const watch = ts.createWatchProgram(host); + let fileExistsIsCalled = false; + ts.tscWatch.runWatchBaseline({ + scenario: "resolutionCache", + subScenario: "caching works", + commandLineArgs: ["--w", root.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [ + { + caption: "Adding text doesnt re-resole the imports", + change: sys => { + // patch fileExists to make sure that disk is not touched + host.fileExists = ts.notImplemented; + sys.writeFile(root.path, `import {x} from "f1" var x: string = 1;`); - }, - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks }, - { - caption: "Resolves f2", - change: sys => { - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - sys.writeFile(root.path, `import {x} from "f2"`); - }, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsIsCalled); - }, + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks + }, + { + caption: "Resolves f2", + change: sys => { + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + sys.writeFile(root.path, `import {x} from "f2"`); }, - { - caption: "Resolve f1", - change: sys => { - fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - sys.writeFile(root.path, `import {x} from "f1"`); - }, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsIsCalled); - } + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsIsCalled); }, - ], - watchOrSolution: watch - }); + }, + { + caption: "Resolve f1", + change: sys => { + fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + sys.writeFile(root.path, `import {x} from "f1"`); + }, + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsIsCalled); + } + }, + ], + watchOrSolution: watch }); + }); - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; + 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 imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, ts.tscWatch.libFile])); - const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles: [root.path], - system: sys, - options: { module: ts.ModuleKind.AMD }, - cb, - watchOptions: undefined - }); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, ts.tscWatch.libFile])); + const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles: [root.path], + system: sys, + options: { module: ts.ModuleKind.AMD }, + cb, + watchOptions: undefined + }); + 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 = ts.createWatchProgram(host); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + ts.tscWatch.runWatchBaseline({ + scenario: "resolutionCache", + subScenario: "loads missing files from disk", + commandLineArgs: ["--w", root.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [{ + caption: "write imported file", + change: sys => { + fileExistsCalledForBar = false; + sys.writeFile(root.path,`import {y} from "bar"`); + sys.writeFile(imported.path, imported.content); + }, + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); } + }], + watchOrSolution: watch + }); + }); - return originalFileExists.call(host, fileName); - }; + 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 watch = ts.createWatchProgram(host); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - ts.tscWatch.runWatchBaseline({ - scenario: "resolutionCache", - subScenario: "loads missing files from disk", - commandLineArgs: ["--w", root.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [{ - caption: "write imported file", + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, imported, ts.tscWatch.libFile])); + const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ + rootFiles: [root.path], + system: sys, + options: { module: ts.ModuleKind.AMD }, + cb, + watchOptions: undefined + }); + 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 = ts.createWatchProgram(host); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + ts.tscWatch.runWatchBaseline({ + scenario: "resolutionCache", + subScenario: "should compile correctly when resolved module goes missing and then comes back", + commandLineArgs: ["--w", root.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [ + { + caption: "Delete imported file", change: sys => { fileExistsCalledForBar = false; - sys.writeFile(root.path,`import {y} from "bar"`); - sys.writeFile(imported.path, imported.content); + sys.deleteFile(imported.path); }, timeouts: sys => { sys.runQueuedTimeoutCallbacks(); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - } - }], - watchOrSolution: watch - }); - }); - - 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 { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([root, imported, ts.tscWatch.libFile])); - const host = ts.tscWatch.createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ - rootFiles: [root.path], - system: sys, - options: { module: ts.ModuleKind.AMD }, - cb, - watchOptions: undefined - }); - 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 = ts.createWatchProgram(host); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - ts.tscWatch.runWatchBaseline({ - scenario: "resolutionCache", - subScenario: "should compile correctly when resolved module goes missing and then comes back", - commandLineArgs: ["--w", root.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [ - { - caption: "Delete imported file", - change: sys => { - fileExistsCalledForBar = false; - sys.deleteFile(imported.path); - }, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }, }, - { - caption: "Create imported file", - change: sys => { - fileExistsCalledForBar = false; - sys.writeFile(imported.path, imported.content); - }, - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // Scheduled invalidation of resolutions - sys.checkTimeoutQueueLengthAndRun(1); // Actual update - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }, + }, + { + caption: "Create imported file", + change: sys => { + fileExistsCalledForBar = false; + sys.writeFile(imported.path, imported.content); }, - ], - watchOrSolution: watch - }); + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // Scheduled invalidation of resolutions + sys.checkTimeoutQueueLengthAndRun(1); // Actual update + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }, + }, + ], + watchOrSolution: watch }); + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "works when module resolution changes to ambient module", - commandLineArgs: ["-w", "/a/b/foo.ts"], - sys: () => ts.tscWatch.createWatchedSystem([{ - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }, ts.tscWatch.libFile], { currentDirectory: "/a/b" }), - changes: [ - { - caption: "npm install node types", - change: sys => { - sys.ensureFileOrFolder({ - path: "/a/b/node_modules/@types/node/package.json", - content: ` + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "works when module resolution changes to ambient module", + commandLineArgs: ["-w", "/a/b/foo.ts"], + sys: () => ts.tscWatch.createWatchedSystem([{ + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }, ts.tscWatch.libFile], { currentDirectory: "/a/b" }), + changes: [ + { + caption: "npm install node types", + change: 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; } }` - }); - }, - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + }); + }, + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.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: ` + ts.tscWatch.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 ts.tscWatch.createWatchedSystem([root, file, ts.tscWatch.libFile], { currentDirectory: "/a/b" }); - }, - changes: [ - { - caption: "Add fs definition", - change: sys => sys.appendFile("/a/b/bar.d.ts", ` + }; + return ts.tscWatch.createWatchedSystem([root, file, ts.tscWatch.libFile], { currentDirectory: "/a/b" }); + }, + changes: [ + { + caption: "Add fs definition", + change: sys => sys.appendFile("/a/b/bar.d.ts", ` declare module "fs" { export interface Stats { isFile(): boolean; } } `), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.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: ts.tscWatch.File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: ts.tscWatch.File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: ts.tscWatch.File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: ts.tscWatch.File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - return ts.tscWatch.createWatchedSystem([file1, file2, module1, ts.tscWatch.libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); - }, - changes: [ - { - caption: "Add new line to file1", - change: sys => sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - } - ] - }); + ts.tscWatch.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: ts.tscWatch.File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: ts.tscWatch.File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: ts.tscWatch.File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: ts.tscWatch.File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + return ts.tscWatch.createWatchedSystem([file1, file2, module1, ts.tscWatch.libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); + }, + changes: [ + { + caption: "Add new line to file1", + change: sys => sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "works when renaming node_modules folder that already contains @types folder", - commandLineArgs: ["--w", `${ts.tscWatch.projectRoot}/a.ts`], - sys: () => { - const file: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, module], { currentDirectory: ts.tscWatch.projectRoot }); + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "works when renaming node_modules folder that already contains @types folder", + commandLineArgs: ["--w", `${ts.tscWatch.projectRoot}/a.ts`], + sys: () => { + const file: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, module], { currentDirectory: ts.tscWatch.projectRoot }); + }, + changes: [ + { + caption: "npm install", + change: sys => sys.renameFolder(`${ts.tscWatch.projectRoot}/node_modules2`, `${ts.tscWatch.projectRoot}/node_modules`), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + } + ] + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, + commandLineArgs, + sys: () => { + const file1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, file2, config]); + }, + changes: [ + { + caption: "npm install file and folder that start with '.'", + change: sys => sys.ensureFileOrFolder({ + path: `${ts.tscWatch.projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }), + timeouts: sys => sys.checkTimeoutQueueLength(0), + } + ] + }); + } + verifyIgnore("watch without configFile", ["--w", `${ts.tscWatch.projectRoot}/test.ts`]); + verifyIgnore("watch with configFile", ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`]); + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "when types in compiler option are global and installed at later point", + commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], + sys: () => { + const app: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/lib/app.ts`, + content: `myapp.component("hello");` + }; + const tsconfig: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + types: ["@myapp/ts-types"] + } + }) + }; + return ts.tscWatch.createWatchedSystem([app, tsconfig, ts.tscWatch.libFile]); + }, + changes: [ + { + caption: "npm install ts-types", + change: sys => { + sys.ensureFileOrFolder({ + path: `${ts.tscWatch.projectRoot}/node_modules/@myapp/ts-types/package.json`, + content: JSON.stringify({ + version: "1.65.1", + types: "types/somefile.define.d.ts" + }) + }); + sys.ensureFileOrFolder({ + path: `${ts.tscWatch.projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, + content: ` +declare namespace myapp { + function component(str: string): number; +}` + }); + }, + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(2); // Scheduled invalidation of resolutions, update that gets cancelled and rescheduled by actual invalidation of resolution + sys.checkTimeoutQueueLengthAndRun(1); // Actual update + }, }, - changes: [ - { - caption: "npm install", - change: sys => sys.renameFolder(`${ts.tscWatch.projectRoot}/node_modules2`, `${ts.tscWatch.projectRoot}/node_modules`), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + { + caption: "No change, just check program", + change: ts.noop, + timeouts: (sys, [[oldProgram, oldBuilderProgram]], watchorSolution) => { + sys.checkTimeoutQueueLength(0); + const newProgram = (watchorSolution as ts.WatchOfConfigFile).getProgram(); + assert.strictEqual(newProgram, oldBuilderProgram, "No change so builder program should be same"); + assert.strictEqual(newProgram.getProgram(), oldProgram, "No change so program should be same"); } - ] - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, - commandLineArgs, - sys: () => { - const file1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - return ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, file1, file2, config]); - }, - changes: [ - { - caption: "npm install file and folder that start with '.'", - change: sys => sys.ensureFileOrFolder({ - path: `${ts.tscWatch.projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }), - timeouts: sys => sys.checkTimeoutQueueLength(0), - } - ] - }); } - verifyIgnore("watch without configFile", ["--w", `${ts.tscWatch.projectRoot}/test.ts`]); - verifyIgnore("watch with configFile", ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`]); - }); + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "with modules linked to sibling folder", + commandLineArgs: ["-w"], + sys: () => { + const mainPackageRoot = `${ts.tscWatch.projectRoot}/main`; + const linkedPackageRoot = `${ts.tscWatch.projectRoot}/linked-package`; + const mainFile: ts.tscWatch.File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: ts.tscWatch.File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: ts.tscWatch.SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: ts.tscWatch.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: ts.tscWatch.File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: ts.tscWatch.File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + const files = [ts.tscWatch.libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + return ts.tscWatch.createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + }, + changes: ts.emptyArray + }); + describe("works when installing something in node_modules or @types when there is no notification from fs for index file", () => { + function getNodeAtTypes() { + const nodeAtTypesIndex: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/index.d.ts`, + content: `/// ` + }; + const nodeAtTypesBase: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/base.d.ts`, + content: `// Base definitions for all NodeJS modules that are not specific to any version of TypeScript: +/// ` + }; + const nodeAtTypes36Base: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/ts3.6/base.d.ts`, + content: `/// ` + }; + const nodeAtTypesGlobals: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/globals.d.ts`, + content: `declare var process: NodeJS.Process; +declare namespace NodeJS { + interface Process { + on(msg: string): void; + } +}` + }; + return { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals }; + } ts.tscWatch.verifyTscWatch({ scenario, - subScenario: "when types in compiler option are global and installed at later point", - commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], + subScenario: "works when installing something in node_modules or @types when there is no notification from fs for index file", + commandLineArgs: ["--w", `--extendedDiagnostics`], sys: () => { - const app: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/lib/app.ts`, - content: `myapp.component("hello");` + const file: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/worker.ts`, + content: `process.on("uncaughtException");` }; const tsconfig: ts.tscWatch.File = { path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - types: ["@myapp/ts-types"] - } - }) + content: "{}" }; - return ts.tscWatch.createWatchedSystem([app, tsconfig, ts.tscWatch.libFile]); + const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); + return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, tsconfig, nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals], { currentDirectory: ts.tscWatch.projectRoot }); }, changes: [ { - caption: "npm install ts-types", + caption: "npm ci step one: remove all node_modules files", + change: sys => sys.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules/@types`, /*recursive*/ true), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }, + { + caption: `npm ci step two: create atTypes but something else in the @types folder`, + change: sys => sys.ensureFileOrFolder({ + path: `${ts.tscWatch.projectRoot}/node_modules/@types/mocha/index.d.ts`, + content: `export const foo = 10;` + }), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks + }, + { + caption: `npm ci step three: create atTypes node folder`, + change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/node_modules/@types/node` }), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks + }, + { + caption: `npm ci step four: create atTypes write all the files but dont invoke watcher for index.d.ts`, change: sys => { - sys.ensureFileOrFolder({ - path: `${ts.tscWatch.projectRoot}/node_modules/@myapp/ts-types/package.json`, - content: JSON.stringify({ - version: "1.65.1", - types: "types/somefile.define.d.ts" - }) - }); - sys.ensureFileOrFolder({ - path: `${ts.tscWatch.projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, - content: ` -declare namespace myapp { - function component(str: string): number; -}` - }); + const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); + sys.ensureFileOrFolder(nodeAtTypesBase); + sys.ensureFileOrFolder(nodeAtTypesIndex, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); + sys.ensureFileOrFolder(nodeAtTypes36Base, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); + sys.ensureFileOrFolder(nodeAtTypesGlobals, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); }, timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(2); // Scheduled invalidation of resolutions, update that gets cancelled and rescheduled by actual invalidation of resolution - sys.checkTimeoutQueueLengthAndRun(1); // Actual update + sys.runQueuedTimeoutCallbacks(); // update failed lookups + sys.runQueuedTimeoutCallbacks(); // actual program update }, }, - { - caption: "No change, just check program", - change: ts.noop, - timeouts: (sys, [[oldProgram, oldBuilderProgram]], watchorSolution) => { - sys.checkTimeoutQueueLength(0); - const newProgram = (watchorSolution as ts.WatchOfConfigFile).getProgram(); - assert.strictEqual(newProgram, oldBuilderProgram, "No change so builder program should be same"); - assert.strictEqual(newProgram.getProgram(), oldProgram, "No change so program should be same"); - } - } ] }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "with modules linked to sibling folder", - commandLineArgs: ["-w"], - sys: () => { - const mainPackageRoot = `${ts.tscWatch.projectRoot}/main`; - const linkedPackageRoot = `${ts.tscWatch.projectRoot}/linked-package`; - const mainFile: ts.tscWatch.File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: ts.tscWatch.File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: ts.tscWatch.SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: ts.tscWatch.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: ts.tscWatch.File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: ts.tscWatch.File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - const files = [ts.tscWatch.libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - return ts.tscWatch.createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - }, - changes: ts.emptyArray - }); - - describe("works when installing something in node_modules or @types when there is no notification from fs for index file", () => { - function getNodeAtTypes() { - const nodeAtTypesIndex: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/index.d.ts`, - content: `/// ` - }; - const nodeAtTypesBase: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/base.d.ts`, - content: `// Base definitions for all NodeJS modules that are not specific to any version of TypeScript: -/// ` - }; - const nodeAtTypes36Base: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/ts3.6/base.d.ts`, - content: `/// ` - }; - const nodeAtTypesGlobals: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/@types/node/globals.d.ts`, - content: `declare var process: NodeJS.Process; -declare namespace NodeJS { - interface Process { - on(msg: string): void; - } -}` - }; - return { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals }; - } - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "works when installing something in node_modules or @types when there is no notification from fs for index file", - commandLineArgs: ["--w", `--extendedDiagnostics`], - sys: () => { - const file: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/worker.ts`, - content: `process.on("uncaughtException");` - }; - const tsconfig: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); - return ts.tscWatch.createWatchedSystem([file, ts.tscWatch.libFile, tsconfig, nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals], { currentDirectory: ts.tscWatch.projectRoot }); - }, - changes: [ - { - caption: "npm ci step one: remove all node_modules files", - change: sys => sys.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules/@types`, /*recursive*/ true), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }, - { - caption: `npm ci step two: create atTypes but something else in the @types folder`, - change: sys => sys.ensureFileOrFolder({ - path: `${ts.tscWatch.projectRoot}/node_modules/@types/mocha/index.d.ts`, - content: `export const foo = 10;` - }), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks - }, - { - caption: `npm ci step three: create atTypes node folder`, - change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/node_modules/@types/node` }), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks - }, - { - caption: `npm ci step four: create atTypes write all the files but dont invoke watcher for index.d.ts`, - change: sys => { - const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); - sys.ensureFileOrFolder(nodeAtTypesBase); - sys.ensureFileOrFolder(nodeAtTypesIndex, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); - sys.ensureFileOrFolder(nodeAtTypes36Base, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); - sys.ensureFileOrFolder(nodeAtTypesGlobals, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); - }, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); // update failed lookups - sys.runQueuedTimeoutCallbacks(); // actual program update - }, - }, - ] - }); - }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts b/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts index 9315991089a4c..9d3e8194a61d2 100644 --- a/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts +++ b/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts @@ -1,170 +1,170 @@ namespace ts.tscWatch { - import getFileFromProject = ts.TestFSWithWatch.getTsBuildProjectFile; - describe("unittests:: tsc-watch:: watchAPI:: with sourceOfProjectReferenceRedirect", () => { - interface VerifyWatchInput { - files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[]; - config: string; - subScenario: string; - } +import getFileFromProject = ts.TestFSWithWatch.getTsBuildProjectFile; +describe("unittests:: tsc-watch:: watchAPI:: with sourceOfProjectReferenceRedirect", () => { + interface VerifyWatchInput { + files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[]; + config: string; + subScenario: string; + } - function verifyWatch({ files, config, subScenario }: VerifyWatchInput, alreadyBuilt: boolean) { - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files), alreadyBuilt ? (sys, originalRead) => { - ts.tscWatch.solutionBuildWithBaseline(sys, [config], originalRead); - sys.clearOutput(); - } : undefined); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config, - system: sys, - cb, - }); - host.useSourceOfProjectReferenceRedirect = ts.returnTrue; - const watch = ts.createWatchProgram(host); - ts.tscWatch.runWatchBaseline({ - scenario: "sourceOfProjectReferenceRedirect", - subScenario: `${subScenario}${alreadyBuilt ? " when solution is already built" : ""}`, - commandLineArgs: ["--w", "--p", config], - sys, - baseline, - oldSnap, - getPrograms, - changes: ts.emptyArray, - watchOrSolution: watch - }); - } + function verifyWatch({ files, config, subScenario }: VerifyWatchInput, alreadyBuilt: boolean) { + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem(files), alreadyBuilt ? (sys, originalRead) => { + ts.tscWatch.solutionBuildWithBaseline(sys, [config], originalRead); + sys.clearOutput(); + } : undefined); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config, + system: sys, + cb, + }); + host.useSourceOfProjectReferenceRedirect = ts.returnTrue; + const watch = ts.createWatchProgram(host); + ts.tscWatch.runWatchBaseline({ + scenario: "sourceOfProjectReferenceRedirect", + subScenario: `${subScenario}${alreadyBuilt ? " when solution is already built" : ""}`, + commandLineArgs: ["--w", "--p", config], + sys, + baseline, + oldSnap, + getPrograms, + changes: ts.emptyArray, + watchOrSolution: watch + }); + } - function verifyScenario(input: () => VerifyWatchInput) { - it("when solution is not built", () => { - verifyWatch(input(), /*alreadyBuilt*/ false); - }); + function verifyScenario(input: () => VerifyWatchInput) { + it("when solution is not built", () => { + verifyWatch(input(), /*alreadyBuilt*/ false); + }); + + it("when solution is already built", () => { + verifyWatch(input(), /*alreadyBuilt*/ true); + }); + } + + describe("with simple project", () => { + verifyScenario(() => { + const baseConfig = getFileFromProject("demo", "tsconfig-base.json"); + const coreTs = getFileFromProject("demo", "core/utilities.ts"); + const coreConfig = getFileFromProject("demo", "core/tsconfig.json"); + const animalTs = getFileFromProject("demo", "animals/animal.ts"); + const dogTs = getFileFromProject("demo", "animals/dog.ts"); + const indexTs = getFileFromProject("demo", "animals/index.ts"); + const animalsConfig = getFileFromProject("demo", "animals/tsconfig.json"); + return { + files: [{ path: ts.tscWatch.libFile.path, content: ts.libContent }, baseConfig, coreTs, coreConfig, animalTs, dogTs, indexTs, animalsConfig], + config: animalsConfig.path, + subScenario: "with simple project" + }; + }); + }); - it("when solution is already built", () => { - verifyWatch(input(), /*alreadyBuilt*/ true); + describe("when references are monorepo like with symlinks", () => { + interface Packages { + bPackageJson: ts.tscWatch.File; + aTest: ts.tscWatch.File; + bFoo: ts.tscWatch.File; + bBar: ts.tscWatch.File; + bSymlink: ts.tscWatch.SymLink; + subScenario: string; + } + function verifySymlinkScenario(packages: () => Packages) { + describe("when preserveSymlinks is turned off", () => { + verifySymlinkScenarioWorker(packages, {}); + }); + describe("when preserveSymlinks is turned on", () => { + verifySymlinkScenarioWorker(packages, { preserveSymlinks: true }); }); } - describe("with simple project", () => { + function verifySymlinkScenarioWorker(packages: () => Packages, extraOptions: ts.CompilerOptions) { verifyScenario(() => { - const baseConfig = getFileFromProject("demo", "tsconfig-base.json"); - const coreTs = getFileFromProject("demo", "core/utilities.ts"); - const coreConfig = getFileFromProject("demo", "core/tsconfig.json"); - const animalTs = getFileFromProject("demo", "animals/animal.ts"); - const dogTs = getFileFromProject("demo", "animals/dog.ts"); - const indexTs = getFileFromProject("demo", "animals/index.ts"); - const animalsConfig = getFileFromProject("demo", "animals/tsconfig.json"); + const { bPackageJson, aTest, bFoo, bBar, bSymlink, subScenario } = packages(); + const aConfig = config("A", extraOptions, ["../B"]); + const bConfig = config("B", extraOptions); return { - files: [{ path: ts.tscWatch.libFile.path, content: ts.libContent }, baseConfig, coreTs, coreConfig, animalTs, dogTs, indexTs, animalsConfig], - config: animalsConfig.path, - subScenario: "with simple project" + files: [ts.tscWatch.libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink], + config: aConfig.path, + subScenario: `${subScenario}${extraOptions.preserveSymlinks ? " with preserveSymlinks" : ""}` }; }); - }); - - describe("when references are monorepo like with symlinks", () => { - interface Packages { - bPackageJson: ts.tscWatch.File; - aTest: ts.tscWatch.File; - bFoo: ts.tscWatch.File; - bBar: ts.tscWatch.File; - bSymlink: ts.tscWatch.SymLink; - subScenario: string; - } - function verifySymlinkScenario(packages: () => Packages) { - describe("when preserveSymlinks is turned off", () => { - verifySymlinkScenarioWorker(packages, {}); - }); - describe("when preserveSymlinks is turned on", () => { - verifySymlinkScenarioWorker(packages, { preserveSymlinks: true }); - }); - } - - function verifySymlinkScenarioWorker(packages: () => Packages, extraOptions: ts.CompilerOptions) { - verifyScenario(() => { - const { bPackageJson, aTest, bFoo, bBar, bSymlink, subScenario } = packages(); - const aConfig = config("A", extraOptions, ["../B"]); - const bConfig = config("B", extraOptions); - return { - files: [ts.tscWatch.libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink], - config: aConfig.path, - subScenario: `${subScenario}${extraOptions.preserveSymlinks ? " with preserveSymlinks" : ""}` - }; - }); - } + } - function config(packageName: string, extraOptions: ts.CompilerOptions, references?: string[]): ts.tscWatch.File { - return { - path: `${ts.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 config(packageName: string, extraOptions: ts.CompilerOptions, references?: string[]): ts.tscWatch.File { + return { + path: `${ts.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): ts.tscWatch.File { - return { - path: `${ts.tscWatch.projectRoot}/packages/${packageName}/src/${fileName}`, - content - }; - } + function file(packageName: string, fileName: string, content: string): ts.tscWatch.File { + return { + path: `${ts.tscWatch.projectRoot}/packages/${packageName}/src/${fileName}`, + content + }; + } - function verifyMonoRepoLike(scope = "") { - describe("when packageJson has types field", () => { - verifySymlinkScenario(() => ({ - bPackageJson: { - path: `${ts.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'; + function verifyMonoRepoLike(scope = "") { + describe("when packageJson has types field", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${ts.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'; 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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${ts.tscWatch.projectRoot}/packages/B` - }, - subScenario: `when packageJson has types field${scope ? " with scoped package" : ""}` - })); - }); + bFoo: file("B", "index.ts", `export function foo() { }`), + bBar: file("B", "bar.ts", `export function bar() { }`), + bSymlink: { + path: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, + symLink: `${ts.tscWatch.projectRoot}/packages/B` + }, + subScenario: `when packageJson has types field${scope ? " with scoped package" : ""}` + })); + }); - describe("when referencing file from subFolder", () => { - verifySymlinkScenario(() => ({ - bPackageJson: { - path: `${ts.tscWatch.projectRoot}/packages/B/package.json`, - content: "{}" - }, - aTest: file("A", "test.ts", `import { foo } from '${scope}b/lib/foo'; + describe("when referencing file from subFolder", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${ts.tscWatch.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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${ts.tscWatch.projectRoot}/packages/B` - }, - subScenario: `when referencing file from subFolder${scope ? " with scoped package" : ""}` - })); - }); - } - 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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, + symLink: `${ts.tscWatch.projectRoot}/packages/B` + }, + subScenario: `when referencing file from subFolder${scope ? " with scoped package" : ""}` + })); }); + } + describe("when package is not scoped", () => { + verifyMonoRepoLike(); + }); + describe("when package is scoped", () => { + verifyMonoRepoLike("@issue/"); }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index b5be24b77b784..6cc80a2e0ef08 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -1,523 +1,523 @@ 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"] +describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { + const configFileJson: any = { + compilerOptions: { module: "commonjs", resolveJsonModule: true }, + files: ["index.ts"] + }; + const mainFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/index.ts`, + content: "import settings from './settings.json';" + }; + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const settingsJson: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/settings.json`, + content: JSON.stringify({ content: "Print this" }) + }; + + it("verify that module resolution with json extension works when returned without extension", () => { + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, mainFile, config, settingsJson], { currentDirectory: ts.tscWatch.projectRoot })); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + system: sys, + cb, + }); + const parsedCommandResult = ts.parseJsonConfigFileContent(configFileJson, sys, config.path); + host.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { + const result = ts.resolveModuleName(m, containingFile, parsedCommandResult.options, host); + const resolvedModule = result.resolvedModule!; + return { + resolvedFileName: resolvedModule.resolvedFileName, + isExternalLibraryImport: resolvedModule.isExternalLibraryImport, + originalFileName: resolvedModule.originalPath, + }; + }); + const watch = ts.createWatchProgram(host); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "verify that module resolution with json extension works when returned without extension", + commandLineArgs: ["--w", "--p", config.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: ts.emptyArray, + watchOrSolution: watch + }); + }); +}); + +describe("unittests:: tsc-watch:: watchAPI:: tsc-watch expose error count to watch status reporter", () => { + it("verify that the error count is correctly passed down to the watch status reporter", () => { + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs" }, + files: ["index.ts"] + }) }; const mainFile: ts.tscWatch.File = { path: `${ts.tscWatch.projectRoot}/index.ts`, - content: "import settings from './settings.json';" + content: "let compiler = new Compiler(); for (let i = 0; j < 5; i++) {}" + }; + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, mainFile, config], { currentDirectory: ts.tscWatch.projectRoot })); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + system: sys, + cb, + }); + const existing = host.onWatchStatusChange!; + let watchedErrorCount; + host.onWatchStatusChange = (diagnostic, newLine, options, errorCount) => { + existing.call(host, diagnostic, newLine, options, errorCount); + watchedErrorCount = errorCount; }; + const watch = ts.createWatchProgram(host); + assert.equal(watchedErrorCount, 2, "The error count was expected to be 2 for the file change"); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "verify that the error count is correctly passed down to the watch status reporter", + commandLineArgs: ["--w", "--p", config.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: ts.emptyArray, + watchOrSolution: watch + }); + }); +}); + +describe("unittests:: tsc-watch:: watchAPI:: when watchHost does not implement setTimeout or clearTimeout", () => { + it("verifies that getProgram gets updated program if new file is added to the program", () => { const config: ts.tscWatch.File = { path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) + content: "{}" }; - const settingsJson: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/settings.json`, - content: JSON.stringify({ content: "Print this" }) + const mainFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/main.ts`, + content: "const x = 10;" }; - - it("verify that module resolution with json extension works when returned without extension", () => { - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, mainFile, config, settingsJson], { currentDirectory: ts.tscWatch.projectRoot })); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - system: sys, - cb, - }); - const parsedCommandResult = ts.parseJsonConfigFileContent(configFileJson, sys, config.path); - host.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { - const result = ts.resolveModuleName(m, containingFile, parsedCommandResult.options, host); - const resolvedModule = result.resolvedModule!; - return { - resolvedFileName: resolvedModule.resolvedFileName, - isExternalLibraryImport: resolvedModule.isExternalLibraryImport, - originalFileName: resolvedModule.originalPath, - }; - }); - const watch = ts.createWatchProgram(host); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "verify that module resolution with json extension works when returned without extension", - commandLineArgs: ["--w", "--p", config.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: ts.emptyArray, - watchOrSolution: watch - }); + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, ts.tscWatch.libFile])); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + system: sys, + cb, }); - }); - - describe("unittests:: tsc-watch:: watchAPI:: tsc-watch expose error count to watch status reporter", () => { - it("verify that the error count is correctly passed down to the watch status reporter", () => { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs" }, - files: ["index.ts"] - }) - }; - const mainFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/index.ts`, - content: "let compiler = new Compiler(); for (let i = 0; j < 5; i++) {}" - }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([ts.tscWatch.libFile, mainFile, config], { currentDirectory: ts.tscWatch.projectRoot })); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - system: sys, - cb, - }); - const existing = host.onWatchStatusChange!; - let watchedErrorCount; - host.onWatchStatusChange = (diagnostic, newLine, options, errorCount) => { - existing.call(host, diagnostic, newLine, options, errorCount); - watchedErrorCount = errorCount; - }; - const watch = ts.createWatchProgram(host); - assert.equal(watchedErrorCount, 2, "The error count was expected to be 2 for the file change"); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "verify that the error count is correctly passed down to the watch status reporter", - commandLineArgs: ["--w", "--p", config.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: ts.emptyArray, - watchOrSolution: watch - }); + host.setTimeout = undefined; + host.clearTimeout = undefined; + const watch = ts.createWatchProgram(host); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "without timesouts on host program gets updated", + commandLineArgs: ["--w", "--p", config.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [{ + caption: "Write a file", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/bar.ts`, "const y =10;"), + timeouts: sys => { + sys.checkTimeoutQueueLength(0); + watch.getProgram(); + } + }], + watchOrSolution: watch }); }); +}); - describe("unittests:: tsc-watch:: watchAPI:: when watchHost does not implement setTimeout or clearTimeout", () => { - it("verifies that getProgram gets updated program if new file is added to the program", () => { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const mainFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/main.ts`, - content: "const x = 10;" - }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, ts.tscWatch.libFile])); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - system: sys, - cb, - }); - host.setTimeout = undefined; - host.clearTimeout = undefined; - const watch = ts.createWatchProgram(host); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "without timesouts on host program gets updated", - commandLineArgs: ["--w", "--p", config.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [{ - caption: "Write a file", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/bar.ts`, "const y =10;"), - timeouts: sys => { - sys.checkTimeoutQueueLength(0); - watch.getProgram(); - } - }], - watchOrSolution: watch - }); +describe("unittests:: tsc-watch:: watchAPI:: when watchHost can add extraFileExtensions to process", () => { + it("verifies that extraFileExtensions are supported to get the program with other extensions", () => { + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const mainFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/main.ts`, + content: "const x = 10;" + }; + const otherFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/other.vue`, + content: "" + }; + const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, otherFile, ts.tscWatch.libFile])); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + optionsToExtend: { allowNonTsExtensions: true }, + extraFileExtensions: [{ extension: ".vue", isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }], + system: sys, + cb, }); - }); - - describe("unittests:: tsc-watch:: watchAPI:: when watchHost can add extraFileExtensions to process", () => { - it("verifies that extraFileExtensions are supported to get the program with other extensions", () => { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const mainFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/main.ts`, - content: "const x = 10;" - }; - const otherFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/other.vue`, - content: "" - }; - const { sys, baseline, oldSnap, cb, getPrograms } = ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, otherFile, ts.tscWatch.libFile])); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - optionsToExtend: { allowNonTsExtensions: true }, - extraFileExtensions: [{ extension: ".vue", isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }], - system: sys, - cb, - }); - const watch = ts.createWatchProgram(host); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "extraFileExtensions are supported", - commandLineArgs: ["--w", "--p", config.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [{ - caption: "Write a file", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/other2.vue`, otherFile.content), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }], - watchOrSolution: watch - }); + const watch = ts.createWatchProgram(host); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "extraFileExtensions are supported", + commandLineArgs: ["--w", "--p", config.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [{ + caption: "Write a file", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/other2.vue`, otherFile.content), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }], + watchOrSolution: watch }); }); +}); - describe("unittests:: tsc-watch:: watchAPI:: when watchHost uses createSemanticDiagnosticsBuilderProgram", () => { - function createSystem(configText: string, mainText: string) { - const config: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: configText - }; - const mainFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/main.ts`, - content: mainText - }; - const otherFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/other.ts`, - content: "export const y = 10;" - }; - return { - ...ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, otherFile, ts.tscWatch.libFile])), - config, - mainFile, - otherFile, - }; - } +describe("unittests:: tsc-watch:: watchAPI:: when watchHost uses createSemanticDiagnosticsBuilderProgram", () => { + function createSystem(configText: string, mainText: string) { + const config: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: configText + }; + const mainFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/main.ts`, + content: mainText + }; + const otherFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/other.ts`, + content: "export const y = 10;" + }; + return { + ...ts.tscWatch.createBaseline(ts.tscWatch.createWatchedSystem([config, mainFile, otherFile, ts.tscWatch.libFile])), + config, + mainFile, + otherFile, + }; + } - function createWatch(baseline: string[], config: ts.tscWatch.File, sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, createProgram: ts.CreateProgram, optionsToExtend?: ts.CompilerOptions) { - const { cb, getPrograms } = ts.commandLineCallbacks(sys); - baseline.push(`tsc --w${optionsToExtend?.noEmit ? " --noEmit" : ""}`); - const oldSnap = sys.snap(); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - optionsToExtend, - createProgram, - system: sys, - cb, - }); - const watch = ts.createWatchProgram(host); - ts.tscWatch.watchBaseline({ - baseline, - getPrograms, - oldPrograms: ts.emptyArray, - sys, - oldSnap, - }); - watch.close(); - } + function createWatch(baseline: string[], config: ts.tscWatch.File, sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, createProgram: ts.CreateProgram, optionsToExtend?: ts.CompilerOptions) { + const { cb, getPrograms } = ts.commandLineCallbacks(sys); + baseline.push(`tsc --w${optionsToExtend?.noEmit ? " --noEmit" : ""}`); + const oldSnap = sys.snap(); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + optionsToExtend, + createProgram, + system: sys, + cb, + }); + const watch = ts.createWatchProgram(host); + ts.tscWatch.watchBaseline({ + baseline, + getPrograms, + oldPrograms: ts.emptyArray, + sys, + oldSnap, + }); + watch.close(); + } - function verifyOutputs(baseline: string[], sys: ts.System, emitSys: ts.System) { - baseline.push("Checking if output is same as EmitAndSemanticDiagnosticsBuilderProgram::"); - for (const output of [`${ts.tscWatch.projectRoot}/main.js`, `${ts.tscWatch.projectRoot}/main.d.ts`, `${ts.tscWatch.projectRoot}/other.js`, `${ts.tscWatch.projectRoot}/other.d.ts`, `${ts.tscWatch.projectRoot}/tsconfig.tsbuildinfo`]) { - baseline.push(`Output file text for ${output} is same:: ${sys.readFile(output) === emitSys.readFile(output)}`); - } - baseline.push(""); + function verifyOutputs(baseline: string[], sys: ts.System, emitSys: ts.System) { + baseline.push("Checking if output is same as EmitAndSemanticDiagnosticsBuilderProgram::"); + for (const output of [`${ts.tscWatch.projectRoot}/main.js`, `${ts.tscWatch.projectRoot}/main.d.ts`, `${ts.tscWatch.projectRoot}/other.js`, `${ts.tscWatch.projectRoot}/other.d.ts`, `${ts.tscWatch.projectRoot}/tsconfig.tsbuildinfo`]) { + baseline.push(`Output file text for ${output} is same:: ${sys.readFile(output) === emitSys.readFile(output)}`); } + baseline.push(""); + } - function createSystemForBuilderTest(configText: string, mainText: string) { - const result = createSystem(configText, mainText); - const { sys: emitSys, baseline: emitBaseline } = createSystem(configText, mainText); - return { ...result, emitSys, emitBaseline }; - } + function createSystemForBuilderTest(configText: string, mainText: string) { + const result = createSystem(configText, mainText); + const { sys: emitSys, baseline: emitBaseline } = createSystem(configText, mainText); + return { ...result, emitSys, emitBaseline }; + } - function applyChangeForBuilderTest(baseline: string[], emitBaseline: string[], sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, emitSys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, change: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void, caption: string) { - // Change file - ts.tscWatch.applyChange(sys, baseline, change, caption); - ts.tscWatch.applyChange(emitSys, emitBaseline, change, caption); - } + function applyChangeForBuilderTest(baseline: string[], emitBaseline: string[], sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, emitSys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, change: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void, caption: string) { + // Change file + ts.tscWatch.applyChange(sys, baseline, change, caption); + ts.tscWatch.applyChange(emitSys, emitBaseline, change, caption); + } - function verifyBuilder(baseline: string[], emitBaseline: string[], config: ts.tscWatch.File, sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, emitSys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, createProgram: ts.CreateProgram, optionsToExtend?: ts.CompilerOptions) { - createWatch(baseline, config, sys, createProgram, optionsToExtend); - createWatch(emitBaseline, config, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, optionsToExtend); - verifyOutputs(baseline, sys, emitSys); - } + function verifyBuilder(baseline: string[], emitBaseline: string[], config: ts.tscWatch.File, sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, emitSys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles, createProgram: ts.CreateProgram, optionsToExtend?: ts.CompilerOptions) { + createWatch(baseline, config, sys, createProgram, optionsToExtend); + createWatch(emitBaseline, config, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, optionsToExtend); + verifyOutputs(baseline, sys, emitSys); + } - it("verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram and typechecking happens only on affected files", () => { - const { sys, baseline, oldSnap, cb, getPrograms, config, mainFile } = createSystem("{}", "export const x = 10;"); - const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - configFileName: config.path, - optionsToExtend: { noEmit: true }, - createProgram: ts.createSemanticDiagnosticsBuilderProgram, - system: sys, - cb, - }); - const watch = ts.createWatchProgram(host); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram", - commandLineArgs: ["--w", "--p", config.path], - sys, - baseline, - oldSnap, - getPrograms, - changes: [{ - caption: "Modify a file", - change: sys => sys.appendFile(mainFile.path, "\n// SomeComment"), - timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, - }], - watchOrSolution: watch - }); + it("verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram and typechecking happens only on affected files", () => { + const { sys, baseline, oldSnap, cb, getPrograms, config, mainFile } = createSystem("{}", "export const x = 10;"); + const host = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + configFileName: config.path, + optionsToExtend: { noEmit: true }, + createProgram: ts.createSemanticDiagnosticsBuilderProgram, + system: sys, + cb, + }); + const watch = ts.createWatchProgram(host); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram", + commandLineArgs: ["--w", "--p", config.path], + sys, + baseline, + oldSnap, + getPrograms, + changes: [{ + caption: "Modify a file", + change: sys => sys.appendFile(mainFile.path, "\n// SomeComment"), + timeouts: ts.tscWatch.runQueuedTimeoutCallbacks, + }], + watchOrSolution: watch }); + }); - describe("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { - let baseline: string[]; - let emitBaseline: string[]; - before(() => { - const configText = JSON.stringify({ compilerOptions: { composite: true } }); - const mainText = "export const x = 10;"; - const result = createSystemForBuilderTest(configText, mainText); - baseline = result.baseline; - emitBaseline = result.emitBaseline; - const { sys, config, mainFile, emitSys } = result; + describe("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { + let baseline: string[]; + let emitBaseline: string[]; + before(() => { + const configText = JSON.stringify({ compilerOptions: { composite: true } }); + const mainText = "export const x = 10;"; + const result = createSystemForBuilderTest(configText, mainText); + baseline = result.baseline; + emitBaseline = result.emitBaseline; + const { sys, config, mainFile, emitSys } = result; - // No Emit - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, { noEmit: true }); + // No Emit + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, { noEmit: true }); - // Emit on both sys should result in same output - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram); + // Emit on both sys should result in same output + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram); - // Change file - applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); + // Change file + applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); - // Verify noEmit results in same output - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram, { noEmit: true }); + // Verify noEmit results in same output + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram, { noEmit: true }); - // Emit on both sys should result in same output - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram); + // Emit on both sys should result in same output + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createEmitAndSemanticDiagnosticsBuilderProgram); - // Change file - applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); + // Change file + applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); - // Emit on both the builders should result in same files - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); - }); - after(() => { - baseline = undefined!; - emitBaseline = undefined!; - }); - it("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { - Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmit-with-composite-with-semantic-builder.js`, baseline.join("\r\n")); - }); - it("baseline in createEmitAndSemanticDiagnosticsBuilderProgram:: noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { - Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmit-with-composite-with-emit-builder.js`, emitBaseline.join("\r\n")); - }); + // Emit on both the builders should result in same files + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); + }); + after(() => { + baseline = undefined!; + emitBaseline = undefined!; + }); + it("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { + Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmit-with-composite-with-semantic-builder.js`, baseline.join("\r\n")); }); + it("baseline in createEmitAndSemanticDiagnosticsBuilderProgram:: noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { + Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmit-with-composite-with-emit-builder.js`, emitBaseline.join("\r\n")); + }); + }); - describe("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { - let baseline: string[]; - let emitBaseline: string[]; - before(() => { - const configText = JSON.stringify({ compilerOptions: { composite: true, noEmitOnError: true } }); - const mainText = "export const x: string = 10;"; - const result = createSystemForBuilderTest(configText, mainText); - baseline = result.baseline; - emitBaseline = result.emitBaseline; - const { sys, config, mainFile, emitSys } = result; + describe("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { + let baseline: string[]; + let emitBaseline: string[]; + before(() => { + const configText = JSON.stringify({ compilerOptions: { composite: true, noEmitOnError: true } }); + const mainText = "export const x: string = 10;"; + const result = createSystemForBuilderTest(configText, mainText); + baseline = result.baseline; + emitBaseline = result.emitBaseline; + const { sys, config, mainFile, emitSys } = result; - // Verify noEmit results in same output - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); + // Verify noEmit results in same output + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); - // Change file - applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); + // Change file + applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.appendFile(mainFile.path, "\n// SomeComment"), "Add comment"); - // Verify noEmit results in same output - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); + // Verify noEmit results in same output + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); - // Fix error - const fixed = "export const x = 10;"; - applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.writeFile(mainFile.path, fixed), "Fix error"); + // Fix error + const fixed = "export const x = 10;"; + applyChangeForBuilderTest(baseline, emitBaseline, sys, emitSys, sys => sys.writeFile(mainFile.path, fixed), "Fix error"); - // Emit on both the builders should result in same files - verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); - }); + // Emit on both the builders should result in same files + verifyBuilder(baseline, emitBaseline, config, sys, emitSys, ts.createSemanticDiagnosticsBuilderProgram); + }); - it("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { - Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmitOnError-with-composite-with-semantic-builder.js`, baseline.join("\r\n")); - }); - it("baseline in createEmitAndSemanticDiagnosticsBuilderProgram:: noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { - Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmitOnError-with-composite-with-emit-builder.js`, emitBaseline.join("\r\n")); - }); + it("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { + Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmitOnError-with-composite-with-semantic-builder.js`, baseline.join("\r\n")); }); + it("baseline in createEmitAndSemanticDiagnosticsBuilderProgram:: noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { + Harness.Baseline.runBaseline(`tscWatch/watchApi/noEmitOnError-with-composite-with-emit-builder.js`, emitBaseline.join("\r\n")); + }); + }); - it("SemanticDiagnosticsBuilderProgram emitDtsOnly does not update affected files pending emit", () => { - // Initial - const { sys, baseline, config, mainFile } = createSystem(JSON.stringify({ compilerOptions: { composite: true, noEmitOnError: true } }), "export const x: string = 10;"); - createWatch(baseline, config, sys, ts.createSemanticDiagnosticsBuilderProgram); + it("SemanticDiagnosticsBuilderProgram emitDtsOnly does not update affected files pending emit", () => { + // Initial + const { sys, baseline, config, mainFile } = createSystem(JSON.stringify({ compilerOptions: { composite: true, noEmitOnError: true } }), "export const x: string = 10;"); + createWatch(baseline, config, sys, ts.createSemanticDiagnosticsBuilderProgram); - // Fix error and emit - ts.tscWatch.applyChange(sys, baseline, sys => sys.writeFile(mainFile.path, "export const x = 10;"), "Fix error"); - const { cb, getPrograms } = ts.commandLineCallbacks(sys); - const oldSnap = sys.snap(); - const reportDiagnostic = ts.createDiagnosticReporter(sys, /*pretty*/ true); - const reportWatchStatus = ts.createWatchStatusReporter(sys, /*pretty*/ true); - const host = ts.createWatchCompilerHostOfConfigFile({ - configFileName: config.path, - createProgram: ts.createSemanticDiagnosticsBuilderProgram, - system: sys, - reportDiagnostic, - reportWatchStatus, - }); - host.afterProgramCreate = program => { - const diagnostics = ts.sortAndDeduplicateDiagnostics(program.getSemanticDiagnostics()); - diagnostics.forEach(reportDiagnostic); - // Buildinfo should still have affectedFilesPendingEmit since we are only emitting dts files - program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDts*/ true); - reportWatchStatus(ts.createCompilerDiagnostic(ts.getWatchErrorSummaryDiagnosticMessage(diagnostics.length), diagnostics.length), sys.newLine, program.getCompilerOptions(), diagnostics.length); - cb(program); - }; - ts.createWatchProgram(host); - ts.tscWatch.watchBaseline({ - baseline, - getPrograms, - oldPrograms: ts.emptyArray, - sys, - oldSnap, - }); - Harness.Baseline.runBaseline(`tscWatch/watchApi/semantic-builder-emitOnlyDts.js`, baseline.join("\r\n")); + // Fix error and emit + ts.tscWatch.applyChange(sys, baseline, sys => sys.writeFile(mainFile.path, "export const x = 10;"), "Fix error"); + const { cb, getPrograms } = ts.commandLineCallbacks(sys); + const oldSnap = sys.snap(); + const reportDiagnostic = ts.createDiagnosticReporter(sys, /*pretty*/ true); + const reportWatchStatus = ts.createWatchStatusReporter(sys, /*pretty*/ true); + const host = ts.createWatchCompilerHostOfConfigFile({ + configFileName: config.path, + createProgram: ts.createSemanticDiagnosticsBuilderProgram, + system: sys, + reportDiagnostic, + reportWatchStatus, + }); + host.afterProgramCreate = program => { + const diagnostics = ts.sortAndDeduplicateDiagnostics(program.getSemanticDiagnostics()); + diagnostics.forEach(reportDiagnostic); + // Buildinfo should still have affectedFilesPendingEmit since we are only emitting dts files + program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDts*/ true); + reportWatchStatus(ts.createCompilerDiagnostic(ts.getWatchErrorSummaryDiagnosticMessage(diagnostics.length), diagnostics.length), sys.newLine, program.getCompilerOptions(), diagnostics.length); + cb(program); + }; + ts.createWatchProgram(host); + ts.tscWatch.watchBaseline({ + baseline, + getPrograms, + oldPrograms: ts.emptyArray, + sys, + oldSnap, }); + Harness.Baseline.runBaseline(`tscWatch/watchApi/semantic-builder-emitOnlyDts.js`, baseline.join("\r\n")); }); +}); - describe("unittests:: tsc-watch:: watchAPI:: when getParsedCommandLine is implemented", () => { - function setup(useSourceOfProjectReferenceRedirect?: () => boolean) { - const config1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - exclude: ["temp"] - }) - }; - const class1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, - content: `class class1 {}` - }; - const class1Dts: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, - content: `declare class class1 {}` - }; - const config2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - references: [ - { path: "../project1" } - ] - }) - }; - const class2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, - content: `class class2 {}` - }; - const system = ts.tscWatch.createWatchedSystem([config1, class1, class1Dts, config2, class2, ts.tscWatch.libFile]); - const baseline = ts.tscWatch.createBaseline(system); - const compilerHost = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ - cb: baseline.cb, - system, - configFileName: config2.path, - optionsToExtend: { extendedDiagnostics: true } +describe("unittests:: tsc-watch:: watchAPI:: when getParsedCommandLine is implemented", () => { + function setup(useSourceOfProjectReferenceRedirect?: () => boolean) { + const config1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true + }, + exclude: ["temp"] + }) + }; + const class1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, + content: `class class1 {}` + }; + const class1Dts: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, + content: `declare class class1 {}` + }; + const config2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true + }, + references: [ + { path: "../project1" } + ] + }) + }; + const class2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, + content: `class class2 {}` + }; + const system = ts.tscWatch.createWatchedSystem([config1, class1, class1Dts, config2, class2, ts.tscWatch.libFile]); + const baseline = ts.tscWatch.createBaseline(system); + const compilerHost = ts.tscWatch.createWatchCompilerHostOfConfigFileForBaseline({ + cb: baseline.cb, + system, + configFileName: config2.path, + optionsToExtend: { extendedDiagnostics: true } + }); + compilerHost.useSourceOfProjectReferenceRedirect = useSourceOfProjectReferenceRedirect; + const calledGetParsedCommandLine = new ts.Set(); + compilerHost.getParsedCommandLine = fileName => { + assert.isFalse(calledGetParsedCommandLine.has(fileName), `Already called on ${fileName}`); + calledGetParsedCommandLine.add(fileName); + return ts.getParsedCommandLineOfConfigFile(fileName, /*optionsToExtend*/ undefined, { + useCaseSensitiveFileNames: true, + fileExists: path => system.fileExists(path), + readFile: path => system.readFile(path), + getCurrentDirectory: () => system.getCurrentDirectory(), + readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), + onUnRecoverableConfigFileDiagnostic: ts.noop, }); - compilerHost.useSourceOfProjectReferenceRedirect = useSourceOfProjectReferenceRedirect; - const calledGetParsedCommandLine = new ts.Set(); - compilerHost.getParsedCommandLine = fileName => { - assert.isFalse(calledGetParsedCommandLine.has(fileName), `Already called on ${fileName}`); - calledGetParsedCommandLine.add(fileName); - return ts.getParsedCommandLineOfConfigFile(fileName, /*optionsToExtend*/ undefined, { - useCaseSensitiveFileNames: true, - fileExists: path => system.fileExists(path), - readFile: path => system.readFile(path), - getCurrentDirectory: () => system.getCurrentDirectory(), - readDirectory: (path, extensions, excludes, includes, depth) => system.readDirectory(path, extensions, excludes, includes, depth), - onUnRecoverableConfigFileDiagnostic: ts.noop, - }); - }; - const watch = ts.createWatchProgram(compilerHost); - return { watch, baseline, config2, calledGetParsedCommandLine }; - } + }; + const watch = ts.createWatchProgram(compilerHost); + return { watch, baseline, config2, calledGetParsedCommandLine }; + } - it("when new file is added to the referenced project with host implementing getParsedCommandLine", () => { - const { watch, baseline, config2, calledGetParsedCommandLine } = setup(ts.returnTrue); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "when new file is added to the referenced project with host implementing getParsedCommandLine", - commandLineArgs: ["--w", "-p", config2.path, "--extendedDiagnostics"], - ...baseline, - changes: [ - { - caption: "Add class3 to project1", - change: sys => { - calledGetParsedCommandLine.clear(); - sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add excluded file to project1", - change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), - timeouts: sys => sys.checkTimeoutQueueLength(0), - }, - { - caption: "Add output of class3", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), - timeouts: sys => sys.checkTimeoutQueueLength(0), + it("when new file is added to the referenced project with host implementing getParsedCommandLine", () => { + const { watch, baseline, config2, calledGetParsedCommandLine } = setup(ts.returnTrue); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "when new file is added to the referenced project with host implementing getParsedCommandLine", + commandLineArgs: ["--w", "-p", config2.path, "--extendedDiagnostics"], + ...baseline, + changes: [ + { + caption: "Add class3 to project1", + change: sys => { + calledGetParsedCommandLine.clear(); + sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`); }, - ], - watchOrSolution: watch - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add excluded file to project1", + change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), + timeouts: sys => sys.checkTimeoutQueueLength(0), + }, + { + caption: "Add output of class3", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), + timeouts: sys => sys.checkTimeoutQueueLength(0), + }, + ], + watchOrSolution: watch }); + }); - it("when new file is added to the referenced project with host implementing getParsedCommandLine without implementing useSourceOfProjectReferenceRedirect", () => { - const { watch, baseline, config2, calledGetParsedCommandLine } = setup(); - ts.tscWatch.runWatchBaseline({ - scenario: "watchApi", - subScenario: "when new file is added to the referenced project with host implementing getParsedCommandLine without implementing useSourceOfProjectReferenceRedirect", - commandLineArgs: ["--w", "-p", config2.path, "--extendedDiagnostics"], - ...baseline, - changes: [ - { - caption: "Add class3 to project1", - change: sys => { - calledGetParsedCommandLine.clear(); - sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`); - }, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add class3 output to project1", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add excluded file to project1", - change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), - timeouts: sys => sys.checkTimeoutQueueLength(0), + it("when new file is added to the referenced project with host implementing getParsedCommandLine without implementing useSourceOfProjectReferenceRedirect", () => { + const { watch, baseline, config2, calledGetParsedCommandLine } = setup(); + ts.tscWatch.runWatchBaseline({ + scenario: "watchApi", + subScenario: "when new file is added to the referenced project with host implementing getParsedCommandLine without implementing useSourceOfProjectReferenceRedirect", + commandLineArgs: ["--w", "-p", config2.path, "--extendedDiagnostics"], + ...baseline, + changes: [ + { + caption: "Add class3 to project1", + change: sys => { + calledGetParsedCommandLine.clear(); + sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.ts`, `class class3 {}`); }, - { - caption: "Delete output of class3", - change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - { - caption: "Add output of class3", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, - }, - ], - watchOrSolution: watch - }); + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add class3 output to project1", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add excluded file to project1", + change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }), + timeouts: sys => sys.checkTimeoutQueueLength(0), + }, + { + caption: "Delete output of class3", + change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Add output of class3", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`, `declare class class3 {}`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + ], + watchOrSolution: watch }); }); +}); } diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 4d4efb9b47fa2..825465d0df842 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -1,586 +1,586 @@ namespace ts.tscWatch { - import Tsc_WatchDirectory = ts.TestFSWithWatch.Tsc_WatchDirectory; - describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { - const scenario = "watchEnvironment"; +import Tsc_WatchDirectory = ts.TestFSWithWatch.Tsc_WatchDirectory; +describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { + const scenario = "watchEnvironment"; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchFile/using dynamic priority polling", + commandLineArgs: ["--w", `/a/username/project/typescript.ts`], + sys: () => { + const projectFolder = "/a/username/project"; + const file1: ts.tscWatch.File = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const environmentVariables = new ts.Map(); + environmentVariables.set("TSC_WATCHFILE", ts.TestFSWithWatch.Tsc_WatchFile.DynamicPolling); + return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile], { environmentVariables }); + }, + changes: [ + { + caption: "Time spent to Transition libFile and file1 to low priority queue", + change: ts.noop, + timeouts: (sys, programs) => { + const initialProgram = programs[0][0]; + const mediumPollingIntervalThreshold = ts.unchangedPollThresholds[ts.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; + }, + }, + { + caption: "Make change to file", + // Make a change to file + change: sys => sys.writeFile("/a/username/project/typescript.ts", "var zz30 = 100;"), + // During this timeout the file would be detected as unchanged + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Callbacks: medium priority + high priority queue and scheduled program update", + change: ts.noop, + // Callbacks: medium priority + high priority queue and scheduled program update + // This should detect change in the file + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(3), + }, + { + caption: "Polling queues polled and everything is in the high polling queue", + change: ts.noop, + timeouts: (sys, programs) => { + const initialProgram = programs[0][0]; + const mediumPollingIntervalThreshold = ts.unchangedPollThresholds[ts.PollingInterval.Medium]; + const newThreshold = ts.unchangedPollThresholds[ts.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; + }, + } + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchFile/using fixed chunk size polling", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchFile: "FixedChunkSizePolling" + } + }) + }; + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + return ts.tscWatch.createWatchedSystem(files); + }, + changes: [ + { + caption: "The timeout is to check the status of all files", + change: ts.noop, + timeouts: (sys, programs) => { + // On each timeout file does not change + const initialProgram = programs[0][0]; + for (let index = 0; index < 4; index++) { + sys.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(programs[0][0], initialProgram); + } + }, + }, + { + caption: "Make change to file but should detect as changed and schedule program update", + // Make a change to file + change: sys => sys.writeFile(ts.tscWatch.commonFile1.path, "var zz30 = 100;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Callbacks: queue and scheduled program update", + change: ts.noop, + // Callbacks: scheduled program update and queue for the polling + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), + }, + { + caption: "The timeout is to check the status of all files", + change: ts.noop, + timeouts: (sys, programs) => { + // On each timeout file does not change + const initialProgram = programs[0][0]; + sys.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(programs[0][0], initialProgram); + }, + }, + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchFile/setting default as fixed chunk size watch file works", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + const sys = ts.tscWatch.createWatchedSystem(files); + sys.defaultWatchFileKind = () => ts.WatchFileKind.FixedChunkSizePolling; + return sys; + }, + changes: [ + { + caption: "Make change to file but should detect as changed and schedule program update", + // Make a change to file + change: sys => sys.writeFile(ts.tscWatch.commonFile1.path, "var zz30 = 100;"), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + }, + { + caption: "Callbacks: queue and scheduled program update", + change: ts.noop, + // Callbacks: scheduled program update and queue for the polling + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), + }, + ] + }); + + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(subScenario: string, tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: ts.tscWatch.File = { + path: `${projectFolder}/tsconfig.json`, + content: JSON.stringify({ + watchOptions: { + synchronousWatchDirectory: true + } + }) + }; + const file: ts.tscWatch.File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `watchDirectories/${subScenario}`, + commandLineArgs: ["--w", "-p", configFile.path], + sys: () => { + const files = [file, configFile, ts.tscWatch.libFile]; + const environmentVariables = new ts.Map(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + return ts.tscWatch.createWatchedSystem(files, { environmentVariables }); + }, + changes: [ + { + caption: "Rename file1 to file2", + // Rename the file: + change: sys => sys.renameFile(file.path, file.path.replace("file1.ts", "file2.ts")), + timeouts: sys => { + 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; + }, + }, + ], + }); + } + + 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); + ts.tscWatch.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: ts.tscWatch.File = { - path: `${projectFolder}/typescript.ts`, - content: "var z = 10;" + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: ts.tscWatch.File = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: ts.tscWatch.File = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: ts.tscWatch.File = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: ts.tscWatch.SymLink = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` }; + const symLinkB: ts.tscWatch.SymLink = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` + }; + const symLinkBInA: ts.tscWatch.SymLink = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: ts.tscWatch.SymLink = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [ts.tscWatch.libFile, file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; const environmentVariables = new ts.Map(); - environmentVariables.set("TSC_WATCHFILE", ts.TestFSWithWatch.Tsc_WatchFile.DynamicPolling); - return ts.tscWatch.createWatchedSystem([file1, ts.tscWatch.libFile], { environmentVariables }); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + return ts.tscWatch.createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + }, + changes: ts.emptyArray + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchDirectories/with non synchronous watch directory", + commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], + sys: () => { + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const file1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/file1.ts`, + content: `import { x } from "file2";` + }; + const file2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, + content: `export const x = 10;` + }; + const files = [ts.tscWatch.libFile, file1, file2, configFile]; + return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); }, changes: [ { - caption: "Time spent to Transition libFile and file1 to low priority queue", + caption: "Directory watch updates because of file1.js creation", change: ts.noop, - timeouts: (sys, programs) => { - const initialProgram = programs[0][0]; - const mediumPollingIntervalThreshold = ts.unchangedPollThresholds[ts.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; + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output + sys.checkTimeoutQueueLength(0); }, }, { - caption: "Make change to file", - // Make a change to file - change: sys => sys.writeFile("/a/username/project/typescript.ts", "var zz30 = 100;"), - // During this timeout the file would be detected as unchanged - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + caption: "Remove directory node_modules", + // Remove directory node_modules + change: sys => sys.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules`, /*recursive*/ true), + timeouts: sys => { + sys.checkTimeoutQueueLength(3); // 1. Failed lookup invalidation 2. For updating program and 3. for updating child watches + sys.runQueuedTimeoutCallbacks(sys.getNextTimeoutId() - 2); // Update program + }, }, { - caption: "Callbacks: medium priority + high priority queue and scheduled program update", + caption: "Pending directory watchers and program update", change: ts.noop, - // Callbacks: medium priority + high priority queue and scheduled program update - // This should detect change in the file - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(3), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers + sys.checkTimeoutQueueLengthAndRun(2); // To Update program and failed lookup update + sys.checkTimeoutQueueLengthAndRun(1); // Actual program update + sys.checkTimeoutQueueLength(0); + }, + }, + { + caption: "Start npm install", + // npm install + change: sys => sys.createDirectory(`${ts.tscWatch.projectRoot}/node_modules`), + timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure }, { - caption: "Polling queues polled and everything is in the high polling queue", + caption: "npm install folder creation of file2", + change: sys => sys.createDirectory(`${ts.tscWatch.projectRoot}/node_modules/file2`), + timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure + }, + { + caption: "npm install index file in file2", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, `export const x = 10;`), + timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure + }, + { + caption: "Updates the program", change: ts.noop, - timeouts: (sys, programs) => { - const initialProgram = programs[0][0]; - const mediumPollingIntervalThreshold = ts.unchangedPollThresholds[ts.PollingInterval.Medium]; - const newThreshold = ts.unchangedPollThresholds[ts.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; + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(2); // To Update program and failed lookup update }, - } - ] + }, + { + caption: "Invalidates module resolution cache", + change: ts.noop, + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(1); // To Update program + }, + }, + { + caption: "Pending updates", + change: ts.noop, + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(0); + }, + }, + ], }); ts.tscWatch.verifyTscWatch({ scenario, - subScenario: "watchFile/using fixed chunk size polling", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + subScenario: "watchDirectories/with non synchronous watch directory with outDir and declaration enabled", + commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], sys: () => { const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - watchFile: "FixedChunkSizePolling" - } - }) + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { outDir: "dist", declaration: true } }) }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - return ts.tscWatch.createWatchedSystem(files); + const file1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/file1.ts`, + content: `import { x } from "file2";` + }; + const file2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, + content: `export const x = 10;` + }; + const files = [ts.tscWatch.libFile, file1, file2, configFile]; + return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); }, changes: [ + ts.tscWatch.noopChange, { - caption: "The timeout is to check the status of all files", - change: ts.noop, - timeouts: (sys, programs) => { - // On each timeout file does not change - const initialProgram = programs[0][0]; - for (let index = 0; index < 4; index++) { - sys.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(programs[0][0], initialProgram); - } - }, - }, - { - caption: "Make change to file but should detect as changed and schedule program update", - // Make a change to file - change: sys => sys.writeFile(ts.tscWatch.commonFile1.path, "var zz30 = 100;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + caption: "Add new file, should schedule and run timeout to update directory watcher", + change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/src/file3.ts`, `export const y = 10;`), + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Update the child watch }, { - caption: "Callbacks: queue and scheduled program update", + caption: "Actual program update to include new file", change: ts.noop, - // Callbacks: scheduled program update and queue for the polling - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), // Scheduling failed lookup update and program update }, { - caption: "The timeout is to check the status of all files", + caption: "After program emit with new file, should schedule and run timeout to update directory watcher", change: ts.noop, - timeouts: (sys, programs) => { - // On each timeout file does not change - const initialProgram = programs[0][0]; - sys.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(programs[0][0], initialProgram); - }, + timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Update the child watch }, - ] + ts.tscWatch.noopChange, + ], }); ts.tscWatch.verifyTscWatch({ scenario, - subScenario: "watchFile/setting default as fixed chunk size watch file works", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + subScenario: "watchDirectories/with non synchronous watch directory renaming a file", + commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], sys: () => { const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: "{}" + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { outDir: "dist" } }) }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - const sys = ts.tscWatch.createWatchedSystem(files); - sys.defaultWatchFileKind = () => ts.WatchFileKind.FixedChunkSizePolling; - return sys; + const file1: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/file1.ts`, + content: `import { x } from "./file2";` + }; + const file2: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/file2.ts`, + content: `export const x = 10;` + }; + const files = [ts.tscWatch.libFile, file1, file2, configFile]; + return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); }, changes: [ + ts.tscWatch.noopChange, { - caption: "Make change to file but should detect as changed and schedule program update", - // Make a change to file - change: sys => sys.writeFile(ts.tscWatch.commonFile1.path, "var zz30 = 100;"), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, + caption: "rename the file", + change: sys => sys.renameFile(`${ts.tscWatch.projectRoot}/src/file2.ts`, `${ts.tscWatch.projectRoot}/src/renamed.ts`), + timeouts: sys => { + sys.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches + sys.runQueuedTimeoutCallbacks(1); // Update program + }, }, { - caption: "Callbacks: queue and scheduled program update", + caption: "Pending directory watchers and program update", change: ts.noop, - // Callbacks: scheduled program update and queue for the polling - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers + sys.checkTimeoutQueueLengthAndRun(2); // To Update program and failed lookup update + sys.checkTimeoutQueueLengthAndRun(1); // Actual program update + sys.checkTimeoutQueueLength(0); + }, }, - ] + ], }); + }); - 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", () => { + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchFile option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchFile: "UseFsEvents" + } + }) + }; + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + return ts.tscWatch.createWatchedSystem(files); + }, + changes: ts.emptyArray + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchDirectory option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchDirectory: "UseFsEvents" + } + }) + }; + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); + }, + changes: ts.emptyArray + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchOptions/with fallbackPolling option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { const configFile: ts.tscWatch.File = { - path: `${projectFolder}/tsconfig.json`, + path: "/a/b/tsconfig.json", content: JSON.stringify({ watchOptions: { - synchronousWatchDirectory: true + fallbackPolling: "PriorityInterval" } }) }; - const file: ts.tscWatch.File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); + }, + changes: ts.emptyArray + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchFile as watch options to extend", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json", "--watchFile", "UseFsEvents"], + sys: () => { + const configFile: ts.tscWatch.File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; + return ts.tscWatch.createWatchedSystem(files); + }, + changes: ts.emptyArray + }); + + describe("exclude options", () => { + function sys(watchOptions: ts.WatchOptions, runWithoutRecursiveWatches?: boolean): ts.tscWatch.WatchedSystem { + const configFile: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ exclude: ["node_modules"], watchOptions }) + }; + const main: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/src/main.ts`, + content: `import { foo } from "bar"; foo();` + }; + const bar: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/bar/index.d.ts`, + content: `export { foo } from "./foo";` + }; + const foo: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, + content: `export function foo(): string;` }; + const fooBar: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/bar/fooBar.d.ts`, + content: `export function fooBar(): string;` + }; + const temp: ts.tscWatch.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/bar/temp/index.d.ts`, + content: "export function temp(): string;" + }; + const files = [ts.tscWatch.libFile, main, bar, foo, fooBar, temp, configFile]; + return ts.tscWatch.createWatchedSystem(files, { currentDirectory: ts.tscWatch.projectRoot, runWithoutRecursiveWatches }); + } + + function verifyWorker(...additionalFlags: string[]) { ts.tscWatch.verifyTscWatch({ scenario, - subScenario: `watchDirectories/${subScenario}`, - commandLineArgs: ["--w", "-p", configFile.path], - sys: () => { - const files = [file, configFile, ts.tscWatch.libFile]; - const environmentVariables = new ts.Map(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - return ts.tscWatch.createWatchedSystem(files, { environmentVariables }); - }, + subScenario: `watchOptions/with excludeFiles option${additionalFlags.join("")}`, + commandLineArgs: ["-w", ...additionalFlags], + sys: () => sys({ excludeFiles: ["node_modules/*"] }), changes: [ { - caption: "Rename file1 to file2", - // Rename the file: - change: sys => sys.renameFile(file.path, file.path.replace("file1.ts", "file2.ts")), + caption: "Change foo", + change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, "foo", "fooBar"), + timeouts: sys => sys.checkTimeoutQueueLength(0), + } + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `watchOptions/with excludeDirectories option${additionalFlags.join("")}`, + commandLineArgs: ["-w", ...additionalFlags], + sys: () => sys({ excludeDirectories: ["node_modules"] }), + changes: [ + { + caption: "delete fooBar", + change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/node_modules/bar/fooBar.d.ts`), + timeouts: sys => sys.checkTimeoutQueueLength(0), + } + ] + }); + + ts.tscWatch.verifyTscWatch({ + scenario, + subScenario: `watchOptions/with excludeDirectories option with recursive directory watching${additionalFlags.join("")}`, + commandLineArgs: ["-w", ...additionalFlags], + sys: () => sys({ excludeDirectories: ["**/temp"] }, /*runWithoutRecursiveWatches*/ true), + changes: [ + { + caption: "Directory watch updates because of main.js creation", + change: ts.noop, timeouts: sys => { - 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; + sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for main.js output + sys.checkTimeoutQueueLength(0); }, }, - ], + { + caption: "add new folder to temp", + change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/node_modules/bar/temp/fooBar/index.d.ts`, content: "export function temp(): string;" }), + timeouts: sys => sys.checkTimeoutQueueLength(0), + } + ] }); } - 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); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchDirectories/when there are symlinks to folders in recursive folders", - commandLineArgs: ["--w"], - sys: () => { - const cwd = "/home/user/projects/myproject"; - const file1: ts.tscWatch.File = { - path: `${cwd}/src/file.ts`, - content: `import * as a from "a"` - }; - const tsconfig: ts.tscWatch.File = { - path: `${cwd}/tsconfig.json`, - content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` - }; - const realA: ts.tscWatch.File = { - path: `${cwd}/node_modules/reala/index.d.ts`, - content: `export {}` - }; - const realB: ts.tscWatch.File = { - path: `${cwd}/node_modules/realb/index.d.ts`, - content: `export {}` - }; - const symLinkA: ts.tscWatch.SymLink = { - path: `${cwd}/node_modules/a`, - symLink: `${cwd}/node_modules/reala` - }; - const symLinkB: ts.tscWatch.SymLink = { - path: `${cwd}/node_modules/b`, - symLink: `${cwd}/node_modules/realb` - }; - const symLinkBInA: ts.tscWatch.SymLink = { - path: `${cwd}/node_modules/reala/node_modules/b`, - symLink: `${cwd}/node_modules/b` - }; - const symLinkAInB: ts.tscWatch.SymLink = { - path: `${cwd}/node_modules/realb/node_modules/a`, - symLink: `${cwd}/node_modules/a` - }; - const files = [ts.tscWatch.libFile, file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; - const environmentVariables = new ts.Map(); - environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - return ts.tscWatch.createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchDirectories/with non synchronous watch directory", - commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], - sys: () => { - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const file1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/file1.ts`, - content: `import { x } from "file2";` - }; - const file2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, - content: `export const x = 10;` - }; - const files = [ts.tscWatch.libFile, file1, file2, configFile]; - return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: [ - { - caption: "Directory watch updates because of file1.js creation", - change: ts.noop, - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output - sys.checkTimeoutQueueLength(0); - }, - }, - { - caption: "Remove directory node_modules", - // Remove directory node_modules - change: sys => sys.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules`, /*recursive*/ true), - timeouts: sys => { - sys.checkTimeoutQueueLength(3); // 1. Failed lookup invalidation 2. For updating program and 3. for updating child watches - sys.runQueuedTimeoutCallbacks(sys.getNextTimeoutId() - 2); // Update program - }, - }, - { - caption: "Pending directory watchers and program update", - change: ts.noop, - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers - sys.checkTimeoutQueueLengthAndRun(2); // To Update program and failed lookup update - sys.checkTimeoutQueueLengthAndRun(1); // Actual program update - sys.checkTimeoutQueueLength(0); - }, - }, - { - caption: "Start npm install", - // npm install - change: sys => sys.createDirectory(`${ts.tscWatch.projectRoot}/node_modules`), - timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure - }, - { - caption: "npm install folder creation of file2", - change: sys => sys.createDirectory(`${ts.tscWatch.projectRoot}/node_modules/file2`), - timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure - }, - { - caption: "npm install index file in file2", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, `export const x = 10;`), - timeouts: sys => sys.checkTimeoutQueueLength(1), // To update folder structure - }, - { - caption: "Updates the program", - change: ts.noop, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(2); // To Update program and failed lookup update - }, - }, - { - caption: "Invalidates module resolution cache", - change: ts.noop, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(1); // To Update program - }, - }, - { - caption: "Pending updates", - change: ts.noop, - timeouts: sys => { - sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(0); - }, - }, - ], - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchDirectories/with non synchronous watch directory with outDir and declaration enabled", - commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], - sys: () => { - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { outDir: "dist", declaration: true } }) - }; - const file1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/file1.ts`, - content: `import { x } from "file2";` - }; - const file2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/file2/index.d.ts`, - content: `export const x = 10;` - }; - const files = [ts.tscWatch.libFile, file1, file2, configFile]; - return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: [ - ts.tscWatch.noopChange, - { - caption: "Add new file, should schedule and run timeout to update directory watcher", - change: sys => sys.writeFile(`${ts.tscWatch.projectRoot}/src/file3.ts`, `export const y = 10;`), - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Update the child watch - }, - { - caption: "Actual program update to include new file", - change: ts.noop, - timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2), // Scheduling failed lookup update and program update - }, - { - caption: "After program emit with new file, should schedule and run timeout to update directory watcher", - change: ts.noop, - timeouts: ts.tscWatch.checkSingleTimeoutQueueLengthAndRun, // Update the child watch - }, - ts.tscWatch.noopChange, - ], - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchDirectories/with non synchronous watch directory renaming a file", - commandLineArgs: ["--w", "-p", `${ts.tscWatch.projectRoot}/tsconfig.json`], - sys: () => { - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { outDir: "dist" } }) - }; - const file1: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/file1.ts`, - content: `import { x } from "./file2";` - }; - const file2: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/file2.ts`, - content: `export const x = 10;` - }; - const files = [ts.tscWatch.libFile, file1, file2, configFile]; - return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: [ - ts.tscWatch.noopChange, - { - caption: "rename the file", - change: sys => sys.renameFile(`${ts.tscWatch.projectRoot}/src/file2.ts`, `${ts.tscWatch.projectRoot}/src/renamed.ts`), - timeouts: sys => { - sys.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches - sys.runQueuedTimeoutCallbacks(1); // Update program - }, - }, - { - caption: "Pending directory watchers and program update", - change: ts.noop, - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers - sys.checkTimeoutQueueLengthAndRun(2); // To Update program and failed lookup update - sys.checkTimeoutQueueLengthAndRun(1); // Actual program update - sys.checkTimeoutQueueLength(0); - }, - }, - ], - }); - }); - - describe("handles watch compiler options", () => { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchFile option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - watchFile: "UseFsEvents" - } - }) - }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - return ts.tscWatch.createWatchedSystem(files); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchDirectory option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - watchDirectory: "UseFsEvents" - } - }) - }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchOptions/with fallbackPolling option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - fallbackPolling: "PriorityInterval" - } - }) - }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - return ts.tscWatch.createWatchedSystem(files, { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); - }, - changes: ts.emptyArray - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchFile as watch options to extend", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json", "--watchFile", "UseFsEvents"], - sys: () => { - const configFile: ts.tscWatch.File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const files = [ts.tscWatch.libFile, ts.tscWatch.commonFile1, ts.tscWatch.commonFile2, configFile]; - return ts.tscWatch.createWatchedSystem(files); - }, - changes: ts.emptyArray - }); - - describe("exclude options", () => { - function sys(watchOptions: ts.WatchOptions, runWithoutRecursiveWatches?: boolean): ts.tscWatch.WatchedSystem { - const configFile: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ exclude: ["node_modules"], watchOptions }) - }; - const main: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/src/main.ts`, - content: `import { foo } from "bar"; foo();` - }; - const bar: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/bar/index.d.ts`, - content: `export { foo } from "./foo";` - }; - const foo: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, - content: `export function foo(): string;` - }; - const fooBar: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/bar/fooBar.d.ts`, - content: `export function fooBar(): string;` - }; - const temp: ts.tscWatch.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/bar/temp/index.d.ts`, - content: "export function temp(): string;" - }; - const files = [ts.tscWatch.libFile, main, bar, foo, fooBar, temp, configFile]; - return ts.tscWatch.createWatchedSystem(files, { currentDirectory: ts.tscWatch.projectRoot, runWithoutRecursiveWatches }); - } - - function verifyWorker(...additionalFlags: string[]) { - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `watchOptions/with excludeFiles option${additionalFlags.join("")}`, - commandLineArgs: ["-w", ...additionalFlags], - sys: () => sys({ excludeFiles: ["node_modules/*"] }), - changes: [ - { - caption: "Change foo", - change: sys => ts.tscWatch.replaceFileText(sys, `${ts.tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, "foo", "fooBar"), - timeouts: sys => sys.checkTimeoutQueueLength(0), - } - ] - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `watchOptions/with excludeDirectories option${additionalFlags.join("")}`, - commandLineArgs: ["-w", ...additionalFlags], - sys: () => sys({ excludeDirectories: ["node_modules"] }), - changes: [ - { - caption: "delete fooBar", - change: sys => sys.deleteFile(`${ts.tscWatch.projectRoot}/node_modules/bar/fooBar.d.ts`), - timeouts: sys => sys.checkTimeoutQueueLength(0), - } - ] - }); - - ts.tscWatch.verifyTscWatch({ - scenario, - subScenario: `watchOptions/with excludeDirectories option with recursive directory watching${additionalFlags.join("")}`, - commandLineArgs: ["-w", ...additionalFlags], - sys: () => sys({ excludeDirectories: ["**/temp"] }, /*runWithoutRecursiveWatches*/ true), - changes: [ - { - caption: "Directory watch updates because of main.js creation", - change: ts.noop, - timeouts: sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for main.js output - sys.checkTimeoutQueueLength(0); - }, - }, - { - caption: "add new folder to temp", - change: sys => sys.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/node_modules/bar/temp/fooBar/index.d.ts`, content: "export function temp(): string;" }), - timeouts: sys => sys.checkTimeoutQueueLength(0), - } - ] - }); - } - - verifyWorker(); - verifyWorker("-extendedDiagnostics"); - }); + verifyWorker(); + verifyWorker("-extendedDiagnostics"); }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 8eb242e6c8ee1..5240dce4d57c1 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -1,181 +1,181 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: applyChangesToOpenFiles", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/file3.ts", - content: "let xyz = 1;" - }; - const app: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let z = 1;" - }; +describe("unittests:: tsserver:: applyChangesToOpenFiles", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/file3.ts", + content: "let xyz = 1;" + }; + const app: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let z = 1;" + }; - function fileContentWithComment(file: ts.projectSystem.File) { - return `// some copy right notice + function fileContentWithComment(file: ts.projectSystem.File) { + return `// some copy right notice ${file.content}`; - } + } - function verifyText(service: ts.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 verifyText(service: ts.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: ts.server.Project, expected: number) { - assert.equal(Number(project.getProjectVersion()), expected); - } + function verifyProjectVersion(project: ts.server.Project, expected: number) { + assert.equal(Number(project.getProjectVersion()), expected); + } - interface Verify { - applyChangesToOpen: (session: ts.projectSystem.TestSession) => void; - openFile1Again: (session: ts.projectSystem.TestSession) => void; - } - function verify({ applyChangesToOpen, openFile1Again }: Verify) { - const host = ts.projectSystem.createServerHost([app, file3, ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host); - session.executeCommandSeq({ - command: ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.Open, - arguments: { - file: file3.path, - fileContent: fileContentWithComment(file3) - } - }); - verifyProjectVersion(project, 2); + interface Verify { + applyChangesToOpen: (session: ts.projectSystem.TestSession) => void; + openFile1Again: (session: ts.projectSystem.TestSession) => void; + } + function verify({ applyChangesToOpen, openFile1Again }: Verify) { + const host = ts.projectSystem.createServerHost([app, file3, ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host); + session.executeCommandSeq({ + command: ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.Open, + arguments: { + file: file3.path, + fileContent: fileContentWithComment(file3) + } + }); + verifyProjectVersion(project, 2); - // Verify Texts - verifyText(service, ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile1.content); - verifyText(service, ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); - verifyText(service, app.path, app.content); - verifyText(service, file3.path, fileContentWithComment(file3)); + // Verify Texts + verifyText(service, ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile1.content); + verifyText(service, ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); + verifyText(service, app.path, app.content); + verifyText(service, file3.path, fileContentWithComment(file3)); - // Apply changes - applyChangesToOpen(session); + // Apply changes + applyChangesToOpen(session); - // Verify again - verifyProjectVersion(project, 3); - // Open file contents - verifyText(service, ts.projectSystem.commonFile1.path, fileContentWithComment(ts.projectSystem.commonFile1)); - verifyText(service, ts.projectSystem.commonFile2.path, fileContentWithComment(ts.projectSystem.commonFile2)); - verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); - verifyText(service, file3.path, file3.content); + // Verify again + verifyProjectVersion(project, 3); + // Open file contents + verifyText(service, ts.projectSystem.commonFile1.path, fileContentWithComment(ts.projectSystem.commonFile1)); + verifyText(service, ts.projectSystem.commonFile2.path, fileContentWithComment(ts.projectSystem.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(ts.projectSystem.commonFile1.path)!.isScriptOpen()); + // Open file1 again + openFile1Again(session); + assert.isTrue(service.getScriptInfo(ts.projectSystem.commonFile1.path)!.isScriptOpen()); - // Verify that file1 contents are changed - verifyProjectVersion(project, 4); - verifyText(service, ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile1.content); - verifyText(service, ts.projectSystem.commonFile2.path, fileContentWithComment(ts.projectSystem.commonFile2)); - verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); - verifyText(service, file3.path, file3.content); - } + // Verify that file1 contents are changed + verifyProjectVersion(project, 4); + verifyText(service, ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile1.content); + verifyText(service, ts.projectSystem.commonFile2.path, fileContentWithComment(ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, - arguments: { - openFiles: [ - { - fileName: ts.projectSystem.commonFile1.path, - content: fileContentWithComment(ts.projectSystem.commonFile1) - }, - { - fileName: ts.projectSystem.commonFile2.path, - content: fileContentWithComment(ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, - arguments: { - openFiles: [{ - fileName: ts.projectSystem.commonFile1.path, - content: ts.projectSystem.commonFile1.content - }] - } - }), - }); + it("with applyChangedToOpenFiles request", () => { + verify({ + applyChangesToOpen: session => session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, + arguments: { + openFiles: [ + { + fileName: ts.projectSystem.commonFile1.path, + content: fileContentWithComment(ts.projectSystem.commonFile1) + }, + { + fileName: ts.projectSystem.commonFile2.path, + content: fileContentWithComment(ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, + arguments: { + openFiles: [{ + fileName: ts.projectSystem.commonFile1.path, + content: ts.projectSystem.commonFile1.content + }] + } + }), }); + }); - it("with updateOpen request", () => { - verify({ - applyChangesToOpen: session => session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - openFiles: [ - { - file: ts.projectSystem.commonFile1.path, - fileContent: fileContentWithComment(ts.projectSystem.commonFile1) - }, - { - file: ts.projectSystem.commonFile2.path, - fileContent: fileContentWithComment(ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - openFiles: [{ - file: ts.projectSystem.commonFile1.path, - fileContent: ts.projectSystem.commonFile1.content - }] - } - }), - }); + it("with updateOpen request", () => { + verify({ + applyChangesToOpen: session => session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [ + { + file: ts.projectSystem.commonFile1.path, + fileContent: fileContentWithComment(ts.projectSystem.commonFile1) + }, + { + file: ts.projectSystem.commonFile2.path, + fileContent: fileContentWithComment(ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [{ + file: ts.projectSystem.commonFile1.path, + fileContent: ts.projectSystem.commonFile1.content + }] + } + }), }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/autoImportProvider.ts b/src/testRunner/unittests/tsserver/autoImportProvider.ts index b7edce1e0a602..c02736ca0ced9 100644 --- a/src/testRunner/unittests/tsserver/autoImportProvider.ts +++ b/src/testRunner/unittests/tsserver/autoImportProvider.ts @@ -1,343 +1,343 @@ namespace ts.projectSystem { - const angularFormsDts: ts.projectSystem.File = { - path: "/node_modules/@angular/forms/forms.d.ts", - content: "export declare class PatternValidator {}", - }; - const angularFormsPackageJson: ts.projectSystem.File = { - path: "/node_modules/@angular/forms/package.json", - content: `{ "name": "@angular/forms", "typings": "./forms.d.ts" }`, - }; - const angularCoreDts: ts.projectSystem.File = { - path: "/node_modules/@angular/core/core.d.ts", - content: "", - }; - const angularCorePackageJson: ts.projectSystem.File = { - path: "/node_modules/@angular/core/package.json", - content: `{ "name": "@angular/core", "typings": "./core.d.ts" }`, - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: `{ "compilerOptions": { "module": "commonjs" } }`, - }; - const packageJson: ts.projectSystem.File = { - path: "/package.json", - content: `{ "dependencies": { "@angular/forms": "*", "@angular/core": "*" } }` - }; - const indexTs: ts.projectSystem.File = { - path: "/index.ts", - content: "" - }; +const angularFormsDts: ts.projectSystem.File = { + path: "/node_modules/@angular/forms/forms.d.ts", + content: "export declare class PatternValidator {}", +}; +const angularFormsPackageJson: ts.projectSystem.File = { + path: "/node_modules/@angular/forms/package.json", + content: `{ "name": "@angular/forms", "typings": "./forms.d.ts" }`, +}; +const angularCoreDts: ts.projectSystem.File = { + path: "/node_modules/@angular/core/core.d.ts", + content: "", +}; +const angularCorePackageJson: ts.projectSystem.File = { + path: "/node_modules/@angular/core/package.json", + content: `{ "name": "@angular/core", "typings": "./core.d.ts" }`, +}; +const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: `{ "compilerOptions": { "module": "commonjs" } }`, +}; +const packageJson: ts.projectSystem.File = { + path: "/package.json", + content: `{ "dependencies": { "@angular/forms": "*", "@angular/core": "*" } }` +}; +const indexTs: ts.projectSystem.File = { + path: "/index.ts", + content: "" +}; + +describe("unittests:: tsserver:: autoImportProvider", () => { + it("Auto import provider program is not created without dependencies listed in package.json", () => { + const { projectService, session } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + { path: packageJson.path, content: `{ "dependencies": {} }` }, + indexTs + ]); + ts.projectSystem.openFilesForSession([indexTs], session); + assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + }); - describe("unittests:: tsserver:: autoImportProvider", () => { - it("Auto import provider program is not created without dependencies listed in package.json", () => { - const { projectService, session } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - { path: packageJson.path, content: `{ "dependencies": {} }` }, - indexTs - ]); - ts.projectSystem.openFilesForSession([indexTs], session); - assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - }); + it("Auto import provider program is not created if dependencies are already in main program", () => { + const { projectService, session } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + packageJson, + { path: indexTs.path, content: "import '@angular/forms';" } + ]); + ts.projectSystem.openFilesForSession([indexTs], session); + assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + }); - it("Auto import provider program is not created if dependencies are already in main program", () => { - const { projectService, session } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - packageJson, - { path: indexTs.path, content: "import '@angular/forms';" } - ]); - ts.projectSystem.openFilesForSession([indexTs], session); - assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - }); + it("Auto-import program is not created for projects already inside node_modules", () => { + // Simulate browsing typings files inside node_modules: no point creating auto import program + // for the InferredProject that gets created in there. + const { projectService, session } = setup([ + angularFormsDts, + { path: angularFormsPackageJson.path, content: `{ "dependencies": { "@angular/core": "*" } }` }, + { path: "/node_modules/@angular/core/package.json", content: `{ "typings": "./core.d.ts" }` }, + { path: "/node_modules/@angular/core/core.d.ts", content: `export namespace angular {};` }, + ]); + + ts.projectSystem.openFilesForSession([angularFormsDts], session); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); + assert.isUndefined(projectService + .getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)! + .getLanguageService() + .getAutoImportProvider()); + }); - it("Auto-import program is not created for projects already inside node_modules", () => { - // Simulate browsing typings files inside node_modules: no point creating auto import program - // for the InferredProject that gets created in there. - const { projectService, session } = setup([ - angularFormsDts, - { path: angularFormsPackageJson.path, content: `{ "dependencies": { "@angular/core": "*" } }` }, - { path: "/node_modules/@angular/core/package.json", content: `{ "typings": "./core.d.ts" }` }, - { path: "/node_modules/@angular/core/core.d.ts", content: `export namespace angular {};` }, - ]); - - ts.projectSystem.openFilesForSession([angularFormsDts], session); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); - assert.isUndefined(projectService - .getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)! - .getLanguageService() - .getAutoImportProvider()); - }); + it("Auto-importable file is in inferred project until imported", () => { + const { projectService, session, updateFile } = setup([angularFormsDts, angularFormsPackageJson, tsconfig, packageJson, indexTs]); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + ts.projectSystem.openFilesForSession([angularFormsDts], session); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + assert.equal(projectService.getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)?.projectKind, ts.server.ProjectKind.Inferred); - it("Auto-importable file is in inferred project until imported", () => { - const { projectService, session, updateFile } = setup([angularFormsDts, angularFormsPackageJson, tsconfig, packageJson, indexTs]); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - ts.projectSystem.openFilesForSession([angularFormsDts], session); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - assert.equal(projectService.getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)?.projectKind, ts.server.ProjectKind.Inferred); + updateFile(indexTs.path, "import '@angular/forms'"); + assert.equal(projectService.getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)?.projectKind, ts.server.ProjectKind.Configured); - updateFile(indexTs.path, "import '@angular/forms'"); - assert.equal(projectService.getDefaultProjectForFile(angularFormsDts.path as ts.server.NormalizedPath, /*ensureProject*/ true)?.projectKind, ts.server.ProjectKind.Configured); + assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + }); - assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - }); + it("Responds to package.json changes", () => { + const { projectService, session, host } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + { path: "/package.json", content: "{}" }, + indexTs + ]); - it("Responds to package.json changes", () => { - const { projectService, session, host } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - { path: "/package.json", content: "{}" }, - indexTs - ]); + ts.projectSystem.openFilesForSession([indexTs], session); + assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - ts.projectSystem.openFilesForSession([indexTs], session); - assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + host.writeFile(packageJson.path, packageJson.content); + assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + }); - host.writeFile(packageJson.path, packageJson.content); - assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - }); + it("Reuses autoImportProvider when program structure is unchanged", () => { + const { projectService, session, updateFile } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + packageJson, + indexTs + ]); + + ts.projectSystem.openFilesForSession([indexTs], session); + const autoImportProvider = projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider(); + assert.ok(autoImportProvider); + + updateFile(indexTs.path, "console.log(0)"); + assert.strictEqual(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider(), autoImportProvider); + }); - it("Reuses autoImportProvider when program structure is unchanged", () => { - const { projectService, session, updateFile } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - packageJson, - indexTs - ]); - - ts.projectSystem.openFilesForSession([indexTs], session); - const autoImportProvider = projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider(); - assert.ok(autoImportProvider); - - updateFile(indexTs.path, "console.log(0)"); - assert.strictEqual(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider(), autoImportProvider); - }); + it("Closes AutoImportProviderProject when host project closes", () => { + const { projectService, session } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + packageJson, + indexTs + ]); + + ts.projectSystem.openFilesForSession([indexTs], session); + const hostProject = projectService.configuredProjects.get(tsconfig.path)!; + hostProject.getPackageJsonAutoImportProvider(); + const autoImportProviderProject = hostProject.autoImportProviderHost; + assert.ok(autoImportProviderProject); + + hostProject.close(); + assert.ok(autoImportProviderProject && autoImportProviderProject.isClosed()); + assert.isUndefined(hostProject.autoImportProviderHost); + }); - it("Closes AutoImportProviderProject when host project closes", () => { - const { projectService, session } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - packageJson, - indexTs - ]); - - ts.projectSystem.openFilesForSession([indexTs], session); - const hostProject = projectService.configuredProjects.get(tsconfig.path)!; - hostProject.getPackageJsonAutoImportProvider(); - const autoImportProviderProject = hostProject.autoImportProviderHost; - assert.ok(autoImportProviderProject); - - hostProject.close(); - assert.ok(autoImportProviderProject && autoImportProviderProject.isClosed()); - assert.isUndefined(hostProject.autoImportProviderHost); - }); + it("Does not schedule ensureProjectForOpenFiles on AutoImportProviderProject creation", () => { + const { projectService, session, host } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + indexTs + ]); + + // Create configured project only, ensure !projectService.pendingEnsureProjectForOpenFiles + ts.projectSystem.openFilesForSession([indexTs], session); + const hostProject = projectService.configuredProjects.get(tsconfig.path)!; + projectService.delayEnsureProjectForOpenFiles(); + host.runQueuedTimeoutCallbacks(); + assert.isFalse(projectService.pendingEnsureProjectForOpenFiles); + + // Create auto import provider project, ensure still !projectService.pendingEnsureProjectForOpenFiles + host.writeFile(packageJson.path, packageJson.content); + hostProject.getPackageJsonAutoImportProvider(); + assert.isFalse(projectService.pendingEnsureProjectForOpenFiles); + }); - it("Does not schedule ensureProjectForOpenFiles on AutoImportProviderProject creation", () => { - const { projectService, session, host } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - indexTs - ]); - - // Create configured project only, ensure !projectService.pendingEnsureProjectForOpenFiles - ts.projectSystem.openFilesForSession([indexTs], session); - const hostProject = projectService.configuredProjects.get(tsconfig.path)!; - projectService.delayEnsureProjectForOpenFiles(); - host.runQueuedTimeoutCallbacks(); - assert.isFalse(projectService.pendingEnsureProjectForOpenFiles); - - // Create auto import provider project, ensure still !projectService.pendingEnsureProjectForOpenFiles - host.writeFile(packageJson.path, packageJson.content); - hostProject.getPackageJsonAutoImportProvider(); - assert.isFalse(projectService.pendingEnsureProjectForOpenFiles); - }); + it("Responds to automatic changes in node_modules", () => { + const { projectService, session, host } = setup([ + angularFormsDts, + angularFormsPackageJson, + angularCoreDts, + angularCorePackageJson, + tsconfig, + packageJson, + indexTs + ]); + + ts.projectSystem.openFilesForSession([indexTs], session); + const project = projectService.configuredProjects.get(tsconfig.path)!; + const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); + assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator")); + + // Directory watchers only fire for add/remove, not change. + // This is ok since a real `npm install` will always trigger add/remove events. + host.deleteFile(angularFormsDts.path); + host.writeFile(angularFormsDts.path, ""); + + const autoImportProvider = project.getLanguageService().getAutoImportProvider(); + const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); + assert.equal(autoImportProvider!.getSourceFile(angularFormsDts.path)!.getText(), ""); + assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator")); + }); - it("Responds to automatic changes in node_modules", () => { - const { projectService, session, host } = setup([ - angularFormsDts, - angularFormsPackageJson, - angularCoreDts, - angularCorePackageJson, - tsconfig, - packageJson, - indexTs - ]); - - ts.projectSystem.openFilesForSession([indexTs], session); - const project = projectService.configuredProjects.get(tsconfig.path)!; - const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); - assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator")); - - // Directory watchers only fire for add/remove, not change. - // This is ok since a real `npm install` will always trigger add/remove events. - host.deleteFile(angularFormsDts.path); - host.writeFile(angularFormsDts.path, ""); - - const autoImportProvider = project.getLanguageService().getAutoImportProvider(); - const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); - assert.equal(autoImportProvider!.getSourceFile(angularFormsDts.path)!.getText(), ""); - assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator")); - }); + it("Responds to manual changes in node_modules", () => { + const { projectService, session, updateFile } = setup([ + angularFormsDts, + angularFormsPackageJson, + angularCoreDts, + angularCorePackageJson, + tsconfig, + packageJson, + indexTs + ]); + + ts.projectSystem.openFilesForSession([indexTs, angularFormsDts], session); + const project = projectService.configuredProjects.get(tsconfig.path)!; + const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); + assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator")); + + updateFile(angularFormsDts.path, "export class ValidatorPattern {}"); + const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); + assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator")); + assert.isTrue(completionsAfter?.entries.some(c => c.name === "ValidatorPattern")); + }); - it("Responds to manual changes in node_modules", () => { - const { projectService, session, updateFile } = setup([ - angularFormsDts, - angularFormsPackageJson, - angularCoreDts, - angularCorePackageJson, - tsconfig, - packageJson, - indexTs - ]); - - ts.projectSystem.openFilesForSession([indexTs, angularFormsDts], session); - const project = projectService.configuredProjects.get(tsconfig.path)!; - const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); - assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator")); - - updateFile(angularFormsDts.path, "export class ValidatorPattern {}"); - const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true }); - assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator")); - assert.isTrue(completionsAfter?.entries.some(c => c.name === "ValidatorPattern")); - }); + it("Recovers from an unparseable package.json", () => { + const { projectService, session, host } = setup([ + angularFormsDts, + angularFormsPackageJson, + tsconfig, + { path: packageJson.path, content: "{" }, + indexTs + ]); - it("Recovers from an unparseable package.json", () => { - const { projectService, session, host } = setup([ - angularFormsDts, - angularFormsPackageJson, - tsconfig, - { path: packageJson.path, content: "{" }, - indexTs - ]); + ts.projectSystem.openFilesForSession([indexTs], session); + assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - ts.projectSystem.openFilesForSession([indexTs], session); - assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + host.writeFile(packageJson.path, packageJson.content); + assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); + }); - host.writeFile(packageJson.path, packageJson.content); - assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider()); - }); + it("Does not create an auto import provider if there are too many dependencies", () => { + const createPackage = (i: number): ts.projectSystem.File[] => ([ + { path: `/node_modules/package${i}/package.json`, content: `{ "name": "package${i}" }` }, + { path: `/node_modules/package${i}/index.d.ts`, content: `` } + ]); - it("Does not create an auto import provider if there are too many dependencies", () => { - const createPackage = (i: number): ts.projectSystem.File[] => ([ - { path: `/node_modules/package${i}/package.json`, content: `{ "name": "package${i}" }` }, - { path: `/node_modules/package${i}/index.d.ts`, content: `` } - ]); + const packages = []; + for (let i = 0; i < 11; i++) { + packages.push(createPackage(i)); + } - const packages = []; - for (let i = 0; i < 11; i++) { - packages.push(createPackage(i)); - } + const dependencies = packages.reduce((hash, p) => ({ ...hash, [JSON.parse(p[0].content).name]: "*" }), {}); + const packageJson: ts.projectSystem.File = { path: "/package.json", content: JSON.stringify(dependencies) }; + const { projectService, session } = setup([...ts.flatten(packages), indexTs, tsconfig, packageJson]); + ts.projectSystem.openFilesForSession([indexTs], session); + const project = projectService.configuredProjects.get(tsconfig.path)!; + assert.isUndefined(project.getPackageJsonAutoImportProvider()); + }); +}); + +describe("unittests:: tsserver:: autoImportProvider - monorepo", () => { + it("Does not create auto import providers upon opening projects for find-all-references", () => { + const files = [ + // node_modules + angularFormsDts, + angularFormsPackageJson, + + // root + { path: tsconfig.path, content: `{ "references": [{ "path": "packages/a" }, { "path": "packages/b" }] }` }, + { path: packageJson.path, content: `{ "private": true }` }, + + // packages/a + { path: "/packages/a/package.json", content: packageJson.content }, + { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "../b" }] }` }, + { path: "/packages/a/index.ts", content: "import { B } from '../b';" }, + + // packages/b + { path: "/packages/b/package.json", content: packageJson.content }, + { path: "/packages/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true } }` }, + { path: "/packages/b/index.ts", content: `export class B {}` } + ]; + + const { projectService, session, findAllReferences } = setup(files); + + ts.projectSystem.openFilesForSession([files.find(f => f.path === "/packages/b/index.ts")!], session); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); // Solution (no files), B + findAllReferences("/packages/b/index.ts", 1, "export class B".length - 1); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 3); // Solution (no files), A, B + + // Project for A is created - ensure it doesn't have an autoImportProvider + assert.isUndefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getLanguageService().getAutoImportProvider()); + }); - const dependencies = packages.reduce((hash, p) => ({ ...hash, [JSON.parse(p[0].content).name]: "*" }), {}); - const packageJson: ts.projectSystem.File = { path: "/package.json", content: JSON.stringify(dependencies) }; - const { projectService, session } = setup([...ts.flatten(packages), indexTs, tsconfig, packageJson]); - ts.projectSystem.openFilesForSession([indexTs], session); - const project = projectService.configuredProjects.get(tsconfig.path)!; - assert.isUndefined(project.getPackageJsonAutoImportProvider()); - }); + it("Does not close when root files are redirects that don't actually exist", () => { + const files = [ + // packages/a + { path: "/packages/a/package.json", content: `{ "dependencies": { "b": "*" } }` }, + { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "./node_modules/b" }] }` }, + { path: "/packages/a/index.ts", content: "" }, + + // packages/b + { path: "/packages/a/node_modules/b/package.json", content: `{ "types": "dist/index.d.ts" }` }, + { path: "/packages/a/node_modules/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true, "outDir": "dist" } }` }, + { path: "/packages/a/node_modules/b/index.ts", content: `export class B {}` } + ]; + + const { projectService, session } = setup(files); + ts.projectSystem.openFilesForSession([files[2]], session); + assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider()); + assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider()); }); - describe("unittests:: tsserver:: autoImportProvider - monorepo", () => { - it("Does not create auto import providers upon opening projects for find-all-references", () => { - const files = [ - // node_modules - angularFormsDts, - angularFormsPackageJson, - - // root - { path: tsconfig.path, content: `{ "references": [{ "path": "packages/a" }, { "path": "packages/b" }] }` }, - { path: packageJson.path, content: `{ "private": true }` }, - - // packages/a - { path: "/packages/a/package.json", content: packageJson.content }, - { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "../b" }] }` }, - { path: "/packages/a/index.ts", content: "import { B } from '../b';" }, - - // packages/b - { path: "/packages/b/package.json", content: packageJson.content }, - { path: "/packages/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true } }` }, - { path: "/packages/b/index.ts", content: `export class B {}` } - ]; - - const { projectService, session, findAllReferences } = setup(files); - - ts.projectSystem.openFilesForSession([files.find(f => f.path === "/packages/b/index.ts")!], session); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); // Solution (no files), B - findAllReferences("/packages/b/index.ts", 1, "export class B".length - 1); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 3); // Solution (no files), A, B - - // Project for A is created - ensure it doesn't have an autoImportProvider - assert.isUndefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getLanguageService().getAutoImportProvider()); - }); + it("Can use the same document registry bucket key as main program", () => { + for (const option of ts.sourceFileAffectingCompilerOptions) { + assert(!ts.hasProperty(ts.server.AutoImportProviderProject.compilerOptionsOverrides, option.name), `'${option.name}' may cause AutoImportProviderProject not to share source files with main program`); + } + }); +}); + +function setup(files: ts.projectSystem.File[]) { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + return { + host, + projectService, + session, + updateFile, + findAllReferences + }; - it("Does not close when root files are redirects that don't actually exist", () => { - const files = [ - // packages/a - { path: "/packages/a/package.json", content: `{ "dependencies": { "b": "*" } }` }, - { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "./node_modules/b" }] }` }, - { path: "/packages/a/index.ts", content: "" }, - - // packages/b - { path: "/packages/a/node_modules/b/package.json", content: `{ "types": "dist/index.d.ts" }` }, - { path: "/packages/a/node_modules/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true, "outDir": "dist" } }` }, - { path: "/packages/a/node_modules/b/index.ts", content: `export class B {}` } - ]; - - const { projectService, session } = setup(files); - ts.projectSystem.openFilesForSession([files[2]], session); - assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider()); - assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider()); + function updateFile(path: string, newText: string) { + ts.Debug.assertIsDefined(files.find(f => f.path === path)); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, + arguments: { + openFiles: [{ + fileName: path, + content: newText + }] + } }); + } - it("Can use the same document registry bucket key as main program", () => { - for (const option of ts.sourceFileAffectingCompilerOptions) { - assert(!ts.hasProperty(ts.server.AutoImportProviderProject.compilerOptionsOverrides, option.name), `'${option.name}' may cause AutoImportProviderProject not to share source files with main program`); + function findAllReferences(file: string, line: number, offset: number) { + ts.Debug.assertIsDefined(files.find(f => f.path === file)); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: { + file, + line, + offset } }); - }); - - function setup(files: ts.projectSystem.File[]) { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - return { - host, - projectService, - session, - updateFile, - findAllReferences - }; - - function updateFile(path: string, newText: string) { - ts.Debug.assertIsDefined(files.find(f => f.path === path)); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.ApplyChangedToOpenFiles, - arguments: { - openFiles: [{ - fileName: path, - content: newText - }] - } - }); - } - - function findAllReferences(file: string, line: number, offset: number) { - ts.Debug.assertIsDefined(files.find(f => f.path === file)); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: { - file, - line, - offset - } - }); - } } } +} diff --git a/src/testRunner/unittests/tsserver/auxiliaryProject.ts b/src/testRunner/unittests/tsserver/auxiliaryProject.ts index 1dc1d3c9a5320..bf599f9caee08 100644 --- a/src/testRunner/unittests/tsserver/auxiliaryProject.ts +++ b/src/testRunner/unittests/tsserver/auxiliaryProject.ts @@ -1,47 +1,47 @@ namespace ts.projectSystem { - const aTs: ts.projectSystem.File = { - path: "/a.ts", - content: `import { B } from "./b";` - }; - const bDts: ts.projectSystem.File = { - path: "/b.d.ts", - content: `export declare class B {}` - }; - const bJs: ts.projectSystem.File = { - path: "/b.js", - content: `export class B {}` - }; - describe("unittests:: tsserver:: auxiliaryProject", () => { - it("AuxiliaryProject does not remove scrips from InferredProject", () => { - const host = ts.projectSystem.createServerHost([aTs, bDts, bJs]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - ts.projectSystem.openFilesForSession([aTs], session); +const aTs: ts.projectSystem.File = { + path: "/a.ts", + content: `import { B } from "./b";` +}; +const bDts: ts.projectSystem.File = { + path: "/b.d.ts", + content: `export declare class B {}` +}; +const bJs: ts.projectSystem.File = { + path: "/b.js", + content: `export class B {}` +}; +describe("unittests:: tsserver:: auxiliaryProject", () => { + it("AuxiliaryProject does not remove scrips from InferredProject", () => { + const host = ts.projectSystem.createServerHost([aTs, bDts, bJs]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + ts.projectSystem.openFilesForSession([aTs], session); - // Open file is in inferred project - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - const inferredProject = projectService.inferredProjects[0]; + // Open file is in inferred project + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + const inferredProject = projectService.inferredProjects[0]; - // getNoDtsResolutionProject will create an AuxiliaryProject with a.ts and b.js - const auxProject = inferredProject.getNoDtsResolutionProject([aTs.path]); - auxProject.updateGraph(); + // getNoDtsResolutionProject will create an AuxiliaryProject with a.ts and b.js + const auxProject = inferredProject.getNoDtsResolutionProject([aTs.path]); + auxProject.updateGraph(); - // b.js ScriptInfo is now available because it's contained by the AuxiliaryProject. - // The AuxiliaryProject should never be the default project for anything, so - // the ScriptInfo should still report being an orphan, and getting its default - // project should throw. - const bJsScriptInfo = ts.Debug.checkDefined(projectService.getScriptInfo(bJs.path)); - assert(bJsScriptInfo.isOrphan()); - assert(bJsScriptInfo.isContainedByBackgroundProject()); - assert.deepEqual(bJsScriptInfo.containingProjects, [auxProject]); - assert.throws(() => bJsScriptInfo.getDefaultProject()); + // b.js ScriptInfo is now available because it's contained by the AuxiliaryProject. + // The AuxiliaryProject should never be the default project for anything, so + // the ScriptInfo should still report being an orphan, and getting its default + // project should throw. + const bJsScriptInfo = ts.Debug.checkDefined(projectService.getScriptInfo(bJs.path)); + assert(bJsScriptInfo.isOrphan()); + assert(bJsScriptInfo.isContainedByBackgroundProject()); + assert.deepEqual(bJsScriptInfo.containingProjects, [auxProject]); + assert.throws(() => bJsScriptInfo.getDefaultProject()); - // When b.js is opened in the editor, it should be put into an InferredProject - // even though it's still contained by the AuxiliaryProject. - ts.projectSystem.openFilesForSession([bJs], session); - assert(!bJsScriptInfo.isOrphan()); - assert(bJsScriptInfo.isContainedByBackgroundProject()); - assert.equal(bJsScriptInfo.getDefaultProject().projectKind, ts.server.ProjectKind.Inferred); - }); + // When b.js is opened in the editor, it should be put into an InferredProject + // even though it's still contained by the AuxiliaryProject. + ts.projectSystem.openFilesForSession([bJs], session); + assert(!bJsScriptInfo.isOrphan()); + assert(bJsScriptInfo.isContainedByBackgroundProject()); + assert.equal(bJsScriptInfo.getDefaultProject().projectKind, ts.server.ProjectKind.Inferred); }); +}); } diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index 03526a6d4eb07..d178e4b1e6afd 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -1,524 +1,524 @@ namespace ts.projectSystem { - function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { - return ts.countWhere(recursiveWatchedDirs, dir => file.length > dir.length && ts.startsWith(file, dir) && file[dir.length] === ts.directorySeparator); - } +function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + return ts.countWhere(recursiveWatchedDirs, dir => file.length > dir.length && ts.startsWith(file, dir) && file[dir.length] === ts.directorySeparator); +} - 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: ts.projectSystem.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) +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: ts.projectSystem.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 = ts.createMultiMap(); + const cb = (host as any)[prop].bind(host); + (host as any)[prop] = (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); }; + return calledMap; + } - return { - verifyNoCall, - verifyCalledOnEachEntryNTimes, - verifyCalledOnEachEntry, - verifyNoHostCalls, - verifyNoHostCallsExceptFileExistsOnce, - verifyCalledOn, - clear + function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { + const calledMap = ts.createMultiMap<[ + U, + V, + W, + X + ]>(); + const cb = (host as any)[prop].bind(host); + (host as any)[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 setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { - const calledMap = ts.createMultiMap(); - const cb = (host as any)[prop].bind(host); - (host as any)[prop] = (f: string) => { - calledMap.add(f, /*value*/ true); - return cb(f); - }; - return calledMap; - } - - function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { - const calledMap = ts.createMultiMap<[ - U, - V, - W, - X - ]>(); - const cb = (host as any)[prop].bind(host); - (host as any)[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}: ${ts.arrayFrom(calledMap.keys())}`); + } - 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}: ${ts.arrayFrom(calledMap.keys())}`); - } + function verifyNoCall(callback: CalledMaps) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${ts.arrayFrom(calledMap.keys())}`); + } - function verifyNoCall(callback: CalledMaps) { - const calledMap = calledMaps[callback]; - assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${ts.arrayFrom(calledMap.keys())}`); - } + function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: ts.ESMap) { + ts.TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys); + } - function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: ts.ESMap) { - ts.TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys); - } + function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: readonly string[], nTimes: number) { + ts.TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys, nTimes); + } - function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: readonly string[], nTimes: number) { - ts.TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys, nTimes); - } + function verifyNoHostCalls() { + iterateOnCalledMaps(key => verifyNoCall(key)); + } - 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 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 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: ts.projectSystem.File = { - path: "/c/d/f0.ts", - content: rootContent - }; - - const imported: ts.projectSystem.File = { - path: "/c/f1.ts", - content: `foo()` - }; - - const host = ts.projectSystem.createServerHost([root, imported]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); - projectService.openClientFile(root.path); - ts.projectSystem.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: ts.projectSystem.File = { + path: "/c/d/f0.ts", + content: rootContent + }; + + const imported: ts.projectSystem.File = { + path: "/c/f1.ts", + content: `foo()` + }; + + const host = ts.projectSystem.createServerHost([root, imported]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); + projectService.openClientFile(root.path); + ts.projectSystem.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", ...ts.projectSystem.mapCombinedPathsInAncestor(ts.getDirectoryPath(root.path), ts.projectSystem.nodeModulesAtTypes, ts.returnTrue)]; + vertifyF1Lookups(); + + // setting compiler options discards module resolution cache + callsTrackingHost.clear(); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true, target: ts.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", ...ts.projectSystem.mapCombinedPathsInAncestor(ts.getDirectoryPath(root.path), ts.projectSystem.nodeModulesAtTypes, ts.returnTrue)]; - vertifyF1Lookups(); - - // setting compiler options discards module resolution cache + function editContent(newContent: string) { callsTrackingHost.clear(); - projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true, target: ts.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, ts.Diagnostics.Cannot_find_name_0.code); - assert.equal(ts.flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); - } - - function getLocationsForModuleLookup(module: string) { - const locations: string[] = []; - ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { - locations.push(ts.combinePaths(ancestor, `${module}.ts`), ts.combinePaths(ancestor, `${module}.tsx`), ts.combinePaths(ancestor, `${module}.d.ts`)); - }); - ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { - locations.push(ts.combinePaths(ancestor, `${module}.js`), ts.combinePaths(ancestor, `${module}.jsx`)); - }); - return locations; - } - - function getLocationsForDirectoryLookup() { - const result = new ts.Map(); - ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { - // To resolve modules - result.set(ancestor, 2); - // for type roots - result.set(ts.combinePaths(ancestor, ts.projectSystem.nodeModules), 1); - result.set(ts.combinePaths(ancestor, ts.projectSystem.nodeModulesAtTypes), 1); - }); - return result; - } - }); - - it("loads missing files from disk", () => { - const root: ts.projectSystem.File = { - path: "/c/foo.ts", - content: `import {y} from "bar"` - }; - - const imported: ts.projectSystem.File = { - path: "/c/bar.d.ts", - content: `export var y = 1` - }; - - const host = ts.projectSystem.createServerHost([root]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); - const callsTrackingHost = createCallsTrackingHost(host); - projectService.openClientFile(root.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - const rootScriptInfo = project.getRootScriptInfos()[0]; - assert.equal(rootScriptInfo.fileName, root.path); + rootScriptInfo.editContent(0, rootContent.length, newContent); + rootContent = newContent; + } - let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + function verifyImportedDiagnostics() { + const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); assert.equal(diags.length, 1); const diag = diags[0]; - assert.equal(diag.code, ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option.code); - assert.equal(ts.flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?"); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + assert.equal(diag.code, ts.Diagnostics.Cannot_find_name_0.code); + assert.equal(ts.flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); + } + function getLocationsForModuleLookup(module: string) { + const locations: string[] = []; + ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { + locations.push(ts.combinePaths(ancestor, `${module}.ts`), ts.combinePaths(ancestor, `${module}.tsx`), ts.combinePaths(ancestor, `${module}.d.ts`)); + }); + ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { + locations.push(ts.combinePaths(ancestor, `${module}.js`), ts.combinePaths(ancestor, `${module}.jsx`)); + }); + return locations; + } - callsTrackingHost.clear(); - host.writeFile(imported.path, imported.content); - host.runQueuedTimeoutCallbacks(); - diags = project.getLanguageService().getSemanticDiagnostics(root.path); - assert.equal(diags.length, 0); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); - }); + function getLocationsForDirectoryLookup() { + const result = new ts.Map(); + ts.forEachAncestorDirectory(ts.getDirectoryPath(root.path), ancestor => { + // To resolve modules + result.set(ancestor, 2); + // for type roots + result.set(ts.combinePaths(ancestor, ts.projectSystem.nodeModules), 1); + result.set(ts.combinePaths(ancestor, ts.projectSystem.nodeModulesAtTypes), 1); + }); + return result; + } + }); - it("when calling goto definition of module", () => { - const clientFile: ts.projectSystem.File = { - path: "/a/b/controllers/vessels/client.ts", - content: ` + it("loads missing files from disk", () => { + const root: ts.projectSystem.File = { + path: "/c/foo.ts", + content: `import {y} from "bar"` + }; + + const imported: ts.projectSystem.File = { + path: "/c/bar.d.ts", + content: `export var y = 1` + }; + + const host = ts.projectSystem.createServerHost([root]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); + const callsTrackingHost = createCallsTrackingHost(host); + projectService.openClientFile(root.path); + ts.projectSystem.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, ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option.code); + assert.equal(ts.flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?"); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + + + callsTrackingHost.clear(); + host.writeFile(imported.path, imported.content); + 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: ts.projectSystem.File = { + path: "/a/b/controllers/vessels/client.ts", + content: ` import { Vessel } from '~/models/vessel'; const v = new Vessel(); ` - }; - const anotherModuleFile: ts.projectSystem.File = { - path: "/a/b/utils/db.ts", - content: "export class Bookshelf { }" - }; - const moduleFile: ts.projectSystem.File = { - path: "/a/b/models/vessel.ts", - content: ` + }; + const anotherModuleFile: ts.projectSystem.File = { + path: "/a/b/utils/db.ts", + content: "export class Bookshelf { }" + }; + const moduleFile: ts.projectSystem.File = { + path: "/a/b/models/vessel.ts", + content: ` import { Bookshelf } from '~/utils/db'; export class Vessel extends Bookshelf {} ` + }; + const tsconfigFile: ts.projectSystem.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 = ts.projectSystem.createServerHost(projectFiles); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + const { configFileName } = projectService.openClientFile(clientFile.path); + + assert.isDefined(configFileName, `should find config`); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigFile.path)!; + ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, f => f.path)); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Get definitions shouldnt make host requests + const getDefinitionRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.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 ts.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"]); + + ts.projectSystem.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) => ts.Path = useCaseSensitiveFileNames ? s => s as ts.Path : s => s.toLowerCase() as ts.Path; + const canonicalFrontendDir = toCanonical(frontendDir); + const file1: ts.projectSystem.File = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" + }; + const file2: ts.projectSystem.File = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" + }; + const file3: ts.projectSystem.File = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" }; + const es2016LibFile: ts.projectSystem.File = { + path: "/a/lib/lib.es2016.full.d.ts", + content: ts.projectSystem.libFile.content + }; + const typeRoots = ["types", "node_modules/@types"]; + const types = ["node", "jest"]; const tsconfigFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", + path: `${frontendDir}/tsconfig.json`, content: JSON.stringify({ compilerOptions: { - target: "es6", - module: "es6", - 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 = ts.projectSystem.createServerHost(projectFiles); - const session = ts.projectSystem.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 = ts.projectSystem.createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = ts.projectSystem.createProjectService(host); + const canonicalConfigPath = toCanonical(tsconfigFile.path); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, tsconfigFile.path as ts.server.NormalizedPath, `should find config`); ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(ts.projectSystem.getNodeModuleDirectories(ts.getDirectoryPath(canonicalFrontendDir))); - const project = projectService.configuredProjects.get(tsconfigFile.path)!; - ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, f => f.path)); + const project = projectService.configuredProjects.get(canonicalConfigPath)!; + verifyProjectAndWatchedDirectories(); const callsTrackingHost = createCallsTrackingHost(host); - // Get definitions shouldnt make host requests - const getDefinitionRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.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 ts.server.protocol.FileSpan[]; - assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); - callsTrackingHost.verifyNoHostCalls(); + // Create file cookie.ts + projectFiles.push(file3); + host.writeFile(file3.path, file3.content); + host.runQueuedTimeoutCallbacks(); - // 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"]); + 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); ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); - }); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); - describe("WatchDirectories for config file with", () => { - function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const frontendDir = "/Users/someuser/work/applications/frontend"; - const toCanonical: (s: string) => ts.Path = useCaseSensitiveFileNames ? s => s as ts.Path : s => s.toLowerCase() as ts.Path; - const canonicalFrontendDir = toCanonical(frontendDir); - const file1: ts.projectSystem.File = { - path: `${frontendDir}/src/app/utils/Analytic.ts`, - content: "export class SomeClass { };" - }; - const file2: ts.projectSystem.File = { - path: `${frontendDir}/src/app/redux/configureStore.ts`, - content: "export class configureStore { }" - }; - const file3: ts.projectSystem.File = { - path: `${frontendDir}/src/app/utils/Cookie.ts`, - content: "export class Cookie { }" - }; - const es2016LibFile: ts.projectSystem.File = { - path: "/a/lib/lib.es2016.full.d.ts", - content: ts.projectSystem.libFile.content - }; - const typeRoots = ["types", "node_modules/@types"]; - const types = ["node", "jest"]; - const tsconfigFile: ts.projectSystem.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 = ts.projectSystem.createServerHost(projectFiles, { useCaseSensitiveFileNames }); - const projectService = ts.projectSystem.createProjectService(host); - const canonicalConfigPath = toCanonical(tsconfigFile.path); - const { configFileName } = projectService.openClientFile(file1.path); - assert.equal(configFileName, tsconfigFile.path as ts.server.NormalizedPath, `should find config`); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(ts.projectSystem.getNodeModuleDirectories(ts.getDirectoryPath(canonicalFrontendDir))); + callsTrackingHost.clear(); - const project = projectService.configuredProjects.get(canonicalConfigPath)!; - verifyProjectAndWatchedDirectories(); + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); - const callsTrackingHost = createCallsTrackingHost(host); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); - // Create file cookie.ts - projectFiles.push(file3); - host.writeFile(file3.path, file3.content); - host.runQueuedTimeoutCallbacks(); + function getFilePathIfNotOpen(f: ts.projectSystem.File) { + const path = toCanonical(f.path); + const info = projectService.getScriptInfoForPath(toCanonical(f.path)); + return info && info.isScriptOpen() ? undefined : path; + } - 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); + function verifyProjectAndWatchedDirectories() { + ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, f => f.path)); + ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(projectFiles, getFilePathIfNotOpen)); + ts.projectSystem.checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + } + } - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); + it("case insensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); - callsTrackingHost.clear(); + it("case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + }); - const { configFileName: configFile2 } = projectService.openClientFile(file3.path); - assert.equal(configFile2, configFileName); + describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { + function runFailedLookupTest(resolution: "Node" | "Classic") { + const projectLocation = "/proj"; + const file1: ts.projectSystem.File = { + path: `${projectLocation}/foo/boo/app.ts`, + content: `import * as debug from "debug"` + }; + const file2: ts.projectSystem.File = { + path: `${projectLocation}/foo/boo/moo/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: ts.projectSystem.File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], + moduleResolution: resolution + }) + }; - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - callsTrackingHost.verifyNoHostCalls(); - - function getFilePathIfNotOpen(f: ts.projectSystem.File) { - const path = toCanonical(f.path); - const info = projectService.getScriptInfoForPath(toCanonical(f.path)); - return info && info.isScriptOpen() ? undefined : path; - } + const files = [file1, file2, tsconfig, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(file1.path); - function verifyProjectAndWatchedDirectories() { - ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, f => f.path)); - ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(projectFiles, getFilePathIfNotOpen)); - ts.projectSystem.checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - } - } + const project = service.configuredProjects.get(tsconfig.path)!; + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); - it("case insensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); - }); + const debugTypesFile: ts.projectSystem.File = { + path: `${projectLocation}/node_modules/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + host.writeFile(debugTypesFile.path, debugTypesFile.content); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update + ts.projectSystem.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("case sensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); - }); + 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("Subfolder invalidations correctly include parent folder failed lookup locations", () => { - function runFailedLookupTest(resolution: "Node" | "Classic") { - const projectLocation = "/proj"; - const file1: ts.projectSystem.File = { - path: `${projectLocation}/foo/boo/app.ts`, - content: `import * as debug from "debug"` - }; - const file2: ts.projectSystem.File = { - path: `${projectLocation}/foo/boo/moo/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: ts.projectSystem.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, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(file1.path); - - const project = service.configuredProjects.get(tsconfig.path)!; - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); - - const debugTypesFile: ts.projectSystem.File = { - path: `${projectLocation}/node_modules/debug/index.d.ts`, - content: "export {}" - }; - files.push(debugTypesFile); - host.writeFile(debugTypesFile.path, debugTypesFile.content); - host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions - host.runQueuedTimeoutCallbacks(); // Actual update - ts.projectSystem.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"); + describe("Verify npm install in directory with tsconfig file works when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const root = "/user/username/rootfolder/otherfolder"; + const getRootedFileOrFolder = (fileOrFolder: ts.projectSystem.File) => { + fileOrFolder.path = root + fileOrFolder.path; + return fileOrFolder; + }; + const app: ts.projectSystem.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: ts.projectSystem.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: ts.projectSystem.File) => { - fileOrFolder.path = root + fileOrFolder.path; - return fileOrFolder; - }; - const app: ts.projectSystem.File = getRootedFileOrFolder({ - path: "/a/b/app.ts", - content: "import _ from 'lodash';" - }); - const tsconfigJson: ts.projectSystem.File = getRootedFileOrFolder({ - path: "/a/b/tsconfig.json", - content: '{ "compilerOptions": { } }' - }); - const packageJson: ts.projectSystem.File = getRootedFileOrFolder({ - path: "/a/b/package.json", - content: ` + const packageJson: ts.projectSystem.File = getRootedFileOrFolder({ + path: "/a/b/package.json", + content: ` { "name": "test", "version": "1.0.0", @@ -540,215 +540,215 @@ namespace ts.projectSystem { "license": "ISC" } ` - }); - const appFolder = ts.getDirectoryPath(app.path); - const projectFiles = [app, ts.projectSystem.libFile, tsconfigJson]; - const typeRootDirectories = ts.projectSystem.getTypeRootsFromLocation(ts.getDirectoryPath(tsconfigJson.path)); - const otherFiles = [packageJson]; - const host = ts.projectSystem.createServerHost(projectFiles.concat(otherFiles)); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setHostConfiguration({ preferences: { includePackageJsonAutoImports: "off" } }); - const { configFileName } = projectService.openClientFile(app.path); - assert.equal(configFileName, tsconfigJson.path as ts.server.NormalizedPath, `should find config`); // TODO: GH#18217 - const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(ts.projectSystem.getNodeModuleDirectories(ts.getDirectoryPath(appFolder))); - verifyProject(); + }); + const appFolder = ts.getDirectoryPath(app.path); + const projectFiles = [app, ts.projectSystem.libFile, tsconfigJson]; + const typeRootDirectories = ts.projectSystem.getTypeRootsFromLocation(ts.getDirectoryPath(tsconfigJson.path)); + const otherFiles = [packageJson]; + const host = ts.projectSystem.createServerHost(projectFiles.concat(otherFiles)); + const projectService = ts.projectSystem.createProjectService(host); + projectService.setHostConfiguration({ preferences: { includePackageJsonAutoImports: "off" } }); + const { configFileName } = projectService.openClientFile(app.path); + assert.equal(configFileName, tsconfigJson.path as ts.server.NormalizedPath, `should find config`); // TODO: GH#18217 + const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(ts.projectSystem.getNodeModuleDirectories(ts.getDirectoryPath(appFolder))); + verifyProject(); + + let npmInstallComplete = false; + + // Simulate npm install + const filesAndFoldersToAdd: ts.projectSystem.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", content: "" } + ].map(getRootedFileOrFolder)); + // Since we added/removed in .staging no timeout + verifyAfterPartialOrCompleteNpmInstall(0); + + // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" + host.deleteFile(ts.last(filesAndFoldersToAdd).path); + 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 + host.deleteFile(ts.last(filesAndFoldersToAdd).path); + 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); + + ts.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, ""); + }); - let npmInstallComplete = false; - - // Simulate npm install - const filesAndFoldersToAdd: ts.projectSystem.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", content: "" } - ].map(getRootedFileOrFolder)); - // Since we added/removed in .staging no timeout - verifyAfterPartialOrCompleteNpmInstall(0); - - // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" - host.deleteFile(ts.last(filesAndFoldersToAdd).path); - 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 - host.deleteFile(ts.last(filesAndFoldersToAdd).path); - 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); - - ts.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, ""); - }); - - host.deleteFolder(root + "/a/b/node_modules/.staging", /*recursive*/ true); - const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; - projectFiles.push(ts.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) { - filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); - if (npmInstallComplete || timeoutDuringPartialInstallation) { - if (timeoutQueueLengthWhenRunningTimeouts) { - // Expected project update - host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts + 1); // Scheduled invalidation of resolutions - host.runQueuedTimeoutCallbacks(); // Actual update - } - else { - host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); - } + host.deleteFolder(root + "/a/b/node_modules/.staging", /*recursive*/ true); + const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; + projectFiles.push(ts.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) { + filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + if (timeoutQueueLengthWhenRunningTimeouts) { + // Expected project update + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts + 1); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update } else { - host.checkTimeoutQueueLength(3); + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); } - verifyProject(); } - - function verifyProject() { - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(tsconfigJson.path)!; - const projectFilePaths = ts.map(projectFiles, f => f.path); - ts.projectSystem.checkProjectActualFiles(project, projectFilePaths); - const filesWatched = ts.filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); - ts.projectSystem.checkWatchedFiles(host, filesWatched); - ts.projectSystem.checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + else { + host.checkTimeoutQueueLength(3); } + verifyProject(); } - it("timeouts occur inbetween installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); - }); - - 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: ts.projectSystem.File = { - path: `${projectLocation}/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: ts.projectSystem.File = { - path: `${projectLocation}/tsconfig.json`, - content: "" - }; - - const files = [app, tsconfig, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(app.path); + function verifyProject() { + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = service.configuredProjects.get(tsconfig.path)!; - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); + const project = projectService.configuredProjects.get(tsconfigJson.path)!; + const projectFilePaths = ts.map(projectFiles, f => f.path); + ts.projectSystem.checkProjectActualFiles(project, projectFilePaths); + const filesWatched = ts.filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); + ts.projectSystem.checkWatchedFiles(host, filesWatched); + ts.projectSystem.checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + } + } - const debugTypesFile: ts.projectSystem.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.writeFile(debugTypesFile.path, debugTypesFile.content); - host.runQueuedTimeoutCallbacks(); - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); }); - it("when creating new file in symlinked folder", () => { - const module1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/client/folder1/module1.ts`, - content: `export class Module1Class { }` - }; - const module2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/folder2/module2.ts`, - content: `import * as M from "folder1/module1";` - }; - const symlink: ts.projectSystem.SymLink = { - path: `${ts.tscWatch.projectRoot}/client/linktofolder2`, - symLink: `${ts.tscWatch.projectRoot}/folder2`, - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - baseUrl: "client", - paths: { "*": ["*"] }, - }, - include: ["client/**/*", "folder2"] - }) - }; - const host = ts.projectSystem.createServerHost([module1, module2, symlink, config, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(`${symlink.path}/module2.ts`); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = ts.Debug.checkDefined(service.configuredProjects.get(config.path)); - ts.projectSystem.checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, ts.projectSystem.libFile.path]); - host.writeFile(`${symlink.path}/module3.ts`, `import * as M from "folder1/module1";`); - host.runQueuedTimeoutCallbacks(); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, ts.projectSystem.libFile.path, `${symlink.path}/module3.ts`]); + 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: ts.projectSystem.File = { + path: `${projectLocation}/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: ts.projectSystem.File = { + path: `${projectLocation}/tsconfig.json`, + content: "" + }; + + const files = [app, tsconfig, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(app.path); + + const project = service.configuredProjects.get(tsconfig.path)!; + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug' or its corresponding type declarations."]); + + const debugTypesFile: ts.projectSystem.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.writeFile(debugTypesFile.path, debugTypesFile.content); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + }); + + it("when creating new file in symlinked folder", () => { + const module1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/client/folder1/module1.ts`, + content: `export class Module1Class { }` + }; + const module2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/folder2/module2.ts`, + content: `import * as M from "folder1/module1";` + }; + const symlink: ts.projectSystem.SymLink = { + path: `${ts.tscWatch.projectRoot}/client/linktofolder2`, + symLink: `${ts.tscWatch.projectRoot}/folder2`, + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "client", + paths: { "*": ["*"] }, + }, + include: ["client/**/*", "folder2"] + }) + }; + const host = ts.projectSystem.createServerHost([module1, module2, symlink, config, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(`${symlink.path}/module2.ts`); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = ts.Debug.checkDefined(service.configuredProjects.get(config.path)); + ts.projectSystem.checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, ts.projectSystem.libFile.path]); + host.writeFile(`${symlink.path}/module3.ts`, `import * as M from "folder1/module1";`); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, ts.projectSystem.libFile.path, `${symlink.path}/module3.ts`]); + }); +}); } diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts index 528e40dcd27b0..bb6f2dc995b80 100644 --- a/src/testRunner/unittests/tsserver/cancellationToken.ts +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -1,274 +1,274 @@ 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: ts.AnyFunction; - before(() => { - oldPrepare = (Error as any).prepareStackTrace; - delete (Error as any).prepareStackTrace; - }); +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: ts.AnyFunction; + before(() => { + oldPrepare = (Error as any).prepareStackTrace; + delete (Error as any).prepareStackTrace; + }); - after(() => { - (Error as any).prepareStackTrace = oldPrepare; - }); + after(() => { + (Error as any).prepareStackTrace = oldPrepare; + }); + + it("is attached to request", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let xyz = 1;" + }; + const host = ts.projectSystem.createServerHost([f1]); + let expectedRequestId: number; + const cancellationToken: ts.server.ServerCancellationToken = { + isCancellationRequested: () => false, + setRequest: requestId => { + if (expectedRequestId === undefined) { + assert.isTrue(false, "unexpected call"); + } + assert.equal(requestId, expectedRequestId); + }, + resetRequest: ts.noop + }; + + const session = ts.projectSystem.createSession(host, { cancellationToken }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + } as ts.server.protocol.OpenRequest); - it("is attached to request", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let xyz = 1;" - }; - const host = ts.projectSystem.createServerHost([f1]); - let expectedRequestId: number; - const cancellationToken: ts.server.ServerCancellationToken = { - isCancellationRequested: () => false, - setRequest: requestId => { - if (expectedRequestId === undefined) { - assert.isTrue(false, "unexpected call"); - } - assert.equal(requestId, expectedRequestId); - }, - resetRequest: ts.noop - }; + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + } as ts.server.protocol.GeterrRequest); - const session = ts.projectSystem.createSession(host, { cancellationToken }); + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "occurrences", + arguments: { file: f1.path, line: 1, offset: 6 } + } as ts.server.protocol.OccurrencesRequest); - expectedRequestId = session.getNextSeq(); + 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 ts.projectSystem.TestServerCancellationToken(); + const host = ts.projectSystem.createServerHost([f1, config]); + const session = ts.projectSystem.createSession(host, { + canUseEvents: true, + eventHandler: ts.noop, + cancellationToken + }); + { session.executeCommandSeq({ command: "open", arguments: { file: f1.path } - } as ts.server.protocol.OpenRequest); - - expectedRequestId = session.getNextSeq(); + } as ts.projectSystem.protocol.OpenRequest); + // send geterr for missing file session.executeCommandSeq({ command: "geterr", - arguments: { files: [f1.path] } - } as ts.server.protocol.GeterrRequest); - - expectedRequestId = session.getNextSeq(); + arguments: { files: ["/a/missing"] } + } as ts.projectSystem.protocol.GeterrRequest); + // 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: "occurrences", - arguments: { file: f1.path, line: 1, offset: 6 } - } as ts.server.protocol.OccurrencesRequest); - - 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: {} - }) - }; + command: "geterr", + arguments: { files: [f1.path] } + } as ts.projectSystem.protocol.GeterrRequest); - const cancellationToken = new ts.projectSystem.TestServerCancellationToken(); - const host = ts.projectSystem.createServerHost([f1, config]); - const session = ts.projectSystem.createSession(host, { - canUseEvents: true, - eventHandler: ts.noop, - cancellationToken - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OpenRequest); - // send geterr for missing file - session.executeCommandSeq({ - command: "geterr", - arguments: { files: ["/a/missing"] } - } as ts.projectSystem.protocol.GeterrRequest); - // 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] } - } as ts.projectSystem.protocol.GeterrRequest); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run new request + session.executeCommandSeq({ + command: "projectInfo", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.ProjectInfoRequest); + session.clearMessages(); - // run new request - session.executeCommandSeq({ - command: "projectInfo", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.ProjectInfoRequest); - session.clearMessages(); + // cancel previously issued Geterr + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedTimeoutCallbacks(); - // cancel previously issued Geterr - cancellationToken.setRequestToCancel(getErrId); - host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); - 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] } + } as ts.projectSystem.protocol.GeterrRequest); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - } as ts.projectSystem.protocol.GeterrRequest); - 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) as ts.projectSystem.protocol.Event; + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0) as ts.projectSystem.protocol.Event; - 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.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] } + } as ts.projectSystem.protocol.GeterrRequest); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - } as ts.projectSystem.protocol.GeterrRequest); - 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) as ts.projectSystem.protocol.Event; + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0) as ts.projectSystem.protocol.Event; - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); + // the semanticDiag message + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1); + const e2 = getMessage(0) as ts.projectSystem.protocol.Event; + assert.equal(e2.event, "semanticDiag"); + session.clearMessages(); - // the semanticDiag message - host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 1); - const e2 = getMessage(0) as ts.projectSystem.protocol.Event; - assert.equal(e2.event, "semanticDiag"); - session.clearMessages(); + host.runQueuedImmediateCallbacks(1); + assert.equal(host.getOutput().length, 2); + const e3 = getMessage(0) as ts.projectSystem.protocol.Event; + assert.equal(e3.event, "suggestionDiag"); + verifyRequestCompleted(getErrId, 1); - host.runQueuedImmediateCallbacks(1); - assert.equal(host.getOutput().length, 2); - const e3 = getMessage(0) as ts.projectSystem.protocol.Event; - assert.equal(e3.event, "suggestionDiag"); - verifyRequestCompleted(getErrId, 1); + cancellationToken.resetToken(); + } + { + const getErr1 = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + } as ts.projectSystem.protocol.GeterrRequest); + 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) as ts.projectSystem.protocol.Event; + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); - cancellationToken.resetToken(); - } - { - const getErr1 = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - } as ts.projectSystem.protocol.GeterrRequest); - 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) as ts.projectSystem.protocol.Event; - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + } as ts.projectSystem.protocol.GeterrRequest); + // make sure that getErr1 is completed + verifyRequestCompleted(getErr1, 0); + } - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - } as ts.projectSystem.protocol.GeterrRequest); - // make sure that getErr1 is completed - verifyRequestCompleted(getErr1, 0); - } + function verifyRequestCompleted(expectedSeq: number, n: number) { + const event = getMessage(n) as ts.projectSystem.protocol.RequestCompletedEvent; + assert.equal(event.event, "requestCompleted"); + assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); + session.clearMessages(); + } - function verifyRequestCompleted(expectedSeq: number, n: number) { - const event = getMessage(n) as ts.projectSystem.protocol.RequestCompletedEvent; - assert.equal(event.event, "requestCompleted"); - assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); - session.clearMessages(); - } + function getMessage(n: number) { + return JSON.parse(ts.server.extractMessage(host.getOutput()[n])); + } + }); - function getMessage(n: number) { - return JSON.parse(ts.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 ts.projectSystem.TestServerCancellationToken(/*cancelAfterRequest*/ 3); + const host = ts.projectSystem.createServerHost([f1, config]); + const session = ts.projectSystem.createSession(host, { + canUseEvents: true, + eventHandler: ts.noop, + cancellationToken, + throttleWaitMilliseconds: 0 }); + { + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OpenRequest); - 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 ts.projectSystem.TestServerCancellationToken(/*cancelAfterRequest*/ 3); - const host = ts.projectSystem.createServerHost([f1, config]); - const session = ts.projectSystem.createSession(host, { - canUseEvents: true, - eventHandler: ts.noop, - cancellationToken, - throttleWaitMilliseconds: 0 - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OpenRequest); - - // send navbar request (normal priority) - session.executeCommandSeq({ - command: "navbar", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.NavBarRequest); + // send navbar request (normal priority) + session.executeCommandSeq({ + command: "navbar", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.NavBarRequest); - // ensure the nav bar request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "navbar", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.NavBarRequest); + // ensure the nav bar request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "navbar", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.NavBarRequest); - // send outlining spans request (normal priority) - session.executeCommandSeq({ - command: "outliningSpans", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OutliningSpansRequestFull); + // send outlining spans request (normal priority) + session.executeCommandSeq({ + command: "outliningSpans", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OutliningSpansRequestFull); - // ensure the outlining spans request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "outliningSpans", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OutliningSpansRequestFull); - } + // ensure the outlining spans request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "outliningSpans", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OutliningSpansRequestFull); + } - 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; + 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 ts.OperationCanceledException); - operationCanceledExceptionThrown = true; - } - assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); + try { + session.executeCommandSeq(request); } - }); + catch (e) { + assert(e instanceof ts.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 b51407005ec63..924477b623521 100644 --- a/src/testRunner/unittests/tsserver/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -1,1057 +1,1057 @@ namespace ts.projectSystem { - import CommandNames = ts.server.CommandNames; - function createTestTypingsInstaller(host: ts.server.ServerHost) { - return new ts.projectSystem.TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } +import CommandNames = ts.server.CommandNames; +function createTestTypingsInstaller(host: ts.server.ServerHost) { + return new ts.projectSystem.TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); +} - describe("unittests:: tsserver:: compileOnSave:: affected list", () => { - function sendAffectedFileRequestAndCheckResult(session: ts.server.Session, request: ts.server.protocol.Request, expectedFileList: { - projectFileName: string; - files: ts.projectSystem.File[]; - }[]) { - const response = session.executeCommand(request).response as ts.server.protocol.CompileOnSaveAffectedFileListSingleProject[]; - const actualResult = response.sort((list1, list2) => ts.compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); - expectedFileList = expectedFileList.sort((list1, list2) => ts.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 = ts.map(expectedResultSingleProject.files, f => f.path).sort(); - assert.isTrue(ts.arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); - } +describe("unittests:: tsserver:: compileOnSave:: affected list", () => { + function sendAffectedFileRequestAndCheckResult(session: ts.server.Session, request: ts.server.protocol.Request, expectedFileList: { + projectFileName: string; + files: ts.projectSystem.File[]; + }[]) { + const response = session.executeCommand(request).response as ts.server.protocol.CompileOnSaveAffectedFileListSingleProject[]; + const actualResult = response.sort((list1, list2) => ts.compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); + expectedFileList = expectedFileList.sort((list1, list2) => ts.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 = ts.map(expectedResultSingleProject.files, f => f.path).sort(); + assert.isTrue(ts.arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); } + } + + describe("for configured projects", () => { + let moduleFile1: ts.projectSystem.File; + let file1Consumer1: ts.projectSystem.File; + let file1Consumer2: ts.projectSystem.File; + let moduleFile2: ts.projectSystem.File; + let globalFile3: ts.projectSystem.File; + let configFile: ts.projectSystem.File; + let changeModuleFile1ShapeRequest1: ts.server.protocol.Request; + let changeModuleFile1InternalRequest1: ts.server.protocol.Request; + // A compile on save affected file request using file1 + let moduleFile1FileListRequest: ts.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;` + }; - describe("for configured projects", () => { - let moduleFile1: ts.projectSystem.File; - let file1Consumer1: ts.projectSystem.File; - let file1Consumer2: ts.projectSystem.File; - let moduleFile2: ts.projectSystem.File; - let globalFile3: ts.projectSystem.File; - let configFile: ts.projectSystem.File; - let changeModuleFile1ShapeRequest1: ts.server.protocol.Request; - let changeModuleFile1InternalRequest1: ts.server.protocol.Request; - // A compile on save affected file request using file1 - let moduleFile1FileListRequest: ts.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: `{ + 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 = ts.projectSystem.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 = ts.projectSystem.makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `var T1: number;` - }); + // Change the content of file1 to `export var T: number;export function Foo() { };` + changeModuleFile1ShapeRequest1 = ts.projectSystem.makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); - moduleFile1FileListRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path }); + // Change the content of file1 to `export var T: number;export function Foo() { };` + changeModuleFile1InternalRequest1 = ts.projectSystem.makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `var T1: 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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.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 = ts.projectSystem.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] }]); + moduleFile1FileListRequest = ts.projectSystem.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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.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 = ts.projectSystem.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] }]); + }); - it("should be up-to-date with the reference map changes", () => { - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.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 = ts.projectSystem.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 = ts.projectSystem.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 = ts.projectSystem.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] }]); + it("should be up-to-date with the reference map changes", () => { + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.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 = ts.projectSystem.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 = ts.projectSystem.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 = ts.projectSystem.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] }]); + }); - it("should be up-to-date with changes made in non-open files", () => { - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); + it("should be up-to-date with changes made in non-open files", () => { + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - host.writeFile(file1Consumer1.path, `let y = 10;`); + host.writeFile(file1Consumer1.path, `let y = 10;`); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); - }); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); + }); - it("should be up-to-date with deleted files", () => { - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - session.executeCommand(changeModuleFile1ShapeRequest1); - // Delete file1Consumer2 - host.deleteFile(file1Consumer2.path); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - }); + it("should be up-to-date with deleted files", () => { + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - it("should be up-to-date with newly created files", () => { - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - const file1Consumer3: ts.projectSystem.File = { - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - host.writeFile(file1Consumer3.path, file1Consumer3.content); - host.runQueuedTimeoutCallbacks(); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); - }); + session.executeCommand(changeModuleFile1ShapeRequest1); + // Delete file1Consumer2 + host.deleteFile(file1Consumer2.path); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + }); + + it("should be up-to-date with newly created files", () => { + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - it("should detect changes in non-root files", () => { - moduleFile1 = { - path: "/a/b/moduleFile1.ts", - content: "export function Foo() { };" - }; + const file1Consumer3: ts.projectSystem.File = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + host.writeFile(file1Consumer3.path, file1Consumer3.content); + host.runQueuedTimeoutCallbacks(); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); + }); - file1Consumer1 = { - path: "/a/b/file1Consumer1.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; + it("should detect changes in non-root files", () => { + moduleFile1 = { + path: "/a/b/moduleFile1.ts", + content: "export function Foo() { };" + }; - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + 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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.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 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] }]); + }); - // 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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([globalFile3], session); + const changeGlobalFile3ShapeRequest = ts.projectSystem.makeSessionRequest(CommandNames.Change, { + file: globalFile3.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `var T2: string;` }); - it("should return all files if a global file changed shape", () => { - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([globalFile3], session); - const changeGlobalFile3ShapeRequest = ts.projectSystem.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 = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path }); + sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]); + }); - // check after file1 shape changes - session.executeCommand(changeGlobalFile3ShapeRequest); - const globalFile3FileListRequest = ts.projectSystem.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: `{}` + }; - it("should return empty array if CompileOnSave is not enabled", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); - }); + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + }); - it("should return empty array if noEmit is set", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + it("should return empty array if noEmit is set", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true, "compilerOptions": { "noEmit": true } }` - }; + }; - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); - }); + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + }); - it("should save when compileOnSave is enabled in base tsconfig.json", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + it("should save when compileOnSave is enabled in base tsconfig.json", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "extends": "/a/tsconfig.json" }` - }; + }; - const configFile2: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: `{ + const configFile2: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: `{ "compileOnSave": true }` - }; + }; - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - }); + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.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: `{ + 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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - const file1ChangeShapeRequest = ts.projectSystem.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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + const file1ChangeShapeRequest = ts.projectSystem.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] }]); + }); - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + 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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1], session); - const file1ChangeShapeRequest = ts.projectSystem.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 = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1], session); + const file1ChangeShapeRequest = ts.projectSystem.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] }]); + }); - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: ts.projectSystem.File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, ts.projectSystem.libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - - const changeFile1Consumer1ShapeRequest = ts.projectSystem.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] }]); + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: ts.projectSystem.File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const host = ts.projectSystem.createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, ts.projectSystem.libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([moduleFile1, file1Consumer1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + + const changeFile1Consumer1ShapeRequest = ts.projectSystem.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] }]); + }); - it("should work fine for files with circular references", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: ` + it("should work fine for files with circular references", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: ` /// export var t1 = 10;` - }; - const file2: ts.projectSystem.File = { - path: "/a/b/file2.ts", - content: ` + }; + const file2: ts.projectSystem.File = { + path: "/a/b/file2.ts", + content: ` /// export var t2 = 10;` - }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([file1, file2], session); - const file1AffectedListRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); - }); + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([file1, file2], session); + const file1AffectedListRequest = ts.projectSystem.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: ts.projectSystem.File = { path: "/a/b/file1.ts", content: "export var t = 10;" }; - const file2: ts.projectSystem.File = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; - const file3: ts.projectSystem.File = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; - const configFile1: ts.projectSystem.File = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; - const configFile2: ts.projectSystem.File = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; - const host = ts.projectSystem.createServerHost([file1, file2, file3, configFile1, configFile2]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1, file2, file3], session); - const file1AffectedListRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ - { projectFileName: configFile1.path, files: [file1, file2] }, - { projectFileName: configFile2.path, files: [file1, file3] } - ]); - }); + it("should return results for all projects if not specifying projectFileName", () => { + const file1: ts.projectSystem.File = { path: "/a/b/file1.ts", content: "export var t = 10;" }; + const file2: ts.projectSystem.File = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; + const file3: ts.projectSystem.File = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; + const configFile1: ts.projectSystem.File = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; + const configFile2: ts.projectSystem.File = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; + const host = ts.projectSystem.createServerHost([file1, file2, file3, configFile1, configFile2]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1, file2, file3], session); + const file1AffectedListRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - it("should detect removed code file", () => { - const referenceFile1: ts.projectSystem.File = { - path: "/a/b/referenceFile1.ts", - content: ` + sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ + { projectFileName: configFile1.path, files: [file1, file2] }, + { projectFileName: configFile2.path, files: [file1, file3] } + ]); + }); + + it("should detect removed code file", () => { + const referenceFile1: ts.projectSystem.File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const host = ts.projectSystem.createServerHost([moduleFile1, referenceFile1, configFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([referenceFile1], session); - host.deleteFile(moduleFile1.path); - - const request = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); - const requestForMissingFile = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); - sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); - }); + }; + const host = ts.projectSystem.createServerHost([moduleFile1, referenceFile1, configFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([referenceFile1], session); + host.deleteFile(moduleFile1.path); - it("should detect non-existing code file", () => { - const referenceFile1: ts.projectSystem.File = { - path: "/a/b/referenceFile1.ts", - content: ` + const request = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + sendAffectedFileRequestAndCheckResult(session, request, [ + { projectFileName: configFile.path, files: [referenceFile1] } + ]); + const requestForMissingFile = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); + sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: ts.projectSystem.File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const host = ts.projectSystem.createServerHost([referenceFile1, configFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([referenceFile1], session); - const request = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); - }); + }; + const host = ts.projectSystem.createServerHost([referenceFile1, configFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([referenceFile1], session); + const request = ts.projectSystem.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: ts.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 = ts.projectSystem.createServerHost([dtsFile, f2, config]); - const session = ts.projectSystem.createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "open", - arguments: { file: dtsFile.path } - } as ts.projectSystem.protocol.OpenRequest); - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - ts.projectSystem.checkProjectRootFiles(project, [dtsFile.path, f2.path]); - session.executeCommand({ - seq: 2, - type: "request", - command: "open", - arguments: { file: f2.path } - } as ts.projectSystem.protocol.OpenRequest); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - const { response } = session.executeCommand({ - seq: 3, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: dtsFile.path } - } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); - if (expectDTSEmit) { - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected to affect 2 files"); - } - else { - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 0, "expected no output"); - } - - - const { response: response2 } = session.executeCommand({ - seq: 4, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: f2.path } - } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); - assert.equal((response2 as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); + describe("for changes in declaration files", () => { + function testDTS(dtsFileContents: string, tsFileContents: string, opts: ts.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 = ts.projectSystem.createServerHost([dtsFile, f2, config]); + const session = ts.projectSystem.createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "open", + arguments: { file: dtsFile.path } + } as ts.projectSystem.protocol.OpenRequest); + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + ts.projectSystem.checkProjectRootFiles(project, [dtsFile.path, f2.path]); + session.executeCommand({ + seq: 2, + type: "request", + command: "open", + arguments: { file: f2.path } + } as ts.projectSystem.protocol.OpenRequest); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand({ + seq: 3, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: dtsFile.path } + } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); + if (expectDTSEmit) { + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected to affect 2 files"); + } + else { + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 0, "expected no output"); } - 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); - }); + const { response: response2 } = session.executeCommand({ + seq: 4, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f2.path } + } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); + assert.equal((response2 as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); + } - 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 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 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); - }); + 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); }); - describe("tsserverProjectSystem emit with outFile or out setting", () => { - function test(opts: ts.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 = ts.projectSystem.createServerHost([f1, f2, config]); - const session = ts.projectSystem.createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "open", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OpenRequest); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - const { response } = session.executeCommand({ - seq: 2, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output for 1 project"); - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected output for 1 project"); - assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); - } + 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("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); - }); + 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); }); - }); - 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 + ts.Extension.Ts, - content: lines.join(newLine) - }; - const host = ts.projectSystem.createServerHost([f], { newLine }); - const session = ts.projectSystem.createSession(host); - const openRequest: ts.server.protocol.OpenRequest = { - seq: 1, - type: "request", - command: ts.server.protocol.CommandTypes.Open, - arguments: { file: f.path } - }; - session.executeCommand(openRequest); - const emitFileRequest: ts.server.protocol.CompileOnSaveEmitFileRequest = { - seq: 2, - type: "request", - command: ts.server.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: f.path } - }; - session.executeCommand(emitFileRequest); - const emitOutput = host.readFile(path + ts.Extension.Js); - assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); - } + 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); }); + }); - it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` + describe("tsserverProjectSystem emit with outFile or out setting", () => { + function test(opts: ts.CompilerOptions, expectedUsesOutFile: boolean) { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" }; - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; let y = Foo();` + const f2 = { + path: "/a/b.ts", + content: "let y = 1" }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: opts, + compileOnSave: true + }) }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile, ts.projectSystem.libFile], { newLine: "\r\n" }); - const typingsInstaller = createTestTypingsInstaller(host); - const session = ts.projectSystem.createSession(host, { typingsInstaller }); - ts.projectSystem.openFilesForSession([file1, file2], session); - const compileFileRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); - session.executeCommand(compileFileRequest); + const host = ts.projectSystem.createServerHost([f1, f2, config]); + const session = ts.projectSystem.createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "open", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OpenRequest); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand({ + seq: 2, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.CompileOnSaveAffectedFileListRequest); + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output for 1 project"); + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected output for 1 project"); + assert.equal((response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); + } - 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("projectUsesOutFile should not be returned if not set", () => { + test({}, /*expectedUsesOutFile*/ false); }); - - it("shoud not emit js files in external projects", () => { - const file1 = { - path: "/a/b/file1.ts", - content: "consonle.log('file1');" + 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); + }); + }); +}); + +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 + ts.Extension.Ts, + content: lines.join(newLine) }; - // file2 has errors. The emitting should not be blocked. - const file2 = { - path: "/a/b/file2.js", - content: "console.log'file2');" + const host = ts.projectSystem.createServerHost([f], { newLine }); + const session = ts.projectSystem.createSession(host); + const openRequest: ts.server.protocol.OpenRequest = { + seq: 1, + type: "request", + command: ts.server.protocol.CommandTypes.Open, + arguments: { file: f.path } }; - const file3 = { - path: "/a/b/file3.js", - content: "console.log('file3');" + session.executeCommand(openRequest); + const emitFileRequest: ts.server.protocol.CompileOnSaveEmitFileRequest = { + seq: 2, + type: "request", + command: ts.server.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: f.path } }; - const externalProjectName = "/a/b/externalproject"; - const host = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); + session.executeCommand(emitFileRequest); + const emitOutput = host.readFile(path + ts.Extension.Js); + assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); + } + }); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), - options: { - allowJs: true, - outFile: "dist.js", - compileOnSave: true - }, - projectFileName: externalProjectName - }); + it("should emit specified file", () => { + 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"; let y = Foo();` + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile, ts.projectSystem.libFile], { newLine: "\r\n" }); + const typingsInstaller = createTestTypingsInstaller(host); + const session = ts.projectSystem.createSession(host, { typingsInstaller }); + ts.projectSystem.openFilesForSession([file1, file2], session); + const compileFileRequest = ts.projectSystem.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 emitRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); + 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 externalProjectName = "/a/b/externalproject"; + const host = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + + projectService.openExternalProject({ + rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), + options: { + allowJs: true, + outFile: "dist.js", + compileOnSave: true + }, + projectFileName: externalProjectName + }); + + const emitRequest = ts.projectSystem.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 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 externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; + const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + + const outFileName = "bar.js"; + projectService.openExternalProject({ + rootFiles: ts.projectSystem.toExternalFiles([file1.path]), + options: { + outFile: outFileName, + sourceMap: true, + compileOnSave: true + }, + projectFileName: externalProjectName }); - 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 externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; - const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); + const emitRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); - const outFileName = "bar.js"; - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([file1.path]), - options: { - outFile: outFileName, - sourceMap: true, - compileOnSave: true - }, - projectFileName: externalProjectName - }); + // 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 - const emitRequest = ts.projectSystem.makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); + // Verify map file + const expectedMapFileName = expectedOutFileName + ".map"; + assert.isTrue(host.fileExists(expectedMapFileName)); + const mapFileContent = host.readFile(expectedMapFileName)!; + verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); - // 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 + function verifyContentHasString(content: string, str: string) { + assert.isTrue(ts.stringContains(content, str), `Expected "${content}" to have "${str}"`); + } + }); - // Verify map file - const expectedMapFileName = expectedOutFileName + ".map"; - assert.isTrue(host.fileExists(expectedMapFileName)); - const mapFileContent = host.readFile(expectedMapFileName)!; - verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); + describe("compile on save emit with and without richResponse", () => { + it("without rich Response", () => { + verify(/*richRepsonse*/ undefined); + }); + it("with rich Response set to false", () => { + verify(/*richRepsonse*/ false); + }); + it("with rich Repsonse", () => { + verify(/*richRepsonse*/ true); + }); - function verifyContentHasString(content: string, str: string) { - assert.isTrue(ts.stringContains(content, str), `Expected "${content}" to have "${str}"`); + function verify(richResponse: boolean | undefined) { + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compileOnSave: true, + compilerOptions: { + outDir: "test", + noEmitOnError: true, + declaration: true, + }, + exclude: ["node_modules"] + }) + }; + const file1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file1.ts`, + content: "const x = 1;" + }; + const file2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file2.ts`, + content: "const y = 2;" + }; + const host = ts.projectSystem.createServerHost([file1, file2, config, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + const affectedFileResponse = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: file1.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(affectedFileResponse, [ + { fileNames: [file1.path, file2.path], projectFileName: config.path, projectUsesOutFile: false } + ]); + const file1SaveResponse = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: file1.path, richResponse } + }).response; + if (richResponse) { + assert.deepEqual(file1SaveResponse, { emitSkipped: false, diagnostics: ts.emptyArray }); } - }); + else { + assert.isTrue(file1SaveResponse); + } + assert.strictEqual(host.readFile(`${ts.tscWatch.projectRoot}/test/file1.d.ts`), "declare const x = 1;\n"); + const file2SaveResponse = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: file2.path, richResponse } + }).response; + if (richResponse) { + assert.deepEqual(file2SaveResponse, { + emitSkipped: true, + diagnostics: [{ + start: undefined, + end: undefined, + fileName: undefined, + text: ts.formatStringFromArgs(ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.message, [`${ts.tscWatch.projectRoot}/test/file1.d.ts`]), + code: ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.code, + category: ts.diagnosticCategoryName(ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file), + reportsUnnecessary: undefined, + reportsDeprecated: undefined, + relatedInformation: undefined, + source: undefined + }] + }); + } + else { + assert.isFalse(file2SaveResponse); + } + assert.isFalse(host.fileExists(`${ts.tscWatch.projectRoot}/test/file2.d.ts`)); + } + }); - describe("compile on save emit with and without richResponse", () => { - it("without rich Response", () => { - verify(/*richRepsonse*/ undefined); - }); - it("with rich Response set to false", () => { - verify(/*richRepsonse*/ false); + describe("compile on save in global files", () => { + describe("when program contains module", () => { + it("when d.ts emit is enabled", () => { + verifyGlobalSave(/*declaration*/ true, /*hasModule*/ true); }); - it("with rich Repsonse", () => { - verify(/*richRepsonse*/ true); + it("when d.ts emit is not enabled", () => { + verifyGlobalSave(/*declaration*/ false, /*hasModule*/ true); }); - - function verify(richResponse: boolean | undefined) { - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compileOnSave: true, - compilerOptions: { - outDir: "test", - noEmitOnError: true, - declaration: true, - }, - exclude: ["node_modules"] - }) - }; - const file1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file1.ts`, - content: "const x = 1;" - }; - const file2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file2.ts`, - content: "const y = 2;" - }; - const host = ts.projectSystem.createServerHost([file1, file2, config, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1], session); - const affectedFileResponse = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: file1.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(affectedFileResponse, [ - { fileNames: [file1.path, file2.path], projectFileName: config.path, projectUsesOutFile: false } - ]); - const file1SaveResponse = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: file1.path, richResponse } - }).response; - if (richResponse) { - assert.deepEqual(file1SaveResponse, { emitSkipped: false, diagnostics: ts.emptyArray }); - } - else { - assert.isTrue(file1SaveResponse); - } - assert.strictEqual(host.readFile(`${ts.tscWatch.projectRoot}/test/file1.d.ts`), "declare const x = 1;\n"); - const file2SaveResponse = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: file2.path, richResponse } - }).response; - if (richResponse) { - assert.deepEqual(file2SaveResponse, { - emitSkipped: true, - diagnostics: [{ - start: undefined, - end: undefined, - fileName: undefined, - text: ts.formatStringFromArgs(ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.message, [`${ts.tscWatch.projectRoot}/test/file1.d.ts`]), - code: ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.code, - category: ts.diagnosticCategoryName(ts.Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file), - reportsUnnecessary: undefined, - reportsDeprecated: undefined, - relatedInformation: undefined, - source: undefined - }] - }); - } - else { - assert.isFalse(file2SaveResponse); - } - assert.isFalse(host.fileExists(`${ts.tscWatch.projectRoot}/test/file2.d.ts`)); - } }); - - describe("compile on save in global files", () => { - describe("when program contains module", () => { - it("when d.ts emit is enabled", () => { - verifyGlobalSave(/*declaration*/ true, /*hasModule*/ true); - }); - it("when d.ts emit is not enabled", () => { - verifyGlobalSave(/*declaration*/ false, /*hasModule*/ true); - }); + describe("when program doesnt have module", () => { + it("when d.ts emit is enabled", () => { + verifyGlobalSave(/*declaration*/ true, /*hasModule*/ false); }); - describe("when program doesnt have module", () => { - it("when d.ts emit is enabled", () => { - verifyGlobalSave(/*declaration*/ true, /*hasModule*/ false); - }); - it("when d.ts emit is not enabled", () => { - verifyGlobalSave(/*declaration*/ false, /*hasModule*/ false); - }); + it("when d.ts emit is not enabled", () => { + verifyGlobalSave(/*declaration*/ false, /*hasModule*/ false); }); - function verifyGlobalSave(declaration: boolean,hasModule: boolean) { - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compileOnSave: true, - compilerOptions: { - declaration, - module: hasModule ? undefined : "none" - }, - }) - }; - const file1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file1.ts`, - content: `const x = 1; + }); + function verifyGlobalSave(declaration: boolean,hasModule: boolean) { + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compileOnSave: true, + compilerOptions: { + declaration, + module: hasModule ? undefined : "none" + }, + }) + }; + const file1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file1.ts`, + content: `const x = 1; function foo() { return "hello"; }` - }; - const file2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file2.ts`, - content: `const y = 2; + }; + const file2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file2.ts`, + content: `const y = 2; function bar() { return "world"; }` - }; - const file3: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file3.ts`, - content: "const xy = 3;" - }; - const module: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/module.ts`, - content: "export const xyz = 4;" - }; - const files = [file1, file2, file3, ...(hasModule ? [module] : ts.emptyArray)]; - const host = ts.projectSystem.createServerHost([...files, config, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1, file2], session); + }; + const file3: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file3.ts`, + content: "const xy = 3;" + }; + const module: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/module.ts`, + content: "export const xyz = 4;" + }; + const files = [file1, file2, file3, ...(hasModule ? [module] : ts.emptyArray)]; + const host = ts.projectSystem.createServerHost([...files, config, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1, file2], session); + const affectedFileResponse = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: file1.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(affectedFileResponse, [ + { fileNames: files.map(f => f.path), projectFileName: config.path, projectUsesOutFile: false } + ]); + verifyFileSave(file1); + verifyFileSave(file2); + verifyFileSave(file3); + if (hasModule) { + verifyFileSave(module); + } + + // Change file1 get affected file list + verifyLocalEdit(file1, "hello", "world"); + + // Change file2 get affected file list = will return only file2 if --declaration otherwise all files + verifyLocalEdit(file2, "world", "hello", /*returnsAllFilesAsAffected*/ !declaration); + + function verifyFileSave(file: ts.projectSystem.File) { + const response = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: file.path } + }).response; + assert.isTrue(response); + assert.strictEqual(host.readFile(ts.changeExtension(file.path, ".js")), file === module ? + `"use strict";\nexports.__esModule = true;\nexports.xyz = void 0;\nexports.xyz = 4;\n` : + `${file.content.replace("const", "var")}\n`); + if (declaration) { + assert.strictEqual(host.readFile(ts.changeExtension(file.path, ".d.ts")), (file.content.substr(0, file.content.indexOf(" {") === -1 ? file.content.length : file.content.indexOf(" {")) + .replace("const ", "declare const ") + .replace("function ", "declare function ") + .replace(")", "): string;")) + "\n"); + } + } + + function verifyLocalEdit(file: ts.projectSystem.File, oldText: string, newText: string, returnsAllFilesAsAffected?: boolean) { + // Change file1 get affected file list + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: file.path, + textChanges: [{ + newText, + ...ts.projectSystem.protocolTextSpanFromSubstring(file.content, oldText) + }] + }] + } + }); const affectedFileResponse = session.executeCommandSeq({ command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: file1.path } + arguments: { file: file.path } }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; assert.deepEqual(affectedFileResponse, [ - { fileNames: files.map(f => f.path), projectFileName: config.path, projectUsesOutFile: false } + { fileNames: [file.path, ...(returnsAllFilesAsAffected ? files.filter(f => f !== file).map(f => f.path) : ts.emptyArray)], projectFileName: config.path, projectUsesOutFile: false } ]); - verifyFileSave(file1); - verifyFileSave(file2); - verifyFileSave(file3); - if (hasModule) { - verifyFileSave(module); - } - - // Change file1 get affected file list - verifyLocalEdit(file1, "hello", "world"); - - // Change file2 get affected file list = will return only file2 if --declaration otherwise all files - verifyLocalEdit(file2, "world", "hello", /*returnsAllFilesAsAffected*/ !declaration); - - function verifyFileSave(file: ts.projectSystem.File) { - const response = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: file.path } - }).response; - assert.isTrue(response); - assert.strictEqual(host.readFile(ts.changeExtension(file.path, ".js")), file === module ? - `"use strict";\nexports.__esModule = true;\nexports.xyz = void 0;\nexports.xyz = 4;\n` : - `${file.content.replace("const", "var")}\n`); - if (declaration) { - assert.strictEqual(host.readFile(ts.changeExtension(file.path, ".d.ts")), (file.content.substr(0, file.content.indexOf(" {") === -1 ? file.content.length : file.content.indexOf(" {")) - .replace("const ", "declare const ") - .replace("function ", "declare function ") - .replace(")", "): string;")) + "\n"); - } - } - - function verifyLocalEdit(file: ts.projectSystem.File, oldText: string, newText: string, returnsAllFilesAsAffected?: boolean) { - // Change file1 get affected file list - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: file.path, - textChanges: [{ - newText, - ...ts.projectSystem.protocolTextSpanFromSubstring(file.content, oldText) - }] - }] - } - }); - const affectedFileResponse = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: file.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(affectedFileResponse, [ - { fileNames: [file.path, ...(returnsAllFilesAsAffected ? files.filter(f => f !== file).map(f => f.path) : ts.emptyArray)], projectFileName: config.path, projectUsesOutFile: false } - ]); - file.content = file.content.replace(oldText, newText); - verifyFileSave(file); - } + file.content = file.content.replace(oldText, newText); + verifyFileSave(file); } - }); - }); - - describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { - const core: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/core/core.ts`, - content: "let z = 10;" - }; - const app1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app1/app.ts`, - content: "let x = 10;" - }; - const app2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app2/app.ts`, - content: "let y = 10;" - }; - const app1Config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app1/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts", "../core/core.ts"], - compilerOptions: { outFile: "build/output.js" }, - compileOnSave: true - }) - }; - const app2Config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app2/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts", "../core/core.ts"], - compilerOptions: { outFile: "build/output.js" }, - compileOnSave: true - }) - }; - const files = [ts.projectSystem.libFile, core, app1, app2, app1Config, app2Config]; - function insertString(session: ts.projectSystem.TestSession, file: ts.projectSystem.File) { - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: file.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: "let k = 1" - } - }); } + }); +}); + +describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { + const core: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/core/core.ts`, + content: "let z = 10;" + }; + const app1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app1/app.ts`, + content: "let x = 10;" + }; + const app2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app2/app.ts`, + content: "let y = 10;" + }; + const app1Config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app1/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const app2Config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app2/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const files = [ts.projectSystem.libFile, core, app1, app2, app1Config, app2Config]; + function insertString(session: ts.projectSystem.TestSession, file: ts.projectSystem.File) { + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: file.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: "let k = 1" + } + }); + } - function getSession() { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([app1, app2, core], session); - const service = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - const project1 = service.configuredProjects.get(app1Config.path)!; - const project2 = service.configuredProjects.get(app2Config.path)!; - ts.projectSystem.checkProjectActualFiles(project1, [ts.projectSystem.libFile.path, app1.path, core.path, app1Config.path]); - ts.projectSystem.checkProjectActualFiles(project2, [ts.projectSystem.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; - } + function getSession() { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([app1, app2, core], session); + const service = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + const project1 = service.configuredProjects.get(app1Config.path)!; + const project2 = service.configuredProjects.get(app2Config.path)!; + ts.projectSystem.checkProjectActualFiles(project1, [ts.projectSystem.libFile.path, app1.path, core.path, app1Config.path]); + ts.projectSystem.checkProjectActualFiles(project2, [ts.projectSystem.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: ts.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 specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: ts.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: ts.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); - }); + it("when projectFile is not specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: ts.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 c3ddb55416fd4..255c13e480573 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -1,267 +1,267 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: completions", () => { - it("works", () => { - const aTs: ts.projectSystem.File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: ts.projectSystem.File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}", - }; +describe("unittests:: tsserver:: completions", () => { + it("works", () => { + const aTs: ts.projectSystem.File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: ts.projectSystem.File = { + path: "/b.ts", + content: "foo", + }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}", + }; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, bTs, tsconfig])); - ts.projectSystem.openFilesForSession([aTs, bTs], session); - const requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, bTs, tsconfig])); + ts.projectSystem.openFilesForSession([aTs, bTs], session); + const requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - const entry: ts.projectSystem.protocol.CompletionEntry = { - hasAction: true, - insertText: undefined, - isRecommended: undefined, - kind: ts.ScriptElementKind.constElement, - kindModifiers: ts.ScriptElementKindModifier.exportedModifier, - name: "foo", - replacementSpan: undefined, - isPackageJsonImport: undefined, - isImportStatementCompletion: undefined, - sortText: ts.Completions.SortText.AutoImportSuggestions, - source: "/a", - sourceDisplay: undefined, - isSnippet: undefined, - data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }, - labelDetails: undefined, - }; + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + const entry: ts.projectSystem.protocol.CompletionEntry = { + hasAction: true, + insertText: undefined, + isRecommended: undefined, + kind: ts.ScriptElementKind.constElement, + kindModifiers: ts.ScriptElementKindModifier.exportedModifier, + name: "foo", + replacementSpan: undefined, + isPackageJsonImport: undefined, + isImportStatementCompletion: undefined, + sortText: ts.Completions.SortText.AutoImportSuggestions, + source: "/a", + sourceDisplay: undefined, + isSnippet: undefined, + data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }, + labelDetails: undefined, + }; - // `data.exportMapKey` contains a SymbolId so should not be mocked up with an expected value here. - // Just assert that it's a string and then delete it so we can compare everything else with `deepEqual`. - const exportMapKey = (response?.entries[0].data as any)?.exportMapKey; - assert.isString(exportMapKey); - delete (response?.entries[0].data as any).exportMapKey; - assert.deepEqual(response, { - flags: ts.CompletionInfoFlags.MayIncludeAutoImports, - isGlobalCompletion: true, - isIncomplete: undefined, - isMemberCompletion: false, - isNewIdentifierLocation: false, - optionalReplacementSpan: { start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 } }, - entries: [entry], - }); + // `data.exportMapKey` contains a SymbolId so should not be mocked up with an expected value here. + // Just assert that it's a string and then delete it so we can compare everything else with `deepEqual`. + const exportMapKey = (response?.entries[0].data as any)?.exportMapKey; + assert.isString(exportMapKey); + delete (response?.entries[0].data as any).exportMapKey; + assert.deepEqual(response, { + flags: ts.CompletionInfoFlags.MayIncludeAutoImports, + isGlobalCompletion: true, + isIncomplete: undefined, + isMemberCompletion: false, + isNewIdentifierLocation: false, + optionalReplacementSpan: { start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 } }, + entries: [entry], + }); - const detailsRequestArgs: ts.projectSystem.protocol.CompletionDetailsRequestArgs = { - ...requestLocation, - entryNames: [{ name: "foo", source: "/a", data: { exportName: "foo", fileName: "/a.ts", exportMapKey } }], - }; + const detailsRequestArgs: ts.projectSystem.protocol.CompletionDetailsRequestArgs = { + ...requestLocation, + entryNames: [{ name: "foo", source: "/a", data: { exportName: "foo", fileName: "/a.ts", exportMapKey } }], + }; - const detailsResponse = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionDetails, detailsRequestArgs); - const detailsCommon: ts.projectSystem.protocol.CompletionEntryDetails & ts.CompletionEntryDetails = { - displayParts: [ - ts.keywordPart(ts.SyntaxKind.ConstKeyword), - ts.spacePart(), - ts.displayPart("foo", ts.SymbolDisplayPartKind.localName), - ts.punctuationPart(ts.SyntaxKind.ColonToken), - ts.spacePart(), - ts.displayPart("0", ts.SymbolDisplayPartKind.stringLiteral), + const detailsResponse = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionDetails, detailsRequestArgs); + const detailsCommon: ts.projectSystem.protocol.CompletionEntryDetails & ts.CompletionEntryDetails = { + displayParts: [ + ts.keywordPart(ts.SyntaxKind.ConstKeyword), + ts.spacePart(), + ts.displayPart("foo", ts.SymbolDisplayPartKind.localName), + ts.punctuationPart(ts.SyntaxKind.ColonToken), + ts.spacePart(), + ts.displayPart("0", ts.SymbolDisplayPartKind.stringLiteral), + ], + documentation: ts.emptyArray, + kind: ts.ScriptElementKind.constElement, + kindModifiers: ts.ScriptElementKindModifier.exportedModifier, + name: "foo", + source: [{ text: "./a", kind: "text" }], + sourceDisplay: [{ text: "./a", kind: "text" }], + }; + assert.deepEqual(detailsResponse, [ + { + codeActions: [ + { + description: `Add import from "./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: ts.emptyArray, - kind: ts.ScriptElementKind.constElement, - kindModifiers: ts.ScriptElementKindModifier.exportedModifier, - name: "foo", - source: [{ text: "./a", kind: "text" }], - sourceDisplay: [{ text: "./a", kind: "text" }], - }; - assert.deepEqual(detailsResponse, [ - { - codeActions: [ - { - description: `Add import from "./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, - }, - ], - tags: [], - ...detailsCommon, - }, - ]); + tags: [], + ...detailsCommon, + }, + ]); - interface CompletionDetailsFullRequest extends ts.projectSystem.protocol.FileLocationRequest { - readonly command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull; - readonly arguments: ts.projectSystem.protocol.CompletionDetailsRequestArgs; - } - interface CompletionDetailsFullResponse extends ts.projectSystem.protocol.Response { - readonly body?: readonly ts.CompletionEntryDetails[]; + interface CompletionDetailsFullRequest extends ts.projectSystem.protocol.FileLocationRequest { + readonly command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull; + readonly arguments: ts.projectSystem.protocol.CompletionDetailsRequestArgs; + } + interface CompletionDetailsFullResponse extends ts.projectSystem.protocol.Response { + readonly body?: readonly ts.CompletionEntryDetails[]; + } + const detailsFullResponse = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); + assert.deepEqual(detailsFullResponse, [ + { + codeActions: [ + { + description: `Add import from "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [ts.createTextChange(ts.createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], + }, + ], + commands: undefined, + } + ], + tags: [], + ...detailsCommon, } - const detailsFullResponse = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); - assert.deepEqual(detailsFullResponse, [ - { - codeActions: [ - { - description: `Add import from "./a"`, - changes: [ - { - fileName: "/b.ts", - textChanges: [ts.createTextChange(ts.createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], - }, - ], - commands: undefined, - } - ], - tags: [], - ...detailsCommon, - } - ]); - }); + ]); + }); - it("works when files are included from two different drives of windows", () => { - const projectRoot = "e:/myproject"; - const appPackage: ts.projectSystem.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: ts.projectSystem.File = { - path: `${projectRoot}/src/app.js`, - content: `import React from 'react'; + it("works when files are included from two different drives of windows", () => { + const projectRoot = "e:/myproject"; + const appPackage: ts.projectSystem.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: ts.projectSystem.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: ts.projectSystem.File = { - path: `${localAtTypes}/react/package.json`, - content: JSON.stringify({ - name: "@types/react", - version: "16.9.14", - }) - }; - const localReact: ts.projectSystem.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: ts.projectSystem.File = { + path: `${localAtTypes}/react/package.json`, + content: JSON.stringify({ + name: "@types/react", + version: "16.9.14", + }) + }; + const localReact: ts.projectSystem.File = { + path: `${localAtTypes}/react/index.d.ts`, + content: `import * as PropTypes from 'prop-types'; ` - }; - const localReactRouterDomPackage: ts.projectSystem.File = { - path: `${localNodeModules}/react-router-dom/package.json`, - content: JSON.stringify({ - name: "react-router-dom", - version: "5.1.2", - }) - }; - const localReactRouterDom: ts.projectSystem.File = { - path: `${localNodeModules}/react-router-dom/index.js`, - content: `export function foo() {}` - }; - const localPropTypesPackage: ts.projectSystem.File = { - path: `${localAtTypes}/prop-types/package.json`, - content: JSON.stringify({ - name: "@types/prop-types", - version: "15.7.3", - }) - }; - const localPropTypes: ts.projectSystem.File = { - path: `${localAtTypes}/prop-types/index.d.ts`, - content: `export type ReactComponentLike = + }; + const localReactRouterDomPackage: ts.projectSystem.File = { + path: `${localNodeModules}/react-router-dom/package.json`, + content: JSON.stringify({ + name: "react-router-dom", + version: "5.1.2", + }) + }; + const localReactRouterDom: ts.projectSystem.File = { + path: `${localNodeModules}/react-router-dom/index.js`, + content: `export function foo() {}` + }; + const localPropTypesPackage: ts.projectSystem.File = { + path: `${localAtTypes}/prop-types/package.json`, + content: JSON.stringify({ + name: "@types/prop-types", + version: "15.7.3", + }) + }; + const localPropTypes: ts.projectSystem.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: ts.projectSystem.File = { - path: `${globalAtTypes}/react-router-dom/package.json`, - content: JSON.stringify({ - name: "@types/react-router-dom", - version: "5.1.2", - }) - }; - const globalReactRouterDom: ts.projectSystem.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: ts.projectSystem.File = { + path: `${globalAtTypes}/react-router-dom/package.json`, + content: JSON.stringify({ + name: "@types/react-router-dom", + version: "5.1.2", + }) + }; + const globalReactRouterDom: ts.projectSystem.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: ts.projectSystem.File = { - path: `${globalAtTypes}/react/package.json`, - content: localReactPackage.content - }; - const globalReact: ts.projectSystem.File = { - path: `${globalAtTypes}/react/index.d.ts`, - content: localReact.content - }; + }; + const globalReactPackage: ts.projectSystem.File = { + path: `${globalAtTypes}/react/package.json`, + content: localReactPackage.content + }; + const globalReact: ts.projectSystem.File = { + path: `${globalAtTypes}/react/index.d.ts`, + content: localReact.content + }; - const filesInProject = [ - appFile, - localReact, - localPropTypes, - globalReactRouterDom, - globalReact, - ]; - const files = [ - ...filesInProject, - appPackage, - ts.projectSystem.libFile, - localReactPackage, - localReactRouterDomPackage, localReactRouterDom, - localPropTypesPackage, - globalReactRouterDomPackage, - globalReactPackage, - ]; + const filesInProject = [ + appFile, + localReact, + localPropTypes, + globalReactRouterDom, + globalReact, + ]; + const files = [ + ...filesInProject, + appPackage, + ts.projectSystem.libFile, + localReactPackage, + localReactRouterDomPackage, localReactRouterDom, + localPropTypesPackage, + globalReactRouterDomPackage, + globalReactPackage, + ]; - const host = ts.projectSystem.createServerHost(files, { windowsStyleRoot: "c:/" }); - const session = ts.projectSystem.createSession(host, { - typingsInstaller: new ts.projectSystem.TestTypingsInstaller(globalCacheLocation, /*throttleLimit*/ 5, host), - }); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([appFile], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - const windowsStyleLibFilePath = "c:/" + ts.projectSystem.libFile.path.substring(1); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], filesInProject.map(f => f.path).concat(windowsStyleLibFilePath)); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, - arguments: { - file: appFile.path, - line: 5, - offset: 1, - includeExternalModuleExports: true, - includeInsertTextCompletions: true - } - }); + const host = ts.projectSystem.createServerHost(files, { windowsStyleRoot: "c:/" }); + const session = ts.projectSystem.createSession(host, { + typingsInstaller: new ts.projectSystem.TestTypingsInstaller(globalCacheLocation, /*throttleLimit*/ 5, host), + }); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([appFile], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + const windowsStyleLibFilePath = "c:/" + ts.projectSystem.libFile.path.substring(1); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], filesInProject.map(f => f.path).concat(windowsStyleLibFilePath)); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, + arguments: { + file: appFile.path, + line: 5, + offset: 1, + includeExternalModuleExports: true, + includeInsertTextCompletions: true + } }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/completionsIncomplete.ts b/src/testRunner/unittests/tsserver/completionsIncomplete.ts index 3d8fc0c476ecb..9b79402a35c68 100644 --- a/src/testRunner/unittests/tsserver/completionsIncomplete.ts +++ b/src/testRunner/unittests/tsserver/completionsIncomplete.ts @@ -1,256 +1,256 @@ namespace ts.projectSystem { - function createExportingModuleFile(path: string, exportPrefix: string, exportCount: number): ts.projectSystem.File { - return { - path, - content: ts.fill(exportCount, i => `export const ${exportPrefix}_${i} = ${i};`).join("\n"), - }; - } +function createExportingModuleFile(path: string, exportPrefix: string, exportCount: number): ts.projectSystem.File { + return { + path, + content: ts.fill(exportCount, i => `export const ${exportPrefix}_${i} = ${i};`).join("\n"), + }; +} - function createExportingModuleFiles(pathPrefix: string, fileCount: number, exportCount: number, getExportPrefix: (fileIndex: number) => string): ts.projectSystem.File[] { - return ts.fill(fileCount, fileIndex => createExportingModuleFile(`${pathPrefix}_${fileIndex}.ts`, getExportPrefix(fileIndex), exportCount)); - } +function createExportingModuleFiles(pathPrefix: string, fileCount: number, exportCount: number, getExportPrefix: (fileIndex: number) => string): ts.projectSystem.File[] { + return ts.fill(fileCount, fileIndex => createExportingModuleFile(`${pathPrefix}_${fileIndex}.ts`, getExportPrefix(fileIndex), exportCount)); +} - function createNodeModulesPackage(packageName: string, fileCount: number, exportCount: number, getExportPrefix: (fileIndex: number) => string): ts.projectSystem.File[] { - const exportingFiles = createExportingModuleFiles(`/node_modules/${packageName}/file`, fileCount, exportCount, getExportPrefix); - return [ - { - path: `/node_modules/${packageName}/package.json`, - content: `{ "types": "index.d.ts" }`, - }, - { - path: `/node_modules/${packageName}/index.d.ts`, - content: exportingFiles - .map(f => `export * from "./${ts.removeFileExtension(ts.convertToRelativePath(f.path, `/node_modules/${packageName}/`, ts.identity))}";`) - .join("\n") + `\nexport default function main(): void;`, - }, - ...exportingFiles, - ]; - } +function createNodeModulesPackage(packageName: string, fileCount: number, exportCount: number, getExportPrefix: (fileIndex: number) => string): ts.projectSystem.File[] { + const exportingFiles = createExportingModuleFiles(`/node_modules/${packageName}/file`, fileCount, exportCount, getExportPrefix); + return [ + { + path: `/node_modules/${packageName}/package.json`, + content: `{ "types": "index.d.ts" }`, + }, + { + path: `/node_modules/${packageName}/index.d.ts`, + content: exportingFiles + .map(f => `export * from "./${ts.removeFileExtension(ts.convertToRelativePath(f.path, `/node_modules/${packageName}/`, ts.identity))}";`) + .join("\n") + `\nexport default function main(): void;`, + }, + ...exportingFiles, + ]; +} - const indexFile: ts.projectSystem.File = { - path: "/index.ts", - content: "" - }; +const indexFile: ts.projectSystem.File = { + path: "/index.ts", + content: "" +}; - const tsconfigFile: ts.projectSystem.File = { - path: "/tsconfig.json", - content: `{ "compilerOptions": { "module": "commonjs" } }` - }; +const tsconfigFile: ts.projectSystem.File = { + path: "/tsconfig.json", + content: `{ "compilerOptions": { "module": "commonjs" } }` +}; - const packageJsonFile: ts.projectSystem.File = { - path: "/package.json", - content: `{ "dependencies": { "dep-a": "*" } }`, - }; +const packageJsonFile: ts.projectSystem.File = { + path: "/package.json", + content: `{ "dependencies": { "dep-a": "*" } }`, +}; - describe("unittests:: tsserver:: completionsIncomplete", () => { - it("works", () => { - const excessFileCount = ts.Completions.moduleSpecifierResolutionLimit + 50; - const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit + excessFileCount, 1, i => `aa_${i}_`); - const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); - ts.projectSystem.openFilesForSession([indexFile], session); +describe("unittests:: tsserver:: completionsIncomplete", () => { + it("works", () => { + const excessFileCount = ts.Completions.moduleSpecifierResolutionLimit + 50; + const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit + excessFileCount, 1, i => `aa_${i}_`); + const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "a", completions => { - assert(completions.isIncomplete); - assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), ts.Completions.moduleSpecifierResolutionLimit); - assert.lengthOf(completions.entries.filter(entry => entry.source && !(entry.data as any)?.moduleSpecifier), excessFileCount); - }) - .continueTyping("a", completions => { - assert(completions.isIncomplete); - assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), ts.Completions.moduleSpecifierResolutionLimit * 2); - }) - .continueTyping("_", completions => { - assert(!completions.isIncomplete); - assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), exportingFiles.length); - }); + typeToTriggerCompletions(indexFile.path, "a", completions => { + assert(completions.isIncomplete); + assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), ts.Completions.moduleSpecifierResolutionLimit); + assert.lengthOf(completions.entries.filter(entry => entry.source && !(entry.data as any)?.moduleSpecifier), excessFileCount); + }) + .continueTyping("a", completions => { + assert(completions.isIncomplete); + assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), ts.Completions.moduleSpecifierResolutionLimit * 2); + }) + .continueTyping("_", completions => { + assert(!completions.isIncomplete); + assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier), exportingFiles.length); }); + }); - it("resolves more when available from module specifier cache (1)", () => { - const exportingFiles = createExportingModuleFiles(`/lib/a`, 50, 50, i => `aa_${i}_`); - const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); - ts.projectSystem.openFilesForSession([indexFile], session); + it("resolves more when available from module specifier cache (1)", () => { + const exportingFiles = createExportingModuleFiles(`/lib/a`, 50, 50, i => `aa_${i}_`); + const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "a", completions => { - assert(!completions.isIncomplete); - }); + typeToTriggerCompletions(indexFile.path, "a", completions => { + assert(!completions.isIncomplete); }); + }); - it("resolves more when available from module specifier cache (2)", () => { - const excessFileCount = 50; - const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit + excessFileCount, 1, i => `aa_${i}_`); - const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); - ts.projectSystem.openFilesForSession([indexFile], session); + it("resolves more when available from module specifier cache (2)", () => { + const excessFileCount = 50; + const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit + excessFileCount, 1, i => `aa_${i}_`); + const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "a", completions => assert(completions.isIncomplete)) - .backspace() - .type("a", completions => assert(!completions.isIncomplete)); - }); + typeToTriggerCompletions(indexFile.path, "a", completions => assert(completions.isIncomplete)) + .backspace() + .type("a", completions => assert(!completions.isIncomplete)); + }); - it("ambient module specifier resolutions do not count against the resolution limit", () => { - const ambientFiles = ts.fill(100, (i): ts.projectSystem.File => ({ - path: `/lib/ambient_${i}.ts`, - content: `declare module "ambient_${i}" { export const aa_${i} = ${i}; }`, - })); + it("ambient module specifier resolutions do not count against the resolution limit", () => { + const ambientFiles = ts.fill(100, (i): ts.projectSystem.File => ({ + path: `/lib/ambient_${i}.ts`, + content: `declare module "ambient_${i}" { export const aa_${i} = ${i}; }`, + })); - const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit, 5, i => `aa_${i}_`); - const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...ambientFiles, ...exportingFiles]); - ts.projectSystem.openFilesForSession([indexFile], session); + const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit, 5, i => `aa_${i}_`); + const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...ambientFiles, ...exportingFiles]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "a", completions => { - assert(!completions.isIncomplete); - assert.lengthOf(completions.entries.filter(e => (e.data as any)?.moduleSpecifier), ambientFiles.length * 5 + exportingFiles.length); - }); + typeToTriggerCompletions(indexFile.path, "a", completions => { + assert(!completions.isIncomplete); + assert.lengthOf(completions.entries.filter(e => (e.data as any)?.moduleSpecifier), ambientFiles.length * 5 + exportingFiles.length); }); + }); - it("works with PackageJsonAutoImportProvider", () => { - const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit, 1, i => `aa_${i}_`); - const nodeModulesPackage = createNodeModulesPackage("dep-a", 50, 1, i => `depA_${i}_`); - const { typeToTriggerCompletions, assertCompletionDetailsOk, session } = setup([tsconfigFile, packageJsonFile, indexFile, ...exportingFiles, ...nodeModulesPackage]); - ts.projectSystem.openFilesForSession([indexFile], session); + it("works with PackageJsonAutoImportProvider", () => { + const exportingFiles = createExportingModuleFiles(`/lib/a`, ts.Completions.moduleSpecifierResolutionLimit, 1, i => `aa_${i}_`); + const nodeModulesPackage = createNodeModulesPackage("dep-a", 50, 1, i => `depA_${i}_`); + const { typeToTriggerCompletions, assertCompletionDetailsOk, session } = setup([tsconfigFile, packageJsonFile, indexFile, ...exportingFiles, ...nodeModulesPackage]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "a", completions => assert(completions.isIncomplete)) - .continueTyping("_", completions => { - assert(!completions.isIncomplete); - assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier?.startsWith("dep-a")), 50); - assertCompletionDetailsOk(indexFile.path, completions.entries.find(entry => (entry.data as any)?.moduleSpecifier?.startsWith("dep-a"))!); - }); - }); + typeToTriggerCompletions(indexFile.path, "a", completions => assert(completions.isIncomplete)) + .continueTyping("_", completions => { + assert(!completions.isIncomplete); + assert.lengthOf(completions.entries.filter(entry => (entry.data as any)?.moduleSpecifier?.startsWith("dep-a")), 50); + assertCompletionDetailsOk(indexFile.path, completions.entries.find(entry => (entry.data as any)?.moduleSpecifier?.startsWith("dep-a"))!); + }); + }); - it("works for transient symbols between requests", () => { - const constantsDts: ts.projectSystem.File = { - path: "/lib/foo/constants.d.ts", - content: ` + it("works for transient symbols between requests", () => { + const constantsDts: ts.projectSystem.File = { + path: "/lib/foo/constants.d.ts", + content: ` type Signals = "SIGINT" | "SIGABRT"; declare const exp: {} & { [K in Signals]: K }; export = exp;`, - }; - const exportingFiles = createExportingModuleFiles("/lib/a", ts.Completions.moduleSpecifierResolutionLimit, 1, i => `S${i}`); - const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles, constantsDts]); - ts.projectSystem.openFilesForSession([indexFile], session); + }; + const exportingFiles = createExportingModuleFiles("/lib/a", ts.Completions.moduleSpecifierResolutionLimit, 1, i => `S${i}`); + const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles, constantsDts]); + ts.projectSystem.openFilesForSession([indexFile], session); - typeToTriggerCompletions(indexFile.path, "s", completions => { - const sigint = completions.entries.find(e => e.name === "SIGINT"); - assert(sigint); - assert(!(sigint.data as any).moduleSpecifier); - }) - .continueTyping("i", completions => { - const sigint = completions.entries.find(e => e.name === "SIGINT"); - assert((sigint!.data as any).moduleSpecifier); - }); + typeToTriggerCompletions(indexFile.path, "s", completions => { + const sigint = completions.entries.find(e => e.name === "SIGINT"); + assert(sigint); + assert(!(sigint.data as any).moduleSpecifier); + }) + .continueTyping("i", completions => { + const sigint = completions.entries.find(e => e.name === "SIGINT"); + assert((sigint!.data as any).moduleSpecifier); }); }); +}); - function setup(files: ts.projectSystem.File[]) { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Configure, - arguments: { - preferences: { - allowIncompleteCompletions: true, - includeCompletionsForModuleExports: true, - includeCompletionsWithInsertText: true, - includeCompletionsForImportStatements: true, - includePackageJsonAutoImports: "auto", - } +function setup(files: ts.projectSystem.File[]) { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Configure, + arguments: { + preferences: { + allowIncompleteCompletions: true, + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + includeCompletionsForImportStatements: true, + includePackageJsonAutoImports: "auto", } - }); - - return { host, session, projectService, typeToTriggerCompletions, assertCompletionDetailsOk }; + } + }); - function typeToTriggerCompletions(fileName: string, typedCharacters: string, cb?: (completions: ts.projectSystem.protocol.CompletionInfo) => void) { - const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!; - return type(typedCharacters, cb, /*isIncompleteContinuation*/ false); + return { host, session, projectService, typeToTriggerCompletions, assertCompletionDetailsOk }; - function type(typedCharacters: string, cb: ((completions: ts.projectSystem.protocol.CompletionInfo) => void) | undefined, isIncompleteContinuation: boolean) { - const file = ts.Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName)); - const { line, character } = ts.getLineAndCharacterOfPosition(file, file.text.length); - const oneBasedEditPosition = { line: line + 1, offset: character + 1 }; - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName, - textChanges: [{ - newText: typedCharacters, - start: oneBasedEditPosition, - end: oneBasedEditPosition, - }], - }], - }, - }); + function typeToTriggerCompletions(fileName: string, typedCharacters: string, cb?: (completions: ts.projectSystem.protocol.CompletionInfo) => void) { + const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!; + return type(typedCharacters, cb, /*isIncompleteContinuation*/ false); - const response = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, - arguments: { - file: fileName, - line: oneBasedEditPosition.line, - offset: oneBasedEditPosition.offset, - triggerKind: isIncompleteContinuation - ? ts.projectSystem.protocol.CompletionTriggerKind.TriggerForIncompleteCompletions - : undefined, - } - }).response as ts.projectSystem.protocol.CompletionInfo; - cb?.(ts.Debug.checkDefined(response)); - return { - backspace, - continueTyping: (typedCharacters: string, cb: (completions: ts.projectSystem.protocol.CompletionInfo) => void) => { - return type(typedCharacters, cb, !!response.isIncomplete); - }, - }; - } - - function backspace(n = 1) { - const file = ts.Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName)); - const startLineCharacter = ts.getLineAndCharacterOfPosition(file, file.text.length - n); - const endLineCharacter = ts.getLineAndCharacterOfPosition(file, file.text.length); - const oneBasedStartPosition = { line: startLineCharacter.line + 1, offset: startLineCharacter.character + 1 }; - const oneBasedEndPosition = { line: endLineCharacter.line + 1, offset: endLineCharacter.character + 1 }; - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName, - textChanges: [{ - newText: "", - start: oneBasedStartPosition, - end: oneBasedEndPosition, - }], + function type(typedCharacters: string, cb: ((completions: ts.projectSystem.protocol.CompletionInfo) => void) | undefined, isIncompleteContinuation: boolean) { + const file = ts.Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName)); + const { line, character } = ts.getLineAndCharacterOfPosition(file, file.text.length); + const oneBasedEditPosition = { line: line + 1, offset: character + 1 }; + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName, + textChanges: [{ + newText: typedCharacters, + start: oneBasedEditPosition, + end: oneBasedEditPosition, }], - }, - }); + }], + }, + }); - return { - backspace, - type: (typedCharacters: string, cb: (completions: ts.projectSystem.protocol.CompletionInfo) => void) => { - return type(typedCharacters, cb, /*isIncompleteContinuation*/ false); - }, - }; - } + const response = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, + arguments: { + file: fileName, + line: oneBasedEditPosition.line, + offset: oneBasedEditPosition.offset, + triggerKind: isIncompleteContinuation + ? ts.projectSystem.protocol.CompletionTriggerKind.TriggerForIncompleteCompletions + : undefined, + } + }).response as ts.projectSystem.protocol.CompletionInfo; + cb?.(ts.Debug.checkDefined(response)); + return { + backspace, + continueTyping: (typedCharacters: string, cb: (completions: ts.projectSystem.protocol.CompletionInfo) => void) => { + return type(typedCharacters, cb, !!response.isIncomplete); + }, + }; } - function assertCompletionDetailsOk(fileName: string, entry: ts.projectSystem.protocol.CompletionEntry) { - const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!; + function backspace(n = 1) { const file = ts.Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName)); - const { line, character } = ts.getLineAndCharacterOfPosition(file, file.text.length - 1); - const details = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, + const startLineCharacter = ts.getLineAndCharacterOfPosition(file, file.text.length - n); + const endLineCharacter = ts.getLineAndCharacterOfPosition(file, file.text.length); + const oneBasedStartPosition = { line: startLineCharacter.line + 1, offset: startLineCharacter.character + 1 }; + const oneBasedEndPosition = { line: endLineCharacter.line + 1, offset: endLineCharacter.character + 1 }; + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, arguments: { - file: fileName, - line: line + 1, - offset: character + 1, - entryNames: [{ - name: entry.name, - source: entry.source, - data: entry.data, - }] - } - }).response as ts.projectSystem.protocol.CompletionEntryDetails[]; + changedFiles: [{ + fileName, + textChanges: [{ + newText: "", + start: oneBasedStartPosition, + end: oneBasedEndPosition, + }], + }], + }, + }); - assert(details[0]); - assert(details[0].codeActions); - assert(details[0].codeActions[0].changes[0].textChanges[0].newText.includes(`"${(entry.data as any).moduleSpecifier}"`)); - return details; + return { + backspace, + type: (typedCharacters: string, cb: (completions: ts.projectSystem.protocol.CompletionInfo) => void) => { + return type(typedCharacters, cb, /*isIncompleteContinuation*/ false); + }, + }; } } + + function assertCompletionDetailsOk(fileName: string, entry: ts.projectSystem.protocol.CompletionEntry) { + const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!; + const file = ts.Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName)); + const { line, character } = ts.getLineAndCharacterOfPosition(file, file.text.length - 1); + const details = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, + arguments: { + file: fileName, + line: line + 1, + offset: character + 1, + entryNames: [{ + name: entry.name, + source: entry.source, + data: entry.data, + }] + } + }).response as ts.projectSystem.protocol.CompletionEntryDetails[]; + + assert(details[0]); + assert(details[0].codeActions); + assert(details[0].codeActions[0].changes[0].textChanges[0].newText.includes(`"${(entry.data as any).moduleSpecifier}"`)); + return details; + } +} } diff --git a/src/testRunner/unittests/tsserver/configFileSearch.ts b/src/testRunner/unittests/tsserver/configFileSearch.ts index f03159f333fff..ad0d80f4f0557 100644 --- a/src/testRunner/unittests/tsserver/configFileSearch.ts +++ b/src/testRunner/unittests/tsserver/configFileSearch.ts @@ -1,186 +1,186 @@ 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 = ts.projectSystem.createServerHost([f1, configFile]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); +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 = ts.projectSystem.createServerHost([f1, configFile]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); + + ts.projectSystem.checkNumberOfConfiguredProjects(service, 0); + ts.projectSystem.checkNumberOfInferredProjects(service, 1); + + service.closeClientFile(f1.path); + service.openClientFile(f1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(service, 1); + ts.projectSystem.checkNumberOfInferredProjects(service, 0); + }); - ts.projectSystem.checkNumberOfConfiguredProjects(service, 0); - ts.projectSystem.checkNumberOfInferredProjects(service, 1); + 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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile, configFile, configFile2]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + const typeRootLocations = ts.projectSystem.getTypeRootsFromLocation(configFileLocation); + ts.projectSystem.checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project and not configured project + host.deleteFile(configFile.path); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + }); - service.closeClientFile(f1.path); - service.openClientFile(f1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(service, 1); - ts.projectSystem.checkNumberOfInferredProjects(service, 0); - }); + 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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile, configFile, configFile2]); + const service = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true }); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project with project root path set + host.deleteFile(configFile.path); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.equal(service.inferredProjects[0].projectRootPath, projectDir); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(projectDir), /*recursive*/ true); + }); - 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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile, configFile, configFile2]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - const typeRootLocations = ts.projectSystem.getTypeRootsFromLocation(configFileLocation); - ts.projectSystem.checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project and not configured project - host.deleteFile(configFile.path); + describe("when the opened file is not from project root", () => { + const projectRoot = "/a/b/projects/project"; + const file: ts.projectSystem.File = { + path: `${projectRoot}/src/index.ts`, + content: "let y = 10" + }; + const tsconfig: ts.projectSystem.File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const dirOfFile = ts.getDirectoryPath(file.path); + function openClientFile(files: ts.projectSystem.File[]) { + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); + return { host, projectService }; + } + + function verifyConfiguredProject(host: ts.projectSystem.TestServerHost, projectService: ts.projectSystem.TestProjectService, orphanInferredProject?: boolean) { + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); + const project = ts.Debug.checkDefined(projectService.configuredProjects.get(tsconfig.path)); + + if (orphanInferredProject) { + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.isOrphan()); + } + + ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path, tsconfig.path]); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, tsconfig.path]); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(ts.projectSystem.getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); + } + + function verifyInferredProject(host: ts.projectSystem.TestServerHost, projectService: ts.projectSystem.TestProjectService) { + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + + const filesToWatch = [ts.projectSystem.libFile.path, ...ts.projectSystem.getConfigFilesToWatch(dirOfFile)]; + ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkWatchedFiles(host, filesToWatch); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); + } + + it("tsconfig for the file exists", () => { + const { host, projectService } = openClientFile([file, ts.projectSystem.libFile, tsconfig]); + verifyConfiguredProject(host, projectService); + + host.deleteFile(tsconfig.path); host.runQueuedTimeoutCallbacks(); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); - }); + verifyInferredProject(host, projectService); - 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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile, configFile, configFile2]); - const service = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true }); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project with project root path set - host.deleteFile(configFile.path); + host.writeFile(tsconfig.path, tsconfig.content); host.runQueuedTimeoutCallbacks(); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - assert.equal(service.inferredProjects[0].projectRootPath, projectDir); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(projectDir), /*recursive*/ true); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); }); - describe("when the opened file is not from project root", () => { - const projectRoot = "/a/b/projects/project"; - const file: ts.projectSystem.File = { - path: `${projectRoot}/src/index.ts`, - content: "let y = 10" - }; - const tsconfig: ts.projectSystem.File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - const dirOfFile = ts.getDirectoryPath(file.path); - function openClientFile(files: ts.projectSystem.File[]) { - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); - return { host, projectService }; - } - - function verifyConfiguredProject(host: ts.projectSystem.TestServerHost, projectService: ts.projectSystem.TestProjectService, orphanInferredProject?: boolean) { - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); - const project = ts.Debug.checkDefined(projectService.configuredProjects.get(tsconfig.path)); - - if (orphanInferredProject) { - const inferredProject = projectService.inferredProjects[0]; - assert.isTrue(inferredProject.isOrphan()); - } - - ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path, tsconfig.path]); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, tsconfig.path]); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(ts.projectSystem.getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); - } - - function verifyInferredProject(host: ts.projectSystem.TestServerHost, projectService: ts.projectSystem.TestProjectService) { - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - assert.isDefined(project); - - const filesToWatch = [ts.projectSystem.libFile.path, ...ts.projectSystem.getConfigFilesToWatch(dirOfFile)]; - ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkWatchedFiles(host, filesToWatch); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, ts.projectSystem.getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); - } - - it("tsconfig for the file exists", () => { - const { host, projectService } = openClientFile([file, ts.projectSystem.libFile, tsconfig]); - verifyConfiguredProject(host, projectService); - - host.deleteFile(tsconfig.path); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - - host.writeFile(tsconfig.path, tsconfig.content); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - }); - - it("tsconfig for the file does not exist", () => { - const { host, projectService } = openClientFile([file, ts.projectSystem.libFile]); - verifyInferredProject(host, projectService); + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile([file, ts.projectSystem.libFile]); + verifyInferredProject(host, projectService); - host.writeFile(tsconfig.path, tsconfig.content); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + host.writeFile(tsconfig.path, tsconfig.content); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - host.deleteFile(tsconfig.path); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - }); + host.deleteFile(tsconfig.path); + 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 = ts.projectSystem.createServerHost([ts.projectSystem.libFile, { path, content: "const x = 10" }], { useCaseSensitiveFileNames: true }); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRootPath); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkWatchedFilesDetailed(host, [ts.projectSystem.libFile.path, ...ts.projectSystem.getConfigFilesToWatch(root)], 1); - } + 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 = ts.projectSystem.createServerHost([ts.projectSystem.libFile, { path, content: "const x = 10" }], { useCaseSensitiveFileNames: true }); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRootPath); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkWatchedFilesDetailed(host, [ts.projectSystem.libFile.path, ...ts.projectSystem.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 not present", () => { + verifyConfigFileWatch(/*projectRootPath*/ undefined); + }); + 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 51e8e877abc61..89a4fb6d90d50 100644 --- a/src/testRunner/unittests/tsserver/configuredProjects.ts +++ b/src/testRunner/unittests/tsserver/configuredProjects.ts @@ -1,1395 +1,1395 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: ConfiguredProjects", () => { - it("create configured project without file list", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: ` +describe("unittests:: tsserver:: ConfiguredProjects", () => { + it("create configured project without file list", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: ` { "compilerOptions": {}, "exclude": [ "e" ] }` - }; - const file1: ts.projectSystem.File = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - - const host = ts.projectSystem.createServerHost([configFile, ts.projectSystem.libFile, file1, file2, file3]); - const projectService = ts.projectSystem.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)}`); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, file2.path, configFile.path]); - ts.projectSystem.checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - ts.projectSystem.checkWatchedFiles(host, [configFile.path, file2.path, ts.projectSystem.libFile.path]); - const configFileDirectory = ts.getDirectoryPath(configFile.path); - ts.projectSystem.checkWatchedDirectories(host, [configFileDirectory, ts.combinePaths(configFileDirectory, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - }); + }; + const file1: ts.projectSystem.File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = ts.projectSystem.createServerHost([configFile, ts.projectSystem.libFile, file1, file2, file3]); + const projectService = ts.projectSystem.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)}`); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, file2.path, configFile.path]); + ts.projectSystem.checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + ts.projectSystem.checkWatchedFiles(host, [configFile.path, file2.path, ts.projectSystem.libFile.path]); + const configFileDirectory = ts.getDirectoryPath(configFile.path); + ts.projectSystem.checkWatchedDirectories(host, [configFileDirectory, ts.combinePaths(configFileDirectory, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); + }); - it("create configured project with the file list", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: ` + it("create configured project with the file list", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: ` { "compilerOptions": {}, "include": ["*.ts"] }` - }; - const file1: ts.projectSystem.File = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/c/f3.ts", - content: "let z = 1" - }; - - const host = ts.projectSystem.createServerHost([configFile, ts.projectSystem.libFile, file1, file2, file3]); - const projectService = ts.projectSystem.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)}`); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, file2.path, configFile.path]); - ts.projectSystem.checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - ts.projectSystem.checkWatchedFiles(host, [configFile.path, file2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkWatchedDirectories(host, [ts.getDirectoryPath(configFile.path)], /*recursive*/ false); - }); + }; + const file1: ts.projectSystem.File = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + + const host = ts.projectSystem.createServerHost([configFile, ts.projectSystem.libFile, file1, file2, file3]); + const projectService = ts.projectSystem.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)}`); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, file2.path, configFile.path]); + ts.projectSystem.checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + ts.projectSystem.checkWatchedFiles(host, [configFile.path, file2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkWatchedDirectories(host, [ts.getDirectoryPath(configFile.path)], /*recursive*/ false); + }); - it("add and then remove a config file in a folder with loose files", () => { - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: `{ + it("add and then remove a config file in a folder with loose files", () => { + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: `{ "files": ["commonFile1.ts"] }` - }; - const commonFile1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/commonFile1.ts`, - content: "let x = 1" - }; - const commonFile2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/commonFile2.ts`, - content: "let y = 1" - }; - - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, commonFile1, commonFile2]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); - const watchedFiles = ts.projectSystem.getConfigFilesToWatch(ts.tscWatch.projectRoot).concat(ts.projectSystem.libFile.path); - ts.projectSystem.checkWatchedFiles(host, watchedFiles); - - // Add a tsconfig file - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - - projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [ts.projectSystem.libFile.path, commonFile1.path, configFile.path]); - ts.projectSystem.checkWatchedFiles(host, watchedFiles); - - // remove the tsconfig file - host.deleteFile(configFile.path); - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); - - host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkWatchedFiles(host, watchedFiles); - }); - - it("add new files to a configured project without file list", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.libFile, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - const configFileDir = ts.getDirectoryPath(configFile.path); - ts.projectSystem.checkWatchedDirectories(host, [configFileDir, ts.combinePaths(configFileDir, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + }; + const commonFile1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/commonFile1.ts`, + content: "let x = 1" + }; + const commonFile2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/commonFile2.ts`, + content: "let y = 1" + }; + + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, commonFile1, commonFile2]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); + const watchedFiles = ts.projectSystem.getConfigFilesToWatch(ts.tscWatch.projectRoot).concat(ts.projectSystem.libFile.path); + ts.projectSystem.checkWatchedFiles(host, watchedFiles); + + // Add a tsconfig file + host.writeFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + + projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [ts.projectSystem.libFile.path, commonFile1.path, configFile.path]); + ts.projectSystem.checkWatchedFiles(host, watchedFiles); + + // remove the tsconfig file + host.deleteFile(configFile.path); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); + + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkWatchedFiles(host, watchedFiles); + }); - // add a new ts file - host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); - host.checkTimeoutQueueLengthAndRun(2); - // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - }); + it("add new files to a configured project without file list", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.libFile, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + const configFileDir = ts.getDirectoryPath(configFile.path); + ts.projectSystem.checkWatchedDirectories(host, [configFileDir, ts.combinePaths(configFileDir, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + + // add a new ts file + host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); + host.checkTimeoutQueueLengthAndRun(2); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + }); - it("should ignore non-existing files specified in the config file", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should ignore non-existing files specified in the config file", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "files": [ "commonFile1.ts", "commonFile3.ts" ] }` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - projectService.openClientFile(ts.projectSystem.commonFile2.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - }); - - it("handle recreated files correctly", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - - // delete commonFile2 - host.deleteFile(ts.projectSystem.commonFile2.path); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + projectService.openClientFile(ts.projectSystem.commonFile2.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + }); - // re-add commonFile2 - host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - }); + it("handle recreated files correctly", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + + // delete commonFile2 + host.deleteFile(ts.projectSystem.commonFile2.path); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + + // re-add commonFile2 + host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + }); - it("files explicitly excluded in config file", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("files explicitly excluded in config file", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "exclude": ["/a/c"] }` - }; - const excludedFile1: ts.projectSystem.File = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, excludedFile1, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - projectService.openClientFile(excludedFile1.path); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - }); + }; + const excludedFile1: ts.projectSystem.File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, excludedFile1, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + }); - it("should properly handle module resolution changes in config file", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: ts.projectSystem.File = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: ts.projectSystem.File = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const randomFile: ts.projectSystem.File = { - path: "/a/file1.ts", - content: `export interface T {}` - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should properly handle module resolution changes in config file", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: ts.projectSystem.File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: ts.projectSystem.File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const randomFile: ts.projectSystem.File = { + path: "/a/file1.ts", + content: `export interface T {}` + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["${file1.path}"] }` - }; - const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - projectService.openClientFile(nodeModuleFile.path); - projectService.openClientFile(classicModuleFile.path); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - const inferredProject0 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); - - host.writeFile(configFile.path, `{ + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + const inferredProject0 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); + + host.writeFile(configFile.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "files": ["${file1.path}"] }`); - host.checkTimeoutQueueLengthAndRun(2); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 - ts.projectSystem.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]; - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); - - // Open random file and it will reuse first inferred project - projectService.openClientFile(randomFile.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project - assert.strictEqual(projectService.inferredProjects[1], inferredProject1); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); - }); + host.checkTimeoutQueueLengthAndRun(2); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 + ts.projectSystem.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]; + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + + // Open random file and it will reuse first inferred project + projectService.openClientFile(randomFile.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + ts.projectSystem.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: ts.projectSystem.File = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - }); + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + }); - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - }); + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + }); - it("should tolerate config file errors and still try to build a project", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should tolerate config file errors and still try to build a project", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6", "allowAnything": true }, "someOtherProperty": {} }` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - }); + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [ts.projectSystem.commonFile1.path, ts.projectSystem.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: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + 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: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts", "main2.ts" ] }` - }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - ts.projectSystem.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); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isTrue(project.hasOpenRef()); // file2 - assert.isFalse(project.isClosed()); - }); + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + + projectService.closeClientFile(file1.path); + ts.projectSystem.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); + ts.projectSystem.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: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + 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: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(ts.projectSystem.libFile.path); - ts.projectSystem.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: `${ts.tscWatch.projectRoot}/a/b/f1.ts`, - content: "export let x = 5" - }; - const file2 = { - path: `${ts.tscWatch.projectRoot}/a/c/f2.ts`, - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: `${ts.tscWatch.projectRoot}/a/c/f3.ts`, - content: "export let y = 1" - }; - const configFile = { - path: `${ts.tscWatch.projectRoot}/a/c/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - - const host = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - - projectService.openClientFile(file3.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - host.writeFile(file2.path, file2.content); - - host.checkTimeoutQueueLengthAndRun(2); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectRootFiles(ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - host.writeFile(configFile.path, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.checkProjectRootFiles(ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); - - host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); + }; + const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + + projectService.closeClientFile(file1.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + + projectService.openClientFile(ts.projectSystem.libFile.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); + assert.isFalse(project.hasOpenRef()); // No files + project closed + assert.isTrue(project.isClosed()); + }); - it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/src/file1.ts", - content: "let x = 1;" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/src/file2.ts", - content: "let y = 1;" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/file3.ts", - content: "let z = 1;" - }; - const file4: ts.projectSystem.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"] }) - }; + it("open file become a part of configured project if it is referenced from root file", () => { + const file1 = { + path: `${ts.tscWatch.projectRoot}/a/b/f1.ts`, + content: "export let x = 5" + }; + const file2 = { + path: `${ts.tscWatch.projectRoot}/a/c/f2.ts`, + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: `${ts.tscWatch.projectRoot}/a/c/f3.ts`, + content: "export let y = 1" + }; + const configFile = { + path: `${ts.tscWatch.projectRoot}/a/c/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + + projectService.openClientFile(file3.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + host.writeFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + }); - const files = [file1, file2, file3, file4]; - const host = ts.projectSystem.createServerHost(files.concat(configFile)); - const projectService = ts.projectSystem.createProjectService(host); + 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 = ts.projectSystem.createServerHost([file1, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + host.writeFile(file2.path, file2.content); + + host.checkTimeoutQueueLengthAndRun(2); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); - 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 ts.Path)!); - ts.projectSystem.checkOpenFiles(projectService, files); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - const configProject1 = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 - ts.projectSystem.checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); - const inferredProject1 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject1, [file2.path]); - const inferredProject2 = projectService.inferredProjects[1]; - ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); - - host.writeFile(configFile.path, "{}"); - host.runQueuedTimeoutCallbacks(); - - verifyScriptInfos(); - ts.projectSystem.checkOpenFiles(projectService, files); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - const inferredProject3 = projectService.inferredProjects[1]; - ts.projectSystem.checkProjectActualFiles(inferredProject3, [file4.path]); - assert.strictEqual(inferredProject3, inferredProject2); - - projectService.closeClientFile(file1.path); - projectService.closeClientFile(file2.path); - projectService.closeClientFile(file4.path); - - verifyScriptInfos(); - ts.projectSystem.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(); - ts.projectSystem.checkOpenFiles(projectService, [file3, file4]); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 - const inferredProject4 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject4, [file4.path]); - - projectService.closeClientFile(file3.path); - verifyScriptInfos(); - ts.projectSystem.checkOpenFiles(projectService, [file4]); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files - const inferredProject5 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject4, [file4.path]); - assert.strictEqual(inferredProject5, inferredProject4); - - const file5: ts.projectSystem.File = { - path: "/file5.ts", - content: "let zz = 1;" - }; - host.writeFile(file5.path, file5.content); - projectService.openClientFile(file5.path); - verifyScriptInfosAreUndefined([file1, file2, file3]); - assert.strictEqual(projectService.getScriptInfoForPath(file4.path as ts.Path), ts.find(infos, info => info.path === file4.path)); - assert.isDefined(projectService.getScriptInfoForPath(file5.path as ts.Path)); - ts.projectSystem.checkOpenFiles(projectService, [file4, file5]); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); - - function verifyScriptInfos() { - infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); - } + 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 = ts.projectSystem.createServerHost([file1, file2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + host.writeFile(configFile.path, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); - function verifyScriptInfosAreUndefined(files: ts.projectSystem.File[]) { - for (const file of files) { - assert.isUndefined(projectService.getScriptInfoForPath(file.path as ts.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 = ts.projectSystem.createServerHost([file1, file2, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); + + host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectRootFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); - function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); - const configProject2 = projectService.configuredProjects.get(configFile.path)!; - assert.strictEqual(configProject2, configProject1); - ts.projectSystem.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", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: ts.projectSystem.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 = ts.projectSystem.createServerHost(files.concat(configFile)); + const projectService = ts.projectSystem.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 ts.Path)!); + ts.projectSystem.checkOpenFiles(projectService, files); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + const configProject1 = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 + ts.projectSystem.checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject1, [file2.path]); + const inferredProject2 = projectService.inferredProjects[1]; + ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); + + host.writeFile(configFile.path, "{}"); + host.runQueuedTimeoutCallbacks(); + + verifyScriptInfos(); + ts.projectSystem.checkOpenFiles(projectService, files); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject3 = projectService.inferredProjects[1]; + ts.projectSystem.checkProjectActualFiles(inferredProject3, [file4.path]); + assert.strictEqual(inferredProject3, inferredProject2); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file2.path); + projectService.closeClientFile(file4.path); + + verifyScriptInfos(); + ts.projectSystem.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(); + ts.projectSystem.checkOpenFiles(projectService, [file3, file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 + const inferredProject4 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject4, [file4.path]); + + projectService.closeClientFile(file3.path); + verifyScriptInfos(); + ts.projectSystem.checkOpenFiles(projectService, [file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files + const inferredProject5 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject4, [file4.path]); + assert.strictEqual(inferredProject5, inferredProject4); + + const file5: ts.projectSystem.File = { + path: "/file5.ts", + content: "let zz = 1;" + }; + host.writeFile(file5.path, file5.content); + projectService.openClientFile(file5.path); + verifyScriptInfosAreUndefined([file1, file2, file3]); + assert.strictEqual(projectService.getScriptInfoForPath(file4.path as ts.Path), ts.find(infos, info => info.path === file4.path)); + assert.isDefined(projectService.getScriptInfoForPath(file5.path as ts.Path)); + ts.projectSystem.checkOpenFiles(projectService, [file4, file5]); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); + + function verifyScriptInfos() { + infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + } + + function verifyScriptInfosAreUndefined(files: ts.projectSystem.File[]) { + for (const file of files) { + assert.isUndefined(projectService.getScriptInfoForPath(file.path as ts.Path)); } - }); - - 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: ts.projectSystem.File = { - path: "/a/b/src/file1.ts", - content: "let x = 1;" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/src/file2.ts", - content: "let y = 1;" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/file3.ts", - content: "let z = 1;" - }; - const file4: ts.projectSystem.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 = ts.projectSystem.createServerHost(hostFiles); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - const configuredProject = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 - ts.projectSystem.checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); - const inferredProject1 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject1, [file2.path]); - - projectService.closeClientFile(file1.path); - projectService.closeClientFile(file3.path); - assert.isFalse(configuredProject.hasOpenRef()); // No files - - host.writeFile(configFile.path, "{}"); - // 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); - - ts.projectSystem.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]; - ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); - - host.runQueuedTimeoutCallbacks(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); - assert.isTrue(configuredProject.hasOpenRef()); // file2 - ts.projectSystem.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); - ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); - }); + } + + function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); + const configProject2 = projectService.configuredProjects.get(configFile.path)!; + assert.strictEqual(configProject2, configProject1); + ts.projectSystem.checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); + assert.equal(configProject2.hasOpenRef(), hasOpenRef); + } + }); - 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 = ts.projectSystem.createServerHost([f1, f2, f3, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - const projectService = ts.projectSystem.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(ts.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}'`); - } + 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: ts.projectSystem.File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: ts.projectSystem.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 = ts.projectSystem.createServerHost(hostFiles); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const configuredProject = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 + ts.projectSystem.checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject1, [file2.path]); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file3.path); + assert.isFalse(configuredProject.hasOpenRef()); // No files + + host.writeFile(configFile.path, "{}"); + // 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); + + ts.projectSystem.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]; + ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); + + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // file2 + ts.projectSystem.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); + ts.projectSystem.checkProjectActualFiles(inferredProject2, [file4.path]); + }); - const f4 = { - path: "/aa.js", - content: "var x = 1" - }; - host.writeFile(f4.path, f4.content); - 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(ts.server.toNormalizedPath(f.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 = ts.projectSystem.createServerHost([f1, f2, f3, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const projectService = ts.projectSystem.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(ts.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.writeFile(f4.path, f4.content); + 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(ts.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 = ts.projectSystem.createServerHost([f1, f2, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLanguageServiceStateEvent); - session.executeCommand({ - seq: 0, - type: "request", - command: "open", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OpenRequest); - - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = ts.projectSystem.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 ts.server.NormalizedPath); - const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); - assert.deepEqual(edits, [{ span: ts.createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); - }); + 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 = ts.projectSystem.createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OpenRequest); + + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = ts.projectSystem.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 ts.server.NormalizedPath); + const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); + assert.deepEqual(edits, [{ span: ts.createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); + }); - it("when multiple projects are open, detects correct default project", () => { - const barConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/bar/tsconfig.json`, - content: JSON.stringify({ - include: ["index.ts"], - compilerOptions: { - lib: ["dom", "es2017"] - } - }) - }; - const barIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/bar/index.ts`, - content: ` + it("when multiple projects are open, detects correct default project", () => { + const barConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/bar/tsconfig.json`, + content: JSON.stringify({ + include: ["index.ts"], + compilerOptions: { + lib: ["dom", "es2017"] + } + }) + }; + const barIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/bar/index.ts`, + content: ` export function bar() { console.log("hello world"); }` - }; - const fooConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foo/tsconfig.json`, - content: JSON.stringify({ - include: ["index.ts"], - compilerOptions: { - lib: ["es2017"] - } - }) - }; - const fooIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foo/index.ts`, - content: ` + }; + const fooConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foo/tsconfig.json`, + content: JSON.stringify({ + include: ["index.ts"], + compilerOptions: { + lib: ["es2017"] + } + }) + }; + const fooIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foo/index.ts`, + content: ` import { bar } from "bar"; bar();` - }; - const barSymLink: ts.projectSystem.SymLink = { - path: `${ts.tscWatch.projectRoot}/foo/node_modules/bar`, - symLink: `${ts.tscWatch.projectRoot}/bar` - }; - - const lib2017: ts.projectSystem.File = { - path: `${ts.getDirectoryPath(ts.projectSystem.libFile.path)}/lib.es2017.d.ts`, - content: ts.projectSystem.libFile.content - }; - const libDom: ts.projectSystem.File = { - path: `${ts.getDirectoryPath(ts.projectSystem.libFile.path)}/lib.dom.d.ts`, - content: ` + }; + const barSymLink: ts.projectSystem.SymLink = { + path: `${ts.tscWatch.projectRoot}/foo/node_modules/bar`, + symLink: `${ts.tscWatch.projectRoot}/bar` + }; + + const lib2017: ts.projectSystem.File = { + path: `${ts.getDirectoryPath(ts.projectSystem.libFile.path)}/lib.es2017.d.ts`, + content: ts.projectSystem.libFile.content + }; + const libDom: ts.projectSystem.File = { + path: `${ts.getDirectoryPath(ts.projectSystem.libFile.path)}/lib.dom.d.ts`, + content: ` declare var console: { log(...args: any[]): void; };` - }; - const host = ts.projectSystem.createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([fooIndex, barIndex], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [barIndex, fooIndex] }); - ts.projectSystem.baselineTsserverLogs("configuredProjects", "when multiple projects are open detects correct default project", session); - }); + }; + const host = ts.projectSystem.createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([fooIndex, barIndex], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [barIndex, fooIndex] }); + ts.projectSystem.baselineTsserverLogs("configuredProjects", "when multiple projects are open detects correct default project", session); + }); - it("when file name starts with ^", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file.ts`, - content: "const x = 10;" - }; - const app: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/^app.ts`, - content: "const y = 10;" - }; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = ts.projectSystem.createServerHost([file, app, tsconfig, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(file.path); - }); + it("when file name starts with ^", () => { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file.ts`, + content: "const x = 10;" + }; + const app: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/^app.ts`, + content: "const y = 10;" + }; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = ts.projectSystem.createServerHost([file, app, tsconfig, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(file.path); + }); - describe("when creating new file", () => { - const foo: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/foo.ts`, - content: "export function foo() { }" - }; - const bar: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/bar.ts`, - content: "export function bar() { }" - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - include: ["./src"] - }) - }; - const fooBar: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/sub/fooBar.ts`, - content: "export function fooBar() { }" - }; - function verifySessionWorker({ withExclude, openFileBeforeCreating }: VerifySession, errorOnNewFileBeforeOldFile: boolean) { - const host = ts.projectSystem.createServerHost([ - foo, bar, - ts.projectSystem.libFile, - { path: `${ts.tscWatch.projectRoot}/src/sub` }, - withExclude ? - { - path: config.path, - content: JSON.stringify({ - include: ["./src"], - exclude: ["./src/sub"] - }) - } : - config - ]); - const session = ts.projectSystem.createSession(host, { - canUseEvents: true, - logger: ts.projectSystem.createLoggerWithInMemoryLogs(), - }); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Open, - arguments: { - file: foo.path, - fileContent: foo.content, - projectRootPath: ts.tscWatch.projectRoot - } - }); - if (!openFileBeforeCreating) { - host.writeFile(fooBar.path, fooBar.content); - } - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Open, - arguments: { - file: fooBar.path, - fileContent: fooBar.content, - projectRootPath: ts.tscWatch.projectRoot - } - }); - if (openFileBeforeCreating) { - host.writeFile(fooBar.path, fooBar.content); + describe("when creating new file", () => { + const foo: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/foo.ts`, + content: "export function foo() { }" + }; + const bar: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/bar.ts`, + content: "export function bar() { }" + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + include: ["./src"] + }) + }; + const fooBar: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/sub/fooBar.ts`, + content: "export function fooBar() { }" + }; + function verifySessionWorker({ withExclude, openFileBeforeCreating }: VerifySession, errorOnNewFileBeforeOldFile: boolean) { + const host = ts.projectSystem.createServerHost([ + foo, bar, + ts.projectSystem.libFile, + { path: `${ts.tscWatch.projectRoot}/src/sub` }, + withExclude ? + { + path: config.path, + content: JSON.stringify({ + include: ["./src"], + exclude: ["./src/sub"] + }) + } : + config + ]); + const session = ts.projectSystem.createSession(host, { + canUseEvents: true, + logger: ts.projectSystem.createLoggerWithInMemoryLogs(), + }); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Open, + arguments: { + file: foo.path, + fileContent: foo.content, + projectRootPath: ts.tscWatch.projectRoot } - ts.projectSystem.verifyGetErrRequest({ - session, - host, - files: errorOnNewFileBeforeOldFile ? - [fooBar, foo] : - [foo, fooBar], - existingTimeouts: withExclude ? 0 : 2 - }); - ts.projectSystem.baselineTsserverLogs("configuredProjects", `creating new file and then open it ${openFileBeforeCreating ? "before" : "after"} watcher is invoked, ask errors on it ${errorOnNewFileBeforeOldFile ? "before" : "after"} old one${withExclude ? " without file being in config" : ""}`, session); + }); + if (!openFileBeforeCreating) { + host.writeFile(fooBar.path, fooBar.content); } - interface VerifySession { - withExclude?: boolean; - openFileBeforeCreating: boolean; + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Open, + arguments: { + file: fooBar.path, + fileContent: fooBar.content, + projectRootPath: ts.tscWatch.projectRoot + } + }); + if (openFileBeforeCreating) { + host.writeFile(fooBar.path, fooBar.content); } - function verifySession(input: VerifySession) { - it("when error on new file are asked before old one", () => { - verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true); - }); + ts.projectSystem.verifyGetErrRequest({ + session, + host, + files: errorOnNewFileBeforeOldFile ? + [fooBar, foo] : + [foo, fooBar], + existingTimeouts: withExclude ? 0 : 2 + }); + ts.projectSystem.baselineTsserverLogs("configuredProjects", `creating new file and then open it ${openFileBeforeCreating ? "before" : "after"} watcher is invoked, ask errors on it ${errorOnNewFileBeforeOldFile ? "before" : "after"} old one${withExclude ? " without file being in config" : ""}`, session); + } + interface VerifySession { + withExclude?: boolean; + openFileBeforeCreating: boolean; + } + 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); - }); - } - describe("when new file creation directory watcher is invoked before file is opened in editor", () => { + it("when error on new file are asked after old one", () => { + verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false); + }); + } + describe("when new file creation directory watcher is invoked before file is opened in editor", () => { + verifySession({ + openFileBeforeCreating: false, + }); + describe("when new file is excluded from config", () => { verifySession({ + withExclude: true, openFileBeforeCreating: false, }); - describe("when new file is excluded from config", () => { - verifySession({ - withExclude: true, - openFileBeforeCreating: false, - }); - }); }); + }); - 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, + }); + describe("when new file is excluded from config", () => { verifySession({ + withExclude: true, openFileBeforeCreating: true, }); - describe("when new file is excluded from config", () => { - verifySession({ - withExclude: true, - openFileBeforeCreating: true, - }); - }); }); }); + }); - it("when default configured project does not contain the file", () => { - const barConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/bar/tsconfig.json`, + it("when default configured project does not contain the file", () => { + const barConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/bar/tsconfig.json`, + content: "{}" + }; + const barIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/bar/index.ts`, + content: `import {foo} from "../foo/lib"; +foo();` + }; + const fooBarConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foobar/tsconfig.json`, + content: barConfig.path + }; + const fooBarIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foobar/index.ts`, + content: barIndex.content + }; + const fooConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foo/tsconfig.json`, + content: JSON.stringify({ + include: ["index.ts"], + compilerOptions: { + declaration: true, + outDir: "lib" + } + }) + }; + const fooIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/foo/index.ts`, + content: `export function foo() {}` + }; + const host = ts.projectSystem.createServerHost([barConfig, barIndex, fooBarConfig, fooBarIndex, fooConfig, fooIndex, ts.projectSystem.libFile]); + ts.tscWatch.ensureErrorFreeBuild(host, [fooConfig.path]); + const fooDts = `${ts.tscWatch.projectRoot}/foo/lib/index.d.ts`; + assert.isTrue(host.fileExists(fooDts)); + const session = ts.projectSystem.createSession(host); + const service = session.getProjectService(); + service.openClientFile(barIndex.path); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(barConfig.path)!, [barIndex.path, fooDts, ts.projectSystem.libFile.path, barConfig.path]); + service.openClientFile(fooBarIndex.path); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(fooBarConfig.path)!, [fooBarIndex.path, fooDts, ts.projectSystem.libFile.path, fooBarConfig.path]); + service.openClientFile(fooIndex.path); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(fooConfig.path)!, [fooIndex.path, ts.projectSystem.libFile.path, fooConfig.path]); + service.openClientFile(fooDts); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.GetApplicableRefactors, + arguments: { + file: fooDts, + startLine: 1, + startOffset: 1, + endLine: 1, + endOffset: 1 + } + }); + assert.equal(service.tryGetDefaultProjectForFile(ts.server.toNormalizedPath(fooDts)), service.configuredProjects.get(barConfig.path)); + }); + + describe("watches extended config files", () => { + function getService(additionalFiles?: ts.projectSystem.File[]) { + const alphaExtendedConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/extended/alpha.tsconfig.json`, content: "{}" }; - const barIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/bar/index.ts`, - content: `import {foo} from "../foo/lib"; -foo();` + const bravoExtendedConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/extended/bravo.tsconfig.json`, + content: JSON.stringify({ + extends: "./alpha.tsconfig.json" + }) }; - const fooBarConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foobar/tsconfig.json`, - content: barConfig.path + const aConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a/tsconfig.json`, + content: JSON.stringify({ + extends: "../extended/alpha.tsconfig.json", + files: ["a.ts"] + }) }; - const fooBarIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foobar/index.ts`, - content: barIndex.content + const aFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a/a.ts`, + content: `let a = 1;` }; - const fooConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foo/tsconfig.json`, + const bConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/tsconfig.json`, content: JSON.stringify({ - include: ["index.ts"], - compilerOptions: { - declaration: true, - outDir: "lib" - } + extends: "../extended/bravo.tsconfig.json", + files: ["b.ts"] }) }; - const fooIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/foo/index.ts`, - content: `export function foo() {}` + const bFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/b.ts`, + content: `let b = 1;` }; - const host = ts.projectSystem.createServerHost([barConfig, barIndex, fooBarConfig, fooBarIndex, fooConfig, fooIndex, ts.projectSystem.libFile]); - ts.tscWatch.ensureErrorFreeBuild(host, [fooConfig.path]); - const fooDts = `${ts.tscWatch.projectRoot}/foo/lib/index.d.ts`; - assert.isTrue(host.fileExists(fooDts)); - const session = ts.projectSystem.createSession(host); - const service = session.getProjectService(); - service.openClientFile(barIndex.path); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(barConfig.path)!, [barIndex.path, fooDts, ts.projectSystem.libFile.path, barConfig.path]); - service.openClientFile(fooBarIndex.path); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(fooBarConfig.path)!, [fooBarIndex.path, fooDts, ts.projectSystem.libFile.path, fooBarConfig.path]); - service.openClientFile(fooIndex.path); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(fooConfig.path)!, [fooIndex.path, ts.projectSystem.libFile.path, fooConfig.path]); - service.openClientFile(fooDts); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.GetApplicableRefactors, - arguments: { - file: fooDts, - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 1 + + const host = ts.projectSystem.createServerHost([alphaExtendedConfig, aConfig, aFile, bravoExtendedConfig, bConfig, bFile, ...(additionalFiles || ts.emptyArray)]); + const projectService = ts.projectSystem.createProjectService(host); + return { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig }; + } + + it("should watch the extended configs of multiple projects", () => { + const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService(); + + projectService.openClientFile(aFile.path); + projectService.openClientFile(bFile.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); + const aProject = projectService.configuredProjects.get(aConfig.path)!; + const bProject = projectService.configuredProjects.get(bConfig.path)!; + ts.projectSystem.checkProjectActualFiles(aProject, [aFile.path, aConfig.path, alphaExtendedConfig.path]); + ts.projectSystem.checkProjectActualFiles(bProject, [bFile.path, bConfig.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); + assert.isUndefined(aProject.getCompilerOptions().strict); + assert.isUndefined(bProject.getCompilerOptions().strict); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); + + host.writeFile(alphaExtendedConfig.path, JSON.stringify({ + compilerOptions: { + strict: true } - }); - assert.equal(service.tryGetDefaultProjectForFile(ts.server.toNormalizedPath(fooDts)), service.configuredProjects.get(barConfig.path)); + })); + assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); + assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); + host.checkTimeoutQueueLengthAndRun(3); + assert.isTrue(aProject.getCompilerOptions().strict); + assert.isTrue(bProject.getCompilerOptions().strict); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); + + host.writeFile(bravoExtendedConfig.path, JSON.stringify({ + extends: "./alpha.tsconfig.json", + compilerOptions: { + strict: false + } + })); + assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); + assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); + host.checkTimeoutQueueLengthAndRun(2); + assert.isTrue(aProject.getCompilerOptions().strict); + assert.isFalse(bProject.getCompilerOptions().strict); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); + + host.writeFile(bConfig.path, JSON.stringify({ + extends: "../extended/alpha.tsconfig.json", + })); + assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); + assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); + host.checkTimeoutQueueLengthAndRun(2); + assert.isTrue(aProject.getCompilerOptions().strict); + assert.isTrue(bProject.getCompilerOptions().strict); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path]); + + host.writeFile(alphaExtendedConfig.path, "{}"); + assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); + assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); + host.checkTimeoutQueueLengthAndRun(3); + assert.isUndefined(aProject.getCompilerOptions().strict); + assert.isUndefined(bProject.getCompilerOptions().strict); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path]); }); - describe("watches extended config files", () => { - function getService(additionalFiles?: ts.projectSystem.File[]) { - const alphaExtendedConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/extended/alpha.tsconfig.json`, - content: "{}" - }; - const bravoExtendedConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/extended/bravo.tsconfig.json`, - content: JSON.stringify({ - extends: "./alpha.tsconfig.json" - }) - }; - const aConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a/tsconfig.json`, - content: JSON.stringify({ - extends: "../extended/alpha.tsconfig.json", - files: ["a.ts"] - }) - }; - const aFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a/a.ts`, - content: `let a = 1;` - }; - const bConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/tsconfig.json`, - content: JSON.stringify({ - extends: "../extended/bravo.tsconfig.json", - files: ["b.ts"] - }) - }; - const bFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/b.ts`, - content: `let b = 1;` - }; - - const host = ts.projectSystem.createServerHost([alphaExtendedConfig, aConfig, aFile, bravoExtendedConfig, bConfig, bFile, ...(additionalFiles || ts.emptyArray)]); - const projectService = ts.projectSystem.createProjectService(host); - return { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig }; - } + it("should stop watching the extended configs of closed projects", () => { + const dummy: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/dummy/dummy.ts`, + content: `let dummy = 1;` + }; + const dummyConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/dummy/tsconfig.json`, + content: "{}" + }; + const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService([dummy, dummyConfig]); - it("should watch the extended configs of multiple projects", () => { - const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService(); - - projectService.openClientFile(aFile.path); - projectService.openClientFile(bFile.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); - const aProject = projectService.configuredProjects.get(aConfig.path)!; - const bProject = projectService.configuredProjects.get(bConfig.path)!; - ts.projectSystem.checkProjectActualFiles(aProject, [aFile.path, aConfig.path, alphaExtendedConfig.path]); - ts.projectSystem.checkProjectActualFiles(bProject, [bFile.path, bConfig.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); - assert.isUndefined(aProject.getCompilerOptions().strict); - assert.isUndefined(bProject.getCompilerOptions().strict); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); - - host.writeFile(alphaExtendedConfig.path, JSON.stringify({ - compilerOptions: { - strict: true - } - })); - assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); - assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); - host.checkTimeoutQueueLengthAndRun(3); - assert.isTrue(aProject.getCompilerOptions().strict); - assert.isTrue(bProject.getCompilerOptions().strict); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); - - host.writeFile(bravoExtendedConfig.path, JSON.stringify({ - extends: "./alpha.tsconfig.json", - compilerOptions: { - strict: false - } - })); - assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); - assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); - host.checkTimeoutQueueLengthAndRun(2); - assert.isTrue(aProject.getCompilerOptions().strict); - assert.isFalse(bProject.getCompilerOptions().strict); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); - - host.writeFile(bConfig.path, JSON.stringify({ - extends: "../extended/alpha.tsconfig.json", - })); - assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); - assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); - host.checkTimeoutQueueLengthAndRun(2); - assert.isTrue(aProject.getCompilerOptions().strict); - assert.isTrue(bProject.getCompilerOptions().strict); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path]); - - host.writeFile(alphaExtendedConfig.path, "{}"); - assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); - assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); - host.checkTimeoutQueueLengthAndRun(3); - assert.isUndefined(aProject.getCompilerOptions().strict); - assert.isUndefined(bProject.getCompilerOptions().strict); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path]); - }); + projectService.openClientFile(aFile.path); + projectService.openClientFile(bFile.path); + projectService.openClientFile(dummy.path); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 3); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path, dummyConfig.path]); - it("should stop watching the extended configs of closed projects", () => { - const dummy: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/dummy/dummy.ts`, - content: `let dummy = 1;` - }; - const dummyConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/dummy/tsconfig.json`, - content: "{}" - }; - const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService([dummy, dummyConfig]); - - projectService.openClientFile(aFile.path); - projectService.openClientFile(bFile.path); - projectService.openClientFile(dummy.path); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 3); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, bConfig.path, ts.projectSystem.libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path, dummyConfig.path]); - - projectService.closeClientFile(bFile.path); - projectService.closeClientFile(dummy.path); - projectService.openClientFile(dummy.path); - - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); - ts.projectSystem.checkWatchedFiles(host, [aConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path, dummyConfig.path]); - - projectService.closeClientFile(aFile.path); - projectService.closeClientFile(dummy.path); - projectService.openClientFile(dummy.path); - - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, dummyConfig.path]); - }); + projectService.closeClientFile(bFile.path); + projectService.closeClientFile(dummy.path); + projectService.openClientFile(dummy.path); + + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 2); + ts.projectSystem.checkWatchedFiles(host, [aConfig.path, ts.projectSystem.libFile.path, alphaExtendedConfig.path, dummyConfig.path]); + + projectService.closeClientFile(aFile.path); + projectService.closeClientFile(dummy.path); + projectService.openClientFile(dummy.path); + + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, dummyConfig.path]); }); }); +}); - 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 = ts.projectSystem.createServerHost([file1, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - host.runQueuedTimeoutCallbacks(); - - // Since file1 refers to config file as the default project, it needs to be kept alive - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - const inferredProject = projectService.inferredProjects[0]; - assert.isTrue(inferredProject.containsFile(file1.path as ts.server.NormalizedPath)); - assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(file1.path as ts.server.NormalizedPath)); - }); - - 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 = ts.projectSystem.createServerHost([f, config, t1, t2], { currentDirectory: ts.getDirectoryPath(f.path) }); - const projectService = ts.projectSystem.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 }); - }); + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = ts.projectSystem.createServerHost([file1, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + host.runQueuedTimeoutCallbacks(); + + // Since file1 refers to config file as the default project, it needs to be kept alive + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.containsFile(file1.path as ts.server.NormalizedPath)); + assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(file1.path as ts.server.NormalizedPath)); + }); - it("should tolerate invalid include files that start in subDirectory", () => { - const f = { - path: `${ts.tscWatch.projectRoot}/src/server/index.ts`, - content: "let x = 1" - }; - const config = { - path: `${ts.tscWatch.projectRoot}/src/server/tsconfig.json`, - content: JSON.stringify({ - compiler: { - module: "commonjs", - outDir: "../../build" - }, - include: [ - "../src/**/*.ts" - ] - }) - }; - const host = ts.projectSystem.createServerHost([f, config, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); - const projectService = ts.projectSystem.createProjectService(host); + 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 = ts.projectSystem.createServerHost([f, config, t1, t2], { currentDirectory: ts.getDirectoryPath(f.path) }); + const projectService = ts.projectSystem.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 }); + }); - 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: `${ts.tscWatch.projectRoot}/src/server/index.ts`, + content: "let x = 1" + }; + const config = { + path: `${ts.tscWatch.projectRoot}/src/server/tsconfig.json`, + content: JSON.stringify({ + compiler: { + module: "commonjs", + outDir: "../../build" + }, + include: [ + "../src/**/*.ts" + ] + }) + }; + const host = ts.projectSystem.createServerHost([f, config, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); + const projectService = ts.projectSystem.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: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: 'import classc from "file2"' - }; - const file2a: ts.projectSystem.File = { - path: "/a/file2.ts", - content: "export classc { method2a() { return 10; } }" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/file2.ts", - content: "export classc { method2() { return 10; } }" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) - }; - const files = [file1, file2a, configFile, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - ts.projectSystem.checkProjectActualFiles(project, ts.map(files, file => file.path)); - ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(files, file => file === file1 ? undefined : file.path)); - ts.projectSystem.checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); - ts.projectSystem.checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); - - files.push(file2); - host.writeFile(file2.path, file2.content); - host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions - host.runQueuedTimeoutCallbacks(); // Actual update - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - ts.projectSystem.checkProjectActualFiles(project, ts.mapDefined(files, file => file === file2a ? undefined : file.path)); - ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(files, file => file === file1 ? undefined : file.path)); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); - ts.projectSystem.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); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - ts.projectSystem.checkProjectActualFiles(project, ts.mapDefined(files, file => file === file2a ? undefined : file.path)); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); - ts.projectSystem.checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); - }); + it("Changed module resolution reflected when specifying files list", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: 'import classc from "file2"' + }; + const file2a: ts.projectSystem.File = { + path: "/a/file2.ts", + content: "export classc { method2a() { return 10; } }" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/file2.ts", + content: "export classc { method2() { return 10; } }" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) + }; + const files = [file1, file2a, configFile, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + ts.projectSystem.checkProjectActualFiles(project, ts.map(files, file => file.path)); + ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(files, file => file === file1 ? undefined : file.path)); + ts.projectSystem.checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); + ts.projectSystem.checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + + files.push(file2); + host.writeFile(file2.path, file2.content); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + ts.projectSystem.checkProjectActualFiles(project, ts.mapDefined(files, file => file === file2a ? undefined : file.path)); + ts.projectSystem.checkWatchedFiles(host, ts.mapDefined(files, file => file === file1 ? undefined : file.path)); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); + ts.projectSystem.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); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + ts.projectSystem.checkProjectActualFiles(project, ts.mapDefined(files, file => file === file2a ? undefined : file.path)); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); + ts.projectSystem.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: ts.projectSystem.File = { - path: "/a/b/src/file1.ts", - content: 'import { classc } from "module1"' - }; - const module1: ts.projectSystem.File = { - path: "/a/b/node_modules/module1/index.d.ts", - content: `import { class2 } from "module2"; + it("Failed lookup locations uses parent most node_modules directory", () => { + const root = "/user/username/rootfolder"; + const file1: ts.projectSystem.File = { + path: "/a/b/src/file1.ts", + content: 'import { classc } from "module1"' + }; + const module1: ts.projectSystem.File = { + path: "/a/b/node_modules/module1/index.d.ts", + content: `import { class2 } from "module2"; export classc { method2a(): class2; }` - }; - const module2: ts.projectSystem.File = { - path: "/a/b/node_modules/module2/index.d.ts", - content: "export class2 { method2() { return 10; } }" - }; - const module3: ts.projectSystem.File = { - path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", - content: "export class3 { method2() { return 10; } }" - }; - const configFile: ts.projectSystem.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(ts.projectSystem.libFile); - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, module1.path, module2.path, configFile.path]); - ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - const watchedRecursiveDirectories = ts.projectSystem.getTypeRootsFromLocation(root + "/a/b/src"); - watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); - ts.projectSystem.checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); - }); + }; + const module2: ts.projectSystem.File = { + path: "/a/b/node_modules/module2/index.d.ts", + content: "export class2 { method2() { return 10; } }" + }; + const module3: ts.projectSystem.File = { + path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", + content: "export class3 { method2() { return 10; } }" + }; + const configFile: ts.projectSystem.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(ts.projectSystem.libFile); + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + ts.projectSystem.checkProjectActualFiles(project, [file1.path, ts.projectSystem.libFile.path, module1.path, module2.path, configFile.path]); + ts.projectSystem.checkWatchedFiles(host, [ts.projectSystem.libFile.path, configFile.path]); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + const watchedRecursiveDirectories = ts.projectSystem.getTypeRootsFromLocation(root + "/a/b/src"); + watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); + ts.projectSystem.checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); }); - - describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file fails", () => { - it("should be tolerated without crashing the server", () => { - const configFile = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "" - }; - const file1 = { - path: `${ts.tscWatch.projectRoot}/file1.ts`, - content: "let t = 10;" - }; - - const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, configFile]); - const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ConfigFileDiagEvent); - const originalReadFile = host.readFile; - host.readFile = f => { - return f === configFile.path ? - undefined : - originalReadFile.call(host, f); - }; - ts.projectSystem.openFilesForSession([file1], session); - - assert.deepEqual(events, [{ - eventName: ts.server.ConfigFileDiagEvent, - data: { - triggerFile: file1.path, - configFileName: configFile.path, - diagnostics: [ - ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0, configFile.path) - ] - } - }]); - }); +}); + +describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file fails", () => { + it("should be tolerated without crashing the server", () => { + const configFile = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "" + }; + const file1 = { + path: `${ts.tscWatch.projectRoot}/file1.ts`, + content: "let t = 10;" + }; + + const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, configFile]); + const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ConfigFileDiagEvent); + const originalReadFile = host.readFile; + host.readFile = f => { + return f === configFile.path ? + undefined : + originalReadFile.call(host, f); + }; + ts.projectSystem.openFilesForSession([file1], session); + + assert.deepEqual(events, [{ + eventName: ts.server.ConfigFileDiagEvent, + data: { + triggerFile: file1.path, + configFileName: configFile.path, + diagnostics: [ + ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0, configFile.path) + ] + } + }]); }); +}); } diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts index 773c561e7c124..673b70ce35daf 100644 --- a/src/testRunner/unittests/tsserver/declarationFileMaps.ts +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -1,755 +1,755 @@ namespace ts.projectSystem { - function documentSpanFromSubstring({ file, text, contextText, options, contextOptions }: ts.projectSystem.DocumentSpanFromSubstring): ts.DocumentSpan { - const contextSpan = contextText !== undefined ? documentSpanFromSubstring({ file, text: contextText, options: contextOptions }) : undefined; - return { - fileName: file.path, - textSpan: ts.projectSystem.textSpanFromSubstring(file.content, text, options), - ...contextSpan && { contextSpan: contextSpan.textSpan } - }; - } - - function renameLocation(input: ts.projectSystem.DocumentSpanFromSubstring): ts.RenameLocation { - return documentSpanFromSubstring(input); - } +function documentSpanFromSubstring({ file, text, contextText, options, contextOptions }: ts.projectSystem.DocumentSpanFromSubstring): ts.DocumentSpan { + const contextSpan = contextText !== undefined ? documentSpanFromSubstring({ file, text: contextText, options: contextOptions }) : undefined; + return { + fileName: file.path, + textSpan: ts.projectSystem.textSpanFromSubstring(file.content, text, options), + ...contextSpan && { contextSpan: contextSpan.textSpan } + }; +} - interface MakeReferenceEntry extends ts.projectSystem.DocumentSpanFromSubstring { - isDefinition?: boolean; - isWriteAccess?: boolean; - } - function makeReferencedSymbolEntry({ isDefinition, isWriteAccess, ...rest }: MakeReferenceEntry): ts.ReferencedSymbolEntry { - const result = { - ...documentSpanFromSubstring(rest), - isDefinition, - isWriteAccess: !!isWriteAccess, - isInString: undefined, - }; - if (isDefinition === undefined) { - delete result.isDefinition; - } - return result; - } +function renameLocation(input: ts.projectSystem.DocumentSpanFromSubstring): ts.RenameLocation { + return documentSpanFromSubstring(input); +} - function checkDeclarationFiles(file: ts.projectSystem.File, session: ts.projectSystem.TestSession, expectedFiles: readonly ts.projectSystem.File[]): void { - ts.projectSystem.openFilesForSession([file], session); - const project = ts.Debug.checkDefined(session.getProjectService().getDefaultProjectForFile(file.path as ts.server.NormalizedPath, /*ensureProject*/ false)); - const program = project.getCurrentProgram()!; - const output = ts.getFileEmitOutput(program, ts.Debug.checkDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); - ts.projectSystem.closeFilesForSession([file], session); - ts.Debug.assert(!output.emitSkipped); - assert.deepEqual(output.outputFiles, expectedFiles.map((e): ts.OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); +interface MakeReferenceEntry extends ts.projectSystem.DocumentSpanFromSubstring { + isDefinition?: boolean; + isWriteAccess?: boolean; +} +function makeReferencedSymbolEntry({ isDefinition, isWriteAccess, ...rest }: MakeReferenceEntry): ts.ReferencedSymbolEntry { + const result = { + ...documentSpanFromSubstring(rest), + isDefinition, + isWriteAccess: !!isWriteAccess, + isInString: undefined, + }; + if (isDefinition === undefined) { + delete result.isDefinition; } + return result; +} - describe("unittests:: tsserver:: with declaration file maps:: project references", () => { - const aTs: ts.projectSystem.File = { - path: "/a/a.ts", - content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", - }; - const compilerOptions: ts.CompilerOptions = { - outDir: "bin", - declaration: true, - declarationMap: true, - composite: true, - }; - const configContent = JSON.stringify({ compilerOptions }); - const aTsconfig: ts.projectSystem.File = { path: "/a/tsconfig.json", content: configContent }; - const aDtsMapContent: ts.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: ts.projectSystem.File = { - path: "/a/bin/a.d.ts.map", - content: JSON.stringify(aDtsMapContent), - }; - const aDts: ts.projectSystem.File = { - path: "/a/bin/a.d.ts", - // ${""} is needed 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: ts.projectSystem.File = { - path: "/b/b.ts", - content: "export function fnB() {}", - }; - const bTsconfig: ts.projectSystem.File = { path: "/b/tsconfig.json", content: configContent }; - const bDtsMapContent: ts.RawSourceMap = { - version: 3, - file: "b.d.ts", - sourceRoot: "", - sources: ["../b.ts"], - names: [], - mappings: "AAAA,wBAAgB,GAAG,SAAK", - }; - const bDtsMap: ts.projectSystem.File = { - path: "/b/bin/b.d.ts.map", - content: JSON.stringify(bDtsMapContent), - }; - const bDts: ts.projectSystem.File = { - // ${""} is need to mangle the sourceMappingURL part so it doesn't break the build - path: "/b/bin/b.d.ts", - content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, - }; - - const dummyFile: ts.projectSystem.File = { - path: "/dummy/dummy.ts", - content: "let a = 10;" - }; - - const userTs: ts.projectSystem.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: ts.projectSystem.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: ts.projectSystem.File = { - path: "/user/tsconfig.json", - content: JSON.stringify({ - file: ["user.ts"], - references: [{ path: "../a" }, { path: "../b" }] - }) - }; - - function makeSampleProjects(addUserTsConfig?: boolean, keepAllFiles?: boolean) { - const host = ts.projectSystem.createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); - const session = ts.projectSystem.createSession(host); +function checkDeclarationFiles(file: ts.projectSystem.File, session: ts.projectSystem.TestSession, expectedFiles: readonly ts.projectSystem.File[]): void { + ts.projectSystem.openFilesForSession([file], session); + const project = ts.Debug.checkDefined(session.getProjectService().getDefaultProjectForFile(file.path as ts.server.NormalizedPath, /*ensureProject*/ false)); + const program = project.getCurrentProgram()!; + const output = ts.getFileEmitOutput(program, ts.Debug.checkDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + ts.projectSystem.closeFilesForSession([file], session); + ts.Debug.assert(!output.emitSkipped); + assert.deepEqual(output.outputFiles, expectedFiles.map((e): ts.OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); +} - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); +describe("unittests:: tsserver:: with declaration file maps:: project references", () => { + const aTs: ts.projectSystem.File = { + path: "/a/a.ts", + content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", + }; + const compilerOptions: ts.CompilerOptions = { + outDir: "bin", + declaration: true, + declarationMap: true, + composite: true, + }; + const configContent = JSON.stringify({ compilerOptions }); + const aTsconfig: ts.projectSystem.File = { path: "/a/tsconfig.json", content: configContent }; + const aDtsMapContent: ts.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: ts.projectSystem.File = { + path: "/a/bin/a.d.ts.map", + content: JSON.stringify(aDtsMapContent), + }; + const aDts: ts.projectSystem.File = { + path: "/a/bin/a.d.ts", + // ${""} is needed 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: ts.projectSystem.File = { + path: "/b/b.ts", + content: "export function fnB() {}", + }; + const bTsconfig: ts.projectSystem.File = { path: "/b/tsconfig.json", content: configContent }; + const bDtsMapContent: ts.RawSourceMap = { + version: 3, + file: "b.d.ts", + sourceRoot: "", + sources: ["../b.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK", + }; + const bDtsMap: ts.projectSystem.File = { + path: "/b/bin/b.d.ts.map", + content: JSON.stringify(bDtsMapContent), + }; + const bDts: ts.projectSystem.File = { + // ${""} is need to mangle the sourceMappingURL part so it doesn't break the build + path: "/b/bin/b.d.ts", + content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, + }; + + const dummyFile: ts.projectSystem.File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + + const userTs: ts.projectSystem.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: ts.projectSystem.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: ts.projectSystem.File = { + path: "/user/tsconfig.json", + content: JSON.stringify({ + file: ["user.ts"], + references: [{ path: "../a" }, { path: "../b" }] + }) + }; + + function makeSampleProjects(addUserTsConfig?: boolean, keepAllFiles?: boolean) { + const host = ts.projectSystem.createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); + const session = ts.projectSystem.createSession(host); + + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); + + // Testing what happens if we delete the original sources. + if (!keepAllFiles) { + host.deleteFile(bTs.path); + } - // Testing what happens if we delete the original sources. - if (!keepAllFiles) { - host.deleteFile(bTs.path); - } + ts.projectSystem.openFilesForSession([userTs], session); + const service = session.getProjectService(); + // If config file then userConfig project and bConfig project since it is referenced + ts.projectSystem.checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 2 } : { inferredProjects: 1 }); + return session; + } - ts.projectSystem.openFilesForSession([userTs], session); - const service = session.getProjectService(); - // If config file then userConfig project and bConfig project since it is referenced - ts.projectSystem.checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 2 } : { inferredProjects: 1 }); - return session; - } + function verifyInferredProjectUnchanged(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); + } - function verifyInferredProjectUnchanged(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); - } + function verifyDummyProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); + } - function verifyDummyProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); - } + function verifyOnlyOrphanInferredProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.openFilesForSession([dummyFile], session); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyDummyProject(session); + } - function verifyOnlyOrphanInferredProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.openFilesForSession([dummyFile], session); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyDummyProject(session); - } + function verifySingleInferredProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyInferredProjectUnchanged(session); - function verifySingleInferredProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyInferredProjectUnchanged(session); + // Close user file should close all the projects after opening dummy file + ts.projectSystem.closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } - // Close user file should close all the projects after opening dummy file - ts.projectSystem.closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } + function verifyATsConfigProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); + } - function verifyATsConfigProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); - } + function verifyATsConfigOriginalProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + // Close user file should close all the projects + ts.projectSystem.closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } - function verifyATsConfigOriginalProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); - // Close user file should close all the projects - ts.projectSystem.closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } + function verifyATsConfigWhenOpened(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); - function verifyATsConfigWhenOpened(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); + ts.projectSystem.closeFilesForSession([userTs], session); + ts.projectSystem.openFilesForSession([dummyFile], session); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyDummyProject(session); + verifyATsConfigProject(session); // ATsConfig should still be alive + } - ts.projectSystem.closeFilesForSession([userTs], session); - ts.projectSystem.openFilesForSession([dummyFile], session); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyDummyProject(session); - verifyATsConfigProject(session); // ATsConfig should still be alive - } + function verifyUserTsConfigProject(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aTs.path, userTsconfig.path]); + } - function verifyUserTsConfigProject(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aTs.path, userTsconfig.path]); - } + it("goToDefinition", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Definition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" + }) + ]); + verifySingleInferredProject(session); + }); - it("goToDefinition", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Definition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ + it("getDefinitionAndBoundSpan", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ ts.projectSystem.protocolFileSpanWithContextFromSubstring({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }) - ]); - verifySingleInferredProject(session); + ], }); + verifySingleInferredProject(session); + }); - it("getDefinitionAndBoundSpan", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }) - ], - }); - verifySingleInferredProject(session); + it("getDefinitionAndBoundSpan with file navigation", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" + }) + ], }); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + verifyUserTsConfigProject(session); - it("getDefinitionAndBoundSpan with file navigation", () => { - const session = makeSampleProjects(/*addUserTsConfig*/ true); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }) - ], - }); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - verifyUserTsConfigProject(session); + // Navigate to the definition + ts.projectSystem.closeFilesForSession([userTs], session); + ts.projectSystem.openFilesForSession([aTs], session); - // Navigate to the definition - ts.projectSystem.closeFilesForSession([userTs], session); - ts.projectSystem.openFilesForSession([aTs], session); + // UserTs configured project should be alive + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + verifyUserTsConfigProject(session); + verifyATsConfigProject(session); - // UserTs configured project should be alive - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - verifyUserTsConfigProject(session); - verifyATsConfigProject(session); + ts.projectSystem.closeFilesForSession([aTs], session); + verifyOnlyOrphanInferredProject(session); + }); - ts.projectSystem.closeFilesForSession([aTs], session); - verifyOnlyOrphanInferredProject(session); - }); + it("goToType", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.TypeDefinition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "instanceA")); + assert.deepEqual(response, [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "IfaceA", + contextText: "export interface IfaceA {}" + }) + ]); + verifySingleInferredProject(session); + }); - it("goToType", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.TypeDefinition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "instanceA")); - assert.deepEqual(response, [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "IfaceA", - contextText: "export interface IfaceA {}" - }) - ]); - verifySingleInferredProject(session); - }); + it("goToImplementation", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Implementation, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" + }) + ]); + verifySingleInferredProject(session); + }); - it("goToImplementation", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Implementation, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }) - ]); - verifySingleInferredProject(session); - }); + it("goToDefinition -- target does not exist", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Definition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); + // bTs does not exist, so stick with bDts + assert.deepEqual(response, [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: bDts, + text: "fnB", + contextText: "export declare function fnB(): void;" + }) + ]); + verifySingleInferredProject(session); + }); - it("goToDefinition -- target does not exist", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Definition, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); - // bTs does not exist, so stick with bDts - assert.deepEqual(response, [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + it("navigateTo", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); + assert.deepEqual(response, [ + // Keep the .d.ts file since the .ts file no longer exists + // (otherwise it would be treated as not in the project) + { + ...ts.projectSystem.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: ts.ScriptElementKind.functionElement, + kindModifiers: "export,declare", + }, + { + ...ts.projectSystem.protocolFileSpanFromSubstring({ + file: userTs, + text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + name: "fnUser", + matchKind: "prefix", + isCaseSensitive: true, + kind: ts.ScriptElementKind.functionElement, + kindModifiers: "export", + }, + ]); + + verifySingleInferredProject(session); + }); + + it("navigateToAll -- when neither file nor project is specified", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true, /*keepAllFiles*/ true); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { file: undefined, searchValue: "fn" }); + assert.deepEqual(response, [ + { + ...ts.projectSystem.protocolFileSpanFromSubstring({ + file: bTs, + text: "export function fnB() {}" + }), + name: "fnB", + matchKind: "prefix", + isCaseSensitive: true, + kind: ts.ScriptElementKind.functionElement, + kindModifiers: "export", + }, + { + ...ts.projectSystem.protocolFileSpanFromSubstring({ + file: aTs, + text: "export function fnA() {}" + }), + name: "fnA", + matchKind: "prefix", + isCaseSensitive: true, + kind: ts.ScriptElementKind.functionElement, + kindModifiers: "export", + }, + { + ...ts.projectSystem.protocolFileSpanFromSubstring({ + file: userTs, + text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + name: "fnUser", + matchKind: "prefix", + isCaseSensitive: true, + kind: ts.ScriptElementKind.functionElement, + kindModifiers: "export", + } + ]); + }); + + it("navigateToAll -- when file is not specified but project is", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true, /*keepAllFiles*/ true); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { projectFileName: bTsconfig.path, file: undefined, searchValue: "fn" }); + assert.deepEqual(response, [ + { + ...ts.projectSystem.protocolFileSpanFromSubstring({ + file: bTs, + text: "export function fnB() {}" + }), + name: "fnB", + matchKind: "prefix", + isCaseSensitive: true, + kind: ts.ScriptElementKind.functionElement, + kindModifiers: "export", + } + ]); + }); + + const referenceATs = (aTs: ts.projectSystem.File, isDefinition: true | undefined): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ + file: aTs, + isDefinition, + isWriteAccess: true, + text: "fnA", + contextText: "export function fnA() {}", + lineText: "export function fnA() {}" + }); + const referencesUserTs = (userTs: ts.projectSystem.File, isDefinition: false | undefined): readonly ts.projectSystem.protocol.ReferencesResponseItem[] => [ + ts.projectSystem.makeReferenceItem({ + file: userTs, + isDefinition, + text: "fnA", + lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + ]; + + it("findAllReferences", () => { + const session = makeSampleProjects(); + + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + refs: [...referencesUserTs(userTs, /*isDefinition*/ undefined), referenceATs(aTs, /*isDefinition*/ true)], + symbolName: "fnA", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(userTs.content, "fnA()").offset, + symbolDisplayString: "function fnA(): void", }); - it("navigateTo", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); - assert.deepEqual(response, [ - // Keep the .d.ts file since the .ts file no longer exists - // (otherwise it would be treated as not in the project) - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ - file: bDts, - text: "export declare function fnB(): void;" - }), - name: "fnB", - matchKind: "prefix", - isCaseSensitive: true, - kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export,declare", - }, - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ - file: userTs, - text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" - }), - name: "fnUser", - matchKind: "prefix", - isCaseSensitive: true, - kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", - }, - ]); + verifyATsConfigOriginalProject(session); + }); - verifySingleInferredProject(session); + it("findAllReferences -- starting at definition", () => { + const session = makeSampleProjects(); + ts.projectSystem.openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + refs: [referenceATs(aTs, /*isDefinition*/ true), ...referencesUserTs(userTs, /*isDefinition*/ false)], + symbolName: "fnA", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(aTs.content, "fnA").offset, + symbolDisplayString: "function fnA(): void", }); + verifyATsConfigWhenOpened(session); + }); - it("navigateToAll -- when neither file nor project is specified", () => { - const session = makeSampleProjects(/*addUserTsConfig*/ true, /*keepAllFiles*/ true); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { file: undefined, searchValue: "fn" }); - assert.deepEqual(response, [ - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ - file: bTs, - text: "export function fnB() {}" + interface ReferencesFullRequest extends ts.projectSystem.protocol.FileLocationRequest { + readonly command: ts.projectSystem.protocol.CommandTypes.ReferencesFull; + } + interface ReferencesFullResponse extends ts.projectSystem.protocol.Response { + readonly body: readonly ts.ReferencedSymbol[]; + } + + it("findAllReferencesFull", () => { + const session = makeSampleProjects(); + + const responseFull = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.ReferencesFull, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(responseFull, [ + { + definition: { + ...documentSpanFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" }), - name: "fnB", - matchKind: "prefix", - isCaseSensitive: true, kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", + name: "function fnA(): void", + containerKind: ts.ScriptElementKind.unknown, + containerName: "", + displayParts: [ + ts.keywordPart(ts.SyntaxKind.FunctionKeyword), + ts.spacePart(), + ts.displayPart("fnA", ts.SymbolDisplayPartKind.functionName), + ts.punctuationPart(ts.SyntaxKind.OpenParenToken), + ts.punctuationPart(ts.SyntaxKind.CloseParenToken), + ts.punctuationPart(ts.SyntaxKind.ColonToken), + ts.spacePart(), + ts.keywordPart(ts.SyntaxKind.VoidKeyword), + ], }, - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ + references: [ + makeReferencedSymbolEntry({ file: userTs, text: "fnA" }), + makeReferencedSymbolEntry({ file: aTs, text: "fnA", isDefinition: true, isWriteAccess: true, contextText: "export function fnA() {}" }), + ], + }, + ]); + verifyATsConfigOriginalProject(session); + }); + + it("findAllReferencesFull definition is in mapped file", () => { + const aTs: ts.projectSystem.File = { path: "/a/a.ts", content: `function f() {}` }; + const aTsconfig: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), + }; + const bTs: ts.projectSystem.File = { path: "/b/b.ts", content: `f();` }; + const bTsconfig: ts.projectSystem.File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; + const aDts: ts.projectSystem.File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; + const aDtsMap: ts.projectSystem.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 = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + ts.projectSystem.openFilesForSession([bTs], session); + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); // configured project of b is alive since a references b + const responseFull = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.ReferencesFull, ts.projectSystem.protocolFileLocationFromSubstring(bTs, "f()")); + assert.deepEqual(responseFull, [ + { + definition: { + ...documentSpanFromSubstring({ file: aTs, - text: "export function fnA() {}" + text: "f", + options: { index: 1 }, + contextText: "function f() {}" }), - name: "fnA", - matchKind: "prefix", - isCaseSensitive: true, + containerKind: ts.ScriptElementKind.unknown, + containerName: "", + displayParts: [ + ts.keywordPart(ts.SyntaxKind.FunctionKeyword), + ts.spacePart(), + ts.displayPart("f", ts.SymbolDisplayPartKind.functionName), + ts.punctuationPart(ts.SyntaxKind.OpenParenToken), + ts.punctuationPart(ts.SyntaxKind.CloseParenToken), + ts.punctuationPart(ts.SyntaxKind.ColonToken), + ts.spacePart(), + ts.keywordPart(ts.SyntaxKind.VoidKeyword), + ], kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", + name: "function f(): void", }, - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ - file: userTs, - text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + references: [ + makeReferencedSymbolEntry({ + file: aTs, + text: "f", + options: { index: 1 }, + contextText: "function f() {}", + isWriteAccess: true, }), - name: "fnUser", - matchKind: "prefix", - isCaseSensitive: true, - kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", - } - ]); - }); + { + fileName: bTs.path, + isInString: undefined, + isWriteAccess: false, + textSpan: { start: 0, length: 1 }, + }, + ], + } + ]); + }); - it("navigateToAll -- when file is not specified but project is", () => { - const session = makeSampleProjects(/*addUserTsConfig*/ true, /*keepAllFiles*/ true); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.Navto, { projectFileName: bTsconfig.path, file: undefined, searchValue: "fn" }); - assert.deepEqual(response, [ - { - ...ts.projectSystem.protocolFileSpanFromSubstring({ - file: bTs, - text: "export function fnB() {}" - }), - name: "fnB", - matchKind: "prefix", - isCaseSensitive: true, - kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", - } - ]); - }); + it("findAllReferences -- target does not exist", () => { + const session = makeSampleProjects(); - const referenceATs = (aTs: ts.projectSystem.File, isDefinition: true | undefined): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ - file: aTs, - isDefinition, - isWriteAccess: true, - text: "fnA", - contextText: "export function fnA() {}", - lineText: "export function fnA() {}" + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + refs: [ + ts.projectSystem.makeReferenceItem({ + file: bDts, + isWriteAccess: true, + text: "fnB", + contextText: "export declare function fnB(): void;", + lineText: "export declare function fnB(): void;" + }), + ts.projectSystem.makeReferenceItem({ + file: userTs, + text: "fnB", + lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + ], + symbolName: "fnB", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(userTs.content, "fnB()").offset, + symbolDisplayString: "function fnB(): void", }); - const referencesUserTs = (userTs: ts.projectSystem.File, isDefinition: false | undefined): readonly ts.projectSystem.protocol.ReferencesResponseItem[] => [ - ts.projectSystem.makeReferenceItem({ - file: userTs, - isDefinition, - text: "fnA", - lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" - }), - ]; - - it("findAllReferences", () => { - const session = makeSampleProjects(); + verifySingleInferredProject(session); + }); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - refs: [...referencesUserTs(userTs, /*isDefinition*/ undefined), referenceATs(aTs, /*isDefinition*/ true)], - symbolName: "fnA", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(userTs.content, "fnA()").offset, - symbolDisplayString: "function fnA(): void", - }); + const renameATs = (aTs: ts.projectSystem.File): ts.projectSystem.protocol.SpanGroup => ({ + file: aTs.path, + locs: [ + ts.projectSystem.protocolRenameSpanFromSubstring({ + fileText: aTs.content, + text: "fnA", + contextText: "export function fnA() {}" + }) + ], + }); + const renameUserTs = (userTs: ts.projectSystem.File): ts.projectSystem.protocol.SpanGroup => ({ + file: userTs.path, + locs: [ + ts.projectSystem.protocolRenameSpanFromSubstring({ + fileText: userTs.content, + text: "fnA" + }) + ], + }); - verifyATsConfigOriginalProject(session); + it("renameLocations", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/bin/a".fnA', + kind: ts.ScriptElementKind.functionElement, + kindModifiers: [ts.ScriptElementKindModifier.exportedModifier, ts.ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), + }, + locs: [renameUserTs(userTs), renameATs(aTs)], }); + verifyATsConfigOriginalProject(session); + }); - it("findAllReferences -- starting at definition", () => { - const session = makeSampleProjects(); - ts.projectSystem.openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - refs: [referenceATs(aTs, /*isDefinition*/ true), ...referencesUserTs(userTs, /*isDefinition*/ false)], - symbolName: "fnA", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(aTs.content, "fnA").offset, - symbolDisplayString: "function fnA(): void", - }); - verifyATsConfigWhenOpened(session); + it("renameLocations -- starting at definition", () => { + const session = makeSampleProjects(); + ts.projectSystem.openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/a".fnA', + kind: ts.ScriptElementKind.functionElement, + kindModifiers: ts.ScriptElementKindModifier.exportedModifier, + triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(aTs.content, "fnA"), + }, + locs: [renameATs(aTs), renameUserTs(userTs)], }); + verifyATsConfigWhenOpened(session); + }); - interface ReferencesFullRequest extends ts.projectSystem.protocol.FileLocationRequest { - readonly command: ts.projectSystem.protocol.CommandTypes.ReferencesFull; - } - interface ReferencesFullResponse extends ts.projectSystem.protocol.Response { - readonly body: readonly ts.ReferencedSymbol[]; - } - - it("findAllReferencesFull", () => { - const session = makeSampleProjects(); + it("renameLocationsFull", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.RenameLocationsFull, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + renameLocation({ file: userTs, text: "fnA" }), + renameLocation({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }), + ]); + verifyATsConfigOriginalProject(session); + }); - const responseFull = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.ReferencesFull, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(responseFull, [ + it("renameLocations -- target does not exist", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnB", + fileToRename: undefined, + fullDisplayName: '"/b/bin/b".fnB', + kind: ts.ScriptElementKind.functionElement, + kindModifiers: [ts.ScriptElementKindModifier.exportedModifier, ts.ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnB"), + }, + locs: [ { - definition: { - ...documentSpanFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }), - kind: ts.ScriptElementKind.functionElement, - name: "function fnA(): void", - containerKind: ts.ScriptElementKind.unknown, - containerName: "", - displayParts: [ - ts.keywordPart(ts.SyntaxKind.FunctionKeyword), - ts.spacePart(), - ts.displayPart("fnA", ts.SymbolDisplayPartKind.functionName), - ts.punctuationPart(ts.SyntaxKind.OpenParenToken), - ts.punctuationPart(ts.SyntaxKind.CloseParenToken), - ts.punctuationPart(ts.SyntaxKind.ColonToken), - ts.spacePart(), - ts.keywordPart(ts.SyntaxKind.VoidKeyword), - ], - }, - references: [ - makeReferencedSymbolEntry({ file: userTs, text: "fnA" }), - makeReferencedSymbolEntry({ file: aTs, text: "fnA", isDefinition: true, isWriteAccess: true, contextText: "export function fnA() {}" }), + file: bDts.path, + locs: [ + ts.projectSystem.protocolRenameSpanFromSubstring({ + fileText: bDts.content, + text: "fnB", + contextText: "export declare function fnB(): void;" + }) ], }, - ]); - verifyATsConfigOriginalProject(session); - }); - - it("findAllReferencesFull definition is in mapped file", () => { - const aTs: ts.projectSystem.File = { path: "/a/a.ts", content: `function f() {}` }; - const aTsconfig: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), - }; - const bTs: ts.projectSystem.File = { path: "/b/b.ts", content: `f();` }; - const bTsconfig: ts.projectSystem.File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; - const aDts: ts.projectSystem.File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; - const aDtsMap: ts.projectSystem.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 = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - ts.projectSystem.openFilesForSession([bTs], session); - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); // configured project of b is alive since a references b - const responseFull = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.ReferencesFull, ts.projectSystem.protocolFileLocationFromSubstring(bTs, "f()")); - assert.deepEqual(responseFull, [ { - definition: { - ...documentSpanFromSubstring({ - file: aTs, - text: "f", - options: { index: 1 }, - contextText: "function f() {}" - }), - containerKind: ts.ScriptElementKind.unknown, - containerName: "", - displayParts: [ - ts.keywordPart(ts.SyntaxKind.FunctionKeyword), - ts.spacePart(), - ts.displayPart("f", ts.SymbolDisplayPartKind.functionName), - ts.punctuationPart(ts.SyntaxKind.OpenParenToken), - ts.punctuationPart(ts.SyntaxKind.CloseParenToken), - ts.punctuationPart(ts.SyntaxKind.ColonToken), - ts.spacePart(), - ts.keywordPart(ts.SyntaxKind.VoidKeyword), - ], - kind: ts.ScriptElementKind.functionElement, - name: "function f(): void", - }, - references: [ - makeReferencedSymbolEntry({ - file: aTs, - text: "f", - options: { index: 1 }, - contextText: "function f() {}", - isWriteAccess: true, - }), - { - fileName: bTs.path, - isInString: undefined, - isWriteAccess: false, - textSpan: { start: 0, length: 1 }, - }, + file: userTs.path, + locs: [ + ts.projectSystem.protocolRenameSpanFromSubstring({ + fileText: userTs.content, + text: "fnB" + }) ], - } - ]); - }); - - it("findAllReferences -- target does not exist", () => { - const session = makeSampleProjects(); - - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - refs: [ - ts.projectSystem.makeReferenceItem({ - file: bDts, - isWriteAccess: true, - text: "fnB", - contextText: "export declare function fnB(): void;", - lineText: "export declare function fnB(): void;" - }), - ts.projectSystem.makeReferenceItem({ - file: userTs, - text: "fnB", - lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" - }), - ], - symbolName: "fnB", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(userTs.content, "fnB()").offset, - symbolDisplayString: "function fnB(): void", - }); - verifySingleInferredProject(session); - }); - - const renameATs = (aTs: ts.projectSystem.File): ts.projectSystem.protocol.SpanGroup => ({ - file: aTs.path, - locs: [ - ts.projectSystem.protocolRenameSpanFromSubstring({ - fileText: aTs.content, - text: "fnA", - contextText: "export function fnA() {}" - }) - ], - }); - const renameUserTs = (userTs: ts.projectSystem.File): ts.projectSystem.protocol.SpanGroup => ({ - file: userTs.path, - locs: [ - ts.projectSystem.protocolRenameSpanFromSubstring({ - fileText: userTs.content, - text: "fnA" - }) + }, ], }); + verifySingleInferredProject(session); + }); - it("renameLocations", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/bin/a".fnA', - kind: ts.ScriptElementKind.functionElement, - kindModifiers: [ts.ScriptElementKindModifier.exportedModifier, ts.ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), - }, - locs: [renameUserTs(userTs), renameATs(aTs)], - }); - verifyATsConfigOriginalProject(session); + it("getEditsForFileRename", () => { + const session = makeSampleProjects(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/aNew.ts", }); + assert.deepEqual(response, [ + { + fileName: userTs.path, + textChanges: [ + { ...ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, + ], + }, + ]); + verifySingleInferredProject(session); + }); - it("renameLocations -- starting at definition", () => { - const session = makeSampleProjects(); - ts.projectSystem.openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/a".fnA', - kind: ts.ScriptElementKind.functionElement, - kindModifiers: ts.ScriptElementKindModifier.exportedModifier, - triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(aTs.content, "fnA"), + it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { + const aTs: ts.projectSystem.File = { path: "/a/src/a.ts", content: "" }; + const aTsconfig: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + outDir: "./build", + } + }), + }; + const bTs: ts.projectSystem.File = { path: "/b/src/b.ts", content: "" }; + const bTsconfig: ts.projectSystem.File = { + path: "/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./build", }, - locs: [renameATs(aTs), renameUserTs(userTs)], - }); - verifyATsConfigWhenOpened(session); - }); + include: ["./src"], + references: [{ path: "../a" }], + }), + }; - it("renameLocationsFull", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.RenameLocationsFull, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ - renameLocation({ file: userTs, text: "fnA" }), - renameLocation({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }), - ]); - verifyATsConfigOriginalProject(session); + const host = ts.projectSystem.createServerHost([aTs, aTsconfig, bTs, bTsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs, bTs], session); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/src/a1.ts", }); + assert.deepEqual(response, []); // Should not change anything + }); - it("renameLocations -- target does not exist", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Rename, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnB", - fileToRename: undefined, - fullDisplayName: '"/b/bin/b".fnB', - kind: ts.ScriptElementKind.functionElement, - kindModifiers: [ts.ScriptElementKindModifier.exportedModifier, ts.ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnB"), - }, - locs: [ - { - file: bDts.path, - locs: [ - ts.projectSystem.protocolRenameSpanFromSubstring({ - fileText: bDts.content, - text: "fnB", - contextText: "export declare function fnB(): void;" - }) - ], - }, - { - file: userTs.path, - locs: [ - ts.projectSystem.protocolRenameSpanFromSubstring({ - fileText: userTs.content, - text: "fnB" - }) - ], - }, + it("does not jump to source if inlined sources", () => { + const aDtsInlinedSources: ts.RawSourceMap = { + ...aDtsMapContent, + sourcesContent: [aTs.content] + }; + const aDtsMapInlinedSources: ts.projectSystem.File = { + path: aDtsMap.path, + content: JSON.stringify(aDtsInlinedSources) + }; + const host = ts.projectSystem.createServerHost([aTs, aDtsMapInlinedSources, aDts, bTs, bDtsMap, bDts, userTs, dummyFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([userTs], session); + const service = session.getProjectService(); + // If config file then userConfig project and bConfig project since it is referenced + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + + // Inlined so does not jump to aTs + assert.deepEqual(ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")), { + textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: aDts, + text: "fnA", + contextText: "export declare function fnA(): void;" + }) ], - }); - verifySingleInferredProject(session); - }); - - it("getEditsForFileRename", () => { - const session = makeSampleProjects(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/aNew.ts", - }); - assert.deepEqual(response, [ - { - fileName: userTs.path, - textChanges: [ - { ...ts.projectSystem.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: ts.projectSystem.File = { path: "/a/src/a.ts", content: "" }; - const aTsconfig: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: true, - outDir: "./build", - } - }), - }; - const bTs: ts.projectSystem.File = { path: "/b/src/b.ts", content: "" }; - const bTsconfig: ts.projectSystem.File = { - path: "/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "./build", - }, - include: ["./src"], - references: [{ path: "../a" }], - }), - }; - - const host = ts.projectSystem.createServerHost([aTs, aTsconfig, bTs, bTsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs, bTs], session); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/src/a1.ts", - }); - assert.deepEqual(response, []); // Should not change anything + // Not inlined, jumps to bTs + assert.deepEqual(ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")), { + textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnB"), + definitions: [ + ts.projectSystem.protocolFileSpanWithContextFromSubstring({ + file: bTs, + text: "fnB", + contextText: "export function fnB() {}" + }) + ], }); - it("does not jump to source if inlined sources", () => { - const aDtsInlinedSources: ts.RawSourceMap = { - ...aDtsMapContent, - sourcesContent: [aTs.content] - }; - const aDtsMapInlinedSources: ts.projectSystem.File = { - path: aDtsMap.path, - content: JSON.stringify(aDtsInlinedSources) - }; - const host = ts.projectSystem.createServerHost([aTs, aDtsMapInlinedSources, aDts, bTs, bDtsMap, bDts, userTs, dummyFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([userTs], session); - const service = session.getProjectService(); - // If config file then userConfig project and bConfig project since it is referenced - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - - // Inlined so does not jump to aTs - assert.deepEqual(ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnA()")), { - textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - ts.projectSystem.protocolFileSpanWithContextFromSubstring({ - file: aDts, - text: "fnA", - contextText: "export declare function fnA(): void;" - }) - ], - }); - - // Not inlined, jumps to bTs - assert.deepEqual(ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, ts.projectSystem.protocolFileLocationFromSubstring(userTs, "fnB()")), { - textSpan: ts.projectSystem.protocolTextSpanFromSubstring(userTs.content, "fnB"), - definitions: [ - ts.projectSystem.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 e364f75af674b..559ea8112c69d 100644 --- a/src/testRunner/unittests/tsserver/documentRegistry.ts +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -1,93 +1,93 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: document registry in project service", () => { - const importModuleContent = `import {a} from "./module1"`; - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/index.ts`, - content: importModuleContent - }; - const moduleFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/module1.d.ts`, - content: "export const a: number;" - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ files: ["index.ts"] }) - }; +describe("unittests:: tsserver:: document registry in project service", () => { + const importModuleContent = `import {a} from "./module1"`; + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/index.ts`, + content: importModuleContent + }; + const moduleFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/module1.d.ts`, + content: "export const a: number;" + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["index.ts"] }) + }; - function getProject(service: ts.projectSystem.TestProjectService) { - return service.configuredProjects.get(configFile.path)!; - } + function getProject(service: ts.projectSystem.TestProjectService) { + return service.configuredProjects.get(configFile.path)!; + } - function checkProject(service: ts.projectSystem.TestProjectService, moduleIsOrphan: boolean) { - // Update the project - const project = getProject(service); - project.getLanguageService(); - ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.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, moduleInfo.scriptKind), [[key, moduleIsOrphan ? undefined : 1]]); - } + function checkProject(service: ts.projectSystem.TestProjectService, moduleIsOrphan: boolean) { + // Update the project + const project = getProject(service); + project.getLanguageService(); + ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.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, moduleInfo.scriptKind), [[key, moduleIsOrphan ? undefined : 1]]); + } - function createServiceAndHost() { - const host = ts.projectSystem.createServerHost([file, moduleFile, ts.projectSystem.libFile, configFile]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(file.path); - checkProject(service, /*moduleIsOrphan*/ false); - return { host, service }; - } + function createServiceAndHost() { + const host = ts.projectSystem.createServerHost([file, moduleFile, ts.projectSystem.libFile, configFile]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(file.path); + checkProject(service, /*moduleIsOrphan*/ false); + return { host, service }; + } - function changeFileToNotImportModule(service: ts.projectSystem.TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, ts.singleIterator({ span: { start: 0, length: importModuleContent.length }, newText: "" })); - checkProject(service, /*moduleIsOrphan*/ true); - } + function changeFileToNotImportModule(service: ts.projectSystem.TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, ts.singleIterator({ span: { start: 0, length: importModuleContent.length }, newText: "" })); + checkProject(service, /*moduleIsOrphan*/ true); + } - function changeFileToImportModule(service: ts.projectSystem.TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, ts.singleIterator({ span: { start: 0, length: 0 }, newText: importModuleContent })); - checkProject(service, /*moduleIsOrphan*/ false); - } + function changeFileToImportModule(service: ts.projectSystem.TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, ts.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); + 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); + 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); + // 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); - }); + // 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); + 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); + 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); + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; - host.writeFile(moduleFile.path, updatedModuleContent); + 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); - }); + // 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 d46689c13a385..bd71382a59135 100644 --- a/src/testRunner/unittests/tsserver/duplicatePackages.ts +++ b/src/testRunner/unittests/tsserver/duplicatePackages.ts @@ -1,53 +1,53 @@ 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: ts.projectSystem.File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; - const aFooPackage: ts.projectSystem.File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; - const bFooIndex: ts.projectSystem.File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; - const bFooPackage: ts.projectSystem.File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; +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: ts.projectSystem.File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; + const aFooPackage: ts.projectSystem.File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; + const bFooIndex: ts.projectSystem.File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; + const bFooPackage: ts.projectSystem.File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; - const userContent = 'import("foo");\nfoo'; - const aUser: ts.projectSystem.File = { path: "/a/user.ts", content: userContent }; - const bUser: ts.projectSystem.File = { path: "/b/user.ts", content: userContent }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}", - }; + const userContent = 'import("foo");\nfoo'; + const aUser: ts.projectSystem.File = { path: "/a/user.ts", content: userContent }; + const bUser: ts.projectSystem.File = { path: "/b/user.ts", content: userContent }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}", + }; - const host = ts.projectSystem.createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aUser, bUser], session); + const host = ts.projectSystem.createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aUser, bUser], session); - for (const user of [aUser, bUser]) { - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetCodeFixes, { - file: user.path, - startLine: 2, - startOffset: 1, - endLine: 2, - endOffset: 4, - errorCodes: [ts.Diagnostics.Cannot_find_name_0.code], - }); - assert.deepEqual(response, [ - { - description: `Add import from "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', - }], + for (const user of [aUser, bUser]) { + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetCodeFixes, { + file: user.path, + startLine: 2, + startOffset: 1, + endLine: 2, + endOffset: 4, + errorCodes: [ts.Diagnostics.Cannot_find_name_0.code], + }); + assert.deepEqual(response, [ + { + description: `Add import from "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', }], - 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 9ebed4b2683eb..8b440657ff84e 100644 --- a/src/testRunner/unittests/tsserver/dynamicFiles.ts +++ b/src/testRunner/unittests/tsserver/dynamicFiles.ts @@ -1,252 +1,252 @@ namespace ts.projectSystem { - export function verifyDynamic(service: ts.server.ProjectService, path: string) { - const info = ts.Debug.checkDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(ts.arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); - assert.isTrue(info.isDynamic); - } +export function verifyDynamic(service: ts.server.ProjectService, path: string) { + const info = ts.Debug.checkDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(ts.arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); + assert.isTrue(info.isDynamic); +} - function verifyPathRecognizedAsDynamic(path: string) { - const file: ts.projectSystem.File = { - path, - content: `/// +function verifyPathRecognizedAsDynamic(path: string) { + const file: ts.projectSystem.File = { + path, + content: `/// /// var x = 10;` + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(file.path, file.content); + verifyDynamic(projectService, projectService.toPath(file.path)); + + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectRootFiles(project, [file.path]); + ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path]); +} + +describe("unittests:: tsserver:: dynamicFiles:: Untitled files", () => { + const untitledFile = "untitled:^Untitled-1"; + it("Can convert positions to locations", () => { + const aTs: ts.projectSystem.File = { path: "/proj/a.ts", content: "" }; + const tsconfig: ts.projectSystem.File = { path: "/proj/tsconfig.json", content: "{}" }; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); + ts.projectSystem.openFilesForSession([aTs], session); + ts.projectSystem.executeSessionRequestNoResponse(session, ts.projectSystem.protocol.CommandTypes.Open, { + file: untitledFile, + fileContent: `/// \nlet foo = 1;\nfooo/**/`, + scriptKindName: "TS", + projectRootPath: "/proj", + }); + verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetCodeFixes, { + file: untitledFile, + startLine: 3, + startOffset: 1, + endLine: 3, + endOffset: 5, + errorCodes: [ts.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: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = ts.projectSystem.createServerHost([config, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true, currentDirectory: ts.tscWatch.projectRoot }); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); + const untitled: ts.projectSystem.File = { + path: `${ts.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, ts.tscWatch.projectRoot); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + + service.closeClientFile(untitledFile); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); + verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); + ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + }); + + it("opening and closing untitled files when projectRootPath is different from currentDirectory", () => { + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile]); + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file.ts`, + content: "const y = 10" + }; + const host = ts.projectSystem.createServerHost([config, file, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); + const service = ts.projectSystem.createProjectService(host, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); + + // Close untitled file + service.closeClientFile(untitledFile); + + // Open file from configured project which should collect inferredProject + service.openClientFile(file.path); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + }); + + it("when changing scriptKind of the untitled files", () => { + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); + const service = ts.projectSystem.createProjectService(host, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(untitledFile, "const x = 10;", ts.ScriptKind.TS, ts.tscWatch.projectRoot); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + const program = service.inferredProjects[0].getCurrentProgram()!; + const sourceFile = program.getSourceFile(untitledFile)!; + + // Close untitled file + service.closeClientFile(untitledFile); + + // Open untitled file with different mode + service.openClientFile(untitledFile, "const x = 10;", ts.ScriptKind.TSX, ts.tscWatch.projectRoot); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); + const newProgram = service.inferredProjects[0].getCurrentProgram()!; + const newSourceFile = newProgram.getSourceFile(untitledFile)!; + assert.notStrictEqual(newProgram, program); + assert.notStrictEqual(newSourceFile, sourceFile); + }); +}); + +describe("unittests:: tsserver:: dynamicFiles:: ", () => { + it("dynamic file without external project", () => { + const file: ts.projectSystem.File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file.path, file.content); - verifyDynamic(projectService, projectService.toPath(file.path)); + projectService.setCompilerOptionsForInferredProjects({ + module: ts.ModuleKind.CommonJS, + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true + }); + projectService.openClientFile(file.path, "var x = 10;"); projectService.checkNumberOfProjects({ inferredProjects: 1 }); const project = projectService.inferredProjects[0]; ts.projectSystem.checkProjectRootFiles(project, [file.path]); ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path]); - } - - describe("unittests:: tsserver:: dynamicFiles:: Untitled files", () => { - const untitledFile = "untitled:^Untitled-1"; - it("Can convert positions to locations", () => { - const aTs: ts.projectSystem.File = { path: "/proj/a.ts", content: "" }; - const tsconfig: ts.projectSystem.File = { path: "/proj/tsconfig.json", content: "{}" }; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); - ts.projectSystem.openFilesForSession([aTs], session); - ts.projectSystem.executeSessionRequestNoResponse(session, ts.projectSystem.protocol.CommandTypes.Open, { - file: untitledFile, - fileContent: `/// \nlet foo = 1;\nfooo/**/`, - scriptKindName: "TS", - projectRootPath: "/proj", - }); - verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetCodeFixes, { - file: untitledFile, - startLine: 3, - startOffset: 1, - endLine: 3, - endOffset: 5, - errorCodes: [ts.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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = ts.projectSystem.createServerHost([config, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true, currentDirectory: ts.tscWatch.projectRoot }); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); - const untitled: ts.projectSystem.File = { - path: `${ts.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, ts.tscWatch.projectRoot); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - - service.closeClientFile(untitledFile); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); - verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); - ts.projectSystem.checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, ts.projectSystem.libFile.path, config.path]); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - }); - - it("opening and closing untitled files when projectRootPath is different from currentDirectory", () => { - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file.ts`, - content: "const y = 10" - }; - const host = ts.projectSystem.createServerHost([config, file, ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); - const service = ts.projectSystem.createProjectService(host, { useInferredProjectPerProjectRoot: true }); - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, ts.tscWatch.projectRoot); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - verifyDynamic(service, `${ts.tscWatch.projectRoot}/${untitledFile}`); - - // Close untitled file - service.closeClientFile(untitledFile); - - // Open file from configured project which should collect inferredProject - service.openClientFile(file.path); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + verifyDynamic(projectService, `/${file.path}`); + + assert.strictEqual(projectService.ensureDefaultProjectForFile(ts.server.toNormalizedPath(file.path)), project); + const indexOfX = file.content.indexOf("x"); + assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { + kind: ts.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("when changing scriptKind of the untitled files", () => { - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); - const service = ts.projectSystem.createProjectService(host, { useInferredProjectPerProjectRoot: true }); - service.openClientFile(untitledFile, "const x = 10;", ts.ScriptKind.TS, ts.tscWatch.projectRoot); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - const program = service.inferredProjects[0].getCurrentProgram()!; - const sourceFile = program.getSourceFile(untitledFile)!; - - // Close untitled file - service.closeClientFile(untitledFile); - - // Open untitled file with different mode - service.openClientFile(untitledFile, "const x = 10;", ts.ScriptKind.TSX, ts.tscWatch.projectRoot); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [untitledFile, ts.projectSystem.libFile.path]); - const newProgram = service.inferredProjects[0].getCurrentProgram()!; - const newSourceFile = newProgram.getSourceFile(untitledFile)!; - assert.notStrictEqual(newProgram, program); - assert.notStrictEqual(newSourceFile, sourceFile); - }); + it("dynamic file with reference paths without external project", () => { + verifyPathRecognizedAsDynamic("^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js"); }); - describe("unittests:: tsserver:: dynamicFiles:: ", () => { - it("dynamic file without external project", () => { - const file: ts.projectSystem.File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile], { useCaseSensitiveFileNames: true }); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ - module: ts.ModuleKind.CommonJS, - allowJs: true, - allowSyntheticDefaultImports: true, - allowNonTsExtensions: true + describe("dynamic file with projectRootPath", () => { + const file: ts.projectSystem.File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const configProjectFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: "let y = 10;" + }; + it("with useInferredProjectPerProjectRoot", () => { + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const session = ts.projectSystem.createSession(host, { useInferredProjectPerProjectRoot: true }); + ts.projectSystem.openFilesForSession([{ file: file.path, projectRootPath: ts.tscWatch.projectRoot }], session); + + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file.path, ts.projectSystem.libFile.path]); + verifyDynamic(projectService, `${ts.tscWatch.projectRoot}/${file.path}`); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.GetOutliningSpans, + arguments: { + file: file.path + } }); - projectService.openClientFile(file.path, "var x = 10;"); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectRootFiles(project, [file.path]); - ts.projectSystem.checkProjectActualFiles(project, [file.path, ts.projectSystem.libFile.path]); - verifyDynamic(projectService, `/${file.path}`); - - assert.strictEqual(projectService.ensureDefaultProjectForFile(ts.server.toNormalizedPath(file.path)), project); - const indexOfX = file.content.indexOf("x"); - assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { - kind: ts.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, - }); + // Without project root + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, ts.projectSystem.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 = ts.projectSystem.createServerHost([ts.projectSystem.libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const projectService = ts.projectSystem.createProjectService(host); + try { + projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, ts.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 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, ts.projectSystem.libFile.path]); }); + }); - describe("dynamic file with projectRootPath", () => { - const file: ts.projectSystem.File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const configProjectFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: "let y = 10;" - }; - it("with useInferredProjectPerProjectRoot", () => { - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const session = ts.projectSystem.createSession(host, { useInferredProjectPerProjectRoot: true }); - ts.projectSystem.openFilesForSession([{ file: file.path, projectRootPath: ts.tscWatch.projectRoot }], session); - - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file.path, ts.projectSystem.libFile.path]); - verifyDynamic(projectService, `${ts.tscWatch.projectRoot}/${file.path}`); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.GetOutliningSpans, - arguments: { - file: file.path - } - }); - - // Without project root - const file2Path = file.path.replace("#1", "#2"); - projectService.openClientFile(file2Path, file.content); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, ts.projectSystem.libFile.path]); - }); - - it("fails when useInferredProjectPerProjectRoot is false", () => { - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const projectService = ts.projectSystem.createProjectService(host); - try { - projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, ts.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 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, ts.projectSystem.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 22b4abdb98442..b465659d25702 100644 --- a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts +++ b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts @@ -1,75 +1,75 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { +describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { - function getLargeFile(useLargeTsFile: boolean) { - return `src/large.${useLargeTsFile ? "ts" : "js"}`; - } - - function createSessionWithEventHandler(files: ts.projectSystem.File[], useLargeTsFile: boolean) { - const largeFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/${getLargeFile(useLargeTsFile)}`, - content: "export var x = 10;", - fileSize: ts.server.maxFileSize + 1 - }; - files.push(largeFile); - const host = ts.projectSystem.createServerHost(files); - const { session, events: largeFileReferencedEvents } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.LargeFileReferencedEvent); + function getLargeFile(useLargeTsFile: boolean) { + return `src/large.${useLargeTsFile ? "ts" : "js"}`; + } - return { session, verifyLargeFile }; + function createSessionWithEventHandler(files: ts.projectSystem.File[], useLargeTsFile: boolean) { + const largeFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/${getLargeFile(useLargeTsFile)}`, + content: "export var x = 10;", + fileSize: ts.server.maxFileSize + 1 + }; + files.push(largeFile); + const host = ts.projectSystem.createServerHost(files); + const { session, events: largeFileReferencedEvents } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.LargeFileReferencedEvent); - function verifyLargeFile(project: ts.server.Project) { - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + return { session, verifyLargeFile }; - // 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 : ""); + function verifyLargeFile(project: ts.server.Project) { + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? ts.emptyArray : [{ - eventName: ts.server.LargeFileReferencedEvent, - data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: ts.server.maxFileSize } - }]); - } - } - - function verifyLargeFile(useLargeTsFile: boolean) { - it("when large file is included by tsconfig", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/file.ts`, - content: "export var y = 10;" - }; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) - }; - const files = [file, ts.projectSystem.libFile, tsconfig]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - verifyLargeFile(service.configuredProjects.get(tsconfig.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 : ""); - it("when large file is included by module resolution", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/file.ts`, - content: `export var y = 10;import {x} from "./large"` - }; - const files = [file, ts.projectSystem.libFile]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - verifyLargeFile(service.inferredProjects[0]); - }); + assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? ts.emptyArray : [{ + eventName: ts.server.LargeFileReferencedEvent, + data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: ts.server.maxFileSize } + }]); } + } - describe("large file is ts file", () => { - verifyLargeFile(/*useLargeTsFile*/ true); + function verifyLargeFile(useLargeTsFile: boolean) { + it("when large file is included by tsconfig", () => { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) + }; + const files = [file, ts.projectSystem.libFile, tsconfig]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.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: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/file.ts`, + content: `export var y = 10;import {x} from "./large"` + }; + const files = [file, ts.projectSystem.libFile]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.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 7dbbd35d67f39..a88e05b26bff9 100644 --- a/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts +++ b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts @@ -1,77 +1,77 @@ 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 = ts.projectSystem.createServerHost([f1, f2, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLanguageServiceStateEvent); - session.executeCommand({ - seq: 0, - type: "request", - command: "open", - arguments: { file: f1.path } - } as ts.projectSystem.protocol.OpenRequest); - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = ts.projectSystem.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"); +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 = ts.projectSystem.createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + } as ts.projectSystem.protocol.OpenRequest); + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = ts.projectSystem.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.writeFile(configWithExclude.path, configWithExclude.content); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.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"); - }); + host.writeFile(configWithExclude.path, configWithExclude.content); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.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"); + }); - it("Large file size is determined correctly", () => { - const f1: ts.projectSystem.File = { - path: "/a/app.js", - content: "let x = 1;" - }; - const f2: ts.projectSystem.File = { - path: "/a/largefile.js", - content: "", - fileSize: ts.server.maxProgramSizeForNonTsFiles + 1 - }; - const f3: ts.projectSystem.File = { - path: "/a/extremlylarge.d.ts", - content: "", - fileSize: ts.server.maxProgramSizeForNonTsFiles + 100 - }; - const config = { - path: "/a/jsconfig.json", - content: "{}" - }; - const host = ts.projectSystem.createServerHost([f1, f2, f3, ts.projectSystem.libFile, config]); - const service = ts.projectSystem.createProjectService(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - service.openClientFile(f1.path); - const project = service.configuredProjects.get(config.path)!; - service.logger.logs.push(`languageServiceEnabled: ${project.languageServiceEnabled}`); - service.logger.logs.push(`lastFileExceededProgramSize: ${project.lastFileExceededProgramSize}`); - ts.projectSystem.baselineTsserverLogs("projectLanguageServiceStateEvent", "large file size is determined correctly", service); - }); + it("Large file size is determined correctly", () => { + const f1: ts.projectSystem.File = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2: ts.projectSystem.File = { + path: "/a/largefile.js", + content: "", + fileSize: ts.server.maxProgramSizeForNonTsFiles + 1 + }; + const f3: ts.projectSystem.File = { + path: "/a/extremlylarge.d.ts", + content: "", + fileSize: ts.server.maxProgramSizeForNonTsFiles + 100 + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const host = ts.projectSystem.createServerHost([f1, f2, f3, ts.projectSystem.libFile, config]); + const service = ts.projectSystem.createProjectService(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + service.openClientFile(f1.path); + const project = service.configuredProjects.get(config.path)!; + service.logger.logs.push(`languageServiceEnabled: ${project.languageServiceEnabled}`); + service.logger.logs.push(`lastFileExceededProgramSize: ${project.lastFileExceededProgramSize}`); + ts.projectSystem.baselineTsserverLogs("projectLanguageServiceStateEvent", "large file size is determined correctly", service); }); +}); } diff --git a/src/testRunner/unittests/tsserver/events/projectLoading.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts index 8eb6cb815d3ad..1529a94aa1e68 100644 --- a/src/testRunner/unittests/tsserver/events/projectLoading.ts +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -1,234 +1,234 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { - const aTs: ts.projectSystem.File = { - path: `${ts.tscWatch.projects}/a/a.ts`, - content: "export class A { }" - }; - const configA: ts.projectSystem.File = { - path: `${ts.tscWatch.projects}/a/tsconfig.json`, - content: "{}" - }; - const bTsPath = `${ts.tscWatch.projects}/b/b.ts`; - const configBPath = `${ts.tscWatch.projects}/b/tsconfig.json`; - const files = [ts.projectSystem.libFile, aTs, configA]; - function verifyProjectLoadingStartAndFinish(createSession: (host: ts.projectSystem.TestServerHost) => { - session: ts.projectSystem.TestSession; - getNumberOfEvents: () => number; - clearEvents: () => void; - verifyProjectLoadEvents: (expected: [ - ts.server.ProjectLoadingStartEvent, - ts.server.ProjectLoadingFinishEvent - ]) => void; - }) { - function createSessionToVerifyEvent(files: readonly ts.projectSystem.File[]) { - const host = ts.projectSystem.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: ts.server.Project, reason: string) { - verifyProjectLoadEvents([ - { eventName: ts.server.ProjectLoadingStartEvent, data: { project, reason } }, - { eventName: ts.server.ProjectLoadingFinishEvent, data: { project } } - ]); - clearEvents(); +describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + const aTs: ts.projectSystem.File = { + path: `${ts.tscWatch.projects}/a/a.ts`, + content: "export class A { }" + }; + const configA: ts.projectSystem.File = { + path: `${ts.tscWatch.projects}/a/tsconfig.json`, + content: "{}" + }; + const bTsPath = `${ts.tscWatch.projects}/b/b.ts`; + const configBPath = `${ts.tscWatch.projects}/b/tsconfig.json`; + const files = [ts.projectSystem.libFile, aTs, configA]; + function verifyProjectLoadingStartAndFinish(createSession: (host: ts.projectSystem.TestServerHost) => { + session: ts.projectSystem.TestSession; + getNumberOfEvents: () => number; + clearEvents: () => void; + verifyProjectLoadEvents: (expected: [ + ts.server.ProjectLoadingStartEvent, + ts.server.ProjectLoadingFinishEvent + ]) => void; + }) { + function createSessionToVerifyEvent(files: readonly ts.projectSystem.File[]) { + const host = ts.projectSystem.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: ts.server.Project, reason: string) { + verifyProjectLoadEvents([ + { eventName: ts.server.ProjectLoadingStartEvent, data: { project, reason } }, + { eventName: ts.server.ProjectLoadingFinishEvent, data: { project } } + ]); + clearEvents(); + } - function verifyEventWithOpenTs(file: ts.projectSystem.File, configPath: string, configuredProjects: number) { - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects }); - const project = service.configuredProjects.get(configPath)!; - assert.isDefined(project); - verifyEvent(project, `Creating possible configured project for ${file.path} to open`); - } + function verifyEventWithOpenTs(file: ts.projectSystem.File, configPath: string, configuredProjects: number) { + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.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: ts.projectSystem.File = { - path: bTsPath, - content: "export class B {}" - }; - const configB: ts.projectSystem.File = { - path: configBPath, - content: "{}" - }; - const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); - verifyEventWithOpenTs(aTs, configA.path, 1); - verifyEventWithOpenTs(bTs, configB.path, 2); - }); + it("when project is created by open file", () => { + const bTs: ts.projectSystem.File = { + path: bTsPath, + content: "export class B {}" + }; + const configB: ts.projectSystem.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); + 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`); + host.writeFile(configA.path, configA.content); + host.checkTimeoutQueueLengthAndRun(2); + const project = service.configuredProjects.get(configA.path)!; + verifyEvent(project, `Change in config file detected`); + }); + + it("when change is detected in an extended config file", () => { + const bTs: ts.projectSystem.File = { + path: bTsPath, + content: "export class B {}" + }; + const configB: ts.projectSystem.File = { + path: configBPath, + content: JSON.stringify({ + extends: "../a/tsconfig.json", + }) + }; + const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files.concat(bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); + + host.writeFile(configA.path, configA.content); + host.checkTimeoutQueueLengthAndRun(2); + const project = service.configuredProjects.get(configB.path)!; + verifyEvent(project, `Change in extended config file ${configA.path} detected`); + }); + + describe("when opening original location project", () => { + it("with project references", () => { + verify(); }); - it("when change is detected in an extended config file", () => { + it("when disableSourceOfProjectReferenceRedirect is true", () => { + verify(/*disableSourceOfProjectReferenceRedirect*/ true); + }); + + function verify(disableSourceOfProjectReferenceRedirect?: true) { + const aDTs: ts.projectSystem.File = { + path: `${ts.tscWatch.projects}/a/a.d.ts`, + content: `export declare class A { +} +//# sourceMappingURL=a.d.ts.map +` + }; + const aDTsMap: ts.projectSystem.File = { + path: `${ts.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: ts.projectSystem.File = { path: bTsPath, - content: "export class B {}" + content: `import {A} from "../a/a"; new A();` }; const configB: ts.projectSystem.File = { path: configBPath, content: JSON.stringify({ - extends: "../a/tsconfig.json", + ...(disableSourceOfProjectReferenceRedirect && { + compilerOptions: { + disableSourceOfProjectReferenceRedirect + } + }), + references: [{ path: "../a" }] }) }; - const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files.concat(bTs, configB)); - verifyEventWithOpenTs(bTs, configB.path, 1); - host.writeFile(configA.path, configA.content); - host.checkTimeoutQueueLengthAndRun(2); - const project = service.configuredProjects.get(configB.path)!; - verifyEvent(project, `Change in extended config file ${configA.path} detected`); - }); - - describe("when opening original location project", () => { - it("with project references", () => { - verify(); - }); + const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); - it("when disableSourceOfProjectReferenceRedirect is true", () => { - verify(/*disableSourceOfProjectReferenceRedirect*/ true); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: { + file: bTs.path, + ...ts.projectSystem.protocolLocationFromSubstring(bTs.content, "A()") + } }); - function verify(disableSourceOfProjectReferenceRedirect?: true) { - const aDTs: ts.projectSystem.File = { - path: `${ts.tscWatch.projects}/a/a.d.ts`, - content: `export declare class A { -} -//# sourceMappingURL=a.d.ts.map -` - }; - const aDTsMap: ts.projectSystem.File = { - path: `${ts.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: ts.projectSystem.File = { - path: bTsPath, - content: `import {A} from "../a/a"; new A();` - }; - const configB: ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.References, - arguments: { - file: bTs.path, - ...ts.projectSystem.protocolLocationFromSubstring(bTs.content, "A()") - } - }); - - ts.projectSystem.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}`); - } - }); + ts.projectSystem.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 = `${ts.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: ts.projectSystem.toExternalFiles([aTs.path, configA.path]), - options: {} - } as ts.projectSystem.protocol.ExternalProject); - ts.projectSystem.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}`); - } + describe("with external projects and config files ", () => { + const projectFileName = `${ts.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: ts.projectSystem.toExternalFiles([aTs.path, configA.path]), + options: {} + } as ts.projectSystem.protocol.ExternalProject); + ts.projectSystem.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); - - ts.projectSystem.openFilesForSession([aTs], session); - verifyEvent(); - }); + it("when lazyConfiguredProjectsFromExternalProject is false", () => { + const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); + verifyEvent(); + }); - it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { - const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); - assert.equal(getNumberOfEvents(), 0); + it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { + const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - verifyEvent(); - }); + ts.projectSystem.openFilesForSession([aTs], session); + verifyEvent(); }); - } - describe("when using event handler", () => { - verifyProjectLoadingStartAndFinish(host => { - const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLoadingStartEvent, ts.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 event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, events } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectLoadingStartEvent, ts.server.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 } = ts.projectSystem.createSessionWithDefaultEventHandler(host, [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]); - return { - session, - getNumberOfEvents: () => getEvents().length, - clearEvents, - verifyProjectLoadEvents - }; - - function verifyProjectLoadEvents(expected: [ - ts.server.ProjectLoadingStartEvent, - ts.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 default event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, getEvents, clearEvents } = ts.projectSystem.createSessionWithDefaultEventHandler(host, [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]); + return { + session, + getNumberOfEvents: () => getEvents().length, + clearEvents, + verifyProjectLoadEvents + }; + + function verifyProjectLoadEvents(expected: [ + ts.server.ProjectLoadingStartEvent, + ts.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); + } }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts index 29922ea15f7f0..38b68e42df9fb 100644 --- a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -1,58 +1,102 @@ 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 = new ts.Map(); - ts.forEach(actual, f => { - assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); - seen.set(f, true); - assert.isTrue(ts.contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); - }); - } - - function createVerifyInitialOpen(session: ts.projectSystem.TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: ts.server.ProjectsUpdatedInBackgroundEvent[]) => void) { - return (file: ts.projectSystem.File) => { - session.executeCommandSeq({ - command: ts.server.CommandNames.Open, - arguments: { - file: file.path - } - } as ts.projectSystem.protocol.OpenRequest); - verifyProjectsUpdatedInBackgroundEventHandler([]); +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 = new ts.Map(); + ts.forEach(actual, f => { + assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); + seen.set(f, true); + assert.isTrue(ts.contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); + }); + } + + function createVerifyInitialOpen(session: ts.projectSystem.TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: ts.server.ProjectsUpdatedInBackgroundEvent[]) => void) { + return (file: ts.projectSystem.File) => { + session.executeCommandSeq({ + command: ts.server.CommandNames.Open, + arguments: { + file: file.path + } + } as ts.projectSystem.protocol.OpenRequest); + verifyProjectsUpdatedInBackgroundEventHandler([]); + }; + } + + interface ProjectsUpdatedInBackgroundEventVerifier { + session: ts.projectSystem.TestSession; + verifyProjectsUpdatedInBackgroundEventHandler(events: ts.server.ProjectsUpdatedInBackgroundEvent[]): void; + verifyInitialOpen(file: ts.projectSystem.File): void; + } + + function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: ts.projectSystem.TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { + it("when adding new file", () => { + const commonFile1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: "export var x = 10;" }; - } - - interface ProjectsUpdatedInBackgroundEventVerifier { - session: ts.projectSystem.TestSession; - verifyProjectsUpdatedInBackgroundEventHandler(events: ts.server.ProjectsUpdatedInBackgroundEvent[]): void; - verifyInitialOpen(file: ts.projectSystem.File): void; - } + const commonFile2: ts.projectSystem.File = { + path: "/a/b/file2.ts", + content: "export var y = 10;" + }; + const commonFile3: ts.projectSystem.File = { + path: "/a/b/file3.ts", + content: "export var z = 10;" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const openFiles = [commonFile1.path]; + const host = ts.projectSystem.createServerHost([commonFile1, ts.projectSystem.libFile, configFile]); + const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + verifyInitialOpen(commonFile1); + + host.writeFile(commonFile2.path, commonFile2.content); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ts.server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + host.writeFile(commonFile3.path, commonFile3.content); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ts.server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + }); - function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: ts.projectSystem.TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { - it("when adding new file", () => { - const commonFile1: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: "export var x = 10;" + describe("with --out or --outFile setting", () => { + function verifyEventWithOutSettings(compilerOptions: ts.CompilerOptions = {}) { + const config: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions + }) }; - const commonFile2: ts.projectSystem.File = { - path: "/a/b/file2.ts", - content: "export var y = 10;" - }; - const commonFile3: ts.projectSystem.File = { - path: "/a/b/file3.ts", - content: "export var z = 10;" + + const f1: ts.projectSystem.File = { + path: "/a/a.ts", + content: "export let x = 1" }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{}` + const f2: ts.projectSystem.File = { + path: "/a/b.ts", + content: "export let y = 1" }; - const openFiles = [commonFile1.path]; - const host = ts.projectSystem.createServerHost([commonFile1, ts.projectSystem.libFile, configFile]); - const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - verifyInitialOpen(commonFile1); - host.writeFile(commonFile2.path, commonFile2.content); + const openFiles = [f1.path]; + const files = [f1, config, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + verifyInitialOpen(f1); + + host.writeFile(f2.path, f2.content); host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ eventName: ts.server.ProjectsUpdatedInBackgroundEvent, data: { @@ -60,7 +104,7 @@ namespace ts.projectSystem { } }]); - host.writeFile(commonFile3.path, commonFile3.content); + host.writeFile(f2.path, "export let x = 11"); host.runQueuedTimeoutCallbacks(); verifyProjectsUpdatedInBackgroundEventHandler([{ eventName: ts.server.ProjectsUpdatedInBackgroundEvent, @@ -68,512 +112,468 @@ namespace ts.projectSystem { openFiles } }]); - }); - - describe("with --out or --outFile setting", () => { - function verifyEventWithOutSettings(compilerOptions: ts.CompilerOptions = {}) { - const config: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions - }) - }; - - const f1: ts.projectSystem.File = { - path: "/a/a.ts", - content: "export let x = 1" - }; - const f2: ts.projectSystem.File = { - path: "/a/b.ts", - content: "export let y = 1" - }; - - const openFiles = [f1.path]; - const files = [f1, config, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - verifyInitialOpen(f1); - - host.writeFile(f2.path, f2.content); - host.runQueuedTimeoutCallbacks(); - - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: ts.server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - - host.writeFile(f2.path, "export let x = 11"); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: ts.server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - } + } - it("when both options are not set", () => { - verifyEventWithOutSettings(); - }); + it("when both options are not set", () => { + verifyEventWithOutSettings(); + }); - it("when --out is set", () => { - const outJs = "/a/out.js"; - verifyEventWithOutSettings({ out: outJs }); - }); + 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 --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?(): ts.projectSystem.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: ts.projectSystem.File = { - path: moduleFile1Path, - content: "export function Foo() { };", - }; + 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?(): ts.projectSystem.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: ts.projectSystem.File = { + path: moduleFile1Path, + content: "export function Foo() { };", + }; - const file1Consumer1: ts.projectSystem.File = { - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }; + const file1Consumer1: ts.projectSystem.File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }; - const file1Consumer2: ts.projectSystem.File = { - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }; + const file1Consumer2: ts.projectSystem.File = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; - const moduleFile2: ts.projectSystem.File = { - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }; + const moduleFile2: ts.projectSystem.File = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }; - const globalFile3: ts.projectSystem.File = { - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }; + const globalFile3: ts.projectSystem.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 additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; + const configFile = { + path: configFilePath, + content: JSON.stringify(configObj || { compilerOptions: {} }) + }; - const files: ts.projectSystem.File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, ts.projectSystem.libFile, configFile]; + const files: ts.projectSystem.File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, ts.projectSystem.libFile, configFile]; - const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; - const host = ts.projectSystem.createServerHost([filesToReload[0], configFile]); + const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; + const host = ts.projectSystem.createServerHost([filesToReload[0], configFile]); - // Initial project creation - const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - const openFiles = [filesToReload[0].path]; - verifyInitialOpen(filesToReload[0]); + // 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 - filesToReload.forEach(f => host.ensureFileOrFolder(f)); - if (!firstReloadFileList) - host.runQueuedTimeoutCallbacks(); // Invalidated module resolutions to schedule project update - verifyProjectsUpdatedInBackgroundEvent(); + // Since this is first event, it will have all the files + filesToReload.forEach(f => host.ensureFileOrFolder(f)); + if (!firstReloadFileList) + host.runQueuedTimeoutCallbacks(); // Invalidated module resolutions to schedule project update + verifyProjectsUpdatedInBackgroundEvent(); - return { - host, - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - updateContentOfOpenFile, - verifyNoProjectsUpdatedInBackgroundEvent, - verifyProjectsUpdatedInBackgroundEvent - }; + return { + host, + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + updateContentOfOpenFile, + verifyNoProjectsUpdatedInBackgroundEvent, + verifyProjectsUpdatedInBackgroundEvent + }; - function getFiles(filelist: string[]) { - return ts.map(filelist, getFile); - } + function getFiles(filelist: string[]) { + return ts.map(filelist, getFile); + } - function getFile(fileName: string) { - return ts.find(files, file => file.path === fileName)!; - } + function getFile(fileName: string) { + return ts.find(files, file => file.path === fileName)!; + } - function verifyNoProjectsUpdatedInBackgroundEvent() { - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([]); - } + function verifyNoProjectsUpdatedInBackgroundEvent() { + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([]); + } - function verifyProjectsUpdatedInBackgroundEvent() { - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: ts.server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - } + function verifyProjectsUpdatedInBackgroundEvent() { + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ts.server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + } - function updateContentOfOpenFile(file: ts.projectSystem.File, newContent: string) { - session.executeCommandSeq({ - command: ts.server.CommandNames.Change, - arguments: { - file: file.path, - insertString: newContent, - endLine: 1, - endOffset: file.content.length, - line: 1, - offset: 1 - } - }); - file.content = newContent; - } + function updateContentOfOpenFile(file: ts.projectSystem.File, newContent: string) { + session.executeCommandSeq({ + command: ts.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 { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + 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, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { console.log('hi'); };`); - verifyProjectsUpdatedInBackgroundEvent(); - }); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { console.log('hi'); };`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - it("should be up-to-date with the reference map changes", () => { - const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); + it("should be up-to-date with the reference map changes", () => { + const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); - // Change file1Consumer1 content to `export let y = Foo();` - updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent(); + // 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() { };` - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + host.writeFile(moduleFile1.path, `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(); + // 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() { };` - host.writeFile(moduleFile1.path, `export var T: number;export var T2: string;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + host.writeFile(moduleFile1.path, `export var T: number;export var T2: string;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); - // Multiple file edits in one go: + // 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();`); - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); - }); + // 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();`); + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - it("should be up-to-date with deleted files", () => { - const { host, moduleFile1, file1Consumer2, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + it("should be up-to-date with deleted files", () => { + const { host, moduleFile1, file1Consumer2, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - // Delete file1Consumer2 - host.deleteFile(file1Consumer2.path); - verifyProjectsUpdatedInBackgroundEvent(); - }); + // Delete file1Consumer2 + host.deleteFile(file1Consumer2.path); + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should be up-to-date with newly created files", () => { + const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); - it("should be up-to-date with newly created files", () => { - const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + host.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - host.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); - verifyProjectsUpdatedInBackgroundEvent(); + it("should detect changes in non-root files", () => { + const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { files: [file1Consumer1Path] }, }); - it("should detect changes in non-root files", () => { - const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { files: [file1Consumer1Path] }, - }); + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + // change file1 internal, and verify only file1 is affected + host.writeFile(moduleFile1.path, moduleFile1.content + "var T1: number;"); + verifyProjectsUpdatedInBackgroundEvent(); + }); - // change file1 internal, and verify only file1 is affected - host.writeFile(moduleFile1.path, moduleFile1.content + "var T1: number;"); - verifyProjectsUpdatedInBackgroundEvent(); - }); + it("should return all files if a global file changed shape", () => { + const { host, globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - it("should return all files if a global file changed shape", () => { - const { host, globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + host.writeFile(globalFile3.path, globalFile3.content + "var T2: string;"); + verifyProjectsUpdatedInBackgroundEvent(); + }); - host.writeFile(globalFile3.path, globalFile3.content + "var T2: string;"); - verifyProjectsUpdatedInBackgroundEvent(); + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { isolatedModules: true } } }); - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { isolatedModules: true } } - }); + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - host.writeFile(moduleFile1.path, `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 { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } } }); - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } } - }); + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: ts.projectSystem.File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] }); - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: ts.projectSystem.File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); - verifyNoProjectsUpdatedInBackgroundEvent(); + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); + verifyNoProjectsUpdatedInBackgroundEvent(); - // Doesnt change the shape of file1Consumer1 - host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); + // Doesnt change the shape of file1Consumer1 + host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); - // Change both files before the timeout - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); - host.writeFile(moduleFile1.path, `export var T2: number;export function Foo() { };`); - verifyProjectsUpdatedInBackgroundEvent(); - }); + // Change both files before the timeout + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); + host.writeFile(moduleFile1.path, `export var T2: number;export function Foo() { };`); + verifyProjectsUpdatedInBackgroundEvent(); + }); - it("should work fine for files with circular references", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: ` + it("should work fine for files with circular references", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: ` /// export var t1 = 10;` - }; - const file2: ts.projectSystem.File = { - path: "/a/b/file2.ts", - content: ` + }; + const file2: ts.projectSystem.File = { + path: "/a/b/file2.ts", + content: ` /// export var t2 = 10;` - }; - const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [file1.path, ts.projectSystem.libFile.path, file2.path, configFilePath] - }); - - host.writeFile(file2.path, file2.content + "export var t3 = 10;"); - verifyProjectsUpdatedInBackgroundEvent(); + }; + const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [file1.path, ts.projectSystem.libFile.path, file2.path, configFilePath] }); - it("should detect removed code file", () => { - const referenceFile1: ts.projectSystem.File = { - path: "/a/b/referenceFile1.ts", - content: ` + host.writeFile(file2.path, file2.content + "export var t3 = 10;"); + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should detect removed code file", () => { + const referenceFile1: ts.projectSystem.File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, ts.projectSystem.libFile.path, moduleFile1Path, configFilePath] - }); - - host.deleteFile(moduleFile1Path); - verifyProjectsUpdatedInBackgroundEvent(); + }; + const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, ts.projectSystem.libFile.path, moduleFile1Path, configFilePath] }); - it("should detect non-existing code file", () => { - const referenceFile1: ts.projectSystem.File = { - path: "/a/b/referenceFile1.ts", - content: ` + host.deleteFile(moduleFile1Path); + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: ts.projectSystem.File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const { host, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, ts.projectSystem.libFile.path, configFilePath] - }); + }; + const { host, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, ts.projectSystem.libFile.path, configFilePath] + }); - updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent(); + updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent(); - // Create module File2 and see both files are saved - host.writeFile(moduleFile2.path, moduleFile2.content); - verifyProjectsUpdatedInBackgroundEvent(); - }); + // Create module File2 and see both files are saved + host.writeFile(moduleFile2.path, moduleFile2.content); + verifyProjectsUpdatedInBackgroundEvent(); }); + }); - describe("resolution when resolution cache size", () => { - function verifyWithMaxCacheLimit(useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { - const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; - const file1: ts.projectSystem.File = { - path: rootFolder + "a/b/project/file1.ts", - content: 'import a from "file2"' - }; - const file2: ts.projectSystem.File = { - path: rootFolder + "a/b/node_modules/file2.d.ts", - content: "export class a { }" - }; - const file3: ts.projectSystem.File = { - path: rootFolder + "a/b/project/file3.ts", - content: "export class c { }" - }; - const configFile: ts.projectSystem.File = { - path: rootFolder + "a/b/project/tsconfig.json", - content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) - }; - - const projectFiles = [file1, file3, ts.projectSystem.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 = ts.projectSystem.createServerHost(projectFiles); - const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - const projectService = session.getProjectService(); - verifyInitialOpen(file1); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - verifyProject(); - - file3.content += "export class d {}"; - host.writeFile(file3.path, file3.content); - host.checkTimeoutQueueLengthAndRun(2); - - // Since this is first event - verifyProject(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: ts.server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); + describe("resolution when resolution cache size", () => { + function verifyWithMaxCacheLimit(useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { + const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; + const file1: ts.projectSystem.File = { + path: rootFolder + "a/b/project/file1.ts", + content: 'import a from "file2"' + }; + const file2: ts.projectSystem.File = { + path: rootFolder + "a/b/node_modules/file2.d.ts", + content: "export class a { }" + }; + const file3: ts.projectSystem.File = { + path: rootFolder + "a/b/project/file3.ts", + content: "export class c { }" + }; + const configFile: ts.projectSystem.File = { + path: rootFolder + "a/b/project/tsconfig.json", + content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) + }; - projectFiles.push(file2); - host.writeFile(file2.path, file2.content); - host.runQueuedTimeoutCallbacks(); // For invalidation - host.runQueuedTimeoutCallbacks(); // For actual update - if (useSlashRootAsSomeNotRootFolderInUserDirectory) { - watchedRecursiveDirectories.length = 3; - } - else { - // file2 addition wont be detected - projectFiles.pop(); - assert.isTrue(host.fileExists(file2.path)); + const projectFiles = [file1, file3, ts.projectSystem.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 = ts.projectSystem.createServerHost(projectFiles); + const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(file1); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + verifyProject(); + + file3.content += "export class d {}"; + host.writeFile(file3.path, file3.content); + host.checkTimeoutQueueLengthAndRun(2); + + // Since this is first event + verifyProject(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ts.server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles } - verifyProject(); + }]); - verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ - eventName: ts.server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }] : []); + projectFiles.push(file2); + host.writeFile(file2.path, file2.content); + host.runQueuedTimeoutCallbacks(); // For invalidation + host.runQueuedTimeoutCallbacks(); // For actual update + if (useSlashRootAsSomeNotRootFolderInUserDirectory) { + watchedRecursiveDirectories.length = 3; + } + else { + // file2 addition wont be detected + projectFiles.pop(); + assert.isTrue(host.fileExists(file2.path)); + } + verifyProject(); - function verifyProject() { - ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, file => file.path)); - ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ + eventName: ts.server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles } + }] : []); + + function verifyProject() { + ts.projectSystem.checkProjectActualFiles(project, ts.map(projectFiles, file => file.path)); + ts.projectSystem.checkWatchedDirectories(host, [], /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); } + } - it("project is not at root level", () => { - verifyWithMaxCacheLimit(/*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); - }); + it("project is not at root level", () => { + verifyWithMaxCacheLimit(/*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); + }); - it("project is at root level", () => { - verifyWithMaxCacheLimit(/*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); - }); + it("project is at root level", () => { + verifyWithMaxCacheLimit(/*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); }); - } + }); + } - describe("when event handler is set in the session", () => { - verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); + describe("when event handler is set in the session", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); - function createSessionWithProjectChangedEventHandler(host: ts.projectSystem.TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { - const { session, events: projectChangedEvents } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectsUpdatedInBackgroundEvent); - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; + function createSessionWithProjectChangedEventHandler(host: ts.projectSystem.TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { + const { session, events: projectChangedEvents } = ts.projectSystem.createSessionWithEventTracking(host, ts.server.ProjectsUpdatedInBackgroundEvent); + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; - function eventToString(event: ts.server.ProjectsUpdatedInBackgroundEvent) { - return JSON.stringify(event && { eventName: event.eventName, data: event.data }); - } + function eventToString(event: ts.server.ProjectsUpdatedInBackgroundEvent) { + return JSON.stringify(event && { eventName: event.eventName, data: event.data }); + } - function eventsToString(events: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { - return "[" + ts.map(events, eventToString).join(",") + "]"; - } + function eventsToString(events: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { + return "[" + ts.map(events, eventToString).join(",") + "]"; + } - function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { - assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); - ts.forEach(projectChangedEvents, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); - verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); - }); + function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { + assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); + ts.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; - } + // 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("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)); - }); + describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { + verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); + }); - function createSessionThatUsesEvents(host: ts.projectSystem.TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { - const { session, getEvents, clearEvents } = ts.projectSystem.createSessionWithDefaultEventHandler(host, ts.server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); + function createSessionThatUsesEvents(host: ts.projectSystem.TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { + const { session, getEvents, clearEvents } = ts.projectSystem.createSessionWithDefaultEventHandler(host, ts.server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; - function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { - const expectedEvents: ts.projectSystem.protocol.ProjectsUpdatedInBackgroundEventBody[] = ts.map(expected, e => { - return { - openFiles: e.data.openFiles - }; - }); - const events = getEvents(); - assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${ts.map(events, e => e.body)} Expected: ${expectedEvents}`); - ts.forEach(events, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); - }); + function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) { + const expectedEvents: ts.projectSystem.protocol.ProjectsUpdatedInBackgroundEventBody[] = ts.map(expected, e => { + return { + openFiles: e.data.openFiles + }; + }); + const events = getEvents(); + assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${ts.map(events, e => e.body)} Expected: ${expectedEvents}`); + ts.forEach(events, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); + }); - // Verified the events, reset them - clearEvents(); + // Verified the events, reset them + clearEvents(); - if (events.length) { - host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate - } + if (events.length) { + host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate } } - }); + } }); +}); } diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 4a8e9c744f3bc..362a733553683 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -1,149 +1,149 @@ namespace ts.projectSystem { - const packageJson: ts.projectSystem.File = { - path: "/package.json", - content: `{ "dependencies": { "mobx": "*" } }` - }; - const aTs: ts.projectSystem.File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: ts.projectSystem.File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}", - }; - const ambientDeclaration: ts.projectSystem.File = { - path: "/ambient.d.ts", - content: "declare module 'ambient' {}" - }; - const mobxPackageJson: ts.projectSystem.File = { - path: "/node_modules/mobx/package.json", - content: `{ "name": "mobx", "version": "1.0.0" }` - }; - const mobxDts: ts.projectSystem.File = { - path: "/node_modules/mobx/index.d.ts", - content: "export declare function observable(): unknown;" - }; - const exportEqualsMappedType: ts.projectSystem.File = { - path: "/lib/foo/constants.d.ts", - content: ` +const packageJson: ts.projectSystem.File = { + path: "/package.json", + content: `{ "dependencies": { "mobx": "*" } }` +}; +const aTs: ts.projectSystem.File = { + path: "/a.ts", + content: "export const foo = 0;", +}; +const bTs: ts.projectSystem.File = { + path: "/b.ts", + content: "foo", +}; +const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}", +}; +const ambientDeclaration: ts.projectSystem.File = { + path: "/ambient.d.ts", + content: "declare module 'ambient' {}" +}; +const mobxPackageJson: ts.projectSystem.File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` +}; +const mobxDts: ts.projectSystem.File = { + path: "/node_modules/mobx/index.d.ts", + content: "export declare function observable(): unknown;" +}; +const exportEqualsMappedType: ts.projectSystem.File = { + path: "/lib/foo/constants.d.ts", + content: ` type Signals = "SIGINT" | "SIGABRT"; declare const exp: {} & { [K in Signals]: K }; export = exp;`, - }; +}; - describe("unittests:: tsserver:: exportMapCache", () => { - it("caches auto-imports in the same file", () => { - const { exportMapCache } = setup(); - assert.ok(exportMapCache.isUsableByFile(bTs.path as ts.Path)); - assert.ok(!exportMapCache.isEmpty()); - }); +describe("unittests:: tsserver:: exportMapCache", () => { + it("caches auto-imports in the same file", () => { + const { exportMapCache } = setup(); + assert.ok(exportMapCache.isUsableByFile(bTs.path as ts.Path)); + assert.ok(!exportMapCache.isEmpty()); + }); - it("invalidates the cache when new files are added", () => { - const { host, exportMapCache } = setup(); - host.writeFile("/src/a2.ts", aTs.content); - host.runQueuedTimeoutCallbacks(); - assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); - assert.ok(exportMapCache.isEmpty()); - }); + it("invalidates the cache when new files are added", () => { + const { host, exportMapCache } = setup(); + host.writeFile("/src/a2.ts", aTs.content); + host.runQueuedTimeoutCallbacks(); + assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); + assert.ok(exportMapCache.isEmpty()); + }); - it("invalidates the cache when files are deleted", () => { - const { host, projectService, exportMapCache } = setup(); - projectService.closeClientFile(aTs.path); - host.deleteFile(aTs.path); - host.runQueuedTimeoutCallbacks(); - assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); - assert.ok(exportMapCache.isEmpty()); - }); + it("invalidates the cache when files are deleted", () => { + const { host, projectService, exportMapCache } = setup(); + projectService.closeClientFile(aTs.path); + host.deleteFile(aTs.path); + host.runQueuedTimeoutCallbacks(); + assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); + assert.ok(exportMapCache.isEmpty()); + }); - it("does not invalidate the cache when package.json is changed inconsequentially", () => { - const { host, exportMapCache, project } = setup(); - host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`); - host.runQueuedTimeoutCallbacks(); - project.getPackageJsonAutoImportProvider(); - assert.ok(exportMapCache.isUsableByFile(bTs.path as ts.Path)); - assert.ok(!exportMapCache.isEmpty()); - }); + it("does not invalidate the cache when package.json is changed inconsequentially", () => { + const { host, exportMapCache, project } = setup(); + host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`); + host.runQueuedTimeoutCallbacks(); + project.getPackageJsonAutoImportProvider(); + assert.ok(exportMapCache.isUsableByFile(bTs.path as ts.Path)); + assert.ok(!exportMapCache.isEmpty()); + }); - it("invalidates the cache when package.json change results in AutoImportProvider change", () => { - const { host, exportMapCache, project } = setup(); - host.writeFile("/package.json", `{}`); - host.runQueuedTimeoutCallbacks(); - project.getPackageJsonAutoImportProvider(); - assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); - assert.ok(exportMapCache.isEmpty()); - }); + it("invalidates the cache when package.json change results in AutoImportProvider change", () => { + const { host, exportMapCache, project } = setup(); + host.writeFile("/package.json", `{}`); + host.runQueuedTimeoutCallbacks(); + project.getPackageJsonAutoImportProvider(); + assert.ok(!exportMapCache.isUsableByFile(bTs.path as ts.Path)); + assert.ok(exportMapCache.isEmpty()); + }); - it("does not store transient symbols through program updates", () => { - const { exportMapCache, project, session } = setup(); - // SIGINT, exported from /lib/foo/constants.d.ts, is a mapped type property, which will be a transient symbol. - // Transient symbols contain types, which retain the checkers they came from, so are not safe to cache. - // We clear symbols from the cache during updateGraph, leaving only the information about how to re-get them - // (see getters on `CachedSymbolExportInfo`). We can roughly test that this is working by ensuring that - // accessing a transient symbol with two different checkers results in different symbol identities, since - // transient symbols are recreated with every new checker. - const programBefore = project.getCurrentProgram()!; - let sigintPropBefore: readonly ts.SymbolExportInfo[] | undefined; - exportMapCache.search(bTs.path as ts.Path, /*preferCapitalized*/ false, ts.returnTrue, (info, symbolName) => { - if (symbolName === "SIGINT") - sigintPropBefore = info; - }); - assert.ok(sigintPropBefore); - assert.ok(sigintPropBefore![0].symbol.flags & ts.SymbolFlags.Transient); - const symbolIdBefore = ts.getSymbolId(sigintPropBefore![0].symbol); + it("does not store transient symbols through program updates", () => { + const { exportMapCache, project, session } = setup(); + // SIGINT, exported from /lib/foo/constants.d.ts, is a mapped type property, which will be a transient symbol. + // Transient symbols contain types, which retain the checkers they came from, so are not safe to cache. + // We clear symbols from the cache during updateGraph, leaving only the information about how to re-get them + // (see getters on `CachedSymbolExportInfo`). We can roughly test that this is working by ensuring that + // accessing a transient symbol with two different checkers results in different symbol identities, since + // transient symbols are recreated with every new checker. + const programBefore = project.getCurrentProgram()!; + let sigintPropBefore: readonly ts.SymbolExportInfo[] | undefined; + exportMapCache.search(bTs.path as ts.Path, /*preferCapitalized*/ false, ts.returnTrue, (info, symbolName) => { + if (symbolName === "SIGINT") + sigintPropBefore = info; + }); + assert.ok(sigintPropBefore); + assert.ok(sigintPropBefore![0].symbol.flags & ts.SymbolFlags.Transient); + const symbolIdBefore = ts.getSymbolId(sigintPropBefore![0].symbol); - // Update program without clearing cache - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: bTs.path, - textChanges: [{ - newText: " ", - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }] + // Update program without clearing cache + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: bTs.path, + textChanges: [{ + newText: " ", + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, }] - } - }); - project.getLanguageService(/*ensureSynchronized*/ true); - assert.notEqual(programBefore, project.getCurrentProgram()!); + }] + } + }); + project.getLanguageService(/*ensureSynchronized*/ true); + assert.notEqual(programBefore, project.getCurrentProgram()!); - // Get same info from cache again - let sigintPropAfter: readonly ts.SymbolExportInfo[] | undefined; - exportMapCache.search(bTs.path as ts.Path, /*preferCapitalized*/ false, ts.returnTrue, (info, symbolName) => { - if (symbolName === "SIGINT") - sigintPropAfter = info; - }); - assert.ok(sigintPropAfter); - assert.notEqual(symbolIdBefore, ts.getSymbolId(sigintPropAfter![0].symbol)); + // Get same info from cache again + let sigintPropAfter: readonly ts.SymbolExportInfo[] | undefined; + exportMapCache.search(bTs.path as ts.Path, /*preferCapitalized*/ false, ts.returnTrue, (info, symbolName) => { + if (symbolName === "SIGINT") + sigintPropAfter = info; }); + assert.ok(sigintPropAfter); + assert.notEqual(symbolIdBefore, ts.getSymbolId(sigintPropAfter![0].symbol)); }); +}); - function setup() { - const host = ts.projectSystem.createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts, exportEqualsMappedType]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs, bTs], session); - const projectService = session.getProjectService(); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - triggerCompletions(); - const checker = project.getLanguageService().getProgram()!.getTypeChecker(); - return { host, project, projectService, session, exportMapCache: project.getCachedExportInfoMap(), checker, triggerCompletions }; +function setup() { + const host = ts.projectSystem.createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts, exportEqualsMappedType]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs, bTs], session); + const projectService = session.getProjectService(); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + triggerCompletions(); + const checker = project.getLanguageService().getProgram()!.getTypeChecker(); + return { host, project, projectService, session, exportMapCache: project.getCachedExportInfoMap(), checker, triggerCompletions }; - function triggerCompletions() { - const requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; - ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - } + function triggerCompletions() { + const requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); } } +} diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 320444cf270f2..628bc50e03eae 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -1,926 +1,926 @@ 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 = ts.projectSystem.createServerHost([f1, config], { useCaseSensitiveFileNames: false }); - const service = ts.projectSystem.createProjectService(host); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - const upperCaseConfigFilePath = ts.combinePaths(ts.getDirectoryPath(config.path).toUpperCase(), ts.getBaseFileName(config.path)); - service.openExternalProject({ - projectFileName: "/a/b/project.csproj", - rootFiles: ts.projectSystem.toExternalFiles([f1.path, upperCaseConfigFilePath]), - options: {} - } as ts.projectSystem.protocol.ExternalProject); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project = service.configuredProjects.get(config.path)!; - if (lazyConfiguredProjectsFromExternalProject) { - assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded - ts.projectSystem.checkProjectActualFiles(project, ts.emptyArray); - } - else { - assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - ts.projectSystem.checkProjectActualFiles(project, [upperCaseConfigFilePath]); - } - - service.openClientFile(f1.path); - service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - - assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated +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 = ts.projectSystem.createServerHost([f1, config], { useCaseSensitiveFileNames: false }); + const service = ts.projectSystem.createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + const upperCaseConfigFilePath = ts.combinePaths(ts.getDirectoryPath(config.path).toUpperCase(), ts.getBaseFileName(config.path)); + service.openExternalProject({ + projectFileName: "/a/b/project.csproj", + rootFiles: ts.projectSystem.toExternalFiles([f1.path, upperCaseConfigFilePath]), + options: {} + } as ts.projectSystem.protocol.ExternalProject); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + if (lazyConfiguredProjectsFromExternalProject) { + assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + ts.projectSystem.checkProjectActualFiles(project, ts.emptyArray); + } + else { + assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded ts.projectSystem.checkProjectActualFiles(project, [upperCaseConfigFilePath]); - ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [f1.path]); } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); + service.openClientFile(f1.path); + service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); + assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated + ts.projectSystem.checkProjectActualFiles(project, [upperCaseConfigFilePath]); + ts.projectSystem.checkProjectActualFiles(service.inferredProjects[0], [f1.path]); + } + + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - it("load global plugins", () => { - const f1 = { - path: "/a/file1.ts", - content: "let x = [1, 2];" - }; - const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [ts.projectSystem.toExternalFile(f1.path)], options: {} }; - const host = ts.projectSystem.createServerHost([f1]); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = Harness.LanguageService.makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Warning, - file: sourceFile, - code: 9999, - length: 3, - messageText: `Plugin diagnostic`, - start: 0 - }); - return prev; - }; - return proxy; - } - }), - error: undefined - }; - }; - const session = ts.projectSystem.createSession(host, { globalPlugins: ["myplugin"] }); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1] } - } as ts.projectSystem.protocol.OpenExternalProjectsRequest); - - const projectService = session.getProjectService(); - ts.projectSystem.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 - } - } as ts.projectSystem.protocol.SemanticDiagnosticsSyncRequest); - - assert.isDefined(handlerResponse.response); - const response = handlerResponse.response as ts.projectSystem.protocol.Diagnostic[]; - assert.equal(response.length, 1); - assert.equal(response[0].text, "Plugin diagnostic"); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); }); + }); - 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: ts.projectSystem.File) => ({ projectFileName: f.path + ".csproj", rootFiles: [ts.projectSystem.toExternalFile(f.path)], options: {} }); - const p1 = makeProject(f1); - const p2 = makeProject(f2); - const p3 = makeProject(f3); - - const host = ts.projectSystem.createServerHost([f1, f2, f3]); - const session = ts.projectSystem.createSession(host); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p2] } - } as ts.projectSystem.protocol.OpenExternalProjectsRequest); - - const projectService = session.getProjectService(); - ts.projectSystem.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] } - } as ts.projectSystem.protocol.OpenExternalProjectsRequest); - ts.projectSystem.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: [] } - } as ts.projectSystem.protocol.OpenExternalProjectsRequest); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 0 }); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p2] } - } as ts.projectSystem.protocol.OpenExternalProjectsRequest); - assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + it("load global plugins", () => { + const f1 = { + path: "/a/file1.ts", + content: "let x = [1, 2];" + }; + const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [ts.projectSystem.toExternalFile(f1.path)], options: {} }; + const host = ts.projectSystem.createServerHost([f1]); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = Harness.LanguageService.makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Warning, + file: sourceFile, + code: 9999, + length: 3, + messageText: `Plugin diagnostic`, + start: 0 + }); + return prev; + }; + return proxy; + } + }), + error: undefined + }; + }; + const session = ts.projectSystem.createSession(host, { globalPlugins: ["myplugin"] }); + + session.executeCommand({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1] } + } as ts.projectSystem.protocol.OpenExternalProjectsRequest); + + const projectService = session.getProjectService(); + ts.projectSystem.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 + } + } as ts.projectSystem.protocol.SemanticDiagnosticsSyncRequest); + + assert.isDefined(handlerResponse.response); + const response = handlerResponse.response as ts.projectSystem.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: ts.projectSystem.File) => ({ projectFileName: f.path + ".csproj", rootFiles: [ts.projectSystem.toExternalFile(f.path)], options: {} }); + const p1 = makeProject(f1); + const p2 = makeProject(f2); + const p3 = makeProject(f3); + + const host = ts.projectSystem.createServerHost([f1, f2, f3]); + const session = ts.projectSystem.createSession(host); + + session.executeCommand({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p2] } + } as ts.projectSystem.protocol.OpenExternalProjectsRequest); + + const projectService = session.getProjectService(); + ts.projectSystem.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] } + } as ts.projectSystem.protocol.OpenExternalProjectsRequest); + ts.projectSystem.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: [] } + } as ts.projectSystem.protocol.OpenExternalProjectsRequest); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 0 }); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p2] } + } as ts.projectSystem.protocol.OpenExternalProjectsRequest); + 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 = ts.projectSystem.createServerHost([file1, file2]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ + rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), + options: {}, + projectFileName: externalProjectName }); - 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 = ts.projectSystem.createServerHost([file1, file2]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), - options: {}, - projectFileName: externalProjectName - }); + ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + // close client file - external project should still exists + projectService.closeClientFile(file1.path); + ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - // close client file - external project should still exists - projectService.closeClientFile(file1.path); - ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + projectService.closeExternalProject(externalProjectName); + ts.projectSystem.checkNumberOfExternalProjects(projectService, 0); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + }); - projectService.closeExternalProject(externalProjectName); - ts.projectSystem.checkNumberOfExternalProjects(projectService, 0); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + it("external project for dynamic file", () => { + const externalProjectName = "^ScriptDocument1 file1.ts"; + const externalFiles = ts.projectSystem.toExternalFiles(["^ScriptDocument1 file1.ts"]); + const host = ts.projectSystem.createServerHost([]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ + rootFiles: externalFiles, + options: {}, + projectFileName: externalProjectName }); - it("external project for dynamic file", () => { - const externalProjectName = "^ScriptDocument1 file1.ts"; - const externalFiles = ts.projectSystem.toExternalFiles(["^ScriptDocument1 file1.ts"]); - const host = ts.projectSystem.createServerHost([]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ - rootFiles: externalFiles, - options: {}, - projectFileName: externalProjectName - }); + ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); + ts.projectSystem.verifyDynamic(projectService, "/^scriptdocument1 file1.ts"); - ts.projectSystem.checkNumberOfExternalProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 0); - ts.projectSystem.verifyDynamic(projectService, "/^scriptdocument1 file1.ts"); + externalFiles[0].content = "let x =1;"; + projectService.applyChangesInOpenFiles(ts.arrayIterator(externalFiles)); + }); - externalFiles[0].content = "let x =1;"; - projectService.applyChangesInOpenFiles(ts.arrayIterator(externalFiles)); - }); + it("when file name starts with ^", () => { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file.ts`, + content: "const x = 10;" + }; + const app: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/^app.ts`, + content: "const y = 10;" + }; + const host = ts.projectSystem.createServerHost([file, app, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host); + service.openExternalProjects([{ + projectFileName: `${ts.tscWatch.projectRoot}/myproject.njsproj`, + rootFiles: [ + ts.projectSystem.toExternalFile(file.path), + ts.projectSystem.toExternalFile(app.path) + ], + options: { }, + }]); + }); - it("when file name starts with ^", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file.ts`, - content: "const x = 10;" - }; - const app: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/^app.ts`, - content: "const y = 10;" - }; - const host = ts.projectSystem.createServerHost([file, app, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host); - service.openExternalProjects([{ - projectFileName: `${ts.tscWatch.projectRoot}/myproject.njsproj`, - rootFiles: [ - ts.projectSystem.toExternalFile(file.path), - ts.projectSystem.toExternalFile(app.path) - ], - 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 = ts.projectSystem.createServerHost([file1, file2, file3, config1, config2]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ + rootFiles: ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, file3, config1, config2]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([config1.path, config2.path, file3.path]), - options: {}, - projectFileName: externalProjectName - }); + ts.projectSystem.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); + ts.projectSystem.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); + ts.projectSystem.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 + ts.projectSystem.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); + ts.projectSystem.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); + ts.projectSystem.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); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config1.path)); + assert.isDefined(projectService.configuredProjects.get(config2.path)); + }); - ts.projectSystem.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); - ts.projectSystem.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); - ts.projectSystem.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 - ts.projectSystem.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); - ts.projectSystem.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); - ts.projectSystem.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); - ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.openExternalProject({ + rootFiles: ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, configFile]); - const projectService = ts.projectSystem.createProjectService(host); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + projectService.closeClientFile(file1.path); + // configured project is alive since it is opened as part of external project + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); + projectService.closeExternalProject(externalProjectName); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0 }); + }); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + 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 = ts.projectSystem.createServerHost([file1, file2, ts.projectSystem.libFile, configFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + + projectService.openExternalProject({ + rootFiles: ts.projectSystem.toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName + }); - projectService.closeClientFile(file1.path); - // configured project is alive since it is opened as part of external project - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - projectService.closeExternalProject(externalProjectName); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0 }); - }); + projectService.closeExternalProject(externalProjectName); + // configured project is alive since file is still open + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - 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 = ts.projectSystem.createServerHost([file1, file2, ts.projectSystem.libFile, configFile]); - const projectService = ts.projectSystem.createProjectService(host); + projectService.closeClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path); + projectService.openClientFile(file2.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + }); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); + 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 = ts.projectSystem.createServerHost([file1, file2]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path]) }); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + }); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + 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 = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ts.ModuleResolutionKind.NodeJs }, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ts.ModuleResolutionKind.Classic }, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + }); - projectService.closeExternalProject(externalProjectName); - // configured project is alive since file is still open - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + 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 = ts.projectSystem.createServerHost([f1, f2]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const service = ts.projectSystem.createProjectService(host); + const projectFileName = "/a/proj.csproj"; + + service.openExternalProject({ + projectFileName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); - projectService.closeClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + service.openExternalProject({ + projectFileName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); - projectService.openClientFile(file2.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + service.openExternalProject({ + projectFileName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), + options: {} }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + }); - it("can correctly update external project when set of root files has changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" + 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 file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" + const configFile = { + path: "/user/someuser/project/tsconfig.json", + content: "{}" }; - const host = ts.projectSystem.createServerHost([file1, file2]); + const projectFileName = "/user/someuser/project/WebApplication6.csproj"; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, site, configFile]); const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path]) }); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - }); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - 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 externalProject: ts.projectSystem.protocol.ExternalProject = { + projectFileName, + rootFiles: [ts.projectSystem.toExternalFile(site.path), ts.projectSystem.toExternalFile(configFile.path)], + options: { allowJs: false }, + typeAcquisition: { include: [] } }; - const host = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ts.ModuleResolutionKind.NodeJs }, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ts.ModuleResolutionKind.Classic }, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) }); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - ts.projectSystem.checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + projectService.openExternalProjects([externalProject]); + + let knownProjects = projectService.synchronizeProjectList([]); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); + const configProject = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? ts.emptyArray : // Since no files opened from this project, its not loaded + [configFile.path]); + + host.deleteFile(configFile.path); + + knownProjects = projectService.synchronizeProjectList(ts.map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); + + externalProject.rootFiles.length = 1; + projectService.openExternalProjects([externalProject]); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [site.path, ts.projectSystem.libFile.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); }); + }); - it("language service disabled state is updated in external projects", () => { + 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 = ts.projectSystem.createServerHost([f1, f2]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - const service = ts.projectSystem.createProjectService(host); - const projectFileName = "/a/proj.csproj"; + const projectService = ts.projectSystem.createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - service.openExternalProject({ - projectFileName, + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), options: {} }); - service.checkNumberOfProjects({ externalProjects: 1 }); - assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - service.openExternalProject({ - projectFileName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path]), + // rename lib.ts to tsconfig.json + host.renameFile(f2.path, tsconfig.path); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, tsconfig.path]), options: {} }); - service.checkNumberOfProjects({ externalProjects: 1 }); - assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + if (lazyConfiguredProjectsFromExternalProject) { + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); - service.openExternalProject({ - projectFileName, + // rename tsconfig.json back to lib.ts + host.renameFile(tsconfig.path, f2.path); + projectService.openExternalProject({ + projectFileName: projectName, rootFiles: ts.projectSystem.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 configFile = { - path: "/user/someuser/project/tsconfig.json", - content: "{}" - }; - const projectFileName = "/user/someuser/project/WebApplication6.csproj"; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, site, configFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - const externalProject: ts.projectSystem.protocol.ExternalProject = { - projectFileName, - rootFiles: [ts.projectSystem.toExternalFile(site.path), ts.projectSystem.toExternalFile(configFile.path)], - options: { allowJs: false }, - typeAcquisition: { include: [] } - }; - - projectService.openExternalProjects([externalProject]); - - let knownProjects = projectService.synchronizeProjectList([]); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); - const configProject = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? ts.emptyArray : // Since no files opened from this project, its not loaded - [configFile.path]); - - host.deleteFile(configFile.path); - - knownProjects = projectService.synchronizeProjectList(ts.map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); - - externalProject.rootFiles.length = 1; - projectService.openExternalProjects([externalProject]); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [site.path, ts.projectSystem.libFile.path]); - } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyDeletingConfigFile(/*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 = ts.projectSystem.createServerHost([f1, f2]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), - options: {} - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - - // rename lib.ts to tsconfig.json - host.renameFile(f2.path, tsconfig.path); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, tsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - if (lazyConfiguredProjectsFromExternalProject) { - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); - - // rename tsconfig.json back to lib.ts - host.renameFile(tsconfig.path, f2.path); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - - 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 = ts.projectSystem.createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), ts.emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - - // remove one config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, dTsconfig.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); - - // remove second config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // open two config files - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), ts.emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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("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" + describe("correctly handling add/remove tsconfig - 2", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" + const cLib = { + path: "/a/b/c/lib.ts", + content: "" }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" + const cTsconfig = { + path: "/a/b/c/tsconfig.json", + content: "{}" }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) + const dLib = { + path: "/a/b/d/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 dTsconfig = { + path: "/a/b/d/tsconfig.json", + content: "{}" }; - const host = ts.projectSystem.createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const host = ts.projectSystem.createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(app.path); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - host.writeFile(config2.path, config2.content); - host.checkTimeoutQueueLengthAndRun(2); + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path]), + options: {} + }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); - }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.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 = ts.projectSystem.createServerHost([f, config]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - assert.isTrue(project.hasOpenRef()); // f + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), ts.emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - 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()); + // remove one config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, dTsconfig.path]), + options: {} + }); - 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()); - }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); - 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 = ts.projectSystem.createServerHost([f1, config]); - const service = ts.projectSystem.createProjectService(host); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); - service.openExternalProject({ - projectFileName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, config.path]), + // remove second config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path]), options: {} - } as ts.projectSystem.protocol.ExternalProject); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project = service.configuredProjects.get(config.path)!; - assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded - ts.projectSystem.checkProjectActualFiles(project, ts.emptyArray); - - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - ts.projectSystem.checkProjectActualFiles(project, [config.path, f1.path]); + }); - service.closeExternalProject(projectFileName); - service.checkNumberOfProjects({}); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - service.openExternalProject({ - projectFileName, - rootFiles: ts.projectSystem.toExternalFiles([f1.path, config.path]), + // open two config files + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), options: {} - } as ts.projectSystem.protocol.ExternalProject); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project2 = service.configuredProjects.get(config.path)!; - assert.equal(project2.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - ts.projectSystem.checkProjectActualFiles(project2, [config.path, f1.path]); + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), ts.emptyArray); // Configured project created but not loaded till actually needed + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 1), ts.emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { - const projectFileName = `${ts.tscWatch.projectRoot}/WebApplication36.csproj`; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const files = [ts.projectSystem.libFile, tsconfig]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.createProjectService(host); + 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 = ts.projectSystem.createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(app.path); - // Create external project - service.openExternalProjects([{ - projectFileName, - rootFiles: [{ fileName: tsconfig.path }], - options: { allowJs: false } - }]); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; - ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); - - // write js file, open external project and open it for edit - const jsFilePath = `${ts.tscWatch.projectRoot}/javascript.js`; - host.writeFile(jsFilePath, ""); - service.openExternalProjects([{ - projectFileName, - rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], - options: { allowJs: false } - }]); - service.applyChangesInOpenFiles(ts.singleIterator({ fileName: jsFilePath, scriptKind: ts.ScriptKind.JS, content: "" })); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); - const inferredProject = service.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject, [ts.projectSystem.libFile.path, jsFilePath]); - - // write jsconfig file - const jsConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/jsconfig.json`, - content: "{}" - }; - // Dont invoke file creation watchers as the repro suggests - host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); - // Open external project - service.openExternalProjects([{ - projectFileName, - rootFiles: [{ fileName: jsConfig.path }, { fileName: tsconfig.path }, { fileName: jsFilePath }], - options: { allowJs: false } - }]); - ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); - assert.isTrue(inferredProject.isOrphan()); - const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; - ts.projectSystem.checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, ts.projectSystem.libFile.path]); - }); + host.writeFile(config2.path, config2.content); + host.checkTimeoutQueueLengthAndRun(2); - it("does not crash if external file does not exist", () => { - const f1 = { - path: "/a/file1.ts", - content: "let x = [1, 2];", - }; - const p1 = { - projectFileName: "/a/proj1.csproj", - rootFiles: [ts.projectSystem.toExternalFile(f1.path)], - options: {}, - }; + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); + }); - const host = ts.projectSystem.createServerHost([f1]); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - return Harness.LanguageService.makeDefaultProxy(info); - }, - getExternalFiles() { - return ["/does/not/exist"]; - }, - }), - error: undefined, - }; - }; - const session = ts.projectSystem.createSession(host, { - globalPlugins: ["myplugin"], - }); - const projectService = session.getProjectService(); - // When the external project is opened, the graph will be updated, - // and in the process getExternalFiles() above will be called. - // Since the external file does not exist, there will not be a script - // info for it. If tsserver does not handle this case, the following - // method call will crash. - projectService.openExternalProject(p1); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + 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 = ts.projectSystem.createServerHost([f, config]); + const projectService = ts.projectSystem.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 = ts.projectSystem.createServerHost([f1, config]); + const service = ts.projectSystem.createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); + service.openExternalProject({ + projectFileName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, config.path]), + options: {} + } as ts.projectSystem.protocol.ExternalProject); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + ts.projectSystem.checkProjectActualFiles(project, ts.emptyArray); + + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + assert.equal(project.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + ts.projectSystem.checkProjectActualFiles(project, [config.path, f1.path]); + + service.closeExternalProject(projectFileName); + service.checkNumberOfProjects({}); + + service.openExternalProject({ + projectFileName, + rootFiles: ts.projectSystem.toExternalFiles([f1.path, config.path]), + options: {} + } as ts.projectSystem.protocol.ExternalProject); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project2 = service.configuredProjects.get(config.path)!; + assert.equal(project2.pendingReload, ts.ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + ts.projectSystem.checkProjectActualFiles(project2, [config.path, f1.path]); + }); + + it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { + const projectFileName = `${ts.tscWatch.projectRoot}/WebApplication36.csproj`; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [ts.projectSystem.libFile, tsconfig]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.createProjectService(host); + + // Create external project + service.openExternalProjects([{ + projectFileName, + rootFiles: [{ fileName: tsconfig.path }], + options: { allowJs: false } + }]); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); + const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; + ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); + + // write js file, open external project and open it for edit + const jsFilePath = `${ts.tscWatch.projectRoot}/javascript.js`; + host.writeFile(jsFilePath, ""); + service.openExternalProjects([{ + projectFileName, + rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], + options: { allowJs: false } + }]); + service.applyChangesInOpenFiles(ts.singleIterator({ fileName: jsFilePath, scriptKind: ts.ScriptKind.JS, content: "" })); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); + const inferredProject = service.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject, [ts.projectSystem.libFile.path, jsFilePath]); + + // write jsconfig file + const jsConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.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 } + }]); + ts.projectSystem.checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(configProject, [tsconfig.path]); + assert.isTrue(inferredProject.isOrphan()); + const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; + ts.projectSystem.checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, ts.projectSystem.libFile.path]); + }); + + it("does not crash if external file does not exist", () => { + const f1 = { + path: "/a/file1.ts", + content: "let x = [1, 2];", + }; + const p1 = { + projectFileName: "/a/proj1.csproj", + rootFiles: [ts.projectSystem.toExternalFile(f1.path)], + options: {}, + }; + + const host = ts.projectSystem.createServerHost([f1]); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + return Harness.LanguageService.makeDefaultProxy(info); + }, + getExternalFiles() { + return ["/does/not/exist"]; + }, + }), + error: undefined, + }; + }; + const session = ts.projectSystem.createSession(host, { + globalPlugins: ["myplugin"], }); + const projectService = session.getProjectService(); + // When the external project is opened, the graph will be updated, + // and in the process getExternalFiles() above will be called. + // Since the external file does not exist, there will not be a script + // info for it. If tsserver does not handle this case, the following + // method call will crash. + projectService.openExternalProject(p1); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts index 30851edb76421..7e2d40bc75c7f 100644 --- a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts +++ b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts @@ -1,127 +1,127 @@ 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: ts.projectSystem.File = { - path: `${rootPath}/index.ts`, - content: 'import {x} from "file2";', - }; - const file2: ts.projectSystem.File = { - path: `${rootPath}/file2.js`, - content: "", - }; - const file2Dts: ts.projectSystem.File = { - path: `${rootPath}/types/file2/index.d.ts`, - content: "export declare const x: string;", - }; - const tsconfigAll: ts.projectSystem.File = { - path: `${rootPath}/tsconfig.all.json`, - content: JSON.stringify({ - compilerOptions: { - baseUrl: ".", - paths: { file2: ["./file2.js"] }, - typeRoots: ["./types"], - forceConsistentCasingInFileNames: true, - }, - }), - }; - const tsconfig: ts.projectSystem.File = { - path: `${rootPath}/tsconfig.json`, - content: JSON.stringify({ extends: "./tsconfig.all.json" }), - }; +describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => { + it("works when extends is specified with a case insensitive file system", () => { + const rootPath = "/Users/username/dev/project"; + const file1: ts.projectSystem.File = { + path: `${rootPath}/index.ts`, + content: 'import {x} from "file2";', + }; + const file2: ts.projectSystem.File = { + path: `${rootPath}/file2.js`, + content: "", + }; + const file2Dts: ts.projectSystem.File = { + path: `${rootPath}/types/file2/index.d.ts`, + content: "export declare const x: string;", + }; + const tsconfigAll: ts.projectSystem.File = { + path: `${rootPath}/tsconfig.all.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { file2: ["./file2.js"] }, + typeRoots: ["./types"], + forceConsistentCasingInFileNames: true, + }, + }), + }; + const tsconfig: ts.projectSystem.File = { + path: `${rootPath}/tsconfig.json`, + content: JSON.stringify({ extends: "./tsconfig.all.json" }), + }; - const host = ts.projectSystem.createServerHost([file1, file2, file2Dts, ts.projectSystem.libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1], session); - const projectService = session.getProjectService(); + const host = ts.projectSystem.createServerHost([file1, file2, file2Dts, ts.projectSystem.libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - }); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); - it("works when renaming file with different casing", () => { - const loggerFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/Logger.ts`, - content: `export class logger { }` - }; - const anotherFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/another.ts`, - content: `import { logger } from "./Logger"; new logger();` - }; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; + it("works when renaming file with different casing", () => { + const loggerFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/Logger.ts`, + content: `export class logger { }` + }; + const anotherFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/another.ts`, + content: `import { logger } from "./Logger"; new logger();` + }; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; - const host = ts.projectSystem.createServerHost([loggerFile, anotherFile, tsconfig, ts.projectSystem.libFile, tsconfig]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([{ file: loggerFile, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [loggerFile] }); + const host = ts.projectSystem.createServerHost([loggerFile, anotherFile, tsconfig, ts.projectSystem.libFile, tsconfig]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([{ file: loggerFile, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [loggerFile] }); - const newLoggerPath = loggerFile.path.toLowerCase(); - host.renameFile(loggerFile.path, newLoggerPath); - ts.projectSystem.closeFilesForSession([loggerFile], session); - ts.projectSystem.openFilesForSession([{ file: newLoggerPath, content: loggerFile.content, projectRootPath: ts.tscWatch.projectRoot }], session); + const newLoggerPath = loggerFile.path.toLowerCase(); + host.renameFile(loggerFile.path, newLoggerPath); + ts.projectSystem.closeFilesForSession([loggerFile], session); + ts.projectSystem.openFilesForSession([{ file: newLoggerPath, content: loggerFile.content, projectRootPath: ts.tscWatch.projectRoot }], session); - // Apply edits for rename - ts.projectSystem.openFilesForSession([{ file: anotherFile, projectRootPath: ts.tscWatch.projectRoot }], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: anotherFile.path, - textChanges: [{ - newText: "./logger", - ...ts.projectSystem.protocolTextSpanFromSubstring(anotherFile.content, "./Logger") - }] + // Apply edits for rename + ts.projectSystem.openFilesForSession([{ file: anotherFile, projectRootPath: ts.tscWatch.projectRoot }], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: anotherFile.path, + textChanges: [{ + newText: "./logger", + ...ts.projectSystem.protocolTextSpanFromSubstring(anotherFile.content, "./Logger") }] - } - }); - - // Check errors in both files - ts.projectSystem.verifyGetErrRequest({ session, host, files: [newLoggerPath, anotherFile] }); - ts.projectSystem.baselineTsserverLogs("forceConsistentCasingInFileNames", "works when renaming file with different casing", session); + }] + } }); - it("when changing module name with different casing", () => { - const loggerFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/Logger.ts`, - content: `export class logger { }` - }; - const anotherFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/another.ts`, - content: `import { logger } from "./Logger"; new logger();` - }; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; + // Check errors in both files + ts.projectSystem.verifyGetErrRequest({ session, host, files: [newLoggerPath, anotherFile] }); + ts.projectSystem.baselineTsserverLogs("forceConsistentCasingInFileNames", "works when renaming file with different casing", session); + }); - const host = ts.projectSystem.createServerHost([loggerFile, anotherFile, tsconfig, ts.projectSystem.libFile, tsconfig]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([{ file: anotherFile, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [anotherFile] }); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: anotherFile.path, - textChanges: [{ - newText: "./logger", - ...ts.projectSystem.protocolTextSpanFromSubstring(anotherFile.content, "./Logger") - }] - }] - } - }); + it("when changing module name with different casing", () => { + const loggerFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/Logger.ts`, + content: `export class logger { }` + }; + const anotherFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/another.ts`, + content: `import { logger } from "./Logger"; new logger();` + }; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; - // Check errors in both files - ts.projectSystem.verifyGetErrRequest({ host, session, files: [anotherFile] }); - ts.projectSystem.baselineTsserverLogs("forceConsistentCasingInFileNames", "when changing module name with different casing", session); + const host = ts.projectSystem.createServerHost([loggerFile, anotherFile, tsconfig, ts.projectSystem.libFile, tsconfig]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([{ file: anotherFile, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [anotherFile] }); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: anotherFile.path, + textChanges: [{ + newText: "./logger", + ...ts.projectSystem.protocolTextSpanFromSubstring(anotherFile.content, "./Logger") + }] + }] + } }); + + // Check errors in both files + ts.projectSystem.verifyGetErrRequest({ host, session, files: [anotherFile] }); + ts.projectSystem.baselineTsserverLogs("forceConsistentCasingInFileNames", "when changing module name with different casing", session); }); +}); } diff --git a/src/testRunner/unittests/tsserver/formatSettings.ts b/src/testRunner/unittests/tsserver/formatSettings.ts index cb49a1da6b1a6..4b5a5bfb28490 100644 --- a/src/testRunner/unittests/tsserver/formatSettings.ts +++ b/src/testRunner/unittests/tsserver/formatSettings.ts @@ -1,39 +1,39 @@ 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 = ts.projectSystem.createServerHost([f1]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(f1.path); +describe("unittests:: tsserver:: format settings", () => { + it("can be set globally", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x;" + }; + const host = ts.projectSystem.createServerHost([f1]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(f1.path); - const defaultSettings = projectService.getFormatCodeOptions(f1.path as ts.server.NormalizedPath); + const defaultSettings = projectService.getFormatCodeOptions(f1.path as ts.server.NormalizedPath); - // set global settings - const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; - projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); + // 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(ts.server.toNormalizedPath(f1.path)); - assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); + // get format options for file - should be equal to new global settings + const s1 = projectService.getFormatCodeOptions(ts.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 }); + // 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(ts.server.toNormalizedPath(f1.path)); - assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); + // get format options for file - should be equal to new per-file settings + const s2 = projectService.getFormatCodeOptions(ts.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 }); + // 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(ts.server.toNormalizedPath(f1.path)); - assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); - }); + // get format options for file - should be equal to new per-file settings + const s3 = projectService.getFormatCodeOptions(ts.server.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 d356bef769d07..0a22e457d7e5c 100644 --- a/src/testRunner/unittests/tsserver/getApplicableRefactors.ts +++ b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts @@ -1,11 +1,11 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: getApplicableRefactors", () => { - it("works when taking position", () => { - const aTs: ts.projectSystem.File = { path: "/a.ts", content: "" }; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs])); - ts.projectSystem.openFilesForSession([aTs], session); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 }); - assert.deepEqual(response, []); - }); +describe("unittests:: tsserver:: getApplicableRefactors", () => { + it("works when taking position", () => { + const aTs: ts.projectSystem.File = { path: "/a.ts", content: "" }; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([aTs])); + ts.projectSystem.openFilesForSession([aTs], session); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.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 ed15c7339c252..750da56314e9a 100644 --- a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts +++ b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts @@ -1,101 +1,101 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: getEditsForFileRename", () => { - it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { - const userTs: ts.projectSystem.File = { - path: "/user.ts", - content: 'import { x } from "./old";', - }; - const newTs: ts.projectSystem.File = { - path: "/new.ts", - content: "export const x = 0;", - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}", - }; +describe("unittests:: tsserver:: getEditsForFileRename", () => { + it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { + const userTs: ts.projectSystem.File = { + path: "/user.ts", + content: 'import { x } from "./old";', + }; + const newTs: ts.projectSystem.File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}", + }; - const host = ts.projectSystem.createServerHost([userTs, newTs, tsconfig]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(userTs.path); - const project = projectService.configuredProjects.get(tsconfig.path)!; + const host = ts.projectSystem.createServerHost([userTs, newTs, tsconfig]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(userTs.path); + const project = projectService.configuredProjects.get(tsconfig.path)!; - ts.Debug.assert(!!project.resolveModuleNames); - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", ts.testFormatSettings, ts.emptyOptions); - assert.deepEqual(edits, [{ - fileName: "/user.ts", - textChanges: [{ - span: ts.projectSystem.textSpanFromSubstring(userTs.content, "./old"), - newText: "./new", - }], - }]); - }); + ts.Debug.assert(!!project.resolveModuleNames); + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", ts.testFormatSettings, ts.emptyOptions); + assert.deepEqual(edits, [{ + fileName: "/user.ts", + textChanges: [{ + span: ts.projectSystem.textSpanFromSubstring(userTs.content, "./old"), + newText: "./new", + }], + }]); + }); - it("works with multiple projects", () => { - const aUserTs: ts.projectSystem.File = { - path: "/a/user.ts", - content: 'import { x } from "./old";', - }; - const aOldTs: ts.projectSystem.File = { - path: "/a/old.ts", - content: "export const x = 0;", - }; - const aTsconfig: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), - }; - const bUserTs: ts.projectSystem.File = { - path: "/b/user.ts", - content: 'import { x } from "../a/old";', - }; - const bTsconfig: ts.projectSystem.File = { - path: "/b/tsconfig.json", - content: "{}", - }; + it("works with multiple projects", () => { + const aUserTs: ts.projectSystem.File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: ts.projectSystem.File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), + }; + const bUserTs: ts.projectSystem.File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: ts.projectSystem.File = { + path: "/b/tsconfig.json", + content: "{}", + }; - const host = ts.projectSystem.createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aUserTs, bUserTs], session); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { - oldFilePath: aOldTs.path, - newFilePath: "/a/new.ts", - }); - assert.deepEqual(response, [ - { - fileName: aTsconfig.path, - textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], - }, - { - fileName: aUserTs.path, - textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], - }, - { - fileName: bUserTs.path, - textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], - }, - ]); + const host = ts.projectSystem.createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aUserTs, bUserTs], session); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { + oldFilePath: aOldTs.path, + newFilePath: "/a/new.ts", }); + assert.deepEqual(response, [ + { + fileName: aTsconfig.path, + textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], + }, + { + fileName: aUserTs.path, + textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); - it("works with file moved to inferred project", () => { - const aTs: ts.projectSystem.File = { path: "/a.ts", content: 'import {} from "./b";' }; - const cTs: ts.projectSystem.File = { path: "/c.ts", content: "export {};" }; - const tsconfig: ts.projectSystem.File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; - const host = ts.projectSystem.createServerHost([aTs, cTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs, cTs], session); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { - oldFilePath: "/b.ts", - newFilePath: cTs.path, - }); - assert.deepEqual(response, [ - { - fileName: "/tsconfig.json", - textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], - }, - { - fileName: "/a.ts", - textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], - }, - ]); + it("works with file moved to inferred project", () => { + const aTs: ts.projectSystem.File = { path: "/a.ts", content: 'import {} from "./b";' }; + const cTs: ts.projectSystem.File = { path: "/c.ts", content: "export {};" }; + const tsconfig: ts.projectSystem.File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; + const host = ts.projectSystem.createServerHost([aTs, cTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs, cTs], session); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.CommandNames.GetEditsForFileRename, { + oldFilePath: "/b.ts", + newFilePath: cTs.path, }); + assert.deepEqual(response, [ + { + fileName: "/tsconfig.json", + textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], + }, + { + fileName: "/a.ts", + textChanges: [{ ...ts.projectSystem.protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], + }, + ]); }); +}); } diff --git a/src/testRunner/unittests/tsserver/getExportReferences.ts b/src/testRunner/unittests/tsserver/getExportReferences.ts index 264fceb9d1462..ecde346699a28 100644 --- a/src/testRunner/unittests/tsserver/getExportReferences.ts +++ b/src/testRunner/unittests/tsserver/getExportReferences.ts @@ -1,167 +1,167 @@ 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: ts.projectSystem.File = { - path: "/main.ts", - content: 'import { value, valueA, valueB, valueC, renamedD, valueE, valueF } from "./mod";', - }; - const modTs: ts.projectSystem.File = { - path: "/mod.ts", - content: `${exportVariable} +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: ts.projectSystem.File = { + path: "/main.ts", + content: 'import { value, valueA, valueB, valueC, renamedD, valueE, valueF } from "./mod";', + }; + const modTs: ts.projectSystem.File = { + path: "/mod.ts", + content: `${exportVariable} ${exportArrayDestructured} ${exportObjectDestructured} ${exportNestedObject} `, - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}", - }; + }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}", + }; + + function makeSampleSession() { + const host = ts.projectSystem.createServerHost([mainTs, modTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([mainTs, modTs], session); + return session; + } + + const referenceMainTs = (mainTs: ts.projectSystem.File, text: string): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ + file: mainTs, + isDefinition: false, + isWriteAccess: true, + lineText: mainTs.content, + contextText: mainTs.content, + text, + }); - function makeSampleSession() { - const host = ts.projectSystem.createServerHost([mainTs, modTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([mainTs, modTs], session); - return session; - } - - const referenceMainTs = (mainTs: ts.projectSystem.File, text: string): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ - file: mainTs, - isDefinition: false, - isWriteAccess: true, - lineText: mainTs.content, - contextText: mainTs.content, - text, - }); - - const referenceModTs = (texts: { - text: string; - lineText: string; - contextText?: string; - }, override: Partial = {}): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ - file: modTs, - isDefinition: true, - ...texts, - ...override, - }); - - it("should get const variable declaration references", () => { - const session = makeSampleSession(); - - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "value")); - - const expectResponse = { - refs: [ - referenceModTs({ text: "value", lineText: exportVariable, contextText: exportVariable }), - referenceMainTs(mainTs, "value"), - ], - symbolDisplayString: "const value: 0", - symbolName: "value", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "value").offset, - }; - - assert.deepEqual(response, expectResponse); + const referenceModTs = (texts: { + text: string; + lineText: string; + contextText?: string; + }, override: Partial = {}): ts.projectSystem.protocol.ReferencesResponseItem => ts.projectSystem.makeReferenceItem({ + file: modTs, + isDefinition: true, + ...texts, + ...override, }); - it("should get array destructuring declaration references", () => { - const session = makeSampleSession(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "valueA")); + it("should get const variable declaration references", () => { + const session = makeSampleSession(); - const expectResponse = { - refs: [ - referenceModTs({ - text: "valueA", - lineText: exportArrayDestructured, - contextText: exportArrayDestructured, - }), - referenceMainTs(mainTs, "valueA"), - ], - symbolDisplayString: "const valueA: number", - symbolName: "valueA", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueA").offset, - }; - - assert.deepEqual(response, expectResponse); - }); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "value")); - it("should get object destructuring declaration references", () => { - const session = makeSampleSession(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.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 }, - isDefinition: false, - isWriteAccess: true, - }), - ], - symbolDisplayString: "const valueC: number", - symbolName: "valueC", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueC").offset, - }; - - assert.deepEqual(response, expectResponse); - }); + const expectResponse = { + refs: [ + referenceModTs({ text: "value", lineText: exportVariable, contextText: exportVariable }), + referenceMainTs(mainTs, "value"), + ], + symbolDisplayString: "const value: 0", + symbolName: "value", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "value").offset, + }; + + assert.deepEqual(response, expectResponse); + }); - it("should get object declaration references that renames destructured property", () => { - const session = makeSampleSession(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "renamedD")); + it("should get array destructuring declaration references", () => { + const session = makeSampleSession(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "valueA")); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueA", + lineText: exportArrayDestructured, + contextText: exportArrayDestructured, + }), + referenceMainTs(mainTs, "valueA"), + ], + symbolDisplayString: "const valueA: number", + symbolName: "valueA", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueA").offset, + }; - const expectResponse = { - refs: [ - referenceModTs({ - text: "renamedD", - lineText: exportObjectDestructured, - contextText: exportObjectDestructured, + assert.deepEqual(response, expectResponse); + }); + + it("should get object destructuring declaration references", () => { + const session = makeSampleSession(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.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 }, + isDefinition: false, + isWriteAccess: true, }), - referenceMainTs(mainTs, "renamedD"), - ], - symbolDisplayString: "const renamedD: number", - symbolName: "renamedD", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "renamedD").offset, - }; - - assert.deepEqual(response, expectResponse); - }); + ], + symbolDisplayString: "const valueC: number", + symbolName: "valueC", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueC").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + + it("should get object declaration references that renames destructured property", () => { + const session = makeSampleSession(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "renamedD")); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "renamedD", + lineText: exportObjectDestructured, + contextText: exportObjectDestructured, + }), + referenceMainTs(mainTs, "renamedD"), + ], + symbolDisplayString: "const renamedD: number", + symbolName: "renamedD", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "renamedD").offset, + }; - it("should get nested object declaration references", () => { - const session = makeSampleSession(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "valueF")); + assert.deepEqual(response, expectResponse); + }); - const expectResponse = { - refs: [ - referenceModTs({ + it("should get nested object declaration references", () => { + const session = makeSampleSession(); + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.References, ts.projectSystem.protocolFileLocationFromSubstring(modTs, "valueF")); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueF", + lineText: exportNestedObject, + contextText: exportNestedObject, + }), + referenceMainTs(mainTs, "valueF"), + referenceModTs({ text: "valueF", lineText: exportNestedObject, - contextText: exportNestedObject, - }), - referenceMainTs(mainTs, "valueF"), - referenceModTs({ - text: "valueF", - lineText: exportNestedObject, - contextText: "valueF: 1", - }, { - options: { index: 1 }, - isDefinition: false, - isWriteAccess: true, - }), - ], - symbolDisplayString: "const valueF: number", - symbolName: "valueF", - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueF").offset, - }; + contextText: "valueF: 1", + }, { + options: { index: 1 }, + isDefinition: false, + isWriteAccess: true, + }), + ], + symbolDisplayString: "const valueF: number", + symbolName: "valueF", + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(modTs.content, "valueF").offset, + }; - assert.deepEqual(response, expectResponse); - }); + assert.deepEqual(response, expectResponse); }); +}); } diff --git a/src/testRunner/unittests/tsserver/getFileReferences.ts b/src/testRunner/unittests/tsserver/getFileReferences.ts index a833f0ac979a3..939a885dbd94a 100644 --- a/src/testRunner/unittests/tsserver/getFileReferences.ts +++ b/src/testRunner/unittests/tsserver/getFileReferences.ts @@ -1,53 +1,53 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: getFileReferences", () => { - const importA = `import "./a";`; - const importCurlyFromA = `import {} from "./a";`; - const importAFromA = `import { a } from "/project/a";`; - const typeofImportA = `type T = typeof import("./a").a;`; +describe("unittests:: tsserver:: getFileReferences", () => { + const importA = `import "./a";`; + const importCurlyFromA = `import {} from "./a";`; + const importAFromA = `import { a } from "/project/a";`; + const typeofImportA = `type T = typeof import("./a").a;`; - const aTs: ts.projectSystem.File = { - path: "/project/a.ts", - content: "export const a = {};", - }; - const bTs: ts.projectSystem.File = { - path: "/project/b.ts", - content: importA, - }; - const cTs: ts.projectSystem.File = { - path: "/project/c.ts", - content: importCurlyFromA - }; - const dTs: ts.projectSystem.File = { - path: "/project/d.ts", - content: [importAFromA, typeofImportA].join("\n") - }; - const tsconfig: ts.projectSystem.File = { - path: "/project/tsconfig.json", - content: "{}", - }; + const aTs: ts.projectSystem.File = { + path: "/project/a.ts", + content: "export const a = {};", + }; + const bTs: ts.projectSystem.File = { + path: "/project/b.ts", + content: importA, + }; + const cTs: ts.projectSystem.File = { + path: "/project/c.ts", + content: importCurlyFromA + }; + const dTs: ts.projectSystem.File = { + path: "/project/d.ts", + content: [importAFromA, typeofImportA].join("\n") + }; + const tsconfig: ts.projectSystem.File = { + path: "/project/tsconfig.json", + content: "{}", + }; - function makeSampleSession() { - const host = ts.projectSystem.createServerHost([aTs, bTs, cTs, dTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs, bTs, cTs, dTs], session); - return session; - } + function makeSampleSession() { + const host = ts.projectSystem.createServerHost([aTs, bTs, cTs, dTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs, bTs, cTs, dTs], session); + return session; + } - it("should get file references", () => { - const session = makeSampleSession(); + it("should get file references", () => { + const session = makeSampleSession(); - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.FileReferences, { file: aTs.path }); - const expectResponse: ts.projectSystem.protocol.FileReferencesResponseBody = { - refs: [ - ts.projectSystem.makeReferenceItem({ file: bTs, text: "./a", lineText: importA, contextText: importA, isWriteAccess: false }), - ts.projectSystem.makeReferenceItem({ file: cTs, text: "./a", lineText: importCurlyFromA, contextText: importCurlyFromA, isWriteAccess: false }), - ts.projectSystem.makeReferenceItem({ file: dTs, text: "/project/a", lineText: importAFromA, contextText: importAFromA, isWriteAccess: false }), - ts.projectSystem.makeReferenceItem({ file: dTs, text: "./a", lineText: typeofImportA, contextText: typeofImportA, isWriteAccess: false }), - ], - symbolName: `"${aTs.path}"`, - }; + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.FileReferences, { file: aTs.path }); + const expectResponse: ts.projectSystem.protocol.FileReferencesResponseBody = { + refs: [ + ts.projectSystem.makeReferenceItem({ file: bTs, text: "./a", lineText: importA, contextText: importA, isWriteAccess: false }), + ts.projectSystem.makeReferenceItem({ file: cTs, text: "./a", lineText: importCurlyFromA, contextText: importCurlyFromA, isWriteAccess: false }), + ts.projectSystem.makeReferenceItem({ file: dTs, text: "/project/a", lineText: importAFromA, contextText: importAFromA, isWriteAccess: false }), + ts.projectSystem.makeReferenceItem({ file: dTs, text: "./a", lineText: typeofImportA, contextText: typeofImportA, isWriteAccess: false }), + ], + symbolName: `"${aTs.path}"`, + }; - 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 95db397d5d6b1..4baa7e7d672a9 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -1,29 +1,29 @@ namespace ts.projectSystem { - 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 = ts.TestFSWithWatch.File; - export type SymLink = ts.TestFSWithWatch.SymLink; - export type Folder = ts.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 ts.convertToObject(ts.parseJsonText("json.json", s.replace(outputEventRegex, "")), []); - } - - export const customTypesMap = { - path: "/typesMap.json" as ts.Path, - content: `{ +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 = ts.TestFSWithWatch.File; +export type SymLink = ts.TestFSWithWatch.SymLink; +export type Folder = ts.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 ts.convertToObject(ts.parseJsonText("json.json", s.replace(outputEventRegex, "")), []); +} + +export const customTypesMap = { + path: "/typesMap.json" as ts.Path, + content: `{ "typesMap": { "jquery": { "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", @@ -43,810 +43,810 @@ namespace ts.projectSystem { "lodash": "lodash" } }` - }; +}; - export interface PostExecAction { - readonly success: boolean; - readonly callback: TI.RequestCompletedAction; - } +export interface PostExecAction { + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; +} - export interface Logger extends ts.server.Logger { - logs: string[]; - } +export interface Logger extends ts.server.Logger { + logs: string[]; +} - export function nullLogger(): Logger { - return { - close: ts.noop, - hasLevel: ts.returnFalse, - loggingEnabled: ts.returnFalse, - perftrc: ts.noop, - info: ts.noop, - msg: ts.noop, - startGroup: ts.noop, - endGroup: ts.noop, - getLogFileName: ts.returnUndefined, - logs: [], - }; - } +export function nullLogger(): Logger { + return { + close: ts.noop, + hasLevel: ts.returnFalse, + loggingEnabled: ts.returnFalse, + perftrc: ts.noop, + info: ts.noop, + msg: ts.noop, + startGroup: ts.noop, + endGroup: ts.noop, + getLogFileName: ts.returnUndefined, + logs: [], + }; +} - export function createHasErrorMessageLogger(): Logger { - return { - ...nullLogger(), - msg: (s, type) => ts.Debug.fail(`Error: ${s}, type: ${type}`), - }; - } +export function createHasErrorMessageLogger(): Logger { + return { + ...nullLogger(), + msg: (s, type) => ts.Debug.fail(`Error: ${s}, type: ${type}`), + }; +} - export function createLoggerWritingToConsole(): Logger { - return { - ...nullLogger(), - hasLevel: ts.returnTrue, - loggingEnabled: ts.returnTrue, - perftrc: s => console.log(s), - info: s => console.log(s), - msg: (s, type) => console.log(`${type}:: ${s}`), - }; - } +export function createLoggerWritingToConsole(): Logger { + return { + ...nullLogger(), + hasLevel: ts.returnTrue, + loggingEnabled: ts.returnTrue, + perftrc: s => console.log(s), + info: s => console.log(s), + msg: (s, type) => console.log(`${type}:: ${s}`), + }; +} - export function createLoggerWithInMemoryLogs(): Logger { - const logger = createHasErrorMessageLogger(); - return { - ...logger, - hasLevel: ts.returnTrue, - loggingEnabled: ts.returnTrue, - info: s => logger.logs.push(s.replace(/Elapsed::?\s*\d+(?:\.\d+)?ms/g, "Elapsed:: *ms") - .replace(/\"updateGraphDurationMs\"\:\d+(?:\.\d+)?/g, `"updateGraphDurationMs":*`) - .replace(/\"createAutoImportProviderProgramDurationMs\"\:\d+(?:\.\d+)?/g, `"createAutoImportProviderProgramDurationMs":*`) - .replace(`"version":"${ts.version}"`, `"version":"FakeVersion"`)) - }; - } +export function createLoggerWithInMemoryLogs(): Logger { + const logger = createHasErrorMessageLogger(); + return { + ...logger, + hasLevel: ts.returnTrue, + loggingEnabled: ts.returnTrue, + info: s => logger.logs.push(s.replace(/Elapsed::?\s*\d+(?:\.\d+)?ms/g, "Elapsed:: *ms") + .replace(/\"updateGraphDurationMs\"\:\d+(?:\.\d+)?/g, `"updateGraphDurationMs":*`) + .replace(/\"createAutoImportProviderProgramDurationMs\"\:\d+(?:\.\d+)?/g, `"createAutoImportProviderProgramDurationMs":*`) + .replace(`"version":"${ts.version}"`, `"version":"FakeVersion"`)) + }; +} - export function baselineTsserverLogs(scenario: string, subScenario: string, sessionOrService: TestSession | TestProjectService) { - ts.Debug.assert(sessionOrService.logger.logs.length); // Ensure caller used in memory logger - Harness.Baseline.runBaseline(`tsserver/${scenario}/${subScenario.split(" ").join("-")}.js`, sessionOrService.logger.logs.join("\r\n")); - } +export function baselineTsserverLogs(scenario: string, subScenario: string, sessionOrService: TestSession | TestProjectService) { + ts.Debug.assert(sessionOrService.logger.logs.length); // Ensure caller used in memory logger + Harness.Baseline.runBaseline(`tsserver/${scenario}/${subScenario.split(" ").join("-")}.js`, sessionOrService.logger.logs.join("\r\n")); +} - export function appendAllScriptInfos(service: ts.server.ProjectService, logs: string[]) { - logs.push(""); - logs.push(`ScriptInfos:`); - service.filenameToScriptInfo.forEach(info => logs.push(`path: ${info.path} fileName: ${info.fileName}`)); - logs.push(""); - } +export function appendAllScriptInfos(service: ts.server.ProjectService, logs: string[]) { + logs.push(""); + logs.push(`ScriptInfos:`); + service.filenameToScriptInfo.forEach(info => logs.push(`path: ${info.path} fileName: ${info.fileName}`)); + logs.push(""); +} - export function appendProjectFileText(project: ts.server.Project, logs: string[]) { - logs.push(""); - logs.push(`Project: ${project.getProjectName()}`); - project.getCurrentProgram()?.getSourceFiles().forEach(f => { - logs.push(JSON.stringify({ fileName: f.fileName, version: f.version })); - logs.push(f.text); - logs.push(""); - }); +export function appendProjectFileText(project: ts.server.Project, logs: string[]) { + logs.push(""); + logs.push(`Project: ${project.getProjectName()}`); + project.getCurrentProgram()?.getSourceFiles().forEach(f => { + logs.push(JSON.stringify({ fileName: f.fileName, version: f.version })); + logs.push(f.text); logs.push(""); - } - - export class TestTypingsInstaller extends TI.TypingsInstaller implements ts.server.ITypingsInstaller { - protected projectService!: ts.server.ProjectService; - constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, installTypingHost: ts.server.ServerHost, readonly typesRegistry = new ts.Map>(), log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, ts.TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); - } - - protected postExecActions: PostExecAction[] = []; - - isKnownTypesPackageName = ts.notImplemented; - installPackage = ts.notImplemented; - inspectValue = ts.notImplemented; - - executePendingCommands() { - const actionsToRun = this.postExecActions; - this.postExecActions = []; - for (const action of actionsToRun) { - action.callback(action.success); - } - } + }); + logs.push(""); +} - checkPendingCommands(expectedCount: number) { - assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); - } +export class TestTypingsInstaller extends TI.TypingsInstaller implements ts.server.ITypingsInstaller { + protected projectService!: ts.server.ProjectService; + constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, installTypingHost: ts.server.ServerHost, readonly typesRegistry = new ts.Map>(), log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, ts.TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); + } - onProjectClosed = ts.noop; - attach(projectService: ts.server.ProjectService) { - this.projectService = projectService; - } + protected postExecActions: PostExecAction[] = []; - getInstallTypingHost() { - return this.installTypingHost; - } + isKnownTypesPackageName = ts.notImplemented; + installPackage = ts.notImplemented; + inspectValue = ts.notImplemented; - installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - this.addPostExecAction("success", cb); + executePendingCommands() { + const actionsToRun = this.postExecActions; + this.postExecActions = []; + for (const action of actionsToRun) { + action.callback(action.success); } + } - sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings) { - this.projectService.updateTypingsForProject(response); - } + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); + } - enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray) { - const request = ts.server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); - this.install(request); - } + onProjectClosed = ts.noop; + attach(projectService: ts.server.ProjectService) { + this.projectService = projectService; + } - addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { - const out = ts.isString(stdout) ? stdout : createNpmPackageJsonString(stdout); - const action: PostExecAction = { - success: !!out, - callback: cb - }; - this.postExecActions.push(action); - } + getInstallTypingHost() { + return this.installTypingHost; } - function createNpmPackageJsonString(installedTypings: string[]): string { - const dependencies: ts.MapLike = {}; - for (const typing of installedTypings) { - dependencies[typing] = "1.0.0"; - } - return JSON.stringify({ dependencies }); - } - - export function createTypesRegistry(...list: string[]): ts.ESMap> { - 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 = new ts.Map>(); - for (const l of list) { - map.set(l, versionMap); - } - return map; + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); } - export function toExternalFile(fileName: string): protocol.ExternalFile { - return { fileName }; + sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings) { + this.projectService.updateTypingsForProject(response); } - export function toExternalFiles(fileNames: string[]) { - return ts.map(fileNames, toExternalFile); + enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray) { + const request = ts.server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); + this.install(request); } - export function fileStats(nonZeroStats: Partial): ts.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 }; + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { + const out = ts.isString(stdout) ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + success: !!out, + callback: cb + }; + this.postExecActions.push(action); } +} - export class TestServerEventManager { - private events: ts.server.ProjectServiceEvent[] = []; - readonly session: TestSession; - readonly service: ts.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(); - } +function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: ts.MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; + } + return JSON.stringify({ dependencies }); +} - getEvents(): readonly ts.server.ProjectServiceEvent[] { - const events = this.events; - this.events = []; - return events; - } +export function createTypesRegistry(...list: string[]): ts.ESMap> { + 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 = new ts.Map>(); + for (const l of list) { + map.set(l, versionMap); + } + return map; +} - getEvent(eventName: T["eventName"]): T["data"] { - let eventData: T["data"] | undefined; - ts.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 ts.Debug.checkDefined(eventData); - } +export function toExternalFile(fileName: string): protocol.ExternalFile { + return { fileName }; +} - hasZeroEvent(eventName: T["eventName"]) { - this.events.forEach(event => assert.notEqual(event.eventName, eventName)); - } +export function toExternalFiles(fileNames: string[]) { + return ts.map(fileNames, toExternalFile); +} - assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { - assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { - projectId: ts.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, - ...partial, - }); - } +export function fileStats(nonZeroStats: Partial): ts.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 }; +} - assertOpenFileTelemetryEvent(info: ts.server.OpenFileInfo): void { - assert.deepEqual(this.getEvent(ts.server.OpenFileInfoTelemetryEvent), { info }); - } - assertNoOpenFilesTelemetryEvent(): void { - this.hasZeroEvent(ts.server.OpenFileInfoTelemetryEvent); - } +export class TestServerEventManager { + private events: ts.server.ProjectServiceEvent[] = []; + readonly session: TestSession; + readonly service: ts.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(); } - export interface TestSessionOptions extends ts.server.SessionOptions { - logger: Logger; + getEvents(): readonly ts.server.ProjectServiceEvent[] { + const events = this.events; + this.events = []; + return events; } - export class TestSession extends ts.server.Session { - private seq = 0; - public events: protocol.Event[] = []; - public testhost: TestServerHost = this.host as TestServerHost; - public logger: Logger; + getEvent(eventName: T["eventName"]): T["data"] { + let eventData: T["data"] | undefined; + ts.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 ts.Debug.checkDefined(eventData); + } - constructor(opts: TestSessionOptions) { - super(opts); - this.logger = opts.logger; - } + hasZeroEvent(eventName: T["eventName"]) { + this.events.forEach(event => assert.notEqual(event.eventName, eventName)); + } - getProjectService() { - return this.projectService; - } + assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { + assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { + projectId: ts.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, + ...partial, + }); + } - public getSeq() { - return this.seq; - } + assertOpenFileTelemetryEvent(info: ts.server.OpenFileInfo): void { + assert.deepEqual(this.getEvent(ts.server.OpenFileInfoTelemetryEvent), { info }); + } + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(ts.server.OpenFileInfoTelemetryEvent); + } +} - public getNextSeq() { - return this.seq + 1; - } +export interface TestSessionOptions extends ts.server.SessionOptions { + logger: Logger; +} - public executeCommand(request: protocol.Request) { - const verboseLogging = this.logger.hasLevel(ts.server.LogLevel.verbose); - if (verboseLogging) - this.logger.info(`request:${JSON.stringify(request)}`); - const result = super.executeCommand(request); - if (verboseLogging) - this.logger.info(`response:${JSON.stringify(result)}`); - return result; - } +export class TestSession extends ts.server.Session { + private seq = 0; + public events: protocol.Event[] = []; + public testhost: TestServerHost = this.host as TestServerHost; + public logger: Logger; - public executeCommandSeq(request: Partial) { - this.seq++; - request.seq = this.seq; - request.type = "request"; - return this.executeCommand(request as T); - } + constructor(opts: TestSessionOptions) { + super(opts); + this.logger = opts.logger; + } - public event(body: T, eventName: string) { - this.events.push(ts.server.toEvent(eventName, body)); - super.event(body, eventName); - } + getProjectService() { + return this.projectService; + } - public clearMessages() { - ts.clear(this.events); - this.testhost.clearOutput(); - } + public getSeq() { + return this.seq; } - export function createSession(host: ts.server.ServerHost, opts: Partial = {}) { - if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } + public getNextSeq() { + return this.seq + 1; + } - if (opts.eventHandler !== undefined) { - opts.canUseEvents = true; - } + public executeCommand(request: protocol.Request) { + const verboseLogging = this.logger.hasLevel(ts.server.LogLevel.verbose); + if (verboseLogging) + this.logger.info(`request:${JSON.stringify(request)}`); + const result = super.executeCommand(request); + if (verboseLogging) + this.logger.info(`response:${JSON.stringify(result)}`); + return result; + } - const sessionOptions: TestSessionOptions = { - host, - cancellationToken: ts.server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: opts.logger || createHasErrorMessageLogger(), - canUseEvents: false - }; + public executeCommandSeq(request: Partial) { + this.seq++; + request.seq = this.seq; + request.type = "request"; + return this.executeCommand(request as T); + } - return new TestSession({ ...sessionOptions, ...opts }); + public event(body: T, eventName: string) { + this.events.push(ts.server.toEvent(eventName, body)); + super.event(body, eventName); } - export function createSessionWithEventTracking(host: ts.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); - } - } - }); + public clearMessages() { + ts.clear(this.events); + this.testhost.clearOutput(); + } +} - return { session, events }; +export function createSession(host: ts.server.ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); } - export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { - const session = createSession(host, { canUseEvents: true, ...opts }); + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } - return { - session, - getEvents, - clearEvents - }; + const sessionOptions: TestSessionOptions = { + host, + cancellationToken: ts.server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger(), + canUseEvents: false + }; - function getEvents() { - return ts.mapDefined(host.getOutput(), s => { - const e = mapOutputToJson(s); - return (ts.isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; - }); - } + return new TestSession({ ...sessionOptions, ...opts }); +} - function clearEvents() { - session.clearMessages(); +export function createSessionWithEventTracking(host: ts.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); + } } - } + }); - export interface TestProjectServiceOptions extends ts.server.ProjectServiceOptions { - logger: Logger; - } + return { session, events }; +} - export class TestProjectService extends ts.server.ProjectService { - constructor(host: ts.server.ServerHost, public logger: Logger, cancellationToken: ts.HostCancellationToken, useSingleInferredProject: boolean, typingsInstaller: ts.server.ITypingsInstaller, opts: Partial = {}) { - super({ - host, - logger, - session: undefined, - cancellationToken, - useSingleInferredProject, - useInferredProjectPerProjectRoot: false, - typingsInstaller, - typesMapLocation: customTypesMap.path, - ...opts - }); - } +export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + const session = createSession(host, { canUseEvents: true, ...opts }); - checkNumberOfProjects(count: { - inferredProjects?: number; - configuredProjects?: number; - externalProjects?: number; - }) { - checkNumberOfProjects(this, count); - } - } + return { + session, + getEvents, + clearEvents + }; - export function createProjectService(host: ts.server.ServerHost, options?: Partial) { - const cancellationToken = options?.cancellationToken || ts.server.nullCancellationToken; - const logger = options?.logger || createHasErrorMessageLogger(); - const useSingleInferredProject = options?.useSingleInferredProject !== undefined ? options.useSingleInferredProject : false; - return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, options?.typingsInstaller || ts.server.nullTypingsInstaller, options); + function getEvents() { + return ts.mapDefined(host.getOutput(), s => { + const e = mapOutputToJson(s); + return (ts.isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + }); } - export function checkNumberOfConfiguredProjects(projectService: ts.server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); + function clearEvents() { + session.clearMessages(); } +} - export function checkNumberOfExternalProjects(projectService: ts.server.ProjectService, expected: number) { - assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); - } +export interface TestProjectServiceOptions extends ts.server.ProjectServiceOptions { + logger: Logger; +} - export function checkNumberOfInferredProjects(projectService: ts.server.ProjectService, expected: number) { - assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); +export class TestProjectService extends ts.server.ProjectService { + constructor(host: ts.server.ServerHost, public logger: Logger, cancellationToken: ts.HostCancellationToken, useSingleInferredProject: boolean, typingsInstaller: ts.server.ITypingsInstaller, opts: Partial = {}) { + super({ + host, + logger, + session: undefined, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + typesMapLocation: customTypesMap.path, + ...opts + }); } - export function checkNumberOfProjects(projectService: ts.server.ProjectService, count: { + checkNumberOfProjects(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: ts.server.ProjectService, index: number) { - const values = projectService.configuredProjects.values(); - while (index > 0) { - const iterResult = values.next(); - if (iterResult.done) - return ts.Debug.fail("Expected a result."); - index--; - } - const iterResult = values.next(); - if (iterResult.done) - return ts.Debug.fail("Expected a result."); - return iterResult.value; + checkNumberOfProjects(this, count); } +} - export function checkOrphanScriptInfos(service: ts.server.ProjectService, expectedFiles: readonly string[]) { - checkArray("Orphan ScriptInfos:", ts.arrayFrom(ts.mapDefinedIterator(service.filenameToScriptInfo.values(), v => v.containingProjects.length === 0 ? v.fileName : undefined)), expectedFiles); - } +export function createProjectService(host: ts.server.ServerHost, options?: Partial) { + const cancellationToken = options?.cancellationToken || ts.server.nullCancellationToken; + const logger = options?.logger || createHasErrorMessageLogger(); + const useSingleInferredProject = options?.useSingleInferredProject !== undefined ? options.useSingleInferredProject : false; + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, options?.typingsInstaller || ts.server.nullTypingsInstaller, options); +} - export function checkProjectActualFiles(project: ts.server.Project, expectedFiles: readonly string[]) { - checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}:: actual files`, project.getFileNames(), expectedFiles); - } +export function checkNumberOfConfiguredProjects(projectService: ts.server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); +} - export function checkProjectRootFiles(project: ts.server.Project, expectedFiles: readonly string[]) { - checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}::, rootFileNames`, project.getRootFiles(), expectedFiles); - } +export function checkNumberOfExternalProjects(projectService: ts.server.ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); +} - export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { - dir = ts.normalizePath(dir); - const result: string[] = []; - ts.forEachAncestorDirectory(dir, ancestor => { - if (mapAncestor(ancestor)) { - result.push(ts.combinePaths(ancestor, path2)); - } - }); - return result; - } +export function checkNumberOfInferredProjects(projectService: ts.server.ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); +} - export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { - return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(ts.directorySeparator).length > 4); - } +export function checkNumberOfProjects(projectService: ts.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 const nodeModules = "node_modules"; - export function getNodeModuleDirectories(dir: string) { - return getRootsToWatchWithAncestorDirectory(dir, nodeModules); +export function configuredProjectAt(projectService: ts.server.ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + const iterResult = values.next(); + if (iterResult.done) + return ts.Debug.fail("Expected a result."); + index--; } + const iterResult = values.next(); + if (iterResult.done) + return ts.Debug.fail("Expected a result."); + return iterResult.value; +} - export const nodeModulesAtTypes = "node_modules/@types"; - export function getTypeRootsFromLocation(currentDirectory: string) { - return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); - } +export function checkOrphanScriptInfos(service: ts.server.ProjectService, expectedFiles: readonly string[]) { + checkArray("Orphan ScriptInfos:", ts.arrayFrom(ts.mapDefinedIterator(service.filenameToScriptInfo.values(), v => v.containingProjects.length === 0 ? v.fileName : undefined)), expectedFiles); +} - export function getConfigFilesToWatch(folder: string) { - return [ - ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"), - ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json") - ]; - } +export function checkProjectActualFiles(project: ts.server.Project, expectedFiles: readonly string[]) { + checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}:: actual files`, project.getFileNames(), expectedFiles); +} - export function checkOpenFiles(projectService: ts.server.ProjectService, expectedFiles: File[]) { - checkArray("Open files", ts.arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as ts.Path)!.fileName), expectedFiles.map(file => file.path)); - } +export function checkProjectRootFiles(project: ts.server.Project, expectedFiles: readonly string[]) { + checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}::, rootFileNames`, project.getRootFiles(), expectedFiles); +} - export function checkScriptInfos(projectService: ts.server.ProjectService, expectedFiles: readonly string[], additionInfo?: string) { - checkArray(`ScriptInfos files: ${additionInfo || ""}`, ts.arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); - } +export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = ts.normalizePath(dir); + const result: string[] = []; + ts.forEachAncestorDirectory(dir, ancestor => { + if (mapAncestor(ancestor)) { + result.push(ts.combinePaths(ancestor, path2)); + } + }); + return result; +} - export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location { - const start = nthIndexOf(str, substring, options ? options.index : 0); - ts.Debug.assert(start !== -1); - return protocolToLocation(str)(start); - } +export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(ts.directorySeparator).length > 4); +} - export function protocolToLocation(text: string): (pos: number) => protocol.Location { - const lineStarts = ts.computeLineStarts(text); - return pos => { - const x = ts.computeLineAndCharacterOfPosition(lineStarts, pos); - return { line: x.line + 1, offset: x.character + 1 }; - }; - } +export const nodeModules = "node_modules"; +export function getNodeModuleDirectories(dir: string) { + return getRootsToWatchWithAncestorDirectory(dir, nodeModules); +} - 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(ts.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(ts.textSpanEnd(span)), - ...contextSpan && { - contextStart: toLocation(contextSpan.start), - contextEnd: toLocation(ts.textSpanEnd(contextSpan)) - } - }; - } +export const nodeModulesAtTypes = "node_modules/@types"; +export function getTypeRootsFromLocation(currentDirectory: string) { + return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); +} - 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 getConfigFilesToWatch(folder: string) { + return [ + ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"), + ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json") + ]; +} - export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): ts.TextSpan { - const start = nthIndexOf(str, substring, options ? options.index : 0); - ts.Debug.assert(start !== -1); - return ts.createTextSpan(start, substring.length); - } +export function checkOpenFiles(projectService: ts.server.ProjectService, expectedFiles: File[]) { + checkArray("Open files", ts.arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as ts.Path)!.fileName), expectedFiles.map(file => file.path)); +} - export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs { - return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) }; - } +export function checkScriptInfos(projectService: ts.server.ProjectService, expectedFiles: readonly string[], additionInfo?: string) { + checkArray(`ScriptInfos files: ${additionInfo || ""}`, ts.arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); +} - export interface SpanFromSubstringOptions { - readonly index: number; - } +export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location { + const start = nthIndexOf(str, substring, options ? options.index : 0); + ts.Debug.assert(start !== -1); + return protocolToLocation(str)(start); +} - 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; - } +export function protocolToLocation(text: string): (pos: number) => protocol.Location { + const lineStarts = ts.computeLineStarts(text); + return pos => { + const x = ts.computeLineAndCharacterOfPosition(lineStarts, pos); + return { line: x.line + 1, offset: x.character + 1 }; + }; +} - /** - * 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 ts.server.ServerCancellationToken { - private currentId: number | undefined = -1; - private requestToCancel = -1; - private isCancellationRequestedCount = 0; +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(ts.textSpanEnd(span)) }; +} - constructor(private cancelAfterRequest = 0) { - } +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) }; +} - setRequest(requestId: number) { - this.currentId = requestId; - } +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; +} - setRequestToCancel(requestId: number) { - this.resetToken(); - this.requestToCancel = requestId; +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(ts.textSpanEnd(span)), + ...contextSpan && { + contextStart: toLocation(contextSpan.start), + contextEnd: toLocation(ts.textSpanEnd(contextSpan)) } + }; +} - resetRequest(requestId: number) { - assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); - this.currentId = undefined; - } +export interface ProtocolRenameSpanFromSubstring extends ProtocolTextSpanWithContextFromString { + prefixSuffixText?: { + readonly prefixText?: string; + readonly suffixText?: string; + }; +} +export function protocolRenameSpanFromSubstring({ prefixSuffixText, ...rest }: ProtocolRenameSpanFromSubstring): protocol.RenameTextSpan { + return { + ...protocolTextSpanWithContextFromSubstring(rest), + ...prefixSuffixText + }; +} - 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; - } +export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): ts.TextSpan { + const start = nthIndexOf(str, substring, options ? options.index : 0); + ts.Debug.assert(start !== -1); + return ts.createTextSpan(start, substring.length); +} - resetToken() { - this.currentId = -1; - this.isCancellationRequestedCount = 0; - this.requestToCancel = -1; - } - } +export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs { + return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) }; +} - export function makeSessionRequest(command: string, args: T): protocol.Request { - return { - seq: 0, - type: "request", - command, - arguments: args - }; - } +export interface SpanFromSubstringOptions { + readonly index: number; +} - export function executeSessionRequest(session: ts.server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { - return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; +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; +} - export function executeSessionRequestNoResponse(session: ts.server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { - session.executeCommand(makeSessionRequest(command, args)); - } +/** + * 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 ts.server.ServerCancellationToken { + private currentId: number | undefined = -1; + private requestToCancel = -1; + private isCancellationRequestedCount = 0; - export function openFilesForSession(files: readonly (File | { - readonly file: File | string; - readonly projectRootPath: string; - content?: string; - })[], session: ts.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 - } + constructor(private cancelAfterRequest = 0) { } - export function closeFilesForSession(files: readonly File[], session: ts.server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); - } + setRequest(requestId: number) { + this.currentId = requestId; } - export interface MakeReferenceItem extends DocumentSpanFromSubstring { - isDefinition?: boolean; - isWriteAccess?: boolean; - lineText: string; + setRequestToCancel(requestId: number) { + this.resetToken(); + this.requestToCancel = requestId; } - export function makeReferenceItem({ isDefinition, isWriteAccess, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { - return { - ...protocolFileSpanWithContextFromSubstring(rest), - isDefinition, - isWriteAccess: isWriteAccess === undefined ? !!isDefinition : isWriteAccess, - lineText, - }; + resetRequest(requestId: number) { + assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); + this.currentId = undefined; } - export interface VerifyGetErrRequestBase { - session: TestSession; - host: TestServerHost; - existingTimeouts?: number; + 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; } - export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { - files: readonly (string | File)[]; - skip?: CheckAllErrors["skip"]; - } - export function verifyGetErrRequest(request: VerifyGetErrRequest) { - const { session, files } = request; - session.executeCommandSeq({ - command: protocol.CommandTypes.Geterr, - arguments: { delay: 0, files: files.map(filePath) } - }); - checkAllErrors(request); + + resetToken() { + this.currentId = -1; + this.isCancellationRequestedCount = 0; + this.requestToCancel = -1; } +} - interface SkipErrors { - semantic?: true; - suggestion?: true; +export function makeSessionRequest(command: string, args: T): protocol.Request { + return { + seq: 0, + type: "request", + command, + arguments: args + }; +} + +export function executeSessionRequest(session: ts.server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { + return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; +} + +export function executeSessionRequestNoResponse(session: ts.server.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: ts.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 } - ; - export interface CheckAllErrors extends VerifyGetErrRequestBase { - files: readonly any[]; - skip?: readonly (SkipErrors | undefined)[]; +} + +export function closeFilesForSession(files: readonly File[], session: ts.server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); } - function checkAllErrors({ session, host, existingTimeouts, files, skip }: CheckAllErrors) { - ts.Debug.assert(session.logger.logs.length); - for (let i = 0; i < files.length; i++) { - if (existingTimeouts !== undefined) { - host.checkTimeoutQueueLength(existingTimeouts + 1); - host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); - } - else { - host.checkTimeoutQueueLengthAndRun(1); - } - if (!skip?.[i]?.semantic) - host.runQueuedImmediateCallbacks(1); - if (!skip?.[i]?.suggestion) - host.runQueuedImmediateCallbacks(1); +} + +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 interface VerifyGetErrRequestBase { + session: TestSession; + host: TestServerHost; + existingTimeouts?: number; +} +export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { + files: readonly (string | File)[]; + skip?: CheckAllErrors["skip"]; +} +export function verifyGetErrRequest(request: VerifyGetErrRequest) { + const { session, files } = request; + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { delay: 0, files: files.map(filePath) } + }); + checkAllErrors(request); +} + +interface SkipErrors { + semantic?: true; + suggestion?: true; +} +; +export interface CheckAllErrors extends VerifyGetErrRequestBase { + files: readonly any[]; + skip?: readonly (SkipErrors | undefined)[]; +} +function checkAllErrors({ session, host, existingTimeouts, files, skip }: CheckAllErrors) { + ts.Debug.assert(session.logger.logs.length); + for (let i = 0; i < files.length; i++) { + if (existingTimeouts !== undefined) { + host.checkTimeoutQueueLength(existingTimeouts + 1); + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); } + else { + host.checkTimeoutQueueLengthAndRun(1); + } + if (!skip?.[i]?.semantic) + host.runQueuedImmediateCallbacks(1); + if (!skip?.[i]?.suggestion) + host.runQueuedImmediateCallbacks(1); } +} - function filePath(file: string | File) { - return ts.isString(file) ? file : file.path; - } +function filePath(file: string | File) { + return ts.isString(file) ? file : file.path; +} - function verifyErrorsUsingGeterr({scenario, subScenario, allFiles, openFiles, getErrRequest }: VerifyGetErrScenario) { - it("verifies the errors in open file", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs() }); - openFilesForSession(openFiles(), session); +function verifyErrorsUsingGeterr({scenario, subScenario, allFiles, openFiles, getErrRequest }: VerifyGetErrScenario) { + it("verifies the errors in open file", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs() }); + openFilesForSession(openFiles(), session); - verifyGetErrRequest({ session, host, files: getErrRequest() }); - baselineTsserverLogs(scenario, `${subScenario} getErr`, session); - }); - } + verifyGetErrRequest({ session, host, files: getErrRequest() }); + baselineTsserverLogs(scenario, `${subScenario} getErr`, session); + }); +} - function verifyErrorsUsingGeterrForProject({ scenario, subScenario, allFiles, openFiles, getErrForProjectRequest }: VerifyGetErrScenario) { - it("verifies the errors in projects", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs() }); - openFilesForSession(openFiles(), session); +function verifyErrorsUsingGeterrForProject({ scenario, subScenario, allFiles, openFiles, getErrForProjectRequest }: VerifyGetErrScenario) { + it("verifies the errors in projects", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs() }); + openFilesForSession(openFiles(), session); - for (const expected of getErrForProjectRequest()) { - session.executeCommandSeq({ - command: protocol.CommandTypes.GeterrForProject, - arguments: { delay: 0, file: filePath(expected.project) } - }); - checkAllErrors({ session, host, files: expected.files }); - } - baselineTsserverLogs(scenario, `${subScenario} geterrForProject`, session); - }); - } + for (const expected of getErrForProjectRequest()) { + session.executeCommandSeq({ + command: protocol.CommandTypes.GeterrForProject, + arguments: { delay: 0, file: filePath(expected.project) } + }); + checkAllErrors({ session, host, files: expected.files }); + } + baselineTsserverLogs(scenario, `${subScenario} geterrForProject`, session); + }); +} - function verifyErrorsUsingSyncMethods({ scenario, subScenario, allFiles, openFiles, syncDiagnostics }: VerifyGetErrScenario) { - it("verifies the errors using sync commands", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs() }); - openFilesForSession(openFiles(), session); - for (const { file, project } of syncDiagnostics()) { - const reqArgs = { file: filePath(file), projectFileName: project && filePath(project) }; - session.executeCommandSeq({ - command: protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: reqArgs - }); - session.executeCommandSeq({ - command: protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: reqArgs - }); - session.executeCommandSeq({ - command: protocol.CommandTypes.SuggestionDiagnosticsSync, - arguments: reqArgs - }); - } - baselineTsserverLogs(scenario, `${subScenario} gerErr with sync commands`, session); - }); - } +function verifyErrorsUsingSyncMethods({ scenario, subScenario, allFiles, openFiles, syncDiagnostics }: VerifyGetErrScenario) { + it("verifies the errors using sync commands", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs() }); + openFilesForSession(openFiles(), session); + for (const { file, project } of syncDiagnostics()) { + const reqArgs = { file: filePath(file), projectFileName: project && filePath(project) }; + session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: reqArgs + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: reqArgs + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.SuggestionDiagnosticsSync, + arguments: reqArgs + }); + } + baselineTsserverLogs(scenario, `${subScenario} gerErr with sync commands`, session); + }); +} - export interface GetErrForProjectDiagnostics { - project: string | File; - files: readonly (string | File)[]; - skip?: CheckAllErrors["skip"]; - } - export interface SyncDiagnostics { - file: string | File; - project?: string | File; - } - export interface VerifyGetErrScenario { - scenario: string; - subScenario: string; - allFiles: () => readonly File[]; - openFiles: () => readonly File[]; - getErrRequest: () => VerifyGetErrRequest["files"]; - getErrForProjectRequest: () => readonly GetErrForProjectDiagnostics[]; - syncDiagnostics: () => readonly SyncDiagnostics[]; - } - export function verifyGetErrScenario(scenario: VerifyGetErrScenario) { - verifyErrorsUsingGeterr(scenario); - verifyErrorsUsingGeterrForProject(scenario); - verifyErrorsUsingSyncMethods(scenario); - } +export interface GetErrForProjectDiagnostics { + project: string | File; + files: readonly (string | File)[]; + skip?: CheckAllErrors["skip"]; +} +export interface SyncDiagnostics { + file: string | File; + project?: string | File; +} +export interface VerifyGetErrScenario { + scenario: string; + subScenario: string; + allFiles: () => readonly File[]; + openFiles: () => readonly File[]; + getErrRequest: () => VerifyGetErrRequest["files"]; + getErrForProjectRequest: () => readonly GetErrForProjectDiagnostics[]; + syncDiagnostics: () => readonly SyncDiagnostics[]; +} +export function verifyGetErrScenario(scenario: VerifyGetErrScenario) { + verifyErrorsUsingGeterr(scenario); + verifyErrorsUsingGeterrForProject(scenario); + verifyErrorsUsingSyncMethods(scenario); +} } diff --git a/src/testRunner/unittests/tsserver/importHelpers.ts b/src/testRunner/unittests/tsserver/importHelpers.ts index de2311274185f..791f18bb8c435 100644 --- a/src/testRunner/unittests/tsserver/importHelpers.ts +++ b/src/testRunner/unittests/tsserver/importHelpers.ts @@ -1,18 +1,18 @@ 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 = ts.projectSystem.createServerHost([f1, tslib]); - const service = ts.projectSystem.createProjectService(host); - service.openExternalProject({ projectFileName: "p", rootFiles: [ts.projectSystem.toExternalFile(f1.path)], options: { importHelpers: true } }); - service.checkNumberOfProjects({ externalProjects: 1 }); - }); +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 = ts.projectSystem.createServerHost([f1, tslib]); + const service = ts.projectSystem.createProjectService(host); + service.openExternalProject({ projectFileName: "p", rootFiles: [ts.projectSystem.toExternalFile(f1.path)], options: { importHelpers: true } }); + service.checkNumberOfProjects({ externalProjects: 1 }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index e36adae6b13e0..84c28ea2f91d0 100644 --- a/src/testRunner/unittests/tsserver/inferredProjects.ts +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -1,475 +1,475 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: Inferred projects", () => { - it("create inferred project", () => { - const appFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app.ts`, - content: ` +describe("unittests:: tsserver:: Inferred projects", () => { + it("create inferred project", () => { + const appFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app.ts`, + content: ` import {f} from "./module" console.log(f) ` - }; - - const moduleFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/module.d.ts`, - content: `export let x: number` - }; - const host = ts.projectSystem.createServerHost([appFile, moduleFile, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host); - const { configFileName } = projectService.openClientFile(appFile.path); - - assert(!configFileName, `should not find config, got: '${configFileName}`); - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - - const project = projectService.inferredProjects[0]; - - ts.projectSystem.checkArray("inferred project", project.getFileNames(), [appFile.path, ts.projectSystem.libFile.path, moduleFile.path]); - ts.projectSystem.checkWatchedFiles(host, ts.projectSystem.getConfigFilesToWatch(ts.tscWatch.projectRoot).concat(ts.projectSystem.libFile.path, moduleFile.path)); - ts.projectSystem.checkWatchedDirectories(host, [ts.tscWatch.projectRoot], /*recursive*/ false); - ts.projectSystem.checkWatchedDirectories(host, [ts.combinePaths(ts.tscWatch.projectRoot, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - }); + }; + + const moduleFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/module.d.ts`, + content: `export let x: number` + }; + const host = ts.projectSystem.createServerHost([appFile, moduleFile, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host); + const { configFileName } = projectService.openClientFile(appFile.path); + + assert(!configFileName, `should not find config, got: '${configFileName}`); + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + + const project = projectService.inferredProjects[0]; + + ts.projectSystem.checkArray("inferred project", project.getFileNames(), [appFile.path, ts.projectSystem.libFile.path, moduleFile.path]); + ts.projectSystem.checkWatchedFiles(host, ts.projectSystem.getConfigFilesToWatch(ts.tscWatch.projectRoot).concat(ts.projectSystem.libFile.path, moduleFile.path)); + ts.projectSystem.checkWatchedDirectories(host, [ts.tscWatch.projectRoot], /*recursive*/ false); + ts.projectSystem.checkWatchedDirectories(host, [ts.combinePaths(ts.tscWatch.projectRoot, ts.projectSystem.nodeModulesAtTypes)], /*recursive*/ true); + }); - it("should use only one inferred project if 'useOneInferredProject' is set", () => { - const file1 = { - path: `${ts.tscWatch.projectRoot}/a/b/main.ts`, - content: "let x =1;" - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a/b/tsconfig.json`, - content: `{ + it("should use only one inferred project if 'useOneInferredProject' is set", () => { + const file1 = { + path: `${ts.tscWatch.projectRoot}/a/b/main.ts`, + content: "let x =1;" + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a/b/tsconfig.json`, + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const file2 = { - path: `${ts.tscWatch.projectRoot}/a/c/main.ts`, - content: "let x =1;" - }; - - const file3 = { - path: `${ts.tscWatch.projectRoot}/a/d/main.ts`, - content: "let x =1;" - }; - - const host = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, ts.projectSystem.libFile.path]); - - - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, ts.projectSystem.libFile.path]); - }); + }; + const file2 = { + path: `${ts.tscWatch.projectRoot}/a/c/main.ts`, + content: "let x =1;" + }; + + const file3 = { + path: `${ts.tscWatch.projectRoot}/a/d/main.ts`, + content: "let x =1;" + }; + + const host = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 0); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, ts.projectSystem.libFile.path]); + + + host.writeFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, ts.projectSystem.libFile.path]); + }); - it("disable inferred project", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; + it("disable inferred project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; - const host = ts.projectSystem.createServerHost([file1]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); + const host = ts.projectSystem.createServerHost([file1]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); - projectService.openClientFile(file1.path, file1.content); + projectService.openClientFile(file1.path, file1.content); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - assert.isDefined(proj); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); - assert.isFalse(proj.languageServiceEnabled); - }); + 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 = ts.projectSystem.createServerHost([file1, modFile]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(modFile.path); - - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - const inferredProjects = projectService.inferredProjects.slice(); - ts.projectSystem.checkProjectActualFiles(inferredProjects[0], [file1.path]); - ts.projectSystem.checkProjectActualFiles(inferredProjects[1], [modFile.path]); - projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ts.ModuleResolutionKind.Classic }); - host.checkTimeoutQueueLengthAndRun(3); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); - assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); - ts.projectSystem.checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); - assert.isTrue(inferredProjects[1].isOrphan()); - }); + 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 = ts.projectSystem.createServerHost([file1, modFile]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(modFile.path); + + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + const inferredProjects = projectService.inferredProjects.slice(); + ts.projectSystem.checkProjectActualFiles(inferredProjects[0], [file1.path]); + ts.projectSystem.checkProjectActualFiles(inferredProjects[1], [modFile.path]); + projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ts.ModuleResolutionKind.Classic }); + host.checkTimeoutQueueLengthAndRun(3); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + ts.projectSystem.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 = ts.projectSystem.createServerHost([f]); - const session = ts.projectSystem.createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "compilerOptionsForInferredProjects", - arguments: { - options: { - allowJs: true - } - } - } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); - session.executeCommand({ - seq: 2, - type: "request", - command: "open", - arguments: { - file: f.path, - fileContent: f.content, - scriptKindName: "JS" + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = ts.projectSystem.createServerHost([f]); + const session = ts.projectSystem.createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "compilerOptionsForInferredProjects", + arguments: { + options: { + allowJs: true } - } as ts.server.protocol.OpenRequest); - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); - }); + } + } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); + session.executeCommand({ + seq: 2, + type: "request", + command: "open", + arguments: { + file: f.path, + fileContent: f.content, + scriptKindName: "JS" + } + } as ts.server.protocol.OpenRequest); + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, file3, file4]); - const session = ts.projectSystem.createSession(host, { - useSingleInferredProject: true, - useInferredProjectPerProjectRoot: true - }); - session.executeCommand({ - seq: 1, - type: "request", - command: ts.projectSystem.CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ts.ScriptTarget.ESNext - } - } - } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); - session.executeCommand({ - seq: 2, - type: "request", - command: ts.projectSystem.CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ts.ScriptTarget.ES2015 - }, - projectRootPath: "/b" - } - } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); - session.executeCommand({ - seq: 3, - type: "request", - command: ts.projectSystem.CommandNames.Open, - arguments: { - file: file1.path, - fileContent: file1.content, - scriptKindName: "JS", - projectRootPath: file1.projectRootPath - } - } as ts.server.protocol.OpenRequest); - session.executeCommand({ - seq: 4, - type: "request", - command: ts.projectSystem.CommandNames.Open, - arguments: { - file: file2.path, - fileContent: file2.content, - scriptKindName: "JS", - projectRootPath: file2.projectRootPath - } - } as ts.server.protocol.OpenRequest); - session.executeCommand({ - seq: 5, - type: "request", - command: ts.projectSystem.CommandNames.Open, - arguments: { - file: file3.path, - fileContent: file3.content, - scriptKindName: "JS", - projectRootPath: file3.projectRootPath - } - } as ts.server.protocol.OpenRequest); - session.executeCommand({ - seq: 6, - type: "request", - command: ts.projectSystem.CommandNames.Open, - arguments: { - file: file4.path, - fileContent: file4.content, - scriptKindName: "JS" - } - } as ts.server.protocol.OpenRequest); - - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 3 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); - assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ts.ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ts.ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ts.ScriptTarget.ES2015); + 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 = ts.projectSystem.createServerHost([file1, file2, file3, file4]); + const session = ts.projectSystem.createSession(host, { + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true }); + session.executeCommand({ + seq: 1, + type: "request", + command: ts.projectSystem.CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ts.ScriptTarget.ESNext + } + } + } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); + session.executeCommand({ + seq: 2, + type: "request", + command: ts.projectSystem.CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ts.ScriptTarget.ES2015 + }, + projectRootPath: "/b" + } + } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); + session.executeCommand({ + seq: 3, + type: "request", + command: ts.projectSystem.CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "JS", + projectRootPath: file1.projectRootPath + } + } as ts.server.protocol.OpenRequest); + session.executeCommand({ + seq: 4, + type: "request", + command: ts.projectSystem.CommandNames.Open, + arguments: { + file: file2.path, + fileContent: file2.content, + scriptKindName: "JS", + projectRootPath: file2.projectRootPath + } + } as ts.server.protocol.OpenRequest); + session.executeCommand({ + seq: 5, + type: "request", + command: ts.projectSystem.CommandNames.Open, + arguments: { + file: file3.path, + fileContent: file3.content, + scriptKindName: "JS", + projectRootPath: file3.projectRootPath + } + } as ts.server.protocol.OpenRequest); + session.executeCommand({ + seq: 6, + type: "request", + command: ts.projectSystem.CommandNames.Open, + arguments: { + file: file4.path, + fileContent: file4.content, + scriptKindName: "JS" + } + } as ts.server.protocol.OpenRequest); + + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 3 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ts.ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ts.ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ts.ScriptTarget.ES2015); + }); - function checkInferredProject(inferredProject: ts.server.InferredProject, actualFiles: ts.projectSystem.File[], target: ts.ScriptTarget) { - ts.projectSystem.checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); - assert.equal(inferredProject.getCompilationSettings().target, target); + function checkInferredProject(inferredProject: ts.server.InferredProject, actualFiles: ts.projectSystem.File[], target: ts.ScriptTarget) { + ts.projectSystem.checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); + assert.equal(inferredProject.getCompilationSettings().target, target); + } + + function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const files: [ + ts.projectSystem.File, + ts.projectSystem.File, + ts.projectSystem.File, + ts.projectSystem.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 = ts.projectSystem.createServerHost(files, { useCaseSensitiveFileNames }); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ts.ScriptTarget.ESNext + }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ts.ScriptTarget.ES2015 + }, "/a"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ts.ScriptTarget.ESNext], + [[files[0], files[1]], ts.ScriptTarget.ES2015], + [[files[2]], ts.ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ts.ScriptTarget.ESNext], + [[files[0]], ts.ScriptTarget.ES2015], + [[files[1]], ts.ScriptTarget.ESNext], + [[files[2]], ts.ScriptTarget.ESNext] + ]); } - - function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const files: [ - ts.projectSystem.File, - ts.projectSystem.File, - ts.projectSystem.File, - ts.projectSystem.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 = ts.projectSystem.createServerHost(files, { useCaseSensitiveFileNames }); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true }); - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ts.ScriptTarget.ESNext - }); - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ts.ScriptTarget.ES2015 - }, "/a"); - - openClientFiles(["/a", "/a", "/b", undefined]); + else { verifyInferredProjectsState([ [[files[3]], ts.ScriptTarget.ESNext], [[files[0], files[1]], ts.ScriptTarget.ES2015], [[files[2]], ts.ScriptTarget.ESNext] ]); - closeClientFiles(); - - openClientFiles(["/a", "/A", "/b", undefined]); - if (useCaseSensitiveFileNames) { - verifyInferredProjectsState([ - [[files[3]], ts.ScriptTarget.ESNext], - [[files[0]], ts.ScriptTarget.ES2015], - [[files[1]], ts.ScriptTarget.ESNext], - [[files[2]], ts.ScriptTarget.ESNext] - ]); - } - else { - verifyInferredProjectsState([ - [[files[3]], ts.ScriptTarget.ESNext], - [[files[0], files[1]], ts.ScriptTarget.ES2015], - [[files[2]], ts.ScriptTarget.ESNext] - ]); - } - closeClientFiles(); - - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ts.ScriptTarget.ES2017 - }, "/A"); - - openClientFiles(["/a", "/a", "/b", undefined]); + } + closeClientFiles(); + + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ts.ScriptTarget.ES2017 + }, "/A"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ts.ScriptTarget.ESNext], + [[files[0], files[1]], useCaseSensitiveFileNames ? ts.ScriptTarget.ES2015 : ts.ScriptTarget.ES2017], + [[files[2]], ts.ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { verifyInferredProjectsState([ [[files[3]], ts.ScriptTarget.ESNext], - [[files[0], files[1]], useCaseSensitiveFileNames ? ts.ScriptTarget.ES2015 : ts.ScriptTarget.ES2017], + [[files[0]], ts.ScriptTarget.ES2015], + [[files[1]], ts.ScriptTarget.ES2017], [[files[2]], ts.ScriptTarget.ESNext] ]); - closeClientFiles(); - - openClientFiles(["/a", "/A", "/b", undefined]); - if (useCaseSensitiveFileNames) { - verifyInferredProjectsState([ - [[files[3]], ts.ScriptTarget.ESNext], - [[files[0]], ts.ScriptTarget.ES2015], - [[files[1]], ts.ScriptTarget.ES2017], - [[files[2]], ts.ScriptTarget.ESNext] - ]); - } - else { - verifyInferredProjectsState([ - [[files[3]], ts.ScriptTarget.ESNext], - [[files[0], files[1]], ts.ScriptTarget.ES2017], - [[files[2]], ts.ScriptTarget.ESNext] - ]); - } - closeClientFiles(); - - function openClientFiles(projectRoots: [ - string | undefined, - string | undefined, - string | undefined, - string | undefined - ]) { - files.forEach((file, index) => { - projectService.openClientFile(file.path, file.content, ts.ScriptKind.JS, projectRoots[index]); - }); - } + } + else { + verifyInferredProjectsState([ + [[files[3]], ts.ScriptTarget.ESNext], + [[files[0], files[1]], ts.ScriptTarget.ES2017], + [[files[2]], ts.ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + + function openClientFiles(projectRoots: [ + string | undefined, + string | undefined, + string | undefined, + string | undefined + ]) { + files.forEach((file, index) => { + projectService.openClientFile(file.path, file.content, ts.ScriptKind.JS, projectRoots[index]); + }); + } - function closeClientFiles() { - files.forEach(file => projectService.closeClientFile(file.path)); - } + function closeClientFiles() { + files.forEach(file => projectService.closeClientFile(file.path)); + } - function verifyInferredProjectsState(expected: [ - ts.projectSystem.File[], - ts.ScriptTarget - ][]) { - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: expected.length }); - projectService.inferredProjects.forEach((p, index) => { - const [actualFiles, target] = expected[index]; - checkInferredProject(p, actualFiles, target); - }); - } + function verifyInferredProjectsState(expected: [ + ts.projectSystem.File[], + ts.ScriptTarget + ][]) { + ts.projectSystem.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 sensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); - it("inferred projects per project root with case insensitive system", () => { - verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); - }); + 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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app.ts`, - content: `const app = 20;` - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const jsFile1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/jsFile1.js`, - content: `const jsFile1 = 10;` - }; - const jsFile2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/jsFile2.js`, - content: `const jsFile2 = 10;` - }; - const host = ts.projectSystem.createServerHost([appFile, ts.projectSystem.libFile, config, jsFile1, jsFile2]); - const projectService = ts.projectSystem.createProjectService(host); - const originalSet = projectService.configuredProjects.set; - const originalDelete = projectService.configuredProjects.delete; - const configuredCreated = new ts.Map(); - const configuredRemoved = new ts.Map(); - 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); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, ts.projectSystem.libFile.path]); - const project = projectService.configuredProjects.get(config.path)!; - ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); - checkConfiguredProjectCreatedAndNotDeleted(); - - // Do not remove config project when opening jsFile that is not present as part of config project - projectService.closeClientFile(jsFile1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - projectService.openClientFile(jsFile2.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); - checkConfiguredProjectNotCreatedAndNotDeleted(); - - // Do not remove config project when opening jsFile that is not present as part of config project - projectService.openClientFile(jsFile1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); - checkConfiguredProjectNotCreatedAndNotDeleted(); - - // When opening file that doesnt fall back to the config file, we remove the config project - projectService.openClientFile(ts.projectSystem.libFile.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, ts.projectSystem.libFile.path]); - checkConfiguredProjectNotCreatedButDeleted(); - - function checkConfiguredProjectCreatedAndNotDeleted() { - assert.equal(configuredCreated.size, 1); - assert.isTrue(configuredCreated.has(config.path)); - assert.equal(configuredRemoved.size, 0); - configuredCreated.clear(); - } + it("should still retain configured project created while opening the file", () => { + const appFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app.ts`, + content: `const app = 20;` + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const jsFile1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/jsFile1.js`, + content: `const jsFile1 = 10;` + }; + const jsFile2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/jsFile2.js`, + content: `const jsFile2 = 10;` + }; + const host = ts.projectSystem.createServerHost([appFile, ts.projectSystem.libFile, config, jsFile1, jsFile2]); + const projectService = ts.projectSystem.createProjectService(host); + const originalSet = projectService.configuredProjects.set; + const originalDelete = projectService.configuredProjects.delete; + const configuredCreated = new ts.Map(); + const configuredRemoved = new ts.Map(); + 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); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, ts.projectSystem.libFile.path]); + const project = projectService.configuredProjects.get(config.path)!; + ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); + checkConfiguredProjectCreatedAndNotDeleted(); + + // Do not remove config project when opening jsFile that is not present as part of config project + projectService.closeClientFile(jsFile1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + projectService.openClientFile(jsFile2.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); + checkConfiguredProjectNotCreatedAndNotDeleted(); + + // Do not remove config project when opening jsFile that is not present as part of config project + projectService.openClientFile(jsFile1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(project, [appFile.path, config.path, ts.projectSystem.libFile.path]); + checkConfiguredProjectNotCreatedAndNotDeleted(); + + // When opening file that doesnt fall back to the config file, we remove the config project + projectService.openClientFile(ts.projectSystem.libFile.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, ts.projectSystem.libFile.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, ts.projectSystem.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 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(); - } - }); + function checkConfiguredProjectNotCreatedButDeleted() { + assert.equal(configuredCreated.size, 0); + assert.equal(configuredRemoved.size, 1); + assert.isTrue(configuredRemoved.has(config.path)); + configuredRemoved.clear(); + } + }); - it("regression test - should infer typeAcquisition for inferred projects when set undefined", () => { - const file1 = { path: "/a/file1.js", content: "" }; - const host = ts.projectSystem.createServerHost([file1]); - const projectService = ts.projectSystem.createProjectService(host); + it("regression test - should infer typeAcquisition for inferred projects when set undefined", () => { + const file1 = { path: "/a/file1.js", content: "" }; + const host = ts.projectSystem.createServerHost([file1]); + const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); + projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const inferredProject = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(inferredProject, [file1.path]); - inferredProject.setTypeAcquisition(undefined); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const inferredProject = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(inferredProject, [file1.path]); + inferredProject.setTypeAcquisition(undefined); - const expected = { - enable: true, - include: [], - exclude: [] - }; - assert.deepEqual(inferredProject.getTypeAcquisition(), expected, "typeAcquisition should be inferred for inferred projects"); - }); + const expected = { + enable: true, + include: [], + exclude: [] + }; + assert.deepEqual(inferredProject.getTypeAcquisition(), expected, "typeAcquisition should be inferred for inferred projects"); + }); - it("Setting compiler options for inferred projects when there are no open files should not schedule any refresh", () => { - const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ts.ScriptTarget.ES2015 - }); - host.checkTimeoutQueueLength(0); + it("Setting compiler options for inferred projects when there are no open files should not schedule any refresh", () => { + const host = ts.projectSystem.createServerHost([ts.projectSystem.commonFile1, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ts.ScriptTarget.ES2015 }); + host.checkTimeoutQueueLength(0); }); +}); } diff --git a/src/testRunner/unittests/tsserver/inlayHints.ts b/src/testRunner/unittests/tsserver/inlayHints.ts index 94e738c8c762a..bd68a86feb1d5 100644 --- a/src/testRunner/unittests/tsserver/inlayHints.ts +++ b/src/testRunner/unittests/tsserver/inlayHints.ts @@ -1,63 +1,63 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: inlayHints", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const app: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "declare function foo(param: any): void;\nfoo(12);" - }; +describe("unittests:: tsserver:: inlayHints", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const app: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "declare function foo(param: any): void;\nfoo(12);" + }; - it("with updateOpen request does not corrupt documents", () => { - const host = ts.projectSystem.createServerHost([app, ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Open, - arguments: { file: app.path } - }); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Configure, - arguments: { - preferences: { - includeInlayParameterNameHints: "all" - } as ts.UserPreferences - } - }); - verifyInlayHintResponse(session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ fileName: app.path, textChanges: [{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 39 }, newText: "//" }] }] - } - }); - verifyInlayHintResponse(session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ fileName: app.path, textChanges: [{ start: { line: 1, offset: 41 }, end: { line: 1, offset: 41 }, newText: "c" }] }] - } - }); - verifyInlayHintResponse(session); - - function verifyInlayHintResponse(session: ts.projectSystem.TestSession) { - verifyParamInlayHint(session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.ProvideInlayHints, - arguments: { - file: app.path, - start: 0, - length: app.content.length, - } - }).response as ts.projectSystem.protocol.InlayHintItem[] | undefined); + it("with updateOpen request does not corrupt documents", () => { + const host = ts.projectSystem.createServerHost([app, ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Open, + arguments: { file: app.path } + }); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Configure, + arguments: { + preferences: { + includeInlayParameterNameHints: "all" + } as ts.UserPreferences } - - function verifyParamInlayHint(response: ts.projectSystem.protocol.InlayHintItem[] | undefined) { - ts.Debug.assert(response); - ts.Debug.assert(response[0]); - ts.Debug.assertEqual(response[0].text, "param:"); - ts.Debug.assertEqual(response[0].position.line, 2); - ts.Debug.assertEqual(response[0].position.offset, 5); + }); + verifyInlayHintResponse(session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ fileName: app.path, textChanges: [{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 39 }, newText: "//" }] }] + } + }); + verifyInlayHintResponse(session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ fileName: app.path, textChanges: [{ start: { line: 1, offset: 41 }, end: { line: 1, offset: 41 }, newText: "c" }] }] } }); + verifyInlayHintResponse(session); + + function verifyInlayHintResponse(session: ts.projectSystem.TestSession) { + verifyParamInlayHint(session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.ProvideInlayHints, + arguments: { + file: app.path, + start: 0, + length: app.content.length, + } + }).response as ts.projectSystem.protocol.InlayHintItem[] | undefined); + } + + function verifyParamInlayHint(response: ts.projectSystem.protocol.InlayHintItem[] | undefined) { + ts.Debug.assert(response); + ts.Debug.assert(response[0]); + ts.Debug.assertEqual(response[0].text, "param:"); + ts.Debug.assertEqual(response[0].position.line, 2); + ts.Debug.assertEqual(response[0].position.offset, 5); + } }); +}); } diff --git a/src/testRunner/unittests/tsserver/jsdocTag.ts b/src/testRunner/unittests/tsserver/jsdocTag.ts index a90fc1771d694..c52c1f65ad93f 100644 --- a/src/testRunner/unittests/tsserver/jsdocTag.ts +++ b/src/testRunner/unittests/tsserver/jsdocTag.ts @@ -1,8 +1,8 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: jsdoc @link ", () => { - const config: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: `{ +describe("unittests:: tsserver:: jsdoc @link ", () => { + const config: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: `{ "compilerOptions": { "checkJs": true, "noEmit": true @@ -10,134 +10,92 @@ namespace ts.projectSystem { "files": ["someFile1.js"] } ` - }; - function assertQuickInfoJSDoc(file: ts.projectSystem.File, options: { - displayPartsForJSDoc: boolean; - command: ts.projectSystem.protocol.CommandTypes; - tags: string | unknown[] | undefined; - documentation: string | unknown[]; - }) { + }; + function assertQuickInfoJSDoc(file: ts.projectSystem.File, options: { + displayPartsForJSDoc: boolean; + command: ts.projectSystem.protocol.CommandTypes; + tags: string | unknown[] | undefined; + documentation: string | unknown[]; + }) { - const { command, displayPartsForJSDoc, tags, documentation } = options; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([file, config])); - session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); - ts.projectSystem.openFilesForSession([file], session); - const indexOfX = file.content.indexOf("x"); - const quickInfo = session.executeCommandSeq({ - command: command as ts.projectSystem.protocol.CommandTypes.Quickinfo, - arguments: { - file: file.path, - position: indexOfX, - } as ts.projectSystem.protocol.FileLocationRequestArgs - }).response; - const summaryAndLocation = command === ts.projectSystem.protocol.CommandTypes.Quickinfo ? { - displayString: "var x: number", - start: { - line: 3, - offset: 5, - }, - end: { - line: 3, - offset: 6, - } - } : { - displayParts: [{ - kind: "keyword", - text: "var", - }, { - kind: "space", - text: " ", - }, { - kind: "localName", - text: "x", - }, { - kind: "punctuation", - text: ":", - }, { - kind: "space", - text: " ", - }, { - kind: "keyword", - text: "number", - }], - textSpan: { - length: 1, - start: 38, - } - }; - assert.deepEqual(quickInfo, { - kind: "var", - kindModifiers: "", - ...summaryAndLocation, - documentation, - tags - }); - } + const { command, displayPartsForJSDoc, tags, documentation } = options; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([file, config])); + session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); + ts.projectSystem.openFilesForSession([file], session); + const indexOfX = file.content.indexOf("x"); + const quickInfo = session.executeCommandSeq({ + command: command as ts.projectSystem.protocol.CommandTypes.Quickinfo, + arguments: { + file: file.path, + position: indexOfX, + } as ts.projectSystem.protocol.FileLocationRequestArgs + }).response; + const summaryAndLocation = command === ts.projectSystem.protocol.CommandTypes.Quickinfo ? { + displayString: "var x: number", + start: { + line: 3, + offset: 5, + }, + end: { + line: 3, + offset: 6, + } + } : { + displayParts: [{ + kind: "keyword", + text: "var", + }, { + kind: "space", + text: " ", + }, { + kind: "localName", + text: "x", + }, { + kind: "punctuation", + text: ":", + }, { + kind: "space", + text: " ", + }, { + kind: "keyword", + text: "number", + }], + textSpan: { + length: 1, + start: 38, + } + }; + assert.deepEqual(quickInfo, { + kind: "var", + kindModifiers: "", + ...summaryAndLocation, + documentation, + tags + }); + } - const linkInTag: ts.projectSystem.File = { - path: "/a/someFile1.js", - content: `class C { } + const linkInTag: ts.projectSystem.File = { + path: "/a/someFile1.js", + content: `class C { } /** @wat {@link C} */ var x = 1` - }; - const linkInComment: ts.projectSystem.File = { - path: "/a/someFile1.js", - content: `class C { } + }; + const linkInComment: ts.projectSystem.File = { + path: "/a/someFile1.js", + content: `class C { } /** {@link C} */ var x = 1 ;` - }; + }; - it("for quickinfo, should provide display parts plus a span for a working link in a tag", () => { - assertQuickInfoJSDoc(linkInTag, { - command: ts.projectSystem.protocol.CommandTypes.Quickinfo, - displayPartsForJSDoc: true, - documentation: [], - tags: [{ - name: "wat", - text: [{ - kind: "text", - text: "", - }, { - kind: "link", - text: "{@link ", - }, { - kind: "linkName", - target: { - end: { - line: 1, - offset: 12, - }, - file: "/a/someFile1.js", - start: { - line: 1, - offset: 1, - }, - }, - text: "C", - }, { - kind: "link", - text: "}", - }] - }], - }); - }); - it("for quickinfo, should provide a string for a working link in a tag", () => { - assertQuickInfoJSDoc(linkInTag, { - command: ts.projectSystem.protocol.CommandTypes.Quickinfo, - displayPartsForJSDoc: false, - documentation: "", - tags: [{ - name: "wat", - text: "{@link C}" - }], - }); - }); - it("for quickinfo, should provide display parts for a working link in a comment", () => { - assertQuickInfoJSDoc(linkInComment, { - command: ts.projectSystem.protocol.CommandTypes.Quickinfo, - displayPartsForJSDoc: true, - documentation: [{ + it("for quickinfo, should provide display parts plus a span for a working link in a tag", () => { + assertQuickInfoJSDoc(linkInTag, { + command: ts.projectSystem.protocol.CommandTypes.Quickinfo, + displayPartsForJSDoc: true, + documentation: [], + tags: [{ + name: "wat", + text: [{ kind: "text", text: "", }, { @@ -160,65 +118,69 @@ var x = 1 }, { kind: "link", text: "}", - }], - tags: [], - }); + }] + }], }); - it("for quickinfo, should provide a string for a working link in a comment", () => { - assertQuickInfoJSDoc(linkInComment, { - command: ts.projectSystem.protocol.CommandTypes.Quickinfo, - displayPartsForJSDoc: false, - documentation: "{@link C}", - tags: [], - }); + }); + it("for quickinfo, should provide a string for a working link in a tag", () => { + assertQuickInfoJSDoc(linkInTag, { + command: ts.projectSystem.protocol.CommandTypes.Quickinfo, + displayPartsForJSDoc: false, + documentation: "", + tags: [{ + name: "wat", + text: "{@link C}" + }], }); - - it("for quickinfo-full, should provide display parts plus a span for a working link in a tag", () => { - assertQuickInfoJSDoc(linkInTag, { - command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, - displayPartsForJSDoc: true, - documentation: [], - tags: [{ - name: "wat", - text: [{ - kind: "text", - text: "", - }, { - kind: "link", - text: "{@link ", - }, { - kind: "linkName", - target: { - fileName: "/a/someFile1.js", - textSpan: { - length: 11, - start: 0 - }, - }, - text: "C", - }, { - kind: "link", - text: "}", - }] - }], - }); + }); + it("for quickinfo, should provide display parts for a working link in a comment", () => { + assertQuickInfoJSDoc(linkInComment, { + command: ts.projectSystem.protocol.CommandTypes.Quickinfo, + displayPartsForJSDoc: true, + documentation: [{ + kind: "text", + text: "", + }, { + kind: "link", + text: "{@link ", + }, { + kind: "linkName", + target: { + end: { + line: 1, + offset: 12, + }, + file: "/a/someFile1.js", + start: { + line: 1, + offset: 1, + }, + }, + text: "C", + }, { + kind: "link", + text: "}", + }], + tags: [], }); - it("for quickinfo-full, should provide a string for a working link in a tag", () => { - assertQuickInfoJSDoc(linkInTag, { - command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, - displayPartsForJSDoc: false, - documentation: [], - tags: [{ - name: "wat", - text: "{@link C}" - }], - }); + }); + it("for quickinfo, should provide a string for a working link in a comment", () => { + assertQuickInfoJSDoc(linkInComment, { + command: ts.projectSystem.protocol.CommandTypes.Quickinfo, + displayPartsForJSDoc: false, + documentation: "{@link C}", + tags: [], }); - it("for quickinfo-full, should provide display parts plus a span for a working link in a comment", () => { - assertQuickInfoJSDoc(linkInComment, { - command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, - displayPartsForJSDoc: true, - documentation: [{ + }); + + it("for quickinfo-full, should provide display parts plus a span for a working link in a tag", () => { + assertQuickInfoJSDoc(linkInTag, { + command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, + displayPartsForJSDoc: true, + documentation: [], + tags: [{ + name: "wat", + text: [{ kind: "text", text: "", }, { @@ -237,195 +199,418 @@ var x = 1 }, { kind: "link", text: "}", - }], - tags: undefined, - }); + }] + }], }); - it("for quickinfo-full, should provide a string for a working link in a comment", () => { - assertQuickInfoJSDoc(linkInComment, { - command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, - displayPartsForJSDoc: false, - documentation: [{ - kind: "text", - text: "", - }, { - kind: "link", - text: "{@link ", - }, { - kind: "linkName", - target: { - fileName: "/a/someFile1.js", - textSpan: { - length: 11, - start: 0 - }, - }, - text: "C", - }, { - kind: "link", - text: "}", - }], - tags: [], - }); + }); + it("for quickinfo-full, should provide a string for a working link in a tag", () => { + assertQuickInfoJSDoc(linkInTag, { + command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, + displayPartsForJSDoc: false, + documentation: [], + tags: [{ + name: "wat", + text: "{@link C}" + }], }); - - function assertSignatureHelpJSDoc(options: { - displayPartsForJSDoc: boolean; - command: ts.projectSystem.protocol.CommandTypes; - documentation: string | unknown[]; - tags: unknown[]; - }) { - const linkInParamTag: ts.projectSystem.File = { - path: "/a/someFile1.js", - content: `class C { } -/** @param y - {@link C} */ -function x(y) { } -x(1)` - }; - - const { command, displayPartsForJSDoc, documentation, tags } = options; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([linkInParamTag, config])); - session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); - ts.projectSystem.openFilesForSession([linkInParamTag], session); - const indexOfX = linkInParamTag.content.lastIndexOf("1"); - const signatureHelp = session.executeCommandSeq({ - command: command as ts.projectSystem.protocol.CommandTypes.SignatureHelp, - arguments: { - triggerReason: { - kind: "invoked" + }); + it("for quickinfo-full, should provide display parts plus a span for a working link in a comment", () => { + assertQuickInfoJSDoc(linkInComment, { + command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, + displayPartsForJSDoc: true, + documentation: [{ + kind: "text", + text: "", + }, { + kind: "link", + text: "{@link ", + }, { + kind: "linkName", + target: { + fileName: "/a/someFile1.js", + textSpan: { + length: 11, + start: 0 }, - file: linkInParamTag.path, - position: indexOfX, - } as ts.projectSystem.protocol.SignatureHelpRequestArgs - }).response; - const applicableSpan = command === ts.projectSystem.protocol.CommandTypes.SignatureHelp ? { - end: { - line: 4, - offset: 4 }, - start: { - line: 4, - offset: 3 - } - } : { - length: 1, - start: 60 - }; - assert.deepEqual(signatureHelp, { - applicableSpan, - argumentCount: 1, - argumentIndex: 0, - selectedItemIndex: 0, - items: [{ - documentation: [], - isVariadic: false, - parameters: [{ - displayParts: [{ - kind: "parameterName", - text: "y" - }, { - kind: "punctuation", - text: ":" - }, { - kind: "space", - text: " " - }, { - kind: "keyword", - text: "any" - }], - documentation, - isOptional: false, - isRest: false, - name: "y" - }], - prefixDisplayParts: [ - { - kind: "functionName", - text: "x", - }, - { - kind: "punctuation", - text: "(", - }, - ], - separatorDisplayParts: [ - { - kind: "punctuation", - text: ",", - }, - { - kind: "space", - text: " ", - }, - ], - suffixDisplayParts: [ - { - kind: "punctuation", - text: ")", - }, - { - kind: "punctuation", - text: ":", - }, - { - kind: "space", - text: " ", - }, - { - kind: "keyword", - text: "void", - } - ], - tags, - }], - }); - } - it("for signature help, should provide a string for a working link in a comment", () => { - assertSignatureHelpJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.SignatureHelp, - displayPartsForJSDoc: false, - tags: [{ - name: "param", - text: "y - {@link C}" - }], - documentation: [{ + text: "C", + }, { + kind: "link", + text: "}", + }], + tags: undefined, + }); + }); + it("for quickinfo-full, should provide a string for a working link in a comment", () => { + assertQuickInfoJSDoc(linkInComment, { + command: ts.projectSystem.protocol.CommandTypes.QuickinfoFull, + displayPartsForJSDoc: false, + documentation: [{ kind: "text", - text: "- " + text: "", }, { kind: "link", - text: "{@link " + text: "{@link ", }, { kind: "linkName", target: { - file: "/a/someFile1.js", - start: { - line: 1, - offset: 1 + fileName: "/a/someFile1.js", + textSpan: { + length: 11, + start: 0 }, - end: { - line: 1, - offset: 12 - } }, - text: "C" + text: "C", }, { kind: "link", - text: "}" + text: "}", }], - }); + tags: [], }); - it("for signature help, should provide display parts for a working link in a comment", () => { - const tags = [{ + }); + + function assertSignatureHelpJSDoc(options: { + displayPartsForJSDoc: boolean; + command: ts.projectSystem.protocol.CommandTypes; + documentation: string | unknown[]; + tags: unknown[]; + }) { + const linkInParamTag: ts.projectSystem.File = { + path: "/a/someFile1.js", + content: `class C { } +/** @param y - {@link C} */ +function x(y) { } +x(1)` + }; + + const { command, displayPartsForJSDoc, documentation, tags } = options; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([linkInParamTag, config])); + session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); + ts.projectSystem.openFilesForSession([linkInParamTag], session); + const indexOfX = linkInParamTag.content.lastIndexOf("1"); + const signatureHelp = session.executeCommandSeq({ + command: command as ts.projectSystem.protocol.CommandTypes.SignatureHelp, + arguments: { + triggerReason: { + kind: "invoked" + }, + file: linkInParamTag.path, + position: indexOfX, + } as ts.projectSystem.protocol.SignatureHelpRequestArgs + }).response; + const applicableSpan = command === ts.projectSystem.protocol.CommandTypes.SignatureHelp ? { + end: { + line: 4, + offset: 4 + }, + start: { + line: 4, + offset: 3 + } + } : { + length: 1, + start: 60 + }; + assert.deepEqual(signatureHelp, { + applicableSpan, + argumentCount: 1, + argumentIndex: 0, + selectedItemIndex: 0, + items: [{ + documentation: [], + isVariadic: false, + parameters: [{ + displayParts: [{ + kind: "parameterName", + text: "y" + }, { + kind: "punctuation", + text: ":" + }, { + kind: "space", + text: " " + }, { + kind: "keyword", + text: "any" + }], + documentation, + isOptional: false, + isRest: false, + name: "y" + }], + prefixDisplayParts: [ + { + kind: "functionName", + text: "x", + }, + { + kind: "punctuation", + text: "(", + }, + ], + separatorDisplayParts: [ + { + kind: "punctuation", + text: ",", + }, + { + kind: "space", + text: " ", + }, + ], + suffixDisplayParts: [ + { + kind: "punctuation", + text: ")", + }, + { + kind: "punctuation", + text: ":", + }, + { + kind: "space", + text: " ", + }, + { + kind: "keyword", + text: "void", + } + ], + tags, + }], + }); + } + it("for signature help, should provide a string for a working link in a comment", () => { + assertSignatureHelpJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.SignatureHelp, + displayPartsForJSDoc: false, + tags: [{ + name: "param", + text: "y - {@link C}" + }], + documentation: [{ + kind: "text", + text: "- " + }, { + kind: "link", + text: "{@link " + }, { + kind: "linkName", + target: { + file: "/a/someFile1.js", + start: { + line: 1, + offset: 1 + }, + end: { + line: 1, + offset: 12 + } + }, + text: "C" + }, { + kind: "link", + text: "}" + }], + }); + }); + it("for signature help, should provide display parts for a working link in a comment", () => { + const tags = [{ + name: "param", + text: [{ + kind: "parameterName", + text: "y" + }, { + kind: "space", + text: " " + }, { + kind: "text", + text: "- " + }, { + kind: "link", + text: "{@link " + }, { + kind: "linkName", + target: { + file: "/a/someFile1.js", + start: { + line: 1, + offset: 1 + }, + end: { + line: 1, + offset: 12 + } + }, + text: "C" + }, { + kind: "link", + text: "}" + }] + }]; + assertSignatureHelpJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.SignatureHelp, + displayPartsForJSDoc: true, + tags, + documentation: tags[0].text.slice(2) + }); + }); + it("for signature help-full, should provide a string for a working link in a comment", () => { + assertSignatureHelpJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.SignatureHelpFull, + displayPartsForJSDoc: false, + tags: [{ + name: "param", + text: "y - {@link C}" + }], + documentation: [{ + kind: "text", + text: "- " + }, { + kind: "link", + text: "{@link " + }, { + kind: "linkName", + target: { + fileName: "/a/someFile1.js", + textSpan: { + length: 11, + start: 0 + } + }, + text: "C" + }, { + kind: "link", + text: "}" + }], + }); + }); + it("for signature help-full, should provide display parts for a working link in a comment", () => { + const tags = [{ + name: "param", + text: [{ + kind: "parameterName", + text: "y" + }, { + kind: "space", + text: " " + }, { + kind: "text", + text: "- " + }, { + kind: "link", + text: "{@link " + }, { + kind: "linkName", + target: { + fileName: "/a/someFile1.js", + textSpan: { + length: 11, + start: 0 + } + }, + text: "C" + }, { + kind: "link", + text: "}" + }] + }]; + assertSignatureHelpJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.SignatureHelpFull, + displayPartsForJSDoc: true, + tags, + documentation: tags[0].text.slice(2), + }); + }); + + function assertCompletionsJSDoc(options: { + displayPartsForJSDoc: boolean; + command: ts.projectSystem.protocol.CommandTypes; + tags: unknown[]; + }) { + const linkInParamJSDoc: ts.projectSystem.File = { + path: "/a/someFile1.js", + content: `class C { } +/** @param x - see {@link C} */ +function foo (x) { } +foo` + }; + const { command, displayPartsForJSDoc, tags } = options; + const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([linkInParamJSDoc, config])); + session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); + ts.projectSystem.openFilesForSession([linkInParamJSDoc], session); + const indexOfFoo = linkInParamJSDoc.content.lastIndexOf("fo"); + const completions = session.executeCommandSeq({ + command: command as ts.projectSystem.protocol.CommandTypes.CompletionDetails, + arguments: { + entryNames: ["foo"], + file: linkInParamJSDoc.path, + position: indexOfFoo, + } as ts.projectSystem.protocol.CompletionDetailsRequestArgs + }).response; + assert.deepEqual(completions, [{ + codeActions: undefined, + displayParts: [{ + kind: "keyword", + text: "function", + }, { + kind: "space", + text: " ", + }, { + kind: "functionName", + text: "foo", + }, { + kind: "punctuation", + text: "(", + }, { + kind: "parameterName", + text: "x", + }, { + kind: "punctuation", + text: ":", + }, { + kind: "space", + text: " ", + }, { + kind: "keyword", + text: "any", + }, { + kind: "punctuation", + text: ")", + }, { + kind: "punctuation", + text: ":", + }, { + kind: "space", + text: " ", + }, { + kind: "keyword", + text: "void", + }], + documentation: [], + kind: "function", + kindModifiers: "", + name: "foo", + source: undefined, + sourceDisplay: undefined, + tags, + }]); + } + it("for completions, should provide display parts for a working link in a comment", () => { + assertCompletionsJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, + displayPartsForJSDoc: true, + tags: [{ name: "param", text: [{ kind: "parameterName", - text: "y" + text: "x" }, { kind: "space", text: " " }, { kind: "text", - text: "- " + text: "- see " }, { kind: "link", text: "{@link " @@ -433,49 +618,13 @@ x(1)` kind: "linkName", target: { file: "/a/someFile1.js", - start: { + end: { line: 1, - offset: 1 + offset: 12, }, - end: { + start: { line: 1, - offset: 12 - } - }, - text: "C" - }, { - kind: "link", - text: "}" - }] - }]; - assertSignatureHelpJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.SignatureHelp, - displayPartsForJSDoc: true, - tags, - documentation: tags[0].text.slice(2) - }); - }); - it("for signature help-full, should provide a string for a working link in a comment", () => { - assertSignatureHelpJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.SignatureHelpFull, - displayPartsForJSDoc: false, - tags: [{ - name: "param", - text: "y - {@link C}" - }], - documentation: [{ - kind: "text", - text: "- " - }, { - kind: "link", - text: "{@link " - }, { - kind: "linkName", - target: { - fileName: "/a/someFile1.js", - textSpan: { - length: 11, - start: 0 + offset: 1, } }, text: "C" @@ -483,20 +632,34 @@ x(1)` kind: "link", text: "}" }], - }); + }], + }); + }); + it("for completions, should provide a string for a working link in a comment", () => { + assertCompletionsJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, + displayPartsForJSDoc: false, + tags: [{ + name: "param", + text: "x - see {@link C}", + }], }); - it("for signature help-full, should provide display parts for a working link in a comment", () => { - const tags = [{ + }); + it("for completions-full, should provide display parts for a working link in a comment", () => { + assertCompletionsJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, + displayPartsForJSDoc: true, + tags: [{ name: "param", text: [{ kind: "parameterName", - text: "y" + text: "x" }, { kind: "space", text: " " }, { kind: "text", - text: "- " + text: "- see " }, { kind: "link", text: "{@link " @@ -513,182 +676,19 @@ x(1)` }, { kind: "link", text: "}" - }] - }]; - assertSignatureHelpJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.SignatureHelpFull, - displayPartsForJSDoc: true, - tags, - documentation: tags[0].text.slice(2), - }); - }); - - function assertCompletionsJSDoc(options: { - displayPartsForJSDoc: boolean; - command: ts.projectSystem.protocol.CommandTypes; - tags: unknown[]; - }) { - const linkInParamJSDoc: ts.projectSystem.File = { - path: "/a/someFile1.js", - content: `class C { } -/** @param x - see {@link C} */ -function foo (x) { } -foo` - }; - const { command, displayPartsForJSDoc, tags } = options; - const session = ts.projectSystem.createSession(ts.projectSystem.createServerHost([linkInParamJSDoc, config])); - session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); - ts.projectSystem.openFilesForSession([linkInParamJSDoc], session); - const indexOfFoo = linkInParamJSDoc.content.lastIndexOf("fo"); - const completions = session.executeCommandSeq({ - command: command as ts.projectSystem.protocol.CommandTypes.CompletionDetails, - arguments: { - entryNames: ["foo"], - file: linkInParamJSDoc.path, - position: indexOfFoo, - } as ts.projectSystem.protocol.CompletionDetailsRequestArgs - }).response; - assert.deepEqual(completions, [{ - codeActions: undefined, - displayParts: [{ - kind: "keyword", - text: "function", - }, { - kind: "space", - text: " ", - }, { - kind: "functionName", - text: "foo", - }, { - kind: "punctuation", - text: "(", - }, { - kind: "parameterName", - text: "x", - }, { - kind: "punctuation", - text: ":", - }, { - kind: "space", - text: " ", - }, { - kind: "keyword", - text: "any", - }, { - kind: "punctuation", - text: ")", - }, { - kind: "punctuation", - text: ":", - }, { - kind: "space", - text: " ", - }, { - kind: "keyword", - text: "void", }], - documentation: [], - kind: "function", - kindModifiers: "", - name: "foo", - source: undefined, - sourceDisplay: undefined, - tags, - }]); - } - it("for completions, should provide display parts for a working link in a comment", () => { - assertCompletionsJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, - displayPartsForJSDoc: true, - tags: [{ - name: "param", - text: [{ - kind: "parameterName", - text: "x" - }, { - kind: "space", - text: " " - }, { - kind: "text", - text: "- see " - }, { - kind: "link", - text: "{@link " - }, { - kind: "linkName", - target: { - file: "/a/someFile1.js", - end: { - line: 1, - offset: 12, - }, - start: { - line: 1, - offset: 1, - } - }, - text: "C" - }, { - kind: "link", - text: "}" - }], - }], - }); - }); - it("for completions, should provide a string for a working link in a comment", () => { - assertCompletionsJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.CompletionDetails, - displayPartsForJSDoc: false, - tags: [{ - name: "param", - text: "x - see {@link C}", - }], - }); - }); - it("for completions-full, should provide display parts for a working link in a comment", () => { - assertCompletionsJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, - displayPartsForJSDoc: true, - tags: [{ - name: "param", - text: [{ - kind: "parameterName", - text: "x" - }, { - kind: "space", - text: " " - }, { - kind: "text", - text: "- see " - }, { - kind: "link", - text: "{@link " - }, { - kind: "linkName", - target: { - fileName: "/a/someFile1.js", - textSpan: { - length: 11, - start: 0 - } - }, - text: "C" - }, { - kind: "link", - text: "}" - }], - }], - }); + }], }); - it("for completions-full, should provide a string for a working link in a comment", () => { - assertCompletionsJSDoc({ - command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, - displayPartsForJSDoc: false, - tags: [{ - name: "param", - text: "x - see {@link C}", - }], - }); + }); + it("for completions-full, should provide a string for a working link in a comment", () => { + assertCompletionsJSDoc({ + command: ts.projectSystem.protocol.CommandTypes.CompletionDetailsFull, + displayPartsForJSDoc: false, + tags: [{ + name: "param", + text: "x - see {@link C}", + }], }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/languageService.ts b/src/testRunner/unittests/tsserver/languageService.ts index 898a542826b3a..dedfc4a907814 100644 --- a/src/testRunner/unittests/tsserver/languageService.ts +++ b/src/testRunner/unittests/tsserver/languageService.ts @@ -1,68 +1,68 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: languageService", () => { - 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 = ts.projectSystem.createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - projectService.inferredProjects[0].getLanguageService().getProgram(); - }); +describe("unittests:: tsserver:: languageService", () => { + 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 = ts.projectSystem.createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + projectService.inferredProjects[0].getLanguageService().getProgram(); + }); - it("should support multiple projects with the same file under differing `paths` settings", () => { - const files = [ - { - path: "/project/shared.ts", - content: Utils.dedent` + it("should support multiple projects with the same file under differing `paths` settings", () => { + const files = [ + { + path: "/project/shared.ts", + content: Utils.dedent` import {foo_a} from "foo"; ` - }, - { - path: `/project/a/tsconfig.json`, - content: `{ "compilerOptions": { "paths": { "foo": ["./foo.d.ts"] } }, "files": ["./index.ts", "./foo.d.ts"] }` - }, - { - path: `/project/a/foo.d.ts`, - content: Utils.dedent` + }, + { + path: `/project/a/tsconfig.json`, + content: `{ "compilerOptions": { "paths": { "foo": ["./foo.d.ts"] } }, "files": ["./index.ts", "./foo.d.ts"] }` + }, + { + path: `/project/a/foo.d.ts`, + content: Utils.dedent` export const foo_a = 1; ` - }, - { - path: "/project/a/index.ts", - content: `import "../shared";` - }, - { - path: `/project/b/tsconfig.json`, - content: `{ "compilerOptions": { "paths": { "foo": ["./foo.d.ts"] } }, "files": ["./index.ts", "./foo.d.ts"] }` - }, - { - path: `/project/b/foo.d.ts`, - content: Utils.dedent` + }, + { + path: "/project/a/index.ts", + content: `import "../shared";` + }, + { + path: `/project/b/tsconfig.json`, + content: `{ "compilerOptions": { "paths": { "foo": ["./foo.d.ts"] } }, "files": ["./index.ts", "./foo.d.ts"] }` + }, + { + path: `/project/b/foo.d.ts`, + content: Utils.dedent` export const foo_b = 1; ` - }, - { - path: "/project/b/index.ts", - content: `import "../shared";` - } - ]; + }, + { + path: "/project/b/index.ts", + content: `import "../shared";` + } + ]; - const host = ts.projectSystem.createServerHost(files, { executingFilePath: "/project/tsc.js", useCaseSensitiveFileNames: true }); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(files[3].path); - projectService.openClientFile(files[6].path); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - const proj1Diags = projectService.configuredProjects.get(files[1].path)!.getLanguageService().getProgram()!.getSemanticDiagnostics(); - ts.Debug.assertEqual(proj1Diags.length, 0); - const proj2Diags = projectService.configuredProjects.get(files[4].path)!.getLanguageService().getProgram()!.getSemanticDiagnostics(); - ts.Debug.assertEqual(proj2Diags.length, 1); - }); + const host = ts.projectSystem.createServerHost(files, { executingFilePath: "/project/tsc.js", useCaseSensitiveFileNames: true }); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(files[3].path); + projectService.openClientFile(files[6].path); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + const proj1Diags = projectService.configuredProjects.get(files[1].path)!.getLanguageService().getProgram()!.getSemanticDiagnostics(); + ts.Debug.assertEqual(proj1Diags.length, 0); + const proj2Diags = projectService.configuredProjects.get(files[4].path)!.getLanguageService().getProgram()!.getSemanticDiagnostics(); + ts.Debug.assertEqual(proj2Diags.length, 1); }); +}); } diff --git a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts index 47ea8f170aa66..5dd82acc6e8e3 100644 --- a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts +++ b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts @@ -1,55 +1,55 @@ 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: ts.projectSystem.File = { - path: "/a/b/file1.js", - content: `var t = require("test"); t.` - }; - const moduleFile: ts.projectSystem.File = { - path: "/a/b/node_modules/test/index.js", - content: `var v = 10; module.exports = v;` - }; - - const host = ts.projectSystem.createServerHost([file1, moduleFile]); - const projectService = ts.projectSystem.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: ts.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 = ts.projectSystem.createServerHost([file1, file2, ts.projectSystem.libFile]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); - - projectService.openClientFile(file1.path); - ts.projectSystem.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); - }); +describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => { + it("should be set to 2 if the project has js root files", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.js", + content: `var t = require("test"); t.` + }; + const moduleFile: ts.projectSystem.File = { + path: "/a/b/node_modules/test/index.js", + content: `var v = 10; module.exports = v;` + }; + + const host = ts.projectSystem.createServerHost([file1, moduleFile]); + const projectService = ts.projectSystem.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: ts.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 = ts.projectSystem.createServerHost([file1, file2, ts.projectSystem.libFile]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true }); + + projectService.openClientFile(file1.path); + ts.projectSystem.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 081264ac9bb11..da5720f806497 100644 --- a/src/testRunner/unittests/tsserver/metadataInResponse.ts +++ b/src/testRunner/unittests/tsserver/metadataInResponse.ts @@ -1,103 +1,103 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: with metadata in response", () => { - const metadata = "Extra Info"; - function verifyOutput(host: ts.projectSystem.TestServerHost, expectedResponse: ts.projectSystem.protocol.Response) { - const output = host.getOutput().map(ts.projectSystem.mapOutputToJson); - assert.deepEqual(output, [expectedResponse]); - host.clearOutput(); - } +describe("unittests:: tsserver:: with metadata in response", () => { + const metadata = "Extra Info"; + function verifyOutput(host: ts.projectSystem.TestServerHost, expectedResponse: ts.projectSystem.protocol.Response) { + const output = host.getOutput().map(ts.projectSystem.mapOutputToJson); + assert.deepEqual(output, [expectedResponse]); + host.clearOutput(); + } - function verifyCommandWithMetadata(session: ts.projectSystem.TestSession, host: ts.projectSystem.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." }); - } + function verifyCommandWithMetadata(session: ts.projectSystem.TestSession, host: ts.projectSystem.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: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { plugins: [{ name: "myplugin" }] } - }) - }; - function createHostWithPlugin(files: readonly ts.projectSystem.File[]) { - const host = ts.projectSystem.createServerHost(files); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: ts.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 - }; + const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { plugins: [{ name: "myplugin" }] } + }) + }; + function createHostWithPlugin(files: readonly ts.projectSystem.File[]) { + const host = ts.projectSystem.createServerHost(files); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: ts.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; - } + }; + return host; + } - describe("With completion requests", () => { - const completionRequestArgs: ts.projectSystem.protocol.CompletionsRequestArgs = { - file: aTs.path, - line: 1, - offset: aTs.content.indexOf("this.") + 1 + "this.".length - }; - const expectedCompletionEntries: readonly ts.projectSystem.protocol.CompletionEntry[] = [ - { name: "foo", kind: ts.ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority }, - { name: "prop", kind: ts.ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority } - ]; + describe("With completion requests", () => { + const completionRequestArgs: ts.projectSystem.protocol.CompletionsRequestArgs = { + file: aTs.path, + line: 1, + offset: aTs.content.indexOf("this.") + 1 + "this.".length + }; + const expectedCompletionEntries: readonly ts.projectSystem.protocol.CompletionEntry[] = [ + { name: "foo", kind: ts.ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority }, + { name: "prop", kind: ts.ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: ts.Completions.SortText.LocationPriority } + ]; - it("can pass through metadata when the command returns array", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: ts.projectSystem.protocol.CommandTypes.Completions, - arguments: completionRequestArgs - }, expectedCompletionEntries); - }); + it("can pass through metadata when the command returns array", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: ts.projectSystem.protocol.CommandTypes.Completions, + arguments: completionRequestArgs + }, expectedCompletionEntries); + }); - it("can pass through metadata when the command returns object", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, - arguments: completionRequestArgs - }, { - flags: 0, - isGlobalCompletion: false, - isMemberCompletion: true, - isNewIdentifierLocation: false, - optionalReplacementSpan: { - start: { line: 1, offset: aTs.content.indexOf("prop;") + 1 }, - end: { line: 1, offset: aTs.content.indexOf("prop;") + 1 + "prop".length } - }, - entries: expectedCompletionEntries - }); + it("can pass through metadata when the command returns object", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: ts.projectSystem.protocol.CommandTypes.CompletionInfo, + arguments: completionRequestArgs + }, { + flags: 0, + isGlobalCompletion: false, + isMemberCompletion: true, + isNewIdentifierLocation: false, + optionalReplacementSpan: { + start: { line: 1, offset: aTs.content.indexOf("prop;") + 1 }, + end: { line: 1, offset: aTs.content.indexOf("prop;") + 1 + "prop".length } + }, + entries: expectedCompletionEntries }); + }); - it("returns undefined correctly", () => { - const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; - const host = createHostWithPlugin([aTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: ts.projectSystem.protocol.CommandTypes.Completions, - arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } - }, /*expectedResponseBody*/ undefined); - }); + it("returns undefined correctly", () => { + const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; + const host = createHostWithPlugin([aTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: ts.projectSystem.protocol.CommandTypes.Completions, + arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } + }, /*expectedResponseBody*/ undefined); }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts index aaaf72f1a4ef9..e4193a15b7352 100644 --- a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts +++ b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts @@ -1,150 +1,150 @@ namespace ts.projectSystem { - const packageJson: ts.projectSystem.File = { - path: "/package.json", - content: `{ "dependencies": { "mobx": "*" } }` - }; - const aTs: ts.projectSystem.File = { - path: "/src/a.ts", - content: "export const foo = 0;", - }; - const bTs: ts.projectSystem.File = { - path: "/src/b.ts", - content: "foo", - }; - const cTs: ts.projectSystem.File = { - path: "/src/c.ts", - content: "import ", - }; - const bSymlink: ts.projectSystem.SymLink = { - path: "/src/b-link.ts", - symLink: "./b.ts", - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: `{ "include": ["src"] }`, - }; - const ambientDeclaration: ts.projectSystem.File = { - path: "/src/ambient.d.ts", - content: "declare module 'ambient' {}" - }; - const mobxPackageJson: ts.projectSystem.File = { - path: "/node_modules/mobx/package.json", - content: `{ "name": "mobx", "version": "1.0.0" }` - }; - const mobxDts: ts.projectSystem.File = { - path: "/node_modules/mobx/index.d.ts", - content: "export declare function observable(): unknown;" - }; +const packageJson: ts.projectSystem.File = { + path: "/package.json", + content: `{ "dependencies": { "mobx": "*" } }` +}; +const aTs: ts.projectSystem.File = { + path: "/src/a.ts", + content: "export const foo = 0;", +}; +const bTs: ts.projectSystem.File = { + path: "/src/b.ts", + content: "foo", +}; +const cTs: ts.projectSystem.File = { + path: "/src/c.ts", + content: "import ", +}; +const bSymlink: ts.projectSystem.SymLink = { + path: "/src/b-link.ts", + symLink: "./b.ts", +}; +const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: `{ "include": ["src"] }`, +}; +const ambientDeclaration: ts.projectSystem.File = { + path: "/src/ambient.d.ts", + content: "declare module 'ambient' {}" +}; +const mobxPackageJson: ts.projectSystem.File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` +}; +const mobxDts: ts.projectSystem.File = { + path: "/node_modules/mobx/index.d.ts", + content: "export declare function observable(): unknown;" +}; - describe("unittests:: tsserver:: moduleSpecifierCache", () => { - it("caches importability within a file", () => { - const { moduleSpecifierCache } = setup(); - assert.isFalse(moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, {}, {})?.isBlockedByPackageJsonDependencies); - }); - - it("caches module specifiers within a file", () => { - const { moduleSpecifierCache, triggerCompletions } = setup(); - // Completion at an import statement will calculate and cache module specifiers - triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); - const mobxCache = moduleSpecifierCache.get(cTs.path as ts.Path, mobxDts.path as ts.Path, {}, {}); - assert.deepEqual(mobxCache, { - modulePaths: [{ - path: mobxDts.path, - isInNodeModules: true, - isRedirect: false - }], - moduleSpecifiers: ["mobx"], - isBlockedByPackageJsonDependencies: false, - }); - }); - - it("invalidates module specifiers when changes happen in contained node_modules directories", () => { - const { host, moduleSpecifierCache, triggerCompletions } = setup(); - // Completion at an import statement will calculate and cache module specifiers - triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); - ts.projectSystem.checkWatchedDirectories(host, ["/src", "/node_modules"], /*recursive*/ true); - host.writeFile("/node_modules/.staging/mobx-12345678/package.json", "{}"); - host.runQueuedTimeoutCallbacks(); - assert.equal(moduleSpecifierCache.count(), 0); - }); +describe("unittests:: tsserver:: moduleSpecifierCache", () => { + it("caches importability within a file", () => { + const { moduleSpecifierCache } = setup(); + assert.isFalse(moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, {}, {})?.isBlockedByPackageJsonDependencies); + }); - it("does not invalidate the cache when new files are added", () => { - const { host, moduleSpecifierCache } = setup(); - host.writeFile("/src/a2.ts", aTs.content); - host.runQueuedTimeoutCallbacks(); - assert.isFalse(moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, {}, {})?.isBlockedByPackageJsonDependencies); + it("caches module specifiers within a file", () => { + const { moduleSpecifierCache, triggerCompletions } = setup(); + // Completion at an import statement will calculate and cache module specifiers + triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); + const mobxCache = moduleSpecifierCache.get(cTs.path as ts.Path, mobxDts.path as ts.Path, {}, {}); + assert.deepEqual(mobxCache, { + modulePaths: [{ + path: mobxDts.path, + isInNodeModules: true, + isRedirect: false + }], + moduleSpecifiers: ["mobx"], + isBlockedByPackageJsonDependencies: false, }); + }); - it("invalidates the cache when symlinks are added or removed", () => { - const { host, moduleSpecifierCache } = setup(); - host.renameFile(bSymlink.path, "/src/b-link2.ts"); - host.runQueuedTimeoutCallbacks(); - assert.equal(moduleSpecifierCache.count(), 0); - }); + it("invalidates module specifiers when changes happen in contained node_modules directories", () => { + const { host, moduleSpecifierCache, triggerCompletions } = setup(); + // Completion at an import statement will calculate and cache module specifiers + triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); + ts.projectSystem.checkWatchedDirectories(host, ["/src", "/node_modules"], /*recursive*/ true); + host.writeFile("/node_modules/.staging/mobx-12345678/package.json", "{}"); + host.runQueuedTimeoutCallbacks(); + assert.equal(moduleSpecifierCache.count(), 0); + }); - it("invalidates the cache when local package.json changes", () => { - const { host, moduleSpecifierCache } = setup(); - host.writeFile("/package.json", `{}`); - host.runQueuedTimeoutCallbacks(); - assert.equal(moduleSpecifierCache.count(), 0); - }); + it("does not invalidate the cache when new files are added", () => { + const { host, moduleSpecifierCache } = setup(); + host.writeFile("/src/a2.ts", aTs.content); + host.runQueuedTimeoutCallbacks(); + assert.isFalse(moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, {}, {})?.isBlockedByPackageJsonDependencies); + }); - it("invalidates the cache when module resolution settings change", () => { - const { host, moduleSpecifierCache } = setup(); - host.writeFile(tsconfig.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "include": ["src"] }`); - host.runQueuedTimeoutCallbacks(); - assert.equal(moduleSpecifierCache.count(), 0); - }); + it("invalidates the cache when symlinks are added or removed", () => { + const { host, moduleSpecifierCache } = setup(); + host.renameFile(bSymlink.path, "/src/b-link2.ts"); + host.runQueuedTimeoutCallbacks(); + assert.equal(moduleSpecifierCache.count(), 0); + }); - it("invalidates the cache when user preferences change", () => { - const { moduleSpecifierCache, session, triggerCompletions } = setup(); - const preferences: ts.UserPreferences = { importModuleSpecifierPreference: "project-relative" }; + it("invalidates the cache when local package.json changes", () => { + const { host, moduleSpecifierCache } = setup(); + host.writeFile("/package.json", `{}`); + host.runQueuedTimeoutCallbacks(); + assert.equal(moduleSpecifierCache.count(), 0); + }); - assert.ok(getWithPreferences({})); - ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Configure, { preferences }); - // Nothing changes yet - assert.ok(getWithPreferences({})); - assert.isUndefined(getWithPreferences(preferences)); - // Completions will request (getting nothing) and set the cache with new preferences - triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); - assert.isUndefined(getWithPreferences({})); - assert.ok(getWithPreferences(preferences)); + it("invalidates the cache when module resolution settings change", () => { + const { host, moduleSpecifierCache } = setup(); + host.writeFile(tsconfig.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "include": ["src"] }`); + host.runQueuedTimeoutCallbacks(); + assert.equal(moduleSpecifierCache.count(), 0); + }); - // Test other affecting preference - ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Configure, { - preferences: { importModuleSpecifierEnding: "js" }, - }); - triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); - assert.isUndefined(getWithPreferences(preferences)); + it("invalidates the cache when user preferences change", () => { + const { moduleSpecifierCache, session, triggerCompletions } = setup(); + const preferences: ts.UserPreferences = { importModuleSpecifierPreference: "project-relative" }; - function getWithPreferences(preferences: ts.UserPreferences) { - return moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, preferences, {}); - } - }); - }); + assert.ok(getWithPreferences({})); + ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Configure, { preferences }); + // Nothing changes yet + assert.ok(getWithPreferences({})); + assert.isUndefined(getWithPreferences(preferences)); + // Completions will request (getting nothing) and set the cache with new preferences + triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); + assert.isUndefined(getWithPreferences({})); + assert.ok(getWithPreferences(preferences)); - function setup() { - const host = ts.projectSystem.createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs, bTs, cTs], session); - const projectService = session.getProjectService(); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); + // Test other affecting preference ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Configure, { - preferences: { - includeCompletionsForImportStatements: true, - includeCompletionsForModuleExports: true, - includeCompletionsWithInsertText: true, - includeCompletionsWithSnippetText: true, - }, + preferences: { importModuleSpecifierEnding: "js" }, }); triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); + assert.isUndefined(getWithPreferences(preferences)); - return { host, project, projectService, session, moduleSpecifierCache: project.getModuleSpecifierCache(), triggerCompletions }; - - function triggerCompletions(requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs) { - ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - }); + function getWithPreferences(preferences: ts.UserPreferences) { + return moduleSpecifierCache.get(bTs.path as ts.Path, aTs.path as ts.Path, preferences, {}); } + }); +}); + +function setup() { + const host = ts.projectSystem.createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs, bTs, cTs], session); + const projectService = session.getProjectService(); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.Configure, { + preferences: { + includeCompletionsForImportStatements: true, + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + }, + }); + triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); + + return { host, project, projectService, session, moduleSpecifierCache: project.getModuleSpecifierCache(), triggerCompletions }; + + function triggerCompletions(requestLocation: ts.projectSystem.protocol.FileLocationRequestArgs) { + ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + }); } } +} diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts index 01be0002cfe7a..ca976b513f0b3 100644 --- a/src/testRunner/unittests/tsserver/navTo.ts +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -1,52 +1,52 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: navigate-to for javascript project", () => { - function findNavToItem(items: ts.projectSystem.protocol.NavtoItem[], itemName: string, itemKind: string) { - return ts.find(items, item => item.name === itemName && item.kind === itemKind); - } +describe("unittests:: tsserver:: navigate-to for javascript project", () => { + function findNavToItem(items: ts.projectSystem.protocol.NavtoItem[], itemName: string, itemKind: string) { + return ts.find(items, item => item.name === itemName && item.kind === itemKind); + } - function containsNavToItem(items: ts.projectSystem.protocol.NavtoItem[], itemName: string, itemKind: string) { - return findNavToItem(items, itemName, itemKind) !== undefined; - } + function containsNavToItem(items: ts.projectSystem.protocol.NavtoItem[], itemName: string, itemKind: string) { + return findNavToItem(items, itemName, itemKind) !== undefined; + } - it("should not include type symbols", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.js", - content: "function foo() {}" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/jsconfig.json", - content: "{}" - }; - const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1], session); + it("should not include type symbols", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.js", + content: "function foo() {}" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/jsconfig.json", + content: "{}" + }; + const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); - // Try to find some interface type defined in lib.d.ts - const libTypeNavToRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); - const items = session.executeCommand(libTypeNavToRequest).response as ts.projectSystem.protocol.NavtoItem[]; - assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); + // Try to find some interface type defined in lib.d.ts + const libTypeNavToRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); + const items = session.executeCommand(libTypeNavToRequest).response as ts.projectSystem.protocol.NavtoItem[]; + assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); - const localFunctionNavToRequst = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items2 = session.executeCommand(localFunctionNavToRequst).response as ts.projectSystem.protocol.NavtoItem[]; - assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); - }); + const localFunctionNavToRequst = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); + const items2 = session.executeCommand(localFunctionNavToRequst).response as ts.projectSystem.protocol.NavtoItem[]; + assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); + }); - it("should de-duplicate symbols", () => { - const configFile1: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: `{ + it("should de-duplicate symbols", () => { + const configFile1: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: `{ "compilerOptions": { "composite": true } }` - }; - const file1: ts.projectSystem.File = { - path: "/a/index.ts", - content: "export const abcdef = 1;" - }; - const configFile2: ts.projectSystem.File = { - path: "/b/tsconfig.json", - content: `{ + }; + const file1: ts.projectSystem.File = { + path: "/a/index.ts", + content: "export const abcdef = 1;" + }; + const configFile2: ts.projectSystem.File = { + path: "/b/tsconfig.json", + content: `{ "compilerOptions": { "composite": true }, @@ -54,43 +54,43 @@ namespace ts.projectSystem { { "path": "../a" } ] }` - }; - const file2: ts.projectSystem.File = { - path: "/b/index.ts", - content: `import a = require("../a"); + }; + const file2: ts.projectSystem.File = { + path: "/b/index.ts", + content: `import a = require("../a"); export const ghijkl = a.abcdef;` - }; - const host = ts.projectSystem.createServerHost([configFile1, file1, configFile2, file2]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file1, file2], session); - const request = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "abcdef", file: file1.path }); - session.executeCommand(request).response as ts.projectSystem.protocol.NavtoItem[]; - ts.projectSystem.baselineTsserverLogs("navTo", "should de-duplicate symbols", session); - }); + }; + const host = ts.projectSystem.createServerHost([configFile1, file1, configFile2, file2]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file1, file2], session); + const request = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "abcdef", file: file1.path }); + session.executeCommand(request).response as ts.projectSystem.protocol.NavtoItem[]; + ts.projectSystem.baselineTsserverLogs("navTo", "should de-duplicate symbols", session); + }); - it("should de-duplicate symbols when searching all projects", () => { - const solutionConfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - references: [{ path: "./a" }, { path: "./b" }], - files: [], - }) - }; - const configFile1: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: `{ + it("should de-duplicate symbols when searching all projects", () => { + const solutionConfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + references: [{ path: "./a" }, { path: "./b" }], + files: [], + }) + }; + const configFile1: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: `{ "compilerOptions": { "composite": true } }` - }; - const file1: ts.projectSystem.File = { - path: "/a/index.ts", - content: "export const abcdef = 1;" - }; - const configFile2: ts.projectSystem.File = { - path: "/b/tsconfig.json", - content: `{ + }; + const file1: ts.projectSystem.File = { + path: "/a/index.ts", + content: "export const abcdef = 1;" + }; + const configFile2: ts.projectSystem.File = { + path: "/b/tsconfig.json", + content: `{ "compilerOptions": { "composite": true }, @@ -98,39 +98,39 @@ export const ghijkl = a.abcdef;` { "path": "../a" } ] }` - }; - const file2: ts.projectSystem.File = { - path: "/b/index.ts", - content: `import a = require("../a"); + }; + const file2: ts.projectSystem.File = { + path: "/b/index.ts", + content: `import a = require("../a"); export const ghijkl = a.abcdef;` - }; - const host = ts.projectSystem.createServerHost([configFile1, file1, configFile2, file2, solutionConfig]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file1], session); - const request = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "abcdef" }); - session.executeCommand(request).response as ts.projectSystem.protocol.NavtoItem[]; - ts.projectSystem.baselineTsserverLogs("navTo", "should de-duplicate symbols when searching all projects", session); - }); + }; + const host = ts.projectSystem.createServerHost([configFile1, file1, configFile2, file2, solutionConfig]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file1], session); + const request = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "abcdef" }); + session.executeCommand(request).response as ts.projectSystem.protocol.NavtoItem[]; + ts.projectSystem.baselineTsserverLogs("navTo", "should de-duplicate symbols when searching all projects", session); + }); - it("should work with Deprecated", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.js", - content: "/** @deprecated */\nfunction foo () {}" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/jsconfig.json", - content: "{}" - }; - const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1], session); + it("should work with Deprecated", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.js", + content: "/** @deprecated */\nfunction foo () {}" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/jsconfig.json", + content: "{}" + }; + const host = ts.projectSystem.createServerHost([file1, configFile, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); - // Try to find some interface type defined in lib.d.ts - const libTypeNavToRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items = session.executeCommand(libTypeNavToRequest).response as ts.projectSystem.protocol.NavtoItem[]; - const fooItem = findNavToItem(items, "foo", "function"); - assert.isNotNull(fooItem, `Cannot find function symbol "foo".`); - assert.isTrue(fooItem?.kindModifiers?.includes("deprecated")); - }); + // Try to find some interface type defined in lib.d.ts + const libTypeNavToRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); + const items = session.executeCommand(libTypeNavToRequest).response as ts.projectSystem.protocol.NavtoItem[]; + const fooItem = findNavToItem(items, "foo", "function"); + assert.isNotNull(fooItem, `Cannot find function symbol "foo".`); + assert.isTrue(fooItem?.kindModifiers?.includes("deprecated")); }); +}); } diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts index 2e6d66614b66b..cd33afd99f27a 100644 --- a/src/testRunner/unittests/tsserver/occurences.ts +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -1,38 +1,38 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: occurrence highlight on string", () => { - it("should be marked if only on string values", () => { - const file1: ts.projectSystem.File = { - path: "/a/b/file1.ts", - content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` - }; +describe("unittests:: tsserver:: occurrence highlight on string", () => { + it("should be marked if only on string values", () => { + const file1: ts.projectSystem.File = { + path: "/a/b/file1.ts", + content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` + }; - const host = ts.projectSystem.createServerHost([file1]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); + const host = ts.projectSystem.createServerHost([file1]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); - projectService.openClientFile(file1.path); - { - const highlightRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 1, offset: 11 }); - const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.protocol.OccurrencesResponseItem[]; - const firstOccurence = highlightResponse[0]; - assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); - } + projectService.openClientFile(file1.path); + { + const highlightRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 1, offset: 11 }); + const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.protocol.OccurrencesResponseItem[]; + const firstOccurence = highlightResponse[0]; + assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); + } - { - const highlightRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 3, offset: 13 }); - const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.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 = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 3, offset: 13 }); + const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.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 = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 4, offset: 14 }); - const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.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"); - } - }); + { + const highlightRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Occurrences, { file: file1.path, line: 4, offset: 14 }); + const highlightResponse = session.executeCommand(highlightRequest).response as ts.projectSystem.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 36f93cb54a379..3c968a0593b15 100644 --- a/src/testRunner/unittests/tsserver/openFile.ts +++ b/src/testRunner/unittests/tsserver/openFile.ts @@ -1,138 +1,138 @@ 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 = ts.projectSystem.createServerHost([f]); - const projectService = ts.projectSystem.createProjectService(host); - // create a project - projectService.openExternalProject({ projectFileName, rootFiles: [ts.projectSystem.toExternalFile(f.path)], options: {} }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); +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 = ts.projectSystem.createServerHost([f]); + const projectService = ts.projectSystem.createProjectService(host); + // create a project + projectService.openExternalProject({ projectFileName, rootFiles: [ts.projectSystem.toExternalFile(f.path)], options: {} }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); - const p = projectService.externalProjects[0]; - // force to load the content of the file - p.updateGraph(); + 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); + 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: ts.IScriptSnapshot, expectedLength: number) { - assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); - } + // open project and replace its content with empty string + projectService.openClientFile(f.path, ""); + checkSnapLength(scriptInfo.getSnapshot(), 0); + }); + function checkSnapLength(snap: ts.IScriptSnapshot, expectedLength: number) { + assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); + } - function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { - const file1: ts.projectSystem.File = { - path: "/a/b/src/app.ts", - content: "let x = 10;" - }; - const file2: ts.projectSystem.File = { - path: "/a/B/lib/module2.ts", - content: "let z = 10;" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: "" - }; - const configFile2: ts.projectSystem.File = { - path: "/a/tsconfig.json", - content: "" - }; - const host = ts.projectSystem.createServerHost([file1, file2, configFile, configFile2], { - useCaseSensitiveFileNames - }); - const service = ts.projectSystem.createProjectService(host); + function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { + const file1: ts.projectSystem.File = { + path: "/a/b/src/app.ts", + content: "let x = 10;" + }; + const file2: ts.projectSystem.File = { + path: "/a/B/lib/module2.ts", + content: "let z = 10;" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: "" + }; + const configFile2: ts.projectSystem.File = { + path: "/a/tsconfig.json", + content: "" + }; + const host = ts.projectSystem.createServerHost([file1, file2, configFile, configFile2], { + useCaseSensitiveFileNames + }); + const service = ts.projectSystem.createProjectService(host); - // Open file1 -> configFile - verifyConfigFileName(file1, "/a", configFile); - verifyConfigFileName(file1, "/a/b", configFile); - verifyConfigFileName(file1, "/a/B", configFile); + // 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); + // 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: ts.projectSystem.File, projectRoot: string, expectedConfigFile: ts.projectSystem.File | undefined) { - const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); - assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); - service.closeClientFile(file.path); - } + function verifyConfigFileName(file: ts.projectSystem.File, projectRoot: string, expectedConfigFile: ts.projectSystem.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-sensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); + }); - it("works when project root is used with case-insensitive system", () => { - verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); - }); + 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: ts.projectSystem.File = { - path: `${projectFolder}/src/a.ts`, - content: "export const x = 0;" - }; - const configFile: ts.projectSystem.File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, configFile, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(aFile.path, /*fileContent*/ undefined, ts.ScriptKind.TS, projectFolder); - verifyProject(); + it("uses existing project even if project refresh is pending", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: ts.projectSystem.File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: ts.projectSystem.File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(aFile.path, /*fileContent*/ undefined, ts.ScriptKind.TS, projectFolder); + verifyProject(); - const bFile: ts.projectSystem.File = { - path: `${projectFolder}/src/b.ts`, - content: `export {}; declare module "./a" { export const y: number; }` - }; - files.push(bFile); - host.writeFile(bFile.path, bFile.content); - service.openClientFile(bFile.path, /*fileContent*/ undefined, ts.ScriptKind.TS, projectFolder); - verifyProject(); + const bFile: ts.projectSystem.File = { + path: `${projectFolder}/src/b.ts`, + content: `export {}; declare module "./a" { export const y: number; }` + }; + files.push(bFile); + host.writeFile(bFile.path, bFile.content); + service.openClientFile(bFile.path, /*fileContent*/ undefined, ts.ScriptKind.TS, projectFolder); + verifyProject(); - function verifyProject() { - assert.isDefined(service.configuredProjects.get(configFile.path)); - const project = service.configuredProjects.get(configFile.path)!; - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - } - }); + function verifyProject() { + assert.isDefined(service.configuredProjects.get(configFile.path)); + const project = service.configuredProjects.get(configFile.path)!; + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + } + }); - it("can open same file again", () => { - const projectFolder = "/user/someuser/projects/myproject"; - const aFile: ts.projectSystem.File = { - path: `${projectFolder}/src/a.ts`, - content: "export const x = 0;" - }; - const configFile: ts.projectSystem.File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, configFile, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.createProjectService(host); - verifyProject(aFile.content); - verifyProject(`${aFile.content}export const y = 10;`); + it("can open same file again", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: ts.projectSystem.File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: ts.projectSystem.File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.createProjectService(host); + verifyProject(aFile.content); + verifyProject(`${aFile.content}export const y = 10;`); - function verifyProject(aFileContent: string) { - service.openClientFile(aFile.path, aFileContent, ts.ScriptKind.TS, projectFolder); - const project = service.configuredProjects.get(configFile.path)!; - ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); - assert.equal(project.getCurrentProgram()?.getSourceFile(aFile.path)!.text, aFileContent); - } - }); + function verifyProject(aFileContent: string) { + service.openClientFile(aFile.path, aFileContent, ts.ScriptKind.TS, projectFolder); + const project = service.configuredProjects.get(configFile.path)!; + ts.projectSystem.checkProjectActualFiles(project, files.map(f => f.path)); + assert.equal(project.getCurrentProgram()?.getSourceFile(aFile.path)!.text, aFileContent); + } + }); - it("when file makes edits to add/remove comment directives, they are handled correcrly", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/file.ts`, - content: `const x = 10; + it("when file makes edits to add/remove comment directives, they are handled correcrly", () => { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/file.ts`, + content: `const x = 10; function foo() { // @ts-ignore let y: string = x; @@ -145,43 +145,43 @@ function bar() { } foo(); bar();` - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); - // Remove first ts-ignore and check only first error is reported - const tsIgnoreComment = `// @ts-ignore`; - const locationOfTsIgnore = ts.projectSystem.protocolTextSpanFromSubstring(file.content, tsIgnoreComment); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: file.path, - textChanges: [{ - newText: " ", - ...locationOfTsIgnore - }] + // Remove first ts-ignore and check only first error is reported + const tsIgnoreComment = `// @ts-ignore`; + const locationOfTsIgnore = ts.projectSystem.protocolTextSpanFromSubstring(file.content, tsIgnoreComment); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: file.path, + textChanges: [{ + newText: " ", + ...locationOfTsIgnore }] - } - }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); - // Revert the change and no errors should be reported - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: file.path, - textChanges: [{ - newText: tsIgnoreComment, - ...locationOfTsIgnore - }] + }] + } + }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); + // Revert the change and no errors should be reported + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: file.path, + textChanges: [{ + newText: tsIgnoreComment, + ...locationOfTsIgnore }] - } - }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); - ts.projectSystem.baselineTsserverLogs("openfile", "when file makes edits to add/remove comment directives, they are handled correcrly", session); + }] + } }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [file] }); + ts.projectSystem.baselineTsserverLogs("openfile", "when file makes edits to add/remove comment directives, they are handled correcrly", session); }); +}); } diff --git a/src/testRunner/unittests/tsserver/packageJsonInfo.ts b/src/testRunner/unittests/tsserver/packageJsonInfo.ts index 7a4229a38ce97..efaebd218c541 100644 --- a/src/testRunner/unittests/tsserver/packageJsonInfo.ts +++ b/src/testRunner/unittests/tsserver/packageJsonInfo.ts @@ -1,112 +1,112 @@ namespace ts.projectSystem { - const tsConfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}" - }; - const packageJsonContent = { - dependencies: { - redux: "*" - }, - peerDependencies: { - react: "*" - }, - optionalDependencies: { - typescript: "*" - }, - devDependencies: { - webpack: "*" - } - }; - const packageJson: ts.projectSystem.File = { - path: "/package.json", - content: JSON.stringify(packageJsonContent, undefined, 2) - }; +const tsConfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}" +}; +const packageJsonContent = { + dependencies: { + redux: "*" + }, + peerDependencies: { + react: "*" + }, + optionalDependencies: { + typescript: "*" + }, + devDependencies: { + webpack: "*" + } +}; +const packageJson: ts.projectSystem.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 { projectService, host } = setup([tsConfig]); - assert.isUndefined(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); +describe("unittests:: tsserver:: packageJsonInfo", () => { + it("detects new package.json files that are added, caches them, and watches them", () => { + // Initialize project without package.json + const { projectService, host } = setup([tsConfig]); + assert.isUndefined(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); - // Add package.json - host.writeFile(packageJson.path, packageJson.content); - let packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.ok(packageJsonInfo); - assert.ok(packageJsonInfo.dependencies); - assert.ok(packageJsonInfo.devDependencies); - assert.ok(packageJsonInfo.peerDependencies); - assert.ok(packageJsonInfo.optionalDependencies); + // Add package.json + host.writeFile(packageJson.path, packageJson.content); + let packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; + assert.ok(packageJsonInfo); + assert.ok(packageJsonInfo.dependencies); + assert.ok(packageJsonInfo.devDependencies); + assert.ok(packageJsonInfo.peerDependencies); + assert.ok(packageJsonInfo.optionalDependencies); - // Edit package.json - host.writeFile(packageJson.path, JSON.stringify({ - ...packageJsonContent, - dependencies: undefined - })); - packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.isUndefined(packageJsonInfo.dependencies); - }); + // Edit package.json + host.writeFile(packageJson.path, JSON.stringify({ + ...packageJsonContent, + dependencies: undefined + })); + packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.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 { projectService, host } = setup(); - projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); - assert.ok(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); + it("finds package.json on demand, watches for deletion, and removes them from cache", () => { + // Initialize project with package.json + const { projectService, host } = setup(); + projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); + assert.ok(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); - // Delete package.json - host.deleteFile(packageJson.path); - assert.isUndefined(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); - }); + // Delete package.json + host.deleteFile(packageJson.path); + assert.isUndefined(projectService.packageJsonCache.getInDirectory("/" as ts.Path)); + }); - it("finds multiple package.json files when present", () => { - // Initialize project with package.json at root - const { projectService, host } = setup(); - // Add package.json in /src - host.writeFile("/src/package.json", packageJson.content); - assert.lengthOf(projectService.getPackageJsonsVisibleToFile("/a.ts" as ts.Path), 1); - assert.lengthOf(projectService.getPackageJsonsVisibleToFile("/src/b.ts" as ts.Path), 2); - }); + it("finds multiple package.json files when present", () => { + // Initialize project with package.json at root + const { projectService, host } = setup(); + // Add package.json in /src + host.writeFile("/src/package.json", packageJson.content); + assert.lengthOf(projectService.getPackageJsonsVisibleToFile("/a.ts" as ts.Path), 1); + assert.lengthOf(projectService.getPackageJsonsVisibleToFile("/src/b.ts" as ts.Path), 2); + }); - it("handles errors in json parsing of package.json", () => { - const packageJsonContent = `{ "mod" }`; - const { projectService, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); - projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); - const packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.isFalse(packageJsonInfo.parseable); + it("handles errors in json parsing of package.json", () => { + const packageJsonContent = `{ "mod" }`; + const { projectService, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); + projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); + const packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; + assert.isFalse(packageJsonInfo.parseable); - host.writeFile(packageJson.path, packageJson.content); - projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); - const packageJsonInfo2 = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.ok(packageJsonInfo2); - assert.ok(packageJsonInfo2.dependencies); - assert.ok(packageJsonInfo2.devDependencies); - assert.ok(packageJsonInfo2.peerDependencies); - assert.ok(packageJsonInfo2.optionalDependencies); - }); + host.writeFile(packageJson.path, packageJson.content); + projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); + const packageJsonInfo2 = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; + assert.ok(packageJsonInfo2); + assert.ok(packageJsonInfo2.dependencies); + assert.ok(packageJsonInfo2.devDependencies); + assert.ok(packageJsonInfo2.peerDependencies); + assert.ok(packageJsonInfo2.optionalDependencies); + }); - it("handles empty package.json", () => { - const packageJsonContent = ""; - const { projectService, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); - projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); - const packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.isFalse(packageJsonInfo.parseable); + it("handles empty package.json", () => { + const packageJsonContent = ""; + const { projectService, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); + projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); + const packageJsonInfo = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; + assert.isFalse(packageJsonInfo.parseable); - host.writeFile(packageJson.path, packageJson.content); - projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); - const packageJsonInfo2 = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; - assert.ok(packageJsonInfo2); - assert.ok(packageJsonInfo2.dependencies); - assert.ok(packageJsonInfo2.devDependencies); - assert.ok(packageJsonInfo2.peerDependencies); - assert.ok(packageJsonInfo2.optionalDependencies); - }); + host.writeFile(packageJson.path, packageJson.content); + projectService.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as ts.Path); + const packageJsonInfo2 = projectService.packageJsonCache.getInDirectory("/" as ts.Path)!; + assert.ok(packageJsonInfo2); + assert.ok(packageJsonInfo2.dependencies); + assert.ok(packageJsonInfo2.devDependencies); + assert.ok(packageJsonInfo2.peerDependencies); + assert.ok(packageJsonInfo2.optionalDependencies); }); +}); - function setup(files: readonly ts.projectSystem.File[] = [tsConfig, packageJson]) { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - projectService.openClientFile(files[0].path); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - return { host, session, project, projectService }; - } +function setup(files: readonly ts.projectSystem.File[] = [tsConfig, packageJson]) { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + projectService.openClientFile(files[0].path); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + return { host, session, project, projectService }; +} } diff --git a/src/testRunner/unittests/tsserver/partialSemanticServer.ts b/src/testRunner/unittests/tsserver/partialSemanticServer.ts index 5428a93c35bd2..9fa4306b504b4 100644 --- a/src/testRunner/unittests/tsserver/partialSemanticServer.ts +++ b/src/testRunner/unittests/tsserver/partialSemanticServer.ts @@ -1,267 +1,267 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: Semantic operations on partialSemanticServer", () => { - function setup() { - const file1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `import { y, cc } from "./b"; +describe("unittests:: tsserver:: Semantic operations on partialSemanticServer", () => { + function setup() { + const file1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `import { y, cc } from "./b"; import { something } from "something"; class c { prop = "hello"; foo() { return this.prop; } }` - }; - const file2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `export { cc } from "./c"; + }; + const file2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `export { cc } from "./c"; import { something } from "something"; export const y = 10;` - }; - const file3: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/c.ts`, - content: `export const cc = 10;` - }; - const something: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/something/index.d.ts`, - content: "export const something = 10;" - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = ts.projectSystem.createServerHost([file1, file2, file3, something, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); - return { host, session, file1, file2, file3, something, configFile }; - } + }; + const file3: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/c.ts`, + content: `export const cc = 10;` + }; + const something: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/something/index.d.ts`, + content: "export const something = 10;" + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = ts.projectSystem.createServerHost([file1, file2, file3, something, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); + return { host, session, file1, file2, file3, something, configFile }; + } - it("open files are added to inferred project even if config file is present and semantic operations succeed", () => { - const { host, session, file1, file2 } = setup(); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // no imports are resolved - verifyCompletions(); + it("open files are added to inferred project even if config file is present and semantic operations succeed", () => { + const { host, session, file1, file2 } = setup(); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // no imports are resolved + verifyCompletions(); - ts.projectSystem.openFilesForSession([file2], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path, file2.path]); - verifyCompletions(); + ts.projectSystem.openFilesForSession([file2], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path, file2.path]); + verifyCompletions(); - function verifyCompletions() { - assert.isTrue(project.languageServiceEnabled); - ts.projectSystem.checkWatchedFiles(host, ts.emptyArray); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ true); - ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); - const response = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Completions, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(file1, "prop", { index: 1 }) - }).response as ts.projectSystem.protocol.CompletionEntry[]; - assert.deepEqual(response, [ - completionEntry("foo", ts.ScriptElementKind.memberFunctionElement), - completionEntry("prop", ts.ScriptElementKind.memberVariableElement), - ]); - } - - function completionEntry(name: string, kind: ts.ScriptElementKind): ts.projectSystem.protocol.CompletionEntry { - return { - name, - kind, - kindModifiers: "", - sortText: ts.Completions.SortText.LocationPriority, - hasAction: undefined, - insertText: undefined, - isPackageJsonImport: undefined, - isImportStatementCompletion: undefined, - isRecommended: undefined, - replacementSpan: undefined, - source: undefined, - data: undefined, - sourceDisplay: undefined, - isSnippet: undefined, - labelDetails: undefined, - }; - } - }); + function verifyCompletions() { + assert.isTrue(project.languageServiceEnabled); + ts.projectSystem.checkWatchedFiles(host, ts.emptyArray); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ true); + ts.projectSystem.checkWatchedDirectories(host, ts.emptyArray, /*recursive*/ false); + const response = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Completions, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(file1, "prop", { index: 1 }) + }).response as ts.projectSystem.protocol.CompletionEntry[]; + assert.deepEqual(response, [ + completionEntry("foo", ts.ScriptElementKind.memberFunctionElement), + completionEntry("prop", ts.ScriptElementKind.memberVariableElement), + ]); + } - it("throws on unsupported commands", () => { - const { session, file1 } = setup(); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - let hasException = false; - const request: ts.projectSystem.protocol.SemanticDiagnosticsSyncRequest = { - type: "request", - seq: 1, - command: ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: { file: file1.path } + function completionEntry(name: string, kind: ts.ScriptElementKind): ts.projectSystem.protocol.CompletionEntry { + return { + name, + kind, + kindModifiers: "", + sortText: ts.Completions.SortText.LocationPriority, + hasAction: undefined, + insertText: undefined, + isPackageJsonImport: undefined, + isImportStatementCompletion: undefined, + isRecommended: undefined, + replacementSpan: undefined, + source: undefined, + data: undefined, + sourceDisplay: undefined, + isSnippet: undefined, + labelDetails: undefined, }; - try { - session.executeCommand(request); - } - catch (e) { - assert.equal(e.message, `Request: semanticDiagnosticsSync not allowed in LanguageServiceMode.PartialSemantic`); - hasException = true; - } - assert.isTrue(hasException); + } + }); - hasException = false; - const project = service.inferredProjects[0]; - try { - project.getLanguageService().getSemanticDiagnostics(file1.path); - } - catch (e) { - assert.equal(e.message, `LanguageService Operation: getSemanticDiagnostics not allowed in LanguageServiceMode.PartialSemantic`); - hasException = true; - } - assert.isTrue(hasException); - }); + it("throws on unsupported commands", () => { + const { session, file1 } = setup(); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + let hasException = false; + const request: ts.projectSystem.protocol.SemanticDiagnosticsSyncRequest = { + type: "request", + seq: 1, + command: ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: { file: file1.path } + }; + try { + session.executeCommand(request); + } + catch (e) { + assert.equal(e.message, `Request: semanticDiagnosticsSync not allowed in LanguageServiceMode.PartialSemantic`); + hasException = true; + } + assert.isTrue(hasException); - it("allows syntactic diagnostic commands", () => { - const file1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `if (a < (b + c) { }` - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: `{}` - }; - const expectedErrorMessage = "')' expected."; + hasException = false; + const project = service.inferredProjects[0]; + try { + project.getLanguageService().getSemanticDiagnostics(file1.path); + } + catch (e) { + assert.equal(e.message, `LanguageService Operation: getSemanticDiagnostics not allowed in LanguageServiceMode.PartialSemantic`); + hasException = true; + } + assert.isTrue(hasException); + }); - const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { - serverMode: ts.LanguageServiceMode.PartialSemantic, - useSingleInferredProject: true, - logger: ts.projectSystem.createLoggerWithInMemoryLogs() - }); + it("allows syntactic diagnostic commands", () => { + const file1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `if (a < (b + c) { }` + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: `{}` + }; + const expectedErrorMessage = "')' expected."; - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - const request: ts.projectSystem.protocol.SyntacticDiagnosticsSyncRequest = { - type: "request", - seq: 1, - command: ts.projectSystem.protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: { file: file1.path } - }; - const response = session.executeCommandSeq(request).response as ts.projectSystem.protocol.SyntacticDiagnosticsSyncResponse["body"]; - assert.isDefined(response); - assert.equal(response!.length, 1); - assert.equal((response![0] as ts.projectSystem.protocol.Diagnostic).text, expectedErrorMessage); + const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { + serverMode: ts.LanguageServiceMode.PartialSemantic, + useSingleInferredProject: true, + logger: ts.projectSystem.createLoggerWithInMemoryLogs() + }); - const project = service.inferredProjects[0]; - const diagnostics = project.getLanguageService().getSyntacticDiagnostics(file1.path); - assert.isTrue(diagnostics.length === 1); - assert.equal(diagnostics[0].messageText, expectedErrorMessage); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + const request: ts.projectSystem.protocol.SyntacticDiagnosticsSyncRequest = { + type: "request", + seq: 1, + command: ts.projectSystem.protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: { file: file1.path } + }; + const response = session.executeCommandSeq(request).response as ts.projectSystem.protocol.SyntacticDiagnosticsSyncResponse["body"]; + assert.isDefined(response); + assert.equal(response!.length, 1); + assert.equal((response![0] as ts.projectSystem.protocol.Diagnostic).text, expectedErrorMessage); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [file1], skip: [{ semantic: true, suggestion: true }] }); - ts.projectSystem.baselineTsserverLogs("partialSemanticServer", "syntactic diagnostics are returned with no error", session); - }); + const project = service.inferredProjects[0]; + const diagnostics = project.getLanguageService().getSyntacticDiagnostics(file1.path); + assert.isTrue(diagnostics.length === 1); + assert.equal(diagnostics[0].messageText, expectedErrorMessage); - it("should not include auto type reference directives", () => { - const { host, session, file1 } = setup(); - const atTypes: ts.projectSystem.File = { - path: `/node_modules/@types/somemodule/index.d.ts`, - content: "export const something = 10;" - }; - host.ensureFileOrFolder(atTypes); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // Should not contain atTypes - }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [file1], skip: [{ semantic: true, suggestion: true }] }); + ts.projectSystem.baselineTsserverLogs("partialSemanticServer", "syntactic diagnostics are returned with no error", session); + }); - it("should not include referenced files from unopened files", () => { - const file1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a.ts`, - content: `/// + it("should not include auto type reference directives", () => { + const { host, session, file1 } = setup(); + const atTypes: ts.projectSystem.File = { + path: `/node_modules/@types/somemodule/index.d.ts`, + content: "export const something = 10;" + }; + host.ensureFileOrFolder(atTypes); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // Should not contain atTypes + }); + + it("should not include referenced files from unopened files", () => { + const file1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a.ts`, + content: `/// /// function fooA() { }` - }; - const file2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b.ts`, - content: `/// + }; + const file2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b.ts`, + content: `/// /// function fooB() { }` - }; - const file3: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/c.ts`, - content: `function fooC() { }` - }; - const something: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/something/index.d.ts`, - content: "function something() {}" - }; - const configFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = ts.projectSystem.createServerHost([file1, file2, file3, something, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // no resolve - }); + }; + const file3: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/c.ts`, + content: `function fooC() { }` + }; + const something: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/something/index.d.ts`, + content: "function something() {}" + }; + const configFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = ts.projectSystem.createServerHost([file1, file2, file3, something, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); // no resolve + }); - it("should not crash when external module name resolution is reused", () => { - const { session, file1, file2, file3 } = setup(); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); + it("should not crash when external module name resolution is reused", () => { + const { session, file1, file2, file3 } = setup(); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + ts.projectSystem.checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file1.path]); - // Close the file that contains non relative external module name and open some file that doesnt have non relative external module import - ts.projectSystem.closeFilesForSession([file1], session); - ts.projectSystem.openFilesForSession([file3], session); - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file3.path]); + // Close the file that contains non relative external module name and open some file that doesnt have non relative external module import + ts.projectSystem.closeFilesForSession([file1], session); + ts.projectSystem.openFilesForSession([file3], session); + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file3.path]); - // Open file with non relative external module name - ts.projectSystem.openFilesForSession([file2], session); - ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file2.path, file3.path]); - }); + // Open file with non relative external module name + ts.projectSystem.openFilesForSession([file2], session); + ts.projectSystem.checkProjectActualFiles(project, [ts.projectSystem.libFile.path, file2.path, file3.path]); + }); - it("should not create autoImportProvider or handle package jsons", () => { - const angularFormsDts: ts.projectSystem.File = { - path: "/node_modules/@angular/forms/forms.d.ts", - content: "export declare class PatternValidator {}", - }; - const angularFormsPackageJson: ts.projectSystem.File = { - path: "/node_modules/@angular/forms/package.json", - content: `{ "name": "@angular/forms", "typings": "./forms.d.ts" }`, - }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: `{ "compilerOptions": { "module": "commonjs" } }`, - }; - const packageJson: ts.projectSystem.File = { - path: "/package.json", - content: `{ "dependencies": { "@angular/forms": "*", "@angular/core": "*" } }` - }; - const indexTs: ts.projectSystem.File = { - path: "/index.ts", - content: "" - }; - const host = ts.projectSystem.createServerHost([angularFormsDts, angularFormsPackageJson, tsconfig, packageJson, indexTs, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); - const service = session.getProjectService(); - ts.projectSystem.openFilesForSession([indexTs], session); - const project = service.inferredProjects[0]; - assert.isFalse(project.autoImportProviderHost); - assert.isUndefined(project.getPackageJsonAutoImportProvider()); - assert.deepEqual(project.getPackageJsonsForAutoImport(), ts.emptyArray); - }); + it("should not create autoImportProvider or handle package jsons", () => { + const angularFormsDts: ts.projectSystem.File = { + path: "/node_modules/@angular/forms/forms.d.ts", + content: "export declare class PatternValidator {}", + }; + const angularFormsPackageJson: ts.projectSystem.File = { + path: "/node_modules/@angular/forms/package.json", + content: `{ "name": "@angular/forms", "typings": "./forms.d.ts" }`, + }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: `{ "compilerOptions": { "module": "commonjs" } }`, + }; + const packageJson: ts.projectSystem.File = { + path: "/package.json", + content: `{ "dependencies": { "@angular/forms": "*", "@angular/core": "*" } }` + }; + const indexTs: ts.projectSystem.File = { + path: "/index.ts", + content: "" + }; + const host = ts.projectSystem.createServerHost([angularFormsDts, angularFormsPackageJson, tsconfig, packageJson, indexTs, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host, { serverMode: ts.LanguageServiceMode.PartialSemantic, useSingleInferredProject: true }); + const service = session.getProjectService(); + ts.projectSystem.openFilesForSession([indexTs], session); + const project = service.inferredProjects[0]; + assert.isFalse(project.autoImportProviderHost); + assert.isUndefined(project.getPackageJsonAutoImportProvider()); + assert.deepEqual(project.getPackageJsonsForAutoImport(), ts.emptyArray); + }); - it("should support go-to-definition on module specifiers", () => { - const { session, file1, file2 } = setup(); - ts.projectSystem.openFilesForSession([file1], session); - const response = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(file1, `"./b"`) - }).response as ts.projectSystem.protocol.DefinitionInfoAndBoundSpan; - assert.isDefined(response); - assert.deepEqual(response.definitions, [{ - file: file2.path, - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }]); - }); + it("should support go-to-definition on module specifiers", () => { + const { session, file1, file2 } = setup(); + ts.projectSystem.openFilesForSession([file1], session); + const response = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(file1, `"./b"`) + }).response as ts.projectSystem.protocol.DefinitionInfoAndBoundSpan; + assert.isDefined(response); + assert.deepEqual(response.definitions, [{ + file: file2.path, + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }]); }); +}); } diff --git a/src/testRunner/unittests/tsserver/plugins.ts b/src/testRunner/unittests/tsserver/plugins.ts index 9e7d249880a1f..a6013168ec723 100644 --- a/src/testRunner/unittests/tsserver/plugins.ts +++ b/src/testRunner/unittests/tsserver/plugins.ts @@ -1,106 +1,106 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: plugins loading", () => { - const testProtocolCommand = "testProtocolCommand"; - const testProtocolCommandRequest = "testProtocolCommandRequest"; - const testProtocolCommandResponse = "testProtocolCommandResponse"; +describe("unittests:: tsserver:: plugins loading", () => { + const testProtocolCommand = "testProtocolCommand"; + const testProtocolCommandRequest = "testProtocolCommandRequest"; + const testProtocolCommandResponse = "testProtocolCommandResponse"; - function createHostWithPlugin(files: readonly ts.projectSystem.File[]) { - const host = ts.projectSystem.createServerHost(files); - const pluginsLoaded: string[] = []; - const protocolHandlerRequests: [ - string, - string - ][] = []; - host.require = (_initialPath, moduleName) => { - pluginsLoaded.push(moduleName); - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - info.session?.addProtocolHandler(testProtocolCommand, request => { - protocolHandlerRequests.push([request.command, request.arguments]); - return { - response: testProtocolCommandResponse - }; - }); - return Harness.LanguageService.makeDefaultProxy(info); - } - }), - error: undefined - }; - }; - return { host, pluginsLoaded, protocolHandlerRequests }; - } - - it("With local plugins", () => { - const expectedToLoad = ["@myscoped/plugin", "unscopedPlugin"]; - const notToLoad = ["../myPlugin", "myPlugin/../malicious"]; - const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - plugins: [ - ...[...expectedToLoad, ...notToLoad].map(name => ({ name })), - { transform: "some-transform" } - ] + function createHostWithPlugin(files: readonly ts.projectSystem.File[]) { + const host = ts.projectSystem.createServerHost(files); + const pluginsLoaded: string[] = []; + const protocolHandlerRequests: [ + string, + string + ][] = []; + host.require = (_initialPath, moduleName) => { + pluginsLoaded.push(moduleName); + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + info.session?.addProtocolHandler(testProtocolCommand, request => { + protocolHandlerRequests.push([request.command, request.arguments]); + return { + response: testProtocolCommandResponse + }; + }); + return Harness.LanguageService.makeDefaultProxy(info); } - }) + }), + error: undefined }; - const { host, pluginsLoaded } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host); - service.openClientFile(aTs.path); - assert.deepEqual(pluginsLoaded, expectedToLoad); - }); + }; + return { host, pluginsLoaded, protocolHandlerRequests }; + } - it("With global plugins", () => { - const expectedToLoad = ["@myscoped/plugin", "unscopedPlugin"]; - const notToLoad = ["../myPlugin", "myPlugin/../malicious"]; - const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: "{}" - }; - const { host, pluginsLoaded } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host, { globalPlugins: [...expectedToLoad, ...notToLoad] }); - service.openClientFile(aTs.path); - assert.deepEqual(pluginsLoaded, expectedToLoad); - }); - - it("With session and custom protocol message", () => { - const pluginName = "some-plugin"; - const expectedToLoad = [pluginName]; - const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: ts.projectSystem.File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - plugins: [ - { name: pluginName } - ] - } - }) - }; + it("With local plugins", () => { + const expectedToLoad = ["@myscoped/plugin", "unscopedPlugin"]; + const notToLoad = ["../myPlugin", "myPlugin/../malicious"]; + const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + plugins: [ + ...[...expectedToLoad, ...notToLoad].map(name => ({ name })), + { transform: "some-transform" } + ] + } + }) + }; + const { host, pluginsLoaded } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host); + service.openClientFile(aTs.path); + assert.deepEqual(pluginsLoaded, expectedToLoad); + }); - const { host, pluginsLoaded, protocolHandlerRequests } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const service = ts.projectSystem.createProjectService(host, { session }); - service.openClientFile(aTs.path); - assert.deepEqual(pluginsLoaded, expectedToLoad); + it("With global plugins", () => { + const expectedToLoad = ["@myscoped/plugin", "unscopedPlugin"]; + const notToLoad = ["../myPlugin", "myPlugin/../malicious"]; + const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: "{}" + }; + const { host, pluginsLoaded } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host, { globalPlugins: [...expectedToLoad, ...notToLoad] }); + service.openClientFile(aTs.path); + assert.deepEqual(pluginsLoaded, expectedToLoad); + }); - const resp = session.executeCommandSeq({ - command: testProtocolCommand, - arguments: testProtocolCommandRequest - }); + it("With session and custom protocol message", () => { + const pluginName = "some-plugin"; + const expectedToLoad = [pluginName]; + const aTs: ts.projectSystem.File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: ts.projectSystem.File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + plugins: [ + { name: pluginName } + ] + } + }) + }; - assert.strictEqual(protocolHandlerRequests.length, 1); - const [command, args] = protocolHandlerRequests[0]; - assert.strictEqual(command, testProtocolCommand); - assert.strictEqual(args, testProtocolCommandRequest); + const { host, pluginsLoaded, protocolHandlerRequests } = createHostWithPlugin([aTs, tsconfig, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const service = ts.projectSystem.createProjectService(host, { session }); + service.openClientFile(aTs.path); + assert.deepEqual(pluginsLoaded, expectedToLoad); - const expectedResp: ts.server.HandlerResponse = { - response: testProtocolCommandResponse - }; - assert.deepEqual(resp, expectedResp); + const resp = session.executeCommandSeq({ + command: testProtocolCommand, + arguments: testProtocolCommandRequest }); + + assert.strictEqual(protocolHandlerRequests.length, 1); + const [command, args] = protocolHandlerRequests[0]; + assert.strictEqual(command, testProtocolCommand); + assert.strictEqual(args, testProtocolCommandRequest); + + const expectedResp: ts.server.HandlerResponse = { + response: testProtocolCommandResponse + }; + assert.deepEqual(resp, expectedResp); }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index a1dbe92beed16..8f080b3ba1983 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -1,890 +1,890 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: Project Errors", () => { - function checkProjectErrors(projectFiles: ts.server.ProjectFilesWithTSDiagnostics, expectedErrors: readonly string[]): void { - assert.isTrue(projectFiles !== undefined, "missing project files"); - checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); - } - - function checkProjectErrorsWorker(errors: readonly ts.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 = ts.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}`); - } +describe("unittests:: tsserver:: Project Errors", () => { + function checkProjectErrors(projectFiles: ts.server.ProjectFilesWithTSDiagnostics, expectedErrors: readonly string[]): void { + assert.isTrue(projectFiles !== undefined, "missing project files"); + checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); + } + + function checkProjectErrorsWorker(errors: readonly ts.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 = ts.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: ts.server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { - assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); - if (expectedErrors.length) { - ts.zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { - assert.isTrue(ts.startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - }); - } + function checkDiagnosticsWithLinePos(errors: ts.server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + ts.zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { + assert.isTrue(ts.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 = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/test.csproj"; + const compilerOptionsRequest: ts.server.protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }; + + { + projectService.openExternalProject({ + projectFileName, + options: {}, + rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) + }); - 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 = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/test.csproj"; - const compilerOptionsRequest: ts.server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }; - - { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]) - }); - - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; - // only file1 exists - expect error - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - } - host.renameFile(file1.path, file2.path); - { - // only file2 exists - expect error - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); - } - - host.writeFile(file1.path, file1.content); - { - // both files exist - expect no errors - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as ts.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 => ts.getBaseFileName(f.path)) }) - }; - const host = ts.projectSystem.createServerHost([file1, config, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - ts.projectSystem.openFilesForSession([file1], session); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - const compilerOptionsRequest: ts.server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: project.getProjectName() } - }; - let diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + // only file1 exists - expect error checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + } + host.renameFile(file1.path, file2.path); + { + // only file2 exists - expect error + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); + } - host.writeFile(file2.path, file2.content); - - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + host.writeFile(file1.path, file1.content); + { + // both files exist - expect no errors + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as ts.server.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 => ts.getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = ts.projectSystem.createServerHost([file1, file2, corruptedConfig]); - const projectService = ts.projectSystem.createProjectService(host); + } + }); - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = ts.projectSystem.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.writeFile(correctConfig.path, correctConfig.content); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = ts.projectSystem.configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - }); + 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 => ts.getBaseFileName(f.path)) }) + }; + const host = ts.projectSystem.createServerHost([file1, config, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + ts.projectSystem.openFilesForSession([file1], session); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + const compilerOptionsRequest: ts.server.protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: project.getProjectName() } + }; + let diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + + host.writeFile(file2.path, file2.content); + + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diags = session.executeCommand(compilerOptionsRequest).response as ts.server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, []); + }); - 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 => ts.getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = ts.projectSystem.createServerHost([file1, file2, correctConfig]); - const projectService = ts.projectSystem.createProjectService(host); + 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 => ts.getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = ts.projectSystem.createServerHost([file1, file2, corruptedConfig]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = ts.projectSystem.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.writeFile(correctConfig.path, correctConfig.content); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = ts.projectSystem.configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + }); - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = ts.projectSystem.configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - // break config and trigger watcher - host.writeFile(corruptedConfig.path, corruptedConfig.content); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = ts.projectSystem.configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); - } - }); + 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 => ts.getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = ts.projectSystem.createServerHost([file1, file2, correctConfig]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = ts.projectSystem.configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + // break config and trigger watcher + host.writeFile(corruptedConfig.path, corruptedConfig.content); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = ts.find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = ts.projectSystem.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", () => { + 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 = ts.projectSystem.createServerHost([file1, corruptedConfig]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const project = projectService.findProject(corruptedConfig.path)!; + ts.projectSystem.checkProjectRootFiles(project, [file1.path]); }); - describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { - it("document is not contained in project", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" + describe("when opening new file that doesnt exist on disk yet", () => { + function verifyNonExistentFile(useProjectRoot: boolean) { + const folderPath = "/user/someuser/projects/someFolder"; + const fileInRoot: ts.projectSystem.File = { + path: `/src/somefile.d.ts`, + content: "class c { }" }; - const corruptedConfig = { - path: "/a/b/tsconfig.json", - content: "{" + const fileInProjectRoot: ts.projectSystem.File = { + path: `${folderPath}/src/somefile.d.ts`, + content: "class c { }" }; - const host = ts.projectSystem.createServerHost([file1, corruptedConfig]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - - const project = projectService.findProject(corruptedConfig.path)!; - ts.projectSystem.checkProjectRootFiles(project, [file1.path]); - }); + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, fileInRoot, fileInProjectRoot]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs(), useInferredProjectPerProjectRoot: true }); - describe("when opening new file that doesnt exist on disk yet", () => { - function verifyNonExistentFile(useProjectRoot: boolean) { - const folderPath = "/user/someuser/projects/someFolder"; - const fileInRoot: ts.projectSystem.File = { - path: `/src/somefile.d.ts`, - content: "class c { }" - }; - const fileInProjectRoot: ts.projectSystem.File = { - path: `${folderPath}/src/somefile.d.ts`, - content: "class c { }" - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, fileInRoot, fileInProjectRoot]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs(), useInferredProjectPerProjectRoot: true }); - - const untitledFile = "untitled:Untitled-1"; - const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; - const refPathNotFound2 = "./src/somefile.d.ts"; - const fileContent = `/// + const untitledFile = "untitled:Untitled-1"; + const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; + const refPathNotFound2 = "./src/somefile.d.ts"; + const fileContent = `/// /// `; - session.executeCommandSeq({ - command: ts.server.CommandNames.Open, - arguments: { - file: untitledFile, - fileContent, - scriptKindName: "TS", - projectRootPath: useProjectRoot ? folderPath : undefined - } - }); - ts.projectSystem.appendAllScriptInfos(session.getProjectService(), session.logger.logs); - - // Since this is not js project so no typings are queued - host.checkTimeoutQueueLength(0); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [untitledFile] }); - ts.projectSystem.baselineTsserverLogs("projectErrors", `when opening new file that doesnt exist on disk yet ${useProjectRoot ? "with projectRoot" : "without projectRoot"}`, session); - } - - it("has projectRoot", () => { - verifyNonExistentFile(/*useProjectRoot*/ true); - }); - - 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: ts.projectSystem.File = { - path: `${projectDir}/bar/app.ts`, - content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" - }; - const foo: ts.projectSystem.File = { - path: `${projectDir}/foo/foo.ts`, - content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" - }; - const configFile: ts.projectSystem.File = { - path: `${projectDir}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) - }; - const host = ts.projectSystem.createServerHost([app, foo, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); session.executeCommandSeq({ command: ts.server.CommandNames.Open, - arguments: { file: app.path, } + arguments: { + file: untitledFile, + fileContent, + scriptKindName: "TS", + projectRootPath: useProjectRoot ? folderPath : undefined + } }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [app] }); + ts.projectSystem.appendAllScriptInfos(session.getProjectService(), session.logger.logs); - host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); - host.runQueuedTimeoutCallbacks(); - host.runQueuedTimeoutCallbacks(); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [app] }); - ts.projectSystem.baselineTsserverLogs("projectErrors", `folder rename updates project structure and reports no errors`, session); + // Since this is not js project so no typings are queued + host.checkTimeoutQueueLength(0); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [untitledFile] }); + ts.projectSystem.baselineTsserverLogs("projectErrors", `when opening new file that doesnt exist on disk yet ${useProjectRoot ? "with projectRoot" : "without projectRoot"}`, session); + } + + it("has projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ true); }); - it("Getting errors before opening file", () => { - const file: ts.projectSystem.File = { - path: "/a/b/project/file.ts", - content: "let x: number = false;" - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - session.executeCommandSeq({ - command: ts.server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path] - } - }); + it("does not have projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ false); + }); + }); - host.checkTimeoutQueueLengthAndRun(1); - ts.projectSystem.baselineTsserverLogs("projectErrors", "getting errors before opening file", session); + it("folder rename updates project structure and reports no errors", () => { + const projectDir = "/a/b/projects/myproject"; + const app: ts.projectSystem.File = { + path: `${projectDir}/bar/app.ts`, + content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" + }; + const foo: ts.projectSystem.File = { + path: `${projectDir}/foo/foo.ts`, + content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" + }; + const configFile: ts.projectSystem.File = { + path: `${projectDir}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) + }; + const host = ts.projectSystem.createServerHost([app, foo, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + session.executeCommandSeq({ + command: ts.server.CommandNames.Open, + arguments: { file: app.path, } }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [app] }); - it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { - const app: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/client/app.js`, - content: "" - }; - const serverUtilities: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/server/utilities.js`, - content: `function getHostName() { return "hello"; } export { getHostName };` - }; - const backendTest: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/test/backend/index.js`, - content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` - }; - const files = [ts.projectSystem.libFile, app, serverUtilities, backendTest]; - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([{ file: app, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.openFilesForSession([{ file: backendTest, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [backendTest.path, app.path] }); - ts.projectSystem.closeFilesForSession([backendTest], session); - ts.projectSystem.openFilesForSession([{ file: serverUtilities.path, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [serverUtilities.path, app.path] }); - ts.projectSystem.baselineTsserverLogs("projectErrors", `reports errors correctly when file referenced by inferred project root, is opened right after closing the root file`, session); + host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); + host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [app] }); + ts.projectSystem.baselineTsserverLogs("projectErrors", `folder rename updates project structure and reports no errors`, session); + }); + + it("Getting errors before opening file", () => { + const file: ts.projectSystem.File = { + path: "/a/b/project/file.ts", + content: "let x: number = false;" + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + session.executeCommandSeq({ + command: ts.server.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: ts.projectSystem.File = { - path: `${projectRootPath}/src/a.ts`, - content: `import * as myModule from "@custom/plugin"; + host.checkTimeoutQueueLengthAndRun(1); + ts.projectSystem.baselineTsserverLogs("projectErrors", "getting errors before opening file", session); + }); + + it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { + const app: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/client/app.js`, + content: "" + }; + const serverUtilities: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/server/utilities.js`, + content: `function getHostName() { return "hello"; } export { getHostName };` + }; + const backendTest: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/test/backend/index.js`, + content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` + }; + const files = [ts.projectSystem.libFile, app, serverUtilities, backendTest]; + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([{ file: app, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.openFilesForSession([{ file: backendTest, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [backendTest.path, app.path] }); + ts.projectSystem.closeFilesForSession([backendTest], session); + ts.projectSystem.openFilesForSession([{ file: serverUtilities.path, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [serverUtilities.path, app.path] }); + ts.projectSystem.baselineTsserverLogs("projectErrors", `reports errors correctly when file referenced by inferred project root, is opened right after closing the root file`, session); + }); + + 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: ts.projectSystem.File = { + path: `${projectRootPath}/src/a.ts`, + content: `import * as myModule from "@custom/plugin"; function foo() { // hello }` - }; - const config: ts.projectSystem.File = { - path: `${projectRootPath}/tsconfig.json`, - content: JSON.stringify({ include: ["src"] }) - }; - const plugin: ts.projectSystem.File = { - path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, - content: `import './proposed'; + }; + const config: ts.projectSystem.File = { + path: `${projectRootPath}/tsconfig.json`, + content: JSON.stringify({ include: ["src"] }) + }; + const plugin: ts.projectSystem.File = { + path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, + content: `import './proposed'; declare module '@custom/plugin' { export const version: string; }` - }; - const pluginProposed: ts.projectSystem.File = { - path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, - content: `declare module '@custom/plugin' { + }; + const pluginProposed: ts.projectSystem.File = { + path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, + content: `declare module '@custom/plugin' { export const bar = 10; }` - }; - const files = [ts.projectSystem.libFile, aFile, config, plugin, pluginProposed]; - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([aFile], session); - - checkErrors(); - - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: aFile.path, - line: 3, - offset: 8, - endLine: 3, - endOffset: 8, - insertString: "o" - } - }); - checkErrors(); - ts.projectSystem.baselineTsserverLogs("projectErrors", `correct errors when resolution resolves to file that has same ambient module and is also module`, session); - - function checkErrors() { - host.checkTimeoutQueueLength(0); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [aFile] }); + }; + const files = [ts.projectSystem.libFile, aFile, config, plugin, pluginProposed]; + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([aFile], session); + + checkErrors(); + + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: aFile.path, + line: 3, + offset: 8, + endLine: 3, + endOffset: 8, + insertString: "o" } }); + checkErrors(); + ts.projectSystem.baselineTsserverLogs("projectErrors", `correct errors when resolution resolves to file that has same ambient module and is also module`, session); - describe("when semantic error returns includes global error", () => { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/ui.ts`, - content: `const x = async (_action: string) => { + function checkErrors() { + host.checkTimeoutQueueLength(0); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [aFile] }); + } + }); + + describe("when semantic error returns includes global error", () => { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/ui.ts`, + content: `const x = async (_action: string) => { };` - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - ts.projectSystem.verifyGetErrScenario({ - scenario: "projectErrors", - subScenario: "when semantic error returns includes global error", - allFiles: () => [ts.projectSystem.libFile, file, config], - openFiles: () => [file], - getErrRequest: () => [file], - getErrForProjectRequest: () => [{ project: file, files: [file] }], - syncDiagnostics: () => [{ file, project: config }], - }); + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + ts.projectSystem.verifyGetErrScenario({ + scenario: "projectErrors", + subScenario: "when semantic error returns includes global error", + allFiles: () => [ts.projectSystem.libFile, file, config], + openFiles: () => [file], + getErrRequest: () => [file], + getErrForProjectRequest: () => [{ project: file, files: [file] }], + syncDiagnostics: () => [{ file, project: config }], }); }); - - describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { - it("are generated when the config file has errors", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ +}); + +describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { + it("are generated when the config file has errors", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true } }` - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file has errors", session); - }); + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file has errors", session); + }); - it("are generated when the config file doesn't have errors", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("are generated when the config file doesn't have errors", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {} }` - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file doesnt have errors", session); - }); + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file doesnt have errors", session); + }); - it("are generated when the config file changes", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ + it("are generated when the config file changes", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {} }` - }; + }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); - configFile.content = `{ + configFile.content = `{ "compilerOptions": { "haha": 123 } }`; - host.writeFile(configFile.path, configFile.content); - host.runQueuedTimeoutCallbacks(); + host.writeFile(configFile.path, configFile.content); + host.runQueuedTimeoutCallbacks(); - configFile.content = `{ + configFile.content = `{ "compilerOptions": {} }`; - host.writeFile(configFile.path, configFile.content); - host.runQueuedTimeoutCallbacks(); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file changes", session); - }); + host.writeFile(configFile.path, configFile.content); + host.runQueuedTimeoutCallbacks(); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file changes", session); + }); - it("are not generated when the config file does not include file opened and config file has errors", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/test2.ts", - content: "let xy = 10" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("are not generated when the config file does not include file opened and config file has errors", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/test2.ts", + content: "let xy = 10" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true }, "files": ["app.ts"] }` - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file2], session); - ts.projectSystem.openFilesForSession([file], session); - // We generate only if project is created when opening file from the project - ts.projectSystem.openFilesForSession([file3], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and config file has errors", session); - }); + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file2], session); + ts.projectSystem.openFilesForSession([file], session); + // We generate only if project is created when opening file from the project + ts.projectSystem.openFilesForSession([file3], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and config file has errors", session); + }); - it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true } }` - }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file has errors but suppressDiagnosticEvents is true", session); - }); + }; + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file has errors but suppressDiagnosticEvents is true", session); + }); - it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: ts.projectSystem.File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const file3: ts.projectSystem.File = { - path: "/a/b/test2.ts", - content: "let xy = 10" - }; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: ts.projectSystem.File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const file3: ts.projectSystem.File = { + path: "/a/b/test2.ts", + content: "let xy = 10" + }; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "files": ["app.ts"] }` - }; - - const host = ts.projectSystem.createServerHost([file, file2, file3, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file2], session); - ts.projectSystem.openFilesForSession([file], session); - // We generate only if project is created when opening file from the project - ts.projectSystem.openFilesForSession([file3], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and doesnt contain any errors", session); - }); + }; + + const host = ts.projectSystem.createServerHost([file, file2, file3, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file2], session); + ts.projectSystem.openFilesForSession([file], session); + // We generate only if project is created when opening file from the project + ts.projectSystem.openFilesForSession([file3], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and doesnt contain any errors", session); + }); - it("contains the project reference errors", () => { - const file: ts.projectSystem.File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const noSuchTsconfig = "no-such-tsconfig.json"; - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("contains the project reference errors", () => { + const file: ts.projectSystem.File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const noSuchTsconfig = "no-such-tsconfig.json"; + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "files": ["app.ts"], "references": [{"path":"./${noSuchTsconfig}"}] }` - }; + }; - const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file], session); - ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events contains the project reference errors", session); - }); + const host = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file], session); + ts.projectSystem.baselineTsserverLogs("projectErrors", "configFileDiagnostic events contains the project reference errors", session); + }); +}); + +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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([f1], session); + + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const projectName = projectService.inferredProjects[0].getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: projectName } + } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.projectSystem.protocol.DiagnosticWithLinePosition[]; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: ts.server.CommandNames.CompilerOptionsForInferredProjects, + seq: 3, + arguments: { options: { module: ts.ModuleKind.CommonJS } } + } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName: projectName } + } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.projectSystem.protocol.DiagnosticWithLinePosition[]; + assert.isTrue(diagsAfterUpdate.length === 0); }); - 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 = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([f1], session); - - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const projectName = projectService.inferredProjects[0].getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: projectName } - } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.projectSystem.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: ts.server.CommandNames.CompilerOptionsForInferredProjects, - seq: 3, - arguments: { options: { module: ts.ModuleKind.CommonJS } } - } as ts.server.protocol.SetCompilerOptionsForInferredProjectsRequest); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName: projectName } - } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.projectSystem.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - - it("for external project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/project.csproj"; - const externalFiles = ts.projectSystem.toExternalFiles([f1.path]); - projectService.openExternalProject({ + it("for external project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = ts.projectSystem.createServerHost([f1, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/project.csproj"; + const externalFiles = ts.projectSystem.toExternalFiles([f1.path]); + projectService.openExternalProject({ + projectFileName, + rootFiles: externalFiles, + options: {} + } as ts.projectSystem.protocol.ExternalProject); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + + const diags = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.server.protocol.DiagnosticWithLinePosition[]; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: ts.server.CommandNames.OpenExternalProject, + seq: 3, + arguments: { projectFileName, rootFiles: externalFiles, - options: {} - } as ts.projectSystem.protocol.ExternalProject); - ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); - - const diags = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: ts.server.CommandNames.OpenExternalProject, - seq: 3, - arguments: { - projectFileName, - rootFiles: externalFiles, - options: { module: ts.ModuleKind.CommonJS } - } - } as ts.server.protocol.OpenExternalProjectRequest); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName } - } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diagsAfterUpdate.length === 0); - }); + options: { module: ts.ModuleKind.CommonJS } + } + } as ts.server.protocol.OpenExternalProjectRequest); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName } + } as ts.server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly ts.server.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 = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file], session); - - const projectService = session.getProjectService(); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const projectName = ts.projectSystem.configuredProjectAt(projectService, 0).getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - } as ts.server.protocol.SemanticDiagnosticsSyncRequest).response as readonly ts.server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 3); - - configFile.content = configFileContentWithoutCommentLine; - host.writeFile(configFile.path, configFile.content); - - const diagsAfterEdit = session.executeCommand({ - type: "request", - command: ts.server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - } as ts.server.protocol.SemanticDiagnosticsSyncRequest).response as readonly ts.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: ts.server.protocol.DiagnosticWithLinePosition, afterEditDiag: ts.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 = ts.projectSystem.createServerHost([file, ts.projectSystem.libFile, configFile]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file], session); + + const projectService = session.getProjectService(); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const projectName = ts.projectSystem.configuredProjectAt(projectService, 0).getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + } as ts.server.protocol.SemanticDiagnosticsSyncRequest).response as readonly ts.server.protocol.DiagnosticWithLinePosition[]; + assert.isTrue(diags.length === 3); + + configFile.content = configFileContentWithoutCommentLine; + host.writeFile(configFile.path, configFile.content); + + const diagsAfterEdit = session.executeCommand({ + type: "request", + command: ts.server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + } as ts.server.protocol.SemanticDiagnosticsSyncRequest).response as readonly ts.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: ts.server.protocol.DiagnosticWithLinePosition, afterEditDiag: ts.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); + } }); - - describe("unittests:: tsserver:: Project Errors with config file change", () => { - it("Updates diagnostics when '--noUnusedLabels' changes", () => { - const aTs: ts.projectSystem.File = { path: "/a.ts", content: "label: while (1) {}" }; - const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; - const tsconfig: ts.projectSystem.File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; - const host = ts.projectSystem.createServerHost([aTs, tsconfig]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([aTs], session); - - host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); - host.runQueuedTimeoutCallbacks(); - - const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as ts.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: ts.Diagnostics.Unused_label.code, - relatedInformation: undefined, - reportsUnnecessary: true, - reportsDeprecated: undefined, - source: undefined, - }, - ]); - }); +}); + +describe("unittests:: tsserver:: Project Errors with config file change", () => { + it("Updates diagnostics when '--noUnusedLabels' changes", () => { + const aTs: ts.projectSystem.File = { path: "/a.ts", content: "label: while (1) {}" }; + const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; + const tsconfig: ts.projectSystem.File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; + const host = ts.projectSystem.createServerHost([aTs, tsconfig]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([aTs], session); + + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); + host.runQueuedTimeoutCallbacks(); + + const response = ts.projectSystem.executeSessionRequest(session, ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as ts.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: ts.Diagnostics.Unused_label.code, + relatedInformation: undefined, + reportsUnnecessary: true, + reportsDeprecated: undefined, + source: undefined, + }, + ]); }); - - describe("unittests:: tsserver:: Project Errors with resolveJsonModule", () => { - function createSessionForTest({ include }: { - include: readonly string[]; - }) { - const test: ts.projectSystem.File = { - path: `${ts.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: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/test.ts`, + content: `import * as blabla from "./blabla.json"; declare var console: any; console.log(blabla);` - }; - const blabla: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/blabla.json`, - content: "{}" - }; - const tsconfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - resolveJsonModule: true, - composite: true - }, - include - }) - }; - - const host = ts.projectSystem.createServerHost([test, blabla, ts.projectSystem.libFile, tsconfig]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.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"] - }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [test] }); - ts.projectSystem.baselineTsserverLogs("projectErrors", `should not report incorrect error when json is root file found by tsconfig`, session); + }; + const blabla: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/blabla.json`, + content: "{}" + }; + const tsconfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + resolveJsonModule: true, + composite: true + }, + include + }) + }; + + const host = ts.projectSystem.createServerHost([test, blabla, ts.projectSystem.libFile, tsconfig]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.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"] }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [test] }); + ts.projectSystem.baselineTsserverLogs("projectErrors", `should not report incorrect error when json is root file found by tsconfig`, session); + }); - it("should report error when json is not root file found by tsconfig", () => { - const { host, session, test } = createSessionForTest({ - include: ["./src/*.ts"] - }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [test] }); - ts.projectSystem.baselineTsserverLogs("projectErrors", `should report error when json is not root file found by tsconfig`, session); + it("should report error when json is not root file found by tsconfig", () => { + const { host, session, test } = createSessionForTest({ + include: ["./src/*.ts"] }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [test] }); + ts.projectSystem.baselineTsserverLogs("projectErrors", `should report error when json is not root file found by tsconfig`, session); }); - - describe("unittests:: tsserver:: Project Errors with npm install when", () => { - function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { - const main: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/main.ts`, - content: "import * as _a from '@angular/core';" - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - // Move things from staging to node_modules without triggering watch - const moduleFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`, - content: `export const y = 10;` - }; - const projectFiles = [main, ts.projectSystem.libFile, config]; - const host = ts.projectSystem.createServerHost(projectFiles); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([{ file: main, projectRootPath: ts.tscWatch.projectRoot }], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [main] }); - - let npmInstallComplete = false; - - // Simulate npm install - let filesAndFoldersToAdd: (ts.projectSystem.File | ts.projectSystem.Folder)[] = [ - { path: `${ts.tscWatch.projectRoot}/node_modules` }, - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging` }, - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@babel` }, - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, - ]; - verifyWhileNpmInstall(3); - - filesAndFoldersToAdd = [ - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, - { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, - { path: `${ts.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(0); - - filesAndFoldersToAdd = []; - host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); - // Since we added/removed in .staging no timeout - verifyWhileNpmInstall(0); - - // Remove staging folder to remove errors - host.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules/.staging`, /*recursive*/ true); - npmInstallComplete = true; - projectFiles.push(moduleFile); - // Additional watch for watching script infos from node_modules - verifyWhileNpmInstall(3); - - ts.projectSystem.baselineTsserverLogs("projectErrors", `npm install when timeout occurs ${timeoutDuringPartialInstallation ? "inbetween" : "after"} installation`, session); - - function verifyWhileNpmInstall(timeouts: number) { - filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); - if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeouts); // Invalidation of failed lookups - if (timeouts) { - host.checkTimeoutQueueLengthAndRun(timeouts - 1); // Actual update - } - } - else { - host.checkTimeoutQueueLength(timeouts ? 3 : 2); +}); + +describe("unittests:: tsserver:: Project Errors with npm install when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const main: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/main.ts`, + content: "import * as _a from '@angular/core';" + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + // Move things from staging to node_modules without triggering watch + const moduleFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`, + content: `export const y = 10;` + }; + const projectFiles = [main, ts.projectSystem.libFile, config]; + const host = ts.projectSystem.createServerHost(projectFiles); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([{ file: main, projectRootPath: ts.tscWatch.projectRoot }], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [main] }); + + let npmInstallComplete = false; + + // Simulate npm install + let filesAndFoldersToAdd: (ts.projectSystem.File | ts.projectSystem.Folder)[] = [ + { path: `${ts.tscWatch.projectRoot}/node_modules` }, + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging` }, + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@babel` }, + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, + ]; + verifyWhileNpmInstall(3); + + filesAndFoldersToAdd = [ + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, + { path: `${ts.tscWatch.projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, + { path: `${ts.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(0); + + filesAndFoldersToAdd = []; + host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); + // Since we added/removed in .staging no timeout + verifyWhileNpmInstall(0); + + // Remove staging folder to remove errors + host.deleteFolder(`${ts.tscWatch.projectRoot}/node_modules/.staging`, /*recursive*/ true); + npmInstallComplete = true; + projectFiles.push(moduleFile); + // Additional watch for watching script infos from node_modules + verifyWhileNpmInstall(3); + + ts.projectSystem.baselineTsserverLogs("projectErrors", `npm install when timeout occurs ${timeoutDuringPartialInstallation ? "inbetween" : "after"} installation`, session); + + function verifyWhileNpmInstall(timeouts: number) { + filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + host.checkTimeoutQueueLengthAndRun(timeouts); // Invalidation of failed lookups + if (timeouts) { + host.checkTimeoutQueueLengthAndRun(timeouts - 1); // Actual update } - ts.projectSystem.verifyGetErrRequest({ session, host, files: [main], existingTimeouts: !npmInstallComplete && !timeoutDuringPartialInstallation ? timeouts ? 3 : 2 : undefined }); } + else { + host.checkTimeoutQueueLength(timeouts ? 3 : 2); + } + ts.projectSystem.verifyGetErrRequest({ session, host, files: [main], existingTimeouts: !npmInstallComplete && !timeoutDuringPartialInstallation ? timeouts ? 3 : 2 : undefined }); } + } - it("timeouts occur inbetween installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); - }); + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); + }); - it("timeout occurs after installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); - }); + 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 29001c8852c81..86e88c5b7baf2 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts @@ -1,95 +1,95 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: with project references and compile on save", () => { - const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; - const usageLocation = `${ts.tscWatch.projectRoot}/usage`; - const dependencyTs: ts.projectSystem.File = { - path: `${dependecyLocation}/fns.ts`, - content: `export function fn1() { } +describe("unittests:: tsserver:: with project references and compile on save", () => { + const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; + const usageLocation = `${ts.tscWatch.projectRoot}/usage`; + const dependencyTs: ts.projectSystem.File = { + path: `${dependecyLocation}/fns.ts`, + content: `export function fn1() { } export function fn2() { } ` - }; - const dependencyConfig: ts.projectSystem.File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, declarationDir: "../decls" }, - compileOnSave: true - }) - }; - const usageTs: ts.projectSystem.File = { - path: `${usageLocation}/usage.ts`, - content: `import { + }; + const dependencyConfig: ts.projectSystem.File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationDir: "../decls" }, + compileOnSave: true + }) + }; + const usageTs: ts.projectSystem.File = { + path: `${usageLocation}/usage.ts`, + content: `import { fn1, fn2, } from '../decls/fns' fn1(); fn2(); ` - }; - const usageConfig: ts.projectSystem.File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compileOnSave: true, - references: [{ path: "../dependency" }] - }) - }; - - const localChange = "function fn3() { }"; - const change = `export ${localChange}`; - const changeJs = `function fn3() { } + }; + const usageConfig: ts.projectSystem.File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compileOnSave: true, + references: [{ path: "../dependency" }] + }) + }; + + const localChange = "function fn3() { }"; + const change = `export ${localChange}`; + const changeJs = `function fn3() { } exports.fn3 = fn3;`; - const changeDts = "export declare function fn3(): void;"; - - function expectedAffectedFiles(config: ts.projectSystem.File, fileNames: readonly ts.projectSystem.File[]): ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject { - return { - projectFileName: config.path, - fileNames: fileNames.map(f => f.path), - projectUsesOutFile: false - }; - } + const changeDts = "export declare function fn3(): void;"; + + function expectedAffectedFiles(config: ts.projectSystem.File, fileNames: readonly ts.projectSystem.File[]): ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject { + return { + projectFileName: config.path, + fileNames: fileNames.map(f => f.path), + projectUsesOutFile: false + }; + } - function expectedUsageEmitFiles(appendJsText?: string): readonly ts.projectSystem.File[] { - const appendJs = appendJsText ? `${appendJsText} + function expectedUsageEmitFiles(appendJsText?: string): readonly ts.projectSystem.File[] { + const appendJs = appendJsText ? `${appendJsText} ` : ""; - return [{ - path: `${usageLocation}/usage.js`, - content: `"use strict"; + return [{ + path: `${usageLocation}/usage.js`, + content: `"use strict"; exports.__esModule = true;${appendJsText === changeJs ? "\nexports.fn3 = void 0;" : ""} var fns_1 = require("../decls/fns"); (0, fns_1.fn1)(); (0, fns_1.fn2)(); ${appendJs}` - }]; - } + }]; + } - function expectedEmitOutput(expectedFiles: readonly ts.projectSystem.File[]): ts.EmitOutput { - return { - outputFiles: expectedFiles.map(({ path, content }) => ({ - name: path, - text: content, - writeByteOrderMark: false - })), - emitSkipped: false, - diagnostics: ts.emptyArray - }; - } + function expectedEmitOutput(expectedFiles: readonly ts.projectSystem.File[]): ts.EmitOutput { + return { + outputFiles: expectedFiles.map(({ path, content }) => ({ + name: path, + text: content, + writeByteOrderMark: false + })), + emitSkipped: false, + diagnostics: ts.emptyArray + }; + } - function noEmitOutput(): ts.EmitOutput { - return { - emitSkipped: true, - outputFiles: [], - diagnostics: ts.emptyArray - }; - } + function noEmitOutput(): ts.EmitOutput { + return { + emitSkipped: true, + outputFiles: [], + diagnostics: ts.emptyArray + }; + } - function expectedDependencyEmitFiles(appendJsText?: string, appendDtsText?: string): readonly ts.projectSystem.File[] { - const appendJs = appendJsText ? `${appendJsText} + function expectedDependencyEmitFiles(appendJsText?: string, appendDtsText?: string): readonly ts.projectSystem.File[] { + const appendJs = appendJsText ? `${appendJsText} ` : ""; - const appendDts = appendDtsText ? `${appendDtsText} + const appendDts = appendDtsText ? `${appendDtsText} ` : ""; - return [ - { - path: `${dependecyLocation}/fns.js`, - content: `"use strict"; + return [ + { + path: `${dependecyLocation}/fns.js`, + content: `"use strict"; exports.__esModule = true; ${appendJsText === changeJs ? "exports.fn3 = " : ""}exports.fn2 = exports.fn1 = void 0; function fn1() { } @@ -97,2093 +97,2093 @@ exports.fn1 = fn1; function fn2() { } exports.fn2 = fn2; ${appendJs}` - }, - { - path: `${ts.tscWatch.projectRoot}/decls/fns.d.ts`, - content: `export declare function fn1(): void; + }, + { + path: `${ts.tscWatch.projectRoot}/decls/fns.d.ts`, + content: `export declare function fn1(): void; export declare function fn2(): void; ${appendDts}` + } + ]; + } + + describe("when dependency project is not open", () => { + describe("Of usageTs", () => { + it("with initial file open, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); } - ]; - } - describe("when dependency project is not open", () => { - describe("Of usageTs", () => { - it("with initial file open, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with initial file open, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with initial file open, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(changeJs); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(changeJs); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(changeJs); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(changeJs); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + }); - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + describe("Of dependencyTs in usage project", () => { + it("with initial file open, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with initial file open, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with local change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); }); - - describe("Of dependencyTs in usage project", () => { - it("with initial file open, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with initial file open, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + it("with local change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${localChange}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with local change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange + } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with local change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange + } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + host.writeFile(dependencyTs.path, `${dependencyTs.content}${change}`); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change + } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change + } }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); }); }); + }); - describe("when the depedency file is open", () => { - describe("Of usageTs", () => { - it("with initial file open, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + describe("when the depedency file is open", () => { + describe("Of usageTs", () => { + it("with initial file open, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with initial file open, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); - } + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with initial file open, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(changeJs); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(changeJs); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedUsageEmitFiles(changeJs); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: usageTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedUsageEmitFiles(changeJs); + 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 as ts.Path), `${file.path} is newly written`); + } + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: usageTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); }); + }); - describe("Of dependencyTs in usage project", () => { - it("with initial file open, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with local change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); - }); - it("with change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response; - assert.isFalse(actualEmit, "Emit files"); - assert.equal(host.writtenFiles.size, 0); - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + describe("Of dependencyTs in usage project", () => { + it("with initial file open, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with local change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange + } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); }); - - describe("Of dependencyTs", () => { - it("with initial file open, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + it("with local change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with initial file open, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + it("with change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response; + assert.isFalse(actualEmit, "Emit files"); + assert.equal(host.writtenFiles.size, 0); + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: usageConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, noEmitOutput(), "Emit output"); + }); + }); + + describe("Of dependencyTs", () => { + it("with initial file open, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with initial file open, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(localChange); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(localChange); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with local change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: localChange - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, [usageTs]), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(changeJs, changeDts); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with local change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: localChange } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to dependency, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); - const location = toLocation(dependencyTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(changeJs, changeDts); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, [usageTs]), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(changeJs, changeDts); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, without specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(usageConfig, ts.emptyArray), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to dependency, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(dependencyTs.content); + const location = toLocation(dependencyTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(changeJs, changeDts); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); - }); - it("with change to usage, with specifying project file", () => { - const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } - }); - const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); - const location = toLocation(usageTs.content.length); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: usageTs.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString: change - } - }); - host.writtenFiles.clear(); - - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response; - assert.isTrue(actualEmit, "Emit files"); - const expectedFiles = expectedDependencyEmitFiles(); - 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 as ts.Path), `${file.path} is newly written`); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, without specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change } + }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(usageConfig, ts.emptyArray), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.EmitOutput, - arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } - }).response as ts.EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); + }); + it("with change to usage, with specifying project file", () => { + const host = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(ts.projectSystem.createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, ts.projectSystem.libFile])); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([usageTs, dependencyTs], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const toLocation = ts.projectSystem.protocolToLocation(usageTs.content); + const location = toLocation(usageTs.content.length); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: usageTs.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString: change + } }); + host.writtenFiles.clear(); + + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response; + assert.isTrue(actualEmit, "Emit files"); + const expectedFiles = expectedDependencyEmitFiles(); + 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 as ts.Path), `${file.path} is newly written`); + } + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.EmitOutput, + arguments: { file: dependencyTs.path, projectFileName: dependencyConfig.path } + }).response as ts.EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput(expectedFiles), "Emit output"); }); }); }); +}); - 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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsbase.json`, - content: JSON.stringify({ - compileOnSave: true, - compilerOptions: { - module: "none", - composite: true - } - }) - }; - const buttonClass = `${ts.tscWatch.projectRoot}/buttonClass`; - const buttonConfig: ts.projectSystem.File = { - path: `${buttonClass}/tsconfig.json`, - content: JSON.stringify({ - extends: "../tsbase.json", - compilerOptions: { - outFile: "Source.js" - }, - files: ["Source.ts"] - }) - }; - const buttonSource: ts.projectSystem.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: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsbase.json`, + content: JSON.stringify({ + compileOnSave: true, + compilerOptions: { + module: "none", + composite: true + } + }) + }; + const buttonClass = `${ts.tscWatch.projectRoot}/buttonClass`; + const buttonConfig: ts.projectSystem.File = { + path: `${buttonClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const buttonSource: ts.projectSystem.File = { + path: `${buttonClass}/Source.ts`, + content: `module Hmi { export class Button { public static myStaticFunction() { } } }` - }; - - const siblingClass = `${ts.tscWatch.projectRoot}/SiblingClass`; - const siblingConfig: ts.projectSystem.File = { - path: `${siblingClass}/tsconfig.json`, - content: JSON.stringify({ - extends: "../tsbase.json", - references: [{ - path: "../buttonClass/" - }], - compilerOptions: { - outFile: "Source.js" - }, - files: ["Source.ts"] - }) - }; - const siblingSource: ts.projectSystem.File = { - path: `${siblingClass}/Source.ts`, - content: `module Hmi { + }; + + const siblingClass = `${ts.tscWatch.projectRoot}/SiblingClass`; + const siblingConfig: ts.projectSystem.File = { + path: `${siblingClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + references: [{ + path: "../buttonClass/" + }], + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const siblingSource: ts.projectSystem.File = { + path: `${siblingClass}/Source.ts`, + content: `module Hmi { export class Sibling { public mySiblingFunction() { } } }` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); - - // ts build should succeed - ts.tscWatch.ensureErrorFreeBuild(host, [siblingConfig.path]); - const sourceJs = ts.changeExtension(siblingSource.path, ".js"); - const expectedSiblingJs = host.readFile(sourceJs); - - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([siblingSource], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { - file: siblingSource.path, - projectFileName: siblingConfig.path - } - }); - assert.equal(host.readFile(sourceJs), expectedSiblingJs); + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); + + // ts build should succeed + ts.tscWatch.ensureErrorFreeBuild(host, [siblingConfig.path]); + const sourceJs = ts.changeExtension(siblingSource.path, ".js"); + const expectedSiblingJs = host.readFile(sourceJs); + + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([siblingSource], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { + file: siblingSource.path, + projectFileName: siblingConfig.path + } }); + assert.equal(host.readFile(sourceJs), expectedSiblingJs); }); -} \ No newline at end of file +}); +} diff --git a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts index bb29587fcdfaf..4d3d136b5214c 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts @@ -1,82 +1,82 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: with project references and error reporting", () => { - const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; - const usageLocation = `${ts.tscWatch.projectRoot}/usage`; - function verifyUsageAndDependency(scenario: string, dependencyTs: ts.projectSystem.File, dependencyConfig: ts.projectSystem.File, usageTs: ts.projectSystem.File, usageConfig: ts.projectSystem.File) { - function usageProjectDiagnostics(): ts.projectSystem.GetErrForProjectDiagnostics { - return { project: usageTs, files: [usageTs, dependencyTs] }; - } +describe("unittests:: tsserver:: with project references and error reporting", () => { + const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; + const usageLocation = `${ts.tscWatch.projectRoot}/usage`; + function verifyUsageAndDependency(scenario: string, dependencyTs: ts.projectSystem.File, dependencyConfig: ts.projectSystem.File, usageTs: ts.projectSystem.File, usageConfig: ts.projectSystem.File) { + function usageProjectDiagnostics(): ts.projectSystem.GetErrForProjectDiagnostics { + return { project: usageTs, files: [usageTs, dependencyTs] }; + } - function dependencyProjectDiagnostics(): ts.projectSystem.GetErrForProjectDiagnostics { - return { project: dependencyTs, files: [dependencyTs] }; - } + function dependencyProjectDiagnostics(): ts.projectSystem.GetErrForProjectDiagnostics { + return { project: dependencyTs, files: [dependencyTs] }; + } - describe("when dependency project is not open", () => { - ts.projectSystem.verifyGetErrScenario({ - scenario: "projectReferenceErrors", - subScenario: `${scenario} when dependency project is not open`, - allFiles: () => [dependencyTs, dependencyConfig, usageTs, usageConfig], - openFiles: () => [usageTs], - getErrRequest: () => [usageTs], - getErrForProjectRequest: () => [ - usageProjectDiagnostics(), - { - project: dependencyTs, - files: [dependencyTs, usageTs] - } - ], - syncDiagnostics: () => [ - // Without project - { file: usageTs }, - { file: dependencyTs }, - // With project - { file: usageTs, project: usageConfig }, - { file: dependencyTs, project: usageConfig }, - ], - }); + describe("when dependency project is not open", () => { + ts.projectSystem.verifyGetErrScenario({ + scenario: "projectReferenceErrors", + subScenario: `${scenario} when dependency project is not open`, + allFiles: () => [dependencyTs, dependencyConfig, usageTs, usageConfig], + openFiles: () => [usageTs], + getErrRequest: () => [usageTs], + getErrForProjectRequest: () => [ + usageProjectDiagnostics(), + { + project: dependencyTs, + files: [dependencyTs, usageTs] + } + ], + syncDiagnostics: () => [ + // Without project + { file: usageTs }, + { file: dependencyTs }, + // With project + { file: usageTs, project: usageConfig }, + { file: dependencyTs, project: usageConfig }, + ], }); + }); - describe("when the depedency file is open", () => { - ts.projectSystem.verifyGetErrScenario({ - scenario: "projectReferenceErrors", - subScenario: `${scenario} when the depedency file is open`, - allFiles: () => [dependencyTs, dependencyConfig, usageTs, usageConfig], - openFiles: () => [usageTs, dependencyTs], - getErrRequest: () => [usageTs, dependencyTs], - getErrForProjectRequest: () => [ - usageProjectDiagnostics(), - dependencyProjectDiagnostics() - ], - syncDiagnostics: () => [ - // Without project - { file: usageTs }, - { file: dependencyTs }, - // With project - { file: usageTs, project: usageConfig }, - { file: dependencyTs, project: usageConfig }, - { file: dependencyTs, project: dependencyConfig }, - ], - }); + describe("when the depedency file is open", () => { + ts.projectSystem.verifyGetErrScenario({ + scenario: "projectReferenceErrors", + subScenario: `${scenario} when the depedency file is open`, + allFiles: () => [dependencyTs, dependencyConfig, usageTs, usageConfig], + openFiles: () => [usageTs, dependencyTs], + getErrRequest: () => [usageTs, dependencyTs], + getErrForProjectRequest: () => [ + usageProjectDiagnostics(), + dependencyProjectDiagnostics() + ], + syncDiagnostics: () => [ + // Without project + { file: usageTs }, + { file: dependencyTs }, + // With project + { file: usageTs, project: usageConfig }, + { file: dependencyTs, project: usageConfig }, + { file: dependencyTs, project: dependencyConfig }, + ], }); - } + }); + } - describe("with module scenario", () => { - const dependencyTs: ts.projectSystem.File = { - path: `${dependecyLocation}/fns.ts`, - content: `export function fn1() { } + describe("with module scenario", () => { + const dependencyTs: ts.projectSystem.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: ts.projectSystem.File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, declarationDir: "../decls" } }) - }; - const usageTs: ts.projectSystem.File = { - path: `${usageLocation}/usage.ts`, - content: `import { + }; + const dependencyConfig: ts.projectSystem.File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationDir: "../decls" } }) + }; + const usageTs: ts.projectSystem.File = { + path: `${usageLocation}/usage.ts`, + content: `import { fn1, fn2, fnErr @@ -85,46 +85,46 @@ fn1(); fn2(); fnErr(); ` - }; - const usageConfig: ts.projectSystem.File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true }, - references: [{ path: "../dependency" }] - }) - }; - verifyUsageAndDependency("with module scenario", dependencyTs, dependencyConfig, usageTs, usageConfig); - }); + }; + const usageConfig: ts.projectSystem.File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true }, + references: [{ path: "../dependency" }] + }) + }; + verifyUsageAndDependency("with module scenario", dependencyTs, dependencyConfig, usageTs, usageConfig); + }); - describe("with non module --out", () => { - const dependencyTs: ts.projectSystem.File = { - path: `${dependecyLocation}/fns.ts`, - content: `function fn1() { } + describe("with non module --out", () => { + const dependencyTs: ts.projectSystem.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: ts.projectSystem.File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, outFile: "../dependency.js" } }) - }; - const usageTs: ts.projectSystem.File = { - path: `${usageLocation}/usage.ts`, - content: `fn1(); + }; + const dependencyConfig: ts.projectSystem.File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, outFile: "../dependency.js" } }) + }; + const usageTs: ts.projectSystem.File = { + path: `${usageLocation}/usage.ts`, + content: `fn1(); fn2(); fnErr(); ` - }; - const usageConfig: ts.projectSystem.File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, outFile: "../usage.js" }, - references: [{ path: "../dependency" }] - }) - }; - verifyUsageAndDependency("with non module", dependencyTs, dependencyConfig, usageTs, usageConfig); - }); + }; + const usageConfig: ts.projectSystem.File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, outFile: "../usage.js" }, + references: [{ path: "../dependency" }] + }) + }; + verifyUsageAndDependency("with non module", dependencyTs, dependencyConfig, usageTs, usageConfig); }); +}); } diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 2d511112bf3ef..6821376987551 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1,471 +1,471 @@ namespace ts.projectSystem { - export function createHostWithSolutionBuild(files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) { - const host = ts.projectSystem.createServerHost(files); - // ts build should succeed - ts.tscWatch.ensureErrorFreeBuild(host, rootNames); - return host; - } - - describe("unittests:: tsserver:: with project references and tsbuild", () => { - describe("with container project", () => { - function getProjectFiles(project: string): [ - ts.projectSystem.File, - ts.projectSystem.File - ] { - return [ - ts.TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), - ts.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 = ts.TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); - const files = [ts.projectSystem.libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; - - it("does not error on container only project", () => { - const host = createHostWithSolutionBuild(files, [containerConfig.path]); +export function createHostWithSolutionBuild(files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) { + const host = ts.projectSystem.createServerHost(files); + // ts build should succeed + ts.tscWatch.ensureErrorFreeBuild(host, rootNames); + return host; +} - // Open external project for the folder - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - const service = session.getProjectService(); - service.openExternalProjects([{ - projectFileName: ts.TestFSWithWatch.getTsBuildProjectFilePath(project, project), - rootFiles: files.map(f => ({ fileName: f.path })), - options: {} - }]); - files.forEach(f => { - const args: ts.projectSystem.protocol.FileRequestArgs = { - file: f.path, - projectFileName: ts.endsWith(f.path, "tsconfig.json") ? f.path : undefined - }; - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: args - }); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: args - }); +describe("unittests:: tsserver:: with project references and tsbuild", () => { + describe("with container project", () => { + function getProjectFiles(project: string): [ + ts.projectSystem.File, + ts.projectSystem.File + ] { + return [ + ts.TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), + ts.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 = ts.TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); + const files = [ts.projectSystem.libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; + + it("does not error on container only project", () => { + const host = createHostWithSolutionBuild(files, [containerConfig.path]); + + // Open external project for the folder + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + const service = session.getProjectService(); + service.openExternalProjects([{ + projectFileName: ts.TestFSWithWatch.getTsBuildProjectFilePath(project, project), + rootFiles: files.map(f => ({ fileName: f.path })), + options: {} + }]); + files.forEach(f => { + const args: ts.projectSystem.protocol.FileRequestArgs = { + file: f.path, + projectFileName: ts.endsWith(f.path, "tsconfig.json") ? f.path : undefined + }; + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: args }); - const containerProject = service.configuredProjects.get(containerConfig.path)!; - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.CompilerOptionsDiagnosticsFull, - arguments: { projectFileName: containerProject.projectName } + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: args }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `does not error on container only project`, session); }); - - it("can successfully find references with --out options", () => { - const host = createHostWithSolutionBuild(files, [containerConfig.path]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([containerCompositeExec[1]], session); - const myConstStart = ts.projectSystem.protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Rename, - arguments: { file: containerCompositeExec[1].path, ...myConstStart } - }); - - ts.projectSystem.baselineTsserverLogs("projectReferences", `can successfully find references with out option`, session); + const containerProject = service.configuredProjects.get(containerConfig.path)!; + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.CompilerOptionsDiagnosticsFull, + arguments: { projectFileName: containerProject.projectName } }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `does not error on container only project`, session); + }); - it("ancestor and project ref management", () => { - const tempFile: ts.projectSystem.File = { - path: `/user/username/projects/temp/temp.ts`, - content: "let x = 10" - }; - const host = createHostWithSolutionBuild(files.concat([tempFile]), [containerConfig.path]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([containerCompositeExec[1]], session); - const service = session.getProjectService(); - - // Open temp file and verify all projects alive - ts.projectSystem.openFilesForSession([tempFile], session); - - // Ref projects are loaded after as part of this command - const locationOfMyConst = ts.projectSystem.protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Rename, - arguments: { - file: containerCompositeExec[1].path, - ...locationOfMyConst - } - }); - - // Open temp file and verify all projects alive - service.closeClientFile(tempFile.path); - ts.projectSystem.openFilesForSession([tempFile], session); - - // Close all files and open temp file, only inferred project should be alive - service.closeClientFile(containerCompositeExec[1].path); - service.closeClientFile(tempFile.path); - ts.projectSystem.openFilesForSession([tempFile], session); - ts.projectSystem.baselineTsserverLogs("projectReferences", `ancestor and project ref management`, session); + it("can successfully find references with --out options", () => { + const host = createHostWithSolutionBuild(files, [containerConfig.path]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([containerCompositeExec[1]], session); + const myConstStart = ts.projectSystem.protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Rename, + arguments: { file: containerCompositeExec[1].path, ...myConstStart } }); + + ts.projectSystem.baselineTsserverLogs("projectReferences", `can successfully find references with out option`, session); }); - describe("when root file is file from referenced project", () => { - function verify(disableSourceOfProjectReferenceRedirect: boolean) { - const projectLocation = `/user/username/projects/project`; - const commonConfig: ts.projectSystem.File = { - path: `${projectLocation}/src/common/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - declarationMap: true, - outDir: "../../out", - baseUrl: "..", - disableSourceOfProjectReferenceRedirect - }, - include: ["./**/*"] - }) - }; - const keyboardTs: ts.projectSystem.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: ts.projectSystem.File = { - path: `${projectLocation}/src/common/input/keyboard.test.ts`, - content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; -function testEvaluateKeyboardEvent() { - return evaluateKeyboardEvent(); -} -` - }; - const srcConfig: ts.projectSystem.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: ts.projectSystem.File = { - path: `${projectLocation}/src/terminal.ts`, - content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; -function foo() { - return evaluateKeyboardEvent(); -} -` - }; - const host = createHostWithSolutionBuild([commonConfig, keyboardTs, keyboardTestTs, srcConfig, terminalTs, ts.projectSystem.libFile], [srcConfig.path]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([keyboardTs, terminalTs], session); + it("ancestor and project ref management", () => { + const tempFile: ts.projectSystem.File = { + path: `/user/username/projects/temp/temp.ts`, + content: "let x = 10" + }; + const host = createHostWithSolutionBuild(files.concat([tempFile]), [containerConfig.path]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + + // Open temp file and verify all projects alive + ts.projectSystem.openFilesForSession([tempFile], session); + + // Ref projects are loaded after as part of this command + const locationOfMyConst = ts.projectSystem.protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Rename, + arguments: { + file: containerCompositeExec[1].path, + ...locationOfMyConst + } + }); - const searchStr = "evaluateKeyboardEvent"; - const importStr = `import { evaluateKeyboardEvent } from 'common/input/keyboard';`; - const result = session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(keyboardTs, searchStr) - }).response as ts.projectSystem.protocol.ReferencesResponseBody; - assert.deepEqual(result, { - refs: [ - ts.projectSystem.makeReferenceItem({ - file: keyboardTs, - text: searchStr, - contextText: `export function evaluateKeyboardEvent() { }`, - isDefinition: true, - lineText: `export function evaluateKeyboardEvent() { }` - }), - ts.projectSystem.makeReferenceItem({ - file: keyboardTestTs, - text: searchStr, - contextText: importStr, - isDefinition: false, - isWriteAccess: true, - lineText: importStr - }), - ts.projectSystem.makeReferenceItem({ - file: keyboardTestTs, - text: searchStr, - options: { index: 1 }, - isDefinition: false, - lineText: ` return evaluateKeyboardEvent();` - }), - ts.projectSystem.makeReferenceItem({ - file: terminalTs, - text: searchStr, - contextText: importStr, - isDefinition: false, - isWriteAccess: true, - lineText: importStr - }), - ts.projectSystem.makeReferenceItem({ - file: terminalTs, - text: searchStr, - options: { index: 1 }, - isDefinition: false, - lineText: ` return evaluateKeyboardEvent();` - }), - ], - symbolName: searchStr, - symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(keyboardTs.content, searchStr).offset, - symbolDisplayString: "function evaluateKeyboardEvent(): void" - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `root file is file from referenced project${disableSourceOfProjectReferenceRedirect ? " and using declaration maps" : ""}`, session); - } + // Open temp file and verify all projects alive + service.closeClientFile(tempFile.path); + ts.projectSystem.openFilesForSession([tempFile], session); - 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); - }); + // Close all files and open temp file, only inferred project should be alive + service.closeClientFile(containerCompositeExec[1].path); + service.closeClientFile(tempFile.path); + ts.projectSystem.openFilesForSession([tempFile], session); + ts.projectSystem.baselineTsserverLogs("projectReferences", `ancestor and project ref management`, session); }); + }); - it("reusing d.ts files from composite and non composite projects", () => { - const configA: ts.projectSystem.File = { - path: `${ts.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: ts.projectSystem.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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositea/a.ts`, - content: `import { b } from "@ref/compositeb/b";` - }; - const a2Ts: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositea/a2.ts`, - content: `export const x = 10;` - }; - const configB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositeb/tsconfig.json`, - content: configA.content - }; - const bTs: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositeb/b.ts`, - content: "export function b() {}" + const keyboardTs: ts.projectSystem.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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/dist/compositeb/b.d.ts`, - content: "export declare function b(): void;" + const keyboardTestTs: ts.projectSystem.File = { + path: `${projectLocation}/src/common/input/keyboard.test.ts`, + content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; +function testEvaluateKeyboardEvent() { + return evaluateKeyboardEvent(); +} +` }; - const configC: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositec/tsconfig.json`, + const srcConfig: ts.projectSystem.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: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/compositec/c.ts`, - content: aTs.content + const terminalTs: ts.projectSystem.File = { + path: `${projectLocation}/src/terminal.ts`, + content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; +function foo() { + return evaluateKeyboardEvent(); +} +` }; - const files = [ts.projectSystem.libFile, aTs, a2Ts, configA, bDts, bTs, configB, cTs, configC]; - const host = ts.projectSystem.createServerHost(files); - const service = ts.projectSystem.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); - ts.projectSystem.checkProjectActualFiles(projectA, [aTs.path, a2Ts.path, bDts.path, ts.projectSystem.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)!; - ts.projectSystem.checkProjectActualFiles(projectC, [cTs.path, bTs.path, ts.projectSystem.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 = createHostWithSolutionBuild([commonConfig, keyboardTs, keyboardTestTs, srcConfig, terminalTs, ts.projectSystem.libFile], [srcConfig.path]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([keyboardTs, terminalTs], session); + + const searchStr = "evaluateKeyboardEvent"; + const importStr = `import { evaluateKeyboardEvent } from 'common/input/keyboard';`; + const result = session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(keyboardTs, searchStr) + }).response as ts.projectSystem.protocol.ReferencesResponseBody; + assert.deepEqual(result, { + refs: [ + ts.projectSystem.makeReferenceItem({ + file: keyboardTs, + text: searchStr, + contextText: `export function evaluateKeyboardEvent() { }`, + isDefinition: true, + lineText: `export function evaluateKeyboardEvent() { }` + }), + ts.projectSystem.makeReferenceItem({ + file: keyboardTestTs, + text: searchStr, + contextText: importStr, + isDefinition: false, + isWriteAccess: true, + lineText: importStr + }), + ts.projectSystem.makeReferenceItem({ + file: keyboardTestTs, + text: searchStr, + options: { index: 1 }, + isDefinition: false, + lineText: ` return evaluateKeyboardEvent();` + }), + ts.projectSystem.makeReferenceItem({ + file: terminalTs, + text: searchStr, + contextText: importStr, + isDefinition: false, + isWriteAccess: true, + lineText: importStr + }), + ts.projectSystem.makeReferenceItem({ + file: terminalTs, + text: searchStr, + options: { index: 1 }, + isDefinition: false, + lineText: ` return evaluateKeyboardEvent();` + }), + ], + symbolName: searchStr, + symbolStartOffset: ts.projectSystem.protocolLocationFromSubstring(keyboardTs.content, searchStr).offset, + symbolDisplayString: "function evaluateKeyboardEvent(): void" + }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `root file is file from referenced project${disableSourceOfProjectReferenceRedirect ? " and using declaration maps" : ""}`, session); + } + + 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); + }); + }); - describe("when references are monorepo like with symlinks", () => { - interface Packages { - bPackageJson: ts.projectSystem.File; - aTest: ts.projectSystem.File; - bFoo: ts.projectSystem.File; - bBar: ts.projectSystem.File; - bSymlink: ts.projectSystem.SymLink; - } - function verifySymlinkScenario(scenario: string, packages: () => Packages) { - describe(`${scenario}: when solution is not built`, () => { - it("with preserveSymlinks turned off", () => { - verifySession(scenario, packages(), /*alreadyBuilt*/ false, {}); - }); - - it("with preserveSymlinks turned on", () => { - verifySession(scenario, packages(), /*alreadyBuilt*/ false, { preserveSymlinks: true }); - }); + it("reusing d.ts files from composite and non composite projects", () => { + const configA: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositea/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../dist/", + rootDir: "../", + baseUrl: "../", + paths: { "@ref/*": ["./dist/*"] } + } + }) + }; + const aTs: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositea/a.ts`, + content: `import { b } from "@ref/compositeb/b";` + }; + const a2Ts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositea/a2.ts`, + content: `export const x = 10;` + }; + const configB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositeb/tsconfig.json`, + content: configA.content + }; + const bTs: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositeb/b.ts`, + content: "export function b() {}" + }; + const bDts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/dist/compositeb/b.d.ts`, + content: "export declare function b(): void;" + }; + const configC: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositec/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../dist/", + rootDir: "../", + baseUrl: "../", + paths: { "@ref/*": ["./*"] } + }, + references: [{ path: "../compositeb" }] + }) + }; + const cTs: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/compositec/c.ts`, + content: aTs.content + }; + const files = [ts.projectSystem.libFile, aTs, a2Ts, configA, bDts, bTs, configB, cTs, configC]; + const host = ts.projectSystem.createServerHost(files); + const service = ts.projectSystem.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); + ts.projectSystem.checkProjectActualFiles(projectA, [aTs.path, a2Ts.path, bDts.path, ts.projectSystem.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)!; + ts.projectSystem.checkProjectActualFiles(projectC, [cTs.path, bTs.path, ts.projectSystem.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: ts.projectSystem.File; + aTest: ts.projectSystem.File; + bFoo: ts.projectSystem.File; + bBar: ts.projectSystem.File; + bSymlink: ts.projectSystem.SymLink; + } + function verifySymlinkScenario(scenario: string, packages: () => Packages) { + describe(`${scenario}: when solution is not built`, () => { + it("with preserveSymlinks turned off", () => { + verifySession(scenario, packages(), /*alreadyBuilt*/ false, {}); }); - describe(`${scenario}: when solution is already built`, () => { - it("with preserveSymlinks turned off", () => { - verifySession(scenario, packages(), /*alreadyBuilt*/ true, {}); - }); + it("with preserveSymlinks turned on", () => { + verifySession(scenario, packages(), /*alreadyBuilt*/ false, { preserveSymlinks: true }); + }); + }); - it("with preserveSymlinks turned on", () => { - verifySession(scenario, packages(), /*alreadyBuilt*/ true, { preserveSymlinks: true }); - }); + describe(`${scenario}: when solution is already built`, () => { + it("with preserveSymlinks turned off", () => { + verifySession(scenario, packages(), /*alreadyBuilt*/ true, {}); }); - } - function verifySession(scenario: string, { bPackageJson, aTest, bFoo, bBar, bSymlink }: Packages, alreadyBuilt: boolean, extraOptions: ts.CompilerOptions) { - const aConfig = config("A", extraOptions, ["../B"]); - const bConfig = config("B", extraOptions); - const files = [ts.projectSystem.libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink]; - const host = alreadyBuilt ? - createHostWithSolutionBuild(files, [aConfig.path]) : - ts.projectSystem.createServerHost(files); - - // Create symlink in node module - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([aTest], session); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [aTest] }); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ - fileName: aTest.path, - textChanges: [{ - newText: "\n", - start: { line: 5, offset: 1 }, - end: { line: 5, offset: 1 } - }] - }] - } + it("with preserveSymlinks turned on", () => { + verifySession(scenario, packages(), /*alreadyBuilt*/ true, { preserveSymlinks: true }); }); - ts.projectSystem.verifyGetErrRequest({ session, host, files: [aTest] }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `monorepo like with symlinks ${scenario} and solution is ${alreadyBuilt ? "built" : "not built"}${extraOptions.preserveSymlinks ? " with preserveSymlinks" : ""}`, session); - } + }); + } - function config(packageName: string, extraOptions: ts.CompilerOptions, references?: string[]): ts.projectSystem.File { - return { - path: `${ts.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 verifySession(scenario: string, { bPackageJson, aTest, bFoo, bBar, bSymlink }: Packages, alreadyBuilt: boolean, extraOptions: ts.CompilerOptions) { + const aConfig = config("A", extraOptions, ["../B"]); + const bConfig = config("B", extraOptions); + const files = [ts.projectSystem.libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink]; + const host = alreadyBuilt ? + createHostWithSolutionBuild(files, [aConfig.path]) : + ts.projectSystem.createServerHost(files); - function file(packageName: string, fileName: string, content: string): ts.projectSystem.File { - return { - path: `${ts.tscWatch.projectRoot}/packages/${packageName}/src/${fileName}`, - content - }; - } + // Create symlink in node module + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([aTest], session); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [aTest] }); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: aTest.path, + textChanges: [{ + newText: "\n", + start: { line: 5, offset: 1 }, + end: { line: 5, offset: 1 } + }] + }] + } + }); + ts.projectSystem.verifyGetErrRequest({ session, host, files: [aTest] }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `monorepo like with symlinks ${scenario} and solution is ${alreadyBuilt ? "built" : "not built"}${extraOptions.preserveSymlinks ? " with preserveSymlinks" : ""}`, session); + } - function verifyMonoRepoLike(scope = "") { - verifySymlinkScenario(`when packageJson has types field and has index.ts${scope ? " with scoped package" : ""}`, () => ({ - bPackageJson: { - path: `${ts.tscWatch.projectRoot}/packages/B/package.json`, - content: JSON.stringify({ - main: "lib/index.js", - types: "lib/index.d.ts" - }) + function config(packageName: string, extraOptions: ts.CompilerOptions, references?: string[]): ts.projectSystem.File { + return { + path: `${ts.tscWatch.projectRoot}/packages/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "lib", + rootDir: "src", + composite: true, + ...extraOptions }, - aTest: file("A", "index.ts", `import { foo } from '${scope}b'; + include: ["src"], + ...(references ? { references: references.map(path => ({ path })) } : {}) + }) + }; + } + + function file(packageName: string, fileName: string, content: string): ts.projectSystem.File { + return { + path: `${ts.tscWatch.projectRoot}/packages/${packageName}/src/${fileName}`, + content + }; + } + + function verifyMonoRepoLike(scope = "") { + verifySymlinkScenario(`when packageJson has types field and has index.ts${scope ? " with scoped package" : ""}`, () => ({ + bPackageJson: { + path: `${ts.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'; 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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${ts.tscWatch.projectRoot}/packages/B` - } - })); - - verifySymlinkScenario(`when referencing file from subFolder${scope ? " with scoped package" : ""}`, () => ({ - bPackageJson: { - path: `${ts.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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, + symLink: `${ts.tscWatch.projectRoot}/packages/B` + } + })); + + verifySymlinkScenario(`when referencing file from subFolder${scope ? " with scoped package" : ""}`, () => ({ + bPackageJson: { + path: `${ts.tscWatch.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: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${ts.tscWatch.projectRoot}/packages/B` - } - })); - } + bFoo: file("B", "foo.ts", `export function foo() { }`), + bBar: file("B", "bar/foo.ts", `export function bar() { }`), + bSymlink: { + path: `${ts.tscWatch.projectRoot}/node_modules/${scope}b`, + symLink: `${ts.tscWatch.projectRoot}/packages/B` + } + })); + } - describe("when package is not scoped", () => { - verifyMonoRepoLike(); - }); - describe("when package is scoped", () => { - verifyMonoRepoLike("@issue/"); - }); + describe("when package is not scoped", () => { + verifyMonoRepoLike(); }); + describe("when package is scoped", () => { + verifyMonoRepoLike("@issue/"); + }); + }); - it("when the referenced projects have allowJs and emitDeclarationOnly", () => { - const compositeConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/emit-composite/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - allowJs: true, - emitDeclarationOnly: true, - outDir: "lib", - rootDir: "src" - }, - include: ["src"] - }) - }; - const compositePackageJson: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/emit-composite/package.json`, - content: JSON.stringify({ - name: "emit-composite", - version: "1.0.0", - main: "src/index.js", - typings: "lib/index.d.ts" - }) - }; - const compositeIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/emit-composite/src/index.js`, - content: `const testModule = require('./testModule'); + it("when the referenced projects have allowJs and emitDeclarationOnly", () => { + const compositeConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/emit-composite/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + allowJs: true, + emitDeclarationOnly: true, + outDir: "lib", + rootDir: "src" + }, + include: ["src"] + }) + }; + const compositePackageJson: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/emit-composite/package.json`, + content: JSON.stringify({ + name: "emit-composite", + version: "1.0.0", + main: "src/index.js", + typings: "lib/index.d.ts" + }) + }; + const compositeIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/emit-composite/src/index.js`, + content: `const testModule = require('./testModule'); module.exports = { ...testModule }` - }; - const compositeTestModule: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/emit-composite/src/testModule.js`, - content: `/** + }; + const compositeTestModule: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/emit-composite/src/testModule.js`, + content: `/** * @param {string} arg */ const testCompositeFunction = (arg) => { @@ -473,974 +473,974 @@ module.exports = { module.exports = { testCompositeFunction }` - }; - const consumerConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/consumer/tsconfig.json`, - content: JSON.stringify({ - include: ["src"], - references: [{ path: "../emit-composite" }] - }) - }; - const consumerIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/packages/consumer/src/index.ts`, - content: `import { testCompositeFunction } from 'emit-composite'; + }; + const consumerConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/consumer/tsconfig.json`, + content: JSON.stringify({ + include: ["src"], + references: [{ path: "../emit-composite" }] + }) + }; + const consumerIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/packages/consumer/src/index.ts`, + content: `import { testCompositeFunction } from 'emit-composite'; testCompositeFunction('why hello there'); testCompositeFunction('why hello there', 42);` - }; - const symlink: ts.projectSystem.SymLink = { - path: `${ts.tscWatch.projectRoot}/node_modules/emit-composite`, - symLink: `${ts.tscWatch.projectRoot}/packages/emit-composite` - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, compositeConfig, compositePackageJson, compositeIndex, compositeTestModule, consumerConfig, consumerIndex, symlink], { useCaseSensitiveFileNames: true }); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([consumerIndex], session); - ts.projectSystem.verifyGetErrRequest({ host, session, files: [consumerIndex] }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `when the referenced projects have allowJs and emitDeclarationOnly`, session); - }); + }; + const symlink: ts.projectSystem.SymLink = { + path: `${ts.tscWatch.projectRoot}/node_modules/emit-composite`, + symLink: `${ts.tscWatch.projectRoot}/packages/emit-composite` + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, compositeConfig, compositePackageJson, compositeIndex, compositeTestModule, consumerConfig, consumerIndex, symlink], { useCaseSensitiveFileNames: true }); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([consumerIndex], session); + ts.projectSystem.verifyGetErrRequest({ host, session, files: [consumerIndex] }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `when the referenced projects have allowJs and emitDeclarationOnly`, session); + }); - it("when finding local reference doesnt load ancestor/sibling projects", () => { - const solutionLocation = "/user/username/projects/solution"; - const solution: ts.projectSystem.File = { - path: `${solutionLocation}/tsconfig.json`, - content: JSON.stringify({ - files: [], - include: [], - references: [ - { path: "./compiler" }, - { path: "./services" }, - ] - }) - }; - const compilerConfig: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - module: "none" - }, - files: ["./types.ts", "./program.ts"] - }) - }; - const typesFile: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/types.ts`, - content: ` + it("when finding local reference doesnt load ancestor/sibling projects", () => { + const solutionLocation = "/user/username/projects/solution"; + const solution: ts.projectSystem.File = { + path: `${solutionLocation}/tsconfig.json`, + content: JSON.stringify({ + files: [], + include: [], + references: [ + { path: "./compiler" }, + { path: "./services" }, + ] + }) + }; + const compilerConfig: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + module: "none" + }, + files: ["./types.ts", "./program.ts"] + }) + }; + const typesFile: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/types.ts`, + content: ` namespace ts { export interface Program { getSourceFiles(): string[]; } }` - }; - const programFile: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/program.ts`, - content: ` + }; + const programFile: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/program.ts`, + content: ` namespace ts { export const program: Program = { getSourceFiles: () => [getSourceFile()] }; function getSourceFile() { return "something"; } }` - }; - const servicesConfig: ts.projectSystem.File = { - path: `${solutionLocation}/services/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true - }, - files: ["./services.ts"], - references: [ - { path: "../compiler" } - ] - }) - }; - const servicesFile: ts.projectSystem.File = { - path: `${solutionLocation}/services/services.ts`, - content: ` + }; + const servicesConfig: ts.projectSystem.File = { + path: `${solutionLocation}/services/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true + }, + files: ["./services.ts"], + references: [ + { path: "../compiler" } + ] + }) + }; + const servicesFile: ts.projectSystem.File = { + path: `${solutionLocation}/services/services.ts`, + content: ` namespace ts { const result = program.getSourceFiles(); }` - }; + }; - const files = [ts.projectSystem.libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([programFile], session); - - // Find all references for getSourceFile - // Shouldnt load more projects - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFile", { index: 1 }) - }); + const files = [ts.projectSystem.libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([programFile], session); + + // Find all references for getSourceFile + // Shouldnt load more projects + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFile", { index: 1 }) + }); - // Find all references for getSourceFiles - // Should load more projects - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFiles") - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `finding local reference doesnt load ancestor/sibling projects`, session); + // Find all references for getSourceFiles + // Should load more projects + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFiles") }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `finding local reference doesnt load ancestor/sibling projects`, session); + }); - describe("special handling of localness of the definitions for findAllRefs", () => { - function verify(scenario: string, definition: string, usage: string, referenceTerm: string) { - it(scenario, () => { - const solutionLocation = "/user/username/projects/solution"; - const solution: ts.projectSystem.File = { - path: `${solutionLocation}/tsconfig.json`, - content: JSON.stringify({ - files: [], - references: [ - { path: "./api" }, - { path: "./app" }, - ] - }) - }; - const apiConfig: ts.projectSystem.File = { - path: `${solutionLocation}/api/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "dist", - rootDir: "src", - }, - include: ["src"], - references: [{ path: "../shared" }] - }) - }; - const apiFile: ts.projectSystem.File = { - path: `${solutionLocation}/api/src/server.ts`, - content: `import * as shared from "../../shared/dist"; + describe("special handling of localness of the definitions for findAllRefs", () => { + function verify(scenario: string, definition: string, usage: string, referenceTerm: string) { + it(scenario, () => { + const solutionLocation = "/user/username/projects/solution"; + const solution: ts.projectSystem.File = { + path: `${solutionLocation}/tsconfig.json`, + content: JSON.stringify({ + files: [], + references: [ + { path: "./api" }, + { path: "./app" }, + ] + }) + }; + const apiConfig: ts.projectSystem.File = { + path: `${solutionLocation}/api/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "dist", + rootDir: "src", + }, + include: ["src"], + references: [{ path: "../shared" }] + }) + }; + const apiFile: ts.projectSystem.File = { + path: `${solutionLocation}/api/src/server.ts`, + content: `import * as shared from "../../shared/dist"; ${usage}` - }; - const appConfig: ts.projectSystem.File = { - path: `${solutionLocation}/app/tsconfig.json`, - content: apiConfig.content - }; - const appFile: ts.projectSystem.File = { - path: `${solutionLocation}/app/src/app.ts`, - content: apiFile.content - }; - const sharedConfig: ts.projectSystem.File = { - path: `${solutionLocation}/shared/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "dist", - rootDir: "src", - }, - include: ["src"] - }) - }; - const sharedFile: ts.projectSystem.File = { - path: `${solutionLocation}/shared/src/index.ts`, - content: definition - }; - const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, solution, ts.projectSystem.libFile, apiConfig, apiFile, appConfig, appFile, sharedConfig, sharedFile]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([apiFile], session); - - // Find all references - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(apiFile, referenceTerm) - }); + }; + const appConfig: ts.projectSystem.File = { + path: `${solutionLocation}/app/tsconfig.json`, + content: apiConfig.content + }; + const appFile: ts.projectSystem.File = { + path: `${solutionLocation}/app/src/app.ts`, + content: apiFile.content + }; + const sharedConfig: ts.projectSystem.File = { + path: `${solutionLocation}/shared/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "dist", + rootDir: "src", + }, + include: ["src"] + }) + }; + const sharedFile: ts.projectSystem.File = { + path: `${solutionLocation}/shared/src/index.ts`, + content: definition + }; + const host = ts.projectSystem.createServerHost([ts.projectSystem.libFile, solution, ts.projectSystem.libFile, apiConfig, apiFile, appConfig, appFile, sharedConfig, sharedFile]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([apiFile], session); - ts.projectSystem.baselineTsserverLogs("projectReferences", `special handling of localness ${scenario}`, session); + // Find all references + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(apiFile, referenceTerm) }); - } - verify("when using arrow function assignment", `export const dog = () => { };`, `shared.dog();`, "dog"); - verify("when using arrow function as object literal property types", `export const foo = { bar: () => { } };`, `shared.foo.bar();`, "bar"); - verify("when using object literal property", `export const foo = { baz: "BAZ" };`, `shared.foo.baz;`, "baz"); - verify("when using method of class expression", `export const foo = class { fly() {} };`, `const instance = new shared.foo(); + ts.projectSystem.baselineTsserverLogs("projectReferences", `special handling of localness ${scenario}`, session); + }); + } + + verify("when using arrow function assignment", `export const dog = () => { };`, `shared.dog();`, "dog"); + verify("when using arrow function as object literal property types", `export const foo = { bar: () => { } };`, `shared.foo.bar();`, "bar"); + verify("when using object literal property", `export const foo = { baz: "BAZ" };`, `shared.foo.baz;`, "baz"); + verify("when using method of class expression", `export const foo = class { fly() {} };`, `const instance = new shared.foo(); instance.fly();`, "fly"); - verify( - // when using arrow function as object literal property is loaded through indirect assignment with original declaration local to project is treated as local - "when using arrow function as object literal property", `const local = { bar: () => { } }; + verify( + // when using arrow function as object literal property is loaded through indirect assignment with original declaration local to project is treated as local + "when using arrow function as object literal property", `const local = { bar: () => { } }; export const foo = local;`, `shared.foo.bar();`, "bar"); - }); + }); - it("when disableSolutionSearching is true, solution and siblings are not loaded", () => { - const solutionLocation = "/user/username/projects/solution"; - const solution: ts.projectSystem.File = { - path: `${solutionLocation}/tsconfig.json`, - content: JSON.stringify({ - files: [], - include: [], - references: [ - { path: "./compiler" }, - { path: "./services" }, - ] - }) - }; - const compilerConfig: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - module: "none", - disableSolutionSearching: true - }, - files: ["./types.ts", "./program.ts"] - }) - }; - const typesFile: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/types.ts`, - content: ` + it("when disableSolutionSearching is true, solution and siblings are not loaded", () => { + const solutionLocation = "/user/username/projects/solution"; + const solution: ts.projectSystem.File = { + path: `${solutionLocation}/tsconfig.json`, + content: JSON.stringify({ + files: [], + include: [], + references: [ + { path: "./compiler" }, + { path: "./services" }, + ] + }) + }; + const compilerConfig: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + module: "none", + disableSolutionSearching: true + }, + files: ["./types.ts", "./program.ts"] + }) + }; + const typesFile: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/types.ts`, + content: ` namespace ts { export interface Program { getSourceFiles(): string[]; } }` - }; - const programFile: ts.projectSystem.File = { - path: `${solutionLocation}/compiler/program.ts`, - content: ` + }; + const programFile: ts.projectSystem.File = { + path: `${solutionLocation}/compiler/program.ts`, + content: ` namespace ts { export const program: Program = { getSourceFiles: () => [getSourceFile()] }; function getSourceFile() { return "something"; } }` - }; - const servicesConfig: ts.projectSystem.File = { - path: `${solutionLocation}/services/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true - }, - files: ["./services.ts"], - references: [ - { path: "../compiler" } - ] - }) - }; - const servicesFile: ts.projectSystem.File = { - path: `${solutionLocation}/services/services.ts`, - content: ` + }; + const servicesConfig: ts.projectSystem.File = { + path: `${solutionLocation}/services/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true + }, + files: ["./services.ts"], + references: [ + { path: "../compiler" } + ] + }) + }; + const servicesFile: ts.projectSystem.File = { + path: `${solutionLocation}/services/services.ts`, + content: ` namespace ts { const result = program.getSourceFiles(); }` - }; + }; - const files = [ts.projectSystem.libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([programFile], session); - - // Find all references - // No new solutions/projects loaded - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFiles") - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `with disableSolutionSearching solution and siblings are not loaded`, session); + const files = [ts.projectSystem.libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([programFile], session); + + // Find all references + // No new solutions/projects loaded + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(programFile, "getSourceFiles") }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `with disableSolutionSearching solution and siblings are not loaded`, session); + }); - describe("when default project is solution project", () => { - interface Setup { - scenario: string; - solutionOptions?: ts.CompilerOptions; - solutionFiles?: string[]; - configRefs: string[]; - additionalFiles: readonly ts.projectSystem.File[]; - } - const main: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/main.ts`, - content: `import { foo } from 'helpers/functions'; + describe("when default project is solution project", () => { + interface Setup { + scenario: string; + solutionOptions?: ts.CompilerOptions; + solutionFiles?: string[]; + configRefs: string[]; + additionalFiles: readonly ts.projectSystem.File[]; + } + const main: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/main.ts`, + content: `import { foo } from 'helpers/functions'; export { foo };` - }; - const helper: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/src/helpers/functions.ts`, - content: `export const foo = 1;` - }; - const mainDts: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/target/src/main.d.ts`, - content: `import { foo } from 'helpers/functions'; + }; + const helper: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/src/helpers/functions.ts`, + content: `export const foo = 1;` + }; + const mainDts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/target/src/main.d.ts`, + content: `import { foo } from 'helpers/functions'; export { foo }; //# sourceMappingURL=main.d.ts.map` - }; - const mainDtsMap: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/target/src/main.d.ts.map`, - content: `{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAExC,OAAO,EAAC,GAAG,EAAC,CAAC"}` - }; - const helperDts: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/target/src/helpers/functions.d.ts`, - content: `export declare const foo = 1; + }; + const mainDtsMap: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/target/src/main.d.ts.map`, + content: `{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAExC,OAAO,EAAC,GAAG,EAAC,CAAC"}` + }; + const helperDts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/target/src/helpers/functions.d.ts`, + content: `export declare const foo = 1; //# sourceMappingURL=functions.d.ts.map` - }; - const helperDtsMap: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/target/src/helpers/functions.d.ts.map`, - content: `{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../../src/helpers/functions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,IAAI,CAAC"}` - }; - const tsconfigIndirect3: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/indirect3/tsconfig.json`, + }; + const helperDtsMap: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/target/src/helpers/functions.d.ts.map`, + content: `{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../../src/helpers/functions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,IAAI,CAAC"}` + }; + const tsconfigIndirect3: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/indirect3/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "../target/src/" + }, + }) + }; + const fileResolvingToMainDts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/indirect3/main.ts`, + content: `import { foo } from 'main'; +foo; +export function bar() {}` + }; + const tsconfigSrcPath = `${ts.tscWatch.projectRoot}/tsconfig-src.json`; + const tsconfigPath = `${ts.tscWatch.projectRoot}/tsconfig.json`; + const dummyFilePath = "/dummy/dummy.ts"; + function setup({ solutionFiles, solutionOptions, configRefs, additionalFiles }: Setup) { + const tsconfigSrc: ts.projectSystem.File = { + path: tsconfigSrcPath, content: JSON.stringify({ compilerOptions: { - baseUrl: "../target/src/" + composite: true, + outDir: "./target/", + baseUrl: "./src/" }, + include: ["./src/**/*"] }) }; - const fileResolvingToMainDts: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/indirect3/main.ts`, - content: `import { foo } from 'main'; -foo; -export function bar() {}` + const tsconfig: ts.projectSystem.File = { + path: tsconfigPath, + content: JSON.stringify({ + ... (solutionOptions ? { compilerOptions: solutionOptions } : {}), + references: configRefs.map(path => ({ path })), + files: solutionFiles || [] + }) }; - const tsconfigSrcPath = `${ts.tscWatch.projectRoot}/tsconfig-src.json`; - const tsconfigPath = `${ts.tscWatch.projectRoot}/tsconfig.json`; - const dummyFilePath = "/dummy/dummy.ts"; - function setup({ solutionFiles, solutionOptions, configRefs, additionalFiles }: Setup) { - const tsconfigSrc: ts.projectSystem.File = { - path: tsconfigSrcPath, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "./target/", - baseUrl: "./src/" - }, - include: ["./src/**/*"] - }) - }; - const tsconfig: ts.projectSystem.File = { - path: tsconfigPath, - content: JSON.stringify({ - ... (solutionOptions ? { compilerOptions: solutionOptions } : {}), - references: configRefs.map(path => ({ path })), - files: solutionFiles || [] - }) - }; - const dummyFile: ts.projectSystem.File = { - path: dummyFilePath, - content: "let a = 10;" - }; - const host = ts.projectSystem.createServerHost([ - tsconfigSrc, tsconfig, main, helper, - ts.projectSystem.libFile, - dummyFile, - mainDts, mainDtsMap, helperDts, helperDtsMap, - tsconfigIndirect3, fileResolvingToMainDts, - ...additionalFiles - ]); - const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - const service = session.getProjectService(); - service.openClientFile(main.path); - return { session, service, host }; - } + const dummyFile: ts.projectSystem.File = { + path: dummyFilePath, + content: "let a = 10;" + }; + const host = ts.projectSystem.createServerHost([ + tsconfigSrc, tsconfig, main, helper, + ts.projectSystem.libFile, + dummyFile, + mainDts, mainDtsMap, helperDts, helperDtsMap, + tsconfigIndirect3, fileResolvingToMainDts, + ...additionalFiles + ]); + const session = ts.projectSystem.createSession(host, { canUseEvents: true, logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + const service = session.getProjectService(); + service.openClientFile(main.path); + return { session, service, host }; + } - function verifySolutionScenario(input: Setup) { - const { session, service, host } = setup(input); + function verifySolutionScenario(input: Setup) { + const { session, service, host } = setup(input); - const info = service.getScriptInfoForPath(main.path as ts.Path)!; - session.logger.logs.push(""); - session.logger.logs.push(`getDefaultProject for ${main.path}: ${info.getDefaultProject().projectName}`); - session.logger.logs.push(`findDefaultConfiguredProject for ${main.path}: ${service.findDefaultConfiguredProject(info)!.projectName}`); - session.logger.logs.push(""); + const info = service.getScriptInfoForPath(main.path as ts.Path)!; + session.logger.logs.push(""); + session.logger.logs.push(`getDefaultProject for ${main.path}: ${info.getDefaultProject().projectName}`); + session.logger.logs.push(`findDefaultConfiguredProject for ${main.path}: ${service.findDefaultConfiguredProject(info)!.projectName}`); + session.logger.logs.push(""); - // Verify errors - ts.projectSystem.verifyGetErrRequest({ session, host, files: [main] }); + // Verify errors + ts.projectSystem.verifyGetErrRequest({ session, host, files: [main] }); - // Verify collection of script infos - service.openClientFile(dummyFilePath); + // Verify collection of script infos + service.openClientFile(dummyFilePath); - service.closeClientFile(main.path); - service.closeClientFile(dummyFilePath); - service.openClientFile(dummyFilePath); + service.closeClientFile(main.path); + service.closeClientFile(dummyFilePath); + service.openClientFile(dummyFilePath); - service.openClientFile(main.path); - service.closeClientFile(dummyFilePath); - service.openClientFile(dummyFilePath); + service.openClientFile(main.path); + service.closeClientFile(dummyFilePath); + service.openClientFile(dummyFilePath); - // Verify Reload projects - service.reloadProjects(); + // Verify Reload projects + service.reloadProjects(); - // Find all refs - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(main, "foo", { index: 1 }) - }).response as ts.projectSystem.protocol.ReferencesResponseBody; + // Find all refs + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(main, "foo", { index: 1 }) + }).response as ts.projectSystem.protocol.ReferencesResponseBody; - service.closeClientFile(main.path); - service.closeClientFile(dummyFilePath); + service.closeClientFile(main.path); + service.closeClientFile(dummyFilePath); - // Verify when declaration map references the file - service.openClientFile(fileResolvingToMainDts.path); + // Verify when declaration map references the file + service.openClientFile(fileResolvingToMainDts.path); - // Find all refs from dts include - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(fileResolvingToMainDts, "foo") - }).response as ts.projectSystem.protocol.ReferencesResponseBody; - ts.projectSystem.baselineTsserverLogs("projectReferences", input.scenario, session); - } + // Find all refs from dts include + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(fileResolvingToMainDts, "foo") + }).response as ts.projectSystem.protocol.ReferencesResponseBody; + ts.projectSystem.baselineTsserverLogs("projectReferences", input.scenario, session); + } + + function getIndirectProject(postfix: string, optionsToExtend?: ts.CompilerOptions) { + const tsconfigIndirect: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig-indirect${postfix}.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./target/", + baseUrl: "./src/", + ...optionsToExtend + }, + files: [`./indirect${postfix}/main.ts`], + references: [{ path: "./tsconfig-src.json" }] + }) + }; + const indirect: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/indirect${postfix}/main.ts`, + content: fileResolvingToMainDts.content + }; + return { tsconfigIndirect, indirect }; + } - function getIndirectProject(postfix: string, optionsToExtend?: ts.CompilerOptions) { - const tsconfigIndirect: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig-indirect${postfix}.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "./target/", - baseUrl: "./src/", - ...optionsToExtend - }, - files: [`./indirect${postfix}/main.ts`], - references: [{ path: "./tsconfig-src.json" }] - }) - }; - const indirect: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/indirect${postfix}/main.ts`, - content: fileResolvingToMainDts.content - }; - return { tsconfigIndirect, indirect }; - } + function verifyDisableReferencedProjectLoad(input: Setup) { + const { session, service } = setup(input); - function verifyDisableReferencedProjectLoad(input: Setup) { - const { session, service } = setup(input); + const info = service.getScriptInfoForPath(main.path as ts.Path)!; + session.logger.logs.push(""); + session.logger.logs.push(`getDefaultProject for ${main.path}: ${info.getDefaultProject().projectName}`); + session.logger.logs.push(`findDefaultConfiguredProject for ${main.path}: ${service.findDefaultConfiguredProject(info)?.projectName}`); + session.logger.logs.push(""); - const info = service.getScriptInfoForPath(main.path as ts.Path)!; - session.logger.logs.push(""); - session.logger.logs.push(`getDefaultProject for ${main.path}: ${info.getDefaultProject().projectName}`); - session.logger.logs.push(`findDefaultConfiguredProject for ${main.path}: ${service.findDefaultConfiguredProject(info)?.projectName}`); - session.logger.logs.push(""); + // Verify collection of script infos + service.openClientFile(dummyFilePath); - // Verify collection of script infos - service.openClientFile(dummyFilePath); + service.closeClientFile(main.path); + service.closeClientFile(dummyFilePath); + service.openClientFile(dummyFilePath); - service.closeClientFile(main.path); - service.closeClientFile(dummyFilePath); - service.openClientFile(dummyFilePath); + service.openClientFile(main.path); - service.openClientFile(main.path); + // Verify Reload projects + service.reloadProjects(); + ts.projectSystem.baselineTsserverLogs("projectReferences", input.scenario, session); + } - // Verify Reload projects - service.reloadProjects(); - ts.projectSystem.baselineTsserverLogs("projectReferences", input.scenario, session); - } + it("when project is directly referenced by solution", () => { + verifySolutionScenario({ + scenario: "project is directly referenced by solution", + configRefs: ["./tsconfig-src.json"], + additionalFiles: ts.emptyArray, + }); + }); - it("when project is directly referenced by solution", () => { + it("when project is indirectly referenced by solution", () => { + const { tsconfigIndirect, indirect } = getIndirectProject("1"); + const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); + verifySolutionScenario({ + scenario: "project is indirectly referenced by solution", + configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set", () => { + verifyDisableReferencedProjectLoad({ + scenario: "disables looking into the child project if disableReferencedProjectLoad is set", + solutionOptions: { disableReferencedProjectLoad: true }, + configRefs: ["./tsconfig-src.json"], + additionalFiles: ts.emptyArray, + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set in indirect project", () => { + const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); + verifyDisableReferencedProjectLoad({ + scenario: "disables looking into the child project if disableReferencedProjectLoad is set in indirect project", + configRefs: ["./tsconfig-indirect1.json"], + additionalFiles: [tsconfigIndirect, indirect], + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", () => { + const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); + const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); + verifyDisableReferencedProjectLoad({ + scenario: "disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", + configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], + }); + }); + + describe("when solution is project that contains its own files", () => { + it("when the project found is not solution but references open file through project reference", () => { + const ownMain: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/own/main.ts`, + content: fileResolvingToMainDts.content + }; verifySolutionScenario({ - scenario: "project is directly referenced by solution", + scenario: "solution with its own files and project found is not solution but references open file through project reference", + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./src/" + }, configRefs: ["./tsconfig-src.json"], - additionalFiles: ts.emptyArray, + additionalFiles: [ownMain], }); }); it("when project is indirectly referenced by solution", () => { + const ownMain: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; const { tsconfigIndirect, indirect } = getIndirectProject("1"); const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); verifySolutionScenario({ - scenario: "project is indirectly referenced by solution", + scenario: "solution with its own files and project is indirectly referenced by solution", + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/" + }, configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], - additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], }); }); it("disables looking into the child project if disableReferencedProjectLoad is set", () => { + const ownMain: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/own/main.ts`, + content: fileResolvingToMainDts.content + }; verifyDisableReferencedProjectLoad({ - scenario: "disables looking into the child project if disableReferencedProjectLoad is set", - solutionOptions: { disableReferencedProjectLoad: true }, + scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set", + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./src/", + disableReferencedProjectLoad: true + }, configRefs: ["./tsconfig-src.json"], - additionalFiles: ts.emptyArray, + additionalFiles: [ownMain], }); }); it("disables looking into the child project if disableReferencedProjectLoad is set in indirect project", () => { + const ownMain: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); verifyDisableReferencedProjectLoad({ - scenario: "disables looking into the child project if disableReferencedProjectLoad is set in indirect project", + scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set in indirect project", + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/", + }, configRefs: ["./tsconfig-indirect1.json"], - additionalFiles: [tsconfigIndirect, indirect], + additionalFiles: [tsconfigIndirect, indirect, ownMain], }); }); it("disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", () => { + const ownMain: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); verifyDisableReferencedProjectLoad({ - scenario: "disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", + scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/", + }, configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], - additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], - }); - }); - - describe("when solution is project that contains its own files", () => { - it("when the project found is not solution but references open file through project reference", () => { - const ownMain: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/own/main.ts`, - content: fileResolvingToMainDts.content - }; - verifySolutionScenario({ - scenario: "solution with its own files and project found is not solution but references open file through project reference", - solutionFiles: [`./own/main.ts`], - solutionOptions: { - outDir: "./target/", - baseUrl: "./src/" - }, - configRefs: ["./tsconfig-src.json"], - additionalFiles: [ownMain], - }); - }); - - it("when project is indirectly referenced by solution", () => { - const ownMain: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/own/main.ts`, - content: `import { bar } from 'main'; -bar;` - }; - const { tsconfigIndirect, indirect } = getIndirectProject("1"); - const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); - verifySolutionScenario({ - scenario: "solution with its own files and project is indirectly referenced by solution", - solutionFiles: [`./own/main.ts`], - solutionOptions: { - outDir: "./target/", - baseUrl: "./indirect1/" - }, - configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], - additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], - }); - }); - - it("disables looking into the child project if disableReferencedProjectLoad is set", () => { - const ownMain: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/own/main.ts`, - content: fileResolvingToMainDts.content - }; - verifyDisableReferencedProjectLoad({ - scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set", - solutionFiles: [`./own/main.ts`], - solutionOptions: { - outDir: "./target/", - baseUrl: "./src/", - disableReferencedProjectLoad: true - }, - configRefs: ["./tsconfig-src.json"], - additionalFiles: [ownMain], - }); - }); - - it("disables looking into the child project if disableReferencedProjectLoad is set in indirect project", () => { - const ownMain: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/own/main.ts`, - content: `import { bar } from 'main'; -bar;` - }; - const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); - verifyDisableReferencedProjectLoad({ - scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set in indirect project", - solutionFiles: [`./own/main.ts`], - solutionOptions: { - outDir: "./target/", - baseUrl: "./indirect1/", - }, - configRefs: ["./tsconfig-indirect1.json"], - additionalFiles: [tsconfigIndirect, indirect, ownMain], - }); - }); - - it("disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", () => { - const ownMain: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/own/main.ts`, - content: `import { bar } from 'main'; -bar;` - }; - const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); - const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); - verifyDisableReferencedProjectLoad({ - scenario: "solution with its own files and disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", - solutionFiles: [`./own/main.ts`], - solutionOptions: { - outDir: "./target/", - baseUrl: "./indirect1/", - }, - configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], - additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], - }); + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], }); }); }); + }); - describe("when new file is added to the referenced project", () => { - function setup(extendOptionsProject2?: ts.CompilerOptions) { - const config1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true - }, - exclude: ["temp"] - }) - }; - const class1: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, - content: `class class1 {}` - }; - const class1Dts: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, - content: `declare class class1 {}` - }; - const config2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "none", - composite: true, - ...(extendOptionsProject2 || {}) - }, - references: [ - { path: "../project1" } - ] - }) - }; - const class2: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, - content: `class class2 {}` - }; - const host = ts.projectSystem.createServerHost([config1, class1, class1Dts, config2, class2, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([class2], session); - return { host, session, class1 }; - } - - it("when referenced project is not open", () => { - const { host, session } = setup(); - - // Add new class to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - host.writeFile(class3, `class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - - // Add excluded file to referenced project - host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - host.checkTimeoutQueueLengthAndRun(0); - - // Add output from new class to referenced project - const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(0); - ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is not open`, session); - }); + describe("when new file is added to the referenced project", () => { + function setup(extendOptionsProject2?: ts.CompilerOptions) { + const config1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true + }, + exclude: ["temp"] + }) + }; + const class1: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.ts`, + content: `class class1 {}` + }; + const class1Dts: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/projects/project1/class1.d.ts`, + content: `declare class class1 {}` + }; + const config2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + composite: true, + ...(extendOptionsProject2 || {}) + }, + references: [ + { path: "../project1" } + ] + }) + }; + const class2: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/projects/project2/class2.ts`, + content: `class class2 {}` + }; + const host = ts.projectSystem.createServerHost([config1, class1, class1Dts, config2, class2, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([class2], session); + return { host, session, class1 }; + } + + it("when referenced project is not open", () => { + const { host, session } = setup(); + + // Add new class to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + host.writeFile(class3, `class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + + // Add excluded file to referenced project + host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + host.checkTimeoutQueueLengthAndRun(0); + + // Add output from new class to referenced project + const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(0); + ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is not open`, session); + }); - it("when referenced project is open", () => { - const { host, session, class1 } = setup(); - ts.projectSystem.openFilesForSession([class1], session); - - // Add new class to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - host.writeFile(class3, `class class3 {}`); - host.checkTimeoutQueueLengthAndRun(3); - // Add excluded file to referenced project - host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - host.checkTimeoutQueueLengthAndRun(0); - // Add output from new class to referenced project - const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(0); - ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is open`, session); - }); + it("when referenced project is open", () => { + const { host, session, class1 } = setup(); + ts.projectSystem.openFilesForSession([class1], session); + + // Add new class to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + host.writeFile(class3, `class class3 {}`); + host.checkTimeoutQueueLengthAndRun(3); + // Add excluded file to referenced project + host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + host.checkTimeoutQueueLengthAndRun(0); + // Add output from new class to referenced project + const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(0); + ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is open`, session); + }); - it("when referenced project is not open with disableSourceOfProjectReferenceRedirect", () => { - const { host, session } = setup({ disableSourceOfProjectReferenceRedirect: true }); - - // Add new class to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - host.writeFile(class3, `class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - // Add output of new class to referenced project - const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - // Add excluded file to referenced project - host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - host.checkTimeoutQueueLengthAndRun(0); - // Delete output from new class to referenced project - host.deleteFile(class3Dts); - host.checkTimeoutQueueLengthAndRun(2); - // Write back output of new class to referenced project - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is not open with disableSourceOfProjectReferenceRedirect`, session); - }); + it("when referenced project is not open with disableSourceOfProjectReferenceRedirect", () => { + const { host, session } = setup({ disableSourceOfProjectReferenceRedirect: true }); + + // Add new class to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + host.writeFile(class3, `class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + // Add output of new class to referenced project + const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + // Add excluded file to referenced project + host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + host.checkTimeoutQueueLengthAndRun(0); + // Delete output from new class to referenced project + host.deleteFile(class3Dts); + host.checkTimeoutQueueLengthAndRun(2); + // Write back output of new class to referenced project + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is not open with disableSourceOfProjectReferenceRedirect`, session); + }); - it("when referenced project is open with disableSourceOfProjectReferenceRedirect", () => { - const { host, session, class1 } = setup({ disableSourceOfProjectReferenceRedirect: true }); - ts.projectSystem.openFilesForSession([class1], session); - - // Add new class to referenced project - const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; - host.writeFile(class3, `class class3 {}`); - host.checkTimeoutQueueLengthAndRun(3); - // Add output of new class to referenced project - const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - // Add excluded file to referenced project - host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); - host.checkTimeoutQueueLengthAndRun(0); - // Delete output from new class to referenced project - host.deleteFile(class3Dts); - host.checkTimeoutQueueLengthAndRun(2); - // Write back output of new class to referenced project - host.writeFile(class3Dts, `declare class class3 {}`); - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is open with disableSourceOfProjectReferenceRedirect`, session); - }); + it("when referenced project is open with disableSourceOfProjectReferenceRedirect", () => { + const { host, session, class1 } = setup({ disableSourceOfProjectReferenceRedirect: true }); + ts.projectSystem.openFilesForSession([class1], session); + + // Add new class to referenced project + const class3 = `${ts.tscWatch.projectRoot}/projects/project1/class3.ts`; + host.writeFile(class3, `class class3 {}`); + host.checkTimeoutQueueLengthAndRun(3); + // Add output of new class to referenced project + const class3Dts = `${ts.tscWatch.projectRoot}/projects/project1/class3.d.ts`; + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + // Add excluded file to referenced project + host.ensureFileOrFolder({ path: `${ts.tscWatch.projectRoot}/projects/project1/temp/file.d.ts`, content: `declare class file {}` }); + host.checkTimeoutQueueLengthAndRun(0); + // Delete output from new class to referenced project + host.deleteFile(class3Dts); + host.checkTimeoutQueueLengthAndRun(2); + // Write back output of new class to referenced project + host.writeFile(class3Dts, `declare class class3 {}`); + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.baselineTsserverLogs("projectReferences", `new file is added to the referenced project when referenced project is open with disableSourceOfProjectReferenceRedirect`, session); }); + }); - describe("auto import with referenced project", () => { - function verifyAutoImport(built: boolean, disableSourceOfProjectReferenceRedirect?: boolean) { - const solnConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - files: [], - references: [ - { path: "shared/src/library" }, - { path: "app/src/program" } - ] - }) - }; - const sharedConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/shared/src/library/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "../../bld/library" - } - }) - }; - const sharedIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/shared/src/library/index.ts`, - content: `export function foo() {}` - }; - const sharedPackage: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/shared/package.json`, - content: JSON.stringify({ - name: "shared", - version: "1.0.0", - main: "bld/library/index.js", - types: "bld/library/index.d.ts" - }) - }; - const appConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app/src/program/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "../../bld/program", - disableSourceOfProjectReferenceRedirect - }, - references: [ - { path: "../../../shared/src/library" } - ] - }) - }; - const appBar: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app/src/program/bar.ts`, - content: `import {foo} from "shared";` - }; - const appIndex: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/app/src/program/index.ts`, - content: `foo` - }; - const sharedSymlink: ts.projectSystem.SymLink = { - path: `${ts.tscWatch.projectRoot}/node_modules/shared`, - symLink: `${ts.tscWatch.projectRoot}/shared` - }; - const files = [solnConfig, sharedConfig, sharedIndex, sharedPackage, appConfig, appBar, appIndex, sharedSymlink, ts.projectSystem.libFile]; - const host = ts.projectSystem.createServerHost(files); - if (built) { - ts.tscWatch.solutionBuildWithBaseline(host, [solnConfig.path]); - host.clearOutput(); - } - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([appIndex], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.GetCodeFixes, - arguments: { - file: appIndex.path, - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 4, - errorCodes: [ts.Diagnostics.Cannot_find_name_0.code], + describe("auto import with referenced project", () => { + function verifyAutoImport(built: boolean, disableSourceOfProjectReferenceRedirect?: boolean) { + const solnConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + files: [], + references: [ + { path: "shared/src/library" }, + { path: "app/src/program" } + ] + }) + }; + const sharedConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/shared/src/library/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../../bld/library" } - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `auto import with referenced project${built ? " when built" : ""}${disableSourceOfProjectReferenceRedirect ? " with disableSourceOfProjectReferenceRedirect" : ""}`, session); + }) + }; + const sharedIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/shared/src/library/index.ts`, + content: `export function foo() {}` + }; + const sharedPackage: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/shared/package.json`, + content: JSON.stringify({ + name: "shared", + version: "1.0.0", + main: "bld/library/index.js", + types: "bld/library/index.d.ts" + }) + }; + const appConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app/src/program/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../../bld/program", + disableSourceOfProjectReferenceRedirect + }, + references: [ + { path: "../../../shared/src/library" } + ] + }) + }; + const appBar: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app/src/program/bar.ts`, + content: `import {foo} from "shared";` + }; + const appIndex: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/app/src/program/index.ts`, + content: `foo` + }; + const sharedSymlink: ts.projectSystem.SymLink = { + path: `${ts.tscWatch.projectRoot}/node_modules/shared`, + symLink: `${ts.tscWatch.projectRoot}/shared` + }; + const files = [solnConfig, sharedConfig, sharedIndex, sharedPackage, appConfig, appBar, appIndex, sharedSymlink, ts.projectSystem.libFile]; + const host = ts.projectSystem.createServerHost(files); + if (built) { + ts.tscWatch.solutionBuildWithBaseline(host, [solnConfig.path]); + host.clearOutput(); } - - it("when project is built", () => { - verifyAutoImport(/*built*/ true); - }); - it("when project is not built", () => { - verifyAutoImport(/*built*/ false); - }); - it("when disableSourceOfProjectReferenceRedirect is true", () => { - verifyAutoImport(/*built*/ true, /*disableSourceOfProjectReferenceRedirect*/ true); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([appIndex], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.GetCodeFixes, + arguments: { + file: appIndex.path, + startLine: 1, + startOffset: 1, + endLine: 1, + endOffset: 4, + errorCodes: [ts.Diagnostics.Cannot_find_name_0.code], + } }); - }); - - it("when files from two projects are open and one project references", () => { - function getPackageAndFile(packageName: string, references?: string[], optionsToExtend?: ts.CompilerOptions): [ - file: ts.projectSystem.File, - config: ts.projectSystem.File - ] { - const file: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/${packageName}/src/file1.ts`, - content: `export const ${packageName}Const = 10;` - }; - const config: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/${packageName}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, ...optionsToExtend || {} }, - references: references?.map(path => ({ path: `../${path}` })) - }) - }; - return [file, config]; - } - const [mainFile, mainConfig] = getPackageAndFile("main", ["core", "indirect", "noCoreRef1", "indirectDisabledChildLoad1", "indirectDisabledChildLoad2", "refToCoreRef3", "indirectNoCoreRef"]); - const [coreFile, coreConfig] = getPackageAndFile("core"); - const [noCoreRef1File, noCoreRef1Config] = getPackageAndFile("noCoreRef1"); - const [indirectFile, indirectConfig] = getPackageAndFile("indirect", ["coreRef1"]); - const [coreRef1File, coreRef1Config] = getPackageAndFile("coreRef1", ["core"]); - const [indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config] = getPackageAndFile("indirectDisabledChildLoad1", ["coreRef2"], { disableReferencedProjectLoad: true }); - const [coreRef2File, coreRef2Config] = getPackageAndFile("coreRef2", ["core"]); - const [indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config] = getPackageAndFile("indirectDisabledChildLoad2", ["coreRef3"], { disableReferencedProjectLoad: true }); - const [coreRef3File, coreRef3Config] = getPackageAndFile("coreRef3", ["core"]); - const [refToCoreRef3File, refToCoreRef3Config] = getPackageAndFile("refToCoreRef3", ["coreRef3"]); - const [indirectNoCoreRefFile, indirectNoCoreRefConfig] = getPackageAndFile("indirectNoCoreRef", ["noCoreRef2"]); - const [noCoreRef2File, noCoreRef2Config] = getPackageAndFile("noCoreRef2"); + ts.projectSystem.baselineTsserverLogs("projectReferences", `auto import with referenced project${built ? " when built" : ""}${disableSourceOfProjectReferenceRedirect ? " with disableSourceOfProjectReferenceRedirect" : ""}`, session); + } - const host = ts.projectSystem.createServerHost([ - ts.projectSystem.libFile, - mainFile, mainConfig, coreFile, coreConfig, noCoreRef1File, noCoreRef1Config, - indirectFile, indirectConfig, coreRef1File, coreRef1Config, - indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config, coreRef2File, coreRef2Config, - indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config, coreRef3File, coreRef3Config, - refToCoreRef3File, refToCoreRef3Config, - indirectNoCoreRefFile, indirectNoCoreRefConfig, noCoreRef2File, noCoreRef2Config - ], { useCaseSensitiveFileNames: true }); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([mainFile, coreFile], session); + it("when project is built", () => { + verifyAutoImport(/*built*/ true); + }); + it("when project is not built", () => { + verifyAutoImport(/*built*/ false); + }); + it("when disableSourceOfProjectReferenceRedirect is true", () => { + verifyAutoImport(/*built*/ true, /*disableSourceOfProjectReferenceRedirect*/ true); + }); + }); - // Find all refs in coreFile - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(coreFile, `coreConst`) - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `when files from two projects are open and one project references`, session); + it("when files from two projects are open and one project references", () => { + function getPackageAndFile(packageName: string, references?: string[], optionsToExtend?: ts.CompilerOptions): [ + file: ts.projectSystem.File, + config: ts.projectSystem.File + ] { + const file: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/${packageName}/src/file1.ts`, + content: `export const ${packageName}Const = 10;` + }; + const config: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, ...optionsToExtend || {} }, + references: references?.map(path => ({ path: `../${path}` })) + }) + }; + return [file, config]; + } + const [mainFile, mainConfig] = getPackageAndFile("main", ["core", "indirect", "noCoreRef1", "indirectDisabledChildLoad1", "indirectDisabledChildLoad2", "refToCoreRef3", "indirectNoCoreRef"]); + const [coreFile, coreConfig] = getPackageAndFile("core"); + const [noCoreRef1File, noCoreRef1Config] = getPackageAndFile("noCoreRef1"); + const [indirectFile, indirectConfig] = getPackageAndFile("indirect", ["coreRef1"]); + const [coreRef1File, coreRef1Config] = getPackageAndFile("coreRef1", ["core"]); + const [indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config] = getPackageAndFile("indirectDisabledChildLoad1", ["coreRef2"], { disableReferencedProjectLoad: true }); + const [coreRef2File, coreRef2Config] = getPackageAndFile("coreRef2", ["core"]); + const [indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config] = getPackageAndFile("indirectDisabledChildLoad2", ["coreRef3"], { disableReferencedProjectLoad: true }); + const [coreRef3File, coreRef3Config] = getPackageAndFile("coreRef3", ["core"]); + const [refToCoreRef3File, refToCoreRef3Config] = getPackageAndFile("refToCoreRef3", ["coreRef3"]); + const [indirectNoCoreRefFile, indirectNoCoreRefConfig] = getPackageAndFile("indirectNoCoreRef", ["noCoreRef2"]); + const [noCoreRef2File, noCoreRef2Config] = getPackageAndFile("noCoreRef2"); + + const host = ts.projectSystem.createServerHost([ + ts.projectSystem.libFile, + mainFile, mainConfig, coreFile, coreConfig, noCoreRef1File, noCoreRef1Config, + indirectFile, indirectConfig, coreRef1File, coreRef1Config, + indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config, coreRef2File, coreRef2Config, + indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config, coreRef3File, coreRef3Config, + refToCoreRef3File, refToCoreRef3Config, + indirectNoCoreRefFile, indirectNoCoreRefConfig, noCoreRef2File, noCoreRef2Config + ], { useCaseSensitiveFileNames: true }); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([mainFile, coreFile], session); + + // Find all refs in coreFile + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(coreFile, `coreConst`) }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `when files from two projects are open and one project references`, session); + }); - describe("find refs to decl in other proj", () => { - const indexA: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a/index.ts`, - content: `import { B } from "../b/lib"; + describe("find refs to decl in other proj", () => { + const indexA: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a/index.ts`, + content: `import { B } from "../b/lib"; const b: B = new B();` - }; + }; - const configB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/tsconfig.json`, - content: `{ + const configB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/tsconfig.json`, + content: `{ "compilerOptions": { "declarationMap": true, "outDir": "lib", "composite": true } }` - }; + }; - const indexB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/index.ts`, - content: `export class B { + const indexB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/index.ts`, + content: `export class B { M() {} }` - }; + }; - const helperB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/helper.ts`, - content: `import { B } from "."; + const helperB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/helper.ts`, + content: `import { B } from "."; const b: B = new B();` - }; + }; - const dtsB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/lib/index.d.ts`, - content: `export declare class B { + const dtsB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/lib/index.d.ts`, + content: `export declare class B { M(): void; } //# sourceMappingURL=index.d.ts.map` + }; + + const dtsMapB: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/b/lib/index.d.ts.map`, + content: `{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;IACV,CAAC;CACJ"}` + }; + + function baselineDisableReferencedProjectLoad(projectAlreadyLoaded: boolean, disableReferencedProjectLoad: boolean, disableSourceOfProjectReferenceRedirect: boolean, dtsMapPresent: boolean) { + + // Mangled to stay under windows path length limit + const subScenario = `when proj ${projectAlreadyLoaded ? "is" : "is not"} loaded` + + ` and refd proj loading is ${disableReferencedProjectLoad ? "disabled" : "enabled"}` + + ` and proj ref redirects are ${disableSourceOfProjectReferenceRedirect ? "disabled" : "enabled"}` + + ` and a decl map is ${dtsMapPresent ? "present" : "missing"}`; + const compilerOptions: ts.CompilerOptions = { + disableReferencedProjectLoad, + disableSourceOfProjectReferenceRedirect, + composite: true }; - const dtsMapB: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/b/lib/index.d.ts.map`, - content: `{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;IACV,CAAC;CACJ"}` - }; - - function baselineDisableReferencedProjectLoad(projectAlreadyLoaded: boolean, disableReferencedProjectLoad: boolean, disableSourceOfProjectReferenceRedirect: boolean, dtsMapPresent: boolean) { - - // Mangled to stay under windows path length limit - const subScenario = `when proj ${projectAlreadyLoaded ? "is" : "is not"} loaded` + - ` and refd proj loading is ${disableReferencedProjectLoad ? "disabled" : "enabled"}` + - ` and proj ref redirects are ${disableSourceOfProjectReferenceRedirect ? "disabled" : "enabled"}` + - ` and a decl map is ${dtsMapPresent ? "present" : "missing"}`; - const compilerOptions: ts.CompilerOptions = { - disableReferencedProjectLoad, - disableSourceOfProjectReferenceRedirect, - composite: true - }; - - it(subScenario, () => { - const configA: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/a/tsconfig.json`, - content: `{ + it(subScenario, () => { + const configA: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/a/tsconfig.json`, + content: `{ "compilerOptions": ${JSON.stringify(compilerOptions)}, "references": [{ "path": "../b" }] }` - }; + }; - const host = ts.projectSystem.createServerHost([configA, indexA, configB, indexB, helperB, dtsB, ...(dtsMapPresent ? [dtsMapB] : [])]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([indexA, ...(projectAlreadyLoaded ? [helperB] : [])], session); - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.References, - arguments: ts.projectSystem.protocolFileLocationFromSubstring(indexA, `B`, { index: 1 }) - }); - ts.projectSystem.baselineTsserverLogs("projectReferences", `find refs to decl in other proj ${subScenario}`, session); + const host = ts.projectSystem.createServerHost([configA, indexA, configB, indexB, helperB, dtsB, ...(dtsMapPresent ? [dtsMapB] : [])]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([indexA, ...(projectAlreadyLoaded ? [helperB] : [])], session); + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.References, + arguments: ts.projectSystem.protocolFileLocationFromSubstring(indexA, `B`, { index: 1 }) }); - } - - /* eslint-disable boolean-trivia */ - - // Pre-loaded = A file from project B is already open when FAR is invoked - // dRPL = Project A has disableReferencedProjectLoad - // dSOPRR = Project A has disableSourceOfProjectReferenceRedirect - // Map = The declaration map file b/lib/index.d.ts.map exists - // B refs = files under directory b in which references are found (all scenarios find all references in a/index.ts) - - // Pre-loaded | dRPL | dSOPRR | Map | B state | Notes | B refs | Notes - // -----------+--------+--------+----------+------------+--------------+---------------------+--------------------------------------------------- - baselineDisableReferencedProjectLoad(true, true, true, true); // Pre-loaded | | index.ts, helper.ts | Via map and pre-loaded project - baselineDisableReferencedProjectLoad(true, true, true, false); // Pre-loaded | | lib/index.d.ts | Even though project is loaded - baselineDisableReferencedProjectLoad(true, true, false, true); // Pre-loaded | | index.ts, helper.ts | - baselineDisableReferencedProjectLoad(true, true, false, false); // Pre-loaded | | index.ts, helper.ts | - baselineDisableReferencedProjectLoad(true, false, true, true); // Pre-loaded | | index.ts, helper.ts | Via map and pre-loaded project - baselineDisableReferencedProjectLoad(true, false, true, false); // Pre-loaded | | lib/index.d.ts | Even though project is loaded - baselineDisableReferencedProjectLoad(true, false, false, true); // Pre-loaded | | index.ts, helper.ts | - baselineDisableReferencedProjectLoad(true, false, false, false); // Pre-loaded | | index.ts, helper.ts | - baselineDisableReferencedProjectLoad(false, true, true, true); // Not loaded | | lib/index.d.ts | Even though map is present - baselineDisableReferencedProjectLoad(false, true, true, false); // Not loaded | | lib/index.d.ts | - baselineDisableReferencedProjectLoad(false, true, false, true); // Not loaded | | index.ts | But not helper.ts, which is not referenced from a - baselineDisableReferencedProjectLoad(false, true, false, false); // Not loaded | | index.ts | But not helper.ts, which is not referenced from a - baselineDisableReferencedProjectLoad(false, false, true, true); // Loaded | Via map | index.ts, helper.ts | Via map and newly loaded project - baselineDisableReferencedProjectLoad(false, false, true, false); // Not loaded | | lib/index.d.ts | - baselineDisableReferencedProjectLoad(false, false, false, true); // Loaded | Via redirect | index.ts, helper.ts | - baselineDisableReferencedProjectLoad(false, false, false, false); // Loaded | Via redirect | index.ts, helper.ts | - - /* eslint-enable boolean-trivia */ - }); + ts.projectSystem.baselineTsserverLogs("projectReferences", `find refs to decl in other proj ${subScenario}`, session); + }); + } + + /* eslint-disable boolean-trivia */ + + // Pre-loaded = A file from project B is already open when FAR is invoked + // dRPL = Project A has disableReferencedProjectLoad + // dSOPRR = Project A has disableSourceOfProjectReferenceRedirect + // Map = The declaration map file b/lib/index.d.ts.map exists + // B refs = files under directory b in which references are found (all scenarios find all references in a/index.ts) + + // Pre-loaded | dRPL | dSOPRR | Map | B state | Notes | B refs | Notes + // -----------+--------+--------+----------+------------+--------------+---------------------+--------------------------------------------------- + baselineDisableReferencedProjectLoad(true, true, true, true); // Pre-loaded | | index.ts, helper.ts | Via map and pre-loaded project + baselineDisableReferencedProjectLoad(true, true, true, false); // Pre-loaded | | lib/index.d.ts | Even though project is loaded + baselineDisableReferencedProjectLoad(true, true, false, true); // Pre-loaded | | index.ts, helper.ts | + baselineDisableReferencedProjectLoad(true, true, false, false); // Pre-loaded | | index.ts, helper.ts | + baselineDisableReferencedProjectLoad(true, false, true, true); // Pre-loaded | | index.ts, helper.ts | Via map and pre-loaded project + baselineDisableReferencedProjectLoad(true, false, true, false); // Pre-loaded | | lib/index.d.ts | Even though project is loaded + baselineDisableReferencedProjectLoad(true, false, false, true); // Pre-loaded | | index.ts, helper.ts | + baselineDisableReferencedProjectLoad(true, false, false, false); // Pre-loaded | | index.ts, helper.ts | + baselineDisableReferencedProjectLoad(false, true, true, true); // Not loaded | | lib/index.d.ts | Even though map is present + baselineDisableReferencedProjectLoad(false, true, true, false); // Not loaded | | lib/index.d.ts | + baselineDisableReferencedProjectLoad(false, true, false, true); // Not loaded | | index.ts | But not helper.ts, which is not referenced from a + baselineDisableReferencedProjectLoad(false, true, false, false); // Not loaded | | index.ts | But not helper.ts, which is not referenced from a + baselineDisableReferencedProjectLoad(false, false, true, true); // Loaded | Via map | index.ts, helper.ts | Via map and newly loaded project + baselineDisableReferencedProjectLoad(false, false, true, false); // Not loaded | | lib/index.d.ts | + baselineDisableReferencedProjectLoad(false, false, false, true); // Loaded | Via redirect | index.ts, helper.ts | + baselineDisableReferencedProjectLoad(false, false, false, false); // Loaded | Via redirect | index.ts, helper.ts | + + /* eslint-enable boolean-trivia */ }); +}); } diff --git a/src/testRunner/unittests/tsserver/projectReferencesSourcemap.ts b/src/testRunner/unittests/tsserver/projectReferencesSourcemap.ts index 9cbf4915fc13b..43d4236a4404f 100644 --- a/src/testRunner/unittests/tsserver/projectReferencesSourcemap.ts +++ b/src/testRunner/unittests/tsserver/projectReferencesSourcemap.ts @@ -1,26 +1,26 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: with project references and tsbuild source map", () => { - const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; - const dependecyDeclsLocation = `${ts.tscWatch.projectRoot}/decls`; - const mainLocation = `${ts.tscWatch.projectRoot}/main`; - const dependencyTs: ts.projectSystem.File = { - path: `${dependecyLocation}/FnS.ts`, - content: `export function fn1() { } +describe("unittests:: tsserver:: with project references and tsbuild source map", () => { + const dependecyLocation = `${ts.tscWatch.projectRoot}/dependency`; + const dependecyDeclsLocation = `${ts.tscWatch.projectRoot}/decls`; + const mainLocation = `${ts.tscWatch.projectRoot}/main`; + const dependencyTs: ts.projectSystem.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: ts.projectSystem.File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } }) - }; - - const mainTs: ts.projectSystem.File = { - path: `${mainLocation}/main.ts`, - content: `import { + }; + const dependencyTsPath = dependencyTs.path.toLowerCase(); + const dependencyConfig: ts.projectSystem.File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } }) + }; + + const mainTs: ts.projectSystem.File = { + path: `${mainLocation}/main.ts`, + content: `import { fn1, fn2, fn3, @@ -34,2785 +34,2785 @@ fn3(); fn4(); fn5(); ` + }; + const mainConfig: ts.projectSystem.File = { + path: `${mainLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true }, + references: [{ path: "../dependency" }] + }) + }; + + const randomFile: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/random/random.ts`, + content: "let a = 10;" + }; + const randomConfig: ts.projectSystem.File = { + path: `${ts.tscWatch.projectRoot}/random/tsconfig.json`, + content: "{}" + }; + const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`; + const dtsPath = dtsLocation.toLowerCase() as ts.Path; + const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`; + const dtsMapPath = dtsMapLocation.toLowerCase() as ts.Path; + const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, ts.projectSystem.libFile, randomFile, randomConfig]; + function changeDtsFile(host: ts.projectSystem.TestServerHost) { + host.writeFile(dtsLocation, host.readFile(dtsLocation)!.replace("//# sourceMappingURL=FnS.d.ts.map", `export declare function fn6(): void; +//# sourceMappingURL=FnS.d.ts.map`)); + } + + function changeDtsMapFile(host: ts.projectSystem.TestServerHost) { + 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"}`); + } + + function verifyScriptInfos(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], additionalInfo?: string) { + ts.projectSystem.checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos), additionalInfo); + ts.projectSystem.checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()), additionalInfo); + } + + function verifyOnlyRandomInfos(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost) { + verifyScriptInfos(session, host, [randomFile.path], [ts.projectSystem.libFile.path], [randomConfig.path], "Random"); + } + + function declarationSpan(fn: number): ts.projectSystem.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: ts.projectSystem.File = { - path: `${mainLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true }, - references: [{ path: "../dependency" }] - }) + } + function importSpan(fn: number): ts.projectSystem.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 randomFile: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/random/random.ts`, - content: "let a = 10;" + } + function usageSpan(fn: number): ts.projectSystem.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: ts.projectSystem.protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; + return { + reqName: "goToDef", + request: { + command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To dependency + definitions: [definition], + textSpan + } }; - const randomConfig: ts.projectSystem.File = { - path: `${ts.tscWatch.projectRoot}/random/tsconfig.json`, - content: "{}" + } + + function goToDefFromMainTsWithNoMap(fn: number): Action { + const textSpan = usageSpan(fn); + const definition = declarationSpan(fn); + const declareSpaceLength = "declare ".length; + return { + reqName: "goToDef", + request: { + command: ts.projectSystem.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 + } }; - const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`; - const dtsPath = dtsLocation.toLowerCase() as ts.Path; - const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`; - const dtsMapPath = dtsMapLocation.toLowerCase() as ts.Path; - const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, ts.projectSystem.libFile, randomFile, randomConfig]; - function changeDtsFile(host: ts.projectSystem.TestServerHost) { - host.writeFile(dtsLocation, host.readFile(dtsLocation)!.replace("//# sourceMappingURL=FnS.d.ts.map", `export declare function fn6(): void; -//# sourceMappingURL=FnS.d.ts.map`)); - } - - function changeDtsMapFile(host: ts.projectSystem.TestServerHost) { - 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"}`); - } + } + + function goToDefFromMainTsWithNoDts(fn: number): Action { + const textSpan = usageSpan(fn); + return { + reqName: "goToDef", + request: { + command: ts.projectSystem.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: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // Definition on fn + 1 line + definitions: [{ file: dependencyTs.path, ...declarationSpan(fn + 1) }], + textSpan + } + }; + } + + function renameFromDependencyTs(fn: number): Action { + const defSpan = declarationSpan(fn); + const { contextStart: _, contextEnd: _1, ...triggerSpan } = defSpan; + return { + reqName: "rename", + request: { + command: ts.projectSystem.protocol.CommandTypes.Rename, + arguments: { file: dependencyTs.path, ...triggerSpan.start } + }, + expectedResponse: { + info: { + canRename: true, + fileToRename: undefined, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + kind: ts.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 ts.projectSystem.protocol.RenameInfoSuccess, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + }, + locs + } + }; + } + + 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 verifyScriptInfos(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], additionalInfo?: string) { - ts.projectSystem.checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos), additionalInfo); - ts.projectSystem.checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()), additionalInfo); + 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; + } + + function verifyAction(session: ts.projectSystem.TestSession, { reqName, request, expectedResponse }: Action) { + const { response } = session.executeCommandSeq(request); + assert.deepEqual(response, expectedResponse, `Failed Request: ${reqName}`); + } + + function verifyDocumentPositionMapper(session: ts.projectSystem.TestSession, dependencyMap: ts.server.ScriptInfo | undefined, documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], equal: boolean, debugInfo?: string) { + assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap, `${debugInfo} dependencyMap`); + if (dependencyMap) { + verifyEquality(dependencyMap.documentPositionMapper, documentPositionMapper, equal, `${debugInfo} DocumentPositionMapper`); } + } - function verifyOnlyRandomInfos(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost) { - verifyScriptInfos(session, host, [randomFile.path], [ts.projectSystem.libFile.path], [randomConfig.path], "Random"); - } + function verifyDocumentPositionMapperEqual(session: ts.projectSystem.TestSession, dependencyMap: ts.server.ScriptInfo | undefined, documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], debugInfo?: string) { + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, /*equal*/ true, debugInfo); + } - function declarationSpan(fn: number): ts.projectSystem.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): ts.projectSystem.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 verifyEquality(actual: T, expected: T, equal: boolean, debugInfo?: string) { + if (equal) { + assert.strictEqual(actual, expected, debugInfo); } - function usageSpan(fn: number): ts.projectSystem.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: ts.projectSystem.protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; - return { - reqName: "goToDef", - request: { - command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To dependency - definitions: [definition], - textSpan - } - }; + else { + assert.notStrictEqual(actual, expected, debugInfo); } - - function goToDefFromMainTsWithNoMap(fn: number): Action { - const textSpan = usageSpan(fn); - const definition = declarationSpan(fn); - const declareSpaceLength = "declare ".length; - return { - reqName: "goToDef", - request: { - command: ts.projectSystem.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 verifyAllFnAction(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, action: (fn: number) => Action, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[], existingDependencyMap: ts.server.ScriptInfo | undefined, existingDocumentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], existingMapEqual: boolean, existingDocumentPositionMapperEqual: boolean, skipMapPathInDtsInfo?: boolean) { + let sourceMapPath: ts.server.ScriptInfo["sourceMapFilePath"] | undefined; + let dependencyMap: ts.server.ScriptInfo | undefined; + let documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"]; + for (let fn = 1; fn <= 5; fn++) { + const fnAction = action(fn); + verifyAction(session, fnAction); + const debugInfo = `${fnAction.reqName}:: ${fn}`; + ts.projectSystem.checkScriptInfos(session.getProjectService(), expectedInfos, debugInfo); + ts.projectSystem.checkWatchedFiles(host, expectedWatchedFiles, debugInfo); + const dtsInfo = session.getProjectService().getScriptInfoForPath(dtsPath); + const dtsMapInfo = session.getProjectService().getScriptInfoForPath(dtsMapPath); + + if (fn === 1) { + if (dtsInfo) { + if (!skipMapPathInDtsInfo) { + if (dtsMapInfo) { + assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath, `${debugInfo} sourceMapFilePath`); + } + else { + assert.isNotString(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); + assert.isNotFalse(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); + assert.isDefined(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); + } + } } - }; + verifyEquality(dtsMapInfo, existingDependencyMap, existingMapEqual, `${debugInfo} dependencyMap`); + verifyEquality(existingDocumentPositionMapper, dtsMapInfo?.documentPositionMapper, existingDocumentPositionMapperEqual, `${debugInfo} DocumentPositionMapper`); + sourceMapPath = dtsInfo?.sourceMapFilePath; + dependencyMap = dtsMapInfo; + documentPositionMapper = dependencyMap?.documentPositionMapper; + } + else { + assert.equal(dtsInfo?.sourceMapFilePath, sourceMapPath, `${debugInfo} sourceMapFilePath`); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper, debugInfo); + } } - - function goToDefFromMainTsWithNoDts(fn: number): Action { - const textSpan = usageSpan(fn); - return { - reqName: "goToDef", - request: { - command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To import declaration - definitions: [{ file: mainTs.path, ...importSpan(fn) }], - textSpan - } - }; + } + + function verifyScriptInfoCollectionWith(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, openFiles: readonly ts.projectSystem.File[], expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { + const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); + + // Collecting at this point retains dependency.d.ts and map + ts.projectSystem.closeFilesForSession([randomFile], session); + ts.projectSystem.openFilesForSession([randomFile], session); + ts.projectSystem.checkScriptInfos(session.getProjectService(), expectedInfos); + ts.projectSystem.checkWatchedFiles(host, expectedWatchedFiles); + // If map is not collected, document position mapper shouldnt change + if (session.getProjectService().filenameToScriptInfo.has(dtsMapPath)) { + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); } - function goToDefFromMainTsWithDependencyChange(fn: number): Action { - const textSpan = usageSpan(fn); - return { - reqName: "goToDef", - request: { - command: ts.projectSystem.protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // Definition on fn + 1 line - definitions: [{ file: dependencyTs.path, ...declarationSpan(fn + 1) }], - textSpan - } - }; - } + // Closing open file, removes dependencies too + ts.projectSystem.closeFilesForSession([...openFiles, randomFile], session); + ts.projectSystem.openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + type OnHostCreate = (host: ts.projectSystem.TestServerHost) => void; + type CreateSessionFn = (onHostCreate?: OnHostCreate) => { + host: ts.projectSystem.TestServerHost; + session: ts.projectSystem.TestSession; + }; + function setupWith(createSession: CreateSessionFn, openFiles: readonly ts.projectSystem.File[], onHostCreate: OnHostCreate | undefined) { + const result = createSession(onHostCreate); + ts.projectSystem.openFilesForSession(openFiles, result.session); + return result; + } + + function setupWithMainTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { + return setupWith(createSession, [mainTs, randomFile], onHostCreate); + } + + function setupWithDependencyTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { + return setupWith(createSession, [dependencyTs, randomFile], onHostCreate); + } + + function setupWithMainTsAndDependencyTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { + return setupWith(createSession, [mainTs, dependencyTs, randomFile], onHostCreate); + } + + function createSessionWithoutProjectReferences(onHostCreate?: OnHostCreate) { + const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true } + })); + onHostCreate?.(host); + const session = ts.projectSystem.createSession(host); + return { host, session }; + } + + function createSessionWithProjectReferences(onHostCreate?: OnHostCreate) { + const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); + onHostCreate?.(host); + const session = ts.projectSystem.createSession(host); + return { host, session }; + } + + function createSessionWithDisabledProjectReferences(onHostCreate?: OnHostCreate) { + const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { + composite: true, + declarationMap: true, + disableSourceOfProjectReferenceRedirect: true + }, + references: [{ path: "../dependency" }] + })); + onHostCreate?.(host); + const session = ts.projectSystem.createSession(host); + return { host, session }; + } + + function getDocumentPositionMapper(session: ts.projectSystem.TestSession) { + const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath); + const documentPositionMapper = dependencyMap?.documentPositionMapper; + return { dependencyMap, documentPositionMapper }; + } + + function checkMainProjectWithoutProjectReferences(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dtsPath]); + } + + function checkMainProjectWithoutProjectReferencesWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path]); + } + + function checkMainProjectWithProjectReferences(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dependencyTs.path]); + } + + function checkMainProjectWithDisabledProjectReferences(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dtsPath]); + } + + function checkMainProjectWithDisabledProjectReferencesWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path]); + } + + function checkDependencyProjectWith(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(dependencyConfig.path)!, [dependencyTs.path, ts.projectSystem.libFile.path, dependencyConfig.path]); + } + + function makeChangeToMainTs(session: ts.projectSystem.TestSession) { + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: mainTs.path, + line: 14, + offset: 1, + endLine: 14, + endOffset: 1, + insertString: "const x = 10;" + } + }); + } + + function makeChangeToDependencyTs(session: ts.projectSystem.TestSession) { + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, + line: 6, + offset: 1, + endLine: 6, + endOffset: 1, + insertString: "const x = 10;" + } + }); + } - function renameFromDependencyTs(fn: number): Action { - const defSpan = declarationSpan(fn); - const { contextStart: _, contextEnd: _1, ...triggerSpan } = defSpan; - return { - reqName: "rename", - request: { - command: ts.projectSystem.protocol.CommandTypes.Rename, - arguments: { file: dependencyTs.path, ...triggerSpan.start } - }, - expectedResponse: { - info: { - canRename: true, - fileToRename: undefined, - displayName: `fn${fn}`, - fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, - kind: ts.ScriptElementKind.functionElement, - kindModifiers: "export", - triggerSpan - }, - locs: [ - { file: dependencyTs.path, locs: [defSpan] } - ] - } - }; + describe("from project that uses dependency: goToDef", () => { + function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { + const result = setup(onHostCreate); + result.session.executeCommandSeq(goToDefFromMainTs(1).request); + return { ...result, ...getDocumentPositionMapper(result.session) }; } - function renameFromDependencyTsWithDependencyChange(fn: number): Action { - const { expectedResponse: { info, locs }, ...rest } = renameFromDependencyTs(fn + 1); - - return { - ...rest, - expectedResponse: { - info: { - ...info as ts.projectSystem.protocol.RenameInfoSuccess, - displayName: `fn${fn}`, - fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, - }, - locs - } - }; + function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { + return verifyScriptInfoCollectionWith(session, host, [mainTs], expectedInfos, expectedWatchedFiles); } - 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) - ] - } - ] - } - }; - } + describe("when main tsconfig doesnt have project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTs(createSessionWithoutProjectReferences, onHostCreate); + } - 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 setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - function removePath(array: readonly string[], ...delPaths: string[]) { - return array.filter(a => { - const aLower = a.toLowerCase(); - return delPaths.every(dPath => dPath !== aLower); - }); - } + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkMainProjectWithoutProjectReferences(session); + } - interface Action { - reqName: string; - request: Partial; - expectedResponse: Response; - } + function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkMainProjectWithoutProjectReferencesWithoutDts(session); + } - function verifyAction(session: ts.projectSystem.TestSession, { reqName, request, expectedResponse }: Action) { - const { response } = session.executeCommandSeq(request); - assert.deepEqual(response, expectedResponse, `Failed Request: ${reqName}`); - } + function expectedScriptInfosWhenMapped() { + return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; + } - function verifyDocumentPositionMapper(session: ts.projectSystem.TestSession, dependencyMap: ts.server.ScriptInfo | undefined, documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], equal: boolean, debugInfo?: string) { - assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap, `${debugInfo} dependencyMap`); - if (dependencyMap) { - verifyEquality(dependencyMap.documentPositionMapper, documentPositionMapper, equal, `${debugInfo} DocumentPositionMapper`); + function expectedWatchedFilesWhenMapped() { + return [dependencyTsPath, ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path]; } - } - function verifyDocumentPositionMapperEqual(session: ts.projectSystem.TestSession, dependencyMap: ts.server.ScriptInfo | undefined, documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], debugInfo?: string) { - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, /*equal*/ true, debugInfo); - } + function expectedScriptInfosWhenNoMap() { + // Because map is deleted, map and dependency are released + return removePath(expectedScriptInfosWhenMapped(), dtsMapPath, dependencyTsPath); + } - function verifyEquality(actual: T, expected: T, equal: boolean, debugInfo?: string) { - if (equal) { - assert.strictEqual(actual, expected, debugInfo); + function expectedWatchedFilesWhenNoMap() { + // Watches deleted file + return removePath(expectedWatchedFilesWhenMapped(), dependencyTsPath); } - else { - assert.notStrictEqual(actual, expected, debugInfo); + + function expectedScriptInfosWhenNoDts() { + // No dts, no map, no dependency + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); } - } - function verifyAllFnAction(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, action: (fn: number) => Action, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[], existingDependencyMap: ts.server.ScriptInfo | undefined, existingDocumentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"], existingMapEqual: boolean, existingDocumentPositionMapperEqual: boolean, skipMapPathInDtsInfo?: boolean) { - let sourceMapPath: ts.server.ScriptInfo["sourceMapFilePath"] | undefined; - let dependencyMap: ts.server.ScriptInfo | undefined; - let documentPositionMapper: ts.server.ScriptInfo["documentPositionMapper"]; - for (let fn = 1; fn <= 5; fn++) { - const fnAction = action(fn); - verifyAction(session, fnAction); - const debugInfo = `${fnAction.reqName}:: ${fn}`; - ts.projectSystem.checkScriptInfos(session.getProjectService(), expectedInfos, debugInfo); - ts.projectSystem.checkWatchedFiles(host, expectedWatchedFiles, debugInfo); - const dtsInfo = session.getProjectService().getScriptInfoForPath(dtsPath); - const dtsMapInfo = session.getProjectService().getScriptInfoForPath(dtsMapPath); - - if (fn === 1) { - if (dtsInfo) { - if (!skipMapPathInDtsInfo) { - if (dtsMapInfo) { - assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath, `${debugInfo} sourceMapFilePath`); - } - else { - assert.isNotString(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); - assert.isNotFalse(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); - assert.isDefined(dtsInfo.sourceMapFilePath, `${debugInfo} sourceMapFilePath`); - } - } - } - verifyEquality(dtsMapInfo, existingDependencyMap, existingMapEqual, `${debugInfo} dependencyMap`); - verifyEquality(existingDocumentPositionMapper, dtsMapInfo?.documentPositionMapper, existingDocumentPositionMapperEqual, `${debugInfo} DocumentPositionMapper`); - sourceMapPath = dtsInfo?.sourceMapFilePath; - dependencyMap = dtsMapInfo; - documentPositionMapper = dependencyMap?.documentPositionMapper; - } - else { - assert.equal(dtsInfo?.sourceMapFilePath, sourceMapPath, `${debugInfo} sourceMapFilePath`); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper, debugInfo); - } + function expectedWatchedFilesWhenNoDts() { + return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); } - } - function verifyScriptInfoCollectionWith(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, openFiles: readonly ts.projectSystem.File[], expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { - const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); + it("can go to definition correctly", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); - // Collecting at this point retains dependency.d.ts and map - ts.projectSystem.closeFilesForSession([randomFile], session); - ts.projectSystem.openFilesForSession([randomFile], session); - ts.projectSystem.checkScriptInfos(session.getProjectService(), expectedInfos); - ts.projectSystem.checkWatchedFiles(host, expectedWatchedFiles); - // If map is not collected, document position mapper shouldnt change - if (session.getProjectService().filenameToScriptInfo.has(dtsMapPath)) { - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - } + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Closing open file, removes dependencies too - ts.projectSystem.closeFilesForSession([...openFiles, randomFile], session); - ts.projectSystem.openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } + // change + makeChangeToMainTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - type OnHostCreate = (host: ts.projectSystem.TestServerHost) => void; - type CreateSessionFn = (onHostCreate?: OnHostCreate) => { - host: ts.projectSystem.TestServerHost; - session: ts.projectSystem.TestSession; - }; - function setupWith(createSession: CreateSessionFn, openFiles: readonly ts.projectSystem.File[], onHostCreate: OnHostCreate | undefined) { - const result = createSession(onHostCreate); - ts.projectSystem.openFilesForSession(openFiles, result.session); - return result; - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function setupWithMainTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { - return setupWith(createSession, [mainTs, randomFile], onHostCreate); - } + // change + makeChangeToMainTs(session); - function setupWithDependencyTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { - return setupWith(createSession, [dependencyTs, randomFile], onHostCreate); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - function setupWithMainTsAndDependencyTs(createSession: CreateSessionFn, onHostCreate: OnHostCreate | undefined) { - return setupWith(createSession, [mainTs, dependencyTs, randomFile], onHostCreate); - } + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function createSessionWithoutProjectReferences(onHostCreate?: OnHostCreate) { - const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); - // Erase project reference - host.writeFile(mainConfig.path, JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true } - })); - onHostCreate?.(host); - const session = ts.projectSystem.createSession(host); - return { host, session }; - } + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function createSessionWithProjectReferences(onHostCreate?: OnHostCreate) { - const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); - onHostCreate?.(host); - const session = ts.projectSystem.createSession(host); - return { host, session }; - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function createSessionWithDisabledProjectReferences(onHostCreate?: OnHostCreate) { - const host = ts.projectSystem.createHostWithSolutionBuild(files, [mainConfig.path]); - // Erase project reference - host.writeFile(mainConfig.path, JSON.stringify({ - compilerOptions: { - composite: true, - declarationMap: true, - disableSourceOfProjectReferenceRedirect: true - }, - references: [{ path: "../dependency" }] - })); - onHostCreate?.(host); - const session = ts.projectSystem.createSession(host); - return { host, session }; - } + // change + changeDtsFile(host); - function getDocumentPositionMapper(session: ts.projectSystem.TestSession) { - const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath); - const documentPositionMapper = dependencyMap?.documentPositionMapper; - return { dependencyMap, documentPositionMapper }; - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - function checkMainProjectWithoutProjectReferences(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dtsPath]); - } + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function checkMainProjectWithoutProjectReferencesWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path]); - } + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function checkMainProjectWithProjectReferences(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dependencyTs.path]); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function checkMainProjectWithDisabledProjectReferences(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path, dtsPath]); - } + // change + changeDtsMapFile(host); - function checkMainProjectWithDisabledProjectReferencesWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(mainConfig.path)!, [mainTs.path, ts.projectSystem.libFile.path, mainConfig.path]); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); - function checkDependencyProjectWith(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkProjectActualFiles(session.getProjectService().configuredProjects.get(dependencyConfig.path)!, [dependencyTs.path, ts.projectSystem.libFile.path, dependencyConfig.path]); - } + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; + host.deleteFile(dtsMapLocation); + }); - function makeChangeToMainTs(session: ts.projectSystem.TestSession) { - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: mainTs.path, - line: 14, - offset: 1, - endLine: 14, - endOffset: 1, - insertString: "const x = 10;" - } + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, + // The script info for map is collected only after file open + expectedScriptInfosWhenNoMap().concat(dependencyTs.path), expectedWatchedFilesWhenNoMap().concat(dependencyTsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); }); - } - function makeChangeToDependencyTs(session: ts.projectSystem.TestSession) { - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, - line: 6, - offset: 1, - endLine: 6, - endOffset: 1, - insertString: "const x = 10;" - } + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjectsWithoutDts(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); }); - } + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; + host.deleteFile(dtsLocation); + }); - describe("from project that uses dependency: goToDef", () => { - function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { - const result = setup(onHostCreate); - result.session.executeCommandSeq(goToDefFromMainTs(1).request); - return { ...result, ...getDocumentPositionMapper(result.session) }; + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, + // The script info for map is collected only after file open + expectedScriptInfosWhenNoDts().concat(dependencyTs.path, dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dependencyTsPath, dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + }); + describe("when main tsconfig has project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTs(createSessionWithProjectReferences, onHostCreate); } - function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { - return verifyScriptInfoCollectionWith(session, host, [mainTs], expectedInfos, expectedWatchedFiles); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); } - describe("when main tsconfig doesnt have project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTs(createSessionWithoutProjectReferences, onHostCreate); - } - - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } - - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkMainProjectWithoutProjectReferences(session); - } + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkMainProjectWithProjectReferences(session); + } - function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkMainProjectWithoutProjectReferencesWithoutDts(session); - } + function expectedScriptInfos() { + return [dependencyTs.path, ts.projectSystem.libFile.path, mainTs.path, randomFile.path]; + } - function expectedScriptInfosWhenMapped() { - return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; - } + function expectedWatchedFiles() { + return [dependencyTsPath, dependencyConfig.path, ts.projectSystem.libFile.path, mainConfig.path, randomConfig.path]; + } - function expectedWatchedFilesWhenMapped() { - return [dependencyTsPath, ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path]; - } + it("can go to definition correctly", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - function expectedScriptInfosWhenNoMap() { - // Because map is deleted, map and dependency are released - return removePath(expectedScriptInfosWhenMapped(), dtsMapPath, dependencyTsPath); - } + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFilesWhenNoMap() { - // Watches deleted file - return removePath(expectedWatchedFilesWhenMapped(), dependencyTsPath); - } + // change + makeChangeToMainTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function expectedScriptInfosWhenNoDts() { - // No dts, no map, no dependency - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFilesWhenNoDts() { - return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); - } + // change + makeChangeToMainTs(session); - it("can go to definition correctly", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - makeChangeToMainTs(session); + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + changeDtsFile(host); - // change - changeDtsFile(host); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // change - changeDtsMapFile(host); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); + // change + changeDtsMapFile(host); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, - // The script info for map is collected only after file open - expectedScriptInfosWhenNoMap().concat(dependencyTs.path), expectedWatchedFilesWhenNoMap().concat(dependencyTsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjectsWithoutDts(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, - // The script info for map is collected only after file open - expectedScriptInfosWhenNoDts().concat(dependencyTs.path, dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dependencyTsPath, dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); }); - }); - describe("when main tsconfig has project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTs(createSessionWithProjectReferences, onHostCreate); - } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } - - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkMainProjectWithProjectReferences(session); - } - - function expectedScriptInfos() { - return [dependencyTs.path, ts.projectSystem.libFile.path, mainTs.path, randomFile.path]; - } - - function expectedWatchedFiles() { - return [dependencyTsPath, dependencyConfig.path, ts.projectSystem.libFile.path, mainConfig.path, randomConfig.path]; - } + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - it("can go to definition correctly", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + it(`when defining project source changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + // Make change, without rebuild of solution + host.writeFile(dependencyTs.path, `function fooBar() { } +${dependencyTs.content}`); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // change - makeChangeToMainTs(session); + // action + verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when defining project source changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session } = setupWithAction(); - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + // Make change, without rebuild of solution + host.writeFile(dependencyTs.path, `function fooBar() { } +${dependencyTs.content}`); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // change - changeDtsFile(host); + it("when projects are not built", () => { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([mainTs, randomFile], session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + }); + describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTs(createSessionWithDisabledProjectReferences, onHostCreate); + } - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkMainProjectWithDisabledProjectReferences(session); + } - // change - changeDtsMapFile(host); + function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkMainProjectWithDisabledProjectReferencesWithoutDts(session); + } - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + function expectedScriptInfosWhenMapped() { + return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; + } - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function expectedWatchedFilesWhenMapped() { + return [dependencyTsPath, ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; + } - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); + function expectedScriptInfosWhenNoMap() { + // Because map is deleted, map and dependency are released + return removePath(expectedScriptInfosWhenMapped(), dtsMapPath, dependencyTsPath); + } - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + function expectedWatchedFilesWhenNoMap() { + // Watches deleted file + return removePath(expectedWatchedFilesWhenMapped(), dependencyTsPath); + } - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function expectedScriptInfosWhenNoDts() { + // No dts, no map, no dependency + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); + } - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); + function expectedWatchedFilesWhenNoDts() { + return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); + } - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + it("can go to definition correctly", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); - it(`when defining project source changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - // Make change, without rebuild of solution - host.writeFile(dependencyTs.path, `function fooBar() { } -${dependencyTs.content}`); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when defining project source changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session } = setupWithAction(); + // change + makeChangeToMainTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // change - // Make change, without rebuild of solution - host.writeFile(dependencyTs.path, `function fooBar() { } -${dependencyTs.content}`); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // action - verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + makeChangeToMainTs(session); - it("when projects are not built", () => { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([mainTs, randomFile], session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); }); - describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTs(createSessionWithDisabledProjectReferences, onHostCreate); - } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkMainProjectWithDisabledProjectReferences(session); - } + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkMainProjectWithDisabledProjectReferencesWithoutDts(session); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedScriptInfosWhenMapped() { - return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; - } + // change + changeDtsFile(host); - function expectedWatchedFilesWhenMapped() { - return [dependencyTsPath, ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - function expectedScriptInfosWhenNoMap() { - // Because map is deleted, map and dependency are released - return removePath(expectedScriptInfosWhenMapped(), dtsMapPath, dependencyTsPath); - } + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFilesWhenNoMap() { - // Watches deleted file - return removePath(expectedWatchedFilesWhenMapped(), dependencyTsPath); - } + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function expectedScriptInfosWhenNoDts() { - // No dts, no map, no dependency - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); - } + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFilesWhenNoDts() { - return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath, dependencyTsPath); - } + // change + changeDtsMapFile(host); - it("can go to definition correctly", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; + host.deleteFile(dtsMapLocation); }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - makeChangeToMainTs(session); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, + // The script info for map is collected only after file open + expectedScriptInfosWhenNoMap().concat(dependencyTs.path), expectedWatchedFilesWhenNoMap().concat(dependencyTsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjectsWithoutDts(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; + host.deleteFile(dtsLocation); }); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, + // The script info for map is collected only after file open + expectedScriptInfosWhenNoDts().concat(dependencyTs.path, dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dependencyTsPath, dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + }); + }); - // change - changeDtsFile(host); + describe("from defining project: rename", () => { + function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { + const result = setup(onHostCreate); + result.session.executeCommandSeq(renameFromDependencyTs(1).request); + return { ...result, ...getDocumentPositionMapper(result.session) }; + } - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { + return verifyScriptInfoCollectionWith(session, host, [dependencyTs], expectedInfos, expectedWatchedFiles); + } - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkDependencyProjectWith(session); + } - // change - changeDtsMapFile(host); + function expectedScriptInfos() { + return [ts.projectSystem.libFile.path, dtsLocation, dtsMapLocation, dependencyTs.path, randomFile.path]; + } - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); + function expectedWatchedFiles() { + return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, dependencyConfig.path, randomConfig.path]; + } - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function expectedScriptInfosWhenNoMap() { + // No map + return removePath(expectedScriptInfos(), dtsMapPath); + } - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, - // The script info for map is collected only after file open - expectedScriptInfosWhenNoMap().concat(dependencyTs.path), expectedWatchedFilesWhenNoMap().concat(dependencyTsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); + function expectedWatchedFilesWhenNoMap() { + // Watches deleted file = map file + return expectedWatchedFiles(); + } - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjectsWithoutDts(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + function expectedScriptInfosWhenNoDts() { + // no dts or map since dts itself doesnt exist + return removePath(expectedScriptInfos(), dtsPath, dtsMapPath); + } - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, - // The script info for map is collected only after file open - expectedScriptInfosWhenNoDts().concat(dependencyTs.path, dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dependencyTsPath, dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - }); - }); + function expectedWatchedFilesWhenNoDts() { + // watch deleted file + return removePath(expectedWatchedFiles(), dtsMapPath); + } - describe("from defining project: rename", () => { - function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { - const result = setup(onHostCreate); - result.session.executeCommandSeq(renameFromDependencyTs(1).request); - return { ...result, ...getDocumentPositionMapper(result.session) }; + describe("when main tsconfig doesnt have project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithDependencyTs(createSessionWithoutProjectReferences, onHostCreate); } - function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { - return verifyScriptInfoCollectionWith(session, host, [dependencyTs], expectedInfos, expectedWatchedFiles); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); } - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - checkDependencyProjectWith(session); - } + it("rename locations from dependency", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - function expectedScriptInfos() { - return [ts.projectSystem.libFile.path, dtsLocation, dtsMapLocation, dependencyTs.path, randomFile.path]; - } + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFiles() { - return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, dependencyConfig.path, randomConfig.path]; - } + // change + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function expectedScriptInfosWhenNoMap() { - // No map - return removePath(expectedScriptInfos(), dtsMapPath); - } + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function expectedWatchedFilesWhenNoMap() { - // Watches deleted file = map file - return expectedWatchedFiles(); - } + // change + makeChangeToDependencyTs(session); - function expectedScriptInfosWhenNoDts() { - // no dts or map since dts itself doesnt exist - return removePath(expectedScriptInfos(), dtsPath, dtsMapPath); - } + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - function expectedWatchedFilesWhenNoDts() { - // watch deleted file - return removePath(expectedWatchedFiles(), dtsMapPath); - } + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - describe("when main tsconfig doesnt have project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithDependencyTs(createSessionWithoutProjectReferences, onHostCreate); - } + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - it("rename locations from dependency", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + // change + changeDtsFile(host); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // change - makeChangeToDependencyTs(session); + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - changeDtsFile(host); + // change + changeDtsMapFile(host); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; + host.deleteFile(dtsMapLocation); }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - changeDtsMapFile(host); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; + host.deleteFile(dtsLocation); }); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, + // Map is collected after file open + expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + }); + describe("when main tsconfig has project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithDependencyTs(createSessionWithProjectReferences, onHostCreate); + } - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); + it("rename locations from dependency", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, - // Map is collected after file open - expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); + // change + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); }); - describe("when main tsconfig has project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithDependencyTs(createSessionWithProjectReferences, onHostCreate); - } + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + // change + makeChangeToDependencyTs(session); - it("rename locations from dependency", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - makeChangeToDependencyTs(session); + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + changeDtsFile(host); - // change - changeDtsFile(host); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // change - changeDtsMapFile(host); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); + // change + changeDtsMapFile(host); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, - // Map is collected after file open - expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); }); - it(`when defining project source changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, + // Map is collected after file open + expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); - // change - // Make change, without rebuild of solution - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } + it(`when defining project source changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + // Make change, without rebuild of solution + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } ` - } - }); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); + } }); - it(`when defining project source changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session } = setupWithAction(); - - // change - // Make change, without rebuild of solution - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, renameFromDependencyTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when defining project source changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session } = setupWithAction(); + + // change + // Make change, without rebuild of solution + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } ` - } - }); - - // action - verifyAllFnAction(session, host, renameFromDependencyTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); + } }); - it("when projects are not built", () => { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([dependencyTs, randomFile], session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTsWithDependencyChange, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); }); - describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithDependencyTs(createSessionWithDisabledProjectReferences, onHostCreate); - } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + it("when projects are not built", () => { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([dependencyTs, randomFile], session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + }); + describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithDependencyTs(createSessionWithDisabledProjectReferences, onHostCreate); + } - it("rename locations from dependency", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + it("rename locations from dependency", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); - // change - makeChangeToDependencyTs(session); + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - changeDtsFile(host); + // change + makeChangeToDependencyTs(session); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - changeDtsMapFile(host); + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // action - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // change + changeDtsFile(host); - // The dependency file is deleted when orphan files are collected - host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + + // action + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; + host.deleteFile(dtsMapLocation); }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // The dependency file is deleted when orphan files are collected + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, renameFromDependencyTs, - // Map is collected after file open - expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); }); + + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfos(), expectedWatchedFiles(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfos(), expectedWatchedFiles()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, renameFromDependencyTs, + // Map is collected after file open + expectedScriptInfosWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesWhenNoDts().concat(dtsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoDts(), expectedWatchedFilesWhenNoDts()); }); }); + }); + + describe("when opening depedency and usage project: goToDef and rename", () => { + function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { + const result = setup(onHostCreate); + result.session.executeCommandSeq(goToDefFromMainTs(1).request); + result.session.executeCommandSeq(renameFromDependencyTsWithBothProjectsOpen(1).request); + return { ...result, ...getDocumentPositionMapper(result.session) }; + } - describe("when opening depedency and usage project: goToDef and rename", () => { - function setupWithActionWith(setup: (onHostCreate?: OnHostCreate) => ReturnType, onHostCreate: OnHostCreate | undefined) { - const result = setup(onHostCreate); - result.session.executeCommandSeq(goToDefFromMainTs(1).request); - result.session.executeCommandSeq(renameFromDependencyTsWithBothProjectsOpen(1).request); - return { ...result, ...getDocumentPositionMapper(result.session) }; + function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { + return verifyScriptInfoCollectionWith(session, host, [mainTs, dependencyTs], expectedInfos, expectedWatchedFiles); + } + + describe("when main tsconfig doesnt have project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTsAndDependencyTs(createSessionWithoutProjectReferences, onHostCreate); } - function verifyScriptInfoCollection(session: ts.projectSystem.TestSession, host: ts.projectSystem.TestServerHost, expectedInfos: readonly string[], expectedWatchedFiles: readonly string[]) { - return verifyScriptInfoCollectionWith(session, host, [mainTs, dependencyTs], expectedInfos, expectedWatchedFiles); + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); } - describe("when main tsconfig doesnt have project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTsAndDependencyTs(createSessionWithoutProjectReferences, onHostCreate); - } + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + checkMainProjectWithoutProjectReferences(session); + checkDependencyProjectWith(session); + } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + checkMainProjectWithoutProjectReferencesWithoutDts(session); + checkDependencyProjectWith(session); + } - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - checkMainProjectWithoutProjectReferences(session); - checkDependencyProjectWith(session); - } + function expectedScriptInfosWhenMapped() { + return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; + } - function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - checkMainProjectWithoutProjectReferencesWithoutDts(session); - checkDependencyProjectWith(session); - } + function expectedWatchedFilesWhenMapped() { + return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; + } - function expectedScriptInfosWhenMapped() { - return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; - } + function expectedScriptInfosWhenNoMap() { + // Because map is deleted, map and dependency are released + return removePath(expectedScriptInfosWhenMapped(), dtsMapPath); + } - function expectedWatchedFilesWhenMapped() { - return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; - } + function expectedWatchedFilesWhenNoMap() { + // Watches deleted file + return expectedWatchedFilesWhenMapped(); + } - function expectedScriptInfosWhenNoMap() { - // Because map is deleted, map and dependency are released - return removePath(expectedScriptInfosWhenMapped(), dtsMapPath); - } + function expectedScriptInfosAfterGotoDefWhenNoDts() { + // No dts, no map + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); + } - function expectedWatchedFilesWhenNoMap() { - // Watches deleted file - return expectedWatchedFilesWhenMapped(); - } + function expectedWatchedFilesAfterGotoDefWhenNoDts() { + return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath); + } - function expectedScriptInfosAfterGotoDefWhenNoDts() { - // No dts, no map - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); - } + function expectedScriptInfosAfterRenameWhenNoDts() { + // No dts, no map + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); + } - function expectedWatchedFilesAfterGotoDefWhenNoDts() { - return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath); - } + function expectedWatchedFilesAfterRenameWhenNoDts() { + // Watches dts file but not map file + return removePath(expectedWatchedFilesWhenMapped(), dtsMapPath); + } - function expectedScriptInfosAfterRenameWhenNoDts() { - // No dts, no map - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); - } + it("goto Definition in usage and rename locations from defining project", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); - function expectedWatchedFilesAfterRenameWhenNoDts() { - // Watches dts file but not map file - return removePath(expectedWatchedFilesWhenMapped(), dtsMapPath); - } + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - it("goto Definition in usage and rename locations from defining project", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // The dependency file is deleted when orphan files are collected + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjectsWithoutDts(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosAfterGotoDefWhenNoDts(), expectedWatchedFilesAfterGotoDefWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjectsWithoutDts(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosAfterGotoDefWhenNoDts(), expectedWatchedFilesAfterGotoDefWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, - // The script info for map is collected only after file open - expectedScriptInfosAfterGotoDefWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterGotoDefWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, - // The script info for map is collected only after file open - expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); }); + + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); }); - describe("when main tsconfig has project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTsAndDependencyTs(createSessionWithProjectReferences, onHostCreate); - } + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, + // The script info for map is collected only after file open + expectedScriptInfosAfterGotoDefWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterGotoDefWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, + // The script info for map is collected only after file open + expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + }); + describe("when main tsconfig has project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTsAndDependencyTs(createSessionWithProjectReferences, onHostCreate); + } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - checkMainProjectWithProjectReferences(session); - checkDependencyProjectWith(session); - } + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + checkMainProjectWithProjectReferences(session); + checkDependencyProjectWith(session); + } - function expectedScriptInfosAfterGotoDef() { - return [dependencyTs.path, ts.projectSystem.libFile.path, mainTs.path, randomFile.path]; - } + function expectedScriptInfosAfterGotoDef() { + return [dependencyTs.path, ts.projectSystem.libFile.path, mainTs.path, randomFile.path]; + } - function expectedWatchedFilesAfterGotoDef() { - return [dependencyConfig.path, ts.projectSystem.libFile.path, mainConfig.path, randomConfig.path]; - } + function expectedWatchedFilesAfterGotoDef() { + return [dependencyConfig.path, ts.projectSystem.libFile.path, mainConfig.path, randomConfig.path]; + } - function expectedScriptInfosAfterRenameWhenMapped() { - return expectedScriptInfosAfterGotoDef().concat(dtsLocation, dtsMapLocation); - } + function expectedScriptInfosAfterRenameWhenMapped() { + return expectedScriptInfosAfterGotoDef().concat(dtsLocation, dtsMapLocation); + } - function expectedWatchedFilesAfterRenameWhenMapped() { - return expectedWatchedFilesAfterGotoDef().concat(dtsPath, dtsMapPath); - } + function expectedWatchedFilesAfterRenameWhenMapped() { + return expectedWatchedFilesAfterGotoDef().concat(dtsPath, dtsMapPath); + } - function expectedScriptInfosAfterRenameWhenNoMap() { - // Map file is not present - return removePath(expectedScriptInfosAfterRenameWhenMapped(), dtsMapPath); - } + function expectedScriptInfosAfterRenameWhenNoMap() { + // Map file is not present + return removePath(expectedScriptInfosAfterRenameWhenMapped(), dtsMapPath); + } - function expectedWatchedFilesAfterRenameWhenNoMap() { - // Watches map file - return expectedWatchedFilesAfterRenameWhenMapped(); - } + function expectedWatchedFilesAfterRenameWhenNoMap() { + // Watches map file + return expectedWatchedFilesAfterRenameWhenMapped(); + } - function expectedScriptInfosAfterRenameWhenNoDts() { - // map and dts not present - return removePath(expectedScriptInfosAfterRenameWhenMapped(), dtsPath, dtsMapPath); - } + function expectedScriptInfosAfterRenameWhenNoDts() { + // map and dts not present + return removePath(expectedScriptInfosAfterRenameWhenMapped(), dtsPath, dtsMapPath); + } - function expectedWatchedFilesAfterRenameWhenNoDts() { - // Watches dts file but not map - return removePath(expectedWatchedFilesAfterRenameWhenMapped(), dtsMapPath); - } + function expectedWatchedFilesAfterRenameWhenNoDts() { + // Watches dts file but not map + return removePath(expectedWatchedFilesAfterRenameWhenMapped(), dtsMapPath); + } - it("goto Definition in usage and rename locations from defining project", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); - }); + it("goto Definition in usage and rename locations from defining project", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); + }); - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenNoMap(), - // Map file is reset so its not watched any more, as this action doesnt need map - removePath(expectedWatchedFilesAfterRenameWhenNoMap(), dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true, - /*skipMapPathInDtsInfo*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); - // The dependency file is deleted when orphan files are collected + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + }); + + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenNoMap(), - // Map file is reset so its not watched any more, as this action doesnt need map - removePath(expectedWatchedFilesAfterRenameWhenNoMap(), dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false, - /*skipMapPathInDtsInfo*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap()); }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), - // Since the project for dependency is not updated, the watcher from rename for dts still there - expectedWatchedFilesAfterGotoDef().concat(dtsPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenNoMap(), + // Map file is reset so its not watched any more, as this action doesnt need map + removePath(expectedWatchedFilesAfterRenameWhenNoMap(), dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true, + /*skipMapPathInDtsInfo*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterRenameWhenNoMap(), + // Map file is reset so its not watched any more, as this action doesnt need map + removePath(expectedWatchedFilesAfterRenameWhenNoMap(), dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false, + /*skipMapPathInDtsInfo*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoMap(), expectedWatchedFilesAfterRenameWhenNoMap()); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTs, - // Map collection after file open - expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), - // not watching dts since this operation doesnt need it - removePath(expectedWatchedFilesAfterRenameWhenNoDts(), dtsPath).concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, - // Map collection after file open - expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); }); - it(`when defining project source changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), + // Since the project for dependency is not updated, the watcher from rename for dts still there + expectedWatchedFilesAfterGotoDef().concat(dtsPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTs, + // Map collection after file open + expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), + // not watching dts since this operation doesnt need it + removePath(expectedWatchedFilesAfterRenameWhenNoDts(), dtsPath).concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, + // Map collection after file open + expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + + it(`when defining project source changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // change - // Make change, without rebuild of solution - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } + // change + // Make change, without rebuild of solution + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } ` - } - }); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpenWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); + } }); - it(`when defining project source changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - // Make change, without rebuild of solution - session.executeCommandSeq({ - command: ts.projectSystem.protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpenWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when defining project source changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + // Make change, without rebuild of solution + session.executeCommandSeq({ + command: ts.projectSystem.protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } ` - } - }); - - // action - verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpenWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); + } }); - it("when projects are not built", () => { - const host = ts.projectSystem.createServerHost(files); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([mainTs, dependencyTs, randomFile], session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); - }); + // action + verifyAllFnAction(session, host, goToDefFromMainTsWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpenWithDependencyChange, expectedScriptInfosAfterRenameWhenMapped(), expectedWatchedFilesAfterRenameWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); }); - describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { - function setup(onHostCreate?: OnHostCreate) { - return setupWithMainTsAndDependencyTs(createSessionWithDisabledProjectReferences, onHostCreate); - } - function setupWithAction(onHostCreate?: OnHostCreate) { - return setupWithActionWith(setup, onHostCreate); - } + it("when projects are not built", () => { + const host = ts.projectSystem.createServerHost(files); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([mainTs, dependencyTs, randomFile], session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosAfterGotoDef(), expectedWatchedFilesAfterGotoDef(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + }); + describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { + function setup(onHostCreate?: OnHostCreate) { + return setupWithMainTsAndDependencyTs(createSessionWithDisabledProjectReferences, onHostCreate); + } - function checkProjects(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - checkMainProjectWithDisabledProjectReferences(session); - checkDependencyProjectWith(session); - } + function setupWithAction(onHostCreate?: OnHostCreate) { + return setupWithActionWith(setup, onHostCreate); + } - function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { - ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - checkMainProjectWithDisabledProjectReferencesWithoutDts(session); - checkDependencyProjectWith(session); - } + function checkProjects(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + checkMainProjectWithDisabledProjectReferences(session); + checkDependencyProjectWith(session); + } - function expectedScriptInfosWhenMapped() { - return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; - } + function checkProjectsWithoutDts(session: ts.projectSystem.TestSession) { + ts.projectSystem.checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + checkMainProjectWithDisabledProjectReferencesWithoutDts(session); + checkDependencyProjectWith(session); + } - function expectedWatchedFilesWhenMapped() { - return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; - } + function expectedScriptInfosWhenMapped() { + return [mainTs.path, randomFile.path, dependencyTs.path, ts.projectSystem.libFile.path, dtsPath, dtsMapLocation]; + } - function expectedScriptInfosWhenNoMap() { - // Because map is deleted, map and dependency are released - return removePath(expectedScriptInfosWhenMapped(), dtsMapPath); - } + function expectedWatchedFilesWhenMapped() { + return [ts.projectSystem.libFile.path, dtsPath, dtsMapPath, mainConfig.path, randomConfig.path, dependencyConfig.path]; + } - function expectedWatchedFilesWhenNoMap() { - // Watches deleted file - return expectedWatchedFilesWhenMapped(); - } + function expectedScriptInfosWhenNoMap() { + // Because map is deleted, map and dependency are released + return removePath(expectedScriptInfosWhenMapped(), dtsMapPath); + } - function expectedScriptInfosAfterGotoDefWhenNoDts() { - // No dts, no map - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); - } + function expectedWatchedFilesWhenNoMap() { + // Watches deleted file + return expectedWatchedFilesWhenMapped(); + } - function expectedWatchedFilesAfterGotoDefWhenNoDts() { - return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath); - } + function expectedScriptInfosAfterGotoDefWhenNoDts() { + // No dts, no map + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); + } - function expectedScriptInfosAfterRenameWhenNoDts() { - // No dts, no map - return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); - } + function expectedWatchedFilesAfterGotoDefWhenNoDts() { + return removePath(expectedWatchedFilesWhenMapped(), dtsPath, dtsMapPath); + } - function expectedWatchedFilesAfterRenameWhenNoDts() { - // Watches dts file but not map file - return removePath(expectedWatchedFilesWhenMapped(), dtsMapPath); - } + function expectedScriptInfosAfterRenameWhenNoDts() { + // No dts, no map + return removePath(expectedScriptInfosWhenMapped(), dtsPath, dtsMapPath); + } - it("goto Definition in usage and rename locations from defining project", () => { - const { host, session } = setup(); - checkProjects(session); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); + function expectedWatchedFilesAfterRenameWhenNoDts() { + // Watches dts file but not map file + return removePath(expectedWatchedFilesWhenMapped(), dtsMapPath); + } - // Edit - it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - makeChangeToMainTs(session); - makeChangeToDependencyTs(session); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + it("goto Definition in usage and rename locations from defining project", () => { + const { host, session } = setup(); + checkProjects(session); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap, documentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); - // Edit dts to add new fn - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // Edit + it(`when usage file changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - // Edit map file to represent added new line - it(`when dependency file's map changes, when timeout occurs before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - host.runQueuedTimeoutCallbacks(); - checkProjects(session); - verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); - it(`when dependency file's map changes, when timeout does not occur before request`, () => { - // Create DocumentPositionMapper - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); - - // change - changeDtsMapFile(host); - - // action - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ false); - const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - }); + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - it(`with depedency files map file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); - checkProjects(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); - }); - it(`with depedency files map file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsMapLocation)!; - host.deleteFile(dtsMapLocation); - }); - - host.writeFile(dtsMapLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency files map file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when usage file changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + makeChangeToMainTs(session); + makeChangeToDependencyTs(session); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + + // Edit dts to add new fn + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); - // The dependency file is deleted when orphan files are collected + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency .d.ts changes, document position mapper doesnt change, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + + // Edit map file to represent added new line + it(`when dependency file's map changes, when timeout occurs before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + host.runQueuedTimeoutCallbacks(); + checkProjects(session); + verifyDocumentPositionMapperEqual(session, dependencyMap, documentPositionMapper); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + it(`when dependency file's map changes, when timeout does not occur before request`, () => { + // Create DocumentPositionMapper + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // change + changeDtsMapFile(host); + + // action + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ false); + const { documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + }); + + it(`with depedency files map file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsMapLocation)); + checkProjects(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); + it(`with depedency files map file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsMapLocation)!; host.deleteFile(dtsMapLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); }); - it(`with depedency .d.ts file, when file is not present`, () => { - const { host, session } = setup(host => host.deleteFile(dtsLocation)); - checkProjectsWithoutDts(session); - - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosAfterGotoDefWhenNoDts(), expectedWatchedFilesAfterGotoDefWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), - /*existingDependencyMap*/ undefined, - /*existingDocumentPositionMapper*/ undefined, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); - }); - it(`with depedency .d.ts file, when file is created after actions on projects`, () => { - let fileContents: string; - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { - fileContents = host.readFile(dtsLocation)!; - host.deleteFile(dtsLocation); - }); - - host.writeFile(dtsLocation, fileContents!); - verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ false, - /*existingDocumentPositionMapperEqual*/ false); - const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); - verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjects(session); - verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); - }); - it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { - const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + host.writeFile(dtsMapLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency files map file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsMapLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoMap, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenNoMap(), expectedWatchedFilesWhenNoMap()); + }); - // The dependency file is deleted when orphan files are collected + it(`with depedency .d.ts file, when file is not present`, () => { + const { host, session } = setup(host => host.deleteFile(dtsLocation)); + checkProjectsWithoutDts(session); + + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, expectedScriptInfosAfterGotoDefWhenNoDts(), expectedWatchedFilesAfterGotoDefWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts(), + /*existingDependencyMap*/ undefined, + /*existingDocumentPositionMapper*/ undefined, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); + }); + it(`with depedency .d.ts file, when file is created after actions on projects`, () => { + let fileContents: string; + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(host => { + fileContents = host.readFile(dtsLocation)!; host.deleteFile(dtsLocation); - verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, - // The script info for map is collected only after file open - expectedScriptInfosAfterGotoDefWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterGotoDefWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - verifyAllFnAction(session, host, renameFromDependencyTs, - // The script info for map is collected only after file open - expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, - /*existingMapEqual*/ true, - /*existingDocumentPositionMapperEqual*/ true); - checkProjectsWithoutDts(session); - - // Script info collection should behave as "noDts" - verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); }); + + host.writeFile(dtsLocation, fileContents!); + verifyAllFnAction(session, host, goToDefFromMainTs, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ false, + /*existingDocumentPositionMapperEqual*/ false); + const { dependencyMap: newDependencyMap, documentPositionMapper: newDocumentPositionMapper } = getDocumentPositionMapper(session); + verifyAllFnAction(session, host, renameFromDependencyTsWithBothProjectsOpen, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped(), newDependencyMap, newDocumentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjects(session); + verifyScriptInfoCollection(session, host, expectedScriptInfosWhenMapped(), expectedWatchedFilesWhenMapped()); + }); + it(`with depedency .d.ts file, when file is deleted after actions on the projects`, () => { + const { host, session, dependencyMap, documentPositionMapper } = setupWithAction(); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(dtsLocation); + verifyAllFnAction(session, host, goToDefFromMainTsWithNoDts, + // The script info for map is collected only after file open + expectedScriptInfosAfterGotoDefWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterGotoDefWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + verifyAllFnAction(session, host, renameFromDependencyTs, + // The script info for map is collected only after file open + expectedScriptInfosAfterRenameWhenNoDts().concat(dtsMapLocation), expectedWatchedFilesAfterRenameWhenNoDts().concat(dtsMapPath), dependencyMap, documentPositionMapper, + /*existingMapEqual*/ true, + /*existingDocumentPositionMapperEqual*/ true); + checkProjectsWithoutDts(session); + + // Script info collection should behave as "noDts" + verifyScriptInfoCollection(session, host, expectedScriptInfosAfterRenameWhenNoDts(), expectedWatchedFilesAfterRenameWhenNoDts()); }); }); }); +}); } diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 3894e832e3334..a95175d944d12 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1,1299 +1,1299 @@ namespace ts.projectSystem { - describe("unittests:: tsserver:: Projects", () => { - it("handles the missing files - that were added to program because they were added with /// { - const file1: ts.projectSystem.File = { - path: "/a/b/commonFile1.ts", - content: `/// +describe("unittests:: tsserver:: Projects", () => { + it("handles the missing files - that were added to program because they were added with /// { + const file1: ts.projectSystem.File = { + path: "/a/b/commonFile1.ts", + content: `/// let x = y` - }; - const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); - const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); - ts.projectSystem.openFilesForSession([file1], session); - const getErrRequest = ts.projectSystem.makeSessionRequest(ts.server.CommandNames.SemanticDiagnosticsSync, { file: file1.path }); - - // Two errors: CommonFile2 not found and cannot find name y - session.executeCommand(getErrRequest); - - host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); - host.runQueuedTimeoutCallbacks(); - session.executeCommand(getErrRequest); - ts.projectSystem.baselineTsserverLogs("projects", "handles the missing files added with tripleslash ref", session); - }); + }; + const host = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile]); + const session = ts.projectSystem.createSession(host, { logger: ts.projectSystem.createLoggerWithInMemoryLogs() }); + ts.projectSystem.openFilesForSession([file1], session); + const getErrRequest = ts.projectSystem.makeSessionRequest(ts.server.CommandNames.SemanticDiagnosticsSync, { file: file1.path }); + + // Two errors: CommonFile2 not found and cannot find name y + session.executeCommand(getErrRequest); + + host.writeFile(ts.projectSystem.commonFile2.path, ts.projectSystem.commonFile2.content); + host.runQueuedTimeoutCallbacks(); + session.executeCommand(getErrRequest); + ts.projectSystem.baselineTsserverLogs("projects", "handles the missing files added with tripleslash ref", session); + }); - it("should create new inferred projects for files excluded from a configured project", () => { - const configFile: ts.projectSystem.File = { - path: "/a/b/tsconfig.json", - content: `{ + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: ts.projectSystem.File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "files": ["${ts.projectSystem.commonFile1.path}", "${ts.projectSystem.commonFile2.path}"] }` - }; - const files = [ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]; - const host = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(ts.projectSystem.commonFile1.path); - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - configFile.content = `{ + }; + const files = [ts.projectSystem.commonFile1, ts.projectSystem.commonFile2, configFile]; + const host = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openClientFile(ts.projectSystem.commonFile1.path); + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + configFile.content = `{ "compilerOptions": {}, "files": ["${ts.projectSystem.commonFile1.path}"] }`; - host.writeFile(configFile.path, configFile.content); - - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); - host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects - ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); - ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); - projectService.openClientFile(ts.projectSystem.commonFile2.path); - ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); - const proj1 = projectService.findProject(proj1name)!; - assert.isTrue(proj1.languageServiceEnabled); + host.writeFile(configFile.path, configFile.content); + + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path, ts.projectSystem.commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects + ts.projectSystem.checkNumberOfConfiguredProjects(projectService, 1); + ts.projectSystem.checkProjectRootFiles(project, [ts.projectSystem.commonFile1.path]); + projectService.openClientFile(ts.projectSystem.commonFile2.path); + ts.projectSystem.checkNumberOfInferredProjects(projectService, 1); + }); - projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); - const proj2 = projectService.findProject(proj2name)!; - assert.isTrue(proj2.languageServiceEnabled); + 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 = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); + const proj1 = projectService.findProject(proj1name)!; + assert.isTrue(proj1.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); + const proj2 = projectService.findProject(proj2name)!; + assert.isTrue(proj2.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); + const proj3 = projectService.findProject(proj3name)!; + assert.isFalse(proj3.languageServiceEnabled); + }); - projectService.openExternalProject({ rootFiles: ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, eventHandler: ts.noop }); + projectService.openExternalProject({ rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName }); + const proj1 = projectService.findProject(projName)!; + assert.isFalse(proj1.languageServiceEnabled); + + assert.doesNotThrow(() => projectService.openClientFile(file2.path)); + }); - it("should not crash when opening a file in a project with a disabled language service", () => { + 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 = ts.projectSystem.createServerHost([file1, file2]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, eventHandler: ts.noop }); - projectService.openExternalProject({ rootFiles: ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, config1]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); - projectService.openExternalProject({ - rootFiles: ts.projectSystem.toExternalFiles([file1.path, config1.path]), - options: {}, - projectFileName: externalProjectName - }); - - ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, config1]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); - projectService.openClientFile(file1.path, file1.content); - - ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, config1]); - const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); - projectService.applyChangesInOpenFiles(ts.singleIterator({ fileName: file1.path, content: file1.content })); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - assert.isDefined(proj); - - assert.isTrue(proj.fileExists(file1.path)); + const externalProjectName = "externalproject"; + const host = ts.projectSystem.createServerHost([file1, config1]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); + projectService.openExternalProject({ + rootFiles: ts.projectSystem.toExternalFiles([file1.path, config1.path]), + options: {}, + projectFileName: externalProjectName }); - }); - - 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 = ts.projectSystem.createServerHost([f1, f2, ts.projectSystem.libFile]); - const service = ts.projectSystem.createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let x: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, ts.projectSystem.libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, ts.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, ts.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 = ts.projectSystem.createServerHost([f1, f2, ts.projectSystem.libFile]); - const service = ts.projectSystem.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 }); - ts.projectSystem.checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, ts.projectSystem.libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, ts.emptyOptions)!; - assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); + ts.projectSystem.checkNumberOfProjects(projectService, { externalProjects: 1 }); + const proj = projectService.externalProjects[0]; + assert.isDefined(proj); - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, ts.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, ""); + assert.isTrue(proj.fileExists(file1.path)); }); - 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 = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.openClientFile(file1.path); + const host = ts.projectSystem.createServerHost([file1, config1]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); + projectService.openClientFile(file1.path, file1.content); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const inferredProject0 = projectService.inferredProjects[0]; - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - - projectService.openClientFile(file3.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - const inferredProject1 = projectService.inferredProjects[1]; - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - host.writeFile(file2.path, `export * from "../c/f3"`); // now inferred project should inclule file3 - host.checkTimeoutQueueLengthAndRun(2); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.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"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` + 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 = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file1.path); - + const host = ts.projectSystem.createServerHost([file1, config1]); + const projectService = ts.projectSystem.createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); + projectService.applyChangesInOpenFiles(ts.singleIterator({ fileName: file1.path, content: file1.content })); ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.openClientFile(file3.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - host.deleteFile(file2.path); - host.checkTimeoutQueueLengthAndRun(2); - - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); - 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 = ts.projectSystem.createServerHost([file1, office, ts.projectSystem.customTypesMap]); - const projectService = ts.projectSystem.createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.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(); - } + assert.isTrue(proj.fileExists(file1.path)); }); + }); - 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 = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, constructorFile, bliss, ts.projectSystem.customTypesMap]); - let request: string | undefined; - const cachePath = "/a/data"; - const typingsInstaller: ts.server.ITypingsInstaller = { - isKnownTypesPackageName: ts.returnFalse, - installPackage: ts.notImplemented, - enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { - assert.isUndefined(request); - request = JSON.stringify(ts.server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || ts.server.emptyArray, cachePath)); - }, - attach: ts.noop, - onProjectClosed: ts.noop, - globalTypingsCacheLocation: cachePath - }; + 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 = ts.projectSystem.createServerHost([f1, f2, ts.projectSystem.libFile]); + const service = ts.projectSystem.createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: ts.projectSystem.toExternalFiles([f1.path, f2.path]), options: {} }); + + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let x: string"); + + service.checkNumberOfProjects({ externalProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, ts.projectSystem.libFile.path]); + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, ts.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, ts.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'"); + }); - const projectName = "project"; - const projectService = ts.projectSystem.createProjectService(host, { typingsInstaller }); - projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); - assert.equal(request, JSON.stringify({ - projectName, - fileNames: [ts.projectSystem.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, - }); + 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 = ts.projectSystem.createServerHost([f1, f2, ts.projectSystem.libFile]); + const service = ts.projectSystem.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 }); + ts.projectSystem.checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, ts.projectSystem.libFile.path]); + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, ts.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, ts.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, ""); + }); - host.checkTimeoutQueueLength(0); - assert.isUndefined(request); - }); + 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 = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const inferredProject0 = projectService.inferredProjects[0]; + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + + projectService.openClientFile(file3.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + const inferredProject1 = projectService.inferredProjects[1]; + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + host.writeFile(file2.path, `export * from "../c/f3"`); // now inferred project should inclule file3 + host.checkTimeoutQueueLengthAndRun(2); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + }); - 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 = ts.projectSystem.createServerHost(files); - const projectService = ts.projectSystem.createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.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("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 = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + + projectService.openClientFile(file3.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + + host.deleteFile(file2.path); + host.checkTimeoutQueueLengthAndRun(2); + + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + }); - 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(ts.removeMinAndVersionNumbers(t[0]), t[1], t[0]); - } - }); + 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 = ts.projectSystem.createServerHost([file1, office, ts.projectSystem.customTypesMap]); + const projectService = ts.projectSystem.createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.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("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 = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.customTypesMap]); - const projectService = ts.projectSystem.createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(), [file2.path]); - } - 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 = ts.projectSystem.createServerHost([file1, ts.projectSystem.libFile, constructorFile, bliss, ts.projectSystem.customTypesMap]); + let request: string | undefined; + const cachePath = "/a/data"; + const typingsInstaller: ts.server.ITypingsInstaller = { + isKnownTypesPackageName: ts.returnFalse, + installPackage: ts.notImplemented, + enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { + assert.isUndefined(request); + request = JSON.stringify(ts.server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || ts.server.emptyArray, cachePath)); + }, + attach: ts.noop, + onProjectClosed: ts.noop, + globalTypingsCacheLocation: cachePath + }; + + const projectName = "project"; + const projectService = ts.projectSystem.createProjectService(host, { typingsInstaller }); + projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); + assert.equal(request, JSON.stringify({ + projectName, + fileNames: [ts.projectSystem.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, }); - 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 = ts.projectSystem.createServerHost([file1, file2, file3]); - const projectService = ts.projectSystem.createProjectService(host); - - projectService.openClientFile(file2.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - let inferredProjects = projectService.inferredProjects.slice(); - - projectService.openClientFile(file3.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - inferredProjects = projectService.inferredProjects.slice(); - - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); - assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); - ts.projectSystem.checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - inferredProjects = projectService.inferredProjects.slice(); - - projectService.closeClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 3 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); - inferredProjects = projectService.inferredProjects.slice(); - - projectService.closeClientFile(file3.path); - ts.projectSystem.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()); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - assert.isTrue(projectService.inferredProjects[2].isOrphan()); - - projectService.openClientFile(file3.path); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); - assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - }); + host.checkTimeoutQueueLength(0); + assert.isUndefined(request); + }); - 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" - }; + 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 = ts.projectSystem.createServerHost(files); + const projectService = ts.projectSystem.createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.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(); + } + }); - const host = ts.projectSystem.createServerHost([]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.applyChangesInOpenFiles(ts.singleIterator(tsFile)); - const projs = projectService.synchronizeProjectList([]); - projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); - projectService.synchronizeProjectList([projs[0].info!]); - projectService.applyChangesInOpenFiles(ts.singleIterator(jsFile)); - }); + 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(ts.removeMinAndVersionNumbers(t[0]), t[1], t[0]); + } + }); - 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 = ts.projectSystem.createServerHost([file1, file2, config]); - const projectService = ts.projectSystem.createProjectService(host); + 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 = ts.projectSystem.createServerHost([file1, file2, file3, ts.projectSystem.customTypesMap]); + const projectService = ts.projectSystem.createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: ts.projectSystem.toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(), [file2.path]); + } + finally { + projectService.resetSafeList(); + } + }); - projectService.openClientFile(file1.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + 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 = ts.projectSystem.createServerHost([file1, file2, file3]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file2.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + let inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file3.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); + ts.projectSystem.checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file3.path); + ts.projectSystem.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()); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + assert.isTrue(projectService.inferredProjects[2].isOrphan()); + + projectService.openClientFile(file3.path); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); - projectService.openClientFile(file2.path); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.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 = ts.projectSystem.createServerHost([]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.applyChangesInOpenFiles(ts.singleIterator(tsFile)); + const projs = projectService.synchronizeProjectList([]); + projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); + projectService.synchronizeProjectList([projs[0].info!]); + projectService.applyChangesInOpenFiles(ts.singleIterator(jsFile)); + }); - host.deleteFile(config.path); - host.checkTimeoutQueueLengthAndRun(1); - ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - }); + 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 = ts.projectSystem.createServerHost([file1, file2, config]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + projectService.openClientFile(file2.path); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + host.deleteFile(config.path); + host.checkTimeoutQueueLengthAndRun(1); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + ts.projectSystem.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 = ts.projectSystem.createServerHost([f1, f2, f3, config]); - const projectService = ts.projectSystem.createProjectService(host); - projectService.setHostConfiguration({ - extraFileExtensions: [ - { extension: ".js", isMixedContent: false }, - { extension: ".html", isMixedContent: true } - ] - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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)); - ts.projectSystem.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)); - ts.projectSystem.checkProjectActualFiles(projectService.inferredProjects[0], [f3.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 = ts.projectSystem.createServerHost([f1, f2, f3, config]); + const projectService = ts.projectSystem.createProjectService(host); + projectService.setHostConfiguration({ + extraFileExtensions: [ + { extension: ".js", isMixedContent: false }, + { extension: ".html", isMixedContent: true } + ] }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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)); + ts.projectSystem.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)); + ts.projectSystem.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 = ts.projectSystem.createServerHost([file1, file2, config]); - const session = ts.projectSystem.createSession(host); - ts.projectSystem.openFilesForSession([file1], session); - const projectService = session.getProjectService(); + 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 = ts.projectSystem.createServerHost([file1, file2, config]); + const session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + // HTML file will not be included in any projects yet + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const configuredProj = ts.projectSystem.configuredProjectAt(projectService, 0); + ts.projectSystem.checkProjectActualFiles(configuredProj, [file1.path, config.path]); + + // Specify .html extension as mixed content + const extraFileExtensions = [{ extension: ".html", scriptKind: ts.ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + // The configured project should now be updated to include html file + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(ts.projectSystem.configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Open HTML file + projectService.applyChangesInOpenFiles(ts.singleIterator({ + fileName: file2.path, + hasMixedContent: true, + scriptKind: ts.ScriptKind.JS, + content: `var hello = "hello";` + })); + // Now HTML file is included in the project + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Check identifiers defined in HTML content are available in .ts file + const project = ts.projectSystem.configuredProjectAt(projectService, 0); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, ts.emptyOptions); + assert(completions && ts.some(completions.entries, e => e.name === "hello"), `expected entry hello to be in completion list`); + + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ undefined, + /*closedFiles*/[file2.path]); + + // HTML file is still included in project + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + ts.projectSystem.checkProjectActualFiles(ts.projectSystem.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, ts.emptyOptions); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); - // HTML file will not be included in any projects yet - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const configuredProj = ts.projectSystem.configuredProjectAt(projectService, 0); - ts.projectSystem.checkProjectActualFiles(configuredProj, [file1.path, config.path]); + it("no tsconfig script block diagnostic errors", () => { - // Specify .html extension as mixed content - const extraFileExtensions = [{ extension: ".html", scriptKind: ts.ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); + // #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 } }) + }; - // The configured project should now be updated to include html file - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(ts.projectSystem.configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Open HTML file - projectService.applyChangesInOpenFiles(ts.singleIterator({ - fileName: file2.path, - hasMixedContent: true, - scriptKind: ts.ScriptKind.JS, - content: `var hello = "hello";` - })); - // Now HTML file is included in the project - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + let host = ts.projectSystem.createServerHost([file1, file2, config1, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); + let session = ts.projectSystem.createSession(host); - // Check identifiers defined in HTML content are available in .ts file - const project = ts.projectSystem.configuredProjectAt(projectService, 0); - let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, ts.emptyOptions); - assert(completions && ts.some(completions.entries, e => e.name === "hello"), `expected entry hello to be in completion list`); + // Specify .html extension as mixed content in a configure host request + const extraFileExtensions = [{ extension: ".html", scriptKind: ts.ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); - // Close HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/ undefined, - /*changedFiles*/ undefined, - /*closedFiles*/[file2.path]); + ts.projectSystem.openFilesForSession([file1], session); + let projectService = session.getProjectService(); - // HTML file is still included in project - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - ts.projectSystem.checkProjectActualFiles(ts.projectSystem.configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + let diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); - // Check identifiers defined in HTML content are not available in .ts file - completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, ts.emptyOptions); - assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); - }); + // #2. Ensure no errors when allowJs is false + const config2 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: false } }) + }; - it("no tsconfig script block diagnostic errors", () => { + host = ts.projectSystem.createServerHost([file1, file2, config2, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); + session = ts.projectSystem.createSession(host); - // #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 } }) - }; + session.executeCommand(configureHostRequest); - let host = ts.projectSystem.createServerHost([file1, file2, config1, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); - let session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + projectService = session.getProjectService(); - // Specify .html extension as mixed content in a configure host request - const extraFileExtensions = [{ extension: ".html", scriptKind: ts.ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = ts.projectSystem.makeSessionRequest(ts.projectSystem.CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); - ts.projectSystem.openFilesForSession([file1], session); - let projectService = session.getProjectService(); + // #3. Ensure no errors when compiler options aren't specified + const config3 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - let diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); + host = ts.projectSystem.createServerHost([file1, file2, config3, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); + session = ts.projectSystem.createSession(host); - // #2. Ensure no errors when allowJs is false - const config2 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: false } }) - }; + session.executeCommand(configureHostRequest); - host = ts.projectSystem.createServerHost([file1, file2, config2, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); - session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + projectService = session.getProjectService(); - session.executeCommand(configureHostRequest); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); - ts.projectSystem.openFilesForSession([file1], session); - projectService = session.getProjectService(); + // #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] }) + }; - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); + host = ts.projectSystem.createServerHost([file1, file2, config4, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); + session = ts.projectSystem.createSession(host); - // #3. Ensure no errors when compiler options aren't specified - const config3 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({}) - }; + session.executeCommand(configureHostRequest); - host = ts.projectSystem.createServerHost([file1, file2, config3, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); - session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + projectService = session.getProjectService(); - session.executeCommand(configureHostRequest); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); - ts.projectSystem.openFilesForSession([file1], session); - projectService = session.getProjectService(); + // #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] }) + }; - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); + host = ts.projectSystem.createServerHost([file1, file2, config5, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); + session = ts.projectSystem.createSession(host); - // #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] }) - }; + session.executeCommand(configureHostRequest); - host = ts.projectSystem.createServerHost([file1, file2, config4, ts.projectSystem.libFile], { executingFilePath: ts.combinePaths(ts.getDirectoryPath(ts.projectSystem.libFile.path), "tsc.js") }); - session = ts.projectSystem.createSession(host); + ts.projectSystem.openFilesForSession([file1], session); + projectService = session.getProjectService(); - session.executeCommand(configureHostRequest); + ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); - ts.projectSystem.openFilesForSession([file1], session); - projectService = session.getProjectService(); + 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 = ts.projectSystem.createServerHost([file1, file2]); + const projectService = ts.projectSystem.createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ ts.singleIterator({ fileName: file1.path, changes: ts.singleIterator({ span: ts.createTextSpan(0, file1.path.length), newText: "let y = 1" }) }), + /*closedFiles*/ undefined); + + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.ensureInferredProjectsUpToDate_TestOnly(); + ts.projectSystem.checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); - ts.projectSystem.checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = ts.projectSystem.configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); + it("files with mixed content are handled correctly", () => { + const file1 = { + path: "/a/b/f1.html", + content: `